어드민 회원관리 기능 추가
This commit is contained in:
@@ -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>`,
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
549
src/views/admin/AdminMemberManagement.tsx
Normal file
549
src/views/admin/AdminMemberManagement.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user