From bf3407b6ec089d5e8414554fd5fc90c00a82dba2 Mon Sep 17 00:00:00 2001 From: king Date: Tue, 1 Sep 2026 11:30:00 +0900 Subject: [PATCH] =?UTF-8?q?feat(cupang):=20=EC=A0=9C=ED=92=88=20=EC=B9=B4?= =?UTF-8?q?=ED=83=88=EB=A1=9C=EA=B7=B8=EC=97=90=20=EC=BF=A0=ED=8C=A1?= =?UTF-8?q?=EC=83=81=ED=92=88=EC=BD=94=EB=93=9C=20+=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=20=ED=86=A0=EA=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cupang_products.coupang_item_code 컬럼 추가 (마이그레이션 004, init.sql 동기화) - 설정 화면: 쿠팡상품코드 열 표시/정렬, 수기 추가 폼 입력란 추가 - 선택 등록 시 바로 등록하지 않고 쿠팡상품코드 입력 팝업을 먼저 표시 - 상태 배지를 클릭 토글로 변경 (POST /cupang/api/products/{id}/toggle) - 빈 쿠팡상품코드는 "미입력"으로 처리해 기존 값을 덮어쓰지 않음 Co-Authored-By: Claude Opus 5 --- app/modules/cupang/db.py | 42 +++++- app/modules/cupang/router.py | 37 ++++- .../cupang/templates/cupang/box_calc.html | 2 +- .../cupang/templates/cupang/box_rules.html | 2 +- .../cupang/templates/cupang/centers.html | 2 +- .../cupang/templates/cupang/detail.html | 2 +- app/modules/cupang/templates/cupang/form.html | 2 +- .../cupang/templates/cupang/index.html | 2 +- .../cupang/templates/cupang/products.html | 127 +++++++++++++++--- app/static/cupang.css | 30 +++++ ...upang_db_004_product_coupang_item_code.sql | 28 ++++ scripts/sql/cupang_db_init.sql | 2 + 12 files changed, 243 insertions(+), 35 deletions(-) create mode 100644 scripts/sql/cupang_db_004_product_coupang_item_code.sql diff --git a/app/modules/cupang/db.py b/app/modules/cupang/db.py index d404673..33ea977 100644 --- a/app/modules/cupang/db.py +++ b/app/modules/cupang/db.py @@ -294,24 +294,38 @@ class CupangDBStore: return [self._product_serialize(r) for r in rows] def upsert_product( - self, *, product_code: str, product_name: str, sort_order: int = 0 + self, + *, + product_code: str, + product_name: str, + coupang_item_code: str | None = None, + sort_order: int = 0, ) -> dict[str, Any]: + """제품 등록/수정. + + coupang_item_code 가 None 이면 기존 값을 유지한다(빈 문자열은 지우기). + """ code = (product_code or "").strip() name = (product_name or "").strip() + cic = None if coupang_item_code is None else coupang_item_code.strip() if not code or not name: raise ValueError("제품코드와 제품명 모두 필요합니다.") with self._pool.connection() as conn: row = conn.execute( """ - INSERT INTO cupang_products (product_code, product_name, sort_order) - VALUES (%s, %s, %s) + INSERT INTO cupang_products + (product_code, product_name, coupang_item_code, sort_order) + VALUES (%(code)s, %(name)s, COALESCE(%(cic)s, ''), %(sort)s) ON CONFLICT (product_code) DO UPDATE - SET product_name = EXCLUDED.product_name, - sort_order = EXCLUDED.sort_order, - active = TRUE + SET product_name = EXCLUDED.product_name, + coupang_item_code = COALESCE( + %(cic)s, cupang_products.coupang_item_code + ), + sort_order = EXCLUDED.sort_order, + active = TRUE RETURNING * """, - (code, name, sort_order), + {"code": code, "name": name, "cic": cic, "sort": sort_order}, ).fetchone() return self._product_serialize(row) @@ -324,6 +338,18 @@ class CupangDBStore: if cur.rowcount == 0: raise KeyError(product_id) + def toggle_product_active(self, *, product_id: int) -> dict[str, Any]: + """활성 ↔ 비활성 뒤집기. 갱신된 행을 반환.""" + with self._pool.connection() as conn: + row = conn.execute( + "UPDATE cupang_products SET active = NOT active " + "WHERE id = %s RETURNING *", + (product_id,), + ).fetchone() + if row is None: + raise KeyError(product_id) + return self._product_serialize(row) + def delete_product(self, *, product_id: int) -> None: with self._pool.connection() as conn: cur = conn.execute( @@ -642,6 +668,7 @@ class CupangDBStore: out["id"] = int(out["id"]) out["active"] = bool(out.get("active", True)) out["sort_order"] = int(out.get("sort_order", 0)) + out["coupang_item_code"] = (out.get("coupang_item_code") or "").strip() for k in ("created_at", "updated_at"): v = out.get(k) if isinstance(v, datetime): @@ -685,6 +712,7 @@ class CupangDBStore: out["id"] = int(out["id"]) out["active"] = bool(out.get("active", True)) out["sort_order"] = int(out.get("sort_order", 0)) + out["coupang_item_code"] = (out.get("coupang_item_code") or "").strip() for k in ("created_at", "updated_at"): v = out.get(k) if isinstance(v, datetime): diff --git a/app/modules/cupang/router.py b/app/modules/cupang/router.py index 7405682..84c6dae 100644 --- a/app/modules/cupang/router.py +++ b/app/modules/cupang/router.py @@ -846,7 +846,11 @@ async def product_bulk( items: list[dict[str, Any]] = Body(..., embed=True), user: dict[str, Any] = Depends(_require_user), ) -> JSONResponse: - """선택한 상품들을 일괄 등록(upsert). body: {"items":[{"code","name"}, ...]}.""" + """선택한 상품들을 일괄 등록(upsert). + + body: {"items":[{"code","name","coupang_item_code"}, ...]} + coupang_item_code 키가 없으면 기존 값을 유지한다. + """ store = _store(request) if store is None: raise HTTPException(status_code=503, detail="cupang_db 미설정") @@ -854,10 +858,15 @@ async def product_bulk( for it in items: code = str(it.get("code") or "").strip() name = str(it.get("name") or "").strip() + cic = it.get("coupang_item_code") if not code or not name: continue try: - store.upsert_product(product_code=code, product_name=name) + store.upsert_product( + product_code=code, + product_name=name, + coupang_item_code=(None if cic is None else str(cic)), + ) added += 1 except ValueError: continue @@ -869,6 +878,7 @@ async def product_upsert( request: Request, product_code: str = Form(...), product_name: str = Form(...), + coupang_item_code: str = Form(""), sort_order: int = Form(0), user: dict[str, Any] = Depends(_require_user), ) -> RedirectResponse: @@ -877,7 +887,11 @@ async def product_upsert( raise HTTPException(status_code=503, detail="cupang_db 미설정") try: store.upsert_product( - product_code=product_code, product_name=product_name, sort_order=sort_order + product_code=product_code, + product_name=product_name, + # 빈 값은 "미입력" 으로 보고 기존 쿠팡상품코드를 유지한다. + coupang_item_code=(coupang_item_code.strip() or None), + sort_order=sort_order, ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) @@ -903,6 +917,23 @@ async def product_set_active( return RedirectResponse(url="/cupang/products", status_code=303) +@router.post("/api/products/{product_id:int}/toggle") +async def product_toggle_active( + request: Request, + product_id: int, + user: dict[str, Any] = Depends(_require_user), +) -> JSONResponse: + """상태 배지 클릭 → 활성/비활성 토글(페이지 새로고침 없이).""" + store = _store(request) + if store is None: + raise HTTPException(status_code=503, detail="cupang_db 미설정") + try: + product = store.toggle_product_active(product_id=product_id) + except KeyError: + raise HTTPException(status_code=404, detail="제품을 찾을 수 없습니다.") + return JSONResponse({"ok": True, "active": bool(product["active"])}) + + @router.post("/products/{product_id:int}/delete") async def product_delete( request: Request, diff --git a/app/modules/cupang/templates/cupang/box_calc.html b/app/modules/cupang/templates/cupang/box_calc.html index 270c0fa..40f11fc 100644 --- a/app/modules/cupang/templates/cupang/box_calc.html +++ b/app/modules/cupang/templates/cupang/box_calc.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/box_rules.html b/app/modules/cupang/templates/cupang/box_rules.html index b00d165..16e0083 100644 --- a/app/modules/cupang/templates/cupang/box_rules.html +++ b/app/modules/cupang/templates/cupang/box_rules.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/centers.html b/app/modules/cupang/templates/cupang/centers.html index 92ab701..7446537 100644 --- a/app/modules/cupang/templates/cupang/centers.html +++ b/app/modules/cupang/templates/cupang/centers.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/detail.html b/app/modules/cupang/templates/cupang/detail.html index 58ec6da..88e05f0 100644 --- a/app/modules/cupang/templates/cupang/detail.html +++ b/app/modules/cupang/templates/cupang/detail.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/form.html b/app/modules/cupang/templates/cupang/form.html index 2389ff7..a951eda 100644 --- a/app/modules/cupang/templates/cupang/form.html +++ b/app/modules/cupang/templates/cupang/form.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/index.html b/app/modules/cupang/templates/cupang/index.html index 66502ea..e9fbbb7 100644 --- a/app/modules/cupang/templates/cupang/index.html +++ b/app/modules/cupang/templates/cupang/index.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/products.html b/app/modules/cupang/templates/cupang/products.html index 84070fb..259caa2 100644 --- a/app/modules/cupang/templates/cupang/products.html +++ b/app/modules/cupang/templates/cupang/products.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
@@ -32,6 +32,8 @@ + @@ -57,29 +59,28 @@ + 상태 동작 {% for p in products %} - + {{ p.product_name }} {{ p.product_code }} - {% if p.active %}활성{% else %}비활성{% endif %} + {% if p.coupang_item_code %}{{ p.coupang_item_code }}{% else %}{% endif %} + + + +
- {% if p.active %} -
- - -
- {% else %} -
- - -
- {% endif %}
@@ -89,7 +90,7 @@ {% endfor %} {% if not products %} - 등록된 제품명이 없습니다. 왼쪽에서 선택해 등록하세요. + 등록된 제품명이 없습니다. 왼쪽에서 선택해 등록하세요. {% endif %} @@ -136,7 +137,50 @@ })(); + + {% if search_enabled %} + + +