Skip to content

Commit

Permalink
Merge pull request #595 from SWM-Morandi/feat/#594
Browse files Browse the repository at this point in the history
✨ Feat 원하는 문제 셋을 풀 수 있는 API 추가 #594
  • Loading branch information
miiiinju1 authored Dec 8, 2023
2 parents 8ca615f + 5222504 commit b81f83f
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String> bojIds;
private List<Long> bojProblems;
private Long testTime;
private String testTypename;

@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime startTime;

}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<CustomTestResponse> customTests;
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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<Member> members = getMembers(customTestRequest);
List<Tests> tests = createTests(customTestRequest, members);

tests.forEach(test -> {
String testingKey = createTestingKey(test.getMember().getMemberId());
storeTestInfoInRedis(testingKey, test, customTestRequest);
List<AttemptProblem> attemptProblems = createAttemptProblems(customTestRequest, test);
attemptProblemRepository.saveAll(attemptProblems);
setupTempCode(test, customTestRequest);
});

final List<CustomTestResponse> 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<Member> getMembers(CustomTestRequest customTestRequest) {
return memberRepository.findAllByBojIdIn(customTestRequest.getBojIds());
}

private List<Tests> createTests(CustomTestRequest customTestRequest, List<Member> members) {
clearTestInfoInRedis(members);
final List<Tests> 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<Member> 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<AttemptProblem> createAttemptProblems(CustomTestRequest customTestRequest, Tests test) {
final List<Problem> 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<String, String, TempCodeDto> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Member, Long> {
Expand All @@ -13,4 +14,6 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
Boolean existsByBojId(String bojId);
Page<Member> findAll(Pageable pageable);

List<Member> findAllByBojIdIn(List<String> bojIdList);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Problem, Long> {

Optional<Problem> findProblemByBojProblemId(Long bojProblemId);
List<Problem> findAllByBojProblemIdIn(List<Long> bojProblemIdList);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit b81f83f

Please sign in to comment.