diff --git a/app/modules/cupang/templates/cupang/box_calc.html b/app/modules/cupang/templates/cupang/box_calc.html index 133f701..00ae37b 100644 --- a/app/modules/cupang/templates/cupang/box_calc.html +++ b/app/modules/cupang/templates/cupang/box_calc.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %} @@ -351,7 +351,6 @@ var had = Object.keys(alloc).some(function (k) { return alloc[k].length; }); alloc = {}; stamped = {}; - pool = {}; allStamped = false; // 계산이 바뀌면 이전 배분은 근거가 사라지므로 비운다. var skipped = restore ? applyRestore(restore) : 0; render(); @@ -433,7 +432,7 @@ units: units, labels: labels, contents: contents, openCenters: openCenters.slice(), alloc: alloc, methods: methods, stamped: stamped, allStamped: allStamped, - mixType: mixType, mixOwner: mixOwner, pool: pool + mixType: mixType, mixOwner: mixOwner }; } @@ -450,7 +449,6 @@ alloc = st.alloc; methods = st.methods; stamped = st.stamped; allStamped = st.allStamped; mixType = st.mixType || {}; mixOwner = st.mixOwner || {}; - pool = st.pool || {}; tbody.innerHTML = ""; (st.items || []).forEach(function (it) { addRow(it.product_code, it.quantity); }); @@ -482,7 +480,7 @@ units: {}, labels: {}, contents: {}, openCenters: [], alloc: {}, methods: {}, stamped: {}, allStamped: false, - mixType: {}, mixOwner: {}, pool: {} + mixType: {}, mixOwner: {} }; function put(cid, key, count) { @@ -691,8 +689,6 @@ // ── 혼합 상자 편집 ──────────────────────────────── // 상자 용량은 부피 합으로 본다: 제품 1개 = 1/입수량 상자. // 빼낸 수량은 센터별 "미배치 자투리" 풀에 남고, 거기서 다시 담는다. - var pool = {}; // centerId -> [{product_code, product_name, quantity}] - function upbOf(code) { var hit = calc.results.filter(function (r) { return r.product_code === code; })[0]; return hit ? (hit.units_per_box || 0) : 0; @@ -766,36 +762,6 @@ .map(function (a) { return a.key; }); } - function poolOf(cid) { - cid = String(cid); - if (!pool[cid]) pool[cid] = []; - return pool[cid]; - } - - function poolAdd(cid, code, name, qty) { - if (qty <= 0) return; - var list = poolOf(cid); - var hit = list.filter(function (it) { return it.product_code === code; })[0]; - if (hit) { hit.quantity += qty; return; } - list.push({ product_code: code, product_name: name, quantity: qty }); - } - - function poolTake(cid, code, qty) { - var list = poolOf(cid); - var hit = list.filter(function (it) { return it.product_code === code; })[0]; - if (!hit) return 0; - var take = Math.min(hit.quantity, qty); - hit.quantity -= take; - pool[String(cid)] = list.filter(function (it) { return it.quantity > 0; }); - return take; - } - - function poolTotal(cid) { - var n = 0; - poolOf(cid).forEach(function (it) { n += it.quantity; }); - return n; - } - function mixItem(key, code) { return (contents[key] || []).filter(function (it) { return it.product_code === code; })[0]; } @@ -812,19 +778,48 @@ return put; } - // 상자에서 빼기 — 뺀 수량은 그 센터의 미배치 풀로. - function mixTake(key, code, qty) { + // 상자에서 덜어내기(내부용) — 호출한 쪽이 곧바로 다른 상자에 담는다. + function mixPull(key, code, qty) { var hit = mixItem(key, code); if (!hit) return 0; var take = Math.min(hit.quantity, qty); if (take <= 0) return 0; - var cid = centerOfKey(key); hit.quantity -= take; syncMix(key); - if (cid) poolAdd(cid, code, hit.product_name, take); return take; } + // 자리가 있는 다른 상자 찾기 (없으면 "") + function boxWithRoom(cid, code, exceptKey) { + var found = ""; + mixKeysOf(cid).forEach(function (k) { + if (found || k === exceptKey) return; + if (mixRoomFor(k, code) > 0) found = k; + }); + return found; + } + + // 그 제품이 담겨 있는 다른 상자 찾기 (없으면 "") + function boxHaving(cid, code, exceptKey) { + var found = ""; + mixKeysOf(cid).forEach(function (k) { + if (found || k === exceptKey) return; + if (mixItem(k, code)) found = k; + }); + return found; + } + + // 상자 → 상자 이동. 실제로 옮긴 수량을 돌려준다(자리만큼만). + function moveUnits(fromKey, toKey, code, name, qty) { + var room = mixRoomFor(toKey, code); + var move = Math.min(qty, room); + if (move <= 0) return 0; + var pulled = mixPull(fromKey, code, move); + if (!pulled) return 0; + mixPut(toKey, code, name, pulled); + return pulled; + } + // 새 혼합 상자 (같은 센터, 비어 있는 상태로 추가) function newMixBox(cid, boxName) { var name = boxName || (calc.mixes[0] && calc.mixes[0].box_name) || MIX_TYPES[0]; @@ -983,7 +978,6 @@ esc(labels[k] || k) + (room ? " (여유 " + room + "개)" : " (가득)") + ""; }); opts += '+ 새 상자로'; - opts += '미배치로 빼기'; return opts; } @@ -1000,15 +994,16 @@ (entry.count > 1 ? '(' + it.quantity + "개 × " + entry.count + "상자)" : "") + ""; } - var canAdd = mixRoomFor(entry.key, it.product_code) > 0; + var canAdd = mixRoomFor(entry.key, it.product_code) > 0 && + !!boxHaving(cid, it.product_code, entry.key); return '' + esc(it.product_name) + "" + '' + '−' + + '" data-code="' + esc(it.product_code) + '" title="다른 상자로 1개 옮기기">−' + '' + qty + "개" + '+' + + ' title="다른 상자에서 1개 가져오기">+' + '' + moveOptions(entry.key, cid, it.product_code) + @@ -1017,34 +1012,6 @@ }).join("") + ""; } - // 센터별 미배치 자투리 — 상자에서 뺀 수량이 여기 모인다. - function poolHtml(cid) { - var list = poolOf(cid); - if (!list.length) return ""; - var rows = list.map(function (it) { - var opts = '담을 상자…'; - mixKeysOf(cid).forEach(function (k) { - var room = mixRoomFor(k, it.product_code); - opts += '" + - esc(labels[k] || k) + (room ? " (여유 " + room + "개)" : " (가득)") + ""; - }); - opts += '+ 새 상자에'; - return '' + esc(it.product_name) + "" + - '' + - '' + it.quantity + "개" + - '' + opts + "" + - ""; - }).join(""); - return '' + - '미배치 자투리 ' + poolTotal(cid) + "개" + - '자동 담기' + - "" + - '' + rows + "" + - ""; - } - // ── ③ 센터 분배 렌더 ────────────────────────────── function renderDist() { if (!distList) return; @@ -1090,7 +1057,6 @@ (rows.length ? '' + items + "" : '여기로 상자를 끌어다 놓기') + - poolHtml(cid) + (rows.length ? '' + ' 0) { + var dest = boxWithRoom(dcid, it.product_code, entry.key) || newMixBox(dcid); + var moved = moveUnits(entry.key, dest, it.product_code, it.product_name, left); + if (!moved) break; + left -= moved; + } }); - contents[entry.key] = []; syncMix(entry.key); render(); - msg.textContent = "혼합 상자를 비우고 미배치로 옮겼습니다."; + msg.textContent = "혼합 상자를 비우고 내용물을 다른 상자로 옮겼습니다."; } return; } + var dec = e.target.closest("[data-mix-dec]"); + if (dec) { + var key = dec.getAttribute("data-key"); + var code = dec.getAttribute("data-code"); + var cid = centerOfKey(key); + var item = mixItem(key, code); + if (!item) return; + var to = boxWithRoom(cid, code, key) || newMixBox(cid); + var n = moveUnits(key, to, code, item.product_name, 1); + if (!n) { msg.textContent = "옮길 자리가 없습니다."; return; } + render(); + msg.textContent = "1개를 " + (labels[to] || "다른 상자") + " 로 옮겼습니다."; + return; + } + + var inc = e.target.closest("[data-mix-inc]"); + if (inc) { + var ikey = inc.getAttribute("data-key"); + var icode = inc.getAttribute("data-code"); + var icid = centerOfKey(ikey); + if (mixRoomFor(ikey, icode) <= 0) { + msg.textContent = "이 상자에 더 들어갈 자리가 없습니다."; return; + } + var from = boxHaving(icid, icode, ikey); + if (!from) { msg.textContent = "다른 상자에 이 제품이 없습니다."; return; } + var src = mixItem(from, icode); + var got = moveUnits(from, ikey, icode, src.product_name, 1); + if (!got) { msg.textContent = "가져올 수 없습니다."; return; } + render(); + msg.textContent = "1개를 " + (labels[from] || "다른 상자") + " 에서 가져왔습니다."; + return; + } + var neu = e.target.closest(".cpg-mix-new"); if (neu) { newMixBox(neu.getAttribute("data-center")); render(); msg.textContent = "빈 혼합 상자를 추가했습니다."; - return; - } - var auto = e.target.closest(".cpg-pool-auto"); - if (auto) { - var acid = auto.getAttribute("data-center"); - var left = 0; - poolOf(acid).slice().forEach(function (it) { - var need = it.quantity; - mixKeysOf(acid).forEach(function (k) { - if (need <= 0) return; - var put = mixPut(k, it.product_code, it.product_name, Math.min(need, mixRoomFor(k, it.product_code))); - if (put) { poolTake(acid, it.product_code, put); need -= put; } - }); - if (need > 0) { - var nk = newMixBox(acid); - var put2 = mixPut(nk, it.product_code, it.product_name, Math.min(need, mixRoomFor(nk, it.product_code))); - if (put2) { poolTake(acid, it.product_code, put2); need -= put2; } - } - left += Math.max(need, 0); - }); - render(); - msg.textContent = left ? "일부는 담지 못했습니다 (" + left + "개)" : "미배치 자투리를 모두 담았습니다."; } }); distList.addEventListener("change", function (e) { var mv = e.target.closest(".cpg-mix-move"); - if (mv) { - var fromKey = mv.getAttribute("data-key"); - var code = mv.getAttribute("data-code"); - var target = mv.value; - mv.value = ""; - if (!target) return; - var src = mixItem(fromKey, code); - if (!src) return; - var qty = src.quantity; - var name = src.product_name; - var cid = centerOfKey(fromKey); - if (target === "__pool__") { - mixTake(fromKey, code, qty); - render(); - msg.textContent = name + " " + qty + "개를 미배치로 뺐습니다."; - return; - } - var toKey = target === "__new__" ? newMixBox(cid) : target; - var room = mixRoomFor(toKey, code); - var move = Math.min(qty, room); - if (move <= 0) { msg.textContent = "옮길 자리가 없습니다."; render(); return; } - mixTake(fromKey, code, move); // 일단 미배치로 - poolTake(cid, code, move); // 바로 꺼내서 - mixPut(toKey, code, name, move); // 대상 상자에 담기 - render(); - msg.textContent = name + " " + move + "개를 " + (labels[toKey] || "새 상자") + " 로 옮겼습니다." + - (move < qty ? " (자리가 부족해 " + (qty - move) + "개는 남음)" : ""); - return; - } - - var pm = e.target.closest(".cpg-pool-move"); - if (pm) { - var pcid = pm.getAttribute("data-center"); - var pcode = pm.getAttribute("data-code"); - var dest = pm.value; - pm.value = ""; - if (!dest) return; - var entry = poolOf(pcid).filter(function (it) { return it.product_code === pcode; })[0]; - if (!entry) return; - var key2 = dest === "__new__" ? newMixBox(pcid) : dest; - var put = Math.min(entry.quantity, mixRoomFor(key2, pcode)); - if (put <= 0) { msg.textContent = "그 상자에는 자리가 없습니다."; render(); return; } - poolTake(pcid, pcode, put); - mixPut(key2, pcode, entry.product_name, put); - render(); - msg.textContent = entry.product_name + " " + put + "개를 담았습니다."; - } + if (!mv) return; + var fromKey = mv.getAttribute("data-key"); + var code = mv.getAttribute("data-code"); + var target = mv.value; + mv.value = ""; + if (!target) return; + var src = mixItem(fromKey, code); + if (!src) return; + var qty = src.quantity; + var name = src.product_name; + var cid = centerOfKey(fromKey); + var toKey = target === "__new__" ? newMixBox(cid) : target; + var moved = moveUnits(fromKey, toKey, code, name, qty); + if (!moved) { msg.textContent = "옮길 자리가 없습니다."; render(); return; } + render(); + msg.textContent = name + " " + moved + "개를 " + (labels[toKey] || "새 상자") + " 로 옮겼습니다." + + (moved < qty ? " (자리가 부족해 " + (qty - moved) + "개는 남음)" : ""); }); } @@ -1573,9 +1502,6 @@ if (!cids.length) return "센터에 담긴 상자가 없습니다."; var missing = cids.filter(function (cid) { return !methods[String(cid)]; }); if (missing.length) return "출고방식 미선택: " + missing.map(centerName).join(", "); - var leftover = 0; - openCenters.forEach(function (cid) { leftover += poolTotal(cid); }); - if (leftover) return "미배치 자투리 " + leftover + "개가 남았습니다."; return ""; } diff --git a/app/modules/cupang/templates/cupang/box_rules.html b/app/modules/cupang/templates/cupang/box_rules.html index bd52dc9..8facdc4 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 154cba6..6ec8aba 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/index.html b/app/modules/cupang/templates/cupang/index.html index 40375aa..d711df1 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 a4a9435..0542670 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/modules/cupang/templates/cupang/view.html b/app/modules/cupang/templates/cupang/view.html index bbb38d5..dac57c4 100644 --- a/app/modules/cupang/templates/cupang/view.html +++ b/app/modules/cupang/templates/cupang/view.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %} {# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #} diff --git a/app/static/cupang.css b/app/static/cupang.css index 499a582..7e281e4 100644 --- a/app/static/cupang.css +++ b/app/static/cupang.css @@ -663,10 +663,14 @@ @media (max-width: 1500px) { .cpg-calc3 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } .cpg-calc3 > .cpg-dist-card { grid-column: 1 / -1; } + /* 3열이 접히면 그리드 행 높이가 auto 라 카드가 계속 늘어난다 → + 카드마다 최대 높이를 줘서 내부 목록이 스크롤되게 한다. */ + .cpg-calc3 > .erp-card { max-height: min(78vh, 860px); } } @media (max-width: 1000px) { .cpg-calc3 { grid-template-columns: minmax(0, 1fr); } .cpg-calc3 > .cpg-dist-card { grid-column: auto; } + .cpg-calc3 > .erp-card { max-height: min(70vh, 720px); } } /* ② 요약: 카드가 드래그 가능함을 알린다 */ @@ -1434,25 +1438,18 @@ body.cpg-dragging { cursor: grabbing; user-select: none; } } .cpg-mix-step:hover:not(:disabled) { background: var(--color-ghost-gray); border-color: var(--color-midtone-gray); } .cpg-mix-step:disabled { opacity: .35; cursor: not-allowed; } -.cpg-mix-move, .cpg-pool-move { +.cpg-mix-move { width: auto; min-width: 74px; max-width: 132px; padding: 1px 4px; font-size: 11px; line-height: 1.5; cursor: pointer; } .cpg-dist-tree .cpg-tree-qty { min-width: 38px; text-align: right; } -.cpg-pool { - margin: 6px 10px 8px; - padding: 8px 10px; - border: 1px dashed var(--color-midtone-gray); border-radius: 10px; - background: var(--color-ghost-gray); -} -.cpg-pool-head { - display: flex; align-items: center; gap: 8px; - font-size: 12px; font-weight: 600; margin-bottom: 4px; -} -.cpg-pool-head > b { font-family: var(--font-geist-mono); } -.cpg-pool-auto { margin-left: auto; padding: 2px 8px; font-size: 11px; line-height: 1.5; } -.cpg-pool .cpg-dist-tree { border-left: 0; padding-left: 0; } -.cpg-pool .cpg-dist-tree li::before { content: ""; } .cpg-dist-foot { padding: 0 10px 10px; } .cpg-mix-new { padding: 3px 10px; font-size: 12px; line-height: 1.5; } + +/* ③ 센터 목록은 항상 카드 안에서만 스크롤 (편집으로 줄이 늘어나도 페이지가 밀리지 않게) */ +.cpg-dist-card { min-height: 0; overflow: hidden; } +.cpg-dist-card .cpg-dist-list { + flex: 1 1 auto; min-height: 0; + overflow-y: auto; overflow-x: hidden; +}
여기로 상자를 끌어다 놓기