From 448781776b332c90a877254b6d4e54d1911bfdae Mon Sep 17 00:00:00 2001 From: SeokHwan13 Date: Fri, 14 Nov 2025 17:06:34 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85,?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20=EC=9D=B8=EC=A6=9D=EC=9D=B8?= =?UTF-8?q?=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20#1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- app/global/auth/useAuth.tsx | 116 ++++++++++++++++++++++++++++++ app/layout.tsx | 43 +++++------ app/login/page.tsx | 129 +++++++++++++++++++++++---------- app/page.tsx | 34 ++++----- app/signup/page.tsx | 138 +++++++++++++++++++++++------------- components/header.tsx | 39 ++++++++-- 7 files changed, 371 insertions(+), 132 deletions(-) create mode 100644 app/global/auth/useAuth.tsx diff --git a/.gitignore b/.gitignore index f650315..a57f478 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts \ No newline at end of file +next-env.d.ts + +.env.local \ No newline at end of file diff --git a/app/global/auth/useAuth.tsx b/app/global/auth/useAuth.tsx new file mode 100644 index 0000000..c7ce78d --- /dev/null +++ b/app/global/auth/useAuth.tsx @@ -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({ + loginMember: null, + isLogin: false, + reloadMember: () => {}, + logoutMember: () => {}, + accessToken: null, + apiKey: null, + setAccessToken: () => {}, + setApiKey: () => {}, +}); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [loginMember, setLoginMember] = useState(null); + const [isLogin, setIsLogin] = useState(false); + const [accessToken, setAccessToken] = useState(""); + const [apiKey, setApiKey] = useState(""); + + 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 ( + + {children} + + ); +} + +// 다른 컴포넌트에서 사용 가능 +export function useAuth() { + return useContext(AuthContext); +} diff --git a/app/layout.tsx b/app/layout.tsx index 1675b29..55329bd 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( - - - {children} - - - - ); + return ( + + + {children} + + + + ); } diff --git a/app/login/page.tsx b/app/login/page.tsx index 44dd021..499fda2 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -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 (
@@ -37,8 +79,12 @@ export default function LoginPage() {
- 로그인 -

OneLife에 오신 것을 환영합니다

+ + 로그인 + +

+ OneLife에 오신 것을 환영합니다 +

@@ -64,14 +110,10 @@ export default function LoginPage() { />
-
- - 비밀번호 찾기 - -
+ {error &&

{error}

} - @@ -80,7 +122,9 @@ export default function LoginPage() {
- 또는 + + 또는 +
@@ -95,15 +139,24 @@ export default function LoginPage() { color: "#000000", }} > - + 카카오 로그인
- 아직 회원이 아니신가요? - + + 아직 회원이 아니신가요?{" "} + + 회원가입
@@ -115,5 +168,5 @@ export default function LoginPage() {