o2o-site-AEO/docs/DEVLOG.md
Mina Choi 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

15 KiB

개발 일지

무엇을 왜 바꿨는지 날짜순(새 것이 위)으로 요약한다. 결론·배경은 각 문서가 단일 출처고, 여기에는 나중에 같은 실수를 막아 주는 것(결정의 이유·밟은 함정·실측값)만 남긴다. 2026-09-29에 요약본으로 다시 썼다. 원문 전체는 git 히스토리(이 파일의 09-29 이전 버전)에 있다.


2026-09-28 — 숙박 템플릿 다섯 개 추가 (라운드 · 시네마 · 빅타이포 · 부티크 · 일러스트)

국내 펜션 사이트 46곳을 모바일에서 재 보니 첫 화면 제목 14~24px, 본문 11~14px였다. 기존 템플릿도 전부 작은 글씨 쪽이라, 토스·카카오뱅크·당근·해든스테이·스테이인터뷰를 390px에서 실측해 뼈대를 새로 만들었다.

  • site/src/layouts/ 에 round cinema bigtype boutique graphic. 섹션·탭 네 개는 고택과 같고 예약 시트·폼·캐러셀은 고택 부품을 쓴다. 공통 구조 CSS는 layouts/kit/kit.css.
  • ★ kit.css 에 고택의 글꼴 규칙을 넣지 않는다 — 넣으면 다른 레이아웃의 워드마크가 17.5px로 눌린다(실측).
  • graphic 은 사진이 거의 없는 집용. 사진 0장이면 기존 발행 게이트("고유 콘텐츠 0건")에 걸린다.

2026-09-28 — 템플릿 정의를 한 파일로 모았다

빌더·렌더러·백엔드가 템플릿을 따로 적어 서로 어긋나 있었다(없는 기본 id, 강조색 오타, 섹션 간격 차이).

  • 템플릿 목록은 shared/src/data/templates.json 하나. TS와 파이썬이 같은 파일을 읽는다.
  • id에서 업종을 뗐다(stay-retro → retro, 마이그레이션 0023, 운영 미적용).
  • 모르는 템플릿 id는 저장·미리보기·발행 모두 거절한다. 기본값으로 슬쩍 굽지 않는다.
  • 고택(paper)을 /s/stay2 시안과 같게 다시 만들었다. 하위 페이지는 한 HTML 안의 탭이다.
  • 구조와 추가 방법: TEMPLATES.md.

2026-09-23 — 개발자용 사이트·유저 관리를 solution 앱에 얹었다

admin 앱을 키우기엔 이르다(대표 지시). UserRole.DEVELOPER 게이트로 /ops/sites·/ops/users(읽기 전용). ★ 메뉴 문자열은 사장님 번들에도 실린다(런타임 조건부 렌더) — 데이터는 백엔드 게이트가 막는다.

2026-09-21 ~ 22 — 사장님 에이전트 (빌더 대화창 → 카카오톡 채널)

설계와 함정은 AGENT.md와 AGENTS.md "에이전트에서 조용히 틀리는 것"이 단일 출처다.

  • 순서: 신원 연결(owner_kakao_links) → 도구 레지스트리·런타임·빌더 채팅창 → 카카오 웹훅. 런타임이 채널을 모르게 만들어 두어, 웹훅을 붙일 때 런타임은 한 줄도 안 바뀌었다.
  • 모델에게 맡기지 않은 셋: 확인 등급, 결과 문구, fact key. 확인(SEMI)은 서버가 인자를 다시 검증한다.
  • ★ 오픈빌더는 서명이 없다 — 공유 시크릿이 유일한 문이고, 없으면 엔드포인트가 404.
  • ★ 카톡 5초 벽: 개발 중 잰 1.3~2.4초는 장난감 프롬프트였고 실사용 첫날 타임아웃이 났다. 콜백(useCallback)으로 즉답 후 따로 보낸다. 오픈빌더 스킬 설정에서 콜백을 켜야 이 경로가 열린다.
  • 카톡 대화에는 홈페이지 목록·발행 여부·가게 바꾸기를 LLM 없이 보여 준다(대화가 막혔을 때 늘 통해야 한다).
  • 밟은 것: execute_lambda 는 람다 반환값을 그대로 준다 — 객체만 돌려주면 언패킹 TypeError 가 나는데 라우터가 예외를 삼켜 "지금은 처리할 수 없어요"만 보였다.

2026-09-17 — 미니 블로그: 팀 검수 폐지 · 배정일 · 달력 화면 · 메일 승인

상세는 MINI_BLOG.md.

  • 팀 사전검수를 없애고 최종 판단을 사장님에게 넘겼다(팀 단계가 병목이었다). 업장당 하루 한 통.
  • place_posts.scheduled_date 로 글마다 날짜를 정했다. 빌더는 달력 + 그 위 일주일치 카로셀, 생성 이력 탭.
  • 메일 승인 링크는 GET 즉시 승인(프리페치 위험을 알고 사장님이 택했다), 링크는 그날 자정 만료.
  • 잡은 버그들:
    • 승인이 BUILD 잡에 owner_user_id 를 안 실어 메일 승인이 재발행을 못 하고 있었다.
    • generate_one 의 죽은 import 로 "지금 생성하기"가 500. 테스트는 그 함수를 monkeypatch 해서 초록이었다 — 단위 테스트 초록과 실제로 도는 것은 다르다.
    • scheduled_date 를 추가하자 값이 NULL인 기존 글 13건이 조회에서 조용히 빠졌다 → 백필. 새 컬럼 마이그레이션은 "기존 행이 조회에서 빠지는지"부터 본다.
    • raw text() 로 timestamptz 에 naive datetime 을 넣으면 드라이버 로컬 시간대(KST)로 9시간 밀린다.
    • 세션 복구보다 늦게 자동 로그인하면 RequireAuth 가 이미 /login 으로 튕긴다 → 복구 단계로 옮겼다.
    • ORM 객체를 commit 뒤까지 들고 있으면 detached 로 깨진다 → flush 직후 dict 로 뽑는다.

2026-09-16 — 생성·수집 실패를 잡이 견디게

  • Gemini 호출 실패(429 등)가 온보딩 COPY 잡을 DEAD 로 보내지 않는다. fact 만으로 계속한다 — 키가 없을 때와 같은 동작. 실측: 사진분석 배치가 분당 쿼터를 다 써서 같은 키의 COPY 가 죽었다.
  • 재수집 때 나는 유니크 충돌 로그를 ERROR → WARN(정상 경로인데 오류처럼 보였다).
  • 크롤링 실패를 jobs.result 에 구조화해 남긴다(common/collect_diagnostics.py).
  • Teams 웹훅: 플로우 수신자가 예약값(48:notes)이라 계속 실패 → 플로우 재생성으로 해결.

2026-09-15 — 발행 버전 전환 · 장애 알림 · 보안 · 서치콘솔 · 생성 진행 복구

  • 워커가 렌더하고 버전별로 보관, 게이트 통과 뒤 공개 링크를 바꾼다. 상시 프리렌더를 없앴다. 배포는 기존 HTML과 목업을 다시 굽지 않는다 → PUBLISH_VERSION.md.
  • 장애 알림: alert_outbox + 재시도·중복 억제·복구 알림, /readyz(DB까지 확인) → ALERTS.md. ★ 앱·DB 시계가 수십 ms만 어긋나도 방금 넣은 알림이 안 잡혔다 → 비교는 DB 시계(func.now()). ★ HTTP 202 는 워크플로 접수일 뿐 채널 게시 성공이 아니다.
  • 운영 번들에서 자동 로그인 자격증명 제거(build arg 삭제 + import.meta.env.DEV 가드). users.token_version 으로 비밀번호 변경 시 기존 refresh 토큰을 무효화한다(전에는 7일간 계속 통했다).
  • Google 사이트맵 자동 제출·색인 관측 → SEARCH_CONSOLE.md.
  • 콘텐츠 생성 단계 상태를 DB에 기록, URL의 jobId로 새로고침 복구 → GENERATION_FLOW.md.

2026-09-14 — SNS 게재 · 엽서 · FAQ 20개 · SEO 키워드

  • SNS(스레드) 게재: 사장님 클릭 → fact로 초안 → 승인 → 사장님 계정으로 게시. 이 레포가 처음으로 외부에 쓰고, 남의 자격증명을 보관하고, 되돌릴 수 없는 일을 한다. 설계는 SOCIAL.md. 승인은 POST만, 주소가 확정된(domain) 사이트만, 사진은 올리지 않는다, 기본 꺼짐. X는 URL 글 요청당 $0.20이라 뺐다. ★ server_default=text("'[]',") 쉼표가 CREATE TABLE 을 통째로 실패시켰다(테스트 DB에서만 드러난다).
  • 엽서 쓰기를 발행본에 넣었다. ★ 남의 도메인 사진을 캔버스에 그리면 오염돼 저장·공유가 막힌다 (네이버 CDN은 CORS를 안 준다) → 발행 때 사진을 우리 오리진으로 내려받는 미러로 해결(AGENTS.md).
  • FAQ를 20개까지: fact로 쓰면 4~8개에서 끝나서, 펜션 공통 질문 카탈로그로 "문의 안내" 답을 채운다. ★ 공통 답에 값을 적지 않고, 이 답은 JSON-LD·llms.txt·고유 콘텐츠 계수에서 뺀다.
  • SiteOntology 키워드를 제목·keywords 메타에 싣는다. ★ 추천 10건 중 사실이 아닌 것(마당·복층)이 섞여 와서 "모든 낱말이 이 가게 자료에 있어야" 싣는다(10건 → 4건). 모르는 regionId 는 저쪽이 500을 준다.

2026-09-11 — 발행하면 이 숙소의 노래가 생긴다 (가사 Gemini → 작곡 Suno)

  • 발행이 노래를 기다린다(첫 화면에 기능이 빠져 보이지 않게). 실패해도 발행은 막지 않는다.
  • 가사는 소개문과 같은 재료로 우리가 쓴다 — Suno에 맡기면 없는 시설을 노래한다.
  • ★ Suno 주소는 만료된다 → mp3를 받아 우리 경로로만 내보낸다. 콜백이 아니라 폴링(우리 서버에 닿을 주소가 없다).
  • 미리보기 빌드에는 만들지 않는다(유료 호출).

2026-09-10 — 소개문 승인 단계 제거 · 렌더러 이식 · 일력

  • 생성된 소개문이 영영 안 나가던 것: 소개문이 수집 확인 화면보다 2분 늦게 도착해 승인할 화면이 없었다. LLM 출력은 확인된 fact로만 쓰므로 승인 없이 노출값으로 둔다. 사장님이 고친 문장은 LLM이 못 덮는다 → DECISIONS.md 7절. ★ ORM의 timestamptz 기본값 (now() AT TIME ZONE 'utc') 가 값을 서버 시간대만큼 미래로 밀어 테스트 DB에서 잡이 영영 안 집혔다 → init.sql과 같은 now().
  • /s/stay 시안이 다른 워크트리의 커밋 안 된 작업본에만 있어 렌더러가 갈렸다 → 시안 payload를 현재 렌더러로 다시 구워 태그 단위 diff(129줄 → 4줄). 카카오 길찾기가 상호의 쉼표 때문에 목적지를 버리던 것도 고쳤다.
  • 일력을 서버 생성에 붙였다. 종류 목록이 두 벌이라 새 종류가 서버에 안 갔고, "한 건이라도 있으면 안 부른다" 가드가 기존 지역에 새 종류를 영영 막았다 → 없는 종류만 부른다.

2026-09-09 — 지역 이야기 서버 생성 · 예약 목업

  • 가요·인물·연표·엽서·퀴즈를 지역 단위로 한 번 생성해 같은 지역 사이트가 나눠 쓴다(Perplexity, 출처 필수) → DECISIONS.md 6절.
  • 예약 흐름 목업(StayBookingDemo). 연동 없음 — "마감/잔여"를 지어내지 않고, 시간 후보는 체크인 fact에서만. ★ 날짜는 브라우저에서 만든다 — 서버에서 구우면 발행일 날짜가 HTML에 박혀 크롤러가 지난 날을 읽는다.

2026-09-08 — 가짜 발행 제거 · /s 정본 주소 · 회사(테넌트) 제거 · 네이버 예약

  • 가짜 발행: 사업장이 없으면 서버를 안 부르고 [사이트 열기]를 그렸다(주소는 404). 분기를 지우고 발행 불가 사유를 모달 안에서 말한다.
  • /s 가 빌더 셸을 200으로 주고 있었다 → nginx location = /s + /s/ 301, absolute_redirect off.
  • 회사 스코프를 걷어냈다 — 스코프 키는 places.owner_user_id, 주인은 토큰이 정한다(body로 받지 않는다). 워커의 UserInfo.user_id 는 사업장 주인이어야 한다(랜덤 uuid면 fact가 0건이 된다).
  • 예약 버튼이 검색 화면을 열었다 → 플레이스 응답의 naverBookingUrl 을 수집해 쓴다. 주소를 조립하지 않는다.
  • 내 사이트 목록에 썸네일·주소·시각. 썸네일 주소에 ?v=<버전> 을 붙여 재발행하면 그림이 바뀌게 했다.

2026-09-07 — 자산 보관과 두 번의 사고 · 로컬 설정 함정 · 숙박 예약 안내

  • 옛 해시 자산을 30일 남긴다(대장 .builds.json, mtime 을 쓰지 않는다). 배포와 전체 재굽기를 뗐다.
  • 사고 1: 대장이 없는 첫 실행에서 기존 자산이 전부 "대장에 없음"으로 지워져 운영 CSS가 끊겼다. → 기록이 없으면 입양한다. "기록이 없다"와 "만료됐다"를 같이 묶지 않는다. 검증은 빈 디렉토리가 아니라 배포 직전 서버 모습으로 재현해야 했다.
  • 사고 2: payload 없는 목업(stay·stay2·stay3)의 자산이 지워져 영영 복구 불가가 됐다. → HTML이 참조하는 자산은 기간과 무관하게 남긴다(referencedAssets). 목업의 존재를 AGENTS.md 맨 위에 적었다.
  • 사이트맵 lastmod 를 파일 mtime 에서 뗐다 — 배포마다 전 사이트가 "오늘 갱신"으로 통보되어 구글이 필드를 무시하게 된다.
  • .env.example 함정: 컨테이너 안의 DB_HOST=127.0.0.1(워커만 조용히 재시작), 값 뒤 주석이 값이 됨, API 기본 주소가 크로스 오리진을 만들어 로그인만 실패. → 같은 오리진 기본값, 주석은 윗줄로.
  • 숙박 "실시간 예약" 섹션이 전화번호 한 줄이었다(읽는 fact가 숙박 스키마에 없었다) → "예약 안내"로 이름을 바꾸고 요금·인원·규정·창구를 모았다. 예약을 처리하지는 않는다. availability 는 넣지 않는다.

2026-09-03 — 레포·호스트 교체 · 랜딩 · 로그인 전 검색 · 썸네일

  • 레포 Web4ai/o2o-site-AEO, 호스트 web4ai.o2osolution.ai. ★ origin 은 payload에 구워진다 → 재발행이 필요하다. ★ init.sql 은 최초 생성 때만 돈다 — 기존 DB에 컬럼이 없어 로그인이 죽었는데 HTTP는 200이었다.
  • 로그인 전 랜딩·요금(월 70만원 한 플랜)·쇼케이스(진짜 발행본만). 상호 검색을 로그인 앞으로(인증 없음, IP 제한). 업종은 카카오 카테고리로 정하고 LLM을 부르지 않는다.
  • 썸네일은 스크린샷이 아니라 대표 사진이다(헤드리스 브라우저는 영구 금지).

2026-09-02 — 가입·구글 로그인 · 내 사이트 홈 · 아이템 · 템플릿 모양(look)

  • 계정 생성 API가 아예 없었다(손으로 INSERT). ★ GOOGLE_CLIENT_ID 는 백엔드·프론트가 같아야 하고 aud 대조가 남의 앱 토큰을 막는다. 같은 이메일이라도 계정을 자동으로 잇지 않는다(DECISIONS 1-5).
  • 로그인한 사장님의 홈(/sites·/account). 위저드에서 사이드바를 뺐다. 삭제 대신 [발행 내리기]만 둔다.
  • 붙여넣기 아이템이 발행본에 하나도 안 나가고 있었다(SectionSetting.data 계약이 없었다). 직접 쓴 소개문도 같은 이유로 사라졌다(body). 계약에 넣고 고유 콘텐츠로 센다.
  • SiteTheme.look(서체·모서리·그림자 등)이 발행본까지 가게 했다. 웹폰트는 템플릿이 쓰는 것만.
  • 계절 추천은 HTML에 전 계절을 굽고 브라우저에서 지금 계절만 보인다(구운 시점의 계절이 박히지 않게).

2026-09-01 — 설정을 .env 하나로

toml → pydantic-settings. 키마다 손으로 덮던 구조에서 client_url 이 빠져 배포 주소 API가 전부 CORS로 막혔다. ★ .env 경로가 없는 파일을 보고 있었다. 환경변수 이름은 validation_alias 로 못 박는다(PORT 를 주워 먹는다).

2026-08-31 — 킹서버 최초 배포

포트를 전부 .env 로 뺐다(:80 은 호스트 nginx가 쓴다) → SERVERS.md. ★ PUBLIC_API_BASE_URL 은 브라우저가 부르는 주소다. ★ deploy.sh api 는 worker·api-admin 도 같이 갈아 끼운다.