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:
@@ -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}`)
|
||||
}
|
||||
Reference in New Issue
Block a user