user.properties.appUserId 는 봇 설정에 앱키가 물려 있을 때만 오고, 그 값이
**카카오 로그인으로 받는 값과 같다**(카카오 공식 문서). 오면 6자리 코드 절차 없이
사장님을 바로 알아볼 수 있다.
★ 로그인 작업을 다 만들고 나서 "키가 안 온다" 를 알면 그게 제일 비싸다 —
그래서 배선보다 이 한 줄이 먼저다.
★ 값 자체는 남기지 않는다. 사용자 식별자라, 어떤 키가 왔는지와 길이만 남긴다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
겨냥하는 것이 AI 검색 노출인데 캡션에 그 단서가 하나도 없었다. "군산 숙소" 같은
지역+업종은 실제 검색어이고 AI 가 인용할 때 잡는 말이다.
★ 해시태그가 아니라 문장에 녹인다. AI 는 태그가 아니라 문장을 읽고, 이 글은
사장님 개인 계정으로 나가는 발화라(DECISIONS 8) 태그 나열은 그 전제와 어긋난다.
기존 해시태그 금지는 그대로 둔다.
★ 지역·업종은 fact 가 아니라 카카오 로컬 검증을 통과한 place 값이다. ground_check
근거에 같이 얹는다 — 안 얹으면 본문에 쓴 순간 '근거 없는 주장' 으로 반려되고
3회 재시도를 태우고 실패한다. 상호명을 그렇게 다루던 방식 그대로다.
- prompts/social: caption_category("숙박업소"→숙소) · caption_region(
"전북특별자치도 군산시"→"전북 군산"). ★ copy.py 라벨과 region_label() 을 그대로
쓰지 않는다 — 저쪽은 홈페이지 본문·쇼케이스 목록용이라 그 자리에선 그게 맞다
- 표에 없는 시·도는 그대로 둔다 — 줄이려다 엉뚱한 지역이 되는 것보다 낫다
- 값이 없으면 그 말만 빠지고 나머지는 그대로 나간다
실모델 확인: "전북 군산의 숙소 스테이,머뭄은…" weighted 141/500(전 120).
test_social 27 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
썸네일이 대표 사진 한 장이라 재발행해도 쇼케이스·목록 카드가 사이트 모습과 달랐다.
- site_thumbnail: Playwright(워커 이미지에 이미 있음)로 /s/<slug> 1280×800 촬영, 실패 시 대표 사진
- THUMBNAIL_CAPTURE=0 이면 촬영을 끈다 — conftest 기본값 0(테스트에서 외부 주소를 열지 않게)
테스트 2건 추가(test_site_thumbnail)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
사업장 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>
컨테이너를 재생성하면 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>
새 템플릿을 숙박에만 등록해서 음식점 사장님 화면에는 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>
백엔드가 뜰 때마다 접속 주소 전체(아이디·비밀번호 포함)를 INFO 로그에 찍고 있었다.
컨테이너 로그를 볼 수 있는 사람이면 누구나 운영 DB 비밀번호를 읽을 수 있었다.
- db_session_manager.create_engine: 읽기·쓰기 로그를 host:port/name 으로
py_compile 통과
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
충돌은 이 브랜치의 주석 정리 커밋(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>
충돌은 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>
추가한 코드를 다시 읽다가 테스트가 못 잡던 여섯 가지가 나왔다. 둘은 조용히 반대로 동작했다 —
"소개 앞쪽으로" 가 소개 뒤로 갔고, "낮 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>
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): 카톡과 메일 둘 다, 승인은 링크가 아니라 연결된 계정 신원으로.
이 커밋은 발송과 메시지 그리기까지고 [승인] 버튼은 다음 단계(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. 실제 카톡 수신은 콘솔 설정·운영 배포 뒤에 확인한다.
"맨 위 · 맨 아래 · 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>
미니블로그 승인 알림을 메일에 더해 연결된 카카오톡으로도 보내기 위한 첫 단계.
승인 방식은 인라인 버튼 + 연결된 계정 신원 확인으로 정했고(링크가 없어 메신저
미리보기가 먼저 열어 승인되는 문제가 없다), 이 커밋은 발송 통로와 규격 확인까지다.
- 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
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>
말로 고친 값이 형식 검증 없이 저장됐다 — "반려동물 이제 돼요" 가 "가능" 으로 들어가면
화면엔 "가능" 이 뜨는데 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>
사장님은 "체크인 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>
여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다.
- 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>
템플릿 정보가 빌더·렌더러·백엔드에 따로 적혀 어긋나 있었다 — 백엔드 기본값이 없는 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>
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). 도구 선택은 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>
사진 쪽은 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>
"문구 변경밖에 안 된다" 는 지적에서 시작했다. 페이지 구성은 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>
주소가 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>
_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
예전에는 소재 목록을 순서대로 뽑아 날짜에 차례로 붙여서, 9월 날짜에
'한겨울' 글이나 끝난 축제 글이 붙을 수 있었다. 이제 날짜를 먼저 정하고
그 날짜에 맞는 소재를 고른다.
- 축제: 시작 14일 전 ~ 종료일 사이만 / 계절: 게시일 절기 하나
/ 날씨: 그 달에 있을 법한 것만(눈 12~2월, 소나기 6~8월) / 주변: 늘 후보
- 프롬프트에 게시일을 넣고, 날씨를 단정하지 않게 문구를 고쳤다
- 계절·날씨·축제 주제 키에 연(월)을 넣어 해마다 다시 쓸 수 있게 했다
- 자동·구간·개별 세 경로가 _compose_for_dates 하나로 만든다
같이 고친 버그: materials 가 스냅샷에 없는 local.festivals·attractions 를
읽어 축제·주변 소재가 늘 비어 있었다. 원문 행(local.contents)을 읽는다.
"콜백을 켰는데 그대로" 를 로그 없이 추측으로 좁히고 있었다. 스킬이 폴백이 아닌
다른 블록에 붙어 있으면 그 블록에는 콜백 설정이 없어 조용히 동기로 돈다 —
어느 블록이 도는지까지 같이 남긴다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
실사용에서 모든 발화가 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>
실사용 첫날 "시설 편의에서 바비큐 이용 문구 빼줘" 가 타임아웃으로 끝났다.
★ 작은 표본으로 잰 수치를 상한 근거로 삼은 것이 틀렸다. 개발 중 잰 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>
실제로 붙여 보니 빠진 것이 드러났다 — 연결은 됐는데 **어느 홈페이지를 다루는
대화인지** 말해 주지 않았다. 가게가 하나면 말없이 자동 선택돼 더 모호했다.
- 연결 직후 목록을 보여준다. 하나면 이름+발행 여부를, 여럿이면 바로가기 버튼으로
- 목록 줄에 발행 여부를 적는다 — 안 그러면 고친 것이 손님에게 보이는 줄 안다
- "목록"·"가게 바꿔줘" 로 언제든 돌아와 바꾼다. ★ 이 경로는 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>
런타임은 한 줄도 안 바뀌었다. 채널을 모르게 만들어 둔 것이 여기서 값을 했다 —
새로 생긴 것은 형식 변환(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>
카카오톡 채널의 통신사 인증이 끝나 어제 걸어 둔 보류를 푼다. 코드는 어제도 오늘도
그대로고 값만 바꿨다 — 닫고 여는 일이 커밋을 되짚는 일이 되면 안 된다는 어제
판단이 하루 만에 값을 쳤다.
- config/agent_config: 기본값 0 → 1
- ★ 켜도 LLM 키가 없으면 안 열린다(runtime.is_configured 가 스위치와 키를 둘 다
본다). 키 없는 환경에서 켜 둔 채 잊어도 "눌러도 안 되는 입구" 가 안 생긴다
- 스위치 테스트를 새 기본값에 맞춰 갱신 — 키가 없을 때도 안 열리는 것을 함께 검사
카카오 연결 카드는 아직 감춰져 있다(KAKAO_CHANNEL_PUBLIC_ID 미설정). 채우면 코드는
발급되지만 소비할 웹훅(4단계)이 없어 연결이 완성되지 않는다.
test_agent_runtime·test_kakao_link 34 passed. npm run lint 통과
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
사장님 지시: "이메일 승인후에 블로그가 올라가는 페이지에 5초 정도 이후에 연결".
승인만 하고 실제로 어디 올라갔는지 못 찾는 걸 줄인다. 재발행(BUILD 잡)은 몇 분
걸리므로 5초 뒤 반영을 보장하진 않지만, 어디로 가면 되는지는 바로 알려준다.
자동 이동을 못 믿어도 되게 같은 주소를 안내 문구의 링크로도 남긴다.
- post_service._blog_url: 발행된 사이트가 있으면 그 미니블로그 자리(#blog) 주소,
없으면 None — 호출부가 자동 이동 없이 확인 문구만 보여준다
- router/v1/site/post.py: <meta http-equiv="refresh"> + 안내 링크 추가.
redirect_url은 서버가 site_payload.publish_url()로 만드는 고정 오리진+slugify
통과 값이라 사용자 입력은 아니지만, HTML 속성에 꽂는 자리라 html.escape 적용
(자동 보안 리뷰 지적 반영)
- 신규 테스트 2건(발행 사이트 있을 때/없을 때), 관련 스위트 전체 49 passed
로컬 solution-backend·solution-worker 재빌드해 반영 확인함
큐 삽입 쪽(social_crud.decide, social_service.create_draft/publish_reused_text)이
JobType enum(SOCIAL_DRAFT=9, SOCIAL_POST=10)을 안 쓰고 숫자를 하드코딩(8, 9)해서,
워커 디스패처(worker/handlers.py)가 그 숫자로 엉뚱한 핸들러를 불렀다 — "게시" 잡(9)은
run_draft로, "초안" 잡(8)은 run_rollback으로. 둘 다 대상 상태 조건이 안 맞아 에러 없이
{"skipped": true}로 끝나 DONE 처리됐다 — 승인해도 실제로는 한 번도 게시되지 않는데
로그만 보면 정상으로 보이는 조용한 실패였다. SOCIAL_POSTING_ENABLED가 계속 꺼져 있어
지금까지 드러나지 않았다(2026-09-14부터 있던 버그).
- 세 호출부를 JobType.SOCIAL_DRAFT.value/SOCIAL_POST.value로 교체
- 기존 테스트의 job_type 기대값(8→9, 9→10)도 실제 enum에 맞게 수정
- 신규: 디스패처 매핑 정적 대조 + 실제 큐 삽입값으로 하는 엔드투엔드 회귀 테스트
(되돌려서 새 테스트가 실패하는 것까지 확인함)
전체 회귀 70 passed
카카오톡 채널 개설이 법인폰 본인인증에 걸려 보류됐다. 채널이 없으면 대화창은
사장님에게 **어디에도 닿지 않는 입구**이고, 열려 있으면 "되는 기능" 으로 오해한다.
- config/agent_config: AGENT_CHAT_ENABLED 신설(기본 0)
- runtime.is_configured(): 스위치와 LLM 키를 둘 다 본다 — 화면을 우회해 API 를
직접 불러도 AGENT_NOT_CONFIGURED 다
- AgentChatDock · KakaoChannelCard: 조건 미충족이면 통째로 감춘다(return null).
연결 카드는 connection_enabled 가 기준이라 설정만 채우면 그대로 다시 나타난다
- ★ 코드를 지우지 않았다 — 되돌릴 때 커밋을 되짚지 않고 값 둘만 채우면 된다
★ Threads 카드와 판단이 갈린 것이 맞다. 저쪽은 사장님이 곧 쓸 수 있는 기능이라
자리를 두고 버튼만 죽였고, 이쪽은 언제 열릴지 말해 줄 수 없다.
test_agent_runtime(스위치 2건 추가)·test_kakao_link 34 passed. npm run lint 통과
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
solution-site 빌드가 TS2440(Import declaration conflicts with local
declaration of 'useCallback')로 실패했다. FastAPI 라우트 핸들러 함수명이
`callback`이라 orval이 만든 React Query 훅 이름이 `useCallback`이 됐고,
같은 파일에서 import한 React의 useCallback과 이름이 겹쳤다.
- router/v1/social/oauth.py: 함수명 callback → oauth_callback
- 백엔드 재빌드 후 npm run orval 로 재생성 → useOauthCallback,
callbackParams.ts → oauthCallbackParams.ts
docker compose build solution-site 로 재검증, 정상 빌드 확인
빌더 앱에서 미니블로그를 운영하는 데 필요했던 몇 가지를 묶었다.
- notify_email: 업장별 승인 메일 수신자를 계정 로그인 이메일과 분리(places.notify_email,
migrations/0021, PlaceCRUD·protocol.py·place_service.py 검증, BlogPostsPage.tsx 설정 UI)
- 글 삭제(soft delete): 상태 제한 없이 지우고, 이미 게재된 글이면 재발행 잡까지 큐에 넣는다
(post_crud.py, router/v1/site/post.py DELETE, BlogPostsPage.tsx 삭제 버튼)
- 지금 발송하기: 아침 9시 스윕을 안 기다리고 바로 발송(post_crud.next_due_for_mail,
BlogPostsPage.tsx 버튼)
- blog_service.generate_one: 하드코딩된 Gemini 대신 services/llm/provider.py(LLM_PROVIDER,
기본 openai)를 타도록 전환, 업종별 분기 구조(현재 숙소만 구현) 추가
- site_payload.publish_url(place, site): 발행 주소 조합을 한 곳에 모은 헬퍼
- 프론트: orval 로 재생성한 API 클라이언트(notify_email·삭제·즉시발송·social 엔드포인트 반영)
관련 스위트는 별도 커밋(쓰레드 연동 작업)에서 이미 PASS 확인함
직전 커밋(cd77171)에서 문서·프론트만 옮기고 서버 쪽 표(site_payload._weather_condition)를
빠뜨렸다. use-live-weather.ts 와 같은 표를 봐야 하이드레이션 전후 문구가 안 바뀐다는
불변식이 절반만 적용된 상태였다 — 여기서 마저 맞춘다.
WMO weather_code 51~57(이슬비 3단계), 61~67(비/어는비 혼재) 등을 "이슬비"·"비"
같은 큰 구간으로 뭉쳐 표시하던 것을 코드 하나당 고유 라벨로 바꿨다. 서버
프리렌더 스냅샷(site_payload._WEATHER_CONDITION_BY_CODE)과 브라우저 재조회
(use-live-weather.ts WEATHER_CONDITION_BY_CODE)가 같은 표를 봐야 하이드레이션
전후로 문구가 안 바뀐다는 기존 불변식은 유지한다.
- docs/WEATHER.md: 27개 코드 전체를 "구간→분류" 표에서 "코드→고유 라벨" 표로 재작성
- weather_notes.json: 코드별 문구 갱신
- use-live-weather.ts/.test.ts, weather.test.tsx: 새 라벨 반영
사장님 지시: "쓰레드에 연동되어 있으면 같이 업로드 되는 기능". 미니블로그의 두
승인 경로(이메일 GET 토큰, 로그인 "바로 발행")를 공통 메서드로 묶고, 그 끝에서
쓰레드 연동을 시도한다. 미니블로그 승인 자체가 발화 동의로 취급되므로 쓰레드
쪽 별도 승인은 묻지 않는다(DECISIONS 7-1-2 개정, 문구를 그대로 재사용하는
경우에 한정). 겸사겸사 쓰레드 초안 생성(generate_social_post)이 LLM_PROVIDER
를 안 타고 Gemini 를 직접 호출하던 것도 다른 생성 함수와 같은 추상화로 맞췄다.
- blog_jobs._published_places: sites.domain IS NOT NULL 조건 추가(쓰레드 기준과 통일)
- social_service.publish_reused_text: 연동 없음/게시 비활성/domain 미확정이면 스킵,
정상이면 APPROVED 삽입 + run_post(job_type=9) enqueue — 새 게시 로직은 안 만든다
- post_service: decide/approve_by_owner → _approve_and_publish 로 공통화,
_try_social_share 는 실패를 전부 삼켜 미니블로그 승인을 막지 않는다
- gemini_text.generate_social_post: services.llm.provider.active() 로 전환,
Gemini 전용 import 제거
- DECISIONS.md 7-1-2, MINI_BLOG.md 5·8절, SOCIAL.md 갱신
test_blog_owner.py·test_social.py 다수 추가/수정, 관련 스위트 전체 PASS
OpenAI strict 모드는 'STRING' 을 거부한다:
Invalid schema for response_format: 'STRING' is not valid under any of
the given schemas
Gemini 는 대소문자를 둘 다 받아서, 대문자로 써 두면 **공급자를 openai 로 바꾸는
순간에만** 터진다. LLM_PROVIDER 기본값이 openai 인데 두 파일만 대문자로 남아
있었다 — 대화창은 첫 발화부터 502 였고 SNS 초안도 같은 이유로 못 돌았다.
- prompts/agent: 소문자로. args 는 strict 가 전 프로퍼티를 required 로 만드므로
안 쓰는 인자가 빈 문자열로 온다 — 도구는 "" 를 '없음' 으로 읽는다
- prompts/social: 같은 수정. 나머지 프롬프트는 원래 소문자였다
실측 확인: "체크인 시간 3시로 바꿔줘" → set_fact(check_in_time, 15:00)
test_agent_runtime·test_social 34 passed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
공급자를 openai 로 바꾼 뒤에도 호출측에 "GEMINI_API_KEY 미설정" 이 문자열로
박혀 있어서, **없는 것은 OPENAI_API_KEY 인데 화면은 Gemini 를 탓했다**
(실측 2026-09-21: 로컬에서 소개문이 안 나와 Gemini 키를 한참 들여다봤다).
원인을 정확히 반대로 가리키는 종류다.
- llm/provider: missing_key() 추가 — 활성 공급자에게 필요한 env 이름을 돌려준다
- copy_service·collect_service·song_service: 하드코딩 문구를 그 함수로 교체
- enums: GENERATOR_NOT_CONFIGURED 주석도 공급자 중립으로
관련 테스트 55 passed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
런타임이 채널을 모르므로 채널·챗봇 심사 없이 에이전트 전체를 빌더 화면에서
검증할 수 있다. 웹훅 핸들러 안에 짜면 빌더에서 같은 걸 못 쓰고, 심사가 끝나야
무엇 하나 확인되지 않는다 — 카톡은 나중에 붙는 두 번째 입구다.
- services/agent/tools.py: 도구 넷 + 등급 셋(READ·REVERSIBLE·SEMI).
★ 도구는 반드시 services/* 를 통과한다 — crud 를 직접 부르면 스키마 검증·
출처 필수·정정본 보호가 아무 증상 없이 사라진다. 테스트가 소스로 검사한다
- services/agent/runtime.py: 발화 → 도구 선택(LLM 1콜) → 실행 → 응답
- services/prompts/agent.py: LLM 네 겹 규약대로 프롬프트만 여기
- router/v1/agent/chat.py + features/agent/AgentChatDock.tsx(/sites 우하단)
모델에게 맡기지 않은 셋:
- 등급 — 응답 스키마에 칸 자체가 없다. 모델이 정하면 프롬프트에 끼어든 한 줄이
확인 절차를 건너뛴다
- 결과 문구 — 도구가 만든다. 모델이 쓰면 하지 않은 일을 했다고 말할 수 있고
사장님에게는 그 말이 사실로 보인다
- key — set_fact 의 key 는 업종 스키마가 최종 판정이다
확인(SEMI)은 실행하지 않고 되묻는다. 돌아온 confirm 값을 믿지 않고 도구는
레지스트리에서 다시 찾고 인자는 도구가 다시 검증한다 — 확인 절차가 검증을
건너뛰는 구멍이 되면 안 된다.
값을 고치면 재발행 안내를 함께 낸다 — fact 는 바뀌어도 사이트는 안 바뀐다.
test_agent_runtime.py 17 passed(LLM 은 monkeypatch, 실제 모델 호출 없음).
전체 796 passed / 50 failed — 그 50건은 HEAD 에서도 동일한 기존 이슈.
npm run lint 통과
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>