리펙토링: 전체 구조 검토, 개선
This commit is contained in:
@@ -15,7 +15,7 @@ There are no tests or linting configured.
|
||||
|
||||
## Architecture
|
||||
|
||||
This is a macOS Electron desktop app for batch-renaming photo files. It follows strict Electron security practices: `contextIsolation: true`, `nodeIntegration: false`.
|
||||
This is a macOS Electron desktop app for batch-renaming and moving photo files. It follows strict Electron security practices: `contextIsolation: true`, `nodeIntegration: false`.
|
||||
|
||||
**Process boundary:** All filesystem access lives exclusively in `src/main/` (main process). The renderer never touches `fs` directly.
|
||||
|
||||
@@ -27,20 +27,27 @@ src/
|
||||
│ ├── index.js # Main process entry point; creates BrowserWindow, calls registerAll()
|
||||
│ └── ipc/
|
||||
│ ├── index.js # registerAll(mainWindow) — delegates to folder and files modules
|
||||
│ ├── folder.js # select-folder, reveal-folder
|
||||
│ └── files.js # list-images, read-image, rename-files
|
||||
│ ├── 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
|
||||
├── preload/
|
||||
│ └── index.js # Bridges IPC to window.api via contextBridge (isolated context)
|
||||
└── renderer/
|
||||
├── index.html # App shell (dark theme, CSS variables in :root)
|
||||
├── index.html # App shell; dark theme, CSS variables in :root, modal HTML
|
||||
├── style.css # All CSS
|
||||
├── renderer.js # Main renderer logic; imports state and handlers
|
||||
├── state.js # Shared mutable state: files[], currentFolder, selectedIdxs, sortMode
|
||||
├── renderer.js # Entry point: init, loadFolder, sortFiles, keyboard shortcuts, startup
|
||||
├── ui.js # showLoading, hideLoading, setStatus, showToast
|
||||
├── grid.js # renderGrid, createCard, card UI update functions, showEmptyGrid
|
||||
├── rename.js # doRename + rename-conflict modal
|
||||
├── move.js # handleFolderDrop + move modals + _doMoveFiles
|
||||
└── handlers/
|
||||
├── drag-drop.js # Card drag-and-drop reorder (single and multi-card)
|
||||
├── selection.js # Rubber-band selection, Shift-click, Cmd+click
|
||||
├── pinch-zoom.js # Trackpad pinch to resize thumbnails (CSS --thumb-size, 150–400px)
|
||||
└── image-preview.js # Hover preview overlay
|
||||
├── drag-drop.js # Card reorder (single/multi) + drag-to-folder in sidebar
|
||||
├── 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)
|
||||
└── image-preview.js # Hover preview overlay with circular loading indicator
|
||||
```
|
||||
|
||||
### IPC surface
|
||||
@@ -50,9 +57,15 @@ Defined in `src/main/ipc/`, bridged via `src/preload/index.js` as `window.api`:
|
||||
| Channel | Direction | Description |
|
||||
|---|---|---|
|
||||
| `select-folder` | invoke | Opens native folder picker, returns path or null |
|
||||
| `list-images` | invoke | Reads directory, returns `{name, path, mtime, size}[]` filtered to image extensions, sorted by filename |
|
||||
| `list-images` | invoke | Reads directory, returns `{name, path, mtime}[]` filtered to image extensions, sorted by filename |
|
||||
| `read-image` | invoke | Reads file, returns base64 data URL for display |
|
||||
| `rename-files({folderPath, files, prefix, digits})` | invoke | Two-phase rename: temp names first, then final names to avoid collisions |
|
||||
| `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) |
|
||||
| `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 |
|
||||
| `get-preview-on` / `set-preview-on` | invoke | Persists hover preview toggle state in userData config.json |
|
||||
| `reveal-folder` | invoke | Opens folder in Finder via `shell.openPath` |
|
||||
| `fullscreen-change` | push (main→renderer) | Sent on `enter-full-screen` / `leave-full-screen` window events |
|
||||
|
||||
@@ -60,14 +73,54 @@ Defined in `src/main/ipc/`, bridged via `src/preload/index.js` as `window.api`:
|
||||
|
||||
Uses ES module imports (`type="module"`). No framework.
|
||||
|
||||
**Dependency direction** (no cycles):
|
||||
```
|
||||
renderer.js → rename.js, move.js → grid.js → handlers/*
|
||||
↘ ↗
|
||||
ui.js
|
||||
```
|
||||
|
||||
**State** (`src/renderer/state.js`):
|
||||
- `files[]` — ordered array of file objects; order determines rename sequence
|
||||
- `files[]` — ordered array of `{name, path, mtime}`; order determines rename sequence
|
||||
- `selectedIdxs` — `Set<number>` of selected card indices
|
||||
- `currentFolder` — active directory path
|
||||
- `sortMode` — `'name'` | `'date'` | `null`
|
||||
|
||||
**Thumbnail loading:** Uses `IntersectionObserver` with `rootMargin: '400px 0px'` — images load lazily as cards enter the viewport. DOM card elements are created immediately without `src`; the observer fills in `img.src` on entry. Drag-and-drop reorder remaps existing card elements via `card.dataset.filepath` to avoid re-fetching images.
|
||||
**Keyboard shortcuts:**
|
||||
- `O` — 폴더 열기
|
||||
- `S` — 이름 변경 실행
|
||||
- `A` — 전체 선택
|
||||
|
||||
**Styling:** All CSS lives in `src/renderer/index.html`. Dark theme with CSS variables defined in `:root`. The macOS traffic-light buttons sit in `#titlebar` with `-webkit-app-region: drag`.
|
||||
**Card selection** (`selection.js`):
|
||||
- Click — 단독 선택 (재클릭 시 해제)
|
||||
- Shift+Click — 범위 선택
|
||||
- Cmd+Click — 개별 토글
|
||||
- Rubber-band drag — 영역 선택 (rAF throttle 적용)
|
||||
- `resetAnchor()` — 그리드 재렌더 시 Shift 기준점 초기화 (grid.js에서 호출)
|
||||
|
||||
**Drag-and-drop** (`drag-drop.js`):
|
||||
- 그리드 내: 카드 순서 재배치 (single/multi)
|
||||
- 사이드바 폴더로: 선택된 파일들을 해당 폴더로 이동
|
||||
- 파일명 충돌 시 rename-and-move 모달 표시
|
||||
- 드래그 중 `.tree-row.drop-target` 클래스로 대상 폴더 하이라이트
|
||||
|
||||
**Rename** (`rename.js`):
|
||||
- 선택된 파일만 이름 변경 (선택 없으면 전체)
|
||||
- 변경될 이름이 미선택 파일과 충돌하면 충돌 목록 모달 표시
|
||||
|
||||
**Move** (`move.js`):
|
||||
- 충돌 없으면 즉시 이동
|
||||
- 충돌 있으면 prefix + digits 입력 모달 → 새 이름으로 이동
|
||||
- 새 이름도 충돌이면 에러 모달
|
||||
|
||||
**Thumbnail loading:** `IntersectionObserver` with `rootMargin: '400px 0px'` — lazy load as cards enter viewport. `entry.target.isConnected` 체크로 폴더 전환 중 스탈 카드 방지. Drag reorder remaps card elements via `card.dataset.filepath` to avoid re-fetching.
|
||||
|
||||
**Hover preview** (`image-preview.js`): 1초 지연 후 전체 크기 팝업. 썸네일 캐시 우선 사용. 상태는 config.json에 영속.
|
||||
|
||||
**Sidebar tree** (`file-explorer.js`): Lazy-load 자식 폴더. 접근 불가 폴더는 `null` 반환 → "접근 불가" 표시. 행 클릭 시 항상 펼침, 화살표 클릭 시 토글.
|
||||
|
||||
**Styling:** CSS는 `src/renderer/style.css`에 집중. Dark theme, CSS variables in `:root`. macOS traffic-light은 `header`의 `-webkit-app-region: drag`.
|
||||
|
||||
**Build output:** `electron-builder` targets DMG for arm64 and x64. The app icon must be at `assets/icon.icns`.
|
||||
|
||||
**Config persistence:** `app.getPath('userData')/config.json`에 `lastFolder`, `previewOn` 저장.
|
||||
|
||||
Reference in New Issue
Block a user