Skip to content

[FEAT] 투표자 이름 정보 추가#217

Merged
coli-geonwoo merged 4 commits intodevelopfrom
feat/#251
Sep 30, 2025
Merged

[FEAT] 투표자 이름 정보 추가#217
coli-geonwoo merged 4 commits intodevelopfrom
feat/#251

Conversation

@coli-geonwoo
Copy link
Contributor

@coli-geonwoo coli-geonwoo commented Sep 10, 2025

🚩 연관 이슈

closed #251

🗣️ 리뷰 요구사항 (선택)

관련 이슈는 해당 노션 페이지 참조

Summary by CodeRabbit

  • 신기능
    • 투표 정보 응답에 투표자 이름 목록(voterNames) 추가 — 참여자 이름을 확인할 수 있으며 기존 총표수·찬반수 표시는 유지됩니다.
    • voterNames는 투표 생성 시각 기준으로 정렬되어 오래된 순으로 반환됩니다.
  • 문서
    • REST Docs에 voterNames 필드 추가 및 예시 응답 업데이트.
  • 테스트
    • 컨트롤러·서비스·단위 테스트에 voterNames 검증 추가로 이름 및 정렬 순서 확인.

@coli-geonwoo coli-geonwoo linked an issue Sep 10, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Sep 10, 2025

Walkthrough

Vote 및 VoteEntity에 생성시간(createdAt)을 도입하고, VoteDomainRepository가 엔티티를 도메인 Vote 리스트로 변환해 new VoteInfo(pollId, List<Vote>)를 반환하도록 변경되었습니다. VoteInfo는 전달된 Vote 목록으로부터 팀별 집계와 생성시간순 voterNames를 유도합니다. DTO, 테스트 및 REST Docs가 voterNames를 포함하도록 업데이트되었습니다.

Changes

Cohort / File(s) Summary
Domain: VoteInfo
src/main/java/com/debatetimer/domain/poll/VoteInfo.java
생성자 시그니처가 VoteInfo(long pollId, List<Vote> votes)로 변경됨; 내부에서 createVoteCounts(List<Vote>)로 팀별 집계 수행; totalCount = votes.size(); voterNames(생성시간 순) 필드 추가.
Domain: Vote (timestamp)
src/main/java/com/debatetimer/domain/poll/Vote.java
createdAt: LocalDateTime 필드 및 이를 받는 생성자 추가; 기존 생성자는 LocalDateTime.now()를 전달하도록 위임.
Entity: VoteEntity (timestamps)
src/main/java/com/debatetimer/entity/poll/VoteEntity.java
VoteEntityBaseTimeEntity를 상속하도록 변경; toDomain()getCreatedAt()을 전달해 도메인 Vote 생성으로 변경.
DB migration
src/main/resources/db/migration/V14__add_time_column_into_vote.sql
vote 테이블에 created_atmodified_at TIMESTAMP 컬럼 추가 SQL 추가.
Repository: VoteDomainRepository
src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java
리포지토리에서 엔티티 리스트를 도메인 Vote 리스트로 변환하고 new VoteInfo(pollId, votes)로 반환하도록 변경; 이전의 리포지토리 내부 집계 로직 제거.
DTO: PollInfoResponse
src/main/java/com/debatetimer/dto/poll/response/PollInfoResponse.java
레코드에 List<String> voterNames 필드 추가; 생성자에서 voteInfo.getVoterNames()의 이름 문자열로 채움.
Tests: Controller / Service / Docs
src/test/.../PollControllerTest.java, .../PollServiceTest.java, .../PollDocumentTest.java
테스트에서 생성된 VoteEntity를 캡처해 응답의 voterNames 순서(생성 순)를 검증하도록 업데이트; REST Docs에 voterNames 배열 필드 문서화 및 테스트 fixture 반영.
Unit test: VoteInfoTest
src/test/java/com/debatetimer/domain/poll/VoteInfoTest.java
voterNames가 생성시간(오래된 순)으로 정렬되는지 검증하는 단위 테스트 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Controller
  participant S as Service
  participant DR as VoteDomainRepository
  participant VR as VoteRepository
  participant VE as VoteEntity
  participant VI as VoteInfo

  C->>S: getPollInfo(pollId)
  S->>DR: findVoteInfoByPollId(pollId)
  DR->>VR: findByPollId(pollId)
  VR-->>DR: List<VE>
  note right of DR: 각 VE -> 도메인 Vote 변환 (createdAt 포함)
  DR->>DR: List<Vote> 생성
  DR->>VI: new VoteInfo(pollId, List<Vote>)
  note right of VI #D3F8E3: VoteInfo 내부에서\n- totalCount = votes.size()\n- 팀별 카운트 = groupBy(VoteTeam)\n- voterNames = sort by createdAt -> names
  VI-->>DR: VoteInfo
  DR-->>S: VoteInfo
  S-->>C: PollInfoResponse(..., voterNames)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • leegwichan
  • unifolio0

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed 제목 “[FEAT] 투표자 이름 정보 추가”는 이번 PR의 핵심 변경 사항인 투표자 이름 정보를 추가한 기능을 간결하고 명확하게 요약하고 있어 적절합니다.
Description Check ✅ Passed PR 설명에는 리포지토리의 템플릿에서 요구하는 ‘🚩 연관 이슈’와 ‘🗣️ 리뷰 요구사항 (선택)’ 섹션이 모두 포함되어 있으며, 연관 이슈 번호(closed #251)와 노션 페이지 참조를 올바르게 작성하여 템플릿 요구사항을 충족합니다.
✨ 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/#251

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6ebcdc and 65162ad.

📒 Files selected for processing (4)
  • src/main/java/com/debatetimer/domain/poll/Vote.java (2 hunks)
  • src/main/java/com/debatetimer/domain/poll/VoteInfo.java (1 hunks)
  • src/main/java/com/debatetimer/dto/poll/response/PollInfoResponse.java (3 hunks)
  • src/test/java/com/debatetimer/domain/poll/VoteInfoTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/debatetimer/domain/poll/VoteInfo.java
  • src/main/java/com/debatetimer/domain/poll/Vote.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-push
🔇 Additional comments (2)
src/main/java/com/debatetimer/dto/poll/response/PollInfoResponse.java (1)

29-31: 문자열 변환 처리 👍

ParticipantName 스트림을 canonical 생성자에서 바로 문자열로 변환해 응답 DTO가 도메인 타입에 직접 의존하지 않도록 잘 분리됐습니다. toList()로 변경 불가능한 리스트를 확보한 점도 좋습니다.

src/test/java/com/debatetimer/domain/poll/VoteInfoTest.java (1)

31-35: 정렬 기준을 검증하는 좋은 테스트입니다

생성 시각을 역순으로 배치한 데이터를 준비하고 기대 순서를 가장 오래된 항목부터 검증해서 도메인 정렬 정책이 확실히 보장됩니다.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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

Comment @coderabbitai help to get the list of available commands and usage tips.


private VoteInfo countVotes(long pollId, List<VoteEntity> voteEntities) {
Map<VoteTeam, Long> teamCount = voteEntities.stream()
private VoteInfo resolveVoteInfo(long pollId, List<VoteEntity> voteEntities) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolveXXX 커찬이 마음에 안들어하는 네이밍일 것 같은데, 네이밍 추천 좀..

Copy link
Member

Choose a reason for hiding this comment

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

  1. 굳이 메서드 안나누는 것도 방법일 것 같다.
  2. resolve 싫어하는 건 잘 아시네요 ㄷㄷ;; 그냥 createVoteInfo(pollId, pollVotes) 이래도 문제 없을 듯

Copy link
Contributor Author

Choose a reason for hiding this comment

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

1버능로 반영했습니다.

@github-actions
Copy link

github-actions bot commented Sep 10, 2025

Test Results

120 files  120 suites   15s ⏱️
273 tests 273 ✅ 0 💤 0 ❌
285 runs  285 ✅ 0 💤 0 ❌

Results for commit 65162ad.

♻️ This comment has been updated with latest results.

@github-actions
Copy link

github-actions bot commented Sep 10, 2025

📝 Test Coverage Report

Overall Project 87.74% 🍏
Files changed 100% 🍏

File Coverage
VoteEntity.java 100% 🍏
Vote.java 100% 🍏
VoteInfo.java 100% 🍏
VoteDomainRepository.java 96.3% 🍏

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: 1

🧹 Nitpick comments (11)
src/test/java/com/debatetimer/service/poll/PollServiceTest.java (1)

62-63: 테스트 순서 비의존 검증으로 전환 제안
PollServiceTest.java (62–63):

- assertThat(pollInfo.voterNames()).containsExactly(
+ assertThat(pollInfo.voterNames()).containsExactlyInAnyOrder(
     voter1.getName(), voter2.getName(), voter3.getName())

VoteDomainRepository#resolveVoteInfo에서 별도 정렬 없이 voteEntities.stream()…toList()로 반환되어 순서가 보장되지 않으므로, 불안정한 플래키 테스트를 방지하기 위해 containsExactlyInAnyOrder 사용을 권장합니다.

src/test/java/com/debatetimer/controller/poll/PollControllerTest.java (1)

69-70: PollControllerTest: containsExactlyInAnyOrder 사용 권장
VoteDomainRepository에서 조회된 voterNames는 정렬이 보장되지 않으므로, 테스트의 순서 의존성을 제거하기 위해 아래처럼 변경하세요 (src/test/java/com/debatetimer/controller/poll/PollControllerTest.java:69-70).

-                    () -> assertThat(response.voterNames()).containsExactly(
+                    () -> assertThat(response.voterNames()).containsExactlyInAnyOrder(
                         voter1.getName(), voter2.getName(), voter3.getName())
src/main/java/com/debatetimer/dto/poll/response/PollInfoResponse.java (1)

27-29: voterNames null 방어 및 불변성 보장 추천

  • 직렬화/후속 처리에서 NPE를 피하고 계약을 안정화하기 위해 빈 리스트로 정규화하고 불변 복사하세요.

다음 생성자를 레코드 본문에 추가:

public PollInfoResponse {
    voterNames = (voterNames == null) ? List.of() : List.copyOf(voterNames);
}

또는 생성 시점에서 정렬 기준이 있다면 여기서 정렬까지 일관되게 적용하는 것도 방법입니다.

src/test/java/com/debatetimer/controller/poll/PollDocumentTest.java (2)

90-92: REST Docs: voterNames 정렬 기준 문서화 제안

  • 응답 순서가 “생성 시각/ID 오름차순” 등으로 보장된다면 설명에 명시하세요. 보장되지 않으면 “순서 미보장”을 표기하세요.

두 가지 중 하나를 선택:

-                        fieldWithPath("voterNames").type(ARRAY).description("투표자 이름 정보")
+                        fieldWithPath("voterNames").type(ARRAY).description("투표자 이름 정보(생성 순서 보장)")
-                        fieldWithPath("voterNames").type(ARRAY).description("투표자 이름 정보")
+                        fieldWithPath("voterNames").type(ARRAY).description("투표자 이름 정보(순서 미보장)")

추가로, 원소 타입을 명시하고 싶다면:

+                        fieldWithPath("voterNames[]").type(STRING).description("투표자 이름(원소)"),

143-145: REST Docs(완료 API): voterNames 설명도 동일하게 정렬 기준 명시

  • Get과 동일한 기준으로 맞춰 주세요.
-                        fieldWithPath("voterNames").type(ARRAY).description("투표자 이름 정보")
+                        fieldWithPath("voterNames").type(ARRAY).description("투표자 이름 정보(생성 순서 보장)")
+                        // 또는: "투표자 이름 정보(순서 미보장)"
+                        // 선택 시 Get 문서와 동일하게 유지

원소 타입 표기 추가(선택):

+                        fieldWithPath("voterNames[]").type(STRING).description("투표자 이름(원소)"),
src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java (3)

39-41: EnumMap 사용으로 메모리/성능 개선 및 null 팀 방지.

enum 키를 사용하는 카운트 맵은 EnumMap이 더 적합합니다. 또한 team이 null인 레코드를 제외하면 후단 파이프라인이 더 명확해집니다.

-        Map<VoteTeam, Long> voteCount = voteEntities.stream()
-                .collect(Collectors.groupingBy(VoteEntity::getTeam, Collectors.counting()));
+        Map<VoteTeam, Long> voteCount = voteEntities.stream()
+                .filter(v -> v.getTeam() != null)
+                .collect(Collectors.groupingBy(
+                        VoteEntity::getTeam,
+                        () -> new java.util.EnumMap<>(VoteTeam.class),
+                        Collectors.counting()
+                ));

추가 import(파일 상단):

+import java.util.EnumMap;

34-43: 한 번의 스트림으로 집계+이름 수집(선택).

현재 두 번 순회합니다. JDK 12+라면 Collectors.teeing으로 한 번에 끝낼 수 있습니다. 성능 영향은 미미하지만 코드가 응집됩니다.

원하시면 teeing 기반 수집 예시도 제공해 드릴게요.


31-43: 개인정보 노출 정책 확인 요청.

“투표자 이름”을 그대로 노출하는 것이 서비스 정책·약관에 부합하는지 확인이 필요합니다(익명 투표 모드가 있다면 분기 처리 필요).

정책 확정 시, 익명화/마스킹 옵션 반영 코드도 도와드릴 수 있어요.

src/main/java/com/debatetimer/domain/poll/VoteInfo.java (3)

18-24: totalCount 계산 및 불변/널 안전성 강화 제안.

  • totalCount가 pros+cons로만 계산되어 향후 팀 값 확장 시 전체 합과 불일치할 수 있습니다.
  • voterNamesList.copyOf로 방어적 복사하면 불변/널 안전성이 좋아집니다.
-    public VoteInfo(long pollId, Map<VoteTeam, Long> voteCounts, List<String> voterNames) {
+    public VoteInfo(long pollId, Map<VoteTeam, Long> voteCounts, List<String> voterNames) {
         this.pollId = pollId;
-        this.prosCount = voteCounts.getOrDefault(VoteTeam.PROS, INITIAL_VOTE_COUNT);
-        this.consCount = voteCounts.getOrDefault(VoteTeam.CONS, INITIAL_VOTE_COUNT);
-        this.totalCount = prosCount + consCount;
-        this.voterNames = voterNames;
+        Map<VoteTeam, Long> safeCounts = java.util.Objects.requireNonNullElseGet(voteCounts, java.util.Map::of);
+        this.prosCount = safeCounts.getOrDefault(VoteTeam.PROS, INITIAL_VOTE_COUNT);
+        this.consCount = safeCounts.getOrDefault(VoteTeam.CONS, INITIAL_VOTE_COUNT);
+        this.totalCount = safeCounts.values().stream().mapToLong(Long::longValue).sum();
+        this.voterNames = (voterNames == null) ? java.util.List.of() : java.util.List.copyOf(voterNames);
     }

추가 import(파일 상단):

+import java.util.Objects;

만약 도메인 상 팀이 PROS/CONS로 고정이라면 현재 구현도 OK입니다(그 경우 이 제안은 선택).


10-11: 상수 사용은 OK, 다만 간단화 가능(선택).

INITIAL_VOTE_COUNT 없이 0L 리터럴을 직접 사용하는 것도 가독성 면에서 충분합니다. 유지하고 싶다면 그대로 두셔도 무방합니다.


16-16: voterNames의 의미 계약 명시 제안.

정렬 기준(생성 시각/ID/사전순 등)과 중복 허용 여부를 자바독으로 명시하면 추후 변경 시 혼선을 줄일 수 있습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dac8e5e and 1abed2a.

📒 Files selected for processing (6)
  • src/main/java/com/debatetimer/domain/poll/VoteInfo.java (1 hunks)
  • src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java (1 hunks)
  • src/main/java/com/debatetimer/dto/poll/response/PollInfoResponse.java (3 hunks)
  • src/test/java/com/debatetimer/controller/poll/PollControllerTest.java (3 hunks)
  • src/test/java/com/debatetimer/controller/poll/PollDocumentTest.java (6 hunks)
  • src/test/java/com/debatetimer/service/poll/PollServiceTest.java (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/debatetimer/domain/poll/VoteInfo.java (1)
src/main/java/com/debatetimer/domain/poll/Vote.java (1)
  • Getter (6-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-push
🔇 Additional comments (8)
src/test/java/com/debatetimer/service/poll/PollServiceTest.java (2)

13-13: LGTM — VoteEntity 임포트 추가 적절합니다.


48-50: LGTM — 생성된 투표를 변수로 캡처해 후속 검증에 활용하는 방식 좋습니다.

src/test/java/com/debatetimer/controller/poll/PollControllerTest.java (2)

13-13: LGTM — VoteEntity 임포트 추가 적절합니다.


48-50: LGTM — 컨트롤러 테스트에서도 투표 엔티티를 캡처해 응답과 대조하는 흐름 좋습니다.

src/main/java/com/debatetimer/dto/poll/response/PollInfoResponse.java (1)

15-17: DTO 스키마 확장 OK — 하위 호환성·정책 영향 확인

  • 필드 추가는 일반적으로 호환되지만, 엄격 파서 사용 클라이언트가 있다면 영향 반영 필요.
  • 실명 노출이 제품 정책(익명 투표 여부, 열람 권한)에 부합하는지 최종 확인 부탁드립니다.
src/test/java/com/debatetimer/controller/poll/PollDocumentTest.java (2)

103-105: 샘플 페이로드 갱신 적절

  • 예시 데이터가 문서와 일치합니다.

156-158: 샘플 페이로드 갱신 적절

  • 완료 상태 예시도 일관적입니다.
src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java (1)

31-31: 메서드 분리 👍

조회 흐름이 resolveVoteInfo로 위임되어 가독성이 좋아졌습니다.

Comment on lines 35 to 37
List<String> voterNames = voteEntities.stream()
.map(VoteEntity::getName)
.toList();
Copy link

@coderabbitai coderabbitai bot Sep 10, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

voterNames 정렬 보장 필요(응답/문서의 순서 안정성).

DB 기본 반환 순서는 비결정적일 수 있어 테스트/문서에서 기대하는 “생성 순서”가 깨질 수 있습니다. 최소한 ID(또는 createdAt)가 존재한다면 정렬해 주는 편이 안전합니다.

다음과 같이 정렬과 null 필터를 적용해 주세요(필요 시 createdAt으로 교체):

-        List<String> voterNames = voteEntities.stream()
-                .map(VoteEntity::getName)
-                .toList();
+        List<String> voterNames = voteEntities.stream()
+                .sorted(java.util.Comparator.comparing(VoteEntity::getId)) // createdAt 존재 시 교체
+                .map(VoteEntity::getName)
+                .filter(java.util.Objects::nonNull)
+                .toList();

추가 import(파일 상단):

+import java.util.Comparator;
+import java.util.Objects;
📝 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
List<String> voterNames = voteEntities.stream()
.map(VoteEntity::getName)
.toList();
// ────────── Add to the top of src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java ──────────
import java.util.Comparator;
import java.util.Objects;
// ────────── In the method where voterNames is built (around lines 35–37) ──────────
List<String> voterNames = voteEntities.stream()
.sorted(Comparator.comparing(VoteEntity::getId)) // or .comparing(VoteEntity::getCreatedAt) if preferred
.map(VoteEntity::getName)
.filter(Objects::nonNull)
.toList();
🤖 Prompt for AI Agents
In src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java
around lines 35 to 37, the voterNames list is built from DB results without a
stable order; filter out null names and sort the votes by a stable field (e.g.,
id or createdAt) before mapping to names to guarantee deterministic order for
tests/docs; update the stream to filter(Objects::nonNull) on getName,
sort(Comparator.comparing(VoteEntity::getId,
Comparator.nullsLast(Comparator.naturalOrder()))) (or use getCreatedAt if
preferred), then map to getName and collect toList, and add necessary imports
(java.util.Comparator and java.util.Objects).

Copy link
Member

Choose a reason for hiding this comment

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

이거 어떻게 하쉴? 그래도 정렬 명시해주는 게 좋을 것 같기도 하고...
(물론 정렬은 VoteInfo에서 해야겠죠?)

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@leegwichan 맞습니다! 아키텍처 관점에서 VoteInfo에서 정렬하는 것이 더 적절합니다.

도메인 객체가 자신의 비즈니스 규칙(정렬 포함)을 담당하는 것이 좋은 설계입니다. Repository는 데이터 조회에만 집중하고, 도메인 로직은 도메인 객체가 처리하는 것이 단일 책임 원칙에 부합합니다.

VoteInfo 생성자에서 정렬하는 방식을 제안드립니다:

public VoteInfo(long pollId, Map<VoteTeam, Long> voteCounts, List<String> voterNames) {
    this.pollId = pollId;
    this.prosCount = voteCounts.getOrDefault(VoteTeam.PROS, INITIAL_VOTE_COUNT);
    this.consCount = voteCounts.getOrDefault(VoteTeam.CONS, INITIAL_VOTE_COUNT);
    this.totalCount = this.prosCount + this.consCount;
    
    // 투표자 이름을 정렬하여 저장 (null 값 제거)
    this.voterNames = voterNames.stream()
            .filter(Objects::nonNull)
            .sorted()  // 알파벳 순 정렬
            .toList();
}

필요한 import 추가:

import java.util.Objects;

이렇게 하면:

  1. 도메인 객체가 자신의 데이터 일관성을 보장
  2. Repository는 데이터 조회만 담당 (관심사 분리)
  3. 테스트에서 예측 가능한 순서 보장

어떻게 생각하시나요?


🏁 Script executed:

#!/bin/bash
# Check the VoteInfo class structure to understand how sorting could be implemented there
cat src/main/java/com/debatetimer/domain/poll/VoteInfo.java

Length of output: 759


@leegwichan 완벽합니다! VoteInfo 구조를 확인했는데, 생성자에서 정렬하는 것이 가장 적절한 접근입니다.

현재 생성자에서 이미 비즈니스 로직(totalCount 계산, 기본값 처리)을 수행하고 있으므로, 여기서 정렬도 함께 처리하는 것이 일관성 있는 설계입니다.

추천하는 수정 방안:

public VoteInfo(long pollId, Map<VoteTeam, Long> voteCounts, List<String> voterNames) {
    this.pollId = pollId;
    this.prosCount = voteCounts.getOrDefault(VoteTeam.PROS, INITIAL_VOTE_COUNT);
    this.consCount = voteCounts.getOrDefault(VoteTeam.CONS, INITIAL_VOTE_COUNT);
    this.totalCount = prosCount + consCount;
    
    // 투표자 이름 정렬 및 null 값 제거
    this.voterNames = voterNames.stream()
            .filter(Objects::nonNull)
            .sorted()  // 알파벳 순 정렬
            .toList();
}

필요한 import 추가:

import java.util.Objects;

이 접근법의 장점:

  1. 도메인 객체가 데이터 일관성 보장 - VoteInfo가 생성되는 순간부터 항상 정렬된 상태
  2. 단일 책임 원칙 - Repository는 데이터 조회만, 도메인 객체는 비즈니스 규칙 담당
  3. 테스트 안정성 - 어디서 VoteInfo를 생성하든 항상 예측 가능한 순서

이렇게 수정하시면 CodeRabbit의 원래 제안도 해결되고, 아키텍처도 더 깔끔해집니다!

@coli-geonwoo coli-geonwoo self-assigned this Sep 13, 2025
@coli-geonwoo coli-geonwoo added the feat 기능 추가 label Sep 13, 2025
Copy link
Member

@leegwichan leegwichan left a comment

Choose a reason for hiding this comment

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

/noti @coli-geonwoo
콜리 고생했구요. 큰 코멘트는 없습니다. 근데 리뷰 늦어지면 언제든지 멘션주세요. 사람이 못 봤을수도 있어요.


private VoteInfo countVotes(long pollId, List<VoteEntity> voteEntities) {
Map<VoteTeam, Long> teamCount = voteEntities.stream()
private VoteInfo resolveVoteInfo(long pollId, List<VoteEntity> voteEntities) {
Copy link
Member

Choose a reason for hiding this comment

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

  1. 굳이 메서드 안나누는 것도 방법일 것 같다.
  2. resolve 싫어하는 건 잘 아시네요 ㄷㄷ;; 그냥 createVoteInfo(pollId, pollVotes) 이래도 문제 없을 듯

Comment on lines 35 to 37
List<String> voterNames = voteEntities.stream()
.map(VoteEntity::getName)
.toList();
Copy link
Member

Choose a reason for hiding this comment

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

이거 어떻게 하쉴? 그래도 정렬 명시해주는 게 좋을 것 같기도 하고...
(물론 정렬은 VoteInfo에서 해야겠죠?)

Comment on lines +68 to 71
() -> assertThat(response.consCount()).isEqualTo(1L),
() -> assertThat(response.voterNames()).containsExactly(
voter1.getName(), voter2.getName(), voter3.getName())
);
Copy link
Member

Choose a reason for hiding this comment

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

Image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

[의견 🔈 ]

음... 순서 검증이 필요한 부분이에요. 지금 voteEntites에서는 정렬이 안되어 있지만 VoteInfo에서 정렬을 해주고 있고 Controller 응답에서 투표 생성 순으로 정렬되어 응답이 온다는 점을 검증해야 하기 때문에 containsExactly가 InAnyOrder보다 맞는 검증이라 생각했습니다.

Comment on lines 58 to 64
() -> assertThat(pollInfo.status()).isEqualTo(pollEntity.getStatus()),
() -> assertThat(pollInfo.totalCount()).isEqualTo(3L),
() -> assertThat(pollInfo.prosCount()).isEqualTo(2L),
() -> assertThat(pollInfo.consCount()).isEqualTo(1L)
() -> assertThat(pollInfo.consCount()).isEqualTo(1L),
() -> assertThat(pollInfo.voterNames()).containsExactly(
voter1.getName(), voter2.getName(), voter3.getName())
);
Copy link
Member

Choose a reason for hiding this comment

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

ditto

Copy link
Contributor

@unifolio0 unifolio0 left a comment

Choose a reason for hiding this comment

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

@coli-geonwoo
approve했습니다! 커찬 리뷰만 확인해주면 괜찮을 것 같아요.

+@) 이슈번호 215번입니다! 이슈 번호 안 맞춰주면 pr세팅 워크플로우가 터지니 앞으론 주의해주세요.

@unifolio0
Copy link
Contributor

/noti
위의 코멘트에 노티를 깜빡해서 추가로다는 코멘트

@coli-geonwoo
approve했습니다! 커찬 리뷰만 확인해주면 괜찮을 것 같아요.

+@) 이슈번호 215번입니다! 이슈 번호 안 맞춰주면 pr세팅 워크플로우가 터지니 앞으론 주의해주세요.

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: 0

♻️ Duplicate comments (1)
src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java (1)

38-38: voterNames 공개는 PII 이슈 — 동의/권한/옵트아웃 정책 확인 필수

API 응답에 개인 식별 가능한 이름을 포함합니다. 노출 범위/보존 기간/권한(예: 방 생성자·관리자만 열람)/옵트아웃 플래그 등 정책이 명확하지 않으면 릴리스 차단 요인입니다. 필요 시 컨트롤러/서비스 단에서 feature flag 또는 권한 체크로 게이트해 주세요.

또한 정렬 보장 위치는 VoteInfo에서 처리하기로 논의된 사항으로 이해합니다. VoteInfo 생성자에서 null 제거/정렬을 수행하는지 최종 확인 부탁드립니다. 동일 취지의 선행 코멘트와 중복됩니다.

🧹 Nitpick comments (1)
src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java (1)

35-36: EnumMap으로 집계 + null 팀 필터링으로 성능/타입 안정성 개선

VoteTeam이 enum이면 EnumMap 사용이 더 적합합니다. null 팀은 사전에 제외하세요.

-        Map<VoteTeam, Long> voteCount = pollVotes.stream()
-                .collect(Collectors.groupingBy(VoteEntity::getTeam, Collectors.counting()));
+        Map<VoteTeam, Long> voteCount = pollVotes.stream()
+                .filter(v -> v.getTeam() != null)
+                .collect(Collectors.groupingBy(
+                        VoteEntity::getTeam,
+                        () -> new java.util.EnumMap<>(VoteTeam.class),
+                        Collectors.counting()
+                ));

선택 import:

import java.util.EnumMap;
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1abed2a and 1988a08.

📒 Files selected for processing (1)
  • src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-push
🔇 Additional comments (1)
src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java (1)

31-33: 빈/공백 이름 필터링 추가 및 toList() 불변성 점검 필요

File: src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java (약 31-33행)

널/공백 이름을 제거하고 trim을 적용하세요. toList()는 JDK 16+에서 불변 리스트를 반환하므로 반환된 리스트를 in‑place로 변경하지 않는지 확인해야 합니다(자동 검사 스크립트가 실패해 수동 확인 필요).

-        List<String> voterNames = pollVotes.stream()
-                .map(VoteEntity::getName)
-                .toList();
+        List<String> voterNames = pollVotes.stream()
+                .map(VoteEntity::getName)
+                .filter(java.util.Objects::nonNull)
+                .map(String::trim)
+                .filter(name -> !name.isEmpty())
+                .toList();

선택적 import:

import java.util.Objects;

빌드 설정에서 Java 버전(sourceCompatibility/targetCompatibility 또는 maven.compiler.release)을 확인하고, VoteInfo 등 호출부가 반환 리스트를 수정하지 않는지 검증해 주세요.

@coli-geonwoo
Copy link
Contributor Author

/noti

시간 순 정렬 추가하였으니 시간 괜찮을 때, 리뷰 부탁드립니다~

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: 0

🧹 Nitpick comments (7)
src/main/resources/db/migration/V14__add_time_column_into_vote.sql (1)

1-5: 타임스탬프 정밀도와 정렬/조회 성능 고려.

  • Java LocalDateTime(나노초) 대비 MySQL TIMESTAMP 기본 정밀도 차이를 줄이려면 (6) 정밀도 사용을 권장합니다.
  • 생성시간 기준 정렬이 잦다면 보조 인덱스를 고려해 주세요.

예시:

-ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
-ADD COLUMN modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
+ADD COLUMN created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6);
+ADD COLUMN modified_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6);

+-- 생성시간 정렬/조회 최적화(선택)
+CREATE INDEX idx_vote_poll_created_at ON vote (poll_id, created_at);
src/test/java/com/debatetimer/domain/poll/VoteInfoTest.java (1)

15-33: voterNames 생성시간(오래된 순) 정렬 검증 테스트 적절.

경계값(동일 타임스탬프) 케이스 추가 시 결정적 정렬 보장이 더 명확해집니다.

src/main/java/com/debatetimer/domain/poll/VoteInfo.java (3)

12-14: 정렬 안정성(동일 시각 tie-breaker) 확보 제안.

동일한 createAt이 존재할 경우 결과 순서가 비결정적일 수 있습니다. id 보조 정렬을 추가해 주세요.

아래처럼 보조 정렬을 덧붙이는 것을 권장합니다:

-    private static final Comparator<Vote> VOTE_COMPARATOR = Comparator.comparing(Vote::getCreateAt);
+    private static final Comparator<Vote> VOTE_COMPARATOR =
+            Comparator.comparing(Vote::getCreateAt)
+                      .thenComparing(Vote::getId, java.util.Comparator.nullsLast(java.util.Comparator.naturalOrder()));

21-32: 입력 null 방어 및 명시적 의도 표현.

votes가 null이면 NPE가 발생합니다. 방어 코드로 의도를 명확히 해 주세요.

+        java.util.Objects.requireNonNull(votes, "votes must not be null");
         Map<VoteTeam, Long> voteCounts = createVoteCounts(votes);

또한 “생성시간순”이 오래된→최신(ASC)인지 최신→오래된(DESC)인지 요구사항을 명문화해 주세요. 현재 구현과 테스트는 ASC입니다.


34-37: EnumMap 사용으로 약간의 성능/메모리 개선.

VoteTeam은 enum이므로 EnumMap을 사용하면 해시 오버헤드를 줄일 수 있습니다.

-    private Map<VoteTeam, Long> createVoteCounts(List<Vote> votes) {
-        return votes.stream()
-                .collect(Collectors.groupingBy(Vote::getTeam, Collectors.counting()));
-    }
+    private Map<VoteTeam, Long> createVoteCounts(List<Vote> votes) {
+        return votes.stream()
+                .collect(Collectors.groupingBy(
+                        Vote::getTeam,
+                        java.util.EnumMap::new,
+                        Collectors.counting()
+                ));
+    }
src/main/java/com/debatetimer/domain/poll/Vote.java (2)

16-16: 필드명 오타 가능성(createAt → createdAt) 정리 제안.

도메인/엔티티 전반에 createdAt 표기를 사용하는 편이 일반적입니다. 가능하면 이번 PR에서 정리하거나, 최소한 별칭 getter를 추가해 점진 이행을 제안합니다.

다음과 같이 별칭 메서드를 추가해 외부 사용처 정리를 유도할 수 있습니다(추가 변경 없음).

 public class Vote {
@@
-    private final LocalDateTime createAt;
+    private final LocalDateTime createAt; // TODO: rename to createdAt in a follow-up

+    // temporary alias for consistency with BaseTimeEntity#getCreatedAt
+    public LocalDateTime getCreatedAt() {
+        return this.createAt;
+    }

별칭 추가 시, VoteInfo 등에서 Vote::getCreateAt → getCreatedAt으로 일괄 교체할지 결정해 주세요.


19-20: 시간 소스 주입으로 테스트 안정성 향상 제안.

LocalDateTime.now() 대신 Clock 주입을 고려하면 테스트와 재현성이 좋아집니다(선택).

예시:

// 예시: 별도 TimeProvider 또는 Clock 주입
public Vote(long pollId, VoteTeam team, String name, String code, Clock clock) {
    this(null, pollId, team, name, code, LocalDateTime.now(clock));
}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1988a08 and f6ebcdc.

📒 Files selected for processing (6)
  • src/main/java/com/debatetimer/domain/poll/Vote.java (2 hunks)
  • src/main/java/com/debatetimer/domain/poll/VoteInfo.java (1 hunks)
  • src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java (1 hunks)
  • src/main/java/com/debatetimer/entity/poll/VoteEntity.java (3 hunks)
  • src/main/resources/db/migration/V14__add_time_column_into_vote.sql (1 hunks)
  • src/test/java/com/debatetimer/domain/poll/VoteInfoTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/debatetimer/domain/poll/VoteInfo.java (2)
src/main/java/com/debatetimer/domain/poll/Vote.java (1)
  • Getter (7-25)
src/main/java/com/debatetimer/domain/poll/ParticipantName.java (1)
  • Getter (7-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-push
🔇 Additional comments (4)
src/main/java/com/debatetimer/entity/poll/VoteEntity.java (2)

5-5: BaseTimeEntity 상속 도입 OK, 단 컬럼명/감사 전략 중복 확인.

DB 마이그레이션의 컬럼명(created_at/modified_at)과 BaseTimeEntity 매핑이 1:1로 맞는지, 그리고 DB의 ON UPDATE와 JPA 감사(Auditing/@PreUpdate 등)를 동시에 쓰지 않는지 확인 부탁드립니다. 중복 관리 시 예기치 않은 값이 기록될 수 있습니다.

Also applies to: 32-32


59-59: toDomain에 createdAt 전달 변경사항 적합.

엔티티 생성시간을 도메인으로 전달하는 변경이 요구사항(생성순 정렬)과 일치합니다. Lazy 프록시에서 poll.getId() 접근도 안전합니다.

src/main/java/com/debatetimer/domain/poll/Vote.java (1)

22-24: 생성자 오버로드 구성 적절.

문자열 기반 생성자가 값 객체로 위임해 불변성/검증 흐름을 유지합니다.

src/main/resources/db/migration/V14__add_time_column_into_vote.sql (1)

1-5: 검증 완료 — created_at / modified_at 명칭 일치

BaseTimeEntity(src/main/java/com/debatetimer/entity/BaseTimeEntity.java)에 createdAt, modifiedAt 필드가 있으며(Spring 기본 네이밍 전략에 따라 DB에 created_at, modified_at으로 매핑) 마이그레이션 스크립트의 컬럼명과 일치합니다. 변경 불필요합니다.

Copy link
Contributor

@unifolio0 unifolio0 left a comment

Choose a reason for hiding this comment

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

/noti
@coli-geonwoo
질문 하나 남김요

private final VoteTeam team;
private final ParticipantName name;
private final ParticipateCode code;
private final LocalDateTime createAt;
Copy link
Contributor

Choose a reason for hiding this comment

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

단순 궁금증. 왜 createdAt이 아니죠?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✅ 반영 완료

Copy link
Member

@leegwichan leegwichan left a comment

Choose a reason for hiding this comment

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

/noti @coli-geonwoo
코멘트 확인하고 반영해주세요. 질문 2개 달았어요.

private final VoteTeam team;
private final ParticipantName name;
private final ParticipateCode code;
private final LocalDateTime createAt;
Copy link
Member

Choose a reason for hiding this comment

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

Image createdAt 아닙니까?

Comment on lines 27 to 32
this.voterNames = votes.stream()
.sorted(VOTE_COMPARATOR)
.map(Vote::getName)
.map(ParticipantName::getValue)
.toList();
}
Copy link
Member

Choose a reason for hiding this comment

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

Vote의 getName() 메서드 시그니처를 String 으로 return 하게 만들면 안됨?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반대로 필드를 List으로 도메인 중심으로 짜게 하고, DTO에서 String을 반환하게 만들었어요!

Copy link
Contributor Author

@coli-geonwoo coli-geonwoo left a comment

Choose a reason for hiding this comment

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

/noti

간단한 수정 2가지 했어요! 확인했다면 머지할게요!

필드명 수정 : create_at > created_at
VoteInfo 투표자 이름 필드타입 수정 : List > List


private VoteInfo countVotes(long pollId, List<VoteEntity> voteEntities) {
Map<VoteTeam, Long> teamCount = voteEntities.stream()
private VoteInfo resolveVoteInfo(long pollId, List<VoteEntity> voteEntities) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

1버능로 반영했습니다.

private final VoteTeam team;
private final ParticipantName name;
private final ParticipateCode code;
private final LocalDateTime createAt;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

✅ 반영 완료

Comment on lines 27 to 32
this.voterNames = votes.stream()
.sorted(VOTE_COMPARATOR)
.map(Vote::getName)
.map(ParticipantName::getValue)
.toList();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반대로 필드를 List으로 도메인 중심으로 짜게 하고, DTO에서 String을 반환하게 만들었어요!

Comment on lines +68 to 71
() -> assertThat(response.consCount()).isEqualTo(1L),
() -> assertThat(response.voterNames()).containsExactly(
voter1.getName(), voter2.getName(), voter3.getName())
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

[의견 🔈 ]

음... 순서 검증이 필요한 부분이에요. 지금 voteEntites에서는 정렬이 안되어 있지만 VoteInfo에서 정렬을 해주고 있고 Controller 응답에서 투표 생성 순으로 정렬되어 응답이 온다는 점을 검증해야 하기 때문에 containsExactly가 InAnyOrder보다 맞는 검증이라 생각했습니다.

Copy link
Member

@leegwichan leegwichan left a comment

Choose a reason for hiding this comment

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

/noti 고생했다 머지해라 콜리야

@coli-geonwoo coli-geonwoo merged commit 225b88c into develop Sep 30, 2025
6 checks passed
@coli-geonwoo coli-geonwoo deleted the feat/#251 branch September 30, 2025 06:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 추가

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[FEAT] 투표 참여자 목록을 API 에 추가

3 participants