From 6e67ea8b6f0bef0194f797d15b81bdfbf3241d0b Mon Sep 17 00:00:00 2001 From: alstn113 Date: Tue, 21 Jan 2025 12:32:48 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=EB=B9=84=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=8B=9C=20toast=EB=A5=BC=20=EB=9D=84=EC=9A=B0=EA=B3=A0=20r?= =?UTF-8?q?eturn=20=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/hooks/api/exam/useExamLikeManager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/hooks/api/exam/useExamLikeManager.ts b/web/src/hooks/api/exam/useExamLikeManager.ts index 11e235a..de589cf 100644 --- a/web/src/hooks/api/exam/useExamLikeManager.ts +++ b/web/src/hooks/api/exam/useExamLikeManager.ts @@ -97,6 +97,7 @@ const useExamLikeManager = ({ const toggleLike = () => { if (!user) { toast.error('로그인이 필요합니다.'); + return; } if (isLiked) { From 2e2cb8f15512447a1dce9505f43062fc2b5aa252 Mon Sep 17 00:00:00 2001 From: alstn113 Date: Tue, 21 Jan 2025 12:36:10 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20footer=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/layouts/base/Footer.tsx | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/web/src/components/layouts/base/Footer.tsx b/web/src/components/layouts/base/Footer.tsx index 35265c8..2c53543 100644 --- a/web/src/components/layouts/base/Footer.tsx +++ b/web/src/components/layouts/base/Footer.tsx @@ -15,23 +15,6 @@ const Footer = () => { @alstn113 -
- service: - - @fluffy - -
-
- email: - - alstn113@gmail.com - -
From f00b23e1a89068a78ec8371e7447f72d0f20b6fd Mon Sep 17 00:00:00 2001 From: alstn113 Date: Tue, 21 Jan 2025 13:09:34 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EB=82=B4=EA=B0=80=20=EC=A0=9C?= =?UTF-8?q?=EC=B6=9C=ED=95=9C=20=EC=8B=9C=ED=97=98=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=EC=97=90=20"=EB=82=B4?= =?UTF-8?q?=EA=B0=80=20=EC=A0=9C=EC=B6=9C=ED=95=9C"=EC=9D=98=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/persistence/ExamRepositoryImpl.java | 4 +- .../exam/domain/ExamRepositoryTest.java | 79 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/fluffy/exam/infra/persistence/ExamRepositoryImpl.java b/server/src/main/java/com/fluffy/exam/infra/persistence/ExamRepositoryImpl.java index 7f68a57..097310c 100644 --- a/server/src/main/java/com/fluffy/exam/infra/persistence/ExamRepositoryImpl.java +++ b/server/src/main/java/com/fluffy/exam/infra/persistence/ExamRepositoryImpl.java @@ -161,7 +161,7 @@ private List getMyExamIds(Pageable pageable, Long memberId, ExamStatus sta @Override public Page findSubmittedExamSummaries(Pageable pageable, Long memberId) { - JPAQuery countQuery = queryFactory.select(exam.count()) + JPAQuery countQuery = queryFactory.select(exam.countDistinct()) .from(exam) .join(submission).on(exam.id.eq(submission.examId)) .where(submission.memberId.eq(memberId)); @@ -184,7 +184,7 @@ public Page findSubmittedExamSummaries(Pageable pageabl .from(exam) .leftJoin(member).on(exam.memberId.eq(member.id)) .join(submission).on(exam.id.eq(submission.examId)) - .where(exam.id.in(examIds)) + .where(exam.id.in(examIds).and(submission.memberId.eq(memberId))) .groupBy( exam.id, exam.title, diff --git a/server/src/test/java/com/fluffy/exam/domain/ExamRepositoryTest.java b/server/src/test/java/com/fluffy/exam/domain/ExamRepositoryTest.java index 71b7e6d..cb77f2a 100644 --- a/server/src/test/java/com/fluffy/exam/domain/ExamRepositoryTest.java +++ b/server/src/test/java/com/fluffy/exam/domain/ExamRepositoryTest.java @@ -7,6 +7,10 @@ import com.fluffy.auth.domain.MemberRepository; import com.fluffy.exam.domain.dto.ExamSummaryDto; import com.fluffy.exam.domain.dto.MyExamSummaryDto; +import com.fluffy.exam.domain.dto.SubmittedExamSummaryDto; +import com.fluffy.submission.domain.Answer; +import com.fluffy.submission.domain.Submission; +import com.fluffy.submission.domain.SubmissionRepository; import com.fluffy.support.AbstractIntegrationTest; import com.fluffy.support.data.MemberTestData; import java.util.List; @@ -24,6 +28,9 @@ class ExamRepositoryTest extends AbstractIntegrationTest { @Autowired private MemberRepository memberRepository; + @Autowired + private SubmissionRepository submissionRepository; + @Test @DisplayName("출제된 시험 요약 목록을 조회할 수 있다.") void findPublishedExamSummaries() { @@ -133,4 +140,76 @@ void findMyExamSummaries() { .containsExactlyElementsOf(List.of(3L, 1L)) ); } + + @Test + @DisplayName("내가 제출한 시험 요약 목록을 조회할 수 있다.") + void findSubmittedExamSummaries() { + // given + Member member1 = MemberTestData.defaultMember().build(); + memberRepository.save(member1); + + Member member2 = MemberTestData.defaultMember().build(); + memberRepository.save(member2); + + Exam publishedExam1 = Exam.create("publishedExam1", member1.getId()); + publishedExam1.updateQuestions(List.of(Question.shortAnswer("질문1", "지문", "답1"))); + publishedExam1.publish(); + examRepository.save(publishedExam1); + + Exam publishedExam2 = Exam.create("publishedExam2", member1.getId()); + publishedExam2.updateQuestions(List.of(Question.shortAnswer("질문1", "지문", "답1"))); + publishedExam2.publish(); + examRepository.save(publishedExam2); + + submissionRepository.save(new Submission( + publishedExam1.getId(), + member1.getId(), + List.of(Answer.textAnswer(1L, "답1")) + )); + + submissionRepository.save(new Submission( + publishedExam2.getId(), + member1.getId(), + List.of(Answer.textAnswer(1L, "답2")) + )); + + submissionRepository.save(new Submission( + publishedExam1.getId(), + member2.getId(), + List.of(Answer.textAnswer(1L, "답3")) + )); + + submissionRepository.save(new Submission( + publishedExam1.getId(), + member1.getId(), + List.of(Answer.textAnswer(1L, "답1")) + )); + + // when + PageRequest pageable = PageRequest.of(0, 2); + Page submittedExamSummaries = examRepository.findSubmittedExamSummaries( + pageable, + member1.getId() + ); + + System.out.println(submittedExamSummaries.getContent()); + System.out.println(submittedExamSummaries.getTotalElements()); + System.out.println(submittedExamSummaries.getTotalPages()); + System.out.println(submittedExamSummaries.getNumber()); + System.out.println(submittedExamSummaries.getSize()); + + // then + assertAll( + () -> assertThat(submittedExamSummaries.getTotalElements()).isEqualTo(2), + () -> assertThat(submittedExamSummaries.getTotalPages()).isEqualTo(1), + () -> assertThat(submittedExamSummaries.getContent() + .stream() + .map(SubmittedExamSummaryDto::getSubmissionCount) + ).containsExactlyElementsOf(List.of(2L, 1L)), + () -> assertThat(submittedExamSummaries.getContent() + .stream() + .map(SubmittedExamSummaryDto::getTitle) + ).containsExactlyElementsOf(List.of("publishedExam1", "publishedExam2")) + ); + } } From 575142f6750796626a0eaa717002aca7d6c5aaef Mon Sep 17 00:00:00 2001 From: alstn113 Date: Tue, 21 Jan 2025 13:50:06 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20useExamLikeManager=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=ED=95=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/api/examAPI.ts | 13 +-- web/src/hooks/api/exam/useExamLikeManager.ts | 107 ++++++++----------- 2 files changed, 48 insertions(+), 72 deletions(-) diff --git a/web/src/api/examAPI.ts b/web/src/api/examAPI.ts index 4238a22..161fc05 100644 --- a/web/src/api/examAPI.ts +++ b/web/src/api/examAPI.ts @@ -68,17 +68,14 @@ export const ExamAPI = { return data; }, - like: async (examId: number, controller?: AbortController) => { - const { data } = await apiV1Client.post(`/exams/${examId}/like`, { - signal: controller?.signal, - }); + like: async (examId: number) => { + const { data } = await apiV1Client.post(`/exams/${examId}/like`); + return data; }, - unlike: async (examId: number, controller?: AbortController) => { - const { data } = await apiV1Client.delete(`/exams/${examId}/like`, { - signal: controller?.signal, - }); + unlike: async (examId: number) => { + const { data } = await apiV1Client.delete(`/exams/${examId}/like`); return data; }, }; diff --git a/web/src/hooks/api/exam/useExamLikeManager.ts b/web/src/hooks/api/exam/useExamLikeManager.ts index de589cf..ac4a479 100644 --- a/web/src/hooks/api/exam/useExamLikeManager.ts +++ b/web/src/hooks/api/exam/useExamLikeManager.ts @@ -5,7 +5,7 @@ import toast from 'react-hot-toast'; import useGetExamDetailSummary from '@/hooks/api/exam/useGetExamDetailSummary.ts'; import { ExamAPI, ExamDetailSummaryResponse } from '@/api/examAPI'; -interface useExamLikeManagerProps { +interface UseExamLikeManagerProps { examId: number; initialIsLiked: boolean; initialLikeCount: number; @@ -15,7 +15,7 @@ const useExamLikeManager = ({ examId, initialIsLiked, initialLikeCount, -}: useExamLikeManagerProps) => { +}: UseExamLikeManagerProps) => { const queryClient = useQueryClient(); const user = useUser(); @@ -24,88 +24,67 @@ const useExamLikeManager = ({ const debounceTimeout = useRef(null); - const invalidateQueryDebounced = () => { + const debounceInvalidateQueries = () => { if (debounceTimeout.current) { clearTimeout(debounceTimeout.current); } - debounceTimeout.current = setTimeout(async () => { - await queryClient.invalidateQueries({ + debounceTimeout.current = setTimeout(() => { + queryClient.invalidateQueries({ queryKey: useGetExamDetailSummary.getKey(examId), refetchType: 'all', }); }, 300); }; - const { mutate: likeExam } = useMutation({ - mutationFn: ExamAPI.like, - onMutate: async () => { - await queryClient.cancelQueries({ - queryKey: useGetExamDetailSummary.getKey(examId), - }); - - const prevData = queryClient.getQueryData( - useGetExamDetailSummary.getKey(examId), - ); - - setIsLiked(true); - setLikeCount(likeCount + 1); - - return prevData; - }, - onError: (_error, _variables, context) => { - if (context) { - queryClient.setQueryData(useGetExamDetailSummary.getKey(examId), context); - } - - setIsLiked(false); - setLikeCount(likeCount - 1); - }, - onSettled: async () => { - invalidateQueryDebounced(); - }, - }); - - const { mutate: unlikeExam } = useMutation({ - mutationFn: ExamAPI.unlike, - onMutate: async () => { - await queryClient.cancelQueries({ - queryKey: useGetExamDetailSummary.getKey(examId), - }); - - const prevData = queryClient.getQueryData( - useGetExamDetailSummary.getKey(examId), - ); - - setIsLiked(false); - setLikeCount(likeCount - 1); - - return prevData; - }, - onError: (_error, _variables, context) => { - if (context) { - queryClient.setQueryData(useGetExamDetailSummary.getKey(examId), context); - } + const useLikeMutation = ( + mutationFunction: (examId: number) => Promise, + isLikeAction: boolean, + ) => { + return useMutation({ + mutationFn: mutationFunction, + onMutate: async () => { + await queryClient.cancelQueries({ + queryKey: useGetExamDetailSummary.getKey(examId), + }); + + setIsLiked(isLikeAction); + setLikeCount((prevCount) => prevCount + (isLikeAction ? 1 : -1)); + + const previousData = queryClient.getQueryData( + useGetExamDetailSummary.getKey(examId), + ); + + return { previousData }; + }, + onError: (_error, _variables, context) => { + if (context) { + queryClient.setQueryData(useGetExamDetailSummary.getKey(examId), context.previousData); + } + + toast.error(`좋아요${isLikeAction ? '에' : ' 취소에'} 실패했습니다.`); + + setIsLiked(!isLikeAction); + setLikeCount((prevCount) => prevCount - (isLikeAction ? 1 : -1)); + }, + onSettled: debounceInvalidateQueries, + }); + }; - setIsLiked(true); - setLikeCount(likeCount + 1); - }, - onSettled: async () => { - invalidateQueryDebounced(); - }, - }); + const { mutate: like } = useLikeMutation(ExamAPI.like, true); + const { mutate: unlike } = useLikeMutation(ExamAPI.unlike, false); const toggleLike = () => { if (!user) { - toast.error('로그인이 필요합니다.'); + toast.error('좋아요를 누르려면 로그인이 필요합니다.'); return; } if (isLiked) { - unlikeExam(examId); + unlike(examId); return; } - likeExam(examId); + like(examId); }; return {