refactor(cupang): UI에서 제거된 기능의 죽은 코드 정리

- 엑셀 내보내기 전체 제거: /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 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 06:37:51 +09:00
parent bcbf3de24c
commit 57f0c3426f
13 changed files with 13 additions and 271 deletions
+3 -182
View File
@@ -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"}