From 358981f5c705855ec8da4c5a76e1351fd68096ea Mon Sep 17 00:00:00 2001 From: ahk0413 Date: Fri, 26 Sep 2025 11:06:38 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[fix,=20docs]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/design-system/page.tsx | 4 ++-- src/app/login/first-user/page.tsx | 7 ------- src/app/login/success/page.tsx | 6 ------ src/app/login/user/first-user/page.tsx | 7 +++++++ src/app/login/user/success/page.tsx | 6 ++++++ .../auth => login/components}/LoginRedirectHandler.tsx | 8 +++----- .../components/LogoutConfirm.tsx} | 0 .../{shared/auth => login/components}/WelcomeModal.tsx | 0 .../modalPop/{ConfirmPop.tsx => ConfirmModal.tsx} | 4 ++-- 9 files changed, 20 insertions(+), 22 deletions(-) delete mode 100644 src/app/login/first-user/page.tsx delete mode 100644 src/app/login/success/page.tsx create mode 100644 src/app/login/user/first-user/page.tsx create mode 100644 src/app/login/user/success/page.tsx rename src/domains/{shared/auth => login/components}/LoginRedirectHandler.tsx (90%) rename src/domains/{shared/auth/LoginConfirm.tsx => login/components/LogoutConfirm.tsx} (100%) rename src/domains/{shared/auth => login/components}/WelcomeModal.tsx (100%) rename src/shared/components/modalPop/{ConfirmPop.tsx => ConfirmModal.tsx} (85%) 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 deleted file mode 100644 index 25b93a4..0000000 --- a/src/app/login/success/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import LoginRedirectHandler from '@/domains/shared/auth/LoginRedirectHandler'; - -function Page() { - return ; -} -export default Page; 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..27f1921 --- /dev/null +++ b/src/app/login/user/first-user/page.tsx @@ -0,0 +1,7 @@ +import LoginRedirectHandler from '@/domains/shared/auth/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..728f9eb --- /dev/null +++ b/src/app/login/user/success/page.tsx @@ -0,0 +1,6 @@ +import LoginRedirectHandler from '@/domains/shared/auth/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 90% rename from src/domains/shared/auth/LoginRedirectHandler.tsx rename to src/domains/login/components/LoginRedirectHandler.tsx index b18d780..2b0e899 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(); diff --git a/src/domains/shared/auth/LoginConfirm.tsx b/src/domains/login/components/LogoutConfirm.tsx similarity index 100% rename from src/domains/shared/auth/LoginConfirm.tsx rename to src/domains/login/components/LogoutConfirm.tsx 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/shared/components/modalPop/ConfirmPop.tsx b/src/shared/components/modalPop/ConfirmModal.tsx similarity index 85% rename from src/shared/components/modalPop/ConfirmPop.tsx rename to src/shared/components/modalPop/ConfirmModal.tsx index faf011d..d557335 100644 --- a/src/shared/components/modalPop/ConfirmPop.tsx +++ b/src/shared/components/modalPop/ConfirmModal.tsx @@ -9,7 +9,7 @@ interface Props { description?: React.ReactNode; children?: React.ReactNode; } -function ConfirmPop({ ref, open, onClose, title, description, children }: Props) { +function ConfirmModal({ ref, open, onClose, title, description, children }: Props) { return ( ); } -export default ConfirmPop; +export default ConfirmModal; From 26b9e365f78e15dae438dbba15aa1c152e866487 Mon Sep 17 00:00:00 2001 From: ahk0413 Date: Fri, 26 Sep 2025 11:10:29 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[feat]=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=ED=8C=9D=EC=97=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/login/components/LogoutConfirm.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/domains/login/components/LogoutConfirm.tsx b/src/domains/login/components/LogoutConfirm.tsx index efed6f9..1015ba9 100644 --- a/src/domains/login/components/LogoutConfirm.tsx +++ b/src/domains/login/components/LogoutConfirm.tsx @@ -1,4 +1,11 @@ -function LoginConfirm() { - return
LoginConfirm
; +import ConfirmModal from '@/shared/components/modalPop/ConfirmModal'; + +interface Props { + open: boolean; + onClose: () => void; +} + +function LoginConfirm({ open, onClose }: Props) { + return ; } export default LoginConfirm; From 28ae19f662ce43224783b46ced95880a5c67122c Mon Sep 17 00:00:00 2001 From: ahk0413 Date: Fri, 26 Sep 2025 11:41:38 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[fix]=20confirm=20=EC=B0=BD=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20props=20=EB=B0=9B=EC=9D=84=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/modalPop/ConfirmModal.tsx | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/shared/components/modalPop/ConfirmModal.tsx b/src/shared/components/modalPop/ConfirmModal.tsx index d557335..c896a82 100644 --- a/src/shared/components/modalPop/ConfirmModal.tsx +++ b/src/shared/components/modalPop/ConfirmModal.tsx @@ -8,8 +8,19 @@ interface Props { title?: string; description?: React.ReactNode; children?: React.ReactNode; + onConfirm?: () => void; + onCancel?: () => void; } -function ConfirmModal({ ref, open, onClose, title, description, children }: Props) { +function ConfirmModal({ + ref, + open, + onClose, + title, + description, + children, + onConfirm, + onCancel, +}: Props) { return ( - - From eb37b881d2871e139429551ecfad87e7efde48bb Mon Sep 17 00:00:00 2001 From: ahk0413 Date: Fri, 26 Sep 2025 12:36:12 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[fix]=20loginRedirect=20=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/login/user/first-user/page.tsx | 2 +- src/app/login/user/success/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/login/user/first-user/page.tsx b/src/app/login/user/first-user/page.tsx index 27f1921..131a5e1 100644 --- a/src/app/login/user/first-user/page.tsx +++ b/src/app/login/user/first-user/page.tsx @@ -1,4 +1,4 @@ -import LoginRedirectHandler from '@/domains/shared/auth/components/LoginRedirectHandler'; +import LoginRedirectHandler from '@/domains/login/components/LoginRedirectHandler'; function Page() { return ; diff --git a/src/app/login/user/success/page.tsx b/src/app/login/user/success/page.tsx index 728f9eb..48cef7d 100644 --- a/src/app/login/user/success/page.tsx +++ b/src/app/login/user/success/page.tsx @@ -1,4 +1,4 @@ -import LoginRedirectHandler from '@/domains/shared/auth/components/LoginRedirectHandler'; +import LoginRedirectHandler from '@/domains/login/components/LoginRedirectHandler'; function Page() { return ; From 43fc21e02356494a8ab9876c1b930394ce8cb0ee Mon Sep 17 00:00:00 2001 From: ahk0413 Date: Fri, 26 Sep 2025 12:37:19 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[feat]=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20confirm=20=EC=B0=BD=20=EC=97=B0=EB=8F=99=20,=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EA=B3=A0=EC=B9=A8=EC=8B=9C=EC=97=90=EB=8F=84?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9C=A0=EC=A7=80,=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20=EB=B9=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=20=EB=A9=94=EB=89=B4=20show/hide=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../login/components/LoginRedirectHandler.tsx | 13 +- .../login/components/LogoutConfirm.tsx | 15 +- src/domains/shared/store/auth.ts | 108 +++++----- src/shared/components/header/DropdownMenu.tsx | 184 +++++++++--------- src/shared/components/header/HeaderBtn.tsx | 120 +++++++----- src/shared/components/header/NavItem.tsx | 4 +- .../components/modalPop/ModalLayout.tsx | 2 +- src/shared/utills/navigation.ts | 5 + 8 files changed, 249 insertions(+), 202 deletions(-) diff --git a/src/domains/login/components/LoginRedirectHandler.tsx b/src/domains/login/components/LoginRedirectHandler.tsx index 2b0e899..79d03b1 100644 --- a/src/domains/login/components/LoginRedirectHandler.tsx +++ b/src/domains/login/components/LoginRedirectHandler.tsx @@ -27,6 +27,8 @@ function LoginRedirectHandler() { router.replace('/login'); }) .finally(() => setLoading(false)); + } else { + setLoading(false); } }, [user, loading, updateUser, router]); @@ -34,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 index 1015ba9..b7f5f26 100644 --- a/src/domains/login/components/LogoutConfirm.tsx +++ b/src/domains/login/components/LogoutConfirm.tsx @@ -3,9 +3,18 @@ import ConfirmModal from '@/shared/components/modalPop/ConfirmModal'; interface Props { open: boolean; onClose: () => void; + onLogout: () => void; } -function LoginConfirm({ open, onClose }: Props) { - return ; +function LogoutConfirm({ open, onClose, onLogout }: Props) { + return ( + + ); } -export default LoginConfirm; +export default LogoutConfirm; 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 c635f8e..bb94881 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); @@ -66,109 +68,109 @@ function DropdownMenu({ isClicked, setIsClicked, visible, setVisible }: Props) { if (!mounted) return null; - // const handleMouseEnter = (index: number) => { - // const el = textRef.current[index]; - // if (!el) return; - // gsap.to(el, { - // y: -5, - // duration: 0.3, - // ease: 'power1.out', - // }); - // }; - - // const handleMouseLeave = (index: number) => { - // const el = textRef.current[index]; - // if (!el) return; - // gsap.to(el, { - // y: 0, - // duration: 0.3, - // ease: 'power1.out', - // }); - // }; - 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 (