Feat: 로그인 여부에 따른 메인페이지 이동 및 dashboard 처리
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { AUTH_ROUTES } from "@/features/auth/constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [비밀번호 찾기 페이지]
|
* [비밀번호 찾기 페이지]
|
||||||
@@ -72,7 +73,7 @@ export default async function ForgotPasswordPage({
|
|||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href={AUTH_ROUTES.LOGIN}
|
||||||
className="text-sm font-medium text-gray-700 hover:text-black dark:text-gray-300 dark:hover:text-white"
|
className="text-sm font-medium text-gray-700 hover:text-black dark:text-gray-300 dark:hover:text-white"
|
||||||
>
|
>
|
||||||
로그인 페이지로 돌아가기
|
로그인 페이지로 돌아가기
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { AUTH_ROUTES } from "@/features/auth/constants";
|
||||||
import FormMessage from "@/components/form-message";
|
import FormMessage from "@/components/form-message";
|
||||||
import SignupForm from "@/features/auth/components/signup-form";
|
import SignupForm from "@/features/auth/components/signup-form";
|
||||||
import {
|
import {
|
||||||
@@ -42,7 +43,7 @@ export default async function SignupPage({
|
|||||||
<p className="text-center text-sm text-gray-600 dark:text-gray-400">
|
<p className="text-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
이미 계정이 있으신가요?{" "}
|
이미 계정이 있으신가요?{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href={AUTH_ROUTES.LOGIN}
|
||||||
className="font-semibold text-gray-900 transition-colors hover:text-black dark:text-gray-100 dark:hover:text-white"
|
className="font-semibold text-gray-900 transition-colors hover:text-black dark:text-gray-100 dark:hover:text-white"
|
||||||
>
|
>
|
||||||
로그인 하러 가기
|
로그인 하러 가기
|
||||||
|
|||||||
126
app/(home)/page.tsx
Normal file
126
app/(home)/page.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { createClient } from "@/utils/supabase/server";
|
||||||
|
import { UserMenu } from "@/features/layout/components/user-menu";
|
||||||
|
import { AUTH_ROUTES } from "@/features/auth/constants";
|
||||||
|
|
||||||
|
export default async function HomePage() {
|
||||||
|
const supabase = await createClient();
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col">
|
||||||
|
<header className="sticky top-0 z-40 flex h-14 w-full items-center justify-between border-b border-zinc-200 bg-white/75 px-6 backdrop-blur-md dark:border-zinc-800 dark:bg-black/75">
|
||||||
|
<Link href={AUTH_ROUTES.HOME} className="flex items-center gap-2">
|
||||||
|
<div className="h-6 w-6 rounded-md bg-zinc-900 dark:bg-zinc-50" />
|
||||||
|
<span className="text-lg font-bold tracking-tight text-zinc-900 dark:text-zinc-50">
|
||||||
|
AutoTrade
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{user ? (
|
||||||
|
<>
|
||||||
|
<Button asChild variant="ghost" size="sm">
|
||||||
|
<Link href={AUTH_ROUTES.DASHBOARD}>대시보드</Link>
|
||||||
|
</Button>
|
||||||
|
<UserMenu user={user} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button asChild variant="ghost" size="sm">
|
||||||
|
<Link href={AUTH_ROUTES.LOGIN}>로그인</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild size="sm">
|
||||||
|
<Link href={AUTH_ROUTES.SIGNUP}>무료로 시작하기</Link>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="flex-1">
|
||||||
|
<section className="space-y-6 pb-8 pt-6 md:pb-12 md:pt-10 lg:py-32">
|
||||||
|
<div className="container flex max-w-5xl flex-col items-center gap-4 text-center">
|
||||||
|
<h1 className="font-heading text-3xl sm:text-5xl md:text-6xl lg:text-7xl">
|
||||||
|
투자의 미래,{" "}
|
||||||
|
<span className="text-gradient bg-linear-to-r from-indigo-500 to-purple-600 bg-clip-text text-transparent">
|
||||||
|
자동화하세요
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-2xl leading-normal text-muted-foreground sm:text-xl sm:leading-8">
|
||||||
|
AutoTrade는 최첨단 알고리즘을 통해 당신의 암호화폐 투자를 24시간
|
||||||
|
관리합니다. 감정에 휘둘리지 않는 투자를 경험하세요.
|
||||||
|
</p>
|
||||||
|
<div className="space-x-4">
|
||||||
|
{user ? (
|
||||||
|
<Button asChild size="lg" className="h-11 px-8">
|
||||||
|
<Link href={AUTH_ROUTES.DASHBOARD}>대시보드로 이동</Link>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button asChild size="lg" className="h-11 px-8">
|
||||||
|
<Link href={AUTH_ROUTES.LOGIN}>지금 시작하기</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!user && (
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
className="h-11 px-8"
|
||||||
|
>
|
||||||
|
<Link href={AUTH_ROUTES.LOGIN}>데모 체험</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="container space-y-6 bg-slate-50 py-8 dark:bg-transparent md:py-12 lg:py-24">
|
||||||
|
<div className="mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center">
|
||||||
|
<h2 className="font-heading text-3xl leading-[1.1] sm:text-3xl md:text-6xl">
|
||||||
|
주요 기능
|
||||||
|
</h2>
|
||||||
|
<p className="max-w-[85%] leading-normal text-muted-foreground sm:text-lg sm:leading-7">
|
||||||
|
성공적인 투자를 위해 필요한 모든 도구가 준비되어 있습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto grid justify-center gap-4 sm:grid-cols-2 md:max-w-5xl md:grid-cols-3">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: "실시간 모니터링",
|
||||||
|
description:
|
||||||
|
"시장 상황을 실시간으로 분석하고 최적의 타이밍을 포착합니다.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "알고리즘 트레이딩",
|
||||||
|
description:
|
||||||
|
"검증된 전략을 기반으로 자동으로 매수와 매도를 실행합니다.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "포트폴리오 관리",
|
||||||
|
description:
|
||||||
|
"자산 분배와 리스크 관리를 통해 안정적인 수익을 추구합니다.",
|
||||||
|
},
|
||||||
|
].map((feature, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="relative overflow-hidden rounded-lg border bg-background p-2"
|
||||||
|
>
|
||||||
|
<div className="flex h-[180px] flex-col justify-between rounded-md p-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-bold">{feature.title}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
103
app/(main)/dashboard/page.tsx
Normal file
103
app/(main)/dashboard/page.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { createClient } from "@/utils/supabase/server";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Activity, CreditCard, DollarSign, Users } from "lucide-react";
|
||||||
|
|
||||||
|
export default async function DashboardPage() {
|
||||||
|
const supabase = await createClient();
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 space-y-4 p-8 pt-6">
|
||||||
|
<div className="flex items-center justify-between space-y-2">
|
||||||
|
<h2 className="text-3xl font-bold tracking-tight">대시보드</h2>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">총 수익</CardTitle>
|
||||||
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">$45,231.89</div>
|
||||||
|
<p className="text-xs text-muted-foreground">지난달 대비 +20.1%</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">구독자</CardTitle>
|
||||||
|
<Users className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">+2350</div>
|
||||||
|
<p className="text-xs text-muted-foreground">지난달 대비 +180.1%</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">판매량</CardTitle>
|
||||||
|
<CreditCard className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">+12,234</div>
|
||||||
|
<p className="text-xs text-muted-foreground">지난달 대비 +19%</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">현재 활동 중</CardTitle>
|
||||||
|
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">+573</div>
|
||||||
|
<p className="text-xs text-muted-foreground">지난 시간 대비 +201</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-7">
|
||||||
|
<Card className="col-span-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>개요</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pl-2">
|
||||||
|
{/* Chart placeholder */}
|
||||||
|
<div className="h-[200px] w-full bg-slate-100 dark:bg-slate-800 rounded-md flex items-center justify-center text-muted-foreground">
|
||||||
|
차트 영역 (준비 중)
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="col-span-3">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>최근 활동</CardTitle>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
이번 달 265건의 거래가 있었습니다.
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="ml-4 space-y-1">
|
||||||
|
<p className="text-sm font-medium leading-none">
|
||||||
|
비트코인 매수
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">BTC/USDT</p>
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto font-medium">+$1,999.00</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="ml-4 space-y-1">
|
||||||
|
<p className="text-sm font-medium leading-none">
|
||||||
|
이더리움 매도
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">ETH/USDT</p>
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto font-medium">+$39.00</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
export default function DashboardPage() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-3xl font-bold tracking-tight">대시보드</h2>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
자동매매 시스템 현황을 한눈에 확인하세요.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{/* 예시 카드들 */}
|
|
||||||
<div className="rounded-xl border bg-card text-card-foreground shadow p-6">
|
|
||||||
<div className="flex flex-col space-y-1.5 p-6 pt-0 px-0">
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
|
||||||
총 자산
|
|
||||||
</span>
|
|
||||||
<span className="text-2xl font-bold">₩ 0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl border bg-card text-card-foreground shadow p-6">
|
|
||||||
<div className="flex flex-col space-y-1.5 p-6 pt-0 px-0">
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
|
||||||
평가 손익
|
|
||||||
</span>
|
|
||||||
<span className="text-2xl font-bold text-green-500">+0%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl border bg-card text-card-foreground shadow p-6">
|
|
||||||
<div className="flex flex-col space-y-1.5 p-6 pt-0 px-0">
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
|
||||||
실현 손익
|
|
||||||
</span>
|
|
||||||
<span className="text-2xl font-bold">₩ 0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl border bg-card text-card-foreground shadow p-6">
|
|
||||||
<div className="flex flex-col space-y-1.5 p-6 pt-0 px-0">
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
|
||||||
가동 봇
|
|
||||||
</span>
|
|
||||||
<span className="text-2xl font-bold">0개</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 차트나 로그 영역 예시 */}
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
|
||||||
<div className="col-span-4 rounded-xl border bg-card text-card-foreground shadow min-h-[300px] p-6">
|
|
||||||
<div className="text-lg font-semibold mb-4">매매 추입</div>
|
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
|
||||||
차트 영역 준비중...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-3 rounded-xl border bg-card text-card-foreground shadow min-h-[300px] p-6">
|
|
||||||
<div className="text-lg font-semibold mb-4">최근 활동</div>
|
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
|
||||||
로그 영역 준비중...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -14,8 +14,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "AutoTrade",
|
||||||
description: "Generated by create next app",
|
description: "Automated Crypto Trading Platform",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export async function signout() {
|
|||||||
revalidatePath("/", "layout");
|
revalidatePath("/", "layout");
|
||||||
|
|
||||||
// 3. 로그인 페이지로 리다이렉트
|
// 3. 로그인 페이지로 리다이렉트
|
||||||
redirect("/login");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { AUTH_ROUTES } from "@/features/auth/constants";
|
||||||
import {
|
import {
|
||||||
login,
|
login,
|
||||||
signInWithGoogle,
|
signInWithGoogle,
|
||||||
@@ -122,7 +123,7 @@ export default function LoginForm() {
|
|||||||
</div>
|
</div>
|
||||||
{/* 비밀번호 찾기 링크 */}
|
{/* 비밀번호 찾기 링크 */}
|
||||||
<Link
|
<Link
|
||||||
href="/forgot-password"
|
href={AUTH_ROUTES.FORGOT_PASSWORD}
|
||||||
className="text-sm font-medium text-gray-700 hover:text-black dark:text-gray-300 dark:hover:text-white"
|
className="text-sm font-medium text-gray-700 hover:text-black dark:text-gray-300 dark:hover:text-white"
|
||||||
>
|
>
|
||||||
비밀번호 찾기
|
비밀번호 찾기
|
||||||
@@ -150,7 +151,7 @@ export default function LoginForm() {
|
|||||||
<p className="text-center text-sm text-gray-600 dark:text-gray-400">
|
<p className="text-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
계정이 없으신가요?{" "}
|
계정이 없으신가요?{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/signup"
|
href={AUTH_ROUTES.SIGNUP}
|
||||||
className="font-semibold text-gray-900 transition-colors hover:text-black dark:text-gray-100 dark:hover:text-white"
|
className="font-semibold text-gray-900 transition-colors hover:text-black dark:text-gray-100 dark:hover:text-white"
|
||||||
>
|
>
|
||||||
회원가입 하기
|
회원가입 하기
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ export const AUTH_ROUTES = {
|
|||||||
AUTH_CONFIRM: "/auth/confirm",
|
AUTH_CONFIRM: "/auth/confirm",
|
||||||
AUTH_CALLBACK: "/auth/callback",
|
AUTH_CALLBACK: "/auth/callback",
|
||||||
HOME: "/",
|
HOME: "/",
|
||||||
|
DASHBOARD: "/dashboard",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createClient } from "@/utils/supabase/server";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { UserMenu } from "./user-menu";
|
import { UserMenu } from "./user-menu";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { AUTH_ROUTES } from "@/features/auth/constants";
|
||||||
|
|
||||||
export async function Header() {
|
export async function Header() {
|
||||||
const supabase = await createClient();
|
const supabase = await createClient();
|
||||||
@@ -12,7 +13,7 @@ export async function Header() {
|
|||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-40 flex h-14 w-full items-center justify-between border-b border-zinc-200 bg-white/75 px-6 backdrop-blur-md transition-all dark:border-zinc-800 dark:bg-black/75">
|
<header className="sticky top-0 z-40 flex h-14 w-full items-center justify-between border-b border-zinc-200 bg-white/75 px-6 backdrop-blur-md transition-all dark:border-zinc-800 dark:bg-black/75">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href="/" className="flex items-center gap-2">
|
<Link href={AUTH_ROUTES.DASHBOARD} className="flex items-center gap-2">
|
||||||
<div className="h-6 w-6 rounded-md bg-zinc-900 dark:bg-zinc-50" />
|
<div className="h-6 w-6 rounded-md bg-zinc-900 dark:bg-zinc-50" />
|
||||||
<span className="text-lg font-bold tracking-tight text-zinc-900 dark:text-zinc-50">
|
<span className="text-lg font-bold tracking-tight text-zinc-900 dark:text-zinc-50">
|
||||||
AutoTrade
|
AutoTrade
|
||||||
@@ -25,7 +26,7 @@ export async function Header() {
|
|||||||
<UserMenu user={user} />
|
<UserMenu user={user} />
|
||||||
) : (
|
) : (
|
||||||
<Button asChild variant="default" size="sm">
|
<Button asChild variant="default" size="sm">
|
||||||
<Link href="/login">로그인</Link>
|
<Link href={AUTH_ROUTES.LOGIN}>로그인</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
64
package-lock.json
generated
64
package-lock.json
generated
@@ -34,6 +34,7 @@
|
|||||||
"zustand": "^5.0.11"
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.1",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
@@ -1298,6 +1299,22 @@
|
|||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.58.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.1.tgz",
|
||||||
|
"integrity": "sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.58.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/number": {
|
"node_modules/@radix-ui/number": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
@@ -6972,6 +6989,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@@ -8715,6 +8747,38 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.58.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz",
|
||||||
|
"integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.58.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.58.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz",
|
||||||
|
"integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"zustand": "^5.0.11"
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.1",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|||||||
85
playwright-report/index.html
Normal file
85
playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
42
playwright.config.ts
Normal file
42
playwright.config.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests/e2e",
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: "html",
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: "http://localhost:3001",
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: "on-first-retry",
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: "npm run dev",
|
||||||
|
url: "http://localhost:3001",
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 120 * 1000,
|
||||||
|
},
|
||||||
|
});
|
||||||
4
test-results/.last-run.json
Normal file
4
test-results/.last-run.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"status": "passed",
|
||||||
|
"failedTests": []
|
||||||
|
}
|
||||||
29
tests/e2e/auth.spec.ts
Normal file
29
tests/e2e/auth.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Authentication Flow", () => {
|
||||||
|
test("Guest should see Landing Page", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
await expect(page).toHaveTitle(/AutoTrade/i);
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: "투자의 미래, 자동화하세요" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole("link", { name: "로그인" }).first(),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Guest trying to access /dashboard should be redirected to /login", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/dashboard");
|
||||||
|
await expect(page).toHaveURL(/\/login/);
|
||||||
|
await expect(page.getByRole("button", { name: "로그인" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Login page should load correctly", async ({ page }) => {
|
||||||
|
await page.goto("/login");
|
||||||
|
await expect(page.getByLabel("이메일", { exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByLabel("비밀번호")).toBeVisible();
|
||||||
|
await expect(page.getByRole("button", { name: "로그인" })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,17 +9,24 @@ import {
|
|||||||
/**
|
/**
|
||||||
* 서버 사이드 인증 상태를 관리하고 보호된 라우트를 처리합니다.
|
* 서버 사이드 인증 상태를 관리하고 보호된 라우트를 처리합니다.
|
||||||
*/
|
*/
|
||||||
|
// 서버 사이드 인증 상태를 관리하고 보호된 라우트를 처리합니다.
|
||||||
export async function updateSession(request: NextRequest) {
|
export async function updateSession(request: NextRequest) {
|
||||||
|
// 1. 초기 Supabase 응답 객체 생성
|
||||||
|
// request 헤더 등을 포함하여 초기 상태 설정
|
||||||
let supabaseResponse = NextResponse.next({ request });
|
let supabaseResponse = NextResponse.next({ request });
|
||||||
|
|
||||||
|
// 2. Supabase 클라이언트 생성 (SSR 전용)
|
||||||
|
// 쿠키 조작을 위한 setAll/getAll 메서드 오버라이딩 포함
|
||||||
const supabase = createServerClient(
|
const supabase = createServerClient(
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
{
|
{
|
||||||
cookies: {
|
cookies: {
|
||||||
|
// 쿠키 가져오기
|
||||||
getAll() {
|
getAll() {
|
||||||
return request.cookies.getAll();
|
return request.cookies.getAll();
|
||||||
},
|
},
|
||||||
|
// 쿠키 설정하기 (요청 및 응답 객체 모두에 적용)
|
||||||
setAll(cookiesToSet) {
|
setAll(cookiesToSet) {
|
||||||
cookiesToSet.forEach(({ name, value }) =>
|
cookiesToSet.forEach(({ name, value }) =>
|
||||||
request.cookies.set(name, value),
|
request.cookies.set(name, value),
|
||||||
@@ -33,13 +40,18 @@ export async function updateSession(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 3. 현재 사용자 정보 조회
|
||||||
|
// getUser() 사용이 보안상 안전함 (getSession보다 권장됨)
|
||||||
const {
|
const {
|
||||||
data: { user },
|
data: { user },
|
||||||
} = await supabase.auth.getUser();
|
} = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
// 4. 현재 요청 URL과 복구용 쿠키 확인
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
const recoveryCookie = request.cookies.get(RECOVERY_COOKIE_NAME)?.value;
|
const recoveryCookie = request.cookies.get(RECOVERY_COOKIE_NAME)?.value;
|
||||||
|
|
||||||
|
// 5. 복구 쿠키가 있는데 로그인이 안 된 경우 (세션 만료 등)
|
||||||
|
// 로그인 페이지로 강제 리다이렉트 후 복구 쿠키 삭제
|
||||||
if (recoveryCookie && !user) {
|
if (recoveryCookie && !user) {
|
||||||
const response = NextResponse.redirect(
|
const response = NextResponse.redirect(
|
||||||
new URL(AUTH_ROUTES.LOGIN, request.url),
|
new URL(AUTH_ROUTES.LOGIN, request.url),
|
||||||
@@ -48,24 +60,39 @@ export async function updateSession(request: NextRequest) {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6. 현재 페이지가 비밀번호 재설정 관련 라우트인지 확인
|
||||||
const isRecoveryRoute =
|
const isRecoveryRoute =
|
||||||
pathname.startsWith(AUTH_ROUTES.RESET_PASSWORD) ||
|
pathname.startsWith(AUTH_ROUTES.RESET_PASSWORD) ||
|
||||||
pathname.startsWith(AUTH_ROUTES.AUTH_CONFIRM);
|
pathname.startsWith(AUTH_ROUTES.AUTH_CONFIRM);
|
||||||
|
|
||||||
|
// 7. 복구 쿠키가 있는데 재설정 라우트가 아닌 다른 곳으로 가려는 경우
|
||||||
|
// 강제로 비밀번호 재설정 페이지로 리다이렉트 (보안 조치)
|
||||||
if (recoveryCookie && !isRecoveryRoute) {
|
if (recoveryCookie && !isRecoveryRoute) {
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL(AUTH_ROUTES.RESET_PASSWORD, request.url),
|
new URL(AUTH_ROUTES.RESET_PASSWORD, request.url),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 8. 현재 페이지가 로그인/회원가입 등 공용 인증 페이지인지 확인
|
||||||
const isAuthPage = PUBLIC_AUTH_PAGES.some((page) =>
|
const isAuthPage = PUBLIC_AUTH_PAGES.some((page) =>
|
||||||
pathname.startsWith(page),
|
pathname.startsWith(page),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!user && !isAuthPage) {
|
// 9. 비로그인 사용자 접근 제어
|
||||||
|
// - 유저가 없음 (!user)
|
||||||
|
// - 인증 페이지 아님 (!isAuthPage)
|
||||||
|
// - 메인 페이지(홈) 아님 (pathname !== AUTH_ROUTES.HOME)
|
||||||
|
// -> 로그인 페이지로 리다이렉트
|
||||||
|
if (!user && !isAuthPage && pathname !== AUTH_ROUTES.HOME) {
|
||||||
return NextResponse.redirect(new URL(AUTH_ROUTES.LOGIN, request.url));
|
return NextResponse.redirect(new URL(AUTH_ROUTES.LOGIN, request.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 10. 로그인 사용자 접근 제어 (인증 페이지 접근 시)
|
||||||
|
// - 유저가 있음 (user)
|
||||||
|
// - 인증 페이지 접근 시도 (isAuthPage) - 예: 이미 로그인했는데 /login 접근
|
||||||
|
// - 비밀번호 재설정은 아님
|
||||||
|
// - 복구 모드 아님
|
||||||
|
// -> 메인 페이지로 리다이렉트
|
||||||
if (
|
if (
|
||||||
user &&
|
user &&
|
||||||
isAuthPage &&
|
isAuthPage &&
|
||||||
@@ -75,5 +102,6 @@ export async function updateSession(request: NextRequest) {
|
|||||||
return NextResponse.redirect(new URL(AUTH_ROUTES.HOME, request.url));
|
return NextResponse.redirect(new URL(AUTH_ROUTES.HOME, request.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 11. 최종 응답 반환 (변경된 쿠키 등이 포함됨)
|
||||||
return supabaseResponse;
|
return supabaseResponse;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user