fix(cupang): 센터 분배 드래그 실패 지점 제거·센터 목록 가나다 정렬
드래그가 브라우저에서 먹지 않던 문제를 추정 대신 실패 가능 지점별로 전부 막았다. - dragstart 리스너를 카드 요소에 직접 바인딩(위임으로는 잡히지 않는 환경 대비). 렌더할 때마다 다시 바인딩한다. - dragenter/dragover 를 모두 preventDefault — 하나만 막으면 드롭을 거부하는 브라우저가 있다. - 드롭 대상 범위를 ③ 카드 전체로 확대하고, 대상 패널은 closest → elementFromPoint → (센터가 하나면) 그 센터 순으로 찾는다. 패널 사이 여백에 놓아도 담긴다. - dataTransfer.setData/getData 가 막힌 환경 대비로 dragKey 를 따로 보관해 드롭 시 사용한다. 그래도 못 읽으면 클릭으로 담으라는 안내를 띄운다. - 드래그 중에는 ③ 영역에 점선 강조를 준다. - 드래그 직후 발생하는 click 은 무시해 대화상자가 두 번 뜨지 않게 한다. 센터 선택 목록은 이름 가나다순 정렬(한글 음절은 코드포인트 순). tests/js: 여백 드롭·dragenter·setData 차단 환경 시나리오 추가. 전체 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -592,7 +592,8 @@ async def box_calc_page(request: Request) -> HTMLResponse:
|
||||
"page_title": "쿠팡 밀크런 — 박스 계산",
|
||||
"page_subtitle": "제품명과 수량을 넣으면 박스 수와 남은 낱개를 계산합니다.",
|
||||
"box_rules": store.list_box_rules(),
|
||||
"centers": store.list_centers(),
|
||||
# 센터 선택 드롭다운은 가나다순 (한글 음절은 코드포인트 순 = 가나다순)
|
||||
"centers": sorted(store.list_centers(), key=lambda c: (c.get("name") or "")),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829c" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829d" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
@@ -143,6 +143,7 @@
|
||||
var sumBoxes = document.getElementById("cpg-sum-boxes");
|
||||
var sumMixNote = document.getElementById("cpg-sum-mix-note");
|
||||
var distList = document.getElementById("cpg-dist-list");
|
||||
var distCard = document.querySelector(".cpg-dist-card");
|
||||
var distEmpty = document.getElementById("cpg-dist-empty");
|
||||
var centerPick = document.getElementById("cpg-center-pick");
|
||||
var centerAdd = document.getElementById("cpg-center-add");
|
||||
@@ -426,7 +427,7 @@
|
||||
distList.innerHTML = html;
|
||||
}
|
||||
|
||||
function render() { renderSummary(); renderDist(); }
|
||||
function render() { renderSummary(); bindDragSources(); renderDist(); }
|
||||
|
||||
// ── 수량 대화상자 ─────────────────────────────────
|
||||
function openDialog(key, centerId) {
|
||||
@@ -476,46 +477,99 @@
|
||||
});
|
||||
|
||||
// ── 드래그 앤 드롭 (+ 클릭 대체 경로) ─────────────
|
||||
sumBody.addEventListener("dragstart", function (e) {
|
||||
var card = e.target.closest("[data-key]");
|
||||
if (!card || card.getAttribute("draggable") !== "true") return;
|
||||
e.dataTransfer.setData("text/plain", card.getAttribute("data-key"));
|
||||
e.dataTransfer.effectAllowed = "copy";
|
||||
card.classList.add("is-dragging");
|
||||
});
|
||||
sumBody.addEventListener("dragend", function (e) {
|
||||
var card = e.target.closest("[data-key]");
|
||||
if (card) card.classList.remove("is-dragging");
|
||||
});
|
||||
// 브라우저마다 다른 실패 지점을 모두 막는다:
|
||||
// · 리스너를 카드에 직접 건다(위임 시 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]);
|
||||
});
|
||||
|
||||
if (distList) {
|
||||
// 드롭 대상은 센터 패널 전체 — 여백에 놓아도 인식된다.
|
||||
distList.addEventListener("dragover", function (e) {
|
||||
var zone = e.target.closest("[data-drop-for]");
|
||||
if (!zone) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
zone.classList.add("is-over");
|
||||
});
|
||||
distList.addEventListener("dragleave", function (e) {
|
||||
var zone = e.target.closest("[data-drop-for]");
|
||||
if (zone && !zone.contains(e.relatedTarget)) zone.classList.remove("is-over");
|
||||
});
|
||||
distList.addEventListener("drop", function (e) {
|
||||
var zone = e.target.closest("[data-drop-for]");
|
||||
if (!zone) return;
|
||||
e.preventDefault();
|
||||
zone.classList.remove("is-over");
|
||||
var key = e.dataTransfer.getData("text/plain");
|
||||
if (!key) return;
|
||||
openDialog(key, zone.getAttribute("data-drop-for"));
|
||||
function clearOver() {
|
||||
if (!distList) return;
|
||||
Array.prototype.forEach.call(distList.querySelectorAll(".is-over"), function (el) {
|
||||
el.classList.remove("is-over");
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
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 || "";
|
||||
}
|
||||
|
||||
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]);
|
||||
});
|
||||
}
|
||||
|
||||
if (distList) {
|
||||
distList.addEventListener("input", function (e) {
|
||||
if (!e.target.classList.contains("cpg-dist-qty")) return;
|
||||
var li = e.target.closest(".cpg-dist-item");
|
||||
@@ -529,6 +583,7 @@
|
||||
entry.count = want;
|
||||
li.querySelector(".cpg-dist-unit").textContent = "박스 · " + (want * (units[entry.key] || 0)) + "개";
|
||||
renderSummary();
|
||||
bindDragSources();
|
||||
updateCenterTotals(cid);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829c" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829d" />{% 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=20260829c" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829d" />{% 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=20260829c" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829d" />{% 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=20260829c" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829d" />{% 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=20260829c" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829d" />{% 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=20260829c" />{% endblock %}
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260829d" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
|
||||
Reference in New Issue
Block a user