540 lines
20 KiB
TypeScript
540 lines
20 KiB
TypeScript
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<CommonCodeGroup | null>(null);
|
|
const [keyword, setKeyword] = useState('');
|
|
|
|
/* 모달 상태 */
|
|
const [groupModal, setGroupModal] = useState<GroupModalState>({ open: false });
|
|
const [codeModal, setCodeModal] = useState<CodeModalState>({ 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: (
|
|
<span>
|
|
<b>{group.groupName}</b> 그룹과 하위 코드를 모두 삭제합니다.
|
|
<br />계속하시겠습니까?
|
|
</span>
|
|
),
|
|
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: (
|
|
<span>
|
|
코드 <b>{code.code} ({code.codeName})</b>을(를) 삭제합니다.
|
|
<br />계속하시겠습니까?
|
|
</span>
|
|
),
|
|
okText: '삭제',
|
|
okType: 'danger',
|
|
cancelText: '취소',
|
|
onOk: async () => {
|
|
await commonCodeApi.deleteCode(code.codeId);
|
|
message.success('코드가 삭제되었습니다.');
|
|
if (selected) {
|
|
await commonCodeApi.refreshCache(selected.groupCode);
|
|
invalidateCodes(selected.groupCode);
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
/* ---------------------------------------------------------------- */
|
|
/* 렌더 */
|
|
/* ---------------------------------------------------------------- */
|
|
return (
|
|
<PageContainer title="공통코드 관리">
|
|
<Row gutter={16}>
|
|
{/* ======================================================= */}
|
|
{/* 왼쪽: 그룹 목록 */}
|
|
{/* ======================================================= */}
|
|
<Col span={10}>
|
|
<Card
|
|
size="small"
|
|
title="코드 그룹"
|
|
extra={
|
|
<Space size={4}>
|
|
<PermissionButton
|
|
menuCode={MENU}
|
|
permCode="CREATE"
|
|
type="primary"
|
|
size="small"
|
|
icon={<PlusOutlined />}
|
|
onClick={openGroupCreate}
|
|
>
|
|
그룹 추가
|
|
</PermissionButton>
|
|
<PermissionButton
|
|
menuCode={MENU}
|
|
permCode="READ"
|
|
size="small"
|
|
icon={<ReloadOutlined />}
|
|
onClick={invalidateGroups}
|
|
/>
|
|
</Space>
|
|
}
|
|
>
|
|
<Input.Search
|
|
placeholder="그룹코드 / 그룹명 검색"
|
|
allowClear
|
|
size="small"
|
|
style={{ marginBottom: 8 }}
|
|
onSearch={(v) => setKeyword(v)}
|
|
onChange={(e) => { if (!e.target.value) setKeyword(''); }}
|
|
/>
|
|
<Table<CommonCodeGroup>
|
|
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' ? (
|
|
<Tag color="orange" style={{ margin: 0, fontSize: 11 }}>시스템</Tag>
|
|
) : (
|
|
<Tag style={{ margin: 0, fontSize: 11 }}>일반</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '작업',
|
|
key: 'actions',
|
|
width: 72,
|
|
align: 'center',
|
|
render: (_, r) => (
|
|
<Space size={2} onClick={(e) => e.stopPropagation()}>
|
|
<PermissionButton
|
|
menuCode={MENU}
|
|
permCode="UPDATE"
|
|
type="text"
|
|
size="small"
|
|
icon={<EditOutlined />}
|
|
onClick={() => openGroupEdit(r)}
|
|
/>
|
|
<PermissionButton
|
|
menuCode={MENU}
|
|
permCode="DELETE"
|
|
type="text"
|
|
size="small"
|
|
danger
|
|
disabled={r.isSystem === 'Y'}
|
|
icon={<DeleteOutlined />}
|
|
onClick={() => confirmDeleteGroup(r)}
|
|
/>
|
|
</Space>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
|
|
{/* ======================================================= */}
|
|
{/* 오른쪽: 코드 목록 */}
|
|
{/* ======================================================= */}
|
|
<Col span={14}>
|
|
<Card
|
|
size="small"
|
|
title={selected ? `코드 목록 — ${selected.groupName}` : '코드 목록'}
|
|
extra={
|
|
selected && (
|
|
<PermissionButton
|
|
menuCode={MENU}
|
|
permCode="CREATE"
|
|
type="primary"
|
|
size="small"
|
|
icon={<PlusOutlined />}
|
|
onClick={openCodeCreate}
|
|
>
|
|
코드 추가
|
|
</PermissionButton>
|
|
)
|
|
}
|
|
>
|
|
{!selected ? (
|
|
<Empty description="왼쪽에서 그룹을 선택하세요" style={{ padding: '40px 0' }} />
|
|
) : (
|
|
<>
|
|
<Table<CommonCode>
|
|
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' ? (
|
|
<Tag color="green" style={{ margin: 0 }}>Y</Tag>
|
|
) : (
|
|
<Tag style={{ margin: 0 }}>N</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '작업',
|
|
key: 'actions',
|
|
width: 72,
|
|
align: 'center',
|
|
render: (_, r) => (
|
|
<Space size={2}>
|
|
<PermissionButton
|
|
menuCode={MENU}
|
|
permCode="UPDATE"
|
|
type="text"
|
|
size="small"
|
|
icon={<EditOutlined />}
|
|
onClick={() => openCodeEdit(r)}
|
|
/>
|
|
<PermissionButton
|
|
menuCode={MENU}
|
|
permCode="DELETE"
|
|
type="text"
|
|
size="small"
|
|
danger
|
|
icon={<DeleteOutlined />}
|
|
onClick={() => confirmDeleteCode(r)}
|
|
/>
|
|
</Space>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
{selected.isSystem === 'Y' && (
|
|
<Text type="warning" style={{ display: 'block', marginTop: 8, fontSize: 12 }}>
|
|
시스템 그룹의 코드는 식별자 변경·삭제가 제한됩니다.
|
|
</Text>
|
|
)}
|
|
</>
|
|
)}
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
{/* ========================================================= */}
|
|
{/* 그룹 추가 / 수정 모달 */}
|
|
{/* ========================================================= */}
|
|
<Modal
|
|
title={groupModal.open && groupModal.mode === 'create' ? '그룹 추가' : '그룹 수정'}
|
|
open={groupModal.open}
|
|
onOk={handleGroupSave}
|
|
onCancel={() => setGroupModal({ open: false })}
|
|
okText={groupModal.open && groupModal.mode === 'create' ? '추가' : '수정'}
|
|
cancelText="취소"
|
|
confirmLoading={groupSaving}
|
|
destroyOnClose
|
|
width={480}
|
|
>
|
|
<Form form={groupForm} layout="vertical" style={{ marginTop: 16 }}>
|
|
<Form.Item
|
|
name="groupCode"
|
|
label="그룹코드"
|
|
rules={[
|
|
{ required: true, message: '그룹코드를 입력하세요' },
|
|
{ pattern: /^[A-Z0-9_]+$/, message: '영문 대문자, 숫자, 밑줄만 사용 가능합니다' },
|
|
]}
|
|
>
|
|
<Input
|
|
placeholder="예) AGENT_STATUS"
|
|
disabled={groupModal.open && groupModal.mode === 'edit'}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="groupName"
|
|
label="그룹명"
|
|
rules={[{ required: true, message: '그룹명을 입력하세요' }]}
|
|
>
|
|
<Input placeholder="예) 설계사 상태" />
|
|
</Form.Item>
|
|
<Form.Item name="description" label="설명">
|
|
<Input.TextArea rows={2} placeholder="그룹에 대한 설명을 입력하세요" />
|
|
</Form.Item>
|
|
{groupModal.open && groupModal.mode === 'create' && (
|
|
<Row gutter={12}>
|
|
<Col span={12}>
|
|
<Form.Item name="isSystem" label="시스템 여부">
|
|
<Select options={[{ value: 'Y', label: '시스템' }, { value: 'N', label: '일반' }]} />
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item name="sortOrder" label="정렬 순서">
|
|
<InputNumber min={0} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
)}
|
|
</Form>
|
|
</Modal>
|
|
|
|
{/* ========================================================= */}
|
|
{/* 코드 추가 / 수정 모달 */}
|
|
{/* ========================================================= */}
|
|
<Modal
|
|
title={codeModal.open && codeModal.mode === 'create' ? '코드 추가' : '코드 수정'}
|
|
open={codeModal.open}
|
|
onOk={handleCodeSave}
|
|
onCancel={() => setCodeModal({ open: false })}
|
|
okText={codeModal.open && codeModal.mode === 'create' ? '추가' : '수정'}
|
|
cancelText="취소"
|
|
confirmLoading={codeSaving}
|
|
destroyOnClose
|
|
width={460}
|
|
>
|
|
<Form form={codeForm} layout="vertical" style={{ marginTop: 16 }}>
|
|
<Form.Item
|
|
name="code"
|
|
label="코드값"
|
|
rules={[
|
|
{ required: true, message: '코드값을 입력하세요' },
|
|
{ pattern: /^[A-Z0-9_]+$/, message: '영문 대문자, 숫자, 밑줄만 사용 가능합니다' },
|
|
]}
|
|
>
|
|
<Input
|
|
placeholder="예) ACTIVE"
|
|
disabled={codeModal.open && codeModal.mode === 'edit'}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="codeName"
|
|
label="코드명"
|
|
rules={[{ required: true, message: '코드명을 입력하세요' }]}
|
|
>
|
|
<Input placeholder="예) 활성" />
|
|
</Form.Item>
|
|
<Form.Item name="codeDesc" label="설명">
|
|
<Input.TextArea rows={2} placeholder="코드에 대한 설명을 입력하세요" />
|
|
</Form.Item>
|
|
<Row gutter={12}>
|
|
<Col span={12}>
|
|
<Form.Item name="sortOrder" label="정렬 순서">
|
|
<InputNumber min={0} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item name="isActive" label="활성 여부">
|
|
<Select
|
|
options={[
|
|
{ value: 'Y', label: '활성' },
|
|
{ value: 'N', label: '비활성' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
</Form>
|
|
</Modal>
|
|
</PageContainer>
|
|
);
|
|
}
|