Feat: 세션 유지 컴포넌트 추가 및 주석 디자인 다크테마 적용
This commit is contained in:
@@ -1,4 +1,14 @@
|
||||
"use server";
|
||||
/**
|
||||
* @file features/auth/actions.ts
|
||||
* @description 인증 관련 서버 액션 (Server Actions) 모음
|
||||
* @remarks
|
||||
* - [레이어] Service/API (Server Actions)
|
||||
* - [역할] 로그인, 회원가입, 로그아웃, 비밀번호 재설정 등 인증 로직 처리
|
||||
* - [데이터 흐름] Client Form -> Server Action -> Supabase Auth -> Client Redirect
|
||||
* - [연관 파일] login-form.tsx, signup-form.tsx, utils/supabase/server.ts
|
||||
*/
|
||||
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { cookies } from "next/headers";
|
||||
@@ -17,14 +27,9 @@ import { getAuthErrorMessage } from "./errors";
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* [FormData 추출 헬퍼]
|
||||
*
|
||||
* FormData에서 이메일과 비밀번호를 안전하게 추출합니다.
|
||||
* - 이메일은 trim()으로 공백 제거
|
||||
* - null/undefined 방지를 위해 기본값 "" 사용
|
||||
*
|
||||
* @param formData - HTML form에서 전달된 FormData 객체
|
||||
* @returns AuthFormData - 추출된 이메일과 비밀번호
|
||||
* FormData 추출 헬퍼 (이메일/비밀번호)
|
||||
* @param formData HTML form 데이터
|
||||
* @returns 이메일(trim 적용), 비밀번호
|
||||
*/
|
||||
function extractAuthData(formData: FormData): AuthFormData {
|
||||
const email = (formData.get("email") as string)?.trim() || "";
|
||||
@@ -34,22 +39,13 @@ function extractAuthData(formData: FormData): AuthFormData {
|
||||
}
|
||||
|
||||
/**
|
||||
* [비밀번호 강도 검증 함수]
|
||||
*
|
||||
* 안전한 비밀번호인지 검사합니다.
|
||||
*
|
||||
* 비밀번호 정책:
|
||||
* - 최소 8자 이상
|
||||
* - 대문자 1개 이상 포함
|
||||
* - 소문자 1개 이상 포함
|
||||
* - 숫자 1개 이상 포함
|
||||
* - 특수문자 1개 이상 포함 (!@#$%^&*(),.?":{}|<> 등)
|
||||
*
|
||||
* @param password - 검증할 비밀번호
|
||||
* @returns AuthError | null - 에러가 있으면 에러 객체, 없으면 null
|
||||
* 비밀번호 강도 검증 함수
|
||||
* @param password 검증할 비밀번호
|
||||
* @returns 에러 객체 또는 null
|
||||
* @remarks 최소 8자, 대/소문자, 숫자, 특수문자 포함 필수
|
||||
*/
|
||||
function validatePassword(password: string): AuthError | null {
|
||||
// 1. 최소 길이 체크 (8자 이상)
|
||||
// [Step 1] 최소 길이 체크 (8자 이상)
|
||||
if (password.length < 8) {
|
||||
return {
|
||||
message: AUTH_ERROR_MESSAGES.PASSWORD_TOO_SHORT,
|
||||
@@ -57,7 +53,7 @@ function validatePassword(password: string): AuthError | null {
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 대문자 포함 여부
|
||||
// [Step 2] 대문자 포함 여부
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
return {
|
||||
message: AUTH_ERROR_MESSAGES.PASSWORD_TOO_WEAK,
|
||||
@@ -65,7 +61,7 @@ function validatePassword(password: string): AuthError | null {
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 소문자 포함 여부
|
||||
// [Step 3] 소문자 포함 여부
|
||||
if (!/[a-z]/.test(password)) {
|
||||
return {
|
||||
message: AUTH_ERROR_MESSAGES.PASSWORD_TOO_WEAK,
|
||||
@@ -73,7 +69,7 @@ function validatePassword(password: string): AuthError | null {
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 숫자 포함 여부
|
||||
// [Step 4] 숫자 포함 여부
|
||||
if (!/[0-9]/.test(password)) {
|
||||
return {
|
||||
message: AUTH_ERROR_MESSAGES.PASSWORD_TOO_WEAK,
|
||||
@@ -81,7 +77,7 @@ function validatePassword(password: string): AuthError | null {
|
||||
};
|
||||
}
|
||||
|
||||
// 5. 특수문자 포함 여부
|
||||
// [Step 5] 특수문자 포함 여부
|
||||
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
||||
return {
|
||||
message: AUTH_ERROR_MESSAGES.PASSWORD_TOO_WEAK,
|
||||
@@ -89,26 +85,20 @@ function validatePassword(password: string): AuthError | null {
|
||||
};
|
||||
}
|
||||
|
||||
// 모든 검증 통과
|
||||
// [Step 6] 모든 검증 통과
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* [입력값 검증 함수]
|
||||
*
|
||||
* 사용자가 입력한 이메일과 비밀번호의 유효성을 검사합니다.
|
||||
*
|
||||
* 검증 항목:
|
||||
* 1. 빈 값 체크 - 이메일 또는 비밀번호가 비어있는지 확인
|
||||
* 2. 이메일 형식 - '@' 포함 여부로 간단한 형식 검증
|
||||
* 3. 비밀번호 강도 - 8자 이상, 대소문자/숫자/특수문자 포함 확인
|
||||
*
|
||||
* @param email - 사용자 이메일
|
||||
* @param password - 사용자 비밀번호
|
||||
* @returns AuthError | null - 에러가 있으면 에러 객체, 없으면 null
|
||||
* 입력값 유효성 검증 함수
|
||||
* @param email 사용자 이메일
|
||||
* @param password 사용자 비밀번호
|
||||
* @returns 에러 객체 또는 null
|
||||
* @see login - 로그인 액션에서 호출
|
||||
* @see signup - 회원가입 액션에서 호출
|
||||
*/
|
||||
function validateAuthInput(email: string, password: string): AuthError | null {
|
||||
// 1. 빈 값 체크
|
||||
// [Step 1] 빈 값 체크
|
||||
if (!email || !password) {
|
||||
return {
|
||||
message: AUTH_ERROR_MESSAGES.EMPTY_FIELDS,
|
||||
@@ -116,7 +106,7 @@ function validateAuthInput(email: string, password: string): AuthError | null {
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 이메일 형식 체크 (간단한 @ 포함 여부 확인)
|
||||
// [Step 2] 이메일 형식 체크 (간단한 @ 포함 여부 확인)
|
||||
if (!email.includes("@")) {
|
||||
return {
|
||||
message: AUTH_ERROR_MESSAGES.INVALID_EMAIL,
|
||||
@@ -124,36 +114,19 @@ function validateAuthInput(email: string, password: string): AuthError | null {
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 비밀번호 강도 체크
|
||||
// [Step 3] 비밀번호 강도 체크
|
||||
const passwordValidation = validatePassword(password);
|
||||
if (passwordValidation) {
|
||||
return passwordValidation;
|
||||
}
|
||||
|
||||
// 모든 검증 통과
|
||||
// [Step 4] 검증 통과
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* [에러 메시지 번역 헬퍼]
|
||||
*
|
||||
* Supabase의 영문 에러 메시지를 사용자 친화적인 한글로 변환합니다.
|
||||
*
|
||||
* @param error - Supabase에서 받은 에러 메시지
|
||||
* @returns string - 한글로 번역된 에러 메시지
|
||||
*/
|
||||
// 에러 메시지는 getAuthErrorMessage로 통일합니다.
|
||||
|
||||
// ========================================
|
||||
// Server Actions (서버 액션)
|
||||
// ========================================
|
||||
// 흐름 요약 (어디서 호출되는지)
|
||||
// - /login: features/auth/components/login-form.tsx -> login, signInWithGoogle, signInWithKakao
|
||||
// - /signup: features/auth/components/signup-form.tsx -> signup
|
||||
// - /forgot-password: app/forgot-password/page.tsx -> requestPasswordReset
|
||||
// - /reset-password: features/auth/components/reset-password-form.tsx -> updatePassword
|
||||
// - 메인(/): app/page.tsx -> signout
|
||||
// - OAuth: signInWithGoogle/Kakao -> Supabase -> /auth/callback 라우트
|
||||
|
||||
/**
|
||||
* [로그인 액션]
|
||||
@@ -164,17 +137,17 @@ function validateAuthInput(email: string, password: string): AuthError | null {
|
||||
* 1. FormData에서 이메일/비밀번호 추출
|
||||
* 2. 입력값 유효성 검증 (빈 값, 이메일 형식, 비밀번호 길이)
|
||||
* 3. Supabase Auth를 통한 로그인 시도
|
||||
* 4. 성공 시 메인 페이지로 리다이렉트
|
||||
* 5. 실패 시 에러 메시지와 함께 로그인 페이지로 리다이렉트
|
||||
* 4. 로그인 실패 시 에러 메시지와 함께 로그인 페이지로 리다이렉트
|
||||
* 5. 로그인 성공 시 캐시 무효화 및 메인 페이지로 리다이렉트
|
||||
*
|
||||
* @param formData - HTML form에서 전달된 FormData (이메일, 비밀번호 포함)
|
||||
* @param formData 이메일, 비밀번호가 포함된 FormData
|
||||
* @see login-form.tsx - 로그인 폼 제출 시 호출
|
||||
*/
|
||||
export async function login(formData: FormData) {
|
||||
// 호출: features/auth/components/login-form.tsx (로그인 폼 action)
|
||||
// 1. FormData에서 이메일/비밀번호 추출
|
||||
// [Step 1] FormData에서 이메일/비밀번호 추출
|
||||
const { email, password } = extractAuthData(formData);
|
||||
|
||||
// 2. 입력값 유효성 검증
|
||||
// [Step 2] 입력값 유효성 검증
|
||||
const validationError = validateAuthInput(email, password);
|
||||
if (validationError) {
|
||||
return redirect(
|
||||
@@ -182,21 +155,20 @@ export async function login(formData: FormData) {
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Supabase 클라이언트 생성 및 로그인 시도
|
||||
// [Step 3] Supabase 클라이언트 생성 및 로그인 시도
|
||||
const supabase = await createClient();
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
// 4. 로그인 실패 시 에러 처리
|
||||
// [Step 4] 로그인 실패 시 에러 처리
|
||||
if (error) {
|
||||
const message = getAuthErrorMessage(error);
|
||||
return redirect(`/login?message=${encodeURIComponent(message)}`);
|
||||
}
|
||||
|
||||
// 5. 로그인 성공 - 캐시 무효화 및 메인 페이지로 리다이렉트
|
||||
// revalidatePath: Next.js 캐시를 무효화하여 최신 인증 상태 반영
|
||||
// [Step 5] 로그인 성공 - 캐시 무효화 및 메인 페이지로 리다이렉트
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/");
|
||||
}
|
||||
@@ -214,14 +186,14 @@ export async function login(formData: FormData) {
|
||||
* 5-1. 즉시 세션 생성 시: 메인 페이지로 리다이렉트 (바로 로그인됨)
|
||||
* 5-2. 이메일 인증 필요 시: 로그인 페이지로 리다이렉트 (안내 메시지 포함)
|
||||
*
|
||||
* @param formData - HTML form에서 전달된 FormData (이메일, 비밀번호 포함)
|
||||
* @param formData 이메일, 비밀번호가 포함된 FormData
|
||||
* @see signup-form.tsx - 회원가입 폼 제출 시 호출
|
||||
*/
|
||||
export async function signup(formData: FormData) {
|
||||
// 호출: features/auth/components/signup-form.tsx (회원가입 폼 action)
|
||||
// 1. FormData에서 이메일/비밀번호 추출
|
||||
// [Step 1] FormData에서 이메일/비밀번호 추출
|
||||
const { email, password } = extractAuthData(formData);
|
||||
|
||||
// 2. 입력값 유효성 검증
|
||||
// [Step 2] 입력값 유효성 검증
|
||||
const validationError = validateAuthInput(email, password);
|
||||
if (validationError) {
|
||||
return redirect(
|
||||
@@ -229,35 +201,31 @@ export async function signup(formData: FormData) {
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Supabase 클라이언트 생성 및 회원가입 시도
|
||||
// [Step 3] Supabase 클라이언트 생성 및 회원가입 시도
|
||||
const supabase = await createClient();
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
// 이메일 인증 완료 후 리다이렉트될 URL
|
||||
// 로컬 개발 환경: http://localhost:3001/auth/callback
|
||||
// 프로덕션: NEXT_PUBLIC_BASE_URL 환경 변수에 설정된 주소
|
||||
emailRedirectTo: `${process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3001"}/auth/callback?auth_type=signup`,
|
||||
},
|
||||
});
|
||||
|
||||
// 4. 회원가입 실패 시 에러 처리
|
||||
// [Step 4] 회원가입 실패 시 에러 처리
|
||||
if (error) {
|
||||
const message = getAuthErrorMessage(error);
|
||||
return redirect(`/signup?message=${encodeURIComponent(message)}`);
|
||||
}
|
||||
|
||||
// 5. 회원가입 성공 - 세션 생성 여부에 따라 분기 처리
|
||||
// [Step 5] 회원가입 성공 처리
|
||||
if (data.session) {
|
||||
// 5-1. 즉시 세션이 생성된 경우 (이메일 인증 불필요)
|
||||
// → 바로 메인 페이지로 이동
|
||||
// [Case 1] 즉시 세션 생성됨 (이메일 인증 불필요)
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
// 5-2. 이메일 인증이 필요한 경우
|
||||
// → 로그인 페이지로 이동 + 안내 메시지
|
||||
// [Case 2] 이메일 인증 필요 (로그인 페이지로 이동)
|
||||
revalidatePath("/", "layout");
|
||||
redirect(
|
||||
`/login?message=${encodeURIComponent("회원가입이 완료되었습니다. 이메일을 확인하여 인증을 완료해 주세요.")}`,
|
||||
@@ -270,21 +238,23 @@ export async function signup(formData: FormData) {
|
||||
* 현재 세션을 종료하고 로그인 페이지로 이동합니다.
|
||||
*
|
||||
* 처리 과정:
|
||||
* 1. Supabase Auth 세션 종료
|
||||
* 2. 캐시 무효화하여 인증 상태 갱신
|
||||
* 1. Supabase Auth 세션 종료 (서버 + 클라이언트 쿠키 삭제)
|
||||
* 2. Next.js 캐시 무효화하여 인증 상태 갱신
|
||||
* 3. 로그인 페이지로 리다이렉트
|
||||
*
|
||||
* @see user-menu.tsx - 로그아웃 메뉴 클릭 시 호출
|
||||
* @see session-manager.tsx - 세션 타임아웃 시 호출
|
||||
*/
|
||||
export async function signout() {
|
||||
// 호출: app/page.tsx (로그아웃 버튼의 formAction)
|
||||
const supabase = await createClient();
|
||||
|
||||
// 1. Supabase 세션 종료 (서버 + 클라이언트 쿠키 삭제)
|
||||
// [Step 1] Supabase 세션 종료 (서버 + 클라이언트 쿠키 삭제)
|
||||
await supabase.auth.signOut();
|
||||
|
||||
// 2. Next.js 캐시 무효화
|
||||
// [Step 2] Next.js 캐시 무효화
|
||||
revalidatePath("/", "layout");
|
||||
|
||||
// 3. 로그인 페이지로 리다이렉트
|
||||
// [Step 3] 로그인 페이지로 리다이렉트
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
@@ -292,10 +262,7 @@ export async function signout() {
|
||||
* [비밀번호 재설정 요청 액션]
|
||||
*
|
||||
* 사용자 이메일로 비밀번호 재설정 링크를 발송합니다.
|
||||
*
|
||||
* 보안 고려사항:
|
||||
* - 이메일 존재 여부와 관계없이 동일한 성공 메시지 표시
|
||||
* - 이메일 열거 공격(Email Enumeration) 방지
|
||||
* 보안을 위해 이메일 존재 여부와 관계없이 동일한 메시지를 표시합니다.
|
||||
*
|
||||
* 처리 과정:
|
||||
* 1. FormData에서 이메일 추출
|
||||
@@ -303,14 +270,14 @@ export async function signout() {
|
||||
* 3. Supabase를 통한 재설정 링크 발송
|
||||
* 4. 성공 메시지와 함께 로그인 페이지로 리다이렉트
|
||||
*
|
||||
* @param formData - 이메일이 포함된 FormData
|
||||
* @param formData 이메일 포함
|
||||
* @see forgot-password/page.tsx - 비밀번호 찾기 폼 제출 시 호출
|
||||
*/
|
||||
export async function requestPasswordReset(formData: FormData) {
|
||||
// 호출: app/forgot-password/page.tsx (비밀번호 재설정 요청 폼)
|
||||
// 1. FormData에서 이메일 추출
|
||||
// [Step 1] FormData에서 이메일 추출
|
||||
const email = (formData.get("email") as string)?.trim() || "";
|
||||
|
||||
// 2. 이메일 검증
|
||||
// [Step 2] 이메일 유효성 검증
|
||||
if (!email) {
|
||||
return redirect(
|
||||
`/forgot-password?message=${encodeURIComponent(AUTH_ERROR_MESSAGES.EMPTY_EMAIL)}`,
|
||||
@@ -323,20 +290,20 @@ export async function requestPasswordReset(formData: FormData) {
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Supabase를 통한 재설정 링크 발송
|
||||
// [Step 3] Supabase를 통한 재설정 링크 발송
|
||||
const supabase = await createClient();
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3001"}/reset-password`,
|
||||
});
|
||||
|
||||
// 4. 에러 처리
|
||||
// [Step 4] 에러 처리
|
||||
if (error) {
|
||||
console.error("Password reset error:", error.message);
|
||||
const message = getAuthErrorMessage(error);
|
||||
return redirect(`/forgot-password?message=${encodeURIComponent(message)}`);
|
||||
}
|
||||
|
||||
// 5. 성공 메시지 표시
|
||||
// [Step 5] 성공 메시지 표시 (보안상 항상 성공 메시지 리턴 권장)
|
||||
redirect(
|
||||
`/login?message=${encodeURIComponent(AUTH_ERROR_MESSAGES.PASSWORD_RESET_SENT)}`,
|
||||
);
|
||||
@@ -349,36 +316,44 @@ export async function requestPasswordReset(formData: FormData) {
|
||||
*
|
||||
* 처리 과정:
|
||||
* 1. FormData에서 새 비밀번호 추출
|
||||
* 2. 비밀번호 길이 검증
|
||||
* 2. 비밀번호 길이 및 강도 검증
|
||||
* 3. Supabase를 통한 비밀번호 업데이트
|
||||
* 4. 성공 시 로그인 페이지로 리다이렉트
|
||||
* 4. 실패 시 에러 메시지 반환
|
||||
* 5. 성공 시 세션/쿠키 정리 후 로그아웃 및 캐시 무효화
|
||||
* 6. 성공 결과 반환
|
||||
*
|
||||
* @param formData - 새 비밀번호가 포함된 FormData
|
||||
* @param formData 새 비밀번호 포함
|
||||
* @see reset-password-form.tsx - 비밀번호 재설정 폼 제출 시 호출
|
||||
*/
|
||||
export async function updatePassword(formData: FormData) {
|
||||
// 호출: features/auth/components/reset-password-form.tsx (재설정 폼)
|
||||
// [Step 1] 새 비밀번호 추출
|
||||
const password = (formData.get("password") as string) || "";
|
||||
|
||||
// [Step 2] 비밀번호 강도 검증
|
||||
const passwordValidation = validatePassword(password);
|
||||
if (passwordValidation) {
|
||||
return { ok: false, message: passwordValidation.message };
|
||||
}
|
||||
|
||||
// [Step 3] Supabase를 통한 비밀번호 업데이트
|
||||
const supabase = await createClient();
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password: password,
|
||||
});
|
||||
|
||||
// [Step 4] 에러 처리
|
||||
if (error) {
|
||||
const message = getAuthErrorMessage(error);
|
||||
return { ok: false, message };
|
||||
}
|
||||
|
||||
// [Step 5] 세션 및 쿠키 정리 후 로그아웃
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete(RECOVERY_COOKIE_NAME);
|
||||
await supabase.auth.signOut();
|
||||
revalidatePath("/", "layout");
|
||||
|
||||
// [Step 6] 성공 응답 반환
|
||||
return {
|
||||
ok: true,
|
||||
message: AUTH_ERROR_MESSAGES.PASSWORD_RESET_SUCCESS,
|
||||
@@ -390,53 +365,47 @@ export async function updatePassword(formData: FormData) {
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* [OAuth 로그인 헬퍼 함수]
|
||||
* [OAuth 로그인 공통 헬퍼]
|
||||
*
|
||||
* Google, Kakao 등의 소셜 로그인 제공자를 통해 인증을 수행합니다.
|
||||
* 처리 과정:
|
||||
* 1. Supabase OAuth 로그인 URL 생성 (PKCE)
|
||||
* 2. 생성 중 에러 발생 시 로그인 페이지로 리다이렉트 (에러 메시지 포함)
|
||||
* 3. 성공 시 해당 OAuth 제공자 페이지(data.url)로 리다이렉트
|
||||
*
|
||||
* PKCE (Proof Key for Code Exchange) 플로우:
|
||||
* 1. 이 함수가 signInWithOAuth를 호출하면 Supabase가 OAuth URL 반환
|
||||
* 2. 사용자를 해당 OAuth 제공자(Google/Kakao) 로그인 페이지로 리다이렉트
|
||||
* 3. 사용자가 로그인 및 권한 동의
|
||||
* 4. OAuth 제공자가 /auth/callback?code=xxx로 리다이렉트
|
||||
* 5. callback 라우트에서 code를 세션으로 교환 (exchangeCodeForSession)
|
||||
* 6. 메인 페이지로 최종 리다이렉트
|
||||
*
|
||||
* 자동 회원가입:
|
||||
* - 첫 로그인 시 auth.users 테이블에 자동으로 사용자 생성
|
||||
* - 이메일, provider, metadata(프로필 사진, 이름 등) 저장
|
||||
* - 기존 사용자는 그냥 로그인
|
||||
*
|
||||
* @param provider - OAuth 제공자 ('google' | 'kakao')
|
||||
* @param provider 'google' | 'kakao'
|
||||
* @param extraOptions 추가 옵션 (예: prompt)
|
||||
* @see signInWithGoogle
|
||||
* @see signInWithKakao
|
||||
*/
|
||||
async function signInWithProvider(provider: "google" | "kakao") {
|
||||
// 호출: 아래 signInWithGoogle / signInWithKakao에서 공통 사용
|
||||
async function signInWithProvider(
|
||||
provider: "google" | "kakao",
|
||||
extraOptions: { queryParams?: { [key: string]: string } } = {},
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
// 1. OAuth 인증 시작
|
||||
// [Step 1] OAuth 인증 시작 (URL 생성)
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
options: {
|
||||
// PKCE 플로우를 위한 콜백 URL
|
||||
// OAuth 제공자 인증 후 이 URL로 code와 함께 리다이렉트됨
|
||||
redirectTo: `${process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3001"}/auth/callback`,
|
||||
...extraOptions,
|
||||
},
|
||||
});
|
||||
|
||||
// 2. 에러 처리
|
||||
// [Step 2] 에러 처리
|
||||
if (error) {
|
||||
console.error(`[${provider} OAuth] 로그인 실패:`, error.message);
|
||||
const message = getAuthErrorMessage(error);
|
||||
return redirect(`/login?message=${encodeURIComponent(message)}`);
|
||||
}
|
||||
|
||||
// 3. OAuth 제공자 로그인 페이지로 리다이렉트
|
||||
// data.url은 Google/Kakao의 인증 페이지 URL
|
||||
// [Step 3] OAuth 제공자 로그인 페이지로 리다이렉트
|
||||
if (data.url) {
|
||||
redirect(data.url);
|
||||
}
|
||||
|
||||
// 4. URL이 없는 경우 (예상치 못한 상황)
|
||||
// [Step 4] URL 생성 실패 시 에러 처리
|
||||
redirect(
|
||||
`/login?message=${encodeURIComponent("로그인 처리 중 오류가 발생했습니다.")}`,
|
||||
);
|
||||
@@ -444,36 +413,16 @@ async function signInWithProvider(provider: "google" | "kakao") {
|
||||
|
||||
/**
|
||||
* [Google 로그인 액션]
|
||||
*
|
||||
* Google 계정으로 로그인합니다.
|
||||
* "Google로 로그인" 버튼에서 호출됩니다.
|
||||
*
|
||||
* 처리 과정:
|
||||
* 1. signInWithOAuth 호출하여 Google OAuth URL 받기
|
||||
* 2. Google 로그인 페이지로 리다이렉트
|
||||
* 3. (사용자가 Google에서 로그인 및 권한 동의)
|
||||
* 4. /auth/callback으로 돌아와서 세션 생성
|
||||
* 5. 메인 페이지로 이동
|
||||
* @see login-form.tsx - 구글 로그인 버튼 클릭 시 호출
|
||||
*/
|
||||
export async function signInWithGoogle() {
|
||||
// 호출: features/auth/components/login-form.tsx (Google 로그인 버튼)
|
||||
return signInWithProvider("google");
|
||||
}
|
||||
|
||||
/**
|
||||
* [Kakao 로그인 액션]
|
||||
*
|
||||
* Kakao 계정으로 로그인합니다.
|
||||
* "Kakao로 로그인" 버튼에서 호출됩니다.
|
||||
*
|
||||
* 처리 과정:
|
||||
* 1. signInWithOAuth 호출하여 Kakao OAuth URL 받기
|
||||
* 2. Kakao 로그인 페이지로 리다이렉트
|
||||
* 3. (사용자가 Kakao에서 로그인 및 권한 동의)
|
||||
* 4. /auth/callback으로 돌아와서 세션 생성
|
||||
* 5. 메인 페이지로 이동
|
||||
* @see login-form.tsx - 카카오 로그인 버튼 클릭 시 호출
|
||||
*/
|
||||
export async function signInWithKakao() {
|
||||
// 호출: features/auth/components/login-form.tsx (Kakao 로그인 버튼)
|
||||
return signInWithProvider("kakao");
|
||||
return signInWithProvider("kakao", { queryParams: { prompt: "login" } });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user