From 5127600a3d0b5c574047a645ce4f36b0e3465c46 Mon Sep 17 00:00:00 2001
From: king
Date: Wed, 19 Aug 2026 17:38:20 +0900
Subject: [PATCH] =?UTF-8?q?feat(cafe24):=20=EC=A7=84=EC=97=B4/=ED=8C=90?=
=?UTF-8?q?=EB=A7=A4=20=EB=B0=B0=EC=A7=80=20=ED=81=B4=EB=A6=AD=EC=9C=BC?=
=?UTF-8?q?=EB=A1=9C=20=EC=83=81=ED=83=9C=20=ED=86=A0=EA=B8=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
편집기 오른쪽 위 배지를 눌러 진열·판매를 바로 바꾼다. 상태만 바꾸려고
카페24 관리자에 들어갈 필요가 없어진다.
- POST /cafe24/products/{no}/status (JSON) 추가. 상세설명은 건드리지 않아
BACKUP revision 을 만들지 않는다 - 되돌릴 HTML 이 없고 다시 눌러 복구된다.
- 요청값을 낙관적으로 반영하지 않고 쓰기 후 카페24가 돌려준 실제 상태로
화면을 다시 그린다. 실패해도 화면과 카페24가 어긋나지 않는다.
- 왼쪽 목록의 점과 정렬용 data-display/data-selling 도 함께 갱신(목록을
다시 받지 않으므로).
- 카페24 조회 실패 시에는 현재 상태를 믿을 수 없어 배지를 버튼으로 만들지
않는다.
Co-Authored-By: Claude Opus 5
---
app/modules/cafe24/routes_products.py | 62 ++++++++++++++-
.../cafe24/templates/cafe24/_editor.html | 14 +++-
.../cafe24/templates/cafe24/products.html | 79 ++++++++++++++++++-
app/static/cafe24.css | 19 +++++
docs/CAFE24_MODULE.md | 1 +
5 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/app/modules/cafe24/routes_products.py b/app/modules/cafe24/routes_products.py
index 416e8d0..35b1a5c 100644
--- a/app/modules/cafe24/routes_products.py
+++ b/app/modules/cafe24/routes_products.py
@@ -34,13 +34,13 @@ import logging
from typing import Any
from urllib.parse import urlencode
-from fastapi import APIRouter, Form, Request
+from fastapi import APIRouter, Body, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from app.integrations.cafe24 import Cafe24Error, build_cafe24_api, products
from . import store
-from .common import base_ctx, guard
+from .common import base_ctx, guard, require_store
logger = logging.getLogger("cafe24.products")
@@ -229,6 +229,64 @@ def product_pane(request: Request, product_no: int) -> HTMLResponse:
return _no_store(render_template(request, "cafe24/_editor.html", ctx))
+@products_router.post("/products/{product_no}/status")
+def product_status(
+ request: Request,
+ product_no: int,
+ payload: dict[str, Any] = Body(default_factory=dict),
+) -> dict[str, Any]:
+ """진열/판매 상태만 바꾼다 — 편집기 오른쪽 위 배지 클릭용(JSON API).
+
+ 상세설명은 건드리지 않는다(`build_update_payload` 는 준 필드만 보낸다). 그래서
+ BACKUP revision 도 만들지 않는다 — 되돌릴 HTML 이 없고, 상태는 다시 눌러
+ 되돌릴 수 있다.
+
+ `value` 는 클라이언트가 **원하는 결과값**이다(현재값을 뒤집지 않는다). 화면의
+ 배지가 카페24와 어긋나 있어도 사용자가 누른 대로 되는 편이 예측 가능하다.
+ 응답에는 쓰기 후 카페24가 돌려준 실제 상태를 담아 화면을 그것에 맞춘다.
+ """
+ st, user = require_store(request)
+ actor = str(user.get("email") or "")
+
+ field = str(payload.get("field") or "").strip()
+ if field not in ("display", "selling"):
+ raise HTTPException(status_code=400, detail="field 는 display 또는 selling 이어야 합니다.")
+ want = bool(payload.get("value"))
+
+ api = build_cafe24_api(st)
+ try:
+ updated = products.update_product(api.client, product_no, **{field: want})
+ except Cafe24Error as exc:
+ st.log_audit(
+ actor=actor, action=f"set_{field}", product_no=product_no,
+ result="FAIL", detail=f"{want} 설정 실패: {exc}",
+ )
+ logger.warning("카페24 상품 %s %s 변경 실패: %s", product_no, field, exc)
+ raise HTTPException(status_code=502, detail=str(exc)) from exc
+
+ # 응답이 상품 dict 면 그것이 곧 현재 상태다. 모양이 다르면(방어) 다시 조회한다.
+ if "display" not in updated or "selling" not in updated:
+ try:
+ updated = products.get_product(api.client, product_no)
+ except Cafe24Error as exc: # 쓰기는 됐다 — 화면만 요청값으로 맞춘다.
+ logger.warning("카페24 상품 %s 상태 재조회 실패: %s", product_no, exc)
+ updated = {}
+
+ info = products.normalize_product(updated) if updated else {}
+ if info.get("product_no"):
+ st.upsert_products([info])
+ state = {
+ "display": bool(info.get("display", want if field == "display" else True)),
+ "selling": bool(info.get("selling", want if field == "selling" else True)),
+ }
+ st.log_audit(
+ actor=actor, action=f"set_{field}", product_no=product_no,
+ result="SUCCESS", detail=f"{field}={'T' if want else 'F'}",
+ )
+ logger.info("카페24 상품 %s %s=%s (%s)", product_no, field, want, actor)
+ return {"ok": True, **state}
+
+
@products_router.get("/products/{product_no}")
def product_redirect(request: Request, product_no: int):
"""옛 단독 화면 주소 → 2분할 화면에서 해당 상품을 선택한 상태로 보낸다."""
diff --git a/app/modules/cafe24/templates/cafe24/_editor.html b/app/modules/cafe24/templates/cafe24/_editor.html
index 94fd1f8..1df77f5 100644
--- a/app/modules/cafe24/templates/cafe24/_editor.html
+++ b/app/modules/cafe24/templates/cafe24/_editor.html
@@ -21,11 +21,23 @@
{% if info.updated_date %}· 최근 수정 {{ info.updated_date }}{% endif %}
-
+ {# 배지 클릭 = 진열/판매 토글. 카페24 조회에 실패했을 때(desc 없음)는 현재 상태를
+ 믿을 수 없으므로 누를 수 없는 표시로만 둔다. 실제 전환은 products.html 의
+ JS 가 POST /products/{no}/status 로 처리하고 응답값으로 다시 그린다. #}
+