[DEPLOY] 배포#463
Conversation
* docs: Gemini + Codex 멀티 에이전트 개발을 위한 부트스트랩 추가 * feat: 화면 공유를 위한 임시 액세스 토큰 발급 기능 구현 * feat: 소켓 훅 디렉토리 변경 및 테스트 코드 추가 * refactor: SocketManager가 URL을 매개변수로 받도록 수정 * feat: 사회자 및 청중을 위한 소켓 훅 추가 * feat: 공유 아이콘 추가 * chore: 기타 변경 사항 * docs: fix spelling error * fix: apply CodeRabbit's reviews * test: GitHub Actions 러너 환경에서의 테스트 오류 해결 * refactor: 에러 메시지 구체화 * fix: 재구독 또는 새 연결 수립 시 기존 데이터가 사라지지 않는 문제 수정 * fix: 린팅 오류 해결 * test: 테스트 로직 보강 * refactor: ResolvedSocketOption 타입의 가독성 개선 * feat: 웹 소켓 연결 종료 시 TanStack Query의 임시 액세스 토큰 캐시 비우도록 수정 * chore: 린팅 경고 수정
* docs: 국제화 항목 추가 * feat: 화면 공유 로직 도메인 변경 * feat: 사회자용 소켓 Hook에 연결 상태를 나타내는 필드 추가 * feat: UI 컴포넌트 구현 * fix: SockJS 오류 수정 * feat: Story 추가 * fix: 일부 오류 수정 * refactor: 조건식 간소화 * refactor: URL 파싱 로직 보강 * fix: 잘못 불러온 패키지 경로 수정 * fix: 테스트 코드의 모방 경로 수정 * fix: 모달을 닫아도 WS 연결이 끊기지 않게 수정 * test: 모달 테스트 코드가 msw 모방 API를 사용하게 수정 * fix: Hook `useSocket`의 상태 `isConnected`를 소켓 연결 종료 시 반드시 갱신하게 수정 * test: 모달 테스트 코드가 msw로 성공, 로딩, 실패를 다 커버하게 수정 * chore: 영문 번역본 추가 * fix: 비회원 모드에서 공유 버튼 출력되는 오류 수정 * refactor: 상태 관리 책임을 모두 TimerPage로 이동 * refactor: 상태 관리 책임 변경에 따른 모달 코드 정리 * refactor: 상태 관리 책임 변경에 따른 타이머 컴포넌트 변경 * chore: 주석 추가 * refactor: 소켓 종료 책임을 훅 useLiveShare에 완전히 위임 * test: 테스트 스위트 이름을 가독성 있게 변경 * fix: 테스트 코드 오류 해결 * fix: 중복 버튼 문제 해결 * fix: 오류 발생 시 소켓 연결 안 끊기던 문제 수정 * fix: 등호 수정 * fix: 터치 시에도 이벤트가 발행되도록 수정 * refactor: 일부 키보드/마우스 동작 비활성화 * refactor: 페이로드의 팀 항목을 필수에서 선택으로 변경 * fix: A/L 키 다시 활성화
* feat: 에러 수집 노이즈 필터링 및 민감 정보 마스킹 적용 * feat: Sentry 이벤트 언어 컨텍스트 태깅 추가 * feat: API 응답 상태 기반 Sentry 에러 레벨 조정 적용 * feat: API 에러를 엔드포인트 기준으로 묶어 이슈 분산 감소 * refactor: Sentry API 에러 extra에서 baseURL 제거 * fix: UUID 경로가 숫자 치환에 먼저 깨지는 문제 순서 수정으로 해결 * fix: Sentry 에러 그룹에 HTTP 메서드 추가해 서로 다른 요청이 섞이지 않게 수정 * fix: authorization 소문자도 추가하여 대소문자 형태 모두 안전하게 삭제하도록 수정 * fix: Sentry beforeSend에서 optional hint 안전 접근 처리 * fix: i18n 언어 변경 시 Sentry language 태그 갱신
* feat: 응답 형식 추가 * feat: API 함수 및 테스트 코드 구현 * feat: API 사용을 위한 Hook 구현 * test: msw 모방 핸들러 추가 * fix: CI 오류 해결 * refactor: 타입 방어 강화 * refactor: API 함수 컨벤션에 맞게 수정 * fix: 기타 오류 수정
* refactor: 소켓 오류 관련 개선 적용 * feat: 시간 포매팅 함수 추가 * feat: 청중 화면 베이스 타입 추가 * feat: 청중 화면 타이머 훅 추가 * feat: 서브컴포넌트 구현 * feat: 청중용 화면 관리를 위한 훅 구현 * feat: 청중용 화면 구현 * docs: 국제화 키-값 쌍 추가 * test: 테스트 설명 국문으로 변경 * fix: 리뷰 반영 * fix: 오류 해결 * refactor: 불필요한 ! 연산자 제거 * test: 코드 변경에 따른 테스트 코드 대응
* feat: 답변시간 설정 UI 추가 * feat: 타이머 최초 시작 시 답변 시간 타이머 안내하는 기능 구현 * feat: 답변시간 타이머 시작 버튼 구현 * feat: 답변시간 타이머 진행바 UI 추가 * refactor: 답변시간 타이머 표시 상태를 TimerView로 이동 * feat: 답변시간 타이머 카운트업 및 진행바 표시 개선 * feat: 답변시간 타이머 시작 조건 추가 * feat: 답변시간 타이머 자동 초기화 추가 * refactor: 답변시간 타이머 수동 초기화 흐름 명시 * feat: 답변시간 타이머 Shift 단축키 연결 * feat: 답변시간 타이머 노출 전환 애니메이션 추가 * refactor: 답변시간 타이머 초기화 경로 통일 * fix: 답변시간 타이머 보장 상태 색상 정책 수정 * fix: 답변 타이머 fade-out시 요소 덜컹거리는 문제 해결 * fix: 답변타이머 안내 모달 자동 닫힘 안정화 * refactor: 답변타이머 클릭 핸들러 참조 안정화 * fix: 답변타이머 시작 버튼 비활성화 동작 수정 * refactor: 답변타이머를 필수 prop으로 받도록 수정 * refactor: 답변 타이머 로직 커스텀 훅으로 분리 * refactor: 답변 타이머 컴포넌트 분리
* chore: 토론 라이브 공유 기능 비활성화 * fix: 빌드에 영향 주는 코드 주석 처리 * chore: Feature Flag 방식으로 수정
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ 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 |
There was a problem hiding this comment.
Code Review
This pull request introduces a live sharing feature for debate timers, including audience and chairman socket hooks, an audience share page, and live share UI components. It also adds an answer time timer with setting controls and hotkey support, improves Sentry error tracking, and refactors modal wrapper rendering. The reviewer feedback highlights several critical issues: a potential remounting bug in useModal due to unstable onClose references, a state synchronization issue in handleTimerEvent where stale state is captured during transition events, an over-broad regex in normalizeEndpoint that incorrectly replaces API version paths, and a contradiction in useTimerHotkey where KeyA and KeyL hotkeys remain active despite being documented as disabled.
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.
| const { | ||
| closeOnOverlayClick = true, | ||
| isCloseButtonExist = true, | ||
| onClose = () => {}, | ||
| onClose = noop, | ||
| } = options; |
There was a problem hiding this comment.
useModal 훅에서 반환하는 ModalWrapper 컴포넌트가 useCallback으로 감싸져 있지만, 의존성 배열에 closeModal이 포함되어 있습니다. closeModal은 onClose 함수에 의존하는데, 만약 사용처에서 useModal({ onClose: () => { ... } })와 같이 인라인 화살표 함수를 전달하면 매 렌더링마다 onClose 참조가 변경됩니다.
이로 인해 closeModal -> handleOverlayClick -> ModalWrapper 순으로 참조가 매번 새로 생성되어, 부모 컴포넌트가 리렌더링될 때마다 모달 내부의 전체 DOM과 상태가 파괴되고 새로 마운트(Remount)되는 심각한 버그가 발생할 수 있습니다.
이를 방지하기 위해 onClose를 useRef에 저장하여 closeModal과 ModalWrapper 컴포넌트의 참조를 완전히 안정화하는 것을 권장합니다. 예를 들어 다음과 같이 리팩토링할 수 있습니다:
const onCloseRef = useRef(onClose);
useEffect(() => {
onCloseRef.current = onClose;
}, [onClose]);
const closeModal = useCallback(() => {
setIsOpen(false);
onCloseRef.current();
}, []);(파일 상단에 useRef, useEffect 임포트가 필요합니다.)
| const handleTimerEvent = (invoke: () => void, eventType: SocketEventType) => { | ||
| // 이벤트 실행 | ||
| invoke(); | ||
|
|
||
| // 만약 소켓 열려 있으면, 발송 | ||
| if (isSocketConnected) { | ||
| // 타입에 따른 페이로드 준비 | ||
| let innerPayload: TimerDataPayload; | ||
|
|
||
| if (timerType === 'NORMAL') { | ||
| innerPayload = { | ||
| timerType: timerType, | ||
| remainingTime: remainingTime, | ||
| sequence: index, | ||
| } as TimerDataPayload; | ||
| } else if (timerType === 'TIME_BASED') { | ||
| innerPayload = { | ||
| currentTeam: prosConsSelected, | ||
| timerType: timerType, | ||
| remainingTime: remainingTime, | ||
| sequence: index, | ||
| } as TimerDataPayload; | ||
| } else { | ||
| // 피드백 타이머 타입은 여기 올 수 없음 | ||
| // 따라서 별도 작업 하지 않고 그냥 반환 | ||
| return; | ||
| } | ||
|
|
||
| const payload = eventType === 'FINISHED' ? null : innerPayload; | ||
|
|
||
| // 이벤트 발행 | ||
| issueEvent(eventType, payload); | ||
| } | ||
| }; |
There was a problem hiding this comment.
handleTimerEvent 함수는 현재 렌더링 시점의 index, timerType, remainingTime 등을 클로저로 캡처합니다. 하지만 invoke()가 실행되어 라운드가 변경(index 변경)되거나 팀이 전환될 때, 상태 업데이트는 비동기적으로 예약되므로 issueEvent가 호출되는 시점에는 여전히 이전 라운드의 stale한 데이터가 전송됩니다.
이로 인해 NEXT 또는 BEFORE 이벤트가 발생했을 때 관전자(Audience) 화면은 새로운 라운드의 타이머 타입이나 초기 시간으로 동기화되지 못하고 이전 라운드의 상태에 멈춰있게 되는 심각한 동기화 버그가 발생합니다.
이벤트 타입(NEXT, BEFORE, TEAM_SWITCH)에 따라 다음 상태의 값들을 미리 계산하여 페이로드로 전송하도록 수정해야 합니다.
// 타이머 이벤트를 핸들링하는 래퍼 함수 선언
const handleTimerEvent = (invoke: () => void, eventType: SocketEventType) => {
// 이벤트 실행
invoke();
// 만약 소켓 열려 있으면, 발송
if (isSocketConnected && data) {
let targetIndex = index;
if (eventType === 'NEXT') {
targetIndex = Math.min(data.table.length - 1, index + 1);
} else if (eventType === 'BEFORE') {
targetIndex = Math.max(0, index - 1);
}
const targetItem = data.table[targetIndex];
const targetTimerType = targetItem.boxType;
let targetRemainingTime = remainingTime;
if (eventType === 'NEXT' || eventType === 'BEFORE') {
targetRemainingTime = targetTimerType === 'NORMAL' ? (targetItem.time ?? 0) : (targetItem.timePerTeam ?? 0);
}
// 타입에 따른 페이로드 준비
let innerPayload: TimerDataPayload;
if (targetTimerType === 'NORMAL') {
innerPayload = {
timerType: targetTimerType,
remainingTime: targetRemainingTime,
sequence: targetIndex,
} as TimerDataPayload;
} else if (targetTimerType === 'TIME_BASED') {
innerPayload = {
currentTeam: prosConsSelected,
timerType: targetTimerType,
remainingTime: targetRemainingTime,
sequence: targetIndex,
} as TimerDataPayload;
} else {
return;
}
const payload = eventType === 'FINISHED' ? null : innerPayload;
// 이벤트 발행
issueEvent(eventType, payload);
}
};
| export function normalizeEndpoint(url?: string) { | ||
| if (!url) { | ||
| return 'unknown'; | ||
| } | ||
|
|
||
| return url | ||
| .replace( | ||
| /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi, | ||
| ':uuid', | ||
| ) | ||
| .replace(/[0-9]+/g, ':id'); | ||
| } |
There was a problem hiding this comment.
normalizeEndpoint 함수에서 모든 숫자 시퀀스를 :id로 치환하는 replace(/[0-9]+/g, ':id') 로직은 /api/v1/polls와 같은 API 버전 경로(v1, v2 등)까지 /api/v:id/polls로 잘못 치환하는 문제를 발생시킵니다.
이로 인해 Sentry에서 서로 다른 API 버전의 에러가 동일한 엔드포인트로 잘못 그룹화될 수 있습니다. 경로 세그먼트 전체가 숫자인 경우만 /:id로 치환하도록 정규식을 수정하는 것이 안전합니다.
| export function normalizeEndpoint(url?: string) { | |
| if (!url) { | |
| return 'unknown'; | |
| } | |
| return url | |
| .replace( | |
| /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi, | |
| ':uuid', | |
| ) | |
| .replace(/[0-9]+/g, ':id'); | |
| } | |
| export function normalizeEndpoint(url?: string) { | |
| if (!url) { | |
| return 'unknown'; | |
| } | |
| return url | |
| .replace( | |
| /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi, | |
| ':uuid', | |
| ) | |
| .replace(/\/\d+\b/g, '/:id'); | |
| } |
| case 'KeyA': | ||
| // 찬성 진영 선택 및 반대 타이머 정지 | ||
| if (prosConsSelected === 'CONS') { | ||
| if (timer1.isDone) { | ||
| setProsConsSelected('PROS'); | ||
| } else { | ||
| switchCamp(); | ||
| } | ||
| const handleSwitching = () => { | ||
| if (timer1.isDone) { | ||
| setProsConsSelected('PROS'); | ||
| } else { | ||
| switchCamp(); | ||
| } | ||
| }; | ||
|
|
||
| onEvent(handleSwitching, 'TEAM_SWITCH'); | ||
| } | ||
|
|
||
| break; | ||
| case 'KeyL': | ||
| // 반대 진영 선택 및 찬성 타이머 정지 | ||
| if (prosConsSelected === 'PROS') { | ||
| if (timer1.isDone) { | ||
| setProsConsSelected('CONS'); | ||
| } else { | ||
| switchCamp(); | ||
| } | ||
| const handleSwitching = () => { | ||
| if (timer1.isDone) { | ||
| setProsConsSelected('CONS'); | ||
| } else { | ||
| switchCamp(); | ||
| } | ||
| }; | ||
|
|
||
| onEvent(handleSwitching, 'TEAM_SWITCH'); | ||
| } | ||
|
|
||
| break; |
There was a problem hiding this comment.
주석 설명('26. 6. 3. 부로 A키와 L키를 활용한 팀 전환은 비활성화) 및 keysToDisable에서의 제외 처리와 달리, switch 문 내부의 case 'KeyA'와 case 'KeyL' 분기 로직이 여전히 활성화되어 있어 실제로 키를 누르면 팀 전환 이벤트가 발생합니다.
의도치 않은 팀 전환을 방지하기 위해 case 'KeyA'와 case 'KeyL' 블록을 완전히 제거하거나 비활성화해 주세요.
case 'KeyA':
// '26. 6. 3. 부로 A키를 활용한 팀 전환은 비활성화
break;
case 'KeyL':
// '26. 6. 3. 부로 L키를 활용한 팀 전환은 비활성화
break;
🚩 연관 이슈
없음
📝 작업 내용
배포
🏞️ 스크린샷 (선택)
없음
🗣️ 리뷰 요구사항 (선택)
없음