diff --git a/public/user/naver-icon.svg b/public/user/naver-icon.svg new file mode 100644 index 00000000..9559accf --- /dev/null +++ b/public/user/naver-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/app/api/auth/kakao/getToken/route.ts b/src/app/api/auth/kakao/getToken/route.ts index 9e3a3089..26f7a2dd 100644 --- a/src/app/api/auth/kakao/getToken/route.ts +++ b/src/app/api/auth/kakao/getToken/route.ts @@ -7,7 +7,7 @@ export async function GET(request: NextRequest) { const code = url.searchParams.get("code"); // 백엔드에 액세스 토큰 재요청 - const backendResponse = await fetch( + const response = await fetch( `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/auth/oauth2/login?type=kakao&redirectUrl=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URL}&code=${code}`, { method: "GET", @@ -18,17 +18,15 @@ export async function GET(request: NextRequest) { credentials: "include", }, ); + - const result = new NextResponse( - backendResponse.status == 200 ? "성공" : "실패", - { - status: backendResponse.status, - headers: { "Content-Type": "application/json" }, - }, - ); - - if (backendResponse.status == 200) { - const cookies = backendResponse.headers.get("set-cookie"); + const data = await response.json(); + const result = new NextResponse(JSON.stringify(data), { + status: 200, + }); + + if (response.status == 200) { + const cookies = response.headers.get("set-cookie"); if (cookies) { // 받은 쿠키를 파싱하여 설정 cookies.split(",").forEach((cookie) => { @@ -51,8 +49,8 @@ export async function POST(request: NextRequest) { const body = await request.json(); // 백엔드에 액세스 토큰 재요청 - const backendResponse = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/auth/oauth2/login/kakao?redirectUrl=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URL}&code=${code}`, + const response = await fetch( + `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/auth/oauth2/login?redirectUrl=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URL}&code=${code}`, { method: "POST", headers: { @@ -69,15 +67,15 @@ export async function POST(request: NextRequest) { ); const result = new NextResponse( - backendResponse.status == 200 ? "성공" : "실패", + response.status == 200 ? "성공" : "실패", { - status: backendResponse.status, + status: response.status, headers: { "Content-Type": "application/json" }, }, ); - if (backendResponse.status == 200) { - const cookies = backendResponse.headers.get("set-cookie"); + if (response.status == 200) { + const cookies = response.headers.get("set-cookie"); if (cookies) { // 받은 쿠키를 파싱하여 설정 cookies.split(",").forEach((cookie) => { diff --git a/src/app/api/auth/naver/getToken/route.ts b/src/app/api/auth/naver/getToken/route.ts new file mode 100644 index 00000000..2579e45d --- /dev/null +++ b/src/app/api/auth/naver/getToken/route.ts @@ -0,0 +1,90 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + try { + // Query string 파싱 + const url = new URL(request.url); + const code = url.searchParams.get("code"); + + // 백엔드에 액세스 토큰 재요청 + const response = await fetch( + `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/auth/oauth2/login?type=naver&redirectUrl=${process.env.NEXT_PUBLIC_NAVER_REDIRECT_URL}&code=${code}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + cache: "no-store", + credentials: "include", + }, + ); + + const data = await response.json(); + const result = new NextResponse(JSON.stringify(data), { + status: 200, + }); + + if (response.ok) { + const cookies = response.headers.get("set-cookie"); + if (cookies) { + // 받은 쿠키를 파싱하여 설정 + cookies.split(",").forEach((cookie) => { + result.headers.append("Set-Cookie", cookie.trim()); + }); + } + } + + return result; + } catch (error) { + return new NextResponse("서버 에러", { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + // Query string 파싱 + const url = new URL(request.url); + const code = url.searchParams.get("code"); + const body = await request.json(); + + // 백엔드에 액세스 토큰 재요청 + const response = await fetch( + `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/auth/oauth2/login/kakao?redirectUrl=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URL}&code=${code}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + createUserInfoRequest: { + ...body + } + }), + cache: "no-store", + credentials: "include", + }, + ); + + const result = new NextResponse( + response.status == 200 ? "성공" : "실패", + { + status: response.status, + headers: { "Content-Type": "application/json" }, + }, + ); + + if (response.status == 200) { + const cookies = response.headers.get("set-cookie"); + if (cookies) { + // 받은 쿠키를 파싱하여 설정 + cookies.split(",").forEach((cookie) => { + result.headers.append("Set-Cookie", cookie.trim()); + }); + } + } + + return result; + } catch (error) { + return new NextResponse("서버 에러", { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/auth/naver/route.ts b/src/app/api/auth/naver/route.ts new file mode 100644 index 00000000..0396aa92 --- /dev/null +++ b/src/app/api/auth/naver/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from "next/server"; + +function generateRandomString(length: number): string { + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + result += characters[randomIndex]; + } + return result; +} + +export function GET() { + try { + const clientId = process.env.NAVER_CLIENT_ID; + const redirectUri = process.env.NEXT_PUBLIC_NAVER_REDIRECT_URL; + const RANDOM_STATE = generateRandomString(150); + const timestamp = new Date().toISOString(); // Get the current timestamp in ISO format + + if (!clientId || !redirectUri) { + return NextResponse.redirect("/auth/signin"); + } + + const naverAuthUrl = `https://nid.naver.com/oauth2.0/authorize?client_id=${clientId}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&state=${RANDOM_STATE}`; + return NextResponse.redirect(naverAuthUrl); + } catch (error) { + return NextResponse.redirect("/auth/signin"); + } +} diff --git a/src/app/api/auth/user/info/agree/route.ts b/src/app/api/auth/user/info/agree/route.ts new file mode 100644 index 00000000..6288f1ab --- /dev/null +++ b/src/app/api/auth/user/info/agree/route.ts @@ -0,0 +1,118 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + // const access_cookie = request.cookies.get("access_token"); + // if (!access_cookie) { + // const refresh_cookie = request.cookies.get("refresh_token"); + // if (!refresh_cookie) { + // // 리프레시 토큰이 없으므로 요청 중단 + // return new NextResponse("Refresh token not found", { status: 403 }); + // } + // // 리프레시 토큰으로 재발급 받아 재요청 보내기 위한 응답 + // return new NextResponse("Refresh token not found", { status: 401 }); + // } + + // // 사용자 정보 조회 API + // const response = await fetch(`${process.env.BACKEND_URL}/api/users/info`, { + // method: "GET", + // headers: { + // Cookie: `${access_cookie?.name}=${access_cookie?.value}`, + // "Content-Type": "application/json", + // "Access-Control-Allow-Origin": "*", + // }, + // cache: "no-store", + // }); + + // if (response.ok) { + // const data = await response.json(); + // return new NextResponse(JSON.stringify(data), { + // status: 200, + // }); + // } + + // if (response.status == 401) { + // return new NextResponse("토큰 만료", { + // status: 401, + // }); + // } + + // cookies().delete("access_token"); + // cookies().delete("refresh_token"); + // return new NextResponse("서버 에러", { + // status: 500, + // }); +} + +export async function PUT(request: NextRequest) { + const access_cookie = request.cookies.get("access_token"); + if (!access_cookie) { + const refresh_cookie = request.cookies.get("refresh_token"); + if (!refresh_cookie) { + // 리프레시 토큰이 없으므로 요청 중단 + return new NextResponse("Refresh token not found", { status: 403 }); + } + // 리프레시 토큰으로 재발급 받아 재요청 보내기 위한 응답 + return new NextResponse("Refresh token not found", { status: 401 }); + } + + const requestData = await request.json(); + + // 사용자 정보 조회 API + const response = await fetch(`${process.env.BACKEND_URL}/api/users/info/agree`, { + method: "PUT", + headers: { + Cookie: `${access_cookie?.name}=${access_cookie?.value}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(requestData), + cache: "no-store", + }); + + if (response.ok) { + return response; + } + + return new NextResponse("서버 에러", { + status: 500, + }); +} + +export async function DELETE(request: NextRequest) { + // const access_cookie = request.cookies.get("access_token"); + // if (!access_cookie) { + // const refresh_cookie = request.cookies.get("refresh_token"); + // if (!refresh_cookie) { + // // 리프레시 토큰이 없으므로 요청 중단 + // return new NextResponse("Refresh token not found", { status: 403 }); + // } + // // 리프레시 토큰으로 재발급 받아 재요청 보내기 위한 응답 + // return new NextResponse("Refresh token not found", { status: 401 }); + // } + + // const url = new URL(request.url); + + // // 사용자 삭제 + // const response = await fetchWithAuth( + // `${process.env.BACKEND_URL}/api/auth/oauth2?type=${url.searchParams.get("type")}`, + // { + // method: "DELETE", + // headers: { + // Cookie: `${access_cookie?.name}=${access_cookie?.value}`, + // "Content-Type": "application/json", + // }, + // cache: "no-store", + // }, + // ); + + // if (!response.ok) { + // return new NextResponse(`${response.statusText}`, { + // status: response.status, + // }); + // } + + // cookies().delete("access_token"); + // cookies().delete("refresh_token"); + // return new NextResponse("회원 탈퇴 성공", { + // status: 200, + // }); +} diff --git a/src/app/api/auth/user/info/disagree/route.ts b/src/app/api/auth/user/info/disagree/route.ts new file mode 100644 index 00000000..2c5b698b --- /dev/null +++ b/src/app/api/auth/user/info/disagree/route.ts @@ -0,0 +1,118 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + // const access_cookie = request.cookies.get("access_token"); + // if (!access_cookie) { + // const refresh_cookie = request.cookies.get("refresh_token"); + // if (!refresh_cookie) { + // // 리프레시 토큰이 없으므로 요청 중단 + // return new NextResponse("Refresh token not found", { status: 403 }); + // } + // // 리프레시 토큰으로 재발급 받아 재요청 보내기 위한 응답 + // return new NextResponse("Refresh token not found", { status: 401 }); + // } + + // // 사용자 정보 조회 API + // const response = await fetch(`${process.env.BACKEND_URL}/api/users/info`, { + // method: "GET", + // headers: { + // Cookie: `${access_cookie?.name}=${access_cookie?.value}`, + // "Content-Type": "application/json", + // "Access-Control-Allow-Origin": "*", + // }, + // cache: "no-store", + // }); + + // if (response.ok) { + // const data = await response.json(); + // return new NextResponse(JSON.stringify(data), { + // status: 200, + // }); + // } + + // if (response.status == 401) { + // return new NextResponse("토큰 만료", { + // status: 401, + // }); + // } + + // cookies().delete("access_token"); + // cookies().delete("refresh_token"); + // return new NextResponse("서버 에러", { + // status: 500, + // }); +} + +export async function PUT(request: NextRequest) { + const access_cookie = request.cookies.get("access_token"); + if (!access_cookie) { + const refresh_cookie = request.cookies.get("refresh_token"); + if (!refresh_cookie) { + // 리프레시 토큰이 없으므로 요청 중단 + return new NextResponse("Refresh token not found", { status: 403 }); + } + // 리프레시 토큰으로 재발급 받아 재요청 보내기 위한 응답 + return new NextResponse("Refresh token not found", { status: 401 }); + } + + const requestData = await request.json(); + + // 사용자 정보 조회 API + const response = await fetch(`${process.env.BACKEND_URL}/api/users/info/disagree`, { + method: "PUT", + headers: { + Cookie: `${access_cookie?.name}=${access_cookie?.value}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(requestData), + cache: "no-store", + }); + + if (response.ok) { + return response; + } + + return new NextResponse("서버 에러", { + status: 500, + }); +} + +export async function DELETE(request: NextRequest) { + // const access_cookie = request.cookies.get("access_token"); + // if (!access_cookie) { + // const refresh_cookie = request.cookies.get("refresh_token"); + // if (!refresh_cookie) { + // // 리프레시 토큰이 없으므로 요청 중단 + // return new NextResponse("Refresh token not found", { status: 403 }); + // } + // // 리프레시 토큰으로 재발급 받아 재요청 보내기 위한 응답 + // return new NextResponse("Refresh token not found", { status: 401 }); + // } + + // const url = new URL(request.url); + + // // 사용자 삭제 + // const response = await fetchWithAuth( + // `${process.env.BACKEND_URL}/api/auth/oauth2?type=${url.searchParams.get("type")}`, + // { + // method: "DELETE", + // headers: { + // Cookie: `${access_cookie?.name}=${access_cookie?.value}`, + // "Content-Type": "application/json", + // }, + // cache: "no-store", + // }, + // ); + + // if (!response.ok) { + // return new NextResponse(`${response.statusText}`, { + // status: response.status, + // }); + // } + + // cookies().delete("access_token"); + // cookies().delete("refresh_token"); + // return new NextResponse("회원 탈퇴 성공", { + // status: 200, + // }); +} diff --git a/src/app/api/auth/user/route.ts b/src/app/api/auth/user/route.ts index 65dec4ef..97abe7de 100644 --- a/src/app/api/auth/user/route.ts +++ b/src/app/api/auth/user/route.ts @@ -58,7 +58,6 @@ export async function PUT(request: NextRequest) { } const requestData = await request.json(); - console.log("route.ts 파일1 : ", requestData); // 사용자 정보 조회 API const response = await fetch(`${process.env.BACKEND_URL}/api/users/info`, { diff --git a/src/app/auth/loading/naver/page.tsx b/src/app/auth/loading/naver/page.tsx new file mode 100644 index 00000000..ba6036bb --- /dev/null +++ b/src/app/auth/loading/naver/page.tsx @@ -0,0 +1,6 @@ +import AuthNaverContainer from "@/containers/auth/AuthNaverContainer"; + +const Page = () => { + return ; +}; +export default Page; diff --git a/src/app/informations/(detail)/[id]/page.tsx b/src/app/informations/(detail)/[id]/page.tsx index 621aab36..6dedaae0 100644 --- a/src/app/informations/(detail)/[id]/page.tsx +++ b/src/app/informations/(detail)/[id]/page.tsx @@ -1,6 +1,7 @@ import Breadcrumbs from "@/components/common/Breadcrumb"; import InformationViewer from "@/components/informations/detail/InformationViewer"; import RecommendationList from "@/components/informations/detail/RecommendationList"; +import CommentListContainer from "@/containers/informations/detail/CommentListContainer"; import { InformationDetailDto } from "@/types/InformationDto"; import { cookies } from "next/headers"; @@ -61,6 +62,7 @@ export default async function page({ params: { id } }: Props) { ]} /> + {/* */} ); diff --git a/src/components/auth/AddUserInformationForm.tsx b/src/components/auth/AddUserInformationForm.tsx index d70e8538..64fdeaa2 100644 --- a/src/components/auth/AddUserInformationForm.tsx +++ b/src/components/auth/AddUserInformationForm.tsx @@ -3,14 +3,14 @@ import { AddUserInformationFormSchema } from "@/lib/zod/schema/AddUserInformationFormSchema"; import useAuthStore from "@/store/authStore"; import useToastifyStore from "@/store/toastifyStore"; +import { IModalComponent } from "@/types/ModalState"; import { fetchWithAuth } from "@/utils/fetchWithAuth"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; +import ModalTemplate from "../common/modal/ModalTemplate"; -interface IAddUserInformationForm { - closeModal: () => void; -} -const AddUserInformationForm = (props: IAddUserInformationForm) => { + +const AddUserInformationForm = (props: IModalComponent) => { const authStore = useAuthStore(); const toastifyStore = useToastifyStore(); @@ -20,6 +20,8 @@ const AddUserInformationForm = (props: IAddUserInformationForm) => { name: "", age: 0, sex: "", + termConditionAgreement: true, + privacyPolicyAgreement: true, }, }); @@ -32,46 +34,45 @@ const AddUserInformationForm = (props: IAddUserInformationForm) => { }; const addUserInformationSubmit = async () => { - const response = await fetchWithAuth("/api/auth/user", { + const response = await fetchWithAuth("/api/auth/user/info/agree", { method: "PUT", body: JSON.stringify({ sex: getValues("sex"), name: getValues("name"), age: getValues("age"), + termConditionAgreement: true, + privacyPolicyAgreement: true, }), }); if (response.status == 204) { toastifyStore.setToastify({ type: "success", - message: "등록 성공", + message: "제출 완료", }); authStore.setUser({ sex: getValues("sex"), age: getValues("age"), }); - props.closeModal(); + props.closeModal!(); } }; return ( -
+ + {props.closeButtonComponent}

안녕하세요 솔리투어입니다

신뢰할 수 있는 이용 환경을 위해 필요한 정보를 입력해 주세요

-
+

이름

{

- {" "} - 연도(나이){" "} + 연도(나이) - {" "} - {new Date().getFullYear() - 58} ~ {new Date().getFullYear() - 19}{" "} + {new Date().getFullYear() - 58} ~ {new Date().getFullYear() - 19}

-
- -
+

정보를 입력하지 않아도 서비스를 이용할 수 있으나 일부 서비스 이용이 제한될 수 있습니다

- - -
+ ); }; export default AddUserInformationForm; diff --git a/src/components/auth/AddUserInformationInitForm.tsx b/src/components/auth/AddUserInformationInitForm.tsx index da6678b9..35aff0db 100644 --- a/src/components/auth/AddUserInformationInitForm.tsx +++ b/src/components/auth/AddUserInformationInitForm.tsx @@ -1,18 +1,17 @@ "use client"; import Image from "next/image"; -import Link from "next/link"; import { useFormContext } from "react-hook-form"; interface IAddUserInformationInitForm { - kakaoLogin: () => void; - addUserInformationSubmit: () => void; + handleSubmit: (isAgree: boolean) => void; handleInputChange: (e: React.ChangeEvent) => void; + handleHomeButtonClick: () => void; } const AddUserInformationInitForm = ({ - kakaoLogin, - addUserInformationSubmit, + handleSubmit, handleInputChange, + handleHomeButtonClick, }: IAddUserInformationInitForm) => { const formContext = useFormContext(); @@ -23,18 +22,14 @@ const AddUserInformationInitForm = ({ >
-
- +

안녕하세요 솔리투어입니다 @@ -50,7 +45,7 @@ const AddUserInformationInitForm = ({

신뢰할 수 있는 이용 환경을 위해 필요한 정보를 입력해 주세요

-
+

이름

- {" "} - 연도(나이){" "} + 연도(나이) + + {new Date().getFullYear() - 58} ~ {new Date().getFullYear() - 19} +

- {" "} - 현재는 {new Date().getFullYear() - 58} ~{" "} + 현재는 {new Date().getFullYear() - 58} ~ {new Date().getFullYear() - 19}년생만 모임 서비스를 이용할 수 - 있습니다.{" "} + 있습니다.
diff --git a/src/components/auth/SignIn.tsx b/src/components/auth/SignIn.tsx index ac908da0..186c0753 100644 --- a/src/components/auth/SignIn.tsx +++ b/src/components/auth/SignIn.tsx @@ -59,6 +59,21 @@ const SignIn = () => {
구글로 로그인 */} + {/* +
+ {"naver-logo-image"} +
+ 네이버로 로그인 + */}
diff --git a/src/components/auth/UserDropDown.tsx b/src/components/auth/UserDropDown.tsx new file mode 100644 index 00000000..b3574372 --- /dev/null +++ b/src/components/auth/UserDropDown.tsx @@ -0,0 +1,104 @@ +import useModalState from "@/hooks/useModalState"; +import useOutsideClick from "@/hooks/useOutsideClick"; +import useAuthStore from "@/store/authStore"; +import { ModalState } from "@/types/ModalState"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useRef } from "react"; +import UserImage from "./UserImage"; + +const UserDropDown = () => { + const authStore = useAuthStore(); + const modalState = useModalState(); + const router = useRouter(); + const ref = useRef(); + const ref1 = useRef(); + const logoutHandler = async () => { + // api로 로그아웃 요청해서 쿠키제거 + const response = await fetch("/api/auth/logout", { + method: "POST", + }); + + if (!response.ok) { + throw new Error(response.statusText); + } + authStore.initialize(); + router.push("/"); + router.refresh(); + }; + + useOutsideClick(ref, () => { + modalState.closeModal(); + }); + + return ( + +

+ } + + ); +}; +export default UserDropDown; \ No newline at end of file diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index 8a1ea5ea..ebb1d342 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -2,8 +2,8 @@ import HeaderSidebarContainer from "@/containers/common/HeaderSidebarContainer"; import Image from "next/image"; import Link from "next/link"; import { MdOutlineMenu } from "react-icons/md"; -import UserImage from "../auth/UserImage"; import ReactToastifyComponent from "./ReactToastifyComponent"; +import UserDropDown from "../auth/UserDropDown"; interface Props { pathname: string; @@ -11,10 +11,7 @@ interface Props { transparent: boolean; onMenuClicked: () => void; onClose: () => void; - logoutHandler: () => void; userId: number; - userSex: string; - userProfile: string; } const Header = ({ @@ -24,9 +21,6 @@ const Header = ({ onMenuClicked, onClose, userId, - logoutHandler, - userSex, - userProfile, }: Props) => { return (
@@ -116,23 +110,7 @@ const Header = ({ ) : userId > 0 ? ( <> - - - - - + ) : ( <> diff --git a/src/components/common/HeaderSidebar.tsx b/src/components/common/HeaderSidebar.tsx index 8767e507..9a348915 100644 --- a/src/components/common/HeaderSidebar.tsx +++ b/src/components/common/HeaderSidebar.tsx @@ -1,7 +1,6 @@ import Image from "next/image"; import Link from "next/link"; import { Dispatch, SetStateAction } from "react"; -import { CiLogout } from "react-icons/ci"; import { MdClose } from "react-icons/md"; interface Props { @@ -28,7 +27,7 @@ const HeaderSidebar = ({ onClick={closeWithFadeOut} className={`${animationFlag ? "animate-sidebarFadeOut" : "animate-sidebarFadeIn"} fixed left-0 top-0 z-50 flex h-[200%] w-full flex-row justify-end bg-black/25`} > -