import React, { useState, useEffect, useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; import { CButton, CCard, CCardBody, CCardHeader, CForm, CFormInput, CFormLabel, CInputGroup, CInputGroupText, CModal, CModalBody, CModalFooter, CModalHeader, CModalTitle, CTable, CTableBody, CTableDataCell, CTableHead, CTableHeaderCell, CTableRow, CFormSelect, CPagination, CPaginationItem, } from '@coreui/react'; import CIcon from '@coreui/icons-react'; import { cilPencil, cilTrash, cilPlus, cilSpeedometer, cilDrop, cilStar, cilMenu, cilBell, cilBrowser, cilCalculator, cilChartPie, cilCursor, cilDescription, cilNotes, cilPuzzle, cilSettings, cilUser, cilPeople, cilHome, cilFolder, cilFile, cilEnvelopeClosed, cilCalendar, cilCart, cilCreditCard, cilGraph, cilLayers, cilList, cilMap, cilMediaPlay, cilMoney, cilLockLocked, cilShieldAlt, cilTask, cilCloudDownload, cilCode, cilApplications, cilGrid, cilInfo, cilWarning, cilCheck, cilX, cilSearch, cilImage, cilCamera, cilHeart, cilThumbUp, cilCog, cilOptions, } from '@coreui/icons'; // 선택 가능한 아이콘 목록 const availableIcons = [ { name: 'cilSpeedometer', icon: cilSpeedometer, label: '대시보드' }, { name: 'cilHome', icon: cilHome, label: '홈' }, { name: 'cilMenu', icon: cilMenu, label: '메뉴' }, { name: 'cilUser', icon: cilUser, label: '사용자' }, { name: 'cilPeople', icon: cilPeople, label: '사람들' }, { name: 'cilSettings', icon: cilSettings, label: '설정' }, { name: 'cilCog', icon: cilCog, label: '톱니바퀴' }, { name: 'cilOptions', icon: cilOptions, label: '옵션' }, { name: 'cilFolder', icon: cilFolder, label: '폴더' }, { name: 'cilFile', icon: cilFile, label: '파일' }, { name: 'cilNotes', icon: cilNotes, label: '노트' }, { name: 'cilDescription', icon: cilDescription, label: '문서' }, { name: 'cilList', icon: cilList, label: '리스트' }, { name: 'cilGrid', icon: cilGrid, label: '그리드' }, { name: 'cilLayers', icon: cilLayers, label: '레이어' }, { name: 'cilApplications', icon: cilApplications, label: '앱' }, { name: 'cilBrowser', icon: cilBrowser, label: '브라우저' }, { name: 'cilChartPie', icon: cilChartPie, label: '파이차트' }, { name: 'cilGraph', icon: cilGraph, label: '그래프' }, { name: 'cilCalculator', icon: cilCalculator, label: '계산기' }, { name: 'cilCalendar', icon: cilCalendar, label: '달력' }, { name: 'cilClock', icon: cilCalendar, label: '시계' }, { name: 'cilBell', icon: cilBell, label: '알림' }, { name: 'cilEnvelopeClosed', icon: cilEnvelopeClosed, label: '메일' }, { name: 'cilCart', icon: cilCart, label: '장바구니' }, { name: 'cilCreditCard', icon: cilCreditCard, label: '카드' }, { name: 'cilMoney', icon: cilMoney, label: '돈' }, { name: 'cilLockLocked', icon: cilLockLocked, label: '잠금' }, { name: 'cilShieldAlt', icon: cilShieldAlt, label: '보안' }, { name: 'cilTask', icon: cilTask, label: '작업' }, { name: 'cilCloudDownload', icon: cilCloudDownload, label: '다운로드' }, { name: 'cilCode', icon: cilCode, label: '코드' }, { name: 'cilMap', icon: cilMap, label: '지도' }, { name: 'cilMediaPlay', icon: cilMediaPlay, label: '재생' }, { name: 'cilImage', icon: cilImage, label: '이미지' }, { name: 'cilCamera', icon: cilCamera, label: '카메라' }, { name: 'cilStar', icon: cilStar, label: '별' }, { name: 'cilHeart', icon: cilHeart, label: '하트' }, { name: 'cilThumbUp', icon: cilThumbUp, label: '좋아요' }, { name: 'cilDrop', icon: cilDrop, label: '색상' }, { name: 'cilPencil', icon: cilPencil, label: '편집' }, { name: 'cilCursor', icon: cilCursor, label: '커서' }, { name: 'cilPuzzle', icon: cilPuzzle, label: '퍼즐' }, { name: 'cilSearch', icon: cilSearch, label: '검색' }, { name: 'cilInfo', icon: cilInfo, label: '정보' }, { name: 'cilWarning', icon: cilWarning, label: '경고' }, { name: 'cilCheck', icon: cilCheck, label: '체크' }, { name: 'cilX', icon: cilX, label: '닫기' }, { name: 'cilPlus', icon: cilPlus, label: '추가' }, { name: 'cilTrash', icon: cilTrash, label: '삭제' }, ]; import { getAdminMenuListByParentSeq, getAdminMenu, addAdminMenu, updateAdminMenu, deleteAdminMenu, AdminMenu, AdminMenuPageResponse, } from 'src/services/adminMenuService'; // 브레드크럼 아이템 인터페이스 interface BreadcrumbItem { adminMenuSeq: number; menuName: string; } // 아이콘 이름으로 아이콘 객체 찾기 const getIconByName = (name: string) => { const found = availableIcons.find((item) => item.name === name); return found ? found.icon : null; }; const AdminMenuManagement: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams(); const [menuList, setMenuList] = useState([]); const [loading, setLoading] = useState(false); const [modalVisible, setModalVisible] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [hasChildMenus, setHasChildMenus] = useState(false); const [iconPickerVisible, setIconPickerVisible] = useState(false); const [isEditMode, setIsEditMode] = useState(false); const [selectedMenu, setSelectedMenu] = useState(null); const [formData, setFormData] = useState({ parentSeq: 0, menuOrder: 1, menuName: '', iconName: '', menuUrl: '', level: 1000, }); // 현재 보고 있는 부모 메뉴 seq (0이면 최상위) const [currentParentSeq, setCurrentParentSeq] = useState(0); // 브레드크럼 (탐색 경로) const [breadcrumb, setBreadcrumb] = useState([]); // 페이징 상태 const [currentPage, setCurrentPage] = useState(1); const [pageInfo, setPageInfo] = useState>({ pageNum: 1, pageSize: 0, totalContent: 0, totalPage: 0, isFirstPage: true, isLastPage: true, }); // 반응형 상태 (화면 너비에 따라 열 표시/숨김) const [isCompact, setIsCompact] = useState(false); // 화면 너비 변경 감지 const handleResize = useCallback(() => { setIsCompact(window.innerWidth < 992); }, []); useEffect(() => { handleResize(); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [handleResize]); // URL 파라미터를 업데이트하는 함수 const updateUrlParams = useCallback((parentSeq: number, pageNum: number) => { const params = new URLSearchParams(); if (parentSeq > 0) { params.set('parentSeq', String(parentSeq)); } if (pageNum > 1) { params.set('pageNum', String(pageNum)); } setSearchParams(params, { replace: false }); }, [setSearchParams]); // 메뉴 목록 조회 (parentSeq 기준) const fetchMenuList = async (parentSeq: number = 0, pageNum: number = 1) => { try { setLoading(true); const data = await getAdminMenuListByParentSeq(parentSeq, pageNum); setMenuList(data.content || []); setPageInfo({ pageNum: data.pageNum, pageSize: data.pageSize, totalContent: data.totalContent, totalPage: data.totalPage, isFirstPage: data.isFirstPage, isLastPage: data.isLastPage, }); setCurrentPage(data.pageNum); setCurrentParentSeq(parentSeq); } catch (error) { console.error('메뉴 목록 조회 실패:', error); alert('메뉴 목록을 불러오는데 실패했습니다.'); } finally { setLoading(false); } }; // 메뉴 클릭 시 하위 메뉴로 이동 const handleMenuClick = (menu: AdminMenu) => { if (menu.adminMenuSeq) { const newBreadcrumb = [...breadcrumb, { adminMenuSeq: menu.adminMenuSeq!, menuName: menu.menuName }]; setBreadcrumb(newBreadcrumb); updateUrlParams(menu.adminMenuSeq, 1); } }; // 브레드크럼 클릭 시 해당 위치로 이동 const handleBreadcrumbClick = (index: number) => { if (index === -1) { // 최상위로 이동 setBreadcrumb([]); updateUrlParams(0, 1); } else { // 특정 위치로 이동 const targetItem = breadcrumb[index]; const newBreadcrumb = breadcrumb.slice(0, index + 1); setBreadcrumb(newBreadcrumb); updateUrlParams(targetItem.adminMenuSeq, 1); } }; // 페이지 이동 함수 (URL 업데이트 포함) const handlePageChange = (pageNum: number) => { updateUrlParams(currentParentSeq, pageNum); }; // 브레드크럼 경로를 API로 복원하는 함수 const buildBreadcrumbPath = async (targetSeq: number): Promise => { const path: BreadcrumbItem[] = []; let currentSeq = targetSeq; while (currentSeq > 0) { try { const menu = await getAdminMenu(currentSeq); path.unshift({ adminMenuSeq: menu.adminMenuSeq!, menuName: menu.menuName }); currentSeq = menu.parentSeq; } catch { break; } } return path; }; // URL 파라미터 변경 감지 및 초기 로드 useEffect(() => { const parentSeqParam = searchParams.get('parentSeq'); const pageNumParam = searchParams.get('pageNum'); const parentSeq = parentSeqParam ? parseInt(parentSeqParam) : 0; const pageNum = pageNumParam ? parseInt(pageNumParam) : 1; const loadData = async () => { // 브레드크럼 복원 const currentBreadcrumbParent = breadcrumb.length > 0 ? breadcrumb[breadcrumb.length - 1].adminMenuSeq : 0; if (parentSeq !== currentBreadcrumbParent) { if (parentSeq === 0) { setBreadcrumb([]); } else { // 브레드크럼에서 해당 parentSeq를 찾아서 그 위치까지만 유지 const foundIndex = breadcrumb.findIndex(item => item.adminMenuSeq === parentSeq); if (foundIndex >= 0) { setBreadcrumb(breadcrumb.slice(0, foundIndex + 1)); } else { // 브레드크럼에 없으면 API로 경로 복원 (새로고침, 직접 URL 접근 등) const newPath = await buildBreadcrumbPath(parentSeq); setBreadcrumb(newPath); } } } setCurrentParentSeq(parentSeq); fetchMenuList(parentSeq, pageNum); }; loadData(); }, [searchParams]); // 메뉴 추가 모달 열기 const handleAddClick = () => { setIsEditMode(false); setFormData({ parentSeq: currentParentSeq, menuOrder: 1, menuName: '', iconName: '', menuUrl: '', level: 1000, }); setModalVisible(true); }; // 메뉴 수정 모달 열기 const handleEditClick = (menu: AdminMenu) => { setIsEditMode(true); setFormData({ ...menu }); setModalVisible(true); }; // 메뉴 삭제 모달 열기 const handleDeleteClick = async (menu: AdminMenu) => { if (!menu.adminMenuSeq) return; try { // 하위 메뉴 존재 여부 확인 const childMenusResponse = await getAdminMenuListByParentSeq(menu.adminMenuSeq); setHasChildMenus(childMenusResponse.totalContent > 0); setSelectedMenu(menu); setDeleteModalVisible(true); } catch (error) { console.error('하위 메뉴 확인 실패:', error); // 에러 발생 시에도 모달은 열되, 삭제 불가로 처리 setHasChildMenus(true); setSelectedMenu(menu); setDeleteModalVisible(true); } }; // 입력 필드 변경 처리 const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: name === 'parentSeq' || name === 'menuOrder' || name === 'level' ? parseInt(value) || 0 : value, })); }; // 아이콘 선택 처리 const handleIconSelect = (iconName: string) => { setFormData((prev) => ({ ...prev, iconName: iconName, })); setIconPickerVisible(false); }; // 아이콘 선택 해제 const handleIconClear = () => { setFormData((prev) => ({ ...prev, iconName: '', })); }; // 메뉴 저장 (추가/수정) const handleSave = async () => { try { if (!formData.menuName.trim()) { alert('메뉴 이름을 입력해주세요.'); return; } if (isEditMode) { await updateAdminMenu(formData); alert('메뉴가 수정되었습니다.'); } else { await addAdminMenu(formData); alert('메뉴가 추가되었습니다.'); } setModalVisible(false); fetchMenuList(currentParentSeq, currentPage); } catch (error) { console.error('메뉴 저장 실패:', error); alert('메뉴 저장에 실패했습니다.'); } }; // 메뉴 삭제 const handleDelete = async () => { if (!selectedMenu || !selectedMenu.adminMenuSeq) return; try { await deleteAdminMenu(selectedMenu.adminMenuSeq); alert('메뉴가 삭제되었습니다.'); setDeleteModalVisible(false); fetchMenuList(currentParentSeq, currentPage); } catch (error) { console.error('메뉴 삭제 실패:', error); alert('메뉴 삭제에 실패했습니다.'); } }; return ( <> 어드민 메뉴 관리 {/* 브레드크럼 네비게이션 */}
메뉴 추가
순번 {!isCompact && ( 아이콘 )} {!isCompact && ( 레벨 )} 메뉴 이름 {!isCompact && ( URL )} 작업 {loading ? ( 로딩 중... ) : menuList.length === 0 ? ( 등록된 메뉴가 없습니다. ) : ( menuList.map((menu) => ( {menu.menuOrder} {!isCompact && ( {menu.iconName && getIconByName(menu.iconName) ? ( ) : ( - )} )} {!isCompact && ( {menu.level} )} {!isCompact && (
{menu.menuUrl || '-'}
)} handleEditClick(menu)} title="수정" > handleDeleteClick(menu)} title="삭제" >
)) )}
{/* 페이징 */} {pageInfo.totalPage > 0 && (
총 {pageInfo.totalContent}건 중 {(currentPage - 1) * pageInfo.pageSize + 1}- {Math.min(currentPage * pageInfo.pageSize, pageInfo.totalContent)}건
handlePageChange(1)} style={{ cursor: pageInfo.isFirstPage ? 'default' : 'pointer' }} > « handlePageChange(currentPage - 1)} style={{ cursor: pageInfo.isFirstPage ? 'default' : 'pointer' }} > ‹ {Array.from({ length: pageInfo.totalPage }, (_, i) => i + 1).map((page) => ( handlePageChange(page)} style={{ cursor: 'pointer' }} > {page} ))} handlePageChange(currentPage + 1)} style={{ cursor: pageInfo.isLastPage ? 'default' : 'pointer' }} > › handlePageChange(pageInfo.totalPage)} style={{ cursor: pageInfo.isLastPage ? 'default' : 'pointer' }} > »
)}
{/* 추가/수정 모달 */} { setModalVisible(false); setIconPickerVisible(false); }} backdrop="static"> {isEditMode ? '메뉴 수정' : '메뉴 추가'}
메뉴 이름 *
부모 메뉴 item.adminMenuSeq === formData.parentSeq)?.menuName || `메뉴 번호: ${formData.parentSeq}` } readOnly disabled />
순번 * { const value = parseInt(e.target.value) || 1; setFormData((prev) => ({ ...prev, menuOrder: value < 1 ? 1 : value, })); }} placeholder="순번 (1 이상)" />
아이콘 이름 {formData.iconName && getIconByName(formData.iconName) ? ( ) : ( - )} setIconPickerVisible(!iconPickerVisible)} style={{ cursor: 'pointer' }} /> {formData.iconName && ( )} setIconPickerVisible(!iconPickerVisible)} > 선택 {iconPickerVisible && (
{availableIcons.map((item) => ( handleIconSelect(item.name)} title={`${item.label} (${item.name})`} style={{ width: '36px', height: '36px', padding: '4px' }} > ))}
)} 클릭하여 아이콘을 선택하세요 (선택사항)
메뉴 URL
접근 가능 최소 회원 레벨
{ setModalVisible(false); setIconPickerVisible(false); }}> 취소 {isEditMode ? '수정' : '추가'}
{/* 삭제 확인 모달 */} setDeleteModalVisible(false)} backdrop="static"> 메뉴 삭제 {hasChildMenus ? ( <>
삭제할 수 없습니다.

{selectedMenu?.menuName} 메뉴에 하위 메뉴가 존재합니다.

하위 메뉴를 먼저 모두 삭제한 후 다시 시도해주세요. ) : ( <> 정말로 {selectedMenu?.menuName} 메뉴를 삭제하시겠습니까? )}
setDeleteModalVisible(false)}> {hasChildMenus ? '닫기' : '취소'} {!hasChildMenus && ( 삭제 )}
); }; export default AdminMenuManagement;