From 9062acb38d5d5c10eb765f45a2730bd46223e96e Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 24 Oct 2026 01:59:07 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[fix]=201=EC=B4=88=20=EB=AF=B8=EB=A7=8C=20?= =?UTF-8?q?=EB=85=B9=EC=9D=8C=20=EA=B5=AC=EA=B0=84=EC=9D=80=20Soniox?= =?UTF-8?q?=EC=97=90=20=EB=B3=B4=EB=82=B4=EC=A7=80=20=EC=95=8A=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 시작 직후 멈춘 구간은 오디오 머리 정보만 담겨 Soniox가 깨진 파일로 거절한다(dev 10/24 01:45). 이런 구간은 문장 없는 구간으로 바로 정리해 전사 실패로 표시되지 않게 한다. Co-Authored-By: Claude Opus 5.5 --- .../hooks/useSonioxTranscriber.test.ts | 26 +++++++++++++++++++ .../TimerPage/hooks/useSonioxTranscriber.ts | 8 +++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/page/TimerPage/hooks/useSonioxTranscriber.test.ts b/src/page/TimerPage/hooks/useSonioxTranscriber.test.ts index a549c3b3..d16f485b 100644 --- a/src/page/TimerPage/hooks/useSonioxTranscriber.test.ts +++ b/src/page/TimerPage/hooks/useSonioxTranscriber.test.ts @@ -6,6 +6,7 @@ import { getTranscriptDraft, } from '../../../util/transcriptDraft'; import useSonioxTranscriber, { + MIN_SEGMENT_DURATION_MS, UseSonioxTranscriberParams, } from './useSonioxTranscriber'; @@ -21,6 +22,8 @@ class FakeMediaRecorder { static isTypeSupported = vi.fn(() => true); // 실제 브라우저처럼 stop 이벤트를 나중에 보내려면 true로 둔다. static isStopDeferred = false; + // 녹음이 끝날 때 흐른 것으로 볼 녹음 시간 + static recordingMs = 2000; mimeType: string; state: 'inactive' | 'recording' = 'inactive'; ondataavailable: ((event: { data: Blob }) => void) | null = null; @@ -36,6 +39,7 @@ class FakeMediaRecorder { }); flush(data: Blob = new Blob(['audio'], { type: this.mimeType })) { + vi.setSystemTime(Date.now() + FakeMediaRecorder.recordingMs); this.ondataavailable?.({ data }); this.onstop?.(); } @@ -78,6 +82,8 @@ describe('Soniox 구간 녹음 훅', () => { mockedPost.mockResolvedValue({ utterances: [] }); FakeMediaRecorder.instances = []; FakeMediaRecorder.isStopDeferred = false; + FakeMediaRecorder.recordingMs = 2000; + vi.useFakeTimers({ toFake: ['Date'] }); trackStop.mockClear(); getUserMedia.mockClear(); fakeStream = createStream(); @@ -90,6 +96,7 @@ describe('Soniox 구간 녹음 훅', () => { afterEach(() => { vi.unstubAllGlobals(); + vi.useRealTimers(); }); it('기록을 켜면 마이크 권한을 받고 Soniox 모드 초안을 시작하지만 타이머가 멈춰 있으면 녹음하지 않는다', async () => { @@ -367,4 +374,23 @@ describe('Soniox 구간 녹음 훅', () => { expect(trackStop).toHaveBeenCalled(); }); + + it('1초 미만으로 녹음된 구간은 Soniox에 보내지 않고 문장 없는 구간으로 정리한다', async () => { + FakeMediaRecorder.recordingMs = MIN_SEGMENT_DURATION_MS - 700; + const { result, rerender } = renderHook( + (params) => useSonioxTranscriber(params), + { initialProps: baseParams }, + ); + await act(() => result.current.enable()); + rerender({ ...baseParams, isRunning: true }); + + rerender({ ...baseParams, isRunning: false }); + + expect(mockedPost).not.toHaveBeenCalled(); + expect(result.current.pendingCount).toBe(0); + expect(result.current.failedCount).toBe(0); + const draft = getTranscriptDraft(7)!; + expect(draft.segments).toEqual([]); + expect(countPendingSegments(draft, Date.now())).toBe(0); + }); }); diff --git a/src/page/TimerPage/hooks/useSonioxTranscriber.ts b/src/page/TimerPage/hooks/useSonioxTranscriber.ts index b139de23..ce0ac23b 100644 --- a/src/page/TimerPage/hooks/useSonioxTranscriber.ts +++ b/src/page/TimerPage/hooks/useSonioxTranscriber.ts @@ -42,6 +42,10 @@ interface RecordingSession { isPendingMarked: boolean; } +// 이보다 짧은 구간은 오디오 머리 정보만 담겨 Soniox가 깨진 파일로 거절하므로 보내지 않는다. +// 타이머를 켜자마자 끄거나 자유토론 팀을 바로 바꿀 때 생긴다. +export const MIN_SEGMENT_DURATION_MS = 1000; + let segmentSequence = 0; const createSegmentKey = ( timeBoxIndex: number, @@ -98,9 +102,11 @@ export default function useSonioxTranscriber( }, []); // 전사 대기는 구간을 끝낼 때 이미 기록했으므로, 결과를 저장하거나 실패로 남기며 대기를 푼다. + // 빈 오디오나 너무 짧은 구간은 보내지 않고 문장 없는 구간으로 대기만 푼다. const transcribe = useCallback((session: RecordingSession, audio: Blob) => { const { tableId: sessionTableId, segment, startedAt } = session; - if (audio.size === 0) { + const isTooShort = Date.now() - startedAt < MIN_SEGMENT_DURATION_MS; + if (audio.size === 0 || isTooShort) { appendSegmentUtterances(sessionTableId, segment, startedAt, []); setPendingCount((prev) => prev - 1); return; From fdd5b36029e47fbb4403d29d65603012052a2649 Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 24 Oct 2026 02:00:05 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[design]=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=85=B9=EC=9D=8C=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EC=9D=84=20=ED=83=80=EC=9B=90=ED=98=95=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=C2=B7=ED=83=9C=EA=B7=B8=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 녹음하지 않을 때는 "토론 녹화하기" 버튼, 녹음 중에는 "토론 녹음 중" 태그로 보인다. 누르면 지금처럼 녹음 설정(모드 선택·동의)을 열거나 녹음을 끈다. Co-Authored-By: Claude Opus 5.5 --- public/locales/en/translation.json | 10 +-- public/locales/ko/translation.json | 10 +-- .../components/TranscribeControl.test.tsx | 33 ++++--- .../components/TranscribeControl.tsx | 86 +++++++++---------- 4 files changed, 74 insertions(+), 65 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index a85c0761..68c7c3c9 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -319,7 +319,6 @@ "링크 복사됨": "Link Copied", "링크 복사 실패": "Copy Failed", "분석 보고서를 만들지 못했어요": "We couldn't create the analysis report", - "{{count}}문장": "{{count}} sentences", "{{label}}: 속기록에서 보기": "{{label}}: view in transcript", "{{side}} 우세": "{{side}} ahead", "근거 없음": "No evidence", @@ -349,8 +348,6 @@ "타이머를 시작하면 기록해요": "Records while the timer runs", "토론 기록": "Debate record", "토론 기록 ID가 올바르지 않습니다.": "The debate record ID is invalid.", - "토론 기록 끄기": "Turn off debate recording", - "토론 기록 켜기": "Turn on debate recording", "토론 기록을 보내고 있어요": "Sending the debate record", "토론 기록을 보내지 못했어요": "We couldn't send the debate record", "토론 음성 기록에 동의하시나요?": "Do you agree to record the debate audio?", @@ -361,7 +358,6 @@ "음성 인식 방식": "Speech recognition mode", "발언 구간이 끝날 때마다 녹음한 오디오를 서버로 보내 Soniox로 텍스트로 바꿔요. 토론이 끝나면 속기록을 서버로 보내 AI가 분석 보고서를 만들어요.": "Each time a speech segment ends, its recorded audio is sent to our server and converted to text with Soniox. When the debate ends, the transcript is sent to our server and AI writes an analysis report.", "이 브라우저에서는 토론 기록을 사용할 수 없어요": "Debate recording is not available in this browser", - "녹음 중": "Recording", "{{count}}개 구간 전사 중": "Transcribing {{count}} segment(s)", "{{count}}개 구간 전사 실패": "{{count}} segment(s) failed to transcribe", "발언을 텍스트로 바꾸고 있어요 ({{count}}개 구간 남음)": "Converting speech to text ({{count}} segment(s) left)", @@ -381,5 +377,9 @@ "주제를 알 수 없어요": "Unknown topic", "아직 토론 기록이 없어요": "No debate records yet", "타이머 화면에서 녹음을 켜고 토론을 진행해 보세요.": "Turn on recording on the timer screen and run a debate.", - "타이머로 진행한 토론이 끝날 때마다 속기록과 분석 보고서가 자동으로 쌓여요.": "Each debate you run with the timer adds a transcript and an analysis report here automatically." + "타이머로 진행한 토론이 끝날 때마다 속기록과 분석 보고서가 자동으로 쌓여요.": "Each debate you run with the timer adds a transcript and an analysis report here automatically.", + "토론 녹화하기": "Record debate", + "토론 녹음 중": "Recording debate", + "토론 녹음 끄기": "Stop recording debate", + "{{count}}문장 기록": "{{count}} sentences recorded" } diff --git a/public/locales/ko/translation.json b/public/locales/ko/translation.json index 66400615..303f66bd 100644 --- a/public/locales/ko/translation.json +++ b/public/locales/ko/translation.json @@ -319,7 +319,6 @@ "링크 복사됨": "링크 복사됨", "링크 복사 실패": "링크 복사 실패", "분석 보고서를 만들지 못했어요": "분석 보고서를 만들지 못했어요", - "{{count}}문장": "{{count}}문장", "{{label}}: 속기록에서 보기": "{{label}}: 속기록에서 보기", "{{side}} 우세": "{{side}} 우세", "근거 없음": "근거 없음", @@ -349,8 +348,6 @@ "타이머를 시작하면 기록해요": "타이머를 시작하면 기록해요", "토론 기록": "토론 기록", "토론 기록 ID가 올바르지 않습니다.": "토론 기록 ID가 올바르지 않습니다.", - "토론 기록 끄기": "토론 기록 끄기", - "토론 기록 켜기": "토론 기록 켜기", "토론 기록을 보내고 있어요": "토론 기록을 보내고 있어요", "토론 기록을 보내지 못했어요": "토론 기록을 보내지 못했어요", "토론 음성 기록에 동의하시나요?": "토론 음성 기록에 동의하시나요?", @@ -361,7 +358,6 @@ "음성 인식 방식": "음성 인식 방식", "발언 구간이 끝날 때마다 녹음한 오디오를 서버로 보내 Soniox로 텍스트로 바꿔요. 토론이 끝나면 속기록을 서버로 보내 AI가 분석 보고서를 만들어요.": "발언 구간이 끝날 때마다 녹음한 오디오를 서버로 보내 Soniox로 텍스트로 바꿔요. 토론이 끝나면 속기록을 서버로 보내 AI가 분석 보고서를 만들어요.", "이 브라우저에서는 토론 기록을 사용할 수 없어요": "이 브라우저에서는 토론 기록을 사용할 수 없어요", - "녹음 중": "녹음 중", "{{count}}개 구간 전사 중": "{{count}}개 구간 전사 중", "{{count}}개 구간 전사 실패": "{{count}}개 구간 전사 실패", "발언을 텍스트로 바꾸고 있어요 ({{count}}개 구간 남음)": "발언을 텍스트로 바꾸고 있어요 ({{count}}개 구간 남음)", @@ -381,5 +377,9 @@ "주제를 알 수 없어요": "주제를 알 수 없어요", "아직 토론 기록이 없어요": "아직 토론 기록이 없어요", "타이머 화면에서 녹음을 켜고 토론을 진행해 보세요.": "타이머 화면에서 녹음을 켜고 토론을 진행해 보세요.", - "타이머로 진행한 토론이 끝날 때마다 속기록과 분석 보고서가 자동으로 쌓여요.": "타이머로 진행한 토론이 끝날 때마다 속기록과 분석 보고서가 자동으로 쌓여요." + "타이머로 진행한 토론이 끝날 때마다 속기록과 분석 보고서가 자동으로 쌓여요.": "타이머로 진행한 토론이 끝날 때마다 속기록과 분석 보고서가 자동으로 쌓여요.", + "토론 녹화하기": "토론 녹화하기", + "토론 녹음 중": "토론 녹음 중", + "토론 녹음 끄기": "토론 녹음 끄기", + "{{count}}문장 기록": "{{count}}문장 기록" } diff --git a/src/page/TimerPage/components/TranscribeControl.test.tsx b/src/page/TimerPage/components/TranscribeControl.test.tsx index 76a2606f..9c5782da 100644 --- a/src/page/TimerPage/components/TranscribeControl.test.tsx +++ b/src/page/TimerPage/components/TranscribeControl.test.tsx @@ -38,7 +38,7 @@ describe('토론 기록 켜기 컨트롤', () => { const user = userEvent.setup(); const props = renderControl(); - await user.click(screen.getByRole('button', { name: '토론 기록 켜기' })); + await user.click(screen.getByRole('button', { name: '토론 녹화하기' })); expect(props.onEnable).not.toHaveBeenCalled(); expect( screen.getByText('토론 음성 기록에 동의하시나요?'), @@ -53,7 +53,7 @@ describe('토론 기록 켜기 컨트롤', () => { const user = userEvent.setup(); const props = renderControl(); - await user.click(screen.getByRole('button', { name: '토론 기록 켜기' })); + await user.click(screen.getByRole('button', { name: '토론 녹화하기' })); await user.click( screen.getByRole('radio', { name: /발언 후 일괄 인식 \(Soniox\)/ }), ); @@ -69,7 +69,7 @@ describe('토론 기록 켜기 컨트롤', () => { const user = userEvent.setup(); renderControl({ supportedModes: { RPC: false, SONIOX: true } }); - await user.click(screen.getByRole('button', { name: '토론 기록 켜기' })); + await user.click(screen.getByRole('button', { name: '토론 녹화하기' })); expect( screen.getByRole('radio', { name: /실시간 인식 \(크롬\)/ }), @@ -83,7 +83,7 @@ describe('토론 기록 켜기 컨트롤', () => { const user = userEvent.setup(); const props = renderControl({ sttMode: 'SONIOX', isModeLocked: true }); - await user.click(screen.getByRole('button', { name: '토론 기록 켜기' })); + await user.click(screen.getByRole('button', { name: '토론 녹화하기' })); expect(screen.queryByRole('radio')).not.toBeInTheDocument(); await user.click(screen.getByRole('button', { name: '동의하고 기록하기' })); @@ -94,24 +94,33 @@ describe('토론 기록 켜기 컨트롤', () => { const user = userEvent.setup(); const props = renderControl(); - await user.click(screen.getByRole('button', { name: '토론 기록 켜기' })); + await user.click(screen.getByRole('button', { name: '토론 녹화하기' })); await user.click(screen.getByRole('button', { name: '취소' })); expect(props.onEnable).not.toHaveBeenCalled(); }); - it('기록 중에 누르면 기록을 끈다', async () => { + it('녹음하지 않을 때는 토론 녹화하기 버튼을 보여준다', () => { + renderControl(); + + const button = screen.getByRole('button', { name: '토론 녹화하기' }); + expect(button).toHaveTextContent('토론 녹화하기'); + expect(button).toHaveAttribute('aria-pressed', 'false'); + }); + + it('녹음 중에는 토론 녹음 중 태그를 보여주고, 누르면 녹음을 끈다', async () => { const user = userEvent.setup(); const props = renderControl({ isEnabled: true }); - const button = screen.getByRole('button', { name: '토론 기록 끄기' }); + const button = screen.getByRole('button', { name: '토론 녹음 끄기' }); + expect(button).toHaveTextContent('토론 녹음 중'); expect(button).toHaveAttribute('aria-pressed', 'true'); await user.click(button); expect(props.onDisable).toHaveBeenCalledTimes(1); }); - it('크롬 실시간 인식도 인식 중인 문장 대신 녹음 중 상태와 기록한 문장 수만 보여준다', () => { + it('크롬 실시간 인식도 인식 중인 문장 대신 녹음 중 태그와 기록한 문장 수만 보여준다', () => { renderControl({ sttMode: 'RPC', isEnabled: true, @@ -119,8 +128,8 @@ describe('토론 기록 켜기 컨트롤', () => { utteranceCount: 3, }); - expect(screen.getByText('녹음 중')).toBeInTheDocument(); - expect(screen.getByText('3문장')).toBeInTheDocument(); + expect(screen.getByText('토론 녹음 중')).toBeInTheDocument(); + expect(screen.getByText('3문장 기록')).toBeInTheDocument(); }); it('타이머 화면에 처음 들어오면 녹음할지 묻는 동의 창을 연다', async () => { @@ -152,7 +161,7 @@ describe('토론 기록 켜기 컨트롤', () => { failedCount: 1, }); - expect(screen.getByText('녹음 중')).toBeInTheDocument(); + expect(screen.getByText('토론 녹음 중')).toBeInTheDocument(); expect(screen.getByText('2개 구간 전사 중')).toBeInTheDocument(); expect(screen.getByText('1개 구간 전사 실패')).toBeInTheDocument(); }); @@ -161,7 +170,7 @@ describe('토론 기록 켜기 컨트롤', () => { renderControl({ supportedModes: { RPC: false, SONIOX: false } }); expect( - screen.queryByRole('button', { name: '토론 기록 켜기' }), + screen.queryByRole('button', { name: '토론 녹화하기' }), ).not.toBeInTheDocument(); expect( screen.getByText('이 브라우저에서는 토론 기록을 사용할 수 없어요'), diff --git a/src/page/TimerPage/components/TranscribeControl.tsx b/src/page/TimerPage/components/TranscribeControl.tsx index c8404d6f..0da491ee 100644 --- a/src/page/TimerPage/components/TranscribeControl.tsx +++ b/src/page/TimerPage/components/TranscribeControl.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { RiMicFill, RiMicOffFill } from 'react-icons/ri'; +import { RiMicFill } from 'react-icons/ri'; import clsx from 'clsx'; import { useModal } from '../../../hooks/useModal'; import DialogModal from '../../../components/DialogModal/DialogModal'; @@ -26,9 +26,9 @@ export interface TranscribeControlProps { const STT_MODES: SttMode[] = ['RPC', 'SONIOX']; -// 바로 위 라이브 공유 버튼과 같은 크기·모양으로 맞춘다. -const MIC_BUTTON_CLASS = - 'relative flex size-[88px] items-center justify-center rounded-full border-[2px] p-[22px] shadow-lg transition-colors'; +// 녹음하지 않을 때는 "토론 녹화하기" 버튼, 녹음 중에는 "토론 녹음 중" 태그로 보이는 타원형 버튼 +const RECORD_BUTTON_CLASS = + 'flex items-center gap-2 rounded-full border-2 px-5 py-2.5 text-base font-bold whitespace-nowrap shadow-lg transition-colors'; const STATUS_PILL_CLASS = 'flex w-fit items-center gap-2 rounded-full bg-default-black/80 px-3 py-1.5 text-sm font-semibold text-default-white'; const MODE_OPTION_CLASS = @@ -105,7 +105,7 @@ export default function TranscribeControl(props: TranscribeControlProps) { } const consentMode = isModeLocked ? sttMode : selectedMode; - const label = isEnabled ? t('토론 기록 끄기') : t('토론 기록 켜기'); + const label = isEnabled ? t('토론 녹음 끄기') : t('토론 녹화하기'); const handleClick = () => { if (isEnabled) { @@ -124,8 +124,8 @@ export default function TranscribeControl(props: TranscribeControlProps) { return ( <> - {/* 오른쪽 위 공유 버튼 밑에 두므로 상태 표시는 버튼 왼쪽에 오른쪽 정렬한다. */} -
+ {/* 오른쪽 위 공유 버튼 밑에 두므로 버튼과 상태 표시를 오른쪽 정렬한다. */} +
- -
- {isEnabled && ( -

+ <>

- )} - {isEnabled && pendingCount > 0 && ( -

- {t('{{count}}개 구간 전사 중', { count: pendingCount })} -

- )} - {failedCount > 0 && ( -

- {t('{{count}}개 구간 전사 실패', { count: failedCount })} -

- )} - {error === 'not-allowed' && ( -

- {t('마이크 권한을 허용해야 토론을 기록할 수 있어요')} -

+ {t('토론 녹음 중')} + + ) : ( + <> +
+ + + {isEnabled && ( +

+ {isListening + ? t('{{count}}문장 기록', { count: utteranceCount }) + : t('타이머를 시작하면 기록해요')} +

+ )} + {isEnabled && pendingCount > 0 && ( +

+ {t('{{count}}개 구간 전사 중', { count: pendingCount })} +

+ )} + {failedCount > 0 && ( +

+ {t('{{count}}개 구간 전사 실패', { count: failedCount })} +

+ )} + {error === 'not-allowed' && ( +

+ {t('마이크 권한을 허용해야 토론을 기록할 수 있어요')} +

+ )}
From d345636b9e26165a55c67f3c13a94092d8e7dca2 Mon Sep 17 00:00:00 2001 From: coli Date: Sat, 24 Oct 2026 02:01:08 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[design]=20=ED=86=A0=EB=A1=A0=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=B9=B4=EB=93=9C=EB=A5=BC=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=ED=91=9C=20=EC=B9=B4=EB=93=9C=EC=99=80=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95=20=ED=81=AC=EA=B8=B0=EB=A1=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주제 길이나 유무와 상관없이 340×220 카드를 줄 바꿈으로 놓아 크기가 들쭉날쭉하지 않게 한다. Co-Authored-By: Claude Opus 5.5 --- .../components/TranscriptListContent.test.tsx | 10 ++++++++++ .../components/TranscriptListContent.tsx | 13 +++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/page/TableListPage/components/TranscriptListContent.test.tsx b/src/page/TableListPage/components/TranscriptListContent.test.tsx index 5faad296..dadca394 100644 --- a/src/page/TableListPage/components/TranscriptListContent.test.tsx +++ b/src/page/TableListPage/components/TranscriptListContent.test.tsx @@ -103,4 +103,14 @@ describe('대시보드 토론 기록 목록', () => { await screen.findByText('아직 토론 기록이 없어요'), ).toBeInTheDocument(); }); + + it('주제 길이나 유무와 상관없이 모든 카드를 시간표 카드와 같은 고정 크기로 보여준다', async () => { + renderList(); + + const cards = await screen.findAllByRole('link'); + + cards.forEach((card) => { + expect(card).toHaveClass('h-[220px]', 'w-[340px]'); + }); + }); }); diff --git a/src/page/TableListPage/components/TranscriptListContent.tsx b/src/page/TableListPage/components/TranscriptListContent.tsx index 497dbc62..d03d639c 100644 --- a/src/page/TableListPage/components/TranscriptListContent.tsx +++ b/src/page/TableListPage/components/TranscriptListContent.tsx @@ -13,7 +13,7 @@ import { // 노란 시간표 카드와 구분되도록 기본블랙(차콜) 카드에 테마 노랑으로 강조한다. const CARD_CLASS = - 'flex h-[200px] flex-col gap-3 rounded-[28px] border-2 p-6 text-left text-default-white shadow-lg transition-all duration-300 hover:-translate-y-1'; + 'm-5 flex h-[220px] w-[340px] flex-col gap-3 rounded-[28px] border-2 p-6 text-left text-default-white shadow-lg transition-all duration-300 hover:-translate-y-1'; const GENERATING_CHIP_CLASS = 'flex flex-none items-center gap-1.5 rounded-full bg-brand px-2.5 py-1 text-xs font-bold text-default-black'; @@ -48,18 +48,18 @@ export default function TranscriptListContent() { } return ( -
-

+

+

{t('토론 기록')}

-

+

{t( '타이머로 진행한 토론이 끝날 때마다 속기록과 분석 보고서가 자동으로 쌓여요.', )}

{data.transcripts.length === 0 && ( -
+

{t('아직 토론 기록이 없어요')}

{t('타이머 화면에서 녹음을 켜고 토론을 진행해 보세요.')} @@ -67,7 +67,8 @@ export default function TranscriptListContent() {

)} -
+ {/* 시간표 탭처럼 주제 길이와 상관없이 같은 크기의 카드를 줄 바꿈으로 놓는다. */} +
{data.transcripts.map((transcript) => { const generating = isGenerating(transcript); return (