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) {