Bing Webmaster 의 XML 파일 방식은 루트에서 파일을 읽는다. nginx `location /` 이
`/srv/app`(= solution-site 이미지)에서 찾으므로 `public/` 에 두고 굽는 것 말고는 자리가 없다.
`docker cp` 로 밀어 넣으면 컨테이너 재생성 때 사라지고, 검색엔진이 인증을 재확인하는
시점에 조용히 풀린다.
- solution/frontend/public/BingSiteAuth.xml: Bing 이 준 파일 그대로(가공하면 파싱이 깨진다)
- docs/DEPLOY.md 2-2절: 세 검색엔진의 소유확인 방식과 사는 자리.
★ 확인은 상태코드가 아니라 내용으로 한다 — `try_files … /index.html` 이라
파일명이 틀리면 404 가 아니라 빌더 HTML 이 200 으로 나간다
검증: 배포 후 curl 로 본문 대조
9b4fe40 이 개발 전용 자동 로그인을 지우면서 빌더 2단계가 막다른 길이 됐다.
로그인한 적도 없는 사람에게 "로그인이 만료되었습니다" 라고 쓰고, 그 화면에는
로그인으로 갈 링크가 없다. 실측(킹서버): 계정이 DB 에 0개라 아무도 통과 못 했다.
- lib/autoSession.ts: 옛 devSession 을 되살리되 `import.meta.env.DEV` 게이트를 뺐다.
운영 번들에서도 돌아야 한다 — 대신 VITE_AUTO_LOGIN_ID·PW 가 **둘 다** 있을 때만
움직이고 기본값은 없다. 진행 중인 로그인을 하나의 약속으로 공유하는 구조는 그대로
가져왔다: 훅 안에만 두면 로그인 전에 누른 검색이 토큰 없이 나가 '만료'로 떨어진다.
- usePlaceSearch: 검색 전에 그 약속을 기다린다(경합을 만료로 오인하던 자리).
- Dockerfile·compose: VITE_* 는 번들에 구워지므로 build args 다. 값을 바꾸면
`./deploy.sh solution-site` 로 다시 굽는다.
⚠️ 계정이 번들에 그대로 들어간다. 내부 테스트 호스트 전용이고, 사장님에게 열기 전에
AUTO_LOGIN_* 을 비우고 재빌드해야 한다.
tsc --noEmit · eslint 통과
킹서버에 `vite dev` 가 떠 있었다. 요청마다 트랜스파일하고, 컨테이너 기동이 npm install
네트워크에 의존하고, /src 원본과 소스맵이 그대로 나간다. 발행물을 파는 사이트의
진입점이 dev 서버일 이유가 없다.
- nginx/Dockerfile 신규: @o2o/frontend 를 굽는 build 스테이지 + 번들을 담은 nginx.
발행 사이트는 이미지에 안 넣는다 — 사이트가 늘 때마다 이미지를 다시 굽지 않으려고
site-out 볼륨에서 읽는다.
- vite.config.ts: build.assetsDir='builder-assets'. 발행본과 같은 오리진이라 `/assets/`
를 서로 뺏는다 — 안 가르면 빌더 JS·CSS 가 404 인데 화면은 떠서 원인이 안 보인다.
- site.conf: `/` 를 /srv/app 정적으로. index.html 은 no-cache — 번들 해시가 여기 박혀
있어 캐시되면 재배포해도 옛 번들 주소를 계속 부른다. `/fonts/` 는 발행본 먼저 보고
없으면 빌더로 떨어뜨린다(둘이 같은 경로를 각자 쓴다).
- compose: solution-frontend(dev 서버)를 profile dev 로 내리고, 굽기만 하는
solution-prerender 를 기본으로 올린다. VITE_* 는 번들에 구워지므로 build args 다 —
주소를 바꾸면 재빌드해야 한다.
docker compose config 통과
개발은 Vite 프록시가 `/s`·`/assets` 를 :3001 로 넘겨 한 오리진을 만드는데, 운영에는 그
몫을 받는 자리가 없었다. site.conf 가 정적 서빙 전용이라 `/`(빌더)와 `/v1`(API)이
전부 404 였다 — 실측(w4ai.o2o.kr): DNS·TLS·프록시는 정상인데 우리 404 페이지만 나왔다.
- nginx/site.conf.example: `/`→solution-frontend:3000, `/v1|healthz|docs`→solution-backend:9800
프록시 추가. `/s/`·`/assets/`·`/fonts/` 는 `^~` 로 잡아 정규식 location 이 못 끼어들게 한다.
업스트림을 변수+resolver 로 둔 건 기동 시점 이름풀이를 피하려는 것 — 프론트가 아직
안 떴을 때 nginx 자체가 죽는다. IndexNow 키 파일(`<key>.txt`)도 루트에서 받는다.
- vite.config.ts: allowedHosts 에 VITE_PUBLISH_HOST. Vite 6 는 모르는 Host 를 403
"Blocked request" 로 막는다 — 프록시는 정상인데 앱만 전부 403 이라 원인이 안 보인다.
nginx -t 통과
배포 서버의 작업트리는 main 의 사본이지 작업 공간이 아니다. pull(--ff-only)은
force-push 가 나면 막히고, 서버에서 손댄 흔적도 남는다.
- deploy.sh: git pull → fetch + reset --hard origin/$BRANCH.
★ fetch 실패 시 리셋하지 않는다. 킹서버엔 gitea 자격증명이 없어 fetch 가 죽는데,
그 상태의 origin/main 은 낡은 ref 다 — 실측 HEAD=9b4fe40 / origin/main=4871e50,
믿고 리셋하면 한 커밋 롤백되고 빌드는 성공한다. 조용히 틀리는 종류다.
- git clean 은 넣지 않는다. .env·nginx/site.conf 는 추적되지 않는 파일이라 clean 이 지운다.
- DEPLOY_BRANCH 로 대상 브랜치를 바꿀 수 있다(기본 main).
킹서버에서 실행 검증: fetch 실패 → 리셋 건너뜀, HEAD 9b4fe40 유지,
solution-site 재생성 정상. bash -n 통과.
킹서버(o2oadmin@172.30.1.36)에 처음 올리면서, 서버에 올려야만 드러나는 결함 넷을 잡았다.
전부 "화면은 뜨는데 안 되는" 종류라 로컬에서는 끝까지 보이지 않는다.
- CORS 허용 오리진(client_url)만 env override 가 없었다. 도커가 굽던 config.local.toml 은
플레이스홀더라 허용 목록이 localhost:3000~3005 뿐이고, 배포 주소에서는 모든 API 호출이
프리플라이트에서 죽었다. 서버 로그에는 400 만 남아 원인이 CORS 라는 게 안 보인다
- .env 경로가 세 단계라 solution/.env(없는 파일)를 보고 있었다. 백엔드를 solution/ 아래로
옮길 때 안 고쳐진 자리. toml 이 값을 들고 있어 로컬에서 드러나지 않았다
- admin 의 "빌더 열기" 가 VITE_SOLUTION_URL 미주입으로 localhost:3000 을 가리켰다
- PUBLIC_API_BASE_URL 은 브라우저가 부르는 주소인데 기본값이 localhost 라 서버에서 즉시 틀린다
설정 — toml 층 제거, pydantic-settings 로 전환 (FastAPI 공식 방식)
- config_loader.py · config.{local,test}.toml.example 삭제, 기본값은 config_models 로
- BaseSettings + env_file. `_apply_*_env_override` 4개 제거 — 키를 손으로 나열하는 구조라
하나 빠뜨리면 조용히 틀렸고, 실제로 client_url 이 빠져 있었다
- 환경변수 이름은 validation_alias 로 못 박음. 필드명만 두면 `port` 가 흔한 `PORT` 를 먹는다
- 테스트 DB 분리(web4ai_test_db)는 config.test.toml 이 하던 몫이라 APP_ENV 기본값으로 이관
- lru_cache 로 .env 재읽기 방지. 새 코드는 Depends(get_*) 주입 가능
- 호출부 21개 파일 무변경 — server_configs 가 같은 이름을 계속 내보낸다
배포 — 킹서버는 :80 을 호스트 nginx 가 물고 있고 사내망에 열린 건 30xxx 뿐이다
- 컴포즈 포트를 전부 .env 변수로 추출(기본값은 기존 값 그대로, 로컬 무영향)
- 컨테이너 이름을 폴더 구조에 맞춤 — solution-backend·solution-worker·solution-frontend·
solution-site·admin-backend·admin-frontend. api·web·nginx 는 어느 폴더 코드인지
이름만으로 알 수 없었고, 백엔드 셋이 이미지 한 벌을 나눠 써서 특히 헷갈렸다
- worker 에 container_name 을 붙여 `-1` 접미사 제거(동시성은 WORKER_CONCURRENCY 가 맡는다)
- 어드민 앱·API 는 compose 프로필 뒤로 — 지금 안 쓴다. 켤 때 --profile admin
- deploy.sh: 서비스 하나를 지정해도 백엔드 형제를 함께 교체한다. 이미지 한 벌을 나눠 써서
하나만 바꾸면 옛 코드로 도는 컨테이너가 남는데 `ps` 로는 셋 다 살아 있다
- log.sh: 1=전체, 2번부터 개별. compose v2.20 이 커스텀 --format 을 파싱하지 못해 상태가
전부 "미기동" 으로 보이던 것도 --services --filter 로 교정
- docs/SERVERS.md 신설(접속·경로·포트·DB·sudo 없음), docs/DEVLOG.md 신설
정리
- 개발 전용 자동 로그인 제거 — 편의 하나에 검색 경로의 비동기 대기가 딸려 있었고,
평문 비밀번호를 .env 에 두라고 권하는 모양새였다
- API 이름을 디렉토리에 맞춤: 사장님/내부 → 솔루션 API · 어드민 API (21곳)
- .env.example 을 읽는 폴더 기준 구역으로 재편 (solution/backend · solution/frontend ·
solution/site · compose)
- AGENTS.md 에 negosium 브랜치·커밋 규약 명시
검증(킹서버 실측) — 컨테이너 4개 새 이름으로 기동, 솔루션 API·사장님 앱 200,
발행 사이트 404(발행물 없음, 정상), CORS 허용/차단 각 확인, toml 없이 부팅,
APP_ENV=test 시 web4ai_test_db·실키 미주입 확인.
지운 것 — 앞으로의 개발에 쓸 데가 없다.
- solution/backend/demo_site.html: 어떤 스크립트도 만들지 않는 고아 산출물이고,
손으로 쓴 HTML 이라 "백엔드는 HTML 을 만들지 않는다" 와도 어긋난다.
- solution/README.md: front/ · admin/.env · 사이트당 rooms/index.html·sitemap.xml 처럼
지금은 전부 틀린 서술이었다. 살아 있는 두 가지(빌더 CSR vs 발행물 SSG 대비표,
하이드레이션 블롭에 미검증 값이 샜던 실측)는 ARCHITECTURE 로 옮겼다.
- docs/API_USAGE.md 의 Claude 개발비 집계: 2026-08-27 스냅샷과 재집계 스크립트는
일회성 지출 기록이라 제품 원가와 성격이 다르다. 문서를 외부 API 원가 하나로 좁혔다.
고친 것 — 코드를 따라가지 못하던 서술.
- 코드 경로가 solution/backend 로 옮겨진 뒤 `backend/...` 로 남아 있던 포인터 전부.
가리키는 자리가 없는 경로는 문서가 아니라 함정이다.
- ARCHITECTURE: 트리의 front→frontend, 컨테이너 표에 api-admin(:9801)·admin(:3002) 추가.
- ★ ARCHITECTURE·AGENTS 의 "admin 전용 라우터가 0개" 는 사실이 아니었다.
/v1/admin/local-content 가 admin 전용인데 :9800 에도 마운트돼 있다 —
포트를 가른 논리에 아직 남은 구멍이라 그렇게 적었다.
- DECISIONS: 결론난 것을 미결로 두면 함정이 된다. 작업 큐(2026-08-27 결론),
날씨 캐시 TTL 1시간, jobs 테이블, media 조회 API, 수집 체인을 결론으로 옮기고
네이버 플레이스 대 TourAPI 실측(2026-08-31)을 1-1 에 이었다.
- API_USAGE: 어댑터가 다 붙고 TourAPI 키도 나왔다. "실호출 0건" 은 낡은 서술이었다.
- backend/README: 16→17 테이블(jobs), 없어진 alters/, MockAdapter 만이라는 서술,
cd backend 경로, media·local 라우터 누락.
- 주소표를 갈랐다. 내부 화면(:3002)·내부 API(:9801)가 127.0.0.1 에만 열린다는 걸 적었다.
- 클론 직후 `cp nginx/site.conf.example` 을 넣었다 — 빼먹으면 Docker 가 그 자리에
디렉토리를 만들어 nginx 가 설정 없이 뜬다.
- npm 워크스페이스 루트가 레포 루트라는 것, 스키마가 init.sql 한 벌이라는 것.
- 레포 구조를 admin/{backend,frontend} 까지 반영.
- 문서 지도에 DEVELOPMENT_DIRECTION 이 두 줄 있었다. "한 사실은 한 곳에"를 이 표가
스스로 어기고 있었다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
admin/ 이 프론트 파일만 널려 있는 폴더였다. :9801 을 띄우는 코드도 solution/backend 안에
얹혀 있어서, 폴더만 봐서는 admin 에 백엔드가 있다는 걸 몰랐다.
admin/backend/ main.py · app.py (:9801 진입점. 도메인 코드는 PYTHONPATH 로 solution/backend)
admin/frontend/ 운영 화면
패키지 이름도 폴더에 맞췄다: @o2o/front → @o2o/frontend.
이미지 빌드 컨텍스트를 레포 루트로 올렸다 — 진입점(admin/)과 도메인 코드(solution/)가
한 이미지에 들어와야 한다. 루트 .dockerignore 로 프론트·문서·시크릿을 잘라냈다.
★ 실측으로 잡은 것: 패키지명을 바꾸면서 compose 의 `-w @o2o/front` 를 안 고쳐
web 컨테이너가 `No workspaces found` 로 재기동 루프에 빠져 있었다.
주석은 짧게 줄였다.
검증: lint·build 6개 전부 0. 컨테이너 5개 엔드포인트(9800·9801·3000·3002·80) 전부 200.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
admin/ 이 프론트만 있는 폴더였다. :9801 을 띄우는 코드는 solution/backend 안에
admin_main.py 로 얹혀 있었는데, 그러면 폴더만 봐서는 admin 에 백엔드가 있다는 걸 모른다.
solution/backend/admin_main.py → admin/backend/main.py
solution/backend/router/admin_router.py → admin/backend/app.py
도메인 코드는 여전히 복제하지 않는다 — `PYTHONPATH=/app/solution/backend` 한 줄이
두 폴더를 잇는다. admin/backend 에 있는 건 진입점 두 파일뿐이다.
## 이미지 빌드 컨텍스트를 레포 루트로 올렸다
진입점이 admin/ 에, 도메인 코드가 solution/ 에 있어서 한 이미지에 둘 다 들어와야 한다.
나누면 requirements 를 두 번 설치하게 된다. 컨텍스트가 넓어진 만큼 루트 .dockerignore 로
프론트·문서·테스트·시크릿을 잘라냈다(옛 solution/backend/.dockerignore 대체).
이미지 배치:
/app/solution/backend ← 도메인 코드. api·worker 의 working_dir
/app/admin/backend ← 내부 API 진입점. api-admin 의 working_dir
검증: api·api-admin 둘 다 healthy, 다섯 엔드포인트(9800·9801·3000·3002·80) 전부 200.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
admin 이 사장님 API(:9800)를 그대로 보고 있었다. 화면만 갈라 두면 내부 요청이
사장님이 닿는 서버로 나가고, 권한도 엔드포인트마다 흩어진 채로 남는다.
## 코드는 한 벌, 진입점만 둘
web_main.py → router/router.py :9800 사장님
admin_main.py → router/admin_router.py :9801 내부
services·crud·models 은 공유한다. **admin 전용 라우터가 0개**이기 때문이다 — 세어봤다:
admin 화면이 부르는 훅(useGetPlace·useListPlaces·useListLinks·useConfirmLink·
useListFacts·useGetSchema·useTransitionFact)이 전부 place·fact 라우터이고,
그 둘은 사장님 빌더도 쓴다. 엔드포인트를 새로 쓰면 같은 DB 의 같은 테이블을 두 벌
구현하는 것뿐이라, 같은 router 객체를 다시 마운트하고 앱 단위로 권한만 덧걸었다.
## 왜 경로 접두어가 아니라 포트인가
/v1/admin/... 는 같은 프로세스 안이라 **사장님이 닿는 서버에 내부 엔드포인트가 존재한다.**
포트를 가르면 사장님이 닿는 네트워크에 아예 없다. compose 에서 이 포트는 127.0.0.1
에만 연다(ADMIN_API_BIND) — 0.0.0.0 으로 열면 가른 의미가 없다.
## 권한
RequireDeveloper 를 앱 단위로 건다. auth 라우터만 게이트 밖이다 —
로그인 자체를 막으면 아무도 들어올 수 없다.
검증 /v1/place/list : USER(1) 403 · OWNER(2) 403 · DEVELOPER(3) 200
OWNER 가 막히는 게 핵심이다. 자기 회사 최상위일 뿐 남의 회사를 볼 권한이 아니다.
## 그 밖
- 이미지의 HEALTHCHECK 는 :9800 을 찌른다. 그대로 두면 이 컨테이너가 멀쩡히 돌면서
영원히 unhealthy 라, 포트만 바꿔 다시 걸었다.
- compose 주석에 negosium-db 가 나오는 이유를 적었다 — 베낀 흔적이 아니라 DB 인스턴스를
따로 안 띄우고 그 postgres 안에 web4ai_db 만 만들어 쓰기 때문이다(DECISIONS.md 3절).
줄이면서 이유를 날려 읽는 사람이 오해하게 만들었다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
`backend` 옆에 `front` 가 있을 이유가 없었다. negosium 의 negodata/front 를 그대로
베꼈고 그게 왜 front 인지는 따져보지 않았다 — 근거 없이 들여온 이름이라 바로잡는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
서버 이름(O2oSiteServer)과 orval 생성 그룹 키(o2oSite)가 옛 이름 그대로였다.
★ localStorage 키(`o2osite.builder.wizard.v*`)는 바꾸지 않는다. 그건 우리가 정하는
이름이 아니라 **이미 사용자 브라우저에 들어가 있는 옛 저장분의 이름**이라, 바꾸면
지워야 할 것을 못 지운다. 왜 그대로 두는지 주석으로 남겼다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
최상단을 프로젝트 단위로 평평하게 둔다 — o2o-negosium 과 같은 규약이고, 이 레포만
다르게 갈 이유가 없다. negodata/{backend,front} 가 프로젝트 안에서 f/b 를 가르는 선례,
lps-admin/ 이 백엔드 없이 프론트만 가진 최상단 폴더의 선례다.
backend/ frontend/{admin,site,shared} → solution/{backend,front,site,shared} + admin/
## 왜
내부 라우트(/local-content, /places/:id/seo)의 이름과 화면 코드가 사장님 번들에
그대로 실려 나가고 있었다. UserRole.DEVELOPER 주석의 "고객사에 존재를 노출하지 않는다"를
번들이 깨고 있었다 — 라우트 가드는 화면을 가리지 번들은 못 가린다.
번들을 갈라 확인했다: 사장님 dist 에서 local-content · /places · SeoAudit 이 전부 0건이다.
그 과정에서 두 곳이 더 새고 있었다.
- AppShell 의 NAV 배열이 내부 메뉴를 하드코딩하고 있었다. 앱을 가른 뒤에도 dist 에
local-content 가 남아서 찾았다. 메뉴는 이제 앱이 prop 으로 들고 온다.
- EditorHeader·BuilderPage·LoginPage 가 /places 로 링크하고 있었다. 그 화면이 admin 으로
나갔으니 사장님 앱에서는 404 다. 링크를 걷어내고 LoginPage 기본 도착지는 '/' 로 바꿨다
(앱마다 홈이 다르고 각 라우터의 '/' 가 이미 그걸 안다).
## admin 에 백엔드를 두지 않았다
내부 화면이 부르는 훅이 전부 router/v1/{place,fact,local,validator} 에 이미 있다.
자체 백엔드를 두면 place·fact·link 를 같은 DB 에 대고 두 번 구현하게 된다.
대가는 solution/backend 가 죽으면 admin 도 멈추는 것 — 내부 도구라 감수한다.
## admin 의 `@` 는 solution/front/src 를 가리킨다
내부 화면이 쓰는 API 클라이언트·UI·수집 배선이 solution 에 한 벌만 있고 그 파일들끼리도
`@/...` 로 서로를 부른다. admin 에서 `@` 를 자기 src 로 잡으면 그 참조가 전부 깨진다
(실측 TS2307 14건). 복제하는 길도 있지만 RecollectPanel 주석이 금지한다 —
"수집 경로를 두 벌 만들면 확정 게이트"가 갈라진다.
admin 자기 파일만 `@admin` 이고, 의존 방향은 admin → solution 한 쪽뿐이다.
admin 이 여는 빌더는 다른 오리진이라 절대 URL + 새 탭이다(admin/src/lib/solutionUrl.ts).
react-router Link 로 두면 admin 안에서 라우트를 찾다 404 다.
## 그 밖
- npm 워크스페이스 루트를 레포 루트로 올렸다(admin 이 solution 밖이라).
- docker-compose 를 255→174줄로 줄이고 admin(:3002) 서비스를 넣었다. ADMIN_BIND 기본값은
127.0.0.1 — 0.0.0.0 으로 열면 앱을 가른 의미가 없다.
- 발행 호스트를 프론트 .env 에 따로 적지 않는다. compose 가 루트의 SITE_PUBLIC_HOST 를
VITE_PUBLISH_HOST 로 흘려보낸다 — 두 곳에 적으면 canonical 과 화면 주소가 조용히 갈라진다.
- nginx/site.conf 를 git 에서 빼고 .example 만 남겼다(.env·*.toml 과 같은 규약).
compose 가 bind mount 하므로 클론 직후 복사해야 한다 — 없으면 Docker 가 그 자리에
디렉토리를 만들어 nginx 가 설정 없이 뜬다.
- config.test.toml.example 을 추가했다. 없으면 클론한 사람이 pytest 를 아예 못 돌린다
(conftest import 단계에서 죽는다). 외부 API 키는 전부 빈값이다 —
APP_ENV=test 가 .env 를 안 읽는 이유를 여기서 우회하면 안 된다.
- 경로가 한 칸 깊어져 test_schema_ddl(parents[2]→[3]) 과 test_site_theme 을 고쳤다.
검증: front·admin·site 전부 lint 0 / build 0. 백엔드 514 passed.
남은 4건(test_build_publish 3 · test_snapshot 1)은 이 변경 전부터 실패하던 것으로,
손대지 않은 메인 체크아웃에서 같은 4건이 같게 실패하는 것을 확인했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
alters/*.sql 은 **기존 DB 를 보정**하는 파일이다. 그런데 이 프로젝트는 아직 git 에도
서버에도 올라가지 않아 보정할 기존 DB 가 없다. 파일마다 주석이
"신규 DB 는 init-data/init.sql 에 반영돼 있어 이 파일이 필요 없다"고 이미 적고 있었다.
지우기 전에 init.sql 이 여섯 개를 전부 담고 있는지 확인했다 —
local_contents 발행 컬럼 4개, site template_id·theme, external_place_id/external_source,
그리고 allow-duplicate-places 가 지우는 두 인덱스가 애초에 없다는 것까지.
운영 DB 가 생기는 순간 이 디렉토리는 다시 필요해진다. 그때 되살린다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
site.conf 는 서버 환경에 딸린 파일이라 커밋 대상이 아니다. 이 레포가 .env·*.toml 에
이미 쓰고 있는 규약(*.example 만 커밋)을 그대로 적용한다.
★ 그냥 지우면 안 되는 이유를 .gitignore 주석에 적었다 — compose 가 이 파일을
bind mount 하는데(docker-compose.yml:224), 파일이 없으면 Docker 가 그 자리에
디렉토리를 만들어 nginx 가 설정 없이 뜬다. 클론 직후 example 을 복사해야 한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
ARCHITECTURE.md 4절이 권고하던 `frontend/{console,admin,site,shared}` 는 사내 다른
레포(o2o-negosium)의 규약과 어긋났다. 이 레포만 다르게 갈 이유가 없어 최상단을
프로젝트 단위로 평평하게 두는 쪽으로 바꾼다 — solution/{backend,front,site} + admin/.
admin 에 백엔드를 두지 않는 근거를 실측으로 적었다: 내부 4장이 부르는 훅이 전부
router/v1/{place,fact,local,validator} 에 이미 있어 새로 만들 게 없고, 자체 백엔드를
두면 같은 DB 에 대고 도메인을 두 번 구현하게 된다. negosium 의 lps-admin 이
프론트 전용 최상단 폴더의 선례다.
shared/ 를 없애는 근거도 적었다 — 그것이 지키던 결합(슬러그 규칙, SitePayload)의
양쪽이 둘 다 solution/ 안에 들어가므로 공용 워크스페이스가 필요 없어진다.
"admin/ 이 두 앱을 반씩 겸한다"는 서술도 고쳤다. 실제로는 사장님 110파일 대
내부 814줄이라, 사실상 사장님 앱에 내부 화면 4장이 얹힌 모양이다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
최초 커밋에서 CLAUDE.md 가 빠져 있었다. 레포 .gitignore 에서는 뺐지만
이 머신의 ~/.gitignore_global 5번 줄이 CLAUDE.md 를 전역으로 무시한다.
전역 설정은 사람마다 달라 레포가 의존할 수 없으므로 `!CLAUDE.md` 로
레포가 스스로 되살린다.
내용 자체는 AGENTS.md 로 이미 커밋돼 있었다 — 빠진 건 링크뿐이다.
git 저장소가 없어 히스토리·협업 기반이 아예 없던 상태를 연다.
함께 문서를 재편했다. 그동안 문서가 있어도 "이 제품이 뭘 푸는가"와
"어떻게 도는가"를 담은 문서가 없어서, 목표 문장이 backend/frontend
README 두 곳에 복붙돼 있었다 — 상위 문서가 없어 아래로 샌 것이다.
신설
README.md 레포 진입점 + 문서 지도 + 문서 규칙 4가지
AGENTS.md 에이전트·신규 합류자용 함정 목록과 규약
(CLAUDE.md 는 여기로 걸린 심볼릭 링크)
docs/PRODUCT.md 제품 정의 — 문제·사용자·원칙·**non-goals**·성공 기준
docs/ARCHITECTURE.md payload 경계·발행 파이프라인·서빙 결정·앱 분리 설계
이동
backend/docs/DECISIONS.md → docs/DECISIONS.md
백엔드만의 결정이 아니다. 게다가 코드 주석 ~25곳이 이미
`docs/DECISIONS.md` 로 적고 있어 레포 루트 기준으로는 그게 맞다.
갱신
docs/DEPLOY.md 서빙 결정 반영 — nginx 정적 서빙이 지금 경로(3절),
Azure 는 나중에 켤 때(4절)로 분리
docs/ARCHITECTURE.md 사이트 = 한 장(2026-08-31) 구조 반영
docs/COLLECTION_SEO_AEO_FLOW.md
robots.txt·sitemap.xml 은 오리진 루트에만 굽는다는 점 명시
frontend/site/scripts/prerender.ts
헤더 주석의 렌더 보고서 경로가 실제(422줄)와 달라 수정
.gitignore
★ CLAUDE.md 를 더 이상 무시하지 않는다. 에이전트 지침은 팀과 모든
에이전트가 공유하는 규약이라 커밋해야 한다 — 무시하면 클론한 사람이
"배포 후 republish_all.py 필수" 같은 함정을 전달받지 못한다.
개인용 오버라이드는 ~/.claude/CLAUDE.md 에 둔다.