Refactor: mutationOptions 도입#214
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough리액트 쿼리 기반으로 뮤테이션을 키/옵션 패턴(mutationKeys/options)으로 전환하고, 훅 기반 자동 무효화들을 제거해 호출 쪽에서 수동으로 캐시 무효화를 수행하도록 변경했습니다. 파일 업로드 훅 삭제 및 presigned PUT 업로드 추가, 타입 강화 및 일부 UI/로딩·오버레이 흐름 조정이 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User
participant P as CapsuleDetail Page
participant RQ as React Query
participant API as Server
rect rgba(200,230,255,0.18)
note over P,RQ: 좋아요/나가기 — mutationOptions + 수동 무효화
U->>P: 클릭(좋아요/나가기)
P->>RQ: mutate(capsuleMutationOptions.like/leave)
RQ->>API: 요청(POST /capsule/like or /leave)
API-->>RQ: 응답(200)
RQ-->>P: 성공 콜백 반환
P->>RQ: invalidateQueries(capsule detail, my-capsule)
end
sequenceDiagram
autonumber
participant U as User
participant WM as WriteModal
participant RQ as React Query
participant API as Server
participant S3 as Storage
rect rgba(200,255,200,0.18)
note over WM,RQ: 레터 업로드(프리사인드) 및 작성(옵션화)
U->>WM: 파일 선택
WM->>RQ: mutateAsync(letterMutationOptions.upload)
RQ->>API: GET presignedUrl
API-->>RQ: presignedUrl, objectKey
RQ->>S3: PUT file (Content-Type)
S3-->>RQ: 200
RQ-->>WM: objectKey
U->>WM: 제출
WM->>RQ: mutate(letterMutationOptions.write)
RQ->>API: POST /letters
API-->>RQ: 200
RQ-->>WM: onSuccess
WM->>RQ: invalidateQueries(capsule detail)
end
sequenceDiagram
autonumber
participant U as User
participant CP as CreateCapsule Page
participant RQ as React Query
participant API as Server
rect rgba(255,240,200,0.18)
note over CP,RQ: 캡슐 생성 — mutationOptions + 수동 무효화
U->>CP: 폼 제출(title, subtitle, openAt, closedAt)
CP->>RQ: mutate(capsuleMutationOptions.create)
RQ->>API: POST /capsule/create
API-->>RQ: CreateCapsuleRes
RQ-->>CP: onSuccess(res)
CP->>RQ: invalidateQueries(["capsule","my"])
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. 📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
Pre-merge checks (3 passed)✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
This pull request (commit
|
🚀 Storybook 배포📖 Storybook: https://683d91ab23651aa0b399e435-deargtijol.chromatic.com/ |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
app/(sub)/capsule-detail/[invite-code]/[id]/page.tsx (2)
61-81: 좋아요 토글 로직 역전 + onSuccess 시그니처 오류.
capsuleMutationOptions.like는 “현재 좋아요 상태(isLiked)”에 따라 API 분기합니다. 현재 코드에서nextLiked를 전달해 반대로 호출되는 버그가 있습니다.onSuccess: () => async (id: string) => { ... }는 함수-반환-함수가 되어 콜백이 실행되지 않습니다.아래처럼 수정이 필요합니다.
- const handleLikeToggle = (nextLiked: boolean) => { + const handleLikeToggle = (currentLiked: boolean) => { if (!isLoggedIn) { const current = oauthUtils.buildCurrentUrl(pathname, searchParams); oauthUtils.saveNextUrl(current); router.push(PATH.LOGIN); return; } - likeToggle( - { id: result.id.toString(), isLiked: nextLiked }, - { - onSuccess: () => async (id: string) => { - await Promise.all([ + likeToggle( + { id: result.id.toString(), isLiked: currentLiked }, + { + onSuccess: async (id: string) => { + await Promise.all([ queryClient.invalidateQueries({ queryKey: capsuleQueryKeys.detail(id), }), queryClient.invalidateQueries({ queryKey: ["capsule", "my"] }), ]); - }, - }, + }, + }, ); };
104-107: 토글 콜백 인자 전달 수정 필요.위 수정과 함께, 현재 상태(
prev)를 그대로 전달해야 합니다.- <LikeButton - isLiked={result.isLiked} - onLikeToggle={(prev) => handleLikeToggle(!prev)} - /> + <LikeButton + isLiked={result.isLiked} + onLikeToggle={(prev) => handleLikeToggle(prev)} + />
🧹 Nitpick comments (22)
shared/ui/dropdown/index.tsx (6)
102-111: 버튼 기본 type 지정 및 ARIA 보강 제안form 내부에서 의도치 않은 submit을 막기 위해 기본 type을 지정하고, 접근성을 위해 aria 속성을 추가해주세요.
return ( <button + type="button" onClick={(e) => { e.stopPropagation(); handleToggleOpen(); onClick?.(); }} + aria-haspopup="menu" + aria-expanded={open} className={cn(styles.triggerBtnStyle, className)} >
127-133: UL에 role="menu" 추가로 ARIA 역할 일관성 확보
DropdownItem에role="menuitem"을 쓰고 있으므로 컨테이너 UL에도role="menu"를 부여해 주세요.return ( <ul + role="menu" className={cn( styles.dropdownContent, isClosing ? styles.dropdownContentClosing : "", className, )} >
159-171: DropdownItem 기본 type="button" 지정기본값이 submit이라 폼 내 사용 시 부작용이 있습니다. 기본 type을 지정해 주세요.
<button {...props} onClick={(e) => { e.stopPropagation(); handleToggleClose(); onClick?.(); }} + type="button" className={cn(styles.dropdownItem, className)} role="menuitem" aria-label={label} >
56-66: 의존성 최적화: 불필요한 open 제거
useEffect본문에서open을 사용하지 않습니다. 의존성에서 제거하면 불필요한 리렌더를 줄일 수 있습니다.- }, [isClosing, open]); + }, [isClosing]);
43-50: 닫힘 애니메이션 중 다시 열기 동작 개선현재
open=true && isClosing=true상태에서 트리거 클릭 시 다시 열 수 없습니다. UX 향상을 위해 토글 시 닫힘 상태를 해제하도록 제안합니다.const handleToggleOpen = () => { - if (open) { - setIsClosing(true); - } else { - setOpen(true); - setIsClosing(false); - } + if (open) { + // 열림 상태에서: 닫힘 중이면 취소, 아니면 닫힘 시작 + setIsClosing((prev) => !prev); + } else { + setOpen(true); + setIsClosing(false); + } };
57-63: 매직 넘버 제거: CSS 애니메이션 시간과 동기화하드코딩된 150ms는 CSS 변경 시 쉽게 불일치가 납니다. 상수로 분리하거나 CSS 변수/transitionend 이벤트로 동기화하세요.
+const CLOSE_ANIM_MS = 150; ... - const timer = setTimeout(() => { + const timer = setTimeout(() => { setOpen(false); setIsClosing(false); - }, 150); + }, CLOSE_ANIM_MS);app/(sub)/capsule-detail/[invite-code]/[id]/letters/page.tsx (3)
62-66: 중복 조건 제거로 가독성/성능 개선
isLetterLoading && !letterData가 두 번 반복됩니다. 한 번만 평가하도록 정리하세요.const isInitialLoading = - (isLetterLoading && !letterData) || - (isLetterLoading && !letterData) || + (isLetterLoading && !letterData) || (isImageLoading && letters.length === 0) || isCapsuleLoading;
46-53: 무한 스크롤 프리페치 개선(선택): rootMargin 활용뷰포트 진입 직전에 더 부드럽게 로드하려면
threshold대신rootMargin: "200px 0px"등으로 여유를 주는 것을 고려해 주세요.const footerRef = useIntersectionObserver<HTMLDivElement>( (entry: IntersectionObserverEntry) => { if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }, - { threshold: 0.8 }, + { rootMargin: "200px 0px" }, );
55-58: 타이핑 단순화 제안
flatMap내부의page: LetterListRes단언 없이도 제네릭 구성이 가능하면(옵션 객체에서 반환 타입 지정), 여기서는 추론에 맡기는 편이 깔끔합니다. 현재 구조 유지 시 무해하나, 타입 소스는 한 곳으로 모으는 편이 유지보수에 유리합니다.- const letters = - letterData?.pages.flatMap((page: LetterListRes) => page.result.letters) || - []; + const letters = + letterData?.pages.flatMap((page) => page.result.letters) ?? [];shared/api/mutations/capsule.ts (2)
25-35: like 토글: 변수 타입/세분화 키 추가 제안
- 재사용을 위해 변수 타입을 export하면 호출부 일관성이 좋아집니다.
- 필요 시 개별 항목 단위 제어를 위해
likeById키를 추가해 둘 것을 권장합니다(집계 키는 유지).export const capsuleMutationKeys = { all: () => ["capsule"], create: () => [...capsuleMutationKeys.all(), "create"], like: () => [...capsuleMutationKeys.all(), "like"], leave: () => [...capsuleMutationKeys.all(), "leave"], + likeById: (id: string) => [...capsuleMutationKeys.like(), id], }; +export type CapsuleLikeToggleVars = { id: string; isLiked: boolean }; + export const capsuleMutationOptions = { create: mutationOptions({ mutationKey: capsuleMutationKeys.create(), mutationFn: async (data: CreateCapsuleReq): Promise<CreateCapsuleRes> => { return await apiClient.post<CreateCapsuleRes>(ENDPOINTS.CREATE_CAPSULE, { json: data, }); }, }), like: mutationOptions({ mutationKey: capsuleMutationKeys.like(), - mutationFn: async ({ id, isLiked }: { id: string; isLiked: boolean }) => { + mutationFn: async ({ id, isLiked }: CapsuleLikeToggleVars) => { if (isLiked) { await apiClient.delete(ENDPOINTS.LIKE_TOGGLE(id)); } else { await apiClient.put(ENDPOINTS.LIKE_TOGGLE(id)); } return id; }, }),
36-41: leave 반환 타입 명시(선택)서버가 204 No Content를 반환한다면
Promise<void>로 명시해 두면 호출부에서 조건 분기가 간결해집니다. 응답 바디가 있다면 해당 타입으로 명시하세요.leave: mutationOptions({ mutationKey: capsuleMutationKeys.leave(), - mutationFn: (id: string) => { - return apiClient.delete(ENDPOINTS.LEAVE_CAPSULE(id)); - }, + mutationFn: (id: string): Promise<void> => + apiClient.delete(ENDPOINTS.LEAVE_CAPSULE(id)), }),app/(sub)/create-capsule/page.tsx (3)
3-3: 쿼리 키 하드코딩 지양: prefix 헬퍼로 무효화 일관성 확보["capsule","my"]를 문자열로 직접 쓰면 키 스키마 변경 시 드리프트가 납니다. queryKeys에서 제공하는 prefix를 활용해 무효화 대상을 안정적으로 지정하세요. (아래는 현재 키 체계를 유지하면서 prefix만 사용하도록 제안)
import CreateCapsuleLoading from "@/app/(sub)/create-capsule/_components/create-capsule-loading"; import { capsuleMutationOptions } from "@/shared/api/mutations/capsule"; +import { capsuleQueryKeys } from "@/shared/api/queries/capsule"; ... - queryClient.invalidateQueries({ queryKey: ["capsule", "my"] }); + queryClient.invalidateQueries({ + queryKey: capsuleQueryKeys.all().concat("my"), + });Also applies to: 79-79
74-74: 불필요한 타입 표기 제거 가능onSuccess의 인자 타입은 mutationOptions의 제네릭에서 이미 추론됩니다. 명시적
(res: CreateCapsuleRes)는 중복이므로 제거해도 가독성이 좋아집니다.- onSuccess: (res: CreateCapsuleRes) => { + onSuccess: (res) => {
63-71: 폼 데이터 정제(트리밍) 및 기본 검증 추가 제안title/subtitle에 앞뒤 공백 제거 정도는 제출 직전에 처리하면 서버/클라이언트 양쪽 모두 일관성이 좋아집니다. 또한 허용 길이/금지 문자 등의 최소 검증을 onSubmit에 추가하는 것을 권장합니다.
app/(sub)/capsule-detail/_components/write-modal/_hooks/use-image-upload.ts (3)
19-39: 이미지 타입/용량 검증 추가로 보안·안정성 보완현재 확장자만 확인하고 있어 비이미지 업로드, 과도한 대용량 업로드를 막지 못합니다. MIME 타입 화이트리스트와 최대 용량 제한(예: 10MB)을 추가해주세요. 재선택 시에도 동일 검증이 필요합니다.
+const MAX_BYTES = 10 * 1024 * 1024; // 10MB +const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"]; const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (!file) { return; } + + if (!ALLOWED_TYPES.includes(file.type)) { + alert("이미지 파일만 업로드할 수 있어요."); + return; + } + if (file.size > MAX_BYTES) { + alert("파일은 10MB 이하만 업로드할 수 있어요."); + return; + } const extension = file.name.split(".").pop(); if (!extension) { alert("파일 확장자를 찾을 수 없습니다."); return; }
41-60: 업로드 에러 처리 노출mutateAsync 실패 시 호출부에만 위임하면 UX 상 조용히 실패할 수 있습니다. 최소한 이 훅에서 에러 메시지를 표준화해 throw 하거나(메시지 포함), 토스트/오버레이로 노출하는 패턴을 권장합니다.
6-7: 핸들러 메모이제이션으로 불필요한 리렌더 감소외부에 노출되는 handleFileChange/uploadFile/removeImage는 useCallback으로 감싸면 의존성 변화가 없을 때 재생성/리렌더를 줄일 수 있습니다.
Also applies to: 19-39, 62-77
shared/api/queries/capsule.ts (1)
14-34: 쿼리 무효화용 prefix 키 제공 제안사용처에서 부분 무효화를 안전하게 수행할 수 있도록 prefix 키를 추가하면 하드코딩을 피할 수 있습니다. 예:
capsuleQueryKeys.myRoot()사용 후invalidateQueries({ queryKey: capsuleQueryKeys.myRoot() }).export const capsuleQueryKeys = { all: () => ["capsule"], detail: (id: string) => [...capsuleQueryKeys.all(), id], lists: (sort?: CapsuleSortType, type?: string) => [ ...capsuleQueryKeys.all(), "lists", sort, type, ], + // invalidate prefix: ["capsule","my"] + myRoot: () => [...capsuleQueryKeys.all(), "my"], my: (sort?: CapsuleSortType, filter?: MyCapsuleFilterType) => [ ...capsuleQueryKeys.all(), "my", sort, filter, ], searchList: (keyword: string) => [ ...capsuleQueryKeys.all(), "searchList", keyword, ], } as const;app/(sub)/capsule-detail/_components/write-modal/index.tsx (3)
48-51: useIsMutating 범위 확인 필요(글로벌 블로킹 가능성).
letterMutationKeys.all()를 사용하면 동일 앱 내 “letter” 네임스페이스의 모든 뮤테이션(예: 업로드, 다른 컴포넌트의 삭제 등) 진행 시 본 모달의 제출/로딩도 함께 막힙니다. 의도하신 전역 일괄 제어라면 OK, 아니라면mutationKey: letterMutationKeys.write()또는exact: true등으로 범위를 좁히는 편이 안전합니다.
94-99: 불필요한undefined할당 제거 제안.
JSON.stringify과정에서undefined필드는 직렬화에서 제외됩니다. 명시적undefined할당 대신 프로퍼티 자체를 생략하는 편이 깔끔합니다.- } else { - submitData.objectKey = undefined; - } + }
101-112: 쿼리 무효화 완료 후 UI 정리 권장(레이스 컨디션 예방).
invalidateQueries는 Promise를 반환합니다. 성공 콜백을async로 전환해 무효화를 기다린 뒤 reset/close를 수행하면 깜빡임/구데이터 표시 가능성을 줄일 수 있습니다.- writeLetterMutate(submitData, { - onSuccess: () => { - queryClient.invalidateQueries({ + writeLetterMutate(submitData, { + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: capsuleQueryKeys.detail(data.capsuleId), }); setIsConfirmOpen(false); reset({ capsuleId: capsuleData.result.id.toString(), content: "", from: "", objectKey: undefined, });shared/api/mutations/letter.ts (1)
41-42: MIME 타입 폴백 추가 제안.일부 환경에서
file.type이 빈 문자열일 수 있습니다. 안전한 기본값을 지정해 주세요.- "Content-Type": file.type, + "Content-Type": file.type || "application/octet-stream",
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
app/(sub)/capsule-detail/[invite-code]/[id]/letters/page.tsx(4 hunks)app/(sub)/capsule-detail/[invite-code]/[id]/page.tsx(6 hunks)app/(sub)/capsule-detail/_components/modal/index.tsx(0 hunks)app/(sub)/capsule-detail/_components/write-modal/_hooks/use-image-upload.ts(1 hunks)app/(sub)/capsule-detail/_components/write-modal/index.tsx(8 hunks)app/(sub)/create-capsule/page.tsx(4 hunks)shared/api/mutations/capsule.ts(2 hunks)shared/api/mutations/file.ts(0 hunks)shared/api/mutations/letter.ts(1 hunks)shared/api/queries/capsule.ts(4 hunks)shared/ui/dropdown/index.tsx(2 hunks)
💤 Files with no reviewable changes (2)
- app/(sub)/capsule-detail/_components/modal/index.tsx
- shared/api/mutations/file.ts
🧰 Additional context used
🧬 Code graph analysis (9)
app/(sub)/capsule-detail/_components/write-modal/_hooks/use-image-upload.ts (1)
shared/api/mutations/letter.ts (1)
letterMutationOptions(14-53)
app/(sub)/capsule-detail/_components/write-modal/index.tsx (2)
shared/api/mutations/letter.ts (2)
letterMutationOptions(14-53)letterMutationKeys(8-12)shared/api/queries/capsule.ts (1)
capsuleQueryKeys(14-34)
app/(sub)/capsule-detail/[invite-code]/[id]/letters/page.tsx (2)
shared/types/api/capsule.ts (1)
CapsuleDetailRes(18-43)shared/types/api/letter.ts (1)
LetterListRes(22-30)
shared/api/mutations/letter.ts (5)
shared/types/api/letter.ts (1)
WriteLetterReq(32-37)shared/api/api-client.ts (1)
apiClient(50-65)shared/constants/endpoints.ts (1)
ENDPOINTS(3-45)shared/types/api/file.ts (1)
FileUploadReq(7-11)shared/api/queries/file.ts (1)
getUploadPresignedUrl(28-30)
shared/api/mutations/capsule.ts (3)
shared/types/api/capsule.ts (2)
CreateCapsuleReq(1-7)CreateCapsuleRes(9-14)shared/api/api-client.ts (1)
apiClient(50-65)shared/constants/endpoints.ts (1)
ENDPOINTS(3-45)
shared/api/queries/capsule.ts (3)
shared/types/api/capsule.ts (2)
CapsuleListsRes(45-53)CapsuleSortType(82-82)shared/api/api-client.ts (1)
apiClient(50-65)shared/constants/endpoints.ts (1)
ENDPOINTS(3-45)
shared/ui/dropdown/index.tsx (1)
shared/utils/cn.ts (1)
cn(3-5)
app/(sub)/create-capsule/page.tsx (3)
shared/hooks/use-overlay.ts (1)
useOverlay(7-46)shared/api/mutations/capsule.ts (1)
capsuleMutationOptions(16-42)shared/types/api/capsule.ts (1)
CreateCapsuleRes(9-14)
app/(sub)/capsule-detail/[invite-code]/[id]/page.tsx (5)
shared/api/queries/capsule.ts (2)
capsuleQueryOptions(36-79)capsuleQueryKeys(14-34)shared/api/queries/user.ts (1)
userQueryOptions(13-38)shared/api/mutations/capsule.ts (1)
capsuleMutationOptions(16-42)shared/hooks/use-overlay.ts (1)
useOverlay(7-46)shared/utils/date.ts (1)
formatDateTime(58-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: storybook-deploy
- GitHub Check: test
🔇 Additional comments (9)
shared/api/mutations/capsule.ts (2)
9-14: mutationKey 설계 좋습니다도메인 단위 키와 세부 키를 분리한 패턴이 명확합니다.
useIsMutating으로 일괄 상태 조회에 적합합니다.
16-24: create 옵션: 반환 타입 명시 유지 OK
CreateCapsuleRes로 명확히 지정되어 있어 후속 invalidate/네비게이션 처리에 유리합니다.shared/api/queries/capsule.ts (1)
49-54: 페이지네이션 경계 체크 로직은 명확하고 안전합니다
pageNumber < totalPages - 1 ? pageNumber + 1 : undefined패턴이 일관되게 적용되어 있어 off-by-one 오류 위험이 낮습니다. 타입 지정(CapsuleListsRes)도 적절합니다.Also applies to: 61-66, 73-78
app/(sub)/capsule-detail/_components/write-modal/index.tsx (2)
88-90: 이중 제출 방지 처리 적절.진행 중에는
handleConfirm을 조기 종료하여 중복 호출을 차단합니다. UI/상태 일관성 측면에서 좋습니다.
269-271: 확인 팝업 로딩 연동 적절.
PopupConfirmLetter의isLoading이 진행 상태와 일관되게 동기화되어 있습니다.shared/api/mutations/letter.ts (3)
8-12: 키 네임스페이스 구성 깔끔합니다.
all/write/upload계층 키 설계가 명확하고 확장에 유리합니다.
14-23: 쓰기 뮤테이션 옵션 구성 적절.서버 반환값을 따로 사용하지 않는 경우 입력데이터 반환 패턴도 합리적입니다.
31-37: 제안 무시
getUploadPresignedUrl는FileRes타입({ presignedUrl, objectKey, expiresAt })을 바로 반환하므로presignedResponse.result가 아니라 현재 코드(const { presignedUrl, objectKey } = presignedResponse;)가 맞습니다.Likely an incorrect or invalid review comment.
app/(sub)/capsule-detail/[invite-code]/[id]/page.tsx (1)
85-95: 탈퇴 플로우 + 무효화 동작 LGTM.라우팅 이후 관련 쿼리 무효화까지 비동기적으로 처리하는 흐름이 안전합니다.
|
This pull request (commit
|
* refactor: 불필요한 상태 제거 * refactor: mutationOptions 기반으로 리팩토링 * refactor: useIsMutating 훅을 통해, 편지 쓰기 뮤테이션 진행상태를 한번에 추적 * refactor: useIsMutating이 boolean을 반환하도록 수정 * refactor: 필요없는 파일 삭제 * refactor: 코드리뷰 반영
📌 Summary
📚 Tasks
👀 To Reviewer
TanStack Query v5.82.0에 새롭게 추가된 mutationOptions를 도입했습니다.
아직 mutation 개수도 별로 없지만 왜 도입했냐하면,,
-> 파일 업로드 뮤테이션도 letters 도메인에 종속되도록 구조를 변경했으며, 더 이상 file.ts가 필요하지 않아 제거했습니다.
또한 뮤테이션 후 쿼리키 무효화하는 로직을, 뮤테이션 사용처에서 호출하도록 모두 수정했습니다! 의견있으심 남겨주세요!
Summary by CodeRabbit
New Features
UI/UX
Refactor