Skip to content

Commit 8b81b23

Browse files
authored
Merge pull request #60 from prgrms-web-devcourse-final-project/feat/login#51
[test] 로그인 테스트를 위해 잠시 올림
2 parents d27bd2a + 15b2233 commit 8b81b23

File tree

16 files changed

+287
-69
lines changed

16 files changed

+287
-69
lines changed

src/app/design-system/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ function Page() {
7777
<div className="flex gap-2">
7878
<button
7979
className="px-4 py-2 bg-green-300 text-black rounded"
80-
onClick={() => customToast.success('성공 메시지')}
80+
onClick={() => customToast.success('성공 메시지 \n 줄바꿈은 이렇게')}
8181
>
8282
Success Toast
8383
</button>

src/app/login/SocialLogin.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ import Naver from '@/shared/assets/icons/naver.svg';
44
import Kakao from '@/shared/assets/icons/kakao.svg';
55
import Google from '@/shared/assets/icons/google.svg';
66
import tw from '@/shared/utills/tw';
7-
import Welcome from './Welcome';
8-
import { useState } from 'react';
7+
import { useAuthStore } from '@/shared/@store/auth';
98

109
function SocialLogin() {
11-
const [isModalOpen, setIsModalOpen] = useState(false);
10+
const { loginWithProvider } = useAuthStore();
1211

1312
const socialButtons = [
1413
{
@@ -33,8 +32,7 @@ function SocialLogin() {
3332

3433
// TODO: 백엔드 연동 로직 구현 필요
3534
const handleLogin = (id: string) => {
36-
console.log(id);
37-
setIsModalOpen(true);
35+
loginWithProvider(id as 'naver' | 'kakao' | 'google');
3836
};
3937

4038
return (
@@ -52,9 +50,6 @@ function SocialLogin() {
5250
</button>
5351
))}
5452
</div>
55-
56-
{/* 웰컴 모달 (임시) */}
57-
<Welcome open={isModalOpen} onClose={() => setIsModalOpen(false)} />
5853
</>
5954
);
6055
}

src/app/login/first-user/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import LoginRedirectHandler from '@/shared/components/auth/LoginRedirectHandler';
2+
3+
function Page() {
4+
return <LoginRedirectHandler />;
5+
}
6+
7+
export default Page;

src/app/login/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ function Page() {
1515
className="absolute bottom-0 left-1/2 -translate-x-1/2 w-full max-w-[75rem] h-full -z-10"
1616
aria-hidden
1717
>
18-
<Image src={loginBg} alt="" fill className="object-cover md:object-contain object-bottom" />
18+
<Image
19+
src={loginBg}
20+
alt=""
21+
fill
22+
priority
23+
className="object-cover md:object-contain object-bottom"
24+
/>
1925
</div>
2026

2127
<div className="flex flex-col gap-3 text-center">

src/app/login/success/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import LoginRedirectHandler from '@/shared/components/auth/LoginRedirectHandler';
2+
3+
function Page() {
4+
return <LoginRedirectHandler />;
5+
}
6+
export default Page;

src/shared/@store/auth.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { create } from 'zustand';
2+
import { customToast } from '../components/toast/CustomToastUtils';
3+
4+
interface User {
5+
id: string;
6+
email: string;
7+
nickname: string;
8+
isFirstLogin: boolean;
9+
abv_degree?: number;
10+
provider?: 'naver' | 'kakao' | 'google';
11+
}
12+
13+
interface AuthState {
14+
user: User | null;
15+
accessToken: string | null;
16+
isLoggedIn: boolean;
17+
setUser: (user: User, token: string) => void;
18+
logout: () => Promise<void>;
19+
loginWithProvider: (provider: User['provider']) => void;
20+
21+
updateUser: () => Promise<User | null>;
22+
}
23+
24+
export const useAuthStore = create<AuthState>((set) => ({
25+
user: null,
26+
accessToken: null,
27+
isLoggedIn: false,
28+
29+
loginWithProvider: (provider) => {
30+
window.location.href = `http://localhost:8080/oauth2/authorization/${provider}`;
31+
},
32+
33+
setUser: (user, token) => {
34+
const updatedUser = { ...user, abv_degree: 5.0 };
35+
set({ user: updatedUser, accessToken: token, isLoggedIn: true });
36+
37+
customToast.success(`${updatedUser.nickname}님, 로그인 성공 🎉`);
38+
},
39+
40+
logout: async () => {
41+
try {
42+
await fetch('http://localhost:8080/user/auth/logout', {
43+
method: 'POST',
44+
credentials: 'include',
45+
});
46+
47+
customToast.success('로그아웃 되었습니다.');
48+
set({ user: null, accessToken: null, isLoggedIn: false });
49+
} catch (err) {
50+
customToast.error('로그아웃 실패❌ \n 다시 시도해주세요.');
51+
console.error('로그아웃 실패', err);
52+
}
53+
},
54+
55+
updateUser: async () => {
56+
try {
57+
const res = await fetch('http://localhost:8080/user/auth/refresh', {
58+
method: 'POST',
59+
credentials: 'include',
60+
headers: { 'Content-Type': 'application/json' },
61+
});
62+
63+
if (!res.ok) throw new Error('토큰 갱신 실패');
64+
const data = await res.json();
65+
66+
console.log('updateUser response:', data);
67+
const userInfo = data?.data?.user;
68+
const accessToken = data?.data?.accessToken;
69+
70+
if (userInfo && accessToken) {
71+
set({ user: userInfo, accessToken, isLoggedIn: true });
72+
console.log('토큰 및 유저 정보 갱신 완료:', userInfo);
73+
return userInfo;
74+
}
75+
76+
return null;
77+
} catch (err) {
78+
console.error('updateUser 실패', err);
79+
set({ accessToken: null, user: null, isLoggedIn: false });
80+
return null;
81+
}
82+
},
83+
}));

src/shared/@store/modal.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { create } from 'zustand';
2+
3+
interface WelcomeModalData {
4+
open: boolean;
5+
nickname: string;
6+
}
7+
8+
interface LogoutConfirmModalData {
9+
open: boolean;
10+
}
11+
12+
interface ModalStore {
13+
welcomeModal: WelcomeModalData;
14+
logoutConfirmModal: LogoutConfirmModalData;
15+
16+
openWelcomeModal: (nickname: string) => void;
17+
closeWelcomeModal: () => void;
18+
19+
openLogoutConfirmModal: () => void;
20+
closeLogoutConfirmModal: () => void;
21+
}
22+
23+
export const useModalStore = create<ModalStore>((set) => ({
24+
welcomeModal: { open: false, nickname: '' },
25+
logoutConfirmModal: { open: false },
26+
27+
openWelcomeModal: (nickname: string) => set({ welcomeModal: { open: true, nickname } }),
28+
29+
closeWelcomeModal: () => set({ welcomeModal: { open: false, nickname: '' } }),
30+
31+
openLogoutConfirmModal: () => set({ logoutConfirmModal: { open: true } }),
32+
closeLogoutConfirmModal: () => set({ logoutConfirmModal: { open: false } }),
33+
}));
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
function LoginConfirm() {
2+
return <div>LoginConfirm</div>;
3+
}
4+
export default LoginConfirm;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { usePathname, useRouter } from 'next/navigation';
5+
import { useAuthStore } from '@/shared/@store/auth';
6+
import { useModalStore } from '@/shared/@store/modal';
7+
import { customToast } from '@/shared/components/toast/CustomToastUtils';
8+
import Spinner from '../spinner/Spinner';
9+
10+
function LoginRedirectHandler() {
11+
const pathname = usePathname();
12+
const router = useRouter();
13+
const { user, updateUser } = useAuthStore();
14+
const { openWelcomeModal } = useModalStore();
15+
const [loading, setLoading] = useState(true);
16+
17+
useEffect(() => {
18+
if (!user && loading) {
19+
updateUser()
20+
.then((fetchedUser) => {
21+
if (!fetchedUser) {
22+
router.replace('/login');
23+
}
24+
})
25+
.catch(() => {
26+
router.replace('/login');
27+
})
28+
.finally(() => setLoading(false));
29+
}
30+
}, [user, loading, updateUser, router]);
31+
32+
useEffect(() => {
33+
if (!user || loading) return;
34+
35+
const preLoginPath = sessionStorage.getItem('preLoginPath') || '/';
36+
37+
if (pathname.startsWith('/login/first-user')) {
38+
openWelcomeModal(user.nickname);
39+
} else if (pathname.startsWith('/login/success')) {
40+
customToast.success(`${user.nickname}님 \n 로그인 성공 🎉`);
41+
router.replace(preLoginPath);
42+
}
43+
}, [pathname, user, router, openWelcomeModal, loading]);
44+
45+
if (loading) {
46+
return (
47+
<div className="page-layout max-w-824 flex-center">
48+
<Spinner />
49+
</div>
50+
);
51+
}
52+
53+
return null;
54+
}
55+
export default LoginRedirectHandler;

src/app/login/Welcome.tsx renamed to src/shared/components/auth/Welcome.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,29 @@ import Button from '@/shared/components/button/Button';
66
import ModalLayout from '@/shared/components/modalPop/ModalLayout';
77
import Ssury from '@/shared/assets/ssury/ssury_jump.webp';
88
import { useRouter } from 'next/navigation';
9+
import { useModalStore } from '@/shared/@store/modal';
10+
import { useAuthStore } from '@/shared/@store/auth';
911

10-
interface Props {
11-
open: boolean;
12-
onClose: () => void;
13-
}
14-
15-
function Welcome({ open, onClose }: Props) {
12+
function Welcome() {
1613
const router = useRouter();
14+
const { user } = useAuthStore();
15+
const { welcomeModal, closeWelcomeModal } = useModalStore();
16+
17+
if (!welcomeModal.open || !user) return null;
1718

1819
return (
1920
<ModalLayout
20-
open={open}
21-
onClose={onClose}
22-
title="환영합니다!"
21+
open={welcomeModal.open}
22+
onClose={closeWelcomeModal}
23+
title={`환영합니다, ${user.nickname}님!`}
2324
description="바텐더 쑤리가 안내해드릴게요"
2425
buttons={
2526
<>
2627
<Button
2728
type="button"
2829
color="purple"
2930
onClick={() => {
30-
onClose();
31+
closeWelcomeModal();
3132
router.push('/recipe');
3233
}}
3334
>
@@ -36,7 +37,7 @@ function Welcome({ open, onClose }: Props) {
3637
<Button
3738
type="button"
3839
onClick={() => {
39-
onClose();
40+
closeWelcomeModal();
4041
router.push('/recommend');
4142
}}
4243
>

0 commit comments

Comments
 (0)