Commit Graph

11 Commits

Author SHA1 Message Date
238d4c25c4 [feat] solution: 네이버 플레이스를 검색 단계에서 찾는다 — 로그인은 수집 직전 한 번
사장님이 후보를 고른 뒤에도 "네이버 플레이스를 자동으로 찾지 못했습니다" 가 떴다.
찾을 수 있는데도 그랬다 — 자동 발견이 확정 경로(로그인 뒤)에만 있었고, 공개 검색은
상호·주소만 돌려줬다. 그리고 확정이 사업장 생성을 요구해서, 로그인 없이 시작하기로 한
위저드가 검색 직후부터 막혔다(자동 로그인이 그걸 가리고 있었다).

- place_service.search_places_public: 응답에 naver_place_url 을 싣는다. 넓은 검색어
  한 페이지에서 못 찾은 후보는 그 후보만 겨냥해 다시 찾는다(상위 2건, 429 회피).
  실측: 12개 상호 전부 발견. 전에는 4개 중 2개
- naver_place_lookup._region_hint: 주소에서 시·군·구까지만 뽑아 검색을 좁힌다.
  첫 토막('경기도')만 쓰면 **다른 동네 동명 업소**가 잡히고, 그 id 로 검증하면 남의
  가게가 이 사이트의 기준 정보가 된다 — 실측으로 한 번 겪었다
- ttl_cache(신규) + 공개 검색 10분 캐시: 검색 1회가 네이버를 최대 3번 긁는데 인증이
  없어 새로고침만으로 나간다. 실측 1.38s → 0.005s. **빈 결과는 캐시하지 않는다** —
  일시적 0건을 굳히면 사장님이 10분간 막힌다
- 확정은 서버를 부르지 않는다(usePlaceSearch). 화면에만 남기고, 수집 직전 로그인 뒤
  ensureServerPlace 가 생성 → 검증을 한 번에 한다. 나눠 두면 "사업장은 생겼는데 검증이
  빠진" 상태가 생기고 수집이 PLACE_NOT_VERIFIED 로 조용히 거절된다
- Step3: 수집 버튼이 로그인 모달을 연다(/login 으로 튕기지 않는다 — 위저드 상태가
  주소창에 없어 돌아올 길이 없다). 로그인하면 이어서 돈다
- Step2: 후보 카드에 '네이버 플레이스 찾음' 배지. 붙여넣기 칸은 접는다 —
  펼쳐 두면 시도도 전에 실패한 것으로 읽힌다. 뒤로 오면 처음 화면으로
- ChannelUrlInput: [추가] → [이 주소로 가져오기]. 로그인 전에는 addLink 가 placeId 가
  없어 **조용히 return** 해서 입력칸만 비워졌다(useChannelLinks.ts:37)
- LoginPage: admin/1234 기본값 제거. 배포 번들에 그대로 나가 있었다
- 기본 발행 호스트를 localhost 로(compose 4곳 · site_payload.DEFAULT_HOST · .env.example).
  운영 도메인을 기본값으로 두면 .env 를 안 채운 로컬 빌드가 조용히 운영 주소를 번들에
  굽는다 — 실측: 로컬에서 만든 링크가 킹서버로 갔다. localhost 는 http 로 조립한다

검증: tsc·eslint·vite build 통과. 브라우저로 전 구간 확인(검색 → 확정 → 로그인 →
자동 발견 → 검증 → 수집 fact 27건·사진 10장·메뉴 23건 → 사진 분석).
백엔드 테스트는 이 워크트리에서 못 돌렸다 — config.test.toml 이 없어 DB 인증이 실패한다.
2026-09-03 16:37:12 +09:00
a61f9724ea [feat] solution/frontend: 온보딩을 상호명부터 — 단계를 주소창으로 옮기고 업종은 검색이 정한다
업종을 먼저 고르게 하면 경계에서 멈춘다("우리는 카페인가 음식점인가"). 그런데 상호명은
100% 안다. 그리고 업종은 AI 를 부를 필요가 없다 — 카카오·네이버 검색 응답에 분류가
이미 들어 있고(category_group_code), 지금까지 받아 놓고 안 썼다.

- 단계가 스토어에서 주소창으로: ?step=search|industry|collect|template|generating|editor.
  번호가 아니라 이름인 이유 — 단계가 4→3 으로 줄어 옛 북마크가 다른 화면을 연다
- 시작점이 상호명 검색(Step2PlaceSearch)이다. 업종 선택은 못 정했을 때의 갈래로 남는다
- 업종은 후보의 category 로 잡히고, 못 정하면 고르게 하고, 정해져도 [바꾸기] 로 바꾼다.
  ★ 확정 뒤 변경은 신원 확인부터 다시 받는다 — Req_UpdatePlace 에 category 가 없어
    PATCH 로 못 고치고, placeAdapter 가 리페치마다 덮어써서 조용히 되돌아간다
- 랜딩 진입: ?new=1 · ?q=<상호명> · ?industry=<업종>. 한 번 읽고 replace 로 지운다
- ★ ?new=1 이 setSearchParams({}) 로 **모든 쿼리를 날리던 것**을 고쳤다 — q 가 읽히기 전에 사라졌다
- selectIndustry 가 사장님이 친 상호·위치를 지우던 것도 고쳤다(업종이 첫 화면일 땐 늘 빈 값이라 안 보였다)
- 로고는 어디서나 / 로 간다. 에디터에서는 span 이라 아예 안 눌렸다

tsc·eslint·vite build 통과
2026-09-03 10:20:57 +09:00
5ef3e5a7de 업종 4번째를 관광체험 → 피부과·성형외과 로 바꾸고, 로그인 관문을 에디터 진입으로 되돌린다
## 업종 교체 (tour → clinic)

PlaceCategory 코드 4번의 의미를 바꾼다. 아직 배포 전이라 데이터 마이그레이션은 없다.

- category_schema: tour_activity.json → clinic.json. 체험 스키마(안전 유의사항·우천 시
  운영·준비물)를 진료 스키마(진료과목·의료진·상담료·보험 적용·야간/주말진료)로 바꿨다.
  unit 은 프로그램 → 시술이다(마취 방식·회복 기간·권장 횟수·시술 후 주의사항).
- 소개문 계열만 allow_llm 이다. 시술 효과·비용 같은 값은 LLM 이 못 쓴다 —
  이 레포의 "검증 전에는 발행 금지" 규칙이 의료 문구에서 특히 중요하다.
- jsonld: TouristAttraction → MedicalClinic. 프론트 AeoReadiness 의 같은 표도 맞췄다.
- 색 팔레트를 병원 톤(클린 블루·세이지·누드·모노)으로, 아이콘을 Compass → Stethoscope 로.
- mock_adapter 목데이터를 시술 기준으로 교체. 스키마에 없는 key 를 쓰면 수집이 죽는다.
- site_payload 의 기본 섹션표를 에디터(industryData)와 같게 맞췄다 —
  test_site_theme 이 이 둘을 대조한다.

## 로그인 관문 되돌리기 (b94daa9·d6a6c8e revert)

두 커밋이 /builder 를 통째로 RequireAuth 뒤로 옮겨 `/` 가 곧바로 로그인 화면이 됐다.
`/` 는 자기 화면 없이 /builder 로 넘기기만 하므로, 문 앞 가드는 곧 루트 가드다.
위저드를 열어 두고 에디터 진입에서 한 번 받는 969fb67 설계로 되돌린다.
d6a6c8e 가 스스로 "969fb67 과 정면으로 다른 설계"라고 적어 두었다.

## 그 밖

- test_site_theme 의 경로가 solution/front 로 남아 있었다(frontend 개명 누락).
- .dockerignore: 이 머신에 buildx 가 없어 레거시 빌더가 돌고, 그러면
  nginx/Dockerfile.dockerignore 가 무시된다. 루트 것 하나로 두 이미지를 다 커버한다.

검증: frontend·admin·site lint·build 0. 백엔드 534 passed / 4 failed —
그 4개(test_build_publish 3 · test_snapshot 1)는 이 변경 전부터 실패하던 것이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xa8ME5FQJy4VA8pPokTo1a
2026-09-02 15:42:34 +09:00
b94daa9dcf [refactor] solution/frontend: 로그인 관문을 문 앞 하나로 — 에디터 관문과 2단계 우회로 제거
같은 날 두 자리에서 같은 문제를 풀어 관문이 두 겹이 됐다. 둘 다 두면 문 앞(RequireAuth)이
먼저 걸려 에디터 관문은 영영 안 뜨는 죽은 코드다. 문 앞을 남긴 이유는 열어 둔 값이
공짜가 아니었기 때문이다.

에디터 관문을 쓰려면 2단계가 토큰 없이 지나가야 했고, 그래서 토큰이 없을 때 서버를 부르지 않고
입력값으로 신원을 세우는 우회로가 생겼다(confirmManual). 그건 이 레포의 단 하나의 규칙
— 검증 전에는 수집·발행 금지 — 을 화면이 비켜 가는 모양이고, 대가는 "로그인 뒤에 검증을 다시"다.
게다가 가입이 이제 그 자리에서 끝나므로(가입 응답에 토큰이 실린다) 문 앞 로그인의 마찰은
"만들어 보기도 전에 막는다" 던 시절보다 훨씬 작다.

- features/auth/EditorSignInGate·SignInForm 삭제. 관문이 하나면 폼도 하나다 —
  SignInForm 이 경고하던 'signIn → me 를 두 벌로 들고 있다' 를 lib/session 한 곳으로 모았다
- Step2PlaceSearch: 토큰 없을 때 검증을 건너뛰던 두 갈래 제거
- usePlaceSearch: confirmManual 제거. 토큰이 없으면 이제 진짜 '만료'다(문 앞을 통과했으므로)
  — 문구를 사실대로 되돌린다
- BuilderPage: 에디터 진입 분기 제거

되돌리려면 app/router 의 RequireAuth 를 벗기고 969fb67·22b7623 을 되살리면 된다.

tsc·eslint·vite build 통과(사장님 앱·admin).
2026-09-02 09:41:28 +09:00
22b7623aeb [fix] solution/frontend: 2단계에서 로그인을 요구하지 않는다
로그인은 에디터 진입에서 한 번 받는 것이 이 앱의 흐름인데, 장소 API 가 전부 토큰을 요구해서
(place.py) 2단계가 로그인 벽이 되어 있었다. 토큰이 없으면 서버를 부르지 않고 입력한 상호·주소로
신원을 세워 3단계로 넘어간다. 검증은 로그인 뒤에 다시 할 수 있다.
2026-09-02 09:20:55 +09:00
139a672138 [fix] solution/frontend: 2단계 안내를 사실대로 — 없는 직접 입력을 하라고 말하고 있었다
직접 입력 폼을 되돌리면서 안내 문구는 그대로 뒀다. 화면에 없는 걸 하라고 말하는 상태였다.
지도 검색·URL 확인 모두 서버가 토큰을 요구하므로(place.py 전 엔드포인트 IsValidAccessToken)
이 단계는 로그인 없이 지나갈 수 없다. 문구를 그대로 적는다.
2026-09-02 09:14:16 +09:00
969fb6773c [feat] solution/frontend: 로그인을 에디터 진입에서 한 번만 받는다
위저드를 걷는 동안 '로그인이 만료되었습니다' 가 떴다. 만료가 아니라 **한 번도 로그인한 적이
없는 것**이었다(자동 로그인 계정이 없으면 ensureAutoSession 이 즉시 끝난다). 문구가 사실과 달라
고장으로 읽혔다.

- features/auth/EditorSignInGate: 에디터에 들어갈 때만 로그인을 받는다. 위저드 1~5단계는
  요구하지 않는다 — 만들어 보기도 전에 막으면 아무도 안 만든다. /login 으로 튕기지 않는 이유는
  위저드에서 쌓은 상태를 들고 돌아올 방법을 사장님이 알 수 없기 때문이다
- features/auth/SignInForm: 로그인 화면과 관문이 같은 폼을 쓴다. 두 벌로 두면 토큰을 심는
  순서(signIn → me)가 한쪽에서만 지켜지고, 그 실수는 "로그인은 됐는데 계속 401" 로 나타난다
- usePlaceSearch: 토큰이 없을 때의 문구에서 '만료' 를 걷어낸다. 검색은 서버가 토큰을 요구하므로
  (place.py 전 엔드포인트가 IsValidAccessToken) 프론트가 없앨 수 있는 제약이 아니다

tsc·eslint·vite build 통과
2026-09-02 09:03:17 +09:00
4debeaed3a [feat] solution/frontend: 템플릿이 색만 바꾸던 걸 끝낸다 — 업종당 5개에서 3개로
업종마다 템플릿이 다섯인데 넷이 "흰 바탕 + 고딕 + 둥근 모서리"에 색조만 달랐다. 고르는 화면의
미리보기도 회색 막대 세 줄 + 색 동그라미라 다섯 장이 전부 같은 그림이었다 — 뭐가 다른지 알 수
없으니 아무거나 골랐다.

- shared/TemplateItem.look: 제목·본문 서체, 모서리, 테두리 두께, 그림자, 제목 자간·굵기, 섹션 여백.
  CSS 에 그대로 들어가는 문자열로 들고 있다 — 숫자로 두면 쓰는 쪽에서 단위를 빠뜨린 곳이 조용히 0 이 된다
- CanvasView: Tailwind v4 의 --radius-* · --shadow-* 를 캔버스 안에서만 덮는다. 변이 파일 40여 개에
  흩어진 rounded-* · shadow-* 를 한 줄도 안 고치고 전부 템플릿을 따르게 된다. 배수는 Tailwind
  기본 비율 그대로라 기준값 0.75rem 이면 지금까지와 픽셀 단위로 같다
- index.css: .site-canvas 는 --tpl-font-heading/body 를 이미 읽고 있었는데 아무도 넣지 않았다.
  서체가 안 갈리던 진짜 이유가 이 빠진 고리다. 제목 굵기도 토큰으로 뺐다 — 간판체(Gugi)는 굵기가
  한 벌뿐이라 700 을 주면 브라우저가 가짜 볼드를 씌워 획이 뭉갠다
- industryData: templatesFor() 팩토리 하나로 심플·매거진·레트로 셋. 업종은 accent 하나만 바꾼다 —
  생김새는 업종이 아니라 취향의 문제다. 537줄 → 237줄
- Step4Template: 미리보기를 그 템플릿의 서체·모서리·테두리·그림자로 실제로 그린다
- index.html: Noto Serif KR 추가. 매거진 제목이 Batang 으로 떨어지는데 맥에는 그 서체가 없다
- SectionFrame: 세로 여백을 --tpl-section-space 로. 이 값 하나로 페이지의 호흡이 바뀐다

옛 템플릿 id 가 DB 에 남아 있어도 resolveTemplate 이 첫 템플릿으로 떨어뜨린다.

tsc·eslint·vite build 통과(frontend·admin·site), 템플릿 12벌 look 전량 대조 + 폴백 확인
2026-09-02 08:49:38 +09:00
db58e94540 [feat] solution/frontend: 자동 로그인 복원 — 계정이 주입돼 있을 때만 붙는다
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 통과
2026-09-01 12:00:07 +09:00
9b4fe4030b [feat] deploy,backend,site: 킹서버 배포 + 설정을 최상위 .env 하나로 통합
킹서버(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·실키 미주입 확인.
2026-09-01 10:04:36 +09:00
c85c577349 이름: solution/front → solution/frontend
`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
2026-08-31 15:27:16 +09:00