change(cupang): 미배치 자투리 제거 — 상자 간 직접 이동만, ③ 스크롤 보정

- "미배치로 빼기" 옵션과 미배치 풀 UI/상태 삭제
- [−] 는 자리가 있는 다른 상자로 1개 이동(없으면 새 상자 자동 생성),
  [+] 는 같은 제품이 있는 다른 상자에서 1개 가져오기 → 총량 항상 보존
- 혼합 상자 ✕ 는 내용물을 다른 상자로 옮긴 뒤 상자를 없앤다
- 3열이 2열/1열로 접히는 폭에서 카드 max-height 를 줘서 ③ 목록이
  카드 안에서 스크롤되도록 수정(편집으로 줄이 늘어나도 페이지가 안 밀림)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 16:33:32 +09:00
parent f3a681ccfe
commit 58ebf1c232
7 changed files with 117 additions and 194 deletions
+100 -174
View File
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901p" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901q" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -351,7 +351,6 @@
var had = Object.keys(alloc).some(function (k) { return alloc[k].length; }); var had = Object.keys(alloc).some(function (k) { return alloc[k].length; });
alloc = {}; alloc = {};
stamped = {}; stamped = {};
pool = {};
allStamped = false; // 계산이 바뀌면 이전 배분은 근거가 사라지므로 비운다. allStamped = false; // 계산이 바뀌면 이전 배분은 근거가 사라지므로 비운다.
var skipped = restore ? applyRestore(restore) : 0; var skipped = restore ? applyRestore(restore) : 0;
render(); render();
@@ -433,7 +432,7 @@
units: units, labels: labels, contents: contents, units: units, labels: labels, contents: contents,
openCenters: openCenters.slice(), alloc: alloc, methods: methods, openCenters: openCenters.slice(), alloc: alloc, methods: methods,
stamped: stamped, allStamped: allStamped, stamped: stamped, allStamped: allStamped,
mixType: mixType, mixOwner: mixOwner, pool: pool mixType: mixType, mixOwner: mixOwner
}; };
} }
@@ -450,7 +449,6 @@
alloc = st.alloc; methods = st.methods; alloc = st.alloc; methods = st.methods;
stamped = st.stamped; allStamped = st.allStamped; stamped = st.stamped; allStamped = st.allStamped;
mixType = st.mixType || {}; mixOwner = st.mixOwner || {}; mixType = st.mixType || {}; mixOwner = st.mixOwner || {};
pool = st.pool || {};
tbody.innerHTML = ""; tbody.innerHTML = "";
(st.items || []).forEach(function (it) { addRow(it.product_code, it.quantity); }); (st.items || []).forEach(function (it) { addRow(it.product_code, it.quantity); });
@@ -482,7 +480,7 @@
units: {}, labels: {}, contents: {}, units: {}, labels: {}, contents: {},
openCenters: [], alloc: {}, methods: {}, openCenters: [], alloc: {}, methods: {},
stamped: {}, allStamped: false, stamped: {}, allStamped: false,
mixType: {}, mixOwner: {}, pool: {} mixType: {}, mixOwner: {}
}; };
function put(cid, key, count) { function put(cid, key, count) {
@@ -691,8 +689,6 @@
// ── 혼합 상자 편집 ──────────────────────────────── // ── 혼합 상자 편집 ────────────────────────────────
// 상자 용량은 부피 합으로 본다: 제품 1개 = 1/입수량 상자. // 상자 용량은 부피 합으로 본다: 제품 1개 = 1/입수량 상자.
// 빼낸 수량은 센터별 "미배치 자투리" 풀에 남고, 거기서 다시 담는다. // 빼낸 수량은 센터별 "미배치 자투리" 풀에 남고, 거기서 다시 담는다.
var pool = {}; // centerId -> [{product_code, product_name, quantity}]
function upbOf(code) { function upbOf(code) {
var hit = calc.results.filter(function (r) { return r.product_code === code; })[0]; var hit = calc.results.filter(function (r) { return r.product_code === code; })[0];
return hit ? (hit.units_per_box || 0) : 0; return hit ? (hit.units_per_box || 0) : 0;
@@ -766,36 +762,6 @@
.map(function (a) { return a.key; }); .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) { function mixItem(key, code) {
return (contents[key] || []).filter(function (it) { return it.product_code === code; })[0]; return (contents[key] || []).filter(function (it) { return it.product_code === code; })[0];
} }
@@ -812,19 +778,48 @@
return put; return put;
} }
// 상자에서 빼기 — 뺀 수량은 그 센터의 미배치 풀로. // 상자에서 덜어내기(내부용) — 호출한 쪽이 곧바로 다른 상자에 담는다.
function mixTake(key, code, qty) { function mixPull(key, code, qty) {
var hit = mixItem(key, code); var hit = mixItem(key, code);
if (!hit) return 0; if (!hit) return 0;
var take = Math.min(hit.quantity, qty); var take = Math.min(hit.quantity, qty);
if (take <= 0) return 0; if (take <= 0) return 0;
var cid = centerOfKey(key);
hit.quantity -= take; hit.quantity -= take;
syncMix(key); syncMix(key);
if (cid) poolAdd(cid, code, hit.product_name, take);
return 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) { function newMixBox(cid, boxName) {
var name = boxName || (calc.mixes[0] && calc.mixes[0].box_name) || MIX_TYPES[0]; var name = boxName || (calc.mixes[0] && calc.mixes[0].box_name) || MIX_TYPES[0];
@@ -983,7 +978,6 @@
esc(labels[k] || k) + (room ? " (여유 " + room + "개)" : " (가득)") + "</option>"; esc(labels[k] || k) + (room ? " (여유 " + room + "개)" : " (가득)") + "</option>";
}); });
opts += '<option value="__new__">+ 새 상자로</option>'; opts += '<option value="__new__">+ 새 상자로</option>';
opts += '<option value="__pool__">미배치로 빼기</option>';
return opts; return opts;
} }
@@ -1000,15 +994,16 @@
(entry.count > 1 ? '<span class="cpg-tree-per">(' + it.quantity + "개 × " + entry.count + "상자)</span>" : "") + (entry.count > 1 ? '<span class="cpg-tree-per">(' + it.quantity + "개 × " + entry.count + "상자)</span>" : "") +
"</li>"; "</li>";
} }
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 '<li><span class="cpg-tree-name">' + esc(it.product_name) + "</span>" + return '<li><span class="cpg-tree-name">' + esc(it.product_name) + "</span>" +
'<span class="cpg-mix-ctl">' + '<span class="cpg-mix-ctl">' +
'<button type="button" class="cpg-mix-step" data-mix-dec data-key="' + esc(entry.key) + '<button type="button" class="cpg-mix-step" data-mix-dec data-key="' + esc(entry.key) +
'" data-code="' + esc(it.product_code) + '" title="1개 기"></button>' + '" data-code="' + esc(it.product_code) + '" title="다른 상자로 1개 옮기기"></button>' +
'<span class="cpg-tree-qty">' + qty + "개</span>" + '<span class="cpg-tree-qty">' + qty + "개</span>" +
'<button type="button" class="cpg-mix-step" data-mix-inc data-key="' + esc(entry.key) + '<button type="button" class="cpg-mix-step" data-mix-inc data-key="' + esc(entry.key) +
'" data-code="' + esc(it.product_code) + '"' + (canAdd ? "" : " disabled") + '" data-code="' + esc(it.product_code) + '"' + (canAdd ? "" : " disabled") +
' title="미배치에서 1개 기">+</button>' + ' title="다른 상자에서 1개 가져오기">+</button>' +
'<select class="erp-select cpg-mix-move" data-key="' + esc(entry.key) + '<select class="erp-select cpg-mix-move" data-key="' + esc(entry.key) +
'" data-code="' + esc(it.product_code) + '" aria-label="이동">' + '" data-code="' + esc(it.product_code) + '" aria-label="이동">' +
moveOptions(entry.key, cid, it.product_code) + moveOptions(entry.key, cid, it.product_code) +
@@ -1017,34 +1012,6 @@
}).join("") + "</ul>"; }).join("") + "</ul>";
} }
// 센터별 미배치 자투리 — 상자에서 뺀 수량이 여기 모인다.
function poolHtml(cid) {
var list = poolOf(cid);
if (!list.length) return "";
var rows = list.map(function (it) {
var opts = '<option value="">담을 상자…</option>';
mixKeysOf(cid).forEach(function (k) {
var room = mixRoomFor(k, it.product_code);
opts += '<option value="' + esc(k) + '"' + (room ? "" : " disabled") + ">" +
esc(labels[k] || k) + (room ? " (여유 " + room + "개)" : " (가득)") + "</option>";
});
opts += '<option value="__new__">+ 새 상자에</option>';
return '<li><span class="cpg-tree-name">' + esc(it.product_name) + "</span>" +
'<span class="cpg-mix-ctl">' +
'<span class="cpg-tree-qty">' + it.quantity + "개</span>" +
'<select class="erp-select cpg-pool-move" data-center="' + esc(cid) +
'" data-code="' + esc(it.product_code) + '" aria-label="담을 상자">' + opts + "</select>" +
"</span></li>";
}).join("");
return '<div class="cpg-pool">' +
'<div class="cpg-pool-head">미배치 자투리 <b>' + poolTotal(cid) + "개</b>" +
'<button type="button" class="erp-btn erp-btn-outline cpg-pool-auto" data-center="' + esc(cid) +
'">자동 담기</button>' +
"</div>" +
'<ul class="cpg-dist-tree">' + rows + "</ul>" +
"</div>";
}
// ── ③ 센터 분배 렌더 ────────────────────────────── // ── ③ 센터 분배 렌더 ──────────────────────────────
function renderDist() { function renderDist() {
if (!distList) return; if (!distList) return;
@@ -1090,7 +1057,6 @@
(rows.length (rows.length
? '<ul class="cpg-dist-items">' + items + "</ul>" ? '<ul class="cpg-dist-items">' + items + "</ul>"
: '<p class="cpg-dist-hint">여기로 상자를 끌어다 놓기</p>') + : '<p class="cpg-dist-hint">여기로 상자를 끌어다 놓기</p>') +
poolHtml(cid) +
(rows.length (rows.length
? '<div class="cpg-dist-foot">' + ? '<div class="cpg-dist-foot">' +
'<button type="button" class="erp-btn erp-btn-outline cpg-mix-new" data-center="' + '<button type="button" class="erp-btn erp-btn-outline cpg-mix-new" data-center="' +
@@ -1292,33 +1258,10 @@
}); });
// ── 혼합 상자 내용물 편집 이벤트 ────────────────── // ── 혼합 상자 내용물 편집 이벤트 ──────────────────
// 수량은 상자 사이에서만 오간다(총량 보존). 갈 곳이 없으면 새 상자를 만든다.
if (distList) { if (distList) {
distList.addEventListener("click", function (e) { distList.addEventListener("click", function (e) {
var dec = e.target.closest("[data-mix-dec]"); // 혼합 상자의 ✕ — 내용물을 다른 상자로 옮기고 상자를 없앤다.
if (dec) {
var moved = mixTake(dec.getAttribute("data-key"), dec.getAttribute("data-code"), 1);
if (moved) { render(); msg.textContent = "1개를 미배치로 뺐습니다."; }
return;
}
var inc = e.target.closest("[data-mix-inc]");
if (inc) {
var key = inc.getAttribute("data-key");
var code = inc.getAttribute("data-code");
var cid = centerOfKey(key);
var got = poolTake(cid, code, Math.min(1, mixRoomFor(key, code)));
if (!got) {
msg.textContent = mixRoomFor(key, code)
? "미배치 자투리에 남은 수량이 없습니다."
: "이 상자에 더 들어갈 자리가 없습니다.";
return;
}
var name = (mixItem(key, code) || {}).product_name || code;
mixPut(key, code, name, got);
render();
msg.textContent = "1개를 담았습니다.";
return;
}
// 혼합 상자의 ✕ — 내용물을 미배치로 옮기고 상자를 없앤다.
var del = e.target.closest(".cpg-dist-del"); var del = e.target.closest(".cpg-dist-del");
if (del) { if (del) {
var li = del.closest(".cpg-dist-item"); var li = del.closest(".cpg-dist-item");
@@ -1330,95 +1273,81 @@
if (entry && entry.key.indexOf("m:") === 0) { if (entry && entry.key.indexOf("m:") === 0) {
e.stopImmediatePropagation(); // 뒤 핸들러가 또 지우지 않게 e.stopImmediatePropagation(); // 뒤 핸들러가 또 지우지 않게
(contents[entry.key] || []).slice().forEach(function (it) { (contents[entry.key] || []).slice().forEach(function (it) {
poolAdd(dcid, it.product_code, it.product_name, it.quantity); var left = it.quantity;
while (left > 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); syncMix(entry.key);
render(); render();
msg.textContent = "혼합 상자를 비우고 미배치로 옮겼습니다."; msg.textContent = "혼합 상자를 비우고 내용물을 다른 상자로 옮겼습니다.";
} }
return; 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"); var neu = e.target.closest(".cpg-mix-new");
if (neu) { if (neu) {
newMixBox(neu.getAttribute("data-center")); newMixBox(neu.getAttribute("data-center"));
render(); render();
msg.textContent = "빈 혼합 상자를 추가했습니다."; 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) { distList.addEventListener("change", function (e) {
var mv = e.target.closest(".cpg-mix-move"); var mv = e.target.closest(".cpg-mix-move");
if (mv) { if (!mv) return;
var fromKey = mv.getAttribute("data-key"); var fromKey = mv.getAttribute("data-key");
var code = mv.getAttribute("data-code"); var code = mv.getAttribute("data-code");
var target = mv.value; var target = mv.value;
mv.value = ""; mv.value = "";
if (!target) return; if (!target) return;
var src = mixItem(fromKey, code); var src = mixItem(fromKey, code);
if (!src) return; if (!src) return;
var qty = src.quantity; var qty = src.quantity;
var name = src.product_name; var name = src.product_name;
var cid = centerOfKey(fromKey); var cid = centerOfKey(fromKey);
if (target === "__pool__") { var toKey = target === "__new__" ? newMixBox(cid) : target;
mixTake(fromKey, code, qty); var moved = moveUnits(fromKey, toKey, code, name, qty);
render(); if (!moved) { msg.textContent = "옮길 자리가 없습니다."; render(); return; }
msg.textContent = name + " " + qty + "개를 미배치로 뺐습니다."; render();
return; msg.textContent = name + " " + moved + "개를 " + (labels[toKey] || "새 상자") + " 로 옮겼습니다." +
} (moved < qty ? " (자리가 부족해 " + (qty - moved) + "개는 남음)" : "");
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 + "개를 담았습니다.";
}
}); });
} }
@@ -1573,9 +1502,6 @@
if (!cids.length) return "센터에 담긴 상자가 없습니다."; if (!cids.length) return "센터에 담긴 상자가 없습니다.";
var missing = cids.filter(function (cid) { return !methods[String(cid)]; }); var missing = cids.filter(function (cid) { return !methods[String(cid)]; });
if (missing.length) return "출고방식 미선택: " + missing.map(centerName).join(", "); if (missing.length) return "출고방식 미선택: " + missing.map(centerName).join(", ");
var leftover = 0;
openCenters.forEach(function (cid) { leftover += poolTotal(cid); });
if (leftover) return "미배치 자투리 " + leftover + "개가 남았습니다.";
return ""; return "";
} }
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901p" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901q" />{% 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=20260901p" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901q" />{% 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=20260901p" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901q" />{% 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=20260901p" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901q" />{% 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=20260901p" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901q" />{% endblock %}
{% block content %} {% block content %}
{# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #} {# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #}
+12 -15
View File
@@ -663,10 +663,14 @@
@media (max-width: 1500px) { @media (max-width: 1500px) {
.cpg-calc3 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } .cpg-calc3 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.cpg-calc3 > .cpg-dist-card { grid-column: 1 / -1; } .cpg-calc3 > .cpg-dist-card { grid-column: 1 / -1; }
/* 3열이 접히면 그리드 행 높이가 auto 라 카드가 계속 늘어난다 →
카드마다 최대 높이를 줘서 내부 목록이 스크롤되게 한다. */
.cpg-calc3 > .erp-card { max-height: min(78vh, 860px); }
} }
@media (max-width: 1000px) { @media (max-width: 1000px) {
.cpg-calc3 { grid-template-columns: minmax(0, 1fr); } .cpg-calc3 { grid-template-columns: minmax(0, 1fr); }
.cpg-calc3 > .cpg-dist-card { grid-column: auto; } .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: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-step:disabled { opacity: .35; cursor: not-allowed; }
.cpg-mix-move, .cpg-pool-move { .cpg-mix-move {
width: auto; min-width: 74px; max-width: 132px; width: auto; min-width: 74px; max-width: 132px;
padding: 1px 4px; font-size: 11px; line-height: 1.5; cursor: pointer; 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-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-dist-foot { padding: 0 10px 10px; }
.cpg-mix-new { padding: 3px 10px; font-size: 12px; line-height: 1.5; } .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;
}