|
|
|
@@ -1,6 +1,6 @@
|
|
|
|
|
{% extends "erp_base.html" %}
|
|
|
|
|
|
|
|
|
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829d" />{% endblock %}
|
|
|
|
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
<section class="cpg">
|
|
|
|
@@ -338,8 +338,7 @@
|
|
|
|
|
var key = prodKey(r.product_code);
|
|
|
|
|
var remain = remainFor(key);
|
|
|
|
|
var used = r.full_boxes - remain;
|
|
|
|
|
ph += '<div class="cpg-sum-prod' + (remain ? "" : " is-done") + '"' +
|
|
|
|
|
(remain ? ' draggable="true"' : "") +
|
|
|
|
|
ph += '<div class="cpg-sum-prod' + (remain ? " is-draggable" : " is-done") + '"' +
|
|
|
|
|
' data-key="' + esc(key) + '">' +
|
|
|
|
|
'<div class="cpg-sum-prod-head">' +
|
|
|
|
|
'<span class="cpg-sum-prod-name">' + esc(r.product_name) + "</span>" +
|
|
|
|
@@ -370,8 +369,7 @@
|
|
|
|
|
var items = b.items.map(function (it) {
|
|
|
|
|
return "<li><span>" + esc(it.product_name) + "</span><b>" + it.quantity + "개</b></li>";
|
|
|
|
|
}).join("");
|
|
|
|
|
bh += '<div class="cpg-box-card' + (remain ? "" : " is-done") + '"' +
|
|
|
|
|
(remain ? ' draggable="true"' : "") +
|
|
|
|
|
bh += '<div class="cpg-box-card' + (remain ? " is-draggable" : " is-done") + '"' +
|
|
|
|
|
' data-key="' + esc(key) + '"' +
|
|
|
|
|
' style="animation-delay:' + (n * 60) + 'ms">' +
|
|
|
|
|
'<div class="cpg-box-art">' + boxSvg(b.fill_percent, m.box_name) + "</div>" +
|
|
|
|
@@ -427,7 +425,7 @@
|
|
|
|
|
distList.innerHTML = html;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function render() { renderSummary(); bindDragSources(); renderDist(); }
|
|
|
|
|
function render() { renderSummary(); renderDist(); }
|
|
|
|
|
|
|
|
|
|
// ── 수량 대화상자 ─────────────────────────────────
|
|
|
|
|
function openDialog(key, centerId) {
|
|
|
|
@@ -476,43 +474,16 @@
|
|
|
|
|
if (e.key === "Escape" && !dlg.hidden) closeDialog();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── 드래그 앤 드롭 (+ 클릭 대체 경로) ─────────────
|
|
|
|
|
// 브라우저마다 다른 실패 지점을 모두 막는다:
|
|
|
|
|
// · 리스너를 카드에 직접 건다(위임 시 dragstart 가 안 잡히는 환경 대비)
|
|
|
|
|
// · dragenter/dragover 둘 다 preventDefault (드롭 허용 신호)
|
|
|
|
|
// · 드롭 대상은 좌표(elementFromPoint)로도 찾고, 못 찾으면 ③ 영역 전체를 대상으로 본다
|
|
|
|
|
// · dataTransfer 를 못 읽는 브라우저 대비로 dragKey 를 따로 보관
|
|
|
|
|
var dragKey = null;
|
|
|
|
|
var dragEndedAt = 0;
|
|
|
|
|
|
|
|
|
|
function bindDragSources() {
|
|
|
|
|
Array.prototype.forEach.call(sumBody.querySelectorAll('[data-key][draggable="true"]'), function (card) {
|
|
|
|
|
card.addEventListener("dragstart", function (e) {
|
|
|
|
|
dragKey = card.getAttribute("data-key");
|
|
|
|
|
try {
|
|
|
|
|
e.dataTransfer.setData("text/plain", dragKey);
|
|
|
|
|
e.dataTransfer.effectAllowed = "copy";
|
|
|
|
|
} catch (err) { /* 일부 브라우저는 setData 를 막는다 — dragKey 로 진행 */ }
|
|
|
|
|
card.classList.add("is-dragging");
|
|
|
|
|
if (distCard) distCard.classList.add("is-drop-ready");
|
|
|
|
|
});
|
|
|
|
|
card.addEventListener("dragend", function () {
|
|
|
|
|
card.classList.remove("is-dragging");
|
|
|
|
|
if (distCard) distCard.classList.remove("is-drop-ready");
|
|
|
|
|
dragEndedAt = Date.now();
|
|
|
|
|
dragKey = null;
|
|
|
|
|
clearOver();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 드래그가 끝난 직후 브라우저가 click 을 한 번 더 쏘는 경우가 있어 무시한다.
|
|
|
|
|
sumBody.addEventListener("click", function (e) {
|
|
|
|
|
if (Date.now() - dragEndedAt < 250) return;
|
|
|
|
|
var card = e.target.closest("[data-key]");
|
|
|
|
|
if (!card) return;
|
|
|
|
|
openDialog(card.getAttribute("data-key"), openCenters[0]);
|
|
|
|
|
});
|
|
|
|
|
// ── 드래그 (마우스 이벤트 기반) ───────────────────
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
function clearOver() {
|
|
|
|
|
if (!distList) return;
|
|
|
|
@@ -521,54 +492,95 @@
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function zoneFromEvent(e) {
|
|
|
|
|
var z = e.target && e.target.closest ? e.target.closest("[data-drop-for]") : null;
|
|
|
|
|
if (z) return z;
|
|
|
|
|
if (document.elementFromPoint && typeof e.clientX === "number") {
|
|
|
|
|
var el = document.elementFromPoint(e.clientX, e.clientY);
|
|
|
|
|
if (el && el.closest) {
|
|
|
|
|
z = el.closest("[data-drop-for]");
|
|
|
|
|
if (z) return z;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// ③ 영역 안이지만 패널 사이 여백에 놓은 경우 — 센터가 하나면 그 센터로.
|
|
|
|
|
if (openCenters.length === 1 && distList) {
|
|
|
|
|
function zoneAt(x, y) {
|
|
|
|
|
if (!distList) return null;
|
|
|
|
|
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 keyFromEvent(e) {
|
|
|
|
|
var key = "";
|
|
|
|
|
try { key = e.dataTransfer.getData("text/plain") || ""; } catch (err) { key = ""; }
|
|
|
|
|
return key || dragKey || "";
|
|
|
|
|
function moveGhost(x, y) {
|
|
|
|
|
if (!drag || !drag.ghost) return;
|
|
|
|
|
drag.ghost.style.left = (x + 12) + "px";
|
|
|
|
|
drag.ghost.style.top = (y + 12) + "px";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (distCard) {
|
|
|
|
|
// ③ 카드 전체에서 드롭을 허용한다(패널 여백·빈 공간 포함).
|
|
|
|
|
["dragenter", "dragover"].forEach(function (type) {
|
|
|
|
|
distCard.addEventListener(type, function (e) {
|
|
|
|
|
if (!dragKey && !(e.dataTransfer && e.dataTransfer.types &&
|
|
|
|
|
Array.prototype.indexOf.call(e.dataTransfer.types, "text/plain") >= 0)) return;
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
|
|
|
var zone = zoneFromEvent(e);
|
|
|
|
|
if (zone) { clearOver(); zone.classList.add("is-over"); }
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
distCard.addEventListener("dragleave", function (e) {
|
|
|
|
|
if (!distCard.contains(e.relatedTarget)) clearOver();
|
|
|
|
|
});
|
|
|
|
|
distCard.addEventListener("drop", function (e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
clearOver();
|
|
|
|
|
var key = keyFromEvent(e);
|
|
|
|
|
if (!key) { msg.textContent = "드래그 정보를 읽지 못했습니다 — 카드를 클릭해 담아 주세요."; return; }
|
|
|
|
|
var zone = zoneFromEvent(e);
|
|
|
|
|
openDialog(key, zone ? zone.getAttribute("data-drop-for") : openCenters[0]);
|
|
|
|
|
});
|
|
|
|
|
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 ? card.offsetWidth + "px" : "240px";
|
|
|
|
|
document.body.appendChild(ghost);
|
|
|
|
|
drag.ghost = ghost;
|
|
|
|
|
drag.moved = true;
|
|
|
|
|
card.classList.add("is-dragging");
|
|
|
|
|
document.body.classList.add("cpg-dragging");
|
|
|
|
|
if (distCard) distCard.classList.add("is-drop-ready");
|
|
|
|
|
moveGhost(x, y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 (drag.card) drag.card.classList.remove("is-dragging");
|
|
|
|
|
document.body.classList.remove("cpg-dragging");
|
|
|
|
|
if (distCard) distCard.classList.remove("is-drop-ready");
|
|
|
|
|
clearOver();
|
|
|
|
|
drag = null;
|
|
|
|
|
if (!moved) return; // 움직이지 않았으면 클릭으로 처리
|
|
|
|
|
// 카드 위에서 손을 뗀 경우에만 뒤따르는 click 을 1회 무시한다.
|
|
|
|
|
suppressClickOn = (card && upTarget && card.contains(upTarget)) ? card : null;
|
|
|
|
|
if (!commit) return;
|
|
|
|
|
var zone = zoneAt(x, y);
|
|
|
|
|
if (!zone) { msg.textContent = "센터 위에 놓아야 담깁니다."; return; }
|
|
|
|
|
openDialog(key, zone.getAttribute("data-drop-for"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 };
|
|
|
|
|
e.preventDefault(); // 텍스트 선택 방지
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener("mousemove", function (e) {
|
|
|
|
|
if (!drag) return;
|
|
|
|
|
if (!drag.moved) {
|
|
|
|
|
if (Math.abs(e.clientX - drag.startX) < DRAG_THRESHOLD &&
|
|
|
|
|
Math.abs(e.clientY - drag.startY) < DRAG_THRESHOLD) return;
|
|
|
|
|
startDrag(drag.card, e.clientX, e.clientY);
|
|
|
|
|
}
|
|
|
|
|
moveGhost(e.clientX, e.clientY);
|
|
|
|
|
var zone = zoneAt(e.clientX, e.clientY);
|
|
|
|
|
clearOver();
|
|
|
|
|
if (zone) zone.classList.add("is-over");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener("mouseup", function (e) {
|
|
|
|
|
if (!drag) return;
|
|
|
|
|
endDrag(true, e.clientX, e.clientY, e.target);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
if (card === suppressClickOn) { suppressClickOn = null; return; }
|
|
|
|
|
openDialog(card.getAttribute("data-key"), openCenters[0]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (distList) {
|
|
|
|
|
distList.addEventListener("input", function (e) {
|
|
|
|
|
if (!e.target.classList.contains("cpg-dist-qty")) return;
|
|
|
|
@@ -583,7 +595,6 @@
|
|
|
|
|
entry.count = want;
|
|
|
|
|
li.querySelector(".cpg-dist-unit").textContent = "박스 · " + (want * (units[entry.key] || 0)) + "개";
|
|
|
|
|
renderSummary();
|
|
|
|
|
bindDragSources();
|
|
|
|
|
updateCenterTotals(cid);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|