// src/renderer/pages/HistoryPage.tsx // 인스트루먼트 미학: MetalCard + PhosphorText + Led + 날짜 그룹핑 // Phase 10: 태그 필터링 + 태그 관리 통합 import { useState, useEffect, useCallback, useMemo } from 'react' import { Box, Chip, IconButton, Tooltip } from '@mui/material' import FileDownloadIcon from '@mui/icons-material/FileDownload' import { PhosphorText } from '@d3ro/ui/components/ds' import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius } from '@d3ro/ui/theme' import { useI18n } from '@d3ro/i18n' import { getDateKey } from '../utils/formatters' import { EmptyStateCard, SearchInput, PageHeader, HistoryEntryCard } from '../components/shared' import type { HistoryEntry, HistoryPage as HistoryPageData, TagCount } from '@d3ro/core/types' const PAGE_SIZE = 50 export function HistoryPage(): React.ReactElement { const { t, formatRelativeDate } = useI18n() const [data, setData] = useState(null) const [search, setSearch] = useState('') const [loading, setLoading] = useState(true) const [allTags, setAllTags] = useState([]) const [activeTag, setActiveTag] = useState(null) const loadTags = useCallback(async () => { const result = await window.electronAPI.memo.getAllTags() if (result.success) setAllTags(result.data) }, []) const loadData = useCallback(async () => { setLoading(true) let result: { success: boolean; data: HistoryPageData } | { success: false; error: unknown } if (activeTag) { result = await window.electronAPI.memo.searchByTag({ tag: activeTag, page: 0, pageSize: PAGE_SIZE }) } else if (search.trim()) { result = await window.electronAPI.history.search({ query: search, page: 0, pageSize: PAGE_SIZE }) } else { result = await window.electronAPI.history.getAll({ page: 0, pageSize: PAGE_SIZE, sortOrder: 'desc' }) } if (result.success) setData(result.data) setLoading(false) }, [search, activeTag]) useEffect(() => { loadData() loadTags() const unsub = window.electronAPI.app.onDataChanged(() => { loadData(); loadTags() }) return unsub }, [loadData, loadTags]) const groupedEntries = useMemo(() => { if (!data) return [] const groups: Record = {} for (const entry of data.entries) { const key = getDateKey(entry.createdAt) if (!groups[key]) { groups[key] = { label: formatRelativeDate(entry.createdAt), entries: [] } } groups[key].entries.push(entry) } return Object.values(groups) }, [data, formatRelativeDate]) const handleCopy = useCallback((text: string) => { navigator.clipboard.writeText(text) }, []) const handleDelete = useCallback((id: string) => { window.electronAPI.history.delete({ id }) loadData() }, [loadData]) const handleTagClick = useCallback((tag: string) => { setActiveTag(prev => prev === tag ? null : tag) setSearch('') }, []) const handleExport = useCallback(async () => { await window.electronAPI.memo.export({ format: 'markdown' as const, tag: activeTag ?? undefined, }) }, [activeTag]) return ( {/* 페이지 헤더 -- 각인 스타일 */} {t('history.entries', { count: data?.total ?? 0 }).toUpperCase()} } /> {/* 태그 필터 바 */} {allTags.length > 0 && ( {allTags.map(tc => ( handleTagClick(tc.tag)} sx={{ height: 22, fontFamily: d3roFontMono, fontSize: d3roTypo.micro.size, borderRadius: d3roRadius.small, borderColor: activeTag === tc.tag ? d3roPalette.accent.amber : d3roPalette.border.subtle, bgcolor: activeTag === tc.tag ? d3roPalette.accent.amber : 'transparent', color: activeTag === tc.tag ? d3roPalette.bg.card : d3roPalette.text.secondary, '&:hover': { bgcolor: activeTag === tc.tag ? d3roPalette.accent.amber : d3roPalette.bg.cardHover, }, }} /> ))} {activeTag && ( setActiveTag(null)} sx={{ height: 22, fontFamily: d3roFontMono, fontSize: d3roTypo.micro.size, borderRadius: d3roRadius.small, color: d3roPalette.text.muted, '&:hover': { color: d3roPalette.tag.red }, }} /> )} )} {!activeTag && ( )} {loading ? ( {t('common.loading').toUpperCase()} ) : !data || data.entries.length === 0 ? ( ) : ( groupedEntries.map((group) => ( {/* 날짜 구분자 */} {group.label} {t('history.count', { count: group.entries.length })} {/* 항목 — 반응형 그리드 */} {group.entries.map((entry: HistoryEntry) => ( ))} )) )} ) }