어드민 메뉴 관리 기능 추가
This commit is contained in:
11
src/_nav.tsx
11
src/_nav.tsx
@@ -15,6 +15,7 @@ import {
|
||||
cilPuzzle,
|
||||
cilSpeedometer,
|
||||
cilStar,
|
||||
cilMenu,
|
||||
} from '@coreui/icons'
|
||||
import { CNavGroup, CNavItem, CNavTitle } from '@coreui/react'
|
||||
|
||||
@@ -45,6 +46,16 @@ const _nav = [
|
||||
to: '/theme/typography',
|
||||
icon: <CIcon icon={cilPencil} customClassName="nav-icon" />,
|
||||
},
|
||||
{
|
||||
component: CNavTitle,
|
||||
name: 'Admin',
|
||||
},
|
||||
{
|
||||
component: CNavItem,
|
||||
name: 'Menu Management',
|
||||
to: '/admin/menu',
|
||||
icon: <CIcon icon={cilMenu} customClassName="nav-icon" />,
|
||||
},
|
||||
{
|
||||
component: CNavTitle,
|
||||
name: 'Extras',
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react'
|
||||
const Dashboard = React.lazy(() => import('src/views/dashboard/Dashboard'))
|
||||
const Colors = React.lazy(() => import('src/views/theme/colors/Colors'))
|
||||
const Typography = React.lazy(() => import('src/views/theme/typography/Typography'))
|
||||
const AdminMenuManagement = React.lazy(() => import('src/views/admin/AdminMenuManagement'))
|
||||
|
||||
const routes = [
|
||||
{ path: '/', exact: true, name: 'Home' },
|
||||
@@ -10,6 +11,7 @@ const routes = [
|
||||
{ path: '/theme', name: 'Theme', element: Colors, exact: true },
|
||||
{ path: '/theme/colors', name: 'Colors', element: Colors },
|
||||
{ path: '/theme/typography', name: 'Typography', element: Typography },
|
||||
{ path: '/admin/menu', name: 'Admin Menu Management', element: AdminMenuManagement },
|
||||
]
|
||||
|
||||
export default routes
|
||||
|
||||
54
src/services/adminMenuService.ts
Normal file
54
src/services/adminMenuService.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import axios from 'src/axios/axios';
|
||||
|
||||
// 어드민 메뉴 인터페이스
|
||||
export interface AdminMenu {
|
||||
adminMenuSeq?: number;
|
||||
parentSeq: number;
|
||||
menuOrder: number;
|
||||
menuName: string;
|
||||
iconName?: string;
|
||||
menuUrl?: string;
|
||||
}
|
||||
|
||||
// API 응답 인터페이스
|
||||
export interface AdminMenuResponse {
|
||||
resultCode: string;
|
||||
resultMessage: string;
|
||||
resultData?: any;
|
||||
}
|
||||
|
||||
// 어드민 메뉴 목록 조회
|
||||
export const getAdminMenuList = async (): Promise<AdminMenu[]> => {
|
||||
const response = await axios.get<AdminMenuResponse>('/admin/menu/list');
|
||||
return response.data.resultData || [];
|
||||
};
|
||||
|
||||
// parentSeq로 어드민 메뉴 목록 조회
|
||||
export const getAdminMenuListByParentSeq = async (parentSeq: number): Promise<AdminMenu[]> => {
|
||||
const response = await axios.get<AdminMenuResponse>(`/admin/menu/listByParentSeq/${parentSeq}`);
|
||||
return response.data.resultData || [];
|
||||
};
|
||||
|
||||
// adminMenuSeq로 어드민 메뉴 조회
|
||||
export const getAdminMenu = async (adminMenuSeq: number): Promise<AdminMenu> => {
|
||||
const response = await axios.get<AdminMenuResponse>(`/admin/menu/${adminMenuSeq}`);
|
||||
return response.data.resultData;
|
||||
};
|
||||
|
||||
// 어드민 메뉴 추가
|
||||
export const addAdminMenu = async (menu: AdminMenu): Promise<AdminMenuResponse> => {
|
||||
const response = await axios.post<AdminMenuResponse>('/admin/menu/add', menu);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 어드민 메뉴 수정
|
||||
export const updateAdminMenu = async (menu: AdminMenu): Promise<AdminMenuResponse> => {
|
||||
const response = await axios.post<AdminMenuResponse>('/admin/menu/update', menu);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 어드민 메뉴 삭제
|
||||
export const deleteAdminMenu = async (adminMenuSeq: number): Promise<AdminMenuResponse> => {
|
||||
const response = await axios.post<AdminMenuResponse>('/admin/menu/delete', { adminMenuSeq });
|
||||
return response.data;
|
||||
};
|
||||
366
src/views/admin/AdminMenuManagement.tsx
Normal file
366
src/views/admin/AdminMenuManagement.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
CButton,
|
||||
CCard,
|
||||
CCardBody,
|
||||
CCardHeader,
|
||||
CCol,
|
||||
CForm,
|
||||
CFormInput,
|
||||
CFormLabel,
|
||||
CModal,
|
||||
CModalBody,
|
||||
CModalFooter,
|
||||
CModalHeader,
|
||||
CModalTitle,
|
||||
CRow,
|
||||
CTable,
|
||||
CTableBody,
|
||||
CTableDataCell,
|
||||
CTableHead,
|
||||
CTableHeaderCell,
|
||||
CTableRow,
|
||||
CBadge,
|
||||
} from '@coreui/react';
|
||||
import CIcon from '@coreui/icons-react';
|
||||
import { cilPencil, cilTrash, cilPlus } from '@coreui/icons';
|
||||
import {
|
||||
getAdminMenuList,
|
||||
addAdminMenu,
|
||||
updateAdminMenu,
|
||||
deleteAdminMenu,
|
||||
AdminMenu,
|
||||
} from 'src/services/adminMenuService';
|
||||
|
||||
const AdminMenuManagement: React.FC = () => {
|
||||
const [menuList, setMenuList] = useState<AdminMenu[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [selectedMenu, setSelectedMenu] = useState<AdminMenu | null>(null);
|
||||
const [formData, setFormData] = useState<AdminMenu>({
|
||||
parentSeq: 0,
|
||||
menuOrder: 0,
|
||||
menuName: '',
|
||||
iconName: '',
|
||||
menuUrl: '',
|
||||
});
|
||||
|
||||
// 메뉴 목록 조회
|
||||
const fetchMenuList = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await getAdminMenuList();
|
||||
setMenuList(data);
|
||||
} catch (error) {
|
||||
console.error('메뉴 목록 조회 실패:', error);
|
||||
alert('메뉴 목록을 불러오는데 실패했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMenuList();
|
||||
}, []);
|
||||
|
||||
// 메뉴 추가 모달 열기
|
||||
const handleAddClick = () => {
|
||||
setIsEditMode(false);
|
||||
setFormData({
|
||||
parentSeq: 0,
|
||||
menuOrder: 0,
|
||||
menuName: '',
|
||||
iconName: '',
|
||||
menuUrl: '',
|
||||
});
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
// 메뉴 수정 모달 열기
|
||||
const handleEditClick = (menu: AdminMenu) => {
|
||||
setIsEditMode(true);
|
||||
setFormData({ ...menu });
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
// 메뉴 삭제 모달 열기
|
||||
const handleDeleteClick = (menu: AdminMenu) => {
|
||||
setSelectedMenu(menu);
|
||||
setDeleteModalVisible(true);
|
||||
};
|
||||
|
||||
// 입력 필드 변경 처리
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: name === 'parentSeq' || name === 'menuOrder' ? parseInt(value) || 0 : value,
|
||||
}));
|
||||
};
|
||||
|
||||
// 메뉴 저장 (추가/수정)
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (!formData.menuName.trim()) {
|
||||
alert('메뉴 이름을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditMode) {
|
||||
await updateAdminMenu(formData);
|
||||
alert('메뉴가 수정되었습니다.');
|
||||
} else {
|
||||
await addAdminMenu(formData);
|
||||
alert('메뉴가 추가되었습니다.');
|
||||
}
|
||||
|
||||
setModalVisible(false);
|
||||
fetchMenuList();
|
||||
} catch (error) {
|
||||
console.error('메뉴 저장 실패:', error);
|
||||
alert('메뉴 저장에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 메뉴 삭제
|
||||
const handleDelete = async () => {
|
||||
if (!selectedMenu || !selectedMenu.adminMenuSeq) return;
|
||||
|
||||
try {
|
||||
await deleteAdminMenu(selectedMenu.adminMenuSeq);
|
||||
alert('메뉴가 삭제되었습니다.');
|
||||
setDeleteModalVisible(false);
|
||||
fetchMenuList();
|
||||
} catch (error) {
|
||||
console.error('메뉴 삭제 실패:', error);
|
||||
alert('메뉴 삭제에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 계층 구조 표시를 위한 메뉴 정렬
|
||||
const getSortedMenuList = () => {
|
||||
const sortedList: AdminMenu[] = [];
|
||||
const menuMap = new Map<number, AdminMenu[]>();
|
||||
|
||||
// parentSeq별로 그룹화
|
||||
menuList.forEach((menu) => {
|
||||
const key = menu.parentSeq;
|
||||
if (!menuMap.has(key)) {
|
||||
menuMap.set(key, []);
|
||||
}
|
||||
menuMap.get(key)?.push(menu);
|
||||
});
|
||||
|
||||
// 각 그룹 내에서 menuOrder로 정렬
|
||||
menuMap.forEach((menus) => {
|
||||
menus.sort((a, b) => a.menuOrder - b.menuOrder);
|
||||
});
|
||||
|
||||
// 계층 구조로 정렬
|
||||
const addChildren = (parentSeq: number, depth: number = 0) => {
|
||||
const children = menuMap.get(parentSeq) || [];
|
||||
children.forEach((menu) => {
|
||||
sortedList.push({ ...menu, depth } as any);
|
||||
addChildren(menu.adminMenuSeq || 0, depth + 1);
|
||||
});
|
||||
};
|
||||
|
||||
addChildren(0);
|
||||
return sortedList;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CCard className="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>어드민 메뉴 관리</strong>
|
||||
<CButton
|
||||
color="primary"
|
||||
size="sm"
|
||||
className="float-end"
|
||||
onClick={handleAddClick}
|
||||
>
|
||||
<CIcon icon={cilPlus} className="me-1" />
|
||||
메뉴 추가
|
||||
</CButton>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<CTable align="middle" className="mb-0 border" hover responsive>
|
||||
<CTableHead className="text-nowrap">
|
||||
<CTableRow>
|
||||
<CTableHeaderCell className="bg-body-tertiary text-center">메뉴 번호</CTableHeaderCell>
|
||||
<CTableHeaderCell className="bg-body-tertiary">메뉴 이름</CTableHeaderCell>
|
||||
<CTableHeaderCell className="bg-body-tertiary text-center">부모 번호</CTableHeaderCell>
|
||||
<CTableHeaderCell className="bg-body-tertiary text-center">정렬 순서</CTableHeaderCell>
|
||||
<CTableHeaderCell className="bg-body-tertiary text-center">아이콘</CTableHeaderCell>
|
||||
<CTableHeaderCell className="bg-body-tertiary">URL</CTableHeaderCell>
|
||||
<CTableHeaderCell className="bg-body-tertiary text-center">액션</CTableHeaderCell>
|
||||
</CTableRow>
|
||||
</CTableHead>
|
||||
<CTableBody>
|
||||
{loading ? (
|
||||
<CTableRow>
|
||||
<CTableDataCell colSpan={7} className="text-center">
|
||||
로딩 중...
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
) : getSortedMenuList().length === 0 ? (
|
||||
<CTableRow>
|
||||
<CTableDataCell colSpan={7} className="text-center">
|
||||
등록된 메뉴가 없습니다.
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
) : (
|
||||
getSortedMenuList().map((menu: any) => (
|
||||
<CTableRow key={menu.adminMenuSeq}>
|
||||
<CTableDataCell className="text-center">
|
||||
<div className="fw-semibold">{menu.adminMenuSeq}</div>
|
||||
</CTableDataCell>
|
||||
<CTableDataCell>
|
||||
<div style={{ marginLeft: `${menu.depth * 20}px` }}>
|
||||
{menu.depth > 0 && '└ '}
|
||||
{menu.menuName}
|
||||
</div>
|
||||
</CTableDataCell>
|
||||
<CTableDataCell className="text-center">
|
||||
{menu.parentSeq === 0 ? (
|
||||
<CBadge color="info">최상위</CBadge>
|
||||
) : (
|
||||
menu.parentSeq
|
||||
)}
|
||||
</CTableDataCell>
|
||||
<CTableDataCell className="text-center">{menu.menuOrder}</CTableDataCell>
|
||||
<CTableDataCell className="text-center">
|
||||
<div className="small text-body-secondary">{menu.iconName || '-'}</div>
|
||||
</CTableDataCell>
|
||||
<CTableDataCell>
|
||||
<div className="small text-body-secondary text-nowrap">{menu.menuUrl || '-'}</div>
|
||||
</CTableDataCell>
|
||||
<CTableDataCell className="text-center">
|
||||
<CButton
|
||||
color="info"
|
||||
size="sm"
|
||||
className="me-2"
|
||||
onClick={() => handleEditClick(menu)}
|
||||
>
|
||||
<CIcon icon={cilPencil} />
|
||||
</CButton>
|
||||
<CButton
|
||||
color="danger"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteClick(menu)}
|
||||
>
|
||||
<CIcon icon={cilTrash} />
|
||||
</CButton>
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
))
|
||||
)}
|
||||
</CTableBody>
|
||||
</CTable>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
|
||||
{/* 추가/수정 모달 */}
|
||||
<CModal visible={modalVisible} onClose={() => setModalVisible(false)}>
|
||||
<CModalHeader>
|
||||
<CModalTitle>{isEditMode ? '메뉴 수정' : '메뉴 추가'}</CModalTitle>
|
||||
</CModalHeader>
|
||||
<CModalBody>
|
||||
<CForm>
|
||||
<div className="mb-3">
|
||||
<CFormLabel htmlFor="menuName">메뉴 이름 *</CFormLabel>
|
||||
<CFormInput
|
||||
type="text"
|
||||
id="menuName"
|
||||
name="menuName"
|
||||
value={formData.menuName}
|
||||
onChange={handleInputChange}
|
||||
placeholder="메뉴 이름을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<CFormLabel htmlFor="parentSeq">부모 번호</CFormLabel>
|
||||
<CFormInput
|
||||
type="number"
|
||||
id="parentSeq"
|
||||
name="parentSeq"
|
||||
value={formData.parentSeq}
|
||||
onChange={handleInputChange}
|
||||
placeholder="0 (최상위 메뉴)"
|
||||
/>
|
||||
<small className="text-muted">0이면 최상위 메뉴입니다.</small>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<CFormLabel htmlFor="menuOrder">정렬 순서</CFormLabel>
|
||||
<CFormInput
|
||||
type="number"
|
||||
id="menuOrder"
|
||||
name="menuOrder"
|
||||
value={formData.menuOrder}
|
||||
onChange={handleInputChange}
|
||||
placeholder="정렬 순서"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<CFormLabel htmlFor="iconName">아이콘 이름</CFormLabel>
|
||||
<CFormInput
|
||||
type="text"
|
||||
id="iconName"
|
||||
name="iconName"
|
||||
value={formData.iconName || ''}
|
||||
onChange={handleInputChange}
|
||||
placeholder="아이콘 이름 (선택사항)"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<CFormLabel htmlFor="menuUrl">메뉴 URL</CFormLabel>
|
||||
<CFormInput
|
||||
type="text"
|
||||
id="menuUrl"
|
||||
name="menuUrl"
|
||||
value={formData.menuUrl || ''}
|
||||
onChange={handleInputChange}
|
||||
placeholder="/admin/example (선택사항)"
|
||||
/>
|
||||
</div>
|
||||
</CForm>
|
||||
</CModalBody>
|
||||
<CModalFooter>
|
||||
<CButton color="secondary" onClick={() => setModalVisible(false)}>
|
||||
취소
|
||||
</CButton>
|
||||
<CButton color="primary" onClick={handleSave}>
|
||||
{isEditMode ? '수정' : '추가'}
|
||||
</CButton>
|
||||
</CModalFooter>
|
||||
</CModal>
|
||||
|
||||
{/* 삭제 확인 모달 */}
|
||||
<CModal visible={deleteModalVisible} onClose={() => setDeleteModalVisible(false)}>
|
||||
<CModalHeader>
|
||||
<CModalTitle>메뉴 삭제</CModalTitle>
|
||||
</CModalHeader>
|
||||
<CModalBody>
|
||||
정말로 <strong>{selectedMenu?.menuName}</strong> 메뉴를 삭제하시겠습니까?
|
||||
<br />
|
||||
<small className="text-danger">
|
||||
하위 메뉴가 있는 경우 함께 삭제되지 않습니다. 먼저 하위 메뉴를 삭제해주세요.
|
||||
</small>
|
||||
</CModalBody>
|
||||
<CModalFooter>
|
||||
<CButton color="secondary" onClick={() => setDeleteModalVisible(false)}>
|
||||
취소
|
||||
</CButton>
|
||||
<CButton color="danger" onClick={handleDelete}>
|
||||
삭제
|
||||
</CButton>
|
||||
</CModalFooter>
|
||||
</CModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminMenuManagement;
|
||||
Reference in New Issue
Block a user