Repository: FESI-7-4/mirujima_FE
Pull request #177: refactor: 대시보드 하이드레이션 에러  처리 및 UX 개선
Author: login: soma0078
Labels: name: 🦩sprint_5
Created: 2025-03-06T05:15:09Z | Merged: 2025-03-06T06:08:36Z

# ☘ 관련 이슈

> #171 
# 📝 작업 내용 요약

> 작업한 내용을 간략히 작성해주세요.

- 로컬 하이드레이션 에러 방지를 위해 todayDate === null 조건식 추가
- 더보기 버튼 렌더링 조건(todo/done 각각 6개 이상인 경우) 및 스타일 수정
- 주간 달성률 체크 완료 상태에 따라 실시간으로 반영되도록 수정
- 대시보드 목표별 할일에선 할 일의 목표가 중복되어 안보이도록 개선
- 대시보드에서 할 일 체크만 가능하고, 노트 작성, 케밥 아이콘 보이지 않도록 수정

# 📸 스크린샷(선택)
![image](https://github.com/user-attachments/assets/edfd5bf2-ebad-4f80-9d7c-6ec45157a1a2)



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit


- **신규 기능**
  - 할 일 항목에 목표 정보를 표시할 수 있도록 개선되어, 작업의 진행 상황을 한눈에 파악할 수 있습니다.
  - 새로운 작업 섹션 도입으로 진행 중 작업과 완료된 작업이 명확히 구분됩니다.
  - 로딩 상태에 따른 스피너 표시 기능이 추가되어 사용자 경험이 향상되었습니다.
  - 대시보드에서 할 일 항목의 추가 정보를 표시할 수 있는 기능이 도입되었습니다.

- **버그 수정**
  - 할 일 목록과 목표 세부 정보의 데이터 로딩 시점이 개선되어, 데이터 준비 상태에 맞춰 UI가 정확히 반영됩니다.

- **리팩토링**
  - 로딩 상태와 데이터 동기화가 개선되어 더 원활한 사용자 경험을 제공합니다.
  - 대시보드 및 목록 화면의 레이아웃과 스타일이 업데이트되었습니다.

- **스타일**
  - 제목, 링크, 버튼 등 UI 요소의 텍스트 크기 및 정렬이 개선되어 인터페이스가 깔끔해졌습니다.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/app/(workspace)/todoList/page.tsx b/app/(workspace)/todoList/page.tsx
index 9d6f54bd..2c828c18 100644
--- a/app/(workspace)/todoList/page.tsx
+++ b/app/(workspace)/todoList/page.tsx
@@ -66,7 +66,7 @@ export default function TodoListPage() {
                   exit={{ opacity: 1 }}
                   layout
                 >
-                  <TodoItem todo={todo} goalId={todo?.goal?.id} />
+                  <TodoItem todo={todo} goalId={todo?.goal?.id} showGoal={true} />
                 </motion.li>
               ))}
             </ul>
diff --git a/app/dashboard/_components/LatestTodoList.tsx b/app/dashboard/_components/LatestTodoList.tsx
index 6524233b..092eea29 100644
--- a/app/dashboard/_components/LatestTodoList.tsx
+++ b/app/dashboard/_components/LatestTodoList.tsx
@@ -22,8 +22,8 @@ export default function LatestTodoList() {
   return (
     <div className="rounded-container flex flex-col desktop:min-h-[250px]">
       <div className="mb-6 flex flex-wrap items-center justify-between gap-2">
-        <h3 className="h3">최근 등록한 일</h3>
-        <Link href="/todoList" className="flex items-center gap-1 text-main">
+        <h3>최근 등록한 일</h3>
+        <Link href="/todoList" className="flex items-center gap-1 text-xs text-main">
           모두 보기
           <ArrowRightIcon width={18} height={18} />
         </Link>
@@ -35,7 +35,7 @@ export default function LatestTodoList() {
         <ul>
           {todoData.map((todo: TodoType) => (
             <motion.li key={todo.id} layout>
-              <TodoItem todo={todo} />
+              <TodoItem todo={todo} showGoal={true} isDashboard={true} />
             </motion.li>
           ))}
         </ul>
diff --git a/app/dashboard/_components/UpcomingGoals.tsx b/app/dashboard/_components/UpcomingGoals.tsx
index fec55930..c0cdcc25 100644
--- a/app/dashboard/_components/UpcomingGoals.tsx
+++ b/app/dashboard/_components/UpcomingGoals.tsx
@@ -10,7 +10,7 @@ import type { GoalType } from '@/types/goal.type';
 
 export default function UpcomingGoals() {
   const { data, isLoading } = useGetGoalList();
-  const [todayDate, setTodayDate] = useState<number>(new Date().getDate());
+  const [todayDate, setTodayDate] = useState<number | null>(null);
 
   // 오늘, 내일, 모레 목표 필터링
   const upcomingGoals = getUpcomingDates(3).map(({ date, day }) => {
@@ -21,9 +21,14 @@ export default function UpcomingGoals() {
   });
 
   useEffect(() => {
-    setTodayDate(new Date().getDate());
+    const today = new Date();
+    setTodayDate(today.getDate());
   }, []);
 
+  if (todayDate === null) {
+    return <LoadingSpinner />;
+  }
+
   return (
     <div className="rounded-container">
       <h3 className="mb-6">남은 일정</h3>
diff --git a/app/dashboard/_components/goals/GoalItem.tsx b/app/dashboard/_components/goals/GoalItem.tsx
index 5229cfdd..59061390 100644
--- a/app/dashboard/_components/goals/GoalItem.tsx
+++ b/app/dashboard/_components/goals/GoalItem.tsx
@@ -4,9 +4,6 @@ import { useInView } from 'react-intersection-observer';
 
 import { useRouter } from 'next/navigation';
 
-import LoadingSpinner from '@/components/loading/LoadingSpinner';
-import TaskList from '@/components/TaskList/TaskList';
-import { useGetGoalDetail } from '@/hooks/goalsDetail/useGetGoalDetail';
 import { useAllTodos } from '@/hooks/todo/useAllTodos';
 import { useInfoStore, useModalStore, useTodoCreateModalStore } from '@/provider/store-provider';
 import ArrowDownIcon from '@/public/icon/arrow-down.svg';
@@ -15,6 +12,7 @@ import PlusIcon from '@/public/icon/plus-border-none.svg';
 import GoalProgressBar from './GoalProgressBar';
 
 import type { TodoType } from '@/types/todo.type';
+import TaskSection from './TaskSection';
 
 interface GoalItemProps {
   goalId: number;
@@ -22,16 +20,18 @@ interface GoalItemProps {
   todos?: TodoType[];
 }
 
+const MAX_TODO_LENGTH = 6;
+
 export default function GoalItem({ goalId, title, todos }: GoalItemProps) {
   const router = useRouter();
   const userId = useInfoStore((state) => state.userId);
   const setIsTodoCreateModalOpen = useModalStore((state) => state.setIsTodoCreateModalOpen);
   const setCreatedTodoState = useTodoCreateModalStore((state) => state.setCreatedTodoState);
 
-  const { data, isLoading, isError } = useGetGoalDetail(goalId.toString());
-
   const { todoData } = useAllTodos(Number(userId));
   const todoForGoal = todoData.filter((todo) => todo?.goal?.id === goalId);
+  const todoLength = todoForGoal.filter((todo) => todo.done === false).length;
+  const doneLength = todoForGoal.filter((todo) => todo.done === true).length;
 
   const [isMoreToggle, setIsMoreToggle] = useState(false);
 
@@ -40,26 +40,6 @@ export default function GoalItem({ goalId, title, todos }: GoalItemProps) {
     threshold: 0.3
   });
 
-  if (isLoading) {
-    return (
-      <div className="rounded-container w-full p-4">
-        <h3 className="text-lg font-bold">{title}</h3>
-        <div>
-          <LoadingSpinner />
-        </div>
-      </div>
-    );
-  }
-
-  if (isError || !data) {
-    return (
-      <div className="rounded-container w-full p-4">
-        <h3 className="text-lg font-bold">{title}</h3>
-        <div>Failed to fetch detail...</div>
-      </div>
-    );
-  }
-
   const handleAddTodo = () => {
     if (goalId) setCreatedTodoState({ goal: { id: goalId } });
     setIsTodoCreateModalOpen(true);
@@ -73,6 +53,8 @@ export default function GoalItem({ goalId, title, todos }: GoalItemProps) {
     router.push(`/goals/${goalId}`);
   };
 
+  const hasMoreButton = todoLength > MAX_TODO_LENGTH || doneLength > MAX_TODO_LENGTH;
+
   return (
     <div className="relative">
       <article onClick={handleGoalClick}>
@@ -83,15 +65,16 @@ export default function GoalItem({ goalId, title, todos }: GoalItemProps) {
 
           <GoalProgressBar todos={todos || []} goalId={goalId} startAnimation={inView} />
 
-          <div className="mt-3 flex flex-col border-none pb-12 desktop:flex-row">
-            <div className="flex-1 overflow-y-auto">
-              <h2 className="z-5 sticky top-0 bg-white py-2 text-[15px] font-medium leading-[20px] text-gray500">
-                To do
-              </h2>
-              <div onClick={(e) => e.stopPropagation()}>
-                <TaskList goalId={goalId} done={false} isMoreToggle={isMoreToggle} />
-              </div>
-            </div>
+          <div
+            className={`mt-3 flex flex-col border-none desktop:flex-row ${hasMoreButton ? 'pb-14' : ''}`}
+          >
+            <TaskSection
+              title="To do"
+              goalId={goalId}
+              done={false}
+              isMoreToggle={isMoreToggle}
+              isDashboard={true}
+            />
 
             <hr className="my-4 border-t border-dashed border-gray200 desktop:hidden" />
 
@@ -99,27 +82,26 @@ export default function GoalItem({ goalId, title, todos }: GoalItemProps) {
               <span className="min-h-[160px] w-px border-l border-dashed border-gray200"></span>
             </div>
 
-            <div className="flex-1 overflow-y-auto">
-              <h2 className="z-5 sticky top-0 bg-white py-2 text-[15px] font-medium leading-[20px] text-gray500">
-                Done
-              </h2>
-              <div onClick={(e) => e.stopPropagation()}>
-                <TaskList goalId={goalId} done={true} isMoreToggle={isMoreToggle} />
-              </div>
-            </div>
+            <TaskSection
+              title="Done"
+              goalId={goalId}
+              done={true}
+              isMoreToggle={isMoreToggle}
+              isDashboard={true}
+            />
           </div>
         </div>
       </article>
 
       <div className="absolute right-6 top-6 z-10 text-xs">
-        <button onClick={handleAddTodo} className="flex-center text-main">
+        <button onClick={handleAddTodo} className="flex-center p-1 text-main">
           <PlusIcon /> 할일 추가
         </button>
       </div>
 
-      {todoForGoal.length > 4 && (
-        <div className="absolute bottom-6 left-1/2 -translate-x-1/2 rounded-full border border-gray200 text-sm shadow-sm transition-all hover:bg-solid">
-          <button className="flex-center gap-1 px-6 py-1" onClick={handleMoreTodo}>
+      {hasMoreButton && (
+        <div className="absolute bottom-6 left-1/2 w-[calc(100%-48px)] -translate-x-1/2 rounded-[10px] border border-gray200 text-sm shadow-sm transition-all hover:bg-solid">
+          <button className="flex-center w-full gap-1 px-6 py-3" onClick={handleMoreTodo}>
             더보기
             <ArrowDownIcon className={`${isMoreToggle ? 'rotate-180' : ''} transition-all`} />
           </button>
diff --git a/app/dashboard/_components/goals/TaskSection.tsx b/app/dashboard/_components/goals/TaskSection.tsx
new file mode 100644
index 00000000..6bbff95a
--- /dev/null
+++ b/app/dashboard/_components/goals/TaskSection.tsx
@@ -0,0 +1,27 @@
+import TaskList from '@/components/TaskList/TaskList';
+
+interface Props {
+  title: string;
+  goalId: number;
+  done: boolean;
+  isMoreToggle: boolean;
+  isDashboard: boolean;
+}
+
+export default function TaskSection({ title, goalId, done, isMoreToggle, isDashboard }: Props) {
+  return (
+    <div className="flex-1 overflow-y-auto">
+      <h2 className="z-5 sticky top-0 bg-white py-2 text-[15px] font-medium leading-[20px] text-gray500">
+        {title}
+      </h2>
+      <div onClick={(e) => e.stopPropagation()}>
+        <TaskList
+          goalId={goalId}
+          done={done}
+          isMoreToggle={isMoreToggle}
+          isDashboard={isDashboard}
+        />
+      </div>
+    </div>
+  );
+}
diff --git a/app/dashboard/_components/weeklyChart/index.tsx b/app/dashboard/_components/weeklyChart/index.tsx
index 00d58410..15ee23e8 100644
--- a/app/dashboard/_components/weeklyChart/index.tsx
+++ b/app/dashboard/_components/weeklyChart/index.tsx
@@ -21,10 +21,6 @@ export default function WeeklyChart() {
   const [progressData, setProgressData] = useState(0);
   const count = useCountUp(Number(progressData), 2000);
 
-  useEffect(() => {
-    readTodoProgress();
-  }, []);
-
   const readTodoProgress = async () => {
     const response = await apiWithClientToken.get<{ result: TodoProgressType }>('/todos/progress');
     const data = response.data.result;
@@ -45,6 +41,8 @@ export default function WeeklyChart() {
         setChartData(getWeeklyCompletionData(todoData));
       }, 300);
     }
+
+    readTodoProgress();
   }, [todoData]);
 
   return (
diff --git a/components/TaskList/TaskList.tsx b/components/TaskList/TaskList.tsx
index a117b9c7..386a267a 100644
--- a/components/TaskList/TaskList.tsx
+++ b/components/TaskList/TaskList.tsx
@@ -15,7 +15,7 @@ interface TaskListProps {
   isLoading?: boolean;
   isError?: boolean;
   isMoreToggle?: boolean;
-  tasksLength?: number;
+  isDashboard?: boolean;
 }
 
 export default function TaskList({
@@ -25,7 +25,7 @@ export default function TaskList({
   isLoading: propLoading,
   isError: propError,
   isMoreToggle,
-  tasksLength
+  isDashboard
 }: TaskListProps) {
   const { data, isLoading, isError } = useGetTodoList(goalId, done);
 
@@ -42,7 +42,7 @@ export default function TaskList({
 
   return (
     <div
-      className={`scrollbar-thin custom-scrollbar overflow-y-auto pr-5 transition-all ${isMoreToggle ? 'max-h-[1000px]' : 'max-h-[260px]'}`}
+      className={`scrollbar-thin custom-scrollbar overflow-y-auto pr-5 transition-all ${isMoreToggle ? 'max-h-full' : 'max-h-[260px]'} ${isDashboard ? 'scrollbar-hide' : ''}`}
     >
       <ul className="mt-2 space-y-2 text-gray350">
         {loading ? (
@@ -57,7 +57,7 @@ export default function TaskList({
           // 3.  TodoItem 렌더링
           tasks.map((task) => (
             <li key={task.id}>
-              <TodoItem todo={task} goalId={goalId} />
+              <TodoItem todo={task} goalId={goalId} isDashboard={isDashboard} />
             </li>
           ))
         )}
diff --git a/components/TodoItem/TodoItem.tsx b/components/TodoItem/TodoItem.tsx
index a81be548..e1bf8271 100644
--- a/components/TodoItem/TodoItem.tsx
+++ b/components/TodoItem/TodoItem.tsx
@@ -13,7 +13,6 @@ import FlagIcon from '@/public/icon/flag-gray.svg';
 import LinkIcon from '@/public/icon/link.svg';
 import NoteIcon from '@/public/icon/note-s.svg';
 import PenIcon from '@/public/icon/pen.svg';
-import SpinIcon from '@/public/icon/spin.svg';
 
 import { CheckedIcon } from '../../app/(workspace)/todoList/_components/CheckedIcon';
 
@@ -22,16 +21,17 @@ import type { TodoType } from '@/types/todo.type';
 interface TodoItemProps {
   todo: TodoType;
   goalId?: number;
+  showGoal?: boolean;
+  isDashboard?: boolean;
 }
 
-export default function TodoItem({ todo, goalId }: TodoItemProps) {
+export default function TodoItem({ todo, goalId, showGoal, isDashboard }: TodoItemProps) {
   const router = useRouter();
   const { setCreatedTodoState } = useTodoCreateModalStore((state) => state);
   const mutation = useDeleteTodoItem(goalId);
   const { mutate: toggleTodo } = useCheckTodo();
   const setIsTodoCreateModalOpen = useModalStore((state) => state.setIsTodoCreateModalOpen);
   const setNoteDetailPageOpen = useModalStore((state) => state.setNoteDetailPageOpen);
-  const [isPenLoading, setIsPenLoading] = useState(false);
 
   // 1) 케밥 메뉴 열림/닫힘 상태
   const [isKebabSelected, setIsKebabSelected] = useState(false);
@@ -49,7 +49,6 @@ export default function TodoItem({ todo, goalId }: TodoItemProps) {
 
   const handlePenIconClick = (e: React.MouseEvent) => {
     e.stopPropagation();
-    setIsPenLoading(true);
     router.push(`/notes/create/${todo.id}`);
   };
 
@@ -60,7 +59,9 @@ export default function TodoItem({ todo, goalId }: TodoItemProps) {
       done: isDone,
       completionDate: isDone ? new Date().toISOString() : null
     };
-    toggleTodo({ todo: updatedTodo, goalId: todo?.goal?.id });
+
+    const goalId = todo?.goal?.id;
+    if (goalId) toggleTodo({ todo: updatedTodo, goalId });
   };
 
   const handleDelete = () => {
@@ -101,15 +102,17 @@ export default function TodoItem({ todo, goalId }: TodoItemProps) {
     }
   };
 
+  const isGoalVisible = showGoal && todo.goal?.id;
+
   return (
     <>
       <div
-        className="relative mb-3 flex items-center justify-between"
+        className={`relative mb-4 grid grid-cols-4 items-center justify-between ${isGoalVisible ? 'grid-rows-2' : 'grid-rows-1'}`}
         onMouseEnter={handleMouseEnter}
         onMouseLeave={handleMouseLeave}
       >
         <div
-          className={`flex min-w-0 flex-1 items-baseline gap-2 ${
+          className={`col-start-1 col-end-4 flex flex-1 items-baseline gap-2 ${
             isHovered || isKebabSelected ? 'text-main' : 'text-gray500'
           }`}
         >
@@ -125,18 +128,10 @@ export default function TodoItem({ todo, goalId }: TodoItemProps) {
             </span>
           </div>
 
-          <div className="flex min-w-0 flex-col gap-1">
-            <span className={`truncate ${todo.done ? 'line-through' : ''}`}>{todo.title}</span>
-            {todo.goal?.id && (
-              <span className="flex items-center gap-1 text-[13px] text-gray350">
-                <FlagIcon />
-                <span className="min-w-0 flex-1 truncate">{todo.goal.title}</span>
-              </span>
-            )}
-          </div>
+          <h4 className={`truncate py-0.5 ${todo.done ? 'line-through' : ''} `}>{todo.title}</h4>
         </div>
 
-        <div className="relative -mt-4 flex shrink-0 items-start gap-1 desktop:-mt-0">
+        <div className="relative flex shrink-0 items-center justify-end gap-1">
           <div className="flex flex-row gap-1 py-[1px]">
             {todo.filePath && <FileIcon width={18} height={18} />}
             {todo.linkUrl && <LinkIcon width={18} height={18} />}
@@ -146,38 +141,38 @@ export default function TodoItem({ todo, goalId }: TodoItemProps) {
               </span>
             )}
           </div>
-
           <span
             className={`${priorityClass} rounded-full border p-1 px-3 py-0.5 text-[11px] leading-[13px]`}
           >
             {todo.priority}
           </span>
-
-          {!todo.noteId &&
-            (isPenLoading ? (
-              <span className={isHovered || isKebabSelected ? 'block' : 'hidden'}>
-                <SpinIcon width={18} height={18} />
-              </span>
-            ) : (
-              <button
-                onClick={handlePenIconClick}
-                className={isHovered || isKebabSelected ? 'block' : 'hidden'}
-              >
-                <PenIcon width={18} height={18} />
-              </button>
-            ))}
-
-          <div
-            className={isHovered || isKebabSelected ? 'block' : 'hidden'}
-            onClick={handleKebabClick}
-          >
-            <KebabForGoal
-              size={18}
-              onEdit={() => handleOpenEditModal(todo)}
-              onDelete={handleDelete}
-            />
-          </div>
+          {!todo.noteId && !isDashboard && (
+            <button
+              onClick={handlePenIconClick}
+              className={isHovered || isKebabSelected ? 'block' : 'hidden'}
+            >
+              <PenIcon width={18} height={18} />
+            </button>
+          )}
+          {!isDashboard && (
+            <div
+              className={isHovered || isKebabSelected ? 'block' : 'hidden'}
+              onClick={handleKebabClick}
+            >
+              <KebabForGoal
+                size={18}
+                onEdit={() => handleOpenEditModal(todo)}
+                onDelete={handleDelete}
+              />
+            </div>
+          )}
         </div>
+        {isGoalVisible && (
+          <span className="col-start-1 -col-end-1 flex items-center gap-1 truncate pl-5 text-body2 text-gray350">
+            <FlagIcon />
+            <span className="min-w-0 flex-1 truncate">{todo?.goal?.title}</span>
+          </span>
+        )}
       </div>
     </>
   );
diff --git a/components/kebab/KebabForGoal.tsx b/components/kebab/KebabForGoal.tsx
index 04f243a5..38335907 100644
--- a/components/kebab/KebabForGoal.tsx
+++ b/components/kebab/KebabForGoal.tsx
@@ -38,7 +38,7 @@ export default function KebabForGoal({ size, onEdit, onDelete, className }: Keba
   };
 
   return (
-    <div className={`relative ${className ?? ''}`}>
+    <div className={`flex-center relative ${className ?? ''}`}>
       <button onClick={toggleMenu} className="rounded-md bg-Cgray" aria-label="목표 수정 및 삭제">
         {isKebabOpen ? (
           <KebabFocusIcon width={size} height={size} />
diff --git a/hooks/goal/useInfiniteGoalList.ts b/hooks/goal/useInfiniteGoalList.ts
index 9492175c..a21919c5 100644
--- a/hooks/goal/useInfiniteGoalList.ts
+++ b/hooks/goal/useInfiniteGoalList.ts
@@ -10,7 +10,8 @@ export const useInfiniteGoalList = () => {
     queryKey: ['goals'],
     queryFn: ({ pageParam = undefined }) => readGoalList({ lastSeenId: pageParam, pageSize: 3 }),
     initialPageParam: 9999,
-    getNextPageParam: (lastPage) => (lastPage.remainingCount > 0 ? lastPage.lastSeenId : undefined),
+    getNextPageParam: (lastPage) =>
+      lastPage?.remainingCount > 0 ? lastPage.lastSeenId : undefined,
     select: (data) => {
       return {
         ...data,
diff --git a/hooks/goalsDetail/useGetTodoList.ts b/hooks/goalsDetail/useGetTodoList.ts
index fd450278..f95a3e1a 100644
--- a/hooks/goalsDetail/useGetTodoList.ts
+++ b/hooks/goalsDetail/useGetTodoList.ts
@@ -23,7 +23,7 @@ const fetchTodoList = async (
   goalId: number | undefined | null,
   done = false,
   lastSeenId = 9999,
-  pageSize = 5
+  pageSize = 50
 ): Promise<TodoType[]> => {
   const response = await apiWithClientToken.get<TodoListResponse>('/todos', {
     params: { goalId, done, lastSeenId, pageSize }
diff --git a/types/todo.type.ts b/types/todo.type.ts
index a160b401..6498352e 100644
--- a/types/todo.type.ts
+++ b/types/todo.type.ts
@@ -7,7 +7,7 @@ export type TodoListType = {
 };
 
 export type TodoType = {
-  goal: Pick<GoalType, 'id' | 'title' | 'completionDate'>;
+  goal: Pick<GoalType, 'id' | 'title' | 'completionDate'> | null;
   noteId: number | null;
   done: boolean;
   linkUrl: string | null;


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>app/dashboard/_components/goals/TaskSection.tsx (1)</summary><blockquote>

`11-27`: **이벤트 전파 중단에 대한 명확한 설명 필요**

TaskSection 컴포넌트가 잘 구현되어 있습니다. sticky 헤더를 사용하여 사용자 경험을 향상시키고, TaskList에 필요한 props를 적절히 전달하고 있습니다.

다만, 17번 줄에서 `e.stopPropagation()`을 사용하는 이유에 대한 주석이 있으면 코드 가독성과 유지보수성이 향상될 것입니다.

```diff
-      <div onClick={(e) => e.stopPropagation()}>
+      {/* 부모 컴포넌트의 이벤트 처리와 충돌을 방지하기 위해 이벤트 전파 중단 */}
+      <div onClick={(e) => e.stopPropagation()}>
```

</blockquote></details>
<details>
<summary>app/dashboard/_components/weeklyChart/index.tsx (1)</summary><blockquote>

`44-46`: **주간 달성률 실시간 업데이트 개선**

이전에는 컴포넌트 마운트 시에만 `readTodoProgress()`를 호출했지만, 이제 `todoData`가 변경될 때마다 호출하도록 변경하여 실시간 업데이트가 가능해졌습니다. 이는 PR 목표 중 "완료 상태에 따라 실시간으로 주간 달성률을 반영"하는 부분을 구현한 것입니다.

다만, `todoData`가 null인 경우에 대한 안전장치가 없습니다.

```diff
   useEffect(() => {
     if (todoData) {
       setTimeout(() => {
         setChartData(getWeeklyCompletionData(todoData));
       }, 300);
+      readTodoProgress();
     }
-
-    readTodoProgress();
   }, [todoData]);
```

이렇게 수정하면 `todoData`가 있을 때만 `readTodoProgress()`가 호출되므로 더 안전합니다.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7586b54a66bb03d8fa34efcfb1765a6ab0b15f85 and b645624b00b4413890fd0f363de4605fc65d4ca8.

</details>

<details>
<summary>📒 Files selected for processing (11)</summary>

* `app/(workspace)/todoList/page.tsx` (1 hunks)
* `app/dashboard/_components/LatestTodoList.tsx` (2 hunks)
* `app/dashboard/_components/UpcomingGoals.tsx` (2 hunks)
* `app/dashboard/_components/goals/GoalItem.tsx` (4 hunks)
* `app/dashboard/_components/goals/TaskSection.tsx` (1 hunks)
* `app/dashboard/_components/weeklyChart/index.tsx` (1 hunks)
* `components/TaskList/TaskList.tsx` (4 hunks)
* `components/TodoItem/TodoItem.tsx` (4 hunks)
* `components/kebab/KebabForGoal.tsx` (1 hunks)
* `hooks/goal/useInfiniteGoalList.ts` (1 hunks)
* `hooks/goalsDetail/useGetTodoList.ts` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* components/kebab/KebabForGoal.tsx

</details>

<details>
<summary>🔇 Additional comments (30)</summary><blockquote>

<details>
<summary>hooks/goalsDetail/useGetTodoList.ts (1)</summary>

`26-26`: **페이지 크기 기본값 변경에 대한 고려사항**

기본 페이지 크기를 5에서 50으로 증가시켰습니다. 이는 한 번에 더 많은 할 일 항목을 가져오게 되어 목표 중복 방지와 관련된 PR 목표를 지원할 수 있습니다.

다만, 페이지 크기 증가는 초기 로딩 시간과 데이터 사용량에 영향을 줄 수 있습니다. 특히 모바일 환경이나 느린 네트워크에서는 성능 저하가 발생할 수 있습니다.

기본값 변경 이유를 설명하는 간단한 주석을 추가하는 것을 고려해보세요.

</details>
<details>
<summary>app/dashboard/_components/goals/TaskSection.tsx (1)</summary>

`3-9`: **인터페이스 구성이 잘 되어 있습니다**

컴포넌트의 props 인터페이스가 명확하게 정의되어 있어 좋습니다.

</details>
<details>
<summary>app/(workspace)/todoList/page.tsx (1)</summary>

`69-69`: **목표 표시 속성 추가**

`TodoItem` 컴포넌트에 `showGoal={true}` 속성을 추가하여 목표 정보를 표시하도록 개선했습니다. 이는 PR 목표 중 "목표 중복 방지" 부분을 지원합니다.

</details>
<details>
<summary>hooks/goal/useInfiniteGoalList.ts (1)</summary>

`13-14`: **Optional chaining을 추가하여 에러 방지**

`lastPage` 객체가 null이나 undefined일 때 안전하게 속성에 접근할 수 있도록 optional chaining을 사용하셨네요. 이는 런타임 에러를 방지하는 좋은 방어적 코딩 방식입니다.

</details>
<details>
<summary>app/dashboard/_components/LatestTodoList.tsx (2)</summary>

`25-26`: **제목 스타일 간소화 및 링크 텍스트 크기 조정**

h3 태그에서 className이 제거되었고, 링크에 `text-xs` 클래스가 추가되었습니다. 스타일 일관성이 유지되는지 확인해 주세요.

---

`38-38`: **새로운 props 추가로 기능 확장**

TodoItem 컴포넌트에 `showGoal={true}`와 `isDashboard={true}` props가 추가되었습니다. 이는 PR 목표에 언급된 대로 대시보드에서 태스크 체크만 허용하고 다른 기능을 숨기기 위한 변경으로 보입니다.

</details>
<details>
<summary>app/dashboard/_components/UpcomingGoals.tsx (3)</summary>

`13-13`: **상태 초기값 변경으로 하이드레이션 오류 방지**

`todayDate` 상태의 초기값을 `null`로 설정하여 서버와 클라이언트 사이의 하이드레이션 불일치를 방지합니다. 이는 PR 목표에 언급된 로컬 하이드레이션 오류 방지와 일치합니다.

---

`24-26`: **Date 객체 생성 로직 개선**

현재 날짜를 가져오는 로직이 좀 더 명확해졌습니다. 변수를 먼저 선언한 후 상태를 업데이트하는 패턴으로 코드 가독성이 향상되었습니다.

---

`28-31`: **로딩 상태 처리 개선**

`todayDate`가 null일 때 LoadingSpinner를 표시하는 조건부 렌더링을 추가했습니다. 이로써 날짜 정보가 준비되기 전에 컴포넌트가 렌더링되는 것을 방지하고 사용자 경험이 향상됩니다.

</details>
<details>
<summary>app/dashboard/_components/goals/GoalItem.tsx (8)</summary>

`15-15`: **새로운 컴포넌트 도입**

TaskSection 컴포넌트를 import하여 코드 구조를 개선했습니다. 이는 코드 재사용성을 높이고 관심사를 분리하는 좋은 리팩토링입니다.

---

`31-33`: **할 일 및 완료된 항목 개수 계산 로직 추가**

할 일(`todoLength`)과 완료된 항목(`doneLength`)의 개수를 계산하는 변수를 추가하여 코드 가독성을 높이고, 이후 조건부 렌더링에 활용할 수 있도록 했습니다.

---

`54-55`: **"더보기" 버튼 표시 조건 개선**

`hasMoreButton` 변수를 통해 할 일 또는 완료된 항목이 6개 이상일 때만 "더보기" 버튼이 표시되도록 조건을 개선했습니다. 이는 PR 목표에서 언급된 "more" 버튼 수정 사항과 일치합니다.

---

`66-68`: **컨테이너 스타일 및 조건부 렌더링 업데이트**

컨테이너의 스타일을 업데이트하고, `hasMoreButton` 값에 따라 하단 패딩을 조건부로 적용하도록 변경했습니다. 이로써 "더보기" 버튼 유무에 따른 레이아웃이 일관되게 유지됩니다.

---

`69-76`: **TaskSection 컴포넌트로 할 일 섹션 대체**

기존 코드를 TaskSection 컴포넌트로 대체하여 코드를 간결하게 만들고 유지보수성을 개선했습니다. `isDashboard={true}` prop을 전달하여 대시보드 컨텍스트에서의 특별한 렌더링을 지원합니다.

---

`83-90`: **TaskSection 컴포넌트로 완료 섹션 대체**

마찬가지로 완료된 항목을 위한 섹션도 TaskSection 컴포넌트로 대체하여 코드 중복을 제거하고 일관된 인터페이스를 제공합니다.

---

`95-95`: **버튼 스타일 개선**

할 일 추가 버튼에 패딩(`p-1`)을 추가하여 클릭 영역을 넓히고 사용자 경험을 개선했습니다.

---

`100-102`: **"더보기" 버튼 조건부 렌더링 업데이트**

`hasMoreButton` 변수를 사용하여 항목이 6개 이상일 때만 "더보기" 버튼이 표시되도록 조건부 렌더링을 업데이트했습니다. 이는 PR 목표에서 언급된 대로 "more" 버튼의 표시 조건이 수정되었음을 확인합니다.

</details>
<details>
<summary>components/TaskList/TaskList.tsx (4)</summary>

`18-18`: **인터페이스에 dashboard 속성 추가**

TaskListProps 인터페이스에 isDashboard 속성을 추가하여 대시보드에서 사용될 때의 동작을 구분할 수 있게 되었습니다. 이는 PR 목표에 맞게 대시보드 컴포넌트를 업데이트하는 좋은 접근 방식입니다.

---

`28-28`: **파라미터 업데이트 완료**

tasksLength를 제거하고 isDashboard 속성으로 대체한 것은 인터페이스 변경과 일치합니다. 컴포넌트 서명이 깔끔하게 업데이트되었습니다.

---

`45-45`: **대시보드 모드에서 스크롤바 숨김 처리**

대시보드에서 사용될 때 scrollbar-hide 클래스를 조건부로 추가하는 좋은 접근입니다. 이렇게 하면 대시보드 환경에서 UI가 더 깔끔하게 표시됩니다.

---

`60-60`: **하위 컴포넌트로 isDashboard 전달**

TodoItem 컴포넌트에 isDashboard 속성을 전달하여 일관된 스타일과 동작을 유지하는 것은 좋은 접근법입니다. 이는 PR 목표에 명시된 "대시보드에서 태스크 체크만 가능하도록" 하는 요구사항을 충족시키는 중요한 변경입니다.

</details>
<details>
<summary>components/TodoItem/TodoItem.tsx (9)</summary>

`24-25`: **새로운 속성 추가**

`showGoal`과 `isDashboard` 속성 추가는 컴포넌트의 유연성을 증가시키는 좋은 변화입니다. 이를 통해 다양한 컨텍스트에서 필요에 맞게 컴포넌트를 조정할 수 있습니다. PR 목표에 맞게 대시보드에서의 기능 제한을 구현하기 위한 기반이 됩니다.

---

`28-28`: **컴포넌트 파라미터 업데이트**

새로운 속성들을 매개변수로 추가하여 인터페이스 변경과 일치시켰습니다. 좋은 업데이트입니다.

---

`103-104`: **목표 표시 조건 설정**

목표 표시 여부를 결정하는 `isGoalVisible` 변수의 추가는 명확하고 간결합니다. `showGoal` 속성과 목표 ID의 존재 여부를 모두 확인하는 것은 안전한 접근 방식입니다.

---

`108-108`: **그리드 시스템 동적 조정**

`isGoalVisible` 값에 따라 그리드 행 수를 동적으로 조정하는 것은 레이아웃을 효율적으로 관리하는 좋은 방법입니다. 이렇게 하면 목표 정보가 표시될 때와 그렇지 않을 때 공간을 최적화할 수 있습니다.

---

`113-115`: **스타일 업데이트**

컨테이너의 스타일링을 업데이트하여 그리드 레이아웃과 일관성을 유지했습니다. 클래스명이 잘 구성되어 있습니다.

---

`129-129`: **할일 제목 스타일 개선**

할일 제목에 truncate 클래스를 추가하여 긴 텍스트가 잘리도록 처리한 것은 UI 일관성을 유지하는 좋은 방법입니다.

---

`132-132`: **컨테이너 스타일 최적화**

아이콘 및 버튼 컨테이너의 스타일을 업데이트하여 레이아웃 일관성을 유지했습니다.

---

`147-166`: **대시보드 모드에서 편집 기능 제한**

PR 목표에 명시된 대로 대시보드에서는 노트 생성 옵션과 케밥 아이콘을 숨기는 조건부 렌더링을 구현했습니다. 이렇게 하면 사용자가 대시보드에서는 할일 체크만 가능하도록 제한됩니다. 조건문이 명확하게 구성되어 있습니다.

---

`168-173`: **목표 정보 조건부 표시**

`isGoalVisible`에 따라 목표 정보를 조건부로 렌더링하는 것은 PR 목표에 부합하는 좋은 구현입니다. 그리드 시스템 내에서 레이아웃도 잘 조정되어 있습니다. 목표 제목에 truncate를 적용하여 UI가 깨지지 않도록 처리한 점도 좋습니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T05:17:25Z
user: login: mimi-0506
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T05:17:58Z
user: login: mimi-0506
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T05:18:07Z
user: login: jinoc-git
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T05:39:49Z
user: login: soma0078
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T05:42:34Z
user: login: kirnjiyun
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T06:00:17Z
user: login: soma0078
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T06:06:57Z

Review comments:
diff_hunk: @@ -73,6 +51,8 @@ export default function GoalItem({ goalId, title, todos }: GoalItemProps) {
     router.push(`/goals/${goalId}`);
   };
 
+  const hasMoreButton = todoLength > 6 || doneLength > 6;
path: app/dashboard/_components/goals/GoalItem.tsx
user: login: mimi-0506
body: 숫자는 모두 상수로 빼주시면 좋을 것 같아요!
created_at: 2025-03-06T05:17:28Z
author_association: MEMBER
diff_hunk: @@ -101,15 +100,17 @@ export default function TodoItem({ todo, goalId }: TodoItemProps) {
     }
   };
 
+  const isGoalVisible = showGoal && todo.goal?.id;
path: components/TodoItem/TodoItem.tsx
user: login: jinoc-git
body: `isGoalVisible`의 타입이 number로 추론될 것 같은데 맞을까용?

추가로 `types/todo.type.ts`파일 `TodoType`의 goal 타입을 바꾸시면 옵셔널이 필요한 곳을 까먹지 않을 것 같아요!
created_at: 2025-03-06T05:39:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,6 +51,8 @@ export default function GoalItem({ goalId, title, todos }: GoalItemProps) {
     router.push(`/goals/${goalId}`);
   };
 
+  const hasMoreButton = todoLength > 6 || doneLength > 6;
path: app/dashboard/_components/goals/GoalItem.tsx
user: login: soma0078
body: 넹 수정했습니다!
created_at: 2025-03-06T05:42:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,15 +100,17 @@ export default function TodoItem({ todo, goalId }: TodoItemProps) {
     }
   };
 
+  const isGoalVisible = showGoal && todo.goal?.id;
path: components/TodoItem/TodoItem.tsx
user: login: soma0078
body: 넹 맞습니다!
반영했습니다
created_at: 2025-03-06T06:06:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-06T05:15:16Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
이번 PR은 여러 컴포넌트의 인터페이스와 렌더링 로직을 업데이트합니다.  
- `TodoItem` 컴포넌트 호출에 `showGoal` 및 `isDashboard`와 같은 새로운 props가 추가되었습니다.  
- `LatestTodoList`, `UpcomingGoals` 등의 페이지 및 대시보드 컴포넌트의 스타일과 상태 관리가 수정되었습니다.  
- 목표(task) 관련 컴포넌트에서는 로딩 및 에러 처리를 단순화하고, 새 컴포넌트인 `TaskSection`이 도입되었습니다.  
- 일부 훅에서는 안전한 옵셔널 체이닝과 페이징 기본값 변경이 이루어졌습니다.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `app/(workspace)/todoList/page.tsx`, <br>`app/dashboard/_components/LatestTodoList.tsx` | `TodoItem` 호출 시 기존 props에 `showGoal` (및 `isDashboard` in LatestTodoList) 추가 |
| `app/dashboard/_components/UpcomingGoals.tsx` | `todayDate` 초기 상태를 `number`에서 `number | null`로 변경하고, 로딩 상태에서 `LoadingSpinner` 렌더링 추가 |
| `app/dashboard/_components/goals/GoalItem.tsx`, <br>`app/dashboard/_components/goals/TaskSection.tsx` | 불필요한 로딩/에러 처리 요소 제거, 직접 작업 개수 산출 및 조건에 따른 "more" 버튼 렌더링, 새 `TaskSection` 컴포넌트 도입 |
| `app/dashboard/_components/weeklyChart/index.tsx` | 초기 마운트 시 호출되던 `useEffect`를 제거하고, `todoData` 변경에 따른 `useEffect` 내에서 `readTodoProgress` 호출 |
| `components/TaskList/TaskList.tsx` | 인터페이스에서 `tasksLength` 제거, 대신 `isDashboard` prop 추가 및 조건부 클래스 적용, `TodoItem`에 해당 prop 전달 |
| `components/TodoItem/TodoItem.tsx` | `TodoItemProps`에 `showGoal` 및 `isDashboard` optional props 추가, `isPenLoading` 관련 로직 제거, 그리드 레이아웃 및 조건부 goal title 렌더링 적용 |
| `components/kebab/KebabForGoal.tsx` | `<div>`의 클래스 속성을 `relative`에서 `flex-center relative`로 변경 |
| `hooks/goal/useInfiniteGoalList.ts`, <br>`hooks/goalsDetail/useGetTodoList.ts` | `useInfiniteGoalList`에서는 `remainingCount` 접근 시 옵셔널 체이닝 추가, <br>`useGetTodoList`에서는 기본 `pageSize` 값을 5에서 50으로 상향 조정 |

## Possibly related PRs
- **FESI-7-4/mirujima_FE#86:** `TodoItem` 컴포넌트의 props 수정, 특히 `showGoal` prop 추가와 관련된 변경사항이 있습니다.
- **FESI-7-4/mirujima_FE#136:** `TodoItem` 컴포넌트의 `showGoal` prop 추가와 관련된 변경사항으로, 강한 코드 수준의 연관성이 있습니다.
- **FESI-7-4/mirujima_FE#132:** `TodoItem` 컴포넌트의 props 수정과 관련된 변경사항으로, 두 PR 모두 `showGoal` 및 `isDashboard` prop을 포함합니다.

## Suggested labels
`🍅sprint_2`, `🧀sprint_3`

## Suggested reviewers
- kirnjiyun
- mimi-0506
- jinoc-git

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: .coderabbit.yml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 478c5da9b0dfe0fb26b63c5f72fd8b52eecd628d and 3929baf33bb67b38c8554bc392c9adb99ce91462.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `components/TodoItem/TodoItem.tsx` (5 hunks)
* `types/todo.type.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* components/TodoItem/TodoItem.tsx

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>types/todo.type.ts (1)</summary>

`10-10`: **`goal` 속성에 null 타입 추가에 대한 의견**

`TodoType`의 `goal` 속성에 `null` 타입을 추가한 것은 좋은 변경입니다. 이 변경은 할 일 항목이 목표와 연결되지 않은 경우를 올바르게 표현할 수 있게 해주며, PR 목표에 언급된 하이드레이션 오류 방지에 기여할 것입니다.

다음과 같은 이점이 있습니다:
- 타입 안전성 향상: 목표가 없는 할 일 항목을 명시적으로 표현 가능
- 조건부 렌더링 지원: 목표 정보가 있을 때만 표시하는 로직을 구현하기 용이함
- 런타임 오류 감소: 관련 컴포넌트에서 `goal` 속성 접근 시 안전한 옵셔널 체이닝 사용 가능

이제 컴포넌트에서 `goal` 속성을 사용할 때 옵셔널 체이닝(`?.`)이나 조건부 렌더링을 통해 `null` 상태를 적절히 처리해야 합니다.

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=FESI-7-4/mirujima_FE&utm_content=177):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `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. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- 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 src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### 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.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: kirnjiyun
created_at: 2025-03-06T06:00:05Z
body: 개선된 화면 너무 깔끔하고 좋아용!!
author_association: CONTRIBUTOR