feat(frontend): 누락된 메뉴 14개 페이지 일괄 구현
신규 페이지 (모두 ProTable / ProCard 패턴): - org/OrgTree: 조직 트리 + 선택 시 우측에 소속 설계사 ProTable - product/CompanyList: 보험사 목록 - product/ProductList: 상품 목록 (보험종 필터) - rule/CommissionRateList: 수수료율 (보험사 통보) - rule/PayoutRuleList: 지급율 (직급 × 보험종 × 회차) - rule/OverrideRuleList: 오버라이드 규정 (직급 페어) - rule/ChargebackRuleList: 환수 규정 (실효 월수 구간) - rule/ExceptionCodeList: 예외 코드 마스터 - ledger/MaintainLedger: 유지수수료 원장 - receive/ReceiveData: 보험사 raw 수신 데이터 - receive/ReceiveMapping: 보험사 프로파일 + 필드 매핑 규칙 - system/MenuManage: 메뉴 트리 표시 (Tree) - system/SystemConfig: 키-값 설정 (그룹별 필터) - system/SystemLog: 로그인/API/변경 이력 3 탭 신규 API 모듈: - api/org.ts (조직 트리) - api/company.ts, api/product.ts - api/rule.ts (5종 통합) - api/receive.ts (profile/field/raw/error) - api/config.ts, api/log.ts App.tsx: 14 라우트 추가 (org/tree, companies, products, rules/*, receive/*, ledger/maintain, system/menus, system/config, system/logs) 검증: - tsc -b 통과 - 12개 백엔드 엔드포인트 200 OK 확인 - dev 서버 HMR 자동 반영 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+42
-4
@@ -3,18 +3,38 @@ import LoginPage from '@/pages/LoginPage';
|
|||||||
import MainLayout from '@/layouts/MainLayout';
|
import MainLayout from '@/layouts/MainLayout';
|
||||||
import Dashboard from '@/pages/Dashboard';
|
import Dashboard from '@/pages/Dashboard';
|
||||||
|
|
||||||
|
import OrgTree from '@/pages/org/OrgTree';
|
||||||
import AgentList from '@/pages/org/AgentList';
|
import AgentList from '@/pages/org/AgentList';
|
||||||
import AgentDetail from '@/pages/org/AgentDetail';
|
import AgentDetail from '@/pages/org/AgentDetail';
|
||||||
import AgentForm from '@/pages/org/AgentForm';
|
import AgentForm from '@/pages/org/AgentForm';
|
||||||
|
|
||||||
|
import CompanyList from '@/pages/product/CompanyList';
|
||||||
|
import ProductList from '@/pages/product/ProductList';
|
||||||
import ContractList from '@/pages/product/ContractList';
|
import ContractList from '@/pages/product/ContractList';
|
||||||
|
|
||||||
|
import CommissionRateList from '@/pages/rule/CommissionRateList';
|
||||||
|
import PayoutRuleList from '@/pages/rule/PayoutRuleList';
|
||||||
|
import OverrideRuleList from '@/pages/rule/OverrideRuleList';
|
||||||
|
import ChargebackRuleList from '@/pages/rule/ChargebackRuleList';
|
||||||
|
import ExceptionCodeList from '@/pages/rule/ExceptionCodeList';
|
||||||
|
|
||||||
|
import ReceiveData from '@/pages/receive/ReceiveData';
|
||||||
|
import ReceiveMapping from '@/pages/receive/ReceiveMapping';
|
||||||
|
|
||||||
import RecruitLedger from '@/pages/ledger/RecruitLedger';
|
import RecruitLedger from '@/pages/ledger/RecruitLedger';
|
||||||
|
import MaintainLedger from '@/pages/ledger/MaintainLedger';
|
||||||
import ExceptionLedger from '@/pages/ledger/ExceptionLedger';
|
import ExceptionLedger from '@/pages/ledger/ExceptionLedger';
|
||||||
|
|
||||||
import SettleList from '@/pages/settle/SettleList';
|
import SettleList from '@/pages/settle/SettleList';
|
||||||
import PaymentList from '@/pages/settle/PaymentList';
|
|
||||||
import BatchRun from '@/pages/settle/BatchRun';
|
import BatchRun from '@/pages/settle/BatchRun';
|
||||||
|
import PaymentList from '@/pages/settle/PaymentList';
|
||||||
|
|
||||||
import UserList from '@/pages/system/UserList';
|
import UserList from '@/pages/system/UserList';
|
||||||
import RoleList from '@/pages/system/RoleList';
|
import RoleList from '@/pages/system/RoleList';
|
||||||
|
import MenuManage from '@/pages/system/MenuManage';
|
||||||
import CodeList from '@/pages/system/CodeList';
|
import CodeList from '@/pages/system/CodeList';
|
||||||
|
import SystemConfig from '@/pages/system/SystemConfig';
|
||||||
|
import SystemLog from '@/pages/system/SystemLog';
|
||||||
|
|
||||||
function RequireAuth({ children }: { children: React.ReactNode }) {
|
function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||||
const token = localStorage.getItem('accessToken');
|
const token = localStorage.getItem('accessToken');
|
||||||
@@ -28,20 +48,35 @@ export default function App() {
|
|||||||
<Route path="/" element={<RequireAuth><MainLayout /></RequireAuth>}>
|
<Route path="/" element={<RequireAuth><MainLayout /></RequireAuth>}>
|
||||||
<Route index element={<Dashboard />} />
|
<Route index element={<Dashboard />} />
|
||||||
|
|
||||||
{/* 조직 / 설계사 */}
|
{/* 조직 / 인사 */}
|
||||||
|
<Route path="org/tree" element={<OrgTree />} />
|
||||||
<Route path="org/agents" element={<AgentList />} />
|
<Route path="org/agents" element={<AgentList />} />
|
||||||
<Route path="org/agents/new" element={<AgentForm />} />
|
<Route path="org/agents/new" element={<AgentForm />} />
|
||||||
<Route path="org/agents/:id" element={<AgentDetail />} />
|
<Route path="org/agents/:id" element={<AgentDetail />} />
|
||||||
<Route path="org/agents/:id/edit" element={<AgentForm />} />
|
<Route path="org/agents/:id/edit" element={<AgentForm />} />
|
||||||
|
|
||||||
{/* 계약 */}
|
{/* 상품 / 계약 */}
|
||||||
|
<Route path="companies" element={<CompanyList />} />
|
||||||
|
<Route path="products" element={<ProductList />} />
|
||||||
<Route path="contracts" element={<ContractList />} />
|
<Route path="contracts" element={<ContractList />} />
|
||||||
|
|
||||||
|
{/* 수수료 규정 */}
|
||||||
|
<Route path="rules/commission" element={<CommissionRateList />} />
|
||||||
|
<Route path="rules/payout" element={<PayoutRuleList />} />
|
||||||
|
<Route path="rules/override" element={<OverrideRuleList />} />
|
||||||
|
<Route path="rules/chargeback" element={<ChargebackRuleList />} />
|
||||||
|
<Route path="rules/exceptions" element={<ExceptionCodeList />} />
|
||||||
|
|
||||||
|
{/* 데이터 수신 */}
|
||||||
|
<Route path="receive/data" element={<ReceiveData />} />
|
||||||
|
<Route path="receive/mapping" element={<ReceiveMapping />} />
|
||||||
|
|
||||||
{/* 원장 */}
|
{/* 원장 */}
|
||||||
<Route path="ledger/recruit" element={<RecruitLedger />} />
|
<Route path="ledger/recruit" element={<RecruitLedger />} />
|
||||||
|
<Route path="ledger/maintain" element={<MaintainLedger />} />
|
||||||
<Route path="ledger/exception" element={<ExceptionLedger />} />
|
<Route path="ledger/exception" element={<ExceptionLedger />} />
|
||||||
|
|
||||||
{/* 정산 / 지급 / 배치 */}
|
{/* 정산 / 지급 */}
|
||||||
<Route path="settle" element={<SettleList />} />
|
<Route path="settle" element={<SettleList />} />
|
||||||
<Route path="settle/batch" element={<BatchRun />} />
|
<Route path="settle/batch" element={<BatchRun />} />
|
||||||
<Route path="payments" element={<PaymentList />} />
|
<Route path="payments" element={<PaymentList />} />
|
||||||
@@ -49,7 +84,10 @@ export default function App() {
|
|||||||
{/* 시스템관리 */}
|
{/* 시스템관리 */}
|
||||||
<Route path="system/users" element={<UserList />} />
|
<Route path="system/users" element={<UserList />} />
|
||||||
<Route path="system/roles" element={<RoleList />} />
|
<Route path="system/roles" element={<RoleList />} />
|
||||||
|
<Route path="system/menus" element={<MenuManage />} />
|
||||||
<Route path="system/codes" element={<CodeList />} />
|
<Route path="system/codes" element={<CodeList />} />
|
||||||
|
<Route path="system/config" element={<SystemConfig />} />
|
||||||
|
<Route path="system/logs" element={<SystemLog />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import api, { PageResponse, unwrap } from './request';
|
||||||
|
|
||||||
|
export interface CompanyResp {
|
||||||
|
companyId: number;
|
||||||
|
companyCode: string;
|
||||||
|
companyName: string;
|
||||||
|
companyType?: string;
|
||||||
|
bizNo?: string;
|
||||||
|
contactName?: string;
|
||||||
|
contactPhone?: string;
|
||||||
|
contactEmail?: string;
|
||||||
|
isActive: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompanySearchParam {
|
||||||
|
pageNum?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
searchKeyword?: string;
|
||||||
|
isActive?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const companyApi = {
|
||||||
|
list: (p: CompanySearchParam) =>
|
||||||
|
unwrap<PageResponse<CompanyResp>>(api.get('/api/companies', { params: p })),
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import api, { unwrap } from './request';
|
||||||
|
|
||||||
|
export interface SystemConfigResp {
|
||||||
|
configKey: string;
|
||||||
|
configValue?: string;
|
||||||
|
configDesc?: string;
|
||||||
|
configGroup?: string;
|
||||||
|
valueType?: string;
|
||||||
|
isEncrypted?: string;
|
||||||
|
isEditable?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
updatedBy?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configApi = {
|
||||||
|
list: (group?: string) =>
|
||||||
|
unwrap<SystemConfigResp[]>(api.get('/api/system/config', { params: { group } })),
|
||||||
|
update: (key: string, value: string) =>
|
||||||
|
unwrap<void>(api.put(`/api/system/config/${key}`, { value })),
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import api, { PageResponse, unwrap } from './request';
|
||||||
|
|
||||||
|
export interface LoginLogResp {
|
||||||
|
logId: number;
|
||||||
|
userId?: number;
|
||||||
|
username?: string;
|
||||||
|
loginType: string;
|
||||||
|
ipAddress?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
failReason?: string;
|
||||||
|
loginAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiAccessLogResp {
|
||||||
|
logId: number;
|
||||||
|
userId?: number;
|
||||||
|
username?: string;
|
||||||
|
requestMethod: string;
|
||||||
|
requestUrl: string;
|
||||||
|
responseStatus?: number;
|
||||||
|
responseTime?: number;
|
||||||
|
ipAddress?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataChangeLogResp {
|
||||||
|
logId: number;
|
||||||
|
tableName: string;
|
||||||
|
recordId?: string;
|
||||||
|
actionType: string;
|
||||||
|
changeData?: any;
|
||||||
|
changeReason?: string;
|
||||||
|
userId?: number;
|
||||||
|
username?: string;
|
||||||
|
ipAddress?: string;
|
||||||
|
menuCode?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logApi = {
|
||||||
|
login: (p: { pageNum?: number; pageSize?: number; username?: string; startDate?: string; endDate?: string }) =>
|
||||||
|
unwrap<PageResponse<LoginLogResp>>(api.get('/api/system/logs/login', { params: p })),
|
||||||
|
api: (p: { pageNum?: number; pageSize?: number; username?: string; startDate?: string; endDate?: string }) =>
|
||||||
|
unwrap<PageResponse<ApiAccessLogResp>>(api.get('/api/system/logs/api', { params: p })),
|
||||||
|
change: (p: { pageNum?: number; pageSize?: number; tableName?: string; username?: string; startDate?: string; endDate?: string }) =>
|
||||||
|
unwrap<PageResponse<DataChangeLogResp>>(api.get('/api/system/logs/data-change', { params: p })),
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import api, { unwrap } from './request';
|
||||||
|
|
||||||
|
export interface OrgNode {
|
||||||
|
orgId: number;
|
||||||
|
parentOrgId?: number;
|
||||||
|
orgName: string;
|
||||||
|
orgType: string; // HQ / BR / TM
|
||||||
|
orgLevel: number;
|
||||||
|
region?: string;
|
||||||
|
effectiveFrom?: string;
|
||||||
|
effectiveTo?: string;
|
||||||
|
isActive: string;
|
||||||
|
children?: OrgNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const orgApi = {
|
||||||
|
tree: () => unwrap<OrgNode[]>(api.get('/api/orgs/tree')),
|
||||||
|
list: () => unwrap<OrgNode[]>(api.get('/api/orgs')),
|
||||||
|
detail: (id: number) => unwrap<OrgNode>(api.get(`/api/orgs/${id}`)),
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import api, { PageResponse, unwrap } from './request';
|
||||||
|
|
||||||
|
export interface ProductResp {
|
||||||
|
productId: number;
|
||||||
|
companyId: number;
|
||||||
|
companyName?: string;
|
||||||
|
productCode: string;
|
||||||
|
productName: string;
|
||||||
|
insuranceType: string;
|
||||||
|
productGroup?: string;
|
||||||
|
payPeriodType?: string;
|
||||||
|
isActive: string;
|
||||||
|
launchDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductSearchParam {
|
||||||
|
pageNum?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
searchKeyword?: string;
|
||||||
|
companyId?: number;
|
||||||
|
insuranceType?: string;
|
||||||
|
isActive?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const productApi = {
|
||||||
|
list: (p: ProductSearchParam) =>
|
||||||
|
unwrap<PageResponse<ProductResp>>(api.get('/api/products', { params: p })),
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import api, { unwrap } from './request';
|
||||||
|
|
||||||
|
export interface CompanyProfileResp {
|
||||||
|
companyCode: string;
|
||||||
|
dataFormat?: string;
|
||||||
|
receiveMethod?: string;
|
||||||
|
fileEncoding?: string;
|
||||||
|
delimiter?: string;
|
||||||
|
headerRow?: number;
|
||||||
|
dataStartRow?: number;
|
||||||
|
sheetName?: string;
|
||||||
|
dateFormat?: string;
|
||||||
|
amountFormat?: string;
|
||||||
|
apiUrl?: string;
|
||||||
|
ftpHost?: string;
|
||||||
|
ftpPath?: string;
|
||||||
|
receiveSchedule?: string;
|
||||||
|
isActive: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldMappingResp {
|
||||||
|
mappingId: number;
|
||||||
|
companyCode: string;
|
||||||
|
sourceField: string;
|
||||||
|
sourceColIndex?: number;
|
||||||
|
targetTable: string;
|
||||||
|
targetField: string;
|
||||||
|
dataType?: string;
|
||||||
|
transformRule?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
isRequired?: string;
|
||||||
|
sortOrder: number;
|
||||||
|
version?: number;
|
||||||
|
effectiveFrom?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RawCommissionResp {
|
||||||
|
rawId: number;
|
||||||
|
companyCode: string;
|
||||||
|
batchId?: string;
|
||||||
|
settleMonth: string;
|
||||||
|
dataType: string;
|
||||||
|
parseStatus: string;
|
||||||
|
mappedLedgerId?: number;
|
||||||
|
mappedTable?: string;
|
||||||
|
fileName?: string;
|
||||||
|
rowNumber?: number;
|
||||||
|
receivedAt?: string;
|
||||||
|
parsedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParseErrorResp {
|
||||||
|
errorId: number;
|
||||||
|
rawId?: number;
|
||||||
|
companyCode: string;
|
||||||
|
errorType: string;
|
||||||
|
errorField?: string;
|
||||||
|
errorValue?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
resolveStatus: string;
|
||||||
|
resolvedBy?: number;
|
||||||
|
resolveNote?: string;
|
||||||
|
resolvedAt?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const receiveApi = {
|
||||||
|
profiles: () => unwrap<CompanyProfileResp[]>(api.get('/api/receive/profiles')),
|
||||||
|
fields: (companyCode: string) =>
|
||||||
|
unwrap<FieldMappingResp[]>(api.get(`/api/receive/mapping/${companyCode}/fields`)),
|
||||||
|
raw: (params: { companyCode?: string; settleMonth?: string; parseStatus?: string }) =>
|
||||||
|
unwrap<RawCommissionResp[]>(api.get('/api/receive/raw', { params })),
|
||||||
|
errors: (params: { companyCode?: string; resolveStatus?: string }) =>
|
||||||
|
unwrap<ParseErrorResp[]>(api.get('/api/receive/errors', { params })),
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import api, { unwrap } from './request';
|
||||||
|
|
||||||
|
export interface CommissionRateResp {
|
||||||
|
rateId: number;
|
||||||
|
productId: number;
|
||||||
|
productName?: string;
|
||||||
|
commissionYear: number;
|
||||||
|
ratePct: number;
|
||||||
|
payMethodCond?: string;
|
||||||
|
minPremium?: number;
|
||||||
|
effectiveFrom?: string;
|
||||||
|
effectiveTo?: string;
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayoutRuleResp {
|
||||||
|
ruleId: number;
|
||||||
|
gradeId: number;
|
||||||
|
gradeName?: string;
|
||||||
|
insuranceType: string;
|
||||||
|
commissionYear: number;
|
||||||
|
payoutPct: number;
|
||||||
|
payMethodCond?: string;
|
||||||
|
performanceGrade?: string;
|
||||||
|
effectiveFrom?: string;
|
||||||
|
effectiveTo?: string;
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OverrideRuleResp {
|
||||||
|
overrideId: number;
|
||||||
|
fromGrade: number;
|
||||||
|
toGrade: number;
|
||||||
|
fromGradeName?: string;
|
||||||
|
toGradeName?: string;
|
||||||
|
overridePct: number;
|
||||||
|
calcType?: string;
|
||||||
|
insuranceTypeCond?: string;
|
||||||
|
capAmount?: number;
|
||||||
|
effectiveFrom?: string;
|
||||||
|
effectiveTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChargebackRuleResp {
|
||||||
|
cbRuleId: number;
|
||||||
|
companyCode: string;
|
||||||
|
insuranceType: string;
|
||||||
|
lapseMonthFrom: number;
|
||||||
|
lapseMonthTo: number;
|
||||||
|
cbRate: number;
|
||||||
|
includeOverride: string;
|
||||||
|
installmentAllowed: string;
|
||||||
|
maxInstallments?: number;
|
||||||
|
effectiveFrom?: string;
|
||||||
|
effectiveTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExceptionCodeResp {
|
||||||
|
exceptionCode: string;
|
||||||
|
exceptionName: string;
|
||||||
|
category?: string;
|
||||||
|
direction: string;
|
||||||
|
autoCalcYn: string;
|
||||||
|
approvalRequired: string;
|
||||||
|
taxIncluded: string;
|
||||||
|
description?: string;
|
||||||
|
isActive: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ruleApi = {
|
||||||
|
commissionRates: (params?: { productId?: number; year?: number }) =>
|
||||||
|
unwrap<CommissionRateResp[]>(api.get('/api/rules/commission-rates', { params })),
|
||||||
|
payoutRules: (params?: { gradeId?: number; insuranceType?: string }) =>
|
||||||
|
unwrap<PayoutRuleResp[]>(api.get('/api/rules/payout', { params })),
|
||||||
|
overrideRules: () => unwrap<OverrideRuleResp[]>(api.get('/api/rules/override')),
|
||||||
|
chargebackRules: (params?: { companyCode?: string }) =>
|
||||||
|
unwrap<ChargebackRuleResp[]>(api.get('/api/rules/chargeback', { params })),
|
||||||
|
exceptionCodes: () => unwrap<ExceptionCodeResp[]>(api.get('/api/rules/exception-codes')),
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import CodeBadge from '@/components/common/CodeBadge';
|
||||||
|
import MoneyText from '@/components/common/MoneyText';
|
||||||
|
import ExcelExportButton from '@/components/common/ExcelExportButton';
|
||||||
|
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||||
|
import { ledgerApi, LedgerResp, LedgerSearchParam } from '@/api/ledger';
|
||||||
|
|
||||||
|
export default function MaintainLedger() {
|
||||||
|
const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
|
||||||
|
const { data: reconCodes = [] } = useCommonCodes('RECON_STATUS');
|
||||||
|
|
||||||
|
const columns: ProColumns<LedgerResp>[] = [
|
||||||
|
{ title: '정산월', dataIndex: 'settleMonth', width: 90, fixed: 'left',
|
||||||
|
valueType: 'dateMonth', fieldProps: { format: 'YYYYMM' } },
|
||||||
|
{ title: '증권번호', dataIndex: 'policyNo', width: 130, fixed: 'left',
|
||||||
|
fieldProps: { placeholder: '증권번호' } },
|
||||||
|
{
|
||||||
|
title: '보험종류', dataIndex: 'insuranceType', width: 100, valueType: 'select',
|
||||||
|
valueEnum: Object.fromEntries(insuranceCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||||
|
render: (_, r) => <CodeBadge groupCode="INSURANCE_TYPE" value={r.insuranceType} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '대사상태', dataIndex: 'reconcileStatus', width: 100, valueType: 'select',
|
||||||
|
valueEnum: Object.fromEntries(reconCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||||
|
render: (_, r) => <CodeBadge groupCode="RECON_STATUS" value={r.reconcileStatus} />,
|
||||||
|
},
|
||||||
|
{ title: '설계사', dataIndex: 'agentName', width: 100, search: false },
|
||||||
|
{ title: '소속', dataIndex: 'orgName', width: 160, search: false },
|
||||||
|
{ title: '보험사', dataIndex: 'companyName', width: 120, search: false },
|
||||||
|
{ title: '상품', dataIndex: 'productName', width: 200, search: false },
|
||||||
|
{
|
||||||
|
title: '보험료', dataIndex: 'premium', width: 110, align: 'right', search: false,
|
||||||
|
render: (_, r) => <MoneyText value={r.premium} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '회사율', dataIndex: 'companyRate', width: 80, align: 'right', search: false,
|
||||||
|
render: (_, r) => <MoneyText value={r.companyRate} fraction={2} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '회사수수료', dataIndex: 'companyAmount', width: 120, align: 'right', search: false,
|
||||||
|
render: (_, r) => <MoneyText value={r.companyAmount} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '지급율', dataIndex: 'payoutRate', width: 80, align: 'right', search: false,
|
||||||
|
render: (_, r) => <MoneyText value={r.payoutRate} fraction={2} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '설계사수수료', dataIndex: 'agentAmount', width: 120, align: 'right', search: false,
|
||||||
|
render: (_, r) => <MoneyText value={r.agentAmount} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '세액', dataIndex: 'taxAmount', width: 100, align: 'right', search: false,
|
||||||
|
render: (_, r) => <MoneyText value={r.taxAmount} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="유지수수료 원장" description="2~5년차 유지 계약에 대한 유지수수료">
|
||||||
|
<ProTable<LedgerResp, LedgerSearchParam>
|
||||||
|
rowKey="ledgerId"
|
||||||
|
columns={columns}
|
||||||
|
scroll={{ x: 1500 }}
|
||||||
|
request={async (params) => {
|
||||||
|
const { current, pageSize, ...rest } = params as any;
|
||||||
|
const res = await ledgerApi.maintain({ ...rest, pageNum: current, pageSize });
|
||||||
|
return { data: res.list, total: res.total, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
|
||||||
|
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||||
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<ExcelExportButton key="export" url="/api/ledger/maintain/export" fileName="유지수수료원장" />,
|
||||||
|
]}
|
||||||
|
dateFormatter="string"
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { Tree, Card, Empty, Tag, Spin } from 'antd';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { ProCard, ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import { IconBuilding } from '@tabler/icons-react';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import CodeBadge from '@/components/common/CodeBadge';
|
||||||
|
import MoneyText from '@/components/common/MoneyText';
|
||||||
|
import { orgApi, OrgNode } from '@/api/org';
|
||||||
|
import { agentApi, AgentResp } from '@/api/agent';
|
||||||
|
|
||||||
|
interface AntdTreeNode { key: string; title: any; children?: AntdTreeNode[]; orgId: number; orgType: string; }
|
||||||
|
|
||||||
|
function toTree(nodes: OrgNode[] = []): AntdTreeNode[] {
|
||||||
|
return nodes.map((n) => ({
|
||||||
|
key: String(n.orgId),
|
||||||
|
orgId: n.orgId,
|
||||||
|
orgType: n.orgType,
|
||||||
|
title: (
|
||||||
|
<span>
|
||||||
|
<Tag color={n.orgType === 'HQ' ? 'blue' : n.orgType === 'BR' ? 'cyan' : 'green'} style={{ marginRight: 6 }}>
|
||||||
|
{n.orgType}
|
||||||
|
</Tag>
|
||||||
|
{n.orgName}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
children: n.children ? toTree(n.children) : undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OrgTree() {
|
||||||
|
const [selectedOrgId, setSelectedOrgId] = useState<number | undefined>();
|
||||||
|
|
||||||
|
const { data: tree = [], isLoading: treeLoading } = useQuery({
|
||||||
|
queryKey: ['orgs', 'tree'],
|
||||||
|
queryFn: orgApi.tree,
|
||||||
|
});
|
||||||
|
const treeData = useMemo(() => toTree(tree), [tree]);
|
||||||
|
|
||||||
|
const columns: ProColumns<AgentResp>[] = [
|
||||||
|
{ title: '설계사명', dataIndex: 'agentName', width: 110 },
|
||||||
|
{ title: '직급', dataIndex: 'gradeName', width: 90 },
|
||||||
|
{
|
||||||
|
title: '상태', dataIndex: 'status', width: 90,
|
||||||
|
render: (_, r) => <CodeBadge groupCode="AGENT_STATUS" value={r.status} />,
|
||||||
|
},
|
||||||
|
{ title: '사번', dataIndex: 'licenseNo', width: 110 },
|
||||||
|
{ title: '전화번호', dataIndex: 'phone', width: 130 },
|
||||||
|
{ title: '계약수', dataIndex: 'contractCount', width: 80, align: 'right',
|
||||||
|
render: (_, r) => <MoneyText value={r.contractCount} /> },
|
||||||
|
{ title: '누적수수료', dataIndex: 'totalCommission', align: 'right',
|
||||||
|
render: (_, r) => <MoneyText value={r.totalCommission} /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="조직 트리" description="본부 / 지점 / 팀 트리 + 소속 설계사">
|
||||||
|
<ProCard split="vertical" ghost>
|
||||||
|
<ProCard colSpan="280px" bordered title={<><IconBuilding size={16} style={{ verticalAlign: -2 }} /> 조직</>}>
|
||||||
|
{treeLoading ? <Spin /> : (
|
||||||
|
treeData.length ? (
|
||||||
|
<Tree
|
||||||
|
treeData={treeData as any}
|
||||||
|
defaultExpandAll
|
||||||
|
onSelect={(keys, e) => {
|
||||||
|
const node = e.node as unknown as AntdTreeNode;
|
||||||
|
setSelectedOrgId(node.orgId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : <Empty description="조직 없음" />
|
||||||
|
)}
|
||||||
|
</ProCard>
|
||||||
|
<ProCard ghost>
|
||||||
|
{selectedOrgId ? (
|
||||||
|
<Card bordered={false} title={<span style={{ fontWeight: 600 }}>소속 설계사</span>}>
|
||||||
|
<ProTable<AgentResp>
|
||||||
|
rowKey="agentId"
|
||||||
|
columns={columns}
|
||||||
|
search={false}
|
||||||
|
options={false}
|
||||||
|
request={async (params) => {
|
||||||
|
const res = await agentApi.list({
|
||||||
|
orgId: selectedOrgId,
|
||||||
|
pageNum: params.current ?? 1,
|
||||||
|
pageSize: params.pageSize ?? 50,
|
||||||
|
});
|
||||||
|
return { data: res.list, total: res.total, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}명` }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Empty description="좌측에서 조직을 선택하세요" />
|
||||||
|
)}
|
||||||
|
</ProCard>
|
||||||
|
</ProCard>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import { Tag } from 'antd';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import { companyApi, CompanyResp, CompanySearchParam } from '@/api/company';
|
||||||
|
|
||||||
|
export default function CompanyList() {
|
||||||
|
const columns: ProColumns<CompanyResp>[] = [
|
||||||
|
{ title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
|
||||||
|
fieldProps: { placeholder: '회사명 / 사업자번호' } },
|
||||||
|
{ title: '회사코드', dataIndex: 'companyCode', width: 100, search: false },
|
||||||
|
{ title: '회사명', dataIndex: 'companyName', width: 180,
|
||||||
|
render: (_, r) => <strong>{r.companyName}</strong> },
|
||||||
|
{ title: '구분', dataIndex: 'companyType', width: 100, search: false },
|
||||||
|
{ title: '사업자번호', dataIndex: 'bizNo', width: 130, search: false },
|
||||||
|
{ title: '담당자', dataIndex: 'contactName', width: 100, search: false },
|
||||||
|
{ title: '연락처', dataIndex: 'contactPhone', width: 130, search: false },
|
||||||
|
{ title: '이메일', dataIndex: 'contactEmail', width: 200, search: false },
|
||||||
|
{
|
||||||
|
title: '활성', dataIndex: 'isActive', width: 80,
|
||||||
|
render: (_, r) => <Tag color={r.isActive === 'Y' ? 'success' : 'default'}>
|
||||||
|
{r.isActive === 'Y' ? '활성' : '비활성'}</Tag>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="보험사 관리" description="제휴 보험사 정보 / 데이터 수신 프로파일">
|
||||||
|
<ProTable<CompanyResp, CompanySearchParam>
|
||||||
|
rowKey="companyId"
|
||||||
|
columns={columns}
|
||||||
|
request={async (params) => {
|
||||||
|
const { current, pageSize, ...rest } = params as any;
|
||||||
|
const res = await companyApi.list({ ...rest, pageNum: current, pageSize });
|
||||||
|
return { data: res.list, total: res.total, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 20, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
|
||||||
|
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||||
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
|
dateFormatter="string"
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import { Tag } from 'antd';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import CodeBadge from '@/components/common/CodeBadge';
|
||||||
|
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||||
|
import { productApi, ProductResp, ProductSearchParam } from '@/api/product';
|
||||||
|
|
||||||
|
export default function ProductList() {
|
||||||
|
const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
|
||||||
|
|
||||||
|
const columns: ProColumns<ProductResp>[] = [
|
||||||
|
{ title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
|
||||||
|
fieldProps: { placeholder: '상품명 / 코드' } },
|
||||||
|
{
|
||||||
|
title: '보험종류', dataIndex: 'insuranceType', width: 110, valueType: 'select',
|
||||||
|
valueEnum: Object.fromEntries(insuranceCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||||
|
render: (_, r) => <CodeBadge groupCode="INSURANCE_TYPE" value={r.insuranceType} />,
|
||||||
|
},
|
||||||
|
{ title: '상품코드', dataIndex: 'productCode', width: 120, search: false },
|
||||||
|
{ title: '상품명', dataIndex: 'productName', width: 240, search: false,
|
||||||
|
render: (_, r) => <strong>{r.productName}</strong> },
|
||||||
|
{ title: '보험사', dataIndex: 'companyName', width: 120, search: false },
|
||||||
|
{ title: '상품군', dataIndex: 'productGroup', width: 100, search: false },
|
||||||
|
{ title: '납입주기', dataIndex: 'payPeriodType', width: 100, search: false },
|
||||||
|
{ title: '판매개시', dataIndex: 'launchDate', width: 110, search: false },
|
||||||
|
{ title: '판매종료', dataIndex: 'endDate', width: 110, search: false },
|
||||||
|
{
|
||||||
|
title: '활성', dataIndex: 'isActive', width: 80, search: false,
|
||||||
|
render: (_, r) => <Tag color={r.isActive === 'Y' ? 'success' : 'default'}>
|
||||||
|
{r.isActive === 'Y' ? '활성' : '비활성'}</Tag>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="상품 관리" description="보험사별 보험 상품 / 보험종류 / 상품군">
|
||||||
|
<ProTable<ProductResp, ProductSearchParam>
|
||||||
|
rowKey="productId"
|
||||||
|
columns={columns}
|
||||||
|
scroll={{ x: 1300 }}
|
||||||
|
request={async (params) => {
|
||||||
|
const { current, pageSize, ...rest } = params as any;
|
||||||
|
const res = await productApi.list({ ...rest, pageNum: current, pageSize });
|
||||||
|
return { data: res.list, total: res.total, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 20, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
|
||||||
|
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||||
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
|
dateFormatter="string"
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import { Tag } from 'antd';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||||
|
import { receiveApi, RawCommissionResp, ParseErrorResp } from '@/api/receive';
|
||||||
|
|
||||||
|
const STATUS_COLOR = { PENDING: 'processing', OK: 'success', ERROR: 'error' } as const;
|
||||||
|
|
||||||
|
export default function ReceiveData() {
|
||||||
|
const { data: companies = [] } = useCommonCodes('COMPANY_CODE');
|
||||||
|
|
||||||
|
const rawColumns: ProColumns<RawCommissionResp>[] = [
|
||||||
|
{
|
||||||
|
title: '보험사', dataIndex: 'companyCode', width: 110, valueType: 'select',
|
||||||
|
valueEnum: Object.fromEntries(companies.map((c) => [c.code, { text: c.codeName }])),
|
||||||
|
},
|
||||||
|
{ title: '정산월', dataIndex: 'settleMonth', width: 90, valueType: 'dateMonth',
|
||||||
|
fieldProps: { format: 'YYYYMM' } },
|
||||||
|
{
|
||||||
|
title: '파싱상태', dataIndex: 'parseStatus', width: 110, valueType: 'select',
|
||||||
|
valueEnum: { PENDING: { text: '대기' }, OK: { text: '성공' }, ERROR: { text: '실패' } },
|
||||||
|
render: (_, r) => <Tag color={STATUS_COLOR[r.parseStatus as 'PENDING' | 'OK' | 'ERROR'] ?? 'default'}>
|
||||||
|
{r.parseStatus}</Tag>,
|
||||||
|
},
|
||||||
|
{ title: '데이터 유형', dataIndex: 'dataType', width: 110, search: false },
|
||||||
|
{ title: '배치 ID', dataIndex: 'batchId', width: 140, search: false },
|
||||||
|
{ title: '파일명', dataIndex: 'fileName', width: 200, search: false, ellipsis: true },
|
||||||
|
{ title: '행 번호', dataIndex: 'rowNumber', width: 80, align: 'right', search: false },
|
||||||
|
{ title: '연결 원장', dataIndex: 'mappedLedgerId', width: 100, align: 'right', search: false },
|
||||||
|
{ title: '수신일시', dataIndex: 'receivedAt', width: 160, search: false },
|
||||||
|
{ title: '파싱일시', dataIndex: 'parsedAt', width: 160, search: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="수신 데이터" description="보험사 수수료 raw 데이터 / 파싱 상태">
|
||||||
|
<ProTable<RawCommissionResp>
|
||||||
|
rowKey="rawId"
|
||||||
|
columns={rawColumns}
|
||||||
|
scroll={{ x: 1300 }}
|
||||||
|
request={async (params) => {
|
||||||
|
const list = await receiveApi.raw({
|
||||||
|
companyCode: (params as any).companyCode,
|
||||||
|
settleMonth: (params as any).settleMonth,
|
||||||
|
parseStatus: (params as any).parseStatus,
|
||||||
|
});
|
||||||
|
return { data: list, total: list.length, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
|
||||||
|
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||||
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
|
dateFormatter="string"
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Empty, Tag } from 'antd';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { ProCard, ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import { receiveApi, CompanyProfileResp, FieldMappingResp } from '@/api/receive';
|
||||||
|
|
||||||
|
export default function ReceiveMapping() {
|
||||||
|
const [selected, setSelected] = useState<string | undefined>();
|
||||||
|
|
||||||
|
const { data: profiles = [] } = useQuery({
|
||||||
|
queryKey: ['receive', 'profiles'],
|
||||||
|
queryFn: receiveApi.profiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
const profileColumns: ProColumns<CompanyProfileResp>[] = [
|
||||||
|
{ title: '보험사', dataIndex: 'companyCode', width: 90,
|
||||||
|
render: (_, r) => <strong>{r.companyCode}</strong> },
|
||||||
|
{ title: '데이터 형식', dataIndex: 'dataFormat', width: 110 },
|
||||||
|
{ title: '수신방법', dataIndex: 'receiveMethod', width: 110 },
|
||||||
|
{
|
||||||
|
title: '활성', dataIndex: 'isActive', width: 70, align: 'center',
|
||||||
|
render: (_, r) => <Tag color={r.isActive === 'Y' ? 'success' : 'default'}>
|
||||||
|
{r.isActive === 'Y' ? 'Y' : 'N'}</Tag>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fieldColumns: ProColumns<FieldMappingResp>[] = [
|
||||||
|
{ title: '#', dataIndex: 'sortOrder', width: 50, align: 'center' },
|
||||||
|
{ title: 'Source 필드', dataIndex: 'sourceField', width: 130 },
|
||||||
|
{ title: 'Source 컬럼', dataIndex: 'sourceColIndex', width: 90, align: 'right' },
|
||||||
|
{ title: 'Target 테이블', dataIndex: 'targetTable', width: 120 },
|
||||||
|
{ title: 'Target 필드', dataIndex: 'targetField', width: 130 },
|
||||||
|
{ title: 'Type', dataIndex: 'dataType', width: 100 },
|
||||||
|
{ title: '변환규칙', dataIndex: 'transformRule', width: 110,
|
||||||
|
render: (_, r) => r.transformRule ? <Tag color="blue">{r.transformRule}</Tag> : '-' },
|
||||||
|
{ title: '기본값', dataIndex: 'defaultValue', width: 110 },
|
||||||
|
{ title: '필수', dataIndex: 'isRequired', width: 70, align: 'center',
|
||||||
|
render: (_, r) => r.isRequired === 'Y' ? <Tag color="error">필수</Tag> : '' },
|
||||||
|
{ title: '버전', dataIndex: 'version', width: 60, align: 'center' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="매핑 관리" description="보험사 raw 데이터 → 표준 원장 매핑 규칙">
|
||||||
|
<ProCard split="vertical" ghost>
|
||||||
|
<ProCard colSpan="380px" bordered title={<span style={{ fontWeight: 600 }}>보험사 프로파일</span>}>
|
||||||
|
<ProTable<CompanyProfileResp>
|
||||||
|
rowKey="companyCode"
|
||||||
|
columns={profileColumns}
|
||||||
|
dataSource={profiles}
|
||||||
|
search={false}
|
||||||
|
options={false}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
onRow={(r) => ({
|
||||||
|
onClick: () => setSelected(r.companyCode),
|
||||||
|
style: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: selected === r.companyCode ? '#E6F1FB' : undefined,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</ProCard>
|
||||||
|
<ProCard
|
||||||
|
ghost
|
||||||
|
title={selected ? <span style={{ fontWeight: 600 }}>필드 매핑 — {selected}</span>
|
||||||
|
: <span>좌측에서 보험사 선택</span>}
|
||||||
|
>
|
||||||
|
{selected ? (
|
||||||
|
<ProTable<FieldMappingResp>
|
||||||
|
rowKey="mappingId"
|
||||||
|
columns={fieldColumns}
|
||||||
|
search={false}
|
||||||
|
options={false}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
request={async () => {
|
||||||
|
const list = await receiveApi.fields(selected);
|
||||||
|
return { data: list, total: list.length, success: true };
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Empty description="좌측에서 보험사를 선택하세요" />
|
||||||
|
)}
|
||||||
|
</ProCard>
|
||||||
|
</ProCard>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Tag } from 'antd';
|
||||||
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import CodeBadge from '@/components/common/CodeBadge';
|
||||||
|
import MoneyText from '@/components/common/MoneyText';
|
||||||
|
import { ruleApi, ChargebackRuleResp } from '@/api/rule';
|
||||||
|
|
||||||
|
export default function ChargebackRuleList() {
|
||||||
|
const columns: ProColumns<ChargebackRuleResp>[] = [
|
||||||
|
{ title: '보험사', dataIndex: 'companyCode', width: 100, search: false,
|
||||||
|
render: (_, r) => <strong>{r.companyCode}</strong> },
|
||||||
|
{
|
||||||
|
title: '보험종', dataIndex: 'insuranceType', width: 110, search: false,
|
||||||
|
render: (_, r) => <CodeBadge groupCode="INSURANCE_TYPE" value={r.insuranceType} />,
|
||||||
|
},
|
||||||
|
{ title: '실효 시작월', dataIndex: 'lapseMonthFrom', width: 100, align: 'center', search: false },
|
||||||
|
{ title: '실효 종료월', dataIndex: 'lapseMonthTo', width: 100, align: 'center', search: false },
|
||||||
|
{
|
||||||
|
title: '환수율(%)', dataIndex: 'cbRate', width: 110, align: 'right', search: false,
|
||||||
|
render: (_, r) => <strong><MoneyText value={r.cbRate} fraction={4} /></strong>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '오버라이드 포함', dataIndex: 'includeOverride', width: 130, align: 'center', search: false,
|
||||||
|
render: (_, r) => <Tag color={r.includeOverride === 'Y' ? 'success' : 'default'}>
|
||||||
|
{r.includeOverride === 'Y' ? '포함' : '제외'}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '분할 가능', dataIndex: 'installmentAllowed', width: 100, align: 'center', search: false,
|
||||||
|
render: (_, r) => <Tag color={r.installmentAllowed === 'Y' ? 'success' : 'default'}>
|
||||||
|
{r.installmentAllowed === 'Y' ? '허용' : '불가'}</Tag>,
|
||||||
|
},
|
||||||
|
{ title: '최대 분할', dataIndex: 'maxInstallments', width: 90, align: 'right', search: false },
|
||||||
|
{ title: '적용시작', dataIndex: 'effectiveFrom', width: 110, search: false },
|
||||||
|
{ title: '적용종료', dataIndex: 'effectiveTo', width: 110, search: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="환수 규정" description="실효 계약 환수율 / 분할환수 정책">
|
||||||
|
<ProTable<ChargebackRuleResp>
|
||||||
|
rowKey="cbRuleId"
|
||||||
|
columns={columns}
|
||||||
|
search={false}
|
||||||
|
request={async () => {
|
||||||
|
const list = await ruleApi.chargebackRules();
|
||||||
|
return { data: list, total: list.length, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}건` }}
|
||||||
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
|
dateFormatter="string"
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import MoneyText from '@/components/common/MoneyText';
|
||||||
|
import { ruleApi, CommissionRateResp } from '@/api/rule';
|
||||||
|
|
||||||
|
export default function CommissionRateList() {
|
||||||
|
const columns: ProColumns<CommissionRateResp>[] = [
|
||||||
|
{ title: '상품ID', dataIndex: 'productId', width: 90 },
|
||||||
|
{ title: '상품명', dataIndex: 'productName', width: 200, search: false },
|
||||||
|
{ title: '회차', dataIndex: 'commissionYear', width: 70, align: 'center', search: false },
|
||||||
|
{
|
||||||
|
title: '수수료율(%)', dataIndex: 'ratePct', width: 110, align: 'right', search: false,
|
||||||
|
render: (_, r) => <strong><MoneyText value={r.ratePct} fraction={4} /></strong>,
|
||||||
|
},
|
||||||
|
{ title: '납입조건', dataIndex: 'payMethodCond', width: 110, search: false },
|
||||||
|
{
|
||||||
|
title: '최소보험료', dataIndex: 'minPremium', width: 130, align: 'right', search: false,
|
||||||
|
render: (_, r) => <MoneyText value={r.minPremium} />,
|
||||||
|
},
|
||||||
|
{ title: '적용시작', dataIndex: 'effectiveFrom', width: 110, search: false },
|
||||||
|
{ title: '적용종료', dataIndex: 'effectiveTo', width: 110, search: false },
|
||||||
|
{ title: '버전', dataIndex: 'version', width: 60, align: 'center', search: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="수수료율 (보험사 → 시스템)" description="상품·회차별 보험사 통보 수수료율">
|
||||||
|
<ProTable<CommissionRateResp>
|
||||||
|
rowKey="rateId"
|
||||||
|
columns={columns}
|
||||||
|
search={false}
|
||||||
|
request={async () => {
|
||||||
|
const list = await ruleApi.commissionRates();
|
||||||
|
return { data: list, total: list.length, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
|
||||||
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
|
dateFormatter="string"
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Tag } from 'antd';
|
||||||
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import { ruleApi, ExceptionCodeResp } from '@/api/rule';
|
||||||
|
|
||||||
|
const DIRECTION_COLOR = { PLUS: 'success', MINUS: 'error' } as const;
|
||||||
|
|
||||||
|
export default function ExceptionCodeList() {
|
||||||
|
const columns: ProColumns<ExceptionCodeResp>[] = [
|
||||||
|
{ title: '예외코드', dataIndex: 'exceptionCode', width: 120, search: false,
|
||||||
|
render: (_, r) => <strong>{r.exceptionCode}</strong> },
|
||||||
|
{ title: '예외명', dataIndex: 'exceptionName', width: 160, search: false },
|
||||||
|
{ title: '카테고리', dataIndex: 'category', width: 110, search: false },
|
||||||
|
{
|
||||||
|
title: '방향', dataIndex: 'direction', width: 80, align: 'center', search: false,
|
||||||
|
render: (_, r) => <Tag color={DIRECTION_COLOR[r.direction as 'PLUS' | 'MINUS'] ?? 'default'}>
|
||||||
|
{r.direction === 'PLUS' ? '가산 (+)' : '차감 (-)'}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '자동계산', dataIndex: 'autoCalcYn', width: 90, align: 'center', search: false,
|
||||||
|
render: (_, r) => r.autoCalcYn === 'Y' ? <Tag color="processing">자동</Tag> : <Tag>수동</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '승인 필요', dataIndex: 'approvalRequired', width: 90, align: 'center', search: false,
|
||||||
|
render: (_, r) => r.approvalRequired === 'Y' ? <Tag color="warning">필요</Tag> : <Tag>불요</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '과세', dataIndex: 'taxIncluded', width: 80, align: 'center', search: false,
|
||||||
|
render: (_, r) => r.taxIncluded === 'Y' ? <Tag color="success">과세</Tag> : <Tag>비과세</Tag>,
|
||||||
|
},
|
||||||
|
{ title: '설명', dataIndex: 'description', ellipsis: true, search: false },
|
||||||
|
{
|
||||||
|
title: '활성', dataIndex: 'isActive', width: 70, align: 'center', search: false,
|
||||||
|
render: (_, r) => <Tag color={r.isActive === 'Y' ? 'success' : 'default'}>
|
||||||
|
{r.isActive === 'Y' ? 'Y' : 'N'}</Tag>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="예외 코드" description="가산/차감/승인/세금 정책 마스터 (예외금액 원장에서 사용)">
|
||||||
|
<ProTable<ExceptionCodeResp>
|
||||||
|
rowKey="exceptionCode"
|
||||||
|
columns={columns}
|
||||||
|
search={false}
|
||||||
|
request={async () => {
|
||||||
|
const list = await ruleApi.exceptionCodes();
|
||||||
|
return { data: list, total: list.length, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}건` }}
|
||||||
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
|
dateFormatter="string"
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import MoneyText from '@/components/common/MoneyText';
|
||||||
|
import { ruleApi, OverrideRuleResp } from '@/api/rule';
|
||||||
|
|
||||||
|
export default function OverrideRuleList() {
|
||||||
|
const columns: ProColumns<OverrideRuleResp>[] = [
|
||||||
|
{ title: '하위 직급', dataIndex: 'fromGradeName', width: 110, search: false,
|
||||||
|
render: (_, r) => <strong>{r.fromGradeName ?? `#${r.fromGrade}`}</strong> },
|
||||||
|
{ title: '상위 직급', dataIndex: 'toGradeName', width: 110, search: false,
|
||||||
|
render: (_, r) => r.toGradeName ?? `#${r.toGrade}` },
|
||||||
|
{
|
||||||
|
title: '오버라이드율(%)', dataIndex: 'overridePct', width: 130, align: 'right', search: false,
|
||||||
|
render: (_, r) => <strong><MoneyText value={r.overridePct} fraction={4} /></strong>,
|
||||||
|
},
|
||||||
|
{ title: '계산방식', dataIndex: 'calcType', width: 110, search: false },
|
||||||
|
{ title: '보험종조건', dataIndex: 'insuranceTypeCond', width: 120, search: false },
|
||||||
|
{
|
||||||
|
title: '월 상한', dataIndex: 'capAmount', width: 130, align: 'right', search: false,
|
||||||
|
render: (_, r) => <MoneyText value={r.capAmount} />,
|
||||||
|
},
|
||||||
|
{ title: '적용시작', dataIndex: 'effectiveFrom', width: 110, search: false },
|
||||||
|
{ title: '적용종료', dataIndex: 'effectiveTo', width: 110, search: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="오버라이드 규정" description="조직 트리 상향 분배율 (하위 → 상위 직급)">
|
||||||
|
<ProTable<OverrideRuleResp>
|
||||||
|
rowKey="overrideId"
|
||||||
|
columns={columns}
|
||||||
|
search={false}
|
||||||
|
request={async () => {
|
||||||
|
const list = await ruleApi.overrideRules();
|
||||||
|
return { data: list, total: list.length, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}건` }}
|
||||||
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
|
dateFormatter="string"
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import CodeBadge from '@/components/common/CodeBadge';
|
||||||
|
import MoneyText from '@/components/common/MoneyText';
|
||||||
|
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||||
|
import { ruleApi, PayoutRuleResp } from '@/api/rule';
|
||||||
|
|
||||||
|
export default function PayoutRuleList() {
|
||||||
|
const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
|
||||||
|
|
||||||
|
const columns: ProColumns<PayoutRuleResp>[] = [
|
||||||
|
{ title: '직급', dataIndex: 'gradeName', width: 100, search: false,
|
||||||
|
render: (_, r) => <strong>{r.gradeName}</strong> },
|
||||||
|
{
|
||||||
|
title: '보험종류', dataIndex: 'insuranceType', width: 110,
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: Object.fromEntries(insuranceCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||||
|
render: (_, r) => <CodeBadge groupCode="INSURANCE_TYPE" value={r.insuranceType} />,
|
||||||
|
},
|
||||||
|
{ title: '회차', dataIndex: 'commissionYear', width: 70, align: 'center', search: false },
|
||||||
|
{
|
||||||
|
title: '지급율(%)', dataIndex: 'payoutPct', width: 100, align: 'right', search: false,
|
||||||
|
render: (_, r) => <strong><MoneyText value={r.payoutPct} fraction={4} /></strong>,
|
||||||
|
},
|
||||||
|
{ title: '납입조건', dataIndex: 'payMethodCond', width: 110, search: false },
|
||||||
|
{ title: '실적등급', dataIndex: 'performanceGrade', width: 100, search: false },
|
||||||
|
{ title: '적용시작', dataIndex: 'effectiveFrom', width: 110, search: false },
|
||||||
|
{ title: '적용종료', dataIndex: 'effectiveTo', width: 110, search: false },
|
||||||
|
{ title: '버전', dataIndex: 'version', width: 60, align: 'center', search: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="지급율 (시스템 → 설계사)" description="직급·보험종·회차별 설계사 지급율">
|
||||||
|
<ProTable<PayoutRuleResp>
|
||||||
|
rowKey="ruleId"
|
||||||
|
columns={columns}
|
||||||
|
request={async (params) => {
|
||||||
|
const list = await ruleApi.payoutRules({ insuranceType: (params as any).insuranceType });
|
||||||
|
return { data: list, total: list.length, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
|
||||||
|
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||||
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
|
dateFormatter="string"
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Tag, Tree } from 'antd';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { ProCard } from '@ant-design/pro-components';
|
||||||
|
import { IconCategory } from '@tabler/icons-react';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import { menuApi, MenuResp } from '@/api/menu';
|
||||||
|
|
||||||
|
interface TreeNode { key: string; title: any; children?: TreeNode[]; }
|
||||||
|
|
||||||
|
function toTree(menus: MenuResp[]): TreeNode[] {
|
||||||
|
return menus.map((m) => ({
|
||||||
|
key: String(m.menuId),
|
||||||
|
title: (
|
||||||
|
<span>
|
||||||
|
<Tag color={m.menuType === 'DIRECTORY' ? 'blue' : 'default'} style={{ marginRight: 6 }}>
|
||||||
|
{m.menuType}
|
||||||
|
</Tag>
|
||||||
|
<strong>{m.menuName}</strong>
|
||||||
|
<span style={{ color: '#999', marginLeft: 8, fontSize: 12 }}>
|
||||||
|
{m.menuPath ?? m.menuCode}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
children: m.children?.length ? toTree(m.children) : undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MenuManage() {
|
||||||
|
const { data: tree = [] } = useQuery({
|
||||||
|
queryKey: ['menu', 'tree'],
|
||||||
|
queryFn: menuApi.tree,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="메뉴 관리" description="시스템 메뉴 트리 / 표시 / 권한 매핑">
|
||||||
|
<ProCard
|
||||||
|
title={<span><IconCategory size={16} style={{ verticalAlign: -3 }} /> 메뉴 트리</span>}
|
||||||
|
headerBordered
|
||||||
|
bordered={false}
|
||||||
|
extra={<Tag color="processing">전체 {countMenus(tree)}개</Tag>}
|
||||||
|
>
|
||||||
|
<Tree
|
||||||
|
treeData={toTree(tree) as any}
|
||||||
|
defaultExpandAll
|
||||||
|
showLine={{ showLeafIcon: false }}
|
||||||
|
style={{ minHeight: 400 }}
|
||||||
|
/>
|
||||||
|
</ProCard>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countMenus(menus: MenuResp[]): number {
|
||||||
|
return menus.reduce((sum, m) => sum + 1 + (m.children ? countMenus(m.children) : 0), 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Tag } from 'antd';
|
||||||
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import { configApi, SystemConfigResp } from '@/api/config';
|
||||||
|
|
||||||
|
const GROUP_COLOR: Record<string, string> = {
|
||||||
|
GENERAL: 'blue', BATCH: 'cyan', SECURITY: 'red', MAIL: 'green', TAX: 'orange',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SystemConfig() {
|
||||||
|
const columns: ProColumns<SystemConfigResp>[] = [
|
||||||
|
{
|
||||||
|
title: '그룹', dataIndex: 'configGroup', width: 110, valueType: 'select',
|
||||||
|
valueEnum: {
|
||||||
|
GENERAL: { text: '일반' }, BATCH: { text: '배치' }, SECURITY: { text: '보안' },
|
||||||
|
MAIL: { text: '메일' }, TAX: { text: '세금' },
|
||||||
|
},
|
||||||
|
render: (_, r) => <Tag color={GROUP_COLOR[r.configGroup ?? ''] ?? 'default'}>
|
||||||
|
{r.configGroup}</Tag>,
|
||||||
|
},
|
||||||
|
{ title: '설정 키', dataIndex: 'configKey', width: 220, search: false,
|
||||||
|
render: (_, r) => <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 4 }}>
|
||||||
|
{r.configKey}</code> },
|
||||||
|
{
|
||||||
|
title: '설정 값', dataIndex: 'configValue', search: false,
|
||||||
|
render: (_, r) => r.isEncrypted === 'Y' ? <Tag color="red">암호화 (***)</Tag> : <span>{r.configValue}</span>,
|
||||||
|
},
|
||||||
|
{ title: '타입', dataIndex: 'valueType', width: 90, search: false },
|
||||||
|
{ title: '설명', dataIndex: 'configDesc', ellipsis: true, search: false },
|
||||||
|
{
|
||||||
|
title: '수정 가능', dataIndex: 'isEditable', width: 100, align: 'center', search: false,
|
||||||
|
render: (_, r) => r.isEditable === 'Y' ? <Tag color="success">가능</Tag> : <Tag>잠김</Tag>,
|
||||||
|
},
|
||||||
|
{ title: '수정일시', dataIndex: 'updatedAt', width: 160, search: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="시스템 설정" description="배치 / 보안 / 세금 / 메일 등 키-값 설정">
|
||||||
|
<ProTable<SystemConfigResp>
|
||||||
|
rowKey="configKey"
|
||||||
|
columns={columns}
|
||||||
|
scroll={{ x: 1200 }}
|
||||||
|
request={async (params) => {
|
||||||
|
const list = await configApi.list((params as any).configGroup);
|
||||||
|
return { data: list, total: list.length, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}건` }}
|
||||||
|
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||||
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
|
dateFormatter="string"
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Tabs, Tag } from 'antd';
|
||||||
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import { logApi, ApiAccessLogResp, DataChangeLogResp, LoginLogResp } from '@/api/log';
|
||||||
|
|
||||||
|
export default function SystemLog() {
|
||||||
|
const [tab, setTab] = useState('login');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="시스템 로그" description="로그인 / API 호출 / 데이터 변경 이력">
|
||||||
|
<Tabs
|
||||||
|
activeKey={tab}
|
||||||
|
onChange={setTab}
|
||||||
|
items={[
|
||||||
|
{ key: 'login', label: '로그인 이력', children: <LoginTable /> },
|
||||||
|
{ key: 'api', label: 'API 호출', children: <ApiTable /> },
|
||||||
|
{ key: 'change', label: '변경 이력', children: <ChangeTable /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoginTable() {
|
||||||
|
const columns: ProColumns<LoginLogResp>[] = [
|
||||||
|
{ title: '로그인일시', dataIndex: 'loginAt', width: 170, valueType: 'dateTime' },
|
||||||
|
{
|
||||||
|
title: '구분', dataIndex: 'loginType', width: 90, valueType: 'select',
|
||||||
|
valueEnum: { LOGIN: { text: '로그인' }, LOGOUT: { text: '로그아웃' }, FAIL: { text: '실패' } },
|
||||||
|
render: (_, r) => {
|
||||||
|
const c = r.loginType === 'LOGIN' ? 'success' : r.loginType === 'FAIL' ? 'error' : 'default';
|
||||||
|
return <Tag color={c}>{r.loginType}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: 'ID', dataIndex: 'username', width: 120 },
|
||||||
|
{ title: 'IP', dataIndex: 'ipAddress', width: 130, search: false },
|
||||||
|
{ title: 'User-Agent', dataIndex: 'userAgent', ellipsis: true, search: false },
|
||||||
|
{ title: '실패사유', dataIndex: 'failReason', width: 200, search: false },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<ProTable<LoginLogResp>
|
||||||
|
rowKey="logId" columns={columns} dateFormatter="string" scroll={{ x: 1100 }}
|
||||||
|
request={async (params) => {
|
||||||
|
const { current, pageSize, ...rest } = params as any;
|
||||||
|
const res = await logApi.login({ ...rest, pageNum: current, pageSize });
|
||||||
|
return { data: res.list, total: res.total, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
|
||||||
|
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApiTable() {
|
||||||
|
const columns: ProColumns<ApiAccessLogResp>[] = [
|
||||||
|
{ title: '시각', dataIndex: 'createdAt', width: 170, valueType: 'dateTime' },
|
||||||
|
{ title: 'Method', dataIndex: 'requestMethod', width: 80, search: false,
|
||||||
|
render: (_, r) => <Tag color="blue">{r.requestMethod}</Tag> },
|
||||||
|
{ title: 'URL', dataIndex: 'requestUrl', ellipsis: true, search: false },
|
||||||
|
{ title: 'Status', dataIndex: 'responseStatus', width: 80, align: 'right', search: false,
|
||||||
|
render: (_, r) => {
|
||||||
|
const s = r.responseStatus ?? 0;
|
||||||
|
const c = s >= 500 ? 'error' : s >= 400 ? 'warning' : s >= 300 ? 'processing' : 'success';
|
||||||
|
return <Tag color={c}>{s}</Tag>;
|
||||||
|
} },
|
||||||
|
{ title: 'Time(ms)', dataIndex: 'responseTime', width: 90, align: 'right', search: false },
|
||||||
|
{ title: 'User', dataIndex: 'username', width: 110 },
|
||||||
|
{ title: 'IP', dataIndex: 'ipAddress', width: 130, search: false },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<ProTable<ApiAccessLogResp>
|
||||||
|
rowKey="logId" columns={columns} dateFormatter="string" scroll={{ x: 1200 }}
|
||||||
|
request={async (params) => {
|
||||||
|
const { current, pageSize, ...rest } = params as any;
|
||||||
|
const res = await logApi.api({ ...rest, pageNum: current, pageSize });
|
||||||
|
return { data: res.list, total: res.total, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
|
||||||
|
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChangeTable() {
|
||||||
|
const columns: ProColumns<DataChangeLogResp>[] = [
|
||||||
|
{ title: '시각', dataIndex: 'createdAt', width: 170, valueType: 'dateTime' },
|
||||||
|
{ title: '테이블', dataIndex: 'tableName', width: 150 },
|
||||||
|
{ title: 'Action', dataIndex: 'actionType', width: 90, search: false,
|
||||||
|
render: (_, r) => {
|
||||||
|
const c = r.actionType === 'CREATE' ? 'success' : r.actionType === 'DELETE' ? 'error' : 'processing';
|
||||||
|
return <Tag color={c}>{r.actionType}</Tag>;
|
||||||
|
} },
|
||||||
|
{ title: 'Record ID', dataIndex: 'recordId', width: 100, search: false },
|
||||||
|
{ title: '메뉴', dataIndex: 'menuCode', width: 130, search: false },
|
||||||
|
{ title: '사용자', dataIndex: 'username', width: 110 },
|
||||||
|
{ title: '사유', dataIndex: 'changeReason', ellipsis: true, search: false },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<ProTable<DataChangeLogResp>
|
||||||
|
rowKey="logId" columns={columns} dateFormatter="string" scroll={{ x: 1100 }}
|
||||||
|
request={async (params) => {
|
||||||
|
const { current, pageSize, ...rest } = params as any;
|
||||||
|
const res = await logApi.change({ ...rest, pageNum: current, pageSize });
|
||||||
|
return { data: res.list, total: res.total, success: true };
|
||||||
|
}}
|
||||||
|
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
|
||||||
|
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user