From de1ef930ca99dfe0750b489e021f6b7fe930a7ab Mon Sep 17 00:00:00 2001 From: Jinwoo Park Date: Mon, 12 Aug 2024 10:16:33 +0900 Subject: [PATCH] =?UTF-8?q?refactor=20:=20=20Admin=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=9D=84=20AdminFindingGame,=20AdminRacingGame=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20(CC-132)=20(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor : Admin 도메인을 AdminFindingGame, AdminRacingGame 도메인으로 분리 (CC-132) * feat : S3 이미지 업로드 에러코드 추가 (CC-132) * rename : HealthTestController 의 패키지명을 api로 변경 (CC-132) * feat : S3 이미지 업로드 응답/요청 객체 생성 (CC-132) * feat : S3 이미지 api에 ResponseEntity 추가 (CC-132) * teat : S3 이미지 업로드 실패 테스트코드 (CC-132) * teat : S3 이미지 업로드 성공 테스트코드 작성 (CC-132) * teat : 테스트코드 실행을 위한 환경 구성 (CC-132) * teat : 테스트코드 실행을 위한 환경 구성 삭제 (CC-132) * test : S3 이미지 업로드 성공 테스트 코드 작성 (CC-132) * refactor : admin 디렉터리 내부로 통합 (CC-132) --- .../admin/api/AdminFindingGameController.java | 52 +++++++++ ...er.java => AdminRacingGameController.java} | 52 ++------- .../caecae/admin/api/S3Controller.java | 21 ++-- .../domain/dto/request/S3RequestDto.java | 11 ++ .../response/RacingGameWinnerResponseDto.java | 8 +- .../domain/dto/response/S3ResponseDto.java | 10 ++ .../exception/AdminFindingGameException.java | 14 +++ .../AdminFindingGameExceptionHandler.java | 21 ++++ ...ion.java => AdminRacingGameException.java} | 4 +- ...a => AdminRacingGameExceptionHandler.java} | 6 +- ...vice.java => AdminFindingGameService.java} | 82 +------------- .../admin/service/AdminRacingGameService.java | 102 ++++++++++++++++++ .../utils => admin/service}/S3Service.java | 45 +++++--- .../HealthTestController.java | 3 +- .../caecae/global/enums/ErrorCode.java | 2 + .../ai/softeer/caecae/global/utils/.gitkeep | 0 .../caecae/admin/service/S3ServiceTest.java | 86 +++++++++++++++ .../controller/HealthTestControllerTest.java | 2 +- .../caecae/global/utils/S3ServiceTest.java | 68 ------------ 19 files changed, 368 insertions(+), 221 deletions(-) create mode 100644 src/main/java/ai/softeer/caecae/admin/api/AdminFindingGameController.java rename src/main/java/ai/softeer/caecae/admin/api/{AdminController.java => AdminRacingGameController.java} (54%) create mode 100644 src/main/java/ai/softeer/caecae/admin/domain/dto/request/S3RequestDto.java create mode 100644 src/main/java/ai/softeer/caecae/admin/domain/dto/response/S3ResponseDto.java create mode 100644 src/main/java/ai/softeer/caecae/admin/domain/exception/AdminFindingGameException.java create mode 100644 src/main/java/ai/softeer/caecae/admin/domain/exception/AdminFindingGameExceptionHandler.java rename src/main/java/ai/softeer/caecae/admin/domain/exception/{AdminException.java => AdminRacingGameException.java} (67%) rename src/main/java/ai/softeer/caecae/admin/domain/exception/{AdminExceptionHandler.java => AdminRacingGameExceptionHandler.java} (82%) rename src/main/java/ai/softeer/caecae/admin/service/{AdminService.java => AdminFindingGameService.java} (61%) create mode 100644 src/main/java/ai/softeer/caecae/admin/service/AdminRacingGameService.java rename src/main/java/ai/softeer/caecae/{global/utils => admin/service}/S3Service.java (52%) rename src/main/java/ai/softeer/caecae/global/{controller => api}/HealthTestController.java (83%) create mode 100644 src/main/java/ai/softeer/caecae/global/utils/.gitkeep create mode 100644 src/test/java/ai/softeer/caecae/admin/service/S3ServiceTest.java diff --git a/src/main/java/ai/softeer/caecae/admin/api/AdminFindingGameController.java b/src/main/java/ai/softeer/caecae/admin/api/AdminFindingGameController.java new file mode 100644 index 0000000..382a8ad --- /dev/null +++ b/src/main/java/ai/softeer/caecae/admin/api/AdminFindingGameController.java @@ -0,0 +1,52 @@ +package ai.softeer.caecae.admin.api; + +import ai.softeer.caecae.admin.domain.dto.request.FindingGameDailyAnswerRequestDto; +import ai.softeer.caecae.admin.domain.dto.response.FindingGameDailyAnswerResponseDto; +import ai.softeer.caecae.admin.service.AdminFindingGameService; +import ai.softeer.caecae.findinggame.service.FindingGameService; +import ai.softeer.caecae.global.dto.response.SuccessResponse; +import ai.softeer.caecae.global.enums.SuccessCode; +import ai.softeer.caecae.racinggame.domain.dto.request.RegisterFindingGamePeriodRequestDto; +import ai.softeer.caecae.racinggame.domain.dto.response.RegisterFindingGamePeriodResponseDto; +import ai.softeer.caecae.racinggame.service.RacingGameInfoService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/admin/finding") +@RequiredArgsConstructor +public class AdminFindingGameController { + private final RacingGameInfoService racingGameService; + private final FindingGameService findingGameService; + private final AdminFindingGameService adminFindingGameService; + + /** + * 어드민이 숨은캐스퍼찾기 게임 기간을 등록하는 api + * + * @param req 게임 시작 날짜 + * @return 등록된 게임 시작 날짜, 종료 날짜(+6일) + */ + @PostMapping("/period") + public ResponseEntity> + registerFindingGamePeriod(@RequestBody RegisterFindingGamePeriodRequestDto req) { + RegisterFindingGamePeriodResponseDto res = adminFindingGameService.registerFindingGamePeriod(req); + return SuccessResponse.of(SuccessCode.CREATED, res); + } + + /** + * 어드민이 숨은캐스퍼찾기 날짜별 정답을 등록하는 api + * + * @param req + * @return + */ + @PostMapping("/answer") + public ResponseEntity> + saveFindingGameDailyInfo(@RequestBody FindingGameDailyAnswerRequestDto req) { + FindingGameDailyAnswerResponseDto res = adminFindingGameService.saveFindingGameDailyAnswer(req); + return SuccessResponse.of(SuccessCode.OK, res); + } +} diff --git a/src/main/java/ai/softeer/caecae/admin/api/AdminController.java b/src/main/java/ai/softeer/caecae/admin/api/AdminRacingGameController.java similarity index 54% rename from src/main/java/ai/softeer/caecae/admin/api/AdminController.java rename to src/main/java/ai/softeer/caecae/admin/api/AdminRacingGameController.java index 84120d0..764289b 100644 --- a/src/main/java/ai/softeer/caecae/admin/api/AdminController.java +++ b/src/main/java/ai/softeer/caecae/admin/api/AdminRacingGameController.java @@ -1,16 +1,12 @@ package ai.softeer.caecae.admin.api; import ai.softeer.caecae.admin.domain.dto.response.RacingGameWinnerResponseDto; -import ai.softeer.caecae.admin.domain.dto.request.FindingGameDailyAnswerRequestDto; -import ai.softeer.caecae.admin.domain.dto.response.FindingGameDailyAnswerResponseDto; -import ai.softeer.caecae.admin.service.AdminService; +import ai.softeer.caecae.admin.service.AdminRacingGameService; import ai.softeer.caecae.findinggame.service.FindingGameService; import ai.softeer.caecae.global.dto.response.SuccessResponse; import ai.softeer.caecae.global.enums.SuccessCode; -import ai.softeer.caecae.racinggame.domain.dto.request.RegisterFindingGamePeriodRequestDto; import ai.softeer.caecae.racinggame.domain.dto.request.RegisterRacingGameInfoRequestDto; import ai.softeer.caecae.racinggame.domain.dto.response.RacingGameInfoResponseDto; -import ai.softeer.caecae.racinggame.domain.dto.response.RegisterFindingGamePeriodResponseDto; import ai.softeer.caecae.racinggame.domain.dto.response.RegisterRacingGameInfoResponseDto; import ai.softeer.caecae.racinggame.service.RacingGameInfoService; import lombok.RequiredArgsConstructor; @@ -20,19 +16,19 @@ import java.util.List; @RestController -@RequestMapping("/api/admin") +@RequestMapping("/api/admin/racing") @RequiredArgsConstructor -public class AdminController { +public class AdminRacingGameController { private final RacingGameInfoService racingGameService; private final FindingGameService findingGameService; - private final AdminService adminService; + private final AdminRacingGameService adminRacingGameService; /** * 관리자가 등록된 레이싱게임의 정보를 조회하는 로직 * * @return 레이싱게임 정보 */ - @GetMapping("/racing/period") + @GetMapping("/period") public ResponseEntity> getRacingGameInfo() { RacingGameInfoResponseDto racingGameInfoDto = racingGameService.getRacingGameInfo(); return SuccessResponse.of(SuccessCode.OK, racingGameInfoDto); @@ -43,7 +39,7 @@ public ResponseEntity> getRacingGameI * * @param req 레이싱게임 정보 */ - @PostMapping("/racing/period") + @PostMapping("/period") public ResponseEntity> registerRacingGame( @RequestBody RegisterRacingGameInfoRequestDto req ) { @@ -56,9 +52,9 @@ public ResponseEntity> regist * * @return 당첨자 리스트 */ - @PostMapping("/racing/winners") + @PostMapping("/winners") public ResponseEntity>> drawRacingGameWinner() { - return SuccessResponse.of(SuccessCode.CREATED, adminService.drawRacingGameWinner()); + return SuccessResponse.of(SuccessCode.CREATED, adminRacingGameService.drawRacingGameWinner()); } /** @@ -66,34 +62,8 @@ public ResponseEntity>> drawRa * * @return 당첨자 리스트 */ - @GetMapping("/racing/winners") + @GetMapping("/winners") public ResponseEntity>> getRacingGameWinner() { - return SuccessResponse.of(SuccessCode.OK, adminService.getRacingGameWinner()); + return SuccessResponse.of(SuccessCode.OK, adminRacingGameService.getRacingGameWinner()); } - - /** - * 어드민이 숨은캐스퍼찾기 게임 기간을 등록하는 api - * - * @param req 게임 시작 날짜 - * @return 등록된 게임 시작 날짜, 종료 날짜(+6일) - */ - @PostMapping("/finding/period") - public ResponseEntity> - registerFindingGamePeriod(@RequestBody RegisterFindingGamePeriodRequestDto req) { - RegisterFindingGamePeriodResponseDto res = adminService.registerFindingGamePeriod(req); - return SuccessResponse.of(SuccessCode.CREATED, res); - } - - /** - * 어드민이 숨은캐스퍼찾기 날짜별 정답을 등록하는 api - * - * @param req - * @return - */ - @PostMapping("/finding/answer") - public ResponseEntity> - saveFindingGameDailyInfo(@RequestBody FindingGameDailyAnswerRequestDto req) { - FindingGameDailyAnswerResponseDto res = adminService.saveFindingGameDailyAnswer(req); - return SuccessResponse.of(SuccessCode.OK, res); - } -} +} \ No newline at end of file diff --git a/src/main/java/ai/softeer/caecae/admin/api/S3Controller.java b/src/main/java/ai/softeer/caecae/admin/api/S3Controller.java index bf658ac..7dd1666 100644 --- a/src/main/java/ai/softeer/caecae/admin/api/S3Controller.java +++ b/src/main/java/ai/softeer/caecae/admin/api/S3Controller.java @@ -1,21 +1,30 @@ package ai.softeer.caecae.admin.api; -import ai.softeer.caecae.global.utils.S3Service; +import ai.softeer.caecae.admin.domain.dto.response.S3ResponseDto; +import ai.softeer.caecae.admin.service.S3Service; +import ai.softeer.caecae.global.dto.response.SuccessResponse; +import ai.softeer.caecae.global.enums.SuccessCode; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor +@RequestMapping("/api/admin/s3") public class S3Controller { + // TBD : Admin의 컨트롤러들, 이미지 업로드 등의 기능을 일반 사용자가 이용 못하게 막아야 하지 않는가? private final S3Service s3Service; - @PostMapping("/api/s3") - public String upload(@RequestParam("file") MultipartFile file) { - String filePath = s3Service.uploadFile(file); - return filePath + "created!"; - //TODO : ResponseEntity 생성하기 + @PostMapping("") + public ResponseEntity> upload( + @RequestParam("file") MultipartFile file, + String directory + ) { + S3ResponseDto res = s3Service.uploadFile(file, directory); + return SuccessResponse.of(SuccessCode.CREATED, res); } } diff --git a/src/main/java/ai/softeer/caecae/admin/domain/dto/request/S3RequestDto.java b/src/main/java/ai/softeer/caecae/admin/domain/dto/request/S3RequestDto.java new file mode 100644 index 0000000..1973ae1 --- /dev/null +++ b/src/main/java/ai/softeer/caecae/admin/domain/dto/request/S3RequestDto.java @@ -0,0 +1,11 @@ +package ai.softeer.caecae.admin.domain.dto.request; + +import lombok.Builder; +import org.springframework.web.multipart.MultipartFile; + +// S3에 이미지를 업로드 할 때 사용하는 요청 객체 +@Builder +public record S3RequestDto( + MultipartFile file +) { +} diff --git a/src/main/java/ai/softeer/caecae/admin/domain/dto/response/RacingGameWinnerResponseDto.java b/src/main/java/ai/softeer/caecae/admin/domain/dto/response/RacingGameWinnerResponseDto.java index 8ad5795..c79c485 100644 --- a/src/main/java/ai/softeer/caecae/admin/domain/dto/response/RacingGameWinnerResponseDto.java +++ b/src/main/java/ai/softeer/caecae/admin/domain/dto/response/RacingGameWinnerResponseDto.java @@ -4,9 +4,9 @@ @Builder public record RacingGameWinnerResponseDto( - int ranking, - String phone, - double distance, - Integer selection + int ranking, + String phone, + double distance, + Integer selection ) { } diff --git a/src/main/java/ai/softeer/caecae/admin/domain/dto/response/S3ResponseDto.java b/src/main/java/ai/softeer/caecae/admin/domain/dto/response/S3ResponseDto.java new file mode 100644 index 0000000..f6b6771 --- /dev/null +++ b/src/main/java/ai/softeer/caecae/admin/domain/dto/response/S3ResponseDto.java @@ -0,0 +1,10 @@ +package ai.softeer.caecae.admin.domain.dto.response; + +import lombok.Builder; + +// S3에 이미지를 업로드 한 후 받는 응답 객체 +@Builder +public record S3ResponseDto( + String imageUrl +) { +} diff --git a/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminFindingGameException.java b/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminFindingGameException.java new file mode 100644 index 0000000..6b13d72 --- /dev/null +++ b/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminFindingGameException.java @@ -0,0 +1,14 @@ +package ai.softeer.caecae.admin.domain.exception; + +import ai.softeer.caecae.global.enums.ErrorCode; +import lombok.Getter; + +@Getter +public class AdminFindingGameException extends RuntimeException { + private final ErrorCode errorCode; + + public AdminFindingGameException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminFindingGameExceptionHandler.java b/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminFindingGameExceptionHandler.java new file mode 100644 index 0000000..caabf66 --- /dev/null +++ b/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminFindingGameExceptionHandler.java @@ -0,0 +1,21 @@ +package ai.softeer.caecae.admin.domain.exception; + +import ai.softeer.caecae.global.dto.response.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +/** + * FindingGame 도메인에서 에러를 핸들링하여 HttpResponse 를 반환하는 핸들러 + */ +@Slf4j +@ControllerAdvice +public class AdminFindingGameExceptionHandler { + // FindingGameException 에 대한 에러 핸들링 + @ExceptionHandler(value = AdminFindingGameException.class) + public ResponseEntity handleFindingGameException(AdminFindingGameException adminException) { + log.error(adminException.getMessage(), adminException); + return ErrorResponse.of(adminException.getErrorCode()); + } +} diff --git a/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminException.java b/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminRacingGameException.java similarity index 67% rename from src/main/java/ai/softeer/caecae/admin/domain/exception/AdminException.java rename to src/main/java/ai/softeer/caecae/admin/domain/exception/AdminRacingGameException.java index 832331f..9c03d2c 100644 --- a/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminException.java +++ b/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminRacingGameException.java @@ -4,10 +4,10 @@ import lombok.Getter; @Getter -public class AdminException extends RuntimeException { +public class AdminRacingGameException extends RuntimeException { private final ErrorCode errorCode; - public AdminException(ErrorCode errorCode) { + public AdminRacingGameException(ErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; } diff --git a/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminExceptionHandler.java b/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminRacingGameExceptionHandler.java similarity index 82% rename from src/main/java/ai/softeer/caecae/admin/domain/exception/AdminExceptionHandler.java rename to src/main/java/ai/softeer/caecae/admin/domain/exception/AdminRacingGameExceptionHandler.java index bb3078a..5b926f4 100644 --- a/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminExceptionHandler.java +++ b/src/main/java/ai/softeer/caecae/admin/domain/exception/AdminRacingGameExceptionHandler.java @@ -11,10 +11,10 @@ */ @Slf4j @ControllerAdvice -public class AdminExceptionHandler { +public class AdminRacingGameExceptionHandler { // RacingGameException 에 대한 에러 핸들링 - @ExceptionHandler(value = AdminException.class) - public ResponseEntity handleRacingGameException(AdminException adminException) { + @ExceptionHandler(value = AdminRacingGameException.class) + public ResponseEntity handleRacingGameException(AdminRacingGameException adminException) { log.error(adminException.getMessage(), adminException); return ErrorResponse.of(adminException.getErrorCode()); } diff --git a/src/main/java/ai/softeer/caecae/admin/service/AdminService.java b/src/main/java/ai/softeer/caecae/admin/service/AdminFindingGameService.java similarity index 61% rename from src/main/java/ai/softeer/caecae/admin/service/AdminService.java rename to src/main/java/ai/softeer/caecae/admin/service/AdminFindingGameService.java index 2089e69..2bfd4a3 100644 --- a/src/main/java/ai/softeer/caecae/admin/service/AdminService.java +++ b/src/main/java/ai/softeer/caecae/admin/service/AdminFindingGameService.java @@ -1,10 +1,9 @@ package ai.softeer.caecae.admin.service; +import ai.softeer.caecae.admin.domain.exception.AdminFindingGameException; import ai.softeer.caecae.admin.domain.dto.FindingGameAnswerDto; import ai.softeer.caecae.admin.domain.dto.request.FindingGameDailyAnswerRequestDto; import ai.softeer.caecae.admin.domain.dto.response.FindingGameDailyAnswerResponseDto; -import ai.softeer.caecae.admin.domain.dto.response.RacingGameWinnerResponseDto; -import ai.softeer.caecae.admin.domain.exception.AdminException; import ai.softeer.caecae.findinggame.domain.entity.FindingGame; import ai.softeer.caecae.findinggame.domain.entity.FindingGameAnswer; import ai.softeer.caecae.findinggame.domain.enums.AnswerType; @@ -13,101 +12,26 @@ import ai.softeer.caecae.global.enums.ErrorCode; import ai.softeer.caecae.racinggame.domain.dto.request.RegisterFindingGamePeriodRequestDto; import ai.softeer.caecae.racinggame.domain.dto.response.RegisterFindingGamePeriodResponseDto; -import ai.softeer.caecae.racinggame.domain.entity.RacingGameParticipant; -import ai.softeer.caecae.racinggame.domain.entity.RacingGameWinner; import ai.softeer.caecae.racinggame.repository.RacingGameInfoRepository; import ai.softeer.caecae.racinggame.repository.RacingGameRepository; import ai.softeer.caecae.racinggame.repository.RacingGameWinnerRepository; -import ai.softeer.caecae.user.domain.entity.User; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.time.LocalDate; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; @Service @RequiredArgsConstructor -public class AdminService { +public class AdminFindingGameService { private final RacingGameInfoRepository raceGameInfoRepository; private final RacingGameRepository racingGameRepository; private final RacingGameWinnerRepository racingGameWinnerRepository; private final FindingGameDbRepository findingGameDbRepository; private final FindingGameAnswerDbRepository findingGameAnswerDbRepository; - /** - * 당첨자를 뽑는 서비스 로직 - * - * @return 당첨자 리스트 - */ - @Transactional - public List drawRacingGameWinner() { - List racingGameWinnerResponseDtoList = new ArrayList<>(); - List participants = racingGameRepository.findAllByAdjustedDistance(315.0); - List winners = new ArrayList<>(); - - int n = participants.size(); - double[] accumulateSector = {0.2, 0.8, 1.5, 3.0, 5.0, 10.0, 20.0, 35.0, 50.0, 1e9}; // 1e9: inf를 의미 - int[] weight = {250, 180, 125, 100, 60, 30, 15, 10, 7, 3}; - int selectionWeight = 10, weightSum = Arrays.stream(weight).sum() + 10 * weight.length; - int currentSector = 0, idx = 0; // 현 순위가 속하는 구간을 가르키는 포인터, 현 참여자 인덱스 - double accumulatedPercentPoint = 100.0 / participants.size(), accumulatedPercent = 0; - int[] arr = new int[participants.size()]; // 각 참여자의 가중치 배열 - // 가중치 배열에 가중치를 넣어주는 과정 - for (RacingGameParticipant p : participants) { - accumulatedPercent += accumulatedPercentPoint; - while (accumulatedPercent > accumulateSector[currentSector] + 0.01) currentSector++; - arr[idx++] = weight[currentSector] + (p.getSelection() != 0 ? selectionWeight : 0); - } - // N명 중에서 한 명을 선택한 후, 그 사람을 가중치 / 전체 가중치 확률로 당첨자로 만든다. 이를 당첨인원수만큼 반복 - int drawNumber = Math.min(315, participants.size()), ranking = 1; // TODO: 315 Global 변수화 - while (ranking <= drawNumber) { - int cur = (int) (Math.random() * n + 0.5) % n; - if (arr[cur] < 0) continue; - double poss = Math.random(); - if (poss <= (double) arr[cur] / weightSum) { - RacingGameParticipant p = participants.get(cur); - User user = p.getUser(); - racingGameWinnerResponseDtoList.add(RacingGameWinnerResponseDto.builder() - .ranking(ranking) - .phone(user.getPhone()) - .distance(p.getDistance()) - .selection(p.getSelection()) - .build()); - winners.add(RacingGameWinner.builder() - .userId(p.getUserId()) - .ranking(ranking) - .build()); - arr[cur] = -1; - ranking++; - } - } - // TODO: 수학적으로 보이기 + 더 나은 방법 생각해보기? - racingGameWinnerRepository.saveAll(winners); - return racingGameWinnerResponseDtoList; - } - - /** - * 당첨자 리스트를 가져오는 서비스 로직 - * - * @return 당첨자 리스트 - */ - public List getRacingGameWinner() { - List winners = racingGameWinnerRepository.findAllByOrderByRankingAsc(); - List WinnerResponseDtoList = new ArrayList<>(); - for (RacingGameWinner winner : winners) { - RacingGameParticipant p = racingGameRepository.findById(winner.getUserId()).get(); - WinnerResponseDtoList.add(RacingGameWinnerResponseDto.builder() - .ranking(winner.getRanking()) - .phone(winner.getUser().getPhone()) - .distance(p.getDistance()) - .selection(p.getSelection()) - .build()); - } - return WinnerResponseDtoList; - } /** * 숨은캐스퍼찾기 게임 날짜별 정답 정보, 게임시작시간, 종료시간, 당첨인원수를 업데이트하는 로직 @@ -124,7 +48,7 @@ public FindingGameDailyAnswerResponseDto saveFindingGameDailyAnswer( FindingGame findingGame = findingGameDbRepository // FindingGame 테이블의 1~7번째 열에 데이터가 들어간다고 가정하고, dayOfEvent 를 id로 활용하여 조회함 // 좋은 방식은 아닌 것 같아서, 추후 어떻게 할지 논의 하면 좋겠음. - .findById(req.dayOfEvent()).orElseThrow(() -> new AdminException(ErrorCode.FINDING_GAME_OF_DAY_NOT_FOUND)); + .findById(req.dayOfEvent()).orElseThrow(() -> new AdminFindingGameException(ErrorCode.FINDING_GAME_OF_DAY_NOT_FOUND)); // fingingGame의 시작시간, 종료시간, 당첨자수, 정답타입 새로운 정보로 업데이트 findingGame.updateFindingGamePeriod( diff --git a/src/main/java/ai/softeer/caecae/admin/service/AdminRacingGameService.java b/src/main/java/ai/softeer/caecae/admin/service/AdminRacingGameService.java new file mode 100644 index 0000000..30b7274 --- /dev/null +++ b/src/main/java/ai/softeer/caecae/admin/service/AdminRacingGameService.java @@ -0,0 +1,102 @@ +package ai.softeer.caecae.admin.service; + +import ai.softeer.caecae.admin.domain.dto.response.RacingGameWinnerResponseDto; +import ai.softeer.caecae.findinggame.repository.FindingGameAnswerDbRepository; +import ai.softeer.caecae.findinggame.repository.FindingGameDbRepository; +import ai.softeer.caecae.racinggame.domain.entity.RacingGameParticipant; +import ai.softeer.caecae.racinggame.domain.entity.RacingGameWinner; +import ai.softeer.caecae.racinggame.repository.RacingGameInfoRepository; +import ai.softeer.caecae.racinggame.repository.RacingGameRepository; +import ai.softeer.caecae.racinggame.repository.RacingGameWinnerRepository; +import ai.softeer.caecae.user.domain.entity.User; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class AdminRacingGameService { + private final RacingGameInfoRepository raceGameInfoRepository; + private final RacingGameRepository racingGameRepository; + private final RacingGameWinnerRepository racingGameWinnerRepository; + private final FindingGameDbRepository findingGameDbRepository; + private final FindingGameAnswerDbRepository findingGameAnswerDbRepository; + + /** + * 당첨자를 뽑는 서비스 로직 + * + * @return 당첨자 리스트 + */ + @Transactional + public List drawRacingGameWinner() { + List racingGameWinnerResponseDtoList = new ArrayList<>(); + List participants = racingGameRepository.findAllByAdjustedDistance(315.0); + List winners = new ArrayList<>(); + + int n = participants.size(); + double[] accumulateSector = {0.2, 0.8, 1.5, 3.0, 5.0, 10.0, 20.0, 35.0, 50.0, 1e9}; // 1e9: inf를 의미 + int[] weight = {250, 180, 125, 100, 60, 30, 15, 10, 7, 3}; + int selectionWeight = 10, weightSum = Arrays.stream(weight).sum() + 10 * weight.length; + int currentSector = 0, idx = 0; // 현 순위가 속하는 구간을 가르키는 포인터, 현 참여자 인덱스 + double accumulatedPercentPoint = 100.0 / participants.size(), accumulatedPercent = 0; + int[] arr = new int[participants.size()]; // 각 참여자의 가중치 배열 + // 가중치 배열에 가중치를 넣어주는 과정 + for (RacingGameParticipant p : participants) { + accumulatedPercent += accumulatedPercentPoint; + while (accumulatedPercent > accumulateSector[currentSector] + 0.01) currentSector++; + arr[idx++] = weight[currentSector] + (p.getSelection() != 0 ? selectionWeight : 0); + } + // N명 중에서 한 명을 선택한 후, 그 사람을 가중치 / 전체 가중치 확률로 당첨자로 만든다. 이를 당첨인원수만큼 반복 + int drawNumber = Math.min(315, participants.size()), ranking = 1; // TODO: 315 Global 변수화 + while (ranking <= drawNumber) { + int cur = (int) (Math.random() * n + 0.5) % n; + if (arr[cur] < 0) continue; + double poss = Math.random(); + if (poss <= (double) arr[cur] / weightSum) { + RacingGameParticipant p = participants.get(cur); + User user = p.getUser(); + racingGameWinnerResponseDtoList.add(RacingGameWinnerResponseDto.builder() + .ranking(ranking) + .phone(user.getPhone()) + .distance(p.getDistance()) + .selection(p.getSelection()) + .build()); + winners.add(RacingGameWinner.builder() + .userId(p.getUserId()) + .ranking(ranking) + .build()); + arr[cur] = -1; + ranking++; + } + } + // TODO: 수학적으로 보이기 + 더 나은 방법 생각해보기? + racingGameWinnerRepository.saveAll(winners); + return racingGameWinnerResponseDtoList; + } + + /** + * 당첨자 리스트를 가져오는 서비스 로직 + * + * @return 당첨자 리스트 + */ + public List getRacingGameWinner() { + List winners = racingGameWinnerRepository.findAllByOrderByRankingAsc(); + List WinnerResponseDtoList = new ArrayList<>(); + for (RacingGameWinner winner : winners) { + RacingGameParticipant p = racingGameRepository.findById(winner.getUserId()).get(); + WinnerResponseDtoList.add(RacingGameWinnerResponseDto.builder() + .ranking(winner.getRanking()) + .phone(winner.getUser().getPhone()) + .distance(p.getDistance()) + .selection(p.getSelection()) + .build()); + } + return WinnerResponseDtoList; + } + + +} diff --git a/src/main/java/ai/softeer/caecae/global/utils/S3Service.java b/src/main/java/ai/softeer/caecae/admin/service/S3Service.java similarity index 52% rename from src/main/java/ai/softeer/caecae/global/utils/S3Service.java rename to src/main/java/ai/softeer/caecae/admin/service/S3Service.java index 76763c1..0b6d191 100644 --- a/src/main/java/ai/softeer/caecae/global/utils/S3Service.java +++ b/src/main/java/ai/softeer/caecae/admin/service/S3Service.java @@ -1,8 +1,12 @@ -package ai.softeer.caecae.global.utils; +package ai.softeer.caecae.admin.service; +import ai.softeer.caecae.admin.domain.dto.response.S3ResponseDto; +import ai.softeer.caecae.admin.domain.exception.AdminFindingGameException; +import ai.softeer.caecae.global.enums.ErrorCode; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -11,11 +15,11 @@ import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.UUID; @Slf4j @Service -//TODO : JPA 의존성 추가 후 @Transcational @RequiredArgsConstructor public class S3Service { private final AmazonS3 amazonS3; @@ -23,35 +27,46 @@ public class S3Service { @Value("${spring.s3.bucket}") private String bucket; + private static final List DIRECTORY = List.of("answer", "question"); + + /** * S3에 파일을 업로드 하는 서비스 로직 * - * @param file + * @param file : S3에 업로드 할 멀티파트 파일 * @return 파일 이름 */ - //TODO : JPA 의존성 추가 후 @Transcational - public String uploadFile(MultipartFile file) { - String fileName = createFileName(file.getOriginalFilename()); + @Transactional + public S3ResponseDto uploadFile(MultipartFile file, String directory) { + if (!DIRECTORY.contains(directory)) { + throw new AdminFindingGameException(ErrorCode.S3_INVALID_DIRECTORY_NAME); + } + String fileName = createFileName(file.getOriginalFilename(), directory); + ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(file.getSize()); objectMetadata.setContentType(file.getContentType()); - String filePath; + String imageUrl; + // 파일 업로드 try (InputStream inputStream = file.getInputStream()) { amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)); - // 올린 오브젝트에 대한 s3 url - filePath = amazonS3.getUrl(bucket, fileName).toString(); + imageUrl = amazonS3.getUrl(bucket, fileName).toString(); } catch (IOException e) { - throw new IllegalArgumentException("파일이 없습니다."); + throw new AdminFindingGameException(ErrorCode.INTERNAL_SERVER_ERROR); //TODO : 커스텀 에러 관리하기 } - log.info(filePath, " is successfully created in S3"); - return filePath; + log.info(imageUrl, " is successfully created in S3"); + return S3ResponseDto.builder().imageUrl(imageUrl).build(); } - // caecae+UUID 로 파일 이름 생성하기 - private String createFileName(String fileName) { - return "caecae-" + UUID.randomUUID().toString().concat(getFileExtension(fileName)); + // caecae + UUID 로 파일 이름 생성하기 + private String createFileName(String fileName, String directory) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(directory); + stringBuilder.append("/caecae-"); + stringBuilder.append(UUID.randomUUID().toString().concat(getFileExtension(fileName))); + return stringBuilder.toString(); } // 파일 확장자 추출하기 diff --git a/src/main/java/ai/softeer/caecae/global/controller/HealthTestController.java b/src/main/java/ai/softeer/caecae/global/api/HealthTestController.java similarity index 83% rename from src/main/java/ai/softeer/caecae/global/controller/HealthTestController.java rename to src/main/java/ai/softeer/caecae/global/api/HealthTestController.java index 1739035..613259b 100644 --- a/src/main/java/ai/softeer/caecae/global/controller/HealthTestController.java +++ b/src/main/java/ai/softeer/caecae/global/api/HealthTestController.java @@ -1,9 +1,8 @@ -package ai.softeer.caecae.global.controller; +package ai.softeer.caecae.global.api; import ai.softeer.caecae.global.dto.response.SuccessResponse; import ai.softeer.caecae.global.enums.SuccessCode; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; diff --git a/src/main/java/ai/softeer/caecae/global/enums/ErrorCode.java b/src/main/java/ai/softeer/caecae/global/enums/ErrorCode.java index 52552a9..8f6e62c 100644 --- a/src/main/java/ai/softeer/caecae/global/enums/ErrorCode.java +++ b/src/main/java/ai/softeer/caecae/global/enums/ErrorCode.java @@ -34,6 +34,8 @@ public enum ErrorCode implements BaseCode { */ FINDING_GAME_OF_DAY_NOT_FOUND(-4000, "해당 날짜에 등록된 틀린그림찾기 게임이 존재하지 않습니다.", HttpStatus.NOT_FOUND), + S3_IMAGE_UPLOAD_FAIL(-4001, "S3 이미지 업로드에 실패하였습니다.", HttpStatus.UNPROCESSABLE_ENTITY), + S3_INVALID_DIRECTORY_NAME(-4001, "S3 버킷 디렉토리명이 잘못되었습니다.", HttpStatus.BAD_REQUEST), /** diff --git a/src/main/java/ai/softeer/caecae/global/utils/.gitkeep b/src/main/java/ai/softeer/caecae/global/utils/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/ai/softeer/caecae/admin/service/S3ServiceTest.java b/src/test/java/ai/softeer/caecae/admin/service/S3ServiceTest.java new file mode 100644 index 0000000..db00519 --- /dev/null +++ b/src/test/java/ai/softeer/caecae/admin/service/S3ServiceTest.java @@ -0,0 +1,86 @@ +package ai.softeer.caecae.admin.service; + +import ai.softeer.caecae.admin.domain.dto.response.S3ResponseDto; +import ai.softeer.caecae.admin.domain.exception.AdminFindingGameException; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.PutObjectRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.ResourceUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +//@TestPropertySource(properties = { +// "spring.s3.bucket=caecae" +//}) +class S3ServiceTest { + + @Mock + private AmazonS3 amazonS3; + + @InjectMocks + private S3Service s3Service; + + private MockMultipartFile mockMultipartFile; + + + @BeforeEach + void setUp() throws IOException { + //given + File file = ResourceUtils.getFile("classpath:hyundai.png"); + FileInputStream fileInputStream = new FileInputStream(file); + mockMultipartFile = new MockMultipartFile( + "file", // 파라미터 이름 + file.getName(), // 원본 파일 이름 + "image/png", // 파일 타입 + fileInputStream // 파일 데이터 + ); + } + + @Test + @DisplayName("S3 이미지 업로드시 잘못된 디렉터리 네임 설정") + void uploadFile_실패() throws IOException { + + //when & then + assertThrows(AdminFindingGameException.class, () -> { + s3Service.uploadFile(mockMultipartFile, "Answer"); + }); + } + + @Test + @DisplayName("S3 이미지 업로드 성공") + void uploadFile_성공() throws IOException { + ReflectionTestUtils.setField(s3Service, "bucket", "test-bucket-name"); + + // 성공할 때 반환되는 URL을 모킹 + String expectedUrl = "https://s3.ap-south-1.amazonaws.com/your-bucket-name/caecae-some-uuid.png"; + Mockito.when(amazonS3.getUrl(Mockito.anyString(), Mockito.anyString())).thenReturn(new URL(expectedUrl)); + + //when + S3ResponseDto response = s3Service.uploadFile(mockMultipartFile, "answer"); + + + //then + Mockito.verify(amazonS3).putObject(Mockito.any(PutObjectRequest.class)); + Assertions.assertNotNull(response); + Assertions.assertEquals(expectedUrl, response.imageUrl()); + } +} + +/// + diff --git a/src/test/java/ai/softeer/caecae/global/controller/HealthTestControllerTest.java b/src/test/java/ai/softeer/caecae/global/controller/HealthTestControllerTest.java index dca949f..1bfc4b8 100644 --- a/src/test/java/ai/softeer/caecae/global/controller/HealthTestControllerTest.java +++ b/src/test/java/ai/softeer/caecae/global/controller/HealthTestControllerTest.java @@ -1,12 +1,12 @@ package ai.softeer.caecae.global.controller; +import ai.softeer.caecae.global.api.HealthTestController; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.web.servlet.MockMvc; -import static org.junit.jupiter.api.Assertions.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; diff --git a/src/test/java/ai/softeer/caecae/global/utils/S3ServiceTest.java b/src/test/java/ai/softeer/caecae/global/utils/S3ServiceTest.java index c09fee6..e69de29 100644 --- a/src/test/java/ai/softeer/caecae/global/utils/S3ServiceTest.java +++ b/src/test/java/ai/softeer/caecae/global/utils/S3ServiceTest.java @@ -1,68 +0,0 @@ -package ai.softeer.caecae.global.utils; - -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.util.ResourceUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.net.MalformedURLException; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class S3ServiceTest { - - @Mock - private AmazonS3 amazonS3; - - @InjectMocks - private S3Service s3Service; - - private MockMultipartFile mockMultipartFile; - - @BeforeEach - void setUp() throws IOException { - //given - File file = ResourceUtils.getFile("classpath:hyundai.png"); - FileInputStream fileInputStream = new FileInputStream(file); - mockMultipartFile = new MockMultipartFile( - "file", // 파라미터 이름 - file.getName(), // 원본 파일 이름 - "image/png", // 파일 타입 - fileInputStream // 파일 데이터 - ); - } - - //TODO : 통합테스트를 위한 CICD 과정에서 의존성 주입 - @Disabled - @Test - @DisplayName("멀티파트 파일을 S3에 업로드하고 url을 반환함") - void uploadFile() throws MalformedURLException { - //given - String expectedUrl = "https://test-bucket.s3.amazonaws.com/caecae-uuid.png"; - - when(amazonS3.getUrl(anyString(), anyString())).thenReturn(new java.net.URL(expectedUrl)); - doNothing().when(amazonS3).putObject(any(PutObjectRequest.class)); - - //when - String result = s3Service.uploadFile(mockMultipartFile); - - //then - assertEquals(expectedUrl, result); - verify(amazonS3, times(1)).putObject(any(PutObjectRequest.class)); - } -}