diff --git a/src/app/design-system/page.tsx b/src/app/design-system/page.tsx index fcdfd70..ad3e5cb 100644 --- a/src/app/design-system/page.tsx +++ b/src/app/design-system/page.tsx @@ -77,7 +77,7 @@ function Page() {
diff --git a/src/app/login/SocialLogin.tsx b/src/app/login/SocialLogin.tsx index 4a69d93..1d60cbf 100644 --- a/src/app/login/SocialLogin.tsx +++ b/src/app/login/SocialLogin.tsx @@ -4,11 +4,10 @@ import Naver from '@/shared/assets/icons/naver.svg'; import Kakao from '@/shared/assets/icons/kakao.svg'; import Google from '@/shared/assets/icons/google.svg'; import tw from '@/shared/utills/tw'; -import Welcome from './Welcome'; -import { useState } from 'react'; +import { useAuthStore } from '@/shared/@store/auth'; function SocialLogin() { - const [isModalOpen, setIsModalOpen] = useState(false); + const { loginWithProvider } = useAuthStore(); const socialButtons = [ { @@ -33,8 +32,7 @@ function SocialLogin() { // TODO: 백엔드 연동 로직 구현 필요 const handleLogin = (id: string) => { - console.log(id); - setIsModalOpen(true); + loginWithProvider(id as 'naver' | 'kakao' | 'google'); }; return ( @@ -52,9 +50,6 @@ function SocialLogin() { ))}
- - {/* 웰컴 모달 (임시) */} - setIsModalOpen(false)} /> ); } diff --git a/src/app/login/first-user/page.tsx b/src/app/login/first-user/page.tsx new file mode 100644 index 0000000..7f902a4 --- /dev/null +++ b/src/app/login/first-user/page.tsx @@ -0,0 +1,7 @@ +import LoginRedirectHandler from '@/shared/components/auth/LoginRedirectHandler'; + +function Page() { + return ; +} + +export default Page; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 370596f..d1e4fb8 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -15,7 +15,13 @@ function Page() { className="absolute bottom-0 left-1/2 -translate-x-1/2 w-full max-w-[75rem] h-full -z-10" aria-hidden > - +
diff --git a/src/app/login/success/page.tsx b/src/app/login/success/page.tsx new file mode 100644 index 0000000..15766eb --- /dev/null +++ b/src/app/login/success/page.tsx @@ -0,0 +1,6 @@ +import LoginRedirectHandler from '@/shared/components/auth/LoginRedirectHandler'; + +function Page() { + return ; +} +export default Page; diff --git a/src/shared/@store/auth.ts b/src/shared/@store/auth.ts new file mode 100644 index 0000000..502da2a --- /dev/null +++ b/src/shared/@store/auth.ts @@ -0,0 +1,83 @@ +import { create } from 'zustand'; +import { customToast } from '../components/toast/CustomToastUtils'; + +interface User { + id: string; + email: string; + nickname: string; + isFirstLogin: boolean; + abv_degree?: number; + provider?: 'naver' | 'kakao' | 'google'; +} + +interface AuthState { + user: User | null; + accessToken: string | null; + isLoggedIn: boolean; + setUser: (user: User, token: string) => void; + logout: () => Promise; + loginWithProvider: (provider: User['provider']) => void; + + updateUser: () => Promise; +} + +export const useAuthStore = create((set) => ({ + user: null, + accessToken: null, + isLoggedIn: false, + + loginWithProvider: (provider) => { + window.location.href = `http://localhost:8080/oauth2/authorization/${provider}`; + }, + + setUser: (user, token) => { + const updatedUser = { ...user, abv_degree: 5.0 }; + set({ user: updatedUser, accessToken: token, isLoggedIn: true }); + + customToast.success(`${updatedUser.nickname}님, 로그인 성공 🎉`); + }, + + logout: async () => { + try { + await fetch('http://localhost:8080/user/auth/logout', { + method: 'POST', + credentials: 'include', + }); + + customToast.success('로그아웃 되었습니다.'); + set({ user: null, accessToken: null, isLoggedIn: false }); + } catch (err) { + customToast.error('로그아웃 실패❌ \n 다시 시도해주세요.'); + console.error('로그아웃 실패', err); + } + }, + + updateUser: async () => { + try { + const res = await fetch('http://localhost:8080/user/auth/refresh', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + }); + + if (!res.ok) throw new Error('토큰 갱신 실패'); + const data = await res.json(); + + console.log('updateUser response:', data); + const userInfo = data?.data?.user; + const accessToken = data?.data?.accessToken; + + if (userInfo && accessToken) { + set({ user: userInfo, accessToken, isLoggedIn: true }); + console.log('토큰 및 유저 정보 갱신 완료:', userInfo); + return userInfo; + } + + return null; + } catch (err) { + console.error('updateUser 실패', err); + set({ accessToken: null, user: null, isLoggedIn: false }); + return null; + } + }, +})); diff --git a/src/shared/@store/modal.ts b/src/shared/@store/modal.ts new file mode 100644 index 0000000..748ba24 --- /dev/null +++ b/src/shared/@store/modal.ts @@ -0,0 +1,33 @@ +import { create } from 'zustand'; + +interface WelcomeModalData { + open: boolean; + nickname: string; +} + +interface LogoutConfirmModalData { + open: boolean; +} + +interface ModalStore { + welcomeModal: WelcomeModalData; + logoutConfirmModal: LogoutConfirmModalData; + + openWelcomeModal: (nickname: string) => void; + closeWelcomeModal: () => void; + + openLogoutConfirmModal: () => void; + closeLogoutConfirmModal: () => void; +} + +export const useModalStore = create((set) => ({ + welcomeModal: { open: false, nickname: '' }, + logoutConfirmModal: { open: false }, + + openWelcomeModal: (nickname: string) => set({ welcomeModal: { open: true, nickname } }), + + closeWelcomeModal: () => set({ welcomeModal: { open: false, nickname: '' } }), + + openLogoutConfirmModal: () => set({ logoutConfirmModal: { open: true } }), + closeLogoutConfirmModal: () => set({ logoutConfirmModal: { open: false } }), +})); diff --git a/src/shared/components/auth/LoginConfirm.tsx b/src/shared/components/auth/LoginConfirm.tsx new file mode 100644 index 0000000..efed6f9 --- /dev/null +++ b/src/shared/components/auth/LoginConfirm.tsx @@ -0,0 +1,4 @@ +function LoginConfirm() { + return
LoginConfirm
; +} +export default LoginConfirm; diff --git a/src/shared/components/auth/LoginRedirectHandler.tsx b/src/shared/components/auth/LoginRedirectHandler.tsx new file mode 100644 index 0000000..4077b54 --- /dev/null +++ b/src/shared/components/auth/LoginRedirectHandler.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import { useAuthStore } from '@/shared/@store/auth'; +import { useModalStore } from '@/shared/@store/modal'; +import { customToast } from '@/shared/components/toast/CustomToastUtils'; +import Spinner from '../spinner/Spinner'; + +function LoginRedirectHandler() { + const pathname = usePathname(); + const router = useRouter(); + const { user, updateUser } = useAuthStore(); + const { openWelcomeModal } = useModalStore(); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!user && loading) { + updateUser() + .then((fetchedUser) => { + if (!fetchedUser) { + router.replace('/login'); + } + }) + .catch(() => { + router.replace('/login'); + }) + .finally(() => setLoading(false)); + } + }, [user, loading, updateUser, router]); + + useEffect(() => { + if (!user || loading) return; + + const preLoginPath = sessionStorage.getItem('preLoginPath') || '/'; + + if (pathname.startsWith('/login/first-user')) { + openWelcomeModal(user.nickname); + } else if (pathname.startsWith('/login/success')) { + customToast.success(`${user.nickname}님 \n 로그인 성공 🎉`); + router.replace(preLoginPath); + } + }, [pathname, user, router, openWelcomeModal, loading]); + + if (loading) { + return ( +
+ +
+ ); + } + + return null; +} +export default LoginRedirectHandler; diff --git a/src/app/login/Welcome.tsx b/src/shared/components/auth/Welcome.tsx similarity index 71% rename from src/app/login/Welcome.tsx rename to src/shared/components/auth/Welcome.tsx index b62d820..b479480 100644 --- a/src/app/login/Welcome.tsx +++ b/src/shared/components/auth/Welcome.tsx @@ -6,20 +6,21 @@ import Button from '@/shared/components/button/Button'; import ModalLayout from '@/shared/components/modalPop/ModalLayout'; import Ssury from '@/shared/assets/ssury/ssury_jump.webp'; import { useRouter } from 'next/navigation'; +import { useModalStore } from '@/shared/@store/modal'; +import { useAuthStore } from '@/shared/@store/auth'; -interface Props { - open: boolean; - onClose: () => void; -} - -function Welcome({ open, onClose }: Props) { +function Welcome() { const router = useRouter(); + const { user } = useAuthStore(); + const { welcomeModal, closeWelcomeModal } = useModalStore(); + + if (!welcomeModal.open || !user) return null; return ( @@ -27,7 +28,7 @@ function Welcome({ open, onClose }: Props) { type="button" color="purple" onClick={() => { - onClose(); + closeWelcomeModal(); router.push('/recipe'); }} > @@ -36,7 +37,7 @@ function Welcome({ open, onClose }: Props) { - +
+ {isLoggedIn ? ( + + ) : ( + { + setIsClicked(false); + sessionStorage.setItem('preLoginPath', window.location.pathname); + }} + className="flex items-center gap-2 text-black font-light text-xl hover:text-black/70" + > + + 로그인/회원가입 + + )} +
diff --git a/src/shared/components/header/HamburgerMenu.tsx b/src/shared/components/header/HamburgerMenu.tsx index 86acc9f..aa4d950 100644 --- a/src/shared/components/header/HamburgerMenu.tsx +++ b/src/shared/components/header/HamburgerMenu.tsx @@ -14,7 +14,7 @@ function HamburgerMenu() { <> ))}
diff --git a/src/shared/components/header/HeaderLogo.tsx b/src/shared/components/header/HeaderLogo.tsx index 79639a9..4d2d06c 100644 --- a/src/shared/components/header/HeaderLogo.tsx +++ b/src/shared/components/header/HeaderLogo.tsx @@ -3,7 +3,7 @@ import Image from 'next/image'; function HeaderLogo() { return ( -
+
-
    +
      {navItem.map(({ href, label }) => (
    • { + const handleScroll = throttle(() => { const currentScroll = document.documentElement.scrollTop || document.body.scrollTop; setIsVisible(currentScroll > 30); - }; + }, 100); window.addEventListener('scroll', handleScroll, { passive: true }); window.addEventListener('wheel', cancelScroll, { passive: true });