diff --git a/client/src/app/auth/forgot-password/page.tsx b/client/src/app/(logoutOnly)/auth/forgot-password/page.tsx similarity index 100% rename from client/src/app/auth/forgot-password/page.tsx rename to client/src/app/(logoutOnly)/auth/forgot-password/page.tsx diff --git a/client/src/app/auth/login/page.tsx b/client/src/app/(logoutOnly)/auth/login/page.tsx similarity index 100% rename from client/src/app/auth/login/page.tsx rename to client/src/app/(logoutOnly)/auth/login/page.tsx diff --git a/client/src/app/auth/signup/page.tsx b/client/src/app/(logoutOnly)/auth/signup/page.tsx similarity index 100% rename from client/src/app/auth/signup/page.tsx rename to client/src/app/(logoutOnly)/auth/signup/page.tsx diff --git a/client/src/app/(logoutOnly)/layout.tsx b/client/src/app/(logoutOnly)/layout.tsx new file mode 100644 index 0000000..edc2ceb --- /dev/null +++ b/client/src/app/(logoutOnly)/layout.tsx @@ -0,0 +1,9 @@ +import useLogoutOnlyProtector from "@/hooks/useLogoutOnlyProtector"; +import { ReactNode } from "react"; + +const LogoutOnlyLayout = async ({ children }: { children: ReactNode }) => { + useLogoutOnlyProtector(); + return <>{children}; +}; + +export default LogoutOnlyLayout; diff --git a/client/src/app/(protectedRoute)/layout.tsx b/client/src/app/(protectedRoute)/layout.tsx new file mode 100644 index 0000000..9ad09ad --- /dev/null +++ b/client/src/app/(protectedRoute)/layout.tsx @@ -0,0 +1,9 @@ +import useAuthProtector from "@/hooks/useAuthProtector"; +import { ReactNode } from "react"; + +const AuthProtectorlayout = async ({ children }: { children: ReactNode }) => { + useAuthProtector(); + return <>{children}; +}; + +export default AuthProtectorlayout; diff --git a/client/src/app/(protectedRoute)/new-post/page.tsx b/client/src/app/(protectedRoute)/new-post/page.tsx new file mode 100644 index 0000000..e7e70ef --- /dev/null +++ b/client/src/app/(protectedRoute)/new-post/page.tsx @@ -0,0 +1,3 @@ +export default function NewpostPage() { + return <>페지이; +} diff --git a/client/src/app/@Modal/(.)new-post/page.tsx b/client/src/app/@Modal/(.)new-post/page.tsx new file mode 100644 index 0000000..28dc5f0 --- /dev/null +++ b/client/src/app/@Modal/(.)new-post/page.tsx @@ -0,0 +1,12 @@ +import ModalWrapper from "@/components/ModalWrapper"; +import React, { ReactNode } from "react"; + +type Props = { + children: ReactNode; +}; + +const NewPostPage = ({ children }: Props) => { + return {children}; +}; + +export default NewPostPage; diff --git a/client/src/app/api/auth/login/route.ts b/client/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..495a405 --- /dev/null +++ b/client/src/app/api/auth/login/route.ts @@ -0,0 +1,19 @@ +import { LOGIN_API_PATH } from "@/const/serverPath"; +import { setCookie } from "@/hooks/useSetCookie"; +import axios from "@/libs/axios"; +import { SigninResponseInterface } from "@/types/auth/signinResponse"; +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(request: NextRequest) { + const { id, password } = await request.json(); + try { + const { data } = await axios.post(LOGIN_API_PATH, { + id, + password, + }); + setCookie({ key: "accessToken", value: data.token, httpOnly: true }); + return NextResponse.json({ ...data }); + } catch { + return NextResponse.json({ message: "로그인 실패" }, { status: 400 }); + } +} diff --git a/client/src/app/page.tsx b/client/src/app/page.tsx index 051987a..66976f4 100644 --- a/client/src/app/page.tsx +++ b/client/src/app/page.tsx @@ -1,4 +1,4 @@ -"use client"; + import PostCardList from "@/components/post/PostCardList"; // import { getPostListQueryFn } from "@/queries/post/useGetPostListQuery"; import { Container } from "@mui/material"; diff --git a/client/src/components/NavigationBar.tsx b/client/src/components/NavigationBar.tsx index b966057..5aabaec 100644 --- a/client/src/components/NavigationBar.tsx +++ b/client/src/components/NavigationBar.tsx @@ -6,7 +6,7 @@ import SearchIcon from "~/assets/icons/SearchIcon.svg"; import PostIcon from "~/assets/icons/PostIcon.svg"; import BeverageIcon from "~/assets/icons/BeverageIcon.svg"; import MyIcon from "~/assets/icons/MyIcon.svg"; -import HOME, { MY_PROFILE, SEARCH, WIKI } from "@/const/clientPath"; +import HOME, { MY_PROFILE, NEW_POST, SEARCH, WIKI } from "@/const/clientPath"; import Link from "next/link"; import { usePathname } from "next/navigation"; @@ -19,10 +19,11 @@ const NavbarData = [ { iconComponent: , label: "검색", - href:SEARCH + href: SEARCH, }, { iconComponent: , + href: NEW_POST, }, { iconComponent: , @@ -55,7 +56,7 @@ const NavigationBar = () => { sx={{ borderRadius: "12px 12px 0 0", border: "1px solid", - borderBottom:'none', + borderBottom: "none", borderColor: "gray.secondary", boxSizing: "border-box", }} diff --git a/client/src/components/user/signin/SigninForm.tsx b/client/src/components/user/signin/SigninForm.tsx index d158fb8..48b42ca 100644 --- a/client/src/components/user/signin/SigninForm.tsx +++ b/client/src/components/user/signin/SigninForm.tsx @@ -1,18 +1,13 @@ "use client"; -import HOME from "@/const/clientPath"; import useLoginMutation from "@/queries/auth/useLoginMutation"; -import errorHandler from "@/utils/errorHandler"; import { Box, Button, TextField, Typography } from "@mui/material"; -import { useRouter } from "next/navigation"; import { useState } from "react"; const SigninForm = () => { const [id, setId] = useState(""); const [password, setPassword] = useState(""); - - const router = useRouter(); - const { mutate: loginHandler, isError } = useLoginMutation(); + const { mutate: loginMutation, isError } = useLoginMutation(); return ( { if (!id || !password) { return; } - try { - loginHandler({ id, password }); - router.push(HOME); - } catch { - errorHandler("로그인에 실패했습다다"); - } + loginMutation({ id, password }); }} sx={{ mt: 1 }} > diff --git a/client/src/const/clientPath.ts b/client/src/const/clientPath.ts index 2de5514..480b2bd 100644 --- a/client/src/const/clientPath.ts +++ b/client/src/const/clientPath.ts @@ -43,6 +43,10 @@ export const POST_DETAIL = (userId: string, postId: string) => { return `/post/@${trimmedUserId}/${trimmedPostId}`; }; +/** + * 새로운 포스트를 작성하는 페이지 + */ +export const NEW_POST = '/new-post' export default HOME; diff --git a/client/src/const/serverPath.ts b/client/src/const/serverPath.ts index f1d85a2..2b1011f 100644 --- a/client/src/const/serverPath.ts +++ b/client/src/const/serverPath.ts @@ -5,4 +5,9 @@ export const LOGIN_API_PATH = '/user/login' as const /** * 내 정보를 받아오는 Path */ -export const MY_INFO = '/user/me' as const \ No newline at end of file +export const MY_INFO = '/user/me' as const + +/** + * 쿠키를 심어주는 로그인 BFF + */ +export const LOGIN_BFF = '/api/auth/login' as const \ No newline at end of file diff --git a/client/src/hooks/useAuthProtector.tsx b/client/src/hooks/useAuthProtector.tsx new file mode 100644 index 0000000..297f962 --- /dev/null +++ b/client/src/hooks/useAuthProtector.tsx @@ -0,0 +1,14 @@ +import { SIGNIN } from "@/const/clientPath"; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +/** + * 쿠키에 accessToken 이나 refreshToken이 없을 경우 로그인 페이지로 리다이렉트 시키는 훅스 + */ +export default function useAuthProtector() { + const cookieStore = cookies(); + const accessToken = cookieStore.get("accessToken")?.value; + + if (!accessToken) { + redirect(SIGNIN); + } +} diff --git a/client/src/hooks/useLogin.ts b/client/src/hooks/useLogin.ts index fb8813d..6e599e4 100644 --- a/client/src/hooks/useLogin.ts +++ b/client/src/hooks/useLogin.ts @@ -1,4 +1,4 @@ -import { LOGIN_API_PATH } from "@/const/serverPath"; +import { LOGIN_BFF } from "@/const/serverPath"; import axios from "@/libs/axios"; import { SigninRequirement } from "@/types/auth/signinRequirement"; import { SigninResponseInterface } from "@/types/auth/signinResponse"; @@ -10,12 +10,16 @@ import { SigninResponseInterface } from "@/types/auth/signinResponse"; export default function useLogin() { const loginHandler = async (props: SigninRequirement) => { const { id, password } = props; - const { data } = await axios.post(LOGIN_API_PATH, { - id, - password, - }); + const { data } = await axios.post( + LOGIN_BFF, + { + id, + password, + }, + { baseURL: process.env.NEXT_PUBLIC_CLIENT_BASE_URL } + ); return data; }; - return {loginHandler}; + return { loginHandler }; } diff --git a/client/src/hooks/useLogoutOnlyProtector.tsx b/client/src/hooks/useLogoutOnlyProtector.tsx new file mode 100644 index 0000000..3876a65 --- /dev/null +++ b/client/src/hooks/useLogoutOnlyProtector.tsx @@ -0,0 +1,14 @@ +import HOME from "@/const/clientPath"; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +/** + * 쿠키에 accessToken 이나 refreshToken이 있을경우 메인페이지로 리다이렉트 시키는 훅스 + */ +export default function useLogoutProtector() { + const cookieStore = cookies(); + const accessToken = cookieStore.get("accessToken")?.value; + + if (accessToken) { + redirect(HOME); + } +} diff --git a/client/src/queries/auth/useLoginMutation.tsx b/client/src/queries/auth/useLoginMutation.tsx index 4f103d2..6d887cb 100644 --- a/client/src/queries/auth/useLoginMutation.tsx +++ b/client/src/queries/auth/useLoginMutation.tsx @@ -3,10 +3,15 @@ import useLogin from "@/hooks/useLogin"; import { SigninRequirement } from "@/types/auth/signinRequirement"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { userInfoQueryKeys } from "./useUserInfoQuery"; +import { useRouter } from "next/navigation"; +import HOME from "@/const/clientPath"; +import errorHandler from "@/utils/errorHandler"; +import { AxiosError } from "axios"; const useLoginMutation = () => { const { loginHandler } = useLogin(); const queryClient = useQueryClient(); + const router = useRouter(); return useMutation({ mutationKey: LoginMuataionKey.all, @@ -16,7 +21,10 @@ const useLoginMutation = () => { onSuccess: async ({ token }) => { localStorage?.setItem("accessToken", token); queryClient.invalidateQueries({ queryKey: userInfoQueryKeys.all }); + router.push(HOME); }, + onError: (error: AxiosError<{ detailMessage: string }>) => + errorHandler(error.response?.data.detailMessage ?? "에러가 발생했니다"), }); };