[FEAT] 청중용 라이브 토론 공유 페이지 개선#464
Conversation
|
Warning Review limit reached
Next review available in: 51 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
Walkthrough청중용 공유 페이지에 진행 바와 시간 기반 타이머 표시를 추가하고, 카운트다운·소켓·이벤트 상태 전이와 로딩/오류 렌더링을 확장했다. TimerPage는 공유 이벤트의 잔여 시간 계산과 FINISHED 발행 흐름을 정리했다. Changes타이머 UI와 표시 컴포넌트
카운트다운과 이벤트 상태 전이
청중 공유 페이지 통합
공유 이벤트 잔여 시간 발행
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 Preview 배포 완료!
|
There was a problem hiding this comment.
Code Review
This pull request implements and refactors the audience share page (AudienceSharePage) and its associated timers (AudienceNormalTimer, AudienceTimeBasedTimerDisplay) to fetch debate table data and synchronize normal and time-based countdowns via sockets. It also introduces the useAudienceTimeBasedCountdown hook and updates socket-related hooks and pages to handle state transitions, accompanied by comprehensive unit tests. The review feedback suggests initializing useMotionValue with the current progress in TimerProgressBar to prevent initial animation glitches, and recommends adding defensive nullish checks before calling .trim() on potentially null or undefined fields (such as teamName, name, and agenda) to avoid runtime errors.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| className, | ||
| }: TimerProgressBarProps) { | ||
| const normalizedProgress = clamp(0, 100, progress); | ||
| const progressMotionValue = useMotionValue(0); |
There was a problem hiding this comment.
페이지가 처음 렌더링되거나 새로고침될 때, 타이머가 이미 진행 중인 상태(예: 80%)라면 진행률 바가 0%부터 80%까지 애니메이션되는 시각적 어색함이 발생할 수 있습니다. useMotionValue를 normalizedProgress로 초기화하면 첫 렌더링 시에는 애니메이션 없이 현재 진행률을 즉시 표시하고, 이후 변경 사항에 대해서만 부드럽게 애니메이션되도록 개선할 수 있습니다.
| const progressMotionValue = useMotionValue(0); | |
| const progressMotionValue = useMotionValue(normalizedProgress); |
| const teamLabel = | ||
| teamName.trim() === '' | ||
| ? t('팀명 없음') | ||
| : t('{{team}} 팀', { team: t(teamName) }); |
There was a problem hiding this comment.
API 응답 데이터에 teamName이 누락되거나 null 또는 undefined로 전달될 경우, teamName.trim() 호출 시 런타임 에러(TypeError)가 발생할 수 있습니다. 안전한 실행을 위해 teamName에 대한 nullish 체크를 추가하는 방어적 프로그래밍을 적용하는 것이 좋습니다.
| const teamLabel = | |
| teamName.trim() === '' | |
| ? t('팀명 없음') | |
| : t('{{team}} 팀', { team: t(teamName) }); | |
| const teamLabel = | |
| !teamName || teamName.trim() === '' | |
| ? t('팀명 없음') | |
| : t('{{team}} 팀', { team: t(teamName) }); |
| const teamLabel = | ||
| teamName.trim() === '' | ||
| ? t('팀명 없음') | ||
| : t('{{team}} 팀', { team: t(teamName) }); |
There was a problem hiding this comment.
API 응답 데이터에 teamName이 누락되거나 null 또는 undefined로 전달될 경우, teamName.trim() 호출 시 런타임 에러(TypeError)가 발생할 수 있습니다. 안전한 실행을 위해 teamName에 대한 nullish 체크를 추가하는 방어적 프로그래밍을 적용하는 것이 좋습니다.
| const teamLabel = | |
| teamName.trim() === '' | |
| ? t('팀명 없음') | |
| : t('{{team}} 팀', { team: t(teamName) }); | |
| const teamLabel = | |
| !teamName || teamName.trim() === '' | |
| ? t('팀명 없음') | |
| : t('{{team}} 팀', { team: t(teamName) }); |
| name={ | ||
| debateTableQuery.data.info.name.trim() === '' | ||
| ? t('테이블 이름 없음') | ||
| : t(debateTableQuery.data.info.name) | ||
| } | ||
| /> | ||
| </DefaultLayout.Header.Left> | ||
| <DefaultLayout.Header.Center> | ||
| <HeaderTitle | ||
| title={ | ||
| debateTableQuery.data.info.agenda.trim() === '' | ||
| ? t('주제 없음') | ||
| : t(debateTableQuery.data.info.agenda) | ||
| } | ||
| /> |
There was a problem hiding this comment.
API 응답 데이터의 name 또는 agenda가 누락되거나 null 또는 undefined로 전달될 경우, .trim() 호출 시 런타임 에러가 발생할 수 있습니다. 안전한 실행을 위해 nullish 체크를 추가하는 방어적 프로그래밍을 적용하는 것이 좋습니다.
| name={ | |
| debateTableQuery.data.info.name.trim() === '' | |
| ? t('테이블 이름 없음') | |
| : t(debateTableQuery.data.info.name) | |
| } | |
| /> | |
| </DefaultLayout.Header.Left> | |
| <DefaultLayout.Header.Center> | |
| <HeaderTitle | |
| title={ | |
| debateTableQuery.data.info.agenda.trim() === '' | |
| ? t('주제 없음') | |
| : t(debateTableQuery.data.info.agenda) | |
| } | |
| /> | |
| name={ | |
| !debateTableQuery.data.info.name || debateTableQuery.data.info.name.trim() === '' | |
| ? t('테이블 이름 없음') | |
| : t(debateTableQuery.data.info.name) | |
| } | |
| /> | |
| </DefaultLayout.Header.Left> | |
| <DefaultLayout.Header.Center> | |
| <HeaderTitle | |
| title={ | |
| !debateTableQuery.data.info.agenda || debateTableQuery.data.info.agenda.trim() === '' | |
| ? t('주제 없음') | |
| : t(debateTableQuery.data.info.agenda) | |
| } | |
| /> |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/hooks/sockets/useAudienceSocket.ts`:
- Around line 84-86: Update the enabled-state handling in useAudienceSocket so
transitioning enabled from true to false also clears latestMessage when
unsubscribing. Preserve the existing subscription behavior for enabled states
and ensure consumers cannot read the previous event while the subscription is
disabled.
In `@src/page/AudienceSharePage/AudienceSharePage.test.tsx`:
- Line 14: Replace the direct mock of useGetDebateTableDataForShare in
AudienceSharePage tests with MSW request handlers. Configure handlers covering
successful, failed, and delayed API responses so the tests exercise the real
query hook and page integration state transitions; remove the hook mock and
update affected test setup and assertions accordingly.
In `@src/page/AudienceSharePage/AudienceSharePage.tsx`:
- Around line 192-199: Update the hasInvalidNormalTimeBox ||
hasInvalidTimeBasedTimeBox branch in AudienceSharePage so validation failures
display a configuration-error message instead of the server-connection failure
text. Preserve the existing ErrorContent and handleReload behavior, and use the
repository’s Korean-string i18n key convention for the new message.
In `@src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.tsx`:
- Around line 79-83: AudienceTimeBasedTimerDisplay의 현재 발언 상태 안내 조건을 수정해,
isCurrentTeam뿐 아니라 타이머가 실행 중인 상태일 때만 `현재 발언 중`을 렌더링하세요. 일시 정지 상태에서는 해당 스크린 리더
안내가 표시되지 않도록 기존 실행 상태 변수를 함께 확인하고, 그 외 렌더링 동작은 유지하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 49d1a801-6e78-4761-b5d5-755910e6006d
📒 Files selected for processing (21)
src/components/TimerProgressBar/TimerProgressBar.test.tsxsrc/components/TimerProgressBar/TimerProgressBar.tsxsrc/hooks/sockets/useAudienceSocket.test.tssrc/hooks/sockets/useAudienceSocket.tssrc/page/AudienceSharePage/AudienceSharePage.test.tsxsrc/page/AudienceSharePage/AudienceSharePage.tsxsrc/page/AudienceSharePage/components/AudienceNormalTimer.test.tsxsrc/page/AudienceSharePage/components/AudienceNormalTimer.tsxsrc/page/AudienceSharePage/components/AudienceTimeBasedTimer.test.tsxsrc/page/AudienceSharePage/components/AudienceTimeBasedTimer.tsxsrc/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.test.tsxsrc/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.tsxsrc/page/AudienceSharePage/hooks/useAudienceCountdown.test.tssrc/page/AudienceSharePage/hooks/useAudienceCountdown.tssrc/page/AudienceSharePage/hooks/useAudienceShareState.test.tssrc/page/AudienceSharePage/hooks/useAudienceShareState.tssrc/page/AudienceSharePage/hooks/useAudienceTimeBasedCountdown.test.tssrc/page/AudienceSharePage/hooks/useAudienceTimeBasedCountdown.tssrc/page/TimerPage/TimerPage.tsxsrc/page/TimerPage/getRemainingTimeForShare.test.tssrc/page/TimerPage/getRemainingTimeForShare.ts
| if (!enabled) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
구독을 비활성화할 때 이전 메시지도 초기화하세요.
enabled가 true에서 false로 바뀌면 구독은 해제되지만 latestMessage는 남아, 소비자가 비활성 기간에 직전 이벤트를 현재 메시지로 읽을 수 있습니다.
수정 예시
if (!enabled) {
+ resetMessage();
return;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!enabled) { | |
| return; | |
| } | |
| if (!enabled) { | |
| resetMessage(); | |
| return; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/sockets/useAudienceSocket.ts` around lines 84 - 86, Update the
enabled-state handling in useAudienceSocket so transitioning enabled from true
to false also clears latestMessage when unsubscribing. Preserve the existing
subscription behavior for enabled states and ensure consumers cannot read the
previous event while the subscription is disabled.
|
|
||
| // 모의 (Mock) | ||
| vi.mock('./hooks/useAudienceShareState'); | ||
| vi.mock('../../hooks/query/useGetDebateTableDataForShare'); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
API 쿼리 훅 직접 모킹을 MSW 핸들러로 교체하세요.
현재 방식은 실제 요청·쿼리 상태 연동을 우회합니다. 성공, 실패, 지연 응답을 MSW 핸들러로 구성해 페이지 통합 경로를 검증해야 합니다.
As per coding guidelines, “Use MSW (Mock Service Worker) for API mocking in tests, minimize other mocks” 규칙을 적용해야 합니다. <coding_guidelines>
Also applies to: 79-89
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/page/AudienceSharePage/AudienceSharePage.test.tsx` at line 14, Replace
the direct mock of useGetDebateTableDataForShare in AudienceSharePage tests with
MSW request handlers. Configure handlers covering successful, failed, and
delayed API responses so the tests exercise the real query hook and page
integration state transitions; remove the hook mock and update affected test
setup and assertions accordingly.
Source: Coding guidelines
| if (hasInvalidNormalTimeBox || hasInvalidTimeBasedTimeBox) { | ||
| return ( | ||
| <ErrorContent | ||
| message={t('서버 연결에 실패했어요.')} | ||
| onReload={handleReload} | ||
| /> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
잘못된 타임박스를 소켓 연결 장애로 표시하지 마세요.
이 분기는 API 시간 설정 검증 실패이므로 “서버 연결에 실패했어요.”는 원인을 잘못 안내합니다. 설정 오류임을 명확히 표시해 주세요.
수정 예시
<ErrorContent
- message={t('서버 연결에 실패했어요.')}
+ message={t('토론 시간 설정을 확인할 수 없어요.')}
onReload={handleReload}
/>Based on learnings, 이 저장소의 TSX i18n 키는 한국어 문자열 규칙을 유지했습니다. <retrieved_learnings>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (hasInvalidNormalTimeBox || hasInvalidTimeBasedTimeBox) { | |
| return ( | |
| <ErrorContent | |
| message={t('서버 연결에 실패했어요.')} | |
| onReload={handleReload} | |
| /> | |
| ); | |
| } | |
| if (hasInvalidNormalTimeBox || hasInvalidTimeBasedTimeBox) { | |
| return ( | |
| <ErrorContent | |
| message={t('토론 시간 설정을 확인할 수 없어요.')} | |
| onReload={handleReload} | |
| /> | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/page/AudienceSharePage/AudienceSharePage.tsx` around lines 192 - 199,
Update the hasInvalidNormalTimeBox || hasInvalidTimeBasedTimeBox branch in
AudienceSharePage so validation failures display a configuration-error message
instead of the server-connection failure text. Preserve the existing
ErrorContent and handleReload behavior, and use the repository’s Korean-string
i18n key convention for the new message.
Source: Learnings
| {isCurrentTeam ? ( | ||
| <span className="sr-only" data-testid={`${teamId}-speaking-status`}> | ||
| {t('현재 발언 중')} | ||
| </span> | ||
| ) : null} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
정지 상태를 “현재 발언 중”으로 안내하지 마세요.
현재 팀이지만 일시 정지된 경우에도 스크린 리더가 현재 발언 중으로 안내합니다. 실행 상태도 함께 확인해야 합니다.
수정안
- {isCurrentTeam ? (
+ {isCurrentTeam && isRunning ? (📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {isCurrentTeam ? ( | |
| <span className="sr-only" data-testid={`${teamId}-speaking-status`}> | |
| {t('현재 발언 중')} | |
| </span> | |
| ) : null} | |
| {isCurrentTeam && isRunning ? ( | |
| <span className="sr-only" data-testid={`${teamId}-speaking-status`}> | |
| {t('현재 발언 중')} | |
| </span> | |
| ) : null} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.tsx`
around lines 79 - 83, AudienceTimeBasedTimerDisplay의 현재 발언 상태 안내 조건을 수정해,
isCurrentTeam뿐 아니라 타이머가 실행 중인 상태일 때만 `현재 발언 중`을 렌더링하세요. 일시 정지 상태에서는 해당 스크린 리더
안내가 표시되지 않도록 기존 실행 상태 변수를 함께 확인하고, 그 외 렌더링 동작은 유지하세요.
useon
left a comment
There was a problem hiding this comment.
숀 !!! 리뷰가 늦어 죄송합니다 ㅜㅜ .. 이번 경우에는 전체적인 공유 페이지 개선이라 코드가 많이 바뀌어 어쩔 수 없었을 것 같아요 👍😊 다음에는 쪼매 작은 단위로 올려주시면 리뷰를 더 빨리할 수 있을 것 같아요!!! 몇 가지 코멘트 남겼으니 편하게 확인해 주세요!! 라이브 공유 관련 구현 감사해요 ~!!
| * @returns {Error | null} returns.error - 가장 최근에 발생한 소켓 오류입니다. | ||
| */ | ||
| export default function useAudienceSocket(roomId: number) { | ||
| interface UseAudienceSocketOptions { |
There was a problem hiding this comment.
인터페이스 이름이 use로 시작이 되어서 살짝 훅 같이 헷갈리는 것 같아요 !
AudienceSocketOptions 요런 이름은 어떨까요?
| roomId: number, | ||
| options: UseAudienceSocketOptions = {}, | ||
| ) { | ||
| const { enabled = true } = options; |
There was a problem hiding this comment.
현재는 AudienceSharePage에서 API 조회 성공 이후에만 소켓을 붙이기 위해 enabled를 넘기고 있는데, 훅 자체의 기본값은 true라서 옵션을 빼먹으면 바로 구독이 시작됩니다. 의도한 설계라면 괜찮지만, 청중 소켓처럼 roomId와 초기 데이터 준비 순서가 중요한 훅이라면 기본값을 명시적으로 둘지, 호출부에서 항상 enabled를 넘기게 할지 정해두면 더 예측 가능할 것 같습니다 !!
| return ( | ||
| <span | ||
| className={clsx( | ||
| 'grid w-[5ch] grid-cols-[2ch_1ch_2ch] items-center justify-center gap-x-[0.33ch] font-bold tabular-nums', |
There was a problem hiding this comment.
이 곳에서만 ch 단위를 쓰신 이유가 있으실까요 ??
| onReload: () => void; | ||
| } | ||
|
|
||
| function LoadingContent() { |
There was a problem hiding this comment.
지금 AudienceSharePage가 API 로딩/에러 처리뿐 아니라 소켓 이벤트 해석, NEXT/BEFORE/RESET/TEAM_SWITCH에 따른 sequence 계산, timeBox 유효성 검증, 일반/자유토론 타이머 분기까지 한 번에 처리하고 있습니다. 그래서 가독성이 떨어지고 복잡도가 높아진 것 같아요.
소켓 이벤트를 받아 청중 화면 상태를 만드는 로직을 별도 도메인 함수로 분리하는 방법은 어떨까요? 예를 들면 WAITING | NORMAL_TIMER | TIME_BASED_TIMER | FINISHED | ERROR 같은 화면 상태를 먼저 만들고, 페이지는 그 결과만 렌더링하는 ..
🚩 연관 이슈
closed #460
📝 작업 내용
1. 청중용 테이블 조회 API 연동
기존 청중 화면은 실시간 소켓 메시지만 받을 수 있어 “몇 초가 남았는지”는 알 수 있었지만, 그 시간이 어떤 발언 순서와 팀에 해당하는지는 표현할 수 없었습니다. 새로 제공된 청중용 테이블 조회 API를
AudienceSharePage에 연결하여 이 제약을 해소했습니다.useGetDebateTableDataForShare로 테이블 정보와 전체 타이머 순서를 조회합니다.useAudienceSocket에enabled옵션을 추가했습니다.sequence와 API의table[sequence]를 결합해 현재 순서의 실제 설정을 표시합니다.2. 일반 발언 타이머 화면 완성
단순히 남은 시간만 표시하던 화면을 실제 토론 테이블 설정에 맞게 확장했습니다.
speechType)을 제목으로 표시합니다.MM:SS형식의 고정폭 타이머와 초과 시간의 음수 표시를 지원합니다.TimerProgressBar로 표시합니다.3. 자유토론 타이머의 팀별 시간 관리
자유토론은 팀 전체 시간과 1회당 발언 시간이 함께 움직이므로 일반 카운트다운만으로는 정확한 상태를 표현할 수 없습니다. 이를 위해 팀별 규칙을 담당하는
useAudienceTimeBasedCountdown을 별도로 추가했습니다.00:00에 도달하면 해당 팀의 카운트다운을 함께 정지합니다.STOP후에는 표시값을 유지하고,PLAY시 유지한 값에서 다시 시작합니다.TEAM_SWITCH시 이전 팀의 시간을 보존하고 새 팀의 현재 발언 시간을 남은 전체 시간에 맞춰 초기화합니다.RESET은 현재 팀을,BEFORE/NEXT는 이동한 순서의 양 팀을 API 설정값으로 다시 초기화합니다.timePerSpeaking === null인 순서는 팀 전체 시간을 한 번에 사용할 수 있는 모드로 처리합니다.저수준 시간 감소와 포맷팅은 기존
useAudienceCountdown이 계속 담당하며, 자유토론 훅은 네 개의 카운트다운을 조합하고 소켓 이벤트 규칙을 조정하는 역할만 담당합니다.syncKey,minimumTime, 실행 상태 전환 옵션을 추가하여 일반 타이머의 초과 시간 동작은 유지하면서 자유토론 시간만 0초에서 제한했습니다.4. 자유토론 전용 2열 UI 구성
AudienceTimeBasedTimer는 두 팀의 배치만 담당하고, 팀 하나의 세부 표현은 재사용 가능한AudienceTimeBasedTimerDisplay로 분리했습니다.timePerSpeaking === null이면 팀 이름, 전체 잔여 시간, 진행도를 표시합니다.aria-current="step"과 스크린 리더용 “현재 발언 중” 상태를 제공합니다.00:00, 진행도는 0~100 범위로 제한합니다.5. 사회자 화면의 소켓 잔여 시간 수정
기존에는 자유토론 소켓 payload가 항상 현재 발언 타이머를 사용해, 1회당 발언 시간이 없는 순서에서 잘못된 값이 전송될 수 있었습니다.
getRemainingTimeForShare순수 함수를 추가해 전송할 시간을 한 곳에서 결정합니다.normalTimer.timer를 사용합니다.speakingTimer, 없으면totalTimer를 사용합니다.FINISHED는 명시적으로nullpayload를 보내고, 유효한 잔여 시간이 없으면 타이머 이벤트를 발행하지 않습니다.6. 공통 진행도와 접근성 개선
TimerProgressBar를 추가했습니다.role="progressbar",aria-valuemin,aria-valuemax,aria-valuenow를 제공합니다.aria-label을 함께 제공합니다.7. 소켓 상태 정규화와 회귀 테스트 보강
useAudienceShareState가 소켓 메시지를 화면에서 바로 사용할 수 있는 상태로 정규화하도록 확장했습니다.sequence,eventType,revision을 보존합니다.RESET이 연속으로 들어와도 각각 새로운 초기화로 인식합니다.TEAM_SWITCH전후의 실행 상태를 유지하고,BEFORE/NEXT의 실제 이동 대상 인덱스를 보정합니다.boxType을 기준으로 표시 상태를 전환합니다.🧪 확인 사항
npm run lint— ESLint, Stylelint, TypeScript 검사npm run build— Vite 프로덕션 빌드🏞️ 스크린샷 (선택)
일반 발언 타이머 (찬성)
일반 발언 타이머 (반대)
일반 발언 타이머 (중립)
자유토론 — 1회당 발언 시간 없음
자유토론 — 1회당 발언 시간 있음
🗣️ 리뷰 요구사항 (선택)
반드시 청중용 테이블을 로그인 세션과 분리된 다음 환경 중 하나에서 열어 주세요.
그 상태에서 본인의 일반 브라우저 환경에는 사회자용 타이머를 열고, 아래 동작을 직접 실행해 청중 화면이 같은 상태를 표시하는지 확인 부탁드립니다.
특히 사회자 화면의 자유토론 타이머와 청중 화면의 “전체 시간/현재 시간”이 팀 전환과 RESET 전후에 동일하게 움직이는지 중점적으로 리뷰 부탁드립니다.
Summary by CodeRabbit
새 기능
개선 사항