diff --git a/package-lock.json b/package-lock.json index eae922efa2..fd53d6c579 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "fe-weekly-mission", "version": "0.1.0", "dependencies": { + "@tanstack/react-query": "^5.35.1", + "@tanstack/react-query-devtools": "^5.35.1", "@types/react-copy-to-clipboard": "^5.0.7", "axios": "^1.6.8", "date-fns": "^3.6.0", @@ -15,7 +17,7 @@ "react": "^18", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18", - "react-hook-form": "^7.51.2" + "react-hook-form": "^7.51.4" }, "devDependencies": { "@types/node": "^20", @@ -334,6 +336,55 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.35.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.35.1.tgz", + "integrity": "sha512-0Dnpybqb8+ps6WgqBnqFEC+1F/xLvUosRAq+wiGisTgolOZzqZfkE2995dEXmhuzINiTM7/a6xSGznU0NIvBkw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.32.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.32.1.tgz", + "integrity": "sha512-7Xq57Ctopiy/4atpb0uNY5VRuCqRS/1fi/WBCKKX6jHMa6cCgDuV/AQuiwRXcKARbq2OkVAOrW2v4xK9nTbcCA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.35.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.35.1.tgz", + "integrity": "sha512-i2T7m2ffQdNqlX3pO+uMsnQ0H4a59Ens2GxtlMsRiOvdSB4SfYmHb27MnvFV8rGmtWRaa4gPli0/rpDoSS5LbQ==", + "dependencies": { + "@tanstack/query-core": "5.35.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.35.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.35.1.tgz", + "integrity": "sha512-G2TP8ekCo+C9IPdEswKB9mqG5pxV+DWq86lmNw/VbUpdyNwNFvKi7GdcqW1pLDi5al+zifSjGSO7QZ7zDMJcQg==", + "dependencies": { + "@tanstack/query-devtools": "5.32.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.35.1", + "react": "^18.0.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -3067,9 +3118,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.51.2", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.2.tgz", - "integrity": "sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==", + "version": "7.51.4", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz", + "integrity": "sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==", "engines": { "node": ">=12.22.0" }, diff --git a/package.json b/package.json index 5338f4c90f..edd1592065 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "lint": "next lint" }, "dependencies": { + "@tanstack/react-query": "^5.35.1", + "@tanstack/react-query-devtools": "^5.35.1", "@types/react-copy-to-clipboard": "^5.0.7", "axios": "^1.6.8", "date-fns": "^3.6.0", @@ -16,7 +18,7 @@ "react": "^18", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18", - "react-hook-form": "^7.51.2" + "react-hook-form": "^7.51.4" }, "devDependencies": { "@types/node": "^20", diff --git a/pages/_app.tsx b/pages/_app.tsx index 51241fb190..d2b874aa05 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,6 +1,17 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import "@/styles/global.css"; import type { AppProps } from "next/app"; +const queryClient = new QueryClient(); + export default function App({ Component, pageProps }: AppProps) { - return ; + return ( + + +
+ +
+
+ ); } diff --git a/pages/api/NavBarApi.ts b/pages/api/NavBarApi.ts new file mode 100644 index 0000000000..ef67a48995 --- /dev/null +++ b/pages/api/NavBarApi.ts @@ -0,0 +1,17 @@ +import { DEFAULT_PROFILE } from "@/src/util/constant"; +import instance from "@/pages/api/instance"; + +export const getUser = async () => { + const response = await instance.get("users"); + const data = response?.data[0]; + const userData = data + ? { + id: data.id, + name: data.name, + email: data.email, + profileImageSource: data.image_source || DEFAULT_PROFILE, + } + : null; + + return userData; +}; diff --git a/pages/api/folderPageApi.ts b/pages/api/folderPageApi.ts new file mode 100644 index 0000000000..279ca60cb7 --- /dev/null +++ b/pages/api/folderPageApi.ts @@ -0,0 +1,15 @@ +import instance from "@/pages/api/instance"; +import { mapFolderFromLink } from "@/src/util/mapFolderFromLink"; + +export const getFolderData = async () => { + const response = await instance.get("folders"); + return response; +}; + +export const getLinks = async (folderId: number = 0) => { + const folderQuery = folderId === 0 ? "" : `folders/${folderId}/`; + const response = await instance.get(`${folderQuery}links`); + const data = mapFolderFromLink(response.data); + + return data; +}; diff --git a/pages/api/hello.ts b/pages/api/hello.ts deleted file mode 100644 index a8d68697c6..0000000000 --- a/pages/api/hello.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type { NextApiRequest, NextApiResponse } from "next"; - -type Data = { - name: string; -}; - -export default function handler(req: NextApiRequest, res: NextApiResponse) { - res.status(200).json({ name: "John Doe" }); -} diff --git a/pages/api/instance.ts b/pages/api/instance.ts new file mode 100644 index 0000000000..71f45332da --- /dev/null +++ b/pages/api/instance.ts @@ -0,0 +1,20 @@ +import axios, { AxiosInstance } from "axios"; + +const instance: AxiosInstance = axios.create({ + baseURL: "https://bootcamp-api.codeit.kr/api/linkbrary/v1/", +}); + +const getToken = () => { + if (typeof window !== undefined) { + const token = window.localStorage.getItem("accessToken"); + return token; + } + return ""; +}; + +instance.interceptors.request.use((config) => { + config.headers.Authorization = `Bearer ${getToken()}`; + return config; +}); + +export default instance; diff --git a/pages/api/sharedPageApi.ts b/pages/api/sharedPageApi.ts new file mode 100644 index 0000000000..75db556f4a --- /dev/null +++ b/pages/api/sharedPageApi.ts @@ -0,0 +1,17 @@ +import instance from "@/pages/api/instance"; +import { mapFolderFromLink } from "@/src/util/mapFolderFromLink"; + +export const getFolder = async (id: number) => { + const response = await instance.get(`folders/${id}`); + return response; +}; +export const getFolderOwner = async (id: number) => { + const response = await instance.get(`users/${id}`); + return response.data; +}; + +export const getLinksByFolderId = async (folderId: number) => { + const response = await instance.get(`folders/${folderId}/links`); + const data = mapFolderFromLink(response.data?.data); + return data; +}; diff --git a/pages/api/signPageApi.ts b/pages/api/signPageApi.ts new file mode 100644 index 0000000000..3512c7553b --- /dev/null +++ b/pages/api/signPageApi.ts @@ -0,0 +1,24 @@ +import instance from "@/pages/api/instance"; + +export interface FormValue { + email: string; + password: string; + passwordConfirm?: string; +} + +export const postUserInfo = async (data: FormValue) => { + const response = await instance.post("auth/sign-in", data); + return response; +}; + +export const postCreateAccount = async (data: FormValue) => { + const response = await instance.post("auth/sign-up", data); + console.log(response); + + return response; +}; + +export const postCheckEmail = async (emailData: FormValue) => { + const response = await instance.post("users/check-email", emailData); + return response; +}; diff --git a/pages/folder/index.tsx b/pages/folder/[[...id]].tsx similarity index 67% rename from pages/folder/index.tsx rename to pages/folder/[[...id]].tsx index 8a5fdcfe79..1e5ac93533 100644 --- a/pages/folder/index.tsx +++ b/pages/folder/[[...id]].tsx @@ -7,41 +7,56 @@ import useAsync from "@/src/hooks/useAsync"; import Category from "@/src/ui/Category"; import { EditLink } from "@/src/ui/EditLink"; import Modal from "@/src/ui/Modal/Modal"; +import ErrorPage from "next/error"; import styles from "@/styles/pages/FolderPage.module.css"; -import { useState, useRef, useEffect, ChangeEventHandler, MouseEventHandler } from "react"; +import { + useState, + useRef, + useEffect, + ChangeEventHandler, + MouseEventHandler, +} from "react"; import Head from "next/head"; import { MappedLink } from "@/src/util/mapFolderFromLink"; -import Router from "next/router"; -import { setAxiosHeader } from "@/src/util/setAxiosToken"; -import { useGetLinks } from "@/src/hooks/useGetLink"; -import { axiosInstance } from "@/src/util/axiosInstance"; +import Router, { useRouter } from "next/router"; +import useFloatingAddLinkBar from "@/src/hooks/useFloatingAddLinkBar"; +import { getFolderData, getLinks } from "../api/folderPageApi"; +import { useQuery } from "@tanstack/react-query"; interface Folder { created_at: string; favorite: boolean; id: number; - link: { count: number }; name: string; - user_id: number; + link_count: number; } const FolderPage: React.FC = () => { - const getFolderData = () => axiosInstance.get("folders"); - const { wrappedFunction: getLinks } = useAsync(useGetLinks); + const router = useRouter(); + const { id } = router.query; + const { wrappedFunction: getLink } = useAsync(getLinks); const { wrappedFunction: getFolderList } = useAsync(getFolderData); const [currentCategory, setCurrentCategory] = useState("전체"); - const [folderId, setFolderId] = useState(0); + const [folderId, setFolderId] = useState(0); const [isModalOpen, setIsModalOpen] = useState(false); const [modal, setModal] = useState(""); const [currentUrl, setCurrentUrl] = useState(""); const [searchTerm, setSearchTerm] = useState(""); - const [linksData, setLinksData] = useState([]); - const [folderData, setFolderData] = useState([]); const [isAddLinkShown, setIsAddLinkShown] = useState(true); const [isFooterShown, setIsFooterShown] = useState(false); + const [isError, setIsError] = useState(false); + + const { data: linksData } = useQuery({ + queryKey: ["links", folderId], + queryFn: () => getLink(folderId).then((response) => response?.data), + }); + const { data: folderData } = useQuery({ + queryKey: ["folderList"], + queryFn: () => getFolderList().then((response) => response?.data), + }); const addLinkRef = useRef(null); const footerRef = useRef(null); @@ -49,12 +64,17 @@ const FolderPage: React.FC = () => { useEffect(() => { const accessToken = localStorage.getItem("accessToken"); - if (accessToken) { - setAxiosHeader(); - getLinks(folderId).then((response) => setLinksData(response.data)); - getFolderList().then((response) => setFolderData(response?.data?.data.folder)); - } else Router.push("/signin"); - }, [folderId]); + if (!accessToken) Router.push("/signin"); + }, []); + + useEffect(() => { + if (!router.isReady) return; + if (id !== undefined && !(id.length > 2 && /^\d+$/.test(id[0]))) + setIsError(true); + id?.length + ? setFolderId(parseInt((id[0] as string) ?? 0, 10)) + : setFolderId(0); + }, [id]); const folderDataWithAll = Array.isArray(folderData) ? [{ name: "전체", id: "0" }, ...folderData] @@ -82,41 +102,24 @@ const FolderPage: React.FC = () => { setSearchTerm(""); }; - useEffect(() => { - const addLinkObserver = new IntersectionObserver( - ([entry]) => { - setIsAddLinkShown(entry.isIntersecting); - }, - { threshold: 0 } - ); - const footerObserver = new IntersectionObserver( - ([entry]) => { - setIsFooterShown(entry.isIntersecting); - }, - { threshold: 0 } - ); - if (addLinkRef.current) { - addLinkObserver.observe(addLinkRef.current); - } - if (footerRef.current) { - footerObserver.observe(footerRef.current); - } - - return () => { - if (addLinkRef.current) { - addLinkObserver.unobserve(addLinkRef.current); - } - if (footerRef.current) { - footerObserver.unobserve(footerRef.current); - } - }; - }, []); + useFloatingAddLinkBar({ + setIsAddLinkShown, + setIsFooterShown, + addLinkRef, + footerRef, + }); + + if (isError) { + return ; + } const filteredLinks = linksData?.filter( (link) => link.title?.toLowerCase().includes(searchTerm.trim().toLowerCase()) || - link.description?.toLowerCase().includes(searchTerm.trim().toLowerCase()) || - link.url.toLowerCase().includes(searchTerm.trim().toLowerCase()) + link.description + ?.toLowerCase() + .includes(searchTerm.trim().toLowerCase()) || + link.url.toLowerCase().includes(searchTerm.trim().toLowerCase()), ); return ( @@ -149,11 +152,18 @@ const FolderPage: React.FC = () => { categoryId={folderId.toString()} handleModalClick={handleModalClick} /> - + {filteredLinks && filteredLinks.length > 0 ? ( {filteredLinks?.map((link) => ( - + ))} ) : ( diff --git a/pages/folder/[id].tsx b/pages/folder/[id].tsx deleted file mode 100644 index 2c2c107bee..0000000000 --- a/pages/folder/[id].tsx +++ /dev/null @@ -1,174 +0,0 @@ -import AddLink from "@/src/ui/AddLink"; -import Layout from "@/src/feature/Layout"; -import SearchBar from "@/src/ui/SearchBar"; -import { CardList } from "@/src/ui/CardList"; -import { Card } from "@/src/ui/Card"; -import useAsync from "@/src/hooks/useAsync"; -import Category from "@/src/ui/Category"; -import { EditLink } from "@/src/ui/EditLink"; -import Modal from "@/src/ui/Modal/Modal"; - -import styles from "@/styles/pages/FolderPage.module.css"; -import { useState, useRef, useEffect, ChangeEventHandler, MouseEventHandler } from "react"; -import Head from "next/head"; -import { MappedLink } from "@/src/util/mapFolderFromLink"; -import Router, { useRouter } from "next/router"; -import { setAxiosHeader } from "@/src/util/setAxiosToken"; -import { useGetLinks } from "@/src/hooks/useGetLink"; -import { axiosInstance } from "@/src/util/axiosInstance"; - -interface Folder { - created_at: string; - favorite: boolean; - id: number; - link: { count: number }; - name: string; - user_id: number; -} - -const FolderPage: React.FC = () => { - const router = useRouter(); - const { id } = router.query; - if (!(typeof id === "string" && /^\d+$/.test(id))) Router.push("/"); - const getFolderData = () => axiosInstance.get("folders"); - const { wrappedFunction: getLinks } = useAsync(useGetLinks); - const { wrappedFunction: getFolderList } = useAsync(getFolderData); - - const [currentCategory, setCurrentCategory] = useState("전체"); - const [folderId, setFolderId] = useState(id ? +id : 0); - const [isModalOpen, setIsModalOpen] = useState(false); - const [modal, setModal] = useState(""); - const [currentUrl, setCurrentUrl] = useState(""); - const [searchTerm, setSearchTerm] = useState(""); - const [linksData, setLinksData] = useState([]); - const [folderData, setFolderData] = useState([]); - - const [isAddLinkShown, setIsAddLinkShown] = useState(true); - const [isFooterShown, setIsFooterShown] = useState(false); - - const addLinkRef = useRef(null); - const footerRef = useRef(null); - const isAddLinkFixed = !isAddLinkShown && !isFooterShown; - - useEffect(() => { - const accessToken = localStorage.getItem("accessToken"); - if (accessToken) { - setAxiosHeader(); - getLinks(folderId).then((response) => setLinksData(response.data)); - getFolderList().then((response) => setFolderData(response?.data?.data.folder)); - } else Router.push("/signin"); - }, [folderId]); - - const folderDataWithAll = Array.isArray(folderData) - ? [{ name: "전체", id: "0" }, ...folderData] - : []; - const navFixed = true; - - const handleCategoryClick: MouseEventHandler = (e) => { - const eventTarget = e.target as HTMLElement; - const category = eventTarget.innerText; - const Id = eventTarget.id; - setCurrentCategory(category); - setFolderId(+Id || 0); - }; - const handleModalClick: MouseEventHandler = (e) => { - const eventTarget = e.target as HTMLElement; - e.preventDefault(); - setIsModalOpen(true); - setModal(e.currentTarget.id); - setCurrentUrl(eventTarget.getAttribute("data-url") || ""); - }; - const handleInputChange: ChangeEventHandler = (e) => { - setSearchTerm(e.target.value); - }; - const handleInputClear = () => { - setSearchTerm(""); - }; - - useEffect(() => { - const addLinkObserver = new IntersectionObserver( - ([entry]) => { - setIsAddLinkShown(entry.isIntersecting); - }, - { threshold: 0 } - ); - const footerObserver = new IntersectionObserver( - ([entry]) => { - setIsFooterShown(entry.isIntersecting); - }, - { threshold: 0 } - ); - if (addLinkRef.current) { - addLinkObserver.observe(addLinkRef.current); - } - if (footerRef.current) { - footerObserver.observe(footerRef.current); - } - - return () => { - if (addLinkRef.current) { - addLinkObserver.unobserve(addLinkRef.current); - } - if (footerRef.current) { - footerObserver.unobserve(footerRef.current); - } - }; - }, []); - - const filteredLinks = linksData?.filter( - (link) => - link.title?.toLowerCase().includes(searchTerm.trim().toLowerCase()) || - link.description?.toLowerCase().includes(searchTerm.trim().toLowerCase()) || - link.url.toLowerCase().includes(searchTerm.trim().toLowerCase()) - ); - - return ( - <> - - Folder - - {isModalOpen && ( - - )} - -
- -
- - - - {filteredLinks && filteredLinks.length > 0 ? ( - - {filteredLinks?.map((link) => ( - - ))} - - ) : ( -
저장된 링크가 없습니다.
- )} - - {isAddLinkFixed && } -
-
-
- - ); -}; - -export default FolderPage; diff --git a/pages/shared/[[...folderId]].tsx b/pages/shared/[[...folderId]].tsx new file mode 100644 index 0000000000..c9900a8b09 --- /dev/null +++ b/pages/shared/[[...folderId]].tsx @@ -0,0 +1,146 @@ +import FolderInfo from "@/src/ui/FolderInfo"; +import SearchBar from "@/src/ui/SearchBar"; +import { CardList } from "@/src/ui/CardList"; +import Layout from "@/src/feature/Layout"; +import { Card } from "@/src/ui/Card"; +import useAsync from "@/src/hooks/useAsync"; +import styles from "@/styles/pages/SharedPage.module.css"; +import { ChangeEventHandler, useEffect, useState } from "react"; +import Head from "next/head"; +import ErrorPage from "next/error"; +import { useQuery } from "@tanstack/react-query"; + +import { useRouter } from "next/router"; +import { + getFolder, + getFolderOwner, + getLinksByFolderId, +} from "../api/sharedPageApi"; + +interface FolderData { + id: number; + created_at: string; + name: string; + user_id: number; + favorite: boolean; +} +interface UserData { + id: number; + created_at: string; + name: string; + image_source: string; + email: string; + auth_id: string; +} +interface LinkData { + id: number; + url: string; + imageSource: string; + title?: string; + alt: string; + elapsedTime: string; + description?: string; + createdAt: string; + favorite?: boolean; +} + +const SharedPage = () => { + const [searchTerm, setSearchTerm] = useState(""); + + const router = useRouter(); + const { folderId } = router.query; + const { wrappedFunction: getFolderInfo } = useAsync(getFolder); + const { wrappedFunction: getOwner } = useAsync(getFolderOwner); + const { wrappedFunction: getLinks } = useAsync(getLinksByFolderId); + + const { + data: folderData, + isError: folderError, + isLoading: folderLoading, + } = useQuery({ + queryKey: ["folderData", folderId], + queryFn: () => + getFolderInfo(folderId?.[0]).then((response) => { + if (response.status !== 200) { + throw new Error("Network response was not ok"); + } + return response.data[0]; + }), + enabled: !!folderId?.length, + }); + + const { data: userData, isError: userError } = useQuery({ + queryKey: ["owner", folderData?.user_id], + queryFn: () => + getOwner(folderData?.user_id).then((response) => { + return response[0]; + }), + enabled: !!folderData?.user_id && !folderLoading, + }); + + const { data: linkData, isError: linkError } = useQuery({ + queryKey: ["links", folderData?.user_id, folderId?.[0]], + queryFn: () => + getLinks(folderData?.user_id, folderId?.[0]).then((response) => { + return response.data || []; + }), + enabled: !!folderData?.user_id && !folderLoading, + }); + + if (folderError || userError || linkError) { + return ; + } + + const handleInputChange: ChangeEventHandler = (e) => { + setSearchTerm(e.target.value); + }; + const handleInputClear = () => { + setSearchTerm(""); + }; + + const filteredLinks = linkData?.filter( + (link) => + link.alt?.toLowerCase().includes(searchTerm.trim().toLowerCase()) || + link.description + ?.toLowerCase() + .includes(searchTerm.trim().toLowerCase()) || + link.url.toLowerCase().includes(searchTerm.trim().toLowerCase()), + ); + + return ( + <> + + Shared + + +
+ +
+ + {filteredLinks && filteredLinks.length > 0 ? ( + + {filteredLinks?.map((link) => ( + + ))} + + ) : ( +
저장된 링크가 없습니다.
+ )} +
+
+
+ + ); +}; + +export default SharedPage; diff --git a/pages/shared/[folderId].tsx b/pages/shared/[folderId].tsx deleted file mode 100644 index eac14e8d2b..0000000000 --- a/pages/shared/[folderId].tsx +++ /dev/null @@ -1,129 +0,0 @@ -import FolderInfo from "@/src/ui/FolderInfo"; -import SearchBar from "@/src/ui/SearchBar"; -import { CardList } from "@/src/ui/CardList"; -import Layout from "@/src/feature/Layout"; -import { Card } from "@/src/ui/Card"; -import useAsync from "@/src/hooks/useAsync"; -import styles from "@/styles/pages/SharedPage.module.css"; -import { ChangeEventHandler, useEffect, useState } from "react"; -import Head from "next/head"; - -import { axiosInstance } from "@/src/util/axiosInstance"; -import Router, { useRouter } from "next/router"; -import { useGetLinksByFolderId } from "@/src/hooks/useGetLinksByFolderId"; - -interface FolderData { - data: { - id: number; - created_at: string; - name: string; - user_id: number; - favorite: boolean; - }[]; -} -interface userData { - data: { - id: number; - created_at: string; - name: string; - image_source: string; - email: string; - auth_id: string; - }[]; -} -interface LinkData { - data: { - id: number; - url: string; - imageSource: string; - title?: string; - alt: string; - elapsedTime: string; - description?: string; - createdAt: string; - favorite?: boolean; - }[]; -} - -const SharedPage = () => { - const [searchTerm, setSearchTerm] = useState(""); - const [folderData, setFolderData] = useState({ - data: [], - }); - const [userData, setUserData] = useState({ - data: [], - }); - const [linkData, setLinkData] = useState({ - data: [], - }); - - const router = useRouter(); - const { folderId } = router.query; - const getFolder = async (id: number) => axiosInstance.get(`folders/${id}`); - const getFolderOwner = async (id: number) => axiosInstance.get(`users/${id}`); - const { wrappedFunction: getFolderInfo } = useAsync(getFolder); - const { wrappedFunction: getOwner } = useAsync(getFolderOwner); - const { wrappedFunction: getLinksByFolderId } = useAsync(useGetLinksByFolderId); - - useEffect(() => { - if (folderId) { - getFolderInfo(folderId).then((result) => { - setFolderData(result?.data); - const ownerId = result?.data.data[0].user_id; - if (ownerId) { - getOwner(ownerId).then((res) => setUserData(res?.data)); - getLinksByFolderId(ownerId, folderId).then(setLinkData); - } - }); - } - }, [folderId]); - - const handleInputChange: ChangeEventHandler = (e) => { - setSearchTerm(e.target.value); - }; - const handleInputClear = () => { - setSearchTerm(""); - }; - - const filteredLinks = linkData?.data?.filter( - (link) => - link.alt?.toLowerCase().includes(searchTerm.trim().toLowerCase()) || - link.description?.toLowerCase().includes(searchTerm.trim().toLowerCase()) || - link.url.toLowerCase().includes(searchTerm.trim().toLowerCase()) - ); - - return ( - <> - - Shared - - -
- -
- - {filteredLinks && filteredLinks.length > 0 ? ( - - {filteredLinks?.map((link) => ( - - ))} - - ) : ( -
저장된 링크가 없습니다.
- )} -
-
-
- - ); -}; - -export default SharedPage; diff --git a/pages/shared/index.tsx b/pages/shared/index.tsx deleted file mode 100644 index d736416f1f..0000000000 --- a/pages/shared/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import FolderInfo from "@/src/ui/FolderInfo"; -import SearchBar from "@/src/ui/SearchBar"; -import { CardList } from "@/src/ui/CardList"; -import Layout from "@/src/feature/Layout"; -import { Card } from "@/src/ui/Card"; -import { useGetFolderInfo } from "@/src/hooks/useGetFolderInfo"; -import useAsync from "@/src/hooks/useAsync"; -import styles from "@/styles/pages/SharedPage.module.css"; -import { ChangeEventHandler, useEffect, useState } from "react"; -import Head from "next/head"; -import { FolderData } from "@/src/util/mapFolderData"; - -const SharedPage = () => { - const [searchTerm, setSearchTerm] = useState(""); - const [folderData, setFolderData] = useState(); - - const { wrappedFunction: getFolderInfo } = useAsync(useGetFolderInfo); - - useEffect(() => { - getFolderInfo().then(setFolderData); - }); - - const { profileImage, ownerName, folderName, links } = folderData || {}; - - const handleInputChange: ChangeEventHandler = (e) => { - setSearchTerm(e.target.value); - }; - const handleInputClear = () => { - setSearchTerm(""); - }; - - const filteredLinks = links?.filter( - (link) => - link.alt?.toLowerCase().includes(searchTerm.trim().toLowerCase()) || - link.description?.toLowerCase().includes(searchTerm.trim().toLowerCase()) || - link.url.toLowerCase().includes(searchTerm.trim().toLowerCase()) - ); - - return ( - <> - - Shared - - -
- -
- - - {filteredLinks?.map((link) => ( - - ))} - -
-
-
- - ); -}; - -export default SharedPage; diff --git a/pages/signin.tsx b/pages/signin.tsx index 3c958aef31..4dd4e32c06 100644 --- a/pages/signin.tsx +++ b/pages/signin.tsx @@ -7,23 +7,33 @@ import Image from "next/image"; import EmailInput from "@/src/ui/EmailInput"; import PasswordInput from "@/src/ui/PasswordInput"; import useAsync from "@/src/hooks/useAsync"; -import { axiosInstance } from "@/src/util/axiosInstance"; import Router from "next/router"; -import { setAxiosHeader } from "@/src/util/setAxiosToken"; - -interface FormValue { - email: string; - password: string; -} +import { postUserInfo, FormValue } from "./api/signPageApi"; +import { useMutation } from "@tanstack/react-query"; +import { AxiosResponse } from "axios"; const Signin: React.FC = () => { if (localStorage.getItem("accessToken")) Router.push("/folder"); - const [isPasswordOpen, setIsPasswordOpen] = useState(false); - - const postUserInfo = (signinData?: FormValue) => axiosInstance.post("sign-in", signinData); const { wrappedFunction: postSignin } = useAsync(postUserInfo); + const signInMutation = useMutation< + AxiosResponse, + Error, + FormValue, + unknown + >({ + mutationFn: postSignin, + onSuccess: (data: any) => { + localStorage.setItem("accessToken", data.data.accessToken); + Router.push("/folder"); + }, + onError: () => { + setError("email", { message: "이메일을 확인해주세요" }); + setError("password", { message: "비밀번호를 확인해주세요" }); + }, + }); + const { register, handleSubmit, @@ -35,19 +45,8 @@ const Signin: React.FC = () => { setIsPasswordOpen(!isPasswordOpen); }; - const onSubmit: SubmitHandler = async (data) => { - const response = await postSignin(data); - if (response?.status === 200) { - console.log(response); - const accessToken = response.data; - localStorage.setItem("accessToken", accessToken.data.accessToken); - setAxiosHeader(accessToken.data.accessToken); - Router.push("/folder"); - } else { - setError("email", { message: "이메일을 확인해주세요" }); - setError("password", { message: "비밀번호를 확인해주세요" }); - } - }; + const onSubmit: SubmitHandler = async (data) => + signInMutation.mutate(data); return ( <> @@ -57,37 +56,50 @@ const Signin: React.FC = () => {
- + Linkbrary 서비스 로고
- 회원이 아니신가요? 회원 가입하기 + 회원이 아니신가요? 회원 가입하기
- + -
소셜 로그인
- - google + + google - - kakaotalk + + kakaotalk
diff --git a/pages/signup.tsx b/pages/signup.tsx index 4dcdb06c24..73c2814c18 100644 --- a/pages/signup.tsx +++ b/pages/signup.tsx @@ -7,25 +7,19 @@ import Image from "next/image"; import PasswordConfirmInput from "@/src/ui/PasswordConfirmInput"; import CreatePasswordInput from "@/src/ui/CreatePasswordInput"; import useAsync from "@/src/hooks/useAsync"; -import { axiosInstance } from "@/src/util/axiosInstance"; import Router from "next/router"; import CreateEmailInput from "@/src/ui/CreateEmailInput"; -import { setAxiosHeader } from "@/src/util/setAxiosToken"; - -interface FormValue { - email: string; - password: string; - passwordConfirm?: string; -} +import { postCreateAccount, FormValue } from "./api/signPageApi"; +import { useMutation } from "@tanstack/react-query"; +import { AxiosResponse } from "axios"; const Signup: React.FC = () => { if (localStorage.getItem("accessToken")) Router.push("/folder"); const [isPasswordOpen, setIsPasswordOpen] = useState(false); - const [isPasswordConfirmOpen, setIsPasswordConfirmOpen] = useState(false); - - const postCheckAccount = (data: FormValue) => axiosInstance.post("sign-up", data); - const { wrappedFunction: postSignup } = useAsync(postCheckAccount); + const [isPasswordConfirmOpen, setIsPasswordConfirmOpen] = + useState(false); + const { wrappedFunction: postSignup } = useAsync(postCreateAccount); const { register, @@ -36,22 +30,30 @@ const Signup: React.FC = () => { const passwordValue = getValues("password"); + const signUpMutation = useMutation< + AxiosResponse, + Error, + FormValue, + unknown + >({ + mutationFn: postSignup, + onSuccess: (data: any) => { + localStorage.setItem("accessToken", data.data.accessToken); + Router.push("/folder"); + }, + }); + const handlePasswordEyeconClick: MouseEventHandler = () => { setIsPasswordOpen(!isPasswordOpen); }; - const handlePasswordConfirmEyeconClick: MouseEventHandler = () => { + const handlePasswordConfirmEyeconClick: MouseEventHandler< + HTMLImageElement + > = () => { setIsPasswordConfirmOpen(!isPasswordConfirmOpen); }; - const onSubmit: SubmitHandler = async (data) => { - const response = await postSignup(data); - if (response?.status === 200) { - const accessToken = response.data; - localStorage.setItem("accessToken", accessToken.data.accessToken); - setAxiosHeader(accessToken.data.accessToken); - Router.push("/folder"); - } - }; + const onSubmit: SubmitHandler = async (data) => + signUpMutation.mutate(data); return ( <> @@ -61,20 +63,23 @@ const Signup: React.FC = () => {
- + Linkbrary 서비스 로고
- 이미 회원이신가요? 로그인하기 + 이미 회원이신가요? 로그인하기
- + { passwordValue={passwordValue} handleEyeconClick={handlePasswordConfirmEyeconClick} /> -
다른 방식으로 가입하기
- - google + + google - - kakaotalk + + kakaotalk
diff --git a/src/feature/Layout.tsx b/src/feature/Layout.tsx index 5d645d5304..bb82f50d88 100644 --- a/src/feature/Layout.tsx +++ b/src/feature/Layout.tsx @@ -1,9 +1,8 @@ -import { useGetUser } from "@/src/hooks/useGetUser"; import Footer from "./Footer/Footer"; import NavigationBar from "./NavigationBar"; import { ReactNode, RefObject, useEffect, useState } from "react"; import useAsync from "../hooks/useAsync"; -import { setAxiosHeader } from "../util/setAxiosToken"; +import { getUser } from "@/pages/api/NavBarApi"; interface layoutProps { children: ReactNode; @@ -19,14 +18,13 @@ type Data = { } | null; const Layout = ({ children, isNavFixed, footerRef }: layoutProps) => { - const { wrappedFunction: getUser } = useAsync(useGetUser); + const { wrappedFunction: getUserData } = useAsync(getUser); const [data, setData] = useState(null); useEffect(() => { if (!localStorage.getItem("accessToken")) { setData(null); } else { - setAxiosHeader(); - getUser().then(setData); + getUserData().then(setData); } }, []); diff --git a/src/hooks/useAsync.ts b/src/hooks/useAsync.ts index 47dedb1ace..a5e8c53615 100644 --- a/src/hooks/useAsync.ts +++ b/src/hooks/useAsync.ts @@ -3,8 +3,6 @@ import { useState } from "react"; function useAsync(callback: (args?: any) => Promise) { const [pending, setPending] = useState(false); const [error, setError] = useState(null); - // TODO - // 에러 상태일 때 setError(false) 확인해보기 const wrappedFunction: (...args: any) => Promise = async (...args) => { try { setPending(true); diff --git a/src/hooks/useFloatingAddLinkBar.ts b/src/hooks/useFloatingAddLinkBar.ts new file mode 100644 index 0000000000..83f6dcd50f --- /dev/null +++ b/src/hooks/useFloatingAddLinkBar.ts @@ -0,0 +1,47 @@ +import { MutableRefObject, SetStateAction, useEffect } from "react"; + +interface RefProps { + setIsAddLinkShown: (value: SetStateAction) => void; + setIsFooterShown: (value: SetStateAction) => void; + addLinkRef: MutableRefObject; + footerRef: MutableRefObject; +} + +const useFloatingAddLinkBar = ({ + setIsAddLinkShown, + setIsFooterShown, + addLinkRef, + footerRef, +}: RefProps) => { + useEffect(() => { + const addLinkObserver = new IntersectionObserver( + ([entry]) => { + setIsAddLinkShown(entry.isIntersecting); + }, + { threshold: 0 }, + ); + const footerObserver = new IntersectionObserver( + ([entry]) => { + setIsFooterShown(entry.isIntersecting); + }, + { threshold: 0 }, + ); + if (addLinkRef.current) { + addLinkObserver.observe(addLinkRef.current); + } + if (footerRef.current) { + footerObserver.observe(footerRef.current); + } + + return () => { + if (addLinkRef.current) { + addLinkObserver.unobserve(addLinkRef.current); + } + if (footerRef.current) { + footerObserver.unobserve(footerRef.current); + } + }; + }, []); +}; + +export default useFloatingAddLinkBar; diff --git a/src/hooks/useGetFolder.ts b/src/hooks/useGetFolder.ts deleted file mode 100644 index 4ebdde9f5e..0000000000 --- a/src/hooks/useGetFolder.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { axiosInstance } from "../util/axiosInstance"; - -export const useGetFolder = async () => { - const response = await axiosInstance.get("users/1/folders"); - const data = response.data?.data; - - return data; -}; diff --git a/src/hooks/useGetFolderInfo.ts b/src/hooks/useGetFolderInfo.ts deleted file mode 100644 index 73f47993bb..0000000000 --- a/src/hooks/useGetFolderInfo.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { mapFolderData } from "../util/mapFolderData"; -import { axiosInstance } from "../util/axiosInstance"; - -export const useGetFolderInfo = async () => { - const response = await axiosInstance.get("sample/folder"); - const data = mapFolderData(response.data?.folder); - return data; -}; diff --git a/src/hooks/useGetLink.ts b/src/hooks/useGetLink.ts deleted file mode 100644 index aa4b243a4f..0000000000 --- a/src/hooks/useGetLink.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { mapFolderFromLink } from "../util/mapFolderFromLink"; -import { axiosInstance } from "../util/axiosInstance"; - -export const useGetLinks = async (folderId: number = 0) => { - const folderQuery = folderId === 0 ? "" : `?folderId=${folderId}`; - const response = await axiosInstance.get(`links${folderQuery}`); - const data = mapFolderFromLink(response.data?.data.folder); - - return data; -}; diff --git a/src/hooks/useGetLinksByFolderId.ts b/src/hooks/useGetLinksByFolderId.ts deleted file mode 100644 index 54c96ac1e2..0000000000 --- a/src/hooks/useGetLinksByFolderId.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { mapFolderFromLink } from "../util/mapFolderFromLink"; -import { axiosInstance } from "../util/axiosInstance"; - -export const useGetLinksByFolderId = async (userId: number, folderId?: number) => { - const folderQuery = folderId === 0 ? "" : `?folderId=${folderId}`; - const response = await axiosInstance.get(`users/${userId}/links${folderQuery}`); - const data = mapFolderFromLink(response.data?.data); - - return data; -}; diff --git a/src/hooks/useGetUser.ts b/src/hooks/useGetUser.ts deleted file mode 100644 index 6f5b87e5ec..0000000000 --- a/src/hooks/useGetUser.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { axiosInstance } from "../util/axiosInstance"; -import { DEFAULT_PROFILE } from "../util/constant"; - -export const useGetUser = async () => { - const response = await axiosInstance.get("users"); - const data = response.data ? response.data?.data[0] : null; - const userData = data - ? { - id: data.id, - name: data.name, - email: data.email, - profileImageSource: data.image_source || DEFAULT_PROFILE, - } - : null; - return userData; -}; diff --git a/src/ui/CreateEmailInput.tsx b/src/ui/CreateEmailInput.tsx index 0701fc64ff..af0faacb72 100644 --- a/src/ui/CreateEmailInput.tsx +++ b/src/ui/CreateEmailInput.tsx @@ -1,8 +1,8 @@ import { UseFormRegister } from "react-hook-form"; import InputLayout from "./InputLayout"; import styles from "@/styles/pages/SignPage.module.css"; -import { axiosInstance } from "../util/axiosInstance"; import useAsync from "../hooks/useAsync"; +import { postCheckEmail } from "@/pages/api/signPageApi"; interface FormValue { email: string; @@ -15,17 +15,20 @@ interface InputValue { } const CreateEmailInput = ({ register, inputError }: InputValue) => { - const postCheckEmail = (emailData: FormValue) => axiosInstance.post("check-email", emailData); const { wrappedFunction: postEmailValidation } = useAsync(postCheckEmail); return ( - + { +const InputLayout = ({ + isEyeOpen, + inputError, + handleEyeconClick, + children, +}: Prop) => { const eyecon = isEyeOpen ? "/assets/eye-on.svg" : "/assets/eye-off.svg"; return ( @@ -17,8 +22,14 @@ const InputLayout = ({ isEyeOpen, inputError, handleEyeconClick, children }: Pro
{children} {isEyeOpen !== undefined && ( - )}
diff --git a/src/ui/Modal/AddToMyfolder.tsx b/src/ui/Modal/AddToMyfolder.tsx index fd4e2aef68..81af2057b9 100644 --- a/src/ui/Modal/AddToMyfolder.tsx +++ b/src/ui/Modal/AddToMyfolder.tsx @@ -2,16 +2,10 @@ import styles from "@/styles/ui/Modal.module.css"; import Image from "next/image"; import { MouseEventHandler, useState } from "react"; -interface Link { - count: number; - name: string; - id: number; -} - interface Category { id: number; name: string; - link: Link; + link_count: number; } function AddToMyFolder({ @@ -21,7 +15,7 @@ function AddToMyFolder({ currentUrl: string; categoryData: Category[]; }) { - const [selectedFolder, setSelectedFolder] = useState(); + const [selectedFolder, setSelectedFolder] = useState(0); const handleFolderClick: MouseEventHandler = (e) => { setSelectedFolder(+e.currentTarget.id); @@ -44,7 +38,7 @@ function AddToMyFolder({ {link.name} {`${link?.link.count}개 링크`} + >{`${link.link_count}개 링크`} {selectedFolder === link.id && ( { - if (!folder) return { profileImage: "", ownerName: "", folderName: "", links: [] }; + if (!folder) + return { profileImage: "", ownerName: "", folderName: "", links: [] }; const { name, owner, links } = folder; const mapLinks = (link: Link) => { diff --git a/src/util/mapFolderFromLink.ts b/src/util/mapFolderFromLink.ts index fc7b6e17dc..f416640f59 100644 --- a/src/util/mapFolderFromLink.ts +++ b/src/util/mapFolderFromLink.ts @@ -23,7 +23,9 @@ interface Link { favorite?: boolean; } -export const mapFolderFromLink: (data: Link[]) => { data: MappedLink[] } = (data) => { +export const mapFolderFromLink: (data: Link[]) => { data: MappedLink[] } = ( + data, +) => { if (!data) return { data: [] }; const mapLinks = (link: Link) => { diff --git a/src/util/setAxiosToken.ts b/src/util/setAxiosToken.ts deleted file mode 100644 index b82fc5c8a2..0000000000 --- a/src/util/setAxiosToken.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { axiosInstance } from "./axiosInstance"; - -export const setAxiosHeader = (accessToken?: string) => { - const token = accessToken ? accessToken : localStorage.getItem("accessToken"); - axiosInstance.interceptors.request.use((config) => { - config.headers.Authorization = `Bearer ${token}`; - return config; - }); -};