o2o-site-AEO/frontend/admin/src/api/README.md
Mina Choi 6784e59ca5 최초 커밋 — 기존 코드 전체 + 문서 체계 신설
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 에 둔다.
2026-08-31 13:57:59 +09:00

3.0 KiB

api

api/
├── index.ts              ★ 화면이 import 하는 단 하나의 입구
├── generated/            orval 산출물 — 손대지 않는다
├── mutator/custom-fetch.ts  모든 호출이 지나는 길목(토큰·에러·baseURL·434 재발급)
└── pollJob.ts            잡 폴링(수집·비전·생성·빌드 공용)

화면은 @/api 하나만 본다.

import {useListPlaces, useTransitionFact, startBuild, pollJob} from '@/api';
import type {PlaceData, FactStatus} from '@/api';

재생성

백엔드 OpenAPI 가 바뀌면 다시 뽑는다. generated/ 를 손으로 고치지 않는다.

# 백엔드가 떠 있을 때
npm run orval -w admin

# 서버 없이 (스펙 파일을 먼저 뽑는다)
cd backend && python scripts/export_openapi.py
cd ../frontend && ORVAL_INPUT=../../backend/openapi.json npm run orval -w admin

생성되는 것 — 태그(도메인)별 훅과 모델.

태그 훅
auth useLogin useRefreshToken useMe useUpdateMe
place useListPlaces useCreatePlace useGetPlace useUpdatePlace useVerifyCandidates useVerifyPlace useListUnits useCreateUnit useListLinks useCreateLink useConfirmLink useStartCollect useStartVision useStartCopy
fact useGetSchema useListFacts useUpsertFact useTransitionFact
job useGetJob useJobOps useRequeueJob
site useGetSite useStartBuild useListVersions useListLogs useChangeStatus

orval.config.ts 의 operationName 이 FastAPI 의 list_places_v1_place_list_get 를 listPlaces 로 되돌린다 — 백엔드는 무수정이다.

규약 세 가지

1. 거절도 HTTP 200 이다. 도메인 거절은 result.success=false + result.desc(ErrorType 이름)로 온다. React Query 는 성공으로 보므로 onSuccess 안에서 직접 봐야 한다. 안 보면 저장 안 된 값이 저장된 것처럼 보인다.

onSuccess: (res) => {
  if (res.result?.success === false) return notifyApiError({data: res});
  ...
}

문구 변환은 @/lib/errorMessages 한 곳에 있다(PLACE_NOT_VERIFIED → "동일 업소 검증을 먼저…").

2. 응답의 None 필드는 키째 사라진다(백엔드 RemoveNoneResponse). 게다가 백엔드가 기본값을 준 필드는 OpenAPI 에서 required 가 아니라 생성 타입이 전부 optional 이다 — undefined 를 각오하고 쓴다.

3. 몇 분 걸리는 일은 잡이다. 수집·비전·생성·빌드는 job_id 를 받고 폴링한다. 루프는 pollJob() 하나뿐이다 — 화면마다 다시 쓰지 않는다.

const started = await startCollect(placeId, {});
const outcome = await pollJob(started.job_id!, {signal, onTick: (job) => ...});
// outcome.kind: done | dead | timeout | aborted | unreachable

★ done 은 "잡이 끝났다"이지 "성공했다"가 아니다. 빌드는 게이트에 막혀도 정상 종료하고 job.result.gate.passed 가 false 로 온다 — 판정은 features/publish/usePublishSite.ts 가 읽는다.