From 45807e545da8fb9ad130c4199c3115d75ed38f80 Mon Sep 17 00:00:00 2001 From: king Date: Sat, 29 Aug 2026 03:33:03 +0900 Subject: [PATCH] =?UTF-8?q?Revert=20"feat(cupang):=20=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=20=EC=A1=B0=EC=9E=91=EA=B0=90=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=E2=80=94=20=ED=95=B8=EB=93=A4=C2=B7=ED=94=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=ED=99=80=EB=8D=94=C2=B7=EC=98=A4=ED=86=A0=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 47e09b94d708f5fa974789ac636e77a5f2869eb5. --- .../cupang/templates/cupang/box_calc.html | 131 +++++------------- .../cupang/templates/cupang/box_rules.html | 2 +- .../cupang/templates/cupang/centers.html | 2 +- .../cupang/templates/cupang/detail.html | 2 +- app/modules/cupang/templates/cupang/form.html | 2 +- .../cupang/templates/cupang/index.html | 2 +- .../cupang/templates/cupang/products.html | 2 +- app/static/cupang.css | 50 +------ tests/js/box_calc_ui.test.js | 64 ++------- 9 files changed, 51 insertions(+), 206 deletions(-) diff --git a/app/modules/cupang/templates/cupang/box_calc.html b/app/modules/cupang/templates/cupang/box_calc.html index e29a60e..0d0685e 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 %}
@@ -159,7 +159,6 @@ var el = document.getElementById(id); try { return JSON.parse((el && el.textContent) || "[]"); } catch (e) { return []; } } - var raf = window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : null; var rules = readJson("cpg-calc-rules"); var centers = readJson("cpg-centers"); @@ -299,11 +298,6 @@ ""; } - // 드래그는 핸들에서만 시작한다 — 카드 본문 클릭/텍스트 선택과 충돌하지 않게. - function handleHtml() { - return ''; - } - function kpi(label, value, hint) { return '
' + esc(label) + "" + '' + esc(value) + "" + @@ -347,7 +341,6 @@ ph += '
' + '
' + - (remain ? handleHtml() : "") + '' + esc(r.product_name) + "" + '' + esc(r.box_name) + "" + "
" + @@ -381,8 +374,7 @@ ' style="animation-delay:' + (n * 60) + 'ms">' + '
' + boxSvg(b.fill_percent, m.box_name) + "
" + '
' + - '
' + (remain ? handleHtml() : "") + - esc(m.box_name) + " 혼합 #" + (i + 1) + + '
' + esc(m.box_name) + " 혼합 #" + (i + 1) + (remain ? "" : ' 배분됨') + "
" + '
    ' + items + "
" + '
' + @@ -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";