Skip to content

Commit 06a2aec

Browse files
authored
Feat: 간편 로그인 / 회원가입 기능 구현
Feat: 간편 로그인 / 회원가입 기능 구현
2 parents af1edc7 + 85f7a70 commit 06a2aec

File tree

12 files changed

+324
-28
lines changed

12 files changed

+324
-28
lines changed

components/Auth/SnsLogin.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,21 @@ const SnsLogin = () => {
66
<div className="flex items-center justify-between bg-gray300 rounded-lg px-6 py-3 mt-8">
77
<span>소셜 로그인</span>
88
<div className="flex gap-4">
9-
<Link href="https://accounts.google.com/o/oauth2/v2/auth?scope=openid%20profile%20email&response_type=token&redirect_uri=http://localhost:3000/google&client_id=1079911783112-7rg5ecp9ia9lorm7pit0m2nb2ti1rpt0.apps.googleusercontent.com">
9+
<Link
10+
href={`https://accounts.google.com/o/oauth2/v2/auth?scope=https://www.googleapis.com/auth/userinfo.profile&response_type=code&redirect_uri=${process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_SIGN_IN}&client_id=${process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}`}
11+
>
1012
<Image src="/icons/google.svg" width="42" height="42" alt="구글" />
1113
</Link>
12-
<Image
13-
src="/icons/kakaotalk.svg"
14-
width="42"
15-
height="42"
16-
alt="카카오톡"
17-
/>
14+
<Link
15+
href={`https://kauth.kakao.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID}&redirect_uri=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI_SIGN_IN}&response_type=code`}
16+
>
17+
<Image
18+
src="/icons/kakaotalk.svg"
19+
width="42"
20+
height="42"
21+
alt="카카오톡"
22+
/>
23+
</Link>
1824
</div>
1925
</div>
2026
);

components/Auth/SnsPassword.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Image from "next/image";
2+
import Link from "next/link";
3+
4+
const SnsLogin = () => {
5+
return (
6+
<div className="flex items-center justify-between bg-gray300 rounded-lg px-6 py-3 mt-8">
7+
<span>소셜 회원가입</span>
8+
<div className="flex gap-4">
9+
<Link
10+
href={`https://accounts.google.com/o/oauth2/v2/auth?scope=https://www.googleapis.com/auth/userinfo.profile&response_type=code&redirect_uri=${process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_SIGN_UP}&client_id=${process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}`}
11+
>
12+
<Image src="/icons/google.svg" width="42" height="42" alt="구글" />
13+
</Link>
14+
<Link
15+
href={`https://kauth.kakao.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID}&redirect_uri=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI_SIGN_UP}&response_type=code`}
16+
>
17+
<Image
18+
src="/icons/kakaotalk.svg"
19+
width="42"
20+
height="42"
21+
alt="카카오톡"
22+
/>
23+
</Link>
24+
</div>
25+
</div>
26+
);
27+
};
28+
29+
export default SnsLogin;

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"axios": "^1.7.7",
1313
"cookie": "^1.0.1",
14+
"jwt-decode": "^4.0.0",
1415
"next": "15.0.2",
1516
"react": "^18.2.0",
1617
"react-dom": "^18.2.0",

pages/api/auth/sign-in/google.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import axios from "axios";
2+
import axiosInstance from "@/lib/api/axiosInstanceApi";
3+
import { NextApiRequest, NextApiResponse } from "next";
4+
import { serialize } from "cookie";
5+
6+
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
7+
try {
8+
const { code } = req.query;
9+
if (!code) {
10+
return res.status(400).json({ message: "인증 코드가 없습니다." });
11+
}
12+
13+
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
14+
const clientSecret = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_SECRET;
15+
const redirectUri =
16+
process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_SIGN_IN ||
17+
"http://localhost:3000/";
18+
19+
if (!clientId || !clientSecret) {
20+
return res
21+
.status(500)
22+
.json({ message: "Google API 클라이언트 정보가 설정되지 않았습니다." });
23+
}
24+
25+
// 토큰 요청
26+
const tokenUrl = "https://oauth2.googleapis.com/token";
27+
const params = {
28+
code: code as string,
29+
client_id: clientId,
30+
client_secret: clientSecret,
31+
redirect_uri: redirectUri,
32+
grant_type: "authorization_code",
33+
};
34+
35+
const tokenResponse = await axios.post(tokenUrl, params);
36+
const { id_token } = tokenResponse.data;
37+
if (!id_token) {
38+
return res
39+
.status(401)
40+
.json({ message: "ID 토큰을 가져오지 못했습니다." });
41+
}
42+
43+
// 이미 회원인지 체크
44+
try {
45+
const loginResponse = await axiosInstance.post("/auth/sign-in/google", {
46+
token: id_token,
47+
redirectUri,
48+
});
49+
50+
const accessToken = loginResponse.data.access_token;
51+
if (accessToken) {
52+
res.setHeader(
53+
"Set-Cookie",
54+
serialize("accessToken", accessToken, {
55+
httpOnly: true,
56+
secure: process.env.NODE_ENV === "production",
57+
sameSite: "lax",
58+
maxAge: 60 * 60 * 24,
59+
path: "/",
60+
})
61+
);
62+
return res.redirect("http://localhost:3000");
63+
}
64+
} catch (loginError: any) {
65+
return res.redirect("/signup");
66+
}
67+
} catch (error: any) {
68+
console.error("Error:", error.response?.data || error.message);
69+
return res.status(500).json({
70+
message: "서버 오류",
71+
error: error.response?.data || error.message,
72+
});
73+
}
74+
};
75+
76+
export default handler;

pages/api/auth/sign-in/kakao.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import axiosInstance from "@/lib/api/axiosInstanceApi";
2+
import { NextApiRequest, NextApiResponse } from "next";
3+
import { serialize } from "cookie";
4+
5+
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
6+
try {
7+
const { code } = req.query;
8+
console.log(code);
9+
if (!code) {
10+
return res.status(400).json({ message: "인증 코드가 없습니다." });
11+
}
12+
13+
const redirectUri =
14+
process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI_SIGN_IN ||
15+
"http://localhost:3000/";
16+
17+
try {
18+
const loginResponse = await axiosInstance.post("/auth/sign-in/kakao", {
19+
token: code,
20+
redirectUri,
21+
});
22+
23+
const accessToken = loginResponse.data.access_token;
24+
if (accessToken) {
25+
res.setHeader(
26+
"Set-Cookie",
27+
serialize("accessToken", accessToken, {
28+
httpOnly: true,
29+
secure: process.env.NODE_ENV === "production",
30+
sameSite: "lax",
31+
maxAge: 60 * 60 * 24,
32+
path: "/",
33+
})
34+
);
35+
return res.redirect("http://localhost:3000");
36+
}
37+
} catch (loginError: any) {
38+
console.error(
39+
"로그인 실패:",
40+
loginError.response?.data || loginError.message
41+
);
42+
return res.redirect("signup");
43+
}
44+
} catch (error: any) {
45+
console.error("Error:", error.response?.data || error.message);
46+
return res.status(500).json({
47+
message: "서버 오류",
48+
error: error.response?.data || error.message,
49+
});
50+
}
51+
};
52+
53+
export default handler;

pages/api/auth/sign-up/google.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import axios from "axios";
2+
import axiosInstance from "@/lib/api/axiosInstanceApi";
3+
import { NextApiRequest, NextApiResponse } from "next";
4+
import { serialize } from "cookie";
5+
import { jwtDecode } from "jwt-decode";
6+
7+
interface GoogleUserInfo {
8+
name: string;
9+
}
10+
11+
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
12+
try {
13+
const { code } = req.query;
14+
if (!code) {
15+
return res.status(400).json({ message: "인증 코드가 없습니다." });
16+
}
17+
18+
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
19+
const clientSecret = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_SECRET;
20+
const redirectUri =
21+
process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_SIGN_UP ||
22+
"http://localhost:3000/";
23+
24+
if (!clientId || !clientSecret) {
25+
return res
26+
.status(500)
27+
.json({ message: "Google API 클라이언트 정보가 설정되지 않았습니다." });
28+
}
29+
30+
const tokenUrl = "https://oauth2.googleapis.com/token";
31+
const params = {
32+
code: code as string,
33+
client_id: clientId,
34+
client_secret: clientSecret,
35+
redirect_uri: redirectUri,
36+
grant_type: "authorization_code",
37+
};
38+
39+
const tokenResponse = await axios.post(tokenUrl, params);
40+
const { id_token } = tokenResponse.data;
41+
if (!id_token) {
42+
return res
43+
.status(401)
44+
.json({ message: "ID 토큰을 가져오지 못했습니다." });
45+
}
46+
47+
const userInfo: GoogleUserInfo = jwtDecode(id_token);
48+
const { name } = userInfo;
49+
50+
try {
51+
const signUpResponse = await axiosInstance.post("/auth/sign-up/google", {
52+
name: name || "사용자",
53+
token: id_token,
54+
redirectUri: "http://localhost:3000",
55+
});
56+
57+
const accessToken = signUpResponse.data.access_token;
58+
if (accessToken) {
59+
res.setHeader(
60+
"Set-Cookie",
61+
serialize("accessToken", accessToken, {
62+
httpOnly: true,
63+
secure: process.env.NODE_ENV === "production",
64+
sameSite: "lax",
65+
maxAge: 60 * 60 * 24,
66+
path: "/",
67+
})
68+
);
69+
return res
70+
.status(200)
71+
.json({ message: "회원가입 성공", redirectUrl: "/" });
72+
}
73+
} catch (signUpError: any) {
74+
return res.redirect("/login");
75+
}
76+
} catch (error: any) {
77+
console.error("Error:", error.response?.data || error.message);
78+
return res.status(500).json({
79+
message: "서버 오류",
80+
error: error.response?.data || error.message,
81+
});
82+
}
83+
};
84+
85+
export default handler;

pages/api/auth/sign-up/kakao.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import axiosInstance from "@/lib/api/axiosInstanceApi";
2+
import { NextApiRequest, NextApiResponse } from "next";
3+
import { serialize } from "cookie";
4+
5+
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
6+
try {
7+
const { code } = req.query;
8+
console.log(code);
9+
if (!code) {
10+
return res.status(400).json({ message: "인증 코드가 없습니다." });
11+
}
12+
13+
const redirectUri =
14+
process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI_SIGN_UP ||
15+
"http://localhost:3000/";
16+
17+
// 회원가입 시도
18+
try {
19+
const signUpResponse = await axiosInstance.post("/auth/sign-up/kakao", {
20+
name: "사용자",
21+
token: code,
22+
redirectUri,
23+
});
24+
25+
const accessToken = signUpResponse.data.access_token;
26+
if (accessToken) {
27+
res.setHeader(
28+
"Set-Cookie",
29+
serialize("accessToken", accessToken, {
30+
httpOnly: true,
31+
secure: process.env.NODE_ENV === "production",
32+
sameSite: "lax",
33+
maxAge: 60 * 60 * 24,
34+
path: "/",
35+
})
36+
);
37+
return res.redirect("http://localhost:3000");
38+
}
39+
} catch (signUpError: any) {
40+
console.error(
41+
"회원가입 실패:",
42+
signUpError.response?.data || signUpError.message
43+
);
44+
return res.redirect("/login");
45+
}
46+
} catch (error: any) {
47+
console.error("Error:", error.response?.data || error.message);
48+
return res.status(500).json({
49+
message: "서버 오류",
50+
error: error.response?.data || error.message,
51+
});
52+
}
53+
};
54+
55+
export default handler;

0 commit comments

Comments
 (0)