diff --git a/src/main/java/hyundai/softeer/orange/comment/controller/CommentController.java b/src/main/java/hyundai/softeer/orange/comment/controller/CommentController.java index 73520335..9eb848c8 100644 --- a/src/main/java/hyundai/softeer/orange/comment/controller/CommentController.java +++ b/src/main/java/hyundai/softeer/orange/comment/controller/CommentController.java @@ -5,6 +5,10 @@ import hyundai.softeer.orange.comment.service.ApiService; import hyundai.softeer.orange.comment.service.CommentService; import hyundai.softeer.orange.common.ErrorResponse; +import hyundai.softeer.orange.core.auth.Auth; +import hyundai.softeer.orange.core.auth.AuthRole; +import hyundai.softeer.orange.eventuser.component.EventUserAnnotation; +import hyundai.softeer.orange.eventuser.dto.EventUserInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -36,6 +40,7 @@ public ResponseEntity getComments() { return ResponseEntity.ok(commentService.getComments()); } + @Auth(AuthRole.event_user) @Tag(name = "Comment") @PostMapping @Operation(summary = "기대평 등록", description = "유저가 신규 기대평을 등록한다.", responses = { @@ -43,11 +48,26 @@ public ResponseEntity getComments() { content = @Content(schema = @Schema(implementation = Boolean.class))), @ApiResponse(responseCode = "400", description = "기대평 등록 실패, 지나치게 부정적인 표현으로 간주될 때", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "해당 정보를 갖는 유저나 이벤트가 존재하지 않을 때", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "409", description = "하루에 여러 번의 기대평을 작성하려 할 때", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity createComment(@RequestBody @Valid CreateCommentDto dto) { + public ResponseEntity createComment(@EventUserAnnotation EventUserInfo userInfo, @RequestBody @Valid CreateCommentDto dto) { boolean isPositive = apiService.analyzeComment(dto.getContent()); - return ResponseEntity.ok(commentService.createComment(dto, isPositive)); + return ResponseEntity.ok(commentService.createComment(userInfo.getUserId(), dto, isPositive)); + } + + @Auth(AuthRole.event_user) + @Tag(name = "Comment") + @GetMapping("/info") + @Operation(summary = "기대평 등록 가능 여부 조회", description = "오늘 기대평 등록 가능 여부를 조회한다.", responses = { + @ApiResponse(responseCode = "200", description = "기대평 작성 가능 여부를 true/false로 반환한다.", + content = @Content(schema = @Schema(implementation = Boolean.class))), + @ApiResponse(responseCode = "404", description = "해당 정보를 갖는 유저가 존재하지 않을 때", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity isCommentable(@EventUserAnnotation EventUserInfo userInfo) { + return ResponseEntity.ok(commentService.isCommentable(userInfo.getUserId())); } } diff --git a/src/main/java/hyundai/softeer/orange/comment/dto/CreateCommentDto.java b/src/main/java/hyundai/softeer/orange/comment/dto/CreateCommentDto.java index b8a26337..19642ee1 100644 --- a/src/main/java/hyundai/softeer/orange/comment/dto/CreateCommentDto.java +++ b/src/main/java/hyundai/softeer/orange/comment/dto/CreateCommentDto.java @@ -12,17 +12,13 @@ @NoArgsConstructor public class CreateCommentDto { - @NotNull(message = MessageUtil.BAD_INPUT) - private Long eventUserId; - @NotNull(message = MessageUtil.BAD_INPUT) private Long eventFrameId; @Size(min = 1, max = 100, message = MessageUtil.OUT_OF_SIZE) private String content; - public CreateCommentDto(Long eventUserId, Long eventFrameId, String content) { - this.eventUserId = eventUserId; + public CreateCommentDto(Long eventFrameId, String content) { this.eventFrameId = eventFrameId; this.content = content; } diff --git a/src/main/java/hyundai/softeer/orange/comment/service/CommentService.java b/src/main/java/hyundai/softeer/orange/comment/service/CommentService.java index 340b3916..e4591c7c 100644 --- a/src/main/java/hyundai/softeer/orange/comment/service/CommentService.java +++ b/src/main/java/hyundai/softeer/orange/comment/service/CommentService.java @@ -42,22 +42,31 @@ public ResponseCommentsDto getComments() { // 신규 기대평을 등록한다. @Transactional - public Boolean createComment(CreateCommentDto dto, Boolean isPositive) { + public Boolean createComment(String userId, CreateCommentDto dto, Boolean isPositive) { + EventUser eventUser = eventUserRepository.findByUserId(userId) + .orElseThrow(() -> new CommentException(ErrorCode.EVENT_USER_NOT_FOUND)); + EventFrame eventFrame = eventFrameRepository.findById(dto.getEventFrameId()) + .orElseThrow(() -> new CommentException(ErrorCode.EVENT_FRAME_NOT_FOUND)); + // 하루에 여러 번의 기대평을 작성하려 할 때 예외처리 - if(commentRepository.existsByCreatedDateAndEventUser(dto.getEventUserId())) { + if(commentRepository.existsByCreatedDateAndEventUser(eventUser.getId())) { throw new CommentException(ErrorCode.COMMENT_ALREADY_EXISTS); } - EventFrame eventFrame = eventFrameRepository.findById(dto.getEventFrameId()) - .orElseThrow(() -> new CommentException(ErrorCode.EVENT_FRAME_NOT_FOUND)); - EventUser eventUser = eventUserRepository.findById(dto.getEventUserId()) - .orElseThrow(() -> new CommentException(ErrorCode.EVENT_USER_NOT_FOUND)); // TODO: 점수정책와 연계하여 기대평 등록 시 점수를 부여 추가해야함 Comment comment = Comment.of(dto.getContent(), eventFrame, eventUser, isPositive); commentRepository.save(comment); return true; } + // 오늘 이 유저가 기대평을 작성할 수 있는지 여부를 조회한다. + @Transactional(readOnly = true) + public Boolean isCommentable(String userId) { + EventUser eventUser = eventUserRepository.findByUserId(userId) + .orElseThrow(() -> new CommentException(ErrorCode.EVENT_USER_NOT_FOUND)); + return !commentRepository.existsByCreatedDateAndEventUser(eventUser.getId()); + } + // 기대평을 삭제한다. 이 동작을 실행하는 주체가 어드민임이 반드시 검증되어야 한다. @Transactional public Long deleteComment(Long commentId) { diff --git a/src/main/java/hyundai/softeer/orange/common/ErrorCode.java b/src/main/java/hyundai/softeer/orange/common/ErrorCode.java index 0664aa04..cc9967aa 100644 --- a/src/main/java/hyundai/softeer/orange/common/ErrorCode.java +++ b/src/main/java/hyundai/softeer/orange/common/ErrorCode.java @@ -17,12 +17,12 @@ public enum ErrorCode { INVALID_EVENT_TIME(HttpStatus.BAD_REQUEST, false, "이벤트 시간이 아닙니다."), INVALID_EVENT_TYPE(HttpStatus.BAD_REQUEST, false, "이벤트 타입이 지원되지 않습니다."), - // 401 Unauthorized UNAUTHORIZED(HttpStatus.UNAUTHORIZED, false, "인증되지 않은 사용자입니다."), AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, false, "아이디 또는 비밀번호가 일치하지 않습니다"), INVALID_AUTH_CODE(HttpStatus.UNAUTHORIZED, false, "인증번호가 일치하지 않습니다."), SESSION_EXPIRED(HttpStatus.UNAUTHORIZED, false, "세션이 만료되었습니다."), + AUTH_CODE_EXPIRED(HttpStatus.UNAUTHORIZED, false, "인증번호가 만료되었거나 존재하지 않습니다."), // 403 Forbidden FORBIDDEN(HttpStatus.FORBIDDEN, false, "권한이 없습니다."), @@ -45,6 +45,7 @@ public enum ErrorCode { COMMENT_ALREADY_EXISTS(HttpStatus.CONFLICT, false, "이미 등록된 기대평입니다."), ADMIN_USER_ALREADY_EXISTS(HttpStatus.CONFLICT, false, "이미 존재하는 관리자입니다."), ALREADY_WINNER(HttpStatus.CONFLICT, false, "이미 당첨된 사용자입니다."), + ALREADY_PARTICIPATED(HttpStatus.CONFLICT, false, "이미 참여한 사용자입니다."), PHONE_NUMBER_ALREADY_EXISTS(HttpStatus.CONFLICT, false, "이미 존재하는 전화번호입니다."), // 500 Internal Server Error diff --git a/src/main/java/hyundai/softeer/orange/common/util/ConstantUtil.java b/src/main/java/hyundai/softeer/orange/common/util/ConstantUtil.java index ad389083..dcd67078 100644 --- a/src/main/java/hyundai/softeer/orange/common/util/ConstantUtil.java +++ b/src/main/java/hyundai/softeer/orange/common/util/ConstantUtil.java @@ -12,7 +12,10 @@ public class ConstantUtil { public static final String PHONE_NUMBER_REGEX = "010\\d{8}"; // 010 + 8자리 숫자 public static final String AUTH_CODE_REGEX = "\\d{6}"; // 6자리 숫자 public static final String AUTH_CODE_CREATE_REGEX = "%06d"; - public static final String CLAIMS_KEY = "user"; + public static final String CLAIMS_USER_KEY = "userId"; + public static final String CLAIMS_ROLE_KEY = "role"; + public static final String CLAIMS_USER_NAME_KEY = "userName"; + public static final String JWT_USER_KEY = "eventUser"; public static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; public static final String LOCATION = "Location"; @@ -20,4 +23,10 @@ public class ConstantUtil { public static final int COMMENTS_SIZE = 20; public static final int SCHEDULED_TIME = 1000 * 60 * 60 * 2; public static final int SHORT_URL_LENGTH = 10; + public static final int USER_ID_LENGTH = 8; + public static final int AUTH_CODE_LENGTH = 6; + public static final int JWT_LIFESPAN = 1; // 1시간 + public static final int AUTH_CODE_EXPIRE_TIME = 5; // 5분 + public static final int FCFS_AVAILABLE_HOUR = 7; + public static final int FCFS_COUNTDOWN_HOUR = 3; } diff --git a/src/main/java/hyundai/softeer/orange/config/WebConfig.java b/src/main/java/hyundai/softeer/orange/config/WebConfig.java index 742d6d5d..d5a0f261 100644 --- a/src/main/java/hyundai/softeer/orange/config/WebConfig.java +++ b/src/main/java/hyundai/softeer/orange/config/WebConfig.java @@ -2,6 +2,8 @@ import hyundai.softeer.orange.admin.component.AdminArgumentResolver; import hyundai.softeer.orange.core.auth.AuthInterceptor; +import hyundai.softeer.orange.eventuser.component.EventUserArgumentResolver; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -10,13 +12,13 @@ import java.util.List; +@RequiredArgsConstructor @Configuration public class WebConfig implements WebMvcConfigurer { - @Autowired - private AuthInterceptor authInterceptor; - @Autowired - private AdminArgumentResolver adminArgumentResolver; + private final AuthInterceptor authInterceptor; + private final AdminArgumentResolver adminArgumentResolver; + private final EventUserArgumentResolver eventUserArgumentResolver; @Override public void addInterceptors(InterceptorRegistry registry) { @@ -28,5 +30,6 @@ public void addInterceptors(InterceptorRegistry registry) { @Override public void addArgumentResolvers(List resolvers) { resolvers.add(adminArgumentResolver); + resolvers.add(eventUserArgumentResolver); } } diff --git a/src/main/java/hyundai/softeer/orange/core/auth/AuthInterceptor.java b/src/main/java/hyundai/softeer/orange/core/auth/AuthInterceptor.java index c4cd2abb..fa87b195 100644 --- a/src/main/java/hyundai/softeer/orange/core/auth/AuthInterceptor.java +++ b/src/main/java/hyundai/softeer/orange/core/auth/AuthInterceptor.java @@ -5,12 +5,14 @@ import hyundai.softeer.orange.core.jwt.JWTManager; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import java.util.*; +@Slf4j @Component // 문제 있으면 변경 public class AuthInterceptor implements HandlerInterceptor { private final JWTManager jwtManager; @@ -21,6 +23,11 @@ public AuthInterceptor(JWTManager jwtManager) { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 유효하지 않은 요청은 정적 리소스로 간주하여 ResourceHttpRequestHandler가 대신 처리하기에, HandlerMethod가 아닌 경우는 무시 + if (!(handler instanceof HandlerMethod)) { + return true; + } + HandlerMethod handlerMethod = (HandlerMethod) handler; Auth classAnnotation = handlerMethod.getClass().getAnnotation(Auth.class); diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/controller/FcfsController.java b/src/main/java/hyundai/softeer/orange/event/fcfs/controller/FcfsController.java index e15b9e75..6163f51b 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/controller/FcfsController.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/controller/FcfsController.java @@ -1,9 +1,15 @@ package hyundai.softeer.orange.event.fcfs.controller; import hyundai.softeer.orange.common.ErrorResponse; +import hyundai.softeer.orange.core.auth.Auth; +import hyundai.softeer.orange.core.auth.AuthRole; +import hyundai.softeer.orange.event.fcfs.dto.ResponseFcfsInfoDto; import hyundai.softeer.orange.event.fcfs.dto.ResponseFcfsResultDto; import hyundai.softeer.orange.event.fcfs.service.FcfsAnswerService; +import hyundai.softeer.orange.event.fcfs.service.FcfsManageService; import hyundai.softeer.orange.event.fcfs.service.FcfsService; +import hyundai.softeer.orange.eventuser.component.EventUserAnnotation; +import hyundai.softeer.orange.eventuser.dto.EventUserInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -21,7 +27,9 @@ public class FcfsController { private final FcfsService fcfsService; private final FcfsAnswerService fcfsAnswerService; + private final FcfsManageService fcfsManageService; + @Auth(AuthRole.event_user) @Tag(name = "fcfs") @PostMapping @Operation(summary = "선착순 이벤트 참여", description = "선착순 이벤트에 참여한 결과(boolean)를 반환한다.", responses = { @@ -30,9 +38,34 @@ public class FcfsController { @ApiResponse(responseCode = "400", description = "선착순 이벤트 시간이 아니거나, 요청 형식이 잘못된 경우", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity participate(@RequestParam Long eventSequence, @RequestParam String userId, @RequestParam String eventAnswer) { + public ResponseEntity participate(@EventUserAnnotation EventUserInfo userInfo, @RequestParam Long eventSequence, @RequestParam String eventAnswer) { boolean answerResult = fcfsAnswerService.judgeAnswer(eventSequence, eventAnswer); - boolean isWin = answerResult && fcfsService.participate(eventSequence, userId); + boolean isWin = answerResult && fcfsService.participate(eventSequence, userInfo.getUserId()); return ResponseEntity.ok(new ResponseFcfsResultDto(answerResult, isWin)); } + + @Tag(name = "fcfs") + @GetMapping("/{eventSequence}/info") + @Operation(summary = "특정 선착순 이벤트의 정보 조회", description = "특정 선착순 이벤트에 대한 정보(서버 기준 시각, 이벤트의 상태)를 반환한다.", responses = { + @ApiResponse(responseCode = "200", description = "선착순 이벤트에 대한 상태 정보", + content = @Content(schema = @Schema(implementation = ResponseFcfsInfoDto.class))), + @ApiResponse(responseCode = "404", description = "선착순 이벤트를 찾을 수 없는 경우", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity getFcfsInfo(@PathVariable Long eventSequence) { + return ResponseEntity.ok(fcfsManageService.getFcfsInfo(eventSequence)); + } + + @Auth(AuthRole.event_user) + @Tag(name = "fcfs") + @GetMapping("/participated") + @Operation(summary = "선착순 이벤트 참여 여부 조회", description = "정답을 맞혀서 선착순 이벤트에 참여했는지 여부를 조회한다. (당첨은 별도)", responses = { + @ApiResponse(responseCode = "200", description = "선착순 이벤트의 정답을 맞혀서 참여했는지에 대한 결과", + content = @Content(schema = @Schema(implementation = ResponseFcfsResultDto.class))), + @ApiResponse(responseCode = "404", description = "선착순 이벤트를 찾을 수 없는 경우", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity isParticipated(@EventUserAnnotation EventUserInfo userInfo, @RequestParam Long eventSequence) { + return ResponseEntity.ok(fcfsManageService.isParticipated(eventSequence, userInfo.getUserId())); + } } diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/dto/ResponseFcfsInfoDto.java b/src/main/java/hyundai/softeer/orange/event/fcfs/dto/ResponseFcfsInfoDto.java new file mode 100644 index 00000000..3698af08 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/dto/ResponseFcfsInfoDto.java @@ -0,0 +1,17 @@ +package hyundai.softeer.orange.event.fcfs.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class ResponseFcfsInfoDto { + + private LocalDateTime nowDateTime; + + private String eventStatus; +} diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsManageService.java b/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsManageService.java index 09686a20..c0e7a45d 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsManageService.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsManageService.java @@ -1,6 +1,8 @@ package hyundai.softeer.orange.event.fcfs.service; import hyundai.softeer.orange.common.ErrorCode; +import hyundai.softeer.orange.common.util.ConstantUtil; +import hyundai.softeer.orange.event.fcfs.dto.ResponseFcfsInfoDto; import hyundai.softeer.orange.event.fcfs.dto.ResponseFcfsWinnerDto; import hyundai.softeer.orange.event.fcfs.entity.FcfsEvent; import hyundai.softeer.orange.event.fcfs.entity.FcfsEventWinningInfo; @@ -69,6 +71,38 @@ public void registerWinners() { } } + // 특정 선착순 이벤트의 정보 조회 + public ResponseFcfsInfoDto getFcfsInfo(Long eventSequence) { + String startTime = stringRedisTemplate.opsForValue().get(FcfsUtil.startTimeFormatting(eventSequence.toString())); + // 선착순 이벤트가 존재하지 않는 경우 + if (startTime == null) { + throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND); + } + + LocalDateTime nowDateTime = LocalDateTime.now(); + LocalDateTime eventStartTime = LocalDateTime.parse(startTime); + + // 서버시간 < 이벤트시작시간 < 서버시간+3시간 -> countdown + // 이벤트시작시간 < 서버시간 < 이벤트시작시간+7시간 -> progress + // 그 외 -> waiting + if(nowDateTime.isBefore(eventStartTime) && nowDateTime.plusHours(ConstantUtil.FCFS_COUNTDOWN_HOUR).isAfter(eventStartTime)) { + return new ResponseFcfsInfoDto(nowDateTime, "countdown"); + } else if(eventStartTime.isBefore(nowDateTime) && eventStartTime.plusHours(ConstantUtil.FCFS_AVAILABLE_HOUR).isAfter(nowDateTime)) { + return new ResponseFcfsInfoDto(nowDateTime, "progress"); + } else { + return new ResponseFcfsInfoDto(nowDateTime, "waiting"); + } + } + + // 특정 유저가 선착순 이벤트의 참여자인지 조회 (정답을 맞힌 경우 참여자로 간주) + @Transactional(readOnly = true) + public Boolean isParticipated(Long eventSequence, String userId) { + if(!fcfsEventRepository.existsById(eventSequence)) { + throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND); + } + return Boolean.TRUE.equals(stringRedisTemplate.opsForSet().isMember(FcfsUtil.participantFormatting(eventSequence.toString()), userId)); + } + // 특정 선착순 이벤트의 당첨자 조회 - 어드민에서 사용 @Transactional(readOnly = true) public List getFcfsWinnersInfo(Long eventSequence) { @@ -94,6 +128,8 @@ private void prepareEventInfo(FcfsEvent event) { public void deleteEventInfo(String eventId) { stringRedisTemplate.delete(FcfsUtil.startTimeFormatting(eventId)); + stringRedisTemplate.delete(FcfsUtil.answerFormatting(eventId)); + stringRedisTemplate.delete(FcfsUtil.participantFormatting(eventId)); stringRedisTemplate.delete(FcfsUtil.winnerFormatting(eventId)); numberRedisTemplate.delete(FcfsUtil.keyFormatting(eventId)); booleanRedisTemplate.delete(FcfsUtil.endFlagFormatting(eventId)); diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLockFcfsService.java b/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLockFcfsService.java index cacf8866..0ce62c4d 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLockFcfsService.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLockFcfsService.java @@ -31,11 +31,12 @@ public boolean participate(Long eventSequence, String userId) { String fcfsId = FcfsUtil.keyFormatting(eventSequence.toString()); // 불필요한 Lock 접근을 막기 위한 종료 flag 확인 if (isEventEnded(fcfsId)) { + stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(eventSequence.toString()), userId); return false; } // 이미 당첨된 사용자인지 확인 - if(stringRedisTemplate.opsForZSet().rank(FcfsUtil.winnerFormatting(eventSequence.toString()), userId) != null){ + if(stringRedisTemplate.opsForSet().isMember(FcfsUtil.winnerFormatting(eventSequence.toString()), userId) != null) { throw new FcfsEventException(ErrorCode.ALREADY_WINNER); } @@ -65,6 +66,7 @@ public boolean participate(Long eventSequence, String userId) { numberRedisTemplate.opsForValue().decrement(fcfsId); stringRedisTemplate.opsForZSet().add(FcfsUtil.winnerFormatting(eventSequence.toString()), userId, System.currentTimeMillis()); + stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(eventSequence.toString()), userId); log.info("{} - 이벤트 참여 성공, 잔여 쿠폰: {}", userId, availableCoupons(fcfsId)); return true; } catch (InterruptedException e) { diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLuaFcfsService.java b/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLuaFcfsService.java index b0fdf187..e6872658 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLuaFcfsService.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLuaFcfsService.java @@ -26,12 +26,13 @@ public class RedisLuaFcfsService implements FcfsService { public boolean participate(Long eventSequence, String userId) { String fcfsId = FcfsUtil.keyFormatting(eventSequence.toString()); if (isEventEnded(fcfsId)) { + stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(eventSequence.toString()), userId); return false; } - // 이미 당첨된 사용자인지 확인 - if(stringRedisTemplate.opsForZSet().rank(FcfsUtil.winnerFormatting(eventSequence.toString()), userId) != null){ - throw new FcfsEventException(ErrorCode.ALREADY_WINNER); + // 이미 선착순 이벤트에 참여한 사용자인지 확인 (참여했다면 이미 성공, 실패 결론은 고정되었기 때문) + if(isParticipated(eventSequence, userId)) { + throw new FcfsEventException(ErrorCode.ALREADY_PARTICIPATED); } // 잘못된 이벤트 참여 시간 @@ -61,15 +62,21 @@ public boolean participate(Long eventSequence, String userId) { ); if(result == null || result <= 0) { + stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(eventSequence.toString()), userId); endEvent(fcfsId); // 이벤트 종료 플래그 설정 return false; } stringRedisTemplate.opsForZSet().add(FcfsUtil.winnerFormatting(eventSequence.toString()), userId, System.currentTimeMillis()); + stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(eventSequence.toString()), userId); log.info("Event Sequence: {}, User ID: {}, Timestamp: {}", eventSequence, userId, timestamp); return true; } + public boolean isParticipated(Long eventSequence, String userId) { + return Boolean.TRUE.equals(stringRedisTemplate.opsForSet().isMember(FcfsUtil.participantFormatting(eventSequence.toString()), userId)); + } + private boolean isEventEnded(String fcfsId) { return booleanRedisTemplate.opsForValue().get(FcfsUtil.endFlagFormatting(fcfsId)) != null; } diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisSetFcfsService.java b/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisSetFcfsService.java index f5d8ed75..9483b21b 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisSetFcfsService.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisSetFcfsService.java @@ -27,11 +27,12 @@ public boolean participate(Long eventSequence, String userId) { // 인원 마감 여부 확인 if (isEventEnd(eventSequence)) { + stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(eventSequence.toString()), userId); return false; } - // 대기열에 등록 stringRedisTemplate.opsForZSet().add(FcfsUtil.winnerFormatting(eventSequence.toString()), userId, System.currentTimeMillis()); + stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(eventSequence.toString()), userId); log.info("{} 선착순 이벤트 참여 성공", userId); return true; } diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/util/FcfsUtil.java b/src/main/java/hyundai/softeer/orange/event/fcfs/util/FcfsUtil.java index 3d1bc43f..72a87a31 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/util/FcfsUtil.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/util/FcfsUtil.java @@ -6,22 +6,32 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class FcfsUtil { + // 선착순 이벤트 tag public static String keyFormatting(String fcfsId) { return fcfsId + ":fcfs"; } + // 선착순 이벤트 시작 시각 tag public static String startTimeFormatting(String fcfsId) { return fcfsId + "_start"; } + // 선착순 이벤트 마감 여부 tag public static String endFlagFormatting(String fcfsId) { return fcfsId + "_end"; } + // 선착순 이벤트 당첨자 tag public static String winnerFormatting(String fcfsId) { return fcfsId + "_winner"; } + // 선착순 이벤트 참여자 tag + public static String participantFormatting(String fcfsId) { + return fcfsId + "_participant"; + } + + // 선착순 이벤트 정답 tag public static String answerFormatting(String key) { return key + ":answer"; } diff --git a/src/main/java/hyundai/softeer/orange/eventuser/component/EventUserAnnotation.java b/src/main/java/hyundai/softeer/orange/eventuser/component/EventUserAnnotation.java new file mode 100644 index 00000000..8cb34f9c --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/eventuser/component/EventUserAnnotation.java @@ -0,0 +1,11 @@ +package hyundai.softeer.orange.eventuser.component; + +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 EventUserAnnotation { +} diff --git a/src/main/java/hyundai/softeer/orange/eventuser/component/EventUserArgumentResolver.java b/src/main/java/hyundai/softeer/orange/eventuser/component/EventUserArgumentResolver.java new file mode 100644 index 00000000..deef8857 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/eventuser/component/EventUserArgumentResolver.java @@ -0,0 +1,43 @@ +package hyundai.softeer.orange.eventuser.component; + +import hyundai.softeer.orange.common.ErrorCode; +import hyundai.softeer.orange.common.util.ConstantUtil; +import hyundai.softeer.orange.core.jwt.JWTConst; +import hyundai.softeer.orange.eventuser.dto.EventUserInfo; +import hyundai.softeer.orange.eventuser.exception.EventUserException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +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; + +@Slf4j +@Component +public class EventUserArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasAnnotation = parameter.hasParameterAnnotation(EventUserAnnotation.class); + boolean hasType = EventUserInfo.class.isAssignableFrom(parameter.getParameterType()); + return hasAnnotation && hasType; + } + + @Override + public EventUserInfo resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + try { + Jws claims = (Jws) request.getAttribute(JWTConst.Token); + String userId = claims.getPayload().get(ConstantUtil.CLAIMS_USER_KEY).toString(); + String role = claims.getPayload().get(ConstantUtil.CLAIMS_ROLE_KEY).toString(); + return new EventUserInfo(userId, role); + } catch (Exception e) { + throw new EventUserException(ErrorCode.UNAUTHORIZED); + } + } +} diff --git a/src/main/java/hyundai/softeer/orange/eventuser/dto/EventUserInfo.java b/src/main/java/hyundai/softeer/orange/eventuser/dto/EventUserInfo.java new file mode 100644 index 00000000..10036b1d --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/eventuser/dto/EventUserInfo.java @@ -0,0 +1,16 @@ +package hyundai.softeer.orange.eventuser.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class EventUserInfo implements Serializable { + + private String userId; + private String role; +} diff --git a/src/main/java/hyundai/softeer/orange/eventuser/service/CoolSmsService.java b/src/main/java/hyundai/softeer/orange/eventuser/service/CoolSmsService.java index 1b0e6cc7..f54c635f 100644 --- a/src/main/java/hyundai/softeer/orange/eventuser/service/CoolSmsService.java +++ b/src/main/java/hyundai/softeer/orange/eventuser/service/CoolSmsService.java @@ -14,8 +14,10 @@ import net.nurigo.sdk.message.service.DefaultMessageService; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Random; +import java.util.concurrent.TimeUnit; @Slf4j @Service @@ -34,6 +36,7 @@ public CoolSmsService(CoolSmsApiConfig coolSmsApiConfig, StringRedisTemplate str } @Override + @Transactional(readOnly = true) public void sendSms(RequestUserDto dto) { if(eventUserRepository.existsByPhoneNumber(dto.getPhoneNumber())) { throw new EventUserException(ErrorCode.PHONE_NUMBER_ALREADY_EXISTS); @@ -47,11 +50,18 @@ public void sendSms(RequestUserDto dto) { SingleMessageSentResponse response = defaultMessageService.sendOne(new SingleMessageSendingRequest(message)); log.info("{}에게 SMS 전송 완료: {}", dto.getPhoneNumber(), response); - stringRedisTemplate.opsForValue().set(dto.getPhoneNumber(), authCode); + + // 5분 동안 인증번호 유효 + stringRedisTemplate.opsForValue().set(dto.getPhoneNumber(), authCode, ConstantUtil.AUTH_CODE_EXPIRE_TIME, TimeUnit.MINUTES); } // 6자리 난수 인증번호 생성 private String generateAuthCode() { - return String.format(ConstantUtil.AUTH_CODE_CREATE_REGEX, new Random().nextInt(1000000)); + StringBuilder authCode = new StringBuilder(); + Random random = new Random(); + for(int i=0; i new EventUserException(ErrorCode.EVENT_USER_NOT_FOUND)); - Map claims = new HashMap<>(Map.of(ConstantUtil.CLAIMS_KEY, eventUser.getUserId())); - String token = jwtManager.generateToken(eventUser.getUserName(), claims, 1); - return new TokenDto(token); + return generateToken(eventUser); } /** @@ -54,10 +52,13 @@ public TokenDto login(RequestUserDto dto) { public TokenDto checkAuthCode(RequestAuthCodeDto dto, Long eventFrameId) { // Redis에서 인증번호 조회 String authCode = stringRedisTemplate.opsForValue().get(dto.getPhoneNumber()); + + // 해당 전화번호로 발송된 인증번호가 없거나 만료된 경우 if(authCode == null) { - throw new EventUserException(ErrorCode.BAD_REQUEST); + throw new EventUserException(ErrorCode.AUTH_CODE_EXPIRED); } + // 인증번호가 틀린 경우 if (!authCode.equals(dto.getAuthCode())) { throw new EventUserException(ErrorCode.INVALID_AUTH_CODE); } @@ -68,7 +69,7 @@ public TokenDto checkAuthCode(RequestAuthCodeDto dto, Long eventFrameId) { // DB에 유저 데이터 저장 EventFrame eventFrame = eventFrameRepository.findById(eventFrameId) .orElseThrow(() -> new EventUserException(ErrorCode.EVENT_FRAME_NOT_FOUND)); - String userId = UUID.randomUUID().toString().substring(0, 8); + String userId = UUID.randomUUID().toString().substring(0, ConstantUtil.USER_ID_LENGTH); EventUser eventUser = EventUser.of(dto.getName(), dto.getPhoneNumber(), eventFrame, userId); eventUserRepository.save(eventUser); return generateToken(eventUser); @@ -76,8 +77,9 @@ public TokenDto checkAuthCode(RequestAuthCodeDto dto, Long eventFrameId) { // JWT 토큰 생성 private TokenDto generateToken(EventUser eventUser) { - Map claims = new HashMap<>(Map.of(ConstantUtil.CLAIMS_KEY, eventUser.getUserId())); - String token = jwtManager.generateToken(eventUser.getUserName(), claims, 1); + Map claims = Map.of(ConstantUtil.CLAIMS_USER_KEY, eventUser.getUserId(), ConstantUtil.CLAIMS_ROLE_KEY, AuthRole.event_user, + ConstantUtil.CLAIMS_USER_NAME_KEY, eventUser.getUserName()); + String token = jwtManager.generateToken(ConstantUtil.JWT_USER_KEY, claims, ConstantUtil.JWT_LIFESPAN); return new TokenDto(token); } } diff --git a/src/test/java/hyundai/softeer/orange/comment/CommentControllerTest.java b/src/test/java/hyundai/softeer/orange/comment/CommentControllerTest.java index 15e80373..fbf2f672 100644 --- a/src/test/java/hyundai/softeer/orange/comment/CommentControllerTest.java +++ b/src/test/java/hyundai/softeer/orange/comment/CommentControllerTest.java @@ -1,6 +1,5 @@ package hyundai.softeer.orange.comment; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import hyundai.softeer.orange.comment.controller.CommentController; import hyundai.softeer.orange.comment.dto.CreateCommentDto; @@ -12,10 +11,14 @@ import hyundai.softeer.orange.common.ErrorCode; import hyundai.softeer.orange.common.ErrorResponse; import hyundai.softeer.orange.common.util.MessageUtil; -import hyundai.softeer.orange.core.jwt.JWTManager; +import hyundai.softeer.orange.core.auth.AuthInterceptor; +import hyundai.softeer.orange.eventuser.component.EventUserArgumentResolver; +import hyundai.softeer.orange.eventuser.dto.EventUserInfo; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -49,15 +52,22 @@ class CommentControllerTest { private ApiService apiService; @MockBean - private JWTManager jwtManager; + private EventUserArgumentResolver eventUserArgumentResolver; + + @MockBean + private AuthInterceptor authInterceptor; ObjectMapper mapper = new ObjectMapper(); - CreateCommentDto createCommentDto = new CreateCommentDto(1L, 1L, "hello"); + CreateCommentDto createCommentDto = new CreateCommentDto(1L, "hello"); String requestBody = ""; @BeforeEach - void setUp() throws JsonProcessingException { + void setUp() throws Exception { + EventUserInfo eventUserInfo = new EventUserInfo("testUserId", "eventUser"); requestBody = mapper.writeValueAsString(createCommentDto); + when(authInterceptor.preHandle(any(), any(), any())).thenReturn(true); + when(eventUserArgumentResolver.supportsParameter(any())).thenReturn(true); + when(eventUserArgumentResolver.resolveArgument(any(), any(), any(), any())).thenReturn(eventUserInfo); } @DisplayName("getComments: 기대평 조회 API를 호출한다.") @@ -99,7 +109,7 @@ void getComments200EmptyTest() throws Exception { void createComment200Test() throws Exception { // given when(apiService.analyzeComment(createCommentDto.getContent())).thenReturn(true); - when(commentService.createComment(any(CreateCommentDto.class), anyBoolean())).thenReturn(true); + when(commentService.createComment(any(), any(CreateCommentDto.class), anyBoolean())).thenReturn(true); // when & then mockMvc.perform(MockMvcRequestBuilders @@ -116,7 +126,7 @@ void createComment200Test() throws Exception { void createComment400Test() throws Exception { // given when(apiService.analyzeComment(createCommentDto.getContent())).thenReturn(true); - when(commentService.createComment(any(CreateCommentDto.class), anyBoolean())) + when(commentService.createComment(any(), any(CreateCommentDto.class), anyBoolean())) .thenThrow(new CommentException(ErrorCode.INVALID_COMMENT)); String responseBody = mapper.writeValueAsString(ErrorResponse.from(ErrorCode.INVALID_COMMENT)); @@ -134,11 +144,10 @@ void createComment400Test() throws Exception { @Test void createComment400BadInputTest() throws Exception { // given - CreateCommentDto badInput = new CreateCommentDto(null, null, ""); + CreateCommentDto badInput = new CreateCommentDto(null, ""); requestBody = mapper.writeValueAsString(badInput); Map expectedErrors = new HashMap<>(); - expectedErrors.put("eventUserId", MessageUtil.BAD_INPUT); expectedErrors.put("eventFrameId", MessageUtil.BAD_INPUT); expectedErrors.put("content", MessageUtil.OUT_OF_SIZE); String responseBody = mapper.writeValueAsString(expectedErrors); @@ -153,12 +162,31 @@ void createComment400BadInputTest() throws Exception { .andExpect(content().json(responseBody)); } + @DisplayName("createComment: 기대평 등록 API를 호출 시 해당 정보를 갖는 유저나 이벤트가 존재하지 않아 실패한다.") + @Test + void createComment404Test() throws Exception { + // given + when(apiService.analyzeComment(createCommentDto.getContent())).thenReturn(true); + when(commentService.createComment(any(), any(CreateCommentDto.class), anyBoolean())) + .thenThrow(new CommentException(ErrorCode.EVENT_USER_NOT_FOUND)); + String responseBody = mapper.writeValueAsString(ErrorResponse.from(ErrorCode.EVENT_USER_NOT_FOUND)); + + // when & then + mockMvc.perform(MockMvcRequestBuilders + .post("/api/v1/comment") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(content().json(responseBody)); + } + @DisplayName("createComment: 기대평 등록 API를 호출 시 기대평 중복 작성으로 인해 실패한다.") @Test void createComment409Test() throws Exception { // given when(apiService.analyzeComment(createCommentDto.getContent())).thenReturn(true); - when(commentService.createComment(any(CreateCommentDto.class), anyBoolean())) + when(commentService.createComment(any(), any(CreateCommentDto.class), anyBoolean())) .thenThrow(new CommentException(ErrorCode.COMMENT_ALREADY_EXISTS)); String responseBody = mapper.writeValueAsString(ErrorResponse.from(ErrorCode.COMMENT_ALREADY_EXISTS)); @@ -171,4 +199,33 @@ void createComment409Test() throws Exception { .andExpect(status().isConflict()) .andExpect(content().json(responseBody)); } + + @DisplayName("isCommentable: 기대평 등록 가능 여부를 조회한다.") + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void isCommentable200Test(boolean isCommentable) throws Exception { + // given + when(commentService.isCommentable(any())).thenReturn(isCommentable); + + // when & then + mockMvc.perform(MockMvcRequestBuilders + .get("/api/v1/comment/info")) + .andExpect(status().isOk()) + .andExpect(content().string(isCommentable ? "true" : "false")); + } + + @DisplayName("isCommentable: 기대평 등록 가능 여부 조회 시 해당 정보를 갖는 유저가 존재하지 않아 실패한다.") + @Test + void isCommentable404Test() throws Exception { + // given + when(commentService.isCommentable(any())) + .thenThrow(new CommentException(ErrorCode.EVENT_USER_NOT_FOUND)); + String responseBody = mapper.writeValueAsString(ErrorResponse.from(ErrorCode.EVENT_USER_NOT_FOUND)); + + // when & then + mockMvc.perform(MockMvcRequestBuilders + .get("/api/v1/comment/info")) + .andExpect(status().isNotFound()) + .andExpect(content().json(responseBody)); + } } diff --git a/src/test/java/hyundai/softeer/orange/comment/CommentServiceTest.java b/src/test/java/hyundai/softeer/orange/comment/CommentServiceTest.java index 09957d6f..3364bea1 100644 --- a/src/test/java/hyundai/softeer/orange/comment/CommentServiceTest.java +++ b/src/test/java/hyundai/softeer/orange/comment/CommentServiceTest.java @@ -48,7 +48,6 @@ void setUp() { Long commentId = 1L; CreateCommentDto createCommentDto = CreateCommentDto.builder() - .eventUserId(1L) .eventFrameId(1L) .content("test") .build(); @@ -89,19 +88,19 @@ void getCommentsTestEmpty() { @Test void createCommentTest() { // given - given(commentRepository.existsByCreatedDateAndEventUser(createCommentDto.getEventUserId())).willReturn(false); + given(commentRepository.existsByCreatedDateAndEventUser(any())).willReturn(false); given(eventFrameRepository.findById(createCommentDto.getEventFrameId())).willReturn(Optional.of(EventFrame.of("eventFrame"))); - given(eventUserRepository.findById(createCommentDto.getEventUserId())).willReturn(Optional.ofNullable(eventUser)); + given(eventUserRepository.findByUserId(eventUser.getUserId())).willReturn(Optional.ofNullable(eventUser)); given(commentRepository.save(any())).willReturn(Comment.of("test", eventFrame, eventUser, true)); // when - commentService.createComment(createCommentDto, true); + commentService.createComment(eventUser.getUserId(), createCommentDto, true); // then verify(commentRepository, times(1)).save(any()); - verify(commentRepository, times(1)).existsByCreatedDateAndEventUser(createCommentDto.getEventUserId()); + verify(commentRepository, times(1)).existsByCreatedDateAndEventUser(any()); verify(eventFrameRepository, times(1)).findById(createCommentDto.getEventFrameId()); - verify(eventUserRepository, times(1)).findById(createCommentDto.getEventUserId()); + verify(eventUserRepository, times(1)).findByUserId(eventUser.getUserId()); verify(commentRepository, times(1)).save(any()); } @@ -109,10 +108,12 @@ void createCommentTest() { @Test void createCommentAlreadyExistsTest() { // given - given(commentRepository.existsByCreatedDateAndEventUser(createCommentDto.getEventUserId())).willReturn(true); + given(eventFrameRepository.findById(createCommentDto.getEventFrameId())).willReturn(Optional.of(eventFrame)); + given(eventUserRepository.findByUserId(eventUser.getUserId())).willReturn(Optional.of(eventUser)); + given(commentRepository.existsByCreatedDateAndEventUser(any())).willReturn(true); // when - assertThatThrownBy(() -> commentService.createComment(createCommentDto, true)) + assertThatThrownBy(() -> commentService.createComment(eventUser.getUserId(), createCommentDto, true)) .isInstanceOf(CommentException.class) .hasMessage(ErrorCode.COMMENT_ALREADY_EXISTS.getMessage()); } @@ -121,11 +122,11 @@ void createCommentAlreadyExistsTest() { @Test void createCommentFrameNotFoundTest() { // given - given(commentRepository.existsByCreatedDateAndEventUser(createCommentDto.getEventUserId())).willReturn(false); + given(eventUserRepository.findByUserId(eventUser.getUserId())).willReturn(Optional.ofNullable(eventUser)); given(eventFrameRepository.findById(createCommentDto.getEventFrameId())).willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> commentService.createComment(createCommentDto, true)) + assertThatThrownBy(() -> commentService.createComment(eventUser.getUserId(), createCommentDto, true)) .isInstanceOf(CommentException.class) .hasMessage(ErrorCode.EVENT_FRAME_NOT_FOUND.getMessage()); } @@ -134,12 +135,10 @@ void createCommentFrameNotFoundTest() { @Test void createCommentUserNotFoundTest() { // given - given(commentRepository.existsByCreatedDateAndEventUser(createCommentDto.getEventUserId())).willReturn(false); - given(eventFrameRepository.findById(createCommentDto.getEventFrameId())).willReturn(Optional.of(EventFrame.of("eventFrame"))); - given(eventUserRepository.findById(createCommentDto.getEventUserId())).willReturn(Optional.empty()); + given(eventUserRepository.findByUserId(eventUser.getUserId())).willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> commentService.createComment(createCommentDto, true)) + assertThatThrownBy(() -> commentService.createComment(eventUser.getUserId(), createCommentDto, true)) .isInstanceOf(CommentException.class) .hasMessage(ErrorCode.EVENT_USER_NOT_FOUND.getMessage()); } diff --git a/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsControllerTest.java b/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsControllerTest.java index 93aab0e4..185aa230 100644 --- a/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsControllerTest.java +++ b/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsControllerTest.java @@ -3,12 +3,17 @@ import com.fasterxml.jackson.databind.ObjectMapper; import hyundai.softeer.orange.common.ErrorCode; import hyundai.softeer.orange.common.ErrorResponse; -import hyundai.softeer.orange.core.jwt.JWTManager; +import hyundai.softeer.orange.core.auth.AuthInterceptor; import hyundai.softeer.orange.event.fcfs.controller.FcfsController; +import hyundai.softeer.orange.event.fcfs.dto.ResponseFcfsInfoDto; import hyundai.softeer.orange.event.fcfs.dto.ResponseFcfsResultDto; import hyundai.softeer.orange.event.fcfs.exception.FcfsEventException; import hyundai.softeer.orange.event.fcfs.service.FcfsAnswerService; +import hyundai.softeer.orange.event.fcfs.service.FcfsManageService; import hyundai.softeer.orange.event.fcfs.service.FcfsService; +import hyundai.softeer.orange.eventuser.component.EventUserArgumentResolver; +import hyundai.softeer.orange.eventuser.dto.EventUserInfo; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -20,6 +25,8 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import java.time.LocalDateTime; + import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -30,6 +37,9 @@ class FcfsControllerTest { @Autowired private MockMvc mockMvc; + @Autowired + private ObjectMapper mapper; + @MockBean private JpaMetamodelMappingContext jpaMetamodelMappingContext; @@ -40,9 +50,26 @@ class FcfsControllerTest { private FcfsAnswerService fcfsAnswerService; @MockBean - private JWTManager jwtManager; + private FcfsManageService fcfsManageService; - ObjectMapper mapper = new ObjectMapper(); + @MockBean + private EventUserArgumentResolver eventUserArgumentResolver; + + @MockBean + private AuthInterceptor authInterceptor; + + String userId = "testUserId"; + String answer = "answer"; + Long eventSequence = 1L; + + @BeforeEach + void setUp() throws Exception { + EventUserInfo mockUserInfo = new EventUserInfo(userId, "event_user"); + when(authInterceptor.preHandle(any(), any(), any())).thenReturn(true); + when(eventUserArgumentResolver.supportsParameter(any())).thenReturn(true); + when(eventUserArgumentResolver.resolveArgument(any(), any(), any(), any())) + .thenReturn(mockUserInfo); + } @DisplayName("participate: 정답을 맞힌 상태에서 선착순 이벤트 참여 혹은 실패") @ParameterizedTest @@ -50,18 +77,17 @@ class FcfsControllerTest { void participateTest(boolean isWinner) throws Exception { // given ResponseFcfsResultDto responseFcfsResultDto = new ResponseFcfsResultDto(true, isWinner); - when(fcfsAnswerService.judgeAnswer(1L, "1")).thenReturn(true); - when(fcfsService.participate(1L, "hyundai")).thenReturn(isWinner); + when(fcfsAnswerService.judgeAnswer(eventSequence, answer)).thenReturn(true); + when(fcfsService.participate(eventSequence, userId)).thenReturn(isWinner); String responseBody = mapper.writeValueAsString(responseFcfsResultDto); // when & then mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event/fcfs") - .param("eventSequence", "1") - .param("userId", "hyundai") - .param("eventAnswer", "1")) + .param("eventSequence", eventSequence.toString()) + .param("eventAnswer", answer)) .andExpect(status().isOk()) .andExpect(content().json(responseBody)); - verify(fcfsService, times(1)).participate(1L, "hyundai"); + verify(fcfsService, times(1)).participate(eventSequence, userId); } @DisplayName("participate: 정답을 맞히지 못하면 무조건 참여 실패하며 fcfsService에 접근조차 하지 않는다.") @@ -69,32 +95,30 @@ void participateTest(boolean isWinner) throws Exception { void participateWrongAnswerTest() throws Exception { // given ResponseFcfsResultDto responseFcfsResultDto = new ResponseFcfsResultDto(false, false); - when(fcfsAnswerService.judgeAnswer(1L, "1")).thenReturn(false); + when(fcfsAnswerService.judgeAnswer(eventSequence, answer)).thenReturn(false); String responseBody = mapper.writeValueAsString(responseFcfsResultDto); // when & then mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event/fcfs") - .param("eventSequence", "1") - .param("userId", "hyundai") - .param("eventAnswer", "1")) + .param("eventSequence", eventSequence.toString()) + .param("eventAnswer", answer)) .andExpect(status().isOk()) .andExpect(content().json(responseBody)); - verify(fcfsService, never()).participate(1L, "hyundai"); + verify(fcfsService, never()).participate(eventSequence, userId); } @DisplayName("participate: 선착순 이벤트 참여 시 이벤트 시간이 아니어서 예외가 발생하는 경우") @Test void participate400Test() throws Exception { // given - when(fcfsAnswerService.judgeAnswer(1L, "1")).thenReturn(true); - when(fcfsService.participate(1L, "hyundai")).thenThrow(new FcfsEventException(ErrorCode.INVALID_EVENT_TIME)); + when(fcfsAnswerService.judgeAnswer(eventSequence, answer)).thenReturn(true); + when(fcfsService.participate(eventSequence, userId)).thenThrow(new FcfsEventException(ErrorCode.INVALID_EVENT_TIME)); String responseBody = mapper.writeValueAsString(ErrorResponse.from(ErrorCode.INVALID_EVENT_TIME)); // when & then mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event/fcfs") - .param("eventSequence", "1") - .param("userId", "hyundai") - .param("eventAnswer", "1")) + .param("eventSequence", eventSequence.toString()) + .param("eventAnswer", answer)) .andExpect(status().isBadRequest()) .andExpect(content().json(responseBody)); } @@ -105,8 +129,61 @@ void participate400Test() throws Exception { void participateBadInputTest(String eventSequence) throws Exception { // when & then mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event/fcfs") - .param("eventSequence", eventSequence) - .param("userId", "hyundai")) + .param("eventSequence", eventSequence)) .andExpect(status().isBadRequest()); } + + @DisplayName("getFcfsInfo: 선착순 이벤트에 대한 정보(서버 기준 시각, 이벤트의 상태)를 조회한다.") + @Test + void getFcfsInfoTest() throws Exception { + // given + ResponseFcfsInfoDto responseFcfsInfoDto = new ResponseFcfsInfoDto(LocalDateTime.now(), "waiting"); + when(fcfsManageService.getFcfsInfo(eventSequence)).thenReturn(responseFcfsInfoDto); + String responseBody = mapper.writeValueAsString(responseFcfsInfoDto); + + // when & then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/event/fcfs/{eventSequence}/info", eventSequence)) + .andExpect(status().isOk()) + .andExpect(content().json(responseBody)); + } + + @DisplayName("getFcfsInfo: 선착순 이벤트를 찾을 수 없는 경우") + @Test + void getFcfsInfo404Test() throws Exception { + // given + when(fcfsManageService.getFcfsInfo(eventSequence)).thenThrow(new FcfsEventException(ErrorCode.EVENT_NOT_FOUND)); + String responseBody = mapper.writeValueAsString(ErrorResponse.from(ErrorCode.EVENT_NOT_FOUND)); + + // when & then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/event/fcfs/{eventSequence}/info", eventSequence)) + .andExpect(status().isNotFound()) + .andExpect(content().json(responseBody)); + } + + @DisplayName("isParticipated: 유저의 특정 선착순 이벤트 참여 여부를 조회한다.") + @Test + void isParticipatedTest() throws Exception { + // given + when(fcfsManageService.isParticipated(eventSequence, userId)).thenReturn(true); + + // when & then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/event/fcfs/participated") + .param("eventSequence", eventSequence.toString())) + .andExpect(status().isOk()) + .andExpect(content().string("true")); + } + + @DisplayName("isParticipated: 선착순 이벤트를 찾을 수 없는 경우") + @Test + void isParticipated404Test() throws Exception { + // given + when(fcfsManageService.isParticipated(eventSequence, userId)).thenThrow(new FcfsEventException(ErrorCode.EVENT_NOT_FOUND)); + String responseBody = mapper.writeValueAsString(ErrorResponse.from(ErrorCode.EVENT_NOT_FOUND)); + + // when & then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/event/fcfs/participated") + .param("eventSequence", eventSequence.toString())) + .andExpect(status().isNotFound()) + .andExpect(content().json(responseBody)); + } } diff --git a/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsManageServiceTest.java b/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsManageServiceTest.java index 9a2721dd..5113ecd6 100644 --- a/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsManageServiceTest.java +++ b/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsManageServiceTest.java @@ -2,6 +2,7 @@ import hyundai.softeer.orange.event.common.entity.EventFrame; import hyundai.softeer.orange.event.common.repository.EventFrameRepository; +import hyundai.softeer.orange.event.fcfs.dto.ResponseFcfsInfoDto; import hyundai.softeer.orange.event.fcfs.dto.ResponseFcfsWinnerDto; import hyundai.softeer.orange.event.fcfs.entity.FcfsEvent; import hyundai.softeer.orange.event.fcfs.entity.FcfsEventWinningInfo; @@ -93,7 +94,7 @@ void tearDown() { @DisplayName("registerFcfsEvents: 오늘의 선착순 이벤트 정보(당첨자 수, 시작 시각)를 Redis에 배치") @Test - void registerFcfsEvents() { + void registerFcfsEventsTest() { // given List events = fcfsEventRepository.findByStartTimeBetween(LocalDateTime.now(), LocalDateTime.now().plusDays(1)); FcfsEvent fcfsEvent = events.get(0); @@ -115,7 +116,7 @@ void registerFcfsEvents() { @DisplayName("registerWinners: redis에 저장된 모든 선착순 이벤트의 당첨자 정보를 DB로 이관") @Test - void registerWinners() { + void registerWinnersTest() { // when fcfsManageService.registerFcfsEvents(); for(int i=0; i<5; i++){ @@ -130,9 +131,35 @@ void registerWinners() { .contains("uuid0", "uuid1", "uuid2", "uuid3", "uuid4"); } + @DisplayName("getFcfsInfo: 특정 선착순 이벤트의 정보 조회") + @Test + void getFcfsInfoTest() { + // when + stringRedisTemplate.opsForValue().set(FcfsUtil.startTimeFormatting(eventSequence.toString()), LocalDateTime.now().toString()); + ResponseFcfsInfoDto fcfsInfo = fcfsManageService.getFcfsInfo(eventSequence); + + // then + assertThat(fcfsInfo.getNowDateTime()).isNotNull(); + assertThat(fcfsInfo.getEventStatus()).isEqualTo("progress"); + } + + @DisplayName("isParticipated: 특정 선착순 이벤트에 참여한 유저임을 확인한다.") + @Test + void isParticipatedTest() { + // given + EventUser eventUser = eventUserRepository.findByUserId("uuid0").get(); + stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(eventSequence.toString()), eventUser.getUserId()); + + // when + boolean participated = fcfsManageService.isParticipated(eventSequence, eventUser.getUserId()); + + // then + assertThat(participated).isTrue(); + } + @DisplayName("getFcfsWinnersInfo: 특정 선착순 이벤트의 당첨자 조회 - 어드민에서 사용") @Test - void getFcfsWinnersInfo() { + void getFcfsWinnersInfoTest() { // when List fcfsWinnersInfo = fcfsManageService.getFcfsWinnersInfo(eventSequence); @@ -142,4 +169,5 @@ void getFcfsWinnersInfo() { .extracting("name") .contains("test0", "test1", "test2", "test3", "test4"); } + } diff --git a/src/test/java/hyundai/softeer/orange/event/fcfs/load/DbFcfsServiceLoadTest.java b/src/test/java/hyundai/softeer/orange/event/fcfs/load/DbFcfsServiceLoadTest.java index 94312925..0fca22fc 100644 --- a/src/test/java/hyundai/softeer/orange/event/fcfs/load/DbFcfsServiceLoadTest.java +++ b/src/test/java/hyundai/softeer/orange/event/fcfs/load/DbFcfsServiceLoadTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; import java.time.LocalDateTime; import java.util.concurrent.CountDownLatch; @@ -22,6 +23,7 @@ @Slf4j @SpringBootTest +@TestPropertySource(locations = "classpath:application-test.yml") class DbFcfsServiceLoadTest { @Autowired diff --git a/src/test/java/hyundai/softeer/orange/eventuser/EventUserServiceTest.java b/src/test/java/hyundai/softeer/orange/eventuser/EventUserServiceTest.java index 9618372c..f7d35a79 100644 --- a/src/test/java/hyundai/softeer/orange/eventuser/EventUserServiceTest.java +++ b/src/test/java/hyundai/softeer/orange/eventuser/EventUserServiceTest.java @@ -2,6 +2,7 @@ import hyundai.softeer.orange.common.ErrorCode; import hyundai.softeer.orange.common.dto.TokenDto; +import hyundai.softeer.orange.common.util.ConstantUtil; import hyundai.softeer.orange.core.jwt.JWTManager; import hyundai.softeer.orange.event.common.entity.EventFrame; import hyundai.softeer.orange.event.common.repository.EventFrameRepository; @@ -99,7 +100,7 @@ void checkAuthCodeTest(String authCode) { // given given(stringRedisTemplate.opsForValue().get(eventUser.getPhoneNumber())).willReturn(authCode); given(eventFrameRepository.findById(any())).willReturn(Optional.of(eventFrame)); - given(jwtManager.generateToken(anyString(), anyMap(), eq(1))) + given(jwtManager.generateToken(anyString(), anyMap(), eq(ConstantUtil.JWT_LIFESPAN))) .willReturn(tokenDto.token()); RequestAuthCodeDto requestAuthCodeDto = RequestAuthCodeDto.builder() .name(eventUser.getUserName()) @@ -149,6 +150,6 @@ void checkAuthCodeBadRequestTest() { // when & then assertThatThrownBy(() -> eventUserService.checkAuthCode(requestAuthCodeDto, eventFrameId)) .isInstanceOf(EventUserException.class) - .hasMessage(ErrorCode.BAD_REQUEST.getMessage()); + .hasMessage(ErrorCode.AUTH_CODE_EXPIRED.getMessage()); } }