({ pageNum: 1, pageSize: 20 });
+ const { data, isLoading } = useQuery({
+ queryKey: ['receive-errors', param],
+ queryFn: () => receiveApi.listErrors(param),
+ });
+
+ return (
+
+ setParam({ ...param, ...v, pageNum: 1 })}
+ />
+ setParam({ ...param, ...p })}
+ loading={isLoading}
+ columns={[
+ { title: '보험사', dataIndex: 'companyName' },
+ { title: '에러 유형', dataIndex: 'errorTypeName',
+ render: (v: string) => {v} },
+ { title: '필드', dataIndex: 'errorField' },
+ { title: '값', dataIndex: 'errorValue', ellipsis: true },
+ { title: '메시지', dataIndex: 'errorMessage', ellipsis: true },
+ { title: '처리상태', dataIndex: 'resolveStatusName',
+ render: (v: string, r: any) => v === '처리완료'
+ ? {v}
+ : {v} },
+ { title: '발생시각', dataIndex: 'createdAt',
+ render: (v: string) => v && dayjs(v).format('YYYY-MM-DD HH:mm') },
+ ]}
+ />
+
+ );
+}
diff --git a/ga-frontend/src/pages/receive/FieldMappingEditor.tsx b/ga-frontend/src/pages/receive/FieldMappingEditor.tsx
new file mode 100644
index 0000000..9acca2d
--- /dev/null
+++ b/ga-frontend/src/pages/receive/FieldMappingEditor.tsx
@@ -0,0 +1,183 @@
+import { useState } from 'react';
+import { Button, Card, Input, Select, Space, Table, message } from 'antd';
+import { ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useQuery } from '@tanstack/react-query';
+import PageContainer from '@/components/common/PageContainer';
+import PermissionButton from '@/components/common/PermissionButton';
+import { receiveApi } from '@/api/receive';
+
+interface MappingRow {
+ mappingId?: number;
+ sourceField: string;
+ sourceColIndex?: number;
+ targetTable: string;
+ targetField: string;
+ dataType?: string; // STRING / NUMBER / DECIMAL / DATE / INTEGER
+ transformRule?: string; // TRIM / UPPER / LOWER / DATE / DIVIDE / MULTIPLY / CODE_MAP / DEFAULT
+ defaultValue?: string;
+ isRequired?: string;
+ sortOrder: number;
+ __dirty?: boolean;
+}
+
+const TARGET_TABLES = [
+ { value: 'recruit_ledger', label: '모집수수료원장' },
+ { value: 'maintain_ledger', label: '유지수수료원장' },
+];
+
+const DATA_TYPES = ['STRING', 'NUMBER', 'DECIMAL', 'DATE', 'INTEGER'].map((v) => ({ value: v, label: v }));
+
+const TRANSFORM_RULES = [
+ { value: '', label: '(없음)' },
+ { value: 'TRIM', label: '공백제거' },
+ { value: 'UPPER', label: '대문자' },
+ { value: 'LOWER', label: '소문자' },
+ { value: 'DATE', label: '날짜변환' },
+ { value: 'DIVIDE', label: '나누기' },
+ { value: 'MULTIPLY', label: '곱하기' },
+ { value: 'CODE_MAP', label: '코드매핑' },
+ { value: 'DEFAULT', label: '기본값' },
+];
+
+/**
+ * 보험사별 필드 매핑 편집기.
+ * URL: /receive/mapping/:companyCode/:targetTable
+ */
+export default function FieldMappingEditor() {
+ const { companyCode = '', targetTable: tt } = useParams<{ companyCode: string; targetTable?: string }>();
+ const [targetTable, setTargetTable] = useState(tt ?? 'recruit_ledger');
+ const [rows, setRows] = useState([]);
+ const navigate = useNavigate();
+
+ const { refetch, isLoading } = useQuery({
+ queryKey: ['fieldMappings', companyCode, targetTable],
+ queryFn: async () => {
+ const r: any = await receiveApi.listFieldMappings(companyCode, targetTable);
+ const list: MappingRow[] = (Array.isArray(r) ? r : (r?.list ?? [])).map((x: any, i: number) => ({
+ ...x,
+ sortOrder: x.sortOrder ?? i + 1,
+ }));
+ setRows(list);
+ return list;
+ },
+ enabled: !!companyCode && !!targetTable,
+ });
+
+ const addRow = () => {
+ setRows([
+ ...rows,
+ {
+ sourceField: '',
+ targetTable,
+ targetField: '',
+ dataType: 'STRING',
+ sortOrder: rows.length + 1,
+ isRequired: 'N',
+ __dirty: true,
+ },
+ ]);
+ };
+
+ const removeRow = (idx: number) => {
+ const next = rows.filter((_, i) => i !== idx).map((r, i) => ({ ...r, sortOrder: i + 1, __dirty: true }));
+ setRows(next);
+ };
+
+ const move = (idx: number, dir: -1 | 1) => {
+ const target = idx + dir;
+ if (target < 0 || target >= rows.length) return;
+ const next = [...rows];
+ [next[idx], next[target]] = [next[target], next[idx]];
+ setRows(next.map((r, i) => ({ ...r, sortOrder: i + 1, __dirty: true })));
+ };
+
+ const setCell = (idx: number, field: keyof MappingRow, value: any) => {
+ const next = [...rows];
+ next[idx] = { ...next[idx], [field]: value, __dirty: true };
+ setRows(next);
+ };
+
+ const save = async () => {
+ // 검증
+ for (const r of rows) {
+ if (!r.sourceField || !r.targetField) {
+ message.error('원본/타겟 필드는 모두 채워야 합니다');
+ return;
+ }
+ }
+ try {
+ await receiveApi.bulkSaveFieldMappings(companyCode, rows);
+ message.success('저장 완료');
+ refetch();
+ } catch {}
+ };
+
+ const columns = [
+ { title: '순서', width: 80, render: (_: any, _r: MappingRow, idx: number) => (
+
+ } disabled={idx === 0} onClick={() => move(idx, -1)} />
+ } disabled={idx === rows.length - 1} onClick={() => move(idx, 1)} />
+ {idx + 1}
+
+ )},
+ { title: '원본 필드명', width: 180, render: (_: any, r: MappingRow, idx: number) => (
+ setCell(idx, 'sourceField', e.target.value)} />
+ )},
+ { title: '컬럼 인덱스', width: 110, render: (_: any, r: MappingRow, idx: number) => (
+
+ setCell(idx, 'sourceColIndex', e.target.value === '' ? undefined : Number(e.target.value))} />
+ )},
+ { title: '타겟 필드명', width: 180, render: (_: any, r: MappingRow, idx: number) => (
+ setCell(idx, 'targetField', e.target.value)} />
+ )},
+ { title: '데이터 타입', width: 120, render: (_: any, r: MappingRow, idx: number) => (
+