Conversation
- 투표하기 API 함수 구현 (postVote) - 투표 관련 타입 정의 추가 (VoteRequest, VoteData, VoteItemResult) - PollOption 타입에 voteItemId, isVoted 필드 추가 - PollRecord 컴포넌트에 투표 클릭 핸들러 구현 - 실시간 투표 결과 업데이트 및 상태 관리 - 에러 코드별 상세 메시지 처리 및 토스트 팝업 - 투표 진행 중 UI 비활성화 처리
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough클라이언트 투표 기능을 추가했습니다. 투표 요청용 타입(VoteRequest/VoteData)을 도입하고, POST 투표 API(postVote)를 신설했습니다. PollRecord가 인터랙티브하게 투표/취소를 수행하며, 변환 로직과 타입에 voteItemId/isVoted 필드를 확장했습니다. RecordItem은 PollRecord에 postId와 onVoteUpdate를 전달합니다. .gitignore에 CLAUDE.md를 무시하도록 추가했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant U as User
participant PR as PollRecord (UI)
participant API as postVote()
participant S as Server
participant RI as RecordItem (Parent)
U->>PR: 옵션 클릭
PR->>API: postVote(roomId, postId, { voteItemId, type })
API->>S: POST /rooms/{roomId}/vote/{postId}
S-->>API: VoteData (voteItems[…])
API-->>PR: ApiResponse<VoteData>
PR->>PR: 옵션 상태 업데이트
PR-->>U: 스낵바 표시 (성공/실패)
PR-->>RI: onVoteUpdate(updatedOptions)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (9)
src/types/record.ts (1)
33-53: 투표 타입 정의 추가 전반적으로 적절 (네이밍 가독성 소폭 개선 제안)현재 API 계약상 boolean 필드명이
type인 점은 이해되지만, FE 코드 레벨에서는 의미가 모호할 수 있습니다. 유지보수성을 위해 주석을 유지/보강하거나, API 호출 직전 매핑을 통해 내부 사용 타입에서는action: 'VOTE' | 'CANCEL'같은 가독성 높은 별도 타입을 쓰고 전송 시 boolean으로 변환하는 패턴을 고려해볼 수 있습니다.src/components/memory/RecordItem/RecordItem.tsx (2)
281-289: parseInt 사용 시 기수(radix) 명시문자열이 10진수임이 확실하더라도 명시적으로 10을 전달하는 것이 안전합니다.
적용 diff:
- postId={parseInt(id)} + postId={parseInt(id, 10)}
281-289: 투표 결과 상위로 전달되지 않음 — 상태 일관성 개선 제안PollRecord의 로컬 상태는 즉시 갱신되지만, RecordItem → 상위(MemoryContent/Memory)로 결과가 전파되지 않아 부모가 리렌더링할 경우 원본 props로 되돌아갈 수 있습니다. onVoteUpdate를 상위까지 버블링해 records 배열의 해당 항목 pollOptions를 교체하는 흐름을 연결하는 것을 권장합니다.
원하시면 상위 컴포넌트 체인(MemoryContent/Memory 포함)까지 onVoteUpdate를 관통시키고, 해당 record를 찾아 불변 업데이트하는 패치를 제안드릴게요.
src/api/record/postVote.ts (2)
8-11: 반환 타입 명시 및 파라미터 네이밍 정리반환 타입을 명시하면 호출부에서 타입 추론이 안정적입니다. 또한
voteData는 응답 타입VoteData와 이름이 유사해 혼동 여지가 있어payload로 변경을 권장합니다.-export const postVote = async (roomId: number, voteId: number, voteData: VoteRequest) => { - const response = await apiClient.post<VoteResponse>(`/rooms/${roomId}/vote/${voteId}`, voteData); - return response.data; -}; +export const postVote = async ( + roomId: number, + voteId: number, + payload: VoteRequest, +): Promise<VoteResponse> => { + const response = await apiClient.post<VoteResponse>(`/rooms/${roomId}/vote/${voteId}`, payload); + return response.data; +};
13-38: 사용 예시는 TSDoc/문서로 이동 권장파일 내 장문의 사용 예시는 소스 가독성을 떨어뜨립니다. 함수 상단에 TSDoc으로 요약하거나 문서(예: Storybook/MDX)로 이동하는 편이 유지보수에 유리합니다.
src/components/memory/RecordItem/PollRecord.tsx (4)
83-97: isHighest 계산 O(n²) 및 find 반복 — 한 번만 계산하고 Map으로 조회하도록 리팩터최댓값을 매번 계산하고 배열을 매번 탐색(find)하고 있어 불필요한 반복이 있습니다. 한 번만 최댓값을 계산하고, Map으로 항목을 조회하면 간결하고 효율적입니다. 동작은 동일합니다.
- // API 응응으로 받은 투표 결과를 현재 옵션 형태로 변환 - const updatedOptions = currentOptions.map(opt => { - const updatedItem = response.data.voteItems.find( - item => item.voteItemId === opt.voteItemId - ); - if (updatedItem) { - return { - ...opt, - percentage: updatedItem.percentage, - isVoted: updatedItem.isVoted, - isHighest: updatedItem.percentage === Math.max(...response.data.voteItems.map(item => item.percentage)) - }; - } - return opt; - }); + // API 응답으로 받은 투표 결과를 현재 옵션 형태로 변환 + const itemsById = new Map(response.data.voteItems.map(item => [item.voteItemId, item] as const)); + const maxPercentage = Math.max(...response.data.voteItems.map(item => item.percentage)); + const updatedOptions = currentOptions.map(opt => { + const updatedItem = itemsById.get(opt.voteItemId); + const nextPercentage = updatedItem?.percentage ?? opt.percentage; + const nextIsVoted = updatedItem?.isVoted ?? opt.isVoted; + return { + ...opt, + percentage: nextPercentage, + isVoted: nextIsVoted, + isHighest: nextPercentage === maxPercentage, + }; + });
80-80: parseInt 기수(radix) 명시 권장명시적으로 10진수로 파싱하여 잠재적 파싱 이슈를 방지하세요.
- const response = await postVote(parseInt(roomId), postId, voteData); + const response = await postVote(parseInt(roomId, 10), postId, voteData);
148-151: React key는 voteItemId 사용 권장서버 응답과 동기화되는 식별자는
voteItemId입니다. 키로voteItemId를 사용하면 재정렬/업데이트 시 React가 항목을 더 안정적으로 식별합니다.- <PollOptionStyled - key={option.id} + <PollOptionStyled + key={option.voteItemId} isHighest={option.isHighest}
80-80: postVote 호출 시 두 번째 인자는 ‘voteId’입니다 — PollRecordProps의postId를voteId로 통일해주세요API 스펙(
/rooms/{roomId}/vote/{voteId})에 따라postVote(roomId, voteId, …)의 두 번째 파라미터는 voteId가 맞습니다. 현재PollRecord컴포넌트는 prop 이름을postId로 사용하고 있어 혼동의 여지가 있습니다. 변수명을 일관성 있게 맞추기 위해 아래를 권장드립니다:• 파일:
src/components/memory/RecordItem/PollRecord.tsx
- PollRecordProps:
postId: number→voteId: number(// 투표 API 호출에 필요한 투표 ID)- 컴포넌트 파라미터:
{ postId, … }→{ voteId, … }- 호출부:
postVote(parseInt(roomId), postId, voteData)→postVote(parseInt(roomId), voteId, voteData)• PollRecord를 사용하는 상위 컴포넌트
<PollRecord postId={…} … />→<PollRecord voteId={…} … />이렇게 하면 API 시그니처와 변수명이 일치하여 가독성과 유지보수성이 개선됩니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
.gitignore(1 hunks)src/api/record/postVote.ts(1 hunks)src/components/memory/RecordItem/PollRecord.tsx(4 hunks)src/components/memory/RecordItem/RecordItem.tsx(1 hunks)src/pages/memory/Memory.tsx(1 hunks)src/types/memory.ts(1 hunks)src/types/record.ts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/api/record/postVote.ts (1)
src/types/record.ts (2)
VoteData(48-52)VoteRequest(42-45)
🪛 Biome (2.1.2)
src/components/memory/RecordItem/PollRecord.tsx
[error] 136-137: This branch can never execute. Its condition is a duplicate or covered by previous conditions in the if-else-if chain.
(lint/suspicious/noDuplicateElseIf)
🪛 ESLint
src/components/memory/RecordItem/PollRecord.tsx
[error] 121-121: This branch can never execute. Its condition is a duplicate or covered by previous conditions in the if-else-if chain.
(no-dupe-else-if)
🔇 Additional comments (6)
.gitignore (1)
26-28: CLAUDE.md 무시 규칙 추가 LGTM런타임/빌드에 영향 없고, 팀 문서 관리에 도움이 됩니다.
src/types/memory.ts (1)
82-84: PollOption에 voteItemId/isVoted 필드 추가 적절UI 상태와 API 페이로드를 직접 연결할 수 있어 사용성이 좋아졌습니다. id는 React key 용도로 string, voteItemId는 서버 연동용 원본 키로 구분해 둔 선택도 합리적입니다.
src/components/memory/RecordItem/RecordItem.tsx (1)
284-284: postId ↔ voteId 의미 확인 필요API 엔드포인트가
/rooms/{roomId}/vote/{voteId}를 사용하므로, 여기서 전달하는postId가 서버에서 기대하는voteId와 동일 의미/값인지 백엔드 계약을 한 번 확인해 주세요. 동일하다면 네이밍만 혼동 요소이고, 다르다면 잘못된 ID가 전달될 수 있습니다.src/api/record/postVote.ts (2)
7-11: 간결하고 일관된 API 래퍼 구현 👍Axios 래핑과 표준 응답 타입 사용이 명확합니다. 호출부에서 에러를 처리하는 전략도 일관적입니다.
2-2: ApiResponse는@/types/record에서 올바르게 export되고 있습니다
src/types/record.ts에 다음과 같이 선언되어 있어, 별도 경로 수정 없이 기존 import를 그대로 유지하셔도 됩니다.src/components/memory/RecordItem/PollRecord.tsx (1)
68-78: UX 흐름/에러 가드 잘 구성됨중복 클릭 방지(isVoting), 요청 전환(투표/취소) 토글 로직, 성공 시 스낵바 안내까지 흐름이 깔끔합니다.
| if (response.code === 120001) { | ||
| errorMessage = '이미 투표한 투표항목입니다.'; | ||
| } else if (response.code === 120002) { | ||
| errorMessage = '투표하지 않은 투표항목은 취소할 수 없습니다.'; | ||
| } else if (response.code === 140011) { | ||
| errorMessage = '방 접근 권한이 없습니다.'; | ||
| } else if (response.code === 120000) { | ||
| errorMessage = '투표는 존재하지만 투표항목이 비어있습니다.'; | ||
| } else if (response.code === 140011) { | ||
| errorMessage = '방 접근 권한이 없습니다.'; | ||
| } |
There was a problem hiding this comment.
중복된 조건 분기(140011)로 인해 도달 불가능한 코드 — 린트 에러 발생
response.code === 140011 조건이 두 번 등장합니다. 두 번째 분기는 절대 실행되지 않아 Biome/ESLint 에러를 유발합니다. 중복 분기를 제거해 주세요.
} else if (response.code === 120000) {
errorMessage = '투표는 존재하지만 투표항목이 비어있습니다.';
- } else if (response.code === 140011) {
- errorMessage = '방 접근 권한이 없습니다.';
- }
+ }📝 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 (response.code === 120001) { | |
| errorMessage = '이미 투표한 투표항목입니다.'; | |
| } else if (response.code === 120002) { | |
| errorMessage = '투표하지 않은 투표항목은 취소할 수 없습니다.'; | |
| } else if (response.code === 140011) { | |
| errorMessage = '방 접근 권한이 없습니다.'; | |
| } else if (response.code === 120000) { | |
| errorMessage = '투표는 존재하지만 투표항목이 비어있습니다.'; | |
| } else if (response.code === 140011) { | |
| errorMessage = '방 접근 권한이 없습니다.'; | |
| } | |
| if (response.code === 120001) { | |
| errorMessage = '이미 투표한 투표항목입니다.'; | |
| } else if (response.code === 120002) { | |
| errorMessage = '투표하지 않은 투표항목은 취소할 수 없습니다.'; | |
| } else if (response.code === 140011) { | |
| errorMessage = '방 접근 권한이 없습니다.'; | |
| } else if (response.code === 120000) { | |
| errorMessage = '투표는 존재하지만 투표항목이 비어있습니다.'; | |
| } |
🧰 Tools
🪛 ESLint
[error] 121-121: This branch can never execute. Its condition is a duplicate or covered by previous conditions in the if-else-if chain.
(no-dupe-else-if)
🤖 Prompt for AI Agents
In src/components/memory/RecordItem/PollRecord.tsx around lines 113 to 123, the
condition response.code === 140011 is duplicated causing the second branch to be
unreachable and triggering a lint error; remove the duplicate else-if (the
second occurrence) or replace it with the intended response.code value if a
different error case was meant, ensuring each response.code value appears only
once and the branches cover the distinct error codes.
| voteItemId: item.voteItemId, | ||
| isVoted: item.isVoted, | ||
| })), |
There was a problem hiding this comment.
🛠️ Refactor suggestion
isHighest 판단을 index 기반으로 처리 — 잘못된 하이라이트 가능
index === 0은 응답 정렬에 의존합니다. 서버가 항상 득표율 내림차순을 보장하지 않으면 최다 득표 옵션이 아닌 항목이 강조될 수 있습니다. 득표율의 최댓값을 기준으로 판단하도록 수정하는 것을 권장합니다.
간단 적용 diff(최대값을 미리 계산해서 사용):
- isHighest: index === 0,
+ isHighest: item.percentage === highestPercentage,위 변경을 위해 convertPostToRecord 내부에서 매핑 전에 최댓값을 계산해 주세요:
// map 호출 직전 추가
const highestPercentage =
post.voteItems.length > 0
? Math.max(...post.voteItems.map(i => i.percentage))
: 0;동률 처리도 위 방식으로 모두 표시되어 UI 관점에서 자연스럽습니다.
🤖 Prompt for AI Agents
In src/pages/memory/Memory.tsx around lines 36 to 38, the code uses index === 0
to set isHighest which incorrectly assumes the server returns items sorted;
compute the maximum percentage from post.voteItems before mapping and use a
comparison against that max to set isHighest instead of checking index.
Specifically, add a highestPercentage variable (0 when no items) before the map,
then inside the map set isHighest when item.percentage === highestPercentage (or
item.percentage >= highestPercentage if you prefer inclusive ties) so all tied
top items are highlighted.
#️⃣ 연관된 이슈
#106
📝 작업 내용
메모리 페이지의 투표 게시글에서 사용자가 직접 투표하거나 투표를 취소할 수 있는 기능을 구현했습니다. 기존에는 투표 결과만 확인할 수 있었지만, 이제 실시간으로 투표에 참여하고 결과를 확인할 수 있습니다.
🕸️ 주요 구현 내용
/rooms/{roomId}/vote/{voteId}엔드포인트를 호출하는postVote함수 구현VoteRequest,VoteData,VoteItemResult) 추가PollRecord컴포넌트에 투표 클릭 핸들러 및 상태 관리 추가voteItemId,isVoted필드 누락 문제 해결사용자 경험
Summary by CodeRabbit