어드민 회원관리 기능 추가

This commit is contained in:
2026-01-21 18:49:39 +09:00
parent b0367c56be
commit 64d9fb8c6c
5 changed files with 592 additions and 33 deletions

View File

@@ -1,18 +1,4 @@
export const logo = [
'599 116',
`<g>
<g fill="none" fill-rule="nonzero">
<g style="fill:#80d0ff;">
<path d="m358.773 79.151-8.768-20.736a.25.25 0 0 0-.255-.191h-9.985a.226.226 0 0 0-.256.255v20.543a.566.566 0 0 1-.64.641h-1.216a.565.565 0 0 1-.64-.64v-43.52a.566.566 0 0 1 .64-.64h12.544a9.979 9.979 0 0 1 7.744 3.23 12.204 12.204 0 0 1 2.944 8.546 12.439 12.439 0 0 1-2.24 7.584 9.37 9.37 0 0 1-6.08 3.744c-.17.086-.214.191-.127.32l8.704 20.608.064.255c0 .342-.192.512-.576.512h-1.152a.703.703 0 0 1-.705-.51Zm-19.264-41.793v18.496a.226.226 0 0 0 .256.257h10.304a7.669 7.669 0 0 0 6.017-2.592 9.878 9.878 0 0 0 2.303-6.816 10.286 10.286 0 0 0-2.272-6.976 7.601 7.601 0 0 0-6.048-2.624h-10.304a.226.226 0 0 0-.256.255ZM398.082 37.102H378.05a.226.226 0 0 0-.256.256v18.496a.226.226 0 0 0 .256.257h13.824a.566.566 0 0 1 .64.64v.96a.566.566 0 0 1-.64.64H378.05a.226.226 0 0 0-.256.256v18.56a.226.226 0 0 0 .256.256h20.032a.567.567 0 0 1 .64.64v.96a.566.566 0 0 1-.64.64h-22.144a.566.566 0 0 1-.64-.64v-43.52a.566.566 0 0 1 .64-.64h22.144a.566.566 0 0 1 .64.64v.96a.566.566 0 0 1-.64.64ZM435.802 79.151l-2.431-8.832a.296.296 0 0 0-.32-.192h-16.768a.295.295 0 0 0-.32.192l-2.368 8.768a.658.658 0 0 1-.704.576h-1.216a.588.588 0 0 1-.48-.191.582.582 0 0 1-.096-.513l12.031-43.584a.644.644 0 0 1 .704-.512h1.6a.644.644 0 0 1 .704.512l12.16 43.584.065.192c0 .342-.214.512-.64.512h-1.217a.643.643 0 0 1-.704-.512ZM416.7 67.92a.303.303 0 0 0 .223.096h15.489a.304.304 0 0 0 .223-.096c.065-.065.075-.117.033-.16l-7.873-28.928c-.043-.085-.085-.128-.127-.128-.042 0-.086.043-.128.128l-7.872 28.928c-.043.043-.033.095.032.16ZM453.357 76.911a11.637 11.637 0 0 1-3.328-8.704V46.19a11.414 11.414 0 0 1 3.36-8.575 12.09 12.09 0 0 1 8.8-3.265 12.253 12.253 0 0 1 8.865 3.233 11.39 11.39 0 0 1 3.36 8.607v.64a.566.566 0 0 1-.641.641l-1.28.064c-.427 0-.64-.192-.64-.576v-.833a9.287 9.287 0 0 0-2.656-6.912 10.67 10.67 0 0 0-14.016 0 9.284 9.284 0 0 0-2.656 6.913v22.272a9.282 9.282 0 0 0 2.656 6.912 10.673 10.673 0 0 0 14.016 0 9.286 9.286 0 0 0 2.656-6.912v-.768c0-.384.213-.576.64-.575l1.28.063a.566.566 0 0 1 .64.64v.511a11.498 11.498 0 0 1-3.36 8.64 13.626 13.626 0 0 1-17.696 0v.001ZM511.193 35.503v.96a.565.565 0 0 1-.64.64H499.8a.226.226 0 0 0-.256.256v41.663a.566.566 0 0 1-.64.641h-1.216a.565.565 0 0 1-.64-.64V37.357a.227.227 0 0 0-.256-.255h-10.176a.565.565 0 0 1-.64-.64v-.96a.566.566 0 0 1 .64-.64h23.936a.566.566 0 0 1 .64.64ZM518.822 78.51a2.835 2.835 0 0 1-.8-2.047 2.923 2.923 0 0 1 .8-2.112c.544-.56 1.3-.862 2.08-.832a2.847 2.847 0 0 1 2.944 2.944c.03.78-.273 1.536-.832 2.08a2.921 2.921 0 0 1-2.112.8 2.754 2.754 0 0 1-2.08-.832ZM539.16 77.007a11.31 11.31 0 0 1-3.2-8.416v-5.44a.566.566 0 0 1 .64-.64h1.217a.567.567 0 0 1 .64.64v5.504a9.144 9.144 0 0 0 2.528 6.72 8.974 8.974 0 0 0 6.687 2.56 8.79 8.79 0 0 0 9.28-9.28V35.504a.565.565 0 0 1 .64-.64h1.217a.566.566 0 0 1 .64.64V68.59a11.252 11.252 0 0 1-3.233 8.416 13.062 13.062 0 0 1-17.055 0ZM577.106 77.102a10.482 10.482 0 0 1-3.36-8.127v-1.792a.565.565 0 0 1 .64-.64h1.088a.566.566 0 0 1 .64.64v1.6a8.544 8.544 0 0 0 2.752 6.655 10.536 10.536 0 0 0 7.36 2.496 9.876 9.876 0 0 0 6.976-2.367 8.215 8.215 0 0 0 2.56-6.336 8.397 8.397 0 0 0-1.12-4.416 11.383 11.383 0 0 0-3.328-3.392 71.626 71.626 0 0 0-6.176-3.712 71.302 71.302 0 0 1-6.24-3.84 12.174 12.174 0 0 1-3.424-3.68 10.257 10.257 0 0 1-1.28-5.345 9.86 9.86 0 0 1 3.072-7.744 12.012 12.012 0 0 1 8.32-2.752c3.796 0 6.783 1.035 8.96 3.105a10.823 10.823 0 0 1 3.264 8.224v1.6a.566.566 0 0 1-.64.64h-1.152a.565.565 0 0 1-.64-.64v-1.471a8.865 8.865 0 0 0-2.624-6.689 9.994 9.994 0 0 0-7.232-2.528 9.365 9.365 0 0 0-6.528 2.144 7.822 7.822 0 0 0-2.368 6.112 7.8 7.8 0 0 0 1.024 4.16 10.376 10.376 0 0 0 3.008 3.04 62.829 62.829 0 0 0 5.952 3.488 71.058 71.058 0 0 1 6.72 4.256 13.454 13.454 0 0 1 3.648 3.936 10.049 10.049 0 0 1 1.28 5.184 10.714 10.714 0 0 1-3.264 8.191c-2.175 2.05-5.12 3.073-8.832 3.073-3.798 0-6.817-1.024-9.057-3.073Z"/>
</g>
<g style="fill:currentColor;">
<path d="m96.59 25.058-39-22.517a12 12 0 0 0-12 0l-39 22.517a12.034 12.034 0 0 0-6 10.392v45.033a12.033 12.033 0 0 0 6 10.393l39 22.516a12 12 0 0 0 12 0l39-22.516a12.033 12.033 0 0 0 6-10.393V35.45a12.034 12.034 0 0 0-6-10.392Zm-2 55.425a4 4 0 0 1-2 3.464l-39 22.517a4 4 0 0 1-4 0l-39-22.517a4 4 0 0 1-2-3.464V35.45a4 4 0 0 1 2-3.464l39-22.517a4 4 0 0 1 4 0l39 22.517a4 4 0 0 1 2 3.464v45.033Z"/>
<path d="M74.612 71.005h-2.866c-.673 0-1.335.17-1.925.493l-17.28 9.485L32.59 69.465V46.487L52.54 34.97l17.29 9.455a4 4 0 0 0 1.919.49h2.863a2 2 0 0 0 2-2v-2.712a2 2 0 0 0-1.04-1.754L56.383 27.952a8.039 8.039 0 0 0-7.842.09L28.59 39.56a8.025 8.025 0 0 0-4 6.929v22.976a8 8 0 0 0 4 6.928l19.95 11.519a8.043 8.043 0 0 0 7.843.087l19.19-10.53a2 2 0 0 0 1.038-1.754v-2.71a2 2 0 0 0-1.999-2Z"/>
<g transform="translate(118 34)">
<path d="M51.335.362c-8.28.009-14.99 6.719-15 15v17.277c0 8.284 6.716 15 15 15 8.284 0 15-6.716 15-15V15.36c-.01-8.28-6.72-14.99-15-15Zm7 32.277a7 7 0 0 1-14 0V15.36a7 7 0 0 1 14 0V32.64ZM14.67 8.421a7.01 7.01 0 0 1 7.867 6.075.99.99 0 0 0 .985.865h6.03a1.01 1.01 0 0 0 .998-1.097C29.945 6.14 22.971-.02 14.834.381 6.751.932.504 7.696.59 15.796v16.407C.503 40.305 6.752 47.068 14.835 47.62c8.137.401 15.11-5.76 15.716-13.884a1.01 1.01 0 0 0-.999-1.097h-6.03a.99.99 0 0 0-.984.865 7.01 7.01 0 0 1-7.868 6.075 7.164 7.164 0 0 1-6.08-7.184v-16.79a7.164 7.164 0 0 1 6.08-7.184ZM97.513 27.928a12.158 12.158 0 0 0 7.184-11.077v-3.702C104.697 6.44 99.257 1 92.547 1H75.59a1 1 0 0 0-1 1v44a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V29h6.621l7.916 17.414a1 1 0 0 0 .91.586h6.591a1 1 0 0 0 .91-1.414l-8.025-17.658Zm-.816-11.077A4.154 4.154 0 0 1 92.547 21h-9.85V9h9.85a4.154 4.154 0 0 1 4.15 4.15v3.7ZM139.59 1h-26a1 1 0 0 0-1 1v44a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1h-19V27h13a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1h-13V9h19a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1ZM177.59 1h-6a1 1 0 0 0-1 1v22.648a7.007 7.007 0 1 1-14 0V2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v22.648a15.003 15.003 0 1 0 30 0V2a1 1 0 0 0-1-1Z"/>
<rect width="8" height="38" x="186.59" y="1" rx="1"/>
</g>
</g>
</g>
</g>`,
'180 32',
`<text x="50%" y="22" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="currentColor">Admin Sample</text>`,
]

View File

@@ -45,7 +45,7 @@ export const AppSidebarNav = ({ items }: AppSidebarNavProps) => {
}
const navItem = (item: NavItem, index: number, indent = false) => {
const { component, name, badge, icon, ...rest } = item
const { component, name, badge, icon, indent: itemIndent, ...rest } = item
const Component = component
return (
<Component as="div" key={index}>
@@ -54,7 +54,7 @@ export const AppSidebarNav = ({ items }: AppSidebarNavProps) => {
{...(rest.href && { target: '_blank', rel: 'noopener noreferrer' })}
{...rest}
>
{navLink(name, icon, badge, indent)}
{navLink(name, icon, badge, itemIndent || indent)}
</CNavLink>
</Component>
)

View File

@@ -2,11 +2,13 @@ import React from 'react'
const Dashboard = React.lazy(() => import('src/views/dashboard/Dashboard'))
const AdminMenuManagement = React.lazy(() => import('src/views/admin/AdminMenuManagement'))
const AdminMemberManagement = React.lazy(() => import('src/views/admin/AdminMemberManagement'))
const routes = [
{ path: '/', exact: true, name: 'Home' },
{ path: '/dashboard', name: 'Dashboard', element: Dashboard },
{ path: '/admin/menu', name: 'Admin Menu Management', element: AdminMenuManagement },
{ path: '/admin/member', name: 'Admin Member Management', element: AdminMemberManagement },
]
export default routes

View File

@@ -2,11 +2,25 @@ import axios from 'src/axios/axios';
// 회원 정보 인터페이스
export interface AdminMember {
memberSeq?: number;
memberId: string;
memberName?: string;
email?: string;
memberName: string;
password?: string;
// 필요한 경우 추가 필드 정의
level: number;
status: number;
lastLoginDatetime?: string;
insertDatetime?: string;
updateDatetime?: string;
}
// 페이징 정보 인터페이스
export interface PageInfo {
pageNum: number;
pageSize: number;
totalContent: number;
totalPage: number;
isFirstPage: boolean;
isLastPage: boolean;
}
// API 응답 인터페이스
@@ -14,17 +28,14 @@ export interface AdminMemberResponse {
resultCode: string;
resultMessage: string;
resultData?: any;
pageInfo?: PageInfo;
}
// 회원 추가
export const addAdminMember = async (member: AdminMember): Promise<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/add', member);
return response.data;
};
// 회원 수정
export const updateAdminMember = async (member: AdminMember): Promise<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/update', member);
// 회원 목록 조회
export const getAdminMemberList = async (pageNum: number = 1): Promise<AdminMemberResponse> => {
const response = await axios.get<AdminMemberResponse>('/admin/member/list', {
params: { pageNum }
});
return response.data;
};
@@ -34,9 +45,20 @@ export const getAdminMember = async (memberId: string): Promise<AdminMember> =>
return response.data.resultData;
};
// 회원 삭제
export const deleteAdminMember = async (memberId: string): Promise<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/delete', { memberId });
// 회원 추가
export const addAdminMember = async (member: Partial<AdminMember>): Promise<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/add', member);
return response.data;
};
// 회원 수정
export const updateAdminMember = async (member: Partial<AdminMember>): Promise<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/update', member);
return response.data;
};
// 회원 삭제
export const deleteAdminMember = async (memberSeq: number): Promise<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/delete', { memberSeq });
return response.data;
};

View File

@@ -0,0 +1,549 @@
import React, { useEffect, useState, useCallback } from 'react';
import {
CCard,
CCardBody,
CCardHeader,
CTable,
CTableHead,
CTableRow,
CTableHeaderCell,
CTableBody,
CTableDataCell,
CButton,
CModal,
CModalHeader,
CModalTitle,
CModalBody,
CModalFooter,
CForm,
CFormLabel,
CFormInput,
CFormSelect,
CPagination,
CPaginationItem,
CSpinner,
CBadge,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilPencil, cilTrash, cilPlus, cilWarning } from '@coreui/icons';
import {
AdminMember,
PageInfo,
getAdminMemberList,
addAdminMember,
updateAdminMember,
deleteAdminMember,
} from 'src/services/adminMemberService';
// 상태 옵션
const statusOptions = [
{ value: 0, label: '가입신청', color: 'warning' },
{ value: 1, label: '정상', color: 'success' },
{ value: 2, label: '휴면', color: 'secondary' },
{ value: 3, label: '정지', color: 'danger' },
{ value: 4, label: '탈퇴', color: 'dark' },
];
const AdminMemberManagement: React.FC = () => {
const [members, setMembers] = useState<AdminMember[]>([]);
const [pageInfo, setPageInfo] = useState<PageInfo | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(false);
// 반응형 상태 (화면 너비에 따라 열 표시/숨김)
const [isCompact, setIsCompact] = useState(false);
// 화면 너비 변경 감지
const handleResize = useCallback(() => {
setIsCompact(window.innerWidth < 992);
}, []);
useEffect(() => {
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [handleResize]);
// 모달 상태
const [modalVisible, setModalVisible] = useState(false);
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
const [selectedMember, setSelectedMember] = useState<AdminMember | null>(null);
// 폼 상태
const [formData, setFormData] = useState({
memberId: '',
memberName: '',
password: '',
passwordConfirm: '',
level: 1,
status: 0,
});
// 회원 목록 조회
const fetchMembers = useCallback(async (page: number) => {
setLoading(true);
try {
const response = await getAdminMemberList(page);
if (response.resultCode === '200' && response.resultData) {
const data = response.resultData;
// resultData.content에 회원 목록, 나머지는 페이지 정보
setMembers(Array.isArray(data.content) ? data.content : []);
setPageInfo({
pageNum: data.pageNum,
pageSize: data.pageSize,
totalContent: data.totalContent,
totalPage: data.totalPage,
isFirstPage: data.isFirstPage,
isLastPage: data.isLastPage,
});
}
} catch (error) {
console.error('회원 목록 조회 실패:', error);
setMembers([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchMembers(currentPage);
}, [currentPage, fetchMembers]);
// 상태 뱃지 가져오기
const getStatusBadge = (status: number) => {
const statusInfo = statusOptions.find(s => s.value === status);
return statusInfo ? (
<CBadge color={statusInfo.color}>{statusInfo.label}</CBadge>
) : (
<CBadge color="secondary"> </CBadge>
);
};
// 날짜 포맷 (날짜와 시간을 줄바꿈으로 구분)
const formatDate = (dateString?: string) => {
if (!dateString) return '-';
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return (
<>
{`${year}/${month}/${day}`}
<br />
{`${hours}:${minutes}`}
</>
);
};
// 모달 열기 (추가)
const openAddModal = () => {
setIsEditMode(false);
setFormData({
memberId: '',
memberName: '',
password: '',
passwordConfirm: '',
level: 1,
status: 0,
});
setModalVisible(true);
};
// 모달 열기 (수정)
const openEditModal = (member: AdminMember) => {
setIsEditMode(true);
setSelectedMember(member);
setFormData({
memberId: member.memberId,
memberName: member.memberName,
password: '',
passwordConfirm: '',
level: member.level,
status: member.status,
});
setModalVisible(true);
};
// 삭제 모달 열기
const openDeleteModal = (member: AdminMember) => {
setSelectedMember(member);
setDeleteModalVisible(true);
};
// 폼 입력 핸들러
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: name === 'level' || name === 'status' ? parseInt(value) : value,
}));
};
// 저장
const handleSave = async () => {
// 유효성 검사
if (!formData.memberId.trim()) {
alert('어드민 회원 ID를 입력해주세요.');
return;
}
if (!formData.memberName.trim()) {
alert('회원 이름을 입력해주세요.');
return;
}
if (!isEditMode && !formData.password) {
alert('비밀번호를 입력해주세요.');
return;
}
if (formData.password && formData.password !== formData.passwordConfirm) {
alert('비밀번호가 일치하지 않습니다.');
return;
}
try {
if (isEditMode && selectedMember) {
const updateData: Partial<AdminMember> = {
memberSeq: selectedMember.memberSeq,
memberName: formData.memberName,
level: formData.level,
status: formData.status,
};
if (formData.password) {
updateData.password = formData.password;
}
const response = await updateAdminMember(updateData);
if (response.resultCode === '200') {
alert('회원 정보가 수정되었습니다.');
setModalVisible(false);
fetchMembers(currentPage);
} else {
alert(response.resultMessage || '수정에 실패했습니다.');
}
} else {
const response = await addAdminMember({
memberId: formData.memberId,
memberName: formData.memberName,
password: formData.password,
level: formData.level,
status: formData.status,
});
if (response.resultCode === '200') {
alert('회원이 추가되었습니다.');
setModalVisible(false);
fetchMembers(1);
setCurrentPage(1);
} else {
alert(response.resultMessage || '추가에 실패했습니다.');
}
}
} catch (error) {
console.error('저장 실패:', error);
alert('저장에 실패했습니다.');
}
};
// 삭제
const handleDelete = async () => {
if (!selectedMember?.memberSeq) return;
try {
const response = await deleteAdminMember(selectedMember.memberSeq);
if (response.resultCode === '200') {
alert('회원이 삭제되었습니다.');
setDeleteModalVisible(false);
fetchMembers(currentPage);
} else {
alert(response.resultMessage || '삭제에 실패했습니다.');
}
} catch (error) {
console.error('삭제 실패:', error);
alert('삭제에 실패했습니다.');
}
};
// 페이지 변경
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
// 페이지네이션 렌더링
const renderPagination = () => {
if (!pageInfo || pageInfo.totalPage <= 1) return null;
const pages = [];
const maxVisiblePages = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(pageInfo.totalPage, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return (
<CPagination className="justify-content-center">
<CPaginationItem
disabled={pageInfo.isFirstPage}
onClick={() => handlePageChange(1)}
>
{'<<'}
</CPaginationItem>
<CPaginationItem
disabled={pageInfo.isFirstPage}
onClick={() => handlePageChange(currentPage - 1)}
>
{'<'}
</CPaginationItem>
{pages.map(page => (
<CPaginationItem
key={page}
active={page === currentPage}
onClick={() => handlePageChange(page)}
>
{page}
</CPaginationItem>
))}
<CPaginationItem
disabled={pageInfo.isLastPage}
onClick={() => handlePageChange(currentPage + 1)}
>
{'>'}
</CPaginationItem>
<CPaginationItem
disabled={pageInfo.isLastPage}
onClick={() => handlePageChange(pageInfo.totalPage)}
>
{'>>'}
</CPaginationItem>
</CPagination>
);
};
return (
<>
<CCard className="mb-4">
<CCardHeader className="d-flex justify-content-between align-items-center">
<strong> </strong>
<CButton color="primary" size="sm" onClick={openAddModal}>
<CIcon icon={cilPlus} className="me-1" />
</CButton>
</CCardHeader>
<CCardBody>
{loading ? (
<div className="text-center py-5">
<CSpinner color="primary" />
</div>
) : (
<>
<CTable hover responsive style={{ tableLayout: 'fixed', minWidth: isCompact ? 'auto' : '800px' }}>
<CTableHead>
<CTableRow>
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center" style={{ width: '80px' }}></CTableHeaderCell>
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center"> ID</CTableHeaderCell>
{!isCompact && (
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center"></CTableHeaderCell>
)}
{!isCompact && (
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center" style={{ width: '80px' }}></CTableHeaderCell>
)}
{!isCompact && (
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center" style={{ width: '80px' }}></CTableHeaderCell>
)}
{!isCompact && (
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center" style={{ width: '110px' }}> </CTableHeaderCell>
)}
{!isCompact && (
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center" style={{ width: '110px' }}></CTableHeaderCell>
)}
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center" style={{ width: '100px' }}></CTableHeaderCell>
</CTableRow>
</CTableHead>
<CTableBody>
{members.length === 0 ? (
<CTableRow>
<CTableDataCell colSpan={isCompact ? 3 : 8} className="text-center py-4">
.
</CTableDataCell>
</CTableRow>
) : (
members.map((member) => (
<CTableRow key={member.memberSeq}>
<CTableDataCell className="text-center">{member.memberSeq}</CTableDataCell>
<CTableDataCell className="text-center">{member.memberId}</CTableDataCell>
{!isCompact && (
<CTableDataCell className="text-center">{member.memberName}</CTableDataCell>
)}
{!isCompact && (
<CTableDataCell className="text-center">{member.level}</CTableDataCell>
)}
{!isCompact && (
<CTableDataCell className="text-center">{getStatusBadge(member.status)}</CTableDataCell>
)}
{!isCompact && (
<CTableDataCell className="text-center">{formatDate(member.lastLoginDatetime)}</CTableDataCell>
)}
{!isCompact && (
<CTableDataCell className="text-center">{formatDate(member.insertDatetime)}</CTableDataCell>
)}
<CTableDataCell className="text-center">
<CButton
color="info"
size="sm"
className="me-1"
onClick={() => openEditModal(member)}
title="수정"
>
<CIcon icon={cilPencil} />
</CButton>
<CButton
color="danger"
size="sm"
onClick={() => openDeleteModal(member)}
title="삭제"
>
<CIcon icon={cilTrash} />
</CButton>
</CTableDataCell>
</CTableRow>
))
)}
</CTableBody>
</CTable>
{pageInfo && pageInfo.totalPage > 1 && (
<div className="mt-3">
{renderPagination()}
</div>
)}
</>
)}
</CCardBody>
</CCard>
{/* 추가/수정 모달 */}
<CModal visible={modalVisible} onClose={() => setModalVisible(false)} backdrop="static">
<CModalHeader>
<CModalTitle>{isEditMode ? '어드민 회원 수정' : '어드민 회원 추가'}</CModalTitle>
</CModalHeader>
<CModalBody>
<CForm>
<div className="mb-3">
<CFormLabel htmlFor="memberId"> ID *</CFormLabel>
<CFormInput
type="text"
id="memberId"
name="memberId"
value={formData.memberId}
onChange={handleInputChange}
disabled={isEditMode}
placeholder="어드민 회원 ID를 입력하세요"
/>
</div>
<div className="mb-3">
<CFormLabel htmlFor="memberName"> *</CFormLabel>
<CFormInput
type="text"
id="memberName"
name="memberName"
value={formData.memberName}
onChange={handleInputChange}
placeholder="이름을 입력하세요"
/>
</div>
<div className="mb-3">
<CFormLabel htmlFor="password">
{isEditMode ? '(변경 시에만 입력)' : '*'}
</CFormLabel>
<CFormInput
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleInputChange}
placeholder="비밀번호를 입력하세요"
/>
</div>
<div className="mb-3">
<CFormLabel htmlFor="passwordConfirm"> </CFormLabel>
<CFormInput
type="password"
id="passwordConfirm"
name="passwordConfirm"
value={formData.passwordConfirm}
onChange={handleInputChange}
placeholder="비밀번호를 다시 입력하세요"
/>
</div>
<div className="mb-3">
<CFormLabel htmlFor="level"></CFormLabel>
<CFormInput
type="number"
id="level"
name="level"
value={formData.level}
onChange={handleInputChange}
min={0}
placeholder="레벨을 입력하세요"
/>
</div>
<div className="mb-3">
<CFormLabel htmlFor="status"></CFormLabel>
<CFormSelect
id="status"
name="status"
value={formData.status}
onChange={handleInputChange}
>
{statusOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</CFormSelect>
</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)} backdrop="static">
<CModalHeader>
<CModalTitle> </CModalTitle>
</CModalHeader>
<CModalBody>
<div className="text-center">
<CIcon icon={cilWarning} size="3xl" className="text-danger mb-3" />
<p>
<strong>{selectedMember?.memberName}</strong> ({selectedMember?.memberId}) ?
</p>
<p className="text-muted small"> .</p>
</div>
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={() => setDeleteModalVisible(false)}>
</CButton>
<CButton color="danger" onClick={handleDelete}>
</CButton>
</CModalFooter>
</CModal>
</>
);
};
export default AdminMemberManagement;