diff --git a/CLAUDE.md b/CLAUDE.md index d256c64..a94dbbc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,24 +31,38 @@ src/ │ ├── index.js # Main process entry point; app.setName(), BrowserWindow, registerAll() │ └── ipc/ │ ├── index.js # registerAll(mainWindow) — delegates to folder and files modules +│ ├── file-utils.js # Pure helpers: listImages, readImageAsDataUrl, fileExists, +│ │ # moveFileWithCopyFallback (EXDEV-safe cross-device move) │ ├── folder.js # select-folder, list-directories, get/set-last-folder, │ │ # get/set-preview-on, get-home-dir, reveal-folder -│ └── files.js # list-images, read-image, rename-files, -│ # check-files-exist, move-files +│ └── files.js # IPC handlers: list-images, read-image, rename-files, +│ # check-files-exist, move-files, stage-delete-files, +│ # restore-staged-files — delegates logic to file-utils.js ├── preload/ │ └── index.js # Bridges IPC to window.api via contextBridge (isolated context) └── renderer/ - ├── index.html # App shell; dark theme, CSS variables in :root, modal HTML - ├── style.css # All CSS - ├── state.js # Shared mutable state: files[], currentFolder, selectedIdxs, sortMode - ├── renderer.js # Entry point: init, loadFolder, sortFiles, undo/redo, keyboard shortcuts - ├── ui.js # showLoading, hideLoading, setStatus, showToast - ├── grid.js # renderGrid, createCard, card UI update functions, showEmptyGrid - ├── rename.js # doRename + rename-conflict modal; resets history after rename - ├── move.js # handleFolderDrop + move modals + _doMoveFiles; resets history after move - ├── history.js # Undo/redo stack (max 50 entries); tracks file order + affected paths + ├── index.html # App shell; dark theme, CSS variables in :root, modal HTML + ├── style.css # All CSS + ├── state.js # Shared mutable state: files[], currentFolder, selectedIdxs, sortMode + ├── renderer.js # Entry point: wires up controllers, handlers, event listeners + ├── folder-controller.js # createFolderController() — loadFolder, openFolder, sortFiles + ├── history-controller.js # createHistoryController() — performUndo, performRedo, applySnapshot + ├── keyboard.js # initKeyboardShortcuts() — modal-guard, all shortcut bindings + ├── ui.js # showLoading, hideLoading, setStatus, showToast + ├── grid.js # renderGrid, createCard, card UI update functions, showEmptyGrid + ├── rename.js # doRename + rename-conflict modal; resets history after rename + ├── move.js # handleFolderDrop + move modals + _doMoveFiles; resets history after move + ├── delete.js # deleteSelected, init (card-delete btn) + staging delete flow + ├── history.js # Undo/redo stack (max 50 entries); reorder + delete entry types + ├── modal.js # initModalControls, openModal, closeModal, focus trap + ├── utils/ + │ ├── file-names.js # getExtension, clampDigits, buildSequentialName, getNameFormat + │ └── dom.js # renderTextList └── handlers/ ├── drag-drop.js # Card reorder (single/multi) + drag-to-folder in sidebar + ├── drag/ + │ ├── auto-scroll.js # createAutoScroller() — rAF-based edge scroll during drag + │ └── ghost.js # createDragGhost() — floating badge during drag ├── file-explorer.js # Sidebar directory tree (lazy-load, expand/collapse) ├── selection.js # Rubber-band, Shift-click, Cmd+click, resetAnchor ├── pinch-zoom.js # Ctrl+scroll to resize thumbnails (CSS --thumb-size, 150–400px) @@ -67,6 +81,8 @@ Defined in `src/main/ipc/`, bridged via `src/preload/index.js` as `window.api`: | `rename-files({folderPath, files, prefix, digits})` | invoke | Two-phase rename: temp names (crypto.randomUUID) first, then final names | | `check-files-exist(folderPath, names[])` | invoke | Returns subset of names that already exist in folderPath | | `move-files({targetFolder, files[]})` | invoke | Moves files; each file may have optional `newName`; handles cross-device (EXDEV) | +| `stage-delete-files(filePaths[])` | invoke | Moves files to userData/deleted_staging//; returns `{staged, errors}` | +| `restore-staged-files(deletedEntries[])` | invoke | Restores staged files to original paths; returns `{restored, errors}` | | `list-directories(folderPath)` | invoke | Returns `{name, path, hasChildren}[]` subdirs, or `null` on access error | | `get-home-dir` | invoke | Returns `os.homedir()` | | `get-last-folder` / `set-last-folder` | invoke | Persists last opened folder in userData config.json | @@ -80,9 +96,15 @@ Uses ES module imports (`type="module"`). No framework. **Dependency direction** (no cycles): ``` -renderer.js → rename.js, move.js → grid.js → handlers/* - ↘ ↘ ↗ - history.js ui.js +renderer.js ─┬─ folder-controller.js ─┐ + ├─ history-controller.js ├──► grid.js ──► handlers/* + ├─ keyboard.js │ ↗ + ├─ rename.js ├──► history.js + ├─ move.js ├──► ui.js + ├─ delete.js │ + └─ modal.js ─┘ + utils/file-names.js, utils/dom.js + (imported by grid, rename, move, delete) ``` **State** (`src/renderer/state.js`): @@ -91,21 +113,37 @@ renderer.js → rename.js, move.js → grid.js → handlers/* - `currentFolder` — active directory path - `sortMode` — `'name'` | `'date'` | `null` -**Keyboard shortcuts** (모두 `e.code` 기준 — 한글 입력 모드에서도 동작): +**Controllers** (`folder-controller.js`, `history-controller.js`): +- Factory functions (`createFolderController`, `createHistoryController`) receive DOM refs at creation time and return method objects. +- `folder-controller`: `loadFolder`, `openFolder`, `sortFiles`, `updateSortButtons`, `setHomeDir` +- `history-controller`: `performUndo`, `performRedo`, `updateButtons`; `updateSortButtons` injected from folder-controller + +**Keyboard shortcuts** (`keyboard.js` — `initKeyboardShortcuts()`): +- 모달이 열려 있거나 INPUT에 포커스된 경우 단축키 전체 무시 (`_hasOpenModal()` 가드) +- 모두 `e.code` 기준 — 한글 입력 모드에서도 동작 - `O` — 폴더 열기 - `S` — 이름 변경 실행 - `A` — 전체 선택 +- `Backspace` / `Delete` — 선택 파일 삭제 - `Cmd+Z` — Undo (되돌리기) - `Cmd+Shift+Z` — Redo (다시하기) - `P` — 미리보기 on/off 토글 -**Undo / Redo** (`history.js`): -- 파일 순서 변경(드래그, 정렬)만 추적; 파일시스템 변경(rename, move)은 히스토리 리셋 -- 각 엔트리: `{ files: [...], affectedPaths: Set | null }` -- `affectedPaths`: undo/redo 실행 후 해당 파일들을 선택 상태로 복원하는 데 사용 +**Undo / Redo** (`history.js` + `history-controller.js`): +- 파일 순서 변경(드래그, 정렬)과 삭제 작업을 추적; rename/move는 히스토리 리셋 +- 엔트리 타입: + - `reorder`: `{ type, files, affectedPaths: Set | null }` + - `delete`: `{ type, files /* filesAfter */, deletedEntries: [{name, originalPath, stagingPath}] }` +- `deletedEntries`는 redo 시 stagingPath가 갱신되는 mutable 객체 - `setOnUpdate(fn)`: push/reset/undo/redo 시 자동 호출되는 콜백 등록 (버튼 상태 갱신용) - 최대 50개 엔트리 유지 +**Delete** (`delete.js`): +- 삭제 시 파일시스템에서 바로 제거하지 않고 userData/deleted_staging/으로 이동 (undo 가능) +- `deleteSelected()` — 키보드 단축키용 +- `init(gridEl)` — 카드의 × 버튼 클릭 이벤트 등록 +- 삭제 확인 모달 → `_doDelete()` → history에 delete 엔트리 push + **Card selection** (`selection.js`): - Click — 단독 선택 (재클릭 시 해제) - Shift+Click — 범위 선택 @@ -116,12 +154,18 @@ renderer.js → rename.js, move.js → grid.js → handlers/* **Drag-and-drop** (`drag-drop.js`): - 그리드 내 카드 순서 재배치 (single/multi) - - 드래그 중인 카드는 `.drag-hidden`으로 숨김; 삽입 위치에 `.card-placeholder` 표시 + - 드래그 중인 카드는 `.drag-hidden`으로 숨김; 삽입 위치에 세로 separator 표시 - 마지막 카드 이후 빈 영역에 드롭하면 맨 끝에 삽입 - 드롭 완료 후 이동한 파일들이 선택 상태로 유지되고 history에 push - 사이드바 폴더로 드래그: 선택된 파일들을 해당 폴더로 이동 - 파일명 충돌 시 rename-and-move 모달 표시 - 드래그 중 `.tree-row.drop-target` 클래스로 대상 폴더 하이라이트 +- `drag/auto-scroll.js`: `createAutoScroller(container, isActive)` — 뷰포트 상하 10% 진입 시 rAF 기반 자동 스크롤 +- `drag/ghost.js`: `createDragGhost(el)` — 드래그 중 floating 배지 위치/텍스트 관리 + +**Utilities** (`utils/`): +- `utils/file-names.js`: `getExtension(fileName, fallback?)`, `clampDigits(value, fallback?)`, `buildSequentialName(fileName, idx, {prefix, digits})`, `getNameFormat(prefixInput, digitsInput)` +- `utils/dom.js`: `renderTextList(listEl, items)` — `
  • ` 목록을 listEl에 직접 렌더링 **Rename** (`rename.js`): - 선택된 파일만 이름 변경 (선택 없으면 전체)