diff --git a/src/app/(landing)/_components/CTASection.tsx b/src/app/(landing)/_components/CTASection.tsx index 7432de9..04ee3e6 100644 --- a/src/app/(landing)/_components/CTASection.tsx +++ b/src/app/(landing)/_components/CTASection.tsx @@ -53,7 +53,7 @@ export default function CTASection() { 오늘의 할 일, Slice로 계획해요 router.push("/login")} + onClick={() => router.push("/dashboard")} className="bg-orange-250 cursor-pointer rounded-full px-10 py-3.5 text-base font-semibold text-white shadow-lg transition-all duration-300 hover:bg-[#FF7043] sm:px-16 sm:py-4 sm:text-lg" whileHover={{ scale: 1.05, diff --git a/src/app/(landing)/_components/HeroSection.tsx b/src/app/(landing)/_components/HeroSection.tsx index 8e6932a..d28f4c1 100644 --- a/src/app/(landing)/_components/HeroSection.tsx +++ b/src/app/(landing)/_components/HeroSection.tsx @@ -17,7 +17,7 @@ export default function HeroSection() { }, []); return ( -
+
router.push("/login")} + onClick={() => router.push("/dashboard")} className="bg-orange-250 cursor-pointer rounded-full px-10 py-3.5 text-base font-semibold text-white shadow-lg transition-all duration-300 hover:bg-[#FF7043] sm:px-16 sm:py-4 sm:text-lg" whileHover={{ scale: 1.05, diff --git a/src/app/(protected)/notes/_components/NoteCreateContainer.tsx b/src/app/(protected)/notes/_components/NoteCreateContainer.tsx index 7b346ef..bd19ba5 100644 --- a/src/app/(protected)/notes/_components/NoteCreateContainer.tsx +++ b/src/app/(protected)/notes/_components/NoteCreateContainer.tsx @@ -25,9 +25,15 @@ export default function NoteCreateContainer({ const { data: todo } = useTodoQuery(todoId); const { mutate: createNoteMutation, isPending } = useCreateNoteMutation(); - const form = useNoteForm({ - todoId, - }); + const { + form, + embed, + draft, + loadModal, + changeTitle, + changeContent, + changeLinkUrl, + } = useNoteForm({ todoId }); const handleSubmit = () => { if (!form.title.trim()) { @@ -40,16 +46,19 @@ export default function NoteCreateContainer({ return; } + const trimmedLinkUrl = form.linkUrl?.trim(); + createNoteMutation( { todoId, title: form.title.trim(), content: JSON.stringify(form.content), - ...(form.linkUrl.trim() && { linkUrl: form.linkUrl.trim() }), + ...(trimmedLinkUrl && { linkUrl: trimmedLinkUrl }), }, { onSuccess: (data) => { draftNoteStorage.remove(todoId); + toast.success("노트가 작성되었습니다."); router.replace(`/notes?goalId=${data.goal.id}`); }, onError: (error) => { @@ -79,7 +88,7 @@ export default function NoteCreateContainer({ } @@ -87,16 +96,16 @@ export default function NoteCreateContainer({ } /> - {form.hasDraftNote && ( + {draft.hasNote && (
)} @@ -105,24 +114,24 @@ export default function NoteCreateContainer({ content={form.content} linkUrl={form.linkUrl} linkMetadata={form.linkMetadata} - isEmbedOpen={form.isEmbedOpen} - onChangeTitle={form.handleTitleChange} - onChangeContent={form.handleContentChange} - onChangeLinkUrl={form.handleLinkUrlChange} - onToggleEmbed={form.handleToggleEmbed} - onDeleteLinkPreview={form.handleDeleteLinkPreview} + isEmbedOpen={embed.isOpen} + onChangeTitle={changeTitle} + onChangeContent={changeContent} + onChangeLinkUrl={changeLinkUrl} + onToggleEmbed={embed.toggle} + onDeleteLinkPreview={embed.deletePreview} metaInfo={metaInfo} - hasDraftNote={form.hasDraftNote} - onLoadDraft={form.handleLoadModalOpen} - onCloseDraftCallout={form.handleDraftCalloutClose} + hasDraftNote={draft.hasNote} + onLoadDraft={loadModal.open} + onCloseDraftCallout={draft.closeCallout} />
- {form.isLoadModalOpen && ( + {loadModal.isOpen && ( )} diff --git a/src/app/(protected)/notes/_components/NoteDetailSkeleton.tsx b/src/app/(protected)/notes/_components/NoteDetailSkeleton.tsx index 55ac21a..4d4e43a 100644 --- a/src/app/(protected)/notes/_components/NoteDetailSkeleton.tsx +++ b/src/app/(protected)/notes/_components/NoteDetailSkeleton.tsx @@ -2,21 +2,21 @@ export default function NoteDetailSkeleton() { return (
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
); diff --git a/src/app/(protected)/notes/_components/NoteEditContainer.tsx b/src/app/(protected)/notes/_components/NoteEditContainer.tsx index af36bb8..c9daf2e 100644 --- a/src/app/(protected)/notes/_components/NoteEditContainer.tsx +++ b/src/app/(protected)/notes/_components/NoteEditContainer.tsx @@ -24,7 +24,15 @@ export default function NoteEditContainer({ noteId }: NoteEditContainerProps) { const todoId = note.todo.id; - const form = useNoteForm({ + const { + form, + embed, + draft, + loadModal, + changeTitle, + changeContent, + changeLinkUrl, + } = useNoteForm({ todoId, isEditMode: true, initialData: { @@ -46,18 +54,21 @@ export default function NoteEditContainer({ noteId }: NoteEditContainerProps) { return; } + const trimmedLinkUrl = form.linkUrl?.trim(); + updateNoteMutation( { noteId, data: { title: form.title.trim(), content: JSON.stringify(form.content), - linkUrl: form.linkUrl.trim() || null, + linkUrl: trimmedLinkUrl || null, }, }, { onSuccess: (data) => { draftNoteStorage.remove(todoId); + toast.success("노트가 수정되었습니다."); router.replace(`/notes?goalId=${data.goal.id}`); }, onError: (error) => { @@ -87,7 +98,7 @@ export default function NoteEditContainer({ noteId }: NoteEditContainerProps) { } @@ -95,16 +106,16 @@ export default function NoteEditContainer({ noteId }: NoteEditContainerProps) { } /> - {form.hasDraftNote && ( + {draft.hasNote && (
)} @@ -113,24 +124,24 @@ export default function NoteEditContainer({ noteId }: NoteEditContainerProps) { content={form.content} linkUrl={form.linkUrl} linkMetadata={form.linkMetadata} - isEmbedOpen={form.isEmbedOpen} - onChangeTitle={form.handleTitleChange} - onChangeContent={form.handleContentChange} - onChangeLinkUrl={form.handleLinkUrlChange} - onToggleEmbed={form.handleToggleEmbed} - onDeleteLinkPreview={form.handleDeleteLinkPreview} + isEmbedOpen={embed.isOpen} + onChangeTitle={changeTitle} + onChangeContent={changeContent} + onChangeLinkUrl={changeLinkUrl} + onToggleEmbed={embed.toggle} + onDeleteLinkPreview={embed.deletePreview} metaInfo={metaInfo} - hasDraftNote={form.hasDraftNote} - onLoadDraft={form.handleLoadModalOpen} - onCloseDraftCallout={form.handleDraftCalloutClose} + hasDraftNote={draft.hasNote} + onLoadDraft={loadModal.open} + onCloseDraftCallout={draft.closeCallout} />
- {form.isLoadModalOpen && ( + {loadModal.isOpen && ( )} diff --git a/src/app/(protected)/notes/_components/NoteEditorForm.tsx b/src/app/(protected)/notes/_components/NoteEditorForm.tsx index efec160..361b326 100644 --- a/src/app/(protected)/notes/_components/NoteEditorForm.tsx +++ b/src/app/(protected)/notes/_components/NoteEditorForm.tsx @@ -18,7 +18,7 @@ import { NoteLinkPreview } from "./NoteLinkPreview"; interface NoteEditorFormProps { title: string; content: JSONContent | null; - linkUrl: string; + linkUrl: string | null; linkMetadata: LinkMetadata | null; isEmbedOpen: boolean; onChangeTitle: (e: React.ChangeEvent) => void; @@ -72,12 +72,12 @@ export default function NoteEditorForm({ const countWithoutSpace = text.replace(/\s+/g, "").length; const handleOpenLinkModal = () => { - setTempLinkUrl(linkUrl); + setTempLinkUrl(linkUrl ?? ""); setIsLinkModalOpen(true); }; const handleCloseLinkModal = () => { - setTempLinkUrl(linkUrl); + setTempLinkUrl(linkUrl ?? ""); setIsLinkModalOpen(false); }; diff --git a/src/app/(protected)/notes/_components/NoteItemSkeleton.tsx b/src/app/(protected)/notes/_components/NoteItemSkeleton.tsx index bb96555..a08744c 100644 --- a/src/app/(protected)/notes/_components/NoteItemSkeleton.tsx +++ b/src/app/(protected)/notes/_components/NoteItemSkeleton.tsx @@ -2,11 +2,11 @@ export default function NoteItemSkeleton() { return (
-
+
-
-
+
+
); diff --git a/src/app/(protected)/notes/_components/NoteListContainer.tsx b/src/app/(protected)/notes/_components/NoteListContainer.tsx index 368232b..18ca5ec 100644 --- a/src/app/(protected)/notes/_components/NoteListContainer.tsx +++ b/src/app/(protected)/notes/_components/NoteListContainer.tsx @@ -6,6 +6,7 @@ import { useRouter } from "next/navigation"; import EmptyState from "@/components/common/empty-state/EmptyState"; import { EMPTY_MESSAGES } from "@/constants/messages"; import ConfirmModal from "@/components/common/popup-modal/ConfirmModal"; +import { toast } from "@/lib/toast"; import GoalBanner from "./GoalBanner"; import NoteList from "./NoteList"; @@ -40,11 +41,12 @@ export default function NoteListContainer({ goalId }: NoteListContainerProps) { onSuccess: () => { setDeleteNoteId(null); setIsDeleteModalOpen(false); + toast.success("노트가 삭제되었습니다."); }, onError: (error) => { console.error("삭제 실패:", error); - alert("노트 삭제에 실패했습니다."); + toast.error("노트 삭제에 실패했습니다."); }, }, ); diff --git a/src/app/(protected)/notes/_hooks/useNoteForm.ts b/src/app/(protected)/notes/_hooks/useNoteForm.ts index 324afbf..76cb531 100644 --- a/src/app/(protected)/notes/_hooks/useNoteForm.ts +++ b/src/app/(protected)/notes/_hooks/useNoteForm.ts @@ -5,17 +5,21 @@ import { LinkMetadata } from "@/api/types/note"; import { toast } from "@/lib/toast"; import { draftNoteStorage } from "@/app/(protected)/notes/_utils/draft-note"; +interface NoteFormData { + title: string; + content: JSONContent | null; + linkUrl: string | null; + linkMetadata: LinkMetadata | null; +} + interface UseNoteFormOptions { todoId: number; isEditMode?: boolean; - initialData?: { - title: string; - content: JSONContent; - linkUrl: string | null; - linkMetadata: LinkMetadata | null; - }; + initialData?: Partial; } +const AUTO_SAVE_INTERVAL = 5 * 60 * 1000; + export function useNoteForm({ todoId, isEditMode = false, @@ -24,20 +28,25 @@ export function useNoteForm({ const { mutate: getLinkMetadataMutation } = useLinkMetadataMutation(); const isInitialized = useRef(false); - const [title, setTitle] = useState(""); - const [content, setContent] = useState(null); - const [linkUrl, setLinkUrl] = useState(""); - const [linkMetadata, setLinkMetadata] = useState(null); + const [form, setForm] = useState({ + title: "", + content: null, + linkUrl: null, + linkMetadata: null, + }); + const [isEmbedOpen, setIsEmbedOpen] = useState(false); const [hasDraftNote, setHasDraftNote] = useState(false); const [isLoadModalOpen, setIsLoadModalOpen] = useState(false); useEffect(() => { if (initialData && !isInitialized.current) { - setTitle(initialData.title); - setContent(initialData.content); - setLinkUrl(initialData.linkUrl ?? ""); - setLinkMetadata(initialData.linkMetadata ?? null); + setForm({ + title: initialData.title ?? "", + content: initialData.content ?? null, + linkUrl: initialData.linkUrl ?? null, + linkMetadata: initialData.linkMetadata ?? null, + }); isInitialized.current = true; } }, [initialData]); @@ -49,131 +58,127 @@ export function useNoteForm({ useEffect(() => { if (isEditMode || !todoId) return; - const interval = setInterval( - () => { - if (title.trim() || content) { - draftNoteStorage.save(todoId, { - title, - content: content || { type: "doc", content: [] }, - linkUrl, - linkMetadata, - }); - setHasDraftNote(true); - toast.success("임시 저장이 완료되었습니다", { hasTime: true }); - } - }, - 5 * 60 * 1000, - ); + const interval = setInterval(() => { + if (form.title.trim() || form.content) { + draftNoteStorage.save(todoId, { + title: form.title, + content: form.content || { type: "doc", content: [] }, + linkUrl: form.linkUrl, + linkMetadata: form.linkMetadata, + }); + setHasDraftNote(true); + toast.success("임시 저장이 완료되었습니다", { hasTime: true }); + } + }, AUTO_SAVE_INTERVAL); return () => clearInterval(interval); - }, [isEditMode, todoId, title, content, linkUrl, linkMetadata]); + }, [isEditMode, todoId, form]); - const handleTitleChange = (e: React.ChangeEvent) => { - setTitle(e.target.value); + const changeTitle = (e: React.ChangeEvent) => { + setForm((prev) => ({ ...prev, title: e.target.value })); }; - const handleContentChange = (newContent: JSONContent) => { - setContent(newContent); + const changeContent = (newContent: JSONContent) => { + setForm((prev) => ({ ...prev, content: newContent })); }; - const handleLinkUrlChange = (url: string) => { - setLinkUrl(url); - + const changeLinkUrl = (url: string) => { if (!url.trim()) { - setLinkMetadata(null); + setForm((prev) => ({ + ...prev, + linkUrl: null, + linkMetadata: null, + })); setIsEmbedOpen(false); return; } + setForm((prev) => ({ ...prev, linkUrl: url })); + try { new URL(url); getLinkMetadataMutation(url, { onSuccess: (data) => { - setLinkMetadata(data); + setForm((prev) => ({ ...prev, linkMetadata: data })); }, onError: (error) => { console.error("링크 메타데이터 가져오기 실패:", error); toast.error("링크 정보를 가져올 수 없습니다."); }, }); - } catch (error) {} - }; - - const handleToggleEmbed = () => { - setIsEmbedOpen(!isEmbedOpen); - }; - - const handleDeleteLinkPreview = () => { - setLinkUrl(""); - setLinkMetadata(null); - setIsEmbedOpen(false); - }; - - const handleDraft = () => { - if (!title.trim() && !content) return; - - draftNoteStorage.save(todoId, { - title, - content: content || { type: "doc", content: [] }, - linkUrl, - linkMetadata, - }); - - setHasDraftNote(true); - toast.success("임시 저장이 완료되었습니다", { hasTime: true }); - }; - - const handleLoadModalOpen = () => { - setIsLoadModalOpen(true); + } catch (error) { + console.error("유효하지 않은 URL:", error); + } }; - const handleLoadModalClose = () => { - setIsLoadModalOpen(false); + const embed = { + isOpen: isEmbedOpen, + toggle: () => setIsEmbedOpen(!isEmbedOpen), + deletePreview: () => { + setForm((prev) => ({ + ...prev, + linkUrl: null, + linkMetadata: null, + })); + setIsEmbedOpen(false); + }, }; - const handleDraftCalloutClose = () => { - setHasDraftNote(false); + const loadModal = { + isOpen: isLoadModalOpen, + open: () => setIsLoadModalOpen(true), + close: () => setIsLoadModalOpen(false), }; - const handleConfirmLoadDraft = () => { - const draftNote = draftNoteStorage.get(todoId); + const draft = { + hasNote: hasDraftNote, + save: () => { + if (!form.title.trim() && !form.content) return; - if (draftNote) { - setTitle(draftNote.title); - setContent(draftNote.content); - setLinkUrl(draftNote.linkUrl); - setLinkMetadata(draftNote.linkMetadata ?? null); - } + draftNoteStorage.save(todoId, { + title: form.title, + content: form.content || { type: "doc", content: [] }, + linkUrl: form.linkUrl, + linkMetadata: form.linkMetadata, + }); - draftNoteStorage.remove(todoId); - setIsLoadModalOpen(false); - setHasDraftNote(false); - }; + setHasDraftNote(true); + toast.success("임시 저장이 완료되었습니다", { hasTime: true }); + }, + load: () => { + const draftNote = draftNoteStorage.get(todoId); + + if (!draftNote) { + loadModal.close(); + return; + } + + setForm({ + title: draftNote.title, + content: draftNote.content, + linkUrl: draftNote.linkUrl, + linkMetadata: draftNote.linkMetadata ?? null, + }); - const getDraftTitle = () => { - const draftNote = draftNoteStorage.get(todoId); - return draftNote?.title.trim() || "제목 없음"; + draftNoteStorage.remove(todoId); + loadModal.close(); + setHasDraftNote(false); + }, + getTitle: () => { + const draftNote = draftNoteStorage.get(todoId); + return draftNote?.title.trim() || "제목 없음"; + }, + closeCallout: () => setHasDraftNote(false), }; return { - title, - content, - linkUrl, - linkMetadata, - isEmbedOpen, - hasDraftNote, - isLoadModalOpen, - handleTitleChange, - handleContentChange, - handleLinkUrlChange, - handleToggleEmbed, - handleDeleteLinkPreview, - handleDraft, - handleLoadModalOpen, - handleLoadModalClose, - handleDraftCalloutClose, - handleConfirmLoadDraft, - getDraftTitle, + form, + embed, + draft, + loadModal, + changeTitle, + changeContent, + changeLinkUrl, }; } diff --git a/src/app/(protected)/notes/_utils/draft-note.ts b/src/app/(protected)/notes/_utils/draft-note.ts index 32f1216..cf0d35a 100644 --- a/src/app/(protected)/notes/_utils/draft-note.ts +++ b/src/app/(protected)/notes/_utils/draft-note.ts @@ -5,7 +5,7 @@ export interface DraftNote { todoId: number; title: string; content: JSONContent; - linkUrl: string; + linkUrl: string | null; savedAt: string; linkMetadata?: LinkMetadata | null; } diff --git a/src/app/globals.css b/src/app/globals.css index 2c0e646..12fff74 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -10,7 +10,6 @@ --color-gray-50: #f5f5f5; --color-gray-80: #e6e6e6; --color-gray-100: #dddddd; - --color-gray-150: #e6e6e6; --color-gray-200: #cccccc; --color-gray-300: #bbbbbb; --color-gray-400: #a4a4a4;