2 Commits

Author SHA1 Message Date
2d34d70948 Fix: 회원가입 인증 리다이렉트 처리와 UI 그라디언트 클래스 수정
app/auth/callback/route.ts
- 이메일 인증 완료 판별을 세션 생성 시간 기준에서 쿼리 파라미터(auth_type=signup) 기반으로 변경
- 회원가입 인증인 경우 자동 로그인 세션 종료 후 로그인 페이지로 리다이렉트 처리

features/auth/actions.ts
- 회원가입 시 이메일 리다이렉트 URL에 auth_type=signup 쿼리 파라미터 추가

app/forgot-password/page.tsx
- 배경 및 카드 아이콘의 Tailwind 클래스명 일부 수정(bg-gradient-to→bg-linear-to, radial-gradient var() 구문 정리)

app/login/page.tsx
- 배경 및 카드 아이콘의 Tailwind 클래스명 일부 정리 및 일관화

app/reset-password/page.tsx
- 배경 및 카드 아이콘의 Tailwind 클래스명 일부 정리 및 일관화

app/signup/page.tsx
- 배경 및 카드 아이콘의 Tailwind 클래스명 일부 정리 및 일관화

features/auth/components/reset-password-form.tsx
- 버튼 그라디언트 클래스명(bg-gradient-to-r→bg-linear-to-r) 수정
2026-02-05 15:39:44 +09:00
9c967af9c1 Feat: 회원가입 직후 이메일 인증 완료 처리 추가
app/auth/callback/route.ts
- OAuth 콜백 처리 시 신규 사용자(생성 시간 1분 이내)를 감지하면 자동 로그인 세션을 종료하고 로그인 페이지로 리다이렉트하며 이메일 인증 완료 메시지를 전달하도록 로직 추가
- 일반 로그인은 기존처럼 코드 파라미터 제거 후 깔끔한 URL로 리다이렉트하도록 유지

features/auth/constants.ts
- 이메일 인증 성공 메시지 상수 추가
- 일부 문자열 포맷팅 정리
2026-02-05 13:19:53 +09:00
8 changed files with 44 additions and 22 deletions

View File

@@ -62,8 +62,27 @@ export async function GET(request: NextRequest) {
if (!exchangeError) {
// ----------------------------------------------------------------------
// 3-1. 교환 성공: 리다이렉트 처리
// 코드 파라미터 등을 제거하고 깨끗한 URL로 이동합니다.
// code 교환으로 세션이 생성된 상태입니다.
// ----------------------------------------------------------------------
// 회원가입 인증 여부 확인 (쿼리 파라미터 기반)
// actions.ts의 signup 함수에서 emailRedirectTo에 auth_type=signup을 추가해서 보냅니다.
const authType = requestUrl.searchParams.get("auth_type");
const isSignupVerification = authType === "signup";
// 회원가입 인증인 경우:
// 이메일 인증만 완료하고, 자동 로그인된 세션은 종료시킨 뒤 로그인 페이지로 보냅니다.
if (isSignupVerification) {
await supabase.auth.signOut();
return NextResponse.redirect(
`${origin}${AUTH_ROUTES.LOGIN}?message=${encodeURIComponent(
AUTH_ERROR_MESSAGES.EMAIL_VERIFIED_SUCCESS,
)}`,
);
}
// 그 외 일반적인 로그인/인증인 경우:
// 코드 파라미터 등을 제거하고 깨끗한 URL로 이동합니다.
const forwardedHost = request.headers.get("x-forwarded-host"); // 로드밸런서 지원
const isLocalEnv = process.env.NODE_ENV === "development";

View File

@@ -27,9 +27,9 @@ export default async function ForgotPasswordPage({
const { message } = await searchParams;
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-gradient-to-br from-gray-50 via-white to-gray-100 px-4 py-12 dark:from-black dark:via-gray-950 dark:to-gray-900">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-gray-200/30 via-gray-100/15 to-transparent dark:from-gray-800/30 dark:via-gray-900/20" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,_var(--tw-gradient-stops))] from-gray-300/30 via-gray-200/15 to-transparent dark:from-gray-700/30 dark:via-gray-800/20" />
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-linear-to-br from-gray-50 via-white to-gray-100 px-4 py-12 dark:from-black dark:via-gray-950 dark:to-gray-900">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,var(--tw-gradient-stops))] from-gray-200/30 via-gray-100/15 to-transparent dark:from-gray-800/30 dark:via-gray-900/20" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,var(--tw-gradient-stops))] from-gray-300/30 via-gray-200/15 to-transparent dark:from-gray-700/30 dark:via-gray-800/20" />
<div className="absolute left-1/4 top-1/4 h-64 w-64 animate-pulse rounded-full bg-gray-300/20 blur-3xl dark:bg-gray-700/20" />
<div className="absolute bottom-1/4 right-1/4 h-64 w-64 animate-pulse rounded-full bg-gray-400/20 blur-3xl delay-700 dark:bg-gray-600/20" />
@@ -39,7 +39,7 @@ export default async function ForgotPasswordPage({
<Card className="border-white/20 bg-white/70 shadow-2xl backdrop-blur-xl dark:border-white/10 dark:bg-gray-900/70">
<CardHeader className="space-y-3 text-center">
<div className="mx-auto mb-2 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-gray-800 to-black shadow-lg dark:from-white dark:to-gray-200">
<div className="mx-auto mb-2 flex h-16 w-16 items-center justify-center rounded-2xl bg-linear-to-br from-gray-800 to-black shadow-lg dark:from-white dark:to-gray-200">
<span className="text-sm font-semibold">MAIL</span>
</div>
<CardTitle className="text-3xl font-bold tracking-tight">
@@ -72,7 +72,7 @@ export default async function ForgotPasswordPage({
<Button
formAction={requestPasswordReset}
className="h-11 w-full bg-gradient-to-r from-gray-900 to-black font-semibold text-white shadow-lg transition-all hover:from-black hover:to-gray-800 hover:shadow-xl dark:from-white dark:to-gray-100 dark:text-black dark:hover:from-gray-100 dark:hover:to-white"
className="h-11 w-full bg-linear-to-r from-gray-900 to-black font-semibold text-white shadow-lg transition-all hover:from-black hover:to-gray-800 hover:shadow-xl dark:from-white dark:to-gray-100 dark:text-black dark:hover:from-gray-100 dark:hover:to-white"
>
</Button>

View File

@@ -27,17 +27,17 @@ export default async function LoginPage({
const { message } = await searchParams;
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-gradient-to-br from-gray-50 via-white to-gray-100 px-4 py-12 dark:from-black dark:via-gray-950 dark:to-gray-900">
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-linear-to-br from-gray-50 via-white to-gray-100 px-4 py-12 dark:from-black dark:via-gray-950 dark:to-gray-900">
{/* ========== 배경 그라디언트 레이어 ========== */}
{/* 웹 페이지 전체 배경을 그라디언트로 채웁니다 */}
{/* 라이트 모드: 부드러운 그레이 톤 (gray → white → gray) */}
{/* 다크 모드: 깊은 블랙 톤으로 고급스러운 느낌 */}
{/* 추가 그라디언트 효과 1: 우상단에서 시작하는 원형 그라디언트 */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-gray-200/30 via-gray-100/15 to-transparent dark:from-gray-800/30 dark:via-gray-900/20" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,var(--tw-gradient-stops))] from-gray-200/30 via-gray-100/15 to-transparent dark:from-gray-800/30 dark:via-gray-900/20" />
{/* 추가 그라디언트 효과 2: 좌하단에서 시작하는 원형 그라디언트 */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,_var(--tw-gradient-stops))] from-gray-300/30 via-gray-200/15 to-transparent dark:from-gray-700/30 dark:via-gray-800/20" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,var(--tw-gradient-stops))] from-gray-300/30 via-gray-200/15 to-transparent dark:from-gray-700/30 dark:via-gray-800/20" />
{/* ========== 애니메이션 블러 효과 ========== */}
{/* 부드럽게 깜빡이는 원형 블러로 생동감 표현 */}
@@ -61,7 +61,7 @@ export default async function LoginPage({
{/* ========== 카드 헤더 영역 ========== */}
<CardHeader className="space-y-3 text-center">
{/* 아이콘 배경: 그라디언트 원형 */}
<div className="mx-auto mb-2 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-gray-800 to-black shadow-lg dark:from-white dark:to-gray-200">
<div className="mx-auto mb-2 flex h-16 w-16 items-center justify-center rounded-2xl bg-linear-to-br from-gray-800 to-black shadow-lg dark:from-white dark:to-gray-200">
<span className="text-4xl">👋</span>
</div>
{/* 페이지 제목 */}

View File

@@ -39,9 +39,9 @@ export default async function ResetPasswordPage({
const { message } = params;
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-gradient-to-br from-gray-50 via-white to-gray-100 px-4 py-12 dark:from-black dark:via-gray-950 dark:to-gray-900">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-gray-200/30 via-gray-100/15 to-transparent dark:from-gray-800/30 dark:via-gray-900/20" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,_var(--tw-gradient-stops))] from-gray-300/30 via-gray-200/15 to-transparent dark:from-gray-700/30 dark:via-gray-800/20" />
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-linear-to-br from-gray-50 via-white to-gray-100 px-4 py-12 dark:from-black dark:via-gray-950 dark:to-gray-900">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,var(--tw-gradient-stops))] from-gray-200/30 via-gray-100/15 to-transparent dark:from-gray-800/30 dark:via-gray-900/20" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,var(--tw-gradient-stops))] from-gray-300/30 via-gray-200/15 to-transparent dark:from-gray-700/30 dark:via-gray-800/20" />
<div className="absolute left-1/4 top-1/4 h-64 w-64 animate-pulse rounded-full bg-gray-300/20 blur-3xl dark:bg-gray-700/20" />
<div className="absolute bottom-1/4 right-1/4 h-64 w-64 animate-pulse rounded-full bg-gray-400/20 blur-3xl delay-700 dark:bg-gray-600/20" />
@@ -51,7 +51,7 @@ export default async function ResetPasswordPage({
<Card className="border-white/20 bg-white/70 shadow-2xl backdrop-blur-xl dark:border-white/10 dark:bg-gray-900/70">
<CardHeader className="space-y-3 text-center">
<div className="mx-auto mb-2 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-gray-800 to-black shadow-lg dark:from-white dark:to-gray-200">
<div className="mx-auto mb-2 flex h-16 w-16 items-center justify-center rounded-2xl bg-linear-to-br from-gray-800 to-black shadow-lg dark:from-white dark:to-gray-200">
<span className="text-sm font-semibold">PW</span>
</div>
<CardTitle className="text-3xl font-bold tracking-tight">

View File

@@ -17,10 +17,10 @@ export default async function SignupPage({
const { message } = await searchParams;
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-gradient-to-br from-gray-50 via-white to-gray-100 px-4 py-12 dark:from-black dark:via-gray-950 dark:to-gray-900">
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-linear-to-br from-gray-50 via-white to-gray-100 px-4 py-12 dark:from-black dark:via-gray-950 dark:to-gray-900">
{/* 배경 그라데이션 효과 */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-gray-200/30 via-gray-100/15 to-transparent dark:from-gray-800/30 dark:via-gray-900/20" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,_var(--tw-gradient-stops))] from-gray-300/30 via-gray-200/15 to-transparent dark:from-gray-700/30 dark:via-gray-800/20" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,var(--tw-gradient-stops))] from-gray-200/30 via-gray-100/15 to-transparent dark:from-gray-800/30 dark:via-gray-900/20" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,var(--tw-gradient-stops))] from-gray-300/30 via-gray-200/15 to-transparent dark:from-gray-700/30 dark:via-gray-800/20" />
{/* 애니메이션 블러 효과 */}
<div className="absolute left-1/4 top-1/4 h-64 w-64 animate-pulse rounded-full bg-gray-300/20 blur-3xl dark:bg-gray-700/20" />
@@ -32,7 +32,7 @@ export default async function SignupPage({
<Card className="border-white/20 bg-white/70 shadow-2xl backdrop-blur-xl dark:border-white/10 dark:bg-gray-900/70">
<CardHeader className="space-y-3 text-center">
<div className="mx-auto mb-2 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-gray-800 to-black shadow-lg dark:from-white dark:to-gray-200">
<div className="mx-auto mb-2 flex h-16 w-16 items-center justify-center rounded-2xl bg-linear-to-br from-gray-800 to-black shadow-lg dark:from-white dark:to-gray-200">
<span className="text-4xl">🚀</span>
</div>
<CardTitle className="text-3xl font-bold tracking-tight">

View File

@@ -238,7 +238,7 @@ export async function signup(formData: FormData) {
// 이메일 인증 완료 후 리다이렉트될 URL
// 로컬 개발 환경: http://localhost:3001/auth/callback
// 프로덕션: NEXT_PUBLIC_BASE_URL 환경 변수에 설정된 주소
emailRedirectTo: `${process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3001"}/auth/callback`,
emailRedirectTo: `${process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3001"}/auth/callback?auth_type=signup`,
},
});

View File

@@ -128,7 +128,7 @@ export default function ResetPasswordForm() {
<Button
type="submit"
disabled={isLoading}
className="h-11 w-full bg-gradient-to-r from-gray-900 to-black font-semibold text-white shadow-lg transition-all hover:from-black hover:to-gray-800 hover:shadow-xl dark:from-white dark:to-gray-100 dark:text-black dark:hover:from-gray-100 dark:hover:to-white"
className="h-11 w-full bg-linear-to-r from-gray-900 to-black font-semibold text-white shadow-lg transition-all hover:from-black hover:to-gray-800 hover:shadow-xl dark:from-white dark:to-gray-100 dark:text-black dark:hover:from-gray-100 dark:hover:to-white"
>
{isLoading ? (
<span className="flex items-center gap-2">

View File

@@ -40,6 +40,7 @@ export const AUTH_ERROR_MESSAGES = {
// === 인증 링크 ===
INVALID_AUTH_LINK: "인증 링크가 만료되었거나 유효하지 않습니다.",
EMAIL_VERIFIED_SUCCESS: "이메일 인증이 완료되었습니다. 로그인해 주세요.",
// === 소셜 로그인 (OAuth) 관련 ===
OAUTH_ACCESS_DENIED: "로그인이 취소되었습니다.",
@@ -106,8 +107,10 @@ export const AUTH_ERROR_CODE_MESSAGES = {
mfa_verification_failed: "MFA 검증에 실패했습니다.",
mfa_verification_rejected: "MFA 검증이 거부되었습니다.",
mfa_verified_factor_exists: "이미 검증된 전화 MFA가 존재합니다.",
mfa_web_authn_enroll_not_enabled: "WebAuthn MFA 등록이 비활성화되어 있습니다.",
mfa_web_authn_verify_not_enabled: "WebAuthn MFA 검증이 비활성화되어 있습니다.",
mfa_web_authn_enroll_not_enabled:
"WebAuthn MFA 등록이 비활성화되어 있습니다.",
mfa_web_authn_verify_not_enabled:
"WebAuthn MFA 검증이 비활성화되어 있습니다.",
no_authorization: "Authorization 헤더가 필요합니다.",
not_admin: "관리자 권한이 없습니다.",
oauth_provider_not_supported: "OAuth 제공자가 비활성화되어 있습니다.",