킹서버에 `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 통과
킹서버(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 라우터 누락.
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
`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