feat(cupang): 발주 업로드 UX 개선 + 3열 프레임 각각 스크롤

- [쿠팡 발주파일 업로드] 라벨 버튼, 파일 선택 즉시 자동 분석(분석 버튼 제거)
- 안내 문구와 분석 결과 상세 표시 제거 — 경고만 남김
- ①②③ 카드가 각자 스크롤: 문서 전체는 스크롤되지 않음
  · ① 표 머리글 sticky, 행 높이 축소로 품목 더 많이 표시
  · ② KPI 요약까지 고정, 아래 목록만 스크롤
  · ③ 헤더/센터 선택 고정, 센터 목록만 스크롤

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 14:08:13 +09:00
parent 5542748539
commit 275e51c93b
8 changed files with 76 additions and 48 deletions
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901k" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901l" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -34,19 +34,16 @@
<div class="erp-card cpg-form-card cpg-calc-card"> <div class="erp-card cpg-form-card cpg-calc-card">
<div class="cpg-card-head"> <div class="cpg-card-head">
<h2>① 쿠팡 발주 업로드</h2> <h2>① 쿠팡 발주 업로드</h2>
<span class="erp-muted">엑셀 여러 개 선택 가능 · F13 입고예정일의 하루 전 = 출고일</span>
</div> </div>
<!-- 쿠팡 발주서(xlsx) 업로드 → 센터별 수량 합산 → ②③ 자동 채움 --> <!-- 쿠팡 발주서(xlsx) 업로드 → 센터별 수량 합산 → ②③ 자동 채움 -->
<div class="cpg-po-box"> <div class="cpg-po-box">
<div class="cpg-po-row"> <div class="cpg-po-row">
<label class="erp-btn erp-btn-primary cpg-po-pick" for="cpg-po-file">쿠팡 발주파일 업로드</label>
<input class="cpg-po-file" type="file" id="cpg-po-file" <input class="cpg-po-file" type="file" id="cpg-po-file"
accept=".xlsx,.xlsm" multiple aria-label="쿠팡 발주 엑셀 선택" /> accept=".xlsx,.xlsm" multiple aria-label="쿠팡 발주 엑셀 선택" />
<button type="button" class="erp-btn erp-btn-primary" id="cpg-po-run">업로드 분석</button> <span class="erp-muted cpg-po-msg" id="cpg-po-msg"></span>
</div> </div>
<p class="erp-muted cpg-po-msg" id="cpg-po-msg">
B열 쿠팡상품코드 · F열 센터 · G열 수량을 22행부터 읽습니다.
</p>
<div class="cpg-po-result" id="cpg-po-result" hidden></div> <div class="cpg-po-result" id="cpg-po-result" hidden></div>
</div> </div>
@@ -90,11 +87,13 @@
<div id="cpg-sum-body" hidden> <div id="cpg-sum-body" hidden>
<div class="cpg-sum-kpis" id="cpg-sum-kpis"></div> <div class="cpg-sum-kpis" id="cpg-sum-kpis"></div>
<h3 class="cpg-sum-h3">제품별 박스 <span class="erp-muted">잔여 기준</span></h3> <div class="cpg-sum-scroll">
<div class="cpg-sum-prods" id="cpg-sum-prods"></div> <h3 class="cpg-sum-h3">제품별 박스 <span class="erp-muted">잔여 기준</span></h3>
<div class="cpg-sum-prods" id="cpg-sum-prods"></div>
<h3 class="cpg-sum-h3">자투리 혼합 박스 <span class="erp-muted" id="cpg-sum-mix-note"></span></h3> <h3 class="cpg-sum-h3">자투리 혼합 박스 <span class="erp-muted" id="cpg-sum-mix-note"></span></h3>
<div class="cpg-sum-boxes" id="cpg-sum-boxes"></div> <div class="cpg-sum-boxes" id="cpg-sum-boxes"></div>
</div>
</div> </div>
</div> </div>
@@ -372,7 +371,6 @@
// 업로드 → 센터별 수량 합산(서버) → 센터마다 박스 계산 → ②③ 자동 채움. // 업로드 → 센터별 수량 합산(서버) → 센터마다 박스 계산 → ②③ 자동 채움.
// 박스는 센터별로 포장하므로 계산도 센터 단위로 한다(자투리 혼합 박스 포함). // 박스는 센터별로 포장하므로 계산도 센터 단위로 한다(자투리 혼합 박스 포함).
var poFile = document.getElementById("cpg-po-file"); var poFile = document.getElementById("cpg-po-file");
var poRun = document.getElementById("cpg-po-run");
var poMsg = document.getElementById("cpg-po-msg"); var poMsg = document.getElementById("cpg-po-msg");
var poResult = document.getElementById("cpg-po-result"); var poResult = document.getElementById("cpg-po-result");
var uploadShipDate = ""; // F13 − 1일 (확정 대화상자 기본값) var uploadShipDate = ""; // F13 − 1일 (확정 대화상자 기본값)
@@ -560,36 +558,17 @@
return st; return st;
} }
// 분석 결과 상세는 화면에 두지 않는다. 놓치면 안 되는 경고만 남긴다.
function renderPoResult(data, skipped) { function renderPoResult(data, skipped) {
if (!poResult) return; if (!poResult) return;
var html = '<div class="cpg-po-sum">' +
'<span class="erp-badge erp-badge-inverse">출고일 ' + esc(data.ship_date || "미확인") + "</span>" +
'<span class="erp-badge erp-badge-neutral">파일 ' + ((data.files || []).length) + "개</span>" +
'<span class="erp-badge erp-badge-neutral">센터 ' + ((data.centers || []).length) + "곳</span>" +
"</div>";
if ((data.groups || []).length > 1) {
html += '<ul class="cpg-po-dates">' + data.groups.map(function (g) {
return "<li><b>" + esc(g.ship_date || "날짜 미확인") + "</b>" +
"<span>센터 " + ((g.centers || []).length) + "곳 · " + (g.quantity || 0) + "개</span></li>";
}).join("") + "</ul>";
}
html += '<ul class="cpg-po-files">';
(data.files || []).forEach(function (f) {
html += "<li><span>" + esc(f.filename) + "</span>" +
"<em>입고 " + esc(f.arrival_date || "?") + " → 출고 " + esc(f.ship_date || "?") + "</em>" +
"<b>" + f.quantity + "개</b></li>";
});
html += "</ul>";
var warns = (data.warnings || []).slice(); var warns = (data.warnings || []).slice();
if (skipped && skipped.length) { if (skipped && skipped.length) {
warns.push("등록되지 않은 센터라 자동 배분에서 제외: " + skipped.join(", ")); warns.push("등록되지 않은 센터라 자동 배분에서 제외: " + skipped.join(", "));
} }
if (warns.length) { if (!warns.length) { poResult.hidden = true; poResult.innerHTML = ""; return; }
html += '<ul class="cpg-po-warn">' + warns.map(function (w) { poResult.innerHTML = '<ul class="cpg-po-warn">' + warns.map(function (w) {
return "<li>" + esc(w) + "</li>"; return "<li>" + esc(w) + "</li>";
}).join("") + "</ul>"; }).join("") + "</ul>";
}
poResult.innerHTML = html;
poResult.hidden = false; poResult.hidden = false;
} }
@@ -599,8 +578,7 @@
} }
var fd = new FormData(); var fd = new FormData();
Array.prototype.forEach.call(poFile.files, function (f) { fd.append("files", f); }); Array.prototype.forEach.call(poFile.files, function (f) { fd.append("files", f); });
poRun.disabled = true; poMsg.textContent = "분석 중… (" + poFile.files.length + "개)";
poMsg.textContent = "분석 중…";
fetch("/cupang/api/box-calc/upload", { method: "POST", body: fd }) fetch("/cupang/api/box-calc/upload", { method: "POST", body: fd })
.then(function (r) { .then(function (r) {
return r.json().catch(function () { return {}; }).then(function (d) { return r.json().catch(function () { return {}; }).then(function (d) {
@@ -668,11 +646,15 @@
: "업로드 결과로 계산했습니다. 센터마다 출고방식을 고르면 확정할 수 있습니다."; : "업로드 결과로 계산했습니다. 센터마다 출고방식을 고르면 확정할 수 있습니다.";
}); });
}) })
.catch(function (err) { poMsg.textContent = (err && err.message) || "업로드 실패"; }) .catch(function (err) { poMsg.textContent = (err && err.message) || "업로드 실패"; });
.then(function () { poRun.disabled = false; });
} }
if (poRun) poRun.addEventListener("click", runUpload); // 파일을 고르고 [열기] 를 누르면 곧바로 분석한다(별도 버튼 없음).
if (poFile) {
poFile.addEventListener("change", function () {
if (poFile.files && poFile.files.length) runUpload();
});
}
// ── 배분 집계 ───────────────────────────────────── // ── 배분 집계 ─────────────────────────────────────
function allocatedFor(key) { function allocatedFor(key) {
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901k" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901l" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901k" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901l" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901k" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901l" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901k" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901l" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901k" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901l" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901k" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901l" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
+48 -2
View File
@@ -601,7 +601,6 @@
.cpg-calc-card .erp-table-wrap { .cpg-calc-card .erp-table-wrap {
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
max-height: min(58vh, 560px);
} }
.cpg-calc-card .cpg-calc-table thead th { .cpg-calc-card .cpg-calc-table thead th {
position: sticky; position: sticky;
@@ -615,7 +614,7 @@
.cpg-calc-layout { align-items: stretch; } .cpg-calc-layout { align-items: stretch; }
.cpg-calc-layout > .erp-card { display: flex; flex-direction: column; } .cpg-calc-layout > .erp-card { display: flex; flex-direction: column; }
.cpg-calc-card .erp-table-wrap { flex: 1 1 auto; } .cpg-calc-card .erp-table-wrap { flex: 1 1 auto; }
.cpg-calc-sum { overflow: auto; max-height: min(78vh, 900px); } .cpg-calc-sum { overflow: hidden; }
/* 제품명 설정 — 수기 추가 폼 */ /* 제품명 설정 — 수기 추가 폼 */
.cpg-prod-manual { .cpg-prod-manual {
@@ -1343,3 +1342,50 @@ body.cpg-dragging { cursor: grabbing; user-select: none; }
padding: 2px 8px; font-size: 12px; line-height: 1.6; padding: 2px 8px; font-size: 12px; line-height: 1.6;
cursor: pointer; cursor: pointer;
} }
/* ══════════════════════════════════════════════════════════════
박스 계산 3열 — 카드마다 자체 스크롤(문서 전체는 스크롤 안 함)
각 카드: 헤더/요약은 고정, 목록만 스크롤
══════════════════════════════════════════════════════════════ */
.cpg-calc3 { flex: 1 1 auto; min-height: 0; }
.cpg-calc3 > .erp-card { min-height: 0; overflow: hidden; }
.cpg-calc3 .cpg-card-head,
.cpg-calc3 .cpg-po-box,
.cpg-calc3 .cpg-sum-kpis,
.cpg-calc3 .cpg-center-pick,
.cpg-calc3 .cpg-calc-actions,
.cpg-calc3 > .erp-card > .cpg-sum-h3 { flex-shrink: 0; }
/* ① 입력 표 — 표 머리글 고정 + 표만 스크롤 */
.cpg-calc-card .erp-table-wrap { flex: 1 1 auto; min-height: 0; }
.cpg-calc-card .cpg-calc-table thead th {
position: sticky; top: 0; z-index: 2; background: var(--color-canvas-white);
}
/* 행 높이 축소 — 한 화면에 더 많은 품목 */
.cpg-calc-card .cpg-calc-table th,
.cpg-calc-card .cpg-calc-table td { padding: 2px 6px; }
.cpg-calc-card .cpg-calc-table .erp-select,
.cpg-calc-card .cpg-calc-table .erp-input {
padding: 2px 6px; font-size: 12px; line-height: 1.5; height: auto;
}
.cpg-calc-card .cpg-calc-table .cpg-calc-del { padding: 0 6px; font-size: 11px; line-height: 1.6; }
/* ② 요약 — KPI 까지 고정, 아래 목록만 스크롤 */
.cpg-calc-sum #cpg-sum-body { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.cpg-sum-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 2px; }
.cpg-sum-scroll > .cpg-sum-h3:first-child { margin-top: 0; }
/* ③ 센터 분배 — 센터 목록만 스크롤 */
.cpg-dist-card .cpg-dist-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 2px; }
/* 업로드 버튼 — 네이티브 file input 은 숨기고 라벨을 버튼처럼 */
.cpg-po-pick { cursor: pointer; }
.cpg-po-box .cpg-po-file {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; border: 0; overflow: hidden; clip: rect(0 0 0 0);
}
.cpg-po-row { align-items: center; }
.cpg-po-msg { margin: 0; font-size: 12px; }
/* 상단 액션/출고일 탭은 줄어들지 않게 (3열 프레임만 남은 높이를 차지) */
.cpg > .erp-page-actions,
.cpg > .cpg-date-tabs { flex-shrink: 0; }