From 53768b2bea3144293000ec57880e853a32846924 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Sat, 30 Mar 2024 16:13:14 +0900 Subject: [PATCH 01/44] =?UTF-8?q?feat:=20solved=20problemnumber=20set=20?= =?UTF-8?q?=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/defense/Defense.java | 3 + .../dailydefense_record/DailyRecord.java | 17 ++++ .../domain/model/record/Detail.java | 4 + .../dailydefense_record/DailyRecordTest.java | 82 ++++++++++++++----- 4 files changed, 85 insertions(+), 21 deletions(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_information/domain/model/defense/Defense.java b/src/main/java/kr/co/morandi/backend/defense_information/domain/model/defense/Defense.java index d0e170fb..7e2ee6db 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/domain/model/defense/Defense.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/domain/model/defense/Defense.java @@ -30,6 +30,9 @@ public abstract class Defense extends BaseEntity { @Enumerated(EnumType.STRING) private DefenseType defenseType; + public void increaseAttemptCount() { + ++this.attemptCount; + } public abstract LocalDateTime getEndTime(LocalDateTime startTime); //팩토리 메소드 패턴 public Map getDefenseProblems(ProblemGenerationService problemGenerationService) { diff --git a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecord.java b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecord.java index 1bbb9b3f..7da9f08a 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecord.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecord.java @@ -29,6 +29,22 @@ public class DailyRecord extends Record { private Long solvedCount; private Integer problemCount; + public void solveProblem(Long problemNumber, String code) { + super.getDetails().stream() + .filter(detail -> detail.getProblemNumber().equals(problemNumber)) + .findFirst() + .ifPresent(detail -> { + detail.solveProblem(code); + this.solvedCount++; + }); + } + + public Set getSolvedProblemNumbers() { + return super.getDetails().stream() + .filter(DailyDetail::getIsSolved) + .map(DailyDetail::getProblemNumber) + .collect(Collectors.toSet()); + } public boolean isSolvedProblem(Long problemNumber) { return super.getDetails().stream() .anyMatch(detail -> detail.getProblemNumber().equals(problemNumber) @@ -71,6 +87,7 @@ private DailyRecord(LocalDateTime date, Defense defense, Member member, Map records, Defense defense) { this.isSolved = INITIAL_IS_SOLVED; this.submitCount = INITIAL_SUBMIT_COUNT; diff --git a/src/test/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecordTest.java b/src/test/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecordTest.java index 1b970192..321fee24 100644 --- a/src/test/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecordTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecordTest.java @@ -13,6 +13,7 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; @@ -25,15 +26,54 @@ @ActiveProfiles("test") class DailyRecordTest { + @DisplayName("풀어낸 문제들에 대한 문제번호 목록을 반환할 수 있다.") + @Test + void getSolvedProblemNumbers() { + // given + DailyDefense dailyDefense = createDailyDefense(); + LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); + Member member = createMember("user"); + Map triedProblem = getProblems(dailyDefense, 2L); + DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, triedProblem); + dailyRecord.tryMoreProblem(getProblems(dailyDefense, 3L)); + dailyRecord.solveProblem(2L, "solvedCode"); + + // when + final Set solvedProblemNumbers = dailyRecord.getSolvedProblemNumbers(); + + + // then + assertThat(solvedProblemNumbers).hasSize(1) + .contains(2L); + + } + + @DisplayName("시험에 응시하면 오늘의 문제 attemptCount가 1 증가한다.") + @Test + void increaseAttempCountWhenTryDefense() { + // given + DailyDefense dailyDefense = createDailyDefense(); + LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); + Member member = createMember("user"); + Map triedProblem = getProblems(dailyDefense, 2L); + + // when + DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, triedProblem); + + // then + assertThat(dailyDefense.getAttemptCount()).isEqualTo(1L); + + } + @DisplayName("오늘의 문제 기록에서 세부 문제의 정답 여부를 확인할 수 있다.") @Test void isSolvedProblem() { // given - DailyDefense DailyDefense = createDailyDefense(); + DailyDefense dailyDefense = createDailyDefense(); LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); Member member = createMember("user"); - Map triedProblem = getProblems(DailyDefense, 2L); - DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, DailyDefense, member, triedProblem); + Map triedProblem = getProblems(dailyDefense, 2L); + DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, triedProblem); // when final boolean solvedProblem = dailyRecord.isSolvedProblem(2L); @@ -46,11 +86,11 @@ void isSolvedProblem() { @Test void tryExistDetailThenReturnExistDetail() { // given - DailyDefense DailyDefense = createDailyDefense(); + DailyDefense dailyDefense = createDailyDefense(); LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); Member member = createMember("user"); - Map triedProblem = getProblems(DailyDefense, 2L); - DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, DailyDefense, member, triedProblem); + Map triedProblem = getProblems(dailyDefense, 2L); + DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, triedProblem); // when dailyRecord.tryMoreProblem(triedProblem); @@ -65,13 +105,13 @@ void tryExistDetailThenReturnExistDetail() { @Test void solvedCountIsZero() { // given - DailyDefense DailyDefense = createDailyDefense(); + DailyDefense dailyDefense = createDailyDefense(); LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); Member member = createMember("user"); - Map problems = getProblems(DailyDefense, 2L); + Map problems = getProblems(dailyDefense, 2L); // when - DailyRecord dailyDefenseRecord = DailyRecord.tryDefense(startTime, DailyDefense, member, problems); + DailyRecord dailyDefenseRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, problems); // then assertThat(dailyDefenseRecord.getSolvedCount()).isZero(); @@ -99,15 +139,15 @@ void recordCreateExceptionWhenOverOneDay() { void recordCreatedWithinOneDay() { // given LocalDate createdDate = LocalDate.of(2024, 3, 1); - DailyDefense DailyDefense = createDailyDefense(createdDate); + DailyDefense dailyDefense = createDailyDefense(createdDate); Member member = createMember("user"); - Map problems = getProblems(DailyDefense, 2L); + Map problems = getProblems(dailyDefense, 2L); LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 23, 59, 59); // when - DailyRecord dailyDefenseRecord = DailyRecord.tryDefense(startTime, DailyDefense, member, problems); + DailyRecord dailyDefenseRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, problems); // then assertNotNull(dailyDefenseRecord); @@ -116,13 +156,13 @@ void recordCreatedWithinOneDay() { @Test void isSolvedIsFalse() { // given - DailyDefense DailyDefense = createDailyDefense(); + DailyDefense dailyDefense = createDailyDefense(); LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); Member member = createMember("user"); - Map problems = getProblems(DailyDefense, 2L); + Map problems = getProblems(dailyDefense, 2L); // when - DailyRecord dailyDefenseRecord = DailyRecord.tryDefense(startTime, DailyDefense, member, problems); + DailyRecord dailyDefenseRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, problems); // then assertThat(dailyDefenseRecord.getDetails()) @@ -133,13 +173,13 @@ void isSolvedIsFalse() { @Test void submitCountIsZero() { // given - DailyDefense DailyDefense = createDailyDefense(); + DailyDefense dailyDefense = createDailyDefense(); LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); Member member = createMember("user"); - Map problems = getProblems(DailyDefense, 2L); + Map problems = getProblems(dailyDefense, 2L); // when - DailyRecord dailyDefenseRecord = DailyRecord.tryDefense(startTime, DailyDefense, member, problems); + DailyRecord dailyDefenseRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, problems); // then assertThat(dailyDefenseRecord.getDetails()) @@ -150,13 +190,13 @@ void submitCountIsZero() { @Test void solvedCodeIsNull() { // given - DailyDefense DailyDefense = createDailyDefense(); + DailyDefense dailyDefense = createDailyDefense(); LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); Member member = createMember("user"); - Map problems = getProblems(DailyDefense,2L); + Map problems = getProblems(dailyDefense,2L); // when - DailyRecord dailyDefenseRecord = DailyRecord.tryDefense(startTime, DailyDefense, member, problems); + DailyRecord dailyDefenseRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, problems); // then assertThat(dailyDefenseRecord.getDetails()) From fdd697327564da913ed48977983010815db6fcf8 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Sat, 30 Mar 2024 17:54:36 +0900 Subject: [PATCH 02/44] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=97=AC=EB=B6=80=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20=EC=98=A4=EB=8A=98=EC=9D=98=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8A=94=20service=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/DailyDefenseInfoResponse.java | 50 +++++++ .../DailyDefenseProblemInfoResponse.java | 59 ++++++++ .../port/in/DailyDefenseUseCase.java | 11 ++ .../service/DailyDefenseUseCaseImpl.java | 46 ++++++ .../service/DailyDefenseUseCaseImplTest.java | 141 ++++++++++++++++++ 5 files changed, 307 insertions(+) create mode 100644 src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponse.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseProblemInfoResponse.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java create mode 100644 src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponse.java b/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponse.java new file mode 100644 index 00000000..8973b69b --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponse.java @@ -0,0 +1,50 @@ +package kr.co.morandi.backend.defense_information.application.dto.response; + +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class DailyDefenseInfoResponse { + + private String defenseName; + private Integer problemCount; + private Long attemptCount; + private List problems; + + + public static DailyDefenseInfoResponse fromNonAttempted(DailyDefense dailyDefense) { + return DailyDefenseInfoResponse.builder() + .defenseName(dailyDefense.getContentName()) + .problemCount(dailyDefense.getProblemCount()) + .attemptCount(dailyDefense.getAttemptCount()) + .problems(DailyDefenseProblemInfoResponse.ofNonAttempted(dailyDefense.getDailyDefenseProblems())) + .build(); + } + + public static DailyDefenseInfoResponse ofAttempted(DailyDefense dailyDefense, DailyRecord dailyRecord) { + return DailyDefenseInfoResponse.builder() + .defenseName(dailyDefense.getContentName()) + .problemCount(dailyDefense.getProblemCount()) + .attemptCount(dailyDefense.getAttemptCount()) + .problems(DailyDefenseProblemInfoResponse.ofAttempted( + dailyDefense.getDailyDefenseProblems(), + dailyRecord.getSolvedProblemNumbers()) + ) + .build(); + } + + @Builder + private DailyDefenseInfoResponse(String defenseName, Integer problemCount, Long attemptCount, List problems) { + this.defenseName = defenseName; + this.problemCount = problemCount; + this.attemptCount = attemptCount; + this.problems = problems; + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseProblemInfoResponse.java b/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseProblemInfoResponse.java new file mode 100644 index 00000000..5aa81c0e --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseProblemInfoResponse.java @@ -0,0 +1,59 @@ +package kr.co.morandi.backend.defense_information.application.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; +import kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Set; + +@Getter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DailyDefenseProblemInfoResponse { + + private Long problemNumber; + private Long problemId; + private Long baekjoonProblemId; + private ProblemTier difficulty; + private Long solvedCount; + private Long submitCount; + private Boolean isSolved; + + public static List ofNonAttempted(List dailyDefenseProblems) { + return dailyDefenseProblems.stream().map(problem -> DailyDefenseProblemInfoResponse.builder() + .problemNumber(problem.getProblemNumber()) + .problemId(problem.getProblem().getProblemId()) + .baekjoonProblemId(problem.getProblem().getBaekjoonProblemId()) + .difficulty(problem.getProblem().getProblemTier()) + .solvedCount(problem.getSolvedCount()) + .submitCount(problem.getSubmitCount()) + .build() + ).toList(); + } + public static List ofAttempted(List dailyDefenseProblems, Set solvedProblemNumbers) { + return dailyDefenseProblems.stream().map(problem -> DailyDefenseProblemInfoResponse.builder() + .problemNumber(problem.getProblemNumber()) + .problemId(problem.getProblem().getProblemId()) + .baekjoonProblemId(problem.getProblem().getBaekjoonProblemId()) + .difficulty(problem.getProblem().getProblemTier()) + .solvedCount(problem.getSolvedCount()) + .submitCount(problem.getSubmitCount()) + .isSolved(solvedProblemNumbers.contains(problem.getProblemNumber())) + .build() + ).toList(); + } + @Builder + private DailyDefenseProblemInfoResponse(Long problemNumber, Long problemId, Long baekjoonProblemId, ProblemTier difficulty, Long solvedCount, Long submitCount, Boolean isSolved) { + this.problemNumber = problemNumber; + this.problemId = problemId; + this.baekjoonProblemId = baekjoonProblemId; + this.difficulty = difficulty; + this.solvedCount = solvedCount; + this.submitCount = submitCount; + this.isSolved = isSolved; + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java b/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java new file mode 100644 index 00000000..5de1c04d --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java @@ -0,0 +1,11 @@ +package kr.co.morandi.backend.defense_information.application.port.in; + +import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; +import kr.co.morandi.backend.member_management.domain.model.member.Member; + +import java.time.LocalDateTime; + +public interface DailyDefenseUseCase { + DailyDefenseInfoResponse getDailyDefenseInfo(Member member, LocalDateTime requestDateTime); + void getDailyDefenseTopRank(); +} diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java b/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java new file mode 100644 index 00000000..0b51c4e7 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java @@ -0,0 +1,46 @@ +package kr.co.morandi.backend.defense_information.application.service; + +import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; +import kr.co.morandi.backend.defense_information.application.port.in.DailyDefenseUseCase; +import kr.co.morandi.backend.defense_information.application.port.out.dailydefense.DailyDefensePort; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; +import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.member_management.domain.model.member.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType.DAILY; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class DailyDefenseUseCaseImpl implements DailyDefenseUseCase { + + private final DailyDefensePort dailyDefensePort; + private final DailyRecordPort dailyRecordPort; + + @Override + public DailyDefenseInfoResponse getDailyDefenseInfo(Member member, LocalDateTime requestDateTime) { + final DailyDefense dailyDefense = dailyDefensePort.findDailyDefense(DAILY, requestDateTime.toLocalDate()); + + if(member != null) { + Optional maybeDailyRecord = dailyRecordPort.findDailyRecord(member, requestDateTime.toLocalDate()); + if(maybeDailyRecord.isPresent()) { + DailyRecord dailyRecord = maybeDailyRecord.get(); + return DailyDefenseInfoResponse.ofAttempted(dailyDefense, dailyRecord); + } + } + + return DailyDefenseInfoResponse.fromNonAttempted(dailyDefense); + } + + @Override + public void getDailyDefenseTopRank() { + + } +} diff --git a/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java b/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java new file mode 100644 index 00000000..0f468ee2 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java @@ -0,0 +1,141 @@ +package kr.co.morandi.backend.defense_information.application.service; + +import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; +import kr.co.morandi.backend.defense_information.application.port.in.DailyDefenseUseCase; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; +import kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier; +import kr.co.morandi.backend.defense_information.domain.service.defense.ProblemGenerationService; +import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; +import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.member_management.domain.model.member.Member; +import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; +import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier.*; +import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class DailyDefenseUseCaseImplTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private DailyDefenseRepository dailyDefenseRepository; + + @Autowired + private ProblemRepository problemRepository; + + @Autowired + private DailyDefenseUseCase dailyDefenseUseCase; + + @Autowired + private ProblemGenerationService problemGenerationService; + + @Autowired + private DailyRecordPort dailyRecordPort; + + @DisplayName("사용자가 없을 때 DailyDefense 정보를 조회하면 isSolved는 null로 반환한다.") + @Test + void getDailyDefenseInfo() { + // given + createDailyDefense(LocalDate.of(2021, 10, 1), "오늘의 문제 테스트"); + + LocalDateTime requestTime = LocalDateTime.of(2021, 10, 1, 12, 0, 0); + + // when + final DailyDefenseInfoResponse response = dailyDefenseUseCase.getDailyDefenseInfo(null, requestTime); + + // then + assertAll( + () -> assertThat(response).isNotNull() + .extracting("defenseName", "problemCount", "attemptCount") + .contains("오늘의 문제 테스트", 3, 0L), + + () -> assertThat(response.getProblems()).hasSize(3) + .extracting("problemNumber", "baekjoonProblemId", "difficulty", "solvedCount", "submitCount", "isSolved") + .containsExactlyInAnyOrder( + tuple(1L, 1000L, B5, 0L, 0L, null), + tuple(2L, 2000L, S5, 0L, 0L, null), + tuple(3L, 3000L, G5, 0L, 0L, null) + ) + ); + } + + @DisplayName("사용자가 있을 때 응시 기록도 있다면 DailyDefense 정보를 조회하면 isSolved는 True/False를 포함하여 반환한다.") + @Test + void getDailyDefenseInfoWithMemberAndRecord() { + // given + final DailyDefense dailyDefense = createDailyDefense(LocalDate.of(2021, 10, 1), "오늘의 문제 테스트"); + Member member = createMember(); + LocalDateTime requestTime = LocalDateTime.of(2021, 10, 1, 12, 0, 0); + + final DailyRecord dailyRecord = createDailyRecord(dailyDefense, member, 2L, requestTime); + dailyRecord.solveProblem(2L, "example"); + dailyRecordPort.saveDailyRecord(dailyRecord); + + // when + final DailyDefenseInfoResponse response = dailyDefenseUseCase.getDailyDefenseInfo(member, requestTime); + + // then + assertAll( + () -> assertThat(response).isNotNull() + .extracting("defenseName", "problemCount", "attemptCount") + .contains("오늘의 문제 테스트", 3, 1L), + + () -> assertThat(response.getProblems()).hasSize(3) + .extracting("problemNumber", "baekjoonProblemId", "difficulty", "solvedCount", "submitCount", "isSolved") + .containsExactlyInAnyOrder( + tuple(1L, 1000L, B5, 0L, 0L, false), + tuple(2L, 2000L, S5, 0L, 0L, true), + tuple(3L, 3000L, G5, 0L, 0L, false) + ) + ); + } + + // 시험에 응시하는 메소드 + private DailyRecord createDailyRecord(DailyDefense dailyDefense, Member member, Long problemNumber, LocalDateTime requestTIme) { + final Map tryingProblem = dailyDefense.getTryingProblem(problemNumber, problemGenerationService); + final DailyRecord dailyRecord = DailyRecord.tryDefense(requestTIme, dailyDefense, member, tryingProblem); + dailyRecord.tryMoreProblem(dailyDefense.getTryingProblem(3L, problemGenerationService)); + + return dailyRecordPort.saveDailyRecord(dailyRecord); + } + private DailyDefense createDailyDefense(LocalDate createdDate, String contentName) { + AtomicLong problemNumber = new AtomicLong(1L); + Map problemMap = createProblems().stream() + .collect(Collectors.toMap(p-> problemNumber.getAndIncrement(), problem -> problem)); + return dailyDefenseRepository.save(DailyDefense.create(createdDate, contentName, problemMap)); + } + private List createProblems() { + Problem problem1 = Problem.create(1000L, B5, 0L); + Problem problem2 = Problem.create(2000L, S5, 0L); + Problem problem3 = Problem.create(3000L, G5, 0L); + + return problemRepository.saveAll(List.of(problem1, problem2, problem3)); + } + private Member createMember() { + return memberRepository.save(Member.create("test", "test" + "@gmail.com", GOOGLE, "test", "test")); + } + +} \ No newline at end of file From bf2a2f496af56a9e1371e1714ab71352550301e6 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 1 Apr 2024 17:07:27 +0900 Subject: [PATCH 03/44] =?UTF-8?q?feat:=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=8B=9C=ED=97=98=20=EC=A0=95=EB=B3=B4,?= =?UTF-8?q?=20=EB=9E=AD=ED=82=B9=20=EC=A0=95=EB=B3=B4=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=A1=9C=20=EB=B0=98=ED=99=98=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/in/DailyDefenseUseCase.java | 1 - .../service/DailyDefenseUseCaseImpl.java | 5 - .../DailyRecordRankUseCaseImpl.java | 55 ++++++++ .../dto/DailyDefenseRankPageResponse.java | 31 ++++ .../dto/DailyDetailRankResponse.java | 36 +++++ .../dto/DailyRecordRankResponse.java | 43 ++++++ .../port/in/DailyRecordRankUseCase.java | 11 ++ .../port/out/dailyrecord/DailyRecordPort.java | 5 +- .../application/util/TimeFormatHelper.java | 8 ++ .../customdefense_record/CustomRecord.java | 3 +- .../dailydefense_record/DailyDetail.java | 1 + .../dailydefense_record/DailyRecord.java | 15 +- .../randomdefense_record/RandomRecord.java | 4 +- .../domain/model/record/Detail.java | 13 +- .../domain/model/record/Record.java | 8 ++ .../stagedefense_record/StageDetail.java | 3 +- .../stagedefense_record/StageRecord.java | 4 +- .../dailydefense/DailyRecordAdapter.java | 11 ++ .../DailyRecordRepository.java | 13 +- .../service/DailyDefenseUseCaseImplTest.java | 2 +- .../DailyRecordRankUseCaseTest.java | 132 ++++++++++++++++++ .../util/TimeFormatHelperTest.java | 27 ++++ .../dailydefense_record/DailyRecordTest.java | 58 +++++++- .../DailyRecordRepositoryTest.java | 72 +++++++--- 24 files changed, 519 insertions(+), 42 deletions(-) create mode 100644 src/main/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseImpl.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_record/application/dto/DailyDefenseRankPageResponse.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_record/application/dto/DailyDetailRankResponse.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_record/application/dto/DailyRecordRankResponse.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_record/application/port/in/DailyRecordRankUseCase.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelper.java create mode 100644 src/test/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseTest.java create mode 100644 src/test/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelperTest.java diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java b/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java index 5de1c04d..19985187 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java @@ -7,5 +7,4 @@ public interface DailyDefenseUseCase { DailyDefenseInfoResponse getDailyDefenseInfo(Member member, LocalDateTime requestDateTime); - void getDailyDefenseTopRank(); } diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java b/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java index 0b51c4e7..e72bdd40 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java @@ -38,9 +38,4 @@ public DailyDefenseInfoResponse getDailyDefenseInfo(Member member, LocalDateTime return DailyDefenseInfoResponse.fromNonAttempted(dailyDefense); } - - @Override - public void getDailyDefenseTopRank() { - - } } diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseImpl.java b/src/main/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseImpl.java new file mode 100644 index 00000000..875e0cda --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseImpl.java @@ -0,0 +1,55 @@ +package kr.co.morandi.backend.defense_record.application; + +import kr.co.morandi.backend.defense_record.application.dto.DailyDefenseRankPageResponse; +import kr.co.morandi.backend.defense_record.application.dto.DailyDetailRankResponse; +import kr.co.morandi.backend.defense_record.application.dto.DailyRecordRankResponse; +import kr.co.morandi.backend.defense_record.application.port.in.DailyRecordRankUseCase; +import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyDetail; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class DailyRecordRankUseCaseImpl implements DailyRecordRankUseCase { + + private final DailyRecordPort dailyRecordPort; + + + // TODO 공통 등수 로직 부분 빠짐 + @Override + public DailyDefenseRankPageResponse getDailyRecordRank(LocalDateTime requestTime, int page, int size) { + final List dailyRecords = dailyRecordPort.findDailyRecordRank(requestTime.toLocalDate(), page, size); + + // 등수 계산 + // TODO 동점자 처리 필요 + AtomicLong initialRank = new AtomicLong(page * size + 1); + + final List dailyRecordRanks = dailyRecords.stream() + .map(dr -> { + String member = dr.getMember().getNickname(); + Long rank = initialRank.getAndIncrement(); + List details = DailyDetailRankResponse.of(dr.getDetails()); + Long totalSolvedTime = dr.getDetails().stream() + .mapToLong(DailyDetail::getSolvedTime) + .sum(); + Long solvedCount = dr.getDetails().stream() + .filter(DailyDetail::getIsSolved) + .count(); + + return DailyRecordRankResponse.of(member, rank, requestTime, totalSolvedTime, solvedCount, details); + }) + .toList(); + + return DailyDefenseRankPageResponse.of(dailyRecordRanks, page, size); + } + + +} diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/dto/DailyDefenseRankPageResponse.java b/src/main/java/kr/co/morandi/backend/defense_record/application/dto/DailyDefenseRankPageResponse.java new file mode 100644 index 00000000..05de8aee --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/dto/DailyDefenseRankPageResponse.java @@ -0,0 +1,31 @@ +package kr.co.morandi.backend.defense_record.application.dto; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class DailyDefenseRankPageResponse { + + private List dailyRecords; + private Integer totalPage; + private Integer currentPage; + + public static DailyDefenseRankPageResponse of(List dailyRecords, Integer totalPage, Integer currentPage) { + return DailyDefenseRankPageResponse.builder() + .dailyRecords(dailyRecords) + .totalPage(totalPage) + .currentPage(currentPage) + .build(); + } + @Builder + private DailyDefenseRankPageResponse(List dailyRecords, Integer totalPage, Integer currentPage) { + this.dailyRecords = dailyRecords; + this.totalPage = totalPage; + this.currentPage = currentPage; + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/dto/DailyDetailRankResponse.java b/src/main/java/kr/co/morandi/backend/defense_record/application/dto/DailyDetailRankResponse.java new file mode 100644 index 00000000..4f5777c8 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/dto/DailyDetailRankResponse.java @@ -0,0 +1,36 @@ +package kr.co.morandi.backend.defense_record.application.dto; + +import kr.co.morandi.backend.defense_record.application.util.TimeFormatHelper; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyDetail; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class DailyDetailRankResponse { + + private Long problemNumber; + private Boolean isSolved; + private String solvedTime; + + public static List of(List dailyDetails) { + return dailyDetails.stream() + .map(details -> DailyDetailRankResponse.builder() + .problemNumber(details.getProblemNumber()) + .isSolved(details.getIsSolved()) + .solvedTime(TimeFormatHelper.solvedTimeToString(details.getSolvedTime())) + .build()) + .toList(); + } + + @Builder + private DailyDetailRankResponse(Long problemNumber, Boolean isSolved, String solvedTime) { + this.problemNumber = problemNumber; + this.isSolved = isSolved; + this.solvedTime = solvedTime; + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/dto/DailyRecordRankResponse.java b/src/main/java/kr/co/morandi/backend/defense_record/application/dto/DailyRecordRankResponse.java new file mode 100644 index 00000000..977791d0 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/dto/DailyRecordRankResponse.java @@ -0,0 +1,43 @@ +package kr.co.morandi.backend.defense_record.application.dto; + +import kr.co.morandi.backend.defense_record.application.util.TimeFormatHelper; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class DailyRecordRankResponse { + + private String nickname; + private Long rank; + private Long solvedCount; + private LocalDateTime updatedAt; + private String totalSolvedTime; + private List rankDetails; + + public static DailyRecordRankResponse of(String nickname, Long rank, LocalDateTime updatedAt, Long totalSolvedTime, Long totalSolvedCount, List rankDetails) { + return DailyRecordRankResponse.builder() + .nickname(nickname) + .rank(rank) + .updatedAt(updatedAt) + .solvedCount(totalSolvedCount) + .rankDetails(rankDetails) + .totalSolvedTime(TimeFormatHelper.solvedTimeToString(totalSolvedTime)) + .build(); + } + @Builder + private DailyRecordRankResponse(String nickname, Long rank, Long solvedCount, LocalDateTime updatedAt, String totalSolvedTime, List rankDetails) { + this.nickname = nickname; + this.rank = rank; + this.solvedCount = solvedCount; + this.updatedAt = updatedAt; + this.totalSolvedTime = totalSolvedTime; + this.rankDetails = rankDetails; + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/port/in/DailyRecordRankUseCase.java b/src/main/java/kr/co/morandi/backend/defense_record/application/port/in/DailyRecordRankUseCase.java new file mode 100644 index 00000000..2d72d3f9 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/port/in/DailyRecordRankUseCase.java @@ -0,0 +1,11 @@ +package kr.co.morandi.backend.defense_record.application.port.in; + +import kr.co.morandi.backend.defense_record.application.dto.DailyDefenseRankPageResponse; + +import java.time.LocalDateTime; + +public interface DailyRecordRankUseCase { + + // TODO 공통 등수 로직 부분 빠짐 + DailyDefenseRankPageResponse getDailyRecordRank(LocalDateTime requestTime, int page, int size); +} diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/dailyrecord/DailyRecordPort.java b/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/dailyrecord/DailyRecordPort.java index 46f3d845..48b88c77 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/dailyrecord/DailyRecordPort.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/dailyrecord/DailyRecordPort.java @@ -4,12 +4,13 @@ import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import java.time.LocalDate; +import java.util.List; import java.util.Optional; public interface DailyRecordPort { + DailyRecord saveDailyRecord(DailyRecord dailyRecord); Optional findDailyRecord(Member member, LocalDate date); Optional findDailyRecord(Member member, Long recordId, LocalDate date); - - + List findDailyRecordRank(LocalDate requestDate, Integer page, Integer size); } diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelper.java b/src/main/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelper.java new file mode 100644 index 00000000..056f622d --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelper.java @@ -0,0 +1,8 @@ +package kr.co.morandi.backend.defense_record.application.util; + +public class TimeFormatHelper { + + public static String solvedTimeToString(Long solvedTime) { + return String.format("%02d:%02d:%02d", solvedTime / 3600, (solvedTime % 3600) / 60, solvedTime % 60); + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/customdefense_record/CustomRecord.java b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/customdefense_record/CustomRecord.java index 03797d50..f55418b6 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/customdefense_record/CustomRecord.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/customdefense_record/CustomRecord.java @@ -21,7 +21,7 @@ @Getter @DiscriminatorValue("CustomRecord") public class CustomRecord extends Record { - private Long totalSolvedTime; + private Integer solvedCount; private Integer problemCount; @Override @@ -30,7 +30,6 @@ public CustomDetail createDetail(Member member, Long sequenceNumber, Problem pro } private CustomRecord(CustomDefense customDefense, Member member, LocalDateTime testDate, Map problems) { super(testDate, customDefense, member, problems); - this.totalSolvedTime = 0L; this.solvedCount = 0; this.problemCount = customDefense.getProblemCount(); } diff --git a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyDetail.java b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyDetail.java index 418cc5de..4df3bb6a 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyDetail.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyDetail.java @@ -21,6 +21,7 @@ public class DailyDetail extends Detail { Long problemNumber; + private DailyDetail(Member member, Long problemNumber, Problem problem, Record records, Defense defense) { super(member, problem, records, defense); this.problemNumber = problemNumber; diff --git a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecord.java b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecord.java index 7da9f08a..ef73c7cd 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecord.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecord.java @@ -13,6 +13,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; +import java.time.Duration; import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -29,13 +30,18 @@ public class DailyRecord extends Record { private Long solvedCount; private Integer problemCount; - public void solveProblem(Long problemNumber, String code) { + public void solveProblem(Long problemNumber, String code, LocalDateTime solvedAt) { super.getDetails().stream() .filter(detail -> detail.getProblemNumber().equals(problemNumber)) .findFirst() .ifPresent(detail -> { - detail.solveProblem(code); - this.solvedCount++; + + Long solvedTime = calculateSolvedTime(solvedAt); + + if(detail.solveProblem(code, solvedTime)) { + ++this.solvedCount; + super.addTotalSolvedTime(solvedTime); + } }); } @@ -90,4 +96,7 @@ private DailyRecord(LocalDateTime date, Defense defense, Member member, Map { - private Long totalSolvedTime; + private Integer solvedCount; private Integer problemCount; - private static final Long INITIAL_TOTAL_SOLVED_TIME = 0L; private static final Integer INITIAL_SOLVED_COUNT = 0; private RandomRecord(LocalDateTime testDate, RandomDefense randomDefense, Member member, Map problems) { super(testDate, randomDefense, member, problems); - this.totalSolvedTime = INITIAL_TOTAL_SOLVED_TIME; this.solvedCount = INITIAL_SOLVED_COUNT; this.problemCount = problems.size(); } diff --git a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Detail.java b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Detail.java index f36c1a6a..1c835430 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Detail.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Detail.java @@ -10,6 +10,8 @@ import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; +import java.time.LocalDateTime; + @Entity @Inheritance(strategy = InheritanceType.JOINED) @DiscriminatorColumn @@ -39,16 +41,25 @@ public abstract class Detail extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private Problem problem; + private Long solvedTime; + private static final Long INITIAL_SUBMIT_COUNT = 0L; + private static final Long INITIAL_SOLVED_TIME = 0L; private static final Boolean INITIAL_IS_SOLVED = false; - public void solveProblem(String solvedCode) { + public boolean solveProblem(String solvedCode, Long solvedTime) { + if(this.isSolved) { + return false; + } this.isSolved = true; this.solvedCode = solvedCode; + this.solvedTime = solvedTime; + return true; } protected Detail(Member member, Problem problem, Record records, Defense defense) { this.isSolved = INITIAL_IS_SOLVED; this.submitCount = INITIAL_SUBMIT_COUNT; + this.solvedTime = INITIAL_SOLVED_TIME; this.solvedCode = null; this.defense = defense; this.record = records; diff --git a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Record.java b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Record.java index 4a0f94f9..0a218ddc 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Record.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Record.java @@ -40,12 +40,20 @@ public abstract class Record extends BaseEntity { @OneToMany(mappedBy = "record", cascade = CascadeType.ALL, targetEntity = Detail.class) private List details = new ArrayList<>(); + private Long totalSolvedTime; + + private static final Long INITIAL_TOTAL_SOLVED_TIME = 0L; + + public void addTotalSolvedTime(Long totalSolvedTime) { + this.totalSolvedTime += totalSolvedTime; + } protected abstract T createDetail(Member member, Long sequenceNumber, Problem problem, Record records, Defense defense); protected Record(LocalDateTime testDate, Defense defense, Member member, Map problems) { this.testDate = testDate; this.defense = defense; this.member = member; + this.totalSolvedTime = INITIAL_TOTAL_SOLVED_TIME; this.details = problems.entrySet().stream() .map(problem -> this.createDetail(member, problem.getKey(), problem.getValue(), this, defense)) .collect(Collectors.toCollection(ArrayList::new)); diff --git a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/stagedefense_record/StageDetail.java b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/stagedefense_record/StageDetail.java index 3b1d3802..58d9ff9b 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/stagedefense_record/StageDetail.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/stagedefense_record/StageDetail.java @@ -18,12 +18,11 @@ @Getter @DiscriminatorValue("StageDefenseProblemRecord") public class StageDetail extends Detail { - private Long solvedTime; + private Long stageNumber; private StageDetail(Member member, Long stageNumber, Problem problem, Record records, Defense defense) { super(member, problem, records, defense); - this.solvedTime = 0L; this.stageNumber = stageNumber; } public static StageDetail create(Member member, Long stageNumber, Problem problem, Record records, Defense defense) { diff --git a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/stagedefense_record/StageRecord.java b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/stagedefense_record/StageRecord.java index d173dd76..fb9eb513 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/stagedefense_record/StageRecord.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/stagedefense_record/StageRecord.java @@ -20,16 +20,14 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @DiscriminatorValue("StageDefenseRecord") public class StageRecord extends Record { - private Long totalSolvedTime; + private Long stageCount; - private static final Long INITIAL_TOTAL_SOLVED_TIME = 0L; private static final Long INITIAL_STAGE_NUMBER = 1L; private static final Long INITIAL_STAGE_COUNT = 1L; private StageRecord(Defense defense, LocalDateTime testDate, Member member, Map problems) { super(testDate, defense, member, problems); - this.totalSolvedTime = INITIAL_TOTAL_SOLVED_TIME; this.stageCount = INITIAL_STAGE_COUNT; } @Override diff --git a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense/DailyRecordAdapter.java b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense/DailyRecordAdapter.java index 13180100..ec78ecd1 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense/DailyRecordAdapter.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense/DailyRecordAdapter.java @@ -5,9 +5,12 @@ import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record.DailyRecordRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; import java.time.LocalDate; +import java.util.List; import java.util.Optional; @Component @@ -29,4 +32,12 @@ public Optional findDailyRecord(Member member, LocalDate date) { public Optional findDailyRecord(Member member, Long recordId, LocalDate date) { return dailyRecordRepository.findDailyRecordByRecordId(member, recordId, date); } + /* + * 조회 시간 별 DailyDefense 등수 조회 + * */ + @Override + public List findDailyRecordRank(LocalDate requestDate, Integer page, Integer size) { + Pageable pageable = PageRequest.of(page, size); + return dailyRecordRepository.getDailyRecordsRankByDate(requestDate, pageable); + } } diff --git a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepository.java b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepository.java index 24c5c4ea..9e3a2264 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepository.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepository.java @@ -2,10 +2,12 @@ import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import java.time.LocalDate; +import java.util.List; import java.util.Optional; public interface DailyRecordRepository extends JpaRepository { @@ -30,5 +32,14 @@ and CAST(dr.testDate as localdate) = :date and CAST(dr.testDate as localdate) = :date """) Optional findDailyRecordByRecordId(Member member, Long recordId, LocalDate date); - + /* + * Paging 처리이기 때문에 fetch join을 사용하지 않는다. + * */ + @Query(""" + select dr + from DailyRecord dr + where CAST(dr.testDate as localdate) = :requestDate + order by dr.solvedCount desc, dr.totalSolvedTime asc, dr.recordId asc + """) + List getDailyRecordsRankByDate(LocalDate requestDate, Pageable pageable); } diff --git a/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java b/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java index 0f468ee2..6d3a06e5 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java @@ -91,7 +91,7 @@ void getDailyDefenseInfoWithMemberAndRecord() { LocalDateTime requestTime = LocalDateTime.of(2021, 10, 1, 12, 0, 0); final DailyRecord dailyRecord = createDailyRecord(dailyDefense, member, 2L, requestTime); - dailyRecord.solveProblem(2L, "example"); + dailyRecord.solveProblem(2L, "example", requestTime.plusHours(1)); dailyRecordPort.saveDailyRecord(dailyRecord); // when diff --git a/src/test/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseTest.java b/src/test/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseTest.java new file mode 100644 index 00000000..5f937e59 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseTest.java @@ -0,0 +1,132 @@ +package kr.co.morandi.backend.defense_record.application; + +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; +import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; +import kr.co.morandi.backend.defense_record.application.dto.DailyDefenseRankPageResponse; +import kr.co.morandi.backend.defense_record.application.port.in.DailyRecordRankUseCase; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record.DailyRecordRepository; +import kr.co.morandi.backend.member_management.domain.model.member.Member; +import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; +import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier.*; +import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class DailyRecordRankUseCaseTest { + + @Autowired + private DailyRecordRankUseCase dailyRecordRankUseCase; + + @Autowired + private DailyRecordRepository dailyRecordRepository; + + @Autowired + private DailyDefenseRepository dailyDefenseRepository; + + @Autowired + private ProblemRepository problemRepository; + + @Autowired + private MemberRepository memberRepository; + + @DisplayName("특정 시점 DailyRecord의 순위를 조회할 수 있다.") + @Test + void getDailyRecordsRankByDate() { + // given + LocalDate today = LocalDate.of(2021, 10, 1); + final DailyDefense dailyDefense = createDailyDefense(today); + + final Member member1 = createMember("userA", "userA"); + final Member member2 = createMember("userB", "userB"); + final Member member3 = createMember("userC", "userC"); + final Member member4= createMember("userD", "userD"); + final Member member5 = createMember("userE", "userE"); + + final DailyRecord dailyRecord1 = DailyRecord.tryDefense(LocalDateTime.of(2021, 10, 1, 0, 0), dailyDefense, member1, getProblem(dailyDefense, 1L)); + final DailyRecord dailyRecord2 = DailyRecord.tryDefense(LocalDateTime.of(2021, 10, 1, 0, 0), dailyDefense, member2, getProblem(dailyDefense, 2L)); + final DailyRecord dailyRecord3 = DailyRecord.tryDefense(LocalDateTime.of(2021, 10, 1, 0, 0), dailyDefense, member3, getProblem(dailyDefense, 3L)); + final DailyRecord dailyRecord4 = DailyRecord.tryDefense(LocalDateTime.of(2021, 10, 1, 0, 0), dailyDefense, member4, getProblem(dailyDefense, 3L)); + final DailyRecord dailyRecord5 = DailyRecord.tryDefense(LocalDateTime.of(2021, 10, 1, 0, 0), dailyDefense, member5, getProblem(dailyDefense, 3L)); + + /* + * member1: 한 문제 해결 일찍 + * member2 : 두 문제 해결 + * member3: 한 문제 해결 1보다 늦게 + * + * -> 등수 = 2 -> 1 -> 3 + * */ + dailyRecord1.solveProblem(1L, "exampleCode", LocalDateTime.of(2021, 10, 1, 0, 15)); + + dailyRecord2.solveProblem(2L, "exampleCode", LocalDateTime.of(2021, 10, 1, 0, 30)); + dailyRecord2.tryMoreProblem(getProblem(dailyDefense, 3L)); + dailyRecord2.solveProblem(3L, "exampleCode", LocalDateTime.of(2021, 10, 1, 0, 45)); + + dailyRecord3.solveProblem(3L, "exampleCode", LocalDateTime.of(2021, 10, 1, 1, 0)); + + dailyRecordRepository.saveAll(List.of(dailyRecord1, dailyRecord2, dailyRecord3, dailyRecord4, dailyRecord5)); + + + // when + LocalDateTime requestTime = LocalDateTime.of(2021, 10, 1, 2, 0); + final DailyDefenseRankPageResponse dailyRecordRank = dailyRecordRankUseCase.getDailyRecordRank(requestTime, 0, 5); + + // then + assertThat(dailyRecordRank.getDailyRecords()).hasSize(5) + .extracting("rank", "nickname", "solvedCount", "totalSolvedTime") + .containsExactly( + tuple(1L, "userB", 2L, "01:15:00"), + tuple(2L, "userA", 1L, "00:15:00"), + tuple(3L, "userC", 1L, "01:00:00"), + //TODO 동점자 처리 로직 반영 X + tuple(4L, "userD", 0L, "00:00:00"), + tuple(5L, "userE", 0L, "00:00:00") + + ); + } + + private Map getProblem(DailyDefense dailyDefense, Long problemNumber) { + return dailyDefense.getDailyDefenseProblems().stream() + .filter(problem -> Objects.equals(problem.getProblemNumber(), problemNumber)) + .collect(Collectors.toMap(DailyDefenseProblem::getProblemNumber, DailyDefenseProblem::getProblem)); + } + + private DailyDefense createDailyDefense(LocalDate createdDate) { + AtomicLong problemNumber = new AtomicLong(1L); + Map problemMap = createProblems().stream() + .collect(Collectors.toMap(p-> problemNumber.getAndIncrement(), problem -> problem)); + return dailyDefenseRepository.save(DailyDefense.create(createdDate, "오늘의 문제 테스트", problemMap)); + } + private List createProblems() { + Problem problem1 = Problem.create(1L, B5, 0L); + Problem problem2 = Problem.create(2L, S5, 0L); + Problem problem3 = Problem.create(3L, G5, 0L); + + return problemRepository.saveAll(List.of(problem1, problem2, problem3)); + } + private Member createMember(String nickname, String email) { + return memberRepository.save(Member.create(nickname, email + "@gmail.com", GOOGLE, "test", "test")); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelperTest.java b/src/test/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelperTest.java new file mode 100644 index 00000000..a76c8f74 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelperTest.java @@ -0,0 +1,27 @@ +package kr.co.morandi.backend.defense_record.application.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test") +class TimeFormatHelperTest { + + @DisplayName("시간을 문자열로 변환한다") + @Test + void solvedTimeToString() { + // given + // 1시간 15분 37초를 초로 변환 + Long time = 3600L + (15L * 60L) + 37L; + + // when + String result = TimeFormatHelper.solvedTimeToString(time); + + // then + assertThat(result).isEqualTo("01:15:37"); + + } + +} \ No newline at end of file diff --git a/src/test/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecordTest.java b/src/test/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecordTest.java index 321fee24..d6a2f541 100644 --- a/src/test/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecordTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecordTest.java @@ -21,11 +21,67 @@ import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.groups.Tuple.tuple; import static org.junit.jupiter.api.Assertions.assertNotNull; @ActiveProfiles("test") class DailyRecordTest { + @DisplayName("오늘의 문제를 정답처리 하면 푼 total 문제 수가 증가하고, 푼 시간이 기록된다.") + @Test + void solveProblem() { + // given + DailyDefense dailyDefense = createDailyDefense(); + LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); + Member member = createMember("user"); + Map triedProblem = getProblems(dailyDefense, 2L); + DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, triedProblem); + + // when + dailyRecord.solveProblem(2L, "solvedCode", LocalDateTime.of(2024, 3, 1, 12, 15, 0)); + + + // then + assertThat(dailyRecord) + .extracting("totalSolvedTime", "solvedCount") + .contains( + 15 * 60L, 1L + ); + assertThat(dailyRecord.getDetails()).hasSize(1) + .extracting("isSolved", "solvedTime") + .contains( + tuple(true, 15 * 60L) + ); + } + + @DisplayName("이미 정답처리된 문제를 정답 solved하려하면 바뀌지 않는다.") + @Test + void solveProblemWhenAlreadySolved() { + // given + DailyDefense dailyDefense = createDailyDefense(); + LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); + Member member = createMember("user"); + Map triedProblem = getProblems(dailyDefense, 2L); + DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, triedProblem); + dailyRecord.solveProblem(2L, "solvedCode", LocalDateTime.of(2024, 3, 1, 12, 15, 0)); + + // when + dailyRecord.solveProblem(2L, "solvedCode", LocalDateTime.of(2024, 3, 1, 12, 20, 0)); + + + + // then + assertThat(dailyRecord) + .extracting("totalSolvedTime", "solvedCount") + .contains( + 15 * 60L, 1L + ); + assertThat(dailyRecord.getDetails()).hasSize(1) + .extracting("isSolved", "solvedTime") + .contains( + tuple(true, 15 * 60L) + ); + } @DisplayName("풀어낸 문제들에 대한 문제번호 목록을 반환할 수 있다.") @Test void getSolvedProblemNumbers() { @@ -36,7 +92,7 @@ void getSolvedProblemNumbers() { Map triedProblem = getProblems(dailyDefense, 2L); DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, triedProblem); dailyRecord.tryMoreProblem(getProblems(dailyDefense, 3L)); - dailyRecord.solveProblem(2L, "solvedCode"); + dailyRecord.solveProblem(2L, "solvedCode", LocalDateTime.of(2024, 3, 1, 12, 15, 0)); // when final Set solvedProblemNumbers = dailyRecord.getSolvedProblemNumbers(); diff --git a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepositoryTest.java b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepositoryTest.java index b1794f6b..0e72077d 100644 --- a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepositoryTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepositoryTest.java @@ -2,20 +2,20 @@ import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; -import kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record.DailyRecordRepository; -import kr.co.morandi.backend.member_management.domain.model.member.Member; -import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; -import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; -import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.LocalDateTime; @@ -32,15 +32,13 @@ import static org.assertj.core.groups.Tuple.tuple; @SpringBootTest +@Transactional @ActiveProfiles("test") class DailyRecordRepositoryTest { @Autowired private DailyRecordRepository dailyRecordRepository; - @Autowired - private DailyDefenseProblemRepository dailyDefenseProblemRepository; - @Autowired private DailyDefenseRepository dailyDefenseRepository; @@ -50,15 +48,52 @@ class DailyRecordRepositoryTest { @Autowired private MemberRepository memberRepository; + @DisplayName("특정 시점 DailyRecord의 순위를 조회할 수 있다.") + @Test + void getDailyRecordsRankByDate() { + // given + LocalDate today = LocalDate.of(2021, 10, 1); + final DailyDefense dailyDefense = createDailyDefense(today); + + final Member member1 = createMember("userA", "userA"); + final Member member2 = createMember("userB", "userB"); + final Member member3 = createMember("userC", "userC"); + + final DailyRecord dailyRecord1 = DailyRecord.tryDefense(LocalDateTime.of(2021, 10, 1, 0, 0), dailyDefense, member1, getProblem(dailyDefense, 1L)); + final DailyRecord dailyRecord2 = DailyRecord.tryDefense(LocalDateTime.of(2021, 10, 1, 0, 0), dailyDefense, member2, getProblem(dailyDefense, 2L)); + final DailyRecord dailyRecord3 = DailyRecord.tryDefense(LocalDateTime.of(2021, 10, 1, 0, 0), dailyDefense, member3, getProblem(dailyDefense, 3L)); + + /* + * member1: 한 문제 해결 일찍 + * member2 : 두 문제 해결 + * member3: 한 문제 해결 1보다 늦게 + * + * -> 등수 = 2 -> 1 -> 3 + * */ + dailyRecord1.solveProblem(1L, "exampleCode", LocalDateTime.of(2021, 10, 1, 0, 15)); + + dailyRecord2.solveProblem(2L, "exampleCode", LocalDateTime.of(2021, 10, 1, 0, 30)); + dailyRecord2.tryMoreProblem(getProblem(dailyDefense, 3L)); + dailyRecord2.solveProblem(3L, "exampleCode", LocalDateTime.of(2021, 10, 1, 0, 45)); + + dailyRecord3.solveProblem(3L, "exampleCode", LocalDateTime.of(2021, 10, 1, 1, 0)); + + dailyRecordRepository.saveAll(List.of(dailyRecord1, dailyRecord2, dailyRecord3)); + + + // when + Pageable pageable = PageRequest.of(0, 5); + List dailyRecords = dailyRecordRepository.getDailyRecordsRankByDate(today, pageable); + + // then + assertThat(dailyRecords).hasSize(3) + .extracting(DailyRecord::getMember, DailyRecord::getSolvedCount, DailyRecord::getTotalSolvedTime) + .containsExactly(// 푼 시간은 초단위 + tuple(member2, 2L, 75L * 60), + tuple(member1, 1L, 15L * 60), + tuple(member3, 1L, 60L * 60) + ); - @AfterEach - void tearDown() { - dailyRecordRepository.deleteAll(); - dailyDefenseProblemRepository.deleteAllInBatch(); - dailyDefenseRepository.deleteAllInBatch(); - dailyRecordRepository.deleteAllInBatch(); - problemRepository.deleteAll(); - memberRepository.deleteAll(); } @DisplayName("원하는 recordId에 해당하는 DailyRecord가 존재할 때 찾아올 수 있다.") @@ -153,5 +188,8 @@ private List createProblems() { private Member createMember() { return memberRepository.save(Member.create("test", "test" + "@gmail.com", GOOGLE, "test", "test")); } + private Member createMember(String nickname, String email) { + return memberRepository.save(Member.create(nickname, email + "@gmail.com", GOOGLE, "test", "test")); + } } \ No newline at end of file From ca91cc98fd5551cd2c09868a1cbd221d3bca54b9 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 1 Apr 2024 17:12:02 +0900 Subject: [PATCH 04/44] =?UTF-8?q?fix:=20RankUseCaseImpl=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99=20=EB=B0=8F=20AtomicLon?= =?UTF-8?q?g=20long=20=EC=BA=90=EC=8A=A4=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/{ => service}/DailyRecordRankUseCaseImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/main/java/kr/co/morandi/backend/defense_record/application/{ => service}/DailyRecordRankUseCaseImpl.java (94%) diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseImpl.java b/src/main/java/kr/co/morandi/backend/defense_record/application/service/DailyRecordRankUseCaseImpl.java similarity index 94% rename from src/main/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseImpl.java rename to src/main/java/kr/co/morandi/backend/defense_record/application/service/DailyRecordRankUseCaseImpl.java index 875e0cda..56869157 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseImpl.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/service/DailyRecordRankUseCaseImpl.java @@ -1,4 +1,4 @@ -package kr.co.morandi.backend.defense_record.application; +package kr.co.morandi.backend.defense_record.application.service; import kr.co.morandi.backend.defense_record.application.dto.DailyDefenseRankPageResponse; import kr.co.morandi.backend.defense_record.application.dto.DailyDetailRankResponse; @@ -30,7 +30,7 @@ public DailyDefenseRankPageResponse getDailyRecordRank(LocalDateTime requestTime // 등수 계산 // TODO 동점자 처리 필요 - AtomicLong initialRank = new AtomicLong(page * size + 1); + AtomicLong initialRank = new AtomicLong((long) page * size + 1); final List dailyRecordRanks = dailyRecords.stream() .map(dr -> { From f90fe32a022a8bd573730ffd6a32c0638a2e4c0f Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 1 Apr 2024 17:29:13 +0900 Subject: [PATCH 05/44] =?UTF-8?q?test:=20DailyDefenseInfoResponse=20Test?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DailyDefenseInfoResponseTest.java | 106 ++++++++++++++++++ .../service/DailyDefenseUseCaseImplTest.java | 1 + 2 files changed, 107 insertions(+) create mode 100644 src/test/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponseTest.java diff --git a/src/test/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponseTest.java b/src/test/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponseTest.java new file mode 100644 index 00000000..399f3b84 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponseTest.java @@ -0,0 +1,106 @@ +package kr.co.morandi.backend.defense_information.application.dto.response; + +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.member_management.domain.model.member.Member; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier.*; +import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; + + +@ActiveProfiles("test") +class DailyDefenseInfoResponseTest { + + @DisplayName("시도한 적이 있는 DailyDefense Response DTO를 반환할 수 있다.") + @Test + void ofNonAttempted() { + // given + DailyDefense dailyDefense = createDailyDefense(); + + // when + DailyDefenseInfoResponse response = DailyDefenseInfoResponse.fromNonAttempted(dailyDefense); + + // then + assertThat(response) + .extracting("defenseName", "problemCount", "attemptCount") + .contains(dailyDefense.getContentName(), dailyDefense.getDailyDefenseProblems().size(), 0L); + + assertThat(response.getProblems()) + .extracting("problemNumber", "baekjoonProblemId", "difficulty", "solvedCount", "submitCount", "isSolved") + .containsExactlyInAnyOrder( + tuple(1L, 1L, B5, 0L, 0L, null), + tuple(2L, 2L, S5, 0L, 0L, null), + tuple(3L, 3L, G5, 0L, 0L, null) + ); + + + } + + @DisplayName("시도한 적이 있는 DailyDefense Response DTO를 반환할 수 있다.") + @Test + void ofAttempted() { + // given + DailyDefense dailyDefense = createDailyDefense(); + LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); + Member member = createMember("user"); + Map problems = getProblems(dailyDefense, 2L); + DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, problems); + + + // when + DailyDefenseInfoResponse response = DailyDefenseInfoResponse.ofAttempted(dailyDefense, dailyRecord); + + // then + assertThat(response) + .extracting("defenseName", "problemCount", "attemptCount") + .contains(dailyDefense.getContentName(), dailyDefense.getDailyDefenseProblems().size(), 1L); + + assertThat(response.getProblems()) + .extracting("problemNumber", "baekjoonProblemId", "difficulty", "solvedCount", "submitCount", "isSolved") + .containsExactlyInAnyOrder( + tuple(1L, 1L, B5, 0L, 0L, false), + tuple(2L, 2L, S5, 0L, 0L, false), + tuple(3L, 3L, G5, 0L, 0L, false) + ); + } + + private Map getProblems(DailyDefense DailyDefense, Long problemNumber) { + return DailyDefense.getDailyDefenseProblems().stream() + .filter(p -> p.getProblemNumber().equals(problemNumber)) + .collect(Collectors.toMap(DailyDefenseProblem::getProblemNumber, DailyDefenseProblem::getProblem)); + } + + private DailyDefense createDailyDefense() { + AtomicLong problemNumber = new AtomicLong(1L); + Map problemMap = createProblems().stream() + .collect(Collectors.toMap(p -> problemNumber.getAndIncrement(), problem -> problem)); + LocalDate createdDate = LocalDate.of(2024, 3, 1); + return DailyDefense.create(createdDate, "오늘의 문제 테스트", problemMap); + } + + private List createProblems() { + Problem problem1 = Problem.create(1L, B5, 0L); + Problem problem2 = Problem.create(2L, S5, 0L); + Problem problem3 = Problem.create(3L, G5, 0L); + return List.of(problem1, problem2, problem3); + } + + private Member createMember(String name) { + return Member.create(name, name + "@gmail.com", GOOGLE, name, name); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java b/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java index 6d3a06e5..41717aaf 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java @@ -86,6 +86,7 @@ void getDailyDefenseInfo() { @Test void getDailyDefenseInfoWithMemberAndRecord() { // given + final DailyDefense dailyDefense = createDailyDefense(LocalDate.of(2021, 10, 1), "오늘의 문제 테스트"); Member member = createMember(); LocalDateTime requestTime = LocalDateTime.of(2021, 10, 1, 12, 0, 0); From ae6ef73c8cb830878d4e4ad14d9c9ead165e2376 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 2 Apr 2024 19:07:58 +0900 Subject: [PATCH 06/44] =?UTF-8?q?refactor:=20DTO=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20Mapper=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../defenseproblem/DefenseProblemMapper.java | 41 ++++++++++++ .../session/StartDailyDefenseMapper.java | 26 +++++++ .../mapper/tempcode/TempCodeMapper.java | 40 +++++++++++ .../session/DefenseProblemResponse.java | 46 +++---------- .../session/StartDailyDefenseResponse.java | 40 +++++++++++ .../StartDailyDefenseServiceResponse.java | 52 -------------- .../response/tempcode/TempCodeResponse.java | 21 ++++++ .../DailyDefenseManagementService.java | 7 +- .../domain/model/session/SessionDetail.java | 4 +- .../mapper/tempcode/TempCodeMapperTest.java | 67 +++++++++++++++++++ .../DailyDefenseManagementServiceTest.java | 10 +-- 11 files changed, 253 insertions(+), 101 deletions(-) create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java delete mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseServiceResponse.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/response/tempcode/TempCodeResponse.java create mode 100644 src/test/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapperTest.java diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java new file mode 100644 index 00000000..9f1aa937 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java @@ -0,0 +1,41 @@ +package kr.co.morandi.backend.defense_management.application.mapper.defenseproblem; + +import kr.co.morandi.backend.defense_management.application.mapper.tempcode.TempCodeMapper; +import kr.co.morandi.backend.defense_management.application.response.session.DefenseProblemResponse; +import kr.co.morandi.backend.defense_management.application.response.tempcode.TempCodeResponse; +import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; +import kr.co.morandi.backend.defense_management.domain.model.session.SessionDetail; +import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class DefenseProblemMapper { + + public static List of(Map tryProblem, DefenseSession defenseSession, DailyRecord dailyRecord) { + return tryProblem.entrySet().stream() + .map(entry -> { + final Long problemNumber = entry.getKey(); + final Problem problem = entry.getValue(); + final boolean isCorrect = dailyRecord.isSolvedProblem(problemNumber); + + final SessionDetail sessionDetail = defenseSession.getSessionDetail(problemNumber); + + final Language lastAccessLanguage = sessionDetail.getLastAccessLanguage(); + final Set tempCodeResponses = TempCodeMapper.createTempCodeResponses(sessionDetail.getTempCodes()); + + return DefenseProblemResponse.builder() + .problemId(problem.getProblemId()) + .baekjoonProblemId(problem.getBaekjoonProblemId()) + .problemNumber(problemNumber) + .isCorrect(isCorrect) + .lastAccessLanguage(lastAccessLanguage) + .tempCodes(tempCodeResponses) + .build(); + }) + .toList(); + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java new file mode 100644 index 00000000..652765bd --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java @@ -0,0 +1,26 @@ +package kr.co.morandi.backend.defense_management.application.mapper.session; + +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; +import kr.co.morandi.backend.defense_management.application.mapper.defenseproblem.DefenseProblemMapper; +import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; +import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; + +import java.util.Map; + +public class StartDailyDefenseMapper { + + public static StartDailyDefenseResponse of(Map tryProblem, + DailyDefense dailyDefense, + DefenseSession defenseSession, + DailyRecord dailyRecord) { + return StartDailyDefenseResponse.builder() + .defenseSessionId(defenseSession.getDefenseSessionId()) + .contentName(dailyDefense.getContentName()) + .defenseType(dailyDefense.getDefenseType()) + .lastAccessTime(defenseSession.getLastAccessDateTime()) + .defenseProblems(DefenseProblemMapper.of(tryProblem, defenseSession, dailyRecord)) + .build(); + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java new file mode 100644 index 00000000..c492ca14 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java @@ -0,0 +1,40 @@ +package kr.co.morandi.backend.defense_management.application.mapper.tempcode; + +import kr.co.morandi.backend.defense_management.application.response.tempcode.TempCodeResponse; +import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language; +import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.TempCode; + +import java.util.*; +import java.util.stream.Collectors; + +public class TempCodeMapper { + private static final Map intialTempCodeMap = + Arrays.stream(Language.values()) + .collect(Collectors.toMap( + language -> language, + language -> TempCodeResponse.builder() + .language(language) + .code(language.getInitialCode()) + .build())); + + /* + * TempCode 전체를 한 번에 반환하기 위해 initialTempCodeMap을 이용하여 + * 수집된 TempCode들로 replace하며 TempCodeResponse를 만들어 반환한다. + * */ + public static Set createTempCodeResponses(Set tempCodes) { + + // 기본 코드를 가지고 있는 Map을 만들어서 + Map tempCodeMap = new HashMap<>(intialTempCodeMap); + + // tempCode를 순회하면서 tempCodeMap에 해당 언어의 TempCodeResponse를 넣어준다. + tempCodes.forEach(tempCode -> { + tempCodeMap.replace(tempCode.getLanguage(), TempCodeResponse.builder() + .language(tempCode.getLanguage()) + .code(tempCode.getCode()) + .build()); + }); + + return new HashSet<>(tempCodeMap.values()); + } + +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java index 5c2cb65d..c65297be 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java @@ -1,18 +1,13 @@ package kr.co.morandi.backend.defense_management.application.response.session; -import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; -import kr.co.morandi.backend.defense_management.domain.model.session.SessionDetail; +import kr.co.morandi.backend.defense_management.application.response.tempcode.TempCodeResponse; import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language; -import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.TempCode; -import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; -import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.List; -import java.util.Map; +import java.util.Set; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -22,42 +17,17 @@ public class DefenseProblemResponse { private Long problemNumber; private Long baekjoonProblemId; private boolean isCorrect; - private Language tempCodeLanguage; - private String tempCode; + private Language lastAccessLanguage; + private Set tempCodes; - - public static List fromDailyDefense(Map tryProblem, DefenseSession defenseSession, DailyRecord dailyRecord) { - return tryProblem.entrySet().stream() - .map(entry -> { - final Long problemNumber = entry.getKey(); - final Problem problem = entry.getValue(); - final boolean isCorrect = dailyRecord.isSolvedProblem(problemNumber); - - final SessionDetail sessionDetail = defenseSession.getSessionDetail(problemNumber); - - final Language lastAccessLanguage = sessionDetail.getLastAccessLanguage(); - final TempCode tempCode = sessionDetail.getTempCode(lastAccessLanguage); - - return DefenseProblemResponse.builder() - .problemId(problem.getProblemId()) - .baekjoonProblemId(problem.getBaekjoonProblemId()) - .problemNumber(problemNumber) - .isCorrect(isCorrect) - .tempCode(tempCode.getCode()) - .tempCodeLanguage(lastAccessLanguage) - .build(); - - }) - .toList(); - } @Builder - private DefenseProblemResponse(Long problemId, Long problemNumber, Long baekjoonProblemId, boolean isCorrect, - Language tempCodeLanguage, String tempCode) { + private DefenseProblemResponse(Long problemId, Long problemNumber, Long baekjoonProblemId, + boolean isCorrect, Language lastAccessLanguage, Set tempCodes) { this.problemId = problemId; this.problemNumber = problemNumber; this.baekjoonProblemId = baekjoonProblemId; this.isCorrect = isCorrect; - this.tempCodeLanguage = tempCodeLanguage; - this.tempCode = tempCode; + this.lastAccessLanguage = lastAccessLanguage; + this.tempCodes = tempCodes; } } diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java new file mode 100644 index 00000000..485b529f --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java @@ -0,0 +1,40 @@ +package kr.co.morandi.backend.defense_management.application.response.session; + +import kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; +import kr.co.morandi.backend.defense_management.application.mapper.defenseproblem.DefenseProblemMapper; +import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class StartDailyDefenseResponse { + + private Long defenseSessionId; + private String contentName; + private DefenseType defenseType; + private LocalDateTime lastAccessTime; + private List defenseProblems; + + @Builder + private StartDailyDefenseResponse(Long defenseSessionId, + String contentName, + DefenseType defenseType, + LocalDateTime lastAccessTime, + List defenseProblems) { + this.defenseSessionId = defenseSessionId; + this.defenseType = defenseType; + this.contentName = contentName; + this.lastAccessTime = lastAccessTime; + this.defenseProblems = defenseProblems; + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseServiceResponse.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseServiceResponse.java deleted file mode 100644 index a12500d3..00000000 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseServiceResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -package kr.co.morandi.backend.defense_management.application.response.session; - -import kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType; -import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; -import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; -import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; -import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class StartDailyDefenseServiceResponse { - - private Long defenseSessionId; - private String contentName; - private DefenseType defenseType; - private LocalDateTime lastAccessTime; - private List defenseProblems; - - public static StartDailyDefenseServiceResponse from(Map tryProblem, - DailyDefense dailyDefense, - DefenseSession defenseSession, - DailyRecord dailyRecord) { - return StartDailyDefenseServiceResponse.builder() - .defenseSessionId(defenseSession.getDefenseSessionId()) - .contentName(dailyDefense.getContentName()) - .defenseType(dailyDefense.getDefenseType()) - .lastAccessTime(defenseSession.getLastAccessDateTime()) - .defenseProblems(DefenseProblemResponse.fromDailyDefense(tryProblem, defenseSession, dailyRecord)) - .build(); - } - - @Builder - private StartDailyDefenseServiceResponse(Long defenseSessionId, - String contentName, - DefenseType defenseType, - LocalDateTime lastAccessTime, - List defenseProblems) { - this.defenseSessionId = defenseSessionId; - this.defenseType = defenseType; - this.contentName = contentName; - this.lastAccessTime = lastAccessTime; - this.defenseProblems = defenseProblems; - } -} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/tempcode/TempCodeResponse.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/tempcode/TempCodeResponse.java new file mode 100644 index 00000000..eaee2872 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/tempcode/TempCodeResponse.java @@ -0,0 +1,21 @@ +package kr.co.morandi.backend.defense_management.application.response.tempcode; + +import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TempCodeResponse { + + private Language language; + private String code; + + @Builder + private TempCodeResponse(Language language, String code) { + this.language = language; + this.code = code; + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java index cf68f643..2119115f 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java @@ -1,12 +1,13 @@ package kr.co.morandi.backend.defense_management.application.service.session; import kr.co.morandi.backend.defense_information.application.port.out.dailydefense.DailyDefensePort; +import kr.co.morandi.backend.defense_management.application.mapper.session.StartDailyDefenseMapper; import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.service.defense.ProblemGenerationService; import kr.co.morandi.backend.defense_management.application.request.session.StartDailyDefenseServiceRequest; -import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseServiceResponse; +import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; @@ -32,7 +33,7 @@ public class DailyDefenseManagementService { private final DefenseSessionPort defenseSessionPort; @Transactional - public StartDailyDefenseServiceResponse startDailyDefense(StartDailyDefenseServiceRequest request, Member member, LocalDateTime requestTime) { + public StartDailyDefenseResponse startDailyDefense(StartDailyDefenseServiceRequest request, Member member, LocalDateTime requestTime) { Long problemNumber = request.getProblemNumber(); // 세션이랑 세션 Detail을 찾아서 응시 기록이 있는지 살펴보기 @@ -60,7 +61,7 @@ public StartDailyDefenseServiceResponse startDailyDefense(StartDailyDefenseServi final DefenseSession savedDefenseSession = defenseSessionPort.saveDefenseSession(defenseSession); // 문제 목록을 DefenseProblemResponse DTO로 변환 - return StartDailyDefenseServiceResponse.from(tryProblem, dailyDefense, savedDefenseSession, dailyRecord); + return StartDailyDefenseMapper.of(tryProblem, dailyDefense, savedDefenseSession, dailyRecord); } private DefenseSession createNewSession(Member member, LocalDateTime now, DailyDefense dailyDefense, Map tryProblem) { DailyRecord dailyRecord = DailyRecord.tryDefense(now, dailyDefense, member, tryProblem); diff --git a/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/SessionDetail.java b/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/SessionDetail.java index 74e7351d..6a42eadc 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/SessionDetail.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/SessionDetail.java @@ -9,9 +9,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; +import java.util.*; @Entity @Getter diff --git a/src/test/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapperTest.java b/src/test/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapperTest.java new file mode 100644 index 00000000..ef04a710 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapperTest.java @@ -0,0 +1,67 @@ +package kr.co.morandi.backend.defense_management.application.mapper.tempcode; + +import kr.co.morandi.backend.defense_management.application.response.tempcode.TempCodeResponse; +import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.TempCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Set; + +import static kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; + +@ActiveProfiles("test") +class TempCodeMapperTest { + + @DisplayName("아무 것도 포함하지 않고 생성하면 초기화된 TempCodeResponses를 반환한다.") + @Test + void createTempCodeResponses() { + // given + Set tempCodes = Set.of(); + + // when + final Set responses = TempCodeMapper.createTempCodeResponses(tempCodes); + + // then + assertThat(responses).hasSize(3) + .extracting("language", "code") + .containsExactlyInAnyOrder( + tuple(JAVA, JAVA.getInitialCode()), + tuple(PYTHON, PYTHON.getInitialCode()), + tuple(CPP, CPP.getInitialCode()) + ); + + } + + @DisplayName("일부 언어를 포함하고 나머지는 초기화된 TempCodeResponses를 반환한다.") + @Test + void createTempCodeResponsesWithSomeTempCodes() { + //given + Set tempCodes = Set.of( + TempCode.builder() + .language(JAVA) + .code("java code") + .build(), + TempCode.builder() + .language(PYTHON) + .code("python code") + .build() + ); + + // when + final Set responses = TempCodeMapper.createTempCodeResponses(tempCodes); + + // then + assertThat(responses).hasSize(3) + .extracting("language", "code") + .containsExactlyInAnyOrder( + tuple(JAVA, "java code"), + tuple(PYTHON, "python code"), + tuple(CPP, CPP.getInitialCode()) + ); + + } + +} \ No newline at end of file diff --git a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java index d1f32cfc..11ec2239 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java @@ -2,7 +2,7 @@ import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_management.application.request.session.StartDailyDefenseServiceRequest; -import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseServiceResponse; +import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; import kr.co.morandi.backend.defense_management.application.service.session.DailyDefenseManagementService; import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; @@ -101,7 +101,7 @@ void retryDailyDefenseWhenDayPassed() { LocalDateTime retryRequestTime = LocalDateTime.of(2021, 10, 2, 12, 0, 0); // when - final StartDailyDefenseServiceResponse response = dailyDefenseManagementService.startDailyDefense(retryRequest, member, retryRequestTime); + final StartDailyDefenseResponse response = dailyDefenseManagementService.startDailyDefense(retryRequest, member, retryRequestTime); // then @@ -139,7 +139,7 @@ void retryDailyDefenseWithOtherProblem() { LocalDateTime retryRequestTime = LocalDateTime.of(2021, 10, 1, 12, 0, 0); // when - final StartDailyDefenseServiceResponse response = dailyDefenseManagementService.startDailyDefense(retryRequest, member, retryRequestTime); + final StartDailyDefenseResponse response = dailyDefenseManagementService.startDailyDefense(retryRequest, member, retryRequestTime); // then assertAll( @@ -173,7 +173,7 @@ void retryDailyDefense() { LocalDateTime retryRequestTime = LocalDateTime.of(2021, 10, 1, 12, 0, 0); // when - final StartDailyDefenseServiceResponse response = dailyDefenseManagementService.startDailyDefense(request, member, retryRequestTime); + final StartDailyDefenseResponse response = dailyDefenseManagementService.startDailyDefense(request, member, retryRequestTime); // then assertAll( @@ -203,7 +203,7 @@ void startDailyDefense() { .build(); // when - final StartDailyDefenseServiceResponse response = dailyDefenseManagementService.startDailyDefense(request, member, requestTime); + final StartDailyDefenseResponse response = dailyDefenseManagementService.startDailyDefense(request, member, requestTime); // then From 8656861faaa71360877f08a70528d206a0d6c776 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 2 Apr 2024 20:04:05 +0900 Subject: [PATCH 07/44] =?UTF-8?q?feat:=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=A0=95=EB=B3=B4=20=EB=B0=98=ED=99=98,?= =?UTF-8?q?=20=EC=98=A4=EB=8A=98=EC=9D=98=20=EB=AC=B8=EC=A0=9C=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=20Controller=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +- .../controller/DailyDefenseController.java | 28 ++++++++ .../controller/DailyRecordController.java | 26 ++++++++ .../DailyDefenseControllerTest.java | 57 +++++++++++++++++ .../controller/DailyRecordControllerTest.java | 64 +++++++++++++++++++ 5 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 src/main/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseController.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordController.java create mode 100644 src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java create mode 100644 src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java diff --git a/build.gradle b/build.gradle index e0487984..775c6a3b 100644 --- a/build.gradle +++ b/build.gradle @@ -76,9 +76,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' +// implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + +// implementation 'org.springframework.boot:spring-boot-starter-security' +// testImplementation 'org.springframework.security:spring-security-test' - implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' @@ -88,7 +90,6 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' testImplementation 'com.h2database:h2' } diff --git a/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseController.java b/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseController.java new file mode 100644 index 00000000..29235bfa --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseController.java @@ -0,0 +1,28 @@ +package kr.co.morandi.backend.defense_information.infrastructure.controller; + +import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; +import kr.co.morandi.backend.defense_information.application.port.in.DailyDefenseUseCase; +import kr.co.morandi.backend.member_management.domain.model.member.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; + +import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; + +@RestController +@RequiredArgsConstructor +public class DailyDefenseController { + + private final DailyDefenseUseCase dailyDefenseUseCase; + + @GetMapping("/daily-defense") + public DailyDefenseInfoResponse getDailyDefenseInfo() { + //TODO SecurityContext에서 Member 정보 가져오기 + Member member = Member.create("", "", GOOGLE, "", ""); + return dailyDefenseUseCase.getDailyDefenseInfo(member, LocalDateTime.now()); + } + + +} diff --git a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordController.java b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordController.java new file mode 100644 index 00000000..e1d6a157 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordController.java @@ -0,0 +1,26 @@ +package kr.co.morandi.backend.defense_record.infrastructure.controller; + +import kr.co.morandi.backend.defense_record.application.dto.DailyDefenseRankPageResponse; +import kr.co.morandi.backend.defense_record.application.port.in.DailyRecordRankUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; + +@RestController +@RequiredArgsConstructor +public class DailyRecordController { + + private final DailyRecordRankUseCase dailyRecordRankUseCase; + + @GetMapping("/daily-record/rankings") + public ResponseEntity getDailyRecordRank(@RequestParam(value = "page", defaultValue = "0") int page, + @RequestParam(value = "size", defaultValue = "5") int size) { + + return ResponseEntity.ok(dailyRecordRankUseCase.getDailyRecordRank(LocalDateTime.now(), page, size)); + } + +} diff --git a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java new file mode 100644 index 00000000..dc5089fa --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java @@ -0,0 +1,57 @@ +package kr.co.morandi.backend.defense_information.infrastructure.controller; + +import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; +import kr.co.morandi.backend.defense_information.application.port.in.DailyDefenseUseCase; +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.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = DailyDefenseController.class) +@ActiveProfiles("test") +class DailyDefenseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private DailyDefenseUseCase dailyDefenseUseCase; + + @DisplayName("DailyDefense 정보를 로그인하지 않은 상태에서 가져올 수 있다.") + @Test + void getDailyDefenseInfo() throws Exception { + // given + when(dailyDefenseUseCase.getDailyDefenseInfo(any(), any())) + .thenReturn(DailyDefenseInfoResponse.builder() + .problems(List.of()) + .defenseName("test") + .attemptCount(1L) + .problemCount(5) + .build()); + + // when + final ResultActions perform = mockMvc.perform(get("/daily-defense")); + + // then + perform + .andExpect(status().isOk()) + .andExpect(jsonPath("$.problems").isArray()) + .andExpect(jsonPath("$.defenseName").isString()) + .andExpect(jsonPath("$.attemptCount").isNumber()) + .andExpect(jsonPath("$.problemCount").isNumber()); + + } + +} \ No newline at end of file diff --git a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java new file mode 100644 index 00000000..fbf5d3a2 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java @@ -0,0 +1,64 @@ +package kr.co.morandi.backend.defense_record.infrastructure.controller; + +import kr.co.morandi.backend.defense_record.application.dto.DailyDefenseRankPageResponse; +import kr.co.morandi.backend.defense_record.application.port.in.DailyRecordRankUseCase; +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.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = DailyRecordController.class) +@ActiveProfiles("test") +class DailyRecordControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private DailyRecordRankUseCase dailyRecordRankUseCase; + + @DisplayName("[GET] DailyDefense 순위를 조회한다.") + @Test + void getDailyRecordRank() throws Exception { + // given + int page = 0; + int size = 5; + when(dailyRecordRankUseCase.getDailyRecordRank(any(), anyInt(), anyInt())) + .thenReturn(DailyDefenseRankPageResponse.builder() + .totalPage(1) + .currentPage(0) + .dailyRecords(List.of()) + .build()); + + + // when + ResultActions perform = mockMvc.perform( + get("/daily-record/rankings") + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size) + )); + + + // then + perform + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalPage").isNumber()) + .andExpect(jsonPath("$.currentPage").isNumber()) + .andExpect(jsonPath("$.dailyRecords").isArray()); + } + + +} \ No newline at end of file From 0a57c33d09a17e72574b907f7c61c49803ba36eb Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 2 Apr 2024 20:06:00 +0900 Subject: [PATCH 08/44] =?UTF-8?q?:art:=20Mapper=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=9E=90=20private=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/defenseproblem/DefenseProblemMapper.java | 3 +++ .../application/mapper/session/StartDailyDefenseMapper.java | 3 +++ .../application/mapper/tempcode/TempCodeMapper.java | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java index 9f1aa937..ebf4207e 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java @@ -8,11 +8,14 @@ import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; import java.util.Set; +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class DefenseProblemMapper { public static List of(Map tryProblem, DefenseSession defenseSession, DailyRecord dailyRecord) { diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java index 652765bd..64b38983 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java @@ -6,9 +6,12 @@ import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import java.util.Map; +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class StartDailyDefenseMapper { public static StartDailyDefenseResponse of(Map tryProblem, diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java index c492ca14..d97d665f 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java @@ -3,10 +3,14 @@ import kr.co.morandi.backend.defense_management.application.response.tempcode.TempCodeResponse; import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language; import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.TempCode; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import java.util.*; import java.util.stream.Collectors; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class TempCodeMapper { private static final Map intialTempCodeMap = Arrays.stream(Language.values()) From fcc7d74270d95b55f03f8fa6b04439ca21cf8c5e Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 2 Apr 2024 20:07:36 +0900 Subject: [PATCH 09/44] =?UTF-8?q?:art:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=A4=91=EA=B4=84=ED=98=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/mapper/tempcode/TempCodeMapper.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java index d97d665f..86782a46 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java @@ -31,12 +31,10 @@ public static Set createTempCodeResponses(Set tempCo Map tempCodeMap = new HashMap<>(intialTempCodeMap); // tempCode를 순회하면서 tempCodeMap에 해당 언어의 TempCodeResponse를 넣어준다. - tempCodes.forEach(tempCode -> { - tempCodeMap.replace(tempCode.getLanguage(), TempCodeResponse.builder() - .language(tempCode.getLanguage()) - .code(tempCode.getCode()) - .build()); - }); + tempCodes.forEach(tempCode -> tempCodeMap.replace(tempCode.getLanguage(), TempCodeResponse.builder() + .language(tempCode.getLanguage()) + .code(tempCode.getCode()) + .build())); return new HashSet<>(tempCodeMap.values()); } From 5ff52c0a58743d67254a29d8a4413e542900cc0f Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 2 Apr 2024 20:08:43 +0900 Subject: [PATCH 10/44] :fire: Remove unused import 'StartDailyDefenseResponse' --- .../response/session/StartDailyDefenseResponse.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java index 485b529f..1b485a72 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java @@ -1,11 +1,6 @@ package kr.co.morandi.backend.defense_management.application.response.session; import kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType; -import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; -import kr.co.morandi.backend.defense_management.application.mapper.defenseproblem.DefenseProblemMapper; -import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; -import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; -import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -13,7 +8,6 @@ import java.time.LocalDateTime; import java.util.List; -import java.util.Map; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) From 014182dcc1ddf113cb5498596db31ca48e24fc38 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 2 Apr 2024 20:17:57 +0900 Subject: [PATCH 11/44] =?UTF-8?q?:art:=20Dailydefense=20mapper=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/DailyDefenseInfoResponse.java | 23 ------------ .../DailyDefenseProblemInfoResponse.java | 27 -------------- .../dailydefense/DailyDefenseInfoMapper.java | 32 ++++++++++++++++ .../DailyDefenseProblemInfoMapper.java | 37 +++++++++++++++++++ .../service/DailyDefenseUseCaseImpl.java | 5 ++- .../DailyDefenseInfoMapperTest.java} | 9 +++-- 6 files changed, 77 insertions(+), 56 deletions(-) create mode 100644 src/main/java/kr/co/morandi/backend/defense_information/application/mapper/dailydefense/DailyDefenseInfoMapper.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_information/application/mapper/dailydefense/DailyDefenseProblemInfoMapper.java rename src/test/java/kr/co/morandi/backend/defense_information/application/{dto/response/DailyDefenseInfoResponseTest.java => mapper/dailydefense/DailyDefenseInfoMapperTest.java} (90%) diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponse.java b/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponse.java index 8973b69b..8c72a786 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponse.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponse.java @@ -1,7 +1,5 @@ package kr.co.morandi.backend.defense_information.application.dto.response; -import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; -import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -19,27 +17,6 @@ public class DailyDefenseInfoResponse { private List problems; - public static DailyDefenseInfoResponse fromNonAttempted(DailyDefense dailyDefense) { - return DailyDefenseInfoResponse.builder() - .defenseName(dailyDefense.getContentName()) - .problemCount(dailyDefense.getProblemCount()) - .attemptCount(dailyDefense.getAttemptCount()) - .problems(DailyDefenseProblemInfoResponse.ofNonAttempted(dailyDefense.getDailyDefenseProblems())) - .build(); - } - - public static DailyDefenseInfoResponse ofAttempted(DailyDefense dailyDefense, DailyRecord dailyRecord) { - return DailyDefenseInfoResponse.builder() - .defenseName(dailyDefense.getContentName()) - .problemCount(dailyDefense.getProblemCount()) - .attemptCount(dailyDefense.getAttemptCount()) - .problems(DailyDefenseProblemInfoResponse.ofAttempted( - dailyDefense.getDailyDefenseProblems(), - dailyRecord.getSolvedProblemNumbers()) - ) - .build(); - } - @Builder private DailyDefenseInfoResponse(String defenseName, Integer problemCount, Long attemptCount, List problems) { this.defenseName = defenseName; diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseProblemInfoResponse.java b/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseProblemInfoResponse.java index 5aa81c0e..8f9b635a 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseProblemInfoResponse.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseProblemInfoResponse.java @@ -1,15 +1,11 @@ package kr.co.morandi.backend.defense_information.application.dto.response; import com.fasterxml.jackson.annotation.JsonInclude; -import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; import kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.List; -import java.util.Set; - @Getter @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) @@ -23,29 +19,6 @@ public class DailyDefenseProblemInfoResponse { private Long submitCount; private Boolean isSolved; - public static List ofNonAttempted(List dailyDefenseProblems) { - return dailyDefenseProblems.stream().map(problem -> DailyDefenseProblemInfoResponse.builder() - .problemNumber(problem.getProblemNumber()) - .problemId(problem.getProblem().getProblemId()) - .baekjoonProblemId(problem.getProblem().getBaekjoonProblemId()) - .difficulty(problem.getProblem().getProblemTier()) - .solvedCount(problem.getSolvedCount()) - .submitCount(problem.getSubmitCount()) - .build() - ).toList(); - } - public static List ofAttempted(List dailyDefenseProblems, Set solvedProblemNumbers) { - return dailyDefenseProblems.stream().map(problem -> DailyDefenseProblemInfoResponse.builder() - .problemNumber(problem.getProblemNumber()) - .problemId(problem.getProblem().getProblemId()) - .baekjoonProblemId(problem.getProblem().getBaekjoonProblemId()) - .difficulty(problem.getProblem().getProblemTier()) - .solvedCount(problem.getSolvedCount()) - .submitCount(problem.getSubmitCount()) - .isSolved(solvedProblemNumbers.contains(problem.getProblemNumber())) - .build() - ).toList(); - } @Builder private DailyDefenseProblemInfoResponse(Long problemNumber, Long problemId, Long baekjoonProblemId, ProblemTier difficulty, Long solvedCount, Long submitCount, Boolean isSolved) { this.problemNumber = problemNumber; diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/mapper/dailydefense/DailyDefenseInfoMapper.java b/src/main/java/kr/co/morandi/backend/defense_information/application/mapper/dailydefense/DailyDefenseInfoMapper.java new file mode 100644 index 00000000..ff38cd66 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/mapper/dailydefense/DailyDefenseInfoMapper.java @@ -0,0 +1,32 @@ +package kr.co.morandi.backend.defense_information.application.mapper.dailydefense; + +import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class DailyDefenseInfoMapper { + + public static DailyDefenseInfoResponse fromNonAttempted(DailyDefense dailyDefense) { + return DailyDefenseInfoResponse.builder() + .defenseName(dailyDefense.getContentName()) + .problemCount(dailyDefense.getProblemCount()) + .attemptCount(dailyDefense.getAttemptCount()) + .problems(DailyDefenseProblemInfoMapper.ofNonAttempted(dailyDefense.getDailyDefenseProblems())) + .build(); + } + + public static DailyDefenseInfoResponse ofAttempted(DailyDefense dailyDefense, DailyRecord dailyRecord) { + return DailyDefenseInfoResponse.builder() + .defenseName(dailyDefense.getContentName()) + .problemCount(dailyDefense.getProblemCount()) + .attemptCount(dailyDefense.getAttemptCount()) + .problems(DailyDefenseProblemInfoMapper.ofAttempted( + dailyDefense.getDailyDefenseProblems(), + dailyRecord.getSolvedProblemNumbers()) + ) + .build(); + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/mapper/dailydefense/DailyDefenseProblemInfoMapper.java b/src/main/java/kr/co/morandi/backend/defense_information/application/mapper/dailydefense/DailyDefenseProblemInfoMapper.java new file mode 100644 index 00000000..fec9293a --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/mapper/dailydefense/DailyDefenseProblemInfoMapper.java @@ -0,0 +1,37 @@ +package kr.co.morandi.backend.defense_information.application.mapper.dailydefense; + +import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseProblemInfoResponse; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Set; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class DailyDefenseProblemInfoMapper { + + public static List ofNonAttempted(List dailyDefenseProblems) { + return dailyDefenseProblems.stream().map(problem -> DailyDefenseProblemInfoResponse.builder() + .problemNumber(problem.getProblemNumber()) + .problemId(problem.getProblem().getProblemId()) + .baekjoonProblemId(problem.getProblem().getBaekjoonProblemId()) + .difficulty(problem.getProblem().getProblemTier()) + .solvedCount(problem.getSolvedCount()) + .submitCount(problem.getSubmitCount()) + .build() + ).toList(); + } + public static List ofAttempted(List dailyDefenseProblems, Set solvedProblemNumbers) { + return dailyDefenseProblems.stream().map(problem -> DailyDefenseProblemInfoResponse.builder() + .problemNumber(problem.getProblemNumber()) + .problemId(problem.getProblem().getProblemId()) + .baekjoonProblemId(problem.getProblem().getBaekjoonProblemId()) + .difficulty(problem.getProblem().getProblemTier()) + .solvedCount(problem.getSolvedCount()) + .submitCount(problem.getSubmitCount()) + .isSolved(solvedProblemNumbers.contains(problem.getProblemNumber())) + .build() + ).toList(); + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java b/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java index e72bdd40..c9817e61 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java @@ -1,6 +1,7 @@ package kr.co.morandi.backend.defense_information.application.service; import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; +import kr.co.morandi.backend.defense_information.application.mapper.dailydefense.DailyDefenseInfoMapper; import kr.co.morandi.backend.defense_information.application.port.in.DailyDefenseUseCase; import kr.co.morandi.backend.defense_information.application.port.out.dailydefense.DailyDefensePort; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; @@ -32,10 +33,10 @@ public DailyDefenseInfoResponse getDailyDefenseInfo(Member member, LocalDateTime Optional maybeDailyRecord = dailyRecordPort.findDailyRecord(member, requestDateTime.toLocalDate()); if(maybeDailyRecord.isPresent()) { DailyRecord dailyRecord = maybeDailyRecord.get(); - return DailyDefenseInfoResponse.ofAttempted(dailyDefense, dailyRecord); + return DailyDefenseInfoMapper.ofAttempted(dailyDefense, dailyRecord); } } - return DailyDefenseInfoResponse.fromNonAttempted(dailyDefense); + return DailyDefenseInfoMapper.fromNonAttempted(dailyDefense); } } diff --git a/src/test/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponseTest.java b/src/test/java/kr/co/morandi/backend/defense_information/application/mapper/dailydefense/DailyDefenseInfoMapperTest.java similarity index 90% rename from src/test/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponseTest.java rename to src/test/java/kr/co/morandi/backend/defense_information/application/mapper/dailydefense/DailyDefenseInfoMapperTest.java index 399f3b84..793009da 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseInfoResponseTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/application/mapper/dailydefense/DailyDefenseInfoMapperTest.java @@ -1,5 +1,6 @@ -package kr.co.morandi.backend.defense_information.application.dto.response; +package kr.co.morandi.backend.defense_information.application.mapper.dailydefense; +import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; @@ -23,7 +24,7 @@ @ActiveProfiles("test") -class DailyDefenseInfoResponseTest { +class DailyDefenseInfoMapperTest { @DisplayName("시도한 적이 있는 DailyDefense Response DTO를 반환할 수 있다.") @Test @@ -32,7 +33,7 @@ void ofNonAttempted() { DailyDefense dailyDefense = createDailyDefense(); // when - DailyDefenseInfoResponse response = DailyDefenseInfoResponse.fromNonAttempted(dailyDefense); + DailyDefenseInfoResponse response = DailyDefenseInfoMapper.fromNonAttempted(dailyDefense); // then assertThat(response) @@ -62,7 +63,7 @@ void ofAttempted() { // when - DailyDefenseInfoResponse response = DailyDefenseInfoResponse.ofAttempted(dailyDefense, dailyRecord); + DailyDefenseInfoResponse response = DailyDefenseInfoMapper.ofAttempted(dailyDefense, dailyRecord); // then assertThat(response) From ff0ea61b5a21dd4d3d9e8a772aef5b4a7ce415b9 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Wed, 3 Apr 2024 14:56:42 +0900 Subject: [PATCH 12/44] =?UTF-8?q?fix:=20@JsonInclude=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=ED=95=84=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/DailyDefenseProblemInfoResponse.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseProblemInfoResponse.java b/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseProblemInfoResponse.java index 8f9b635a..37927f6a 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseProblemInfoResponse.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/dto/response/DailyDefenseProblemInfoResponse.java @@ -8,7 +8,6 @@ @Getter @NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) public class DailyDefenseProblemInfoResponse { private Long problemNumber; @@ -17,6 +16,8 @@ public class DailyDefenseProblemInfoResponse { private ProblemTier difficulty; private Long solvedCount; private Long submitCount; + + @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean isSolved; @Builder @@ -30,3 +31,5 @@ private DailyDefenseProblemInfoResponse(Long problemNumber, Long problemId, Long this.isSolved = isSolved; } } + + From 4e6175c11f519afbad21cdb812629bbf9f9e2bf9 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Wed, 3 Apr 2024 15:02:40 +0900 Subject: [PATCH 13/44] =?UTF-8?q?fix:=20dailydefense=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=8B=9C=20problem=20fecth=20join?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/dailydefense/DailyDefenseRepository.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/dailydefense/DailyDefenseRepository.java b/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/dailydefense/DailyDefenseRepository.java index 9342bfae..9cce7ceb 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/dailydefense/DailyDefenseRepository.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/dailydefense/DailyDefenseRepository.java @@ -14,6 +14,7 @@ public interface DailyDefenseRepository extends JpaRepository Date: Sun, 7 Apr 2024 22:06:44 +0900 Subject: [PATCH 14/44] =?UTF-8?q?:sparkles:=20DailyDefense=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=20=EC=8B=9C=20=EB=AC=B8=EC=A0=9C=20content=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=20dto=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../problemcontent/ProblemContent.java | 44 +++++++++++++++++++ .../response/problemcontent/SampleData.java | 22 ++++++++++ .../response/problemcontent/Subtask.java | 23 ++++++++++ .../session/DefenseProblemResponse.java | 7 ++- 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/ProblemContent.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/SampleData.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/Subtask.java diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/ProblemContent.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/ProblemContent.java new file mode 100644 index 00000000..f4139bc5 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/ProblemContent.java @@ -0,0 +1,44 @@ +package kr.co.morandi.backend.defense_management.application.response.problemcontent; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; + +import java.util.List; + +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ProblemContent { + + private Long baekjoonProblemId; + private String title; + private String memoryLimit; + private String timeLimit; + private String description; + private String input; + private String output; + private List samples; + private String hint; + private List subtasks; + private String problemLimit; + private String additionalTimeLimit; + private String additionalJudgeInfo; + + + @Builder + private ProblemContent(Long baekjoonProblemId, String title, String memoryLimit, String timeLimit, String description, String input, String output, List samples, String hint, List subtasks, String problemLimit, String additionalTimeLimit, String additionalJudgeInfo) { + this.baekjoonProblemId = baekjoonProblemId; + this.title = title; + this.memoryLimit = memoryLimit; + this.timeLimit = timeLimit; + this.description = description; + this.input = input; + this.output = output; + this.samples = samples; + this.hint = hint; + this.subtasks = subtasks; + this.problemLimit = problemLimit; + this.additionalTimeLimit = additionalTimeLimit; + this.additionalJudgeInfo = additionalJudgeInfo; + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/SampleData.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/SampleData.java new file mode 100644 index 00000000..ec349b4f --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/SampleData.java @@ -0,0 +1,22 @@ +package kr.co.morandi.backend.defense_management.application.response.problemcontent; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SampleData { + + private String input; + private String output; + private String explanation; + + @Builder + private SampleData(String input, String output, String explanation) { + this.input = input; + this.output = output; + this.explanation = explanation; + } +} + diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/Subtask.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/Subtask.java new file mode 100644 index 00000000..7efdbddf --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/Subtask.java @@ -0,0 +1,23 @@ +package kr.co.morandi.backend.defense_management.application.response.problemcontent; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Subtask { + + private String title; + private List conditions; + private String tableConditionsHtml; + + @Builder + private Subtask(String title, List conditions, String tableConditionsHtml) { + this.title = title; + this.conditions = conditions; + this.tableConditionsHtml = tableConditionsHtml; + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java index c65297be..28b81b6a 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java @@ -1,7 +1,9 @@ package kr.co.morandi.backend.defense_management.application.response.session; +import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.defense_management.application.response.tempcode.TempCodeResponse; import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -16,16 +18,19 @@ public class DefenseProblemResponse { private Long problemId; private Long problemNumber; private Long baekjoonProblemId; + private ProblemContent problemContent; private boolean isCorrect; private Language lastAccessLanguage; private Set tempCodes; @Builder private DefenseProblemResponse(Long problemId, Long problemNumber, Long baekjoonProblemId, - boolean isCorrect, Language lastAccessLanguage, Set tempCodes) { + ProblemContent problemContent, boolean isCorrect, Language lastAccessLanguage, + Set tempCodes) { this.problemId = problemId; this.problemNumber = problemNumber; this.baekjoonProblemId = baekjoonProblemId; + this.problemContent = problemContent; this.isCorrect = isCorrect; this.lastAccessLanguage = lastAccessLanguage; this.tempCodes = tempCodes; From 3e3e868ffd6fa65c751e85c8a6064ad2e179bd5a Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 8 Apr 2024 19:12:31 +0900 Subject: [PATCH 15/44] =?UTF-8?q?:sparkles:=20=EC=8B=9C=ED=97=98=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=8B=9C=20=EB=AC=B8=EC=A0=9C=20=EB=B3=B8?= =?UTF-8?q?=EB=AC=B8=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B2=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 + .../common/config/WebClientConfig.java | 14 ++ .../defenseproblem/DefenseProblemMapper.java | 7 +- .../session/StartDailyDefenseMapper.java | 6 +- .../problemcontent/ProblemContentPort.java | 11 ++ .../problemcontent/ProblemContent.java | 6 +- .../session/DefenseProblemResponse.java | 6 +- .../DailyDefenseManagementService.java | 23 ++- .../problemcontent/ProblemContentAdapter.java | 65 +++++++++ .../DailyDefenseManagementServiceTest.java | 34 +++++ .../ProblemContentAdapterTest.java | 133 ++++++++++++++++++ 11 files changed, 303 insertions(+), 8 deletions(-) create mode 100644 src/main/java/kr/co/morandi/backend/common/config/WebClientConfig.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java create mode 100644 src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java diff --git a/build.gradle b/build.gradle index 775c6a3b..f2f32c86 100644 --- a/build.gradle +++ b/build.gradle @@ -81,6 +81,8 @@ dependencies { // implementation 'org.springframework.boot:spring-boot-starter-security' // testImplementation 'org.springframework.security:spring-security-test' + // webclient + implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-web' @@ -90,6 +92,10 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // MockWebServer + testImplementation 'com.squareup.okhttp3:mockwebserver' + testImplementation 'com.h2database:h2' } diff --git a/src/main/java/kr/co/morandi/backend/common/config/WebClientConfig.java b/src/main/java/kr/co/morandi/backend/common/config/WebClientConfig.java new file mode 100644 index 00000000..2760f0df --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/common/config/WebClientConfig.java @@ -0,0 +1,14 @@ +package kr.co.morandi.backend.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java index ebf4207e..0a3be671 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java @@ -1,6 +1,7 @@ package kr.co.morandi.backend.defense_management.application.mapper.defenseproblem; import kr.co.morandi.backend.defense_management.application.mapper.tempcode.TempCodeMapper; +import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.defense_management.application.response.session.DefenseProblemResponse; import kr.co.morandi.backend.defense_management.application.response.tempcode.TempCodeResponse; import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; @@ -18,7 +19,10 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class DefenseProblemMapper { - public static List of(Map tryProblem, DefenseSession defenseSession, DailyRecord dailyRecord) { + public static List of(Map tryProblem, + DefenseSession defenseSession, + DailyRecord dailyRecord, + Map problemContents) { return tryProblem.entrySet().stream() .map(entry -> { final Long problemNumber = entry.getKey(); @@ -36,6 +40,7 @@ public static List of(Map tryProblem, Def .problemNumber(problemNumber) .isCorrect(isCorrect) .lastAccessLanguage(lastAccessLanguage) + .content(problemContents.get(problemNumber)) .tempCodes(tempCodeResponses) .build(); }) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java index 64b38983..b54dbc0f 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java @@ -2,6 +2,7 @@ import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_management.application.mapper.defenseproblem.DefenseProblemMapper; +import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; @@ -17,13 +18,14 @@ public class StartDailyDefenseMapper { public static StartDailyDefenseResponse of(Map tryProblem, DailyDefense dailyDefense, DefenseSession defenseSession, - DailyRecord dailyRecord) { + DailyRecord dailyRecord, + Map problemContents) { return StartDailyDefenseResponse.builder() .defenseSessionId(defenseSession.getDefenseSessionId()) .contentName(dailyDefense.getContentName()) .defenseType(dailyDefense.getDefenseType()) .lastAccessTime(defenseSession.getLastAccessDateTime()) - .defenseProblems(DefenseProblemMapper.of(tryProblem, defenseSession, dailyRecord)) + .defenseProblems(DefenseProblemMapper.of(tryProblem, defenseSession, dailyRecord, problemContents)) .build(); } } diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java b/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java new file mode 100644 index 00000000..dadb28ac --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java @@ -0,0 +1,11 @@ +package kr.co.morandi.backend.defense_management.application.port.out.problemcontent; + +import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; + +import java.util.List; +import java.util.Map; + +public interface ProblemContentPort { + + Map getProblemContents(List baekjoonProblemIds); +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/ProblemContent.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/ProblemContent.java index f4139bc5..307755d3 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/ProblemContent.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/ProblemContent.java @@ -24,9 +24,12 @@ public class ProblemContent { private String additionalTimeLimit; private String additionalJudgeInfo; + // 오류 날 경우 error 필드만 반환됨 + private String error; + @Builder - private ProblemContent(Long baekjoonProblemId, String title, String memoryLimit, String timeLimit, String description, String input, String output, List samples, String hint, List subtasks, String problemLimit, String additionalTimeLimit, String additionalJudgeInfo) { + private ProblemContent(Long baekjoonProblemId, String title, String memoryLimit, String timeLimit, String description, String input, String output, List samples, String hint, List subtasks, String problemLimit, String additionalTimeLimit, String additionalJudgeInfo, String error) { this.baekjoonProblemId = baekjoonProblemId; this.title = title; this.memoryLimit = memoryLimit; @@ -40,5 +43,6 @@ private ProblemContent(Long baekjoonProblemId, String title, String memoryLimit, this.problemLimit = problemLimit; this.additionalTimeLimit = additionalTimeLimit; this.additionalJudgeInfo = additionalJudgeInfo; + this.error = error; } } diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java index 28b81b6a..e5b6d565 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java @@ -18,19 +18,19 @@ public class DefenseProblemResponse { private Long problemId; private Long problemNumber; private Long baekjoonProblemId; - private ProblemContent problemContent; + private ProblemContent content; private boolean isCorrect; private Language lastAccessLanguage; private Set tempCodes; @Builder private DefenseProblemResponse(Long problemId, Long problemNumber, Long baekjoonProblemId, - ProblemContent problemContent, boolean isCorrect, Language lastAccessLanguage, + ProblemContent content, boolean isCorrect, Language lastAccessLanguage, Set tempCodes) { this.problemId = problemId; this.problemNumber = problemNumber; this.baekjoonProblemId = baekjoonProblemId; - this.problemContent = problemContent; + this.content = content; this.isCorrect = isCorrect; this.lastAccessLanguage = lastAccessLanguage; this.tempCodes = tempCodes; diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java index 2119115f..9881f440 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java @@ -3,6 +3,8 @@ import kr.co.morandi.backend.defense_information.application.port.out.dailydefense.DailyDefensePort; import kr.co.morandi.backend.defense_management.application.mapper.session.StartDailyDefenseMapper; import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; +import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; +import kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent.ProblemContentAdapter; import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.service.defense.ProblemGenerationService; @@ -32,6 +34,8 @@ public class DailyDefenseManagementService { private final ProblemGenerationService problemGenerationService; private final DefenseSessionPort defenseSessionPort; + private final ProblemContentAdapter problemContentAdapter; + @Transactional public StartDailyDefenseResponse startDailyDefense(StartDailyDefenseServiceRequest request, Member member, LocalDateTime requestTime) { Long problemNumber = request.getProblemNumber(); @@ -60,9 +64,26 @@ public StartDailyDefenseResponse startDailyDefense(StartDailyDefenseServiceReque final DefenseSession savedDefenseSession = defenseSessionPort.saveDefenseSession(defenseSession); + // 문제 내용 가져오기 + final Map problemContent = getProblemContents(tryProblem); + // 문제 목록을 DefenseProblemResponse DTO로 변환 - return StartDailyDefenseMapper.of(tryProblem, dailyDefense, savedDefenseSession, dailyRecord); + return StartDailyDefenseMapper.of(tryProblem, dailyDefense, savedDefenseSession, dailyRecord, problemContent); } + + /* + * 백준 문제 ID 목록을 받아서 문제 내용을 가져오는 메소드 + * */ + private Map getProblemContents(Map tryProblem) { + return problemContentAdapter.getProblemContents(tryProblem.values() + .stream() + .map(Problem::getBaekjoonProblemId) + .toList()); + } + + /* + * 세션이 존재하지 않을 경우 새롭게 시험을 시작하는 메소드 + * */ private DefenseSession createNewSession(Member member, LocalDateTime now, DailyDefense dailyDefense, Map tryProblem) { DailyRecord dailyRecord = DailyRecord.tryDefense(now, dailyDefense, member, tryProblem); DailyRecord savedDailyRecord = dailyRecordPort.saveDailyRecord(dailyRecord); diff --git a/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java b/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java new file mode 100644 index 00000000..b6543db6 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java @@ -0,0 +1,65 @@ +package kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import kr.co.morandi.backend.defense_management.application.port.out.problemcontent.ProblemContentPort; +import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class ProblemContentAdapter implements ProblemContentPort { + + private final WebClient webClient; + private final ObjectMapper objectMapper; + + private static final String PROBLEM_CONTENTS_API_URL = "https://n1bcmtru2j.execute-api.ap-northeast-2.amazonaws.com/default/getBaekjoonProblemContents?baekjoonProblemIds=%s"; + + @Override + public Map getProblemContents(List baekjoonProblemIds) { + + if(baekjoonProblemIds.isEmpty()) { + return Map.of(); + } + + if(baekjoonProblemIds.size() > 10) { + throw new IllegalArgumentException("문제 번호는 10개 이하로 요청해주세요."); + } + + String baekjoonProblemIdsParam = baekjoonProblemIds.stream() + .map(String::valueOf) + .collect(Collectors.joining(",")); + + String responseBody = webClient.get() + .uri(String.format(PROBLEM_CONTENTS_API_URL, baekjoonProblemIdsParam)) + .retrieve() + .bodyToMono(String.class) + .block(); + + return parseResponse(responseBody); + } + + + private Map parseResponse(String responseBody) { + try { + objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + List problemContents = objectMapper.readValue(responseBody, new TypeReference<>() { + }); + + return problemContents.stream() + .filter(content -> content.getError() == null && content.getBaekjoonProblemId() != null) + .collect(Collectors.toMap(ProblemContent::getBaekjoonProblemId, content -> content)); + + } catch (Exception e) { + throw new RuntimeException("Error parsing problem contents", e); + } + } + +} diff --git a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java index 11ec2239..077a141c 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java @@ -2,8 +2,10 @@ import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_management.application.request.session.StartDailyDefenseServiceRequest; +import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; import kr.co.morandi.backend.defense_management.application.service.session.DailyDefenseManagementService; +import kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent.ProblemContentAdapter; import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; @@ -15,10 +17,17 @@ import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; import kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record.DailyRecordRepository; import org.junit.jupiter.api.AfterEach; +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.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ActiveProfiles; import java.time.LocalDate; @@ -34,9 +43,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.anyList; @SpringBootTest +@ExtendWith(MockitoExtension.class) @ActiveProfiles("test") class DailyDefenseManagementServiceTest { @@ -67,6 +78,29 @@ class DailyDefenseManagementServiceTest { @Autowired private SessionDetailRepository sessionDetailRepository; + @MockBean + private ProblemContentAdapter problemContentAdapter; + + @BeforeEach + void setUp() { + Map problemContentMap = Map.of( + 1L, ProblemContent.builder() + .baekjoonProblemId(1000L) + .title("test") + .build(), + 2L, ProblemContent.builder() + .baekjoonProblemId(2000L) + .title("test2") + .build(), + 3L, ProblemContent.builder() + .baekjoonProblemId(3000L) + .title("test3") + .build() + ); + Mockito.when(problemContentAdapter.getProblemContents(anyList())) + .thenReturn(problemContentMap); + } + @AfterEach void tearDown() { sessionDetailRepository.deleteAll(); diff --git a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java new file mode 100644 index 00000000..3b753d47 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java @@ -0,0 +1,133 @@ +package kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent; + +import com.fasterxml.jackson.databind.ObjectMapper; +import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.reactive.function.client.WebClient; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.groups.Tuple.tuple; + +@ActiveProfiles("test") +class ProblemContentAdapterTest { + + private ProblemContentAdapter problemContentAdapter; + + private MockWebServer mockWebServer; + + @BeforeEach + void setUp() throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + mockWebServer = new MockWebServer(); + mockWebServer.start(); + String mockServerUrl = mockWebServer.url("/") + .toString(); + + WebClient webClient = WebClient.builder() + .baseUrl(mockServerUrl) + .build(); + + problemContentAdapter = new ProblemContentAdapter(webClient, objectMapper); + } + + @AfterEach + void tearDown() throws IOException { + mockWebServer.shutdown(); + } + + @DisplayName("문제 번호 리스트를 받아서 해당 문제 번호의 문제 정보를 반환한다.") + @Test + void getProblemContents() { + // given + List list = List.of(1000L, 1001L); + + mockWebServer.enqueue(new MockResponse() + .setResponseCode(200) + .setBody(""" + [ + { + "baekjoonProblemId": 1000, + "title": "A+B", + }, + { + "baekjoonProblemId": 1001, + "title": "A-B", + } + ]""") + .addHeader("Content-Type", "application/json")); + + + // when + final Map result = problemContentAdapter.getProblemContents(list); + + // then + assertThat(result.values()).hasSize(2) + .extracting("baekjoonProblemId", "title") + .containsExactlyInAnyOrder( + tuple(1000L, "A+B"), + tuple(1001L, "A-B") + ); + + + } + + @DisplayName("존재하지 않는 문제 번호를 포함하여 요청하면 해당 문제 번호를 제외하고 반환한다.") + @Test + void getProblemContentsContainsInvalidBaekjoonProblemId() { + // given + List list = List.of(1000L, 1001L, 999L); + mockWebServer.enqueue(new MockResponse() + .setResponseCode(200) + .setBody(""" + [ + { + "baekjoonProblemId": 1000, + "title": "A+B", + }, + { + "baekjoonProblemId": 1001, + "title": "A-B", + }, + { + "error" : "problem/999.json not exist" + } + ]""") + .addHeader("Content-Type", "application/json")); + + // when + final Map result = problemContentAdapter.getProblemContents(list); + + // then + assertThat(result.values()).hasSize(2) + .extracting("baekjoonProblemId", "title") + .containsExactlyInAnyOrder( + tuple(1000L, "A+B"), + tuple(1001L, "A-B") + ); + + } + + @DisplayName("10개 이상의 문제 번호를 요청하면 예외가 발생한다.") + @Test + void getProblemContentsContainsMoreThan10BaekjoonProblemId() { + // given + List list = List.of(1000L, 1001L, 1002L, 1003L, 1004L, 1005L, 1006L, 1007L, 1008L, 1009L, 1010L); + + // when & then + assertThatThrownBy(() -> problemContentAdapter.getProblemContents(list)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("문제 번호는 10개 이하로 요청해주세요."); + + } +} \ No newline at end of file From cf4c7a0f6611a4050cf9c88e85b24885654b0fd3 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 8 Apr 2024 23:03:35 +0900 Subject: [PATCH 16/44] =?UTF-8?q?fix:=20=EC=9D=91=EC=9A=A9=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EC=97=90=EC=84=9C=20Port=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/session/DailyDefenseManagementService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java index 9881f440..5b9a5569 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java @@ -2,9 +2,9 @@ import kr.co.morandi.backend.defense_information.application.port.out.dailydefense.DailyDefensePort; import kr.co.morandi.backend.defense_management.application.mapper.session.StartDailyDefenseMapper; +import kr.co.morandi.backend.defense_management.application.port.out.problemcontent.ProblemContentPort; import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; -import kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent.ProblemContentAdapter; import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.service.defense.ProblemGenerationService; @@ -34,7 +34,7 @@ public class DailyDefenseManagementService { private final ProblemGenerationService problemGenerationService; private final DefenseSessionPort defenseSessionPort; - private final ProblemContentAdapter problemContentAdapter; + private final ProblemContentPort problemContentPort; @Transactional public StartDailyDefenseResponse startDailyDefense(StartDailyDefenseServiceRequest request, Member member, LocalDateTime requestTime) { @@ -75,7 +75,7 @@ public StartDailyDefenseResponse startDailyDefense(StartDailyDefenseServiceReque * 백준 문제 ID 목록을 받아서 문제 내용을 가져오는 메소드 * */ private Map getProblemContents(Map tryProblem) { - return problemContentAdapter.getProblemContents(tryProblem.values() + return problemContentPort.getProblemContents(tryProblem.values() .stream() .map(Problem::getBaekjoonProblemId) .toList()); From 45bb1f158993290ca3a823333ef02cbdf4bc8610 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 8 Apr 2024 23:16:47 +0900 Subject: [PATCH 17/44] =?UTF-8?q?:bug:=20WebClient=20=EC=A0=95=EC=83=81?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20mocking=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 - .../ProblemContentAdapterTest.java | 87 ++++++++++--------- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/build.gradle b/build.gradle index f2f32c86..3309132d 100644 --- a/build.gradle +++ b/build.gradle @@ -93,9 +93,6 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' - // MockWebServer - testImplementation 'com.squareup.okhttp3:mockwebserver' - testImplementation 'com.h2database:h2' } diff --git a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java index 3b753d47..a948007c 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java @@ -8,8 +8,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.http.HttpStatus; import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; import java.io.IOException; import java.util.List; @@ -24,27 +29,23 @@ class ProblemContentAdapterTest { private ProblemContentAdapter problemContentAdapter; - private MockWebServer mockWebServer; + + private ExchangeFunction exchangeFunction; @BeforeEach void setUp() throws IOException { ObjectMapper objectMapper = new ObjectMapper(); - mockWebServer = new MockWebServer(); - mockWebServer.start(); - String mockServerUrl = mockWebServer.url("/") - .toString(); + + exchangeFunction = Mockito.mock(ExchangeFunction.class); WebClient webClient = WebClient.builder() - .baseUrl(mockServerUrl) + .exchangeFunction(exchangeFunction) .build(); problemContentAdapter = new ProblemContentAdapter(webClient, objectMapper); } - @AfterEach - void tearDown() throws IOException { - mockWebServer.shutdown(); - } + @DisplayName("문제 번호 리스트를 받아서 해당 문제 번호의 문제 정보를 반환한다.") @Test @@ -52,20 +53,21 @@ void getProblemContents() { // given List list = List.of(1000L, 1001L); - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .setBody(""" - [ - { - "baekjoonProblemId": 1000, - "title": "A+B", - }, - { - "baekjoonProblemId": 1001, - "title": "A-B", - } - ]""") - .addHeader("Content-Type", "application/json")); + Mockito.when(exchangeFunction.exchange(Mockito.any())) + .thenReturn(Mono.just(ClientResponse.create(HttpStatus.OK) + .header("Content-Type", "application/json") + .body(""" + [ + { + "baekjoonProblemId": 1000, + "title": "A+B" + }, + { + "baekjoonProblemId": 1001, + "title": "A-B" + } + ]""") + .build())); // when @@ -87,23 +89,26 @@ void getProblemContents() { void getProblemContentsContainsInvalidBaekjoonProblemId() { // given List list = List.of(1000L, 1001L, 999L); - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .setBody(""" - [ - { - "baekjoonProblemId": 1000, - "title": "A+B", - }, - { - "baekjoonProblemId": 1001, - "title": "A-B", - }, - { - "error" : "problem/999.json not exist" - } - ]""") - .addHeader("Content-Type", "application/json")); + + Mockito.when(exchangeFunction.exchange(Mockito.any())) + .thenReturn(Mono.just(ClientResponse.create(HttpStatus.OK) + .header("Content-Type", "application/json") + .body(""" + [ + { + "baekjoonProblemId": 1000, + "title": "A+B" + }, + { + "baekjoonProblemId": 1001, + "title": "A-B" + }, + { + "error" : "problem/999.json not exist" + } + ]""") + .build())); + // when final Map result = problemContentAdapter.getProblemContents(list); From c21af050903d86649ab4a743cb20a42d557044dd Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 8 Apr 2024 23:17:42 +0900 Subject: [PATCH 18/44] :fire: Remove unused import --- .../adapter/problemcontent/ProblemContentAdapterTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java index a948007c..a64958c9 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java @@ -2,9 +2,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; From c36ee7d78c7bba58c9fe5c8feba69a9450f79cea Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 9 Apr 2024 18:14:59 +0900 Subject: [PATCH 19/44] =?UTF-8?q?:fire:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20throws=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/problemcontent/ProblemContentAdapterTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java index a64958c9..9ad3fe5b 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java @@ -13,7 +13,6 @@ import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; -import java.io.IOException; import java.util.List; import java.util.Map; @@ -26,11 +25,10 @@ class ProblemContentAdapterTest { private ProblemContentAdapter problemContentAdapter; - private ExchangeFunction exchangeFunction; @BeforeEach - void setUp() throws IOException { + void setUp() { ObjectMapper objectMapper = new ObjectMapper(); exchangeFunction = Mockito.mock(ExchangeFunction.class); From cccc8e54619ac7fb861c48a5361903c5bd42d9bd Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 9 Apr 2024 18:34:38 +0900 Subject: [PATCH 20/44] =?UTF-8?q?:sparkles:=20Webclient=20retryWhen?= =?UTF-8?q?=EC=9D=84=20=ED=86=B5=ED=95=B4=20=EC=9E=AC=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/problemcontent/ProblemContentAdapter.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java b/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java index b6543db6..5f9643d1 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java @@ -8,7 +8,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; +import reactor.util.retry.Retry; +import java.time.Duration; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -41,6 +43,9 @@ public Map getProblemContents(List baekjoonProblemId .uri(String.format(PROBLEM_CONTENTS_API_URL, baekjoonProblemIdsParam)) .retrieve() .bodyToMono(String.class) + .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)) + .maxBackoff(Duration.ofSeconds(5)) + .jitter(0.5)) .block(); return parseResponse(responseBody); From a7ba850b2b7d83d302829b131b557166666f6612 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 9 Apr 2024 20:28:06 +0900 Subject: [PATCH 21/44] =?UTF-8?q?:recycle:=20Problem=20Content=20=EC=B1=85?= =?UTF-8?q?=EC=9E=84=EC=97=90=20=EB=94=B0=EB=9D=BC=20problem=5Finformation?= =?UTF-8?q?=ED=95=98=EC=9C=84=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/defenseproblem/DefenseProblemMapper.java | 2 +- .../mapper/session/StartDailyDefenseMapper.java | 2 +- .../port/out/problemcontent/ProblemContentPort.java | 11 ----------- .../response/session/DefenseProblemResponse.java | 3 +-- .../session/DailyDefenseManagementService.java | 4 ++-- .../port/out/problemcontent/ProblemContentPort.java | 11 +++++++++++ .../response/problemcontent/ProblemContent.java | 2 +- .../response/problemcontent/SampleData.java | 2 +- .../application/response/problemcontent/Subtask.java | 2 +- .../adapter/problemcontent/ProblemContentAdapter.java | 6 +++--- .../DailyDefenseManagementServiceTest.java | 6 ++---- .../problemcontent/ProblemContentAdapterTest.java | 5 +++-- 12 files changed, 27 insertions(+), 29 deletions(-) delete mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java create mode 100644 src/main/java/kr/co/morandi/backend/problem_information/application/port/out/problemcontent/ProblemContentPort.java rename src/main/java/kr/co/morandi/backend/{defense_management => problem_information}/application/response/problemcontent/ProblemContent.java (94%) rename src/main/java/kr/co/morandi/backend/{defense_management => problem_information}/application/response/problemcontent/SampleData.java (84%) rename src/main/java/kr/co/morandi/backend/{defense_management => problem_information}/application/response/problemcontent/Subtask.java (86%) rename src/main/java/kr/co/morandi/backend/{defense_management => problem_information}/infrastructure/adapter/problemcontent/ProblemContentAdapter.java (89%) rename src/test/java/kr/co/morandi/backend/{defense_management => problem_information}/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java (94%) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java index 0a3be671..3664767a 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java @@ -1,7 +1,7 @@ package kr.co.morandi.backend.defense_management.application.mapper.defenseproblem; import kr.co.morandi.backend.defense_management.application.mapper.tempcode.TempCodeMapper; -import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.defense_management.application.response.session.DefenseProblemResponse; import kr.co.morandi.backend.defense_management.application.response.tempcode.TempCodeResponse; import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java index b54dbc0f..59385a72 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/session/StartDailyDefenseMapper.java @@ -2,7 +2,7 @@ import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_management.application.mapper.defenseproblem.DefenseProblemMapper; -import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java b/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java deleted file mode 100644 index dadb28ac..00000000 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java +++ /dev/null @@ -1,11 +0,0 @@ -package kr.co.morandi.backend.defense_management.application.port.out.problemcontent; - -import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; - -import java.util.List; -import java.util.Map; - -public interface ProblemContentPort { - - Map getProblemContents(List baekjoonProblemIds); -} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java index e5b6d565..34560df8 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java @@ -1,9 +1,8 @@ package kr.co.morandi.backend.defense_management.application.response.session; -import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.defense_management.application.response.tempcode.TempCodeResponse; import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language; -import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java index 5b9a5569..5bb17f79 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java @@ -2,9 +2,9 @@ import kr.co.morandi.backend.defense_information.application.port.out.dailydefense.DailyDefensePort; import kr.co.morandi.backend.defense_management.application.mapper.session.StartDailyDefenseMapper; -import kr.co.morandi.backend.defense_management.application.port.out.problemcontent.ProblemContentPort; +import kr.co.morandi.backend.problem_information.application.port.out.problemcontent.ProblemContentPort; import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; -import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.service.defense.ProblemGenerationService; diff --git a/src/main/java/kr/co/morandi/backend/problem_information/application/port/out/problemcontent/ProblemContentPort.java b/src/main/java/kr/co/morandi/backend/problem_information/application/port/out/problemcontent/ProblemContentPort.java new file mode 100644 index 00000000..498a1896 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/problem_information/application/port/out/problemcontent/ProblemContentPort.java @@ -0,0 +1,11 @@ +package kr.co.morandi.backend.problem_information.application.port.out.problemcontent; + +import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; + +import java.util.List; +import java.util.Map; + +public interface ProblemContentPort { + + Map getProblemContents(List baekjoonProblemIds); +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/ProblemContent.java b/src/main/java/kr/co/morandi/backend/problem_information/application/response/problemcontent/ProblemContent.java similarity index 94% rename from src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/ProblemContent.java rename to src/main/java/kr/co/morandi/backend/problem_information/application/response/problemcontent/ProblemContent.java index 307755d3..1dd2ea5d 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/ProblemContent.java +++ b/src/main/java/kr/co/morandi/backend/problem_information/application/response/problemcontent/ProblemContent.java @@ -1,4 +1,4 @@ -package kr.co.morandi.backend.defense_management.application.response.problemcontent; +package kr.co.morandi.backend.problem_information.application.response.problemcontent; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.*; diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/SampleData.java b/src/main/java/kr/co/morandi/backend/problem_information/application/response/problemcontent/SampleData.java similarity index 84% rename from src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/SampleData.java rename to src/main/java/kr/co/morandi/backend/problem_information/application/response/problemcontent/SampleData.java index ec349b4f..b532fecb 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/SampleData.java +++ b/src/main/java/kr/co/morandi/backend/problem_information/application/response/problemcontent/SampleData.java @@ -1,4 +1,4 @@ -package kr.co.morandi.backend.defense_management.application.response.problemcontent; +package kr.co.morandi.backend.problem_information.application.response.problemcontent; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.*; diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/Subtask.java b/src/main/java/kr/co/morandi/backend/problem_information/application/response/problemcontent/Subtask.java similarity index 86% rename from src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/Subtask.java rename to src/main/java/kr/co/morandi/backend/problem_information/application/response/problemcontent/Subtask.java index 7efdbddf..899b9183 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/problemcontent/Subtask.java +++ b/src/main/java/kr/co/morandi/backend/problem_information/application/response/problemcontent/Subtask.java @@ -1,4 +1,4 @@ -package kr.co.morandi.backend.defense_management.application.response.problemcontent; +package kr.co.morandi.backend.problem_information.application.response.problemcontent; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.*; diff --git a/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java b/src/main/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problemcontent/ProblemContentAdapter.java similarity index 89% rename from src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java rename to src/main/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problemcontent/ProblemContentAdapter.java index 5f9643d1..8d2ebea0 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java +++ b/src/main/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problemcontent/ProblemContentAdapter.java @@ -1,10 +1,10 @@ -package kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent; +package kr.co.morandi.backend.problem_information.infrastructure.adapter.problemcontent; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import kr.co.morandi.backend.defense_management.application.port.out.problemcontent.ProblemContentPort; -import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; +import kr.co.morandi.backend.problem_information.application.port.out.problemcontent.ProblemContentPort; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; diff --git a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java index 077a141c..c69a0b63 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java @@ -2,10 +2,10 @@ import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_management.application.request.session.StartDailyDefenseServiceRequest; -import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; import kr.co.morandi.backend.defense_management.application.service.session.DailyDefenseManagementService; -import kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent.ProblemContentAdapter; +import kr.co.morandi.backend.problem_information.infrastructure.adapter.problemcontent.ProblemContentAdapter; import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; @@ -21,10 +21,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; diff --git a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java b/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java similarity index 94% rename from src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java rename to src/test/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java index 9ad3fe5b..24506319 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java +++ b/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java @@ -1,7 +1,8 @@ -package kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent; +package kr.co.morandi.backend.problem_information.infrastructure.adapter.problemcontent; import com.fasterxml.jackson.databind.ObjectMapper; -import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; +import kr.co.morandi.backend.problem_information.infrastructure.adapter.problemcontent.ProblemContentAdapter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; From f31c4512f29859a2c201e8fbbf7b2ff054f9d4c4 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 9 Apr 2024 20:31:57 +0900 Subject: [PATCH 22/44] =?UTF-8?q?:zap:=20TempCode=20hashmap=20enummap?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/mapper/tempcode/TempCodeMapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java index 86782a46..e9588ddf 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/tempcode/TempCodeMapper.java @@ -28,7 +28,7 @@ public class TempCodeMapper { public static Set createTempCodeResponses(Set tempCodes) { // 기본 코드를 가지고 있는 Map을 만들어서 - Map tempCodeMap = new HashMap<>(intialTempCodeMap); + Map tempCodeMap = new EnumMap<>(intialTempCodeMap); // tempCode를 순회하면서 tempCodeMap에 해당 언어의 TempCodeResponse를 넣어준다. tempCodes.forEach(tempCode -> tempCodeMap.replace(tempCode.getLanguage(), TempCodeResponse.builder() From 42886fedc9b3d51be2b154844dcbc4aaeb621cc1 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 9 Apr 2024 20:33:33 +0900 Subject: [PATCH 23/44] :fire: Remove unused import --- .../backend/defense_record/domain/model/record/Detail.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Detail.java b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Detail.java index 1c835430..9beaf3fc 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Detail.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Detail.java @@ -10,8 +10,6 @@ import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; -import java.time.LocalDateTime; - @Entity @Inheritance(strategy = InheritanceType.JOINED) @DiscriminatorColumn From ddb9199fa4308ab0dbaf0cd75f249b8da997be77 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 9 Apr 2024 20:34:19 +0900 Subject: [PATCH 24/44] =?UTF-8?q?:art:=20=EA=B8=B0=EB=B3=B8=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=9E=90=20private=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../defense_record/application/util/TimeFormatHelper.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelper.java b/src/main/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelper.java index 056f622d..c59a1853 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelper.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelper.java @@ -1,5 +1,9 @@ package kr.co.morandi.backend.defense_record.application.util; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class TimeFormatHelper { public static String solvedTimeToString(Long solvedTime) { From 32cd24a63d93d6e621cba9fcc108d3f6d4469d8c Mon Sep 17 00:00:00 2001 From: Jeong Yong Choi <51875177+aj4941@users.noreply.github.com> Date: Sun, 21 Apr 2024 16:52:58 +0900 Subject: [PATCH 25/44] =?UTF-8?q?Google=20OAuth=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Google OAuth 적용 및 커스텀 예외 추가 * :pencil2: 패키지 구조 일부 변경 * :pencil2: 출력 관련 코드 제거 * :pencil2: MemberAdapter 코드 수정 * :pencil2: OAuth Login과 관련한 Cookie 발급과 관련한 작업을 Service 단에서 처리하도록 수정 * :pencil2: OAuth AccessToken 발급과 관련한 메서드 분리 * :pencil2: GoogleUserDto에 @Setter, @AllArgsConstructor 제거 * :pencil2: SetDomain 관련 url, path를 yml에 저장하여 관리 * :pencil2: ErrorCode 관련 코드 수정 및 RestControllerAdvice 코드 수정 * :pencil2: OAuth domain 관련 패키지 구조 변경 * :pencil2: LoginUseCase를 목적에 맞게 AuthenticationUseCase로 변경 (인증과 관련한 작업) * :pencil2: JwtToken 및 publicKey와 PrivateKey를 발급하는 코드 수정 * :pencil2: LoginMember에서 Repository를 호출하는 형태 변경 및 패키지 구조 일부 변경 * :pencil2: Filter를 통과하는 url 리스트를 정규 표현식으로 검사하도록 수정 * :pencil2: Spring Security Filter와 관련한 코드 수정 * :pencil2: CORS 설정을 시큐리티 기본값에서 직접 정의한 내용으로 수정 * :sparkles: RefreshToken 관리를 위한 Redis 환경 구성 * :sparkles: RefreshToken을 검증하여 accessToken을 재발급하도록 코드 추가 * :pencil2: Redis에 저장하는 refreshToken에 대한 key 값을 명확하게 하기 위해 코드 수정 * :pencil2: RefreshToken을 Redis에 저장하는 로직 수정 * :pencil2: OAuth 정보와 관련된 DTO를 OAuthUserInfo, GoogleOAuthUserInfo 형태로 변경 * :pencil2: RefreshToken을 구하는 로직 수정 * :pencil2: JwtAuthenticationFilter 구조 변경 (jwtProvider, authenticationProvider, isIgnoredURIManager로 분리) * :pencil2: JwtAuthenticationFilter에서 else if를 if로 수정 * :pencil2: AccessToken, RefreshToken을 검사하는 필터에서 주석 추가 * :pencil2: Cookie 발급 로직을 CookieUtils에서 처리하도록 수정 * :pencil2: Security, OAuth 부분 패키지 구조 변경 * :pencil2: ErrorCode 및 일부 패키지 구조 수정 * :pencil2: 사용하지 않는 ErrorCode 삭제 * :pencil2: RestControllerAdvice에서 쿠키를 받는 로직을 cookieUtils 를 사용하도록 수정 * :pencil2: OAuth 관련 패키지 구조 일부 수정 * :pencil2: Jwt 검증과 관련한 메서드를 JwtValidator로 분리 * :pencil2: Securty, Jwt, OAuth 관련 패키지 분리 * :pencil2: Domain security 관련 패키지를 Application로 이동 * :pencil2: 패키지 구조 일부 변경 * :art: RefreshToken 오류 수정 및 임시 coverage 하향 * :art: Google oauth 예외처리 * :bug: Google oauth 예외처리 --------- Co-authored-by: miiiinju1 --- build.gradle | 12 +- .../backend/common/config/CorsConfig.java | 28 +++++ .../common/exception/MorandiException.java | 16 +++ .../common/exception/errorcode/ErrorCode.java | 9 ++ .../handler/GlobalExceptionHandler.java | 82 ++++++++++++++ .../handler/exception/CommonErrorCode.java | 17 +++ .../exception/response/ErrorResponse.java | 25 ++++ .../model/oauth/OAuthServiceFactory.java | 21 ++++ .../port/in/oauth/AuthenticationUseCase.java | 7 ++ .../port/out/member/MemberPort.java | 12 ++ .../service/jwt/MemberLoginService.java | 28 +++++ .../service/oauth/LoginService.java | 26 +++++ .../service/oauth/OAuthService.java | 8 ++ .../service/oauth/google/GoogleService.java | 107 ++++++++++++++++++ .../security/OAuthUserDetailsService.java | 22 ++++ .../domain/model/member/Member.java | 14 ++- .../domain/model/member/Role.java | 5 + .../domain/model/member/SocialType.java | 1 - .../domain/model/security/OAuthDetails.java | 44 +++++++ .../adapter/member/MemberAdapter.java | 33 ++++++ .../config/cookie/utils/CookieUtils.java | 37 ++++++ .../config/jwt/constants/TokenType.java | 11 ++ .../jwt/response/AuthenticationToken.java | 22 ++++ .../config/jwt/utils/JwtProvider.java | 95 ++++++++++++++++ .../config/jwt/utils/JwtValidator.java | 32 ++++++ .../config/jwt/utils/SecretKeyProvider.java | 68 +++++++++++ .../config/oauth/constants/OAuthUserInfo.java | 9 ++ .../constants/google/GoogleOAuthUserInfo.java | 40 +++++++ .../config/oauth/response/TokenResponse.java | 21 ++++ .../config/security/SecurityConfig.java | 45 ++++++++ .../utils/AuthenticationProvider.java | 40 +++++++ .../security/utils/IgnoredURIManager.java | 22 ++++ .../config/security/utils/SecurityUtils.java | 11 ++ .../controller/oauth/OAuthController.java | 43 +++++++ .../controller/oauth/OAuthURLController.java | 20 ++++ .../exception/OAuthErrorCode.java | 25 ++++ .../oauth/CachedBodyHttpServletWrapper.java | 58 ++++++++++ .../filter/oauth/JwtAuthenticationFilter.java | 74 ++++++++++++ .../filter/oauth/JwtExceptionFilter.java | 83 ++++++++++++++ .../filter/oauth/RequestCachingFilter.java | 20 ++++ .../persistence/member/MemberRepository.java | 4 +- .../model/session/DefenseSessionTest.java | 6 +- .../session/DefenseSessionAdapterTest.java | 4 +- .../domain/model/member/MemberTest.java | 2 - .../member/MemberRepositoryTest.java | 10 +- 45 files changed, 1297 insertions(+), 22 deletions(-) create mode 100644 src/main/java/kr/co/morandi/backend/common/config/CorsConfig.java create mode 100644 src/main/java/kr/co/morandi/backend/common/exception/MorandiException.java create mode 100644 src/main/java/kr/co/morandi/backend/common/exception/errorcode/ErrorCode.java create mode 100644 src/main/java/kr/co/morandi/backend/common/exception/handler/GlobalExceptionHandler.java create mode 100644 src/main/java/kr/co/morandi/backend/common/exception/handler/exception/CommonErrorCode.java create mode 100644 src/main/java/kr/co/morandi/backend/common/exception/response/ErrorResponse.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/application/model/oauth/OAuthServiceFactory.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/application/port/in/oauth/AuthenticationUseCase.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/application/port/out/member/MemberPort.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/application/service/jwt/MemberLoginService.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/application/service/oauth/LoginService.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/application/service/oauth/OAuthService.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/application/service/oauth/google/GoogleService.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/application/service/security/OAuthUserDetailsService.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/domain/model/member/Role.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/domain/model/security/OAuthDetails.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/adapter/member/MemberAdapter.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/cookie/utils/CookieUtils.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/constants/TokenType.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/response/AuthenticationToken.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtProvider.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtValidator.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/SecretKeyProvider.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/oauth/constants/OAuthUserInfo.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/oauth/constants/google/GoogleOAuthUserInfo.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/oauth/response/TokenResponse.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/AuthenticationProvider.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/IgnoredURIManager.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/SecurityUtils.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/controller/oauth/OAuthController.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/controller/oauth/OAuthURLController.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/exception/OAuthErrorCode.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/CachedBodyHttpServletWrapper.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtAuthenticationFilter.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtExceptionFilter.java create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/RequestCachingFilter.java diff --git a/build.gradle b/build.gradle index 3309132d..73d3f0d5 100644 --- a/build.gradle +++ b/build.gradle @@ -86,6 +86,14 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' + + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2' + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' compileOnly 'org.projectlombok:lombok' @@ -141,13 +149,13 @@ jacocoTestCoverageVerification { limit { counter = 'LINE' value = 'COVEREDRATIO' - minimum = 0.50 + minimum = 0.40 } limit { counter = 'BRANCH' value = 'COVEREDRATIO' - minimum = 0.50 + minimum = 0.40 } diff --git a/src/main/java/kr/co/morandi/backend/common/config/CorsConfig.java b/src/main/java/kr/co/morandi/backend/common/config/CorsConfig.java new file mode 100644 index 00000000..b90429eb --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/common/config/CorsConfig.java @@ -0,0 +1,28 @@ +package kr.co.morandi.backend.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +public class CorsConfig { + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.setAllowedOrigins(List.of("http://localhost:3000", "http://localhost:8080", "http://morandi.co.kr", + "https://morandi.co.kr", "http://api.morandi.co.kr", "https://api.morandi.co.kr")); + + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setExposedHeaders(List.of("*")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } +} diff --git a/src/main/java/kr/co/morandi/backend/common/exception/MorandiException.java b/src/main/java/kr/co/morandi/backend/common/exception/MorandiException.java new file mode 100644 index 00000000..66dec4f0 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/common/exception/MorandiException.java @@ -0,0 +1,16 @@ +package kr.co.morandi.backend.common.exception; + +import kr.co.morandi.backend.common.exception.errorcode.ErrorCode; +import lombok.Getter; +@Getter +public class MorandiException extends RuntimeException { + + private final ErrorCode errorCode; + + public MorandiException(ErrorCode errorCode) { + this.errorCode = errorCode; + } + public MorandiException(ErrorCode errorCode, String message) { + this.errorCode = errorCode; + } +} diff --git a/src/main/java/kr/co/morandi/backend/common/exception/errorcode/ErrorCode.java b/src/main/java/kr/co/morandi/backend/common/exception/errorcode/ErrorCode.java new file mode 100644 index 00000000..71ecbff6 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/common/exception/errorcode/ErrorCode.java @@ -0,0 +1,9 @@ +package kr.co.morandi.backend.common.exception.errorcode; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + String name(); + HttpStatus getHttpStatus(); + String getMessage(); +} diff --git a/src/main/java/kr/co/morandi/backend/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/kr/co/morandi/backend/common/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 00000000..3efd9cfc --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/common/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,82 @@ +package kr.co.morandi.backend.common.exception.handler; + +import jakarta.servlet.http.Cookie; +import kr.co.morandi.backend.common.exception.MorandiException; +import kr.co.morandi.backend.common.exception.errorcode.ErrorCode; +import kr.co.morandi.backend.common.exception.response.ErrorResponse; +import kr.co.morandi.backend.member_management.infrastructure.config.cookie.utils.CookieUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.net.URI; + +import static kr.co.morandi.backend.common.exception.handler.exception.CommonErrorCode.INTERNAL_SERVER_ERROR; +import static kr.co.morandi.backend.member_management.infrastructure.config.jwt.constants.TokenType.REFRESH_TOKEN; + +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + private final CookieUtils cookieUtils; + + @Value("${oauth2.signup-url}") + private String signupPath; + + @ExceptionHandler(MorandiException.class) + public ResponseEntity morandiExceptionHandler(MorandiException e) { + log.error(e.getErrorCode().name()+" : ", e.getErrorCode().getMessage() + " : ", e); + + // Unauthorized 에러가 발생한 경우 + if (e.getErrorCode().getHttpStatus() == HttpStatus.UNAUTHORIZED) { + HttpHeaders headers = createUnauthorizedHeaders(); + + // 로그인 페이지로 리다이렉트 + return new ResponseEntity<>(headers, HttpStatus.FOUND); + } + + // 그 외의 에러가 발생한 경우 + return handleException(e.getErrorCode()); + } + + /** + * 예상 외의 에러가 발생한 경우 + */ + @ExceptionHandler(Exception.class) + public ResponseEntity handleAllException(Exception e) { + log.error(INTERNAL_SERVER_ERROR.name() + " : ", e); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(e.getMessage()); + } + + /** + * Unauthorized 에러가 발생한 경우 + * Refresh Token 쿠키를 제거하고 + * 로그인 페이지로 리다이렉트 + */ + private HttpHeaders createUnauthorizedHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(URI.create(signupPath)); + Cookie cookie = cookieUtils.removeCookie(REFRESH_TOKEN, null); + headers.add("Set-Cookie", cookie.toString()); + return headers; + } + + /** + * 에러 응답 생성 + */ + private ResponseEntity handleException(ErrorCode errorCode) { + return ResponseEntity.status(errorCode.getHttpStatus()) + .body(ErrorResponse.of(errorCode)); + } + +} diff --git a/src/main/java/kr/co/morandi/backend/common/exception/handler/exception/CommonErrorCode.java b/src/main/java/kr/co/morandi/backend/common/exception/handler/exception/CommonErrorCode.java new file mode 100644 index 00000000..719ea45a --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/common/exception/handler/exception/CommonErrorCode.java @@ -0,0 +1,17 @@ +package kr.co.morandi.backend.common.exception.handler.exception; + +import kr.co.morandi.backend.common.exception.errorcode.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum CommonErrorCode implements ErrorCode { + + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "내부 서버 오류입니다."); + + private final HttpStatus httpStatus; + + private final String message; +} \ No newline at end of file diff --git a/src/main/java/kr/co/morandi/backend/common/exception/response/ErrorResponse.java b/src/main/java/kr/co/morandi/backend/common/exception/response/ErrorResponse.java new file mode 100644 index 00000000..dddbc644 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/common/exception/response/ErrorResponse.java @@ -0,0 +1,25 @@ +package kr.co.morandi.backend.common.exception.response; + +import kr.co.morandi.backend.common.exception.errorcode.ErrorCode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ErrorResponse { + + private final String code; + private final String message; + + public static ErrorResponse of(ErrorCode errorCode) { + return ErrorResponse.builder() + .code(errorCode.name()) + .message(errorCode.getMessage()) + .build(); + } + @Builder + private ErrorResponse(String code, String message) { + this.code = code; + this.message = message; + } +} \ No newline at end of file diff --git a/src/main/java/kr/co/morandi/backend/member_management/application/model/oauth/OAuthServiceFactory.java b/src/main/java/kr/co/morandi/backend/member_management/application/model/oauth/OAuthServiceFactory.java new file mode 100644 index 00000000..eff1d738 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/application/model/oauth/OAuthServiceFactory.java @@ -0,0 +1,21 @@ +package kr.co.morandi.backend.member_management.application.model.oauth; + +import kr.co.morandi.backend.member_management.application.service.oauth.OAuthService; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Component +public class OAuthServiceFactory { + private final Map serviceMap; + public OAuthServiceFactory(List oAuthServices) { + this.serviceMap = oAuthServices.stream() + .collect(Collectors.toMap(OAuthService::getType, Function.identity())); + } + public OAuthService getServiceByType(String type) { + return serviceMap.get(type); + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/application/port/in/oauth/AuthenticationUseCase.java b/src/main/java/kr/co/morandi/backend/member_management/application/port/in/oauth/AuthenticationUseCase.java new file mode 100644 index 00000000..14ce3dfc --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/application/port/in/oauth/AuthenticationUseCase.java @@ -0,0 +1,7 @@ +package kr.co.morandi.backend.member_management.application.port.in.oauth; + +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.response.AuthenticationToken; + +public interface AuthenticationUseCase { + AuthenticationToken getAuthenticationToken(String type, String authenticationCode); +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/application/port/out/member/MemberPort.java b/src/main/java/kr/co/morandi/backend/member_management/application/port/out/member/MemberPort.java new file mode 100644 index 00000000..11285144 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/application/port/out/member/MemberPort.java @@ -0,0 +1,12 @@ +package kr.co.morandi.backend.member_management.application.port.out.member; + +import kr.co.morandi.backend.member_management.domain.model.member.Member; +import kr.co.morandi.backend.member_management.domain.model.member.SocialType; + +import java.util.Optional; + +public interface MemberPort { + Member saveMemberByEmail(String email, SocialType type); + Member findMemberById(Long memberId); + Optional findMemberByEmail(String email); +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/application/service/jwt/MemberLoginService.java b/src/main/java/kr/co/morandi/backend/member_management/application/service/jwt/MemberLoginService.java new file mode 100644 index 00000000..306b5841 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/application/service/jwt/MemberLoginService.java @@ -0,0 +1,28 @@ +package kr.co.morandi.backend.member_management.application.service.jwt; + +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.JwtProvider; +import kr.co.morandi.backend.member_management.application.port.out.member.MemberPort; +import kr.co.morandi.backend.member_management.domain.model.member.Member; +import kr.co.morandi.backend.member_management.infrastructure.config.oauth.constants.OAuthUserInfo; +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.response.AuthenticationToken; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MemberLoginService { + + private final JwtProvider jwtProvider; + + private final MemberPort memberPort; + public AuthenticationToken loginMember(OAuthUserInfo oAuthUserInfo) { + Optional maybeMember = memberPort.findMemberByEmail(oAuthUserInfo.getEmail()); + Member member = maybeMember.isPresent() + ? maybeMember.get() : memberPort.saveMemberByEmail(oAuthUserInfo.getEmail(), oAuthUserInfo.getType()); + return jwtProvider.getAuthenticationToken(member); + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/application/service/oauth/LoginService.java b/src/main/java/kr/co/morandi/backend/member_management/application/service/oauth/LoginService.java new file mode 100644 index 00000000..c158e86a --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/application/service/oauth/LoginService.java @@ -0,0 +1,26 @@ +package kr.co.morandi.backend.member_management.application.service.oauth; + +import kr.co.morandi.backend.member_management.application.port.in.oauth.AuthenticationUseCase; +import kr.co.morandi.backend.member_management.infrastructure.config.oauth.constants.OAuthUserInfo; +import kr.co.morandi.backend.member_management.application.model.oauth.OAuthServiceFactory; +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.response.AuthenticationToken; +import kr.co.morandi.backend.member_management.application.service.jwt.MemberLoginService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LoginService implements AuthenticationUseCase { + + private final OAuthServiceFactory oAuthServiceFactory; + + private final MemberLoginService memberLoginService; + @Override + public AuthenticationToken getAuthenticationToken(String type, String authenticationCode) { + OAuthService oAuthService = oAuthServiceFactory.getServiceByType(type); + String oAuthAccessToken = oAuthService.getAccessToken(authenticationCode); + OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(oAuthAccessToken); + AuthenticationToken authenticationToken = memberLoginService.loginMember(oAuthUserInfo); + return authenticationToken; + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/application/service/oauth/OAuthService.java b/src/main/java/kr/co/morandi/backend/member_management/application/service/oauth/OAuthService.java new file mode 100644 index 00000000..a3a8033c --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/application/service/oauth/OAuthService.java @@ -0,0 +1,8 @@ +package kr.co.morandi.backend.member_management.application.service.oauth; +import kr.co.morandi.backend.member_management.infrastructure.config.oauth.constants.OAuthUserInfo; + +public interface OAuthService { + String getType(); + String getAccessToken(String authorizationCode); + OAuthUserInfo getUserInfo(String accessToken); +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/application/service/oauth/google/GoogleService.java b/src/main/java/kr/co/morandi/backend/member_management/application/service/oauth/google/GoogleService.java new file mode 100644 index 00000000..0389020c --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/application/service/oauth/google/GoogleService.java @@ -0,0 +1,107 @@ +package kr.co.morandi.backend.member_management.application.service.oauth.google; + +import kr.co.morandi.backend.common.exception.MorandiException; +import kr.co.morandi.backend.member_management.infrastructure.config.oauth.constants.OAuthUserInfo; +import kr.co.morandi.backend.member_management.domain.model.member.SocialType; +import kr.co.morandi.backend.member_management.infrastructure.config.oauth.response.TokenResponse; +import kr.co.morandi.backend.member_management.infrastructure.config.oauth.constants.google.GoogleOAuthUserInfo; +import kr.co.morandi.backend.member_management.application.service.oauth.OAuthService; +import kr.co.morandi.backend.member_management.infrastructure.exception.OAuthErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; + +@Service +@RequiredArgsConstructor +@Slf4j +public class GoogleService implements OAuthService { + + @Value("${oauth2.google.client-id}") + private String googleClientId; + + @Value("${oauth2.google.client-secret}") + private String googleClientSecret; + + @Value("${oauth2.google.redirect-callback-url}") + private String googleClientRedirectUrl; + + @Value("${oauth2.google.api-token-url}") + private String googleApiTokenUrl; + + @Value("${oauth2.google.userinfo-url}") + private String googleUserInfoUrl; + + @Value("${oauth2.google.type}") + private String type; + + private final WebClient webClient; + @Override + public String getType() { + return type; + } + @Override + public String getAccessToken(String authorizationCode) { + LinkedMultiValueMap params = buildParams(authorizationCode); + TokenResponse tokenResponse = getTokenResponse(params); + + return tokenResponse.getAccess_token(); + } + private LinkedMultiValueMap buildParams(String authorizationCode) { + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", authorizationCode); + params.add("client_id", googleClientId); + params.add("client_secret", googleClientSecret); + params.add("grant_type", "authorization_code"); + params.add("redirect_uri", googleClientRedirectUrl); + return params; + } + private TokenResponse getTokenResponse(LinkedMultiValueMap params) { + TokenResponse tokenResponse = webClient.post() + .uri(googleApiTokenUrl) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(BodyInserters.fromValue(params)) + .retrieve() + .bodyToMono(TokenResponse.class) + .retry(3) + .block(); + + if(tokenResponse == null) { + throw new MorandiException(OAuthErrorCode.GOOGLE_OAUTH_ERROR); + } + + return tokenResponse; + } + + @Override + public OAuthUserInfo getUserInfo(String accessToken) { + HttpHeaders headers = getBearerHeader(accessToken); + return getGoogleUserDto(headers); + } + private HttpHeaders getBearerHeader(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + accessToken); + return headers; + } + private GoogleOAuthUserInfo getGoogleUserDto(HttpHeaders headers) { + GoogleOAuthUserInfo googleUserDto = webClient.get() + .uri(googleUserInfoUrl) + .headers(httpHeaders -> httpHeaders.addAll(headers)) + .retrieve() + .bodyToMono(GoogleOAuthUserInfo.class) + .retry(3) + .block(); + + if(googleUserDto == null) { + throw new MorandiException(OAuthErrorCode.GOOGLE_OAUTH_ERROR); + } + + googleUserDto.setSocialType(SocialType.GOOGLE); + return googleUserDto; + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/application/service/security/OAuthUserDetailsService.java b/src/main/java/kr/co/morandi/backend/member_management/application/service/security/OAuthUserDetailsService.java new file mode 100644 index 00000000..7aeb08ec --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/application/service/security/OAuthUserDetailsService.java @@ -0,0 +1,22 @@ +package kr.co.morandi.backend.member_management.application.service.security; + +import kr.co.morandi.backend.member_management.application.port.out.member.MemberPort; +import kr.co.morandi.backend.member_management.domain.model.member.Member; +import kr.co.morandi.backend.member_management.domain.model.security.OAuthDetails; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class OAuthUserDetailsService implements UserDetailsService { + + private final MemberPort memberPort; + @Override + public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException { + Member member = memberPort.findMemberById(Long.parseLong(memberId)); + return new OAuthDetails(memberId, member.getBaekjoonId()); + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/domain/model/member/Member.java b/src/main/java/kr/co/morandi/backend/member_management/domain/model/member/Member.java index 4f7c569e..9528c18d 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/domain/model/member/Member.java +++ b/src/main/java/kr/co/morandi/backend/member_management/domain/model/member/Member.java @@ -25,9 +25,9 @@ public class Member extends BaseEntity { private String profileImageURL; private String description; - @Builder - private Member(String nickname, String baekjoonId, String email, SocialType socialType, String profileImageURL, String description) { + private Member(String nickname, String baekjoonId, String email, + SocialType socialType, String profileImageURL, String description) { this.nickname = nickname; this.baekjoonId = baekjoonId; this.email = email; @@ -35,8 +35,8 @@ private Member(String nickname, String baekjoonId, String email, SocialType soci this.profileImageURL = profileImageURL; this.description = description; } - - public static Member create(String nickname, String email, SocialType socialType, String profileImageURL, String description) { + public static Member create(String nickname, String email, SocialType socialType, + String profileImageURL, String description) { return Member.builder() .nickname(nickname) .email(email) @@ -45,4 +45,10 @@ public static Member create(String nickname, String email, SocialType socialType .description(description) .build(); } + public static Member create(String email, SocialType socialType) { + return Member.builder() + .email(email) + .socialType(socialType) + .build(); + } } diff --git a/src/main/java/kr/co/morandi/backend/member_management/domain/model/member/Role.java b/src/main/java/kr/co/morandi/backend/member_management/domain/model/member/Role.java new file mode 100644 index 00000000..5e91c88a --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/domain/model/member/Role.java @@ -0,0 +1,5 @@ +package kr.co.morandi.backend.member_management.domain.model.member; + +public enum Role { + USER, ADMIN +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/domain/model/member/SocialType.java b/src/main/java/kr/co/morandi/backend/member_management/domain/model/member/SocialType.java index 13a36697..7c94f272 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/domain/model/member/SocialType.java +++ b/src/main/java/kr/co/morandi/backend/member_management/domain/model/member/SocialType.java @@ -2,7 +2,6 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; - @Getter @RequiredArgsConstructor public enum SocialType { diff --git a/src/main/java/kr/co/morandi/backend/member_management/domain/model/security/OAuthDetails.java b/src/main/java/kr/co/morandi/backend/member_management/domain/model/security/OAuthDetails.java new file mode 100644 index 00000000..23d06887 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/domain/model/security/OAuthDetails.java @@ -0,0 +1,44 @@ +package kr.co.morandi.backend.member_management.domain.model.security; + +import lombok.AllArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +@AllArgsConstructor +public class OAuthDetails implements UserDetails { + + private String memberId; + + private String baekjoonId; + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + @Override + public String getPassword() { + return null; + } + @Override + public String getUsername() { + return memberId; + } + @Override + public boolean isAccountNonExpired() { + return true; + } + @Override + public boolean isAccountNonLocked() { + return true; + } + @Override + public boolean isCredentialsNonExpired() { + return true; + } + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/adapter/member/MemberAdapter.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/adapter/member/MemberAdapter.java new file mode 100644 index 00000000..222b8e28 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/adapter/member/MemberAdapter.java @@ -0,0 +1,33 @@ +package kr.co.morandi.backend.member_management.infrastructure.adapter.member; + +import kr.co.morandi.backend.common.exception.MorandiException; +import kr.co.morandi.backend.member_management.infrastructure.exception.OAuthErrorCode; +import kr.co.morandi.backend.member_management.application.port.out.member.MemberPort; +import kr.co.morandi.backend.member_management.domain.model.member.Member; +import kr.co.morandi.backend.member_management.domain.model.member.SocialType; +import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class MemberAdapter implements MemberPort { + + private final MemberRepository memberRepository; + @Override + public Member saveMemberByEmail(String email, SocialType type) { + return memberRepository.save(Member.create(email, type)); + } + @Override + public Optional findMemberByEmail(String email) { + return memberRepository.findByEmail(email); + } + @Override + public Member findMemberById(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new MorandiException(OAuthErrorCode.MEMBER_NOT_FOUND)); + } + +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/cookie/utils/CookieUtils.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/cookie/utils/CookieUtils.java new file mode 100644 index 00000000..27b422a7 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/cookie/utils/CookieUtils.java @@ -0,0 +1,37 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.cookie.utils; + +import jakarta.servlet.http.Cookie; +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.constants.TokenType; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class CookieUtils { + + @Value("${oauth2.cookie.domain}") + private String domain; + + @Value("${oauth2.cookie.path}") + private String path; + + private final int COOKIE_AGE = 60 * 60 * 24 * 10; + private final Integer COOKIE_REMOVE_AGE = 0; + + public Cookie getCookie(TokenType type, String value) { + Cookie cookie = new Cookie(type.name(), value); + cookie.setHttpOnly(true); + cookie.setMaxAge(COOKIE_AGE); + cookie.setDomain(domain); + cookie.setPath(path); + return cookie; + } + + public Cookie removeCookie(TokenType type, String value) { + Cookie cookie = new Cookie(type.name(), value); + cookie.setHttpOnly(true); + cookie.setMaxAge(COOKIE_REMOVE_AGE); + cookie.setDomain(domain); + cookie.setPath(path); + return cookie; + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/constants/TokenType.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/constants/TokenType.java new file mode 100644 index 00000000..6fd2ff99 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/constants/TokenType.java @@ -0,0 +1,11 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.jwt.constants; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum TokenType { + ACCESS_TOKEN("accessToken"), + REFRESH_TOKEN("refreshToken"); + + private final String name; +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/response/AuthenticationToken.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/response/AuthenticationToken.java new file mode 100644 index 00000000..6cc82376 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/response/AuthenticationToken.java @@ -0,0 +1,22 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.jwt.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class AuthenticationToken { + + private String accessToken; + + private String refreshToken; + public static AuthenticationToken create(String accessToken, String refreshToken) { + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} + diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtProvider.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtProvider.java new file mode 100644 index 00000000..affc3525 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtProvider.java @@ -0,0 +1,95 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils; + +import io.jsonwebtoken.*; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import kr.co.morandi.backend.common.exception.MorandiException; +import kr.co.morandi.backend.member_management.infrastructure.exception.OAuthErrorCode; +import kr.co.morandi.backend.member_management.domain.model.member.Member; +import kr.co.morandi.backend.member_management.domain.model.member.Role; +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.constants.TokenType; +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.response.AuthenticationToken; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.util.WebUtils; +import java.security.PrivateKey; +import java.util.Date; + +@RequiredArgsConstructor +@Slf4j +@Component +public class JwtProvider { + + private final Long ACCESS_TOKEN_EXPIRATION = 60 * 60 * 3 * 1000L; // 3 hours + private final Long REFRESH_TOKEN_EXPIRATION = 60 * 60 * 24 * 7 * 1000L; // 7 days + + private final SecretKeyProvider secretKeyProvider; + + public AuthenticationToken getAuthenticationToken(Member member) { + String accessToken = generateAccessToken(member.getMemberId(), Role.USER); + String refreshToken = generateRefreshToken(member.getMemberId(), Role.USER); + return AuthenticationToken.create(accessToken, refreshToken); + } + + public String parseAccessToken(HttpServletRequest request) { + String accessToken = request.getHeader("Authorization"); + if (StringUtils.hasText(accessToken) && accessToken.startsWith("Bearer ")) { + return accessToken.substring(7); + } + throw new MorandiException(OAuthErrorCode.ACCESS_TOKEN_NOT_FOUND); + } + public String parseRefreshToken(HttpServletRequest request) { + Cookie cookie = WebUtils.getCookie(request, "REFRESH_TOKEN"); + if(cookie==null) + throw new MorandiException(OAuthErrorCode.REFRESH_TOKEN_NOT_FOUND); + + return cookie.getValue(); + } + public String reissueAccessToken(String refreshToken) { + Long memberId = getMemberIdFromToken(refreshToken); + + return generateAccessToken(memberId, Role.USER); + } + + private String generateAccessToken(Long id, Role role) { + final Date issuedAt = new Date(); + final Date accessTokenExpiresIn = new Date(issuedAt.getTime() + ACCESS_TOKEN_EXPIRATION); + return buildAccessToken(id, issuedAt, accessTokenExpiresIn, role); + } + private String generateRefreshToken(Long id, Role role) { + final Date issuedAt = new Date(); + final Date refreshTokenExpiresIn = new Date(issuedAt.getTime() + REFRESH_TOKEN_EXPIRATION); + return buildRefreshToken(id, issuedAt, refreshTokenExpiresIn, role); + } + private String buildAccessToken(Long id, Date issuedAt, Date expiresIn, Role role) { + final PrivateKey encodedKey = secretKeyProvider.getPrivateKey(); + return jwtCreate(id, issuedAt, expiresIn, role, encodedKey, TokenType.ACCESS_TOKEN); + } + private String buildRefreshToken(Long id, Date issuedAt, Date expiresIn, Role role) { + final PrivateKey encodedKey = secretKeyProvider.getPrivateKey(); + + return jwtCreate(id, issuedAt, expiresIn, role, encodedKey, TokenType.REFRESH_TOKEN); + } + + private String jwtCreate(Long id, Date issuedAt, Date expiresIn, Role role, + PrivateKey encodedKey, TokenType tokenType) { + return Jwts.builder() + .setIssuer("MORANDI") + .setIssuedAt(issuedAt) + .setSubject(id.toString()) + .claim("type", tokenType) + .claim("role", role) + .setExpiration(expiresIn) + .signWith(encodedKey) + .compact(); + } + private Long getMemberIdFromToken(String token) { + Jws claimsJws = Jwts.parserBuilder().setSigningKey(secretKeyProvider.getPublicKey()) + .build() + .parseClaimsJws(token); + Claims claims = claimsJws.getBody(); + return Long.parseLong(claims.getSubject()); + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtValidator.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtValidator.java new file mode 100644 index 00000000..55458dbd --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtValidator.java @@ -0,0 +1,32 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import kr.co.morandi.backend.common.exception.MorandiException; +import kr.co.morandi.backend.member_management.infrastructure.exception.OAuthErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +@RequiredArgsConstructor +public class JwtValidator { + + private final SecretKeyProvider secretKeyProvider; + public boolean validateToken(String token) { + if (!StringUtils.hasText(token)) + throw new MorandiException(OAuthErrorCode.INVALID_TOKEN); + try { + Jwts.parserBuilder() + .setSigningKey(secretKeyProvider.getPublicKey()) + .build() + .parseClaimsJws(token); + return true; + } catch (ExpiredJwtException e) { + return false; + } catch (JwtException e) { + throw new MorandiException(OAuthErrorCode.INVALID_TOKEN); + } + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/SecretKeyProvider.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/SecretKeyProvider.java new file mode 100644 index 00000000..3f4a4035 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/SecretKeyProvider.java @@ -0,0 +1,68 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +@Component +@Getter +public class SecretKeyProvider { + + private final PublicKey publicKey; + + private final PrivateKey privateKey; + + public SecretKeyProvider(@Value("${security.publicKey}") String publicKey, + @Value("${security.privateKey}") String privateKey) { + this.publicKey = convertPEMToPublicKey(decoding(publicKey)); + this.privateKey = convertPEMToPrivateKey(decoding(privateKey)); + } + private String decoding(String key) { + byte[] decoded = Base64.getDecoder().decode(key); + return new String(decoded, StandardCharsets.UTF_8); + } + public PublicKey convertPEMToPublicKey(String publicKeyPemFile) { + String publicKeyPEM = extractPublicPemKeyContent(publicKeyPemFile); + byte[] encodedKey = Base64.getDecoder().decode(publicKeyPEM); + try { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encodedKey); + return keyFactory.generatePublic(keySpec); + } + catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new RuntimeException(e); + } + } + private PrivateKey convertPEMToPrivateKey(String privateKeyPemFile) { + String privateKeyPEM = extractPrivatePemKeyContent(privateKeyPemFile); + byte[] encodedKey = Base64.getDecoder().decode(privateKeyPEM); + try { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedKey); + return keyFactory.generatePrivate(keySpec); + } + catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new RuntimeException(e); + } + } + private String extractPublicPemKeyContent(String pemKey) { + return pemKey.replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s", ""); + } + + private String extractPrivatePemKeyContent(String pemKey) { + return pemKey.replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s", ""); + } +} + diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/oauth/constants/OAuthUserInfo.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/oauth/constants/OAuthUserInfo.java new file mode 100644 index 00000000..c3dc0e87 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/oauth/constants/OAuthUserInfo.java @@ -0,0 +1,9 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.oauth.constants; + +import kr.co.morandi.backend.member_management.domain.model.member.SocialType; + +public interface OAuthUserInfo { + SocialType getType(); + String getEmail(); + String getPicture(); +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/oauth/constants/google/GoogleOAuthUserInfo.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/oauth/constants/google/GoogleOAuthUserInfo.java new file mode 100644 index 00000000..cfe90c97 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/oauth/constants/google/GoogleOAuthUserInfo.java @@ -0,0 +1,40 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.oauth.constants.google; + +import kr.co.morandi.backend.member_management.infrastructure.config.oauth.constants.OAuthUserInfo; +import kr.co.morandi.backend.member_management.domain.model.member.SocialType; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class GoogleOAuthUserInfo implements OAuthUserInfo { + + private String id; + private String email; + private String verified_email; + private String hd; + private String name; + private String given_name; + private String family_name; + private String picture; + private String locale; + private SocialType type; + + @Override + public SocialType getType() { + return type; + } + public void setSocialType(SocialType type) { + this.type = type; + } + + @Override + public String getEmail() { + return email; + } + + @Override + public String getPicture() { + return picture; + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/oauth/response/TokenResponse.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/oauth/response/TokenResponse.java new file mode 100644 index 00000000..83342eac --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/oauth/response/TokenResponse.java @@ -0,0 +1,21 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.oauth.response; + +import lombok.Getter; + +@Getter +public class TokenResponse { + + public String token_type; + + public String access_token; + + public String id_token; + + public Integer expires_in; + + public String refresh_token; + + public Integer refresh_token_expires_in; + + public String scope; +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java new file mode 100644 index 00000000..0cbeb7a3 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java @@ -0,0 +1,45 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.security; + +import kr.co.morandi.backend.member_management.infrastructure.filter.oauth.JwtAuthenticationFilter; +import kr.co.morandi.backend.member_management.infrastructure.filter.oauth.JwtExceptionFilter; +import kr.co.morandi.backend.member_management.infrastructure.filter.oauth.RequestCachingFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.web.cors.CorsConfigurationSource; + +@EnableWebSecurity +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final JwtExceptionFilter jwtExceptionFilter; + private final RequestCachingFilter requestCachingFilter; + private final CorsConfigurationSource corsConfigurationSource; + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{ + http + .httpBasic(HttpBasicConfigurer::disable) + .csrf(CsrfConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource)) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/oauths/**","/swagger-ui/**", "/swagger-resources/**", + "/v3/api-docs/**").permitAll() + .anyRequest().authenticated()) + .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class) + .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class) + .addFilterBefore(requestCachingFilter, JwtExceptionFilter.class); + + return http.build(); + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/AuthenticationProvider.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/AuthenticationProvider.java new file mode 100644 index 00000000..7a54b97c --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/AuthenticationProvider.java @@ -0,0 +1,40 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.security.utils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import kr.co.morandi.backend.member_management.application.service.security.OAuthUserDetailsService; +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.SecretKeyProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class AuthenticationProvider { + + private final OAuthUserDetailsService oAuthUserDetailsService; + + private final SecretKeyProvider secretKeyProvider; + public void setAuthentication(String accessToken) { + Authentication authentication = getAuthentication(accessToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + private Authentication getAuthentication(String accessToken) { + Long memberId = getMemberIdFromToken(accessToken); + UserDetails userDetails = oAuthUserDetailsService.loadUserByUsername(memberId.toString()); + return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + } + private Long getMemberIdFromToken(String token) { + Jws claimsJws = Jwts.parserBuilder().setSigningKey(secretKeyProvider.getPublicKey()) + .build() + .parseClaimsJws(token); + Claims claims = claimsJws.getBody(); + return Long.parseLong(claims.getSubject()); + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/IgnoredURIManager.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/IgnoredURIManager.java new file mode 100644 index 00000000..1d932ec1 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/IgnoredURIManager.java @@ -0,0 +1,22 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.security.utils; + +import org.springframework.stereotype.Component; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Component +public class IgnoredURIManager { + private static final String[] IGNORED_URIS = { + "/oauths/", + "/swagger-ui/", + "/v3/api-docs/", + "/swagger-resources/" + }; + private String PATTERN_STRING = String.join("|", IGNORED_URIS); + public Pattern PATTERN = Pattern.compile(PATTERN_STRING); + public boolean isIgnoredURI(String uri) { + Matcher matcher = PATTERN.matcher(uri); + return matcher.find(); + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/SecurityUtils.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/SecurityUtils.java new file mode 100644 index 00000000..b28f5dc5 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/SecurityUtils.java @@ -0,0 +1,11 @@ +package kr.co.morandi.backend.member_management.infrastructure.config.security.utils; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public class SecurityUtils { + public static Long getCurrentMemberId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return Long.valueOf(authentication.getName()); + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/controller/oauth/OAuthController.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/controller/oauth/OAuthController.java new file mode 100644 index 00000000..e820d5ab --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/controller/oauth/OAuthController.java @@ -0,0 +1,43 @@ +package kr.co.morandi.backend.member_management.infrastructure.controller.oauth; + +import jakarta.servlet.http.HttpServletResponse; +import kr.co.morandi.backend.member_management.application.port.in.oauth.AuthenticationUseCase; +import kr.co.morandi.backend.member_management.infrastructure.config.cookie.utils.CookieUtils; +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.response.AuthenticationToken; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; + +import static kr.co.morandi.backend.member_management.infrastructure.config.jwt.constants.TokenType.REFRESH_TOKEN; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/oauths") +@Slf4j +public class OAuthController { + + private final AuthenticationUseCase authenticationUseCase; + + private final CookieUtils cookieUtils; + + @Value("${morandi.redirect-url}") + private String redirectUrl; + + @GetMapping("/{type}/callback") + public ResponseEntity OAuthLogin(@PathVariable String type, + @RequestParam String code, + HttpServletResponse response) { + AuthenticationToken authenticationToken = authenticationUseCase.getAuthenticationToken(type, code); + response.setHeader("Authorization", "Bearer " + authenticationToken.getAccessToken()); + response.addCookie(cookieUtils.getCookie(REFRESH_TOKEN, authenticationToken.getRefreshToken())); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(redirectUrl)) + .build(); + } + +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/controller/oauth/OAuthURLController.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/controller/oauth/OAuthURLController.java new file mode 100644 index 00000000..0a4fe6d1 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/controller/oauth/OAuthURLController.java @@ -0,0 +1,20 @@ +package kr.co.morandi.backend.member_management.infrastructure.controller.oauth; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/oauths") +@RequiredArgsConstructor +public class OAuthURLController { + + @Value("${oauth2.google.redirect-url}") + private String googleRedirectUrl; + @GetMapping("/google") + public String googleRedirect() { + return "redirect:" + googleRedirectUrl; + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/exception/OAuthErrorCode.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/exception/OAuthErrorCode.java new file mode 100644 index 00000000..c4ddb706 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/exception/OAuthErrorCode.java @@ -0,0 +1,25 @@ +package kr.co.morandi.backend.member_management.infrastructure.exception; + + +import kr.co.morandi.backend.common.exception.errorcode.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum OAuthErrorCode implements ErrorCode { + + MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST,"사용자를 찾을 수 없습니다"), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED,"인증 시간이 만료된 토큰입니다."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED,"유효하지 않은 토큰입니다."), + TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "액세스 토큰을 찾을 수 없습니다"), + ACCESS_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "액세스 토큰을 찾을 수 없습니다."), + REFRESH_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "리프레시 토큰을 찾을 수 없습니다."), + UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"알 수 없는 오류"), + GOOGLE_OAUTH_ERROR(HttpStatus.BAD_REQUEST,"구글 OAuth 인증을 실패했습니다."); + + + private final HttpStatus httpStatus; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/CachedBodyHttpServletWrapper.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/CachedBodyHttpServletWrapper.java new file mode 100644 index 00000000..a675a9be --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/CachedBodyHttpServletWrapper.java @@ -0,0 +1,58 @@ +package kr.co.morandi.backend.member_management.infrastructure.filter.oauth; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import lombok.SneakyThrows; +import org.springframework.util.StreamUtils; + +import java.io.*; + +public class CachedBodyHttpServletWrapper extends HttpServletRequestWrapper { + private final byte[] cachedBody; + + public CachedBodyHttpServletWrapper(HttpServletRequest request) throws IOException { + super(request); + InputStream requestInputStream = request.getInputStream(); + this.cachedBody = StreamUtils.copyToByteArray(requestInputStream); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + return new CachedBodyServletInputStream(this.cachedBody); + } + + @Override + public BufferedReader getReader() throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody); + return new BufferedReader(new InputStreamReader(byteArrayInputStream, "UTF-8")); + } + public static class CachedBodyServletInputStream extends ServletInputStream { + + private final InputStream cachedBodyInputStream; + + public CachedBodyServletInputStream(byte[] cachedBody) { + this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody); + } + @SneakyThrows + @Override + public boolean isFinished() { + return cachedBodyInputStream.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener listener) { + + } + @Override + public int read() throws IOException { + return cachedBodyInputStream.read(); + } + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtAuthenticationFilter.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtAuthenticationFilter.java new file mode 100644 index 00000000..11a0e398 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtAuthenticationFilter.java @@ -0,0 +1,74 @@ +package kr.co.morandi.backend.member_management.infrastructure.filter.oauth; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import kr.co.morandi.backend.common.exception.MorandiException; +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.JwtValidator; +import kr.co.morandi.backend.member_management.infrastructure.exception.OAuthErrorCode; +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.JwtProvider; +import kr.co.morandi.backend.member_management.infrastructure.config.security.utils.AuthenticationProvider; +import kr.co.morandi.backend.member_management.infrastructure.config.security.utils.IgnoredURIManager; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtProvider jwtProvider; + + private final JwtValidator jwtValidator; + + private final AuthenticationProvider authenticationProvider; + + private final IgnoredURIManager isIgnoredURIManager; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + /* + * 인증 필요 없는 URI인 경우 필터링하지 않고 바로 다음 필터로 넘어간다. + * */ + if (isIgnoredURIManager.isIgnoredURI(request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + + String accessToken = jwtProvider.parseAccessToken(request); + String refreshToken = jwtProvider.parseRefreshToken(request); + + /* + * accessToken이 유효한 경우, accessToken을 이용하여 인증을 수행하고 다음 필터로 넘어간다. + * */ + if (jwtValidator.validateToken(accessToken)) { // accessToken이 유효할 경우 + authenticationProvider.setAuthentication(accessToken); + + filterChain.doFilter(request, response); + return ; + } + + /* + * accessToken의 유효 기간이 만료된 경우, refreshToken을 이용하여 accessToken을 재발급하고 다음 필터로 넘어간다. + * */ + if (jwtValidator.validateToken(refreshToken)) { // refreshToken이 유효할 경우 + accessToken = jwtProvider.reissueAccessToken(refreshToken); + response.setHeader("Authorization", "Bearer " + accessToken); + + authenticationProvider.setAuthentication(accessToken); + filterChain.doFilter(request, response); + return ; + } + + /* + * refreshToken의 유효 기간도 만료된 경우, refreshToken이 만료되었다는 오류를 반환한다. + * */ + throw new MorandiException(OAuthErrorCode.EXPIRED_TOKEN); + } + +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtExceptionFilter.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtExceptionFilter.java new file mode 100644 index 00000000..cdda741a --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtExceptionFilter.java @@ -0,0 +1,83 @@ +package kr.co.morandi.backend.member_management.infrastructure.filter.oauth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import kr.co.morandi.backend.common.exception.MorandiException; +import kr.co.morandi.backend.member_management.infrastructure.exception.OAuthErrorCode; +import kr.co.morandi.backend.common.exception.errorcode.ErrorCode; +import kr.co.morandi.backend.common.exception.response.ErrorResponse; +import kr.co.morandi.backend.member_management.infrastructure.config.cookie.utils.CookieUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static kr.co.morandi.backend.member_management.infrastructure.config.jwt.constants.TokenType.REFRESH_TOKEN; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtExceptionFilter extends OncePerRequestFilter { + + private final ObjectMapper objectMapper; + + private final CookieUtils cookieUtils; + + @Value("${oauth2.signup-url}") + private String signupPath; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException { + try { + /* + * 다음 필터인 JwtAuthenticationFilter에서 발생한 예외를 처리하기 위해 필터를 실행한다. + * */ + filterChain.doFilter(request, response); + } catch (MorandiException e) { + /* + * JwtAuthenticationFilter에서 발생한 예외가 인증 오류인 경우, refreshToken을 제거하고 로그인 페이지로 리다이렉트한다. + * */ + if (isAuthError(e)) { + Cookie cookie = cookieUtils.removeCookie(REFRESH_TOKEN,null); + response.addCookie(cookie); + response.sendRedirect(signupPath); + } + + /* + * JwtAuthenticationFilter에서 발생한 예외가 인증 오류가 아닌 경우, 예외에 해당하는 오류 응답을 반환한다. + */ + setErrorResponse(response, e.getErrorCode()); + } catch (Exception e) { + /* + * JwtAuthenticationFilter에서 발생한 예외가 MorandiException이 아닌 경우, 알 수 없는 오류 응답을 반환한다. + * */ + setErrorResponse(response, OAuthErrorCode.UNKNOWN_ERROR); + } + } + private boolean isAuthError(MorandiException e) { + return e.getErrorCode().getHttpStatus() == (HttpStatus.UNAUTHORIZED); + } + private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) { + response.setStatus(errorCode.getHttpStatus().value()); + response.setCharacterEncoding("UTF-8"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + ErrorResponse errorResponse = ErrorResponse.of(errorCode); + + try { + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } catch (IOException e){ + log.error("IOException occurred while writing error response", e); + } + } + +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/RequestCachingFilter.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/RequestCachingFilter.java new file mode 100644 index 00000000..cb6a4063 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/RequestCachingFilter.java @@ -0,0 +1,20 @@ +package kr.co.morandi.backend.member_management.infrastructure.filter.oauth; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class RequestCachingFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + CachedBodyHttpServletWrapper cachedBodyHttpServletWrapper = new CachedBodyHttpServletWrapper(request); + filterChain.doFilter(cachedBodyHttpServletWrapper, response); + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/persistence/member/MemberRepository.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/persistence/member/MemberRepository.java index d24ec1d4..c1e0da80 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/persistence/member/MemberRepository.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/persistence/member/MemberRepository.java @@ -3,7 +3,9 @@ import kr.co.morandi.backend.member_management.domain.model.member.Member; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface MemberRepository extends JpaRepository { Boolean existsByNickname(String nickname); - + Optional findByEmail(String email); } diff --git a/src/test/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSessionTest.java b/src/test/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSessionTest.java index 8ca8e81c..cf4d8bb0 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSessionTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSessionTest.java @@ -2,10 +2,9 @@ import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; -import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; -import kr.co.morandi.backend.defense_management.domain.model.session.SessionDetail; import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.TempCode; import kr.co.morandi.backend.member_management.domain.model.member.Member; +import kr.co.morandi.backend.member_management.domain.model.member.SocialType; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import org.junit.jupiter.api.DisplayName; @@ -22,7 +21,6 @@ import static kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier.*; import static kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language.CPP; -import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -183,7 +181,7 @@ void startSession() { } private Member createMember() { - return Member.create("nickname", "email", GOOGLE, "imageURL", "description"); + return Member.create("nickname", "email", SocialType.GOOGLE, "imageURL", "description"); } private DailyDefense createDailyDefense(LocalDate createdDate) { AtomicLong problemNumber = new AtomicLong(1L); diff --git a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/session/DefenseSessionAdapterTest.java b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/session/DefenseSessionAdapterTest.java index 83daf72f..1b2db030 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/session/DefenseSessionAdapterTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/session/DefenseSessionAdapterTest.java @@ -5,6 +5,7 @@ import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.defense_management.infrastructure.persistence.session.DefenseSessionRepository; import kr.co.morandi.backend.defense_management.infrastructure.persistence.session.SessionDetailRepository; +import kr.co.morandi.backend.member_management.domain.model.member.SocialType; import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -18,7 +19,6 @@ import java.util.Set; import static kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType.DAILY; -import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest @@ -70,7 +70,7 @@ void findDailyDefenseSession() { private Member createMember() { - return memberRepository.save(Member.create("test", "test" + "@gmail.com", GOOGLE, "test", "test")); + return memberRepository.save(Member.create("test", "test" + "@gmail.com", SocialType.GOOGLE, "test", "test")); } } \ No newline at end of file diff --git a/src/test/java/kr/co/morandi/backend/member_management/domain/model/member/MemberTest.java b/src/test/java/kr/co/morandi/backend/member_management/domain/model/member/MemberTest.java index 08f26db2..3862e479 100644 --- a/src/test/java/kr/co/morandi/backend/member_management/domain/model/member/MemberTest.java +++ b/src/test/java/kr/co/morandi/backend/member_management/domain/model/member/MemberTest.java @@ -1,7 +1,5 @@ package kr.co.morandi.backend.member_management.domain.model.member; -import kr.co.morandi.backend.member_management.domain.model.member.Member; -import kr.co.morandi.backend.member_management.domain.model.member.SocialType; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/kr/co/morandi/backend/member_management/infrastructure/persistence/member/MemberRepositoryTest.java b/src/test/java/kr/co/morandi/backend/member_management/infrastructure/persistence/member/MemberRepositoryTest.java index bcc0beb7..1a637980 100644 --- a/src/test/java/kr/co/morandi/backend/member_management/infrastructure/persistence/member/MemberRepositoryTest.java +++ b/src/test/java/kr/co/morandi/backend/member_management/infrastructure/persistence/member/MemberRepositoryTest.java @@ -1,7 +1,7 @@ package kr.co.morandi.backend.member_management.infrastructure.persistence.member; import kr.co.morandi.backend.member_management.domain.model.member.Member; -import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; +import kr.co.morandi.backend.member_management.domain.model.member.SocialType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -9,8 +9,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.test.context.ActiveProfiles; - -import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -31,7 +29,7 @@ void tearDown() { void existsByNickname() { // given String nickname = "test"; - Member member = Member.create(nickname, "test@test.com", GOOGLE, "testImageUrl", "testDescription"); + Member member = Member.create(nickname, "test@test.com", SocialType.GOOGLE, "testImageUrl", "testDescription"); memberRepository.save(member); // when @@ -58,10 +56,10 @@ void whenNicknameNotExists() { void test() { // given String nickname = "test"; - Member originMember = Member.create(nickname, "test@test.com", GOOGLE, "testImageUrl", "testDescription"); + Member originMember = Member.create(nickname, "test@test.com", SocialType.GOOGLE, "testImageUrl", "testDescription"); memberRepository.save(originMember); - Member newMember = Member.create(nickname, "test2@test.com", GOOGLE, "testImageUrl", "testDescription"); + Member newMember = Member.create(nickname, "test2@test.com", SocialType.GOOGLE, "testImageUrl", "testDescription"); // when & then assertThatThrownBy(() -> memberRepository.save(newMember)) .isInstanceOf(DataIntegrityViolationException.class); From 49654506287315d86eed1c73d9a157dc7a361e40 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 8 Apr 2024 19:12:31 +0900 Subject: [PATCH 26/44] =?UTF-8?q?:sparkles:=20=EC=8B=9C=ED=97=98=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=8B=9C=20=EB=AC=B8=EC=A0=9C=20=EB=B3=B8?= =?UTF-8?q?=EB=AC=B8=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B2=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../problemcontent/ProblemContentPort.java | 11 ++ .../DailyDefenseManagementService.java | 1 + .../problemcontent/ProblemContentAdapter.java | 65 +++++++++ .../ProblemContentAdapterTest.java | 133 ++++++++++++++++++ 4 files changed, 210 insertions(+) create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java create mode 100644 src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java b/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java new file mode 100644 index 00000000..dadb28ac --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java @@ -0,0 +1,11 @@ +package kr.co.morandi.backend.defense_management.application.port.out.problemcontent; + +import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; + +import java.util.List; +import java.util.Map; + +public interface ProblemContentPort { + + Map getProblemContents(List baekjoonProblemIds); +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java index 5bb17f79..1199cb68 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java @@ -5,6 +5,7 @@ import kr.co.morandi.backend.problem_information.application.port.out.problemcontent.ProblemContentPort; import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; +import kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent.ProblemContentAdapter; import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.service.defense.ProblemGenerationService; diff --git a/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java b/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java new file mode 100644 index 00000000..b6543db6 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java @@ -0,0 +1,65 @@ +package kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import kr.co.morandi.backend.defense_management.application.port.out.problemcontent.ProblemContentPort; +import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class ProblemContentAdapter implements ProblemContentPort { + + private final WebClient webClient; + private final ObjectMapper objectMapper; + + private static final String PROBLEM_CONTENTS_API_URL = "https://n1bcmtru2j.execute-api.ap-northeast-2.amazonaws.com/default/getBaekjoonProblemContents?baekjoonProblemIds=%s"; + + @Override + public Map getProblemContents(List baekjoonProblemIds) { + + if(baekjoonProblemIds.isEmpty()) { + return Map.of(); + } + + if(baekjoonProblemIds.size() > 10) { + throw new IllegalArgumentException("문제 번호는 10개 이하로 요청해주세요."); + } + + String baekjoonProblemIdsParam = baekjoonProblemIds.stream() + .map(String::valueOf) + .collect(Collectors.joining(",")); + + String responseBody = webClient.get() + .uri(String.format(PROBLEM_CONTENTS_API_URL, baekjoonProblemIdsParam)) + .retrieve() + .bodyToMono(String.class) + .block(); + + return parseResponse(responseBody); + } + + + private Map parseResponse(String responseBody) { + try { + objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + List problemContents = objectMapper.readValue(responseBody, new TypeReference<>() { + }); + + return problemContents.stream() + .filter(content -> content.getError() == null && content.getBaekjoonProblemId() != null) + .collect(Collectors.toMap(ProblemContent::getBaekjoonProblemId, content -> content)); + + } catch (Exception e) { + throw new RuntimeException("Error parsing problem contents", e); + } + } + +} diff --git a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java new file mode 100644 index 00000000..3b753d47 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java @@ -0,0 +1,133 @@ +package kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent; + +import com.fasterxml.jackson.databind.ObjectMapper; +import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.reactive.function.client.WebClient; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.groups.Tuple.tuple; + +@ActiveProfiles("test") +class ProblemContentAdapterTest { + + private ProblemContentAdapter problemContentAdapter; + + private MockWebServer mockWebServer; + + @BeforeEach + void setUp() throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + mockWebServer = new MockWebServer(); + mockWebServer.start(); + String mockServerUrl = mockWebServer.url("/") + .toString(); + + WebClient webClient = WebClient.builder() + .baseUrl(mockServerUrl) + .build(); + + problemContentAdapter = new ProblemContentAdapter(webClient, objectMapper); + } + + @AfterEach + void tearDown() throws IOException { + mockWebServer.shutdown(); + } + + @DisplayName("문제 번호 리스트를 받아서 해당 문제 번호의 문제 정보를 반환한다.") + @Test + void getProblemContents() { + // given + List list = List.of(1000L, 1001L); + + mockWebServer.enqueue(new MockResponse() + .setResponseCode(200) + .setBody(""" + [ + { + "baekjoonProblemId": 1000, + "title": "A+B", + }, + { + "baekjoonProblemId": 1001, + "title": "A-B", + } + ]""") + .addHeader("Content-Type", "application/json")); + + + // when + final Map result = problemContentAdapter.getProblemContents(list); + + // then + assertThat(result.values()).hasSize(2) + .extracting("baekjoonProblemId", "title") + .containsExactlyInAnyOrder( + tuple(1000L, "A+B"), + tuple(1001L, "A-B") + ); + + + } + + @DisplayName("존재하지 않는 문제 번호를 포함하여 요청하면 해당 문제 번호를 제외하고 반환한다.") + @Test + void getProblemContentsContainsInvalidBaekjoonProblemId() { + // given + List list = List.of(1000L, 1001L, 999L); + mockWebServer.enqueue(new MockResponse() + .setResponseCode(200) + .setBody(""" + [ + { + "baekjoonProblemId": 1000, + "title": "A+B", + }, + { + "baekjoonProblemId": 1001, + "title": "A-B", + }, + { + "error" : "problem/999.json not exist" + } + ]""") + .addHeader("Content-Type", "application/json")); + + // when + final Map result = problemContentAdapter.getProblemContents(list); + + // then + assertThat(result.values()).hasSize(2) + .extracting("baekjoonProblemId", "title") + .containsExactlyInAnyOrder( + tuple(1000L, "A+B"), + tuple(1001L, "A-B") + ); + + } + + @DisplayName("10개 이상의 문제 번호를 요청하면 예외가 발생한다.") + @Test + void getProblemContentsContainsMoreThan10BaekjoonProblemId() { + // given + List list = List.of(1000L, 1001L, 1002L, 1003L, 1004L, 1005L, 1006L, 1007L, 1008L, 1009L, 1010L); + + // when & then + assertThatThrownBy(() -> problemContentAdapter.getProblemContents(list)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("문제 번호는 10개 이하로 요청해주세요."); + + } +} \ No newline at end of file From 391bc83b006d15e24cabf1cf944e7f2d80815d83 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 22 Apr 2024 18:39:05 +0900 Subject: [PATCH 27/44] =?UTF-8?q?:white=5Fcheck=5Fmark:=20Spring=20Securit?= =?UTF-8?q?y=20&=20WebMvcTest=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 26 ++++++++------- .../config/security/SecurityConfig.java | 4 ++- .../DailyDefenseControllerTest.java | 14 +++++++- .../controller/DailyRecordControllerTest.java | 32 ++++++++++++++++++- 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index 73d3f0d5..0903452c 100644 --- a/build.gradle +++ b/build.gradle @@ -72,35 +72,37 @@ repositories { } dependencies { + + // Spring Data Jpa implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + // Spring Data Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' -// implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - -// implementation 'org.springframework.boot:spring-boot-starter-security' -// testImplementation 'org.springframework.security:spring-security-test' + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' - // webclient + // WebFlux (WebClient) implementation 'org.springframework.boot:spring-boot-starter-webflux' + // Spring Web implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-webflux' - + // JWT implementation 'io.jsonwebtoken:jjwt-api:0.11.2' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2' + // MariaDB runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + // lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + // Test Dependencies testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'com.h2database:h2' } @@ -149,13 +151,13 @@ jacocoTestCoverageVerification { limit { counter = 'LINE' value = 'COVEREDRATIO' - minimum = 0.40 + minimum = 0.50 } limit { counter = 'BRANCH' value = 'COVEREDRATIO' - minimum = 0.40 + minimum = 0.50 } diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java index 0cbeb7a3..134d887c 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java @@ -6,7 +6,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.Customizer; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; @@ -34,6 +34,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(authorize -> authorize .requestMatchers("/oauths/**","/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/daily-record/rankings/**").permitAll() + .requestMatchers(HttpMethod.GET, "/daily-defense/**").permitAll() .anyRequest().authenticated()) .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class) diff --git a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java index dc5089fa..79bb513f 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java @@ -2,14 +2,19 @@ import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; import kr.co.morandi.backend.defense_information.application.port.in.DailyDefenseUseCase; +import kr.co.morandi.backend.member_management.infrastructure.config.cookie.utils.CookieUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.web.filter.OncePerRequestFilter; import java.util.List; @@ -19,7 +24,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(controllers = DailyDefenseController.class) +@WebMvcTest(controllers = DailyDefenseController.class, + excludeAutoConfiguration = SecurityAutoConfiguration.class, + excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {OncePerRequestFilter.class})}) @ActiveProfiles("test") class DailyDefenseControllerTest { @@ -29,8 +37,12 @@ class DailyDefenseControllerTest { @MockBean private DailyDefenseUseCase dailyDefenseUseCase; + @MockBean + private CookieUtils cookieUtils; + @DisplayName("DailyDefense 정보를 로그인하지 않은 상태에서 가져올 수 있다.") @Test +// @WithMockUser void getDailyDefenseInfo() throws Exception { // given when(dailyDefenseUseCase.getDailyDefenseInfo(any(), any())) diff --git a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java index fbf5d3a2..c7414e93 100644 --- a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java @@ -2,14 +2,38 @@ import kr.co.morandi.backend.defense_record.application.dto.DailyDefenseRankPageResponse; import kr.co.morandi.backend.defense_record.application.port.in.DailyRecordRankUseCase; +import kr.co.morandi.backend.member_management.infrastructure.config.cookie.utils.CookieUtils; +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.JwtProvider; +import kr.co.morandi.backend.member_management.infrastructure.config.security.SecurityConfig; +import kr.co.morandi.backend.member_management.infrastructure.filter.oauth.JwtAuthenticationFilter; +import kr.co.morandi.backend.member_management.infrastructure.filter.oauth.JwtExceptionFilter; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationFilter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.web.filter.GenericFilterBean; +import org.springframework.web.filter.OncePerRequestFilter; import java.util.List; @@ -20,7 +44,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(controllers = DailyRecordController.class) +@WebMvcTest(controllers = DailyRecordController.class, + excludeAutoConfiguration = SecurityAutoConfiguration.class, + excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {OncePerRequestFilter.class})}) @ActiveProfiles("test") class DailyRecordControllerTest { @@ -30,6 +57,9 @@ class DailyRecordControllerTest { @MockBean private DailyRecordRankUseCase dailyRecordRankUseCase; + @MockBean + private CookieUtils cookieUtils; + @DisplayName("[GET] DailyDefense 순위를 조회한다.") @Test void getDailyRecordRank() throws Exception { From 81758af0d7da633c0f3a90f8dbacbd54752a27b0 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 22 Apr 2024 18:58:00 +0900 Subject: [PATCH 28/44] =?UTF-8?q?:fire:=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../problemcontent/ProblemContentPort.java | 11 -- .../problemcontent/ProblemContentAdapter.java | 65 --------- .../ProblemContentAdapterTest.java | 133 ------------------ .../ProblemContentAdapterTest.java | 1 - 4 files changed, 210 deletions(-) delete mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java delete mode 100644 src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java delete mode 100644 src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java b/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java deleted file mode 100644 index dadb28ac..00000000 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/problemcontent/ProblemContentPort.java +++ /dev/null @@ -1,11 +0,0 @@ -package kr.co.morandi.backend.defense_management.application.port.out.problemcontent; - -import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; - -import java.util.List; -import java.util.Map; - -public interface ProblemContentPort { - - Map getProblemContents(List baekjoonProblemIds); -} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java b/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java deleted file mode 100644 index b6543db6..00000000 --- a/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapter.java +++ /dev/null @@ -1,65 +0,0 @@ -package kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import kr.co.morandi.backend.defense_management.application.port.out.problemcontent.ProblemContentPort; -import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClient; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Component -@RequiredArgsConstructor -public class ProblemContentAdapter implements ProblemContentPort { - - private final WebClient webClient; - private final ObjectMapper objectMapper; - - private static final String PROBLEM_CONTENTS_API_URL = "https://n1bcmtru2j.execute-api.ap-northeast-2.amazonaws.com/default/getBaekjoonProblemContents?baekjoonProblemIds=%s"; - - @Override - public Map getProblemContents(List baekjoonProblemIds) { - - if(baekjoonProblemIds.isEmpty()) { - return Map.of(); - } - - if(baekjoonProblemIds.size() > 10) { - throw new IllegalArgumentException("문제 번호는 10개 이하로 요청해주세요."); - } - - String baekjoonProblemIdsParam = baekjoonProblemIds.stream() - .map(String::valueOf) - .collect(Collectors.joining(",")); - - String responseBody = webClient.get() - .uri(String.format(PROBLEM_CONTENTS_API_URL, baekjoonProblemIdsParam)) - .retrieve() - .bodyToMono(String.class) - .block(); - - return parseResponse(responseBody); - } - - - private Map parseResponse(String responseBody) { - try { - objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); - List problemContents = objectMapper.readValue(responseBody, new TypeReference<>() { - }); - - return problemContents.stream() - .filter(content -> content.getError() == null && content.getBaekjoonProblemId() != null) - .collect(Collectors.toMap(ProblemContent::getBaekjoonProblemId, content -> content)); - - } catch (Exception e) { - throw new RuntimeException("Error parsing problem contents", e); - } - } - -} diff --git a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java deleted file mode 100644 index 3b753d47..00000000 --- a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java +++ /dev/null @@ -1,133 +0,0 @@ -package kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent; - -import com.fasterxml.jackson.databind.ObjectMapper; -import kr.co.morandi.backend.defense_management.application.response.problemcontent.ProblemContent; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.web.reactive.function.client.WebClient; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.groups.Tuple.tuple; - -@ActiveProfiles("test") -class ProblemContentAdapterTest { - - private ProblemContentAdapter problemContentAdapter; - - private MockWebServer mockWebServer; - - @BeforeEach - void setUp() throws IOException { - ObjectMapper objectMapper = new ObjectMapper(); - mockWebServer = new MockWebServer(); - mockWebServer.start(); - String mockServerUrl = mockWebServer.url("/") - .toString(); - - WebClient webClient = WebClient.builder() - .baseUrl(mockServerUrl) - .build(); - - problemContentAdapter = new ProblemContentAdapter(webClient, objectMapper); - } - - @AfterEach - void tearDown() throws IOException { - mockWebServer.shutdown(); - } - - @DisplayName("문제 번호 리스트를 받아서 해당 문제 번호의 문제 정보를 반환한다.") - @Test - void getProblemContents() { - // given - List list = List.of(1000L, 1001L); - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .setBody(""" - [ - { - "baekjoonProblemId": 1000, - "title": "A+B", - }, - { - "baekjoonProblemId": 1001, - "title": "A-B", - } - ]""") - .addHeader("Content-Type", "application/json")); - - - // when - final Map result = problemContentAdapter.getProblemContents(list); - - // then - assertThat(result.values()).hasSize(2) - .extracting("baekjoonProblemId", "title") - .containsExactlyInAnyOrder( - tuple(1000L, "A+B"), - tuple(1001L, "A-B") - ); - - - } - - @DisplayName("존재하지 않는 문제 번호를 포함하여 요청하면 해당 문제 번호를 제외하고 반환한다.") - @Test - void getProblemContentsContainsInvalidBaekjoonProblemId() { - // given - List list = List.of(1000L, 1001L, 999L); - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .setBody(""" - [ - { - "baekjoonProblemId": 1000, - "title": "A+B", - }, - { - "baekjoonProblemId": 1001, - "title": "A-B", - }, - { - "error" : "problem/999.json not exist" - } - ]""") - .addHeader("Content-Type", "application/json")); - - // when - final Map result = problemContentAdapter.getProblemContents(list); - - // then - assertThat(result.values()).hasSize(2) - .extracting("baekjoonProblemId", "title") - .containsExactlyInAnyOrder( - tuple(1000L, "A+B"), - tuple(1001L, "A-B") - ); - - } - - @DisplayName("10개 이상의 문제 번호를 요청하면 예외가 발생한다.") - @Test - void getProblemContentsContainsMoreThan10BaekjoonProblemId() { - // given - List list = List.of(1000L, 1001L, 1002L, 1003L, 1004L, 1005L, 1006L, 1007L, 1008L, 1009L, 1010L); - - // when & then - assertThatThrownBy(() -> problemContentAdapter.getProblemContents(list)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("문제 번호는 10개 이하로 요청해주세요."); - - } -} \ No newline at end of file diff --git a/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java b/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java index 24506319..fd31f7b4 100644 --- a/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java +++ b/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problemcontent/ProblemContentAdapterTest.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; -import kr.co.morandi.backend.problem_information.infrastructure.adapter.problemcontent.ProblemContentAdapter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; From d77563027996cfb2742eef01604cadc95e429c3b Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 22 Apr 2024 18:59:35 +0900 Subject: [PATCH 29/44] :fire: Remove unused import --- .../session/DailyDefenseManagementService.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java index 1199cb68..40fee845 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java @@ -1,20 +1,19 @@ package kr.co.morandi.backend.defense_management.application.service.session; import kr.co.morandi.backend.defense_information.application.port.out.dailydefense.DailyDefensePort; -import kr.co.morandi.backend.defense_management.application.mapper.session.StartDailyDefenseMapper; -import kr.co.morandi.backend.problem_information.application.port.out.problemcontent.ProblemContentPort; -import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; -import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; -import kr.co.morandi.backend.defense_management.infrastructure.adapter.problemcontent.ProblemContentAdapter; -import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.service.defense.ProblemGenerationService; +import kr.co.morandi.backend.defense_management.application.mapper.session.StartDailyDefenseMapper; +import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; import kr.co.morandi.backend.defense_management.application.request.session.StartDailyDefenseServiceRequest; import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; +import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import kr.co.morandi.backend.member_management.domain.model.member.Member; +import kr.co.morandi.backend.problem_information.application.port.out.problemcontent.ProblemContentPort; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; -import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; From 1091e77bd8e8c68c24bcc19a29b2619222cf5477 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 22 Apr 2024 20:02:04 +0900 Subject: [PATCH 30/44] =?UTF-8?q?:sparkles:=20HandlerMethodArgumentResolve?= =?UTF-8?q?r=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20controller=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +++ .../backend/common/config/WebMvcConfig.java | 24 +++++++++++++++ .../morandi/backend/common/web/MemberId.java | 11 +++++++ .../MemberHandlerMethodArgumentResolver.java | 22 ++++++++++++++ .../port/in/DailyDefenseUseCase.java | 2 +- .../service/DailyDefenseUseCaseImpl.java | 7 +++-- .../controller/DailyDefenseController.java | 11 +++---- .../defenseproblem/DefenseProblemMapper.java | 2 +- .../DailyDefenseManagementService.java | 5 +++- .../domain/model/session/SessionDetail.java | 1 + .../DefenseMangementController.java | 29 +++++++++++++++++++ .../port/out/dailyrecord/DailyRecordPort.java | 3 +- .../service/DailyRecordRankUseCaseImpl.java | 5 ++-- .../dailydefense/DailyRecordAdapter.java | 3 +- .../DailyRecordRepository.java | 3 +- .../security/utils/IgnoredURIManager.java | 3 +- .../config/security/utils/SecurityUtils.java | 3 ++ .../service/DailyDefenseUseCaseImplTest.java | 2 +- .../DailyDefenseControllerTest.java | 1 - .../DailyDefenseManagementServiceTest.java | 14 ++++----- .../DailyRecordRepositoryTest.java | 3 +- 21 files changed, 130 insertions(+), 28 deletions(-) create mode 100644 src/main/java/kr/co/morandi/backend/common/config/WebMvcConfig.java create mode 100644 src/main/java/kr/co/morandi/backend/common/web/MemberId.java create mode 100644 src/main/java/kr/co/morandi/backend/common/web/resolver/MemberHandlerMethodArgumentResolver.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/infrastructure/controller/DefenseMangementController.java diff --git a/build.gradle b/build.gradle index 2f24d46f..be7d3d2c 100644 --- a/build.gradle +++ b/build.gradle @@ -83,6 +83,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' + // validation + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // WebFlux (WebClient) implementation 'org.springframework.boot:spring-boot-starter-webflux' diff --git a/src/main/java/kr/co/morandi/backend/common/config/WebMvcConfig.java b/src/main/java/kr/co/morandi/backend/common/config/WebMvcConfig.java new file mode 100644 index 00000000..3c5f8756 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/common/config/WebMvcConfig.java @@ -0,0 +1,24 @@ +package kr.co.morandi.backend.common.config; + +import kr.co.morandi.backend.common.web.resolver.MemberHandlerMethodArgumentResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Bean + MemberHandlerMethodArgumentResolver memberHandlerMethodArgumentResolver() { + return new MemberHandlerMethodArgumentResolver(); + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(memberHandlerMethodArgumentResolver()); + } + +} diff --git a/src/main/java/kr/co/morandi/backend/common/web/MemberId.java b/src/main/java/kr/co/morandi/backend/common/web/MemberId.java new file mode 100644 index 00000000..c1858536 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/common/web/MemberId.java @@ -0,0 +1,11 @@ +package kr.co.morandi.backend.common.web; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface MemberId { +} diff --git a/src/main/java/kr/co/morandi/backend/common/web/resolver/MemberHandlerMethodArgumentResolver.java b/src/main/java/kr/co/morandi/backend/common/web/resolver/MemberHandlerMethodArgumentResolver.java new file mode 100644 index 00000000..c5891b03 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/common/web/resolver/MemberHandlerMethodArgumentResolver.java @@ -0,0 +1,22 @@ +package kr.co.morandi.backend.common.web.resolver; + +import kr.co.morandi.backend.common.web.MemberId; +import kr.co.morandi.backend.member_management.infrastructure.config.security.utils.SecurityUtils; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +public class MemberHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterAnnotation(MemberId.class) != null + && parameter.getParameterType().equals(Long.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + return SecurityUtils.getCurrentMemberId(); + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java b/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java index 19985187..3f767397 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java @@ -6,5 +6,5 @@ import java.time.LocalDateTime; public interface DailyDefenseUseCase { - DailyDefenseInfoResponse getDailyDefenseInfo(Member member, LocalDateTime requestDateTime); + DailyDefenseInfoResponse getDailyDefenseInfo(Long memberId, LocalDateTime requestDateTime); } diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java b/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java index c9817e61..b3350a23 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java @@ -7,6 +7,7 @@ import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.member_management.application.port.out.member.MemberPort; import kr.co.morandi.backend.member_management.domain.model.member.Member; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -22,14 +23,16 @@ @RequiredArgsConstructor public class DailyDefenseUseCaseImpl implements DailyDefenseUseCase { + private final MemberPort memberPort; private final DailyDefensePort dailyDefensePort; private final DailyRecordPort dailyRecordPort; @Override - public DailyDefenseInfoResponse getDailyDefenseInfo(Member member, LocalDateTime requestDateTime) { + public DailyDefenseInfoResponse getDailyDefenseInfo(Long memberId, LocalDateTime requestDateTime) { final DailyDefense dailyDefense = dailyDefensePort.findDailyDefense(DAILY, requestDateTime.toLocalDate()); - if(member != null) { + if(memberId != null) { + final Member member = memberPort.findMemberById(memberId); Optional maybeDailyRecord = dailyRecordPort.findDailyRecord(member, requestDateTime.toLocalDate()); if(maybeDailyRecord.isPresent()) { DailyRecord dailyRecord = maybeDailyRecord.get(); diff --git a/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseController.java b/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseController.java index 29235bfa..bd57233c 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseController.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseController.java @@ -1,16 +1,14 @@ package kr.co.morandi.backend.defense_information.infrastructure.controller; +import kr.co.morandi.backend.common.web.MemberId; import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; import kr.co.morandi.backend.defense_information.application.port.in.DailyDefenseUseCase; -import kr.co.morandi.backend.member_management.domain.model.member.Member; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; -import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; - @RestController @RequiredArgsConstructor public class DailyDefenseController { @@ -18,10 +16,9 @@ public class DailyDefenseController { private final DailyDefenseUseCase dailyDefenseUseCase; @GetMapping("/daily-defense") - public DailyDefenseInfoResponse getDailyDefenseInfo() { - //TODO SecurityContext에서 Member 정보 가져오기 - Member member = Member.create("", "", GOOGLE, "", ""); - return dailyDefenseUseCase.getDailyDefenseInfo(member, LocalDateTime.now()); + public DailyDefenseInfoResponse getDailyDefenseInfo(@MemberId Long memberId) { + + return dailyDefenseUseCase.getDailyDefenseInfo(memberId, LocalDateTime.now()); } diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java index 3664767a..8f52adf7 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/mapper/defenseproblem/DefenseProblemMapper.java @@ -40,7 +40,7 @@ public static List of(Map tryProblem, .problemNumber(problemNumber) .isCorrect(isCorrect) .lastAccessLanguage(lastAccessLanguage) - .content(problemContents.get(problemNumber)) + .content(problemContents.get(problem.getBaekjoonProblemId())) .tempCodes(tempCodeResponses) .build(); }) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java index 40fee845..e08f8cd6 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java @@ -10,6 +10,7 @@ import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.member_management.application.port.out.member.MemberPort; import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.problem_information.application.port.out.problemcontent.ProblemContentPort; import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; @@ -33,12 +34,14 @@ public class DailyDefenseManagementService { private final DailyRecordPort dailyRecordPort; private final ProblemGenerationService problemGenerationService; private final DefenseSessionPort defenseSessionPort; + private final MemberPort memberPort; private final ProblemContentPort problemContentPort; @Transactional - public StartDailyDefenseResponse startDailyDefense(StartDailyDefenseServiceRequest request, Member member, LocalDateTime requestTime) { + public StartDailyDefenseResponse startDailyDefense(StartDailyDefenseServiceRequest request, Long memberId, LocalDateTime requestTime) { Long problemNumber = request.getProblemNumber(); + Member member = memberPort.findMemberById(memberId); // 세션이랑 세션 Detail을 찾아서 응시 기록이 있는지 살펴보기 final Optional maybeDefenseSession = defenseSessionPort.findTodaysDailyDefenseSession(member, requestTime); diff --git a/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/SessionDetail.java b/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/SessionDetail.java index 6a42eadc..8c339c7e 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/SessionDetail.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/SessionDetail.java @@ -27,6 +27,7 @@ public class SessionDetail extends BaseEntity { private Long problemNumber; + @Enumerated(EnumType.STRING) private Language lastAccessLanguage; public static final Language INITIAL_LANGUAGE = Language.CPP; diff --git a/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/controller/DefenseMangementController.java b/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/controller/DefenseMangementController.java new file mode 100644 index 00000000..4f3a4612 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/controller/DefenseMangementController.java @@ -0,0 +1,29 @@ +package kr.co.morandi.backend.defense_management.infrastructure.controller; + +import jakarta.validation.Valid; +import kr.co.morandi.backend.common.web.MemberId; +import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; +import kr.co.morandi.backend.defense_management.application.service.session.DailyDefenseManagementService; +import kr.co.morandi.backend.defense_management.infrastructure.request.dailydefense.StartDailyDefenseRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; + +@RestController +@RequestMapping("/daily-defense") +@RequiredArgsConstructor +public class DefenseMangementController { + + private final DailyDefenseManagementService dailyDefenseManagementService; + + @PostMapping + public ResponseEntity startDailyDefense(@MemberId Long memberId, + @Valid @RequestBody StartDailyDefenseRequest request) { + + return ResponseEntity.ok(dailyDefenseManagementService + .startDailyDefense(request.toServiceRequest(), memberId, LocalDateTime.now()) + ); + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/dailyrecord/DailyRecordPort.java b/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/dailyrecord/DailyRecordPort.java index 48b88c77..ad590e16 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/dailyrecord/DailyRecordPort.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/dailyrecord/DailyRecordPort.java @@ -2,6 +2,7 @@ import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import org.springframework.data.domain.Page; import java.time.LocalDate; import java.util.List; @@ -12,5 +13,5 @@ public interface DailyRecordPort { DailyRecord saveDailyRecord(DailyRecord dailyRecord); Optional findDailyRecord(Member member, LocalDate date); Optional findDailyRecord(Member member, Long recordId, LocalDate date); - List findDailyRecordRank(LocalDate requestDate, Integer page, Integer size); + Page findDailyRecordRank(LocalDate requestDate, Integer page, Integer size); } diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/service/DailyRecordRankUseCaseImpl.java b/src/main/java/kr/co/morandi/backend/defense_record/application/service/DailyRecordRankUseCaseImpl.java index 56869157..dfd27164 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/application/service/DailyRecordRankUseCaseImpl.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/service/DailyRecordRankUseCaseImpl.java @@ -8,6 +8,7 @@ import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyDetail; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,7 +27,7 @@ public class DailyRecordRankUseCaseImpl implements DailyRecordRankUseCase { // TODO 공통 등수 로직 부분 빠짐 @Override public DailyDefenseRankPageResponse getDailyRecordRank(LocalDateTime requestTime, int page, int size) { - final List dailyRecords = dailyRecordPort.findDailyRecordRank(requestTime.toLocalDate(), page, size); + final Page dailyRecords = dailyRecordPort.findDailyRecordRank(requestTime.toLocalDate(), page, size); // 등수 계산 // TODO 동점자 처리 필요 @@ -48,7 +49,7 @@ public DailyDefenseRankPageResponse getDailyRecordRank(LocalDateTime requestTime }) .toList(); - return DailyDefenseRankPageResponse.of(dailyRecordRanks, page, size); + return DailyDefenseRankPageResponse.of(dailyRecordRanks, dailyRecords.getTotalPages(), page); } diff --git a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense/DailyRecordAdapter.java b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense/DailyRecordAdapter.java index ec78ecd1..fa3d0455 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense/DailyRecordAdapter.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense/DailyRecordAdapter.java @@ -5,6 +5,7 @@ import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record.DailyRecordRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; @@ -36,7 +37,7 @@ public Optional findDailyRecord(Member member, Long recordId, Local * 조회 시간 별 DailyDefense 등수 조회 * */ @Override - public List findDailyRecordRank(LocalDate requestDate, Integer page, Integer size) { + public Page findDailyRecordRank(LocalDate requestDate, Integer page, Integer size) { Pageable pageable = PageRequest.of(page, size); return dailyRecordRepository.getDailyRecordsRankByDate(requestDate, pageable); } diff --git a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepository.java b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepository.java index 9e3a2264..505e0f5e 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepository.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepository.java @@ -2,6 +2,7 @@ import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -41,5 +42,5 @@ and CAST(dr.testDate as localdate) = :date where CAST(dr.testDate as localdate) = :requestDate order by dr.solvedCount desc, dr.totalSolvedTime asc, dr.recordId asc """) - List getDailyRecordsRankByDate(LocalDate requestDate, Pageable pageable); + Page getDailyRecordsRankByDate(LocalDate requestDate, Pageable pageable); } diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/IgnoredURIManager.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/IgnoredURIManager.java index 1d932ec1..b2fd8bf5 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/IgnoredURIManager.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/IgnoredURIManager.java @@ -11,7 +11,8 @@ public class IgnoredURIManager { "/oauths/", "/swagger-ui/", "/v3/api-docs/", - "/swagger-resources/" + "/swagger-resources/", + "/daily-record/rankings" }; private String PATTERN_STRING = String.join("|", IGNORED_URIS); public Pattern PATTERN = Pattern.compile(PATTERN_STRING); diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/SecurityUtils.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/SecurityUtils.java index b28f5dc5..1b7822d4 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/SecurityUtils.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/SecurityUtils.java @@ -6,6 +6,9 @@ public class SecurityUtils { public static Long getCurrentMemberId() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if(authentication == null || authentication.getName().equals("anonymousUser")) { + return null; + } return Long.valueOf(authentication.getName()); } } diff --git a/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java b/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java index 41717aaf..91d0b52b 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java @@ -96,7 +96,7 @@ void getDailyDefenseInfoWithMemberAndRecord() { dailyRecordPort.saveDailyRecord(dailyRecord); // when - final DailyDefenseInfoResponse response = dailyDefenseUseCase.getDailyDefenseInfo(member, requestTime); + final DailyDefenseInfoResponse response = dailyDefenseUseCase.getDailyDefenseInfo(member.getMemberId(), requestTime); // then assertAll( diff --git a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java index 79bb513f..501d81ea 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java @@ -42,7 +42,6 @@ class DailyDefenseControllerTest { @DisplayName("DailyDefense 정보를 로그인하지 않은 상태에서 가져올 수 있다.") @Test -// @WithMockUser void getDailyDefenseInfo() throws Exception { // given when(dailyDefenseUseCase.getDailyDefenseInfo(any(), any())) diff --git a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java index c69a0b63..82845969 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java @@ -124,7 +124,7 @@ void retryDailyDefenseWhenDayPassed() { StartDailyDefenseServiceRequest request = StartDailyDefenseServiceRequest.builder() .problemNumber(1L) .build(); - dailyDefenseManagementService.startDailyDefense(request, member, requestTime); + dailyDefenseManagementService.startDailyDefense(request, member.getMemberId(), requestTime); StartDailyDefenseServiceRequest retryRequest = StartDailyDefenseServiceRequest.builder() .problemNumber(2L) @@ -133,7 +133,7 @@ void retryDailyDefenseWhenDayPassed() { LocalDateTime retryRequestTime = LocalDateTime.of(2021, 10, 2, 12, 0, 0); // when - final StartDailyDefenseResponse response = dailyDefenseManagementService.startDailyDefense(retryRequest, member, retryRequestTime); + final StartDailyDefenseResponse response = dailyDefenseManagementService.startDailyDefense(retryRequest, member.getMemberId(), retryRequestTime); // then @@ -163,7 +163,7 @@ void retryDailyDefenseWithOtherProblem() { StartDailyDefenseServiceRequest request = StartDailyDefenseServiceRequest.builder() .problemNumber(1L) .build(); - dailyDefenseManagementService.startDailyDefense(request, member, requestTime); + dailyDefenseManagementService.startDailyDefense(request, member.getMemberId(), requestTime); StartDailyDefenseServiceRequest retryRequest = StartDailyDefenseServiceRequest.builder() .problemNumber(2L) @@ -171,7 +171,7 @@ void retryDailyDefenseWithOtherProblem() { LocalDateTime retryRequestTime = LocalDateTime.of(2021, 10, 1, 12, 0, 0); // when - final StartDailyDefenseResponse response = dailyDefenseManagementService.startDailyDefense(retryRequest, member, retryRequestTime); + final StartDailyDefenseResponse response = dailyDefenseManagementService.startDailyDefense(retryRequest, member.getMemberId(), retryRequestTime); // then assertAll( @@ -200,12 +200,12 @@ void retryDailyDefense() { StartDailyDefenseServiceRequest request = StartDailyDefenseServiceRequest.builder() .problemNumber(2L) .build(); - dailyDefenseManagementService.startDailyDefense(request, member, requestTime); + dailyDefenseManagementService.startDailyDefense(request, member.getMemberId(), requestTime); LocalDateTime retryRequestTime = LocalDateTime.of(2021, 10, 1, 12, 0, 0); // when - final StartDailyDefenseResponse response = dailyDefenseManagementService.startDailyDefense(request, member, retryRequestTime); + final StartDailyDefenseResponse response = dailyDefenseManagementService.startDailyDefense(request, member.getMemberId(), retryRequestTime); // then assertAll( @@ -235,7 +235,7 @@ void startDailyDefense() { .build(); // when - final StartDailyDefenseResponse response = dailyDefenseManagementService.startDailyDefense(request, member, requestTime); + final StartDailyDefenseResponse response = dailyDefenseManagementService.startDailyDefense(request, member.getMemberId(), requestTime); // then diff --git a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepositoryTest.java b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepositoryTest.java index 0e72077d..0645fde1 100644 --- a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepositoryTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepositoryTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.test.context.ActiveProfiles; @@ -83,7 +84,7 @@ void getDailyRecordsRankByDate() { // when Pageable pageable = PageRequest.of(0, 5); - List dailyRecords = dailyRecordRepository.getDailyRecordsRankByDate(today, pageable); + Page dailyRecords = dailyRecordRepository.getDailyRecordsRankByDate(today, pageable); // then assertThat(dailyRecords).hasSize(3) From 2202b2b77312cb3d079761be4b64a5ae0cdcf51b Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 22 Apr 2024 20:46:30 +0900 Subject: [PATCH 31/44] =?UTF-8?q?:sparkles:=20SetAuthentication=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/AuthenticationProvider.java | 13 +++++--- .../config/security/utils/SecurityUtils.java | 32 +++++++++++++++++-- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/AuthenticationProvider.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/AuthenticationProvider.java index 7a54b97c..7ca291b5 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/AuthenticationProvider.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/AuthenticationProvider.java @@ -9,26 +9,31 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; +import java.util.List; + @Component @RequiredArgsConstructor @Slf4j public class AuthenticationProvider { - private final OAuthUserDetailsService oAuthUserDetailsService; - private final SecretKeyProvider secretKeyProvider; + public void setAuthentication(String accessToken) { Authentication authentication = getAuthentication(accessToken); SecurityContextHolder.getContext().setAuthentication(authentication); } private Authentication getAuthentication(String accessToken) { Long memberId = getMemberIdFromToken(accessToken); - UserDetails userDetails = oAuthUserDetailsService.loadUserByUsername(memberId.toString()); - return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + //TODO : authorities를 이후에 저장해야함 + List authorities = null;//getAuthoritiesFromToken(accessToken); + + return new UsernamePasswordAuthenticationToken(memberId, null, authorities); } private Long getMemberIdFromToken(String token) { Jws claimsJws = Jwts.parserBuilder().setSigningKey(secretKeyProvider.getPublicKey()) diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/SecurityUtils.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/SecurityUtils.java index 1b7822d4..8bc4e1c2 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/SecurityUtils.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/utils/SecurityUtils.java @@ -1,14 +1,42 @@ package kr.co.morandi.backend.member_management.infrastructure.config.security.utils; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; public class SecurityUtils { public static Long getCurrentMemberId() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if(authentication == null || authentication.getName().equals("anonymousUser")) { + + if (authentication == null || !authentication.isAuthenticated() || + authentication instanceof AnonymousAuthenticationToken) { return null; } - return Long.valueOf(authentication.getName()); + + Object principal = authentication.getPrincipal(); + + /* + * 일반적인 JWT 토큰을 사용할 때 발생. + * */ + if (principal instanceof Long) { + return (Long) principal; + } + /* + * @WithMockUser를 포함한 테스트나 기타 UserDetails 서비스를 사용할 때 발생한다. + * */ + if (principal instanceof UserDetails) { + /* + * principal이 UserDetails 타입인 경우, getUsername()에서 사용자 ID를 추출 + * 따라서 @WithMockUser를 사용할 때는 getUsername에 Member ID를 넣어줘야 한다. + */ + UserDetails userDetails = (UserDetails) principal; + try { + return Long.valueOf(userDetails.getUsername()); + } catch (NumberFormatException e) { + return null; + } + } + return null; } } From 515a758db59a9c52bbbd2e3c5eb698fd81d412b8 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 22 Apr 2024 20:51:49 +0900 Subject: [PATCH 32/44] =?UTF-8?q?:art:=20GetDailydefenseInfo=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DailyDefenseUseCaseImpl.java | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java b/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java index b3350a23..5225abe4 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImpl.java @@ -30,16 +30,28 @@ public class DailyDefenseUseCaseImpl implements DailyDefenseUseCase { @Override public DailyDefenseInfoResponse getDailyDefenseInfo(Long memberId, LocalDateTime requestDateTime) { final DailyDefense dailyDefense = dailyDefensePort.findDailyDefense(DAILY, requestDateTime.toLocalDate()); - - if(memberId != null) { - final Member member = memberPort.findMemberById(memberId); - Optional maybeDailyRecord = dailyRecordPort.findDailyRecord(member, requestDateTime.toLocalDate()); - if(maybeDailyRecord.isPresent()) { - DailyRecord dailyRecord = maybeDailyRecord.get(); - return DailyDefenseInfoMapper.ofAttempted(dailyDefense, dailyRecord); - } + /* + * 비로그인 상태인 경우 + * */ + if(memberId == null) { + return DailyDefenseInfoMapper.fromNonAttempted(dailyDefense); } - + /* + * 로그인 상태인 경우 + * */ + final Member member = memberPort.findMemberById(memberId); + Optional maybeDailyRecord = dailyRecordPort.findDailyRecord(member, requestDateTime.toLocalDate()); + + /* + * 시험 기록이 존재하는 경우 + * */ + if(maybeDailyRecord.isPresent()) { + DailyRecord dailyRecord = maybeDailyRecord.get(); + return DailyDefenseInfoMapper.ofAttempted(dailyDefense, dailyRecord); + } + /* + * 시험 응시 기록이 없는 경우 + * */ return DailyDefenseInfoMapper.fromNonAttempted(dailyDefense); } } From 4e6852beefc79867f482f771cd255ed4d8fe6ae3 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Tue, 23 Apr 2024 20:21:54 +0900 Subject: [PATCH 33/44] =?UTF-8?q?:art:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=EA=B4=80=EA=B3=84=EC=97=86=EB=8A=94=20API?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20filter=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/GlobalExceptionHandler.java | 1 - .../config/jwt/utils/JwtProvider.java | 5 +- .../config/security/SecurityConfig.java | 21 ++++--- .../exception/OAuthErrorCode.java | 2 +- .../JwtAuthenticationEntryPoint.java | 60 +++++++++++++++++++ .../oauth/CachedBodyHttpServletWrapper.java | 2 +- .../filter/oauth/JwtAuthenticationFilter.java | 50 +++++++++------- .../filter/oauth/JwtExceptionFilter.java | 19 +++--- .../filter/oauth/RequestCachingFilter.java | 2 +- .../controller/DailyRecordControllerTest.java | 19 ------ 10 files changed, 114 insertions(+), 67 deletions(-) create mode 100644 src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/entrypoint/JwtAuthenticationEntryPoint.java rename src/main/java/kr/co/morandi/backend/member_management/infrastructure/{ => security}/filter/oauth/CachedBodyHttpServletWrapper.java (95%) rename src/main/java/kr/co/morandi/backend/member_management/infrastructure/{ => security}/filter/oauth/JwtAuthenticationFilter.java (58%) rename src/main/java/kr/co/morandi/backend/member_management/infrastructure/{ => security}/filter/oauth/JwtExceptionFilter.java (85%) rename src/main/java/kr/co/morandi/backend/member_management/infrastructure/{ => security}/filter/oauth/RequestCachingFilter.java (89%) diff --git a/src/main/java/kr/co/morandi/backend/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/kr/co/morandi/backend/common/exception/handler/GlobalExceptionHandler.java index 3efd9cfc..6bb805fa 100644 --- a/src/main/java/kr/co/morandi/backend/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/kr/co/morandi/backend/common/exception/handler/GlobalExceptionHandler.java @@ -13,7 +13,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import java.net.URI; diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtProvider.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtProvider.java index affc3525..f6f0d252 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtProvider.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtProvider.java @@ -38,13 +38,12 @@ public String parseAccessToken(HttpServletRequest request) { if (StringUtils.hasText(accessToken) && accessToken.startsWith("Bearer ")) { return accessToken.substring(7); } - throw new MorandiException(OAuthErrorCode.ACCESS_TOKEN_NOT_FOUND); + return null; } public String parseRefreshToken(HttpServletRequest request) { Cookie cookie = WebUtils.getCookie(request, "REFRESH_TOKEN"); if(cookie==null) - throw new MorandiException(OAuthErrorCode.REFRESH_TOKEN_NOT_FOUND); - + return null; return cookie.getValue(); } public String reissueAccessToken(String refreshToken) { diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java index 8581d0a7..95f2df9d 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java @@ -1,22 +1,23 @@ package kr.co.morandi.backend.member_management.infrastructure.config.security; -import kr.co.morandi.backend.member_management.infrastructure.filter.oauth.JwtAuthenticationFilter; -import kr.co.morandi.backend.member_management.infrastructure.filter.oauth.JwtExceptionFilter; -import kr.co.morandi.backend.member_management.infrastructure.filter.oauth.RequestCachingFilter; +import kr.co.morandi.backend.member_management.infrastructure.security.filter.entrypoint.JwtAuthenticationEntryPoint; +import kr.co.morandi.backend.member_management.infrastructure.security.filter.oauth.JwtAuthenticationFilter; +import kr.co.morandi.backend.member_management.infrastructure.security.filter.oauth.JwtExceptionFilter; +import kr.co.morandi.backend.member_management.infrastructure.security.filter.oauth.RequestCachingFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfigurationSource; +import static org.springframework.http.HttpMethod.GET; + @EnableWebSecurity @Configuration @RequiredArgsConstructor @@ -26,6 +27,7 @@ public class SecurityConfig { private final JwtExceptionFilter jwtExceptionFilter; private final RequestCachingFilter requestCachingFilter; private final CorsConfigurationSource corsConfigurationSource; + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{ http @@ -36,10 +38,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/oauths/**","/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**").permitAll() .requestMatchers("/daily-record/rankings/**").permitAll() - .requestMatchers(HttpMethod.GET, "/daily-defense/**").permitAll() + .requestMatchers(GET, "/daily-defense/**").permitAll() .anyRequest().authenticated()) + .exceptionHandling((exceptionHandling) -> exceptionHandling + .authenticationEntryPoint(jwtAuthenticationEntryPoint) + ) .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class) .addFilterBefore(requestCachingFilter, JwtExceptionFilter.class); diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/exception/OAuthErrorCode.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/exception/OAuthErrorCode.java index c4ddb706..5e09e8a5 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/exception/OAuthErrorCode.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/exception/OAuthErrorCode.java @@ -11,7 +11,7 @@ public enum OAuthErrorCode implements ErrorCode { MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST,"사용자를 찾을 수 없습니다"), - EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED,"인증 시간이 만료된 토큰입니다."), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED,"인증 시간이 만료된 토큰입니다. 다시 로그인하세요"), INVALID_TOKEN(HttpStatus.UNAUTHORIZED,"유효하지 않은 토큰입니다."), TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "액세스 토큰을 찾을 수 없습니다"), ACCESS_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "액세스 토큰을 찾을 수 없습니다."), diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/entrypoint/JwtAuthenticationEntryPoint.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/entrypoint/JwtAuthenticationEntryPoint.java new file mode 100644 index 00000000..767cdcfa --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/entrypoint/JwtAuthenticationEntryPoint.java @@ -0,0 +1,60 @@ +package kr.co.morandi.backend.member_management.infrastructure.security.filter.entrypoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import kr.co.morandi.backend.common.exception.response.ErrorResponse; +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.JwtProvider; +import kr.co.morandi.backend.member_management.infrastructure.exception.OAuthErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + private final JwtProvider jwtProvider; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + /* + * AccessToken이 존재하지 않는 경우 + * */ + if(jwtProvider.parseAccessToken(request) == null) { + response.setStatus(OAuthErrorCode.ACCESS_TOKEN_NOT_FOUND.getHttpStatus().value()); + response.getWriter().write(objectMapper.writeValueAsString(ErrorResponse.of(OAuthErrorCode.ACCESS_TOKEN_NOT_FOUND))); + response.getWriter().flush(); + + return; + } + /* + * RefreshToken이 존재하지 않는 경우 + * */ + if(jwtProvider.parseRefreshToken(request) == null) { + response.setStatus(OAuthErrorCode.REFRESH_TOKEN_NOT_FOUND.getHttpStatus().value()); + response.setContentType("application/json"); + response.getWriter().write(objectMapper.writeValueAsString(ErrorResponse.of(OAuthErrorCode.REFRESH_TOKEN_NOT_FOUND))); + response.getWriter().flush(); + + return; + } + + /* + * RefreshToken이 만료된 경우 + * */ + response.setStatus(OAuthErrorCode.EXPIRED_TOKEN.getHttpStatus().value()); + response.getWriter().write(objectMapper.writeValueAsString(ErrorResponse.of(OAuthErrorCode.EXPIRED_TOKEN))); + + + } +} diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/CachedBodyHttpServletWrapper.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/CachedBodyHttpServletWrapper.java similarity index 95% rename from src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/CachedBodyHttpServletWrapper.java rename to src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/CachedBodyHttpServletWrapper.java index a675a9be..e38676e2 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/CachedBodyHttpServletWrapper.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/CachedBodyHttpServletWrapper.java @@ -1,4 +1,4 @@ -package kr.co.morandi.backend.member_management.infrastructure.filter.oauth; +package kr.co.morandi.backend.member_management.infrastructure.security.filter.oauth; import jakarta.servlet.ReadListener; import jakarta.servlet.ServletInputStream; diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtAuthenticationFilter.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/JwtAuthenticationFilter.java similarity index 58% rename from src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtAuthenticationFilter.java rename to src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/JwtAuthenticationFilter.java index 11a0e398..dd078f53 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtAuthenticationFilter.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/JwtAuthenticationFilter.java @@ -1,18 +1,19 @@ -package kr.co.morandi.backend.member_management.infrastructure.filter.oauth; +package kr.co.morandi.backend.member_management.infrastructure.security.filter.oauth; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import kr.co.morandi.backend.common.exception.MorandiException; -import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.JwtValidator; -import kr.co.morandi.backend.member_management.infrastructure.exception.OAuthErrorCode; import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.JwtProvider; +import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.JwtValidator; import kr.co.morandi.backend.member_management.infrastructure.config.security.utils.AuthenticationProvider; import kr.co.morandi.backend.member_management.infrastructure.config.security.utils.IgnoredURIManager; +import kr.co.morandi.backend.member_management.infrastructure.exception.OAuthErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; + import java.io.IOException; @Component @@ -43,32 +44,35 @@ protected void doFilterInternal(HttpServletRequest request, String accessToken = jwtProvider.parseAccessToken(request); String refreshToken = jwtProvider.parseRefreshToken(request); - /* - * accessToken이 유효한 경우, accessToken을 이용하여 인증을 수행하고 다음 필터로 넘어간다. - * */ - if (jwtValidator.validateToken(accessToken)) { // accessToken이 유효할 경우 - authenticationProvider.setAuthentication(accessToken); + if (accessToken != null && refreshToken != null) { + /* + * accessToken이 유효한 경우, accessToken을 이용하여 인증을 수행하고 다음 필터로 넘어간다. + * */ + if (jwtValidator.validateToken(accessToken)) { + authenticationProvider.setAuthentication(accessToken); - filterChain.doFilter(request, response); - return ; - } - - /* - * accessToken의 유효 기간이 만료된 경우, refreshToken을 이용하여 accessToken을 재발급하고 다음 필터로 넘어간다. - * */ - if (jwtValidator.validateToken(refreshToken)) { // refreshToken이 유효할 경우 - accessToken = jwtProvider.reissueAccessToken(refreshToken); - response.setHeader("Authorization", "Bearer " + accessToken); + filterChain.doFilter(request, response); + return; + } + /* + * accessToken의 유효 기간이 만료된 경우, refreshToken을 이용하여 accessToken을 재발급하고 다음 필터로 넘어간다. + * */ + else if (jwtValidator.validateToken(refreshToken)) { + // refreshToken이 유효할 경우 + accessToken = jwtProvider.reissueAccessToken(refreshToken); + response.setHeader("Authorization", "Bearer " + accessToken); - authenticationProvider.setAuthentication(accessToken); - filterChain.doFilter(request, response); - return ; + authenticationProvider.setAuthentication(accessToken); + filterChain.doFilter(request, response); + return; + } } /* - * refreshToken의 유효 기간도 만료된 경우, refreshToken이 만료되었다는 오류를 반환한다. + * accessToken이나 refreshToken이 없는 경우 다음 필터로 넘어가고 + * entryPoint에서 authentication되지 않은 요청에 대한 응답을 처리한다. * */ - throw new MorandiException(OAuthErrorCode.EXPIRED_TOKEN); + filterChain.doFilter(request, response); } } diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtExceptionFilter.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/JwtExceptionFilter.java similarity index 85% rename from src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtExceptionFilter.java rename to src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/JwtExceptionFilter.java index cdda741a..3cc7de28 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/JwtExceptionFilter.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/JwtExceptionFilter.java @@ -1,4 +1,4 @@ -package kr.co.morandi.backend.member_management.infrastructure.filter.oauth; +package kr.co.morandi.backend.member_management.infrastructure.security.filter.oauth; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; @@ -31,16 +31,15 @@ public class JwtExceptionFilter extends OncePerRequestFilter { private final CookieUtils cookieUtils; - @Value("${oauth2.signup-url}") - private String signupPath; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, FilterChain filterChain) throws IOException { + /* + * 다음 필터인 JwtAuthenticationFilter에서 발생한 예외를 처리하기 위해 필터를 실행한다. + * */ try { - /* - * 다음 필터인 JwtAuthenticationFilter에서 발생한 예외를 처리하기 위해 필터를 실행한다. - * */ filterChain.doFilter(request, response); } catch (MorandiException e) { /* @@ -49,7 +48,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (isAuthError(e)) { Cookie cookie = cookieUtils.removeCookie(REFRESH_TOKEN,null); response.addCookie(cookie); - response.sendRedirect(signupPath); } /* @@ -64,7 +62,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } private boolean isAuthError(MorandiException e) { - return e.getErrorCode().getHttpStatus() == (HttpStatus.UNAUTHORIZED); + return e.getErrorCode().getHttpStatus() == (HttpStatus.UNAUTHORIZED) || e.getErrorCode().getHttpStatus() == HttpStatus.FORBIDDEN; } private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) { response.setStatus(errorCode.getHttpStatus().value()); @@ -75,7 +73,8 @@ private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) try { response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); - } catch (IOException e){ + response.getWriter().flush(); + } catch (IOException e) { log.error("IOException occurred while writing error response", e); } } diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/RequestCachingFilter.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/RequestCachingFilter.java similarity index 89% rename from src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/RequestCachingFilter.java rename to src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/RequestCachingFilter.java index cb6a4063..8efd9bf6 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/filter/oauth/RequestCachingFilter.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/RequestCachingFilter.java @@ -1,4 +1,4 @@ -package kr.co.morandi.backend.member_management.infrastructure.filter.oauth; +package kr.co.morandi.backend.member_management.infrastructure.security.filter.oauth; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java index c7414e93..899973ff 100644 --- a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java @@ -3,36 +3,17 @@ import kr.co.morandi.backend.defense_record.application.dto.DailyDefenseRankPageResponse; import kr.co.morandi.backend.defense_record.application.port.in.DailyRecordRankUseCase; import kr.co.morandi.backend.member_management.infrastructure.config.cookie.utils.CookieUtils; -import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.JwtProvider; -import kr.co.morandi.backend.member_management.infrastructure.config.security.SecurityConfig; -import kr.co.morandi.backend.member_management.infrastructure.filter.oauth.JwtAuthenticationFilter; -import kr.co.morandi.backend.member_management.infrastructure.filter.oauth.JwtExceptionFilter; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; -import org.springframework.context.annotation.Import; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.context.SecurityContextHolderStrategy; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.security.web.DefaultSecurityFilterChain; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.AuthenticationFilter; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import org.springframework.web.filter.GenericFilterBean; import org.springframework.web.filter.OncePerRequestFilter; import java.util.List; From bea3db7cb18e3067f5e72593ea8f414bd3d923e6 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Wed, 24 Apr 2024 18:48:44 +0900 Subject: [PATCH 34/44] =?UTF-8?q?:sparkles:=20=EC=A0=9C=ED=95=9C=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=ED=9B=84=20=EC=8B=9C=ED=97=98=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EC=A2=85=EB=A3=8C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/MorandiException.java | 3 + .../handler/GlobalExceptionHandler.java | 11 +- .../port/out/session/DefenseSessionPort.java | 1 + .../DailyDefenseManagementService.java | 190 ++++++++-------- .../service/timer/DefenseTimerService.java | 42 ++++ .../domain/error/SessionErrorCode.java | 26 +++ .../domain/model/session/DefenseSession.java | 12 + .../domain/service/SessionService.java | 38 ++++ .../session/DefenseSessionAdapter.java | 5 + .../port/out/record/RecordPort.java | 10 + .../domain/error/RecordErrorCode.java | 26 +++ .../domain/model/record/Record.java | 13 ++ .../domain/model/record/RecordStatus.java | 5 + .../DailyRecordAdapter.java | 3 +- .../adapter/record/RecordAdapter.java | 25 +++ .../persistence/record/RecordRepository.java | 7 + .../model/session/DefenseSessionTest.java | 45 +++- .../domain/service/SessionServiceTest.java | 212 ++++++++++++++++++ .../dailydefense_record/DailyRecordTest.java | 36 ++- .../adapter/record/RecordAdapterTest.java | 135 +++++++++++ 20 files changed, 739 insertions(+), 106 deletions(-) create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/domain/error/SessionErrorCode.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/domain/service/SessionService.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_record/application/port/out/record/RecordPort.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_record/domain/error/RecordErrorCode.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/RecordStatus.java rename src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/{dailydefense => dailyrecord}/DailyRecordAdapter.java (97%) create mode 100644 src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/record/RecordAdapter.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/record/RecordRepository.java create mode 100644 src/test/java/kr/co/morandi/backend/defense_management/domain/service/SessionServiceTest.java create mode 100644 src/test/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/record/RecordAdapterTest.java diff --git a/src/main/java/kr/co/morandi/backend/common/exception/MorandiException.java b/src/main/java/kr/co/morandi/backend/common/exception/MorandiException.java index 66dec4f0..dc6e3c69 100644 --- a/src/main/java/kr/co/morandi/backend/common/exception/MorandiException.java +++ b/src/main/java/kr/co/morandi/backend/common/exception/MorandiException.java @@ -8,9 +8,12 @@ public class MorandiException extends RuntimeException { private final ErrorCode errorCode; public MorandiException(ErrorCode errorCode) { + super(errorCode.getMessage()); this.errorCode = errorCode; } + public MorandiException(ErrorCode errorCode, String message) { + super(message); this.errorCode = errorCode; } } diff --git a/src/main/java/kr/co/morandi/backend/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/kr/co/morandi/backend/common/exception/handler/GlobalExceptionHandler.java index 6bb805fa..dec59955 100644 --- a/src/main/java/kr/co/morandi/backend/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/kr/co/morandi/backend/common/exception/handler/GlobalExceptionHandler.java @@ -7,7 +7,6 @@ import kr.co.morandi.backend.member_management.infrastructure.config.cookie.utils.CookieUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -15,8 +14,6 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import java.net.URI; - import static kr.co.morandi.backend.common.exception.handler.exception.CommonErrorCode.INTERNAL_SERVER_ERROR; import static kr.co.morandi.backend.member_management.infrastructure.config.jwt.constants.TokenType.REFRESH_TOKEN; @@ -27,9 +24,6 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { private final CookieUtils cookieUtils; - @Value("${oauth2.signup-url}") - private String signupPath; - @ExceptionHandler(MorandiException.class) public ResponseEntity morandiExceptionHandler(MorandiException e) { log.error(e.getErrorCode().name()+" : ", e.getErrorCode().getMessage() + " : ", e); @@ -38,8 +32,7 @@ public ResponseEntity morandiExceptionHandler(MorandiException e) if (e.getErrorCode().getHttpStatus() == HttpStatus.UNAUTHORIZED) { HttpHeaders headers = createUnauthorizedHeaders(); - // 로그인 페이지로 리다이렉트 - return new ResponseEntity<>(headers, HttpStatus.FOUND); + return new ResponseEntity<>(headers, HttpStatus.UNAUTHORIZED); } // 그 외의 에러가 발생한 경우 @@ -60,11 +53,9 @@ public ResponseEntity handleAllException(Exception e) { /** * Unauthorized 에러가 발생한 경우 * Refresh Token 쿠키를 제거하고 - * 로그인 페이지로 리다이렉트 */ private HttpHeaders createUnauthorizedHeaders() { HttpHeaders headers = new HttpHeaders(); - headers.setLocation(URI.create(signupPath)); Cookie cookie = cookieUtils.removeCookie(REFRESH_TOKEN, null); headers.add("Set-Cookie", cookie.toString()); return headers; diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/session/DefenseSessionPort.java b/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/session/DefenseSessionPort.java index 949863b7..4a04b0e8 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/session/DefenseSessionPort.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/port/out/session/DefenseSessionPort.java @@ -10,4 +10,5 @@ public interface DefenseSessionPort { DefenseSession saveDefenseSession(DefenseSession defenseSession); Optional findTodaysDailyDefenseSession(Member member, LocalDateTime now); + Optional findDefenseSessionById(Long sessionId); } diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java index e08f8cd6..138dc636 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java @@ -1,99 +1,105 @@ -package kr.co.morandi.backend.defense_management.application.service.session; - -import kr.co.morandi.backend.defense_information.application.port.out.dailydefense.DailyDefensePort; -import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; -import kr.co.morandi.backend.defense_information.domain.service.defense.ProblemGenerationService; -import kr.co.morandi.backend.defense_management.application.mapper.session.StartDailyDefenseMapper; -import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; -import kr.co.morandi.backend.defense_management.application.request.session.StartDailyDefenseServiceRequest; -import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; -import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; -import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; -import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; -import kr.co.morandi.backend.member_management.application.port.out.member.MemberPort; -import kr.co.morandi.backend.member_management.domain.model.member.Member; -import kr.co.morandi.backend.problem_information.application.port.out.problemcontent.ProblemContentPort; -import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; -import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.Map; -import java.util.Optional; - -import static kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType.DAILY; - -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor -public class DailyDefenseManagementService { - - private final DailyDefensePort dailyDefensePort; - private final DailyRecordPort dailyRecordPort; - private final ProblemGenerationService problemGenerationService; - private final DefenseSessionPort defenseSessionPort; - private final MemberPort memberPort; - - private final ProblemContentPort problemContentPort; - - @Transactional - public StartDailyDefenseResponse startDailyDefense(StartDailyDefenseServiceRequest request, Long memberId, LocalDateTime requestTime) { - Long problemNumber = request.getProblemNumber(); - Member member = memberPort.findMemberById(memberId); - - // 세션이랑 세션 Detail을 찾아서 응시 기록이 있는지 살펴보기 - final Optional maybeDefenseSession = defenseSessionPort.findTodaysDailyDefenseSession(member, requestTime); - - // 오늘의 Defense를 찾아오기 - final DailyDefense dailyDefense = dailyDefensePort.findDailyDefense(DAILY, requestTime.toLocalDate()); - - // 오늘의 문제 목록 중에서 원하는 문제를 찾아서 시도하려는 문제 목록에 추가 (오늘의 문제 목록에 해당 문제가 없으면 예외 발생) - final Map tryProblem = dailyDefense.getTryingProblem(problemNumber, problemGenerationService); - - // DefenseSession이 있으면 get, 없으면 새로운 DefenseSession을 생성 - final DefenseSession defenseSession = maybeDefenseSession.orElseGet(() -> createNewSession(member, requestTime, dailyDefense, tryProblem)); - - // DefenseSession의 recordId로 DailyRecord를 찾고 문제를 시도했는지 확인하고 시도하지 않았으면 시도하도록 함 - Long recordId = defenseSession.getRecordId(); - DailyRecord dailyRecord = dailyRecordPort.findDailyRecord(member, recordId, requestTime.toLocalDate()) - .orElseThrow(() -> new IllegalArgumentException("세션에 해당하는 응시 기록이 없습니다.")); - - if (!defenseSession.hasTriedProblem(problemNumber)) { - dailyRecord.tryMoreProblem(tryProblem); - defenseSession.tryMoreProblem(problemNumber, requestTime); + package kr.co.morandi.backend.defense_management.application.service.session; + + import kr.co.morandi.backend.defense_information.application.port.out.dailydefense.DailyDefensePort; + import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; + import kr.co.morandi.backend.defense_information.domain.service.defense.ProblemGenerationService; + import kr.co.morandi.backend.defense_management.application.mapper.session.StartDailyDefenseMapper; + import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; + import kr.co.morandi.backend.defense_management.application.request.session.StartDailyDefenseServiceRequest; + import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; + import kr.co.morandi.backend.defense_management.application.service.timer.DefenseTimerService; + import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; + import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; + import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; + import kr.co.morandi.backend.member_management.application.port.out.member.MemberPort; + import kr.co.morandi.backend.member_management.domain.model.member.Member; + import kr.co.morandi.backend.problem_information.application.port.out.problemcontent.ProblemContentPort; + import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; + import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; + import org.springframework.transaction.annotation.Transactional; + + import java.time.LocalDateTime; + import java.util.Map; + import java.util.Optional; + + import static kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType.DAILY; + + @Service + @Transactional(readOnly = true) + @RequiredArgsConstructor + public class DailyDefenseManagementService { + + private final DailyDefensePort dailyDefensePort; + private final DailyRecordPort dailyRecordPort; + private final ProblemGenerationService problemGenerationService; + private final DefenseSessionPort defenseSessionPort; + private final MemberPort memberPort; + private final ProblemContentPort problemContentPort; + private final DefenseTimerService defenseTimerService; + + @Transactional + public StartDailyDefenseResponse startDailyDefense(StartDailyDefenseServiceRequest request, Long memberId, LocalDateTime requestTime) { + Long problemNumber = request.getProblemNumber(); + Member member = memberPort.findMemberById(memberId); + + // 세션이랑 세션 Detail을 찾아서 응시 기록이 있는지 살펴보기 + final Optional maybeDefenseSession = defenseSessionPort.findTodaysDailyDefenseSession(member, requestTime); + + // 오늘의 Defense를 찾아오기 + final DailyDefense dailyDefense = dailyDefensePort.findDailyDefense(DAILY, requestTime.toLocalDate()); + + // 오늘의 문제 목록 중에서 원하는 문제를 찾아서 시도하려는 문제 목록에 추가 (오늘의 문제 목록에 해당 문제가 없으면 예외 발생) + final Map tryProblem = dailyDefense.getTryingProblem(problemNumber, problemGenerationService); + + // DefenseSession이 있으면 get, 없으면 새로운 DefenseSession을 생성 + final DefenseSession defenseSession = maybeDefenseSession.orElseGet(() -> createNewSession(member, requestTime, dailyDefense, tryProblem)); + + // DefenseSession의 recordId로 DailyRecord를 찾고 문제를 시도했는지 확인하고 시도하지 않았으면 시도하도록 함 + Long recordId = defenseSession.getRecordId(); + DailyRecord dailyRecord = dailyRecordPort.findDailyRecord(member, recordId, requestTime.toLocalDate()) + .orElseThrow(() -> new IllegalArgumentException("세션에 해당하는 응시 기록이 없습니다.")); + + if (!defenseSession.hasTriedProblem(problemNumber)) { + dailyRecord.tryMoreProblem(tryProblem); + defenseSession.tryMoreProblem(problemNumber, requestTime); + } + + final DefenseSession savedDefenseSession = defenseSessionPort.saveDefenseSession(defenseSession); + + /* + * DefenseSession에 관련된 타이머 시작 + * */ + defenseTimerService.startDefenseTimer(savedDefenseSession); + + // 문제 내용 가져오기 + final Map problemContent = getProblemContents(tryProblem); + + // 문제 목록을 DefenseProblemResponse DTO로 변환 + return StartDailyDefenseMapper.of(tryProblem, dailyDefense, savedDefenseSession, dailyRecord, problemContent); } - final DefenseSession savedDefenseSession = defenseSessionPort.saveDefenseSession(defenseSession); - - // 문제 내용 가져오기 - final Map problemContent = getProblemContents(tryProblem); + /* + * 백준 문제 ID 목록을 받아서 문제 내용을 가져오는 메소드 + * */ + private Map getProblemContents(Map tryProblem) { + return problemContentPort.getProblemContents(tryProblem.values() + .stream() + .map(Problem::getBaekjoonProblemId) + .toList()); + } - // 문제 목록을 DefenseProblemResponse DTO로 변환 - return StartDailyDefenseMapper.of(tryProblem, dailyDefense, savedDefenseSession, dailyRecord, problemContent); - } + /* + * 세션이 존재하지 않을 경우 새롭게 시험을 시작하는 메소드 + * */ + private DefenseSession createNewSession(Member member, LocalDateTime now, DailyDefense dailyDefense, Map tryProblem) { + DailyRecord dailyRecord = DailyRecord.tryDefense(now, dailyDefense, member, tryProblem); + DailyRecord savedDailyRecord = dailyRecordPort.saveDailyRecord(dailyRecord); + Long recordId = savedDailyRecord.getRecordId(); - /* - * 백준 문제 ID 목록을 받아서 문제 내용을 가져오는 메소드 - * */ - private Map getProblemContents(Map tryProblem) { - return problemContentPort.getProblemContents(tryProblem.values() - .stream() - .map(Problem::getBaekjoonProblemId) - .toList()); - } + return DefenseSession.startSession(member, recordId, dailyDefense.getDefenseType(), tryProblem.keySet(), now, dailyDefense.getEndTime(now)); + } - /* - * 세션이 존재하지 않을 경우 새롭게 시험을 시작하는 메소드 - * */ - private DefenseSession createNewSession(Member member, LocalDateTime now, DailyDefense dailyDefense, Map tryProblem) { - DailyRecord dailyRecord = DailyRecord.tryDefense(now, dailyDefense, member, tryProblem); - DailyRecord savedDailyRecord = dailyRecordPort.saveDailyRecord(dailyRecord); - Long recordId = savedDailyRecord.getRecordId(); - return DefenseSession.startSession(member, recordId, dailyDefense.getDefenseType(), tryProblem.keySet(), now, dailyDefense.getEndTime(now)); } - - -} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java new file mode 100644 index 00000000..8553397c --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java @@ -0,0 +1,42 @@ +package kr.co.morandi.backend.defense_management.application.service.timer; + +import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; +import kr.co.morandi.backend.defense_management.domain.service.SessionService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.concurrent.*; + +@Service +@RequiredArgsConstructor +public class DefenseTimerService { + + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final ConcurrentHashMap> defenseSessionTimer = new ConcurrentHashMap<>(); + private final SessionService sessionService; + + public void startDefenseTimer(DefenseSession defenseSession) { + if(isAlreadySettedTimer(defenseSession)) { + return; + } + long delay = Duration.between(defenseSession.getStartDateTime(), defenseSession.getEndDateTime()).toMillis(); + + final ScheduledFuture schedule = scheduler.schedule(() -> { + sessionService.terminateDefense(defenseSession.getDefenseSessionId()); + defenseSessionTimer.remove(defenseSession.getDefenseSessionId()); + }, delay, TimeUnit.MILLISECONDS); + + defenseSessionTimer.put(defenseSession.getDefenseSessionId(), schedule); + } + + /* + * 현재는 단일 인스턴스이므로 ConcurrentHashMap을 사용하여 중복 타이머가 설정되지 않도록 하였습니다. + * + * scale-out될 경우에는 분산 환경에서 중복 타이머가 설정되지 않도록 하기 위해 Redis와 같은 분산 캐시를 사용하여 중복 타이머가 설정되지 않도록 해야합니다. + * */ + private boolean isAlreadySettedTimer(DefenseSession defenseSession) { + return defenseSessionTimer.containsKey(defenseSession.getDefenseSessionId()); + } + +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/domain/error/SessionErrorCode.java b/src/main/java/kr/co/morandi/backend/defense_management/domain/error/SessionErrorCode.java new file mode 100644 index 00000000..cdb6b248 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/domain/error/SessionErrorCode.java @@ -0,0 +1,26 @@ +package kr.co.morandi.backend.defense_management.domain.error; + +import kr.co.morandi.backend.common.exception.errorcode.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum SessionErrorCode implements ErrorCode { + SESSION_NOT_FOUND(HttpStatus.NOT_FOUND, "세션을 찾을 수 없습니다."), + SESSION_ALREADY_STARTED(HttpStatus.BAD_REQUEST, "이미 시작된 세션입니다."), + SESSION_ALREADY_ENDED(HttpStatus.BAD_REQUEST, "이미 종료된 세션입니다."),; + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + @Override + public String getMessage() { + return message; + } + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSession.java b/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSession.java index 94b069e0..924eee13 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSession.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSession.java @@ -1,10 +1,13 @@ package kr.co.morandi.backend.defense_management.domain.model.session; import jakarta.persistence.*; +import kr.co.morandi.backend.common.exception.MorandiException; import kr.co.morandi.backend.common.model.BaseEntity; import kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType; +import kr.co.morandi.backend.defense_management.domain.error.SessionErrorCode; import kr.co.morandi.backend.member_management.domain.model.member.Member; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -47,6 +50,13 @@ public class DefenseSession extends BaseEntity { private static final Long INITIAL_ACCESS_PROBLEM_NUMBER = 1L; + public boolean terminateSession() { + if(examStatus == ExamStatus.COMPLETED) { + throw new MorandiException(SessionErrorCode.SESSION_ALREADY_ENDED); + } + examStatus = ExamStatus.COMPLETED; + return true; + } public SessionDetail getSessionDetail(Long problemNumber) { return getSessionDetails().stream() .filter(detail -> Objects.equals(detail.getProblemNumber(), problemNumber)) @@ -75,6 +85,8 @@ public static DefenseSession startSession(Member member, Long recordId, DefenseT LocalDateTime startDateTime, LocalDateTime endDateTime) { return new DefenseSession(member, recordId, defenseType, problemNumbers, startDateTime, endDateTime); } + + @Builder private DefenseSession(Member member, Long recordId, DefenseType defenseType, Set problemNumbers, LocalDateTime startDateTime, LocalDateTime endDateTime) { if(problemNumbers==null || problemNumbers.isEmpty()) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/domain/service/SessionService.java b/src/main/java/kr/co/morandi/backend/defense_management/domain/service/SessionService.java new file mode 100644 index 00000000..d9f6115c --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/domain/service/SessionService.java @@ -0,0 +1,38 @@ +package kr.co.morandi.backend.defense_management.domain.service; + +import kr.co.morandi.backend.common.exception.MorandiException; +import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; +import kr.co.morandi.backend.defense_management.domain.error.SessionErrorCode; +import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; +import kr.co.morandi.backend.defense_record.application.port.out.record.RecordPort; +import kr.co.morandi.backend.defense_record.domain.error.RecordErrorCode; +import kr.co.morandi.backend.defense_record.domain.model.record.Record; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class SessionService { + + private final DefenseSessionPort defenseSessionPort; + private final RecordPort recordPort; + + @Async + @Transactional + public void terminateDefense(Long sessionId) { + final DefenseSession defenseSession = defenseSessionPort.findDefenseSessionById(sessionId) + .orElseThrow(() -> new MorandiException(SessionErrorCode.SESSION_NOT_FOUND)); + + defenseSession.terminateSession(); + + final Record record = recordPort.findRecordById(defenseSession.getRecordId()) + .orElseThrow(() -> new MorandiException(RecordErrorCode.RECORD_NOT_FOUND)); + + record.terminteDefense(); + + defenseSessionPort.saveDefenseSession(defenseSession); + recordPort.saveRecord(record); + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/session/DefenseSessionAdapter.java b/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/session/DefenseSessionAdapter.java index ae98400f..37a2af7b 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/session/DefenseSessionAdapter.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/session/DefenseSessionAdapter.java @@ -27,4 +27,9 @@ public DefenseSession saveDefenseSession(DefenseSession defenseSession) { public Optional findTodaysDailyDefenseSession(Member member, LocalDateTime now) { return defenseSessionRepository.findDailyDefenseSession(member, DAILY, now); } + + @Override + public Optional findDefenseSessionById(Long sessionId) { + return defenseSessionRepository.findById(sessionId); + } } diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/record/RecordPort.java b/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/record/RecordPort.java new file mode 100644 index 00000000..ab893949 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/record/RecordPort.java @@ -0,0 +1,10 @@ +package kr.co.morandi.backend.defense_record.application.port.out.record; + +import kr.co.morandi.backend.defense_record.domain.model.record.Record; + +import java.util.Optional; + +public interface RecordPort { + Optional> findRecordById(Long recordId); + void saveRecord(Record record); +} diff --git a/src/main/java/kr/co/morandi/backend/defense_record/domain/error/RecordErrorCode.java b/src/main/java/kr/co/morandi/backend/defense_record/domain/error/RecordErrorCode.java new file mode 100644 index 00000000..da8bfd95 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_record/domain/error/RecordErrorCode.java @@ -0,0 +1,26 @@ +package kr.co.morandi.backend.defense_record.domain.error; + +import kr.co.morandi.backend.common.exception.errorcode.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum RecordErrorCode implements ErrorCode { + RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "시험 기록(Record)를 찾을 수 없습니다."), + RECORD_ALREADY_TERMINATED(HttpStatus.BAD_REQUEST, "이미 종료된 시험 기록입니다.") + ; + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Record.java b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Record.java index 0a218ddc..252676f7 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Record.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/Record.java @@ -1,8 +1,10 @@ package kr.co.morandi.backend.defense_record.domain.model.record; import jakarta.persistence.*; +import kr.co.morandi.backend.common.exception.MorandiException; import kr.co.morandi.backend.common.model.BaseEntity; import kr.co.morandi.backend.defense_information.domain.model.defense.Defense; +import kr.co.morandi.backend.defense_record.domain.error.RecordErrorCode; import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import lombok.AccessLevel; @@ -42,8 +44,18 @@ public abstract class Record extends BaseEntity { private Long totalSolvedTime; + @Enumerated(EnumType.STRING) + private RecordStatus status; + private static final Long INITIAL_TOTAL_SOLVED_TIME = 0L; + public boolean terminteDefense() { + if(this.status.equals(RecordStatus.COMPLETED)) { + throw new MorandiException(RecordErrorCode.RECORD_ALREADY_TERMINATED); + } + this.status = RecordStatus.COMPLETED; + return true; + } public void addTotalSolvedTime(Long totalSolvedTime) { this.totalSolvedTime += totalSolvedTime; } @@ -53,6 +65,7 @@ protected Record(LocalDateTime testDate, Defense defense, Member member, Map this.createDetail(member, problem.getKey(), problem.getValue(), this, defense)) diff --git a/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/RecordStatus.java b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/RecordStatus.java new file mode 100644 index 00000000..a344ed71 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_record/domain/model/record/RecordStatus.java @@ -0,0 +1,5 @@ +package kr.co.morandi.backend.defense_record.domain.model.record; + +public enum RecordStatus { + IN_PROGRESS, COMPLETED +} diff --git a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense/DailyRecordAdapter.java b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailyrecord/DailyRecordAdapter.java similarity index 97% rename from src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense/DailyRecordAdapter.java rename to src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailyrecord/DailyRecordAdapter.java index fa3d0455..2ce8a4fa 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense/DailyRecordAdapter.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailyrecord/DailyRecordAdapter.java @@ -1,4 +1,4 @@ -package kr.co.morandi.backend.defense_record.infrastructure.adapter.dailydefense; +package kr.co.morandi.backend.defense_record.infrastructure.adapter.dailyrecord; import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; import kr.co.morandi.backend.member_management.domain.model.member.Member; @@ -11,7 +11,6 @@ import org.springframework.stereotype.Component; import java.time.LocalDate; -import java.util.List; import java.util.Optional; @Component diff --git a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/record/RecordAdapter.java b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/record/RecordAdapter.java new file mode 100644 index 00000000..fd48f8b3 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/record/RecordAdapter.java @@ -0,0 +1,25 @@ +package kr.co.morandi.backend.defense_record.infrastructure.adapter.record; + +import kr.co.morandi.backend.defense_record.application.port.out.record.RecordPort; +import kr.co.morandi.backend.defense_record.domain.model.record.Record; +import kr.co.morandi.backend.defense_record.infrastructure.persistence.record.RecordRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class RecordAdapter implements RecordPort { + + private final RecordRepository recordRepository; + @Override + public Optional> findRecordById(Long recordId) { + return recordRepository.findById(recordId); + } + + @Override + public void saveRecord(Record record) { + recordRepository.save(record); + } +} diff --git a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/record/RecordRepository.java b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/record/RecordRepository.java new file mode 100644 index 00000000..7fc61c7f --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/record/RecordRepository.java @@ -0,0 +1,7 @@ +package kr.co.morandi.backend.defense_record.infrastructure.persistence.record; + +import kr.co.morandi.backend.defense_record.domain.model.record.Record; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RecordRepository extends JpaRepository, Long> { +} diff --git a/src/test/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSessionTest.java b/src/test/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSessionTest.java index cf4d8bb0..2fa35445 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSessionTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSessionTest.java @@ -1,12 +1,13 @@ package kr.co.morandi.backend.defense_management.domain.model.session; +import kr.co.morandi.backend.common.exception.MorandiException; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.TempCode; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.member_management.domain.model.member.SocialType; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; -import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; @@ -26,6 +27,48 @@ @ActiveProfiles("test") class DefenseSessionTest { + @DisplayName("세션을 종료할 수 있다.") + @Test + void terminateSession() { + // given + Member member = createMember(); + LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); + DailyDefense dailyDefense = createDailyDefense(startTime.toLocalDate()); + Map problems = getProblems(dailyDefense, 1L); + DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, problems); + + DefenseSession defenseSession = DefenseSession.startSession(member, dailyRecord.getRecordId(), dailyDefense.getDefenseType(), problems.keySet(), startTime, dailyDefense.getEndTime(startTime)); + + + // when + final boolean result = defenseSession.terminateSession(); + + // then + assertThat(result).isTrue(); + + } + + @DisplayName("세션이 종료상태일 때 종료하려하면 false를 반환한다.") + @Test + void terminateSessionWhenAlreadyTerminated() { + // given + Member member = createMember(); + LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); + DailyDefense dailyDefense = createDailyDefense(startTime.toLocalDate()); + Map problems = getProblems(dailyDefense, 1L); + DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, problems); + + DefenseSession defenseSession = DefenseSession.startSession(member, dailyRecord.getRecordId(), dailyDefense.getDefenseType(), problems.keySet(), startTime, dailyDefense.getEndTime(startTime)); + defenseSession.terminateSession(); + + // when & then + assertThatThrownBy( + () -> defenseSession.terminateSession() + ) + .isInstanceOf(MorandiException.class) + .hasMessage("이미 종료된 세션입니다."); + + } @DisplayName("문제 번호를 가지고 있는지 확인한다.") @Test diff --git a/src/test/java/kr/co/morandi/backend/defense_management/domain/service/SessionServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_management/domain/service/SessionServiceTest.java new file mode 100644 index 00000000..4b2e7fba --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_management/domain/service/SessionServiceTest.java @@ -0,0 +1,212 @@ +package kr.co.morandi.backend.defense_management.domain.service; + +import kr.co.morandi.backend.common.exception.MorandiException; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; +import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; +import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; +import kr.co.morandi.backend.defense_management.domain.model.session.ExamStatus; +import kr.co.morandi.backend.defense_management.infrastructure.persistence.session.DefenseSessionRepository; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyDetail; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.defense_record.domain.model.record.RecordStatus; +import kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record.DailyRecordRepository; +import kr.co.morandi.backend.member_management.domain.model.member.Member; +import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; +import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier.*; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class SessionServiceTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private DailyRecordRepository dailyRecordRepository; + + @Autowired + private DailyDefenseRepository dailyDefenseRepository; + + @Autowired + private ProblemRepository problemRepository; + + @Autowired + private DefenseSessionRepository defenseSessionRepository; + + @Autowired + private SessionService sessionService; + + + @DisplayName("DailyDefense를 시작했을 때 세션과 Record를 종료할 수 있다.") + @Test + void terminateDefense() { + // given + LocalDateTime today = LocalDateTime.of(2021, 10, 1, 0, 0); + Member member = createMember(); + DailyRecord dailyRecord = tryDailyDefense(today, member); + DefenseSession session = DefenseSession.builder() + .recordId(dailyRecord.getRecordId()) + .problemNumbers(Set.of(2L)) + .startDateTime(today) + .endDateTime(today.plusMinutes(1)) + .build(); + + final DefenseSession defenseSession = defenseSessionRepository.save(session); + + // when + sessionService.terminateDefense(defenseSession.getDefenseSessionId()); + + // then + defenseSessionRepository.findById(defenseSession.getDefenseSessionId()) + .ifPresent(s -> assertEquals(s.getExamStatus(), ExamStatus.COMPLETED)); + + } + + @DisplayName("Session이 종료된 상태에서 세션을 종료하려고 하면 예외가 발생한다.") + @Test + void terminateDefenseWhenSessionTerminated() { + // given + LocalDateTime today = LocalDateTime.of(2021, 10, 1, 0, 0); + Member member = createMember(); + DailyRecord dailyRecord = tryDailyDefense(today, member); + DefenseSession session = DefenseSession.builder() + .recordId(dailyRecord.getRecordId()) + .problemNumbers(Set.of(2L)) + .startDateTime(today) + .endDateTime(today.plusMinutes(1)) + .build(); + + final DefenseSession defenseSession = defenseSessionRepository.save(session); + defenseSession.terminateSession(); + + // when & then + assertThatThrownBy(() -> sessionService.terminateDefense(defenseSession.getDefenseSessionId())) + .isInstanceOf(MorandiException.class) + .hasMessageContaining("이미 종료된 세션입니다."); + } + + @DisplayName("없는 Session ID로 세션을 종료하려고 하면 예외가 발생한다.") + @Test + void terminateDefenseWhenSessionNotFound() { + // given + Long notFoundSessionId = 1L; + + + // when & then + assertThatThrownBy(() -> sessionService.terminateDefense(notFoundSessionId)) + .isInstanceOf(MorandiException.class) + .hasMessageContaining("세션을 찾을 수 없습니다."); + } + + @DisplayName("Session이 종료된 상태에서 세션을 종료하려고 하면 예외가 발생한다.") + @Test + void terminateDefenseWhenRecordTerminated() { + // given + LocalDateTime today = LocalDateTime.of(2021, 10, 1, 0, 0); + Member member = createMember(); + DailyRecord dailyRecord = tryDailyDefense(today, member); + DefenseSession session = DefenseSession.builder() + .recordId(dailyRecord.getRecordId()) + .problemNumbers(Set.of(2L)) + .startDateTime(today) + .endDateTime(today.plusMinutes(1)) + .build(); + dailyRecord.terminteDefense(); + + final DefenseSession defenseSession = defenseSessionRepository.save(session); + + + + // when & then + assertThatThrownBy(() -> sessionService.terminateDefense(defenseSession.getDefenseSessionId())) + .isInstanceOf(MorandiException.class) + .hasMessageContaining("이미 종료된 시험 기록입니다."); + + } + + private DailyRecord tryDailyDefense(LocalDateTime today, Member member) { + final DailyDefense dailyDefense = createDailyDefense(today.toLocalDate()); + final Map problem = getProblem(dailyDefense, 2L); + + DailyRecord dailyRecord = DailyRecord.builder() + .testDate(today) + .defense(dailyDefense) + .status(RecordStatus.IN_PROGRESS) + .member(member) + .build(); + + final DailyRecord savedDailyRecord = dailyRecordRepository.save(dailyRecord); + + final DailyDetail dailyDetail = DailyDetail.builder() + .member(member) + .problemNumber(1L) + .problem(problem.get(1L)) + .record(savedDailyRecord) + .defense(dailyDefense) + .build(); + + savedDailyRecord.getDetails().add(dailyDetail); + + return dailyRecordRepository.save(savedDailyRecord); + } + + private Map getProblem(DailyDefense dailyDefense, Long problemNumber) { + return dailyDefense.getDailyDefenseProblems().stream() + .filter(problem -> Objects.equals(problem.getProblemNumber(), problemNumber)) + .collect(Collectors.toMap(DailyDefenseProblem::getProblemNumber, DailyDefenseProblem::getProblem)); + } + + private DailyDefense createDailyDefense(LocalDate createdDate) { + AtomicLong problemNumber = new AtomicLong(1L); + Map problemMap = createProblems().stream() + .collect(Collectors.toMap(p-> problemNumber.getAndIncrement(), problem -> problem)); + + return dailyDefenseRepository.save(DailyDefense.create(createdDate, "오늘의 문제 테스트", problemMap)); + } + private List createProblems() { + Problem problem1 = Problem.builder() + .baekjoonProblemId(1L) + .problemTier(B5) + .build(); + + Problem problem2 = Problem.builder() + .baekjoonProblemId(2L) + .problemTier(S5) + .build(); + + Problem problem3 = Problem.builder() + .baekjoonProblemId(3L) + .problemTier(G5) + .build(); + + return problemRepository.saveAll(List.of(problem1, problem2, problem3)); + } + private Member createMember() { + return memberRepository.save(Member.builder() + .email("test") + .build()); + } +} \ No newline at end of file diff --git a/src/test/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecordTest.java b/src/test/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecordTest.java index d6a2f541..35a270db 100644 --- a/src/test/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecordTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_record/domain/model/dailydefense_record/DailyRecordTest.java @@ -1,10 +1,10 @@ package kr.co.morandi.backend.defense_record.domain.model.dailydefense_record; +import kr.co.morandi.backend.common.exception.MorandiException; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; -import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; @@ -27,6 +27,40 @@ @ActiveProfiles("test") class DailyRecordTest { + @DisplayName("시험 기록(Record)를 종료하면 종료 상태로 변경된다.") + @Test + void terminateDefense() { + // given + DailyDefense dailyDefense = createDailyDefense(); + LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); + Member member = createMember("user"); + Map triedProblem = getProblems(dailyDefense, 2L); + DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, triedProblem); + + + // when + final boolean result = dailyRecord.terminteDefense(); + + // then + assertThat(result).isTrue(); + + } + + @DisplayName("시험 기록(Record)가 종료된 상태에서 다시 종료하려고 하면 false를 반환한다.") + @Test + void terminateDefenseWhenAlreadyTerminated() { + // given + DailyDefense dailyDefense = createDailyDefense(); + LocalDateTime startTime = LocalDateTime.of(2024, 3, 1, 12, 0, 0); + Member member = createMember("user"); + Map triedProblem = getProblems(dailyDefense, 2L); + DailyRecord dailyRecord = DailyRecord.tryDefense(startTime, dailyDefense, member, triedProblem); + dailyRecord.terminteDefense(); + + // when & then + assertThatThrownBy(() -> dailyRecord.terminteDefense()) + .isInstanceOf(MorandiException.class); + } @DisplayName("오늘의 문제를 정답처리 하면 푼 total 문제 수가 증가하고, 푼 시간이 기록된다.") @Test void solveProblem() { diff --git a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/record/RecordAdapterTest.java b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/record/RecordAdapterTest.java new file mode 100644 index 00000000..ad05eec5 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/record/RecordAdapterTest.java @@ -0,0 +1,135 @@ +package kr.co.morandi.backend.defense_record.infrastructure.adapter.record; + +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; +import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyDetail; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.defense_record.domain.model.record.Record; +import kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record.DailyRecordRepository; +import kr.co.morandi.backend.member_management.domain.model.member.Member; +import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; +import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier.*; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class RecordAdapterTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private DailyDefenseRepository dailyDefenseRepository; + + @Autowired + private RecordAdapter recordAdapter; + + @Autowired + private ProblemRepository problemRepository; + + @Autowired + private DailyRecordRepository dailyRecordRepository; + + @DisplayName("RecordId로 Record를 찾아올 수 있다.") + @Test + void findRecordById() { + // given + LocalDateTime today = LocalDateTime.of(2021, 10, 1, 0, 0); + + final Member member = createMember(); + final DailyRecord dailyRecord = tryDailyDefense(today, member); + + // when + final Optional> record = recordAdapter.findRecordById(dailyRecord.getRecordId()); + + // then + assertThat(record).isPresent() + .get() + .extracting("recordId", "defense.contentName") + .contains(dailyRecord.getRecordId(), "오늘의 문제 테스트"); + + } + + private DailyRecord tryDailyDefense(LocalDateTime today, Member member) { + final DailyDefense dailyDefense = createDailyDefense(today.toLocalDate()); + final Map problem = getProblem(dailyDefense, 2L); + + DailyRecord dailyRecord = DailyRecord.builder() + .testDate(today) + .defense(dailyDefense) + .member(member) + .build(); + + final DailyRecord savedDailyRecord = dailyRecordRepository.save(dailyRecord); + + final DailyDetail dailyDetail = DailyDetail.builder() + .member(member) + .problemNumber(1L) + .problem(problem.get(1L)) + .record(savedDailyRecord) + .defense(dailyDefense) + .build(); + + savedDailyRecord.getDetails().add(dailyDetail); + + return dailyRecordRepository.save(savedDailyRecord); + } + + private Map getProblem(DailyDefense dailyDefense, Long problemNumber) { + return dailyDefense.getDailyDefenseProblems().stream() + .filter(problem -> Objects.equals(problem.getProblemNumber(), problemNumber)) + .collect(Collectors.toMap(DailyDefenseProblem::getProblemNumber, DailyDefenseProblem::getProblem)); + } + + private DailyDefense createDailyDefense(LocalDate createdDate) { + AtomicLong problemNumber = new AtomicLong(1L); + Map problemMap = createProblems().stream() + .collect(Collectors.toMap(p-> problemNumber.getAndIncrement(), problem -> problem)); + + return dailyDefenseRepository.save(DailyDefense.create(createdDate, "오늘의 문제 테스트", problemMap)); + } + private List createProblems() { + Problem problem1 = Problem.builder() + .baekjoonProblemId(1L) + .problemTier(B5) + .build(); + + Problem problem2 = Problem.builder() + .baekjoonProblemId(2L) + .problemTier(S5) + .build(); + + Problem problem3 = Problem.builder() + .baekjoonProblemId(3L) + .problemTier(G5) + .build(); + + return problemRepository.saveAll(List.of(problem1, problem2, problem3)); + } + private Member createMember() { + return memberRepository.save(Member.builder() + .email("test") + .build()); + } + +} \ No newline at end of file From db548fc69c33410190f4f039d8f26969c4572809 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Wed, 24 Apr 2024 19:06:31 +0900 Subject: [PATCH 35/44] =?UTF-8?q?:art:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/port/in/DailyDefenseUseCase.java | 1 - .../application/service/timer/DefenseTimerService.java | 2 +- .../domain/model/session/DefenseSession.java | 6 ++++++ .../defense_management/domain/service/SessionService.java | 2 -- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java b/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java index 3f767397..da2cd7b1 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/port/in/DailyDefenseUseCase.java @@ -1,7 +1,6 @@ package kr.co.morandi.backend.defense_information.application.port.in; import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; -import kr.co.morandi.backend.member_management.domain.model.member.Member; import java.time.LocalDateTime; diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java index 8553397c..e77b770a 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java @@ -23,7 +23,7 @@ public void startDefenseTimer(DefenseSession defenseSession) { long delay = Duration.between(defenseSession.getStartDateTime(), defenseSession.getEndDateTime()).toMillis(); final ScheduledFuture schedule = scheduler.schedule(() -> { - sessionService.terminateDefense(defenseSession.getDefenseSessionId()); + defenseSession.terminateDefense(sessionService); defenseSessionTimer.remove(defenseSession.getDefenseSessionId()); }, delay, TimeUnit.MILLISECONDS); diff --git a/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSession.java b/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSession.java index 924eee13..2545be6b 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSession.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/domain/model/session/DefenseSession.java @@ -5,6 +5,7 @@ import kr.co.morandi.backend.common.model.BaseEntity; import kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType; import kr.co.morandi.backend.defense_management.domain.error.SessionErrorCode; +import kr.co.morandi.backend.defense_management.domain.service.SessionService; import kr.co.morandi.backend.member_management.domain.model.member.Member; import lombok.AccessLevel; import lombok.Builder; @@ -50,6 +51,10 @@ public class DefenseSession extends BaseEntity { private static final Long INITIAL_ACCESS_PROBLEM_NUMBER = 1L; + public void terminateDefense(SessionService sessionService) { + sessionService.terminateDefense(this.getDefenseSessionId()); + } + public boolean terminateSession() { if(examStatus == ExamStatus.COMPLETED) { throw new MorandiException(SessionErrorCode.SESSION_ALREADY_ENDED); @@ -57,6 +62,7 @@ public boolean terminateSession() { examStatus = ExamStatus.COMPLETED; return true; } + public SessionDetail getSessionDetail(Long problemNumber) { return getSessionDetails().stream() .filter(detail -> Objects.equals(detail.getProblemNumber(), problemNumber)) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/domain/service/SessionService.java b/src/main/java/kr/co/morandi/backend/defense_management/domain/service/SessionService.java index d9f6115c..51fbbfde 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/domain/service/SessionService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/domain/service/SessionService.java @@ -8,7 +8,6 @@ import kr.co.morandi.backend.defense_record.domain.error.RecordErrorCode; import kr.co.morandi.backend.defense_record.domain.model.record.Record; import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,7 +18,6 @@ public class SessionService { private final DefenseSessionPort defenseSessionPort; private final RecordPort recordPort; - @Async @Transactional public void terminateDefense(Long sessionId) { final DefenseSession defenseSession = defenseSessionPort.findDefenseSessionById(sessionId) From 0a9891be1afd5da1b65660c934835fc12d7e3e59 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Fri, 26 Apr 2024 14:40:17 +0900 Subject: [PATCH 36/44] =?UTF-8?q?:art:=20ConcurrentHashMap=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20=EA=B5=AC=EC=A1=B0=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=ED=83=80=EC=9D=B4=EB=A8=B8=20=EB=93=B1=EB=A1=9D=201=ED=9A=8C?= =?UTF-8?q?=20=EB=B3=B4=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/DailyDefenseManagementService.java | 14 +++++++++----- .../service/timer/DefenseTimerService.java | 18 ++---------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java index 138dc636..e754ba33 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java @@ -68,10 +68,6 @@ public StartDailyDefenseResponse startDailyDefense(StartDailyDefenseServiceReque final DefenseSession savedDefenseSession = defenseSessionPort.saveDefenseSession(defenseSession); - /* - * DefenseSession에 관련된 타이머 시작 - * */ - defenseTimerService.startDefenseTimer(savedDefenseSession); // 문제 내용 가져오기 final Map problemContent = getProblemContents(tryProblem); @@ -98,7 +94,15 @@ private DefenseSession createNewSession(Member member, LocalDateTime now, DailyD DailyRecord savedDailyRecord = dailyRecordPort.saveDailyRecord(dailyRecord); Long recordId = savedDailyRecord.getRecordId(); - return DefenseSession.startSession(member, recordId, dailyDefense.getDefenseType(), tryProblem.keySet(), now, dailyDefense.getEndTime(now)); + final DefenseSession defenseSession = defenseSessionPort.saveDefenseSession( + DefenseSession.startSession(member, recordId, dailyDefense.getDefenseType(), tryProblem.keySet(), now, dailyDefense.getEndTime(now))); + + /* + * DefenseSession에 관련된 타이머 시작 + * */ + defenseTimerService.startDefenseTimer(defenseSession); + + return defenseSession; } diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java index e77b770a..8f34d236 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java @@ -13,30 +13,16 @@ public class DefenseTimerService { private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - private final ConcurrentHashMap> defenseSessionTimer = new ConcurrentHashMap<>(); private final SessionService sessionService; public void startDefenseTimer(DefenseSession defenseSession) { - if(isAlreadySettedTimer(defenseSession)) { - return; - } + long delay = Duration.between(defenseSession.getStartDateTime(), defenseSession.getEndDateTime()).toMillis(); - final ScheduledFuture schedule = scheduler.schedule(() -> { + scheduler.schedule(() -> { defenseSession.terminateDefense(sessionService); - defenseSessionTimer.remove(defenseSession.getDefenseSessionId()); }, delay, TimeUnit.MILLISECONDS); - defenseSessionTimer.put(defenseSession.getDefenseSessionId(), schedule); - } - - /* - * 현재는 단일 인스턴스이므로 ConcurrentHashMap을 사용하여 중복 타이머가 설정되지 않도록 하였습니다. - * - * scale-out될 경우에는 분산 환경에서 중복 타이머가 설정되지 않도록 하기 위해 Redis와 같은 분산 캐시를 사용하여 중복 타이머가 설정되지 않도록 해야합니다. - * */ - private boolean isAlreadySettedTimer(DefenseSession defenseSession) { - return defenseSessionTimer.containsKey(defenseSession.getDefenseSessionId()); } } From d286429736146768d630fd384435b36dbaffc88e Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Fri, 26 Apr 2024 18:31:52 +0900 Subject: [PATCH 37/44] =?UTF-8?q?:art:=20=EB=A1=A4=EB=B0=B1=EC=8B=9C=20?= =?UTF-8?q?=ED=83=80=EC=9D=B4=EB=A8=B8=20=EC=A0=95=EC=83=81=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=EB=A5=BC=20=EC=9C=84=ED=95=B4=20Timer=20Event=20?= =?UTF-8?q?=EB=B0=9C=ED=96=89=20=EB=B0=A9=EC=8B=9D=20=EC=84=A0=ED=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DailyDefenseManagementService.java | 9 ++- .../service/timer/DefenseTimerService.java | 40 +++++----- .../domain/event/DefenseStartTimerEvent.java | 16 ++++ .../domain/service/DefenseEventService.java | 20 +++++ .../DailyDefenseManagementServiceTest.java | 53 ++++++++++--- .../service/DefenseEventServiceTest.java | 74 +++++++++++++++++++ 6 files changed, 178 insertions(+), 34 deletions(-) create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/domain/event/DefenseStartTimerEvent.java create mode 100644 src/main/java/kr/co/morandi/backend/defense_management/domain/service/DefenseEventService.java create mode 100644 src/test/java/kr/co/morandi/backend/defense_management/domain/service/DefenseEventServiceTest.java diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java index e754ba33..15455ed3 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/session/DailyDefenseManagementService.java @@ -7,7 +7,7 @@ import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; import kr.co.morandi.backend.defense_management.application.request.session.StartDailyDefenseServiceRequest; import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; - import kr.co.morandi.backend.defense_management.application.service.timer.DefenseTimerService; + import kr.co.morandi.backend.defense_management.domain.event.DefenseStartTimerEvent; import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; @@ -17,6 +17,7 @@ import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import lombok.RequiredArgsConstructor; + import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -37,7 +38,7 @@ public class DailyDefenseManagementService { private final DefenseSessionPort defenseSessionPort; private final MemberPort memberPort; private final ProblemContentPort problemContentPort; - private final DefenseTimerService defenseTimerService; + private final ApplicationEventPublisher publisher; @Transactional public StartDailyDefenseResponse startDailyDefense(StartDailyDefenseServiceRequest request, Long memberId, LocalDateTime requestTime) { @@ -98,9 +99,9 @@ private DefenseSession createNewSession(Member member, LocalDateTime now, DailyD DefenseSession.startSession(member, recordId, dailyDefense.getDefenseType(), tryProblem.keySet(), now, dailyDefense.getEndTime(now))); /* - * DefenseSession에 관련된 타이머 시작 + * DefenseSession에 관련된 타이머 시작 이벤트 발행 * */ - defenseTimerService.startDefenseTimer(defenseSession); + publisher.publishEvent(new DefenseStartTimerEvent(defenseSession.getDefenseSessionId(), defenseSession.getStartDateTime(), defenseSession.getEndDateTime())); return defenseSession; } diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java index 8f34d236..68d0bf2c 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java @@ -1,28 +1,30 @@ -package kr.co.morandi.backend.defense_management.application.service.timer; + package kr.co.morandi.backend.defense_management.application.service.timer; -import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; -import kr.co.morandi.backend.defense_management.domain.service.SessionService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; + import kr.co.morandi.backend.defense_management.domain.service.SessionService; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; -import java.time.Duration; -import java.util.concurrent.*; + import java.time.Duration; + import java.time.LocalDateTime; + import java.util.concurrent.Executors; + import java.util.concurrent.ScheduledExecutorService; + import java.util.concurrent.TimeUnit; -@Service -@RequiredArgsConstructor -public class DefenseTimerService { + @Service + @RequiredArgsConstructor + public class DefenseTimerService { - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - private final SessionService sessionService; + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final SessionService sessionService; - public void startDefenseTimer(DefenseSession defenseSession) { + public void startDefenseTimer(Long defenseSessionId, LocalDateTime startDateTime, LocalDateTime endDateTime) { - long delay = Duration.between(defenseSession.getStartDateTime(), defenseSession.getEndDateTime()).toMillis(); + long delay = Duration.between(startDateTime, endDateTime).toMillis(); - scheduler.schedule(() -> { - defenseSession.terminateDefense(sessionService); - }, delay, TimeUnit.MILLISECONDS); + scheduler.schedule(() -> { + sessionService.terminateDefense(defenseSessionId); + }, delay, TimeUnit.MILLISECONDS); - } + } -} + } diff --git a/src/main/java/kr/co/morandi/backend/defense_management/domain/event/DefenseStartTimerEvent.java b/src/main/java/kr/co/morandi/backend/defense_management/domain/event/DefenseStartTimerEvent.java new file mode 100644 index 00000000..7b12032e --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/domain/event/DefenseStartTimerEvent.java @@ -0,0 +1,16 @@ +package kr.co.morandi.backend.defense_management.domain.event; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@RequiredArgsConstructor +public class DefenseStartTimerEvent { + + private final Long sessionId; + private final LocalDateTime startDateTime; + private final LocalDateTime endDateTime; + +} diff --git a/src/main/java/kr/co/morandi/backend/defense_management/domain/service/DefenseEventService.java b/src/main/java/kr/co/morandi/backend/defense_management/domain/service/DefenseEventService.java new file mode 100644 index 00000000..ce382763 --- /dev/null +++ b/src/main/java/kr/co/morandi/backend/defense_management/domain/service/DefenseEventService.java @@ -0,0 +1,20 @@ +package kr.co.morandi.backend.defense_management.domain.service; + +import kr.co.morandi.backend.defense_management.application.service.timer.DefenseTimerService; +import kr.co.morandi.backend.defense_management.domain.event.DefenseStartTimerEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Service +@RequiredArgsConstructor +public class DefenseEventService { + + private final DefenseTimerService defenseTimerService; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onDefenseStartTimerEvent(DefenseStartTimerEvent event) { + defenseTimerService.startDefenseTimer(event.getSessionId(), event.getStartDateTime(), event.getEndDateTime()); + } +} \ No newline at end of file diff --git a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java index 82845969..539f6998 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java @@ -1,32 +1,33 @@ package kr.co.morandi.backend.defense_management.application.service.dailydefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; +import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; +import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; +import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDetailRepository; import kr.co.morandi.backend.defense_management.application.request.session.StartDailyDefenseServiceRequest; -import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; import kr.co.morandi.backend.defense_management.application.service.session.DailyDefenseManagementService; -import kr.co.morandi.backend.problem_information.infrastructure.adapter.problemcontent.ProblemContentAdapter; -import kr.co.morandi.backend.member_management.domain.model.member.Member; -import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; -import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; -import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; +import kr.co.morandi.backend.defense_management.domain.event.DefenseStartTimerEvent; import kr.co.morandi.backend.defense_management.infrastructure.persistence.session.DefenseSessionRepository; import kr.co.morandi.backend.defense_management.infrastructure.persistence.session.SessionDetailRepository; -import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDetailRepository; +import kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record.DailyRecordRepository; +import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; +import kr.co.morandi.backend.problem_information.infrastructure.adapter.problemcontent.ProblemContentAdapter; import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; -import kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record.DailyRecordRepository; import org.junit.jupiter.api.AfterEach; 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.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; import java.time.LocalDate; import java.time.LocalDateTime; @@ -45,8 +46,8 @@ @SpringBootTest -@ExtendWith(MockitoExtension.class) @ActiveProfiles("test") +@RecordApplicationEvents class DailyDefenseManagementServiceTest { @Autowired @@ -79,6 +80,9 @@ class DailyDefenseManagementServiceTest { @MockBean private ProblemContentAdapter problemContentAdapter; + @Autowired + private ApplicationEvents applicationEvents; + @BeforeEach void setUp() { Map problemContentMap = Map.of( @@ -111,6 +115,33 @@ void tearDown() { memberRepository.deleteAllInBatch(); } + @DisplayName("오늘의 문제가 시작될 때 타이머 이벤트를 발행한다.") + @Test + void eventPublishWhenStartDailyDefense() { + // given + Member member = createMember(); + createDailyDefense(LocalDate.of(2021, 10, 1), "오늘의 문제 테스트"); + + LocalDateTime requestTime = LocalDateTime.of(2021, 10, 1, 0, 0, 0); + + StartDailyDefenseServiceRequest request = StartDailyDefenseServiceRequest.builder() + .problemNumber(2L) + .build(); + + // when + dailyDefenseManagementService.startDailyDefense(request, member.getMemberId(), requestTime); + + // then + assertThat(applicationEvents.stream(DefenseStartTimerEvent.class)) + .hasSize(1) + .anySatisfy(event -> { + assertAll( + () -> assertThat(event.getSessionId()).isNotNull(), + () -> assertThat(event.getStartDateTime()).isNotNull(), + () -> assertThat(event.getEndDateTime()).isNotNull() + ); + }); + } @DisplayName("전날 시작했던 시험이 안 끝났더라도 오늘의 문제를 시도하면 해당하는 날짜의 문제를 제공한다.") @Test void retryDailyDefenseWhenDayPassed() { diff --git a/src/test/java/kr/co/morandi/backend/defense_management/domain/service/DefenseEventServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_management/domain/service/DefenseEventServiceTest.java new file mode 100644 index 00000000..ca1dfb22 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_management/domain/service/DefenseEventServiceTest.java @@ -0,0 +1,74 @@ +package kr.co.morandi.backend.defense_management.domain.service; + +import kr.co.morandi.backend.defense_management.application.service.timer.DefenseTimerService; +import kr.co.morandi.backend.defense_management.domain.event.DefenseStartTimerEvent; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.support.TransactionTemplate; + +import java.time.LocalDateTime; + +import static org.mockito.Mockito.*; + +@SpringBootTest +@ActiveProfiles("test") +class DefenseEventServiceTest { + + @Autowired + private ApplicationEventPublisher publisher; + + @MockBean + private DefenseTimerService defenseTimerService; + + @Autowired + private DefenseEventService defenseEventService; + + @Autowired + private TransactionTemplate transactionTemplate; + + + @DisplayName("DefenseStartTimerEvent가 발생하면 DefenseTimerService의 startDefenseTimer가 호출된다.") + @Test + void onDefenseStartTimerEvent() { + // given + LocalDateTime startDateTime = LocalDateTime.of(2021, 10, 1, 0, 0, 0); + LocalDateTime endDateTime = LocalDateTime.of(2021, 10, 1, 0, 1, 0); + DefenseStartTimerEvent event = new DefenseStartTimerEvent(1L, startDateTime, endDateTime); + + // when + transactionTemplate.execute(status -> { + publisher.publishEvent(event); + return null; + }); + + // then + verify(defenseTimerService, times(1)) + .startDefenseTimer(1L, startDateTime, endDateTime); + } + + @DisplayName("DefenseStartTimerEvent 발행 후 rollback되면 DefenseTimerService의 startDefenseTimer가 호출되지 않는다.") + @Test + void onDefenseStartTimerEventRollback() { + // given + LocalDateTime startDateTime = LocalDateTime.of(2021, 10, 1, 0, 0, 0); + LocalDateTime endDateTime = LocalDateTime.of(2021, 10, 1, 0, 1, 0); + DefenseStartTimerEvent event = new DefenseStartTimerEvent(1L, startDateTime, endDateTime); + + // when + transactionTemplate.execute(status -> { + publisher.publishEvent(event); + status.setRollbackOnly(); + return null; + }); + + // then + verify(defenseTimerService, never()) + .startDefenseTimer(1L, startDateTime, endDateTime); + } + +} \ No newline at end of file From 52a52e10073cb2796995ffa1d2579a42145a35dc Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Mon, 29 Apr 2024 17:58:17 +0900 Subject: [PATCH 38/44] =?UTF-8?q?:white=5Fcheck=5Fmark:=20=EB=A1=A4?= =?UTF-8?q?=EB=B0=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DailyDefenseManagementServiceTest.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java index 539f6998..aef94860 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java @@ -7,6 +7,7 @@ import kr.co.morandi.backend.defense_management.application.request.session.StartDailyDefenseServiceRequest; import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; import kr.co.morandi.backend.defense_management.application.service.session.DailyDefenseManagementService; +import kr.co.morandi.backend.defense_management.application.service.timer.DefenseTimerService; import kr.co.morandi.backend.defense_management.domain.event.DefenseStartTimerEvent; import kr.co.morandi.backend.defense_management.infrastructure.persistence.session.DefenseSessionRepository; import kr.co.morandi.backend.defense_management.infrastructure.persistence.session.SessionDetailRepository; @@ -28,6 +29,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.event.ApplicationEvents; import org.springframework.test.context.event.RecordApplicationEvents; +import org.springframework.transaction.support.TransactionTemplate; import java.time.LocalDate; import java.time.LocalDateTime; @@ -42,7 +44,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.*; @SpringBootTest @@ -83,6 +87,12 @@ class DailyDefenseManagementServiceTest { @Autowired private ApplicationEvents applicationEvents; + @Autowired + private TransactionTemplate transactionTemplate; + + @MockBean + private DefenseTimerService defenseTimerService; + @BeforeEach void setUp() { Map problemContentMap = Map.of( @@ -115,6 +125,40 @@ void tearDown() { memberRepository.deleteAllInBatch(); } + @DisplayName("오늘의 문제가 시작되다가 롤백되면 타이머 이벤트가 실행되지 않는다.") + @Test + void eventPublishWhenStartDailyDefenseWhenRollback() { + // given + Member member = createMember(); + createDailyDefense(LocalDate.of(2021, 10, 1), "오늘의 문제 테스트"); + + LocalDateTime requestTime = LocalDateTime.of(2021, 10, 1, 0, 0, 0); + + StartDailyDefenseServiceRequest request = StartDailyDefenseServiceRequest.builder() + .problemNumber(2L) + .build(); + + // when + transactionTemplate.execute(status -> { + dailyDefenseManagementService.startDailyDefense(request, member.getMemberId(), requestTime); + status.setRollbackOnly(); // Force rollback + return null; + }); + + // then + assertThat(applicationEvents.stream(DefenseStartTimerEvent.class)) + .hasSize(1) + .anySatisfy(event -> { + assertAll( + () -> assertThat(event.getSessionId()).isNotNull(), + () -> assertThat(event.getStartDateTime()).isNotNull(), + () -> assertThat(event.getEndDateTime()).isNotNull() + ); + }); + + verify(defenseTimerService, never()).startDefenseTimer(any(), any(), any()); + } + @DisplayName("오늘의 문제가 시작될 때 타이머 이벤트를 발행한다.") @Test void eventPublishWhenStartDailyDefense() { @@ -141,6 +185,7 @@ void eventPublishWhenStartDailyDefense() { () -> assertThat(event.getEndDateTime()).isNotNull() ); }); + verify(defenseTimerService, times(1)).startDefenseTimer(any(), any(), any()); } @DisplayName("전날 시작했던 시험이 안 끝났더라도 오늘의 문제를 시도하면 해당하는 날짜의 문제를 제공한다.") @Test From 340b587f89787c950e12de723f1311ec4adac062 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Wed, 1 May 2024 14:01:59 +0900 Subject: [PATCH 39/44] =?UTF-8?q?:sparkles:=20RestDocs=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20docstest=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 31 +++ .../api/dailydefense/dailydefense.adoc | 19 ++ src/docs/asciidoc/index.adoc | 16 ++ .../session/DefenseProblemResponse.java | 5 + .../session/StartDailyDefenseResponse.java | 4 + .../DefenseMangementControllerTest.java | 10 + .../morandi/backend/docs/RestDocsSupport.java | 33 +++ .../DailyDefenseControllerDocsTest.java | 96 ++++++++ .../DefenseManagementControllerDocsTest.java | 209 ++++++++++++++++++ .../request-fields.snippet | 14 ++ .../response-fields.snippet | 14 ++ 11 files changed, 451 insertions(+) create mode 100644 src/docs/asciidoc/api/dailydefense/dailydefense.adoc create mode 100644 src/docs/asciidoc/index.adoc create mode 100644 src/test/java/kr/co/morandi/backend/defense_management/infrastructure/controller/DefenseMangementControllerTest.java create mode 100644 src/test/java/kr/co/morandi/backend/docs/RestDocsSupport.java create mode 100644 src/test/java/kr/co/morandi/backend/docs/dailydefense/DailyDefenseControllerDocsTest.java create mode 100644 src/test/java/kr/co/morandi/backend/docs/dailydefense/DefenseManagementControllerDocsTest.java create mode 100644 src/test/resources/org.springframework.restdocs.templates/request-fields.snippet create mode 100644 src/test/resources/org.springframework.restdocs.templates/response-fields.snippet diff --git a/build.gradle b/build.gradle index be7d3d2c..41e8057c 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.2.2' id 'io.spring.dependency-management' version '1.1.4' + id 'org.asciidoctor.jvm.convert' version '3.3.2' } plugins { id "org.sonarqube" version "4.4.1.3373" @@ -65,6 +66,8 @@ configurations { compileOnly { extendsFrom annotationProcessor } + + asciidoctorExt } repositories { @@ -108,6 +111,10 @@ dependencies { // Test Dependencies testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'com.h2database:h2' + + // RestDocs + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' } tasks.named('sonarqube').configure { @@ -119,7 +126,31 @@ tasks.named('test') { finalizedBy 'jacocoTestReport' } +ext { //전역 변수 + snippetsDir = file("build/generated-snippets") +} + +test { + outputs.dir snippetsDir +} + +asciidoctor { + inputs.dir snippetsDir + configurations 'asciidoctorExt' + sources { // 특정 파일만 html로 만든다 + include '**/index.adoc' + } + baseDirFollowsSourceFile() // 다른 adoc 파일을 참조할 때 경로를 baseDir 기준으로 찾음 + dependsOn test +} + +bootJar { + dependsOn asciidoctor + from("${asciidoctor.outputDir}") { + into 'static/docs' + } +} jacocoTestReport { dependsOn test diff --git a/src/docs/asciidoc/api/dailydefense/dailydefense.adoc b/src/docs/asciidoc/api/dailydefense/dailydefense.adoc new file mode 100644 index 00000000..bcb38d4c --- /dev/null +++ b/src/docs/asciidoc/api/dailydefense/dailydefense.adoc @@ -0,0 +1,19 @@ +[[Daily-Defense]] +== 오늘의 문제 정보 조회 + +==== Request +include::{snippets}/daily-defense-info/http-request.adoc[] + +==== Response +include::{snippets}/daily-defense-info/http-response.adoc[] +include::{snippets}/daily-defense-info/response-fields.adoc[] + + +== 오늘의 문제 시작 + +==== Request +include::{snippets}/daily-defense-start/http-request.adoc[] + +==== Response +include::{snippets}/daily-defense-start/http-response.adoc[] +include::{snippets}/daily-defense-start/response-fields.adoc[] diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc new file mode 100644 index 00000000..b23195f2 --- /dev/null +++ b/src/docs/asciidoc/index.adoc @@ -0,0 +1,16 @@ +ifndef::snippets[] +:snippets: ../../build/generated-snippets +endif::[] += 모두의 랜덤 디펜스 REST API +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 1 +:sectlinks: + +include::api/dailydefense/dailydefense.adoc[] + + + +[[Daily-Defense-List]] \ No newline at end of file diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java index 34560df8..2d0bc108 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java @@ -1,5 +1,6 @@ package kr.co.morandi.backend.defense_management.application.response.session; +import com.fasterxml.jackson.annotation.JsonProperty; import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.defense_management.application.response.tempcode.TempCodeResponse; import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language; @@ -22,6 +23,10 @@ public class DefenseProblemResponse { private Language lastAccessLanguage; private Set tempCodes; + public boolean getIsCorrect() { + return isCorrect; + } + @Builder private DefenseProblemResponse(Long problemId, Long problemNumber, Long baekjoonProblemId, ProblemContent content, boolean isCorrect, Language lastAccessLanguage, diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java index 1b485a72..45c64716 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java @@ -1,5 +1,8 @@ package kr.co.morandi.backend.defense_management.application.response.session; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType; import lombok.AccessLevel; import lombok.Builder; @@ -16,6 +19,7 @@ public class StartDailyDefenseResponse { private Long defenseSessionId; private String contentName; private DefenseType defenseType; +// @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime lastAccessTime; private List defenseProblems; diff --git a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/controller/DefenseMangementControllerTest.java b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/controller/DefenseMangementControllerTest.java new file mode 100644 index 00000000..3b92d9b8 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/controller/DefenseMangementControllerTest.java @@ -0,0 +1,10 @@ +package kr.co.morandi.backend.defense_management.infrastructure.controller; + +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.*; + +class DefenseMangementControllerTest { + + +} \ No newline at end of file diff --git a/src/test/java/kr/co/morandi/backend/docs/RestDocsSupport.java b/src/test/java/kr/co/morandi/backend/docs/RestDocsSupport.java new file mode 100644 index 00000000..e35b275d --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/docs/RestDocsSupport.java @@ -0,0 +1,33 @@ +package kr.co.morandi.backend.docs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +@ExtendWith(RestDocumentationExtension.class) +public abstract class RestDocsSupport { + + protected MockMvc mockMvc; + protected ObjectMapper objectMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + @BeforeEach + void setUp(RestDocumentationContextProvider provider) { + this.mockMvc = MockMvcBuilders.standaloneSetup(initController()) + .setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper)) + .apply(documentationConfiguration(provider)) + .build(); + } + + protected abstract Object initController(); +} \ No newline at end of file diff --git a/src/test/java/kr/co/morandi/backend/docs/dailydefense/DailyDefenseControllerDocsTest.java b/src/test/java/kr/co/morandi/backend/docs/dailydefense/DailyDefenseControllerDocsTest.java new file mode 100644 index 00000000..b657fcc9 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/docs/dailydefense/DailyDefenseControllerDocsTest.java @@ -0,0 +1,96 @@ +package kr.co.morandi.backend.docs.dailydefense; + +import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; +import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseProblemInfoResponse; +import kr.co.morandi.backend.defense_information.application.port.in.DailyDefenseUseCase; +import kr.co.morandi.backend.defense_information.infrastructure.controller.DailyDefenseController; +import kr.co.morandi.backend.docs.RestDocsSupport; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.List; + +import static kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier.S5; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class DailyDefenseControllerDocsTest extends RestDocsSupport { + + private final DailyDefenseUseCase dailyDefenseUseCase = mock(DailyDefenseUseCase.class); + @Override + protected Object initController() { + return new DailyDefenseController(dailyDefenseUseCase); + } + + @DisplayName("DailyDefense 정보를 가져오는 API") + @Test + void getDailyDefenseInfo() throws Exception { + + final DailyDefenseProblemInfoResponse problem = DailyDefenseProblemInfoResponse.builder() + .problemNumber(1L) + .problemId(1L) + .baekjoonProblemId(1000L) + .difficulty(S5) + .solvedCount(1L) + .submitCount(1L) + .isSolved(true) + .build(); + + when(dailyDefenseUseCase.getDailyDefenseInfo(any(), any())) + .thenReturn(DailyDefenseInfoResponse.builder() + .problems(List.of(problem)) + .defenseName("test") + .attemptCount(1L) + .problemCount(5) + .build()); + + final ResultActions perform = mockMvc.perform(get("/daily-defense")); + + perform + .andExpect(status().isOk()) + .andExpect(jsonPath("$.problems").isArray()) + .andExpect(jsonPath("$.defenseName").isString()) + .andExpect(jsonPath("$.attemptCount").isNumber()) + .andExpect(jsonPath("$.problemCount").isNumber()) + .andDo(document("daily-defense-info", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath("defenseName").type(JsonFieldType.STRING) + .description("디펜스 이름"), + fieldWithPath("problemCount").type(JsonFieldType.NUMBER) + .description("총 문제 수"), + fieldWithPath("attemptCount").type(JsonFieldType.NUMBER) + .description("디펜스 시도 횟수"), + fieldWithPath("problems").type(JsonFieldType.ARRAY) + .description("디펜스 문제 목록"), + fieldWithPath("problems[].problemNumber").type(JsonFieldType.NUMBER) + .description("시도하는 문제 번호"), + fieldWithPath("problems[].problemId").type(JsonFieldType.NUMBER) + .description("시도하는 문제의 PK"), + fieldWithPath("problems[].baekjoonProblemId").type(JsonFieldType.NUMBER) + .description("백준 문제 번호"), + fieldWithPath("problems[].difficulty").type(JsonFieldType.STRING) + .description("문제 난이도 ex) SILVER"), + fieldWithPath("problems[].solvedCount").type(JsonFieldType.NUMBER) + .description("정답자 수"), + fieldWithPath("problems[].submitCount").type(JsonFieldType.NUMBER) + .description("제출한 사람 수"), + fieldWithPath("problems[].isSolved").type(JsonFieldType.BOOLEAN) + .optional() + .description("해당 사용자가 정답을 맞췄는지 여부, 이 필드가 없으면 아직 시도하지 않은 문제") + ) + )); + + } +} diff --git a/src/test/java/kr/co/morandi/backend/docs/dailydefense/DefenseManagementControllerDocsTest.java b/src/test/java/kr/co/morandi/backend/docs/dailydefense/DefenseManagementControllerDocsTest.java new file mode 100644 index 00000000..7f6a3772 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/docs/dailydefense/DefenseManagementControllerDocsTest.java @@ -0,0 +1,209 @@ +package kr.co.morandi.backend.docs.dailydefense; + +import kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType; +import kr.co.morandi.backend.defense_management.application.response.session.DefenseProblemResponse; +import kr.co.morandi.backend.defense_management.application.response.session.StartDailyDefenseResponse; +import kr.co.morandi.backend.defense_management.application.response.tempcode.TempCodeResponse; +import kr.co.morandi.backend.defense_management.application.service.session.DailyDefenseManagementService; +import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language; +import kr.co.morandi.backend.defense_management.infrastructure.controller.DefenseMangementController; +import kr.co.morandi.backend.defense_management.infrastructure.request.dailydefense.StartDailyDefenseRequest; +import kr.co.morandi.backend.docs.RestDocsSupport; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.SampleData; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.Subtask; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class DefenseManagementControllerDocsTest extends RestDocsSupport { + + private final DailyDefenseManagementService dailyDefenseManagementService = mock(DailyDefenseManagementService.class); + + @Override + protected Object initController() { + return new DefenseMangementController(dailyDefenseManagementService); + } + + @DisplayName("DailyDefense 정보를 가져오는 API") + @Test + void getDailyDefenseInfo() throws Exception { + Subtask subtask = Subtask.builder() + .title("Basic Cases") + .conditions(List.of("Input range 1-100", "No negative numbers")) + .tableConditionsHtml("
Condition
Input range 1-100
No negative numbers
") + .build(); + + SampleData sampleData = SampleData.builder() + .input("1 2") + .output("3") + .explanation("The output 3 is the sum of 1 and 2.") + .build(); + + ProblemContent problemContent = ProblemContent.builder() + .baekjoonProblemId(1001L) + .title("A+B Problem") + .memoryLimit("128MB") + .timeLimit("1s") + .description("Calculate A + B.") + .input("Two integers A and B.") + .output("Output of A + B.") + .samples(List.of(sampleData)) + .hint("Use simple addition.") + .subtasks(List.of(subtask)) + .problemLimit("No specific limits.") + .additionalTimeLimit("None") + .additionalJudgeInfo("Standard problem.") + .error(null) + .build(); + + TempCodeResponse java = TempCodeResponse.builder() + .language(Language.JAVA) + .code("public class Main { public static void main(String[] args) { System.out.println(1 + 1); } }") + .build(); + + TempCodeResponse cpp = TempCodeResponse.builder() + .language(Language.CPP) + .code("#include \nint main() { std::cout << 1 + 1 << std::endl; return 0; }") + .build(); + + TempCodeResponse python = TempCodeResponse.builder() + .language(Language.PYTHON) + .code("print(1 + 1)") + .build(); + + DefenseProblemResponse defenseProblemResponse = DefenseProblemResponse.builder() + .problemId(100L) + .problemNumber(1L) + .baekjoonProblemId(1001L) + .content(problemContent) + .isCorrect(true) + .lastAccessLanguage(Language.JAVA) + .tempCodes(Set.of(java, cpp, python)) + .build(); + final StartDailyDefenseResponse startDailyDefenseResponse = StartDailyDefenseResponse.builder() + .defenseSessionId(101L) + .contentName("Daily Challenge") + .defenseType(DefenseType.DAILY) + .lastAccessTime(LocalDateTime.of(2021, 4, 27, 0, 0, 0)) + .defenseProblems(List.of(defenseProblemResponse)) + .build(); + + when(dailyDefenseManagementService.startDailyDefense(any(), any(), any())) + .thenReturn(startDailyDefenseResponse); + + final StartDailyDefenseRequest request = StartDailyDefenseRequest.builder() + .problemNumber(1L) + .build(); + + final ResultActions perform = mockMvc.perform(post("/daily-defense") + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))); + + perform + .andExpect(status().isOk()) + .andDo(document("daily-defense-start", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + responseFields( + // StartDailyDefenseResponse + fieldWithPath("defenseSessionId").type(JsonFieldType.NUMBER) + .description("오늘의 문제 세션의 고유 ID"), + fieldWithPath("contentName").type(JsonFieldType.STRING) + .description("오늘의 문제 이름"), + fieldWithPath("defenseType").type(JsonFieldType.STRING) + .description("디펜스 유형"), + fieldWithPath("lastAccessTime").type(JsonFieldType.STRING) + .description("마지막으로 콘텐츠에 접근한 시간"), + fieldWithPath("defenseProblems").type(JsonFieldType.ARRAY) + .description("오늘의 문제의 문제 목록[]"), + + // DefenseProblemResponse - Array Element + fieldWithPath("defenseProblems[].problemId").type(JsonFieldType.NUMBER) + .description("문제의 고유 ID"), + fieldWithPath("defenseProblems[].problemNumber").type(JsonFieldType.NUMBER) + .description("문제 번호"), + fieldWithPath("defenseProblems[].baekjoonProblemId").type(JsonFieldType.NUMBER) + .description("백준 문제 ID"), + fieldWithPath("defenseProblems[].content").type(JsonFieldType.OBJECT) + .description("문제의 내용 상세"), + fieldWithPath("defenseProblems[].isCorrect").type(JsonFieldType.BOOLEAN) + .description("문제가 올바르게 해결되었는지 여부"), + fieldWithPath("defenseProblems[].lastAccessLanguage").type(JsonFieldType.STRING) + .description("사용된 마지막 프로그래밍 언어"), + fieldWithPath("defenseProblems[].tempCodes").type(JsonFieldType.ARRAY) + .description("문제에 대해 작성된 임시 코드[]"), + + // ProblemContent within DefenseProblemResponse + fieldWithPath("defenseProblems[].content.baekjoonProblemId").type(JsonFieldType.NUMBER) + .description("백준 문제 ID"), + fieldWithPath("defenseProblems[].content.title").type(JsonFieldType.STRING) + .description("문제의 제목"), + fieldWithPath("defenseProblems[].content.memoryLimit").type(JsonFieldType.STRING) + .description("문제의 메모리 제한"), + fieldWithPath("defenseProblems[].content.timeLimit").type(JsonFieldType.STRING) + .description("문제의 시간 제한"), + fieldWithPath("defenseProblems[].content.description").type(JsonFieldType.STRING) + .description("문제의 설명"), + fieldWithPath("defenseProblems[].content.input").type(JsonFieldType.STRING) + .description("문제의 입력 형식"), + fieldWithPath("defenseProblems[].content.output").type(JsonFieldType.STRING) + .description("문제의 출력 형식"), + fieldWithPath("defenseProblems[].content.samples").type(JsonFieldType.ARRAY) + .description("문제의 샘플 입력/출력 값 배열"), + fieldWithPath("defenseProblems[].content.samples[].input").type(JsonFieldType.STRING) + .description("문제의 샘플 입력값"), + fieldWithPath("defenseProblems[].content.samples[].output").type(JsonFieldType.STRING) + .description("문제의 샘플 출력값"), + fieldWithPath("defenseProblems[].content.samples[].explanation").type(JsonFieldType.STRING) + .optional() + .description("입출력 예제에 대한 설명"), + fieldWithPath("defenseProblems[].content.hint").type(JsonFieldType.STRING) + .optional() + .description("문제를 해결하기 위한 힌트"), + fieldWithPath("defenseProblems[].content.subtasks").type(JsonFieldType.ARRAY) + .description("서브테스크 목록"), + fieldWithPath("defenseProblems[].content.subtasks[].title").type(JsonFieldType.STRING) + .description("부분 작업의 제목"), + fieldWithPath("defenseProblems[].content.subtasks[].conditions").type(JsonFieldType.ARRAY) + .description("부분 작업의 조건 목록 (일반적으로 conditions 와 tableConditionsHtml 중 1개가 주어짐)"), + fieldWithPath("defenseProblems[].content.subtasks[].tableConditionsHtml").type(JsonFieldType.STRING) + .description("HTML 형식의 조건 표"), + fieldWithPath("defenseProblems[].content.problemLimit").type(JsonFieldType.STRING) + .optional() + .description("문제 제한"), + fieldWithPath("defenseProblems[].content.additionalTimeLimit").type(JsonFieldType.STRING) + .optional() + .description("추가 시간 제한"), + fieldWithPath("defenseProblems[].content.additionalJudgeInfo").type(JsonFieldType.STRING) + .optional() + .description("추가 채점 정보"), + fieldWithPath("defenseProblems[].content.error").type(JsonFieldType.STRING) + .optional() + .description("문제 발생 시 반환되는 오류 필드만 반환됨"), + + // TempCodeResponse within DefenseProblemResponse + fieldWithPath("defenseProblems[].tempCodes[].language").type(JsonFieldType.STRING) + .description("임시 저장된 코드의 프로그래밍 언어"), + fieldWithPath("defenseProblems[].tempCodes[].code").type(JsonFieldType.STRING) + .description("임시 저장된 코드 스니펫") + ) + )); + + } +} diff --git a/src/test/resources/org.springframework.restdocs.templates/request-fields.snippet b/src/test/resources/org.springframework.restdocs.templates/request-fields.snippet new file mode 100644 index 00000000..95aa88f3 --- /dev/null +++ b/src/test/resources/org.springframework.restdocs.templates/request-fields.snippet @@ -0,0 +1,14 @@ +==== Request Fields +|=== +|Path|Type|Optional|Description + +{{#fields}} + +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{#optional}}O{{/optional}}{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/fields}} + +|=== \ No newline at end of file diff --git a/src/test/resources/org.springframework.restdocs.templates/response-fields.snippet b/src/test/resources/org.springframework.restdocs.templates/response-fields.snippet new file mode 100644 index 00000000..795882b6 --- /dev/null +++ b/src/test/resources/org.springframework.restdocs.templates/response-fields.snippet @@ -0,0 +1,14 @@ +==== Response Fields +|=== +|Path|Type|Optional|Description + +{{#fields}} + +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{#optional}}O{{/optional}}{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/fields}} + +|=== \ No newline at end of file From be50f0e6deabf8c319053d431fc6a6b8c9c2cd2b Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Wed, 1 May 2024 15:49:48 +0900 Subject: [PATCH 40/44] =?UTF-8?q?:memo:=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EB=9E=AD=ED=82=B9=20restdocs=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/dailydefense/dailydefense.adoc | 17 ++- .../application/util/TimeFormatHelper.java | 5 + .../DailyRecordControllerDocsTest.java | 140 ++++++++++++++++++ .../DefenseManagementControllerDocsTest.java | 2 +- 4 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 src/test/java/kr/co/morandi/backend/docs/dailydefense/DailyRecordControllerDocsTest.java diff --git a/src/docs/asciidoc/api/dailydefense/dailydefense.adoc b/src/docs/asciidoc/api/dailydefense/dailydefense.adoc index bcb38d4c..95ec52fa 100644 --- a/src/docs/asciidoc/api/dailydefense/dailydefense.adoc +++ b/src/docs/asciidoc/api/dailydefense/dailydefense.adoc @@ -1,19 +1,28 @@ [[Daily-Defense]] == 오늘의 문제 정보 조회 -==== Request +=== Request include::{snippets}/daily-defense-info/http-request.adoc[] -==== Response +=== Response include::{snippets}/daily-defense-info/http-response.adoc[] include::{snippets}/daily-defense-info/response-fields.adoc[] +== 오늘의 문제 기록 조회 + +=== Request +include::{snippets}/daily-defense-ranking/http-request.adoc[] + +=== Response +include::{snippets}/daily-defense-ranking/http-response.adoc[] +include::{snippets}/daily-defense-ranking/response-fields.adoc[] + == 오늘의 문제 시작 -==== Request +=== Request include::{snippets}/daily-defense-start/http-request.adoc[] -==== Response +=== Response include::{snippets}/daily-defense-start/http-response.adoc[] include::{snippets}/daily-defense-start/response-fields.adoc[] diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelper.java b/src/main/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelper.java index c59a1853..6d6ea7ea 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelper.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/util/TimeFormatHelper.java @@ -9,4 +9,9 @@ public class TimeFormatHelper { public static String solvedTimeToString(Long solvedTime) { return String.format("%02d:%02d:%02d", solvedTime / 3600, (solvedTime % 3600) / 60, solvedTime % 60); } + + public static Long stringToSolvedTime(String solvedTime) { + String[] time = solvedTime.split(":"); + return Long.parseLong(time[0]) * 3600 + Long.parseLong(time[1]) * 60 + Long.parseLong(time[2]); + } } diff --git a/src/test/java/kr/co/morandi/backend/docs/dailydefense/DailyRecordControllerDocsTest.java b/src/test/java/kr/co/morandi/backend/docs/dailydefense/DailyRecordControllerDocsTest.java new file mode 100644 index 00000000..40b3392a --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/docs/dailydefense/DailyRecordControllerDocsTest.java @@ -0,0 +1,140 @@ +package kr.co.morandi.backend.docs.dailydefense; + +import kr.co.morandi.backend.defense_record.application.dto.DailyDefenseRankPageResponse; +import kr.co.morandi.backend.defense_record.application.dto.DailyDetailRankResponse; +import kr.co.morandi.backend.defense_record.application.dto.DailyRecordRankResponse; +import kr.co.morandi.backend.defense_record.application.port.in.DailyRecordRankUseCase; +import kr.co.morandi.backend.defense_record.application.util.TimeFormatHelper; +import kr.co.morandi.backend.defense_record.infrastructure.controller.DailyRecordController; +import kr.co.morandi.backend.docs.RestDocsSupport; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class DailyRecordControllerDocsTest extends RestDocsSupport { + + private final DailyRecordRankUseCase dailyRecordRankUseCase = mock(DailyRecordRankUseCase.class); + + @Override + protected Object initController() { + return new DailyRecordController(dailyRecordRankUseCase); + } + + @DisplayName("오늘의 문제 랭킹 반환 API") + @Test + void getDailyRecordRank() throws Exception { + // 5개 문제에 대한 세부 정보 생성 + Map allDetails = new HashMap<>(); + for (long i = 1; i <= 5; i++) { // 5개의 문제 + allDetails.put(i, DailyDetailRankResponse.builder() + .problemNumber(i) + .isSolved(i % 2 == 0) // 홀수 문제는 해결하지 못함, 짝수 문제는 해결함 + .solvedTime(TimeFormatHelper.solvedTimeToString(i * 60000)) // 문제별로 1, 2, 3, 4, 5분 소요 + .build()); + } + + // 각 유저가 5개의 문제에 대한 세부 정보를 갖도록 설정 + List records = new ArrayList<>(); + for (long i = 1; i <= 5; i++) { // 5명의 유저 + List details = allDetails.values().stream() + .collect(Collectors.toList()); + + records.add(DailyRecordRankResponse.builder() + .nickname("test" + i) + .rank(i) + .solvedCount(details.stream().filter(DailyDetailRankResponse::getIsSolved).count()) // 해결한 문제 수 + .updatedAt(LocalDateTime.now()) + .totalSolvedTime(TimeFormatHelper.solvedTimeToString(details.stream() + .mapToLong(detail -> TimeFormatHelper.stringToSolvedTime(detail.getSolvedTime())) + .sum())) + .rankDetails(details) + .build()); + + } + + DailyDefenseRankPageResponse response = DailyDefenseRankPageResponse.builder() + .dailyRecords(records) + .totalPage(1) + .currentPage(0) + .build(); + + when(dailyRecordRankUseCase.getDailyRecordRank(any(), anyInt(), anyInt())) + .thenReturn(response); + + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("page", "0"); + params.add("size", "5"); + + + final ResultActions perform = mockMvc.perform(get("/daily-record/rankings") + .params(params)); + + perform + .andExpect(status().isOk()) + .andDo(document("daily-defense-ranking", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath("dailyRecords") + .type(JsonFieldType.ARRAY) + .description("오늘의 문제 순위 기록 목록"), + fieldWithPath("dailyRecords[].nickname") + .type(JsonFieldType.STRING) + .description("사용자 닉네임"), + fieldWithPath("dailyRecords[].rank") + .type(JsonFieldType.NUMBER) + .description("사용자의 순위"), + fieldWithPath("dailyRecords[].solvedCount") + .type(JsonFieldType.NUMBER) + .description("해결한 문제 수"), + fieldWithPath("dailyRecords[].updatedAt") + .type(JsonFieldType.STRING) + .description("최근 업데이트 시간"), + fieldWithPath("dailyRecords[].totalSolvedTime") + .type(JsonFieldType.STRING) + .description("총 해결 시간"), + fieldWithPath("dailyRecords[].rankDetails") + .type(JsonFieldType.ARRAY) + .description("문제별 세부 순위 정보"), + fieldWithPath("dailyRecords[].rankDetails[].problemNumber") + .type(JsonFieldType.NUMBER) + .description("문제 번호"), + fieldWithPath("dailyRecords[].rankDetails[].isSolved") + .type(JsonFieldType.BOOLEAN) + .description("해결 여부"), + fieldWithPath("dailyRecords[].rankDetails[].solvedTime") + .type(JsonFieldType.STRING) + .description("해결 시간"), + fieldWithPath("totalPage") + .type(JsonFieldType.NUMBER) + .description("전체 페이지 수"), + fieldWithPath("currentPage") + .type(JsonFieldType.NUMBER) + .description("현재 페이지 번호") + ))); + + + } +} diff --git a/src/test/java/kr/co/morandi/backend/docs/dailydefense/DefenseManagementControllerDocsTest.java b/src/test/java/kr/co/morandi/backend/docs/dailydefense/DefenseManagementControllerDocsTest.java index 7f6a3772..01909971 100644 --- a/src/test/java/kr/co/morandi/backend/docs/dailydefense/DefenseManagementControllerDocsTest.java +++ b/src/test/java/kr/co/morandi/backend/docs/dailydefense/DefenseManagementControllerDocsTest.java @@ -40,7 +40,7 @@ protected Object initController() { return new DefenseMangementController(dailyDefenseManagementService); } - @DisplayName("DailyDefense 정보를 가져오는 API") + @DisplayName("DailyDefense를 시작하는 API") @Test void getDailyDefenseInfo() throws Exception { Subtask subtask = Subtask.builder() From fa1e93058816bd87d3c5e74aade330ebb1e593b3 Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Wed, 1 May 2024 16:19:56 +0900 Subject: [PATCH 41/44] :fire: Remove unused import --- .../response/session/StartDailyDefenseResponse.java | 4 ---- .../application/service/timer/DefenseTimerService.java | 5 ++--- .../application/port/out/dailyrecord/DailyRecordPort.java | 3 +-- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java index 45c64716..1b485a72 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/StartDailyDefenseResponse.java @@ -1,8 +1,5 @@ package kr.co.morandi.backend.defense_management.application.response.session; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType; import lombok.AccessLevel; import lombok.Builder; @@ -19,7 +16,6 @@ public class StartDailyDefenseResponse { private Long defenseSessionId; private String contentName; private DefenseType defenseType; -// @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime lastAccessTime; private List defenseProblems; diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java b/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java index 68d0bf2c..6a7bdb4b 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/service/timer/DefenseTimerService.java @@ -21,9 +21,8 @@ public void startDefenseTimer(Long defenseSessionId, LocalDateTime startDateTime long delay = Duration.between(startDateTime, endDateTime).toMillis(); - scheduler.schedule(() -> { - sessionService.terminateDefense(defenseSessionId); - }, delay, TimeUnit.MILLISECONDS); + scheduler.schedule(() -> + sessionService.terminateDefense(defenseSessionId), delay, TimeUnit.MILLISECONDS); } diff --git a/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/dailyrecord/DailyRecordPort.java b/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/dailyrecord/DailyRecordPort.java index ad590e16..ebdf1f39 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/dailyrecord/DailyRecordPort.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/application/port/out/dailyrecord/DailyRecordPort.java @@ -1,11 +1,10 @@ package kr.co.morandi.backend.defense_record.application.port.out.dailyrecord; -import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.member_management.domain.model.member.Member; import org.springframework.data.domain.Page; import java.time.LocalDate; -import java.util.List; import java.util.Optional; public interface DailyRecordPort { From 8263edff1e2bdd624d7a2d01495f7c951947aa6f Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Wed, 1 May 2024 16:25:36 +0900 Subject: [PATCH 42/44] :fire: Remove unused import --- .../response/session/DefenseProblemResponse.java | 3 +-- .../dailydefense_record/DailyRecordRepository.java | 3 +-- .../infrastructure/config/jwt/utils/JwtProvider.java | 7 ++++--- .../infrastructure/config/security/SecurityConfig.java | 2 +- .../filter/entrypoint/JwtAuthenticationEntryPoint.java | 1 - .../filter/oauth/CachedBodyHttpServletWrapper.java | 3 ++- .../security/filter/oauth/JwtAuthenticationFilter.java | 2 -- 7 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java index 2d0bc108..3c8205e4 100644 --- a/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java +++ b/src/main/java/kr/co/morandi/backend/defense_management/application/response/session/DefenseProblemResponse.java @@ -1,9 +1,8 @@ package kr.co.morandi.backend.defense_management.application.response.session; -import com.fasterxml.jackson.annotation.JsonProperty; -import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; import kr.co.morandi.backend.defense_management.application.response.tempcode.TempCodeResponse; import kr.co.morandi.backend.defense_management.domain.model.tempcode.model.Language; +import kr.co.morandi.backend.problem_information.application.response.problemcontent.ProblemContent; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepository.java b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepository.java index 505e0f5e..2bb3c0af 100644 --- a/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepository.java +++ b/src/main/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepository.java @@ -1,14 +1,13 @@ package kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record; -import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.member_management.domain.model.member.Member; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import java.time.LocalDate; -import java.util.List; import java.util.Optional; public interface DailyRecordRepository extends JpaRepository { diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtProvider.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtProvider.java index f6f0d252..f3bdacea 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtProvider.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/jwt/utils/JwtProvider.java @@ -1,10 +1,10 @@ package kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils; -import io.jsonwebtoken.*; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; -import kr.co.morandi.backend.common.exception.MorandiException; -import kr.co.morandi.backend.member_management.infrastructure.exception.OAuthErrorCode; import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.member_management.domain.model.member.Role; import kr.co.morandi.backend.member_management.infrastructure.config.jwt.constants.TokenType; @@ -14,6 +14,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.util.WebUtils; + import java.security.PrivateKey; import java.util.Date; diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java index 95f2df9d..8b7d5b95 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/config/security/SecurityConfig.java @@ -40,7 +40,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/daily-record/rankings/**").permitAll() .requestMatchers(GET, "/daily-defense/**").permitAll() .anyRequest().authenticated()) - .exceptionHandling((exceptionHandling) -> exceptionHandling + .exceptionHandling(exceptionHandling -> exceptionHandling .authenticationEntryPoint(jwtAuthenticationEntryPoint) ) .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/entrypoint/JwtAuthenticationEntryPoint.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/entrypoint/JwtAuthenticationEntryPoint.java index 767cdcfa..9f24f76c 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/entrypoint/JwtAuthenticationEntryPoint.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/entrypoint/JwtAuthenticationEntryPoint.java @@ -8,7 +8,6 @@ import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.JwtProvider; import kr.co.morandi.backend.member_management.infrastructure.exception.OAuthErrorCode; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/CachedBodyHttpServletWrapper.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/CachedBodyHttpServletWrapper.java index e38676e2..51c6231c 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/CachedBodyHttpServletWrapper.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/CachedBodyHttpServletWrapper.java @@ -8,6 +8,7 @@ import org.springframework.util.StreamUtils; import java.io.*; +import java.nio.charset.StandardCharsets; public class CachedBodyHttpServletWrapper extends HttpServletRequestWrapper { private final byte[] cachedBody; @@ -26,7 +27,7 @@ public ServletInputStream getInputStream() throws IOException { @Override public BufferedReader getReader() throws IOException { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody); - return new BufferedReader(new InputStreamReader(byteArrayInputStream, "UTF-8")); + return new BufferedReader(new InputStreamReader(byteArrayInputStream, StandardCharsets.UTF_8)); } public static class CachedBodyServletInputStream extends ServletInputStream { diff --git a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/JwtAuthenticationFilter.java b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/JwtAuthenticationFilter.java index dd078f53..e0c2aebd 100644 --- a/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/JwtAuthenticationFilter.java +++ b/src/main/java/kr/co/morandi/backend/member_management/infrastructure/security/filter/oauth/JwtAuthenticationFilter.java @@ -4,12 +4,10 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import kr.co.morandi.backend.common.exception.MorandiException; import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.JwtProvider; import kr.co.morandi.backend.member_management.infrastructure.config.jwt.utils.JwtValidator; import kr.co.morandi.backend.member_management.infrastructure.config.security.utils.AuthenticationProvider; import kr.co.morandi.backend.member_management.infrastructure.config.security.utils.IgnoredURIManager; -import kr.co.morandi.backend.member_management.infrastructure.exception.OAuthErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; From a6436aaa7723f569abd5064ea6864eaa8a87d95f Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Thu, 2 May 2024 13:09:09 +0900 Subject: [PATCH 43/44] =?UTF-8?q?:white=5Fcheck=5Fmark:=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=99=98=EA=B2=BD=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/ControllerTestSupport.java | 44 +++++++++++++++++++ .../backend/IntegrationTestSupport.java | 9 ++++ .../backend/NewMorandiApplicationTests.java | 4 +- .../service/DailyDefenseUseCaseImplTest.java | 10 ++--- .../DailyDefenseGenerationServiceTest.java | 10 ++--- .../defense/ProblemGenerationServiceTest.java | 5 +-- .../DailyDefenseProblemAdapterTest.java | 10 ++--- .../DailyDefenseControllerTest.java | 27 +----------- .../algorithm/AlgorithmRepositoryTest.java | 8 ++-- .../CustomDefenseRepositoryTest.java | 11 ++--- .../DailyDefenseProblemRepositoryTest.java | 11 ++--- .../RandomDefenseRepositoryTest.java | 10 ++--- .../out/session/DefenseSessionPortTest.java | 14 +++--- .../DailyDefenseManagementServiceTest.java | 7 +-- .../service/DefenseEventServiceTest.java | 7 +-- .../domain/service/SessionServiceTest.java | 9 ++-- .../session/DefenseSessionAdapterTest.java | 9 ++-- .../DailyRecordRankUseCaseTest.java | 7 +-- .../DailyRecordAdapterTest.java | 17 +++---- .../adapter/record/RecordAdapterTest.java | 7 +-- .../controller/DailyRecordControllerTest.java | 28 +----------- .../DailyRecordRepositoryTest.java | 7 +-- .../member/MemberRepositoryTest.java | 8 ++-- .../adapter/problem/ProblemAdapterTest.java | 6 +-- .../config/AlgorithmInitializerTest.java | 7 +-- .../problem/ProblemRepositoryTest.java | 8 +--- 26 files changed, 119 insertions(+), 181 deletions(-) create mode 100644 src/test/java/kr/co/morandi/backend/ControllerTestSupport.java create mode 100644 src/test/java/kr/co/morandi/backend/IntegrationTestSupport.java diff --git a/src/test/java/kr/co/morandi/backend/ControllerTestSupport.java b/src/test/java/kr/co/morandi/backend/ControllerTestSupport.java new file mode 100644 index 00000000..815d75c2 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/ControllerTestSupport.java @@ -0,0 +1,44 @@ +package kr.co.morandi.backend; + +import kr.co.morandi.backend.defense_information.application.port.in.DailyDefenseUseCase; +import kr.co.morandi.backend.defense_information.infrastructure.controller.DailyDefenseController; +import kr.co.morandi.backend.defense_record.application.port.in.DailyRecordRankUseCase; +import kr.co.morandi.backend.defense_record.infrastructure.controller.DailyRecordController; +import kr.co.morandi.backend.member_management.infrastructure.config.cookie.utils.CookieUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.filter.OncePerRequestFilter; + +@WebMvcTest(controllers = { + DailyDefenseController.class, + DailyRecordController.class +}, + excludeAutoConfiguration = SecurityAutoConfiguration.class, + excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = { + OncePerRequestFilter.class + }) +} +) +@ActiveProfiles("test") +public abstract class ControllerTestSupport { + + @Autowired + protected MockMvc mockMvc; + + @MockBean + protected DailyDefenseUseCase dailyDefenseUseCase; + + @MockBean + protected CookieUtils cookieUtils; + + @MockBean + protected DailyRecordRankUseCase dailyRecordRankUseCase; + +} diff --git a/src/test/java/kr/co/morandi/backend/IntegrationTestSupport.java b/src/test/java/kr/co/morandi/backend/IntegrationTestSupport.java new file mode 100644 index 00000000..cc5ed845 --- /dev/null +++ b/src/test/java/kr/co/morandi/backend/IntegrationTestSupport.java @@ -0,0 +1,9 @@ +package kr.co.morandi.backend; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@SpringBootTest +public abstract class IntegrationTestSupport { +} diff --git a/src/test/java/kr/co/morandi/backend/NewMorandiApplicationTests.java b/src/test/java/kr/co/morandi/backend/NewMorandiApplicationTests.java index 15f09e85..2ac08c14 100644 --- a/src/test/java/kr/co/morandi/backend/NewMorandiApplicationTests.java +++ b/src/test/java/kr/co/morandi/backend/NewMorandiApplicationTests.java @@ -1,10 +1,8 @@ package kr.co.morandi.backend; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest -class NewMorandiApplicationTests { +class NewMorandiApplicationTests extends IntegrationTestSupport{ @Test void contextLoads() { diff --git a/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java b/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java index 91d0b52b..8068edc0 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/application/service/DailyDefenseUseCaseImplTest.java @@ -1,9 +1,9 @@ package kr.co.morandi.backend.defense_information.application.service; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; import kr.co.morandi.backend.defense_information.application.port.in.DailyDefenseUseCase; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; -import kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier; import kr.co.morandi.backend.defense_information.domain.service.defense.ProblemGenerationService; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; @@ -15,8 +15,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -30,12 +28,10 @@ import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; -@SpringBootTest @Transactional -@ActiveProfiles("test") -class DailyDefenseUseCaseImplTest { +class DailyDefenseUseCaseImplTest extends IntegrationTestSupport { @Autowired private MemberRepository memberRepository; diff --git a/src/test/java/kr/co/morandi/backend/defense_information/domain/service/dailydefense/DailyDefenseGenerationServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_information/domain/service/dailydefense/DailyDefenseGenerationServiceTest.java index af6109f3..b060f6f6 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/domain/service/dailydefense/DailyDefenseGenerationServiceTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/domain/service/dailydefense/DailyDefenseGenerationServiceTest.java @@ -1,18 +1,16 @@ package kr.co.morandi.backend.defense_information.domain.service.dailydefense; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.application.port.out.dailydefense.DailyDefensePort; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; -import kr.co.morandi.backend.defense_information.domain.service.dailydefense.DailyDefenseGenerationService; -import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.time.LocalDateTime; import java.util.List; @@ -21,9 +19,7 @@ import static kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier.*; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest -@ActiveProfiles("test") -class DailyDefenseGenerationServiceTest { +class DailyDefenseGenerationServiceTest extends IntegrationTestSupport { @Autowired private DailyDefenseGenerationService dailyDefenseGenerationService; diff --git a/src/test/java/kr/co/morandi/backend/defense_information/domain/service/defense/ProblemGenerationServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_information/domain/service/defense/ProblemGenerationServiceTest.java index 362a3c01..24448372 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/domain/service/defense/ProblemGenerationServiceTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/domain/service/defense/ProblemGenerationServiceTest.java @@ -1,5 +1,6 @@ package kr.co.morandi.backend.defense_information.domain.service.defense; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.service.defense.ProblemGenerationService; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; @@ -23,9 +24,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; -@SpringBootTest -@ActiveProfiles("test") -class ProblemGenerationServiceTest { +class ProblemGenerationServiceTest extends IntegrationTestSupport { @Autowired private ProblemGenerationService problemGenerationService; diff --git a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/adapter/dailydefense/DailyDefenseProblemAdapterTest.java b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/adapter/dailydefense/DailyDefenseProblemAdapterTest.java index 7cd85c8d..c455d76a 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/adapter/dailydefense/DailyDefenseProblemAdapterTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/adapter/dailydefense/DailyDefenseProblemAdapterTest.java @@ -1,18 +1,16 @@ package kr.co.morandi.backend.defense_information.infrastructure.adapter.dailydefense; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; -import kr.co.morandi.backend.defense_information.infrastructure.adapter.dailydefense.DailyDefenseProblemAdapter; import kr.co.morandi.backend.defense_information.domain.model.defense.RandomCriteria; -import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.time.LocalDate; import java.util.List; @@ -22,9 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; -@SpringBootTest -@ActiveProfiles("test") -class DailyDefenseProblemAdapterTest { +class DailyDefenseProblemAdapterTest extends IntegrationTestSupport { @Autowired private ProblemRepository problemRepository; diff --git a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java index 501d81ea..ff01117a 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/controller/DailyDefenseControllerTest.java @@ -1,20 +1,10 @@ package kr.co.morandi.backend.defense_information.infrastructure.controller; +import kr.co.morandi.backend.ControllerTestSupport; import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse; -import kr.co.morandi.backend.defense_information.application.port.in.DailyDefenseUseCase; -import kr.co.morandi.backend.member_management.infrastructure.config.cookie.utils.CookieUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import org.springframework.web.filter.OncePerRequestFilter; import java.util.List; @@ -24,21 +14,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(controllers = DailyDefenseController.class, - excludeAutoConfiguration = SecurityAutoConfiguration.class, - excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {OncePerRequestFilter.class})}) -@ActiveProfiles("test") -class DailyDefenseControllerTest { +class DailyDefenseControllerTest extends ControllerTestSupport { - @Autowired - private MockMvc mockMvc; - - @MockBean - private DailyDefenseUseCase dailyDefenseUseCase; - - @MockBean - private CookieUtils cookieUtils; @DisplayName("DailyDefense 정보를 로그인하지 않은 상태에서 가져올 수 있다.") @Test diff --git a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/algorithm/AlgorithmRepositoryTest.java b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/algorithm/AlgorithmRepositoryTest.java index 05e19131..836a5485 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/algorithm/AlgorithmRepositoryTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/algorithm/AlgorithmRepositoryTest.java @@ -1,21 +1,19 @@ package kr.co.morandi.backend.defense_information.infrastructure.persistence.algorithm; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.problem_information.domain.model.algorithm.Algorithm; import kr.co.morandi.backend.problem_information.infrastructure.persistence.algorithm.AlgorithmRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest -@ActiveProfiles("test") -class AlgorithmRepositoryTest { + +class AlgorithmRepositoryTest extends IntegrationTestSupport { @Autowired private AlgorithmRepository algorithmRepository; diff --git a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/customdefense/CustomDefenseRepositoryTest.java b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/customdefense/CustomDefenseRepositoryTest.java index eaa69588..69b07601 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/customdefense/CustomDefenseRepositoryTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/customdefense/CustomDefenseRepositoryTest.java @@ -1,8 +1,7 @@ package kr.co.morandi.backend.defense_information.infrastructure.persistence.customdefense; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.domain.model.customdefense.CustomDefense; -import kr.co.morandi.backend.defense_information.infrastructure.persistence.customdefense.CustomDefenseProblemRepository; -import kr.co.morandi.backend.defense_information.infrastructure.persistence.customdefense.CustomDefenseRepository; import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; @@ -11,23 +10,19 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.time.LocalDateTime; import java.util.List; -import static kr.co.morandi.backend.defense_information.domain.model.defense.DefenseTier.*; import static kr.co.morandi.backend.defense_information.domain.model.customdefense.Visibility.CLOSE; import static kr.co.morandi.backend.defense_information.domain.model.customdefense.Visibility.OPEN; +import static kr.co.morandi.backend.defense_information.domain.model.defense.DefenseTier.*; import static kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier.*; import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; -@SpringBootTest -@ActiveProfiles("test") -class CustomDefenseRepositoryTest { +class CustomDefenseRepositoryTest extends IntegrationTestSupport { @Autowired private CustomDefenseRepository customDefenseRepository; diff --git a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/dailydefense/DailyDefenseProblemRepositoryTest.java b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/dailydefense/DailyDefenseProblemRepositoryTest.java index 86c953b4..4cdcc50d 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/dailydefense/DailyDefenseProblemRepositoryTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/dailydefense/DailyDefenseProblemRepositoryTest.java @@ -1,18 +1,15 @@ package kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense; -import kr.co.morandi.backend.defense_information.domain.model.defense.Defense; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; -import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; -import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; +import kr.co.morandi.backend.defense_information.domain.model.defense.Defense; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.time.LocalDate; import java.util.List; @@ -24,9 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; -@SpringBootTest -@ActiveProfiles("test") -class DailyDefenseProblemRepositoryTest { +class DailyDefenseProblemRepositoryTest extends IntegrationTestSupport { @Autowired private DailyDefenseProblemRepository dailyDefenseProblemRepository; diff --git a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/randomdefense/RandomDefenseRepositoryTest.java b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/randomdefense/RandomDefenseRepositoryTest.java index 1188f548..bc9cd7b8 100644 --- a/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/randomdefense/RandomDefenseRepositoryTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_information/infrastructure/persistence/randomdefense/RandomDefenseRepositoryTest.java @@ -1,15 +1,13 @@ package kr.co.morandi.backend.defense_information.infrastructure.persistence.randomdefense; -import kr.co.morandi.backend.defense_information.domain.model.randomdefense.model.RandomDefense; -import kr.co.morandi.backend.defense_information.infrastructure.persistence.randomdefense.RandomDefenseRepository; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.domain.model.defense.RandomCriteria; +import kr.co.morandi.backend.defense_information.domain.model.randomdefense.model.RandomDefense; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.util.List; @@ -17,9 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; -@SpringBootTest -@ActiveProfiles("test") -class RandomDefenseRepositoryTest { +class RandomDefenseRepositoryTest extends IntegrationTestSupport { @Autowired private RandomDefenseRepository randomDefenseRepository; diff --git a/src/test/java/kr/co/morandi/backend/defense_management/application/port/out/session/DefenseSessionPortTest.java b/src/test/java/kr/co/morandi/backend/defense_management/application/port/out/session/DefenseSessionPortTest.java index e5d21651..ce1d4802 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/application/port/out/session/DefenseSessionPortTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/application/port/out/session/DefenseSessionPortTest.java @@ -1,22 +1,20 @@ package kr.co.morandi.backend.defense_management.application.port.out.session; -import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; -import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; -import kr.co.morandi.backend.member_management.domain.model.member.Member; -import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; +import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; import kr.co.morandi.backend.defense_management.infrastructure.persistence.session.DefenseSessionRepository; +import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.time.LocalDate; import java.time.LocalDateTime; @@ -29,9 +27,7 @@ import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest -@ActiveProfiles("test") -class DefenseSessionPortTest { +class DefenseSessionPortTest extends IntegrationTestSupport { @Autowired private DefenseSessionPort defenseSessionPort; diff --git a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java index aef94860..a9569415 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/application/service/dailydefense/DailyDefenseManagementServiceTest.java @@ -1,5 +1,6 @@ package kr.co.morandi.backend.defense_management.application.service.dailydefense; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; @@ -24,9 +25,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.event.ApplicationEvents; import org.springframework.test.context.event.RecordApplicationEvents; import org.springframework.transaction.support.TransactionTemplate; @@ -49,10 +48,8 @@ import static org.mockito.Mockito.*; -@SpringBootTest -@ActiveProfiles("test") @RecordApplicationEvents -class DailyDefenseManagementServiceTest { +class DailyDefenseManagementServiceTest extends IntegrationTestSupport { @Autowired private DailyDefenseManagementService dailyDefenseManagementService; diff --git a/src/test/java/kr/co/morandi/backend/defense_management/domain/service/DefenseEventServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_management/domain/service/DefenseEventServiceTest.java index ca1dfb22..3347b6be 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/domain/service/DefenseEventServiceTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/domain/service/DefenseEventServiceTest.java @@ -1,23 +1,20 @@ package kr.co.morandi.backend.defense_management.domain.service; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_management.application.service.timer.DefenseTimerService; import kr.co.morandi.backend.defense_management.domain.event.DefenseStartTimerEvent; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.support.TransactionTemplate; import java.time.LocalDateTime; import static org.mockito.Mockito.*; -@SpringBootTest -@ActiveProfiles("test") -class DefenseEventServiceTest { +class DefenseEventServiceTest extends IntegrationTestSupport { @Autowired private ApplicationEventPublisher publisher; diff --git a/src/test/java/kr/co/morandi/backend/defense_management/domain/service/SessionServiceTest.java b/src/test/java/kr/co/morandi/backend/defense_management/domain/service/SessionServiceTest.java index 4b2e7fba..7f616e14 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/domain/service/SessionServiceTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/domain/service/SessionServiceTest.java @@ -1,5 +1,6 @@ package kr.co.morandi.backend.defense_management.domain.service; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.common.exception.MorandiException; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; @@ -18,8 +19,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -33,13 +32,11 @@ import static kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier.*; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; -@SpringBootTest @Transactional -@ActiveProfiles("test") -class SessionServiceTest { +class SessionServiceTest extends IntegrationTestSupport { @Autowired private MemberRepository memberRepository; diff --git a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/session/DefenseSessionAdapterTest.java b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/session/DefenseSessionAdapterTest.java index 1b2db030..e0682886 100644 --- a/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/session/DefenseSessionAdapterTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_management/infrastructure/adapter/session/DefenseSessionAdapterTest.java @@ -1,18 +1,17 @@ package kr.co.morandi.backend.defense_management.infrastructure.adapter.session; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_management.application.port.out.session.DefenseSessionPort; import kr.co.morandi.backend.defense_management.domain.model.session.DefenseSession; -import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.defense_management.infrastructure.persistence.session.DefenseSessionRepository; import kr.co.morandi.backend.defense_management.infrastructure.persistence.session.SessionDetailRepository; +import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.member_management.domain.model.member.SocialType; import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.time.LocalDateTime; import java.util.Optional; @@ -21,9 +20,7 @@ import static kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType.DAILY; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest -@ActiveProfiles("test") -class DefenseSessionAdapterTest { +class DefenseSessionAdapterTest extends IntegrationTestSupport { @Autowired private DefenseSessionPort defenseSessionPort; diff --git a/src/test/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseTest.java b/src/test/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseTest.java index 5f937e59..6ab76909 100644 --- a/src/test/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_record/application/DailyRecordRankUseCaseTest.java @@ -1,5 +1,6 @@ package kr.co.morandi.backend.defense_record.application; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; @@ -14,8 +15,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -31,10 +30,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; -@SpringBootTest @Transactional -@ActiveProfiles("test") -class DailyRecordRankUseCaseTest { +class DailyRecordRankUseCaseTest extends IntegrationTestSupport { @Autowired private DailyRecordRankUseCase dailyRecordRankUseCase; diff --git a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense_record/DailyRecordAdapterTest.java b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense_record/DailyRecordAdapterTest.java index 84711ca6..80a5ec0b 100644 --- a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense_record/DailyRecordAdapterTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/dailydefense_record/DailyRecordAdapterTest.java @@ -1,21 +1,20 @@ package kr.co.morandi.backend.defense_record.infrastructure.adapter.dailydefense_record; -import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; -import kr.co.morandi.backend.member_management.domain.model.member.Member; -import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; -import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; +import kr.co.morandi.backend.defense_record.application.port.out.dailyrecord.DailyRecordPort; +import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord; +import kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record.DailyRecordRepository; +import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.member_management.infrastructure.persistence.member.MemberRepository; +import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; -import kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record.DailyRecordRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.time.LocalDate; import java.time.LocalDateTime; @@ -30,9 +29,7 @@ import static kr.co.morandi.backend.member_management.domain.model.member.SocialType.GOOGLE; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest -@ActiveProfiles("test") -class DailyRecordAdapterTest { +class DailyRecordAdapterTest extends IntegrationTestSupport { @Autowired private DailyRecordPort dailyRecordPort; diff --git a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/record/RecordAdapterTest.java b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/record/RecordAdapterTest.java index ad05eec5..33b0b957 100644 --- a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/record/RecordAdapterTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/adapter/record/RecordAdapterTest.java @@ -1,5 +1,6 @@ package kr.co.morandi.backend.defense_record.infrastructure.adapter.record; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; @@ -14,8 +15,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -30,10 +29,8 @@ import static kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier.*; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest @Transactional -@ActiveProfiles("test") -class RecordAdapterTest { +class RecordAdapterTest extends IntegrationTestSupport { @Autowired private MemberRepository memberRepository; diff --git a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java index 899973ff..017492bc 100644 --- a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/controller/DailyRecordControllerTest.java @@ -1,20 +1,10 @@ package kr.co.morandi.backend.defense_record.infrastructure.controller; +import kr.co.morandi.backend.ControllerTestSupport; import kr.co.morandi.backend.defense_record.application.dto.DailyDefenseRankPageResponse; -import kr.co.morandi.backend.defense_record.application.port.in.DailyRecordRankUseCase; -import kr.co.morandi.backend.member_management.infrastructure.config.cookie.utils.CookieUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import org.springframework.web.filter.OncePerRequestFilter; import java.util.List; @@ -25,21 +15,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(controllers = DailyRecordController.class, - excludeAutoConfiguration = SecurityAutoConfiguration.class, - excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {OncePerRequestFilter.class})}) -@ActiveProfiles("test") -class DailyRecordControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private DailyRecordRankUseCase dailyRecordRankUseCase; - - @MockBean - private CookieUtils cookieUtils; +class DailyRecordControllerTest extends ControllerTestSupport { @DisplayName("[GET] DailyDefense 순위를 조회한다.") @Test diff --git a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepositoryTest.java b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepositoryTest.java index 0645fde1..d60a3986 100644 --- a/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepositoryTest.java +++ b/src/test/java/kr/co/morandi/backend/defense_record/infrastructure/persistence/dailydefense_record/DailyRecordRepositoryTest.java @@ -1,5 +1,6 @@ package kr.co.morandi.backend.defense_record.infrastructure.persistence.dailydefense_record; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseRepository; @@ -11,11 +12,9 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -32,10 +31,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; -@SpringBootTest @Transactional -@ActiveProfiles("test") -class DailyRecordRepositoryTest { +class DailyRecordRepositoryTest extends IntegrationTestSupport { @Autowired private DailyRecordRepository dailyRecordRepository; diff --git a/src/test/java/kr/co/morandi/backend/member_management/infrastructure/persistence/member/MemberRepositoryTest.java b/src/test/java/kr/co/morandi/backend/member_management/infrastructure/persistence/member/MemberRepositoryTest.java index 1a637980..5f821f53 100644 --- a/src/test/java/kr/co/morandi/backend/member_management/infrastructure/persistence/member/MemberRepositoryTest.java +++ b/src/test/java/kr/co/morandi/backend/member_management/infrastructure/persistence/member/MemberRepositoryTest.java @@ -1,20 +1,18 @@ package kr.co.morandi.backend.member_management.infrastructure.persistence.member; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.member_management.domain.model.member.Member; import kr.co.morandi.backend.member_management.domain.model.member.SocialType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.test.context.ActiveProfiles; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -@SpringBootTest -@ActiveProfiles("test") -class MemberRepositoryTest { +class MemberRepositoryTest extends IntegrationTestSupport { @Autowired private MemberRepository memberRepository; diff --git a/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problem/ProblemAdapterTest.java b/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problem/ProblemAdapterTest.java index 1154e496..dfa221a5 100644 --- a/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problem/ProblemAdapterTest.java +++ b/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/adapter/problem/ProblemAdapterTest.java @@ -1,9 +1,9 @@ package kr.co.morandi.backend.problem_information.infrastructure.adapter.problem; -import org.springframework.boot.test.context.SpringBootTest; +import kr.co.morandi.backend.IntegrationTestSupport; -@SpringBootTest -class ProblemAdapterTest { + +class ProblemAdapterTest extends IntegrationTestSupport { } \ No newline at end of file diff --git a/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/config/AlgorithmInitializerTest.java b/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/config/AlgorithmInitializerTest.java index dff7a15b..85c0c152 100644 --- a/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/config/AlgorithmInitializerTest.java +++ b/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/config/AlgorithmInitializerTest.java @@ -1,5 +1,6 @@ package kr.co.morandi.backend.problem_information.infrastructure.config; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.problem_information.domain.model.algorithm.Algorithm; import kr.co.morandi.backend.problem_information.infrastructure.initializer.AlgorithmInitializer; import kr.co.morandi.backend.problem_information.infrastructure.persistence.algorithm.AlgorithmRepository; @@ -7,8 +8,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.io.IOException; import java.util.List; @@ -16,9 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; -@SpringBootTest -@ActiveProfiles("test") -class AlgorithmInitializerTest { +class AlgorithmInitializerTest extends IntegrationTestSupport { @Autowired private AlgorithmInitializer algorithmInitializer; diff --git a/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/persistence/problem/ProblemRepositoryTest.java b/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/persistence/problem/ProblemRepositoryTest.java index a8fd239d..17d30b3e 100644 --- a/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/persistence/problem/ProblemRepositoryTest.java +++ b/src/test/java/kr/co/morandi/backend/problem_information/infrastructure/persistence/problem/ProblemRepositoryTest.java @@ -1,15 +1,13 @@ package kr.co.morandi.backend.problem_information.infrastructure.persistence.problem; +import kr.co.morandi.backend.IntegrationTestSupport; import kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; -import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; -import org.springframework.test.context.ActiveProfiles; import java.util.List; @@ -18,9 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; -@SpringBootTest -@ActiveProfiles("test") -class ProblemRepositoryTest { +class ProblemRepositoryTest extends IntegrationTestSupport { @Autowired private ProblemRepository problemRepository; From fb738f4bc88820b56c4e17156a1ed83548961fcb Mon Sep 17 00:00:00 2001 From: miiiinju1 Date: Fri, 3 May 2024 17:27:56 +0900 Subject: [PATCH 44/44] =?UTF-8?q?:art:=20DailyDefenseProblemStrategy?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/dailydefense/DailyDefenseProblemPort.java | 4 ++++ ...Strategy.java => DailyDefenseProblemStrategy.java} | 9 ++++----- .../dailydefense/DailyDefenseProblemAdapter.java | 11 ++++++++++- 3 files changed, 18 insertions(+), 6 deletions(-) rename src/main/java/kr/co/morandi/backend/defense_information/domain/service/dailydefense/{DailyDefenseStrategy.java => DailyDefenseProblemStrategy.java} (75%) diff --git a/src/main/java/kr/co/morandi/backend/defense_information/application/port/out/dailydefense/DailyDefenseProblemPort.java b/src/main/java/kr/co/morandi/backend/defense_information/application/port/out/dailydefense/DailyDefenseProblemPort.java index bc14f190..d05ffef0 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/application/port/out/dailydefense/DailyDefenseProblemPort.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/application/port/out/dailydefense/DailyDefenseProblemPort.java @@ -1,11 +1,15 @@ package kr.co.morandi.backend.defense_information.application.port.out.dailydefense; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; import kr.co.morandi.backend.defense_information.domain.model.defense.RandomCriteria; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; +import java.util.List; import java.util.Map; public interface DailyDefenseProblemPort { Map getDailyDefenseProblem(Map criteria); + + List findAllProblemsContainsDefenseId(Long defenseId); } diff --git a/src/main/java/kr/co/morandi/backend/defense_information/domain/service/dailydefense/DailyDefenseStrategy.java b/src/main/java/kr/co/morandi/backend/defense_information/domain/service/dailydefense/DailyDefenseProblemStrategy.java similarity index 75% rename from src/main/java/kr/co/morandi/backend/defense_information/domain/service/dailydefense/DailyDefenseStrategy.java rename to src/main/java/kr/co/morandi/backend/defense_information/domain/service/dailydefense/DailyDefenseProblemStrategy.java index b366f089..07b3fed5 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/domain/service/dailydefense/DailyDefenseStrategy.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/domain/service/dailydefense/DailyDefenseProblemStrategy.java @@ -1,10 +1,10 @@ package kr.co.morandi.backend.defense_information.domain.service.dailydefense; +import kr.co.morandi.backend.defense_information.application.port.out.dailydefense.DailyDefenseProblemPort; import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; import kr.co.morandi.backend.defense_information.domain.model.defense.Defense; import kr.co.morandi.backend.defense_information.domain.model.defense.DefenseType; import kr.co.morandi.backend.defense_information.domain.model.problem_generation_strategy.ProblemGenerationStrategy; -import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -17,13 +17,12 @@ @Component @RequiredArgsConstructor -public class DailyDefenseStrategy implements ProblemGenerationStrategy { +public class DailyDefenseProblemStrategy implements ProblemGenerationStrategy { - //TODO 여기도 Port로 바꿔야함 - private final DailyDefenseProblemRepository dailyDefenseProblemRepository; + private final DailyDefenseProblemPort dailyDefenseProblemPort; @Override public Map generateDefenseProblems(Defense defense) { - final List defenseProblems = dailyDefenseProblemRepository.findAllProblemsContainsDefenseId(defense.getDefenseId()); + final List defenseProblems = dailyDefenseProblemPort.findAllProblemsContainsDefenseId(defense.getDefenseId()); return defenseProblems.stream() .collect(Collectors.toMap(DailyDefenseProblem::getProblemNumber, DailyDefenseProblem::getProblem)); } diff --git a/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/adapter/dailydefense/DailyDefenseProblemAdapter.java b/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/adapter/dailydefense/DailyDefenseProblemAdapter.java index be2b09ae..1e18aef5 100644 --- a/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/adapter/dailydefense/DailyDefenseProblemAdapter.java +++ b/src/main/java/kr/co/morandi/backend/defense_information/infrastructure/adapter/dailydefense/DailyDefenseProblemAdapter.java @@ -1,8 +1,10 @@ package kr.co.morandi.backend.defense_information.infrastructure.adapter.dailydefense; import kr.co.morandi.backend.defense_information.application.port.out.dailydefense.DailyDefenseProblemPort; -import kr.co.morandi.backend.defense_information.domain.model.defense.RandomCriteria; +import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefenseProblem; import kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier; +import kr.co.morandi.backend.defense_information.domain.model.defense.RandomCriteria; +import kr.co.morandi.backend.defense_information.infrastructure.persistence.dailydefense.DailyDefenseProblemRepository; import kr.co.morandi.backend.problem_information.domain.model.problem.Problem; import kr.co.morandi.backend.problem_information.infrastructure.persistence.problem.ProblemRepository; import lombok.RequiredArgsConstructor; @@ -20,6 +22,8 @@ public class DailyDefenseProblemAdapter implements DailyDefenseProblemPort { private final ProblemRepository problemRepository; + private final DailyDefenseProblemRepository dailyDefenseProblemRepository; + @Override public Map getDailyDefenseProblem(Map criteria) { @@ -42,5 +46,10 @@ public Map getDailyDefenseProblem(Map crite }) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } + + @Override + public List findAllProblemsContainsDefenseId(Long defenseId) { + return dailyDefenseProblemRepository.findAllProblemsContainsDefenseId(defenseId); + } }