ui(cupang): 박스룰 폼 3행 배치 + 목록 컬럼 정리/하드삭제

- 폼: 1행 제품명180/제품코드100, 2행 박스이름/박스당입수량, 3행 메모(전체폭)
- 목록 순서 제품명·제품코드·박스명·입수량·메모, 상태열/비활성 제거
- 삭제 버튼=완전삭제(delete_box_rule, 참조 라인 box_rule_id NULL 처리)
- 캐시 버전 f

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 06:11:57 +09:00
parent 2e3ea610b7
commit 90e286dc4d
10 changed files with 66 additions and 44 deletions
+15
View File
@@ -197,6 +197,21 @@ class CupangDBStore:
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 처리."""
with self._pool.connection() as conn:
with conn.transaction():
conn.execute(
"UPDATE cupang_shipment_lines SET box_rule_id = NULL WHERE box_rule_id = %s",
(rule_id,),
)
cur = conn.execute(
"DELETE FROM cupang_box_rules WHERE id = %s", (rule_id,)
)
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()}
+1 -1
View File
@@ -594,7 +594,7 @@ async def box_rule_delete(
if store is None:
raise HTTPException(status_code=503, detail="cupang_db 미설정")
try:
store.deactivate_box_rule(rule_id=rule_id)
store.delete_box_rule(rule_id=rule_id)
except KeyError:
raise HTTPException(status_code=404, detail="규칙을 찾을 수 없습니다.")
return RedirectResponse(url="/cupang/box-rules", status_code=303)
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530e" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530f" />{% endblock %}
{% block content %}
<section class="cpg">
@@ -20,24 +20,30 @@
<form method="post" action="/cupang/box-rules">
<input type="hidden" name="product_name_snapshot" id="brule-name-snap" />
<div class="cpg-brule-fields">
<label class="erp-field"><span>제품명 *</span>
<select class="erp-select" id="brule-name" required>
<option value="">— 제품명 선택 —</option>
{% for p in products %}
<option value="{{ p.product_code }}" data-name="{{ p.product_name }}">{{ p.product_name }}</option>
{% endfor %}
</select></label>
<label class="erp-field"><span>제품코드</span>
<input class="erp-input" type="text" name="product_code" id="brule-code" required placeholder="자동" /></label>
<label class="erp-field"><span>박스이름</span>
<input class="erp-input" type="text" name="box_name" value="쿠팡박스" /></label>
<label class="erp-field"><span>박스당 입수량 *</span>
<span class="cpg-upb-wrap">
<input class="erp-input cpg-brule-upb" type="number" name="units_per_box" min="1" required />
<span class="cpg-upb-unit"></span>
</span></label>
<label class="erp-field cpg-full"><span>메모</span>
<input class="erp-input cpg-brule-memo" type="text" name="memo" /></label>
<div class="cpg-brule-row">
<label class="erp-field"><span>제품명 *</span>
<select class="erp-select cpg-brule-name" id="brule-name" required>
<option value="">— 제품명 선택 —</option>
{% for p in products %}
<option value="{{ p.product_code }}" data-name="{{ p.product_name }}">{{ p.product_name }}</option>
{% endfor %}
</select></label>
<label class="erp-field"><span>제품코드</span>
<input class="erp-input cpg-brule-code" type="text" name="product_code" id="brule-code" required placeholder="자동" /></label>
</div>
<div class="cpg-brule-row">
<label class="erp-field"><span>박스이름</span>
<input class="erp-input cpg-brule-box" type="text" name="box_name" value="쿠팡박스" /></label>
<label class="erp-field"><span>박스당 입수량 *</span>
<span class="cpg-upb-wrap">
<input class="erp-input cpg-brule-upb" type="number" name="units_per_box" min="1" required />
<span class="cpg-upb-unit"></span>
</span></label>
</div>
<div class="cpg-brule-row">
<label class="erp-field cpg-brule-memo-field"><span>메모</span>
<input class="erp-input cpg-brule-memo" type="text" name="memo" /></label>
</div>
</div>
<div class="erp-page-actions" style="margin-top:12px;">
<button type="submit" class="erp-btn erp-btn-primary">저장</button>
@@ -68,27 +74,27 @@
<div class="erp-table-wrap">
<table class="erp-table">
<thead>
<tr><th>제품코드</th><th>제품</th><th>박스명</th><th>입수량</th><th>상태</th><th>메모</th><th></th></tr>
<tr><th>제품</th><th>제품코드</th><th>박스명</th><th>입수량</th><th>메모</th><th>동작</th></tr>
</thead>
<tbody>
{% for r in box_rules %}
<tr {% if not r.active %}style="opacity:.55"{% endif %}>
<td>{{ r.product_code }}</td>
<tr>
<td>{{ r.product_name_snapshot or '—' }}</td>
<td>{{ r.product_code }}</td>
<td>{{ r.box_name }}</td>
<td>{{ r.units_per_box }}</td>
<td>{% if r.active %}<span class="erp-badge erp-badge-success">활성</span>{% else %}<span class="erp-badge erp-badge-neutral">비활성</span>{% endif %}</td>
<td>{{ r.memo or '—' }}</td>
<td>
{% if r.active %}
<form method="post" action="/cupang/box-rules/{{ r.id }}/delete" class="cpg-inline-form"
onsubmit="return confirm('비활성화합니다. 계속할까요?');">
<button type="submit" class="erp-btn erp-btn-danger">비활성화</button>
onsubmit="return confirm('이 규칙을 삭제합니다. 계속할까요?');">
<button type="submit" class="erp-btn erp-btn-danger">삭제</button>
</form>
{% else %}—{% endif %}
</td>
</tr>
{% endfor %}
{% if not box_rules %}
<tr><td colspan="6" class="erp-muted">등록된 입수량 규칙이 없습니다.</td></tr>
{% endif %}
</tbody>
</table>
</div>
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530e" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530f" />{% 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=20260530e" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530f" />{% 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=20260530e" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530f" />{% endblock %}
{% block content %}
<section class="cpg">
@@ -118,4 +118,4 @@
</script>
{% endblock %}
{% block scripts %}<script src="/static/cupang.js?v=20260530e" defer></script>{% endblock %}
{% block scripts %}<script src="/static/cupang.js?v=20260530f" defer></script>{% endblock %}
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530e" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530f" />{% 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=20260530e" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260530f" />{% endblock %}
{% block content %}
<section class="cpg">
+9 -8
View File
@@ -246,20 +246,21 @@
@media (max-width: 1420px) { .cpg-brule-list { flex-basis: auto; width: 100%; } }
@media (max-width: 540px) { .cpg-brule-add { flex-basis: 100%; width: 100%; } }
.cpg-brule-fields { display: flex; flex-wrap: wrap; gap: 12px 16px; }
/* 제품명·제품코드·박스이름 140px 고정 (erp.css min-width:240 오버라이드) */
.cpg .cpg-brule-fields .erp-field > .erp-input,
.cpg .cpg-brule-fields .erp-field > .erp-select {
width: 140px; min-width: 140px; max-width: 140px; box-sizing: border-box;
}
.cpg-brule-fields { display: flex; flex-direction: column; gap: 12px; }
.cpg-brule-row { display: flex; gap: 16px; align-items: flex-end; }
.cpg-brule-memo-field { flex: 1 1 auto; min-width: 0; }
/* 필드별 고정 폭 (erp.css min-width:240 오버라이드) */
.cpg .cpg-brule-fields .cpg-brule-name { width: 180px; min-width: 180px; max-width: 180px; box-sizing: border-box; }
.cpg .cpg-brule-fields .cpg-brule-code { width: 100px; min-width: 100px; max-width: 100px; box-sizing: border-box; }
.cpg .cpg-brule-fields .cpg-brule-box { width: 140px; min-width: 140px; max-width: 140px; box-sizing: border-box; }
/* 박스당 입수량 60px + "개" */
.cpg-upb-wrap { display: inline-flex; align-items: center; gap: 4px; }
.cpg .cpg-brule-fields .cpg-upb-wrap > .cpg-brule-upb {
width: 60px; min-width: 60px; max-width: 60px; box-sizing: border-box;
}
.cpg-upb-unit { font-size: 13px; color: var(--color-midtone-gray); }
/* 메모는 넓게 */
.cpg .cpg-brule-fields .cpg-brule-memo { width: 300px; min-width: 300px; max-width: 300px; }
/* 메모: 프레임 전체 너비 */
.cpg .cpg-brule-fields .cpg-brule-memo { width: 100%; min-width: 0; max-width: 100%; box-sizing: border-box; }
/* ── 상품 검색 드롭다운 ── */
.cpg-search-pop {
+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=20260530e" />
<link rel="stylesheet" href="/static/erp-shell.css?v=20260530e" />
<link rel="stylesheet" href="/static/erp.css?v=20260530f" />
<link rel="stylesheet" href="/static/erp-shell.css?v=20260530f" />
<link rel="stylesheet" href="/static/erp-attach-viewer.css" />
{% block head_extra %}{% endblock %}
</head>