Skip to content

Commit 598474f

Browse files
committed
feat(front) : 로그인 화면 구현
1 parent 3a91a8e commit 598474f

File tree

6 files changed

+193
-31
lines changed

6 files changed

+193
-31
lines changed
Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,69 @@
11
// 로그인 페이지
22
'use client';
3-
import Form from '@/components/Form';
4-
import { http } from '@/lib/api/client';
5-
import type { LoginRequest, LoginResponse } from '@/lib/types';
3+
import { useState, FormEvent } from 'react';
4+
import { authApi } from '@/lib/api/auth';
5+
import type { LoginRequest } from '@/types/auth';
66
import { useToast } from '@/components/ui/Toast';
77
import { useAuth } from '@/hooks/auth/useAuth';
88
import { useRouter } from 'next/navigation';
9+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
10+
import { Label } from '@/components/ui/label';
11+
import { Input } from '@/components/ui/input';
12+
import { Button } from '@/components/ui/Button';
913

1014
export default function LoginPage() {
1115
const toast = useToast();
1216
const auth = useAuth();
1317
const router = useRouter();
18+
const [username, setUsername] = useState('');
19+
const [password, setPassword] = useState('');
20+
const [loading, setLoading] = useState(false);
1421

15-
async function submit(values: Record<string,string>) {
16-
const payload: LoginRequest = { username: values.username, password: values.password };
22+
async function submit(e: FormEvent) {
23+
e.preventDefault();
24+
const payload: LoginRequest = { username, password };
1725
try {
18-
const res = await http.post<LoginResponse>('/api/v1/auth/login', payload, 'cookie'); // 쿠키 세션 예시
26+
setLoading(true);
27+
const res = await authApi.login(payload);
1928
if (res.accessToken) {
2029
auth.loginWithToken(res.accessToken);
2130
}
2231
toast.push(res.message ?? '로그인 성공');
2332
router.push('/');
2433
} catch (e: any) {
2534
toast.push(`로그인 실패: ${e.message}`);
35+
} finally {
36+
setLoading(false);
2637
}
2738
}
2839

2940
return (
30-
<section>
31-
<h1>로그인</h1>
32-
<Form
33-
fields={[
34-
{ name: 'username', label: '아이디' },
35-
{ name: 'password', label: '비밀번호', type: 'password' },
36-
]}
37-
onSubmit={submit}
38-
submitText="로그인"
39-
/>
40-
<p style={{marginTop:12, opacity:.7}}>스프링 엔드포인트: POST /api/v1/auth/login</p>
41-
</section>
41+
<div className="min-h-screen bg-background">
42+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
43+
<div className="mx-auto max-w-md">
44+
<Card className="shadow-lg">
45+
<CardHeader>
46+
<CardTitle className="text-2xl">로그인</CardTitle>
47+
</CardHeader>
48+
<CardContent>
49+
<form className="space-y-6" onSubmit={submit}>
50+
<div className="space-y-2">
51+
<Label htmlFor="username">아이디</Label>
52+
<Input id="username" value={username} onChange={(e) => setUsername(e.target.value)} required />
53+
</div>
54+
<div className="space-y-2">
55+
<Label htmlFor="password">비밀번호</Label>
56+
<Input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
57+
</div>
58+
<Button type="submit" className="w-full" size="lg" disabled={loading}>
59+
{loading ? '처리중...' : '로그인'}
60+
</Button>
61+
<p className="text-xs text-muted-foreground">스프링 엔드포인트: POST /api/v1/auth/login</p>
62+
</form>
63+
</CardContent>
64+
</Card>
65+
</div>
66+
</div>
67+
</div>
4268
);
4369
}
Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,78 @@
1-
// 회원가입 페이지
1+
// 회원가입 페이지
2+
'use client';
3+
import { useState, FormEvent } from 'react';
4+
import { authApi } from '@/lib/api/auth';
5+
import type { SignupRequest } from '@/types/auth';
6+
import { useToast } from '@/components/ui/Toast';
7+
import { useRouter } from 'next/navigation';
8+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
9+
import { Label } from '@/components/ui/label';
10+
import { Input } from '@/components/ui/input';
11+
import { Button } from '@/components/ui/Button';
12+
13+
export default function SignupPage() {
14+
const toast = useToast();
15+
const router = useRouter();
16+
const [username, setUsername] = useState('');
17+
const [email, setEmail] = useState('');
18+
const [password, setPassword] = useState('');
19+
const [confirmPassword, setConfirmPassword] = useState('');
20+
const [loading, setLoading] = useState(false);
21+
22+
async function submit(e: FormEvent) {
23+
e.preventDefault();
24+
if (password !== confirmPassword) {
25+
toast.push('비밀번호가 일치하지 않습니다.');
26+
return;
27+
}
28+
const payload: SignupRequest = { username, email, password, confirmPassword };
29+
try {
30+
setLoading(true);
31+
const res = await authApi.signup(payload);
32+
toast.push(res.message ?? '회원가입 성공');
33+
router.push('/login');
34+
} catch (e: any) {
35+
toast.push(`회원가입 실패: ${e.message}`);
36+
} finally {
37+
setLoading(false);
38+
}
39+
}
40+
41+
return (
42+
<div className="min-h-screen bg-background">
43+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
44+
<div className="mx-auto max-w-md">
45+
<Card className="shadow-lg">
46+
<CardHeader>
47+
<CardTitle className="text-2xl">회원가입</CardTitle>
48+
</CardHeader>
49+
<CardContent>
50+
<form className="space-y-6" onSubmit={submit}>
51+
<div className="space-y-2">
52+
<Label htmlFor="username">아이디</Label>
53+
<Input id="username" value={username} onChange={(e) => setUsername(e.target.value)} required />
54+
</div>
55+
<div className="space-y-2">
56+
<Label htmlFor="email">이메일</Label>
57+
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
58+
</div>
59+
<div className="space-y-2">
60+
<Label htmlFor="password">비밀번호</Label>
61+
<Input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
62+
</div>
63+
<div className="space-y-2">
64+
<Label htmlFor="confirmPassword">비밀번호 확인</Label>
65+
<Input id="confirmPassword" type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} required />
66+
</div>
67+
<Button type="submit" className="w-full" size="lg" disabled={loading}>
68+
{loading ? '처리중...' : '회원가입'}
69+
</Button>
70+
<p className="text-xs text-muted-foreground">스프링 엔드포인트: POST /api/v1/auth/signup</p>
71+
</form>
72+
</CardContent>
73+
</Card>
74+
</div>
75+
</div>
76+
</div>
77+
);
78+
}

front/src/app/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default function LandingPage() {
1212
const handleStartAnalysis = () => {
1313
if (!isAuthed) {
1414
alert("로그인해주세요")
15+
router.push("/login")
1516
return
1617
}
1718
router.push("/analysis")
@@ -20,6 +21,7 @@ export default function LandingPage() {
2021
const handleGoCommunity = () => {
2122
if (!isAuthed) {
2223
alert("로그인해주세요")
24+
router.push("/login")
2325
return
2426
}
2527
router.push("/community")

front/src/components/Header.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
"use client"
22

33
import Link from "next/link"
4-
import { useState } from "react"
54
import { useRouter } from "next/navigation"
65
import { Sparkles } from "lucide-react"
76
import { Button } from "@/components/ui/Button"
87
import { useAuth } from "@/hooks/auth/useAuth"
98

109
export default function Header() {
11-
const [isLoggedIn, setIsLoggedIn] = useState(false)
1210
const router = useRouter()
13-
const { isAuthed } = useAuth()
11+
const { isAuthed, logout } = useAuth()
1412

1513
const guardNav = (path: string) => () => {
1614
if (!isAuthed) {
1715
alert("로그인해주세요")
16+
router.push("/login")
1817
return
1918
}
2019
router.push(path)
@@ -38,7 +37,7 @@ export default function Header() {
3837

3938
{/* 메뉴 */}
4039
<div className="flex items-center gap-6">
41-
{isLoggedIn ? (
40+
{isAuthed ? (
4241
<>
4342
<button onClick={guardNav("/history")} className="text-sm text-muted-foreground hover:text-foreground transition-colors">
4443
히스토리
@@ -49,11 +48,10 @@ export default function Header() {
4948
<button onClick={guardNav("/settings")} className="text-sm text-muted-foreground hover:text-foreground transition-colors">
5049
마이페이지
5150
</button>
52-
{/* 로그아웃 구현 필요요 */}
5351
<Button
5452
variant="ghost"
5553
size="sm"
56-
onClick={() => setIsLoggedIn(false)}
54+
onClick={logout}
5755
className="text-muted-foreground hover:text-foreground"
5856
>
5957
로그아웃
@@ -66,12 +64,10 @@ export default function Header() {
6664
</button>
6765

6866
<div className="flex items-center gap-3">
69-
{/* 현재는 로그인 누르면 바로 로그인 상태로 변경 중 */}
70-
{/* 로그인 누르면 로그인 화면으로, 시작하기 누르면 회원가입으로 넘어가야 함함 */}
71-
<Button variant="ghost" size="sm" onClick={() => setIsLoggedIn(true)}>
67+
<Button variant="ghost" size="sm" onClick={() => router.push("/login")}>
7268
로그인
7369
</Button>
74-
<Button size="sm" className="bg-primary text-primary-foreground hover:bg-primary/90" onClick={guardNav("/signup")}>
70+
<Button size="sm" className="bg-primary text-primary-foreground hover:bg-primary/90" onClick={() => router.push("/signup")}>
7571
시작하기
7672
</Button>
7773
</div>

front/src/lib/api/auth.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,33 @@
1-
// user 도메인 API
1+
// auth 도메인 API
2+
import { http } from './client'
3+
import type { LoginRequest, LoginResponse, SignupRequest, SignupResponse } from '@/types/auth'
4+
5+
export const authApi = {
6+
/**
7+
* 로그인
8+
* POST /api/v1/auth/login
9+
*/
10+
login: (data: LoginRequest): Promise<LoginResponse> =>
11+
http.post('/api/v1/auth/login', data, 'cookie'),
12+
13+
/**
14+
* 회원가입
15+
* POST /api/v1/auth/signup
16+
*/
17+
signup: (data: SignupRequest): Promise<SignupResponse> =>
18+
http.post('/api/v1/auth/signup', data, 'none'),
19+
20+
/**
21+
* 로그아웃
22+
* POST /api/v1/auth/logout
23+
*/
24+
logout: (): Promise<void> =>
25+
http.post('/api/v1/auth/logout'),
26+
27+
/**
28+
* 토큰 갱신
29+
* POST /api/v1/auth/refresh
30+
*/
31+
refreshToken: (): Promise<LoginResponse> =>
32+
http.post('/api/v1/auth/refresh'),
33+
}

front/src/types/auth.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,30 @@
1-
// user 도메인 타입
1+
// auth 도메인 타입
2+
export interface LoginRequest {
3+
username: string;
4+
password: string;
5+
}
6+
7+
export interface SignupRequest {
8+
username: string;
9+
email: string;
10+
password: string;
11+
confirmPassword: string;
12+
}
13+
14+
export interface LoginResponse {
15+
accessToken: string;
16+
refreshToken?: string;
17+
message?: string;
18+
}
19+
20+
export interface SignupResponse {
21+
message: string;
22+
userId?: number;
23+
}
24+
25+
export interface User {
26+
id: number;
27+
username: string;
28+
email: string;
29+
createdAt: string;
30+
}

0 commit comments

Comments
 (0)