feat(cupang): 드래그 조작감 개선 — 핸들·플레이스홀더·오토스크롤
Pointer Events 기반으로 바꾸고(터치·펜 지원, setPointerCapture 로 창 밖 이동에도 이벤트 유실 없음) 업무툴 수준의 조작 피드백을 붙였다. - 드래그 핸들(⠿)에서만 드래그 시작 → 카드 클릭/텍스트 선택과 충돌 없음 - 커서를 따라오는 고스트에 "잔여 N박스" 배지, 담을 수 없는 위치에서는 빨간 테두리로 금지 표시 - 대상 센터에 "○○ — 여기에 담기" 플레이스홀더를 끼워 놓일 자리를 보여줌 - 센터 목록 위/아래 44px 안에 커서가 오면 자동 스크롤(센터가 많을 때 필수) - pointermove 처리는 requestAnimationFrame 으로 1프레임 1회만 실행 - pointercancel / 창 blur / Esc 에서 드래그를 안전하게 취소 SortableJS 도입은 보류했다. 웨일에서 네이티브 DnD 가 시작조차 되지 않아 어차피 fallback 모드로만 동작하고, 브라우저 실측 검증 수단이 없는 상태에서 의존성만 늘어난다. tests/js: 핸들 밖 드래그 차단·고스트 배지·플레이스홀더·금지 표시·Esc 취소 시나리오 추가(rAF 지연을 고려해 프레임 대기). 전체 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,20 +35,24 @@ const $ = (s) => d.querySelector(s);
|
||||
const $$ = (s) => Array.from(d.querySelectorAll(s));
|
||||
const text = (s) => ($(s) ? $(s).textContent.replace(/\s+/g, " ").trim() : "<none>");
|
||||
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 mouse(el, type, x, y) {
|
||||
el.dispatchEvent(new w.MouseEvent(type, {
|
||||
bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0,
|
||||
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 handleOf(card) { return card.querySelector(".cpg-drag-handle"); }
|
||||
|
||||
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);
|
||||
pointer(handleOf(card), "pointerdown", 10, 10);
|
||||
pointer(d, "pointermove", 60, 60); // 임계값(4px) 초과 → 드래그 시작
|
||||
pointer(d, "pointermove", 200, 200);
|
||||
pointer(d, "pointerup", 200, 200);
|
||||
d.elementFromPoint = () => null;
|
||||
}
|
||||
|
||||
@@ -86,15 +90,39 @@ function dragCardTo(card, target) {
|
||||
|
||||
// ── 마우스 드래그 → 드롭 → 대화상자 ───────────────
|
||||
const card = $(".cpg-sum-prod.is-draggable");
|
||||
check("드래그 핸들이 있다", !!handleOf(card));
|
||||
d.elementFromPoint = () => $(".cpg-dist-center");
|
||||
mouse(card, "mousedown", 10, 10);
|
||||
mouse(d, "mousemove", 60, 60);
|
||||
check("4px 이상 움직이면 드래그 시작", card.classList.contains("is-dragging"));
|
||||
|
||||
// 핸들이 아닌 카드 본문에서는 드래그가 시작되지 않는다
|
||||
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"));
|
||||
check("고스트(복제 카드) 생성", !!$(".cpg-drag-ghost"));
|
||||
check("고스트에 잔여 배지", text(".cpg-drag-badge") === "잔여 2박스", text(".cpg-drag-badge"));
|
||||
check("드래그 중 대상 센터 강조", $(".cpg-dist-center").classList.contains("is-over"));
|
||||
mouse(d, "mouseup", 200, 200);
|
||||
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);
|
||||
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");
|
||||
@@ -169,7 +197,17 @@ 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) 드래그 없이 클릭한 경우
|
||||
// (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) 드래그 없이 클릭한 경우
|
||||
$(".cpg-sum-prod.is-draggable").click();
|
||||
check("클릭 대체 경로 동작", $("#cpg-dlg").hidden === false);
|
||||
$("#cpg-dlg-qty").value = "1";
|
||||
|
||||
Reference in New Issue
Block a user