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 typing import Any
from .router import router 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__ = [ __all__ = [
"router", "router",
"STATUSES", "STATUSES",
"SHIP_METHODS", "SHIP_METHODS",
"SHEET_COLUMNS",
"DEFAULT_CENTERS", "DEFAULT_CENTERS",
"compute_boxes", "compute_boxes",
"build_cupang_store", "build_cupang_store",
-39
View File
@@ -144,17 +144,6 @@ class CupangDBStore:
).fetchall() ).fetchall()
return [self._rule_serialize(r) for r in rows] 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( def upsert_box_rule(
self, self,
*, *,
@@ -188,15 +177,6 @@ class CupangDBStore:
).fetchone() ).fetchone()
return self._rule_serialize(row) 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: def delete_box_rule(self, *, rule_id: int) -> None:
"""완전 삭제(hard). 라인의 box_rule_id 는 ON DELETE 미설정이므로 """완전 삭제(hard). 라인의 box_rule_id 는 ON DELETE 미설정이므로
참조 중이면 FK 위반 가능 → 참조 라인의 box_rule_id 를 먼저 NULL 처리.""" 참조 중이면 FK 위반 가능 → 참조 라인의 box_rule_id 를 먼저 NULL 처리."""
@@ -212,10 +192,6 @@ class CupangDBStore:
if cur.rowcount == 0: if cur.rowcount == 0:
raise KeyError(rule_id) 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) # 제품명 카탈로그 (cupang_products)
# itemcode_db 에서 가져와 등록한 제품 목록. 폼의 제품명 드롭다운 소스. # itemcode_db 에서 가져와 등록한 제품 목록. 폼의 제품명 드롭다운 소스.
@@ -498,21 +474,6 @@ class CupangDBStore:
_accumulate(arr, "arrival") _accumulate(arr, "arrival")
return out 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 # 정규화 / 직렬화 helpers
# ════════════════════════════════════════════════════════════ # ════════════════════════════════════════════════════════════
+3 -182
View File
@@ -10,21 +10,15 @@
from __future__ import annotations from __future__ import annotations
import calendar as _calendar import calendar as _calendar
import io
import json import json
from datetime import date, datetime from datetime import date
from typing import Any from typing import Any
from fastapi import APIRouter, Body, Depends, Form, HTTPException, Request from fastapi import APIRouter, Body, Depends, Form, HTTPException, Request
from fastapi.responses import ( from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
HTMLResponse,
JSONResponse,
RedirectResponse,
StreamingResponse,
)
from .holidays import is_holiday 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"]) router = APIRouter(prefix="/cupang", tags=["cupang"])
@@ -184,24 +178,10 @@ async def index(request: Request) -> HTMLResponse:
"cal_weeks": cal_weeks, "cal_weeks": cal_weeks,
"selected_date": sel, "selected_date": sel,
"sel_shipments": sel_shipments, "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(), "box_rules": store.list_box_rules(),
"products": store.list_products(), "products": store.list_products(),
"ship_methods": list(SHIP_METHODS), "ship_methods": list(SHIP_METHODS),
"statuses": list(STATUSES),
"search_enabled": bool(reader and reader.enabled), "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_title": f"출고 #{ship['id']}",
"page_subtitle": f"{ship['ship_date']} · {ship['center_name_snapshot']}", "page_subtitle": f"{ship['ship_date']} · {ship['center_name_snapshot']}",
"shipment": ship, "shipment": ship,
"statuses": list(STATUSES),
}, },
) )
@@ -383,25 +361,6 @@ async def update(
return RedirectResponse(url=f"/cupang/{shipment_id}", status_code=303) 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") @router.post("/{shipment_id:int}/delete")
async def delete( async def delete(
request: Request, request: Request,
@@ -725,144 +684,6 @@ async def product_delete(
return RedirectResponse(url="/cupang/products", status_code=303) 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") @router.get("/health")
async def health() -> dict[str, str]: async def health() -> dict[str, str]:
return {"status": "ok", "module": "cupang"} return {"status": "ok", "module": "cupang"}
-15
View File
@@ -23,21 +23,6 @@ STATUSES: tuple[str, ...] = (
# 출고방식 기본 후보 (자유 입력 허용, 아래는 select 기본값) # 출고방식 기본 후보 (자유 입력 허용, 아래는 select 기본값)
SHIP_METHODS: tuple[str, ...] = ("택배", "직접배송", "화물", "파렛트", "기타") SHIP_METHODS: tuple[str, ...] = ("택배", "직접배송", "화물", "파렛트", "기타")
# 엑셀/시트 컬럼 순서 — 기존 구글시트와 동일하게 유지
SHEET_COLUMNS: tuple[str, ...] = (
"구분",
"작성일",
"출고일",
"센터입고일",
"입고센터",
"출고방식",
"제품코드",
"제품명",
"수량",
"출고",
"작업자",
)
# 초기 입고센터 seed — cupang_db_init.sql 에도 동일 목록을 INSERT 한다. # 초기 입고센터 seed — cupang_db_init.sql 에도 동일 목록을 INSERT 한다.
# 화면에서 추가/수정/비활성화 가능. 사용 중인 센터는 hard delete 하지 않는다. # 화면에서 추가/수정/비활성화 가능. 사용 중인 센터는 hard delete 하지 않는다.
DEFAULT_CENTERS: tuple[str, ...] = ( DEFAULT_CENTERS: tuple[str, ...] = (
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% 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 %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% 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 %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% 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 %} {% block content %}
<section class="cpg"> <section class="cpg">
+2 -12
View File
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% 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 %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -96,9 +96,6 @@
</div><!-- /cpg-form-2col --> </div><!-- /cpg-form-2col -->
</form> </form>
<!-- 검색 결과 드롭다운 (JS 가 위치 이동) -->
<div id="cpg-search-pop" class="cpg-search-pop" hidden></div>
</section> </section>
<script type="application/json" id="cpg-init-lines"> <script type="application/json" id="cpg-init-lines">
@@ -110,13 +107,6 @@
<script type="application/json" id="cpg-products"> <script type="application/json" id="cpg-products">
{{ products | tojson }} {{ products | tojson }}
</script> </script>
<script>
window.CPG = {
searchEnabled: {{ 'true' if search_enabled else 'false' }},
searchUrl: "/cupang/api/products/search",
calcUrl: "/cupang/api/box-calc"
};
</script>
{% endblock %} {% 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" %} {% 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 %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% 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 %} {% block content %}
<section class="cpg"> <section class="cpg">
-13
View File
@@ -270,16 +270,3 @@
.cpg-upb-unit { font-size: 13px; color: var(--color-midtone-gray); } .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 .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 로 재계산). */ 수량 입력 시 박스 수 미리보기(서버가 저장 시 store.compute_boxes 로 재계산). */
(function () { (function () {
"use strict"; "use strict";
var CFG = window.CPG || {};
var body = document.getElementById("cpg-lines-body"); var body = document.getElementById("cpg-lines-body");
var form = document.getElementById("cpg-form"); var form = document.getElementById("cpg-form");
if (!body || !form) return; if (!body || !form) return;
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ page_title or "ERP" }} — DBX Corporation</title> <title>{{ page_title or "ERP" }} — DBX Corporation</title>
<link rel="stylesheet" href="/static/erp.css?v=20260530j" /> <link rel="stylesheet" href="/static/erp.css?v=20260530k" />
<link rel="stylesheet" href="/static/erp-shell.css?v=20260530j" /> <link rel="stylesheet" href="/static/erp-shell.css?v=20260530k" />
<link rel="stylesheet" href="/static/erp-attach-viewer.css" /> <link rel="stylesheet" href="/static/erp-attach-viewer.css" />
{% block head_extra %}{% endblock %} {% block head_extra %}{% endblock %}
</head> </head>