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:
+19
@@ -13,6 +13,8 @@ from jinja2 import ChoiceLoader, FileSystemLoader
|
||||
from pydantic import BaseModel
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from .modules.cafe24 import build_cafe24_store
|
||||
from .modules.cafe24 import router as cafe24_router
|
||||
from .modules.cupang import build_cupang_store, build_itemcode_reader
|
||||
from .modules.cupang import router as cupang_router
|
||||
from .modules.dispatch import build_dispatch_store
|
||||
@@ -45,6 +47,7 @@ MODULE_LABELS: dict[str, str] = {
|
||||
"malaysia": "말레이시아 재고관리",
|
||||
"dispatch": "말레이시아 배송",
|
||||
"project": "프로젝트 관리",
|
||||
"cafe24": "카페24 상품관리",
|
||||
"expense_approver": "개인경비",
|
||||
"vacation_approver": "휴가",
|
||||
}
|
||||
@@ -120,6 +123,7 @@ _MODULE_TEMPLATE_DIRS = [
|
||||
BASE_DIR / "modules" / "malaysia" / "templates",
|
||||
BASE_DIR / "modules" / "dispatch" / "templates",
|
||||
BASE_DIR / "modules" / "project" / "templates",
|
||||
BASE_DIR / "modules" / "cafe24" / "templates",
|
||||
]
|
||||
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
||||
templates.env.loader = ChoiceLoader(
|
||||
@@ -170,6 +174,9 @@ app.state.dispatch_store = build_dispatch_store(dsn=env("DISPATCH_DB_URL") or No
|
||||
# 프로젝트 관리(아사나식): PROJECT_DB_URL 없으면 store=None(라우터가 "설정 필요" 안내).
|
||||
# 메일 알림은 SMTP_* 환경변수 기반(app/mail.py). 미설정 시 조용히 skip.
|
||||
app.state.project_store = build_project_store(dsn=env("PROJECT_DB_URL") or None)
|
||||
# 카페24 상품관리: CAFE24_DB_URL 없으면 store=None(라우터가 "설정 필요" 안내).
|
||||
# 카페24 API 호출/토큰은 app/integrations/cafe24 공통 계층(CAFE24_* 환경변수).
|
||||
app.state.cafe24_store = build_cafe24_store(dsn=env("CAFE24_DB_URL") or None)
|
||||
|
||||
# 모듈 라우터 등록 — 신규 모듈 추가 시 여기 한 줄.
|
||||
app.include_router(expense_router)
|
||||
@@ -178,6 +185,7 @@ app.include_router(vacation_router)
|
||||
app.include_router(malaysia_router)
|
||||
app.include_router(dispatch_router)
|
||||
app.include_router(project_router)
|
||||
app.include_router(cafe24_router)
|
||||
|
||||
|
||||
def public_url_for(request: Request, route_name: str) -> str:
|
||||
@@ -350,6 +358,16 @@ def _menu_items_for(user_rec: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"status": "ready",
|
||||
"category": "관리",
|
||||
},
|
||||
{
|
||||
"key": "cafe24",
|
||||
"title": "카페24 상품관리",
|
||||
"subtitle": "Cafe24 Products",
|
||||
"description": "카페24 관리자에 들어가지 않고 상품 상세페이지를 편집·예약 적용하고 이전 버전으로 되돌립니다.",
|
||||
"url": "/cafe24/",
|
||||
"health_url": "/cafe24/health",
|
||||
"status": "ready",
|
||||
"category": "운영",
|
||||
},
|
||||
]
|
||||
allowed = allowed_modules(user_rec)
|
||||
for item in items:
|
||||
@@ -369,6 +387,7 @@ def _icon_svg(name: str) -> str:
|
||||
"malaysia": '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
|
||||
"dispatch": '<rect x="1" y="3" width="15" height="13"/><path d="M16 8h4l3 3v5h-7V8z"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/>',
|
||||
"project": '<rect x="3" y="4" width="18" height="16" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="8" y1="13" x2="13" y2="13"/><line x1="8" y1="16" x2="11" y2="16"/>',
|
||||
"cafe24": '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9h10"/><path d="M7 13h7"/><path d="M7 17h4"/>',
|
||||
"modules": '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>',
|
||||
}
|
||||
body = paths.get(name, paths["modules"])
|
||||
|
||||
Reference in New Issue
Block a user