Files
photoai/src/renderer/components/ReportView.tsx
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

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