@@ -204,6 +236,7 @@
var labels = {}; // key -> 표시 이름
var openCenters = []; // ③ 에 추가한 센터 id (문자열)
var alloc = {}; // centerId -> [{id, key, count}]
+ var methods = {}; // centerId -> 출고방식(택배/파렛트)
var seq = 0;
var pending = null; // 대화상자 대상 {key, centerId}
@@ -270,7 +303,7 @@
units[mixKey(m.box_name, i)] = n;
labels[mixKey(m.box_name, i)] = m.box_name + " 혼합 #" + (i + 1);
contents[mixKey(m.box_name, i)] = b.items.map(function (it) {
- return { product_name: it.product_name, quantity: it.quantity };
+ return { product_code: it.product_code, product_name: it.product_name, quantity: it.quantity };
});
});
});
@@ -486,9 +519,16 @@
treeHtml(a) +
"";
});
+ var method = methods[String(cid)] || "";
html += '
' +
'
' +
"" + esc(centerName(cid)) + "" +
+ '" +
'' +
'' + boxes + "박스" +
'' + pieces + "개" +
@@ -501,6 +541,7 @@
"
";
});
distList.innerHTML = html;
+ syncConfirm();
}
function render() { renderSummary(); renderDist(); }
@@ -690,6 +731,7 @@
if (tree) tree.outerHTML = treeHtml(entry);
renderSummary();
updateCenterTotals(cid);
+ syncConfirm();
});
distList.addEventListener("click", function (e) {
@@ -699,6 +741,7 @@
if (e.target.closest(".cpg-dist-close")) { // 센터 자체를 목록에서 뺀다
openCenters = openCenters.filter(function (x) { return x !== cid; });
delete alloc[cid];
+ delete methods[cid];
render();
return;
}
@@ -778,6 +821,196 @@
msg.textContent = "";
});
+ // ── 분배 확정 ─────────────────────────────────────
+ var confirmBtn = document.getElementById("cpg-confirm");
+ var cfmDlg = document.getElementById("cpg-cfm-dlg");
+ var cfmOk = document.getElementById("cpg-cfm-ok");
+ var cfmMsg = document.getElementById("cpg-cfm-msg");
+ var cfmPick = document.getElementById("cpg-cfm-pick");
+ var cfmList = document.getElementById("cpg-cfm-list");
+ var calGrid = document.getElementById("cpg-cal-grid");
+ var calLabel = document.getElementById("cpg-cal-label");
+ var picked = ""; // 선택한 출고일 (YYYY-MM-DD)
+ var calY = 0, calM = 0; // 달력이 보여주는 연/월
+
+ function pad2(n) { return (n < 10 ? "0" : "") + n; }
+ function iso(y, m, d) { return y + "-" + pad2(m + 1) + "-" + pad2(d); }
+
+ // 배분이 남았는지 — ② 요약과 같은 기준
+ function boxTotals() {
+ var total = 0, remain = 0;
+ calc.results.forEach(function (r) {
+ total += r.full_boxes;
+ remain += remainFor(prodKey(r.product_code));
+ });
+ calc.mixes.forEach(function (m) {
+ m.boxes.forEach(function (b, i) {
+ total += 1;
+ remain += remainFor(mixKey(m.box_name, i));
+ });
+ });
+ return { total: total, remain: remain };
+ }
+
+ function filledCenters() {
+ return openCenters.filter(function (cid) { return (alloc[cid] || []).length > 0; });
+ }
+
+ // 확정 가능 여부 + 왜 안 되는지 사유
+ function confirmBlockReason() {
+ var t = boxTotals();
+ if (!t.total) return "먼저 ① 에서 계산하세요.";
+ if (t.remain) return "아직 " + t.remain + "박스가 남았습니다.";
+ var cids = filledCenters();
+ if (!cids.length) return "센터에 담긴 박스가 없습니다.";
+ var missing = cids.filter(function (cid) { return !methods[String(cid)]; });
+ if (missing.length) return "출고방식 미선택: " + missing.map(centerName).join(", ");
+ return "";
+ }
+
+ function syncConfirm() {
+ if (!confirmBtn) return;
+ var why = confirmBlockReason();
+ confirmBtn.disabled = !!why;
+ confirmBtn.title = why || "출고일자를 골라 출고 묶음을 만듭니다.";
+ }
+
+ // 센터별 전송 payload — 혼합 박스는 내용물 제품으로 풀어서 담는다.
+ function buildPlans() {
+ return filledCenters().map(function (cid) {
+ var rows = alloc[cid] || [];
+ var items = [];
+ var boxes = 0;
+ rows.forEach(function (a) {
+ boxes += a.count;
+ if (a.key.indexOf("p:") === 0) {
+ items.push({ product_code: a.key.slice(2), quantity: a.count * (units[a.key] || 0) });
+ } else {
+ (contents[a.key] || []).forEach(function (it) {
+ items.push({ product_code: it.product_code, quantity: it.quantity * a.count });
+ });
+ }
+ });
+ return {
+ center_id: cid,
+ ship_method: methods[String(cid)] || "",
+ boxes: boxes,
+ items: items
+ };
+ });
+ }
+
+ function renderCal() {
+ if (!calGrid) return;
+ calLabel.textContent = calY + "년 " + (calM + 1) + "월";
+ var first = new Date(calY, calM, 1).getDay();
+ var days = new Date(calY, calM + 1, 0).getDate();
+ var now = new Date();
+ var todayIso = iso(now.getFullYear(), now.getMonth(), now.getDate());
+ var html = "";
+ for (var i = 0; i < first; i++) html += '
';
+ for (var d = 1; d <= days; d++) {
+ var v = iso(calY, calM, d);
+ var dow = (first + d - 1) % 7;
+ html += '
";
+ }
+ calGrid.innerHTML = html;
+ cfmPick.textContent = picked ? "출고일: " + picked : "달력에서 출고일자를 고르세요.";
+ cfmOk.disabled = !picked;
+ }
+
+ function openConfirm() {
+ var why = confirmBlockReason();
+ if (why) { msg.textContent = why; return; }
+ var t = new Date();
+ picked = iso(t.getFullYear(), t.getMonth(), t.getDate());
+ calY = t.getFullYear();
+ calM = t.getMonth();
+ cfmMsg.textContent = "";
+ cfmList.innerHTML = buildPlans().map(function (pl) {
+ var pieces = pl.items.reduce(function (n, it) { return n + it.quantity; }, 0);
+ return '
' +
+ '' + esc(centerName(pl.center_id)) + "" +
+ '' + esc(pl.ship_method) + " · " + pl.boxes + "박스 · " + pieces + "개" +
+ "
";
+ }).join("");
+ renderCal();
+ cfmDlg.hidden = false;
+ cfmOk.focus();
+ }
+
+ function closeConfirm() { cfmDlg.hidden = true; }
+
+ function submitConfirm() {
+ if (!picked) return;
+ var plans = buildPlans();
+ if (!plans.length) { cfmMsg.textContent = "담긴 품목이 없습니다."; return; }
+ cfmOk.disabled = true;
+ cfmMsg.textContent = "저장 중…";
+ fetch("/cupang/api/box-calc/confirm", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ ship_date: picked, centers: plans })
+ })
+ .then(function (r) {
+ if (!r.ok) {
+ return r.json().catch(function () { return {}; }).then(function (e) {
+ throw new Error(e.detail || "http " + r.status);
+ });
+ }
+ return r.json();
+ })
+ .then(function (data) {
+ var d = (data && data.ship_date) || picked;
+ var parts = d.split("-");
+ // 달력으로 이동해 방금 만든 출고 묶음을 보여준다.
+ window.location.href = "/cupang/?year=" + parts[0] + "&month=" + parseInt(parts[1], 10) + "&date=" + d;
+ })
+ .catch(function (err) {
+ cfmMsg.textContent = (err && err.message) || "확정 실패 — 다시 시도하세요.";
+ cfmOk.disabled = false;
+ });
+ }
+
+ if (confirmBtn) confirmBtn.addEventListener("click", openConfirm);
+ if (cfmDlg) {
+ Array.prototype.forEach.call(cfmDlg.querySelectorAll("[data-cfm-close]"), function (el) {
+ el.addEventListener("click", closeConfirm);
+ });
+ calGrid.addEventListener("click", function (e) {
+ var cell = e.target.closest("[data-day]");
+ if (!cell) return;
+ picked = cell.getAttribute("data-day");
+ renderCal();
+ });
+ document.getElementById("cpg-cal-prev").addEventListener("click", function () {
+ calM -= 1; if (calM < 0) { calM = 11; calY -= 1; } renderCal();
+ });
+ document.getElementById("cpg-cal-next").addEventListener("click", function () {
+ calM += 1; if (calM > 11) { calM = 0; calY += 1; } renderCal();
+ });
+ cfmOk.addEventListener("click", submitConfirm);
+ }
+ document.addEventListener("keydown", function (e) {
+ if (e.key === "Escape" && cfmDlg && !cfmDlg.hidden) closeConfirm();
+ });
+
+ // 센터 헤더의 출고방식 선택
+ if (distList) {
+ distList.addEventListener("change", function (e) {
+ var sel = e.target.closest("[data-method-for]");
+ if (!sel) return;
+ var cid = String(sel.getAttribute("data-method-for"));
+ if (sel.value) { methods[cid] = sel.value; } else { delete methods[cid]; }
+ sel.classList.toggle("is-unset", !sel.value);
+ syncConfirm();
+ });
+ }
+
// ── 임시 저장 / 불러오기 ──────────────────────────
var saveBtn = document.getElementById("cpg-calc-save");
var loadBtn = document.getElementById("cpg-calc-load");
@@ -792,6 +1025,7 @@
items: collect(),
openCenters: openCenters.slice(),
alloc: alloc,
+ methods: methods,
stamped: stamped,
allStamped: allStamped
};
@@ -814,6 +1048,7 @@
if (addAlloc(cid, a.key, a.count) <= 0) skipped += 1;
});
});
+ methods = snap.methods && typeof snap.methods === "object" ? snap.methods : {};
stamped = snap.stamped && typeof snap.stamped === "object" ? snap.stamped : {};
allStamped = !!snap.allStamped;
return skipped;
diff --git a/app/modules/cupang/templates/cupang/box_rules.html b/app/modules/cupang/templates/cupang/box_rules.html
index d5c9734..9f2dcd8 100644
--- a/app/modules/cupang/templates/cupang/box_rules.html
+++ b/app/modules/cupang/templates/cupang/box_rules.html
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
-{% block head_extra %}
{% endblock %}
+{% block head_extra %}
{% endblock %}
{% block content %}
diff --git a/app/modules/cupang/templates/cupang/centers.html b/app/modules/cupang/templates/cupang/centers.html
index 71f61b7..570d415 100644
--- a/app/modules/cupang/templates/cupang/centers.html
+++ b/app/modules/cupang/templates/cupang/centers.html
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
-{% block head_extra %}{% endblock %}
+{% block head_extra %}{% endblock %}
{% block content %}
diff --git a/app/modules/cupang/templates/cupang/detail.html b/app/modules/cupang/templates/cupang/detail.html
index cc2dcc1..76bad8d 100644
--- a/app/modules/cupang/templates/cupang/detail.html
+++ b/app/modules/cupang/templates/cupang/detail.html
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
-{% block head_extra %}{% endblock %}
+{% block head_extra %}{% endblock %}
{% block content %}
diff --git a/app/modules/cupang/templates/cupang/form.html b/app/modules/cupang/templates/cupang/form.html
index bd4bf96..63f5bf1 100644
--- a/app/modules/cupang/templates/cupang/form.html
+++ b/app/modules/cupang/templates/cupang/form.html
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
-{% block head_extra %}{% endblock %}
+{% block head_extra %}{% endblock %}
{% block content %}
diff --git a/app/modules/cupang/templates/cupang/index.html b/app/modules/cupang/templates/cupang/index.html
index 407c7cc..d13ad62 100644
--- a/app/modules/cupang/templates/cupang/index.html
+++ b/app/modules/cupang/templates/cupang/index.html
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
-{% block head_extra %}{% endblock %}
+{% block head_extra %}{% endblock %}
{% block content %}
diff --git a/app/modules/cupang/templates/cupang/products.html b/app/modules/cupang/templates/cupang/products.html
index a40d905..affa544 100644
--- a/app/modules/cupang/templates/cupang/products.html
+++ b/app/modules/cupang/templates/cupang/products.html
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
-{% block head_extra %}{% endblock %}
+{% block head_extra %}{% endblock %}
{% block content %}
diff --git a/app/static/cupang.css b/app/static/cupang.css
index 79cde69..30d92d4 100644
--- a/app/static/cupang.css
+++ b/app/static/cupang.css
@@ -809,6 +809,79 @@ body.cpg-dragging { cursor: grabbing; user-select: none; }
.cpg-dist-card.is-drop-ready { outline: 2px dashed var(--color-rich-black); outline-offset: 2px; }
.cpg-dist-card.is-drop-ready .cpg-dist-center { border-color: var(--color-midtone-gray); }
+/* ③ 헤더의 분배 확정 버튼 */
+.cpg-dist-head-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
+.cpg-dist-head-bar > .erp-muted { flex: 1 1 auto; min-width: 0; }
+.cpg-confirm-btn { flex: 0 0 auto; }
+.cpg-confirm-btn:disabled { opacity: .4; cursor: not-allowed; }
+
+/* 센터별 출고방식 */
+.cpg-dist-method {
+ flex: 0 0 auto;
+ width: auto; min-width: 0;
+ padding: 2px 22px 2px 8px;
+ font-size: 11px; line-height: 1.6;
+ border-radius: 6px;
+}
+.cpg-dist-center.has-items .cpg-dist-method {
+ background-color: rgba(255, 255, 255, .14);
+ border-color: rgba(255, 255, 255, .3);
+ color: var(--color-canvas-white);
+}
+.cpg-dist-center.has-items .cpg-dist-method option { color: var(--color-rich-black); }
+/* 미선택은 눈에 띄게 — 확정 버튼이 막히는 이유 */
+.cpg-dist-center.has-items .cpg-dist-method.is-unset {
+ background-color: var(--color-callout-red);
+ border-color: var(--color-callout-red);
+}
+
+/* 분배 확정 대화상자 — 달력 */
+.cpg-cfm-box { width: min(460px, calc(100vw - 32px)); }
+.cpg-cal { border: 1px solid var(--color-subtle-ash); border-radius: 8px; overflow: hidden; }
+.cpg-cal-head {
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
+ padding: 6px 8px;
+ background: var(--color-ghost-gray);
+ border-bottom: 1px solid var(--color-subtle-ash);
+ font-size: 13px;
+}
+.cpg-cal-dow, .cpg-cal-grid {
+ display: grid; grid-template-columns: repeat(7, 1fr);
+}
+.cpg-cal-dow {
+ padding: 4px 6px 2px;
+ font-size: 11px; color: var(--color-midtone-gray); text-align: center;
+}
+.cpg-cal-dow > span:first-child { color: var(--color-callout-red); }
+.cpg-cal-grid { gap: 2px; padding: 6px; }
+.cpg-cal-cell {
+ border: 1px solid transparent; background: none; cursor: pointer;
+ font-family: var(--font-geist-mono);
+ font-size: 13px; line-height: 1;
+ padding: 7px 0; border-radius: 6px;
+ color: var(--color-rich-black);
+}
+.cpg-cal-cell.is-blank { cursor: default; }
+.cpg-cal-cell.is-sun { color: var(--color-callout-red); }
+.cpg-cal-cell.is-sat { color: #1a56c2; }
+.cpg-cal-cell:not(.is-blank):hover { background: var(--color-ghost-gray); }
+.cpg-cal-cell.is-today { border-color: var(--color-subtle-ash); font-weight: 700; }
+.cpg-cal-cell.is-picked {
+ background: var(--color-rich-black); border-color: var(--color-rich-black);
+ color: var(--color-canvas-white); font-weight: 700;
+}
+
+.cpg-cfm-pick { margin: 0; font-size: 13px; font-weight: 600; }
+.cpg-cfm-list { border: 1px solid var(--color-subtle-ash); border-radius: 8px; max-height: 140px; overflow-y: auto; }
+.cpg-cfm-row { display: flex; align-items: center; gap: 8px; padding: 6px 10px; }
+.cpg-cfm-row + .cpg-cfm-row { border-top: 1px solid var(--color-ghost-gray); }
+.cpg-cfm-cname { flex: 1 1 auto; min-width: 0; font-size: 13px; font-weight: 600;
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.cpg-cfm-cmeta { flex: 0 0 auto; font-family: var(--font-geist-mono); font-size: 11px; color: var(--color-midtone-gray); }
+.cpg-cfm-note { margin: 0; font-size: 12px; }
+.cpg-cfm-act { display: flex; align-items: center; gap: 6px; justify-content: flex-end; }
+.cpg-cfm-act > .erp-muted { flex: 1 1 auto; min-width: 0; font-size: 12px; }
+
/* 임시 저장 / 불러오기 대화상자 */
.cpg-draft-box { width: min(520px, calc(100vw - 32px)); }
.cpg-draft-saveact { display: flex; align-items: center; gap: 8px; }