refactor: 프론트엔드에서 Admin 인증 Context 분리 및 별도의 로그인 페이지 생성

This commit is contained in:
이상진 2025-09-14 21:10:29 +09:00
parent c33ec686f9
commit 9361ea606b
12 changed files with 276 additions and 72 deletions

View File

@ -1,8 +1,9 @@
import {BrowserRouter as Router, Route, Routes} from 'react-router-dom'; import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import AdminRoute from './components/AdminRoute';
import Layout from './components/Layout'; 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 AdminLayout from './pages/admin/AdminLayout';
import AdminLoginPage from './pages/admin/AdminLoginPage';
import AdminPage from './pages/admin/AdminPage'; import AdminPage from './pages/admin/AdminPage';
import AdminSchedulePage from './pages/admin/AdminSchedulePage'; import AdminSchedulePage from './pages/admin/AdminSchedulePage';
import AdminThemeEditPage from './pages/admin/AdminThemeEditPage'; import AdminThemeEditPage from './pages/admin/AdminThemeEditPage';
@ -16,7 +17,16 @@ import ReservationStep2Page from '@_pages/ReservationStep2Page';
import ReservationSuccessPage from '@_pages/ReservationSuccessPage'; import ReservationSuccessPage from '@_pages/ReservationSuccessPage';
import SignupPage from '@_pages/SignupPage'; import SignupPage from '@_pages/SignupPage';
const AdminRoutes = () => ( function App() {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/admin/*" element={
<AdminAuthProvider>
<Routes>
<Route path="/login" element={<AdminLoginPage />} />
<Route path="/*" element={
<AdminLayout> <AdminLayout>
<Routes> <Routes>
<Route path="/" element={<AdminPage />} /> <Route path="/" element={<AdminPage />} />
@ -25,17 +35,9 @@ const AdminRoutes = () => (
<Route path="/schedule" element={<AdminSchedulePage />} /> <Route path="/schedule" element={<AdminSchedulePage />} />
</Routes> </Routes>
</AdminLayout> </AdminLayout>
); } />
</Routes>
function App() { </AdminAuthProvider>
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/admin/*" element={
<AdminRoute>
<AdminRoutes />
</AdminRoute>
} /> } />
<Route path="/*" element={ <Route path="/*" element={
<Layout> <Layout>

View File

@ -1,19 +1,31 @@
import apiClient from '@_api/apiClient'; import apiClient from '@_api/apiClient';
import type {CurrentUserContext, LoginRequest, LoginSuccessResponse} from './authTypes'; import {
type AdminLoginSuccessResponse,
type LoginRequest,
PrincipalType,
type UserLoginSuccessResponse,
} from './authTypes';
export const userLogin = async (
export const login = async (data: LoginRequest): Promise<LoginSuccessResponse> => { data: Omit<LoginRequest, 'principalType'>,
const response = await apiClient.post<LoginSuccessResponse>('/auth/login', data, false); ): Promise<UserLoginSuccessResponse> => {
localStorage.setItem('accessToken', response.accessToken); return await apiClient.post<UserLoginSuccessResponse>(
'/auth/login',
return response; { ...data, principalType: PrincipalType.USER },
false,
);
}; };
export const checkLogin = async (): Promise<CurrentUserContext> => { export const adminLogin = async (
return await apiClient.get<CurrentUserContext>('/auth/login/check', true); data: Omit<LoginRequest, 'principalType'>,
): Promise<AdminLoginSuccessResponse> => {
return await apiClient.post<AdminLoginSuccessResponse>(
'/auth/login',
{ ...data, principalType: PrincipalType.ADMIN },
false,
);
}; };
export const logout = async (): Promise<void> => { export const logout = async (): Promise<void> => {
await apiClient.post('/auth/logout', {}, true); await apiClient.post('/auth/logout', {}, true);
localStorage.removeItem('accessToken');
}; };

View File

@ -5,6 +5,13 @@ export const PrincipalType = {
export type PrincipalType = typeof PrincipalType[keyof typeof 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 { export interface LoginRequest {
account: string, account: string,
password: string; password: string;
@ -13,6 +20,15 @@ export interface LoginRequest {
export interface LoginSuccessResponse { export interface LoginSuccessResponse {
accessToken: string; accessToken: string;
name: string;
}
export interface UserLoginSuccessResponse extends LoginSuccessResponse {
}
export interface AdminLoginSuccessResponse extends LoginSuccessResponse {
type: AdminType;
storeId: number | null;
} }
export interface CurrentUserContext { export interface CurrentUserContext {

View File

@ -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<LoginRequest, 'principalType'>) => Promise<AdminLoginSuccessResponse>;
logout: () => Promise<void>;
}
const AdminAuthContext = createContext<AdminAuthContextType | undefined>(undefined);
export const AdminAuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [isAdmin, setIsAdmin] = useState(false);
const [name, setName] = useState<string | null>(null);
const [type, setType] = useState<AdminType | null>(null);
const [storeId, setStoreId] = useState<number | null>(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<LoginRequest, 'principalType'>) => {
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 (
<AdminAuthContext.Provider value={{ isAdmin, name, type, storeId, loading, login, logout }}>
{children}
</AdminAuthContext.Provider>
);
};
export const useAdminAuth = (): AdminAuthContextType => {
const context = useContext(AdminAuthContext);
if (!context) {
throw new Error('useAdminAuth must be used within an AdminAuthProvider');
}
return context;
};

View File

@ -1,15 +1,13 @@
import {checkLogin as apiCheckLogin, login as apiLogin, logout as apiLogout} from '@_api/auth/authAPI'; import { logout as apiLogout, userLogin as apiLogin } from '@_api/auth/authAPI';
import {type LoginRequest, type LoginSuccessResponse, PrincipalType} from '@_api/auth/authTypes'; import { type LoginRequest, type UserLoginSuccessResponse } from '@_api/auth/authTypes';
import React, {createContext, type ReactNode, useContext, useEffect, useState} from 'react'; import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react';
interface AuthContextType { interface AuthContextType {
loggedIn: boolean; loggedIn: boolean;
userName: string | null; userName: string | null;
type: PrincipalType | null;
loading: boolean; loading: boolean;
login: (data: LoginRequest) => Promise<LoginSuccessResponse>; login: (data: Omit<LoginRequest, 'principalType'>) => Promise<UserLoginSuccessResponse>;
logout: () => Promise<void>; logout: () => Promise<void>;
checkLogin: () => Promise<void>;
} }
const AuthContext = createContext<AuthContextType | undefined>(undefined); const AuthContext = createContext<AuthContextType | undefined>(undefined);
@ -17,33 +15,33 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [loggedIn, setLoggedIn] = useState(false); const [loggedIn, setLoggedIn] = useState(false);
const [userName, setUserName] = useState<string | null>(null); const [userName, setUserName] = useState<string | null>(null);
const [type, setType] = useState<PrincipalType | null>(null); const [loading, setLoading] = useState(true);
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
}
};
useEffect(() => { 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 login = async (data: Omit<LoginRequest, 'principalType'>) => {
const response = await apiLogin({ ...data }); const response = await apiLogin(data);
localStorage.setItem('accessToken', response.accessToken);
localStorage.setItem('userName', response.name);
setLoggedIn(true); setLoggedIn(true);
setType(data.principalType); setUserName(response.name);
return response; return response;
}; };
@ -51,15 +49,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
try { try {
await apiLogout(); await apiLogout();
} finally { } finally {
localStorage.removeItem('accessToken');
localStorage.removeItem('userName');
setLoggedIn(false); setLoggedIn(false);
setUserName(null); setUserName(null);
setType(null);
localStorage.removeItem('accessToken');
} }
}; };
return ( return (
<AuthContext.Provider value={{ loggedIn, userName, type, loading, login, logout, checkLogin }}> <AuthContext.Provider value={{ loggedIn, userName, loading, login, logout }}>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );

View File

@ -15,11 +15,11 @@ const LoginPage: React.FC = () => {
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
try { try {
const principalType = from.startsWith('/admin') ? 'ADMIN' : 'USER'; await login({ account: email, password: password });
await login({ account: email, password: password, principalType: principalType });
alert('로그인에 성공했어요!'); alert('로그인에 성공했어요!');
navigate(from, { replace: true }); const redirectTo = from.startsWith('/admin') ? '/' : from;
navigate(redirectTo, { replace: true });
} catch (error: any) { } catch (error: any) {
const message = error.response?.data?.message || '로그인에 실패했어요. 이메일과 비밀번호를 확인해주세요.'; const message = error.response?.data?.message || '로그인에 실패했어요. 이메일과 비밀번호를 확인해주세요.';
alert(message); alert(message);

View File

@ -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'; import AdminNavbar from './AdminNavbar';
interface AdminLayoutProps { interface AdminLayoutProps {
@ -6,6 +8,23 @@ interface AdminLayoutProps {
} }
const AdminLayout: React.FC<AdminLayoutProps> = ({ children }) => { const AdminLayout: React.FC<AdminLayoutProps> = ({ children }) => {
const { isAdmin, loading } = useAdminAuth();
const navigate = useNavigate();
useEffect(() => {
if (!loading && !isAdmin) {
navigate('/admin/login');
}
}, [isAdmin, loading, navigate]);
if (loading) {
return <div>Loading...</div>;
}
if (!isAdmin) {
return null;
}
return ( return (
<> <>
<AdminNavbar /> <AdminNavbar />

View File

@ -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 (
<div className="login-container-v2">
<h2 className="page-title"> </h2>
<form className="login-form-v2" onSubmit={handleLogin}>
<div className="form-group">
<input
type="text"
className="form-input"
placeholder="계정"
value={account}
onChange={e => setAccount(e.target.value)}
required
/>
</div>
<div className="form-group">
<input
type="password"
className="form-input"
placeholder="비밀번호"
value={password}
onChange={e => setPassword(e.target.value)}
required
/>
</div>
<div className="button-group">
<button type="submit" className="btn btn-primary"></button>
</div>
</form>
</div>
);
};
export default AdminLoginPage;

View File

@ -1,10 +1,10 @@
import {useAdminAuth} from '@_context/AdminAuthContext';
import React from 'react'; import React from 'react';
import {Link, useNavigate} from 'react-router-dom'; import {Link, useNavigate} from 'react-router-dom';
import {useAuth} from '@_context/AuthContext';
import '@_css/navbar.css'; import '@_css/navbar.css';
const AdminNavbar: React.FC = () => { const AdminNavbar: React.FC = () => {
const { loggedIn, userName, logout } = useAuth(); const { isAdmin, name, logout } = useAdminAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const handleLogout = async (e: React.MouseEvent) => { const handleLogout = async (e: React.MouseEvent) => {
@ -25,12 +25,12 @@ const AdminNavbar: React.FC = () => {
<Link className="nav-link" to="/admin/schedule"></Link> <Link className="nav-link" to="/admin/schedule"></Link>
</div> </div>
<div className="nav-actions"> <div className="nav-actions">
{!loggedIn ? ( {!isAdmin ? (
<button className="btn btn-primary" onClick={() => navigate('/v2/login')}>Login</button> <button className="btn btn-primary" onClick={() => navigate('/admin/login')}>Login</button>
) : ( ) : (
<div className="profile-info"> <div className="profile-info">
<img className="profile-image" src="/image/default-profile.png" alt="Profile" /> <img className="profile-image" src="/image/default-profile.png" alt="Profile" />
<span>{userName || 'Profile'}</span> <span>{name || 'Profile'}</span>
<div className="dropdown-menu"> <div className="dropdown-menu">
<a className="dropdown-item" href="#" onClick={handleLogout}>Logout</a> <a className="dropdown-item" href="#" onClick={handleLogout}>Logout</a>
</div> </div>

View File

@ -53,7 +53,7 @@ const AdminSchedulePage: React.FC = () => {
const handleError = (err: any) => { const handleError = (err: any) => {
if (isLoginRequiredError(err)) { if (isLoginRequiredError(err)) {
alert('로그인이 필요해요.'); alert('로그인이 필요해요.');
navigate('/login', { state: { from: location } }); navigate('/admin/login', { state: { from: location } });
} else { } else {
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.'; const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
alert(message); alert(message);

View File

@ -25,7 +25,7 @@ const AdminThemeEditPage: React.FC = () => {
const handleError = (err: any) => { const handleError = (err: any) => {
if (isLoginRequiredError(err)) { if (isLoginRequiredError(err)) {
alert('로그인이 필요해요.'); alert('로그인이 필요해요.');
navigate('/login', { state: { from: location } }); navigate('/admin/login', { state: { from: location } });
} else { } else {
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.'; const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
alert(message); alert(message);

View File

@ -13,7 +13,7 @@ const AdminThemePage: React.FC = () => {
const handleError = (err: any) => { const handleError = (err: any) => {
if (isLoginRequiredError(err)) { if (isLoginRequiredError(err)) {
alert('로그인이 필요해요.'); alert('로그인이 필요해요.');
navigate('/login', { state: { from: location } }); navigate('/admin/login', { state: { from: location } });
} else { } else {
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.'; const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
alert(message); alert(message);