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:
2026-08-29 03:28:12 +09:00
parent e7db2a90fc
commit 47e09b94d7
9 changed files with 208 additions and 53 deletions
+49 -1
View File
@@ -596,7 +596,7 @@
}
/* ② 요약: 카드가 드래그 가능함을 알린다 */
.cpg-sum-prod.is-draggable, .cpg-box-card.is-draggable { cursor: grab; }
.cpg-sum-prod.is-draggable, .cpg-box-card.is-draggable { cursor: pointer; }
.cpg-sum-prod.is-draggable:hover, .cpg-box-card.is-draggable:hover {
border-color: var(--color-rich-black);
}
@@ -695,3 +695,51 @@ 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); }