From 57f0c3426f92eb4c8dc2c2893f2851e888b71fbe Mon Sep 17 00:00:00 2001 From: king Date: Sat, 30 May 2026 06:37:51 +0900 Subject: [PATCH] =?UTF-8?q?refactor(cupang):=20UI=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=EB=90=9C=20=EA=B8=B0=EB=8A=A5=EC=9D=98=20?= =?UTF-8?q?=EC=A3=BD=EC=9D=80=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 엑셀 내보내기 전체 제거: /export, /{id}/export, _build_xlsx, _flatten_for_sheet, shipments_for_export, SHEET_COLUMNS - 미사용 라우트 제거: /api/calendar, /api/products/search, /api/box-calc, /{id}/status(상태변경 UI 삭제됨) - 미사용 db 메서드 제거: deactivate_box_rule, get_box_rule, box_rule_map - form.html window.CPG + cpg-search-pop, cupang.js CFG, 검색 드롭다운 CSS 제거 - 템플릿 미사용 statuses 컨텍스트 제거, router import 정리 - 캐시 버전 k Co-Authored-By: Claude Opus 4.8 --- app/modules/cupang/__init__.py | 3 +- app/modules/cupang/db.py | 39 ---- app/modules/cupang/router.py | 185 +----------------- app/modules/cupang/store.py | 15 -- .../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 | 14 +- .../cupang/templates/cupang/index.html | 2 +- .../cupang/templates/cupang/products.html | 2 +- app/static/cupang.css | 13 -- app/static/cupang.js | 1 - app/templates/erp_base.html | 4 +- 13 files changed, 13 insertions(+), 271 deletions(-) diff --git a/app/modules/cupang/__init__.py b/app/modules/cupang/__init__.py index b047d18..5fa598a 100644 --- a/app/modules/cupang/__init__.py +++ b/app/modules/cupang/__init__.py @@ -13,13 +13,12 @@ None 을 반환하고, 라우터가 "설정 필요" 안내 페이지를 보여 from typing import Any from .router import router -from .store import DEFAULT_CENTERS, SHEET_COLUMNS, SHIP_METHODS, STATUSES, compute_boxes +from .store import DEFAULT_CENTERS, SHIP_METHODS, STATUSES, compute_boxes __all__ = [ "router", "STATUSES", "SHIP_METHODS", - "SHEET_COLUMNS", "DEFAULT_CENTERS", "compute_boxes", "build_cupang_store", diff --git a/app/modules/cupang/db.py b/app/modules/cupang/db.py index 582876c..0d2abf3 100644 --- a/app/modules/cupang/db.py +++ b/app/modules/cupang/db.py @@ -144,17 +144,6 @@ class CupangDBStore: ).fetchall() return [self._rule_serialize(r) for r in rows] - def get_box_rule(self, *, product_code: str) -> dict[str, Any] | None: - code = (product_code or "").strip() - if not code: - return None - with self._pool.connection() as conn: - row = conn.execute( - "SELECT * FROM cupang_box_rules WHERE product_code = %s AND active = TRUE", - (code,), - ).fetchone() - return self._rule_serialize(row) if row else None - def upsert_box_rule( self, *, @@ -188,15 +177,6 @@ class CupangDBStore: ).fetchone() return self._rule_serialize(row) - def deactivate_box_rule(self, *, rule_id: int) -> None: - with self._pool.connection() as conn: - cur = conn.execute( - "UPDATE cupang_box_rules SET active = FALSE WHERE id = %s", - (rule_id,), - ) - if cur.rowcount == 0: - raise KeyError(rule_id) - def delete_box_rule(self, *, rule_id: int) -> None: """완전 삭제(hard). 라인의 box_rule_id 는 ON DELETE 미설정이므로 참조 중이면 FK 위반 가능 → 참조 라인의 box_rule_id 를 먼저 NULL 처리.""" @@ -212,10 +192,6 @@ class CupangDBStore: if cur.rowcount == 0: raise KeyError(rule_id) - def box_rule_map(self) -> dict[str, dict[str, Any]]: - """product_code → rule. 폼/계산에서 빠르게 조회하기 위한 dict.""" - return {r["product_code"]: r for r in self.list_box_rules()} - # ════════════════════════════════════════════════════════════ # 제품명 카탈로그 (cupang_products) # itemcode_db 에서 가져와 등록한 제품 목록. 폼의 제품명 드롭다운 소스. @@ -498,21 +474,6 @@ class CupangDBStore: _accumulate(arr, "arrival") return out - def shipments_for_export( - self, *, year: int | None = None, month: int | None = None, shipment_id: int | None = None - ) -> list[dict[str, Any]]: - """엑셀 내보내기용 — 헤더+라인 평탄화 전 단계. 라인 포함 묶음 리스트 반환.""" - if shipment_id is not None: - s = self.get_shipment(shipment_id=shipment_id) - return [s] if s else [] - heads = self.list_shipments(year=year, month=month) - result = [] - for h in heads: - full = self.get_shipment(shipment_id=h["id"]) - if full: - result.append(full) - return result - # ════════════════════════════════════════════════════════════ # 정규화 / 직렬화 helpers # ════════════════════════════════════════════════════════════ diff --git a/app/modules/cupang/router.py b/app/modules/cupang/router.py index c293be7..00d9ab9 100644 --- a/app/modules/cupang/router.py +++ b/app/modules/cupang/router.py @@ -10,21 +10,15 @@ from __future__ import annotations import calendar as _calendar -import io import json -from datetime import date, datetime +from datetime import date from typing import Any from fastapi import APIRouter, Body, Depends, Form, HTTPException, Request -from fastapi.responses import ( - HTMLResponse, - JSONResponse, - RedirectResponse, - StreamingResponse, -) +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from .holidays import is_holiday -from .store import DEFAULT_CENTERS, SHEET_COLUMNS, SHIP_METHODS, STATUSES, compute_boxes +from .store import SHIP_METHODS router = APIRouter(prefix="/cupang", tags=["cupang"]) @@ -184,24 +178,10 @@ async def index(request: Request) -> HTMLResponse: "cal_weeks": cal_weeks, "selected_date": sel, "sel_shipments": sel_shipments, - "statuses": list(STATUSES), }, ) -@router.get("/api/calendar") -async def api_calendar( - request: Request, _: dict[str, Any] = Depends(_require_user) -) -> JSONResponse: - store = _store(request) - if store is None: - raise HTTPException(status_code=503, detail="cupang_db 미설정") - year, month = _ym(request) - return JSONResponse( - {"year": year, "month": month, "counts": store.calendar_counts(year=year, month=month)} - ) - - # ════════════════════════════════════════════════════════════ # 출고 묶음 — 등록 / 수정 / 상세 # ════════════════════════════════════════════════════════════ @@ -218,7 +198,6 @@ def _form_context(request: Request, store: Any, user: dict[str, Any]) -> dict[st "box_rules": store.list_box_rules(), "products": store.list_products(), "ship_methods": list(SHIP_METHODS), - "statuses": list(STATUSES), "search_enabled": bool(reader and reader.enabled), } @@ -308,7 +287,6 @@ async def detail(request: Request, shipment_id: int) -> HTMLResponse: "page_title": f"출고 #{ship['id']}", "page_subtitle": f"{ship['ship_date']} · {ship['center_name_snapshot']}", "shipment": ship, - "statuses": list(STATUSES), }, ) @@ -383,25 +361,6 @@ async def update( return RedirectResponse(url=f"/cupang/{shipment_id}", status_code=303) -@router.post("/{shipment_id:int}/status") -async def change_status( - request: Request, - shipment_id: int, - status: 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.set_status(shipment_id=shipment_id, status=status) - except KeyError: - raise HTTPException(status_code=404, detail="출고 묶음을 찾을 수 없습니다.") - except ValueError as exc: - raise HTTPException(status_code=400, detail=str(exc)) - return RedirectResponse(url=f"/cupang/{shipment_id}", status_code=303) - - @router.post("/{shipment_id:int}/delete") async def delete( request: Request, @@ -725,144 +684,6 @@ async def product_delete( return RedirectResponse(url="/cupang/products", status_code=303) -# ════════════════════════════════════════════════════════════ -# 상품 검색 (itemcode_db 읽기 전용) + 박스 계산 미리보기 -# ════════════════════════════════════════════════════════════ -@router.get("/api/products/search") -async def product_search( - request: Request, - q: str = "", - user: dict[str, Any] = Depends(_require_user), -) -> JSONResponse: - reader = _itemcode(request) - store = _store(request) - results = reader.search(q) if reader else [] - # box_rule 의 units_per_box 를 결과에 병합 (있으면) - rule_map = store.box_rule_map() if store else {} - for r in results: - rule = rule_map.get(r["code"]) - r["units_per_box"] = rule["units_per_box"] if rule else None - r["box_rule_id"] = rule["id"] if rule else None - return JSONResponse( - { - "enabled": bool(reader and reader.enabled), - "reason": (reader.reason if reader else "itemcode 리더 미초기화"), - "results": results, - } - ) - - -@router.get("/api/box-calc") -async def box_calc( - request: Request, - product_code: str = "", - quantity: int = 0, - units_per_box: str = "", - user: dict[str, Any] = Depends(_require_user), -) -> JSONResponse: - store = _store(request) - upb: int | None - if units_per_box.strip(): - try: - upb = int(units_per_box) - except ValueError: - upb = None - else: - rule = store.get_box_rule(product_code=product_code) if store else None - upb = rule["units_per_box"] if rule else None - return JSONResponse(compute_boxes(quantity, upb)) - - -# ════════════════════════════════════════════════════════════ -# 엑셀 내보내기 (구글시트 컬럼 순서 유지) -# ════════════════════════════════════════════════════════════ -def _flatten_for_sheet(shipments: list[dict[str, Any]]) -> list[list[Any]]: - """묶음 → 시트 행. 같은 묶음 2번째 라인부터 공통 헤더 칸은 비운다.""" - rows: list[list[Any]] = [] - seq = 0 - for s in shipments: - lines = s.get("lines") or [] - for i, ln in enumerate(lines): - seq += 1 - if i == 0: - rows.append([ - seq, - s.get("document_date", ""), - s.get("ship_date", ""), - s.get("center_arrival_date", ""), - s.get("center_name_snapshot", ""), - s.get("ship_method", ""), - ln.get("product_code", ""), - ln.get("product_name_snapshot", ""), - ln.get("quantity", 0), - s.get("outbound_summary", ""), - s.get("worker", ""), - ]) - else: - # 공통 헤더 칸 비움 (구분/제품/수량만 채움) - rows.append([ - seq, "", "", "", "", "", - ln.get("product_code", ""), - ln.get("product_name_snapshot", ""), - ln.get("quantity", 0), - "", "", - ]) - return rows - - -@router.get("/export") -async def export_month( - request: Request, - user: dict[str, Any] = Depends(_require_user), -) -> StreamingResponse: - store = _store(request) - if store is None: - raise HTTPException(status_code=503, detail="cupang_db 미설정") - year, month = _ym(request) - shipments = store.shipments_for_export(year=year, month=month) - fname = f"cupang_milkrun_{year:04d}{month:02d}.xlsx" - return _build_xlsx(shipments, fname, sheet_title=f"{year}-{month:02d}") - - -@router.get("/{shipment_id:int}/export") -async def export_one( - request: Request, - shipment_id: int, - user: dict[str, Any] = Depends(_require_user), -) -> StreamingResponse: - store = _store(request) - if store is None: - raise HTTPException(status_code=503, detail="cupang_db 미설정") - shipments = store.shipments_for_export(shipment_id=shipment_id) - if not shipments: - raise HTTPException(status_code=404, detail="출고 묶음을 찾을 수 없습니다.") - fname = f"cupang_milkrun_{shipment_id}.xlsx" - return _build_xlsx(shipments, fname, sheet_title=f"출고{shipment_id}") - - -def _build_xlsx(shipments: list[dict[str, Any]], fname: str, *, sheet_title: str) -> StreamingResponse: - from openpyxl import Workbook # 지연 import - - wb = Workbook() - ws = wb.active - ws.title = sheet_title[:31] or "cupang" - ws.append(list(SHEET_COLUMNS)) - for row in _flatten_for_sheet(shipments): - ws.append(row) - widths = [6, 12, 12, 12, 12, 10, 14, 26, 8, 24, 12] - for col, w in enumerate(widths, start=1): - ws.column_dimensions[ws.cell(row=1, column=col).column_letter].width = w - - buf = io.BytesIO() - wb.save(buf) - buf.seek(0) - return StreamingResponse( - buf, - media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - headers={"Content-Disposition": f'attachment; filename="{fname}"'}, - ) - - @router.get("/health") async def health() -> dict[str, str]: return {"status": "ok", "module": "cupang"} diff --git a/app/modules/cupang/store.py b/app/modules/cupang/store.py index 6280119..da4353c 100644 --- a/app/modules/cupang/store.py +++ b/app/modules/cupang/store.py @@ -23,21 +23,6 @@ STATUSES: tuple[str, ...] = ( # 출고방식 기본 후보 (자유 입력 허용, 아래는 select 기본값) SHIP_METHODS: tuple[str, ...] = ("택배", "직접배송", "화물", "파렛트", "기타") -# 엑셀/시트 컬럼 순서 — 기존 구글시트와 동일하게 유지 -SHEET_COLUMNS: tuple[str, ...] = ( - "구분", - "작성일", - "출고일", - "센터입고일", - "입고센터", - "출고방식", - "제품코드", - "제품명", - "수량", - "출고", - "작업자", -) - # 초기 입고센터 seed — cupang_db_init.sql 에도 동일 목록을 INSERT 한다. # 화면에서 추가/수정/비활성화 가능. 사용 중인 센터는 hard delete 하지 않는다. DEFAULT_CENTERS: tuple[str, ...] = ( diff --git a/app/modules/cupang/templates/cupang/box_rules.html b/app/modules/cupang/templates/cupang/box_rules.html index df15139..5f2f76d 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 0d9ae21..c922698 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 82cc5f5..343d15e 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 f314430..44f9d16 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 %}
@@ -96,9 +96,6 @@ - - -
- {% endblock %} -{% block scripts %}{% endblock %} +{% block scripts %}{% endblock %} diff --git a/app/modules/cupang/templates/cupang/index.html b/app/modules/cupang/templates/cupang/index.html index bb76207..c0ae8ef 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 f51275b..d904b16 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 %}
diff --git a/app/static/cupang.css b/app/static/cupang.css index 90c5a4d..931ee0a 100644 --- a/app/static/cupang.css +++ b/app/static/cupang.css @@ -270,16 +270,3 @@ .cpg-upb-unit { font-size: 13px; color: var(--color-midtone-gray); } /* 메모: 프레임 전체 너비 */ .cpg .cpg-brule-fields .cpg-brule-memo { width: 100%; min-width: 0; max-width: 100%; box-sizing: border-box; } - -/* ── 상품 검색 드롭다운 ── */ -.cpg-search-pop { - position: absolute; z-index: 50; - background: #fff; border: 1px solid var(--color-subtle-ash); - border-radius: 10px; box-shadow: 0 4px 16px rgba(0,0,0,.08); - max-height: 260px; overflow-y: auto; min-width: 280px; -} -.cpg-search-item { padding: 8px 12px; cursor: pointer; font-size: 13px; } -.cpg-search-item:hover, .cpg-search-item.is-active { background: var(--color-ghost-gray); } -.cpg-search-item .cpg-sc { font-weight: 600; } -.cpg-search-item .cpg-sn { color: var(--color-midtone-gray); margin-left: 8px; } -.cpg-search-empty { padding: 10px 12px; font-size: 12px; color: var(--color-midtone-gray); } diff --git a/app/static/cupang.js b/app/static/cupang.js index 5a03e5f..fa9ac94 100644 --- a/app/static/cupang.js +++ b/app/static/cupang.js @@ -3,7 +3,6 @@ 수량 입력 시 박스 수 미리보기(서버가 저장 시 store.compute_boxes 로 재계산). */ (function () { "use strict"; - var CFG = window.CPG || {}; var body = document.getElementById("cpg-lines-body"); var form = document.getElementById("cpg-form"); if (!body || !form) return; diff --git a/app/templates/erp_base.html b/app/templates/erp_base.html index 8fa686a..9324a27 100644 --- a/app/templates/erp_base.html +++ b/app/templates/erp_base.html @@ -4,8 +4,8 @@ {{ page_title or "ERP" }} — DBX Corporation - - + + {% block head_extra %}{% endblock %}