Skip to content

Commit

Permalink
save
Browse files Browse the repository at this point in the history
  • Loading branch information
LeegwangYeol committed Jan 20, 2025
1 parent c593a01 commit 6d490bc
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 49 deletions.
39 changes: 39 additions & 0 deletions app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { AuthService } from "@/lib/auth-service";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
try {
const body = await request.json();
const { username, password } = body;

const authResponse = await AuthService.login({ username, password });

console.log("authResponse:", authResponse); // 로그 추가

if (!authResponse) {
return NextResponse.json(
{ error: "잘못된 아이디 또는 비밀번호입니다." },
{ status: 401 }
);
}

// Set the access token cookie
(await cookies()).set("accessToken", authResponse.accessToken, {
// httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 24 * 60 * 60, // 24 hours
});

return NextResponse.json({
user: authResponse.user,
});
} catch (error) {
console.error("Login error:", error);
return NextResponse.json(
{ error: "로그인 처리 중 오류가 발생했습니다." },
{ status: 500 }
);
}
}
16 changes: 16 additions & 0 deletions app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export async function POST() {
try {
// 쿠키 삭제
(await cookies()).delete("accessToken");
return NextResponse.json({ success: true });
} catch (error) {
console.error("Logout error:", error);
return NextResponse.json(
{ error: "로그아웃 처리 중 오류가 발생했습니다." },
{ status: 500 }
);
}
}
119 changes: 95 additions & 24 deletions app/component/login-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,76 +4,147 @@ import { useState } from "react";
import { Activity, Apple } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useTheme } from "../context/theme-context";

export default function LoginPage() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const { theme } = useTheme();

const handleLogin = (e: React.FormEvent) => {
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
// Handle login logic here
console.log("Login attempted with:", { username, password });
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});

if (!response.ok) {
throw new Error("로그인에 실패했습니다.");
}

const data = await response.json();

// URL 파라미터에서 리다이렉트 URL 가져오기
const params = new URLSearchParams(window.location.search);
const redirectUrl = params.get("redirect") || "/";

// 리다이렉트
window.location.href = redirectUrl;
} catch (error) {
console.error("Login error:", error);
alert("로그인에 실패했습니다. 다시 시도해주세요.");
}
};

return (
<div className="min-h-screen bg-gradient-to-br from-pink-500 via-red-500 to-yellow-500 flex items-center justify-center p-4">
<div className="bg-[rgb(var(--background))] rounded-lg shadow-xl p-8 w-full max-w-md transform transition-all hover:scale-105 duration-300">
<div
className={`min-h-screen flex items-center justify-center p-4 ml-24 ${
theme === "dark"
? "bg-[rgb(var(--background))]"
: "bg-[rgb(var(--background))]"
}`}
>
<div
className={`${
theme === "dark" ? "glassmorphism-dark" : "glassmorphism-light"
} rounded-lg p-8 w-full max-w-md transform transition-all hover:scale-105 duration-300`}
>
<div className="flex flex-col items-center mb-6">
<Activity className="w-16 h-16 text-[rgb(var(--accent))] mb-4" />
<Activity className="w-16 h-16 text-[rgb(var(--accent))] mb-4 animate-pulse" />
<h1 className="text-2xl font-bold text-[rgb(var(--foreground))]">
Activity
무련
</h1>
</div>

<form onSubmit={handleLogin} className="space-y-6">
<div>
<Input
type="text"
placeholder="Username"
placeholder="아이디"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-4 py-2 border border-[rgb(var(--secondary))] rounded-md focus:ring-2 focus:ring-[rgb(var(--accent))] focus:border-transparent"
className={`w-full px-4 py-2 border rounded-md transition-all duration-300 ${
theme === "dark"
? "bg-[rgb(var(--background))] border-[rgb(var(--secondary))] text-[rgb(var(--foreground))]"
: "bg-white border-[rgb(var(--secondary))] text-[rgb(var(--foreground))]"
} focus:ring-2 focus:ring-[rgb(var(--accent))] focus:border-transparent`}
/>
</div>
<div>
<Input
type="password"
placeholder="Password"
placeholder="비밀번호"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 border border-[rgb(var(--secondary))] rounded-md focus:ring-2 focus:ring-[rgb(var(--accent))] focus:border-transparent"
className={`w-full px-4 py-2 border rounded-md transition-all duration-300 ${
theme === "dark"
? "bg-[rgb(var(--background))] border-[rgb(var(--secondary))] text-[rgb(var(--foreground))]"
: "bg-white border-[rgb(var(--secondary))] text-[rgb(var(--foreground))]"
} focus:ring-2 focus:ring-[rgb(var(--accent))] focus:border-transparent`}
/>
</div>
<Button
type="submit"
className="w-full bg-gradient-to-r from-pink-500 to-red-500 hover:from-pink-600 hover:to-red-600 text-white font-bold py-2 px-4 rounded-md transition duration-300 ease-in-out transform hover:-translate-y-1 hover:scale-105"
className={`w-full font-bold py-2 px-4 rounded-md transition duration-300 ease-in-out transform hover:-translate-y-1 hover:scale-105 ${
theme === "dark"
? "bg-[rgb(var(--accent))] text-white hover:bg-[rgb(var(--accent))/90]"
: "bg-[rgb(var(--accent))] text-white hover:bg-[rgb(var(--accent))/90]"
}`}
>
Log In
로그인
</Button>
</form>

<div className="mt-6 flex items-center justify-center">
<div className="border-t border-gray-300 flex-grow mr-3"></div>
<span className="text-gray-500 font-medium">OR</span>
<div className="border-t border-gray-300 flex-grow ml-3"></div>
<div
className={`border-t flex-grow mr-3 ${
theme === "dark"
? "border-[rgb(var(--secondary))]"
: "border-[rgb(var(--secondary))]"
}`}
></div>
<span className="text-[rgb(var(--secondary))] font-medium">또는</span>
<div
className={`border-t flex-grow ml-3 ${
theme === "dark"
? "border-[rgb(var(--secondary))]"
: "border-[rgb(var(--secondary))]"
}`}
></div>
</div>

<Button className="w-full mt-6 bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md flex items-center justify-center transition duration-300 ease-in-out">
<Button
className={`w-full mt-6 font-bold py-2 px-4 rounded-md flex items-center justify-center transition duration-300 ease-in-out ${
theme === "dark"
? "bg-blue-600 hover:bg-blue-700 text-white"
: "bg-blue-600 hover:bg-blue-700 text-white"
}`}
>
<Apple className="mr-2" />
Log in with Facebook
Apple로 로그인
</Button>

<div className="mt-6 text-center">
<a href="#" className="text-sm text-blue-500 hover:underline">
Forgot password?
<a
href="#"
className="text-sm text-[rgb(var(--accent))] hover:underline transition-colors duration-300"
>
비밀번호를 잊으셨나요?
</a>
</div>

<div className="mt-8 text-center">
<p className="text-gray-600">
Don't have an account?
<a href="#" className="text-blue-500 hover:underline ml-1">
Sign up
<p className="text-[rgb(var(--secondary))]">
계정이 없으신가요?
<a
href="#"
className="text-[rgb(var(--accent))] hover:underline ml-1 transition-colors duration-300"
>
회원가입
</a>
</p>
</div>
Expand Down
72 changes: 53 additions & 19 deletions app/component/navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useState, useEffect } from "react";
import Link from "next/link";
import {
Home,
Expand All @@ -16,6 +16,7 @@ import {
Scissors,
Sun,
Moon,
LogOut,
} from "lucide-react";
import VideoModal from "./VideoModal";
import { useTheme } from "../context/theme-context";
Expand Down Expand Up @@ -71,6 +72,38 @@ export default function Navigation({
const [isVideoModalOpen, setIsVideoModalOpen] = useState(false);
const [isExpanded, setIsExpanded] = useState(true);
const { theme, toggleTheme } = useTheme();
const [isLoggedIn, setIsLoggedIn] = useState(false);

useEffect(() => {
// 쿠키에서 accessToken 확인
console.log("accessToken", document.cookie);
const hasToken = document.cookie.includes("accessToken");
console.log("hasToken", hasToken);
setIsLoggedIn(hasToken);
}, []);

const handleLogout = async () => {
try {
const response = await fetch("/api/auth/logout", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});

if (!response.ok) {
throw new Error("로그아웃 실패");
}

// 로그아웃 성공
setIsLoggedIn(false);
alert("로그아웃 되었습니다.");
window.location.href = "/";
} catch (error) {
console.error("Logout error:", error);
alert("로그아웃 처리 중 오류가 발생했습니다.");
}
};

const handleExpand = (expanded: boolean) => {
setIsExpanded(expanded);
Expand Down Expand Up @@ -143,24 +176,25 @@ export default function Navigation({
</Link>
</li>
))}
<li>
{/* <button
type="button"
onClick={() => setIsVideoModalOpen(true)}
className={`w-full text-left flex items-center gap-4 p-4 rounded-lg transition-all duration-300 group ${
theme === "dark"
? "hover:bg-primary/90 hover:text-primary-foreground"
: "hover:bg-secondary/90 hover:text-secondary-foreground"
}`}
>
<div className="group-hover:animate-shake">
<Swords size={24} />
</div>
{isExpanded && (
<span className="group-hover:animate-shake">대련</span>
)}
</button> */}
</li>
{isLoggedIn && (
<li>
<button
onClick={handleLogout}
className={`w-full flex items-center gap-4 p-4 rounded-lg mb-2 transition-all duration-300 group ${
theme === "dark"
? "hover:bg-primary/90 hover:text-primary-foreground"
: "hover:bg-secondary/90 hover:text-secondary-foreground"
}`}
>
<div className="group-hover:animate-shake">
<LogOut size={24} />
</div>
{isExpanded && (
<span className="group-hover:animate-shake">로그아웃</span>
)}
</button>
</li>
)}
</ul>
</nav>
<VideoModal
Expand Down
7 changes: 6 additions & 1 deletion app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import LoginPage from "../component/login-page";
import { MainLayout } from "@/components/layout/main-layout";

export default function Login() {
return <LoginPage />;
return (
<MainLayout>
<LoginPage />
</MainLayout>
);
}
32 changes: 32 additions & 0 deletions lib/auth-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AuthResponse, LoginCredentials, User } from '@/types/auth';
import { TokenService } from './token-service';

export class AuthService {
static async login(credentials: LoginCredentials): Promise<AuthResponse | null> {
// 하드코딩된 사용자 인증
const { username, password } = credentials;

let user: User | null = null;

if (username === '1111' && password === '1111') {
user = { id: '1', role: 'admin' };
} else if (username === '2222' && password === '2222') {
user = { id: '2', role: 'user' };
}

if (!user) {
return null;
}

const accessToken = await TokenService.generateToken(user);

return {
user,
accessToken
};
}

static async validateToken(token: string): Promise<User | null> {
return TokenService.verifyToken(token);
}
}
25 changes: 25 additions & 0 deletions lib/token-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { User } from '@/types/auth';
import { jwtVerify, SignJWT } from 'jose';

const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET || 'your-secret-key');

export class TokenService {
static async generateToken(user: User): Promise<string> {
return new SignJWT({ sub: user.id, role: user.role })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('24h')
.sign(SECRET_KEY);
}

static async verifyToken(token: string): Promise<User | null> {
try {
const { payload } = await jwtVerify(token, SECRET_KEY);
return {
id: payload.sub as string,
role: payload.role as 'admin' | 'user'
};
} catch {
return null;
}
}
}
Loading

0 comments on commit 6d490bc

Please sign in to comment.