Initial commit: AI Photo Organizer (Electron + face-api)

Local-first photo organizer that auto-sorts images by face recognition
and EXIF capture date.

- Electron app with 3-process split: Main (Node) / UI Renderer (React) /
  hidden Inference Renderer (face-api + WebGL)
- Core pipeline: scan -> face match + EXIF -> path build -> atomic move/copy
- Move = copy -> verify -> delete; auto-rename on filename collision
- 1st-registered profile = move, others = copy; unmatched -> [미정]/YYYY/MM
- EXIF date with mtime fallback
- Vitest unit tests (pathBuilder / fileOps / concurrency) all green
- electron-builder config for Windows (nsis) + macOS (dmg)
- Docs: PRD / DECISIONS / ARCHITECTURE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 13:36:40 +09:00
commit 8a8c10248c
54 changed files with 11507 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
import { join, extname, basename } from 'node:path'
import type { CaptureDate } from '@shared/types'
import { UNMATCHED_FOLDER } from '@shared/constants'
/**
* 인물/미정 + 연/월 기준의 대상 디렉터리 경로를 생성한다.
* 실제 파일명 충돌 해소는 fileOps에서 수행 (여기서는 디렉터리 + 원본 파일명까지).
*
* @param who 인물 폴더명, 또는 미검출이면 null → [미정]
*/
export function buildTargetPath(
outputRoot: string,
who: string | null,
date: CaptureDate,
sourceFile: string
): string {
const folder = who ?? UNMATCHED_FOLDER
const filename = basename(sourceFile)
return join(outputRoot, folder, date.year, date.month, filename)
}
/**
* 파일명 충돌 시 사용할 후보 경로를 생성 (name_1.ext, name_2.ext ...).
* @param index 1부터 시작하는 충돌 회피 인덱스
*/
export function withCollisionSuffix(targetPath: string, index: number): string {
const dir = targetPath.slice(0, targetPath.length - basename(targetPath).length)
const ext = extname(targetPath)
const stem = basename(targetPath, ext)
return join(dir, `${stem}_${index}${ext}`)
}