Feat/vote 투표 관련 구현#103
Conversation
… for new VoteResultDetail
| @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())); |
There was a problem hiding this comment.
너무 모든 에러에 대해서 정의하고 있는 것 같아. 이제는 상위 비즈니스 예외를 정의하고 하나로 묶을 수 있어보여.
예를들면
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleVoteEnded(BusinessException e) {
return ResponseEntity.status(e.statusCode).body(new ErrorResponse(e.getErrorCode(), e.getMessage()));
}
There was a problem hiding this comment.
ErrorCode 인터페이스로 묶어서 처리할게
| 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; | ||
| } |
There was a problem hiding this comment.
기존에 사용자가 로그인해있어도 anonymous_id가 부여될 것 같아요.
| public Clock systemClock() { | ||
| return Clock.systemUTC(); | ||
| } |
|
|
||
| private String gender; | ||
|
|
||
| private LocalDate birthDate; |
There was a problem hiding this comment.
TODO로 수정해야한다고 남겨주면 좋을 것 같네, 성윤님 작업 범위와 겹치는 것 같아서!
| 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); |
There was a problem hiding this comment.
Record를 정의해서 Projection을 사용해도 괜찮아보여, object로하면 타입 안정성이 떨어져보여서
There was a problem hiding this comment.
다른 건 그렇게 해둔 거 같은데 여기는 좀 그렇게 바꿔둘게
| // TODO: currentViewerCount — Redis 도입 후 갱신 예정 | ||
| return new ImmersiveLiveResult(voteId, aRatio, bRatio, (int) total, 0); |
There was a problem hiding this comment.
오호 이건 어떤 내용이야? 좀 더 자세히 알 수 있을까
There was a problem hiding this comment.
실시간으로 화면 보고 있는 유저 수 세볼려고 해둿어
|
|
||
| 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; |
There was a problem hiding this comment.
내가 보기에는 VoteStatus.ONGOING, VoteStatus.ENDED 이게 컬럼으로 저장하지 않으면 좋을 것 같아. 예를들면 vote.getStatue() { if(this.isOngoing()) ONGOING else ENDED}로 내려도 괜찮아보여
There was a problem hiding this comment.
Status 칼럼 삭제하고 시간으로 계산해서 처리하는 과정으로 바꾸는걸로 해볼게
| 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; |
There was a problem hiding this comment.
내가 보기에는 너무 복잡한 것 같아. 결국에는 기존에걸 다 지우고 새로 저장해도 괜찮지 않을까
| if (totalGender == 0) return new GenderDistribution(0, 0); | ||
|
|
||
| long maleCount = genderCounts.stream() | ||
| .filter(gc -> "MALE".equals(gc.gender())).mapToLong(GenderCount::count).sum(); |
There was a problem hiding this comment.
요것도 나중에 수정될 것 같은데 TODO 남겨주면 좋을듯?
공통 Exception 처리의 일관성을 위해 `BusinessException` 및 `ErrorCode` 인터페이스를 생성하고, 기존 `InvalidDurationException`과 `ImageRequiredException`을 `BusinessException`으로 변경. 각 예외에 대해 `VoteErrorCode` enum 사용으로 에러 코드와 상태 코드를 관리. GlobalExceptionHandler에 `BusinessException` 통합 처리 로직 추가.
| // @Enumerated(EnumType.STRING) | ||
| // @Column(nullable = false) | ||
| // private VoteStatus status; |
There was a problem hiding this comment.
Vote 상태를 알기 위해서 뭔가 필요할거같긴한데 주석처리한 이유가 있을까요??
There was a problem hiding this comment.
@tlarbals824 님과 이야기 해본 결과 status를 굳이 필드에 넣지 말고 시간으로 계산해서 내려주기로 했습니다. 시간으로 계산하는 거랑 Status랑 틀린 결과가 나올 수도 있을 꺼 같아서요
- 기존 vote 예외 클래스들(VoteNotFoundException 등 7개)을 BusinessException 상속으로 변환 - VoteErrorCode에 누락된 에러 코드(VOTE_NOT_FOUND, VOTE_ENDED 등) 추가 - GlobalExceptionHandler에서 개별 vote 예외 핸들러 제거, BusinessException 단일 핸들러로 통합
- 서비스 레이어에서 직접 계산하던 VoteStatus를 vote.getStatus(clock) 위임으로 변경"
📌 관련 이슈
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 리팩터링에 따른 테스트 정리
💬 리뷰어에게