Commit Graph

284 Commits

Author SHA1 Message Date
14b6e95f9d [feat] solution/backend,frontend: 관리자가 모든 사업장을 열고 관리 — 사이트관리 검색·업종 필터·삭제, 사장님 더보기 정리
사업장 API 가 전부 "요청자 = 소유자" 로 걸러져서 관리자가 남의 빌더·미니블로그를 열면
PLACE_NOT_FOUND 였다. 운영 중 사업장 정리도 사장님 계정으로 들어가야 했다.

- validator/dependencies: DEVELOPER 가 place_id 경로를 부르면 그 사업장 소유자 신원으로 처리 — [admin-access] 로그
- ops/sites: search(상호·소유자 이메일·이름·아이디) · category 필터, 건수도 같은 조건
- OpsSitesPage: 검색창 · 업종 칩 · 행마다 빌더·미니블로그·삭제(공개 중이면 발행 내린 뒤 삭제)
- SitesPage: 더보기에서 미니블로그 관리 · 예약요청 관리 제거

테스트 4건 추가(test_admin_access). 전체 900 passed · 145 failed(변경 전과 같은 파일: agent_runtime·gemini 등)
frontend tsc·eslint 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:34:20 +09:00
87343c791b [feat] solution/backend,frontend,site: 수집·사진 분석·발행 진행을 Teams 로 발송 — 수집 중 배포에도 사진 목록 복구
컨테이너를 재생성하면 docker logs 가 사라져 크롤링이 잘 됐는지 확인할 방법이 없었다.
실측(09-30 버터브루): 수집 도중 배포로 API 가 80초 끊기자 화면이 사진 분석 폴링을
3회 실패 후 포기해 사진 10장이 DB 에 있는데도 0장으로 보였다.

- services/activity_feed.py: 기존 alert_outbox·장애 채널로 kind=activity 이벤트 적재(중복 억제 없음)
- collect_service: 수집 시작 · 채널 크롤링 실패 즉시 · 완료 요약(채널별·fact·사진·누락 필수항목·소요초) · 실패
- vision_service: 사진 분석 결과
- build_service · rollback_service: 첫 발행/재발행/빌드만/되돌리기 시작·끝 — URL · 굽기 사진 미러링 수
- site/prerender.ts: 렌더 보고서에 사진 미러링 수(media) 추가
- frontend pollJob: 연속 3회 실패여도 3분간 무응답일 때만 unreachable
- frontend collectJobs: 사진 분석 폴링을 못 끝내도 사진 목록을 다시 읽고 경고
- test_build_publish: 게이트 반려 검사에서 activity 이벤트는 제외

관련 테스트 17개 파일 295 passed · 2 failed(test_search_console_service — 변경 전에도 실패)
frontend tsc·eslint, site tsc·eslint 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:09:52 +09:00
e5153eb66c merge: feat/templates-all-industries(업종 확장 본체) 을 main 에 병합
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:58:29 +09:00
fad20e8958 [feat] solution/site,shared: 새 템플릿 8종 업종 확장 — 앞 커밋에서 빠진 본체
앞 커밋(18a2874)에 템플릿 등록과 레이아웃 변경이 빠지고 다른 작업의 새 파일
(services/activity_feed.py, 어디서도 아직 import 하지 않음)이 대신 들어갔다.
같은 작업 폴더를 다른 세션이 함께 쓰고 있었다.

- templates.json: cafe · restaurant · clinic 에 라운드~솔숲 8종
- layouts/*/Rooms.tsx: 객실별 '예약 요청' 버튼은 숙박일 때만, 영문 라벨은 unitWord
- layouts/*/Frame.tsx · Intro.tsx: 이용안내 머리말 · 미니 블로그 설명을 업종 중립으로

eslint·tsc 통과, vitest 123 passed, 33개 사이트 굽기 성공

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:58:29 +09:00
8219d3f45e merge: feat/templates-all-industries(새 템플릿 전 업종) 을 main 에 병합
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:57:02 +09:00
18a287451e [feat] solution/site,shared: 새 템플릿 8종을 카페·음식점·병원에도 연다 — 객실 문구를 업종별로
새 템플릿을 숙박에만 등록해서 음식점 사장님 화면에는 4종만 보였다.

- templates.json: cafe · restaurant · clinic 에 라운드~솔숲 8종 추가
- kit/unitWord.ts: Room / Menu / Program — 코랄·미니멀·시네마·부티크·빅타이포의 영문 라벨
- 각 Rooms.tsx: 객실별 '예약 요청' 버튼은 숙박일 때만(예약 시트가 숙박에만 있어 누르면 무반응)
- 이용안내 머리말 · 미니 블로그 설명을 업종 중립 문구로, 부티크 탭 'Stay' → 비숙박은 'Guide'
- docs/TEMPLATES.md: 업종별 사용 범위

eslint·tsc 통과, vitest 123 passed, 숙박 9 + 카페·음식점·병원 각 8 = 33개 사이트 굽기 성공 ·
숙박 전용 문구(객실·체크인·Room·예약 요청) 0건 · 간격 검사 0건

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:57:02 +09:00
30f8101988 merge: fix/db-url-log(DB 로그 비밀번호 제거) 을 main 에 병합
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:07:41 +09:00
2985557838 [fix] solution/backend: DB 엔진 생성 로그에서 비밀번호 제거 — 호스트·포트·DB 이름만 남긴다
백엔드가 뜰 때마다 접속 주소 전체(아이디·비밀번호 포함)를 INFO 로그에 찍고 있었다.
컨테이너 로그를 볼 수 있는 사람이면 누구나 운영 DB 비밀번호를 읽을 수 있었다.

- db_session_manager.create_engine: 읽기·쓰기 로그를 host:port/name 으로

py_compile 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:07:40 +09:00
e6a46154b1 merge: feature/template-catalog(숙박 템플릿 8종 · 문서 정리) 을 main 에 병합
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:00:12 +09:00
1dc4f5fbf5 merge: origin/main(에이전트 여러 동작 · 사진 도구 · 카톡 승인 알림) 을 feature/template-catalog 에 병합
충돌은 이 브랜치의 주석 정리 커밋(11d30bb)과 main 의 기능 변경이 겹친 백엔드 9개 파일 —
main 쪽을 그대로 썼다(기능이 main 에만 있다). AGENTS.md 는 양쪽 링크를 모두 두고,
DEVLOG 는 요약본 위에 main 의 09-28 에이전트 항목 셋을 붙였다.

site eslint 통과 · vitest 123 passed · frontend tsc·eslint 통과 · 백엔드 py_compile 통과
(백엔드 pytest 는 이 기계에 테스트 DB 계정이 없어 실행하지 못함)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:51:53 +09:00
4ff92fb0a6 [docs] docs: 문서 정리 — DEVLOG 요약 · 렌더링 흐름 문서 추가 · 삭제한 방향 문서 링크 정리
DEVLOG 가 1,900줄이 넘어 최근에 무엇을 왜 바꿨는지 찾기 어려웠다.

- DEVLOG.md: 개발 이력상 남길 가치가 있는 항목만 요약
- RENDERING.md: 정적 사이트 · 미리보기 · 발행 세 경우의 흐름과 담당 파일
- README · AGENTS · PRODUCT: 삭제한 DEVELOPMENT_DIRECTION.md 링크 정리(삭제 자체는 앞 커밋),
  admin 은 필요한 규모가 되면 개발한다는 안내, 템플릿 문서 링크

문서만 바꿈

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:45:24 +09:00
013cdcfc9f [feat] solution/site,shared: 숙박 템플릿 8종 추가 — 라운드·시네마·빅타이포·부티크·일러스트·코랄·미니멀·솔숲
사장님이 고를 수 있는 숙박 템플릿이 고택 하나뿐이었다. 모두 stay2 콘텐츠(날씨·미니 블로그·
지역 이야기·객실·축제·일정·후기·엽서·노래)를 그대로 싣고 모양만 다르다.

- layouts/<id>/: 템플릿별 Frame·섹션·CSS. 데스크톱은 넓은 배치(1024px 이상), 모바일 우선
- layouts/kit/kit.css: 탭 전환·시트·예약 폼·접기 등 공통 구조
- shared templates.json · builder.ts · catalog.ts: 템플릿·LayoutId 등록
- seo/head.ts: 템플릿별 한글 웹폰트 — 서로 겹치지 않게 배정
- paper Frame·Intro: 같은 탭을 다시 눌러도 맨 위로, 미니 블로그를 글 여러 개로
- docs/TEMPLATES.md · TEMPLATE_DESIGN.md: 목록과 화면 규칙(간격·접기·✓ 표시·글꼴)

eslint·tsc 통과, vitest 123 passed, 템플릿 9개 굽기 성공 · 간격 검사 0건
2026-09-30 13:45:10 +09:00
민헌
273efb717f merge: origin/main(카톡 승인 알림 Event API) 을 fix/agent-multi-action 에 병합
충돌은 tests/test_kakao_webhook.py 하나 — 양쪽이 파일 끝에 서로 다른 테스트를 붙였다.
둘 다 살렸다(여러 가게·확인 경로 테스트 → 승인 알림 테스트 순).
channel.py 는 자동 병합 — 저쪽은 approval_notice·approve_post 추가와 _say 선택 인자 확장이라
이쪽의 _pick_place·확인 경로 변경과 겹치지 않는다.

전체 994 passed / 47 failed — 실패 목록은 병합 전과 동일(gemini·openai 키 미설정, search_console, weather_notes)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:26:28 +09:00
민헌
3408735290 [docs] docs: 사장님 에이전트 사용 안내 추가 — 말로 할 수 있는 일 목록(AGENT_GUIDE.md)
AGENT.md 는 개발자용 설계 문서라 "왜" 를 다루고, 운영자·CS 가 "카톡으로 무엇을 시킬 수 있나"
를 찾을 자리가 없었다. 무엇을 할 수 있는지만 새 파일에 두고, 동작 근거는 AGENT.md 로 링크한다
(한 사실은 한 곳에).

- docs/AGENT_GUIDE.md: 연결·가게 선택 · 업종별 수정 항목 · 가게 정보 · 섹션 조회/켜기·끄기/옮기기 ·
  사진 · 발행 · 여러 요청 · 할 수 없는 것. 기준 커밋(091d5d9)과 항목 목록의 원본(category_schema) 표기
- docs/AGENT.md: 머리에 사용 안내 링크
- AGENTS.md: 문서 표에 한 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:00:27 +09:00
민헌
091d5d9c0e [fix] solution/backend: 에이전트 재검증 결함 정리 — 옮기기 합치기·모르는 방향·낮/밤 시각·만원·첫 번째
추가한 코드를 다시 읽다가 테스트가 못 잡던 여섯 가지가 나왔다. 둘은 조용히 반대로 동작했다 —
"소개 앞쪽으로" 가 소개 뒤로 갔고, "낮 3시" 가 03:00 으로 저장됐다.

- tools: move_section 의 target 제거 — 옮기기는 차례가 뜻이라 "맨 위로, 그리고 한 칸 아래로" 를
  합치면 두 번째가 아니라 원래 자리에서 한 칸 아래가 됐다. 인자까지 같은 것만 합친다
- tools: 못 알아들은 where 는 되묻는다 — 예전 표기(to 만)는 where 가 비었을 때만 읽는다
- tools: 낮은 오후, 밤·새벽 12시는 자정 — '낮 3시' 03:00 · '밤 12시' 12:00 이던 것
- tools: '만원'·'천원'(앞 숫자 없음)을 1로 읽는다. '만 오천원' 은 여전히 되묻는다
- tools: '첫 번째' 순번, toggle 이름 구분에서 '·' 제외('공간 · 좌석 안내' 가 쪼개질 수 있었다)
- runtime: skipped 이름의 줄바꿈·연속 공백을 한 칸으로
- docs/AGENT.md: 값 형식 표 · 옮기기는 합치지 않음 · 모르는 방향은 되묻기

테스트 12건 추가, 에이전트·카카오 186 passed. 전체 965 passed / 47 failed —
실패 목록은 변경 전과 동일(gemini·openai 키 미설정, search_console, weather_notes). pyflakes 새 경고 없음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:42:43 +09:00
e0155d0423 [feat] solution/backend: 카톡 승인 알림에 [승인] 버튼 — 연결된 계정이 권한 (Event API 2-2)
2-1 로 카톡에 글과 [수정하기] 가 오게 됐고, 이제 카톡 안에서 바로 올린다.
승인 권한은 링크가 아니라 연결된 계정이다 — 버튼이 들고 온 글 ID 는 믿지 않는다.

- kakao_bot: 알림 카드에 [승인](action: block, blockId=KAKAO_APPROVE_BLOCK_ID,
  extra={kind:approve, post_id}) 을 그리고, 클릭은 action.clientExtra 로 받는다.
  블록 ID 가 비면 버튼을 그리지 않는다(눌러도 안 되는 버튼을 보내지 않는다)
- channel.approve_post: 누른 발화자 → 사장님 → 그 가게의 미처리·기한 전 글인지 재확인 후
  approve_by_owner(메일·'바로 발행' 과 같은 경로라 재발행 잡·쓰레드 공유까지 동일).
  연결 안 됨·남의 글·이미 올림·기한 지남은 같은 안내로 끝나고 두 번 올라가지 않는다.
  답장은 "올렸습니다" + [사이트 보기](#blog). LLM 을 부르지 않는다
- 5초를 넘겨도 승인 작업은 취소하지 않는다(asyncio.shield) — 여러 번 커밋하는 작업이라
  중간에 끊기면 승인만 되고 재발행이 안 걸린 글이 남는다
- PostService._blog_url → blog_url(채널이 [사이트 보기] 에 쓴다)

test_kakao_webhook 10건 추가(구현 전 5건 실패 확인), 관련 11개 스위트 239 passed.
실제 클릭 본문(action.clientExtra 위치)은 첫 클릭의 로그로 확인한다.
2026-09-29 16:50:40 +09:00
f1b473968d [feat] solution/backend: 승인 알림을 카톡으로도 발송하고 웹훅이 메시지를 그린다 (Event API 2-1)
결정(2026-09-29): 카톡과 메일 둘 다, 승인은 링크가 아니라 연결된 계정 신원으로.
이 커밋은 발송과 메시지 그리기까지고 [승인] 버튼은 다음 단계(2-2)다.

- blog_jobs._send_one: 메일에 더해 Event API 로 보낸다. 하나라도 나가면 SENT,
  아무 데도 안 나가면 SENT 로 표시하지 않아 다음 스윕이 다시 시도한다. 카톡은 채널
  친구가 아니거나 차단했으면 실패하므로 메일을 빼지 않는다. send_now 도 같은 경로
- 카톡 params 로 post_id 와 수정용 일회용 코드(edit_token)를 넘긴다. 코드 평문은
  발송 시점에만 알아서다. 로그에는 params 의 키만 남기고 값은 남기지 않는다
- channel.approval_notice: 발화자 키 → 사장님 → 그 글이 그 사장님 가게 것·미처리·
  기한 전일 때만 본문과 [수정하기] 를 준다. 연결 안 됨·남의 글·처리됨·만료·이상한
  ID 는 구분 없이 같은 안내(구분해 주면 글 ID 를 탐색할 수 있다)
- kakao_bot: userRequest.params.post_id 가 있으면 승인 알림 요청으로 처리하고 링크
  버튼은 본문과 따로 textCard 로 그린다(카드 설명 길이 제한을 피한다)
- KAKAO_APPROVAL_PUSH_ENABLED(기본 0), KAKAO_APPROVAL_EVENT_NAME 추가 —
  오픈빌더 이벤트 블록(스킬 연결)과 배포가 끝나기 전에는 켜지 않는다

test_blog_owner 6건·test_kakao_webhook 6건 추가, 카카오·미니블로그 스위트 119 passed,
인접 스위트 110 passed. 실제 카톡 수신은 콘솔 설정·운영 배포 뒤에 확인한다.
2026-09-29 16:13:07 +09:00
민헌
36b818eda3 [feat] solution/backend: 대화로 섹션 옮기기·숨기기 확장 — 앞·한 칸·N번째·자리 바꾸기 · 여러 개 끄기 · 꺼진 목록
"맨 위 · 맨 아래 · X 다음으로" 만 되고 "X 앞으로" "한 칸 위로" "세 번째로" "자리 바꿔줘" 는
거절됐다. 발행본(HomePage.tsx)은 히어로를 늘 맨 위, SNS 를 늘 맨 아래에 그리는데 대화는
그 둘을 옮기고 "옮겼습니다" 라고 답했다 — 화면은 그대로였다.

- tools: move_section 에 where(맨 위·맨 아래·앞·뒤·위로·아래로·번째·바꾸기)·count —
  where 가 비면 예전 to 표기로 읽는다. 한 칸·N번째는 보이는 순서로 센다(꺼진 부분과 자리만
  바꾸는 헛이동 방지). 없는 순번·꺼진 부분의 칸 이동은 거절, 이미 그 자리면 Unchanged
- tools: PINNED(히어로 맨 위 · SNS 맨 아래) — 옮기기와 그 둘을 기준으로 한 앞·뒤·바꾸기를 막는다.
  히어로 다음은 맨 위, SNS 앞은 맨 아래로 읽는다
- tools: toggle_section 이 쉼표로 여럿을 받는다 — 하나라도 못 찾거나 잠겼으면 아무것도 안 바꾼다
- tools: list_sections 는 보이는 순서에 번호(= N번째 기준)를 붙이고 꺼진 것을 모은다, only=꺼진
- prompts/runtime: 스키마에 where·count·only, 섹션 줄에 [항상 맨 위]·[항상 맨 아래] —
  목록은 tools.PINNED 하나를 runtime 이 넘긴다(prompts 는 services 를 import 하지 않는다)
- docs/AGENT.md: 옮기기·숨기기 표와 근거. 동작하지 않던 예시("후기 빼줘") 교체 —
  이용 후기는 섹션 목록에 없고 발행본이 늘 그린다

테스트 21건 추가, 에이전트·카카오 174 passed. pyflakes 새 경고 없음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:49:20 +09:00
983c05b3c3 [feat] solution/backend: 카카오 챗봇 Event API 클라이언트와 테스트 발송 스크립트 (승인 알림 카톡 발송 1단계)
미니블로그 승인 알림을 메일에 더해 연결된 카카오톡으로도 보내기 위한 첫 단계.
승인 방식은 인라인 버튼 + 연결된 계정 신원 확인으로 정했고(링크가 없어 메신저
미리보기가 먼저 열어 승인되는 문제가 없다), 이 커밋은 발송 통로와 규격 확인까지다.

- services/external/kakao_event.py: POST bot-api.kakao.com/v2/bots/{botId}/talk.
  예외 문구(str)에는 키·발화자 ID·응답 원문을 넣지 않고 진단용은 detail 에만 둔다
- KAKAO_BOT_REST_API_KEY 를 기존 KAKAO_REST_API_KEY(카카오 로컬 API)와 갈랐다 —
  Event API 는 채널을 연결한 비즈니스 인증 앱의 키를 써야 해서 앱이 다를 수 있다
- KAKAO_EVENT_DEV=1 이면 봇 ID 뒤에 "!"(개발 채널). KAKAO_BOT_ID 자체는 웹훅이
  bot.id 대조에 쓰므로 고쳐 쓰지 않는다
- scripts/kakao_event_send_test.py: 실제 카톡으로 한 건 보내 규격을 확인하는 스크립트

실제 발송으로 확인함(요청 성공 + 카톡 수신). 이벤트 미배포 시 "Invalid Event name" 404 를
돌려주는 것도 확인했다.

test_kakao_event.py 7건, 카카오·설정 관련 스위트 75 passed
2026-09-29 15:25:06 +09:00
민헌
4b48be7e81 [chore] solution/backend: 에이전트 테스트의 죽은 도우미 user_of 삭제 — 없는 함수를 import
user_of 는 처음 들어온 커밋(b1a34ba)부터 한 번도 호출되지 않았고, import 하는
decode_access_token 은 어느 브랜치 기록에도 정의된 적이 없다 — 부르는 순간 ImportError 다.
같은 일은 DecodeAccessToken 을 쓰는 _sections 가 이미 한다.

- tests/test_agent_runtime.py: user_of 삭제(호출처 0 — 레포 전체·문자열 참조까지 확인)

pyflakes 경고 없음, test_agent_runtime 84 passed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:13:44 +09:00
민헌
aaf97f2797 [fix] solution/backend: 대화 수정의 값 형식 검증 · 여러 요청의 못 한 것·남은 것 안내 — 조용히 틀리던 경로 정리
말로 고친 값이 형식 검증 없이 저장됐다 — "반려동물 이제 돼요" 가 "가능" 으로 들어가면
화면엔 "가능" 이 뜨는데 factBool 은 'true' 만 참으로 읽어 구조화 데이터가 거짓이 된다.
여러 요청을 한 번에 받을 때도 할 수 없는 것·멈춘 뒤의 것이 말없이 사라져 사장님은
전부 된 줄 알았다(message 는 actions 가 있으면 버려졌다).

- tools: set_fact 값을 스키마 형식으로 맞춘다(bool true/false · time HH:MM · number 숫자) —
  "3시" 처럼 오전·오후를 모르면 저장하지 않고 되묻는다. 알림 문구는 가능·불가로 말한다
- tools: enabled 를 모르면 끄지 않고 되묻는다 — "" 를 끄기로 읽어 "다시 보여줘" 가 섹션을 껐다
- tools: 대표 지정·사진 목록·프롬프트를 '나가는 사진' 기준으로 — 내린 사진을 대표로 지정하고
  "바꿨습니다" 라고 하던 것. 정확히 맞는 이름을 부분 일치보다 먼저 고른다
- tools: Unchanged 표시 — "이미 켜져 있어요" 에 재발행 안내·카톡 발행 대기가 붙던 것
- tools/runtime: 인자가 문자열·dict 가 아니어도 죽지 않는다(모델의 스키마 위반)
- runtime: 같은 대상은 마지막 하나로 합친다(Tool.target, 상한 세기 전). 멈춘 뒤 남은 요청과
  지어낸 도구를 코드가 만든 이름으로 알린다. 발행 확인 문구는 맨 끝에 선다
- prompts: 항목마다 형식을 싣고 skipped(할 수 없는 요청의 이름) 칸 추가 — 문장은 런타임이 만든다.
  다른 가게 이야기면 되묻는 규칙
- channel: 발화에 다른 내 가게 이름이 나오면 모델을 부르기 전에 고르게 한다. 기억한 가게가
  목록에 없으면 비우고 목록으로. 확인 경로의 AgentError 를 사장님 말로 옮긴다
- docs/AGENT.md: 값 형식 · 못 한 것·남은 것·겹친 것 · 나가는 사진 기준

테스트 48건 추가, 에이전트·카카오 153 passed. 전체 932 passed / 47 failed —
실패 47건은 변경 전과 동일(gemini·openai 키 미설정, search_console KeyError, weather_notes)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:11:00 +09:00
86d870bab5 [feat] solution/frontend: 랜딩 하단에 사업자 정보 표기 — showBusinessInfo 로 랜딩에만 2026-09-29 13:33:40 +09:00
7f6f16f307 [fix] solution/site: 고택 — 날씨를 첫 화면 바로 아래로 · 예약 요청 창은 달력부터 펼친다
섹션 순서가 업종 기본(날씨 맨 뒤)을 따라가서 stay2 와 달리 날씨가 맨 아래로 갔고,
예약 요청 창은 '날짜 · 시간 선택' 을 접어 두어 손님이 한 번 더 눌러야 달력이 보였다.

- paper/Hero: 날씨 섹션이 켜져 있으면 첫 화면 바로 뒤에 그린다(레이아웃의 weather 자리는 비움)
- paper/BookingForm: 날짜·시간 칸을 처음부터 펼친다
- app.test: 날씨를 섹션 목록 맨 뒤에 둬도 첫 화면 바로 다음에 나오는지

site tsc·eslint·vitest 107 passed, 로컬 발행(stayyyy v3)에서 순서와 예약 창 확인

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:05:39 +09:00
c4aa1794e2 [feat] solution/backend: 한 발화에 여러 가지 — 응답을 actions 배열로
사장님은 "체크인 3시로 바꾸고 후기도 빼줘" 처럼 한 번에 시킨다.

- prompts/agent: 응답 스키마를 actions 배열로
- runtime: 시킨 순서대로 실행. MAX_ACTIONS=5 — 무한정이면 "다 지워줘" 한 마디에
  연쇄 실행된다
- ★ publish(SEMI)가 섞이면 그 앞까지만 하고 확인을 받는다. 확인이 필요한 행위를
  다른 일에 묻어 실행하면 확인의 의미가 없다
- ★ 중간에 실패해도 앞의 것을 되돌리지 않는다(사장님 결정). 되돌리는 것도 시키지
  않은 변경이다 — 대신 무엇이 됐고 무엇이 안 됐는지 그대로 말한다
- Tool.republish 플래그로 재발행 안내를 런타임이 한 번만 붙인다. 도구 문장에
  박아 두면 셋을 고쳤을 때 같은 말이 세 번 나왔다

실모델 4/4 정확히 쪼갬(1.8~2.8초).
test_agent_runtime·test_kakao_webhook 63 passed.
전체 878 passed / 53 failed — 53 은 기존과 동일. npm run lint 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 17:00:30 +09:00
0a0ae4e054 [fix] solution/site: 첫 화면 문구가 따로 있으면 한 줄 요약이 화면에 없어 발행이 막히던 것
첫 화면은 tagline → heroSubline → summary 중 하나만 쓰고, 이용안내는 intro fact 를 먼저 쓴다.
둘 다 있으면 JSON-LD description(summary)이 화면에 없어 구조화 데이터 대조에서 굽기가 실패했다.

- AboutSection: 첫 화면이 다른 문구를 쓸 때만 제목 아래에 summary 한 줄
- app.test: tagline·intro fact·summary 가 다 있는 payload 로 simple·paper 모두 화면에 summary 가 있는지

재현 테스트 실패 확인 후 수정, site vitest 106 passed, 머뭄 payload 심플 굽기 통과

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:10:37 +09:00
7941e5a9f6 [feat] solution/site: 고택 노래 목록 상자를 종이 톤으로 — 가는 선 · 명조 제목 · 흙색 판
stay2 는 머리글 아래로 펼치는 목록이지만 떠 있는 상자가 쓰기 낫다(대표 결정). 모양만 고택에 맞춘다.

- paper.css: #w4d-panel 배경 --paper · 1px --line · 4px 모서리 · 옅은 그림자
- 줄 구분 실선, 지금 곡은 --soft 바탕, 판 라벨·이퀄라이저 #8c4a2f, 가사 명조

site vitest 105 passed, 390px 에서 목록을 펼쳐 확인

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:06:44 +09:00
456a7c288b [docs] docs: DEVLOG 중복 항목 제거 — 09-23·09-28 이 두 번 들어가 있었다
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:05:38 +09:00
765b9b6463 [feat] solution/site: 고택(paper) 레이아웃을 /s/stay2 시안과 같게 — 탭 4개 · stay2 마크업 이식
고택이 stay2 DOM 이식 1차에 머물러 화면 전체 폭·다른 머리글로 나가 시안과 달랐다.

- layouts/paper/paper.css: stay2 CSS 를 .w4p 안으로 옮겼다(@layer components, Tailwind 유틸이 이긴다)
- layouts/paper: 머리글·탭·첫 화면·날씨·블로그·소개·객실·사진·영상·주변·예약·이용안내·오시는 길·FAQ·축제·일정·지역 이야기·예약 요청 창
- stay2 하위 페이지(지역 소개·이용안내·이야기)는 #/area · #/stay · #/story 탭 전환, 내용은 한 HTML 에 둔다
- 공용 섹션에서 계산 함수만 꺼내 쓴다: infoView · useBookingRequest · StayBookingDemo 달력 함수
- SectionList: 레이아웃이 blog · reviews · postcardMaker 자리를 바꿀 수 있다
- templates.json paper: 보조색 #5b564e · 본문 고딕 · 모서리 4px · 간격 2.5rem(stay2 값)
- app.test: 고택 Frame 표식을 새 클래스(w4p)로

머뭄 데이터로 구워 1280·390px 에서 stay2 와 나란히 비교(첫 페이지 4855/4857px).
site tsc·eslint·vitest 105 passed, frontend vite build 통과

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:05:21 +09:00
11d30bb3d1 [chore] solution,admin,ontology: 코드 주석을 한 줄로 — 히스토리 주석 삭제
여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다.

- ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제
- 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다
- eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다

파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이).
site·frontend·admin tsc, site vitest 105 passed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:05:19 +09:00
c6908629f3 [feat] solution: 템플릿 정의를 templates.json 한 파일로 — 업종 없는 id · 레이아웃 basic/paper 정리
템플릿 정보가 빌더·렌더러·백엔드에 따로 적혀 어긋나 있었다 — 백엔드 기본값이 없는 id 를
가리켰고, 음식점 강조색 오타, 섹션 간격이 빌더와 서버에서 달랐다.

- shared/src/data/templates.json: 템플릿 4개(simple·magazine·retro·paper)와 업종별 허용·기본
- shared/lib/catalog.ts · backend/common/template_catalog.py: 같은 JSON 을 읽는다, 모르는 id 는 에러
- 저장·미리보기·발행에서 모르는 id 를 거절한다(site_service · site.py 422 · build_service 실패)
- site: 레이아웃 등록표(basic·paper) + LayoutProvider, Shell→Frame, HomePage→SectionList
- 연결 안 된 레이아웃 5개, 배치 고르기(variant), 서체 선택, 빌더 canvas/DevShowcase 삭제
- 빌더: 템플릿을 바꾸면 이전 템플릿이 켠 섹션을 끄고 안내 문구를 띄운다
- postgres-init/migrations/0023: stay-retro → retro, 병원 허용 밖은 NULL(운영 미적용)
- Dockerfile·worker: 백엔드 이미지에 shared/src/data 복사
- docs: TEMPLATES.md 신설(세 폴더 역할·템플릿 추가·렌더링 순서), DATA_MODEL·ARCHITECTURE 등 갱신

shared·site·frontend tsc 통과, site vitest 통과, 백엔드 DB 없는 테스트 41 passed(DB 테스트 미실행)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 15:57:35 +09:00
46b74b310b [feat] solution/backend,frontend: 개발자 전용 사이트관리·유저관리 — 조회 전용 경량 화면
admin 앱을 새 메뉴로 키우려면 새 도메인이 필요해서, solution 앱에 DEVELOPER 게이트 하나로 얹었다.
메뉴 문자열은 사장님 번들에도 실리지만 데이터 접근은 백엔드 게이트가 막는다.

- router/v1/ops: GET /v1/ops/sites · /v1/ops/users (RequireDeveloper)
- services/ops_service.py, crud/site_crud.list_all_sites, crud/user_crud.list_users
- frontend: OpsSitesPage · OpsUsersPage, AppShell 에 role===DEVELOPER 일 때만 두 줄
- api/generated: orval 코드젠 결과

프론트 tsc·eslint 통과, openapi 코드젠 성공. DB 조회는 미검증(DEVLOG 2026-09-23)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 15:53:02 +09:00
47bde7388e [fix] solution/backend: 새 도구의 인자가 모델에 닿지 않던 것 — 응답 스키마 칸 부족
실배포 확인에서 잡았다(킹서버, 2026-09-28). 도구 선택은 6/6 정확했는데 인자가
엉뚱하게 왔다:
  move_section       기대 {name,to}  → 실제 {key,value}
  hide_photo         기대 {name}     → 실제 {key}
  set_primary_photo  기대 {name}     → 실제 {key}

응답 스키마의 args 가 {key,value,keyword} 로 고정돼 있어 모델이 name·to·enabled 를
넣을 자리가 없었다. 이대로면 새 도구 다섯이 전부 "못 찾았어요" 로 끝난다.

- prompts/agent: args 에 name·to·enabled 추가(strict 라 required 도 같이)
- ★ test_도구가_선언한_인자는_응답_스키마에_있다 추가 — 다른 테스트는 _choose 를
  monkeypatch 해서 이 층을 건너뛴다. 그래서 단위 테스트가 전부 초록인데도 실제로는
  안 됐다. 소스로 대조해 같은 일이 다시 나지 않게 한다

test_agent_runtime 33 passed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 15:33:55 +09:00
af6e3799eb [feat] solution/backend: 에이전트가 사진을 내리고 대표를 지정한다
사진 쪽은 MediaService 에 list_media 하나뿐이었다 — 쓰기 경로가 아예 없었고
빌더 화면에서도 보기만 됐다. 서비스·라우터부터 열고 도구를 붙였다.

- crud: set_sort_order / service: hide_media · set_primary
- POST .../media/{id}/hide · /primary — ★ 에이전트 전용 뒷문을 만들지 않는다.
  그러면 빌더 화면이 그 기능을 못 쓰고 나중에 붙일 때 로직이 두 벌이 된다
- 도구 셋: list_photos(READ) · hide_photo · set_primary_photo(REVERSIBLE)

★ 대표 사진에 별도 칸을 두지 않았다. primary_media 가 '첫 장' 을 쓰고 목록이
ORDER BY sort_order 라, 지정은 sort_order 를 가장 작게 내리는 일이다 —
칸을 따로 두면 규칙이 둘이 되어 검색 결과의 그림과 화면 첫 장이 갈린다.

★ 내려도 지우지 않는다(REJECTED). origin_url·source_type 이 남아야 재게시
권리(DECISIONS 1-2) 결론이 났을 때 되짚을 수 있다.

★★ 업로드·교체는 만들지 않았다 — 미결 사항을 코드가 먼저 푸는 자리다.
테스트가 레지스트리에 upload·replace 가 없는지 실제로 검사한다.

test_agent_runtime 32 passed(사진 6건 추가).
전체 871 passed / 53 failed — 53 은 이번 변경 전과 동일. npm run lint 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 14:16:08 +09:00
ea9ae1ac2a [feat] solution/backend: 에이전트가 페이지 구성을 바꾼다 — 섹션 on/off·순서
"문구 변경밖에 안 된다" 는 지적에서 시작했다. 페이지 구성은 sites.theme.sections
배열 하나이고 배열 순서가 곧 발행본의 순서라, 그 JSON 을 만지는 도구 셋을 붙였다.

- list_sections(READ) · toggle_section(REVERSIBLE) · move_section(REVERSIBLE)
- 목록은 site_payload._sections 를 그대로 쓴다 — 발행본이 쓰는 그 함수다.
  표를 따로 만들면 에디터·발행본·대화 셋이 갈라지고 "껐는데 나온다" 가 된다
- 잠긴 섹션은 못 끈다. _sections 가 어차피 켜서 내보내므로 끌 수 있게 두면
  화면만 거짓말한다
- 이름이 둘 이상 걸리면 고르지 않는다 — 추측으로 고르면 발행하고 나서야 안다
- sections 만 갈아끼운다 — theme 을 통째로 쓰면 고른 색·서체가 말없이 사라진다

★ 템플릿·색은 넣지 않았다. templatesFor() 가 색·look·기본 섹션·배리에이션을 함께
계산해서, 백엔드가 template_id 만 바꾸면 "레이아웃은 새것, 색은 옛것" 이 된다 —
레지스트리를 공유 단일 출처로 옮기는 작업이 먼저다(docs/AGENT.md).

test_agent_runtime 26 passed(구성 7건 추가).
전체 864 passed / 53 failed — 53 은 이번 변경 전과 동일. npm run lint 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 11:50:38 +09:00
e35ce311d3 [fix] solution/backend,frontend: 메일 '고쳐서 올리려면' 링크를 일회용 코드로
주소가 500자였던 것은 곁가지고, 진짜 문제는 그 auto= 가 빌더 액세스 토큰
통짜(sub 에 UserInfo 전체 — role 포함)였다는 것이다. 메일 전달 한 번이 그날
자정까지의 권한 양도였고, 브라우저 히스토리·프록시 로그·Referer 에도 남았다.
SNS 승인 흐름에서는 같은 이유로 "기존 액세스 토큰을 승인 링크에 얹지 않는다" 를
원칙으로 박아 뒀는데 이 경로에만 남아 있었다.

- 0023: place_posts.edit_token_hash. 승인 토큰과 같은 규약(평문은 메일에만)
- GET /v1/site/post/edit?t=<코드> → 검증 후 day-pass 를 그 자리에서 만들어
  /blog?...#auto=<JWT> 로 303. ★ 프래그먼트는 서버 로그·Referer 에 안 남는다
- 프론트는 hash 에서 읽고 **주소창에서 지운다**. 쿼리도 계속 받는다 —
  이미 나간 메일이 자정까지 살아 있고 그걸 깨면 그 링크들이 통째로 죽는다
- 두 코드는 서로 다른 칸에 산다. 하나로 둘 다 되면 일회성이 무의미해진다
- blog_service.app_origin() 으로 오리진 계산을 옮겼다 — 라우터도 같은 값을 쓴다

링크 길이 약 500자 → 약 75자.

test_blog_owner 45 passed (신규 6건: 길이·프래그먼트·소유자·만료·없는 코드·
승인 코드 교차 사용). test_upcoming_only_returns_next_week_in_date_order 1건은
기준(stash)에서도 동일하게 실패하는 기존 건. npm run lint 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 09:00:02 +09:00
79962b93e2 Merge branch 'feature/site-features-and-mockup' into main 2026-09-23 13:19:19 +09:00
5e2def200b [chore] site/mockup: 번들 갱신·문서 정리 · 2안(사진) 빌더 스크립트 추가
- vendor/ 옛 해시 번들을 retired/ 로 옮기고 새 해시로 교체
- build_photo6.py 신설 — /s/stay6 "2안(사진)" 판, 슬러그별로 다시 구울 수 있다
  (python3 build_photo6.py <slug>)
- build_pension.py·build_reading.py·build_stay6.py·patch_stay.py 정리
- AUTOPLAY.md·README.md·audit-all.mjs 갱신
2026-09-23 13:18:51 +09:00
a74f7918c6 [feat] solution/site: "고택" 템플릿 레이아웃 신설 — stay2 DOM 이식 1차
기존 template-look 시스템(색·서체 토큰)만으로는 stay2 실물과 구조 자체가 달랐다.
reservation/oasi/studio/pastel/editorial 다섯 레이아웃과 같은 자리(Shell·Hero·Rooms·
SectionHead)에 paper 를 여섯 번째로 추가 — layoutOf(templateId) 로 갈아 끼운다.

- layouts/paper/{Shell,Hero,Rooms,SectionHead}.tsx 신설
  Shell: 세리프 워드마크+미니플레이어(SongPlayer)+전화, 앵커 탭 헤더, 얇은 텍스트 푸터
  Hero: 풀블리드 회전 캐러셀(HeroPension 뼈대) + 왼쪽 정렬 카피, 가격띠 없음
  Rooms: 유닛마다 전체폭 판 + 캐러셀(카드 격자 아님)
- lib/layout.ts: LayoutId 에 'paper', stay/restaurant/cafe-paper → paper 매핑
- App.tsx·HeroSection.tsx·UnitsSection.tsx·lib/ui/Section.tsx: 기존 5분기에 paper 추가

★ 실물 대조 결과 아직 stay2 와 다르다(사장님 지적) — 탭 개수(4개 vs 지금 섹션 수만큼),
  히어로가 여백 있는 박스가 아니라 풀블리드, 객실 카드가 2열 나란히가 아니라 전체폭
  세로 배치, 영문 눈썹 라벨 없음. 다음 커밋에서 이어서 맞춘다.

★ 배포 함정 실측: prerender.ts 의 "성공한 버전은 불변" 규칙 때문에, 같은 site_version 은
  렌더러 코드를 바꿔도 재빌드 때 옛 HTML 을 그대로 재사용한다 — 버전 캐시를 지워야
  강제로 다시 구워진다(운영 영향은 별도 확인 필요).
2026-09-23 13:18:21 +09:00
b7b8cb856c [fix] solution/site: 흐린 보조 글자 대비 올림 · 예약 달력 기본으로 펼침
여러 섹션의 opacity-55~70 짜리 보조 텍스트가 밝은 배경에서 너무 흐렸다 — 85~100 으로
올림(Carousel 카운터·SectionHead 리드문·SNS 소식·주변 안내 거리·공식 채널 링크).
StayBookingDemo: "날짜·시간 선택" 토글을 없애고 달력을 기본으로 펼쳐 보여준다 —
접어 두면 손님이 그 버튼을 눌러야만 날짜를 볼 수 있었다.
2026-09-23 13:18:02 +09:00
362c76d6c9 [fix] solution/frontend: 뒤로가기 후 재검색이 옛 사업장을 되살림 — placeId URL 우선순위 정정
주소창의 placeId 가 store 보다 우선이라(BuilderPage.tsx), 뒤로가기로 검색 단계에
돌아와도 그 placeId 가 그대로 남아 있으면 usePlaceSync 가 옛 사업장을 다시 확정
정보로 채워 넣었다 — 다른 상호로 재검색해도 이전 확정 화면이 그대로 떴다.

- BuilderPage.tsx: step === 'search' 일 땐 주소창 placeId 를 안 쓴다
- Step2PlaceSearch.tsx: 뒤로가기 가드가 store 뿐 아니라 주소창 placeId·flow 도 지운다

[feat] solution/frontend: "고택" 템플릿 — 숙박·요식업·카페 공용, stay2 목업 색·서체 이식

industryData.ts 에 LOOK.paper(크림 종이·가는 명조·그림자 없음, stay2 실물에서 추출)와
paperTemplate() 을 추가해 stay·restaurant·cafe 세 업종의 네 번째 템플릿으로 등록.
DOM 구조 이식은 solution/site 커밋에서 별도로 한다.
2026-09-23 13:17:39 +09:00
951e451ef8 [fix] solution/backend: 네이버 플레이스 수집이 음식점·카페 요금표를 통째로 반려 — 업종별 unit key 분기
_to_unit_facts() 가 업종을 안 가리고 room_type·weekday_price/weekend_price 로만 냈다.
그 key 는 숙박 스키마에만 있어서, 음식점·카페·클리닉은 메뉴/프로그램 요금표가
FACT_INVALID_KEY 로 전량 거부됐다(실측: "도플로" fact 19건 중 19건 반려).

- naver_place_adapter.py: _UNIT_NAME_KEY·_UNIT_PRICE_KEY 로 업종별 key 매핑
  (숙박 room_type/weekday·weekend_price, 카페·음식점 menu_name/menu_price,
  클리닉 program_name/price_adult)
- SourceAdapter.fetch() 계약에 category 파라미터 추가, 어댑터 5개 시그니처 반영
- collect_service.py: fetch_one() 에 place.category 전달

검증: 재수집 후 stored 0 → 35(음식점), test_collector·test_category_schema·
test_fact_api·test_tour_api_adapter 173 passed
2026-09-23 13:17:28 +09:00
c366361513 Merge remote-tracking branch 'origin/main' into feature/site-features-and-mockup 2026-09-23 10:17:18 +09:00
0f58e1485a [fix] site/mockup: stay 날씨 문구 교체 — 없는 시설·어색한 표현 제거
대표 지적: "벽난로와 따뜻한 물이 있는 날씨입니다" 처럼 말이 성립하지 않는 문장.
확인해 보니 벽난로는 사진 설명이 전부 "벽난로 조명" 이라 장식이고, 테라스는
발행본 어디에도 근거가 없었다. 쓰지 않는 표현("실내로 도는", "묶어 도는",
"국물이 도는")과 펜션과 무관한 일반 안전 안내도 섞여 있었다.

- local.weather.noteSets 45줄 · tempNoteSets 25줄 전면 교체
- notes · tempNotes(한 줄짜리 기본값)도 같은 묶음 첫 줄로 맞춤
- 근거 없는 시설(벽난로 · 테라스 · 대문 안쪽 우산) 삭제
- 시각을 못 박던 줄("흐릿하게 보이는 아침입니다") 정리 — 문구는 하루 중 아무 때나 뜬다
- .gitignore: backup/ · king-stay2/ · build6p*/ · siann6/ · _sub*.mjs
  (발행본 원본은 도커 볼륨이라 레포에 사본을 두면 원본이 둘이 된다)

검증: 70줄 중복 0 · 쓴 낱말 31개 전부 발행본에 존재 · 크롬 렌더 오류 0
킹서버 반영 완료(/s/stay e1c0d8ff, 이전본 index.html.bak-20260923 보관)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 10:17:10 +09:00
36dbe26c91 [feat] solution/backend: 미니 블로그 글을 배정 날짜에 맞는 소재로 만든다
예전에는 소재 목록을 순서대로 뽑아 날짜에 차례로 붙여서, 9월 날짜에
'한겨울' 글이나 끝난 축제 글이 붙을 수 있었다. 이제 날짜를 먼저 정하고
그 날짜에 맞는 소재를 고른다.

- 축제: 시작 14일 전 ~ 종료일 사이만 / 계절: 게시일 절기 하나
  / 날씨: 그 달에 있을 법한 것만(눈 12~2월, 소나기 6~8월) / 주변: 늘 후보
- 프롬프트에 게시일을 넣고, 날씨를 단정하지 않게 문구를 고쳤다
- 계절·날씨·축제 주제 키에 연(월)을 넣어 해마다 다시 쓸 수 있게 했다
- 자동·구간·개별 세 경로가 _compose_for_dates 하나로 만든다

같이 고친 버그: materials 가 스냅샷에 없는 local.festivals·attractions 를
읽어 축제·주변 소재가 늘 비어 있었다. 원문 행(local.contents)을 읽는다.
2026-09-23 09:41:23 +09:00
be3e43162a [chore] solution/backend: 미니 블로그 새벽 자동 생성 잡을 잠시 끈다
사장님이 빌더에서 '생성'을 눌러야 만들어지는 흐름으로 간다. 등록 두 줄만
주석으로 막고 잡 함수는 그대로 둔다. 09:00 메일 발송 잡은 사장님이 만든
글도 보내야 해서 켜 둔다.
2026-09-23 09:41:23 +09:00
324a329b9e [chore] solution/backend: 카톡 요청에 callbackUrl·블록 이름 로그 — 콜백이 왜 안 먹는지 눈에 보이게
"콜백을 켰는데 그대로" 를 로그 없이 추측으로 좁히고 있었다. 스킬이 폴백이 아닌
다른 블록에 붙어 있으면 그 블록에는 콜백 설정이 없어 조용히 동기로 돈다 —
어느 블록이 도는지까지 같이 남긴다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 17:01:56 +09:00
bbcdf78c62 [fix] solution/backend: 카톡 응답이 5초 벽에 계속 걸리던 것 — 프롬프트 축소
실사용에서 모든 발화가 4.5초 상한에 걸려 "확인하는 데 시간이 조금 걸리네요" 만
반복됐다(킹서버 로그: 4.60s · 4.51s · 4.51s).

- 이미 연결된 사람이 코드를 또 보내면 LLM 을 부르지 않는다. 실제로 그랬고,
  6자리가 그냥 발화로 넘어가 유료 호출 + 대기만 쌓였다
- 사이트 상태를 프롬프트에서 뺀다. 그 한 줄 때문에 매 턴 사이트 조회 + 슬러그
  계산이 돌았고, 정작 필요할 때는 get_site_status 도구를 부르면 된다
- fact 는 key:value 만, 상한 30개. label 은 항목 목록에 이미 있어 두 번 보내면
  프롬프트만 커지고 모델이 얻는 것이 없다
- 항목 목록도 JSON 대신 `key: 이름` 줄로

★ 근본 해결은 콜백이다(f500210). 오픈빌더에서 '콜백 사용' 이 꺼져 있으면
callbackUrl 이 안 와서 조용히 동기 경로로만 돈다 — 지금 로그가 그 상태다.

test_kakao_webhook·test_agent_runtime 43 passed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 16:52:24 +09:00
f500210cd8 [fix] solution/backend: 카톡 5초 벽을 콜백으로 넘는다
실사용 첫날 "시설 편의에서 바비큐 이용 문구 빼줘" 가 타임아웃으로 끝났다.

★ 작은 표본으로 잰 수치를 상한 근거로 삼은 것이 틀렸다. 개발 중 잰 1.3~2.4초는
업종 필드 두 개짜리 장난감 프롬프트였고, 진짜 요청에는 필드 43개 + fact 수십 개가
실린다. "여유가 있다" 고 적어 둔 판단이 하루 만에 깨졌다.

- userRequest.callbackUrl 이 오면 {"useCallback": true} 로 즉답하고 백그라운드에서
  답을 만든 뒤 그 주소로 POST. 콜백 주소는 1분·1회라 재시도하지 않는다 —
  두 번째 POST 는 거절되고 사장님에게는 이미 "확인하고 있어요" 가 가 있다
- 콜백이 꺼져 있으면 예전처럼 동기, 상한만 4.0 → 4.5 (카카오가 5초에 끊는다)

★ 오픈빌더 스킬 설정에서 '콜백 사용' 을 켜야 열린다. 안 켜면 callbackUrl 이 안 와서
조용히 예전 경로로만 돈다.

test_kakao_webhook.py 24 passed(콜백 3건 추가)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 16:33:16 +09:00
1300652b09 [feat] solution/backend: 카톡 대화에 홈페이지 목록·가게 고르기
실제로 붙여 보니 빠진 것이 드러났다 — 연결은 됐는데 **어느 홈페이지를 다루는
대화인지** 말해 주지 않았다. 가게가 하나면 말없이 자동 선택돼 더 모호했다.

- 연결 직후 목록을 보여준다. 하나면 이름+발행 여부를, 여럿이면 바로가기 버튼으로
- 목록 줄에 발행 여부를 적는다 — 안 그러면 고친 것이 손님에게 보이는 줄 안다
- "목록"·"가게 바꿔줘" 로 언제든 돌아와 바꾼다. ★ 이 경로는 LLM 을 부르지 않는다:
  대화가 막혔을 때 처음 찾는 길이라 늘 통해야 하고, 목록에 돈을 쓸 이유가 없다
- 사업장 목록이 아니라 list_my_sites 를 쓴다 — 사장님이 알아야 하는 건
  "가게가 있다" 가 아니라 "발행돼 있나" 다(/sites 화면이 같은 이유로 그걸 쓴다)

test_kakao_webhook.py 21 passed(목록·전환 4건 추가).
전체 845 passed / 53 failed — 53 은 이번 변경 전과 동일

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 16:26:40 +09:00
d32df10cf7 [feat] solution/backend: 카카오 채널 웹훅 — 에이전트 4단계
런타임은 한 줄도 안 바뀌었다. 채널을 모르게 만들어 둔 것이 여기서 값을 했다 —
새로 생긴 것은 형식 변환(kakao_bot)과 대화 상태(channel)뿐이다.

★★ 오픈빌더는 서명을 주지 않는다. URL 만 알면 누구나 때릴 수 있고
userRequest.user.id 를 위조하면 그 사장님 행세를 한다 — 1단계의 신원 연결이
통째로 무의미해지는 자리다. 공유 시크릿(헤더 X-Agent-Secret, compare_digest)
+ 선택적 KAKAO_BOT_ID 대조로 막고, 시크릿이 없으면 엔드포인트가 404 다
(401 은 "여기 뭔가 있다" 를 알려 준다).

- router/v1/agent/kakao_bot: 카카오 형식을 아는 유일한 파일. 헤더·경로 두 경로
- services/agent/channel: 신원(★ 토큰을 발급하지 않는다) · 가게 고르기 · 확인
- 0022: owner_kakao_links 에 current_place_id · pending_*

빌더 화면과 다른 것 셋:
- 로그인 토큰이 없다 → 발화자 키로 사장님을 찾는다
- place_id 가 URL 에 없다 → 여럿이면 추측하지 않고 되묻는다. 임의로 첫 가게를
  고르면 사장님은 엉뚱한 가게를 고쳐 놓고도 모른다
- 확인을 되돌려 줄 프론트가 없다 → 서버가 pending 을 든다. ★ 3분 만료가 없으면
  한참 뒤의 "네" 한 마디에 묵은 발행이 돈다

5초 벽은 DEADLINE_SEC=4.0 으로 끊고, 어떤 실패도 200+안내다 —
메신저에서는 500 도 침묵으로 보인다.

밟은 것: execute_lambda 는 람다 반환값을 그대로 준다(CRUD 관례가 (ErrorType,값)).
우리 람다가 객체만 돌려주자 언패킹 TypeError 가 났고, 라우터가 예외를 삼켜
화면에는 안내 한 줄만 보였다 — 원인이 안 보이는 종류다.

test_kakao_webhook.py 17 passed. 전체 841 passed / 53 failed(이전과 동일).
npm run lint 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 15:13:33 +09:00