Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. Walkthrough방 완료 상태 판정(isRoomCompleted) 유틸 추가 및 여러 컴포넌트에서 방 진행 상태를 조회해 입력/추가 UI를 숨기도록 변경했고, 팝업 렌더 시 ConfirmModal/MoreMenu에 중앙화된 onClose를 전달하며 댓글·기록·투표 관련 API 오류 처리를 일관화했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant UI as Page/Component
participant API as getRoomPlaying
participant Util as isRoomCompleted
UI->>API: fetch room playing status (roomId)
API-->>UI: { progressEndDate, ... }
UI->>Util: isRoomCompleted(progressEndDate)
Util-->>UI: true/false
alt roomCompleted == true
UI-->>UI: 입력/추가 UI 숨김 (MessageInput, MemoryAddButton, more 버튼)
else roomCompleted == false
UI-->>UI: 입력/추가 UI 표시
end
sequenceDiagram
autonumber
participant User as User
participant Popup as PopupContainer
participant Modal as ConfirmModal/MoreMenu
User->>Popup: open popup
Popup->>Modal: render with onClose = closePopup
User->>Modal: trigger close
Modal-->>Popup: call onClose()
Popup-->>Popup: closePopup() 실행
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro Disabled knowledge base sources:
📒 Files selected for processing (1)
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.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/memory/RecordItem/RecordItem.tsx (2)
139-141: roomId 없음 시 삭제 API가 방 ID 1에 호출될 위험
roomId || '1'기본값은 잘못된 방에 삭제를 발생시킬 수 있습니다. 없으면 즉시 중단하고 사용자에게 안내하세요.- const currentRoomId = roomId || '1'; + if (!roomId) { + openSnackbar({ message: '방 정보를 찾을 수 없습니다.', variant: 'top', onClose: () => {} }); + return; + } + const currentRoomId = roomId;
199-201: roomId 없을 때 API 호출 차단 — delete / pin 핸들러 방어 로직 추가roomId || '1' 기본값은 잘못된 room(1)으로 API가 호출될 수 있으니 delete/pin 핸들러에서 roomId 없으면 스낵바 노출 후 즉시 반환하도록 수정하세요.
- 수정 대상: src/components/memory/RecordItem/RecordItem.tsx — handleDelete (currentRoomId: line 139), handlePinRecord (currentRoomId: line 199)
- 참고(확인 필요): src/components/memory/MemoryAddButton/MemoryAddButton.tsx — navigate 기본값 '1' 사용 (lines 39, 49)
- const currentRoomId = roomId || '1'; + if (!roomId) { + openSnackbar({ message: '방 정보를 찾을 수 없습니다.', variant: 'top', onClose: () => {} }); + return; + } + const currentRoomId = roomId;
🧹 Nitpick comments (10)
src/components/memory/RecordItem/PollRecord.tsx (1)
92-105: 최대값 계산 중복(O(n^2)) 및 빈 배열 시 -Infinity 엣지케이스
Math.max(...arr)를 매 항목마다 계산하면 불필요합니다. 사전에maxCount한 번 계산하고, 빈 배열일 때 0으로 방어하세요.- const updatedOptions = currentOptions.map(opt => { - const updatedItem = response.data.voteItems.find( - item => item.voteItemId === opt.voteItemId, - ); + const voteItems = response.data.voteItems ?? []; + const maxCount = Math.max(0, ...voteItems.map(item => item.count)); + const updatedOptions = currentOptions.map(opt => { + const updatedItem = voteItems.find(item => item.voteItemId === opt.voteItemId); if (updatedItem) { return { ...opt, percentage: updatedItem.percentage, count: updatedItem.count, isVoted: updatedItem.isVoted, - isHighest: - updatedItem.count === Math.max(...response.data.voteItems.map(item => item.count)), + isHighest: updatedItem.count === maxCount, }; } return opt; });src/components/memory/RecordItem/RecordItem.tsx (1)
79-79: 좋아요 카운트 음수 방지동기화 이슈 등으로 0에서 감소할 가능성 방어 차원에서 하한을 두는 편이 안전합니다.
- setCurrentLikeCount((prev: number) => (response.data.isLiked ? prev + 1 : prev - 1)); + setCurrentLikeCount((prev: number) => + Math.max(0, response.data.isLiked ? prev + 1 : prev - 1), + );src/pages/groupDetail/ParticipatedGroupDetail.tsx (1)
247-256: 완료 전/후 상태 전환 시 열려있는 바텀시트 자동 닫기방이 완료 상태로 전환될 때 이미 열린 더보기 바텀시트를 자동으로 닫아 UX/행위 차단을 명확히 하세요.
// 모임방 완료 여부 확인 const isCompleted = roomData ? isRoomCompleted(roomData.data.progressEndDate) : false; + // 완료로 전환되면 바텀시트 닫기 + useEffect(() => { + if (isCompleted && isBottomSheetOpen) { + setIsBottomSheetOpen(false); + } + }, [isCompleted, isBottomSheetOpen]);src/pages/today-words/TodayWords.tsx (2)
174-191: 접근 권한 없음(140011)일 때 그룹 홈으로 리다이렉트 권장
getRoomPlaying이 권한 오류 시 Error를 던지므로 동일 UX로 이동 처리하면 일관성이 좋아집니다.useEffect(() => { const checkRoomStatus = async () => { if (!roomId) return; try { const response = await getRoomPlaying(parseInt(roomId)); if (response.isSuccess) { const completed = isRoomCompleted(response.data.progressEndDate); setRoomCompleted(completed); } - } + } } catch (error) { - console.error('모임방 상태 확인 오류:', error); + console.error('모임방 상태 확인 오류:', error); + if (error instanceof Error && error.message === '방 접근 권한이 없습니다.') { + navigate('/group', { replace: true }); + } } }; checkRoomStatus(); }, [roomId]);
215-227: 백엔드 상태 변화 대비: 전송 핸들러에서도 완료 상태 가드UI에서 입력을 숨기지만, 상태 경쟁이나 개발자 도구로의 호출을 고려해 핸들러에서도 한 번 더 차단하세요.
const handleSendMessage = useCallback(async () => { - if (inputValue.trim() === '' || isSubmitting) return; + if (inputValue.trim() === '' || isSubmitting) return; + if (roomCompleted) { + openSnackbar({ + message: '이미 완료된 모임방에서는 작성할 수 없습니다.', + variant: 'top', + onClose: () => {}, + }); + return; + } // roomId가 없으면 에러 처리 if (!roomId) {src/pages/memory/Memory.tsx (2)
72-74: 초기값 false로 인한 잠깐 노출(flicker) 가능성 — 완료 방에서 FAB가 순간 보일 수 있음상태 로딩 전에 안전하게 숨기려면 tri-state(null|boolean)로 전환하고, 명시적으로 false일 때만 노출하세요.
적용 diff:
- const [roomCompleted, setRoomCompleted] = useState(false); + const [roomCompleted, setRoomCompleted] = useState<boolean | null>(null); ... - {!roomCompleted && ( + {roomCompleted === false && ( <FloatingElements> <MemoryAddButton /> </FloatingElements> )}Also applies to: 319-323
154-172: 비동기 상태 업데이트 안전성 + parseInt radix 지정언마운트/roomId 변경 중 setState 경합을 방지하고, parseInt에 기수(10)를 명시하세요.
적용 diff:
- useEffect(() => { - const checkRoomStatus = async () => { + useEffect(() => { + let isActive = true; + const checkRoomStatus = async () => { if (!roomId) return; try { - const response = await getRoomPlaying(parseInt(roomId)); + const response = await getRoomPlaying(parseInt(roomId, 10)); if (response.isSuccess) { const completed = isRoomCompleted(response.data.progressEndDate); - setRoomCompleted(completed); + if (isActive) setRoomCompleted(completed); } } catch (error) { console.error('모임방 상태 확인 오류:', error); } }; checkRoomStatus(); - }, [roomId]); + return () => { + isActive = false; + }; + }, [roomId]);추가 제안: 동일 로직이 여러 컴포넌트에서 반복됩니다.
useRoomCompleted(roomId)훅으로 공통화하여 중복 호출/상태 불일치를 줄이길 권장합니다.src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx (3)
31-31: 초기값 false로 인한 잠깐 노출(flicker) — 완료 방에서 입력 영역이 순간 보일 수 있음tri-state(null|boolean)로 전환해 로딩 중에는 입력 영역을 숨기도록 하세요.
적용 diff:
- const [roomCompleted, setRoomCompleted] = useState(false); + const [roomCompleted, setRoomCompleted] = useState<boolean | null>(null); ... - {!roomCompleted && ( + {roomCompleted === false && ( <InputSection> <MessageInput placeholder={ isReplying ? `@${nickname}님에게 답글을 남겨보세요` : '댓글을 남겨보세요' } value={inputValue} onChange={setInputValue} onSend={handleSendComment} isReplying={isReplying} onCancelReply={handleCancelReply} nickname={nickname} disabled={isSending} /> </InputSection> )}Also applies to: 167-182
110-130: 비동기 안전성 + parseInt radix 지정언마운트/닫힘(isOpen=false) 타이밍에 대비해 안전 가드와 radix(10) 명시가 필요합니다.
적용 diff:
- useEffect(() => { - const checkRoomStatus = async () => { + useEffect(() => { + let isActive = true; + const checkRoomStatus = async () => { if (!roomId) return; try { - const response = await getRoomPlaying(parseInt(roomId)); + const response = await getRoomPlaying(parseInt(roomId, 10)); if (response.isSuccess) { const completed = isRoomCompleted(response.data.progressEndDate); - setRoomCompleted(completed); + if (isOpen && isActive) setRoomCompleted(completed); } } catch (error) { console.error('모임방 상태 확인 오류:', error); } }; if (isOpen) { checkRoomStatus(); } - }, [isOpen, roomId]); + return () => { isActive = false; }; + }, [isOpen, roomId]);
58-96: 댓글 전송 로직 중복 — 훅으로 일원화 제안
useReplyActions.submitComment와 유사 로직이 여기 재구현되어 유지보수 비용이 증가합니다. 훅에onError/onSuccess콜백을 받아 스낵바만 외부에서 주입하도록 확장하거나, 공용 유틸로 추출하세요.원하시면
useRoomCompleted훅과 함께submitComment콜백 확장안까지 패치 제안 드리겠습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (8)
src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx(5 hunks)src/components/common/Modal/PopupContainer.tsx(1 hunks)src/components/memory/RecordItem/PollRecord.tsx(5 hunks)src/components/memory/RecordItem/RecordItem.tsx(3 hunks)src/pages/groupDetail/ParticipatedGroupDetail.tsx(2 hunks)src/pages/memory/Memory.tsx(4 hunks)src/pages/today-words/TodayWords.tsx(4 hunks)src/utils/roomStatus.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
src/pages/today-words/TodayWords.tsx (2)
src/api/rooms/getRoomPlaying.ts (1)
getRoomPlaying(65-79)src/utils/roomStatus.ts (1)
isRoomCompleted(10-24)
src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx (8)
src/stores/useCommentBottomSheetStore.ts (1)
useCommentBottomSheetStore(14-26)src/api/comments/getComments.ts (1)
CommentData(3-16)src/hooks/useReplyActions.ts (1)
useReplyActions(10-54)src/stores/useReplyStore.ts (1)
useReplyStore(15-28)src/hooks/usePopupActions.ts (1)
usePopupActions(9-35)src/api/rooms/getRoomPlaying.ts (1)
getRoomPlaying(65-79)src/utils/roomStatus.ts (1)
isRoomCompleted(10-24)src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.styled.ts (1)
InputSection(65-70)
src/pages/groupDetail/ParticipatedGroupDetail.tsx (3)
src/utils/roomStatus.ts (1)
isRoomCompleted(10-24)src/pages/groupDetail/ParticipatedGroupDetail.styled.ts (1)
ParticipatedWrapper(4-16)src/pages/groupDetail/GroupDetail.styled.ts (2)
TopBackground(20-29)Header(31-50)
src/components/common/Modal/PopupContainer.tsx (1)
src/stores/usePopupStore.ts (2)
ConfirmModalProps(12-17)MoreMenuProps(19-27)
src/pages/memory/Memory.tsx (3)
src/api/rooms/getRoomPlaying.ts (1)
getRoomPlaying(65-79)src/utils/roomStatus.ts (1)
isRoomCompleted(10-24)src/pages/memory/Memory.styled.ts (1)
FloatingElements(48-58)
src/components/memory/RecordItem/PollRecord.tsx (1)
src/components/memory/RecordItem/PollRecord.styled.ts (2)
PollNumber(44-51)PollText(53-60)
🔇 Additional comments (8)
src/utils/roomStatus.ts (1)
13-23: 날짜 파싱 안정화(브라우저 간 파싱 차이/타임존 엣지 케이스 방어 필요)
new Date('YYYY-MM-DD')는 환경에 따라 UTC/로컬 해석이 달라질 수 있습니다. 정규식으로 연/월/일을 추출해 로컬 타임존의 자정/자정-1ms로 직접 구성하면 안전합니다.
[ suggest_recommended_refactor ]- // 날짜 형식 정규화 (YYYY.MM.DD -> YYYY-MM-DD) - const normalizedDate = progressEndDate.replace(/\./g, '-'); - - const endDate = new Date(normalizedDate); - const today = new Date(); - - // 시간 부분을 제거하고 날짜만 비교 - today.setHours(0, 0, 0, 0); - endDate.setHours(23, 59, 59, 999); - - return today > endDate; + // 입력 정규화 및 안전한 파싱 (YYYY.MM.DD / YYYY-MM-DD / YYYY/MM/DD 지원) + const trimmed = progressEndDate.trim(); + const m = trimmed.match(/^(\d{4})[.\-/](\d{1,2})[.\-/](\d{1,2})$/); + if (!m) return false; + const [, y, mo, d] = m; + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + // 로컬 타임존 기준 해당 일의 23:59:59.999로 설정 + const endDate = new Date(Number(y), Number(mo) - 1, Number(d), 23, 59, 59, 999); + return today > endDate;src/components/common/Modal/PopupContainer.tsx (1)
45-46: ConfirmModal/MoreMenu에 onClose 전달한 변경 좋습니다팝업 종료 경로를 통일해 회귀 버그를 줄일 수 있습니다. 두 컴포넌트 내부에서 ‘아니요/닫기/바깥 클릭/ESC’ 모두
onClose로 귀결되는지 한 번만 확인 부탁드립니다.테스트 제안: ConfirmModal과 MoreMenu에서 onClose 핸들러가 실제로 호출되는지 스냅샷/유닛 테스트 또는 E2E에서 확인해 주세요.
Also applies to: 49-49
src/components/memory/RecordItem/PollRecord.tsx (1)
168-170: 잘못된 지적 — PollOption.id가 정의되어 있어 key={option.id}는 유효함src/types/memory.ts에서 PollOption 인터페이스에 id: string이 정의되어 있습니다(예: src/types/memory.ts:82–84). 따라서 src/components/memory/RecordItem/PollRecord.tsx의 key={option.id}는 undefined 경고를 유발하지 않으며 voteItemId로 변경할 필요가 없습니다.
Likely an incorrect or invalid review comment.
src/pages/memory/Memory.tsx (2)
11-12: 진행 상태 체크 관련 import 추가 적절페이지 단에서 방 상태 확인을 위한 의존성 추가가 목적에 부합합니다.
319-323: FAB 조건 렌더링 방향성 OK완료 방에서 작성 버튼을 숨기는 정책은 요구사항과 일치합니다. tri‑state 반영 시 조건을 roomCompleted === false로 유지하세요.
검증 결과: 제공하신 스크립트 출력이 "No files were searched"로 리포지토리 검색이 수행되지 않아 확인이 불충분합니다. 아래로 재실행해 주세요:
#!/bin/bash set -euo pipefail echo "isRoomCompleted 사용 위치:" rg -n --hidden -uu -S 'isRoomCompleted\s*\(' -g '!node_modules/**' -C2 || true echo echo "MessageInput 사용 파일:" rg -n --hidden -uu -S '<MessageInput' -g '!node_modules/**' -C2 || true echo echo "roomCompleted 가드가 없는 MessageInput 파일(후보):" python - <<'PY' import subprocess p = subprocess.run(['rg','-n','--hidden','-uu','-S','<MessageInput','-g','!node_modules/**','-l'], capture_output=True, text=True) files = [f for f in p.stdout.splitlines() if f] unguarded=[] for f in files: try: with open(f,encoding='utf-8',errors='ignore') as fh: t=fh.read() if 'roomCompleted' not in t: unguarded.append(f) except: pass print('\n'.join(unguarded)) PYsrc/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx (3)
2-12: 라우팅 파라미터/스낵바/방 상태 체크 import 추가 적절목표한 UX(완료 방 입력 비노출, 서버 메시지 스낵바 전달)에 필요한 의존성이 잘 추가되었습니다.
24-24: roomId 비존재 시 정책 확인 필요일부 라우트에서 roomId가 없으면 상태 체크가 스킵되어 입력이 그대로 노출됩니다. 의도한 동작인지 확인 바랍니다(예: 전역 댓글 시트가 그룹 외 페이지에서 열릴 수 있는지).
79-85: 서버/네트워크 에러 스낵바 전달 좋습니다실패 사유를 사용자에게 명확히 전달합니다. 서버 메시지의 길이/민감도에 따른 truncate/정제 로직은 추후 필요 시 고려하세요.
Also applies to: 88-92
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/api/comments/postLike.ts (1)
1-31: 반환 타입을 지켜주세요.
error.response.data가any로 추론되면서postLike전체가Promise<any>로 바뀌어 호출부에서isSuccess·data구조를 컴파일 단계에서 검증할 수 없게 됩니다. 새로 추가한 필드를 안전하게 활용하려면 반환 타입을 명시하고 Axios 에러인 경우에만 동일한 응답 스펙을 돌려주도록 가드해 주세요.+import { isAxiosError } from 'axios'; import { apiClient } from '../index'; @@ -export const postLike = async (commentId: number, type: boolean) => { +export const postLike = async (commentId: number, type: boolean): Promise<PostLikeResponse> => { @@ - } catch (error: any) { + } catch (error) { console.error('댓글 좋아요 API 오류:', error); // 서버에서 에러 응답을 보낸 경우 해당 응답을 반환 - if (error.response?.data) { - return error.response.data; + if (isAxiosError<PostLikeResponse>(error) && error.response?.data) { + return error.response.data; } throw error; }src/api/record/postVote.ts (1)
1-19: 타입 안정성을 유지해주세요.
postVote역시error.response.data때문에Promise<any>로 추론되고 있어, 호출부에서isSuccess등의 필드를 오타 없이 다루는 안전장치가 사라졌습니다. 반환 타입을 명시하고 Axios 에러인지 확인한 뒤 동일 스키마일 때만 값을 반환하도록 정리해 주세요.+import { isAxiosError } from 'axios'; import { apiClient } from '../index'; import type { VoteRequest, VoteData, ApiResponse } from '@/types/record'; @@ -export const postVote = async (roomId: number, voteId: number, voteData: VoteRequest) => { +export const postVote = async ( + roomId: number, + voteId: number, + voteData: VoteRequest, +): Promise<VoteResponse> => { try { const response = await apiClient.post<VoteResponse>(`/rooms/${roomId}/vote/${voteId}`, voteData); return response.data; - } catch (error: any) { + } catch (error) { console.error('투표 API 오류:', error); // 서버에서 에러 응답을 보낸 경우 해당 응답을 반환 - if (error.response?.data) { - return error.response.data; + if (isAxiosError<VoteResponse>(error) && error.response?.data) { + return error.response.data; } throw error; }src/api/comments/postReply.ts (1)
1-30: 댓글 작성 API도 동일하게 타입을 보존해야 합니다.
postReply가Promise<any>로 변하면서 댓글 작성 결과 객체의 구조를 컴파일 타임에 검증할 수 없습니다. Axios 에러만 안전하게 캐치하도록 수정해 타입 안정성을 회복해 주세요.+import { isAxiosError } from 'axios'; import { apiClient } from '../index'; @@ -export const postReply = async (postId: number, request: PostReplyRequest) => { +export const postReply = async ( + postId: number, + request: PostReplyRequest, +): Promise<PostReplyResponse> => { try { const response = await apiClient.post<PostReplyResponse>(`/comments/${postId}`, request); return response.data; - } catch (error: any) { + } catch (error) { console.error('댓글 작성 API 오류:', error); // 서버에서 에러 응답을 보낸 경우 해당 응답을 반환 - if (error.response?.data) { - return error.response.data; + if (isAxiosError<PostReplyResponse>(error) && error.response?.data) { + return error.response.data; } throw error; }
🧹 Nitpick comments (4)
src/components/memory/RecordItem/PollRecord.tsx (1)
93-106: isHighest 계산 시 불필요한 O(n²) 반복을 제거해주세요.현재
currentOptions.map루프 안에서Math.max(...response.data.voteItems.map(...))가 매 옵션마다 다시 계산되고 있어, 옵션 수가 많을 경우 불필요한 계산 부담이 큽니다. 응답 받은voteItems를 한 번만 순회해 최대 득표수를 구한 뒤 재사용하도록 리팩터링하는 편이 좋겠습니다. 예시는 아래와 같습니다.- const updatedOptions = currentOptions.map(opt => { - const updatedItem = response.data.voteItems.find( - (item: PollOption) => item.voteItemId === opt.voteItemId, - ); + const voteItems = response.data.voteItems; + const maxCount = Math.max(...voteItems.map((item: PollOption) => item.count)); + const updatedOptions = currentOptions.map(opt => { + const updatedItem = voteItems.find( + (item: PollOption) => item.voteItemId === opt.voteItemId, + ); if (updatedItem) { return { ...opt, percentage: updatedItem.percentage, count: updatedItem.count, isVoted: updatedItem.isVoted, - isHighest: - updatedItem.count === - Math.max(...response.data.voteItems.map((item: PollOption) => item.count)), + isHighest: updatedItem.count === maxCount, };src/api/roomPosts/postRoomPostLike.ts (1)
15-21:AxiosError를 활용해 오류 타입을 명확히 해주세요.
Line [15]에서error를any로 선언하면response존재 여부를 정적으로 보장받지 못해 유지보수 시 추론이 어려워집니다. Axios 환경이라면axios.isAxiosError로 분기해RoomPostLikeResponse스펙을 확실히 지킬 수 있도록 타입을 좁혀 주세요.+import axios from 'axios'; ... - } catch (error: any) { - console.error('방 게시물 좋아요 API 오류:', error); - if (error.response?.data) { - return error.response.data; - } - throw error; - } + } catch (error) { + console.error('방 게시물 좋아요 API 오류:', error); + if (axios.isAxiosError<RoomPostLikeResponse>(error)) { + const errorData = error.response?.data; + if (errorData) { + return errorData; + } + } + throw error; + }src/components/common/Post/SubReply.tsx (1)
50-62: 좋아요 카운트 감소 시 음수로 내려가지 않도록 보정해 주세요.
Line [54]에서 실패한 동기화나 초기 데이터 불일치로prev가 0인데response.data.isLiked가false로 내려오면 UI에-1이 노출됩니다. 서버 응답의 카운트 값을 그대로 쓰거나, 최소 0으로 클램프해서 사용자 경험을 보호하는 편이 안전합니다.- setCurrentLikeCount(prev => (response.data.isLiked ? prev + 1 : prev - 1)); + setCurrentLikeCount(prev => { + const next = response.data.isLiked ? prev + 1 : prev - 1; + return Math.max(next, 0); + });src/components/common/Post/Reply.tsx (1)
44-55: 댓글 좋아요 카운트도 음수 방지를 위해 클램프가 필요합니다.
Line [47] 역시prev - 1이 즉시 실행되는데, 동시성 이슈나 초기 값 불일치가 있으면-1이 내려옵니다. 위 SubReply와 동일하게 최소값을 0으로 보정하거나 서버가 내려주는 최신 카운트를 직접 반영하는 방식으로 보완해 주세요.- setLikeCount(prev => (response.data.isLiked ? prev + 1 : prev - 1)); + setLikeCount(prev => { + const next = response.data.isLiked ? prev + 1 : prev - 1; + return Math.max(next, 0); + });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (9)
src/api/comments/postLike.ts(2 hunks)src/api/comments/postReply.ts(1 hunks)src/api/record/postVote.ts(1 hunks)src/api/roomPosts/postRoomPostLike.ts(1 hunks)src/components/common/Post/Reply.tsx(1 hunks)src/components/common/Post/SubReply.tsx(1 hunks)src/components/common/TitleHeader.tsx(1 hunks)src/components/memory/RecordItem/PollRecord.tsx(7 hunks)src/components/memory/RecordItem/RecordItem.tsx(4 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/api/comments/postLike.ts (1)
src/api/index.ts (1)
apiClient(7-13)
src/api/comments/postReply.ts (1)
src/api/index.ts (1)
apiClient(7-13)
src/components/memory/RecordItem/PollRecord.tsx (2)
src/types/memory.ts (1)
PollOption(82-90)src/components/memory/RecordItem/PollRecord.styled.ts (3)
PollOption(23-32)PollNumber(44-51)PollText(53-60)
src/api/record/postVote.ts (1)
src/api/index.ts (1)
apiClient(7-13)
🔇 Additional comments (5)
src/components/common/TitleHeader.tsx (1)
81-83: TitleHeader 우측 버튼 클릭 방어 로직 추가 또는 사용처 검증 필요
isNextActive가 기본값(false)일 때 onRightClick이 undefined로 설정되어 기존에 암묵적으로 클릭 가능하던 화면의 우측 버튼이 동작을 멈춥니다. 모든 TitleHeader 사용처에서 isNextActive를 명시했는지 검토하거나, 기본값일 때도 클릭 이벤트가 동작하도록 방어 로직을 추가하세요.src/components/memory/RecordItem/RecordItem.tsx (4)
84-89: 서버 오류 메시지 우선 노출 👍
직접 작성한 문구보다 백엔드에서 내려오는response.message를 우선적으로 보여주도록 바꾼 덕분에 QA 피드백과도 잘 맞습니다.
236-243: 실패 시에도 팝업 정리한 점 좋습니다
에러 처리 전에closePopup()을 호출해서 레이어가 잔류하지 않도록 정리해 둔 덕분에 UI 일관성이 확보됩니다.
286-303: MoreMenu 액션 후 Confirm 즉시 닫힘 여부 확인 필요
openMoreMenu에onClose: closePopup을 넘기면서,MoreMenu컴포넌트가 콜백 실행 뒤에onClose?.()를 호출하는 구조라면handlePinConfirm에서 띄운 Confirm이 바로 닫혀 버릴 수 있습니다. 실제 QA 시나리오에서 “텍스트 기록 → … → 핀하기”를 눌렀을 때 확인 모달이 정상적으로 유지되는지 꼭 한 번 확인 부탁드립니다. 만약 문제가 재현되면handlePinConfirm에서 먼저closePopup()으로 메뉴를 닫고 이후openConfirm을 호출하는 구조로 조정하면 안전할 것 같습니다.
311-320: 의존성 배열 보강 감사합니다
shouldBlur와closePopup을 포함시켜 최신 레퍼런스를 유지하도록 정리된 점이 좋습니다.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/group/HotTopicSection.tsx (1)
137-139: 터치 이벤트에서preventDefault()사용 주의
handleTouchStart에서e.preventDefault()를 호출하면 터치 스크롤이 차단될 수 있습니다. 이는 모바일 사용자 경험을 해칠 수 있습니다.슬라이드 드래그와 페이지 스크롤을 구분하기 위해 수평 이동을 감지한 후에만 스크롤을 차단하는 방식을 고려해보세요:
const handleTouchStart = (e: React.TouchEvent) => { - e.preventDefault(); handleDragStart(e.touches[0].clientX); }; const handleTouchMove = (e: React.TouchEvent) => { - e.preventDefault(); + if (dragStateRef.current.hasMoved) { + e.preventDefault(); + } handleDragMove(e.touches[0].clientX); };
🧹 Nitpick comments (4)
src/components/group/HotTopicSection.tsx (2)
55-60: 터치 이벤트 핸들링 개선 필요현재 코드에서 몇 가지 개선이 필요합니다:
handleVoteClick에서React.TouchEvent를 처리하지만 실제로는onClick에만 바인딩되어 있어 터치 이벤트가 전달되지 않습니다.handleVoteTouchEnd에서 불필요한e.preventDefault()가 있습니다. 이미handleVoteTouchStart에서 이벤트 전파를 막고 있으므로 중복됩니다.- 터치와 마우스 이벤트 로직이 중복되어 있습니다.
- const handleVoteClick = (e: React.MouseEvent | React.TouchEvent, poll: Poll) => { + const handleVoteClick = (e: React.MouseEvent, poll: Poll) => { e.stopPropagation(); if (!isDragging || !dragStateRef.current.hasMoved) { onPollClick(poll.pageNumber); } }; // 터치 이벤트 핸들러 const handleVoteTouchEnd = (e: React.TouchEvent, poll: Poll) => { - e.preventDefault(); e.stopPropagation(); if (!isDragging || !dragStateRef.current.hasMoved) { onPollClick(poll.pageNumber); } };Also applies to: 63-69, 71-73
254-256: HotTopicSectionHeader에서 cursor 스타일 제거
클릭 핸들러가 없으므로src/components/group/HotTopicSection.styled.ts에서cursor: pointer;를 삭제하세요.src/pages/memory/Memory.tsx (2)
165-183: 비동기 상태 업데이트 취소 처리 및 안정성 보강 제안
아래처럼cancelled플래그를 추가해 언마운트 혹은roomId변경 시 stale 응답으로 인한setState호출을 방지하세요.useEffect(() => { + let cancelled = false; const checkRoomStatus = async () => { if (!roomId) return; try { - const response = await getRoomPlaying(parseInt(roomId)); + const response = await getRoomPlaying(Number(roomId)); + if (!cancelled && response.isSuccess) { const completed = isRoomCompleted(response.data.progressEndDate); setRoomCompleted(completed); - } + } } catch (error) { - console.error('모임방 상태 확인 오류:', error); + if (!cancelled) { + console.error('모임방 상태 확인 오류:', error); + } } }; checkRoomStatus(); + return () => { cancelled = true; }; }, [roomId]);
35-47: maxCount 계산을 map 외부로 옮기기
src/pages/memory/Memory.tsx에서pollOptions생성 시 매 항목마다Math.max(...post.voteItems.map(...))를 호출해 O(n²)이 됩니다.post.voteItems.map(v ⇒ v.count||0)로 counts 배열을 만들고Math.max를 한 번만 실행하도록 수정하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/components/group/HotTopicSection.tsx(6 hunks)src/pages/memory/Memory.tsx(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/pages/memory/Memory.tsx (3)
src/api/rooms/getRoomPlaying.ts (1)
getRoomPlaying(65-79)src/utils/roomStatus.ts (1)
isRoomCompleted(10-24)src/pages/memory/Memory.styled.ts (1)
FloatingElements(48-58)
src/components/group/HotTopicSection.tsx (1)
src/components/group/HotTopicSection.styled.ts (2)
HotTopicSection(4-13)HotTopicSectionHeader(15-20)
🔇 Additional comments (3)
src/pages/memory/Memory.tsx (3)
11-12: LGTM: 완료 상태 확인 의존성 추가 적절
getRoomPlaying,isRoomCompleted모두 아래 useEffect에서 정상 사용됩니다.
90-92: LGTM: 완료 상태 state 추가 적절후속 렌더 조건부 처리와 잘 연결됩니다.
330-334: LGTM: 완료된 방에서 FAB 숨김 처리 충족PR 의도(완료된 방에서 작성 UI 숨김)에 부합합니다.
테스트 데이터로 다음을 확인 부탁드립니다:
- 완료된 방 진입 시 FAB가 표시되지 않아야 함.
- 진행 중인 방 진입 시 FAB가 정상 표시되어야 함.
- 경계일(마감일 당일)에서 의도한 표시/숨김 동작이 맞는지 확인.
| polls: Poll[]; | ||
| hasPolls: boolean; | ||
| onClick: () => void; | ||
| onClick?: () => void; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
onClick prop이 사용되지 않음
props에서 onClick이 optional로 정의되어 있지만 컴포넌트 내부에서 실제로 사용되지 않습니다. 사용하지 않는 prop은 제거하는 것이 좋습니다.
interface HotTopicSectionProps {
polls: Poll[];
hasPolls: boolean;
- onClick?: () => void;
onPollClick: (pageNumber: number) => void;
}Also applies to: 39-39
🤖 Prompt for AI Agents
In src/components/group/HotTopicSection.tsx around lines 35 and 39, the props
declare an optional onClick property but the component never uses it; remove
onClick from the prop type/interface (and any related prop forwarding) or, if
intended, wire it up to the correct clickable element (e.g., pass it to the root
button/div or list item) and ensure proper typing—update the component props
definition and usages accordingly to either eliminate the unused prop or attach
it to the interactive element.
| useEffect(() => { | ||
| const searchParams = new URLSearchParams(location.search); | ||
| const pageParam = searchParams.get('page'); | ||
| const filterParam = searchParams.get('filter'); | ||
|
|
||
| if (pageParam && filterParam === 'poll') { | ||
| const page = parseInt(pageParam); | ||
| if (!isNaN(page)) { | ||
| console.log('✅ 페이지 필터 적용:', { page }); | ||
| setSelectedPageRange({ start: page, end: page }); | ||
| setActiveFilter('page'); | ||
| setActiveTab('group'); | ||
|
|
||
| navigate(location.pathname, { replace: true }); | ||
| } | ||
| } | ||
| }, [location.search]); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
URL 쿼리 파싱: 'filter=poll' 네이밍 불일치 가능, parseInt/로그 정리 제안
- 내부 상태는
FilterType = 'page' | 'overall'인데, 쿼리는filter=poll을 기대합니다. 의도(“투표 페이지 필터”)라면 OK지만, 일관성 측면에서filter=page로 맞추거나 쿼리 파라미터명을 명확히 하는 것을 권장합니다. parseInt대신Number가 간결합니다.- 콘솔 로그는 배포 시 노이즈가 될 수 있어 제거 권장.
- if (pageParam && filterParam === 'poll') {
- const page = parseInt(pageParam);
+ if (pageParam && filterParam === 'poll') {
+ const page = Number(pageParam);
if (!isNaN(page)) {
- console.log('✅ 페이지 필터 적용:', { page });
setSelectedPageRange({ start: page, end: page });
setActiveFilter('page');
setActiveTab('group');
navigate(location.pathname, { replace: true });
}
}다른 라우팅/딥링크에서 filter=poll을 참조하는 곳이 있는지 확인 부탁드립니다. 있으면 유지, 없으면 filter=page로 통일 고려.
🏁 Script executed:
#!/bin/bash
# filter=poll 사용처 확인
rg -nP -C3 'filter=poll'Length of output: 557
URL 쿼리 파라미터 filter=poll → filter=page 통일, parseInt→Number, console.log 제거
- src/pages/memory/Memory.tsx:
filterParam==='poll'→filterParam==='page',const page = Number(pageParam), console.log 삭제 - src/pages/groupDetail/ParticipatedGroupDetail.tsx: navigate URL의
&filter=poll→&filter=page
--- src/pages/memory/Memory.tsx
@@ -65,12 +65,11 @@
- if (pageParam && filterParam === 'poll') {
- const page = parseInt(pageParam);
+ if (pageParam && filterParam === 'page') {
+ const page = Number(pageParam);
if (!isNaN(page)) {
- console.log('✅ 페이지 필터 적용:', { page });
setSelectedPageRange({ start: page, end: page });
setActiveFilter('page');
setActiveTab('group');--- src/pages/groupDetail/ParticipatedGroupDetail.tsx
@@ -187,7 +187,7 @@
- navigate(`/rooms/${roomId}/memory?page=${pageNumber}&filter=poll`);
+ navigate(`/rooms/${roomId}/memory?page=${pageNumber}&filter=page`);📝 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.
| useEffect(() => { | |
| const searchParams = new URLSearchParams(location.search); | |
| const pageParam = searchParams.get('page'); | |
| const filterParam = searchParams.get('filter'); | |
| if (pageParam && filterParam === 'poll') { | |
| const page = parseInt(pageParam); | |
| if (!isNaN(page)) { | |
| console.log('✅ 페이지 필터 적용:', { page }); | |
| setSelectedPageRange({ start: page, end: page }); | |
| setActiveFilter('page'); | |
| setActiveTab('group'); | |
| navigate(location.pathname, { replace: true }); | |
| } | |
| } | |
| }, [location.search]); | |
| useEffect(() => { | |
| const searchParams = new URLSearchParams(location.search); | |
| const pageParam = searchParams.get('page'); | |
| const filterParam = searchParams.get('filter'); | |
| if (pageParam && filterParam === 'page') { | |
| const page = Number(pageParam); | |
| if (!isNaN(page)) { | |
| setSelectedPageRange({ start: page, end: page }); | |
| setActiveFilter('page'); | |
| setActiveTab('group'); | |
| navigate(location.pathname, { replace: true }); | |
| } | |
| } | |
| }, [location.search]); |
| useEffect(() => { | |
| const searchParams = new URLSearchParams(location.search); | |
| const pageParam = searchParams.get('page'); | |
| const filterParam = searchParams.get('filter'); | |
| if (pageParam && filterParam === 'poll') { | |
| const page = parseInt(pageParam); | |
| if (!isNaN(page)) { | |
| console.log('✅ 페이지 필터 적용:', { page }); | |
| setSelectedPageRange({ start: page, end: page }); | |
| setActiveFilter('page'); | |
| setActiveTab('group'); | |
| navigate(location.pathname, { replace: true }); | |
| } | |
| } | |
| }, [location.search]); | |
| navigate(`/rooms/${roomId}/memory?page=${pageNumber}&filter=page`); |
🤖 Prompt for AI Agents
In src/pages/memory/Memory.tsx around lines 65 to 82, update the URL query
handling so it checks for filterParam === 'page' (not 'poll'), convert the
pageParam using Number(pageParam) instead of parseInt, and remove the
console.log call; after converting, validate with isNaN and then call
setSelectedPageRange, setActiveFilter('page'), setActiveTab('group') and
navigate(location.pathname, { replace: true }) as before. Also update the
related navigate call in src/pages/groupDetail/ParticipatedGroupDetail.tsx to
use &filter=page instead of &filter=poll.
#️⃣ 연관된 이슈
#250
📝 작업 내용
💬 리뷰 요구사항(선택)
Summary by CodeRabbit