// src/renderer/components/TemplateSection.tsx // Phase 12.3: 딕테이션 템플릿 관리 UI (CommandsPage 내 섹션) import { useState, useEffect, useCallback } from 'react' import { Box, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Tooltip } 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 PlayArrowIcon from '@mui/icons-material/PlayArrow' import { MetalCard, PhosphorText, Led, PhysicalButton } from './ds' import { PageHeader, EmptyStateCard } from './shared' import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '../theme' import { useI18n } from '../i18n' import type { DictationTemplate, TemplateField, TemplateSessionInfo } from '@shared/types' export function TemplateSection(): React.ReactElement { const { t } = useI18n() const [templates, setTemplates] = useState([]) const [loading, setLoading] = useState(true) const [dialogOpen, setDialogOpen] = useState(false) const [editId, setEditId] = useState(null) const [formName, setFormName] = useState('') const [formDesc, setFormDesc] = useState('') const [formOutput, setFormOutput] = useState('') const [formFields, setFormFields] = useState([]) const [session, setSession] = useState(null) const loadData = useCallback(async () => { setLoading(true) const result = await window.electronAPI.dictationTemplate.getAll() if (result.success) setTemplates(result.data) const sessionResult = await window.electronAPI.dictationTemplate.getSessionState() if (sessionResult.success) setSession(sessionResult.data) setLoading(false) }, []) useEffect(() => { loadData() }, [loadData]) useEffect(() => { const unsub = window.electronAPI.dictationTemplate.onSessionStateChanged((data) => { setSession(data) }) const unsubComplete = window.electronAPI.dictationTemplate.onSessionCompleted(() => { setSession(null) loadData() }) return () => { unsub(); unsubComplete() } }, [loadData]) const openCreate = () => { setEditId(null) setFormName('') setFormDesc('') setFormOutput('{{field1}}') setFormFields([{ id: 'field1', name: 'field1', label: 'Field 1', promptText: '', required: true, maxDurationSec: 30 }]) setDialogOpen(true) } const openEdit = (template: DictationTemplate) => { setEditId(template.id) setFormName(template.name) setFormDesc(template.description) setFormOutput(template.outputFormat) setFormFields([...template.fields]) setDialogOpen(true) } const handleSave = async () => { if (editId) { await window.electronAPI.dictationTemplate.update({ id: editId, name: formName.trim(), description: formDesc.trim(), fields: formFields, outputFormat: formOutput, }) } else { await window.electronAPI.dictationTemplate.create({ name: formName.trim(), description: formDesc.trim(), fields: formFields, outputFormat: formOutput, }) } setDialogOpen(false) loadData() } const handleDelete = async (id: string) => { await window.electronAPI.dictationTemplate.delete({ id }) loadData() } const handleStartSession = async (templateId: string) => { await window.electronAPI.dictationTemplate.startSession({ templateId }) } const handleCancelSession = async () => { await window.electronAPI.dictationTemplate.cancelSession() setSession(null) } const addField = () => { const idx = formFields.length + 1 setFormFields([...formFields, { id: `field${idx}`, name: `field${idx}`, label: `Field ${idx}`, promptText: '', required: true, maxDurationSec: 30, }]) } const updateField = (index: number, updates: Partial) => { const updated = [...formFields] updated[index] = { ...updated[index], ...updates } setFormFields(updated) } const removeField = (index: number) => { setFormFields(formFields.filter((_, i) => i !== index)) } return ( {t('template.create')} } /> {/* 활성 세션 표시 */} {session && ( {session.templateName}: {session.currentField?.label ?? '...'} ({session.currentFieldIndex + 1}/{session.totalFields}) {t('template.cancelSession')} )} {/* 템플릿 목록 */} {templates.length === 0 && !loading ? ( ) : ( {templates.map((tmpl) => ( {tmpl.name} {tmpl.isBuiltin && ( PRESET )} {tmpl.fields.length} {t('template.fields').toLowerCase()} — {tmpl.description} handleStartSession(tmpl.id)} disabled={!!session} sx={{ color: d3roPalette.accent.amber, '&:hover': { opacity: 0.8 } }} > openEdit(tmpl)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }} > {!tmpl.isBuiltin && ( handleDelete(tmpl.id)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }} > )} ))} )} {/* 편집 다이얼로그 */} setDialogOpen(false)} maxWidth="sm" fullWidth> {editId ? t('template.edit') : t('template.create')} setFormName(e.target.value)} size="small" fullWidth /> setFormDesc(e.target.value)} size="small" fullWidth /> {t('template.fields').toUpperCase()} {formFields.map((field, idx) => ( updateField(idx, { name: e.target.value, id: e.target.value })} size="small" sx={{ flex: 1 }} /> updateField(idx, { label: e.target.value })} size="small" sx={{ flex: 1 }} /> updateField(idx, { promptText: e.target.value })} size="small" sx={{ flex: 2 }} /> removeField(idx)} disabled={formFields.length <= 1} sx={{ color: d3roPalette.text.inactive }} > ))} setFormOutput(e.target.value)} size="small" fullWidth multiline rows={3} helperText={t('template.outputHelperText')} /> ) }