Skip to content

Commit

Permalink
Hot fix/v1.0.4 좋아요 로직 관련 문제 (#36)
Browse files Browse the repository at this point in the history
* fix: 비로그인 시 toast를 띄우고 return 한다.

* refactor: 불필요한 footer 제거

* fix: 내가 제출한 시험 목록 조회 쿼리에 "내가 제출한"의 필터링 추가

* refactor: useExamLikeManager 공통 로직 분할
  • Loading branch information
alstn113 authored Jan 21, 2025
1 parent 2b6204f commit 58428b5
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ private List<Long> getMyExamIds(Pageable pageable, Long memberId, ExamStatus sta

@Override
public Page<SubmittedExamSummaryDto> findSubmittedExamSummaries(Pageable pageable, Long memberId) {
JPAQuery<Long> countQuery = queryFactory.select(exam.count())
JPAQuery<Long> countQuery = queryFactory.select(exam.countDistinct())
.from(exam)
.join(submission).on(exam.id.eq(submission.examId))
.where(submission.memberId.eq(memberId));
Expand All @@ -184,7 +184,7 @@ public Page<SubmittedExamSummaryDto> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +28,9 @@ class ExamRepositoryTest extends AbstractIntegrationTest {
@Autowired
private MemberRepository memberRepository;

@Autowired
private SubmissionRepository submissionRepository;

@Test
@DisplayName("출제된 시험 요약 목록을 조회할 수 있다.")
void findPublishedExamSummaries() {
Expand Down Expand Up @@ -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<SubmittedExamSummaryDto> 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"))
);
}
}
13 changes: 5 additions & 8 deletions web/src/api/examAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,14 @@ export const ExamAPI = {
return data;
},

like: async (examId: number, controller?: AbortController) => {
const { data } = await apiV1Client.post<void>(`/exams/${examId}/like`, {
signal: controller?.signal,
});
like: async (examId: number) => {
const { data } = await apiV1Client.post<void>(`/exams/${examId}/like`);

return data;
},

unlike: async (examId: number, controller?: AbortController) => {
const { data } = await apiV1Client.delete<void>(`/exams/${examId}/like`, {
signal: controller?.signal,
});
unlike: async (examId: number) => {
const { data } = await apiV1Client.delete<void>(`/exams/${examId}/like`);
return data;
},
};
Expand Down
17 changes: 0 additions & 17 deletions web/src/components/layouts/base/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,6 @@ const Footer = () => {
@alstn113
</a>
</div>
<div>
service:
<a
href="https://github.com/alstn113/fluffy"
target="_blank"
rel="noopener noreferrer"
className="font-semibold hover:underline ml-1"
>
@fluffy
</a>
</div>
<div>
email:
<a href="mailto:[email protected]" className="font-semibold hover:underline ml-1">
[email protected]
</a>
</div>
</div>
</div>
</div>
Expand Down
108 changes: 44 additions & 64 deletions web/src/hooks/api/exam/useExamLikeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,7 +15,7 @@ const useExamLikeManager = ({
examId,
initialIsLiked,
initialLikeCount,
}: useExamLikeManagerProps) => {
}: UseExamLikeManagerProps) => {
const queryClient = useQueryClient();
const user = useUser();

Expand All @@ -24,87 +24,67 @@ const useExamLikeManager = ({

const debounceTimeout = useRef<number | null>(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<ExamDetailSummaryResponse>(
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<ExamDetailSummaryResponse>(
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<void>,
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<ExamDetailSummaryResponse>(
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 {
Expand Down

0 comments on commit 58428b5

Please sign in to comment.