Skip to content

Commit 2941571

Browse files
committed
Refactor: mutationOptions 도입 (#214)
* refactor: 불필요한 상태 제거 * refactor: mutationOptions 기반으로 리팩토링 * refactor: useIsMutating 훅을 통해, 편지 쓰기 뮤테이션 진행상태를 한번에 추적 * refactor: useIsMutating이 boolean을 반환하도록 수정 * refactor: 필요없는 파일 삭제 * refactor: 코드리뷰 반영
1 parent f5019e2 commit 2941571

11 files changed

Lines changed: 185 additions & 176 deletions

File tree

app/(sub)/capsule-detail/[invite-code]/[id]/letters/page.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { letterQueryOptions } from "@/shared/api/queries/letter";
55
import Grid from "@/shared/assets/icon/grid.svg";
66
import Layers from "@/shared/assets/icon/layers.svg";
77
import { useLetterImages } from "@/shared/hooks/use-letter-images";
8+
import type { CapsuleDetailRes } from "@/shared/types/api/capsule";
9+
import type { LetterListRes } from "@/shared/types/api/letter";
810
import LoadingSpinner from "@/shared/ui/loading-spinner";
911
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
1012
import { useParams, useRouter } from "next/navigation";
@@ -26,7 +28,7 @@ const CapsuleLettersPage = () => {
2628

2729
const { data: capsuleData, isLoading: isCapsuleLoading } = useQuery({
2830
...capsuleQueryOptions.capsuleDetail(capsuleId),
29-
select: (data) => ({
31+
select: (data: CapsuleDetailRes) => ({
3032
title: data.result.title,
3133
participantCount: data.result.participantCount,
3234
isFirstOpen: data.result.isFirstOpen,
@@ -42,7 +44,7 @@ const CapsuleLettersPage = () => {
4244
} = useInfiniteQuery(letterQueryOptions.letterList(capsuleId));
4345

4446
const footerRef = useIntersectionObserver<HTMLDivElement>(
45-
(entry) => {
47+
(entry: IntersectionObserverEntry) => {
4648
if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) {
4749
fetchNextPage();
4850
}
@@ -51,7 +53,8 @@ const CapsuleLettersPage = () => {
5153
);
5254

5355
const letters =
54-
letterData?.pages.flatMap((page) => page.result.letters) || [];
56+
letterData?.pages.flatMap((page: LetterListRes) => page.result.letters) ||
57+
[];
5558

5659
const totalLetterCount = letterData?.pages[0]?.result.totalElements || 0;
5760
const { imageUrls, isImageLoading } = useLetterImages(letters);

app/(sub)/capsule-detail/[invite-code]/[id]/page.tsx

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"use client";
2-
import { useLikeToggle } from "@/shared/api/mutations/capsule";
2+
import { capsuleMutationOptions } from "@/shared/api/mutations/capsule";
33
import { capsuleQueryOptions } from "@/shared/api/queries/capsule";
4+
import { capsuleQueryKeys } from "@/shared/api/queries/capsule";
45
import { userQueryOptions } from "@/shared/api/queries/user";
56
import MenuIcon from "@/shared/assets/icon/menu.svg";
67
import { PATH } from "@/shared/constants/path";
8+
import { useOverlay } from "@/shared/hooks/use-overlay";
79
import Dropdown from "@/shared/ui/dropdown";
810
import InfoToast from "@/shared/ui/info-toast";
911
import LikeButton from "@/shared/ui/like-button";
@@ -13,7 +15,9 @@ import NavbarDetail from "@/shared/ui/navbar/navbar-detail";
1315
import PopupReport from "@/shared/ui/popup/popup-report";
1416
import PopupWarningCapsule from "@/shared/ui/popup/popup-warning-capsule";
1517
import { formatDateTime } from "@/shared/utils/date";
16-
import { useQuery } from "@tanstack/react-query";
18+
import { oauthUtils } from "@/shared/utils/oauth";
19+
import { useMutation, useQuery } from "@tanstack/react-query";
20+
import { useQueryClient } from "@tanstack/react-query";
1721
import {
1822
useParams,
1923
usePathname,
@@ -25,30 +29,26 @@ import CaptionSection from "../../_components/caption-section";
2529
import InfoTitle from "../../_components/info-title";
2630
import OpenInfoSection from "../../_components/open-info-section";
2731
import ResponsiveFooter from "../../_components/responsive-footer";
28-
import { useLeaveCapsule } from "@/shared/api/mutations/capsule";
2932
import * as styles from "./page.css";
30-
import { oauthUtils } from "@/shared/utils/oauth";
31-
import { useOverlay } from "@/shared/hooks/use-overlay";
3233

3334
const CapsuleDetailPage = () => {
3435
const params = useParams();
3536
const id = params.id as string;
36-
const { mutate: likeToggle } = useLikeToggle();
37-
const { data, isLoading, isError } = useQuery(
37+
const queryClient = useQueryClient();
38+
const { data, isPending, isError } = useQuery(
3839
capsuleQueryOptions.capsuleDetail(id),
3940
);
40-
const { data: user } = useQuery(
41-
userQueryOptions.userInfo()
42-
);
43-
const { mutate: leaveCapsule } = useLeaveCapsule();
41+
const { data: user } = useQuery(userQueryOptions.userInfo());
42+
const { mutate: likeToggle } = useMutation(capsuleMutationOptions.like);
43+
const { mutate: leaveCapsule } = useMutation(capsuleMutationOptions.leave);
4444
const router = useRouter();
4545
const pathname = usePathname();
4646
const searchParams = useSearchParams();
4747
const { open } = useOverlay();
4848

4949
const isLoggedIn = !!user?.result;
5050

51-
if (isLoading) {
51+
if (isPending) {
5252
return <LoadingSpinner loading={true} size={20} />;
5353
}
5454

@@ -65,14 +65,32 @@ const CapsuleDetailPage = () => {
6565
router.push(PATH.LOGIN);
6666
return;
6767
}
68-
likeToggle({ id: result.id.toString(), isLiked: nextLiked });
68+
likeToggle(
69+
{ id: result.id.toString(), isLiked: nextLiked },
70+
{
71+
onSuccess: () => async (id: string) => {
72+
await Promise.all([
73+
queryClient.invalidateQueries({
74+
queryKey: capsuleQueryKeys.detail(id),
75+
}),
76+
queryClient.invalidateQueries({ queryKey: ["capsule", "my"] }),
77+
]);
78+
},
79+
},
80+
);
6981
};
7082

7183
const handleLeaveCapsule = (close: () => void) => {
7284
close();
7385
leaveCapsule(result.id.toString(), {
74-
onSuccess: () => {
86+
onSuccess: async () => {
7587
router.push(PATH.EXPLORE);
88+
await Promise.all([
89+
queryClient.invalidateQueries({
90+
queryKey: capsuleQueryKeys.detail(id),
91+
}),
92+
queryClient.invalidateQueries({ queryKey: ["capsule", "my"] }),
93+
]);
7694
},
7795
});
7896
};
@@ -104,14 +122,14 @@ const CapsuleDetailPage = () => {
104122
<Dropdown.Item
105123
label="캡슐 떠나기"
106124
className={styles.textHighlight}
107-
onClick={() => {
108-
open(({ isOpen, close }) => (
109-
<PopupWarningCapsule
110-
isOpen={isOpen}
111-
close={close}
112-
onConfirm={() => handleLeaveCapsule(close)}
113-
/>
114-
));
125+
onClick={() => {
126+
open(({ isOpen, close }) => (
127+
<PopupWarningCapsule
128+
isOpen={isOpen}
129+
close={close}
130+
onConfirm={() => handleLeaveCapsule(close)}
131+
/>
132+
));
115133
}}
116134
/>
117135
)}
@@ -121,25 +139,25 @@ const CapsuleDetailPage = () => {
121139
);
122140
}}
123141
/>
124-
125-
<RevealMotion>
126-
<InfoTitle
127-
title={result.title}
128-
participantCount={result.participantCount}
129-
joinLettersCount={result.letterCount}
130-
/>
142+
143+
<RevealMotion>
144+
<InfoTitle
145+
title={result.title}
146+
participantCount={result.participantCount}
147+
joinLettersCount={result.letterCount}
148+
/>
149+
</RevealMotion>
150+
<CapsuleImage imageUrl={result.beadVideoUrl} />
151+
<div className={styles.container}>
152+
<RevealMotion delay={0.8}>
153+
{result.subtitle && <CaptionSection description={result.subtitle} />}
154+
</RevealMotion>
155+
<RevealMotion delay={1.2}>
156+
<OpenInfoSection openAt={formatDateTime(result.openAt)} />
131157
</RevealMotion>
132-
<CapsuleImage imageUrl={result.beadVideoUrl} />
133-
<div className={styles.container}>
134-
<RevealMotion delay={0.8}>
135-
{result.subtitle && <CaptionSection description={result.subtitle} />}
136-
</RevealMotion>
137-
<RevealMotion delay={1.2}>
138-
<OpenInfoSection openAt={formatDateTime(result.openAt)} />
139-
</RevealMotion>
140-
</div>
141-
<ResponsiveFooter capsuleData={data} isLoggedIn={isLoggedIn} />
142-
{result.status !== "WRITABLE" && <InfoToast status={result.status} />}
158+
</div>
159+
<ResponsiveFooter capsuleData={data} isLoggedIn={isLoggedIn} />
160+
{result.status !== "WRITABLE" && <InfoToast status={result.status} />}
143161
</>
144162
);
145163
};

app/(sub)/capsule-detail/_components/modal/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"use client";
22

3-
import type React from "react";
43
import { useEffect } from "react";
54
import { createPortal } from "react-dom";
65
import * as styles from "./modal.css";

app/(sub)/capsule-detail/_components/write-modal/_hooks/use-image-upload.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { useFileUpload } from "@/shared/api/mutations/file";
1+
import { letterMutationOptions } from "@/shared/api/mutations/letter";
2+
import { useMutation } from "@tanstack/react-query";
23
import type { ChangeEvent } from "react";
34
import { useEffect, useState } from "react";
45

56
export const useImageUpload = () => {
6-
const fileUploadMutation = useFileUpload();
7+
const fileUploadMutation = useMutation(letterMutationOptions.upload);
78
const [selectedFile, setSelectedFile] = useState<File | null>(null);
89
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
910

app/(sub)/capsule-detail/_components/write-modal/index.tsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use client";
22

3-
import { useWriteLetter } from "@/shared/api/mutations/letter";
3+
import { letterMutationOptions } from "@/shared/api/mutations/letter";
4+
import { letterMutationKeys } from "@/shared/api/mutations/letter";
5+
import { capsuleQueryKeys } from "@/shared/api/queries/capsule";
46
import Close from "@/shared/assets/icon/close.svg";
57
import Plus from "@/shared/assets/icon/plus.svg";
68
import type { CapsuleDetailRes } from "@/shared/types/api/capsule";
@@ -11,6 +13,11 @@ import ShakeYMotion from "@/shared/ui/motion/shakeY-motion";
1113
import PopupConfirmLetter from "@/shared/ui/popup/popup-confirm-letter";
1214
import PopupWarningLetter from "@/shared/ui/popup/popup-warning-letter";
1315
import { formatOpenDateString } from "@/shared/utils/date";
16+
import {
17+
useIsMutating,
18+
useMutation,
19+
useQueryClient,
20+
} from "@tanstack/react-query";
1421
import Image from "next/image";
1522
import { useState } from "react";
1623
import { useController, useForm } from "react-hook-form";
@@ -32,9 +39,16 @@ const WriteModal = ({
3239
onClose,
3340
onSuccess,
3441
}: WriteModalProps) => {
35-
const { mutate: writeLetterMutate, isPending } = useWriteLetter();
42+
const queryClient = useQueryClient();
43+
const { mutate: writeLetterMutate } = useMutation(
44+
letterMutationOptions.write,
45+
);
3646
const [isWarningOpen, setIsWarningOpen] = useState(false);
3747
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
48+
const isMutatingPost =
49+
useIsMutating({
50+
mutationKey: letterMutationKeys.all(),
51+
}) > 0;
3852

3953
const { handleSubmit, getValues, control, reset } = useForm<WriteLetterReq>({
4054
defaultValues: {
@@ -72,7 +86,7 @@ const WriteModal = ({
7286
};
7387

7488
const handleConfirm = async (data: WriteLetterReq) => {
75-
if (isPending || isUploading) return;
89+
if (isMutatingPost) return;
7690

7791
try {
7892
const submitData = { ...data };
@@ -81,11 +95,14 @@ const WriteModal = ({
8195
const objectKey = await uploadFile();
8296
submitData.objectKey = objectKey;
8397
} else {
84-
delete submitData.objectKey;
98+
submitData.objectKey = undefined;
8599
}
86100

87101
writeLetterMutate(submitData, {
88102
onSuccess: () => {
103+
queryClient.invalidateQueries({
104+
queryKey: capsuleQueryKeys.detail(data.capsuleId),
105+
});
89106
setIsConfirmOpen(false);
90107
reset({
91108
capsuleId: capsuleData.result.id.toString(),
@@ -99,14 +116,12 @@ const WriteModal = ({
99116
onClose();
100117
onSuccess();
101118
},
102-
onError: (error) => {
119+
onError: () => {
103120
setIsConfirmOpen(false);
104-
console.error("편지 제출 실패:", error);
105121
},
106122
});
107123
} catch (error) {
108124
setIsConfirmOpen(false);
109-
console.error("파일 업로드 실패:", error);
110125
alert(
111126
`업로드 실패: ${
112127
error instanceof Error ? error.message : "알 수 없는 오류"
@@ -148,9 +163,9 @@ const WriteModal = ({
148163
<button
149164
type="submit"
150165
className={styles.title}
151-
disabled={isPending || isUploading}
166+
disabled={isMutatingPost}
152167
>
153-
{isPending || isUploading ? (
168+
{isMutatingPost ? (
154169
<PulseLoader color="#FFFFFF" size={5} />
155170
) : (
156171
"편지담기"
@@ -252,7 +267,7 @@ const WriteModal = ({
252267
</form>
253268
{isConfirmOpen && (
254269
<PopupConfirmLetter
255-
isLoading={isPending || isUploading}
270+
isLoading={isMutatingPost}
256271
openDate={formatOpenDateString(capsuleData.result.openAt)}
257272
isOpen={isConfirmOpen}
258273
close={() => setIsConfirmOpen(false)}

0 commit comments

Comments
 (0)