diff --git a/src/main/ipc/files.js b/src/main/ipc/files.js index a486a0c..72c1d46 100644 --- a/src/main/ipc/files.js +++ b/src/main/ipc/files.js @@ -36,6 +36,39 @@ function register() { } }); + ipcMain.handle('check-files-exist', (_event, folderPath, names) => { + return names.filter(name => { + try { fs.accessSync(path.join(folderPath, name)); return true; } + catch { return false; } + }); + }); + + ipcMain.handle('move-files', async (_event, { targetFolder, files }) => { + const errors = []; + const moved = []; + for (const file of files) { + const destName = file.newName || file.name; + const dest = path.join(targetFolder, destName); + try { + fs.renameSync(file.path, dest); + moved.push({ src: file.path, dest, name: destName }); + } catch (e) { + if (e.code === 'EXDEV') { + try { + fs.copyFileSync(file.path, dest); + fs.unlinkSync(file.path); + moved.push({ src: file.path, dest, name: destName }); + } catch (e2) { + errors.push(`${file.name}: ${e2.message}`); + } + } else { + errors.push(`${file.name}: ${e.message}`); + } + } + } + return { moved, errors }; + }); + ipcMain.handle('rename-files', async (_event, { folderPath, files, prefix, digits }) => { const errors = []; const pad = (n) => String(n + 1).padStart(digits, '0'); diff --git a/src/preload/index.js b/src/preload/index.js index ac29c70..61bfdde 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -11,4 +11,6 @@ contextBridge.exposeInMainWorld('api', { setLastFolder: (p) => ipcRenderer.invoke('set-last-folder', p), listDirectories: (p) => ipcRenderer.invoke('list-directories', p), revealFolder: (p) => ipcRenderer.invoke('reveal-folder', p), + checkFilesExist: (folder, names) => ipcRenderer.invoke('check-files-exist', folder, names), + moveFiles: (args) => ipcRenderer.invoke('move-files', args), }); diff --git a/src/renderer/handlers/drag-drop.js b/src/renderer/handlers/drag-drop.js index d1a7614..f465cbc 100644 --- a/src/renderer/handlers/drag-drop.js +++ b/src/renderer/handlers/drag-drop.js @@ -1,21 +1,30 @@ -let _grid, _gridContainer, _dragGhostEl; -let _state, _cb; +let _grid, _gridContainer, _dragGhostEl, _fileExplorerEl; +let _state, _cb, _onFolderDrop; let dragMode = null; let dragSrcIdx = null; +let _hoveredTreeRow = null; let autoScrollRAF = null; let autoScrollSpeed = 0; let lastScrollTs = null; -export function init({ grid, gridContainer, dragGhostEl, state, callbacks }) { - _grid = grid; - _gridContainer = gridContainer; - _dragGhostEl = dragGhostEl; - _state = state; - _cb = callbacks; +export function init({ grid, gridContainer, dragGhostEl, state, callbacks, fileExplorerEl, onFolderDrop }) { + _grid = grid; + _gridContainer = gridContainer; + _dragGhostEl = dragGhostEl; + _state = state; + _cb = callbacks; + _fileExplorerEl = fileExplorerEl; + _onFolderDrop = onFolderDrop; window.addEventListener('dragover', _onWindowDragOver); + + if (fileExplorerEl) { + fileExplorerEl.addEventListener('dragover', _onExplorerDragOver); + fileExplorerEl.addEventListener('dragleave', _onExplorerDragLeave); + fileExplorerEl.addEventListener('drop', _onExplorerDrop); + } } // 카드 생성 시 호출해 드래그 이벤트를 부착 @@ -32,7 +41,7 @@ export function setupCard(card) { function _onWindowDragOver(e) { if (!dragMode) return; - if (dragMode === 'multi') _moveGhost(e); + _moveGhost(e); _trackScroll(e.clientY); } @@ -97,6 +106,7 @@ function _onDrop(e) { function _onDragEnd() { _stopScroll(); _hideGhost(); + _clearTreeHover(); document.querySelectorAll('.card').forEach(c => c.classList.remove('dragging', 'multi-dragging', 'drag-over') ); @@ -104,6 +114,63 @@ function _onDragEnd() { dragSrcIdx = null; } +// ── 폴더 탐색기 드롭 핸들러 ───────────────────────── + +function _onExplorerDragOver(e) { + if (!dragMode) return; + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + + const row = e.target.closest('.tree-row'); + if (row !== _hoveredTreeRow) { + _clearTreeHover(); + _hoveredTreeRow = row; + if (_hoveredTreeRow) _hoveredTreeRow.classList.add('drop-target'); + } + + if (dragMode === 'single' && row && _dragGhostEl.style.display === 'none') { + _showGhost(e, '📁 1개 폴더로 이동'); + } + if (dragMode === 'multi' && row) { + _dragGhostEl.textContent = `📁 ${_state.selectedIdxs.size}개 폴더로 이동`; + } +} + +function _onExplorerDragLeave(e) { + if (!_fileExplorerEl.contains(e.relatedTarget)) { + _clearTreeHover(); + if (dragMode === 'single') _hideGhost(); + if (dragMode === 'multi') _dragGhostEl.textContent = `📦 ${_state.selectedIdxs.size}개 이동 중`; + } +} + +function _onExplorerDrop(e) { + e.preventDefault(); + const row = e.target.closest('.tree-row'); + _clearTreeHover(); + + if (!row || !_onFolderDrop) return; + const folderNode = row.closest('.tree-node'); + if (!folderNode) return; + + const targetPath = folderNode.dataset.path; + const indices = _getDraggedFileIndices(); + _onFolderDrop(targetPath, indices); +} + +function _getDraggedFileIndices() { + if (dragMode === 'multi') return [..._state.selectedIdxs]; + if (dragMode === 'single' && dragSrcIdx !== null) return [dragSrcIdx]; + return []; +} + +function _clearTreeHover() { + if (_hoveredTreeRow) { + _hoveredTreeRow.classList.remove('drop-target'); + _hoveredTreeRow = null; + } +} + // ── 이동 로직 ───────────────────────────────────── function _moveSingle(srcIdx, targetIdx) { diff --git a/src/renderer/index.html b/src/renderer/index.html index b0237a0..4250979 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -75,6 +75,29 @@
+ + + +