From fb50ac5d2208cda7d486cbaada3c9cb7f1906853 Mon Sep 17 00:00:00 2001 From: ahk0413 Date: Wed, 24 Sep 2025 11:34:24 +0900 Subject: [PATCH 01/10] =?UTF-8?q?[feat]=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20?= =?UTF-8?q?=ED=83=91=20=EB=B2=84=ED=8A=BC=20throttle=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/components/scrollTop/ScrollTopBtn.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/shared/components/scrollTop/ScrollTopBtn.tsx b/src/shared/components/scrollTop/ScrollTopBtn.tsx index e9cf395..5ad671f 100644 --- a/src/shared/components/scrollTop/ScrollTopBtn.tsx +++ b/src/shared/components/scrollTop/ScrollTopBtn.tsx @@ -1,6 +1,7 @@ 'use client'; import Arrow from '@/shared/assets/icons/arrow_up_24.svg'; +import { throttle } from '@/shared/utills/throttle'; import { useEffect, useRef, useState } from 'react'; function ScrollTopBtn() { @@ -25,10 +26,10 @@ function ScrollTopBtn() { } }; - const handleScroll = () => { + 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 }); From 7dbade824ad2a2119c8b2f38156b3e4b9168e189 Mon Sep 17 00:00:00 2001 From: ahk0413 Date: Wed, 24 Sep 2025 11:35:22 +0900 Subject: [PATCH 02/10] =?UTF-8?q?[design]=20=ED=97=A4=EB=8D=94=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20hover=20/=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=EC=8B=9C=20=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/components/header/HeaderBtn.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/shared/components/header/HeaderBtn.tsx b/src/shared/components/header/HeaderBtn.tsx index cd5c8bd..7aaa909 100644 --- a/src/shared/components/header/HeaderBtn.tsx +++ b/src/shared/components/header/HeaderBtn.tsx @@ -21,7 +21,7 @@ function HeaderBtn({ pathname }: { pathname: string }) { { icon: User, label: '마이 페이지', - className: `fill-white w-[27px] h-[27px] ${pathname === '/mypage' ? 'fill-tertiary hover:fill-tertiary' : ''}`, + className: `${pathname === '/mypage' ? 'text-tertiary' : ''}`, onClick: (router: RouterType) => { // console.log('유저 클릭'); router.push('/mypage'); @@ -30,7 +30,7 @@ function HeaderBtn({ pathname }: { pathname: string }) { { icon: SignIn, label: '로그인', - className: 'sm:block hidden', + className: `${pathname === '/login' ? 'text-tertiary' : ''}`, onClick: () => { // console.log('로그아웃 클릭'); router.push('/login'); @@ -43,14 +43,14 @@ function HeaderBtn({ pathname }: { pathname: string }) { {headerBtn.map(({ icon: Icon, label, onClick, className }) => ( ))} From fa4f146f141f1fc5da296938766b222de54a3cb3 Mon Sep 17 00:00:00 2001 From: ahk0413 Date: Wed, 24 Sep 2025 12:24:22 +0900 Subject: [PATCH 03/10] =?UTF-8?q?[fix]=20=ED=97=A4=EB=8D=94=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EA=B2=BD=EA=B3=A0=20=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/components/header/HeaderBtn.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/components/header/HeaderBtn.tsx b/src/shared/components/header/HeaderBtn.tsx index 7aaa909..ce0ede6 100644 --- a/src/shared/components/header/HeaderBtn.tsx +++ b/src/shared/components/header/HeaderBtn.tsx @@ -21,7 +21,7 @@ function HeaderBtn({ pathname }: { pathname: string }) { { icon: User, label: '마이 페이지', - className: `${pathname === '/mypage' ? 'text-tertiary' : ''}`, + className: `${pathname === '/mypage' ? 'text-tertiary' : 'text-current'}`, onClick: (router: RouterType) => { // console.log('유저 클릭'); router.push('/mypage'); From 22d8b6cd6c4a6e2100ae9bf6b422b3d9aa3d9217 Mon Sep 17 00:00:00 2001 From: ahk0413 Date: Wed, 24 Sep 2025 12:25:43 +0900 Subject: [PATCH 04/10] =?UTF-8?q?[design]=20toast=20=EC=A4=84=EB=B0=94?= =?UTF-8?q?=EA=BF=88=20=EC=98=88=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/design-system/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() {
From 4efbbdd9b48314cb4ea7df4675cefe701b373701 Mon Sep 17 00:00:00 2001 From: ahk0413 Date: Wed, 24 Sep 2025 15:31:37 +0900 Subject: [PATCH 05/10] =?UTF-8?q?[feat]=20auth=20=EA=B4=80=EB=A0=A8=20stor?= =?UTF-8?q?e=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/@store/auth.ts | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/shared/@store/auth.ts diff --git a/src/shared/@store/auth.ts b/src/shared/@store/auth.ts new file mode 100644 index 0000000..047f711 --- /dev/null +++ b/src/shared/@store/auth.ts @@ -0,0 +1,68 @@ +import { create } from 'zustand'; +import { customToast } from '../components/toast/CustomToastUtils'; + +interface User { + id: string; + email: string; + nickname: string; + 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; + refreshToken: () => Promise; + loginWithProvider: (provider: User['provider']) => void; +} + +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) => set({ user, accessToken: token, isLoggedIn: true }), + + logout: async () => { + try { + await fetch('http://localhost:8080/api/user/auth/logout', { + method: 'POST', + credentials: 'include', + }); + + customToast.success('로그아웃 되었습니다.'); + set({ user: null, accessToken: null, isLoggedIn: false }); + } catch (err) { + customToast.error('로그아웃 실패❌ \n 다시 시도해주세요.'); + console.error('로그아웃 실패', err); + } + }, + + refreshToken: async () => { + try { + const res = await fetch('http://localhost:8080/api/user/auth/refresh', { + method: 'POST', + credentials: 'include', + }); + if (!res.ok) throw new Error('토큰 갱신 실패'); + + const result = await res.json(); + if (result.code === 0) { + set({ isLoggedIn: true, user: result.data.user }); + customToast.success(`환영합니다😊 \n ${result.data.user.nickname}님`); + } else { + throw new Error(result.message || '토큰 갱신 실패'); + } + } catch (err) { + console.error(err); + set({ user: null, accessToken: null, isLoggedIn: false }); + } + }, +})); From a11a2dd9313b28daa4e7544b37a95db509c56ba1 Mon Sep 17 00:00:00 2001 From: ahk0413 Date: Wed, 24 Sep 2025 15:32:51 +0900 Subject: [PATCH 06/10] =?UTF-8?q?[feat]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/components/header/DropdownMenu.tsx | 54 +++++++++++++++ src/shared/components/header/HeaderBtn.tsx | 65 +++++++++++-------- src/shared/components/header/NavItem.tsx | 2 +- 3 files changed, 92 insertions(+), 29 deletions(-) diff --git a/src/shared/components/header/DropdownMenu.tsx b/src/shared/components/header/DropdownMenu.tsx index 65bbb2f..a899eae 100644 --- a/src/shared/components/header/DropdownMenu.tsx +++ b/src/shared/components/header/DropdownMenu.tsx @@ -6,6 +6,7 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { useEffect, useRef } from 'react'; import gsap from 'gsap'; +import { useAuthStore } from '@/shared/@store/auth'; interface Props { isClicked: boolean; @@ -17,6 +18,8 @@ function DropdownMenu({ isClicked, setIsClicked }: Props) { const menuRef = useRef(null); const textRef = useRef<(HTMLSpanElement | null)[]>([]); + const { isLoggedIn, logout } = useAuthStore(); + useEffect(() => { if (!menuRef.current) return; @@ -110,6 +113,56 @@ function DropdownMenu({ isClicked, setIsClicked }: Props) { +
+
    + {navItem.map(({ label, href }, idx) => ( +
  • + setIsClicked(false)} + className={`items-start ${pathname === href ? 'bg-tertiary/70 inline-flex pr-5 p-2 rounded-md text-secondary' : 'hover:text-black/70 flex'}`} + aria-current={pathname === href ? 'page' : undefined} + > + {idx + 1}. + { + textRef.current[idx] = el; + }} + onMouseEnter={() => handleMouseEnter(idx)} + onMouseLeave={() => handleMouseLeave(idx)} + > + {label} + + +
  • + ))} +
+
+
+ {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/HeaderBtn.tsx b/src/shared/components/header/HeaderBtn.tsx index ce0ede6..9da60c0 100644 --- a/src/shared/components/header/HeaderBtn.tsx +++ b/src/shared/components/header/HeaderBtn.tsx @@ -1,41 +1,50 @@ 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 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 '@/shared/@store/auth'; type RouterType = ReturnType; function HeaderBtn({ pathname }: { pathname: string }) { - const router = useRouter(); + const { isLoggedIn, logout } = useAuthStore(); + const router = useRouter(); const headerBtn = [ - { - icon: Bell, - label: '알림', - onClick: () => { - // console.log('알림 클릭'); - }, - }, - { - icon: User, - label: '마이 페이지', - className: `${pathname === '/mypage' ? 'text-tertiary' : 'text-current'}`, - onClick: (router: RouterType) => { - // console.log('유저 클릭'); - router.push('/mypage'); - }, - }, - { - icon: SignIn, - label: '로그인', - className: `${pathname === '/login' ? 'text-tertiary' : ''}`, - onClick: () => { - // console.log('로그아웃 클릭'); - router.push('/login'); - }, - }, + ...(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: () => { + sessionStorage.setItem('preLoginPath', window.location.pathname); + router.push('/login'); + }, + }, + ]), ]; return ( @@ -46,7 +55,7 @@ function HeaderBtn({ pathname }: { pathname: string }) { aria-label={label} onClick={() => onClick(router)} className={tw( - 'flex-center rounded-full w-7 h-7 hover:bg-secondary/30 transition-colors duration-200', + 'flex-center rounded-full w-7 h-7 hover:bg-secondary/10 transition-colors duration-200', className )} > diff --git a/src/shared/components/header/NavItem.tsx b/src/shared/components/header/NavItem.tsx index 3d3c490..570871f 100644 --- a/src/shared/components/header/NavItem.tsx +++ b/src/shared/components/header/NavItem.tsx @@ -10,7 +10,7 @@ interface Props { function NavItem({ pathname, className }: Props) { return (
{/* 웰컴 모달 (임시) */} - setIsModalOpen(false)} /> + setIsModalOpen(false)} + nickname={user?.nickname || '게스트'} + /> ); } diff --git a/src/app/login/Welcome.tsx b/src/app/login/Welcome.tsx index b62d820..0133bea 100644 --- a/src/app/login/Welcome.tsx +++ b/src/app/login/Welcome.tsx @@ -10,16 +10,17 @@ import { useRouter } from 'next/navigation'; interface Props { open: boolean; onClose: () => void; + nickname: string; } -function Welcome({ open, onClose }: Props) { +function Welcome({ open, onClose, nickname }: Props) { const router = useRouter(); return ( diff --git a/src/app/oauth/success/page.tsx b/src/app/oauth/success/page.tsx new file mode 100644 index 0000000..ac18c86 --- /dev/null +++ b/src/app/oauth/success/page.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuthStore } from '@/shared/@store/auth'; +import Spinner from '@/shared/components/spinner/Spinner'; + +function Page() { + const router = useRouter(); + const { user } = useAuthStore(); + + useEffect(() => { + if (user) { + const prevPath = sessionStorage.getItem('preLoginPath') || '/'; + router.push(prevPath); + sessionStorage.removeItem('preLoginPath'); + } + }, [user, router]); + return ( +
+ +
+ ); +} +export default Page; diff --git a/src/shared/@store/modal.ts b/src/shared/@store/modal.ts new file mode 100644 index 0000000..ed43335 --- /dev/null +++ b/src/shared/@store/modal.ts @@ -0,0 +1,14 @@ +import { create } from 'zustand'; + +interface ModalState { + welcomeOpen: boolean; + openModal: (modal: keyof ModalState) => void; + closeModal: (modal: keyof ModalState) => void; +} + +export const useModalStore = create((set) => ({ + welcomeOpen: false, + + openModal: (modal) => set({ [modal]: true }), + closeModal: (modal) => set({ [modal]: false }), +})); 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() { <> ))}
- - {/* 웰컴 모달 (임시) */} - setIsModalOpen(false)} - nickname={user?.nickname || '게스트'} - /> ); } 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/oauth/success/page.tsx b/src/app/oauth/success/page.tsx index ac18c86..12a1b0b 100644 --- a/src/app/oauth/success/page.tsx +++ b/src/app/oauth/success/page.tsx @@ -3,23 +3,48 @@ import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { useAuthStore } from '@/shared/@store/auth'; +import { useModalStore } from '@/shared/@store/modal'; import Spinner from '@/shared/components/spinner/Spinner'; function Page() { const router = useRouter(); - const { user } = useAuthStore(); + const { setUser, updateUser } = useAuthStore(); + const { openWelcomeModal } = useModalStore(); useEffect(() => { - if (user) { - const prevPath = sessionStorage.getItem('preLoginPath') || '/'; - router.push(prevPath); - sessionStorage.removeItem('preLoginPath'); - } - }, [user, router]); + const fetchUser = async () => { + try { + const data = await updateUser(); + + console.log(data); + // if (data && data.user && data.accessToken) { + // setUser(data.user, data.accessToken); // ✅ Zustand 상태 업데이트 + toast + + // // 첫 로그인 시 웰컴 모달 + // if (data.user.is_first_login) { + // openWelcomeModal(data.user.nickname); + // } + + // const prevPath = sessionStorage.getItem('preLoginPath') || '/'; + // router.push(prevPath); + // sessionStorage.removeItem('preLoginPath'); + // } else { + // router.push('/login'); + // } + } catch (err) { + console.error(err); + router.push('/login'); + } + }; + + fetchUser(); + }, [updateUser, setUser, openWelcomeModal, router]); + return (
); } + export default Page; diff --git a/src/shared/@store/auth.ts b/src/shared/@store/auth.ts index 047f711..0c9ae74 100644 --- a/src/shared/@store/auth.ts +++ b/src/shared/@store/auth.ts @@ -5,6 +5,7 @@ interface User { id: string; email: string; nickname: string; + is_first_login: boolean; abv_degree?: number; provider?: 'naver' | 'kakao' | 'google'; } @@ -15,8 +16,9 @@ interface AuthState { isLoggedIn: boolean; setUser: (user: User, token: string) => void; logout: () => Promise; - refreshToken: () => Promise; loginWithProvider: (provider: User['provider']) => void; + + updateUser: () => Promise; } export const useAuthStore = create((set) => ({ @@ -28,7 +30,12 @@ export const useAuthStore = create((set) => ({ window.location.href = `http://localhost:8080/oauth2/authorization/${provider}`; }, - setUser: (user, token) => set({ user, 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}님, 로그인 성공 🎉`); + }, logout: async () => { try { @@ -45,24 +52,29 @@ export const useAuthStore = create((set) => ({ } }, - refreshToken: async () => { + updateUser: async () => { try { const res = await fetch('http://localhost:8080/api/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(data); + // if (data.accessToken && data.user) { + // set({ accessToken: data.accessToken, user: data.user, isLoggedIn: true }); + // console.log('토큰 및 유저 정보 갱신 완료:', data.user); + // return data.user; + // } - const result = await res.json(); - if (result.code === 0) { - set({ isLoggedIn: true, user: result.data.user }); - customToast.success(`환영합니다😊 \n ${result.data.user.nickname}님`); - } else { - throw new Error(result.message || '토큰 갱신 실패'); - } + return null; } catch (err) { - console.error(err); - set({ user: null, accessToken: null, isLoggedIn: false }); + 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 index ed43335..449b190 100644 --- a/src/shared/@store/modal.ts +++ b/src/shared/@store/modal.ts @@ -1,14 +1,21 @@ import { create } from 'zustand'; -interface ModalState { - welcomeOpen: boolean; - openModal: (modal: keyof ModalState) => void; - closeModal: (modal: keyof ModalState) => void; +interface WelcomeModalData { + open: boolean; + nickname: string; } -export const useModalStore = create((set) => ({ - welcomeOpen: false, +interface ModalStore { + welcomeModal: WelcomeModalData; - openModal: (modal) => set({ [modal]: true }), - closeModal: (modal) => set({ [modal]: false }), + openWelcomeModal: (nickname: string) => void; + closeWelcomeModal: () => void; +} + +export const useModalStore = create((set) => ({ + welcomeModal: { open: false, nickname: '' }, + + openWelcomeModal: (nickname: string) => set({ welcomeModal: { open: true, nickname } }), + + closeWelcomeModal: () => set({ welcomeModal: { open: false, nickname: '' } }), })); From d8511ff3332db89d4729b8a48bfa88641e98b3c2 Mon Sep 17 00:00:00 2001 From: ahk0413 Date: Thu, 25 Sep 2025 17:25:12 +0900 Subject: [PATCH 09/10] =?UTF-8?q?[text]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EA=B5=AC=ED=98=84=EC=A4=91=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/login/first-user/page.tsx | 7 +++ src/app/login/success/page.tsx | 6 ++ src/app/oauth/success/page.tsx | 50 ----------------- src/shared/@store/auth.ts | 21 ++++--- src/shared/@store/modal.ts | 12 ++++ src/shared/components/auth/LoginConfirm.tsx | 4 ++ .../components/auth/LoginRedirectHandler.tsx | 55 +++++++++++++++++++ .../components/auth}/Welcome.tsx | 24 ++++---- src/shared/components/header/DropdownMenu.tsx | 42 +------------- src/shared/components/header/HeaderBtn.tsx | 2 +- src/shared/components/header/HeaderLogo.tsx | 2 +- src/shared/styles/_utilities.css | 5 ++ 12 files changed, 118 insertions(+), 112 deletions(-) create mode 100644 src/app/login/first-user/page.tsx create mode 100644 src/app/login/success/page.tsx delete mode 100644 src/app/oauth/success/page.tsx create mode 100644 src/shared/components/auth/LoginConfirm.tsx create mode 100644 src/shared/components/auth/LoginRedirectHandler.tsx rename src/{app/login => shared/components/auth}/Welcome.tsx (71%) 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/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/app/oauth/success/page.tsx b/src/app/oauth/success/page.tsx deleted file mode 100644 index 12a1b0b..0000000 --- a/src/app/oauth/success/page.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import { useAuthStore } from '@/shared/@store/auth'; -import { useModalStore } from '@/shared/@store/modal'; -import Spinner from '@/shared/components/spinner/Spinner'; - -function Page() { - const router = useRouter(); - const { setUser, updateUser } = useAuthStore(); - const { openWelcomeModal } = useModalStore(); - - useEffect(() => { - const fetchUser = async () => { - try { - const data = await updateUser(); - - console.log(data); - // if (data && data.user && data.accessToken) { - // setUser(data.user, data.accessToken); // ✅ Zustand 상태 업데이트 + toast - - // // 첫 로그인 시 웰컴 모달 - // if (data.user.is_first_login) { - // openWelcomeModal(data.user.nickname); - // } - - // const prevPath = sessionStorage.getItem('preLoginPath') || '/'; - // router.push(prevPath); - // sessionStorage.removeItem('preLoginPath'); - // } else { - // router.push('/login'); - // } - } catch (err) { - console.error(err); - router.push('/login'); - } - }; - - fetchUser(); - }, [updateUser, setUser, openWelcomeModal, router]); - - return ( -
- -
- ); -} - -export default Page; diff --git a/src/shared/@store/auth.ts b/src/shared/@store/auth.ts index 0c9ae74..502da2a 100644 --- a/src/shared/@store/auth.ts +++ b/src/shared/@store/auth.ts @@ -5,7 +5,7 @@ interface User { id: string; email: string; nickname: string; - is_first_login: boolean; + isFirstLogin: boolean; abv_degree?: number; provider?: 'naver' | 'kakao' | 'google'; } @@ -39,7 +39,7 @@ export const useAuthStore = create((set) => ({ logout: async () => { try { - await fetch('http://localhost:8080/api/user/auth/logout', { + await fetch('http://localhost:8080/user/auth/logout', { method: 'POST', credentials: 'include', }); @@ -54,7 +54,7 @@ export const useAuthStore = create((set) => ({ updateUser: async () => { try { - const res = await fetch('http://localhost:8080/api/user/auth/refresh', { + const res = await fetch('http://localhost:8080/user/auth/refresh', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, @@ -63,12 +63,15 @@ export const useAuthStore = create((set) => ({ if (!res.ok) throw new Error('토큰 갱신 실패'); const data = await res.json(); - console.log(data); - // if (data.accessToken && data.user) { - // set({ accessToken: data.accessToken, user: data.user, isLoggedIn: true }); - // console.log('토큰 및 유저 정보 갱신 완료:', data.user); - // return data.user; - // } + 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) { diff --git a/src/shared/@store/modal.ts b/src/shared/@store/modal.ts index 449b190..748ba24 100644 --- a/src/shared/@store/modal.ts +++ b/src/shared/@store/modal.ts @@ -5,17 +5,29 @@ interface WelcomeModalData { 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 0133bea..b479480 100644 --- a/src/app/login/Welcome.tsx +++ b/src/shared/components/auth/Welcome.tsx @@ -6,21 +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; - nickname: string; -} - -function Welcome({ open, onClose, nickname }: Props) { +function Welcome() { const router = useRouter(); + const { user } = useAuthStore(); + const { welcomeModal, closeWelcomeModal } = useModalStore(); + + if (!welcomeModal.open || !user) return null; return ( @@ -28,7 +28,7 @@ function Welcome({ open, onClose, nickname }: Props) { type="button" color="purple" onClick={() => { - onClose(); + closeWelcomeModal(); router.push('/recipe'); }} > @@ -37,7 +37,7 @@ function Welcome({ open, onClose, nickname }: Props) { - - -
-
    - {navItem.map(({ label, href }, idx) => ( -
  • - setIsClicked(false)} - className={`items-start ${pathname === href ? 'bg-tertiary/70 inline-flex pr-5 p-2 rounded-md text-secondary' : 'hover:text-black/70 flex'}`} - aria-current={pathname === href ? 'page' : undefined} - > - {idx + 1}. - { - textRef.current[idx] = el; - }} - onMouseEnter={() => handleMouseEnter(idx)} - onMouseLeave={() => handleMouseLeave(idx)} - > - {label} - - -
  • - ))} -
-
{isLoggedIn ? (
+
diff --git a/src/shared/components/header/HeaderBtn.tsx b/src/shared/components/header/HeaderBtn.tsx index 9da60c0..ad2c91d 100644 --- a/src/shared/components/header/HeaderBtn.tsx +++ b/src/shared/components/header/HeaderBtn.tsx @@ -48,7 +48,7 @@ function HeaderBtn({ pathname }: { pathname: string }) { ]; return ( -
+
{headerBtn.map(({ icon: Icon, label, onClick, className }) => (