generated from pricelees/issue-pr-template
[#44] 매장 기능 도입 #45
@ -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 = () => (
|
||||
<AdminLayout>
|
||||
<Routes>
|
||||
<Route path="/" element={<AdminPage />} />
|
||||
<Route path="/theme" element={<AdminThemePage />} />
|
||||
<Route path="/theme/edit/:themeId" element={<AdminThemeEditPage />} />
|
||||
<Route path="/schedule" element={<AdminSchedulePage />} />
|
||||
</Routes>
|
||||
</AdminLayout>
|
||||
);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/admin/*" element={
|
||||
<AdminRoute>
|
||||
<AdminRoutes />
|
||||
</AdminRoute>
|
||||
<AdminAuthProvider>
|
||||
<Routes>
|
||||
<Route path="/login" element={<AdminLoginPage />} />
|
||||
<Route path="/*" element={
|
||||
<AdminLayout>
|
||||
<Routes>
|
||||
<Route path="/" element={<AdminPage />} />
|
||||
<Route path="/theme" element={<AdminThemePage />} />
|
||||
<Route path="/theme/edit/:themeId" element={<AdminThemeEditPage />} />
|
||||
<Route path="/schedule" element={<AdminSchedulePage />} />
|
||||
</Routes>
|
||||
</AdminLayout>
|
||||
} />
|
||||
</Routes>
|
||||
</AdminAuthProvider>
|
||||
} />
|
||||
<Route path="/*" element={
|
||||
<Layout>
|
||||
|
||||
@ -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<LoginSuccessResponse> => {
|
||||
const response = await apiClient.post<LoginSuccessResponse>('/auth/login', data, false);
|
||||
localStorage.setItem('accessToken', response.accessToken);
|
||||
|
||||
return response;
|
||||
export const userLogin = async (
|
||||
data: Omit<LoginRequest, 'principalType'>,
|
||||
): Promise<UserLoginSuccessResponse> => {
|
||||
return await apiClient.post<UserLoginSuccessResponse>(
|
||||
'/auth/login',
|
||||
{ ...data, principalType: PrincipalType.USER },
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
export const checkLogin = async (): Promise<CurrentUserContext> => {
|
||||
return await apiClient.get<CurrentUserContext>('/auth/login/check', true);
|
||||
export const adminLogin = async (
|
||||
data: Omit<LoginRequest, 'principalType'>,
|
||||
): Promise<AdminLoginSuccessResponse> => {
|
||||
return await apiClient.post<AdminLoginSuccessResponse>(
|
||||
'/auth/login',
|
||||
{ ...data, principalType: PrincipalType.ADMIN },
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
export const logout = async (): Promise<void> => {
|
||||
await apiClient.post('/auth/logout', {}, true);
|
||||
localStorage.removeItem('accessToken');
|
||||
};
|
||||
|
||||
@ -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 {
|
||||
|
||||
96
frontend/src/context/AdminAuthContext.tsx
Normal file
96
frontend/src/context/AdminAuthContext.tsx
Normal 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;
|
||||
};
|
||||
@ -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<LoginSuccessResponse>;
|
||||
login: (data: Omit<LoginRequest, 'principalType'>) => Promise<UserLoginSuccessResponse>;
|
||||
logout: () => Promise<void>;
|
||||
checkLogin: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
@ -17,33 +15,33 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [loggedIn, setLoggedIn] = useState(false);
|
||||
const [userName, setUserName] = useState<string | null>(null);
|
||||
const [type, setType] = useState<PrincipalType | null>(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<LoginRequest, 'principalType'>) => {
|
||||
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 (
|
||||
<AuthContext.Provider value={{ loggedIn, userName, type, loading, login, logout, checkLogin }}>
|
||||
<AuthContext.Provider value={{ loggedIn, userName, loading, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<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 (
|
||||
<>
|
||||
<AdminNavbar />
|
||||
|
||||
61
frontend/src/pages/admin/AdminLoginPage.tsx
Normal file
61
frontend/src/pages/admin/AdminLoginPage.tsx
Normal 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;
|
||||
@ -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 = () => {
|
||||
<Link className="nav-link" to="/admin/schedule">일정</Link>
|
||||
</div>
|
||||
<div className="nav-actions">
|
||||
{!loggedIn ? (
|
||||
<button className="btn btn-primary" onClick={() => navigate('/v2/login')}>Login</button>
|
||||
{!isAdmin ? (
|
||||
<button className="btn btn-primary" onClick={() => navigate('/admin/login')}>Login</button>
|
||||
) : (
|
||||
<div className="profile-info">
|
||||
<img className="profile-image" src="/image/default-profile.png" alt="Profile" />
|
||||
<span>{userName || 'Profile'}</span>
|
||||
<span>{name || 'Profile'}</span>
|
||||
<div className="dropdown-menu">
|
||||
<a className="dropdown-item" href="#" onClick={handleLogout}>Logout</a>
|
||||
</div>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user