diff --git a/jabiseo-api/src/main/java/com/jabiseo/problem/controller/BookmarkController.java b/jabiseo-api/src/main/java/com/jabiseo/problem/controller/BookmarkController.java index 0a30f5f..a0a77d4 100644 --- a/jabiseo-api/src/main/java/com/jabiseo/problem/controller/BookmarkController.java +++ b/jabiseo-api/src/main/java/com/jabiseo/problem/controller/BookmarkController.java @@ -24,7 +24,8 @@ public class BookmarkController { public ResponseEntity createBookmark( @RequestBody CreateBookmarkRequest request ) { - String bookmarkId = createBookmarkUseCase.execute(request); + String memberId = "1"; // TODO : 로그인 기능 구현 후 로그인한 사용자의 ID로 변경 + String bookmarkId = createBookmarkUseCase.execute(memberId, request); URI location = ServletUriComponentsBuilder .fromCurrentRequest() @@ -39,7 +40,8 @@ public ResponseEntity createBookmark( public ResponseEntity deleteBookmark( @RequestBody DeleteBookmarkRequest request ) { - deleteBookmarkUseCase.execute(request); + String memberId = "1"; // TODO : 로그인 기능 구현 후 로그인한 사용자의 ID로 변경 + deleteBookmarkUseCase.execute(memberId, request); return ResponseEntity.noContent().build(); } } diff --git a/jabiseo-api/src/main/java/com/jabiseo/problem/usecase/CreateBookmarkUseCase.java b/jabiseo-api/src/main/java/com/jabiseo/problem/usecase/CreateBookmarkUseCase.java index 3744578..3bbf1ba 100644 --- a/jabiseo-api/src/main/java/com/jabiseo/problem/usecase/CreateBookmarkUseCase.java +++ b/jabiseo-api/src/main/java/com/jabiseo/problem/usecase/CreateBookmarkUseCase.java @@ -1,12 +1,41 @@ package com.jabiseo.problem.usecase; +import com.jabiseo.member.domain.Member; +import com.jabiseo.member.domain.MemberRepository; +import com.jabiseo.problem.domain.Bookmark; +import com.jabiseo.problem.domain.BookmarkRepository; +import com.jabiseo.problem.domain.Problem; +import com.jabiseo.problem.domain.ProblemRepository; import com.jabiseo.problem.dto.CreateBookmarkRequest; +import com.jabiseo.problem.exception.ProblemBusinessException; +import com.jabiseo.problem.exception.ProblemErrorCode; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service +@Transactional +@RequiredArgsConstructor public class CreateBookmarkUseCase { - public String execute(CreateBookmarkRequest request) { - return "bookmarkId"; + private final MemberRepository memberRepository; + + private final ProblemRepository problemRepository; + + private final BookmarkRepository bookmarkRepository; + + public String execute(String memberId, CreateBookmarkRequest request) { + if (bookmarkRepository.existsByMemberIdAndProblemId(memberId, request.problemId())) { + throw new ProblemBusinessException(ProblemErrorCode.BOOKMARK_ALREADY_EXISTS); + } + + Member member = memberRepository.getReferenceById(memberId); + Problem problem = problemRepository.findById(request.problemId()) + .orElseThrow(() -> new ProblemBusinessException(ProblemErrorCode.PROBLEM_NOT_FOUND)); + + + Bookmark bookmark = Bookmark.of(member, problem); + + return bookmarkRepository.save(bookmark).getId(); } } diff --git a/jabiseo-api/src/main/java/com/jabiseo/problem/usecase/DeleteBookmarkUseCase.java b/jabiseo-api/src/main/java/com/jabiseo/problem/usecase/DeleteBookmarkUseCase.java index 23c0067..c74d258 100644 --- a/jabiseo-api/src/main/java/com/jabiseo/problem/usecase/DeleteBookmarkUseCase.java +++ b/jabiseo-api/src/main/java/com/jabiseo/problem/usecase/DeleteBookmarkUseCase.java @@ -1,12 +1,25 @@ package com.jabiseo.problem.usecase; +import com.jabiseo.problem.domain.Bookmark; +import com.jabiseo.problem.domain.BookmarkRepository; import com.jabiseo.problem.dto.DeleteBookmarkRequest; +import com.jabiseo.problem.exception.ProblemBusinessException; +import com.jabiseo.problem.exception.ProblemErrorCode; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service +@Transactional +@RequiredArgsConstructor public class DeleteBookmarkUseCase { - public void execute(DeleteBookmarkRequest request) { - return; + private final BookmarkRepository bookmarkRepository; + + public void execute(String memberId, DeleteBookmarkRequest request) { + Bookmark bookmark = bookmarkRepository.findByMemberIdAndProblemId(memberId, request.problemId()) + .orElseThrow(() -> new ProblemBusinessException(ProblemErrorCode.BOOKMARK_NOT_FOUND)); + + bookmarkRepository.delete(bookmark); } } diff --git a/jabiseo-api/src/main/java/com/jabiseo/problem/usecase/FindProblemsUseCase.java b/jabiseo-api/src/main/java/com/jabiseo/problem/usecase/FindProblemsUseCase.java index 3ddbadd..821ce21 100644 --- a/jabiseo-api/src/main/java/com/jabiseo/problem/usecase/FindProblemsUseCase.java +++ b/jabiseo-api/src/main/java/com/jabiseo/problem/usecase/FindProblemsUseCase.java @@ -6,6 +6,7 @@ import com.jabiseo.certificate.dto.SubjectResponse; import com.jabiseo.certificate.exception.CertificateBusinessException; import com.jabiseo.certificate.exception.CertificateErrorCode; +import com.jabiseo.problem.domain.Problem; import com.jabiseo.problem.domain.ProblemRepository; import com.jabiseo.problem.dto.ChoiceResponse; import com.jabiseo.problem.dto.FindProblemsRequest; @@ -43,7 +44,7 @@ public List execute(String certificateId, List sub validateProblemCount(count); - return subjectIds.stream() + List problems = subjectIds.stream() .map(subjectId -> { if (examId.isPresent()) { return problemRepository.findRandomByExamIdAndSubjectId(examId.get(), subjectId, count); @@ -51,6 +52,9 @@ public List execute(String certificateId, List sub return problemRepository.findRandomBySubjectId(subjectId, count); }) .flatMap(List::stream) + .toList(); + + return problems.stream() .map(FindProblemsResponse::from) .toList(); } diff --git a/jabiseo-api/src/test/java/com/jabiseo/fixture/ProblemFixture.java b/jabiseo-api/src/test/java/com/jabiseo/fixture/ProblemFixture.java index 987dcff..a73c7c0 100644 --- a/jabiseo-api/src/test/java/com/jabiseo/fixture/ProblemFixture.java +++ b/jabiseo-api/src/test/java/com/jabiseo/fixture/ProblemFixture.java @@ -5,6 +5,10 @@ import com.jabiseo.certificate.domain.Subject; import com.jabiseo.problem.domain.Problem; +import static com.jabiseo.fixture.CertificateFixture.createCertificate; +import static com.jabiseo.fixture.ExamFixture.createExam; +import static com.jabiseo.fixture.SubjectFixture.createSubject; + public class ProblemFixture { public static Problem createProblem(String id, Certificate certificate, Exam exam, Subject subject) { return Problem.of( @@ -23,4 +27,23 @@ public static Problem createProblem(String id, Certificate certificate, Exam exa subject ); } + + public static Problem createProblem(String id) { + Certificate certificate = createCertificate("1234"); + return Problem.of( + id, + "problem description", + "choice1", + "choice2", + "choice3", + "choice4", + "choice5", + 1, + "problem theory", + "problem solution", + certificate, + createExam("5432", certificate), + createSubject("9876", certificate) + ); + } } diff --git a/jabiseo-api/src/test/java/com/jabiseo/problem/usecase/CreateBookmarkUseCaseTest.java b/jabiseo-api/src/test/java/com/jabiseo/problem/usecase/CreateBookmarkUseCaseTest.java new file mode 100644 index 0000000..6bbaf98 --- /dev/null +++ b/jabiseo-api/src/test/java/com/jabiseo/problem/usecase/CreateBookmarkUseCaseTest.java @@ -0,0 +1,106 @@ +package com.jabiseo.problem.usecase; + +import com.jabiseo.member.domain.Member; +import com.jabiseo.member.domain.MemberRepository; +import com.jabiseo.problem.domain.Bookmark; +import com.jabiseo.problem.domain.BookmarkRepository; +import com.jabiseo.problem.domain.Problem; +import com.jabiseo.problem.domain.ProblemRepository; +import com.jabiseo.problem.dto.CreateBookmarkRequest; +import com.jabiseo.problem.exception.ProblemBusinessException; +import com.jabiseo.problem.exception.ProblemErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static com.jabiseo.fixture.MemberFixture.createMember; +import static com.jabiseo.fixture.ProblemFixture.createProblem; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@DisplayName("북마크 생성 테스트") +@ExtendWith(MockitoExtension.class) +class CreateBookmarkUseCaseTest { + + @InjectMocks + CreateBookmarkUseCase sut; + + @Mock + MemberRepository memberRepository; + + @Mock + ProblemRepository problemRepository; + + @Mock + BookmarkRepository bookmarkRepository; + + @Test + @DisplayName("북마크 생성 테스트 성공 케이스") + void givenMemberIdAndProblemId_whenCreatingBookmark_thenCreateBookmark() { + //given + String memberId = "1"; + String problemId = "2"; + Member member = createMember(memberId); + Problem problem = createProblem(problemId); + given(memberRepository.getReferenceById(memberId)).willReturn(member); + given(problemRepository.findById(problemId)).willReturn(Optional.of(problem)); + given(bookmarkRepository.existsByMemberIdAndProblemId(memberId, problemId)).willReturn(false); + given(bookmarkRepository.save(any())).willReturn(Bookmark.of(member, problem)); + + + //when + sut.execute(memberId, new CreateBookmarkRequest(problemId)); + + //then + ArgumentCaptor bookmarkCaptor = ArgumentCaptor.forClass(Bookmark.class); + verify(bookmarkRepository).save(bookmarkCaptor.capture()); + Bookmark savedBookmark = bookmarkCaptor.getValue(); + + assertThat(savedBookmark).isNotNull(); + assertThat(savedBookmark.getMember().getId()).isEqualTo(memberId); + assertThat(savedBookmark.getProblem().getId()).isEqualTo(problemId); + } + + @Test + @DisplayName("이미 북마크한 문제를 북마크하는 경우 테스트") + void givenAlreadyExistedMemberIdAndProblemId_whenCreatingBookmark_thenReturnError() { + //given + String memberId = "1"; + String problemId = "2"; + given(bookmarkRepository.existsByMemberIdAndProblemId(memberId, problemId)).willReturn(true); + + + //when & then + assertThatThrownBy(() -> sut.execute(memberId, new CreateBookmarkRequest(problemId))) + .isInstanceOf(ProblemBusinessException.class) + .hasFieldOrPropertyWithValue("errorCode", ProblemErrorCode.BOOKMARK_ALREADY_EXISTS); + + } + + @Test + @DisplayName("존재하지 않는 문제의 북마크 생성 테스트") + void givenMemberIdAndNonExistedProblemId_whenCreatingBookmark_thenReturnError() { + //given + String memberId = "1"; + String problemId = "2"; + given(bookmarkRepository.existsByMemberIdAndProblemId(memberId, problemId)).willReturn(false); + given(problemRepository.findById(problemId)).willReturn(Optional.empty()); + + //when & then + assertThatThrownBy(() -> sut.execute(memberId, new CreateBookmarkRequest(problemId))) + .isInstanceOf(ProblemBusinessException.class) + .hasFieldOrPropertyWithValue("errorCode", ProblemErrorCode.PROBLEM_NOT_FOUND); + + } + + +} diff --git a/jabiseo-api/src/test/java/com/jabiseo/problem/usecase/DeleteBookmarkUseCaseTest.java b/jabiseo-api/src/test/java/com/jabiseo/problem/usecase/DeleteBookmarkUseCaseTest.java new file mode 100644 index 0000000..a2d7d89 --- /dev/null +++ b/jabiseo-api/src/test/java/com/jabiseo/problem/usecase/DeleteBookmarkUseCaseTest.java @@ -0,0 +1,72 @@ +package com.jabiseo.problem.usecase; + +import com.jabiseo.member.domain.Member; +import com.jabiseo.problem.domain.Bookmark; +import com.jabiseo.problem.domain.BookmarkRepository; +import com.jabiseo.problem.domain.Problem; +import com.jabiseo.problem.dto.DeleteBookmarkRequest; +import com.jabiseo.problem.exception.ProblemBusinessException; +import com.jabiseo.problem.exception.ProblemErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static com.jabiseo.fixture.MemberFixture.createMember; +import static com.jabiseo.fixture.ProblemFixture.createProblem; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@DisplayName("북마크 삭제 테스트") +@ExtendWith(MockitoExtension.class) +class DeleteBookmarkUseCaseTest { + + @InjectMocks + DeleteBookmarkUseCase sut; + + @Mock + BookmarkRepository bookmarkRepository; + + @Test + @DisplayName("북마크 삭제 테스트 성공 케이스") + void givenMemberIdAndProblemId_whenDeletingBookmark_thenDeleteBookmark() { + //given + String memberId = "1"; + String problemId = "2"; + Member member = createMember(memberId); + Problem problem = createProblem(problemId); + Bookmark bookmark = Bookmark.of(member, problem); + given(bookmarkRepository.findByMemberIdAndProblemId(memberId, problemId)).willReturn(Optional.of(bookmark)); + + //when + sut.execute(memberId, new DeleteBookmarkRequest(problemId)); + + //then + ArgumentCaptor bookmarkCaptor = ArgumentCaptor.forClass(Bookmark.class); + verify(bookmarkRepository).delete(bookmarkCaptor.capture()); + Bookmark deletedBookmark = bookmarkCaptor.getValue(); + assertThat(deletedBookmark).isEqualTo(bookmark); + } + + @Test + @DisplayName("존재하지 않는 북마크를 삭제하는 경우 테스트") + void givenNonExistBookmarkWithMemberIdAndProblemId_whenDeletingBookmark_thenReturnError() { + //given + String memberId = "1"; + String problemId = "2"; + given(bookmarkRepository.findByMemberIdAndProblemId(memberId, problemId)).willReturn(Optional.empty()); + + //when & then + assertThatThrownBy(() -> sut.execute(memberId, new DeleteBookmarkRequest(problemId))) + .isInstanceOf(ProblemBusinessException.class) + .hasFieldOrPropertyWithValue("errorCode", ProblemErrorCode.BOOKMARK_NOT_FOUND); + } + +} diff --git a/jabiseo-domain/src/main/java/com/jabiseo/member/domain/Member.java b/jabiseo-domain/src/main/java/com/jabiseo/member/domain/Member.java index b3b6231..f26b133 100644 --- a/jabiseo-domain/src/main/java/com/jabiseo/member/domain/Member.java +++ b/jabiseo-domain/src/main/java/com/jabiseo/member/domain/Member.java @@ -1,6 +1,7 @@ package com.jabiseo.member.domain; import com.jabiseo.certificate.domain.Certificate; +import com.jabiseo.problem.domain.Bookmark; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -12,6 +13,8 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -51,6 +54,9 @@ public class Member { @JoinColumn(name = "certificate_state_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) private Certificate certificateState; + @OneToMany(mappedBy = "member") + private List bookmarks = new ArrayList<>(); + private Member(String id, String email, String nickname, String oauthId, String oauthServer, String profileImage) { this.id = id; this.email = email; @@ -70,4 +76,7 @@ public Member updateCertificateState(Certificate certificate) { return this; } + public void addBookmark(Bookmark bookmark) { + bookmarks.add(bookmark); + } } diff --git a/jabiseo-domain/src/main/java/com/jabiseo/problem/domain/Bookmark.java b/jabiseo-domain/src/main/java/com/jabiseo/problem/domain/Bookmark.java new file mode 100644 index 0000000..273ff3c --- /dev/null +++ b/jabiseo-domain/src/main/java/com/jabiseo/problem/domain/Bookmark.java @@ -0,0 +1,48 @@ +package com.jabiseo.problem.domain; + +import com.jabiseo.member.domain.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Bookmark { + + @Id + @Column(name = "bookmark_id") + private String id; + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "problem_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) + private Problem problem; + + private Bookmark(String id, Member member, Problem problem) { + this.id = id; + this.member = member; + this.problem = problem; + } + + public static Bookmark of(Member member, Problem problem) { + String id = UUID.randomUUID().toString(); //TODO: PK 생성 전략 변경 필요 + Bookmark bookmark = new Bookmark(id, member, problem); + member.addBookmark(bookmark); + problem.addBookmark(bookmark); + return bookmark; + } + +} diff --git a/jabiseo-domain/src/main/java/com/jabiseo/problem/domain/BookmarkRepository.java b/jabiseo-domain/src/main/java/com/jabiseo/problem/domain/BookmarkRepository.java new file mode 100644 index 0000000..09b0937 --- /dev/null +++ b/jabiseo-domain/src/main/java/com/jabiseo/problem/domain/BookmarkRepository.java @@ -0,0 +1,13 @@ +package com.jabiseo.problem.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface BookmarkRepository extends JpaRepository { + + boolean existsByMemberIdAndProblemId(String memberId, String problemId); + + Optional findByMemberIdAndProblemId(String memberId, String problemId); + +} diff --git a/jabiseo-domain/src/main/java/com/jabiseo/problem/domain/Problem.java b/jabiseo-domain/src/main/java/com/jabiseo/problem/domain/Problem.java index 563da77..d4d8dab 100644 --- a/jabiseo-domain/src/main/java/com/jabiseo/problem/domain/Problem.java +++ b/jabiseo-domain/src/main/java/com/jabiseo/problem/domain/Problem.java @@ -7,6 +7,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; @@ -49,6 +50,9 @@ public class Problem { @JoinColumn(name = "subject_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) private Subject subject; + @OneToMany(mappedBy = "problem") + private List bookmarks = new ArrayList<>(); + public List getChoices() { return Stream.of(choice1, choice2, choice3, choice4, choice5) .filter((choice) -> choice != null && !choice.isBlank()) @@ -79,4 +83,8 @@ public static Problem of(String id, String description, String choice1, String c return new Problem(id, description, choice1, choice2, choice3, choice4, choice5, answerNumber, theory, solution, certificate, exam, subject); } + + public void addBookmark(Bookmark bookmark) { + bookmarks.add(bookmark); + } } diff --git a/jabiseo-domain/src/main/java/com/jabiseo/problem/exception/ProblemErrorCode.java b/jabiseo-domain/src/main/java/com/jabiseo/problem/exception/ProblemErrorCode.java index d56202a..9860e5d 100644 --- a/jabiseo-domain/src/main/java/com/jabiseo/problem/exception/ProblemErrorCode.java +++ b/jabiseo-domain/src/main/java/com/jabiseo/problem/exception/ProblemErrorCode.java @@ -6,8 +6,11 @@ @Getter public enum ProblemErrorCode implements ErrorCode { - PROBLEM_NOT_FOUND("문제를 찾을 수 없습니다.", "PRO_001", ErrorCode.NOT_FOUND), - INVALID_PROBLEM_COUNT("문제의 개수가 올바르지 않습니다.", "PRO_002", ErrorCode.BAD_REQUEST); + PROBLEM_NOT_FOUND("문제를 찾을 수 없습니다.", "PRB_001", ErrorCode.NOT_FOUND), + INVALID_PROBLEM_COUNT("문제의 개수가 올바르지 않습니다.", "PRB_002", ErrorCode.BAD_REQUEST), + BOOKMARK_ALREADY_EXISTS("이미 북마크한 문제입니다.", "PRB_003", ErrorCode.BAD_REQUEST), + BOOKMARK_NOT_FOUND("북마크 정보를 찾을 수 없습니다.", "PRB_004", ErrorCode.NOT_FOUND) + ; private final String message;