base.css 의 @font-face 가 /fonts/PretendardVariable.woff2 를 가리키는데 public/fonts 에는 README 만 있었다.
킹서버 실측: 그 주소가 404 — 윈도우 콘솔에 폰트 오류로 보였고, 빌더와 Pretendard 를 쓰는
기본 템플릿은 맥·윈도우 모두 Noto Sans KR 로 떨어져 있었다. Pretendard 는 구글 폰트에 없다.
- public/fonts/PretendardVariable.woff2 (v1.3.9 가변, 2.0MB, orioncactus/pretendard 공식 배포본,
sha256 9599f12f…) — 빌더(frontend)·발행본(site) 양쪽
- Pretendard-OFL.txt: SIL OFL 1.1 은 폰트 배포 시 라이선스 동봉을 요구한다
vite build 산출물에 fonts/ 포함 확인
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
컨테이너를 새로 만들 때마다 모델 465MB 를 다시 받았다(실측 2분 49초). 그 사이 첫 발행은
publish 60초 상한을 넘겨 키워드 없이 나간다. 4.x 는 이 변수를 읽지 않고 node_modules 아래에
받아서 ontology-model 볼륨(/app/.cache)이 비어 있었다.
- config/env: embedding.cacheDir ← TRANSFORMERS_CACHE
- local.provider: transformers.js env.cacheDir 로 넘김
tsc 통과. 로컬 compose: 재생성 후 /app/.cache 465M, 다시 재생성하면 모델 로딩 1.7초
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
발행본 HTML 에 그 순간 기온·관측 시각이 박혀 구글과 첫 화면이 며칠 전 날씨를 봤다.
라운드·코랄·미니멀·솔숲은 관측 시각(시간대 없는 KST)을 new Date 로 읽어 굽는 서버(UTC)가
9시간 틀린 값을 박았고, 브라우저 값과 달라 열 때마다 React #418 → 페이지 전체를 다시 그렸다
(실측 /s/joyyy: HTML "22:45 관측" ↔ 브라우저 "13:45 관측").
- lib/use-live-weather.ts: 첫 렌더는 날씨 없음, 하이드레이션 뒤 스냅샷 → 최신 값
- layouts/{round,coral,minimal,pine}/Weather.tsx: 관측 시각을 글자 그대로 읽는다(다른 템플릿과 같게)
- lib/format.ts formatKoreanDate: 한국 시간 기준 날짜 — UTC 서버가 0~9시 KST 발행에서 하루 전 날짜를 박던 것
- WeatherSection: 문구 고르는 부분을 weatherLineSources 로 떼어 테스트. HTML 에 날씨가 없음을 테스트로 고정
tsc·eslint 통과 · vitest 128 passed · TZ=UTC 로 구운 10개 사이트 #418 0건
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
가게별 첫 화면 문구를 만드는 곳이 없어 모든 숙박이 공통 문구 10개를 돌려 썼다.
- COPY 잡에 catchphrase 단계(prepare→generate→save→catchphrase→faq_fill), 숙박이 아니면 건너뜀
- prompts/catchphrase.py·gemini_text.generate_catchphrases: 캐치프레이즈 1 + 일반 20·계절 3×4·월 12·날씨 2×4.
문구마다 길이·상호명·ground_check(근거 없는 시설·수치) 검사
- fact tagline·catchphrases(lodging 스키마, allow_llm). 순환 문구는 사장님이 줄 단위로 고치게 글로 저장
(catchphrase_text: `봄 | ` · `9월 | ` · `비 | ` 머리)
- site_payload: narrative.tagline·catchphrases 로 넘기고 이용정보 표(facts)에서는 뺀다
- 빌더 생성 화면 단계 문구, DATA_MODEL·GENERATION_FLOW
테스트 6건 추가. 관련 20개 파일 386 passed · 실패 102건은 main 과 목록이 같다(기존 실패)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/s/stay 목업은 캐치프레이즈(tagline) 한 줄이 고정이고 그 아래 줄에서 문구가 돈다
(2026-09-11 "메인 모토는 고정, 그 아래 변동되는 모토"). 템플릿은 큰 줄에 공통 문구 10개를
돌리고 있었고, 앞 커밋의 소문구는 "명소까지 거리" 로 목업과 달랐다.
- sections/HeroCatchphrase.tsx: HeroCatchphrase = tagline 고정(없으면 공통 문구 중 사이트별 하나),
HeroRotator = narrative.catchphrases 를 일반→계절→일반→월→일반→날씨 순서로 6초마다, 줄 높이는 가장 긴 문구
- 9종 Hero: 소문구 자리를 HeroRotator 로. kit/heroSub.ts 삭제
- shared: catchphrases 항목에 season·month·weather
- 테스트: 고정·폴백·비숙박·빈 순환·순서·날씨 분류
tsc·eslint 통과 · vitest 127 passed · 9종 모바일 캡처, 라운드·부티크 6초 간격 순환·높이 고정 확인
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 라운드·코랄·미니멀·솔숲: 히어로의 ‘주차 가능’ 칩 삭제. 일러스트: 지역 말풍선 삭제
- kit/heroSub.ts: 소문구 = 사장님 문구, 없으면 가장 가까운 명소까지 거리. 솔숲에만 있던 규칙을 9종 공통으로
- 고택·부티크: 캐치프레이즈 아래로 소문구 순서 변경
- 부티크 모바일: 전체화면 사진(664px)을 데스크톱과 같은 아치로 — 첫 화면 593px
- docs/TEMPLATE_DESIGN.md: 첫 화면 규칙
tsc·eslint 통과 · vitest 123 passed · 9종 모바일·1440px 첫 화면 캡처 확인
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9종이 함께 쓰는 Sheet 가 stay2 모바일 시안의 하단 시트를 그대로 옮긴 것이라
데스크톱에서도 640px 판이 화면 바닥에 붙어 떴다.
- kit.css·paper.css: .sheet 를 가운데 정렬, .sbox 폭 560px·높이 100svh-48px, 모서리는 템플릿 --k-r
- kit.css: 라운드·코랄·미니멀·솔숲 예약 창(.sheetbody)에 여백이 없어 글자가 창 끝에 붙던 것
- docs/TEMPLATE_DESIGN.md: 창 위치 규칙
eslint 통과 · vitest 123 passed · 9종 모바일·1440px 에서 위아래·좌우 여백 같음 확인
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
같은 템플릿 안에서도 축제 카드(흰 바탕·글 안쪽)와 명소 카드(바탕 없음·글 아래)가 따로 놀았다.
실측(9종 전수): 제목 굵기 400/500/700/900, 사진 모서리 0/12/18/22px 가 한 화면에 섞여 있었다.
- kit/Card.tsx·card.css: 사진 3:2 + 제목 1줄 + 거리 1줄 + 설명 2줄. 템플릿 차이는 --kc-* 변수만
- 9종 Around·Festival, 고택 인물: 사진 카드를 PhotoCard 로 교체. 맛집·홈 목록 행은 그대로
- 템플릿 CSS: 모서리·글꼴·강조색 변수와 그리드 간격. 일러스트는 상자형, 빅타이포는 선 격자
- 모바일 캐러셀 화살표가 카드를 가려 1024px 미만에서 숨김
- docs/TEMPLATE_DESIGN.md: 사진 카드 규칙
tsc·eslint 통과 · vitest 123 passed · 12개 사이트 굽기 · 카드 모양 템플릿당 1종 확인
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
generate:false 로 불러 새 숙소가 사전 밖 단어를 받지 못했다. 생성을 켜면 호텔이 펜션 사전의
`군산 독채펜션` 을 받는데, 메타 게이트가 '펜션' 을 업종어로 보고 자료 없이 통과시켰다.
- site_ontology: generate+sync — 처음 보는 숙소만 저쪽이 기다리게 하고 publish 상한 60초
- seo_keywords: 외부 분류(없으면 상호)에 '호텔' 이면 stay.hotel, 자기 유형어만 자료 없이 통과
- snapshot: place.external_category 를 실음 — 호텔 판정 근거
- .env.example · ARCHITECTURE · DEVLOG: ONTOLOGY_LLM_PROVIDER, 발행 흐름, 실측
test_seo_keywords 20 passed(호텔 4건 추가). 전체 1041 passed / 47 failed —
47건은 이 변경 전 main 과 같은 목록
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
사전이 고정 데이터셋뿐이라 사전에 없는 숙소는 새 단어를 받지 못했고, 생성분은 MATCH_SOURCES 가
llm 을 빼서 만들어도 추천에 안 나왔다. 숙소 검색은 가까운 키워드를 사전 전체(7천)에서 뽑아
상위가 아무 숙소에도 안 붙은 단어로 차서 `선유도 독채` 가 0건이었다.
- merchants.controller: 처음 보는 업체·기한 지난 업체만 생성, sync 는 처음 보는 업체만 기다림 —
재빌드마다 불리는 창구라 조건이 없으면 저장마다 요금. 동기 생성 실패는 200 + generation.failed
- generation·dedup·keyword.repository: mock 생성분은 source='mock' — compose 기본이 mock 이라
키 없는 환경의 가짜 단어가 매칭 후보가 되지 않게
- match.service: llm 출처를 후보에 넣음(brand 는 그 업체 것만), 업종은 뿌리(stay)로 좁힘 —
새 호텔은 호텔 사전이 비어 잎으로 좁히면 후보 0건
- openai.provider: 미확인 시설·다른 숙박 유형어·가격 주장 금지
- drizzle/0002 · seed: stay.hotel — 시드만 두면 떠 있는 DB 에서 외래키 위반 500
- serving.service: 숙소에 연결된 키워드만 비교, 검색어가 지역을 말하면 그 지역 숙소만 —
e5-small 은 지역이 틀려도 0.85(`여수 호텔` → 군산 호텔 0.848)라 임계값으로는 못 가른다
tsc 통과. 로컬 :3101 + 실 OpenAI(가짜 숙소 2곳): 첫 발행 7초 · 키워드 12 생성 · 재발행 fresh,
검색 7건(`은파호수공원 근처 호텔`·`선유도 독채`·`군산시내호텔` 적중, `여수 호텔` 0건) 확인 후 테스트 데이터 삭제
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
9~11월 글이 전부 가을 빨강 날짜·분홍 카드로 나와 레트로 템플릿에서 경고처럼 보였다. 4초 넘김은 읽기 전에 지나갔다.
- BlogSection: 계절색 제거 — 카드 --tpl-card, 큰 날짜 숫자 --tpl-primary, 월·요일 두 줄, 구분선, 본문 --fs-body 4줄, 카드 폭 272px
- 미니블로그만 autoplay 7000ms
site tsc·eslint, blog.test 4 passed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
승인 알림보내기·아침 9시 발송이 배정일 오늘 이전 글 중 가장 오래된 것을 골라, 10/1 에 9/23 글이 메일로 갔다.
- post_crud: due_for_mail · next_due_for_mail 를 scheduled_date == today 로
테스트 1건 추가. 블로그 70 passed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
카톡·메일 발송이 실패하면 로그 한 줄만 남고 글은 REVIEWED 로 계속 머물렀다. 손님 예약 요청은
사장님께 못 가도 아무도 몰랐다.
- blog_jobs: 카톡 실패(메일 대체) · 메일 실패 · 보낼 곳 없음 → blog_send_failed, 사업장별 dedupe, 성공 시 resolve
- booking_request_service: 전달 실패 → booking_request_failed, 손님 이름·연락처는 싣지 않는다
- docs/ALERTS.md: 두 kind 추가
테스트 2건 추가. 블로그·예약·알림 93 passed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stay 는 굽기 보호 주소라 그 사업장 글을 승인하면 발행이 거절되고 글은 승인 상태로 남았다.
목업 화면의 미니블로그는 파일에 박힌 글이라 승인해도 바뀌지 않았다.
- services/mockup_blog.py: window.__SITE_PAYLOAD__.posts 만 교체 — 기존 글 유지 · 새 글 추가 · 최신순, 직전 파일은 index.html.bak-blog
- build_service: 발행 주소가 stay·stay2 면 굽지 않고 글만 갱신 후 승인분을 게재로(버전·Teams 실패 알림 없음)
테스트 3건 추가(test_mockup_blog). 빌드·블로그·롤백·썸네일 105 passed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
어제 더보기에서 미니블로그 관리를 통째로 뺐더니 최고관리자도 들어갈 길이 사이트관리 화면뿐이었다.
- SitesPage: role >= OWNER 일 때만 더보기에 미니블로그 관리(/blog?placeId=)
frontend tsc·eslint 통과
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
발행된 사이트가 전부 자동 생성·발송 대상이라, 테스트로 만든 업장까지 매일 알림을 받는다.
실측(2026-10-01): 발행 25곳 중 스테이,머뭄 5개·게으르게펜션 2개 등 중복·테스트 업장이
상당수였고, 수신자는 내부 계정 7명이었다. 시험 운영 중에는 받을 사람을 좁혀 둔다.
- blog_jobs._for_auto_sweep: user_id 목록으로 거른다. **비면 전체**다 — 운영 기본값이라
칸이 비었다고 기능이 꺼지면 안 된다(쉼표만 있는 입력도 전체로 친다)
- generate_drafts · send_reviewed 두 스윕에 **같이** 건다 — 한쪽만 걸면 만들어 놓고
안 보내거나 그 반대가 된다
★★ 거르는 자리가 `_published_places()` 안이 아니다. 그 함수는 빌더의 [지금 생성하기]
(`generate_range`·`generate_one_for_date`)도 쓰는데, 거기서 거르면 **목록 밖 사장님이
자기 화면에서 자기 글도 못 만든다** — 자동으로 보낼지와 직접 만들지는 다른 질문이다.
테스트가 그 두 함수의 소스에 `_for_auto_sweep` 이 없는지까지 본다.
★ 목록 밖 업장의 글은 건너뛸 뿐 **버리지 않는다** — REVIEWED 로 남아 목록을 풀면 그때
나간다. 거기서 SENT 로 찍으면 아무에게도 안 간 글이 보낸 것으로 기록된다.
테스트 5건 추가. blog 관련 78 passed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 대표 지시("이거는 안보내도 될것같아"). 켜면 발행된 사이트 **전체**가 대상이고
업장당 최대 30건이라 첫 회차에 AI 호출이 한꺼번에 일어난다 — 그 비용은 사람이 정할 일이지
기본값이 정할 일이 아니다. 글은 빌더의 [지금 생성하기] 로 사장님이 직접 만든다.
★ add_job 을 다시 지우지 않고 스위치로 뺀 이유: 바로 앞 커밋(4650125)이 고친 게
"등록 자체가 없는데 주석만 새벽에 채운다고 말하는" 상태였다. 지우면 그 상태로 돌아가
**끈 것과 빠뜨린 것이 구별되지 않는다** — 증상("메일이 안 온다")을 보고 발송 쪽을
뒤지게 된다. 지금은 꺼져 있다는 사실이 코드에 남는다.
★ 발송(09:00)은 이 스위치와 무관하게 항상 돈다 — 손으로 만든 글도 나가야 한다.
- scheduler/__init__: BLOG_AUTO_DRAFTS_ENABLED=1 일 때만 blog-drafts 등록
- docker-compose · .env.example: 스위치 전달, 기본 0
- docs/MINI_BLOG 2절: 기본 꺼짐과 그 이유, 예전에 등록이 없었다는 실측
테스트 5건(2건 신설 — 기본 꺼짐 · 켜면 등록됨), 전부 통과
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
증상은 둘 다 "메일이 안 온다" / "승인했는데 홈페이지가 그대로" 로만 보인다. 서버는 정상으로
뜨고 로그에도 에러가 없어서 눈으로 원인을 못 찾는 종류다.
① 재고 채우기가 스케줄러에 **등록돼 있지 않았다.** `scheduler/jobs.py` 에 함수는 있고
`__init__.py` 주석도 "새벽에 재고를 채운다" 라고 말하는데 add_job 한 줄이 없어 한 번도
돈 적이 없다. 09:00 발송만 돌고 보낼 글은 0건이었다 — 지금 DB 의 254건은 전부 화면의
[지금 생성하기] 로 손으로 만든 것이다.
→ 04:10 KST 등록(발송보다 앞서야 그날 아침에 나갈 재고가 있다)
② 승인 뒤 재발행이 죽는다. `post_service._enqueue_build` 가 requested_by 에
"blog-approval" 이라는 **라벨**을 넣었고 `build_service._log` 가 그걸 uuid.UUID() 에
넣다 ValueError 를 던졌다. 하필 _log 는 사이트를 다 구운 **뒤**, sites.status 를
PUBLISHED 로 찍기 **전**에 불린다 — 굽기는 끝났는데 발행만 안 된 채 3회 재시도 후 DEAD.
실측: BUILD 잡 5건(2026-09-23~09-30)이 전부 이 원인이고 전부 미니블로그 승인분이었다.
→ 호출부는 사장님 ID 를 넣고, 파서(_actor_uuid)는 모양이 틀리면 기록만 비우고 진행한다.
감사 기록 한 줄이 발행을 막는 것은 순서가 뒤집힌 것이다. rollback_service 도 같은 파서.
- scheduler/__init__: blog-drafts 등록 + 왜 빠져 있었는지
- services/build_service: _actor_uuid 신설, _log 가 그것만 쓴다
- services/post_service: requested_by = str(owner_user_id)
- services/rollback_service: 같은 파서 재사용
테스트 7건 추가(build 4 · scheduler 3), 전부 통과.
test_build_publish.py 의 기존 실패 13건은 변동 없음 — 원본으로 되돌려 측정해 확인했다
(원본 13 failed/2 passed, 변경 후 13 failed/6 passed). 그 13건은 컨테이너 테스트 DB
쪽 문제다(로그: lease 갱신 실패 InvalidCatalogNameError) — 이 변경과 무관하고 미해결로 남긴다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SignInForm 에만 붙였는데 `/login`·`/signup` 은 그 컴포넌트를 쓰지 않고 **각자 폼을 갖고
있다**(구글 버튼도 세 군데에 따로 적혀 있었다). 번들에는 키가 구워졌고 isKakaoLoginEnabled()
도 true 였는데 화면에만 없어서, 설정 문제로 보이는 종류였다.
- pages/LoginPage: 카카오 버튼 추가. returnTo 로 `from`(원래 가려던 곳)을 넘긴다
- pages/SignupPage: 가입도 같은 동작이라 같은 버튼을 쓴다(처음 온 회원번호면 백엔드가 생성)
lint(react-router typegen + tsc + eslint) 통과
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
콘솔에 발급된 설정이 설계와 어긋나 있었다. 클라이언트 시크릿이 켜져 있고 Redirect URI 가
프론트 경로(/auth/kakao/callback)인데, 코드는 "프론트가 액세스 토큰을 들고 온다" 는
전제였다 — 시크릿을 번들에 구울 수 없으므로 그 흐름으로는 토큰을 받을 수 없다.
- services/external/kakao_identity: access_token 검증 → **인가 코드 교환**으로 교체.
★ 우리 REST 키·시크릿으로 교환한 토큰이라 정의상 우리 앱 것이다 — 구글의 aud 대조에
해당하는 검사(KAKAO_LOGIN_APP_ID)가 구조적으로 필요 없어져 설정 칸째로 지웠다.
★ 로그인 앱과 챗봇 앱이 다르면 로그인은 되는데 채널 매칭만 조용히 안 된다 —
REST 키가 어긋나면 경고 로그를 남긴다.
★ 미인증 이메일은 신원으로 쓰지 않는다(is_email_valid·is_email_verified 둘 다). 이 값으로
auth_service 가 다른 수단 가입과의 충돌을 판정한다.
- config/config_models: KAKAO_LOGIN_APP_ID → REST_API_KEY · CLIENT_SECRET · REDIRECT_URI
- router/v1/auth: Req_KakaoLogin {access_token} → {code, redirect_uri}
- frontend: lib/kakaoIdentity(인가 URL · sessionStorage state) · KakaoSignInButton ·
pages/KakaoCallbackPage(코드는 1회용이라 ref 로 한 번만 보낸다) · routes
★ 로그인 폼에서 카카오를 구글 위에 둔다 — 이 수단으로 들어와야 6자리 코드 없이 채널이 이어진다
- compose · nginx/Dockerfile: REST 키·Redirect URI 만 프론트로 흘려보낸다. 시크릿은 안 나간다
- 어드민 키는 저장하지 않는다 — 탈퇴까지 되는 키인데 로그인에는 쓰이지 않는다
- api/generated: orval 재생성. 카카오 외에도 오래 밀려 있던 문구·agent 경로가 함께 따라왔다
테스트 8건 추가(tests/test_kakao_identity.py), 관련 85 passed
(test_auth::test_google_login_is_off_when_client_id_is_empty 는 기존 실패)
lint(react-router typegen + tsc + eslint) 통과
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
수집 1회에 3~4장, 발행 1회에 2장씩 와서 장애 알림이 묻혔다. 빌드·되돌리기 실패와 수집 예외는
기존 build_failed · job_dead 알림이 이미 보내고 있어 같은 사고가 두 번씩 갔다.
- collect_service: 시작·완료 요약·예외 알림 제거 — 채널 크롤링 실패만 남긴다(채널별 결과는 잡 결과 channels 에 그대로)
- vision_service: 실패가 있을 때만
- build_service · rollback_service: activity 알림 되돌림 — build_failed · recovery 가 그대로 담당
- docs/ALERTS.md: activity 행을 실제 발송 범위로
관련 테스트 17개 파일 297 passed · 2 failed(test_search_console_service — 변경 전에도 실패)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
★★ 챗봇 웹훅의 user.properties.appUserId 는 카카오 로그인의 회원번호와 **같은 값**이다
(카카오 공식 문서, 봇에 앱키가 물려 있을 때). 그래서 카카오로 로그인만 해 두면 채널에
말을 거는 순간 누구인지 알 수 있고, 6자리 코드 절차가 필요 없어진다.
- external/kakao_identity: 액세스 토큰을 카카오에 되물어 확인한다. ★ 응답의 app_id 를
우리 앱과 대조하는 것이 구글의 aud 검사에 해당한다 — 이게 없으면 남의 앱 토큰으로
우리 계정이 된다. 이름·이메일은 동의 항목이라 못 받아도 로그인은 되게 했다
- auth_service.kakao_login: google_login 과 같은 세 갈래. 이메일이 겹쳐도 자동으로
잇지 않는다(DECISIONS 1 — 계정 선점)
- kakao_link_service.link_by_app_user_id: 자동 매칭. ★ 이미 다른 사장님에게 묶인
카톡은 빼앗지 않는다 — 조용히 빼앗으면 앞사람이 남의 가게를 보게 된다
- ★ 코드 경로는 그대로 둔다: id/pw·구글 가입자에겐 appUserId 가 없고, 봇에 앱키가
안 물린 환경에서는 값 자체가 안 온다
★ 함께 고친 것 — services/agent/tools.py 가 사라진 site_payload._DEFAULT_THEME 를
보고 있었다(c690862 템플릿 정의 통합에서 이름이 없어졌는데 이 한 줄만 남았다).
**대화의 섹션 기능이 통째로 죽어 있었고** 웹훅이 AttributeError 를 삼켜 "지금은
처리할 수 없어요" 로만 보였다 — common/template_catalog.industry_of 로 바꿨다.
test_kakao_link·test_kakao_webhook·test_agent_runtime·test_auth 194 passed
(신규 4: 자동 매칭·미가입자·빼앗지 않음·재진입). 남은 1건은 컨테이너에 실제
GOOGLE_CLIENT_ID 가 있어 나는 기존 실패다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
다른 세션(SNS AEO SEO 팔로워 수집)에서 만들던 작업이 작업 트리에 미커밋으로
남아 있었다. 잃지 않게 그 자리에서 커밋한다 — 커밋 하나에 무관한 변경을 섞지
않으려고 카카오 로그인 작업과 분리했다.
- POST serving/lodgings/search — 지역·유형·표기 변형을 받아 숙소와 SEO 메타를 돌려준다
- ★ SEARCH_FLOOR=0.94: 짧은 한글 키워드는 전혀 다른 말도 코사인이 높다
(dedup.service 실측: '선유도 펜션' ↔ '새만금 펜션' = 0.936). 그래서 벡터 갈래는
이 값 이상만 "같은 뜻" 으로 치고, 지역 구분은 벡터가 아니라 위치 갈래에 맡긴다
검증: npm install 후 tsc --noEmit — src/ 오류 0(기존 scripts/ 오류 2건은 이번 변경과 무관).
런타임 동작은 확인하지 못했다(DB·임베딩 제공자가 필요하다).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
앞 커밋(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>
새 템플릿을 숙박에만 등록해서 음식점 사장님 화면에는 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>