refactor(cupang): 입고센터 관리 화면 제거
- /cupang/centers GET/POST, 수정·삭제 라우트와 centers.html 삭제 - 달력 상단 "입고센터 관리" 버튼 제거, 상자 계산의 안내 문구 정리 - 센터 관리 화면 전용 CSS 제거(공용 .cpg-icon-btn/.cpg-btn-sm 은 유지) - 센터 데이터(cupang_centers)와 db 계층은 그대로 — 출고 확정·발주 업로드에서 계속 사용 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,10 +11,8 @@ from __future__ import annotations
|
||||
|
||||
import calendar as _calendar
|
||||
import json
|
||||
import re
|
||||
from fractions import Fraction
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
from datetime import date as _date, timedelta as _timedelta
|
||||
|
||||
@@ -558,150 +556,6 @@ async def day_delete(
|
||||
)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 입고센터 관리
|
||||
# ════════════════════════════════════════════════════════════
|
||||
_CENTER_PREFIX_RE = re.compile(r"^\D*")
|
||||
|
||||
|
||||
def _center_group(name: str) -> str:
|
||||
"""센터명 앞부분(숫자 전까지)을 지역 그룹 키로 쓴다. 예) 인천14 → 인천."""
|
||||
name = (name or "").strip()
|
||||
prefix = _CENTER_PREFIX_RE.match(name).group(0).strip()
|
||||
return prefix or name or "기타"
|
||||
|
||||
|
||||
def _center_sort_key(name: str) -> list[Any]:
|
||||
"""숫자를 숫자로 비교하는 자연 정렬. 예) 인천4 < 인천14."""
|
||||
parts = re.split(r"(\d+)", (name or "").strip())
|
||||
return [(1, int(p), "") if p.isdigit() else (0, 0, p.lower()) for p in parts]
|
||||
|
||||
|
||||
def _group_centers(centers: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
buckets: dict[str, list[dict[str, Any]]] = {}
|
||||
for c in centers:
|
||||
buckets.setdefault(_center_group(c["name"]), []).append(c)
|
||||
groups = []
|
||||
for key, items in buckets.items():
|
||||
items.sort(key=lambda c: _center_sort_key(c["name"]))
|
||||
groups.append({"name": key, "items": items, "count": len(items)})
|
||||
groups.sort(key=lambda g: _center_sort_key(g["name"]))
|
||||
return groups
|
||||
|
||||
|
||||
@router.get("/centers", response_class=HTMLResponse)
|
||||
async def centers_page(request: Request) -> HTMLResponse:
|
||||
from app.main import build_erp_nav, render_template # noqa: WPS433
|
||||
from app.store import is_admin # noqa: WPS433
|
||||
|
||||
guard = _guard(request)
|
||||
if not isinstance(guard, tuple):
|
||||
return guard
|
||||
store, user = guard
|
||||
centers = sorted(
|
||||
store.list_centers(include_inactive=True),
|
||||
key=lambda c: _center_sort_key(c["name"]),
|
||||
)
|
||||
return render_template(
|
||||
request,
|
||||
"cupang/centers.html",
|
||||
{
|
||||
"user": user,
|
||||
"is_admin": is_admin(user),
|
||||
"nav_items": build_erp_nav(user, active="cupang"),
|
||||
"page_title": "쿠팡 밀크런 — 입고센터 관리",
|
||||
"page_subtitle": "센터명 등록 · 수정 · 활성/비활성 · 삭제.",
|
||||
"centers": centers,
|
||||
"center_groups": _group_centers(centers),
|
||||
"flash": request.query_params.get("msg", ""),
|
||||
"flash_name": request.query_params.get("name", ""),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/centers")
|
||||
async def center_create(
|
||||
request: Request,
|
||||
name: str = Form(...),
|
||||
sort_order: int = Form(0),
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
) -> RedirectResponse:
|
||||
store = _store(request)
|
||||
if store is None:
|
||||
raise HTTPException(status_code=503, detail="cupang_db 미설정")
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
return RedirectResponse(url="/cupang/centers", status_code=303)
|
||||
|
||||
# 같은 이름이 이미 있으면 새로 만들지 않고 알림만 돌려준다.
|
||||
existing = next(
|
||||
(
|
||||
c
|
||||
for c in store.list_centers(include_inactive=True)
|
||||
if (c.get("name") or "").strip().lower() == name.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if existing is not None:
|
||||
msg = "dup" if existing.get("active") else "dup_inactive"
|
||||
return RedirectResponse(
|
||||
url=f"/cupang/centers?msg={msg}&name={quote(name)}", status_code=303
|
||||
)
|
||||
|
||||
try:
|
||||
store.create_center(name=name, sort_order=sort_order)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
return RedirectResponse(
|
||||
url=f"/cupang/centers?msg=added&name={quote(name)}", status_code=303
|
||||
)
|
||||
|
||||
|
||||
@router.post("/centers/{center_id}/edit")
|
||||
async def center_edit(
|
||||
request: Request,
|
||||
center_id: int,
|
||||
active: str = Form(""),
|
||||
sort_order: 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 미설정")
|
||||
# 이름 변경은 지원하지 않는다(기존 출고 이력과 어긋날 수 있음). 활성/정렬만 수정.
|
||||
kwargs: dict[str, Any] = {"center_id": center_id}
|
||||
if active != "":
|
||||
kwargs["active"] = active in ("1", "true", "on", "True")
|
||||
if sort_order.strip():
|
||||
try:
|
||||
kwargs["sort_order"] = int(sort_order)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
store.update_center(**kwargs)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="센터를 찾을 수 없습니다.")
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
return RedirectResponse(url="/cupang/centers", status_code=303)
|
||||
|
||||
|
||||
@router.post("/centers/{center_id}/delete")
|
||||
async def center_delete(
|
||||
request: Request,
|
||||
center_id: int,
|
||||
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.delete_center(center_id=center_id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="센터를 찾을 수 없습니다.")
|
||||
return RedirectResponse(url="/cupang/centers", status_code=303)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 상자 입수량 관리
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903e" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903f" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
@@ -108,7 +108,7 @@
|
||||
|
||||
{% if not centers %}
|
||||
<p class="erp-muted">
|
||||
등록된 입고센터가 없습니다. <a href="/cupang/centers">입고센터 관리</a>에서 먼저 센터를 추가하세요.
|
||||
등록된 입고센터가 없습니다. 관리자에게 문의하세요.
|
||||
</p>
|
||||
{% else %}
|
||||
<div class="cpg-dist-list" id="cpg-dist-list"></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903e" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903f" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903e" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
|
||||
<div class="erp-page-actions">
|
||||
<a class="erp-btn erp-btn-primary" href="/cupang/">◀◀ 달력</a>
|
||||
</div>
|
||||
|
||||
<div class="erp-card cpg-form-card cpg-center-card">
|
||||
|
||||
<!-- 상단: 추가 + 검색 -->
|
||||
<div class="cpg-center-bar">
|
||||
<form method="post" action="/cupang/centers" class="cpg-center-addform">
|
||||
<input class="erp-input cpg-center-addinput" type="text" name="name"
|
||||
placeholder="센터명 입력 (예: 대구3)" required autocomplete="off" />
|
||||
<button type="submit" class="erp-btn erp-btn-primary">추가</button>
|
||||
</form>
|
||||
<input class="erp-input cpg-center-search" type="search" id="cpgCenterSearch"
|
||||
placeholder="센터 검색" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="cpg-card-head cpg-center-head">
|
||||
<h2>센터 목록 <span class="erp-muted" id="cpgCenterCount">({{ centers|length }})</span></h2>
|
||||
<span class="erp-muted">지역별로 묶어 표시. 이름 변경은 지원하지 않음 — 삭제 후 새로 등록.</span>
|
||||
</div>
|
||||
|
||||
<!-- 목록: 지역 그룹 패널, 위→아래로 읽고 열로 흐름 -->
|
||||
<div class="cpg-center-cols" id="cpgCenterGrid">
|
||||
{% for g in center_groups %}
|
||||
<section class="cpg-center-group" data-group="{{ g.name|lower }}">
|
||||
<button type="button" class="cpg-center-gtitle" aria-expanded="true">
|
||||
<span class="cpg-center-caret" aria-hidden="true"></span>
|
||||
<span class="cpg-center-gname">{{ g.name }}</span>
|
||||
<span class="cpg-center-gcount">{{ g.count }}</span>
|
||||
</button>
|
||||
<div class="cpg-center-glist">
|
||||
{% for c in g["items"] %}
|
||||
<div class="cpg-center-row {% if not c.active %}is-inactive{% endif %}" data-name="{{ c.name|lower }}">
|
||||
<span class="cpg-center-dot" title="{% if c.active %}활성{% else %}비활성{% endif %}"></span>
|
||||
<span class="cpg-center-name">{{ c.name }}</span>
|
||||
<span class="cpg-center-acts">
|
||||
<form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form">
|
||||
<input type="hidden" name="active" value="{% if c.active %}0{% else %}1{% endif %}" />
|
||||
<button type="submit" class="cpg-icon-btn"
|
||||
title="{% if c.active %}비활성으로 전환{% else %}활성으로 전환{% endif %}">{% if c.active %}비활성{% else %}활성{% endif %}</button>
|
||||
</form>
|
||||
<button type="button" class="cpg-icon-btn is-danger" title="삭제"
|
||||
data-del-url="/cupang/centers/{{ c.id }}/delete" data-del-name="{{ c.name }}">삭제</button>
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<p class="cpg-center-empty erp-muted" id="cpgCenterEmpty" hidden>검색 결과 없음</p>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 화면 가운데 알림/확인 팝업 -->
|
||||
<div class="cpg-modal" id="cpgCenterDlg" hidden>
|
||||
<div class="cpg-modal-back" data-dlg-close></div>
|
||||
<div class="cpg-modal-box cpg-center-dlg" role="dialog" aria-modal="true" aria-labelledby="cpgCenterDlgTitle">
|
||||
<h3 id="cpgCenterDlgTitle">알림</h3>
|
||||
<p class="cpg-center-dlgmsg" id="cpgCenterDlgMsg"></p>
|
||||
<div class="cpg-dlg-actions cpg-center-dlgact">
|
||||
<button type="button" class="erp-btn erp-btn-outline" id="cpgCenterDlgCancel" data-dlg-close hidden>취소</button>
|
||||
<button type="button" class="erp-btn erp-btn-primary" id="cpgCenterDlgOk">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" id="cpgCenterDelForm" hidden></form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// ── 가운데 팝업 (alert/confirm 대체) ──
|
||||
var dlg = document.getElementById('cpgCenterDlg');
|
||||
var dlgTitle = document.getElementById('cpgCenterDlgTitle');
|
||||
var dlgMsg = document.getElementById('cpgCenterDlgMsg');
|
||||
var dlgOk = document.getElementById('cpgCenterDlgOk');
|
||||
var dlgCancel = document.getElementById('cpgCenterDlgCancel');
|
||||
var onConfirm = null;
|
||||
var lastFocus = null;
|
||||
|
||||
function closeDlg() {
|
||||
dlg.hidden = true;
|
||||
onConfirm = null;
|
||||
if (lastFocus && lastFocus.focus) lastFocus.focus();
|
||||
}
|
||||
|
||||
function openDlg(opts) {
|
||||
lastFocus = document.activeElement;
|
||||
dlgTitle.textContent = opts.title || '알림';
|
||||
dlgMsg.textContent = opts.message || '';
|
||||
onConfirm = opts.onConfirm || null;
|
||||
dlgCancel.hidden = !opts.onConfirm;
|
||||
dlgOk.textContent = opts.okText || '확인';
|
||||
dlgOk.classList.toggle('erp-btn-danger', !!opts.danger);
|
||||
dlgOk.classList.toggle('erp-btn-primary', !opts.danger);
|
||||
dlg.hidden = false;
|
||||
dlgOk.focus();
|
||||
}
|
||||
|
||||
dlgOk.addEventListener('click', function () {
|
||||
var fn = onConfirm;
|
||||
closeDlg();
|
||||
if (fn) fn();
|
||||
});
|
||||
Array.prototype.forEach.call(dlg.querySelectorAll('[data-dlg-close]'), function (el) {
|
||||
el.addEventListener('click', closeDlg);
|
||||
});
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && !dlg.hidden) closeDlg();
|
||||
});
|
||||
|
||||
// ── 추가 결과 알림 (중복 등록 차단) ──
|
||||
var flash = {{ flash|tojson }};
|
||||
var fname = {{ flash_name|tojson }};
|
||||
if (flash === 'dup') {
|
||||
openDlg({ title: '이미 등록된 센터', message: '"' + fname + '" 센터는 이미 목록에 있습니다.' });
|
||||
} else if (flash === 'dup_inactive') {
|
||||
openDlg({
|
||||
title: '이미 등록된 센터',
|
||||
message: '"' + fname + '" 센터는 이미 목록에 있습니다. 지금은 비활성 상태이니 목록에서 [활성] 을 눌러 사용하세요.'
|
||||
});
|
||||
}
|
||||
if (flash) history.replaceState(null, '', '/cupang/centers');
|
||||
|
||||
// ── 삭제 확인 ──
|
||||
var delForm = document.getElementById('cpgCenterDelForm');
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('[data-del-url]');
|
||||
if (!btn) return;
|
||||
openDlg({
|
||||
title: '센터 삭제',
|
||||
message: '"' + btn.dataset.delName + '" 센터를 삭제할까요?',
|
||||
okText: '삭제',
|
||||
danger: true,
|
||||
onConfirm: function () {
|
||||
delForm.action = btn.dataset.delUrl;
|
||||
delForm.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── 그룹 접기 ──
|
||||
document.addEventListener('click', function (e) {
|
||||
var head = e.target.closest('.cpg-center-gtitle');
|
||||
if (!head) return;
|
||||
var group = head.closest('.cpg-center-group');
|
||||
var collapsed = group.classList.toggle('is-collapsed');
|
||||
head.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
||||
});
|
||||
|
||||
// ── 검색 ──
|
||||
var box = document.getElementById('cpgCenterSearch');
|
||||
var grid = document.getElementById('cpgCenterGrid');
|
||||
var count = document.getElementById('cpgCenterCount');
|
||||
var empty = document.getElementById('cpgCenterEmpty');
|
||||
if (!box || !grid) return;
|
||||
var groups = Array.prototype.slice.call(grid.querySelectorAll('.cpg-center-group'));
|
||||
box.addEventListener('input', function () {
|
||||
var q = box.value.trim().toLowerCase();
|
||||
var n = 0;
|
||||
groups.forEach(function (g) {
|
||||
var shown = 0;
|
||||
Array.prototype.forEach.call(g.querySelectorAll('.cpg-center-row'), function (r) {
|
||||
var hit = !q || (r.dataset.name || '').indexOf(q) !== -1;
|
||||
r.style.display = hit ? '' : 'none';
|
||||
if (hit) shown += 1;
|
||||
});
|
||||
g.style.display = shown ? '' : 'none';
|
||||
if (q && shown) g.classList.remove('is-collapsed');
|
||||
n += shown;
|
||||
});
|
||||
if (count) count.textContent = '(' + n + ')';
|
||||
if (empty) empty.hidden = n !== 0;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903e" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903f" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
@@ -11,7 +11,6 @@
|
||||
<a class="erp-btn erp-btn-primary" href="/cupang/box-calc">+ 신규 등록</a>
|
||||
<span class="cpg-settings-btns">
|
||||
<a class="erp-btn erp-btn-outline" href="/cupang/products">제품명 설정</a>
|
||||
<a class="erp-btn erp-btn-outline" href="/cupang/centers">입고센터 관리</a>
|
||||
<a class="erp-btn erp-btn-outline" href="/cupang/box-rules">상자 입수량 설정</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903e" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903f" />{% 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=20260903e" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903f" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #}
|
||||
|
||||
Reference in New Issue
Block a user