d3ro-voice/src/renderer/pages/DictionaryPage.tsx
Yun Chan 5596a52dcb Commands/Dictionary 미연결 버그 4건 수정
1. CommandsPage: handleSave/handleDelete에 실제 IPC 호출 연결
   - instruction:create/update/delete 호출 추가
   - 프리셋은 삭제 불가 (isBuiltin 체크)

2. Preload: instruction API 추가 (getAll/create/update/delete/reorder)
   - 기존에 preload 브릿지가 없어서 렌더러에서 호출 불가했음

3. Dictionary → STT initialPrompt 주입
   - VoiceModeService._transcribe에서 DictionaryService.getPromptHints() 호출
   - 사용자 사전 단어를 Whisper initialPrompt로 전달

4. DictionaryPage: 편집 기능 추가
   - 각 항목에 Edit 버튼 추가
   - Add/Edit 공용 다이얼로그 (editId로 분기)
2026-04-05 12:45:45 +09:00

141 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/renderer/pages/DictionaryPage.tsx
// 인스트루먼트 미학: MetalCard + PhosphorText
import { useState, useEffect, useCallback } from 'react'
import { Box, TextField, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, InputAdornment } from '@mui/material'
import AddIcon from '@mui/icons-material/Add'
import DeleteIcon from '@mui/icons-material/Delete'
import EditIcon from '@mui/icons-material/Edit'
import SearchIcon from '@mui/icons-material/Search'
import { MetalCard, PhosphorText, PhysicalButton } from '../components/ds'
import { d3roPalette, d3roFontMono } from '../theme'
import type { DictionaryEntry, DictionaryPage as DictPageData } from '@shared/types'
const PAGE_SIZE = 50
export function DictionaryPage(): React.ReactElement {
const [data, setData] = useState<DictPageData | null>(null)
const [search, setSearch] = useState('')
const [loading, setLoading] = useState(true)
const [dialogOpen, setDialogOpen] = useState(false)
const [editId, setEditId] = useState<string | null>(null)
const [formWord, setFormWord] = useState('')
const [formPronunciation, setFormPronunciation] = useState('')
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 })
if (result.success) setData(result.data)
setLoading(false)
}, [search])
useEffect(() => { loadData() }, [loadData])
const openAdd = () => {
setEditId(null)
setFormWord('')
setFormPronunciation('')
setDialogOpen(true)
}
const openEdit = (entry: DictionaryEntry) => {
setEditId(entry.id)
setFormWord(entry.word)
setFormPronunciation(entry.pronunciation ?? '')
setDialogOpen(true)
}
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,
})
}
setDialogOpen(false)
loadData()
}
return (
<Box sx={{ maxWidth: 900, mx: 'auto', p: 4 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<PhosphorText variant="label" sx={{ color: d3roPalette.text.inactive }}>
CUSTOM DICTIONARY {data?.total ?? 0} WORDS
</PhosphorText>
<Button variant="contained" startIcon={<AddIcon />} onClick={openAdd} size="small">
ADD
</Button>
</Box>
<TextField
placeholder="SEARCH..."
value={search}
onChange={(e) => setSearch(e.target.value)}
fullWidth
sx={{ mb: 3, '& .MuiInputBase-input': { fontFamily: d3roFontMono, fontSize: '12px', letterSpacing: '0.5px' } }}
slotProps={{ input: { startAdornment: <InputAdornment position="start"><SearchIcon sx={{ color: d3roPalette.text.inactive, fontSize: 18 }} /></InputAdornment> } }}
/>
{loading ? (
<PhosphorText variant="dim">LOADING...</PhosphorText>
) : !data || data.entries.length === 0 ? (
<MetalCard>
<Box sx={{ py: 6, textAlign: 'center' }}>
<PhosphorText variant="dim">{search ? 'NO RESULTS' : 'NO WORDS — ADD CUSTOM WORDS FOR BETTER STT'}</PhosphorText>
</Box>
</MetalCard>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{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: '14px', fontWeight: 600, color: d3roPalette.text.primary }}>{entry.word}</Box>
{entry.pronunciation && (
<Box sx={{ fontFamily: d3roFontMono, fontSize: '11px', color: d3roPalette.text.dimLabel }}>[{entry.pronunciation}]</Box>
)}
</Box>
<Box sx={{ fontFamily: d3roFontMono, fontSize: '10px', color: d3roPalette.text.inactive, mt: 0.5, letterSpacing: '0.5px' }}>
{entry.category.toUpperCase()} · {entry.usageCount}× USED
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 0.5 }}>
<IconButton size="small" onClick={() => openEdit(entry)}
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}>
<EditIcon sx={{ fontSize: 16 }} />
</IconButton>
<IconButton size="small" onClick={() => { window.electronAPI.dictionary.delete({ id: entry.id }); loadData() }}
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
<DeleteIcon sx={{ fontSize: 16 }} />
</IconButton>
</Box>
</Box>
</MetalCard>
))}
</Box>
)}
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="xs" fullWidth>
<DialogTitle sx={{ fontWeight: 700 }}>{editId ? '단어 편집' : '단어 추가'}</DialogTitle>
<DialogContent>
<TextField label="단어" value={formWord} onChange={(e) => setFormWord(e.target.value)} fullWidth autoFocus sx={{ mt: 1 }} />
<TextField label="발음 (선택)" value={formPronunciation} onChange={(e) => setFormPronunciation(e.target.value)} fullWidth sx={{ mt: 2 }} />
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={() => setDialogOpen(false)} sx={{ color: d3roPalette.text.inactive }}></Button>
<Button onClick={handleSave} variant="contained" disabled={!formWord.trim()}></Button>
</DialogActions>
</Dialog>
</Box>
)
}