Skip to content

Feat/vote 투표 관련 구현#103

Merged
Junhyukkkk merged 37 commits into
developfrom
feat/vote-domain-impl
May 12, 2026
Merged

Feat/vote 투표 관련 구현#103
Junhyukkkk merged 37 commits into
developfrom
feat/vote-domain-impl

Conversation

@Junhyukkkk
Copy link
Copy Markdown
Member

📌 관련 이슈

closes #

🔍 작업 내용

투표(Vote) 도메인 전체 구현 — 엔티티/레포지토리 정의부터 커맨드·쿼리 서비스, REST 컨트롤러, 스케줄러까지 Vote 바운디드 컨텍스트를 완성합니다.

📝 변경 사항

DB 마이그레이션 — V3__vote_schema.sql 추가 (vote, vote_option, vote_participation, vote_emoji_reaction, guest_free_vote 테이블)

도메인 엔티티 — Vote, VoteOption, VoteParticipation, VoteEmojiReaction, GuestFreeVote 및 관련 enum/value 타입 (VoteType, VoteStatus, VoteEmoji, AgeGroup, VoteDuration 등)

레포지토리 — 위 엔티티에 대응하는 Spring Data JPA 레포지토리 인터페이스 정의

인바운드 포트(UseCase) — VoteCommandUseCase, VoteQueryUseCase, VoteResultQueryUseCase, VoteEmojiCommandUseCase, ImmersiveVoteCommandUseCase, ImmersiveVoteQueryUseCase

서비스 구현 — VoteCommandService, VoteQueryService, VoteDetailQueryService, VoteResultQueryService, VoteEmojiCommandService, GuestFreeVoteService, ImmersiveVoteCommandService, ImmersiveVoteQueryService

컨트롤러 & DTO — VoteController, VoteResultController, VoteEmojiController, GuestFreeVoteController, ImmersiveVoteController 및 요청/응답 DTO 전체

스케줄러 — VoteCloseScheduler (투표 자동 종료), AsyncConfig로 비동기 실행 설정

인프라/설정 — AnonymousIdResolver (쿠키 기반 익명 ID), ClockConfig, WebConfig, GlobalExceptionHandler

테스트 — 도메인 단위 테스트, 서비스 단위 테스트(Mockito), WebMvcTest 슬라이스 테스트 전체 추가; 기존 VoteService → VoteQueryService 리팩터링에 따른 테스트 정리

💬 리뷰어에게

Junhyukkkk added 27 commits May 8, 2026 14:44
@Junhyukkkk Junhyukkkk self-assigned this May 10, 2026
@Junhyukkkk Junhyukkkk added ✨feature 구현, 개선 사항 관련 부분 👨🏻‍💻backend 백엔드 작업 labels May 10, 2026
Comment on lines +13 to +25
@ExceptionHandler(VoteNotFoundException.class)
public ResponseEntity<ErrorResponse> handleVoteNotFound(VoteNotFoundException e) {
return ResponseEntity.status(404).body(new ErrorResponse(e.getErrorCode(), e.getMessage()));
}

@ExceptionHandler(VoteEndedException.class)
public ResponseEntity<ErrorResponse> handleVoteEnded(VoteEndedException e) {
return ResponseEntity.status(403).body(new ErrorResponse(e.getErrorCode(), e.getMessage()));
}

@ExceptionHandler(VoteNotEndedException.class)
public ResponseEntity<ErrorResponse> handleVoteNotEnded(VoteNotEndedException e) {
return ResponseEntity.status(403).body(new ErrorResponse(e.getErrorCode(), e.getMessage()));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

너무 모든 에러에 대해서 정의하고 있는 것 같아. 이제는 상위 비즈니스 예외를 정의하고 하나로 묶을 수 있어보여.

예를들면

@ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleVoteEnded(BusinessException e) {
        return ResponseEntity.status(e.statusCode).body(new ErrorResponse(e.getErrorCode(), e.getMessage()));
    }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ErrorCode 인터페이스로 묶어서 처리할게

Comment on lines +38 to +51
String existing = extractCookie(req, COOKIE_NAME);
if (existing != null) return existing;

String newId = UUID.randomUUID().toString();
ResponseCookie cookie = ResponseCookie.from(COOKIE_NAME, newId)
.httpOnly(true)
.secure(true)
.sameSite("None")
.maxAge(MAX_AGE)
.path("/")
.build();
res.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return newId;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

기존에 사용자가 로그인해있어도 anonymous_id가 부여될 것 같아요.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

회원 도메인 구현 하고 있는 @KII1ua랑 같이 논의하고 처리할게요

Comment thread src/main/java/com/ject/vs/config/AsyncConfig.java
Comment on lines +12 to +14
public Clock systemClock() {
return Clock.systemUTC();
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👍

Comment on lines +16 to +19

private String gender;

private LocalDate birthDate;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

TODO로 수정해야한다고 남겨주면 좋을 것 같네, 성윤님 작업 범위와 겹치는 것 같아서!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

좋습니다

Optional<VoteEmojiReaction> findByVoteIdAndAnonymousId(Long voteId, String anonymousId);

@Query("SELECT r.emoji, COUNT(r) FROM VoteEmojiReaction r WHERE r.voteId = :voteId GROUP BY r.emoji")
java.util.List<Object[]> countByEmojiForVote(@Param("voteId") Long voteId);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Record를 정의해서 Projection을 사용해도 괜찮아보여, object로하면 타입 안정성이 떨어져보여서

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

다른 건 그렇게 해둔 거 같은데 여기는 좀 그렇게 바꿔둘게

Comment on lines +56 to +57
// TODO: currentViewerCount — Redis 도입 후 갱신 예정
return new ImmersiveLiveResult(voteId, aRatio, bRatio, (int) total, 0);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

오호 이건 어떤 내용이야? 좀 더 자세히 알 수 있을까

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

실시간으로 화면 보고 있는 유저 수 세볼려고 해둿어


public VoteDetailResult getDetail(Long voteId, Long userId, String anonymousId) {
Vote vote = voteRepository.findById(voteId).orElseThrow(VoteNotFoundException::new);
VoteStatus status = vote.isOngoing(clock) ? VoteStatus.ONGOING : VoteStatus.ENDED;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

내가 보기에는 VoteStatus.ONGOING, VoteStatus.ENDED 이게 컬럼으로 저장하지 않으면 좋을 것 같아. 예를들면 vote.getStatue() { if(this.isOngoing()) ONGOING else ENDED}로 내려도 괜찮아보여

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Status 칼럼 삭제하고 시간으로 계산해서 처리하는 과정으로 바꾸는걸로 해볼게

Comment on lines +44 to +58
private VoteEmoji applyReaction(Optional<VoteEmojiReaction> existing,
VoteEmoji emoji,
Runnable newReactionSaver) {
if (existing.isPresent()) {
VoteEmojiReaction reaction = existing.get();
if (emoji == null || reaction.getEmoji() == emoji) {
reactionRepository.delete(reaction);
return null;
}
reaction.changeEmoji(emoji);
return emoji;
}
if (emoji == null) return null;
newReactionSaver.run();
return emoji;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

내가 보기에는 너무 복잡한 것 같아. 결국에는 기존에걸 다 지우고 새로 저장해도 괜찮지 않을까

if (totalGender == 0) return new GenderDistribution(0, 0);

long maleCount = genderCounts.stream()
.filter(gc -> "MALE".equals(gc.gender())).mapToLong(GenderCount::count).sum();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

요것도 나중에 수정될 것 같은데 TODO 남겨주면 좋을듯?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

굿굿

공통 Exception 처리의 일관성을 위해 `BusinessException` 및 `ErrorCode` 인터페이스를 생성하고, 기존 `InvalidDurationException`과 `ImageRequiredException`을 `BusinessException`으로 변경. 각 예외에 대해 `VoteErrorCode` enum 사용으로 에러 코드와 상태 코드를 관리. GlobalExceptionHandler에 `BusinessException` 통합 처리 로직 추가.
Comment on lines +35 to +37
// @Enumerated(EnumType.STRING)
// @Column(nullable = false)
// private VoteStatus status;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Vote 상태를 알기 위해서 뭔가 필요할거같긴한데 주석처리한 이유가 있을까요??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@tlarbals824 님과 이야기 해본 결과 status를 굳이 필드에 넣지 말고 시간으로 계산해서 내려주기로 했습니다. 시간으로 계산하는 거랑 Status랑 틀린 결과가 나올 수도 있을 꺼 같아서요

Junhyukkkk and others added 4 commits May 12, 2026 11:50
- 기존 vote 예외 클래스들(VoteNotFoundException 등 7개)을 BusinessException 상속으로 변환
- VoteErrorCode에 누락된 에러 코드(VOTE_NOT_FOUND, VOTE_ENDED 등) 추가
- GlobalExceptionHandler에서 개별 vote 예외 핸들러 제거, BusinessException 단일 핸들러로 통합
- 서비스 레이어에서 직접 계산하던 VoteStatus를 vote.getStatus(clock) 위임으로 변경"
@Junhyukkkk Junhyukkkk merged commit d9890cb into develop May 12, 2026
@Junhyukkkk Junhyukkkk deleted the feat/vote-domain-impl branch May 12, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

👨🏻‍💻backend 백엔드 작업 ✨feature 구현, 개선 사항 관련 부분 feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants