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>
61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
import { useStore } from '../store'
|
|
|
|
function fmtDuration(ms: number): string {
|
|
const s = Math.round(ms / 1000)
|
|
const m = Math.floor(s / 60)
|
|
const rem = s % 60
|
|
return m > 0 ? `${m}분 ${rem}초` : `${rem}초`
|
|
}
|
|
|
|
/** 잡 완료 후 결과 리포트 */
|
|
export function ReportView(): JSX.Element {
|
|
const report = useStore((s) => s.report)
|
|
const errors = useStore((s) => s.errors)
|
|
if (!report) return <></>
|
|
|
|
const stats = [
|
|
{ label: '총 처리', value: report.total, cls: 'text-slate-700' },
|
|
{ label: '이동', value: report.moved, cls: 'text-emerald-600' },
|
|
{ label: '복사', value: report.copied, cls: 'text-sky-600' },
|
|
{ label: '미정', value: report.unmatched, cls: 'text-slate-500' },
|
|
{ label: '실패', value: report.failed, cls: 'text-red-600' }
|
|
]
|
|
|
|
return (
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h2 className="font-semibold">✅ 작업 완료</h2>
|
|
<span className="text-sm text-slate-500">소요 {fmtDuration(report.elapsedMs)}</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-5 gap-2 mb-3">
|
|
{stats.map((s) => (
|
|
<div key={s.label} className="bg-slate-50 rounded-lg p-2 text-center">
|
|
<div className={`text-lg font-bold ${s.cls}`}>{s.value}</div>
|
|
<div className="text-[11px] text-slate-400">{s.label}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="text-xs text-slate-400 mono truncate" title={report.logPath}>
|
|
로그: {report.logPath}
|
|
</div>
|
|
|
|
{errors.length > 0 && (
|
|
<details className="mt-3">
|
|
<summary className="text-xs text-red-600 cursor-pointer">
|
|
오류 {errors.length}건 보기
|
|
</summary>
|
|
<ul className="mt-1 max-h-32 overflow-y-auto text-[11px] text-red-500 mono">
|
|
{errors.map((e, i) => (
|
|
<li key={i} className="truncate">
|
|
{e.file}: {e.message}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</details>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|