diff --git a/components/Auth/SnsLogin.tsx b/components/Auth/SnsLogin.tsx index 2a4a2a6..d0c8b30 100644 --- a/components/Auth/SnsLogin.tsx +++ b/components/Auth/SnsLogin.tsx @@ -6,15 +6,21 @@ const SnsLogin = () => {
소셜 로그인
- + 구글 - 카카오톡 + + 카카오톡 +
); diff --git a/components/Auth/SnsPassword.tsx b/components/Auth/SnsPassword.tsx new file mode 100644 index 0000000..cea086b --- /dev/null +++ b/components/Auth/SnsPassword.tsx @@ -0,0 +1,29 @@ +import Image from "next/image"; +import Link from "next/link"; + +const SnsLogin = () => { + return ( +
+ 소셜 회원가입 +
+ + 구글 + + + 카카오톡 + +
+
+ ); +}; + +export default SnsLogin; diff --git a/package-lock.json b/package-lock.json index a01fe10..c930b05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "axios": "^1.7.7", "cookie": "^1.0.1", + "jwt-decode": "^4.0.0", "next": "15.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -3782,6 +3783,15 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index 9ba9f72..6b36999 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "axios": "^1.7.7", "cookie": "^1.0.1", + "jwt-decode": "^4.0.0", "next": "15.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/pages/api/auth/sign-in/google.ts b/pages/api/auth/sign-in/google.ts new file mode 100644 index 0000000..9334068 --- /dev/null +++ b/pages/api/auth/sign-in/google.ts @@ -0,0 +1,76 @@ +import axios from "axios"; +import axiosInstance from "@/lib/api/axiosInstanceApi"; +import { NextApiRequest, NextApiResponse } from "next"; +import { serialize } from "cookie"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { code } = req.query; + if (!code) { + return res.status(400).json({ message: "인증 코드가 없습니다." }); + } + + const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID; + const clientSecret = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_SECRET; + const redirectUri = + process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_SIGN_IN || + "http://localhost:3000/"; + + if (!clientId || !clientSecret) { + return res + .status(500) + .json({ message: "Google API 클라이언트 정보가 설정되지 않았습니다." }); + } + + // 토큰 요청 + const tokenUrl = "https://oauth2.googleapis.com/token"; + const params = { + code: code as string, + client_id: clientId, + client_secret: clientSecret, + redirect_uri: redirectUri, + grant_type: "authorization_code", + }; + + const tokenResponse = await axios.post(tokenUrl, params); + const { id_token } = tokenResponse.data; + if (!id_token) { + return res + .status(401) + .json({ message: "ID 토큰을 가져오지 못했습니다." }); + } + + // 이미 회원인지 체크 + try { + const loginResponse = await axiosInstance.post("/auth/sign-in/google", { + token: id_token, + redirectUri, + }); + + const accessToken = loginResponse.data.access_token; + if (accessToken) { + res.setHeader( + "Set-Cookie", + serialize("accessToken", accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 60 * 60 * 24, + path: "/", + }) + ); + return res.redirect("http://localhost:3000"); + } + } catch (loginError: any) { + return res.redirect("/signup"); + } + } catch (error: any) { + console.error("Error:", error.response?.data || error.message); + return res.status(500).json({ + message: "서버 오류", + error: error.response?.data || error.message, + }); + } +}; + +export default handler; diff --git a/pages/api/auth/sign-in.ts b/pages/api/auth/sign-in/index.ts similarity index 100% rename from pages/api/auth/sign-in.ts rename to pages/api/auth/sign-in/index.ts diff --git a/pages/api/auth/sign-in/kakao.ts b/pages/api/auth/sign-in/kakao.ts new file mode 100644 index 0000000..3389bc3 --- /dev/null +++ b/pages/api/auth/sign-in/kakao.ts @@ -0,0 +1,53 @@ +import axiosInstance from "@/lib/api/axiosInstanceApi"; +import { NextApiRequest, NextApiResponse } from "next"; +import { serialize } from "cookie"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { code } = req.query; + console.log(code); + if (!code) { + return res.status(400).json({ message: "인증 코드가 없습니다." }); + } + + const redirectUri = + process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI_SIGN_IN || + "http://localhost:3000/"; + + try { + const loginResponse = await axiosInstance.post("/auth/sign-in/kakao", { + token: code, + redirectUri, + }); + + const accessToken = loginResponse.data.access_token; + if (accessToken) { + res.setHeader( + "Set-Cookie", + serialize("accessToken", accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 60 * 60 * 24, + path: "/", + }) + ); + return res.redirect("http://localhost:3000"); + } + } catch (loginError: any) { + console.error( + "로그인 실패:", + loginError.response?.data || loginError.message + ); + return res.redirect("signup"); + } + } catch (error: any) { + console.error("Error:", error.response?.data || error.message); + return res.status(500).json({ + message: "서버 오류", + error: error.response?.data || error.message, + }); + } +}; + +export default handler; diff --git a/pages/api/auth/sign-up/google.ts b/pages/api/auth/sign-up/google.ts new file mode 100644 index 0000000..b98efd9 --- /dev/null +++ b/pages/api/auth/sign-up/google.ts @@ -0,0 +1,85 @@ +import axios from "axios"; +import axiosInstance from "@/lib/api/axiosInstanceApi"; +import { NextApiRequest, NextApiResponse } from "next"; +import { serialize } from "cookie"; +import { jwtDecode } from "jwt-decode"; + +interface GoogleUserInfo { + name: string; +} + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { code } = req.query; + if (!code) { + return res.status(400).json({ message: "인증 코드가 없습니다." }); + } + + const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID; + const clientSecret = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_SECRET; + const redirectUri = + process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_SIGN_UP || + "http://localhost:3000/"; + + if (!clientId || !clientSecret) { + return res + .status(500) + .json({ message: "Google API 클라이언트 정보가 설정되지 않았습니다." }); + } + + const tokenUrl = "https://oauth2.googleapis.com/token"; + const params = { + code: code as string, + client_id: clientId, + client_secret: clientSecret, + redirect_uri: redirectUri, + grant_type: "authorization_code", + }; + + const tokenResponse = await axios.post(tokenUrl, params); + const { id_token } = tokenResponse.data; + if (!id_token) { + return res + .status(401) + .json({ message: "ID 토큰을 가져오지 못했습니다." }); + } + + const userInfo: GoogleUserInfo = jwtDecode(id_token); + const { name } = userInfo; + + try { + const signUpResponse = await axiosInstance.post("/auth/sign-up/google", { + name: name || "사용자", + token: id_token, + redirectUri: "http://localhost:3000", + }); + + const accessToken = signUpResponse.data.access_token; + if (accessToken) { + res.setHeader( + "Set-Cookie", + serialize("accessToken", accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 60 * 60 * 24, + path: "/", + }) + ); + return res + .status(200) + .json({ message: "회원가입 성공", redirectUrl: "/" }); + } + } catch (signUpError: any) { + return res.redirect("/login"); + } + } catch (error: any) { + console.error("Error:", error.response?.data || error.message); + return res.status(500).json({ + message: "서버 오류", + error: error.response?.data || error.message, + }); + } +}; + +export default handler; diff --git a/pages/api/auth/sign-up.ts b/pages/api/auth/sign-up/index.ts similarity index 100% rename from pages/api/auth/sign-up.ts rename to pages/api/auth/sign-up/index.ts diff --git a/pages/api/auth/sign-up/kakao.ts b/pages/api/auth/sign-up/kakao.ts new file mode 100644 index 0000000..628aa3f --- /dev/null +++ b/pages/api/auth/sign-up/kakao.ts @@ -0,0 +1,55 @@ +import axiosInstance from "@/lib/api/axiosInstanceApi"; +import { NextApiRequest, NextApiResponse } from "next"; +import { serialize } from "cookie"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { code } = req.query; + console.log(code); + if (!code) { + return res.status(400).json({ message: "인증 코드가 없습니다." }); + } + + const redirectUri = + process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI_SIGN_UP || + "http://localhost:3000/"; + + // 회원가입 시도 + try { + const signUpResponse = await axiosInstance.post("/auth/sign-up/kakao", { + name: "사용자", + token: code, + redirectUri, + }); + + const accessToken = signUpResponse.data.access_token; + if (accessToken) { + res.setHeader( + "Set-Cookie", + serialize("accessToken", accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 60 * 60 * 24, + path: "/", + }) + ); + return res.redirect("http://localhost:3000"); + } + } catch (signUpError: any) { + console.error( + "회원가입 실패:", + signUpError.response?.data || signUpError.message + ); + return res.redirect("/login"); + } + } catch (error: any) { + console.error("Error:", error.response?.data || error.message); + return res.status(500).json({ + message: "서버 오류", + error: error.response?.data || error.message, + }); + } +}; + +export default handler; diff --git a/pages/google.tsx b/pages/google.tsx deleted file mode 100644 index 9f0059a..0000000 --- a/pages/google.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import axios from "axios"; -import { useEffect } from "react"; - -const Google = () => { - useEffect(() => { - const hash = window.location.hash; - const params = new URLSearchParams(hash.substring(1)); - const token = params.get("access_token"); - console.log(token); - - axios - .get(`https://www.googleapis.com/oauth2/v2/userinfo`, { - headers: { Authorization: `Bearer ${token}` }, - }) - .then((res) => console.log(res)); - }, []); - - return
안녕
; -}; - -export default Google; diff --git a/pages/signup/index.tsx b/pages/signup/index.tsx index cec6cad..864b4da 100644 --- a/pages/signup/index.tsx +++ b/pages/signup/index.tsx @@ -3,6 +3,7 @@ import SubmitButton from "@/components/SubMitButton"; import AuthLayout from "@/components/Layout/AuthLayout"; import Link from "next/link"; import useForm from "@/hooks/useForm"; +import SnsPassword from "@/components/Auth/SnsPassword"; const SignupPage = () => { const { values, errors, handleChange, handleBlur, handleSubmit } = @@ -68,6 +69,7 @@ const SignupPage = () => { 회원가입 +