프로젝트를 불러오는 중…
프로젝트를 불러오는 중…
스마트스토어 챗봇(naver-smartstore)이 의도 분류와 응답 생성에 집중하려면, 네이버 커머스 API의 인증·페이지네이션·상태 코드 같은 사정은 챗봇 코드 바깥에 있어야 했다. 그런데 네이버 커머스 API의 OAuth2는 표준 client_secret 모델이 아니다 — bcrypt 해시 + base64 인코딩으로 직접 서명을 생성해 token endpoint에 보내야 한다.
| 사정 | 챗봇이 직접 다룰 때의 비용 |
|---|---|
| bcrypt 서명 알고리즘 | LangGraph 노드마다 서명 재계산 → 코드 복제 + 키 노출 위험 |
| 토큰 만료 | 호출마다 토큰 발급 → 호출 비용·레이트리밋 압박 |
| 응답 페이로드 한글 상태 코드 | DELIVERED·PAYMENT_WAITING 같은 영문 상태를 LLM 프롬프트로 그대로 노출 → 사용자 응답 일관성 저하 |
| 에러 형식 | Naver의 비표준 에러 응답을 챗봇 노드마다 파싱 → 분산된 핸들링 |
목표는 세 가지로 좁혔다.
핵심은 CommerceTokenManager 단일 클래스. 서명 생성·토큰 발급·만료 추적·재발급을 한 객체 안에 가두고, 라우터에서는 await token_manager.get_token() 한 호출로 받는다.
| 책임 | 구현 |
|---|---|
| 서명 생성 | bcrypt.hashpw(password, client_secret) → base64.urlsafe_b64encode (Naver 스펙) |
| 토큰 발급 | POST /v1/oauth2/token with client_credentials + signature |
| 캐시 | 메모리 dict + expires_at (발급 시각 + 85m, 실제 만료보다 보수적으로) |
| 재발급 | expires_at 도래 또는 401 응답 수신 시 자동 재발급 |
| 상태 노출 | GET /api/auth/status로 캐시 잔여 시간·토큰 prefix 노출 (운영 디버그용) |
85분(공식 만료 90분에서 5분 마진)으로 잡은 이유는 두 가지 — 만료 직전 호출이 401로 떨어지는 레이스 컨디션 회피, 그리고 워커가 여러 개일 때 캐시 만료 직후 동시 재발급이 몰리는 thundering-herd 완화.
ProductService와 OrderService는 동일한 패턴 — 토큰 매니저에서 Bearer 받고, httpx.AsyncClient로 호출하고, 결과를 정규화. 차이는 정규화 깊이다.
ProductService — 검색(/v1/products/search) + 채널 상품 상세(/v2/products/channel-products/{id})OrderService — 주문 검색(/v1/pay-order/seller/product-orders) + 상세 + 한글 상태 번역주문 검색은 공식 필터 스펙(startSearchDate/endSearchDate/productOrderStatuses CSV 등)에 맞췄다. 04-03에 한 번 내부 변경한 시그니처를 공식 필터에 다시 정렬한 게 이 라운드의 핵심 정리 작업이었다 — 챗봇이 "Naver의 페이로드 형태"를 그대로 의식하지 않게 하기 위함.
영문 상태 코드를 LLM 프롬프트에 그대로 노출하면, 모델이 PAYMENT_WAITING을 "결제 대기"로 의역할 때마다 표현이 흔들린다. 챗봇이 안정적으로 답하려면 백엔드가 한 가지 한국어 표현으로 고정해서 내려줘야 한다.
| 영문 상태 | 한글 (고정) |
|---|---|
PAYMENT_WAITING | 결제 대기 |
PAYED | 결제 완료 |
DELIVERING | 배송 중 |
DELIVERED | 배송 완료 |
PURCHASE_DECIDED | 구매 확정 |
CANCELED / RETURNED / EXCHANGED | 취소 / 반품 / 교환 |
번역은 _normalize_order_item에서 일괄 처리 — 챗봇 노드는 영문 상태를 볼 일이 없다.
FastAPI 단일 프로세스만 띄우면 콜드스타트 후 첫 요청이 토큰 발급에 묶여 느려진다. Gunicorn으로 워커를 띄우고, 워커 단위로 토큰 캐시를 공유(메모리 dict)하는 방식 — 워커가 늘어나도 외부 토큰 호출은 한 번에 한 번씩만 추가된다.
배포는 04-08 단일 커밋으로 GitHub Actions → Cloud Run 워크플로우를 한 번에 박았다. gunicorn.conf.py가 워커 수·바인드·로그 포맷을 한 곳에서 관리한다.
빠른 스캐폴드 + 운영 시그니처 정리 패턴.
| 시기 | 주요 변경 |
|---|---|
| Day 1 (2026-03-30) | bcrypt 인증 + 토큰 매니저 · 상품/주문 엔드포인트 (smartstore와 같은 날 푸시) |
| Week 1 (~04-03) | 인증 재시도 정리 · 공식 주문 조회 필터로 시그니처 재정렬 · PRD/Postman 검증 자산 갱신 |
| Week 2 (~04-07) | 상품 질문 생성 스크립트 추가 (smartstore 벤치마크 자산 연계) |
| Week 2 (04-08) | Cloud Run 배포 워크플로우 · gunicorn.conf.py · uv.lock 트래킹 |
특이점: 스캐폴드 첫날(2026-03-30)이 naver-smartstore의 Day 1과 같다. 두 저장소를 한 작업 세션에서 같이 푸시한 셈 — 챗봇이 호출할 표면이 명확했기에 가능했다.
| 항목 | 결과 |
|---|---|
| 안정화 기간 | 10일 (Day 1 스캐폴드 → 04-08 prod 배포) |
| 토큰 호출 절감 | 매 호출 발급 → 85분당 1회 (워커당) |
| 인증 코드 격리 | bcrypt 서명·재발급·캐시가 CommerceTokenManager 단일 객체에 캡슐화 |
| 한글 상태 일관성 | 영문 상태 코드 0건 LLM 입력 (모든 노출 채널에서 고정 한국어) |
| 운영 산출물 | Containerfile · gunicorn.conf.py · GitHub Actions → Cloud Run · /api/auth/status |
기술·운영적으로 가져간 학습은 네 가지다.
select-hotel에서 검증한 5회/30초 패턴을 이식./api/auth/status 확장 → 운영 대시보드 — 토큰 잔여 시간·재발급 횟수·최근 401 발생을 메트릭으로 노출. 비정상 인증 사이클을 사후가 아니라 실시간으로 감지.