Files
photoai/docs/ARCHITECTURE.md
T
koriweb 8a8c10248c 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>
2026-06-01 13:36:40 +09:00

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 buildelectron-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)

전 항목 확정. 본 설계로 구현 착수.