diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx index 19d8173..f22bf8d 100644 --- a/src/app/community/page.tsx +++ b/src/app/community/page.tsx @@ -1,4 +1,3 @@ - import CommunityFilter from '@/domains/community/main/CommunityFilter'; import CommunityTab from '@/domains/community/main/CommunityTab'; import PostCard from '@/domains/community/main/PostCard'; diff --git a/src/app/design-system/page.tsx b/src/app/design-system/page.tsx index ca12035..048b3f6 100644 --- a/src/app/design-system/page.tsx +++ b/src/app/design-system/page.tsx @@ -6,13 +6,13 @@ import Input from '@/shared/components/InputBox/Input'; import { useState } from 'react'; import { customToast } from '@/shared/components/toast/CustomToastUtils'; import ModalLayout from '@/shared/components/modalPop/ModalLayout'; -import ConfirmPop from '@/shared/components/modalPop/ConfirmPop'; import SelectBox from '@/domains/shared/select-box/SelectBox'; import Spinner from '@/shared/components/spinner/Spinner'; import LikeBtn from '@/domains/community/components/like/LikeBtn'; import Share from '@/domains/shared/share/Share'; import Keep from '@/domains/shared/keep/Keep'; +import ConfirmModal from '@/shared/components/modalPop/ConfirmModal'; function Page() { const [isModalOpen, setModalOpen] = useState(false); @@ -129,7 +129,7 @@ function Page() {
모달팝업 내용
- setConfirmOpen(false)} title="Confirm제목" diff --git a/src/app/login/first-user/page.tsx b/src/app/login/first-user/page.tsx deleted file mode 100644 index 6a939dc..0000000 --- a/src/app/login/first-user/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import LoginRedirectHandler from '@/domains/shared/auth/LoginRedirectHandler'; - -function Page() { - return ; -} - -export default Page; diff --git a/src/app/login/success/page.tsx b/src/app/login/success/page.tsx index 25b93a4..48cef7d 100644 --- a/src/app/login/success/page.tsx +++ b/src/app/login/success/page.tsx @@ -1,4 +1,4 @@ -import LoginRedirectHandler from '@/domains/shared/auth/LoginRedirectHandler'; +import LoginRedirectHandler from '@/domains/login/components/LoginRedirectHandler'; function Page() { return ; diff --git a/src/app/login/user/first-user/page.tsx b/src/app/login/user/first-user/page.tsx new file mode 100644 index 0000000..131a5e1 --- /dev/null +++ b/src/app/login/user/first-user/page.tsx @@ -0,0 +1,7 @@ +import LoginRedirectHandler from '@/domains/login/components/LoginRedirectHandler'; + +function Page() { + return ; +} + +export default Page; diff --git a/src/app/login/user/success/page.tsx b/src/app/login/user/success/page.tsx new file mode 100644 index 0000000..48cef7d --- /dev/null +++ b/src/app/login/user/success/page.tsx @@ -0,0 +1,6 @@ +import LoginRedirectHandler from '@/domains/login/components/LoginRedirectHandler'; + +function Page() { + return ; +} +export default Page; diff --git a/src/domains/shared/auth/LoginRedirectHandler.tsx b/src/domains/login/components/LoginRedirectHandler.tsx similarity index 77% rename from src/domains/shared/auth/LoginRedirectHandler.tsx rename to src/domains/login/components/LoginRedirectHandler.tsx index b18d780..79d03b1 100644 --- a/src/domains/shared/auth/LoginRedirectHandler.tsx +++ b/src/domains/login/components/LoginRedirectHandler.tsx @@ -2,13 +2,11 @@ import { useEffect, useState } from 'react'; import { usePathname, useRouter } from 'next/navigation'; - import { customToast } from '@/shared/components/toast/CustomToastUtils'; - -import WelcomeModal from './WelcomeModal'; -import { getCookie, removeCookie } from './utils/cookie'; -import { useAuthStore } from '../store/auth'; +import { getCookie, removeCookie } from '@/domains/shared/auth/utils/cookie'; +import { useAuthStore } from '@/domains/shared/store/auth'; import Spinner from '@/shared/components/spinner/Spinner'; +import WelcomeModal from '@/domains/login/components/WelcomeModal'; function LoginRedirectHandler() { const pathname = usePathname(); @@ -29,6 +27,8 @@ function LoginRedirectHandler() { router.replace('/login'); }) .finally(() => setLoading(false)); + } else { + setLoading(false); } }, [user, loading, updateUser, router]); @@ -36,14 +36,19 @@ function LoginRedirectHandler() { if (!user || loading) return; const preLoginPath = getCookie('preLoginPath') || '/'; - console.log(preLoginPath); + // 로그인 상태인데 이전 페이지가 /login이면 메인으로 이동 + if (user && preLoginPath === '/login') { + router.replace('/'); + removeCookie('preLoginPath'); + return; + } // 첫 유저일 경우 모달 오픈 - if (pathname.startsWith('/login/first-user')) { + if (pathname.startsWith('/login/user/first-user')) { setWelcomeModalOpen(true); } // 기존 유저일 경우 - else if (pathname.startsWith('/login/success')) { + else if (pathname.startsWith('/login/user/success')) { customToast.success(`${user.nickname}님 \n 로그인 성공 🎉`); router.replace(preLoginPath); removeCookie('preLoginPath'); diff --git a/src/domains/login/components/LogoutConfirm.tsx b/src/domains/login/components/LogoutConfirm.tsx new file mode 100644 index 0000000..b7f5f26 --- /dev/null +++ b/src/domains/login/components/LogoutConfirm.tsx @@ -0,0 +1,20 @@ +import ConfirmModal from '@/shared/components/modalPop/ConfirmModal'; + +interface Props { + open: boolean; + onClose: () => void; + onLogout: () => void; +} + +function LogoutConfirm({ open, onClose, onLogout }: Props) { + return ( + + ); +} +export default LogoutConfirm; diff --git a/src/domains/shared/auth/WelcomeModal.tsx b/src/domains/login/components/WelcomeModal.tsx similarity index 100% rename from src/domains/shared/auth/WelcomeModal.tsx rename to src/domains/login/components/WelcomeModal.tsx diff --git a/src/domains/shared/auth/LoginConfirm.tsx b/src/domains/shared/auth/LoginConfirm.tsx deleted file mode 100644 index efed6f9..0000000 --- a/src/domains/shared/auth/LoginConfirm.tsx +++ /dev/null @@ -1,4 +0,0 @@ -function LoginConfirm() { - return
LoginConfirm
; -} -export default LoginConfirm; diff --git a/src/domains/shared/store/auth.ts b/src/domains/shared/store/auth.ts index f29a788..26f0796 100644 --- a/src/domains/shared/store/auth.ts +++ b/src/domains/shared/store/auth.ts @@ -1,5 +1,6 @@ import { customToast } from '@/shared/components/toast/CustomToastUtils'; import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; interface User { id: string; @@ -21,63 +22,62 @@ interface AuthState { updateUser: () => Promise; } -export const useAuthStore = create((set) => ({ - user: null, - accessToken: null, - isLoggedIn: false, +export const useAuthStore = create()( + persist( + (set) => ({ + user: null, + accessToken: null, + isLoggedIn: false, - loginWithProvider: (provider) => { - window.location.href = `http://localhost:8080/oauth2/authorization/${provider}`; - }, + 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 }); + setUser: (user, token) => { + const updatedUser = { ...user, abv_degree: 5.0 }; + set({ user: updatedUser, accessToken: token, isLoggedIn: true }); + customToast.success(`${updatedUser.nickname}님, 로그인 성공 🎉`); + }, - 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); + } + }, - logout: async () => { - try { - await fetch('http://localhost:8080/user/auth/logout', { - method: 'POST', - credentials: 'include', - }); + updateUser: async () => { + try { + const res = await fetch('http://localhost:8080/user/auth/refresh', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + }); - customToast.success('로그아웃 되었습니다.'); - set({ user: null, accessToken: null, isLoggedIn: false }); - } catch (err) { - customToast.error('로그아웃 실패❌ \n 다시 시도해주세요.'); - console.error('로그아웃 실패', err); - } - }, + if (!res.ok) throw new Error('토큰 갱신 실패'); + const data = await res.json(); + const userInfo = data?.data?.user; + const accessToken = data?.data?.accessToken; - 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; - } - }, -})); + if (userInfo && accessToken) { + set({ user: userInfo, accessToken, isLoggedIn: true }); + return userInfo; + } + return null; + } catch (err) { + console.error('updateUser 실패', err); + set({ accessToken: null, user: null, isLoggedIn: false }); + return null; + } + }, + }), + { name: 'auth-storage' } // localStorage key + ) +); diff --git a/src/shared/components/header/DropdownMenu.tsx b/src/shared/components/header/DropdownMenu.tsx index 045b707..b333b04 100644 --- a/src/shared/components/header/DropdownMenu.tsx +++ b/src/shared/components/header/DropdownMenu.tsx @@ -12,6 +12,7 @@ import gsap from 'gsap'; import { createPortal } from 'react-dom'; import { useAuthStore } from '@/domains/shared/store/auth'; import { setPreLoginPath } from '@/domains/shared/auth/utils/setPreLoginPath'; +import LogoutConfirm from '@/domains/login/components/LogoutConfirm'; interface Props { isClicked: boolean; @@ -28,6 +29,7 @@ function DropdownMenu({ isClicked, setIsClicked, visible, setVisible }: Props) { const [mounted, setMounted] = useState(false); const { isLoggedIn, logout } = useAuthStore(); + const [logoutModalOpen, setLogoutModalOpen] = useState(false); useEffect(() => { setMounted(true); @@ -62,92 +64,113 @@ function DropdownMenu({ isClicked, setIsClicked, visible, setVisible }: Props) { return () => { // tl.kill(); }; - }, [isClicked, setVisible]); if (!mounted) return null; return createPortal( - + {logoutModalOpen && ( + setLogoutModalOpen(false)} + onLogout={async () => { + await logout(); + setLogoutModalOpen(false); setIsClicked(false); }} - > - - - - , + /> + )} + , document.body ); } diff --git a/src/shared/components/header/HeaderBtn.tsx b/src/shared/components/header/HeaderBtn.tsx index a3d6cdb..07808a4 100644 --- a/src/shared/components/header/HeaderBtn.tsx +++ b/src/shared/components/header/HeaderBtn.tsx @@ -1,69 +1,93 @@ +'use client'; + import Bell from '@/shared/assets/icons/bell_24.svg'; import User from '@/shared/assets/icons/user_24.svg'; -import SignOut from '@/shared/assets/icons/sign_out_24.svg'; -import SignIn from '@/shared/assets/icons/sign_in_24.svg'; import { useRouter } from 'next/navigation'; import tw from '@/shared/utills/tw'; import { useAuthStore } from '@/domains/shared/store/auth'; import { setPreLoginPath } from '@/domains/shared/auth/utils/setPreLoginPath'; - -type RouterType = ReturnType; +import { useState } from 'react'; +import LogoutConfirm from '@/domains/login/components/LogoutConfirm'; function HeaderBtn({ pathname }: { pathname: string }) { const { isLoggedIn, logout } = useAuthStore(); - const router = useRouter(); - const headerBtn = [ - ...(isLoggedIn - ? [ - { - icon: Bell, - label: '알림', - onClick: () => {}, - }, - { - icon: User, - label: '마이 페이지', - className: pathname === '/mypage' ? 'text-tertiary' : 'text-current', - onClick: (router: RouterType) => router.push('/mypage'), - }, - { - icon: SignOut, - label: '로그아웃', - onClick: async () => { - await logout(); - }, - }, - ] - : [ - { - icon: SignIn, - label: '로그인', - className: `${pathname === '/login' ? 'text-tertiary' : ''}`, - onClick: async () => { - await setPreLoginPath(window.location.pathname); - router.push('/login'); - }, - }, - ]), + const [logoutModalOpen, setLogoutModalOpen] = useState(false); + + const navButtons = [ + { + icon: Bell, + label: '알림', + onClick: () => setLogoutModalOpen(true), + }, + { + icon: User, + label: '마이 페이지', + className: pathname === '/mypage' ? 'text-tertiary' : 'text-current', + hiddenMobile: true, + onClick: () => router.push('/mypage'), + }, ]; + const authButton = isLoggedIn + ? { + label: 'Logout', + onClick: () => setLogoutModalOpen(true), + } + : { + label: 'Login', + className: pathname === '/login' ? 'bg-white text-primary' : 'bg-transparent text-white', + onClick: async () => { + await setPreLoginPath(window.location.pathname); + router.push('/login'); + }, + }; return ( -
- {headerBtn.map(({ icon: Icon, label, onClick, className }) => ( + <> +
+ {/* 아이콘 버튼들 */} +
+ {isLoggedIn && + navButtons.map(({ icon: Icon, label, onClick, className, hiddenMobile }) => ( + + ))} +
+ + {/* 로그인/로그아웃 버튼 */} - ))} -
+
+ + {/* 로그아웃 확인 모달 */} + {logoutModalOpen && ( + setLogoutModalOpen(false)} + onLogout={async () => { + await logout(); + setLogoutModalOpen(false); + }} + /> + )} + ); } diff --git a/src/shared/components/header/NavItem.tsx b/src/shared/components/header/NavItem.tsx index 570871f..8442c29 100644 --- a/src/shared/components/header/NavItem.tsx +++ b/src/shared/components/header/NavItem.tsx @@ -11,8 +11,8 @@ function NavItem({ pathname, className }: Props) { return (