Files
photoai/src/renderer/App.tsx
T
koriweb 9b044449a0 Birthday & anniversary photo collections
- profiles get an optional birthday (MM-DD); photos of that person taken on the
  date are also copied into Birthdays/<person>/<year>/
- app-wide anniversaries (label + MM-DD); any photo taken on the date is copied
  into Anniversaries/<label>/<year>/ (including faceless photos and videos)
- copy (not move) so normal person/date sorting is preserved
- CaptureDate gains day; new collection path builder; scanner skips the new folders
- UI: birthday input in profile create/edit + new Anniversaries manager

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 20:11:29 +09:00

149 lines
6.1 KiB
TypeScript

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 { AnniversaryManager } from './components/AnniversaryManager'
import { FolderPicker } from './components/FolderPicker'
import { RunControl } from './components/RunControl'
import { ProgressView } from './components/ProgressView'
import { FileList } from './components/FileList'
import { ReportView } from './components/ReportView'
import { LibraryView } from './components/LibraryView'
import { SearchView } from './components/SearchView'
import { GroupsView } from './components/GroupsView'
import { MapView } from './components/MapView'
import { FileExplorer } from './components/FileExplorer'
import { Overlays } from './overlays'
import type { AppView } from './store'
export default function App(): JSX.Element {
const t = useT()
const phase = useStore((s) => s.phase)
const view = useStore((s) => s.view)
const setView = useStore((s) => s.setView)
const easyMode = useStore((s) => s.easyMode)
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, initSettings])
if (!ready) return <div className="h-full" />
if (!onboarded) return <Onboarding />
const tabs: { id: AppView; label: string; easyLabel: string; icon: string }[] = [
{ id: 'organize', label: t('nav.organize'), easyLabel: t('easynav.organize'), icon: '📂' },
{ id: 'library', label: t('nav.library'), easyLabel: t('easynav.library'), icon: '🖼️' },
{ id: 'search', label: t('nav.search'), easyLabel: t('easynav.search'), icon: '🔍' },
{ id: 'map', label: t('nav.map'), easyLabel: t('easynav.map'), icon: '🗺️' },
{ id: 'groups', label: t('nav.groups'), easyLabel: t('easynav.groups'), icon: '🧹' }
]
return (
<div className="h-full flex flex-col">
{easyMode ? (
/* 쉬운 모드: 대형 아이콘+구어체 버튼 네비 */
<header className="px-5 py-3 bg-slate-100 dark:bg-slate-800 border-b border-slate-300 dark:border-slate-700 shrink-0">
<nav className="grid grid-cols-5 gap-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setView(tab.id)}
className={`flex flex-col items-center gap-1 rounded-md py-3 border transition-colors ${
view === tab.id
? 'border-brand bg-brand/15 text-brand'
: 'border-slate-300 dark:border-slate-700 text-slate-600 dark:text-slate-300 hover:border-brand'
}`}
>
<span className="text-2xl leading-none">{tab.icon}</span>
<span className="text-base font-semibold">{tab.easyLabel}</span>
</button>
))}
</nav>
</header>
) : (
/* darktable식 컴팩트 상단바: 좌측 로고 · 우측 파이프 구분 탭 */
<header className="h-11 px-4 bg-slate-100 dark:bg-slate-800 border-b border-slate-300 dark:border-slate-700 shrink-0 flex items-center justify-between">
<div className="flex items-center gap-2 select-none">
<span className="text-brand text-base leading-none"></span>
<span className="text-sm font-semibold tracking-widest lowercase text-slate-600 dark:text-slate-300">
photoai
</span>
</div>
<nav className="flex items-center">
{tabs.map((tab, i) => (
<span key={tab.id} className="flex items-center">
{i > 0 && <span className="text-slate-300 dark:text-slate-600 px-1.5">|</span>}
<button
onClick={() => setView(tab.id)}
className={`text-sm tracking-wide transition-colors ${
view === tab.id
? 'text-brand font-semibold'
: 'text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-100'
}`}
>
{tab.label}
</button>
</span>
))}
</nav>
</header>
)}
{view === 'organize' ? (
<main className="flex-1 min-h-0 flex">
{/* 좌측: 파일 탐색기 사이드바 */}
<aside className="w-56 shrink-0 bg-slate-100 dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700">
<FileExplorer />
</aside>
<div className="flex-1 min-w-0 min-h-0 grid grid-cols-12 gap-3 p-4">
{/* 설정 패널 (자체 스크롤) */}
<section className="col-span-5 min-h-0 flex flex-col gap-4 overflow-y-auto pr-2">
<ProfileManager />
<AnniversaryManager />
<FolderPicker />
<RunControl />
</section>
{/* 진행/결과 — 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>
</div>
</main>
) : view === 'library' ? (
<main className="flex-1 min-h-0">
<LibraryView />
</main>
) : view === 'search' ? (
<main className="flex-1 min-h-0 overflow-y-auto p-4">
<SearchView />
</main>
) : view === 'map' ? (
<main className="flex-1 min-h-0 overflow-y-auto p-4">
<MapView />
</main>
) : (
<main className="flex-1 min-h-0 overflow-y-auto p-4">
<GroupsView />
</main>
)}
{/* 토스트 / 확인 모달 호스트 (전역) */}
<Overlays />
</div>
)
}