fix(cupang): 혼합 상자 이동은 용량 제한 없이, ③ 목록 max-height 로 스크롤 보장

- mixPut/moveUnits 에서 용량 제한 제거. 이동 목록도 "가득"이어도 선택 가능
  (채움률이 100% 를 넘으면 빨간색으로 표시만)
- 부모 flex 높이 계산에 기대던 스크롤을 max-height 로 직접 지정
  (③ 72vh, ① 표 64vh, ② 목록 64vh)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 16:42:51 +09:00
parent 58ebf1c232
commit da6ae2cd13
7 changed files with 33 additions and 30 deletions
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901q" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901r" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -766,10 +766,9 @@
return (contents[key] || []).filter(function (it) { return it.product_code === code; })[0]; return (contents[key] || []).filter(function (it) { return it.product_code === code; })[0];
} }
// 상자에 담기 — 용량만큼만 들어간다. 담은 수량을 돌려준다. // 상자에 담기 — 용량은 표시용일 뿐, 이동은 막지 않는다(넘치면 100% 초과 표시).
function mixPut(key, code, name, qty) { function mixPut(key, code, name, qty) {
var room = mixRoomFor(key, code); var put = qty;
var put = Math.min(room, qty);
if (put <= 0) return 0; if (put <= 0) return 0;
var hit = mixItem(key, code); var hit = mixItem(key, code);
if (hit) { hit.quantity += put; } if (hit) { hit.quantity += put; }
@@ -809,12 +808,9 @@
return found; return found;
} }
// 상자 → 상자 이동. 실제로 옮긴 수량을 돌려준다(자리만큼만). // 상자 → 상자 이동. 용량과 무관하게 요청한 수량을 옮긴다.
function moveUnits(fromKey, toKey, code, name, qty) { function moveUnits(fromKey, toKey, code, name, qty) {
var room = mixRoomFor(toKey, code); var pulled = mixPull(fromKey, code, qty);
var move = Math.min(qty, room);
if (move <= 0) return 0;
var pulled = mixPull(fromKey, code, move);
if (!pulled) return 0; if (!pulled) return 0;
mixPut(toKey, code, name, pulled); mixPut(toKey, code, name, pulled);
return pulled; return pulled;
@@ -974,7 +970,7 @@
mixKeysOf(cid).forEach(function (k) { mixKeysOf(cid).forEach(function (k) {
if (k === fromKey) return; if (k === fromKey) return;
var room = mixRoomFor(k, code); var room = mixRoomFor(k, code);
opts += '<option value="' + esc(k) + '"' + (room ? "" : " disabled") + ">" + opts += '<option value="' + esc(k) + '">' +
esc(labels[k] || k) + (room ? " (여유 " + room + "개)" : " (가득)") + "</option>"; esc(labels[k] || k) + (room ? " (여유 " + room + "개)" : " (가득)") + "</option>";
}); });
opts += '<option value="__new__">+ 새 상자로</option>'; opts += '<option value="__new__">+ 새 상자로</option>';
@@ -994,8 +990,7 @@
(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 = !!boxHaving(cid, it.product_code, entry.key);
!!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) +
@@ -1026,11 +1021,13 @@
boxes += a.count; boxes += a.count;
pieces += a.count * per; pieces += a.count * per;
var isMix = a.key.indexOf("m:") === 0; var isMix = a.key.indexOf("m:") === 0;
var fillTxt = isMix ? Math.round(mixFill(a.key) * 100) + "% 채움" : ""; var fillPct = isMix ? Math.round(mixFill(a.key) * 100) : 0;
var fillTxt = isMix ? fillPct + "% 채움" : "";
var fillCls = fillPct > 100 ? " is-over" : "";
items += '<li class="cpg-dist-item' + (isMix ? " is-mix" : "") + '" data-entry="' + a.id + '">' + items += '<li class="cpg-dist-item' + (isMix ? " is-mix" : "") + '" data-entry="' + a.id + '">' +
'<div class="cpg-dist-line">' + '<div class="cpg-dist-line">' +
'<span class="cpg-dist-name">' + esc(labels[a.key] || a.key) + '<span class="cpg-dist-name">' + esc(labels[a.key] || a.key) +
(fillTxt ? ' <em class="cpg-mix-fill">' + fillTxt + "</em>" : "") + "</span>" + (fillTxt ? ' <em class="cpg-mix-fill' + fillCls + '">' + fillTxt + "</em>" : "") + "</span>" +
'<input class="erp-input cpg-dist-qty" type="number" min="1" max="999" step="1" inputmode="numeric" value="' + a.count + '" />' + '<input class="erp-input cpg-dist-qty" type="number" min="1" max="999" step="1" inputmode="numeric" value="' + a.count + '" />' +
'<span class="cpg-dist-unit">상자 · ' + (a.count * per) + "개</span>" + '<span class="cpg-dist-unit">상자 · ' + (a.count * per) + "개</span>" +
'<button type="button" class="erp-btn erp-btn-danger cpg-dist-del" title="빼기" aria-label="빼기">✕</button>' + '<button type="button" class="erp-btn erp-btn-danger cpg-dist-del" title="빼기" aria-label="빼기">✕</button>' +
@@ -1273,13 +1270,9 @@
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) {
var left = it.quantity; var rest = mixKeysOf(dcid).filter(function (k) { return k !== entry.key; });
while (left > 0) { var dest = boxWithRoom(dcid, it.product_code, entry.key) || rest[0] || newMixBox(dcid);
var dest = boxWithRoom(dcid, it.product_code, entry.key) || newMixBox(dcid); moveUnits(entry.key, dest, it.product_code, it.product_name, it.quantity);
var moved = moveUnits(entry.key, dest, it.product_code, it.product_name, left);
if (!moved) break;
left -= moved;
}
}); });
syncMix(entry.key); syncMix(entry.key);
render(); render();
@@ -1295,7 +1288,8 @@
var cid = centerOfKey(key); var cid = centerOfKey(key);
var item = mixItem(key, code); var item = mixItem(key, code);
if (!item) return; if (!item) return;
var to = boxWithRoom(cid, code, key) || newMixBox(cid); var others = mixKeysOf(cid).filter(function (k) { return k !== key; });
var to = boxWithRoom(cid, code, key) || others[0] || newMixBox(cid);
var n = moveUnits(key, to, code, item.product_name, 1); var n = moveUnits(key, to, code, item.product_name, 1);
if (!n) { msg.textContent = "옮길 자리가 없습니다."; return; } if (!n) { msg.textContent = "옮길 자리가 없습니다."; return; }
render(); render();
@@ -1308,9 +1302,6 @@
var ikey = inc.getAttribute("data-key"); var ikey = inc.getAttribute("data-key");
var icode = inc.getAttribute("data-code"); var icode = inc.getAttribute("data-code");
var icid = centerOfKey(ikey); var icid = centerOfKey(ikey);
if (mixRoomFor(ikey, icode) <= 0) {
msg.textContent = "이 상자에 더 들어갈 자리가 없습니다."; return;
}
var from = boxHaving(icid, icode, ikey); var from = boxHaving(icid, icode, ikey);
if (!from) { msg.textContent = "다른 상자에 이 제품이 없습니다."; return; } if (!from) { msg.textContent = "다른 상자에 이 제품이 없습니다."; return; }
var src = mixItem(from, icode); var src = mixItem(from, icode);
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901q" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901r" />{% 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=20260901q" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901r" />{% 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=20260901q" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901r" />{% 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=20260901q" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901r" />{% 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=20260901q" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901r" />{% endblock %}
{% block content %} {% block content %}
{# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #} {# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #}
+12
View File
@@ -1453,3 +1453,15 @@ body.cpg-dragging { cursor: grabbing; user-select: none; }
flex: 1 1 auto; min-height: 0; flex: 1 1 auto; min-height: 0;
overflow-y: auto; overflow-x: hidden; overflow-y: auto; overflow-x: hidden;
} }
/* ③ 목록 스크롤 — 부모 높이 계산에 기대지 않도록 최대 높이를 직접 준다 */
.cpg-dist-card .cpg-dist-list {
max-height: min(72vh, 820px);
overflow-y: auto;
overflow-x: hidden;
}
/* ①② 도 같은 방식으로 안전장치 */
.cpg-calc-card .erp-table-wrap { max-height: min(64vh, 720px); }
.cpg-calc-sum .cpg-sum-scroll { max-height: min(64vh, 720px); }
/* 용량 초과 표시 */
.cpg-mix-fill.is-over { color: var(--color-callout-red); font-weight: 600; }