feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped

This commit is contained in:
Yun Chan 2026-08-20 11:12:05 +09:00
parent 5cd1de6859
commit 708e20f747
406 changed files with 42464 additions and 6199 deletions

View file

@ -1,12 +1,11 @@
// src/renderer/pages/HistoryPage.tsx
// 인스트루먼트 미학: MetalCard + PhosphorText + Led + 날짜 그룹핑
// Phase 10: 태그 필터링 + 태그 관리 통합
// High-End Agency Dictation History & Memory Timeline
import { useState, useEffect, useCallback, useMemo } from 'react'
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { Box, Chip, IconButton, Tooltip } from '@mui/material'
import { Download } from 'lucide-react'
import { PhosphorText } from '@d3ro/ui/components/ds'
import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius } from '@d3ro/ui/theme'
import { Download, Trash2, Tag as TagIcon, Clock, Filter } from 'lucide-react'
import { PhosphorText, TactileBadge, DoubleBezelCard, PhysicalButton } from '@d3ro/ui/components/ds'
import { d3roPalette, d3roTypo, d3roFontMono, d3roFontSans, d3roRadius } from '@d3ro/ui/theme'
import { useI18n } from '@d3ro/i18n'
import { getDateKey } from '../utils/formatters'
import { EmptyStateCard, SearchInput, PageHeader, HistoryEntryCard } from '../components/shared'
@ -45,7 +44,10 @@ export function HistoryPage(): React.ReactElement {
useEffect(() => {
loadData()
loadTags()
const unsub = window.electronAPI.app.onDataChanged(() => { loadData(); loadTags() })
const unsub = window.electronAPI.app.onDataChanged(() => {
loadData()
loadTags()
})
return unsub
}, [loadData, loadTags])
@ -66,126 +68,164 @@ export function HistoryPage(): React.ReactElement {
navigator.clipboard.writeText(text)
}, [])
const handleDelete = useCallback((id: string) => {
window.electronAPI.history.delete({ id })
loadData()
}, [loadData])
const isExportingRef = useRef(false)
const isDeletingAllRef = useRef(false)
const isDeletingSingleRef = useRef(false)
const handleDelete = useCallback(
async (id: string) => {
if (isDeletingSingleRef.current) return
isDeletingSingleRef.current = true
try {
await window.electronAPI.history.delete({ id })
loadData()
} finally {
isDeletingSingleRef.current = false
}
},
[loadData],
)
const handleTagClick = useCallback((tag: string) => {
setActiveTag(prev => prev === tag ? null : tag)
setActiveTag((prev) => (prev === tag ? null : tag))
setSearch('')
}, [])
const handleExport = useCallback(async () => {
await window.electronAPI.memo.export({
format: 'markdown' as const,
tag: activeTag ?? undefined,
})
if (isExportingRef.current) return
isExportingRef.current = true
try {
await window.electronAPI.memo.export({
format: 'markdown' as const,
tag: activeTag ?? undefined,
})
} finally {
isExportingRef.current = false
}
}, [activeTag])
const handleDeleteAll = useCallback(async () => {
if (isDeletingAllRef.current) return
isDeletingAllRef.current = true
try {
await window.electronAPI.history.deleteAll()
loadData()
} finally {
isDeletingAllRef.current = false
}
}, [loadData])
return (
<Box sx={{ mx: 'auto', p: 4, overflow: 'hidden' }}>
{/* 페이지 헤더 -- 각인 스타일 */}
<Box sx={{ maxWidth: 1060, mx: 'auto', p: { xs: 2.5, md: 4 }, pb: 10 }}>
{/* Page Header */}
<PageHeader
title={t('history.title').toUpperCase()}
title={t('history.title')}
count={t('history.entries', { count: data?.total ?? 0 })}
action={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25 }}>
<Tooltip title={t('memo.export')} arrow>
<PhysicalButton size="small" onClick={handleExport} sx={{ height: 34, px: 1.5 }}>
<Download size={14} style={{ marginRight: 4 }} />
{t('memo.export')}
</PhysicalButton>
</Tooltip>
<Tooltip title={t('common.clearAll') || '모두 지우기'} arrow>
<IconButton
size="small"
onClick={handleExport}
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.main } }}
data-testid="clear-all-history-button"
onClick={handleDeleteAll}
sx={{
color: d3roPalette.text.inactive,
p: 0.75,
bgcolor: d3roPalette.glass.raised,
border: `1px solid ${d3roPalette.glass.hairline}`,
'&:hover': { color: d3roPalette.tag.red, bgcolor: d3roPalette.tag.redBg },
}}
>
<Download size={18} />
<Trash2 size={16} />
</IconButton>
</Tooltip>
<PhosphorText variant="meta" sx={{ color: d3roPalette.text.dimLabel }}>
{t('history.entries', { count: data?.total ?? 0 }).toUpperCase()}
</PhosphorText>
</Box>
}
/>
{/* 태그 필터 바 */}
{allTags.length > 0 && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mb: 2 }}>
{allTags.map(tc => (
<Chip
key={tc.tag}
label={`#${tc.tag} (${tc.count})`}
size="small"
variant={activeTag === tc.tag ? 'filled' : 'outlined'}
onClick={() => handleTagClick(tc.tag)}
sx={{
height: 22,
fontFamily: d3roFontMono,
fontSize: d3roTypo.micro.size,
borderRadius: d3roRadius.small,
borderColor: activeTag === tc.tag ? d3roPalette.accent.main : d3roPalette.border.subtle,
bgcolor: activeTag === tc.tag ? d3roPalette.accent.main : 'transparent',
color: activeTag === tc.tag ? d3roPalette.bg.card : d3roPalette.text.secondary,
'&:hover': {
bgcolor: activeTag === tc.tag ? d3roPalette.accent.main : d3roPalette.bg.cardHover,
},
}}
/>
))}
{activeTag && (
<Chip
label={t('memo.clearFilter')}
size="small"
onClick={() => setActiveTag(null)}
sx={{
height: 22,
fontFamily: d3roFontMono,
fontSize: d3roTypo.micro.size,
borderRadius: d3roRadius.small,
color: d3roPalette.text.muted,
'&:hover': { color: d3roPalette.tag.red },
}}
/>
)}
</Box>
)}
{/* Search Input Bar */}
{!activeTag && (
<SearchInput
value={search}
onChange={setSearch}
placeholder={t('history.search').toUpperCase()}
placeholder={t('history.search')}
/>
)}
{/* Tag Filters Bar */}
{allTags.length > 0 && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 1, mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mr: 0.5, color: d3roPalette.text.dimLabel }}>
<Filter size={12} />
<PhosphorText variant="meta">{t('memo.tags') || 'TAGS'}:</PhosphorText>
</Box>
{allTags.map((tc) => {
const isSelected = activeTag === tc.tag
return (
<TactileBadge
key={tc.tag}
mono
tone={isSelected ? 'accent' : 'default'}
onClick={() => handleTagClick(tc.tag)}
sx={{ cursor: 'pointer' }}
>
#{tc.tag} <Box component="span" sx={{ opacity: 0.6 }}>({tc.count})</Box>
</TactileBadge>
)
})}
{activeTag && (
<TactileBadge
tone="error"
onClick={() => setActiveTag(null)}
sx={{ cursor: 'pointer' }}
>
{t('memo.clearFilter')}
</TactileBadge>
)}
</Box>
)}
{loading ? (
<PhosphorText variant="dim">{t('common.loading').toUpperCase()}</PhosphorText>
<Box sx={{ py: 6, textAlign: 'center' }}>
<PhosphorText variant="dim">{t('common.loading')}</PhosphorText>
</Box>
) : !data || data.entries.length === 0 ? (
<EmptyStateCard
message={search ? t('history.noResults').toUpperCase() : t('history.noHistory').toUpperCase()}
message={search ? t('history.noResults') : t('history.noHistory')}
icon={<Clock />}
/>
) : (
groupedEntries.map((group) => (
<Box key={group.label} sx={{ mb: 3 }}>
{/* 날짜 구분자 */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1.5 }}>
<Box key={group.label} sx={{ mb: 3.5 }}>
{/* Date Group Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1.75 }}>
<PhosphorText variant="label" sx={{ color: d3roPalette.text.dimLabel, whiteSpace: 'nowrap' }}>
{group.label}
</PhosphorText>
<Box sx={{ flex: 1, height: '1px', bgcolor: d3roPalette.border.subtle }} />
<Box sx={{ flex: 1, height: '1px', bgcolor: d3roPalette.glass.hairline }} />
<PhosphorText variant="dim" sx={{ fontSize: d3roTypo.label.size }}>
{t('history.count', { count: group.entries.length })}
</PhosphorText>
</Box>
{/* 항목 — 반응형 그리드 */}
<Box sx={{
display: 'grid',
gridTemplateColumns: {
xs: '1fr',
sm: 'repeat(2, 1fr)',
md: 'repeat(3, 1fr)',
},
gap: 1.5,
}}>
{/* Entries Grid */}
<Box
sx={{
display: 'grid',
gridTemplateColumns: {
xs: '1fr',
md: 'repeat(2, 1fr)',
},
gap: 1.75,
}}
>
{group.entries.map((entry: HistoryEntry) => (
<HistoryEntryCard
key={entry.id}