diff --git a/.eslintrc.json b/.eslintrc.json index bffb357a71..1042102043 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,49 @@ { - "extends": "next/core-web-vitals" + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "parser": "@typescript-eslint/parser", + "extends": [ + "eslint:recommended", + "next/core-web-vitals", + "plugin:@typescript-eslint/recommended" + ], + + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module", + "requireConfigFile": false + }, + "plugins": ["react", "@typescript-eslint", "import"], + + "rules": { + "import/order": [ + "error", + { + "groups": [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index", + "object", + "type" + ] + } + ], + "@typescript-eslint/no-explicit-any": 0, + "semi": ["error", "always"], + "quotes": ["error", "double"], + "react/prop-types": "off", + "@typescript-eslint/no-unused-vars": "warn", + "react/no-unused-state": "error", + "no-unused-vars": "error", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "array-callback-return": 0, + "react/self-closing-comp": "warn" + } } diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..8c0096297c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "arrowParens": "avoid", + "bracketSpacing": true, + "bracketSameLine": true, + "jsxBracketSameLine": true, + "jsxSingleQuote": false, + "printWidth": 120, + "semi": true, + "tabWidth": 2, + "trailingComma": "es5", + "endOfLine": "auto", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/components/common/CardContent/CardContent.tsx b/components/common/CardContent/CardContent.tsx index ab45e0b306..b19e819765 100644 --- a/components/common/CardContent/CardContent.tsx +++ b/components/common/CardContent/CardContent.tsx @@ -1,8 +1,8 @@ import { KebabMenu } from "@components/folder/KebabMenu/KebabMenu"; import * as S from "./CardContentStyled"; -import { MouseEvent, useState } from "react"; -import { FolderListDataForm } from "../../../types/DataForm"; import Image from "next/image"; +import { usePortalContents } from "@hooks/usePortalContents"; +import { FolderListDataForm } from "@data-access/getCategory"; interface CardContentProps { elapsedTime: string; @@ -11,6 +11,8 @@ interface CardContentProps { isHovered: boolean; currentLocation: string; selectURL: string; + folderList: FolderListDataForm[]; + linkId: number; } export const CardContent = ({ @@ -20,28 +22,28 @@ export const CardContent = ({ isHovered, currentLocation, selectURL, + folderList, + linkId, }: CardContentProps) => { - const [isOpened, setIsClick] = useState(false); - const className = isHovered - ? "CardContent CardContent-hovered" - : "CardContent"; - - const handleClickMenu = (e: MouseEvent) => { - e.preventDefault(); - setIsClick(isOpened === false ? true : false); - }; + const kebabMenu = usePortalContents(); return ( -
+ <> {elapsedTime} - {currentLocation === "/folder" && ( - + {!currentLocation.includes("shared") && ( + 메뉴 보기 )} - {isOpened && } -
+ {kebabMenu.isOpenModal && ( + + )} + {description} {createdAt} diff --git a/components/common/CardItem/CardItem.tsx b/components/common/CardItem/CardItem.tsx index aeede60c98..d75d889e61 100644 --- a/components/common/CardItem/CardItem.tsx +++ b/components/common/CardItem/CardItem.tsx @@ -13,6 +13,10 @@ export const CardItem = ({ image_source, description, created_at, + folderList, + linkId, + favorite, + folderId, }: CardInfoDataForm) => { const [isHovered, setIsHovered] = useState(false); const location = useRouter(); @@ -26,7 +30,13 @@ export const CardItem = ({ - {currentLocation === "/folder" && } + {currentLocation.includes("folder") && ( + + )} diff --git a/components/common/CardList/CardListStyled.ts b/components/common/CardList/CardListStyled.ts index 16c1dfdb57..d4110b6cb2 100644 --- a/components/common/CardList/CardListStyled.ts +++ b/components/common/CardList/CardListStyled.ts @@ -7,6 +7,7 @@ export const CardListContainer = styled.div` width: 100%; max-width: 106rem; row-gap: 2rem; + min-height: 10rem; @media (min-width: 768px) { grid-template-columns: repeat(auto-fill, 34rem); diff --git a/components/common/EmptyLink/EmptyLinkStyled.ts b/components/common/EmptyLink/EmptyLinkStyled.ts index 90ac4ea4e9..5d90cfd6ba 100644 --- a/components/common/EmptyLink/EmptyLinkStyled.ts +++ b/components/common/EmptyLink/EmptyLinkStyled.ts @@ -1,6 +1,9 @@ import styled from "styled-components"; export const EmptyLinkContainer = styled.div` + position: absolute; + left: 50%; + transform: translate(-50%, 0); width: auto; height: 10rem; margin-top: 4rem; diff --git a/components/common/Layout/Layout.tsx b/components/common/Layout/Layout.tsx index 5f25f7766c..1bbea0bdf6 100644 --- a/components/common/Layout/Layout.tsx +++ b/components/common/Layout/Layout.tsx @@ -1,21 +1,8 @@ import Footer from "../Footer"; -import { PropsWithChildren, useEffect } from "react"; +import { PropsWithChildren } from "react"; import { NavigationBar } from "../NavigationBar"; -import { useRouter } from "next/router"; export const Layout = ({ children }: PropsWithChildren) => { - const router = useRouter(); - const currentPath = router.pathname; - - useEffect(() => { - const localStorageToken = localStorage.getItem("accessToken"); - if (currentPath !== "/") { - if (localStorageToken === null) { - router.push("/signin"); - } - } - }, [currentPath, router]); - return ( <>
diff --git a/components/common/Modals/AddFolder/AddFolder.tsx b/components/common/Modals/AddFolder/AddFolder.tsx new file mode 100644 index 0000000000..b7fe078402 --- /dev/null +++ b/components/common/Modals/AddFolder/AddFolder.tsx @@ -0,0 +1,45 @@ +import { ModalInput } from "../ModalElements/ModalInput"; +import Modal from "../Modal"; +import { BaseModalProps } from "../ModalProp"; +import { PrimaryButton } from "@styles/common/PrimaryButton"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useInputValue } from "@hooks/useInputValue"; +import { MouseEvent } from "react"; +import { postNewFolder } from "@data-access/postNewFolder"; + +export function AddFolder({ handleCloseModal }: BaseModalProps) { + const { insertValue, onChange } = useInputValue(); + const queryClient = useQueryClient(); + const createFolderMutation = useMutation({ + mutationFn: (createFolderName: string) => + postNewFolder({ folderName: createFolderName }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["folderList"], + }); + }, + }); + + const handleCreateNewFolder = async ( + event: MouseEvent + ) => { + const createFolderName = insertValue; + + createFolderMutation.mutate(createFolderName); + handleCloseModal(event); + }; + + return ( + + + + 추가하기 + + + ); +} diff --git a/components/common/Modals/AddFolder/index.ts b/components/common/Modals/AddFolder/index.ts new file mode 100644 index 0000000000..8e8c4420bd --- /dev/null +++ b/components/common/Modals/AddFolder/index.ts @@ -0,0 +1 @@ +export * from "./AddFolder"; diff --git a/components/common/Modals/AddFolderContent/AddFolderContent.tsx b/components/common/Modals/AddFolderContent/AddFolderContent.tsx deleted file mode 100644 index 0584b1dfd5..0000000000 --- a/components/common/Modals/AddFolderContent/AddFolderContent.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { ModalInput } from "../ModalElements/ModalInput"; -import { ModalButtonBlue } from "../ModalElements/ModalButtonBlue"; - -export function AddFolderContent() { - return ( - <> - - 추가하기 - - ); -} diff --git a/components/common/Modals/AddFolderContent/index.ts b/components/common/Modals/AddFolderContent/index.ts deleted file mode 100644 index b990ef07b9..0000000000 --- a/components/common/Modals/AddFolderContent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./AddFolderContent"; diff --git a/components/common/Modals/AddToFolder/AddToFolder.tsx b/components/common/Modals/AddToFolder/AddToFolder.tsx index d97fff575e..a13c7092fe 100644 --- a/components/common/Modals/AddToFolder/AddToFolder.tsx +++ b/components/common/Modals/AddToFolder/AddToFolder.tsx @@ -1,21 +1,73 @@ -import { ModalButtonBlue } from "../ModalElements/ModalButtonBlue"; import * as S from "./AddToFolderStyled"; +import Modal from "../Modal"; import { AddToFolderProps } from "../ModalProp"; +import { PrimaryButton } from "@styles/common/PrimaryButton"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { MouseEvent, useState } from "react"; +import { postAddToFolder } from "@data-access/postAddToFolder"; + +export function AddToFolder({ + linkURL, + folderList, + handleCloseModal, + handleReset, +}: AddToFolderProps) { + const [selectFolderId, setSelectFolderId] = useState(); + const queryClient = useQueryClient(); + const addToFolderMutation = useMutation({ + mutationFn: ({ url, folderId }: { url: string; folderId: number }) => + postAddToFolder({ url, folderId }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [`folderContents-${selectFolderId}`], + }); + queryClient.invalidateQueries({ + queryKey: [`folderContents-${""}`], + }); + }, + }); + + const handleFolderId = (event: MouseEvent) => { + setSelectFolderId(Number(event.currentTarget.id)); + }; + + const handleAddToFolder = async (event: MouseEvent) => { + if (linkURL && selectFolderId) { + addToFolderMutation.mutate({ url: linkURL, folderId: selectFolderId }); + handleCloseModal(event); + if(handleReset){ + handleReset(); + } + } else if (!linkURL && selectFolderId){ + alert("url을 입력해주세요."); + } else if (linkURL && !selectFolderId){ + alert("폴더를 선택해주세요."); + } + }; -export function AddToFolder({ linkURL, data }: AddToFolderProps) { return ( - <> + {linkURL} - {data?.map((folder) => ( - - {folder.name} - {folder.link.count}개 링크 - - - ))} + {folderList?.map((folder) => { + console.log(selectFolderId === folder.id); + return ( + + {folder.name} + {folder.link_count}개 링크 + + + ); + })} - 삭제하기 - + + 추가하기 + + ); } diff --git a/components/common/Modals/AddToFolder/AddToFolderStyled.ts b/components/common/Modals/AddToFolder/AddToFolderStyled.ts index 7b0587ac6a..79b0c173f5 100644 --- a/components/common/Modals/AddToFolder/AddToFolderStyled.ts +++ b/components/common/Modals/AddToFolder/AddToFolderStyled.ts @@ -1,5 +1,10 @@ import styled from "styled-components"; +export const AddToFolderContainer = styled.div` + width: 30rem; + display: flex; + flex-direction: column; +`; export const FolderListContainer = styled.div` display: flex; flex-direction: column; @@ -8,24 +13,27 @@ export const FolderListContainer = styled.div` overflow: hidden scroll; `; -export const SelectFolder = styled.div` +export const SelectFolder = styled.button<{ isSelect: boolean }>` display: flex; flex-direction: row; justify-content: flex-start; align-items: center; padding: 0.8rem; gap: 0.8rem; - - &:hover { - background-color: var(--light-blue); - } + background-color: ${({ isSelect }) => + isSelect ? "var(--light-blue)" : "transparent"}; `; export const SelectLink = styled.p` + width: 30rem; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; color: var(--gray60); font-size: 1.4rem; line-height: 2.2rem; text-align: center; + margin: 0.5rem 0 2.5rem 0; `; export const FolderName = styled.p` diff --git a/components/common/Modals/DeleteFolder/DeleteFolder.tsx b/components/common/Modals/DeleteFolder/DeleteFolder.tsx index 62f55ef797..485624f7f1 100644 --- a/components/common/Modals/DeleteFolder/DeleteFolder.tsx +++ b/components/common/Modals/DeleteFolder/DeleteFolder.tsx @@ -1,12 +1,36 @@ import * as S from "./DeleteFolderStyled"; import { ModalButtonRed } from "../ModalElements/ModalButtonRed"; import { DeleteFolderProps } from "../ModalProp"; +import Modal from "../Modal"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { MouseEvent } from "react"; +import { deleteFolder } from "@data-access/deleteFolder"; + +export default function DeleteFolder({ + selectFolder, + folderId, + handleCloseModal, +}: DeleteFolderProps) { + const queryClient = useQueryClient(); + const deleteFolderMutation = useMutation({ + mutationFn: ({ folderId }: { folderId: number | string }) => + deleteFolder({ folderId }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["folderList"], + }); + }, + }); + + const handleDeleteFolder = (event: MouseEvent) => { + deleteFolderMutation.mutate({ folderId: folderId }); + handleCloseModal(event); + }; -export default function DeleteFolder({ selectFolder }: DeleteFolderProps) { return ( - <> + {selectFolder} - 삭제하기 - + 삭제하기 + ); } diff --git a/components/common/Modals/DeleteLink/DeleteLink.tsx b/components/common/Modals/DeleteLink/DeleteLink.tsx index 1d9360e58c..17b503117d 100644 --- a/components/common/Modals/DeleteLink/DeleteLink.tsx +++ b/components/common/Modals/DeleteLink/DeleteLink.tsx @@ -1,12 +1,30 @@ import { ModalButtonRed } from "../ModalElements/ModalButtonRed"; import * as S from "./DeleteLinkStyled"; import { DeleteLinkProps } from "../ModalProp"; +import Modal from "../Modal"; +import { useMutation } from "@tanstack/react-query"; +import { deleteLink } from "@data-access/deleteLink"; +import { MouseEvent } from "react"; + +export default function DeleteLink({ + deleteURL, + handleCloseModal, + linkId, +}: DeleteLinkProps) { + const deleteLinkMutation = useMutation({ + mutationFn: ({ linkId }: { linkId: number }) => deleteLink({ linkId }), + }); + + const handleDeleteLink = (event: MouseEvent) => { + deleteLinkMutation.mutate({ linkId: Number(event.currentTarget.id) }); + }; -export default function DeleteLink({ deleteURL }: DeleteLinkProps) { return ( - <> + {deleteURL} - 삭제하기 - + + 삭제하기 + + ); } diff --git a/components/common/Modals/Modal/Modal.tsx b/components/common/Modals/Modal/Modal.tsx index ace8102283..9d98caf438 100644 --- a/components/common/Modals/Modal/Modal.tsx +++ b/components/common/Modals/Modal/Modal.tsx @@ -1,25 +1,20 @@ import ModalPortal from "@components/Portal"; -import { ModalCloseButton } from "../ModalElements/ModalCloseButton"; -import { ModalContainer } from "../ModalElements/ModalContainer"; +import { ModalProps } from "../ModalProp"; import { ModalDim } from "../ModalElements/ModalDim"; +import { ModalContainer } from "../ModalElements/ModalContainer"; +import { ModalCloseButton } from "../ModalElements/ModalCloseButton"; import { ModalTitle } from "../ModalElements/ModalTitle"; -import { ModalProps } from "../ModalProp"; -import { ModalContext } from "@components/common/RefactorModal/ModalContext"; -import { useContext } from "react"; - -export default function Modal({ children, title }: ModalProps) { - const { handleModalState } = useContext(ModalContext); - function handleCloseModal() { - handleModalState({ - isOpenModal: false, - }); - } +export default function Modal({ + children, + title, + handleCloseModal, +}: ModalProps) { return ( - + {title} {children} diff --git a/components/common/Modals/ModalElements/ModalButtonBlue.ts b/components/common/Modals/ModalElements/ModalButtonBlue.ts deleted file mode 100644 index 8d1d6862e6..0000000000 --- a/components/common/Modals/ModalElements/ModalButtonBlue.ts +++ /dev/null @@ -1,12 +0,0 @@ -import styled from "styled-components"; - -export const ModalButtonBlue = styled.button` - width: 28rem; - border-radius: 0.8rem; - padding: 1.6rem 2rem; - background: linear-gradient(90.99deg, #6d6afe 0.12%, #6ae3fe 101.84%); - font-size: 1.6rem; - line-height: 1.909rem; - font-weight: 600; - color: var(--gray-light); -`; diff --git a/components/common/Modals/ModalElements/ModalCloseButton.tsx b/components/common/Modals/ModalElements/ModalCloseButton.tsx index 724b0e6d60..e1f87a7c1b 100644 --- a/components/common/Modals/ModalElements/ModalCloseButton.tsx +++ b/components/common/Modals/ModalElements/ModalCloseButton.tsx @@ -12,9 +12,9 @@ export const CloseButtonModal = styled.button` right: 1.2rem; `; -export function ModalCloseButton({ handleModalClose }: BaseModalProps) { +export function ModalCloseButton({ handleCloseModal }: BaseModalProps) { return ( - + X ); diff --git a/components/common/Modals/ModalProp.ts b/components/common/Modals/ModalProp.ts index ef312a5486..d950f6dfba 100644 --- a/components/common/Modals/ModalProp.ts +++ b/components/common/Modals/ModalProp.ts @@ -1,25 +1,35 @@ +import { FolderListDataForm } from "@data-access/getCategory"; import { MouseEvent } from "react"; -import { FolderListDataForm } from "../../../types/DataForm"; export interface BaseModalProps { - isOpenModal?: boolean; - handleModalClose: (e: MouseEvent) => void; + handleCloseModal: (event: MouseEvent) => void; } -export interface DeleteLinkProps { +export interface DeleteLinkProps extends BaseModalProps { deleteURL: string; + linkId: number; } -export interface DeleteFolderProps { +export interface DeleteFolderProps extends BaseModalProps { selectFolder: string; + folderId: number; } -export interface ModalProps { +export interface ModalProps extends BaseModalProps { children: JSX.Element | JSX.Element[]; title: string; } -export interface AddToFolderProps { +export interface AddToFolderProps extends BaseModalProps { linkURL: string | undefined; - data: FolderListDataForm[] | undefined; + folderList: FolderListDataForm[] | undefined; + handleReset?: () => void; +} + +export interface RenameFolderProps extends BaseModalProps { + selectFolderId: number; +} + +export interface SharedFolderProps extends BaseModalProps { + selectFolder: string; } diff --git a/components/common/Modals/RenameModal/RenameModal.tsx b/components/common/Modals/RenameModal/RenameModal.tsx index f67d4efa03..4db84c73e5 100644 --- a/components/common/Modals/RenameModal/RenameModal.tsx +++ b/components/common/Modals/RenameModal/RenameModal.tsx @@ -1,13 +1,51 @@ +import { PrimaryButton } from "@styles/common/PrimaryButton"; import Modal from "../Modal"; -import { ModalButtonBlue } from "../ModalElements/ModalButtonBlue"; import { ModalInput } from "../ModalElements/ModalInput"; -import { BaseModalProps } from "../ModalProp"; +import { RenameFolderProps } from "../ModalProp"; +import { MouseEvent } from "react"; +import { useInputValue } from "@hooks/useInputValue"; +import { putRenameFolder } from "@data-access/putRenameFolder"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +export function RenameModal({ + handleCloseModal, + selectFolderId, +}: RenameFolderProps) { + const { insertValue, onChange } = useInputValue(); + const queryClient = useQueryClient(); + const renameFolderMutation = useMutation({ + mutationFn: ({ + folderId, + folderTitle, + }: { + folderId: number; + folderTitle: string; + }) => putRenameFolder({ folderId, folderTitle }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["folderList"] }); + }, + }); + + async function handleRenameFolder(event: MouseEvent) { + if (insertValue) { + renameFolderMutation.mutate({ + folderId: selectFolderId, + folderTitle: insertValue, + }); + } + handleCloseModal(event); + } -export function RenameModal() { return ( - <> - - 변경하기 - + + + + 변경하기 + + ); } diff --git a/components/common/Modals/SharedFolder/SharedFolder.tsx b/components/common/Modals/SharedFolder/SharedFolder.tsx index 96fb666473..3ee25c7df0 100644 --- a/components/common/Modals/SharedFolder/SharedFolder.tsx +++ b/components/common/Modals/SharedFolder/SharedFolder.tsx @@ -1,8 +1,9 @@ import * as S from "./SharedFolderStyled"; -import { DeleteFolderProps } from "../ModalProp"; +import { SharedFolderProps } from "../ModalProp"; import { handleCopyClipBoard } from "@util/copyClipBoard"; import Link from "next/link"; import { shareKakao } from "@util/sharedKakao"; +import Modal from "../Modal"; declare global { interface Window { @@ -10,13 +11,16 @@ declare global { } } -export function SharedFolder({ selectFolder }: DeleteFolderProps) { +export function SharedFolder({ + selectFolder, + handleCloseModal, +}: SharedFolderProps) { const handleSharedKakao = () => { shareKakao("https://codingaring-week11-linkbrary.netlify.app"); }; https: return ( - <> + {selectFolder} @@ -45,6 +49,6 @@ export function SharedFolder({ selectFolder }: DeleteFolderProps) { 링크 복사 - + ); } diff --git a/components/common/Modals/SharedFolder/SharedFolderStyled.ts b/components/common/Modals/SharedFolder/SharedFolderStyled.ts index 7cbb4ff8a5..e05cec4bd0 100644 --- a/components/common/Modals/SharedFolder/SharedFolderStyled.ts +++ b/components/common/Modals/SharedFolder/SharedFolderStyled.ts @@ -32,6 +32,7 @@ export const SharedButton = styled.button` display: flex; flex-direction: column; justify-content: center; + align-items: center; gap: 1rem; & img { diff --git a/components/common/NavigationBar/NavigationBar.tsx b/components/common/NavigationBar/NavigationBar.tsx index 5a490d4825..3c3559535d 100644 --- a/components/common/NavigationBar/NavigationBar.tsx +++ b/components/common/NavigationBar/NavigationBar.tsx @@ -3,38 +3,25 @@ import { Profile } from "./Profile"; import { LOGO_IMAGE, TEXT } from "./constant"; import { useRouter } from "next/router"; import { ROUTE } from "@util/constant"; -import { useEffectOnce } from "@hooks/useEffectOnce"; import { getLoginUserInfo } from "@data-access/getLoginUserInfo"; -import { useContext, useState } from "react"; +import { useContext, useEffect } from "react"; import { UserContext } from "context/UserContext"; +import { useQuery } from "@tanstack/react-query"; export const NavigationBar = () => { const { handleUserDataState } = useContext(UserContext); - const router = useRouter(); - const [profile, setProfile] = useState({ - auth_id: "", - email: "", - image_source: "", - }); const Location = useRouter(); const LocationPath = Location.pathname; + const { data: profile } = useQuery({ + queryKey: ["loginUserProfile"], + queryFn: getLoginUserInfo, + }); - async function handleLoadUserInfo() { - try { - const { data } = await getLoginUserInfo(); - const { email, image_source, auth_id } = data[0] || []; - setProfile({ - auth_id: auth_id, - email: email, - image_source: image_source, - }); - handleUserDataState({ userId: auth_id }); - } catch (error) { - router.push("/signin"); + useEffect(() => { + if (profile) { + handleUserDataState({ isLogin: true, userId: profile.id }); } - } - - useEffectOnce(handleLoadUserInfo); + }, [profile]); return ( diff --git a/components/common/NavigationBar/NavigationBarStyled.ts b/components/common/NavigationBar/NavigationBarStyled.ts index 2a141b5d98..09e8f6c7eb 100644 --- a/components/common/NavigationBar/NavigationBarStyled.ts +++ b/components/common/NavigationBar/NavigationBarStyled.ts @@ -13,7 +13,7 @@ export const NavigationBarContainer = styled.header<{ pathName: string }>` position: sticky; top: 0; `} - z-index: var(--z-index-nav); + z-index: 1; width: 100%; background-color: var(--light-blue); `; diff --git a/components/common/RefactorModal/ModalContext.ts b/components/common/RefactorModal/ModalContext.ts deleted file mode 100644 index f15e9d6691..0000000000 --- a/components/common/RefactorModal/ModalContext.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { FolderListDataForm } from "../../../types/DataForm"; -import React from "react"; - -export type ModalContextType = { - modalStateProperty: { - isOpenModal: boolean; - selectURL: string; - data: FolderListDataForm[] | undefined; - selectFolder: string; - modalType: string; - selectFolderId: string; - }; - handleModalState: ( - newState: Partial - ) => void; -}; - -export const ModalContextInitial: ModalContextType = { - modalStateProperty: { - isOpenModal: false, - selectURL: "", - data: [], - selectFolder: "", - modalType: "", - selectFolderId: "", - }, - handleModalState: () => {}, -}; - -export const ModalContext = React.createContext(ModalContextInitial); diff --git a/components/common/RefactorModal/ModalProvider.tsx b/components/common/RefactorModal/ModalProvider.tsx deleted file mode 100644 index bd5446ee36..0000000000 --- a/components/common/RefactorModal/ModalProvider.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ReactNode, useState } from "react"; -import { - ModalContext, - ModalContextInitial, - ModalContextType, -} from "./ModalContext"; - -export function ModalProvider({ children }: { children: ReactNode }) { - const [modalState, setModalState] = useState< - ModalContextType["modalStateProperty"] - >(ModalContextInitial["modalStateProperty"]); - - function handleModalState( - newState: Partial - ) { - setModalState((prevState) => ({ - ...prevState, - ...newState, - })); - } - - const modalStateValue: ModalContextType = { - modalStateProperty: modalState, - handleModalState, - }; - - return ( - - {children} - - ); -} diff --git a/components/common/RefactorModal/RefactorModal.tsx b/components/common/RefactorModal/RefactorModal.tsx deleted file mode 100644 index ca3fe18def..0000000000 --- a/components/common/RefactorModal/RefactorModal.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useContext } from "react"; -import { AddFolderContent } from "../Modals/AddFolderContent"; -import { AddToFolder } from "../Modals/AddToFolder"; -import DeleteFolder from "../Modals/DeleteFolder"; -import DeleteLink from "../Modals/DeleteLink"; -import { RenameModal } from "../Modals/RenameModal"; -import { SharedFolder } from "../Modals/SharedFolder/SharedFolder"; -import { ModalContext } from "./ModalContext"; -import Modal from "../Modals/Modal"; - -export function RefactorModal() { - const { modalStateProperty } = useContext(ModalContext); - const { modalType, isOpenModal, selectURL, selectFolder, data } = - modalStateProperty; - - let modalContent; - let modalTitle = ""; - - switch (modalType) { - case "deleteLink": - modalContent = ; - modalTitle = "링크 삭제"; - break; - case "addToFolder": - modalContent = ; - modalTitle = "폴더에 추가"; - break; - case "addFolderContent": - modalContent = ; - modalTitle = "폴더 추가"; - break; - case "sharedFolder": - modalContent = ; - modalTitle = "폴더 공유"; - break; - case "renameModal": - modalContent = ; - modalTitle = "폴더 이름 변경"; - break; - case "deleteFolder": - modalContent = ; - modalTitle = "폴더 삭제"; - break; - default: - modalContent = <>; - } - - return isOpenModal ? {modalContent} : <>; -} diff --git a/components/folder/CategoryButton/CategoryButtonStyled.ts b/components/folder/CategoryButton/CategoryButtonStyled.ts index a1d9d02a37..21d0c7a4ea 100644 --- a/components/folder/CategoryButton/CategoryButtonStyled.ts +++ b/components/folder/CategoryButton/CategoryButtonStyled.ts @@ -1,15 +1,12 @@ import styled from "styled-components"; -export const Button = styled.button` +export const Button = styled.button<{ isSelectCategory: boolean }>` padding: 0.5rem 0.75rem; font-weight: 400; line-height: 1.193125rem; border: 0.0625rem solid var(--primary); border-radius: 0.3125rem; - - &:hover, - &:active { - background-color: var(--primary); - color: #fff; - } + background-color: ${({ isSelectCategory }) => + isSelectCategory ? "var(--primary)" : "#fff"}; + color: ${({ isSelectCategory }) => (isSelectCategory ? "#fff" : "#000")}; `; diff --git a/components/folder/CategoryNav/CategoryNavButtons/CategoryNavButtons.tsx b/components/folder/CategoryNav/CategoryNavButtons/CategoryNavButtons.tsx index 6d5bb30382..e434284903 100644 --- a/components/folder/CategoryNav/CategoryNavButtons/CategoryNavButtons.tsx +++ b/components/folder/CategoryNav/CategoryNavButtons/CategoryNavButtons.tsx @@ -1,9 +1,10 @@ -import { MouseEvent, useContext, useState } from "react"; import { DELETE_ICON, RENAME_ICON, SHARED_ICON } from "./constant"; import * as S from "./CategoryNavButtonsStyled"; import Image from "next/image"; -import { ModalContext } from "@components/common/RefactorModal/ModalContext"; -import { RefactorModal } from "@components/common/RefactorModal/RefactorModal"; +import { usePortalContents } from "@hooks/usePortalContents"; +import { RenameModal } from "@components/common/Modals/RenameModal"; +import DeleteFolder from "@components/common/Modals/DeleteFolder"; +import { SharedFolder } from "@components/common/Modals/SharedFolder/SharedFolder"; export function CategoryNavButtons({ selectFolder, @@ -12,52 +13,53 @@ export function CategoryNavButtons({ selectFolder: string; folderId: string; }) { - const { handleModalState } = useContext(ModalContext); - - const handleShowModal = (e: MouseEvent) => { - e.preventDefault(); - - switch (e.currentTarget.id) { - case "sharedFolder": - handleModalState({ - isOpenModal: true, - modalType: "sharedFolder", - selectFolder: selectFolder, - selectFolderId: folderId, - }); - break; - case "renameModal": - handleModalState({ - isOpenModal: true, - modalType: "renameModal", - }); - break; - case "deleteFolder": - handleModalState({ - isOpenModal: true, - modalType: "deleteFolder", - selectFolder: selectFolder, - }); - break; - } - }; + const shardFolderModal = usePortalContents(); + const renameModal = usePortalContents(); + const deleteFolderModal = usePortalContents(); return ( <> - - + {shardFolderModal.isOpenModal && ( + + )} + {renameModal.isOpenModal && ( + + )} + {deleteFolderModal.isOpenModal && ( + + )} + 공유하기를 나타내는 아이콘

공유

- + 이름 변경하기를 나타내는 아이콘

이름 변경

- + 삭제하기를 나타내는 아이콘 diff --git a/components/folder/FolderContent/FolderContent.tsx b/components/folder/FolderContent/FolderContent.tsx index 11cb145c9b..c142ba5862 100644 --- a/components/folder/FolderContent/FolderContent.tsx +++ b/components/folder/FolderContent/FolderContent.tsx @@ -1,50 +1,50 @@ import * as S from "./FolderContentStyled"; -import { useEffect, useState, MouseEvent, useContext } from "react"; +import { useEffect, useState, MouseEvent } from "react"; import { CategoryNav } from "../CategoryNav/CategoryNav"; -import { FolderListDataForm, getFolderDataForm } from "../../../types/DataForm"; import { useRecoilValue } from "recoil"; import { searchState } from "recoil/SearchKeyWord"; -import { getFolders } from "@data-access/getFolders"; import { SearchResultComment } from "@components/common/SearchResultComment"; import { Button } from "../CategoryButton/CategoryButtonStyled"; import { EmptyLink } from "@components/common/EmptyLink"; import { CardList } from "@components/common/CardList"; import { CardItem } from "@components/common/CardItem"; -import { RefactorModal } from "@components/common/RefactorModal/RefactorModal"; -import { ModalContext } from "@components/common/RefactorModal/ModalContext"; import { useRouter } from "next/router"; +import { usePortalContents } from "@hooks/usePortalContents"; +import { AddFolder } from "@components/common/Modals/AddFolder"; +import { useQuery } from "@tanstack/react-query"; +import { getFolders } from "@data-access/getFolders"; +import { FolderListDataForm } from "@data-access/getCategory"; interface LoadFolderDataProps { folderId: string; searchKeyWord: string; } -export function FolderContent({ data }: { data: FolderListDataForm[] }) { - const [folder, setFolder] = useState([]); +interface FolderContents { + id: number; + favorite: boolean; + created_at: string; + url: string; + title: string; + image_source: string; + description: string; +} + +export function FolderContent({ + folderInfo, +}: { + folderInfo: FolderListDataForm[] | undefined; +}) { + const [folder, setFolder] = useState([]); const [folderId, setFolderId] = useState(""); const [activeCategoryName, setActiveCategoryName] = useState("전체"); - const { handleModalState } = useContext(ModalContext); const searchKeyWord = useRecoilValue(searchState); const router = useRouter(); - - const handleLoadFolder = async ({ - folderId, - searchKeyWord, - }: LoadFolderDataProps) => { - const { data } = await getFolders(folderId); - setFolder(data); - - if (searchKeyWord) { - setFolder((prevFolder) => - prevFolder.filter( - (link) => - link.description?.includes(searchKeyWord) || - link.url?.includes(searchKeyWord) || - link.title?.includes(searchKeyWord) - ) - ); - } - }; + const addFolderModal = usePortalContents(); + const { data: folderContents } = useQuery({ + queryKey: [`folderContents-${folderId}`], + queryFn: () => getFolders({ folderId: Number(folderId) }), + }); const handleCategoryActive = (e: MouseEvent) => { setActiveCategoryName(e.currentTarget.value); @@ -54,36 +54,59 @@ export function FolderContent({ data }: { data: FolderListDataForm[] }) { router.push(folderPath); }; - const handleShowModal = () => { - handleModalState({ isOpenModal: true, modalType: "addFolderContent" }); - }; - useEffect(() => { + const handleLoadFolder = async ({ searchKeyWord }: LoadFolderDataProps) => { + if (folderContents) { + setFolder(folderContents); + } + + if (searchKeyWord) { + setFolder((prevFolder) => + prevFolder.filter( + (link) => + link.description?.includes(searchKeyWord) || + link.url?.includes(searchKeyWord) || + link.title?.includes(searchKeyWord) + ) + ); + } + }; handleLoadFolder({ folderId, searchKeyWord }); - }, [folderId, searchKeyWord]); + }, [folderId, searchKeyWord, folderContents]); return ( <> - + {addFolderModal.isOpenModal && ( + + )} {searchKeyWord && } - - {data.map((category) => ( - - ))} + {folderInfo && + folderInfo.map((category) => ( + + ))} - + 폴더 추가 + @@ -91,21 +114,25 @@ export function FolderContent({ data }: { data: FolderListDataForm[] }) { activeCategoryName={activeCategoryName} folderId={folderId} /> - {!folder.length ? ( - - ) : ( - - {folder?.map((link) => ( + + {folder && folder.length !== 0 ? ( + folder.map((link) => ( - ))} - - )} + )) + ) : ( + + )} + ); } diff --git a/components/folder/FolderContent/FolderContentStyled.ts b/components/folder/FolderContent/FolderContentStyled.ts index 9f0ae2565c..2b34df85f9 100644 --- a/components/folder/FolderContent/FolderContentStyled.ts +++ b/components/folder/FolderContent/FolderContentStyled.ts @@ -5,10 +5,18 @@ export const ClassificationContainer = styled.div` display: flex; justify-content: space-between; `; + export const ClassificationButtons = styled.div` display: flex; flex-direction: row; gap: 0.5rem; + max-width: 97rem; + overflow-x: auto; + white-space: nowrap; + cursor: grab; + use-select: none; + scrollbar-width: thin; + padding-bottom: 0.5rem; @media (max-width: 768px) { font-size: 1.4rem; @@ -16,6 +24,7 @@ export const ClassificationButtons = styled.div` flex-wrap: wrap; } `; + export const AddFolderButton = styled.button` line-height: 1.909rem; text-align: center; diff --git a/components/folder/FolderHeader/FolderHeader.tsx b/components/folder/FolderHeader/FolderHeader.tsx index 9466f2a68b..3d4ff88939 100644 --- a/components/folder/FolderHeader/FolderHeader.tsx +++ b/components/folder/FolderHeader/FolderHeader.tsx @@ -1,21 +1,22 @@ -import { forwardRef, useEffect, useState } from "react"; -import { useGetUser as getUser } from "@data-access/useGetUser"; -import { FolderListDataForm } from "../../../types/DataForm"; -import { DEFAULT_IMAGE } from "@components/common/CardImage/constant"; +import { forwardRef } from "react"; +import * as S from "./FolderHeaderStyled"; import { NavigationBar } from "@components/common/NavigationBar"; import { AddLinkBar } from "@components/shared/AddLinkBar/AddLinkBar"; -import * as S from "./FolderHeaderStyled"; +import { FolderListDataForm } from "@data-access/getCategory"; const FolderHeader = forwardRef( ( - { data, isFloating }: { data: FolderListDataForm[]; isFloating: boolean }, + { + folderInfo, + isFloating, + }: { folderInfo: FolderListDataForm[] | undefined; isFloating: boolean }, ref ) => { return (
}> - - + +
); diff --git a/components/folder/KebabMenu/KebabMenu.tsx b/components/folder/KebabMenu/KebabMenu.tsx index bb24067965..509d219289 100644 --- a/components/folder/KebabMenu/KebabMenu.tsx +++ b/components/folder/KebabMenu/KebabMenu.tsx @@ -1,62 +1,45 @@ -import { useContext, useEffect, useState } from "react"; import * as S from "./KebabMenuStyled"; -import { MouseEvent } from "react"; -import { ModalContext } from "@components/common/RefactorModal/ModalContext"; -import { RefactorModal } from "@components/common/RefactorModal/RefactorModal"; -import { getCategory } from "@data-access/getCategory"; -import { useEffectOnce } from "@hooks/useEffectOnce"; +import { usePortalContents } from "@hooks/usePortalContents"; +import DeleteLink from "@components/common/Modals/DeleteLink"; +import { AddToFolder } from "@components/common/Modals/AddToFolder"; +import { FolderListDataForm } from "@data-access/getCategory"; -interface Props { +interface KebabProps { selectURL: string; + folderList: FolderListDataForm[]; + linkId: number; } -export function KebabMenu({ selectURL }: Props) { - const [categoryList, setCategoryList] = useState(); - const { handleModalState } = useContext(ModalContext); - - const handleShowModal = (e: MouseEvent) => { - e.preventDefault(); - - const handleCategoryList = () => { - const { data }: any = getCategory(); - setCategoryList(data); - }; - - handleCategoryList(); - - switch (e.currentTarget.id) { - case "deleteLink": - handleModalState({ - isOpenModal: true, - selectURL: selectURL, - modalType: "deleteLink", - }); - break; - case "addToFolder": - handleModalState({ - isOpenModal: true, - selectURL: selectURL, - data: categoryList, - modalType: "addToFolder", - }); - } - }; +export function KebabMenu({ selectURL, folderList, linkId }: KebabProps) { + const deleteLinkModal = usePortalContents(); + const addToFolderModal = usePortalContents(); return ( <> - + {deleteLinkModal.isOpenModal && ( + + )} + {addToFolderModal.isOpenModal && ( + + )} 삭제하기 폴더에 추가 diff --git a/components/folder/WishListButton/WishListButton.tsx b/components/folder/WishListButton/WishListButton.tsx index 146c6d22ae..8938f765f1 100644 --- a/components/folder/WishListButton/WishListButton.tsx +++ b/components/folder/WishListButton/WishListButton.tsx @@ -3,21 +3,56 @@ import { EMPTY_STAR, FULL_STAR } from "./constant"; import * as S from "./WishListButtonStyled"; import { MouseEvent } from "react"; import Image from "next/image"; +import { useMutation } from "@tanstack/react-query"; +import { putBookmark } from "@data-access/putBookmark"; -export default function WishListButton() { - const [wishListBtn, setWishListBtn] = useState(EMPTY_STAR); +interface WishListButtonProp { + wishListBtn: string; + isFavorite: boolean; +} + +export default function WishListButton({ + linkId, + isFavorite, + folderId, +}: { + linkId: number; + isFavorite: boolean; + folderId: number; +}) { + const [wishListState, setWishListState] = useState({ + wishListBtn: EMPTY_STAR, + isFavorite: false, + }); + const wishListMutation = useMutation({ + mutationFn: async ({ + linkId, + isFavorite, + }: { + linkId: number; + isFavorite: boolean; + }) => { + await putBookmark({ linkId, isFavorite }); + }, + }); const handleWishListBtn = (e: MouseEvent) => { e.preventDefault(); - wishListBtn === EMPTY_STAR - ? setWishListBtn(FULL_STAR) - : setWishListBtn(EMPTY_STAR); + setWishListState((prevState) => ({ + wishListBtn: prevState.isFavorite ? FULL_STAR : EMPTY_STAR, + isFavorite: !prevState.isFavorite, + })); + wishListMutation.mutate({ linkId, isFavorite: wishListState.isFavorite }); }; return ( - - 즐겨찾기로 추가하기 버튼 + + 즐겨찾기로 추가하기 버튼 ); } diff --git a/components/home/LandingHeader/LandingHeader.tsx b/components/home/LandingHeader/LandingHeader.tsx index 0dbdf4492f..c424536a34 100644 --- a/components/home/LandingHeader/LandingHeader.tsx +++ b/components/home/LandingHeader/LandingHeader.tsx @@ -2,8 +2,22 @@ import Link from "next/link"; import { HERO_IMAGE_SRC } from "../constant"; import * as S from "./LandingHeaderStyled"; import Image from "next/image"; +import { useRouter } from "next/router"; +import { useContext, useEffect } from "react"; +import { UserContext } from "context/UserContext"; export default function LandingHeader() { + const router = useRouter(); + const { loginData } = useContext(UserContext); + + const handleGoFolders = () => { + if (loginData.isLogin) { + router.push("/folder"); + } else { + router.push("/signin"); + } + }; + return ( @@ -11,7 +25,9 @@ export default function LandingHeader() { 쉽게 저장하고 관리해 보세요 - 링크 추가하기 + + 링크 추가하기 + 링크브러리 메인 화면 diff --git a/components/shared/AddLinkBar/AddLinkBar.tsx b/components/shared/AddLinkBar/AddLinkBar.tsx index 05da15794f..3f2b8ee186 100644 --- a/components/shared/AddLinkBar/AddLinkBar.tsx +++ b/components/shared/AddLinkBar/AddLinkBar.tsx @@ -1,53 +1,44 @@ -import { useState, ChangeEvent, useContext } from "react"; import * as S from "./AddLinkBarStyled"; import { ADD_ICON } from "./constant"; -import { FolderListDataForm } from "../../../types/DataForm"; -import { ModalContext } from "@components/common/RefactorModal/ModalContext"; -import { RefactorModal } from "@components/common/RefactorModal/RefactorModal"; +import { FolderListDataForm } from "@data-access/getCategory"; +import { usePortalContents } from "@hooks/usePortalContents"; +import { AddToFolder } from "@components/common/Modals/AddToFolder"; +import { useInputValue } from "@hooks/useInputValue"; export function AddLinkBar({ - data, + folderInfo, isFloating = false, }: { - data: FolderListDataForm[]; + folderInfo: FolderListDataForm[] | undefined; isFloating?: boolean; }) { - const [inputValue, setInputValue] = useState(""); - const [isEmpty, setIsEmpty] = useState(false); - const { handleModalState } = useContext(ModalContext); - - const handleEmptyError = (e: React.FocusEvent) => { - setIsEmpty(e.target.value === "" ? true : false); - }; - - function handleShowModal() { - handleModalState({ - isOpenModal: true, - selectURL: inputValue, - data: data, - modalType: "addToFolder", - }); - } - - const handleInputValue = (e: ChangeEvent) => { - setInputValue(e.target.value); - }; + const { insertValue, onChange, handleReset } = useInputValue(); + const { isOpenModal, toggleContents } = usePortalContents(); return ( <> - + {isOpenModal && ( + + )} - + - + 추가하기 diff --git a/components/shared/AddLinkBar/AddLinkBarStyled.ts b/components/shared/AddLinkBar/AddLinkBarStyled.ts index 9f5b0c3059..6892e5cdc1 100644 --- a/components/shared/AddLinkBar/AddLinkBarStyled.ts +++ b/components/shared/AddLinkBar/AddLinkBarStyled.ts @@ -1,3 +1,4 @@ +import { PrimaryButton } from "@styles/common/PrimaryButton"; import styled from "styled-components"; export const AddLinkContainer = styled.div<{ isFloating: boolean }>` @@ -64,13 +65,9 @@ export const AddLinkInput = styled.input` } `; -export const AddInputButton = styled.button` - width: 8rem; +export const AddInputButton = styled(PrimaryButton)` font-size: 1.4rem; font-weight: 600; line-height: 1.671rem; - color: var(--gray-light); - background: linear-gradient(90.99deg, #6d6afe 0.12%, #6ae3fe 101.84%); padding: 1rem 1.6rem; - border-radius: 0.8rem; `; diff --git a/components/sign/signinForm/SignInForm.tsx b/components/sign/signinForm/SignInForm.tsx index ef0212c286..774a275a9b 100644 --- a/components/sign/signinForm/SignInForm.tsx +++ b/components/sign/signinForm/SignInForm.tsx @@ -1,15 +1,25 @@ -import { checkSignin } from "@data-access/checkSignin"; import { useRouter } from "next/router"; import { SubmitHandler, useForm } from "react-hook-form"; import { EMAIL_REGEX } from "../constant"; import * as S from "../SignFormStyled"; -import { useEffectOnce } from "@hooks/useEffectOnce"; +import { getLoginUserInfo } from "@data-access/getLoginUserInfo"; +import { useQuery } from "@tanstack/react-query"; +import { useEffect } from "react"; +import { getToken, removeToken } from "@util/handleToken"; +import { authAPI } from "@data-access/authAPI"; interface IFormInput { email: string; password: string; } +interface loginUserProfile { + id: number; + name: string; + image_source: string; + email: string; +} + export function SignForm() { const router = useRouter(); const { @@ -18,6 +28,10 @@ export function SignForm() { formState: { errors }, setError, } = useForm({ mode: "onBlur" }); + const { data: profile } = useQuery({ + queryKey: ["loginUserProfile"], + queryFn: getLoginUserInfo, + }); const handleInputValue: SubmitHandler = async (data) => { const inputValue = { email: data.email, @@ -25,8 +39,8 @@ export function SignForm() { }; try { - const data = await checkSignin(inputValue); - const { accessToken, refreshToken } = data; + const { accessToken, refreshToken } = + await authAPI.checkSignin(inputValue); router.push("/folder"); localStorage.setItem("accessToken", accessToken); localStorage.setItem("refreshToken", refreshToken); @@ -40,16 +54,22 @@ export function SignForm() { } }; - function hasAccessToken() { - const localStorageToken = localStorage.getItem("accessToken"); - if (localStorageToken === null) { - return; + function hasValidateAccessToken({ profile }: { profile?: loginUserProfile }) { + const accessToken = getToken(); + if (accessToken) { + if (profile) { + router.push("/folder"); + } else { + return; + } } else { - router.push("/folder"); + removeToken(); } } - useEffectOnce(hasAccessToken); + useEffect(() => { + hasValidateAccessToken({ profile }); + }, []); return ( diff --git a/components/sign/signupForm/SignUpForm.tsx b/components/sign/signupForm/SignUpForm.tsx index 25702fe354..deba86cf9b 100644 --- a/components/sign/signupForm/SignUpForm.tsx +++ b/components/sign/signupForm/SignUpForm.tsx @@ -1,4 +1,4 @@ -import { checkSignup, checkValidationEmail } from "@data-access/checkSignup"; +import { authAPI } from "@data-access/authAPI"; import { useRouter } from "next/router"; import { SubmitHandler, useForm } from "react-hook-form"; import * as S from "../SignFormStyled"; @@ -21,12 +21,12 @@ export function SignUpForm() { mode: "onBlur", }); - const isConfirmPassword = async (insertEmail: { email: string }) => { + const isConfirmPassword = async (insertEmail: string) => { try { - await checkValidationEmail(insertEmail); + await authAPI.checkValidationEmail({ email: insertEmail }); return true; - } catch { - return "이미 사용 중인 이메일입니다."; + } catch (error: any) { + alert(error.message); } }; @@ -43,10 +43,10 @@ export function SignUpForm() { }; try { - await checkSignup(insertValue); - router.push("/folder"); - } catch { - return; + await authAPI.checkSignup(insertValue); + router.push("/signin"); + } catch (error: any) { + console.error(error.message); } }; @@ -67,7 +67,7 @@ export function SignUpForm() { message: "올바른 이메일 주소가 아닙니다.", }, validate: async (value) => { - const result = await isConfirmPassword({ email: value }); + const result = await isConfirmPassword(value); return result; }, })} diff --git a/context/UserContext.ts b/context/UserContext.ts index 18af69bce8..9ead7c3df1 100644 --- a/context/UserContext.ts +++ b/context/UserContext.ts @@ -3,7 +3,7 @@ import React from "react"; export interface UserContextProp { loginData: { isLogin: boolean; - userId: string; + userId: number | null; }; handleUserDataState: ( newState: Partial @@ -13,7 +13,7 @@ export interface UserContextProp { const InitialModalStateValue: UserContextProp = { loginData: { isLogin: false, - userId: "", + userId: null, }, handleUserDataState: () => {}, }; diff --git a/context/UserContextProvider.tsx b/context/UserContextProvider.tsx index a244e4a638..17e0867044 100644 --- a/context/UserContextProvider.tsx +++ b/context/UserContextProvider.tsx @@ -2,11 +2,10 @@ import { ReactNode, useState } from "react"; import { UserContext, UserContextProp } from "./UserContext"; export function UserContextProvider({ children }: { children: ReactNode }) { - const [loginData, setLoginData] = useState({ + const [loginData, setLoginData] = useState({ isLogin: false, - userId: "", + userId: null, }); - function handleUserDataState( newState: Partial ) { diff --git a/data-access/BASE_URL.ts b/data-access/BASE_URL.ts deleted file mode 100644 index 50304430bb..0000000000 --- a/data-access/BASE_URL.ts +++ /dev/null @@ -1 +0,0 @@ -export const BASE_URL = "https://bootcamp-api.codeit.kr/api/"; diff --git a/data-access/authAPI.ts b/data-access/authAPI.ts new file mode 100644 index 0000000000..593bde145a --- /dev/null +++ b/data-access/authAPI.ts @@ -0,0 +1,68 @@ +import { createHttpClient } from "./createHttpClient"; + +interface TrySignValueProp { + email: string; + password: string; +} + +interface TrySignResponse { + accessToken: string; + refreshToken: string; +} + +interface ValidateEmailResponse { + isUsableEmail: boolean; +} + +interface UserResponse { + data: [ + { + id: number; + created_at: string; + name: string; + image_source: string; + email: string; + }, + ]; +} + +export const authAPI = { + checkSignin: async function (trySignValue: { + email: string; + password: string; + }) { + const baseHttp = createHttpClient(); + const response = await baseHttp.post( + { + email: trySignValue.email, + password: trySignValue.password, + }, + "/auth/sign-in" + ); + const { accessToken, refreshToken } = response; + return { accessToken, refreshToken }; + }, + checkSignup: async function (trySignUpValue: TrySignValueProp) { + const baseHttp = createHttpClient(); + const response = await baseHttp.post( + { email: trySignUpValue.email, password: trySignUpValue.password }, + "/auth/sign-up" + ); + const { accessToken, refreshToken } = response; + return { accessToken, refreshToken }; + }, + checkValidationEmail: async function ({ email }: { email: string }) { + const baseHttp = createHttpClient(); + const response = await baseHttp.post< + { email: string }, + ValidateEmailResponse + >({ email: email }, "/users/check-email"); + return response; + }, + getUserProfile: async function ({ userId }: { userId: any }) { + const baseHttp = createHttpClient(); + const response = await baseHttp.get(`/users/${userId}`); + const { data } = response; + return data[0]; + }, +}; diff --git a/data-access/axios/axiosInstance.ts b/data-access/axios/axiosInstance.ts index a7507e232d..c855ec1f83 100644 --- a/data-access/axios/axiosInstance.ts +++ b/data-access/axios/axiosInstance.ts @@ -1,7 +1,8 @@ +import { removeToken } from "@util/handleToken"; import axios from "axios"; export const axiosInstance = axios.create({ - baseURL: "https://bootcamp-api.codeit.kr/api", + baseURL: "https://bootcamp-api.codeit.kr/api/linkbrary/v1", }); axiosInstance.interceptors.request.use( @@ -34,8 +35,8 @@ axios.interceptors.response.use( originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; return axios(originalRequest); - } catch (error) { - console.log("회원 정보 인증에 실패했습니다."); + } catch (error: any) { + removeToken(); } } return Promise.reject(error); diff --git a/data-access/checkSignin.ts b/data-access/checkSignin.ts deleted file mode 100644 index 8d1cdb0dcc..0000000000 --- a/data-access/checkSignin.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BASE_URL } from "./BASE_URL"; - -export async function checkSignin(trySignValue: { - email: string; - password: string; -}) { - const response = await fetch(`${BASE_URL}sign-in`, { - method: "POST", - body: JSON.stringify(trySignValue), - headers: { - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error("로그인에 실패했습니다."); - } - - const { data } = await response.json(); - const { accessToken, refreshToken } = data; - return { accessToken, refreshToken }; -} diff --git a/data-access/checkSignup.ts b/data-access/checkSignup.ts deleted file mode 100644 index 0ba7f78aca..0000000000 --- a/data-access/checkSignup.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { BASE_URL } from "./BASE_URL"; - -export async function checkSignup(trySignUpValue: { - email: string; - password: string; -}) { - const response = await fetch(`${BASE_URL}sign-up`, { - method: "POST", - body: JSON.stringify(trySignUpValue), - headers: { - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error("회원가입에 실패했습니다."); - } -} - -export async function checkValidationEmail(insertEmail: { email: string }) { - const response = await fetch(`${BASE_URL}check-email`, { - method: "POST", - body: JSON.stringify(insertEmail), - headers: { - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error("이메일 중복 여부를 확인에 실패했습니다."); - } -} diff --git a/data-access/createHttpClient.ts b/data-access/createHttpClient.ts new file mode 100644 index 0000000000..0fe302e6d5 --- /dev/null +++ b/data-access/createHttpClient.ts @@ -0,0 +1,71 @@ +import axios, { AxiosResponse } from "axios"; +import { axiosInstance } from "./axios/axiosInstance"; + +export function createHttpClient() { + async function get(url: string): Promise { + try { + const response: AxiosResponse = await axiosInstance.get(url); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(error.message); + } else { + throw new Error("데이터를 불러오는데 실패했습니다."); + } + } + } + + async function del(url: string): Promise { + try { + const response: AxiosResponse = await axiosInstance.delete(url); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(error.message); + } else { + throw new Error("데이터를 삭제하는데 실패했습니다."); + } + } + } + + async function put(data: T, url: string): Promise { + try { + const response: AxiosResponse = await axiosInstance.put(url, data); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(error.message); + } else { + throw new Error("데이터를 저장하는데 실패했습니다."); + } + } + } + + async function post( + data: T, + url: string, + headers?: string + ): Promise { + try { + const response: AxiosResponse = await axiosInstance.post(url, data, { + headers: { + "Content-Type": headers ? headers : "application/json", + }, + }); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(error.message); + } else { + throw new Error("데이터를 불러오는데 실패했습니다."); + } + } + } + + return { + get, + post, + put, + del, + }; +} diff --git a/data-access/deleteFolder.ts b/data-access/deleteFolder.ts new file mode 100644 index 0000000000..65e3e1efde --- /dev/null +++ b/data-access/deleteFolder.ts @@ -0,0 +1,11 @@ +import { axiosInstance } from "./axios/axiosInstance"; + +export async function deleteFolder({ + folderId, +}: { + folderId: number | string; +}) { + const response = await axiosInstance.delete(`/folders/${folderId}`); + + return response.status; +} diff --git a/data-access/deleteLink.ts b/data-access/deleteLink.ts new file mode 100644 index 0000000000..3ca5f562f8 --- /dev/null +++ b/data-access/deleteLink.ts @@ -0,0 +1,7 @@ +import { axiosInstance } from "./axios/axiosInstance"; + +export async function deleteLink({ linkId }: { linkId: number }) { + const response = await axiosInstance.delete(`/folders/${linkId}`); + + return response.status; +} diff --git a/data-access/getCategory.ts b/data-access/getCategory.ts index e98653c457..3fb060955d 100644 --- a/data-access/getCategory.ts +++ b/data-access/getCategory.ts @@ -1,7 +1,14 @@ -import { BASE_URL } from "./BASE_URL"; +import { axiosInstance } from "./axios/axiosInstance"; -export async function getCategory() { - const response = await fetch(`${BASE_URL}users/1/folders`); - const result = await response.json(); - return result; +export interface FolderListDataForm { + id: number; + created_at: string; + favorite: boolean; + name: string; + link_count: number; +} + +export async function getCategory(): Promise { + const response = await axiosInstance.get(`/folders`); + return response.data; } diff --git a/data-access/getFolderInfo.ts b/data-access/getFolderInfo.ts index fe2851ef5d..f249d0749d 100644 --- a/data-access/getFolderInfo.ts +++ b/data-access/getFolderInfo.ts @@ -1,8 +1,19 @@ -import { BASE_URL } from "./BASE_URL"; +import { axiosInstance } from "./axios/axiosInstance"; -export async function getFolderInfo(folderId: string | string[] | undefined) { - const response = await fetch(`${BASE_URL}folders/${folderId}`); - const result = await response.json(); +interface FolderInfo { + id: number; + created_at: string; + favorite: boolean; + name: string; + user_id: number; +} + +export async function getFolderInfo({ + folderId, +}: { + folderId?: any; +}): Promise { + const response = await axiosInstance.get(`/folders/${folderId}`); - return result; + return response.data[0]; } diff --git a/data-access/getFolderList.ts b/data-access/getFolderList.ts deleted file mode 100644 index cf43465c33..0000000000 --- a/data-access/getFolderList.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { BASE_URL } from "./BASE_URL"; - -export async function getFolderList( - folderId: string | string[] | undefined, - userId?: string -) { - const query = `?folderId=${folderId}`; - - const response = await fetch(`${BASE_URL}users/${userId}/links${query}`); - const result = await response.json(); - - return result; -} diff --git a/data-access/getFolders.ts b/data-access/getFolders.ts index 9444450abd..e668a4d5a6 100644 --- a/data-access/getFolders.ts +++ b/data-access/getFolders.ts @@ -1,10 +1,20 @@ -import { BASE_URL } from "./BASE_URL"; +import { createHttpClient } from "./createHttpClient"; -export async function getFolders(folderId: string | string[] | undefined) { - const query = `?folderId=${folderId}`; - - const response = await fetch(`${BASE_URL}users/1/links${query}`); - const result = await response.json(); +export interface FolderContentsDataForm { + id: number; + favorite: boolean; + created_at: string; + url: string; + title: string; + image_source: string; + description: string; +} - return result; +export async function getFolders({ folderId }: { folderId: number }) { + const baseHttp = createHttpClient(); + const query = folderId ? `folders/${folderId}/` : ""; + const response = await baseHttp.get( + `/${query}links` + ); + return response; } diff --git a/data-access/getLoginUserInfo.ts b/data-access/getLoginUserInfo.ts index c021de15c4..a310c052e5 100644 --- a/data-access/getLoginUserInfo.ts +++ b/data-access/getLoginUserInfo.ts @@ -1,10 +1,14 @@ -import { GetUserInfoForm } from "../types/DataForm"; import { axiosInstance } from "./axios/axiosInstance"; -export async function getLoginUserInfo(): Promise<{ - data: GetUserInfoForm[]; -}> { +interface loginUserProfile { + id: number; + name: string; + image_source: string; + email: string; +} + +export async function getLoginUserInfo(): Promise { const response = await axiosInstance.get(`/users`); - return response.data; + return response.data[0]; } diff --git a/data-access/getRefreshToken.ts b/data-access/getRefreshToken.ts deleted file mode 100644 index a115d3460a..0000000000 --- a/data-access/getRefreshToken.ts +++ /dev/null @@ -1,22 +0,0 @@ -export async function getRefreshToken() { - try { - const response = await fetch( - `https://bootcamp-api.codeit.kr/api/refresh-token`, - { - method: "POST", - headers: { - ContentType: "application/json", - }, - } - ); - - if (!response.ok) { - throw new Error("유저 정보를 읽는데 실패했습니다."); - } - - const userInfo = await response.json(); - return userInfo; - } catch (error) { - return null; - } -} diff --git a/data-access/getUserProfile.ts b/data-access/getUserProfile.ts deleted file mode 100644 index 6bbabdb956..0000000000 --- a/data-access/getUserProfile.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { BASE_URL } from "./BASE_URL"; - -export async function getUserProfile(userId: string) { - const response = await fetch(`${BASE_URL}users/${userId}`); - const result = response.json(); - - return result; -} diff --git a/data-access/postAddToFolder.ts b/data-access/postAddToFolder.ts new file mode 100644 index 0000000000..2b153281c1 --- /dev/null +++ b/data-access/postAddToFolder.ts @@ -0,0 +1,15 @@ +import { axiosInstance } from "./axios/axiosInstance"; + +export async function postAddToFolder({ + url, + folderId, +}: { + url: string; + folderId: number; +}) { + const response = await axiosInstance.post(`/links`, { + url: url, + folderId: folderId, + }); + return response.data; +} diff --git a/data-access/postNewFolder.ts b/data-access/postNewFolder.ts new file mode 100644 index 0000000000..3e912e5dad --- /dev/null +++ b/data-access/postNewFolder.ts @@ -0,0 +1,20 @@ +import { axiosInstance } from "./axios/axiosInstance"; + +interface createNewFolder { + id: number; + created_at: string; + favorite: boolean; + name: string; +} + +export async function postNewFolder({ + folderName, +}: { + folderName: string; +}): Promise { + const response = await axiosInstance.post(`/folders`, { + name: folderName, + }); + + return response.data; +} diff --git a/data-access/putBookmark.ts b/data-access/putBookmark.ts new file mode 100644 index 0000000000..88fd757f56 --- /dev/null +++ b/data-access/putBookmark.ts @@ -0,0 +1,22 @@ +import { axiosInstance } from "./axios/axiosInstance"; + +interface PutBookmarkRequest { + code: string; + details: string; + hint: null; + message: string; +} + +export async function putBookmark({ + linkId, + isFavorite, +}: { + linkId: number; + isFavorite: boolean; +}): Promise { + const response = await axiosInstance.put(`/links/${linkId}`, { + favorite: isFavorite, + }); + + return response.data; +} diff --git a/data-access/putRenameFolder.ts b/data-access/putRenameFolder.ts new file mode 100644 index 0000000000..2dcfeee7ad --- /dev/null +++ b/data-access/putRenameFolder.ts @@ -0,0 +1,23 @@ +import { axiosInstance } from "./axios/axiosInstance"; + +interface PutRenameFolder { + id: number; + created_at: string; + name: string; + user_id: number; + favorite: boolean; +} + +export async function putRenameFolder({ + folderId, + folderTitle, +}: { + folderId: number; + folderTitle: string; +}): Promise { + const response = await axiosInstance.put(`/folders/${folderId}`, { + name: folderTitle, + }); + + return response.data; +} diff --git a/data-access/useGetFolder.ts b/data-access/useGetFolder.ts deleted file mode 100644 index 4c9d5e495c..0000000000 --- a/data-access/useGetFolder.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { BASE_URL } from "./BASE_URL"; - -export async function getFolder({ - folderId, -}: { - folderId: string; -}): Promise { - const response = await fetch(`${BASE_URL}sample/folder${folderId}`); - const result = await response.json(); - - return result; -} diff --git a/data-access/useGetUser.ts b/data-access/useGetUser.ts deleted file mode 100644 index 40fddb89f6..0000000000 --- a/data-access/useGetUser.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BASE_URL } from "./BASE_URL"; - -export async function useGetUser() { - const response = await fetch(`${BASE_URL}users/1`); - const result = await response.json(); - return result; -} diff --git a/hooks/useInputValue.ts b/hooks/useInputValue.ts index c7353a242f..7aad7e58e7 100644 --- a/hooks/useInputValue.ts +++ b/hooks/useInputValue.ts @@ -7,5 +7,9 @@ export function useInputValue() { setInsertValue(e.target.value); }; - return { insertValue, onChange: handleChange }; + const handleReset = () => { + setInsertValue(""); + }; + + return { insertValue, onChange: handleChange, handleReset }; } diff --git a/hooks/usePortalContents.ts b/hooks/usePortalContents.ts new file mode 100644 index 0000000000..652af0d806 --- /dev/null +++ b/hooks/usePortalContents.ts @@ -0,0 +1,12 @@ +import { MouseEvent, useState } from "react"; + +export function usePortalContents() { + const [isOpenContents, setIsOpenContents] = useState(false); + + function toggleContents(event: MouseEvent) { + event.preventDefault(); + setIsOpenContents((prevIsOpenContents) => !isOpenContents); + } + + return { isOpenModal: isOpenContents, toggleContents }; +} diff --git a/package-lock.json b/package-lock.json index 76c0f76db7..f72523edd2 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", "axios": "^1.6.8", "moment": "^2.30.1", "next": "13.5.6", @@ -352,6 +354,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", diff --git a/package.json b/package.json index 8f7923954d..754f90ed90 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", "axios": "^1.6.8", "moment": "^2.30.1", "next": "13.5.6", diff --git a/pages/_app.tsx b/pages/_app.tsx index 41e0fe71b6..66c89e5ebb 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,13 +2,20 @@ import { RecoilRoot } from "recoil"; import "../setting-files/global.css"; import type { AppProps } from "next/app"; import { UserContextProvider } from "context/UserContextProvider"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; + +const queryClient = new QueryClient(); export default function App({ Component, pageProps }: AppProps) { return ( - - - - - + + + + + + + + ); } diff --git a/pages/folder/[[...folderId]].tsx b/pages/folder/[[...folderId]].tsx index 0bf2c0f490..3621471136 100644 --- a/pages/folder/[[...folderId]].tsx +++ b/pages/folder/[[...folderId]].tsx @@ -1,45 +1,37 @@ -import { useEffect, useState } from "react"; -import { FolderListDataForm } from "../../types/DataForm"; import * as S from "../../styles/pages/FolderStyled"; import { useIntersectionObserver } from "@hooks/useIntersectionObserver"; -import { getCategory } from "@data-access/getCategory"; import FolderHeader from "@components/folder/FolderHeader"; -import { SearchBar } from "@components/common/SearchBar"; import { FolderContent } from "@components/folder/FolderContent/FolderContent"; import Footer from "@components/common/Footer"; -import { ModalProvider } from "@components/common/RefactorModal/ModalProvider"; +import { SearchBar } from "@components/common/SearchBar"; +import { useQuery } from "@tanstack/react-query"; +import { getCategory } from "@data-access/getCategory"; function Folder() { - const [categoryData, setCategoryData] = useState([]); const { isVisible: isHeaderVisible, targetRef: headerRef } = useIntersectionObserver(); const { isVisible: isFooterVisible, targetRef: footerRef } = useIntersectionObserver(); + const { data: folderNameList } = useQuery({ + queryKey: ["folderList"], + queryFn: getCategory, + }); const floatingState = !isHeaderVisible && !isFooterVisible ? true : false; - const handleLoadCategory = async () => { - const { data } = await getCategory(); - setCategoryData(data); - }; - - useEffect(() => { - handleLoadCategory(); - }, []); - return ( - + <> - +