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
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:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue