o2o-site-AEO/solution/frontend/src/api
Mina Choi 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
..
generated [feat] solution/frontend: 온보딩을 상호명부터 — 단계를 주소창으로 옮기고 업종은 검색이 정한다 2026-09-03 10:20:57 +09:00
mutator 이름: solution/front → solution/frontend 2026-08-31 15:27:16 +09:00
index.ts 이름: solution/front → solution/frontend 2026-08-31 15:27:16 +09:00
pollJob.ts 이름: solution/front → solution/frontend 2026-08-31 15:27:16 +09:00
README.md 문서: 앱을 가른 뒤 낡아진 서술을 고치고, 개발과 무관해진 기록을 지운다 2026-08-31 16:58:09 +09:00

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

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

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

태그 훅
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
faq useListFaqs useCreateFaq useTransitionFaq
media useListMedia
local-content usePublish useSyncFestivals useUpdateContent useEndContent — 내부 운영 전용

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 가 읽는다.