Skip to content

Commit

Permalink
Merge pull request #38 from softeerbootcamp4th/feature/35-feat-argume…
Browse files Browse the repository at this point in the history
…ntresolver

[feat] EventUserArgumentResolver 적용 및 FE 요청에 따른 추가 API 구현 (#35)
  • Loading branch information
win-luck authored Aug 7, 2024
2 parents b945d50 + 544357d commit fe1105d
Show file tree
Hide file tree
Showing 25 changed files with 483 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -36,18 +40,34 @@ public ResponseEntity<ResponseCommentsDto> getComments() {
return ResponseEntity.ok(commentService.getComments());
}

@Auth(AuthRole.event_user)
@Tag(name = "Comment")
@PostMapping
@Operation(summary = "기대평 등록", description = "유저가 신규 기대평을 등록한다.", responses = {
@ApiResponse(responseCode = "200", description = "기대평 등록 성공",
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<Boolean> createComment(@RequestBody @Valid CreateCommentDto dto) {
public ResponseEntity<Boolean> 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<Boolean> isCommentable(@EventUserAnnotation EventUserInfo userInfo) {
return ResponseEntity.ok(commentService.isCommentable(userInfo.getUserId()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/hyundai/softeer/orange/common/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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, "권한이 없습니다."),
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,21 @@ 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";

public static final double LIMIT_NEGATIVE_CONFIDENCE = 99.5;
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;
}
11 changes: 7 additions & 4 deletions src/main/java/hyundai/softeer/orange/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -28,5 +30,6 @@ public void addInterceptors(InterceptorRegistry registry) {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(adminArgumentResolver);
resolvers.add(eventUserArgumentResolver);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 = {
Expand All @@ -30,9 +38,34 @@ public class FcfsController {
@ApiResponse(responseCode = "400", description = "선착순 이벤트 시간이 아니거나, 요청 형식이 잘못된 경우",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<ResponseFcfsResultDto> participate(@RequestParam Long eventSequence, @RequestParam String userId, @RequestParam String eventAnswer) {
public ResponseEntity<ResponseFcfsResultDto> 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<ResponseFcfsInfoDto> 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<Boolean> isParticipated(@EventUserAnnotation EventUserInfo userInfo, @RequestParam Long eventSequence) {
return ResponseEntity.ok(fcfsManageService.isParticipated(eventSequence, userInfo.getUserId()));
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<ResponseFcfsWinnerDto> getFcfsWinnersInfo(Long eventSequence) {
Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit fe1105d

Please sign in to comment.