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,107 @@
|
||||
import { useStore } from '../store'
|
||||
|
||||
/** 실행/취소 + 옵션(임계값, 동시성, 검출기) */
|
||||
export function RunControl(): JSX.Element {
|
||||
const { source, outputRoot, profiles, options, phase } = useStore((s) => ({
|
||||
source: s.source,
|
||||
outputRoot: s.outputRoot,
|
||||
profiles: s.profiles,
|
||||
options: s.options,
|
||||
phase: s.phase
|
||||
}))
|
||||
const setOptions = useStore((s) => s.setOptions)
|
||||
const startJob = useStore((s) => s.startJob)
|
||||
const cancelJob = useStore((s) => s.cancelJob)
|
||||
const resetJob = useStore((s) => s.resetJob)
|
||||
|
||||
const hasDescriptors = profiles.some((p) => p.descriptors.length > 0)
|
||||
const canRun = !!source && !!outputRoot && phase !== 'running'
|
||||
const running = phase === 'running'
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<h2 className="font-semibold mb-3">3. 실행 옵션</h2>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||
<label className="text-sm">
|
||||
<span className="block text-xs text-slate-500 mb-1">
|
||||
매칭 임계값 ({options.matchThreshold.toFixed(2)})
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0.3}
|
||||
max={0.7}
|
||||
step={0.01}
|
||||
value={options.matchThreshold}
|
||||
onChange={(e) => setOptions({ matchThreshold: Number(e.target.value) })}
|
||||
disabled={running}
|
||||
className="w-full"
|
||||
/>
|
||||
<span className="text-[11px] text-slate-400">낮을수록 엄격</span>
|
||||
</label>
|
||||
|
||||
<label className="text-sm">
|
||||
<span className="block text-xs text-slate-500 mb-1">
|
||||
동시 처리 ({options.concurrency})
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={8}
|
||||
step={1}
|
||||
value={options.concurrency}
|
||||
onChange={(e) => setOptions({ concurrency: Number(e.target.value) })}
|
||||
disabled={running}
|
||||
className="w-full"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="text-sm col-span-2">
|
||||
<span className="block text-xs text-slate-500 mb-1">검출 엔진</span>
|
||||
<select
|
||||
className="w-full border border-slate-300 rounded-lg px-2 py-1.5 text-sm"
|
||||
value={options.detector}
|
||||
onChange={(e) => setOptions({ detector: e.target.value as 'ssd' | 'tiny' })}
|
||||
disabled={running}
|
||||
>
|
||||
<option value="ssd">정확도 우선 (SSD MobileNet)</option>
|
||||
<option value="tiny">속도 우선 (Tiny Face)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!hasDescriptors && (
|
||||
<p className="text-xs text-amber-600 mb-2">
|
||||
⚠️ 등록된 얼굴이 없습니다. 매칭 인물 없이 모두 [미정]으로 분류됩니다.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
{!running ? (
|
||||
<button
|
||||
className="flex-1 bg-brand text-white rounded-lg py-2.5 font-semibold disabled:opacity-40"
|
||||
onClick={startJob}
|
||||
disabled={!canRun}
|
||||
>
|
||||
{phase === 'done' ? '다시 실행' : '정리 시작'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="flex-1 bg-red-500 text-white rounded-lg py-2.5 font-semibold"
|
||||
onClick={cancelJob}
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
)}
|
||||
{phase === 'done' && (
|
||||
<button
|
||||
className="border border-slate-300 rounded-lg px-4 text-sm"
|
||||
onClick={resetJob}
|
||||
>
|
||||
초기화
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user