Phase 8 마무리: HistoryPage 날짜 그룹핑 + AppConfig 음성 모드 필드
- HistoryPage: 평면 리스트 → TODAY/YESTERDAY/날짜 그룹핑 (DashboardPage와 동일 패턴) - AppConfig: dictationEnabled, agentModeEnabled, handsFreeEnabled 필드 추가 - ConfigService: 기본값 설정 (dictation=true, agent=false, handsFree=false) - recording-tip: 에러 아이콘 색상 #D32F2F → #ef4444 (d3roPalette.tag.red)
This commit is contained in:
parent
28371a9d1a
commit
5741d55c0b
4 changed files with 98 additions and 41 deletions
|
|
@ -55,7 +55,10 @@ const CONFIG_DEFAULTS: AppConfig = {
|
|||
hotkeyEnabled: true,
|
||||
insertMethod: 'clipboard',
|
||||
autoInsert: true,
|
||||
maxHistoryEntries: 1000
|
||||
maxHistoryEntries: 1000,
|
||||
dictationEnabled: true,
|
||||
agentModeEnabled: false,
|
||||
handsFreeEnabled: false,
|
||||
}
|
||||
|
||||
let store: ElectronStore<AppConfig> | null = null
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// src/renderer/pages/HistoryPage.tsx
|
||||
// 인스트루먼트 미학: MetalCard + PhosphorText + Led
|
||||
// 인스트루먼트 미학: MetalCard + PhosphorText + Led + 날짜 그룹핑
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { Box, TextField, IconButton, InputAdornment } from '@mui/material'
|
||||
import SearchIcon from '@mui/icons-material/Search'
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
|
|
@ -10,10 +10,10 @@ import { MetalCard, PhosphorText, Led } from '../components/ds'
|
|||
import { d3roPalette, d3roFontMono } from '../theme'
|
||||
import type { HistoryEntry, HistoryPage as HistoryPageData } from '@shared/types'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
function formatDate(ts: number): string {
|
||||
return new Date(ts).toLocaleString('ko-KR', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
||||
function formatTime(ts: number): string {
|
||||
return new Date(ts).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function formatDuration(sec: number): string {
|
||||
|
|
@ -22,6 +22,24 @@ function formatDuration(sec: number): string {
|
|||
return `${m}:${s.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function getDateKey(ts: number): string {
|
||||
const d = new Date(ts)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function getDateLabel(ts: number): string {
|
||||
const d = new Date(ts)
|
||||
d.setHours(0, 0, 0, 0)
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const yesterday = new Date(today)
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
|
||||
if (d.getTime() === today.getTime()) return 'TODAY'
|
||||
if (d.getTime() === yesterday.getTime()) return 'YESTERDAY'
|
||||
return d.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' }).toUpperCase()
|
||||
}
|
||||
|
||||
export function HistoryPage(): React.ReactElement {
|
||||
const [data, setData] = useState<HistoryPageData | null>(null)
|
||||
const [search, setSearch] = useState('')
|
||||
|
|
@ -31,13 +49,27 @@ export function HistoryPage(): React.ReactElement {
|
|||
setLoading(true)
|
||||
const result = search.trim()
|
||||
? await window.electronAPI.history.search({ query: search, page: 0, pageSize: PAGE_SIZE })
|
||||
: await window.electronAPI.history.getAll({ page: 0, pageSize: PAGE_SIZE })
|
||||
: await window.electronAPI.history.getAll({ page: 0, pageSize: PAGE_SIZE, sortOrder: 'desc' })
|
||||
if (result.success) setData(result.data)
|
||||
setLoading(false)
|
||||
}, [search])
|
||||
|
||||
useEffect(() => { loadData() }, [loadData])
|
||||
|
||||
// 날짜별 그룹핑
|
||||
const groupedEntries = useMemo(() => {
|
||||
if (!data) return []
|
||||
const groups: Record<string, { label: string; entries: HistoryEntry[] }> = {}
|
||||
for (const entry of data.entries) {
|
||||
const key = getDateKey(entry.createdAt)
|
||||
if (!groups[key]) {
|
||||
groups[key] = { label: getDateLabel(entry.createdAt), entries: [] }
|
||||
}
|
||||
groups[key].entries.push(entry)
|
||||
}
|
||||
return Object.values(groups)
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: 900, mx: 'auto', p: 4 }}>
|
||||
<PhosphorText variant="label" sx={{ mb: 2, display: 'block', color: d3roPalette.text.inactive }}>
|
||||
|
|
@ -69,8 +101,22 @@ export function HistoryPage(): React.ReactElement {
|
|||
</Box>
|
||||
</MetalCard>
|
||||
) : (
|
||||
groupedEntries.map((group) => (
|
||||
<Box key={group.label} sx={{ mb: 3 }}>
|
||||
{/* 날짜 구분자 */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1.5 }}>
|
||||
<PhosphorText variant="label" sx={{ color: d3roPalette.text.dimLabel, whiteSpace: 'nowrap' }}>
|
||||
{group.label}
|
||||
</PhosphorText>
|
||||
<Box sx={{ flex: 1, height: '1px', bgcolor: d3roPalette.border.subtle }} />
|
||||
<PhosphorText variant="dim" sx={{ fontSize: '10px' }}>
|
||||
{group.entries.length}건
|
||||
</PhosphorText>
|
||||
</Box>
|
||||
|
||||
{/* 항목 */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{data.entries.map((entry: HistoryEntry) => (
|
||||
{group.entries.map((entry: HistoryEntry) => (
|
||||
<MetalCard key={entry.id}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||
<Led color={entry.status === 'completed' ? 'green' : 'red'} size={6} />
|
||||
|
|
@ -83,7 +129,7 @@ export function HistoryPage(): React.ReactElement {
|
|||
{entry.polishedText || entry.originalText}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 2, mt: 1, fontFamily: d3roFontMono, fontSize: '10px', color: d3roPalette.text.dimLabel, letterSpacing: '0.5px' }}>
|
||||
<span>{formatDate(entry.createdAt)}</span>
|
||||
<span>{formatTime(entry.createdAt)}</span>
|
||||
<span>{formatDuration(entry.duration)}</span>
|
||||
{entry.detectedLanguage && <span>{entry.detectedLanguage.toUpperCase()}</span>}
|
||||
<span>{entry.mode.toUpperCase()}</span>
|
||||
|
|
@ -103,6 +149,8 @@ export function HistoryPage(): React.ReactElement {
|
|||
</MetalCard>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ body {
|
|||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #D32F2F;
|
||||
background: #ef4444; /* d3roPalette.tag.red */
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
|
|
|
|||
|
|
@ -374,6 +374,12 @@ export interface AppConfig {
|
|||
insertMethod: 'clipboard' | 'keyboard'
|
||||
autoInsert: boolean
|
||||
maxHistoryEntries: number
|
||||
/** 받아쓰기 모드 활성화 (hold-to-talk) */
|
||||
dictationEnabled: boolean
|
||||
/** Agent 모드 활성화 (더블프레스, dictation 의존) */
|
||||
agentModeEnabled: boolean
|
||||
/** 핸즈프리 모드 활성화 (토글) */
|
||||
handsFreeEnabled: boolean
|
||||
}
|
||||
|
||||
export interface ConfigGetParams {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue