Skip to content

Refactor: mutationOptions 도입#214

Merged
seueooo merged 6 commits into
developfrom
refactor/unmount-animation/#210
Sep 12, 2025
Merged

Refactor: mutationOptions 도입#214
seueooo merged 6 commits into
developfrom
refactor/unmount-animation/#210

Conversation

@seueooo
Copy link
Copy Markdown
Contributor

@seueooo seueooo commented Sep 10, 2025

📌 Summary

📚 Tasks

  • mutationKey 생성
  • mutationOptions 도입
  • useIsMutating으로 편지쓰기 mutation 진행 여부 일괄적으로 확인

👀 To Reviewer

TanStack Query v5.82.0에 새롭게 추가된 mutationOptions를 도입했습니다.

  • mutationKey의 역할: 이미 구성된 기본 옵션과 뮤테이션을 연결하거나, 로딩/오류/데이터 상태를 구성 요소 간에 공유할 뮤테이션을 식별합니다.

아직 mutation 개수도 별로 없지만 왜 도입했냐하면,,

  • 같은 키를 공유하는 뮤테이션들의 진행 상황을 일괄적으로 확인할 수 있으며, 하나의 키로 해당 키를 공유하는 모든 뮤테이션을 동시에 호출할 수도 있습니다. ( 참고 )
  • 추후 편지 쓰기 페이지의 기능이 확장될 가능성이 매우매우 크기에,, 여러가지 커스텀 기능이 추가되면 뮤테이션을 일관성있게 사용할 수 있도록 관리하는 게 좋을 것 같아 도입했습니당
  • useIsMutating으로 리팩토링하여, 'letters' 키 하나로 편지쓰기와 관련된 모든 뮤테이션의 진행상황을 추적할 수 있습니다!!
    -> 파일 업로드 뮤테이션도 letters 도메인에 종속되도록 구조를 변경했으며, 더 이상 file.ts가 필요하지 않아 제거했습니다.
  • 쿼리와 동일한 패턴으로 사용되어 일관성 측면에서도 좋을 것 같다는 생각입니다.

또한 뮤테이션 후 쿼리키 무효화하는 로직을, 뮤테이션 사용처에서 호출하도록 모두 수정했습니다! 의견있으심 남겨주세요!

Summary by CodeRabbit

  • New Features

    • 캡슐 생성에 ‘부제목’ 입력이 추가되어 제출에 반영됩니다.
  • UI/UX

    • 캡슐 상세 화면 레이아웃 개편(이미지, 캡션, 오픈 정보 섹션, 모션 적용).
    • 드롭다운 열림/닫힘 동작 단순화로 반응성 향상.
    • 작성·파일업로드·좋아요·나가기 등 진행 상태와 로딩·에러 표시 강화.
    • 팝업(경고/확인/취소) 관리 개선으로 로그인·작업 흐름이 더 일관됨.
  • Refactor

    • 데이터 갱신 및 쓰기/업로드 흐름 재구성으로 동기화 신뢰도 향상.

@seueooo seueooo self-assigned this Sep 10, 2025
@seueooo seueooo requested a review from seung365 as a code owner September 10, 2025 13:12
@seueooo seueooo linked an issue Sep 10, 2025 that may be closed by this pull request
@vercel
Copy link
Copy Markdown

vercel Bot commented Sep 10, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
time-capsule Ready Ready Preview Comment Sep 11, 2025 6:19am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 10, 2025

Walkthrough

리액트 쿼리 기반으로 뮤테이션을 키/옵션 패턴(mutationKeys/options)으로 전환하고, 훅 기반 자동 무효화들을 제거해 호출 쪽에서 수동으로 캐시 무효화를 수행하도록 변경했습니다. 파일 업로드 훅 삭제 및 presigned PUT 업로드 추가, 타입 강화 및 일부 UI/로딩·오버레이 흐름 조정이 포함됩니다.

Changes

Cohort / File(s) Summary of changes
Capsule Detail Pages
app/(sub)/capsule-detail/[invite-code]/[id]/page.tsx, app/(sub)/capsule-detail/[invite-code]/[id]/letters/page.tsx
뮤테이션을 capsuleMutationOptions/useMutation으로 전환하고 수동으로 쿼리 무효화(capsuleQueryKeys) 수행. 로딩/에러 가드 및 타입 강화(CapsuleDetailRes, LetterListRes, IntersectionObserverEntry) 적용. UI 구조(이미지, 캡션, 오버레이) 및 로딩 상태 처리 변경.
Write Modal + Upload Hook
app/(sub)/capsule-detail/_components/write-modal/index.tsx, app/(sub)/capsule-detail/_components/write-modal/_hooks/use-image-upload.ts
파일 업로드 훅(useFileUpload) 제거 후 letterMutationOptions.upload로 업로드 구현. 작성은 letterMutationOptions.write 사용, 전역 진행 상태는 useIsMutating으로 관찰. 성공 시 캡슐 상세 쿼리 수동 무효화. 에러 로깅/콘솔 출력 일부 제거, 파일 없을 때 objectKey: undefined 처리.
Create Capsule Page
app/(sub)/create-capsule/page.tsx
useCreateCapsule 훅 삭제, useMutation(capsuleMutationOptions.create)로 대체. 성공 콜백에 CreateCapsuleRes 타입 적용 후 ["capsule","my"] 쿼리 무효화. 폼에 subtitle 추가 및 페이로드 반영. 컴포넌트를 Suspense로 감쌈.
API Mutations Refactor
shared/api/mutations/capsule.ts, shared/api/mutations/letter.ts, (deleted) shared/api/mutations/file.ts
훅 기반 변이(useCreateCapsule, useLikeToggle, useLeaveCapsule, useWriteLetter, useFileUpload) 제거. 대신 capsuleMutationKeys/options, letterMutationKeys/options 추가. 레터 업로드는 presigned URL을 받아 S3에 PUT 수행. 자동 쿼리 무효화 제거로 호출자에게 무효화 책임 이전. shared/api/mutations/file.ts 파일 삭제.
API Queries Typing
shared/api/queries/capsule.ts
infiniteQuery의 getNextPageParamCapsuleListsRes 타입 명시로 타입 안정성 강화(로직 불변). 문법적 정리(트레일링 콤마 등).
UI Behavior - Dropdown
shared/ui/dropdown/index.tsx
내부 애니메이션 상태(isAnimating) 및 관련 타이머 제거. 열기는 즉시 수행, 닫기만 150ms 타이머 유지. 외부 API(컴포넌트 시그니처)는 변경 없음.
Modal Minor Cleanup
app/(sub)/capsule-detail/_components/modal/index.tsx
import type React 타입 전용 import 제거(파일 내 React.ReactNode 사용은 유지). 공개 API 변경 없음.

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
Loading
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
Loading
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
Loading

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.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

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 details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 770b7d7 and 5841582.

📒 Files selected for processing (1)
  • app/(sub)/create-capsule/page.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/(sub)/create-capsule/page.tsx
⏰ 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: deploy
  • GitHub Check: test

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed 제목 "Refactor: mutationOptions 도입"은 PR의 핵심 변경사항인 TanStack Query의 mutationOptions 도입 및 기존 훅 기반 뮤테이션 리팩터를 명확히 요약합니다. 간결하고 불필요한 노이즈가 없으므로 팀원이 히스토리를 스캔할 때 주요 목적을 바로 파악할 수 있습니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/unmount-animation/#210

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

This pull request (commit 770b7d7) has been deployed to Vercel ▲ - View GitHub Actions Workflow Logs

Name Link
🌐 Unique https://time-capsule-iozrky054-hs-projects-b4a69d5f.vercel.app
🔍 Inspect https://vercel.com/hs-projects-b4a69d5f/time-capsule/E6Fm6NadzWjduNpwCXDUWBcjYXD4

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Sep 10, 2025

🚀 Storybook 배포

📖 Storybook: https://683d91ab23651aa0b399e435-deargtijol.chromatic.com/
🔗 Chromatic Build: https://www.chromatic.com/build?appId=683d91ab23651aa0b399e435&number=230
✅ Status: success

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 역할 일관성 확보

DropdownItemrole="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

📥 Commits

Reviewing files that changed from the base of the PR and between f62f7bb and 770b7d7.

📒 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: 확인 팝업 로딩 연동 적절.

PopupConfirmLetterisLoading이 진행 상태와 일관되게 동기화되어 있습니다.

shared/api/mutations/letter.ts (3)

8-12: 키 네임스페이스 구성 깔끔합니다.

all/write/upload 계층 키 설계가 명확하고 확장에 유리합니다.


14-23: 쓰기 뮤테이션 옵션 구성 적절.

서버 반환값을 따로 사용하지 않는 경우 입력데이터 반환 패턴도 합리적입니다.


31-37: 제안 무시
getUploadPresignedUrlFileRes 타입({ 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.

라우팅 이후 관련 쿼리 무효화까지 비동기적으로 처리하는 흐름이 안전합니다.

Comment thread app/(sub)/create-capsule/page.tsx Outdated
@github-actions
Copy link
Copy Markdown

This pull request (commit 5841582) has been deployed to Vercel ▲ - View GitHub Actions Workflow Logs

Name Link
🌐 Unique https://time-capsule-6qzjfgzsc-hs-projects-b4a69d5f.vercel.app
🔍 Inspect https://vercel.com/hs-projects-b4a69d5f/time-capsule/AmEoeBFix7wqeRBGj1Zm7FG4wh2b

Copy link
Copy Markdown
Member

@seung365 seung365 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mutationOptions 좋은거 같네요 말씀하신 것처럼 queryOptions를 사용하고 있으니,,동일한 패턴을 사용해 일관성 측면에서 좋은거 같아요! 👍

@seueooo seueooo merged commit 121e23a into develop Sep 12, 2025
10 checks passed
@seueooo seueooo deleted the refactor/unmount-animation/#210 branch September 12, 2025 00:38
seueooo added a commit that referenced this pull request Sep 27, 2025
* refactor: 불필요한 상태 제거

* refactor: mutationOptions 기반으로 리팩토링

* refactor: useIsMutating 훅을 통해, 편지 쓰기 뮤테이션 진행상태를 한번에 추적

* refactor: useIsMutating이 boolean을 반환하도록 수정

* refactor: 필요없는 파일 삭제

* refactor: 코드리뷰 반영
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor]: mutationOptions 도입

2 participants