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
+1 -2
View File
@@ -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",
-39
View File
@@ -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
# ════════════════════════════════════════════════════════════
+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"}
-15
View File
@@ -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, ...] = (
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530j" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530k" />{% endblock %}
{% block content %}
<section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530j" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530k" />{% endblock %}
{% block content %}
<section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530j" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530k" />{% endblock %}
{% block content %}
<section class="cpg">
+2 -12
View File
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530j" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530k" />{% endblock %}
{% block content %}
<section class="cpg">
@@ -96,9 +96,6 @@
</div><!-- /cpg-form-2col -->
</form>
<!-- 검색 결과 드롭다운 (JS 가 위치 이동) -->
<div id="cpg-search-pop" class="cpg-search-pop" hidden></div>
</section>
<script type="application/json" id="cpg-init-lines">
@@ -110,13 +107,6 @@
<script type="application/json" id="cpg-products">
{{ products | tojson }}
</script>
<script>
window.CPG = {
searchEnabled: {{ 'true' if search_enabled else 'false' }},
searchUrl: "/cupang/api/products/search",
calcUrl: "/cupang/api/box-calc"
};
</script>
{% endblock %}
{% block scripts %}<script src="/static/cupang.js?v=20260530j" defer></script>{% endblock %}
{% block scripts %}<script src="/static/cupang.js?v=20260530k" defer></script>{% endblock %}
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530j" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530k" />{% endblock %}
{% block content %}
<section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530j" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530k" />{% endblock %}
{% block content %}
<section class="cpg">
-13
View File
@@ -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); }
-1
View File
@@ -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;
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ page_title or "ERP" }} — DBX Corporation</title>
<link rel="stylesheet" href="/static/erp.css?v=20260530j" />
<link rel="stylesheet" href="/static/erp-shell.css?v=20260530j" />
<link rel="stylesheet" href="/static/erp.css?v=20260530k" />
<link rel="stylesheet" href="/static/erp-shell.css?v=20260530k" />
<link rel="stylesheet" href="/static/erp-attach-viewer.css" />
{% block head_extra %}{% endblock %}
</head>