import { useState } from 'react'; import { Card, Col, Empty, Form, Input, InputNumber, Modal, Row, Select, Space, Table, Tag, Typography, message, } from 'antd'; import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, } from '@ant-design/icons'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import PageContainer from '@/components/common/PageContainer'; import PermissionButton from '@/components/common/PermissionButton'; import { commonCodeApi, CommonCodeGroup, CommonCode } from '@/api/user'; const { Text } = Typography; const MENU = 'SYSTEM_CODE'; /* ------------------------------------------------------------------ */ /* 내부 상태 타입 */ /* ------------------------------------------------------------------ */ type GroupModalState = | { open: false } | { open: true; mode: 'create' } | { open: true; mode: 'edit'; group: CommonCodeGroup }; type CodeModalState = | { open: false } | { open: true; mode: 'create' } | { open: true; mode: 'edit'; code: CommonCode }; /* ------------------------------------------------------------------ */ /* 메인 컴포넌트 */ /* ------------------------------------------------------------------ */ export default function CodeList() { const qc = useQueryClient(); /* 선택 그룹 & 검색어 */ const [selected, setSelected] = useState(null); const [keyword, setKeyword] = useState(''); /* 모달 상태 */ const [groupModal, setGroupModal] = useState({ open: false }); const [codeModal, setCodeModal] = useState({ open: false }); /* 폼 인스턴스 */ const [groupForm] = Form.useForm(); const [codeForm] = Form.useForm(); /* 로딩 상태 */ const [groupSaving, setGroupSaving] = useState(false); const [codeSaving, setCodeSaving] = useState(false); /* ---------------------------------------------------------------- */ /* 쿼리 */ /* ---------------------------------------------------------------- */ const { data: groups = [], isFetching: groupLoading } = useQuery({ queryKey: ['code', 'groups', keyword], queryFn: () => commonCodeApi.groups(keyword || undefined), }); const { data: codes = [], isFetching: codeLoading } = useQuery({ queryKey: ['code', 'codes', selected?.groupCode], queryFn: () => commonCodeApi.codes(selected!.groupCode), enabled: !!selected, }); /* ---------------------------------------------------------------- */ /* 공통 리로드 */ /* ---------------------------------------------------------------- */ const invalidateGroups = () => qc.invalidateQueries({ queryKey: ['code', 'groups'] }); const invalidateCodes = (gc: string) => qc.invalidateQueries({ queryKey: ['code', 'codes', gc] }); /* ---------------------------------------------------------------- */ /* 그룹 모달 열기 */ /* ---------------------------------------------------------------- */ const openGroupCreate = () => { groupForm.resetFields(); groupForm.setFieldsValue({ isSystem: 'N', sortOrder: 0 }); setGroupModal({ open: true, mode: 'create' }); }; const openGroupEdit = (group: CommonCodeGroup) => { groupForm.resetFields(); groupForm.setFieldsValue({ groupCode: group.groupCode, groupName: group.groupName, description: group.description, isSystem: group.isSystem, sortOrder: group.sortOrder, }); setGroupModal({ open: true, mode: 'edit', group }); }; /* ---------------------------------------------------------------- */ /* 그룹 저장 */ /* ---------------------------------------------------------------- */ const handleGroupSave = async () => { if (!groupModal.open) return; try { setGroupSaving(true); const values = await groupForm.validateFields(); if (groupModal.mode === 'create') { await commonCodeApi.createGroup(values); message.success('그룹이 추가되었습니다.'); } else { const { group } = groupModal; await commonCodeApi.updateGroup(group.groupCode, { groupName: values.groupName, description: values.description, }); message.success('그룹이 수정되었습니다.'); // 선택된 그룹 이름 동기화 if (selected?.groupCode === group.groupCode) { setSelected((prev) => prev ? { ...prev, groupName: values.groupName, description: values.description } : null, ); } await commonCodeApi.refreshCache(group.groupCode); } await invalidateGroups(); setGroupModal({ open: false }); } catch { // validateFields 실패 시 폼 에러 표시 — 별도 처리 불필요 } finally { setGroupSaving(false); } }; /* ---------------------------------------------------------------- */ /* 그룹 삭제 */ /* ---------------------------------------------------------------- */ const confirmDeleteGroup = (group: CommonCodeGroup) => { Modal.confirm({ title: '그룹 삭제', content: ( {group.groupName} 그룹과 하위 코드를 모두 삭제합니다.
계속하시겠습니까?
), okText: '삭제', okType: 'danger', cancelText: '취소', onOk: async () => { await commonCodeApi.deleteGroup(group.groupCode); message.success('그룹이 삭제되었습니다.'); if (selected?.groupCode === group.groupCode) setSelected(null); await commonCodeApi.refreshCache(group.groupCode); invalidateGroups(); }, }); }; /* ---------------------------------------------------------------- */ /* 코드 모달 열기 */ /* ---------------------------------------------------------------- */ const openCodeCreate = () => { codeForm.resetFields(); codeForm.setFieldsValue({ isActive: 'Y', sortOrder: 0 }); setCodeModal({ open: true, mode: 'create' }); }; const openCodeEdit = (code: CommonCode) => { codeForm.resetFields(); codeForm.setFieldsValue({ code: code.code, codeName: code.codeName, codeDesc: code.codeDesc, sortOrder: code.sortOrder, isActive: code.isActive, }); setCodeModal({ open: true, mode: 'edit', code }); }; /* ---------------------------------------------------------------- */ /* 코드 저장 */ /* ---------------------------------------------------------------- */ const handleCodeSave = async () => { if (!codeModal.open || !selected) return; try { setCodeSaving(true); const values = await codeForm.validateFields(); if (codeModal.mode === 'create') { await commonCodeApi.createCode(selected.groupCode, values); message.success('코드가 추가되었습니다.'); } else { await commonCodeApi.updateCode(codeModal.code.codeId, values); message.success('코드가 수정되었습니다.'); } await commonCodeApi.refreshCache(selected.groupCode); invalidateCodes(selected.groupCode); setCodeModal({ open: false }); } catch { // 폼 검증 실패 — 별도 처리 불필요 } finally { setCodeSaving(false); } }; /* ---------------------------------------------------------------- */ /* 코드 삭제 */ /* ---------------------------------------------------------------- */ const confirmDeleteCode = (code: CommonCode) => { Modal.confirm({ title: '코드 삭제', content: ( 코드 {code.code} ({code.codeName})을(를) 삭제합니다.
계속하시겠습니까?
), okText: '삭제', okType: 'danger', cancelText: '취소', onOk: async () => { await commonCodeApi.deleteCode(code.codeId); message.success('코드가 삭제되었습니다.'); if (selected) { await commonCodeApi.refreshCache(selected.groupCode); invalidateCodes(selected.groupCode); } }, }); }; /* ---------------------------------------------------------------- */ /* 렌더 */ /* ---------------------------------------------------------------- */ return ( {/* ======================================================= */} {/* 왼쪽: 그룹 목록 */} {/* ======================================================= */} } onClick={openGroupCreate} > 그룹 추가 } onClick={invalidateGroups} /> } > setKeyword(v)} onChange={(e) => { if (!e.target.value) setKeyword(''); }} /> rowKey="groupCode" dataSource={groups} loading={groupLoading} size="small" pagination={false} scroll={{ y: 520 }} onRow={(r) => ({ onClick: () => setSelected(r), style: { cursor: 'pointer' }, })} rowClassName={(r) => r.groupCode === selected?.groupCode ? 'ant-table-row-selected' : '' } columns={[ { title: '그룹코드', dataIndex: 'groupCode', width: 140, ellipsis: true }, { title: '그룹명', dataIndex: 'groupName', ellipsis: true }, { title: '유형', dataIndex: 'isSystem', width: 68, align: 'center', render: (v: string) => v === 'Y' ? ( 시스템 ) : ( 일반 ), }, { title: '작업', key: 'actions', width: 72, align: 'center', render: (_, r) => ( e.stopPropagation()}> } onClick={() => openGroupEdit(r)} /> } onClick={() => confirmDeleteGroup(r)} /> ), }, ]} /> {/* ======================================================= */} {/* 오른쪽: 코드 목록 */} {/* ======================================================= */} } onClick={openCodeCreate} > 코드 추가 ) } > {!selected ? ( ) : ( <> rowKey="codeId" dataSource={codes} loading={codeLoading} size="small" pagination={false} scroll={{ y: 520 }} columns={[ { title: '코드', dataIndex: 'code', width: 120, ellipsis: true }, { title: '코드명', dataIndex: 'codeName', width: 130, ellipsis: true }, { title: '설명', dataIndex: 'codeDesc', ellipsis: true }, { title: '정렬', dataIndex: 'sortOrder', width: 52, align: 'right' }, { title: '활성', dataIndex: 'isActive', width: 60, align: 'center', render: (v: string) => v === 'Y' ? ( Y ) : ( N ), }, { title: '작업', key: 'actions', width: 72, align: 'center', render: (_, r) => ( } onClick={() => openCodeEdit(r)} /> } onClick={() => confirmDeleteCode(r)} /> ), }, ]} /> {selected.isSystem === 'Y' && ( 시스템 그룹의 코드는 식별자 변경·삭제가 제한됩니다. )} )} {/* ========================================================= */} {/* 그룹 추가 / 수정 모달 */} {/* ========================================================= */} setGroupModal({ open: false })} okText={groupModal.open && groupModal.mode === 'create' ? '추가' : '수정'} cancelText="취소" confirmLoading={groupSaving} destroyOnClose width={480} >
{groupModal.open && groupModal.mode === 'create' && (