change(cupang): 출고 상세를 읽기 전용 3열 보기로 교체, 수정 기능 제거

- detail.html / form.html 삭제, GET·POST /{id}/edit 라우트와
  _form_context / _parse_lines 헬퍼 제거
- GET /{id} → view.html: ① 품목 / ② 박스 요약(서버 재계산) / ③ 센터
  (박스 계산 화면과 같은 3열 구성, 편집 불가)
- 박스 계산 로직을 _compute_boxes 로 분리해 API 와 보기 화면이 공유

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 15:48:05 +09:00
parent da90d1eec8
commit 8c8f6aebb2
5 changed files with 213 additions and 296 deletions
+32 -103
View File
@@ -93,16 +93,6 @@ def _guard(request: Request) -> tuple[Any, dict[str, Any]] | HTMLResponse | Redi
return store, user return store, user
def _parse_lines(lines_json: str) -> list[dict[str, Any]]:
try:
data = json.loads(lines_json or "[]")
except (json.JSONDecodeError, TypeError):
raise HTTPException(status_code=400, detail="라인 데이터 형식 오류")
if not isinstance(data, list):
raise HTTPException(status_code=400, detail="라인 데이터는 배열이어야 합니다.")
return data
def _ym(request: Request) -> tuple[int, int]: def _ym(request: Request) -> tuple[int, int]:
today = today_kst() today = today_kst()
try: try:
@@ -342,23 +332,6 @@ async def export_shipments_xlsx(request: Request, date: str = "") -> Any:
# ════════════════════════════════════════════════════════════ # ════════════════════════════════════════════════════════════
# 출고 묶음 — 등록 / 수정 / 상세 # 출고 묶음 — 등록 / 수정 / 상세
# ════════════════════════════════════════════════════════════ # ════════════════════════════════════════════════════════════
def _form_context(request: Request, store: Any, user: dict[str, Any]) -> dict[str, Any]:
from app.main import build_erp_nav # noqa: WPS433
from app.store import is_admin # noqa: WPS433
reader = _itemcode(request)
return {
"user": user,
"is_admin": is_admin(user),
"nav_items": build_erp_nav(user, active="cupang"),
"centers": sorted(store.list_centers(), key=lambda c: c["name"]),
"box_rules": store.list_box_rules(),
"products": store.list_products(),
"ship_methods": list(SHIP_METHODS),
"search_enabled": bool(reader and reader.enabled),
}
@router.get("/new") @router.get("/new")
async def new_form(request: Request) -> RedirectResponse: async def new_form(request: Request) -> RedirectResponse:
"""신규 등록 폼은 없앴다. 출고 묶음은 박스 계산의 [분배 확정] 으로만 만든다. """신규 등록 폼은 없앴다. 출고 묶음은 박스 계산의 [분배 확정] 으로만 만든다.
@@ -370,6 +343,10 @@ async def new_form(request: Request) -> RedirectResponse:
@router.get("/{shipment_id:int}", response_class=HTMLResponse) @router.get("/{shipment_id:int}", response_class=HTMLResponse)
async def detail(request: Request, shipment_id: int) -> HTMLResponse: async def detail(request: Request, shipment_id: int) -> HTMLResponse:
"""출고 묶음 보기 — 박스 계산 화면과 같은 3열 구성(읽기 전용).
수정 기능은 없앴다. 잘못 만들었으면 달력에서 삭제하고 다시 확정한다.
"""
from app.main import build_erp_nav, render_template # noqa: WPS433 from app.main import build_erp_nav, render_template # noqa: WPS433
from app.store import is_admin # noqa: WPS433 from app.store import is_admin # noqa: WPS433
@@ -384,9 +361,22 @@ async def detail(request: Request, shipment_id: int) -> HTMLResponse:
{"reason": "출고 묶음을 찾을 수 없습니다.", "is_admin": is_admin(user)}, {"reason": "출고 묶음을 찾을 수 없습니다.", "is_admin": is_admin(user)},
status_code=404, status_code=404,
) )
lines = ship.get("lines") or []
items = [
{
"product_code": ln.get("product_code"),
"product_name": ln.get("product_name_snapshot") or ln.get("product_code"),
"quantity": int(ln.get("quantity") or 0),
}
for ln in lines
]
calc = _compute_boxes(store, items)
total_qty = sum(it["quantity"] for it in items)
return render_template( return render_template(
request, request,
"cupang/detail.html", "cupang/view.html",
{ {
"user": user, "user": user,
"is_admin": is_admin(user), "is_admin": is_admin(user),
@@ -394,80 +384,13 @@ 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,
"items": items,
"calc": calc,
"total_qty": total_qty,
}, },
) )
@router.get("/{shipment_id:int}/edit", response_class=HTMLResponse)
async def edit_form(request: Request, shipment_id: int) -> HTMLResponse:
from app.main import 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
ship = store.get_shipment(shipment_id=shipment_id)
if not ship:
return render_template(
request, "denied.html",
{"reason": "출고 묶음을 찾을 수 없습니다.", "is_admin": is_admin(user)},
status_code=404,
)
ctx = _form_context(request, store, user)
ctx.update(
{
"page_title": f"출고 #{ship['id']} 수정",
"page_subtitle": "헤더/라인 수정 후 저장",
"mode": "edit",
"shipment": ship,
"default_date": ship["document_date"],
}
)
return render_template(request, "cupang/form.html", ctx)
@router.post("/{shipment_id:int}/edit")
async def update(
request: Request,
shipment_id: int,
lines_json: str = Form("[]"),
document_date: str = Form(...),
ship_date: str = Form(...),
center_arrival_date: str = Form(...),
center_id: str = Form(""),
center_name_snapshot: str = Form(""),
ship_method: str = Form("택배"),
outbound_summary: str = Form(""),
worker: str = Form(""),
memo: 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 미설정")
header = {
"document_date": document_date,
"ship_date": ship_date,
"center_arrival_date": center_arrival_date,
"center_id": center_id,
"center_name_snapshot": center_name_snapshot,
"ship_method": ship_method,
"outbound_summary": outbound_summary,
"worker": worker,
"memo": memo,
}
try:
store.update_shipment(
shipment_id=shipment_id, header=header, lines=_parse_lines(lines_json)
)
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)
def _safe_next(raw: str, fallback: str) -> str: def _safe_next(raw: str, fallback: str) -> str:
"""열린 리다이렉트 방지 — /cupang/ 안쪽 경로만 허용.""" """열린 리다이렉트 방지 — /cupang/ 안쪽 경로만 허용."""
nxt = (raw or "").strip() nxt = (raw or "").strip()
@@ -1008,8 +931,6 @@ async def box_calc_api(
클라이언트 계산을 신뢰하지 않고 store.compute_boxes 로 서버에서 계산한다. 클라이언트 계산을 신뢰하지 않고 store.compute_boxes 로 서버에서 계산한다.
""" """
from .store import compute_boxes # noqa: WPS433
store = _store(request) store = _store(request)
if store is None: if store is None:
raise HTTPException(status_code=503, detail="cupang_db 미설정") raise HTTPException(status_code=503, detail="cupang_db 미설정")
@@ -1018,6 +939,16 @@ async def box_calc_api(
if not isinstance(raw_items, list): if not isinstance(raw_items, list):
raise HTTPException(status_code=400, detail="items 는 배열이어야 합니다.") raise HTTPException(status_code=400, detail="items 는 배열이어야 합니다.")
return JSONResponse(_compute_boxes(store, raw_items))
def _compute_boxes(store: Any, raw_items: list[Any]) -> dict[str, Any]:
"""[{product_code, quantity}] → 제품별 박스 + 자투리 혼합 박스 + 합계.
박스 계산 화면(API)과 출고 상세 보기가 같은 결과를 쓰도록 한 곳에 둔다.
"""
from .store import compute_boxes # noqa: WPS433
rules = {r["product_code"]: r for r in store.list_box_rules()} rules = {r["product_code"]: r for r in store.list_box_rules()}
results: list[dict[str, Any]] = [] results: list[dict[str, Any]] = []
@@ -1063,14 +994,12 @@ async def box_calc_api(
grand_total = sum(r["full_boxes"] or 0 for r in results if r["configured"]) grand_total = sum(r["full_boxes"] or 0 for r in results if r["configured"])
grand_total += sum(m["box_count"] for m in mixes) grand_total += sum(m["box_count"] for m in mixes)
return JSONResponse( return {
{
"results": results, "results": results,
"totals": sorted(totals.values(), key=lambda t: t["box_name"]), "totals": sorted(totals.values(), key=lambda t: t["box_name"]),
"mixes": mixes, "mixes": mixes,
"grand_total_boxes": grand_total, "grand_total_boxes": grand_total,
} }
)
def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]: def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
@@ -1,76 +0,0 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901m" />{% endblock %}
{% block content %}
<section class="cpg">
<div class="erp-page-actions cpg-actions">
<a class="erp-btn erp-btn-primary" href="/cupang/?date={{ shipment.ship_date }}">◀◀ 달력</a>
<form method="post" action="/cupang/{{ shipment.id }}/delete" class="cpg-inline-form"
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>
<a class="erp-btn erp-btn-primary cpg-push-right" href="/cupang/{{ shipment.id }}/edit">수정</a>
</div>
<!-- 헤더 -->
<div class="erp-card cpg-form-card">
<div class="cpg-card-head">
<h2>출고 #{{ shipment.id }}
{% set badge = 'erp-badge-neutral' %}
{% if shipment.status == '출고완료' %}{% set badge = 'erp-badge-inverse' %}
{% elif shipment.status == '센터입고완료' %}{% set badge = 'erp-badge-success' %}
{% elif shipment.status == '취소' %}{% set badge = 'erp-badge-danger' %}{% endif %}
<span class="erp-badge {{ badge }}">{{ shipment.status }}</span>
</h2>
</div>
<dl class="cpg-detail-grid">
<div><dt>작성일</dt><dd>{{ shipment.document_date }}</dd></div>
<div><dt>출고일</dt><dd>{{ shipment.ship_date }}</dd></div>
<div><dt>센터입고일</dt><dd>{{ shipment.center_arrival_date }}</dd></div>
<div><dt>입고센터</dt><dd>{{ shipment.center_name_snapshot or '—' }}</dd></div>
<div><dt>출고방식</dt><dd>{{ shipment.ship_method }}</dd></div>
<div><dt>작업자</dt><dd>{{ shipment.worker or '—' }}</dd></div>
<div class="cpg-full"><dt>출고/박스 요약</dt><dd>{{ shipment.outbound_summary or '—' }}</dd></div>
<div class="cpg-full"><dt>메모</dt><dd>{{ shipment.memo or '—' }}</dd></div>
</dl>
</div>
<!-- 라인 -->
<div class="erp-card cpg-form-card">
<div class="cpg-card-head"><h2>품목 ({{ shipment.lines|length }})</h2></div>
<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><th>수동보정</th><th>메모</th></tr>
</thead>
<tbody>
{% for ln in shipment.lines %}
<tr>
<td>{{ ln.line_no }}</td>
<td>{{ ln.product_code }}</td>
<td>{{ ln.product_name_snapshot }}</td>
<td>{{ ln.quantity }}</td>
<td>{{ ln.units_per_box if ln.units_per_box else '미설정' }}</td>
<td>{{ ln.calculated_boxes if ln.calculated_boxes is not none else '—' }}</td>
<td>{{ ln.remainder_units if ln.remainder_units is not none else '—' }}</td>
<td>{{ ln.manual_box_text or '—' }}</td>
<td>{{ ln.memo or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</section>
{% endblock %}
@@ -1,112 +0,0 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901m" />{% endblock %}
{% block content %}
<section class="cpg">
{% set action = '/cupang/new' if mode == 'new' else '/cupang/' ~ shipment.id ~ '/edit' %}
<form id="cpg-form" method="post" action="{{ action }}">
<input type="hidden" name="lines_json" id="cpg-lines-json" value="[]" />
<div class="cpg-form-2col">
<!-- ── 공통 헤더 (왼쪽) ── -->
<div class="erp-card cpg-form-card cpg-form-head">
<div class="cpg-card-head"><h2>공통 헤더</h2></div>
<div class="cpg-header-grid">
<label class="erp-field"><span>작성일 *</span>
<input class="erp-input" type="date" name="document_date" required
value="{{ shipment.document_date if shipment else default_date }}" /></label>
<label class="erp-field"><span>출고일 *</span>
<input class="erp-input" type="date" name="ship_date" required
value="{{ shipment.ship_date if shipment else default_date }}" /></label>
<label class="erp-field"><span>센터입고일 *</span>
<input class="erp-input" type="date" name="center_arrival_date" required
value="{{ shipment.center_arrival_date if shipment else default_date }}" /></label>
<label class="erp-field"><span>입고센터</span>
<select class="erp-select" name="center_id" id="cpg-center-select">
<option value="">— 선택 —</option>
{% for c in centers %}
<option value="{{ c.id }}" data-name="{{ c.name }}"
{% if shipment and shipment.center_id == c.id %}selected{% endif %}>{{ c.name }}</option>
{% endfor %}
</select></label>
<!-- 센터 스냅샷(자유 입력 허용: 과거 명칭 보존/센터 미등록 시) -->
<input type="hidden" name="center_name_snapshot" id="cpg-center-name"
value="{{ shipment.center_name_snapshot if shipment else '' }}" />
<label class="erp-field"><span>출고방식</span>
<select class="erp-select" name="ship_method">
{% for m in ship_methods %}
<option value="{{ m }}" {% if shipment and shipment.ship_method == m %}selected{% endif %}>{{ m }}</option>
{% endfor %}
</select></label>
<label class="erp-field"><span>작업자</span>
<input class="erp-input" type="text" name="worker"
value="{{ shipment.worker if shipment else '' }}" /></label>
</div>
<label class="erp-field cpg-full"><span>출고/박스 요약 (수동 보정 메모)</span>
<input class="erp-input" type="text" name="outbound_summary"
placeholder="예: 쿠팡박스 50, 6호상자 1, (50번 박스)"
value="{{ shipment.outbound_summary if shipment else '' }}" /></label>
<label class="erp-field cpg-full"><span>메모</span>
<textarea class="erp-input" name="memo" rows="2">{{ shipment.memo if shipment else '' }}</textarea></label>
<div class="erp-page-actions cpg-form-actions">
<button type="submit" class="erp-btn erp-btn-primary">저장</button>
{% if mode == 'edit' %}
<a class="erp-btn erp-btn-outline" href="/cupang/{{ shipment.id }}">취소</a>
{% else %}
<a class="erp-btn erp-btn-outline" href="/cupang/">취소</a>
{% endif %}
</div>
</div>
<!-- ── 품목 라인 (오른쪽) ── -->
<div class="erp-card cpg-form-card cpg-form-lines">
<div class="cpg-card-head cpg-lines-head">
<h2>품목 라인</h2>
<span class="erp-muted">
제품명 선택 시 제품코드 자동 입력. 수량 입력 시 박스 수 자동 계산.
{% if not products %}<a href="/cupang/products">설정에서 제품명 먼저 등록</a>{% endif %}
</span>
<div class="cpg-lines-btns">
<button type="button" class="erp-btn erp-btn-outline" id="cpg-add-line">+ 라인 추가</button>
<button type="button" class="erp-btn erp-btn-danger" id="cpg-del-line">선택 라인 삭제</button>
</div>
</div>
<div class="erp-table-wrap cpg-lines-scroll">
<table class="erp-table cpg-lines">
<thead>
<tr>
<th class="cpg-check-col"><input type="checkbox" id="cpg-check-all" title="전체 선택" /></th>
<th>제품명</th><th>제품코드</th><th>수량</th>
<th>입수량</th><th>박스 계산</th><th>라인메모</th>
</tr>
</thead>
<tbody id="cpg-lines-body"><!-- JS 렌더 --></tbody>
</table>
</div>
</div>
</div><!-- /cpg-form-2col -->
</form>
</section>
<script type="application/json" id="cpg-init-lines">
{% if shipment and shipment.lines %}{{ shipment.lines | tojson }}{% else %}[]{% endif %}
</script>
<script type="application/json" id="cpg-box-rules">
{{ box_rules | tojson }}
</script>
<script type="application/json" id="cpg-products">
{{ products | tojson }}
</script>
{% endblock %}
{% block scripts %}<script src="/static/cupang.js?v=20260530p" defer></script>{% endblock %}
@@ -0,0 +1,166 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901m" />{% endblock %}
{% block content %}
{# 출고 묶음 보기 — 박스 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #}
<section class="cpg">
<div class="erp-page-actions">
<a class="erp-btn erp-btn-primary" href="/cupang/?date={{ shipment.ship_date }}">◀◀ 달력</a>
<span class="erp-badge erp-badge-neutral">{{ shipment.status }}</span>
</div>
<div class="cpg-calc3">
<!-- ① 품목 -->
<div class="erp-card cpg-form-card cpg-calc-card">
<div class="cpg-card-head">
<h2>① 품목</h2>
<span class="erp-muted">{{ items|length }}종 · {{ total_qty }}개</span>
</div>
<div class="erp-table-wrap">
<table class="erp-table cpg-calc-table">
<colgroup>
<col class="cpg-col-name" />
<col class="cpg-col-qty" />
</colgroup>
<thead>
<tr><th>제품명</th><th>수량</th></tr>
</thead>
<tbody>
{% for it in items %}
<tr class="cpg-view-row">
<td title="{{ it.product_name }}">{{ it.product_name }}</td>
<td class="cpg-calc-num">{{ it.quantity }}</td>
</tr>
{% endfor %}
{% if not items %}
<tr><td colspan="2" class="erp-muted">품목이 없습니다.</td></tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<!-- ② 박스 요약 -->
<div class="erp-card cpg-form-card cpg-calc-sum">
<div class="cpg-card-head">
<h2>② 박스 요약</h2>
<span class="erp-muted">확정 당시 수량 기준으로 다시 계산한 값</span>
</div>
<div id="cpg-sum-body">
<div class="cpg-sum-kpis">
<div class="cpg-kpi">
<span class="cpg-kpi-label">총 박스</span>
<strong class="cpg-kpi-value">{{ calc.grand_total_boxes }}박스</strong>
<span class="cpg-kpi-hint">제품별 + 혼합</span>
</div>
<div class="cpg-kpi">
<span class="cpg-kpi-label">출고</span>
<strong class="cpg-kpi-value">{{ shipment.outbound_summary or '—' }}</strong>
<span class="cpg-kpi-hint">확정 시 기록</span>
</div>
<div class="cpg-kpi">
<span class="cpg-kpi-label">총 수량</span>
<strong class="cpg-kpi-value">{{ total_qty }}개</strong>
<span class="cpg-kpi-hint">{{ items|length }}종</span>
</div>
</div>
<div class="cpg-sum-scroll">
<h3 class="cpg-sum-h3">제품별 박스</h3>
<div class="cpg-sum-prods">
{% for r in calc.results if r.configured %}
<div class="cpg-sum-prod is-done">
<div class="cpg-sum-prod-head">
<span class="cpg-sum-prod-name">{{ r.product_name }}</span>
</div>
<div class="cpg-sum-prod-nums">
<strong>{{ r.full_boxes }}박스</strong>
<span class="erp-muted">{{ r.quantity }}개 · 입수량 {{ r.units_per_box }}</span>
{% if r.remainder_units %}
<span class="cpg-sum-left">자투리 {{ r.remainder_units }}개</span>
{% else %}
<span class="cpg-sum-exact">딱 맞음</span>
{% endif %}
</div>
</div>
{% endfor %}
{% set unconfigured = calc.results | rejectattr('configured') | list %}
{% if unconfigured %}
<p class="erp-muted">박스 입수량 미설정:
{% for r in unconfigured %}{{ r.product_name }}{% if not loop.last %}, {% endif %}{% endfor %}
</p>
{% endif %}
</div>
<h3 class="cpg-sum-h3">자투리 혼합 박스
<span class="erp-muted">{% if not calc.mixes %}— 자투리 없음{% else %}— 1장 = 1박스{% endif %}</span>
</h3>
<div class="cpg-sum-boxes">
{% for m in calc.mixes %}
{% for b in m.boxes %}
<div class="cpg-box-card is-done">
<div class="cpg-box-info">
<div class="cpg-box-title">{{ m.box_name }} 혼합 #{{ loop.index }}</div>
<ul class="cpg-box-items">
{% for it in b['items'] %}
<li><span>{{ it.product_name }}</span><b>{{ it.quantity }}개</b></li>
{% endfor %}
</ul>
<div class="cpg-box-bar"><span style="width:{{ b.fill_percent }}%"></span></div>
<div class="cpg-box-fillnum">{{ b.fill_percent }}% 채움</div>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
</div>
</div>
</div>
<!-- ③ 센터 -->
<div class="erp-card cpg-form-card cpg-dist-card">
<div class="cpg-card-head">
<h2>③ 센터</h2>
<span class="erp-muted">출고 #{{ shipment.id }}</span>
</div>
<div class="cpg-dist-list">
<div class="cpg-dist-center has-items">
<div class="cpg-dist-head">
<strong>{{ shipment.center_name_snapshot or '센터 미지정' }}</strong>
<span class="erp-badge erp-badge-neutral">{{ shipment.ship_method }}</span>
<span class="cpg-dist-sum">
<span class="erp-badge erp-badge-neutral">{{ calc.grand_total_boxes }}박스</span>
<span class="erp-badge erp-badge-neutral">{{ total_qty }}개</span>
</span>
</div>
<ul class="cpg-dist-items">
{% for it in items %}
<li class="cpg-dist-item">
<div class="cpg-dist-line">
<span class="cpg-dist-name">{{ it.product_name }}</span>
<span class="cpg-dist-unit">{{ it.quantity }}개</span>
</div>
</li>
{% endfor %}
</ul>
</div>
<dl class="cpg-view-meta">
<div><dt>작성일</dt><dd>{{ shipment.document_date }}</dd></div>
<div><dt>출고일</dt><dd>{{ shipment.ship_date }}</dd></div>
<div><dt>센터입고일</dt><dd>{{ shipment.center_arrival_date }}</dd></div>
<div><dt>작업자</dt><dd>{{ shipment.worker or '—' }}</dd></div>
{% if shipment.memo %}<div><dt>메모</dt><dd>{{ shipment.memo }}</dd></div>{% endif %}
</dl>
</div>
</div>
</div><!-- /cpg-calc3 -->
</section>
{% endblock %}
+10
View File
@@ -1393,3 +1393,13 @@ body.cpg-dragging { cursor: grabbing; user-select: none; }
/* ① 직접 입력 — 수량 입력란 70px 고정 */ /* ① 직접 입력 — 수량 입력란 70px 고정 */
.cpg-calc-card .cpg-calc-table col.cpg-col-qty { width: 70px; } .cpg-calc-card .cpg-calc-table col.cpg-col-qty { width: 70px; }
.cpg-calc-card .cpg-calc-table .cpg-calc-qty { width: 70px; max-width: 70px; margin-left: auto; } .cpg-calc-card .cpg-calc-table .cpg-calc-qty { width: 70px; max-width: 70px; margin-left: auto; }
/* 출고 보기(읽기 전용) — ③ 하단 메타 정보 */
.cpg-view-meta {
margin: 12px 0 0; padding: 10px 12px;
border: 1px solid var(--color-subtle-ash); border-radius: 10px;
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px;
}
.cpg-view-meta dt { font-size: 11px; color: var(--color-midtone-gray); }
.cpg-view-meta dd { margin: 2px 0 0; font-size: 13px; }
.cpg-view-row td { padding: 4px 6px; }