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,12 @@
// src/renderer/pages/DictionaryPage.tsx
// 인스트루먼트 미학: MetalCard + PhosphorText + 타이포 토큰
// Precision Vocabulary & Phonetic Dictionary Editor
import { useState, useEffect, useCallback } from 'react'
import { Box, TextField, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'
import { Plus, Trash2, Pencil } from 'lucide-react'
import { MetalCard, PhosphorText } from '@d3ro/ui/components/ds'
import React, { useState, useEffect, useCallback, useRef } from 'react'
import { Box, TextField, Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Tooltip } from '@mui/material'
import { Plus, Trash2, Pencil, BookA, Sparkles } from 'lucide-react'
import { MetalCard, PhosphorText, PhysicalButton, TactileBadge } from '@d3ro/ui/components/ds'
import { PageHeader, SearchInput, EmptyStateCard } from '../components/shared'
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
import { d3roPalette, d3roFontSans, d3roFontMono, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
import { useI18n } from '@d3ro/i18n'
import type { DictionaryEntry, DictionaryPage as DictPageData } from '@d3ro/core/types'
@ -21,19 +21,23 @@ export function DictionaryPage(): React.ReactElement {
const [editId, setEditId] = useState<string | null>(null)
const [formWord, setFormWord] = useState('')
const [formPronunciation, setFormPronunciation] = useState('')
const [saving, setSaving] = useState(false)
const isSavingRef = useRef(false)
const isDeletingRef = useRef(false)
const loadData = useCallback(async () => {
setLoading(true)
const result = search.trim()
? await window.electronAPI.dictionary.search({ query: search, page: 0, pageSize: PAGE_SIZE })
: await window.electronAPI.dictionary.getAll({ page: 0, pageSize: PAGE_SIZE })
const result = await window.electronAPI.dictionary.getAll({ search: search || undefined })
if (result.success) setData(result.data)
setLoading(false)
}, [search])
useEffect(() => { loadData() }, [loadData])
useEffect(() => {
loadData()
}, [loadData])
const openAdd = () => {
if (dialogOpen) return
setEditId(null)
setFormWord('')
setFormPronunciation('')
@ -41,6 +45,7 @@ export function DictionaryPage(): React.ReactElement {
}
const openEdit = (entry: DictionaryEntry) => {
if (dialogOpen) return
setEditId(entry.id)
setFormWord(entry.word)
setFormPronunciation(entry.pronunciation ?? '')
@ -48,76 +53,142 @@ export function DictionaryPage(): React.ReactElement {
}
const handleSave = async () => {
if (!formWord.trim()) return
if (editId) {
await window.electronAPI.dictionary.update({
id: editId,
word: formWord.trim(),
pronunciation: formPronunciation.trim() || undefined,
})
} else {
await window.electronAPI.dictionary.add({
word: formWord.trim(),
pronunciation: formPronunciation.trim() || undefined,
})
if (!formWord.trim() || saving || isSavingRef.current) return
isSavingRef.current = true
setSaving(true)
try {
if (editId) {
await window.electronAPI.dictionary.update({
id: editId,
word: formWord.trim(),
pronunciation: formPronunciation.trim() || undefined,
})
} else {
await window.electronAPI.dictionary.add({
word: formWord.trim(),
pronunciation: formPronunciation.trim() || undefined,
})
}
setDialogOpen(false)
loadData()
} finally {
setSaving(false)
isSavingRef.current = false
}
}
const handleDelete = async (id: string) => {
if (isDeletingRef.current) return
isDeletingRef.current = true
try {
await window.electronAPI.dictionary.delete({ id })
loadData()
} finally {
isDeletingRef.current = false
}
setDialogOpen(false)
loadData()
}
return (
<Box sx={{ maxWidth: 900, mx: 'auto', p: 4 }}>
<Box sx={{ maxWidth: 1060, mx: 'auto', p: { xs: 2.5, md: 4 }, pb: 10 }}>
<PageHeader
title={t('dictionary.title').toUpperCase()}
count={t('dictionary.words', { count: data?.total ?? 0 }).toUpperCase()}
title={t('dictionary.title')}
count={t('dictionary.words', { count: data?.total ?? 0 })}
action={
<Button variant="contained" startIcon={<Plus size={16} />} onClick={openAdd} size="small">
{t('dictionary.add').toUpperCase()}
</Button>
<PhysicalButton tone="accent" onClick={openAdd} size="small" trailingIcon={<Plus size={14} />}>
{t('dictionary.add')}
</PhysicalButton>
}
/>
<SearchInput
placeholder={t('dictionary.search').toUpperCase()}
placeholder={t('dictionary.search')}
value={search}
onChange={setSearch}
/>
{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('dictionary.noResults').toUpperCase() : t('dictionary.noWords').toUpperCase()} />
<EmptyStateCard
message={search ? t('dictionary.noResults') : t('dictionary.noWords')}
icon={<BookA />}
action={
<PhysicalButton tone="accent" onClick={openAdd}>
<Plus size={15} style={{ marginRight: 4 }} />
{t('dictionary.add')}
</PhysicalButton>
}
/>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 1.75 }}>
{data.entries.map((entry: DictionaryEntry) => (
<MetalCard key={entry.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box>
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 1.5 }}>
<Box sx={{ fontSize: d3roTypo.body.size, fontWeight: d3roTypo.heading.weight, color: d3roPalette.text.primary }}>{entry.word}</Box>
<MetalCard key={entry.id} sx={{ p: 2.5 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 2 }}>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, mb: 0.75, flexWrap: 'wrap' }}>
<Box
sx={{
fontSize: d3roTypo.heading.size,
fontWeight: 700,
fontFamily: d3roFontSans,
color: d3roPalette.text.primary,
}}
>
{entry.word}
</Box>
{entry.pronunciation && (
<Box sx={{ fontFamily: d3roFontMono, fontSize: d3roTypo.meta.size, color: d3roPalette.text.dimLabel }}>[{entry.pronunciation}]</Box>
<TactileBadge mono tone="accent">
[{entry.pronunciation}]
</TactileBadge>
)}
</Box>
<Box sx={{
fontFamily: d3roFontMono,
fontSize: d3roTypo.label.size,
color: d3roPalette.text.inactive,
mt: 0.5,
letterSpacing: d3roTypo.label.spacing,
}}>
{entry.category.toUpperCase()} · {t('dictionary.used', { count: entry.usageCount })}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, mt: 1 }}>
<TactileBadge mono tone="mono">
{entry.category.toUpperCase()}
</TactileBadge>
<PhosphorText variant="meta" sx={{ color: d3roPalette.text.dimLabel }}>
{t('dictionary.used', { count: entry.usageCount })}
</PhosphorText>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 0.5 }}>
<IconButton size="small" onClick={() => openEdit(entry)}
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.main } }}>
<Pencil size={16} />
</IconButton>
<IconButton size="small" onClick={() => { window.electronAPI.dictionary.delete({ id: entry.id }); loadData() }}
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
<Trash2 size={16} />
</IconButton>
<Tooltip title={t('common.edit')}>
<IconButton
size="small"
onClick={() => openEdit(entry)}
sx={{
p: 0.6,
color: d3roPalette.text.inactive,
bgcolor: d3roPalette.glass.raised,
border: `1px solid ${d3roPalette.glass.hairline}`,
'&:hover': { color: d3roPalette.accent.light, bgcolor: d3roPalette.glass.hairlineStrong },
}}
>
<Pencil size={15} />
</IconButton>
</Tooltip>
<Tooltip title={t('common.delete')}>
<IconButton
size="small"
onClick={() => {
window.electronAPI.dictionary.delete({ id: entry.id })
loadData()
}}
sx={{
p: 0.6,
color: d3roPalette.text.inactive,
bgcolor: d3roPalette.glass.raised,
border: `1px solid ${d3roPalette.glass.hairline}`,
'&:hover': { color: d3roPalette.tag.red, bgcolor: d3roPalette.tag.redBg },
}}
>
<Trash2 size={15} />
</IconButton>
</Tooltip>
</Box>
</Box>
</MetalCard>
@ -125,15 +196,50 @@ export function DictionaryPage(): React.ReactElement {
</Box>
)}
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="xs" fullWidth>
<DialogTitle sx={{ fontWeight: 700 }}>{editId ? t('dictionary.editTitle') : t('dictionary.addTitle')}</DialogTitle>
<DialogContent>
<TextField label={t('dictionary.word')} value={formWord} onChange={(e) => setFormWord(e.target.value)} fullWidth autoFocus sx={{ mt: 1 }} />
<TextField label={t('dictionary.pronunciation')} value={formPronunciation} onChange={(e) => setFormPronunciation(e.target.value)} fullWidth sx={{ mt: 2 }} />
{/* Add / Edit Dialog */}
<Dialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
maxWidth="xs"
fullWidth
PaperProps={{
sx: {
bgcolor: d3roPalette.bg.card,
borderRadius: d3roRadius.doubleBezelOuter,
border: `1px solid ${d3roPalette.glass.hairlineStrong}`,
boxShadow: d3roShadow.dialog,
p: 1,
},
}}
>
<DialogTitle sx={{ fontWeight: 700, fontFamily: d3roFontSans, color: d3roPalette.text.primary, pt: 2, px: 2.5 }}>
{editId ? t('dictionary.editTitle') : t('dictionary.addTitle')}
</DialogTitle>
<DialogContent sx={{ px: 2.5 }}>
<TextField
label={t('dictionary.word')}
value={formWord}
onChange={(e) => setFormWord(e.target.value)}
fullWidth
autoFocus
sx={{ mt: 1.5 }}
/>
<TextField
label={t('dictionary.pronunciation')}
value={formPronunciation}
onChange={(e) => setFormPronunciation(e.target.value)}
fullWidth
sx={{ mt: 2.5 }}
helperText="e.g. 디쓰리오"
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={() => setDialogOpen(false)} sx={{ color: d3roPalette.text.inactive }}>{t('common.cancel')}</Button>
<Button onClick={handleSave} variant="contained" disabled={!formWord.trim()}>{t('common.save')}</Button>
<DialogActions sx={{ px: 2.5, pb: 2, pt: 1, gap: 1 }}>
<PhysicalButton tone="glass" onClick={() => setDialogOpen(false)}>
{t('common.cancel')}
</PhysicalButton>
<PhysicalButton tone="accent" onClick={handleSave} disabled={!formWord.trim() || saving}>
{saving ? t('common.saving') || '저장 중...' : t('common.save')}
</PhysicalButton>
</DialogActions>
</Dialog>
</Box>