Files
artwork21c.sample.admin.react/src/views/admin/AdminMenuManagement.tsx

828 lines
37 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<AdminMenu[]>([]);
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<AdminMenu | null>(null);
const [formData, setFormData] = useState<AdminMenu>({
parentSeq: 0,
menuOrder: 1,
menuName: '',
iconName: '',
menuUrl: '',
level: 1000,
});
// 현재 보고 있는 부모 메뉴 seq (0이면 최상위)
const [currentParentSeq, setCurrentParentSeq] = useState(0);
// 브레드크럼 (탐색 경로)
const [breadcrumb, setBreadcrumb] = useState<BreadcrumbItem[]>([]);
// 페이징 상태
const [currentPage, setCurrentPage] = useState(1);
const [pageInfo, setPageInfo] = useState<Omit<AdminMenuPageResponse, 'content'>>({
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<BreadcrumbItem[]> => {
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<HTMLInputElement>) => {
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 (
<>
<CCard className="mb-4">
<CCardHeader>
<strong> </strong>
</CCardHeader>
<CCardBody>
{/* 브레드크럼 네비게이션 */}
<div className="d-flex justify-content-between align-items-center mb-3">
<nav aria-label="breadcrumb">
<ol className="breadcrumb mb-0">
<li className={`breadcrumb-item ${breadcrumb.length === 0 ? 'active' : ''}`}>
{breadcrumb.length === 0 ? (
<span><CIcon icon={cilHome} className="me-1" /> </span>
) : (
<a
href="#"
onClick={(e) => { e.preventDefault(); handleBreadcrumbClick(-1); }}
style={{ textDecoration: 'none' }}
>
<CIcon icon={cilHome} className="me-1" />
</a>
)}
</li>
{breadcrumb.map((item, index) => (
<li
key={item.adminMenuSeq}
className={`breadcrumb-item ${index === breadcrumb.length - 1 ? 'active' : ''}`}
>
{index === breadcrumb.length - 1 ? (
<span>{item.menuName}</span>
) : (
<a
href="#"
onClick={(e) => { e.preventDefault(); handleBreadcrumbClick(index); }}
style={{ textDecoration: 'none' }}
>
{item.menuName}
</a>
)}
</li>
))}
</ol>
</nav>
<CButton
color="primary"
size="sm"
onClick={handleAddClick}
>
<CIcon icon={cilPlus} className="me-1" />
</CButton>
</div>
<CTable align="middle" className="mb-0 border" hover responsive style={{ tableLayout: 'fixed', minWidth: isCompact ? 'auto' : '650px' }}>
<CTableHead className="text-nowrap">
<CTableRow>
<CTableHeaderCell className="bg-body-tertiary text-center" style={{ width: '70px' }}></CTableHeaderCell>
{!isCompact && (
<CTableHeaderCell className="bg-body-tertiary text-center" style={{ width: '60px' }}></CTableHeaderCell>
)}
{!isCompact && (
<CTableHeaderCell className="bg-body-tertiary text-center" style={{ width: '80px' }}></CTableHeaderCell>
)}
<CTableHeaderCell className="bg-body-tertiary text-center" style={{ width: isCompact ? 'auto' : '180px' }}> </CTableHeaderCell>
{!isCompact && (
<CTableHeaderCell className="bg-body-tertiary text-center" style={{ width: 'auto' }}>URL</CTableHeaderCell>
)}
<CTableHeaderCell className="bg-body-tertiary text-center" style={{ width: '100px' }}></CTableHeaderCell>
</CTableRow>
</CTableHead>
<CTableBody style={{ minHeight: '200px' }}>
{loading ? (
<CTableRow>
<CTableDataCell colSpan={isCompact ? 3 : 5} className="text-center py-5">
...
</CTableDataCell>
</CTableRow>
) : menuList.length === 0 ? (
<CTableRow>
<CTableDataCell colSpan={isCompact ? 3 : 5} className="text-center">
.
</CTableDataCell>
</CTableRow>
) : (
menuList.map((menu) => (
<CTableRow key={menu.adminMenuSeq}>
<CTableDataCell className="text-center">{menu.menuOrder}</CTableDataCell>
{!isCompact && (
<CTableDataCell className="text-center">
{menu.iconName && getIconByName(menu.iconName) ? (
<CIcon icon={getIconByName(menu.iconName)!} />
) : (
<span className="text-body-secondary">-</span>
)}
</CTableDataCell>
)}
{!isCompact && (
<CTableDataCell className="text-center">{menu.level}</CTableDataCell>
)}
<CTableDataCell className="text-center">
<div
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={`${menu.menuName} - 클릭하여 하위 메뉴 보기`}
>
<a
href="#"
onClick={(e) => { e.preventDefault(); handleMenuClick(menu); }}
style={{ textDecoration: 'none', color: 'inherit' }}
className="fw-semibold"
>
{menu.menuName}
</a>
</div>
</CTableDataCell>
{!isCompact && (
<CTableDataCell className="text-center">
<div
className="small text-body-secondary"
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={menu.menuUrl || ''}
>
{menu.menuUrl || '-'}
</div>
</CTableDataCell>
)}
<CTableDataCell className="text-center">
<CButton
color="info"
size="sm"
className="me-2"
onClick={() => handleEditClick(menu)}
title="수정"
>
<CIcon icon={cilPencil} />
</CButton>
<CButton
color="danger"
size="sm"
onClick={() => handleDeleteClick(menu)}
title="삭제"
>
<CIcon icon={cilTrash} />
</CButton>
</CTableDataCell>
</CTableRow>
))
)}
</CTableBody>
</CTable>
{/* 페이징 */}
{pageInfo.totalPage > 0 && (
<div className="d-flex justify-content-between align-items-center mt-3">
<div className="text-muted small">
{pageInfo.totalContent} {(currentPage - 1) * pageInfo.pageSize + 1}-
{Math.min(currentPage * pageInfo.pageSize, pageInfo.totalContent)}
</div>
<CPagination aria-label="Page navigation">
<CPaginationItem
aria-label="First"
disabled={pageInfo.isFirstPage}
onClick={() => handlePageChange(1)}
style={{ cursor: pageInfo.isFirstPage ? 'default' : 'pointer' }}
>
«
</CPaginationItem>
<CPaginationItem
aria-label="Previous"
disabled={pageInfo.isFirstPage}
onClick={() => handlePageChange(currentPage - 1)}
style={{ cursor: pageInfo.isFirstPage ? 'default' : 'pointer' }}
>
</CPaginationItem>
{Array.from({ length: pageInfo.totalPage }, (_, i) => i + 1).map((page) => (
<CPaginationItem
key={page}
active={page === currentPage}
onClick={() => handlePageChange(page)}
style={{ cursor: 'pointer' }}
>
{page}
</CPaginationItem>
))}
<CPaginationItem
aria-label="Next"
disabled={pageInfo.isLastPage}
onClick={() => handlePageChange(currentPage + 1)}
style={{ cursor: pageInfo.isLastPage ? 'default' : 'pointer' }}
>
</CPaginationItem>
<CPaginationItem
aria-label="Last"
disabled={pageInfo.isLastPage}
onClick={() => handlePageChange(pageInfo.totalPage)}
style={{ cursor: pageInfo.isLastPage ? 'default' : 'pointer' }}
>
»
</CPaginationItem>
</CPagination>
</div>
)}
</CCardBody>
</CCard>
{/* 추가/수정 모달 */}
<CModal visible={modalVisible} onClose={() => { setModalVisible(false); setIconPickerVisible(false); }} backdrop="static">
<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> </CFormLabel>
<CFormInput
type="text"
value={
formData.parentSeq === 0
? '최상위 메뉴'
: breadcrumb.find(item => item.adminMenuSeq === formData.parentSeq)?.menuName || `메뉴 번호: ${formData.parentSeq}`
}
readOnly
disabled
/>
</div>
<div className="mb-3">
<CFormLabel htmlFor="menuOrder"> *</CFormLabel>
<CFormInput
type="number"
id="menuOrder"
name="menuOrder"
min={1}
value={formData.menuOrder}
onChange={(e) => {
const value = parseInt(e.target.value) || 1;
setFormData((prev) => ({
...prev,
menuOrder: value < 1 ? 1 : value,
}));
}}
placeholder="순번 (1 이상)"
/>
</div>
<div className="mb-3">
<CFormLabel> </CFormLabel>
<CInputGroup>
<CInputGroupText style={{ minWidth: '40px', justifyContent: 'center' }}>
{formData.iconName && getIconByName(formData.iconName) ? (
<CIcon icon={getIconByName(formData.iconName)!} />
) : (
<span className="text-muted">-</span>
)}
</CInputGroupText>
<CFormInput
type="text"
value={formData.iconName || ''}
readOnly
placeholder="아이콘을 선택하세요"
onClick={() => setIconPickerVisible(!iconPickerVisible)}
style={{ cursor: 'pointer' }}
/>
{formData.iconName && (
<CButton
color="secondary"
variant="outline"
onClick={handleIconClear}
>
<CIcon icon={cilX} />
</CButton>
)}
<CButton
color="primary"
variant="outline"
onClick={() => setIconPickerVisible(!iconPickerVisible)}
>
</CButton>
</CInputGroup>
{iconPickerVisible && (
<div
className="border rounded p-2 mt-2"
style={{
backgroundColor: 'var(--cui-body-bg)',
}}
>
<div className="d-flex flex-wrap gap-1">
{availableIcons.map((item) => (
<CButton
key={item.name}
color={formData.iconName === item.name ? 'primary' : 'light'}
size="sm"
onClick={() => handleIconSelect(item.name)}
title={`${item.label} (${item.name})`}
style={{ width: '36px', height: '36px', padding: '4px' }}
>
<CIcon icon={item.icon} />
</CButton>
))}
</div>
</div>
)}
<small className="text-muted"> ()</small>
</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>
<div className="mb-3">
<CFormLabel htmlFor="level"> </CFormLabel>
<CFormInput
type="number"
id="level"
name="level"
value={formData.level}
onChange={handleInputChange}
placeholder="메뉴 접근에 필요한 최소 회원 레벨을 입력하세요 (기본 1000)"
/>
</div>
</CForm>
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={() => { setModalVisible(false); setIconPickerVisible(false); }}>
</CButton>
<CButton color="primary" onClick={handleSave}>
{isEditMode ? '수정' : '추가'}
</CButton>
</CModalFooter>
</CModal>
{/* 삭제 확인 모달 */}
<CModal visible={deleteModalVisible} onClose={() => setDeleteModalVisible(false)} backdrop="static">
<CModalHeader>
<CModalTitle> </CModalTitle>
</CModalHeader>
<CModalBody>
{hasChildMenus ? (
<>
<div className="text-danger mb-2">
<CIcon icon={cilWarning} className="me-2" />
<strong> .</strong>
</div>
<p>
<strong>{selectedMenu?.menuName}</strong> .
</p>
<small className="text-muted">
.
</small>
</>
) : (
<>
<strong>{selectedMenu?.menuName}</strong> ?
</>
)}
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={() => setDeleteModalVisible(false)}>
{hasChildMenus ? '닫기' : '취소'}
</CButton>
{!hasChildMenus && (
<CButton color="danger" onClick={handleDelete}>
</CButton>
)}
</CModalFooter>
</CModal>
</>
);
};
export default AdminMenuManagement;