Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ yarn-error.log*

# typescript
*.tsbuildinfo
next-env.d.ts
next-env.d.ts

.env.local
116 changes: 116 additions & 0 deletions app/global/auth/useAuth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"use client";

import {
createContext,
useContext,
useEffect,
useState,
ReactNode,
} from "react";

const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;

export interface MemberDto {
id: number;
email: string;
nickname: string;
}

interface AuthContextType {
loginMember: MemberDto | null; //로그인 멤버의 정보 { id:long, email:string, nickname:string }
isLogin: boolean; //로그인 상태 보여주는 boolean
reloadMember: () => void; //로그인 멤버의 상태 최신화
logoutMember: () => void; //로그아웃 요청 후 쿠키삭제
accessToken: string | null;
apiKey: string | null;
setAccessToken: (value: string) => void;
setApiKey: (value: string) => void;
}

const AuthContext = createContext<AuthContextType>({
loginMember: null,
isLogin: false,
reloadMember: () => {},
logoutMember: () => {},
accessToken: null,
apiKey: null,
setAccessToken: () => {},
setApiKey: () => {},
});

export function AuthProvider({ children }: { children: ReactNode }) {
const [loginMember, setLoginMember] = useState<MemberDto | null>(null);
const [isLogin, setIsLogin] = useState(false);
const [accessToken, setAccessToken] = useState<string | null>("");
const [apiKey, setApiKey] = useState<string | null>("");

const fetchMember = async () => {
try {
const res = await fetch(`${baseUrl}/api/v1/auth/me`, {
method: "GET",
headers: {
"Content-Type": "application/json",
...(accessToken && apiKey
? { Authorization: `Bearer ${apiKey} ${accessToken}` }
: {}),
},
credentials: "include",
});
if (!res.ok) {
setLoginMember(null);
setIsLogin(false);
return;
}
const data = await res.json();
setLoginMember(data.data);
setIsLogin(true);
} catch (err) {
console.error("로그인 정보 요청 실패:", err);
setLoginMember(null);
setIsLogin(false);
}
};

const logoutMember = async () => {
try {
const res = await fetch(`${baseUrl}/api/v1/auth/logout`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
credentials: "include",
});
if (!res.ok) {
return;
}
setLoginMember(null);
setIsLogin(false);
} catch (err) {
console.error("로그아웃 요청 실패:", err);
}
};

useEffect(() => {
fetchMember();
}, [apiKey, accessToken]);

return (
<AuthContext.Provider
value={{
loginMember,
isLogin,
reloadMember: fetchMember,
logoutMember: logoutMember,
accessToken,
apiKey,
setAccessToken: setAccessToken,
setApiKey: setApiKey,
}}
>
{children}
</AuthContext.Provider>
);
}

// 다른 컴포넌트에서 사용 가능
export function useAuth() {
return useContext(AuthContext);
}
43 changes: 22 additions & 21 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
import type React from 'react';
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import { Analytics } from '@vercel/analytics/next';
import './globals.css';
import type React from "react";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Analytics } from "@vercel/analytics/next";
import "./globals.css";
import { AuthProvider } from "./global/auth/useAuth";

const _geist = Geist({ subsets: ['latin'] });
const _geistMono = Geist_Mono({ subsets: ['latin'] });
const _geist = Geist({ subsets: ["latin"] });
const _geistMono = Geist_Mono({ subsets: ["latin"] });

export const metadata: Metadata = {
title: 'OneLife - 1인 가구 커뮤니티 플랫폼',
description:
'혼자여도 외롭지 않은 따뜻한 일상. 1인 가구를 위한 커뮤니티 플랫폼 OneLife에서 이웃들과 함께 더 풍요로운 혼라이프를 만들어보세요',
generator: 'v0.app',
title: "OneLife - 1인 가구 커뮤니티 플랫폼",
description:
"혼자여도 외롭지 않은 따뜻한 일상. 1인 가구를 위한 커뮤니티 플랫폼 OneLife에서 이웃들과 함께 더 풍요로운 혼라이프를 만들어보세요",
generator: "v0.app",
};

export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<html lang="ko">
<body className={`font-sans antialiased`}>
{children}
<Analytics />
</body>
</html>
);
return (
<html lang="ko">
<body className={`font-sans antialiased`}>
<AuthProvider>{children}</AuthProvider>
<Analytics />
</body>
</html>
);
}
129 changes: 91 additions & 38 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,74 @@
"use client"

import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { Header } from "@/components/header"
import { Footer } from "@/components/footer"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { Header } from "@/components/header";
import { Footer } from "@/components/footer";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { useAuth } from "@/app/global/auth/useAuth";

export default function LoginPage() {
const router = useRouter()
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const { setAccessToken, setApiKey } = useAuth();

const handleSubmit = async (e: React.FormEvent) => {
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
e.preventDefault();
setLoading(true);
setError("");

try {
// 백엔드 로그인 API 호출
const res = await fetch(`${baseUrl}/api/v1/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
credentials: "include",
});

if (!res.ok) {
throw new Error("로그인 실패: 이메일 또는 비밀번호를 확인하세요.");
}

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
console.log("[v0] Login attempt:", { email, password })
// TODO: Implement actual login logic with backend
router.push("/")
}
const data = await res.json();
console.log("이게 데이터", data);

if (res.ok) {
const authHeader = res.headers.get("Authorization");

if (authHeader) {
const parts = authHeader.split(" ");

if (parts.length >= 3) {
const [, apiKey, accessToken] = parts;
setApiKey(apiKey);
setAccessToken(accessToken);
}
}
//await reloadMember(); // context 상태 갱신
router.push("/"); // 메인 이동
}
} catch (err: any) {
setError(err.message);
console.error("Login error:", err);
} finally {
setLoading(false);
}
};

const handleKakaoLogin = () => {
console.log("[v0] Kakao login clicked")
// TODO: Implement Kakao OAuth login
}
//TODO 카카오 Oauth 구현
};

return (
<div className="min-h-screen flex flex-col bg-background">
Expand All @@ -37,8 +79,12 @@ export default function LoginPage() {
<div className="max-w-md mx-auto">
<Card>
<CardHeader className="space-y-1">
<CardTitle className="text-2xl font-bold text-center">로그인</CardTitle>
<p className="text-sm text-muted-foreground text-center">OneLife에 오신 것을 환영합니다</p>
<CardTitle className="text-2xl font-bold text-center">
로그인
</CardTitle>
<p className="text-sm text-muted-foreground text-center">
OneLife에 오신 것을 환영합니다
</p>
</CardHeader>
<CardContent className="space-y-4">
<form onSubmit={handleSubmit} className="space-y-4">
Expand All @@ -64,14 +110,10 @@ export default function LoginPage() {
/>
</div>

<div className="flex items-center justify-between text-sm">
<Link href="/forgot-password" className="text-primary hover:underline">
비밀번호 찾기
</Link>
</div>
{error && <p className="text-red-500 text-sm">{error}</p>}

<Button type="submit" className="w-full">
로그인
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "로그인 중..." : "로그인"}
</Button>
</form>

Expand All @@ -80,7 +122,9 @@ export default function LoginPage() {
<Separator />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-card px-2 text-muted-foreground">또는</span>
<span className="bg-card px-2 text-muted-foreground">
또는
</span>
</div>
</div>

Expand All @@ -95,15 +139,24 @@ export default function LoginPage() {
color: "#000000",
}}
>
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<svg
className="mr-2 h-4 w-4"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 3C6.477 3 2 6.477 2 10.5c0 2.636 1.752 4.944 4.4 6.32-.192.71-.633 2.366-.732 2.742-.118.448.164.441.345.32.145-.096 2.118-1.405 2.923-1.946.61.083 1.233.126 1.864.126 5.523 0 10-3.477 10-7.5S17.523 3 12 3z" />
</svg>
카카오 로그인
</Button>

<div className="text-center text-sm">
<span className="text-muted-foreground">아직 회원이 아니신가요? </span>
<Link href="/signup" className="text-primary hover:underline font-medium">
<span className="text-muted-foreground">
아직 회원이 아니신가요?{" "}
</span>
<Link
href="/signup"
className="text-primary hover:underline font-medium"
>
회원가입
</Link>
</div>
Expand All @@ -115,5 +168,5 @@ export default function LoginPage() {

<Footer />
</div>
)
);
}
Loading