Skip to content

Conversation

@Hys-Lee
Copy link
Collaborator

@Hys-Lee Hys-Lee commented Aug 17, 2025

버그 수정

  • 안드로이드에서 일부 기종에 대해 바텀시트 고장나는 것 🔼
    • 스크롤 위치 등에 따라 바텀시트 위치가 변동함. 다만 키보드에 가리진 않음.
  • 디바운스 전체 api처리에 대해 적용하기 ✅
  • 바텀탭바 제대로 클릭 안되는 상황 처리하기 ✅

기능 추가

  • 메인 페이지 및 상세 페이지 UI 수정
  • 그룹 페이지 게스트 처리
  • 게스트 모드 적용

Summary by CodeRabbit

  • New Features

    • Guest browsing preview with login CTA; guest-mode UI for groups and onboarding; home goals list (GoalDataContainer) and persistent bottom tab bar; modal to add todos; global bottom-sheet manager enabling duration/todo/result sheets.
  • Improvements

    • Refined D‑Day/progress display; full sub‑goal list and 5‑item todo preview with "더보기"; API error toasts and debounced requests; keyboard-safe bottom-sheet behavior; chat/message layout now full-width.
  • Documentation

    • README rewritten as a branded landing/docs page with demo link, features, tech stack, pages, team, and contribution guide.

Hys-Lee added 30 commits May 21, 2025 12:44
포크한 레포에 작동 테스틀 위해 연결
스토리북 형태도 세팅함
구조 수정 및 tailwind prefix에 맞게 mapping
이전 패키지 버전에선 빌드가 안 됐음.
httpClient에서  게스트모드에서도 format을 가져서 api 콜을 할 수 있도록 코드 수정
@coderabbitai
Copy link

coderabbitai bot commented Aug 17, 2025

Walkthrough

Adds a generic bottom-sheet system (store, hook, renderer) and integrates it throughout the app; introduces guest-mode support with IndexedDB-backed MSW handlers; debounces and types the API client; converts Drawer-based bottom sheets to UI-only components and hook-managed flows; mounts guest and bottom-sheet handlers in layout; and rewrites README and Storybook manager head.

Changes

Cohort / File(s) Summary of changes
Bottom-sheet core & store
hooks/useBottomSheet.ts, stores/useBottomSheetStore.ts
New generic hook and Zustand store managing a history/stack of bottom-sheet entries with open/update/close/reset APIs and typed content props.
Bottom-sheet renderer & CSS
app/_components/BottomSheetRenderer.tsx, app/globals.css
New renderer using vaul Drawer to render top history entry with keyboard-height fixer and remount key logic; utility CSS to disable vaul overlay ::after pointer-events.
Hook integrations (pages & components)
app/adding-goal/page.tsx, app/details/edit/.../GoalEdit.tsx, app/details/[goal_id]/_components/.../DetailBody.tsx, components/details/ListCard/ListCard.tsx, components/main/GoalCard/GoalCard.tsx, components/main/GoalMenuContainer/GoalMenuContainer.tsx, components/main/GoalDataContainer/GoalDataContainer.tsx
Replace inline Drawer usage with useBottomSheet-driven flows, add lifecycle effects and submission helpers; add GoalDataContainer; some previous inline UIs commented out.
Shared bottom-sheet components (UI-only)
components/shared/BottomSheets/TodoBottomSheet/*, components/shared/BottomSheets/TodoResultBottomSheet/*, components/details/BottomSheets/GoalDurationBottomSheet/*
Converted from Drawer-root components to plain div-based UI containers; removed external openBottomSheet/hasBottomTabBar props; updated stories to render components directly.
Layout & global integration
app/layout.tsx, app/page.tsx, app/details/[goal_id]/page.tsx, app/group/page.tsx
Mounted GuestModeHandler and BottomSheetRenderer in layout; replaced GoalCard with GoalDataContainer; dynamic goal title via useGoalDetail; conditional Group rendering for guest users; BottomTabBar fixed positioning.
Guest mode: auth, UI, and mocks
stores/useAuthStore.ts, app/_components/GuestModeHandler.tsx, app/onboarding/_components/LoginScreen.tsx, components/group/Guest/GuestGroup/*, mocks/browser.ts, mocks/server.ts, mocks/guestMode/*
Added isGuest flag and setter to auth store; onboarding browse flow sets guest; GuestGroup component and story added; MSW switched to guestHandlers and new IndexedDB-backed guest-mode handlers/types; exported dbService.
API client updates
api/service.ts
Added 1s debounced wrapper for httpClient.request, guest JSON format when no token, toast-on-error integration, and new typed exports plus named API accessors and default export.
New modals & UI tweaks
components/details/Modals/ModalAddingTodo/*, components/shared/TodoItem/TodoItem.tsx, components/group/GroupChatItem.tsx, components/group/groupRoom/GroupChatRoom/GroupChatRoom.tsx
New ModalAddingTodo component; simplified TodoItem icon; GroupChatItem width made fluid; message wrapper centered in GroupChatRoom.
Stories & docs
.storybook/main.ts, various *.stories.tsx, README.md
Storybook manager head updated to inject favicon; several stories adjusted to render components directly and remove open controls; README rewritten and reorganized.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Component
  participant Hook as useBottomSheet
  participant Store as useBottomSheetStore
  participant Renderer as BottomSheetRenderer

  UI->>Hook: openBottomSheet(info)
  Hook->>Store: push {id, info} to history
  Renderer->>Store: read top history
  Renderer-->>UI: render ContentComponent with contentProps
  UI->>Hook: updateBottomSheet(nextInfo)
  Hook->>Store: update top history (by id)
  UI->>Hook: closeBottomSheet()
  Hook->>Store: remove entry by id
  Renderer-->>Store: no top -> unmount content
Loading
sequenceDiagram
  participant Caller as API consumer
  participant Debouncer as debouncedRequester
  participant SW as securityWorker
  participant Toast as ToastStore

  Caller->>Debouncer: request(args)
  Debouncer->>Debouncer: debounce window (1s)
  Debouncer->>SW: prepare headers (token or guest JSON)
  Debouncer->>Debouncer: perform fetch
  alt error
    Debouncer->>Toast: show "API ERROR"
  end
  Debouncer-->>Caller: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • devgony

Poem

I nibble at the drawer below,
Little sheets pop up in tow.
Guests hop in, mocks hum a tune,
Debounced calls beneath the moon.
Hop-hop — the UI feels like home. 🥕🐇


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7cd8527 and 78d9b87.

📒 Files selected for processing (1)
  • app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 27

🔭 Outside diff range comments (5)
components/group/groupRoom/GroupChatRoom/GroupChatRoom.tsx (1)

62-73: Fix dependency array: current expression can throw and breaks exhaustive-deps

The dependency expression dereferences properties on a potentially undefined page and is flagged by ESLint. It can also throw at runtime on initial load. Extract to variables and depend on a single stable value.

-useEffect(() => {
-  if (lastMessageRef.current) {
-    lastMessageRef.current.scrollIntoView({
-      behavior: "smooth",
-    });
-  }
-}, [
-  // 마지막 메시지 id
-  data?.[data?.length - 1].messages[
-    data?.[data?.length - 1].messages.length - 1
-  ].messageId,
-]);
+const lastPage = data?.[data.length - 1];
+const lastMessage =
+  lastPage?.messages?.[lastPage.messages.length - 1];
+const lastMessageId = lastMessage?.messageId;
+
+useEffect(() => {
+  if (lastMessageId && lastMessageRef.current) {
+    lastMessageRef.current.scrollIntoView({
+      behavior: "smooth",
+    });
+  }
+}, [lastMessageId]);
app/onboarding/_components/LoginScreen.tsx (1)

36-38: Redact sensitive tokens and URLs from logs

Access/refresh tokens and full URLs with query params are being logged. This is a security/PII risk in production and can leak secrets into logs.

Apply these changes to sanitize logs:

-    console.log("현재 URL:", window.location.href);
+    // Avoid logging full URL as it may contain sensitive query params
+    console.log("현재 Pathname:", window.location.pathname);
-    console.log("URL 파라미터들:");
-    console.log("- code:", code);
-    console.log("- error:", error);
-    console.log("- token:", token);
-    console.log("- state:", state);
-    console.log("- access_token:", accessTokenFromUrl);
-    console.log("- refresh_token:", refreshTokenFromUrl);
+    console.log("URL 파라미터들:");
+    console.log("- code:", code);
+    console.log("- error:", error);
+    console.log("- token:", token ? "[REDACTED]" : null);
+    console.log("- state:", state);
+    console.log("- access_token:", accessTokenFromUrl ? "[REDACTED]" : null);
+    console.log("- refresh_token:", refreshTokenFromUrl ? "[REDACTED]" : null);
-    console.log("모든 URL 파라미터:");
-    for (const [key, value] of urlParams.entries()) {
-      console.log(`- ${key}: ${value}`);
-    }
+    console.log("모든 URL 파라미터:");
+    for (const [key, value] of urlParams.entries()) {
+      const safeValue = /token/i.test(key) ? "[REDACTED]" : value;
+      console.log(`- ${key}: ${safeValue}`);
+    }
-        console.log("✅ Access Token 저장됨:", accessTokenFromUrl);
+        console.log("✅ Access Token 저장됨: [REDACTED]");
-        console.log("✅ Refresh Token 저장됨:", refreshTokenFromUrl);
+        console.log("✅ Refresh Token 저장됨: [REDACTED]");

Also applies to: 51-58, 60-63, 96-104

app/adding-goal/page.tsx (1)

96-118: Prevent default form submission to avoid page reload

onSubmit should call e.preventDefault(); otherwise, the browser may attempt a navigation even in client components.

Apply this diff:

-        onSubmit={async () => {
+        onSubmit={async (e) => {
+          e.preventDefault();
           const res = await goalApi.createGoal({
             isPeriodByMonth: goalAddInfo.durationType === "month",
             title: goalAddInfo.goal,
             dueDate:
               goalAddInfo.durationType === "month"
                 ? undefined
                 : date2StringWithSpliter(
                     goalAddInfo.durationValue as Date,
                     "-",
                   ),
             month:
               goalAddInfo.durationType === "month"
                 ? (goalAddInfo.durationValue as number)
                 : undefined,
             subGoals: goalAddInfo.subGoals.map((subGoalName) => ({
               title: subGoalName,
             })),
           });
           if (res) {
             router.back();
           }
         }}
components/shared/BottomSheets/TodoBottomSheet/TodoBottomSheet.tsx (1)

33-38: Type bug: TodoInfo.id references the wrong type

id should be string (same as submission id), not the todo string.

 type TodoInfo = {
-  id?: TodoInfoForSubmission["todo"];
+  id?: TodoInfoForSubmission["id"];
   todo: TodoInfoForSubmission["todo"];
   date?: TodoInfoForSubmission["date"];
   subGoalTitle?: TodoInfoForSubmission["subGoalTitle"];
   subGoalId?: TodoInfoForSubmission["subGoalId"];
 };
components/shared/BottomSheets/TodoResultBottomSheet/TodoResultBottomSheet.tsx (1)

29-34: Close via bottom-sheet store, not parent prop; streamline props

Use the centralized hook to close the sheet. Remove openBottomSheet/setOpenBottomSheet props and wire the close button to closeBottomSheet().

 interface TodoResultBottomSheetProps {
-  openBottomSheet: boolean;
-  setOpenBottomSheet: (nextIsOpen: boolean) => void;
   onSubmit: (todoResult: TodoResult) => Promise<void>;
-  // hasBottomTabBar: boolean;
 }
-const TodoResultBottomSheet = ({
-  openBottomSheet,
-  setOpenBottomSheet,
-  onSubmit,
-  // hasBottomTabBar,
-}: TodoResultBottomSheetProps) => {
+import useBottomSheet from "@/hooks/useBottomSheet";
+import { useEffect, useState } from "react";
+
+const TodoResultBottomSheet = ({ onSubmit }: TodoResultBottomSheetProps) => {
+  const { closeBottomSheet } = useBottomSheet();
   const [todoResult, setTodoResult] = useState<TodoResult>(initTodoResult);
-  const resetBottomSheet = () => {
-    setOpenBottomSheet(false);
-    // setTodoResult(initTodoResult);
-  };
+  const resetBottomSheet = () => {
+    closeBottomSheet();
+    // setTodoResult(initTodoResult);
+  };
-              <div className="flex justify-center items-center w-6 h-6 relative overflow-hidden text-label-alternative">
+              <div className="flex justify-center items-center w-6 h-6 relative overflow-hidden text-label-alternative">
                 <CloseSvg />
               </div>

Also applies to: 44-48, 51-55, 93-101

🧹 Nitpick comments (64)
components/main/GoalMenuContainer/GoalMenuContainer.tsx (1)

86-121: Commented-out list leaves dead code; memoize list and fix effect deps (ESLint warnings)

With the goal list UI commented out, several measurements/drag pieces above are now effectively dead, and ESLint warns about unstable deps:

  • Wrap goalMenuInfoList in useMemo to stabilize references and satisfy exhaustiveness.
  • Fix the effect that updates the store to depend on goalMenuInfoList and selectedGoalIdx.
  • Remove or guard the unused refs/measurements and motion usage until the list returns.

Follow-up (outside this range), suggested adjustments:

// 1) Memoize derived list
const goalMenuInfoList = useMemo<GoalMenuInfo[]>(
  () =>
    rawGoalData?.goals?.map((goalInfo) => ({
      goal: goalInfo.title ?? "",
      percentage: goalInfo.progress ?? 0,
      goalId: goalInfo.id ?? "",
    })) ?? [],
  [rawGoalData?.goals],
);

// 2) Correct effect deps
useEffect(() => {
  updateGoalId(goalMenuInfoList[selectedGoalIdx]?.goalId ?? null);
}, [goalMenuInfoList, selectedGoalIdx, updateGoalId]);

// 3) Optionally remove/guard the sizing logic while list is disabled
useEffect(() => {
  if (!containerRef.current || !contentRef.current) return;
  setContainerW(containerRef.current.offsetWidth);
  setContentW(contentRef.current.scrollWidth);
}, [goalMenuInfoList.length]);

Consider removing motion/useMotionValue imports and the related refs until the carousel returns, to keep the file lean.

components/shared/TodoItem/TodoItem.tsx (1)

118-135: Avoid hard-coded SVG stroke color; inherit from text color (tokens/Tailwind)

Use currentColor for the stroke and drive color via Tailwind class using your design tokens. Also mark as decorative if it has no semantic meaning.

-    <div className="self-stretch w-6 h-6 flex justify-center items-center relative bg-background-assistive rounded-full overflow-hidden">
+    <div className="self-stretch w-6 h-6 flex justify-center items-center relative bg-background-assistive text-label-assistive rounded-full overflow-hidden" aria-hidden="true">
       <svg
-        width="18"
-        height="18"
+        width="18"
+        height="18"
         viewBox="0 0 18 18"
-        fill="none"
+        fill="none"
         xmlns="http://www.w3.org/2000/svg"
       >
         <path
           d="M9 6.00009L3 12.0001V15.0001L6 15.0001L12 9.00008M9 6.00009L11.1515 3.8486L11.1528 3.84732C11.4489 3.55116 11.5973 3.40282 11.7683 3.34725C11.9189 3.29831 12.0812 3.29831 12.2318 3.34725C12.4027 3.40278 12.5509 3.55095 12.8466 3.84669L14.1515 5.15154C14.4485 5.44855 14.597 5.59713 14.6527 5.76837C14.7016 5.91901 14.7016 6.08127 14.6527 6.2319C14.5971 6.40302 14.4487 6.55137 14.1521 6.84796L14.1515 6.8486L12 9.00008M9 6.00009L12 9.00008"
-          stroke="#8A949E"
+          stroke="currentColor"
           strokeWidth="1.5"
           strokeLinecap="round"
           strokeLinejoin="round"
         />
       </svg>
     </div>
README.md (1)

50-66: Heading levels jump (MD001) and emphasis-as-heading (MD036)

Use H3 under H2 for the feature subsections, not H4, and keep headings as actual headings (not bold). This improves accessibility and satisfies markdownlint.

-#### 🌟 **중장기 목표 설정**
+### 🌟 중장기 목표 설정
 ...
-#### 🔥 **기록 남기기**
+### 🔥 기록 남기기
 ...
-#### 💡 **그룹**
+### 💡 그룹
 ...
-#### 🎯 **피드**
+### 🎯 피드
components/main/GoalCard/GoalCard.tsx (5)

198-201: Remove debug logging or guard it

Console logs in production spam the console. Remove or guard with a dev flag.

-  //test
-  console.log(
-    "shouldTodoResultBottomSheetOpened: ",
-    shouldTodoResultBottomSheetOpened,
-  );
+  // console.debug('[GoalCard] shouldTodoResultBottomSheetOpened:', shouldTodoResultBottomSheetOpened);

184-191: Effect deps incomplete (ESLint) and potentially stale closures

The effect manipulates the TodoBottomSheet via functions returned by useBottomSheet, but they’re omitted from the deps. Either:

  • Add all referenced functions/values to dependencies, or
  • Wrap them in refs or integrate the open/update/close into a single stable callback.

At minimum, include: checkRendered, openTodoBottomSheet, updateTodoBottomSheet, closeTodoBottomSheet, and the subGoals reference you actually use (a memoized projection is safest).


129-139: Backdrops: state consistency and UX

  • You set hasBackdrop to isActive initially, but you also flip hasBackdrop to false inside backdrop onClick. On the next render, the effect will overwrite hasBackdrop back to isActive, which may cause a flicker/inconsistency.
  • Consider driving hasBackdrop solely from state (e.g., isActive) and let backdrop onClick only toggle the state (not hasBackdrop directly).

Also, inline style bottomSheetFixerStyle: { bottom: "56px" } violates the “Tailwind for styling” guideline. Prefer a CSS var or Tailwind utility:

  • If BottomSheet accepts className, use bottom-14.
  • If it requires a style, set a tokenized CSS var: style={{ bottom: "var(--bottom-tabbar-height)" }} and define the var in tokens.css.

Also applies to: 170-173


146-149: Animation timing via setTimeout: add cleanup

setTimeout without cleanup can update state after unmount. Track the timer and clear it.

// example pattern:
const timeoutRef = useRef<number | null>(null);

setIsActivated: (newState: boolean) => {
  if (timeoutRef.current) clearTimeout(timeoutRef.current);
  timeoutRef.current = window.setTimeout(() => setIsActive(newState), 400);
},

useEffect(() => {
  return () => {
    if (timeoutRef.current) clearTimeout(timeoutRef.current);
  };
}, []);

386-414: Harden API submit: handle errors and type outcomes explicitly

Treat API failures explicitly and surface errors. swagger-typescript-api clients often throw on non-2xx; a try/catch yields clearer semantics.

-const handleTodoBottomSheetSubmit: (
+const handleTodoBottomSheetSubmit: (
   newTodoInfo: TodoInfoForSubmission,
   afterSubmit: () => void,
 ) => Promise<boolean> = async (newTodoInfo, afterSubmit: () => void) => {
-  const isCreating = newTodoInfo.id ? false : true;
-  let fetchRes;
-  if (isCreating) {
-    fetchRes = await subGoalApi.createTodo(newTodoInfo.subGoalId, {
-      title: newTodoInfo.todo,
-      date: newTodoInfo?.date
-        ? date2StringWithSpliter(newTodoInfo?.date, "-")
-        : undefined,
-    });
-  } else {
-    fetchRes = await todoApi.updateTodo(newTodoInfo.id ?? "", {
-      date: newTodoInfo.date
-        ? date2StringWithSpliter(newTodoInfo.date, "-")
-        : undefined,
-      title: newTodoInfo.todo,
-    });
-  }
-
-  const isFetchOk = fetchRes ? true : false;
-  if (isFetchOk) {
-    afterSubmit();
-  }
-
-  return isFetchOk;
+  try {
+    const isCreating = !newTodoInfo.id;
+    if (isCreating) {
+      await subGoalApi.createTodo(newTodoInfo.subGoalId, {
+        title: newTodoInfo.todo,
+        date: newTodoInfo?.date
+          ? date2StringWithSpliter(newTodoInfo?.date, "-")
+          : undefined,
+      });
+    } else {
+      await todoApi.updateTodo(newTodoInfo.id ?? "", {
+        date: newTodoInfo.date
+          ? date2StringWithSpliter(newTodoInfo.date, "-")
+          : undefined,
+        title: newTodoInfo.todo,
+      });
+    }
+    afterSubmit();
+    return true;
+  } catch (e) {
+    // TODO: surface toast or error boundary if desired
+    return false;
+  }
 };
mocks/guestMode/types.ts (3)

42-51: Remove redundant | undefined in optional field

In TypeScript, todoResultId?: ID already implies ID | undefined. The explicit union is redundant.

 export interface DBTodo {
   id: ID;
   subGoalId?: ID;
   title: string;
   date?: string; // YYYY-MM-DD (optional)
   status?: TodoStatus;
-  todoResultId?: ID | undefined;
+  todoResultId?: ID;
   ownerId?: ID; // 소유자 (예: "me")
   createdAt?: string;
 }

81-86: Avoid any in message.content; prefer a discriminated union

any defeats type safety. If possible, model message as a discriminated union keyed by type, or use unknown and narrow where used. This will catch shape regressions at compile time.

Example shape:

type GroupMessageContent =
  | { type: "JOIN"; content: { userName: string } }
  | { type: "LEAVE"; content: { userName: string } }
  | { type: "TODO_RESULT_SUBMIT"; content: TodoResultSubmittedContent }
  | { type: "TODO_COMPLETE"; content: TodoCompletedContent }
  | { type: "MESSAGE_REACTION"; content: MessageReactionContent };

export interface DBGroupMessage {
  // ...
  message?: GroupMessageContent;
  // ...
}

22-27: Introduce an explicit date-string alias for consistency

Multiple fields carry date strings. Consider a reusable alias to document the expected format and ease future validation/migration.

export type ISODateString = string; // e.g., "YYYY-MM-DD" or ISO 8601

// Examples:
dueDate?: ISODateString;
createdAt?: ISODateString;
date?: ISODateString;
joinedAt?: ISODateString;

Also applies to: 46-51, 59-60, 66-67, 74-75, 86-87, 103-104, 110-111

components/main/GoalData/GoalData.tsx (1)

31-38: Clamp progress between 0 and 100 to prevent overflow/negative widths

Protect against invalid values to avoid visual glitches and potential layout overflow.

   <div className="flex-1 h-2 relative bg-background-normal rounded-[999px] overflow-hidden">
     <div
       className={` h-2 left-0 top-[0.50px] absolute bg-background-primary rounded-[999px]`}
-      style={{ width: `${progress}%` }}
+      style={{ width: `${Math.min(100, Math.max(0, progress))}%` }}
     ></div>
   </div>
   <p className="flex justify-center text-label-alternative text-sm font-medium font-['SUIT_Variable'] leading-none">
-    {`${progress}%`}
+    {`${Math.min(100, Math.max(0, progress))}%`}
   </p>
components/group/groupRoom/GroupChatRoom/GroupChatRoom.tsx (3)

239-255: Handle API errors when leaving a reaction

Wrap the mutation in try/catch to avoid unhandled rejections and improve UX on failure.

-                    onLeaveReaction={async (selectedType) => {
-                      const emotionEnumVal = Object.entries(
-                        reactionTypeMaps,
-                      ).find(
-                        ([key, val]) => val === selectedType,
-                      )![0] as unknown as UpsertGroupReactionParamsTypeEnum;
-
-                      const res = await groupApi.upsertGroupReaction(
-                        messageInfo.messageId,
-                        {
-                          type: emotionEnumVal,
-                        },
-                      );
-                      if (res) {
-                        closeModal();
-                        mutate();
-                      }
-                    }}
+                    onLeaveReaction={async (selectedType) => {
+                      try {
+                        const emotionEnumVal = Object.entries(reactionTypeMaps).find(
+                          ([, val]) => val === selectedType,
+                        )![0] as unknown as UpsertGroupReactionParamsTypeEnum;
+
+                        const res = await groupApi.upsertGroupReaction(
+                          messageInfo.messageId,
+                          { type: emotionEnumVal },
+                        );
+                        if (res) {
+                          closeModal();
+                          mutate();
+                        }
+                      } catch (e) {
+                        // TODO: surface an error toast
+                        console.error("upsertGroupReaction failed", e);
+                      }
+                    }}

53-54: Type the observingFirstRef ref

Avoid any by specifying the element type.

-  const observingFirstRef = useRef(null);
+  const observingFirstRef = useRef<HTMLDivElement | null>(null);

98-107: Avoid O(n·m) search for referUserName if reactions are frequent

The reduce + findIndex scans all pages per reaction. If this runs often or pages are large, consider precomputing a Map<messageId, userName> once per data fetch.

components/group/GroupChatItem.tsx (2)

51-58: Replace hardcoded hex colors with design tokens (tokens.css)

Per guidelines, prefer tokens over raw hex (e.g., #33363D, #F7F7F8). You’re already using tokens like bg-background-assistive and text-label-normal elsewhere—apply them consistently here for theming parity and maintainability.

If you share a mapping from your design tokens to these shades, I can generate a targeted diff replacing each hex with the closest token utility.

Also applies to: 153-168, 174-180


211-212: Unify font utility usage

Use the same font-SUIT_Variable utility you use elsewhere instead of inline font family syntax for consistency.

-                  <p className="font-['SUIT_Variable']"> Invalid URL path</p>
+                  <p className="font-SUIT_Variable">Invalid URL path</p>
mocks/server.ts (1)

3-5: Don’t hard-switch to guest handlers for all Node/MSW contexts; gate or compose handlers.

Switching to guestHandlers globally will break tests or dev flows that expect the non-guest mocks. Prefer an env-gated selection (or compose both) so you can run either profile without editing code.

Apply this diff:

-// import { handlers } from "./handlers";
+import { handlers } from "./handlers";
 import guestHandlers from "./guestMode/handlers";

-export const server = setupServer(...guestHandlers);
+const isGuestMSW = process.env.NEXT_PUBLIC_MSW_PROFILE === "guest";
+const allHandlers = isGuestMSW ? guestHandlers : handlers;
+
+export const server = setupServer(...allHandlers);

Ensure your test/dev scripts set NEXT_PUBLIC_MSW_PROFILE=guest when you want guest-mode mocks. Otherwise, the default remains the full/auth handlers.

mocks/browser.ts (1)

3-5: Mirror Node-side gating for browser worker to avoid locking into guest-only mocks.

Like the Node server, prefer choosing the handler set via an env profile (or compose them) so you can run either mode without editing code.

Apply this diff:

-// import { handlers } from "./handlers";
+import { handlers } from "./handlers";
 import guestHandlers from "./guestMode/handlers";

-export const worker = setupWorker(...guestHandlers);
+const isGuestMSW = process.env.NEXT_PUBLIC_MSW_PROFILE === "guest";
+const allHandlers = isGuestMSW ? guestHandlers : handlers;
+
+export const worker = setupWorker(...allHandlers);

Note: If you eventually need both sets active simultaneously, ensure route patterns don’t collide or control precedence explicitly.

Confirm your dev startup path exports/uses NEXT_PUBLIC_MSW_PROFILE=guest when testing guest mode (and omits it otherwise), to align with GuestModeHandler’s conditional start/stop.

app/page.tsx (1)

10-10: GoalDataContainer integration and layout adjustments look good.

Replacing GoalCard with GoalDataContainer and adding min-h-screen pb-14 resolves the bottom bar overlap and maintains scroll height. Consider removing the commented GoalCard once the new container is fully vetted.

Also applies to: 34-36

app/onboarding/_components/LoginScreen.tsx (2)

174-180: Guest browse likely shouldn’t call login(); consider removing to avoid inconsistent auth state

Calling login() in the browse path can flip isLoggedIn to true while simultaneously marking the session as guest, which may confuse downstream code and trigger authenticated API calls without tokens.

If guest mode should be unauthenticated, remove the login() call:

   const handleBrowse = () => {
-    // TODO: Handle browse without login
+    // Guest browse: clear auth state and mark guest
     reset();
-    login();
     setIsGuest(true);
     onNext();
   };

If login() intentionally enables certain UI while in guest mode, please confirm the store semantics (isLoggedIn vs isGuest) so we can harden checks where needed.


154-156: Use a single source of truth for FRONTEND_BASE_URL

You import FRONTEND_BASE_URL but use process.env.NEXT_PUBLIC_FRONTEND_BASE_URL inline. Prefer one consistent constant.

-    const redirect_uri = `${process.env.NEXT_PUBLIC_FRONTEND_BASE_URL}/onboarding`;
+    const redirect_uri = `${FRONTEND_BASE_URL}/onboarding`;
-    const redirect_uri = `${process.env.NEXT_PUBLIC_FRONTEND_BASE_URL}/onboarding`;
+    const redirect_uri = `${FRONTEND_BASE_URL}/onboarding`;

Also applies to: 170-172

app/group/page.tsx (3)

26-27: Trim unused store actions from destructure

setIsGuest and logout aren’t used here.

-  const { isGuest, setIsGuest, logout, reset } = useAuthStore();
+  const { isGuest, reset } = useAuthStore();

43-44: Avoid hardcoding the persist storage key

Hardcoding "auth-storage" couples this page to the store’s internal key. Prefer a store helper to purge/persist-reset, or export the key from the store and import it here.

Would you like me to add an explicit purge method to useAuthStore (e.g., purgePersist()) so pages don’t reach into localStorage directly?


36-54: Suppress SWR fetches in guest mode by null-key gating

SWR doesn’t support a React-Query-style enabled flag—instead it skips fetching whenever the key is null. You can expose an enabled option in your hooks (or add an internal guard) so that when isGuest is true, you pass params as null, e.g.:

• In api/hooks.ts, around your useQuery object entries for goalsNotInGroup and joinedGroups, change:

// before
goalsNotInGroup: (config?: SWRConfiguration) =>
  useApiQuery("목표Api", "getGoalsNotInGroup", [], undefined, config),
joinedGroups: (config?: SWRConfiguration) =>
  useApiQuery("그룹Api", "joinedGroups", [], undefined, config),

to:

// after
goalsNotInGroup: ({
  enabled = true,
  ...config
}: { enabled?: boolean } & SWRConfiguration = {}) =>
  useApiQuery(
    "목표Api",
    "getGoalsNotInGroup",
    enabled ? [] : null,
    undefined,
    config,
  ),
joinedGroups: ({
  enabled = true,
  ...config
}: { enabled?: boolean } & SWRConfiguration = {}) =>
  useApiQuery(
    "그룹Api",
    "joinedGroups",
    enabled ? [] : null,
    undefined,
    config,
  ),

• In app/group/page.tsx, invoke with your guard:

const { isGuest } = useAuthStore();
const { data: joinedGroups } = useJoinedGroups({ enabled: !isGuest });
const { data: pendingGroups } = useGoalsNotInGroup({ enabled: !isGuest });

This leverages SWR’s null-key behavior to prevent any network calls (and avoid 401s) when in guest mode.

components/main/GoalDataContainer/GoalDataContainer.tsx (3)

56-63: Use valid Tailwind width classes (w-82 isn’t standard)

w-82 is not a default Tailwind utility and will be ignored unless extended. Use w-full or an arbitrary value like w-[328px].

-        <div className="w-82 h-[130px] bg-background-alternative rounded-lg outline-1 outline-offset-[-1px] outline-line-normal inline-flex flex-col justify-center items-center gap-3">
+        <div className="w-full h-[130px] bg-background-alternative rounded-lg outline-1 outline-offset-[-1px] outline-line-normal inline-flex flex-col justify-center items-center gap-3">

If you intended a specific pixel width, replace w-full with w-[].


51-51: Decide on a user-friendly D-day fallback

Passing NaN to GoalData may render oddly. Consider a semantic fallback (e.g., undefined or 0) and have GoalData render “—” or hide D-day when dueDate is absent.

Do you want me to update GoalData to handle undefined dDay gracefully and adjust this component accordingly?


1-68: Add a Storybook story for GoalDataContainer

Per team conventions, new components should ship with stories. A simple “Primary” story rendering a few mocked goals would help visual regression and design review.

I can scaffold components/main/GoalDataContainer/GoalDataContainer.stories.tsx with controls and mocked data. Want me to open a PR chunk for this?

components/details/BottomSheets/GoalDurationBottomSheet/GoalDurationBottomSheet.stories.tsx (2)

27-36: Make close button actually hide the component in Storybook (wrap with conditional rendering)

Currently the close button calls setOpen(false) but the component remains mounted unconditionally. Wrap the component with a local open state so the close button affects visibility. Also, type args without referring to Primary to avoid circular type usage.

-import { useState } from "react";
+import { useState, type ComponentProps } from "react";
@@
-const Wrapper = ({ args }: { args: typeof Primary.args }) => {
-  const [open, setOpen] = useState(true);
-  return (
-    <GoalDurationBottomSheet
-      {...args}
-      // openBottomSheet={open}
-      setopenBottomSheet={setOpen}
-    />
-  );
-};
+const Wrapper = ({
+  args,
+}: {
+  args: ComponentProps<typeof GoalDurationBottomSheet>;
+}) => {
+  const [open, setOpen] = useState(true);
+  return open ? (
+    <GoalDurationBottomSheet {...args} setopenBottomSheet={setOpen} />
+  ) : (
+    <button type="button" onClick={() => setOpen(true)}>Open Bottom Sheet</button>
+  );
+};

Also applies to: 5-5


55-58: Drop redundant setopenBottomSheet default in args

Wrapper explicitly overrides setopenBottomSheet, so the default here is unused and can confuse readers.

   args: {
     onEdit: () => {},
-    // openBottomSheet: true,
-    setopenBottomSheet: () => {},
     // Primary 스토리에만 적용될 Props
     initDate: undefined,
     initMonth: undefined,
   },
components/shared/BottomSheets/TodoBottomSheet/TodoBottomSheet.stories.tsx (1)

12-17: Update docs description to reflect portal-free rendering in this story

The story now renders the component directly (without a Drawer/portal), so the warning that Docs won’t display correctly is outdated.

   parameters: {
     // Canvas 레이아웃을 중앙으로 정렬하거나 패딩을 추가할 수 있습니다.
     layout: "centered",
     docs: {
       description: {
-        component:
-          "docs에서는 제대로 보이지 않습니다. portal을 사용해서요. 직접 스토리에 들어가서 확인하세요.",
+        component:
+          "이 스토리는 포털 없이 직접 렌더링합니다. Docs와 Canvas 모두에서 확인할 수 있습니다.",
       },
     },
   },
components/details/BottomSheets/GoalDurationBottomSheet/GoalDurationBottomSheet.tsx (3)

105-110: Close the sheet after applying edits

If the desired UX is to close after saving, call setopenBottomSheet(false) after onEdit. Keeps behavior consistent with other bottom sheets.

   <button
     onClick={() => {
       onEdit({
         type: durationType,
         value: durationType === "month" ? selectedMonth : selectedDate,
       });
+      setopenBottomSheet(false);
     }}
     type="button"

33-41: Remove deprecated Drawer remnants and unused import

Large commented blocks and an unused Drawer import add noise and risk confusion during maintenance. Since orchestration moved to a central renderer, this component should be lean content only.

-import { Drawer } from "vaul";
@@
-      {/* <Drawer.Root open={openBottomSheet}>
-        <Drawer.Portal>
-          <Drawer.Overlay
-            className="fixed inset-0 z-20 bg-neutral-700/50 "
-            onClick={() => {
-              setopenBottomSheet(false);
-            }}
-          />
-          <div className="flex justify-center w-[100vw] relative"> */}
-      <div
-        // <Drawer.Content
-        className="
-          z-30
-          
-          w-[360px] h-[585px]  bg-background-alternative rounded-tl-lg rounded-tr-lg shadow-[0px_0px_4px_0px_rgba(0,0,0,0.24)] overflow-hidden
-          flex flex-col items-center
-          pt-5 pl-4 pr-4 pb-6
-          "
-        // className="
-        //   z-30
-        //   bottom-0
-        //   w-[360px] h-[585px]  bg-background-alternative rounded-tl-lg rounded-tr-lg shadow-[0px_0px_4px_0px_rgba(0,0,0,0.24)] overflow-hidden
-        //   flex flex-col items-center
-        //   pt-5 pl-4 pr-4 pb-6
-        //   fixed
-        //   "
-      >
-        {/* <Drawer.Title className="invisible"></Drawer.Title> */}
+      <div
+        className="
+          z-30
+          w-[360px] h-[585px]  bg-background-alternative rounded-tl-lg rounded-tr-lg shadow-[0px_0px_4px_0px_rgba(0,0,0,0.24)] overflow-hidden
+          flex flex-col items-center
+          pt-5 pl-4 pr-4 pb-6
+        "
+      >
@@
-        {/* </Drawer.Content> */}
       </div>
-      {/* </div>
-        </Drawer.Portal>
-      </Drawer.Root> */}

Also applies to: 42-59, 60-61, 120-125, 1-1


13-15: Clean up commented prop remnants

Remove commented openBottomSheet references to keep the public API clear.

 interface GoalDurationBottomSheetProps {
   initMonth?: number;
   initDate?: Date;
-  // openBottomSheet: boolean;
   setopenBottomSheet: (newOpenBottomSheetStatus: boolean) => void;
@@
   initDate,
   initMonth,
-  // openBottomSheet,
   setopenBottomSheet,
   onEdit,
 }: GoalDurationBottomSheetProps) => {

Also applies to: 24-26

components/details/Modals/ModalAddingTodo/ModalAddingTodo.tsx (1)

9-11: Verify ModalCommon typing import or ambient declaration

ModalCommon is referenced but not imported. If it’s not globally declared, TypeScript will error. Import the type from its source or switch to an explicit props shape.

Do you want me to scan the repo and wire the correct import?

app/adding-goal/page.tsx (2)

29-35: Remove unused checkRendered to satisfy lint and reduce noise

checkRendered is destructured but never used.

Apply this diff:

   const {
     checkOpened,
-    checkRendered,
     closeBottomSheet,
     openBottomSheet: openGoalDurationBottomSheet,
     updateBottomSheet: updateGoalDurationBottomSheet,
   } = useBottomSheet<typeof GoalDurationBottomSheet>();

235-247: Dead/commented bottom-sheet JSX can be removed

This commented block is superseded by the hook-driven sheet. Consider removing to keep the file lean.

components/group/Guest/GuestGroup/GuestGroup.tsx (5)

2-2: Add PanInfo typing for drag handler

Avoid any and use PanInfo for stronger type-safety.

Apply this diff:

-import { motion, AnimatePresence } from "motion/react";
+import { motion, AnimatePresence } from "motion/react";
+import type { PanInfo } from "motion";

80-95: Type the drag handler and drop the unused event param

This reduces implicit any usage and tightens intent.

Apply this diff:

-  const handleDragEnd = (event: any, info: any) => {
+  const handleDragEnd = (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {
     const threshold = 50; // 드래그 거리 기준을 조금 늘림
     const { offset } = info;

121-129: Use design tokens instead of raw color classes

Replace text-black with tokenized text color (e.g., text-label-strong) to align with tokens.css usage across components.

Apply this diff:

-            <h2 className="whitespace-pre-wrap w-80 text-center justify-center text-black text-xl font-bold font-['SUIT_Variable'] leading-normal">
+            <h2 className="whitespace-pre-wrap w-80 text-center justify-center text-label-strong text-xl font-bold font-['SUIT_Variable'] leading-normal">

149-151: Tokenize secondary text color

Same rationale as the title: avoid raw text-black.

Apply this diff:

-                      <p className="whitespace-pre-wrap text-center self-stretch justify-center text-black text-base font-bold font-['SUIT_Variable'] leading-tight">
+                      <p className="whitespace-pre-wrap text-center self-stretch justify-center text-label-strong text-base font-bold font-['SUIT_Variable'] leading-tight">

127-127: Verify custom Tailwind classes exist (h-100, w-50)

These aren’t default Tailwind utilities. If not defined in tailwind.config, they’ll be no-ops. Prefer arbitrary values (e.g., h-[100px], w-[200px]) or tokens-based sizing.

Also applies to: 168-168

app/details/edit/[goal_id]/_components/GoalEdit/GoalEdit.tsx (2)

49-57: Align useBottomSheet typing and remove unused checkRendered

Use the same typing pattern as other usages and drop dead code to avoid type/lint drift.

Apply this diff:

   // GoalDurationBottomSheet 관리
   const {
     checkOpened,
-    checkRendered,
     closeBottomSheet,
     openBottomSheet: openGoalDurationBottomSheet,
     updateBottomSheet: updateGoalDurationBottomSheet,
-  } = useBottomSheet<Parameters<typeof GoalDurationBottomSheet>[0]>();
+  } = useBottomSheet<typeof GoalDurationBottomSheet>();

170-183: Remove legacy commented JSX to keep component lean

Hook-based bottom sheet fully replaces this block.

mocks/guestMode/handlers.ts (2)

122-124: Remove debug logging from handlers

Leaking console logs from MSW handlers is noisy and can pollute test output.

Apply this diff:

-      //test
-      console.log("goal in handler: ", goals);

517-524: Optional: return stored points if present

You’re reading points but always returning { point: 0 }. Consider returning the first stored value when available.

Apply this diff:

-      return HttpResponse.json(
-        { point: 0 } as PointRs,
-        // (points[0] as PointRs) || ({ point: 0 } as PointRs),
-      );
+      return HttpResponse.json((points[0] as PointRs) || ({ point: 0 } as PointRs));
app/_components/BottomSheetRenderer.tsx (2)

11-17: Stabilize keys and fix typo; use top entry id for consistent remount behavior

  • Typo: keyCandinates → keyCandidates.
  • Use the top history entry’s id as the Drawer.Root key for consistent remounting when pushing/popping sheets.

Apply this diff:

-  const {
+  const {
+    id: topId,
     hasBackdrop,
     backdropProps,
     ContentComponent,
     contentProps,
     bottomSheetFixerStyle,
   } = bottomSheetInfoHistory?.[bottomSheetInfoHistory.length - 1] ?? {};
 
-  const keyCandinates = ["one", "other"];
+  const keyCandidates = ["one", "other"];
   const [keyIdx, setKeyIdx] = useState(0);
   const isBottomSheetOpen = !!ContentComponent;
-      <Drawer.Root
-        key={`${ContentComponent}`}
+      <Drawer.Root
+        key={topId ?? "no-sheet"}
         open={isBottomSheetOpen}
         defaultOpen={false}
         handleOnly
         disablePreventScroll={isBottomSheetOpen && !hasBackdrop}
       >
-                <ContentComponent
-                  key={keyCandinates[keyIdx]}
+                <ContentComponent
+                  key={keyCandidates[keyIdx]}
                   {...contentProps}
                 />

Also applies to: 19-25, 63-69, 82-85


63-69: Optional: close on swipe/backdrop via onOpenChange and pop history

Hook Drawer.Root’s onOpenChange to pop the top of BottomSheetInfoHistory when it becomes false. This keeps gestures/backdrop interactions in sync with the store.

Example patch (requires adding the updater selector):

-  const bottomSheetInfoHistory = useBottomSheetStore(
-    (state) => state.BottomSheetInfoHistory,
-  );
+  const bottomSheetInfoHistory = useBottomSheetStore(
+    (state) => state.BottomSheetInfoHistory,
+  );
+  const updateBottomSheetInfoHistory = useBottomSheetStore(
+    (state) => state.updateBottomSheetInfoHistory,
+  );
       <Drawer.Root
         key={topId ?? "no-sheet"}
         open={isBottomSheetOpen}
         defaultOpen={false}
         handleOnly
         disablePreventScroll={isBottomSheetOpen && !hasBackdrop}
+        onOpenChange={(next) => {
+          if (!next) {
+            updateBottomSheetInfoHistory((prev) =>
+              prev.length > 0 ? prev.slice(0, -1) : prev,
+            );
+          }
+        }}
       >
hooks/useBottomSheet.ts (2)

58-63: Doc vs behavior mismatch: updateBottomSheet only updates the top sheet

The comment says it updates even if not rendering, but the code requires the item to be the top of the stack. Align the comment or relax the condition.

Apply this diff to align the comment with current behavior:

   /**
-   * 바텀시트를 업데이트 합니다. 해당 바텀시트가 렌더링 되고 있지 않아도 props등은 업데이트 됩니다.
-   * @param newBottomSheetInfo 업데이트할 bottomsheetinfo. id값이 필요합니다.
+   * 바텀시트를 업데이트 합니다. 현재 렌더링(스택 top) 중인 항목에만 적용됩니다.
+   * @param newBottomSheetInfo 업데이트할 bottomsheetinfo
    * @returns id에 매칭되는 바텀시트가 존재하면 업데이트 후 true반환. 아니면 false 반환.
    */

Also applies to: 71-80


42-56: Return the created id from openBottomSheet (or update the JSDoc)

Callers may need the id; the comment promises it. Return it to avoid confusion.

-  const openBottomSheet = (
+  const openBottomSheet = (
     newBottomSheetInfo: NewBottomSheetInfo<TContentProps>,
-  ) => {
+  ) => {
     //
     // const newBottomSheetId = crypto.randomUUID();
     const newBottomSheetId = uuidV4();
     updateBottomSheetInfoHistory((prev) => {
       const newHistory = [
         ...prev,
         { ...newBottomSheetInfo, id: newBottomSheetId },
       ];
       return newHistory;
     });
     setId(newBottomSheetId);
+    return newBottomSheetId;
   };
components/shared/BottomSheets/TodoBottomSheet/TodoBottomSheet.tsx (2)

3-3: Remove unused import

Drawer is no longer used in this component.

-import { Drawer } from "vaul";

134-143: Replace inline styles with Tailwind classes for consistency with guidelines

Avoid inline style for height/transition. Use Tailwind conditional classes.

-        <div
-          // <Drawer.Content
-          className={`              
-                w-[360px]
-               z-30 pl-4 pr-4  bg-white flex flex-col justify-start   rounded-t-[10px]`}
-          style={{
-            height: isActivated ? (showDate ? "382px" : "100px") : "66px", // 고정값 강제
-            transition: "height 0.3s ",
-            bottom: 0,
-          }}
-        >
+        <div
+          className={`w-[360px] z-30 pl-4 pr-4 bg-white flex flex-col justify-start rounded-t-[10px] transition-[height] duration-300 ${
+            isActivated ? (showDate ? "h-[382px]" : "h-[100px]") : "h-[66px]"
+          }`}
+        >
components/shared/BottomSheets/TodoResultBottomSheet/TodoResultBottomSheet.tsx (2)

1-1: Remove unused Drawer import

This component no longer uses Drawer.

-import { Drawer } from "vaul";

156-166: Revoke object URLs to avoid memory leaks

When previewing images via URL.createObjectURL, revoke them on change/unmount.

Add this effect near component bottom:

useEffect(() => {
  return () => {
    if (todoResult.imagePreviewUrl?.startsWith("blob:")) {
      URL.revokeObjectURL(todoResult.imagePreviewUrl);
    }
  };
}, [todoResult.imagePreviewUrl]);
api/service.ts (1)

20-24: Guest mode behavior OK; consider always setting format to 'json'

For consistency, you can set format: "json" in the default branch to avoid response type surprises.

-    return {};
+    return { format: "json" };
stores/useBottomSheetStore.ts (2)

25-31: Prefer using Zustand's get instead of referencing the store inside its own initializer

Avoids potential circular references and is the conventional pattern with Zustand. This also improves type-safety.

-const useBottomSheetStore = create<BottomSheetStore<any>>((set) => ({
+const useBottomSheetStore = create<BottomSheetStore<any>>((set, get) => ({
   BottomSheetInfoHistory: [],
   updateBottomSheetInfoHistory: (newBottomSheetInfoHistoryOrFunc) => {
     if (typeof newBottomSheetInfoHistoryOrFunc === "function") {
-      const prev = useBottomSheetStore.getState().BottomSheetInfoHistory;
+      const prev = get().BottomSheetInfoHistory;
       const newHistory = newBottomSheetInfoHistoryOrFunc(prev);
       set({ BottomSheetInfoHistory: newHistory });
       return;
     }
 
     set({ BottomSheetInfoHistory: newBottomSheetInfoHistoryOrFunc });
   },
 }));

7-15: Type ContentComponent as a React component

Using React.ComponentType<TProps> better communicates intent and integrates with JSX tooling.

 interface BottomSheetStore<TContentProps> {
   BottomSheetInfoHistory: {
     id: string;
-    ContentComponent: (params: TContentProps) => ReactNode;
+    ContentComponent: React.ComponentType<TContentProps>;
     contentProps: TContentProps;
     hasBackdrop: boolean;
     backdropProps: Parameters<typeof Overlay>[0];
     bottomSheetFixerStyle?: HTMLAttributes<HTMLDivElement>["style"];
   }[];
components/details/ListCard/ListCard.tsx (4)

96-146: Fix missing dependencies in useEffect to avoid stale closures and ESLint warnings

The effect captures several functions/values but doesn't list them in the dependency array. This can lead to stale references and is flagged by ESLint.

-  }, [openBottomSheet, todoIdForResult]);
+  }, [
+    openBottomSheet,
+    todoIdForResult,
+    checkRendered,
+    checkOpened,
+    openTodoRootBottomSheet,
+    updateTodoResultBottomSheet,
+    closeBottomSheet,
+    mutate,
+  ]);

102-113: Backdrop props are provided while hasBackdrop is false

If you expect the overlay to render and be clickable, hasBackdrop should be true; otherwise backdropProps are inert. Confirm intended behavior.


242-281: Use the imported PlusSvg or remove the unused import; avoid inline SVG duplication

Keeps icon usage consistent and reduces inline SVG duplication.

-          <div className="w-8 h-8">
+          <div className="w-8 h-8">
             <button
               onClick={() => {
                 openModal(
                   <ModalAddingTodo
                     onAddTodo={async (newTodo) => {
                       if (!subGoalInfo.id) return;
 
                       const res = await subGoalApi.createTodo(subGoalInfo.id, {
                         title: newTodo,
                       });
 
                       if (res) {
                         closeModal();
                         mutate();
                       }
                     }}
                     onClose={closeModal}
                   />,
                 );
               }}
               className="w-full h-full rounded-full bg-background-normal flex justify-center items-center"
               type="button"
             >
-              <svg
-                width="20"
-                height="20"
-                viewBox="0 0 20 20"
-                fill="none"
-                xmlns="http://www.w3.org/2000/svg"
-              >
-                <path
-                  d="M5 10H10M10 10H15M10 10V15M10 10V5"
-                  stroke="#464C53"
-                  strokeWidth="1.5"
-                  strokeLinecap="round"
-                  strokeLinejoin="round"
-                />
-              </svg>
+              <PlusSvg width={20} height={20} />
             </button>
           </div>

150-231: Remove or consolidate large commented blocks

The commented sections add noise and make maintenance harder. Consider removing them or moving them to a doc/notes if needed.

Also applies to: 329-356

app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (3)

91-95: Include openModalCompletingGoal in the dependency array

Prevents stale reference to the callback and addresses the ESLint warning.

-  }, [allSubGoalCompleted, goalDetail?.isCompleted]);
+  }, [allSubGoalCompleted, goalDetail?.isCompleted, openModalCompletingGoal]);

108-166: Fix missing dependencies and avoid closing the bottom sheet on every rerun

  • Add all referenced functions/values to the dependency array to avoid stale closures.
  • The cleanup unconditionally closing the bottom sheet can cause flicker; the effect already handles closing when shouldBottomSheetOpened is false. Remove the cleanup unless you truly want to force-close on unmount only.
   useEffect(() => {
     const isRendered = checkRendered();

     if (!shouldBottomSheetOpened && isRendered) {
       closeBottomSheet();
       return;
     }

     const bottomSheetInfo: Parameters<typeof openBottomSheet>[0] = {
       backdropProps: {
         onClick: () => {
           // 내용물을 초기화 해야 함. -> key값 바꿔도 애니메이션이나 바텀시트 높이 정상적일까?

           setIsActive(false);
           updateBottomSheet((prev) => {
             return { ...prev, hasBackdrop: false };
           });
         },
         className: "fixed inset-0 bg-black/20 z-20",
       },
       ContentComponent: TodoBottomSheet,
       contentProps: {
         isActivated: isActive,
         initTodoInfo: initContent,
         setIsActivated: setIsActive,
         subGoals:
           data.subGoals?.map((subGoalInfo) => ({
             id: subGoalInfo.subGoalId ?? "",
             title: subGoalInfo.subGoal ?? "",
           })) ?? [],
         onSubmitTodo: async (newTodoInfo) => {
           const afterSubmit = () => {
             mutate();
             // 바텀시트 리셋
             setIsActive(false);
           };
           const res = await handleTodoBottomSheetSubmit(
             newTodoInfo,
             afterSubmit,
           );
           return res;
         },
       },

       hasBackdrop: isActive,
       bottomSheetFixerStyle: { bottom: "0px" },
     };

     if (shouldBottomSheetOpened && !isRendered) {
       openBottomSheet(bottomSheetInfo);
       return;
     }

     updateBottomSheet(bottomSheetInfo);

-    return () => {
-      closeBottomSheet();
-    };
-  }, [isActive, initContent, data.subGoals?.length, shouldBottomSheetOpened]);
+  }, [
+    isActive,
+    initContent,
+    shouldBottomSheetOpened,
+    // functions/values used inside effect
+    checkRendered,
+    openBottomSheet,
+    updateBottomSheet,
+    closeBottomSheet,
+    setIsActive,
+    mutate,
+    data.subGoals,
+  ]);

69-72: Guard against NaN in D-day calculation to avoid showing “D+NaN”

When dueDate is absent, leftDay becomes NaN. Provide a safe label.

   const leftDay = goalDetail?.dueDate?.dueDate
     ? calcLeftDay(goalDetail?.dueDate.dueDate)
     : NaN;
+
+  const ddayLabel = Number.isFinite(leftDay)
+    ? `D${leftDay >= 0 ? "-" : "+"}${leftDay !== 0 ? Math.abs(leftDay) : "Day"}`
+    : "D-";
-            <p className="justify-center text-label-primary text-base font-bold font-['SUIT_Variable'] leading-tight">{`D${leftDay >= 0 ? "-" : "+"}${leftDay !== 0 ? Math.abs(leftDay) : "Day"}`}</p>
+            <p className="justify-center text-label-primary text-base font-bold font-['SUIT_Variable'] leading-tight">
+              {ddayLabel}
+            </p>

Also applies to: 177-183

Comment on lines +7 to +10
managerHead: (head) => `
${head}
<link rel="icon" type="image/png" href="/app/icon.png" />
`,
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Favicon path likely 404s in Storybook; point to /public instead of /app

Storybook serves static assets from staticDirs = ../public, so /app/icon.png won’t be resolvable. Use /icon.png or add the app folder to staticDirs.

   managerHead: (head) => `
   ${head}
-  <link rel="icon" type="image/png" href="/app/icon.png" />
+  <link rel="icon" type="image/png" href="/icon.png" />
   `,

Run this to verify where icon.png lives:

Expected: public/icon.png exists; if it’s instead at app/icon.png, either move it to public or add path.resolve(__dirname, "../app") to staticDirs.


🏁 Script executed:

#!/bin/bash
fd -a --strip-cwd-prefix -t f -g "icon.png"

Length of output: 75


Fix broken Storybook favicon by updating staticDirs and link path

Icon.png lives in app/icon.png, but Storybook only serves ../public by default. You’ll need to include the app folder in staticDirs and point the link at /icon.png (or configure a custom mount if you want to preserve /app/icon.png).

• File: .storybook/main.ts
• Add your app directory to staticDirs and change the link href:

--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ export default {
-  staticDirs: [path.resolve(__dirname, '../public')],
+  staticDirs: [
+    path.resolve(__dirname, '../public'),
+    path.resolve(__dirname, '../app'),
+  ],
@@
-  managerHead: (head) => `
-  ${head}
-  <link rel="icon" type="image/png" href="/app/icon.png" />
-  `,
+  managerHead: (head) => `
+  ${head}
+  <link rel="icon" type="image/png" href="/icon.png" />
+  `,

If you prefer to keep the /app prefix, you can instead use:

staticDirs: [
  path.resolve(__dirname, '../public'),
  { from: path.resolve(__dirname, '../app'), to: '/app' },
],
// then keep href="/app/icon.png"
🤖 Prompt for AI Agents
In .storybook/main.ts around lines 7 to 10 the Storybook managerHead injects a
link to /app/icon.png but Storybook only serves ../public by default so the
favicon is broken; update the Storybook config to add the app directory to
staticDirs and change the link href to /icon.png (or alternatively add a
staticDirs mapping from the app folder to the /app mount and keep
href="/app/icon.png"); specifically, add the app path to staticDirs (or a {
from: appPath, to: '/app' } entry) and update the managerHead link to point to
the served path accordingly.

Comment on lines +35 to +65
const debounceer = <T, E>(apiRequest: typeof httpClient.request<T, E>) => {
const timeLimit = 1000;
let timer: number;
let rejectTimer: (reason?: any) => void;
return (
requestParams: Parameters<typeof httpClient.request<T, E>>[0],
): ReturnType<typeof httpClient.request<T>> => {
if (timer) {
clearTimeout(timer);
rejectTimer("debouncing");
}
const apiRes: Promise<T> = new Promise((resolve, reject) => {
rejectTimer = reject;
timer = Number(
setTimeout(async () => {
try {
const res = apiRequest(requestParams);
resolve(res);
} catch (error) {
console.error(error);
showToast(`API ERROR`, new Date());
}
}, timeLimit),
);
});
return apiRes;
};
};

httpClient.request = debounceer(httpClient.request);

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Debouncer breaks typing, loses this context, and doesn’t catch async errors; fix implementation

  • TypeScript: typeof httpClient.request<T, E> is invalid; generics can’t be applied to typeof result like that.
  • Context: calling apiRequest(...) may lose HttpClient’s this. Use the client instance.
  • Error handling: try/catch around a non-awaited promise won’t catch network errors.
  • Global debounce drops every request in the window; verify this is intended. Consider scoping by method+URL.

Apply this diff to fix typing, context, and error handling:

-// Debouncer 감싸도 될 것 같은데?
-const debounceer = <T, E>(apiRequest: typeof httpClient.request<T, E>) => {
-  const timeLimit = 1000;
-  let timer: number;
-  let rejectTimer: (reason?: any) => void;
-  return (
-    requestParams: Parameters<typeof httpClient.request<T, E>>[0],
-  ): ReturnType<typeof httpClient.request<T>> => {
-    if (timer) {
-      clearTimeout(timer);
-      rejectTimer("debouncing");
-    }
-    const apiRes: Promise<T> = new Promise((resolve, reject) => {
-      rejectTimer = reject;
-      timer = Number(
-        setTimeout(async () => {
-          try {
-            const res = apiRequest(requestParams);
-            resolve(res);
-          } catch (error) {
-            console.error(error);
-            showToast(`API ERROR`, new Date());
-          }
-        }, timeLimit),
-      );
-    });
-    return apiRes;
-  };
-};
-
-httpClient.request = debounceer(httpClient.request);
+// Global debouncer (verify this behavior is desired)
+const debounceRequests = (client: HttpClient, ms = 1000): HttpClient["request"] => {
+  let timer: ReturnType<typeof setTimeout> | undefined;
+  let rejectTimer: ((reason?: unknown) => void) | undefined;
+
+  return async function request<T, E>(
+    params: Parameters<HttpClient["request"]>[0],
+  ): ReturnType<HttpClient["request"]> {
+    if (timer) {
+      clearTimeout(timer);
+      rejectTimer?.(new Error("debounced"));
+    }
+    return await new Promise<T>((resolve, reject) => {
+      rejectTimer = reject;
+      timer = setTimeout(async () => {
+        try {
+          const res = await client.request<T, E>(params);
+          resolve(res);
+        } catch (error) {
+          console.error(error);
+          showToast("API ERROR", new Date());
+          reject(error);
+        } finally {
+          timer = undefined;
+          rejectTimer = undefined;
+        }
+      }, ms);
+    });
+  };
+};
+
+httpClient.request = debounceRequests(httpClient);

If the intent is to debounce only identical or mutating requests, I can provide a keyed debouncer (method+URL+[body]) to avoid dropping unrelated calls. Do you want that?


Fix Debouncer in api/service.ts: correct typing, preserve this, and handle async errors

The current implementation introduces three critical issues:

  • Typing is invalid: typeof httpClient.request<T, E> cannot accept generics.
  • Context is lost: calling the unbound apiRequest(...) strips away the HttpClient instance’s this.
  • Async errors aren’t caught: wrapping a non-awaited call in try/catch won’t capture promise rejections.

Additionally, the global debounce drops every request in the window. If you only intend to debounce identical requests, consider a key-based approach (e.g., by URL+params).

Apply this diff to fix all three issues:

--- a/api/service.ts
+++ b/api/service.ts
@@ -35,7 +35,7 @@
-const debounceer = <T, E>(apiRequest: typeof httpClient.request<T, E>) => {
+const debounceRequests = (client: HttpClient, ms = 1000): HttpClient["request"] => {
   let timer: ReturnType<typeof setTimeout> | undefined;
   let rejectTimer: ((reason?: unknown) => void) | undefined;

-  return (requestParams: Parameters<typeof httpClient.request<T, E>>[0]): ReturnType<typeof httpClient.request<T>> => {
+  return async function request<T, E>(
+    params: Parameters<HttpClient["request"]>[0]
+  ): ReturnType<HttpClient["request"]> {
     if (timer) {
       clearTimeout(timer);
       rejectTimer?.(new Error("debounced"));
@@ -45,20 +45,32 @@
-    const apiRes: Promise<T> = new Promise((resolve, reject) => {
-      rejectTimer = reject;
-      timer = Number(
-        setTimeout(async () => {
-          try {
-            const res = apiRequest(requestParams);
-            resolve(res);
-          } catch (error) {
-            console.error(error);
-            showToast(`API ERROR`, new Date());
-          }
-        }, timeLimit),
-      );
-    });
-    return apiRes;
+    return new Promise<T>((resolve, reject) => {
+      rejectTimer = reject;
+      timer = setTimeout(async () => {
+        try {
+          // Preserve context and catch async errors
+          const res = await client.request<T, E>(params);
+          resolve(res);
+        } catch (error) {
+          console.error(error);
+          showToast("API ERROR", new Date());
+          reject(error);
+        } finally {
+          timer = undefined;
+          rejectTimer = undefined;
+        }
+      }, ms);
+    });
   };
 };

-httpClient.request = debounceer(httpClient.request);
+// Override with the debounced version of request
+httpClient.request = debounceRequests(httpClient, 1000);

Let me know if you’d like a keyed-debouncer (method+URL+body) instead of a global one.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In api/service.ts around lines 35 to 65, the debouncer loses the original this,
uses invalid generic typing for typeof httpClient.request, and won’t catch async
rejections; fix it by typing debounceer to accept and return the concrete
function signature (avoid using typeof httpClient.request with generics — use
the actual parameter/return types or a generic function type like <T>(fn: (this:
any, params: RequestParams) => Promise<T>) => (this: any, params: RequestParams)
=> Promise<T>), implement the returned function as a regular function(this: any,
requestParams) so you can call apiRequest.call(this, requestParams) to preserve
context, and await the apiRequest call inside the timer’s async handler within
try/catch (resolve(await apiRequest.call(this, requestParams)) / reject(error))
so promise rejections are caught; also make timer and reject handler optional
(number | undefined) and consider switching to per-request-key timers if you
only want to debounce identical requests.

Comment on lines +30 to +45
const handleMobileKeyboardResize = () => {
const bottomSheet = document.querySelector(
// "#fixer",
"#bottom-sheet",
) as HTMLDivElement;
if (!bottomSheet) return;
const keyboardHeight = window.innerHeight - window.visualViewport!.height;

// 키보드가 올라온 경우
if (keyboardHeight > 0) {
bottomSheet.style.transform = `translateY(-${keyboardHeight}px)`;
}
// 키보드가 내려간 경우
else {
bottomSheet.style.transform = "translateY(0px)";
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid overriding Drawer.Content transform; move keyboard offset to the wrapper to prevent conflicts with Vaul animations

Overwriting style.transform on Drawer.Content will fight Vaul’s own transform-based animations, causing visual glitches and broken interactions (especially on Android). Apply the translateY on the fixed wrapper instead.

Apply this diff:

-    const handleMobileKeyboardResize = () => {
-      const bottomSheet = document.querySelector(
-        // "#fixer",
-        "#bottom-sheet",
-      ) as HTMLDivElement;
+    const handleMobileKeyboardResize = () => {
+      const bottomSheetWrapper = document.querySelector(
+        "#bottom-sheet-fixer",
+      ) as HTMLDivElement;
-      if (!bottomSheet) return;
+      if (!bottomSheetWrapper) return;
       const keyboardHeight = window.innerHeight - window.visualViewport!.height;
 
       // 키보드가 올라온 경우
       if (keyboardHeight > 0) {
-        bottomSheet.style.transform = `translateY(-${keyboardHeight}px)`;
+        bottomSheetWrapper.style.transform = `translateY(-${keyboardHeight}px)`;
       }
       // 키보드가 내려간 경우
       else {
-        bottomSheet.style.transform = "translateY(0px)";
+        bottomSheetWrapper.style.transform = "translateY(0px)";
       }
     };
-          <div
+          <div
+            id="bottom-sheet-fixer"
             className={
               `flex justify-center w-[100vw]  fixed  bottom-14 z-30` /* 바텀탭바 따라 높이 조절은 일단 나중에..*/
             }
             style={bottomSheetFixerStyle}
           >
-            <Drawer.Content id="bottom-sheet">
+            <Drawer.Content>

Also applies to: 73-79

🤖 Prompt for AI Agents
In app/_components/BottomSheetRenderer.tsx around lines 30 to 45 (and similarly
lines 73 to 79), the code sets style.transform directly on the Drawer.Content
element (#bottom-sheet), which conflicts with Vaul's transform-based animations;
instead, move the keyboard translateY to the fixed wrapper element that contains
the Drawer.Content. Update the selector to target the wrapper (e.g., the fixed
container ID/class) and apply bottomSheetWrapper.style.transform =
`translateY(-${keyboardHeight}px)` when keyboardHeight > 0 and reset to
"translateY(0px)" when not, leaving Drawer.Content transform untouched so Vaul
animations remain unaffected.

Comment on lines +5 to +8
const GuestModeHandler = () => {
const { isGuest } = useAuthStore();
return <>{isGuest && <MSWComponent />}</>;
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Stop MSW when leaving guest mode to avoid intercepting real requests.

As written, the worker starts when isGuest becomes true but never stops when it flips back to false. The worker will keep intercepting calls post-login. Add a small effect to stop MSW on exit from guest mode.

Apply this diff:

 import useAuthStore from "@/stores/useAuthStore";
 import { MSWComponent } from "@/components/_mocks/MSWComponent";
+import { useEffect } from "react";
 const GuestModeHandler = () => {
   const { isGuest } = useAuthStore();
+  useEffect(() => {
+    if (isGuest) return;
+    // best-effort stop; no-op if stop API is not exposed
+    const stop = async () => {
+      try {
+        const { stopMsw } = await import("@/mocks/index");
+        await stopMsw?.();
+      } catch {
+        // ignore when not available (e.g., test/server)
+      }
+    };
+    stop();
+  }, [isGuest]);
   return <>{isGuest && <MSWComponent />}</>;
 };

If stopMsw isn’t available yet, I can help wire it up in mocks/index.ts (export the worker and a stopMsw() helper).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const GuestModeHandler = () => {
const { isGuest } = useAuthStore();
return <>{isGuest && <MSWComponent />}</>;
};
import useAuthStore from "@/stores/useAuthStore";
import { MSWComponent } from "@/components/_mocks/MSWComponent";
import { useEffect } from "react";
const GuestModeHandler = () => {
const { isGuest } = useAuthStore();
useEffect(() => {
if (isGuest) return;
// best‐effort stop; no‐op if stop API is not exposed
const stop = async () => {
try {
const { stopMsw } = await import("@/mocks/index");
await stopMsw?.();
} catch {
// ignore when not available (e.g., test/server)
}
};
stop();
}, [isGuest]);
return <>{isGuest && <MSWComponent />}</>;
};
export default GuestModeHandler;
🤖 Prompt for AI Agents
In app/_components/GuestModeHandler.tsx around lines 5 to 8, the component
starts MSW when isGuest is true but never stops it when isGuest flips back to
false, causing the worker to continue intercepting requests after login; update
the component to import stopMsw (from mocks/index.ts), add a useEffect that
watches isGuest and calls stopMsw() when isGuest becomes false (and optionally
calls stopMsw on unmount), and guard the call so you only attempt to stop if the
worker was started or stopMsw is defined.

Comment on lines +430 to +441
const newResult = {
todoResultId: genId(),
todoId: String(todoId),
...(body.request as any),
createdAt: new Date().toISOString(),
} as TodoResultRs;
await dbService.add("todoResults", newResult);
return HttpResponse.json({
todoResultId: newResult.todoResultId,
} as TodoResultIdRs);
} catch (e) {
return HttpResponse.json(
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Store todoResults with id and delete by that id

You’re storing todoResultId instead of id, but later deleting by id. This will make deletions fail. Normalize to id and return todoResultId from the response.

Apply this diff:

-      const newResult = {
-        todoResultId: genId(),
-        todoId: String(todoId),
-        ...(body.request as any),
-        createdAt: new Date().toISOString(),
-      } as TodoResultRs;
-      await dbService.add("todoResults", newResult);
-      return HttpResponse.json({
-        todoResultId: newResult.todoResultId,
-      } as TodoResultIdRs);
+      const newResult: DBTodoResult = {
+        id: genId(),
+        todoId: String(todoId),
+        ...(body.request as any),
+        createdAt: new Date().toISOString(),
+      };
+      await dbService.add("todoResults", newResult);
+      return HttpResponse.json({
+        todoResultId: newResult.id,
+      } as TodoResultIdRs);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const newResult = {
todoResultId: genId(),
todoId: String(todoId),
...(body.request as any),
createdAt: new Date().toISOString(),
} as TodoResultRs;
await dbService.add("todoResults", newResult);
return HttpResponse.json({
todoResultId: newResult.todoResultId,
} as TodoResultIdRs);
} catch (e) {
return HttpResponse.json(
const newResult: DBTodoResult = {
id: genId(),
todoId: String(todoId),
...(body.request as any),
createdAt: new Date().toISOString(),
};
await dbService.add("todoResults", newResult);
return HttpResponse.json({
todoResultId: newResult.id,
} as TodoResultIdRs);
🤖 Prompt for AI Agents
In mocks/guestMode/handlers.ts around lines 430 to 441, the code saves the new
todo result with a property named todoResultId but later deletions expect an id
field; change the stored object to include id: newResultId (i.e., set id:
genId() or map todoResultId to id) so the DB record uses id, keep returning
todoResultId in the HttpResponse payload (generate/assign todoResultId, set id
equal to it in the saved object), and ensure any typing reflects TodoResultRs
has id while the response remains TodoResultIdRs with todoResultId.

Comment on lines +30 to +31
- [🚀 Moiomo 디자인 시스템 둘러보기](#-motimo-디자인-시스템-둘러보기)
- [🤝 기여하기](#-기여하기)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Typo in ToC: “Moiomo” → “Motimo”

Rename for consistency and to avoid broken ToC expectations.

-- [🚀 Moiomo 디자인 시스템 둘러보기](#-motimo-디자인-시스템-둘러보기)
+- [🚀 Motimo 디자인 시스템 둘러보기](#-motimo-디자인-시스템-둘러보기)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- [🚀 Moiomo 디자인 시스템 둘러보기](#-motimo-디자인-시스템-둘러보기)
- [🤝 기여하기](#-기여하기)
- [🚀 Motimo 디자인 시스템 둘러보기](#-motimo-디자인-시스템-둘러보기)
- [🤝 기여하기](#-기여하기)
🤖 Prompt for AI Agents
In README.md around lines 30 to 31, fix the ToC typo by renaming the link label
"Moiomo" to "Motimo" (i.e., change "[🚀 Moiomo 디자인 시스템 둘러보기]" to "[🚀 Motimo 디자인
시스템 둘러보기]") and ensure the anchor target (#-motimo-디자인-시스템-둘러보기) matches the
header ID if needed so the Table of Contents link works correctly.

Comment on lines +186 to +188
<a href="10th-motimo-storybook.vercel.app">10th-motimo-storybook.vercel.app</a>

<br/><br/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

External link missing protocol; make it clickable

Add https:// to the Storybook link so GitHub renders it correctly.

-<a href="10th-motimo-storybook.vercel.app">10th-motimo-storybook.vercel.app</a>
+<a href="https://10th-motimo-storybook.vercel.app">10th-motimo-storybook.vercel.app</a>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<img width="1894" height="945" alt="image" src="https://github.com/user-attachments/assets/0154c0fc-1dcf-4f58-b415-591a75fd751e" />
<a href="10th-motimo-storybook.vercel.app">10th-motimo-storybook.vercel.app</a>
<img width="1894" height="945" alt="image" src="https://github.com/user-attachments/assets/0154c0fc-1dcf-4f58-b415-591a75fd751e" />
<a href="https://10th-motimo-storybook.vercel.app">10th-motimo-storybook.vercel.app</a>
🤖 Prompt for AI Agents
In README.md around lines 186 to 188, the Storybook anchor uses a bare URL
without protocol so GitHub won't render it as a clickable link; update the
anchor href and visible link text to include the full protocol
(https://10th-motimo-storybook.vercel.app) so the link becomes clickable and
renders correctly on GitHub.

Comment on lines +232 to +237
- [Motion](https://github.com/motiondivision/motion) - 애니메이션에 사용된 라이브러리
- [Contributors](https://github.com/prography/10th-Motimo-FE/contributors) - 모든 기여자분들께 감사드립니다
- [Prography](https://prography.org/) - 활발한 개발 환경 지원
- Motimo 팀원들과 Progarphy 10기 분들

<br/><br/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Spelling: “Progarphy” → “Prography”

Minor typo fix in acknowledgements.

-- Motimo 팀원들과 Progarphy 10기 분들
+- Motimo 팀원들과 Prography 10기 분들
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- [Vaul](https://github.com/emilkowalski/vaul) - 바텀 시트에 사용된 라이브러리
- [Motion](https://github.com/motiondivision/motion) - 애니메이션에 사용된 라이브러리
- [Contributors](https://github.com/prography/10th-Motimo-FE/contributors) - 모든 기여자분들께 감사드립니다
- [Prography](https://prography.org/) - 활발한 개발 환경 지원
- Motimo 팀원들과 Progarphy 10기 분들
- [Vaul](https://github.com/emilkowalski/vaul) - 바텀 시트에 사용된 라이브러리
- [Motion](https://github.com/motiondivision/motion) - 애니메이션에 사용된 라이브러리
- [Contributors](https://github.com/prography/10th-Motimo-FE/contributors) - 모든 기여자분들께 감사드립니다
- [Prography](https://prography.org/) - 활발한 개발 환경 지원
- Motimo 팀원들과 Prography 10기 분들
🤖 Prompt for AI Agents
In README.md around lines 232 to 237, there's a typo "Progarphy" in the
acknowledgements; update the spelling to "Prography" so the link text and any
plain occurrences read correctly, and verify there are no other instances of the
misspelling elsewhere in the file.

Comment on lines +111 to 113
// Guest
setIsGuest: (newVal) => set({ isGuest: newVal }),
}),
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Enforce mutual exclusivity between guest and logged-in states in the setter.

To avoid inconsistent UI/logic, ensure setIsGuest(true) forces isLoggedIn to false.

Apply this diff:

-      // Guest
-      setIsGuest: (newVal) => set({ isGuest: newVal }),
+      // Guest
+      setIsGuest: (newVal) =>
+        set((state) => ({
+          isGuest: newVal,
+          // ensure mutual exclusivity
+          isLoggedIn: newVal ? false : state.isLoggedIn,
+        })),

Optionally, also clear tokens when entering guest mode if your network layer relies on token presence to branch behavior. If desired, I can provide a minimal safe change for that as well.
[suggest_optional_refactor]

Run this to check call sites set the expected values across flows:


🏁 Script executed:

#!/bin/bash
# Inspect how guest mode and login are toggled throughout the app
rg -n -C3 -P --type tsx --type ts --type tsx '\bsetIsGuest\s*\('
rg -n -C3 -P --type tsx --type ts --type tsx '\blogin\s*\('

Length of output: 184


Let’s broaden the search to all .ts/.tsx files without relying on ripgrep file-type definitions:


🏁 Script executed:

#!/bin/bash
# Inspect how guest mode and login are toggled throughout the app
rg -n -C3 -g '*.ts' -g '*.tsx' '\bsetIsGuest\s*\(' .
rg -n -C3 -g '*.ts' -g '*.tsx' '\blogin\s*\(' .

Length of output: 2712


Enforce mutual exclusivity in setIsGuest

We’ve identified flows (e.g. the handleBrowse flow in
app/onboarding/_components/LoginScreen.tsx) that call login() then
setIsGuest(true), which currently leaves both isLoggedIn and isGuest
set to true. To avoid inconsistent state, update the setIsGuest setter so
that turning on guest mode automatically clears isLoggedIn.

Files to update:

  • stores/useAuthStore.ts (setter for setIsGuest)

Apply this change:

--- a/stores/useAuthStore.ts
@@ lines 111-113
-      // Guest
-      setIsGuest: (newVal) => set({ isGuest: newVal }),
+      // Guest
+      setIsGuest: (newVal) =>
+        set((state) => ({
+          isGuest: newVal,
+          // ensure mutual exclusivity
+          isLoggedIn: newVal ? false : state.isLoggedIn,
+        })),

Optional cleanup (if your network layer uses tokens to branch behavior), clear tokens when entering guest mode:

-      setIsGuest: (newVal) =>
-        set((state) => ({
-          isGuest: newVal,
-          isLoggedIn: newVal ? false : state.isLoggedIn,
-        })),
+      setIsGuest: (newVal) =>
+        set((state) => ({
+          isGuest: newVal,
+          isLoggedIn: newVal ? false : state.isLoggedIn,
+          authToken: newVal ? null : state.authToken,
+        })),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Guest
setIsGuest: (newVal) => set({ isGuest: newVal }),
}),
// Guest
setIsGuest: (newVal) =>
set((state) => ({
isGuest: newVal,
// ensure mutual exclusivity
isLoggedIn: newVal ? false : state.isLoggedIn,
})),
}),
🤖 Prompt for AI Agents
In stores/useAuthStore.ts around lines 111 to 113, the setIsGuest setter should
enforce mutual exclusivity by ensuring when isGuest is set to true it also
clears isLoggedIn (and optionally clears any auth tokens); update the setter so
that set({ isGuest: newVal, isLoggedIn: newVal ? false : get().isLoggedIn }) (or
equivalent) is used, and if your network layer relies on tokens also clear them
when turning on guest mode.

Copy link

@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: 0

🔭 Outside diff range comments (2)
app/adding-goal/page.tsx (2)

96-118: Add error handling for API call

The form submission doesn't handle potential API failures, which could leave users confused if the request fails.

Add try-catch error handling:

         onSubmit={async () => {
+          try {
             const res = await goalApi.createGoal({
               isPeriodByMonth: goalAddInfo.durationType === "month",
               title: goalAddInfo.goal,
               dueDate:
                 goalAddInfo.durationType === "month"
                   ? undefined
                   : date2StringWithSpliter(
                       goalAddInfo.durationValue as Date,
                       "-",
                     ),
               month:
                 goalAddInfo.durationType === "month"
                   ? (goalAddInfo.durationValue as number)
                   : undefined,
               subGoals: goalAddInfo.subGoals.map((subGoalName) => ({
                 title: subGoalName,
               })),
             });
             if (res) {
               router.back();
             }
+          } catch (error) {
+            console.error("Failed to create goal:", error);
+            // Consider showing a user-friendly error message
+          }
         }}

96-118: Prevent default form submission behavior

The form submission handler doesn't prevent the default browser behavior, which could cause page refresh.

-        onSubmit={async () => {
+        onSubmit={async (e) => {
+          e.preventDefault();
           const res = await goalApi.createGoal({
♻️ Duplicate comments (1)
app/adding-goal/page.tsx (1)

37-82: Fix missing dependencies in useEffect hook

The ESLint warning indicates missing dependencies in the useEffect. This can lead to stale closures and unexpected behavior.

Apply this diff to fix the dependency array and simplify the logic:

   useEffect(() => {
     if (!openBottomSheet) {
       closeBottomSheet();
+      return;
     }
 
     const bottomSheetInfo: Parameters<typeof openGoalDurationBottomSheet>[0] = {
       backdropProps: {
         className: "fixed inset-0 z-20 bg-neutral-700/50 ",
         onClick: () => {
           setOpenBottomSheet(false);
         },
       },
       ContentComponent: GoalDurationBottomSheet,
       hasBackdrop: true,
       bottomSheetFixerStyle: {
         bottom: 0,
       },
       contentProps: {
         setopenBottomSheet: setOpenBottomSheet,
         onEdit: async ({ type, value }) => {
           setGoalAddInfo((prev) => ({
             ...prev,
             durationType: type,
             durationValue: value,
           }));
           // 닫기
           setOpenBottomSheet(false);
         },
       },
     };
-    const isOpended = checkOpened();
-    if (openBottomSheet && !isOpended) {
+    const isOpened = checkOpened();
+    if (!isOpened) {
       openGoalDurationBottomSheet(bottomSheetInfo);
-      return;
-    }
-
-    if (openBottomSheet) {
+    } else {
       updateGoalDurationBottomSheet(bottomSheetInfo);
     }
 
     return () => {
-      if (!openBottomSheet) {
-        closeBottomSheet();
-      }
+      closeBottomSheet();
     };
-  }, [openBottomSheet]);
+  }, [
+    openBottomSheet,
+    checkOpened,
+    closeBottomSheet,
+    openGoalDurationBottomSheet,
+    updateGoalDurationBottomSheet,
+  ]);
🧹 Nitpick comments (5)
components/details/ListCard/ListCard.stories.tsx (2)

38-40: Remove stale commented-out props to prevent drift

These callbacks were removed from the component API; keeping commented placeholders adds noise and risks confusion.

Apply this minimal cleanup:

-    // applyOnGoalData: () => {},
-    // onLeft: () => {},
-    // onRight: () => {},

42-43: Drop commented subGoalInfo fields that no longer exist

Stories should mirror the current public props; if you need to document prior fields, move that into Docs descriptions instead of commented code.

Apply this minimal cleanup:

-      // idx: 0,
-      // totalSubGoalsLen: 4,
app/adding-goal/page.tsx (3)

67-67: Fix typo in variable name

The variable name has a typo that should be corrected for consistency.

-    const isOpended = checkOpened();
+    const isOpened = checkOpened();

180-194: Add unique keys for list items

Using array indices as keys can cause issues with React's reconciliation when the list changes. Consider using a more stable identifier.

Consider using a unique identifier for each sub-goal instead of the array index. You could generate IDs when adding new sub-goals:

-            {goalAddInfo.subGoals.map((subGoal, idx) => (
+            {goalAddInfo.subGoals.map((subGoal, idx) => (
               <TextField
-                key={`${idx}`}
+                key={`subgoal-${idx}-${subGoal}`}
                 isError={false}

Or better yet, modify the data structure to include unique IDs for each sub-goal.


235-247: Remove commented-out code

The commented-out JSX for the old bottom sheet implementation should be removed as it's no longer needed with the new hook-based approach.

-      {/* <GoalDurationBottomSheet
-        openBottomSheet={openBottomSheet}
-        setopenBottomSheet={setOpenBottomSheet}
-        onEdit={async ({ type, value }) => {
-          setGoalAddInfo((prev) => ({
-            ...prev,
-            durationType: type,
-            durationValue: value,
-          }));
-          // 닫기
-          setOpenBottomSheet(false);
-        }}
-      /> */}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 43e0d7b and e5eebd3.

📒 Files selected for processing (3)
  • app/adding-goal/page.tsx (4 hunks)
  • components/details/BottomSheets/GoalDurationBottomSheet/GoalDurationBottomSheet.stories.tsx (2 hunks)
  • components/details/ListCard/ListCard.stories.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/details/BottomSheets/GoalDurationBottomSheet/GoalDurationBottomSheet.stories.tsx
🧰 Additional context used
📓 Path-based instructions (4)
components/**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/global-rule.mdc)

For every styling, use tailwind css.

Files:

  • components/details/ListCard/ListCard.stories.tsx
{components/**/*.{js,jsx,ts,tsx},**/*.css}

📄 CodeRabbit Inference Engine (.cursor/rules/global-rule.mdc)

For tokens, use tokens.css (mdc:app/tokens.css).

Files:

  • components/details/ListCard/ListCard.stories.tsx
components/**/*

📄 CodeRabbit Inference Engine (.cursor/rules/global-rule.mdc)

Component should be created in components/

Files:

  • components/details/ListCard/ListCard.stories.tsx
components/**/*.stories.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/global-rule.mdc)

components/**/*.stories.{js,jsx,ts,tsx}: If Component is created, add storybook files as well.
The Default example of storybook should be named Primary

Files:

  • components/details/ListCard/ListCard.stories.tsx
🪛 GitHub Actions: CI Pipeline
app/adding-goal/page.tsx

[warning] 82-82: React Hook useEffect has missing dependencies: 'checkOpened', 'closeBottomSheet', 'openGoalDurationBottomSheet', and 'updateGoalDurationBottomSheet'. Either include them or remove the dependency array. (react-hooks/exhaustive-deps)

⏰ 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). (1)
  • GitHub Check: chromatic-test

DoneItemDetail에서 useGoalDetail훅을 사용하는데, 이는 스토리북에서 사용하려면 따로 render에서 불러와야 함. 그러나 컴포넌트 내부에서 사용되지 않아 주석처리함.
Copy link

@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: 3

🔭 Outside diff range comments (1)
app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (1)

45-47: Fix possible runtime crash: data can be undefined when dereferencing

data may be undefined on initial render, causing Cannot read properties of undefined (reading 'subGoals') in the hook argument.

Apply this diff:

-  const { mutate } = useSubGoalTodosAllInfinite(
-    data.subGoals?.[targetSubGoalIdx]?.subGoalId ?? "",
-  );
+  const { mutate } = useSubGoalTodosAllInfinite(
+    data?.subGoals?.[targetSubGoalIdx]?.subGoalId ?? "",
+  );
♻️ Duplicate comments (1)
app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (1)

248-262: Provide a stable fallback for React list keys (align with previous review)

Keys can be undefined if subGoalId is missing, leading to unstable reconciliation. Use subGoalId first, and fall back to index.

Apply this diff:

-          {data.subGoals?.map((subGoalInfo) => (
+          {data.subGoals?.map((subGoalInfo, idx) => (
             <ListCard
-              key={subGoalInfo.subGoalId}
+              key={subGoalInfo?.subGoalId ?? `subgoal-${idx}`}
               initTodoInfoList={subGoalInfo?.initTodoItemsInfo}
               subGoalInfo={{
                 id: subGoalInfo?.subGoalId,
                 // idx: targetSubGoalIdx,
                 name: subGoalInfo?.subGoal,
                 // totalSubGoalsLen: data.subGoals?.length ?? 0,
                 isCompleted: subGoalInfo?.isCompleted,
               }}
             />
           ))}
🧹 Nitpick comments (2)
app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (2)

187-193: Use the already-rounded fixedProgress consistently

You compute fixedProgress but render raw goalDetail?.progress, creating rounding inconsistencies between bar width and label when decimals exist.

Apply this diff:

-                style={{ width: `${goalDetail?.progress ?? 0}%` }}
+                style={{ width: `${fixedProgress}%` }}
...
-              {`${goalDetail?.progress ?? 0}%`}
+              {`${fixedProgress}%`}

345-373: Add error handling in submission helper

API calls may throw/return errors. Wrap in try/catch and surface failure toasts or logs. This will also make isFetchOk reflect actual success/failure rather than truthiness of the response.

Example:

const handleTodoBottomSheetSubmit = async (newTodoInfo, afterSubmit) => {
  try {
    const isCreating = !newTodoInfo.id;
    const fetchRes = isCreating
      ? await subGoalApi.createTodo(newTodoInfo.subGoalId, {
          title: newTodoInfo.todo,
          date: newTodoInfo?.date ? date2StringWithSpliter(newTodoInfo?.date, "-") : undefined,
        })
      : await todoApi.updateTodo(newTodoInfo.id!, {
          date: newTodoInfo.date ? date2StringWithSpliter(newTodoInfo.date, "-") : undefined,
          title: newTodoInfo.todo,
        });

    const ok = !!fetchRes;
    if (ok) afterSubmit();
    return ok;
  } catch (e) {
    // e.g., setToast("할 일 저장에 실패했어요. 잠시 후 다시 시도해주세요.");
    return false;
  }
};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e5eebd3 and 742c292.

📒 Files selected for processing (1)
  • app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (7 hunks)
⏰ 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). (1)
  • GitHub Check: chromatic-test

Comment on lines +69 to +72
const leftDay = goalDetail?.dueDate?.dueDate
? calcLeftDay(goalDetail?.dueDate.dueDate)
: NaN;

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against NaN left-day and ensure D-Day formatting is correct when 0

  • When goalDetail?.dueDate?.dueDate is missing, leftDay becomes NaN, rendering D+NaN.
  • For leftDay === 0, you currently render D-Day (which is fine if intended), but the logic is easier to read if we avoid numeric ops on null/NaN.

Apply these diffs:

Set a null fallback instead of NaN:

-  const leftDay = goalDetail?.dueDate?.dueDate
-    ? calcLeftDay(goalDetail?.dueDate.dueDate)
-    : NaN;
+  const leftDay = goalDetail?.dueDate?.dueDate
+    ? calcLeftDay(goalDetail?.dueDate.dueDate)
+    : null;

Render label defensively:

-            <p className="justify-center text-label-primary text-base font-bold font-['SUIT_Variable'] leading-tight">{`D${leftDay >= 0 ? "-" : "+"}${leftDay !== 0 ? Math.abs(leftDay) : "Day"}`}</p>
+            <p className="justify-center text-label-primary text-base font-bold font-['SUIT_Variable'] leading-tight">
+              {leftDay === null
+                ? "-"
+                : `D${leftDay === 0 ? "-Day" : `${leftDay > 0 ? "-" : "+"}${Math.abs(leftDay)}`}`}
+            </p>

Also applies to: 178-194

🤖 Prompt for AI Agents
In app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx around lines
69-72 (and similarly 178-194), replace the NaN fallback with a null fallback
when dueDate is missing (e.g., const leftDay = goalDetail?.dueDate?.dueDate ?
calcLeftDay(...) : null), and update rendering logic to treat null defensively:
if leftDay === null show a safe placeholder (e.g., '-' or empty), if leftDay ===
0 render "D-Day", otherwise render "D‑N" for future days and "D+N" for past days
using Math.abs for sign handling; apply the same null-aware checks in the other
block at lines 178-194.

Comment on lines 139 to 147
const afterSubmit = () => {
mutate();
// 바텀시트 리셋
setIsActive(false);
};
const res = await handleTodoBottomSheetSubmit(
newTodoInfo,
afterSubmit,
);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Refetch likely targets the wrong sub-goal after submit

afterSubmit calls the mutate returned from a hook that is bound to data?.subGoals?.[targetSubGoalIdx]?.subGoalId (with targetSubGoalIdx fixed at 0). If the user creates/edits a todo for a different sub-goal, you’re invalidating/refetching the wrong list.

Consider invalidating/refetching by the sub-goal ID contained in newTodoInfo.subGoalId using your query client (React Query) or global mutate (SWR). For example with React Query:

// inside onSubmitTodo -> after successful submission
const queryClient = useQueryClient();
await queryClient.invalidateQueries({
  queryKey: ['subGoalTodosAllInfinite', { subGoalId: newTodoInfo.subGoalId }],
});
setIsActive(false);

If you prefer not to couple to the query key here, you can expose an invalidator function from the todos query module that takes subGoalId.


#!/bin/bash
echo "Locating DetailBody.tsx..."
file=$(fd DetailBody.tsx)
echo "File found at: $file"

echo -e "\n📄 Displaying lines 100–180 of DetailBody.tsx"
sed -n '100,180p' "$file"

echo -e "\n🔍 Searching for targetSubGoalIdx usage in DetailBody.tsx"
rg -n "targetSubGoalIdx" -C5 "$file"

echo -e "\n🔍 Searching for useSWR or useSWRInfinite across the repo"
rg -n --type ts,tsx "useSWR" -C3 .
rg -n --type ts,tsx "useSWRInfinite" -C3 .

echo -e "\n🔍 Searching for mutate( calls under app/details"
rg -n --type ts,tsx "mutate(" -C5 app/details


🏁 Script executed:

#!/bin/bash
echo "Locating DetailBody.tsx..."
file=$(fd DetailBody.tsx)
echo "Found file: $file"

echo -e "\nDisplaying lines 100–200 of DetailBody.tsx:"
sed -n '100,200p' "$file"

echo -e "\nSearching for mutate destructuring in DetailBody.tsx:"
rg -n "const\s+{[^}]*mutate" -C3 "$file"

echo -e "\nSearching for useSWR or useSWRInfinite in DetailBody.tsx:"
rg -n "useSWR" -C3 "$file"
rg -n "useSWRInfinite" -C3 "$file"

echo -e "\nSearching for targetSubGoalIdx definitions and usage:"
rg -n "targetSubGoalIdx" -C3 "$file"

echo -e "\nSearching for handleTodoBottomSheetSubmit import and call:"
rg -n "handleTodoBottomSheetSubmit" -C3 "$file"

Length of output: 7144


Ensure Todos are Refetched for the Submitted Sub-goal

The current afterSubmit callback always calls the mutate function from
useSubGoalTodosAllInfinite bound to data.subGoals[targetSubGoalIdx] (which remains at index 0). If a todo is created or edited for any other sub-goal, that sub-goal’s list won’t be updated.

• File: app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx
• Affected lines: the afterSubmit definition and its invocation of mutate() around lines 141–147

Replace:

const afterSubmit = () => {
  mutate();           // always invalidates subGoals[0]
  setIsActive(false);
};

With (React Query example):

const queryClient = useQueryClient();

const afterSubmit = async (submittedInfo: TodoInfoForSubmission) => {
  await queryClient.invalidateQueries({
    queryKey: [
      'subGoalTodosAllInfinite',
      { subGoalId: submittedInfo.subGoalId },
    ],
  });
  setIsActive(false);
};

// …

const res = await handleTodoBottomSheetSubmit(
  newTodoInfo,
  () => afterSubmit(newTodoInfo),
);

If you’d rather not reference the raw query key here, expose a helper (e.g. invalidateSubGoalTodos(subGoalId)) from your todos-query module and call that instead.

🤖 Prompt for AI Agents
In app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx around lines 139
to 147, the afterSubmit callback calls mutate() which always targets the bound
subGoals[0], so todos for other sub-goals aren’t refetched; replace this with
logic that invalidates/refetches only the specific sub-goal's todos (use
useQueryClient().invalidateQueries with the subGoalId-specific query key or call
an exported helper like invalidateSubGoalTodos(subGoalId)), change afterSubmit
to accept the submitted todo info (or subGoalId) and call the invalidate helper
with that id, and pass a wrapper to handleTodoBottomSheetSubmit so afterSubmit
is invoked with the newTodoInfo before closing the bottom sheet.

@Hys-Lee Hys-Lee requested a review from devgony August 17, 2025 10:09
Copy link

@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

🧹 Nitpick comments (7)
app/onboarding/page.tsx (3)

35-40: Prefer optional chaining over nested default destructuring to preserve types (and enable isValidating).

Nested default destructuring (data: { goals } = {}) erases the hook’s response type and can turn goals into any. It also makes it harder to include additional flags like isValidating for better loading states.

Apply this diff to keep type information intact (and expose isValidating for the spinner logic below):

-  const {
-    data: { goals } = {},
-    isLoading,
-    error,
-    mutate,
-  } = useGoals({
+  const {
+    data,
+    isLoading,
+    isValidating,
+    error,
+    mutate,
+  } = useGoals({

Then, right below the hook call, derive goals with proper optional chaining:

// Right below the hook call
const goals = data?.goals;

63-63: Redirect now gated by isLoggedIn; verify guest-mode semantics.

Given guest mode was added in this PR, confirm whether guests who’ve completed onboarding should also be redirected to “/”. If yes, include isGuest in the condition.

-    if (hasCompletedOnboarding && isLoggedIn) {
+    if (hasCompletedOnboarding && (isLoggedIn || isGuest)) {

And ensure isGuest is pulled from the store:

// Above
const { setHasCompletedOnboarding, isLoggedIn, hasCompletedOnboarding, isGuest } = useAuthStore();

118-123: Avoid UI flicker on login-triggered refetch; tie the spinner to revalidation.

With the current condition, during a post-login refetch the presence of a previous error can suppress the spinner momentarily. Consider also showing the spinner during revalidation.

If your hook exposes isValidating (SWR v2), fold it into the condition:

-  if (
-    hasHydrated &&
-    isLoggedIn &&
-    (isLoading || (!goals && !error) || (goals && goals.length > 0))
-  ) {
+  if (
+    hasHydrated &&
+    isLoggedIn &&
+    (isLoading || isValidating || (!goals && !error) || (goals && goals.length > 0))
+  ) {

Additionally, your login-retry effect (Lines 47-52) can loop if the error persists after login (each mutate produces a new error object). Guard it to run once per login transition:

// Above effects
const retriedAfterLoginRef = useRef(false);

// Replace the login-retry effect body:
useEffect(() => {
  if (hasHydrated && isLoggedIn && error && !retriedAfterLoginRef.current) {
    retriedAfterLoginRef.current = true;
    mutate();
  }
  if (!isLoggedIn) {
    // reset guard when user logs out
    retriedAfterLoginRef.current = false;
  }
}, [hasHydrated, isLoggedIn, error, mutate]);
components/mypage/DoneItemDetail.stories.tsx (4)

5-20: Solid Story meta; consider “satisfies Meta” for stronger typing and clarify goalDetail description

  • Using the satisfies operator preserves arg inference and catches excess props without widening types.
  • The description can reflect both todos/completedTodos used across stories.

Apply this diff:

-const meta: Meta<typeof DoneItemDetail> = {
+const meta = {
   title: "mypage/DoneItemDetail",
   component: DoneItemDetail,
   parameters: {
     layout: "centered",
   },
   tags: ["autodocs"],
   argTypes: {
     goalDetail: {
-      description: "Complete goal detail with sub-goals and completed todos",
+      description: "Complete goal detail with sub-goals and items (todos/completedTodos)",
     },
     className: {
       control: "text",
       description: "Additional CSS classes",
     },
   },
-};
+} satisfies Meta<typeof DoneItemDetail>;

26-47: Standardize date string format (ISO 8601) for consistency

Primary uses dueDate as YYYY-MM-DD but todos use dots. Prefer ISO 8601 across stories to avoid confusion (and to be friendlier to parsers if reused).

Apply this diff:

-              date: "2025.01.15",
+              date: "2025-01-15",

106-136: Standardize completedDate strings to ISO 8601

Keeps story data consistent and predictable.

Apply this diff:

-              completedDate: "2025.01.20",
+              completedDate: "2025-01-20",
-              completedDate: "2025.01.21",
+              completedDate: "2025-01-21",
-              completedDate: "2025.01.22",
+              completedDate: "2025-01-22",

106-136: Optional: add a variant showcasing className customization

A small story demonstrating custom className (e.g., padding or background) can help consumers understand styling hooks.

You can add:

export const WithCustomClassName: Story = {
  args: {
    className: "p-4 bg-Color-gray-50",
    goalDetail: {
      id: "2",
      title: "ClassName Showcase",
      subGoals: [{ id: "1", title: "예시", completedTodos: [] }],
    },
  },
};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fc3f8b0 and efee88a.

📒 Files selected for processing (2)
  • app/onboarding/page.tsx (4 hunks)
  • components/mypage/DoneItemDetail.stories.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
components/**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/global-rule.mdc)

For every styling, use tailwind css.

Files:

  • components/mypage/DoneItemDetail.stories.tsx
{components/**/*.{js,jsx,ts,tsx},**/*.css}

📄 CodeRabbit Inference Engine (.cursor/rules/global-rule.mdc)

For tokens, use tokens.css (mdc:app/tokens.css).

Files:

  • components/mypage/DoneItemDetail.stories.tsx
components/**/*

📄 CodeRabbit Inference Engine (.cursor/rules/global-rule.mdc)

Component should be created in components/

Files:

  • components/mypage/DoneItemDetail.stories.tsx
components/**/*.stories.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/global-rule.mdc)

components/**/*.stories.{js,jsx,ts,tsx}: If Component is created, add storybook files as well.
The Default example of storybook should be named Primary

Files:

  • components/mypage/DoneItemDetail.stories.tsx
🧠 Learnings (1)
📚 Learning: 2025-07-21T07:45:37.405Z
Learnt from: CR
PR: prography/10th-Motimo-FE#0
File: .cursor/rules/global-rule.mdc:0-0
Timestamp: 2025-07-21T07:45:37.405Z
Learning: Applies to components/**/*.stories.{js,jsx,ts,tsx} : The `Default` example of storybook should be named `Primary`

Applied to files:

  • components/mypage/DoneItemDetail.stories.tsx
🧬 Code Graph Analysis (2)
app/onboarding/page.tsx (1)
api/hooks.ts (1)
  • useGoals (174-174)
components/mypage/DoneItemDetail.stories.tsx (1)
components/mypage/DoneItemDetail.tsx (1)
  • DoneItemDetail (44-74)
⏰ 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). (1)
  • GitHub Check: chromatic-test
🔇 Additional comments (2)
app/onboarding/page.tsx (1)

74-81: Deps array LGTM.

Explicitly listing all used values keeps the effect predictable. No functional concerns here.

components/mypage/DoneItemDetail.stories.tsx (1)

26-47: Primary story name LGTM and aligns with our guideline

You adhered to our learning that the default example should be named “Primary.” Nice.

Comment on lines +50 to 104
args: {
goalDetail: {
id: "1",
title: "독서 습관 만들기",
subGoals: [
{
id: "1",
title: "매일 30분 책 읽기",
completedTodos: [
{
id: "1",
title: "소설 '데미안' 읽기",
completedDate: "2025.01.15",
attachment: {
type: "image",
url: "https://picsum.photos/200",
},
},
{
id: "2",
title: "독서 노트 작성하기",
completedDate: "2025.01.16",
attachment: {
type: "file",
url: "/reading-note.pdf",
name: "독서노트_데미안.pdf",
},
},
{
id: "3",
title: "독서 후기 SNS 게시",
completedDate: "2025.01.17",
},
],
},
{
id: "2",
title: "독서 리스트 작성",
completedTodos: [
{
id: "4",
title: "올해 읽을 책 10권 선정",
completedDate: "2024.12.31",
attachment: {
type: "file",
url: "/book-list.pdf",
name: "2025년_독서목록.pdf",
},
},
],
},
],
},
},
};
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Make story data deterministic and verify local asset paths

  • Consider using a deterministic placeholder image to avoid flaky network calls in Storybook (e.g., seed-based picsum).
  • Local PDF paths may 404 in Storybook unless served from public/static. Verify their presence or switch to a known-accessible URL.
  • Also, standardize completedDate to ISO 8601.

[suggest_nitpick]

Apply this diff:

-              completedDate: "2025.01.15",
+              completedDate: "2025-01-15",
-              attachment: {
-                type: "image",
-                url: "https://picsum.photos/200",
-              },
+              attachment: {
+                type: "image",
+                url: "https://picsum.photos/seed/done-item/200",
+              },
-              completedDate: "2025.01.16",
+              completedDate: "2025-01-16",
-              completedDate: "2024.12.31",
+              completedDate: "2024-12-31",

Run this script to confirm the static files actually exist (adjust if they live under a different path like public/ or static/):


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Searching for referenced PDFs..."
fd -t f -i 'reading-note.pdf' -uu || true
fd -t f -i 'book-list.pdf' -uu || true

echo "If no results above, consider placing PDFs under public/ and referencing them as /reading-note.pdf or using a remote URL."

Length of output: 412


Ensure deterministic assets and accessible PDFs in Storybook

  • The placeholder image URL should be seed-based to avoid flaky network calls.
  • Standardize all completedDate values to ISO 8601 (YYYY-MM-DD).
  • The script confirmed that reading-note.pdf and book-list.pdf are not in the repo. Place them under public/ (or static/) so Storybook can serve them at /reading-note.pdf and /book-list.pdf, or switch to a publicly hosted URL.

Apply these changes:

-              completedDate: "2025.01.15",
+              completedDate: "2025-01-15",
-              attachment: {
-                type: "image",
-                url: "https://picsum.photos/200",
-              },
+              attachment: {
+                type: "image",
+                url: "https://picsum.photos/seed/done-item/200",
+              },
-              completedDate: "2025.01.16",
+              completedDate: "2025-01-16",
-              completedDate: "2025.01.17",
+              completedDate: "2025-01-17",
-              completedDate: "2024.12.31",
+              completedDate: "2024-12-31",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
args: {
goalDetail: {
id: "1",
title: "독서 습관 만들기",
subGoals: [
{
id: "1",
title: "매일 30분 책 읽기",
completedTodos: [
{
id: "1",
title: "소설 '데미안' 읽기",
completedDate: "2025.01.15",
attachment: {
type: "image",
url: "https://picsum.photos/200",
},
},
{
id: "2",
title: "독서 노트 작성하기",
completedDate: "2025.01.16",
attachment: {
type: "file",
url: "/reading-note.pdf",
name: "독서노트_데미안.pdf",
},
},
{
id: "3",
title: "독서 후기 SNS 게시",
completedDate: "2025.01.17",
},
],
},
{
id: "2",
title: "독서 리스트 작성",
completedTodos: [
{
id: "4",
title: "올해 읽을 책 10권 선정",
completedDate: "2024.12.31",
attachment: {
type: "file",
url: "/book-list.pdf",
name: "2025년_독서목록.pdf",
},
},
],
},
],
},
},
};
args: {
goalDetail: {
id: "1",
title: "독서 습관 만들기",
subGoals: [
{
id: "1",
title: "매일 30분 책 읽기",
completedTodos: [
{
id: "1",
title: "소설 '데미안' 읽기",
completedDate: "2025-01-15",
attachment: {
type: "image",
url: "https://picsum.photos/seed/done-item/200",
},
},
{
id: "2",
title: "독서 노트 작성하기",
completedDate: "2025-01-16",
attachment: {
type: "file",
url: "/reading-note.pdf",
name: "독서노트_데미안.pdf",
},
},
{
id: "3",
title: "독서 후기 SNS 게시",
completedDate: "2025-01-17",
},
],
},
{
id: "2",
title: "독서 리스트 작성",
completedTodos: [
{
id: "4",
title: "올해 읽을 책 10권 선정",
completedDate: "2024-12-31",
attachment: {
type: "file",
url: "/book-list.pdf",
name: "2025년_독서목록.pdf",
},
},
],
},
],
},
},
};
🤖 Prompt for AI Agents
In components/mypage/DoneItemDetail.stories.tsx around lines 50 to 104, make
assets and dates deterministic and accessible: replace the placeholder image URL
with a seeded one (e.g. use a picsum seed URL like
https://picsum.photos/seed/reading/200), convert all completedDate values to ISO
8601 format (YYYY-MM-DD), and ensure the two PDF attachments are actually served
by Storybook by moving reading-note.pdf and book-list.pdf into the repo’s
public/ (or static/) directory so they are reachable at /reading-note.pdf and
/book-list.pdf (or update the attachment URLs to point to stable public URLs).

Copy link
Collaborator

@devgony devgony left a comment

Choose a reason for hiding this comment

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

뭔가 의도치 않게 todo input 이 남아있게 되는거같아요
저도 왜 발생하는지 더 찾아볼게요
image
image

Copy link

@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: 5

♻️ Duplicate comments (5)
components/details/Modals/ModalAddingTodo/ModalAddingTodo.tsx (1)

78-90: Make form submission async, trim input, await onAddTodo, and reset state

Prevents duplicate submits and ignores whitespace-only entries. Mirrors prior feedback.

-      onSubmit={(e) => {
-        // 새로고침 방지
-        e.preventDefault();
-        if (!todo) {
-          setError(true);
-          return;
-        }
-        setError(false);
-        onAddTodo(todo, date);
-      }}
+      onSubmit={async (e) => {
+        e.preventDefault();
+        const value = todo.trim();
+        if (!value) {
+          setError(true);
+          return;
+        }
+        setError(false);
+        try {
+          await onAddTodo(value, date);
+          setTodo("");
+        } catch (err) {
+          // TODO: surface error to user (toast or TextField isError)
+          console.error(err);
+        }
+      }}
app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (4)

79-82: Guard against NaN D‑Day when due date is missing

Use a null fallback and handle it in rendering to avoid “D+NaN”. This mirrors prior feedback.

-  const leftDay = goalDetail?.dueDate?.dueDate
-    ? calcLeftDay(goalDetail?.dueDate.dueDate)
-    : NaN;
+  const leftDay =
+    goalDetail?.dueDate?.dueDate !== undefined
+      ? calcLeftDay(goalDetail.dueDate.dueDate)
+      : null;

204-209: Render D‑Day safely when leftDay is null

Avoid rendering D+NaN by handling the null case explicitly.

-            <p className="justify-center text-label-primary text-base font-bold font-['SUIT_Variable'] leading-tight">{`D${leftDay >= 0 ? "-" : "+"}${leftDay !== 0 ? Math.abs(leftDay) : "Day"}`}</p>
+            <p className="justify-center text-label-primary text-base font-bold font-['SUIT_Variable'] leading-tight">
+              {leftDay === null
+                ? "-"
+                : `D${leftDay === 0 ? "-Day" : `${leftDay > 0 ? "-" : "+"}${Math.abs(leftDay)}`}`}
+            </p>

134-193: Bottom sheet effect: remove unconditional cleanup close; include stable deps

The cleanup currently closes the sheet on any dependency change, causing flicker. This was previously flagged; apply the same fix here.

   useEffect(() => {
     const isRendered = checkRendered();

     if (!shouldBottomSheetOpened && isRendered) {
       closeBottomSheet();
       return;
     }

     const bottomSheetInfo: Parameters<typeof openBottomSheet>[0] = {
       backdropProps: {
         onClick: () => {
           // 내용물을 초기화 해야 함. -> key값 바꿔도 애니메이션이나 바텀시트 높이 정상적일까?

           setIsActive(false);
           updateBottomSheet((prev) => {
             return { ...prev, hasBackdrop: false };
           });
         },
         className: "fixed inset-0 bg-black/20 z-20",
       },
       ContentComponent: TodoBottomSheet,
       contentProps: {
         isActivated: isActive,
         initTodoInfo: initContent,
         setIsActivated: setIsActive,
         subGoals:
           data.subGoals?.map((subGoalInfo) => ({
             id: subGoalInfo.subGoalId ?? "",
             title: subGoalInfo.subGoal ?? "",
           })) ?? [],
         onSubmitTodo: async (newTodoInfo) => {
           const afterSubmit = () => {
             mutate();
             // 바텀시트 리셋
             setIsActive(false);
           };
           const res = await handleTodoBottomSheetSubmit(
             newTodoInfo,
             afterSubmit,
           );
           return res;
         },
       },

       hasBackdrop: isActive,
       bottomSheetFixerStyle: { bottom: "0px" },
     };

     if (shouldBottomSheetOpened && !isRendered) {
       openBottomSheet(bottomSheetInfo);
       return;
     }

     updateBottomSheet(bottomSheetInfo);

-    return () => {
-      closeBottomSheet();
-    };
-  }, [isActive, initContent, data.subGoals?.length, shouldBottomSheetOpened]);
+  }, [
+    isActive,
+    initContent,
+    data.subGoals?.length,
+    shouldBottomSheetOpened,
+    checkRendered,
+    openBottomSheet,
+    updateBottomSheet,
+    closeBottomSheet,
+  ]);

If you want to close on unmount only, add a separate useEffect(() => closeBottomSheet, []).


165-173: Refetch the correct sub-goal after submit

mutate() here targets the list bound to data.subGoals[targetSubGoalIdx] (idx defaults to 0). Submissions for other sub-goals won’t refresh their lists.

Option 1 (React Query): invalidate by subGoalId.

// Pseudocode: inject queryClient and invalidate by key containing subGoalId
const res = await handleTodoBottomSheetSubmit(newTodoInfo, async () => {
  await queryClient.invalidateQueries({
    queryKey: ["subGoalTodosAllInfinite", { subGoalId: newTodoInfo.subGoalId }],
  });
  setIsActive(false);
});

Option 2 (SWR): expose an invalidator in your queries module or use a scoped mutate keyed by subGoalId.

// e.g., invalidateSubGoalTodos(newTodoInfo.subGoalId);
🧹 Nitpick comments (11)
components/mypage/MyPage.tsx (2)

60-66: Logout flow: remove stale setIsGuest(false) comment and ensure guest state is handled in store

The comment suggests toggling guest to false on logout, which is likely the opposite of what you'd want. Either rely on logout() to set guest mode appropriately or explicitly set guest=true if needed. Remove the misleading comment to avoid confusion.

Apply this small cleanup:

-            // setIsGuest(false);

And verify that logout() transitions the store to guest mode (or a well-defined logged-out state) so the MyPage gating works as expected.


89-117: Remove legacy comment and gate profile fetching in guest mode

  • Delete the stale conditional comment at components/mypage/MyPage.tsx:89:

    -        {/* {isLoggedIn ? ( */}
  • Avoid calling useMyProfile() when isGuest is true to prevent unnecessary API requests. You can choose one of the following approaches:

    1. Extend the hook to accept an enabled flag
      In api/hooks.ts, update useMyProfile to:

      export const useMyProfile = (
        config?: SWRConfiguration,
        enabled = true,
      ) => useApiQuery(
        "사용자Api",
        "getMyProfile",
        enabled ? [] : null,    // pass `null` to skip fetch
        undefined,
        config,
      );

      Then in your component:

      const { data: user, isLoading, error } = useMyProfile(undefined, !isGuest);
    2. Conditionally invoke the hook in-line

      const guestProfile = { data: undefined, isLoading: false, error: undefined };
      const { data: user, isLoading, error } = !isGuest
        ? useMyProfile()
        : guestProfile;
components/shared/Modal/ModalCompletingGoal/ModalCompletingGoal.tsx (1)

36-41: Await completion to prevent double submit/race on “목표 달성!”

If a user taps quickly, multiple requests can fire. Await the handler to serialize submissions.

Apply:

-            onClick={() => {
-              onCompleteGoal();
-            }}
+            onClick={async () => {
+              await onCompleteGoal();
+            }}

Optionally introduce a local loading flag to disable the button while pending if Modal.Button supports disabled or loading.

components/main/GoalData/GoalData.tsx (2)

33-33: Clamp progress bar width to [0, 100]%

Prevent visual overflow for out-of-bound values.

-              style={{ width: `${progress.toFixed(2)}%` }}
+              style={{ width: `${Math.min(100, Math.max(0, progress))}%` }}

37-37: Conditional decimal display is fine; consider using the clamped value

Small consistency nit: display the same clamped value you render in the bar (optionally with two decimals for 0<progress<100).

Example:

-            {`${progress > 0 && progress < 100 ? progress.toFixed(2) : progress}%`}
+            {(() => {
+              const p = Math.min(100, Math.max(0, progress));
+              return `${p > 0 && p < 100 ? p.toFixed(2) : p}%`;
+            })()}
components/details/Modals/ModalAddingTodo/ModalAddingTodo.tsx (2)

72-75: Fix function name typo: handleDateCLick → handleDateClick

Minor readability/consistency improvement.

-  const handleDateCLick = () => {
+  const handleDateClick = () => {
     if (!ref.current) return;
     ref.current.showPicker();
   };
-          onClick={handleDateCLick}
+          onClick={handleDateClick}

Also applies to: 103-108


46-55: Optional: disable “추가하기” while pending

If Modal.Button supports disabled or loading, wire a local pending flag in ModalBody via React Context/prop lift to block duplicate clicks.

components/details/ListCard/ListCard.tsx (3)

101-151: Avoid bottom-sheet flicker: remove cleanup close and include stable deps

The effect’s cleanup closes on every dependency change, which can cause close/reopen jank. Also include the hook functions in the deps to align with React rules (assuming they’re stable, this is safe).

-  useEffect(() => {
+  useEffect(() => {
     const isRendered = checkRendered();
     if (!openBottomSheet && isRendered) {
       closeBottomSheet();
       return;
     }
     const bottomSheetInfo: Parameters<typeof openTodoRootBottomSheet>[0] = {
       bottomSheetFixerStyle: { bottom: "0px" },
       backdropProps: {
         onClick: () => {
           // 내용물을 초기화 해야 함. -> key값 바꿔도 애니메이션이나 바텀시트 높이 정상적일까?

           closeBottomSheet();
         },
         className: "fixed inset-0 bg-black/10 z-20",
       },
       hasBackdrop: false,
       ContentComponent: TodoResultBottomSheet,
       contentProps: {
         onSubmit: async (todoResult) => {
           if (todoIdForResult && todoResult.emotion !== null) {
             const res = await postTodoResult(
               todoIdForResult,
               todoResult.emotion as TodoResultRqEmotionEnum,
               todoResult.memo,
               todoResult.file || undefined,
             );

             if (!res) return;
             setOpenBottomSheet(false);
             mutate();
           }
         },
         openBottomSheet: openBottomSheet,
         setOpenBottomSheet: setOpenBottomSheet,
       },
     };
     const isOpened = checkOpened();
     if (openBottomSheet && !isOpened) {
       openTodoRootBottomSheet(bottomSheetInfo);
       return;
     }

     if (isRendered) updateTodoResultBottomSheet(bottomSheetInfo);
-
-    return () => {
-      if (isOpened && !openBottomSheet) {
-        closeBottomSheet();
-      }
-    };
-  }, [openBottomSheet, todoIdForResult]);
+  }, [
+    openBottomSheet,
+    todoIdForResult,
+    checkRendered,
+    checkOpened,
+    openTodoRootBottomSheet,
+    updateTodoResultBottomSheet,
+    closeBottomSheet,
+  ]);

296-301: Await the onTodoCheck promise to serialize toggles

Prevents overlapping toggles when tapped rapidly.

                   onChecked={async () => {
-                    onTodoCheck && onTodoCheck(todoInfo.id);
+                    if (onTodoCheck) {
+                      await onTodoCheck(todoInfo.id);
+                    }
                   }}

248-269: Add error handling to Todo creation flow

Surface failures and avoid silent no-ops; optionally guard against rapid double tap.

               onClick={() => {
                 openModal(
                   <ModalAddingTodo
                     onAddTodo={async (newTodo, date) => {
                       if (!subGoalInfo.id) return;

-                      const res = await subGoalApi.createTodo(subGoalInfo.id, {
-                        title: newTodo,
-                        date: date
-                          ? date2StringWithSpliter(date, "-")
-                          : undefined,
-                      });
-
-                      if (res) {
-                        closeModal();
-                        mutate();
-                      }
+                      try {
+                        const res = await subGoalApi.createTodo(subGoalInfo.id, {
+                          title: newTodo,
+                          date: date ? date2StringWithSpliter(date, "-") : undefined,
+                        });
+                        if (res) {
+                          closeModal();
+                          mutate();
+                        } else {
+                          // TODO: toast error
+                          console.error("Failed to create todo");
+                        }
+                      } catch (e) {
+                        // TODO: toast error
+                        console.error(e);
+                      }
                     }}
                     onClose={closeModal}
                   />,
                 );
               }}
app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (1)

214-219: Clamp and format progress values for consistency

Prevent overflow and match main/GoalData formatting.

-                style={{ width: `${goalDetail?.progress ?? 0}%` }}
+                style={{
+                  width: `${Math.min(100, Math.max(0, goalDetail?.progress ?? 0))}%`,
+                }}
-              {`${goalDetail?.progress ?? 0}%`}
+              {(() => {
+                const p = Math.min(100, Math.max(0, goalDetail?.progress ?? 0));
+                return `${p > 0 && p < 100 ? p.toFixed(2) : p}%`;
+              })()}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between efee88a and b78646d.

📒 Files selected for processing (7)
  • app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (8 hunks)
  • components/details/ListCard/ListCard.tsx (7 hunks)
  • components/details/Modals/ModalAddingTodo/ModalAddingTodo.tsx (1 hunks)
  • components/main/GoalData/GoalData.tsx (2 hunks)
  • components/main/GoalDataContainer/GoalDataContainer.tsx (1 hunks)
  • components/mypage/MyPage.tsx (3 hunks)
  • components/shared/Modal/ModalCompletingGoal/ModalCompletingGoal.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/main/GoalDataContainer/GoalDataContainer.tsx
🧰 Additional context used
📓 Path-based instructions (3)
components/**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/global-rule.mdc)

For every styling, use tailwind css.

Files:

  • components/shared/Modal/ModalCompletingGoal/ModalCompletingGoal.tsx
  • components/main/GoalData/GoalData.tsx
  • components/mypage/MyPage.tsx
  • components/details/Modals/ModalAddingTodo/ModalAddingTodo.tsx
  • components/details/ListCard/ListCard.tsx
{components/**/*.{js,jsx,ts,tsx},**/*.css}

📄 CodeRabbit Inference Engine (.cursor/rules/global-rule.mdc)

For tokens, use tokens.css (mdc:app/tokens.css).

Files:

  • components/shared/Modal/ModalCompletingGoal/ModalCompletingGoal.tsx
  • components/main/GoalData/GoalData.tsx
  • components/mypage/MyPage.tsx
  • components/details/Modals/ModalAddingTodo/ModalAddingTodo.tsx
  • components/details/ListCard/ListCard.tsx
components/**/*

📄 CodeRabbit Inference Engine (.cursor/rules/global-rule.mdc)

Component should be created in components/

Files:

  • components/shared/Modal/ModalCompletingGoal/ModalCompletingGoal.tsx
  • components/main/GoalData/GoalData.tsx
  • components/mypage/MyPage.tsx
  • components/details/Modals/ModalAddingTodo/ModalAddingTodo.tsx
  • components/details/ListCard/ListCard.tsx
🧬 Code Graph Analysis (3)
app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (4)
utils/calcLeftDay.ts (1)
  • calcLeftDay (1-12)
api/service.ts (2)
  • subGoalApi (90-90)
  • todoApi (88-88)
components/shared/BottomSheets/TodoBottomSheet/TodoBottomSheet.tsx (2)
  • TodoBottomSheetProps (255-255)
  • TodoInfoForSubmission (255-255)
utils/date2String.ts (1)
  • date2StringWithSpliter (7-7)
components/details/Modals/ModalAddingTodo/ModalAddingTodo.tsx (1)
utils/date2String.ts (1)
  • date2StringWithSpliter (7-7)
components/details/ListCard/ListCard.tsx (4)
types/todoList.ts (1)
  • TodoItemsInfo (7-7)
lib/fetching/postTodoResult.ts (1)
  • postTodoResult (26-26)
api/service.ts (1)
  • subGoalApi (90-90)
utils/date2String.ts (1)
  • date2StringWithSpliter (7-7)
⏰ 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). (1)
  • GitHub Check: chromatic-test
🔇 Additional comments (2)
components/shared/Modal/ModalCompletingGoal/ModalCompletingGoal.tsx (1)

59-65: Copy updates LGTM

The revised heading/subtitle read well and match the intent.

components/main/GoalData/GoalData.tsx (1)

16-16: D‑Day formatting LGTM

The sign-aware D-Day string avoids D--N and D-0 edge cases.

Comment on lines +24 to +32
import TodoBottomSheet, {
TodoBottomSheetProps,
} from "@/components/shared/BottomSheets/TodoBottomSheet/TodoBottomSheet";
import useActiveTodoBottomSheet from "@/stores/useActiveTodoBottomSheet";
import { date2StringWithSpliter } from "@/utils/date2String";
import useToast from "@/hooks/useToast";
import useBottomSheet from "@/hooks/useBottomSheet";
import { TodoInfoForSubmission } from "@/components/shared/BottomSheets/TodoBottomSheet/TodoBottomSheet";

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm date2StringWithSpliter accepts Date|string

In this file you pass newTodoInfo.date which may be Date | string by type. Ensure the helper handles both.


🏁 Script executed:

#!/bin/bash
# Verify the function signature and usage across the repo
rg -nC2 'export\s+const\s+date2StringWithSpliter' utils/date2String.ts
rg -nC2 'date2StringWithSpliter\(' --type=ts,tsx

Length of output: 154


🏁 Script executed:

#!/bin/bash
# Locate the file(s) defining or referencing date2StringWithSpliter in TS/TSX
rg -n 'date2StringWithSpliter' -g '*.ts' -g '*.tsx' -C2

# Show the helper’s export/definition with context
rg -n 'export\s\+const\s\+date2StringWithSpliter' -g '*.ts' -g '*.tsx' -C3

# Show all call sites to verify argument types
rg -n 'date2StringWithSpliter\(' -g '*.ts' -g '*.tsx' -C2

Length of output: 18313


Update date2StringWithSpliter to accept string inputs
The helper in utils/date2String.ts is currently typed as

const date2StringWithSpliter = (dateValue: Date, spliter: string) => {  }

but in DetailBody.tsx (and other components) you pass newTodoInfo.date which is typed Date | string. This mismatch will throw at runtime if a string is passed.

Please address by either:

  • Expanding the helper signature to (dateValue: Date | string, spliter: string) and normalizing with new Date(dateValue) when it’s a string.
  • Or ensuring all callers convert string dates to Date before invoking the helper.

Key locations:

  • utils/date2String.ts:1 – function signature
  • app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx:352 – date2StringWithSpliter(newTodoInfo?.date, "-")
🤖 Prompt for AI Agents
In app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx around line 352
and utils/date2String.ts at line 1, the helper date2StringWithSpliter is
currently typed to accept Date but callers (e.g. newTodoInfo.date) can be Date |
string; update the helper signature to (dateValue: Date | string, spliter:
string) and inside normalize the input by creating a Date from string inputs
(e.g. const d = typeof dateValue === "string" ? new Date(dateValue) :
dateValue), then use d for formatting; also update exported types/annotations so
callers no longer need to pre-convert, and add a small guard for invalid dates
(NaN) to return an empty string or a sensible default.

Comment on lines +89 to +105
const subGoalCompletingFeftches = data.subGoals?.map(
(subGoalInfo) => {
return subGoalApi.subGoalCompleteToggle(
subGoalInfo?.subGoalId ?? "",
);
},
);

const subGoalCompletion = await Promise.all(
subGoalCompletingFeftches ?? [],
).then(
(resList) => resList.filter((res) => res).length === resList.length,
);
if (!subGoalCompletion) {
setToast("세부 목표 달성에 실패했습니다!");
return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don’t toggle already-completed sub-goals; only complete the incomplete ones

Calling a “toggle” API for every sub-goal will flip already-completed items back to incomplete. Filter to incomplete sub-goals only.

-          const subGoalCompletingFeftches = data.subGoals?.map(
-            (subGoalInfo) => {
-              return subGoalApi.subGoalCompleteToggle(
-                subGoalInfo?.subGoalId ?? "",
-              );
-            },
-          );
+          const subGoalCompletingFeftches = data.subGoals
+            ?.filter((sg) => !sg.isCompleted)
+            .map((sg) =>
+              subGoalApi.subGoalCompleteToggle(sg?.subGoalId ?? ""),
+            );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const subGoalCompletingFeftches = data.subGoals?.map(
(subGoalInfo) => {
return subGoalApi.subGoalCompleteToggle(
subGoalInfo?.subGoalId ?? "",
);
},
);
const subGoalCompletion = await Promise.all(
subGoalCompletingFeftches ?? [],
).then(
(resList) => resList.filter((res) => res).length === resList.length,
);
if (!subGoalCompletion) {
setToast("세부 목표 달성에 실패했습니다!");
return;
}
const subGoalCompletingFeftches = data.subGoals
?.filter((sg) => !sg.isCompleted)
.map((sg) =>
subGoalApi.subGoalCompleteToggle(sg?.subGoalId ?? ""),
);
const subGoalCompletion = await Promise.all(
subGoalCompletingFeftches ?? [],
).then(
(resList) => resList.filter((res) => res).length === resList.length,
);
if (!subGoalCompletion) {
setToast("세부 목표 달성에 실패했습니다!");
return;
}
🤖 Prompt for AI Agents
In app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx around lines 89
to 105, the code currently calls a toggle API for every sub-goal which will flip
already-completed items back to incomplete; change it to first filter subGoals
to only those that are not completed (e.g., subGoalInfo.completed === false or
equivalent flag), then map that filtered list to the sub-goal completion API
(use the explicit complete endpoint if available rather than a toggle), await
Promise.all on that filtered list (handle the case when there are no incomplete
sub-goals by skipping the request and treating as success), and determine
overall success by checking each response; if any request fails show the
existing toast and return.

export function MyPage({ className = "" }: MyPageProps) {
const router = useSafeRouter();
const { isLoggedIn, login, logout } = useAuthStore();
const { isLoggedIn, login, logout, isGuest } = useAuthStore();
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Unify auth gating: use a single source of truth (isGuest) across UI and actions

You gate rendering with !isGuest but compute navigationItems from isLoggedIn. This can drift if the two flags ever get out of sync. Prefer consistently deriving UI/logic from isGuest (or from a single derived flag like isAuthed = !isGuest) for clarity and future-proofing.
[suggest_optional_refactor]

To verify store invariants and ensure logout/login keep these flags in sync, run:

If isGuest is the canonical flag, update navigationItems to use !isGuest instead of isLoggedIn for consistency.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate useAuthStore implementation(s)"
fd -t f -i 'useAuthStore.*' | tee /tmp/auth_store_files

echo
echo "Inspect isGuest/isLoggedIn and login/logout behaviors"
while read -r f; do
  echo ">>> $f"
  rg -n -C3 -P '\bisGuest\b|\bisLoggedIn\b|logout\s*\(|login\s*\(' "$f" || true
done </tmp/auth_store_files

echo
echo "Find other components gating on isGuest vs isLoggedIn"
rg -n -C2 -P '\bisGuest\b' --type=ts --type=tsx || true
rg -n -C2 -P '\bisLoggedIn\b' --type=ts --type=tsx || true

Length of output: 256


Let’s simplify the search and directly locate the store definition and all uses of these flags:

This will verify:

  • How useAuthStore is implemented.
  • Whether isGuest and isLoggedIn are strictly derived from one another.
  • Where else in .ts/.tsx we gate UI or logic on these flags.

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1. Files that reference useAuthStore:"
rg -l --type=ts --type=tsx 'useAuthStore' || true

echo
echo "2. Inspecting useAuthStore definitions and related methods:"
rg -n 'useAuthStore' -C3 --type=ts --type=tsx || true

echo
echo "3. In each candidate file, show isGuest, isLoggedIn, login, logout:"
for file in $(rg -l --type=ts --type=tsx 'useAuthStore'); do
  echo ">>> $file"
  rg -n -C3 '\b(isGuest|isLoggedIn|login|logout)\b' --type=ts --type=tsx "$file" || true
done

echo
echo "4. Components gating on isGuest:"
rg -n -C2 '\bisGuest\b' --type=ts --type=tsx || true

echo
echo "5. Components gating on isLoggedIn:"
rg -n -C2 '\bisLoggedIn\b' --type=ts --type=tsx || true

Length of output: 962


Let's try without relying on built-in file-type filters:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1. Files referencing useAuthStore:"
rg -l 'useAuthStore' -g '*.ts' -g '*.tsx' || true

echo
echo "2. Inspecting useAuthStore definitions and related methods:"
rg -n 'useAuthStore' -C3 -g '*.ts' -g '*.tsx' || true

echo
echo "3. In each candidate file, show isGuest, isLoggedIn, login, logout:"
for file in $(rg -l 'useAuthStore' -g '*.ts' -g '*.tsx'); do
  echo ">>> $file"
  rg -n -C3 '\b(isGuest|isLoggedIn|login|logout)\b' "$file" || true
done

echo
echo "4. Components gating on isGuest:"
rg -n '\bisGuest\b' -g '*.ts' -g '*.tsx' || true

echo
echo "5. Components gating on isLoggedIn:"
rg -n '\bisLoggedIn\b' -g '*.ts' -g '*.tsx' || true

Length of output: 27599


Unify auth gating: pick a single flag for UI and logic

Currently, login()/logout() only toggle isLoggedInisGuest never changes — so gating on !isGuest (render) but deriving nav items from isLoggedIn can drift and break the UI.

• components/mypage/MyPage.tsx
– Line 30: const { isLoggedIn, login, logout, isGuest } = useAuthStore()
– Line 37: const navigationItems = isLoggedIn ? […] : […]
– Line 90: {!isGuest ? (…) : (…)}

Suggested fix (choose one):

  1. Standardize on isLoggedIn:
    – Replace {!isGuest} with {isLoggedIn} for rendering
    – Compute nav items and all gating from isLoggedIn only
  2. Standardize on isGuest:
    – Update login() to also set({ isGuest: false })
    – Update logout() to also set({ isGuest: true })
    – Compute nav items from !isGuest everywhere
🤖 Prompt for AI Agents
In components/mypage/MyPage.tsx around lines 30, 37 and 90, the component mixes
isLoggedIn and isGuest causing UI drift; standardize on isLoggedIn: keep using
const { isLoggedIn, login, logout } = useAuthStore() (remove reliance on
isGuest), change the navigationItems computation to use isLoggedIn (already
does), replace the render conditional at line 90 from {!isGuest ? … : …} to
{isLoggedIn ? … : …}, and remove or stop reading isGuest anywhere in this file
(or add a TODO to remove from the store if unused).

Comment on lines +27 to +34
// <Modal.Button
// onClick={() => {
// onWait();
// }}
// key={"wait"}
// text="잠깐만요!"
// color="alternative"
// />,
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Remove dead onWait prop or re-enable the “잠깐만요!” action

You’ve commented out the button that used to call onWait, but the prop is still part of the component API and is passed by callers. This is dead API surface and can confuse future maintainers.

  • Option A: Remove onWait from the props and all call sites.
  • Option B: Re-enable the button intentionally.

[suggest_optional_refactor]

Run this to find all onWait usages before removal:

If you choose removal, here’s a minimal diff for this file (you’ll need to update call sites too):

-interface ModalCompletingGoalProps extends ModalCommon {
-  onWait: () => void;
-  onCompleteGoal: () => Promise<void>;
-}
+interface ModalCompletingGoalProps extends ModalCommon {
+  onCompleteGoal: () => Promise<void>;
+}

 const ModalCompletingGoal = ({
   onClose,
-  onWait,
   onCompleteGoal,
 }: ModalCompletingGoalProps) => {

🏁 Script executed:

#!/bin/bash
rg -nP --type=ts,tsx -C2 '\bonWait\b'

Length of output: 70


🏁 Script executed:

#!/bin/bash
# Search for all usages of `onWait` in .ts and .tsx files
rg -nP -C2 '\bonWait\b' -g '*.ts' -g '*.tsx'

Length of output: 2965


Remove unused onWait API or restore the “잠깐만요!” button

You’ve commented out the “잠깐만요!” action but left the onWait prop in the component signature and callers—this creates dead API surface and unused locals. Address it in one of two ways:

Option A: Remove onWait everywhere

  • In ModalCompletingGoal.tsx
    • Delete onWait from ModalCompletingGoalProps
    • Remove onWait from the destructuring
    • Delete the commented-out <Modal.Button> block
  • In call sites
    • app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx – remove onWait={closeModal}
    • components/shared/Modal/ModalCompletingGoal/ModalCompletingGoal.stories.tsx – drop the onWait argType and stub

Option B: Re-enable the button

  • Uncomment lines 27–34 in ModalCompletingGoal.tsx so that onWait() is actually called

Choose one approach to keep the API consistent.

🤖 Prompt for AI Agents
In components/shared/Modal/ModalCompletingGoal/ModalCompletingGoal.tsx around
lines 27 to 34, the component still exposes an unused onWait prop while the
“잠깐만요!” button is commented out; remove the dead API by deleting onWait from
ModalCompletingGoalProps and from the props destructuring, and remove the
commented-out Modal.Button block; then update call sites: remove
onWait={closeModal} from
app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx and drop the onWait
argType/stub from
components/shared/Modal/ModalCompletingGoal/ModalCompletingGoal.stories.tsx so
the prop is fully removed everywhere.

<h3 className="justify-center text-label-alternative text-base font-bold font-['SUIT_Variable'] leading-tight">
남은 기간
</h3>
<p className="justify-center text-label-primary text-base font-bold font-['SUIT_Variable'] leading-tight">{`D${leftDay >= 0 ? "-" : "+"}${leftDay !== 0 ? Math.abs(leftDay) : "Day"}`}</p>
Copy link
Collaborator

Choose a reason for hiding this comment

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

게스트 모드에서 보면 NaN 이 출력되는거 같아요
image

Copy link
Collaborator

Choose a reason for hiding this comment

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

로딩되는 동안에는 ""0 로 로 처리하거나 하는게 낫지않을까 싶습니당

Comment on lines +31 to +34
const bottomSheet = document.querySelector(
// "#fixer",
"#bottom-sheet",
) as HTMLDivElement;
Copy link
Collaborator

Choose a reason for hiding this comment

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

useRef 로 변경할 수 있을 것 같아요

const bottomSheetRef = useRef<HTMLDivElement>(null);
..
const handleMobileKeyboardResize = () => {
  const bottomSheet = bottomSheetRef.current;
..
            <Drawer.Content id="bottom-sheet" ref={bottomSheetRef}>

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이거 처음엔 useRef로 했는데, 이러니까 동작 처리가 느리더라구요...
일단 id로 처리하고 추후에 아예 통으로 변경할까 합니답...

Copy link
Collaborator

Choose a reason for hiding this comment

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

좋습니당

Copy link

@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

🔭 Outside diff range comments (1)
app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (1)

45-47: Fix incorrect todo refetch: bind mutate per sub-goal

Currently, you call

const { mutate } = useSubGoalTodosAllInfinite(
  data.subGoals?.[targetSubGoalIdx]?.subGoalId ?? "",
);

once for the initial sub-goal index, but then render all sub-goals in a map. As a result, every onTodoCheck invokes mutate() only for that first sub-goal, leaving other lists stale.

To resolve this critical bug:

• Remove the targetSubGoalIdx state and the top-level useSubGoalTodosAllInfinite call (lines 42–47).
• Inside the data.subGoals?.map block, instantiate the hook for each subGoalInfo.subGoalId.
• Call that per-sub-goal mutate in onTodoCheck.

Example diff in DetailBody.tsx:

-  const [targetSubGoalIdx, setTargetSubGoalIdx] = useState(0);
-  const { mutate } = useSubGoalTodosAllInfinite(
-    data.subGoals?.[targetSubGoalIdx]?.subGoalId ?? "",
-  );
+  // Removed: one-off hook. Instantiate per sub-goal below.

   …
   {data.subGoals?.map((subGoalInfo) => {
+    const { mutate: mutateTodos } = useSubGoalTodosAllInfinite(subGoalInfo.subGoalId);
     return (
       <ListCard
         onTodoCheck={async (todoId) => {
           const res = await todoApi.toggleTodoCompletion(todoId);
           if (res) {
-            mutate();
+            mutateTodos();
             mutateForSubgoalCompleted();
           }
         }}
         key={subGoalInfo.subGoalId}
         …
       />
     );
   })}

This ensures each sub-goal’s todo list is re-fetched correctly when its check state changes.

♻️ Duplicate comments (6)
app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (6)

122-193: Avoid closing the bottom sheet on any dependency change (if you re-enable this effect)

The cleanup that calls closeBottomSheet will cause close+reopen flicker on every dependency change. Close explicitly when desired or on unmount only; also include stable function deps.

Suggested diff when re-enabling:

-  }, [isActive, initContent, data.subGoals?.length, shouldBottomSheetOpened]);
+  }, [
+    isActive,
+    initContent,
+    data.subGoals?.length,
+    shouldBottomSheetOpened,
+    checkRendered,
+    openBottomSheet,
+    updateBottomSheet,
+    closeBottomSheet,
+  ]);
-    return () => {
-      closeBottomSheet();
-    };
+    // If you want to close on unmount only:
+    // useEffect(() => closeBottomSheet, []);

378-406: Let afterSubmit know which sub-goal was affected; adjust signature and call

Pass the submitted info so the caller can invalidate the correct sub-goal’s list. Also ensures future wiring from the bottom sheet invalidates by subGoalId.

Apply this diff:

-const handleTodoBottomSheetSubmit: (
-  newTodoInfo: TodoInfoForSubmission,
-  afterSubmit: () => void,
-) => Promise<boolean> = async (newTodoInfo, afterSubmit: () => void) => {
+const handleTodoBottomSheetSubmit: (
+  newTodoInfo: TodoInfoForSubmission,
+  afterSubmit: (submitted: TodoInfoForSubmission) => void,
+) => Promise<boolean> = async (
+  newTodoInfo,
+  afterSubmit: (submitted: TodoInfoForSubmission) => void,
+) => {
@@
-  if (isFetchOk) {
-    afterSubmit();
+  if (isFetchOk) {
+    afterSubmit(newTodoInfo);
   }

Then, at the (currently commented) call site, update afterSubmit to accept the payload and invalidate using newTodoInfo.subGoalId before closing the sheet.


387-395: Ensure date2StringWithSpliter can accept Date | string inputs

newTodoInfo.date is likely typed as Date | string. Confirm the helper’s signature and normalize string inputs (e.g., new Date(value)) inside the helper to avoid runtime errors.

#!/bin/bash
# Locate helper signature and call sites
fd date2String.ts
rg -nC2 'export\s+const\s+date2StringWithSpliter' $(fd date2String.ts)
rg -nC2 'date2StringWithSpliter\(' -g '*.ts' -g '*.tsx'

If currently typed as (date: Date, ...), update it to (date: Date | string, ...) and guard invalid dates.


79-82: Prevent D+NaN and show “D‑Day” for zero; render “-” when due date missing

When dueDate is missing you render NaN; also handle D‑Day explicitly for leftDay === 0. This also addresses the guest-mode NaN report.

Apply this diff:

-  const leftDay = goalDetail?.dueDate?.dueDate
-    ? calcLeftDay(goalDetail?.dueDate.dueDate)
-    : NaN;
+  const leftDay = goalDetail?.dueDate?.dueDate
+    ? calcLeftDay(goalDetail?.dueDate.dueDate)
+    : null;
-            <p className="justify-center text-label-primary text-base font-bold font-['SUIT_Variable'] leading-tight">{`D${leftDay >= 0 ? "-" : "+"}${leftDay !== 0 ? Math.abs(leftDay) : "Day"}`}</p>
+            <p className="justify-center text-label-primary text-base font-bold font-['SUIT_Variable'] leading-tight">
+              {leftDay === null
+                ? "-"
+                : `D${leftDay === 0 ? "-Day" : `${leftDay > 0 ? "-" : "+"}${Math.abs(leftDay)}`}`}
+            </p>

Also applies to: 205-209


89-95: Don’t toggle already-completed sub-goals; only complete the incomplete ones

Toggling every sub-goal will flip already-completed items back to incomplete.

Apply this diff:

-          const subGoalCompletingFeftches = data.subGoals?.map(
-            (subGoalInfo) => {
-              return subGoalApi.subGoalCompleteToggle(
-                subGoalInfo?.subGoalId ?? "",
-              );
-            },
-          );
+          const subGoalCompletingFeftches = data.subGoals
+            ?.filter((sg) => !sg.isCompleted)
+            .map((sg) =>
+              subGoalApi.subGoalCompleteToggle(sg?.subGoalId ?? ""),
+            );

53-62: Fix false-positive “all todos completed” when there are zero todos

Current reduce-equality yields true for empty lists (0 === 0), which will trigger the completion modal even when there are no todos.

Apply this diff:

-  const allTodoCompleted =
-    data.subGoals &&
-    data.subGoals.reduce((acc, cur) => {
-      const count =
-        cur.initTodoItemsInfo?.filter((info) => info.checked).length ?? 0;
-      return count + acc;
-    }, 0) ===
-      data.subGoals.reduce((acc, cur) => {
-        return acc + cur.initTodoTotalLen;
-      }, 0);
+  const totalTodos =
+    data.subGoals?.reduce((acc, cur) => acc + (cur.initTodoTotalLen ?? 0), 0) ?? 0;
+  const completedTodos =
+    data.subGoals?.reduce(
+      (acc, cur) => acc + (cur.initTodoItemsInfo?.filter((i) => i.checked).length ?? 0),
+      0,
+    ) ?? 0;
+  const allTodoCompleted = totalTodos > 0 && completedTodos === totalTodos;
🧹 Nitpick comments (1)
app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (1)

214-219: Use the rounded fixedProgress consistently in UI

You compute fixedProgress above but render goalDetail.progress directly. Use fixedProgress for consistent display.

-                style={{ width: `${goalDetail?.progress ?? 0}%` }}
+                style={{ width: `${fixedProgress}%` }}
...
-              {`${goalDetail?.progress ?? 0}%`}
+              {`${fixedProgress}%`}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b78646d and 8543b55.

📒 Files selected for processing (1)
  • app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (8 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx (4)
utils/calcLeftDay.ts (1)
  • calcLeftDay (1-12)
api/service.ts (2)
  • subGoalApi (90-90)
  • todoApi (88-88)
components/shared/BottomSheets/TodoBottomSheet/TodoBottomSheet.tsx (1)
  • TodoInfoForSubmission (255-255)
utils/date2String.ts (1)
  • date2StringWithSpliter (7-7)
⏰ 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). (1)
  • GitHub Check: chromatic-test

Comment on lines +275 to +295
{data.subGoals?.map((subGoalInfo) => (
<ListCard
onTodoCheck={async (todoId) => {
const res = await todoApi.toggleTodoCompletion(todoId);
if (res) {
mutate();
mutateForSubgoalCompleted();
}
}}
key={subGoalInfo.subGoalId}
initTodoInfoList={subGoalInfo?.initTodoItemsInfo}
subGoalInfo={{
id: subGoalInfo?.subGoalId,
// idx: targetSubGoalIdx,
name: subGoalInfo?.subGoal,
// totalSubGoalsLen: data.subGoals?.length ?? 0,
isCompleted: subGoalInfo?.isCompleted,
}}
/>
))}
{/* <ListCard
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Invalidate todos for the correct sub-goal and add stable key fallback

  • Refetch the toggled sub-goal’s todos via query invalidation by subGoalId (not the static mutate bound to index 0).
  • Add a fallback to the list key to avoid undefined keys at runtime.

Apply this diff:

-          {data.subGoals?.map((subGoalInfo) => (
+          {data.subGoals?.map((subGoalInfo, idx) => (
             <ListCard
               onTodoCheck={async (todoId) => {
                 const res = await todoApi.toggleTodoCompletion(todoId);
                 if (res) {
-                  mutate();
+                  // Refetch the toggled sub-goal's todos only
+                  await queryClient.invalidateQueries({
+                    queryKey: [
+                      "subGoalTodosAllInfinite",
+                      { subGoalId: subGoalInfo?.subGoalId ?? "" },
+                    ],
+                  });
                   mutateForSubgoalCompleted();
                 }
               }}
-              key={subGoalInfo.subGoalId}
+              key={subGoalInfo?.subGoalId ?? `subgoal-${idx}`}

Additional changes required outside this hunk:

  • Import and initialize the query client near the top of the component.

TypeScript (outside selected lines):

// import
import { useQueryClient } from "@tanstack/react-query";

// inside DetailBody component, near other hooks
const queryClient = useQueryClient();

새로고침 막고, mutate처리 및 뒤로가기 정상화
Copy link

@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: 0

♻️ Duplicate comments (1)
app/adding-goal/page.tsx (1)

40-85: Fix useEffect dependencies and lifecycle; always close on cleanup

Currently missing deps will trigger ESLint and may cause stale closures. Also, you build bottomSheetInfo even when closed and only conditionally close on cleanup. Early-return when closed, unify open/update, and include all external deps. Also fix the isOpended typo.

Apply this diff:

-  useEffect(() => {
-    if (!openBottomSheet) {
-      closeBottomSheet();
-    }
-
-    const bottomSheetInfo: Parameters<typeof openGoalDurationBottomSheet>[0] = {
+  useEffect(() => {
+    if (!openBottomSheet) {
+      closeBottomSheet();
+      return;
+    }
+
+    const bottomSheetInfo: Parameters<typeof openGoalDurationBottomSheet>[0] = {
       backdropProps: {
         className: "fixed inset-0 z-20 bg-neutral-700/50 ",
         onClick: () => {
           setOpenBottomSheet(false);
         },
       },
       ContentComponent: GoalDurationBottomSheet,
       hasBackdrop: true,
       bottomSheetFixerStyle: {
         bottom: 0,
       },
       contentProps: {
         setopenBottomSheet: setOpenBottomSheet,
         onEdit: async ({ type, value }) => {
           setGoalAddInfo((prev) => ({
             ...prev,
             durationType: type,
             durationValue: value,
           }));
           // 닫기
           setOpenBottomSheet(false);
         },
       },
     };
-    const isOpended = checkOpened();
-    if (openBottomSheet && !isOpended) {
-      openGoalDurationBottomSheet(bottomSheetInfo);
-      return;
-    }
-
-    if (openBottomSheet) {
-      updateGoalDurationBottomSheet(bottomSheetInfo);
-    }
-
-    return () => {
-      if (!openBottomSheet) {
-        closeBottomSheet();
-      }
-    };
-  }, [openBottomSheet]);
+    const isOpened = checkOpened();
+    if (!isOpened) {
+      openGoalDurationBottomSheet(bottomSheetInfo);
+    } else {
+      updateGoalDurationBottomSheet(bottomSheetInfo);
+    }
+
+    return () => {
+      // Always close to prevent a lingering sheet on unmount/navigation
+      closeBottomSheet();
+    };
+  }, [
+    openBottomSheet,
+    checkOpened,
+    closeBottomSheet,
+    openGoalDurationBottomSheet,
+    updateGoalDurationBottomSheet,
+  ]);
🧹 Nitpick comments (4)
app/adding-goal/page.tsx (4)

33-38: Remove unused checkRendered to satisfy no-unused-vars

checkRendered is destructured but never used, likely tripping ESLint.

Apply this diff:

   const {
     checkOpened,
-    checkRendered,
     closeBottomSheet,
     openBottomSheet: openGoalDurationBottomSheet,
     updateBottomSheet: updateGoalDurationBottomSheet,
   } = useBottomSheet<Parameters<typeof GoalDurationBottomSheet>[0]>();

99-123: Add pending state and error handling to prevent duplicate submissions

Rapid taps can fire multiple creates; also no error handling/UI feedback. Guard with an isSubmitting flag and wrap with try/catch/finally.

Apply this diff within onSubmit:

-        onSubmit={async (e) => {
-          e.preventDefault();
-          const res = await goalApi.createGoal({
+        onSubmit={async (e) => {
+          e.preventDefault();
+          if (isSubmitting) return;
+          setIsSubmitting(true);
+          try {
+            const res = await goalApi.createGoal({
               isPeriodByMonth: goalAddInfo.durationType === "month",
               title: goalAddInfo.goal,
               dueDate:
                 goalAddInfo.durationType === "month"
                   ? undefined
                   : date2StringWithSpliter(
                       goalAddInfo.durationValue as Date,
                       "-",
                     ),
               month:
                 goalAddInfo.durationType === "month"
                   ? (goalAddInfo.durationValue as number)
                   : undefined,
               subGoals: goalAddInfo.subGoals.map((subGoalName) => ({
                 title: subGoalName,
               })),
             });
-          if (res) {
-            mutate();
-            router.back();
-          }
+            if (res) {
+              // If using React Query: await refetch(); otherwise mutate();
+              mutate?.();
+              router.back();
+            }
+          } catch (err) {
+            // TODO: surface error (toast/snackbar)
+            console.error(err);
+          } finally {
+            setIsSubmitting(false);
+          }
         }}

And add this state near your other useState calls:

const [isSubmitting, setIsSubmitting] = useState(false);

229-236: Disable the submit button while submitting for better UX

Also reflect disabled styling when isSubmitting is true.

Apply this diff:

-          disabled={!goalAddInfo.goal}
-          className={`w-82 h-14 px-6 py-4 ${!goalAddInfo.goal ? "bg-background-disabled" : "bg-background-strong"}  rounded-[999px] inline-flex justify-center items-center gap-2 overflow-hidden`}
+          disabled={!goalAddInfo.goal || isSubmitting}
+          className={`w-82 h-14 px-6 py-4 ${!goalAddInfo.goal || isSubmitting ? "bg-background-disabled" : "bg-background-strong"}  rounded-[999px] inline-flex justify-center items-center gap-2 overflow-hidden`}

And the label color:

-            className={`flex-1 text-center justify-center ${!goalAddInfo.goal ? "text-label-disabled" : "text-label-inverse"}   text-xl font-bold font-['SUIT'] leading-normal`}
+            className={`flex-1 text-center justify-center ${!goalAddInfo.goal || isSubmitting ? "text-label-disabled" : "text-label-inverse"}   text-xl font-bold font-['SUIT'] leading-normal`}

240-252: Remove commented-out legacy JSX

The old inline <GoalDurationBottomSheet /> block is commented out. Dead code adds noise and can drift.

Apply this diff to delete it:

-      {/* <GoalDurationBottomSheet
-        openBottomSheet={openBottomSheet}
-        setopenBottomSheet={setOpenBottomSheet}
-        onEdit={async ({ type, value }) => {
-          setGoalAddInfo((prev) => ({
-            ...prev,
-            durationType: type,
-            durationValue: value,
-          }));
-          // 닫기
-          setOpenBottomSheet(false);
-        }}
-      /> */}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8543b55 and e750173.

📒 Files selected for processing (1)
  • app/adding-goal/page.tsx (6 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/adding-goal/page.tsx (1)
api/hooks.ts (1)
  • useGoals (174-174)
⏰ 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). (1)
  • GitHub Check: chromatic-test
🔇 Additional comments (2)
app/adding-goal/page.tsx (2)

31-39: Nice adoption of the centralized bottom-sheet API with strong typing

Good use of useBottomSheet<Parameters<typeof GoalDurationBottomSheet>[0]>() and aliasing openBottomSheet to openGoalDurationBottomSheet to avoid name collision with local state. This improves clarity and type-safety.


29-30: No change needed—useGoals is SWR-based and returns mutate

The useGoals hook is an alias for useQuery.goals, which calls useApiQuery with an SWRConfiguration and under the hood uses SWR’s useSWR. SWR hooks expose mutate (not refetch), so keeping const { mutate } = useGoals() is correct.

Copy link
Collaborator

@devgony devgony left a comment

Choose a reason for hiding this comment

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

👍🏼 👍🏼 👍🏼

@Hys-Lee Hys-Lee merged commit 0242dbc into prography:main Aug 19, 2025
3 of 4 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Aug 20, 2025
13 tasks
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.

2 participants