' + (remain ? handleHtml() : "") +
- esc(m.box_name) + " 혼합 #" + (i + 1) +
+ '
' + esc(m.box_name) + " 혼합 #" + (i + 1) +
(remain ? "" : ' 배분됨') + "
" +
'
" +
'
' +
@@ -482,22 +474,16 @@
if (e.key === "Escape" && !dlg.hidden) closeDialog();
});
- // ── 드래그 엔진 (Pointer Events) ─────────────────
- // HTML5 네이티브 DnD 는 브라우저(웨일 등)에서 시작조차 안 되는 경우가 있어 쓰지 않는다.
- // · 핸들(⠿)에서만 드래그 시작 → 카드 클릭/텍스트 선택과 충돌 없음
- // · setPointerCapture 로 창 밖·다른 요소 위에서도 이벤트를 놓치지 않음
- // · 커서 위치의 센터에 "여기에 담기" 플레이스홀더를 끼워 넣어 놓일 자리를 보여줌
- // · 목록 위/아래 끝에 커서가 오면 자동 스크롤(센터가 많을 때 필수)
- // · 이동 처리는 rAF 로 1프레임 1회만 — 커서를 빨리 흔들어도 렉이 없다
- var drag = null; // {key, card, ghost, startX, startY, moved, pointerId}
- var suppressClickOn = null; // 드래그로 끝난 카드의 잔여 click 1회 무시
- var placeholder = null; // 놓일 자리 표시 요소
- var lastPoint = { x: 0, y: 0 };
- var moveScheduled = false;
- var autoScrollTimer = null;
+ // ── 드래그 (마우스 이벤트 기반) ───────────────────
+ // HTML5 네이티브 DnD(draggable + dragstart)는 브라우저에 따라 드래그가
+ // 아예 시작되지 않는다(웨일에서 미리보기조차 안 생김) → 직접 구현한다.
+ // · mousedown 후 4px 이상 움직이면 드래그 시작, 카드 복제본(고스트)이 커서를 따라온다
+ // · 커서 아래 요소를 elementFromPoint 로 찾아 센터 패널을 강조
+ // · mouseup 위치의 센터에 놓으면 수량 대화상자가 열린다
+ // · 드래그 없이 클릭하면 첫 번째 센터를 기본값으로 대화상자가 열린다
+ var drag = null; // {key, card, ghost, startX, startY, moved}
+ var suppressClickOn = null; // 드래그로 끝난 카드의 잔여 click 1회 무시
var DRAG_THRESHOLD = 4;
- var EDGE = 44; // 오토스크롤이 걸리는 가장자리 두께(px)
- var EDGE_SPEED = 14; // 1틱 스크롤량(px)
function clearOver() {
if (!distList) return;
@@ -511,53 +497,24 @@
var el = document.elementFromPoint ? document.elementFromPoint(x, y) : null;
var zone = el && el.closest ? el.closest("[data-drop-for]") : null;
if (zone) return zone;
- // ③ 영역 안(패널 사이 여백 포함)이고 센터가 하나뿐이면 그 센터로.
+ // ③ 영역 안(패널 사이 여백 포함)에서 놓았고 센터가 하나뿐이면 그 센터로.
if (el && el.closest && el.closest(".cpg-dist-card") && openCenters.length === 1) {
return distList.querySelector("[data-drop-for]");
}
return null;
}
- function showPlaceholder(zone) {
- if (!drag) return;
- if (!placeholder) {
- placeholder = document.createElement("div");
- placeholder.className = "cpg-drop-placeholder";
- }
- placeholder.textContent = (labels[drag.key] || drag.key) + " — 여기에 담기";
- var list = zone.querySelector(".cpg-dist-items");
- (list || zone).appendChild(placeholder);
- }
-
- function hidePlaceholder() {
- if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder);
- }
-
- function autoScrollTick() {
- if (!drag || !distList || !distList.getBoundingClientRect) return;
- var box = distList.getBoundingClientRect();
- if (!box.height) return;
- var y = lastPoint.y;
- if (y > box.top && y < box.top + EDGE) distList.scrollTop -= EDGE_SPEED;
- else if (y < box.bottom && y > box.bottom - EDGE) distList.scrollTop += EDGE_SPEED;
- }
-
function moveGhost(x, y) {
if (!drag || !drag.ghost) return;
- drag.ghost.style.left = (x + 14) + "px";
- drag.ghost.style.top = (y + 14) + "px";
+ drag.ghost.style.left = (x + 12) + "px";
+ drag.ghost.style.top = (y + 12) + "px";
}
- function startDrag(x, y) {
- var card = drag.card;
+ function startDrag(card, x, y) {
var ghost = card.cloneNode(true);
ghost.className = card.className + " cpg-drag-ghost";
ghost.removeAttribute("style");
- ghost.style.width = (card.offsetWidth || 260) + "px";
- var badge = document.createElement("span");
- badge.className = "cpg-drag-badge";
- badge.textContent = "잔여 " + remainFor(drag.key) + "박스";
- ghost.appendChild(badge);
+ ghost.style.width = card.offsetWidth ? card.offsetWidth + "px" : "240px";
document.body.appendChild(ghost);
drag.ghost = ghost;
drag.moved = true;
@@ -565,34 +522,15 @@
document.body.classList.add("cpg-dragging");
if (distCard) distCard.classList.add("is-drop-ready");
moveGhost(x, y);
- if (!autoScrollTimer) autoScrollTimer = setInterval(autoScrollTick, 40);
- }
-
- function handleMove() {
- moveScheduled = false;
- if (!drag) return;
- moveGhost(lastPoint.x, lastPoint.y);
- var zone = zoneAt(lastPoint.x, lastPoint.y);
- clearOver();
- hidePlaceholder();
- if (zone) {
- zone.classList.add("is-over");
- showPlaceholder(zone);
- if (drag.ghost) drag.ghost.classList.remove("is-invalid");
- } else if (drag.ghost) {
- drag.ghost.classList.add("is-invalid");
- }
}
function endDrag(commit, x, y, upTarget) {
if (!drag) return;
var key = drag.key, moved = drag.moved, card = drag.card;
if (drag.ghost && drag.ghost.parentNode) drag.ghost.parentNode.removeChild(drag.ghost);
- if (card) card.classList.remove("is-dragging");
+ if (drag.card) drag.card.classList.remove("is-dragging");
document.body.classList.remove("cpg-dragging");
if (distCard) distCard.classList.remove("is-drop-ready");
- if (autoScrollTimer) { clearInterval(autoScrollTimer); autoScrollTimer = null; }
- hidePlaceholder();
clearOver();
drag = null;
if (!moved) return; // 움직이지 않았으면 클릭으로 처리
@@ -604,45 +542,38 @@
openDialog(key, zone.getAttribute("data-drop-for"));
}
- sumBody.addEventListener("pointerdown", function (e) {
- if (e.pointerType === "mouse" && e.button !== 0) return;
- var handle = e.target.closest(".cpg-drag-handle");
- if (!handle) return;
- var card = handle.closest("[data-key]");
+ sumBody.addEventListener("mousedown", function (e) {
+ if (e.button !== 0) return;
+ var card = e.target.closest("[data-key]");
if (!card || card.classList.contains("is-done")) return;
- drag = { key: card.getAttribute("data-key"), card: card, startX: e.clientX, startY: e.clientY,
- moved: false, ghost: null, pointerId: e.pointerId };
- lastPoint = { x: e.clientX, y: e.clientY };
- if (handle.setPointerCapture && e.pointerId != null) {
- try { handle.setPointerCapture(e.pointerId); } catch (err) { /* 지원 안 하면 무시 */ }
- }
- e.preventDefault(); // 텍스트 선택·스크롤 제스처 방지
+ drag = { key: card.getAttribute("data-key"), card: card, startX: e.clientX, startY: e.clientY, moved: false, ghost: null };
+ e.preventDefault(); // 텍스트 선택 방지
});
- document.addEventListener("pointermove", function (e) {
+ document.addEventListener("mousemove", function (e) {
if (!drag) return;
- lastPoint = { x: e.clientX, y: e.clientY };
if (!drag.moved) {
if (Math.abs(e.clientX - drag.startX) < DRAG_THRESHOLD &&
Math.abs(e.clientY - drag.startY) < DRAG_THRESHOLD) return;
- startDrag(e.clientX, e.clientY);
+ startDrag(drag.card, e.clientX, e.clientY);
}
- if (moveScheduled) return;
- moveScheduled = true;
- if (raf) raf(handleMove); else handleMove();
+ moveGhost(e.clientX, e.clientY);
+ var zone = zoneAt(e.clientX, e.clientY);
+ clearOver();
+ if (zone) zone.classList.add("is-over");
});
- document.addEventListener("pointerup", function (e) {
+ document.addEventListener("mouseup", function (e) {
if (!drag) return;
endDrag(true, e.clientX, e.clientY, e.target);
});
- document.addEventListener("pointercancel", function () { endDrag(false, 0, 0, null); });
- window.addEventListener("blur", function () { if (drag) endDrag(false, 0, 0, null); });
+
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && drag) endDrag(false, 0, 0, null);
});
// 드래그하지 않고 클릭한 경우 — 첫 센터를 기본값으로 대화상자를 연다.
+ // (드래그를 끝낸 카드에서 브라우저가 한 번 더 쏘는 click 은 1회만 무시)
sumBody.addEventListener("click", function (e) {
var card = e.target.closest("[data-key]");
if (!card) return;
diff --git a/app/modules/cupang/templates/cupang/box_rules.html b/app/modules/cupang/templates/cupang/box_rules.html
index 5419e65..09574fc 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 169b874..8a88e83 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 4ae6f47..a5ce896 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 5d6e0de..6e96960 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 9ecdada..38505d2 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 bb08a54..e4bf8e6 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 dde2f54..dea8490 100644
--- a/app/static/cupang.css
+++ b/app/static/cupang.css
@@ -596,7 +596,7 @@
}
/* ② 요약: 카드가 드래그 가능함을 알린다 */
-.cpg-sum-prod.is-draggable, .cpg-box-card.is-draggable { cursor: pointer; }
+.cpg-sum-prod.is-draggable, .cpg-box-card.is-draggable { cursor: grab; }
.cpg-sum-prod.is-draggable:hover, .cpg-box-card.is-draggable:hover {
border-color: var(--color-rich-black);
}
@@ -695,51 +695,3 @@ 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-drag-handle {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- margin-right: 4px;
- border-radius: 5px;
- color: var(--color-midtone-gray);
- cursor: grab;
- touch-action: none; /* 모바일에서 스크롤 제스처와 충돌 방지 */
- user-select: none;
- flex: 0 0 auto;
-}
-.cpg-drag-handle:hover { background: var(--color-ghost-gray); color: var(--color-rich-black); }
-body.cpg-dragging .cpg-drag-handle { cursor: grabbing; }
-
-.cpg-drop-placeholder {
- margin-top: 6px;
- padding: 8px;
- border: 1.5px dashed var(--color-rich-black);
- border-radius: 8px;
- font-size: 12px;
- color: var(--color-rich-black);
- background: var(--color-ghost-gray);
- animation: cpg-ph-in .15s ease-out;
-}
-@keyframes cpg-ph-in {
- from { opacity: 0; transform: translateY(-4px); }
- to { opacity: 1; transform: none; }
-}
-
-.cpg-drag-badge {
- position: absolute;
- top: -10px;
- right: -10px;
- padding: 2px 8px;
- border-radius: 999px;
- background: var(--color-rich-black);
- color: var(--color-canvas-white);
- font-size: 11px;
- font-weight: 700;
- white-space: nowrap;
-}
-.cpg-drag-ghost.is-invalid { border-color: var(--color-callout-red); }
-.cpg-drag-ghost.is-invalid .cpg-drag-badge { background: var(--color-callout-red); }
diff --git a/tests/js/box_calc_ui.test.js b/tests/js/box_calc_ui.test.js
index 5eb7a77..cd8d922 100644
--- a/tests/js/box_calc_ui.test.js
+++ b/tests/js/box_calc_ui.test.js
@@ -35,24 +35,20 @@ const $ = (s) => d.querySelector(s);
const $$ = (s) => Array.from(d.querySelectorAll(s));
const text = (s) => ($(s) ? $(s).textContent.replace(/\s+/g, " ").trim() : "");
const fire = (el, type) => el.dispatchEvent(new w.Event(type, { bubbles: true }));
-// 이동 처리는 requestAnimationFrame 으로 1프레임 지연되므로 한 프레임 기다린다.
-const frame = () => new Promise(r => setTimeout(r, 30));
-// 포인터 드래그 시뮬레이션.
+// 마우스 드래그 시뮬레이션.
// jsdom 은 레이아웃이 없어 elementFromPoint 가 항상 null 이므로 드롭 대상을 스텁한다.
-function pointer(el, type, x, y) {
- el.dispatchEvent(new w.PointerEvent(type, {
- bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0, pointerId: 1, pointerType: "mouse",
+function mouse(el, type, x, y) {
+ el.dispatchEvent(new w.MouseEvent(type, {
+ bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0,
}));
}
-function handleOf(card) { return card.querySelector(".cpg-drag-handle"); }
-
function dragCardTo(card, target) {
d.elementFromPoint = () => target;
- pointer(handleOf(card), "pointerdown", 10, 10);
- pointer(d, "pointermove", 60, 60); // 임계값(4px) 초과 → 드래그 시작
- pointer(d, "pointermove", 200, 200);
- pointer(d, "pointerup", 200, 200);
+ mouse(card, "mousedown", 10, 10);
+ mouse(d, "mousemove", 60, 60); // 임계값(4px) 초과 → 드래그 시작
+ mouse(d, "mousemove", 200, 200);
+ mouse(d, "mouseup", 200, 200);
d.elementFromPoint = () => null;
}
@@ -90,39 +86,15 @@ function dragCardTo(card, target) {
// ── 마우스 드래그 → 드롭 → 대화상자 ───────────────
const card = $(".cpg-sum-prod.is-draggable");
- check("드래그 핸들이 있다", !!handleOf(card));
d.elementFromPoint = () => $(".cpg-dist-center");
-
- // 핸들이 아닌 카드 본문에서는 드래그가 시작되지 않는다
- pointer(card.querySelector(".cpg-sum-prod-name"), "pointerdown", 10, 10);
- pointer(d, "pointermove", 60, 60);
- check("핸들 밖에서는 드래그 안 됨", !card.classList.contains("is-dragging") && !$(".cpg-drag-ghost"));
-
- pointer(handleOf(card), "pointerdown", 10, 10);
- pointer(d, "pointermove", 60, 60);
- await frame();
- check("핸들에서 4px 이상 움직이면 드래그 시작", card.classList.contains("is-dragging"));
+ mouse(card, "mousedown", 10, 10);
+ mouse(d, "mousemove", 60, 60);
+ check("4px 이상 움직이면 드래그 시작", card.classList.contains("is-dragging"));
check("고스트(복제 카드) 생성", !!$(".cpg-drag-ghost"));
- check("고스트에 잔여 배지", text(".cpg-drag-badge") === "잔여 2박스", text(".cpg-drag-badge"));
check("드래그 중 대상 센터 강조", $(".cpg-dist-center").classList.contains("is-over"));
- check("놓일 자리 플레이스홀더 표시", !!$(".cpg-drop-placeholder"));
- check("플레이스홀더에 대상 이름", text(".cpg-drop-placeholder").includes("미라네 1호 세트"),
- text(".cpg-drop-placeholder"));
-
- // 대상 밖으로 나가면 금지 표시
- d.elementFromPoint = () => $(".cpg-calc-card");
- pointer(d, "pointermove", 400, 400);
- await frame();
- check("대상 밖이면 고스트가 금지 표시", $(".cpg-drag-ghost").classList.contains("is-invalid"));
- check("대상 밖이면 플레이스홀더 제거", !$(".cpg-drop-placeholder"));
-
- d.elementFromPoint = () => $(".cpg-dist-center");
- pointer(d, "pointermove", 200, 200);
- await frame();
- pointer(d, "pointerup", 200, 200);
+ mouse(d, "mouseup", 200, 200);
d.elementFromPoint = () => null;
check("드롭 후 고스트 제거", !$(".cpg-drag-ghost"));
- check("드롭 후 플레이스홀더 제거", !$(".cpg-drop-placeholder"));
check("드롭하면 대화상자가 열린다", $("#cpg-dlg").hidden === false);
check("대화상자에 잔여/개수 표시", text("#cpg-dlg-item").includes("잔여 2박스 (16개)"), text("#cpg-dlg-item"));
check("대화상자 센터 = 드롭한 센터", $("#cpg-dlg-center").value === "1");
@@ -197,17 +169,7 @@ function dragCardTo(card, target) {
check("③ 밖에 놓으면 담기지 않는다", $("#cpg-dlg").hidden === true && $$(".cpg-dist-item").length === 0);
check("③ 밖 드롭 안내 메시지", text("#cpg-calc-msg").includes("센터 위에 놓아야"), text("#cpg-calc-msg"));
- // (3) Esc 로 드래그 취소
- d.elementFromPoint = () => $(".cpg-dist-center");
- const card4 = $(".cpg-sum-prod.is-draggable");
- pointer(handleOf(card4), "pointerdown", 10, 10);
- pointer(d, "pointermove", 80, 80);
- d.dispatchEvent(new w.KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
- check("Esc 로 드래그 취소", !$(".cpg-drag-ghost") && $("#cpg-dlg").hidden === true);
- pointer(d, "pointerup", 200, 200);
- d.elementFromPoint = () => null;
-
- // (4) 드래그 없이 클릭한 경우
+ // (3) 드래그 없이 클릭한 경우
$(".cpg-sum-prod.is-draggable").click();
check("클릭 대체 경로 동작", $("#cpg-dlg").hidden === false);
$("#cpg-dlg-qty").value = "1";