8a8c10248c
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>
16 KiB
16 KiB
AI Photo Organizer — 시스템 아키텍처 설계
상태: 확정 (Final) · 최종 갱신: 2026-06-01 선행 문서: PRD · DECISIONS.md 승인 완료 — 본 설계로 스캐폴딩/구현 착수.
0. 한눈에 보기 (TL;DR)
- 플랫폼: Electron 데스크톱 앱 (Windows/macOS).
- 3-프로세스 구조:
Main(파일·오케스트레이션) +UI Renderer(React 화면) +Inference Renderer(숨김 창, 얼굴인식 전담). - 왜 분리? face-api.js는 브라우저(WebGL/Canvas) 환경이 필요하고, 파일 I/O는 Node 환경이 필요하다. 둘을 다른 프로세스로 나눠 UI 프리징 0, 빌드 안정성을 동시에 확보한다.
- 데이터 흐름: 스캔(Main) → 얼굴인식(Inference) → 날짜추출(Main) → 경로생성/이동·복사(Main) → 진행률 이벤트(UI).
1. 기술 스택 확정 (Tech Stack & Versions)
| 영역 | 라이브러리 | 버전(기준) | 비고 |
|---|---|---|---|
| 런타임/셸 | Electron | ^33.0.0 |
Chromium + Node 번들. WebGL 가속 사용. |
| 빌드 도구 | electron-vite | ^2.3.0 |
Main/Preload/Renderer 멀티 엔트리를 Vite로 통합 빌드. |
| 번들러 | Vite | ^5.4 |
electron-vite 내부. |
| UI | React | ^18.3 |
함수형 + Hooks. |
| 언어 | TypeScript | ^5.5 |
전 프로세스 공통. strict: true. |
| 상태관리 | Zustand | ^4.5 |
경량 글로벌 스토어 (Redux 과잉 회피). |
| 스타일 | Tailwind CSS | ^3.4 |
빠른 UI 구성. (선호 시 교체 가능) |
| 얼굴 인식 | @vladmandic/face-api | ^1.7 |
원본 face-api.js(유지보수 중단)의 API 호환 포크. tfjs(WebGL 백엔드)를 내부 번들로 포함 → 별도 tfjs 의존 불필요. Inference Renderer에서만 사용. |
| EXIF 파싱 | exifr | ^7.1 |
빠르고 HEIC/주요 포맷 지원. Main(Node)에서 실행. |
| 파일 작업 | node:fs/promises | 내장 | 비동기 I/O. |
| 동시성 제어 | (자체 구현 세마포어) | - | 외부 p-limit 대신 경량 직접 구현. |
| 패키징 | electron-builder | ^25 |
배포용 인스톨러(.exe/.dmg) 생성. |
| 테스트 | Vitest | ^2 |
순수 로직(스캐너/EXIF/경로/충돌) 단위 테스트. |
Node는 Electron 번들 버전(Electron 33 → Node 20 계열)을 사용하므로 별도 Node 버전 고정 불필요.
2. 프로세스 아키텍처 (Process Topology)
┌──────────────────────────────────────────────────────────────────┐
│ Electron App │
│ │
│ ┌─────────────────┐ IPC ┌──────────────────────┐ │
│ │ UI Renderer │◀──────────────────▶│ Main Process │ │
│ │ (visible window)│ progress/control │ (orchestrator/Node) │ │
│ │ React + Zustand │ │ │ │
│ └─────────────────┘ │ scanner / exif / │ │
│ ▲ preload(contextBridge) │ fileOps / pathBuilder│ │
│ │ │ profileStore /reporter│ │
│ │ └──────────┬───────────┘ │
│ │ │ IPC (descriptor│
│ │ │ 요청/응답) │
│ │ ┌──────────▼───────────┐ │
│ │ │ Inference Renderer │ │
│ └─────────(미표시)────────────────▶│ (hidden window) │ │
│ │ face-api + WebGL │ │
│ │ faceEngine/imageLoader│ │
│ └──────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
2.1 각 프로세스 책임
| 프로세스 | 환경 | 책임 |
|---|---|---|
| Main | Node | 폴더 스캔, EXIF 추출, 파일 이동/복사(원자성), 프로필 영속화, 작업 오케스트레이션, 리포트/로그, IPC 라우팅. |
| UI Renderer | Chromium(보임) | 프로필 등록 UI, 폴더 선택, 실행 제어, 실시간 진행률/파일목록, 결과 리포트 표시. 무거운 연산 없음 → 프리징 없음. |
| Inference Renderer | Chromium(숨김) | face-api 모델 로드, 이미지 디코딩, 얼굴 검출·descriptor 계산, 프로필과 매칭. UI 스레드와 분리되어 연산이 화면을 막지 않음. |
대안 검토: Inference를 별도 숨김 창 대신 Web Worker + OffscreenCanvas로 둘 수도 있으나, face-api는
document/canvas의존이 있어 worker 환경 패치가 까다롭다. v1은 숨김 BrowserWindow가 가장 안정적. (성능 이슈 시 worker로 이전 검토.)
3. 모듈 구조 (Module Breakdown)
3.1 Main Process (src/main/)
| 모듈 | 파일 | 역할 | 핵심 인터페이스(개념) |
|---|---|---|---|
| Scanner | scanner.ts |
소스 폴더 재귀 순회, 확장자 필터, 이미지 경로 스트림 산출. | async *scan(root): AsyncIterable<string> |
| ExifReader | exif.ts |
DateTimeOriginal 추출, 없으면 fs.stat().mtime 폴백 → {year, month}. |
getCaptureDate(path): Promise<{year,month}> |
| FileOps | fileOps.ts |
이동(복사→검증→삭제)/복사, 충돌 시 자동 리네임, 원자성·오류처리. | move(src,dest), copy(src,dest) |
| PathBuilder | pathBuilder.ts |
/<프로필>/YYYY/MM/ · [미정]/YYYY/MM/ 경로 생성, 파일명 충돌 해소. |
build(outRoot, who, date, filename) |
| ProfileStore | profileStore.ts |
프로필(이름·순서·참조이미지·descriptor) JSON 영속화/로드. | load(), save(), upsert() |
| Orchestrator | orchestrator.ts |
전체 잡 파이프라인 구동, 동시성 제어, 진행률 이벤트 발행, 중단/취소. | run(job), cancel() |
| InferenceBridge | inferenceWindow.ts |
숨김 Inference 창 생성/관리, descriptor 요청-응답 RPC. | detect(imagePath): Promise<MatchResult> |
| Reporter | reporter.ts |
통계 집계(총/성공/실패/소요), 결과 리포트 + 로그파일 출력. | summarize(), writeLog() |
| IpcMain | ipc.ts |
UI/Inference 채널 핸들러 등록·라우팅. | - |
| Logger | logger.ts |
구조적 로그(파일+콘솔), 오류 추적. | - |
3.2 Inference Renderer (src/inference/)
| 모듈 | 파일 | 역할 |
|---|---|---|
| FaceEngine | faceEngine.ts |
모델 1회 로드(SSD MobileNet v1 + Landmark68 + Recognition), 이미지당 얼굴검출+descriptor, FaceMatcher로 프로필 매칭(거리 <0.5). |
| ImageLoader | imageLoader.ts |
파일→HTMLImageElement/canvas 디코딩, 대형 이미지 다운스케일(예: 장변 1024px), 처리 후 canvas/objectURL 해제로 메모리 회수. |
| bootstrap | main.ts |
모델 로드 완료 시 Main에 ready 통지, descriptor 요청 수신 루프. |
3.3 UI Renderer (src/renderer/)
| 컴포넌트 | 파일 | 역할 |
|---|---|---|
| App 셸 | App.tsx |
단계별 화면 라우팅(프로필→폴더→실행→리포트). |
| ProfileManager | components/ProfileManager.tsx |
최대 3인 프로필 등록/수정, 참조 이미지 추가, descriptor 생성 트리거. |
| FolderPicker | components/FolderPicker.tsx |
소스 폴더·출력 루트 선택(다이얼로그). |
| RunControl | components/RunControl.tsx |
실행/취소, 임계값 등 옵션. |
| ProgressView | components/ProgressView.tsx |
진행률 바, 처리 중 파일명, 카운터. |
| FileList | components/FileList.tsx |
처리 결과 스트림(파일→대상폴더/판정) 가상 스크롤. |
| ReportView | components/ReportView.tsx |
총처리/성공/실패/소요시간 + 로그 열기. |
| Store | store.ts |
Zustand: 프로필·잡상태·진행률·결과. |
3.4 Preload (src/preload/)
contextBridge로 안전한 API만 노출 (nodeIntegration: false, contextIsolation: true).
window.api = {
profiles: { list, upsert, remove, addReference },
dialog: { pickSource, pickOutput },
job: { run, cancel },
on: (event, cb) => unsubscribe, // progress / fileProcessed / done / error
}
3.5 Shared (src/shared/)
types.ts(Profile, Job, MatchResult, ProgressEvent, Report 등), constants.ts(확장자, 기본 임계값, IPC 채널명).
4. 데이터 흐름 (End-to-End Sequence)
4.1 프로필 등록
UI: 이름 + 참조이미지 선택
→ IPC profiles.addReference(path[])
→ Main: 이미지 경로를 InferenceBridge.detect()로 전달
→ Inference: 참조이미지의 descriptor 계산 → 반환
→ Main ProfileStore: {name, order, refImages, descriptors[]} 저장 (다수 → 평균 descriptor)
→ UI: 등록 완료 표시
4.2 정리 잡 실행 (핵심 파이프라인)
UI: [실행] (source, outputRoot, options)
→ IPC job.run
→ Main Orchestrator 시작
1) Scanner.scan(source) ──▶ 이미지 경로 큐 (스트리밍)
2) 동시성 N(기본 3)개 워커로 각 경로 처리:
a. InferenceBridge.detect(path)
Inference: 이미지 로드 → 얼굴검출 → descriptor
→ FaceMatcher로 프로필 매칭(거리<0.5)
→ MatchResult { matched: [{name, order, distance}], faceFound: bool }
b. ExifReader.getCaptureDate(path) → {year, month} (없으면 mtime)
c. 분기:
- matched.length ≥ 1:
정렬(등록순서 asc) → first=이동, rest=복사
PathBuilder로 각 대상경로 생성 → FileOps.move/copy
- matched.length == 0 (미검출/실패):
PathBuilder([미정], date) → FileOps.move
d. 진행률/결과 이벤트 발행 → UI 갱신
3) 큐 소진 시 Reporter.summarize() → 로그 작성
→ IPC done(report) → UI ReportView
4.3 이동(Move)의 원자성 (데이터 무결성 0 Error)
move(src, dest):
1. dest 충돌 검사 → 있으면 자동 리네임(dest_1, dest_2 …)
2. fs.copyFile(src, tmp) # 임시로 복사
3. 무결성 검증 (size 일치 + 필요 시 해시)
4. rename(tmp → dest) # 원자적 노출
5. unlink(src) # 원본 삭제 (검증 통과 후에만)
실패 시: 원본 보존, tmp 정리, 에러 로깅 → 잡 계속 (해당 파일만 실패 처리)
동일 볼륨이면
fs.rename최적화 가능하나, 안전성 우선으로 copy-verify-delete를 기본으로 한다. (옵션화 가능)
5. 프로젝트 폴더 트리
PhotoAI/
├─ docs/
│ ├─ PRD.md
│ ├─ DECISIONS.md
│ └─ ARCHITECTURE.md ← 본 문서
├─ models/ # face-api 가중치 (ssd_mobilenetv1, landmark68, recognition)
├─ src/
│ ├─ main/ # Main 프로세스
│ │ ├─ index.ts # 앱 진입, 창 생성
│ │ ├─ orchestrator.ts
│ │ ├─ scanner.ts
│ │ ├─ exif.ts
│ │ ├─ fileOps.ts
│ │ ├─ pathBuilder.ts
│ │ ├─ profileStore.ts
│ │ ├─ reporter.ts
│ │ ├─ inferenceWindow.ts
│ │ ├─ ipc.ts
│ │ └─ logger.ts
│ ├─ preload/
│ │ └─ index.ts # contextBridge API
│ ├─ renderer/ # UI 창
│ │ ├─ index.html
│ │ ├─ main.tsx
│ │ ├─ App.tsx
│ │ ├─ store.ts
│ │ ├─ components/
│ │ │ ├─ ProfileManager.tsx
│ │ │ ├─ FolderPicker.tsx
│ │ │ ├─ RunControl.tsx
│ │ │ ├─ ProgressView.tsx
│ │ │ ├─ FileList.tsx
│ │ │ └─ ReportView.tsx
│ │ └─ styles/index.css
│ ├─ inference/ # 숨김 추론 창
│ │ ├─ index.html
│ │ ├─ main.ts
│ │ ├─ faceEngine.ts
│ │ └─ imageLoader.ts
│ └─ shared/
│ ├─ types.ts
│ └─ constants.ts
├─ tests/ # Vitest 단위 테스트 (scanner/exif/pathBuilder/fileOps)
├─ electron.vite.config.ts
├─ electron-builder.yml
├─ tsconfig.json
├─ tailwind.config.js
├─ package.json
└─ README.md
5.1 런타임 데이터 위치
- 프로필 저장:
app.getPath('userData')/profiles.json(OS 표준 사용자 데이터 경로). 내보내기 기능으로 백업 가능. - 작업 로그: 출력 루트 하위
_PhotoAI_logs/run-<타임스탬프>.log(사용자가 결과와 함께 확인). - 모델: 앱 번들 내
models/(오프라인 동작, 클라우드 미사용).
6. IPC 채널 명세
| 채널 | 방향 | 페이로드 | 설명 |
|---|---|---|---|
profiles:list |
UI→Main(invoke) | - | 프로필 목록 |
profiles:upsert |
UI→Main | {id?, name, order} |
등록/수정 |
profiles:addReference |
UI→Main | {id, imagePaths[]} |
참조이미지+descriptor |
profiles:remove |
UI→Main | {id} |
삭제 |
dialog:pickSource / dialog:pickOutput |
UI→Main | - | 폴더 선택 |
job:run |
UI→Main | {source, outputRoot, options} |
잡 시작 |
job:cancel |
UI→Main | - | 취소 |
job:progress |
Main→UI(send) | {done, total, current} |
진행률 |
job:fileProcessed |
Main→UI | {file, decision, target} |
파일 결과 |
job:done |
Main→UI | Report |
완료 |
job:error |
Main→UI | {file, message} |
파일 단위 오류 |
infer:detect |
Main→Inference(invoke) | {imagePath} |
descriptor/매칭 |
infer:ready |
Inference→Main | - | 모델 로드 완료 |
7. 얼굴 인식 상세 (Accuracy ≥ 98% 전략)
- 모델 조합:
SsdMobilenetv1(검출, 정확도 우선) +faceLandmark68Net+faceRecognitionNet(128-d descriptor).- 속도 우선 옵션으로
TinyFaceDetector토글 제공.
- 속도 우선 옵션으로
- 매칭:
faceapi.FaceMatcher, Euclidean distance 임계값 0.5(노출형 파라미터). 낮출수록 엄격(오탐↓/미탐↑). - 정확도 향상: 인물당 참조이미지 다수 등록 → 라벨별 다중 descriptor 또는 평균값 사용.
- 메모리 관리: 이미지 장변 다운스케일, 처리 후
canvas/ObjectURL/텐서 즉시 해제, 동시성 상한으로 피크 메모리 제어. 모델은 1회 로드·앱 생애 유지.
8. 비기능 요구 매핑 (PRD §5 대응)
| 요구 | 설계 반영 |
|---|---|
| Atomic Operation | §4.3 copy-verify-delete, 파일 단위 try/catch, 실패 격리 |
| No Data Loss | 자동 리네임(덮어쓰기 금지), 검증 후에만 원본 삭제 |
| Asynchronous Processing | 모든 fs I/O 비동기 + 스트리밍 스캔 + 별도 추론 프로세스 → UI 프리징 0 |
| Memory Management | 다운스케일·즉시 해제·동시성 상한·모델 1회 로드 |
| Extension Auto-detection | constants.ts의 확장자 화이트리스트(.jpg/.jpeg/.png/.webp) |
9. 빌드 · 실행 · 테스트
- 개발:
electron-vite dev(HMR, 3개 엔트리 동시). - 배포:
electron-vite build→electron-builder인스톨러. - 테스트: Vitest로 순수 로직(스캐너/EXIF 폴백/경로생성/충돌 리네임/이동 원자성) 검증. 얼굴인식은 고정 샘플로 통합 스모크.
10. 열린 결정 — 확정됨 (Resolved)
| # | 항목 | 확정 |
|---|---|---|
| 1 | 얼굴인식 라이브러리 | @vladmandic/face-api (원본 face-api.js 유지보수 포크, API 호환) |
| 2 | Inference 격리 방식 | 숨김 BrowserWindow (v1) |
| 3 | UI 프레임워크 | React + TypeScript + Vite + Tailwind |
| 4 | 프로필 저장 위치 | OS userData 경로 |
| 5 | 패키징 범위 | Windows + macOS 동시 (electron-builder: nsis + dmg) |
전 항목 확정. 본 설계로 구현 착수.