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) 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,10 +15,11 @@
<h2>박스 입수량 추가 / 수정</h2> <h2>박스 입수량 추가 / 수정</h2>
<span class="erp-muted">같은 제품코드는 덮어씁니다. 쿠팡박스 1박스당 들어가는 수량.</span> <span class="erp-muted">같은 제품코드는 덮어씁니다. 쿠팡박스 1박스당 들어가는 수량.</span>
</div> </div>
<form method="post" action="/cupang/box-rules" class="cpg-rule-grid"> <form method="post" action="/cupang/box-rules">
<div class="cpg-header-grid">
<label class="erp-field"><span>제품코드 *</span> <label class="erp-field"><span>제품코드 *</span>
<input class="erp-input" type="text" name="product_code" required placeholder="예: MS-1001" /></label> <input class="erp-input" type="text" name="product_code" required placeholder="예: MS-1001" /></label>
<label class="erp-field"><span>제품명(스냅샷)</span> <label class="erp-field"><span>제품명</span>
<input class="erp-input" type="text" name="product_name_snapshot" placeholder="예: 미라네 1호세트" /></label> <input class="erp-input" type="text" name="product_name_snapshot" placeholder="예: 미라네 1호세트" /></label>
<label class="erp-field"><span>박스명</span> <label class="erp-field"><span>박스명</span>
<input class="erp-input" type="text" name="box_name" value="쿠팡박스" /></label> <input class="erp-input" type="text" name="box_name" value="쿠팡박스" /></label>
@@ -26,7 +27,10 @@
<input class="erp-input" type="number" name="units_per_box" min="1" required /></label> <input class="erp-input" type="number" name="units_per_box" min="1" required /></label>
<label class="erp-field cpg-full"><span>메모</span> <label class="erp-field cpg-full"><span>메모</span>
<input class="erp-input" type="text" name="memo" /></label> <input class="erp-input" type="text" name="memo" /></label>
<div><button type="submit" class="erp-btn erp-btn-primary">저장</button></div> </div>
<div class="erp-page-actions" style="margin-top:12px;">
<button type="submit" class="erp-btn erp-btn-primary">저장</button>
</div>
</form> </form>
</div> </div>
@@ -9,65 +9,56 @@
<a class="erp-btn erp-btn-outline" href="/cupang/">← 달력</a> <a class="erp-btn erp-btn-outline" href="/cupang/">← 달력</a>
</div> </div>
<!-- 추가 --> <!-- 추가 (슬림) -->
<div class="erp-card cpg-form-card"> <div class="erp-card cpg-form-card cpg-center-add">
<div class="cpg-card-head"><h2>입고센터 추가</h2></div> <h2 class="cpg-center-add-title">입고센터 추가</h2>
<form method="post" action="/cupang/centers" class="cpg-inline-form"> <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="text" name="name" placeholder="센터명 (예: 대구3)" required style="min-width:180px" />
<input class="erp-input" type="number" name="sort_order" value="0" style="max-width:90px" title="정렬순서" /> <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> <button type="submit" class="erp-btn erp-btn-primary">추가</button>
</form> </form>
</div> </div>
<!-- 목록 --> <!-- 목록 (2열 그리드, 한 줄/센터) -->
<div class="erp-card cpg-form-card"> <div class="erp-card cpg-form-card">
<div class="cpg-card-head"><h2>센터 목록 ({{ centers|length }})</h2></div> <div class="cpg-card-head">
<div class="erp-table-wrap"> <h2>센터 목록 ({{ centers|length }})</h2>
<table class="erp-table"> <span class="erp-muted">사용 중 센터는 삭제 시 비활성화됩니다.</span>
<thead> </div>
<tr><th>센터명</th><th>정렬</th><th>상태</th><th>사용중</th><th>동작</th></tr>
</thead> <div class="cpg-center-grid">
<tbody>
{% for c in centers %} {% for c in centers %}
<tr {% if not c.active %}style="opacity:.55"{% endif %}> <div class="cpg-center-row {% if not c.active %}is-inactive{% endif %}">
<td> <form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-center-edit">
<form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form"> <input class="erp-input cpg-center-name" type="text" name="name" value="{{ c.name }}" />
<input class="erp-input" type="text" name="name" value="{{ c.name }}" style="max-width:160px" /> <input class="erp-input cpg-center-sort" type="number" name="sort_order" value="{{ c.sort_order }}" title="정렬" />
<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 cpg-btn-sm" title="이름/정렬 저장">저장</button>
<button type="submit" class="erp-btn erp-btn-outline">수정</button>
</form> </form>
</td> <span class="cpg-center-state">
<td>{{ c.sort_order }}</td> {% if c.in_use %}<span class="erp-badge erp-badge-inverse cpg-mini">사용중</span>{% endif %}
<td> {% if c.active %}<span class="erp-badge erp-badge-success cpg-mini">활성</span>
{% if c.active %}<span class="erp-badge erp-badge-success">활성</span> {% else %}<span class="erp-badge erp-badge-neutral cpg-mini">활성</span>{% endif %}
{% else %}<span class="erp-badge erp-badge-neutral">비활성</span>{% endif %} </span>
</td> <span class="cpg-center-act">
<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 %} {% if c.active %}
<form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form"> <form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form">
<input type="hidden" name="active" value="0" /> <input type="hidden" name="active" value="0" />
<button type="submit" class="erp-btn erp-btn-outline">비활성</button> <button type="submit" class="erp-btn erp-btn-outline cpg-btn-sm">비활성</button>
</form> </form>
{% else %} {% else %}
<form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form"> <form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form">
<input type="hidden" name="active" value="1" /> <input type="hidden" name="active" value="1" />
<button type="submit" class="erp-btn erp-btn-outline">활성</button> <button type="submit" class="erp-btn erp-btn-outline cpg-btn-sm">활성</button>
</form> </form>
{% endif %} {% endif %}
<form method="post" action="/cupang/centers/{{ c.id }}/delete" class="cpg-inline-form" <form method="post" action="/cupang/centers/{{ c.id }}/delete" class="cpg-inline-form"
onsubmit="return confirm('{% if c.in_use %}사용 중인 센터는 비활성화됩니다.{% else %}삭제합니다.{% endif %} 계속할까요?');"> onsubmit="return confirm('{% if c.in_use %}사용 중 비활성화됩니다.{% else %}삭제합니다.{% endif %} 계속?');">
<button type="submit" class="erp-btn erp-btn-danger">삭제</button> <button type="submit" class="erp-btn erp-btn-danger cpg-btn-sm">삭제</button>
</form> </form>
</span>
</div> </div>
</td>
</tr>
{% endfor %} {% endfor %}
</tbody>
</table>
</div> </div>
<p class="erp-muted">※ 출고 데이터에서 사용 중인 센터는 삭제 시 hard delete 되지 않고 비활성화됩니다.</p>
</div> </div>
</section> </section>
@@ -20,8 +20,13 @@
</form> </form>
<form method="post" action="/cupang/{{ shipment.id }}/delete" class="cpg-inline-form" <form method="post" action="/cupang/{{ shipment.id }}/delete" class="cpg-inline-form"
onsubmit="return confirm('이 출고 묶음을 취소 처리합니다. 계속할까요?');"> onsubmit="return confirm('이 출고 묶음을 취소 처리합니다(상태=취소). 계속할까요?');">
<button type="submit" class="erp-btn erp-btn-danger">취소 처리</button> <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> </form>
</div> </div>
+25
View File
@@ -98,6 +98,31 @@
.cpg-inline-form { display: inline-flex; gap: 6px; align-items: center; margin: 0; } .cpg-inline-form { display: inline-flex; gap: 6px; align-items: center; margin: 0; }
.cpg-row-actions { display: flex; gap: 6px; flex-wrap: wrap; } .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 td { vertical-align: middle; }
.cpg-lines .cpg-name-sel { width: 100%; min-width: 160px; } .cpg-lines .cpg-name-sel { width: 100%; min-width: 160px; }