feat(cupang): 설정 화면 2열 개편 — itemcode 다중선택 등록 + 활성/삭제

- 수동 등록 카드 제거
- 왼쪽: itemcode_db 전체 목록(낱개+세트) 다중선택, 등록된 항목 진회색+흰글씨
  /cupang/api/products/all + /cupang/products/bulk(JSON 일괄 upsert)
- 오른쪽: 등록 제품명 목록 — 제품명/코드/상태 + 활성·비활성 토글 + 완전삭제
  /products/{id}/active (토글), /products/{id}/delete (hard delete)
- ItemcodeReader.list_all 추가

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 03:46:35 +09:00
parent 96e2c49506
commit 1d33761003
5 changed files with 229 additions and 94 deletions
+65 -5
View File
@@ -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)