feat(cafe24): 상품 상세페이지 관리 모듈 Phase 1

카페24 관리자에 직접 접속하지 않고 상품 상세페이지(description HTML)를
편집·예약 적용·복원하기 위한 모듈의 기반을 만든다. Phase 1 은 공통
Integration 계층, cafe24_db, OAuth 연결 화면까지다.

카페24 OAuth/API 클라이언트를 상품관리 모듈 안에 두지 않고
app/integrations/cafe24/ 로 분리했다. 향후 추가할 주문관리(주문 조회·송장
일괄등록·취소/반품/교환)가 같은 토큰과 클라이언트를 그대로 재사용해야 하기
때문이다. 라우터에서 httpx 를 직접 부르지 않고 Cafe24Client 만 쓰게 해서
재시도·rate limit·API 로그·토큰 갱신을 한 곳에 모았다.

토큰은 Fernet 으로 암호화해 저장한다(CAFE24_TOKEN_SECRET). DB 덤프가
유출돼도 access/refresh token 이 평문으로 남지 않게 하기 위함이며, API 로그와
연결 상태 화면에는 토큰·시크릿을 일절 기록/표시하지 않는다.

토큰 갱신은 행 잠금(SELECT ... FOR UPDATE) 안에서 한다. 카페24는 refresh
token 을 회전시키므로, 이후 추가될 예약 worker 컨테이너와 web 컨테이너가
동시에 갱신하면 한쪽 토큰이 무효화된다.

기존 파일 변경은 목록에 한 줄씩 추가하는 형태로 44줄뿐이며 기존 라우트·
테이블·인증 로직은 건드리지 않았다. CAFE24_DB_URL 미설정 시 store 가 None
이라 앱은 정상 기동하고 모듈만 "설정 필요" 안내를 표시한다.

가드 헬퍼를 common.py 로 분리한 것은 router.py 가 routes_system.py 를
include 하는 구조에서 순환 import 가 생기기 때문이다.

검증: 신규 테스트 16개 통과(암호화 왕복, 토큰 만료·자동갱신, 상태 노출 시
토큰 미유출, 재시도 예산, 예약 상태 전이). dispatch 기존 테스트 9개 통과.
cafe24_db_init.sql 은 로컬에 Docker 가 없어 미실행 — 서버 적용 시 확인 필요.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 00:23:02 +09:00
parent 31eab0d4cb
commit c6fb8ed375
29 changed files with 2689 additions and 0 deletions
+41
View File
@@ -10,6 +10,7 @@
| `expense_db` | 개인경비 / 법인카드 사용내역 / 정산 |
| `cupang_db` | 쿠팡 밀크런 출고 묶음 / 출고 라인 / 입고센터 / 박스 입수량 규칙 |
| `malaysia_stock_db` | 말레이시아 창고 재고관리 — 창고/아이템/세트 BOM/입출고 이력/일일 재고조사 |
| `cafe24_db` | 카페24 연동 — OAuth 토큰/상품 캐시/상세페이지 버전/예약/감사·API 로그 |
---
@@ -290,6 +291,46 @@ cd /opt/www/main && docker compose up -d --build
---
## cafe24_db 스키마 / 초기화
DDL: `scripts/sql/cafe24_db_init.sql` (멱등). DB·역할(`cafe24_app`)·테이블·인덱스·트리거를 한 번에 생성. **JSON 폴백 없음**`CAFE24_DB_URL` 미설정 시 모듈이 "설정 필요" 안내만 표시.
테이블:
| 테이블 | 용도 |
| --- | --- |
| `cafe24_oauth_tokens` | 쇼핑몰별 OAuth 토큰(`mall_id` UNIQUE). access/refresh 는 **Fernet 암호문**으로 저장. 상품관리 + 향후 주문관리가 공유 |
| `cafe24_products` | 상품 캐시(`product_no` UNIQUE). 목록/검색 속도용이며 source of truth 는 언제나 카페24 |
| `cafe24_product_revisions` | 상세페이지 HTML 버전(append-only). `revision_type` SYNC/DRAFT/**BACKUP**/MANUAL/SCHEDULED/ROLLBACK |
| `cafe24_product_schedules` | 예약 작업. `status` PENDING/PROCESSING/SUCCESS/FAILED/CANCELLED, 재시도·자동종료·복원 대상 포함 |
| `cafe24_audit_logs` | 누가 무엇을 바꿨나. worker 수행분은 `actor='SCHEDULER'` |
| `cafe24_api_logs` | 카페24 API 호출 기록. **토큰/Authorization/client_secret 미기록** |
핵심 규칙:
- 카페24에 쓰기 직전 **반드시 현재 HTML 을 다시 조회해 `BACKUP` revision 으로 저장**한다. 로컬 DB 의 마지막 값을 현재값으로 가정하지 않는다.
- 예약의 `restore_revision_id`**예약 실행 순간** 만든 BACKUP 을 가리킨다(예약 생성 시점 값이 아님).
- 일괄 예약은 상품 1건당 1행 + 공통 `parent_job_id` — 한 상품 실패가 나머지를 막지 않는다.
- 토큰 암호화 키는 `.env``CAFE24_TOKEN_SECRET`. **값을 바꾸면 기존 토큰을 복호화할 수 없어 카페24 재연결이 필요하다.**
### 운영 서버 초기화 (1회, 사용자 승인 후)
```bash
read -s -p "cafe24_app password: " APP_PWD; echo
docker exec -i postgres-db psql -U postgres \
-v app_password="$APP_PWD" \
< scripts/sql/cafe24_db_init.sql
# main-app .env 에 추가:
# CAFE24_DB_URL=postgresql://cafe24_app:<APP_PWD>@postgres-db:5432/cafe24_db
# CAFE24_MALL_ID / CAFE24_CLIENT_ID / CAFE24_CLIENT_SECRET / CAFE24_REDIRECT_URI
# CAFE24_TOKEN_SECRET=$(openssl rand -hex 32)
cd /opt/www/main && docker compose up -d --build web
```
> 멱등 스크립트. 기존 DB 가 있으면 DROP 하지 않음. 상세는 `docs/CAFE24_MODULE.md`.
---
## 백업 / 복구 (안전 절차)
### 백업