Repository: ICan-5/ICan-Frontend
Pull request #35: [CAN-56/feat] 노트 단일 조회 API 연동
Author: login: izodam
Labels: name: ✨ Feature
description: 기능 개발
name: 📬 API
description: 서버 통신
name: 🌈Style
description: 스타일링 관련
Created: 2025-03-01T10:15:27Z | Merged: 2025-03-04T00:29:45Z

## 개요 🔎

## 작업내용 📝
> 노트 조회 페이지
- Next Parallel Route 사용하여 url이 변경되는 모달창으로 구현
- url 디자인 추가 (임의로 추가함)
- 노트 내용 길어질 때 내부 스크롤 구현

> 노트 조회 API 연동
- 서버사이드에서 호출 후 클라이언트 컴포넌트로 넘겨줌
- 노트에 있는 날짜는 마지막 수정 일시로 구현
- fetch 실패 시 404페이지로 이동

## 패키지 설치내용 📦

`적용했던 Package의 install 명령어를 남겨주세요.`


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **신규 기능**
  - 노트 상세 정보를 확인할 수 있는 전용 모달 및 상세 페이지 도입
  - 업데이트된 레이아웃에 모달 콘텐츠가 함께 표시되어 사용자 경험이 향상됨

- **리팩토링**
  - 노트 아이템 인터랙션 개선: 모달 호출 대신 상세 페이지로 이동하도록 변경
  - 노트 목록 및 아이템 관련 불필요한 속성 제거로 구성 간소화

- **스타일**
  - 아이콘 버튼의 크기 및 디자인 규칙 업데이트

- **서비스 개선**
  - 노트 상세 정보 조회 시 날짜 형식 개선 및 오류 발생 시 안내 처리 강화
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/app/(member)/@modal/(.)note/[noteId]/page.tsx b/src/app/(member)/@modal/(.)note/[noteId]/page.tsx
new file mode 100644
index 00000000..1894ab52
--- /dev/null
+++ b/src/app/(member)/@modal/(.)note/[noteId]/page.tsx
@@ -0,0 +1,10 @@
+import { renderNoteDetail } from '@/utils/renderNotePage';
+
+export default async function NoteDetailModal({
+  params,
+}: {
+  params: { noteId: string };
+}) {
+  const noteId = Number(params.noteId);
+  return renderNoteDetail(noteId);
+}
diff --git a/src/app/(member)/@modal/default.tsx b/src/app/(member)/@modal/default.tsx
new file mode 100644
index 00000000..6ddf1b76
--- /dev/null
+++ b/src/app/(member)/@modal/default.tsx
@@ -0,0 +1,3 @@
+export default function Default() {
+  return null;
+}
diff --git a/src/app/(member)/goals/[id]/note/page.tsx b/src/app/(member)/goals/[id]/note/page.tsx
index 285b33e2..ec9e0be3 100644
--- a/src/app/(member)/goals/[id]/note/page.tsx
+++ b/src/app/(member)/goals/[id]/note/page.tsx
@@ -10,7 +10,7 @@ config.autoAddCss = false;
 
 const notes = [
   {
-    id: 1,
+    id: 494,
     title: '자바스크립트를 배우기 전 알아두어야 할 것',
     todo: '자바스크립트 기초 1',
     content: '내용이 어쩌구 저쩌구 내용이 어쩌구 저쩌구',
@@ -52,7 +52,7 @@ export default function Page({ params }: { params: { id: string } }) {
         <NoteHeader id={params.id} />
       </div>
       <div>
-        <NoteList notes={notes} goalId={params.id} />
+        <NoteList notes={notes} />
       </div>
     </div>
   );
diff --git a/src/app/(member)/layout.tsx b/src/app/(member)/layout.tsx
index 1f82aae4..fb71bd3e 100644
--- a/src/app/(member)/layout.tsx
+++ b/src/app/(member)/layout.tsx
@@ -2,8 +2,10 @@ import Navbar from '@/components/common/navbar/Navbar';
 
 export default function Layout({
   children,
+  modal,
 }: Readonly<{
   children: React.ReactNode;
+  modal: React.ReactNode;
 }>) {
   return (
     <div className="flex h-screen w-screen overflow-hidden bg-gs100 will-change-scroll">
@@ -12,6 +14,7 @@ export default function Layout({
         <div className="relative left-1/2 flex max-h-[1000px] w-full max-w-screen-xl -translate-x-1/2 flex-col gap-4 overflow-auto p-4 md:h-screen md:gap-8 md:p-10">
           {children}
         </div>
+        {modal}
       </div>
     </div>
   );
diff --git a/src/app/(member)/note/[noteId]/page.tsx b/src/app/(member)/note/[noteId]/page.tsx
new file mode 100644
index 00000000..0f05b091
--- /dev/null
+++ b/src/app/(member)/note/[noteId]/page.tsx
@@ -0,0 +1,10 @@
+import { renderNoteDetail } from '@/utils/renderNotePage';
+
+export default async function NoteDetailPage({
+  params,
+}: {
+  params: { noteId: string };
+}) {
+  const noteId = Number(params.noteId);
+  return renderNoteDetail(noteId);
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index b2c6179b..3cf0f054 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -24,7 +24,9 @@ export default function RootLayout({
     <html lang="ko" className={`${pretendard.variable}`}>
       <body className={pretendard.className}>
         <SessionProvider>
-          <ClientProvider>{children}</ClientProvider>
+          <ClientProvider>
+            <div>{children}</div>
+          </ClientProvider>
         </SessionProvider>
       </body>
     </html>
diff --git a/src/components/common/button/IconButton.tsx b/src/components/common/button/IconButton.tsx
index 35116b26..55901401 100644
--- a/src/components/common/button/IconButton.tsx
+++ b/src/components/common/button/IconButton.tsx
@@ -17,10 +17,10 @@ export default function IconButton({
   return (
     <button
       type="button"
-      className={cn('flex h-7 w-7 items-center justify-center', className)}
+      className={cn('flex size-7 items-center justify-center', className)}
       {...props}
     >
-      <FontAwesomeIcon className="h-4 w-4" icon={icon} size="sm" />
+      <FontAwesomeIcon className="size-4" icon={icon} size="sm" />
     </button>
   );
 }
diff --git a/src/components/note/NoteItem.tsx b/src/components/note/NoteItem.tsx
index 2c75b1bc..d05e2e11 100644
--- a/src/components/note/NoteItem.tsx
+++ b/src/components/note/NoteItem.tsx
@@ -6,7 +6,7 @@ import {
 } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import React, { useState } from 'react';
-import NoteModal from '@/components/note/NoteModal';
+import Link from 'next/link';
 import ConfirmDeleteModal from '@/components/note/ConfirmDeleteModal';
 import { useClickOutside } from '@/hooks/useClickOutside';
 
@@ -20,11 +20,9 @@ interface Note {
 
 interface NoteItemProps {
   note: Note;
-  goalId: string;
 }
 
-export default function NoteItem({ note, goalId }: NoteItemProps) {
-  const [isModalOpen, setIsModalOpen] = useState(false);
+export default function NoteItem({ note }: NoteItemProps) {
   const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
   const [menuRef, isMenuOpen, setIsMenuOpen] = useClickOutside<HTMLDivElement>(
     () => setIsMenuOpen(false),
@@ -81,13 +79,12 @@ export default function NoteItem({ note, goalId }: NoteItemProps) {
           </div>
 
           {/* 제목 */}
-          <button
-            type="button"
-            className="mt-2 w-full cursor-pointer border-b pb-3 text-left text-18M font-semibold hover:text-slate500"
-            onClick={() => setIsModalOpen((prev) => !prev)}
+          <Link
+            href={`/note/${note.id}`}
+            className="mt-2 block w-full cursor-pointer border-b pb-3 text-left text-18M font-semibold hover:text-slate500"
           >
             {note.title}
-          </button>
+          </Link>
 
           {/* todo */}
           <div className="mb-6 mt-3 flex items-center gap-2 text-12M text-gs700">
@@ -96,14 +93,7 @@ export default function NoteItem({ note, goalId }: NoteItemProps) {
           </div>
         </div>
       </div>
-
       {/* 모달 */}
-      <NoteModal
-        isOpen={isModalOpen}
-        onClose={() => setIsModalOpen(false)}
-        note={note}
-        goalId={goalId}
-      />
       <ConfirmDeleteModal
         isOpen={isDeleteModalOpen}
         onClose={() => setIsDeleteModalOpen(false)}
diff --git a/src/components/note/NoteList.tsx b/src/components/note/NoteList.tsx
index ac91dbf8..d233752e 100644
--- a/src/components/note/NoteList.tsx
+++ b/src/components/note/NoteList.tsx
@@ -11,10 +11,9 @@ interface Note {
 
 interface NoteListProps {
   notes: Note[];
-  goalId: string;
 }
 
-export default function NoteList({ notes, goalId }: NoteListProps) {
+export default function NoteList({ notes }: NoteListProps) {
   return (
     <div>
       {notes.length === 0 && (
@@ -23,7 +22,7 @@ export default function NoteList({ notes, goalId }: NoteListProps) {
         </div>
       )}
       {notes.map((note) => (
-        <NoteItem key={note.id} note={note} goalId={goalId} />
+        <NoteItem key={note.id} note={note} />
       ))}
     </div>
   );
diff --git a/src/components/note/NoteModal.tsx b/src/components/note/NoteModal.tsx
index c0fa3127..c7829533 100644
--- a/src/components/note/NoteModal.tsx
+++ b/src/components/note/NoteModal.tsx
@@ -1,53 +1,55 @@
 'use client';
 
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faFlag, faXmark } from '@fortawesome/free-solid-svg-icons';
+import { faFlag, faLink, faXmark } from '@fortawesome/free-solid-svg-icons';
 import { motion, AnimatePresence } from 'framer-motion';
-import ReactDOM from 'react-dom';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import Link from 'next/link';
+import { createPortal } from 'react-dom';
 import { useClickOutside } from '@/hooks/useClickOutside';
-import { useGoals } from '@/hooks/useGoals';
-import { Goal } from '@/types/goals';
 
 interface NoteModalProps {
-  isOpen: boolean;
-  onClose: () => void;
-  note: {
-    id: number;
-    title: string;
-    todo: string;
-    content: string;
-    date: string;
-  };
-  goalId: string;
+  goalTitle?: string | null;
+  todoTitle: string;
+  title: string;
+  content: string;
+  linkUrl?: string;
+  updatedAt: string;
 }
 
 export default function NoteModal({
-  isOpen,
-  onClose,
-  note,
-  goalId,
+  goalTitle,
+  todoTitle,
+  title,
+  content,
+  linkUrl,
+  updatedAt,
 }: NoteModalProps) {
-  const [modalRef] = useClickOutside<HTMLDivElement>(onClose);
-  const { data: goals, isLoading } = useGoals();
+  const router = useRouter();
+  const [isClosing, setIsClosing] = useState(false);
 
-  const goalTitle = goals?.find(
-    (goal: Goal) => goal.goalId === Number(goalId),
-  )?.title;
+  const closeModal = () => {
+    setIsClosing(true);
+    setTimeout(() => {
+      router.back();
+    }, 200);
+  };
+  const [modalRef] = useClickOutside<HTMLDivElement>(closeModal);
 
-  const modalContent = (
+  return createPortal(
     <AnimatePresence>
-      {isOpen && (
+      {!isClosing && (
         <>
           <motion.div
             className="fixed inset-0 z-40 bg-gsBk/50"
             initial={{ opacity: 0 }}
             animate={{ opacity: 1 }}
             exit={{ opacity: 0 }}
-            onClick={onClose}
           />
           <motion.div
             ref={modalRef}
-            className="fixed inset-y-0 right-0 z-40 bg-gs00 p-6 shadow-lg sm:w-full lg:w-[45%] xl:w-[45%]"
+            className="fixed inset-y-0 right-0 z-40 flex max-h-full flex-col bg-gs00 pb-6 shadow-lg sm:w-full lg:w-[45%] xl:w-[45%]"
             initial={{ x: '100%' }}
             animate={{ x: 0 }}
             exit={{ x: '100%' }}
@@ -56,45 +58,64 @@ export default function NoteModal({
             {/* 닫기 버튼 */}
             <button
               type="button"
-              className="mb-4 text-18R text-gs500 hover:text-gsBk"
-              onClick={onClose}
+              className="my-6 size-6 items-center justify-center px-6 text-18R text-gs500 hover:text-gsBk"
+              onClick={closeModal}
             >
-              <FontAwesomeIcon icon={faXmark} />
+              <FontAwesomeIcon icon={faXmark} className="size-3" size="sm" />
             </button>
 
-            {/* 목표 제목 */}
-            <div className="flex items-center justify-between">
-              <h1 className="flex items-center gap-3 text-18SB">
-                <div className="flex size-8 items-center justify-center rounded-full bg-slate100 p-1 text-slate500">
-                  <FontAwesomeIcon icon={faFlag} />
-                </div>
-                {isLoading
-                  ? '목표 로딩 중...'
-                  : goalTitle || '목표를 찾을 수 없습니다.'}
-              </h1>
-            </div>
+            <div className="flex grow flex-col gap-6 overflow-y-auto px-6">
+              <div className="flex flex-col gap-3">
+                {/* 목표 제목 */}
+                {goalTitle && (
+                  <div className="flex items-center justify-between">
+                    <h1 className="flex items-center gap-3 text-16M">
+                      <div className="flex size-8 items-center justify-center rounded-full bg-slate100 p-1 text-slate500">
+                        <FontAwesomeIcon icon={faFlag} />
+                      </div>
+                      {goalTitle}
+                    </h1>
+                  </div>
+                )}
 
-            {/* To do */}
-            <div className="my-2 flex items-center gap-2 text-gs600">
-              <span className="rounded bg-gs200 px-1 text-16M">To do</span>
-              <span className="text-16M">{note.todo}</span>
-              <span className="ml-auto text-14R">{note.date}</span>
-            </div>
+                {/* To do */}
+                <div className="flex items-center gap-2 text-gs700">
+                  <span className="rounded bg-gs200 p-1 text-12M">To do</span>
+                  <span className="text-14R">{todoTitle}</span>
+                  <span className="ml-auto text-12R">{updatedAt}</span>
+                </div>
+              </div>
 
-            {/* 노트 제목 */}
-            <div className="flex items-center justify-between border-t pt-2">
-              <h1 className="flex items-center text-16SB">{note.title}</h1>
-            </div>
+              <div className="flex flex-col gap-4">
+                {/* 노트 제목 */}
+                <div className="flex items-center justify-between border-y py-3">
+                  <h1 className="flex items-center text-18M">{title}</h1>
+                </div>
+                {linkUrl && (
+                  <Link
+                    href={linkUrl}
+                    className="flex w-full items-center gap-2 rounded-3xl bg-gs200 px-2 py-1"
+                  >
+                    <div className="flex size-6 flex-none items-center justify-center rounded-full bg-slate500">
+                      <FontAwesomeIcon
+                        icon={faLink}
+                        className="size-3 text-gs00"
+                      />
+                    </div>
+                    <p className="truncate text-16M">{linkUrl}</p>
+                  </Link>
+                )}
 
-            {/* 내용 */}
-            <div className="mt-3 border-t pt-2">
-              <p className="text-gs600">{note.content}</p>
+                {/* 내용 */}
+                <p className="whitespace-pre-line text-16R text-gs700">
+                  {content}
+                </p>
+              </div>
             </div>
           </motion.div>
         </>
       )}
-    </AnimatePresence>
+    </AnimatePresence>,
+    document.body,
   );
-
-  return ReactDOM.createPortal(modalContent, document.body);
 }
diff --git a/src/services/note.ts b/src/services/note.ts
new file mode 100644
index 00000000..f3e2ab8d
--- /dev/null
+++ b/src/services/note.ts
@@ -0,0 +1,52 @@
+import { notFound } from 'next/navigation';
+import { fetchIntance } from './fetchInstance';
+
+const formatDate = (isoString: string) => {
+  const date = new Date(isoString);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, '0'); // 1월 = 0이므로 +1 필요
+  const day = String(date.getDate()).padStart(2, '0');
+
+  return `${year}.${month}.${day}`;
+};
+
+export interface NoteResponse {
+  id: number;
+  title: string;
+  content: string;
+  linkUrl: string | null;
+  updatedAt: string;
+  todo: {
+    id: number;
+    title: string;
+    done: boolean;
+  };
+  goal: {
+    id: number;
+    title: string;
+  } | null;
+}
+
+export const getNoteDetail = async (noteId: number) => {
+  try {
+    const response = await fetchIntance({
+      base: 'CODEIT',
+      method: 'GET',
+      url: `/notes/${noteId}`,
+    });
+
+    const data: NoteResponse = await response.json();
+
+    return {
+      goalTitle: data.goal?.title,
+      todoTitle: data.todo.title,
+      title: data.title,
+      content: data.content,
+      linkUrl: data.linkUrl,
+      updatedAt: formatDate(data.updatedAt),
+    };
+  } catch {
+    notFound();
+    return null;
+  }
+};
diff --git a/src/utils/renderNotePage.tsx b/src/utils/renderNotePage.tsx
new file mode 100644
index 00000000..e23be98f
--- /dev/null
+++ b/src/utils/renderNotePage.tsx
@@ -0,0 +1,28 @@
+import NoteModal from '@/components/note/NoteModal';
+import { getNoteDetail } from '@/services/note';
+
+export async function renderNoteDetail(noteId: number) {
+  try {
+    const note = await getNoteDetail(noteId);
+
+    return (
+      note && (
+        <NoteModal
+          goalTitle={note.goalTitle}
+          todoTitle={note.todoTitle}
+          title={note.title}
+          content={note.content}
+          updatedAt={note.updatedAt}
+        />
+      )
+    );
+  } catch {
+    return (
+      <div className="flex h-full items-center justify-center">
+        <p className="text-gs60 text-16M">
+          노트를 불러오는 중 오류가 발생했습니다.
+        </p>
+      </div>
+    );
+  }
+}


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

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>src/app/(member)/goals/[id]/note/page.tsx (1)</summary><blockquote>

`11-40`: _🛠️ Refactor suggestion_

**하드코딩된 목업 데이터를 실제 API 호출로 대체해야 합니다.**

노트 목록이 하드코딩되어 있습니다. PR의 목적이 API 연동이라면, 이 부분도 실제 API에서 데이터를 가져오도록 구현해야 합니다. `getNotes` 또는 유사한 서비스 함수를 사용하여 실제 데이터를 가져오는 것이 좋습니다.

```diff
-const notes = [
-  {
-    id: 494,
-    title: '자바스크립트를 배우기 전 알아두어야 할 것',
-    todo: '자바스크립트 기초 1',
-    content: '내용이 어쩌구 저쩌구 내용이 어쩌구 저쩌구',
-    date: '2024.02.04',
-  },
-  // ... 다른 하드코딩된 노트들
-];

+export default async function Page({ params }: { params: { id: string } }) {
+  const goalId = Number(params.id);
+  const notes = await getNotesByGoalId(goalId);
```

</blockquote></details>

</blockquote></details>

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

<details>
<summary>src/app/(member)/@modal/(.)note/[noteId]/page.tsx (2)</summary><blockquote>

`4-8`: **함수 이름이 기능과 일치하지 않습니다.**

`PhotoModal`이라는 함수 이름은 노트를 표시하는 기능과 일치하지 않습니다. 노트 정보를 표시하는 모달이므로 `NoteModal` 또는 `NoteDetailModal`과 같은 이름이 더 적절할 것 같습니다.

```diff
-export default async function PhotoModal({
+export default async function NoteDetailModal({
  params,
}: {
  params: { noteId: string };
}) {
```

---

`1-21`: **로딩 상태에 대한 처리가 필요합니다.**

데이터를 불러오는 동안 사용자에게 로딩 중임을 알려주는 UI가 없습니다. Next.js의 Suspense와 loading.tsx 파일을 활용하여 로딩 상태를 표시하는 것이 좋습니다.


next.js의 Suspense를 활용하거나 별도의 loading.tsx 파일을 추가하여 로딩 상태를 표시할 수 있습니다. 아래는 같은 디렉토리에 추가할 수 있는 loading.tsx 파일의 예시입니다:

```tsx
export default function Loading() {
  return (
    <div className="flex h-full items-center justify-center">
      <p className="text-16M text-gs60">노트를 불러오는 중...</p>
    </div>
  );
}
```

</blockquote></details>
<details>
<summary>src/services/note.ts (1)</summary><blockquote>

`29-51`: **노트 데이터 조회 함수 개선 제안**

노트 상세 정보를 가져오는 함수가 잘 구현되어 있습니다. 다만 몇 가지 개선 사항을 제안합니다:

1. 오류 메시지가 사용자 친화적인 것은 좋지만, 더 구체적인 오류 정보를 포함하면 디버깅에 도움이 될 수 있습니다.
2. 반복적인 요청을 위한 캐싱 메커니즘을 고려해 보세요.
3. 31번 줄의 `fetchIntance`도 올바른 이름으로 수정해야 합니다.

아래와 같이 수정을 제안합니다:

```diff
-    const response = await fetchIntance({
+    const response = await fetchInstance({
```

</blockquote></details>
<details>
<summary>src/components/note/NoteModal.tsx (1)</summary><blockquote>

`29-38`: **모달 닫기 애니메이션 개선 제안**

모달 닫기 애니메이션을 위한 상태 관리와 타이밍 처리가 잘 구현되어 있습니다. 다만, 현재는 애니메이션 지속 시간(200ms)이 하드코딩되어 있어 향후 애니메이션 지속 시간이 변경되면 이 값도 함께 수정해야 합니다.

프레이머 모션(framer-motion)의 AnimatePresence onExitComplete 콜백을 활용하면 애니메이션이 완료된 후 정확히 라우팅을 처리할 수 있습니다.

아래와 같은 개선을 고려해보세요:

```diff
-  const closeModal = () => {
-    setIsClosing(true);
-    setTimeout(() => {
-      router.back();
-    }, 200);
-  };
+  const closeModal = () => {
+    setIsClosing(true);
+  };
+
+  const handleExitComplete = () => {
+    router.back();
+  };

   return createPortal(
-    <AnimatePresence>
+    <AnimatePresence onExitComplete={handleExitComplete}>
```

이렇게 하면 애니메이션 시간에 관계없이 애니메이션이 완료된 후 라우팅이 처리됩니다.

</blockquote></details>

</blockquote></details>

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

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

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

Reviewing files that changed from the base of the PR and between 12eb62927c8ed1fa92f0875d159c5874def7e7e7 and 39ee34b4473f7220073507f023c664fe1515d7f8.

</details>

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

* `src/app/(member)/@modal/(.)note/[noteId]/page.tsx` (1 hunks)
* `src/app/(member)/@modal/default.tsx` (1 hunks)
* `src/app/(member)/goals/[id]/note/page.tsx` (2 hunks)
* `src/app/(member)/layout.tsx` (2 hunks)
* `src/app/(member)/note/[noteId]/page.tsx` (1 hunks)
* `src/app/api/notes/[nodeId]/route.ts` (1 hunks)
* `src/app/layout.tsx` (1 hunks)
* `src/components/common/button/IconButton.tsx` (1 hunks)
* `src/components/note/NoteItem.tsx` (3 hunks)
* `src/components/note/NoteList.tsx` (2 hunks)
* `src/components/note/NoteModal.tsx` (2 hunks)
* `src/components/todoCalendar/TodoListItem.tsx` (1 hunks)
* `src/services/note.ts` (1 hunks)

</details>

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

* src/app/(member)/@modal/default.tsx
* src/app/layout.tsx

</details>

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

<details>
<summary>src/components/common/button/IconButton.tsx (2)</summary>

`20-20`: **스타일 개선이 잘 이루어졌습니다!**

`h-7 w-7`에서 `size-7`로 변경하여 코드가 더 간결해졌습니다. Tailwind의 `size` 유틸리티는 width와 height를 동시에 설정하는 더 간결한 방식입니다.

---

`23-23`: **FontAwesomeIcon의 스타일 속성도 잘 개선되었습니다.**

아이콘에 대해서도 동일하게 `h-4 w-4`에서 `size-4`로 변경하여 일관성 있게 코드가 정리되었습니다.

</details>
<details>
<summary>src/app/(member)/goals/[id]/note/page.tsx (1)</summary>

`55-55`: **NoteList 컴포넌트의 goalId prop 제거는 좋은 변경입니다.**

이전에는 `NoteList`에 `goalId`를 전달했지만, 지금은 제거되었습니다. 이는 컴포넌트 간의 의존성을 줄이고 각 노트 아이템이 자체적으로 라우팅을 처리할 수 있게 하는 좋은 리팩토링으로 보입니다. 이 변경으로 컴포넌트 재사용성이 향상되었습니다.

</details>
<details>
<summary>src/components/todoCalendar/TodoListItem.tsx (1)</summary>

`24-27`: **TodoItem 업데이트 시 목표 정보 보존 구현**

할 일 상태를 토글할 때 이제 `done` 상태뿐만 아니라 `goal` 속성도 함께 업데이트하도록 변경되었습니다. 이는 데이터 일관성을 유지하는 데 중요한 개선사항입니다.

</details>
<details>
<summary>src/app/(member)/layout.tsx (2)</summary>

`5-8`: **Next.js 병렬 라우트를 위한 모달 지원 추가**

레이아웃에 모달 컴포넌트를 렌더링할 수 있도록 `modal` prop이 추가되었습니다. Next.js 병렬 라우트를 활용한 좋은 구현입니다.

---

`17-17`: **모달 렌더링 위치 적절**

모달 컴포넌트가 메인 콘텐츠 영역 내에 적절히 배치되었습니다.

</details>
<details>
<summary>src/components/note/NoteList.tsx (3)</summary>

`12-14`: **NoteListProps에서 goalId 제거**

인터페이스에서 `goalId` 속성이 제거되었습니다. 이는 PR 목표에 언급된 URL 설계 개선과 연관된 변경으로 보입니다.

---

`16-16`: **props 구조 단순화**

NoteList 컴포넌트의 props가 단순화되어 `goalId`가 제거되었습니다. 이는 컴포넌트의 책임을 더 명확하게 만들어 줍니다.

---

`25-25`: **NoteItem 컴포넌트에 goalId 전달 제거**

NoteItem 컴포넌트에 `goalId`를 더 이상 전달하지 않습니다. Link 기반 네비게이션으로 전환했다는 AI 요약과 일치하는 변경입니다.

</details>
<details>
<summary>src/services/note.ts (1)</summary>

`12-27`: **인터페이스 정의가 잘 구성되어 있습니다.**

API 응답을 위한 NoteResponse 인터페이스가 명확하게 정의되어 있으며, 필요한 모든 필드를 포함하고 있습니다. null 값이 될 수 있는 필드에 대한 타입 처리도 적절합니다.

</details>
<details>
<summary>src/components/note/NoteItem.tsx (1)</summary>

`8-9`: **Next.js Link를 활용한 라우팅 개선이 잘 구현되었습니다**

모달 방식에서 Link 컴포넌트를 활용한 라우팅 방식으로 전환한 것은 좋은 접근법입니다. 이는 PR의 목표인 Next Parallel Route를 사용한 URL 변경이 있는 모달 창 구현에 적합합니다. 

작은 개선 사항으로, React의 useState import는 여전히 필요하지만 이전에 모달 상태 관리를 위해 import 했던 다른 불필요한 의존성이 있다면 제거하는 것이 좋겠습니다.



Also applies to: 25-25, 82-87

</details>
<details>
<summary>src/components/note/NoteModal.tsx (2)</summary>

`12-28`: **props 인터페이스 구조 개선이 잘 되었습니다**

NoteModalProps 인터페이스가 전체 note 객체 대신 필요한 특정 필드들을 직접 받도록 변경한 것은 좋은 접근법입니다. 이렇게 하면 컴포넌트가 더 명확하고 타입-세이프하게 사용될 수 있습니다. 필요한 데이터만 전달받기 때문에 의존성이 줄어들고 재사용성이 향상됩니다.

---

`40-120`: **모달 UI 구조와 스타일링이 잘 구현되었습니다**

모달의 UI 구조가 논리적으로 잘 분리되어 있고, 조건부 렌더링(goalTitle, linkUrl)이 적절하게 구현되어 있습니다. 특히 createPortal을 사용하여 모달을 document.body에 직접 렌더링한 것은 z-index 관련 문제를 피하는 좋은 방법입니다.

스크롤 처리와 레이아웃도 잘 구성되어 있어 긴 콘텐츠에도 대응할 수 있습니다. 반응형 디자인도 잘 적용되어 있습니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-01T10:20:02Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>src/services/note.ts (1)</summary><blockquote>

`32-32`: _⚠️ Potential issue_

**잘못된 함수명 사용**

import에서 오타가 발생했던 것과 마찬가지로, 여기에서도 `fetchIntance` 대신 `fetchInstance`를 사용해야 합니다.

```diff
-    const response = await fetchIntance({
+    const response = await fetchInstance({
```

</blockquote></details>

</blockquote></details>

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

<details>
<summary>src/services/note.ts (4)</summary><blockquote>

`4-11`: **함수 유틸리티로 분리 고려**

`formatDate` 함수는 날짜 포맷팅에 유용한 유틸리티 함수입니다. 이런 함수는 나중에 다른 서비스에서도 재사용할 가능성이 높습니다. 별도의 유틸리티 파일로 분리하는 것을 고려해 보세요.

```diff
-const formatDate = (isoString: string) => {
+// 별도의 src/utils/date.ts 파일로 이동
+export const formatDate = (isoString: string) => {
```

---

`30-36`: **noteId 유효성 검사 추가 고려**

API 요청 전에 `noteId`가 유효한 숫자인지 확인하는 검사를 추가하면 안정성이 향상됩니다.

```diff
export const getNoteDetail = async (noteId: number) => {
  try {
+    if (!noteId || isNaN(Number(noteId))) {
+      notFound();
+    }
+    
    const response = await fetchInstance({
```

---

`13-28`: **인터페이스 위치 고려**

`NoteResponse` 인터페이스는 API 응답 형식을 정의합니다. 프로젝트 규모가 커질수록 타입/인터페이스를 별도의 파일에서 관리하면 유지보수가 더 쉬워집니다. `src/types/note.ts`와 같은 파일로 분리하는 것을 고려해 보세요.

---

`30-52`: **에러 처리 구체화 고려**

현재 에러 처리는 모든 예외 상황에 대해 동일하게 `notFound()`를 호출합니다. 하지만 서버 에러(500), 인증 실패(401), 권한 부족(403) 등 다양한 에러 상황이 있을 수 있습니다. 상황에 맞는 에러 처리를 구현하면 사용자 경험이 향상될 수 있습니다.

```diff
  try {
    // ...
-  } catch {
+  } catch (error) {
+    // 에러 종류에 따라 다른 처리
+    if (error.response?.status === 404) {
+      notFound();
+    } else if (error.response?.status === 401) {
+      // 인증 관련 처리
+    } else {
+      // 기타 에러 처리
+      console.error('Note 조회 중 에러 발생:', error);
+      notFound();
+    }
-    notFound();
-    return null;
   }
```

</blockquote></details>

</blockquote></details>

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

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

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

Reviewing files that changed from the base of the PR and between 39ee34b4473f7220073507f023c664fe1515d7f8 and 1e063ee6d05376dd9b85aaf1cb6da8446391a4cb.

</details>

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

* `src/services/note.ts` (1 hunks)

</details>

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

<details>
<summary>src/services/note.ts (1)</summary>

`2-2`: **fetchInstance 오타 수정 필요**

`fetchIntance`의 스펠링이 잘못되었습니다. 정확한 이름은 `fetchInstance`여야 합니다.

아래와 같이 수정해 주세요:

```diff
-import { fetchIntance } from './fetchInstance';
+import { fetchInstance } from './fetchInstance';
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-01T10:23:41Z
user: login: chocorogit
body: 굿굿 잘 구현하신 것 같습니다!
특히 대시보드 페이지라서 
노트 조회를 Parallel Routes 활용해서 
모달 기능 구현한 점이 좋습니다 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-02T12:29:17Z
user: login: kxmmxnzx
body: 혹시 테스트르 어떻게 하나요? 코드는 좋은 것 같습니다!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T12:53:52Z
user: login: jeongeunhq
body: 고생하셨어요!!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-03T15:02:49Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

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

<details>
<summary>src/utils/renderNotePage.tsx (2)</summary><blockquote>

`9-10`: **불필요한 조건부 렌더링을 개선할 수 있습니다.**

현재 `note && (<NoteModal />)` 형태로 조건부 렌더링이 되어 있지만, 이미 앞에서 `await getNoteDetail(noteId)`를 통해 note를 가져온 후에는 note가 존재한다고 가정할 수 있습니다. 오류가 발생할 경우 catch 블록에서 처리되므로, 불필요한 조건부 검사를 제거하여 코드를 더 간결하게 만들 수 있습니다.


```diff
  return (
-    note && (
-      <NoteModal
-        goalTitle={note.goalTitle}
-        todoTitle={note.todoTitle}
-        title={note.title}
-        content={note.content}
-        updatedAt={note.updatedAt}
-      />
-    )
+    <NoteModal
+      goalTitle={note.goalTitle}
+      todoTitle={note.todoTitle}
+      title={note.title}
+      content={note.content}
+      updatedAt={note.updatedAt}
+    />
  );
```

---

`19-19`: **오류 유형에 따른 세분화된 처리를 고려해보세요.**

현재 catch 블록에서 오류 유형을 특정하지 않고 모든 오류에 대해 동일한 메시지를 표시합니다. 오류 유형(예: 404 Not Found, 네트워크 오류 등)에 따라 다른 메시지를 표시하면 사용자에게 더 유용한 정보를 제공할 수 있습니다.


```diff
-  } catch {
+  } catch (error) {
+    console.error('노트 조회 중 오류 발생:', error);
+    
+    // 오류 유형에 따른 다른 메시지 표시
+    let errorMessage = '노트를 불러오는 중 오류가 발생했습니다.';
+    
+    if (error instanceof Response && error.status === 404) {
+      errorMessage = '존재하지 않는 노트입니다.';
+    } else if (error instanceof Error && error.message.includes('network')) {
+      errorMessage = '네트워크 연결을 확인해주세요.';
+    }
+    
     return (
       <div className="flex h-full items-center justify-center">
-        <p className="text-gs60 text-16M">
-          노트를 불러오는 중 오류가 발생했습니다.
-        </p>
+        <p className="text-gs60 text-16M">{errorMessage}</p>
       </div>
     );
   }
```

</blockquote></details>
<details>
<summary>src/components/note/NoteItem.tsx (2)</summary><blockquote>

`8-9`: **useState import 최적화가 필요합니다.**

Link가 추가되고 모달 관련 코드가 제거되었으므로, `useState`는 이제 `isDeleteModalOpen`과 관련해서만 사용됩니다. import 구문을 최적화하여 불필요한 import를 제거하는 것이 좋습니다.


```diff
-import React, { useState } from 'react';
+import React from 'react';
+import { useState } from 'react';
 import Link from 'next/link';
```

---

`21-23`: **Note 인터페이스를 재사용 가능하도록 별도 파일로 분리하는 것이 좋습니다.**

`Note` 인터페이스는 여러 컴포넌트에서 재사용될 가능성이 높습니다. 이를 별도의 `types` 디렉토리로 분리하여 재사용성을 높이는 것이 좋습니다.


```diff
-interface Note {
-  id: number;
-  title: string;
-  todo: string;
-  content: string;
-  date: string;
-}
+import { Note } from '@/types/note';
```

그리고 새로운 파일 생성:
```typescript
// src/types/note.ts
export interface Note {
  id: number;
  title: string;
  todo: string;
  content: string;
  date: string;
}
```

</blockquote></details>
<details>
<summary>src/components/note/NoteModal.tsx (1)</summary><blockquote>

`91-115`: **노트 컨텐츠 영역이 개선되었습니다.**

노트 제목과 내용을 명확하게 구분하고, 링크 URL에 대한 조건부 렌더링과 스타일링이 잘 구현되었습니다. 특히 `whitespace-pre-line` 클래스를 사용하여 줄바꿈을 보존하는 것은 사용자 경험 측면에서 좋은 선택입니다.

다만, 링크 URL이 매우 긴 경우 `truncate` 클래스가 적용되어 있지만, 사용자가 전체 URL을 확인할 수 있는 방법(예: 툴팁)을 고려해보는 것이 좋을 것 같습니다.

</blockquote></details>

</blockquote></details>

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

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

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

Reviewing files that changed from the base of the PR and between 1e063ee6d05376dd9b85aaf1cb6da8446391a4cb and 0937db85f409157feb1e247d483c6a740b3f3763.

</details>

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

* `src/app/(member)/@modal/(.)note/[noteId]/page.tsx` (1 hunks)
* `src/app/(member)/@modal/default.tsx` (1 hunks)
* `src/app/(member)/goals/[id]/note/page.tsx` (2 hunks)
* `src/app/(member)/layout.tsx` (2 hunks)
* `src/app/(member)/note/[noteId]/page.tsx` (1 hunks)
* `src/app/layout.tsx` (1 hunks)
* `src/components/common/button/IconButton.tsx` (1 hunks)
* `src/components/note/NoteItem.tsx` (3 hunks)
* `src/components/note/NoteList.tsx` (2 hunks)
* `src/components/note/NoteModal.tsx` (2 hunks)
* `src/services/note.ts` (1 hunks)
* `src/utils/renderNotePage.tsx` (1 hunks)

</details>

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

* src/app/(member)/@modal/default.tsx
* src/app/layout.tsx
* src/services/note.ts
* src/app/(member)/layout.tsx
* src/app/(member)/goals/[id]/note/page.tsx
* src/components/note/NoteList.tsx
* src/components/common/button/IconButton.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 ESLint</summary>

<details>
<summary>src/components/note/NoteModal.tsx</summary>

[error] 11-11: 'useGoals' is defined but never used.

(no-unused-vars)

---

[error] 11-11: 'useGoals' is defined but never used.

(@typescript-eslint/no-unused-vars)

---

[error] 12-12: 'Goal' is defined but never used.

(no-unused-vars)

---

[error] 12-12: 'Goal' is defined but never used.

(@typescript-eslint/no-unused-vars)

</details>

</details>

</details>

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

<details>
<summary>src/app/(member)/@modal/(.)note/[noteId]/page.tsx (1)</summary>

`1-10`: **모달 렌더링 함수가 잘 구현되었습니다.**

파라미터 타입 정의와 noteId 변환이 올바르게 구현되었으며, 공통 유틸리티 함수를 활용해 코드 중복을 방지하였습니다. 이전 리뷰에서 제안된 오류 처리도 renderNoteDetail 함수에 이미 구현되어 있어 잘 반영되었습니다.

</details>
<details>
<summary>src/app/(member)/note/[noteId]/page.tsx (1)</summary>

`1-10`: **페이지 렌더링 함수가 적절히 구현되었습니다.**

이전 리뷰에서 지적된 함수명 문제가 해결되었으며 (`NoteDetailPage`), 공통 렌더링 로직을 추출하여 코드 중복이 제거되었습니다. 오류 처리 역시 공통 유틸리티 함수에 통합되어 있어 잘 해결되었습니다.

</details>
<details>
<summary>src/utils/renderNotePage.tsx (1)</summary>

`4-28`: **공통 렌더링 로직이 효과적으로 구현되었습니다.**

모달과 페이지 컴포넌트 간의 코드 중복을 제거하고 오류 처리를 통합하여 유지보수성을 향상시켰습니다. try-catch 블록을 사용하여 적절한 오류 처리가 구현되었고, 사용자에게 친숙한 오류 메시지가 제공됩니다.

</details>
<details>
<summary>src/components/note/NoteItem.tsx (3)</summary>

`25-25`: **goalId 제거로 인한 리팩토링이 잘 이루어졌습니다.**

더 이상 사용되지 않는 `goalId` 파라미터가 제거되어 코드가 간결해졌습니다.

---

`82-87`: **Link 컴포넌트로의 전환이 적절하게 이루어졌습니다.**

모달에서 직접 링크로의 전환이 잘 이루어졌으며, 라우팅 구조에 맞게 `/note/${note.id}` 경로가 올바르게 설정되었습니다. 또한 스타일링도 적절히 유지되었습니다.

---

`97-101`: **삭제 기능에 대한 구현이 미완성 상태입니다.**

현재 `handleDelete` 함수는 모달만 닫고 실제 삭제 기능은 구현되어 있지 않습니다. 노트 삭제 API가 구현되면 이 부분을 완성해야 합니다.


삭제 기능 구현이 계획되어 있는지 확인이 필요합니다. API가 준비되면 다음과 같이 구현할 수 있습니다:

```diff
-  const handleDelete = () => {
-    setIsDeleteModalOpen(false);
-  };
+  const handleDelete = async () => {
+    try {
+      await deleteNote(note.id);
+      // 성공적으로 삭제 후 필요한 처리 (예: 목록 새로고침)
+      // 예: router.refresh() 또는 상태 업데이트
+    } catch (error) {
+      console.error('노트 삭제 중 오류 발생:', error);
+      // 오류 처리 (예: 토스트 메시지 표시)
+    } finally {
+      setIsDeleteModalOpen(false);
+    }
+  };
```

</details>
<details>
<summary>src/components/note/NoteModal.tsx (9)</summary>

`4-9`: **import 문이 업데이트되었습니다.**

필요한 아이콘과 기능을 위한 적절한 import 문이 추가되었습니다. 특히 `createPortal`을 사용하여 모달을 document.body에 렌더링하는 것은 z-index 및 스타일 관련 문제를 방지하는 좋은 방법입니다.

---

`14-21`: **NoteModalProps 인터페이스가 개선되었습니다.**

기존의 중첩된 객체 구조 대신 개별 속성을 명시적으로 정의함으로써 타입 체크가 더 명확해졌습니다. 또한 옵셔널 속성(`goalTitle`, `linkUrl`)에 대한 처리가 적절히 추가되었습니다.

---

`23-30`: **컴포넌트 매개변수가 업데이트되었습니다.**

새로운 인터페이스에 맞게 컴포넌트 매개변수가 업데이트되었습니다. 구조분해할당을 통해 각 속성을 명확하게 접근할 수 있도록 했습니다.

---

`31-40`: **모달 닫기 로직이 개선되었습니다.**

애니메이션을 위한 상태(`isClosing`)를 추가하고, `setTimeout`을 통해 애니메이션이 완료된 후 라우팅이 발생하도록 구현한 것은 좋은 UX를 제공합니다. `useRouter`와 `router.back()`을 사용한 네비게이션 방식도 적절합니다.

---

`42-44`: **createPortal을 사용하여 모달 렌더링이 개선되었습니다.**

`createPortal`을 사용하여 모달을 document.body에 렌더링함으로써 z-index 관련 문제를 해결하고, 모달의 위치를 더 유연하게 제어할 수 있습니다. `isClosing` 상태를 활용한 조건부 렌더링도 적절합니다.

---

`54-54`: **모달 스타일 클래스가 업데이트되었습니다.**

반응형 디자인을 위한 width 클래스(sm, lg, xl)를 적용한 것이 좋습니다. 또한 `max-h-full`과 `overflow-y-auto`를 통해 컨텐츠가 화면을 넘어갈 경우를 대비한 것도 적절합니다.

---

`62-67`: **닫기 버튼 스타일이 개선되었습니다.**

버튼의 스타일 클래스가 더 명확해졌으며, 아이콘 크기 지정도 `size-3`로 통일되었습니다. hover 시 색상 변경을 통해 사용자 피드백을 제공하는 것도 좋습니다.

---

`69-89`: **상단 컨텐츠 영역이 체계적으로 구조화되었습니다.**

목표 제목과 할 일 정보를 체계적으로 배치하고, 조건부 렌더링(`goalTitle && (...)`)을 통해 목표 제목이 없는 경우를 처리한 것이 좋습니다. 또한 아이콘과 텍스트의 배치가 시각적으로 잘 정렬되어 있습니다.

---

`120-122`: **createPortal의 대상이 명확하게 지정되었습니다.**

`document.body`를 createPortal의 대상으로 명시적으로 지정한 것은 적절합니다. 이는 모달이 페이지의 다른 요소들에 영향을 받지 않고 독립적으로 렌더링되도록 합니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-04T00:13:45Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

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

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

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

Reviewing files that changed from the base of the PR and between 0937db85f409157feb1e247d483c6a740b3f3763 and 16b486ebced81256f9d15b2882154fe9d0081ffa.

</details>

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

* `src/components/note/NoteModal.tsx` (2 hunks)

</details>

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

<details>
<summary>src/components/note/NoteModal.tsx (13)</summary>

`4-4`: **FontAwesome 아이콘 추가가 적절하게 이루어졌습니다.**

필요한 아이콘들이 정확하게 임포트되었습니다.

---

`6-9`: **필요한 라이브러리와 훅이 적절하게 추가되었습니다.**

Next.js의 routing 기능과 React의 상태 관리가 잘 활용되었습니다. `createPortal`을 사용하여 모달을 body에 렌더링하는 것은 좋은 접근 방식입니다.

---

`12-19`: **인터페이스가 명확하게 정의되었습니다.**

필요한 속성들이 잘 정의되어 있으며, 선택적 속성(`goalTitle`, `linkUrl`)에 대한 타입 처리가 적절합니다.

---

`21-28`: **컴포넌트 파라미터가 인터페이스와 일치합니다.**

모든 필요한 속성들이 적절하게 구조분해할당되었습니다.

---

`29-31`: **상태 관리와 라우팅 설정이 적절합니다.**

모달 닫기 애니메이션을 위한 상태(`isClosing`)와 라우터 설정이 잘 구현되었습니다.

---

`32-38`: **모달 닫기 함수가 효과적으로 구현되었습니다.**

애니메이션 효과를 위한 지연 시간 설정과 라우터 이동이 적절하게 구현되었습니다.

---

`40-43`: **createPortal 사용과 조건부 렌더링이 적절합니다.**

`createPortal`을 사용하여 모달을 DOM 계층 외부에 렌더링하는 것은 좋은 방식입니다. `isClosing` 상태에 따른 조건부 렌더링도 잘 구현되었습니다.

---

`52-52`: **모달 스타일이 반응형으로 잘 구현되었습니다.**

다양한 화면 크기에 대응하는 반응형 클래스가 잘 적용되었습니다.

---

`67-67`: **스크롤 처리가 잘 구현되었습니다.**

긴 내용에 대한 스크롤 처리가 `overflow-y-auto`를 통해 적절하게 구현되었습니다.

---

`70-79`: **목표 제목 조건부 렌더링이 잘 구현되었습니다.**

`goalTitle`이 있을 때만 해당 섹션을 렌더링하는 조건부 처리가 적절합니다.

---

`82-86`: **Todo 정보 표시가 깔끔하게 구현되었습니다.**

할 일 제목과 업데이트 날짜가 적절하게 배치되어 있습니다.

---

`110-112`: **내용 표시가 적절하게 처리되었습니다.**

`whitespace-pre-line` 클래스를 사용하여 줄바꿈이 있는 텍스트를 적절하게 표시하고 있습니다.

---

`118-119`: **createPortal의 대상이 적절하게 지정되었습니다.**

모달을 `document.body`에 렌더링하는 것은 적절한 방식입니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-04T00:23:21Z

Review comments:
diff_hunk: @@ -0,0 +1,21 @@
+import NoteModal from '@/components/note/NoteModal';
+import { getNoteDetail } from '@/services/note';
+
+export default async function PhotoModal({
+  params,
+}: {
+  params: { noteId: string };
+}) {
+  const noteId = Number(params.noteId);
+  const note = await getNoteDetail(noteId);
path: src/app/(member)/@modal/(.)note/[noteId]/page.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**오류 처리 로직이 누락되었습니다.**

`getNoteDetail` 호출 시 발생할 수 있는 예외 상황(존재하지 않는 노트, 서버 오류 등)에 대한 처리가 없습니다. try-catch 블록을 사용하여 오류를 처리하고 사용자에게 적절한 피드백을 제공해야 합니다.

```diff
  const noteId = Number(params.noteId);
-  const note = await getNoteDetail(noteId);
+  try {
+    const note = await getNoteDetail(noteId);
+    
+    return (
+      <NoteModal
+        goalTitle={note.goalTitle}
+        todoTitle={note.todoTitle}
+        title={note.title}
+        content={note.content}
+        updatedAt={note.updatedAt}
+      />
+    );
+  } catch (error) {
+    return (
+      <div className="flex h-full items-center justify-center">
+        <p className="text-16M text-gs60">노트를 불러오는 중 오류가 발생했습니다.</p>
+      </div>
+    );
+  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
  const noteId = Number(params.noteId);
  try {
    const note = await getNoteDetail(noteId);
    
    return (
      <NoteModal
        goalTitle={note.goalTitle}
        todoTitle={note.todoTitle}
        title={note.title}
        content={note.content}
        updatedAt={note.updatedAt}
      />
    );
  } catch (error) {
    return (
      <div className="flex h-full items-center justify-center">
        <p className="text-16M text-gs60">노트를 불러오는 중 오류가 발생했습니다.</p>
      </div>
    );
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-01T10:20:00Z
author_association: NONE
diff_hunk: @@ -0,0 +1,21 @@
+import NoteModal from '@/components/note/NoteModal';
+import { getNoteDetail } from '@/services/note';
+
+export default async function PhotoModal({
+  params,
+}: {
+  params: { noteId: string };
+}) {
+  const noteId = Number(params.noteId);
+  const note = await getNoteDetail(noteId);
+
+  return (
+    <NoteModal
+      goalTitle={note.goalTitle}
+      todoTitle={note.todoTitle}
+      title={note.title}
+      content={note.content}
+      updatedAt={note.updatedAt}
+    />
+  );
+}
path: src/app/(member)/note/[noteId]/page.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**코드 중복이 발생했습니다.**

이 파일은 `src/app/(member)/@modal/(.)note/[noteId]/page.tsx` 파일과 거의 동일합니다. 코드 중복은 유지보수 문제를 일으킬 수 있습니다. 공통 로직을 별도의 유틸리티 함수나 커스텀 훅으로 추출하는 것이 좋습니다.


공통 로직을 분리하기 위해 다음과 같이 별도의 유틸리티 함수를 만들 수 있습니다:

```tsx
// src/utils/note-helpers.tsx
import NoteModal from '@/components/note/NoteModal';
import { getNoteDetail } from '@/services/note';

export async function renderNoteDetail(noteId: number) {
  try {
    const note = await getNoteDetail(noteId);
    
    return (
      <NoteModal
        goalTitle={note.goalTitle}
        todoTitle={note.todoTitle}
        title={note.title}
        content={note.content}
        updatedAt={note.updatedAt}
      />
    );
  } catch (error) {
    return (
      <div className="flex h-full items-center justify-center">
        <p className="text-16M text-gs60">노트를 불러오는 중 오류가 발생했습니다.</p>
      </div>
    );
  }
}
```

그리고 각 페이지에서 이 함수를 활용:

```tsx
// 모달 페이지에서
export default async function NoteDetailModal({ params }: { params: { noteId: string } }) {
  const noteId = Number(params.noteId);
  return await renderNoteDetail(noteId);
}

// 전체 페이지에서
export default async function NoteDetailPage({ params }: { params: { noteId: string } }) {
  const noteId = Number(params.noteId);
  return await renderNoteDetail(noteId);
}
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-01T10:20:00Z
author_association: NONE
diff_hunk: @@ -0,0 +1,21 @@
+import NoteModal from '@/components/note/NoteModal';
+import { getNoteDetail } from '@/services/note';
+
+export default async function PhotoModal({
+  params,
+}: {
+  params: { noteId: string };
+}) {
path: src/app/(member)/note/[noteId]/page.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**함수 이름이 컨텍스트에 맞지 않습니다.**

이 파일은 모달이 아닌 전체 페이지에서 노트 상세 정보를 표시하는데, 함수 이름이 `PhotoModal`인 것은 적절하지 않습니다. 페이지 컨텍스트에서는 `NoteDetailPage`와 같은 이름이 더 적절합니다.

```diff
-export default async function PhotoModal({
+export default async function NoteDetailPage({
  params,
}: {
  params: { noteId: string };
}) {
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
export default async function NoteDetailPage({
  params,
}: {
  params: { noteId: string };
}) {
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-01T10:20:00Z
author_association: NONE
diff_hunk: @@ -0,0 +1,21 @@
+import NoteModal from '@/components/note/NoteModal';
+import { getNoteDetail } from '@/services/note';
+
+export default async function PhotoModal({
+  params,
+}: {
+  params: { noteId: string };
+}) {
+  const noteId = Number(params.noteId);
+  const note = await getNoteDetail(noteId);
path: src/app/(member)/note/[noteId]/page.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**오류 처리 로직이 필요합니다.**

`getNoteDetail` 호출 시 발생할 수 있는 오류에 대한 처리가 없습니다. try-catch 블록을 사용하여 API 호출 오류를 처리하고 사용자에게 적절한 피드백을 제공해야 합니다.

```diff
  const noteId = Number(params.noteId);
-  const note = await getNoteDetail(noteId);
+  try {
+    const note = await getNoteDetail(noteId);
+    
+    return (
+      <NoteModal
+        goalTitle={note.goalTitle}
+        todoTitle={note.todoTitle}
+        title={note.title}
+        content={note.content}
+        updatedAt={note.updatedAt}
+      />
+    );
+  } catch (error) {
+    return (
+      <div className="flex h-full items-center justify-center">
+        <p className="text-16M text-gs60">노트를 불러오는 중 오류가 발생했습니다.</p>
+      </div>
+    );
+  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
  const noteId = Number(params.noteId);
  try {
    const note = await getNoteDetail(noteId);
    
    return (
      <NoteModal
        goalTitle={note.goalTitle}
        todoTitle={note.todoTitle}
        title={note.title}
        content={note.content}
        updatedAt={note.updatedAt}
      />
    );
  } catch (error) {
    return (
      <div className="flex h-full items-center justify-center">
        <p className="text-16M text-gs60">노트를 불러오는 중 오류가 발생했습니다.</p>
      </div>
    );
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-01T10:20:00Z
author_association: NONE
diff_hunk: @@ -0,0 +1,17 @@
+import { NextRequest } from 'next/server';
+import { fetchIntance } from '@/services/fetchInstance';
+
+export async function GET(
+  req: NextRequest,
+  { params }: { params: { nodeId: number } },
+) {
+  const { nodeId } = params;
+
+  const res1 = fetchIntance({
+    base: 'CODEIT',
+    method: 'GET',
+    url: `/notes/${nodeId}`,
+  });
+
+  return res1;
+}
path: src/app/api/notes/[nodeId]/route.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**라우트 매개변수 이름 수정 필요**

라우트 파일명과 매개변수가 일치하지 않습니다. 파일 경로는 `[nodeId]`이지만 목적상 `[noteId]`가 맞습니다. 일관성을 위해 파일 경로명을 수정하거나 코드 내 변수명을 조정해야 합니다.

다음과 같이 수정하세요:

```diff
- { params }: { params: { nodeId: number } }
+ { params }: { params: { noteId: number } }
```

그리고 후속 코드도 수정:

```diff
- const { nodeId } = params;
+ const { noteId } = params;

  const res1 = fetchIntance({
    base: 'CODEIT',
    method: 'GET',
-   url: `/notes/${nodeId}`,
+   url: `/notes/${noteId}`,
  });
```

또는 파일명을 `[noteId]`로 변경하는 것이 바람직합니다.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
import { NextRequest } from 'next/server';
import { fetchIntance } from '@/services/fetchInstance';

export async function GET(
  req: NextRequest,
  { params }: { params: { noteId: number } },
) {
  const { noteId } = params;

  const res1 = fetchIntance({
    base: 'CODEIT',
    method: 'GET',
    url: `/notes/${noteId}`,
  });

  return res1;
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-01T10:20:00Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+import { fetchIntance } from './fetchInstance';
path: src/services/note.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**타입 오류 수정 필요**

`fetchIntance`의 스펠링이 잘못되었습니다. 정확한 이름은 `fetchInstance`여야 합니다.

아래와 같이 수정해 주세요:

```diff
-import { fetchIntance } from './fetchInstance';
+import { fetchInstance } from './fetchInstance';
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
import { fetchInstance } from './fetchInstance';
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-01T10:20:00Z
author_association: NONE
diff_hunk: @@ -0,0 +1,52 @@
+import { notFound } from 'next/navigation';
+import { fetchIntance } from './fetchInstance';
+
+const formatDate = (isoString: string) => {
+  const date = new Date(isoString);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, '0'); // 1월 = 0이므로 +1 필요
+  const day = String(date.getDate()).padStart(2, '0');
+
+  return `${year}.${month}.${day}`;
+};
+
+export interface NoteResponse {
+  id: number;
+  title: string;
+  content: string;
+  linkUrl: string | null;
+  updatedAt: string;
+  todo: {
+    id: number;
+    title: string;
+    done: boolean;
+  };
+  goal: {
+    id: number;
+    title: string;
+  } | null;
+}
+
+export const getNoteDetail = async (noteId: number) => {
+  try {
+    const response = await fetchIntance({
+      base: 'CODEIT',
+      method: 'GET',
+      url: `/notes/${noteId}`,
+    });
+
+    const data: NoteResponse = await response.json();
+
+    return {
+      goalTitle: data.goal?.title,
+      todoTitle: data.todo.title,
+      title: data.title,
+      content: data.content,
+      linkUrl: data.linkUrl,
+      updatedAt: formatDate(data.updatedAt),
+    };
+  } catch {
+    notFound();
+    return null;
+  }
path: src/services/note.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**도달할 수 없는 코드 제거**

`notFound()` 함수는 Next.js에서 404 에러 페이지로 리다이렉트하며 실행 흐름을 중단합니다. 따라서 `return null` 코드는 실행되지 않습니다.

```diff
  } catch {
    notFound();
-    return null;
  }
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
  } catch {
    notFound();
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-01T10:23:40Z
author_association: NONE
diff_hunk: @@ -1,53 +1,57 @@
 'use client';
 
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faFlag, faXmark } from '@fortawesome/free-solid-svg-icons';
+import { faFlag, faLink, faXmark } from '@fortawesome/free-solid-svg-icons';
 import { motion, AnimatePresence } from 'framer-motion';
-import ReactDOM from 'react-dom';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import Link from 'next/link';
+import { createPortal } from 'react-dom';
 import { useClickOutside } from '@/hooks/useClickOutside';
 import { useGoals } from '@/hooks/useGoals';
 import { Goal } from '@/types/goals';
path: src/components/note/NoteModal.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**사용되지 않는 import 문이 있습니다.**

`useGoals`와 `Goal` 타입을 import하고 있으나 코드에서 사용되지 않습니다. 사용하지 않는 import는 제거하는 것이 좋습니다.


```diff
-import { useGoals } from '@/hooks/useGoals';
-import { Goal } from '@/types/goals';
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion

`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 ESLint</summary>

[error] 11-11: 'useGoals' is defined but never used.

(no-unused-vars)

---

[error] 11-11: 'useGoals' is defined but never used.

(@typescript-eslint/no-unused-vars)

---

[error] 12-12: 'Goal' is defined but never used.

(no-unused-vars)

---

[error] 12-12: 'Goal' is defined but never used.

(@typescript-eslint/no-unused-vars)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-04T00:13:44Z
author_association: NONE
diff_hunk: @@ -56,45 +58,64 @@ export default function NoteModal({
             {/* 닫기 버튼 */}
             <button
               type="button"
-              className="mb-4 text-18R text-gs500 hover:text-gsBk"
-              onClick={onClose}
+              className="my-6 size-6 items-center justify-center px-6 text-18R text-gs500 hover:text-gsBk"
+              onClick={closeModal}
path: src/components/note/NoteModal.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**닫기 버튼의 접근성을 개선해야 합니다.**

모달의 닫기 버튼에 접근성 속성이 부족합니다. 스크린 리더 사용자를 위해 `aria-label`을 추가하는 것이 좋습니다.

```diff
<button
  type="button"
  className="my-6 size-6 items-center justify-center px-6 text-18R text-gs500 hover:text-gsBk"
  onClick={closeModal}
+ aria-label="모달 닫기"
>
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
<button
  type="button"
  className="my-6 size-6 items-center justify-center px-6 text-18R text-gs500 hover:text-gsBk"
  onClick={closeModal}
  aria-label="모달 닫기"
>
  {/* ...button content... */}
</button>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-04T00:23:20Z
author_association: NONE
diff_hunk: @@ -56,45 +58,64 @@ export default function NoteModal({
             {/* 닫기 버튼 */}
             <button
               type="button"
-              className="mb-4 text-18R text-gs500 hover:text-gsBk"
-              onClick={onClose}
+              className="my-6 size-6 items-center justify-center px-6 text-18R text-gs500 hover:text-gsBk"
+              onClick={closeModal}
             >
-              <FontAwesomeIcon icon={faXmark} />
+              <FontAwesomeIcon icon={faXmark} className="size-3" size="sm" />
             </button>
 
-            {/* 목표 제목 */}
-            <div className="flex items-center justify-between">
-              <h1 className="flex items-center gap-3 text-18SB">
-                <div className="flex size-8 items-center justify-center rounded-full bg-slate100 p-1 text-slate500">
-                  <FontAwesomeIcon icon={faFlag} />
-                </div>
-                {isLoading
-                  ? '목표 로딩 중...'
-                  : goalTitle || '목표를 찾을 수 없습니다.'}
-              </h1>
-            </div>
+            <div className="flex grow flex-col gap-6 overflow-y-auto px-6">
+              <div className="flex flex-col gap-3">
+                {/* 목표 제목 */}
+                {goalTitle && (
+                  <div className="flex items-center justify-between">
+                    <h1 className="flex items-center gap-3 text-16M">
+                      <div className="flex size-8 items-center justify-center rounded-full bg-slate100 p-1 text-slate500">
+                        <FontAwesomeIcon icon={faFlag} />
+                      </div>
+                      {goalTitle}
+                    </h1>
+                  </div>
+                )}
 
-            {/* To do */}
-            <div className="my-2 flex items-center gap-2 text-gs600">
-              <span className="rounded bg-gs200 px-1 text-16M">To do</span>
-              <span className="text-16M">{note.todo}</span>
-              <span className="ml-auto text-14R">{note.date}</span>
-            </div>
+                {/* To do */}
+                <div className="flex items-center gap-2 text-gs700">
+                  <span className="rounded bg-gs200 p-1 text-12M">To do</span>
+                  <span className="text-14R">{todoTitle}</span>
+                  <span className="ml-auto text-12R">{updatedAt}</span>
+                </div>
+              </div>
 
-            {/* 노트 제목 */}
-            <div className="flex items-center justify-between border-t pt-2">
-              <h1 className="flex items-center text-16SB">{note.title}</h1>
-            </div>
+              <div className="flex flex-col gap-4">
+                {/* 노트 제목 */}
+                <div className="flex items-center justify-between border-y py-3">
+                  <h1 className="flex items-center text-18M">{title}</h1>
+                </div>
+                {linkUrl && (
+                  <Link
+                    href={linkUrl}
+                    className="flex w-full items-center gap-2 rounded-3xl bg-gs200 px-2 py-1"
+                  >
+                    <div className="flex size-6 flex-none items-center justify-center rounded-full bg-slate500">
+                      <FontAwesomeIcon
+                        icon={faLink}
+                        className="size-3 text-gs00"
+                      />
+                    </div>
+                    <p className="truncate text-16M">{linkUrl}</p>
+                  </Link>
+                )}
path: src/components/note/NoteModal.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**링크 URL 처리 보안 개선이 필요합니다.**

외부 URL을 직접 링크로 사용할 때 보안 위험이 있을 수 있습니다. `rel="noopener noreferrer"`를 추가하고, 가능하면 URL의 유효성을 검사하는 것이 좋습니다.

```diff
<Link
  href={linkUrl}
  className="flex w-full items-center gap-2 rounded-3xl bg-gs200 px-2 py-1"
+ rel="noopener noreferrer"
+ target="_blank"
>
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
                {linkUrl && (
                  <Link
                    href={linkUrl}
                    className="flex w-full items-center gap-2 rounded-3xl bg-gs200 px-2 py-1"
                    rel="noopener noreferrer"
                    target="_blank"
                  >
                    <div className="flex size-6 flex-none items-center justify-center rounded-full bg-slate500">
                      <FontAwesomeIcon
                        icon={faLink}
                        className="size-3 text-gs00"
                      />
                    </div>
                    <p className="truncate text-16M">{linkUrl}</p>
                  </Link>
                )}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-04T00:23:20Z
author_association: NONE

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

## Walkthrough
이번 PR은 노트 세부 정보 표시와 관련된 여러 컴포넌트와 함수들을 추가 및 수정합니다. 새 비동기 함수인 `NoteDetailModal`과 `NoteDetailPage`가 도입되어 `noteId`를 숫자로 변환한 후 `renderNoteDetail` 함수를 호출합니다. 또한, 모달과 관련된 레이아웃에 `modal` prop이 추가되고, 노트 관련 컴포넌트에서 불필요한 `goalId` prop 제거 및 기능 변경, 스타일 클래스 개선 등이 이루어졌습니다. 추가로, 노트 서비스 파일(`note.ts`)과 관련 유틸 함수(`renderNoteDetail`)가 새로 생성되었습니다.

## Changes

| 파일 경로 | 변경 요약 |
| --- | --- |
| `src/app/(member)/@modal/(.)note/[noteId]/page.tsx`<br>`src/app/(member)/note/[noteId]/page.tsx` | `NoteDetailModal`와 `NoteDetailPage` 비동기 함수 추가: `noteId`를 숫자로 변환 후 `renderNoteDetail` 호출 |
| `src/app/(member)/@modal/default.tsx` | 기본 `Default` 함수 추가 (null 반환) |
| `src/app/(member)/goals/[id]/note/page.tsx` | 노트 배열 첫 요소 id 변경 (1 → 494) 및 `NoteList` 컴포넌트에 전달되는 props에서 `goalId` 제거 |
| `src/app/(member)/layout.tsx` | 레이아웃 컴포넌트에 `modal` prop 추가 및 렌더링 구조 수정 |
| `src/app/layout.tsx` | `RootLayout`의 `ClientProvider` 자식 요소에 `<div>` 래퍼 추가 (구조 변경) |
| `src/components/common/button/IconButton.tsx` | 버튼 및 아이콘의 클래스 이름 수정 (기존 크기 클래스에서 `size-7`, `size-4`로 변경) |
| `src/components/note/NoteItem.tsx`<br>`src/components/note/NoteList.tsx` | `goalId` prop 및 관련 코드 제거, `NoteModal` 컴포넌트 삭제 후 `Link` 컴포넌트로 대체 |
| `src/components/note/NoteModal.tsx` | `NoteModalProps` 인터페이스 전면 개편: 기존 프로퍼티 제거, 새로운 프로퍼티 추가 및 닫기 로직 변경, 조건부 `Link` 렌더링 도입 |
| `src/services/note.ts` | 새로운 note 서비스 파일 추가: `formatDate` 함수, `NoteResponse` 인터페이스, `getNoteDetail` 비동기 함수 도입 |
| `src/utils/renderNotePage.tsx` | `renderNoteDetail` 함수 추가: `getNoteDetail` 호출 후 `NoteModal` 렌더링, 에러 발생 시 대체 메시지 표시 |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant U as 사용자
    participant ND as NoteDetailModal/NoteDetailPage
    participant RND as renderNoteDetail
    participant NS as getNoteDetail (서비스)
    participant NM as NoteModal

    U->>ND: noteId 요청
    ND->>RND: renderNoteDetail(noteId) 호출
    RND->>NS: getNoteDetail(noteId) 요청
    NS-->>RND: 노트 세부 정보 응답 또는 에러
    alt 성공
        RND->>NM: NoteModal에 세부 정보 전달
        NM-->>RND: 렌더링된 모달 반환
    else 실패
        RND-->>ND: 에러 메시지 반환
    end
    RND-->>ND: 최종 컴포넌트 반환
    ND-->>U: 노트 세부 정보 표시
```

## Possibly related PRs
- **ICan-5/ICan-Frontend#33**: `renderNoteDetail` 함수를 사용하는 `NoteDetailModal` 추가와 관련된 코드 변경.
- **ICan-5/ICan-Frontend#30**: 모달 내 노트 렌더링 로직 및 props 수정과 직접 연관된 변경 사항.
- **ICan-5/ICan-Frontend#27**: `NoteDetailModal`과 `renderNoteDetail` 함수 도입에 따른 노트 세부 정보 처리 방식 변경과 연결.

## Suggested reviewers
- jeongeunhq
- kxmmxnzx

## Poem
> 나는 귀여운 토끼, 코드의 숲을 달려가요,  
> 새 함수와 모달 속에 비밀이 담겨 있네요,  
> 노트 세부 정보 춤추듯 불러내며,  
> 한 줄 한 줄 스며드는 변화의 멜로디,  
> 당근 같은 기쁨을 품은 코드 속에서,  
> 🐰 토끼의 발자국을 따라 함께 웃어요,  
> CodeRabbit과 함께, 변화의 축제를 즐겨요!

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

---

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

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

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai):

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

### 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](https://docs.coderabbit.ai/guides/configure-coderabbit) 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`

### 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: NONE
user: login: netlify[bot]
created_at: 2025-03-01T10:15:42Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *ican-5* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 16b486ebced81256f9d15b2882154fe9d0081ffa |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/ican-5/deploys/67c64737318e4000080ceda2 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-35--ican-5.netlify.app](https://deploy-preview-35--ican-5.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTM1LS1pY2FuLTUubmV0bGlmeS5hcHAifQ.XXr1DhIaowOHFs5wKCrIQxZyGWz1DWenOiZ31QvBSbw)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [ican-5 Preview](https://deploy-preview-35--ican-5.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/ican-5/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: izodam
created_at: 2025-03-03T14:44:53Z
body: > 혹시 테스트르 어떻게 하나요? 코드는 좋은 것 같습니다!

아직 노트 생성 api가 머지되기 전이라서 저는 따로 생성 api 호출 후 확인했습니다...!! 
author_association: COLLABORATOR