feat(cupang): 제품 카탈로그에 쿠팡상품코드 + 상태 토글
- cupang_products.coupang_item_code 컬럼 추가 (마이그레이션 004, init.sql 동기화)
- 설정 화면: 쿠팡상품코드 열 표시/정렬, 수기 추가 폼 입력란 추가
- 선택 등록 시 바로 등록하지 않고 쿠팡상품코드 입력 팝업을 먼저 표시
- 상태 배지를 클릭 토글로 변경 (POST /cupang/api/products/{id}/toggle)
- 빈 쿠팡상품코드는 "미입력"으로 처리해 기존 값을 덮어쓰지 않음
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user