-
Notifications
You must be signed in to change notification settings - Fork 3
[feat] 소셜 로그인 OAuth 인증을 프론트엔드에서 직접 처리 #248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
256194d
5531d98
8795502
5e03f72
b5f4067
6879bca
3f14f4e
3f83a6d
4e92a14
ceca9fc
fdc2da5
bf48757
f3af50f
f8a5a25
4319350
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,33 @@ | ||||||||||||||||
| import { API_BASE_URL, TEAM_ID } from "@/shared/config/env"; | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * 구글 소셜 로그인 | ||||||||||||||||
| * Google Identity Services SDK로 받은 access_token을 백엔드로 전달 | ||||||||||||||||
| * 백엔드가 토큰 검증 후 자체 JWT(accessToken, refreshToken) 발급 | ||||||||||||||||
| */ | ||||||||||||||||
| export async function loginWithGoogle(accessToken: string) { | ||||||||||||||||
| const res = await fetch(`${API_BASE_URL}/${TEAM_ID}/oauth/google`, { | ||||||||||||||||
|
haesol2022 marked this conversation as resolved.
|
||||||||||||||||
| method: "POST", | ||||||||||||||||
| headers: { "Content-Type": "application/json" }, | ||||||||||||||||
| body: JSON.stringify({ token: accessToken }), | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| if (!res.ok) throw new Error("구글 로그인 실패"); | ||||||||||||||||
| return res.json() as Promise<{ accessToken: string; refreshToken: string }>; | ||||||||||||||||
|
Comment on lines
+15
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 에러 응답 디버깅 정보 부족
♻️ 에러 정보 포함 제안- if (!res.ok) throw new Error("구글 로그인 실패");
+ if (!res.ok) {
+ const errorBody = await res.text().catch(() => "");
+ throw new Error(`구글 로그인 실패: ${res.status} ${errorBody}`);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * 카카오 소셜 로그인 | ||||||||||||||||
| * Next.js Route Handler에서 카카오 access_token으로 교환 후 백엔드로 전달 | ||||||||||||||||
| * 백엔드가 토큰 검증 후 자체 JWT(accessToken, refreshToken) 발급 | ||||||||||||||||
| */ | ||||||||||||||||
| export async function loginWithKakao(accessToken: string) { | ||||||||||||||||
| const res = await fetch(`${API_BASE_URL}/${TEAM_ID}/oauth/kakao`, { | ||||||||||||||||
| method: "POST", | ||||||||||||||||
| headers: { "Content-Type": "application/json" }, | ||||||||||||||||
| body: JSON.stringify({ token: accessToken }), | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| if (!res.ok) throw new Error("카카오 로그인 실패"); | ||||||||||||||||
| return res.json() as Promise<{ accessToken: string; refreshToken: string }>; | ||||||||||||||||
| } | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { type NextRequest, NextResponse } from "next/server"; | ||
|
|
||
| // 카카오 인가 코드 -> access_token 교환 | ||
| // client_secret이 필요하기 때문에 브라우저가 아닌 서버(Route Handler)에서 처리 | ||
| export async function GET(request: NextRequest) { | ||
| const { searchParams } = new URL(request.url); | ||
| const code = searchParams.get("code"); | ||
|
|
||
| if (!code) { | ||
| return NextResponse.json({ message: "인가 코드가 없습니다." }, { status: 400 }); | ||
| } | ||
|
|
||
| const tokenRes = await fetch("https://kauth.kakao.com/oauth/token", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/x-www-form-urlencoded" }, | ||
| body: new URLSearchParams({ | ||
| grant_type: "authorization_code", | ||
| client_id: process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID!, | ||
| client_secret: process.env.KAKAO_CLIENT_SECRET!, | ||
| code, | ||
| redirect_uri: process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI!, | ||
|
Comment on lines
+18
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 환경변수 미설정 시 런타임 오류 발생 가능
🛡️ 환경변수 검증 추가 제안 export async function GET(request: NextRequest) {
+ const clientId = process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID;
+ const clientSecret = process.env.KAKAO_CLIENT_SECRET;
+ const redirectUri = process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI;
+
+ if (!clientId || !clientSecret || !redirectUri) {
+ console.error("Missing Kakao OAuth environment variables");
+ return NextResponse.json({ message: "서버 설정 오류" }, { status: 500 });
+ }
+
const { searchParams } = new URL(request.url);
const code = searchParams.get("code");
// ... 이하 동일
body: new URLSearchParams({
grant_type: "authorization_code",
- client_id: process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID!,
- client_secret: process.env.KAKAO_CLIENT_SECRET!,
+ client_id: clientId,
+ client_secret: clientSecret,
code,
- redirect_uri: process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI!,
+ redirect_uri: redirectUri,
}),🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 배포 환경에서 환경변수가 항상 설정되어 있어서 별도로 런타임 검증하는 건 불필요하다고 판단함. |
||
| }), | ||
| }); | ||
|
|
||
| if (!tokenRes.ok) { | ||
| return NextResponse.json({ message: "카카오 토큰 교환 실패" }, { status: 400 }); | ||
| } | ||
|
|
||
| const { access_token } = await tokenRes.json(); | ||
|
|
||
| if (!access_token) { | ||
| return NextResponse.json({ message: "카카오 토큰 응답 오류" }, { status: 400 }); | ||
| } | ||
|
|
||
| return NextResponse.json({ accessToken: access_token }); | ||
|
haesol2022 marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,10 @@ | ||
| "use client"; | ||
| import { Button, InputField, SocialButton } from "@moum-zip/ui/components"; | ||
| import { Button, InputField, SocialButton, toast } from "@moum-zip/ui/components"; | ||
| import Link from "next/link"; | ||
| import { startTransition, useActionState } from "react"; | ||
| import { useForm } from "react-hook-form"; | ||
| import { signupAction } from "@/_pages/auth/actions"; | ||
| import { getGoogleLoginUrl, getKakaoLoginUrl } from "@/_pages/auth/use-cases/social-login-url"; | ||
| import { loginWithGoogle } from "@/_pages/auth/use-cases/social-login"; | ||
| import { ROUTES } from "@/shared/config/routes"; | ||
| import { PasswordInput } from "./password-input"; | ||
|
|
||
|
|
@@ -127,14 +127,33 @@ export const SignupForm = () => { | |
| provider="google" | ||
| className="w-full md:w-[222px]" | ||
| onClick={() => { | ||
| window.location.href = getGoogleLoginUrl(); | ||
| const client = window.google.accounts.oauth2.initTokenClient({ | ||
| client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!, | ||
| scope: "email profile", | ||
| callback: async (response) => { | ||
| try { | ||
| const { accessToken, refreshToken } = await loginWithGoogle(response.access_token); | ||
| const res = await fetch("/api/auth/token", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ accessToken, refreshToken }), | ||
| }); | ||
| if (!res.ok) throw new Error("토큰 저장 실패"); | ||
| window.location.replace(ROUTES.home); | ||
| } catch { | ||
| toast({ message: "회원가입 중 오류가 발생했어요. 다시 시도해주세요.", size: "small" }); | ||
| window.location.replace(ROUTES.signup); | ||
| } | ||
| }, | ||
| }); | ||
| client.requestAccessToken(); | ||
| }} | ||
|
Comment on lines
129
to
150
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major login-form.tsx와 동일한 에러 처리 누락 및 코드 중복 Google OAuth 콜백에 에러 처리가 없으며, ♻️ 공통 훅 또는 유틸리티로 추출 제안소셜 로그인 로직을 커스텀 훅으로 추출하여 재사용하세요: // hooks/use-google-login.ts
export function useGoogleLogin() {
const handleGoogleLogin = useCallback(() => {
const client = window.google.accounts.oauth2.initTokenClient({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
scope: "email profile",
callback: async (response) => {
try {
if (response.error) throw new Error(response.error);
const { accessToken, refreshToken } = await loginWithGoogle(response.access_token);
const res = await fetch("/api/auth/token", { /* ... */ });
if (!res.ok) throw new Error("Token storage failed");
window.location.replace(ROUTES.home);
} catch (error) {
console.error("Google login failed:", error);
// 에러 상태 업데이트
}
},
});
client.requestAccessToken();
}, []);
return { handleGoogleLogin };
}🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 에러 처리는 try/catch + toast로 이미 반영함. 공통 훅 추출은 login과 signup의 토스트 메시지와 리다이렉트 경로가 달라서 훅으로 묶으면 오히려 복잡해질 것 같아 그대로 유지하겠음. |
||
| /> | ||
| <SocialButton | ||
| provider="kakao" | ||
| className="w-full md:w-[222px]" | ||
| onClick={() => { | ||
| window.location.href = getKakaoLoginUrl(); | ||
| window.location.href = `https://kauth.kakao.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID}&redirect_uri=${encodeURIComponent(process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI!)}&response_type=code`; | ||
| }} | ||
|
haesol2022 marked this conversation as resolved.
|
||
| /> | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| declare namespace google.accounts.oauth2 { | ||
| interface TokenResponse { | ||
| error?: string; | ||
| error_description?: string; | ||
| } | ||
| } | ||
|
|
||
| declare global { | ||
| interface Window { | ||
| google: typeof google; | ||
| } | ||
| } | ||
|
|
||
| export {}; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://together-dallaem-api.vercel.app"; | ||
| export const API_BASE_URL = (process.env.NEXT_PUBLIC_API_URL ?? "https://together-dallaem-api.vercel.app").replace( | ||
| /\/+$/, | ||
| "", | ||
| ); | ||
|
|
||
| export const TEAM_ID = process.env.NEXT_PUBLIC_TEAM_ID ?? "moum-zip-dev"; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.