Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
33cd5d7
feat: add V3 vote schema migration
Junhyukkkk May 8, 2026
0ed1529
feat: add vote domain enums and value types
Junhyukkkk May 8, 2026
aaaf4f4
feat: implement Vote, VoteParticipation, VoteOption entities and repo…
Junhyukkkk May 8, 2026
7e477f2
feat: add vote domain exception classes
Junhyukkkk May 8, 2026
16d87c7
feat: replace VoteService with VoteQueryService and define VoteQueryU…
Junhyukkkk May 8, 2026
4b7b777
refactor: update chat bounded context to use VoteStatus from domain p…
Junhyukkkk May 8, 2026
04a5244
test: add vote domain tests and fix DataJpaTest configuration
Junhyukkkk May 8, 2026
712cfec
feat: add anonymous id cookie resolver and web mvc config
Junhyukkkk May 8, 2026
90d65f1
feat: add GuestFreeVote domain entity and service
Junhyukkkk May 8, 2026
a090bc0
test: add STEP 2 anonymous id and guest free vote tests
Junhyukkkk May 8, 2026
ea80f9d
feat: add VoteEmojiReaction entity and repository
Junhyukkkk May 8, 2026
c087f51
feat: define VoteCommandUseCase and VoteEmojiCommandUseCase inbound p…
Junhyukkkk May 8, 2026
a5de8a1
feat: implement VoteCommandService and VoteEmojiCommandService
Junhyukkkk May 8, 2026
10983fa
feat: add GlobalExceptionHandler for all vote domain exceptions
Junhyukkkk May 8, 2026
a7c204d
test: add VoteCommandService and VoteEmojiCommandService tests
Junhyukkkk May 8, 2026
a410c36
feat: STEP4 - Immersive 투표 커맨드/쿼리 서비스 구현
Junhyukkkk May 8, 2026
4f863f4
feat: 투표 종료 스케줄러 + 비동기 실행 설정 추가
Junhyukkkk May 8, 2026
0aa08cb
feat(vote): add VoteDetail/VoteResult query services and exception ha…
Junhyukkkk May 8, 2026
75cafd9
feat(vote): add VoteController and request/response DTOs
Junhyukkkk May 8, 2026
a8cf1d2
feat(vote): add ImmersiveVoteController and immersive-specific DTOs
Junhyukkkk May 8, 2026
1a3e8d3
feat(vote): add VoteResultController and result/share-link DTOs
Junhyukkkk May 8, 2026
6a8abdb
feat(vote): add VoteEmojiController and GuestFreeVoteController with …
Junhyukkkk May 8, 2026
6e3ea7c
test(vote): add WebMvcTest slice tests for all vote controllers
Junhyukkkk May 8, 2026
537d652
feat(vote): expand VoteResultDetail with Insight and AiInsightView
Junhyukkkk May 9, 2026
f101ded
feat(vote): implement VoteResultQueryService with MY_SELECTION/TOTAL/…
Junhyukkkk May 9, 2026
622d5a8
feat(vote): update VoteResultResponse to include insight and aiInsigh…
Junhyukkkk May 9, 2026
811870b
test(vote): add VoteResultQueryServiceTest and update controller test…
Junhyukkkk May 9, 2026
f3be66f
Extract 공통 BusinessException 및 ErrorCode 인터페이스 추가
tlarbals824 May 10, 2026
b356061
VoteEmoji를 활용하도록 DTO의 emojiSummary 타입 변경 및 변환 로직 삭제
tlarbals824 May 10, 2026
f8269f3
VoteEmojiReaction의 이모지 통계 쿼리 리턴 타입을 EmoijCount DTO로 수정 및 관련 클래스 추가
tlarbals824 May 10, 2026
2433c9d
투표 조회 WebSocket 연동 관련 Redis 사용 계획 주석 추가
tlarbals824 May 10, 2026
2ecd352
투표 상태(status) 관련 로직 제거 및 VoteEndedEvent 처리 핸들러 추가
tlarbals824 May 10, 2026
85d9395
VoteEmojiReaction 로직 리팩토링 및 Nullable 처리 개선
tlarbals824 May 10, 2026
c10b9bb
refactor: 예외 처리 구조를 BusinessException + VoteErrorCode 방식으로 통합
Junhyukkkk May 12, 2026
b43f871
git commit -m "refactor: Object[] 캐스팅을 EmoijCount Record Projection으로…
Junhyukkkk May 12, 2026
4940ee2
git commit -m "refactor: status 컬럼 제거 후 endAt 기준 동적 계산으로 통일
Junhyukkkk May 12, 2026
b4f7522
Merge branch 'develop' into feat/vote-domain-impl
Junhyukkkk May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/java/com/ject/vs/VsServerApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@ConfigurationPropertiesScan
@EnableScheduling
public class VsServerApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.ject.vs.chat.adapter.web.dto.*;
import com.ject.vs.chat.port.in.*;
import com.ject.vs.chat.port.in.dto.*;
import com.ject.vs.vote.port.in.dto.VoteStatus;
import com.ject.vs.vote.domain.VoteStatus;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/ject/vs/chat/adapter/web/ChatDocs.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import com.ject.vs.chat.adapter.web.dto.MessagePageResponse;
import com.ject.vs.chat.adapter.web.dto.MessageResponse;
import com.ject.vs.chat.adapter.web.dto.SendMessageRequest;
import com.ject.vs.vote.port.in.dto.VoteStatus;
import com.ject.vs.vote.domain.VoteStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.ject.vs.chat.adapter.web.dto;

import com.ject.vs.chat.port.in.dto.ChatRoomResult;
import com.ject.vs.vote.port.in.dto.VoteStatus;
import com.ject.vs.vote.domain.VoteStatus;
import io.swagger.v3.oas.annotations.media.Schema;

import java.time.Instant;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/ject/vs/chat/port/ChatService.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import com.ject.vs.chat.port.in.dto.*;
import com.ject.vs.vote.port.in.VoteParticipationQueryUseCase;
import com.ject.vs.vote.port.in.VoteQueryUseCase;
import com.ject.vs.vote.port.in.dto.VoteStatus;
import com.ject.vs.vote.domain.VoteStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.ject.vs.chat.port.in.dto.ChatRoomResult;
import com.ject.vs.chat.port.in.dto.GaugeResult;
import com.ject.vs.chat.port.in.dto.MessagePageResult;
import com.ject.vs.vote.port.in.dto.VoteStatus;
import com.ject.vs.vote.domain.VoteStatus;

import java.util.List;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.ject.vs.chat.port.in.dto;

import com.ject.vs.vote.port.in.dto.VoteStatus;
import com.ject.vs.vote.domain.VoteStatus;

import java.time.Instant;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.ject.vs.common.exception;

import lombok.Getter;

@Getter
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;

public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public String getErrorCode() {
return this.errorCode.getCode();
}

public String getErrorMessage() {
return this.errorCode.getMessage();
}

public Integer getStatusCode() {
return this.errorCode.getStatusCode();
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/ject/vs/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ject.vs.common.exception;

public interface ErrorCode {
String getCode();
String getMessage();
Integer getStatusCode();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ject.vs.common.exception;

import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(BusinessException e) {
return ResponseEntity.status(e.getStatusCode())
.body(new ErrorResponse(e.getErrorCode(), e.getErrorMessage()));
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.findFirst()
.orElse("입력값이 올바르지 않습니다");
return ResponseEntity.status(400).body(new ErrorResponse("INVALID_INPUT", message));
}

public record ErrorResponse(String code, String message) {
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/ject/vs/config/AnonymousId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ject.vs.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnonymousId {
}
61 changes: 61 additions & 0 deletions src/main/java/com/ject/vs/config/AnonymousIdResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.ject.vs.config;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import java.time.Duration;
import java.util.Arrays;
import java.util.UUID;

@Component
public class AnonymousIdResolver implements HandlerMethodArgumentResolver {

private static final String COOKIE_NAME = "anonymous_id";
private static final Duration MAX_AGE = Duration.ofDays(365);

@Override
public boolean supportsParameter(org.springframework.core.MethodParameter parameter) {
return parameter.hasParameterAnnotation(AnonymousId.class)
&& parameter.getParameterType().equals(String.class);
}

@Override
public String resolveArgument(org.springframework.core.MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
HttpServletRequest req = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse res = webRequest.getNativeResponse(HttpServletResponse.class);

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;
}
Comment on lines +38 to +51
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랑 같이 논의하고 처리할게요


private String extractCookie(HttpServletRequest req, String name) {
if (req == null || req.getCookies() == null) return null;
return Arrays.stream(req.getCookies())
.filter(c -> name.equals(c.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
}
}
35 changes: 35 additions & 0 deletions src/main/java/com/ject/vs/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.ject.vs.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig {

@Bean("voteCloseExecutor")
public Executor voteCloseExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("vote-close-");
executor.initialize();
return executor;
}

@Bean("aiInsightExecutor")
public Executor aiInsightExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("ai-insight-");
executor.initialize();
return executor;
Comment thread
Junhyukkkk marked this conversation as resolved.
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/ject/vs/config/ClockConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ject.vs.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Clock;

@Configuration
public class ClockConfig {

@Bean
public Clock systemClock() {
return Clock.systemUTC();
}
Comment on lines +12 to +14
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.

👍

}
20 changes: 20 additions & 0 deletions src/main/java/com/ject/vs/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ject.vs.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

private final AnonymousIdResolver anonymousIdResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(anonymousIdResolver);
}
}
6 changes: 5 additions & 1 deletion src/main/java/com/ject/vs/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import lombok.Getter;

import java.time.LocalDate;
import java.time.Year;

@Entity
@Getter
Expand All @@ -15,6 +14,11 @@ public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String sub;

private String gender;

private LocalDate birthDate;
Comment on lines +18 to +21
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.

좋습니다

private String email;
// 아직 유저에 대한 정보 확정 아님
private String nickname;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.ject.vs.vote.adapter.handler;

import com.ject.vs.vote.event.VoteEndedEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class VoteEventHandler {

/**
* VoteCloseScheduler 요기서 해당 이벤트가 발행됨\
*
* EventListener는 동기로 동작해 그래서 처리량을 위해 async를 사용합니다.
*
* 다만 다음과 같은 고민할 부분이 존재합니다.
*
* 1. 비동기로 처리할 때, 유실되면 어떻게 처리할거에요?
* 2. 스케줄러에 의해 트리거 되는데, 1분 주기보다 처리 시간이 길어지면 같은 처리가 동시에 진행될 수 있어 보여요 어떻게 처리할거에요?
* 3. 멱등하게 어떻게 처리할 수 있을까요?
*
*
* 트랜잭션 아웃박스 패턴을 보면 좋을듯!
* 분산락을 사용할 수 있어보임
*
* 아웃박스를 구현할 떄, 발행 기준으로 저장할래? 아니면 컨슈밍 기준으로 만들래?
*/
@EventListener(classes = VoteEndedEvent.class)
@Async("voteCloseExecutor")
public void handleVoteEnded(VoteEndedEvent event){
// log 추가하면 좋아보임
// ai 분석을 시작하면 좋아보임
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ject.vs.vote.adapter.web;

import com.ject.vs.config.AnonymousId;
import com.ject.vs.vote.adapter.web.dto.FreeVotesResponse;
import com.ject.vs.vote.port.GuestFreeVoteService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/me")
@RequiredArgsConstructor
public class GuestFreeVoteController {

private final GuestFreeVoteService guestFreeVoteService;

@GetMapping("/free-votes")
public FreeVotesResponse getFreeVotes(@AnonymousId String anonymousId) {
return new FreeVotesResponse(guestFreeVoteService.remaining(anonymousId));
}
}
Loading
Loading