diff --git a/AGENTS.md b/AGENTS.md index 6f1893c..7ea580a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md -This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. +This file provides guidance to Claude Code (claude.ai/code) and Codex when working with code in this repository. ## Commands @@ -32,8 +32,8 @@ src/ │ ├── config.js # Shared userData/config.json read/write helpers │ └── ipc/ │ ├── index.js # registerAll(mainWindow) — delegates to folder and files modules -│ ├── file-utils.js # Shared main-process file helpers: image listing/data URLs, -│ │ # exists check, EXDEV-safe move/copy fallback +│ ├── 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/set-sidebar-width, get/set-thumb-size, │ │ # get-home-dir, reveal-folder diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index a94dbbc..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,193 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Commands - -```bash -npm start # Run the Electron app in development -npm run build # Build macOS app bundle (no DMG, arm64 + x64) -npm run dist # Build and package as DMG for distribution -./setup.sh # First-time setup: npm install + npm start -``` - -There are no tests or linting configured. - -## Architecture - -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. - -### File structure - -``` -assets/ -├── icon.png # Source icon (2048×2048, square) -└── icon.icns # macOS app icon for electron-builder - -src/ -├── main/ -│ ├── 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 # 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: 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) - └── image-preview.js # Hover preview overlay with circular loading indicator -``` - -### IPC surface - -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}[]` filtered to image extensions, sorted by filename | -| `read-image` | invoke | Reads file, returns base64 data URL (supports jpg/png/gif/webp/tiff/bmp/heic) | -| `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 | -| `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 | - -### Renderer (`src/renderer/`) - -Uses ES module imports (`type="module"`). No framework. - -**Dependency direction** (no cycles): -``` -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`): -- `files[]` — ordered array of `{name, path, mtime}`; order determines rename sequence -- `selectedIdxs` — `Set` of selected card indices -- `currentFolder` — active directory path -- `sortMode` — `'name'` | `'date'` | `null` - -**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` + `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 — 범위 선택 -- Cmd+Click — 개별 토글 -- Rubber-band drag — 영역 선택 (rAF throttle 적용) -- Shift+Rubber-band — 토글 선택 (드래그 시작 시점 스냅샷 기준으로 선택/해제 반전) -- `resetAnchor()` — 그리드 재렌더 시 Shift 기준점 초기화 (grid.js에서 호출) - -**Drag-and-drop** (`drag-drop.js`): -- 그리드 내 카드 순서 재배치 (single/multi) - - 드래그 중인 카드는 `.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`): -- 선택된 파일만 이름 변경 (선택 없으면 전체) -- 변경될 이름이 미선택 파일과 충돌하면 충돌 목록 모달 표시 -- 완료 후 history 리셋 (rename은 파일 경로가 바뀌므로 undo 대상 아님) - -**Move** (`move.js`): -- 충돌 없으면 즉시 이동 -- 충돌 있으면 prefix + digits 입력 모달 → 새 이름으로 이동 -- 새 이름도 충돌이면 에러 모달 -- 완료 후 history 리셋 - -**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`. - -**App icon:** 소스는 `assets/icon.png` (2048×2048). `assets/icon.icns`는 10개 해상도(16px~1024px@2x)를 포함하며 electron-builder가 사용. `app.setName('Photo Renamer')`로 개발 모드 메뉴바 이름도 지정. - -**Build output:** `electron-builder` targets DMG for arm64 and x64. - -**Config persistence:** `app.getPath('userData')/config.json`에 `lastFolder`, `previewOn` 저장. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file