diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 44f1900c..8a5e2ee4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,9 @@ -import {BrowserRouter as Router, Route, Routes} from 'react-router-dom'; -import AdminRoute from './components/AdminRoute'; +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; import Layout from './components/Layout'; -import {AuthProvider} from './context/AuthContext'; +import { AdminAuthProvider } from './context/AdminAuthContext'; +import { AuthProvider } from './context/AuthContext'; import AdminLayout from './pages/admin/AdminLayout'; +import AdminLoginPage from './pages/admin/AdminLoginPage'; import AdminPage from './pages/admin/AdminPage'; import AdminSchedulePage from './pages/admin/AdminSchedulePage'; import AdminThemeEditPage from './pages/admin/AdminThemeEditPage'; @@ -16,26 +17,27 @@ import ReservationStep2Page from '@_pages/ReservationStep2Page'; import ReservationSuccessPage from '@_pages/ReservationSuccessPage'; import SignupPage from '@_pages/SignupPage'; -const AdminRoutes = () => ( - - - } /> - } /> - } /> - } /> - - -); - function App() { return ( - - + + + } /> + + + } /> + } /> + } /> + } /> + + + } /> + + } /> @@ -57,4 +59,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/frontend/src/api/auth/authAPI.ts b/frontend/src/api/auth/authAPI.ts index 93216199..daebb77a 100644 --- a/frontend/src/api/auth/authAPI.ts +++ b/frontend/src/api/auth/authAPI.ts @@ -1,19 +1,31 @@ import apiClient from '@_api/apiClient'; -import type {CurrentUserContext, LoginRequest, LoginSuccessResponse} from './authTypes'; +import { + type AdminLoginSuccessResponse, + type LoginRequest, + PrincipalType, + type UserLoginSuccessResponse, +} from './authTypes'; - -export const login = async (data: LoginRequest): Promise => { - const response = await apiClient.post('/auth/login', data, false); - localStorage.setItem('accessToken', response.accessToken); - - return response; +export const userLogin = async ( + data: Omit, +): Promise => { + return await apiClient.post( + '/auth/login', + { ...data, principalType: PrincipalType.USER }, + false, + ); }; -export const checkLogin = async (): Promise => { - return await apiClient.get('/auth/login/check', true); +export const adminLogin = async ( + data: Omit, +): Promise => { + return await apiClient.post( + '/auth/login', + { ...data, principalType: PrincipalType.ADMIN }, + false, + ); }; export const logout = async (): Promise => { await apiClient.post('/auth/logout', {}, true); - localStorage.removeItem('accessToken'); }; diff --git a/frontend/src/api/auth/authTypes.ts b/frontend/src/api/auth/authTypes.ts index 426168c4..2d53a88c 100644 --- a/frontend/src/api/auth/authTypes.ts +++ b/frontend/src/api/auth/authTypes.ts @@ -5,6 +5,13 @@ export const PrincipalType = { export type PrincipalType = typeof PrincipalType[keyof typeof PrincipalType]; +export const AdminType = { + HQ: 'HQ', + STORE: 'STORE', +} as const; + +export type AdminType = typeof AdminType[keyof typeof AdminType]; + export interface LoginRequest { account: string, password: string; @@ -13,6 +20,15 @@ export interface LoginRequest { export interface LoginSuccessResponse { accessToken: string; + name: string; +} + +export interface UserLoginSuccessResponse extends LoginSuccessResponse { +} + +export interface AdminLoginSuccessResponse extends LoginSuccessResponse { + type: AdminType; + storeId: number | null; } export interface CurrentUserContext { diff --git a/frontend/src/context/AdminAuthContext.tsx b/frontend/src/context/AdminAuthContext.tsx new file mode 100644 index 00000000..3cfe96d0 --- /dev/null +++ b/frontend/src/context/AdminAuthContext.tsx @@ -0,0 +1,96 @@ +import { adminLogin as apiLogin, logout as apiLogout } from '@_api/auth/authAPI'; +import { + type AdminLoginSuccessResponse, + type AdminType, + type LoginRequest, +} from '@_api/auth/authTypes'; +import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react'; + +interface AdminAuthContextType { + isAdmin: boolean; + name: string | null; + type: AdminType | null; + storeId: number | null; + loading: boolean; + login: (data: Omit) => Promise; + logout: () => Promise; +} + +const AdminAuthContext = createContext(undefined); + +export const AdminAuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [isAdmin, setIsAdmin] = useState(false); + const [name, setName] = useState(null); + const [type, setType] = useState(null); + const [storeId, setStoreId] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + try { + const token = localStorage.getItem('adminAccessToken'); + const storedName = localStorage.getItem('adminName'); + const storedType = localStorage.getItem('adminType') as AdminType | null; + const storedStoreId = localStorage.getItem('adminStoreId'); + + if (token && storedName && storedType) { + setIsAdmin(true); + setName(storedName); + setType(storedType); + setStoreId(storedStoreId ? parseInt(storedStoreId, 10) : null); + } + } catch (error) { + console.error("Failed to load admin auth state from storage", error); + } finally { + setLoading(false); + } + }, []); + + const login = async (data: Omit) => { + const response = await apiLogin(data); + + localStorage.setItem('adminAccessToken', response.accessToken); + localStorage.setItem('adminName', response.name); + localStorage.setItem('adminType', response.type); + if (response.storeId) { + localStorage.setItem('adminStoreId', response.storeId.toString()); + } else { + localStorage.removeItem('adminStoreId'); + } + + setIsAdmin(true); + setName(response.name); + setType(response.type); + setStoreId(response.storeId); + + return response; + }; + + const logout = async () => { + try { + await apiLogout(); + } finally { + localStorage.removeItem('adminAccessToken'); + localStorage.removeItem('adminName'); + localStorage.removeItem('adminType'); + localStorage.removeItem('adminStoreId'); + setIsAdmin(false); + setName(null); + setType(null); + setStoreId(null); + } + }; + + return ( + + {children} + + ); +}; + +export const useAdminAuth = (): AdminAuthContextType => { + const context = useContext(AdminAuthContext); + if (!context) { + throw new Error('useAdminAuth must be used within an AdminAuthProvider'); + } + return context; +}; diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index e220f436..0acfca2f 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -1,15 +1,13 @@ -import {checkLogin as apiCheckLogin, login as apiLogin, logout as apiLogout} from '@_api/auth/authAPI'; -import {type LoginRequest, type LoginSuccessResponse, PrincipalType} from '@_api/auth/authTypes'; -import React, {createContext, type ReactNode, useContext, useEffect, useState} from 'react'; +import { logout as apiLogout, userLogin as apiLogin } from '@_api/auth/authAPI'; +import { type LoginRequest, type UserLoginSuccessResponse } from '@_api/auth/authTypes'; +import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react'; interface AuthContextType { loggedIn: boolean; userName: string | null; - type: PrincipalType | null; - loading: boolean; - login: (data: LoginRequest) => Promise; + loading: boolean; + login: (data: Omit) => Promise; logout: () => Promise; - checkLogin: () => Promise; } const AuthContext = createContext(undefined); @@ -17,33 +15,33 @@ const AuthContext = createContext(undefined); export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [loggedIn, setLoggedIn] = useState(false); const [userName, setUserName] = useState(null); - const [type, setType] = useState(null); - const [loading, setLoading] = useState(true); // Add loading state - - const checkLogin = async () => { - try { - const response = await apiCheckLogin(); - setLoggedIn(true); - setUserName(response.name); - setType(response.type); - } catch (error) { - setLoggedIn(false); - setUserName(null); - setType(null); - localStorage.removeItem('accessToken'); - } finally { - setLoading(false); // Set loading to false after check is complete - } - }; + const [loading, setLoading] = useState(true); useEffect(() => { - checkLogin(); + try { + const token = localStorage.getItem('accessToken'); + const storedUserName = localStorage.getItem('userName'); + + if (token && storedUserName) { + setLoggedIn(true); + setUserName(storedUserName); + } + } catch (error) { + console.error("Failed to load user auth state from storage", error); + } finally { + setLoading(false); + } }, []); - const login = async (data: LoginRequest) => { - const response = await apiLogin({ ...data }); + const login = async (data: Omit) => { + const response = await apiLogin(data); + + localStorage.setItem('accessToken', response.accessToken); + localStorage.setItem('userName', response.name); + setLoggedIn(true); - setType(data.principalType); + setUserName(response.name); + return response; }; @@ -51,15 +49,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => try { await apiLogout(); } finally { + localStorage.removeItem('accessToken'); + localStorage.removeItem('userName'); setLoggedIn(false); setUserName(null); - setType(null); - localStorage.removeItem('accessToken'); } }; return ( - + {children} ); diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index db4704d3..61474db7 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -15,11 +15,11 @@ const LoginPage: React.FC = () => { const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); try { - const principalType = from.startsWith('/admin') ? 'ADMIN' : 'USER'; - await login({ account: email, password: password, principalType: principalType }); + await login({ account: email, password: password }); alert('로그인에 성공했어요!'); - navigate(from, { replace: true }); + const redirectTo = from.startsWith('/admin') ? '/' : from; + navigate(redirectTo, { replace: true }); } catch (error: any) { const message = error.response?.data?.message || '로그인에 실패했어요. 이메일과 비밀번호를 확인해주세요.'; alert(message); diff --git a/frontend/src/pages/admin/AdminLayout.tsx b/frontend/src/pages/admin/AdminLayout.tsx index e5ca8e7d..4bdf205d 100644 --- a/frontend/src/pages/admin/AdminLayout.tsx +++ b/frontend/src/pages/admin/AdminLayout.tsx @@ -1,4 +1,6 @@ -import React, {type ReactNode} from 'react'; +import { useAdminAuth } from '@_context/AdminAuthContext'; +import React, { type ReactNode, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import AdminNavbar from './AdminNavbar'; interface AdminLayoutProps { @@ -6,6 +8,23 @@ interface AdminLayoutProps { } const AdminLayout: React.FC = ({ children }) => { + const { isAdmin, loading } = useAdminAuth(); + const navigate = useNavigate(); + + useEffect(() => { + if (!loading && !isAdmin) { + navigate('/admin/login'); + } + }, [isAdmin, loading, navigate]); + + if (loading) { + return
Loading...
; + } + + if (!isAdmin) { + return null; + } + return ( <> diff --git a/frontend/src/pages/admin/AdminLoginPage.tsx b/frontend/src/pages/admin/AdminLoginPage.tsx new file mode 100644 index 00000000..72e6b3bb --- /dev/null +++ b/frontend/src/pages/admin/AdminLoginPage.tsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useAdminAuth } from '@_context/AdminAuthContext'; +import '@_css/login-page-v2.css'; + +const AdminLoginPage: React.FC = () => { + const [account, setAccount] = useState(''); + const [password, setPassword] = useState(''); + const { login } = useAdminAuth(); + const navigate = useNavigate(); + const location = useLocation(); + + const from = location.state?.from?.pathname || '/admin'; + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await login({ account: account, password: password }); + alert('관리자 로그인에 성공했어요!'); + navigate(from, { replace: true }); + } catch (error: any) { + const message = error.response?.data?.message || '로그인에 실패했어요. 계정과 비밀번호를 확인해주세요.'; + alert(message); + console.error('관리자 로그인 실패:', error); + setPassword(''); + } + }; + + return ( +
+

관리자 로그인

+
+
+ setAccount(e.target.value)} + required + /> +
+
+ setPassword(e.target.value)} + required + /> +
+
+ +
+
+
+ ); +}; + +export default AdminLoginPage; diff --git a/frontend/src/pages/admin/AdminNavbar.tsx b/frontend/src/pages/admin/AdminNavbar.tsx index 8928fb1f..a5563d6f 100644 --- a/frontend/src/pages/admin/AdminNavbar.tsx +++ b/frontend/src/pages/admin/AdminNavbar.tsx @@ -1,10 +1,10 @@ +import {useAdminAuth} from '@_context/AdminAuthContext'; import React from 'react'; import {Link, useNavigate} from 'react-router-dom'; -import {useAuth} from '@_context/AuthContext'; import '@_css/navbar.css'; const AdminNavbar: React.FC = () => { - const { loggedIn, userName, logout } = useAuth(); + const { isAdmin, name, logout } = useAdminAuth(); const navigate = useNavigate(); const handleLogout = async (e: React.MouseEvent) => { @@ -25,12 +25,12 @@ const AdminNavbar: React.FC = () => { 일정
- {!loggedIn ? ( - + {!isAdmin ? ( + ) : (
Profile - {userName || 'Profile'} + {name || 'Profile'} diff --git a/frontend/src/pages/admin/AdminSchedulePage.tsx b/frontend/src/pages/admin/AdminSchedulePage.tsx index 9af77826..05d09faa 100644 --- a/frontend/src/pages/admin/AdminSchedulePage.tsx +++ b/frontend/src/pages/admin/AdminSchedulePage.tsx @@ -53,7 +53,7 @@ const AdminSchedulePage: React.FC = () => { const handleError = (err: any) => { if (isLoginRequiredError(err)) { alert('로그인이 필요해요.'); - navigate('/login', { state: { from: location } }); + navigate('/admin/login', { state: { from: location } }); } else { const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.'; alert(message); diff --git a/frontend/src/pages/admin/AdminThemeEditPage.tsx b/frontend/src/pages/admin/AdminThemeEditPage.tsx index 91af8d32..25658360 100644 --- a/frontend/src/pages/admin/AdminThemeEditPage.tsx +++ b/frontend/src/pages/admin/AdminThemeEditPage.tsx @@ -25,7 +25,7 @@ const AdminThemeEditPage: React.FC = () => { const handleError = (err: any) => { if (isLoginRequiredError(err)) { alert('로그인이 필요해요.'); - navigate('/login', { state: { from: location } }); + navigate('/admin/login', { state: { from: location } }); } else { const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.'; alert(message); diff --git a/frontend/src/pages/admin/AdminThemePage.tsx b/frontend/src/pages/admin/AdminThemePage.tsx index 8d69b19a..d9d7dd4b 100644 --- a/frontend/src/pages/admin/AdminThemePage.tsx +++ b/frontend/src/pages/admin/AdminThemePage.tsx @@ -13,7 +13,7 @@ const AdminThemePage: React.FC = () => { const handleError = (err: any) => { if (isLoginRequiredError(err)) { alert('로그인이 필요해요.'); - navigate('/login', { state: { from: location } }); + navigate('/admin/login', { state: { from: location } }); } else { const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.'; alert(message);