diff --git a/src/_nav.tsx b/src/_nav.tsx index 1809cc9..11548bf 100644 --- a/src/_nav.tsx +++ b/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: , }, + { + component: CNavTitle, + name: 'Admin', + }, + { + component: CNavItem, + name: 'Menu Management', + to: '/admin/menu', + icon: , + }, { component: CNavTitle, name: 'Extras', diff --git a/src/routes/routes.ts b/src/routes/routes.ts index da4b3aa..17a5ef9 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -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 diff --git a/src/services/adminMenuService.ts b/src/services/adminMenuService.ts new file mode 100644 index 0000000..6fa1910 --- /dev/null +++ b/src/services/adminMenuService.ts @@ -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 => { + const response = await axios.get('/admin/menu/list'); + return response.data.resultData || []; +}; + +// parentSeq로 어드민 메뉴 목록 조회 +export const getAdminMenuListByParentSeq = async (parentSeq: number): Promise => { + const response = await axios.get(`/admin/menu/listByParentSeq/${parentSeq}`); + return response.data.resultData || []; +}; + +// adminMenuSeq로 어드민 메뉴 조회 +export const getAdminMenu = async (adminMenuSeq: number): Promise => { + const response = await axios.get(`/admin/menu/${adminMenuSeq}`); + return response.data.resultData; +}; + +// 어드민 메뉴 추가 +export const addAdminMenu = async (menu: AdminMenu): Promise => { + const response = await axios.post('/admin/menu/add', menu); + return response.data; +}; + +// 어드민 메뉴 수정 +export const updateAdminMenu = async (menu: AdminMenu): Promise => { + const response = await axios.post('/admin/menu/update', menu); + return response.data; +}; + +// 어드민 메뉴 삭제 +export const deleteAdminMenu = async (adminMenuSeq: number): Promise => { + const response = await axios.post('/admin/menu/delete', { adminMenuSeq }); + return response.data; +}; diff --git a/src/views/admin/AdminMenuManagement.tsx b/src/views/admin/AdminMenuManagement.tsx new file mode 100644 index 0000000..7d84231 --- /dev/null +++ b/src/views/admin/AdminMenuManagement.tsx @@ -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([]); + const [loading, setLoading] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [deleteModalVisible, setDeleteModalVisible] = useState(false); + const [isEditMode, setIsEditMode] = useState(false); + const [selectedMenu, setSelectedMenu] = useState(null); + const [formData, setFormData] = useState({ + 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) => { + 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(); + + // 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 ( + <> + + + 어드민 메뉴 관리 + + + 메뉴 추가 + + + + + + + 메뉴 번호 + 메뉴 이름 + 부모 번호 + 정렬 순서 + 아이콘 + URL + 액션 + + + + {loading ? ( + + + 로딩 중... + + + ) : getSortedMenuList().length === 0 ? ( + + + 등록된 메뉴가 없습니다. + + + ) : ( + getSortedMenuList().map((menu: any) => ( + + +
{menu.adminMenuSeq}
+
+ +
+ {menu.depth > 0 && '└ '} + {menu.menuName} +
+
+ + {menu.parentSeq === 0 ? ( + 최상위 + ) : ( + menu.parentSeq + )} + + {menu.menuOrder} + +
{menu.iconName || '-'}
+
+ +
{menu.menuUrl || '-'}
+
+ + handleEditClick(menu)} + > + + + handleDeleteClick(menu)} + > + + + +
+ )) + )} +
+
+
+
+ + {/* 추가/수정 모달 */} + setModalVisible(false)}> + + {isEditMode ? '메뉴 수정' : '메뉴 추가'} + + + +
+ 메뉴 이름 * + +
+
+ 부모 번호 + + 0이면 최상위 메뉴입니다. +
+
+ 정렬 순서 + +
+
+ 아이콘 이름 + +
+
+ 메뉴 URL + +
+
+
+ + setModalVisible(false)}> + 취소 + + + {isEditMode ? '수정' : '추가'} + + +
+ + {/* 삭제 확인 모달 */} + setDeleteModalVisible(false)}> + + 메뉴 삭제 + + + 정말로 {selectedMenu?.menuName} 메뉴를 삭제하시겠습니까? +
+ + 하위 메뉴가 있는 경우 함께 삭제되지 않습니다. 먼저 하위 메뉴를 삭제해주세요. + +
+ + setDeleteModalVisible(false)}> + 취소 + + + 삭제 + + +
+ + ); +}; + +export default AdminMenuManagement;