Skip to content

Commit 25aa7ff

Browse files
authored
Merge pull request #261 from CSE-Shaco/develop
Refactor: endpoint 변경점 반영
2 parents ff50650 + c392c60 commit 25aa7ff

13 files changed

Lines changed: 755 additions & 110 deletions

File tree

src/app/api/auth/login/route.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { NextResponse } from 'next/server'
2+
import axios from 'axios'
3+
4+
import { rateLimit } from '@/lib/rate-limit'
5+
6+
const ORIGINAL_AUTH_URL = process.env.NEXT_PUBLIC_BASE_API_URL
7+
8+
interface LoginRequest {
9+
email: string
10+
password: string
11+
}
12+
13+
interface LoginResponse {
14+
error?: string
15+
[key: string]: any
16+
}
17+
18+
export async function POST(request: Request): Promise<NextResponse> {
19+
try {
20+
// Rate limiting 적용
21+
const limiter = rateLimit({
22+
interval: 60 * 1000, // 1분
23+
uniqueTokenPerInterval: 500
24+
})
25+
26+
try {
27+
await limiter.check(5, 'LOGIN_ATTEMPT') // 1분당 5회 시도 제한
28+
} catch {
29+
return NextResponse.json(
30+
{ error: '너무 많은 로그인 시도가 있었습니다. 잠시 후 다시 시도해주세요.' },
31+
{ status: 429 }
32+
)
33+
}
34+
35+
// 클라이언트로부터 받은 요청 데이터 추출
36+
const { email, password }: LoginRequest = await request.json()
37+
38+
// 입력값 검증
39+
if (!email || !password) {
40+
return NextResponse.json({ error: '이메일과 비밀번호를 모두 입력해주세요.' }, { status: 400 })
41+
}
42+
43+
if (!email.includes('@') || !email.includes('.')) {
44+
return NextResponse.json({ error: '유효한 이메일 주소를 입력해주세요.' }, { status: 400 })
45+
}
46+
47+
if (password.length < 8) {
48+
return NextResponse.json({ error: '비밀번호는 8자 이상이어야 합니다.' }, { status: 400 })
49+
}
50+
51+
const isProd = process.env.NODE_ENV === 'production'
52+
53+
// 기존 refresh_token 쿠키 삭제
54+
const response = NextResponse.json({})
55+
response.cookies.set('refresh_token', '', {
56+
path: '/',
57+
httpOnly: true,
58+
secure: isProd,
59+
sameSite: isProd ? 'none' : 'lax',
60+
domain: isProd ? '.gdgocinha.com' : undefined,
61+
expires: new Date(0)
62+
})
63+
64+
const authResponse = await axios.post(
65+
`${ORIGINAL_AUTH_URL}/auth/login`,
66+
{ email, password },
67+
{
68+
headers: { 'Content-Type': 'application/json' },
69+
withCredentials: true
70+
}
71+
)
72+
73+
const data = authResponse.data
74+
75+
const nextResponse = NextResponse.json(data, {
76+
status: authResponse.status,
77+
statusText: authResponse.statusText
78+
})
79+
80+
// 원본 응답의 쿠키가 있으면 추출하여 현재 도메인에 설정
81+
const cookies = authResponse.headers['set-cookie']
82+
if (cookies) {
83+
cookies.forEach((cookie: string) => {
84+
const cookieParts = cookie.split(';')[0].split('=')
85+
const cookieName = cookieParts[0]
86+
const cookieValue = cookieParts.slice(1).join('=')
87+
88+
nextResponse.cookies.set(cookieName, cookieValue, {
89+
path: '/',
90+
httpOnly: true,
91+
secure: isProd,
92+
sameSite: isProd ? 'none' : 'lax',
93+
domain: isProd ? '.gdgocinha.com' : undefined
94+
})
95+
})
96+
}
97+
98+
return nextResponse
99+
} catch (error: any) {
100+
console.error('로그인 프록시 오류:', error)
101+
102+
// 구체적인 에러 메시지 처리
103+
if (error.response) {
104+
switch (error.response.status) {
105+
case 401:
106+
return NextResponse.json(
107+
{ error: '이메일 또는 비밀번호가 올바르지 않습니다.' },
108+
{ status: 401 }
109+
)
110+
case 403:
111+
return NextResponse.json({ error: '접근이 거부되었습니다.' }, { status: 403 })
112+
case 404:
113+
return NextResponse.json({ error: '서비스를 찾을 수 없습니다.' }, { status: 404 })
114+
default:
115+
return NextResponse.json(
116+
{ error: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' },
117+
{ status: error.response.status }
118+
)
119+
}
120+
}
121+
122+
return NextResponse.json(
123+
{ error: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' },
124+
{ status: 500 }
125+
)
126+
}
127+
}

src/app/api/auth/signout/route.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { NextResponse } from 'next/server';
2-
import axios from 'axios';
1+
import { NextResponse } from 'next/server'
2+
import axios from 'axios'
33

4-
const ORIGINAL_AUTH_URL = process.env.NEXT_PUBLIC_BASE_API_URL;
4+
const ORIGINAL_AUTH_URL = process.env.NEXT_PUBLIC_BASE_API_URL
55

66
export async function POST(request: Request): Promise<NextResponse> {
77
try {
@@ -10,46 +10,46 @@ export async function POST(request: Request): Promise<NextResponse> {
1010
{},
1111
{
1212
headers: { 'Content-Type': 'application/json' },
13-
withCredentials: true,
13+
withCredentials: true
1414
}
15-
);
15+
)
1616

1717
// 응답 생성
1818
const nextResponse = NextResponse.json(
1919
{ message: '로그아웃이 완료되었습니다.' },
2020
{
2121
status: response.status,
22-
statusText: response.statusText,
22+
statusText: response.statusText
2323
}
24-
);
24+
)
2525

2626
// 쿠키 삭제
27-
const cookies = response.headers['set-cookie'];
27+
const cookies = response.headers['set-cookie']
2828
if (cookies) {
2929
cookies.forEach((cookie: string) => {
30-
const cookieParts = cookie.split(';')[0].split('=');
31-
const cookieName = cookieParts[0];
30+
const cookieParts = cookie.split(';')[0].split('=')
31+
const cookieName = cookieParts[0]
3232

3333
// 쿠키 삭제
34-
nextResponse.cookies.delete(cookieName);
35-
});
34+
nextResponse.cookies.delete(cookieName)
35+
})
3636
}
3737

38-
nextResponse.cookies.delete('refresh_token');
38+
nextResponse.cookies.delete('refresh_token')
3939

40-
return nextResponse;
40+
return nextResponse
4141
} catch (error: any) {
42-
console.error('로그아웃 프록시 오류:', error);
42+
console.error('로그아웃 프록시 오류:', error)
4343

4444
// 에러 응답 생성
4545
const errorResponse = NextResponse.json(
4646
{ error: '로그아웃 처리 중 오류가 발생했습니다.' },
4747
{ status: error.response?.status || 500 }
48-
);
48+
)
4949

5050
// 에러가 발생하더라도 클라이언트 측 쿠키는 삭제
51-
errorResponse.cookies.delete('refresh_token');
51+
errorResponse.cookies.delete('refresh_token')
5252

53-
return errorResponse;
53+
return errorResponse
5454
}
5555
}

src/app/login/layout.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { ReactNode } from 'react';
2+
3+
import Header2 from '@/components/ui/common/Header2';
4+
5+
export const metadata = {
6+
title: 'SignIn',
7+
description: 'SignIn to your account',
8+
};
9+
10+
export default function LoginLayout({ children }: { children: ReactNode }) {
11+
return (
12+
<div className='min-h-screen flex flex-col overflow-hidden relative'>
13+
<Header2 />
14+
{children}
15+
</div>
16+
);
17+
}

src/app/login/page.tsx

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
'use client';
2+
3+
import { type FormEvent, useMemo, useState } from 'react';
4+
import { useRouter, useSearchParams } from 'next/navigation';
5+
import Image from 'next/image';
6+
7+
import Loader from '@/components/ui/common/Loader';
8+
9+
import AuthLogin from '@/components/auth/screen/AuthLogin';
10+
import AuthFindId from '@/components/auth/screen/AuthFindId';
11+
import AuthResetPassword from '@/components/auth/screen/AuthResetPassword';
12+
import AuthResetRequest from '@/components/auth/screen/AuthResetRequest';
13+
14+
import { GoogleLogin } from '@/services/auth/signin/google/GoogleLogin';
15+
import { login } from '@/services/auth/signin/custom/CustomAuthApi';
16+
17+
import { useAuth } from '@/hooks/useAuth';
18+
19+
import loginBg from '@public/images/bgimg.png';
20+
21+
const DEFAULT_FALLBACK_ROUTE = '/main';
22+
23+
const getSafeNextUrl = (raw: string | null): string => {
24+
if (!raw) return DEFAULT_FALLBACK_ROUTE;
25+
26+
try {
27+
const decoded = decodeURIComponent(raw);
28+
return decoded.startsWith('/') ? decoded : DEFAULT_FALLBACK_ROUTE;
29+
} catch {
30+
return DEFAULT_FALLBACK_ROUTE;
31+
}
32+
};
33+
34+
export default function LoginPage() {
35+
const router = useRouter();
36+
const searchParams = useSearchParams();
37+
const { setAccessToken } = useAuth();
38+
const { handleGoogleLogin } = GoogleLogin();
39+
40+
const nextUrl = useMemo(
41+
() => getSafeNextUrl(searchParams?.get('next') ?? null),
42+
[searchParams],
43+
);
44+
45+
const [password, setPassword] = useState('');
46+
const [errors, setErrors] = useState<string[]>([]);
47+
const [isRendering, setIsRendering] = useState(0);
48+
const [loading, setLoading] = useState(false);
49+
const [verifiedEmail, setVerifiedEmail] = useState('');
50+
51+
const handleBackToLogin = () => setIsRendering(0);
52+
const handleFindIdClick = () => setIsRendering(1);
53+
const handleResetPasswordClick = () => setIsRendering(2);
54+
const handleBackToResetRequest = () => setIsRendering(2);
55+
const handleResetPasswordNext = (email: string) => {
56+
setVerifiedEmail(email);
57+
setIsRendering(3);
58+
};
59+
60+
const validatePassword = (value: string): string[] => {
61+
const newErrors: string[] = [];
62+
if (value.length <= 0) {
63+
newErrors.push('비밀번호를 입력해주세요.');
64+
}
65+
return newErrors;
66+
};
67+
68+
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
69+
e.preventDefault();
70+
71+
const formData = new FormData(e.currentTarget);
72+
const email = formData.get('email')?.toString() ?? '';
73+
const passwordErrors = validatePassword(password);
74+
75+
if (passwordErrors.length > 0) {
76+
setErrors(passwordErrors);
77+
return;
78+
}
79+
80+
setErrors([]);
81+
setLoading(true);
82+
83+
try {
84+
const res = await login(email, password);
85+
const { exists, access_token } = res.data.data;
86+
87+
if (!exists) {
88+
alert('아이디 혹은 비밀번호가 올바르지 않습니다.');
89+
setLoading(false);
90+
return;
91+
}
92+
93+
setAccessToken(access_token);
94+
95+
router.push(nextUrl);
96+
} catch (error) {
97+
console.error('로그인 실패:', error);
98+
alert('로그인 중 오류가 발생했습니다.');
99+
setLoading(false);
100+
}
101+
};
102+
103+
return (
104+
<>
105+
<Loader isLoading={loading} />
106+
<Image
107+
src={loginBg}
108+
alt='loginBg'
109+
fill
110+
className='absolute top-0 left-0 -z-10 object-cover opacity-70 blur-sm'
111+
/>
112+
<div className='flex justify-center items-center flex-1 relative'>
113+
<div
114+
key='screen1'
115+
className={`absolute w-full transition-all duration-500 ease-in-out transform ${
116+
isRendering === 0 ? 'translate-x-0 opacity-100' : '-translate-x-full opacity-0'
117+
} flex justify-center items-center`}
118+
>
119+
<AuthLogin
120+
router={router}
121+
onSubmit={onSubmit}
122+
errors={errors}
123+
password={password}
124+
setPassword={setPassword}
125+
setErrors={setErrors}
126+
handleGoogleLogin={() => handleGoogleLogin({ next: nextUrl })}
127+
handleFindIdClick={handleFindIdClick}
128+
handleResetPasswordClick={handleResetPasswordClick}
129+
/>
130+
</div>
131+
132+
<div
133+
key='screen2'
134+
className={`absolute w-full transition-all duration-500 ease-in-out transform ${
135+
isRendering === 1 ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
136+
} flex justify-center items-center mt-[-30px]`}
137+
>
138+
<AuthFindId handleBackToLogin={handleBackToLogin} />
139+
</div>
140+
141+
<div
142+
key='screen3'
143+
className={`absolute w-full transition-all duration-500 ease-in-out transform ${
144+
isRendering === 2
145+
? 'translate-x-0 opacity-100'
146+
: `${isRendering === 3 ? '-translate-x-full' : 'translate-x-full'} opacity-0`
147+
} flex justify-center items-center`}
148+
>
149+
<AuthResetRequest
150+
handleNextStep={handleResetPasswordNext}
151+
handleBackToLogin={handleBackToLogin}
152+
setLoading={setLoading}
153+
/>
154+
</div>
155+
156+
<div
157+
key='screen4'
158+
className={`absolute w-full transition-all duration-500 ease-in-out transform ${
159+
isRendering === 3 ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
160+
} flex justify-center items-center`}
161+
>
162+
<AuthResetPassword
163+
email={verifiedEmail}
164+
handleBackToLogin={handleBackToLogin}
165+
handleBackToResetRequest={handleBackToResetRequest}
166+
setLoading={setLoading}
167+
/>
168+
</div>
169+
</div>
170+
</>
171+
);
172+
}

0 commit comments

Comments
 (0)