>(
+ API_ENDPOINTS.FOLDER_DETAIL(data.folderId),
+ {
+ name: data.name,
+ }
+ );
+ return response.data;
+ },
+ onSuccess: () =>
+ queryClient.invalidateQueries({
+ queryKey: QUERY_KEYS.FOLDERS,
+ }),
+ onError: (error) => {
+ throw error;
+ },
+ });
+};
diff --git a/lib/axios.ts b/lib/axios.ts
index 8200de696..8b15696b3 100644
--- a/lib/axios.ts
+++ b/lib/axios.ts
@@ -1,7 +1,37 @@
import axios from "axios";
+import type {
+ InternalAxiosRequestConfig,
+ AxiosRequestConfig,
+ AxiosError,
+} from "axios";
+import useAuthStore from "store/authStore";
const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
+ headers: { "Content-Type": "application/json" },
});
+const onRequest = (config: InternalAxiosRequestConfig) => {
+ const accessToken = useAuthStore.getState().accessToken;
+
+ if (accessToken) {
+ config.headers.Authorization = `Bearer ${accessToken}`;
+ }
+
+ return config;
+};
+
+const onError = (error: AxiosError) => {
+ // Unauthorized 응답
+ if (error.isAxiosError) {
+ if (error.response?.status === 404) {
+ console.log("존재하지 않는 유저");
+ } else {
+ console.log("인증 오류");
+ }
+ }
+};
+
+instance.interceptors.request.use(onRequest, onError);
+
export default instance;
diff --git a/middleware.ts b/middleware.ts
deleted file mode 100644
index 5353c4bed..000000000
--- a/middleware.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { NextResponse } from "next/server";
-import { NextRequest } from "next/server";
-import { ROUTE_PATHS } from "constants/route";
-import { TOKEN } from "constants/auth";
-
-export function middleware(request: NextRequest) {
- if (typeof window !== "undefined") {
- const accessToken = localStorage.getItem(TOKEN.access);
- if (accessToken) {
- return NextResponse.redirect(new URL(ROUTE_PATHS.folder, request.url));
- }
- }
- return NextResponse.next();
-}
-
-export const config = {
- matcher: ["/signin/:path", "/signup/:path"],
-};
diff --git a/package-lock.json b/package-lock.json
index 971dd17cd..96a722e0d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@hookform/resolvers": "^3.3.4",
+ "@tanstack/react-query": "^5.35.5",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -17,16 +18,20 @@
"@types/react-dom": "^18.2.22",
"axios": "^1.6.8",
"clsx": "^2.1.0",
+ "js-cookie": "^3.0.5",
"next": "^14.1.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.2",
"typescript": "^5.4.2",
"web-vitals": "^2.1.4",
- "zod": "^3.22.4"
+ "zod": "^3.22.4",
+ "zustand": "^4.5.2"
},
"devDependencies": {
- "@svgr/webpack": "^8.1.0"
+ "@svgr/webpack": "^8.1.0",
+ "@tanstack/react-query-devtools": "^5.35.5",
+ "@types/js-cookie": "^3.0.6"
}
},
"node_modules/@adobe/css-tools": {
@@ -2424,6 +2429,57 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.35.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.35.5.tgz",
+ "integrity": "sha512-OMWvlEqG01RfGj+XZb/piDzPp0eZkkHWSDHt2LvE/fd1zWburP/xwm0ghk6Iv8cuPlP+ACFkZviKXK0OVt6lhg==",
+ "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==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.35.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.35.5.tgz",
+ "integrity": "sha512-sppX7L+PVn5GBV3In6zzj0zcKfnZRKhXbX1MfIfKo1OjIq2GMaopvAFOP0x1bRYTUk2ikrdYcQYOozX7PWkb8A==",
+ "dependencies": {
+ "@tanstack/query-core": "5.35.5"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0"
+ }
+ },
+ "node_modules/@tanstack/react-query-devtools": {
+ "version": "5.35.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.35.5.tgz",
+ "integrity": "sha512-4Xll14B9uhgEJ+uqZZ5tqZ7G1LDR7wGYgb+NOZHGn11TTABnlV8GWon7zDMqdaHeR5mjjuY1UFo9pbz39kuZKQ==",
+ "dev": true,
+ "dependencies": {
+ "@tanstack/query-devtools": "5.32.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^5.35.5",
+ "react": "^18.0.0"
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "9.3.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
@@ -2599,6 +2655,12 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
+ "node_modules/@types/js-cookie": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
+ "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
+ "dev": true
+ },
"node_modules/@types/node": {
"version": "20.11.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
@@ -3973,6 +4035,14 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4873,6 +4943,14 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/web-vitals": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
@@ -4938,6 +5016,33 @@
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+ },
+ "node_modules/zustand": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz",
+ "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==",
+ "dependencies": {
+ "use-sync-external-store": "1.2.0"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index d102ee975..ca7b1e60c 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@hookform/resolvers": "^3.3.4",
+ "@tanstack/react-query": "^5.35.5",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -12,13 +13,15 @@
"@types/react-dom": "^18.2.22",
"axios": "^1.6.8",
"clsx": "^2.1.0",
+ "js-cookie": "^3.0.5",
"next": "^14.1.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.2",
"typescript": "^5.4.2",
"web-vitals": "^2.1.4",
- "zod": "^3.22.4"
+ "zod": "^3.22.4",
+ "zustand": "^4.5.2"
},
"scripts": {
"dev": "next dev",
@@ -44,6 +47,8 @@
]
},
"devDependencies": {
- "@svgr/webpack": "^8.1.0"
+ "@svgr/webpack": "^8.1.0",
+ "@tanstack/react-query-devtools": "^5.35.5",
+ "@types/js-cookie": "^3.0.6"
}
}
diff --git a/pages/Home.module.css b/pages/Home.module.css
new file mode 100644
index 000000000..516154cc0
--- /dev/null
+++ b/pages/Home.module.css
@@ -0,0 +1,60 @@
+.container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+}
+
+.headerWrapper {
+ width: 100%;
+ background-color: #f0f6ff;
+}
+
+.header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 40px;
+ margin-top: 70px;
+}
+
+.headerText {
+ font-size: 64px;
+ font-weight: 700;
+}
+
+.block {
+ display: block;
+ margin: auto;
+}
+
+.headerText .gradient {
+ background: linear-gradient(91deg, #6d6afe 17.28%, #ff9f9f 74.98%);
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.linkAddBtn {
+ width: 350px;
+ padding: 16px 20px;
+ border-radius: 8px;
+ background: var(
+ --gra-purpleblue-to-skyblue,
+ linear-gradient(91deg, #6d6afe 0.12%, #6ae3fe 101.84%)
+ );
+ color: #ffffff;
+ font-size: 18px;
+}
+
+.section {
+ display: flex;
+ align-items: center;
+ gap: 157px;
+ margin: 100px 0;
+}
+
+.h2{
+ font-size: 48px;
+}
\ No newline at end of file
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 8f8a0c0c7..17ff00fcc 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -1,18 +1,31 @@
-import type { ReactElement, ReactNode } from 'react'
-import type { NextPage } from 'next'
+import type { ReactElement, ReactNode } from "react";
+import type { NextPage } from "next";
import type { AppProps } from "next/app";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import "styles/globals.css";
+const queryClient = new QueryClient();
+
export type NextPageWithLayout = NextPage
& {
- getLayout?: (page: ReactElement) => ReactNode
-}
-
+ getLayout?: (page: ReactElement) => ReactNode;
+};
+
type AppPropsWithLayout = AppProps & {
- Component: NextPageWithLayout
-}
+ Component: NextPageWithLayout;
+};
export default function App({ Component, pageProps }: AppPropsWithLayout) {
- const getLayout = Component.getLayout ?? ((page) => page)
-
- return getLayout()
+ const getLayout = Component.getLayout ?? ((page) => page);
+
+ return (
+
+ {getLayout(
+ <>
+
+
+ >
+ )}
+
+ );
}
diff --git a/pages/folder/index.tsx b/pages/folder/index.tsx
index bad1ee215..60730aa56 100644
--- a/pages/folder/index.tsx
+++ b/pages/folder/index.tsx
@@ -1,4 +1,5 @@
import React, { ChangeEvent, useState, ReactElement, useRef } from "react";
+import { useRouter } from "next/router";
import styles from "./folder.module.css";
import SearchBar from "@/components/common/SearchBar/SearchBar";
@@ -12,12 +13,20 @@ import { useGetLinks } from "hooks/useGetLinks";
import { useGetFolders } from "hooks/useGetFolders";
import useIntersectionObserver from "hooks/useIntersectionObserver";
-import type { LinkItem } from "types";
import type { NextPageWithLayout } from "../_app";
-import { ALL } from "constants/etc";
-
-const USERID = 11;
+const ALL = "전체";
+
+export type Link = {
+ id: number;
+ favorite: boolean;
+ created_at: Date;
+ url: string;
+ title: string;
+ image_source: string;
+ description: string;
+ [key: string]: number | Date | string | boolean;
+};
export type SelectedCategory = {
id: number | null;
@@ -25,14 +34,16 @@ export type SelectedCategory = {
};
const FolderPage: NextPageWithLayout = () => {
+ const router = useRouter();
+
const [selectedCategory, setSelectedCategory] = useState({
id: null,
name: ALL,
});
- const { data: folders } = useGetFolders(USERID);
+ const { data: folders, isPending, isError } = useGetFolders();
- const { data: folderLinks } = useGetLinks(USERID, selectedCategory.id);
+ const { data: folderLinks } = useGetLinks(selectedCategory.id);
const [searchText, setSearchText] = useState("");
@@ -42,7 +53,7 @@ const FolderPage: NextPageWithLayout = () => {
setSearchText(e.target.value);
};
- const filterSearchText = (items: LinkItem[]) => {
+ const filterSearchText = (items: Link[]) => {
return items.filter((item) => {
return searchParam.some((newItem) => {
return (
@@ -58,6 +69,7 @@ const FolderPage: NextPageWithLayout = () => {
const handleCategoryClick = (id: number | null, name: string) => {
setSelectedCategory({ id, name });
+ router.push(`/folder?folderId=${id}`, undefined, { shallow: true });
};
const handleDeletedClick = () => {
@@ -71,6 +83,14 @@ const FolderPage: NextPageWithLayout = () => {
});
const isVisibleFooter = useIntersectionObserver(fooerRef, { threshold: 1 });
+ if (isPending) {
+ return 로딩 중 입니다.
;
+ }
+
+ if (isError) {
+ return 에러가 발생했습니다. 다시 시도해주세요.
;
+ }
+
return (
@@ -99,6 +119,7 @@ const FolderPage: NextPageWithLayout = () => {
{folderLinks && (
diff --git a/pages/index.tsx b/pages/index.tsx
index d7175aaf7..d4eb13bc8 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -1,24 +1,46 @@
import { ReactElement } from "react";
-import Link from "next/link";
import Layout from "@/components/common/Layout/Layout";
-import { ROUTE_PATHS } from "constants/route";
import type { NextPageWithLayout } from "./_app";
+import styles from "./Home.module.css";
+import Image from "next/image";
+import { ROUTE_PATHS } from "constants/route";
+import Link from "next/link";
const HomePage: NextPageWithLayout = () => {
return (
-
- Home 페이지
-
- -
- 폴더 페이지 이동
-
- -
- 공유 페이지 이동
-
- -
- 로그인 페이지 이동
-
-
+
+
+
+
+
+ 세상의 모든 정보를
+
+ 쉽게 저장하고 관리해 보세요
+
+
+
+
+
+
+
+
+
+
+
+ 원하는 링크를저장하세요
+
+
+
+ 나중에 읽고 싶은 글, 다시 보고 싶은 영상,
+
+ 사고 싶은 옷, 기억하고 싶은
+
+ 모든 것을 한 공간에 저장하세요.
+
+
+
+
+
);
};
diff --git a/pages/signin/index.tsx b/pages/signin/index.tsx
index ad1c018e8..882bc48d1 100644
--- a/pages/signin/index.tsx
+++ b/pages/signin/index.tsx
@@ -12,6 +12,8 @@ import styles from "./signin.module.css";
import Logo from "@/images/logo.svg";
import { ROUTE_PATHS } from "constants/route";
import { TOKEN } from "constants/auth";
+import LoginCheck from "@/components/common/LoginCheck/LoginCheck";
+import useLogin from "hooks/useLogin";
const SignIn = () => {
const {
@@ -26,26 +28,14 @@ const SignIn = () => {
const router = useRouter();
- const postData = async (email: string, password: string) => {
+ const { login, isPending } = useLogin();
+
+ const onSubmit: SubmitHandler
= async (data) => {
try {
- const response = await instance.post("/sign-in", { email, password });
- const result = response.data;
- return result;
+ await login(data);
+ router.push(ROUTE_PATHS.home);
} catch (error) {
if (axios.isAxiosError(error)) {
- throw error;
- }
- }
- };
-
- const onSubmit: SubmitHandler = async (data) => {
- const { email, password } = data;
- postData(email, password)
- .then((res) => {
- useLocalStorage(TOKEN.access, res.data.accessToken);
- router.push(ROUTE_PATHS.folder);
- })
- .catch(() => {
setError("email", {
type: "400",
message: "이메일을 확인해 주세요.",
@@ -54,44 +44,51 @@ const SignIn = () => {
type: "400",
message: "비밀번호를 확인해 주세요.",
});
- });
+ }
+ }
};
return (
-
-
+
+
+
-
-
-
- 소셜 로그인
-
+
+
+ 소셜 로그인
+
+
);
};
diff --git a/pages/signup/index.tsx b/pages/signup/index.tsx
index 778077866..550d72cff 100644
--- a/pages/signup/index.tsx
+++ b/pages/signup/index.tsx
@@ -1,5 +1,4 @@
import { useRouter } from "next/router";
-import instance from "lib/axios";
import axios from "axios";
import { useForm, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -7,6 +6,7 @@ import styles from "./signup.module.css";
import { Register, registerSchema } from "lib/zod/schema/RegisterSchema";
import InputField from "@/components/common/InputField/InputField";
import { ROUTE_PATHS } from "constants/route";
+import useSignUp from "hooks/useSignUp";
const SignUp = () => {
const {
@@ -21,31 +21,30 @@ const SignUp = () => {
const router = useRouter();
- const postData = async (email: string, password: string) => {
+ const { signUp } = useSignUp();
+
+ const onSubmit: SubmitHandler = async (data) => {
+ const { email, password } = data;
+
try {
- const response = await instance.post("/sign-up", { email, password });
- const result = response.data;
+ await signUp(email, password);
+ router.push(ROUTE_PATHS.login);
} catch (error) {
if (axios.isAxiosError(error)) {
- throw error;
+ if (error.response?.status === 409) {
+ setError("email", {
+ type: "409",
+ message: "이미 사용 중인 이메일입니다.",
+ });
+ } else {
+ setError("email", {
+ message: "다시 시도해 주세요.",
+ });
+ }
}
}
};
- const onSubmit: SubmitHandler = async (data) => {
- const { email, password } = data;
- postData(email, password)
- .then(() => {
- router.push(ROUTE_PATHS.folder);
- })
- .catch(() => {
- setError("email", {
- type: "400",
- message: "이미 사용 중인 이메일입니다.",
- });
- });
- };
-
return (