diff --git a/src/renderer/handlers/drag-drop.js b/src/renderer/handlers/drag-drop.js index f465cbc..bdcc36f 100644 --- a/src/renderer/handlers/drag-drop.js +++ b/src/renderer/handlers/drag-drop.js @@ -1,5 +1,7 @@ let _grid, _gridContainer, _dragGhostEl, _fileExplorerEl; let _state, _cb, _onFolderDrop; +let _placeholder = null; +let _dropTargetIdx = null; let dragMode = null; let dragSrcIdx = null; @@ -18,7 +20,18 @@ export function init({ grid, gridContainer, dragGhostEl, state, callbacks, fileE _fileExplorerEl = fileExplorerEl; _onFolderDrop = onFolderDrop; + _placeholder = document.createElement('div'); + _placeholder.className = 'card card-placeholder'; + const phLabel = document.createElement('div'); + phLabel.className = 'card-placeholder-label'; + _placeholder.appendChild(phLabel); + _placeholder.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }); + _placeholder.addEventListener('dragenter', e => e.preventDefault()); + _placeholder.addEventListener('drop', e => { e.preventDefault(); _performDrop(); }); + window.addEventListener('dragover', _onWindowDragOver); + grid.addEventListener('dragover', _onGridDragOver); + grid.addEventListener('drop', _onGridDrop); if (fileExplorerEl) { fileExplorerEl.addEventListener('dragover', _onExplorerDragOver); @@ -27,7 +40,6 @@ export function init({ grid, gridContainer, dragGhostEl, state, callbacks, fileE } } -// 카드 생성 시 호출해 드래그 이벤트를 부착 export function setupCard(card) { card.addEventListener('dragstart', _onDragStart); card.addEventListener('dragover', _onDragOver); @@ -53,20 +65,37 @@ function _onDragStart(e) { if (_state.selectedIdxs.has(idx) && _state.selectedIdxs.size > 1) { dragMode = 'multi'; dragSrcIdx = idx; - _state.selectedIdxs.forEach(i => { - const c = _getCardEl(i); - if (c) c.classList.add('multi-dragging'); - }); - _showGhost(e, `📦 ${_state.selectedIdxs.size}개 이동 중`); + + const count = _state.selectedIdxs.size; + const toHide = [..._state.selectedIdxs].sort((a, b) => a - b); + _placeholder.querySelector('.card-placeholder-label').textContent = `${count}개`; + const img = new Image(); img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; e.dataTransfer.setDragImage(img, 0, 0); + _showGhost(e, `📦 ${count}개 이동 중`); + + // 브라우저가 드래그 이미지를 캡처한 뒤 카드 숨기고 플레이스홀더 삽입 + requestAnimationFrame(() => { + const firstCard = _getCardEl(toHide[0]); + if (firstCard) _grid.insertBefore(_placeholder, firstCard); + toHide.forEach(i => { + const c = _getCardEl(i); + if (c) c.classList.add('drag-hidden'); + }); + }); } else { dragMode = 'single'; dragSrcIdx = idx; - this.classList.add('dragging'); + _placeholder.querySelector('.card-placeholder-label').textContent = '1개'; _state.selectedIdxs.clear(); _cb.refreshCardSelection(); + + const srcCard = this; + requestAnimationFrame(() => { + _grid.insertBefore(_placeholder, srcCard); + srcCard.classList.add('drag-hidden'); + }); } e.dataTransfer.effectAllowed = 'move'; @@ -80,26 +109,40 @@ function _onDragOver(e) { function _onDragEnter(e) { e.preventDefault(); - const idx = parseInt(this.dataset.idx); + const idx = parseInt(this.dataset.idx); const isSrc = dragMode === 'single' ? idx === dragSrcIdx : _state.selectedIdxs.has(idx); - if (!isSrc) this.classList.add('drag-over'); + if (isSrc) return; + + _dropTargetIdx = idx; + + // 플레이스홀더를 삽입될 위치로 이동 + if (dragMode === 'single' && dragSrcIdx < idx) { + _grid.insertBefore(_placeholder, this.nextSibling); // 오른쪽으로 이동: 타겟 뒤 + } else { + _grid.insertBefore(_placeholder, this); // 왼쪽 이동 또는 multi: 타겟 앞 + } } function _onDragLeave() { - this.classList.remove('drag-over'); + // 플레이스홀더가 위치를 표시하므로 별도 처리 없음 } function _onDrop(e) { e.preventDefault(); - this.classList.remove('drag-over'); - const targetIdx = parseInt(this.dataset.idx); + _dropTargetIdx = parseInt(this.dataset.idx); + _performDrop(); +} +function _performDrop() { + if (_dropTargetIdx === null) return; if (dragMode === 'single') { - if (dragSrcIdx === null || dragSrcIdx === targetIdx) return; - _moveSingle(dragSrcIdx, targetIdx); + if (dragSrcIdx !== null && dragSrcIdx !== _dropTargetIdx) { + _moveSingle(dragSrcIdx, _dropTargetIdx); + } } else if (dragMode === 'multi') { - if (_state.selectedIdxs.has(targetIdx)) return; - _moveMulti(targetIdx); + if (!_state.selectedIdxs.has(_dropTargetIdx)) { + _moveMulti(_dropTargetIdx); + } } } @@ -107,8 +150,11 @@ function _onDragEnd() { _stopScroll(); _hideGhost(); _clearTreeHover(); + // 플레이스홀더 제거 및 숨긴 카드 복원 (_moveSingle/_moveMulti 에서 이미 처리했으면 no-op) + if (_placeholder.parentNode) _placeholder.parentNode.removeChild(_placeholder); + _dropTargetIdx = null; document.querySelectorAll('.card').forEach(c => - c.classList.remove('dragging', 'multi-dragging', 'drag-over') + c.classList.remove('drag-hidden', 'drag-over') ); dragMode = null; dragSrcIdx = null; @@ -171,22 +217,53 @@ function _clearTreeHover() { } } +// ── 그리드 빈 공간 드래그 핸들러 ───────────────────── + +function _onGridDragOver(e) { + if (!dragMode) return; + if (e.target.closest('.card')) return; + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + + _dropTargetIdx = _state.files.length; + if (_grid.lastElementChild !== _placeholder) { + _grid.appendChild(_placeholder); + } +} + +function _onGridDrop(e) { + if (!dragMode) return; + if (e.target.closest('.card')) return; + e.preventDefault(); + _dropTargetIdx = _state.files.length; + _performDrop(); +} + // ── 이동 로직 ───────────────────────────────────── function _moveSingle(srcIdx, targetIdx) { const [moved] = _state.files.splice(srcIdx, 1); _state.files.splice(targetIdx, 0, moved); - const cards = Array.from(_grid.children); - const src = cards[srcIdx]; - const target = cards[targetIdx]; - if (srcIdx < targetIdx) { - _grid.insertBefore(src, target.nextSibling); - } else { - _grid.insertBefore(src, target); - } - Array.from(_grid.children).forEach((c, i) => { c.dataset.idx = i; }); + // 플레이스홀더 제거 후 소스 카드 복원 + if (_placeholder.parentNode) _placeholder.parentNode.removeChild(_placeholder); + const srcCard = _getCardEl(srcIdx); + const targetCard = _getCardEl(targetIdx); + if (srcCard) srcCard.classList.remove('drag-hidden'); + if (srcCard) { + if (targetCard) { + if (srcIdx < targetIdx) { + _grid.insertBefore(srcCard, targetCard.nextSibling); + } else { + _grid.insertBefore(srcCard, targetCard); + } + } else { + _grid.appendChild(srcCard); + } + } + + Array.from(_grid.children).forEach((c, i) => { c.dataset.idx = i; }); _cb.updateNumberBadges(); _cb.updateNewNameLabels(); } @@ -207,9 +284,13 @@ function _moveMulti(targetIdx) { for (let i = 0; i < movedFiles.length; i++) newSel.add(adjustedTarget + i); _state.selectedIdxs = newSel; - // DOM 재정렬 (이미지 재로딩 없이) + // 플레이스홀더 제거, 숨긴 카드 복원 후 DOM 재정렬 + if (_placeholder.parentNode) _placeholder.parentNode.removeChild(_placeholder); const byPath = new Map(); - Array.from(_grid.children).forEach(c => byPath.set(c.dataset.filepath, c)); + Array.from(_grid.children).forEach(c => { + c.classList.remove('drag-hidden'); + byPath.set(c.dataset.filepath, c); + }); _grid.innerHTML = ''; _state.files.forEach((f, idx) => { const c = byPath.get(f.path); diff --git a/src/renderer/style.css b/src/renderer/style.css index b620785..8f2728d 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -243,6 +243,23 @@ main { .card.dragging, .card.multi-dragging { opacity: .3; transform: scale(.96); cursor: grabbing; } .card.drag-over { border-color: var(--accent) !important; background: var(--drag-over); } +.card.drag-hidden { display: none; } + +.card-placeholder { + border: 2px dashed var(--accent); + background: rgba(124, 106, 247, 0.08); + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + pointer-events: auto; +} +.card-placeholder-label { + color: var(--accent); + font-size: 13px; + font-weight: 600; + opacity: 0.8; +} /* ── 썸네일: 정사각형 + object-fit: contain ── */ .card-thumb {