Files
photo-renamer/CLAUDE.md
T
2026-05-17 23:06:14 +09:00

12 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Commands

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, 150400px)
        └── 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
  • selectedIdxsSet<number> 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.jsinitKeyboardShortcuts()):

  • 모달이 열려 있거나 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)<li> 목록을 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.jsonlastFolder, previewOn 저장.