feat(cupang): 박스 계산 임시 저장 / 불러오기
계산 중 화면 상태를 서버에 이름 붙여 저장하고 다시 불러온다. 다른 PC 에서도
이어서 작업할 수 있어야 해서 localStorage 대신 DB 에 둔다.
저장 대상은 화면 스냅샷(입력 품목·열어둔 센터·분배 내역·도장 상태)뿐이고,
박스 수는 불러온 뒤 기존 계산 API 로 서버에서 다시 구한다. 저장값을 그대로
믿지 않으므로 입수량 규칙이 바뀌어도 어긋나지 않고, 계산 결과에서 사라진
항목은 제외한 건수를 알려준다. 같은 제목으로 저장하면 덮어써 목록이 무한히
늘어나지 않는다.
- scripts/sql/cupang_db_003_box_calc_drafts.sql (신규 테이블 + 권한, 멱등)
- db.py: list/get/save/delete_box_calc_draft
- router: GET·POST /cupang/api/box-calc/drafts, GET·DELETE /{id}
- box_calc.html: [임시 저장]·[불러오기] 버튼 + 목록 대화상자
This commit is contained in:
@@ -18,6 +18,7 @@ from datetime import date, datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from app.timezone import KST
|
||||
@@ -194,6 +195,90 @@ class CupangDBStore:
|
||||
if cur.rowcount == 0:
|
||||
raise KeyError(rule_id)
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 박스 계산 임시 저장 (cupang_box_calc_drafts)
|
||||
# payload 는 화면 상태 스냅샷(JSONB). 박스 수 계산은 불러온 뒤
|
||||
# 서버에서 다시 하므로 여기 값은 신뢰 대상이 아니다.
|
||||
# ════════════════════════════════════════════════════════════
|
||||
def list_box_calc_drafts(self, *, limit: int = 50) -> list[dict[str, Any]]:
|
||||
"""목록용 — payload 는 크므로 제외한다."""
|
||||
with self._pool.connection() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, title, created_by, created_at, updated_at
|
||||
FROM cupang_box_calc_drafts
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT %s
|
||||
""",
|
||||
(int(limit),),
|
||||
).fetchall()
|
||||
return [self._draft_serialize(r) for r in rows]
|
||||
|
||||
def get_box_calc_draft(self, *, draft_id: int) -> dict[str, Any] | None:
|
||||
with self._pool.connection() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM cupang_box_calc_drafts WHERE id = %s", (draft_id,)
|
||||
).fetchone()
|
||||
return self._draft_serialize(row) if row else None
|
||||
|
||||
def save_box_calc_draft(
|
||||
self,
|
||||
*,
|
||||
title: str,
|
||||
payload: dict[str, Any],
|
||||
created_by: str = "",
|
||||
draft_id: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""draft_id 가 있으면 덮어쓰고, 없으면 새로 만든다.
|
||||
|
||||
같은 제목이 이미 있으면 그 건을 덮어쓴다(임시 저장이라 목록이 무한히
|
||||
늘어나지 않게).
|
||||
"""
|
||||
name = (title or "").strip()
|
||||
if not name:
|
||||
raise ValueError("제목 필수")
|
||||
data = Jsonb(payload if isinstance(payload, dict) else {})
|
||||
|
||||
with self._pool.connection() as conn:
|
||||
if draft_id is None:
|
||||
hit = conn.execute(
|
||||
"SELECT id FROM cupang_box_calc_drafts WHERE title = %s",
|
||||
(name,),
|
||||
).fetchone()
|
||||
if hit:
|
||||
draft_id = int(hit["id"])
|
||||
|
||||
if draft_id is not None:
|
||||
row = conn.execute(
|
||||
"""
|
||||
UPDATE cupang_box_calc_drafts
|
||||
SET title = %s, payload = %s, created_by = %s
|
||||
WHERE id = %s
|
||||
RETURNING *
|
||||
""",
|
||||
(name, data, (created_by or "").strip(), draft_id),
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise KeyError(draft_id)
|
||||
else:
|
||||
row = conn.execute(
|
||||
"""
|
||||
INSERT INTO cupang_box_calc_drafts (title, payload, created_by)
|
||||
VALUES (%s, %s, %s)
|
||||
RETURNING *
|
||||
""",
|
||||
(name, data, (created_by or "").strip()),
|
||||
).fetchone()
|
||||
return self._draft_serialize(row)
|
||||
|
||||
def delete_box_calc_draft(self, *, draft_id: int) -> None:
|
||||
with self._pool.connection() as conn:
|
||||
cur = conn.execute(
|
||||
"DELETE FROM cupang_box_calc_drafts WHERE id = %s", (draft_id,)
|
||||
)
|
||||
if cur.rowcount == 0:
|
||||
raise KeyError(draft_id)
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 제품명 카탈로그 (cupang_products)
|
||||
# itemcode_db 에서 가져와 등록한 제품 목록. 폼의 제품명 드롭다운 소스.
|
||||
@@ -577,6 +662,21 @@ class CupangDBStore:
|
||||
out[k] = v.astimezone(KST).isoformat(timespec="seconds")
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _draft_serialize(row: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
if row is None:
|
||||
return None
|
||||
out = dict(row)
|
||||
out["id"] = int(out["id"])
|
||||
out["title"] = str(out.get("title") or "")
|
||||
if "payload" in out and not isinstance(out.get("payload"), dict):
|
||||
out["payload"] = {}
|
||||
for k in ("created_at", "updated_at"):
|
||||
v = out.get(k)
|
||||
if isinstance(v, datetime):
|
||||
out[k] = v.astimezone(KST).isoformat(timespec="seconds")
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _product_serialize(row: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
if row is None:
|
||||
|
||||
Reference in New Issue
Block a user