diff --git a/src/assets/brand/logo.ts b/src/assets/brand/logo.ts index 63961c6..55d6b65 100644 --- a/src/assets/brand/logo.ts +++ b/src/assets/brand/logo.ts @@ -1,18 +1,4 @@ export const logo = [ - '599 116', - ` - - - - - - - - - - - - - - `, + '180 32', + `Admin Sample`, ] diff --git a/src/components/AppSidebarNav.tsx b/src/components/AppSidebarNav.tsx index 96852ff..24ac96c 100644 --- a/src/components/AppSidebarNav.tsx +++ b/src/components/AppSidebarNav.tsx @@ -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 ( @@ -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)} ) diff --git a/src/routes/routes.ts b/src/routes/routes.ts index bea520f..82f91b3 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -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 diff --git a/src/services/adminMemberService.ts b/src/services/adminMemberService.ts index 8c8777a..1dd97a8 100644 --- a/src/services/adminMemberService.ts +++ b/src/services/adminMemberService.ts @@ -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 => { - const response = await axios.post('/admin/member/add', member); - return response.data; -}; - -// 회원 수정 -export const updateAdminMember = async (member: AdminMember): Promise => { - const response = await axios.post('/admin/member/update', member); +// 회원 목록 조회 +export const getAdminMemberList = async (pageNum: number = 1): Promise => { + const response = await axios.get('/admin/member/list', { + params: { pageNum } + }); return response.data; }; @@ -34,9 +45,20 @@ export const getAdminMember = async (memberId: string): Promise => return response.data.resultData; }; -// 회원 삭제 -export const deleteAdminMember = async (memberId: string): Promise => { - const response = await axios.post('/admin/member/delete', { memberId }); +// 회원 추가 +export const addAdminMember = async (member: Partial): Promise => { + const response = await axios.post('/admin/member/add', member); return response.data; }; +// 회원 수정 +export const updateAdminMember = async (member: Partial): Promise => { + const response = await axios.post('/admin/member/update', member); + return response.data; +}; + +// 회원 삭제 +export const deleteAdminMember = async (memberSeq: number): Promise => { + const response = await axios.post('/admin/member/delete', { memberSeq }); + return response.data; +}; diff --git a/src/views/admin/AdminMemberManagement.tsx b/src/views/admin/AdminMemberManagement.tsx new file mode 100644 index 0000000..2f9c26b --- /dev/null +++ b/src/views/admin/AdminMemberManagement.tsx @@ -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([]); + const [pageInfo, setPageInfo] = useState(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(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 ? ( + {statusInfo.label} + ) : ( + 알 수 없음 + ); + }; + + // 날짜 포맷 (날짜와 시간을 줄바꿈으로 구분) + 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}`} + + {`${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) => { + 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 = { + 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 ( + + handlePageChange(1)} + > + {'<<'} + + handlePageChange(currentPage - 1)} + > + {'<'} + + {pages.map(page => ( + handlePageChange(page)} + > + {page} + + ))} + handlePageChange(currentPage + 1)} + > + {'>'} + + handlePageChange(pageInfo.totalPage)} + > + {'>>'} + + + ); + }; + + return ( + <> + + + 어드민 회원 관리 + + + 어드민 회원 추가 + + + + {loading ? ( + + + + ) : ( + <> + + + + 번호 + 어드민 회원 ID + {!isCompact && ( + 이름 + )} + {!isCompact && ( + 레벨 + )} + {!isCompact && ( + 상태 + )} + {!isCompact && ( + 최종 로그인 + )} + {!isCompact && ( + 가입일시 + )} + 관리 + + + + {members.length === 0 ? ( + + + 등록된 어드민 회원이 없습니다. + + + ) : ( + members.map((member) => ( + + {member.memberSeq} + {member.memberId} + {!isCompact && ( + {member.memberName} + )} + {!isCompact && ( + {member.level} + )} + {!isCompact && ( + {getStatusBadge(member.status)} + )} + {!isCompact && ( + {formatDate(member.lastLoginDatetime)} + )} + {!isCompact && ( + {formatDate(member.insertDatetime)} + )} + + openEditModal(member)} + title="수정" + > + + + openDeleteModal(member)} + title="삭제" + > + + + + + )) + )} + + + + {pageInfo && pageInfo.totalPage > 1 && ( + + {renderPagination()} + + )} + > + )} + + + + {/* 추가/수정 모달 */} + setModalVisible(false)} backdrop="static"> + + {isEditMode ? '어드민 회원 수정' : '어드민 회원 추가'} + + + + + 어드민 회원 ID * + + + + 이름 * + + + + + 비밀번호 {isEditMode ? '(변경 시에만 입력)' : '*'} + + + + + 비밀번호 확인 + + + + 레벨 + + + + 상태 + + {statusOptions.map(option => ( + + {option.label} + + ))} + + + + + + setModalVisible(false)}> + 취소 + + + {isEditMode ? '수정' : '추가'} + + + + + {/* 삭제 확인 모달 */} + setDeleteModalVisible(false)} backdrop="static"> + + 어드민 회원 삭제 + + + + + + {selectedMember?.memberName} ({selectedMember?.memberId}) 어드민 회원을 삭제하시겠습니까? + + 이 작업은 되돌릴 수 없습니다. + + + + setDeleteModalVisible(false)}> + 취소 + + + 삭제 + + + + > + ); +}; + +export default AdminMemberManagement;
+ {selectedMember?.memberName} ({selectedMember?.memberId}) 어드민 회원을 삭제하시겠습니까? +
이 작업은 되돌릴 수 없습니다.