Phase 5 구현: SQLite DB + History/Dictionary 서비스 + UI

- DB: better-sqlite3 + drizzle-orm (history/dictionary/stats, WAL 모드)
- HistoryService: CRUD + 검색 + 통계 + 30일 보존 정책, 세션 완료 시 자동 저장
- DictionaryService: CRUD + 검색 + 사용 횟수 추적 + STT 프롬프트 힌트
- HistoryPage: 목록 + 검색 + 삭제 + 복사 + 페이지네이션
- DictionaryPage: 목록 + 검색 + 추가 다이얼로그 + 삭제
- DashboardPage: 실데이터 통계 (총/오늘 세션, 시간, 단어수, 연속일수)
- IPC: history/dictionary/stats 핸들러 + preload API
- Bootstrap: DB 초기화 (critical) + 이력 자동 저장 연동
This commit is contained in:
Yun Chan 2026-04-05 02:32:53 +09:00
parent 4a5cf6c819
commit 291e2a29d0
17 changed files with 3111 additions and 35 deletions

View file

@ -1,10 +1,12 @@
// src/renderer/pages/DashboardPage.tsx
import { useState, useEffect } from 'react'
import { Box, Card, CardContent, Typography, Grid } from '@mui/material'
import MicIcon from '@mui/icons-material/Mic'
import TimerIcon from '@mui/icons-material/Timer'
import TextFieldsIcon from '@mui/icons-material/TextFields'
import TodayIcon from '@mui/icons-material/Today'
import type { StatsSummary } from '@shared/types'
interface StatCardProps {
title: string
@ -28,7 +30,33 @@ function StatCard({ title, value, icon }: StatCardProps): React.ReactElement {
)
}
function formatTime(ms: number): string {
const totalSec = Math.round(ms / 1000)
const hours = Math.floor(totalSec / 3600)
const minutes = Math.floor((totalSec % 3600) / 60)
const seconds = totalSec % 60
if (hours > 0) return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
export function DashboardPage(): React.ReactElement {
const [stats, setStats] = useState<StatsSummary | null>(null)
useEffect(() => {
window.electronAPI.stats.getSummary().then((result) => {
if (result.success) setStats(result.data)
})
// 30초마다 갱신
const interval = setInterval(() => {
window.electronAPI.stats.getSummary().then((result) => {
if (result.success) setStats(result.data)
})
}, 30000)
return () => clearInterval(interval)
}, [])
return (
<Box>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 600 }}>
@ -37,30 +65,66 @@ export function DashboardPage(): React.ReactElement {
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<StatCard title="Total Sessions" value="0" icon={<MicIcon />} />
<StatCard
title="Total Sessions"
value={String(stats?.totalSessionCount ?? 0)}
icon={<MicIcon />}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<StatCard title="Total Time" value="0:00" icon={<TimerIcon />} />
<StatCard
title="Total Time"
value={formatTime(stats?.totalRecordingTimeMs ?? 0)}
icon={<TimerIcon />}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<StatCard title="Total Words" value="0" icon={<TextFieldsIcon />} />
<StatCard
title="Total Words"
value={String(stats?.totalWordCount ?? 0)}
icon={<TextFieldsIcon />}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<StatCard title="Streak" value="0 days" icon={<TodayIcon />} />
<StatCard
title="Streak"
value={`${stats?.streakDays ?? 0} days`}
icon={<TodayIcon />}
/>
</Grid>
</Grid>
<Box sx={{ mt: 4 }}>
{/* Today's stats */}
<Box sx={{ mt: 3 }}>
<Typography variant="h6" sx={{ mb: 2 }}>
Recent Sessions
Today
</Typography>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 4 }}>
No sessions yet. Press the hotkey to start recording.
</Typography>
</CardContent>
</Card>
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 4 }}>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">Sessions</Typography>
<Typography variant="h5">{stats?.todaySessionCount ?? 0}</Typography>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 4 }}>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">Time</Typography>
<Typography variant="h5">{formatTime(stats?.todayRecordingTimeMs ?? 0)}</Typography>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 4 }}>
<Card>
<CardContent>
<Typography variant="body2" color="text.secondary">Words</Typography>
<Typography variant="h5">{stats?.todayWordCount ?? 0}</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
</Box>
)