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로 분기)
This commit is contained in:
Yun Chan 2026-04-05 12:45:45 +09:00
parent 6e9e8b1f29
commit 5596a52dcb
4 changed files with 185 additions and 44 deletions

View file

@ -5,6 +5,7 @@ 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'
@ -16,9 +17,10 @@ export function DictionaryPage(): React.ReactElement {
const [data, setData] = useState<DictPageData | null>(null)
const [search, setSearch] = useState('')
const [loading, setLoading] = useState(true)
const [addOpen, setAddOpen] = useState(false)
const [newWord, setNewWord] = useState('')
const [newPronunciation, setNewPronunciation] = useState('')
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)
@ -31,10 +33,36 @@ export function DictionaryPage(): React.ReactElement {
useEffect(() => { loadData() }, [loadData])
const handleAdd = async () => {
if (!newWord.trim()) return
await window.electronAPI.dictionary.add({ word: newWord.trim(), pronunciation: newPronunciation.trim() || undefined })
setNewWord(''); setNewPronunciation(''); setAddOpen(false); 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 (
@ -43,7 +71,7 @@ export function DictionaryPage(): React.ReactElement {
<PhosphorText variant="label" sx={{ color: d3roPalette.text.inactive }}>
CUSTOM DICTIONARY {data?.total ?? 0} WORDS
</PhosphorText>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setAddOpen(true)} size="small">
<Button variant="contained" startIcon={<AddIcon />} onClick={openAdd} size="small">
ADD
</Button>
</Box>
@ -81,25 +109,31 @@ export function DictionaryPage(): React.ReactElement {
{entry.category.toUpperCase()} · {entry.usageCount}× USED
</Box>
</Box>
<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 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={addOpen} onClose={() => setAddOpen(false)} maxWidth="xs" fullWidth>
<DialogTitle sx={{ fontWeight: 700 }}>Add Word</DialogTitle>
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="xs" fullWidth>
<DialogTitle sx={{ fontWeight: 700 }}>{editId ? '단어 편집' : '단어 추가'}</DialogTitle>
<DialogContent>
<TextField label="Word" value={newWord} onChange={(e) => setNewWord(e.target.value)} fullWidth autoFocus sx={{ mt: 1 }} />
<TextField label="Pronunciation (optional)" value={newPronunciation} onChange={(e) => setNewPronunciation(e.target.value)} fullWidth sx={{ mt: 2 }} />
<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={() => setAddOpen(false)} color="secondary" variant="contained">Cancel</Button>
<Button onClick={handleAdd} variant="contained" disabled={!newWord.trim()}>Add</Button>
<Button onClick={() => setDialogOpen(false)} sx={{ color: d3roPalette.text.inactive }}></Button>
<Button onClick={handleSave} variant="contained" disabled={!formWord.trim()}></Button>
</DialogActions>
</Dialog>
</Box>