diff --git a/components/Link/AddLinkInput.tsx b/components/Link/AddLinkInput.tsx index e66fbe5..8eefa39 100644 --- a/components/Link/AddLinkInput.tsx +++ b/components/Link/AddLinkInput.tsx @@ -4,6 +4,9 @@ import { Modal } from "../modal/modalManager/ModalManager"; import Image from "next/image"; import SubmitButton from "../SubMitButton"; import useModalStore from "@/store/useModalStore"; +import toast from "react-hot-toast"; +import toastMessages from "@/lib/toastMessage"; +import { urlRegex } from "@/util/regex"; const AddLinkInput = ({ folderList }: FolderListData) => { const { isOpen, openModal } = useModalStore(); @@ -14,8 +17,14 @@ const AddLinkInput = ({ folderList }: FolderListData) => { }; const handleClick = () => { - openModal("AddModal", { list: folderList, link: link }); - setLink(""); + if (link === "") { + toast.error(toastMessages.error.inputLink); + } else if (!urlRegex.test(link.trim())) { + toast.error(toastMessages.error.invalidLink); + } else { + openModal("AddModal", { list: folderList, link: link }); + setLink(""); + } }; const handleKeyDown = (e: KeyboardEvent) => { diff --git a/components/Search/SearchInput.tsx b/components/Search/SearchInput.tsx index b73fa1c..36272a5 100644 --- a/components/Search/SearchInput.tsx +++ b/components/Search/SearchInput.tsx @@ -1,6 +1,8 @@ import { ChangeEvent, FormEvent, useState } from "react"; import { useRouter } from "next/router"; import Image from "next/image"; +import toast from "react-hot-toast"; +import toastMessages from "@/lib/toastMessage"; export const SearchInput = () => { const router = useRouter(); diff --git a/components/modal/AddFolderModal.tsx b/components/modal/AddFolderModal.tsx index a6a1d2f..cbeecfb 100644 --- a/components/modal/AddFolderModal.tsx +++ b/components/modal/AddFolderModal.tsx @@ -4,6 +4,8 @@ import ModalContainer from "./modalComponents/ModalContainer"; import ModalInput from "./modalComponents/ModalInput"; import useModalStore from "@/store/useModalStore"; import SubmitButton from "../SubMitButton"; +import toast from "react-hot-toast"; +import toastMessages from "@/lib/toastMessage"; const AddFolderModal = ({ folderName }: { folderName: string }) => { const [value, setValue] = useState(""); @@ -11,7 +13,12 @@ const AddFolderModal = ({ folderName }: { folderName: string }) => { const { closeModal } = useModalStore(); const handleChange = (e: ChangeEvent) => { - setValue(e.target.value); + const newValue = e.target.value; + if (newValue.length > 5) { + toast.error(toastMessages.error.limitFolderNameLength); + } else { + setValue(newValue); + } }; const handleSubmit = async () => { const body = { @@ -20,8 +27,9 @@ const AddFolderModal = ({ folderName }: { folderName: string }) => { if (value !== "") { try { await postFolders(body); + toast.success(toastMessages.success.addFolder); } catch (error) { - console.log(error, "폴더 생성 에러"); + toast.error(toastMessages.error.addFolder); } } closeModal(); diff --git a/components/modal/AddModal.tsx b/components/modal/AddModal.tsx index 035c046..12be93d 100644 --- a/components/modal/AddModal.tsx +++ b/components/modal/AddModal.tsx @@ -5,20 +5,29 @@ import SubmitButton from "../SubMitButton"; import { useState } from "react"; import { postLink } from "@/lib/api/link"; import useModalStore from "@/store/useModalStore"; +import toast from "react-hot-toast"; +import toastMessages from "@/lib/toastMessage"; +import { useRouter } from "next/router"; const AddModal = ({ list, link }: { list: FolderItemType[]; link: string }) => { const [selectedId, setSelectedId] = useState(null); const { closeModal } = useModalStore(); + const route = useRouter(); + const handleSubmit = async () => { const body = { folderId: Number(selectedId), url: link, }; - if (link !== "" && selectedId) { + if (!selectedId) { + toast.error(toastMessages.error.selectFolder); + } else { try { await postLink(body); + toast.success(toastMessages.success.addLink); + route.push(`/link?folder=${selectedId}`); } catch (error) { - console.log(error, "링크 생성 에러"); + toast.error(toastMessages.error.addLink); } finally { closeModal(); } diff --git a/components/modal/DeleteFolderModal.tsx b/components/modal/DeleteFolderModal.tsx index dbcc6fc..437706f 100644 --- a/components/modal/DeleteFolderModal.tsx +++ b/components/modal/DeleteFolderModal.tsx @@ -2,6 +2,8 @@ import useModalStore from "@/store/useModalStore"; import SubmitButton from "../SubMitButton"; import ModalContainer from "./modalComponents/ModalContainer"; import { deleteFolder } from "@/lib/api/folder"; +import toast from "react-hot-toast"; +import toastMessages from "@/lib/toastMessage"; const DeleteFolderModal = ({ folderName, @@ -11,13 +13,20 @@ const DeleteFolderModal = ({ folderId: number; }) => { const { closeModal } = useModalStore(); + let linkCount: number; const handleSubmit = async () => { - // 폴더 내에 링크 개수 0 일때만 삭제 가능 -> 링크 1개 이상이면 error toast 띄우겠습니다. - try { - await deleteFolder(folderId); - } catch (error) { - console.log(error, "DeleteFolderModal 폴더 삭제 에러"); - } finally { + // 폴더 내에 링크 개수 0 일때만 폴더 삭제 가능 -> 링크 1개 이상이면 error toast 띄우고 있음 or 전체 링크 삭제 후 폴더 삭제 + if (linkCount === 0) { + try { + await deleteFolder(folderId); + toast.success(toastMessages.success.deleteFolder); + } catch (error) { + toast.error(toastMessages.error.deleteFolder); + } finally { + closeModal(); + } + } else { + toast.error(toastMessages.error.deleteNonEmptyFolder); closeModal(); } }; diff --git a/components/modal/DeleteLinkModal.tsx b/components/modal/DeleteLinkModal.tsx index 7e71fb0..ca39559 100644 --- a/components/modal/DeleteLinkModal.tsx +++ b/components/modal/DeleteLinkModal.tsx @@ -2,6 +2,8 @@ import useModalStore from "@/store/useModalStore"; import SubmitButton from "../SubMitButton"; import ModalContainer from "./modalComponents/ModalContainer"; import { useLinkCardStore } from "@/store/useLinkCardStore"; +import toast from "react-hot-toast"; +import toastMessages from "@/lib/toastMessage"; const DeleteLinkModal = ({ link, @@ -17,8 +19,9 @@ const DeleteLinkModal = ({ try { await deleteLink(linkId); closeModal(); + toast.success(toastMessages.success.deleteLink); } catch (error) { - console.error("Failed to delete the link:", error); + toast.error(toastMessages.error.deleteLink); } }; diff --git a/components/modal/EditLink.tsx b/components/modal/EditLink.tsx index 4322740..0759a69 100644 --- a/components/modal/EditLink.tsx +++ b/components/modal/EditLink.tsx @@ -4,6 +4,10 @@ import ModalContainer from "./modalComponents/ModalContainer"; import ModalInput from "./modalComponents/ModalInput"; import useModalStore from "@/store/useModalStore"; import SubmitButton from "../SubMitButton"; +import toast from "react-hot-toast"; +import toastMessages from "@/lib/toastMessage"; +import { urlRegex } from "@/util/regex"; +import { error } from "console"; const EditLink = ({ folderName, @@ -25,10 +29,21 @@ const EditLink = ({ const body = { url: value, }; - if (value !== "") { - await updateLink(linkId, body); + if (value === link) { + toast.error(toastMessages.error.sameLink); + } else if (value === "") { + toast.error(toastMessages.error.inputLink); + } else if (!urlRegex.test(value)) { + toast.error(toastMessages.error.invalidLink); + } else { + try { + await updateLink(linkId, body); + closeModal(); + toast.success(toastMessages.success.editLink); + } catch (err) { + toast.error(toastMessages.error.editLink); + } } - closeModal(); }; return ( diff --git a/components/modal/EditModal.tsx b/components/modal/EditModal.tsx index 98e2e2f..eccb5cf 100644 --- a/components/modal/EditModal.tsx +++ b/components/modal/EditModal.tsx @@ -4,6 +4,8 @@ import ModalInput from "./modalComponents/ModalInput"; import useModalStore from "@/store/useModalStore"; import { putFolder } from "@/lib/api/folder"; import SubmitButton from "../SubMitButton"; +import toast from "react-hot-toast"; +import toastMessages from "@/lib/toastMessage"; const EditModal = ({ folderName, @@ -23,12 +25,16 @@ const EditModal = ({ const body = { name: value, }; - if (value !== "") { + if (value === folderName) { + toast.error(toastMessages.error.sameFolderName); + } else if (value === "") { + toast.error(toastMessages.error.inputFolderName); + } else { try { await putFolder(folderId, body); - console.log("폴더 수정 완료"); + toast.success(toastMessages.success.editFolder); } catch (error) { - console.log(error); + toast.error(toastMessages.error.editFolder); } } closeModal(); diff --git a/components/modal/modalComponents/ModalContainer.tsx b/components/modal/modalComponents/ModalContainer.tsx index ceb606e..e28887c 100644 --- a/components/modal/modalComponents/ModalContainer.tsx +++ b/components/modal/modalComponents/ModalContainer.tsx @@ -1,7 +1,7 @@ import { IoIosClose } from "react-icons/io"; import { ModalPropType } from "@/types/modalTypes"; import useModalStore from "@/store/useModalStore"; -import { MouseEvent, useRef } from "react"; +import { MouseEvent, useEffect, useRef } from "react"; const ModalContainer = ({ title, subtitle, children }: ModalPropType) => { const { isOpen, closeModal } = useModalStore(); @@ -10,11 +10,18 @@ const ModalContainer = ({ title, subtitle, children }: ModalPropType) => { if (modalRef.current && !modalRef.current.contains(e.target as Node)) closeModal(); }; - if (isOpen) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = "auto"; - } + + useEffect(() => { + if (isOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "auto"; + } + + return () => { + document.body.style.overflow = "auto"; + }; + }, [isOpen]); return (
{ case "AddFolderModal": return ; case "AddModal": - return ( - - ); + return ; case "DeleteFolderModal": return ( { if (data) { router.push("/login"); + toast.success(toastMessages.success.signup); } else { - alert("회원가입 실패: 이메일 또는 비밀번호를 확인해주세요."); + toast.error(toastMessages.error.signup); } } else { const data = await login({ email, password }); if (data) { router.push("/"); + toast.success(toastMessages.success.login); } else { - alert("로그인 실패: 이메일 또는 비밀번호를 확인해주세요."); + toast.error(toastMessages.error.login); } } diff --git a/lib/api/folder.ts b/lib/api/folder.ts index 8de87b0..9f11160 100644 --- a/lib/api/folder.ts +++ b/lib/api/folder.ts @@ -52,5 +52,6 @@ export const putFolder = async (folderId: number, body: folderApiProps) => { if (res.status >= 200 && res.status < 300) return res.data; } catch (err) { console.error("에러 메시지: ", err instanceof Error ? err.message : err); + throw err; } }; diff --git a/lib/api/link.ts b/lib/api/link.ts index 2eb1226..5fb5f6a 100644 --- a/lib/api/link.ts +++ b/lib/api/link.ts @@ -32,9 +32,14 @@ export const getLink = async (query: any, forderId: number) => { export const postLink = async (body: postLinkProps) => { try { const res = await proxy.post("/api/links", body); - if (res.status >= 200 && res.status < 300) return res.data; + if (res.status >= 200 && res.status < 300) { + return res.data; + } else { + throw new Error("Request failed"); + } } catch (err) { - console.error("에러 메시지: ", err instanceof Error ? err.message : err); + // console.error("에러 메시지: ", err instanceof Error ? err.message : err); + throw err; } }; @@ -65,6 +70,7 @@ export const putLinkURL = async (linkId: number, body: putLinkURLProps) => { if (res.status >= 200 && res.status < 300) return res.data; } catch (err) { console.error("에러 메시지: ", err instanceof Error ? err.message : err); + throw err; } }; @@ -75,6 +81,7 @@ export const deleteLinkURL = async (linkId: number) => { if (res.status >= 200 && res.status < 300) return res.data; } catch (err) { console.error("에러 메시지: ", err instanceof Error ? err.message : err); + throw err; } }; diff --git a/lib/toastMessage.ts b/lib/toastMessage.ts new file mode 100644 index 0000000..e829ef1 --- /dev/null +++ b/lib/toastMessage.ts @@ -0,0 +1,36 @@ +const toastMessages = { + success: { + login: "로그인 되었습니다", + signup: "회원가입에 성공했습니다", + addLink: "링크를 추가했습니다", + addFolder: "폴더가 추가되었습니다.", + deleteLink: "링크가 삭제되었습니다", + deleteFolder: "폴더가 삭제되었습니다", + editLink: "링크가 수정되었습니다", + editFolder: "폴더가 수정되었습니다", + copyLink: "링크가 복사되었습니다", // SNS 모달에서 링크 복사 시 + SNSshare: "공유가 완료되었습니다", + }, + error: { + login: "로그인 실패: 이메일 또는 비밀번호를 확인해주세요", + signup: "회원가입 실패: 이메일 또는 비밀번호를 확인해주세요", + addLink: "링크 추가에 실패했습니다", + addFolder: "폴더 추가에 실패했습니다", + deleteLink: "링크 삭제에 실패했습니다", + deleteFolder: "폴더 삭제에 실패했습니다", + deleteNonEmptyFolder: "비어있는 폴더만 삭제할 수 있습니다", + editLink: "링크 수정에 실패했습니다", + editFolder: "폴더 수정에 실패했습니다", + copyLink: "링크가 복사되지 않았습니다", // SNS 모달에서 링크 복사 시 + SNSshare: "공유에 실패했습니다", + inputLink: "링크를 입력해주세요", + inputFolderName: "폴더 이름을 입력해주세요", + limitFolderNameLength: "6자 이하로 입력해주세요", + selectFolder: "폴더를 선택해주세요", + sameLink: "이미 저장된 링크 주소입니다", + sameFolderName: "같은 이름으로는 수정할 수 없습니다", + invalidLink: "잘못된 링크 형식입니다", + }, +}; + +export default toastMessages; diff --git a/package-lock.json b/package-lock.json index c930b05..eff8f08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "next": "15.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", "react-icons": "^5.3.0", "react-spinners": "^0.14.1", "zustand": "^5.0.1" @@ -1780,7 +1781,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -3056,6 +3056,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -4697,6 +4706,22 @@ "react": "^18.2.0" } }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "license": "MIT", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-icons": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", diff --git a/package.json b/package.json index 6b36999..9e266f7 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "next": "15.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", "react-icons": "^5.3.0", "react-spinners": "^0.14.1", "zustand": "^5.0.1" diff --git a/pages/_app.tsx b/pages/_app.tsx index 4cace64..699c3a2 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -3,6 +3,7 @@ import Header from "@/components/Layout/Header"; import "@/styles/globals.css"; import type { AppProps } from "next/app"; import { useRouter } from "next/router"; +import { Toaster } from "react-hot-toast"; export default function App({ Component, pageProps }: AppProps) { const router = useRouter(); @@ -10,6 +11,9 @@ export default function App({ Component, pageProps }: AppProps) { return (
+
+ +
{!hidePaths.includes(router.pathname) &&
}
diff --git a/pages/api/folders/[folderId]/index.ts b/pages/api/folders/[folderId]/index.ts index 5f25cd4..ab82904 100644 --- a/pages/api/folders/[folderId]/index.ts +++ b/pages/api/folders/[folderId]/index.ts @@ -7,8 +7,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { folderId } = req.query; const id = Number(folderId); - console.log("Token:", token); - if (!token) { return res.status(401).json({ error: "사용자 정보를 찾을 수 없습니다." }); } @@ -55,6 +53,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const status = error.response.status; const message = error.response.data?.message || "알 수 없는 오류 발생"; + throw error; return res.status(status).json({ message }); } } diff --git a/util/regex.ts b/util/regex.ts new file mode 100644 index 0000000..b320959 --- /dev/null +++ b/util/regex.ts @@ -0,0 +1,3 @@ +// url 입력값 검증 +export const urlRegex = + /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6}(:[0-9]{1,5})?(\/[^\s]*)?$/;