Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Feat 원하는 문제 셋을 풀 수 있는 API 추가 #594 #596

Merged
merged 2 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading