ui(cupang): 박스입수량 라벨/겹침, 입고센터 컴팩트 재디자인, 출고 완전삭제

- 박스 입수량 폼: 제품명(스냅샷)→제품명, 헤더그리드로 교체해 입력란 겹침 해소
- 입고센터: 추가 폼 슬림화 + 센터 목록 2열 그리드(한 줄/센터)로 압축
- 출고 상세: 완전삭제 버튼 추가(/{id}/hard-delete, 라인 CASCADE). 취소 처리는 outline 로

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 04:16:24 +09:00
parent 69b84db4bb
commit 867bc510da
5 changed files with 108 additions and 66 deletions
+17
View File
@@ -425,6 +425,23 @@ async def delete(
return RedirectResponse(url=f"/cupang/{shipment_id}", status_code=303)
@router.post("/{shipment_id:int}/hard-delete")
async def hard_delete(
request: Request,
shipment_id: int,
user: dict[str, Any] = Depends(_require_user),
) -> RedirectResponse:
"""완전 삭제(헤더+라인 CASCADE). 달력으로 복귀."""
store = _store(request)
if store is None:
raise HTTPException(status_code=503, detail="cupang_db 미설정")
try:
store.hard_delete(shipment_id=shipment_id)
except KeyError:
raise HTTPException(status_code=404, detail="출고 묶음을 찾을 수 없습니다.")
return RedirectResponse(url="/cupang/", status_code=303)
# ════════════════════════════════════════════════════════════
# 입고센터 관리
# ════════════════════════════════════════════════════════════
@@ -15,18 +15,22 @@
<h2>박스 입수량 추가 / 수정</h2>
<span class="erp-muted">같은 제품코드는 덮어씁니다. 쿠팡박스 1박스당 들어가는 수량.</span>
</div>
<form method="post" action="/cupang/box-rules" class="cpg-rule-grid">
<label class="erp-field"><span>제품코드 *</span>
<input class="erp-input" type="text" name="product_code" required placeholder="예: MS-1001" /></label>
<label class="erp-field"><span>제품명(스냅샷)</span>
<input class="erp-input" type="text" name="product_name_snapshot" placeholder="예: 미라네 1호세트" /></label>
<label class="erp-field"><span>박스명</span>
<input class="erp-input" type="text" name="box_name" value="쿠팡박스" /></label>
<label class="erp-field"><span>박스당 입수량 *</span>
<input class="erp-input" type="number" name="units_per_box" min="1" required /></label>
<label class="erp-field cpg-full"><span>메모</span>
<input class="erp-input" type="text" name="memo" /></label>
<div><button type="submit" class="erp-btn erp-btn-primary">저장</button></div>
<form method="post" action="/cupang/box-rules">
<div class="cpg-header-grid">
<label class="erp-field"><span>제품코드 *</span>
<input class="erp-input" type="text" name="product_code" required placeholder="예: MS-1001" /></label>
<label class="erp-field"><span>제품명</span>
<input class="erp-input" type="text" name="product_name_snapshot" placeholder="예: 미라네 1호세트" /></label>
<label class="erp-field"><span>박스명</span>
<input class="erp-input" type="text" name="box_name" value="쿠팡박스" /></label>
<label class="erp-field"><span>박스당 입수량 *</span>
<input class="erp-input" type="number" name="units_per_box" min="1" required /></label>
<label class="erp-field cpg-full"><span>메모</span>
<input class="erp-input" type="text" name="memo" /></label>
</div>
<div class="erp-page-actions" style="margin-top:12px;">
<button type="submit" class="erp-btn erp-btn-primary">저장</button>
</div>
</form>
</div>
@@ -9,65 +9,56 @@
<a class="erp-btn erp-btn-outline" href="/cupang/">← 달력</a>
</div>
<!-- 추가 -->
<div class="erp-card cpg-form-card">
<div class="cpg-card-head"><h2>입고센터 추가</h2></div>
<!-- 추가 (슬림) -->
<div class="erp-card cpg-form-card cpg-center-add">
<h2 class="cpg-center-add-title">입고센터 추가</h2>
<form method="post" action="/cupang/centers" class="cpg-inline-form">
<input class="erp-input" type="text" name="name" placeholder="센터명 (예: 대구3)" required />
<input class="erp-input" type="number" name="sort_order" value="0" style="max-width:90px" title="정렬순서" />
<input class="erp-input" type="text" name="name" placeholder="센터명 (예: 대구3)" required style="min-width:180px" />
<input class="erp-input" type="number" name="sort_order" value="0" style="width:70px" title="정렬순서" />
<button type="submit" class="erp-btn erp-btn-primary">추가</button>
</form>
</div>
<!-- 목록 -->
<!-- 목록 (2열 그리드, 한 줄/센터) -->
<div class="erp-card cpg-form-card">
<div class="cpg-card-head"><h2>센터 목록 ({{ centers|length }})</h2></div>
<div class="erp-table-wrap">
<table class="erp-table">
<thead>
<tr><th>센터명</th><th>정렬</th><th>상태</th><th>사용중</th><th>동작</th></tr>
</thead>
<tbody>
{% for c in centers %}
<tr {% if not c.active %}style="opacity:.55"{% endif %}>
<td>
<form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form">
<input class="erp-input" type="text" name="name" value="{{ c.name }}" style="max-width:160px" />
<input class="erp-input" type="number" name="sort_order" value="{{ c.sort_order }}" style="max-width:80px" />
<button type="submit" class="erp-btn erp-btn-outline">수정</button>
</form>
</td>
<td>{{ c.sort_order }}</td>
<td>
{% if c.active %}<span class="erp-badge erp-badge-success">활성</span>
{% else %}<span class="erp-badge erp-badge-neutral">비활성</span>{% endif %}
</td>
<td>{% if c.in_use %}<span class="erp-badge erp-badge-inverse">사용중</span>{% else %}—{% endif %}</td>
<td>
<div class="cpg-row-actions">
{% if c.active %}
<form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form">
<input type="hidden" name="active" value="0" />
<button type="submit" class="erp-btn erp-btn-outline">활성</button>
</form>
{% else %}
<form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form">
<input type="hidden" name="active" value="1" />
<button type="submit" class="erp-btn erp-btn-outline">활성화</button>
</form>
{% endif %}
<form method="post" action="/cupang/centers/{{ c.id }}/delete" class="cpg-inline-form"
onsubmit="return confirm('{% if c.in_use %}사용 중인 센터는 비활성화됩니다.{% else %}삭제합니다.{% endif %} 계속할까요?');">
<button type="submit" class="erp-btn erp-btn-danger">삭제</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="cpg-card-head">
<h2>센터 목록 ({{ centers|length }})</h2>
<span class="erp-muted">사용 중 센터는 삭제 시 비활성화됩니다.</span>
</div>
<div class="cpg-center-grid">
{% for c in centers %}
<div class="cpg-center-row {% if not c.active %}is-inactive{% endif %}">
<form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-center-edit">
<input class="erp-input cpg-center-name" type="text" name="name" value="{{ c.name }}" />
<input class="erp-input cpg-center-sort" type="number" name="sort_order" value="{{ c.sort_order }}" title="정렬" />
<button type="submit" class="erp-btn erp-btn-outline cpg-btn-sm" title="이름/정렬 저장">저장</button>
</form>
<span class="cpg-center-state">
{% if c.in_use %}<span class="erp-badge erp-badge-inverse cpg-mini">사용중</span>{% endif %}
{% if c.active %}<span class="erp-badge erp-badge-success cpg-mini">활성</span>
{% else %}<span class="erp-badge erp-badge-neutral cpg-mini">비활성</span>{% endif %}
</span>
<span class="cpg-center-act">
{% if c.active %}
<form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form">
<input type="hidden" name="active" value="0" />
<button type="submit" class="erp-btn erp-btn-outline cpg-btn-sm">비활성</button>
</form>
{% else %}
<form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form">
<input type="hidden" name="active" value="1" />
<button type="submit" class="erp-btn erp-btn-outline cpg-btn-sm">활성</button>
</form>
{% endif %}
<form method="post" action="/cupang/centers/{{ c.id }}/delete" class="cpg-inline-form"
onsubmit="return confirm('{% if c.in_use %}사용 중 → 비활성화됩니다.{% else %}삭제합니다.{% endif %} 계속?');">
<button type="submit" class="erp-btn erp-btn-danger cpg-btn-sm">삭제</button>
</form>
</span>
</div>
{% endfor %}
</div>
<p class="erp-muted">※ 출고 데이터에서 사용 중인 센터는 삭제 시 hard delete 되지 않고 비활성화됩니다.</p>
</div>
</section>
@@ -20,8 +20,13 @@
</form>
<form method="post" action="/cupang/{{ shipment.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-outline">취소 처리</button>
</form>
<form method="post" action="/cupang/{{ shipment.id }}/hard-delete" class="cpg-inline-form"
onsubmit="return confirm('이 출고를 완전 삭제합니다(복구 불가, 품목 포함). 계속할까요?');">
<button type="submit" class="erp-btn erp-btn-danger">삭제</button>
</form>
</div>
+25
View File
@@ -98,6 +98,31 @@
.cpg-inline-form { display: inline-flex; gap: 6px; align-items: center; margin: 0; }
.cpg-row-actions { display: flex; gap: 6px; flex-wrap: wrap; }
/* ── 입고센터 관리 (컴팩트) ── */
.cpg-center-add { padding: 12px 16px; }
.cpg-center-add-title { font-size: 15px; font-weight: 600; margin: 0 0 8px; }
.cpg-btn-sm { padding: 3px 8px; font-size: 12px; }
.cpg-center-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px 16px;
}
@media (max-width: 820px) { .cpg-center-grid { grid-template-columns: 1fr; } }
.cpg-center-row {
display: flex; align-items: center; gap: 8px;
padding: 5px 8px;
border: 1px solid var(--color-subtle-ash); border-radius: 10px;
min-width: 0;
}
.cpg-center-row.is-inactive { opacity: .55; }
.cpg-center-edit { display: flex; align-items: center; gap: 6px; flex: 1 1 auto; min-width: 0; }
.cpg-center-name { flex: 1 1 auto; min-width: 0; }
.cpg-center-sort { width: 56px; flex: 0 0 auto; }
.cpg-center-state { display: inline-flex; gap: 4px; flex: 0 0 auto; }
.cpg-center-act { display: inline-flex; gap: 4px; flex: 0 0 auto; }
/* ── 라인 테이블 ── */
.cpg-lines td { vertical-align: middle; }
.cpg-lines .cpg-name-sel { width: 100%; min-width: 160px; }