Feat: 소셜 로그인 추가 및 인증 페이지 UI 테마 정리

app/login/page.tsx
- Google/Kakao 소셜 로그인 버튼을 폼으로 연동하고 액션 호출 추가
- 로그인 페이지의 배경/버튼/텍스트 색상 등 UI 테마를 파스텔에서 그레이/다크톤으로 통일
- 소셜 버튼 마크업 및 스타일 정리

app/signup/page.tsx
- 회원가입 페이지 배경 및 버튼 색상을 그레이/다크톤으로 변경
- 아이콘 배경 그라디언트 및 링크 색상 업데이트

app/forgot-password/page.tsx
- 비밀번호 재설정 요청 페이지의 배경, 블러 효과 및 버튼 스타일을 그레이/다크톤으로 변경
- 아이콘 배경 및 링크 색상 업데이트

app/reset-password/page.tsx
- 비밀번호 재설정 페이지의 배경, 블러 효과 및 버튼 스타일을 그레이/다크톤으로 변경
- 아이콘 배경 업데이트

features/auth/actions.ts
- Google 및 Kakao OAuth 시작을 위한 공통 헬퍼(signInWithProvider) 추가
- signInWithGoogle 및 signInWithKakao 액션을 구현하여 OAuth 흐름을 시작하도록 추가
This commit is contained in:
2026-02-03 15:44:55 +09:00
parent 43119caf80
commit 151626b181
5 changed files with 175 additions and 73 deletions

View File

@@ -29,14 +29,14 @@ export default async function ForgotPasswordPage({
const { message } = await searchParams; const { message } = await searchParams;
return ( return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 px-4 py-12 dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950"> <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-indigo-200/40 via-purple-200/20 to-transparent dark:from-indigo-900/30 dark:via-purple-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" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,_var(--tw-gradient-stops))] from-pink-200/40 via-purple-200/20 to-transparent dark:from-pink-900/30 dark:via-purple-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-indigo-400/30 blur-3xl dark:bg-indigo-600/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-purple-400/30 blur-3xl delay-700 dark:bg-purple-600/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" />
{/* ========== 메인 콘텐츠 ========== */} {/* ========== 메인 콘텐츠 ========== */}
<div className="relative z-10 w-full max-w-md animate-in fade-in slide-in-from-bottom-4 duration-700"> <div className="relative z-10 w-full max-w-md animate-in fade-in slide-in-from-bottom-4 duration-700">
@@ -47,7 +47,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"> <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"> <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-indigo-500 to-purple-600 shadow-lg"> <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">
<span className="text-4xl">🔑</span> <span className="text-4xl">🔑</span>
</div> </div>
{/* 페이지 제목 */} {/* 페이지 제목 */}
@@ -85,7 +85,7 @@ export default async function ForgotPasswordPage({
{/* ========== 재설정 링크 발송 버튼 ========== */} {/* ========== 재설정 링크 발송 버튼 ========== */}
<Button <Button
formAction={requestPasswordReset} formAction={requestPasswordReset}
className="h-11 w-full bg-gradient-to-r from-indigo-600 to-purple-600 font-semibold text-white shadow-lg transition-all hover:from-indigo-700 hover:to-purple-700 hover:shadow-xl" 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"
> >
</Button> </Button>
@@ -95,7 +95,7 @@ export default async function ForgotPasswordPage({
<div className="text-center"> <div className="text-center">
<Link <Link
href="/login" href="/login"
className="text-sm font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300" className="text-sm font-medium text-gray-700 hover:text-black dark:text-gray-300 dark:hover:text-white"
> >
</Link> </Link>

View File

@@ -1,5 +1,9 @@
import Link from "next/link"; import Link from "next/link";
import { login } from "@/features/auth/actions"; import {
login,
signInWithGoogle,
signInWithKakao,
} from "@/features/auth/actions";
import FormMessage from "@/components/form-message"; import FormMessage from "@/components/form-message";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -33,24 +37,24 @@ export default async function LoginPage({
const { message } = await searchParams; const { message } = await searchParams;
return ( return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 px-4 py-12 dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950"> <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">
{/* ========== 배경 그라디언트 레이어 ========== */} {/* ========== 배경 그라디언트 레이어 ========== */}
{/* 웹 페이지 전체 배경을 그라디언트로 채웁니다 */} {/* 웹 페이지 전체 배경을 그라디언트로 채웁니다 */}
{/* 라이트 모드: 부드러운 파스텔 톤 (indigo → purple → pink) */} {/* 라이트 모드: 부드러운 그레이 톤 (gray → white → gray) */}
{/* 다크 모드: 진한 어두운 톤으로 눈부심 방지 */} {/* 다크 모드: 깊은 블랙 톤으로 고급스러운 느낌 */}
{/* 추가 그라디언트 효과 1: 우상단에서 시작하는 원형 그라디언트 */} {/* 추가 그라디언트 효과 1: 우상단에서 시작하는 원형 그라디언트 */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-indigo-200/40 via-purple-200/20 to-transparent dark:from-indigo-900/30 dark:via-purple-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: 좌하단에서 시작하는 원형 그라디언트 */} {/* 추가 그라디언트 효과 2: 좌하단에서 시작하는 원형 그라디언트 */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,_var(--tw-gradient-stops))] from-pink-200/40 via-purple-200/20 to-transparent dark:from-pink-900/30 dark:via-purple-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" />
{/* ========== 애니메이션 블러 효과 ========== */} {/* ========== 애니메이션 블러 효과 ========== */}
{/* 부드럽게 깜빡이는 원형 블러로 생동감 표현 */} {/* 부드럽게 깜빡이는 원형 블러로 생동감 표현 */}
{/* animate-pulse: 1.5초 주기로 opacity 변화 */} {/* animate-pulse: 1.5초 주기로 opacity 변화 */}
<div className="absolute left-1/4 top-1/4 h-64 w-64 animate-pulse rounded-full bg-indigo-400/30 blur-3xl dark:bg-indigo-600/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" />
{/* delay-700: 700ms 지연으로 교차 애니메이션 효과 */} {/* delay-700: 700ms 지연으로 교차 애니메이션 효과 */}
<div className="absolute bottom-1/4 right-1/4 h-64 w-64 animate-pulse rounded-full bg-purple-400/30 blur-3xl delay-700 dark:bg-purple-600/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" />
{/* ========== 메인 콘텐츠 영역 ========== */} {/* ========== 메인 콘텐츠 영역 ========== */}
{/* z-10: 배경보다 위에 표시 */} {/* z-10: 배경보다 위에 표시 */}
@@ -67,7 +71,7 @@ export default async function LoginPage({
{/* ========== 카드 헤더 영역 ========== */} {/* ========== 카드 헤더 영역 ========== */}
<CardHeader className="space-y-3 text-center"> <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-indigo-500 to-purple-600 shadow-lg"> <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">
<span className="text-4xl">👋</span> <span className="text-4xl">👋</span>
</div> </div>
{/* 페이지 제목 */} {/* 페이지 제목 */}
@@ -138,7 +142,7 @@ export default async function LoginPage({
{/* 비밀번호 찾기 링크 */} {/* 비밀번호 찾기 링크 */}
<Link <Link
href="/forgot-password" href="/forgot-password"
className="text-sm font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300" className="text-sm font-medium text-gray-700 hover:text-black dark:text-gray-300 dark:hover:text-white"
> >
</Link> </Link>
@@ -148,7 +152,7 @@ export default async function LoginPage({
<Button <Button
formAction={login} formAction={login}
type="submit" type="submit"
className="h-11 w-full bg-gradient-to-r from-indigo-600 to-purple-600 font-semibold shadow-lg transition-all duration-200 hover:from-indigo-700 hover:to-purple-700 hover:shadow-xl" className="h-11 w-full bg-gradient-to-r from-gray-900 to-black font-semibold shadow-lg transition-all duration-200 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"
size="lg" size="lg"
> >
@@ -159,7 +163,7 @@ export default async function LoginPage({
?{" "} ?{" "}
<Link <Link
href="/signup" href="/signup"
className="font-semibold text-indigo-600 transition-colors hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300" className="font-semibold text-gray-900 transition-colors hover:text-black dark:text-gray-100 dark:hover:text-white"
> >
</Link> </Link>
@@ -176,45 +180,52 @@ export default async function LoginPage({
{/* 소셜 로그인 버튼들 */} {/* 소셜 로그인 버튼들 */}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<Button {/* ========== Google 로그인 버튼 ========== */}
type="button" <form action={signInWithGoogle}>
variant="outline" <Button
className="h-11 border-gray-200 bg-white shadow-sm transition-all duration-200 hover:bg-gray-50 hover:shadow-md dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-750" type="submit"
> variant="outline"
<svg className="mr-2 h-5 w-5" viewBox="0 0 24 24"> className="h-11 w-full border-gray-200 bg-white shadow-sm transition-all duration-200 hover:bg-gray-50 hover:shadow-md dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-750"
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Google
</Button>
<Button
type="button"
variant="outline"
className="h-11 border-none bg-[#FEE500] font-semibold text-[#3C1E1E] shadow-sm transition-all duration-200 hover:bg-[#FDD835] hover:shadow-md"
>
<svg
className="mr-2 h-5 w-5"
viewBox="0 0 24 24"
fill="currentColor"
> >
<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9-4.03-9-9-9zm-1.5 14.5h-3v-9h3v9zm3 0h-3v-5h3v5zm0-6h-3v-3h3v3z" /> <svg className="mr-2 h-5 w-5" viewBox="0 0 24 24">
</svg> <path
Kakao fill="currentColor"
</Button> d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Google
</Button>
</form>
{/* ========== Kakao 로그인 버튼 ========== */}
<form action={signInWithKakao}>
<Button
type="submit"
variant="outline"
className="h-11 w-full border-none bg-[#FEE500] font-semibold text-[#3C1E1E] shadow-sm transition-all duration-200 hover:bg-[#FDD835] hover:shadow-md"
>
<svg
className="mr-2 h-5 w-5"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9-4.03-9-9-9zm-1.5 14.5h-3v-9h3v9zm3 0h-3v-5h3v5zm0-6h-3v-3h3v3z" />
</svg>
Kakao
</Button>
</form>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -72,14 +72,14 @@ export default async function ResetPasswordPage({
const { message } = params; const { message } = params;
return ( return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 px-4 py-12 dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950"> <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-indigo-200/40 via-purple-200/20 to-transparent dark:from-indigo-900/30 dark:via-purple-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" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,_var(--tw-gradient-stops))] from-pink-200/40 via-purple-200/20 to-transparent dark:from-pink-900/30 dark:via-purple-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-indigo-400/30 blur-3xl dark:bg-indigo-600/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-purple-400/30 blur-3xl delay-700 dark:bg-purple-600/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" />
{/* ========== 메인 콘텐츠 ========== */} {/* ========== 메인 콘텐츠 ========== */}
<div className="relative z-10 w-full max-w-md animate-in fade-in slide-in-from-bottom-4 duration-700"> <div className="relative z-10 w-full max-w-md animate-in fade-in slide-in-from-bottom-4 duration-700">
@@ -90,7 +90,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"> <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"> <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-indigo-500 to-purple-600 shadow-lg"> <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">
<span className="text-4xl">🔐</span> <span className="text-4xl">🔐</span>
</div> </div>
{/* 페이지 제목 */} {/* 페이지 제목 */}
@@ -132,7 +132,7 @@ export default async function ResetPasswordPage({
{/* ========== 비밀번호 변경 버튼 ========== */} {/* ========== 비밀번호 변경 버튼 ========== */}
<Button <Button
formAction={updatePassword} formAction={updatePassword}
className="h-11 w-full bg-gradient-to-r from-indigo-600 to-purple-600 font-semibold text-white shadow-lg transition-all hover:from-indigo-700 hover:to-purple-700 hover:shadow-xl" 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"
> >
</Button> </Button>

View File

@@ -20,14 +20,14 @@ export default async function SignupPage({
const { message } = await searchParams; const { message } = await searchParams;
return ( return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 px-4 py-12 dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950"> <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-indigo-200/40 via-purple-200/20 to-transparent dark:from-indigo-900/30 dark:via-purple-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" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,_var(--tw-gradient-stops))] from-pink-200/40 via-purple-200/20 to-transparent dark:from-pink-900/30 dark:via-purple-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-indigo-400/30 blur-3xl dark:bg-indigo-600/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-purple-400/30 blur-3xl delay-700 dark:bg-purple-600/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" />
<div className="relative z-10 w-full max-w-md animate-in fade-in slide-in-from-bottom-4 duration-700"> <div className="relative z-10 w-full max-w-md animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* 메시지 알림 */} {/* 메시지 알림 */}
@@ -35,7 +35,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"> <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"> <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-indigo-500 to-purple-600 shadow-lg"> <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">
<span className="text-4xl">🚀</span> <span className="text-4xl">🚀</span>
</div> </div>
<CardTitle className="text-3xl font-bold tracking-tight"> <CardTitle className="text-3xl font-bold tracking-tight">
@@ -91,7 +91,7 @@ export default async function SignupPage({
<Button <Button
formAction={signup} formAction={signup}
type="submit" type="submit"
className="h-11 w-full bg-gradient-to-r from-indigo-600 to-purple-600 font-semibold shadow-lg transition-all duration-200 hover:from-indigo-700 hover:to-purple-700 hover:shadow-xl" className="h-11 w-full bg-gradient-to-r from-gray-900 to-black font-semibold shadow-lg transition-all duration-200 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"
size="lg" size="lg"
> >
@@ -102,7 +102,7 @@ export default async function SignupPage({
?{" "} ?{" "}
<Link <Link
href="/login" href="/login"
className="font-semibold text-indigo-600 transition-colors hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300" className="font-semibold text-gray-900 transition-colors hover:text-black dark:text-gray-100 dark:hover:text-white"
> >
</Link> </Link>

View File

@@ -385,3 +385,94 @@ export async function updatePassword(formData: FormData) {
`/login?message=${encodeURIComponent(AUTH_ERROR_MESSAGES.PASSWORD_RESET_SUCCESS)}`, `/login?message=${encodeURIComponent(AUTH_ERROR_MESSAGES.PASSWORD_RESET_SUCCESS)}`,
); );
} }
// ========================================
// 소셜 로그인 (OAuth)
// ========================================
/**
* [OAuth 로그인 헬퍼 함수]
*
* Google, Kakao 등의 소셜 로그인 제공자를 통해 인증을 수행합니다.
*
* 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')
*/
async function signInWithProvider(provider: "google" | "kakao") {
const supabase = await createClient();
// 1. OAuth 인증 시작
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`,
},
});
// 2. 에러 처리
if (error) {
console.error(`[${provider} OAuth] 로그인 실패:`, error.message);
return redirect(
`/login?message=${encodeURIComponent(`${provider} 로그인에 실패했습니다. 다시 시도해 주세요.`)}`,
);
}
// 3. OAuth 제공자 로그인 페이지로 리다이렉트
// data.url은 Google/Kakao의 인증 페이지 URL
if (data.url) {
redirect(data.url);
}
// 4. URL이 없는 경우 (예상치 못한 상황)
redirect(
`/login?message=${encodeURIComponent("로그인 처리 중 오류가 발생했습니다.")}`,
);
}
/**
* [Google 로그인 액션]
*
* Google 계정으로 로그인합니다.
* "Google로 로그인" 버튼에서 호출됩니다.
*
* 처리 과정:
* 1. signInWithOAuth 호출하여 Google OAuth URL 받기
* 2. Google 로그인 페이지로 리다이렉트
* 3. (사용자가 Google에서 로그인 및 권한 동의)
* 4. /auth/callback으로 돌아와서 세션 생성
* 5. 메인 페이지로 이동
*/
export async function signInWithGoogle() {
return signInWithProvider("google");
}
/**
* [Kakao 로그인 액션]
*
* Kakao 계정으로 로그인합니다.
* "Kakao로 로그인" 버튼에서 호출됩니다.
*
* 처리 과정:
* 1. signInWithOAuth 호출하여 Kakao OAuth URL 받기
* 2. Kakao 로그인 페이지로 리다이렉트
* 3. (사용자가 Kakao에서 로그인 및 권한 동의)
* 4. /auth/callback으로 돌아와서 세션 생성
* 5. 메인 페이지로 이동
*/
export async function signInWithKakao() {
return signInWithProvider("kakao");
}