From 5222504bf902de99ef190daf099a3edb46c56bb4 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Fri, 8 Dec 2023 20:09:07 +0900 Subject: [PATCH] =?UTF-8?q?:sparkles:=20Feat=20=EC=9B=90=ED=95=98=EB=8A=94?= =?UTF-8?q?=20=EB=AC=B8=EC=A0=9C=20=EC=85=8B=EC=9D=84=20=ED=92=80=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8A=94=20API=20=EC=B6=94=EA=B0=80=20#59?= =?UTF-8?q?4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CustomTestController.java | 25 +++ .../customTest/request/CustomTestRequest.java | 27 ++++ .../response/CustomTestResponse.java | 14 ++ .../response/CustomTestResponses.java | 16 ++ .../customTest/service/CustomTestService.java | 153 ++++++++++++++++++ .../member/repository/MemberRepository.java | 3 + .../problem/repository/ProblemRepository.java | 3 + .../exception/errorcode/AuthErrorCode.java | 3 +- 8 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 src/main/java/swm_nm/morandi/domain/customTest/controller/CustomTestController.java create mode 100644 src/main/java/swm_nm/morandi/domain/customTest/request/CustomTestRequest.java create mode 100644 src/main/java/swm_nm/morandi/domain/customTest/response/CustomTestResponse.java create mode 100644 src/main/java/swm_nm/morandi/domain/customTest/response/CustomTestResponses.java create mode 100644 src/main/java/swm_nm/morandi/domain/customTest/service/CustomTestService.java diff --git a/src/main/java/swm_nm/morandi/domain/customTest/controller/CustomTestController.java b/src/main/java/swm_nm/morandi/domain/customTest/controller/CustomTestController.java new file mode 100644 index 00000000..b906f2c1 --- /dev/null +++ b/src/main/java/swm_nm/morandi/domain/customTest/controller/CustomTestController.java @@ -0,0 +1,25 @@ +package swm_nm.morandi.domain.customTest.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import swm_nm.morandi.domain.customTest.request.CustomTestRequest; +import swm_nm.morandi.domain.customTest.response.CustomTestResponses; +import swm_nm.morandi.domain.customTest.service.CustomTestService; +import swm_nm.morandi.global.annotations.CurrentMember; + +@RestController +@RequiredArgsConstructor +public class CustomTestController { + + private final CustomTestService customTestService; + @ResponseStatus(HttpStatus.OK) + @PostMapping("/custom") + public CustomTestResponses customTestGenerate(@CurrentMember Long memberId, + @RequestBody CustomTestRequest customTestRequest) { + return customTestService.generateCustomTest(memberId,customTestRequest); + } +} diff --git a/src/main/java/swm_nm/morandi/domain/customTest/request/CustomTestRequest.java b/src/main/java/swm_nm/morandi/domain/customTest/request/CustomTestRequest.java new file mode 100644 index 00000000..b56c7e89 --- /dev/null +++ b/src/main/java/swm_nm/morandi/domain/customTest/request/CustomTestRequest.java @@ -0,0 +1,27 @@ +package swm_nm.morandi.domain.customTest.request; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class CustomTestRequest { + private List bojIds; + private List bojProblems; + private Long testTime; + private String testTypename; + + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + private LocalDateTime startTime; + +} diff --git a/src/main/java/swm_nm/morandi/domain/customTest/response/CustomTestResponse.java b/src/main/java/swm_nm/morandi/domain/customTest/response/CustomTestResponse.java new file mode 100644 index 00000000..7b6f972c --- /dev/null +++ b/src/main/java/swm_nm/morandi/domain/customTest/response/CustomTestResponse.java @@ -0,0 +1,14 @@ +package swm_nm.morandi.domain.customTest.response; + +import lombok.*; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CustomTestResponse { + private Long customTestId; + private String testTypename; + private String bojId; + private Long memberId; +} diff --git a/src/main/java/swm_nm/morandi/domain/customTest/response/CustomTestResponses.java b/src/main/java/swm_nm/morandi/domain/customTest/response/CustomTestResponses.java new file mode 100644 index 00000000..8d2e82f6 --- /dev/null +++ b/src/main/java/swm_nm/morandi/domain/customTest/response/CustomTestResponses.java @@ -0,0 +1,16 @@ +package swm_nm.morandi.domain.customTest.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CustomTestResponses { + List customTests; +} diff --git a/src/main/java/swm_nm/morandi/domain/customTest/service/CustomTestService.java b/src/main/java/swm_nm/morandi/domain/customTest/service/CustomTestService.java new file mode 100644 index 00000000..08052085 --- /dev/null +++ b/src/main/java/swm_nm/morandi/domain/customTest/service/CustomTestService.java @@ -0,0 +1,153 @@ +package swm_nm.morandi.domain.customTest.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import swm_nm.morandi.aop.annotation.MemberLock; +import swm_nm.morandi.domain.customTest.request.CustomTestRequest; +import swm_nm.morandi.domain.customTest.response.CustomTestResponse; +import swm_nm.morandi.domain.customTest.response.CustomTestResponses; +import swm_nm.morandi.domain.member.entity.Member; +import swm_nm.morandi.domain.member.repository.MemberRepository; +import swm_nm.morandi.domain.problem.dto.DifficultyLevel; +import swm_nm.morandi.domain.problem.entity.Problem; +import swm_nm.morandi.domain.problem.repository.ProblemRepository; +import swm_nm.morandi.domain.testDuring.dto.TempCodeDto; +import swm_nm.morandi.domain.testDuring.dto.TestInfo; +import swm_nm.morandi.domain.testDuring.dto.factory.TempCodeFactory; +import swm_nm.morandi.domain.testInfo.entity.AttemptProblem; +import swm_nm.morandi.domain.testInfo.entity.Tests; +import swm_nm.morandi.domain.testInfo.repository.TestRepository; +import swm_nm.morandi.domain.testRecord.repository.AttemptProblemRepository; +import swm_nm.morandi.global.exception.MorandiException; +import swm_nm.morandi.global.exception.errorcode.AuthErrorCode; +import swm_nm.morandi.redis.utils.RedisKeyGenerator; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class CustomTestService { + + private final RedisTemplate redisTemplate; + private final MemberRepository memberRepository; + private final TestRepository testRepository; + private final AttemptProblemRepository attemptProblemRepository; + private final ProblemRepository problemRepository; + private final TempCodeFactory tempCodeFactory; + private final RedisKeyGenerator redisKeyGenerator; + + private static final String TEST_PREFIX = "testing:memberId:"; + + @MemberLock + @Transactional + public CustomTestResponses generateCustomTest(Long memberId, CustomTestRequest customTestRequest) { + validateMemberId(memberId); + List members = getMembers(customTestRequest); + List tests = createTests(customTestRequest, members); + + tests.forEach(test -> { + String testingKey = createTestingKey(test.getMember().getMemberId()); + storeTestInfoInRedis(testingKey, test, customTestRequest); + List attemptProblems = createAttemptProblems(customTestRequest, test); + attemptProblemRepository.saveAll(attemptProblems); + setupTempCode(test, customTestRequest); + }); + + final List customTestResponses = tests.stream().map(test -> CustomTestResponse + .builder() + .testTypename(test.getTestTypename()) + .customTestId(test.getTestId()) + .bojId(test.getMember().getBojId()) + .memberId(test.getMember().getMemberId()) + .build()) + .collect(Collectors.toList()); + + return CustomTestResponses.builder() + .customTests(customTestResponses).build(); + } + + private void validateMemberId(Long memberId) { + if (!memberId.equals(1L)) { + throw new MorandiException(AuthErrorCode.AUTHENTICATION_FAILED); + } + } + + private List getMembers(CustomTestRequest customTestRequest) { + return memberRepository.findAllByBojIdIn(customTestRequest.getBojIds()); + } + + private List createTests(CustomTestRequest customTestRequest, List members) { + clearTestInfoInRedis(members); + final List tests = members.stream() + .map(member -> Tests.builder() + .member(member) + .testTypename(customTestRequest.getTestTypename()) + .testTime(customTestRequest.getTestTime()) + .testDate(customTestRequest.getStartTime()) + .problemCount(customTestRequest.getBojProblems().size()) + .endDifficulty(DifficultyLevel.G5) + .startDifficulty(DifficultyLevel.S3) + .build()) + .collect(Collectors.toList()); + testRepository.saveAll(tests); + return tests; + } + private void clearTestInfoInRedis(List members) { + for (Member member : members) { + String testingKey = createTestingKey(member.getMemberId()); + if(Boolean.TRUE.equals(redisTemplate.hasKey(testingKey))) { + redisTemplate.delete(testingKey); + } + } + } + + private String createTestingKey(Long memberId) { + return TEST_PREFIX + memberId; + } + + private void storeTestInfoInRedis(String testingKey, Tests test, CustomTestRequest customTestRequest) { + TestInfo testInfo = TestInfo.builder() + .testId(test.getTestId()) + .endTime(customTestRequest.getStartTime().plusMinutes(customTestRequest.getTestTime())) + .build(); + redisTemplate.opsForValue().set(testingKey, testInfo); + } + + private List createAttemptProblems(CustomTestRequest customTestRequest, Tests test) { + final List problems = problemRepository.findAllByBojProblemIdIn(customTestRequest.getBojProblems()); + return problems.stream() + .map(problem -> { + final AttemptProblem attemptProblem = AttemptProblem.builder() + .testDate(customTestRequest.getStartTime().toLocalDate()) + .member(test.getMember()) + .problem(problem) + .test(test) + .build(); + attemptProblem.setTest(test); + return attemptProblem; + }).collect(Collectors.toList()); + } + + private void setupTempCode(Tests test, CustomTestRequest customTestRequest) { + String tempCodeKey = redisKeyGenerator.generateTempCodeKey(test.getTestId()); + TempCodeDto initialTempCode = tempCodeFactory.getTempCodeDto(); + int problemCount = test.getProblemCount(); + HashOperations hashOps = redisTemplate.opsForHash(); + for (int problemNumber = 1; problemNumber <= problemCount; problemNumber++) { + hashOps.put(tempCodeKey, String.valueOf(problemNumber), initialTempCode); + } + setTempCodeExpiry(customTestRequest, tempCodeKey); + } + + private void setTempCodeExpiry(CustomTestRequest customTestRequest, String tempCodeKey) { + long expireTime = Duration.between(LocalDateTime.now(), customTestRequest.getStartTime().plusMinutes(customTestRequest.getTestTime())).toMinutes(); + redisTemplate.expire(tempCodeKey, expireTime, TimeUnit.MINUTES); + } +} diff --git a/src/main/java/swm_nm/morandi/domain/member/repository/MemberRepository.java b/src/main/java/swm_nm/morandi/domain/member/repository/MemberRepository.java index 17785290..4dec7ff5 100644 --- a/src/main/java/swm_nm/morandi/domain/member/repository/MemberRepository.java +++ b/src/main/java/swm_nm/morandi/domain/member/repository/MemberRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import swm_nm.morandi.domain.member.entity.Member; +import java.util.List; import java.util.Optional; public interface MemberRepository extends JpaRepository { @@ -13,4 +14,6 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); Boolean existsByBojId(String bojId); Page findAll(Pageable pageable); + + List findAllByBojIdIn(List bojIdList); } diff --git a/src/main/java/swm_nm/morandi/domain/problem/repository/ProblemRepository.java b/src/main/java/swm_nm/morandi/domain/problem/repository/ProblemRepository.java index 07727967..75793ca9 100644 --- a/src/main/java/swm_nm/morandi/domain/problem/repository/ProblemRepository.java +++ b/src/main/java/swm_nm/morandi/domain/problem/repository/ProblemRepository.java @@ -3,9 +3,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import swm_nm.morandi.domain.problem.entity.Problem; +import java.util.List; import java.util.Optional; public interface ProblemRepository extends JpaRepository { Optional findProblemByBojProblemId(Long bojProblemId); + List findAllByBojProblemIdIn(List bojProblemIdList); + } diff --git a/src/main/java/swm_nm/morandi/global/exception/errorcode/AuthErrorCode.java b/src/main/java/swm_nm/morandi/global/exception/errorcode/AuthErrorCode.java index 1ef487fd..4acef95f 100644 --- a/src/main/java/swm_nm/morandi/global/exception/errorcode/AuthErrorCode.java +++ b/src/main/java/swm_nm/morandi/global/exception/errorcode/AuthErrorCode.java @@ -21,7 +21,8 @@ public enum AuthErrorCode implements ErrorCode { SSO_USERINFO(HttpStatus.UNAUTHORIZED,"SSO에서 사용자 정보를 가져올 수 없습니다" ), SSO_SERVER_ERROR(HttpStatus.UNAUTHORIZED,"SSO서버와 통신할 수 없습니다." ), BAEKJOON_ID_NULL(HttpStatus.UNPROCESSABLE_ENTITY,"백준 ID는 크롬 익스텐션을 통해 필수로 등록해야합니다. " ),//상태코드 422 에외처리 - INVALID_SOCIAL_TYPE(HttpStatus.BAD_REQUEST,"지원되지 않는 OAuth provider 입니다."); + INVALID_SOCIAL_TYPE(HttpStatus.BAD_REQUEST,"지원되지 않는 OAuth provider 입니다."), + AUTHENTICATION_FAILED(HttpStatus.BAD_REQUEST, "지원하지 않는 사용자입니다. "),; private final HttpStatus httpStatus;