diff --git a/app/modules/cupang/db.py b/app/modules/cupang/db.py index 182ceae..a5aa4f6 100644 --- a/app/modules/cupang/db.py +++ b/app/modules/cupang/db.py @@ -237,11 +237,11 @@ class CupangDBStore: ).fetchone() return self._product_serialize(row) - def deactivate_product(self, *, product_id: int) -> None: + def set_product_active(self, *, product_id: int, active: bool) -> None: with self._pool.connection() as conn: cur = conn.execute( - "UPDATE cupang_products SET active = FALSE WHERE id = %s", - (product_id,), + "UPDATE cupang_products SET active = %s WHERE id = %s", + (bool(active), product_id), ) if cur.rowcount == 0: raise KeyError(product_id) diff --git a/app/modules/cupang/itemcode.py b/app/modules/cupang/itemcode.py index a93bb3d..d5520d1 100644 --- a/app/modules/cupang/itemcode.py +++ b/app/modules/cupang/itemcode.py @@ -114,9 +114,17 @@ class ItemcodeReader: 비활성 상태이거나 조회 실패 시 빈 리스트(예외 비전파 — UI 는 수동 입력 폴백). """ q = (query or "").strip() - if not self.enabled or not self._pool or not self._sql or not q: + if not q: + return [] + return self._run(f"%{q}%", limit) + + def list_all(self, *, limit: int = 2000) -> list[dict[str, Any]]: + """전체 상품 목록(낱개+세트). 설정 화면 왼쪽 리스트용.""" + return self._run("%", limit) + + def _run(self, like: str, limit: int) -> list[dict[str, Any]]: + if not self.enabled or not self._pool or not self._sql: return [] - like = f"%{q}%" try: with self._pool.connection() as conn: rows = conn.execute( diff --git a/app/modules/cupang/router.py b/app/modules/cupang/router.py index ed31f53..82e909b 100644 --- a/app/modules/cupang/router.py +++ b/app/modules/cupang/router.py @@ -15,7 +15,7 @@ import json from datetime import date, datetime from typing import Any -from fastapi import APIRouter, Depends, Form, HTTPException, Request +from fastapi import APIRouter, Body, Depends, Form, HTTPException, Request from fastapi.responses import ( HTMLResponse, JSONResponse, @@ -609,14 +609,54 @@ async def products_page(request: Request) -> HTMLResponse: "is_admin": is_admin(user), "nav_items": build_erp_nav(user, active="cupang"), "page_title": "쿠팡 밀크런 — 설정 (제품명)", - "page_subtitle": "제품명 리스트 관리. itemcode_db 에서 검색해 등록하면 폼 드롭다운에 노출됩니다.", + "page_subtitle": "왼쪽 itemcode_db 목록에서 선택해 등록하면 폼 드롭다운에 노출됩니다.", "products": store.list_products(include_inactive=True), + "registered_codes": [p["product_code"] for p in store.list_products(include_inactive=True)], "search_enabled": bool(reader and reader.enabled), "search_reason": (reader.reason if reader else ""), }, ) +@router.get("/api/products/all") +async def product_all( + request: Request, _: dict[str, Any] = Depends(_require_user) +) -> JSONResponse: + """itemcode_db 전체 상품 목록(낱개+세트). 설정 화면 왼쪽 리스트 소스.""" + reader = _itemcode(request) + return JSONResponse( + { + "enabled": bool(reader and reader.enabled), + "reason": (reader.reason if reader else "itemcode 리더 미초기화"), + "results": reader.list_all() if reader else [], + } + ) + + +@router.post("/products/bulk") +async def product_bulk( + request: Request, + items: list[dict[str, Any]] = Body(..., embed=True), + user: dict[str, Any] = Depends(_require_user), +) -> JSONResponse: + """선택한 상품들을 일괄 등록(upsert). body: {"items":[{"code","name"}, ...]}.""" + store = _store(request) + if store is None: + raise HTTPException(status_code=503, detail="cupang_db 미설정") + added = 0 + for it in items: + code = str(it.get("code") or "").strip() + name = str(it.get("name") or "").strip() + if not code or not name: + continue + try: + store.upsert_product(product_code=code, product_name=name) + added += 1 + except ValueError: + continue + return JSONResponse({"ok": True, "added": added}) + + @router.post("/products") async def product_upsert( request: Request, @@ -637,17 +677,37 @@ async def product_upsert( return RedirectResponse(url="/cupang/products", status_code=303) -@router.post("/products/{product_id:int}/delete") -async def product_delete( +@router.post("/products/{product_id:int}/active") +async def product_set_active( request: Request, product_id: int, + active: str = Form(...), user: dict[str, Any] = Depends(_require_user), ) -> RedirectResponse: store = _store(request) if store is None: raise HTTPException(status_code=503, detail="cupang_db 미설정") try: - store.deactivate_product(product_id=product_id) + store.set_product_active( + product_id=product_id, active=active in ("1", "true", "on", "True") + ) + except KeyError: + raise HTTPException(status_code=404, detail="제품을 찾을 수 없습니다.") + return RedirectResponse(url="/cupang/products", status_code=303) + + +@router.post("/products/{product_id:int}/delete") +async def product_delete( + request: Request, + product_id: int, + user: dict[str, Any] = Depends(_require_user), +) -> RedirectResponse: + """완전 삭제(hard delete).""" + store = _store(request) + if store is None: + raise HTTPException(status_code=503, detail="cupang_db 미설정") + try: + store.delete_product(product_id=product_id) except KeyError: raise HTTPException(status_code=404, detail="제품을 찾을 수 없습니다.") return RedirectResponse(url="/cupang/products", status_code=303) diff --git a/app/modules/cupang/templates/cupang/products.html b/app/modules/cupang/templates/cupang/products.html index 6201f17..c1bc9fb 100644 --- a/app/modules/cupang/templates/cupang/products.html +++ b/app/modules/cupang/templates/cupang/products.html @@ -9,102 +9,140 @@ ← 달력 - -
불러오는 중…
ITEMCODE_DB_URL / ITEMCODE_SEARCH_SQL 설정 후 사용 가능합니다.
+ {% endif %}| 제품명 | 제품코드 | 상태 | 동작 |
|---|---|---|---|
| {{ p.product_name }} | +{{ p.product_code }} | +{% if p.active %}활성{% else %}비활성{% endif %} | +
+
+ {% if p.active %}
+
+ {% else %}
+
+ {% endif %}
+
+
+ |
+
| 등록된 제품명이 없습니다. 왼쪽에서 선택해 등록하세요. | |||
| 제품명 | 제품코드 | 정렬 | 상태 | |
|---|---|---|---|---|
| {{ p.product_name }} | -{{ p.product_code }} | -{{ p.sort_order }} | -{% if p.active %}활성{% else %}비활성{% endif %} | -- {% if p.active %} - - {% else %}—{% endif %} - | -