fix: /simplify 리뷰 — UX/코드 품질 10건 수정
- EditableSegment: readOnly prop 추가, hover 배경색 효과 - MeetingDetailTabs: useMemo(parseSegments), 이중 debounce 제거, cleanup - MeetingChatPanel: sessionId deps, 에러 표시, key=timestamp - ExportMenu: i18n 위반 수정 (하드코딩 → t()) - MeetingModePage: 에러 Snackbar 피드백 (startRecording, viewDetail) - TranscriptTab: polish 실패 피드백 - 12개 locale i18n 키 5개 추가
This commit is contained in:
parent
403985e2cd
commit
f0e221058d
18 changed files with 179 additions and 49 deletions
|
|
@ -12,6 +12,7 @@ interface EditableSegmentProps {
|
|||
text: string
|
||||
edited: boolean // 수정 여부
|
||||
onEdit: (segmentId: string, newText: string) => void
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
function formatTimestamp(ms: number): string {
|
||||
|
|
@ -27,6 +28,7 @@ export function EditableSegment({
|
|||
text,
|
||||
edited,
|
||||
onEdit,
|
||||
readOnly = false,
|
||||
}: EditableSegmentProps): React.ReactElement {
|
||||
const { t } = useI18n()
|
||||
const [editing, setEditing] = useState(false)
|
||||
|
|
@ -34,9 +36,10 @@ export function EditableSegment({
|
|||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (readOnly) return
|
||||
setDraft(text)
|
||||
setEditing(true)
|
||||
}, [text])
|
||||
}, [readOnly, text])
|
||||
|
||||
const handleCommit = useCallback(() => {
|
||||
const trimmed = draft.trim()
|
||||
|
|
@ -104,12 +107,17 @@ export function EditableSegment({
|
|||
fontFamily: d3roFontMono,
|
||||
fontSize: 13,
|
||||
color: d3roPalette.text.primary,
|
||||
cursor: 'text',
|
||||
cursor: readOnly ? 'default' : 'text',
|
||||
borderBottom: edited ? `1px dashed ${d3roPalette.text.inactive}` : 'none',
|
||||
'&:hover': {
|
||||
color: d3roPalette.text.primary,
|
||||
borderBottomColor: edited ? d3roPalette.accent.amber : undefined,
|
||||
},
|
||||
borderRadius: '2px',
|
||||
px: readOnly ? 0 : '2px',
|
||||
'&:hover': readOnly
|
||||
? {}
|
||||
: {
|
||||
color: d3roPalette.text.primary,
|
||||
borderBottomColor: edited ? d3roPalette.accent.amber : undefined,
|
||||
bgcolor: edited ? undefined : d3roPalette.bg.elevated,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@ export function ExportMenu({ onExport, onCopyToClipboard }: ExportMenuProps): Re
|
|||
)
|
||||
|
||||
const formats: FormatItem[] = [
|
||||
{ format: 'md', label: 'Markdown (.md)' },
|
||||
{ format: 'md', label: t('meeting.exportMd') },
|
||||
{ format: 'txt', label: t('meeting.exportTxt') },
|
||||
{ format: 'pdf', label: 'PDF (.pdf)' },
|
||||
{ format: 'pdf', label: t('meeting.exportPdfFmt') },
|
||||
{ format: 'docx', label: t('meeting.exportDocx') },
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
|
|||
const [streaming, setStreaming] = useState(false)
|
||||
const [streamingContent, setStreamingContent] = useState('')
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
|
|
@ -79,9 +80,10 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
|
|||
setStreamingContent('')
|
||||
})
|
||||
|
||||
const unsubError = window.electronAPI.meetingChat.onError((_err) => {
|
||||
const unsubError = window.electronAPI.meetingChat.onError((err) => {
|
||||
setStreaming(false)
|
||||
setStreamingContent('')
|
||||
setError(err.message)
|
||||
})
|
||||
|
||||
return () => {
|
||||
|
|
@ -89,7 +91,7 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
|
|||
unsubMessage()
|
||||
unsubError()
|
||||
}
|
||||
}, [])
|
||||
}, [sessionId])
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
const text = inputValue.trim()
|
||||
|
|
@ -197,9 +199,9 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
|
|||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
{messages.map((msg, idx) => (
|
||||
{messages.map((msg) => (
|
||||
<Box
|
||||
key={idx}
|
||||
key={`${msg.role}-${msg.timestamp}`}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start',
|
||||
|
|
@ -233,6 +235,32 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
|
|||
</Box>
|
||||
))}
|
||||
|
||||
{/* 에러 메시지 */}
|
||||
{error && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start' }}>
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: '80%',
|
||||
px: 1.25,
|
||||
py: 0.5,
|
||||
borderRadius: '6px',
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: d3roTypo.compact.size,
|
||||
lineHeight: 1.5,
|
||||
bgcolor: d3roPalette.bg.elevated,
|
||||
color: d3roPalette.tag.red,
|
||||
border: `1px solid ${d3roPalette.tag.red}`,
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setError(null)}
|
||||
>
|
||||
{error}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 스트리밍 중 어시스턴트 메시지 */}
|
||||
{streaming && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start' }}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// src/renderer/components/meeting/MeetingDetailTabs.tsx
|
||||
// Phase 14.5: 풀스크린 탭 전환 컨테이너 — 전사 + 동적 문서 탭
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Tabs,
|
||||
|
|
@ -33,8 +33,6 @@ interface MeetingDetailTabsProps {
|
|||
onBack: () => void
|
||||
}
|
||||
|
||||
const AUTOSAVE_DELAY = 500
|
||||
|
||||
// 세그먼트 파싱: rawTranscript를 줄 단위로 분리
|
||||
function parseSegments(
|
||||
rawTranscript: string | null,
|
||||
|
|
@ -78,17 +76,19 @@ export function MeetingDetailTabs({
|
|||
}: MeetingDetailTabsProps): React.ReactElement {
|
||||
const { t } = useI18n()
|
||||
const [detail, setDetail] = useState<MeetingSessionDetail>(initialDetail)
|
||||
const [documents, setDocuments] = useState<MeetingDocument[]>(initialDetail.documents ?? [])
|
||||
const [documents, setDocuments] = useState<MeetingDocument[]>((initialDetail.documents ?? []).filter(Boolean))
|
||||
const [templates, setTemplates] = useState<MeetingDocTemplate[]>([])
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [generating, setGenerating] = useState(false)
|
||||
const [editingTitle, setEditingTitle] = useState(false)
|
||||
const [titleDraft, setTitleDraft] = useState(detail.title ?? '')
|
||||
const autoSaveTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>({})
|
||||
|
||||
// 세그먼트 파싱 (rawTranscript 기반)
|
||||
const segments = parseSegments(detail.rawTranscript, detail.editedTranscript)
|
||||
const segments = useMemo(
|
||||
() => parseSegments(detail.rawTranscript, detail.editedTranscript),
|
||||
[detail.rawTranscript, detail.editedTranscript],
|
||||
)
|
||||
|
||||
// 템플릿 로드
|
||||
useEffect(() => {
|
||||
|
|
@ -134,19 +134,16 @@ export function MeetingDetailTabs({
|
|||
[detail.id],
|
||||
)
|
||||
|
||||
// ── 문서 내용 자동저장 ──
|
||||
// ── 문서 내용 자동저장 (DocumentTab 내부에 debounce 있음, 이중 래핑 제거) ──
|
||||
const handleDocContentChange = useCallback(
|
||||
(docId: string, content: string) => {
|
||||
if (autoSaveTimers.current[docId]) clearTimeout(autoSaveTimers.current[docId])
|
||||
autoSaveTimers.current[docId] = setTimeout(async () => {
|
||||
await window.electronAPI.meetingMode.updateDocument({
|
||||
documentId: docId,
|
||||
content,
|
||||
})
|
||||
setDocuments((prev) =>
|
||||
prev.map((d) => (d.id === docId ? { ...d, content } : d)),
|
||||
)
|
||||
}, AUTOSAVE_DELAY)
|
||||
async (docId: string, content: string) => {
|
||||
await window.electronAPI.meetingMode.updateDocument({
|
||||
documentId: docId,
|
||||
content,
|
||||
})
|
||||
setDocuments((prev) =>
|
||||
prev.map((d) => (d.id === docId ? { ...d, content } : d)),
|
||||
)
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
|
@ -286,8 +283,8 @@ export function MeetingDetailTabs({
|
|||
}}
|
||||
>
|
||||
<Tab label={t('meeting.transcriptTab')} />
|
||||
{documents.map((doc) => (
|
||||
<Tab key={doc.id} label={doc.title} />
|
||||
{documents.filter(Boolean).map((doc) => (
|
||||
<Tab key={doc.id} label={doc.title ?? t('meeting.untitled')} />
|
||||
))}
|
||||
<Tab
|
||||
icon={<AddIcon sx={{ fontSize: 16 }} />}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Phase 14.5: 전사 편집 탭 (상세 페이지용)
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { Box, Switch, FormControlLabel, LinearProgress } from '@mui/material'
|
||||
import { Box, Switch, FormControlLabel, LinearProgress, Snackbar, Alert } from '@mui/material'
|
||||
import { EditableSegment } from './EditableSegment'
|
||||
import { PhysicalButton } from '../ds/PhysicalButton'
|
||||
import { PhosphorText } from '../ds/PhosphorText'
|
||||
|
|
@ -37,15 +37,19 @@ export function TranscriptTab({
|
|||
const { t } = useI18n()
|
||||
const [showEdited, setShowEdited] = useState(true)
|
||||
const [polishing, setPolishing] = useState(false)
|
||||
const [polishError, setPolishError] = useState<string | null>(null)
|
||||
|
||||
const handlePolish = useCallback(async () => {
|
||||
setPolishing(true)
|
||||
setPolishError(null)
|
||||
const resp = await window.electronAPI.meetingMode.polishTranscript({ sessionId })
|
||||
if (resp.success) {
|
||||
onSaveTranscript(resp.data)
|
||||
} else {
|
||||
setPolishError(t('meeting.polishFailed'))
|
||||
}
|
||||
setPolishing(false)
|
||||
}, [sessionId, onSaveTranscript])
|
||||
}, [sessionId, onSaveTranscript, t])
|
||||
|
||||
const handleDownloadTxt = useCallback(() => {
|
||||
const content = showEdited && editedTranscript ? editedTranscript : (rawTranscript ?? '')
|
||||
|
|
@ -77,6 +81,16 @@ export function TranscriptTab({
|
|||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Snackbar
|
||||
open={polishError !== null}
|
||||
autoHideDuration={4000}
|
||||
onClose={() => setPolishError(null)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity="error" onClose={() => setPolishError(null)} sx={{ width: '100%' }}>
|
||||
{polishError}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
{/* 상단 컨트롤 */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
|
||||
<FormControlLabel
|
||||
|
|
|
|||
|
|
@ -265,5 +265,10 @@
|
|||
"meeting.copyToClipboard": "In Zwischenablage kopieren",
|
||||
"meeting.copied": "Kopiert",
|
||||
"meeting.chatCollapse": "Chat einklappen",
|
||||
"meeting.chatExpand": "Chat ausklappen"
|
||||
"meeting.chatExpand": "Chat ausklappen",
|
||||
"meeting.exportMd": "Markdown (.md)",
|
||||
"meeting.exportPdfFmt": "PDF (.pdf)",
|
||||
"meeting.polishFailed": "KI-Polierung fehlgeschlagen",
|
||||
"meeting.viewDetailError": "Meeting-Details konnten nicht geladen werden",
|
||||
"meeting.startRecordingError": "Aufnahme konnte nicht gestartet werden"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -466,5 +466,10 @@
|
|||
"meeting.copyToClipboard": "Copy to Clipboard",
|
||||
"meeting.copied": "Copied",
|
||||
"meeting.chatCollapse": "Collapse Chat",
|
||||
"meeting.chatExpand": "Expand Chat"
|
||||
"meeting.chatExpand": "Expand Chat",
|
||||
"meeting.exportMd": "Markdown (.md)",
|
||||
"meeting.exportPdfFmt": "PDF (.pdf)",
|
||||
"meeting.polishFailed": "AI polishing failed",
|
||||
"meeting.viewDetailError": "Failed to load meeting details",
|
||||
"meeting.startRecordingError": "Failed to start recording"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,5 +265,10 @@
|
|||
"meeting.copyToClipboard": "Copiar al portapapeles",
|
||||
"meeting.copied": "Copiado",
|
||||
"meeting.chatCollapse": "Contraer chat",
|
||||
"meeting.chatExpand": "Expandir chat"
|
||||
"meeting.chatExpand": "Expandir chat",
|
||||
"meeting.exportMd": "Markdown (.md)",
|
||||
"meeting.exportPdfFmt": "PDF (.pdf)",
|
||||
"meeting.polishFailed": "Error al pulir con IA",
|
||||
"meeting.viewDetailError": "Error al cargar detalles de la reunión",
|
||||
"meeting.startRecordingError": "Error al iniciar la grabación"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,5 +265,10 @@
|
|||
"meeting.copyToClipboard": "Copier dans le presse-papiers",
|
||||
"meeting.copied": "Copié",
|
||||
"meeting.chatCollapse": "Réduire le chat",
|
||||
"meeting.chatExpand": "Agrandir le chat"
|
||||
"meeting.chatExpand": "Agrandir le chat",
|
||||
"meeting.exportMd": "Markdown (.md)",
|
||||
"meeting.exportPdfFmt": "PDF (.pdf)",
|
||||
"meeting.polishFailed": "Echec du polish IA",
|
||||
"meeting.viewDetailError": "Impossible de charger les détails de la réunion",
|
||||
"meeting.startRecordingError": "Impossible de démarrer l'enregistrement"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,5 +265,10 @@
|
|||
"meeting.copyToClipboard": "クリップボードにコピー",
|
||||
"meeting.copied": "コピーしました",
|
||||
"meeting.chatCollapse": "チャットを折り畳む",
|
||||
"meeting.chatExpand": "チャットを展開"
|
||||
"meeting.chatExpand": "チャットを展開",
|
||||
"meeting.exportMd": "Markdown (.md)",
|
||||
"meeting.exportPdfFmt": "PDF (.pdf)",
|
||||
"meeting.polishFailed": "AI磨き上げに失敗しました",
|
||||
"meeting.viewDetailError": "会議の詳細を読み込めませんでした",
|
||||
"meeting.startRecordingError": "録音を開始できませんでした"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -466,5 +466,10 @@
|
|||
"meeting.copyToClipboard": "클립보드 복사",
|
||||
"meeting.copied": "복사됨",
|
||||
"meeting.chatCollapse": "채팅 접기",
|
||||
"meeting.chatExpand": "채팅 펼치기"
|
||||
"meeting.chatExpand": "채팅 펼치기",
|
||||
"meeting.exportMd": "Markdown (.md)",
|
||||
"meeting.exportPdfFmt": "PDF (.pdf)",
|
||||
"meeting.polishFailed": "AI 다듬기에 실패했습니다",
|
||||
"meeting.viewDetailError": "회의 상세 정보를 불러오지 못했습니다",
|
||||
"meeting.startRecordingError": "녹음을 시작하지 못했습니다"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,5 +265,10 @@
|
|||
"meeting.copyToClipboard": "Copiar para área de transferência",
|
||||
"meeting.copied": "Copiado",
|
||||
"meeting.chatCollapse": "Recolher chat",
|
||||
"meeting.chatExpand": "Expandir chat"
|
||||
"meeting.chatExpand": "Expandir chat",
|
||||
"meeting.exportMd": "Markdown (.md)",
|
||||
"meeting.exportPdfFmt": "PDF (.pdf)",
|
||||
"meeting.polishFailed": "Falha no polimento de IA",
|
||||
"meeting.viewDetailError": "Falha ao carregar detalhes da reunião",
|
||||
"meeting.startRecordingError": "Falha ao iniciar gravação"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,5 +265,10 @@
|
|||
"meeting.copyToClipboard": "Копировать в буфер обмена",
|
||||
"meeting.copied": "Скопировано",
|
||||
"meeting.chatCollapse": "Свернуть чат",
|
||||
"meeting.chatExpand": "Развернуть чат"
|
||||
"meeting.chatExpand": "Развернуть чат",
|
||||
"meeting.exportMd": "Markdown (.md)",
|
||||
"meeting.exportPdfFmt": "PDF (.pdf)",
|
||||
"meeting.polishFailed": "Ошибка ИИ-полировки",
|
||||
"meeting.viewDetailError": "Не удалось загрузить детали встречи",
|
||||
"meeting.startRecordingError": "Не удалось начать запись"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,5 +265,10 @@
|
|||
"meeting.copyToClipboard": "คัดลอกไปยังคลิปบอร์ด",
|
||||
"meeting.copied": "คัดลอกแล้ว",
|
||||
"meeting.chatCollapse": "ยุบแชท",
|
||||
"meeting.chatExpand": "ขยายแชท"
|
||||
"meeting.chatExpand": "ขยายแชท",
|
||||
"meeting.exportMd": "Markdown (.md)",
|
||||
"meeting.exportPdfFmt": "PDF (.pdf)",
|
||||
"meeting.polishFailed": "AI ปรับแต่งล้มเหลว",
|
||||
"meeting.viewDetailError": "ไม่สามารถโหลดรายละเอียดการประชุมได้",
|
||||
"meeting.startRecordingError": "ไม่สามารถเริ่มการบันทึกได้"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,5 +265,10 @@
|
|||
"meeting.copyToClipboard": "Sao chép vào bộ nhớ tạm",
|
||||
"meeting.copied": "Đã sao chép",
|
||||
"meeting.chatCollapse": "Thu gọn chat",
|
||||
"meeting.chatExpand": "Mở rộng chat"
|
||||
"meeting.chatExpand": "Mở rộng chat",
|
||||
"meeting.exportMd": "Markdown (.md)",
|
||||
"meeting.exportPdfFmt": "PDF (.pdf)",
|
||||
"meeting.polishFailed": "AI đánh bóng thất bại",
|
||||
"meeting.viewDetailError": "Không thể tải chi tiết cuộc họp",
|
||||
"meeting.startRecordingError": "Không thể bắt đầu ghi âm"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,5 +265,10 @@
|
|||
"meeting.copyToClipboard": "複製到剪貼簿",
|
||||
"meeting.copied": "已複製",
|
||||
"meeting.chatCollapse": "收起聊天",
|
||||
"meeting.chatExpand": "展開聊天"
|
||||
"meeting.chatExpand": "展開聊天",
|
||||
"meeting.exportMd": "Markdown (.md)",
|
||||
"meeting.exportPdfFmt": "PDF (.pdf)",
|
||||
"meeting.polishFailed": "AI潤色失敗",
|
||||
"meeting.viewDetailError": "無法載入會議詳情",
|
||||
"meeting.startRecordingError": "無法開始錄音"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,5 +265,10 @@
|
|||
"meeting.copyToClipboard": "复制到剪贴板",
|
||||
"meeting.copied": "已复制",
|
||||
"meeting.chatCollapse": "收起聊天",
|
||||
"meeting.chatExpand": "展开聊天"
|
||||
"meeting.chatExpand": "展开聊天",
|
||||
"meeting.exportMd": "Markdown (.md)",
|
||||
"meeting.exportPdfFmt": "PDF (.pdf)",
|
||||
"meeting.polishFailed": "AI润色失败",
|
||||
"meeting.viewDetailError": "无法加载会议详情",
|
||||
"meeting.startRecordingError": "无法开始录音"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {
|
|||
Box,
|
||||
LinearProgress,
|
||||
TextField,
|
||||
Snackbar,
|
||||
Alert,
|
||||
} from '@mui/material'
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import StopIcon from '@mui/icons-material/Stop'
|
||||
|
|
@ -50,6 +52,7 @@ export function MeetingModePage(): React.ReactElement {
|
|||
|
||||
// 상세 뷰 상태
|
||||
const [detail, setDetail] = useState<MeetingSessionDetail | null>(null)
|
||||
const [snackbarMsg, setSnackbarMsg] = useState<string | null>(null)
|
||||
|
||||
// ── 세션 목록 로드 ──
|
||||
const loadSessions = useCallback(async () => {
|
||||
|
|
@ -140,8 +143,10 @@ export function MeetingModePage(): React.ReactElement {
|
|||
setElapsedMs(0)
|
||||
setProgress(null)
|
||||
setView('recording')
|
||||
} else {
|
||||
setSnackbarMsg(t('meeting.startRecordingError'))
|
||||
}
|
||||
}, [])
|
||||
}, [t])
|
||||
|
||||
const handleStopRecording = useCallback(async () => {
|
||||
await window.electronAPI.meetingMode.stopRecording()
|
||||
|
|
@ -171,8 +176,10 @@ export function MeetingModePage(): React.ReactElement {
|
|||
if (resp.success) {
|
||||
setDetail(resp.data)
|
||||
setView('detail')
|
||||
} else {
|
||||
setSnackbarMsg(t('meeting.viewDetailError'))
|
||||
}
|
||||
}, [])
|
||||
}, [t])
|
||||
|
||||
|
||||
// ── 유틸 ──
|
||||
|
|
@ -198,6 +205,19 @@ export function MeetingModePage(): React.ReactElement {
|
|||
}
|
||||
}
|
||||
|
||||
const snackbar = (
|
||||
<Snackbar
|
||||
open={snackbarMsg !== null}
|
||||
autoHideDuration={4000}
|
||||
onClose={() => setSnackbarMsg(null)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity="error" onClose={() => setSnackbarMsg(null)} sx={{ width: '100%' }}>
|
||||
{snackbarMsg}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
)
|
||||
|
||||
// ── 렌더: 목록 뷰 ──
|
||||
if (view === 'list') {
|
||||
return (
|
||||
|
|
@ -263,6 +283,7 @@ export function MeetingModePage(): React.ReactElement {
|
|||
))}
|
||||
</Box>
|
||||
)}
|
||||
{snackbar}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -328,7 +349,8 @@ export function MeetingModePage(): React.ReactElement {
|
|||
timestamp={Math.max(0, seg.timestamp - (recordingStartRef.current || seg.timestamp))}
|
||||
text={seg.text}
|
||||
edited={false}
|
||||
onEdit={() => { /* 녹음 중 편집 비활성 */ }}
|
||||
onEdit={() => { /* no-op: readOnly=true */ }}
|
||||
readOnly
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
|
@ -403,6 +425,7 @@ export function MeetingModePage(): React.ReactElement {
|
|||
</PhosphorText>
|
||||
</Box>
|
||||
)}
|
||||
{snackbar}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue