Revert "feat(cupang): 드래그 조작감 개선 — 핸들·플레이스홀더·오토스크롤"
This reverts commit 47e09b94d7.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829f" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
@@ -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 @@
|
||||
"</svg>";
|
||||
}
|
||||
|
||||
// 드래그는 핸들에서만 시작한다 — 카드 본문 클릭/텍스트 선택과 충돌하지 않게.
|
||||
function handleHtml() {
|
||||
return '<span class="cpg-drag-handle" title="끌어서 센터로 담기" aria-label="드래그 핸들">⠿</span>';
|
||||
}
|
||||
|
||||
function kpi(label, value, hint) {
|
||||
return '<div class="cpg-kpi"><span class="cpg-kpi-label">' + esc(label) + "</span>" +
|
||||
'<strong class="cpg-kpi-value">' + esc(value) + "</strong>" +
|
||||
@@ -347,7 +341,6 @@
|
||||
ph += '<div class="cpg-sum-prod' + (remain ? " is-draggable" : " is-done") + '"' +
|
||||
' data-key="' + esc(key) + '">' +
|
||||
'<div class="cpg-sum-prod-head">' +
|
||||
(remain ? handleHtml() : "") +
|
||||
'<span class="cpg-sum-prod-name">' + esc(r.product_name) + "</span>" +
|
||||
'<span class="erp-badge erp-badge-neutral">' + esc(r.box_name) + "</span>" +
|
||||
"</div>" +
|
||||
@@ -381,8 +374,7 @@
|
||||
' style="animation-delay:' + (n * 60) + 'ms">' +
|
||||
'<div class="cpg-box-art">' + boxSvg(b.fill_percent, m.box_name) + "</div>" +
|
||||
'<div class="cpg-box-info">' +
|
||||
'<div class="cpg-box-title">' + (remain ? handleHtml() : "") +
|
||||
esc(m.box_name) + " 혼합 #" + (i + 1) +
|
||||
'<div class="cpg-box-title">' + esc(m.box_name) + " 혼합 #" + (i + 1) +
|
||||
(remain ? "" : ' <span class="erp-badge erp-badge-success">배분됨</span>') + "</div>" +
|
||||
'<ul class="cpg-box-items">' + items + "</ul>" +
|
||||
'<div class="cpg-box-bar"><span style="width:' + b.fill_percent + '%"></span></div>' +
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829f" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829f" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829f" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829f" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829f" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829f" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
|
||||
+1
-49
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user