From cd69616d897df5b4baa06b0e07a1ebc402a7f645 Mon Sep 17 00:00:00 2001 From: brgndy Date: Tue, 8 Oct 2024 10:39:58 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EB=AE=A4=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=9D=B8=EC=9E=90=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/useSingleRequestMutation.test.tsx | 12 ++++++------ frontend/src/hooks/useSingleRequestMutation.ts | 3 +-- frontend/src/hooks/useSubmitDiscussionMutation.ts | 2 +- frontend/src/hooks/useSubmitSolutionMutation.ts | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/frontend/src/hooks/__tests__/useSingleRequestMutation.test.tsx b/frontend/src/hooks/__tests__/useSingleRequestMutation.test.tsx index c2d2bfe3..f7890967 100644 --- a/frontend/src/hooks/__tests__/useSingleRequestMutation.test.tsx +++ b/frontend/src/hooks/__tests__/useSingleRequestMutation.test.tsx @@ -33,7 +33,7 @@ describe('useSingleRequestMutation 훅 테스트', () => { const { result } = renderHook( () => useSingleRequestMutation({ - queryFn: mockMutationFn, + mutationFn: mockMutationFn, onSuccess: mockOnSuccess, onSettled: mockOnSettled, }), @@ -55,7 +55,7 @@ describe('useSingleRequestMutation 훅 테스트', () => { const { result } = renderHook( () => useSingleRequestMutation({ - queryFn: mockMutationFn, + mutationFn: mockMutationFn, onError: mockOnError, onSettled: mockOnSettled, }), @@ -73,7 +73,7 @@ describe('useSingleRequestMutation 훅 테스트', () => { it('동일한 requestId로 중복 요청이 방지된다.', async () => { const { result } = renderHook( - () => useSingleRequestMutation({ queryFn: mockMutationFn, requestId: 'testRequestId' }), + () => useSingleRequestMutation({ mutationFn: mockMutationFn, requestId: 'testRequestId' }), { wrapper }, ); @@ -95,12 +95,12 @@ describe('useSingleRequestMutation 훅 테스트', () => { it('다른 requestId로 요청이 가능하다.', async () => { const { result: result1 } = renderHook( - () => useSingleRequestMutation({ queryFn: mockMutationFn, requestId: 'requestId1' }), + () => useSingleRequestMutation({ mutationFn: mockMutationFn, requestId: 'requestId1' }), { wrapper }, ); const { result: result2 } = renderHook( - () => useSingleRequestMutation({ queryFn: mockMutationFn, requestId: 'requestId2' }), + () => useSingleRequestMutation({ mutationFn: mockMutationFn, requestId: 'requestId2' }), { wrapper }, ); @@ -114,7 +114,7 @@ describe('useSingleRequestMutation 훅 테스트', () => { it('onMutate가 호출되면 요청 전에 실행된다.', async () => { const { result } = renderHook( - () => useSingleRequestMutation({ queryFn: mockMutationFn, onMutate: mockOnMutate }), + () => useSingleRequestMutation({ mutationFn: mockMutationFn, onMutate: mockOnMutate }), { wrapper }, ); diff --git a/frontend/src/hooks/useSingleRequestMutation.ts b/frontend/src/hooks/useSingleRequestMutation.ts index f2e75ddf..60f48fb3 100644 --- a/frontend/src/hooks/useSingleRequestMutation.ts +++ b/frontend/src/hooks/useSingleRequestMutation.ts @@ -6,7 +6,6 @@ import { ERROR_MESSAGE } from '@/constants/messages'; interface SingleFlightMutationOptions extends UseMutationOptions { requestId?: string; - queryFn: (variables: TVariables) => Promise; } const useSingleRequestMutation = ( @@ -17,7 +16,7 @@ const useSingleRequestMutation = ( return useMutation({ ...options, - mutationFn: options.queryFn, + mutationFn: options.mutationFn, onMutate: async (variables: TVariables) => { const canProceed = startRequest(requestId); if (!canProceed) { diff --git a/frontend/src/hooks/useSubmitDiscussionMutation.ts b/frontend/src/hooks/useSubmitDiscussionMutation.ts index cff640fd..c324a372 100644 --- a/frontend/src/hooks/useSubmitDiscussionMutation.ts +++ b/frontend/src/hooks/useSubmitDiscussionMutation.ts @@ -10,7 +10,7 @@ const useSubmitDiscussionMutation = () => { const navigate = useNavigate(); const { mutate: submitDiscussionMutation, isPending } = useSingleRequestMutation({ - queryFn: postDiscussionSubmit, + mutationFn: postDiscussionSubmit, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['all'] }); // TODO: all, 필터링까지 캐시 무효화 잘 되는지 확인 필요 @프룬 navigate(ROUTES.discussions); diff --git a/frontend/src/hooks/useSubmitSolutionMutation.ts b/frontend/src/hooks/useSubmitSolutionMutation.ts index 770b1081..fab4c4d1 100644 --- a/frontend/src/hooks/useSubmitSolutionMutation.ts +++ b/frontend/src/hooks/useSubmitSolutionMutation.ts @@ -19,7 +19,7 @@ const useSubmitSolutionMutation = ({ const navigate = useNavigate(); const { mutate: submitSolutionMutation, isPending } = useSingleRequestMutation({ - queryFn: postSolutionSubmit, + mutationFn: postSolutionSubmit, onSuccess: () => { onSuccessCallback(); queryClient.invalidateQueries({ queryKey: missionKeys.detail(missionId) }); From 7ada97b36845415a038cbd57516a03b4c179e820 Mon Sep 17 00:00:00 2001 From: brgndy Date: Fri, 11 Oct 2024 10:56:41 +0900 Subject: [PATCH 2/4] Merge branch 'dev' of https://github.com/woowacourse-teams/2024-devel-up into dev --- .../main/java/develup/api/SolutionApi.java | 3 +- .../discussion/DiscussionReadService.java | 2 +- .../SummarizedDiscussionResponse.java | 2 + .../comment/DiscussionCommentReadService.java | 19 +---- .../comment/MyDiscussionCommentResponse.java | 8 +- .../mission/MissionReadService.java | 2 +- .../solution/MySolutionResponse.java | 2 +- .../solution/SolutionReadService.java | 11 +-- .../comment/MySolutionCommentResponse.java | 8 +- .../comment/SolutionCommentReadService.java | 19 +---- .../DiscussionRepositoryCustom.java | 3 +- .../DiscussionCommentRepositoryCustom.java | 3 +- .../domain/member/.Provider.java.icloud | Bin 0 -> 160 bytes .../develup/domain/solution/Solution.java | 10 ++- .../domain/solution/SolutionRepository.java | 4 +- .../solution/SolutionRepositoryCustom.java | 48 ++++++------ .../comment/SolutionCommentCounts.java | 23 ------ .../SolutionCommentRepositoryCustom.java | 3 +- backend/src/main/resources/data.sql | 14 ++-- .../V2__add_solution_submitted_at.sql | 2 + .../V3__remove_comment_foreign_key.sql | 2 + .../java/develup/api/DiscussionApiTest.java | 5 +- .../develup/api/DiscussionCommentApiTest.java | 5 +- .../java/develup/api/SolutionApiTest.java | 2 +- .../develup/api/SolutionCommentApiTest.java | 5 +- .../DiscussionCommentReadServiceTest.java | 43 ++--------- .../mission/MissionReadServiceTest.java | 5 +- .../solution/SolutionReadServiceTest.java | 27 ++++++- .../SolutionCommentReadServiceTest.java | 43 ++--------- .../DiscussionRepositoryCustomTest.java | 16 +++- ...DiscussionCommentRepositoryCustomTest.java | 15 +++- .../SolutionRepositoryCustomTest.java | 70 ++++++++++++++++-- .../solution/SolutionRepositoryTest.java | 25 +++++-- .../SolutionCommentRepositoryCustomTest.java | 16 ++-- .../support/data/SolutionTestData.java | 13 +++- frontend/index.html | 2 +- frontend/src/App.tsx | 2 + frontend/src/apis/solutions.ts | 9 ++- frontend/src/assets/images/noContent.svg | 68 +++++++++++++++++ frontend/src/assets/images/upArrow.svg | 3 + frontend/src/components/Carousel/Carousel.tsx | 8 +- .../CommentForm/CommentForm.styled.ts | 5 +- .../CommentSection/CommentForm/index.tsx | 2 +- .../CommentItem/CommentReplyItem.tsx | 14 ++-- .../CommentItem/CommentReplySection.tsx | 29 +++++--- .../CommentList/CommentList.styled.ts | 8 +- .../DashboardDiscussion/DiscussionItem.tsx | 2 +- .../DashBoard/MyComments/MyComment.tsx | 8 +- .../DiscussionDetailHeader.tsx | 29 +++----- .../DiscussionList/DiscussionList.styled.ts | 13 +++- .../DiscussionList/DiscussionListContent.tsx | 28 +++---- .../DiscussionList/DiscussionListHeader.tsx | 32 +++++--- .../DiscussionDescription.tsx | 11 ++- ...it.style.ts => DiscussionSubmit.styled.ts} | 17 ++++- .../DiscussionSubmitHeader.tsx | 14 ++++ .../DiscussionSubmit/DiscussionTitle.tsx | 4 +- .../src/components/DiscussionSubmit/index.tsx | 6 +- frontend/src/components/Header/index.tsx | 6 +- .../MissionDetail/MissionDetail.styled.ts | 27 +++++++ .../MissionDetail/MissionDetailButtons.tsx | 34 ++++++--- .../MissionDetail/MissionDetailHeader.tsx | 6 +- .../MissionProcess/MissionProcess.styled.ts | 19 ++++- .../ModalContent/MissionProcess/index.tsx | 23 +++--- .../SolutionDetail/SolutionDetailHeader.tsx | 6 +- .../src/components/SolutionDetail/index.tsx | 4 +- .../SolutionList/SolutionList.styled.ts | 23 ++++++ .../src/components/SolutionList/index.tsx | 40 ++++++++++ .../src/components/common/Error/ErrorLogo.tsx | 1 - .../LoadingSpinner/LoadingSpinner.styled.ts | 2 - .../common/NoContent/NoContent.styled.ts | 14 ++++ .../NoContent/NoContentWithoutButton.tsx | 20 +++++ .../ScrollToTopButton.styled.ts | 25 +++++++ .../common/ScrollToTopButton/index.tsx | 28 +++++++ .../common/TagButton/TagButton.styled.ts | 30 ++++++-- .../src/components/common/TagButton/index.tsx | 4 +- .../common/TagList/TagList.styled.ts | 13 +++- .../src/components/common/TagList/index.tsx | 37 +++++---- .../TagMultipleList/TagMultipleList.styled.ts | 13 +++- .../common/TagMultipleList/index.tsx | 37 +++++---- frontend/src/constants/messages.ts | 3 +- frontend/src/hooks/useDescription.ts | 17 ++--- frontend/src/hooks/useDiscussionTitle.ts | 5 ++ frontend/src/hooks/useLogoutMutation.ts | 23 ++++++ frontend/src/hooks/useMissionStartMutation.ts | 4 +- frontend/src/hooks/useSolutionSummaries.ts | 6 +- .../src/hooks/useSubmitSolutionMutation.ts | 4 +- frontend/src/index.tsx | 15 +--- .../src/pages/AboutPage/AboutPage.styled.ts | 2 +- frontend/src/pages/AboutPage/AboutPage.tsx | 50 ++----------- .../DiscussionDetailPage.styled.ts | 4 +- .../src/pages/DiscussionListPage/index.tsx | 6 +- ...tyle.ts => DiscussionSubmitPage.styled.ts} | 4 - .../src/pages/DiscussionSubmitPage/index.tsx | 5 +- .../src/pages/MainPage/MainPage.styled.tsx | 8 +- frontend/src/pages/MainPage/index.tsx | 24 ++---- .../MissionListPage/MissionListPage.styled.ts | 5 +- .../SolutionDetailPage.styled.ts | 1 + .../SolutionListPage.styled.ts | 24 +----- frontend/src/pages/SolutionListPage/index.tsx | 35 ++++----- frontend/src/styles/GlobalStyle.tsx | 3 - frontend/src/types/mission.ts | 5 ++ 101 files changed, 913 insertions(+), 544 deletions(-) create mode 100644 backend/src/main/java/develup/domain/member/.Provider.java.icloud delete mode 100644 backend/src/main/java/develup/domain/solution/comment/SolutionCommentCounts.java create mode 100644 backend/src/main/resources/db/migration/V2__add_solution_submitted_at.sql create mode 100644 backend/src/main/resources/db/migration/V3__remove_comment_foreign_key.sql create mode 100644 frontend/src/assets/images/noContent.svg create mode 100644 frontend/src/assets/images/upArrow.svg rename frontend/src/components/DiscussionSubmit/{DiscussionSubmit.style.ts => DiscussionSubmit.styled.ts} (69%) create mode 100644 frontend/src/components/DiscussionSubmit/DiscussionSubmitHeader.tsx create mode 100644 frontend/src/components/SolutionList/SolutionList.styled.ts create mode 100644 frontend/src/components/SolutionList/index.tsx create mode 100644 frontend/src/components/common/NoContent/NoContent.styled.ts create mode 100644 frontend/src/components/common/NoContent/NoContentWithoutButton.tsx create mode 100644 frontend/src/components/common/ScrollToTopButton/ScrollToTopButton.styled.ts create mode 100644 frontend/src/components/common/ScrollToTopButton/index.tsx rename frontend/src/pages/DiscussionSubmitPage/{DiscussionSubmitPage.style.ts => DiscussionSubmitPage.styled.ts} (70%) diff --git a/backend/src/main/java/develup/api/SolutionApi.java b/backend/src/main/java/develup/api/SolutionApi.java index 68172e75..700f7962 100644 --- a/backend/src/main/java/develup/api/SolutionApi.java +++ b/backend/src/main/java/develup/api/SolutionApi.java @@ -81,9 +81,10 @@ public ResponseEntity> deleteSolution( @GetMapping("/solutions") @Operation(summary = "솔루션 목록 조회 API", description = "솔루션 목록을 조회합니다.") public ResponseEntity>> getSolutions( + @RequestParam(defaultValue = "all") String mission, @RequestParam(defaultValue = "all") String hashTag ) { - List responses = solutionReadService.getCompletedSummaries(hashTag); + List responses = solutionReadService.getCompletedSummaries(mission, hashTag); return ResponseEntity.ok(new ApiResponse<>(responses)); } diff --git a/backend/src/main/java/develup/application/discussion/DiscussionReadService.java b/backend/src/main/java/develup/application/discussion/DiscussionReadService.java index 67b7258f..095d88b3 100644 --- a/backend/src/main/java/develup/application/discussion/DiscussionReadService.java +++ b/backend/src/main/java/develup/application/discussion/DiscussionReadService.java @@ -32,7 +32,7 @@ public List getSummaries(String mission, String ha } public List getDiscussionsByMemberId(Long memberId) { - List myDiscussions = discussionRepositoryCustom.findAllByMemberId(memberId); + List myDiscussions = discussionRepositoryCustom.findAllByMemberIdOrderByDesc(memberId); DiscussionCommentCounts discussionCommentCounts = new DiscussionCommentCounts( discussionRepositoryCustom.findAllDiscussionCommentCounts() ); diff --git a/backend/src/main/java/develup/application/discussion/SummarizedDiscussionResponse.java b/backend/src/main/java/develup/application/discussion/SummarizedDiscussionResponse.java index 8c31c5af..1e7aafde 100644 --- a/backend/src/main/java/develup/application/discussion/SummarizedDiscussionResponse.java +++ b/backend/src/main/java/develup/application/discussion/SummarizedDiscussionResponse.java @@ -10,6 +10,7 @@ public record SummarizedDiscussionResponse( Long id, String title, + String content, String mission, List hashTags, MemberResponse member, @@ -25,6 +26,7 @@ public static SummarizedDiscussionResponse of(Discussion discussion, Long count) return new SummarizedDiscussionResponse( discussion.getId(), discussion.getTitle(), + discussion.getContent(), discussion.getMissionTitle(), hashTagResponses, MemberResponse.from(discussion.getMember()), diff --git a/backend/src/main/java/develup/application/discussion/comment/DiscussionCommentReadService.java b/backend/src/main/java/develup/application/discussion/comment/DiscussionCommentReadService.java index 639a55df..a6fd7780 100644 --- a/backend/src/main/java/develup/application/discussion/comment/DiscussionCommentReadService.java +++ b/backend/src/main/java/develup/application/discussion/comment/DiscussionCommentReadService.java @@ -3,9 +3,7 @@ import java.util.List; import develup.api.exception.DevelupException; import develup.api.exception.ExceptionType; -import develup.domain.discussion.DiscussionRepositoryCustom; import develup.domain.discussion.comment.DiscussionComment; -import develup.domain.discussion.comment.DiscussionCommentCounts; import develup.domain.discussion.comment.DiscussionCommentRepository; import develup.domain.discussion.comment.DiscussionCommentRepositoryCustom; import develup.domain.discussion.comment.MyDiscussionComment; @@ -21,7 +19,6 @@ public class DiscussionCommentReadService { private final DiscussionCommentGroupingService commentGroupingService; private final DiscussionCommentRepositoryCustom discussionCommentRepositoryCustom; private final DiscussionCommentRepository discussionCommentRepository; - private final DiscussionRepositoryCustom discussionRepositoryCustom; public DiscussionComment getById(Long commentId) { DiscussionComment comment = discussionCommentRepository.findById(commentId) @@ -41,22 +38,10 @@ public List getCommentsWithReplies(Long discus } public List getMyComments(Long memberId) { - List mySolutionComments = discussionCommentRepositoryCustom.findAllMyDiscussionComment(memberId); - DiscussionCommentCounts discussionCommentCounts = new DiscussionCommentCounts( - discussionRepositoryCustom.findAllDiscussionCommentCounts() - ); + List mySolutionComments = discussionCommentRepositoryCustom.findAllMyDiscussionCommentOrderByCreatedAtDesc(memberId); return mySolutionComments.stream() - .map(myDiscussionComment -> mapToMyDiscussionCommentResponse(myDiscussionComment, discussionCommentCounts)) + .map(MyDiscussionCommentResponse::from) .toList(); } - - private MyDiscussionCommentResponse mapToMyDiscussionCommentResponse( - MyDiscussionComment myDiscussionComment, - DiscussionCommentCounts discussionCommentCounts - ) { - Long discussionId = myDiscussionComment.discussionId(); - Long commentCount = discussionCommentCounts.getCount(discussionId); - return MyDiscussionCommentResponse.of(myDiscussionComment, commentCount); - } } diff --git a/backend/src/main/java/develup/application/discussion/comment/MyDiscussionCommentResponse.java b/backend/src/main/java/develup/application/discussion/comment/MyDiscussionCommentResponse.java index 7cec6509..d8931da4 100644 --- a/backend/src/main/java/develup/application/discussion/comment/MyDiscussionCommentResponse.java +++ b/backend/src/main/java/develup/application/discussion/comment/MyDiscussionCommentResponse.java @@ -8,18 +8,16 @@ public record MyDiscussionCommentResponse( Long discussionId, String content, LocalDateTime createdAt, - String discussionTitle, - Long discussionCommentCount + String discussionTitle ) { - public static MyDiscussionCommentResponse of(MyDiscussionComment myDiscussionComment, Long discussionCommentCount) { + public static MyDiscussionCommentResponse from(MyDiscussionComment myDiscussionComment) { return new MyDiscussionCommentResponse( myDiscussionComment.id(), myDiscussionComment.discussionId(), myDiscussionComment.content(), myDiscussionComment.createdAt(), - myDiscussionComment.discussionTitle(), - discussionCommentCount + myDiscussionComment.discussionTitle() ); } } diff --git a/backend/src/main/java/develup/application/mission/MissionReadService.java b/backend/src/main/java/develup/application/mission/MissionReadService.java index b8e44298..3b2ab9b8 100644 --- a/backend/src/main/java/develup/application/mission/MissionReadService.java +++ b/backend/src/main/java/develup/application/mission/MissionReadService.java @@ -30,7 +30,7 @@ public List getMissions(String hashTagName) { } public List getInProgressMissions(Long memberId) { - return solutionRepository.findAllByMember_IdAndStatus(memberId, SolutionStatus.IN_PROGRESS) + return solutionRepository.findAllByMember_IdAndStatusOrderByIdDesc(memberId, SolutionStatus.IN_PROGRESS) .stream() .map(Solution::getMission) .distinct() diff --git a/backend/src/main/java/develup/application/solution/MySolutionResponse.java b/backend/src/main/java/develup/application/solution/MySolutionResponse.java index e2c58c0a..cdb30a35 100644 --- a/backend/src/main/java/develup/application/solution/MySolutionResponse.java +++ b/backend/src/main/java/develup/application/solution/MySolutionResponse.java @@ -10,7 +10,7 @@ public static MySolutionResponse from(Solution solution) { solution.getId(), solution.getMissionThumbnail(), solution.getTitle(), - solution.getCreatedAt() + solution.getSubmittedAt() ); } } diff --git a/backend/src/main/java/develup/application/solution/SolutionReadService.java b/backend/src/main/java/develup/application/solution/SolutionReadService.java index 3d398e15..ca7cdafb 100644 --- a/backend/src/main/java/develup/application/solution/SolutionReadService.java +++ b/backend/src/main/java/develup/application/solution/SolutionReadService.java @@ -27,19 +27,14 @@ public SolutionResponse getById(Long id) { } public List getSubmittedSolutionsByMemberId(Long memberId) { - List mySolutions = solutionRepository.findAllByMember_IdAndStatus(memberId, SolutionStatus.COMPLETED); + List mySolutions = solutionRepository.findAllByMember_IdAndStatusOrderBySubmittedAtDesc(memberId, SolutionStatus.COMPLETED); return mySolutions.stream() .map(MySolutionResponse::from) .toList(); } - public List getCompletedSummaries(String hashTagName) { - if (hashTagName.equalsIgnoreCase("all")) { - return solutionRepositoryCustom.findAllCompletedSolution().stream() - .map(SummarizedSolutionResponse::from) - .toList(); - } - return solutionRepositoryCustom.findAllCompletedSolutionByHashTagName(hashTagName).stream() + public List getCompletedSummaries(String missionTitle, String hashTagName) { + return solutionRepositoryCustom.findAllCompletedSolutionByHashTagName(missionTitle, hashTagName).stream() .map(SummarizedSolutionResponse::from) .toList(); } diff --git a/backend/src/main/java/develup/application/solution/comment/MySolutionCommentResponse.java b/backend/src/main/java/develup/application/solution/comment/MySolutionCommentResponse.java index c2aff577..3758f82c 100644 --- a/backend/src/main/java/develup/application/solution/comment/MySolutionCommentResponse.java +++ b/backend/src/main/java/develup/application/solution/comment/MySolutionCommentResponse.java @@ -8,18 +8,16 @@ public record MySolutionCommentResponse( Long solutionId, String content, LocalDateTime createdAt, - String solutionTitle, - Long solutionCommentCount + String solutionTitle ) { - public static MySolutionCommentResponse of(MySolutionComment mySolutionComment, Long solutionCommentCount) { + public static MySolutionCommentResponse from(MySolutionComment mySolutionComment) { return new MySolutionCommentResponse( mySolutionComment.id(), mySolutionComment.solutionId(), mySolutionComment.content(), mySolutionComment.createdAt(), - mySolutionComment.solutionTitle(), - solutionCommentCount + mySolutionComment.solutionTitle() ); } } diff --git a/backend/src/main/java/develup/application/solution/comment/SolutionCommentReadService.java b/backend/src/main/java/develup/application/solution/comment/SolutionCommentReadService.java index bdaf7c1b..05b23c5f 100644 --- a/backend/src/main/java/develup/application/solution/comment/SolutionCommentReadService.java +++ b/backend/src/main/java/develup/application/solution/comment/SolutionCommentReadService.java @@ -3,10 +3,8 @@ import java.util.List; import develup.api.exception.DevelupException; import develup.api.exception.ExceptionType; -import develup.domain.solution.SolutionRepositoryCustom; import develup.domain.solution.comment.MySolutionComment; import develup.domain.solution.comment.SolutionComment; -import develup.domain.solution.comment.SolutionCommentCounts; import develup.domain.solution.comment.SolutionCommentRepository; import develup.domain.solution.comment.SolutionCommentRepositoryCustom; import lombok.RequiredArgsConstructor; @@ -20,7 +18,6 @@ public class SolutionCommentReadService { private final CommentGroupingService commentGroupingService; private final SolutionCommentRepository solutionCommentRepository; - private final SolutionRepositoryCustom solutionRepositoryCustom; private final SolutionCommentRepositoryCustom solutionCommentRepositoryCustom; public SolutionComment getById(Long commentId) { @@ -43,22 +40,10 @@ public List getCommentsWithReplies(Long solution public List getMyComments(Long memberId) { List mySolutionComments = solutionCommentRepositoryCustom - .findAllMySolutionComment(memberId); - SolutionCommentCounts solutionCommentCounts = new SolutionCommentCounts( - solutionRepositoryCustom.findAllSolutionCommentCounts() - ); + .findAllMySolutionCommentOrderByDesc(memberId); return mySolutionComments.stream() - .map(mySolutionComment -> mapToMySolutionCommentResponse(mySolutionComment, solutionCommentCounts)) + .map(MySolutionCommentResponse::from) .toList(); } - - private MySolutionCommentResponse mapToMySolutionCommentResponse( - MySolutionComment mySolutionComment, - SolutionCommentCounts solutionCommentCounts - ) { - Long solutionId = mySolutionComment.solutionId(); - Long commentCount = solutionCommentCounts.getCount(solutionId); - return MySolutionCommentResponse.of(mySolutionComment, commentCount); - } } diff --git a/backend/src/main/java/develup/domain/discussion/DiscussionRepositoryCustom.java b/backend/src/main/java/develup/domain/discussion/DiscussionRepositoryCustom.java index 248ca923..52e2b634 100644 --- a/backend/src/main/java/develup/domain/discussion/DiscussionRepositoryCustom.java +++ b/backend/src/main/java/develup/domain/discussion/DiscussionRepositoryCustom.java @@ -67,7 +67,7 @@ public Optional findFetchById(Long id) { return Optional.ofNullable(result); } - public List findAllByMemberId(Long memberId) { + public List findAllByMemberIdOrderByDesc(Long memberId) { return queryFactory .selectFrom(discussion) .innerJoin(discussion.member, member).fetchJoin() @@ -75,6 +75,7 @@ public List findAllByMemberId(Long memberId) { .leftJoin(discussion.discussionHashTags.hashTags, discussionHashTag).fetchJoin() .leftJoin(discussionHashTag.hashTag, hashTag).fetchJoin() .where(member.id.eq(memberId)) + .orderBy(discussion.id.desc()) .fetch(); } diff --git a/backend/src/main/java/develup/domain/discussion/comment/DiscussionCommentRepositoryCustom.java b/backend/src/main/java/develup/domain/discussion/comment/DiscussionCommentRepositoryCustom.java index 6a78c0af..0bdcaa8a 100644 --- a/backend/src/main/java/develup/domain/discussion/comment/DiscussionCommentRepositoryCustom.java +++ b/backend/src/main/java/develup/domain/discussion/comment/DiscussionCommentRepositoryCustom.java @@ -24,7 +24,7 @@ public List findAllByDiscussionIdOrderByCreatedAtAsc(Long dis .fetch(); } - public List findAllMyDiscussionComment(Long memberId) { + public List findAllMyDiscussionCommentOrderByCreatedAtDesc(Long memberId) { return queryFactory.select(Projections.constructor(MyDiscussionComment.class, discussionComment.id, discussionComment.discussion.id, @@ -36,6 +36,7 @@ public List findAllMyDiscussionComment(Long memberId) { .join(discussionComment.discussion, discussion) .join(discussionComment.member) .where(discussionComment.member.id.eq(memberId).and(discussionComment.deletedAt.isNull())) + .orderBy(discussionComment.createdAt.desc()) .fetch(); } } diff --git a/backend/src/main/java/develup/domain/member/.Provider.java.icloud b/backend/src/main/java/develup/domain/member/.Provider.java.icloud new file mode 100644 index 0000000000000000000000000000000000000000..4fc2a315900425e82d66f85aa856dc26d3b338f3 GIT binary patch literal 160 zcmYc)$jK}&F)+By$i&RT$`<1n92(@~mzbOComv?$AOPmNW#*&?XI4RkB;Z0psm1xF zMaiill?5QF*npz^vdom!BE77{vP1#bcmWx#N`q3 { boolean existsByMember_IdAndMission_IdAndStatus(Long memberId, Long missionId, SolutionStatus status); - List findAllByMember_IdAndStatus(Long memberId, SolutionStatus status); + List findAllByMember_IdAndStatusOrderBySubmittedAtDesc(Long memberId, SolutionStatus status); Optional findByMember_IdAndMission_IdAndStatus(Long memberId, Long missionId, SolutionStatus status); + + List findAllByMember_IdAndStatusOrderByIdDesc(Long memberId, SolutionStatus solutionStatus); } diff --git a/backend/src/main/java/develup/domain/solution/SolutionRepositoryCustom.java b/backend/src/main/java/develup/domain/solution/SolutionRepositoryCustom.java index 6623939e..62bc1306 100644 --- a/backend/src/main/java/develup/domain/solution/SolutionRepositoryCustom.java +++ b/backend/src/main/java/develup/domain/solution/SolutionRepositoryCustom.java @@ -9,9 +9,10 @@ import java.util.List; import java.util.Optional; import com.querydsl.core.types.Projections; -import com.querydsl.jpa.JPAExpressions; +import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import develup.domain.solution.comment.SolutionCommentCount; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -20,36 +21,36 @@ public class SolutionRepositoryCustom { private final JPAQueryFactory queryFactory; + private final EntityManager entityManager; - public List findAllCompletedSolutionByHashTagName(String name) { + public List findAllCompletedSolutionByHashTagName(String missionTitle, String hashTagName) { return queryFactory.selectFrom(solution) .join(solution.mission, mission).fetchJoin() .join(mission.missionHashTags.hashTags, missionHashTag).fetchJoin() .join(missionHashTag.hashTag).fetchJoin() - .where( - solution.status.eq(SolutionStatus.COMPLETED) - .and(JPAExpressions.selectOne() - .from(missionHashTag) - .join(missionHashTag.hashTag) - .where( - missionHashTag.mission.id.eq(mission.id) - .and(missionHashTag.hashTag.name.eq(name)) - ) - .exists() - ) - ) + .where(eqCompleted(), eqMissionTitle(missionTitle), eqHashTagName(hashTagName)) .orderBy(solution.id.desc()) .fetch(); } - public List findAllCompletedSolution() { - return queryFactory.selectFrom(solution) - .join(solution.mission, mission).fetchJoin() - .join(mission.missionHashTags.hashTags, missionHashTag).fetchJoin() - .join(missionHashTag.hashTag).fetchJoin() - .where(solution.status.eq(SolutionStatus.COMPLETED)) - .orderBy(solution.id.desc()) - .fetch(); + private BooleanExpression eqCompleted() { + return solution.status.eq(SolutionStatus.COMPLETED); + } + + private BooleanExpression eqMissionTitle(String missionTitle) { + if ("all".equalsIgnoreCase(missionTitle)) { + return null; + } + + return mission.title.eq(missionTitle); + } + + private BooleanExpression eqHashTagName(String hashTagName) { + if ("all".equalsIgnoreCase(hashTagName)) { + return null; + } + + return missionHashTag.hashTag.name.eq(hashTagName); } public Optional findFetchById(Long solutionId) { @@ -67,6 +68,9 @@ public void deleteAllComments(Long solutionId) { queryFactory.delete(solutionComment) .where(solutionComment.solution.id.eq(solutionId)) .execute(); + + entityManager.flush(); + entityManager.clear(); } public List findAllSolutionCommentCounts() { diff --git a/backend/src/main/java/develup/domain/solution/comment/SolutionCommentCounts.java b/backend/src/main/java/develup/domain/solution/comment/SolutionCommentCounts.java deleted file mode 100644 index 52038ec4..00000000 --- a/backend/src/main/java/develup/domain/solution/comment/SolutionCommentCounts.java +++ /dev/null @@ -1,23 +0,0 @@ -package develup.domain.solution.comment; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class SolutionCommentCounts { - - private final Map solutionCommentCounts; - - public SolutionCommentCounts(List solutionCommentCounts) { - this.solutionCommentCounts = toMap(solutionCommentCounts); - } - - private Map toMap(List solutionCommentCounts) { - return solutionCommentCounts.stream() - .collect(Collectors.toMap(SolutionCommentCount::id, SolutionCommentCount::count)); - } - - public Long getCount(Long solutionId) { - return solutionCommentCounts.getOrDefault(solutionId, 0L); - } -} diff --git a/backend/src/main/java/develup/domain/solution/comment/SolutionCommentRepositoryCustom.java b/backend/src/main/java/develup/domain/solution/comment/SolutionCommentRepositoryCustom.java index 5d9322bc..745c132f 100644 --- a/backend/src/main/java/develup/domain/solution/comment/SolutionCommentRepositoryCustom.java +++ b/backend/src/main/java/develup/domain/solution/comment/SolutionCommentRepositoryCustom.java @@ -24,7 +24,7 @@ public List findAllBySolutionIdOrderByCreatedAtAsc(Long solutio .fetch(); } - public List findAllMySolutionComment(Long memberId) { + public List findAllMySolutionCommentOrderByDesc(Long memberId) { return queryFactory .select(Projections.constructor(MySolutionComment.class, solutionComment.id, @@ -37,6 +37,7 @@ public List findAllMySolutionComment(Long memberId) { .join(solutionComment.solution) .join(solutionComment.member) .where(solutionComment.member.id.eq(memberId).and(solutionComment.deletedAt.isNull())) + .orderBy(solutionComment.createdAt.desc()) .fetch(); } diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql index f6bee9a6..876824a7 100644 --- a/backend/src/main/resources/data.sql +++ b/backend/src/main/resources/data.sql @@ -46,19 +46,19 @@ VALUES (1, 1), (6, 4), (6, 5); -INSERT INTO solution (mission_id, member_id, title, description, url, status, created_at) +INSERT INTO solution (mission_id, member_id, title, description, url, status, created_at, submitted_at) VALUES (1, 1, '릴리 미션 제출합니다.', '안녕하세요. 잘 부탁 드립니다.', 'https://github.com/develup/mission/pull/1', 'COMPLETED', - '2024-08-16 13:40:00'), + '2024-08-16 13:40:00', '2024-08-17 13:40:00'), (1, 2, '아톰 미션 제출합니다.', '안녕하세요. 잘 부탁 드립니다.', 'https://github.com/develup/mission/pull/1', 'COMPLETED', - '2024-08-16 13:40:00'), + '2024-08-16 13:40:00', '2024-08-18 13:40:00'), (1, 3, '라이언 미션 제출합니다.', '안녕하세요. 잘 부탁 드립니다.', 'https://github.com/develup/mission/pull/1', 'COMPLETED', - '2024-08-16 13:40:00'), + '2024-08-16 13:40:00', '2024-08-16 13:40:00'), (2, 1, '아톰 미션 제출합니다.', '안녕하세요. 잘 부탁 드립니다.', 'https://github.com/develup/mission/pull/1', 'COMPLETED', - '2024-08-16 13:40:00'), + '2024-08-16 13:40:00', '2024-08-18 13:40:00'), (2, 2, '릴리 미션 제출합니다.', '안녕하세요. 잘 부탁 드립니다.', 'https://github.com/develup/mission/pull/1', 'COMPLETED', - '2024-08-16 13:40:00'), + '2024-08-16 13:40:00', '2024-08-19 13:40:00'), (2, 3, '아톰 미션 제출합니다.', '안녕하세요. 잘 부탁 드립니다.', 'https://github.com/develup/mission/pull/1', 'COMPLETED', - '2024-08-16 13:40:00'); + '2024-08-16 13:40:00', '2024-08-19 13:40:00'); -- root-1 -- ㄴ root-1-1 diff --git a/backend/src/main/resources/db/migration/V2__add_solution_submitted_at.sql b/backend/src/main/resources/db/migration/V2__add_solution_submitted_at.sql new file mode 100644 index 00000000..c16ee993 --- /dev/null +++ b/backend/src/main/resources/db/migration/V2__add_solution_submitted_at.sql @@ -0,0 +1,2 @@ +ALTER TABLE solution + ADD COLUMN submitted_at TIMESTAMP(6) NULL; diff --git a/backend/src/main/resources/db/migration/V3__remove_comment_foreign_key.sql b/backend/src/main/resources/db/migration/V3__remove_comment_foreign_key.sql new file mode 100644 index 00000000..45af94f5 --- /dev/null +++ b/backend/src/main/resources/db/migration/V3__remove_comment_foreign_key.sql @@ -0,0 +1,2 @@ +ALTER TABLE discussion_comment DROP FOREIGN KEY fk_discussion_comment_discussion_comment; +ALTER TABLE solution_comment DROP FOREIGN KEY fk_solution_comment_solution_comment; diff --git a/backend/src/test/java/develup/api/DiscussionApiTest.java b/backend/src/test/java/develup/api/DiscussionApiTest.java index 9582ba66..c426d7ff 100644 --- a/backend/src/test/java/develup/api/DiscussionApiTest.java +++ b/backend/src/test/java/develup/api/DiscussionApiTest.java @@ -31,7 +31,7 @@ import org.mockito.BDDMockito; import org.springframework.http.MediaType; -public class DiscussionApiTest extends ApiTestSupport { +class DiscussionApiTest extends ApiTestSupport { @Test @DisplayName("디스커션 목록을 조회한다.") @@ -48,6 +48,7 @@ void getSolutions() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.data[0].id", equalTo(1))) .andExpect(jsonPath("$.data[0].title", equalTo("루터회관 흡연단속 구현에 대한 고찰"))) + .andExpect(jsonPath("$.data[0].content", equalTo("루터회관 흡연단속을 구현하면서 느낀 점을 공유합니다."))) .andExpect(jsonPath("$.data[0].mission", equalTo("루터회관 흡연단속"))) .andExpect(jsonPath("$.data[0].hashTags[0].id", is(1))) .andExpect(jsonPath("$.data[0].hashTags[0].name", equalTo("JAVA"))) @@ -119,6 +120,7 @@ void getMyDiscussions() throws Exception { new SummarizedDiscussionResponse( 1L, "루터회관 흡연단속 구현에 대한 고찰", + "루터회관 흡연단속을 구현하면서 느낀 점을 공유합니다.", "루터회관 흡연단속", hashTags, memberResponse, @@ -135,6 +137,7 @@ void getMyDiscussions() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.data[0].id", equalTo(1))) .andExpect(jsonPath("$.data[0].title", equalTo("루터회관 흡연단속 구현에 대한 고찰"))) + .andExpect(jsonPath("$.data[0].content", equalTo("루터회관 흡연단속을 구현하면서 느낀 점을 공유합니다."))) .andExpect(jsonPath("$.data[0].mission", equalTo("루터회관 흡연단속"))) .andExpect(jsonPath("$.data[0].hashTags[0].id", equalTo(1))) .andExpect(jsonPath("$.data[0].hashTags[0].name", equalTo("JAVA"))) diff --git a/backend/src/test/java/develup/api/DiscussionCommentApiTest.java b/backend/src/test/java/develup/api/DiscussionCommentApiTest.java index 172f7b32..53c7c061 100644 --- a/backend/src/test/java/develup/api/DiscussionCommentApiTest.java +++ b/backend/src/test/java/develup/api/DiscussionCommentApiTest.java @@ -55,7 +55,7 @@ void getComments() throws Exception { @DisplayName("내가 디스커션에 작성한 댓글 목록을 조회한다.") void getMyComments() throws Exception { LocalDateTime now = LocalDateTime.now(); - MyDiscussionCommentResponse myDiscussionCommentResponse = new MyDiscussionCommentResponse(1L, 1L, "댓글 내용", now, "디스커션 제목", 123L); + MyDiscussionCommentResponse myDiscussionCommentResponse = new MyDiscussionCommentResponse(1L, 1L, "댓글 내용", now, "디스커션 제목"); List responses = List.of(myDiscussionCommentResponse); BDDMockito.given(discussionCommentReadService.getMyComments(any())) @@ -69,8 +69,7 @@ void getMyComments() throws Exception { .andExpect(jsonPath("$.data[0].discussionId", equalTo(1))) .andExpect(jsonPath("$.data[0].content", equalTo("댓글 내용"))) .andExpect(jsonPath("$.data[0].createdAt").exists()) - .andExpect(jsonPath("$.data[0].discussionTitle", equalTo("디스커션 제목"))) - .andExpect(jsonPath("$.data[0].discussionCommentCount", equalTo(123))); + .andExpect(jsonPath("$.data[0].discussionTitle", equalTo("디스커션 제목"))); } @Test diff --git a/backend/src/test/java/develup/api/SolutionApiTest.java b/backend/src/test/java/develup/api/SolutionApiTest.java index 5bde2c6a..04646491 100644 --- a/backend/src/test/java/develup/api/SolutionApiTest.java +++ b/backend/src/test/java/develup/api/SolutionApiTest.java @@ -41,7 +41,7 @@ void getSolutions() throws Exception { SummarizedSolutionResponse.from(createSolution()), SummarizedSolutionResponse.from(createSolution()) ); - BDDMockito.given(solutionReadService.getCompletedSummaries(any())) + BDDMockito.given(solutionReadService.getCompletedSummaries(any(), any())) .willReturn(responses); mockMvc.perform(get("/solutions")) diff --git a/backend/src/test/java/develup/api/SolutionCommentApiTest.java b/backend/src/test/java/develup/api/SolutionCommentApiTest.java index 53de8370..890e13dd 100644 --- a/backend/src/test/java/develup/api/SolutionCommentApiTest.java +++ b/backend/src/test/java/develup/api/SolutionCommentApiTest.java @@ -55,7 +55,7 @@ void getComments() throws Exception { @DisplayName("내가 솔루션에 작성한 댓글 목록을 조회한다.") void getMyComments() throws Exception { LocalDateTime now = LocalDateTime.now(); - MySolutionCommentResponse mySolutionCommentResponse = new MySolutionCommentResponse(1L, 1L, "댓글 내용", now, "솔루션 제목", 123L); + MySolutionCommentResponse mySolutionCommentResponse = new MySolutionCommentResponse(1L, 1L, "댓글 내용", now, "솔루션 제목"); List responses = List.of(mySolutionCommentResponse); BDDMockito.given(solutionCommentReadService.getMyComments(any())) @@ -69,8 +69,7 @@ void getMyComments() throws Exception { .andExpect(jsonPath("$.data[0].solutionId", equalTo(1))) .andExpect(jsonPath("$.data[0].content", equalTo("댓글 내용"))) .andExpect(jsonPath("$.data[0].createdAt").exists()) - .andExpect(jsonPath("$.data[0].solutionTitle", equalTo("솔루션 제목"))) - .andExpect(jsonPath("$.data[0].solutionCommentCount", equalTo(123))); + .andExpect(jsonPath("$.data[0].solutionTitle", equalTo("솔루션 제목"))); } @Test diff --git a/backend/src/test/java/develup/application/discussion/comment/DiscussionCommentReadServiceTest.java b/backend/src/test/java/develup/application/discussion/comment/DiscussionCommentReadServiceTest.java index 50106fee..6f3a4d28 100644 --- a/backend/src/test/java/develup/application/discussion/comment/DiscussionCommentReadServiceTest.java +++ b/backend/src/test/java/develup/application/discussion/comment/DiscussionCommentReadServiceTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; import java.time.LocalDateTime; import java.util.List; @@ -114,54 +113,22 @@ void getMyComments() { Long memberId = discussion.getMember().getId(); List myComments = discussionCommentReadService.getMyComments(memberId); - assertAll( - () -> assertThat(myComments).hasSize(10), - () -> assertThat(myComments.getFirst().discussionCommentCount()).isEqualTo(11) - ); + assertThat(myComments).hasSize(10); } - private DiscussionComment createDiscussionComment(Discussion discussion) { + private void createDiscussionComment(Discussion discussion) { DiscussionComment discussionComment = DiscussionCommentTestData.defaultDiscussionComment() .withDiscussion(discussion) .withMember(discussion.getMember()) .build(); - return discussionCommentRepository.save(discussionComment); + discussionCommentRepository.save(discussionComment); } - private DiscussionComment createDiscussionComment(Discussion discussion, Member writer) { + private void createDiscussionComment(Discussion discussion, Member writer) { DiscussionComment discussionComment = DiscussionCommentTestData.defaultDiscussionComment() .withDiscussion(discussion) .withMember(writer) .build(); - return discussionCommentRepository.save(discussionComment); - } - - @Test - @DisplayName("사용자가 작성한 댓글을 조회시 솔루션에 달린 댓글 수는 부모 댓글만 반영한다.") - void getMyCommentsWhenContainReplyComments() { - Discussion discussion = createRootDiscussion(); - Member otherMember = memberRepository.save(MemberTestData.defaultMember().build()); - DiscussionComment parentComment = createDiscussionComment(discussion, otherMember); - - for (int i = 0; i < 10; i++) { - createDiscussionReplyComment(discussion, parentComment); - } - - Long memberId = discussion.getMember().getId(); - List myComments = discussionCommentReadService.getMyComments(memberId); - - assertAll( - () -> assertThat(myComments).hasSize(10), - () -> assertThat(myComments.getFirst().discussionCommentCount()).isEqualTo(1) - ); - } - - private DiscussionComment createDiscussionReplyComment(Discussion discussion, DiscussionComment parentComment) { - DiscussionComment discussionComment = DiscussionCommentTestData.defaultDiscussionComment() - .withDiscussion(discussion) - .withParentCommentId(parentComment.getId()) - .withMember(discussion.getMember()) - .build(); - return discussionCommentRepository.save(discussionComment); + discussionCommentRepository.save(discussionComment); } } diff --git a/backend/src/test/java/develup/application/mission/MissionReadServiceTest.java b/backend/src/test/java/develup/application/mission/MissionReadServiceTest.java index b6172e77..60867f05 100644 --- a/backend/src/test/java/develup/application/mission/MissionReadServiceTest.java +++ b/backend/src/test/java/develup/application/mission/MissionReadServiceTest.java @@ -101,7 +101,7 @@ void getById_started() { } @Test - @DisplayName("사용자가 시작한 미션 목록을 조회한다.") + @DisplayName("사용자가 시작한 미션 목록을 역순으로 조회한다.") void getInProgressMissions() { Member member = memberRepository.save(MemberTestData.defaultMember().build()); @@ -123,6 +123,9 @@ void getInProgressMissions() { List inProgressMissions = missionReadService.getInProgressMissions(member.getId()); assertThat(inProgressMissions).hasSize(2); + assertThat(inProgressMissions) + .map(MissionResponse::id) + .containsExactlyInAnyOrder(otherMission.getId(), mission.getId()); } private Mission createMission() { diff --git a/backend/src/test/java/develup/application/solution/SolutionReadServiceTest.java b/backend/src/test/java/develup/application/solution/SolutionReadServiceTest.java index bbb6f37f..d3ff7934 100644 --- a/backend/src/test/java/develup/application/solution/SolutionReadServiceTest.java +++ b/backend/src/test/java/develup/application/solution/SolutionReadServiceTest.java @@ -2,7 +2,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import java.util.List; import develup.api.exception.DevelupException; import develup.domain.member.Member; import develup.domain.member.MemberRepository; @@ -44,19 +46,36 @@ void getById() { } @Test - @DisplayName("나의 솔루션 리스트를 조회한다.") + @DisplayName("나의 솔루션 리스트를 제출 일자 역순으로 조회한다.") void getSubmittedSolutionsByMemberId() { Member member = memberRepository.save(MemberTestData.defaultMember().build()); Mission mission = missionRepository.save(MissionTestData.defaultMission().build()); - Solution solution = SolutionTestData.defaultSolution() + Solution solution1 = SolutionTestData.defaultSolution() + .withId(1L) + .withMember(member) + .withMission(mission) + .withStatus(SolutionStatus.COMPLETED) + .build(); + Solution solution2 = SolutionTestData.defaultSolution() + .withId(2L) .withMember(member) .withMission(mission) .withStatus(SolutionStatus.COMPLETED) .build(); - solutionRepository.save(solution); + solutionRepository.save(solution1); + solutionRepository.save(solution2); + + List mySolutions = solutionReadService.getSubmittedSolutionsByMemberId(member.getId()); + + assertAll( + () -> assertThat(mySolutions).hasSize(2), + () -> assertThat(mySolutions) + .map(MySolutionResponse::id) + .containsExactly(solution2.getId(), solution1.getId()) + ); + - assertThat(solutionReadService.getSubmittedSolutionsByMemberId(member.getId())).hasSize(1); } @Test diff --git a/backend/src/test/java/develup/application/solution/comment/SolutionCommentReadServiceTest.java b/backend/src/test/java/develup/application/solution/comment/SolutionCommentReadServiceTest.java index 01f903cb..16e87fae 100644 --- a/backend/src/test/java/develup/application/solution/comment/SolutionCommentReadServiceTest.java +++ b/backend/src/test/java/develup/application/solution/comment/SolutionCommentReadServiceTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; import java.time.LocalDateTime; import java.util.List; @@ -114,54 +113,22 @@ void getMyComments() { Long memberId = solution.getMember().getId(); List myComments = solutionCommentReadService.getMyComments(memberId); - assertAll( - () -> assertThat(myComments).hasSize(10), - () -> assertThat(myComments.getFirst().solutionCommentCount()).isEqualTo(11) - ); + assertThat(myComments).hasSize(10); } - private SolutionComment createSolutionComment(Solution solution) { + private void createSolutionComment(Solution solution) { SolutionComment solutionComment = SolutionCommentTestData.defaultSolutionComment() .withSolution(solution) .withMember(solution.getMember()) .build(); - return solutionCommentRepository.save(solutionComment); + solutionCommentRepository.save(solutionComment); } - private SolutionComment createSolutionComment(Solution solution, Member writer) { + private void createSolutionComment(Solution solution, Member writer) { SolutionComment solutionComment = SolutionCommentTestData.defaultSolutionComment() .withSolution(solution) .withMember(writer) .build(); - return solutionCommentRepository.save(solutionComment); - } - - @Test - @DisplayName("사용자가 작성한 댓글을 조회시 솔루션에 달린 댓글 수는 부모 댓글만 반영한다.") - void getMyCommentsWhenContainReplyComments() { - Solution solution = createSolution(); - Member otherMember = memberRepository.save(MemberTestData.defaultMember().build()); - SolutionComment parentComment = createSolutionComment(solution, otherMember); - - for (int i = 0; i < 10; i++) { - createSolutionReplyComment(solution, parentComment); - } - - Long memberId = solution.getMember().getId(); - List myComments = solutionCommentReadService.getMyComments(memberId); - - assertAll( - () -> assertThat(myComments).hasSize(10), - () -> assertThat(myComments.getFirst().solutionCommentCount()).isEqualTo(1) - ); - } - - private SolutionComment createSolutionReplyComment(Solution solution, SolutionComment parentComment) { - SolutionComment solutionComment = SolutionCommentTestData.defaultSolutionComment() - .withParentCommentId(parentComment.getId()) - .withSolution(solution) - .withMember(solution.getMember()) - .build(); - return solutionCommentRepository.save(solutionComment); + solutionCommentRepository.save(solutionComment); } } diff --git a/backend/src/test/java/develup/domain/discussion/DiscussionRepositoryCustomTest.java b/backend/src/test/java/develup/domain/discussion/DiscussionRepositoryCustomTest.java index b485ca06..9299054c 100644 --- a/backend/src/test/java/develup/domain/discussion/DiscussionRepositoryCustomTest.java +++ b/backend/src/test/java/develup/domain/discussion/DiscussionRepositoryCustomTest.java @@ -182,7 +182,7 @@ void findFetchByIdWithoutMissionAndHashTag() { } @Test - @DisplayName("멤버 식별자를 통해 디스커션을 조회한다.") + @DisplayName("멤버 식별자를 통해 디스커션을 작성일자 역순으로 조회한다.") @Transactional void findByMemberId() { Member member1 = memberRepository.save(MemberTestData.defaultMember().withId(1L).build()); @@ -222,12 +222,20 @@ void findByMemberId() { discussionRepository.save(discussionByMember1_4); discussionRepository.save(discussionByMember2); - List discussionsByMember1 = discussionRepositoryCustom.findAllByMemberId(member1.getId()); - List discussionsByMember2 = discussionRepositoryCustom.findAllByMemberId(member2.getId()); + List discussionsByMember1 = discussionRepositoryCustom.findAllByMemberIdOrderByDesc(member1.getId()); + List discussionsByMember2 = discussionRepositoryCustom.findAllByMemberIdOrderByDesc(member2.getId()); assertAll( () -> assertThat(discussionsByMember1).hasSize(4), - () -> assertThat(discussionsByMember2).hasSize(1) + () -> assertThat(discussionsByMember2).hasSize(1), + () -> assertThat(discussionsByMember1) + .map(Discussion::getId) + .containsExactly( + discussionByMember1_4.getId(), + discussionByMember1_3.getId(), + discussionByMember1_2.getId(), + discussionByMember1_1.getId() + ) ); } diff --git a/backend/src/test/java/develup/domain/discussion/comment/DiscussionCommentRepositoryCustomTest.java b/backend/src/test/java/develup/domain/discussion/comment/DiscussionCommentRepositoryCustomTest.java index 9f3abf29..be377c4b 100644 --- a/backend/src/test/java/develup/domain/discussion/comment/DiscussionCommentRepositoryCustomTest.java +++ b/backend/src/test/java/develup/domain/discussion/comment/DiscussionCommentRepositoryCustomTest.java @@ -1,6 +1,7 @@ package develup.domain.discussion.comment; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import java.time.LocalDateTime; import java.util.ArrayList; @@ -49,10 +50,16 @@ void getMyComments() { } createRootDiscussionComment(); - List myComments = discussionCommentRepositoryCustom.findAllMyDiscussionComment(member.getId()); + List myComments = discussionCommentRepositoryCustom.findAllMyDiscussionCommentOrderByCreatedAtDesc(member.getId()); + + assertAll( + () -> assertThat(myComments) + .hasSize(discussionComments.size()), + () -> assertThat(myComments) + .map(MyDiscussionComment::id) + .containsExactly(4L, 3L, 2L, 1L) + ); - assertThat(myComments) - .hasSize(discussionComments.size()); } @Test @@ -68,7 +75,7 @@ void getMyCommentsWithDelete() { createRootDiscussionComment(); createRootDeletedDiscussionComment(discussion, member); - List myComments = discussionCommentRepositoryCustom.findAllMyDiscussionComment(member.getId()); + List myComments = discussionCommentRepositoryCustom.findAllMyDiscussionCommentOrderByCreatedAtDesc(member.getId()); assertThat(myComments) .hasSize(discussionComments.size()); diff --git a/backend/src/test/java/develup/domain/solution/SolutionRepositoryCustomTest.java b/backend/src/test/java/develup/domain/solution/SolutionRepositoryCustomTest.java index 767389f8..7b7b8fce 100644 --- a/backend/src/test/java/develup/domain/solution/SolutionRepositoryCustomTest.java +++ b/backend/src/test/java/develup/domain/solution/SolutionRepositoryCustomTest.java @@ -12,15 +12,19 @@ import develup.domain.mission.Mission; import develup.domain.mission.MissionHashTag; import develup.domain.mission.MissionRepository; +import develup.domain.solution.comment.SolutionComment; +import develup.domain.solution.comment.SolutionCommentRepository; import develup.support.IntegrationTestSupport; import develup.support.data.HashTagTestData; import develup.support.data.MemberTestData; import develup.support.data.MissionTestData; +import develup.support.data.SolutionCommentTestData; import develup.support.data.SolutionTestData; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; public class SolutionRepositoryCustomTest extends IntegrationTestSupport { @@ -30,6 +34,9 @@ public class SolutionRepositoryCustomTest extends IntegrationTestSupport { @Autowired private SolutionRepository solutionRepository; + @Autowired + private SolutionCommentRepository solutionCommentRepository; + @Autowired private MemberRepository memberRepository; @@ -43,9 +50,10 @@ public class SolutionRepositoryCustomTest extends IntegrationTestSupport { @DisplayName("주어진 해시태그가 포함된 완료된 솔루션을 조회할 수 있다.") void findAllCompletedSolutionByHashTag() { Member member = memberRepository.save(MemberTestData.defaultMember().build()); - HashTag hashTag = hashTagRepository.save(HashTagTestData.defaultHashTag().withName("JAVA").build()); - Mission mission1 = missionRepository.save(MissionTestData.defaultMission().withHashTags(List.of(hashTag)).build()); - Mission mission2 = missionRepository.save(MissionTestData.defaultMission().build()); + HashTag hashTag1 = hashTagRepository.save(HashTagTestData.defaultHashTag().withName("JAVA").build()); + HashTag hashTag2 = hashTagRepository.save(HashTagTestData.defaultHashTag().withName("JS").build()); + Mission mission1 = missionRepository.save(MissionTestData.defaultMission().withHashTags(List.of(hashTag1)).build()); + Mission mission2 = missionRepository.save(MissionTestData.defaultMission().withHashTags(List.of(hashTag2)).build()); Solution solution1 = SolutionTestData.defaultSolution() .withMember(member) .withMission(mission1) @@ -59,13 +67,40 @@ void findAllCompletedSolutionByHashTag() { solutionRepository.saveAll(List.of(solution1, solution2)); - List solutions = solutionRepositoryCustom.findAllCompletedSolutionByHashTagName("JAVA"); + List solutions = solutionRepositoryCustom.findAllCompletedSolutionByHashTagName("all", "JAVA"); assertThat(solutions) .map(Solution::getHashTags) .flatMap(Function.identity()) .map(MissionHashTag::getHashTag) - .contains(hashTag); + .contains(hashTag1); + } + + @Test + @DisplayName("주어진 미션에 대한 완료된 솔루션을 조회할 수 있다.") + void findAllCompletedSolutionByMissionName() { + Member member = memberRepository.save(MemberTestData.defaultMember().build()); + HashTag hashTag = hashTagRepository.save(HashTagTestData.defaultHashTag().withName("JAVA").build()); + Mission otherMission = missionRepository.save(MissionTestData.defaultMission().withHashTags(List.of(hashTag)).build()); + Mission targetMission = missionRepository.save(MissionTestData.defaultMission().withHashTags(List.of(hashTag)).withTitle("테스트 미션 제목").build()); + Solution otherSolution = SolutionTestData.defaultSolution() + .withMember(member) + .withMission(otherMission) + .withStatus(SolutionStatus.COMPLETED) + .build(); + Solution tragetSolution = SolutionTestData.defaultSolution() + .withMember(member) + .withMission(targetMission) + .withStatus(SolutionStatus.COMPLETED) + .build(); + + solutionRepository.saveAll(List.of(otherSolution, tragetSolution)); + + List solutions = solutionRepositoryCustom.findAllCompletedSolutionByHashTagName("테스트 미션 제목", "all"); + + assertThat(solutions) + .map(Solution::getId) + .containsOnly(targetMission.getId()); } @Test @@ -75,7 +110,7 @@ void findAllCompletedSolution() { createSolution(SolutionStatus.COMPLETED); createSolution(SolutionStatus.IN_PROGRESS); - List actual = solutionRepositoryCustom.findAllCompletedSolution(); + List actual = solutionRepositoryCustom.findAllCompletedSolutionByHashTagName("all", "all"); assertThat(actual).hasSize(2); } @@ -113,6 +148,29 @@ void findFetchById() { ); } + @Test + @DisplayName("솔루션 댓글을 모두 지운다.") + @Transactional + void deleteAllComments() { + HashTag hashTag = hashTagRepository.save(HashTagTestData.defaultHashTag().build()); + Member member = memberRepository.save(MemberTestData.defaultMember().build()); + Mission mission = MissionTestData.defaultMission().withHashTags(List.of(hashTag)).build(); + missionRepository.save(mission); + Solution solution = SolutionTestData.defaultSolution().withId(1L).withMember(member) + .withMission(mission) + .build(); + SolutionComment comment = SolutionCommentTestData.defaultSolutionComment() + .withSolution(solution) + .withMember(member) + .build(); + solutionRepository.save(solution); + solutionCommentRepository.save(comment); + + solutionRepositoryCustom.deleteAllComments(solution.getId()); + + assertThat(solutionCommentRepository.findById(comment.getId())).isEmpty(); + } + private void createSolution(SolutionStatus status) { HashTag hashTag = hashTagRepository.save(HashTagTestData.defaultHashTag().withName("A").build()); Member member = memberRepository.save(MemberTestData.defaultMember().build()); diff --git a/backend/src/test/java/develup/domain/solution/SolutionRepositoryTest.java b/backend/src/test/java/develup/domain/solution/SolutionRepositoryTest.java index 9f0667e9..c908c918 100644 --- a/backend/src/test/java/develup/domain/solution/SolutionRepositoryTest.java +++ b/backend/src/test/java/develup/domain/solution/SolutionRepositoryTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import develup.domain.hashtag.HashTag; @@ -109,8 +110,8 @@ void findByMember_IdAndMission_IdAndStatus() { } @Test - @DisplayName("멤버 식별자와 특정 상태에 해당하는 솔루션을 조회한다.") - void findByMember_IdAndStatus() { + @DisplayName("멤버 식별자와 특정 상태에 해당하는 솔루션을 제출일자 역순으로 조회한다.") + void findByMember_IdAndStatusOrderBySubmittedAtDesc() { Member member = memberRepository.save(MemberTestData.defaultMember().build()); Mission mission = missionRepository.save(MissionTestData.defaultMission().build()); SolutionStatus inProgress = SolutionStatus.IN_PROGRESS; @@ -119,21 +120,31 @@ void findByMember_IdAndStatus() { .withMember(member) .withMission(mission) .withStatus(inProgress) + .withSubmittedAt(LocalDateTime.of(2024, 1, 1, 0, 0)) .build(); - Solution completeSolution = SolutionTestData.defaultSolution() + Solution completeSolution1 = SolutionTestData.defaultSolution() + .withMember(member) + .withMission(mission) + .withStatus(completed) + .withSubmittedAt(LocalDateTime.of(2024, 1, 1, 0, 0)) + .build(); + Solution completeSolution2 = SolutionTestData.defaultSolution() .withMember(member) .withMission(mission) .withStatus(completed) + .withSubmittedAt(LocalDateTime.of(2024, 1, 2, 0, 0)) .build(); solutionRepository.save(inProgressSolution); - solutionRepository.save(completeSolution); + solutionRepository.save(completeSolution1); + solutionRepository.save(completeSolution2); - List solutionInProgress = solutionRepository.findAllByMember_IdAndStatus(member.getId(), inProgress); - List solutionCompleted = solutionRepository.findAllByMember_IdAndStatus(member.getId(), completed); + List solutionInProgress = solutionRepository.findAllByMember_IdAndStatusOrderBySubmittedAtDesc(member.getId(), inProgress); + List solutionCompleted = solutionRepository.findAllByMember_IdAndStatusOrderBySubmittedAtDesc(member.getId(), completed); assertAll( () -> assertThat(solutionInProgress).hasSize(1), - () -> assertThat(solutionCompleted).hasSize(1) + () -> assertThat(solutionCompleted).hasSize(2), + () -> assertThat(solutionCompleted).containsExactly(completeSolution2, completeSolution1) ); } diff --git a/backend/src/test/java/develup/domain/solution/comment/SolutionCommentRepositoryCustomTest.java b/backend/src/test/java/develup/domain/solution/comment/SolutionCommentRepositoryCustomTest.java index b3873a16..759235a9 100644 --- a/backend/src/test/java/develup/domain/solution/comment/SolutionCommentRepositoryCustomTest.java +++ b/backend/src/test/java/develup/domain/solution/comment/SolutionCommentRepositoryCustomTest.java @@ -1,6 +1,7 @@ package develup.domain.solution.comment; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import java.time.LocalDateTime; import java.util.ArrayList; @@ -56,7 +57,7 @@ void findAllBySolutionIdOrderByCreatedAtAsc() { } @Test - @DisplayName("특정 회원이 작성한 댓글 목록을 조회한다.") + @DisplayName("특정 회원이 작성한 댓글 목록을 작성일자 역순으로 조회한다.") void getMyComments() { Solution solution = createSolution(); Member member = createMember(); @@ -67,10 +68,15 @@ void getMyComments() { } createSolutionComment(); - List myComments = solutionCommentRepositoryCustom.findAllMySolutionComment(member.getId()); + List myComments = solutionCommentRepositoryCustom.findAllMySolutionCommentOrderByDesc(member.getId()); - assertThat(myComments) - .hasSize(solutionComments.size()); + assertAll( + () -> assertThat(myComments) + .hasSize(solutionComments.size()), + () -> assertThat(myComments) + .map(MySolutionComment::id) + .containsExactly(4L, 3L, 2L, 1L) + ); } @Test @@ -86,7 +92,7 @@ void getMyCommentsWithDelete() { createSolutionComment(); createDeletedSolutionComment(solution, member); - List myComments = solutionCommentRepositoryCustom.findAllMySolutionComment(member.getId()); + List myComments = solutionCommentRepositoryCustom.findAllMySolutionCommentOrderByDesc(member.getId()); assertThat(myComments) .hasSize(solutionComments.size()); diff --git a/backend/src/test/java/develup/support/data/SolutionTestData.java b/backend/src/test/java/develup/support/data/SolutionTestData.java index 044cad87..3d331453 100644 --- a/backend/src/test/java/develup/support/data/SolutionTestData.java +++ b/backend/src/test/java/develup/support/data/SolutionTestData.java @@ -1,5 +1,6 @@ package develup.support.data; +import java.time.LocalDateTime; import develup.domain.member.Member; import develup.domain.mission.Mission; import develup.domain.solution.PullRequestUrl; @@ -16,7 +17,8 @@ public static SolutionBuilder defaultSolution() { .withTitle("루터회관 흡연단속 제출합니다.") .withDescription("안녕하세요. 피드백 잘 부탁 드려요.") .withUrl("https://github.com/develup/mission/pull/1") - .withStatus(SolutionStatus.COMPLETED); + .withStatus(SolutionStatus.COMPLETED) + .withSubmittedAt(LocalDateTime.now()); } public static class SolutionBuilder { @@ -28,6 +30,7 @@ public static class SolutionBuilder { private String description; private PullRequestUrl url; private SolutionStatus status; + private LocalDateTime submittedAt; public SolutionBuilder withId(Long id) { this.id = id; @@ -64,6 +67,11 @@ public SolutionBuilder withStatus(SolutionStatus status) { return this; } + public SolutionBuilder withSubmittedAt(LocalDateTime submittedAt) { + this.submittedAt = submittedAt; + return this; + } + public Solution build() { return new Solution( id, @@ -72,7 +80,8 @@ public Solution build() { title, description, url, - status + status, + submittedAt ); } } diff --git a/frontend/index.html b/frontend/index.html index c1ab66e6..132927a7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ - + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index be510ccd..a3549e1a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import Header from './components/Header'; import { GlobalLayout } from './styles/GlobalLayout'; import type { PropsWithChildren } from 'react'; import Footer from './components/Footer'; +import { ScrollToTopButton } from './components/common/ScrollToTopButton'; export default function App({ children }: PropsWithChildren) { return ( @@ -11,6 +12,7 @@ export default function App({ children }: PropsWithChildren) { {children}