From b286b970b21c93a1f5b4a3610a63a45627a58bb5 Mon Sep 17 00:00:00 2001 From: Son Chanhyeok <127181634+hyeokson@users.noreply.github.com> Date: Thu, 15 Aug 2024 17:40:23 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=EC=84=A0=EC=B0=A9=EC=88=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Infra] CI/CD test (#42) * infra: 빌드 테스트 yml 작성 * infra: DB 정보 추가 * infra: ssh-agent 버전 변경 * infra: known_hosts 추가 * infra: db port 변경 * infra: database test 설정 변경 * infra: DB 환경변수 설정 및 application.yml 생성 * infra: application.yml 동적 생성 스크립트 수정 * infra: 레디스 설정 추가 * infra: redis test 추가 * infra: redis 버전 변경 * infra: redis cli 설치 * infra: application.yml 위치 및 내용 확인 * infra: Github Actions 환경변수에 REDIS_HOST, REDIS_PORT 추가 * infra: 환경변수 확인 추가 * infra: zip file 만들기 추가, AWS credentials 추가 * infra: 환경변수 이름 변경 - ARN -> AWS_ARN * infra: s3 bucket에 업로드 추가 * infra: code deploy 추가 * infra: code deploy 수정 * infra: code deploy 수정 * infra: appspec.yml 작성 * infra: application.yml 생성 경로 변경 * infra: application.yml 확인 스크립트 삭제 * infra: application.yml 생성 스크립트 수정 * infra: application-prod.yml 추가 * infra: appspec.yml 수정, 배포를 위한 sh파일 추가 * infra: deploy.yml 이름 변경 - test_deploy -> deploy * infra: body = null 설정 * infra: develop에 머지되었을 때만 발동하도록 수정 * feat: draw_rank column 이름 수정 * Infra: environment 삭제 * [Infra] CI CD test 3 (#45) * infra: 빌드 테스트 yml 작성 * infra: DB 정보 추가 * infra: ssh-agent 버전 변경 * infra: known_hosts 추가 * infra: db port 변경 * infra: database test 설정 변경 * infra: DB 환경변수 설정 및 application.yml 생성 * infra: application.yml 동적 생성 스크립트 수정 * infra: 레디스 설정 추가 * infra: redis test 추가 * infra: redis 버전 변경 * infra: redis cli 설치 * infra: application.yml 위치 및 내용 확인 * infra: Github Actions 환경변수에 REDIS_HOST, REDIS_PORT 추가 * infra: 환경변수 확인 추가 * infra: zip file 만들기 추가, AWS credentials 추가 * infra: 환경변수 이름 변경 - ARN -> AWS_ARN * infra: s3 bucket에 업로드 추가 * infra: code deploy 추가 * infra: code deploy 수정 * infra: code deploy 수정 * infra: appspec.yml 작성 * infra: application.yml 생성 경로 변경 * infra: application.yml 확인 스크립트 삭제 * infra: application.yml 생성 스크립트 수정 * infra: application-prod.yml 추가 * infra: appspec.yml 수정, 배포를 위한 sh파일 추가 * infra: deploy.yml 이름 변경 - test_deploy -> deploy * infra: body = null 설정 * infra: develop에 머지되었을 때만 발동하도록 수정 * feat: draw_rank column 이름 수정 * Infra: environment 삭제 * Infra: environment 삭제 * config: jwt 속성을 yml에 설정 * rebase: 원본 develop 브랜치와 병합 * feat: FcfsException 클래스 구현 * feat: Fcfs 퀴즈 화면 응답 dto 구현 * feat: 선착순 페이지 접근을 관리하는 인터셉터 구현 * feat: 선착순 퀴즈 entity 클래스 구현 * feat: 선착순 퀴즈 dto 클래스 구현 * feat: 선착순 퀴즈 repository 클래스 구현 * feat: 인터셉터 등록 * feat: 선착순 정적 텍스트 등록 * feat: 선착순 동적 텍스트 바인딩 * feat: swagger에서 파라미터가 보이지 않도록 설정 * refactor: 이벤트 지표 응답 dto 수정 - 이벤트 시작, 종료 날짜를 DrawSettingManager에서 가져오도록 수정 - 각 이벤트 참여 비율 계산 시, 분모가 0이 되는 경우를 처리 * refactor: 사용하지 않는 메서드 삭제 * chore: 임시로 주석처리 * feat: 선착순 컨트롤러 메서드 구현 - 선착순 튜토리얼 페이지 정보를 제공하는 메서드 구현 - 선착순 결과를 응답하는 메서드 구현 * refactor: 패키지 변경 * feat: 선착순 당첨 실패 응답 dto에 필드 추가 * chore: import문 삭제 * feat: 선착순 퀴즈 페이지 정보를 제공하는 메서드 구현 * feat: 선착순 설정 매니저에서 메서드 추가 - 선착순 당첨가능한 수를 반환하는 메서드 - 다음 선착순 게임에서 사용될 힌트 반환하는 메서드 - 현재 선착순 게임의 퀴즈 정보를 반환하는 메서드 - 선착순 페이지에 접근 가능한지 여부를 반환하는 메서드 - 현재 선착순 게임이 몇 라운드인지를 반환하는 메서드 * refactor: 사용하지 않는 메서드 삭제 * feat: 선착순 당첨 응답 dto에 필드 추가 * refactor: json format에서 시간값 형식 변경 * feat: 메인 페이지 응답 dto에 선착순 힌트 필드 추가 * feat: 메서드에 파라미터 추가 * feat: 이벤트 페이지에 선착순 힌트를 설정 * Revert "어드민 기능 인덱스 에러 수정 및 선착순 기능 일부 구현 (#106)" (#107) This reverts commit ceb48fa64f41bdad525d846db3a9324ee2a82b2b. * [Infra] CI/CD test (#42) * infra: 빌드 테스트 yml 작성 * infra: DB 정보 추가 * infra: ssh-agent 버전 변경 * infra: known_hosts 추가 * infra: db port 변경 * infra: database test 설정 변경 * infra: DB 환경변수 설정 및 application.yml 생성 * infra: application.yml 동적 생성 스크립트 수정 * infra: 레디스 설정 추가 * infra: redis test 추가 * infra: redis 버전 변경 * infra: redis cli 설치 * infra: application.yml 위치 및 내용 확인 * infra: Github Actions 환경변수에 REDIS_HOST, REDIS_PORT 추가 * infra: 환경변수 확인 추가 * infra: zip file 만들기 추가, AWS credentials 추가 * infra: 환경변수 이름 변경 - ARN -> AWS_ARN * infra: s3 bucket에 업로드 추가 * infra: code deploy 추가 * infra: code deploy 수정 * infra: code deploy 수정 * infra: appspec.yml 작성 * infra: application.yml 생성 경로 변경 * infra: application.yml 확인 스크립트 삭제 * infra: application.yml 생성 스크립트 수정 * infra: application-prod.yml 추가 * infra: appspec.yml 수정, 배포를 위한 sh파일 추가 * infra: deploy.yml 이름 변경 - test_deploy -> deploy * infra: body = null 설정 * infra: develop에 머지되었을 때만 발동하도록 수정 * feat: draw_rank column 이름 수정 * Infra: environment 삭제 * [Infra] CI CD test 3 (#45) * infra: 빌드 테스트 yml 작성 * infra: DB 정보 추가 * infra: ssh-agent 버전 변경 * infra: known_hosts 추가 * infra: db port 변경 * infra: database test 설정 변경 * infra: DB 환경변수 설정 및 application.yml 생성 * infra: application.yml 동적 생성 스크립트 수정 * infra: 레디스 설정 추가 * infra: redis test 추가 * infra: redis 버전 변경 * infra: redis cli 설치 * infra: application.yml 위치 및 내용 확인 * infra: Github Actions 환경변수에 REDIS_HOST, REDIS_PORT 추가 * infra: 환경변수 확인 추가 * infra: zip file 만들기 추가, AWS credentials 추가 * infra: 환경변수 이름 변경 - ARN -> AWS_ARN * infra: s3 bucket에 업로드 추가 * infra: code deploy 추가 * infra: code deploy 수정 * infra: code deploy 수정 * infra: appspec.yml 작성 * infra: application.yml 생성 경로 변경 * infra: application.yml 확인 스크립트 삭제 * infra: application.yml 생성 스크립트 수정 * infra: application-prod.yml 추가 * infra: appspec.yml 수정, 배포를 위한 sh파일 추가 * infra: deploy.yml 이름 변경 - test_deploy -> deploy * infra: body = null 설정 * infra: develop에 머지되었을 때만 발동하도록 수정 * feat: draw_rank column 이름 수정 * Infra: environment 삭제 * Infra: environment 삭제 * rebase: 원본 repo의 develop 브랜치와 충돌 rebase * feat: 매개변수 swagger에서 나타나지 않도록 설정 * [Infra] CI/CD test (#42) * infra: 빌드 테스트 yml 작성 * infra: DB 정보 추가 * infra: ssh-agent 버전 변경 * infra: known_hosts 추가 * infra: db port 변경 * infra: database test 설정 변경 * infra: DB 환경변수 설정 및 application.yml 생성 * infra: application.yml 동적 생성 스크립트 수정 * infra: 레디스 설정 추가 * infra: redis test 추가 * infra: redis 버전 변경 * infra: redis cli 설치 * infra: application.yml 위치 및 내용 확인 * infra: Github Actions 환경변수에 REDIS_HOST, REDIS_PORT 추가 * infra: 환경변수 확인 추가 * infra: zip file 만들기 추가, AWS credentials 추가 * infra: 환경변수 이름 변경 - ARN -> AWS_ARN * infra: s3 bucket에 업로드 추가 * infra: code deploy 추가 * infra: code deploy 수정 * infra: code deploy 수정 * infra: appspec.yml 작성 * infra: application.yml 생성 경로 변경 * infra: application.yml 확인 스크립트 삭제 * infra: application.yml 생성 스크립트 수정 * infra: application-prod.yml 추가 * infra: appspec.yml 수정, 배포를 위한 sh파일 추가 * infra: deploy.yml 이름 변경 - test_deploy -> deploy * infra: body = null 설정 * infra: develop에 머지되었을 때만 발동하도록 수정 * feat: draw_rank column 이름 수정 * Infra: environment 삭제 * [Infra] CI CD test 3 (#45) * infra: 빌드 테스트 yml 작성 * infra: DB 정보 추가 * infra: ssh-agent 버전 변경 * infra: known_hosts 추가 * infra: db port 변경 * infra: database test 설정 변경 * infra: DB 환경변수 설정 및 application.yml 생성 * infra: application.yml 동적 생성 스크립트 수정 * infra: 레디스 설정 추가 * infra: redis test 추가 * infra: redis 버전 변경 * infra: redis cli 설치 * infra: application.yml 위치 및 내용 확인 * infra: Github Actions 환경변수에 REDIS_HOST, REDIS_PORT 추가 * infra: 환경변수 확인 추가 * infra: zip file 만들기 추가, AWS credentials 추가 * infra: 환경변수 이름 변경 - ARN -> AWS_ARN * infra: s3 bucket에 업로드 추가 * infra: code deploy 추가 * infra: code deploy 수정 * infra: code deploy 수정 * infra: appspec.yml 작성 * infra: application.yml 생성 경로 변경 * infra: application.yml 확인 스크립트 삭제 * infra: application.yml 생성 스크립트 수정 * infra: application-prod.yml 추가 * infra: appspec.yml 수정, 배포를 위한 sh파일 추가 * infra: deploy.yml 이름 변경 - test_deploy -> deploy * infra: body = null 설정 * infra: develop에 머지되었을 때만 발동하도록 수정 * feat: draw_rank column 이름 수정 * Infra: environment 삭제 * Infra: environment 삭제 * config: jwt 속성을 yml에 설정 * rebase: 원본 develop 브랜치와 병합 * feat: FcfsException 클래스 구현 * feat: Fcfs 퀴즈 화면 응답 dto 구현 * feat: 선착순 페이지 접근을 관리하는 인터셉터 구현 * feat: 선착순 퀴즈 entity 클래스 구현 * feat: 선착순 퀴즈 dto 클래스 구현 * feat: 선착순 퀴즈 repository 클래스 구현 * feat: 인터셉터 등록 * feat: 선착순 정적 텍스트 등록 * feat: 선착순 동적 텍스트 바인딩 * refactor: 이벤트 지표 응답 dto 수정 - 이벤트 시작, 종료 날짜를 DrawSettingManager에서 가져오도록 수정 - 각 이벤트 참여 비율 계산 시, 분모가 0이 되는 경우를 처리 * refactor: 사용하지 않는 메서드 삭제 * chore: 임시로 주석처리 * feat: 선착순 컨트롤러 메서드 구현 - 선착순 튜토리얼 페이지 정보를 제공하는 메서드 구현 - 선착순 결과를 응답하는 메서드 구현 * refactor: 패키지 변경 * feat: 선착순 당첨 실패 응답 dto에 필드 추가 * chore: import문 삭제 * feat: 선착순 퀴즈 페이지 정보를 제공하는 메서드 구현 * feat: 선착순 설정 매니저에서 메서드 추가 - 선착순 당첨가능한 수를 반환하는 메서드 - 다음 선착순 게임에서 사용될 힌트 반환하는 메서드 - 현재 선착순 게임의 퀴즈 정보를 반환하는 메서드 - 선착순 페이지에 접근 가능한지 여부를 반환하는 메서드 - 현재 선착순 게임이 몇 라운드인지를 반환하는 메서드 * refactor: 사용하지 않는 메서드 삭제 * feat: 선착순 당첨 응답 dto에 필드 추가 * feat: 메인 페이지 응답 dto에 선착순 힌트 필드 추가 * feat: 메서드에 파라미터 추가 * feat: 이벤트 페이지에 선착순 힌트를 설정 * feat: 선착순 redis util 클래스 구현 * refactor: FcfsRedisUtil을 사용하도록 변경 * feat: 추첨 이벤트 시간을 변경하는 메서드 구현 * feat: 추첨 시간을 변경하는 코드 삽입 * feat: EventLock 예외가 발생하면 redirect 하도록 구현 * feat: 선착순 entity에 code 필드 추가 * feat: 선착순 code값과 당첨여부를 redirect하는 기능 구현 * feat: 선착순 등록 로직 구현 * feat: ScheduledFuture import * feat: 선착순 관련 redis key 상수 추가 * feat: 어드민 페이지 cors 설정 * feat: 선착순 fcfsClosed 변수를 false로 초가화 * feat: integer 레디스 템플릿으로 hash값 저장 * feat: 에러 로그 추가 * refactor: list 초기화 * refactor: get, post요청 모두 round값 전달 * feat: hash 키, 값 serializer 등록 --------- Co-authored-by: DrRivaski <48974215+DrRivaski@users.noreply.github.com> Co-authored-by: hyeokson --- .../admin/service/EventPageService.java | 2 + .../draw/service/DrawSettingManager.java | 5 + .../fcfs/controller/FcfsController.java | 23 ++- .../backend/fo_domain/fcfs/domain/Fcfs.java | 3 + .../interceptor/FcfsTimeCheckInterceptor.java | 8 +- .../fo_domain/fcfs/service/FcfsService.java | 144 +++++++++++------- .../fcfs/service/FcfsSettingManager.java | 11 +- .../common/constant/RedisKeyPrefix.java | 9 +- .../common/exception/ExceptionAdvice.java | 22 ++- .../global/config/redis/RedisConfig.java | 4 + .../global/config/web/WebMvcConfig.java | 3 +- .../global/scheduler/DbInsertScheduler.java | 20 ++- .../backend/global/util/FcfsRedisUtil.java | 80 ++++++++++ 13 files changed, 240 insertions(+), 94 deletions(-) create mode 100644 src/main/java/com/softeer/backend/global/util/FcfsRedisUtil.java diff --git a/src/main/java/com/softeer/backend/bo_domain/admin/service/EventPageService.java b/src/main/java/com/softeer/backend/bo_domain/admin/service/EventPageService.java index 3e305de2..d0208e40 100644 --- a/src/main/java/com/softeer/backend/bo_domain/admin/service/EventPageService.java +++ b/src/main/java/com/softeer/backend/bo_domain/admin/service/EventPageService.java @@ -82,6 +82,8 @@ private void updateDrawSetting(DrawSetting drawSetting, LocalDate startDate, Loc drawSetting.setStartDate(startDateOfDraw); drawSetting.setEndDate(endDateOfDraw); + + drawSettingManager.setDrawDate(drawSetting); } public void updateDrawEventTime(DrawEventTimeRequestDto drawEventTimeRequestDto) { diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/service/DrawSettingManager.java b/src/main/java/com/softeer/backend/fo_domain/draw/service/DrawSettingManager.java index 94f4988b..bc0573a1 100644 --- a/src/main/java/com/softeer/backend/fo_domain/draw/service/DrawSettingManager.java +++ b/src/main/java/com/softeer/backend/fo_domain/draw/service/DrawSettingManager.java @@ -95,6 +95,11 @@ private void addWinnerToDatabase() { } } + public void setDrawDate(DrawSetting drawSetting) { + this.startDate = drawSetting.getStartDate(); + this.endDate = drawSetting.getEndDate(); + } + public void setDrawTime(DrawSetting drawSetting) { this.startTime = drawSetting.getStartTime(); this.endTime = drawSetting.getEndTime(); diff --git a/src/main/java/com/softeer/backend/fo_domain/fcfs/controller/FcfsController.java b/src/main/java/com/softeer/backend/fo_domain/fcfs/controller/FcfsController.java index da947d08..241497b6 100644 --- a/src/main/java/com/softeer/backend/fo_domain/fcfs/controller/FcfsController.java +++ b/src/main/java/com/softeer/backend/fo_domain/fcfs/controller/FcfsController.java @@ -48,19 +48,28 @@ public String handleFcfs(@Parameter(hidden = true) HttpServletRequest request, int round = (Integer) request.getAttribute("round"); -// boolean isFcfsWinner = fcfsService.handleFcfsEvent(userId, round, answer); -// -// // 리다이렉트 시 쿼리 파라미터를 추가하여 정보 전달 -// redirectAttributes.addAttribute("fcfsWin", isFcfsWinner); + String fcfsCode = fcfsService.handleFcfsEvent(userId, round, answer); + + if(fcfsCode != null){ + request.getSession().setAttribute("fcfsCode", fcfsCode); + + redirectAttributes.addAttribute("fcfsWin", true); + } + else{ + redirectAttributes.addAttribute("fcfsWin", false); + } - // GET 요청으로 리다이렉트 return "redirect:/fcfs/result"; } @GetMapping("/result") @ResponseBody - public ResponseDto getFcfsResult(@RequestParam("fcfsWin") Boolean fcfsWin){ - FcfsResponseDto fcfsResponseDto = fcfsService.getFcfsResult(fcfsWin); + public ResponseDto getFcfsResult(@Parameter(hidden = true) HttpServletRequest request, + @RequestParam("fcfsWin") Boolean fcfsWin){ + + String fcfsCode = (String) request.getSession().getAttribute("fcfsCode"); + + FcfsResponseDto fcfsResponseDto = fcfsService.getFcfsResult(fcfsWin, fcfsCode); return ResponseDto.onSuccess(fcfsResponseDto); } diff --git a/src/main/java/com/softeer/backend/fo_domain/fcfs/domain/Fcfs.java b/src/main/java/com/softeer/backend/fo_domain/fcfs/domain/Fcfs.java index d56a8ecc..2684335f 100644 --- a/src/main/java/com/softeer/backend/fo_domain/fcfs/domain/Fcfs.java +++ b/src/main/java/com/softeer/backend/fo_domain/fcfs/domain/Fcfs.java @@ -32,6 +32,9 @@ public class Fcfs { @Column(name = "round") private int round; + @Column(name = "code") + private String code; + @CreatedDate @Column(name = "winning_date", nullable = false) private LocalDateTime winningDate; diff --git a/src/main/java/com/softeer/backend/fo_domain/fcfs/interceptor/FcfsTimeCheckInterceptor.java b/src/main/java/com/softeer/backend/fo_domain/fcfs/interceptor/FcfsTimeCheckInterceptor.java index e4e4035a..24f21ebd 100644 --- a/src/main/java/com/softeer/backend/fo_domain/fcfs/interceptor/FcfsTimeCheckInterceptor.java +++ b/src/main/java/com/softeer/backend/fo_domain/fcfs/interceptor/FcfsTimeCheckInterceptor.java @@ -29,10 +29,10 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons throw new FcfsException(ErrorStatus._BAD_REQUEST); } - if(request.getMethod().equals("GET")){ - int round = fcfsSettingManager.getFcfsRound(now); - request.setAttribute("round", round); - } + + int round = fcfsSettingManager.getFcfsRound(now); + request.setAttribute("round", round); + return true; } diff --git a/src/main/java/com/softeer/backend/fo_domain/fcfs/service/FcfsService.java b/src/main/java/com/softeer/backend/fo_domain/fcfs/service/FcfsService.java index 564d9c23..55c3a267 100644 --- a/src/main/java/com/softeer/backend/fo_domain/fcfs/service/FcfsService.java +++ b/src/main/java/com/softeer/backend/fo_domain/fcfs/service/FcfsService.java @@ -1,16 +1,29 @@ package com.softeer.backend.fo_domain.fcfs.service; +import com.softeer.backend.fo_domain.fcfs.domain.Fcfs; import com.softeer.backend.fo_domain.fcfs.dto.*; import com.softeer.backend.fo_domain.fcfs.dto.result.FcfsFailResponseDto; import com.softeer.backend.fo_domain.fcfs.dto.result.FcfsResponseDto; +import com.softeer.backend.fo_domain.fcfs.dto.result.FcfsSuccessResponseDto; +import com.softeer.backend.fo_domain.fcfs.exception.FcfsException; import com.softeer.backend.fo_domain.fcfs.repository.FcfsRepository; +import com.softeer.backend.fo_domain.user.domain.User; +import com.softeer.backend.fo_domain.user.exception.UserException; import com.softeer.backend.fo_domain.user.repository.UserRepository; +import com.softeer.backend.global.annotation.EventLock; +import com.softeer.backend.global.common.code.status.ErrorStatus; +import com.softeer.backend.global.common.constant.RedisKeyPrefix; import com.softeer.backend.global.staticresources.util.StaticResourcesUtil; import com.softeer.backend.global.util.EventLockRedisUtil; +import com.softeer.backend.global.util.FcfsRedisUtil; +import com.softeer.backend.global.util.RandomCodeUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.cache.CacheProperties; import org.springframework.stereotype.Service; +import java.util.Set; + /** * 선착순 관련 이벤트를 처리하는 클래스 */ @@ -20,10 +33,9 @@ public class FcfsService { private final FcfsSettingManager fcfsSettingManager; - private final FcfsRepository fcfsRepository; - private final EventLockRedisUtil eventLockRedisUtil; - private final UserRepository userRepository; + private final FcfsRedisUtil fcfsRedisUtil; private final StaticResourcesUtil staticResourcesUtil; + private final RandomCodeUtil randomCodeUtil; public FcfsPageResponseDto getFcfsPage(int round) { @@ -55,60 +67,78 @@ public FcfsPageResponseDto getFcfsTutorialPage() { * 1. 선착순 당첨자가 아직 다 결정되지 않았으면, 선착순 당첨 응답 생성 및 반환 * 2. 선착순 당첨자가 다 결정됐다면, Redisson lock을 사용하지 않고 Redis에 저장된 선착순 이벤트 참여자 수를 1명씩 더한다. */ -// public FcfsResponseDto handleFcfsEvent(int userId, int round, String answer) { -// if (fcfsSettingManager.isFcfsClosed()) -// return countFcfsParticipant(fcfsSettingManager.getRound()); -// -// return saveFcfsWinners(userId, fcfsSettingManager.getRound()); -// } + public String handleFcfsEvent(int userId, int round, String answer) { - /** - * 1. Redisson lock을 걸고 선착순 이벤트 참여자 수가 지정된 수보다 적다면, 선착순 당첨 정보를 DB에 저장하고 - * Redis에 저장된 선착순 이벤트 참여자 수를 1만큼 증가시키도 선착순 당첨 응답을 생성하여 반환한다. - * 만약, 참여자 수가 총 당첨자 수와 같아졌으면, fcfsSettingManager의 setFcfsClosed를 true로 변환한다. - * 2. setFcfsClosed가 true로 바뀌게 전에 요청이 들어왔다면, 선착순 실패 응답을 생성하여 반환한다. - */ -// @EventLock(key = "FCFS_WINNER_#{#round}") -// private FcfsResponseDto saveFcfsWinners(int userId, int round) { -// Set participantIds = eventLockRedisUtil.getAllDataAsSet(RedisLockPrefix.FCFS_LOCK_PREFIX.getPrefix() + round); -// -// if (participantIds.size() < fcfsSettingManager.getWinnerNum() && -// !eventLockRedisUtil.isParticipantExists(RedisLockPrefix.FCFS_LOCK_PREFIX.getPrefix() + round, userId)) { -// User user = userRepository.findById(userId) -// .orElseThrow(() -> { -// log.error("user not found in saveFcfsWinners method."); -// return new UserException(ErrorStatus._NOT_FOUND); -// }); -// -// Fcfs fcfs = Fcfs.builder() -// .user(user) -// .round(round) -// .build(); -// fcfsRepository.save(fcfs); -// -// eventLockRedisUtil.incrementParticipantCount(RedisLockPrefix.FCFS_PARTICIPANT_COUNT_PREFIX.getPrefix() + round); -// if (participantIds.size() + 1 == fcfsSettingManager.getWinnerNum()) { -// fcfsSettingManager.setFcfsClosed(true); -// } -// -// return new FcfsSuccessResponseDto(1); -// } -// -// return new FcfsFailResponseDtoDto(1); -// } - - public FcfsResponseDto getFcfsResult(boolean fcfsWin){ -// if(fcfsWin){ -// return FcfsSuccessResponseDto.builder() -// .title(staticResourcesUtil.getData("FCFS_WINNER_TITLE")) -// .subTitle(staticResourcesUtil.getData("FCFS_WINNER_SUBTITLE")) -// .qrCode(staticResourcesUtil.getData("barcode_image")) -// .codeWord(staticResourcesUtil.getData("FCFS_WINNER_CODE_WORD")) -// .fcfsCode() -// .expirationDate(staticResourcesUtil.getData("FCFS_WINNER_EXPIRY_DATE")) -// .caution(staticResourcesUtil.getData("FCFS_WINNER_CAUTION")) -// .build(); -// } + if(!answer.equals(fcfsSettingManager.getQuiz(round).getAnswerWord())) { + log.error("fcfs quiz answer is not match, correct answer: {}, wrong anwer: {}", + fcfsSettingManager.getQuiz(round).getAnswerWord(), answer); + throw new FcfsException(ErrorStatus._BAD_REQUEST); + } + + if (fcfsSettingManager.isFcfsClosed()){ + countFcfsParticipant(round); + + return null; + } + + return saveFcfsWinners(userId, round); + } + + @EventLock(key = "FCFS_WINNER_#{#round}") + private String saveFcfsWinners(int userId, int round) { + + long numOfWinners = fcfsRedisUtil.getIntegerSetSize(RedisKeyPrefix.FCFS_LOCK_PREFIX.getPrefix() + round); + + if (numOfWinners < fcfsSettingManager.getFcfsWinnerNum() + && !fcfsRedisUtil.isValueInIntegerSet(RedisKeyPrefix.FCFS_LOCK_PREFIX.getPrefix() + round, userId)) { + + // redis에 userId 등록 + fcfsRedisUtil.addToIntegerSet(RedisKeyPrefix.FCFS_LOCK_PREFIX.getPrefix() + round, userId); + + // redis에 code 등록 + String code = makeFcfsCode(round); + while(fcfsRedisUtil.isValueInStringSet(RedisKeyPrefix.FCFS_CODE_PREFIX.getPrefix() + round, code)){ + code = makeFcfsCode(round); + } + fcfsRedisUtil.addToStringSet(RedisKeyPrefix.FCFS_CODE_PREFIX.getPrefix() + round, code); + + // redis에 code-userId 형태로 등록(hash) + fcfsRedisUtil.addToHash(RedisKeyPrefix.FCFS_CODE_USERID_PREFIX.getPrefix() + round, code, userId); + + // redis에 선착순 참가자 수 +1 + countFcfsParticipant(round); + + // 선착순 당첨이 마감되면 FcfsSettingManager의 fcfsClodes 변수값을 true로 설정 + if (numOfWinners + 1 == fcfsSettingManager.getFcfsWinnerNum()) { + fcfsSettingManager.setFcfsClosed(true); + } + + return code; + } + + return null; + } + + private String makeFcfsCode(int round){ + return (char)('A'+round-1) + randomCodeUtil.generateRandomCode(5); + } + + private void countFcfsParticipant(int round) { + fcfsRedisUtil.incrementValue(RedisKeyPrefix.FCFS_PARTICIPANT_COUNT_PREFIX.getPrefix() + round); + } + + public FcfsResponseDto getFcfsResult(boolean fcfsWin, String fcfsCode){ + if(fcfsWin){ + return FcfsSuccessResponseDto.builder() + .title(staticResourcesUtil.getData("FCFS_WINNER_TITLE")) + .subTitle(staticResourcesUtil.getData("FCFS_WINNER_SUBTITLE")) + .qrCode(staticResourcesUtil.getData("barcode_image")) + .codeWord(staticResourcesUtil.getData("FCFS_WINNER_CODE_WORD")) + .fcfsCode(fcfsCode) + .expirationDate(staticResourcesUtil.getData("FCFS_WINNER_EXPIRY_DATE")) + .caution(staticResourcesUtil.getData("FCFS_WINNER_CAUTION")) + .build(); + } return FcfsFailResponseDto.builder() .title(staticResourcesUtil.getData("FCFS_LOSER_TITLE")) @@ -117,4 +147,6 @@ public FcfsResponseDto getFcfsResult(boolean fcfsWin){ .build(); } + + } diff --git a/src/main/java/com/softeer/backend/fo_domain/fcfs/service/FcfsSettingManager.java b/src/main/java/com/softeer/backend/fo_domain/fcfs/service/FcfsSettingManager.java index 7d7e08c8..a11e3084 100644 --- a/src/main/java/com/softeer/backend/fo_domain/fcfs/service/FcfsSettingManager.java +++ b/src/main/java/com/softeer/backend/fo_domain/fcfs/service/FcfsSettingManager.java @@ -19,7 +19,6 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ScheduledFuture; /** * 선착순 이벤트 정보를 관리하는 클래스 @@ -60,7 +59,7 @@ public FcfsSettingDto getFcfsSettingByRound(int round) { public void loadInitialData() { List fcfsSettings = fcfsSettingRepository.findAll(); - fcfsSettingList = new ArrayList<>(4); + fcfsSettingList = new ArrayList<>(); for (int i = 0; i < 4; i++) { fcfsSettingList.add(null); // 인덱스 0부터 3까지 빈 슬롯을 추가 @@ -76,11 +75,7 @@ public void loadInitialData() { }); List quizs = quizRepository.findAll(Sort.by(Sort.Direction.ASC, "id")); - quizList = new ArrayList<>(4); - - for (int i = 0; i < 4; i++) { - quizList.add(null); // 인덱스 0부터 3까지 빈 슬롯을 추가 - } + quizList = new ArrayList<>(); quizs.forEach((quiz) -> { @@ -157,7 +152,7 @@ public String getHint(){ } public QuizDto getQuiz(int round){ - + log.info("quiz: {}", quizList.get(round-1)); return quizList.get(round - 1); } diff --git a/src/main/java/com/softeer/backend/global/common/constant/RedisKeyPrefix.java b/src/main/java/com/softeer/backend/global/common/constant/RedisKeyPrefix.java index 6950c0e6..d19af667 100644 --- a/src/main/java/com/softeer/backend/global/common/constant/RedisKeyPrefix.java +++ b/src/main/java/com/softeer/backend/global/common/constant/RedisKeyPrefix.java @@ -4,11 +4,18 @@ @Getter public enum RedisKeyPrefix { + // 선착순 FCFS_LOCK_PREFIX("LOCK:FCFS_WINNER_"), + FCFS_CODE_PREFIX("FCFS_CODE_"), + FCFS_CODE_USERID_PREFIX("FCFS_CODE_USERID_"), + FCFS_PARTICIPANT_COUNT_PREFIX("FCFS_PARTICIPANT_COUNT_"), + + // 추첨 DRAW_LOCK_PREFIX("LOCK:DRAW_WINNER"), DRAW_WINNER_LIST_PREFIX("DRAW_WINNER_LIST_"), - FCFS_PARTICIPANT_COUNT_PREFIX("FCFS_PARTICIPANT_COUNT_"), DRAW_PARTICIPANT_COUNT_PREFIX("DRAW_PARTICIPANT_COUNT"), + + // 사이트 방문자 수 TOTAL_VISITORS_COUNT_PREFIX("TOTAL_VISITORS_COUNT_"); diff --git a/src/main/java/com/softeer/backend/global/common/exception/ExceptionAdvice.java b/src/main/java/com/softeer/backend/global/common/exception/ExceptionAdvice.java index 15a57671..8c9208d5 100644 --- a/src/main/java/com/softeer/backend/global/common/exception/ExceptionAdvice.java +++ b/src/main/java/com/softeer/backend/global/common/exception/ExceptionAdvice.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import java.util.*; @@ -39,7 +40,7 @@ public ResponseEntity handleGeneralException(GeneralException generalExc } @ExceptionHandler - public ResponseEntity handleEventLockException(EventLockException eventLockException, WebRequest webRequest) { + public ModelAndView handleEventLockException(EventLockException eventLockException, WebRequest webRequest) { return handleEventLockExceptionInternal(eventLockException, HttpHeaders.EMPTY, webRequest); } @@ -129,27 +130,24 @@ private ResponseEntity handleGeneralExceptionInternal(Exception e, Respo } // EventLockException에 대한 client 응답 객체를 생성하는 메서드 - private ResponseEntity handleEventLockExceptionInternal(EventLockException e, HttpHeaders headers, WebRequest webRequest) { + private ModelAndView handleEventLockExceptionInternal(EventLockException e, HttpHeaders headers, WebRequest webRequest) { log.error("EventLockException captured in ExceptionAdvice", e); String redissonKeyName = e.getRedissonKeyName(); - ResponseDto body = null; + ModelAndView modelAndView = new ModelAndView(); -// if (redissonKeyName.contains("FCFS")) -// body = ResponseDto.onSuccess(new FcfsFailResponseDtoDto(1)); + if (redissonKeyName.contains("FCFS")){ + + modelAndView.setViewName("redirect:/fcfs/result"); + modelAndView.addObject("fcfsWin", false); + } //TODO // DRAW 관련 예외일 경우, body 구성하는 코드 필요 - return super.handleExceptionInternal( - e, - body, - headers, - HttpStatus.OK, - webRequest - ); + return modelAndView; } // ConstraintViolationException에 대한 client 응답 객체를 생성하는 메서드 diff --git a/src/main/java/com/softeer/backend/global/config/redis/RedisConfig.java b/src/main/java/com/softeer/backend/global/config/redis/RedisConfig.java index 1b053a8c..5c21b033 100644 --- a/src/main/java/com/softeer/backend/global/config/redis/RedisConfig.java +++ b/src/main/java/com/softeer/backend/global/config/redis/RedisConfig.java @@ -38,6 +38,10 @@ public RedisTemplate redisTemplateForInteger(RedisConnectionFac template.setValueSerializer(new GenericToStringSerializer<>(Integer.class)); + template.setHashKeySerializer(new StringRedisSerializer()); + + template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class)); + return template; } diff --git a/src/main/java/com/softeer/backend/global/config/web/WebMvcConfig.java b/src/main/java/com/softeer/backend/global/config/web/WebMvcConfig.java index efe1d7a3..ca089431 100644 --- a/src/main/java/com/softeer/backend/global/config/web/WebMvcConfig.java +++ b/src/main/java/com/softeer/backend/global/config/web/WebMvcConfig.java @@ -58,7 +58,8 @@ public void addInterceptors(InterceptorRegistry registry) { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("https://softeer.site", "http://localhost:5173", "https://softeer.shop") // 허용할 도메인 설정 + .allowedOrigins("https://softeer.site", "http://localhost:5173", "https://softeer.shop", + "https://d3qmq1ffhp5il9.cloudfront.net") // 허용할 도메인 설정 .allowedMethods("OPTIONS", "GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드 설정 .allowedHeaders("Content-Type", "Authorization", "Authorization-Refresh") // 허용할 헤더 설정 .exposedHeaders("Authorization", "Authorization-Refresh") // 클라이언트에 노출할 헤더 설정 diff --git a/src/main/java/com/softeer/backend/global/scheduler/DbInsertScheduler.java b/src/main/java/com/softeer/backend/global/scheduler/DbInsertScheduler.java index e154cc91..536969db 100644 --- a/src/main/java/com/softeer/backend/global/scheduler/DbInsertScheduler.java +++ b/src/main/java/com/softeer/backend/global/scheduler/DbInsertScheduler.java @@ -12,6 +12,7 @@ import com.softeer.backend.global.common.code.status.ErrorStatus; import com.softeer.backend.global.common.constant.RedisKeyPrefix; import com.softeer.backend.global.util.EventLockRedisUtil; +import com.softeer.backend.global.util.FcfsRedisUtil; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -21,6 +22,8 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledFuture; @@ -31,6 +34,7 @@ public class DbInsertScheduler { private final ThreadPoolTaskScheduler taskScheduler; private final EventLockRedisUtil eventLockRedisUtil; + private final FcfsRedisUtil fcfsRedisUtil; private final FcfsSettingManager fcfsSettingManager; private final DrawSettingManager drawSettingManager; private final EventParticipationRepository eventParticipationRepository; @@ -66,26 +70,32 @@ protected void updateFcfsSetting() { int drawParticipantCount = 0; if(fcfsSettingManager.getRoundForScheduler(now)!=-1){ + fcfsSettingManager.setFcfsClosed(false); + int round = fcfsSettingManager.getRoundForScheduler(now); - Set participantIds = eventLockRedisUtil.getAllDataAsSet(RedisKeyPrefix.FCFS_LOCK_PREFIX.getPrefix() + round); - participantIds.forEach((userId) -> { + Map participantIds = fcfsRedisUtil.getHashEntries(RedisKeyPrefix.FCFS_CODE_USERID_PREFIX.getPrefix() + round); + participantIds.forEach((code, userId) -> { User user = userRepository.findById(userId) .orElseThrow(() -> { log.error("user not found in saveFcfsWinners method."); return new UserException(ErrorStatus._NOT_FOUND); }); + Fcfs fcfs = Fcfs.builder() .user(user) .round(round) + .code(code) .build(); fcfsRepository.save(fcfs); }); - fcfsParticipantCount += eventLockRedisUtil.getData(RedisKeyPrefix.FCFS_PARTICIPANT_COUNT_PREFIX.getPrefix() + round); + fcfsParticipantCount += fcfsRedisUtil.getValue(RedisKeyPrefix.FCFS_PARTICIPANT_COUNT_PREFIX.getPrefix() + round); - eventLockRedisUtil.deleteData(RedisKeyPrefix.FCFS_PARTICIPANT_COUNT_PREFIX.getPrefix() + round); - eventLockRedisUtil.deleteData(RedisKeyPrefix.FCFS_LOCK_PREFIX.getPrefix() + round); + fcfsRedisUtil.clearValue(RedisKeyPrefix.FCFS_PARTICIPANT_COUNT_PREFIX.getPrefix() + round); + fcfsRedisUtil.clearIntegerSet(RedisKeyPrefix.FCFS_LOCK_PREFIX.getPrefix() + round); + fcfsRedisUtil.clearStringSet(RedisKeyPrefix.FCFS_CODE_PREFIX.getPrefix() + round); + fcfsRedisUtil.clearHash(RedisKeyPrefix.FCFS_CODE_USERID_PREFIX.getPrefix() + round); } // TODO: drawParticipantCount에 추첨 이벤트 참가자 수 할당하기 diff --git a/src/main/java/com/softeer/backend/global/util/FcfsRedisUtil.java b/src/main/java/com/softeer/backend/global/util/FcfsRedisUtil.java new file mode 100644 index 00000000..3469ff85 --- /dev/null +++ b/src/main/java/com/softeer/backend/global/util/FcfsRedisUtil.java @@ -0,0 +1,80 @@ +package com.softeer.backend.global.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.*; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class FcfsRedisUtil { + + private final StringRedisTemplate stringRedisTemplate; + private final RedisTemplate integerRedisTemplate; + + public void addToIntegerSet(String key, Integer value) { + integerRedisTemplate.opsForSet().add(key, value); + } + + public void addToStringSet(String key, String value) { + stringRedisTemplate.opsForSet().add(key, value); + } + + public void addToHash(String key, String field, Integer value) { + integerRedisTemplate.opsForHash().put(key, field, value); + } + + public void incrementValue(String key){ + integerRedisTemplate.opsForValue().increment(key); + } + + public Long getIntegerSetSize(String key) { + return integerRedisTemplate.opsForSet().size(key); + } + + public boolean isValueInIntegerSet(String key, Integer value) { + return Boolean.TRUE.equals(integerRedisTemplate.opsForSet().isMember(key, value)); + } + + public boolean isValueInStringSet(String key, String value) { + return Boolean.TRUE.equals(stringRedisTemplate.opsForSet().isMember(key, value)); + } + + public Map getHashEntries(String key) { + Map entries = integerRedisTemplate.opsForHash().entries(key); + Map result = new HashMap<>(); + + for (Map.Entry entry : entries.entrySet()) { + String mapKey = (String) entry.getKey(); + Integer mapValue = (Integer) entry.getValue(); + result.put(mapKey, mapValue); + } + + return result; + } + + public Integer getValue(String key) { + return integerRedisTemplate.opsForValue().get(key); + } + + public void clearIntegerSet(String key) { + integerRedisTemplate.delete(key); + } + + public void clearStringSet(String key) { + stringRedisTemplate.delete(key); + } + + public void clearHash(String key) { + integerRedisTemplate.delete(key); + } + + public void clearValue(String key) { + stringRedisTemplate.delete(key); + } + + + +}