Add video sorting, reference thumbnails, theme/i18n, menu, DnD/paste, presets

Feature work on top of the initial organizer:

- Videos: .mp4/.mov/.avi/.mkv/.webm/.m4v sorted into output/Movie/YYYY/MM
- Profiles: reference-image thumbnail cards via a secure photoai-media:// protocol
  (serves only registered reference images); per-thumbnail delete; add via
  click, drag & drop, or paste (Ctrl+V) using webUtils.getPathForFile + a
  byte-based addReferenceData path for clipboard images
- Presets: local person library (max 5) saved to userData; one-click apply into
  active profiles; reusing stored descriptors (no recompute)
- Theme: dark/light with dark default (Tailwind class strategy)
- i18n: ko/en table-based localization; first-run onboarding to pick
  language + theme; English-neutral "Unsorted" folder (was [미정])
- App menu: File/Edit/View/Window/Help, localized; Help opens a detailed,
  theme-aware user guide window
- UI: History block scrolls internally (no whole-window scroll);
  threshold/concurrency tooltips; generic example name
- Settings persisted to userData/settings.json; menu + renderer kept in sync
- Docs: NextGen Photo AI feasibility review + Phase 0/1 roadmap

All typecheck/tests (12) /build green; boot smoke verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 15:40:44 +09:00
parent 8a8c10248c
commit 6dce580846
38 changed files with 1916 additions and 212 deletions
+25 -14
View File
@@ -1,5 +1,7 @@
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useStore, wireEvents } from './store'
import { useT } from './i18n'
import { Onboarding } from './components/Onboarding'
import { ProfileManager } from './components/ProfileManager'
import { FolderPicker } from './components/FolderPicker'
import { RunControl } from './components/RunControl'
@@ -8,35 +10,44 @@ import { FileList } from './components/FileList'
import { ReportView } from './components/ReportView'
export default function App(): JSX.Element {
const t = useT()
const phase = useStore((s) => s.phase)
const onboarded = useStore((s) => s.onboarded)
const refreshProfiles = useStore((s) => s.refreshProfiles)
const initSettings = useStore((s) => s.initSettings)
const [ready, setReady] = useState(false)
useEffect(() => {
const unwire = wireEvents()
// 설정 로드(테마 적용 포함) 후 화면 표시
void initSettings().then(() => setReady(true))
void refreshProfiles()
return unwire
}, [refreshProfiles])
}, [refreshProfiles, initSettings])
if (!ready) return <div className="h-full" />
if (!onboarded) return <Onboarding />
return (
<div className="min-h-screen flex flex-col">
<header className="px-6 py-4 bg-white border-b border-slate-200 shadow-sm">
<h1 className="text-xl font-bold text-brand-dark">AI Photo Organizer</h1>
<p className="text-sm text-slate-500">
+ ·
</p>
<div className="h-full flex flex-col">
<header className="px-6 py-4 bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 shadow-sm shrink-0">
<h1 className="text-xl font-bold text-brand-dark dark:text-brand">{t('app.title')}</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">{t('app.subtitle')}</p>
</header>
<main className="flex-1 grid grid-cols-12 gap-4 p-6 overflow-hidden">
{/* 좌측: 설정 패널 */}
<section className="col-span-5 flex flex-col gap-4 overflow-y-auto pr-2">
<main className="flex-1 min-h-0 grid grid-cols-12 gap-4 p-6">
{/* 좌측: 설정 패널 (자체 스크롤) */}
<section className="col-span-5 min-h-0 flex flex-col gap-4 overflow-y-auto pr-2">
<ProfileManager />
<FolderPicker />
<RunControl />
</section>
{/* 우측: 진행/결과 */}
<section className="col-span-7 flex flex-col gap-4 overflow-hidden">
{phase === 'done' ? <ReportView /> : <ProgressView />}
{/* 우측: 진행/결과 — FileList만 내부 스크롤 */}
<section className="col-span-7 min-h-0 flex flex-col gap-4">
<div className="shrink-0">
{phase === 'done' ? <ReportView /> : <ProgressView />}
</div>
<FileList />
</section>
</main>