콘솔에 발급된 설정이 설계와 어긋나 있었다. 클라이언트 시크릿이 켜져 있고 Redirect URI 가
프론트 경로(/auth/kakao/callback)인데, 코드는 "프론트가 액세스 토큰을 들고 온다" 는
전제였다 — 시크릿을 번들에 구울 수 없으므로 그 흐름으로는 토큰을 받을 수 없다.
- services/external/kakao_identity: access_token 검증 → **인가 코드 교환**으로 교체.
★ 우리 REST 키·시크릿으로 교환한 토큰이라 정의상 우리 앱 것이다 — 구글의 aud 대조에
해당하는 검사(KAKAO_LOGIN_APP_ID)가 구조적으로 필요 없어져 설정 칸째로 지웠다.
★ 로그인 앱과 챗봇 앱이 다르면 로그인은 되는데 채널 매칭만 조용히 안 된다 —
REST 키가 어긋나면 경고 로그를 남긴다.
★ 미인증 이메일은 신원으로 쓰지 않는다(is_email_valid·is_email_verified 둘 다). 이 값으로
auth_service 가 다른 수단 가입과의 충돌을 판정한다.
- config/config_models: KAKAO_LOGIN_APP_ID → REST_API_KEY · CLIENT_SECRET · REDIRECT_URI
- router/v1/auth: Req_KakaoLogin {access_token} → {code, redirect_uri}
- frontend: lib/kakaoIdentity(인가 URL · sessionStorage state) · KakaoSignInButton ·
pages/KakaoCallbackPage(코드는 1회용이라 ref 로 한 번만 보낸다) · routes
★ 로그인 폼에서 카카오를 구글 위에 둔다 — 이 수단으로 들어와야 6자리 코드 없이 채널이 이어진다
- compose · nginx/Dockerfile: REST 키·Redirect URI 만 프론트로 흘려보낸다. 시크릿은 안 나간다
- 어드민 키는 저장하지 않는다 — 탈퇴까지 되는 키인데 로그인에는 쓰이지 않는다
- api/generated: orval 재생성. 카카오 외에도 오래 밀려 있던 문구·agent 경로가 함께 따라왔다
테스트 8건 추가(tests/test_kakao_identity.py), 관련 85 passed
(test_auth::test_google_login_is_off_when_client_id_is_empty 는 기존 실패)
lint(react-router typegen + tsc + eslint) 통과
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| generated | ||
| mutator | ||
| index.ts | ||
| pollJob.ts | ||
| README.md | ||
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 가 읽는다.