Skip to content

Commit 376b4a2

Browse files
authored
Merge pull request #102 from FE9-2/feat/oauth
Feat: OAuth 간편 구글/카카오 회원가입/로그인 구현 완료
2 parents 7d5feb3 + 320db9d commit 376b4a2

File tree

9 files changed

+181
-122
lines changed

9 files changed

+181
-122
lines changed

src/app/(auth)/login/page.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,16 @@ export default function LoginPage() {
6565
</div>
6666
<div className="flex justify-center space-x-6">
6767
<Link
68-
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}&client_id=${process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}`}
68+
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}&client_id=${process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}&state=${encodeURIComponent(
69+
JSON.stringify({ provider: "google", action: "login" })
70+
)}`}
6971
>
7072
<Image src="/icons/social/social_google.svg" width={72} height={72} alt="구글 로그인" />
7173
</Link>
7274
<Link
73-
href={`https://kauth.kakao.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY}&redirect_uri=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI}&response_type=code`}
75+
href={`https://kauth.kakao.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY}&redirect_uri=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI}&response_type=code&state=${encodeURIComponent(
76+
JSON.stringify({ provider: "kakao", action: "login" })
77+
)}`}
7478
>
7579
<Image src="/icons/social/social_kakao.svg" width={72} height={72} alt="카카오 로그인" />
7680
</Link>

src/app/(auth)/signup/applicant/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,14 @@ export default function ApplicantSignupPage() {
109109
<div className="flex justify-center space-x-4">
110110
<Link
111111
href={`https://accounts.google.com/o/oauth2/v2/auth?scope=https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile&response_type=code&redirect_uri=${process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI}&client_id=${process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}&state=${encodeURIComponent(
112-
JSON.stringify({ provider: "google", role: "APPLICANT" })
112+
JSON.stringify({ provider: "google", action: "signup", role: "APPLICANT" })
113113
)}`}
114114
>
115115
<Image src="/icons/social/social_google.svg" width={72} height={72} alt="구글 회원가입" />
116116
</Link>
117117
<Link
118118
href={`https://kauth.kakao.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY}&redirect_uri=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI}&response_type=code&state=${encodeURIComponent(
119-
JSON.stringify({ provider: "kakao", role: "APPLICANT" })
119+
JSON.stringify({ provider: "kakao", action: "signup", role: "APPLICANT" })
120120
)}`}
121121
>
122122
<Image src="/icons/social/social_kakao.svg" width={72} height={72} alt="카카오 회원가입" />

src/app/(auth)/signup/owner/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,14 @@ export default function OwnerSignupPage() {
131131
<div className="flex justify-center space-x-4">
132132
<Link
133133
href={`https://accounts.google.com/o/oauth2/v2/auth?scope=https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile&response_type=code&redirect_uri=${process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI}&client_id=${process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}&state=${encodeURIComponent(
134-
JSON.stringify({ provider: "google", role: "OWNER" })
134+
JSON.stringify({ provider: "google", action: "signup", role: "OWNER" })
135135
)}`}
136136
>
137137
<Image src="/icons/social/social_google.svg" width={72} height={72} alt="구글 회원가입" />
138138
</Link>
139139
<Link
140140
href={`https://kauth.kakao.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY}&redirect_uri=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI}&response_type=code&state=${encodeURIComponent(
141-
JSON.stringify({ provider: "kakao", role: "OWNER" })
141+
JSON.stringify({ provider: "kakao", action: "signup", role: "OWNER" })
142142
)}`}
143143
>
144144
<Image src="/icons/social/social_kakao.svg" width={72} height={72} alt="카카오 회원가입" />
Lines changed: 74 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,27 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import axios from "axios";
33
import { decodeJwt } from "@/middleware";
4-
import { OauthUser } from "@/types/oauth/oauthReq";
54
import apiClient from "@/lib/apiClient";
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: OauthUser = {
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: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,102 @@
11
import { NextRequest, NextResponse } from "next/server";
2-
import { OauthUser } from "@/types/oauth/oauthReq";
2+
import axios from "axios";
33
import apiClient from "@/lib/apiClient";
4+
import { OauthLoginUser, OauthResponse, OauthSignupUser } from "@/types/oauth/oauth";
5+
import { cookies } from "next/headers";
46

5-
export const GET = async (req: NextRequest) => {
6-
const searchParams = req.nextUrl.searchParams;
7+
export const GET = async (request: NextRequest) => {
8+
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-
const { provider, role } = parsedState;
2622

27-
const clientId = process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY;
23+
const { provider, action, role } = parsedState;
2824
const redirectUri = process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI;
2925

30-
if (!clientId || !redirectUri) {
26+
if (!redirectUri) {
3127
return NextResponse.json({ message: "Environment variables not set" }, { status: 500 });
3228
}
3329

34-
const kakaoUser: OauthUser = {
35-
role: role,
36-
name: "", // 기본값 설정 (빈 문자열)
37-
token: code, // 인가코드 그대로 전달
38-
redirectUri: redirectUri,
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+
},
3940
};
4041

41-
try {
42-
// 인가코드를 포함한 데이터를 백엔드로 전달
43-
const kakaoSignupResponse = await apiClient.post(`/oauth/sign-up/${provider}`, kakaoUser);
44-
console.log("카카오 회원가입 성공:", kakaoSignupResponse.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+
}
55+
} else if (action === "login") {
56+
await loginUser();
57+
} else {
58+
throw new Error("Invalid action");
59+
}
60+
};
4561

46-
// 사용자 정보를 클라이언트에 반환
47-
// return NextResponse.json(kakaoSignupResponse.data);
48-
} catch (error: any) {
49-
// 에러 타입 명시
50-
console.error("카카오 회원가입 에러:", error.response?.data || error.message);
62+
const loginUser = async () => {
63+
try {
64+
const { data: loginResponse } = await axios.post<OauthResponse>(
65+
`${process.env.NEXT_PUBLIC_DOMAIN_URL}/api/oauth/login/${provider}`,
66+
kakaoUser.login
67+
);
68+
console.log("카카오 로그인 성공:", loginResponse);
5169

52-
// return NextResponse.json({ message: error.response?.data || "Error during Kakao signup" }, { status: 500 });
53-
}
70+
// 쿠키 저장
71+
const { accessToken, refreshToken } = loginResponse;
72+
setCookies(accessToken, refreshToken);
73+
} catch (error: any) {
74+
console.error("카카오 로그인 중 오류:", error.message || error);
75+
throw new Error("로그인 중 서버 오류");
76+
}
77+
};
5478

55-
try {
56-
// 사용자 정보를 클라이언트에 반환
57-
const response = NextResponse.redirect("http://localhost:3000");
58-
response.cookies.set("user", JSON.stringify(kakaoUser), {
79+
const setCookies = (accessToken: string, refreshToken: string) => {
80+
cookies().set("accessToken", accessToken, {
5981
httpOnly: true,
6082
secure: process.env.NODE_ENV === "production",
61-
sameSite: "strict",
62-
maxAge: 60 * 60 * 24, // 1일
83+
sameSite: "lax",
6384
path: "/",
6485
});
65-
return response;
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();
6696
} catch (error: any) {
67-
console.error("카카오 회원가입 에러:", error.response?.data || error.message);
68-
return NextResponse.json({ message: error.response?.data || "서버에러" }, { status: 500 });
97+
console.error("OAuth 처리 중 오류:", error.message || error);
98+
return NextResponse.json({ message: error.message || "서버 오류" }, { status: 500 });
6999
}
100+
101+
return NextResponse.redirect(new URL("/", request.url));
70102
};

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

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import apiClient from "@/lib/apiClient";
66
// OAuth 로그인 API
77
export async function POST(request: Request, { params }: { params: { provider: string } }) {
88
try {
9+
console.log("/api/oauth/login");
910
const provider = params.provider;
1011

1112
// provider 유효성 검사
@@ -15,28 +16,10 @@ export async function POST(request: Request, { params }: { params: { provider: s
1516

1617
// 요청 본문 파싱
1718
const body = await request.json();
18-
19+
console.log("Received body:", body); // 요청 본문 로그 출력
1920
// OAuth 로그인 요청
2021
const response = await apiClient.post(`/oauth/sign-in/${provider}`, body);
2122

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

0 commit comments

Comments
 (0)