Repository: Quesddo/Client
Pull request #159: feat(TODO-208): [노트 작성/수정] 임시저장 workflow 개선
Author: login: euNung24
Labels: name: ✨ feat
name: ✂ refactor
Created: 2025-03-06T09:40:41Z | Merged: 2025-03-07T00:00:58Z

## 🔗 연관된 이슈

- [연관된 이슈 링크](https://quesddo.atlassian.net/browse/TODO-208)
  
<br/>

## 📗 작업 내용

- 임시저장
  - 불러오기 시 임시저장 제목 관련 팝업 모달 추가
  - 노트 작성/수정 완료 시 임시저장 노트는 제거
  - localStorage + Interval 처리가 함께 있던 hooks 분리
  - 하나의 노트만 저장되던 오류 처리
  - 토스트가 두 번 불러지는 오류 처리

- NoteDrawer에 있던 InputProvider 제거 (최상단 컴포넌트에 있기 때문)
- 컬러 팔레트 위치 조정

<br/>


## 💬 리뷰 요구사항(선택)

- 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요





Diff:
diff --git a/src/hooks/note/useAutoSaveNoteDraft.ts b/src/hooks/note/useAutoSaveNoteDraft.ts
new file mode 100644
index 00000000..68e61206
--- /dev/null
+++ b/src/hooks/note/useAutoSaveNoteDraft.ts
@@ -0,0 +1,71 @@
+import { useEffect, useRef } from "react";
+import { type UseFormReturn } from "react-hook-form";
+
+import { CreateNoteBodyDto, UpdateNoteBodyDto } from "@/types/types";
+
+import useToast from "../useToast";
+import useNoteStorage from "./useNoteStorage";
+
+interface UseAutoSaveNoteDraftProps<
+  TNoteBody extends CreateNoteBodyDto | UpdateNoteBodyDto,
+> {
+  id: number;
+  methods: UseFormReturn<TNoteBody>;
+  isEditMode: boolean;
+}
+
+const TOAST_INTERVAL_TIME = 1000 * 60 * 5;
+
+export default function useAutoSaveNoteDraft<
+  TNoteBody extends CreateNoteBodyDto | UpdateNoteBodyDto,
+>({ id, methods, isEditMode }: UseAutoSaveNoteDraftProps<TNoteBody>) {
+  const {
+    formState: { isDirty },
+  } = methods;
+  const { addToast } = useToast();
+  const { saveDraftNote } = useNoteStorage({ id, isEditMode });
+
+  const toastIntervalRef = useRef<NodeJS.Timeout>(null);
+
+  const saveDraftNoteAndShowToast = () => {
+    saveDraftNote(methods.getValues());
+    addToast({
+      content: "임시 저장이 완료되었습니다",
+    });
+  };
+
+  const removeInterval = () => {
+    if (toastIntervalRef.current) {
+      clearInterval(toastIntervalRef.current);
+      toastIntervalRef.current = null;
+    }
+  };
+
+  const addInterval = () => {
+    removeInterval();
+
+    toastIntervalRef.current = setInterval(() => {
+      saveDraftNoteAndShowToast();
+    }, TOAST_INTERVAL_TIME);
+  };
+
+  const handleClickSaveDraft = () => {
+    saveDraftNoteAndShowToast();
+    addInterval();
+  };
+
+  // 첫 데이터 입력 시 임시저장 토스트 시작
+  useEffect(() => {
+    if (isDirty) {
+      addInterval();
+    }
+
+    return () => {
+      removeInterval();
+    };
+  }, [isDirty]);
+
+  return {
+    handleClickSaveDraft,
+  };
+}
diff --git a/src/hooks/note/useNoteDraft.ts b/src/hooks/note/useNoteDraft.ts
deleted file mode 100644
index e4585166..00000000
--- a/src/hooks/note/useNoteDraft.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { useEffect, useRef } from "react";
-import { type Path, type PathValue, type UseFormReturn } from "react-hook-form";
-
-import { CreateNoteBodyDto, UpdateNoteBodyDto } from "@/types/types";
-import {
-  CREATE_NOTE_STORAGE,
-  NoteStorage,
-  UPDATE_NOTE_STORAGE,
-} from "@/views/note/note-form/utils/noteStorage";
-
-import useToast from "../useToast";
-
-interface UseNoteDraftProps<
-  TNoteBody extends CreateNoteBodyDto | UpdateNoteBodyDto,
-> {
-  id: number;
-  methods: UseFormReturn<TNoteBody>;
-  isEditMode: boolean;
-}
-
-const TOAST_INTERVAL_TIME = 1000 * 60 * 5;
-
-export default function useNoteDraft<
-  TNoteBody extends CreateNoteBodyDto | UpdateNoteBodyDto,
->({ id, isEditMode, methods }: UseNoteDraftProps<TNoteBody>) {
-  const { watch } = methods;
-  const { addToast } = useToast();
-  const toastIntervalRef = useRef<NodeJS.Timeout>(null);
-
-  const saveDraft = () => {
-    const values = methods.getValues();
-    const noteStorage =
-      "todoId" in values ? CREATE_NOTE_STORAGE : UPDATE_NOTE_STORAGE;
-
-    if ("todoId" in values) {
-      noteStorage.set(id, values);
-    } else {
-      (noteStorage as NoteStorage<UpdateNoteBodyDto>).set(id, values);
-    }
-  };
-
-  const handleSaveAndToast = () => {
-    saveDraft();
-    addToast({
-      content: "임시 저장이 완료되었습니다",
-    });
-  };
-
-  const addInterval = () => {
-    toastIntervalRef.current = setInterval(() => {
-      handleSaveAndToast();
-    }, TOAST_INTERVAL_TIME);
-  };
-
-  const removeInterval = () => {
-    if (toastIntervalRef.current) {
-      clearInterval(toastIntervalRef.current);
-    }
-  };
-
-  const checkFormkValueChange = () => {
-    if (!toastIntervalRef.current) {
-      addInterval();
-    }
-  };
-
-  const handleClickSaveDraft = () => {
-    removeInterval();
-    handleSaveAndToast();
-    addInterval();
-  };
-
-  const handleLoadNoteDraft = () => {
-    const noteStorage = isEditMode ? UPDATE_NOTE_STORAGE : CREATE_NOTE_STORAGE;
-
-    const data = noteStorage.get(id) as TNoteBody | null;
-
-    if (data) {
-      Object.keys(data).forEach((key) => {
-        methods.setValue(
-          key as Path<TNoteBody>,
-          data[key as keyof TNoteBody] as PathValue<TNoteBody, Path<TNoteBody>>,
-        );
-      });
-    }
-  };
-
-  const isNoteDraftSaved = () => {
-    const noteStorage = isEditMode ? UPDATE_NOTE_STORAGE : CREATE_NOTE_STORAGE;
-
-    return !!noteStorage.get(id);
-  };
-
-  // form 내용 변화 감지
-  useEffect(() => {
-    const { unsubscribe } = watch(checkFormkValueChange);
-
-    return () => unsubscribe();
-  }, [watch]);
-
-  return { handleClickSaveDraft, handleLoadNoteDraft, isNoteDraftSaved };
-}
diff --git a/src/hooks/note/useNoteStorage.ts b/src/hooks/note/useNoteStorage.ts
new file mode 100644
index 00000000..0f7f302b
--- /dev/null
+++ b/src/hooks/note/useNoteStorage.ts
@@ -0,0 +1,31 @@
+import { CreateNoteBodyDto, UpdateNoteBodyDto } from "@/types/types";
+import {
+  CREATE_NOTE_STORAGE,
+  NoteStorage,
+  UPDATE_NOTE_STORAGE,
+} from "@/views/note/note-form/utils/noteStorage";
+
+interface UseNoteDraftProps {
+  id: number;
+  isEditMode: boolean;
+}
+
+export default function useNoteStorage({ id, isEditMode }: UseNoteDraftProps) {
+  const noteStorage = isEditMode ? UPDATE_NOTE_STORAGE : CREATE_NOTE_STORAGE;
+
+  const saveDraftNote = (values: CreateNoteBodyDto | UpdateNoteBodyDto) => {
+    if ("todoId" in values) {
+      noteStorage.set(id, values);
+    } else {
+      (noteStorage as NoteStorage<UpdateNoteBodyDto>).set(id, values);
+    }
+  };
+
+  return {
+    saveDraftNote,
+    isNoteDraftSaved: () => !!noteStorage.get(id),
+    getDraftNoteData: () =>
+      noteStorage.get(id) as CreateNoteBodyDto | UpdateNoteBodyDto | null,
+    removeNoteDraft: noteStorage.remove,
+  };
+}
diff --git a/src/views/note/editor/ReactQuillEditor.tsx b/src/views/note/editor/ReactQuillEditor.tsx
index f3b5c5a8..894af092 100644
--- a/src/views/note/editor/ReactQuillEditor.tsx
+++ b/src/views/note/editor/ReactQuillEditor.tsx
@@ -21,6 +21,8 @@ const ReactQuillEditor = dynamic(
           "relative flex min-h-0 flex-1 flex-col-reverse gap-4",
           // toolbar, link item
           "[&_.ql-toolbar]:rounded-[22px] [&_.ql-toolbar.ql-snow]:!flex [&_.ql-toolbar.ql-snow_.ql-formats]:last:ml-auto [&_.ql-toolbar.ql-snow_.ql-formats]:last:rounded-full [&_.ql-toolbar.ql-snow_.ql-formats]:last:bg-slate-200",
+          // color palette
+          "[&_.ql-snow_.ql-picker.ql-expanded_.ql-picker-options]:!top-auto [&_.ql-snow_.ql-picker.ql-expanded_.ql-picker-options]:!bottom-[100%]",
           // content container
           "[&>.ql-container]:min-h-0 [&>.ql-container]:!text-base [&>.ql-container]:!font-normal [&>.ql-container.ql-snow]:!border-none",
           // content, content placeholder
diff --git a/src/views/note/note-drawer/NoteDrawer.tsx b/src/views/note/note-drawer/NoteDrawer.tsx
index 725d1245..e413c2bf 100644
--- a/src/views/note/note-drawer/NoteDrawer.tsx
+++ b/src/views/note/note-drawer/NoteDrawer.tsx
@@ -6,6 +6,7 @@ import { ErrorBoundary } from "react-error-boundary";
 import ExitBtn from "@/components/atoms/exit-btn/ExitBtn";
 import Spinner from "@/components/atoms/spinner/Spinner";
 import ErrorFallback from "@/components/molecules/error-fallback/ErrorFallback";
+import BoundaryWrapper from "@/components/organisms/boundary-wrapper/BoundaryWrapper";
 import { InputModalProvider } from "@/contexts/InputModalContext";
 
 import NoteCreationForm from "../note-form/NoteCreationForm";
@@ -52,14 +53,10 @@ export default function NoteDrawer() {
     <div className="fixed inset-0 z-20 bg-black/50">
       <section className="fixed inset-0 flex flex-col gap-4 bg-white p-6 sm:left-auto sm:w-[512px] sm:border-l sm:border-slate-200 md:w-[800px]">
         <ExitBtn onClick={handleClick} />
-        <InputModalProvider>
-          <ErrorBoundary FallbackComponent={ErrorFallback}>
-            <Suspense fallback={<Spinner size={80} />}>
-              {mode === MODE.CREATE && <NoteCreationForm todoId={todoId} />}
-              {mode === MODE.EDIT && <NoteUpdateForm noteId={noteId} />}
-            </Suspense>
-          </ErrorBoundary>
-        </InputModalProvider>
+        <BoundaryWrapper>
+          {mode === MODE.CREATE && <NoteCreationForm todoId={todoId} />}
+          {mode === MODE.EDIT && <NoteUpdateForm noteId={noteId} />}
+        </BoundaryWrapper>
       </section>
     </div>
   );
diff --git a/src/views/note/note-form/DraftNoteReminderToast.tsx b/src/views/note/note-form/DraftNoteReminderToast.tsx
new file mode 100644
index 00000000..4794d805
--- /dev/null
+++ b/src/views/note/note-form/DraftNoteReminderToast.tsx
@@ -0,0 +1,113 @@
+import { useMemo, useState } from "react";
+import { useFormContext } from "react-hook-form";
+
+import Button from "@/components/atoms/button/Button";
+import ExitBtn from "@/components/atoms/exit-btn/ExitBtn";
+import Popup from "@/components/molecules/popup/Popup";
+import useNoteStorage from "@/hooks/note/useNoteStorage";
+import { CreateNoteBodyDto, UpdateNoteBodyDto } from "@/types/types";
+
+interface DraftNoteReminderToastProps {
+  id: number;
+  isEditMode: boolean;
+}
+
+type NoteBodyKeyType = (keyof (CreateNoteBodyDto | UpdateNoteBodyDto))[];
+
+export default function DraftNoteReminderToast({
+  id,
+  isEditMode,
+}: DraftNoteReminderToastProps) {
+  const methods = useFormContext();
+
+  const { isNoteDraftSaved, getDraftNoteData } = useNoteStorage({
+    id,
+    isEditMode,
+  });
+
+  const [isOpenToast, setIsOpenToast] = useState(isNoteDraftSaved());
+  const [isOpenPopup, setIsOpenPopup] = useState(false);
+  const draftNoteData = useMemo(() => getDraftNoteData(), [isOpenPopup]);
+
+  const loadNoteDraft = () => {
+    const data = getDraftNoteData();
+
+    if (data) {
+      (Object.keys(data) as NoteBodyKeyType).forEach((key) => {
+        methods.setValue(key as string, data[key], {
+          shouldValidate: true,
+          shouldDirty: false,
+        });
+      });
+    }
+  };
+
+  const handleCloseToast = () => {
+    setIsOpenToast(false);
+  };
+
+  const handleOpenPopUp = () => {
+    setIsOpenPopup(true);
+  };
+
+  const handleClosePopup = () => {
+    setIsOpenPopup(false);
+  };
+
+  const handleConfirmPopup = () => {
+    loadNoteDraft();
+    handleClosePopup();
+  };
+
+  return (
+    isOpenToast && (
+      <>
+        <div
+          className="flex items-center justify-between gap-3 rounded-[28px] bg-blue-50 px-3 py-2.5 text-blue-500 data-[open=false]:hidden"
+          data-open={isOpenToast}
+        >
+          <div className="flex items-center gap-4">
+            <button onClick={handleCloseToast}>
+              <img
+                src="/icons/delete_circle.png"
+                alt="삭제"
+                width={24}
+                height={24}
+              />
+            </button>
+            <p>임시 저장된 노트가 있어요. 저장된 노트를 불러오시겠어요?</p>
+          </div>
+          <Button
+            variant="outline"
+            size="xs"
+            rounded
+            className="shrink-0"
+            onClick={handleOpenPopUp}
+          >
+            불러오기
+          </Button>
+        </div>
+        {isOpenPopup && (
+          <div>
+            <Popup
+              onClose={handleClosePopup}
+              onConfirm={handleConfirmPopup}
+              isCancelEnabled
+            >
+              <div className="flex flex-col pt-2">
+                <ExitBtn
+                  className="absolute top-6 right-6"
+                  onClick={handleClosePopup}
+                />
+                <p className="flex w-full flex-col items-center">
+                  <span>‘{draftNoteData?.title || "제목 없음"}’</span>
+                  <span>제목의 노트를 불러오시겠어요?</span>
+                </p>
+              </div>
+            </Popup>
+          </div>
+        )}
+      </>
+    )
+  );
+}
diff --git a/src/views/note/note-form/Editor.tsx b/src/views/note/note-form/Editor.tsx
index 49b6f90d..ce8a0b3b 100644
--- a/src/views/note/note-form/Editor.tsx
+++ b/src/views/note/note-form/Editor.tsx
@@ -37,6 +37,9 @@ export default function Editor() {
     methods.setValue(
       "plainText",
       editorRef.current?.getEditor().getText().replace(/\n+$/, ""),
+      {
+        shouldDirty: false,
+      },
     );
   };
 
@@ -48,12 +51,12 @@ export default function Editor() {
   }, []);
 
   return (
-    <Controller
-      control={methods.control}
-      name="content"
-      rules={{ required: true }}
-      render={({ field: { onChange, value } }) => (
-        <>
+    <>
+      <Controller
+        control={methods.control}
+        name="content"
+        rules={{ required: true }}
+        render={({ field: { onChange, value } }) => (
           <ReactQuillEditor
             forwardedRef={setEditorRef}
             theme="snow"
@@ -65,9 +68,9 @@ export default function Editor() {
             value={value}
             placeholder="내용을 입력하세요"
           ></ReactQuillEditor>
-          <input type="hidden" {...methods.register("plainText")} />
-        </>
-      )}
-    />
+        )}
+      />
+      <input type="hidden" defaultValue="" onChange={handleChangePlainText} />
+    </>
   );
 }
diff --git a/src/views/note/note-form/LinkModal.tsx b/src/views/note/note-form/LinkModal.tsx
index ab3980bb..e53c2bfe 100644
--- a/src/views/note/note-form/LinkModal.tsx
+++ b/src/views/note/note-form/LinkModal.tsx
@@ -1,4 +1,4 @@
-import { register } from "module";
+import { ChangeEventHandler } from "react";
 import { useFormContext } from "react-hook-form";
 
 import InputModal from "@/components/organisms/modal/InputModal";
@@ -11,12 +11,22 @@ export default function LinkModal({}) {
   const { closeModal } = useModalContext();
   const methods = useFormContext();
 
-  const resetTempUrl = () => {
-    methods.resetField("tempLinkUrl");
+  const resetTempUrl = () => {};
+
+  const handleChangeTempLinkUrl: ChangeEventHandler = (e) => {
+    methods.setValue(
+      "tempLinkUrl",
+      (e.currentTarget as HTMLInputElement).value,
+      {
+        shouldDirty: false,
+      },
+    );
   };
 
   const handleSubmitLink = () => {
-    methods.setValue("linkUrl", methods.getValues("tempLinkUrl"));
+    methods.setValue("linkUrl", methods.getValues("tempLinkUrl"), {
+      shouldDirty: true,
+    });
     resetTempUrl();
     closeModal();
   };
@@ -33,7 +43,8 @@ export default function LinkModal({}) {
           <InputModal.Label>링크</InputModal.Label>
           <InputModal.TextInput
             placeholder="링크를 입력하세요."
-            {...methods.register("tempLinkUrl", {})}
+            defaultValue=""
+            onChange={handleChangeTempLinkUrl}
           />
         </div>
         <input
diff --git a/src/views/note/note-form/NoteCreationForm.tsx b/src/views/note/note-form/NoteCreationForm.tsx
index ce23adf3..7e70464a 100644
--- a/src/views/note/note-form/NoteCreationForm.tsx
+++ b/src/views/note/note-form/NoteCreationForm.tsx
@@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
 
 import { todoApi } from "@/apis/todoApi";
 import useCreateNote from "@/hooks/note/useCreateNote";
+import useNoteStorage from "@/hooks/note/useNoteStorage";
 import { CreateNoteBodyDto } from "@/types/types";
 
 import NoteForm from "./NoteForm";
@@ -14,7 +15,14 @@ interface NoteCreationFormProps {
 }
 
 export default function NoteCreationForm({ todoId }: NoteCreationFormProps) {
-  const methods = useForm<CreateNoteBodyDto>();
+  const methods = useForm<CreateNoteBodyDto>({
+    defaultValues: {
+      title: "",
+      todoId,
+      content: "",
+      linkUrl: "",
+    },
+  });
   const mutation = useCreateNote();
   const pathname = usePathname();
   const router = useRouter();
@@ -24,11 +32,17 @@ export default function NoteCreationForm({ todoId }: NoteCreationFormProps) {
     queryFn: () => todoApi.fetchTodo(todoId),
   });
 
+  const { removeNoteDraft } = useNoteStorage({
+    id: todoId,
+    isEditMode: false,
+  });
+
   const handleSubmit = async (data: CreateNoteBodyDto) => {
     mutation.mutate(data, {
       onSuccess: (data) => {
         const queryParams = new URLSearchParams();
 
+        removeNoteDraft(todoId);
         queryParams.set("noteId", data.id.toString());
         queryParams.set("mode", "detail");
         router.push(`${pathname}?${queryParams.toString()}`);
diff --git a/src/views/note/note-form/NoteForm.tsx b/src/views/note/note-form/NoteForm.tsx
index 04ea69d1..d482ba23 100644
--- a/src/views/note/note-form/NoteForm.tsx
+++ b/src/views/note/note-form/NoteForm.tsx
@@ -3,11 +3,11 @@ import { FormProvider, type UseFormReturn } from "react-hook-form";
 
 import Button from "@/components/atoms/button/Button";
 import PageTitle from "@/components/atoms/page-title/PageTitle";
-import useNoteDraft from "@/hooks/note/useNoteDraft";
+import useAutoSaveNoteDraft from "@/hooks/note/useAutoSaveNoteDraft";
 import { CreateNoteBodyDto, UpdateNoteBodyDto } from "@/types/types";
+import DraftNoteReminderToast from "@/views/note/note-form/DraftNoteReminderToast";
 import Editor from "@/views/note/note-form/Editor";
-import InputWithCount from "@/views/note/note-form/TitleCounter";
-import ToastBtn from "@/views/note/note-form/ToastBtn";
+import TitleWithCounter from "@/views/note/note-form/TitleWithCounter";
 
 import EditorTextCounter from "./EditorTextCounter";
 import GoalTodoDisplay from "./GoalTodoDisplay";
@@ -35,12 +35,11 @@ export default function NoteForm<
   todo,
   children,
 }: NoteFormProps<TNoteBody>) {
-  const { handleClickSaveDraft, handleLoadNoteDraft, isNoteDraftSaved } =
-    useNoteDraft({
-      id,
-      methods,
-      isEditMode: editMode,
-    });
+  const { handleClickSaveDraft } = useAutoSaveNoteDraft({
+    id,
+    methods,
+    isEditMode: editMode,
+  });
 
   const {
     formState: { isValid },
@@ -75,12 +74,9 @@ export default function NoteForm<
         </div>
         <div className="flex flex-col gap-4 pt-[11px] pb-[24px] sm:pt-4 md:flex-col-reverse">
           <GoalTodoDisplay goal={goal} todo={todo} />
-          <ToastBtn
-            isOpen={isNoteDraftSaved()}
-            onLoadData={handleLoadNoteDraft}
-          />
+          <DraftNoteReminderToast id={id} isEditMode={editMode} />
         </div>
-        <InputWithCount />
+        <TitleWithCounter />
         <div className="flex min-h-0 flex-1 flex-col gap-2">
           <EditorTextCounter />
           <LinkDisplay />
diff --git a/src/views/note/note-form/NoteUpdateForm.tsx b/src/views/note/note-form/NoteUpdateForm.tsx
index e3d93646..f7a3e1d6 100644
--- a/src/views/note/note-form/NoteUpdateForm.tsx
+++ b/src/views/note/note-form/NoteUpdateForm.tsx
@@ -1,10 +1,10 @@
 import { useSuspenseQuery } from "@tanstack/react-query";
 import { usePathname } from "next/navigation";
 import { useRouter } from "next/router";
-import { useEffect } from "react";
 import { useForm } from "react-hook-form";
 
 import noteApi from "@/apis/noteApi";
+import useNoteStorage from "@/hooks/note/useNoteStorage";
 import useUpdateNote from "@/hooks/note/useUpdateNote";
 import { UpdateNoteBodyDto } from "@/types/types";
 
@@ -15,7 +15,6 @@ interface NoteUpdateFormProps {
 }
 
 export default function NoteUpdateForm({ noteId }: NoteUpdateFormProps) {
-  const methods = useForm<UpdateNoteBodyDto>();
   const mutation = useUpdateNote(noteId);
   const pathname = usePathname();
   const router = useRouter();
@@ -25,11 +24,25 @@ export default function NoteUpdateForm({ noteId }: NoteUpdateFormProps) {
     queryFn: () => noteApi.fetchNote(noteId),
   });
 
+  const methods = useForm<UpdateNoteBodyDto>({
+    defaultValues: {
+      title: data.title,
+      content: data.content,
+      linkUrl: data.linkUrl,
+    },
+  });
+
+  const { removeNoteDraft } = useNoteStorage({
+    id: noteId,
+    isEditMode: true,
+  });
+
   const handleSubmit = (data: UpdateNoteBodyDto) => {
     mutation.mutate(data, {
       onSuccess: (data) => {
         const queryParams = new URLSearchParams();
 
+        removeNoteDraft(noteId);
         queryParams.set("noteId", data.id.toString());
         queryParams.set("mode", "detail");
         router.push(`${pathname}?${queryParams.toString()}`);
@@ -37,13 +50,6 @@ export default function NoteUpdateForm({ noteId }: NoteUpdateFormProps) {
     });
   };
 
-  useEffect(() => {
-    if (!data) return;
-    methods.setValue("title", data.title, { shouldValidate: true });
-    methods.setValue("content", data.content, { shouldValidate: true });
-    methods.setValue("linkUrl", data.linkUrl);
-  }, [data, methods]);
-
   return (
     <NoteForm
       id={noteId}
diff --git a/src/views/note/note-form/TitleCounter.tsx b/src/views/note/note-form/TitleWithCounter.tsx
similarity index 95%
rename from src/views/note/note-form/TitleCounter.tsx
rename to src/views/note/note-form/TitleWithCounter.tsx
index b8ae6a19..f9273523 100644
--- a/src/views/note/note-form/TitleCounter.tsx
+++ b/src/views/note/note-form/TitleWithCounter.tsx
@@ -5,7 +5,7 @@ import { cn } from "@/utils/cn";
 
 const MAX_TITLE_COUNT = 30;
 
-export default function TitleCounter() {
+export default function TitleWithCounter() {
   const { register, watch } = useFormContext();
   const title = watch("title");
 
diff --git a/src/views/note/note-form/ToastBtn.tsx b/src/views/note/note-form/ToastBtn.tsx
deleted file mode 100644
index 69c29ae4..00000000
--- a/src/views/note/note-form/ToastBtn.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { useState } from "react";
-
-import Button from "@/components/atoms/button/Button";
-
-interface ToastBtnProps {
-  isOpen: boolean;
-  onLoadData: () => void;
-}
-
-export default function ToastBtn({ isOpen, onLoadData }: ToastBtnProps) {
-  const [isOpenToast, setIsOpenToast] = useState(isOpen);
-
-  const handleClose = () => {
-    setIsOpenToast(false);
-  };
-
-  return (
-    <div
-      className="flex items-center justify-between gap-3 rounded-[28px] bg-blue-50 px-3 py-2.5 text-blue-500 data-[open=false]:hidden"
-      data-open={isOpenToast}
-    >
-      <div className="flex items-center gap-4">
-        <button onClick={handleClose}>
-          <img
-            src="/icons/delete_circle.png"
-            alt="삭제"
-            width={24}
-            height={24}
-          />
-        </button>
-        <p>임시 저장된 노트가 있어요. 저장된 노트를 불러오시겠어요?</p>
-      </div>
-      <Button
-        variant="outline"
-        size="xs"
-        rounded
-        className="shrink-0"
-        onClick={onLoadData}
-      >
-        불러오기
-      </Button>
-    </div>
-  );
-}
diff --git a/src/views/note/note-form/utils/noteStorage.ts b/src/views/note/note-form/utils/noteStorage.ts
index 32f85f28..4a8ea148 100644
--- a/src/views/note/note-form/utils/noteStorage.ts
+++ b/src/views/note/note-form/utils/noteStorage.ts
@@ -15,9 +15,12 @@ export interface NoteStorage<T extends CreateNoteBodyDto | UpdateNoteBodyDto> {
 
 export const CREATE_NOTE_STORAGE: NoteStorage<CreateNoteBodyDto> = {
   set: (todoId, data) => {
+    const storageData = localStorage.getItem(NOTE_DRAFT_CREATE_KEY);
+    const storedData = storageData ? JSON.parse(storageData) : {};
+
     localStorage.setItem(
       NOTE_DRAFT_CREATE_KEY,
-      JSON.stringify({ [todoId]: { ...data, todoId } }),
+      JSON.stringify({ ...storedData, [todoId]: { ...data, todoId } }),
     );
   },
   get: (todoId) => {
@@ -39,9 +42,12 @@ export const CREATE_NOTE_STORAGE: NoteStorage<CreateNoteBodyDto> = {
 
 export const UPDATE_NOTE_STORAGE: NoteStorage<UpdateNoteBodyDto> = {
   set: (noteId: number, data: UpdateNoteBodyDto) => {
+    const storageData = localStorage.getItem(NOTE_DRAFT_UPDATE_KEY);
+    const storedData = storageData ? JSON.parse(storageData) : {};
+
     localStorage.setItem(
       NOTE_DRAFT_UPDATE_KEY,
-      JSON.stringify({ [noteId]: { ...data, noteId } }),
+      JSON.stringify({ ...storedData, [noteId]: { ...data, noteId } }),
     );
   },
   get: (noteId: number) => {
@@ -56,7 +62,7 @@ export const UPDATE_NOTE_STORAGE: NoteStorage<UpdateNoteBodyDto> = {
       const parsedData = JSON.parse(data) as NoteStorageDataType;
       delete parsedData[noteId];
 
-      localStorage.setItem(NOTE_DRAFT_CREATE_KEY, JSON.stringify(parsedData));
+      localStorage.setItem(NOTE_DRAFT_UPDATE_KEY, JSON.stringify(parsedData));
     }
   },
 };


Reviews:
user: login: thgee
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T12:26:48Z
user: login: euNung24
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T12:27:58Z
user: login: hdayeon
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T12:31:36Z

Review comments:
diff_hunk: @@ -0,0 +1,71 @@
+import { useEffect, useRef } from "react";
+import { type UseFormReturn } from "react-hook-form";
+
+import { CreateNoteBodyDto, UpdateNoteBodyDto } from "@/types/types";
+
+import useToast from "../useToast";
+import useNoteStorage from "./useNoteStorage";
+
+interface UseAutoSaveNoteDraftProps<
+  TNoteBody extends CreateNoteBodyDto | UpdateNoteBodyDto,
+> {
+  id: number;
+  methods: UseFormReturn<TNoteBody>;
+  isEditMode: boolean;
+}
+
+const TOAST_INTERVAL_TIME = 1000 * 60 * 5;
+
+export default function useAutoSaveNoteDraft<
+  TNoteBody extends CreateNoteBodyDto | UpdateNoteBodyDto,
+>({ id, methods, isEditMode }: UseAutoSaveNoteDraftProps<TNoteBody>) {
+  const {
+    formState: { isDirty },
+  } = methods;
path: src/hooks/note/useAutoSaveNoteDraft.ts
user: login: thgee
body: isDirty라니.. 신기한 속성이 있군요
created_at: 2025-03-06T12:24:40Z
author_association: MEMBER
path: src/hooks/note/useAutoSaveNoteDraft.ts
user: login: thgee
body: 요구사항에는 없지만, 노트 작성중에 페이지를 나가려고 할 때 beforeunload 이벤트 사용해서 임시저장 하게 하는 것도 리팩토링 때 해보면 좋을 것 같아요 !!
created_at: 2025-03-06T12:26:44Z
author_association: MEMBER
path: src/hooks/note/useAutoSaveNoteDraft.ts
user: login: euNung24
body: 아....! 그거 안써놨더니 까먹었군요 ㅎ.ㅎ 
기록해놔야겠어요👍
created_at: 2025-03-06T12:27:58Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-06T09:40:52Z
body: @thgee, @hdayeon 님 랜덤 리뷰어로 당첨되셨습니다! 리뷰를 부탁드립니다. 🙏
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T09:42:26Z
body: 🎨 Storybook: [스토리북 바로가기](https://67acd5f79f2abb10c31d0c10-rpwfipwqgr.chromatic.com/)
⏰ Update: 2025년 03월 06일 18시 42분 11초
<!-- thollander/actions-comment-pull-request "159-storybook" -->
author_association: CONTRIBUTOR