From 020939e5638cb82f2390b94bbb6cbe5361dd9fbd Mon Sep 17 00:00:00 2001 From: GA Pro Date: Sun, 10 May 2026 22:20:33 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Ant=20Design=20Pro=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=A0=84=EB=A9=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @ant-design/pro-components ^2.8.10 + @ant-design/icons ^6.2 추가. ProLayout (MainLayout): - mix 레이아웃 (사이드바 + 헤더 통합), navTheme=light - 자체 메뉴 트리 렌더링 (children 트리), 동적 메뉴 매핑 - 헤더 actionsRender: 정산월 MonthPicker / 알림 뱃지 - avatarProps: 그라디언트 아바타 + Dropdown 로그아웃 - token: 사이드바/헤더 컬러, pageContainer padding - footerRender: 버전 표기 PageContainer (Pro): - @ant-design/pro-layout 의 PageContainer 사용 - 자동 Breadcrumb / Title / extra 슬롯 ProTable 마이그레이션 (6 페이지): - AgentList: search + valueEnum, request → PageHelper 변환 - ContractList, RecruitLedger, ExceptionLedger, SettleList, PaymentList, UserList - 검색 폼 자동 생성 (valueType: select/dateMonth 등) - toolBarRender: 등록/엑셀 버튼 - options: density/fullScreen/reload/setting Dashboard: - ProCard + StatisticCard 로 KPI 4종 그리드 - 차트 + 배치 진행 + 알림을 ProCard 로 묶음 검증: - tsc -b 통과 - vite build 성공 (2.6MB / gzip 811KB) - 백엔드 76건 단위테스트 영향 없음 Co-Authored-By: Claude Opus 4.7 (1M context) --- ga-frontend/package-lock.json | 711 +++++++++++++++++- ga-frontend/package.json | 2 + .../src/components/common/PageContainer.tsx | 64 +- ga-frontend/src/layouts/MainLayout.tsx | 291 +++---- ga-frontend/src/pages/Dashboard.tsx | 259 +++---- .../src/pages/ledger/ExceptionLedger.tsx | 136 ++-- .../src/pages/ledger/RecruitLedger.tsx | 126 ++-- ga-frontend/src/pages/org/AgentList.tsx | 129 ++-- .../src/pages/product/ContractList.tsx | 104 ++- ga-frontend/src/pages/settle/PaymentList.tsx | 84 +-- ga-frontend/src/pages/settle/SettleList.tsx | 162 ++-- ga-frontend/src/pages/system/UserList.tsx | 116 +-- 12 files changed, 1387 insertions(+), 797 deletions(-) diff --git a/ga-frontend/package-lock.json b/ga-frontend/package-lock.json index 0849fba..c3163bf 100644 --- a/ga-frontend/package-lock.json +++ b/ga-frontend/package-lock.json @@ -12,6 +12,8 @@ "@ag-grid-community/core": "^31.3.2", "@ag-grid-community/react": "^31.3.2", "@ag-grid-community/styles": "^31.3.2", + "@ant-design/icons": "^6.2.2", + "@ant-design/pro-components": "^2.8.10", "@tabler/icons-react": "^3.5.0", "@tanstack/react-query": "^5.40.0", "antd": "^5.18.0", @@ -126,6 +128,69 @@ } }, "node_modules/@ant-design/icons": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.2.tgz", + "integrity": "sha512-zlJtE7AMbG12TeYVPhtBXwNpFInNy8mjLzcIm+0BPw16/b8ODG87YJ1G37VIF5VFscdgfsf6EweAFPTobu/3iQ==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/icons-svg": "^4.4.2", + "@rc-component/util": "^1.10.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/icons/node_modules/@ant-design/colors": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", + "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/icons/node_modules/@ant-design/fast-color": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz", + "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/pro-card": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-card/-/pro-card-2.10.0.tgz", + "integrity": "sha512-sLONn1odmE0Wkbse8pol4WiaEzBV8JU5s3FAMflPpycfUcbSaa1ktXzQ7LCo2SAvOS7gkfmpFjBPtrfbigKh4g==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.4.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-card/node_modules/@ant-design/icons": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", @@ -145,12 +210,409 @@ "react-dom": ">=16.0.0" } }, - "node_modules/@ant-design/icons-svg": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", - "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "node_modules/@ant-design/pro-components": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/@ant-design/pro-components/-/pro-components-2.8.10.tgz", + "integrity": "sha512-QHnnIXdmC5GTAtm6i8eeJy5yT9npPlFyxpDm+duiDrTRKRFaAQBduArxlH3DA/hoRCCypzPONxfK9BQNIhIyZA==", + "license": "MIT", + "dependencies": { + "@ant-design/pro-card": "2.10.0", + "@ant-design/pro-descriptions": "2.6.10", + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-form": "2.32.0", + "@ant-design/pro-layout": "7.22.7", + "@ant-design/pro-list": "2.6.10", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-skeleton": "2.2.1", + "@ant-design/pro-table": "3.21.0", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.16.3" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-descriptions": { + "version": "2.6.10", + "resolved": "https://registry.npmjs.org/@ant-design/pro-descriptions/-/pro-descriptions-2.6.10.tgz", + "integrity": "sha512-+4MbiOfumnWlW0Awm4m8JML5o3lR649FD24AaivCmr8BQvIAAXdTITnDMXEg8BqvdP4KOvNsStZrvYfqoev33A==", + "license": "MIT", + "dependencies": { + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-form": "2.32.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-skeleton": "2.2.1", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "rc-resize-observer": "^0.2.3", + "rc-util": "^5.0.6" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-descriptions/node_modules/rc-resize-observer": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-0.2.6.tgz", + "integrity": "sha512-YX6nYnd6fk7zbuvT6oSDMKiZjyngjHoy+fz+vL3Tez38d/G5iGdaDJa2yE7345G6sc4Mm1IGRUIwclvltddhmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-util": "^5.0.0", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/pro-field": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-field/-/pro-field-3.1.0.tgz", + "integrity": "sha512-+Dgp31WjD+iwg9KIRAMgNkfQivkJKMcYBrIBmho1e8ep/O0HgWSp48g70tBIWi/Lfem/Ky2schF7O8XCFouczw==", + "license": "MIT", + "dependencies": { + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "@chenshuai2144/sketch-color": "^1.0.8", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "rc-util": "^5.4.0", + "swr": "^2.0.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-field/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/pro-form": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-form/-/pro-form-2.32.0.tgz", + "integrity": "sha512-GZnVAMeYv+YHJb17lJ7rX5PYuQPvEA6EotQnPbHi9tGLN3PfexcAd21rqzuO+OrulU2x7TEMDIxtY9MzvvOGbg==", + "license": "MIT", + "dependencies": { + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "@chenshuai2144/sketch-color": "^1.0.7", + "@umijs/use-params": "^1.0.9", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.0.6" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "rc-field-form": ">=1.22.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-form/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/pro-layout": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.7.tgz", + "integrity": "sha512-fvmtNA1r9SaasVIQIQt611VSlNxtVxDbQ3e+1GhYQza3tVJi/3gCZuDyfMfTnbLmf3PaW/YvLkn7MqDbzAzoLA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "@umijs/route-utils": "^4.0.0", + "@umijs/use-params": "^1.0.9", + "classnames": "^2.3.2", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "path-to-regexp": "8.2.0", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.0.6", + "swr": "^2.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-layout/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/pro-list": { + "version": "2.6.10", + "resolved": "https://registry.npmjs.org/@ant-design/pro-list/-/pro-list-2.6.10.tgz", + "integrity": "sha512-xSWwnqCr+hPEYR4qY7nFUaxO5RQBxNlFaPNmobP2i+Im31slk9JuAusgWeIYO0mNhLJuLbxd8CCma2AZij3fBQ==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-card": "2.10.0", + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-table": "3.21.0", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "rc-resize-observer": "^1.0.0", + "rc-util": "^4.19.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-list/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/pro-list/node_modules/@ant-design/icons/node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/pro-list/node_modules/@ant-design/icons/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/@ant-design/pro-list/node_modules/rc-util": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.21.1.tgz", + "integrity": "sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==", + "license": "MIT", + "dependencies": { + "add-dom-event-listener": "^1.1.0", + "prop-types": "^15.5.10", + "react-is": "^16.12.0", + "react-lifecycles-compat": "^3.0.4", + "shallowequal": "^1.1.0" + } + }, + "node_modules/@ant-design/pro-provider": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@ant-design/pro-provider/-/pro-provider-2.16.2.tgz", + "integrity": "sha512-0KmCH1EaOND787Jz6VRMYtLNZmqfT0JPjdUfxhyOxFfnBRfrjyfZgIa6CQoAJLEUMWv57PccWS8wRHVUUk2Yiw==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@babel/runtime": "^7.18.0", + "@ctrl/tinycolor": "^3.4.0", + "dayjs": "^1.11.10", + "rc-util": "^5.0.1", + "swr": "^2.0.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-skeleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/pro-skeleton/-/pro-skeleton-2.2.1.tgz", + "integrity": "sha512-3M2jNOZQZWEDR8pheY00OkHREfb0rquvFZLCa6DypGmiksiuuYuR9Y4iA82ZF+mva2FmpHekdwbje/GpbxqBeg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-table": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-table/-/pro-table-3.21.0.tgz", + "integrity": "sha512-sI81d3FYRv5sXamUc+M5CsHZ9CchuUQgOAPzo5H4oPAVL5h+mkYGRsBzPsxQX7khTNpWjrAtPoRm5ipx3vvWog==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-card": "2.10.0", + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-form": "2.32.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/modifiers": "^6.0.1", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.0.1" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "rc-field-form": ">=1.22.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-table/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/pro-utils": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.18.0.tgz", + "integrity": "sha512-8+ikyrN8L8a8Ph4oeHTOJEiranTj18+9+WHCHjKNdEfukI7Rjn8xpYdLJWb2AUJkb9d4eoAqjd5+k+7w81Df0w==", + "license": "MIT", + "dependencies": { + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-provider": "2.16.2", + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "rc-util": "^5.0.6", + "safe-stable-stringify": "^2.4.3", + "swr": "^2.0.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-utils/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, "node_modules/@ant-design/react-slick": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", @@ -458,6 +920,95 @@ "node": ">=6.9.0" } }, + "node_modules/@chenshuai2144/sketch-color": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@chenshuai2144/sketch-color/-/sketch-color-1.0.9.tgz", + "integrity": "sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w==", + "license": "MIT", + "dependencies": { + "reactcss": "^1.2.3", + "tinycolor2": "^1.4.2" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/modifiers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz", + "integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.1", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.6", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", + "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.7", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", @@ -1058,6 +1609,26 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@rc-component/util": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.10.1.tgz", + "integrity": "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", @@ -1658,6 +2229,21 @@ "@types/react": "^18.0.0" } }, + "node_modules/@umijs/route-utils": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@umijs/route-utils/-/route-utils-4.0.3.tgz", + "integrity": "sha512-zPEcYhl1cSfkSRDzzGgoD1mDvGjxoOTJFvkn55srfgdQ3NZe2ZMCScCU6DEnOxuKP1XDVf8pqyqCDVd2+RCQIw==", + "license": "MIT" + }, + "node_modules/@umijs/use-params": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@umijs/use-params/-/use-params-1.0.9.tgz", + "integrity": "sha512-QlN0RJSBVQBwLRNxbxjQ5qzqYIGn+K7USppMoIOVlf7fxXHsnQZ2bEsa6Pm74bt6DVQxpUE8HqvdStn6Y9FV1w==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1679,6 +2265,15 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/add-dom-event-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz", + "integrity": "sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==", + "license": "MIT", + "dependencies": { + "object-assign": "4.x" + } + }, "node_modules/antd": { "version": "5.29.3", "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz", @@ -1744,6 +2339,26 @@ "react-dom": ">=16.9.0" } }, + "node_modules/antd/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2057,6 +2672,15 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -2364,6 +2988,12 @@ "node": ">=12" } }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2411,6 +3041,12 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2505,6 +3141,15 @@ "node": ">=0.10.0" } }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3204,6 +3849,12 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -3277,6 +3928,15 @@ "react-dom": ">=16.6.0" } }, + "node_modules/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.0.1" + } + }, "node_modules/recharts": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", @@ -3366,6 +4026,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -3394,6 +4063,12 @@ "semver": "bin/semver.js" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3416,6 +4091,19 @@ "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", "license": "MIT" }, + "node_modules/swr": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", + "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/throttle-debounce": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", @@ -3431,6 +4119,12 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, "node_modules/toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", @@ -3579,6 +4273,15 @@ } } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/ga-frontend/package.json b/ga-frontend/package.json index 975674e..a7ebccf 100644 --- a/ga-frontend/package.json +++ b/ga-frontend/package.json @@ -14,6 +14,8 @@ "@ag-grid-community/core": "^31.3.2", "@ag-grid-community/react": "^31.3.2", "@ag-grid-community/styles": "^31.3.2", + "@ant-design/icons": "^6.2.2", + "@ant-design/pro-components": "^2.8.10", "@tabler/icons-react": "^3.5.0", "@tanstack/react-query": "^5.40.0", "antd": "^5.18.0", diff --git a/ga-frontend/src/components/common/PageContainer.tsx b/ga-frontend/src/components/common/PageContainer.tsx index d4dbea6..ade568b 100644 --- a/ga-frontend/src/components/common/PageContainer.tsx +++ b/ga-frontend/src/components/common/PageContainer.tsx @@ -1,7 +1,5 @@ import { ReactNode } from 'react'; -import { Typography } from 'antd'; - -const { Title } = Typography; +import { PageContainer as ProPageContainer } from '@ant-design/pro-components'; interface Props { title: string; @@ -11,52 +9,24 @@ interface Props { } /** - * 페이지 표준 컨테이너. - * - 상단: 제목 + 좌측 컬러 바 + 우측 액션 - * - 본문: 흰 카드 + 그림자 - * 09_UI_DESIGN.md §4 타입 A 의 시각적 보강 버전. + * Ant Design Pro 의 PageContainer 래퍼. + * - 자동 Breadcrumb / Title / Tabs / Footer 슬롯 제공 + * - extra 는 우측 상단 액션 영역 */ export default function PageContainer({ title, description, extra, children }: Props) { return ( -
- {/* 페이지 제목 영역 */} -
-
- - {title} - - {description && ( -
- {description} -
- )} -
- {extra &&
{extra}
} -
- - {/* 본문 카드 */} -
- {children} -
-
+ {description} : undefined + } + > + {children} + ); } diff --git a/ga-frontend/src/layouts/MainLayout.tsx b/ga-frontend/src/layouts/MainLayout.tsx index 1574f9f..a45cc34 100644 --- a/ga-frontend/src/layouts/MainLayout.tsx +++ b/ga-frontend/src/layouts/MainLayout.tsx @@ -1,33 +1,33 @@ import { useState } from 'react'; -import { Layout, Menu, Dropdown, Badge, Space, Avatar, Button, DatePicker, theme } from 'antd'; -import { useNavigate, Outlet, useLocation } from 'react-router-dom'; +import { ProLayout, type MenuDataItem } from '@ant-design/pro-components'; +import { Avatar, Badge, Button, DatePicker, Dropdown, Space } from 'antd'; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; -import { - IconMenu2, IconBell, IconUser, IconLogout, IconChevronDown, -} from '@tabler/icons-react'; +import { IconBell, IconLogout, IconUser, IconChevronDown } from '@tabler/icons-react'; import dayjs from 'dayjs'; -import { menuApi, MenuResp } from '@/api/menu'; +import { menuApi, type MenuResp } from '@/api/menu'; import { authApi } from '@/api/auth'; import { useAuthStore } from '@/stores/auth'; import { getMenuIcon } from '@/components/common/MenuIcon'; -const { Header, Sider, Content, Footer } = Layout; - -function toMenuItems(menus: MenuResp[]): any[] { - return menus.map((m) => ({ - key: m.menuPath ?? m.menuCode, - label: m.menuName, - icon: getMenuIcon(m.menuCode, m.menuPath), - children: m.children?.length ? toMenuItems(m.children) : undefined, - })); +/** API 메뉴 트리 → ProLayout MenuDataItem */ +function toProMenu(menus: MenuResp[]): MenuDataItem[] { + return menus.map((m) => { + const item: MenuDataItem = { + name: m.menuName, + path: m.menuPath ?? `/_grp/${m.menuCode}`, + key: m.menuCode, + icon: getMenuIcon(m.menuCode, m.menuPath), + }; + if (m.children?.length) item.children = toProMenu(m.children); + return item; + }); } export default function MainLayout() { const navigate = useNavigate(); const location = useLocation(); const auth = useAuthStore(); - const { token } = theme.useToken(); - const [collapsed, setCollapsed] = useState(false); const [settleMonth, setSettleMonth] = useState(dayjs()); const { data: menus = [] } = useQuery({ @@ -44,181 +44,100 @@ export default function MainLayout() { }; return ( - - - {/* 로고 영역 */} + -
GA
- {!collapsed && 정산 시스템} + >GA + } + layout="mix" + navTheme="light" + contentWidth="Fluid" + fixedHeader + fixSiderbar + siderWidth={220} + menu={{ locale: false }} + menuDataRender={() => toProMenu(menus)} + location={{ pathname: location.pathname }} + menuItemRender={(item, dom) => ( +
item.path && !item.path.startsWith('/_grp/') && navigate(item.path)}> + {dom}
- - navigate(String(key))} - style={{ borderRight: 0, paddingTop: 8, paddingBottom: 8 }} - /> - - {!collapsed && ( -
routers} + onMenuHeaderClick={() => navigate('/')} + token={{ + // ProLayout 디자인 토큰 — antd pro 컬러 시스템 + sider: { + colorMenuBackground: '#ffffff', + colorTextMenuSelected: '#185FA5', + colorBgMenuItemSelected: '#E6F1FB', + colorTextMenuItemHover: '#1677FF', + colorMenuItemDivider: '#f0f0f0', + }, + header: { + colorBgHeader: '#ffffff', + colorHeaderTitle: '#185FA5', + colorBgRightActionsItemHover: '#f5f7fa', + heightLayoutHeader: 48, + }, + bgLayout: '#f5f7fa', + pageContainer: { + paddingBlockPageContainerContent: 16, + paddingInlinePageContainerContent: 20, + }, + }} + actionsRender={() => [ + + 정산월 + d && setSettleMonth(d)} + allowClear={false} + format="YYYY-MM" + size="small" + style={{ width: 110 }} + /> + , + +
- )} - - - -
- -
- -
- -
-
-
- © GA Commission System · trading_ai · admin@local -
-
- + + {dom} + + + + ), + }} + footerRender={() => ( +
+ © GA Commission System v1.0.0-alpha · trading_ai +
+ )} + > + + ); } - -/** 경로 → 한글 breadcrumb 라벨 */ -function breadcrumbOf(pathname: string): string { - const map: Record = { - '/': '대시보드', - '/org/agents': '조직 / 설계사', - '/contracts': '계약 관리', - '/ledger/recruit': '원장 / 모집수수료', - '/ledger/exception': '원장 / 예외금액', - '/settle': '정산 / 결과', - '/settle/batch': '정산 / 배치 실행', - '/payments': '지급 관리', - '/system/users': '시스템 / 사용자', - '/system/roles': '시스템 / 역할 권한', - '/system/codes': '시스템 / 공통 코드', - }; - return map[pathname] ?? ''; -} diff --git a/ga-frontend/src/pages/Dashboard.tsx b/ga-frontend/src/pages/Dashboard.tsx index 8dbe09e..c643d34 100644 --- a/ga-frontend/src/pages/Dashboard.tsx +++ b/ga-frontend/src/pages/Dashboard.tsx @@ -1,28 +1,27 @@ -import { Card, Col, Row, Typography } from 'antd'; -import { - IconCash, IconCheck, IconClock, IconAlertTriangle, - IconTrendingUp, IconTrendingDown, -} from '@tabler/icons-react'; +import { ProCard, StatisticCard } from '@ant-design/pro-components'; +import { Tag } from 'antd'; import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, Legend, } from 'recharts'; +import { + IconCash, IconCheck, IconClock, IconAlertTriangle, IconTrendingUp, +} from '@tabler/icons-react'; import PageContainer from '@/components/common/PageContainer'; -import MoneyText from '@/components/common/MoneyText'; -const { Text } = Typography; +const { Statistic } = StatisticCard; -const KPI_CARDS = [ - { key: 'recruit', label: '모집수수료', value: 124_500_000, delta: +8.2, - icon: , color: '#1677FF', bg: '#E6F1FB' }, - { key: 'maintain', label: '유지수수료', value: 82_300_000, delta: +3.1, - icon: , color: '#0F6E56', bg: '#E1F5EE' }, - { key: 'pending', label: '확정 대기', value: 12, delta: -2.0, suffix: '건', - icon: , color: '#BA7517', bg: '#FAEEDA' }, - { key: 'alert', label: '대사 차이', value: 3, delta: +50.0, suffix: '건', - icon: , color: '#E24B4A', bg: '#FCEBEB' }, +const KPI = [ + { title: '모집수수료', value: 124_500_000, suffix: '원', precision: 0, + color: '#1677FF', bg: '#E6F1FB', icon: , delta: '+8.2%' }, + { title: '유지수수료', value: 82_300_000, suffix: '원', precision: 0, + color: '#0F6E56', bg: '#E1F5EE', icon: , delta: '+3.1%' }, + { title: '확정 대기', value: 12, suffix: '건', precision: 0, + color: '#BA7517', bg: '#FAEEDA', icon: , delta: '-2건' }, + { title: '대사 차이', value: 3, suffix: '건', precision: 0, + color: '#E24B4A', bg: '#FCEBEB', icon: , delta: '+1건' }, ]; -const MONTHLY_TREND = [ +const MONTHLY = [ { month: '12월', recruit: 95, maintain: 70, chargeback: 5 }, { month: '1월', recruit: 105, maintain: 72, chargeback: 8 }, { month: '2월', recruit: 110, maintain: 75, chargeback: 6 }, @@ -31,7 +30,7 @@ const MONTHLY_TREND = [ { month: '5월', recruit: 125, maintain: 82, chargeback: 4 }, ]; -const BATCH_STEPS = [ +const STEPS = [ { name: '데이터 수신', status: 'done', time: '02:14' }, { name: '데이터 변환', status: 'done', time: '00:52' }, { name: '대사 검증', status: 'done', time: '00:21' }, @@ -46,143 +45,129 @@ export default function Dashboard() { return ( - {/* KPI 카드 4개 */} - - {KPI_CARDS.map((c) => { - const isUp = c.delta >= 0; - return ( - - + {KPI.map((k) => ( + +
+
-
+ {k.icon} +
+
+ + {k.delta} + + } + /> +
+
+ + ))} + + + {/* 차트 + 배치 진행 */} + + + + + + + + + + + + + + + + + + {STEPS.map((s, i) => { + const dot = s.status === 'done' ? '#0F6E56' + : s.status === 'progress' ? '#1677FF' : '#ddd'; + return ( +
+
- {c.icon} -
-
-
{c.label}
-
- {c.suffix === '건' ? c.value.toLocaleString() : ( - - )} - - {c.suffix ?? '원'} - -
-
- {isUp ? : } - {Math.abs(c.delta).toFixed(1)}% 전월 대비 -
-
-
- - - ); - })} - - - {/* 차트 + 배치 현황 */} - - - 월별 추이 (최근 6개월)} - bordered={false} - style={{ boxShadow: 'var(--ga-shadow-card)' }} - styles={{ body: { padding: 16, height: 280 } }} - > - - - - - - - - - - - - - - - - 배치 진행 (5월)} - bordered={false} - style={{ boxShadow: 'var(--ga-shadow-card)' }} - styles={{ body: { padding: 16 } }} - > - {BATCH_STEPS.map((s, i) => { - const dotColor = s.status === 'done' ? '#0F6E56' : - s.status === 'progress' ? '#1677FF' : '#ddd'; - return ( -
-
-
- - Step {i + 1}. {s.name} - -
- {s.time ?? '-'} + }} + /> + + Step {i + 1}. {s.name} +
- ); - })} - - - + {s.time ?? '-'} +
+ ); + })} + + {/* 알림 */} - 최근 알림} - bordered={false} - style={{ boxShadow: 'var(--ga-shadow-card)' }} - styles={{ body: { padding: 0 } }} - > + {[ - { tag: '대사불일치', tagColor: '#E24B4A', tagBg: '#FCEBEB', - text: '삼성생명 5월 통보액 vs 시스템 계산액 3건 차이', time: '10분 전' }, - { tag: '파싱에러', tagColor: '#BA7517', tagBg: '#FAEEDA', + { tag: '대사불일치', color: 'error', + text: '삼성생명 5월 통보액 vs 시스템 계산액 3건 차이', time: '10분 전' }, + { tag: '파싱에러', color: 'warning', text: '한화생명 raw 파일 12행 날짜 형식 오류 (parse_error_log #4521)', time: '1시간 전' }, - { tag: '승인대기', tagColor: '#1677FF', tagBg: '#E6F1FB', + { tag: '승인대기', color: 'processing', text: '예외금액 원장 승인 대기 5건 (정산월 202605)', time: '3시간 전' }, - ].map((n, i) => ( + ].map((n, i, arr) => (
- {n.tag} - {n.text} + {n.tag} + {n.text} {n.time}
))} -
+ ); } diff --git a/ga-frontend/src/pages/ledger/ExceptionLedger.tsx b/ga-frontend/src/pages/ledger/ExceptionLedger.tsx index 2431d2c..473cd58 100644 --- a/ga-frontend/src/pages/ledger/ExceptionLedger.tsx +++ b/ga-frontend/src/pages/ledger/ExceptionLedger.tsx @@ -1,26 +1,16 @@ -import { useState } from 'react'; -import { Table, Space, message, Modal } from 'antd'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useRef } from 'react'; +import { Modal, Space, message } from 'antd'; +import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components'; import PageContainer from '@/components/common/PageContainer'; -import SearchForm from '@/components/common/SearchForm'; import CodeBadge from '@/components/common/CodeBadge'; import MoneyText from '@/components/common/MoneyText'; import PermissionButton from '@/components/common/PermissionButton'; -import { defaultPagination } from '@/utils/tablePagination'; +import { useCommonCodes } from '@/hooks/useCommonCodes'; import { exceptionApi, ExceptionLedgerResp } from '@/api/exceptionLedger'; export default function ExceptionLedger() { - const qc = useQueryClient(); - const [param, setParam] = useState<{ settleMonth?: string; status?: string; pageNum: number; pageSize: number }>({ - pageNum: 1, pageSize: 30, - }); - - const { data, isLoading } = useQuery({ - queryKey: ['ledger', 'exception', param], - queryFn: () => exceptionApi.list(param), - }); - - const refresh = () => qc.invalidateQueries({ queryKey: ['ledger', 'exception'] }); + const actionRef = useRef(); + const { data: approveCodes = [] } = useCommonCodes('APPROVE_STATUS'); const onApprove = (id: number, status: 'APPROVED' | 'REJECTED') => Modal.confirm({ @@ -28,71 +18,67 @@ export default function ExceptionLedger() { onOk: async () => { await exceptionApi.approve(id, status); message.success(status === 'APPROVED' ? '승인 완료' : '반려 완료'); - refresh(); + actionRef.current?.reload(); }, }); - return ( - - setParam({ ...param, ...v, pageNum: 1 })} - onReset={() => setParam({ pageNum: 1, pageSize: 30 })} - /> + const columns: ProColumns[] = [ + { title: '정산월', dataIndex: 'settleMonth', width: 90, valueType: 'dateMonth', fieldProps: { format: 'YYYYMM' } }, + { + title: '승인상태', dataIndex: 'approveStatus', width: 100, valueType: 'select', + valueEnum: Object.fromEntries(approveCodes.map((c) => [c.code, { text: c.codeName }])), + render: (_, r) => , + }, + { title: '설계사', dataIndex: 'agentName', width: 100, search: false }, + { title: '소속', dataIndex: 'orgName', width: 160, search: false }, + { title: '예외코드', dataIndex: 'exceptionName', width: 120, search: false }, + { + title: '구분', dataIndex: 'direction', width: 80, search: false, + render: (_, r) => , + }, + { + title: '금액', dataIndex: 'amount', width: 130, align: 'right', search: false, + render: (_, r) => , + }, + { title: '내용', dataIndex: 'description', ellipsis: true, search: false }, + { + title: '재반복', dataIndex: 'recurringLeft', width: 90, align: 'center', search: false, + render: (_, r) => r.recurringYn === 'Y' ? `${r.recurringLeft}회 남음` : '-', + }, + { title: '승인자', dataIndex: 'approverName', width: 100, search: false }, + { title: '승인일시', dataIndex: 'approveDate', width: 160, search: false }, + { + title: '액션', dataIndex: 'ledgerId', width: 180, fixed: 'right', search: false, + render: (_, r) => r.approveStatus === 'PENDING' ? ( + + onApprove(r.ledgerId, 'APPROVED')}>승인 + onApprove(r.ledgerId, 'REJECTED')}>반려 + + ) : null, + }, + ]; - + return ( + + + actionRef={actionRef} rowKey="ledgerId" - loading={isLoading} - dataSource={data?.list} - size="small" - scroll={{ x: 1400 }} - pagination={{ - ...defaultPagination, - current: param.pageNum, pageSize: param.pageSize, total: data?.total, - onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }), + columns={columns} + scroll={{ x: 1500 }} + request={async (params) => { + const { current, pageSize, ...rest } = params as any; + const res = await exceptionApi.list({ ...rest, pageNum: current, pageSize }); + return { data: res.list, total: res.total, success: true }; }} - columns={[ - { title: '정산월', dataIndex: 'settleMonth', width: 90 }, - { title: '설계사', dataIndex: 'agentName', width: 100 }, - { title: '소속', dataIndex: 'orgName', width: 160 }, - { title: '예외코드', dataIndex: 'exceptionName', width: 120 }, - { - title: '구분', dataIndex: 'direction', width: 80, - render: (v) => , - }, - { - title: '금액', dataIndex: 'amount', width: 130, align: 'right', - render: (v, r) => , - }, - { title: '내용', dataIndex: 'description', ellipsis: true }, - { title: '재반복', dataIndex: 'recurringLeft', width: 90, align: 'center', - render: (v, r) => r.recurringYn === 'Y' ? `${v}회 남음` : '-' }, - { - title: '승인상태', dataIndex: 'approveStatus', width: 100, - render: (v) => , - }, - { title: '승인자', dataIndex: 'approverName', width: 100 }, - { title: '승인일시', dataIndex: 'approveDate', width: 160 }, - { - title: '액션', dataIndex: 'ledgerId', width: 180, fixed: 'right', - render: (id, r) => - r.approveStatus === 'PENDING' ? ( - - onApprove(id, 'APPROVED')}> - 승인 - - onApprove(id, 'REJECTED')}> - 반려 - - - ) : null, - }, - ]} + pagination={{ + pageSize: 30, showSizeChanger: true, pageSizeOptions: ['20', '50', '100'], + showTotal: (total) => `총 ${total.toLocaleString()}건`, + }} + search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }} + options={{ density: false, fullScreen: true, reload: true, setting: true }} + dateFormatter="string" /> ); diff --git a/ga-frontend/src/pages/ledger/RecruitLedger.tsx b/ga-frontend/src/pages/ledger/RecruitLedger.tsx index d59cdc6..cc59f05 100644 --- a/ga-frontend/src/pages/ledger/RecruitLedger.tsx +++ b/ga-frontend/src/pages/ledger/RecruitLedger.tsx @@ -1,75 +1,83 @@ -import { useState } from 'react'; -import { Table } from 'antd'; -import { useQuery } from '@tanstack/react-query'; +import { ProTable, type ProColumns } from '@ant-design/pro-components'; import PageContainer from '@/components/common/PageContainer'; -import SearchForm from '@/components/common/SearchForm'; -import MoneyText from '@/components/common/MoneyText'; import CodeBadge from '@/components/common/CodeBadge'; +import MoneyText from '@/components/common/MoneyText'; import ExcelExportButton from '@/components/common/ExcelExportButton'; -import { defaultPagination } from '@/utils/tablePagination'; +import { useCommonCodes } from '@/hooks/useCommonCodes'; import { ledgerApi, LedgerResp, LedgerSearchParam } from '@/api/ledger'; export default function RecruitLedger() { - const [param, setParam] = useState({ pageNum: 1, pageSize: 50 }); + const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE'); + const { data: reconCodes = [] } = useCommonCodes('RECON_STATUS'); - const { data, isLoading } = useQuery({ - queryKey: ['ledger', 'recruit', param], - queryFn: () => ledgerApi.recruit(param), - }); + const columns: ProColumns[] = [ + { + 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) => , + }, + { + title: '대사상태', dataIndex: 'reconcileStatus', width: 100, valueType: 'select', + valueEnum: Object.fromEntries(reconCodes.map((c) => [c.code, { text: c.codeName }])), + render: (_, r) => , + }, + { 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) => , + }, + { + title: '회사율', dataIndex: 'companyRate', width: 80, align: 'right', search: false, + render: (_, r) => , + }, + { + title: '회사수수료', dataIndex: 'companyAmount', width: 120, align: 'right', search: false, + render: (_, r) => , + }, + { + title: '지급율', dataIndex: 'payoutRate', width: 80, align: 'right', search: false, + render: (_, r) => , + }, + { + title: '설계사수수료', dataIndex: 'agentAmount', width: 120, align: 'right', search: false, + render: (_, r) => , + }, + { + title: '세액', dataIndex: 'taxAmount', width: 100, align: 'right', search: false, + render: (_, r) => , + }, + ]; return ( - - } - > - setParam({ ...param, ...v, pageNum: 1 })} - onReset={() => setParam({ pageNum: 1, pageSize: 50 })} - /> - - + + rowKey="ledgerId" - loading={isLoading} - dataSource={data?.list} - size="small" + columns={columns} scroll={{ x: 1500 }} - pagination={{ - ...defaultPagination, - current: param.pageNum, pageSize: param.pageSize, total: data?.total, - onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }), + request={async (params) => { + const { current, pageSize, ...rest } = params as any; + const res = await ledgerApi.recruit({ ...rest, pageNum: current, pageSize }); + return { data: res.list, total: res.total, success: true }; }} - columns={[ - { title: '정산월', dataIndex: 'settleMonth', width: 90, fixed: 'left' }, - { title: '증권번호', dataIndex: 'policyNo', width: 130, fixed: 'left' }, - { title: '설계사', dataIndex: 'agentName', width: 100 }, - { title: '소속', dataIndex: 'orgName', width: 160 }, - { title: '보험사', dataIndex: 'companyName', width: 120 }, - { title: '상품', dataIndex: 'productName', width: 200 }, - { title: '보험종류', dataIndex: 'insuranceType', width: 100, - render: (v) => }, - { title: '보험료', dataIndex: 'premium', width: 110, align: 'right', - render: (v) => }, - { title: '회사율', dataIndex: 'companyRate', width: 80, align: 'right', - render: (v) => }, - { title: '회사수수료', dataIndex: 'companyAmount', width: 120, align: 'right', - render: (v) => }, - { title: '지급율', dataIndex: 'payoutRate', width: 80, align: 'right', - render: (v) => }, - { title: '설계사수수료', dataIndex: 'agentAmount', width: 120, align: 'right', - render: (v) => }, - { title: '세액', dataIndex: 'taxAmount', width: 100, align: 'right', - render: (v) => }, - { title: '대사상태', dataIndex: 'reconcileStatus', width: 100, - render: (v) => }, + pagination={{ + pageSize: 50, showSizeChanger: true, pageSizeOptions: ['20', '50', '100', '200'], + showTotal: (total) => `총 ${total.toLocaleString()}건`, + }} + search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }} + options={{ density: false, fullScreen: true, reload: true, setting: true }} + toolBarRender={() => [ + , ]} + dateFormatter="string" /> ); diff --git a/ga-frontend/src/pages/org/AgentList.tsx b/ga-frontend/src/pages/org/AgentList.tsx index 03862f4..1066f86 100644 --- a/ga-frontend/src/pages/org/AgentList.tsx +++ b/ga-frontend/src/pages/org/AgentList.tsx @@ -1,79 +1,92 @@ -import { useState } from 'react'; -import { Table } from 'antd'; -import { useQuery } from '@tanstack/react-query'; +import { useRef } from 'react'; +import { Space } from 'antd'; +import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components'; import { useNavigate } from 'react-router-dom'; -import PageContainer from '@/components/common/PageContainer'; -import SearchForm from '@/components/common/SearchForm'; import CodeBadge from '@/components/common/CodeBadge'; import MoneyText from '@/components/common/MoneyText'; import PermissionButton from '@/components/common/PermissionButton'; import ExcelExportButton from '@/components/common/ExcelExportButton'; -import { defaultPagination } from '@/utils/tablePagination'; +import PageContainer from '@/components/common/PageContainer'; +import { useCommonCodes } from '@/hooks/useCommonCodes'; import { agentApi, AgentResp, AgentSearchParam } from '@/api/agent'; export default function AgentList() { const navigate = useNavigate(); - const [param, setParam] = useState({ pageNum: 1, pageSize: 20 }); + const actionRef = useRef(); + const { data: statusCodes = [] } = useCommonCodes('AGENT_STATUS'); - const { data, isLoading } = useQuery({ - queryKey: ['agent', 'list', param], - queryFn: () => agentApi.list(param), - }); + const columns: ProColumns[] = [ + { + title: '설계사명', dataIndex: 'agentName', width: 120, fixed: 'left', + render: (_, r) => ( + navigate(`/org/agents/${r.agentId}`)} style={{ fontWeight: 500 }}> + {r.agentName} + + ), + }, + { title: '사번', dataIndex: 'licenseNo', width: 120, search: false }, + { title: '소속', dataIndex: 'orgName', width: 200, search: false }, + { title: '직급', dataIndex: 'gradeName', width: 100, search: false }, + { title: '전화번호', dataIndex: 'phone', width: 140, search: false }, + { + title: '상태', dataIndex: 'status', width: 100, valueType: 'select', + valueEnum: Object.fromEntries(statusCodes.map((c) => [c.code, { text: c.codeName }])), + render: (_, r) => , + }, + { + title: '계약수', dataIndex: 'contractCount', width: 90, align: 'right', search: false, + render: (_, r) => , + }, + { + title: '누적수수료', dataIndex: 'totalCommission', width: 130, align: 'right', search: false, + render: (_, r) => , + }, + { title: '입사일', dataIndex: 'joinDate', width: 110, search: false }, + { + title: '검색어', dataIndex: 'searchKeyword', hideInTable: true, + fieldProps: { placeholder: '설계사명' }, + }, + ]; return ( - navigate('/org/agents/new')} style={{ marginRight: 8 }}> - 등록 - - -
- } + description="조직별 설계사 등록·관리, 누적 수수료 추적" > - setParam({ ...param, ...v, pageNum: 1 })} - onReset={() => setParam({ pageNum: 1, pageSize: 20 })} - /> - - + + actionRef={actionRef} rowKey="agentId" - loading={isLoading} - dataSource={data?.list} - pagination={{ - ...defaultPagination, - current: param.pageNum, - pageSize: param.pageSize, - total: data?.total, - onChange: (page, size) => setParam({ ...param, pageNum: page, pageSize: size }), + columns={columns} + scroll={{ x: 1200 }} + request={async (params) => { + const { current, pageSize, ...rest } = params as any; + const res = await agentApi.list({ ...rest, pageNum: current, pageSize }); + return { data: res.list, total: res.total, success: true }; }} - onRow={(r) => ({ onClick: () => navigate(`/org/agents/${r.agentId}`) })} - columns={[ - { title: '설계사명', dataIndex: 'agentName', width: 120 }, - { title: '사번', dataIndex: 'licenseNo', width: 120 }, - { title: '소속', dataIndex: 'orgName', width: 200 }, - { title: '직급', dataIndex: 'gradeName', width: 100 }, - { title: '전화번호', dataIndex: 'phone', width: 140 }, - { - title: '상태', dataIndex: 'status', width: 100, - render: (v) => , - }, - { - title: '계약수', dataIndex: 'contractCount', align: 'right', width: 90, - render: (v) => , - }, - { - title: '누적수수료', dataIndex: 'totalCommission', align: 'right', width: 130, - render: (v) => , - }, - { title: '입사일', dataIndex: 'joinDate', width: 110 }, + pagination={{ + pageSize: 20, + showSizeChanger: true, + pageSizeOptions: ['20', '50', '100'], + showTotal: (total) => `총 ${total.toLocaleString()}건`, + }} + search={{ + labelWidth: 'auto', + searchText: '검색', + resetText: '초기화', + collapsed: false, + collapseRender: false, + }} + options={{ density: false, fullScreen: true, reload: true, setting: true }} + toolBarRender={() => [ + , + navigate('/org/agents/new')} + >+ 설계사 등록, ]} + dateFormatter="string" + headerTitle={설계사 목록} /> ); diff --git a/ga-frontend/src/pages/product/ContractList.tsx b/ga-frontend/src/pages/product/ContractList.tsx index de6e3d0..b34d347 100644 --- a/ga-frontend/src/pages/product/ContractList.tsx +++ b/ga-frontend/src/pages/product/ContractList.tsx @@ -1,67 +1,65 @@ -import { useState } from 'react'; -import { Table } from 'antd'; -import { useQuery } from '@tanstack/react-query'; +import { ProTable, type ProColumns } from '@ant-design/pro-components'; import PageContainer from '@/components/common/PageContainer'; -import SearchForm from '@/components/common/SearchForm'; import CodeBadge from '@/components/common/CodeBadge'; import MoneyText from '@/components/common/MoneyText'; -import { defaultPagination } from '@/utils/tablePagination'; +import { useCommonCodes } from '@/hooks/useCommonCodes'; import { contractApi, ContractResp, ContractSearchParam } from '@/api/contract'; export default function ContractList() { - const [param, setParam] = useState({ pageNum: 1, pageSize: 20 }); + const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE'); + const { data: statusCodes = [] } = useCommonCodes('CONTRACT_STATUS'); - const { data, isLoading } = useQuery({ - queryKey: ['contract', 'list', param], - queryFn: () => contractApi.list(param), - }); + const columns: ProColumns[] = [ + { + title: '검색어', dataIndex: 'searchKeyword', hideInTable: true, + fieldProps: { placeholder: '계약자/증권번호' }, + }, + { + title: '보험종류', dataIndex: 'insuranceType', width: 100, valueType: 'select', + valueEnum: Object.fromEntries(insuranceCodes.map((c) => [c.code, { text: c.codeName }])), + render: (_, r) => , + }, + { + title: '상태', dataIndex: 'status', width: 90, valueType: 'select', + valueEnum: Object.fromEntries(statusCodes.map((c) => [c.code, { text: c.codeName }])), + render: (_, r) => , + }, + { title: '증권번호', dataIndex: 'policyNo', width: 140, search: false }, + { title: '계약자', dataIndex: 'contractorName', width: 100, search: false }, + { title: '설계사', dataIndex: 'agentName', width: 100, search: false }, + { title: '보험사', dataIndex: 'companyName', width: 120, search: false }, + { title: '상품', dataIndex: 'productName', width: 220, search: false }, + { + title: '보험료', dataIndex: 'premium', width: 120, align: 'right', search: false, + render: (_, r) => , + }, + { + title: '납입주기', dataIndex: 'payCycle', width: 90, search: false, + render: (_, r) => , + }, + { title: '계약일', dataIndex: 'contractDate', width: 110, search: false }, + ]; return ( - - setParam({ ...param, ...v, pageNum: 1 })} - onReset={() => setParam({ pageNum: 1, pageSize: 20 })} - /> - - + + rowKey="contractId" - loading={isLoading} - dataSource={data?.list} - pagination={{ - ...defaultPagination, - current: param.pageNum, pageSize: param.pageSize, total: data?.total, - onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }), + columns={columns} + scroll={{ x: 1300 }} + request={async (params) => { + const { current, pageSize, ...rest } = params as any; + const res = await contractApi.list({ ...rest, pageNum: current, pageSize }); + return { data: res.list, total: res.total, success: true }; }} - columns={[ - { title: '증권번호', dataIndex: 'policyNo', width: 140 }, - { title: '계약자', dataIndex: 'contractorName', width: 100 }, - { title: '설계사', dataIndex: 'agentName', width: 100 }, - { title: '보험사', dataIndex: 'companyName', width: 120 }, - { title: '상품', dataIndex: 'productName', width: 220 }, - { - title: '보험종류', dataIndex: 'insuranceType', width: 100, - render: (v) => , - }, - { - title: '보험료', dataIndex: 'premium', width: 120, align: 'right', - render: (v) => , - }, - { - title: '납입주기', dataIndex: 'payCycle', width: 90, - render: (v) => , - }, - { - title: '상태', dataIndex: 'status', width: 90, - render: (v) => , - }, - { title: '계약일', dataIndex: 'contractDate', width: 110 }, - ]} + pagination={{ + pageSize: 20, + showSizeChanger: true, + pageSizeOptions: ['20', '50', '100'], + showTotal: (total) => `총 ${total.toLocaleString()}건`, + }} + search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }} + options={{ density: false, fullScreen: true, reload: true, setting: true }} + dateFormatter="string" /> ); diff --git a/ga-frontend/src/pages/settle/PaymentList.tsx b/ga-frontend/src/pages/settle/PaymentList.tsx index 2ad2ae9..60f5713 100644 --- a/ga-frontend/src/pages/settle/PaymentList.tsx +++ b/ga-frontend/src/pages/settle/PaymentList.tsx @@ -1,60 +1,52 @@ -import { useState } from 'react'; -import { Table } from 'antd'; -import { useQuery } from '@tanstack/react-query'; +import { ProTable, type ProColumns } from '@ant-design/pro-components'; import PageContainer from '@/components/common/PageContainer'; -import SearchForm from '@/components/common/SearchForm'; import CodeBadge from '@/components/common/CodeBadge'; import MoneyText from '@/components/common/MoneyText'; -import { defaultPagination } from '@/utils/tablePagination'; +import { useCommonCodes } from '@/hooks/useCommonCodes'; import { paymentApi, PaymentResp } from '@/api/payment'; export default function PaymentList() { - const [param, setParam] = useState<{ settleMonth?: string; status?: string; pageNum: number; pageSize: number }>({ - pageNum: 1, pageSize: 30, - }); + const { data: statusCodes = [] } = useCommonCodes('PAYMENT_STATUS'); - const { data, isLoading } = useQuery({ - queryKey: ['payment', 'list', param], - queryFn: () => paymentApi.list(param), - }); + const columns: ProColumns[] = [ + { + title: '정산월', dataIndex: 'settleMonth', width: 90, + valueType: 'dateMonth', fieldProps: { format: 'YYYYMM' }, + }, + { + title: '지급상태', dataIndex: 'payStatus', width: 100, valueType: 'select', + valueEnum: Object.fromEntries(statusCodes.map((c) => [c.code, { text: c.codeName }])), + render: (_, r) => , + }, + { title: '설계사', dataIndex: 'agentName', width: 100, search: false }, + { title: '은행', dataIndex: 'bankName', width: 100, search: false }, + { title: '계좌', dataIndex: 'accountNo', width: 180, search: false }, + { + title: '지급액', dataIndex: 'payAmount', width: 140, align: 'right', search: false, + render: (_, r) => , + }, + { title: '지급일', dataIndex: 'payDate', width: 110, search: false }, + { title: '실패사유', dataIndex: 'failReason', ellipsis: true, search: false }, + ]; return ( - - setParam({ ...param, ...v, pageNum: 1 })} - onReset={() => setParam({ pageNum: 1, pageSize: 30 })} - /> - - + + rowKey="paymentId" - loading={isLoading} - dataSource={data?.list} - size="small" - pagination={{ - ...defaultPagination, - current: param.pageNum, pageSize: param.pageSize, total: data?.total, - onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }), + columns={columns} + scroll={{ x: 1100 }} + request={async (params) => { + const { current, pageSize, ...rest } = params as any; + const res = await paymentApi.list({ ...rest, pageNum: current, pageSize }); + return { data: res.list, total: res.total, success: true }; }} - columns={[ - { title: '정산월', dataIndex: 'settleMonth', width: 90 }, - { title: '설계사', dataIndex: 'agentName', width: 100 }, - { title: '은행', dataIndex: 'bankName', width: 100 }, - { title: '계좌', dataIndex: 'accountNo', width: 180 }, - { - title: '지급액', dataIndex: 'payAmount', width: 140, align: 'right', - render: (v) => , - }, - { title: '지급일', dataIndex: 'payDate', width: 110 }, - { - title: '상태', dataIndex: 'payStatus', width: 100, - render: (v) => , - }, - { title: '실패사유', dataIndex: 'failReason', ellipsis: true }, - ]} + pagination={{ + pageSize: 30, showSizeChanger: true, pageSizeOptions: ['20', '50', '100'], + showTotal: (total) => `총 ${total.toLocaleString()}건`, + }} + search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }} + options={{ density: false, fullScreen: true, reload: true, setting: true }} + dateFormatter="string" /> ); diff --git a/ga-frontend/src/pages/settle/SettleList.tsx b/ga-frontend/src/pages/settle/SettleList.tsx index 3639293..3993252 100644 --- a/ga-frontend/src/pages/settle/SettleList.tsx +++ b/ga-frontend/src/pages/settle/SettleList.tsx @@ -1,30 +1,32 @@ -import { useState } from 'react'; -import { Card, Col, Row, Statistic, Table, message, Modal, Input, Space } from 'antd'; +import { useRef, useState } from 'react'; +import { Input, Modal, Space, message } from 'antd'; +import { ProCard, ProTable, StatisticCard, type ActionType, type ProColumns } from '@ant-design/pro-components'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import PageContainer from '@/components/common/PageContainer'; -import SearchForm from '@/components/common/SearchForm'; import CodeBadge from '@/components/common/CodeBadge'; import MoneyText from '@/components/common/MoneyText'; import PermissionButton from '@/components/common/PermissionButton'; -import { defaultPagination } from '@/utils/tablePagination'; +import { useCommonCodes } from '@/hooks/useCommonCodes'; import { settleApi, SettleResp, SettleSearchParam } from '@/api/settle'; +const { Statistic } = StatisticCard; + export default function SettleList() { const qc = useQueryClient(); - const [param, setParam] = useState({ pageNum: 1, pageSize: 30 }); - - const { data, isLoading } = useQuery({ - queryKey: ['settle', 'list', param], - queryFn: () => settleApi.list(param), - }); + const actionRef = useRef(); + const [settleMonth, setSettleMonth] = useState(undefined); + const { data: statusCodes = [] } = useCommonCodes('SETTLE_STATUS'); const { data: summary } = useQuery({ - queryKey: ['settle', 'summary', param.settleMonth], - queryFn: () => settleApi.summary(param.settleMonth!), - enabled: !!param.settleMonth, + queryKey: ['settle', 'summary', settleMonth], + queryFn: () => settleApi.summary(settleMonth!), + enabled: !!settleMonth, }); - const refresh = () => qc.invalidateQueries({ queryKey: ['settle'] }); + const refresh = () => { + qc.invalidateQueries({ queryKey: ['settle'] }); + actionRef.current?.reload(); + }; const onConfirm = (id: number) => Modal.confirm({ @@ -41,73 +43,83 @@ export default function SettleList() { }); }; - return ( - - setParam({ ...param, ...v, pageNum: 1 })} - onReset={() => setParam({ pageNum: 1, pageSize: 30 })} - /> + const columns: ProColumns[] = [ + { title: '정산월', dataIndex: 'settleMonth', width: 90, fixed: 'left', + valueType: 'dateMonth', fieldProps: { format: 'YYYYMM' } }, + { + title: '상태', dataIndex: 'status', width: 100, valueType: 'select', fixed: 'right', + valueEnum: Object.fromEntries(statusCodes.map((c) => [c.code, { text: c.codeName }])), + render: (_, r) => , + }, + { title: '설계사', dataIndex: 'agentName', width: 100, fixed: 'left', search: false }, + { title: '소속', dataIndex: 'orgName', width: 160, search: false }, + { title: '직급', dataIndex: 'gradeName', width: 80, search: false }, + { title: '모집', dataIndex: 'recruitTotal', width: 110, align: 'right', search: false, render: (_, r) => }, + { title: '유지', dataIndex: 'maintainTotal', width: 110, align: 'right', search: false, render: (_, r) => }, + { title: '예외(+)', dataIndex: 'exceptionPlus', width: 100, align: 'right', search: false, render: (_, r) => }, + { title: '예외(-)', dataIndex: 'exceptionMinus', width: 100, align: 'right', search: false, render: (_, r) => }, + { title: '오버라이드', dataIndex: 'overrideTotal', width: 110, align: 'right', search: false, render: (_, r) => }, + { + title: '환수', dataIndex: 'chargebackTotal', width: 100, align: 'right', search: false, + render: (_, r) => , + }, + { title: '총액', dataIndex: 'grossAmount', width: 120, align: 'right', search: false, render: (_, r) => }, + { title: '세액', dataIndex: 'taxAmount', width: 100, align: 'right', search: false, render: (_, r) => }, + { + title: '실지급', dataIndex: 'netAmount', width: 130, align: 'right', search: false, + render: (_, r) => , + }, + { + title: '액션', dataIndex: 'settleId', width: 200, fixed: 'right', search: false, + render: (_, r) => ( + + onConfirm(r.settleId)} + disabled={r.status === 'CONFIRMED' || r.status === 'PAID'}>확정 + onHold(r.settleId)} disabled={r.status === 'PAID'}>보류 + + ), + }, + ]; + return ( + {summary && ( - - - - - - + + + + + + + + + + + + + + )} - + + actionRef={actionRef} rowKey="settleId" - loading={isLoading} - dataSource={data?.list} - size="small" - scroll={{ x: 1600 }} - pagination={{ - ...defaultPagination, - current: param.pageNum, pageSize: param.pageSize, total: data?.total, - onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }), + columns={columns} + scroll={{ x: 1700 }} + request={async (params) => { + const { current, pageSize, settleMonth: sm, ...rest } = params as any; + setSettleMonth(sm); + const res = await settleApi.list({ ...rest, settleMonth: sm, pageNum: current, pageSize }); + return { data: res.list, total: res.total, success: true }; }} - columns={[ - { title: '정산월', dataIndex: 'settleMonth', width: 90, fixed: 'left' }, - { title: '설계사', dataIndex: 'agentName', width: 100, fixed: 'left' }, - { title: '소속', dataIndex: 'orgName', width: 160 }, - { title: '직급', dataIndex: 'gradeName', width: 80 }, - { title: '모집', dataIndex: 'recruitTotal', width: 110, align: 'right', render: (v) => }, - { title: '유지', dataIndex: 'maintainTotal', width: 110, align: 'right', render: (v) => }, - { title: '예외(+)', dataIndex: 'exceptionPlus', width: 100, align: 'right', render: (v) => }, - { title: '예외(-)', dataIndex: 'exceptionMinus', width: 100, align: 'right', render: (v) => }, - { title: '오버라이드', dataIndex: 'overrideTotal', width: 110, align: 'right', render: (v) => }, - { title: '환수', dataIndex: 'chargebackTotal', width: 100, align: 'right', - render: (v) => }, - { title: '총액', dataIndex: 'grossAmount', width: 120, align: 'right', render: (v) => }, - { title: '세액', dataIndex: 'taxAmount', width: 100, align: 'right', render: (v) => }, - { title: '실지급', dataIndex: 'netAmount', width: 130, align: 'right', - render: (v) => }, - { title: '상태', dataIndex: 'status', width: 100, fixed: 'right', - render: (v) => }, - { - title: '액션', dataIndex: 'settleId', width: 200, fixed: 'right', - render: (id, r) => ( - - onConfirm(id)} - disabled={r.status === 'CONFIRMED' || r.status === 'PAID'}> - 확정 - - onHold(id)} - disabled={r.status === 'PAID'}> - 보류 - - - ), - }, - ]} + pagination={{ + pageSize: 30, showSizeChanger: true, pageSizeOptions: ['20', '50', '100'], + showTotal: (total) => `총 ${total.toLocaleString()}건`, + }} + search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }} + options={{ density: false, fullScreen: true, reload: true, setting: true }} + dateFormatter="string" /> ); diff --git a/ga-frontend/src/pages/system/UserList.tsx b/ga-frontend/src/pages/system/UserList.tsx index 5cd692b..b2869a4 100644 --- a/ga-frontend/src/pages/system/UserList.tsx +++ b/ga-frontend/src/pages/system/UserList.tsx @@ -1,25 +1,22 @@ -import { useState } from 'react'; -import { Table, Space, message, Modal } from 'antd'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useRef } from 'react'; +import { Modal, Space, message } from 'antd'; +import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components'; +import { useQueryClient } from '@tanstack/react-query'; import PageContainer from '@/components/common/PageContainer'; -import SearchForm from '@/components/common/SearchForm'; import CodeBadge from '@/components/common/CodeBadge'; import PermissionButton from '@/components/common/PermissionButton'; -import { defaultPagination } from '@/utils/tablePagination'; +import { useCommonCodes } from '@/hooks/useCommonCodes'; import { userApi, UserResp } from '@/api/user'; export default function UserList() { const qc = useQueryClient(); - const [param, setParam] = useState<{ searchKeyword?: string; status?: string; pageNum: number; pageSize: number }>({ - pageNum: 1, pageSize: 30, - }); + const actionRef = useRef(); + const { data: statusCodes = [] } = useCommonCodes('AGENT_STATUS'); - const { data, isLoading } = useQuery({ - queryKey: ['user', 'list', param], - queryFn: () => userApi.list(param), - }); - - const refresh = () => qc.invalidateQueries({ queryKey: ['user'] }); + const refresh = () => { + qc.invalidateQueries({ queryKey: ['user'] }); + actionRef.current?.reload(); + }; const onReset = (id: number) => Modal.confirm({ @@ -34,52 +31,57 @@ export default function UserList() { onOk: async () => { await userApi.unlock(id); message.success('해제 완료'); refresh(); }, }); - return ( - - setParam({ ...param, ...v, pageNum: 1 })} - onReset={() => setParam({ pageNum: 1, pageSize: 30 })} - /> + const columns: ProColumns[] = [ + { + title: '검색어', dataIndex: 'searchKeyword', hideInTable: true, + fieldProps: { placeholder: '아이디 / 이름' }, + }, + { + title: '상태', dataIndex: 'status', width: 100, valueType: 'select', + valueEnum: Object.fromEntries(statusCodes.map((c) => [c.code, { text: c.codeName }])), + render: (_, r) => , + }, + { title: '로그인 ID', dataIndex: 'loginId', width: 120, search: false }, + { title: '이름', dataIndex: 'userName', width: 120, search: false }, + { title: '이메일', dataIndex: 'email', width: 200, search: false }, + { title: '소속', dataIndex: 'orgName', width: 160, search: false }, + { title: '연결 설계사', dataIndex: 'agentName', width: 100, search: false }, + { title: '실패횟수', dataIndex: 'failCount', width: 90, align: 'right', search: false }, + { title: '마지막 로그인', dataIndex: 'lastLoginAt', width: 160, search: false }, + { + title: '액션', width: 220, fixed: 'right', search: false, + render: (_, r) => ( + + onReset(r.userId)}>비번초기화 + {r.status === 'LOCKED' && ( + onUnlock(r.userId)}>잠금해제 + )} + + ), + }, + ]; - + return ( + + + actionRef={actionRef} rowKey="userId" - loading={isLoading} - dataSource={data?.list} - pagination={{ - ...defaultPagination, - current: param.pageNum, pageSize: param.pageSize, total: data?.total, - onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }), + columns={columns} + scroll={{ x: 1300 }} + request={async (params) => { + const { current, pageSize, ...rest } = params as any; + const res = await userApi.list({ ...rest, pageNum: current, pageSize }); + return { data: res.list, total: res.total, success: true }; }} - columns={[ - { title: '로그인 ID', dataIndex: 'loginId', width: 120 }, - { title: '이름', dataIndex: 'userName', width: 120 }, - { title: '이메일', dataIndex: 'email', width: 200 }, - { title: '소속', dataIndex: 'orgName', width: 160 }, - { title: '연결 설계사', dataIndex: 'agentName', width: 100 }, - { - title: '상태', dataIndex: 'status', width: 100, - render: (v) => , - }, - { title: '실패횟수', dataIndex: 'failCount', width: 90, align: 'right' }, - { title: '마지막 로그인', dataIndex: 'lastLoginAt', width: 160 }, - { - title: '액션', width: 220, fixed: 'right', - render: (_, r) => ( - - onReset(r.userId)}>비번초기화 - {r.status === 'LOCKED' && ( - onUnlock(r.userId)}>잠금해제 - )} - - ), - }, - ]} + pagination={{ + pageSize: 30, showSizeChanger: true, pageSizeOptions: ['20', '50', '100'], + showTotal: (total) => `총 ${total.toLocaleString()}건`, + }} + search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }} + options={{ density: false, fullScreen: true, reload: true, setting: true }} + dateFormatter="string" /> );