Skip to content

feat: 투표하기 API 연동 및 실시간 투표 기능 구현#157

Merged
ljh130334 merged 2 commits intodevelopfrom
feat/api-rooms-poll
Aug 18, 2025
Merged

feat: 투표하기 API 연동 및 실시간 투표 기능 구현#157
ljh130334 merged 2 commits intodevelopfrom
feat/api-rooms-poll

Conversation

@ljh130334
Copy link
Member

@ljh130334 ljh130334 commented Aug 18, 2025

#️⃣ 연관된 이슈

#106

📝 작업 내용

메모리 페이지의 투표 게시글에서 사용자가 직접 투표하거나 투표를 취소할 수 있는 기능을 구현했습니다. 기존에는 투표 결과만 확인할 수 있었지만, 이제 실시간으로 투표에 참여하고 결과를 확인할 수 있습니다.

🕸️ 주요 구현 내용

  • API 레이어: POST /rooms/{roomId}/vote/{voteId} 엔드포인트를 호출하는 postVote 함수 구현
  • 타입 정의: 투표 요청/응답 타입(VoteRequest, VoteData, VoteItemResult) 추가
  • 컴포넌트 업데이트: PollRecord 컴포넌트에 투표 클릭 핸들러 및 상태 관리 추가
  • 실시간 업데이트: 투표 후 서버 응답 데이터를 바탕으로 투표 결과 실시간 반영
  • 에러 처리: 에러 코드별 상세 메시지 및 토스트 팝업 표시
  • 데이터 동기화: Memory 페이지에서 voteItemId, isVoted 필드 누락 문제 해결

사용자 경험

  • 투표 옵션 클릭으로 투표/취소 가능
  • 투표 진행 중 UI 비활성화 및 로딩 상태 표시
  • 성공/실패 시 토스트 메시지 피드백 제공
  • 투표 후 즉시 결과 업데이트

Summary by CodeRabbit

  • New Features
    • 기록의 설문 항목에 투표/취소 기능을 추가했습니다. 옵션 클릭 시 즉시 반영되며 퍼센트와 선택 여부가 업데이트됩니다. 중복 클릭 방지, 성공/오류 스낵바 안내(중복 투표, 취소 불가, 접근 권한, 항목 없음, 네트워크 오류 등)를 제공합니다.
  • Style
    • 투표 옵션에 인터랙션 개선(커서/투명도 변화)과 단계적 애니메이션(지연 효과)을 적용했습니다.
  • Chores
    • CLAUDE.md를 Git 무시 목록에 추가했습니다.

  - 투표하기 API 함수 구현 (postVote)
  - 투표 관련 타입 정의 추가 (VoteRequest, VoteData, VoteItemResult)
  - PollOption 타입에 voteItemId, isVoted 필드 추가
  - PollRecord 컴포넌트에 투표 클릭 핸들러 구현
  - 실시간 투표 결과 업데이트 및 상태 관리
  - 에러 코드별 상세 메시지 처리 및 토스트 팝업
  - 투표 진행 중 UI 비활성화 처리
@vercel
Copy link

vercel bot commented Aug 18, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Aug 18, 2025 9:52am

@coderabbitai
Copy link

coderabbitai bot commented Aug 18, 2025

Walkthrough

클라이언트 투표 기능을 추가했습니다. 투표 요청용 타입(VoteRequest/VoteData)을 도입하고, POST 투표 API(postVote)를 신설했습니다. PollRecord가 인터랙티브하게 투표/취소를 수행하며, 변환 로직과 타입에 voteItemId/isVoted 필드를 확장했습니다. RecordItem은 PollRecord에 postId와 onVoteUpdate를 전달합니다. .gitignore에 CLAUDE.md를 무시하도록 추가했습니다.

Changes

Cohort / File(s) Summary of changes
Git ignore 업데이트
\.gitignore
CLAUDE.md 무시 규칙과 주석 추가.
투표 API 및 타입 추가
src/api/record/postVote.ts, src/types/record.ts
POST /rooms/{roomId}/vote/{voteId} 호출 함수 postVote 추가. 투표 요청/응답용 공개 타입 VoteRequest, VoteData, VoteItemResult, 별칭 VoteResponse 도입.
Poll UI 투표 로직
src/components/memory/RecordItem/PollRecord.tsx, src/components/memory/RecordItem/RecordItem.tsx
PollRecord에 투표 흐름 추가: URL에서 roomId 사용, postVote 호출, 결과 반영/스낵바 노출, 중복 요청 방지. Props에 postId, onVoteUpdate 추가. RecordItem이 해당 props 전달.
메모리 변환 및 타입 확장
src/pages/memory/Memory.tsx, src/types/memory.ts
PollOption에 voteItemId: number, isVoted: boolean 필드 추가 및 변환 로직에서 해당 값 채움.

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)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

✨ Feature

Poem

작은 발로 톡, 선택지에 점프!
표를 실어 휙—서버로 점프! 🐇
퍼센트는 흔들흔들, 그래프는 방긋
투표했지요? 딩동! 스낵바 반짝 ✨
오늘도 기억의 들판에 체크 한 표!

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/api-rooms-poll

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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의 postIdvoteId로 통일해주세요

API 스펙(/rooms/{roomId}/vote/{voteId})에 따라 postVote(roomId, voteId, …)의 두 번째 파라미터는 voteId가 맞습니다. 현재 PollRecord 컴포넌트는 prop 이름을 postId로 사용하고 있어 혼동의 여지가 있습니다. 변수명을 일관성 있게 맞추기 위해 아래를 권장드립니다:

• 파일: src/components/memory/RecordItem/PollRecord.tsx

  • PollRecordProps: postId: numbervoteId: 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.

📥 Commits

Reviewing files that changed from the base of the PR and between f39b5cb and a265d79.

📒 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), 요청 전환(투표/취소) 토글 로직, 성공 시 스낵바 안내까지 흐름이 깔끔합니다.

Comment on lines +113 to +123
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 = '방 접근 권한이 없습니다.';
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

중복된 조건 분기(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.

Suggested change
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.

Comment on lines +36 to 38
voteItemId: item.voteItemId,
isVoted: item.isVoted,
})),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

@ljh130334 ljh130334 merged commit 5831632 into develop Aug 18, 2025
3 checks passed
@ljh130334 ljh130334 deleted the feat/api-rooms-poll branch August 19, 2025 01:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant