Skip to content

Commit 320db9d

Browse files
committed
feat: OAuth 간편 로그인/회원가입 구현 완료
1 parent 7e3fe04 commit 320db9d

File tree

3 files changed

+144
-105
lines changed

3 files changed

+144
-105
lines changed

src/app/api/oauth/callback/google/route.ts

Lines changed: 74 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,26 @@ import { NextRequest, NextResponse } from "next/server";
22
import axios from "axios";
33
import { decodeJwt } from "@/middleware";
44
import apiClient from "@/lib/apiClient";
5-
import { OauthSignupUser } from "@/types/oauth/oauth";
5+
import { OauthLoginUser, OauthResponse, OauthSignupUser } from "@/types/oauth/oauth";
6+
import { cookies } from "next/headers";
67

7-
export const GET = async (req: NextRequest) => {
8-
const searchParams = req.nextUrl.searchParams;
8+
export const GET = async (request: NextRequest) => {
9+
const searchParams = request.nextUrl.searchParams;
910
const code = searchParams.get("code");
1011
const state = searchParams.get("state");
1112

12-
if (!code) {
13-
return NextResponse.json({ message: "Code not found" }, { status: 400 });
13+
if (!code || !state) {
14+
return NextResponse.json({ message: `${!code ? "Code" : "State"} not found` }, { status: 400 });
1415
}
1516

16-
if (!state) {
17-
return NextResponse.json({ message: "State not found" }, { status: 400 });
18-
}
19-
20-
// `state`를 JSON으로 파싱
2117
let parsedState;
2218
try {
2319
parsedState = JSON.parse(decodeURIComponent(state));
24-
} catch (error) {
25-
console.error("Failed to parse state:", error);
20+
} catch {
2621
return NextResponse.json({ message: "Invalid state format" }, { status: 400 });
2722
}
2823

29-
const { provider, role } = parsedState;
30-
24+
const { provider, action, role } = parsedState;
3125
const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
3226
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
3327
const clientSecret = process.env.NEXT_PUBLIC_GOOGLE_SECRET;
@@ -38,8 +32,8 @@ export const GET = async (req: NextRequest) => {
3832
}
3933

4034
try {
41-
// Access Token 요청
42-
const tokenResponse = await axios.post(GOOGLE_TOKEN_URL, null, {
35+
// Google Access Token 요청
36+
const { data: tokenResponse } = await axios.post(GOOGLE_TOKEN_URL, null, {
4337
params: {
4438
code,
4539
client_id: clientId,
@@ -49,41 +43,76 @@ export const GET = async (req: NextRequest) => {
4943
},
5044
});
5145

52-
const { id_token } = tokenResponse.data;
53-
54-
// id_token 디코딩
46+
const { id_token } = tokenResponse;
5547
const decodedIdToken = decodeJwt(id_token);
5648
if (!decodedIdToken) {
5749
return NextResponse.json({ message: "Invalid ID token" }, { status: 400 });
5850
}
5951

60-
const googleUser: OauthSignupUser = {
61-
role: role,
62-
name: decodedIdToken.name,
63-
token: id_token,
52+
const googleUser: { signup: OauthSignupUser; login: OauthLoginUser } = {
53+
signup: {
54+
role,
55+
name: decodedIdToken.name,
56+
token: id_token,
57+
},
58+
login: {
59+
token: id_token,
60+
redirectUri,
61+
},
6462
};
65-
console.log("Google user:", googleUser);
66-
// OAuth 회원가입 API로 회원가입 요청
67-
try {
68-
const googleSignupResponse = await apiClient.post(`/oauth/sign-up/${provider}`, googleUser);
69-
console.log("구글 회원가입 성공:", googleSignupResponse.data);
70-
} catch (error) {
71-
const errorMessage = (error as any).response?.data;
72-
console.error("구글 회원가입 에러:", errorMessage);
73-
}
7463

75-
// 사용자 정보를 클라이언트에 반환
76-
const response = NextResponse.redirect("http://localhost:3000");
77-
response.cookies.set("user", JSON.stringify(googleUser), {
78-
httpOnly: true,
79-
secure: process.env.NODE_ENV === "production",
80-
sameSite: "strict",
81-
maxAge: 60 * 60 * 24, // 1일
82-
path: "/",
83-
});
84-
return response;
85-
} catch (error) {
86-
console.error("Google login error:", error);
87-
return NextResponse.json({ message: "서버에러" }, { status: 500 });
64+
const processUser = async () => {
65+
if (action === "signup") {
66+
try {
67+
const response = await apiClient.post<OauthResponse>(`/oauth/sign-up/${provider}`, googleUser.signup);
68+
console.log("구글 회원가입 성공:", response.data);
69+
} catch (error: any) {
70+
if (error.response?.status === 400) {
71+
console.log("이미 등록된 사용자입니다. 로그인 시도 중...");
72+
await loginUser();
73+
} else {
74+
throw new Error("회원가입 중 서버 오류");
75+
}
76+
}
77+
} else if (action === "login") {
78+
await loginUser();
79+
} else {
80+
throw new Error("잘못된 작업 에러");
81+
}
82+
};
83+
84+
const loginUser = async () => {
85+
const { data: loginResponse } = await axios.post<OauthResponse>(
86+
`${process.env.NEXT_PUBLIC_DOMAIN_URL}/api/oauth/login/${provider}`,
87+
googleUser.login
88+
);
89+
console.log("구글 로그인 성공:", loginResponse);
90+
91+
// 쿠키 저장
92+
const { accessToken, refreshToken } = loginResponse;
93+
setCookies(accessToken, refreshToken);
94+
};
95+
96+
const setCookies = (accessToken: string, refreshToken: string) => {
97+
cookies().set("accessToken", accessToken, {
98+
httpOnly: true,
99+
secure: process.env.NODE_ENV === "production",
100+
sameSite: "lax",
101+
path: "/",
102+
});
103+
cookies().set("refreshToken", refreshToken, {
104+
httpOnly: true,
105+
secure: process.env.NODE_ENV === "production",
106+
sameSite: "lax",
107+
path: "/",
108+
});
109+
};
110+
111+
await processUser();
112+
} catch (error: any) {
113+
console.error("OAuth 처리 중 오류:", error.message || error);
114+
return NextResponse.json({ message: error.message || "서버 오류" }, { status: 500 });
88115
}
116+
117+
return NextResponse.redirect(new URL("/", request.url));
89118
};
Lines changed: 69 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
11
import { NextRequest, NextResponse } from "next/server";
2-
import { OauthLoginUser, OauthResponse, OauthSignupUser } from "@/types/oauth/oauth";
32
import axios from "axios";
3+
import apiClient from "@/lib/apiClient";
4+
import { OauthLoginUser, OauthResponse, OauthSignupUser } from "@/types/oauth/oauth";
5+
import { cookies } from "next/headers";
46

57
export const GET = async (request: NextRequest) => {
68
const searchParams = request.nextUrl.searchParams;
79
const code = searchParams.get("code");
810
const state = searchParams.get("state");
911

10-
if (!code) {
11-
return NextResponse.json({ message: "Code not found" }, { status: 400 });
12-
}
13-
14-
if (!state) {
15-
return NextResponse.json({ message: "State not found" }, { status: 400 });
12+
if (!code || !state) {
13+
return NextResponse.json({ message: `${!code ? "Code" : "State"} not found` }, { status: 400 });
1614
}
1715

1816
let parsedState;
1917
try {
2018
parsedState = JSON.parse(decodeURIComponent(state));
21-
} catch (error) {
22-
console.error("Failed to parse state:", error);
19+
} catch {
2320
return NextResponse.json({ message: "Invalid state format" }, { status: 400 });
2421
}
25-
console.log("parsedState:", parsedState);
2622

2723
const { provider, action, role } = parsedState;
2824
const redirectUri = process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI;
@@ -31,44 +27,76 @@ export const GET = async (request: NextRequest) => {
3127
return NextResponse.json({ message: "Environment variables not set" }, { status: 500 });
3228
}
3329

34-
try {
35-
if (action === "signup") {
36-
// 회원가입 로직
37-
const signupUser: OauthSignupUser = {
38-
role: role || "user", // role 값이 없으면 기본값으로 "user" 설정
39-
name: "", // 회원가입 시 이름은 추후 API로 받아오거나 기본값으로 처리
40-
token: code, // 인가코드 전달
41-
redirectUri,
42-
};
43-
console.log("회원가입 시도:", signupUser);
30+
const kakaoUser: { signup: OauthSignupUser; login: OauthLoginUser } = {
31+
signup: {
32+
role: role || "user", // 기본 역할 설정
33+
name: "", // Kakao는 이름을 제공하지 않으므로 기본값
34+
token: code, // 인가 코드 전달
35+
},
36+
login: {
37+
token: code, // 인가 코드 전달
38+
redirectUri, // 리다이렉트 URI 포함
39+
},
40+
};
4441

45-
const signupResponse = await axios.post<OauthResponse>(`${process.env.NEXT_PUBLIC_DOMAIN_URL}/api/oauth/signup`, {
46-
provider,
47-
...signupUser,
48-
});
49-
console.log("회원가입 성공:", signupResponse.data);
42+
const processUser = async () => {
43+
if (action === "signup") {
44+
try {
45+
const response = await apiClient.post(`/oauth/sign-up/${provider}`, kakaoUser.signup);
46+
console.log("카카오 회원가입 성공:", response.data);
47+
} catch (error: any) {
48+
if (error.response?.status === 400) {
49+
console.log("이미 등록된 사용자입니다. 로그인 시도 중...");
50+
await loginUser();
51+
} else {
52+
throw new Error("회원가입 중 서버 오류");
53+
}
54+
}
5055
} else if (action === "login") {
51-
// 로그인 로직
52-
const loginUser: OauthLoginUser = {
53-
token: code,
54-
redirectUri,
55-
};
56-
console.log("로그인 시도", loginUser);
56+
await loginUser();
57+
} else {
58+
throw new Error("Invalid action");
59+
}
60+
};
5761

58-
const loginResponse = await axios.post<OauthResponse>(
62+
const loginUser = async () => {
63+
try {
64+
const { data: loginResponse } = await axios.post<OauthResponse>(
5965
`${process.env.NEXT_PUBLIC_DOMAIN_URL}/api/oauth/login/${provider}`,
60-
{
61-
...loginUser,
62-
}
66+
kakaoUser.login
6367
);
64-
console.log("로그인 성공:", loginResponse.data);
65-
} else {
66-
return NextResponse.json({ message: "Invalid action" }, { status: 400 });
68+
console.log("카카오 로그인 성공:", loginResponse);
69+
70+
// 쿠키 저장
71+
const { accessToken, refreshToken } = loginResponse;
72+
setCookies(accessToken, refreshToken);
73+
} catch (error: any) {
74+
console.error("카카오 로그인 중 오류:", error.message || error);
75+
throw new Error("로그인 중 서버 오류");
6776
}
77+
};
78+
79+
const setCookies = (accessToken: string, refreshToken: string) => {
80+
cookies().set("accessToken", accessToken, {
81+
httpOnly: true,
82+
secure: process.env.NODE_ENV === "production",
83+
sameSite: "lax",
84+
path: "/",
85+
});
86+
cookies().set("refreshToken", refreshToken, {
87+
httpOnly: true,
88+
secure: process.env.NODE_ENV === "production",
89+
sameSite: "lax",
90+
path: "/",
91+
});
92+
};
93+
94+
try {
95+
await processUser();
6896
} catch (error: any) {
69-
console.error(`${provider} ${action} 에러:`, error);
70-
return NextResponse.json({ message: error.response?.data || "Internal Server Error" }, { status: 500 });
97+
console.error("OAuth 처리 중 오류:", error.message || error);
98+
return NextResponse.json({ message: error.message || "서버 오류" }, { status: 500 });
7199
}
72-
// 로그인 성공 후 리다이렉트
100+
73101
return NextResponse.redirect(new URL("/", request.url));
74102
};

src/app/api/oauth/login/[provider]/route.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,6 @@ export async function POST(request: Request, { params }: { params: { provider: s
2020
// OAuth 로그인 요청
2121
const response = await apiClient.post(`/oauth/sign-in/${provider}`, body);
2222

23-
// 응답에서 토큰 추출
24-
const { accessToken, refreshToken } = response.data;
25-
26-
// 쿠키에 토큰 저장
27-
cookies().set("accessToken", accessToken, {
28-
httpOnly: true,
29-
secure: process.env.NODE_ENV === "production",
30-
sameSite: "lax",
31-
path: "/",
32-
});
33-
34-
cookies().set("refreshToken", refreshToken, {
35-
httpOnly: true,
36-
secure: process.env.NODE_ENV === "production",
37-
sameSite: "lax",
38-
path: "/",
39-
});
40-
4123
return NextResponse.json(response.data);
4224
} catch (error: unknown) {
4325
if (error instanceof AxiosError) {
@@ -46,6 +28,6 @@ export async function POST(request: Request, { params }: { params: { provider: s
4628
return NextResponse.json({ message: error.response.data.message }, { status: error.response.status });
4729
}
4830
}
49-
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
31+
return NextResponse.json({ message: "서버오류" }, { status: 500 });
5032
}
5133
}

0 commit comments

Comments
 (0)