Skip to content

Commit 2e724f4

Browse files
ahk0413EunbinJung
authored andcommitted
[fix, refactor] 스크롤 커스텀 및 토스트/로그인 로직 및 훅 분리 (#84)
* [fix] next이미지 경고 수정 및 챗봇 챗 로딩 위치 이동 * [style] 전체페이지 스크롤 커스텀 * [fix] toast 훅으로 변경, 로그아웃 로직 훅으로 변경 * [refactor] 훅 분리 * [docs] useToast 훅 폴더 경로 변경 * [docs] 폴더 명 변경
1 parent 247810d commit 2e724f4

File tree

12 files changed

+515
-10
lines changed

12 files changed

+515
-10
lines changed

src/app/design-system/page.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import Button from '@/shared/components/button/Button';
44
import TextButton from '@/shared/components/button/TextButton';
55
import Input from '@/shared/components/Input-box/Input';
66
import { useState } from 'react';
7-
import { customToast } from '@/shared/components/toast/CustomToastUtils';
87
import ModalLayout from '@/shared/components/modal-pop/ModalLayout';
98
import SelectBox from '@/domains/shared/components/select-box/SelectBox';
109

@@ -13,10 +12,12 @@ import LikeBtn from '@/domains/community/components/like/LikeBtn';
1312
import Share from '@/domains/shared/components/share/Share';
1413
import Keep from '@/domains/shared/components/keep/Keep';
1514
import ConfirmModal from '@/shared/components/modal-pop/ConfirmModal';
15+
import { useToast } from '@/shared/hook/useToast';
1616

1717
function Page() {
1818
const [isModalOpen, setModalOpen] = useState(false);
1919
const [isConfirmOpen, setConfirmOpen] = useState(false);
20+
const { toastSuccess, toastInfo, toastError } = useToast();
2021

2122
return (
2223
<div className="p-6 space-y-6 bg-primary">
@@ -78,21 +79,21 @@ function Page() {
7879
<div className="flex gap-2">
7980
<button
8081
className="px-4 py-2 bg-green-300 text-black rounded"
81-
onClick={() => customToast.success('성공 메시지 \n 줄바꿈은 이렇게')}
82+
onClick={() => toastSuccess('성공 메시지 \n 줄바꿈은 이렇게')}
8283
>
8384
Success Toast
8485
</button>
8586

8687
<button
8788
className="px-4 py-2 bg-yellow-100 text-black rounded"
88-
onClick={() => customToast.info('정보 메시지')}
89+
onClick={() => toastInfo('정보 메시지')}
8990
>
9091
Info Toast
9192
</button>
9293

9394
<button
9495
className="px-4 py-2 bg-red-200 text-black rounded"
95-
onClick={() => customToast.error('오류 메시지')}
96+
onClick={() => toastError('오류 메시지')}
9697
>
9798
Error Toast
9899
</button>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use client';
2+
3+
import Spinner from '@/shared/components/spinner/Spinner';
4+
import WelcomeModal from '@/domains/login/components/WelcomeModal';
5+
import { useLoginRedirect } from '../hook/useAuthHooks';
6+
7+
function LoginRedirectHandler() {
8+
const { loading, welcomeModalOpen, handleCloseWelcomeModal, user } = useLoginRedirect();
9+
10+
if (loading) {
11+
return (
12+
<div className="page-layout max-w-824 flex-center">
13+
<Spinner />
14+
</div>
15+
);
16+
}
17+
18+
return (
19+
<>
20+
{/* 첫 유저 모달 */}
21+
{user && (
22+
<WelcomeModal
23+
userNickname={user.nickname}
24+
open={welcomeModalOpen}
25+
onClose={handleCloseWelcomeModal}
26+
/>
27+
)}
28+
</>
29+
);
30+
}
31+
export default LoginRedirectHandler;

src/domains/login/components/LogoutConfirm.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import ConfirmModal from '@/shared/components/modal-pop/ConfirmModal';
2+
import { useLogout } from '../hook/useAuthHooks';
23

34
interface Props {
45
open: boolean;
56
onClose: () => void;
6-
onLogout: () => void;
77
}
88

9-
function LogoutConfirm({ open, onClose, onLogout }: Props) {
9+
function LogoutConfirm({ open, onClose }: Props) {
10+
const logoutHandler = useLogout();
11+
1012
return (
1113
<ConfirmModal
1214
open={open}
1315
onClose={onClose}
1416
description="정말 로그아웃 하시겠어요?"
15-
onConfirm={onLogout}
17+
onConfirm={async () => {
18+
await logoutHandler();
19+
onClose();
20+
}}
1621
onCancel={onClose}
1722
/>
1823
);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useAuthStore } from '@/domains/shared/store/auth';
2+
import { useCallback } from 'react';
3+
import { useEffect, useState } from 'react';
4+
import { usePathname, useRouter } from 'next/navigation';
5+
import { getCookie, removeCookie } from '@/domains/shared/auth/utils/cookie';
6+
import { useToast } from '@/shared/hook/useToast';
7+
8+
export const useLogout = () => {
9+
const logout = useAuthStore((state) => state.logout);
10+
const { toastSuccess, toastError } = useToast();
11+
12+
const handleLogout = useCallback(async () => {
13+
try {
14+
await logout();
15+
toastSuccess('로그아웃 되었습니다.');
16+
} catch (err) {
17+
console.error('로그아웃 실패', err);
18+
toastError('로그아웃 실패 ❌ 다시 시도해주세요.');
19+
}
20+
}, [logout, toastSuccess, toastError]);
21+
22+
return handleLogout;
23+
};
24+
25+
export const useLoginRedirect = () => {
26+
const router = useRouter();
27+
const pathname = usePathname();
28+
const { user, updateUser } = useAuthStore();
29+
const { toastSuccess } = useToast();
30+
31+
const [loading, setLoading] = useState(true);
32+
const [welcomeModalOpen, setWelcomeModalOpen] = useState(false);
33+
34+
useEffect(() => {
35+
if (!user && loading) {
36+
updateUser()
37+
.then((fetchedUser) => {
38+
if (!fetchedUser) router.replace('/login');
39+
})
40+
.catch(() => router.replace('/login'))
41+
.finally(() => setLoading(false));
42+
} else {
43+
setLoading(false);
44+
}
45+
}, [user, loading, updateUser, router]);
46+
47+
useEffect(() => {
48+
if (!user || loading) return;
49+
50+
const preLoginPath = getCookie('preLoginPath') || '/';
51+
52+
if (user && preLoginPath === '/login') {
53+
router.replace('/');
54+
removeCookie('preLoginPath');
55+
return;
56+
}
57+
58+
if (pathname.startsWith('/login/user/first-user')) {
59+
setWelcomeModalOpen(true);
60+
} else if (pathname.startsWith('/login/user/success')) {
61+
toastSuccess(`${user.nickname}님 \n 로그인 성공 🎉`);
62+
router.replace(preLoginPath);
63+
removeCookie('preLoginPath');
64+
}
65+
}, [pathname, user, loading, router, toastSuccess]);
66+
67+
const handleCloseWelcomeModal = () => {
68+
setWelcomeModalOpen(false);
69+
const preLoginPath = getCookie('preLoginPath') || '/';
70+
removeCookie('preLoginPath');
71+
router.replace(preLoginPath);
72+
};
73+
74+
return { loading, welcomeModalOpen, handleCloseWelcomeModal, user };
75+
};

src/domains/recommend/components/BotMessage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Image from 'next/image';
55
import { useState } from 'react';
66
import BotCocktailCard from './BotCocktailCard';
77
import BotOptions from './BotOptions';
8+
import TypingIndicator from './TypingIndicator';
89

910
interface Message {
1011
id: string;
@@ -82,6 +83,7 @@ function BotMessage() {
8283
)}
8384
</div>
8485
))}
86+
<TypingIndicator />
8587
</div>
8688
</article>
8789
);

src/domains/recommend/components/ChatSection.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useState } from 'react';
44
import BotMessage from './BotMessage';
55
import UserMessage from './UserMessage';
66
import MessageInput from './MessageInput';
7-
import TypingIndicator from './TypingIndicator';
87

98
function ChatSection() {
109
const [messages, setMessages] = useState<string[]>([]);
@@ -18,7 +17,7 @@ function ChatSection() {
1817
<h2 className="sr-only">대화 목록 및 입력 창</h2>
1918
<div className="flex flex-col gap-10 pb-20">
2019
<BotMessage />
21-
<TypingIndicator />
20+
2221
{messages.map((msg, i) => (
2322
<UserMessage key={i} message={msg} />
2423
))}

src/domains/recommend/components/TypingIndicator.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ function TypingIndicator() {
66
<div className="relative flex items-center w-fit p-3 rounded-2xl rounded-tl-none bg-white text-black overflow-hidden">
77
<p className="inline-block animate-fade-in">준비 중…</p>
88
<div className="relative w-10 h-10 animate-shake">
9-
<Image src={shaker} alt="Cocktail Shaker" fill className="object-contain" priority />
9+
<Image
10+
src={shaker}
11+
alt=""
12+
width={40}
13+
height={40}
14+
className="object-contain"
15+
priority
16+
aria-hidden
17+
/>
1018
</div>
1119
</div>
1220
);

src/domains/shared/store/auth.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { create } from 'zustand';
2+
import { persist } from 'zustand/middleware';
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>()(
25+
persist(
26+
(set) => ({
27+
user: null,
28+
accessToken: null,
29+
isLoggedIn: false,
30+
31+
loginWithProvider: (provider) => {
32+
window.location.href = `http://localhost:8080/oauth2/authorization/${provider}`;
33+
},
34+
35+
setUser: (user, token) => {
36+
const updatedUser = { ...user, abv_degree: 5.0 };
37+
set({ user: updatedUser, accessToken: token, isLoggedIn: true });
38+
},
39+
40+
logout: async () => {
41+
try {
42+
await fetch('http://localhost:8080/user/auth/logout', {
43+
method: 'POST',
44+
credentials: 'include',
45+
});
46+
set({ user: null, accessToken: null, isLoggedIn: false });
47+
} catch (err) {
48+
console.error('로그아웃 실패', err);
49+
}
50+
},
51+
52+
updateUser: async () => {
53+
try {
54+
const res = await fetch('http://localhost:8080/user/auth/refresh', {
55+
method: 'POST',
56+
credentials: 'include',
57+
headers: { 'Content-Type': 'application/json' },
58+
});
59+
60+
if (!res.ok) throw new Error('토큰 갱신 실패');
61+
const data = await res.json();
62+
const userInfo = data?.data?.user;
63+
const accessToken = data?.data?.accessToken;
64+
65+
if (userInfo && accessToken) {
66+
set({ user: userInfo, accessToken, isLoggedIn: true });
67+
return userInfo;
68+
}
69+
return null;
70+
} catch (err) {
71+
console.error('updateUser 실패', err);
72+
set({ accessToken: null, user: null, isLoggedIn: false });
73+
return null;
74+
}
75+
},
76+
}),
77+
{ name: 'auth-storage' } // localStorage key
78+
)
79+
);

0 commit comments

Comments
 (0)