Repository: Quesddo/Client
Pull request #164: feat(TODO-220): 목표상세 무한스크롤 기능
Author: login: yo-ong
Labels: name: ✨ feat
name: 🎨 style
name: ✂ refactor
Created: 2025-03-07T06:56:01Z | Merged: 2025-03-08T13:36:28Z

🔗 연관된 이슈
[연관된 이슈 링크](https://quesddo.atlassian.net/browse/TODO-220)

📗 작업 내용
- 목표상세 무한스크롤 기능 추가
- todo list, done list 분리
- 레이아웃 수정
- progress 계산방식에서 api로 변경
  - 할일 추가를 했을 때 할일의 totalCount 수가 변경되어 api방식이 나은것으로 판단


💬 리뷰 요구사항(선택)
- 무한스크롤의 기능 동작은 하는데 원하는 동작인지는 모르겠네요.
- 데이터 전처리는 다음 PR에 진행할 예정입니다.


Diff:
diff --git a/src/contexts/GoalDetailContext.tsx b/src/contexts/GoalDetailContext.tsx
index 2e659372..100dfc35 100644
--- a/src/contexts/GoalDetailContext.tsx
+++ b/src/contexts/GoalDetailContext.tsx
@@ -1,11 +1,8 @@
-import { createContext, ReactNode, useContext, useState } from "react";
+import { createContext, ReactNode, useContext } from "react";
 
 interface GoalDetailContextProps {
   goalId: number;
-  progress: number;
-  updateProgress: (doneCount: number, totalcount: number) => void;
 }
-
 const GoalDetailContext = createContext<GoalDetailContextProps | null>(null);
 
 export const GoalDetailProvider = ({
@@ -15,19 +12,7 @@ export const GoalDetailProvider = ({
   children: ReactNode;
   goalId: number;
 }) => {
-  const [progress, setProgress] = useState<number>(0);
-
-  const updateProgress = (doneCount: number, totalCount: number) => {
-    if (totalCount > 0) {
-      setProgress(Math.round((doneCount / totalCount) * 100));
-    } else {
-      setProgress(0);
-    }
-  };
-
   const values: GoalDetailContextProps = {
-    progress,
-    updateProgress,
     goalId,
   };
 
diff --git a/src/hooks/goal/useDeleteGoal.ts b/src/hooks/goal/useDeleteGoal.ts
index 7f42c832..7ee352e8 100644
--- a/src/hooks/goal/useDeleteGoal.ts
+++ b/src/hooks/goal/useDeleteGoal.ts
@@ -3,16 +3,22 @@ import { useRouter } from "next/router";
 
 import instance from "@/apis/apiClient";
 
+import useToast from "../useToast";
+
 export const useDeleteGoal = (goalId?: number) => {
   const router = useRouter();
   const queryClient = useQueryClient();
+  const { addToast } = useToast();
   return useMutation({
     mutationFn: async () => {
       const { data } = await instance.delete(`/goals/${goalId}`);
-
       return data;
     },
     onSuccess: () => {
+      addToast({
+        variant: "error",
+        content: "목표가 삭제되었습니다.",
+      });
       queryClient.invalidateQueries({ queryKey: ["goals"] });
       router.push("/dashboard");
     },
diff --git a/src/hooks/todo/useInfiniteTodo.ts b/src/hooks/todo/useInfiniteTodo.ts
index e3f84f74..fd7fdf7b 100644
--- a/src/hooks/todo/useInfiniteTodo.ts
+++ b/src/hooks/todo/useInfiniteTodo.ts
@@ -7,18 +7,37 @@ import {
   TodoResponseDto,
 } from "@/types/types";
 
-export const useInfiniteTodo = () => {
+type FilterType = "todo" | "done";
+
+interface UseInfiniteTodoProsp {
+  goalId?: number;
+  filter?: FilterType;
+  size: number;
+}
+
+const createTodoParams = (
+  pageParam: unknown,
+  props?: UseInfiniteTodoProsp,
+): teamIdTodosGetParams => {
+  const filterMap = { todo: false, done: true };
+  const done = props?.filter ? filterMap[props.filter] : undefined;
+  return {
+    cursor: pageParam as number,
+    size: props?.size ?? 40,
+    goalId: props?.goalId,
+    done,
+  };
+};
+
+export const useInfiniteTodo = (props?: UseInfiniteTodoProsp) => {
   return useSuspenseInfiniteQuery<
     TeamIdTodosGet200Response,
     Error,
     { todos: TodoResponseDto[]; totalCount: number }
   >({
-    queryKey: ["todos", "infinite"],
+    queryKey: ["todos", "infinite", props?.goalId, props?.filter],
     queryFn: async ({ pageParam = 0 }) => {
-      const params: teamIdTodosGetParams = {
-        cursor: pageParam as number,
-        size: 40,
-      };
+      const params = createTodoParams(pageParam, props);
       const response = await instance.get("todos", { params });
       return response.data;
     },
diff --git a/src/pages/goal/[goalId]/index.tsx b/src/pages/goal/[goalId]/index.tsx
index aed8d971..de8e7ce4 100644
--- a/src/pages/goal/[goalId]/index.tsx
+++ b/src/pages/goal/[goalId]/index.tsx
@@ -13,20 +13,20 @@ export default function GoalDetailPage() {
 
   return (
     <GoalDetailProvider goalId={goalId}>
-      <div>
-        <Head>
-          <title>{goalId}</title>
-        </Head>
-      </div>
-      <div className="flex h-full flex-col gap-[16px] bg-slate-100 px-[16px] pt-[64px] sm:pt-[24px] sm:pr-[24px] sm:pl-[84px] md:px-[360px]">
-        {/* 목표 */}
-        <PageTitle title="목표" isMobileFixed={true} />
-        {/* 목표 이름,진행률 */}
-        <GoalHeader />
-        {/* 노트 모아보기 */}
-        <GoalNote />
-        {/* 투두 리스트 */}
-        <GoalTodoContainer />
+      <Head>
+        <title>{goalId}</title>
+      </Head>
+      <div className="box-border h-full bg-slate-100 px-[16px] pt-[64px] sm:pt-[24px] sm:pr-[24px] sm:pl-[84px] md:pl-[360px]">
+        <div className="flex max-w-[1200px] flex-col gap-[16px]">
+          {/* 목표 */}
+          <PageTitle title="목표" isMobileFixed={true} />
+          {/* 목표 이름,진행률 */}
+          <GoalHeader />
+          {/* 노트 모아보기 */}
+          <GoalNote />
+          {/* 투두 리스트 */}
+          <GoalTodoContainer />
+        </div>
       </div>
     </GoalDetailProvider>
   );
diff --git a/src/views/goal/header/Progress.tsx b/src/views/goal/header/Progress.tsx
index de4732a8..69b8b8b1 100644
--- a/src/views/goal/header/Progress.tsx
+++ b/src/views/goal/header/Progress.tsx
@@ -1,7 +1,8 @@
 import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
-import { useEffect } from "react";
+import { useEffect, useMemo } from "react";
 
 import { useGoalDetailContext } from "@/contexts/GoalDetailContext";
+import useProgressTodo from "@/hooks/todo/useProgressTodo";
 
 import ProgressBar from "../../../components/atoms/progress-bar/ProgressBar";
 
@@ -32,7 +33,9 @@ export default function Progress({
   label = "Progress",
   showLabel = true,
 }: ProgressBarProps) {
-  const { progress } = useGoalDetailContext();
+  const { goalId } = useGoalDetailContext();
+  const { data } = useProgressTodo(goalId);
+  const progress = useMemo(() => data?.progress ?? 0, [data]);
 
   return (
     <>
@@ -41,6 +44,7 @@ export default function Progress({
           {label}
         </p>
       )}
+
       <div className="flex items-center justify-between whitespace-nowrap">
         <ProgressBar progress={progress} />
         <p className="inline-block text-xs font-semibold">
diff --git a/src/views/goal/todo/GoalDoneList.tsx b/src/views/goal/todo/GoalDoneList.tsx
index ff763093..03a11b01 100644
--- a/src/views/goal/todo/GoalDoneList.tsx
+++ b/src/views/goal/todo/GoalDoneList.tsx
@@ -1,27 +1,57 @@
+import { useEffect } from "react";
+import { useInView } from "react-intersection-observer";
+
+import TodoList from "@/components/organisms/todo-list/TodoList";
+import { useGoalDetailContext } from "@/contexts/GoalDetailContext";
+import { useInfiniteTodo } from "@/hooks/todo/useInfiniteTodo";
+
 import Section from "../component/Section";
 
-export default function GoalDoneList() {
+interface GoalDoneListProps {
+  handleToggleTodo: (todoId: number, isDone: boolean) => void;
+  setSelectedTodoId: (todoId: number | null) => void;
+  onOpenDeletePopup: () => void;
+}
+
+export default function GoalDoneList({
+  handleToggleTodo,
+  setSelectedTodoId,
+  onOpenDeletePopup,
+}: GoalDoneListProps) {
+  const { goalId } = useGoalDetailContext();
+  const { data, fetchNextPage, hasNextPage } = useInfiniteTodo({
+    goalId,
+    filter: "done",
+    size: 20,
+  });
+  const { todos, totalCount } = data;
+  const { ref, inView } = useInView();
+
+  useEffect(() => {
+    if (inView && hasNextPage) fetchNextPage();
+  }, [inView]);
+
   return (
     <Section className="flex-1 bg-slate-200 hover:shadow">
       <div className="mb-[16px] flex justify-between">
         <p className="text-lg font-bold">done</p>
       </div>
       <div className="h-[168px] overflow-x-hidden overflow-y-auto pr-4 md:h-[512px]">
-        <div className="flex h-full items-center justify-center text-sm font-normal text-slate-500">
-          다 한 일이 아직 없어요
-        </div>
-        {/* {dones?.length ? (
-          <TodoList
-            data={dones}
-            handleToggleTodo={handleToggleTodo}
-            setSelectedTodoId={setSelectedTodoId}
-            onOpenDeletePopup={() => setIsPopupOpen(true)}
-          />
+        {totalCount ? (
+          <>
+            <TodoList
+              data={todos}
+              handleToggleTodo={handleToggleTodo}
+              setSelectedTodoId={setSelectedTodoId}
+              onOpenDeletePopup={onOpenDeletePopup}
+            />
+            <div ref={ref}></div>
+          </>
         ) : (
           <div className="flex h-full items-center justify-center text-sm font-normal text-slate-500">
             다 한 일이 아직 없어요
           </div>
-        )} */}
+        )}
       </div>
     </Section>
   );
diff --git a/src/views/goal/todo/GoalTodoContainer.tsx b/src/views/goal/todo/GoalTodoContainer.tsx
index 0684a212..7fe7cc01 100644
--- a/src/views/goal/todo/GoalTodoContainer.tsx
+++ b/src/views/goal/todo/GoalTodoContainer.tsx
@@ -7,9 +7,9 @@ import DeletePopup from "@/views/todo/popup/DeletePopup";
 import TodoCreateForm from "@/views/todo/todo-create-form/TodoCreateForm";
 import TodoUpdateForm from "@/views/todo/todo-update-form/TodoUpdateForm";
 
-import Section from "../component/Section";
 import GoalDoneList from "./GoalDoneList";
 import GoalTodoList from "./GoalTodoList";
+import Section from "../component/Section";
 
 export default function GoalTodoContainer() {
   /* todolist */
@@ -42,7 +42,11 @@ export default function GoalTodoContainer() {
         onOpenDeletePopup={onOpenDeletePopup}
       />
       {/* done list */}
-      <GoalDoneList />
+      <GoalDoneList
+        handleToggleTodo={handleToggleTodo}
+        setSelectedTodoId={setSelectedTodoId}
+        onOpenDeletePopup={onOpenDeletePopup}
+      />
 
       {modalType === "createTodo" && <TodoCreateForm />}
       {modalType === "updateTodo" && selectedTodoId && (
diff --git a/src/views/goal/todo/GoalTodoList.tsx b/src/views/goal/todo/GoalTodoList.tsx
index 844cb372..73a18533 100644
--- a/src/views/goal/todo/GoalTodoList.tsx
+++ b/src/views/goal/todo/GoalTodoList.tsx
@@ -1,7 +1,10 @@
+import { useEffect } from "react";
+import { useInView } from "react-intersection-observer";
+
 import PlusIcon from "@/components/atoms/plus-icon/PlusIcon";
 import TodoList from "@/components/organisms/todo-list/TodoList";
 import { useGoalDetailContext } from "@/contexts/GoalDetailContext";
-import { useTodos } from "@/hooks/todo/useTodos";
+import { useInfiniteTodo } from "@/hooks/todo/useInfiniteTodo";
 
 import Section from "../component/Section";
 
@@ -19,9 +22,17 @@ export default function GoalTodoList({
   onOpenDeletePopup,
 }: GoalTodoListProps) {
   const { goalId } = useGoalDetailContext();
+  const { data, fetchNextPage, hasNextPage } = useInfiniteTodo({
+    goalId,
+    filter: "todo",
+    size: 20,
+  });
+  const { todos, totalCount } = data;
+  const { ref, inView } = useInView();
 
-  const { data } = useTodos(goalId);
-  const todos = data.todos;
+  useEffect(() => {
+    if (inView && hasNextPage) fetchNextPage();
+  }, [inView, hasNextPage]);
 
   return (
     <Section className="flex-1 bg-white hover:shadow">
@@ -36,13 +47,16 @@ export default function GoalTodoList({
         </button>
       </div>
       <div className="h-[168px] overflow-x-hidden overflow-y-auto pr-4 md:h-[512px]">
-        {todos?.length ? (
-          <TodoList
-            data={todos}
-            handleToggleTodo={handleToggleTodo}
-            setSelectedTodoId={setSelectedTodoId}
-            onOpenDeletePopup={onOpenDeletePopup}
-          />
+        {totalCount ? (
+          <>
+            <TodoList
+              data={todos}
+              handleToggleTodo={handleToggleTodo}
+              setSelectedTodoId={setSelectedTodoId}
+              onOpenDeletePopup={onOpenDeletePopup}
+            />
+            <div ref={ref}></div>
+          </>
         ) : (
           <div className="flex h-full items-center justify-center text-sm font-normal text-slate-500">
             해야할 일이 아직 없어요


Reviews:
user: login: hdayeon
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T07:36:54Z
user: login: euNung24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T07:39:17Z
user: login: yo-ong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T09:07:09Z
user: login: yo-ong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T09:08:26Z
user: login: yo-ong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T09:10:10Z
user: login: yo-ong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T09:14:30Z
user: login: thgee
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-07T09:52:22Z
user: login: hdayeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T09:53:32Z
user: login: yunsung-hodoopapa
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-07T12:51:09Z

Review comments:
diff_hunk: @@ -32,7 +33,13 @@ export default function Progress({
   label = "Progress",
   showLabel = true,
 }: ProgressBarProps) {
-  const { progress } = useGoalDetailContext();
+  const { goalId } = useGoalDetailContext();
+  const { data } = useProgressTodo(goalId);
+  const [progress, setProgress] = useState(0);
+
+  useEffect(() => {
+    setProgress(data.progress ?? 0);
+  }, [data]);
path: src/views/goal/header/Progress.tsx
user: login: euNung24
body: 이거 useEffect 사용없이 useMemo 사용해도 좋을 것 같아요!
`
useMemo(() => data.progress ?? 0, [data])
`
created_at: 2025-03-07T07:33:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,17 +7,33 @@ import {
   TodoResponseDto,
 } from "@/types/types";
 
-export const useInfiniteTodo = () => {
+type FilterType = "todo" | "done";
+
+export const useInfiniteTodo = (
+  goalId?: number,
+  filter?: FilterType,
+  size = 40,
+) => {
   return useSuspenseInfiniteQuery<
     TeamIdTodosGet200Response,
     Error,
     { todos: TodoResponseDto[]; totalCount: number }
   >({
-    queryKey: ["todos", "infinite"],
+    queryKey: ["todos", "infinite", goalId, filter],
     queryFn: async ({ pageParam = 0 }) => {
+      let done;
+      if (filter) {
+        if (filter === "todo") {
+          done = false;
+        } else {
+          done = true;
+        }
+      }
path: src/hooks/todo/useInfiniteTodo.ts
user: login: euNung24
body: early return 문이랑 삼항연산자 사용하면 좀 더 깔끔해보일 것 같아욥
created_at: 2025-03-07T07:34:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,9 +23,22 @@ export default function GoalTodoList({
   onOpenDeletePopup,
 }: GoalTodoListProps) {
   const { goalId } = useGoalDetailContext();
+  const { data, fetchNextPage, hasNextPage } = useInfiniteTodo(
+    goalId,
+    "todo",
+    20,
+  );
+  const { todos, totalCount } = data;
+  const { ref, inView } = useInView();
+  const queryClient = useQueryClient();
+
+  useEffect(() => {
+    if (inView && hasNextPage) fetchNextPage();
+  }, [inView]);
path: src/views/goal/todo/GoalTodoList.tsx
user: login: euNung24
body: hasNextPage는 의존성 배열에 안넣어도 되나요?
created_at: 2025-03-07T07:36:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,17 +7,33 @@ import {
   TodoResponseDto,
 } from "@/types/types";
 
-export const useInfiniteTodo = () => {
+type FilterType = "todo" | "done";
+
+export const useInfiniteTodo = (
+  goalId?: number,
+  filter?: FilterType,
+  size = 40,
+) => {
   return useSuspenseInfiniteQuery<
     TeamIdTodosGet200Response,
     Error,
     { todos: TodoResponseDto[]; totalCount: number }
   >({
-    queryKey: ["todos", "infinite"],
+    queryKey: ["todos", "infinite", goalId, filter],
     queryFn: async ({ pageParam = 0 }) => {
+      let done;
+      if (filter) {
+        if (filter === "todo") {
+          done = false;
+        } else {
+          done = true;
+        }
+      }
path: src/hooks/todo/useInfiniteTodo.ts
user: login: hdayeon
body: 보기 쉽게 객체 매핑해도 좋을 것 같아요! const filterMap = { todo: false, done: true } 이런식으루
created_at: 2025-03-07T07:36:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,17 +7,33 @@ import {
   TodoResponseDto,
 } from "@/types/types";
 
-export const useInfiniteTodo = () => {
+type FilterType = "todo" | "done";
+
+export const useInfiniteTodo = (
+  goalId?: number,
+  filter?: FilterType,
+  size = 40,
+) => {
   return useSuspenseInfiniteQuery<
     TeamIdTodosGet200Response,
     Error,
     { todos: TodoResponseDto[]; totalCount: number }
   >({
-    queryKey: ["todos", "infinite"],
+    queryKey: ["todos", "infinite", goalId, filter],
     queryFn: async ({ pageParam = 0 }) => {
+      let done;
+      if (filter) {
+        if (filter === "todo") {
+          done = false;
+        } else {
+          done = true;
+        }
+      }
path: src/hooks/todo/useInfiniteTodo.ts
user: login: yo-ong
body: 오오 그러면
```ts
const filterMap = { todo: false, done: true }
const done = filterMap[filter]
```
이렇게 쓰면 될까요?!
created_at: 2025-03-07T09:07:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,7 +33,13 @@ export default function Progress({
   label = "Progress",
   showLabel = true,
 }: ProgressBarProps) {
-  const { progress } = useGoalDetailContext();
+  const { goalId } = useGoalDetailContext();
+  const { data } = useProgressTodo(goalId);
+  const [progress, setProgress] = useState(0);
+
+  useEffect(() => {
+    setProgress(data.progress ?? 0);
+  }, [data]);
path: src/views/goal/header/Progress.tsx
user: login: yo-ong
body: 넵 useMemo도 참고하겠습니당
created_at: 2025-03-07T09:08:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,17 +7,33 @@ import {
   TodoResponseDto,
 } from "@/types/types";
 
-export const useInfiniteTodo = () => {
+type FilterType = "todo" | "done";
+
+export const useInfiniteTodo = (
+  goalId?: number,
+  filter?: FilterType,
+  size = 40,
+) => {
   return useSuspenseInfiniteQuery<
     TeamIdTodosGet200Response,
     Error,
     { todos: TodoResponseDto[]; totalCount: number }
   >({
-    queryKey: ["todos", "infinite"],
+    queryKey: ["todos", "infinite", goalId, filter],
     queryFn: async ({ pageParam = 0 }) => {
+      let done;
+      if (filter) {
+        if (filter === "todo") {
+          done = false;
+        } else {
+          done = true;
+        }
+      }
path: src/hooks/todo/useInfiniteTodo.ts
user: login: yo-ong
body: undefined도 사용해야 하다보니 이렇게 했습니당
created_at: 2025-03-07T09:10:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,9 +23,22 @@ export default function GoalTodoList({
   onOpenDeletePopup,
 }: GoalTodoListProps) {
   const { goalId } = useGoalDetailContext();
+  const { data, fetchNextPage, hasNextPage } = useInfiniteTodo(
+    goalId,
+    "todo",
+    20,
+  );
+  const { todos, totalCount } = data;
+  const { ref, inView } = useInView();
+  const queryClient = useQueryClient();
+
+  useEffect(() => {
+    if (inView && hasNextPage) fetchNextPage();
+  }, [inView]);
path: src/views/goal/todo/GoalTodoList.tsx
user: login: yo-ong
body: 포함시켜야 겠어요 ㄷㄷ
created_at: 2025-03-07T09:14:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,17 +7,33 @@ import {
   TodoResponseDto,
 } from "@/types/types";
 
-export const useInfiniteTodo = () => {
+type FilterType = "todo" | "done";
+
+export const useInfiniteTodo = (
+  goalId?: number,
+  filter?: FilterType,
+  size = 40,
+) => {
   return useSuspenseInfiniteQuery<
     TeamIdTodosGet200Response,
     Error,
     { todos: TodoResponseDto[]; totalCount: number }
   >({
-    queryKey: ["todos", "infinite"],
+    queryKey: ["todos", "infinite", goalId, filter],
     queryFn: async ({ pageParam = 0 }) => {
+      let done;
+      if (filter) {
+        if (filter === "todo") {
+          done = false;
+        } else {
+          done = true;
+        }
+      }
path: src/hooks/todo/useInfiniteTodo.ts
user: login: hdayeon
body: undefined 잡으려면 filter ? filterMap[filter] : undefined 이렇게? 해야할듯 하네요!
created_at: 2025-03-07T09:53:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,17 +7,33 @@ import {
   TodoResponseDto,
 } from "@/types/types";
 
-export const useInfiniteTodo = () => {
+type FilterType = "todo" | "done";
+
+export const useInfiniteTodo = (
+  goalId?: number,
+  filter?: FilterType,
+  size = 40,
+) => {
   return useSuspenseInfiniteQuery<
     TeamIdTodosGet200Response,
     Error,
     { todos: TodoResponseDto[]; totalCount: number }
   >({
-    queryKey: ["todos", "infinite"],
+    queryKey: ["todos", "infinite", goalId, filter],
     queryFn: async ({ pageParam = 0 }) => {
+      let done;
path: src/hooks/todo/useInfiniteTodo.ts
user: login: yunsung-hodoopapa
body: 함수가 전혀 순수하지 않습니다. 
쿼리 펑션 자체는 필터값 그자체로 인해서 프로미스를 리턴하게 끔 만들어주시고,
필터를 넘길때 지정된 값을 전달하도록 구현을 해주세요. 
created_at: 2025-03-07T12:39:59Z
author_association: NONE
diff_hunk: @@ -7,17 +7,33 @@ import {
   TodoResponseDto,
 } from "@/types/types";
 
-export const useInfiniteTodo = () => {
+type FilterType = "todo" | "done";
+
+export const useInfiniteTodo = (
+  goalId?: number,
+  filter?: FilterType,
+  size = 40,
+) => {
   return useSuspenseInfiniteQuery<
     TeamIdTodosGet200Response,
     Error,
     { todos: TodoResponseDto[]; totalCount: number }
   >({
-    queryKey: ["todos", "infinite"],
+    queryKey: ["todos", "infinite", goalId, filter],
     queryFn: async ({ pageParam = 0 }) => {
+      let done;
+      if (filter) {
+        if (filter === "todo") {
+          done = false;
+        } else {
+          done = true;
+        }
+      }
       const params: teamIdTodosGetParams = {
         cursor: pageParam as number,
-        size: 40,
+        size: size,
path: src/hooks/todo/useInfiniteTodo.ts
user: login: yunsung-hodoopapa
body: 객체의 키가 값이 같으면 생략할 수 있습니다. 

size, 
created_at: 2025-03-07T12:41:07Z
author_association: NONE
diff_hunk: @@ -32,7 +33,13 @@ export default function Progress({
   label = "Progress",
   showLabel = true,
 }: ProgressBarProps) {
-  const { progress } = useGoalDetailContext();
+  const { goalId } = useGoalDetailContext();
+  const { data } = useProgressTodo(goalId);
+  const [progress, setProgress] = useState(0);
+
+  useEffect(() => {
+    setProgress(data.progress ?? 0);
path: src/views/goal/header/Progress.tsx
user: login: yunsung-hodoopapa
body: if (data && data.progress) {
 setProgress(data.progress)
}
created_at: 2025-03-07T12:41:58Z
author_association: NONE
diff_hunk: @@ -1,27 +1,63 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { useEffect } from "react";
+import { useInView } from "react-intersection-observer";
+
+import TodoList from "@/components/organisms/todo-list/TodoList";
+import { useGoalDetailContext } from "@/contexts/GoalDetailContext";
+import { useInfiniteTodo } from "@/hooks/todo/useInfiniteTodo";
+
 import Section from "../component/Section";
 
-export default function GoalDoneList() {
+interface GoalDoneListProps {
+  handleToggleTodo: (todoId: number, isDone: boolean) => void;
+  setSelectedTodoId: (todoId: number | null) => void;
+  onOpenDeletePopup: () => void;
+}
+
+export default function GoalDoneList({
+  handleToggleTodo,
+  setSelectedTodoId,
+  onOpenDeletePopup,
+}: GoalDoneListProps) {
+  const { goalId } = useGoalDetailContext();
+  const { data, fetchNextPage, hasNextPage } = useInfiniteTodo(
path: src/views/goal/todo/GoalDoneList.tsx
user: login: yunsung-hodoopapa
body: 파라미터가 1개 베스트, 
2개는 쏘쏘 
3개 이상부터는 죄악이 됩니다. 
2개를 초과한 파라미터를 전달해줄때는 객체를 사용해주세요. 
created_at: 2025-03-07T12:45:45Z
author_association: NONE
diff_hunk: @@ -1,27 +1,63 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { useEffect } from "react";
+import { useInView } from "react-intersection-observer";
+
+import TodoList from "@/components/organisms/todo-list/TodoList";
+import { useGoalDetailContext } from "@/contexts/GoalDetailContext";
+import { useInfiniteTodo } from "@/hooks/todo/useInfiniteTodo";
+
 import Section from "../component/Section";
 
-export default function GoalDoneList() {
+interface GoalDoneListProps {
+  handleToggleTodo: (todoId: number, isDone: boolean) => void;
+  setSelectedTodoId: (todoId: number | null) => void;
+  onOpenDeletePopup: () => void;
+}
+
+export default function GoalDoneList({
+  handleToggleTodo,
+  setSelectedTodoId,
+  onOpenDeletePopup,
+}: GoalDoneListProps) {
+  const { goalId } = useGoalDetailContext();
+  const { data, fetchNextPage, hasNextPage } = useInfiniteTodo(
path: src/views/goal/todo/GoalDoneList.tsx
user: login: yunsung-hodoopapa
body: {
 id: goalId, 
params : {
 status: 'done',
 size: 20,
}
created_at: 2025-03-07T12:46:40Z
author_association: NONE
diff_hunk: @@ -1,27 +1,63 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { useEffect } from "react";
+import { useInView } from "react-intersection-observer";
+
+import TodoList from "@/components/organisms/todo-list/TodoList";
+import { useGoalDetailContext } from "@/contexts/GoalDetailContext";
+import { useInfiniteTodo } from "@/hooks/todo/useInfiniteTodo";
+
 import Section from "../component/Section";
 
-export default function GoalDoneList() {
+interface GoalDoneListProps {
+  handleToggleTodo: (todoId: number, isDone: boolean) => void;
+  setSelectedTodoId: (todoId: number | null) => void;
+  onOpenDeletePopup: () => void;
+}
+
+export default function GoalDoneList({
+  handleToggleTodo,
+  setSelectedTodoId,
+  onOpenDeletePopup,
+}: GoalDoneListProps) {
+  const { goalId } = useGoalDetailContext();
+  const { data, fetchNextPage, hasNextPage } = useInfiniteTodo(
+    goalId,
+    "done",
+    20,
+  );
+  const { todos, totalCount } = data;
+  const { ref, inView } = useInView();
+  const queryClient = useQueryClient();
+
+  useEffect(() => {
+    if (inView && hasNextPage) fetchNextPage();
+  }, [inView]);
+
+  useEffect(() => {
+    queryClient.invalidateQueries({ queryKey: ["progress", goalId] });
path: src/views/goal/todo/GoalDoneList.tsx
user: login: yunsung-hodoopapa
body: refetch라는 메소드도 한번 고려해주시고, 
저는 가급적 새로운 api 요청을 일으키는 요청이 useEffect 말고 다른 곧에서 실행되면 좋겠습니다. 
created_at: 2025-03-07T12:49:30Z
author_association: NONE

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-07T06:56:12Z
body: @thgee, @hdayeon 님 랜덤 리뷰어로 당첨되셨습니다! 리뷰를 부탁드립니다. 🙏
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-07T06:57:08Z
body: 🎨 Storybook: [스토리북 바로가기](https://67acd5f79f2abb10c31d0c10-ajdorvpjzu.chromatic.com/)
⏰ Update: 2025년 03월 08일 22시 35분 46초
<!-- thollander/actions-comment-pull-request "164-storybook" -->
author_association: CONTRIBUTOR