fix(cupang): 센터 분배를 마우스 기반 드래그로 교체 (웨일 대응)
웨일에서 카드를 눌러 움직여도 드래그 미리보기조차 생기지 않았다 = HTML5 네이티브 DnD 가 시작 자체를 못 한 것. 브라우저 구현에 기대는 draggable/dragstart 를 걷어내고 마우스 이벤트로 직접 구현했다. - mousedown 후 4px 이상 이동하면 드래그 시작, 카드 복제본(고스트)이 커서를 따라온다. 커서 아래 센터 패널은 elementFromPoint 로 찾아 강조. - mouseup 위치의 센터에 놓으면 수량 대화상자가 열린다. ③ 카드 여백에 놓아도 센터가 하나면 그 센터로, ③ 밖이면 안내만 띄우고 담지 않는다. - Esc 로 드래그 취소. 카드 위에서 손을 뗀 경우에만 뒤따르는 click 을 1회 무시해, 드래그 후에도 클릭 경로가 막히지 않는다. tests/js: 드래그 시작 임계값·고스트 생성/제거·대상 강조·여백 드롭· ③ 밖 드롭·클릭 대체 경로까지 마우스 이벤트로 검증. 전체 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% 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 %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
@@ -338,8 +338,7 @@
|
|||||||
var key = prodKey(r.product_code);
|
var key = prodKey(r.product_code);
|
||||||
var remain = remainFor(key);
|
var remain = remainFor(key);
|
||||||
var used = r.full_boxes - remain;
|
var used = r.full_boxes - remain;
|
||||||
ph += '<div class="cpg-sum-prod' + (remain ? "" : " is-done") + '"' +
|
ph += '<div class="cpg-sum-prod' + (remain ? " is-draggable" : " is-done") + '"' +
|
||||||
(remain ? ' draggable="true"' : "") +
|
|
||||||
' data-key="' + esc(key) + '">' +
|
' data-key="' + esc(key) + '">' +
|
||||||
'<div class="cpg-sum-prod-head">' +
|
'<div class="cpg-sum-prod-head">' +
|
||||||
'<span class="cpg-sum-prod-name">' + esc(r.product_name) + "</span>" +
|
'<span class="cpg-sum-prod-name">' + esc(r.product_name) + "</span>" +
|
||||||
@@ -370,8 +369,7 @@
|
|||||||
var items = b.items.map(function (it) {
|
var items = b.items.map(function (it) {
|
||||||
return "<li><span>" + esc(it.product_name) + "</span><b>" + it.quantity + "개</b></li>";
|
return "<li><span>" + esc(it.product_name) + "</span><b>" + it.quantity + "개</b></li>";
|
||||||
}).join("");
|
}).join("");
|
||||||
bh += '<div class="cpg-box-card' + (remain ? "" : " is-done") + '"' +
|
bh += '<div class="cpg-box-card' + (remain ? " is-draggable" : " is-done") + '"' +
|
||||||
(remain ? ' draggable="true"' : "") +
|
|
||||||
' data-key="' + esc(key) + '"' +
|
' data-key="' + esc(key) + '"' +
|
||||||
' style="animation-delay:' + (n * 60) + 'ms">' +
|
' style="animation-delay:' + (n * 60) + 'ms">' +
|
||||||
'<div class="cpg-box-art">' + boxSvg(b.fill_percent, m.box_name) + "</div>" +
|
'<div class="cpg-box-art">' + boxSvg(b.fill_percent, m.box_name) + "</div>" +
|
||||||
@@ -427,7 +425,7 @@
|
|||||||
distList.innerHTML = html;
|
distList.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() { renderSummary(); bindDragSources(); renderDist(); }
|
function render() { renderSummary(); renderDist(); }
|
||||||
|
|
||||||
// ── 수량 대화상자 ─────────────────────────────────
|
// ── 수량 대화상자 ─────────────────────────────────
|
||||||
function openDialog(key, centerId) {
|
function openDialog(key, centerId) {
|
||||||
@@ -476,43 +474,16 @@
|
|||||||
if (e.key === "Escape" && !dlg.hidden) closeDialog();
|
if (e.key === "Escape" && !dlg.hidden) closeDialog();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 드래그 앤 드롭 (+ 클릭 대체 경로) ─────────────
|
// ── 드래그 (마우스 이벤트 기반) ───────────────────
|
||||||
// 브라우저마다 다른 실패 지점을 모두 막는다:
|
// HTML5 네이티브 DnD(draggable + dragstart)는 브라우저에 따라 드래그가
|
||||||
// · 리스너를 카드에 직접 건다(위임 시 dragstart 가 안 잡히는 환경 대비)
|
// 아예 시작되지 않는다(웨일에서 미리보기조차 안 생김) → 직접 구현한다.
|
||||||
// · dragenter/dragover 둘 다 preventDefault (드롭 허용 신호)
|
// · mousedown 후 4px 이상 움직이면 드래그 시작, 카드 복제본(고스트)이 커서를 따라온다
|
||||||
// · 드롭 대상은 좌표(elementFromPoint)로도 찾고, 못 찾으면 ③ 영역 전체를 대상으로 본다
|
// · 커서 아래 요소를 elementFromPoint 로 찾아 센터 패널을 강조
|
||||||
// · dataTransfer 를 못 읽는 브라우저 대비로 dragKey 를 따로 보관
|
// · mouseup 위치의 센터에 놓으면 수량 대화상자가 열린다
|
||||||
var dragKey = null;
|
// · 드래그 없이 클릭하면 첫 번째 센터를 기본값으로 대화상자가 열린다
|
||||||
var dragEndedAt = 0;
|
var drag = null; // {key, card, ghost, startX, startY, moved}
|
||||||
|
var suppressClickOn = null; // 드래그로 끝난 카드의 잔여 click 1회 무시
|
||||||
function bindDragSources() {
|
var DRAG_THRESHOLD = 4;
|
||||||
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]);
|
|
||||||
});
|
|
||||||
|
|
||||||
function clearOver() {
|
function clearOver() {
|
||||||
if (!distList) return;
|
if (!distList) return;
|
||||||
@@ -521,54 +492,95 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function zoneFromEvent(e) {
|
function zoneAt(x, y) {
|
||||||
var z = e.target && e.target.closest ? e.target.closest("[data-drop-for]") : null;
|
if (!distList) return null;
|
||||||
if (z) return z;
|
var el = document.elementFromPoint ? document.elementFromPoint(x, y) : null;
|
||||||
if (document.elementFromPoint && typeof e.clientX === "number") {
|
var zone = el && el.closest ? el.closest("[data-drop-for]") : null;
|
||||||
var el = document.elementFromPoint(e.clientX, e.clientY);
|
if (zone) return zone;
|
||||||
if (el && el.closest) {
|
// ③ 영역 안(패널 사이 여백 포함)에서 놓았고 센터가 하나뿐이면 그 센터로.
|
||||||
z = el.closest("[data-drop-for]");
|
if (el && el.closest && el.closest(".cpg-dist-card") && openCenters.length === 1) {
|
||||||
if (z) return z;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ③ 영역 안이지만 패널 사이 여백에 놓은 경우 — 센터가 하나면 그 센터로.
|
|
||||||
if (openCenters.length === 1 && distList) {
|
|
||||||
return distList.querySelector("[data-drop-for]");
|
return distList.querySelector("[data-drop-for]");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyFromEvent(e) {
|
function moveGhost(x, y) {
|
||||||
var key = "";
|
if (!drag || !drag.ghost) return;
|
||||||
try { key = e.dataTransfer.getData("text/plain") || ""; } catch (err) { key = ""; }
|
drag.ghost.style.left = (x + 12) + "px";
|
||||||
return key || dragKey || "";
|
drag.ghost.style.top = (y + 12) + "px";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distCard) {
|
function startDrag(card, x, y) {
|
||||||
// ③ 카드 전체에서 드롭을 허용한다(패널 여백·빈 공간 포함).
|
var ghost = card.cloneNode(true);
|
||||||
["dragenter", "dragover"].forEach(function (type) {
|
ghost.className = card.className + " cpg-drag-ghost";
|
||||||
distCard.addEventListener(type, function (e) {
|
ghost.removeAttribute("style");
|
||||||
if (!dragKey && !(e.dataTransfer && e.dataTransfer.types &&
|
ghost.style.width = card.offsetWidth ? card.offsetWidth + "px" : "240px";
|
||||||
Array.prototype.indexOf.call(e.dataTransfer.types, "text/plain") >= 0)) return;
|
document.body.appendChild(ghost);
|
||||||
e.preventDefault();
|
drag.ghost = ghost;
|
||||||
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
drag.moved = true;
|
||||||
var zone = zoneFromEvent(e);
|
card.classList.add("is-dragging");
|
||||||
if (zone) { clearOver(); zone.classList.add("is-over"); }
|
document.body.classList.add("cpg-dragging");
|
||||||
});
|
if (distCard) distCard.classList.add("is-drop-ready");
|
||||||
});
|
moveGhost(x, y);
|
||||||
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 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) {
|
if (distList) {
|
||||||
distList.addEventListener("input", function (e) {
|
distList.addEventListener("input", function (e) {
|
||||||
if (!e.target.classList.contains("cpg-dist-qty")) return;
|
if (!e.target.classList.contains("cpg-dist-qty")) return;
|
||||||
@@ -583,7 +595,6 @@
|
|||||||
entry.count = want;
|
entry.count = want;
|
||||||
li.querySelector(".cpg-dist-unit").textContent = "박스 · " + (want * (units[entry.key] || 0)) + "개";
|
li.querySelector(".cpg-dist-unit").textContent = "박스 · " + (want * (units[entry.key] || 0)) + "개";
|
||||||
renderSummary();
|
renderSummary();
|
||||||
bindDragSources();
|
|
||||||
updateCenterTotals(cid);
|
updateCenterTotals(cid);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% 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 %}
|
{% 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=20260829d" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% 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=20260829d" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% 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=20260829d" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% 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=20260829d" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% 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=20260829d" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829e" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
+19
-3
@@ -596,13 +596,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ② 요약: 카드가 드래그 가능함을 알린다 */
|
/* ② 요약: 카드가 드래그 가능함을 알린다 */
|
||||||
.cpg-sum-prod[draggable="true"], .cpg-box-card[draggable="true"] { cursor: grab; }
|
.cpg-sum-prod.is-draggable, .cpg-box-card.is-draggable { cursor: grab; }
|
||||||
.cpg-sum-prod[draggable="true"]:hover, .cpg-box-card[draggable="true"]:hover {
|
.cpg-sum-prod.is-draggable:hover, .cpg-box-card.is-draggable:hover {
|
||||||
border-color: var(--color-rich-black);
|
border-color: var(--color-rich-black);
|
||||||
}
|
}
|
||||||
.cpg-sum-prod.is-dragging, .cpg-box-card.is-dragging { opacity: .45; }
|
.cpg-sum-prod.is-dragging, .cpg-box-card.is-dragging { opacity: .35; }
|
||||||
.cpg-sum-prod.is-done, .cpg-box-card.is-done { opacity: .5; }
|
.cpg-sum-prod.is-done, .cpg-box-card.is-done { opacity: .5; }
|
||||||
|
|
||||||
|
/* 커서를 따라다니는 복제 카드 */
|
||||||
|
body.cpg-dragging { cursor: grabbing; user-select: none; }
|
||||||
|
.cpg-drag-ghost {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1300;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: .9;
|
||||||
|
max-width: 320px;
|
||||||
|
background: var(--color-canvas-white);
|
||||||
|
border: 1px solid var(--color-rich-black);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 12px 28px rgba(0, 0, 0, .25);
|
||||||
|
transform: rotate(-1.5deg);
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ③ 센터 분배 */
|
/* ③ 센터 분배 */
|
||||||
.cpg-dist-list {
|
.cpg-dist-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
// 박스 계산 화면(cupang/box_calc.html) UI 테스트 — jsdom.
|
||||||
|
// 실행 전 `python tests/js/render_box_calc.py` 로 템플릿을 렌더해 둔다.
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
const { JSDOM } = require("jsdom");
|
const { JSDOM } = require("jsdom");
|
||||||
|
|
||||||
const path = require("path");
|
|
||||||
const RENDER = path.join(__dirname, ".out", "box_calc_render.html");
|
const RENDER = path.join(__dirname, ".out", "box_calc_render.html");
|
||||||
const html = fs.readFileSync(RENDER, "utf8"); // 먼저 render_box_calc.py 실행
|
const html = fs.readFileSync(RENDER, "utf8");
|
||||||
|
|
||||||
// 서버 응답 고정: 미라네 1호 18개(2박스+자투리2), 미라네 4호 12개(1박스+자투리4)
|
// 서버 응답 고정: 미라네 1호 18개(2박스+자투리2), 미라네 4호 12개(1박스+자투리4)
|
||||||
const apiResponse = {
|
const apiResponse = {
|
||||||
@@ -32,8 +34,23 @@ function check(name, cond, extra) {
|
|||||||
const $ = (s) => d.querySelector(s);
|
const $ = (s) => d.querySelector(s);
|
||||||
const $$ = (s) => Array.from(d.querySelectorAll(s));
|
const $$ = (s) => Array.from(d.querySelectorAll(s));
|
||||||
const text = (s) => ($(s) ? $(s).textContent.replace(/\s+/g, " ").trim() : "<none>");
|
const text = (s) => ($(s) ? $(s).textContent.replace(/\s+/g, " ").trim() : "<none>");
|
||||||
|
const fire = (el, type) => el.dispatchEvent(new w.Event(type, { bubbles: true }));
|
||||||
|
|
||||||
function fire(el, type, init) { el.dispatchEvent(new w.Event(type, Object.assign({bubbles:true}, init))); }
|
// 마우스 드래그 시뮬레이션.
|
||||||
|
// jsdom 은 레이아웃이 없어 elementFromPoint 가 항상 null 이므로 드롭 대상을 스텁한다.
|
||||||
|
function mouse(el, type, x, y) {
|
||||||
|
el.dispatchEvent(new w.MouseEvent(type, {
|
||||||
|
bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
function dragCardTo(card, target) {
|
||||||
|
d.elementFromPoint = () => target;
|
||||||
|
mouse(card, "mousedown", 10, 10);
|
||||||
|
mouse(d, "mousemove", 60, 60); // 임계값(4px) 초과 → 드래그 시작
|
||||||
|
mouse(d, "mousemove", 200, 200);
|
||||||
|
mouse(d, "mouseup", 200, 200);
|
||||||
|
d.elementFromPoint = () => null;
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
// ── ① 입력 → 계산 ──────────────────────────────────
|
// ── ① 입력 → 계산 ──────────────────────────────────
|
||||||
@@ -41,7 +58,6 @@ function fire(el, type, init) { el.dispatchEvent(new w.Event(type, Object.assign
|
|||||||
row1.querySelector(".cpg-calc-name").value = "MS-1001";
|
row1.querySelector(".cpg-calc-name").value = "MS-1001";
|
||||||
row1.querySelector(".cpg-calc-qty").value = "18";
|
row1.querySelector(".cpg-calc-qty").value = "18";
|
||||||
|
|
||||||
// Tab 으로 다음 행 생성 + 포커스 이동
|
|
||||||
const ke = new w.KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
|
const ke = new w.KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
|
||||||
row1.querySelector(".cpg-calc-qty").dispatchEvent(ke);
|
row1.querySelector(".cpg-calc-qty").dispatchEvent(ke);
|
||||||
check("Tab 이 새 행을 만든다", $$(".cpg-calc-row").length === 2, "행수=" + $$(".cpg-calc-row").length);
|
check("Tab 이 새 행을 만든다", $$(".cpg-calc-row").length === 2, "행수=" + $$(".cpg-calc-row").length);
|
||||||
@@ -57,7 +73,8 @@ function fire(el, type, init) { el.dispatchEvent(new w.Event(type, Object.assign
|
|||||||
check("요약이 표시된다", $("#cpg-sum-body").hidden === false);
|
check("요약이 표시된다", $("#cpg-sum-body").hidden === false);
|
||||||
check("제품 카드 2장", $$(".cpg-sum-prod").length === 2, String($$(".cpg-sum-prod").length));
|
check("제품 카드 2장", $$(".cpg-sum-prod").length === 2, String($$(".cpg-sum-prod").length));
|
||||||
check("혼합 박스 1장", $$(".cpg-box-card").length === 1);
|
check("혼합 박스 1장", $$(".cpg-box-card").length === 1);
|
||||||
check("제품별 잔여 = 2박스(16개)", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("2박스16개"), text(".cpg-sum-prod .cpg-sum-prod-nums"));
|
check("제품별 잔여 = 2박스(16개)", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("2박스16개"),
|
||||||
|
text(".cpg-sum-prod .cpg-sum-prod-nums"));
|
||||||
check("KPI 총 박스 4", text(".cpg-kpi .cpg-kpi-value") === "4박스", text(".cpg-kpi .cpg-kpi-value"));
|
check("KPI 총 박스 4", text(".cpg-kpi .cpg-kpi-value") === "4박스", text(".cpg-kpi .cpg-kpi-value"));
|
||||||
|
|
||||||
// ── ③ 센터는 추가한 것만 표시 ─────────────────────
|
// ── ③ 센터는 추가한 것만 표시 ─────────────────────
|
||||||
@@ -67,20 +84,17 @@ function fire(el, type, init) { el.dispatchEvent(new w.Event(type, Object.assign
|
|||||||
check("센터 추가 후 1개 표시", $$(".cpg-dist-center").length === 1, String($$(".cpg-dist-center").length));
|
check("센터 추가 후 1개 표시", $$(".cpg-dist-center").length === 1, String($$(".cpg-dist-center").length));
|
||||||
check("추가한 센터 이름", text(".cpg-dist-center strong") === "대구3", text(".cpg-dist-center strong"));
|
check("추가한 센터 이름", text(".cpg-dist-center strong") === "대구3", text(".cpg-dist-center strong"));
|
||||||
|
|
||||||
// ── 드래그 → 드롭 → 대화상자 ──────────────────────
|
// ── 마우스 드래그 → 드롭 → 대화상자 ───────────────
|
||||||
const card = $(".cpg-sum-prod[draggable='true']");
|
const card = $(".cpg-sum-prod.is-draggable");
|
||||||
const dt = { data: {}, setData(t, v) { this.data[t] = v; }, getData(t) { return this.data[t]; } };
|
d.elementFromPoint = () => $(".cpg-dist-center");
|
||||||
const ds = new w.Event("dragstart", { bubbles: true }); ds.dataTransfer = dt;
|
mouse(card, "mousedown", 10, 10);
|
||||||
card.dispatchEvent(ds);
|
mouse(d, "mousemove", 60, 60);
|
||||||
check("dragstart 가 key 를 싣는다", dt.getData("text/plain") === "p:MS-1001", dt.getData("text/plain"));
|
check("4px 이상 움직이면 드래그 시작", card.classList.contains("is-dragging"));
|
||||||
|
check("고스트(복제 카드) 생성", !!$(".cpg-drag-ghost"));
|
||||||
const zone = $(".cpg-dist-center");
|
check("드래그 중 대상 센터 강조", $(".cpg-dist-center").classList.contains("is-over"));
|
||||||
const dov = new w.Event("dragover", { bubbles: true, cancelable: true }); dov.dataTransfer = dt;
|
mouse(d, "mouseup", 200, 200);
|
||||||
zone.dispatchEvent(dov);
|
d.elementFromPoint = () => null;
|
||||||
check("dragover 가 기본동작을 막는다(드롭 허용)", dov.defaultPrevented);
|
check("드롭 후 고스트 제거", !$(".cpg-drag-ghost"));
|
||||||
|
|
||||||
const drop = new w.Event("drop", { bubbles: true, cancelable: true }); drop.dataTransfer = dt;
|
|
||||||
zone.dispatchEvent(drop);
|
|
||||||
check("드롭하면 대화상자가 열린다", $("#cpg-dlg").hidden === false);
|
check("드롭하면 대화상자가 열린다", $("#cpg-dlg").hidden === false);
|
||||||
check("대화상자에 잔여/개수 표시", text("#cpg-dlg-item").includes("잔여 2박스 (16개)"), text("#cpg-dlg-item"));
|
check("대화상자에 잔여/개수 표시", text("#cpg-dlg-item").includes("잔여 2박스 (16개)"), text("#cpg-dlg-item"));
|
||||||
check("대화상자 센터 = 드롭한 센터", $("#cpg-dlg-center").value === "1");
|
check("대화상자 센터 = 드롭한 센터", $("#cpg-dlg-center").value === "1");
|
||||||
@@ -90,35 +104,37 @@ function fire(el, type, init) { el.dispatchEvent(new w.Event(type, Object.assign
|
|||||||
$("#cpg-dlg-ok").click();
|
$("#cpg-dlg-ok").click();
|
||||||
check("대화상자가 닫힌다", $("#cpg-dlg").hidden === true);
|
check("대화상자가 닫힌다", $("#cpg-dlg").hidden === true);
|
||||||
check("센터에 항목 1줄", $$(".cpg-dist-item").length === 1);
|
check("센터에 항목 1줄", $$(".cpg-dist-item").length === 1);
|
||||||
check("센터 항목에 박스·개수 표시", text(".cpg-dist-item .cpg-dist-unit") === "박스 · 8개", text(".cpg-dist-item .cpg-dist-unit"));
|
check("센터 항목에 박스·개수 표시", text(".cpg-dist-item .cpg-dist-unit") === "박스 · 8개",
|
||||||
check("센터 합계 배지 1박스/8개", text(".cpg-dist-sum").startsWith("1박스 8개✕") || text(".cpg-dist-sum").startsWith("1박스8개"), text(".cpg-dist-sum"));
|
text(".cpg-dist-item .cpg-dist-unit"));
|
||||||
check("요약 잔여가 1박스로 줄어든다", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("1박스8개"), text(".cpg-sum-prod .cpg-sum-prod-nums"));
|
check("센터 합계 배지 1박스/8개", text(".cpg-dist-sum").replace(/\s/g, "").startsWith("1박스8개"),
|
||||||
|
text(".cpg-dist-sum"));
|
||||||
|
check("요약 잔여가 1박스로 줄어든다", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("1박스8개"),
|
||||||
|
text(".cpg-sum-prod .cpg-sum-prod-nums"));
|
||||||
check("KPI 센터 배분 1박스", $$(".cpg-kpi-value")[1].textContent === "1박스", $$(".cpg-kpi-value")[1].textContent);
|
check("KPI 센터 배분 1박스", $$(".cpg-kpi-value")[1].textContent === "1박스", $$(".cpg-kpi-value")[1].textContent);
|
||||||
|
|
||||||
// ── 잔여 전부 담기 ────────────────────────────────
|
// ── 클릭 대체 경로 + 잔여 전부 담기 ───────────────
|
||||||
$$(".cpg-sum-prod")[0].click(); // 클릭 대체 경로
|
$$(".cpg-sum-prod")[0].click();
|
||||||
check("클릭으로도 대화상자가 열린다", $("#cpg-dlg").hidden === false);
|
check("클릭으로도 대화상자가 열린다", $("#cpg-dlg").hidden === false);
|
||||||
$("#cpg-dlg-all").click();
|
$("#cpg-dlg-all").click();
|
||||||
check("전량 담기 후 잔여 0", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("0박스0개"), text(".cpg-sum-prod .cpg-sum-prod-nums"));
|
check("전량 담기 후 잔여 0", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("0박스0개"),
|
||||||
check("잔여 0 카드는 드래그 불가", $$(".cpg-sum-prod")[0].getAttribute("draggable") === null);
|
text(".cpg-sum-prod .cpg-sum-prod-nums"));
|
||||||
check("센터 합계 2박스/16개", text(".cpg-dist-sum").replace(/\s/g,"").startsWith("2박스16개"), text(".cpg-dist-sum"));
|
check("잔여 0 카드는 드래그 불가", $$(".cpg-sum-prod")[0].classList.contains("is-done"));
|
||||||
|
check("센터 합계 2박스/16개", text(".cpg-dist-sum").replace(/\s/g, "").startsWith("2박스16개"),
|
||||||
|
text(".cpg-dist-sum"));
|
||||||
|
|
||||||
// ── 수량 직접 수정 (상한 = 잔여+자기수량) ─────────
|
// ── 수량 직접 수정 (상한 = 잔여 + 자기 수량) ──────
|
||||||
const qty = $(".cpg-dist-item .cpg-dist-qty");
|
const qty = $(".cpg-dist-item .cpg-dist-qty");
|
||||||
qty.value = "5"; fire(qty, "input");
|
qty.value = "5"; fire(qty, "input");
|
||||||
check("잔여 초과 입력은 최대치로 잘림", qty.value === "2", qty.value);
|
check("잔여 초과 입력은 최대치로 잘림", qty.value === "2", qty.value);
|
||||||
|
|
||||||
qty.value = "1"; fire(qty, "input");
|
qty.value = "1"; fire(qty, "input");
|
||||||
check("수량 줄이면 개수 표시 갱신", text(".cpg-dist-item .cpg-dist-unit") === "박스 · 8개", text(".cpg-dist-item .cpg-dist-unit"));
|
check("수량 줄이면 개수 표시 갱신", text(".cpg-dist-item .cpg-dist-unit") === "박스 · 8개",
|
||||||
check("수량 줄이면 요약 잔여 복귀", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("1박스8개"), text(".cpg-sum-prod .cpg-sum-prod-nums"));
|
text(".cpg-dist-item .cpg-dist-unit"));
|
||||||
|
check("수량 줄이면 요약 잔여 복귀", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("1박스8개"),
|
||||||
|
text(".cpg-sum-prod .cpg-sum-prod-nums"));
|
||||||
|
|
||||||
// ── 혼합 박스 담기 ────────────────────────────────
|
// ── 혼합 박스 담기 ────────────────────────────────
|
||||||
const mix = $(".cpg-box-card[draggable='true']");
|
dragCardTo($(".cpg-box-card.is-draggable"), $(".cpg-dist-center"));
|
||||||
const dt2 = { data:{}, setData(t,v){this.data[t]=v;}, getData(t){return this.data[t];} };
|
|
||||||
const ds2 = new w.Event("dragstart", { bubbles: true }); ds2.dataTransfer = dt2;
|
|
||||||
mix.dispatchEvent(ds2);
|
|
||||||
const drop2 = new w.Event("drop", { bubbles: true, cancelable: true }); drop2.dataTransfer = dt2;
|
|
||||||
$(".cpg-dist-center").dispatchEvent(drop2);
|
|
||||||
check("혼합 박스도 대화상자가 열린다", $("#cpg-dlg").hidden === false);
|
check("혼합 박스도 대화상자가 열린다", $("#cpg-dlg").hidden === false);
|
||||||
check("혼합 박스 잔여 1박스(6개)", text("#cpg-dlg-item").includes("잔여 1박스 (6개)"), text("#cpg-dlg-item"));
|
check("혼합 박스 잔여 1박스(6개)", text("#cpg-dlg-item").includes("잔여 1박스 (6개)"), text("#cpg-dlg-item"));
|
||||||
$("#cpg-dlg-ok").click();
|
$("#cpg-dlg-ok").click();
|
||||||
@@ -132,38 +148,33 @@ function fire(el, type, init) { el.dispatchEvent(new w.Event(type, Object.assign
|
|||||||
|
|
||||||
$(".cpg-dist-close").click();
|
$(".cpg-dist-close").click();
|
||||||
check("센터 빼기", $$(".cpg-dist-center").length === 0);
|
check("센터 빼기", $$(".cpg-dist-center").length === 0);
|
||||||
check("센터 빼면 전량 잔여 복귀", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("2박스16개"), text(".cpg-sum-prod .cpg-sum-prod-nums"));
|
check("센터 빼면 전량 잔여 복귀", text(".cpg-sum-prod .cpg-sum-prod-nums").startsWith("2박스16개"),
|
||||||
|
text(".cpg-sum-prod .cpg-sum-prod-nums"));
|
||||||
|
|
||||||
// ── 드롭 실패 경로 방어 확인 ──────────────────────
|
// ── 드롭 실패 경로 방어 ───────────────────────────
|
||||||
$("#cpg-center-pick").value = "2";
|
$("#cpg-center-pick").value = "2";
|
||||||
$("#cpg-center-add").click();
|
$("#cpg-center-add").click();
|
||||||
check("센터 다시 추가", $$(".cpg-dist-center").length === 1, String($$(".cpg-dist-center").length));
|
check("센터 다시 추가", $$(".cpg-dist-center").length === 1, String($$(".cpg-dist-center").length));
|
||||||
|
|
||||||
// (1) 패널이 아닌 여백(리스트 자체)에 놓아도, 센터가 하나면 그 센터로 담긴다
|
// (1) 패널이 아닌 ③ 카드 여백에 놓아도 센터가 하나면 그 센터로
|
||||||
const card2 = $(".cpg-sum-prod[draggable='true']");
|
dragCardTo($(".cpg-sum-prod.is-draggable"), $(".cpg-dist-card"));
|
||||||
const dt3 = { data: {}, setData(t, v) { this.data[t] = v; }, getData(t) { return this.data[t] || ""; }, types: ["text/plain"] };
|
|
||||||
const ds3 = new w.Event("dragstart", { bubbles: true }); ds3.dataTransfer = dt3;
|
|
||||||
card2.dispatchEvent(ds3);
|
|
||||||
const den = new w.Event("dragenter", { bubbles: true, cancelable: true }); den.dataTransfer = dt3;
|
|
||||||
$(".cpg-dist-card").dispatchEvent(den);
|
|
||||||
check("dragenter 도 기본동작을 막는다", den.defaultPrevented);
|
|
||||||
const drop3 = new w.Event("drop", { bubbles: true, cancelable: true }); drop3.dataTransfer = dt3;
|
|
||||||
$("#cpg-dist-list").dispatchEvent(drop3);
|
|
||||||
check("여백에 놓아도 대화상자가 열린다", $("#cpg-dlg").hidden === false);
|
check("여백에 놓아도 대화상자가 열린다", $("#cpg-dlg").hidden === false);
|
||||||
|
$("#cpg-dlg-qty").value = "1";
|
||||||
$("#cpg-dlg-ok").click();
|
$("#cpg-dlg-ok").click();
|
||||||
check("여백 드롭도 센터에 담긴다", $$(".cpg-dist-item").length === 1, String($$(".cpg-dist-item").length));
|
check("여백 드롭도 센터에 담긴다", $$(".cpg-dist-item").length === 1, String($$(".cpg-dist-item").length));
|
||||||
|
|
||||||
// (2) setData 를 막는 브라우저: dataTransfer 가 비어도 dragKey 로 담긴다
|
// (2) ③ 밖에 놓으면 담기지 않고 안내만 뜬다
|
||||||
$(".cpg-dist-del").click();
|
$(".cpg-dist-del").click();
|
||||||
const card3 = $(".cpg-sum-prod[draggable='true']");
|
dragCardTo($(".cpg-sum-prod.is-draggable"), $(".cpg-calc-card"));
|
||||||
const dtBlocked = { setData() { throw new Error("blocked"); }, getData() { return ""; }, types: [] };
|
check("③ 밖에 놓으면 담기지 않는다", $("#cpg-dlg").hidden === true && $$(".cpg-dist-item").length === 0);
|
||||||
const ds4 = new w.Event("dragstart", { bubbles: true }); ds4.dataTransfer = dtBlocked;
|
check("③ 밖 드롭 안내 메시지", text("#cpg-calc-msg").includes("센터 위에 놓아야"), text("#cpg-calc-msg"));
|
||||||
card3.dispatchEvent(ds4);
|
|
||||||
const drop4 = new w.Event("drop", { bubbles: true, cancelable: true }); drop4.dataTransfer = dtBlocked;
|
// (3) 드래그 없이 클릭한 경우
|
||||||
$(".cpg-dist-center").dispatchEvent(drop4);
|
$(".cpg-sum-prod.is-draggable").click();
|
||||||
check("setData 가 막혀도 드롭이 동작한다", $("#cpg-dlg").hidden === false, text("#cpg-calc-msg"));
|
check("클릭 대체 경로 동작", $("#cpg-dlg").hidden === false);
|
||||||
|
$("#cpg-dlg-qty").value = "1";
|
||||||
$("#cpg-dlg-ok").click();
|
$("#cpg-dlg-ok").click();
|
||||||
check("setData 차단 환경에서도 담긴다", $$(".cpg-dist-item").length === 1);
|
check("클릭으로 담긴다", $$(".cpg-dist-item").length === 1);
|
||||||
|
|
||||||
console.log("\n" + (fails.length ? "FAILED: " + fails.join(" | ") : "ALL PASS"));
|
console.log("\n" + (fails.length ? "FAILED: " + fails.join(" | ") : "ALL PASS"));
|
||||||
process.exit(fails.length ? 1 : 0);
|
process.exit(fails.length ? 1 : 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user