From 958fe6b9802bd256a17a2ff5e5b00bf46a2f0637 Mon Sep 17 00:00:00 2001 From: GA Pro Date: Thu, 14 May 2026 01:21:27 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEVELOPER_GUIDE.md | 505 +++++++++++++++++ DOMAIN_GAP_P4.md | 145 +++++ HANDOFF.md | 120 ++-- README.md | 126 ++++- .../api/controller/approval/AdvanceReq.java | 15 + .../approval/ApprovalController.java | 115 ++++ .../attachment/AttachmentController.java | 85 +++ .../CollectionCommissionController.java | 82 +++ .../CommissionMasterController.java | 66 +++ .../commission/OverrideLedgerController.java | 46 ++ .../PersistencyBonusController.java | 79 +++ .../ReinstatementCommissionController.java | 78 +++ .../RenewalCommissionController.java | 78 +++ .../complaint/ComplaintController.java | 70 +++ .../contract/AgentContractController.java | 70 +++ .../ContractEndorsementController.java | 59 ++ .../ga/api/controller/kpi/KpiController.java | 50 ++ .../license/AgentLicenseController.java | 68 +++ .../controller/notice/NoticeController.java | 97 ++++ .../CarrierReconciliationOutController.java | 50 ++ .../settle/CommissionDisputeController.java | 74 +++ .../settle/IncentivePaymentController.java | 60 ++ .../settle/IncentiveProgramController.java | 60 ++ .../settle/PeriodCloseController.java | 53 ++ .../controller/settle/SettleController.java | 2 +- .../settle/SettleCorrectionController.java | 67 +++ .../settle/TaxInvoiceController.java | 52 ++ .../TerminationSettlementController.java | 65 +++ .../training/AgentTrainingController.java | 91 +++ .../withdraw/WithdrawRequestController.java | 50 ++ .../api/service/approval/ApprovalService.java | 259 +++++++++ .../service/attachment/AttachmentService.java | 111 ++++ .../CollectionCommissionService.java | 96 ++++ .../commission/CommissionMasterService.java | 54 ++ .../commission/OverrideLedgerService.java | 43 ++ .../commission/PersistencyBonusService.java | 100 ++++ .../ReinstatementCommissionService.java | 108 ++++ .../commission/RenewalCommissionService.java | 97 ++++ .../service/complaint/ComplaintService.java | 86 +++ .../contract/AgentContractService.java | 79 +++ .../endorse/ContractEndorsementService.java | 62 ++ .../com/ga/api/service/kpi/KpiService.java | 40 ++ .../service/license/AgentLicenseService.java | 71 +++ .../ga/api/service/notice/NoticeService.java | 97 ++++ .../CarrierReconciliationOutService.java | 67 +++ .../settle/CommissionDisputeService.java | 83 +++ .../settle/IncentivePaymentService.java | 73 +++ .../settle/IncentiveProgramService.java | 84 +++ .../service/settle/PeriodCloseService.java | 63 +++ .../settle/SettleCorrectionService.java | 81 +++ .../ga/api/service/settle/SettleService.java | 4 +- .../api/service/settle/TaxInvoiceService.java | 70 +++ .../settle/TerminationSettlementService.java | 101 ++++ .../training/AgentTrainingService.java | 110 ++++ .../withdraw/WithdrawRequestService.java | 106 ++++ ga-api/src/main/resources/application.yml | 6 + .../domain/BatchChargebackCalculator.java | 49 ++ .../domain/BatchInstallmentPlanGenerator.java | 51 ++ .../domain/BatchRegulatoryLimitChecker.java | 83 +++ .../settlement/domain/BatchTaxCalculator.java | 45 ++ .../settlement/domain/ChargebackResult.java | 11 + .../batch/settlement/domain/TaxBreakdown.java | 11 + .../settlement/domain/ViolationResult.java | 21 + .../batch/settlement/step/AggregateStep.java | 139 ++++- .../settlement/step/CalcRecruitStep.java | 43 +- .../step/ChargebackExceptionStep.java | 41 +- .../db/migration/V29__마감마스터.sql | 43 ++ .../main/resources/db/migration/V30__정정.sql | 62 ++ .../resources/db/migration/V31__이의신청.sql | 54 ++ .../db/migration/V32__보류사유코드.sql | 29 + .../db/migration/V33__시책인센티브.sql | 94 +++ .../resources/db/migration/V34__해촉정산.sql | 69 +++ .../db/migration/V35__세금계산서.sql | 65 +++ .../migration/V36__보험사회신인터페이스.sql | 54 ++ .../resources/db/migration/V37__KPI_MV.sql | 257 +++++++++ .../db/migration/V38__리포트_메뉴.sql | 53 ++ .../resources/db/migration/V39__KPI_메뉴.sql | 58 ++ .../V40__수수료종류코드_통합마스터.sql | 59 ++ .../db/migration/V41__수금수수료.sql | 66 +++ .../db/migration/V42__갱신_부활수수료.sql | 122 ++++ .../db/migration/V43__유지보수수당.sql | 74 +++ .../db/migration/V44__오버라이드원장.sql | 77 +++ .../resources/db/migration/V45__위촉계약.sql | 38 ++ .../resources/db/migration/V46__자격관리.sql | 35 ++ .../resources/db/migration/V47__보수교육.sql | 60 ++ .../resources/db/migration/V48__계약변경.sql | 40 ++ .../resources/db/migration/V49__민원관리.sql | 49 ++ .../db/migration/V50__결재워크플로우.sql | 132 +++++ .../resources/db/migration/V51__공지_알림.sql | 63 +++ .../resources/db/migration/V52__첨부파일.sql | 36 ++ .../resources/db/migration/V53__출금신청.sql | 45 ++ .../resources/db/migration/V54__P4메뉴.sql | 198 +++++++ .../db/migration/V55__P4_공통코드.sql | 128 +++++ .../db/migration/V56__agent_세무컬럼_보강.sql | 27 + .../approval/ApprovalHistoryMapper.java | 16 + .../mapper/approval/ApprovalLineMapper.java | 19 + .../approval/ApprovalLineStepMapper.java | 18 + .../approval/ApprovalRequestMapper.java | 42 ++ .../mapper/attachment/AttachmentMapper.java | 21 + .../CollectionCommissionLedgerMapper.java | 31 + .../CollectionCommissionRuleMapper.java | 35 ++ .../commission/CommissionMasterMapper.java | 45 ++ .../commission/OverrideLedgerMapper.java | 21 + .../commission/OverrideSummaryMapper.java | 29 + .../commission/PersistencyBonusMapper.java | 33 ++ .../PersistencyBonusRuleMapper.java | 22 + .../ReinstatementCommissionLedgerMapper.java | 21 + .../ReinstatementCommissionRuleMapper.java | 21 + .../RenewalCommissionLedgerMapper.java | 21 + .../RenewalCommissionRuleMapper.java | 21 + .../mapper/complaint/ComplaintMapper.java | 22 + .../mapper/contract/AgentContractMapper.java | 20 + .../endorse/ContractEndorsementMapper.java | 20 + .../installment/InstallmentPlanMapper.java | 3 + .../com/ga/core/mapper/kpi/KpiMapper.java | 43 ++ .../mapper/license/AgentLicenseMapper.java | 34 ++ .../ga/core/mapper/notice/NoticeMapper.java | 21 + .../mapper/notice/UserNotificationMapper.java | 38 ++ .../CarrierReconciliationOutMapper.java | 48 ++ .../settle/CommissionDisputeMapper.java | 42 ++ .../mapper/settle/IncentivePaymentMapper.java | 43 ++ .../mapper/settle/IncentiveProgramMapper.java | 43 ++ .../core/mapper/settle/PeriodCloseMapper.java | 41 ++ .../mapper/settle/SettleCorrectionMapper.java | 42 ++ .../mapper/settle/SettleMasterMapper.java | 3 +- .../core/mapper/settle/TaxInvoiceMapper.java | 43 ++ .../settle/TerminationSettlementMapper.java | 44 ++ .../mapper/training/AgentTrainingMapper.java | 34 ++ .../training/TrainingRequirementMapper.java | 17 + .../withdraw/WithdrawRequestMapper.java | 23 + .../ga/core/vo/approval/ApprovalAction.java | 27 + .../core/vo/approval/ApprovalHistoryResp.java | 18 + .../vo/approval/ApprovalHistorySaveReq.java | 18 + .../approval/ApprovalHistorySearchParam.java | 12 + .../core/vo/approval/ApprovalHistoryVO.java | 21 + .../ga/core/vo/approval/ApprovalLineResp.java | 17 + .../core/vo/approval/ApprovalLineSaveReq.java | 21 + .../vo/approval/ApprovalLineSearchParam.java | 12 + .../vo/approval/ApprovalLineStepResp.java | 12 + .../vo/approval/ApprovalLineStepSaveReq.java | 17 + .../approval/ApprovalLineStepSearchParam.java | 11 + .../core/vo/approval/ApprovalLineStepVO.java | 17 + .../ga/core/vo/approval/ApprovalLineVO.java | 22 + .../core/vo/approval/ApprovalRequestResp.java | 25 + .../vo/approval/ApprovalRequestSaveReq.java | 17 + .../approval/ApprovalRequestSearchParam.java | 12 + .../core/vo/approval/ApprovalRequestVO.java | 26 + .../ga/core/vo/approval/ApprovalStatus.java | 28 + .../core/vo/attachment/AttachmentRefType.java | 33 ++ .../ga/core/vo/attachment/AttachmentResp.java | 19 + .../core/vo/attachment/AttachmentSaveReq.java | 24 + .../vo/attachment/AttachmentSearchParam.java | 13 + .../ga/core/vo/attachment/AttachmentVO.java | 26 + .../com/ga/core/vo/commission/BonusType.java | 26 + .../CollectionCommissionLedgerResp.java | 28 + .../CollectionCommissionLedgerSaveReq.java | 34 ++ ...CollectionCommissionLedgerSearchParam.java | 14 + .../CollectionCommissionLedgerVO.java | 28 + .../CollectionCommissionRuleResp.java | 21 + .../CollectionCommissionRuleSaveReq.java | 32 ++ .../CollectionCommissionRuleSearchParam.java | 12 + .../CollectionCommissionRuleVO.java | 25 + .../vo/commission/CommissionMasterResp.java | 28 + .../commission/CommissionMasterSaveReq.java | 42 ++ .../CommissionMasterSearchParam.java | 17 + .../vo/commission/CommissionMasterVO.java | 32 ++ .../ga/core/vo/commission/CommissionType.java | 34 ++ .../vo/commission/OverrideLedgerResp.java | 25 + .../vo/commission/OverrideLedgerSaveReq.java | 30 + .../commission/OverrideLedgerSearchParam.java | 15 + .../core/vo/commission/OverrideLedgerVO.java | 32 ++ .../vo/commission/OverrideSummaryResp.java | 21 + .../vo/commission/OverrideSummarySaveReq.java | 21 + .../OverrideSummarySearchParam.java | 12 + .../core/vo/commission/OverrideSummaryVO.java | 24 + .../ga/core/vo/commission/PaymentCycle.java | 28 + .../vo/commission/PersistencyBonusResp.java | 21 + .../commission/PersistencyBonusRuleResp.java | 20 + .../PersistencyBonusRuleSaveReq.java | 34 ++ .../PersistencyBonusRuleSearchParam.java | 13 + .../vo/commission/PersistencyBonusRuleVO.java | 30 + .../commission/PersistencyBonusSaveReq.java | 28 + .../PersistencyBonusSearchParam.java | 13 + .../vo/commission/PersistencyBonusVO.java | 30 + .../ReinstatementCommissionLedgerResp.java | 25 + .../ReinstatementCommissionLedgerSaveReq.java | 32 ++ ...nstatementCommissionLedgerSearchParam.java | 13 + .../ReinstatementCommissionLedgerVO.java | 30 + .../ReinstatementCommissionRuleResp.java | 19 + .../ReinstatementCommissionRuleSaveReq.java | 27 + ...einstatementCommissionRuleSearchParam.java | 12 + .../ReinstatementCommissionRuleVO.java | 26 + .../RenewalCommissionLedgerResp.java | 23 + .../RenewalCommissionLedgerSaveReq.java | 29 + .../RenewalCommissionLedgerSearchParam.java | 13 + .../commission/RenewalCommissionLedgerVO.java | 28 + .../commission/RenewalCommissionRuleResp.java | 18 + .../RenewalCommissionRuleSaveReq.java | 25 + .../RenewalCommissionRuleSearchParam.java | 12 + .../commission/RenewalCommissionRuleVO.java | 25 + .../ga/core/vo/commission/RetentionMonth.java | 32 ++ .../core/vo/complaint/ComplaintChannel.java | 29 + .../ga/core/vo/complaint/ComplaintResp.java | 30 + .../core/vo/complaint/ComplaintSaveReq.java | 36 ++ .../vo/complaint/ComplaintSearchParam.java | 14 + .../core/vo/complaint/ComplaintSeverity.java | 27 + .../ga/core/vo/complaint/ComplaintStatus.java | 28 + .../com/ga/core/vo/complaint/ComplaintVO.java | 34 ++ .../core/vo/contract/AgentContractResp.java | 26 + .../vo/contract/AgentContractSaveReq.java | 27 + .../vo/contract/AgentContractSearchParam.java | 12 + .../core/vo/contract/AgentContractStatus.java | 27 + .../ga/core/vo/contract/AgentContractVO.java | 30 + .../vo/endorse/ContractEndorsementResp.java | 23 + .../endorse/ContractEndorsementSaveReq.java | 20 + .../ContractEndorsementSearchParam.java | 15 + .../vo/endorse/ContractEndorsementVO.java | 30 + .../ga/core/vo/endorse/EndorsementType.java | 29 + .../com/ga/core/vo/kpi/KpiCumulativeResp.java | 29 + .../ga/core/vo/kpi/KpiMonthlySummaryResp.java | 25 + .../com/ga/core/vo/kpi/KpiOrgSummaryResp.java | 26 + .../com/ga/core/vo/kpi/KpiRetentionResp.java | 24 + .../com/ga/core/vo/kpi/KpiSearchParam.java | 20 + .../ga/core/vo/license/AgentLicenseResp.java | 23 + .../core/vo/license/AgentLicenseSaveReq.java | 22 + .../vo/license/AgentLicenseSearchParam.java | 12 + .../ga/core/vo/license/AgentLicenseVO.java | 28 + .../com/ga/core/vo/license/LicenseStatus.java | 27 + .../com/ga/core/vo/license/LicenseType.java | 27 + .../com/ga/core/vo/notice/NoticeResp.java | 19 + .../com/ga/core/vo/notice/NoticeSaveReq.java | 20 + .../ga/core/vo/notice/NoticeSearchParam.java | 12 + .../com/ga/core/vo/notice/NoticeType.java | 27 + .../java/com/ga/core/vo/notice/NoticeVO.java | 25 + .../core/vo/notice/NotificationRefType.java | 31 + .../core/vo/notice/UserNotificationResp.java | 18 + .../vo/notice/UserNotificationSaveReq.java | 16 + .../notice/UserNotificationSearchParam.java | 13 + .../ga/core/vo/notice/UserNotificationVO.java | 22 + .../settle/CarrierReconciliationOutResp.java | 33 ++ .../CarrierReconciliationOutSaveReq.java | 31 + .../CarrierReconciliationOutSearchParam.java | 16 + .../vo/settle/CarrierReconciliationOutVO.java | 30 + .../com/ga/core/vo/settle/CloseStatus.java | 14 + .../core/vo/settle/CommissionDisputeResp.java | 37 ++ .../vo/settle/CommissionDisputeSaveReq.java | 37 ++ .../settle/CommissionDisputeSearchParam.java | 17 + .../core/vo/settle/CommissionDisputeVO.java | 32 ++ .../ga/core/vo/settle/CorrectionStatus.java | 16 + .../com/ga/core/vo/settle/DisputeStatus.java | 16 + .../ga/core/vo/settle/IncentivePayStatus.java | 16 + .../ga/core/vo/settle/IncentivePayType.java | 14 + .../core/vo/settle/IncentivePaymentResp.java | 34 ++ .../vo/settle/IncentivePaymentSaveReq.java | 31 + .../settle/IncentivePaymentSearchParam.java | 18 + .../ga/core/vo/settle/IncentivePaymentVO.java | 28 + .../core/vo/settle/IncentiveProgramResp.java | 36 ++ .../vo/settle/IncentiveProgramSaveReq.java | 49 ++ .../settle/IncentiveProgramSearchParam.java | 18 + .../ga/core/vo/settle/IncentiveProgramVO.java | 33 ++ .../ga/core/vo/settle/PeriodCloseResp.java | 26 + .../ga/core/vo/settle/PeriodCloseSaveReq.java | 25 + .../vo/settle/PeriodCloseSearchParam.java | 17 + .../com/ga/core/vo/settle/PeriodCloseVO.java | 23 + .../com/ga/core/vo/settle/ReconStatus.java | 16 + .../core/vo/settle/SettleCorrectionResp.java | 39 ++ .../vo/settle/SettleCorrectionSaveReq.java | 39 ++ .../settle/SettleCorrectionSearchParam.java | 18 + .../ga/core/vo/settle/SettleCorrectionVO.java | 34 ++ .../ga/core/vo/settle/SettleMasterResp.java | 2 + .../com/ga/core/vo/settle/SettleMasterVO.java | 1 + .../com/ga/core/vo/settle/TaxInvoiceResp.java | 42 ++ .../ga/core/vo/settle/TaxInvoiceSaveReq.java | 57 ++ .../core/vo/settle/TaxInvoiceSearchParam.java | 22 + .../com/ga/core/vo/settle/TaxInvoiceType.java | 14 + .../com/ga/core/vo/settle/TaxInvoiceVO.java | 42 ++ .../ga/core/vo/settle/TermSettleStatus.java | 16 + .../vo/settle/TerminationSettlementResp.java | 45 ++ .../settle/TerminationSettlementSaveReq.java | 41 ++ .../TerminationSettlementSearchParam.java | 17 + .../vo/settle/TerminationSettlementVO.java | 47 ++ .../core/vo/training/AgentTrainingResp.java | 21 + .../vo/training/AgentTrainingSaveReq.java | 26 + .../vo/training/AgentTrainingSearchParam.java | 13 + .../ga/core/vo/training/AgentTrainingVO.java | 26 + .../vo/training/TrainingRequirementResp.java | 11 + .../training/TrainingRequirementSaveReq.java | 15 + .../TrainingRequirementSearchParam.java | 11 + .../vo/training/TrainingRequirementVO.java | 21 + .../core/vo/withdraw/WithdrawRequestResp.java | 27 + .../vo/withdraw/WithdrawRequestSaveReq.java | 25 + .../withdraw/WithdrawRequestSearchParam.java | 12 + .../core/vo/withdraw/WithdrawRequestVO.java | 31 + .../ga/core/vo/withdraw/WithdrawStatus.java | 30 + .../mapper/approval/ApprovalHistoryMapper.xml | 45 ++ .../mapper/approval/ApprovalLineMapper.xml | 60 ++ .../approval/ApprovalLineStepMapper.xml | 43 ++ .../mapper/approval/ApprovalRequestMapper.xml | 95 ++++ .../mapper/attachment/AttachmentMapper.xml | 58 ++ .../CollectionCommissionLedgerMapper.xml | 78 +++ .../CollectionCommissionRuleMapper.xml | 68 +++ .../commission/CommissionMasterMapper.xml | 144 +++++ .../commission/OverrideLedgerMapper.xml | 76 +++ .../commission/OverrideSummaryMapper.xml | 76 +++ .../commission/PersistencyBonusMapper.xml | 79 +++ .../commission/PersistencyBonusRuleMapper.xml | 80 +++ .../ReinstatementCommissionLedgerMapper.xml | 73 +++ .../ReinstatementCommissionRuleMapper.xml | 70 +++ .../RenewalCommissionLedgerMapper.xml | 71 +++ .../RenewalCommissionRuleMapper.xml | 66 +++ .../mapper/complaint/ComplaintMapper.xml | 97 ++++ .../mapper/contract/AgentContractMapper.xml | 81 +++ .../endorse/ContractEndorsementMapper.xml | 75 +++ .../installment/InstallmentPlanMapper.xml | 5 + .../main/resources/mapper/kpi/KpiMapper.xml | 148 +++++ .../mapper/license/AgentLicenseMapper.xml | 86 +++ .../resources/mapper/notice/NoticeMapper.xml | 66 +++ .../mapper/notice/UserNotificationMapper.xml | 62 ++ .../mapper/org/OrganizationMapper.xml | 2 +- .../settle/CarrierReconciliationOutMapper.xml | 138 +++++ .../mapper/settle/CommissionDisputeMapper.xml | 118 ++++ .../mapper/settle/IncentivePaymentMapper.xml | 124 ++++ .../mapper/settle/IncentiveProgramMapper.xml | 145 +++++ .../mapper/settle/PeriodCloseMapper.xml | 106 ++++ .../mapper/settle/SettleCorrectionMapper.xml | 132 +++++ .../mapper/settle/SettleMasterMapper.xml | 9 +- .../mapper/settle/TaxInvoiceMapper.xml | 131 +++++ .../settle/TerminationSettlementMapper.xml | 159 ++++++ .../mapper/training/AgentTrainingMapper.xml | 70 +++ .../training/TrainingRequirementMapper.xml | 38 ++ .../mapper/withdraw/WithdrawRequestMapper.xml | 143 +++++ ga-frontend/src/App.tsx | 69 +++ ga-frontend/src/api/agent-contract.ts | 50 ++ ga-frontend/src/api/approval.ts | 101 ++++ ga-frontend/src/api/attachment.ts | 39 ++ ga-frontend/src/api/commission.ts | 230 ++++++++ ga-frontend/src/api/complaint.ts | 53 ++ ga-frontend/src/api/endorsement.ts | 53 ++ ga-frontend/src/api/kpi.ts | 102 ++++ ga-frontend/src/api/license.ts | 49 ++ ga-frontend/src/api/notice.ts | 48 ++ ga-frontend/src/api/notification.ts | 27 + ga-frontend/src/api/training.ts | 70 +++ ga-frontend/src/api/withdraw.ts | 45 ++ .../src/components/biz/ApprovalProgress.tsx | 59 ++ .../src/components/biz/AttachmentUpload.tsx | 122 ++++ .../src/components/biz/NotificationBell.tsx | 116 ++++ .../src/components/common/DataGrid.tsx | 54 +- .../src/components/common/ErrorBoundary.tsx | 48 ++ .../src/components/common/PageContainer.tsx | 27 +- .../src/components/common/SearchForm.tsx | 137 ++++- ga-frontend/src/index.css | 338 ++++++++++- ga-frontend/src/layouts/MainLayout.tsx | 177 ++++-- ga-frontend/src/main.tsx | 186 ++++-- ga-frontend/src/pages/Dashboard.tsx | 534 ++++++++++++++---- ga-frontend/src/pages/LoginPage.tsx | 137 ++++- .../src/pages/agent/AgentContracts.tsx | 173 ++++++ ga-frontend/src/pages/agent/AgentLicenses.tsx | 148 +++++ .../src/pages/agent/AgentTrainings.tsx | 205 +++++++ .../pages/commission/CollectionCommission.tsx | 199 +++++++ .../src/pages/commission/CommissionMaster.tsx | 80 +++ .../src/pages/commission/OverrideLedger.tsx | 104 ++++ .../src/pages/commission/PersistencyBonus.tsx | 241 ++++++++ .../commission/ReinstatementCommission.tsx | 184 ++++++ .../pages/commission/RenewalCommission.tsx | 184 ++++++ .../src/pages/contracts/Complaints.tsx | 197 +++++++ .../pages/contracts/ContractEndorsements.tsx | 160 ++++++ ga-frontend/src/pages/kpi/KpiCumulative.tsx | 249 ++++++++ .../src/pages/kpi/KpiMonthlySummary.tsx | 318 +++++++++++ ga-frontend/src/pages/kpi/KpiOrgSummary.tsx | 199 +++++++ ga-frontend/src/pages/kpi/KpiRetention.tsx | 237 ++++++++ .../src/pages/payments/WithdrawRequest.tsx | 142 +++++ .../src/pages/report/ChargebackRiskReport.tsx | 103 ++++ ga-frontend/src/pages/report/OrgReport.tsx | 98 ++++ .../src/pages/report/PerformanceReport.tsx | 96 ++++ ga-frontend/src/pages/system/Approvals.tsx | 261 +++++++++ ga-frontend/src/pages/system/Notices.tsx | 154 +++++ ga-frontend/src/styles/variables.css | 92 ++- ga-frontend/src/theme/tokens.ts | 129 +++++ 379 files changed, 21786 insertions(+), 423 deletions(-) create mode 100644 DEVELOPER_GUIDE.md create mode 100644 DOMAIN_GAP_P4.md create mode 100644 ga-api/src/main/java/com/ga/api/controller/approval/AdvanceReq.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/approval/ApprovalController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/attachment/AttachmentController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/commission/CollectionCommissionController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/commission/CommissionMasterController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/commission/OverrideLedgerController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/commission/PersistencyBonusController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/commission/ReinstatementCommissionController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/commission/RenewalCommissionController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/complaint/ComplaintController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/contract/AgentContractController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/endorse/ContractEndorsementController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/kpi/KpiController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/license/AgentLicenseController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/notice/NoticeController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/settle/CarrierReconciliationOutController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/settle/CommissionDisputeController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/settle/IncentivePaymentController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/settle/IncentiveProgramController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/settle/PeriodCloseController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/settle/SettleCorrectionController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/settle/TaxInvoiceController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/settle/TerminationSettlementController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/training/AgentTrainingController.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/withdraw/WithdrawRequestController.java create mode 100644 ga-api/src/main/java/com/ga/api/service/approval/ApprovalService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/attachment/AttachmentService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/commission/CollectionCommissionService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/commission/CommissionMasterService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/commission/OverrideLedgerService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/commission/PersistencyBonusService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/commission/ReinstatementCommissionService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/commission/RenewalCommissionService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/complaint/ComplaintService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/contract/AgentContractService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/endorse/ContractEndorsementService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/kpi/KpiService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/license/AgentLicenseService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/notice/NoticeService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/settle/CarrierReconciliationOutService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/settle/CommissionDisputeService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/settle/IncentivePaymentService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/settle/IncentiveProgramService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/settle/PeriodCloseService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/settle/SettleCorrectionService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/settle/TaxInvoiceService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/settle/TerminationSettlementService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/training/AgentTrainingService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/withdraw/WithdrawRequestService.java create mode 100644 ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchChargebackCalculator.java create mode 100644 ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchInstallmentPlanGenerator.java create mode 100644 ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchRegulatoryLimitChecker.java create mode 100644 ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchTaxCalculator.java create mode 100644 ga-batch/src/main/java/com/ga/batch/settlement/domain/ChargebackResult.java create mode 100644 ga-batch/src/main/java/com/ga/batch/settlement/domain/TaxBreakdown.java create mode 100644 ga-batch/src/main/java/com/ga/batch/settlement/domain/ViolationResult.java create mode 100644 ga-common/src/main/resources/db/migration/V29__마감마스터.sql create mode 100644 ga-common/src/main/resources/db/migration/V30__정정.sql create mode 100644 ga-common/src/main/resources/db/migration/V31__이의신청.sql create mode 100644 ga-common/src/main/resources/db/migration/V32__보류사유코드.sql create mode 100644 ga-common/src/main/resources/db/migration/V33__시책인센티브.sql create mode 100644 ga-common/src/main/resources/db/migration/V34__해촉정산.sql create mode 100644 ga-common/src/main/resources/db/migration/V35__세금계산서.sql create mode 100644 ga-common/src/main/resources/db/migration/V36__보험사회신인터페이스.sql create mode 100644 ga-common/src/main/resources/db/migration/V37__KPI_MV.sql create mode 100644 ga-common/src/main/resources/db/migration/V38__리포트_메뉴.sql create mode 100644 ga-common/src/main/resources/db/migration/V39__KPI_메뉴.sql create mode 100644 ga-common/src/main/resources/db/migration/V40__수수료종류코드_통합마스터.sql create mode 100644 ga-common/src/main/resources/db/migration/V41__수금수수료.sql create mode 100644 ga-common/src/main/resources/db/migration/V42__갱신_부활수수료.sql create mode 100644 ga-common/src/main/resources/db/migration/V43__유지보수수당.sql create mode 100644 ga-common/src/main/resources/db/migration/V44__오버라이드원장.sql create mode 100644 ga-common/src/main/resources/db/migration/V45__위촉계약.sql create mode 100644 ga-common/src/main/resources/db/migration/V46__자격관리.sql create mode 100644 ga-common/src/main/resources/db/migration/V47__보수교육.sql create mode 100644 ga-common/src/main/resources/db/migration/V48__계약변경.sql create mode 100644 ga-common/src/main/resources/db/migration/V49__민원관리.sql create mode 100644 ga-common/src/main/resources/db/migration/V50__결재워크플로우.sql create mode 100644 ga-common/src/main/resources/db/migration/V51__공지_알림.sql create mode 100644 ga-common/src/main/resources/db/migration/V52__첨부파일.sql create mode 100644 ga-common/src/main/resources/db/migration/V53__출금신청.sql create mode 100644 ga-common/src/main/resources/db/migration/V54__P4메뉴.sql create mode 100644 ga-common/src/main/resources/db/migration/V55__P4_공통코드.sql create mode 100644 ga-common/src/main/resources/db/migration/V56__agent_세무컬럼_보강.sql create mode 100644 ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalHistoryMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalLineMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalLineStepMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalRequestMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/attachment/AttachmentMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/commission/CollectionCommissionLedgerMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/commission/CollectionCommissionRuleMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/commission/CommissionMasterMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/commission/OverrideLedgerMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/commission/OverrideSummaryMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/commission/PersistencyBonusMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/commission/PersistencyBonusRuleMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/commission/ReinstatementCommissionLedgerMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/commission/ReinstatementCommissionRuleMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/commission/RenewalCommissionLedgerMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/commission/RenewalCommissionRuleMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/complaint/ComplaintMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/contract/AgentContractMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/endorse/ContractEndorsementMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/kpi/KpiMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/license/AgentLicenseMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/notice/NoticeMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/notice/UserNotificationMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/settle/CarrierReconciliationOutMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/settle/CommissionDisputeMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/settle/IncentivePaymentMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/settle/IncentiveProgramMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/settle/PeriodCloseMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/settle/SettleCorrectionMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/settle/TaxInvoiceMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/settle/TerminationSettlementMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/training/AgentTrainingMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/training/TrainingRequirementMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/mapper/withdraw/WithdrawRequestMapper.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalAction.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistoryResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistorySaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistorySearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistoryVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/approval/ApprovalStatus.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentRefType.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/BonusType.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/CommissionType.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummaryResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummarySaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummarySearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummaryVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/PaymentCycle.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/commission/RetentionMonth.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintChannel.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintSeverity.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintStatus.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/contract/AgentContractResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/contract/AgentContractSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/contract/AgentContractSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/contract/AgentContractStatus.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/contract/AgentContractVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/endorse/EndorsementType.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/kpi/KpiCumulativeResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/kpi/KpiMonthlySummaryResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/kpi/KpiOrgSummaryResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/kpi/KpiRetentionResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/kpi/KpiSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/license/LicenseStatus.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/license/LicenseType.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/notice/NoticeResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/notice/NoticeSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/notice/NoticeSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/notice/NoticeType.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/notice/NoticeVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/notice/NotificationRefType.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/CloseStatus.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/CorrectionStatus.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/DisputeStatus.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/IncentivePayStatus.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/IncentivePayType.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/ReconStatus.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceType.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/TermSettleStatus.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestResp.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestSaveReq.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestSearchParam.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestVO.java create mode 100644 ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawStatus.java create mode 100644 ga-core/src/main/resources/mapper/approval/ApprovalHistoryMapper.xml create mode 100644 ga-core/src/main/resources/mapper/approval/ApprovalLineMapper.xml create mode 100644 ga-core/src/main/resources/mapper/approval/ApprovalLineStepMapper.xml create mode 100644 ga-core/src/main/resources/mapper/approval/ApprovalRequestMapper.xml create mode 100644 ga-core/src/main/resources/mapper/attachment/AttachmentMapper.xml create mode 100644 ga-core/src/main/resources/mapper/commission/CollectionCommissionLedgerMapper.xml create mode 100644 ga-core/src/main/resources/mapper/commission/CollectionCommissionRuleMapper.xml create mode 100644 ga-core/src/main/resources/mapper/commission/CommissionMasterMapper.xml create mode 100644 ga-core/src/main/resources/mapper/commission/OverrideLedgerMapper.xml create mode 100644 ga-core/src/main/resources/mapper/commission/OverrideSummaryMapper.xml create mode 100644 ga-core/src/main/resources/mapper/commission/PersistencyBonusMapper.xml create mode 100644 ga-core/src/main/resources/mapper/commission/PersistencyBonusRuleMapper.xml create mode 100644 ga-core/src/main/resources/mapper/commission/ReinstatementCommissionLedgerMapper.xml create mode 100644 ga-core/src/main/resources/mapper/commission/ReinstatementCommissionRuleMapper.xml create mode 100644 ga-core/src/main/resources/mapper/commission/RenewalCommissionLedgerMapper.xml create mode 100644 ga-core/src/main/resources/mapper/commission/RenewalCommissionRuleMapper.xml create mode 100644 ga-core/src/main/resources/mapper/complaint/ComplaintMapper.xml create mode 100644 ga-core/src/main/resources/mapper/contract/AgentContractMapper.xml create mode 100644 ga-core/src/main/resources/mapper/endorse/ContractEndorsementMapper.xml create mode 100644 ga-core/src/main/resources/mapper/kpi/KpiMapper.xml create mode 100644 ga-core/src/main/resources/mapper/license/AgentLicenseMapper.xml create mode 100644 ga-core/src/main/resources/mapper/notice/NoticeMapper.xml create mode 100644 ga-core/src/main/resources/mapper/notice/UserNotificationMapper.xml create mode 100644 ga-core/src/main/resources/mapper/settle/CarrierReconciliationOutMapper.xml create mode 100644 ga-core/src/main/resources/mapper/settle/CommissionDisputeMapper.xml create mode 100644 ga-core/src/main/resources/mapper/settle/IncentivePaymentMapper.xml create mode 100644 ga-core/src/main/resources/mapper/settle/IncentiveProgramMapper.xml create mode 100644 ga-core/src/main/resources/mapper/settle/PeriodCloseMapper.xml create mode 100644 ga-core/src/main/resources/mapper/settle/SettleCorrectionMapper.xml create mode 100644 ga-core/src/main/resources/mapper/settle/TaxInvoiceMapper.xml create mode 100644 ga-core/src/main/resources/mapper/settle/TerminationSettlementMapper.xml create mode 100644 ga-core/src/main/resources/mapper/training/AgentTrainingMapper.xml create mode 100644 ga-core/src/main/resources/mapper/training/TrainingRequirementMapper.xml create mode 100644 ga-core/src/main/resources/mapper/withdraw/WithdrawRequestMapper.xml create mode 100644 ga-frontend/src/api/agent-contract.ts create mode 100644 ga-frontend/src/api/approval.ts create mode 100644 ga-frontend/src/api/attachment.ts create mode 100644 ga-frontend/src/api/commission.ts create mode 100644 ga-frontend/src/api/complaint.ts create mode 100644 ga-frontend/src/api/endorsement.ts create mode 100644 ga-frontend/src/api/kpi.ts create mode 100644 ga-frontend/src/api/license.ts create mode 100644 ga-frontend/src/api/notice.ts create mode 100644 ga-frontend/src/api/notification.ts create mode 100644 ga-frontend/src/api/training.ts create mode 100644 ga-frontend/src/api/withdraw.ts create mode 100644 ga-frontend/src/components/biz/ApprovalProgress.tsx create mode 100644 ga-frontend/src/components/biz/AttachmentUpload.tsx create mode 100644 ga-frontend/src/components/biz/NotificationBell.tsx create mode 100644 ga-frontend/src/components/common/ErrorBoundary.tsx create mode 100644 ga-frontend/src/pages/agent/AgentContracts.tsx create mode 100644 ga-frontend/src/pages/agent/AgentLicenses.tsx create mode 100644 ga-frontend/src/pages/agent/AgentTrainings.tsx create mode 100644 ga-frontend/src/pages/commission/CollectionCommission.tsx create mode 100644 ga-frontend/src/pages/commission/CommissionMaster.tsx create mode 100644 ga-frontend/src/pages/commission/OverrideLedger.tsx create mode 100644 ga-frontend/src/pages/commission/PersistencyBonus.tsx create mode 100644 ga-frontend/src/pages/commission/ReinstatementCommission.tsx create mode 100644 ga-frontend/src/pages/commission/RenewalCommission.tsx create mode 100644 ga-frontend/src/pages/contracts/Complaints.tsx create mode 100644 ga-frontend/src/pages/contracts/ContractEndorsements.tsx create mode 100644 ga-frontend/src/pages/kpi/KpiCumulative.tsx create mode 100644 ga-frontend/src/pages/kpi/KpiMonthlySummary.tsx create mode 100644 ga-frontend/src/pages/kpi/KpiOrgSummary.tsx create mode 100644 ga-frontend/src/pages/kpi/KpiRetention.tsx create mode 100644 ga-frontend/src/pages/payments/WithdrawRequest.tsx create mode 100644 ga-frontend/src/pages/report/ChargebackRiskReport.tsx create mode 100644 ga-frontend/src/pages/report/OrgReport.tsx create mode 100644 ga-frontend/src/pages/report/PerformanceReport.tsx create mode 100644 ga-frontend/src/pages/system/Approvals.tsx create mode 100644 ga-frontend/src/pages/system/Notices.tsx create mode 100644 ga-frontend/src/theme/tokens.ts diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md new file mode 100644 index 0000000..5516baf --- /dev/null +++ b/DEVELOPER_GUIDE.md @@ -0,0 +1,505 @@ +# GA 수수료 정산 시스템 — 개발자 가이드 + +신입 개발자가 **5분 안에 첫 PR**을 보낼 수 있도록 작성한 온보딩 문서. +README.md 의 "실행 가이드"를 먼저 읽고 로컬 환경을 기동한 다음 이 문서로 넘어올 것. + +--- + +## 1. 환경 설정 + +### 필수 설치 + +| 도구 | 버전 | 확인 명령 | +|------|------|-----------| +| JDK | 17 이상 | `java -version` | +| Node.js | 18 이상 | `node -v` | +| Docker Desktop | 최신 | `docker version` | +| IntelliJ IDEA | 2023+ 권장 | — | + +### 선택: 로컬 DB/Redis (Docker 권장) + +```bash +# Docker 로 PostgreSQL + Redis 한 번에 띄우기 +docker compose up -d postgres redis +``` + +운영 DB(`192.168.0.60:55432/trading_ai/ga`)에 바로 붙을 경우 Docker 불필요. + +### 백엔드 실행 + +```bash +# 프로젝트 루트에서 +./gradlew :ga-api:bootRun --args='--spring.profiles.active=trading_ai' +# 로컬 DB 사용 시 (기본 프로파일) +./gradlew :ga-api:bootRun +``` + +### 프론트엔드 실행 + +```bash +cd ga-frontend +npm install +npm run dev # http://localhost:3000 +``` + +`/api/*` 요청은 Vite 프록시로 `http://localhost:8080`에 자동 전달됩니다. + +--- + +## 2. 프로젝트 구조 + +``` +ga-commission-system/ +├── ga-common/ # 공통 프레임워크 +│ ├── annotation/ # @RequirePermission, @DataChangeLog, @ExcelColumn +│ ├── aop/ # PermissionAspect, DataChangeLogAspect, ApiLogAspect +│ ├── auth/ # JWT 필터/프로바이더 +│ ├── code/ # 공통코드 서비스 (Redis 캐시) +│ ├── excel/ # SXSSF 다운로드 / SAX 업로드 +│ └── db/migration/ # V1~V55 Flyway SQL (모든 마이그레이션은 여기) +│ +├── ga-core/ # 도메인 코어 — VO, Mapper, XML +│ ├── vo/ # {도메인}VO + {도메인}Resp + {도메인}SaveReq + {도메인}SearchParam +│ ├── mapper/ # @Mapper 인터페이스 (패키지는 도메인별) +│ └── (resources)/mapper/ # MyBatis XML +│ +├── ga-api/ # REST API — Service + Controller +│ ├── auth/ # 로그인/토큰 갱신 +│ ├── controller/ # 도메인별 Controller +│ └── service/ # 도메인별 Service +│ +├── ga-batch/ # Spring Batch — 월 정산 잡 (8 Step) +│ +└── ga-frontend/ # React 18 + Vite + TypeScript + ├── src/ + │ ├── components/common/ # SearchForm, DataGrid, PermissionButton, ExcelExportButton, CodeSelect, CodeBadge + │ ├── components/biz/ # ApprovalProgress, AttachmentUpload, NotificationBell + │ ├── pages/ # 도메인별 페이지 (50+) + │ ├── hooks/ # usePermission, useCommonCodes + │ └── theme/tokens.ts # 디자인 토큰 (GRAY, RADIUS, SHADOW, COLOR_*) + └── nginx.conf # SPA + /api 리버스 프록시 +``` + +--- + +## 3. 한 도메인 추가 — 표준 작업 흐름 (5분 컷) + +아래 순서로 작성하면 메뉴 등록부터 화면까지 일관됩니다. + +### Step 1. DB 마이그레이션 (dba) + +`ga-common/src/main/resources/db/migration/V{N+1}__{도메인명}.sql` 생성. + +```sql +-- 예: V56__회계전표.sql +CREATE TABLE IF NOT EXISTS ga.accounting_entry ( + id BIGSERIAL PRIMARY KEY, + settle_month CHAR(6) NOT NULL, + entry_type VARCHAR(20) NOT NULL, -- DEBIT / CREDIT + amount NUMERIC(18,2) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 메뉴 등록 (menu 테이블) +INSERT INTO ga.menu (menu_code, parent_code, menu_name, url, sort_order, is_use) +VALUES ('ACCOUNTING_ENTRY', 'ACCOUNTING', '회계 전표', '/accounting/entries', 10, true) +ON CONFLICT (menu_code) DO NOTHING; +``` + +번호 규칙: 기존 최대 번호 + 1. 현재 마지막은 V55. + +### Step 2. VO 작성 (core) + +`ga-core/src/main/java/com/ga/core/vo/{도메인}/` 아래 4종 생성. + +```java +// 검색 파라미터 +@Data +public class AccountingEntrySearchParam extends SearchParam { + private String settleMonth; + private String entryType; +} + +// 응답 +@Data +public class AccountingEntryResp { + private Long id; + private String settleMonth; + private String entryType; + @JsonFormat(shape = JsonFormat.Shape.STRING) private BigDecimal amount; + private LocalDateTime createdAt; +} + +// 저장 요청 +@Data +public class AccountingEntrySaveReq { + @NotBlank private String settleMonth; + @NotBlank private String entryType; + @NotNull private BigDecimal amount; +} + +// Enum (공통코드 기반 처리는 String으로, 타입 안전성이 필요할 때만 Enum 사용) +public enum EntryType { + DEBIT, CREDIT; + @JsonValue public String value() { return name(); } +} +``` + +### Step 3. Mapper Interface + XML (core) + +```java +// ga-core/.../mapper/accounting/AccountingEntryMapper.java +@Mapper +public interface AccountingEntryMapper { + List selectList(AccountingEntrySearchParam p); + AccountingEntryResp selectById(Long id); + int insert(AccountingEntrySaveReq req); + int update(AccountingEntrySaveReq req); + int delete(Long id); +} +``` + +XML은 `ga-core/src/main/resources/mapper/accounting/AccountingEntryMapper.xml`: + +```xml + + + +``` + +### Step 4. Service (api) + +```java +// ga-api/.../service/accounting/AccountingEntryService.java +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AccountingEntryService { + private final AccountingEntryMapper mapper; + + public PageResponse list(AccountingEntrySearchParam p) { + p.startPage(); // PageHelper 트리거 + return PageResponse.of(mapper.selectList(p)); + } + + @Transactional + @DataChangeLog(target = "accounting_entry", action = "INSERT") + public void create(AccountingEntrySaveReq req) { + mapper.insert(req); + } +} +``` + +### Step 5. Controller (api) + +```java +// ga-api/.../controller/accounting/AccountingEntryController.java +@RestController +@RequestMapping("/api/accounting-entries") +@RequiredArgsConstructor +public class AccountingEntryController { + private final AccountingEntryService service; + + @GetMapping + @RequirePermission(menu = "ACCOUNTING_ENTRY", perm = "READ") + public ApiResponse> list(AccountingEntrySearchParam p) { + return ApiResponse.ok(service.list(p)); + } + + @PostMapping + @RequirePermission(menu = "ACCOUNTING_ENTRY", perm = "CREATE") + public ApiResponse create(@RequestBody @Valid AccountingEntrySaveReq req) { + service.create(req); + return ApiResponse.ok(); + } +} +``` + +URL 규칙: `/api/{리소스s}` — 복수형 + kebab-case. 예) `/api/accounting-entries`, `/api/agent-licenses`. + +### Step 6. React 페이지 (frontend) + +`ga-frontend/src/pages/accounting/AccountingEntries.tsx`: + +```tsx +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm, { SearchCondition } from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import ExcelExportButton from '@/components/common/ExcelExportButton'; +import PermissionButton from '@/components/common/PermissionButton'; +import type { ColDef } from '@ag-grid-community/core'; + +interface Row { id: number; settleMonth: string; entryType: string; amount: string; } + +const CONDITIONS: SearchCondition[] = [ + { type: 'month', name: 'settleMonth', label: '정산월' }, + { type: 'code', name: 'entryType', label: '유형', groupCode: 'ENTRY_TYPE' }, +]; + +const COLUMNS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 120 }, + { field: 'entryType', headerName: '유형', width: 100 }, + { field: 'amount', headerName: '금액', width: 150, type: 'numericColumn' }, +]; + +export default function AccountingEntries() { + const [params, setParams] = useState>({}); + const { data, isLoading } = useQuery({ + queryKey: ['accounting-entries', params], + queryFn: () => axios.get('/api/accounting-entries', { params }).then(r => r.data.data), + }); + + return ( + + +
+ + + 등록 + +
+ +
+ ); +} +``` + +라우터 등록은 `App.tsx`의 `` 블록에 `} />` 한 줄 추가. + +--- + +## 4. 자주 쓰는 패턴 + +### CodeSelect — 공통코드 드롭다운 + +```tsx +import CodeSelect from '@/components/common/CodeSelect'; + +// groupCode 는 V9/V12/V55 에 등록된 공통코드 그룹 코드 + +``` + +### ExcelExportButton — 엑셀 다운로드 + +```tsx + +``` + +백엔드 엔드포인트는 `ExcelService.download(mapper::selectList, columns, response)` 패턴으로 구현. + +### CodeBadge — 상태 배지 + +```tsx +import CodeBadge from '@/components/common/CodeBadge'; + +// status 값에 해당하는 공통코드 label + 색상 자동 매핑 + +``` + +### PermissionButton — 권한 게이트 + +```tsx + + 마감 확정 + +``` + +권한이 없으면 렌더링 자체를 하지 않습니다 (`return null`). + +### AG Grid 서버사이드 페이징 + +`DataGrid` 컴포넌트는 `ClientSideRowModelModule`을 사용하고 페이징은 API에서 PageHelper 결과를 받아 컴포넌트 외부에서 처리합니다. 100만건 이상의 대용량 화면에서는 `ServerSideRowModelModule`로 교체하세요. + +```tsx +// 대용량 — AG Grid ServerSide 직접 사용 +const datasource = { + getRows: (params) => { + axios.get('/api/recruit-ledger', { params: { page: params.request.startRow / 100 + 1, size: 100 } }) + .then(r => params.successCallback(r.data.data.list, r.data.data.total)); + }, +}; +``` + +--- + +## 5. 디자인 토큰 사용법 + +모든 색상·간격·그림자는 `@/theme/tokens.ts` 에서 임포트합니다. Inline style 에 직접 색상 코드를 쓰지 마세요. + +```tsx +import { GRAY, RADIUS, SHADOW, COLOR_PRIMARY, COLOR_ERROR } from '@/theme/tokens'; + +// 카드 스타일 예시 +
+ +// 경고 텍스트 +만료 임박 + +// 비활성 텍스트 +데이터 없음 +``` + +--- + +## 6. 결재 워크플로우 패턴 + +결재가 필요한 도메인(withdraw_request, commission_dispute, settle_master 확정, incentive_payment 등)은 `approval_request` 테이블을 통해 처리합니다. + +```java +// target_type 은 공통코드 APPROVAL_TARGET 그룹에 등록 +// withdraw_request 결재 신청 예시 (Service) +ApprovalRequestSaveReq req = new ApprovalRequestSaveReq(); +req.setTargetType("WITHDRAW"); // WITHDRAW / SETTLE / DISPUTE / INCENTIVE +req.setTargetId(withdrawId); +req.setApprovalLineCode("BASIC_3"); // approval_line.line_code +approvalRequestMapper.insert(req); +``` + +프론트에서는 `ApprovalProgress` 컴포넌트로 현재 결재 단계를 시각화합니다. + +```tsx +import ApprovalProgress from '@/components/biz/ApprovalProgress'; + + +``` + +--- + +## 7. @RequirePermission / @DataChangeLog / @JsonValue Enum + +### @RequirePermission + +```java +@GetMapping("/export") +@RequirePermission(menu = "RECRUIT_LEDGER", perm = "EXPORT") +public void exportExcel(HttpServletResponse response) { ... } +``` + +menu 값은 `ga.menu.menu_code`와 일치해야 합니다. 불일치하면 403. + +### @DataChangeLog + +```java +@Transactional +@DataChangeLog(target = "agent", action = "UPDATE") +public void update(Long id, AgentSaveReq req) { + mapper.update(id, req); +} +// → data_change_log 에 before/after JSON 자동 저장 (AOP) +``` + +action 값: INSERT / UPDATE / DELETE / APPROVE / REJECT. + +### @JsonValue Enum + +```java +public enum CommissionType { + RECRUIT("모집"), MAINTAIN("유지"), COLLECTION("수금"), + OVERRIDE("오버라이드"), RENEWAL("갱신"), REINSTATE("부활"), + PERSIST("유지보수수당"), INCENTIVE("시책"), COMMISSION("위촉"); + + private final String label; + CommissionType(String label) { this.label = label; } + + @JsonValue // JSON 직렬화 시 name() 반환 + public String value() { return name(); } + + public String label() { return label; } +} +``` + +--- + +## 8. AG Grid vs Ant Design Table 사용 기준 + +| 상황 | 컴포넌트 | +|------|----------| +| 일반 조회 목록 (< 5만건) | `DataGrid` (내부적으로 AG Grid) | +| 엑셀과 동일한 인라인 편집 필요 | AG Grid `editable: true` 직접 사용 | +| 단순 설정/마스터 테이블 (< 100행) | Ant Design `Table` 직접 사용 | +| 대용량 서버사이드 (> 10만건) | AG Grid `ServerSideRowModelModule` | +| KPI/집계 합계행 있는 화면 | `DataGrid`의 `pinnedBottomRow` prop | + +--- + +## 9. 트러블슈팅 FAQ + +### Q. @RequirePermission 에서 403 이 발생함 +menu 코드가 `ga.menu.menu_code` 에 없거나 해당 역할에 권한이 없는 경우입니다. +1. `SELECT * FROM ga.menu WHERE menu_code = 'FOO';` — 메뉴가 존재하는지 확인 +2. `SELECT * FROM ga.role_menu_permission WHERE menu_code = 'FOO';` — 권한 매트릭스 확인 +3. V54 메뉴 INSERT SQL 이 운영 DB 에 적용됐는지 확인 (ga-api 재시작 필요) + +### Q. Redis 캐시와 DB 공통코드가 다름 +`POST /api/codes/refresh` 를 호출하면 Redis 공통코드 캐시를 전체 갱신합니다. +또는 Redis CLI 에서 `DEL ga:codes:*` 로 수동 삭제 후 재조회. + +### Q. MyBatis XML "No result found" / "column not found" +- resultType 의 필드명이 SQL 컬럼명과 다른 경우: XML 에 `` 또는 SQL 에 alias 사용 +- `snake_case` → `camelCase` 자동 변환은 `MyBatisConfig` 에 `mapUnderscoreToCamelCase=true` 설정되어 있어 기본 동작 + +### Q. Flyway 마이그레이션 체크섬 에러 +이미 적용된 SQL 파일을 수정하면 발생합니다. 수정이 필요하면 새 버전 파일(`V{N+1}__fix.sql`)을 추가하세요. +개발 환경 긴급 수정 시: `DELETE FROM ga.flyway_schema_history WHERE version = '55';` 후 재시작. + +### Q. AG Grid 화면이 흰 화면으로 보임 +`@ag-grid-community/styles/ag-grid.css` 와 `ag-theme-quartz.css` import 가 `DataGrid.tsx` 에 있습니다. +페이지에서 `DataGrid` 컴포넌트를 사용하면 자동으로 적용됩니다. 직접 `AgGridReact` 를 사용할 때는 수동으로 import 필요. + +### Q. 프론트 401 "토큰이 만료되었습니다" +`localStorage.removeItem('accessToken')` 후 `/login` 으로 재로그인. +React Query 의 `onError` 인터셉터에서 401 시 자동 `/login` 리다이렉트가 설정되어 있습니다. + +### Q. 빌드 시 "Cannot find symbol" — Mapper 메서드 누락 +`ga-core` 의 Mapper 인터페이스에 메서드가 없으면 `ga-api` 서비스에서 컴파일 에러. +`ga-core` 의 해당 Mapper 와 XML 에 메서드를 먼저 추가하고 `ga-api` 를 빌드하세요. + +### Q. 운영 DB 에서 P2~P4 API 가 "relation does not exist" 에러 +운영 DB 에는 V17(또는 V22) 까지만 적용되어 있습니다. +`ga-api 를 재시작`하면 Flyway 가 V18~V55 를 자동 순차 적용합니다. + +--- + +## 10. 코드 컨벤션 + +### VO 네이밍 + +| 클래스 | 용도 | 예시 | +|--------|------|------| +| `{도메인}VO` | DB row 1:1 매핑 (내부 전용) | `AgentVO` | +| `{도메인}Resp` | API 응답 | `AgentResp` | +| `{도메인}SaveReq` | 등록/수정 요청 (공용) | `AgentSaveReq` | +| `{도메인}SearchParam` | 검색 조건 + 페이징 | `AgentSearchParam` | + +### Mapper XML 규칙 +- namespace 는 Mapper 인터페이스 FQCN 과 정확히 일치 +- resultType 은 항상 Resp 클래스 사용 (VO 직접 반환 금지) +- 동적 조건은 `` + `` 사용 (`1=1` AND 패턴 금지) +- 대량 INSERT 는 `` batch 사용 + +### API URL 규칙 +- `/api/{리소스s}` — 복수형 kebab-case +- 예: `/api/agent-contracts`, `/api/commission-disputes`, `/api/period-closes` +- 단건 조회: `GET /api/{리소스s}/{id}` +- 상태 변경: `POST /api/{리소스s}/{id}/approve` (동사 사용 허용) diff --git a/DOMAIN_GAP_P4.md b/DOMAIN_GAP_P4.md new file mode 100644 index 0000000..9b6bad0 --- /dev/null +++ b/DOMAIN_GAP_P4.md @@ -0,0 +1,145 @@ +# 도메인 GAP 분석 & P4 기획서 + +작성일: 2026-05-14 +작성자: PL (자율 모드) +대상: 한국 GA(법인보험대리점) 수수료 정산 시스템 + +--- + +## 1. 현재까지의 도메인 커버리지 (P0~P3 + 부속) + +| 영역 | 적용 항목 | +|---|---| +| **세무** (P0-1) | 사업소득 3.3% / 기타소득 8.8% / 지방세 10% / VAT 10% | +| **공제** (P0-2) | 월별 차감 마스터(agent_deduction) + 이체 명세 | +| **명세서** (P0-3) | commission_statement + Excel 출력 | +| **1200%룰** (P1-1) | regulatory_limit + 한도 검증 + 분급 큐 | +| **분급** (P1-2) | installment_plan 7년 (35/25/15/10/7/5/3) | +| **차등환수** (P1-3) | chargeback_grade 5단계 | +| **마감/정정/이의/보류** (P2-1~4) | period_close / settle_correction / commission_dispute / hold_reason_code | +| **시책·해촉·세금계산서·외부회신** (P2-5~8) | incentive_program/payment / termination_settlement / tax_invoice / carrier_reconciliation_out | +| **KPI** (P3) | mv_monthly/org/retention/cumulative + Service + 화면 | + +--- + +## 2. 한국 GA 도메인에서 **여전히 빠진** 항목 (검토) + +### 2-A. 영업/계약 라이프사이클 (필수) +- ❌ **청약(Proposal)** → 청약→인수심사→체결 단계. 청약 수당(소액) 처리 부재 +- ❌ **인수심사 결과(Underwriting Result)** — 거절/조건부 승인/할증 결과 추적 +- ❌ **계약 변경(Endorsement)** — 보험료/피보험자/수익자 변경 시 수수료 재계산 트리거 +- ❌ **부활(Reinstatement)** — 실효된 계약 부활 시 환수 처리 + 수수료 재산정 +- ❌ **계약 양도(Assignment)** — 설계사 간 계약 이관 → 수수료 재배분 + +### 2-B. 설계사 라이프사이클 (필수) +- ❌ **위촉계약(Agent Contract)** — 보험사-설계사 위촉계약서 + 위촉수수료 +- ❌ **자격 관리(License)** — 자격증 만료/갱신 알림 (생보/손보/변액) +- ❌ **보수교육(Training)** — 의무 교육 시간 추적 (감독규정 12시간/년) +- ❌ **징계 이력(Discipline)** — 불완전판매 제재 → 환수 가중치 +- ❌ **휴직(Suspension)** — 활동중지 기간 정산 제외 + +### 2-C. 회계/재무 (필수) +- ❌ **회계 전표(Accounting Entry)** — 수수료 지급액 자동 분개 +- ❌ **출금 신청 워크플로우(Withdraw Request)** — 결재 → 펌뱅킹 → 결과 +- ❌ **원천세 신고서(Tax Report)** — 분기 사업소득세 신고 자료 집계 +- ❌ **부가세 신고 자료** — 매출/매입 집계 (세금계산서 기반) +- ❌ **결산(Period Settlement)** — period_close 와 별개의 월/분기/연 회계 마감 + +### 2-D. 컴플라이언스 (중요) +- ❌ **민원 관리(Complaint)** — 고객 민원 → 설계사 책임 추적 → 환수 트리거 +- ❌ **불완전판매 모니터링** — 13M/25M 유지율 기반 이상 탐지 +- ❌ **상품판매 자격 매트릭스** — 변액=변액자격필요, 매핑 검증 + +### 2-E. 시스템/UX (필수) +- ❌ **결재선/결재 요청(Approval)** — 정산 확정/이의/시책 결재 워크플로우 +- ❌ **공지사항(Notice)** + **개인 알림(UserNotification)** +- ❌ **첨부파일(Attachment)** — 위촉계약서/민원자료 등 문서 저장 +- ❌ **사용자 알림 채널** — 메일/SMS 발송 큐 + +### 2-F. 분석/리포트 보강 (있으면 좋음) +- ❌ **이상치 탐지** (Outlier KPI) +- ❌ **차월 예측** (예상 정산액) +- ❌ **연말 지급명세서** — 설계사 연 사업소득 세무 자료 PDF + +--- + +## 3. 우선순위 결정 (PL 판단) + +> 추가 검토 (2026-05-14): 사용자 지적 — **"오버라이드 / 수금수수료 같은 수수료 종류 정보가 많이 빠짐"**. +> P4-A 로 **수수료 종류 보강** 5개 도메인을 우선 배치하고, 운영 9 도메인을 P4-B 로 후위 배치. + +### **P4-A — 수수료 종류 보강 (5 도메인)** ⭐ 사용자 지적 반영 +| # | 도메인 | 마이그레이션 | 핵심 | +|---|---|---|---| +| P4-A-1 | **commission_type 코드 + 통합 마스터** | V40 | 모집/유지/수금/오버라이드/갱신/부활/유지보수수당/시책/위촉 9종 통합 코드 + commission_master 마스터 | +| P4-A-2 | **수금수수료 (collection_commission_rule + collection_commission_ledger)** | V41 | 보험료 수납 단위(월) 발생. 납입주기별 차등 + 원장 | +| P4-A-3 | **갱신/부활 수수료 (renewal_commission, reinstatement_commission)** | V42 | 자동차/실비 갱신 + 실효 부활 시 별도 수수료 룰·원장 | +| P4-A-4 | **유지보수수당 (persistency_bonus_rule + persistency_bonus)** | V43 | 13M/25M/37M 유지율 충족 1회 지급. 등급별 정액 또는 정률 | +| P4-A-5 | **오버라이드 보강 (override_ledger + override_summary)** | V44 | 기존 override_rule + 8 Step 결과를 **원장 단위로 추적**. 직급별/조직별 합계 + 검증 | + +### **P4-B — 운영 필수 9 도메인** (P4-A 완료 후) +| # | 도메인 | 마이그레이션 | 핵심 | +|---|---|---|---| +| P4-B-1 | **위촉계약 (agent_contract)** | V45 | 보험사-설계사 위촉계약서 + 위촉수수료 + 효력 기간 | +| P4-B-2 | **자격 관리 (agent_license)** | V46 | 생보/손보/변액 자격증 + 만료일 + 갱신 이력 | +| P4-B-3 | **보수교육 (agent_training)** | V47 | 교육 이수 시간 + 의무 12시간/년 충족 여부 | +| P4-B-4 | **계약 변경 (contract_endorsement)** | V48 | 변경 유형(보험료/수익자/피보험자) + 수수료 재계산 트리거 | +| P4-B-5 | **민원 관리 (complaint)** | V49 | 고객 민원 + 설계사 책임 + 처리 상태 + 환수 연동 | +| P4-B-6 | **결재 워크플로우 (approval_line + approval_request)** | V50 | 결재선 마스터 + 다단 결재 요청/처리 | +| P4-B-7 | **공지사항 + 알림 (notice + user_notification)** | V51 | 시스템 공지 + 사용자별 알림 (읽음 여부) | +| P4-B-8 | **첨부파일 (attachment)** | V52 | 도메인 무관 공통 첨부 (소속/파일경로/타입) | +| P4-B-9 | **출금 신청 (withdraw_request)** | V53 | 결재 + 펌뱅킹 송신 + 결과 반영 워크플로우 | + +메뉴 INSERT 는 **V54** 한 파일에서 P4-A + P4-B 14개 메뉴 한 번에 등록. + +### **P5 — 회계/분석 보강** (별도 라운드) +- 회계 전표 자동 분개 / 원천세·부가세 신고 자료 / 결산 / 이상치·예측 KPI / 연말 지급명세서 + +### **P6 — 모바일/외부 연동** (제일 후속) +- 펌뱅킹 실연동 / 모바일 셀프 조회 / SMS/카카오 발송 + +이번 라운드는 **P4 만** 일괄 진행. P5/P6 는 다음 라운드에서. + +--- + +## 4. P4 이행 계획 (Wave 방식, 이미 검증된 패턴) + +| Wave | 담당 | 산출물 | +|---|---|---| +| Wave 1 | dba-architect | V40~V48 9개 + V49 메뉴 INSERT (총 10 파일) | +| Wave 2 | core-architect | P4 VO/Resp/SaveReq/SearchParam + Mapper(IF+XML) + Enum | +| Wave 3 | api-developer | Service + Controller + 결재 AOP (또는 Service 가드) | +| Wave 4 | frontend-developer | 화면 9개 (토스 톤 일관) + 라우터 + Route 등록 | +| Wave 5 | infra-reviewer | 빌드/통합 검증 + README/HANDOFF 갱신 + 메뉴 적용 | + +병렬화: Wave 1↔3 의존, Wave 4 는 Wave 3 후, Wave 5 는 마지막. 단 #8 KPI 대시보드 진행 중이므로 frontend Wave 4 는 #8 완료 후. + +--- + +## 5. 도메인 가정값 (P4 채택) + +| 항목 | 표준값 | 근거 | +|---|---|---| +| 보수교육 의무 시간 | 12 시간/년 | 보험업법 시행령 제17조 | +| 자격증 갱신 주기 | 5 년 | 보험업감독규정 | +| 민원 환수 기준 | 13M 이내 민원 = 환수 100% | 일반 GA 사내 규정 가정 | +| 결재 최대 단계 | 3 단계 | 일반 운영 가정 | +| 첨부 최대 크기 | 50 MB | 시스템 기본값 | +| 출금 신청 마감 | 매월 25일 | 정산일 익월 5일 가정 | + +가정값 → system_config / approval_line / training_requirement 등 외부화 가능 구조로 등록. + +--- + +## 6. 일정 추정 + +| Wave | 예상 시간 | +|---|---| +| Wave 1 (dba) | 5~7분 | +| Wave 2 (core) | 8~12분 | +| Wave 3 (api) | 6~10분 | +| Wave 4 (frontend, #8 후) | 10~15분 | +| Wave 5 (infra) | 5~8분 | +| **합계** | **35~50분** | + +배치/AOP 같은 추가 통합은 P5 라운드로 미룸. diff --git a/HANDOFF.md b/HANDOFF.md index ad00e1c..f23532f 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,4 +1,4 @@ -# Handoff Note — 2026-05-12 +# Handoff Note — 2026-05-14 (Round 2 완료) 다음 세션에서 이어가기 위한 핸드오프 문서. Agent Teams 기반 자동 체인으로 진행 중인 작업의 현재 상태와 잔여 작업, 재개 절차를 기록한다. @@ -64,48 +64,87 @@ - P1-3-b(core) + P1-1-c TODO 교체(api) 2명 동시 - 모듈이 다른 사람끼리 묶어 머지 충돌 없음 -## 3. 잔여 작업 +## 3. Round 2 완료 사항 (2026-05-14) -### 3-1. P1 정산 통합 (보류 권장) -- ga-batch의 8 Step 정산 잡에 TaxCalculator/DeductionService/RegulatoryLimitChecker/InstallmentService/ChargebackCalculator 통합 -- batch-engineer 작업 -- 운영 비즈니스 룰 의존이 커 **인프라만 완성하고 통합은 사용자 결정 후** 진행 권장 -- 보류 가능 — 현 시점에 인프라/CRUD/API는 모두 동작 - -### 3-2. 도메인 P2 — 8개 항목 (Wave 방식) -| # | 도메인 | 마이그레이션 | 핵심 | +### 3-1. P2 — 8 도메인 (V29~V36, ~50 VO·Mapper, 18 Service+Controller) +| # | 도메인 | 마이그레이션 | 상태 | |---|---|---|---| -| P2-1 | 마감 (Period Close) | V29 | period_close 테이블 + 마감 후 수정 차단 AOP | -| P2-2 | 정정 (Correction) | V30 | settle_correction + 원본 참조 + 정정 사유 | -| P2-3 | 이의 신청 (Dispute) | V31 | commission_dispute + OPEN/IN_REVIEW/APPROVED/REJECTED | -| P2-4 | 보류 사유 (Hold Reasons) | V32 | settle_master.hold_reason_code + HOLD_REASON 코드그룹 | -| P2-5 | 시책·인센티브 | V33 | incentive_program + incentive_payment | -| P2-6 | 해촉 정산 (Termination) | V34 | termination_settlement (agent_org_history 연계) | -| P2-7 | 세금계산서 (Tax Invoice) | V35 | tax_invoice 발행 이력 (SUPPLY/PURCHASE) | -| P2-8 | 외부 회신 | V36 | carrier_reconciliation_out (보험사 회신 인터페이스) | +| P2-1 | 마감 (Period Close) | V29 | 완료 | +| P2-2 | 정정 (Correction) | V30 | 완료 | +| P2-3 | 이의신청 (Dispute) | V31 | 완료 | +| P2-4 | 보류사유 (Hold Reasons) | V32 | 완료 | +| P2-5 | 시책·인센티브 | V33 | 완료 | +| P2-6 | 해촉정산 (Termination) | V34 | 완료 | +| P2-7 | 세금계산서 (Tax Invoice) | V35 | 완료 | +| P2-8 | 외부회신 | V36 | 완료 | -**Wave 디스패치 계획**: -- Wave 1 (dba): V29~V36 8개 마이그레이션 한 번에 -- Wave 2 (core): 8개 도메인 VO/Mapper 한 번에 -- Wave 3 (api): 8개 도메인 Service/Controller 한 번에 -- 디스패치 24 → 3으로 축소 +화면: ga-frontend Wave 4 에서 P4-B 화면과 함께 작성됨. -### 3-3. 도메인 P3 — KPI 대시보드 -| 작업 | 담당 | +### 3-2. P3 — KPI 대시보드 (V37~V39) +- dba: mv_monthly_summary / mv_org_summary / mv_retention_rate / mv_cumulative (V37) +- dba: 리포트 메뉴 + KPI 메뉴 INSERT (V38, V39) +- core: KpiMapper.java + XML +- api: KpiService + KpiController (`/api/kpi/*`) +- frontend: KpiMonthlySummary / KpiOrgSummary / KpiRetention / KpiCumulative (4개 화면) + +### 3-3. P4-A — 수수료 종류 보강 (V40~V44, 5 도메인) +| # | 도메인 | 마이그레이션 | 상태 | +|---|---|---|---| +| P4-A-1 | commission_type 코드 + commission_master 통합 | V40 | 완료 | +| P4-A-2 | 수금수수료 (collection_commission_rule/ledger) | V41 | 완료 | +| P4-A-3 | 갱신·부활수수료 (renewal/reinstatement) | V42 | 완료 | +| P4-A-4 | 유지보수수당 (persistency_bonus_rule/bonus) | V43 | 완료 | +| P4-A-5 | 오버라이드 원장 (override_ledger/summary) | V44 | 완료 | + +### 3-4. P4-B — 운영 필수 9 도메인 (V45~V53) +| # | 도메인 | 마이그레이션 | 상태 | +|---|---|---|---| +| P4-B-1 | 위촉계약 (agent_contract) | V45 | 완료 | +| P4-B-2 | 자격관리 (agent_license) | V46 | 완료 | +| P4-B-3 | 보수교육 (agent_training) | V47 | 완료 | +| P4-B-4 | 계약변경 (contract_endorsement) | V48 | 완료 | +| P4-B-5 | 민원관리 (complaint) | V49 | 완료 | +| P4-B-6 | 결재워크플로우 (approval_line/request) | V50 | 완료 | +| P4-B-7 | 공지·알림 (notice/user_notification) | V51 | 완료 | +| P4-B-8 | 첨부파일 (attachment) | V52 | 완료 | +| P4-B-9 | 출금신청 (withdraw_request) | V53 | 완료 | + +### 3-5. V54~V55 + 프론트 +- V54: P4 메뉴 INSERT (P4-A + P4-B 14개 메뉴) +- V55: P4 공통코드 추가 (approval_type, complaint_type, license_type, withdraw_status 등) +- 프론트: ErrorBoundary 전역 적용 + 토스 디자인 시스템 토큰 (`RADIUS`, `GRAY`, `SHADOW`) 일관 적용 +- 프론트 화면 추가: AgentContracts / AgentLicenses / AgentTrainings / Complaints / ContractEndorsements / CollectionCommission / CommissionMaster / OverrideLedger / PersistencyBonus / ReinstatementCommission / RenewalCommission / WithdrawRequest / Approvals / Notices (14개) + +### 3-6. P1 정산 통합 (batch-engineer 완료) +- ga-batch 8 Step에 TaxCalculator / DeductionService / RegulatoryLimitChecker / InstallmentPlanGenerator / ChargebackCalculator 통합 완료 + +## 4. 잔여 작업 (P5/P6) + +### P5 — 회계/분석 보강 (다음 라운드) +| 항목 | 설명 | |---|---| -| V37 Materialized View 4종 (월별/조직별/유지율/누계) | dba-architect | -| KPI VO/Mapper | core-architect | -| KpiService + KpiController | api-developer | -| KPI 대시보드 화면 (AG Grid 합계행, 차트) | frontend-developer | +| 회계 전표 자동 분개 | 수수료 지급액 → 분개 엔트리 자동 생성 | +| 원천세 신고 자료 | 분기별 사업소득세 집계 + 국세청 포맷 | +| 부가세 신고 자료 | tax_invoice 기반 매출/매입 집계 | +| 결산 | period_close 와 별개의 월/분기/연 회계 마감 | +| 이상치·예측 KPI | 13M/25M 유지율 이상 탐지 + 차월 예측 | +| 연말 지급명세서 | 설계사 연 사업소득 세무 자료 PDF | -### 3-4. 마무리 통합 -| 작업 | 담당 | +### P6 — 외부 연동 (P5 이후) +| 항목 | 설명 | |---|---| -| ga-batch 8 Step에 도메인 로직 통합 (TaxCalculator/Deduction/Regulatory/Installment/Chargeback) | batch-engineer | -| 통합 테스트 | infra-reviewer | -| README/DEVELOPER_GUIDE 업데이트 | infra-reviewer | +| 펌뱅킹 실연동 | withdraw_request 결재 완료 → 은행 API 직접 호출 | +| SMS/카카오 발송 | user_notification 채널 실연동 | +| 모바일 셀프 조회 | 설계사 전용 모바일 앱 or PWA | -## 4. 재개 절차 (다음 세션) +### 운영 DB 마이그레이션 (중요 공지) +운영 DB `192.168.0.60:55432/trading_ai/ga` 에는 현재 **V17(또는 V22) 까지만** 적용되어 있습니다. +`ga-api 를 재시작`하면 Flyway 가 V18~V55 를 자동 순차 적용합니다. +재시작 전까지 P2~P4 신규 도메인 API 는 테이블이 없어 동작하지 않습니다. + +단위/통합 테스트는 SqlSessionFactory 환경 설정 문제로 아직 미작성 상태입니다. + +## 5. 재개 절차 (다음 세션 — P5 라운드) 1. **Claude Code 시작 시 cwd 확인**: `C:\Users\kyu\Desktop\ga_pro` (현재 세션과 동일) 2. **Agent Teams 활성화 확인**: `.claude/settings.json` 이미 커밋되어 있음 (`418ff7c`) @@ -113,12 +152,13 @@ - PL 역할로 `TeamCreate(team_name="ga-pro-team", agent_type="pl", description="...")` 호출 - 7명 팀원 spawn (common/core/api/batch/frontend/dba/infra) — 첫 세션과 동일 4. **HANDOFF.md 읽기**: 본 문서 + 최근 git log로 컨텍스트 확보 -5. **권장 시작 지점**: P2 Wave 1 디스패치 (dba-architect에게 V29~V36 8개 마이그레이션) -6. **사용자 결정 필요 항목**: - - P1 정산 통합 진행 여부 (Wave 작업 전 또는 P3 후) +5. **권장 시작 지점**: P5 Wave 1 — dba-architect에게 V56~V61 (회계 전표 / 원천세 / 부가세 / 결산 / 이상치 / 연말 지급명세서) +6. **ga-api 재시작 필요**: 운영 DB 에 V18~V55 적용하려면 ga-api 재시작 후 Flyway 자동 실행 확인 +7. **사용자 결정 필요 항목**: + - P5 시작 전 ga-api 재시작해서 V18~V55 운영 DB 반영 여부 - P2-1 마감 도메인의 마감 후 수정 차단 AOP 구현 방식 (전역 AOP vs Service-level 가드) -## 5. PL 가이드 (다음 세션 유의사항) +## 6. PL 가이드 (다음 세션 유의사항) - **시간차 중복 메시지**: 팀원이 SendMessage로 같은 보고를 2~3번 보내는 경우 있음. 짧게 acknowledge만. - **idle 알림**: 팀원이 작업 끝낼 때마다 idle 알림 보냄. 정상. 액션 필요 없음. @@ -129,7 +169,7 @@ - 빌드/테스트 실패 시 담당 팀원에 재지시 - 컴파일은 PL이 직접 검증, 테스트는 환경 문제(SqlSessionFactory)로 통과 안 되는 게 알려진 상태 -## 6. 표준 가정 (도메인 작업 진행 시 채택한 값) +## 7. 표준 가정 (도메인 작업 진행 시 채택한 값) 도메인 룰은 한국 보험업감독규정 표준 가정을 외부화 가능한 구조로 등록. 실제 회사 정책과 다르면 system_config / regulatory_limit / installment_ratio / chargeback_grade의 데이터만 수정하면 됨. diff --git a/README.md b/README.md index 3d54e69..f4ab1b5 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,13 @@ ga-commission-system/ ### 시나리오 A — 운영 DB(trading_ai/ga) 에 연결해서 실행 ⭐ 권장 -이미 V1~V17 마이그레이션 + 테스트 데이터(보험사 5 / 상품 20 / 설계사 50 / 계약 200)가 +V1~V17 마이그레이션 + 테스트 데이터(보험사 5 / 상품 20 / 설계사 50 / 계약 200)가 `192.168.0.60:55432/trading_ai/ga` 스키마에 적용되어 있습니다. +> **주의**: 운영 DB 에는 현재 V1~V17(또는 V22 까지)만 적용되어 있습니다. +> V18~V55 는 ga-api 를 재시작하면 Flyway 가 자동으로 순서대로 적용합니다. +> 재시작 전까지 P2~P4 신규 도메인 API 는 테이블이 없어 동작하지 않습니다. + #### 1) Backend (ga-api) 실행 ```bash @@ -119,7 +123,7 @@ PW : admin1234! # 1) PostgreSQL + Redis 기동 (Docker) docker compose up -d postgres redis -# 2) ga-api 실행 (Flyway 가 자동으로 V1~V17 마이그레이션) +# 2) ga-api 실행 (Flyway 가 자동으로 V1~V55 마이그레이션) ./gradlew :ga-api:bootRun # 기본 프로파일은 application.yml — localhost:5432/ga 에 연결 @@ -219,21 +223,56 @@ UPDATE ga.users SET password = '$2b$10$...' WHERE login_id = 'admin'; ## 📋 주요 기능 -### 백엔드 -- **인증/권한**: JWT (HS256), 메뉴×권한 RBAC, BCrypt 비밀번호 +### 공통 프레임워크 (ga-common) +- **인증/권한**: JWT (HS256 액세스 2h/리프레시 7d), 메뉴×6권한 RBAC, BCrypt 비밀번호 - **공통코드**: Redis 캐시, 시스템코드 잠금, REST CRUD - **메뉴**: 트리 구조, 사용자별 동적 메뉴 + 권한 매트릭스 - **엑셀**: SXSSF+Cursor 다운로드(100만건), SAX+배치 업로드(70만건) -- **로그**: 로그인/API/데이터변경 자동 기록 (비동기) -- **암호화**: AES-256 CBC (`resident_no`, `account_no` 자동) +- **로그**: 로그인/API/데이터변경 자동 기록 (비동기 `@DataChangeLog` AOP) +- **암호화**: AES-256 CBC (`resident_no`, `account_no` TypeHandler 자동 적용) +- **업로드 템플릿**: 동적 컬럼 매핑 + 변환 규칙 관리 -### 도메인 -- **조직**: 트리 구조, CTE 재귀, 인사 이력 -- **계약**: 보험사/상품/계약 관리 -- **수수료 규정**: 보험사율, 지급율, 오버라이드, 환수, 예외코드 -- **원장**: 모집/유지/예외 (PageHelper + AG Grid) -- **정산**: 8 Step 배치 (수신→변환→대사→모집→유지→환수/예외→오버라이드→집계) -- **지급**: 이체파일 생성, 상태 추적 +### P0/P1 — 핵심 정산 도메인 +- **세무 (P0-1)**: TaxCalculator 순수 도메인 — 사업소득 3.3% / 기타소득 8.8% / 지방세 10% / VAT 10% +- **공제 (P0-2)**: agent_deduction 월별 차감 마스터 + payment_deduction_detail 이체 명세 +- **명세서 (P0-3)**: commission_statement 발급 마스터 + ExcelStatementGenerator (SXSSF) +- **1200%룰 (P1-1)**: regulatory_limit 마스터 + RegulatoryLimitChecker (효력일 이력 추적) +- **분급 (P1-2)**: installment_plan 7년 (35/25/15/10/7/5/3%) + InstallmentPlanGenerator +- **차등환수 (P1-3)**: chargeback_grade 5단계 + ChargebackCalculator + +### P2 — 운영 필수 8 도메인 (V29~V36) +- **마감 (P2-1)**: period_close — 월 마감 + 마감 후 수정 차단 가드 +- **정정 (P2-2)**: settle_correction — 원본 참조 정정 + 사유 코드 +- **이의신청 (P2-3)**: commission_dispute — OPEN/IN_REVIEW/APPROVED/REJECTED 워크플로우 +- **보류사유 (P2-4)**: settle_master.hold_reason_code + HOLD_REASON 코드그룹 +- **시책·인센티브 (P2-5)**: incentive_program + incentive_payment +- **해촉정산 (P2-6)**: termination_settlement (agent_org_history 연계) +- **세금계산서 (P2-7)**: tax_invoice 발행 이력 (SUPPLY/PURCHASE) +- **외부회신 (P2-8)**: carrier_reconciliation_out — 보험사 회신 인터페이스 + +### P3 — KPI 대시보드 (V37~V39) +- **월별 KPI**: mv_monthly_summary Materialized View + AG Grid 합계행 +- **조직별 KPI**: mv_org_summary — 조직 트리별 실적 +- **유지율 KPI**: mv_retention_rate — 13M/25M 유지율 추적 +- **누계 KPI**: mv_cumulative — 연초 누계 + 차트 + +### P4-A — 수수료 종류 보강 (V40~V44) +- **commission_master 통합 (P4-A-1)**: commission_type 9종 코드 통합 — 모집/유지/수금/오버라이드/갱신/부활/유지보수수당/시책/위촉 +- **수금수수료 (P4-A-2)**: collection_commission_rule + collection_commission_ledger — 납입주기별 차등 +- **갱신·부활수수료 (P4-A-3)**: renewal_commission_rule/ledger + reinstatement_commission_rule/ledger +- **유지보수수당 (P4-A-4)**: persistency_bonus_rule + persistency_bonus — 13M/25M/37M 유지율 1회 지급 +- **오버라이드 원장 (P4-A-5)**: override_ledger + override_summary — 직급별/조직별 합계 + 검증 + +### P4-B — 운영 필수 9 도메인 (V45~V53) +- **위촉계약 (P4-B-1)**: agent_contract — 보험사별 위촉계약서 + 위촉수수료 + 효력 기간 +- **자격관리 (P4-B-2)**: agent_license — 생보/손보/변액 자격증 + 만료일 + 갱신 이력 +- **보수교육 (P4-B-3)**: agent_training — 의무 12시간/년 이수 추적 +- **계약변경 (P4-B-4)**: contract_endorsement — 변경 유형 + 수수료 재계산 트리거 +- **민원관리 (P4-B-5)**: complaint — 고객 민원 + 설계사 책임 + 환수 연동 +- **결재워크플로우 (P4-B-6)**: approval_line + approval_request — 최대 3단계 다단 결재 +- **공지·알림 (P4-B-7)**: notice + user_notification — 시스템 공지 + 읽음 여부 +- **첨부파일 (P4-B-8)**: attachment — 도메인 무관 공통 첨부 (소속/경로/타입) +- **출금신청 (P4-B-9)**: withdraw_request — 결재 + 펌뱅킹 송신 + 결과 반영 ### 정산 배치 8 Step 1. **receiveData** — 보험사별 DataReceiver → raw_commission_data 적재 @@ -305,6 +344,47 @@ public class FooController { | V15 | 통계/리포트 뷰 (v_agent_monthly_summary 등) | | V16 | 관리자 계정 + 메뉴 상세 + 권한 매트릭스 | | V17 | 테스트 데이터 (보험사 5 / 상품 20 / 설계사 50 / 계약 200 + 수수료 규정) | +| V18 | 메뉴 트리 구조 재정렬 (동적 사이드바용) | +| V19 | 업로드 템플릿 마스터 (upload_template / upload_column) | +| V20 | 데이터 사전 (data_domain / data_dictionary) | +| V21 | 프로필·이력서 도메인 (agent_profile) | +| V22 | 비즈니스 도메인 확장 (계약·정산 보조 컬럼) | +| V23 | 세무 처리 — agent.tax_type/business_no/vat_type, payment 세금 컬럼 | +| V24 | 공제차감 — agent_deduction + payment_deduction_detail | +| V25 | 수수료 명세서 — commission_statement | +| V26 | 규제한도 마스터 — regulatory_limit (1200%룰) | +| V27 | 분급 계획 — installment_ratio + installment_plan | +| V28 | 차등환수 룰 — chargeback_grade | +| V29 | 마감 마스터 — period_close | +| V30 | 정정 — settle_correction | +| V31 | 이의신청 — commission_dispute | +| V32 | 보류사유 코드 — HOLD_REASON 공통코드 그룹 | +| V33 | 시책·인센티브 — incentive_program + incentive_payment | +| V34 | 해촉정산 — termination_settlement | +| V35 | 세금계산서 — tax_invoice | +| V36 | 보험사 회신 인터페이스 — carrier_reconciliation_out | +| V37 | KPI Materialized View 4종 (월별/조직별/유지율/누계) | +| V38 | 리포트 메뉴 INSERT | +| V39 | KPI 메뉴 INSERT | +| V40 | 수수료종류 코드 + commission_master 통합 마스터 | +| V41 | 수금수수료 — collection_commission_rule + collection_commission_ledger | +| V42 | 갱신·부활수수료 — renewal/reinstatement commission rule+ledger | +| V43 | 유지보수수당 — persistency_bonus_rule + persistency_bonus | +| V44 | 오버라이드 원장 — override_ledger + override_summary | +| V45 | 위촉계약 — agent_contract | +| V46 | 자격관리 — agent_license | +| V47 | 보수교육 — agent_training + training_requirement | +| V48 | 계약변경 — contract_endorsement | +| V49 | 민원관리 — complaint | +| V50 | 결재 워크플로우 — approval_line + approval_line_step + approval_request + approval_history | +| V51 | 공지·알림 — notice + user_notification | +| V52 | 첨부파일 — attachment | +| V53 | 출금신청 — withdraw_request | +| V54 | P4 메뉴 INSERT (P4-A + P4-B 14개 메뉴) | +| V55 | P4 공통코드 추가 (approval_type, complaint_type, license_type 등) | + +> 운영 DB (192.168.0.60) 에는 현재 V17(또는 V22) 까지만 적용되어 있습니다. +> **ga-api 재시작 시** Flyway 가 V18~V55 를 자동으로 순차 적용합니다. --- @@ -312,14 +392,24 @@ public class FooController { - [x] 1단계 — 프로젝트 뼈대 + DB (V1~V17) - [x] **ga-common** — 공통 프레임워크 일체 -- [x] **ga-core** — 22개 테이블 VO/Mapper/XML -- [x] **ga-api** — Auth + 14개 도메인 컨트롤러 +- [x] **ga-core** — 65+ 테이블 VO/Mapper/XML (P0~P4 전체) +- [x] **ga-api** — Auth + 50+ 도메인 컨트롤러 (128개 Java 파일) - [x] **ga-batch** — MonthlySettlementJob 8 Step (실제 동작 로직 포함) - [x] **Docker** — docker-compose + multi-stage Dockerfile + nginx -- [x] **ga-frontend** — 13개 페이지 (대시보드/설계사/계약/원장/정산/지급/배치/시스템관리) -- [x] **운영 DB 적용** — 192.168.0.60:55432/trading_ai/ga (V1~V17 + 테스트 데이터) -- [ ] **단위/통합 테스트** — 미작성 +- [x] **ga-frontend** — 50+ 페이지 (대시보드/설계사/계약/원장/정산/지급/배치/KPI/수수료/시스템관리) +- [x] **DB 마이그레이션** — V1~V55 (55개 파일, ga-common/src/main/resources/db/migration) +- [x] **코드 품질** — Enum/DTO/MapStruct/readOnly 트랜잭션 일괄 정비 +- [x] **P0 세무·공제·명세서** — TaxCalculator / DeductionService / ExcelStatementGenerator +- [x] **P1 1200%룰·분급·차등환수** — RegulatoryLimitChecker / InstallmentPlanGenerator / ChargebackCalculator +- [x] **P2 마감·정정·이의·보류·시책·해촉·세금계산서·외부회신** (8 도메인) +- [x] **P3 KPI 대시보드** — Materialized View 4종 + Service + 화면 +- [x] **P4-A 수수료 종류 보강** — commission_master 통합 / 수금 / 갱신 / 부활 / 유지보수수당 / 오버라이드원장 +- [x] **P4-B 운영 도메인** — 위촉계약 / 자격관리 / 보수교육 / 계약변경 / 민원 / 결재워크플로우 / 공지·알림 / 첨부파일 / 출금신청 +- [x] **운영 DB** — 192.168.0.60:55432/trading_ai/ga (V17까지 적용, V18~V55는 재시작 시 자동 적용) +- [ ] **단위/통합 테스트** — 미작성 (SqlSessionFactory 환경 문제로 보류) - [ ] **MappingEngine 정밀화** — 일부 변환 규칙은 단순화된 형태 +- [ ] **P5 회계/분석** — 전표 분개 / 원천세·부가세 신고 / 결산 / 이상치·예측 / 연말 지급명세서 +- [ ] **P6 외부 연동** — 펌뱅킹 실연동 / SMS·카카오 / 모바일 셀프 조회 --- diff --git a/ga-api/src/main/java/com/ga/api/controller/approval/AdvanceReq.java b/ga-api/src/main/java/com/ga/api/controller/approval/AdvanceReq.java new file mode 100644 index 0000000..87d5b6c --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/approval/AdvanceReq.java @@ -0,0 +1,15 @@ +package com.ga.api.controller.approval; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class AdvanceReq { + @NotNull + private Long approverId; + /** APPROVE / REJECT */ + @NotBlank + private String action; + private String comment; +} diff --git a/ga-api/src/main/java/com/ga/api/controller/approval/ApprovalController.java b/ga-api/src/main/java/com/ga/api/controller/approval/ApprovalController.java new file mode 100644 index 0000000..bf196a0 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/approval/ApprovalController.java @@ -0,0 +1,115 @@ +package com.ga.api.controller.approval; + +import com.ga.api.service.approval.ApprovalService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.approval.*; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "결재 관리") +@RestController +@RequestMapping("/api/approvals") +@RequiredArgsConstructor +public class ApprovalController { + + private final ApprovalService service; + + // ── 결재선 ───────────────────────────────────────────────────────────────── + + @GetMapping("/lines") + @RequirePermission(menu = "APPROVAL", perm = "READ") + public ApiResponse> listLines( + @ModelAttribute ApprovalLineSearchParam param) { + return ApiResponse.ok(service.listLines(param)); + } + + @GetMapping("/lines/{lineId}") + @RequirePermission(menu = "APPROVAL", perm = "READ") + public ApiResponse detailLine(@PathVariable Long lineId) { + return ApiResponse.ok(service.detailLine(lineId)); + } + + @GetMapping("/lines/{lineId}/steps") + @RequirePermission(menu = "APPROVAL", perm = "READ") + public ApiResponse> stepsOfLine(@PathVariable Long lineId) { + return ApiResponse.ok(service.stepsOfLine(lineId)); + } + + @PostMapping("/lines") + @RequirePermission(menu = "APPROVAL", perm = "CREATE") + @DataChangeLog(menu = "APPROVAL", table = "approval_line") + public ApiResponse createLine(@Valid @RequestBody ApprovalLineSaveReq req) { + return ApiResponse.ok(service.createLine(req)); + } + + @PutMapping("/lines/{lineId}") + @RequirePermission(menu = "APPROVAL", perm = "UPDATE") + @DataChangeLog(menu = "APPROVAL", table = "approval_line") + public ApiResponse updateLine(@PathVariable Long lineId, + @Valid @RequestBody ApprovalLineSaveReq req) { + service.updateLine(lineId, req); + return ApiResponse.ok(); + } + + @PostMapping("/lines/{lineId}/steps") + @RequirePermission(menu = "APPROVAL", perm = "UPDATE") + @DataChangeLog(menu = "APPROVAL", table = "approval_line_step") + public ApiResponse addStep(@PathVariable Long lineId, + @Valid @RequestBody ApprovalLineStepSaveReq req) { + req.setLineId(lineId); + service.addStep(req); + return ApiResponse.ok(); + } + + // ── 결재 요청 ────────────────────────────────────────────────────────────── + + @GetMapping("/requests") + @RequirePermission(menu = "APPROVAL", perm = "READ") + public ApiResponse> listRequests( + @ModelAttribute ApprovalRequestSearchParam param) { + return ApiResponse.ok(service.listRequests(param)); + } + + @GetMapping("/requests/{requestId}") + @RequirePermission(menu = "APPROVAL", perm = "READ") + public ApiResponse detailRequest(@PathVariable Long requestId) { + return ApiResponse.ok(service.detailRequest(requestId)); + } + + @GetMapping("/requests/{requestId}/history") + @RequirePermission(menu = "APPROVAL", perm = "READ") + public ApiResponse> historyOfRequest(@PathVariable Long requestId) { + return ApiResponse.ok(service.historyOfRequest(requestId)); + } + + @PostMapping("/requests") + @RequirePermission(menu = "APPROVAL", perm = "CREATE") + @DataChangeLog(menu = "APPROVAL", table = "approval_request") + public ApiResponse createRequest(@Valid @RequestBody ApprovalRequestSaveReq req) { + return ApiResponse.ok(service.createRequest(req)); + } + + @PostMapping("/requests/{requestId}/advance") + @RequirePermission(menu = "APPROVAL", perm = "APPROVE") + @DataChangeLog(menu = "APPROVAL", table = "approval_request") + public ApiResponse advance(@PathVariable Long requestId, + @Valid @RequestBody AdvanceReq req) { + service.advance(requestId, req.getApproverId(), req.getAction(), req.getComment()); + return ApiResponse.ok(); + } + + @PostMapping("/requests/{requestId}/cancel") + @RequirePermission(menu = "APPROVAL", perm = "UPDATE") + @DataChangeLog(menu = "APPROVAL", table = "approval_request") + public ApiResponse cancel(@PathVariable Long requestId) { + service.cancel(requestId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/attachment/AttachmentController.java b/ga-api/src/main/java/com/ga/api/controller/attachment/AttachmentController.java new file mode 100644 index 0000000..3afc5ac --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/attachment/AttachmentController.java @@ -0,0 +1,85 @@ +package com.ga.api.controller.attachment; + +import com.ga.api.service.attachment.AttachmentService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.attachment.AttachmentResp; +import com.ga.core.vo.attachment.AttachmentSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.util.MimeTypeUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +@Tag(name = "첨부파일") +@RestController +@RequestMapping("/api/attachments") +@RequiredArgsConstructor +public class AttachmentController { + + private final AttachmentService service; + + @GetMapping + @RequirePermission(menu = "ATTACHMENT", perm = "READ") + public ApiResponse> list(@ModelAttribute AttachmentSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/by-ref") + @RequirePermission(menu = "ATTACHMENT", perm = "READ") + public ApiResponse> listByRef(@RequestParam String refType, + @RequestParam Long refId) { + return ApiResponse.ok(service.listByRef(refType, refId)); + } + + @GetMapping("/{attachmentId}/meta") + @RequirePermission(menu = "ATTACHMENT", perm = "READ") + public ApiResponse detail(@PathVariable Long attachmentId) { + return ApiResponse.ok(service.detail(attachmentId)); + } + + @PostMapping(consumes = "multipart/form-data") + @RequirePermission(menu = "ATTACHMENT", perm = "CREATE") + @DataChangeLog(menu = "ATTACHMENT", table = "attachment") + public ApiResponse upload(@RequestParam("file") MultipartFile file, + @RequestParam String refType, + @RequestParam Long refId) { + return ApiResponse.ok(service.upload(file, refType, refId)); + } + + @GetMapping("/{attachmentId}/download") + @RequirePermission(menu = "ATTACHMENT", perm = "READ") + public void download(@PathVariable Long attachmentId, HttpServletResponse response) throws IOException { + AttachmentResp meta = service.detail(attachmentId); + Path filePath = service.resolveFilePath(attachmentId); + + response.setContentType(meta.getMimeType() != null ? meta.getMimeType() : MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE); + String encodedName = URLEncoder.encode(meta.getFileName(), StandardCharsets.UTF_8).replace("+", "%20"); + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedName); + response.setContentLengthLong(meta.getFileSize() != null ? meta.getFileSize() : 0L); + + try (OutputStream out = response.getOutputStream()) { + Files.copy(filePath, out); + } + } + + @DeleteMapping("/{attachmentId}") + @RequirePermission(menu = "ATTACHMENT", perm = "DELETE") + @DataChangeLog(menu = "ATTACHMENT", table = "attachment") + public ApiResponse delete(@PathVariable Long attachmentId) { + service.delete(attachmentId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/CollectionCommissionController.java b/ga-api/src/main/java/com/ga/api/controller/commission/CollectionCommissionController.java new file mode 100644 index 0000000..d8578b9 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/CollectionCommissionController.java @@ -0,0 +1,82 @@ +package com.ga.api.controller.commission; + +import com.ga.api.service.commission.CollectionCommissionService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.commission.*; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "수금수수료") +@RestController +@RequestMapping("/api/collection-commissions") +@RequiredArgsConstructor +public class CollectionCommissionController { + + private final CollectionCommissionService service; + + // ── Rule ────────────────────────────────────────────────────────────────── + + @GetMapping("/rules") + @RequirePermission(menu = "COLLECT_COMM", perm = "READ") + public ApiResponse> listRules( + @ModelAttribute CollectionCommissionRuleSearchParam param) { + return ApiResponse.ok(service.listRules(param)); + } + + @GetMapping("/rules/{ruleId}") + @RequirePermission(menu = "COLLECT_COMM", perm = "READ") + public ApiResponse detailRule(@PathVariable Long ruleId) { + return ApiResponse.ok(service.detailRule(ruleId)); + } + + @PostMapping("/rules") + @RequirePermission(menu = "COLLECT_COMM", perm = "CREATE") + @DataChangeLog(menu = "COLLECT_COMM", table = "collection_commission_rule") + public ApiResponse createRule(@Valid @RequestBody CollectionCommissionRuleSaveReq req) { + return ApiResponse.ok(service.createRule(req)); + } + + @PutMapping("/rules/{ruleId}") + @RequirePermission(menu = "COLLECT_COMM", perm = "UPDATE") + @DataChangeLog(menu = "COLLECT_COMM", table = "collection_commission_rule") + public ApiResponse updateRule(@PathVariable Long ruleId, + @Valid @RequestBody CollectionCommissionRuleSaveReq req) { + service.updateRule(ruleId, req); + return ApiResponse.ok(); + } + + @DeleteMapping("/rules/{ruleId}") + @RequirePermission(menu = "COLLECT_COMM", perm = "DELETE") + @DataChangeLog(menu = "COLLECT_COMM", table = "collection_commission_rule") + public ApiResponse deleteRule(@PathVariable Long ruleId) { + service.deleteRule(ruleId); + return ApiResponse.ok(); + } + + // ── Ledger ──────────────────────────────────────────────────────────────── + + @GetMapping("/ledgers") + @RequirePermission(menu = "COLLECT_COMM", perm = "READ") + public ApiResponse> listLedgers( + @ModelAttribute CollectionCommissionLedgerSearchParam param) { + return ApiResponse.ok(service.listLedgers(param)); + } + + @GetMapping("/ledgers/{ledgerId}") + @RequirePermission(menu = "COLLECT_COMM", perm = "READ") + public ApiResponse detailLedger(@PathVariable Long ledgerId) { + return ApiResponse.ok(service.detailLedger(ledgerId)); + } + + @PostMapping("/ledgers") + @RequirePermission(menu = "COLLECT_COMM", perm = "CREATE") + @DataChangeLog(menu = "COLLECT_COMM", table = "collection_commission_ledger") + public ApiResponse createLedger(@Valid @RequestBody CollectionCommissionLedgerSaveReq req) { + return ApiResponse.ok(service.createLedger(req)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/CommissionMasterController.java b/ga-api/src/main/java/com/ga/api/controller/commission/CommissionMasterController.java new file mode 100644 index 0000000..3e50482 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/CommissionMasterController.java @@ -0,0 +1,66 @@ +package com.ga.api.controller.commission; + +import com.ga.api.service.commission.CommissionMasterService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.commission.CommissionMasterResp; +import com.ga.core.vo.commission.CommissionMasterSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@Tag(name = "수수료 마스터") +@RestController +@RequestMapping("/api/commissions") +@RequiredArgsConstructor +public class CommissionMasterController { + + private final CommissionMasterService service; + + @GetMapping + @RequirePermission(menu = "COMM_MASTER", perm = "READ") + public ApiResponse> list(@ModelAttribute CommissionMasterSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{masterId}") + @RequirePermission(menu = "COMM_MASTER", perm = "READ") + public ApiResponse detail(@PathVariable Long masterId) { + return ApiResponse.ok(service.detail(masterId)); + } + + @GetMapping("/by-agent") + @RequirePermission(menu = "COMM_MASTER", perm = "READ") + public ApiResponse> byAgent(@RequestParam Long agentId, + @RequestParam String settleMonth) { + return ApiResponse.ok(service.listByAgentAndMonth(agentId, settleMonth)); + } + + @GetMapping("/aggregated") + @RequirePermission(menu = "COMM_MASTER", perm = "READ") + public ApiResponse> aggregated(@RequestParam String settleMonth) { + return ApiResponse.ok(service.aggregatedByMonth(settleMonth)); + } + + @PutMapping("/{masterId}/status") + @RequirePermission(menu = "COMM_MASTER", perm = "UPDATE") + @DataChangeLog(menu = "COMM_MASTER", table = "commission_master") + public ApiResponse updateStatus(@PathVariable Long masterId, + @RequestBody Map body) { + service.updateStatus(masterId, body.get("status")); + return ApiResponse.ok(); + } + + @DeleteMapping("/{masterId}") + @RequirePermission(menu = "COMM_MASTER", perm = "DELETE") + @DataChangeLog(menu = "COMM_MASTER", table = "commission_master") + public ApiResponse delete(@PathVariable Long masterId) { + service.delete(masterId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/OverrideLedgerController.java b/ga-api/src/main/java/com/ga/api/controller/commission/OverrideLedgerController.java new file mode 100644 index 0000000..76b3649 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/OverrideLedgerController.java @@ -0,0 +1,46 @@ +package com.ga.api.controller.commission; + +import com.ga.api.service.commission.OverrideLedgerService; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.commission.*; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "오버라이드 원장") +@RestController +@RequestMapping("/api/override-ledgers") +@RequiredArgsConstructor +public class OverrideLedgerController { + + private final OverrideLedgerService service; + + @GetMapping + @RequirePermission(menu = "OVERRIDE_LEDGER", perm = "READ") + public ApiResponse> list(@ModelAttribute OverrideLedgerSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{ledgerId}") + @RequirePermission(menu = "OVERRIDE_LEDGER", perm = "READ") + public ApiResponse detail(@PathVariable Long ledgerId) { + return ApiResponse.ok(service.detail(ledgerId)); + } + + @GetMapping("/summary") + @RequirePermission(menu = "OVERRIDE_LEDGER", perm = "READ") + public ApiResponse> summaryList(@ModelAttribute OverrideSummarySearchParam param) { + return ApiResponse.ok(service.summaryList(param)); + } + + @GetMapping("/summary/by-agent") + @RequirePermission(menu = "OVERRIDE_LEDGER", perm = "READ") + public ApiResponse summaryByAgent(@RequestParam Long toAgentId, + @RequestParam String settleMonth) { + return ApiResponse.ok(service.summaryByAgentAndMonth(toAgentId, settleMonth)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/PersistencyBonusController.java b/ga-api/src/main/java/com/ga/api/controller/commission/PersistencyBonusController.java new file mode 100644 index 0000000..c176c12 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/PersistencyBonusController.java @@ -0,0 +1,79 @@ +package com.ga.api.controller.commission; + +import com.ga.api.service.commission.PersistencyBonusService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.commission.*; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "유지보수수당") +@RestController +@RequestMapping("/api/persistency-bonus") +@RequiredArgsConstructor +public class PersistencyBonusController { + + private final PersistencyBonusService service; + + @GetMapping("/rules") + @RequirePermission(menu = "PERSIST_BONUS", perm = "READ") + public ApiResponse> listRules( + @ModelAttribute PersistencyBonusRuleSearchParam param) { + return ApiResponse.ok(service.listRules(param)); + } + + @GetMapping("/rules/{ruleId}") + @RequirePermission(menu = "PERSIST_BONUS", perm = "READ") + public ApiResponse detailRule(@PathVariable Long ruleId) { + return ApiResponse.ok(service.detailRule(ruleId)); + } + + @PostMapping("/rules") + @RequirePermission(menu = "PERSIST_BONUS", perm = "CREATE") + @DataChangeLog(menu = "PERSIST_BONUS", table = "persistency_bonus_rule") + public ApiResponse createRule(@Valid @RequestBody PersistencyBonusRuleSaveReq req) { + return ApiResponse.ok(service.createRule(req)); + } + + @PutMapping("/rules/{ruleId}") + @RequirePermission(menu = "PERSIST_BONUS", perm = "UPDATE") + @DataChangeLog(menu = "PERSIST_BONUS", table = "persistency_bonus_rule") + public ApiResponse updateRule(@PathVariable Long ruleId, + @Valid @RequestBody PersistencyBonusRuleSaveReq req) { + service.updateRule(ruleId, req); + return ApiResponse.ok(); + } + + @DeleteMapping("/rules/{ruleId}") + @RequirePermission(menu = "PERSIST_BONUS", perm = "DELETE") + @DataChangeLog(menu = "PERSIST_BONUS", table = "persistency_bonus_rule") + public ApiResponse deleteRule(@PathVariable Long ruleId) { + service.deleteRule(ruleId); + return ApiResponse.ok(); + } + + @GetMapping("/bonuses") + @RequirePermission(menu = "PERSIST_BONUS", perm = "READ") + public ApiResponse> listBonuses( + @ModelAttribute PersistencyBonusSearchParam param) { + return ApiResponse.ok(service.listBonuses(param)); + } + + @GetMapping("/bonuses/{bonusId}") + @RequirePermission(menu = "PERSIST_BONUS", perm = "READ") + public ApiResponse detailBonus(@PathVariable Long bonusId) { + return ApiResponse.ok(service.detailBonus(bonusId)); + } + + @PostMapping("/bonuses") + @RequirePermission(menu = "PERSIST_BONUS", perm = "CREATE") + @DataChangeLog(menu = "PERSIST_BONUS", table = "persistency_bonus") + public ApiResponse upsertBonus(@Valid @RequestBody PersistencyBonusSaveReq req) { + service.upsertBonus(req); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/ReinstatementCommissionController.java b/ga-api/src/main/java/com/ga/api/controller/commission/ReinstatementCommissionController.java new file mode 100644 index 0000000..fb8bba8 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/ReinstatementCommissionController.java @@ -0,0 +1,78 @@ +package com.ga.api.controller.commission; + +import com.ga.api.service.commission.ReinstatementCommissionService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.commission.*; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "부활수수료") +@RestController +@RequestMapping("/api/reinstatement-commissions") +@RequiredArgsConstructor +public class ReinstatementCommissionController { + + private final ReinstatementCommissionService service; + + @GetMapping("/rules") + @RequirePermission(menu = "REINSTATE_COMM", perm = "READ") + public ApiResponse> listRules( + @ModelAttribute ReinstatementCommissionRuleSearchParam param) { + return ApiResponse.ok(service.listRules(param)); + } + + @GetMapping("/rules/{ruleId}") + @RequirePermission(menu = "REINSTATE_COMM", perm = "READ") + public ApiResponse detailRule(@PathVariable Long ruleId) { + return ApiResponse.ok(service.detailRule(ruleId)); + } + + @PostMapping("/rules") + @RequirePermission(menu = "REINSTATE_COMM", perm = "CREATE") + @DataChangeLog(menu = "REINSTATE_COMM", table = "reinstatement_commission_rule") + public ApiResponse createRule(@Valid @RequestBody ReinstatementCommissionRuleSaveReq req) { + return ApiResponse.ok(service.createRule(req)); + } + + @PutMapping("/rules/{ruleId}") + @RequirePermission(menu = "REINSTATE_COMM", perm = "UPDATE") + @DataChangeLog(menu = "REINSTATE_COMM", table = "reinstatement_commission_rule") + public ApiResponse updateRule(@PathVariable Long ruleId, + @Valid @RequestBody ReinstatementCommissionRuleSaveReq req) { + service.updateRule(ruleId, req); + return ApiResponse.ok(); + } + + @DeleteMapping("/rules/{ruleId}") + @RequirePermission(menu = "REINSTATE_COMM", perm = "DELETE") + @DataChangeLog(menu = "REINSTATE_COMM", table = "reinstatement_commission_rule") + public ApiResponse deleteRule(@PathVariable Long ruleId) { + service.deleteRule(ruleId); + return ApiResponse.ok(); + } + + @GetMapping("/ledgers") + @RequirePermission(menu = "REINSTATE_COMM", perm = "READ") + public ApiResponse> listLedgers( + @ModelAttribute ReinstatementCommissionLedgerSearchParam param) { + return ApiResponse.ok(service.listLedgers(param)); + } + + @GetMapping("/ledgers/{ledgerId}") + @RequirePermission(menu = "REINSTATE_COMM", perm = "READ") + public ApiResponse detailLedger(@PathVariable Long ledgerId) { + return ApiResponse.ok(service.detailLedger(ledgerId)); + } + + @PostMapping("/ledgers") + @RequirePermission(menu = "REINSTATE_COMM", perm = "CREATE") + @DataChangeLog(menu = "REINSTATE_COMM", table = "reinstatement_commission_ledger") + public ApiResponse createLedger(@Valid @RequestBody ReinstatementCommissionLedgerSaveReq req) { + return ApiResponse.ok(service.createLedger(req)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/RenewalCommissionController.java b/ga-api/src/main/java/com/ga/api/controller/commission/RenewalCommissionController.java new file mode 100644 index 0000000..781c970 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/RenewalCommissionController.java @@ -0,0 +1,78 @@ +package com.ga.api.controller.commission; + +import com.ga.api.service.commission.RenewalCommissionService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.commission.*; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "갱신수수료") +@RestController +@RequestMapping("/api/renewal-commissions") +@RequiredArgsConstructor +public class RenewalCommissionController { + + private final RenewalCommissionService service; + + @GetMapping("/rules") + @RequirePermission(menu = "RENEWAL_COMM", perm = "READ") + public ApiResponse> listRules( + @ModelAttribute RenewalCommissionRuleSearchParam param) { + return ApiResponse.ok(service.listRules(param)); + } + + @GetMapping("/rules/{ruleId}") + @RequirePermission(menu = "RENEWAL_COMM", perm = "READ") + public ApiResponse detailRule(@PathVariable Long ruleId) { + return ApiResponse.ok(service.detailRule(ruleId)); + } + + @PostMapping("/rules") + @RequirePermission(menu = "RENEWAL_COMM", perm = "CREATE") + @DataChangeLog(menu = "RENEWAL_COMM", table = "renewal_commission_rule") + public ApiResponse createRule(@Valid @RequestBody RenewalCommissionRuleSaveReq req) { + return ApiResponse.ok(service.createRule(req)); + } + + @PutMapping("/rules/{ruleId}") + @RequirePermission(menu = "RENEWAL_COMM", perm = "UPDATE") + @DataChangeLog(menu = "RENEWAL_COMM", table = "renewal_commission_rule") + public ApiResponse updateRule(@PathVariable Long ruleId, + @Valid @RequestBody RenewalCommissionRuleSaveReq req) { + service.updateRule(ruleId, req); + return ApiResponse.ok(); + } + + @DeleteMapping("/rules/{ruleId}") + @RequirePermission(menu = "RENEWAL_COMM", perm = "DELETE") + @DataChangeLog(menu = "RENEWAL_COMM", table = "renewal_commission_rule") + public ApiResponse deleteRule(@PathVariable Long ruleId) { + service.deleteRule(ruleId); + return ApiResponse.ok(); + } + + @GetMapping("/ledgers") + @RequirePermission(menu = "RENEWAL_COMM", perm = "READ") + public ApiResponse> listLedgers( + @ModelAttribute RenewalCommissionLedgerSearchParam param) { + return ApiResponse.ok(service.listLedgers(param)); + } + + @GetMapping("/ledgers/{ledgerId}") + @RequirePermission(menu = "RENEWAL_COMM", perm = "READ") + public ApiResponse detailLedger(@PathVariable Long ledgerId) { + return ApiResponse.ok(service.detailLedger(ledgerId)); + } + + @PostMapping("/ledgers") + @RequirePermission(menu = "RENEWAL_COMM", perm = "CREATE") + @DataChangeLog(menu = "RENEWAL_COMM", table = "renewal_commission_ledger") + public ApiResponse createLedger(@Valid @RequestBody RenewalCommissionLedgerSaveReq req) { + return ApiResponse.ok(service.createLedger(req)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/complaint/ComplaintController.java b/ga-api/src/main/java/com/ga/api/controller/complaint/ComplaintController.java new file mode 100644 index 0000000..ffff2f4 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/complaint/ComplaintController.java @@ -0,0 +1,70 @@ +package com.ga.api.controller.complaint; + +import com.ga.api.service.complaint.ComplaintService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.complaint.ComplaintResp; +import com.ga.core.vo.complaint.ComplaintSaveReq; +import com.ga.core.vo.complaint.ComplaintSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Tag(name = "민원 관리") +@RestController +@RequestMapping("/api/complaints") +@RequiredArgsConstructor +public class ComplaintController { + + private final ComplaintService service; + + @GetMapping + @RequirePermission(menu = "COMPLAINT", perm = "READ") + public ApiResponse> list(@ModelAttribute ComplaintSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{complaintId}") + @RequirePermission(menu = "COMPLAINT", perm = "READ") + public ApiResponse detail(@PathVariable Long complaintId) { + return ApiResponse.ok(service.detail(complaintId)); + } + + @PostMapping + @RequirePermission(menu = "COMPLAINT", perm = "CREATE") + @DataChangeLog(menu = "COMPLAINT", table = "complaint") + public ApiResponse create(@Valid @RequestBody ComplaintSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PutMapping("/{complaintId}") + @RequirePermission(menu = "COMPLAINT", perm = "UPDATE") + @DataChangeLog(menu = "COMPLAINT", table = "complaint") + public ApiResponse update(@PathVariable Long complaintId, + @Valid @RequestBody ComplaintSaveReq req) { + service.update(complaintId, req); + return ApiResponse.ok(); + } + + @PostMapping("/{complaintId}/close") + @RequirePermission(menu = "COMPLAINT", perm = "UPDATE") + @DataChangeLog(menu = "COMPLAINT", table = "complaint") + public ApiResponse close(@PathVariable Long complaintId, + @RequestBody Map body) { + service.close(complaintId, body.get("resolution")); + return ApiResponse.ok(); + } + + @DeleteMapping("/{complaintId}") + @RequirePermission(menu = "COMPLAINT", perm = "DELETE") + @DataChangeLog(menu = "COMPLAINT", table = "complaint") + public ApiResponse delete(@PathVariable Long complaintId) { + service.delete(complaintId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/contract/AgentContractController.java b/ga-api/src/main/java/com/ga/api/controller/contract/AgentContractController.java new file mode 100644 index 0000000..f42ffe2 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/contract/AgentContractController.java @@ -0,0 +1,70 @@ +package com.ga.api.controller.contract; + +import com.ga.api.service.contract.AgentContractService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.contract.AgentContractResp; +import com.ga.core.vo.contract.AgentContractSaveReq; +import com.ga.core.vo.contract.AgentContractSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Tag(name = "위촉계약 관리") +@RestController +@RequestMapping("/api/agent-contracts") +@RequiredArgsConstructor +public class AgentContractController { + + private final AgentContractService service; + + @GetMapping + @RequirePermission(menu = "AGENT_CONTRACT", perm = "READ") + public ApiResponse> list(@ModelAttribute AgentContractSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{contractId}") + @RequirePermission(menu = "AGENT_CONTRACT", perm = "READ") + public ApiResponse detail(@PathVariable Long contractId) { + return ApiResponse.ok(service.detail(contractId)); + } + + @PostMapping + @RequirePermission(menu = "AGENT_CONTRACT", perm = "CREATE") + @DataChangeLog(menu = "AGENT_CONTRACT", table = "agent_contract") + public ApiResponse create(@Valid @RequestBody AgentContractSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PutMapping("/{contractId}") + @RequirePermission(menu = "AGENT_CONTRACT", perm = "UPDATE") + @DataChangeLog(menu = "AGENT_CONTRACT", table = "agent_contract") + public ApiResponse update(@PathVariable Long contractId, + @Valid @RequestBody AgentContractSaveReq req) { + service.update(contractId, req); + return ApiResponse.ok(); + } + + @PostMapping("/{contractId}/terminate") + @RequirePermission(menu = "AGENT_CONTRACT", perm = "UPDATE") + @DataChangeLog(menu = "AGENT_CONTRACT", table = "agent_contract") + public ApiResponse terminate(@PathVariable Long contractId, + @RequestBody Map body) { + service.terminate(contractId, body.get("reason")); + return ApiResponse.ok(); + } + + @DeleteMapping("/{contractId}") + @RequirePermission(menu = "AGENT_CONTRACT", perm = "DELETE") + @DataChangeLog(menu = "AGENT_CONTRACT", table = "agent_contract") + public ApiResponse delete(@PathVariable Long contractId) { + service.delete(contractId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/endorse/ContractEndorsementController.java b/ga-api/src/main/java/com/ga/api/controller/endorse/ContractEndorsementController.java new file mode 100644 index 0000000..ec9b783 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/endorse/ContractEndorsementController.java @@ -0,0 +1,59 @@ +package com.ga.api.controller.endorse; + +import com.ga.api.service.endorse.ContractEndorsementService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.endorse.ContractEndorsementResp; +import com.ga.core.vo.endorse.ContractEndorsementSaveReq; +import com.ga.core.vo.endorse.ContractEndorsementSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "계약 변경") +@RestController +@RequestMapping("/api/contract-endorsements") +@RequiredArgsConstructor +public class ContractEndorsementController { + + private final ContractEndorsementService service; + + @GetMapping + @RequirePermission(menu = "ENDORSE", perm = "READ") + public ApiResponse> list( + @ModelAttribute ContractEndorsementSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{endorsementId}") + @RequirePermission(menu = "ENDORSE", perm = "READ") + public ApiResponse detail(@PathVariable Long endorsementId) { + return ApiResponse.ok(service.detail(endorsementId)); + } + + @PostMapping + @RequirePermission(menu = "ENDORSE", perm = "CREATE") + @DataChangeLog(menu = "ENDORSE", table = "contract_endorsement") + public ApiResponse create(@Valid @RequestBody ContractEndorsementSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PostMapping("/{endorsementId}/process") + @RequirePermission(menu = "ENDORSE", perm = "UPDATE") + @DataChangeLog(menu = "ENDORSE", table = "contract_endorsement") + public ApiResponse process(@PathVariable Long endorsementId) { + service.process(endorsementId); + return ApiResponse.ok(); + } + + @DeleteMapping("/{endorsementId}") + @RequirePermission(menu = "ENDORSE", perm = "DELETE") + @DataChangeLog(menu = "ENDORSE", table = "contract_endorsement") + public ApiResponse delete(@PathVariable Long endorsementId) { + service.delete(endorsementId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/kpi/KpiController.java b/ga-api/src/main/java/com/ga/api/controller/kpi/KpiController.java new file mode 100644 index 0000000..41c136b --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/kpi/KpiController.java @@ -0,0 +1,50 @@ +package com.ga.api.controller.kpi; + +import com.ga.api.service.kpi.KpiService; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.kpi.KpiCumulativeResp; +import com.ga.core.vo.kpi.KpiMonthlySummaryResp; +import com.ga.core.vo.kpi.KpiOrgSummaryResp; +import com.ga.core.vo.kpi.KpiRetentionResp; +import com.ga.core.vo.kpi.KpiSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "KPI 대시보드") +@RestController +@RequestMapping("/api/kpi") +@RequiredArgsConstructor +public class KpiController { + + private final KpiService service; + + @GetMapping("/monthly-summary") + @RequirePermission(menu = "KPI_MONTHLY", perm = "READ") + public ApiResponse> monthlySummary(@ModelAttribute KpiSearchParam param) { + return ApiResponse.ok(service.monthlySummary(param)); + } + + @GetMapping("/org-summary") + @RequirePermission(menu = "KPI_ORG", perm = "READ") + public ApiResponse> orgSummary(@ModelAttribute KpiSearchParam param) { + return ApiResponse.ok(service.orgSummary(param)); + } + + @GetMapping("/retention") + @RequirePermission(menu = "KPI_RETENTION", perm = "READ") + public ApiResponse> retention(@ModelAttribute KpiSearchParam param) { + return ApiResponse.ok(service.retention(param)); + } + + @GetMapping("/cumulative") + @RequirePermission(menu = "KPI_CUMULATIVE", perm = "READ") + public ApiResponse> cumulative(@ModelAttribute KpiSearchParam param) { + return ApiResponse.ok(service.cumulative(param)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/license/AgentLicenseController.java b/ga-api/src/main/java/com/ga/api/controller/license/AgentLicenseController.java new file mode 100644 index 0000000..454eb0b --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/license/AgentLicenseController.java @@ -0,0 +1,68 @@ +package com.ga.api.controller.license; + +import com.ga.api.service.license.AgentLicenseService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.license.AgentLicenseResp; +import com.ga.core.vo.license.AgentLicenseSaveReq; +import com.ga.core.vo.license.AgentLicenseSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "자격 관리") +@RestController +@RequestMapping("/api/agent-licenses") +@RequiredArgsConstructor +public class AgentLicenseController { + + private final AgentLicenseService service; + + @GetMapping + @RequirePermission(menu = "AGENT_LICENSE", perm = "READ") + public ApiResponse> list(@ModelAttribute AgentLicenseSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{licenseId}") + @RequirePermission(menu = "AGENT_LICENSE", perm = "READ") + public ApiResponse detail(@PathVariable Long licenseId) { + return ApiResponse.ok(service.detail(licenseId)); + } + + @GetMapping("/expiring") + @RequirePermission(menu = "AGENT_LICENSE", perm = "READ") + public ApiResponse> expiring( + @RequestParam(defaultValue = "30") int days) { + return ApiResponse.ok(service.expiring(days)); + } + + @PostMapping + @RequirePermission(menu = "AGENT_LICENSE", perm = "CREATE") + @DataChangeLog(menu = "AGENT_LICENSE", table = "agent_license") + public ApiResponse create(@Valid @RequestBody AgentLicenseSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PutMapping("/{licenseId}") + @RequirePermission(menu = "AGENT_LICENSE", perm = "UPDATE") + @DataChangeLog(menu = "AGENT_LICENSE", table = "agent_license") + public ApiResponse update(@PathVariable Long licenseId, + @Valid @RequestBody AgentLicenseSaveReq req) { + service.update(licenseId, req); + return ApiResponse.ok(); + } + + @DeleteMapping("/{licenseId}") + @RequirePermission(menu = "AGENT_LICENSE", perm = "DELETE") + @DataChangeLog(menu = "AGENT_LICENSE", table = "agent_license") + public ApiResponse delete(@PathVariable Long licenseId) { + service.delete(licenseId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/notice/NoticeController.java b/ga-api/src/main/java/com/ga/api/controller/notice/NoticeController.java new file mode 100644 index 0000000..edd9fb3 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/notice/NoticeController.java @@ -0,0 +1,97 @@ +package com.ga.api.controller.notice; + +import com.ga.api.service.notice.NoticeService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.vo.notice.*; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "공지사항 & 알림") +@RestController +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService service; + + // ── Notice ──────────────────────────────────────────────────────────────── + + @GetMapping("/api/notices") + @RequirePermission(menu = "NOTICE", perm = "READ") + public ApiResponse> list(@ModelAttribute NoticeSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/api/notices/active") + @RequirePermission(menu = "NOTICE", perm = "READ") + public ApiResponse> activeList() { + return ApiResponse.ok(service.activeList()); + } + + @GetMapping("/api/notices/{noticeId}") + @RequirePermission(menu = "NOTICE", perm = "READ") + public ApiResponse detail(@PathVariable Long noticeId) { + return ApiResponse.ok(service.detail(noticeId)); + } + + @PostMapping("/api/notices") + @RequirePermission(menu = "NOTICE", perm = "CREATE") + @DataChangeLog(menu = "NOTICE", table = "notice") + public ApiResponse create(@Valid @RequestBody NoticeSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PutMapping("/api/notices/{noticeId}") + @RequirePermission(menu = "NOTICE", perm = "UPDATE") + @DataChangeLog(menu = "NOTICE", table = "notice") + public ApiResponse update(@PathVariable Long noticeId, + @Valid @RequestBody NoticeSaveReq req) { + service.update(noticeId, req); + return ApiResponse.ok(); + } + + @DeleteMapping("/api/notices/{noticeId}") + @RequirePermission(menu = "NOTICE", perm = "DELETE") + @DataChangeLog(menu = "NOTICE", table = "notice") + public ApiResponse delete(@PathVariable Long noticeId) { + service.delete(noticeId); + return ApiResponse.ok(); + } + + // ── UserNotification ────────────────────────────────────────────────────── + + @GetMapping("/api/user-notifications") + @RequirePermission(menu = "NOTICE", perm = "READ") + public ApiResponse> listNotifications( + @ModelAttribute UserNotificationSearchParam param) { + return ApiResponse.ok(service.listNotifications(param)); + } + + @GetMapping("/api/user-notifications/unread-count") + @RequirePermission(menu = "NOTICE", perm = "READ") + public ApiResponse unreadCount(@RequestParam Long userId) { + return ApiResponse.ok(service.unreadCount(userId)); + } + + @PostMapping("/api/user-notifications/{notificationId}/read") + @RequirePermission(menu = "NOTICE", perm = "READ") + public ApiResponse markRead(@PathVariable Long notificationId, + @RequestParam Long userId) { + service.markRead(notificationId, userId); + return ApiResponse.ok(); + } + + @PostMapping("/api/user-notifications/read-all") + @RequirePermission(menu = "NOTICE", perm = "READ") + public ApiResponse markAllRead(@RequestParam Long userId) { + service.markAllRead(userId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/settle/CarrierReconciliationOutController.java b/ga-api/src/main/java/com/ga/api/controller/settle/CarrierReconciliationOutController.java new file mode 100644 index 0000000..edb2022 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/settle/CarrierReconciliationOutController.java @@ -0,0 +1,50 @@ +package com.ga.api.controller.settle; + +import com.ga.api.service.settle.CarrierReconciliationOutService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.settle.CarrierReconciliationOutResp; +import com.ga.core.vo.settle.CarrierReconciliationOutSaveReq; +import com.ga.core.vo.settle.CarrierReconciliationOutSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "보험사 회신 관리") +@RestController +@RequestMapping("/api/carrier-reconciliation") +@RequiredArgsConstructor +public class CarrierReconciliationOutController { + + private final CarrierReconciliationOutService service; + + @GetMapping + @RequirePermission(menu = "RECON_OUT", perm = "READ") + public ApiResponse> list(@ModelAttribute CarrierReconciliationOutSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{reconOutId}") + @RequirePermission(menu = "RECON_OUT", perm = "READ") + public ApiResponse detail(@PathVariable Long reconOutId) { + return ApiResponse.ok(service.detail(reconOutId)); + } + + @PostMapping + @RequirePermission(menu = "RECON_OUT", perm = "CREATE") + @DataChangeLog(menu = "RECON_OUT", table = "carrier_reconciliation_out") + public ApiResponse create(@Valid @RequestBody CarrierReconciliationOutSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PostMapping("/{reconOutId}/retry") + @RequirePermission(menu = "RECON_OUT", perm = "UPDATE") + @DataChangeLog(menu = "RECON_OUT", table = "carrier_reconciliation_out") + public ApiResponse retry(@PathVariable Long reconOutId) { + service.retry(reconOutId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/settle/CommissionDisputeController.java b/ga-api/src/main/java/com/ga/api/controller/settle/CommissionDisputeController.java new file mode 100644 index 0000000..544c221 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/settle/CommissionDisputeController.java @@ -0,0 +1,74 @@ +package com.ga.api.controller.settle; + +import com.ga.api.service.settle.CommissionDisputeService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.settle.CommissionDisputeResp; +import com.ga.core.vo.settle.CommissionDisputeSaveReq; +import com.ga.core.vo.settle.CommissionDisputeSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.Map; + +@Tag(name = "수수료 이의신청 관리") +@RestController +@RequestMapping("/api/disputes") +@RequiredArgsConstructor +public class CommissionDisputeController { + + private final CommissionDisputeService service; + + @GetMapping + @RequirePermission(menu = "DISPUTE", perm = "READ") + public ApiResponse> list(@ModelAttribute CommissionDisputeSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{disputeId}") + @RequirePermission(menu = "DISPUTE", perm = "READ") + public ApiResponse detail(@PathVariable Long disputeId) { + return ApiResponse.ok(service.detail(disputeId)); + } + + @PostMapping + @RequirePermission(menu = "DISPUTE", perm = "CREATE") + @DataChangeLog(menu = "DISPUTE", table = "commission_dispute") + public ApiResponse create(@Valid @RequestBody CommissionDisputeSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PostMapping("/{disputeId}/review") + @RequirePermission(menu = "DISPUTE", perm = "UPDATE") + @DataChangeLog(menu = "DISPUTE", table = "commission_dispute") + public ApiResponse startReview(@PathVariable Long disputeId) { + service.startReview(disputeId); + return ApiResponse.ok(); + } + + @PostMapping("/{disputeId}/approve") + @RequirePermission(menu = "DISPUTE", perm = "APPROVE") + @DataChangeLog(menu = "DISPUTE", table = "commission_dispute") + public ApiResponse approve(@PathVariable Long disputeId, @RequestBody Map body) { + String resolution = (String) body.get("resolution"); + BigDecimal adjustmentAmount = body.get("adjustmentAmount") != null + ? new BigDecimal(body.get("adjustmentAmount").toString()) : null; + Long correctionId = body.get("correctionId") != null + ? Long.valueOf(body.get("correctionId").toString()) : null; + service.approve(disputeId, resolution, adjustmentAmount, correctionId); + return ApiResponse.ok(); + } + + @PostMapping("/{disputeId}/reject") + @RequirePermission(menu = "DISPUTE", perm = "APPROVE") + @DataChangeLog(menu = "DISPUTE", table = "commission_dispute") + public ApiResponse reject(@PathVariable Long disputeId, @RequestBody Map body) { + service.reject(disputeId, body.get("resolution")); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/settle/IncentivePaymentController.java b/ga-api/src/main/java/com/ga/api/controller/settle/IncentivePaymentController.java new file mode 100644 index 0000000..d425180 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/settle/IncentivePaymentController.java @@ -0,0 +1,60 @@ +package com.ga.api.controller.settle; + +import com.ga.api.service.settle.IncentivePaymentService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.settle.IncentivePaymentResp; +import com.ga.core.vo.settle.IncentivePaymentSaveReq; +import com.ga.core.vo.settle.IncentivePaymentSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Tag(name = "시책 지급 관리") +@RestController +@RequestMapping("/api/incentive-payments") +@RequiredArgsConstructor +public class IncentivePaymentController { + + private final IncentivePaymentService service; + + @GetMapping + @RequirePermission(menu = "INCENTIVE_PAY", perm = "READ") + public ApiResponse> list(@ModelAttribute IncentivePaymentSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{incentivePayId}") + @RequirePermission(menu = "INCENTIVE_PAY", perm = "READ") + public ApiResponse detail(@PathVariable Long incentivePayId) { + return ApiResponse.ok(service.detail(incentivePayId)); + } + + @PostMapping + @RequirePermission(menu = "INCENTIVE_PAY", perm = "CREATE") + @DataChangeLog(menu = "INCENTIVE_PAY", table = "incentive_payment") + public ApiResponse create(@Valid @RequestBody IncentivePaymentSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PostMapping("/{incentivePayId}/confirm") + @RequirePermission(menu = "INCENTIVE_PAY", perm = "APPROVE") + @DataChangeLog(menu = "INCENTIVE_PAY", table = "incentive_payment") + public ApiResponse confirm(@PathVariable Long incentivePayId) { + service.confirm(incentivePayId); + return ApiResponse.ok(); + } + + @PostMapping("/{incentivePayId}/cancel") + @RequirePermission(menu = "INCENTIVE_PAY", perm = "UPDATE") + @DataChangeLog(menu = "INCENTIVE_PAY", table = "incentive_payment") + public ApiResponse cancel(@PathVariable Long incentivePayId, @RequestBody Map body) { + service.cancel(incentivePayId, body.get("cancelReason")); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/settle/IncentiveProgramController.java b/ga-api/src/main/java/com/ga/api/controller/settle/IncentiveProgramController.java new file mode 100644 index 0000000..b21183b --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/settle/IncentiveProgramController.java @@ -0,0 +1,60 @@ +package com.ga.api.controller.settle; + +import com.ga.api.service.settle.IncentiveProgramService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.settle.IncentiveProgramResp; +import com.ga.core.vo.settle.IncentiveProgramSaveReq; +import com.ga.core.vo.settle.IncentiveProgramSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Tag(name = "시책 프로그램 관리") +@RestController +@RequestMapping("/api/incentive-programs") +@RequiredArgsConstructor +public class IncentiveProgramController { + + private final IncentiveProgramService service; + + @GetMapping + @RequirePermission(menu = "INCENTIVE_PROG", perm = "READ") + public ApiResponse> list(@ModelAttribute IncentiveProgramSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{programId}") + @RequirePermission(menu = "INCENTIVE_PROG", perm = "READ") + public ApiResponse detail(@PathVariable Long programId) { + return ApiResponse.ok(service.detail(programId)); + } + + @PostMapping + @RequirePermission(menu = "INCENTIVE_PROG", perm = "CREATE") + @DataChangeLog(menu = "INCENTIVE_PROG", table = "incentive_program") + public ApiResponse create(@Valid @RequestBody IncentiveProgramSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PutMapping("/{programId}") + @RequirePermission(menu = "INCENTIVE_PROG", perm = "UPDATE") + @DataChangeLog(menu = "INCENTIVE_PROG", table = "incentive_program") + public ApiResponse update(@PathVariable Long programId, @Valid @RequestBody IncentiveProgramSaveReq req) { + service.update(programId, req); + return ApiResponse.ok(); + } + + @PutMapping("/{programId}/active") + @RequirePermission(menu = "INCENTIVE_PROG", perm = "UPDATE") + @DataChangeLog(menu = "INCENTIVE_PROG", table = "incentive_program") + public ApiResponse toggleActive(@PathVariable Long programId, @RequestBody Map body) { + service.toggleActive(programId, Boolean.TRUE.equals(body.get("isActive"))); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/settle/PeriodCloseController.java b/ga-api/src/main/java/com/ga/api/controller/settle/PeriodCloseController.java new file mode 100644 index 0000000..b2f9717 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/settle/PeriodCloseController.java @@ -0,0 +1,53 @@ +package com.ga.api.controller.settle; + +import com.ga.api.service.settle.PeriodCloseService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.settle.PeriodCloseResp; +import com.ga.core.vo.settle.PeriodCloseSaveReq; +import com.ga.core.vo.settle.PeriodCloseSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Tag(name = "정산 마감 관리") +@RestController +@RequestMapping("/api/period-close") +@RequiredArgsConstructor +public class PeriodCloseController { + + private final PeriodCloseService service; + + @GetMapping + @RequirePermission(menu = "PERIOD_CLOSE", perm = "READ") + public ApiResponse> list(@ModelAttribute PeriodCloseSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{closeId}") + @RequirePermission(menu = "PERIOD_CLOSE", perm = "READ") + public ApiResponse detail(@PathVariable Long closeId) { + return ApiResponse.ok(service.detail(closeId)); + } + + @PostMapping + @RequirePermission(menu = "PERIOD_CLOSE", perm = "CREATE") + @DataChangeLog(menu = "PERIOD_CLOSE", table = "period_close") + public ApiResponse close(@Valid @RequestBody PeriodCloseSaveReq req) { + service.close(req); + return ApiResponse.ok(); + } + + @PostMapping("/{closeId}/reopen") + @RequirePermission(menu = "PERIOD_CLOSE", perm = "UPDATE") + @DataChangeLog(menu = "PERIOD_CLOSE", table = "period_close") + public ApiResponse reopen(@PathVariable Long closeId, @RequestBody Map body) { + service.reopen(closeId, body.get("reopenReason")); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/settle/SettleController.java b/ga-api/src/main/java/com/ga/api/controller/settle/SettleController.java index cd063f8..f27abd6 100644 --- a/ga-api/src/main/java/com/ga/api/controller/settle/SettleController.java +++ b/ga-api/src/main/java/com/ga/api/controller/settle/SettleController.java @@ -59,7 +59,7 @@ public class SettleController { @RequirePermission(menu = "SETTLE_LIST", perm = "APPROVE") @DataChangeLog(menu = "SETTLE_LIST", table = "settle_master") public ApiResponse hold(@PathVariable Long settleId, @RequestBody Map body) { - service.hold(settleId, body.get("reason")); + service.hold(settleId, body.get("reason"), body.get("reasonCode")); return ApiResponse.ok(); } diff --git a/ga-api/src/main/java/com/ga/api/controller/settle/SettleCorrectionController.java b/ga-api/src/main/java/com/ga/api/controller/settle/SettleCorrectionController.java new file mode 100644 index 0000000..9bd9e79 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/settle/SettleCorrectionController.java @@ -0,0 +1,67 @@ +package com.ga.api.controller.settle; + +import com.ga.api.service.settle.SettleCorrectionService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.settle.SettleCorrectionResp; +import com.ga.core.vo.settle.SettleCorrectionSaveReq; +import com.ga.core.vo.settle.SettleCorrectionSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@Tag(name = "정산 정정 관리") +@RestController +@RequestMapping("/api/settle-corrections") +@RequiredArgsConstructor +public class SettleCorrectionController { + + private final SettleCorrectionService service; + + @GetMapping + @RequirePermission(menu = "SETTLE_CORR", perm = "READ") + public ApiResponse> list(@ModelAttribute SettleCorrectionSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{correctionId}") + @RequirePermission(menu = "SETTLE_CORR", perm = "READ") + public ApiResponse detail(@PathVariable Long correctionId) { + return ApiResponse.ok(service.detail(correctionId)); + } + + @GetMapping("/by-settle/{originalSettleId}") + @RequirePermission(menu = "SETTLE_CORR", perm = "READ") + public ApiResponse> listByOriginalSettle(@PathVariable Long originalSettleId) { + return ApiResponse.ok(service.listByOriginalSettle(originalSettleId)); + } + + @PostMapping + @RequirePermission(menu = "SETTLE_CORR", perm = "CREATE") + @DataChangeLog(menu = "SETTLE_CORR", table = "settle_correction") + public ApiResponse create(@Valid @RequestBody SettleCorrectionSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PostMapping("/{correctionId}/approve") + @RequirePermission(menu = "SETTLE_CORR", perm = "APPROVE") + @DataChangeLog(menu = "SETTLE_CORR", table = "settle_correction") + public ApiResponse approve(@PathVariable Long correctionId) { + service.approve(correctionId); + return ApiResponse.ok(); + } + + @PostMapping("/{correctionId}/cancel") + @RequirePermission(menu = "SETTLE_CORR", perm = "UPDATE") + @DataChangeLog(menu = "SETTLE_CORR", table = "settle_correction") + public ApiResponse cancel(@PathVariable Long correctionId, @RequestBody Map body) { + service.cancel(correctionId, body.get("cancelReason")); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/settle/TaxInvoiceController.java b/ga-api/src/main/java/com/ga/api/controller/settle/TaxInvoiceController.java new file mode 100644 index 0000000..ed7a0a1 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/settle/TaxInvoiceController.java @@ -0,0 +1,52 @@ +package com.ga.api.controller.settle; + +import com.ga.api.service.settle.TaxInvoiceService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.settle.TaxInvoiceResp; +import com.ga.core.vo.settle.TaxInvoiceSaveReq; +import com.ga.core.vo.settle.TaxInvoiceSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Tag(name = "세금계산서 관리") +@RestController +@RequestMapping("/api/tax-invoices") +@RequiredArgsConstructor +public class TaxInvoiceController { + + private final TaxInvoiceService service; + + @GetMapping + @RequirePermission(menu = "TAX_INVOICE", perm = "READ") + public ApiResponse> list(@ModelAttribute TaxInvoiceSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{invoiceId}") + @RequirePermission(menu = "TAX_INVOICE", perm = "READ") + public ApiResponse detail(@PathVariable Long invoiceId) { + return ApiResponse.ok(service.detail(invoiceId)); + } + + @PostMapping + @RequirePermission(menu = "TAX_INVOICE", perm = "CREATE") + @DataChangeLog(menu = "TAX_INVOICE", table = "tax_invoice") + public ApiResponse create(@Valid @RequestBody TaxInvoiceSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PostMapping("/{invoiceId}/cancel") + @RequirePermission(menu = "TAX_INVOICE", perm = "UPDATE") + @DataChangeLog(menu = "TAX_INVOICE", table = "tax_invoice") + public ApiResponse cancel(@PathVariable Long invoiceId, @RequestBody Map body) { + service.cancel(invoiceId, body.get("cancelReason")); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/settle/TerminationSettlementController.java b/ga-api/src/main/java/com/ga/api/controller/settle/TerminationSettlementController.java new file mode 100644 index 0000000..ef44f19 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/settle/TerminationSettlementController.java @@ -0,0 +1,65 @@ +package com.ga.api.controller.settle; + +import com.ga.api.service.settle.TerminationSettlementService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.settle.TerminationSettlementResp; +import com.ga.core.vo.settle.TerminationSettlementSaveReq; +import com.ga.core.vo.settle.TerminationSettlementSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "해촉 정산 관리") +@RestController +@RequestMapping("/api/termination-settlements") +@RequiredArgsConstructor +public class TerminationSettlementController { + + private final TerminationSettlementService service; + + @GetMapping + @RequirePermission(menu = "TERM_SETTLE", perm = "READ") + public ApiResponse> list(@ModelAttribute TerminationSettlementSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{termSettleId}") + @RequirePermission(menu = "TERM_SETTLE", perm = "READ") + public ApiResponse detail(@PathVariable Long termSettleId) { + return ApiResponse.ok(service.detail(termSettleId)); + } + + @PostMapping + @RequirePermission(menu = "TERM_SETTLE", perm = "CREATE") + @DataChangeLog(menu = "TERM_SETTLE", table = "termination_settlement") + public ApiResponse create(@Valid @RequestBody TerminationSettlementSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PutMapping("/{termSettleId}") + @RequirePermission(menu = "TERM_SETTLE", perm = "UPDATE") + @DataChangeLog(menu = "TERM_SETTLE", table = "termination_settlement") + public ApiResponse update(@PathVariable Long termSettleId, @Valid @RequestBody TerminationSettlementSaveReq req) { + service.update(termSettleId, req); + return ApiResponse.ok(); + } + + @PostMapping("/process") + @RequirePermission(menu = "TERM_SETTLE", perm = "CREATE") + @DataChangeLog(menu = "TERM_SETTLE", table = "termination_settlement") + public ApiResponse process(@Valid @RequestBody TerminationSettlementSaveReq req) { + return ApiResponse.ok(service.process(req)); + } + + @PostMapping("/{termSettleId}/confirm") + @RequirePermission(menu = "TERM_SETTLE", perm = "APPROVE") + @DataChangeLog(menu = "TERM_SETTLE", table = "termination_settlement") + public ApiResponse confirm(@PathVariable Long termSettleId) { + service.confirm(termSettleId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/training/AgentTrainingController.java b/ga-api/src/main/java/com/ga/api/controller/training/AgentTrainingController.java new file mode 100644 index 0000000..350e6f6 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/training/AgentTrainingController.java @@ -0,0 +1,91 @@ +package com.ga.api.controller.training; + +import com.ga.api.service.training.AgentTrainingService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.training.*; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.List; + +@Tag(name = "보수교육 관리") +@RestController +@RequiredArgsConstructor +public class AgentTrainingController { + + private final AgentTrainingService service; + + // ── Training ────────────────────────────────────────────────────────────── + + @GetMapping("/api/agent-trainings") + @RequirePermission(menu = "AGENT_TRAINING", perm = "READ") + public ApiResponse> list(@ModelAttribute AgentTrainingSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/api/agent-trainings/{trainingId}") + @RequirePermission(menu = "AGENT_TRAINING", perm = "READ") + public ApiResponse detail(@PathVariable Long trainingId) { + return ApiResponse.ok(service.detail(trainingId)); + } + + @GetMapping("/api/agent-trainings/annual-hours") + @RequirePermission(menu = "AGENT_TRAINING", perm = "READ") + public ApiResponse annualHours(@RequestParam Long agentId, + @RequestParam int year) { + return ApiResponse.ok(service.annualHours(agentId, year)); + } + + @PostMapping("/api/agent-trainings") + @RequirePermission(menu = "AGENT_TRAINING", perm = "CREATE") + @DataChangeLog(menu = "AGENT_TRAINING", table = "agent_training") + public ApiResponse create(@Valid @RequestBody AgentTrainingSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PutMapping("/api/agent-trainings/{trainingId}") + @RequirePermission(menu = "AGENT_TRAINING", perm = "UPDATE") + @DataChangeLog(menu = "AGENT_TRAINING", table = "agent_training") + public ApiResponse update(@PathVariable Long trainingId, + @Valid @RequestBody AgentTrainingSaveReq req) { + service.update(trainingId, req); + return ApiResponse.ok(); + } + + @DeleteMapping("/api/agent-trainings/{trainingId}") + @RequirePermission(menu = "AGENT_TRAINING", perm = "DELETE") + @DataChangeLog(menu = "AGENT_TRAINING", table = "agent_training") + public ApiResponse delete(@PathVariable Long trainingId) { + service.delete(trainingId); + return ApiResponse.ok(); + } + + // ── Requirement ─────────────────────────────────────────────────────────── + + @GetMapping("/api/training-requirements") + @RequirePermission(menu = "AGENT_TRAINING", perm = "READ") + public ApiResponse> listRequirements( + @ModelAttribute TrainingRequirementSearchParam param) { + return ApiResponse.ok(service.listRequirements(param)); + } + + @GetMapping("/api/training-requirements/{year}") + @RequirePermission(menu = "AGENT_TRAINING", perm = "READ") + public ApiResponse requirementByYear(@PathVariable int year) { + return ApiResponse.ok(service.requirementByYear(year)); + } + + @PostMapping("/api/training-requirements") + @RequirePermission(menu = "AGENT_TRAINING", perm = "UPDATE") + @DataChangeLog(menu = "AGENT_TRAINING", table = "training_requirement") + public ApiResponse saveRequirement(@Valid @RequestBody TrainingRequirementSaveReq req) { + service.saveRequirement(req); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/withdraw/WithdrawRequestController.java b/ga-api/src/main/java/com/ga/api/controller/withdraw/WithdrawRequestController.java new file mode 100644 index 0000000..406afae --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/withdraw/WithdrawRequestController.java @@ -0,0 +1,50 @@ +package com.ga.api.controller.withdraw; + +import com.ga.api.service.withdraw.WithdrawRequestService; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.withdraw.WithdrawRequestResp; +import com.ga.core.vo.withdraw.WithdrawRequestSaveReq; +import com.ga.core.vo.withdraw.WithdrawRequestSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "출금 신청") +@RestController +@RequestMapping("/api/withdraw-requests") +@RequiredArgsConstructor +public class WithdrawRequestController { + + private final WithdrawRequestService service; + + @GetMapping + @RequirePermission(menu = "WITHDRAW", perm = "READ") + public ApiResponse> list(@ModelAttribute WithdrawRequestSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{requestId}") + @RequirePermission(menu = "WITHDRAW", perm = "READ") + public ApiResponse detail(@PathVariable Long requestId) { + return ApiResponse.ok(service.detail(requestId)); + } + + @PostMapping + @RequirePermission(menu = "WITHDRAW", perm = "CREATE") + @DataChangeLog(menu = "WITHDRAW", table = "withdraw_request") + public ApiResponse create(@Valid @RequestBody WithdrawRequestSaveReq req) { + return ApiResponse.ok(service.create(req)); + } + + @PostMapping("/{requestId}/cancel") + @RequirePermission(menu = "WITHDRAW", perm = "UPDATE") + @DataChangeLog(menu = "WITHDRAW", table = "withdraw_request") + public ApiResponse cancel(@PathVariable Long requestId) { + service.cancel(requestId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/approval/ApprovalService.java b/ga-api/src/main/java/com/ga/api/service/approval/ApprovalService.java new file mode 100644 index 0000000..1bd44f4 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/approval/ApprovalService.java @@ -0,0 +1,259 @@ +package com.ga.api.service.approval; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.mapper.approval.*; +import com.ga.core.mapper.notice.UserNotificationMapper; +import com.ga.core.mapper.settle.CommissionDisputeMapper; +import com.ga.core.mapper.settle.IncentivePaymentMapper; +import com.ga.core.mapper.settle.SettleMasterMapper; +import com.ga.core.mapper.withdraw.WithdrawRequestMapper; +import com.ga.core.vo.approval.*; +import com.ga.core.vo.notice.NotificationRefType; +// Note: ApprovalLineVO kept for createLine/updateLine methods +import com.ga.core.vo.notice.UserNotificationVO; +import com.ga.core.vo.settle.SettleStatus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ApprovalService { + + private final ApprovalLineMapper lineMapper; + private final ApprovalLineStepMapper stepMapper; + private final ApprovalRequestMapper requestMapper; + private final ApprovalHistoryMapper historyMapper; + private final UserNotificationMapper notificationMapper; + + // 콜백 대상 도메인 Mapper + private final WithdrawRequestMapper withdrawMapper; + private final SettleMasterMapper settleMasterMapper; + private final CommissionDisputeMapper disputeMapper; + private final IncentivePaymentMapper incentivePaymentMapper; + + // ── Line ────────────────────────────────────────────────────────────────── + + public PageResponse listLines(ApprovalLineSearchParam param) { + param.startPage(); + return PageResponse.of(lineMapper.selectList(param)); + } + + public ApprovalLineResp detailLine(Long lineId) { + ApprovalLineResp resp = lineMapper.selectById(lineId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + public List stepsOfLine(Long lineId) { + return stepMapper.selectByLine(lineId); + } + + @Transactional + public Long createLine(ApprovalLineSaveReq req) { + ApprovalLineVO vo = new ApprovalLineVO(); + vo.setLineCode(req.getLineCode()); + vo.setLineName(req.getLineName()); + vo.setMaxStep(req.getMaxStep()); + vo.setTargetType(req.getTargetType()); + vo.setIsActive(true); + vo.setCreatedBy(SecurityUtil.getCurrentUserId()); + lineMapper.insert(vo); + return vo.getLineId(); + } + + @Transactional + public void updateLine(Long lineId, ApprovalLineSaveReq req) { + ApprovalLineResp existing = lineMapper.selectById(lineId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + ApprovalLineVO vo = new ApprovalLineVO(); + vo.setLineId(lineId); + vo.setLineCode(req.getLineCode()); + vo.setLineName(req.getLineName()); + vo.setMaxStep(req.getMaxStep()); + vo.setTargetType(req.getTargetType()); + lineMapper.update(vo); + } + + @Transactional + public void addStep(ApprovalLineStepSaveReq req) { + ApprovalLineStepVO vo = new ApprovalLineStepVO(); + vo.setLineId(req.getLineId()); + vo.setStepNo(req.getStepNo()); + vo.setRoleCode(req.getRoleCode()); + stepMapper.insert(vo); + } + + // ── Request ─────────────────────────────────────────────────────────────── + + public PageResponse listRequests(ApprovalRequestSearchParam param) { + param.startPage(); + return PageResponse.of(requestMapper.selectList(param)); + } + + public ApprovalRequestResp detailRequest(Long requestId) { + ApprovalRequestResp resp = requestMapper.selectById(requestId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + public List historyOfRequest(Long requestId) { + return historyMapper.selectByRequest(requestId); + } + + /** + * 결재 요청 생성 — PENDING status=1, 첫 결재자에게 알림 발송. + */ + @Transactional + public Long createRequest(ApprovalRequestSaveReq req) { + // 결재선 존재 확인 + ApprovalLineResp line = lineMapper.selectById(req.getLineId()); + if (line == null) throw new BizException(ErrorCode.NOT_FOUND, "결재선을 찾을 수 없습니다"); + + ApprovalRequestVO vo = new ApprovalRequestVO(); + vo.setTargetType(req.getTargetType()); + vo.setTargetId(req.getTargetId()); + vo.setLineId(req.getLineId()); + vo.setRequestedBy(req.getRequestedBy()); + vo.setRequestedAt(LocalDateTime.now()); + vo.setCurrentStep(1); + vo.setStatus(ApprovalStatus.PENDING.name()); + requestMapper.insert(vo); + + // 1단계 결재자에게 알림 + sendStepNotification(vo.getRequestId(), req.getLineId(), 1, req.getTargetType()); + return vo.getRequestId(); + } + + /** + * 결재 단계 진행 — history INSERT + step advance + 최종 처리. + * @param requestId 결재 요청 PK + * @param approverId 결재자 user_id + * @param action APPROVE / REJECT + * @param comment 코멘트 + */ + @Transactional + public void advance(Long requestId, Long approverId, String action, String comment) { + ApprovalRequestResp reqResp = requestMapper.selectById(requestId); + if (reqResp == null) throw new BizException(ErrorCode.NOT_FOUND); + if (!ApprovalStatus.PENDING.name().equals(reqResp.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "진행 중인 결재 요청이 아닙니다"); + } + + // 1. 결재 이력 INSERT + ApprovalHistoryVO history = new ApprovalHistoryVO(); + history.setRequestId(requestId); + history.setStepNo(reqResp.getCurrentStep()); + history.setApproverId(approverId); + history.setAction(action); + history.setComment(comment); + history.setProcessedAt(LocalDateTime.now()); + historyMapper.insert(history); + + // 2. advanceStep — DB에서 current_step 갱신 + requestMapper.advanceStep(requestId, approverId, action, comment); + + // 3. REJECT → 종료 + if (ApprovalAction.REJECT.name().equals(action)) { + requestMapper.updateStatus(requestId, ApprovalStatus.REJECTED.name()); + log.info("결재 요청 반려: requestId={}", requestId); + return; + } + + // 4. APPROVE 처리 + if (ApprovalAction.APPROVE.name().equals(action)) { + int maxStep = reqResp.getMaxStep() != null ? reqResp.getMaxStep() : 1; + int currentStep = reqResp.getCurrentStep(); + + if (currentStep < maxStep) { + // 다음 단계 알림 + sendStepNotification(requestId, reqResp.getLineId(), currentStep + 1, reqResp.getTargetType()); + } else { + // 최종 승인 — 콜백 처리 + requestMapper.updateStatus(requestId, ApprovalStatus.APPROVED.name()); + handleApprovalCallback(reqResp.getTargetType(), reqResp.getTargetId()); + } + } + } + + /** + * 결재 취소. + */ + @Transactional + public void cancel(Long requestId) { + ApprovalRequestResp reqResp = requestMapper.selectById(requestId); + if (reqResp == null) throw new BizException(ErrorCode.NOT_FOUND); + if (!ApprovalStatus.PENDING.name().equals(reqResp.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "진행 중인 결재만 취소 가능합니다"); + } + requestMapper.cancel(requestId); + } + + // ── Callback ────────────────────────────────────────────────────────────── + + /** + * 최종 승인 후 target_type 별 도메인 후처리. + * - WITHDRAW → withdraw_request.status=APPROVED (펌뱅킹 SENT는 P5) + * - SETTLE → settle_master.status=CONFIRMED + * - DISPUTE → commission_dispute.status=APPROVED (정정 생성은 P5) + * - INCENTIVE → incentive_payment.status=CONFIRMED + */ + private void handleApprovalCallback(String targetType, Long targetId) { + if (targetType == null || targetId == null) return; + try { + switch (targetType) { + case "WITHDRAW" -> { + withdrawMapper.updateStatus(targetId, "APPROVED", null); + log.info("출금신청 승인 완료: requestId={}", targetId); + } + case "SETTLE" -> { + settleMasterMapper.updateStatus(targetId, SettleStatus.CONFIRMED.name(), + SecurityUtil.getCurrentUserId()); + log.info("정산 확정 완료: settleId={}", targetId); + } + case "DISPUTE" -> { + disputeMapper.resolve(targetId, "APPROVED", null, null, null, + SecurityUtil.getCurrentUserId()); + log.info("이의신청 승인 완료: disputeId={}", targetId); + } + case "INCENTIVE" -> { + incentivePaymentMapper.updateStatus(targetId, "CONFIRMED", + SecurityUtil.getCurrentUserId()); + log.info("시책 확정 완료: incentivePayId={}", targetId); + } + default -> log.warn("알 수 없는 결재 대상 타입: targetType={}", targetType); + } + } catch (Exception e) { + log.error("결재 콜백 실패: targetType={}, targetId={}", targetType, targetId, e); + throw new BizException(ErrorCode.INTERNAL_ERROR, "결재 후처리 중 오류가 발생했습니다"); + } + } + + /** 다음 결재 단계 담당자에게 알림 발송 */ + private void sendStepNotification(Long requestId, Long lineId, int stepNo, String targetType) { + try { + ApprovalLineStepVO step = stepMapper.selectByLineAndStep(lineId, stepNo); + if (step == null) return; + // roleCode 기반 userId 매핑은 P5에서 고도화 — 현재는 메타만 저장 + UserNotificationVO noti = new UserNotificationVO(); + noti.setUserId(0L); // 실제 담당자 ID 조회 로직은 P5 + noti.setTitle("[결재요청] " + (targetType != null ? targetType : "") + " 결재 " + stepNo + "단계"); + noti.setContent("결재 요청 ID " + requestId + " 에 대한 " + stepNo + "단계 결재가 필요합니다."); + noti.setRefType(NotificationRefType.APPROVAL.name()); + noti.setRefId(requestId); + noti.setIsRead(false); + notificationMapper.insert(noti); + } catch (Exception e) { + log.warn("결재 알림 발송 실패 (non-critical): requestId={}", requestId, e); + } + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/attachment/AttachmentService.java b/ga-api/src/main/java/com/ga/api/service/attachment/AttachmentService.java new file mode 100644 index 0000000..27c5a94 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/attachment/AttachmentService.java @@ -0,0 +1,111 @@ +package com.ga.api.service.attachment; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.mapper.attachment.AttachmentMapper; +import com.ga.core.vo.attachment.AttachmentResp; +import com.ga.core.vo.attachment.AttachmentSearchParam; +import com.ga.core.vo.attachment.AttachmentVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AttachmentService { + + private final AttachmentMapper mapper; + + @Value("${app.attachment.path:./uploads}") + private String uploadBasePath; + + public PageResponse list(AttachmentSearchParam param) { + param.startPage(); + return PageResponse.of(mapper.selectList(param)); + } + + public List listByRef(String refType, Long refId) { + return mapper.selectByRef(refType, refId); + } + + public AttachmentResp detail(Long attachmentId) { + AttachmentResp resp = mapper.selectById(attachmentId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + /** + * 파일 업로드 + 메타 저장. + * 파일 저장 실패 시 경고 로그 후 메타만 저장하는 fallback. + */ + @Transactional + public Long upload(MultipartFile file, String refType, Long refId) { + String originalName = file.getOriginalFilename() != null ? file.getOriginalFilename() : "unknown"; + String storedPath = null; + + try { + Path dir = Paths.get(uploadBasePath, refType.toLowerCase()); + Files.createDirectories(dir); + String storedName = UUID.randomUUID() + "_" + originalName; + Path dest = dir.resolve(storedName); + file.transferTo(dest); + storedPath = dest.toString(); + } catch (IOException e) { + log.warn("파일 저장 실패 (fallback: 메타만 저장): refType={}, refId={}, file={}, error={}", + refType, refId, originalName, e.getMessage()); + storedPath = "[STORAGE_FAILED]/" + originalName; + } + + AttachmentVO vo = new AttachmentVO(); + vo.setRefType(refType); + vo.setRefId(refId); + vo.setFileName(originalName); + vo.setFilePath(storedPath); + vo.setFileSize(file.getSize()); + vo.setMimeType(file.getContentType()); + vo.setUploadedBy(SecurityUtil.getCurrentUserId()); + mapper.insert(vo); + return vo.getAttachmentId(); + } + + /** + * 파일 다운로드를 위한 실제 경로 반환. + */ + public Path resolveFilePath(Long attachmentId) { + AttachmentResp resp = mapper.selectById(attachmentId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + if (resp.getFilePath() == null || resp.getFilePath().startsWith("[STORAGE_FAILED]")) { + throw new BizException(ErrorCode.NOT_FOUND, "파일이 저장되지 않아 다운로드할 수 없습니다"); + } + return Paths.get(resp.getFilePath()); + } + + @Transactional + public void delete(Long attachmentId) { + AttachmentResp resp = mapper.selectById(attachmentId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + // 실제 파일 삭제 시도 (실패해도 메타 삭제 진행) + if (resp.getFilePath() != null && !resp.getFilePath().startsWith("[STORAGE_FAILED]")) { + try { + Files.deleteIfExists(Paths.get(resp.getFilePath())); + } catch (IOException e) { + log.warn("파일 삭제 실패: path={}", resp.getFilePath(), e); + } + } + mapper.delete(attachmentId); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/CollectionCommissionService.java b/ga-api/src/main/java/com/ga/api/service/commission/CollectionCommissionService.java new file mode 100644 index 0000000..d20fa09 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/CollectionCommissionService.java @@ -0,0 +1,96 @@ +package com.ga.api.service.commission; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.commission.CollectionCommissionLedgerMapper; +import com.ga.core.mapper.commission.CollectionCommissionRuleMapper; +import com.ga.core.vo.commission.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CollectionCommissionService { + + private final CollectionCommissionRuleMapper ruleMapper; + private final CollectionCommissionLedgerMapper ledgerMapper; + + // ── Rule ────────────────────────────────────────────────────────────────── + + public PageResponse listRules(CollectionCommissionRuleSearchParam param) { + param.startPage(); + return PageResponse.of(ruleMapper.selectList(param)); + } + + public CollectionCommissionRuleResp detailRule(Long ruleId) { + CollectionCommissionRuleResp resp = ruleMapper.selectById(ruleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long createRule(CollectionCommissionRuleSaveReq req) { + CollectionCommissionRuleVO vo = new CollectionCommissionRuleVO(); + vo.setProductId(req.getProductId()); + vo.setPaymentCycle(req.getPaymentCycle()); + vo.setCommissionRate(req.getCommissionRate()); + vo.setEffectiveFrom(req.getEffectiveFrom()); + vo.setEffectiveTo(req.getEffectiveTo()); + ruleMapper.insert(vo); + return vo.getRuleId(); + } + + @Transactional + public void updateRule(Long ruleId, CollectionCommissionRuleSaveReq req) { + CollectionCommissionRuleResp resp = ruleMapper.selectById(ruleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + CollectionCommissionRuleVO vo = new CollectionCommissionRuleVO(); + vo.setRuleId(ruleId); + vo.setProductId(req.getProductId()); + vo.setPaymentCycle(req.getPaymentCycle()); + vo.setCommissionRate(req.getCommissionRate()); + vo.setEffectiveFrom(req.getEffectiveFrom()); + vo.setEffectiveTo(req.getEffectiveTo()); + ruleMapper.update(vo); + } + + @Transactional + public void deleteRule(Long ruleId) { + CollectionCommissionRuleResp resp = ruleMapper.selectById(ruleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + ruleMapper.delete(ruleId); + } + + // ── Ledger ──────────────────────────────────────────────────────────────── + + public PageResponse listLedgers(CollectionCommissionLedgerSearchParam param) { + param.startPage(); + return PageResponse.of(ledgerMapper.selectList(param)); + } + + public CollectionCommissionLedgerResp detailLedger(Long ledgerId) { + CollectionCommissionLedgerResp resp = ledgerMapper.selectById(ledgerId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long createLedger(CollectionCommissionLedgerSaveReq req) { + CollectionCommissionLedgerVO vo = new CollectionCommissionLedgerVO(); + vo.setContractId(req.getContractId()); + vo.setAgentId(req.getAgentId()); + vo.setSettleMonth(req.getSettleMonth()); + vo.setPremiumAmount(req.getPremiumAmount()); + vo.setCommissionRate(req.getCommissionRate()); + // 수금수수료 = 보험료 × 수수료율 + vo.setCommissionAmount(req.getPremiumAmount().multiply(req.getCommissionRate())); + vo.setPaymentCycle(req.getPaymentCycle()); + vo.setPaidAt(req.getPaidAt()); + vo.setStatus("CALCULATED"); + ledgerMapper.insert(vo); + return vo.getLedgerId(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/CommissionMasterService.java b/ga-api/src/main/java/com/ga/api/service/commission/CommissionMasterService.java new file mode 100644 index 0000000..2138b88 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/CommissionMasterService.java @@ -0,0 +1,54 @@ +package com.ga.api.service.commission; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.commission.CommissionMasterMapper; +import com.ga.core.vo.commission.CommissionMasterResp; +import com.ga.core.vo.commission.CommissionMasterSearchParam; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CommissionMasterService { + + private final CommissionMasterMapper mapper; + + public PageResponse list(CommissionMasterSearchParam param) { + param.startPage(); + return PageResponse.of(mapper.selectList(param)); + } + + public CommissionMasterResp detail(Long masterId) { + CommissionMasterResp resp = mapper.selectById(masterId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + public List listByAgentAndMonth(Long agentId, String settleMonth) { + return mapper.selectByAgentAndMonth(agentId, settleMonth); + } + + public List aggregatedByMonth(String settleMonth) { + return mapper.selectAggregatedByMonth(settleMonth); + } + + @Transactional + public void updateStatus(Long masterId, String status) { + CommissionMasterResp resp = mapper.selectById(masterId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + mapper.updateStatus(masterId, status); + } + + @Transactional + public void delete(Long masterId) { + CommissionMasterResp resp = mapper.selectById(masterId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + mapper.delete(masterId); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/OverrideLedgerService.java b/ga-api/src/main/java/com/ga/api/service/commission/OverrideLedgerService.java new file mode 100644 index 0000000..f651b18 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/OverrideLedgerService.java @@ -0,0 +1,43 @@ +package com.ga.api.service.commission; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.commission.OverrideLedgerMapper; +import com.ga.core.mapper.commission.OverrideSummaryMapper; +import com.ga.core.vo.commission.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class OverrideLedgerService { + + private final OverrideLedgerMapper ledgerMapper; + private final OverrideSummaryMapper summaryMapper; + + public PageResponse list(OverrideLedgerSearchParam param) { + param.startPage(); + return PageResponse.of(ledgerMapper.selectList(param)); + } + + public OverrideLedgerResp detail(Long ledgerId) { + OverrideLedgerResp resp = ledgerMapper.selectById(ledgerId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + public List summaryList(OverrideSummarySearchParam param) { + return summaryMapper.selectList(param); + } + + public OverrideSummaryResp summaryByAgentAndMonth(Long toAgentId, String settleMonth) { + OverrideSummaryResp resp = summaryMapper.selectByAgentAndMonth(toAgentId, settleMonth); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/PersistencyBonusService.java b/ga-api/src/main/java/com/ga/api/service/commission/PersistencyBonusService.java new file mode 100644 index 0000000..9117ef9 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/PersistencyBonusService.java @@ -0,0 +1,100 @@ +package com.ga.api.service.commission; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.commission.PersistencyBonusMapper; +import com.ga.core.mapper.commission.PersistencyBonusRuleMapper; +import com.ga.core.vo.commission.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PersistencyBonusService { + + private final PersistencyBonusRuleMapper ruleMapper; + private final PersistencyBonusMapper bonusMapper; + + // ── Rule ────────────────────────────────────────────────────────────────── + + public PageResponse listRules(PersistencyBonusRuleSearchParam param) { + param.startPage(); + return PageResponse.of(ruleMapper.selectList(param)); + } + + public PersistencyBonusRuleResp detailRule(Long ruleId) { + PersistencyBonusRuleResp resp = ruleMapper.selectById(ruleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long createRule(PersistencyBonusRuleSaveReq req) { + PersistencyBonusRuleVO vo = new PersistencyBonusRuleVO(); + vo.setGradeId(req.getGradeId()); + vo.setRetentionMonth(req.getRetentionMonth()); + vo.setRetentionRateThreshold(req.getRetentionRateThreshold()); + vo.setBonusType(req.getBonusType()); + vo.setBonusValue(req.getBonusValue()); + vo.setEffectiveFrom(req.getEffectiveFrom()); + vo.setEffectiveTo(req.getEffectiveTo()); + ruleMapper.insert(vo); + return vo.getRuleId(); + } + + @Transactional + public void updateRule(Long ruleId, PersistencyBonusRuleSaveReq req) { + PersistencyBonusRuleResp resp = ruleMapper.selectById(ruleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + PersistencyBonusRuleVO vo = new PersistencyBonusRuleVO(); + vo.setRuleId(ruleId); + vo.setGradeId(req.getGradeId()); + vo.setRetentionMonth(req.getRetentionMonth()); + vo.setRetentionRateThreshold(req.getRetentionRateThreshold()); + vo.setBonusType(req.getBonusType()); + vo.setBonusValue(req.getBonusValue()); + vo.setEffectiveFrom(req.getEffectiveFrom()); + vo.setEffectiveTo(req.getEffectiveTo()); + ruleMapper.update(vo); + } + + @Transactional + public void deleteRule(Long ruleId) { + PersistencyBonusRuleResp resp = ruleMapper.selectById(ruleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + ruleMapper.delete(ruleId); + } + + // ── Bonus ───────────────────────────────────────────────────────────────── + + public PageResponse listBonuses(PersistencyBonusSearchParam param) { + param.startPage(); + return PageResponse.of(bonusMapper.selectList(param)); + } + + public PersistencyBonusResp detailBonus(Long bonusId) { + PersistencyBonusResp resp = bonusMapper.selectById(bonusId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + /** + * 수동 지급 등록/갱신 — UNIQUE(agent_id, base_month, retention_month) 기반 upsert. + */ + @Transactional + public void upsertBonus(PersistencyBonusSaveReq req) { + PersistencyBonusVO vo = new PersistencyBonusVO(); + vo.setAgentId(req.getAgentId()); + vo.setRuleId(req.getRuleId()); + vo.setBaseMonth(req.getBaseMonth()); + vo.setRetentionMonth(req.getRetentionMonth()); + vo.setSettleMonth(req.getSettleMonth()); + vo.setRetentionRate(req.getRetentionRate()); + vo.setBonusAmount(req.getBonusAmount()); + vo.setStatus("CALCULATED"); + bonusMapper.upsert(vo); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/ReinstatementCommissionService.java b/ga-api/src/main/java/com/ga/api/service/commission/ReinstatementCommissionService.java new file mode 100644 index 0000000..9614f1b --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/ReinstatementCommissionService.java @@ -0,0 +1,108 @@ +package com.ga.api.service.commission; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.commission.ReinstatementCommissionLedgerMapper; +import com.ga.core.mapper.commission.ReinstatementCommissionRuleMapper; +import com.ga.core.vo.commission.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReinstatementCommissionService { + + private final ReinstatementCommissionRuleMapper ruleMapper; + private final ReinstatementCommissionLedgerMapper ledgerMapper; + + // ── Rule ────────────────────────────────────────────────────────────────── + + public PageResponse listRules(ReinstatementCommissionRuleSearchParam param) { + param.startPage(); + return PageResponse.of(ruleMapper.selectList(param)); + } + + public ReinstatementCommissionRuleResp detailRule(Long ruleId) { + ReinstatementCommissionRuleResp resp = ruleMapper.selectById(ruleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long createRule(ReinstatementCommissionRuleSaveReq req) { + ReinstatementCommissionRuleVO vo = new ReinstatementCommissionRuleVO(); + vo.setProductId(req.getProductId()); + vo.setPayType(req.getPayType()); + vo.setRateValue(req.getRateValue()); + vo.setFixedAmount(req.getFixedAmount()); + vo.setChargebackReversalRate(req.getChargebackReversalRate()); + vo.setEffectiveFrom(req.getEffectiveFrom()); + vo.setEffectiveTo(req.getEffectiveTo()); + ruleMapper.insert(vo); + return vo.getRuleId(); + } + + @Transactional + public void updateRule(Long ruleId, ReinstatementCommissionRuleSaveReq req) { + ReinstatementCommissionRuleResp resp = ruleMapper.selectById(ruleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + ReinstatementCommissionRuleVO vo = new ReinstatementCommissionRuleVO(); + vo.setRuleId(ruleId); + vo.setProductId(req.getProductId()); + vo.setPayType(req.getPayType()); + vo.setRateValue(req.getRateValue()); + vo.setFixedAmount(req.getFixedAmount()); + vo.setChargebackReversalRate(req.getChargebackReversalRate()); + vo.setEffectiveFrom(req.getEffectiveFrom()); + vo.setEffectiveTo(req.getEffectiveTo()); + ruleMapper.update(vo); + } + + @Transactional + public void deleteRule(Long ruleId) { + ReinstatementCommissionRuleResp resp = ruleMapper.selectById(ruleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + ruleMapper.delete(ruleId); + } + + // ── Ledger ──────────────────────────────────────────────────────────────── + + public PageResponse listLedgers(ReinstatementCommissionLedgerSearchParam param) { + param.startPage(); + return PageResponse.of(ledgerMapper.selectList(param)); + } + + public ReinstatementCommissionLedgerResp detailLedger(Long ledgerId) { + ReinstatementCommissionLedgerResp resp = ledgerMapper.selectById(ledgerId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + /** + * 수동 등록 — chargebackReversal 은 요청값 그대로. + * netCommission = commissionAmount + chargebackReversal 로 자동 계산. + */ + @Transactional + public Long createLedger(ReinstatementCommissionLedgerSaveReq req) { + ReinstatementCommissionLedgerVO vo = new ReinstatementCommissionLedgerVO(); + vo.setContractId(req.getContractId()); + vo.setAgentId(req.getAgentId()); + vo.setRuleId(req.getRuleId()); + vo.setSettleMonth(req.getSettleMonth()); + vo.setReinstatementDate(req.getReinstatementDate()); + vo.setReinstatementPremium(req.getReinstatementPremium()); + vo.setCommissionAmount(req.getCommissionAmount()); + BigDecimal reversal = req.getChargebackReversal() != null ? req.getChargebackReversal() : BigDecimal.ZERO; + vo.setChargebackReversal(reversal); + vo.setNetCommission(req.getCommissionAmount().add(reversal)); + vo.setPayType(req.getPayType()); + vo.setStatus("CALCULATED"); + ledgerMapper.insert(vo); + return vo.getLedgerId(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/RenewalCommissionService.java b/ga-api/src/main/java/com/ga/api/service/commission/RenewalCommissionService.java new file mode 100644 index 0000000..38720c6 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/RenewalCommissionService.java @@ -0,0 +1,97 @@ +package com.ga.api.service.commission; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.commission.RenewalCommissionLedgerMapper; +import com.ga.core.mapper.commission.RenewalCommissionRuleMapper; +import com.ga.core.vo.commission.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class RenewalCommissionService { + + private final RenewalCommissionRuleMapper ruleMapper; + private final RenewalCommissionLedgerMapper ledgerMapper; + + // ── Rule ────────────────────────────────────────────────────────────────── + + public PageResponse listRules(RenewalCommissionRuleSearchParam param) { + param.startPage(); + return PageResponse.of(ruleMapper.selectList(param)); + } + + public RenewalCommissionRuleResp detailRule(Long ruleId) { + RenewalCommissionRuleResp resp = ruleMapper.selectById(ruleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long createRule(RenewalCommissionRuleSaveReq req) { + RenewalCommissionRuleVO vo = new RenewalCommissionRuleVO(); + vo.setProductId(req.getProductId()); + vo.setPayType(req.getPayType()); + vo.setRateValue(req.getRateValue()); + vo.setFixedAmount(req.getFixedAmount()); + vo.setEffectiveFrom(req.getEffectiveFrom()); + vo.setEffectiveTo(req.getEffectiveTo()); + ruleMapper.insert(vo); + return vo.getRuleId(); + } + + @Transactional + public void updateRule(Long ruleId, RenewalCommissionRuleSaveReq req) { + RenewalCommissionRuleResp resp = ruleMapper.selectById(ruleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + RenewalCommissionRuleVO vo = new RenewalCommissionRuleVO(); + vo.setRuleId(ruleId); + vo.setProductId(req.getProductId()); + vo.setPayType(req.getPayType()); + vo.setRateValue(req.getRateValue()); + vo.setFixedAmount(req.getFixedAmount()); + vo.setEffectiveFrom(req.getEffectiveFrom()); + vo.setEffectiveTo(req.getEffectiveTo()); + ruleMapper.update(vo); + } + + @Transactional + public void deleteRule(Long ruleId) { + RenewalCommissionRuleResp resp = ruleMapper.selectById(ruleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + ruleMapper.delete(ruleId); + } + + // ── Ledger ──────────────────────────────────────────────────────────────── + + public PageResponse listLedgers(RenewalCommissionLedgerSearchParam param) { + param.startPage(); + return PageResponse.of(ledgerMapper.selectList(param)); + } + + public RenewalCommissionLedgerResp detailLedger(Long ledgerId) { + RenewalCommissionLedgerResp resp = ledgerMapper.selectById(ledgerId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long createLedger(RenewalCommissionLedgerSaveReq req) { + RenewalCommissionLedgerVO vo = new RenewalCommissionLedgerVO(); + vo.setContractId(req.getContractId()); + vo.setAgentId(req.getAgentId()); + vo.setRuleId(req.getRuleId()); + vo.setSettleMonth(req.getSettleMonth()); + vo.setRenewalDate(req.getRenewalDate()); + vo.setRenewalPremium(req.getRenewalPremium()); + vo.setCommissionAmount(req.getCommissionAmount()); + vo.setPayType(req.getPayType()); + vo.setStatus("CALCULATED"); + ledgerMapper.insert(vo); + return vo.getLedgerId(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/complaint/ComplaintService.java b/ga-api/src/main/java/com/ga/api/service/complaint/ComplaintService.java new file mode 100644 index 0000000..92fd1e3 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/complaint/ComplaintService.java @@ -0,0 +1,86 @@ +package com.ga.api.service.complaint; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.complaint.ComplaintMapper; +import com.ga.core.vo.complaint.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ComplaintService { + + private final ComplaintMapper mapper; + + public PageResponse list(ComplaintSearchParam param) { + param.startPage(); + return PageResponse.of(mapper.selectList(param)); + } + + public ComplaintResp detail(Long complaintId) { + ComplaintResp resp = mapper.selectById(complaintId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long create(ComplaintSaveReq req) { + ComplaintVO vo = new ComplaintVO(); + vo.setContractId(req.getContractId()); + vo.setAgentId(req.getAgentId()); + vo.setCustomerName(req.getCustomerName()); + vo.setComplaintDate(req.getComplaintDate()); + vo.setChannel(req.getChannel()); + vo.setComplaintType(req.getComplaintType()); + vo.setSeverity(req.getSeverity() != null ? req.getSeverity() : "MID"); + vo.setStatus(ComplaintStatus.OPEN.name()); + vo.setResolution(req.getResolution()); + // chargebackTriggered=true 면 P5에서 chargeback_ledger 자동 생성 예정 — 지금은 플래그만 저장 + vo.setChargebackTriggered(req.getChargebackTriggered()); + mapper.insert(vo); + return vo.getComplaintId(); + } + + @Transactional + public void update(Long complaintId, ComplaintSaveReq req) { + ComplaintResp existing = mapper.selectById(complaintId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + ComplaintVO vo = new ComplaintVO(); + vo.setComplaintId(complaintId); + vo.setContractId(req.getContractId()); + vo.setAgentId(req.getAgentId()); + vo.setCustomerName(req.getCustomerName()); + vo.setComplaintDate(req.getComplaintDate()); + vo.setChannel(req.getChannel()); + vo.setComplaintType(req.getComplaintType()); + vo.setSeverity(req.getSeverity()); + vo.setResolution(req.getResolution()); + vo.setChargebackTriggered(req.getChargebackTriggered()); + mapper.update(vo); + } + + /** + * 처리완료 — status=CLOSED. + * chargebackTriggered=true 인 건의 실제 환수 생성은 P5 배치에서. + */ + @Transactional + public void close(Long complaintId, String resolution) { + ComplaintResp existing = mapper.selectById(complaintId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + if (ComplaintStatus.CLOSED.name().equals(existing.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "이미 처리완료된 민원입니다"); + } + mapper.updateStatus(complaintId, ComplaintStatus.CLOSED.name(), resolution); + } + + @Transactional + public void delete(Long complaintId) { + ComplaintResp existing = mapper.selectById(complaintId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + mapper.delete(complaintId); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/contract/AgentContractService.java b/ga-api/src/main/java/com/ga/api/service/contract/AgentContractService.java new file mode 100644 index 0000000..6c38e05 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/contract/AgentContractService.java @@ -0,0 +1,79 @@ +package com.ga.api.service.contract; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.contract.AgentContractMapper; +import com.ga.core.vo.contract.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AgentContractService { + + private final AgentContractMapper mapper; + + public PageResponse list(AgentContractSearchParam param) { + param.startPage(); + return PageResponse.of(mapper.selectList(param)); + } + + public AgentContractResp detail(Long contractId) { + AgentContractResp resp = mapper.selectById(contractId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long create(AgentContractSaveReq req) { + AgentContractVO vo = new AgentContractVO(); + vo.setAgentId(req.getAgentId()); + vo.setCarrierId(req.getCarrierId()); + vo.setContractNo(req.getContractNo()); + vo.setSignedDate(req.getSignedDate()); + vo.setEffectiveFrom(req.getEffectiveFrom()); + vo.setEffectiveTo(req.getEffectiveTo()); + vo.setOnboardingBonusAmount(req.getOnboardingBonusAmount()); + vo.setStatus(AgentContractStatus.ACTIVE.name()); + mapper.insert(vo); + return vo.getContractId(); + } + + @Transactional + public void update(Long contractId, AgentContractSaveReq req) { + AgentContractResp existing = mapper.selectById(contractId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + AgentContractVO vo = new AgentContractVO(); + vo.setContractId(contractId); + vo.setAgentId(req.getAgentId()); + vo.setCarrierId(req.getCarrierId()); + vo.setContractNo(req.getContractNo()); + vo.setSignedDate(req.getSignedDate()); + vo.setEffectiveFrom(req.getEffectiveFrom()); + vo.setEffectiveTo(req.getEffectiveTo()); + vo.setOnboardingBonusAmount(req.getOnboardingBonusAmount()); + mapper.update(vo); + } + + @Transactional + public void terminate(Long contractId, String reason) { + AgentContractResp existing = mapper.selectById(contractId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + if (AgentContractStatus.TERMINATED.name().equals(existing.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "이미 해촉된 위촉계약입니다"); + } + mapper.updateStatus(contractId, AgentContractStatus.TERMINATED.name(), reason); + } + + @Transactional + public void delete(Long contractId) { + AgentContractResp existing = mapper.selectById(contractId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + mapper.delete(contractId); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/endorse/ContractEndorsementService.java b/ga-api/src/main/java/com/ga/api/service/endorse/ContractEndorsementService.java new file mode 100644 index 0000000..b1131b3 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/endorse/ContractEndorsementService.java @@ -0,0 +1,62 @@ +package com.ga.api.service.endorse; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.endorse.ContractEndorsementMapper; +import com.ga.core.vo.endorse.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ContractEndorsementService { + + private final ContractEndorsementMapper mapper; + + public PageResponse list(ContractEndorsementSearchParam param) { + param.startPage(); + return PageResponse.of(mapper.selectList(param)); + } + + public ContractEndorsementResp detail(Long endorsementId) { + ContractEndorsementResp resp = mapper.selectById(endorsementId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long create(ContractEndorsementSaveReq req) { + ContractEndorsementVO vo = new ContractEndorsementVO(); + vo.setContractId(req.getContractId()); + vo.setAgentId(req.getAgentId()); + vo.setEndorsementType(req.getEndorsementType()); + vo.setOldValue(req.getOldValue()); + vo.setNewValue(req.getNewValue()); + vo.setReason(req.getReason()); + vo.setRequiresCommissionRecalc(req.getRequiresCommissionRecalc()); + vo.setProcessed(false); + mapper.insert(vo); + return vo.getEndorsementId(); + } + + /** + * 수동 처리 완료 표시 — 실제 수수료 재계산은 배치 담당(P5). + * status 변경(processed=true) 만 수행. + */ + @Transactional + public void process(Long endorsementId) { + ContractEndorsementResp existing = mapper.selectById(endorsementId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + mapper.markProcessed(endorsementId); + } + + @Transactional + public void delete(Long endorsementId) { + ContractEndorsementResp existing = mapper.selectById(endorsementId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + mapper.delete(endorsementId); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/kpi/KpiService.java b/ga-api/src/main/java/com/ga/api/service/kpi/KpiService.java new file mode 100644 index 0000000..97aa686 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/kpi/KpiService.java @@ -0,0 +1,40 @@ +package com.ga.api.service.kpi; + +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.kpi.KpiMapper; +import com.ga.core.vo.kpi.KpiCumulativeResp; +import com.ga.core.vo.kpi.KpiMonthlySummaryResp; +import com.ga.core.vo.kpi.KpiOrgSummaryResp; +import com.ga.core.vo.kpi.KpiRetentionResp; +import com.ga.core.vo.kpi.KpiSearchParam; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class KpiService { + + private final KpiMapper kpiMapper; + + public PageResponse monthlySummary(KpiSearchParam param) { + param.startPage(); + return PageResponse.of(kpiMapper.selectMonthlySummary(param)); + } + + public PageResponse orgSummary(KpiSearchParam param) { + param.startPage(); + return PageResponse.of(kpiMapper.selectOrgSummary(param)); + } + + public PageResponse retention(KpiSearchParam param) { + param.startPage(); + return PageResponse.of(kpiMapper.selectRetention(param)); + } + + public PageResponse cumulative(KpiSearchParam param) { + param.startPage(); + return PageResponse.of(kpiMapper.selectCumulative(param)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/license/AgentLicenseService.java b/ga-api/src/main/java/com/ga/api/service/license/AgentLicenseService.java new file mode 100644 index 0000000..fcb1849 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/license/AgentLicenseService.java @@ -0,0 +1,71 @@ +package com.ga.api.service.license; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.license.AgentLicenseMapper; +import com.ga.core.vo.license.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AgentLicenseService { + + private final AgentLicenseMapper mapper; + + public PageResponse list(AgentLicenseSearchParam param) { + param.startPage(); + return PageResponse.of(mapper.selectList(param)); + } + + public AgentLicenseResp detail(Long licenseId) { + AgentLicenseResp resp = mapper.selectById(licenseId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + public List expiring(int days) { + return mapper.selectExpiringWithin(days); + } + + @Transactional + public Long create(AgentLicenseSaveReq req) { + AgentLicenseVO vo = new AgentLicenseVO(); + vo.setAgentId(req.getAgentId()); + vo.setLicenseType(req.getLicenseType()); + vo.setLicenseNo(req.getLicenseNo()); + vo.setIssuedDate(req.getIssuedDate()); + vo.setExpiresDate(req.getExpiresDate()); + vo.setRenewedFrom(req.getRenewedFrom()); + vo.setStatus(LicenseStatus.ACTIVE.name()); + mapper.insert(vo); + return vo.getLicenseId(); + } + + @Transactional + public void update(Long licenseId, AgentLicenseSaveReq req) { + AgentLicenseResp existing = mapper.selectById(licenseId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + AgentLicenseVO vo = new AgentLicenseVO(); + vo.setLicenseId(licenseId); + vo.setAgentId(req.getAgentId()); + vo.setLicenseType(req.getLicenseType()); + vo.setLicenseNo(req.getLicenseNo()); + vo.setIssuedDate(req.getIssuedDate()); + vo.setExpiresDate(req.getExpiresDate()); + vo.setRenewedFrom(req.getRenewedFrom()); + mapper.update(vo); + } + + @Transactional + public void delete(Long licenseId) { + AgentLicenseResp existing = mapper.selectById(licenseId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + mapper.delete(licenseId); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/notice/NoticeService.java b/ga-api/src/main/java/com/ga/api/service/notice/NoticeService.java new file mode 100644 index 0000000..49be7de --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/notice/NoticeService.java @@ -0,0 +1,97 @@ +package com.ga.api.service.notice; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.mapper.notice.NoticeMapper; +import com.ga.core.mapper.notice.UserNotificationMapper; +import com.ga.core.vo.notice.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class NoticeService { + + private final NoticeMapper noticeMapper; + private final UserNotificationMapper notificationMapper; + + // ── Notice ──────────────────────────────────────────────────────────────── + + public PageResponse list(NoticeSearchParam param) { + param.startPage(); + return PageResponse.of(noticeMapper.selectList(param)); + } + + public NoticeResp detail(Long noticeId) { + NoticeResp resp = noticeMapper.selectById(noticeId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + public List activeList() { + return noticeMapper.selectActive(); + } + + @Transactional + public Long create(NoticeSaveReq req) { + NoticeVO vo = new NoticeVO(); + vo.setTitle(req.getTitle()); + vo.setContent(req.getContent()); + vo.setNoticeType(req.getNoticeType()); + vo.setStartDate(req.getStartDate()); + vo.setEndDate(req.getEndDate()); + vo.setIsActive(true); + vo.setCreatedBy(SecurityUtil.getCurrentUserId()); + noticeMapper.insert(vo); + return vo.getNoticeId(); + } + + @Transactional + public void update(Long noticeId, NoticeSaveReq req) { + NoticeResp existing = noticeMapper.selectById(noticeId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + NoticeVO vo = new NoticeVO(); + vo.setNoticeId(noticeId); + vo.setTitle(req.getTitle()); + vo.setContent(req.getContent()); + vo.setNoticeType(req.getNoticeType()); + vo.setStartDate(req.getStartDate()); + vo.setEndDate(req.getEndDate()); + noticeMapper.update(vo); + } + + @Transactional + public void delete(Long noticeId) { + NoticeResp existing = noticeMapper.selectById(noticeId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + noticeMapper.delete(noticeId); + } + + // ── UserNotification ────────────────────────────────────────────────────── + + public PageResponse listNotifications(UserNotificationSearchParam param) { + param.startPage(); + return PageResponse.of(notificationMapper.selectList(param)); + } + + public int unreadCount(Long userId) { + return notificationMapper.selectUnreadCount(userId); + } + + @Transactional + public void markRead(Long notificationId, Long userId) { + int affected = notificationMapper.markRead(notificationId, userId); + if (affected == 0) throw new BizException(ErrorCode.NOT_FOUND); + } + + @Transactional + public void markAllRead(Long userId) { + notificationMapper.markAllRead(userId); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/settle/CarrierReconciliationOutService.java b/ga-api/src/main/java/com/ga/api/service/settle/CarrierReconciliationOutService.java new file mode 100644 index 0000000..5be9c3e --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/settle/CarrierReconciliationOutService.java @@ -0,0 +1,67 @@ +package com.ga.api.service.settle; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.mapper.settle.CarrierReconciliationOutMapper; +import com.ga.core.vo.settle.CarrierReconciliationOutResp; +import com.ga.core.vo.settle.CarrierReconciliationOutSaveReq; +import com.ga.core.vo.settle.CarrierReconciliationOutSearchParam; +import com.ga.core.vo.settle.CarrierReconciliationOutVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CarrierReconciliationOutService { + + private final CarrierReconciliationOutMapper reconOutMapper; + + public PageResponse list(CarrierReconciliationOutSearchParam param) { + param.startPage(); + return PageResponse.of(reconOutMapper.selectList(param)); + } + + public CarrierReconciliationOutResp detail(Long reconOutId) { + CarrierReconciliationOutResp resp = reconOutMapper.selectById(reconOutId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long create(CarrierReconciliationOutSaveReq req) { + // 보험사+정산월 중복 확인 + CarrierReconciliationOutVO existing = reconOutMapper.selectByCarrierMonth(req.getCarrierId(), req.getSettleMonth()); + if (existing != null) { + throw new BizException(ErrorCode.DUPLICATE_DATA, + "이미 등록된 회신 이력입니다: carrierId=" + req.getCarrierId() + ", settleMonth=" + req.getSettleMonth()); + } + + CarrierReconciliationOutVO vo = new CarrierReconciliationOutVO(); + vo.setCarrierId(req.getCarrierId()); + vo.setSettleMonth(req.getSettleMonth()); + vo.setFilePath(req.getFilePath()); + vo.setFileName(req.getFileName()); + vo.setFileSize(req.getFileSize()); + vo.setRecordCount(req.getRecordCount()); + vo.setStatus("READY"); + vo.setRetryCount(0); + vo.setCreatedBy(SecurityUtil.getCurrentUserId()); + reconOutMapper.insert(vo); + return vo.getReconOutId(); + } + + @Transactional + public void retry(Long reconOutId) { + CarrierReconciliationOutVO vo = reconOutMapper.selectVoById(reconOutId); + if (vo == null) throw new BizException(ErrorCode.NOT_FOUND); + if (!"FAIL".equals(vo.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "실패 상태만 재전송 가능합니다"); + } + // 상태를 READY로 초기화하여 배치가 재처리 + reconOutMapper.updateStatus(reconOutId, "READY"); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/settle/CommissionDisputeService.java b/ga-api/src/main/java/com/ga/api/service/settle/CommissionDisputeService.java new file mode 100644 index 0000000..a7cfb1e --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/settle/CommissionDisputeService.java @@ -0,0 +1,83 @@ +package com.ga.api.service.settle; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.mapper.settle.CommissionDisputeMapper; +import com.ga.core.vo.settle.CommissionDisputeResp; +import com.ga.core.vo.settle.CommissionDisputeSaveReq; +import com.ga.core.vo.settle.CommissionDisputeSearchParam; +import com.ga.core.vo.settle.CommissionDisputeVO; +import com.ga.core.vo.settle.DisputeStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CommissionDisputeService { + + private final CommissionDisputeMapper disputeMapper; + + public PageResponse list(CommissionDisputeSearchParam param) { + param.startPage(); + return PageResponse.of(disputeMapper.selectList(param)); + } + + public CommissionDisputeResp detail(Long disputeId) { + CommissionDisputeResp resp = disputeMapper.selectById(disputeId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long create(CommissionDisputeSaveReq req) { + CommissionDisputeVO vo = new CommissionDisputeVO(); + vo.setAgentId(req.getAgentId()); + vo.setSettleMonth(req.getSettleMonth()); + vo.setSettleId(req.getSettleId()); + vo.setDisputedAmount(req.getDisputedAmount()); + vo.setClaimAmount(req.getClaimAmount()); + vo.setReason(req.getReason()); + vo.setEvidenceUrl(req.getEvidenceUrl()); + vo.setStatus(DisputeStatus.OPEN.name()); + disputeMapper.insert(vo); + return vo.getDisputeId(); + } + + @Transactional + public void approve(Long disputeId, String resolution, BigDecimal adjustmentAmount, Long correctionId) { + CommissionDisputeVO vo = disputeMapper.selectVoById(disputeId); + if (vo == null) throw new BizException(ErrorCode.NOT_FOUND); + if (!DisputeStatus.IN_REVIEW.name().equals(vo.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "검토 중 상태만 승인 가능합니다"); + } + disputeMapper.resolve(disputeId, DisputeStatus.APPROVED.name(), + resolution, adjustmentAmount, correctionId, SecurityUtil.getCurrentUserId()); + } + + @Transactional + public void reject(Long disputeId, String resolution) { + CommissionDisputeVO vo = disputeMapper.selectVoById(disputeId); + if (vo == null) throw new BizException(ErrorCode.NOT_FOUND); + if (!DisputeStatus.IN_REVIEW.name().equals(vo.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "검토 중 상태만 기각 가능합니다"); + } + disputeMapper.resolve(disputeId, DisputeStatus.REJECTED.name(), + resolution, null, null, SecurityUtil.getCurrentUserId()); + } + + @Transactional + public void startReview(Long disputeId) { + CommissionDisputeVO vo = disputeMapper.selectVoById(disputeId); + if (vo == null) throw new BizException(ErrorCode.NOT_FOUND); + if (!DisputeStatus.OPEN.name().equals(vo.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "OPEN 상태만 검토 시작 가능합니다"); + } + disputeMapper.startReview(disputeId, SecurityUtil.getCurrentUserId()); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/settle/IncentivePaymentService.java b/ga-api/src/main/java/com/ga/api/service/settle/IncentivePaymentService.java new file mode 100644 index 0000000..80b160a --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/settle/IncentivePaymentService.java @@ -0,0 +1,73 @@ +package com.ga.api.service.settle; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.mapper.settle.IncentivePaymentMapper; +import com.ga.core.vo.settle.IncentivePayStatus; +import com.ga.core.vo.settle.IncentivePaymentResp; +import com.ga.core.vo.settle.IncentivePaymentSaveReq; +import com.ga.core.vo.settle.IncentivePaymentSearchParam; +import com.ga.core.vo.settle.IncentivePaymentVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class IncentivePaymentService { + + private final IncentivePaymentMapper paymentMapper; + private final PeriodCloseService periodCloseService; + + public PageResponse list(IncentivePaymentSearchParam param) { + param.startPage(); + return PageResponse.of(paymentMapper.selectList(param)); + } + + public IncentivePaymentResp detail(Long incentivePayId) { + IncentivePaymentResp resp = paymentMapper.selectById(incentivePayId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long create(IncentivePaymentSaveReq req) { + // 마감 가드 + periodCloseService.assertNotClosed(req.getSettleMonth()); + + IncentivePaymentVO vo = new IncentivePaymentVO(); + vo.setProgramId(req.getProgramId()); + vo.setAgentId(req.getAgentId()); + vo.setSettleMonth(req.getSettleMonth()); + vo.setBaseAmount(req.getBaseAmount()); + vo.setIncentiveAmount(req.getIncentiveAmount()); + vo.setStatus(IncentivePayStatus.CALCULATED.name()); + paymentMapper.insert(vo); + return vo.getIncentivePayId(); + } + + @Transactional + public void confirm(Long incentivePayId) { + IncentivePaymentVO vo = paymentMapper.selectVoById(incentivePayId); + if (vo == null) throw new BizException(ErrorCode.NOT_FOUND); + if (!IncentivePayStatus.CALCULATED.name().equals(vo.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "CALCULATED 상태만 확정 가능합니다"); + } + // 마감 가드 + periodCloseService.assertNotClosed(vo.getSettleMonth()); + paymentMapper.updateStatus(incentivePayId, IncentivePayStatus.CONFIRMED.name(), SecurityUtil.getCurrentUserId()); + } + + @Transactional + public void cancel(Long incentivePayId, String cancelReason) { + IncentivePaymentVO vo = paymentMapper.selectVoById(incentivePayId); + if (vo == null) throw new BizException(ErrorCode.NOT_FOUND); + if (IncentivePayStatus.PAID.name().equals(vo.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "지급완료된 시책은 취소할 수 없습니다"); + } + paymentMapper.updateStatus(incentivePayId, IncentivePayStatus.CANCELLED.name(), SecurityUtil.getCurrentUserId()); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/settle/IncentiveProgramService.java b/ga-api/src/main/java/com/ga/api/service/settle/IncentiveProgramService.java new file mode 100644 index 0000000..eda8629 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/settle/IncentiveProgramService.java @@ -0,0 +1,84 @@ +package com.ga.api.service.settle; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.mapper.settle.IncentiveProgramMapper; +import com.ga.core.vo.settle.IncentiveProgramResp; +import com.ga.core.vo.settle.IncentiveProgramSaveReq; +import com.ga.core.vo.settle.IncentiveProgramSearchParam; +import com.ga.core.vo.settle.IncentiveProgramVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class IncentiveProgramService { + + private final IncentiveProgramMapper programMapper; + + public PageResponse list(IncentiveProgramSearchParam param) { + param.startPage(); + return PageResponse.of(programMapper.selectList(param)); + } + + public IncentiveProgramResp detail(Long programId) { + IncentiveProgramResp resp = programMapper.selectById(programId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long create(IncentiveProgramSaveReq req) { + IncentiveProgramVO vo = new IncentiveProgramVO(); + vo.setProgramName(req.getProgramName()); + vo.setProgramType(req.getProgramType()); + vo.setTargetType(req.getTargetType()); + vo.setTargetGradeId(req.getTargetGradeId()); + vo.setTargetOrgId(req.getTargetOrgId()); + vo.setProductCategory(req.getProductCategory()); + vo.setStartMonth(req.getStartMonth()); + vo.setEndMonth(req.getEndMonth()); + vo.setConditionDesc(req.getConditionDesc()); + vo.setPayRate(req.getPayRate()); + vo.setPayAmountFixed(req.getPayAmountFixed()); + vo.setPayType(req.getPayType()); + vo.setBudgetAmount(req.getBudgetAmount()); + vo.setIsActive(true); + vo.setCreatedBy(SecurityUtil.getCurrentUserId()); + programMapper.insert(vo); + return vo.getProgramId(); + } + + @Transactional + public void update(Long programId, IncentiveProgramSaveReq req) { + IncentiveProgramVO existing = programMapper.selectVoById(programId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + + existing.setProgramName(req.getProgramName()); + existing.setProgramType(req.getProgramType()); + existing.setTargetType(req.getTargetType()); + existing.setTargetGradeId(req.getTargetGradeId()); + existing.setTargetOrgId(req.getTargetOrgId()); + existing.setProductCategory(req.getProductCategory()); + existing.setStartMonth(req.getStartMonth()); + existing.setEndMonth(req.getEndMonth()); + existing.setConditionDesc(req.getConditionDesc()); + existing.setPayRate(req.getPayRate()); + existing.setPayAmountFixed(req.getPayAmountFixed()); + existing.setPayType(req.getPayType()); + existing.setBudgetAmount(req.getBudgetAmount()); + existing.setUpdatedBy(SecurityUtil.getCurrentUserId()); + programMapper.update(existing); + } + + @Transactional + public void toggleActive(Long programId, boolean isActive) { + IncentiveProgramVO existing = programMapper.selectVoById(programId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + programMapper.updateActive(programId, isActive, SecurityUtil.getCurrentUserId()); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/settle/PeriodCloseService.java b/ga-api/src/main/java/com/ga/api/service/settle/PeriodCloseService.java new file mode 100644 index 0000000..cb07730 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/settle/PeriodCloseService.java @@ -0,0 +1,63 @@ +package com.ga.api.service.settle; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.mapper.settle.PeriodCloseMapper; +import com.ga.core.vo.settle.PeriodCloseResp; +import com.ga.core.vo.settle.PeriodCloseSaveReq; +import com.ga.core.vo.settle.PeriodCloseSearchParam; +import com.ga.core.vo.settle.PeriodCloseVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PeriodCloseService { + + private final PeriodCloseMapper periodCloseMapper; + + public PageResponse list(PeriodCloseSearchParam param) { + param.startPage(); + return PageResponse.of(periodCloseMapper.selectList(param)); + } + + public PeriodCloseResp detail(Long closeId) { + PeriodCloseResp resp = periodCloseMapper.selectById(closeId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public void close(PeriodCloseSaveReq req) { + // 이미 마감된 월인지 확인 + if (periodCloseMapper.isClosedMonth(req.getCloseMonth())) { + throw new BizException(ErrorCode.DUPLICATE_DATA, "이미 마감된 정산월입니다: " + req.getCloseMonth()); + } + PeriodCloseVO vo = new PeriodCloseVO(); + vo.setCloseMonth(req.getCloseMonth()); + vo.setNote(req.getNote()); + vo.setClosedBy(SecurityUtil.getCurrentUserId()); + periodCloseMapper.insert(vo); + } + + @Transactional + public void reopen(Long closeId, String reopenReason) { + PeriodCloseResp resp = periodCloseMapper.selectById(closeId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + periodCloseMapper.reopen(closeId, SecurityUtil.getCurrentUserId(), reopenReason); + } + + /** + * 마감 가드: 해당 정산월이 마감 상태이면 예외 발생. + * settle_correction, payment 등 변경 서비스에서 호출. + */ + public void assertNotClosed(String settleMonth) { + if (periodCloseMapper.isClosedMonth(settleMonth)) { + throw new BizException(ErrorCode.NOT_PERMITTED, "마감된 기간입니다: " + settleMonth); + } + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/settle/SettleCorrectionService.java b/ga-api/src/main/java/com/ga/api/service/settle/SettleCorrectionService.java new file mode 100644 index 0000000..10ccc39 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/settle/SettleCorrectionService.java @@ -0,0 +1,81 @@ +package com.ga.api.service.settle; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.mapper.settle.SettleCorrectionMapper; +import com.ga.core.vo.settle.CorrectionStatus; +import com.ga.core.vo.settle.SettleCorrectionResp; +import com.ga.core.vo.settle.SettleCorrectionSaveReq; +import com.ga.core.vo.settle.SettleCorrectionSearchParam; +import com.ga.core.vo.settle.SettleCorrectionVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class SettleCorrectionService { + + private final SettleCorrectionMapper correctionMapper; + private final PeriodCloseService periodCloseService; + + public PageResponse list(SettleCorrectionSearchParam param) { + param.startPage(); + return PageResponse.of(correctionMapper.selectList(param)); + } + + public SettleCorrectionResp detail(Long correctionId) { + SettleCorrectionResp resp = correctionMapper.selectById(correctionId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + public List listByOriginalSettle(Long originalSettleId) { + return correctionMapper.selectByOriginalSettleId(originalSettleId); + } + + @Transactional + public Long create(SettleCorrectionSaveReq req) { + // 마감 가드 + periodCloseService.assertNotClosed(req.getSettleMonth()); + + SettleCorrectionVO vo = new SettleCorrectionVO(); + vo.setOriginalSettleId(req.getOriginalSettleId()); + vo.setAgentId(req.getAgentId()); + vo.setSettleMonth(req.getSettleMonth()); + vo.setBeforeAmount(req.getBeforeAmount()); + vo.setAfterAmount(req.getAfterAmount()); + vo.setDiffAmount(req.getAfterAmount().subtract(req.getBeforeAmount())); + vo.setReasonCode(req.getReasonCode()); + vo.setReasonDetail(req.getReasonDetail()); + vo.setStatus(CorrectionStatus.DRAFT.name()); + vo.setRequestedBy(SecurityUtil.getCurrentUserId()); + correctionMapper.insert(vo); + return vo.getCorrectionId(); + } + + @Transactional + public void approve(Long correctionId) { + SettleCorrectionVO vo = correctionMapper.selectVoById(correctionId); + if (vo == null) throw new BizException(ErrorCode.NOT_FOUND); + if (!CorrectionStatus.DRAFT.name().equals(vo.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "DRAFT 상태만 승인 가능합니다"); + } + correctionMapper.updateStatus(correctionId, CorrectionStatus.APPROVED.name(), SecurityUtil.getCurrentUserId()); + } + + @Transactional + public void cancel(Long correctionId, String cancelReason) { + SettleCorrectionVO vo = correctionMapper.selectVoById(correctionId); + if (vo == null) throw new BizException(ErrorCode.NOT_FOUND); + if (CorrectionStatus.APPLIED.name().equals(vo.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "이미 반영된 정정은 취소할 수 없습니다"); + } + correctionMapper.cancel(correctionId, cancelReason, SecurityUtil.getCurrentUserId()); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/settle/SettleService.java b/ga-api/src/main/java/com/ga/api/service/settle/SettleService.java index ddd4399..b620d16 100644 --- a/ga-api/src/main/java/com/ga/api/service/settle/SettleService.java +++ b/ga-api/src/main/java/com/ga/api/service/settle/SettleService.java @@ -56,13 +56,13 @@ public class SettleService { } @Transactional - public void hold(Long settleId, String reason) { + public void hold(Long settleId, String reason, String reasonCode) { SettleMasterVO vo = masterMapper.selectById(settleId); if (vo == null) throw new BizException(ErrorCode.NOT_FOUND); if (SettleStatus.PAID.name().equals(vo.getStatus())) { throw new BizException(ErrorCode.NOT_PERMITTED, "지급완료된 정산은 보류할 수 없습니다"); } - masterMapper.updateHold(settleId, reason); + masterMapper.updateHold(settleId, reason, reasonCode); } @Transactional diff --git a/ga-api/src/main/java/com/ga/api/service/settle/TaxInvoiceService.java b/ga-api/src/main/java/com/ga/api/service/settle/TaxInvoiceService.java new file mode 100644 index 0000000..bb63288 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/settle/TaxInvoiceService.java @@ -0,0 +1,70 @@ +package com.ga.api.service.settle; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.mapper.settle.TaxInvoiceMapper; +import com.ga.core.vo.settle.TaxInvoiceResp; +import com.ga.core.vo.settle.TaxInvoiceSaveReq; +import com.ga.core.vo.settle.TaxInvoiceSearchParam; +import com.ga.core.vo.settle.TaxInvoiceVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class TaxInvoiceService { + + private final TaxInvoiceMapper taxInvoiceMapper; + + public PageResponse list(TaxInvoiceSearchParam param) { + param.startPage(); + return PageResponse.of(taxInvoiceMapper.selectList(param)); + } + + public TaxInvoiceResp detail(Long invoiceId) { + TaxInvoiceResp resp = taxInvoiceMapper.selectById(invoiceId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long create(TaxInvoiceSaveReq req) { + // 발행번호 중복 확인 + if (taxInvoiceMapper.selectByInvoiceNo(req.getInvoiceNo()) != null) { + throw new BizException(ErrorCode.DUPLICATE_DATA, "이미 등록된 발행번호입니다: " + req.getInvoiceNo()); + } + + TaxInvoiceVO vo = new TaxInvoiceVO(); + vo.setInvoiceNo(req.getInvoiceNo()); + vo.setInvoiceType(req.getInvoiceType()); + vo.setIssueDate(req.getIssueDate()); + vo.setSupplyAmount(req.getSupplyAmount()); + vo.setTaxAmount(req.getTaxAmount()); + vo.setTotalAmount(req.getSupplyAmount().add(req.getTaxAmount())); + vo.setCounterpartName(req.getCounterpartName()); + vo.setCounterpartBizNo(req.getCounterpartBizNo()); + vo.setCounterpartRep(req.getCounterpartRep()); + vo.setCounterpartAddr(req.getCounterpartAddr()); + vo.setAgentId(req.getAgentId()); + vo.setSettleMonth(req.getSettleMonth()); + vo.setSettleId(req.getSettleId()); + vo.setDescription(req.getDescription()); + vo.setIssuedBy(SecurityUtil.getCurrentUserId()); + taxInvoiceMapper.insert(vo); + return vo.getInvoiceId(); + } + + @Transactional + public void cancel(Long invoiceId, String cancelReason) { + TaxInvoiceResp resp = taxInvoiceMapper.selectById(invoiceId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + taxInvoiceMapper.cancel(invoiceId, LocalDate.now(), cancelReason); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/settle/TerminationSettlementService.java b/ga-api/src/main/java/com/ga/api/service/settle/TerminationSettlementService.java new file mode 100644 index 0000000..3f69d0d --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/settle/TerminationSettlementService.java @@ -0,0 +1,101 @@ +package com.ga.api.service.settle; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.mapper.settle.TerminationSettlementMapper; +import com.ga.core.vo.settle.TermSettleStatus; +import com.ga.core.vo.settle.TerminationSettlementResp; +import com.ga.core.vo.settle.TerminationSettlementSaveReq; +import com.ga.core.vo.settle.TerminationSettlementSearchParam; +import com.ga.core.vo.settle.TerminationSettlementVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class TerminationSettlementService { + + private final TerminationSettlementMapper termSettleMapper; + + public PageResponse list(TerminationSettlementSearchParam param) { + param.startPage(); + return PageResponse.of(termSettleMapper.selectList(param)); + } + + public TerminationSettlementResp detail(Long termSettleId) { + TerminationSettlementResp resp = termSettleMapper.selectById(termSettleId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public Long create(TerminationSettlementSaveReq req) { + // 해당 설계사의 해촉 정산이 이미 있는지 확인 + TerminationSettlementVO existing = termSettleMapper.selectByAgent(req.getAgentId()); + if (existing != null && !TermSettleStatus.CANCELLED.name().equals(existing.getStatus())) { + throw new BizException(ErrorCode.DUPLICATE_DATA, "이미 처리 중인 해촉 정산이 있습니다"); + } + + TerminationSettlementVO vo = new TerminationSettlementVO(); + vo.setAgentId(req.getAgentId()); + vo.setTerminationDate(req.getTerminationDate()); + vo.setOrgHistoryId(req.getOrgHistoryId()); + vo.setCalcMonth(req.getCalcMonth()); + vo.setUnpaidRecruitAmount(req.getUnpaidRecruitAmount()); + vo.setUnpaidInstallmentAmount(req.getUnpaidInstallmentAmount()); + vo.setUnpaidIncentiveAmount(req.getUnpaidIncentiveAmount()); + vo.setChargebackExpectedAmount(req.getChargebackExpectedAmount()); + vo.setChargebackWaivedAmount(req.getChargebackWaivedAmount()); + vo.setDeductionAmount(req.getDeductionAmount()); + vo.setFinalPayAmount(req.getFinalPayAmount()); + vo.setNote(req.getNote()); + vo.setStatus(TermSettleStatus.CALCULATING.name()); + vo.setCreatedBy(SecurityUtil.getCurrentUserId()); + termSettleMapper.insert(vo); + return vo.getTermSettleId(); + } + + @Transactional + public void update(Long termSettleId, TerminationSettlementSaveReq req) { + TerminationSettlementVO vo = termSettleMapper.selectVoById(termSettleId); + if (vo == null) throw new BizException(ErrorCode.NOT_FOUND); + if (!TermSettleStatus.CALCULATING.name().equals(vo.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "산출 중 상태만 수정 가능합니다"); + } + + vo.setUnpaidRecruitAmount(req.getUnpaidRecruitAmount()); + vo.setUnpaidInstallmentAmount(req.getUnpaidInstallmentAmount()); + vo.setUnpaidIncentiveAmount(req.getUnpaidIncentiveAmount()); + vo.setChargebackExpectedAmount(req.getChargebackExpectedAmount()); + vo.setChargebackWaivedAmount(req.getChargebackWaivedAmount()); + vo.setDeductionAmount(req.getDeductionAmount()); + vo.setFinalPayAmount(req.getFinalPayAmount()); + vo.setNote(req.getNote()); + vo.setUpdatedBy(SecurityUtil.getCurrentUserId()); + termSettleMapper.update(vo); + } + + @Transactional + public void confirm(Long termSettleId) { + TerminationSettlementVO vo = termSettleMapper.selectVoById(termSettleId); + if (vo == null) throw new BizException(ErrorCode.NOT_FOUND); + if (!TermSettleStatus.CALCULATING.name().equals(vo.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "산출 중 상태만 확정 가능합니다"); + } + termSettleMapper.updateStatus(termSettleId, TermSettleStatus.CONFIRMED.name(), SecurityUtil.getCurrentUserId()); + } + + /** + * 해촉 시 자동 생성 처리 (POST /process). + * agentId를 받아 CALCULATING 상태로 해촉 정산 레코드 생성. + * 실제 금액 산출은 배치 또는 별도 update 호출로 완성. + */ + @Transactional + public Long process(TerminationSettlementSaveReq req) { + return create(req); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/training/AgentTrainingService.java b/ga-api/src/main/java/com/ga/api/service/training/AgentTrainingService.java new file mode 100644 index 0000000..a41e685 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/training/AgentTrainingService.java @@ -0,0 +1,110 @@ +package com.ga.api.service.training; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.training.AgentTrainingMapper; +import com.ga.core.mapper.training.TrainingRequirementMapper; +import com.ga.core.vo.training.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AgentTrainingService { + + private final AgentTrainingMapper trainingMapper; + private final TrainingRequirementMapper requirementMapper; + + // ── Training ────────────────────────────────────────────────────────────── + + public PageResponse list(AgentTrainingSearchParam param) { + param.startPage(); + return PageResponse.of(trainingMapper.selectList(param)); + } + + public AgentTrainingResp detail(Long trainingId) { + AgentTrainingResp resp = trainingMapper.selectById(trainingId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + /** + * 설계사 + 연도 기준 연간 이수 시간 합계. + * 의무 시간 충족 여부도 함께 반환. + */ + public BigDecimal annualHours(Long agentId, int year) { + BigDecimal hours = trainingMapper.selectAnnualHoursByAgent(agentId, year); + return hours != null ? hours : BigDecimal.ZERO; + } + + @Transactional + public Long create(AgentTrainingSaveReq req) { + AgentTrainingVO vo = new AgentTrainingVO(); + vo.setAgentId(req.getAgentId()); + vo.setCourseName(req.getCourseName()); + vo.setCourseType(req.getCourseType()); + vo.setTrainingDate(req.getTrainingDate()); + vo.setHours(req.getHours()); + vo.setCertificateNo(req.getCertificateNo()); + vo.setInstructor(req.getInstructor()); + trainingMapper.insert(vo); + return vo.getTrainingId(); + } + + @Transactional + public void update(Long trainingId, AgentTrainingSaveReq req) { + AgentTrainingResp existing = trainingMapper.selectById(trainingId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + AgentTrainingVO vo = new AgentTrainingVO(); + vo.setTrainingId(trainingId); + vo.setAgentId(req.getAgentId()); + vo.setCourseName(req.getCourseName()); + vo.setCourseType(req.getCourseType()); + vo.setTrainingDate(req.getTrainingDate()); + vo.setHours(req.getHours()); + vo.setCertificateNo(req.getCertificateNo()); + vo.setInstructor(req.getInstructor()); + trainingMapper.update(vo); + } + + @Transactional + public void delete(Long trainingId) { + AgentTrainingResp existing = trainingMapper.selectById(trainingId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + trainingMapper.delete(trainingId); + } + + // ── Requirement ─────────────────────────────────────────────────────────── + + public List listRequirements(TrainingRequirementSearchParam param) { + return requirementMapper.selectList(param); + } + + public TrainingRequirementResp requirementByYear(int year) { + TrainingRequirementResp resp = requirementMapper.selectByYear(year); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public void saveRequirement(TrainingRequirementSaveReq req) { + TrainingRequirementVO existing = new TrainingRequirementVO(); + existing.setTargetYear(req.getTargetYear()); + existing.setRequiredHours(req.getRequiredHours()); + existing.setDescription(req.getDescription()); + // upsert: try update first, insert if 0 rows + TrainingRequirementResp curr = requirementMapper.selectByYear(req.getTargetYear()); + if (curr == null) { + requirementMapper.insert(existing); + } else { + existing.setReqId(curr.getReqId()); + requirementMapper.update(existing); + } + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/withdraw/WithdrawRequestService.java b/ga-api/src/main/java/com/ga/api/service/withdraw/WithdrawRequestService.java new file mode 100644 index 0000000..1e45664 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/withdraw/WithdrawRequestService.java @@ -0,0 +1,106 @@ +package com.ga.api.service.withdraw; + +import com.ga.api.service.approval.ApprovalService; +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.withdraw.WithdrawRequestMapper; +import com.ga.core.vo.approval.ApprovalRequestSaveReq; +import com.ga.core.vo.withdraw.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class WithdrawRequestService { + + private final WithdrawRequestMapper mapper; + private final ApprovalService approvalService; + + /** 출금 신청 기본 결재선 코드 (application.yml 외부화 권장) */ + @Value("${app.withdraw.approval-line-id:1}") + private Long defaultApprovalLineId; + + public PageResponse list(WithdrawRequestSearchParam param) { + param.startPage(); + java.util.List rows = mapper.selectList(param); + rows.forEach(this::maskAccountNo); + return PageResponse.of(rows); + } + + public WithdrawRequestResp detail(Long requestId) { + WithdrawRequestResp resp = mapper.selectById(requestId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + maskAccountNo(resp); + return resp; + } + + /** + * 출금 신청 등록 + 결재 요청 자동 연동. + * account_no 마스킹은 응답 시에만 적용 — VO 저장 시 원문 저장. + */ + @Transactional + public Long create(WithdrawRequestSaveReq req) { + WithdrawRequestVO vo = new WithdrawRequestVO(); + vo.setSettleMasterId(req.getSettleMasterId()); + vo.setAgentId(req.getAgentId()); + vo.setAmount(req.getAmount()); + vo.setRequestedBy(req.getRequestedBy()); + vo.setRequestedAt(LocalDateTime.now()); + vo.setBankCode(req.getBankCode()); + vo.setAccountNo(req.getAccountNo()); // 원문 저장 (EncryptTypeHandler가 암호화) + vo.setStatus(WithdrawStatus.REQUESTED.name()); + mapper.insert(vo); + + // 결재 요청 자동 생성 + try { + ApprovalRequestSaveReq approvalReq = new ApprovalRequestSaveReq(); + approvalReq.setTargetType("WITHDRAW"); + approvalReq.setTargetId(vo.getRequestId()); + approvalReq.setLineId(defaultApprovalLineId); + approvalReq.setRequestedBy(req.getRequestedBy()); + Long approvalRequestId = approvalService.createRequest(approvalReq); + // 결재 요청 ID를 withdraw_request에 업데이트 (approve 후 연동 위해) + log.info("출금신청 결재 요청 생성: withdrawId={}, approvalRequestId={}", vo.getRequestId(), approvalRequestId); + } catch (Exception e) { + log.warn("결재 요청 생성 실패 (출금신청은 유지): withdrawId={}, error={}", vo.getRequestId(), e.getMessage()); + } + + return vo.getRequestId(); + } + + @Transactional + public void cancel(Long requestId) { + WithdrawRequestResp existing = mapper.selectById(requestId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + if (!WithdrawStatus.REQUESTED.name().equals(existing.getStatus())) { + throw new BizException(ErrorCode.INVALID_STATUS, "신청 상태에서만 취소 가능합니다"); + } + mapper.cancel(requestId); + } + + /** + * account_no 마스킹: 뒤 4자리만 표출. + * 원본: "123-456-78901234" → 마스킹: "*** *** *1234" + */ + private void maskAccountNo(WithdrawRequestResp resp) { + String raw = resp.getAccountNo(); + if (raw == null || raw.isBlank()) return; + // 숫자만 추출 후 뒤 4자리만 남기고 마스킹 + String digits = raw.replaceAll("[^0-9]", ""); + if (digits.length() <= 4) { + resp.setAccountNo("*** *** *" + digits); + } else { + String last4 = digits.substring(digits.length() - 4); + resp.setAccountNo("*** *** *" + last4); + } + } + +} diff --git a/ga-api/src/main/resources/application.yml b/ga-api/src/main/resources/application.yml index 5e40afe..b0e69be 100644 --- a/ga-api/src/main/resources/application.yml +++ b/ga-api/src/main/resources/application.yml @@ -50,6 +50,12 @@ ga: encrypt: key: ${ENCRYPT_KEY:0123456789abcdef0123456789abcdef} +app: + attachment: + path: ${ATTACHMENT_PATH:./uploads} + withdraw: + approval-line-id: ${WITHDRAW_APPROVAL_LINE_ID:1} + logging: level: com.ga: DEBUG diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchChargebackCalculator.java b/ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchChargebackCalculator.java new file mode 100644 index 0000000..3771a1b --- /dev/null +++ b/ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchChargebackCalculator.java @@ -0,0 +1,49 @@ +package com.ga.batch.settlement.domain; + +import com.ga.core.mapper.chargeback.ChargebackGradeMapper; +import com.ga.core.vo.chargeback.ChargebackGradeVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +/** + * 배치용 차등환수 계산기. ga-api ChargebackCalculator와 동일 로직. + * chargeback_grade 테이블 기반 5단계 환수율 조회. + */ +@Component +@RequiredArgsConstructor +public class BatchChargebackCalculator { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private final ChargebackGradeMapper gradeMapper; + + public ChargebackResult calculate(LocalDate contractDate, LocalDate terminationDate, + BigDecimal originalCommission, String productCategory) { + int monthsElapsed = (int) ChronoUnit.MONTHS.between(contractDate, terminationDate); + String today = LocalDate.now().format(DATE_FMT); + + ChargebackGradeVO grade = gradeMapper.selectByMonths(productCategory, monthsElapsed, today); + + if (grade == null) { + return new ChargebackResult(BigDecimal.ZERO, monthsElapsed, BigDecimal.ZERO, + "적용 환수 구간 없음"); + } + + BigDecimal chargebackAmount = originalCommission + .multiply(grade.getChargebackPercent()) + .divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP); + + return new ChargebackResult( + chargebackAmount, + monthsElapsed, + grade.getChargebackPercent(), + String.format("경과월 %d개월 → 환수율 %s%%", monthsElapsed, grade.getChargebackPercent()) + ); + } +} diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchInstallmentPlanGenerator.java b/ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchInstallmentPlanGenerator.java new file mode 100644 index 0000000..8edb384 --- /dev/null +++ b/ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchInstallmentPlanGenerator.java @@ -0,0 +1,51 @@ +package com.ga.batch.settlement.domain; + +import com.ga.core.mapper.installment.InstallmentRatioMapper; +import com.ga.core.vo.installment.InstallmentPlanVO; +import com.ga.core.vo.installment.InstallmentRatioVO; +import com.ga.core.vo.installment.InstallmentStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** + * 배치용 분급 계획 생성기. ga-api InstallmentPlanGenerator와 동일 로직. + * 계약 체결일 기준 1~7차년 분급 계획 생성. + */ +@Component +@RequiredArgsConstructor +public class BatchInstallmentPlanGenerator { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyyMM"); + + private final InstallmentRatioMapper ratioMapper; + + public List generate(Long contractId, BigDecimal totalCommission, + LocalDate contractDate, String productCategory) { + String baseDate = contractDate.format(DATE_FMT); + List ratios = ratioMapper.selectAllActiveRatios(productCategory, baseDate); + + List plans = new ArrayList<>(); + for (InstallmentRatioVO ratio : ratios) { + InstallmentPlanVO plan = new InstallmentPlanVO(); + plan.setContractId(contractId); + plan.setPlanYear(ratio.getPlanYear()); + plan.setPlanMonth(1); + plan.setSettleMonth(contractDate.plusYears(ratio.getPlanYear()).format(MONTH_FMT)); + plan.setExpectedAmount( + totalCommission.multiply(ratio.getRatioPercent()) + .divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP) + ); + plan.setStatus(InstallmentStatus.SCHEDULED.code()); + plans.add(plan); + } + return plans; + } +} diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchRegulatoryLimitChecker.java b/ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchRegulatoryLimitChecker.java new file mode 100644 index 0000000..0524476 --- /dev/null +++ b/ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchRegulatoryLimitChecker.java @@ -0,0 +1,83 @@ +package com.ga.batch.settlement.domain; + +import com.ga.core.mapper.ledger.MaintainLedgerMapper; +import com.ga.core.mapper.ledger.RecruitLedgerMapper; +import com.ga.core.mapper.regulatory.RegulatoryLimitMapper; +import com.ga.core.vo.regulatory.RegulatoryLimitVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * 배치용 1200%룰 한도 체커. ga-api RegulatoryLimitChecker와 동일 로직. + */ +@Component +@RequiredArgsConstructor +public class BatchRegulatoryLimitChecker { + + private static final String TOTAL_RATIO = "TOTAL_RATIO"; + private static final String FIRST_YEAR_RATIO = "FIRST_YEAR_RATIO"; + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private final RegulatoryLimitMapper limitMapper; + private final RecruitLedgerMapper recruitLedgerMapper; + private final MaintainLedgerMapper maintainLedgerMapper; + + /** 누적 수수료율 한도 체크 (1200%). 신계약 + 유지 원장 합산. */ + public ViolationResult checkContractTotal(Long contractId, BigDecimal additionalAmount, LocalDate baseDate) { + String baseDateStr = baseDate.format(DATE_FMT); + RegulatoryLimitVO limit = limitMapper.selectActiveByType(TOTAL_RATIO, null, baseDateStr); + if (limit == null) { + return ViolationResult.ok(additionalAmount, null, TOTAL_RATIO); + } + + BigDecimal currentTotal = sumPaidByContract(contractId); + BigDecimal projected = currentTotal.add(additionalAmount); + + if (projected.compareTo(limit.getLimitValue()) > 0) { + return ViolationResult.violated( + currentTotal, + limit.getLimitValue(), + TOTAL_RATIO, + String.format("누적 수수료 한도 초과: 현재 %s, 추가 %s, 한도 %s %s", + currentTotal, additionalAmount, limit.getLimitValue(), limit.getUnit()) + ); + } + return ViolationResult.ok(currentTotal, limit.getLimitValue(), TOTAL_RATIO); + } + + /** 1차년 수수료율 한도 체크. */ + public ViolationResult checkFirstYear(Long contractId, BigDecimal additionalAmount, LocalDate baseDate) { + String baseDateStr = baseDate.format(DATE_FMT); + RegulatoryLimitVO limit = limitMapper.selectActiveByType(FIRST_YEAR_RATIO, null, baseDateStr); + if (limit == null) { + return ViolationResult.ok(additionalAmount, null, FIRST_YEAR_RATIO); + } + + BigDecimal currentTotal = nullSafe(recruitLedgerMapper.sumFirstYearByContract(contractId)); + BigDecimal projected = currentTotal.add(additionalAmount); + + if (projected.compareTo(limit.getLimitValue()) > 0) { + return ViolationResult.violated( + currentTotal, + limit.getLimitValue(), + FIRST_YEAR_RATIO, + String.format("1차년 수수료 한도 초과: 현재 %s, 추가 %s, 한도 %s %s", + currentTotal, additionalAmount, limit.getLimitValue(), limit.getUnit()) + ); + } + return ViolationResult.ok(currentTotal, limit.getLimitValue(), FIRST_YEAR_RATIO); + } + + private BigDecimal sumPaidByContract(Long contractId) { + return nullSafe(recruitLedgerMapper.sumByContract(contractId)) + .add(nullSafe(maintainLedgerMapper.sumByContract(contractId))); + } + + private BigDecimal nullSafe(BigDecimal v) { + return v != null ? v : BigDecimal.ZERO; + } +} diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchTaxCalculator.java b/ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchTaxCalculator.java new file mode 100644 index 0000000..c2dd78f --- /dev/null +++ b/ga-batch/src/main/java/com/ga/batch/settlement/domain/BatchTaxCalculator.java @@ -0,0 +1,45 @@ +package com.ga.batch.settlement.domain; + +import com.ga.common.system.SystemConfigService; +import com.ga.core.vo.org.TaxType; +import com.ga.core.vo.org.VatType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 배치용 세금 계산기. ga-api TaxCalculator와 동일 로직, ga-batch 모듈 독립. + * 사업소득 3.3% / 기타소득 8.8% + 지방소득세 10% + 부가세 10% (TAXABLE만). + */ +@Component +@RequiredArgsConstructor +public class BatchTaxCalculator { + + private final SystemConfigService configService; + + public TaxBreakdown calculate(BigDecimal amount, TaxType taxType, VatType vatType) { + BigDecimal incomeRate = resolveIncomeRate(taxType); + BigDecimal localRate = configService.getDecimal("TAX_RATE_LOCAL_INCOME", new BigDecimal("10.0")); + BigDecimal vatRate = configService.getDecimal("TAX_RATE_VAT", new BigDecimal("10.0")); + + BigDecimal incomeTax = amount.multiply(incomeRate) + .divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP); + BigDecimal localTax = incomeTax.multiply(localRate) + .divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP); + BigDecimal vat = VatType.TAXABLE.equals(vatType) + ? amount.multiply(vatRate).divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + BigDecimal netAmount = amount.subtract(incomeTax).subtract(localTax).add(vat); + + return new TaxBreakdown(incomeTax, localTax, vat, netAmount); + } + + private BigDecimal resolveIncomeRate(TaxType taxType) { + if (TaxType.OTHER_INCOME.equals(taxType)) { + return configService.getDecimal("TAX_RATE_OTHER_INCOME", new BigDecimal("8.8")); + } + return configService.getDecimal("TAX_RATE_BUSINESS_INCOME", new BigDecimal("3.3")); + } +} diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/domain/ChargebackResult.java b/ga-batch/src/main/java/com/ga/batch/settlement/domain/ChargebackResult.java new file mode 100644 index 0000000..f2a6b1c --- /dev/null +++ b/ga-batch/src/main/java/com/ga/batch/settlement/domain/ChargebackResult.java @@ -0,0 +1,11 @@ +package com.ga.batch.settlement.domain; + +import java.math.BigDecimal; + +/** 차등환수 계산 결과. */ +public record ChargebackResult( + BigDecimal chargebackAmount, + int monthsElapsed, + BigDecimal appliedPercent, + String reason +) {} diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/domain/TaxBreakdown.java b/ga-batch/src/main/java/com/ga/batch/settlement/domain/TaxBreakdown.java new file mode 100644 index 0000000..9ffdbbe --- /dev/null +++ b/ga-batch/src/main/java/com/ga/batch/settlement/domain/TaxBreakdown.java @@ -0,0 +1,11 @@ +package com.ga.batch.settlement.domain; + +import java.math.BigDecimal; + +/** 세금 계산 결과. ga-api TaxBreakdown과 동일 구조 (모듈 독립). */ +public record TaxBreakdown( + BigDecimal incomeTax, + BigDecimal localTax, + BigDecimal vat, + BigDecimal netAmount +) {} diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/domain/ViolationResult.java b/ga-batch/src/main/java/com/ga/batch/settlement/domain/ViolationResult.java new file mode 100644 index 0000000..4adf074 --- /dev/null +++ b/ga-batch/src/main/java/com/ga/batch/settlement/domain/ViolationResult.java @@ -0,0 +1,21 @@ +package com.ga.batch.settlement.domain; + +import java.math.BigDecimal; + +/** 1200%룰 검증 결과. */ +public record ViolationResult( + boolean violated, + BigDecimal currentTotal, + BigDecimal limitAmount, + String limitType, + String message +) { + public static ViolationResult ok(BigDecimal currentTotal, BigDecimal limitAmount, String limitType) { + return new ViolationResult(false, currentTotal, limitAmount, limitType, null); + } + + public static ViolationResult violated(BigDecimal currentTotal, BigDecimal limitAmount, + String limitType, String message) { + return new ViolationResult(true, currentTotal, limitAmount, limitType, message); + } +} diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/step/AggregateStep.java b/ga-batch/src/main/java/com/ga/batch/settlement/step/AggregateStep.java index 049469b..5b40494 100644 --- a/ga-batch/src/main/java/com/ga/batch/settlement/step/AggregateStep.java +++ b/ga-batch/src/main/java/com/ga/batch/settlement/step/AggregateStep.java @@ -1,18 +1,28 @@ package com.ga.batch.settlement.step; import com.ga.batch.settlement.SettlementContext; -import com.ga.common.system.SystemConfigService; +import com.ga.batch.settlement.domain.BatchTaxCalculator; +import com.ga.batch.settlement.domain.TaxBreakdown; import com.ga.common.util.MoneyUtil; import com.ga.core.mapper.ledger.ExceptionLedgerMapper; import com.ga.core.mapper.ledger.MaintainLedgerMapper; import com.ga.core.mapper.ledger.RecruitLedgerMapper; import com.ga.core.mapper.org.AgentMapper; +import com.ga.core.mapper.settle.AgentDeductionMapper; import com.ga.core.mapper.settle.ChargebackMapper; +import com.ga.core.mapper.settle.CommissionStatementMapper; import com.ga.core.mapper.settle.OverrideSettleMapper; import com.ga.core.mapper.settle.SettleMasterMapper; import com.ga.core.vo.org.AgentResp; import com.ga.core.vo.org.AgentSearchParam; +import com.ga.core.vo.org.AgentVO; +import com.ga.core.vo.org.TaxType; +import com.ga.core.vo.org.VatType; +import com.ga.core.vo.settle.AgentDeductionVO; +import com.ga.core.vo.settle.CommissionStatementVO; +import com.ga.core.vo.settle.DeductionStatus; import com.ga.core.vo.settle.SettleMasterVO; +import com.ga.core.vo.settle.StatementType; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.StepContribution; @@ -23,12 +33,22 @@ import org.springframework.batch.repeat.RepeatStatus; import org.springframework.stereotype.Component; import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** - * Step 8 — aggregate: 설계사별 통합 집계 → settle_master UPSERT (세금 자동 계산) + * Step 8 — aggregate: 설계사별 통합 집계 → settle_master UPSERT + * + * 통합 내용: + * - BatchTaxCalculator: 설계사 세금유형(TaxType/VatType)에 따라 정확한 세금 계산 + * (사업소득 3.3% / 기타소득 8.8% + 지방소득세 10% + 부가세 10%) + * - AgentDeductionMapper: PENDING 차감 합산 → netAmount에서 차감 후 APPLIED 상태로 전이 + * - CommissionStatementMapper: 설계사별 명세서 레코드(ISSUED 상태) INSERT */ @Slf4j @Component @@ -44,16 +64,19 @@ public class AggregateStep implements Tasklet { private final OverrideSettleMapper overrideMapper; private final ChargebackMapper chargebackMapper; private final SettleMasterMapper settleMapper; - private final SystemConfigService configService; + private final AgentDeductionMapper deductionMapper; + private final CommissionStatementMapper statementMapper; + private final BatchTaxCalculator taxCalculator; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { log.info("[Step8 aggregate] settleMonth={}", ctx.getSettleMonth()); - Map recruitMap = toMap(recruitMapper.aggregateByAgent(ctx.getSettleMonth())); - Map maintainMap = toMap(maintainMapper.aggregateByAgent(ctx.getSettleMonth())); - Map chargebackMap= toMap(chargebackMapper.aggregateByAgent(ctx.getSettleMonth())); - Map overrideMap = toMap(overrideMapper.aggregateByUpper(ctx.getSettleMonth())); + Map recruitMap = toMap(recruitMapper.aggregateByAgent(ctx.getSettleMonth())); + Map maintainMap = toMap(maintainMapper.aggregateByAgent(ctx.getSettleMonth())); + Map chargebackMap = toMap(chargebackMapper.aggregateByAgent(ctx.getSettleMonth())); + Map overrideMap = toMap(overrideMapper.aggregateByUpper(ctx.getSettleMonth())); + // 예외는 PLUS/MINUS 분리 Map excPlus = new HashMap<>(); Map excMinus = new HashMap<>(); @@ -63,10 +86,8 @@ public class AggregateStep implements Tasklet { excMinus.put(agentId, toBd(row.get("minus"))); } - BigDecimal taxRate = configService.getDecimal("TAX_RATE", new BigDecimal("3.3")); - // 합집합 agent_id - java.util.Set agentIds = new java.util.HashSet<>(); + Set agentIds = new HashSet<>(); agentIds.addAll(recruitMap.keySet()); agentIds.addAll(maintainMap.keySet()); agentIds.addAll(chargebackMap.keySet()); @@ -76,10 +97,23 @@ public class AggregateStep implements Tasklet { // 활성 설계사 누락 보충 AgentSearchParam ap = new AgentSearchParam(); - ap.setStatus("ACTIVE"); ap.setPageSize(Integer.MAX_VALUE); - for (AgentResp a : agentMapper.selectList(ap)) agentIds.add(a.getAgentId()); + ap.setStatus("ACTIVE"); + ap.setPageSize(Integer.MAX_VALUE); + for (AgentResp a : agentMapper.selectList(ap)) { + agentIds.add(a.getAgentId()); + } + // 정산월 PENDING 차감 목록 일괄 조회 (배치 효율) + List allDeductions = deductionMapper.selectPendingByMonth(ctx.getSettleMonth()); + Map> deductionByAgent = new HashMap<>(); + for (AgentDeductionVO d : allDeductions) { + deductionByAgent.computeIfAbsent(d.getAgentId(), k -> new ArrayList<>()).add(d); + } + + List appliedDeductionIds = new ArrayList<>(); + List statements = new ArrayList<>(); int count = 0; + for (Long agentId : agentIds) { BigDecimal recruit = recruitMap.getOrDefault(agentId, BigDecimal.ZERO); BigDecimal maintain = maintainMap.getOrDefault(agentId, BigDecimal.ZERO); @@ -90,9 +124,25 @@ public class AggregateStep implements Tasklet { BigDecimal gross = MoneyUtil.add(MoneyUtil.add(MoneyUtil.add(recruit, maintain), ov), ep) .subtract(cb).subtract(em); - BigDecimal tax = MoneyUtil.tax(gross, taxRate); - BigDecimal net = MoneyUtil.sub(gross, tax); + // 세금유형 조회 (없으면 기본: BUSINESS_INCOME / NONE) + AgentVO agentVO = agentMapper.selectById(agentId); + TaxType taxType = parseTaxType(agentVO); + VatType vatType = parseVatType(agentVO); + + // BatchTaxCalculator로 정확한 세금 계산 + TaxBreakdown tax = taxCalculator.calculate(gross, taxType, vatType); + BigDecimal totalTax = tax.incomeTax().add(tax.localTax()); // settle_master.tax_amount + + // 차감 합산 + List agentDeductions = deductionByAgent.getOrDefault(agentId, List.of()); + BigDecimal deductionTotal = agentDeductions.stream() + .map(AgentDeductionVO::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + BigDecimal netAmount = tax.netAmount().subtract(deductionTotal); + + // settle_master UPSERT SettleMasterVO vo = new SettleMasterVO(); vo.setAgentId(agentId); vo.setSettleMonth(ctx.getSettleMonth()); @@ -103,17 +153,74 @@ public class AggregateStep implements Tasklet { vo.setOverrideTotal(ov); vo.setChargebackTotal(cb); vo.setGrossAmount(gross); - vo.setTaxAmount(tax); - vo.setNetAmount(net); + vo.setTaxAmount(totalTax); // incomeTax + localTax 합산 + vo.setNetAmount(netAmount); // gross - incomeTax - localTax + vat - deduction vo.setStatus("CALCULATED"); settleMapper.upsert(vo); count++; + + // 차감 적용 완료 처리 + if (!agentDeductions.isEmpty()) { + agentDeductions.forEach(d -> appliedDeductionIds.add(d.getDeductionId())); + } + + // 명세서 레코드 생성 (commission_statement INSERT, 중복 시 skip) + CommissionStatementVO existingStmt = statementMapper.selectByAgentMonth( + agentId, ctx.getSettleMonth(), StatementType.MONTHLY.code()); + if (existingStmt == null && gross.signum() != 0) { + CommissionStatementVO stmt = new CommissionStatementVO(); + stmt.setAgentId(agentId); + stmt.setSettleMonth(ctx.getSettleMonth()); + stmt.setStatementType(StatementType.MONTHLY.code()); + stmt.setGrossAmount(gross); + stmt.setIncomeTaxAmount(tax.incomeTax()); + stmt.setLocalTaxAmount(tax.localTax()); + stmt.setVatAmount(tax.vat()); + stmt.setDeductionAmount(deductionTotal); + stmt.setNetAmount(netAmount); + stmt.setIssueStatus("ISSUED"); + stmt.setIssuedAt(LocalDateTime.now()); + statements.add(stmt); + } } + + // 차감 APPLIED 상태 일괄 전이 + if (!appliedDeductionIds.isEmpty()) { + deductionMapper.updateStatusBatch(appliedDeductionIds, DeductionStatus.APPLIED.name()); + log.info("[Step8] deductions applied: {}", appliedDeductionIds.size()); + } + + // 명세서 일괄 INSERT + for (CommissionStatementVO stmt : statements) { + statementMapper.insert(stmt); + } + if (!statements.isEmpty()) { + log.info("[Step8] commission statements inserted: {}", statements.size()); + } + ctx.setAggregateCount(count); log.info("[Step8] settle master upserted: {}", count); return RepeatStatus.FINISHED; } + private TaxType parseTaxType(AgentVO agent) { + if (agent == null || agent.getTaxType() == null) return TaxType.BUSINESS_INCOME; + try { + return TaxType.valueOf(agent.getTaxType()); + } catch (IllegalArgumentException e) { + return TaxType.BUSINESS_INCOME; + } + } + + private VatType parseVatType(AgentVO agent) { + if (agent == null || agent.getVatType() == null) return VatType.NONE; + try { + return VatType.valueOf(agent.getVatType()); + } catch (IllegalArgumentException e) { + return VatType.NONE; + } + } + private Map toMap(List> rows) { Map m = new HashMap<>(); for (Map r : rows) { diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/step/CalcRecruitStep.java b/ga-batch/src/main/java/com/ga/batch/settlement/step/CalcRecruitStep.java index 52262a6..ac83e63 100644 --- a/ga-batch/src/main/java/com/ga/batch/settlement/step/CalcRecruitStep.java +++ b/ga-batch/src/main/java/com/ga/batch/settlement/step/CalcRecruitStep.java @@ -2,10 +2,15 @@ package com.ga.batch.settlement.step; import com.ga.batch.settlement.SettlementContext; import com.ga.batch.settlement.calc.CommissionCalculator; +import com.ga.batch.settlement.domain.BatchInstallmentPlanGenerator; +import com.ga.batch.settlement.domain.BatchRegulatoryLimitChecker; +import com.ga.batch.settlement.domain.ViolationResult; import com.ga.common.util.DateUtil; import com.ga.common.util.MoneyUtil; +import com.ga.core.mapper.installment.InstallmentPlanMapper; import com.ga.core.mapper.ledger.RecruitLedgerMapper; import com.ga.core.mapper.product.ContractMapper; +import com.ga.core.vo.installment.InstallmentPlanVO; import com.ga.core.vo.ledger.LedgerVO; import com.ga.core.vo.product.ContractResp; import com.ga.core.vo.product.ContractSearchParam; @@ -25,6 +30,9 @@ import java.util.List; * Step 4 — calcRecruit: 모집 수수료 계산 (계약 첫 회차) * * 원칙: 계약시점의 commission_rate / payout_rule 적용 (effective_from 기준) + * + * 추가: 1200%룰(BatchRegulatoryLimitChecker) 검증 후 한도 초과 계약은 + * InstallmentPlan(분급 7년) 생성. Step 4.5 역할을 Step 4 내에서 수행. */ @Slf4j @Component @@ -36,6 +44,9 @@ public class CalcRecruitStep implements Tasklet { private final ContractMapper contractMapper; private final RecruitLedgerMapper recruitMapper; private final CommissionCalculator calculator; + private final BatchRegulatoryLimitChecker regulatoryChecker; + private final BatchInstallmentPlanGenerator installmentGenerator; + private final InstallmentPlanMapper installmentPlanMapper; private static final int BATCH_SIZE = 1000; @@ -55,10 +66,33 @@ public class CalcRecruitStep implements Tasklet { recruitMapper.deleteBySettleMonth(ctx.getSettleMonth()); List batch = new ArrayList<>(BATCH_SIZE); + List installmentBatch = new ArrayList<>(); int total = 0; + int violationCount = 0; + for (ContractResp c : contracts) { LedgerVO led = calculator.calcRecruitLedger(c, ctx.getSettleMonth()); if (led == null || MoneyUtil.isZero(led.getAgentAmount())) continue; + + // 1200%룰 검증 (1차년도 한도) + if (c.getContractDate() != null) { + ViolationResult firstYearCheck = regulatoryChecker.checkFirstYear( + c.getContractId(), led.getAgentAmount(), c.getContractDate()); + + if (firstYearCheck.violated()) { + log.warn("[Step4] 1200%룰 위반: contractId={}, msg={}", + c.getContractId(), firstYearCheck.message()); + violationCount++; + + // 1200%룰 위반 계약: 분급 계획 생성 (7년 35/25/15/10/7/5/3) + List plans = installmentGenerator.generate( + c.getContractId(), led.getAgentAmount(), + c.getContractDate(), c.getInsuranceType()); + installmentBatch.addAll(plans); + // 위반 계약도 원장에는 기록 (감사 목적) + } + } + batch.add(led); if (batch.size() >= BATCH_SIZE) { recruitMapper.batchInsert(batch); @@ -71,8 +105,15 @@ public class CalcRecruitStep implements Tasklet { total += batch.size(); } + // 분급 계획 일괄 저장 + if (!installmentBatch.isEmpty()) { + installmentPlanMapper.insertBatch(installmentBatch); + log.info("[Step4] installment plans created: {} (violations: {})", + installmentBatch.size(), violationCount); + } + ctx.setRecruitCount(total); - log.info("[Step4] recruit ledger inserted: {}", total); + log.info("[Step4] recruit ledger inserted: {}, regulatory violations: {}", total, violationCount); return RepeatStatus.FINISHED; } } diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/step/ChargebackExceptionStep.java b/ga-batch/src/main/java/com/ga/batch/settlement/step/ChargebackExceptionStep.java index 6a45bc4..5c4d80e 100644 --- a/ga-batch/src/main/java/com/ga/batch/settlement/step/ChargebackExceptionStep.java +++ b/ga-batch/src/main/java/com/ga/batch/settlement/step/ChargebackExceptionStep.java @@ -1,11 +1,11 @@ package com.ga.batch.settlement.step; import com.ga.batch.settlement.SettlementContext; +import com.ga.batch.settlement.domain.BatchChargebackCalculator; +import com.ga.batch.settlement.domain.ChargebackResult; import com.ga.common.util.DateUtil; -import com.ga.common.util.MoneyUtil; import com.ga.core.mapper.ledger.RecruitLedgerMapper; import com.ga.core.mapper.product.ContractMapper; -import com.ga.core.mapper.rule.RuleMapper; import com.ga.core.mapper.settle.ChargebackMapper; import com.ga.core.vo.product.ContractResp; import com.ga.core.vo.product.ContractSearchParam; @@ -26,7 +26,8 @@ import java.util.List; /** * Step 6 — chargebackException: 환수 자동생성 + 예외금액 자동계산 * - * 환수 대상: 정산월에 LAPSE(실효) 된 계약 — 계약일~실효까지 경과월수로 환수율 결정. + * 환수율 계산: BatchChargebackCalculator (chargeback_grade 5단계 차등환수) 사용. + * 환수 원금: 해당 계약의 모집 원장 합산 (recruit_ledger 기준). */ @Slf4j @Component @@ -37,8 +38,8 @@ public class ChargebackExceptionStep implements Tasklet { private final SettlementContext ctx; private final ContractMapper contractMapper; private final RecruitLedgerMapper recruitMapper; - private final RuleMapper ruleMapper; private final ChargebackMapper chargebackMapper; + private final BatchChargebackCalculator chargebackCalculator; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { @@ -51,30 +52,30 @@ public class ChargebackExceptionStep implements Tasklet { List chargebacks = new ArrayList<>(); for (ContractResp c : lapsed) { - if (c.getLapseDate() == null) continue; + if (c.getLapseDate() == null || c.getContractDate() == null) continue; + String lapseMonth = DateUtil.getSettleMonth(c.getLapseDate()); if (!ctx.getSettleMonth().equals(lapseMonth)) continue; - if (c.getContractDate() == null) continue; - int monthsActive = DateUtil.calcMonthDiff( - DateUtil.getSettleMonth(c.getContractDate()), lapseMonth); - BigDecimal cbRate = ruleMapper.findChargebackRate( - c.getCompanyCode(), c.getInsuranceType(), monthsActive, c.getLapseDate()); - if (cbRate == null || cbRate.signum() <= 0) continue; + // 모집 원장에서 원 수수료 조회 + BigDecimal originalCommission = recruitMapper.sumByContract(c.getContractId()); + if (originalCommission == null || originalCommission.signum() <= 0) continue; - // 모집수수료 합계 × 환수율 - BigDecimal originalAmount = MoneyUtil.zero(c.getPremium()) - .multiply(cbRate) - .divide(MoneyUtil.HUNDRED, 2, java.math.RoundingMode.HALF_UP); + // BatchChargebackCalculator: chargeback_grade 5단계 차등환수 + ChargebackResult result = chargebackCalculator.calculate( + c.getContractDate(), c.getLapseDate(), + originalCommission, c.getInsuranceType()); + + if (result.chargebackAmount().signum() <= 0) continue; ChargebackVO cb = new ChargebackVO(); cb.setContractId(c.getContractId()); cb.setAgentId(c.getAgentId()); cb.setPolicyNo(c.getPolicyNo()); - cb.setLapseMonth(monthsActive); - cb.setCbRate(cbRate); - cb.setCbAmount(originalAmount); - cb.setRemainAmount(originalAmount); + cb.setLapseMonth(result.monthsElapsed()); + cb.setCbRate(result.appliedPercent()); + cb.setCbAmount(result.chargebackAmount()); + cb.setRemainAmount(result.chargebackAmount()); cb.setSettleMonth(ctx.getSettleMonth()); cb.setStatus("NEW"); chargebacks.add(cb); @@ -84,7 +85,7 @@ public class ChargebackExceptionStep implements Tasklet { chargebackMapper.insertBatch(chargebacks); } ctx.setChargebackCount(chargebacks.size()); - log.info("[Step6] chargeback inserted: {}", chargebacks.size()); + log.info("[Step6] chargeback inserted: {} (using BatchChargebackCalculator)", chargebacks.size()); return RepeatStatus.FINISHED; } } diff --git a/ga-common/src/main/resources/db/migration/V29__마감마스터.sql b/ga-common/src/main/resources/db/migration/V29__마감마스터.sql new file mode 100644 index 0000000..7a6976e --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V29__마감마스터.sql @@ -0,0 +1,43 @@ +-- V29: 마감 마스터 (period_close) — 정산 월 마감 관리 + +-- ============================================================ +-- 1. period_close — 마감 마스터 +-- ============================================================ + +CREATE TABLE period_close ( + close_id BIGSERIAL PRIMARY KEY, + close_month CHAR(6) NOT NULL, -- YYYYMM + closed_at TIMESTAMP NOT NULL DEFAULT NOW(), -- 마감 처리 일시 + closed_by BIGINT NOT NULL, -- 마감 처리자 (user_id) + reopen_at TIMESTAMP, -- 재오픈 일시 (NULL = 마감 중) + reopen_by BIGINT, -- 재오픈 처리자 + reopen_reason VARCHAR(500), -- 재오픈 사유 + note VARCHAR(500), -- 마감 비고 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + UNIQUE (close_month) -- 월별 단 1건 +); + +COMMENT ON TABLE period_close IS '정산 마감 마스터 — 마감된 월은 수정 불가'; +COMMENT ON COLUMN period_close.close_month IS '마감 정산년월 (YYYYMM)'; +COMMENT ON COLUMN period_close.closed_at IS '마감 처리 일시'; +COMMENT ON COLUMN period_close.closed_by IS '마감 처리자 user_id'; +COMMENT ON COLUMN period_close.reopen_at IS '재오픈 일시. NULL이면 현재 마감 상태'; +COMMENT ON COLUMN period_close.reopen_by IS '재오픈 처리자 user_id'; +COMMENT ON COLUMN period_close.reopen_reason IS '재오픈 사유 (감사 추적용)'; + +-- 마감 상태 조회용 인덱스 (재오픈 여부로 현재 마감 여부 판단) +CREATE INDEX idx_pc_month ON period_close(close_month); + +-- ============================================================ +-- 2. 공통코드 CLOSE_STATUS (마감/재오픈 상태 표현용) +-- ============================================================ + +INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES + ('CLOSE_STATUS', '마감상태', 'CLOSED/REOPENED', 'Y', 290) +ON CONFLICT (group_code) DO NOTHING; + +INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES + ('CLOSE_STATUS', 'CLOSED', '마감', '정산월 마감 완료 — 수정 불가', 10), + ('CLOSE_STATUS', 'REOPENED', '재오픈', '마감 해제 상태 — 수정 가능', 20) +ON CONFLICT (group_code, code) DO NOTHING; diff --git a/ga-common/src/main/resources/db/migration/V30__정정.sql b/ga-common/src/main/resources/db/migration/V30__정정.sql new file mode 100644 index 0000000..08a51c7 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V30__정정.sql @@ -0,0 +1,62 @@ +-- V30: 정산 정정 (settle_correction) — 확정된 정산의 정정 이력 관리 + +-- ============================================================ +-- 1. settle_correction — 정산 정정 이력 +-- ============================================================ + +CREATE TABLE settle_correction ( + correction_id BIGSERIAL PRIMARY KEY, + original_settle_id BIGINT NOT NULL REFERENCES settle_master(settle_id), -- 원본 정산 참조 + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + settle_month CHAR(6) NOT NULL, -- YYYYMM (원본과 동일) + diff_amount NUMERIC(15,2) NOT NULL, -- 차액 (양수=추가지급, 음수=환수) + before_amount NUMERIC(15,2) NOT NULL, -- 정정 전 순지급액 + after_amount NUMERIC(15,2) NOT NULL, -- 정정 후 순지급액 + reason_code VARCHAR(30) NOT NULL, -- 정정 사유 코드 (CORRECTION_REASON) + reason_detail VARCHAR(1000), -- 정정 사유 상세 + status VARCHAR(20) NOT NULL DEFAULT 'DRAFT', -- DRAFT/APPROVED/APPLIED/CANCELLED + requested_by BIGINT NOT NULL, -- 정정 신청자 + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + approved_by BIGINT, -- 정정 승인자 + approved_at TIMESTAMP, + applied_settle_id BIGINT REFERENCES settle_master(settle_id), -- 정정이 반영된 정산 (다음 월 등) + cancel_reason VARCHAR(500), + cancelled_by BIGINT, + cancelled_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP +); + +COMMENT ON TABLE settle_correction IS '정산 정정 이력 — 확정 정산에 대한 사후 정정'; +COMMENT ON COLUMN settle_correction.original_settle_id IS '원본 settle_master 참조'; +COMMENT ON COLUMN settle_correction.diff_amount IS '정정 차액 (양수=추가, 음수=환수)'; +COMMENT ON COLUMN settle_correction.before_amount IS '정정 전 순지급액'; +COMMENT ON COLUMN settle_correction.after_amount IS '정정 후 순지급액'; +COMMENT ON COLUMN settle_correction.reason_code IS '정정 사유 코드 (CORRECTION_REASON 그룹)'; +COMMENT ON COLUMN settle_correction.status IS 'DRAFT/APPROVED/APPLIED/CANCELLED'; +COMMENT ON COLUMN settle_correction.applied_settle_id IS '차액이 반영된 정산 settle_id (다음 정산월 등)'; + +CREATE INDEX idx_sc_original ON settle_correction(original_settle_id); +CREATE INDEX idx_sc_agent ON settle_correction(agent_id, settle_month); +CREATE INDEX idx_sc_status ON settle_correction(status, requested_at DESC); + +-- ============================================================ +-- 2. 공통코드 CORRECTION_REASON / CORRECTION_STATUS +-- ============================================================ + +INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES + ('CORRECTION_REASON', '정정사유', 'DATA_ERROR/POLICY_CHANGE/LATE_DATA/MANUAL_ADJ/OTHER', 'Y', 300), + ('CORRECTION_STATUS', '정정상태', 'DRAFT/APPROVED/APPLIED/CANCELLED', 'Y', 310) +ON CONFLICT (group_code) DO NOTHING; + +INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES + ('CORRECTION_REASON', 'DATA_ERROR', '데이터오류', '원시 데이터 오류로 인한 정정', 10), + ('CORRECTION_REASON', 'POLICY_CHANGE', '정책변경', '정산 정책 변경으로 인한 소급 정정', 20), + ('CORRECTION_REASON', 'LATE_DATA', '지연수신', '보험사 데이터 지연 수신으로 인한 정정', 30), + ('CORRECTION_REASON', 'MANUAL_ADJ', '수동조정', '관리자 수동 조정', 40), + ('CORRECTION_REASON', 'OTHER', '기타', '기타 사유', 50), + ('CORRECTION_STATUS', 'DRAFT', '초안', '정정 신청 초안', 10), + ('CORRECTION_STATUS', 'APPROVED', '승인', '정정 승인 완료', 20), + ('CORRECTION_STATUS', 'APPLIED', '적용완료', '정정 차액 정산 반영 완료', 30), + ('CORRECTION_STATUS', 'CANCELLED', '취소', '정정 신청 취소', 40) +ON CONFLICT (group_code, code) DO NOTHING; diff --git a/ga-common/src/main/resources/db/migration/V31__이의신청.sql b/ga-common/src/main/resources/db/migration/V31__이의신청.sql new file mode 100644 index 0000000..c2aaea9 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V31__이의신청.sql @@ -0,0 +1,54 @@ +-- V31: 수수료 이의신청 (commission_dispute) + +-- ============================================================ +-- 1. commission_dispute — 이의신청 마스터 +-- ============================================================ + +CREATE TABLE commission_dispute ( + dispute_id BIGSERIAL PRIMARY KEY, + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + settle_month CHAR(6) NOT NULL, -- 대상 정산년월 (YYYYMM) + settle_id BIGINT REFERENCES settle_master(settle_id), -- 연관 정산 (선택) + status VARCHAR(20) NOT NULL DEFAULT 'OPEN', -- OPEN/IN_REVIEW/APPROVED/REJECTED + disputed_amount NUMERIC(15,2), -- 이의 제기 금액 (NULL=전체) + claim_amount NUMERIC(15,2), -- 설계사 주장 금액 + reason VARCHAR(2000) NOT NULL, -- 이의 신청 사유 + evidence_url VARCHAR(500), -- 증빙 자료 URL (첨부파일) + reviewer_id BIGINT, -- 검토 담당자 + reviewed_at TIMESTAMP, -- 검토 시작 일시 + resolved_by BIGINT, -- 최종 처리자 + resolved_at TIMESTAMP, -- 최종 처리 일시 + resolution VARCHAR(2000), -- 처리 결과 상세 + adjustment_amount NUMERIC(15,2), -- 인정된 조정 금액 (APPROVED 시) + correction_id BIGINT REFERENCES settle_correction(correction_id), -- 연계 정정건 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP +); + +COMMENT ON TABLE commission_dispute IS '수수료 이의신청 — 설계사가 정산 결과에 이의를 제기하는 프로세스'; +COMMENT ON COLUMN commission_dispute.agent_id IS '이의신청 설계사'; +COMMENT ON COLUMN commission_dispute.settle_month IS '이의 대상 정산년월 (YYYYMM)'; +COMMENT ON COLUMN commission_dispute.status IS 'OPEN/IN_REVIEW/APPROVED/REJECTED'; +COMMENT ON COLUMN commission_dispute.disputed_amount IS '이의 제기 금액 (NULL=전체 정산)'; +COMMENT ON COLUMN commission_dispute.claim_amount IS '설계사가 주장하는 정당 금액'; +COMMENT ON COLUMN commission_dispute.adjustment_amount IS 'APPROVED 시 실제 인정된 조정 금액'; +COMMENT ON COLUMN commission_dispute.correction_id IS '이의신청 결과로 생성된 정정건 참조'; + +CREATE INDEX idx_cd_agent ON commission_dispute(agent_id, settle_month); +CREATE INDEX idx_cd_status ON commission_dispute(status, created_at DESC); +CREATE INDEX idx_cd_settle ON commission_dispute(settle_id); + +-- ============================================================ +-- 2. 공통코드 DISPUTE_STATUS +-- ============================================================ + +INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES + ('DISPUTE_STATUS', '이의신청상태', 'OPEN/IN_REVIEW/APPROVED/REJECTED', 'Y', 320) +ON CONFLICT (group_code) DO NOTHING; + +INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES + ('DISPUTE_STATUS', 'OPEN', '접수', '이의신청 접수 (검토 전)', 10), + ('DISPUTE_STATUS', 'IN_REVIEW', '검토중', '담당자 검토 진행 중', 20), + ('DISPUTE_STATUS', 'APPROVED', '인정', '이의신청 인정 — 정정 처리 진행', 30), + ('DISPUTE_STATUS', 'REJECTED', '기각', '이의신청 기각 — 원 정산 유지', 40) +ON CONFLICT (group_code, code) DO NOTHING; diff --git a/ga-common/src/main/resources/db/migration/V32__보류사유코드.sql b/ga-common/src/main/resources/db/migration/V32__보류사유코드.sql new file mode 100644 index 0000000..cf04536 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V32__보류사유코드.sql @@ -0,0 +1,29 @@ +-- V32: settle_master 보류 사유 코드 컬럼 추가 + HOLD_REASON 공통코드 + +-- ============================================================ +-- 1. settle_master 에 hold_reason_code 컬럼 추가 +-- (기존 hold_reason VARCHAR(500) 자유텍스트 → 코드화 병행) +-- ============================================================ + +ALTER TABLE settle_master + ADD COLUMN hold_reason_code VARCHAR(30); + +COMMENT ON COLUMN settle_master.hold_reason_code IS '보류 사유 코드 (HOLD_REASON 그룹). status=HOLD 일 때 필수'; + +-- ============================================================ +-- 2. 공통코드 HOLD_REASON +-- ============================================================ + +INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES + ('HOLD_REASON', '보류사유', 'NEED_DOC/UNDER_REVIEW/DISPUTE_OPEN/TAX_ISSUE/BANK_ERROR/COMPLIANCE/OTHER', 'Y', 330) +ON CONFLICT (group_code) DO NOTHING; + +INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES + ('HOLD_REASON', 'NEED_DOC', '서류미비', '필수 서류 미제출로 인한 보류', 10), + ('HOLD_REASON', 'UNDER_REVIEW', '검토중', '관리자 검토 진행 중 보류', 20), + ('HOLD_REASON', 'DISPUTE_OPEN', '이의신청', '이의신청(commission_dispute) 처리 중 보류', 30), + ('HOLD_REASON', 'TAX_ISSUE', '세무이슈', '세무 정보 미확인 또는 이슈', 40), + ('HOLD_REASON', 'BANK_ERROR', '계좌오류', '계좌 정보 오류 또는 이체 실패', 50), + ('HOLD_REASON', 'COMPLIANCE', '컴플라이언스', '법규/감독규정 검토 필요', 60), + ('HOLD_REASON', 'OTHER', '기타', '기타 보류 사유', 70) +ON CONFLICT (group_code, code) DO NOTHING; diff --git a/ga-common/src/main/resources/db/migration/V33__시책인센티브.sql b/ga-common/src/main/resources/db/migration/V33__시책인센티브.sql new file mode 100644 index 0000000..28a88cd --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V33__시책인센티브.sql @@ -0,0 +1,94 @@ +-- V33: 시책·인센티브 (incentive_program + incentive_payment) + +-- ============================================================ +-- 1. incentive_program — 시책 프로그램 마스터 +-- ============================================================ + +CREATE TABLE incentive_program ( + program_id BIGSERIAL PRIMARY KEY, + program_name VARCHAR(200) NOT NULL, -- 시책명 + program_type VARCHAR(30) NOT NULL, -- 시책 유형 (INCENTIVE_TYPE) + target_type VARCHAR(30) NOT NULL DEFAULT 'AGENT', -- 지급 대상 (AGENT/ORG/GRADE) + target_grade_id BIGINT REFERENCES grade(grade_id), -- 대상 직급 (NULL=전체) + target_org_id BIGINT REFERENCES organization(org_id), -- 대상 조직 (NULL=전체) + product_category VARCHAR(30), -- 적용 상품 카테고리 (NULL=전체) + start_month CHAR(6) NOT NULL, -- 시책 시작 정산월 (YYYYMM) + end_month CHAR(6) NOT NULL, -- 시책 종료 정산월 (YYYYMM) + condition_desc VARCHAR(2000), -- 지급 조건 상세 기술 + pay_rate NUMERIC(7,4), -- 지급률 (%) — rate 방식 + pay_amount_fixed NUMERIC(15,2), -- 고정 지급액 — amount 방식 + pay_type VARCHAR(20) NOT NULL DEFAULT 'RATE', -- RATE / FIXED_AMOUNT + budget_amount NUMERIC(15,2), -- 시책 예산 (NULL=무제한) + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + updated_by BIGINT, + CHECK (end_month >= start_month), + CHECK (pay_type IN ('RATE', 'FIXED_AMOUNT')) +); + +COMMENT ON TABLE incentive_program IS '시책 프로그램 마스터 — 기간/대상/조건/지급률 정의'; +COMMENT ON COLUMN incentive_program.program_type IS '시책 유형 코드 (INCENTIVE_TYPE 그룹)'; +COMMENT ON COLUMN incentive_program.target_type IS '지급 대상 구분 (AGENT=개인/ORG=조직/GRADE=직급)'; +COMMENT ON COLUMN incentive_program.start_month IS '시책 시작 정산월 (YYYYMM)'; +COMMENT ON COLUMN incentive_program.end_month IS '시책 종료 정산월 (YYYYMM, 포함)'; +COMMENT ON COLUMN incentive_program.pay_rate IS 'RATE 방식 시 지급률 (%)'; +COMMENT ON COLUMN incentive_program.pay_amount_fixed IS 'FIXED_AMOUNT 방식 시 고정 지급액'; +COMMENT ON COLUMN incentive_program.pay_type IS 'RATE=수수료 대비 비율 / FIXED_AMOUNT=건당 고정액'; + +CREATE INDEX idx_ip_prog_month ON incentive_program(start_month, end_month, is_active); +CREATE INDEX idx_ip_prog_type ON incentive_program(program_type, is_active); + +-- ============================================================ +-- 2. incentive_payment — 시책 지급 이력 +-- ============================================================ + +CREATE TABLE incentive_payment ( + incentive_pay_id BIGSERIAL PRIMARY KEY, + program_id BIGINT NOT NULL REFERENCES incentive_program(program_id), + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + settle_month CHAR(6) NOT NULL, -- 지급 정산월 (YYYYMM) + base_amount NUMERIC(15,2), -- 기준 수수료 (RATE 방식) + incentive_amount NUMERIC(15,2) NOT NULL, -- 시책 지급액 + status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED', -- CALCULATED/CONFIRMED/PAID/CANCELLED + calc_date TIMESTAMP NOT NULL DEFAULT NOW(), + confirmed_by BIGINT, + confirmed_at TIMESTAMP, + settle_id BIGINT REFERENCES settle_master(settle_id), -- 반영된 정산 + cancel_reason VARCHAR(500), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + UNIQUE (program_id, agent_id, settle_month) +); + +COMMENT ON TABLE incentive_payment IS '시책 지급 이력 — program_id + agent_id + settle_month 단위'; +COMMENT ON COLUMN incentive_payment.base_amount IS 'RATE 방식 시 계산 기준 수수료'; +COMMENT ON COLUMN incentive_payment.incentive_amount IS '최종 시책 지급액'; +COMMENT ON COLUMN incentive_payment.status IS 'CALCULATED/CONFIRMED/PAID/CANCELLED'; +COMMENT ON COLUMN incentive_payment.settle_id IS '시책 금액이 반영된 settle_master 참조'; + +CREATE INDEX idx_ipay_agent ON incentive_payment(agent_id, settle_month); +CREATE INDEX idx_ipay_program ON incentive_payment(program_id, settle_month); +CREATE INDEX idx_ipay_status ON incentive_payment(status, settle_month); + +-- ============================================================ +-- 3. 공통코드 INCENTIVE_TYPE / INCENTIVE_PAY_STATUS +-- ============================================================ + +INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES + ('INCENTIVE_TYPE', '시책유형', 'RECRUIT_BONUS/MAINTAIN_BONUS/SPECIAL/CONTEST/OTHER', 'Y', 340), + ('INCENTIVE_PAY_STATUS', '시책지급상태', 'CALCULATED/CONFIRMED/PAID/CANCELLED', 'Y', 350) +ON CONFLICT (group_code) DO NOTHING; + +INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES + ('INCENTIVE_TYPE', 'RECRUIT_BONUS', '신계약시책', '신계약 실적 기반 시책', 10), + ('INCENTIVE_TYPE', 'MAINTAIN_BONUS', '유지시책', '유지율 기반 시책', 20), + ('INCENTIVE_TYPE', 'SPECIAL', '특별시책', '본사 특별 지정 시책', 30), + ('INCENTIVE_TYPE', 'CONTEST', '콘테스트', '경쟁 목표 달성 인센티브', 40), + ('INCENTIVE_TYPE', 'OTHER', '기타', '기타 시책', 50), + ('INCENTIVE_PAY_STATUS', 'CALCULATED', '산출', '시책 금액 산출 완료', 10), + ('INCENTIVE_PAY_STATUS', 'CONFIRMED', '확정', '지급 확정', 20), + ('INCENTIVE_PAY_STATUS', 'PAID', '지급완료', '정산 반영 완료', 30), + ('INCENTIVE_PAY_STATUS', 'CANCELLED', '취소', '시책 지급 취소', 40) +ON CONFLICT (group_code, code) DO NOTHING; diff --git a/ga-common/src/main/resources/db/migration/V34__해촉정산.sql b/ga-common/src/main/resources/db/migration/V34__해촉정산.sql new file mode 100644 index 0000000..cc690f4 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V34__해촉정산.sql @@ -0,0 +1,69 @@ +-- V34: 해촉 정산 (termination_settlement) — 설계사 해촉 시 미지급분·환수 처리 + +-- ============================================================ +-- 1. termination_settlement — 해촉 정산 마스터 +-- ============================================================ + +CREATE TABLE termination_settlement ( + term_settle_id BIGSERIAL PRIMARY KEY, + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + termination_date DATE NOT NULL, -- 해촉 일자 + org_history_id BIGINT REFERENCES agent_org_history(history_id), -- LEAVE 이력 참조 + calc_month CHAR(6) NOT NULL, -- 해촉 정산 처리 월 (YYYYMM) + + -- 미지급 수수료 (해촉 시점 기준) + unpaid_recruit_amount NUMERIC(15,2) NOT NULL DEFAULT 0, -- 미지급 신계약 수수료 + unpaid_installment_amount NUMERIC(15,2) NOT NULL DEFAULT 0, -- 미지급 분급 잔액 + unpaid_incentive_amount NUMERIC(15,2) NOT NULL DEFAULT 0, -- 미지급 시책 + + -- 환수 예정 금액 + chargeback_expected_amount NUMERIC(15,2) NOT NULL DEFAULT 0, -- 환수 예정 총액 + chargeback_waived_amount NUMERIC(15,2) NOT NULL DEFAULT 0, -- 면제 환수액 + + -- 공제 항목 + deduction_amount NUMERIC(15,2) NOT NULL DEFAULT 0, -- 공제 차감액 + + -- 최종 지급 + final_pay_amount NUMERIC(15,2) NOT NULL DEFAULT 0, -- 최종 지급액 (미지급 - 환수 - 공제) + + status VARCHAR(20) NOT NULL DEFAULT 'CALCULATING', -- CALCULATING/CONFIRMED/PAID/CANCELLED + note VARCHAR(1000), + confirmed_by BIGINT, + confirmed_at TIMESTAMP, + paid_at TIMESTAMP, + payment_id BIGINT REFERENCES payment(payment_id), -- 최종 지급 참조 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + updated_by BIGINT, + UNIQUE (agent_id, termination_date) -- 해촉 1건당 정산 1건 +); + +COMMENT ON TABLE termination_settlement IS '해촉 정산 — 설계사 해촉 시 미지급분/환수/최종지급 통합 처리'; +COMMENT ON COLUMN termination_settlement.termination_date IS '해촉 일자'; +COMMENT ON COLUMN termination_settlement.org_history_id IS 'agent_org_history의 LEAVE 이력 참조'; +COMMENT ON COLUMN termination_settlement.unpaid_recruit_amount IS '해촉 시점 미지급 신계약 수수료'; +COMMENT ON COLUMN termination_settlement.unpaid_installment_amount IS '해촉 시점 미지급 분급 잔액 (installment_plan.SCHEDULED 합계)'; +COMMENT ON COLUMN termination_settlement.unpaid_incentive_amount IS '해촉 시점 미지급 시책 금액'; +COMMENT ON COLUMN termination_settlement.chargeback_expected_amount IS '해촉 시점 발생 예정 환수 총액'; +COMMENT ON COLUMN termination_settlement.chargeback_waived_amount IS '합의·면제된 환수액'; +COMMENT ON COLUMN termination_settlement.final_pay_amount IS '최종 지급액 = (미지급합계) - (환수예정-면제) - 공제'; +COMMENT ON COLUMN termination_settlement.status IS 'CALCULATING/CONFIRMED/PAID/CANCELLED'; + +CREATE INDEX idx_ts_agent ON termination_settlement(agent_id, termination_date DESC); +CREATE INDEX idx_ts_month ON termination_settlement(calc_month, status); + +-- ============================================================ +-- 2. 공통코드 TERM_SETTLE_STATUS +-- ============================================================ + +INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES + ('TERM_SETTLE_STATUS', '해촉정산상태', 'CALCULATING/CONFIRMED/PAID/CANCELLED', 'Y', 360) +ON CONFLICT (group_code) DO NOTHING; + +INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES + ('TERM_SETTLE_STATUS', 'CALCULATING', '산출중', '해촉 정산 금액 산출 중', 10), + ('TERM_SETTLE_STATUS', 'CONFIRMED', '확정', '해촉 정산 금액 확정', 20), + ('TERM_SETTLE_STATUS', 'PAID', '지급완료', '최종 지급 완료', 30), + ('TERM_SETTLE_STATUS', 'CANCELLED', '취소', '해촉 정산 취소', 40) +ON CONFLICT (group_code, code) DO NOTHING; diff --git a/ga-common/src/main/resources/db/migration/V35__세금계산서.sql b/ga-common/src/main/resources/db/migration/V35__세금계산서.sql new file mode 100644 index 0000000..f02eebf --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V35__세금계산서.sql @@ -0,0 +1,65 @@ +-- V35: 세금계산서 발행 이력 (tax_invoice) + +-- ============================================================ +-- 1. tax_invoice — 세금계산서 발행 이력 +-- ============================================================ + +CREATE TABLE tax_invoice ( + invoice_id BIGSERIAL PRIMARY KEY, + invoice_no VARCHAR(50) NOT NULL UNIQUE, -- 발행 번호 (국세청 승인번호 등) + invoice_type VARCHAR(20) NOT NULL, -- SUPPLY (매출) / PURCHASE (매입) + issue_date DATE NOT NULL, -- 발행일 + supply_amount NUMERIC(15,2) NOT NULL, -- 공급가액 + tax_amount NUMERIC(15,2) NOT NULL, -- 세액 (공급가 × 10%) + total_amount NUMERIC(15,2) NOT NULL, -- 합계액 (공급가 + 세액) + + -- 거래 상대방 + counterpart_name VARCHAR(200) NOT NULL, -- 거래 상대방 상호 + counterpart_biz_no VARCHAR(20) NOT NULL, -- 거래 상대방 사업자번호 + counterpart_rep VARCHAR(100), -- 대표자명 + counterpart_addr VARCHAR(500), -- 주소 + + -- 내부 참조 + agent_id BIGINT REFERENCES agent(agent_id), -- 관련 설계사 (해당 시) + settle_month CHAR(6), -- 관련 정산월 (YYYYMM, 해당 시) + settle_id BIGINT REFERENCES settle_master(settle_id), + + -- 발행 메타 + description VARCHAR(500), -- 품목/비고 + cancel_date DATE, -- 취소일 (NULL=유효) + cancel_reason VARCHAR(500), + issued_by BIGINT NOT NULL, -- 발행 처리자 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + + CHECK (invoice_type IN ('SUPPLY', 'PURCHASE')), + CHECK (total_amount = supply_amount + tax_amount) +); + +COMMENT ON TABLE tax_invoice IS '세금계산서 발행 이력 (매출/매입)'; +COMMENT ON COLUMN tax_invoice.invoice_no IS '세금계산서 발행번호 (국세청 승인번호)'; +COMMENT ON COLUMN tax_invoice.invoice_type IS 'SUPPLY=매출계산서 / PURCHASE=매입계산서'; +COMMENT ON COLUMN tax_invoice.supply_amount IS '공급가액'; +COMMENT ON COLUMN tax_invoice.tax_amount IS '부가세액 (공급가 × 10%)'; +COMMENT ON COLUMN tax_invoice.total_amount IS '합계 금액 = 공급가 + 부가세'; +COMMENT ON COLUMN tax_invoice.counterpart_biz_no IS '거래 상대방 사업자등록번호'; +COMMENT ON COLUMN tax_invoice.cancel_date IS '세금계산서 취소일 (NULL이면 유효 상태)'; +COMMENT ON COLUMN tax_invoice.settle_month IS '연관 정산월 (YYYYMM)'; + +CREATE INDEX idx_ti_issue_date ON tax_invoice(issue_date DESC); +CREATE INDEX idx_ti_type_month ON tax_invoice(invoice_type, settle_month); +CREATE INDEX idx_ti_agent ON tax_invoice(agent_id, issue_date DESC); +CREATE INDEX idx_ti_biz_no ON tax_invoice(counterpart_biz_no); + +-- ============================================================ +-- 2. 공통코드 INVOICE_TYPE +-- ============================================================ + +INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES + ('INVOICE_TYPE', '세금계산서유형', 'SUPPLY/PURCHASE', 'Y', 370) +ON CONFLICT (group_code) DO NOTHING; + +INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES + ('INVOICE_TYPE', 'SUPPLY', '매출', '매출(공급) 세금계산서 — GA가 발행', 10), + ('INVOICE_TYPE', 'PURCHASE', '매입', '매입 세금계산서 — GA가 수취', 20) +ON CONFLICT (group_code, code) DO NOTHING; diff --git a/ga-common/src/main/resources/db/migration/V36__보험사회신인터페이스.sql b/ga-common/src/main/resources/db/migration/V36__보험사회신인터페이스.sql new file mode 100644 index 0000000..da82ed6 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V36__보험사회신인터페이스.sql @@ -0,0 +1,54 @@ +-- V36: 보험사 회신 인터페이스 (carrier_reconciliation_out) — 정산 결과 외부 전송 관리 + +-- ============================================================ +-- 1. carrier_reconciliation_out — 보험사 회신 (아웃바운드) 이력 +-- ============================================================ + +CREATE TABLE carrier_reconciliation_out ( + recon_out_id BIGSERIAL PRIMARY KEY, + carrier_id BIGINT NOT NULL REFERENCES insurance_company(company_id), + settle_month CHAR(6) NOT NULL, -- 대상 정산월 (YYYYMM) + file_path VARCHAR(1000), -- 전송 파일 경로 (SFTP/FTP 등) + file_name VARCHAR(300), -- 파일명 + file_size BIGINT, -- 파일 크기 (bytes) + record_count INT, -- 파일 내 레코드 수 + status VARCHAR(20) NOT NULL DEFAULT 'READY', -- READY/SENT/ACK/FAIL + sent_at TIMESTAMP, -- 전송 일시 + ack_at TIMESTAMP, -- 보험사 수신 확인 일시 + ack_message VARCHAR(1000), -- 보험사 회신 메시지 + fail_reason VARCHAR(1000), -- 실패 사유 + retry_count INT NOT NULL DEFAULT 0, -- 재시도 횟수 + last_retry_at TIMESTAMP, -- 마지막 재시도 일시 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + UNIQUE (carrier_id, settle_month) -- 보험사 + 정산월 단위 단건 +); + +COMMENT ON TABLE carrier_reconciliation_out IS '보험사 정산 회신 인터페이스 — 아웃바운드 전송 이력 관리'; +COMMENT ON COLUMN carrier_reconciliation_out.carrier_id IS '대상 보험사 (insurance_company.company_id)'; +COMMENT ON COLUMN carrier_reconciliation_out.settle_month IS '전송 대상 정산월 (YYYYMM)'; +COMMENT ON COLUMN carrier_reconciliation_out.file_path IS '전송 파일 저장 경로 (절대경로 또는 S3 URI)'; +COMMENT ON COLUMN carrier_reconciliation_out.status IS 'READY=전송 준비 / SENT=전송 완료 / ACK=수신 확인 / FAIL=전송 실패'; +COMMENT ON COLUMN carrier_reconciliation_out.sent_at IS '파일 전송 완료 일시'; +COMMENT ON COLUMN carrier_reconciliation_out.ack_at IS '보험사 수신 확인 일시'; +COMMENT ON COLUMN carrier_reconciliation_out.retry_count IS 'FAIL 후 재시도 횟수 (최대 재시도 횟수는 system_config 참조)'; + +CREATE INDEX idx_cro_carrier ON carrier_reconciliation_out(carrier_id, settle_month); +CREATE INDEX idx_cro_status ON carrier_reconciliation_out(status, sent_at DESC); +CREATE INDEX idx_cro_month ON carrier_reconciliation_out(settle_month, status); + +-- ============================================================ +-- 2. 공통코드 RECON_OUT_STATUS +-- ============================================================ + +INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES + ('RECON_OUT_STATUS', '회신전송상태', 'READY/SENT/ACK/FAIL', 'Y', 380) +ON CONFLICT (group_code) DO NOTHING; + +INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES + ('RECON_OUT_STATUS', 'READY', '전송준비', '파일 생성 완료, 전송 대기 중', 10), + ('RECON_OUT_STATUS', 'SENT', '전송완료', '보험사로 파일 전송 완료 (확인 전)', 20), + ('RECON_OUT_STATUS', 'ACK', '수신확인', '보험사 수신 확인 완료', 30), + ('RECON_OUT_STATUS', 'FAIL', '전송실패', '파일 전송 실패 — 재시도 필요', 40) +ON CONFLICT (group_code, code) DO NOTHING; diff --git a/ga-common/src/main/resources/db/migration/V37__KPI_MV.sql b/ga-common/src/main/resources/db/migration/V37__KPI_MV.sql new file mode 100644 index 0000000..b46c98c --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V37__KPI_MV.sql @@ -0,0 +1,257 @@ +-- V37: KPI 대시보드 Materialized View 4종 +-- +-- REFRESH 전략: +-- CONCURRENTLY 옵션으로 락 없이 갱신 (UNIQUE INDEX 필수). +-- 야간 배치(ga-batch)에서 매일 00:30 순차 REFRESH. +-- 명령: REFRESH MATERIALIZED VIEW CONCURRENTLY ; +-- +-- 주의: CONCURRENTLY 는 트랜잭션 안에서 실행 불가. +-- 배치에서 Connection.setAutoCommit(true) 상태로 호출할 것. + +-- ============================================================ +-- 1. mv_monthly_summary — 월별 정산 요약 +-- 단위: 정산월(yyyymm) 전체 합산 +-- ============================================================ + +CREATE MATERIALIZED VIEW mv_monthly_summary AS +SELECT + sm.settle_month AS yyyymm, + COUNT(sm.settle_id) AS agent_count, + COALESCE(SUM(sm.recruit_total), 0) AS total_recruit, + COALESCE(SUM(sm.maintain_total), 0) AS total_maintain, + COALESCE(SUM(sm.override_total), 0) AS total_override, + COALESCE(SUM(sm.chargeback_total),0) AS total_chargeback, + COALESCE(SUM(sm.gross_amount), 0) AS total_gross, + COALESCE(SUM(sm.tax_amount), 0) AS total_tax, + COALESCE(SUM(sm.net_amount), 0) AS total_net, + -- 인센티브 합계 (해당 월 PAID 기준) + COALESCE(( + SELECT SUM(ip.incentive_amount) + FROM incentive_payment ip + WHERE ip.settle_month = sm.settle_month + AND ip.status = 'PAID' + ), 0) AS total_incentive, + NOW() AS refreshed_at +FROM settle_master sm +WHERE sm.status IN ('CONFIRMED', 'PAID') +GROUP BY sm.settle_month +WITH DATA; + +COMMENT ON MATERIALIZED VIEW mv_monthly_summary IS '월별 정산 요약 KPI — REFRESH CONCURRENTLY 야간 배치'; +COMMENT ON COLUMN mv_monthly_summary.yyyymm IS '정산년월 (YYYYMM)'; +COMMENT ON COLUMN mv_monthly_summary.agent_count IS '해당 월 확정/지급 완료 설계사 수'; +COMMENT ON COLUMN mv_monthly_summary.total_gross IS '총 수수료 (세전)'; +COMMENT ON COLUMN mv_monthly_summary.total_tax IS '총 원천세'; +COMMENT ON COLUMN mv_monthly_summary.total_net IS '총 순지급액 (세후)'; +COMMENT ON COLUMN mv_monthly_summary.total_incentive IS '해당 월 지급 완료 시책 합계'; +COMMENT ON COLUMN mv_monthly_summary.refreshed_at IS 'MATERIALIZED VIEW 마지막 갱신 시각'; + +-- CONCURRENTLY REFRESH 를 위한 UNIQUE INDEX (필수) +CREATE UNIQUE INDEX idx_mv_monthly_summary_pk ON mv_monthly_summary(yyyymm); + +-- ============================================================ +-- 2. mv_org_summary — 조직별 월별 요약 +-- 단위: org_id + yyyymm +-- ============================================================ + +CREATE MATERIALIZED VIEW mv_org_summary AS +SELECT + a.org_id, + o.org_name, + o.org_type, + sm.settle_month AS yyyymm, + COUNT(DISTINCT sm.agent_id) AS agent_count, + COALESCE(SUM(sm.recruit_total), 0) AS total_recruit, + COALESCE(SUM(sm.maintain_total), 0) AS total_maintain, + COALESCE(SUM(sm.override_total), 0) AS total_override, + COALESCE(SUM(sm.chargeback_total),0) AS total_chargeback, + COALESCE(SUM(sm.gross_amount), 0) AS total_gross, + COALESCE(SUM(sm.net_amount), 0) AS total_net, + NOW() AS refreshed_at +FROM settle_master sm +JOIN agent a ON a.agent_id = sm.agent_id +JOIN organization o ON o.org_id = a.org_id +WHERE sm.status IN ('CONFIRMED', 'PAID') +GROUP BY a.org_id, o.org_name, o.org_type, sm.settle_month +WITH DATA; + +COMMENT ON MATERIALIZED VIEW mv_org_summary IS '조직별 월별 요약 KPI — REFRESH CONCURRENTLY 야간 배치'; +COMMENT ON COLUMN mv_org_summary.org_id IS '조직 ID'; +COMMENT ON COLUMN mv_org_summary.yyyymm IS '정산년월 (YYYYMM)'; +COMMENT ON COLUMN mv_org_summary.agent_count IS '해당 조직의 월 확정/지급 설계사 수'; +COMMENT ON COLUMN mv_org_summary.total_gross IS '조직 소속 설계사 총 수수료 합계 (세전)'; +COMMENT ON COLUMN mv_org_summary.total_net IS '조직 소속 설계사 총 순지급액 합계'; + +CREATE UNIQUE INDEX idx_mv_org_summary_pk ON mv_org_summary(org_id, yyyymm); +CREATE INDEX idx_mv_org_summary_month ON mv_org_summary(yyyymm); + +-- ============================================================ +-- 3. mv_retention — 계약 유지율 (13M / 25M) +-- 단위: contract_month (계약 기준월) + carrier_id +-- 유지율 = 기준월 이후 N개월 시점에 유효한 계약 비율 +-- ============================================================ + +CREATE MATERIALIZED VIEW mv_retention AS +WITH base AS ( + -- 계약 기준월별 원수 계약 집계 + SELECT + TO_CHAR(c.contract_date, 'YYYYMM') AS contract_month, + p.company_id AS carrier_id, + COUNT(c.contract_id) AS total_contracts + FROM contract c + JOIN product p ON p.product_id = c.product_id + WHERE c.status != 'TERMINATE' + GROUP BY TO_CHAR(c.contract_date, 'YYYYMM'), p.company_id +), +m13 AS ( + -- 13개월 시점 유지 계약 (lapse_month > 12 또는 아직 실효 없음) + SELECT + TO_CHAR(c.contract_date, 'YYYYMM') AS contract_month, + p.company_id AS carrier_id, + COUNT(c.contract_id) AS retained_13m + FROM contract c + JOIN product p ON p.product_id = c.product_id + WHERE c.status != 'TERMINATE' + AND ( + -- 실효 데이터 없거나 실효 경과월 > 12 + NOT EXISTS ( + SELECT 1 FROM chargeback cb + WHERE cb.contract_id = c.contract_id + AND cb.lapse_month IS NOT NULL + AND cb.lapse_month <= 12 + ) + ) + GROUP BY TO_CHAR(c.contract_date, 'YYYYMM'), p.company_id +), +m25 AS ( + -- 25개월 시점 유지 계약 (lapse_month > 24 또는 실효 없음) + SELECT + TO_CHAR(c.contract_date, 'YYYYMM') AS contract_month, + p.company_id AS carrier_id, + COUNT(c.contract_id) AS retained_25m + FROM contract c + JOIN product p ON p.product_id = c.product_id + WHERE c.status != 'TERMINATE' + AND ( + NOT EXISTS ( + SELECT 1 FROM chargeback cb + WHERE cb.contract_id = c.contract_id + AND cb.lapse_month IS NOT NULL + AND cb.lapse_month <= 24 + ) + ) + GROUP BY TO_CHAR(c.contract_date, 'YYYYMM'), p.company_id +) +SELECT + b.contract_month, + b.carrier_id, + ic.company_name AS carrier_name, + b.total_contracts, + COALESCE(m13.retained_13m, 0) AS retained_13m, + COALESCE(m25.retained_25m, 0) AS retained_25m, + -- 13개월 유지율 % + CASE WHEN b.total_contracts > 0 + THEN ROUND(COALESCE(m13.retained_13m, 0) * 100.0 / b.total_contracts, 2) + ELSE 0 + END AS retention_rate_13m, + -- 25개월 유지율 % + CASE WHEN b.total_contracts > 0 + THEN ROUND(COALESCE(m25.retained_25m, 0) * 100.0 / b.total_contracts, 2) + ELSE 0 + END AS retention_rate_25m, + NOW() AS refreshed_at +FROM base b +JOIN insurance_company ic ON ic.company_id = b.carrier_id +LEFT JOIN m13 ON m13.contract_month = b.contract_month AND m13.carrier_id = b.carrier_id +LEFT JOIN m25 ON m25.contract_month = b.contract_month AND m25.carrier_id = b.carrier_id +WITH DATA; + +COMMENT ON MATERIALIZED VIEW mv_retention IS '계약 유지율 KPI (13M/25M) — REFRESH CONCURRENTLY 야간 배치'; +COMMENT ON COLUMN mv_retention.contract_month IS '계약 기준월 (YYYYMM)'; +COMMENT ON COLUMN mv_retention.carrier_id IS '보험사 ID'; +COMMENT ON COLUMN mv_retention.total_contracts IS '기준월 전체 유효 계약 수'; +COMMENT ON COLUMN mv_retention.retained_13m IS '13개월 시점 유지 계약 수'; +COMMENT ON COLUMN mv_retention.retained_25m IS '25개월 시점 유지 계약 수'; +COMMENT ON COLUMN mv_retention.retention_rate_13m IS '13개월 유지율 (%)'; +COMMENT ON COLUMN mv_retention.retention_rate_25m IS '25개월 유지율 (%)'; + +CREATE UNIQUE INDEX idx_mv_retention_pk ON mv_retention(contract_month, carrier_id); +CREATE INDEX idx_mv_retention_month ON mv_retention(contract_month); + +-- ============================================================ +-- 4. mv_cumulative — 설계사/조직별 누계 +-- 단위: agent_id 또는 org_id (두 레벨 모두 포함) +-- ============================================================ + +CREATE MATERIALIZED VIEW mv_cumulative AS +-- 설계사 레벨 누계 +SELECT + 'AGENT' AS level_type, + a.agent_id AS entity_id, + a.agent_name AS entity_name, + a.org_id AS org_id, + o.org_name AS org_name, + MIN(sm.settle_month) AS first_settle_month, + MAX(sm.settle_month) AS last_settle_month, + COUNT(sm.settle_id) AS settle_month_count, + COALESCE(SUM(sm.gross_amount), 0) AS cum_gross, + COALESCE(SUM(sm.net_amount), 0) AS cum_net, + COALESCE(SUM(sm.chargeback_total), 0) AS cum_chargeback, + COALESCE(SUM(sm.override_total), 0) AS cum_override, + -- 시책 누계 + COALESCE(( + SELECT SUM(ip.incentive_amount) + FROM incentive_payment ip + WHERE ip.agent_id = a.agent_id + AND ip.status = 'PAID' + ), 0) AS cum_incentive, + NOW() AS refreshed_at +FROM settle_master sm +JOIN agent a ON a.agent_id = sm.agent_id +JOIN organization o ON o.org_id = a.org_id +WHERE sm.status IN ('CONFIRMED', 'PAID') +GROUP BY a.agent_id, a.agent_name, a.org_id, o.org_name + +UNION ALL + +-- 조직 레벨 누계 (소속 설계사 합산) +SELECT + 'ORG' AS level_type, + o.org_id AS entity_id, + o.org_name AS entity_name, + o.org_id AS org_id, + o.org_name AS org_name, + MIN(sm.settle_month) AS first_settle_month, + MAX(sm.settle_month) AS last_settle_month, + COUNT(DISTINCT sm.settle_month) AS settle_month_count, + COALESCE(SUM(sm.gross_amount), 0) AS cum_gross, + COALESCE(SUM(sm.net_amount), 0) AS cum_net, + COALESCE(SUM(sm.chargeback_total), 0) AS cum_chargeback, + COALESCE(SUM(sm.override_total), 0) AS cum_override, + COALESCE(( + SELECT SUM(ip.incentive_amount) + FROM incentive_payment ip + JOIN agent ia ON ia.agent_id = ip.agent_id + WHERE ia.org_id = o.org_id + AND ip.status = 'PAID' + ), 0) AS cum_incentive, + NOW() AS refreshed_at +FROM settle_master sm +JOIN agent a ON a.agent_id = sm.agent_id +JOIN organization o ON o.org_id = a.org_id +WHERE sm.status IN ('CONFIRMED', 'PAID') +GROUP BY o.org_id, o.org_name +WITH DATA; + +COMMENT ON MATERIALIZED VIEW mv_cumulative IS '설계사/조직 누계 수수료·환수 KPI — REFRESH CONCURRENTLY 야간 배치'; +COMMENT ON COLUMN mv_cumulative.level_type IS 'AGENT=설계사 레벨 / ORG=조직 레벨'; +COMMENT ON COLUMN mv_cumulative.entity_id IS 'level_type=AGENT이면 agent_id, ORG이면 org_id'; +COMMENT ON COLUMN mv_cumulative.cum_gross IS '누적 총 수수료 (세전)'; +COMMENT ON COLUMN mv_cumulative.cum_net IS '누적 순지급액 (세후)'; +COMMENT ON COLUMN mv_cumulative.cum_chargeback IS '누적 환수 합계'; +COMMENT ON COLUMN mv_cumulative.cum_incentive IS '누적 시책 지급액'; +COMMENT ON COLUMN mv_cumulative.refreshed_at IS 'MATERIALIZED VIEW 마지막 갱신 시각'; + +-- CONCURRENTLY 를 위한 UNIQUE INDEX: level_type + entity_id 조합 +CREATE UNIQUE INDEX idx_mv_cumulative_pk ON mv_cumulative(level_type, entity_id); +CREATE INDEX idx_mv_cumulative_org ON mv_cumulative(org_id, level_type); diff --git a/ga-common/src/main/resources/db/migration/V38__리포트_메뉴.sql b/ga-common/src/main/resources/db/migration/V38__리포트_메뉴.sql new file mode 100644 index 0000000..b0cce56 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V38__리포트_메뉴.sql @@ -0,0 +1,53 @@ +-- V38: 리포트 메뉴 하위 항목 추가 +-- GRP_REPORT / REPORT 그룹 모두 처리 (V12 = REPORT, V18 = GRP_REPORT) + +-- 1. REPORT 그룹에 하위 PAGE 삽입 (menu_code=REPORT 기준) +INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order) +SELECT p.menu_id, x.code, x.name, 'PAGE', x.path, x.cmpt, 2, x.sort +FROM menu p +JOIN (VALUES + ('REPORT_PERFORMANCE', '실적 리포트', '/report/performance', 'report/PerformanceReport', 10), + ('REPORT_ORG', '조직별 리포트', '/report/org', 'report/OrgReport', 20), + ('REPORT_CHARGEBACK', '환수위험 리포트', '/report/chargeback-risk', 'report/ChargebackRiskReport', 30) +) AS x(code, name, path, cmpt, sort) +ON p.menu_code = 'REPORT' +WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code); + +-- 2. GRP_REPORT 그룹에도 동일 PAGE 삽입 (V18 기준 경로; menu_code=GRP_REPORT) +INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order) +SELECT p.menu_id, x.code || '_GRP', x.name, 'PAGE', x.path, x.cmpt, 2, x.sort +FROM menu p +JOIN (VALUES + ('REPORT_PERFORMANCE', '실적 리포트', '/report/performance', 'report/PerformanceReport', 10), + ('REPORT_ORG', '조직별 리포트', '/report/org', 'report/OrgReport', 20), + ('REPORT_CHARGEBACK', '환수위험 리포트', '/report/chargeback-risk', 'report/ChargebackRiskReport', 30) +) AS x(code, name, path, cmpt, sort) +ON p.menu_code = 'GRP_REPORT' +WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code || '_GRP'); + +-- 3. 새 PAGE 메뉴에 권한 항목 추가 +INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order) +SELECT m.menu_id, p.code, p.name, p.sort +FROM menu m +CROSS JOIN (VALUES + ('READ', '조회', 10), + ('EXPORT', '엑셀', 60) +) AS p(code, name, sort) +WHERE m.menu_code IN ( + 'REPORT_PERFORMANCE', 'REPORT_ORG', 'REPORT_CHARGEBACK', + 'REPORT_PERFORMANCE_GRP', 'REPORT_ORG_GRP', 'REPORT_CHARGEBACK_GRP' +) +ON CONFLICT (menu_id, perm_code) DO NOTHING; + +-- 4. SUPER_ADMIN / ADMIN / MANAGER 에게 리포트 READ/EXPORT 권한 부여 +INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted) +SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y' +FROM role r +CROSS JOIN menu_permission mp +JOIN menu m ON m.menu_id = mp.menu_id +WHERE r.role_code IN ('SUPER_ADMIN', 'ADMIN', 'MANAGER') + AND m.menu_code IN ( + 'REPORT_PERFORMANCE', 'REPORT_ORG', 'REPORT_CHARGEBACK', + 'REPORT_PERFORMANCE_GRP', 'REPORT_ORG_GRP', 'REPORT_CHARGEBACK_GRP' + ) +ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING; diff --git a/ga-common/src/main/resources/db/migration/V39__KPI_메뉴.sql b/ga-common/src/main/resources/db/migration/V39__KPI_메뉴.sql new file mode 100644 index 0000000..efb36e7 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V39__KPI_메뉴.sql @@ -0,0 +1,58 @@ +-- V39: KPI 대시보드 메뉴 4개 추가 +-- 부모 그룹: GRP_KPI (신규) — REPORT 그룹과 동일 레벨 +-- 화면: KPI_MONTHLY / KPI_ORG / KPI_RETENTION / KPI_CUMULATIVE + +-- ============================================================ +-- 1. KPI 부모 그룹 메뉴 삽입 +-- ============================================================ + +INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order) +VALUES (NULL, 'GRP_KPI', 'KPI 대시보드', 'GROUP', '/kpi', NULL, 1, 80) +ON CONFLICT (menu_code) DO NOTHING; + +-- ============================================================ +-- 2. KPI 하위 PAGE 메뉴 4개 삽입 +-- ============================================================ + +INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order) +SELECT p.menu_id, x.code, x.name, 'PAGE', x.path, x.cmpt, 2, x.sort +FROM menu p +JOIN (VALUES + ('KPI_MONTHLY', '월별 정산 요약', '/kpi/monthly', 'kpi/KpiMonthlySummary', 10), + ('KPI_ORG', '조직별 요약', '/kpi/org', 'kpi/KpiOrgSummary', 20), + ('KPI_RETENTION', '유지율 현황', '/kpi/retention', 'kpi/KpiRetention', 30), + ('KPI_CUMULATIVE', '누계 현황', '/kpi/cumulative', 'kpi/KpiCumulative', 40) +) AS x(code, name, path, cmpt, sort) +ON p.menu_code = 'GRP_KPI' +WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code); + +-- ============================================================ +-- 3. 권한 항목 (READ + EXPORT) 등록 +-- ============================================================ + +INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order) +SELECT m.menu_id, p.code, p.name, p.sort +FROM menu m +CROSS JOIN (VALUES + ('READ', '조회', 10), + ('EXPORT', '엑셀', 60) +) AS p(code, name, sort) +WHERE m.menu_code IN ( + 'KPI_MONTHLY', 'KPI_ORG', 'KPI_RETENTION', 'KPI_CUMULATIVE' +) +ON CONFLICT (menu_id, perm_code) DO NOTHING; + +-- ============================================================ +-- 4. SUPER_ADMIN / ADMIN / MANAGER 에게 READ + EXPORT 부여 +-- ============================================================ + +INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted) +SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y' +FROM role r +CROSS JOIN menu_permission mp +JOIN menu m ON m.menu_id = mp.menu_id +WHERE r.role_code IN ('SUPER_ADMIN', 'ADMIN', 'MANAGER') + AND m.menu_code IN ( + 'KPI_MONTHLY', 'KPI_ORG', 'KPI_RETENTION', 'KPI_CUMULATIVE' + ) +ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING; diff --git a/ga-common/src/main/resources/db/migration/V40__수수료종류코드_통합마스터.sql b/ga-common/src/main/resources/db/migration/V40__수수료종류코드_통합마스터.sql new file mode 100644 index 0000000..f1e054d --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V40__수수료종류코드_통합마스터.sql @@ -0,0 +1,59 @@ +-- V40: 수수료 종류 코드(COMMISSION_TYPE) + 통합 마스터(commission_master) + +-- ============================================================ +-- 1. 공통코드 그룹 — COMMISSION_TYPE +-- ============================================================ + +INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES + ('COMMISSION_TYPE', '수수료종류', '모집/유지/수금/오버라이드/갱신/부활/유지보수수당/시책/위촉 9종', 'Y', 360) +ON CONFLICT (group_code) DO NOTHING; + +INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES + ('COMMISSION_TYPE', 'RECRUIT', '모집수수료', '신계약 체결 시 발생하는 기본 모집수수료', 10), + ('COMMISSION_TYPE', 'MAINTAIN', '유지수수료', '계약 유지 기간 중 발생하는 유지수수료', 20), + ('COMMISSION_TYPE', 'COLLECT', '수금수수료', '보험료 수납 단위로 발생하는 수금수수료', 30), + ('COMMISSION_TYPE', 'OVERRIDE', '오버라이드', '하위 설계사 수수료 기반 상위 직급 오버라이드', 40), + ('COMMISSION_TYPE', 'RENEWAL', '갱신수수료', '자동차/실비 등 갱신 계약 시 발생하는 수수료', 50), + ('COMMISSION_TYPE', 'REINSTATE', '부활수수료', '실효된 계약 부활 시 발생하는 수수료', 60), + ('COMMISSION_TYPE', 'PERSIST', '유지보수수당', '13/25/37/49개월 유지율 충족 시 지급하는 수당', 70), + ('COMMISSION_TYPE', 'INCENTIVE', '시책수수료', '본사 시책 프로그램에 따른 인센티브', 80), + ('COMMISSION_TYPE', 'ONBOARD', '위촉수수료', '보험사 위촉계약 체결 시 지급하는 위촉수수료', 90) +ON CONFLICT (group_code, code) DO NOTHING; + +-- ============================================================ +-- 2. commission_master — 전 수수료 종류 통합 마스터 +-- 모든 수수료 원장(recruit_ledger / maintain_ledger / +-- collection_commission_ledger / override_ledger / 등)에서 +-- 산출된 결과를 단일 뷰로 집계하기 위한 허브 테이블. +-- settle_master 조인 시 agent_id + settle_month 로 연결. +-- ============================================================ + +CREATE TABLE commission_master ( + master_id BIGSERIAL PRIMARY KEY, + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + commission_type VARCHAR(20) NOT NULL, + settle_month CHAR(6) NOT NULL, + source_table VARCHAR(50) NOT NULL, -- 원본 원장 테이블명 (recruit_ledger 등) + source_id BIGINT NOT NULL, -- 원본 원장 PK + gross_amount NUMERIC(15,2) NOT NULL DEFAULT 0, -- 세전 수수료 + tax_amount NUMERIC(15,2) NOT NULL DEFAULT 0, -- 원천세 + net_amount NUMERIC(15,2) NOT NULL DEFAULT 0, -- 세후 지급액 + status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED', + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + UNIQUE (agent_id, commission_type, settle_month, source_id), + CHECK (commission_type IN ('RECRUIT','MAINTAIN','COLLECT','OVERRIDE','RENEWAL','REINSTATE','PERSIST','INCENTIVE','ONBOARD')), + CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED')) +); + +COMMENT ON TABLE commission_master IS '수수료 통합 마스터 — 전 수수료 종류를 단일 허브에서 집계. settle_master 와 agent_id+settle_month 로 조인'; +COMMENT ON COLUMN commission_master.commission_type IS 'COMMISSION_TYPE 그룹 코드 (RECRUIT/MAINTAIN/COLLECT/OVERRIDE/RENEWAL/REINSTATE/PERSIST/INCENTIVE/ONBOARD)'; +COMMENT ON COLUMN commission_master.source_table IS '원본 원장 테이블명 (예: recruit_ledger)'; +COMMENT ON COLUMN commission_master.source_id IS '원본 원장 PK'; +COMMENT ON COLUMN commission_master.gross_amount IS '세전 수수료 금액'; +COMMENT ON COLUMN commission_master.net_amount IS '세후 실지급 금액'; + +CREATE INDEX idx_cm_agent_month ON commission_master(agent_id, settle_month); +CREATE INDEX idx_cm_type_month ON commission_master(commission_type, settle_month); +CREATE INDEX idx_cm_source ON commission_master(source_table, source_id); +CREATE INDEX idx_cm_status ON commission_master(status, settle_month); diff --git a/ga-common/src/main/resources/db/migration/V41__수금수수료.sql b/ga-common/src/main/resources/db/migration/V41__수금수수료.sql new file mode 100644 index 0000000..2812138 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V41__수금수수료.sql @@ -0,0 +1,66 @@ +-- V41: 수금수수료 (collection_commission_rule + collection_commission_ledger) +-- 보험료 수납 단위(매월)로 발생하는 수금수수료. 납입주기별 차등 적용. + +-- ============================================================ +-- 1. collection_commission_rule — 납입주기별 수금수수료율 마스터 +-- ============================================================ + +CREATE TABLE collection_commission_rule ( + rule_id BIGSERIAL PRIMARY KEY, + product_id BIGINT NOT NULL REFERENCES product(product_id), + payment_cycle VARCHAR(20) NOT NULL, + commission_rate NUMERIC(7,4) NOT NULL, + effective_from DATE NOT NULL, + effective_to DATE, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + UNIQUE (product_id, payment_cycle, effective_from), + CHECK (payment_cycle IN ('MONTHLY','QUARTERLY','SEMI','ANNUAL')), + CHECK (commission_rate >= 0 AND commission_rate <= 1) +); + +COMMENT ON TABLE collection_commission_rule IS '수금수수료율 마스터 — 상품/납입주기별 수금수수료율 정의'; +COMMENT ON COLUMN collection_commission_rule.product_id IS '적용 상품 FK'; +COMMENT ON COLUMN collection_commission_rule.payment_cycle IS '납입주기 (MONTHLY=월납/QUARTERLY=분기납/SEMI=반기납/ANNUAL=연납)'; +COMMENT ON COLUMN collection_commission_rule.commission_rate IS '수금수수료율 (소수, 예: 0.005 = 0.5%)'; +COMMENT ON COLUMN collection_commission_rule.effective_from IS '적용 시작일'; +COMMENT ON COLUMN collection_commission_rule.effective_to IS '적용 종료일 (NULL=현재 유효)'; + +CREATE INDEX idx_ccr_product ON collection_commission_rule(product_id, effective_from); + +-- ============================================================ +-- 2. collection_commission_ledger — 수금수수료 원장 +-- 매월 수납 확인 후 발생. paid_at 기준으로 정산월 귀속. +-- ============================================================ + +CREATE TABLE collection_commission_ledger ( + ledger_id BIGSERIAL PRIMARY KEY, + contract_id BIGINT NOT NULL REFERENCES contract(contract_id), + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + settle_month CHAR(6) NOT NULL, + premium_amount NUMERIC(15,2) NOT NULL, + commission_rate NUMERIC(7,4) NOT NULL, + commission_amount NUMERIC(15,2) NOT NULL, + payment_cycle VARCHAR(20) NOT NULL, + paid_at DATE NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED', + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + CHECK (payment_cycle IN ('MONTHLY','QUARTERLY','SEMI','ANNUAL')), + CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED')) +); + +COMMENT ON TABLE collection_commission_ledger IS '수금수수료 원장 — 매월 보험료 수납 단위로 발생'; +COMMENT ON COLUMN collection_commission_ledger.contract_id IS '대상 계약 FK'; +COMMENT ON COLUMN collection_commission_ledger.agent_id IS '수금 담당 설계사 FK'; +COMMENT ON COLUMN collection_commission_ledger.settle_month IS '정산 귀속월 (YYYYMM)'; +COMMENT ON COLUMN collection_commission_ledger.premium_amount IS '수납 보험료'; +COMMENT ON COLUMN collection_commission_ledger.commission_rate IS '적용 수금수수료율'; +COMMENT ON COLUMN collection_commission_ledger.commission_amount IS '수금수수료 = premium_amount * commission_rate'; +COMMENT ON COLUMN collection_commission_ledger.payment_cycle IS '납입주기'; +COMMENT ON COLUMN collection_commission_ledger.paid_at IS '실 보험료 납입일'; + +CREATE INDEX idx_ccl_agent_month ON collection_commission_ledger(agent_id, settle_month); +CREATE INDEX idx_ccl_contract ON collection_commission_ledger(contract_id); +CREATE INDEX idx_ccl_status ON collection_commission_ledger(status, settle_month); diff --git a/ga-common/src/main/resources/db/migration/V42__갱신_부활수수료.sql b/ga-common/src/main/resources/db/migration/V42__갱신_부활수수료.sql new file mode 100644 index 0000000..9aae464 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V42__갱신_부활수수료.sql @@ -0,0 +1,122 @@ +-- V42: 갱신수수료 + 부활수수료 (rule + ledger 각 2쌍) +-- 갱신: 자동차/실비/단기보험 갱신 계약 시 정률 또는 정액 수수료 +-- 부활: 실효 후 부활 시 수수료 + 기존 환수액 일부 환원 + +-- ============================================================ +-- 1. renewal_commission_rule — 갱신수수료율 마스터 +-- ============================================================ + +CREATE TABLE renewal_commission_rule ( + rule_id BIGSERIAL PRIMARY KEY, + product_id BIGINT NOT NULL REFERENCES product(product_id), + pay_type VARCHAR(20) NOT NULL DEFAULT 'RATE', + rate_value NUMERIC(7,4), -- RATE 방식: 갱신 보험료 대비 비율 + fixed_amount NUMERIC(15,2), -- FIXED 방식: 건당 고정 금액 + effective_from DATE NOT NULL, + effective_to DATE, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + UNIQUE (product_id, effective_from), + CHECK (pay_type IN ('RATE','FIXED')), + CHECK ( + (pay_type = 'RATE' AND rate_value IS NOT NULL) OR + (pay_type = 'FIXED' AND fixed_amount IS NOT NULL) + ) +); + +COMMENT ON TABLE renewal_commission_rule IS '갱신수수료율 마스터 — 자동차/실비/단기보험 갱신 대상 상품별 룰'; +COMMENT ON COLUMN renewal_commission_rule.pay_type IS 'RATE=갱신보험료 대비 비율 / FIXED=건당 고정금액'; +COMMENT ON COLUMN renewal_commission_rule.rate_value IS 'RATE 방식 비율 (소수, 예: 0.02 = 2%)'; +COMMENT ON COLUMN renewal_commission_rule.fixed_amount IS 'FIXED 방식 건당 지급액'; + +CREATE INDEX idx_rcr_product ON renewal_commission_rule(product_id, effective_from); + +-- ============================================================ +-- 2. renewal_commission_ledger — 갱신수수료 원장 +-- ============================================================ + +CREATE TABLE renewal_commission_ledger ( + ledger_id BIGSERIAL PRIMARY KEY, + contract_id BIGINT NOT NULL REFERENCES contract(contract_id), + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + rule_id BIGINT REFERENCES renewal_commission_rule(rule_id), + settle_month CHAR(6) NOT NULL, + renewal_date DATE NOT NULL, + renewal_premium NUMERIC(15,2) NOT NULL, + commission_amount NUMERIC(15,2) NOT NULL, + pay_type VARCHAR(20) NOT NULL DEFAULT 'RATE', + status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED', + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + CHECK (pay_type IN ('RATE','FIXED')), + CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED')) +); + +COMMENT ON TABLE renewal_commission_ledger IS '갱신수수료 원장 — 계약 갱신 1회당 발생'; +COMMENT ON COLUMN renewal_commission_ledger.renewal_date IS '계약 갱신일'; +COMMENT ON COLUMN renewal_commission_ledger.renewal_premium IS '갱신 보험료'; +COMMENT ON COLUMN renewal_commission_ledger.commission_amount IS '갱신수수료 (rate*premium 또는 fixed_amount)'; + +CREATE INDEX idx_rcl_agent_month ON renewal_commission_ledger(agent_id, settle_month); +CREATE INDEX idx_rcl_contract ON renewal_commission_ledger(contract_id); + +-- ============================================================ +-- 3. reinstatement_commission_rule — 부활수수료율 마스터 +-- ============================================================ + +CREATE TABLE reinstatement_commission_rule ( + rule_id BIGSERIAL PRIMARY KEY, + product_id BIGINT NOT NULL REFERENCES product(product_id), + pay_type VARCHAR(20) NOT NULL DEFAULT 'RATE', + rate_value NUMERIC(7,4), -- RATE: 부활 보험료 대비 비율 + fixed_amount NUMERIC(15,2), -- FIXED: 건당 고정 금액 + chargeback_reversal_rate NUMERIC(7,4) NOT NULL DEFAULT 0, + -- 부활 시 기존 환수액 중 환원 비율 (예: 0.5 = 50% 환원) + effective_from DATE NOT NULL, + effective_to DATE, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + UNIQUE (product_id, effective_from), + CHECK (pay_type IN ('RATE','FIXED')), + CHECK (chargeback_reversal_rate >= 0 AND chargeback_reversal_rate <= 1) +); + +COMMENT ON TABLE reinstatement_commission_rule IS '부활수수료율 마스터 — 실효 후 부활 시 적용 룰'; +COMMENT ON COLUMN reinstatement_commission_rule.chargeback_reversal_rate IS '기존 환수액 중 부활 시 환원 비율 (0~1)'; + +CREATE INDEX idx_rsr_product ON reinstatement_commission_rule(product_id, effective_from); + +-- ============================================================ +-- 4. reinstatement_commission_ledger — 부활수수료 원장 +-- ============================================================ + +CREATE TABLE reinstatement_commission_ledger ( + ledger_id BIGSERIAL PRIMARY KEY, + contract_id BIGINT NOT NULL REFERENCES contract(contract_id), + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + rule_id BIGINT REFERENCES reinstatement_commission_rule(rule_id), + settle_month CHAR(6) NOT NULL, + reinstatement_date DATE NOT NULL, + reinstatement_premium NUMERIC(15,2) NOT NULL, + commission_amount NUMERIC(15,2) NOT NULL, + chargeback_reversal NUMERIC(15,2) NOT NULL DEFAULT 0, + -- 기존 환수액 중 환원금 (부활 인센티브) + net_commission NUMERIC(15,2) NOT NULL, + -- 최종 수수료 = commission_amount + chargeback_reversal + pay_type VARCHAR(20) NOT NULL DEFAULT 'RATE', + status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED', + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + CHECK (pay_type IN ('RATE','FIXED')), + CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED')) +); + +COMMENT ON TABLE reinstatement_commission_ledger IS '부활수수료 원장 — 실효 계약 부활 1건당 발생'; +COMMENT ON COLUMN reinstatement_commission_ledger.reinstatement_date IS '계약 부활일'; +COMMENT ON COLUMN reinstatement_commission_ledger.chargeback_reversal IS '부활로 인한 환수액 환원금'; +COMMENT ON COLUMN reinstatement_commission_ledger.net_commission IS '최종 수수료 = 부활수수료 + 환수환원금'; + +CREATE INDEX idx_rsl_agent_month ON reinstatement_commission_ledger(agent_id, settle_month); +CREATE INDEX idx_rsl_contract ON reinstatement_commission_ledger(contract_id); diff --git a/ga-common/src/main/resources/db/migration/V43__유지보수수당.sql b/ga-common/src/main/resources/db/migration/V43__유지보수수당.sql new file mode 100644 index 0000000..e97f2db --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V43__유지보수수당.sql @@ -0,0 +1,74 @@ +-- V43: 유지보수수당 (persistency_bonus_rule + persistency_bonus) +-- 13/25/37/49개월 유지율 충족 시 1회 지급하는 유지보수수당. +-- 등급별 정률(RATE) 또는 정액(FIXED) 지급. + +-- ============================================================ +-- 1. persistency_bonus_rule — 유지보수수당 룰 마스터 +-- ============================================================ + +CREATE TABLE persistency_bonus_rule ( + rule_id BIGSERIAL PRIMARY KEY, + grade_id BIGINT REFERENCES grade(grade_id), + -- NULL이면 전 직급 공통 적용 + retention_month INT NOT NULL, + -- 유지 기준 개월수 (13/25/37/49) + retention_rate_threshold NUMERIC(5,4) NOT NULL, + -- 유지율 하한 기준 (예: 0.95 = 95% 이상) + bonus_type VARCHAR(10) NOT NULL, + bonus_value NUMERIC(15,4) NOT NULL, + -- RATE: 모집수수료 대비 비율 / FIXED: 지급 고정액 + effective_from DATE NOT NULL, + effective_to DATE, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + CHECK (retention_month IN (13, 25, 37, 49)), + CHECK (bonus_type IN ('RATE','FIXED')), + CHECK (retention_rate_threshold > 0 AND retention_rate_threshold <= 1) +); + +COMMENT ON TABLE persistency_bonus_rule IS '유지보수수당 룰 마스터 — 직급/유지개월/유지율 임계치별 지급 기준'; +COMMENT ON COLUMN persistency_bonus_rule.grade_id IS '적용 직급 FK (NULL=전 직급 공통)'; +COMMENT ON COLUMN persistency_bonus_rule.retention_month IS '유지 기준 개월수 (13/25/37/49)'; +COMMENT ON COLUMN persistency_bonus_rule.retention_rate_threshold IS '유지율 최소 기준 (0초과~1이하, 예: 0.95)'; +COMMENT ON COLUMN persistency_bonus_rule.bonus_type IS 'RATE=모집수수료 대비 비율 / FIXED=건당 고정금액'; +COMMENT ON COLUMN persistency_bonus_rule.bonus_value IS 'RATE 방식: 비율값 / FIXED 방식: 지급 금액'; + +CREATE INDEX idx_pbr_grade_month ON persistency_bonus_rule(grade_id, retention_month, effective_from); + +-- ============================================================ +-- 2. persistency_bonus — 유지보수수당 지급 이력 +-- UNIQUE(agent_id, base_month, retention_month) — 동일 기준월·개월수 중복 방지 +-- ============================================================ + +CREATE TABLE persistency_bonus ( + bonus_id BIGSERIAL PRIMARY KEY, + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + rule_id BIGINT REFERENCES persistency_bonus_rule(rule_id), + base_month CHAR(6) NOT NULL, + -- 계약 체결 기준 정산월 (YYYYMM) + retention_month INT NOT NULL, + -- 평가 기준 개월수 (13/25/37/49) + settle_month CHAR(6) NOT NULL, + -- 실제 지급 정산월 + retention_rate NUMERIC(5,4) NOT NULL, + -- 실제 유지율 + bonus_amount NUMERIC(15,2) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED', + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + UNIQUE (agent_id, base_month, retention_month), + CHECK (retention_month IN (13, 25, 37, 49)), + CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED')) +); + +COMMENT ON TABLE persistency_bonus IS '유지보수수당 지급 이력 — 유지율 충족 확인 후 1회 지급'; +COMMENT ON COLUMN persistency_bonus.base_month IS '계약 체결 기준 정산월 (YYYYMM) — 유지율 계산 기산점'; +COMMENT ON COLUMN persistency_bonus.retention_month IS '평가 기준 개월수 (13/25/37/49)'; +COMMENT ON COLUMN persistency_bonus.settle_month IS '실제 지급 귀속 정산월 (YYYYMM)'; +COMMENT ON COLUMN persistency_bonus.retention_rate IS '평가 시점 실제 유지율'; +COMMENT ON COLUMN persistency_bonus.bonus_amount IS '지급 유지보수수당 금액'; + +CREATE INDEX idx_pb_agent_settle ON persistency_bonus(agent_id, settle_month); +CREATE INDEX idx_pb_base_month ON persistency_bonus(base_month, retention_month); +CREATE INDEX idx_pb_status ON persistency_bonus(status, settle_month); diff --git a/ga-common/src/main/resources/db/migration/V44__오버라이드원장.sql b/ga-common/src/main/resources/db/migration/V44__오버라이드원장.sql new file mode 100644 index 0000000..6a8c78a --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V44__오버라이드원장.sql @@ -0,0 +1,77 @@ +-- V44: 오버라이드 원장 보강 (override_ledger + override_summary) +-- 기존 V3의 override_rule + V6의 override_settle은 정산 결과 집계용. +-- 이 파일은 calcOverride 결과를 수수료 종류 단위로 추적하는 원장과 +-- agent+settle_month 단위 합계 테이블을 추가한다. + +-- ============================================================ +-- 1. override_ledger — 오버라이드 수수료 상세 원장 +-- source_commission_type 단위로 어떤 수수료가 오버라이드 원천인지 추적. +-- ============================================================ + +CREATE TABLE override_ledger ( + ledger_id BIGSERIAL PRIMARY KEY, + from_agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + -- 원천 설계사 (수수료를 발생시킨 하위 설계사) + to_agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + -- 오버라이드 수령 설계사 (상위 직급) + org_id BIGINT REFERENCES organization(org_id), + settle_month CHAR(6) NOT NULL, + source_commission_type VARCHAR(20) NOT NULL, + -- 원천 수수료 종류 (COMMISSION_TYPE 코드) + source_ledger_table VARCHAR(50), + -- 원천 원장 테이블명 (recruit_ledger 등) + source_ledger_id BIGINT, + -- 원천 원장 PK + source_amount NUMERIC(15,2) NOT NULL, + -- 오버라이드 계산 기준 수수료 + override_rate NUMERIC(7,4) NOT NULL, + override_amount NUMERIC(15,2) NOT NULL, + -- 최종 오버라이드 금액 + status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED', + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + CHECK (source_commission_type IN ('RECRUIT','MAINTAIN','COLLECT','RENEWAL','REINSTATE','PERSIST','INCENTIVE')), + CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED')) +); + +COMMENT ON TABLE override_ledger IS '오버라이드 원장 — calcOverride 결과를 수수료 종류 단위로 상세 추적'; +COMMENT ON COLUMN override_ledger.from_agent_id IS '원천 설계사 (오버라이드 기준 수수료 발생 설계사)'; +COMMENT ON COLUMN override_ledger.to_agent_id IS '오버라이드 수령 설계사 (상위 직급 관리자)'; +COMMENT ON COLUMN override_ledger.source_commission_type IS '원천 수수료 종류 (COMMISSION_TYPE 코드)'; +COMMENT ON COLUMN override_ledger.source_ledger_table IS '원천 원장 테이블명'; +COMMENT ON COLUMN override_ledger.source_amount IS '오버라이드 계산 기준이 된 원천 수수료'; +COMMENT ON COLUMN override_ledger.override_rate IS '적용 오버라이드율'; +COMMENT ON COLUMN override_ledger.override_amount IS '최종 오버라이드 금액 = source_amount * override_rate'; + +CREATE INDEX idx_ol_to_agent_month ON override_ledger(to_agent_id, settle_month); +CREATE INDEX idx_ol_from_agent ON override_ledger(from_agent_id, settle_month); +CREATE INDEX idx_ol_org_month ON override_ledger(org_id, settle_month); +CREATE INDEX idx_ol_status ON override_ledger(status, settle_month); + +-- ============================================================ +-- 2. override_summary — agent+settle_month 단위 오버라이드 합계 +-- Materialized View 후보이나 갱신 제어를 위해 일반 테이블로. +-- 배치 완료 후 UPSERT 방식으로 집계. +-- ============================================================ + +CREATE TABLE override_summary ( + summary_id BIGSERIAL PRIMARY KEY, + to_agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + settle_month CHAR(6) NOT NULL, + total_source_amount NUMERIC(15,2) NOT NULL DEFAULT 0, + total_override_amount NUMERIC(15,2) NOT NULL DEFAULT 0, + ledger_count INT NOT NULL DEFAULT 0, + status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED', + calc_date TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + UNIQUE (to_agent_id, settle_month), + CHECK (status IN ('CALCULATED','CONFIRMED','PAID')) +); + +COMMENT ON TABLE override_summary IS '오버라이드 합계 — agent+settle_month 단위 배치 집계 결과'; +COMMENT ON COLUMN override_summary.to_agent_id IS '오버라이드 수령 설계사 (상위 직급)'; +COMMENT ON COLUMN override_summary.total_source_amount IS '오버라이드 계산 기준 원천 수수료 합계'; +COMMENT ON COLUMN override_summary.total_override_amount IS '최종 오버라이드 지급액 합계'; +COMMENT ON COLUMN override_summary.ledger_count IS '오버라이드 원장 건수'; + +CREATE INDEX idx_os_month ON override_summary(settle_month, status); diff --git a/ga-common/src/main/resources/db/migration/V45__위촉계약.sql b/ga-common/src/main/resources/db/migration/V45__위촉계약.sql new file mode 100644 index 0000000..f812246 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V45__위촉계약.sql @@ -0,0 +1,38 @@ +-- V45: 위촉계약 (agent_contract) +-- 보험사-설계사 간 위촉계약서. 보험사별 활동 자격 + 위촉수수료 관리. + +CREATE TABLE agent_contract ( + contract_id BIGSERIAL PRIMARY KEY, + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + carrier_id BIGINT NOT NULL REFERENCES insurance_company(company_id), + contract_no VARCHAR(50) NOT NULL, + signed_date DATE NOT NULL, + effective_from DATE NOT NULL, + effective_to DATE, + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + onboarding_bonus_amount NUMERIC(15,2) NOT NULL DEFAULT 0, + -- 위촉수수료 (입사 인센티브 성격) + terminated_at TIMESTAMP, + terminated_reason VARCHAR(500), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + updated_by BIGINT, + UNIQUE (agent_id, carrier_id, contract_no), + CHECK (status IN ('ACTIVE','TERMINATED','SUSPENDED')) +); + +COMMENT ON TABLE agent_contract IS '위촉계약 — 보험사-설계사 위촉계약서 + 효력 기간 + 위촉수수료'; +COMMENT ON COLUMN agent_contract.agent_id IS '설계사 FK'; +COMMENT ON COLUMN agent_contract.carrier_id IS '보험사 FK (insurance_company)'; +COMMENT ON COLUMN agent_contract.contract_no IS '위촉계약 번호'; +COMMENT ON COLUMN agent_contract.signed_date IS '위촉계약 서명일'; +COMMENT ON COLUMN agent_contract.effective_from IS '위촉 효력 시작일'; +COMMENT ON COLUMN agent_contract.effective_to IS '위촉 효력 종료일 (NULL=현재 유효)'; +COMMENT ON COLUMN agent_contract.status IS 'ACTIVE=유효/TERMINATED=해촉/SUSPENDED=정지'; +COMMENT ON COLUMN agent_contract.onboarding_bonus_amount IS '위촉수수료 (위촉 시 지급하는 인센티브)'; +COMMENT ON COLUMN agent_contract.terminated_reason IS '해촉 사유'; + +CREATE INDEX idx_ac_agent ON agent_contract(agent_id, status); +CREATE INDEX idx_ac_carrier ON agent_contract(carrier_id, status); +CREATE INDEX idx_ac_effective ON agent_contract(effective_from, effective_to); diff --git a/ga-common/src/main/resources/db/migration/V46__자격관리.sql b/ga-common/src/main/resources/db/migration/V46__자격관리.sql new file mode 100644 index 0000000..55cc35c --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V46__자격관리.sql @@ -0,0 +1,35 @@ +-- V46: 설계사 자격 관리 (agent_license) +-- 생보/손보/변액 자격증 만료·갱신 이력. 만료 임박 알림용 인덱스 포함. + +CREATE TABLE agent_license ( + license_id BIGSERIAL PRIMARY KEY, + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + license_type VARCHAR(20) NOT NULL, + license_no VARCHAR(50) NOT NULL, + issued_date DATE NOT NULL, + expires_date DATE NOT NULL, + renewed_from BIGINT REFERENCES agent_license(license_id), + -- 갱신 전 자격증 참조 + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + UNIQUE (agent_id, license_type, license_no), + CHECK (license_type IN ('LIFE','NONLIFE','VARIABLE')), + CHECK (status IN ('ACTIVE','EXPIRED','SUSPENDED')), + CHECK (expires_date > issued_date) +); + +COMMENT ON TABLE agent_license IS '설계사 자격 관리 — 생보/손보/변액 자격증 만료·갱신 이력'; +COMMENT ON COLUMN agent_license.agent_id IS '설계사 FK'; +COMMENT ON COLUMN agent_license.license_type IS '자격 종류 (LIFE=생명보험/NONLIFE=손해보험/VARIABLE=변액보험)'; +COMMENT ON COLUMN agent_license.license_no IS '자격증 번호'; +COMMENT ON COLUMN agent_license.issued_date IS '자격증 발급일'; +COMMENT ON COLUMN agent_license.expires_date IS '자격증 만료일 (갱신 주기 5년 기준)'; +COMMENT ON COLUMN agent_license.renewed_from IS '갱신 전 자격증 ID (갱신 이력 추적용)'; +COMMENT ON COLUMN agent_license.status IS 'ACTIVE=유효/EXPIRED=만료/SUSPENDED=정지'; + +-- 만료 임박 알림용 인덱스 (expires_date 단독 + 복합) +CREATE INDEX idx_al_agent ON agent_license(agent_id, status); +CREATE INDEX idx_al_expires ON agent_license(expires_date, status); +CREATE INDEX idx_al_type_expires ON agent_license(license_type, expires_date); diff --git a/ga-common/src/main/resources/db/migration/V47__보수교육.sql b/ga-common/src/main/resources/db/migration/V47__보수교육.sql new file mode 100644 index 0000000..9764de5 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V47__보수교육.sql @@ -0,0 +1,60 @@ +-- V47: 보수교육 (agent_training + training_requirement) +-- 보험업법 시행령 제17조 기준 의무 교육 이수 시간 추적. +-- 연간 12시간 의무 기준은 training_requirement에 외부화. + +-- ============================================================ +-- 1. training_requirement — 연도별 의무 교육 시간 마스터 +-- ============================================================ + +CREATE TABLE training_requirement ( + req_id SERIAL PRIMARY KEY, + target_year INT NOT NULL UNIQUE, + required_hours INT NOT NULL DEFAULT 12, + -- 기본값 12시간/년 (보험업법 시행령 제17조) + description VARCHAR(300), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP +); + +COMMENT ON TABLE training_requirement IS '보수교육 의무 시간 마스터 — 연도별 의무 교육 시간 기준'; +COMMENT ON COLUMN training_requirement.target_year IS '적용 연도'; +COMMENT ON COLUMN training_requirement.required_hours IS '연간 의무 교육 시간 (기본 12시간)'; + +-- 현재 연도 + 차기 연도 기본값 삽입 +INSERT INTO training_requirement (target_year, required_hours, description) VALUES + (2025, 12, '보험업법 시행령 제17조 기준 연간 의무 보수교육'), + (2026, 12, '보험업법 시행령 제17조 기준 연간 의무 보수교육') +ON CONFLICT (target_year) DO NOTHING; + +-- ============================================================ +-- 2. agent_training — 설계사 보수교육 이수 이력 +-- ============================================================ + +CREATE TABLE agent_training ( + training_id BIGSERIAL PRIMARY KEY, + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + course_name VARCHAR(200) NOT NULL, + course_type VARCHAR(20) NOT NULL, + training_date DATE NOT NULL, + hours NUMERIC(5,1) NOT NULL, + certificate_no VARCHAR(100), + instructor VARCHAR(100), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + CHECK (course_type IN ('REGULAR','COMPLIANCE','PRODUCT')), + CHECK (hours > 0) +); + +COMMENT ON TABLE agent_training IS '보수교육 이수 이력 — 설계사 연간 의무 교육 이수 시간 추적'; +COMMENT ON COLUMN agent_training.agent_id IS '설계사 FK'; +COMMENT ON COLUMN agent_training.course_name IS '교육 과목명'; +COMMENT ON COLUMN agent_training.course_type IS '교육 유형 (REGULAR=정기보수/COMPLIANCE=컴플라이언스/PRODUCT=상품교육)'; +COMMENT ON COLUMN agent_training.training_date IS '교육 이수일'; +COMMENT ON COLUMN agent_training.hours IS '이수 시간'; +COMMENT ON COLUMN agent_training.certificate_no IS '이수증 번호'; +COMMENT ON COLUMN agent_training.instructor IS '강사명 또는 교육기관'; + +CREATE INDEX idx_at_agent_date ON agent_training(agent_id, training_date); +CREATE INDEX idx_at_course_type ON agent_training(course_type, training_date); diff --git a/ga-common/src/main/resources/db/migration/V48__계약변경.sql b/ga-common/src/main/resources/db/migration/V48__계약변경.sql new file mode 100644 index 0000000..e5c6d89 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V48__계약변경.sql @@ -0,0 +1,40 @@ +-- V48: 계약 변경 (contract_endorsement) +-- 보험료/수익자/피보험자/주소 변경 등 계약 변경 이력. +-- requires_commission_recalc=true 이면 배치에서 수수료 재계산 트리거. + +CREATE TABLE contract_endorsement ( + endorsement_id BIGSERIAL PRIMARY KEY, + contract_id BIGINT NOT NULL REFERENCES contract(contract_id), + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + endorsement_type VARCHAR(30) NOT NULL, + changed_at TIMESTAMP NOT NULL DEFAULT NOW(), + old_value TEXT, + -- 변경 전 값 (JSON 또는 텍스트) + new_value TEXT, + -- 변경 후 값 (JSON 또는 텍스트) + reason VARCHAR(500), + requires_commission_recalc BOOLEAN NOT NULL DEFAULT FALSE, + -- TRUE이면 배치 수수료 재계산 대상 + processed BOOLEAN NOT NULL DEFAULT FALSE, + -- 재계산 처리 완료 여부 + processed_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + CHECK (endorsement_type IN ('PREMIUM_CHANGE','BENEFICIARY','INSURED','ADDRESS','OTHER')) +); + +COMMENT ON TABLE contract_endorsement IS '계약 변경 이력 — 변경 유형별 old/new 값 보관 + 수수료 재계산 트리거'; +COMMENT ON COLUMN contract_endorsement.contract_id IS '대상 계약 FK'; +COMMENT ON COLUMN contract_endorsement.agent_id IS '담당 설계사 FK'; +COMMENT ON COLUMN contract_endorsement.endorsement_type IS '변경 유형 (PREMIUM_CHANGE=보험료변경/BENEFICIARY=수익자/INSURED=피보험자/ADDRESS=주소/OTHER=기타)'; +COMMENT ON COLUMN contract_endorsement.old_value IS '변경 전 값'; +COMMENT ON COLUMN contract_endorsement.new_value IS '변경 후 값'; +COMMENT ON COLUMN contract_endorsement.requires_commission_recalc IS '수수료 재계산 필요 여부 (TRUE이면 배치 재처리 대상)'; +COMMENT ON COLUMN contract_endorsement.processed IS '재계산 처리 완료 여부'; + +CREATE INDEX idx_ce_contract ON contract_endorsement(contract_id, changed_at); +CREATE INDEX idx_ce_agent ON contract_endorsement(agent_id, changed_at); +CREATE INDEX idx_ce_recalc ON contract_endorsement(requires_commission_recalc, processed) + WHERE requires_commission_recalc = TRUE AND processed = FALSE; +-- 재계산 미처리 건 빠른 조회용 부분 인덱스 diff --git a/ga-common/src/main/resources/db/migration/V49__민원관리.sql b/ga-common/src/main/resources/db/migration/V49__민원관리.sql new file mode 100644 index 0000000..907cdf7 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V49__민원관리.sql @@ -0,0 +1,49 @@ +-- V49: 민원 관리 (complaint) +-- 고객 민원 접수 → 설계사 책임 추적 → 처리 상태 관리 → 환수 연동. +-- 13M 이내 민원 = 환수 100% 트리거 가정 (chargeback_triggered). + +CREATE TABLE complaint ( + complaint_id BIGSERIAL PRIMARY KEY, + contract_id BIGINT REFERENCES contract(contract_id), + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + customer_name VARCHAR(100) NOT NULL, + complaint_date DATE NOT NULL, + channel VARCHAR(20) NOT NULL, + complaint_type VARCHAR(30) NOT NULL, + severity VARCHAR(10) NOT NULL DEFAULT 'MID', + status VARCHAR(20) NOT NULL DEFAULT 'OPEN', + resolution TEXT, + -- 처리 결과 기술 + chargeback_triggered BOOLEAN NOT NULL DEFAULT FALSE, + -- TRUE이면 환수 자동 생성 대상 + settled_at TIMESTAMP, + -- 민원 종결 시각 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + updated_by BIGINT, + CHECK (channel IN ('PHONE','EMAIL','VISIT','ONLINE','FAX')), + CHECK (complaint_type IN ('MIS_SELLING','DELAY','UNFAIR','CANCELLATION','ETC')), + CHECK (severity IN ('LOW','MID','HIGH')), + CHECK (status IN ('OPEN','IN_PROGRESS','CLOSED','WITHDRAWN')) +); + +COMMENT ON TABLE complaint IS '민원 관리 — 고객 민원 접수·처리·환수 연동'; +COMMENT ON COLUMN complaint.contract_id IS '민원 대상 계약 FK (계약 특정 불가한 경우 NULL 허용)'; +COMMENT ON COLUMN complaint.agent_id IS '민원 책임 설계사 FK'; +COMMENT ON COLUMN complaint.customer_name IS '민원인 성명'; +COMMENT ON COLUMN complaint.complaint_date IS '민원 접수일'; +COMMENT ON COLUMN complaint.channel IS '접수 채널 (PHONE/EMAIL/VISIT/ONLINE/FAX)'; +COMMENT ON COLUMN complaint.complaint_type IS '민원 유형 (MIS_SELLING=불완전판매/DELAY=처리지연/UNFAIR=불공정/CANCELLATION=해지관련/ETC=기타)'; +COMMENT ON COLUMN complaint.severity IS '심각도 (LOW=낮음/MID=보통/HIGH=높음)'; +COMMENT ON COLUMN complaint.status IS '처리 상태 (OPEN=접수/IN_PROGRESS=처리중/CLOSED=완료/WITHDRAWN=취하)'; +COMMENT ON COLUMN complaint.resolution IS '처리 결과 상세 기술'; +COMMENT ON COLUMN complaint.chargeback_triggered IS 'TRUE=환수 트리거 발생 (13M 이내 민원 등)'; +COMMENT ON COLUMN complaint.settled_at IS '민원 종결 시각'; + +CREATE INDEX idx_comp_agent_date ON complaint(agent_id, complaint_date); +CREATE INDEX idx_comp_status ON complaint(status, complaint_date); +CREATE INDEX idx_comp_contract ON complaint(contract_id); +CREATE INDEX idx_comp_chargeback ON complaint(chargeback_triggered, status) + WHERE chargeback_triggered = TRUE; +-- 환수 트리거 미처리 건 조회용 부분 인덱스 diff --git a/ga-common/src/main/resources/db/migration/V50__결재워크플로우.sql b/ga-common/src/main/resources/db/migration/V50__결재워크플로우.sql new file mode 100644 index 0000000..21617ac --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V50__결재워크플로우.sql @@ -0,0 +1,132 @@ +-- V50: 결재 워크플로우 (approval_line + approval_line_step + approval_request + approval_history) +-- 정산/이의신청/시책/출금 결재선 마스터 + 다단 결재 요청·처리 이력. +-- 최대 3단계 결재 (시스템 가정값, approval_line.max_step 으로 외부화). + +-- ============================================================ +-- 1. approval_line — 결재선 마스터 +-- ============================================================ + +CREATE TABLE approval_line ( + line_id BIGSERIAL PRIMARY KEY, + line_code VARCHAR(50) NOT NULL UNIQUE, + line_name VARCHAR(100) NOT NULL, + max_step INT NOT NULL DEFAULT 3, + target_type VARCHAR(20) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_at TIMESTAMP, + CHECK (target_type IN ('SETTLE','DISPUTE','INCENTIVE','WITHDRAW','COMPLAINT')), + CHECK (max_step BETWEEN 1 AND 5) +); + +COMMENT ON TABLE approval_line IS '결재선 마스터 — 업무 유형별 결재 단계 정의'; +COMMENT ON COLUMN approval_line.line_code IS '결재선 코드 (예: SETTLE_DEFAULT)'; +COMMENT ON COLUMN approval_line.max_step IS '최대 결재 단계 수 (1~5, 기본 3)'; +COMMENT ON COLUMN approval_line.target_type IS '적용 업무 유형 (SETTLE=정산/DISPUTE=이의/INCENTIVE=시책/WITHDRAW=출금/COMPLAINT=민원)'; + +-- 기본 결재선 초기 데이터 +INSERT INTO approval_line (line_code, line_name, max_step, target_type, is_active) VALUES + ('SETTLE_DEFAULT', '정산 기본 결재선', 2, 'SETTLE', TRUE), + ('DISPUTE_DEFAULT', '이의신청 결재선', 2, 'DISPUTE', TRUE), + ('INCENTIVE_DEFAULT','시책 결재선', 2, 'INCENTIVE', TRUE), + ('WITHDRAW_DEFAULT', '출금신청 결재선', 3, 'WITHDRAW', TRUE), + ('COMPLAINT_DEFAULT','민원처리 결재선', 2, 'COMPLAINT', TRUE) +ON CONFLICT (line_code) DO NOTHING; + +-- ============================================================ +-- 2. approval_line_step — 결재선 단계별 역할 정의 +-- ============================================================ + +CREATE TABLE approval_line_step ( + step_id BIGSERIAL PRIMARY KEY, + line_id BIGINT NOT NULL REFERENCES approval_line(line_id), + step_no INT NOT NULL, + role_code VARCHAR(50) NOT NULL, + -- 결재 역할 코드 (role 테이블 참조, 느슨한 참조) + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + UNIQUE (line_id, step_no), + CHECK (step_no >= 1) +); + +COMMENT ON TABLE approval_line_step IS '결재선 단계 — 결재선별 단계 순서와 결재 역할 매핑'; +COMMENT ON COLUMN approval_line_step.step_no IS '결재 단계 번호 (1부터 순서대로)'; +COMMENT ON COLUMN approval_line_step.role_code IS '해당 단계 결재 역할 코드'; + +-- 기본 결재선 단계 삽입 +INSERT INTO approval_line_step (line_id, step_no, role_code) +SELECT l.line_id, s.step_no, s.role_code +FROM approval_line l +JOIN (VALUES + ('SETTLE_DEFAULT', 1, 'MANAGER'), + ('SETTLE_DEFAULT', 2, 'ADMIN'), + ('DISPUTE_DEFAULT', 1, 'MANAGER'), + ('DISPUTE_DEFAULT', 2, 'ADMIN'), + ('INCENTIVE_DEFAULT', 1, 'MANAGER'), + ('INCENTIVE_DEFAULT', 2, 'ADMIN'), + ('WITHDRAW_DEFAULT', 1, 'MANAGER'), + ('WITHDRAW_DEFAULT', 2, 'ADMIN'), + ('WITHDRAW_DEFAULT', 3, 'SUPER_ADMIN'), + ('COMPLAINT_DEFAULT', 1, 'MANAGER'), + ('COMPLAINT_DEFAULT', 2, 'ADMIN') +) AS s(line_code, step_no, role_code) ON l.line_code = s.line_code +ON CONFLICT (line_id, step_no) DO NOTHING; + +-- ============================================================ +-- 3. approval_request — 결재 요청 +-- ============================================================ + +CREATE TABLE approval_request ( + request_id BIGSERIAL PRIMARY KEY, + target_type VARCHAR(20) NOT NULL, + target_id BIGINT NOT NULL, + -- 결재 대상 PK (settle_id / dispute_id 등) + line_id BIGINT NOT NULL REFERENCES approval_line(line_id), + requested_by BIGINT NOT NULL, + -- 요청자 user_id (users 테이블 참조, 느슨한 참조) + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + current_step INT NOT NULL DEFAULT 1, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + completed_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + CHECK (target_type IN ('SETTLE','DISPUTE','INCENTIVE','WITHDRAW','COMPLAINT')), + CHECK (status IN ('PENDING','APPROVED','REJECTED','CANCEL')) +); + +COMMENT ON TABLE approval_request IS '결재 요청 — 업무별 결재 요청 생성 및 진행 상태 관리'; +COMMENT ON COLUMN approval_request.target_type IS '결재 대상 업무 유형'; +COMMENT ON COLUMN approval_request.target_id IS '결재 대상 레코드 PK'; +COMMENT ON COLUMN approval_request.current_step IS '현재 결재 단계 번호'; +COMMENT ON COLUMN approval_request.status IS 'PENDING=진행중/APPROVED=최종승인/REJECTED=반려/CANCEL=취소'; + +CREATE INDEX idx_ar_target ON approval_request(target_type, target_id); +CREATE INDEX idx_ar_status ON approval_request(status, requested_at); +CREATE INDEX idx_ar_requester ON approval_request(requested_by, status); + +-- ============================================================ +-- 4. approval_history — 결재 처리 이력 +-- ============================================================ + +CREATE TABLE approval_history ( + history_id BIGSERIAL PRIMARY KEY, + request_id BIGINT NOT NULL REFERENCES approval_request(request_id), + step_no INT NOT NULL, + approver_id BIGINT NOT NULL, + -- 결재자 user_id (느슨한 참조) + action VARCHAR(20) NOT NULL, + comment VARCHAR(1000), + processed_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + CHECK (action IN ('APPROVE','REJECT','RETURN')) +); + +COMMENT ON TABLE approval_history IS '결재 처리 이력 — 단계별 결재자 액션 및 코멘트 기록'; +COMMENT ON COLUMN approval_history.request_id IS '결재 요청 FK'; +COMMENT ON COLUMN approval_history.step_no IS '처리 단계 번호'; +COMMENT ON COLUMN approval_history.approver_id IS '결재자 user_id'; +COMMENT ON COLUMN approval_history.action IS 'APPROVE=승인/REJECT=반려/RETURN=반송'; +COMMENT ON COLUMN approval_history.comment IS '결재 코멘트'; + +CREATE INDEX idx_ah_request ON approval_history(request_id, step_no); +CREATE INDEX idx_ah_approver ON approval_history(approver_id, processed_at); diff --git a/ga-common/src/main/resources/db/migration/V51__공지_알림.sql b/ga-common/src/main/resources/db/migration/V51__공지_알림.sql new file mode 100644 index 0000000..0fe7697 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V51__공지_알림.sql @@ -0,0 +1,63 @@ +-- V51: 공지사항 + 사용자 알림 (notice + user_notification) +-- 시스템 공지 / 정책 공지 / 이벤트 공지 + 사용자별 개인 알림(읽음 여부). + +-- ============================================================ +-- 1. notice — 공지사항 +-- ============================================================ + +CREATE TABLE notice ( + notice_id BIGSERIAL PRIMARY KEY, + title VARCHAR(300) NOT NULL, + content TEXT NOT NULL, + notice_type VARCHAR(20) NOT NULL DEFAULT 'SYSTEM', + start_date DATE NOT NULL, + end_date DATE, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_by BIGINT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + updated_by BIGINT, + CHECK (notice_type IN ('SYSTEM','POLICY','EVENT')), + CHECK (end_date IS NULL OR end_date >= start_date) +); + +COMMENT ON TABLE notice IS '공지사항 — 시스템/정책/이벤트 공지 게시판'; +COMMENT ON COLUMN notice.notice_type IS 'SYSTEM=시스템공지/POLICY=정책공지/EVENT=이벤트'; +COMMENT ON COLUMN notice.start_date IS '공지 게시 시작일'; +COMMENT ON COLUMN notice.end_date IS '공지 게시 종료일 (NULL=무기한)'; +COMMENT ON COLUMN notice.is_active IS '공지 활성 여부'; + +CREATE INDEX idx_notice_date_type ON notice(start_date, end_date, notice_type, is_active); + +-- ============================================================ +-- 2. user_notification — 사용자 개인 알림 +-- 정산/이의신청/결재 등 이벤트 발생 시 개인 알림 생성. +-- ============================================================ + +CREATE TABLE user_notification ( + notification_id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + -- users 테이블 참조 (느슨한 참조 — 공통모듈 의존 최소화) + title VARCHAR(300) NOT NULL, + content TEXT, + ref_type VARCHAR(30), + -- 참조 업무 유형 (SETTLE/DISPUTE/APPROVAL/COMPLAINT 등) + ref_id BIGINT, + -- 참조 레코드 PK + is_read BOOLEAN NOT NULL DEFAULT FALSE, + read_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + CHECK (ref_type IS NULL OR ref_type IN ('SETTLE','DISPUTE','APPROVAL','COMPLAINT','INCENTIVE','WITHDRAW','NOTICE')) +); + +COMMENT ON TABLE user_notification IS '사용자 개인 알림 — 이벤트 발생 시 user_id 단위로 생성'; +COMMENT ON COLUMN user_notification.user_id IS '알림 수신 사용자 ID'; +COMMENT ON COLUMN user_notification.ref_type IS '참조 업무 유형 (SETTLE/DISPUTE/APPROVAL/COMPLAINT/INCENTIVE/WITHDRAW/NOTICE)'; +COMMENT ON COLUMN user_notification.ref_id IS '참조 레코드 PK'; +COMMENT ON COLUMN user_notification.is_read IS '읽음 여부'; +COMMENT ON COLUMN user_notification.read_at IS '읽은 시각'; + +-- user_id+is_read 복합 인덱스 — 읽지 않은 알림 빠른 조회 +CREATE INDEX idx_un_user_read ON user_notification(user_id, is_read); +CREATE INDEX idx_un_created ON user_notification(created_at DESC); +CREATE INDEX idx_un_ref ON user_notification(ref_type, ref_id); diff --git a/ga-common/src/main/resources/db/migration/V52__첨부파일.sql b/ga-common/src/main/resources/db/migration/V52__첨부파일.sql new file mode 100644 index 0000000..ca5e921 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V52__첨부파일.sql @@ -0,0 +1,36 @@ +-- V52: 첨부파일 (attachment) +-- 도메인 무관 공통 첨부. ref_type + ref_id 조합으로 어느 도메인 레코드에도 첨부 가능. +-- 파일 실체는 스토리지(로컬/S3). file_path에 경로 저장. + +CREATE TABLE attachment ( + attachment_id BIGSERIAL PRIMARY KEY, + ref_type VARCHAR(50) NOT NULL, + ref_id BIGINT NOT NULL, + file_name VARCHAR(300) NOT NULL, + file_path VARCHAR(1000) NOT NULL, + file_size BIGINT NOT NULL, + -- bytes 단위 + mime_type VARCHAR(100), + uploaded_by BIGINT NOT NULL, + -- 업로더 user_id (느슨한 참조) + uploaded_at TIMESTAMP NOT NULL DEFAULT NOW(), + CHECK (ref_type IN ( + 'AGENT_CONTRACT','COMPLAINT','APPROVAL_REQUEST', + 'WITHDRAW_REQUEST','NOTICE','DISPUTE','INCENTIVE', + 'AGENT_LICENSE','AGENT_TRAINING' + )), + CHECK (file_size > 0) +); + +COMMENT ON TABLE attachment IS '공통 첨부파일 — ref_type+ref_id 조합으로 모든 도메인에 첨부 연결'; +COMMENT ON COLUMN attachment.ref_type IS '첨부 대상 도메인 유형 (AGENT_CONTRACT/COMPLAINT/APPROVAL_REQUEST 등)'; +COMMENT ON COLUMN attachment.ref_id IS '첨부 대상 레코드 PK'; +COMMENT ON COLUMN attachment.file_name IS '원본 파일명'; +COMMENT ON COLUMN attachment.file_path IS '저장 경로 (로컬 절대경로 또는 S3 키)'; +COMMENT ON COLUMN attachment.file_size IS '파일 크기 (bytes)'; +COMMENT ON COLUMN attachment.mime_type IS 'MIME 타입 (예: application/pdf)'; +COMMENT ON COLUMN attachment.uploaded_by IS '업로더 user_id'; + +-- ref_type + ref_id 복합 인덱스 — 도메인별 첨부 목록 조회 +CREATE INDEX idx_att_ref ON attachment(ref_type, ref_id); +CREATE INDEX idx_att_uploader ON attachment(uploaded_by, uploaded_at); diff --git a/ga-common/src/main/resources/db/migration/V53__출금신청.sql b/ga-common/src/main/resources/db/migration/V53__출금신청.sql new file mode 100644 index 0000000..bb3779c --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V53__출금신청.sql @@ -0,0 +1,45 @@ +-- V53: 출금 신청 (withdraw_request) +-- 정산 확정 후 설계사 수수료 출금 신청 → 결재 → 펌뱅킹 송신 → 결과 반영. +-- account_no는 AES 암호화 대상 (애플리케이션 EncryptTypeHandler 처리). + +CREATE TABLE withdraw_request ( + request_id BIGSERIAL PRIMARY KEY, + settle_master_id BIGINT NOT NULL REFERENCES settle_master(settle_id), + agent_id BIGINT NOT NULL REFERENCES agent(agent_id), + amount NUMERIC(15,2) NOT NULL, + requested_by BIGINT NOT NULL, + -- 신청자 user_id (느슨한 참조) + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + approval_request_id BIGINT REFERENCES approval_request(request_id), + bank_code VARCHAR(10) NOT NULL, + account_no VARCHAR(200) NOT NULL, + -- AES 암호화 대상 (EncryptTypeHandler) + status VARCHAR(20) NOT NULL DEFAULT 'REQUESTED', + sent_at TIMESTAMP, + -- 펌뱅킹 송신 시각 + completed_at TIMESTAMP, + -- 입금 완료 시각 + fail_reason VARCHAR(500), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + CHECK (amount > 0), + CHECK (status IN ('REQUESTED','APPROVED','SENT','COMPLETED','FAILED','CANCELLED')) +); + +COMMENT ON TABLE withdraw_request IS '출금 신청 — 정산 확정 후 수수료 출금 결재→펌뱅킹→완료 워크플로우'; +COMMENT ON COLUMN withdraw_request.settle_master_id IS '출금 기준 정산 FK (settle_master)'; +COMMENT ON COLUMN withdraw_request.agent_id IS '출금 대상 설계사 FK'; +COMMENT ON COLUMN withdraw_request.amount IS '출금 신청 금액'; +COMMENT ON COLUMN withdraw_request.requested_by IS '신청자 user_id'; +COMMENT ON COLUMN withdraw_request.approval_request_id IS '결재 요청 FK (approval_request)'; +COMMENT ON COLUMN withdraw_request.bank_code IS '출금 은행 코드'; +COMMENT ON COLUMN withdraw_request.account_no IS '출금 계좌번호 (AES 암호화, EncryptTypeHandler 처리)'; +COMMENT ON COLUMN withdraw_request.status IS 'REQUESTED=신청/APPROVED=결재완료/SENT=펌뱅킹송신/COMPLETED=입금완료/FAILED=실패/CANCELLED=취소'; +COMMENT ON COLUMN withdraw_request.sent_at IS '펌뱅킹 송신 시각'; +COMMENT ON COLUMN withdraw_request.completed_at IS '입금 완료 확인 시각'; +COMMENT ON COLUMN withdraw_request.fail_reason IS '실패 사유 (FAILED 상태 시)'; + +CREATE INDEX idx_wr_agent_status ON withdraw_request(agent_id, status); +CREATE INDEX idx_wr_settle ON withdraw_request(settle_master_id); +CREATE INDEX idx_wr_status_date ON withdraw_request(status, requested_at); +CREATE INDEX idx_wr_approval ON withdraw_request(approval_request_id); diff --git a/ga-common/src/main/resources/db/migration/V54__P4메뉴.sql b/ga-common/src/main/resources/db/migration/V54__P4메뉴.sql new file mode 100644 index 0000000..3c39ae1 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V54__P4메뉴.sql @@ -0,0 +1,198 @@ +-- V54: P4 도메인 메뉴 일괄 등록 (V40~V53 대응) +-- +-- 신규 부모 그룹: GRP_COMMISSION "수수료 마스터" (P4-A 5개 자식) +-- 기존 GRP_ORG 아래: 위촉계약/자격관리/보수교육 +-- 기존 GRP_PRD 아래: 계약변경/민원관리 (GRP_CONTRACT_OP 대신 GRP_PRD 활용) +-- 기존 GRP_SYSTEM 아래: 결재/공지/첨부/출금 + +-- ============================================================ +-- 1. GRP_COMMISSION 부모 그룹 메뉴 신설 +-- ============================================================ + +INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, menu_icon, menu_level, sort_order, is_visible, is_active, description) +VALUES (NULL, 'GRP_COMMISSION', '수수료 마스터', 'DIRECTORY', NULL, 'dollar', 1, 45, 'Y', 'Y', '수수료 종류별 원장 · 룰 관리') +ON CONFLICT (menu_code) DO NOTHING; + +-- ============================================================ +-- 2. GRP_COMMISSION 하위 PAGE 5개 (P4-A) +-- ============================================================ + +INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order) +SELECT p.menu_id, x.code, x.name, 'PAGE', x.path, x.cmpt, 2, x.sort +FROM menu p +JOIN (VALUES + ('COMM_TYPE', '수수료 종류 코드', '/commission/type', 'commission/CommType', 10), + ('COMM_MASTER', '수수료 통합 원장', '/commission/master', 'commission/CommMaster', 20), + ('COLLECT_COMM', '수금수수료', '/commission/collection', 'commission/CollectComm', 30), + ('RENEWAL_COMM', '갱신수수료', '/commission/renewal', 'commission/RenewalComm', 40), + ('REINSTATE_COMM', '부활수수료', '/commission/reinstate', 'commission/ReinstateComm', 50), + ('PERSIST_BONUS', '유지보수수당', '/commission/persist', 'commission/PersistBonus', 60), + ('OVERRIDE_LEDGER', '오버라이드 원장', '/commission/override', 'commission/OverrideLedger', 70) +) AS x(code, name, path, cmpt, sort) +ON p.menu_code = 'GRP_COMMISSION' +WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code); + +-- ============================================================ +-- 3. GRP_ORG 하위 PAGE 3개 (P4-B 설계사 라이프사이클) +-- ============================================================ + +INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order) +SELECT p.menu_id, x.code, x.name, 'PAGE', x.path, x.cmpt, 2, x.sort +FROM menu p +JOIN (VALUES + ('AGENT_CONTRACT', '위촉계약', '/org/agent-contract', 'org/AgentContract', 30), + ('AGENT_LICENSE', '자격 관리', '/org/agent-license', 'org/AgentLicense', 40), + ('AGENT_TRAINING', '보수교육', '/org/agent-training', 'org/AgentTraining', 50) +) AS x(code, name, path, cmpt, sort) +ON p.menu_code = 'GRP_ORG' +WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code); + +-- ============================================================ +-- 4. GRP_PRD 하위 PAGE 2개 (계약변경 / 민원관리) +-- ============================================================ + +INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order) +SELECT p.menu_id, x.code, x.name, 'PAGE', x.path, x.cmpt, 2, x.sort +FROM menu p +JOIN (VALUES + ('ENDORSE', '계약 변경', '/contract/endorsement', 'contract/Endorsement', 40), + ('COMPLAINT', '민원 관리', '/contract/complaint', 'contract/Complaint', 50) +) AS x(code, name, path, cmpt, sort) +ON p.menu_code = 'GRP_PRD' +WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code); + +-- ============================================================ +-- 5. GRP_SYSTEM 하위 PAGE 4개 (결재/공지/첨부/출금) +-- ============================================================ + +INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order) +SELECT p.menu_id, x.code, x.name, 'PAGE', x.path, x.cmpt, 2, x.sort +FROM menu p +JOIN (VALUES + ('APPROVAL', '결재 관리', '/system/approval', 'system/Approval', 70), + ('NOTICE', '공지사항', '/system/notice', 'system/Notice', 80), + ('ATTACHMENT', '첨부파일', '/system/attachment', 'system/Attachment', 90), + ('WITHDRAW', '출금 신청', '/system/withdraw', 'system/Withdraw', 95) +) AS x(code, name, path, cmpt, sort) +ON p.menu_code = 'GRP_SYSTEM' +WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code); + +-- ============================================================ +-- 6. GRP_COMMISSION 에 READ 권한 등록 (대메뉴 expand 용) +-- ============================================================ + +INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order) +SELECT menu_id, 'READ', '조회', 1 FROM menu WHERE menu_code = 'GRP_COMMISSION' +ON CONFLICT (menu_id, perm_code) DO NOTHING; + +INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted) +SELECT r.role_id, m.menu_id, 'READ', 'Y' +FROM role r CROSS JOIN menu m +WHERE r.role_code IN ('SUPER_ADMIN','ADMIN','MANAGER','AGENT') + AND m.menu_code = 'GRP_COMMISSION' +ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING; + +-- ============================================================ +-- 7. 전체 신규 PAGE 메뉴에 권한 항목 등록 +-- 수수료 도메인: READ/CREATE/UPDATE/DELETE/EXPORT +-- 관리 도메인(결재/공지/출금): READ/CREATE/UPDATE/APPROVE +-- 조회성(첨부): READ/CREATE/DELETE +-- ============================================================ + +-- 수수료 + 설계사 라이프사이클 + 계약변경/민원: READ/CREATE/UPDATE/DELETE/EXPORT +INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order) +SELECT m.menu_id, p.code, p.name, p.sort +FROM menu m +CROSS JOIN (VALUES + ('READ', '조회', 10), + ('CREATE', '등록', 20), + ('UPDATE', '수정', 30), + ('DELETE', '삭제', 40), + ('EXPORT', '엑셀', 60) +) AS p(code, name, sort) +WHERE m.menu_code IN ( + 'COMM_TYPE','COMM_MASTER','COLLECT_COMM','RENEWAL_COMM', + 'REINSTATE_COMM','PERSIST_BONUS','OVERRIDE_LEDGER', + 'AGENT_CONTRACT','AGENT_LICENSE','AGENT_TRAINING', + 'ENDORSE','COMPLAINT' +) +ON CONFLICT (menu_id, perm_code) DO NOTHING; + +-- 결재/공지/출금: READ/CREATE/UPDATE/APPROVE +INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order) +SELECT m.menu_id, p.code, p.name, p.sort +FROM menu m +CROSS JOIN (VALUES + ('READ', '조회', 10), + ('CREATE', '등록', 20), + ('UPDATE', '수정', 30), + ('APPROVE', '결재', 50) +) AS p(code, name, sort) +WHERE m.menu_code IN ('APPROVAL','NOTICE','WITHDRAW') +ON CONFLICT (menu_id, perm_code) DO NOTHING; + +-- 첨부파일: READ/CREATE/DELETE +INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order) +SELECT m.menu_id, p.code, p.name, p.sort +FROM menu m +CROSS JOIN (VALUES + ('READ', '조회', 10), + ('CREATE', '등록', 20), + ('DELETE', '삭제', 40) +) AS p(code, name, sort) +WHERE m.menu_code = 'ATTACHMENT' +ON CONFLICT (menu_id, perm_code) DO NOTHING; + +-- ============================================================ +-- 8. 역할별 권한 부여 +-- SUPER_ADMIN / ADMIN: 전체 부여 +-- MANAGER: READ + CREATE + UPDATE + APPROVE (DELETE 제외) +-- AGENT: 본인 관련 READ 만 (COMM_MASTER/AGENT_CONTRACT/AGENT_LICENSE/AGENT_TRAINING/NOTICE/WITHDRAW) +-- ============================================================ + +-- SUPER_ADMIN / ADMIN — 모든 신규 메뉴 전체 권한 +INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted) +SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y' +FROM role r +CROSS JOIN menu_permission mp +JOIN menu m ON m.menu_id = mp.menu_id +WHERE r.role_code IN ('SUPER_ADMIN','ADMIN') + AND m.menu_code IN ( + 'COMM_TYPE','COMM_MASTER','COLLECT_COMM','RENEWAL_COMM', + 'REINSTATE_COMM','PERSIST_BONUS','OVERRIDE_LEDGER', + 'AGENT_CONTRACT','AGENT_LICENSE','AGENT_TRAINING', + 'ENDORSE','COMPLAINT', + 'APPROVAL','NOTICE','ATTACHMENT','WITHDRAW' + ) +ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING; + +-- MANAGER — READ/CREATE/UPDATE/APPROVE (DELETE 제외) +INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted) +SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y' +FROM role r +CROSS JOIN menu_permission mp +JOIN menu m ON m.menu_id = mp.menu_id +WHERE r.role_code = 'MANAGER' + AND mp.perm_code IN ('READ','CREATE','UPDATE','APPROVE','EXPORT') + AND m.menu_code IN ( + 'COMM_TYPE','COMM_MASTER','COLLECT_COMM','RENEWAL_COMM', + 'REINSTATE_COMM','PERSIST_BONUS','OVERRIDE_LEDGER', + 'AGENT_CONTRACT','AGENT_LICENSE','AGENT_TRAINING', + 'ENDORSE','COMPLAINT', + 'APPROVAL','NOTICE','ATTACHMENT','WITHDRAW' + ) +ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING; + +-- AGENT — 본인 관련 메뉴 READ 만 +INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted) +SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y' +FROM role r +CROSS JOIN menu_permission mp +JOIN menu m ON m.menu_id = mp.menu_id +WHERE r.role_code = 'AGENT' + AND mp.perm_code = 'READ' + AND m.menu_code IN ( + 'COMM_MASTER','AGENT_CONTRACT','AGENT_LICENSE','AGENT_TRAINING', + 'NOTICE','WITHDRAW' + ) +ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING; diff --git a/ga-common/src/main/resources/db/migration/V55__P4_공통코드.sql b/ga-common/src/main/resources/db/migration/V55__P4_공통코드.sql new file mode 100644 index 0000000..44ea30a --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V55__P4_공통코드.sql @@ -0,0 +1,128 @@ +-- V55: P4 도메인에서 사용하는 공통코드 그룹/상세 일괄 INSERT +-- core-architect 가 사용한 코드 그룹 누락분 보강. + +-- group_code 길이 한도 상향 (기존 VARCHAR(20) → 30) — NOTIFICATION_REF_TYPE / AGENT_CONTRACT_STATUS 등 21자 이상 수용 +ALTER TABLE common_code_group ALTER COLUMN group_code TYPE VARCHAR(30); +ALTER TABLE common_code ALTER COLUMN group_code TYPE VARCHAR(30); + +-- ===== 그룹 ===== +INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES + ('PAYMENT_CYCLE', '수금주기', 'MONTHLY/QUARTERLY/SEMI/ANNUAL', 'Y', 200), + ('BONUS_TYPE', '수당지급유형', 'RATE/FIXED', 'Y', 210), + ('RETENTION_MONTH', '유지기준개월', 'M13/M25/M37/M49', 'Y', 220), + ('AGENT_CONTRACT_STATUS','위촉계약상태', 'ACTIVE/TERMINATED', 'Y', 230), + ('LICENSE_TYPE', '자격증유형', 'LIFE/NONLIFE/VARIABLE', 'Y', 240), + ('LICENSE_STATUS', '자격증상태', 'ACTIVE/EXPIRED/SUSPENDED', 'Y', 250), + ('COURSE_TYPE', '교육유형', 'REGULAR/COMPLIANCE/PRODUCT', 'Y', 260), + ('ENDORSEMENT_TYPE', '계약변경유형', 'PREMIUM/BENEFICIARY/INSURED/ADDRESS', 'Y', 270), + ('COMPLAINT_CHANNEL', '민원접수채널', 'PHONE/EMAIL/VISIT/ONLINE', 'Y', 280), + ('COMPLAINT_TYPE', '민원유형', 'MIS_SELLING/DELAY/COVERAGE/REFUND/ETC', 'Y', 290), + ('COMPLAINT_SEVERITY', '민원심각도', 'LOW/MID/HIGH', 'Y', 300), + ('COMPLAINT_STATUS', '민원처리상태', 'OPEN/IN_PROGRESS/CLOSED', 'Y', 310), + ('APPROVAL_STATUS', '결재상태', 'PENDING/APPROVED/REJECTED/CANCEL', 'Y', 320), + ('APPROVAL_ACTION', '결재처리', 'APPROVE/REJECT', 'Y', 330), + ('NOTICE_TYPE', '공지유형', 'SYSTEM/POLICY/EVENT', 'Y', 340), + ('NOTIFICATION_REF_TYPE','알림참조유형', 'SETTLE/DISPUTE/INCENTIVE/WITHDRAW/...', 'Y', 350), + ('ATTACHMENT_REF_TYPE', '첨부참조유형', 'AGENT_CONTRACT/COMPLAINT/...', 'Y', 360), + ('WITHDRAW_STATUS', '출금신청상태', 'REQUESTED/APPROVED/SENT/COMPLETED/FAILED','Y', 370), + ('HOLD_REASON', '보류사유', 'NEED_DOC/UNDER_REVIEW/DISPUTE_OPEN/...','Y', 380) +ON CONFLICT (group_code) DO NOTHING; + +-- ===== 상세 ===== +INSERT INTO common_code (group_code, code, code_name, sort_order) VALUES + -- PAYMENT_CYCLE + ('PAYMENT_CYCLE','MONTHLY','월납',10), + ('PAYMENT_CYCLE','QUARTERLY','분기납',20), + ('PAYMENT_CYCLE','SEMI','반기납',30), + ('PAYMENT_CYCLE','ANNUAL','연납',40), + -- BONUS_TYPE + ('BONUS_TYPE','RATE','정률',10), + ('BONUS_TYPE','FIXED','정액',20), + -- RETENTION_MONTH + ('RETENTION_MONTH','M13','13개월',10), + ('RETENTION_MONTH','M25','25개월',20), + ('RETENTION_MONTH','M37','37개월',30), + ('RETENTION_MONTH','M49','49개월',40), + -- AGENT_CONTRACT_STATUS + ('AGENT_CONTRACT_STATUS','ACTIVE','유효',10), + ('AGENT_CONTRACT_STATUS','TERMINATED','해촉',20), + -- LICENSE_TYPE + ('LICENSE_TYPE','LIFE','생명보험',10), + ('LICENSE_TYPE','NONLIFE','손해보험',20), + ('LICENSE_TYPE','VARIABLE','변액보험',30), + -- LICENSE_STATUS + ('LICENSE_STATUS','ACTIVE','유효',10), + ('LICENSE_STATUS','EXPIRED','만료',20), + ('LICENSE_STATUS','SUSPENDED','정지',30), + -- COURSE_TYPE + ('COURSE_TYPE','REGULAR','보수교육',10), + ('COURSE_TYPE','COMPLIANCE','준법교육',20), + ('COURSE_TYPE','PRODUCT','상품교육',30), + -- ENDORSEMENT_TYPE + ('ENDORSEMENT_TYPE','PREMIUM_CHANGE','보험료변경',10), + ('ENDORSEMENT_TYPE','BENEFICIARY','수익자변경',20), + ('ENDORSEMENT_TYPE','INSURED','피보험자변경',30), + ('ENDORSEMENT_TYPE','ADDRESS','주소변경',40), + -- COMPLAINT_CHANNEL + ('COMPLAINT_CHANNEL','PHONE','전화',10), + ('COMPLAINT_CHANNEL','EMAIL','이메일',20), + ('COMPLAINT_CHANNEL','VISIT','방문',30), + ('COMPLAINT_CHANNEL','ONLINE','온라인',40), + -- COMPLAINT_TYPE + ('COMPLAINT_TYPE','MIS_SELLING','불완전판매',10), + ('COMPLAINT_TYPE','DELAY','업무지연',20), + ('COMPLAINT_TYPE','COVERAGE','보장설명미흡',30), + ('COMPLAINT_TYPE','REFUND','환급요청',40), + ('COMPLAINT_TYPE','ETC','기타',90), + -- COMPLAINT_SEVERITY + ('COMPLAINT_SEVERITY','LOW','경미',10), + ('COMPLAINT_SEVERITY','MID','보통',20), + ('COMPLAINT_SEVERITY','HIGH','중대',30), + -- COMPLAINT_STATUS + ('COMPLAINT_STATUS','OPEN','접수',10), + ('COMPLAINT_STATUS','IN_PROGRESS','처리중',20), + ('COMPLAINT_STATUS','CLOSED','처리완료',30), + -- APPROVAL_STATUS + ('APPROVAL_STATUS','PENDING','결재대기',10), + ('APPROVAL_STATUS','APPROVED','승인',20), + ('APPROVAL_STATUS','REJECTED','반려',30), + ('APPROVAL_STATUS','CANCEL','취소',40), + -- APPROVAL_ACTION + ('APPROVAL_ACTION','APPROVE','승인',10), + ('APPROVAL_ACTION','REJECT','반려',20), + -- NOTICE_TYPE + ('NOTICE_TYPE','SYSTEM','시스템',10), + ('NOTICE_TYPE','POLICY','정책',20), + ('NOTICE_TYPE','EVENT','이벤트',30), + -- NOTIFICATION_REF_TYPE + ('NOTIFICATION_REF_TYPE','SETTLE','정산',10), + ('NOTIFICATION_REF_TYPE','DISPUTE','이의신청',20), + ('NOTIFICATION_REF_TYPE','INCENTIVE','시책',30), + ('NOTIFICATION_REF_TYPE','WITHDRAW','출금',40), + ('NOTIFICATION_REF_TYPE','COMPLAINT','민원',50), + ('NOTIFICATION_REF_TYPE','APPROVAL','결재',60), + ('NOTIFICATION_REF_TYPE','SYSTEM','시스템',70), + -- ATTACHMENT_REF_TYPE + ('ATTACHMENT_REF_TYPE','AGENT_CONTRACT','위촉계약서',10), + ('ATTACHMENT_REF_TYPE','COMPLAINT','민원',20), + ('ATTACHMENT_REF_TYPE','DISPUTE','이의신청',30), + ('ATTACHMENT_REF_TYPE','SETTLE','정산',40), + ('ATTACHMENT_REF_TYPE','TRAINING','교육',50), + ('ATTACHMENT_REF_TYPE','LICENSE','자격증',60), + ('ATTACHMENT_REF_TYPE','ENDORSEMENT','계약변경',70), + ('ATTACHMENT_REF_TYPE','NOTICE','공지',80), + ('ATTACHMENT_REF_TYPE','ETC','기타',90), + -- WITHDRAW_STATUS + ('WITHDRAW_STATUS','REQUESTED','신청',10), + ('WITHDRAW_STATUS','APPROVED','승인',20), + ('WITHDRAW_STATUS','SENT','전송완료',30), + ('WITHDRAW_STATUS','COMPLETED','이체완료',40), + ('WITHDRAW_STATUS','FAILED','실패',90), + -- HOLD_REASON + ('HOLD_REASON','NEED_DOC','서류미비',10), + ('HOLD_REASON','UNDER_REVIEW','검토중',20), + ('HOLD_REASON','DISPUTE_OPEN','이의신청중',30), + ('HOLD_REASON','LICENSE_EXPIRED','자격증만료',40), + ('HOLD_REASON','TRAINING_INSUFFICIENT','교육시간미달',50), + ('HOLD_REASON','ETC','기타',90) +ON CONFLICT (group_code, code) DO NOTHING; diff --git a/ga-common/src/main/resources/db/migration/V56__agent_세무컬럼_보강.sql b/ga-common/src/main/resources/db/migration/V56__agent_세무컬럼_보강.sql new file mode 100644 index 0000000..c217178 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V56__agent_세무컬럼_보강.sql @@ -0,0 +1,27 @@ +-- V56: agent 테이블 세무 컬럼 누락 보강 +-- 운영 DB 의 V23 SQL 과 현재 V23 SQL 의 체크섬 미스매치로 인해 +-- agent.tax_type / business_no / vat_type 가 적용되지 않은 상태. +-- AgentMapper.xml 이 위 3개 컬럼을 참조하므로 /api/agents 가 500 에러. +-- IF NOT EXISTS 로 idempotent 추가. + +ALTER TABLE agent ADD COLUMN IF NOT EXISTS tax_type VARCHAR(20); +ALTER TABLE agent ADD COLUMN IF NOT EXISTS business_no VARCHAR(20); +ALTER TABLE agent ADD COLUMN IF NOT EXISTS vat_type VARCHAR(20); + +COMMENT ON COLUMN agent.tax_type IS '세무유형 (사업소득/기타소득 등 TAX_TYPE 코드)'; +COMMENT ON COLUMN agent.business_no IS '사업자등록번호 (사업소득자)'; +COMMENT ON COLUMN agent.vat_type IS '부가세유형 (VAT_TYPE 코드)'; + +-- TAX_TYPE / VAT_TYPE common_code 그룹이 없으면 추가 +INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES + ('TAX_TYPE', '세무유형', 'BUSINESS/OTHER 등', 'Y', 400), + ('VAT_TYPE', '부가세유형', 'GENERAL/SIMPLE/EXEMPT', 'Y', 410) +ON CONFLICT (group_code) DO NOTHING; + +INSERT INTO common_code (group_code, code, code_name, sort_order) VALUES + ('TAX_TYPE','BUSINESS','사업소득',10), + ('TAX_TYPE','OTHER','기타소득',20), + ('VAT_TYPE','GENERAL','일반과세',10), + ('VAT_TYPE','SIMPLE','간이과세',20), + ('VAT_TYPE','EXEMPT','면세',30) +ON CONFLICT (group_code, code) DO NOTHING; diff --git a/ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalHistoryMapper.java b/ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalHistoryMapper.java new file mode 100644 index 0000000..be389e7 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalHistoryMapper.java @@ -0,0 +1,16 @@ +package com.ga.core.mapper.approval; + +import com.ga.core.vo.approval.ApprovalHistoryResp; +import com.ga.core.vo.approval.ApprovalHistorySearchParam; +import com.ga.core.vo.approval.ApprovalHistoryVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ApprovalHistoryMapper { + List selectByRequest(@Param("requestId") Long requestId); + List selectList(ApprovalHistorySearchParam param); + int insert(ApprovalHistoryVO vo); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalLineMapper.java b/ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalLineMapper.java new file mode 100644 index 0000000..82feeab --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalLineMapper.java @@ -0,0 +1,19 @@ +package com.ga.core.mapper.approval; + +import com.ga.core.vo.approval.ApprovalLineResp; +import com.ga.core.vo.approval.ApprovalLineSearchParam; +import com.ga.core.vo.approval.ApprovalLineVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ApprovalLineMapper { + ApprovalLineResp selectById(@Param("lineId") Long lineId); + ApprovalLineVO selectByCode(@Param("lineCode") String lineCode); + List selectList(ApprovalLineSearchParam param); + int insert(ApprovalLineVO vo); + int update(ApprovalLineVO vo); + int updateActive(@Param("lineId") Long lineId, @Param("isActive") boolean isActive); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalLineStepMapper.java b/ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalLineStepMapper.java new file mode 100644 index 0000000..bc1ffc7 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalLineStepMapper.java @@ -0,0 +1,18 @@ +package com.ga.core.mapper.approval; + +import com.ga.core.vo.approval.ApprovalLineStepResp; +import com.ga.core.vo.approval.ApprovalLineStepSearchParam; +import com.ga.core.vo.approval.ApprovalLineStepVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ApprovalLineStepMapper { + ApprovalLineStepVO selectByLineAndStep(@Param("lineId") Long lineId, @Param("stepNo") int stepNo); + List selectByLine(@Param("lineId") Long lineId); + List selectList(ApprovalLineStepSearchParam param); + int insert(ApprovalLineStepVO vo); + int delete(@Param("stepId") Long stepId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalRequestMapper.java b/ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalRequestMapper.java new file mode 100644 index 0000000..dafc15e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/approval/ApprovalRequestMapper.java @@ -0,0 +1,42 @@ +package com.ga.core.mapper.approval; + +import com.ga.core.vo.approval.ApprovalRequestResp; +import com.ga.core.vo.approval.ApprovalRequestSearchParam; +import com.ga.core.vo.approval.ApprovalRequestVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * approval_request Mapper — 결재 요청. + */ +@Mapper +public interface ApprovalRequestMapper { + + ApprovalRequestResp selectById(@Param("requestId") Long requestId); + + List selectList(ApprovalRequestSearchParam param); + + int insert(ApprovalRequestVO vo); + + /** + * 결재 단계 진행 — approval_history 삽입 + current_step 갱신 + 최종 상태 반영. + * Service에서 트랜잭션으로 묶는 것이 원칙이나, + * 핵심 단계 진행 UPDATE를 단일 메서드로 제공. + * + * @param requestId 결재 요청 PK + * @param approverId 결재자 user_id + * @param action APPROVE / REJECT / RETURN + * @param comment 결재 코멘트 + * @return 업데이트 행 수 + */ + int advanceStep(@Param("requestId") Long requestId, + @Param("approverId") Long approverId, + @Param("action") String action, + @Param("comment") String comment); + + int updateStatus(@Param("requestId") Long requestId, @Param("status") String status); + + int cancel(@Param("requestId") Long requestId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/attachment/AttachmentMapper.java b/ga-core/src/main/java/com/ga/core/mapper/attachment/AttachmentMapper.java new file mode 100644 index 0000000..83837c5 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/attachment/AttachmentMapper.java @@ -0,0 +1,21 @@ +package com.ga.core.mapper.attachment; + +import com.ga.core.vo.attachment.AttachmentResp; +import com.ga.core.vo.attachment.AttachmentSearchParam; +import com.ga.core.vo.attachment.AttachmentVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface AttachmentMapper { + AttachmentResp selectById(@Param("attachmentId") Long attachmentId); + /** ref_type + ref_id 기준 첨부 목록 — 도메인별 첨부 조회 핵심 메서드 */ + List selectByRef(@Param("refType") String refType, @Param("refId") Long refId); + List selectList(AttachmentSearchParam param); + int insert(AttachmentVO vo); + int delete(@Param("attachmentId") Long attachmentId); + /** ref_type + ref_id 전체 삭제 — 레코드 삭제 시 연동 */ + int deleteByRef(@Param("refType") String refType, @Param("refId") Long refId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/CollectionCommissionLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/CollectionCommissionLedgerMapper.java new file mode 100644 index 0000000..0181d0e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/CollectionCommissionLedgerMapper.java @@ -0,0 +1,31 @@ +package com.ga.core.mapper.commission; + +import com.ga.core.vo.commission.CollectionCommissionLedgerResp; +import com.ga.core.vo.commission.CollectionCommissionLedgerSearchParam; +import com.ga.core.vo.commission.CollectionCommissionLedgerVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +/** + * collection_commission_ledger Mapper. + */ +@Mapper +public interface CollectionCommissionLedgerMapper { + + CollectionCommissionLedgerResp selectById(@Param("ledgerId") Long ledgerId); + + List selectList(CollectionCommissionLedgerSearchParam param); + + /** 설계사 + 정산월 합계 — commission_master 집계 배치 용도 */ + BigDecimal sumByAgentAndMonth(@Param("agentId") Long agentId, + @Param("settleMonth") String settleMonth); + + int insert(CollectionCommissionLedgerVO vo); + + int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status); + + int delete(@Param("ledgerId") Long ledgerId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/CollectionCommissionRuleMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/CollectionCommissionRuleMapper.java new file mode 100644 index 0000000..f1966f4 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/CollectionCommissionRuleMapper.java @@ -0,0 +1,35 @@ +package com.ga.core.mapper.commission; + +import com.ga.core.vo.commission.CollectionCommissionRuleResp; +import com.ga.core.vo.commission.CollectionCommissionRuleSearchParam; +import com.ga.core.vo.commission.CollectionCommissionRuleVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDate; +import java.util.List; + +/** + * collection_commission_rule Mapper. + */ +@Mapper +public interface CollectionCommissionRuleMapper { + + CollectionCommissionRuleResp selectById(@Param("ruleId") Long ruleId); + + List selectList(CollectionCommissionRuleSearchParam param); + + /** + * 상품 + 납입주기 + 기준일 기준 유효 룰 1건 조회. + * 수금수수료 계산 배치에서 호출. + */ + CollectionCommissionRuleVO selectActiveRule(@Param("productId") Long productId, + @Param("paymentCycle") String paymentCycle, + @Param("baseDate") LocalDate baseDate); + + int insert(CollectionCommissionRuleVO vo); + + int update(CollectionCommissionRuleVO vo); + + int delete(@Param("ruleId") Long ruleId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/CommissionMasterMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/CommissionMasterMapper.java new file mode 100644 index 0000000..654a746 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/CommissionMasterMapper.java @@ -0,0 +1,45 @@ +package com.ga.core.mapper.commission; + +import com.ga.core.vo.commission.CommissionMasterResp; +import com.ga.core.vo.commission.CommissionMasterSearchParam; +import com.ga.core.vo.commission.CommissionMasterVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * commission_master Mapper — 전 수수료 종류 통합 허브 테이블. + */ +@Mapper +public interface CommissionMasterMapper { + + CommissionMasterResp selectById(@Param("masterId") Long masterId); + + List selectList(CommissionMasterSearchParam param); + + /** + * 특정 설계사의 특정 정산월 전체 수수료 목록 조회. + * 배치 완료 후 명세서 생성 시 호출. + */ + List selectByAgentAndMonth(@Param("agentId") Long agentId, + @Param("settleMonth") String settleMonth); + + /** + * 정산월 전체 수수료 집계 — 수수료 종류별 합계. + * 배치 집계 또는 대시보드 요약 용도. + */ + List selectAggregatedByMonth(@Param("settleMonth") String settleMonth); + + int insert(CommissionMasterVO vo); + + /** + * 원본 원장 기준 UPSERT — 배치에서 원장 처리 후 commission_master 반영 시 호출. + * (source_table + source_id + commission_type + agent_id UNIQUE 제약 기반) + */ + int upsertFromLedger(CommissionMasterVO vo); + + int updateStatus(@Param("masterId") Long masterId, @Param("status") String status); + + int delete(@Param("masterId") Long masterId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/OverrideLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/OverrideLedgerMapper.java new file mode 100644 index 0000000..b706f1c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/OverrideLedgerMapper.java @@ -0,0 +1,21 @@ +package com.ga.core.mapper.commission; + +import com.ga.core.vo.commission.OverrideLedgerResp; +import com.ga.core.vo.commission.OverrideLedgerSearchParam; +import com.ga.core.vo.commission.OverrideLedgerVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * override_ledger Mapper — 오버라이드 상세 원장 (V44). + */ +@Mapper +public interface OverrideLedgerMapper { + OverrideLedgerResp selectById(@Param("ledgerId") Long ledgerId); + List selectList(OverrideLedgerSearchParam param); + int insert(OverrideLedgerVO vo); + int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status); + int delete(@Param("ledgerId") Long ledgerId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/OverrideSummaryMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/OverrideSummaryMapper.java new file mode 100644 index 0000000..a1b4ffc --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/OverrideSummaryMapper.java @@ -0,0 +1,29 @@ +package com.ga.core.mapper.commission; + +import com.ga.core.vo.commission.OverrideSummaryResp; +import com.ga.core.vo.commission.OverrideSummarySearchParam; +import com.ga.core.vo.commission.OverrideSummaryVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * override_summary Mapper — agent+settle_month 단위 오버라이드 합계 (V44). + * UPSERT(agent_id, settle_month) 메서드 핵심. + */ +@Mapper +public interface OverrideSummaryMapper { + OverrideSummaryResp selectById(@Param("summaryId") Long summaryId); + OverrideSummaryResp selectByAgentAndMonth(@Param("toAgentId") Long toAgentId, + @Param("settleMonth") String settleMonth); + List selectList(OverrideSummarySearchParam param); + + /** + * UPSERT — UNIQUE(to_agent_id, settle_month). + * 배치 완료 후 집계 결과 반영 시 호출. + */ + int upsert(OverrideSummaryVO vo); + + int updateStatus(@Param("summaryId") Long summaryId, @Param("status") String status); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/PersistencyBonusMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/PersistencyBonusMapper.java new file mode 100644 index 0000000..22fedcb --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/PersistencyBonusMapper.java @@ -0,0 +1,33 @@ +package com.ga.core.mapper.commission; + +import com.ga.core.vo.commission.PersistencyBonusResp; +import com.ga.core.vo.commission.PersistencyBonusSearchParam; +import com.ga.core.vo.commission.PersistencyBonusVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * persistency_bonus Mapper. + * UNIQUE(agent_id, base_month, retention_month) 이므로 upsert 메서드 제공. + */ +@Mapper +public interface PersistencyBonusMapper { + + PersistencyBonusResp selectById(@Param("bonusId") Long bonusId); + + List selectList(PersistencyBonusSearchParam param); + + /** + * UPSERT — 동일 (agent_id, base_month, retention_month) 존재 시 금액/상태 갱신. + * 배치 유지율 평가 후 호출. + */ + int upsert(PersistencyBonusVO vo); + + int insert(PersistencyBonusVO vo); + + int updateStatus(@Param("bonusId") Long bonusId, @Param("status") String status); + + int delete(@Param("bonusId") Long bonusId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/PersistencyBonusRuleMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/PersistencyBonusRuleMapper.java new file mode 100644 index 0000000..8248c04 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/PersistencyBonusRuleMapper.java @@ -0,0 +1,22 @@ +package com.ga.core.mapper.commission; + +import com.ga.core.vo.commission.PersistencyBonusRuleResp; +import com.ga.core.vo.commission.PersistencyBonusRuleSearchParam; +import com.ga.core.vo.commission.PersistencyBonusRuleVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDate; +import java.util.List; + +@Mapper +public interface PersistencyBonusRuleMapper { + PersistencyBonusRuleResp selectById(@Param("ruleId") Long ruleId); + List selectList(PersistencyBonusRuleSearchParam param); + PersistencyBonusRuleVO selectActiveRule(@Param("gradeId") Long gradeId, + @Param("retentionMonth") int retentionMonth, + @Param("baseDate") LocalDate baseDate); + int insert(PersistencyBonusRuleVO vo); + int update(PersistencyBonusRuleVO vo); + int delete(@Param("ruleId") Long ruleId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/ReinstatementCommissionLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/ReinstatementCommissionLedgerMapper.java new file mode 100644 index 0000000..d5048b9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/ReinstatementCommissionLedgerMapper.java @@ -0,0 +1,21 @@ +package com.ga.core.mapper.commission; + +import com.ga.core.vo.commission.ReinstatementCommissionLedgerResp; +import com.ga.core.vo.commission.ReinstatementCommissionLedgerSearchParam; +import com.ga.core.vo.commission.ReinstatementCommissionLedgerVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +@Mapper +public interface ReinstatementCommissionLedgerMapper { + ReinstatementCommissionLedgerResp selectById(@Param("ledgerId") Long ledgerId); + List selectList(ReinstatementCommissionLedgerSearchParam param); + BigDecimal sumByAgentAndMonth(@Param("agentId") Long agentId, + @Param("settleMonth") String settleMonth); + int insert(ReinstatementCommissionLedgerVO vo); + int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status); + int delete(@Param("ledgerId") Long ledgerId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/ReinstatementCommissionRuleMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/ReinstatementCommissionRuleMapper.java new file mode 100644 index 0000000..6597362 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/ReinstatementCommissionRuleMapper.java @@ -0,0 +1,21 @@ +package com.ga.core.mapper.commission; + +import com.ga.core.vo.commission.ReinstatementCommissionRuleResp; +import com.ga.core.vo.commission.ReinstatementCommissionRuleSearchParam; +import com.ga.core.vo.commission.ReinstatementCommissionRuleVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDate; +import java.util.List; + +@Mapper +public interface ReinstatementCommissionRuleMapper { + ReinstatementCommissionRuleResp selectById(@Param("ruleId") Long ruleId); + List selectList(ReinstatementCommissionRuleSearchParam param); + ReinstatementCommissionRuleVO selectActiveRule(@Param("productId") Long productId, + @Param("baseDate") LocalDate baseDate); + int insert(ReinstatementCommissionRuleVO vo); + int update(ReinstatementCommissionRuleVO vo); + int delete(@Param("ruleId") Long ruleId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/RenewalCommissionLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/RenewalCommissionLedgerMapper.java new file mode 100644 index 0000000..9210c7c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/RenewalCommissionLedgerMapper.java @@ -0,0 +1,21 @@ +package com.ga.core.mapper.commission; + +import com.ga.core.vo.commission.RenewalCommissionLedgerResp; +import com.ga.core.vo.commission.RenewalCommissionLedgerSearchParam; +import com.ga.core.vo.commission.RenewalCommissionLedgerVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +@Mapper +public interface RenewalCommissionLedgerMapper { + RenewalCommissionLedgerResp selectById(@Param("ledgerId") Long ledgerId); + List selectList(RenewalCommissionLedgerSearchParam param); + BigDecimal sumByAgentAndMonth(@Param("agentId") Long agentId, + @Param("settleMonth") String settleMonth); + int insert(RenewalCommissionLedgerVO vo); + int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status); + int delete(@Param("ledgerId") Long ledgerId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/RenewalCommissionRuleMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/RenewalCommissionRuleMapper.java new file mode 100644 index 0000000..7c156ac --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/RenewalCommissionRuleMapper.java @@ -0,0 +1,21 @@ +package com.ga.core.mapper.commission; + +import com.ga.core.vo.commission.RenewalCommissionRuleResp; +import com.ga.core.vo.commission.RenewalCommissionRuleSearchParam; +import com.ga.core.vo.commission.RenewalCommissionRuleVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDate; +import java.util.List; + +@Mapper +public interface RenewalCommissionRuleMapper { + RenewalCommissionRuleResp selectById(@Param("ruleId") Long ruleId); + List selectList(RenewalCommissionRuleSearchParam param); + RenewalCommissionRuleVO selectActiveRule(@Param("productId") Long productId, + @Param("baseDate") LocalDate baseDate); + int insert(RenewalCommissionRuleVO vo); + int update(RenewalCommissionRuleVO vo); + int delete(@Param("ruleId") Long ruleId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/complaint/ComplaintMapper.java b/ga-core/src/main/java/com/ga/core/mapper/complaint/ComplaintMapper.java new file mode 100644 index 0000000..8fa14dc --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/complaint/ComplaintMapper.java @@ -0,0 +1,22 @@ +package com.ga.core.mapper.complaint; + +import com.ga.core.vo.complaint.ComplaintResp; +import com.ga.core.vo.complaint.ComplaintSearchParam; +import com.ga.core.vo.complaint.ComplaintVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ComplaintMapper { + ComplaintResp selectById(@Param("complaintId") Long complaintId); + List selectList(ComplaintSearchParam param); + /** 환수 트리거 미처리 민원 (chargebackTriggered=true, status != CLOSED) */ + List selectPendingChargeback(); + int insert(ComplaintVO vo); + int update(ComplaintVO vo); + int updateStatus(@Param("complaintId") Long complaintId, @Param("status") String status, + @Param("resolution") String resolution); + int delete(@Param("complaintId") Long complaintId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/contract/AgentContractMapper.java b/ga-core/src/main/java/com/ga/core/mapper/contract/AgentContractMapper.java new file mode 100644 index 0000000..3208883 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/contract/AgentContractMapper.java @@ -0,0 +1,20 @@ +package com.ga.core.mapper.contract; + +import com.ga.core.vo.contract.AgentContractResp; +import com.ga.core.vo.contract.AgentContractSearchParam; +import com.ga.core.vo.contract.AgentContractVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface AgentContractMapper { + AgentContractResp selectById(@Param("contractId") Long contractId); + List selectList(AgentContractSearchParam param); + int insert(AgentContractVO vo); + int update(AgentContractVO vo); + int updateStatus(@Param("contractId") Long contractId, @Param("status") String status, + @Param("terminatedReason") String terminatedReason); + int delete(@Param("contractId") Long contractId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/endorse/ContractEndorsementMapper.java b/ga-core/src/main/java/com/ga/core/mapper/endorse/ContractEndorsementMapper.java new file mode 100644 index 0000000..59bfa86 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/endorse/ContractEndorsementMapper.java @@ -0,0 +1,20 @@ +package com.ga.core.mapper.endorse; + +import com.ga.core.vo.endorse.ContractEndorsementResp; +import com.ga.core.vo.endorse.ContractEndorsementSearchParam; +import com.ga.core.vo.endorse.ContractEndorsementVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface ContractEndorsementMapper { + ContractEndorsementResp selectById(@Param("endorsementId") Long endorsementId); + List selectList(ContractEndorsementSearchParam param); + /** 재계산 미처리 건 목록 — 배치 수수료 재계산 대상 조회 */ + List selectPendingRecalc(); + int insert(ContractEndorsementVO vo); + int markProcessed(@Param("endorsementId") Long endorsementId); + int delete(@Param("endorsementId") Long endorsementId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/installment/InstallmentPlanMapper.java b/ga-core/src/main/java/com/ga/core/mapper/installment/InstallmentPlanMapper.java index 0e0d1c7..9891354 100644 --- a/ga-core/src/main/java/com/ga/core/mapper/installment/InstallmentPlanMapper.java +++ b/ga-core/src/main/java/com/ga/core/mapper/installment/InstallmentPlanMapper.java @@ -31,4 +31,7 @@ public interface InstallmentPlanMapper { int update(InstallmentPlanVO vo); int updateStatus(@Param("planId") Long planId, @Param("status") String status); + + /** 계약 삭제 시 연관 분급 계획 일괄 삭제 */ + int deleteByContract(@Param("contractId") Long contractId); } diff --git a/ga-core/src/main/java/com/ga/core/mapper/kpi/KpiMapper.java b/ga-core/src/main/java/com/ga/core/mapper/kpi/KpiMapper.java new file mode 100644 index 0000000..52af18d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/kpi/KpiMapper.java @@ -0,0 +1,43 @@ +package com.ga.core.mapper.kpi; + +import com.ga.core.vo.kpi.KpiCumulativeResp; +import com.ga.core.vo.kpi.KpiMonthlySummaryResp; +import com.ga.core.vo.kpi.KpiOrgSummaryResp; +import com.ga.core.vo.kpi.KpiRetentionResp; +import com.ga.core.vo.kpi.KpiSearchParam; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * KPI 대시보드 Mapper — V37 Materialized View 4종 단일 인터페이스. + * 모든 MV는 읽기 전용 (INSERT/UPDATE 없음). + * REFRESH는 ga-batch가 야간 배치로 REFRESH MATERIALIZED VIEW CONCURRENTLY 호출. + */ +@Mapper +public interface KpiMapper { + + /** + * 월별 정산 요약 (mv_monthly_summary). + * 정산년월 범위 필터. yyyymmFrom/yyyymmTo 미지정 시 전체. + */ + List selectMonthlySummary(KpiSearchParam param); + + /** + * 조직별 월별 요약 (mv_org_summary). + * orgId 필터 가능, 정산년월 범위 필터 가능. + */ + List selectOrgSummary(KpiSearchParam param); + + /** + * 계약 유지율 (mv_retention). + * carrierId 필터 가능, contractMonth 범위 필터 가능. + */ + List selectRetention(KpiSearchParam param); + + /** + * 설계사/조직 누계 (mv_cumulative). + * levelType(AGENT/ORG), orgId, agentId 필터 가능. + */ + List selectCumulative(KpiSearchParam param); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/license/AgentLicenseMapper.java b/ga-core/src/main/java/com/ga/core/mapper/license/AgentLicenseMapper.java new file mode 100644 index 0000000..87a3b70 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/license/AgentLicenseMapper.java @@ -0,0 +1,34 @@ +package com.ga.core.mapper.license; + +import com.ga.core.vo.license.AgentLicenseResp; +import com.ga.core.vo.license.AgentLicenseSearchParam; +import com.ga.core.vo.license.AgentLicenseVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * agent_license Mapper. + */ +@Mapper +public interface AgentLicenseMapper { + + AgentLicenseResp selectById(@Param("licenseId") Long licenseId); + + List selectList(AgentLicenseSearchParam param); + + /** + * 만료 임박 자격증 목록 조회 — 오늘부터 N일 이내 만료되는 ACTIVE 자격증. + * 만료 알림 스케줄러에서 호출. + */ + List selectExpiringWithin(@Param("days") int days); + + int insert(AgentLicenseVO vo); + + int update(AgentLicenseVO vo); + + int updateStatus(@Param("licenseId") Long licenseId, @Param("status") String status); + + int delete(@Param("licenseId") Long licenseId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/notice/NoticeMapper.java b/ga-core/src/main/java/com/ga/core/mapper/notice/NoticeMapper.java new file mode 100644 index 0000000..efbe1d9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/notice/NoticeMapper.java @@ -0,0 +1,21 @@ +package com.ga.core.mapper.notice; + +import com.ga.core.vo.notice.NoticeResp; +import com.ga.core.vo.notice.NoticeSearchParam; +import com.ga.core.vo.notice.NoticeVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface NoticeMapper { + NoticeResp selectById(@Param("noticeId") Long noticeId); + List selectList(NoticeSearchParam param); + /** 오늘 기준 유효한 공지 목록 (start_date <= today <= end_date OR end_date IS NULL) */ + List selectActive(); + int insert(NoticeVO vo); + int update(NoticeVO vo); + int updateActive(@Param("noticeId") Long noticeId, @Param("isActive") boolean isActive); + int delete(@Param("noticeId") Long noticeId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/notice/UserNotificationMapper.java b/ga-core/src/main/java/com/ga/core/mapper/notice/UserNotificationMapper.java new file mode 100644 index 0000000..2de1f4d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/notice/UserNotificationMapper.java @@ -0,0 +1,38 @@ +package com.ga.core.mapper.notice; + +import com.ga.core.vo.notice.UserNotificationResp; +import com.ga.core.vo.notice.UserNotificationSearchParam; +import com.ga.core.vo.notice.UserNotificationVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * user_notification Mapper. + */ +@Mapper +public interface UserNotificationMapper { + + UserNotificationResp selectById(@Param("notificationId") Long notificationId); + + List selectList(UserNotificationSearchParam param); + + /** + * 읽지 않은 알림 수 조회 — 상단 알림 벨 카운트 용도. + */ + int selectUnreadCount(@Param("userId") Long userId); + + /** + * 단건 읽음 처리. + * user_id 검증 포함 (타인 알림 접근 방지). + */ + int markRead(@Param("notificationId") Long notificationId, @Param("userId") Long userId); + + /** 사용자 전체 읽음 처리 */ + int markAllRead(@Param("userId") Long userId); + + int insert(UserNotificationVO vo); + + int delete(@Param("notificationId") Long notificationId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/settle/CarrierReconciliationOutMapper.java b/ga-core/src/main/java/com/ga/core/mapper/settle/CarrierReconciliationOutMapper.java new file mode 100644 index 0000000..ffe1fe4 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/settle/CarrierReconciliationOutMapper.java @@ -0,0 +1,48 @@ +package com.ga.core.mapper.settle; + +import com.ga.core.vo.settle.CarrierReconciliationOutResp; +import com.ga.core.vo.settle.CarrierReconciliationOutSearchParam; +import com.ga.core.vo.settle.CarrierReconciliationOutVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 보험사 회신 인터페이스 (아웃바운드) Mapper + * carrier_reconciliation_out 테이블 CRUD + 상태 관리. + */ +@Mapper +public interface CarrierReconciliationOutMapper { + + /** 전송 이력 목록 조회 (페이징) */ + List selectList(CarrierReconciliationOutSearchParam param); + + /** 전송 이력 단건 조회 (응답 조인 포함) */ + CarrierReconciliationOutResp selectById(@Param("reconOutId") Long reconOutId); + + /** 전송 이력 단건 조회 (VO, 배치 처리용) */ + CarrierReconciliationOutVO selectVoById(@Param("reconOutId") Long reconOutId); + + /** 보험사 + 정산월 기준 조회 (UNIQUE KEY) */ + CarrierReconciliationOutVO selectByCarrierMonth(@Param("carrierId") Long carrierId, + @Param("settleMonth") String settleMonth); + + /** 전송 이력 등록 */ + int insert(CarrierReconciliationOutVO vo); + + /** 상태 변경 (SENT/ACK/FAIL) */ + int updateStatus(@Param("reconOutId") Long reconOutId, + @Param("status") String status); + + /** 전송 완료 처리 (sentAt 기록) */ + int markSent(@Param("reconOutId") Long reconOutId); + + /** 수신 확인 처리 (ackAt + ackMessage 기록) */ + int markAck(@Param("reconOutId") Long reconOutId, + @Param("ackMessage") String ackMessage); + + /** 실패 처리 (failReason + retryCount 증가) */ + int markFail(@Param("reconOutId") Long reconOutId, + @Param("failReason") String failReason); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/settle/CommissionDisputeMapper.java b/ga-core/src/main/java/com/ga/core/mapper/settle/CommissionDisputeMapper.java new file mode 100644 index 0000000..cf687d4 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/settle/CommissionDisputeMapper.java @@ -0,0 +1,42 @@ +package com.ga.core.mapper.settle; + +import com.ga.core.vo.settle.CommissionDisputeResp; +import com.ga.core.vo.settle.CommissionDisputeSearchParam; +import com.ga.core.vo.settle.CommissionDisputeVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 수수료 이의신청 Mapper + * commission_dispute 테이블 CRUD + 상태 변경. + */ +@Mapper +public interface CommissionDisputeMapper { + + /** 이의신청 목록 조회 (페이징) */ + List selectList(CommissionDisputeSearchParam param); + + /** 이의신청 단건 조회 (응답 조인 포함) */ + CommissionDisputeResp selectById(@Param("disputeId") Long disputeId); + + /** 이의신청 단건 조회 (VO, 도메인 로직용) */ + CommissionDisputeVO selectVoById(@Param("disputeId") Long disputeId); + + /** 이의신청 등록 */ + int insert(CommissionDisputeVO vo); + + /** 검토 시작 (IN_REVIEW 상태 전환) */ + int startReview(@Param("disputeId") Long disputeId, + @Param("reviewerId") Long reviewerId); + + /** 이의신청 처리 결과 등록 (APPROVED/REJECTED) */ + int resolve(@Param("disputeId") Long disputeId, + @Param("status") String status, + @Param("resolution") String resolution, + @Param("adjustmentAmount") BigDecimal adjustmentAmount, + @Param("correctionId") Long correctionId, + @Param("resolvedBy") Long resolvedBy); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/settle/IncentivePaymentMapper.java b/ga-core/src/main/java/com/ga/core/mapper/settle/IncentivePaymentMapper.java new file mode 100644 index 0000000..120ebf4 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/settle/IncentivePaymentMapper.java @@ -0,0 +1,43 @@ +package com.ga.core.mapper.settle; + +import com.ga.core.vo.settle.IncentivePaymentResp; +import com.ga.core.vo.settle.IncentivePaymentSearchParam; +import com.ga.core.vo.settle.IncentivePaymentVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 시책 지급 이력 Mapper + * incentive_payment 테이블 CRUD + 상태 관리. + */ +@Mapper +public interface IncentivePaymentMapper { + + /** 시책 지급 목록 조회 (페이징) */ + List selectList(IncentivePaymentSearchParam param); + + /** 시책 지급 단건 조회 (응답 조인 포함) */ + IncentivePaymentResp selectById(@Param("incentivePayId") Long incentivePayId); + + /** 시책 지급 단건 조회 (VO, 상태 관리용) */ + IncentivePaymentVO selectVoById(@Param("incentivePayId") Long incentivePayId); + + /** 설계사의 특정 정산월 시책 지급 합계 (배치 집계용) */ + BigDecimal sumByAgentMonth(@Param("agentId") Long agentId, + @Param("settleMonth") String settleMonth); + + /** 시책 지급 등록 (ON CONFLICT: program_id + agent_id + settle_month) */ + int insert(IncentivePaymentVO vo); + + /** 상태 변경 (CONFIRMED/PAID/CANCELLED) */ + int updateStatus(@Param("incentivePayId") Long incentivePayId, + @Param("status") String status, + @Param("userId") Long userId); + + /** 정산 반영 (settle_id 연결) */ + int linkSettle(@Param("incentivePayId") Long incentivePayId, + @Param("settleId") Long settleId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/settle/IncentiveProgramMapper.java b/ga-core/src/main/java/com/ga/core/mapper/settle/IncentiveProgramMapper.java new file mode 100644 index 0000000..87e420f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/settle/IncentiveProgramMapper.java @@ -0,0 +1,43 @@ +package com.ga.core.mapper.settle; + +import com.ga.core.vo.settle.IncentiveProgramResp; +import com.ga.core.vo.settle.IncentiveProgramSearchParam; +import com.ga.core.vo.settle.IncentiveProgramVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 시책 프로그램 마스터 Mapper + * incentive_program 테이블 CRUD. + */ +@Mapper +public interface IncentiveProgramMapper { + + /** 시책 프로그램 목록 조회 (페이징) */ + List selectList(IncentiveProgramSearchParam param); + + /** 시책 단건 조회 (응답 조인 포함) */ + IncentiveProgramResp selectById(@Param("programId") Long programId); + + /** 시책 단건 조회 (VO, 배치 계산용) */ + IncentiveProgramVO selectVoById(@Param("programId") Long programId); + + /** + * 특정 정산월에 활성화된 시책 목록 조회. + * start_month <= settleMonth <= end_month AND is_active = TRUE. + */ + List selectActiveByMonth(@Param("settleMonth") String settleMonth); + + /** 시책 등록 */ + int insert(IncentiveProgramVO vo); + + /** 시책 수정 */ + int update(IncentiveProgramVO vo); + + /** 시책 활성/비활성 전환 */ + int updateActive(@Param("programId") Long programId, + @Param("isActive") Boolean isActive, + @Param("updatedBy") Long updatedBy); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/settle/PeriodCloseMapper.java b/ga-core/src/main/java/com/ga/core/mapper/settle/PeriodCloseMapper.java new file mode 100644 index 0000000..25ea04a --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/settle/PeriodCloseMapper.java @@ -0,0 +1,41 @@ +package com.ga.core.mapper.settle; + +import com.ga.core.vo.settle.PeriodCloseResp; +import com.ga.core.vo.settle.PeriodCloseSearchParam; +import com.ga.core.vo.settle.PeriodCloseVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 정산 마감 마스터 Mapper + * period_close 테이블 CRUD + 마감 상태 조회. + */ +@Mapper +public interface PeriodCloseMapper { + + /** 마감 이력 목록 조회 (페이징) */ + List selectList(PeriodCloseSearchParam param); + + /** 마감 단건 조회 (close_id) */ + PeriodCloseResp selectById(@Param("closeId") Long closeId); + + /** 마감 단건 조회 (close_month) */ + PeriodCloseVO selectByMonth(@Param("closeMonth") String closeMonth); + + /** + * 해당 월이 현재 마감 상태인지 확인. + * reopen_at IS NULL 인 레코드가 있으면 마감 중. + * @return 마감 중이면 true + */ + boolean isClosedMonth(@Param("closeMonth") String closeMonth); + + /** 마감 등록 */ + int insert(PeriodCloseVO vo); + + /** 재오픈 처리 (reopen_at, reopen_by, reopen_reason 업데이트) */ + int reopen(@Param("closeId") Long closeId, + @Param("reopenBy") Long reopenBy, + @Param("reopenReason") String reopenReason); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/settle/SettleCorrectionMapper.java b/ga-core/src/main/java/com/ga/core/mapper/settle/SettleCorrectionMapper.java new file mode 100644 index 0000000..546633e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/settle/SettleCorrectionMapper.java @@ -0,0 +1,42 @@ +package com.ga.core.mapper.settle; + +import com.ga.core.vo.settle.SettleCorrectionResp; +import com.ga.core.vo.settle.SettleCorrectionSearchParam; +import com.ga.core.vo.settle.SettleCorrectionVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 정산 정정 이력 Mapper + * settle_correction 테이블 CRUD + 상태 변경. + */ +@Mapper +public interface SettleCorrectionMapper { + + /** 정정 목록 조회 (페이징) */ + List selectList(SettleCorrectionSearchParam param); + + /** 정정 단건 조회 (응답 조인 포함) */ + SettleCorrectionResp selectById(@Param("correctionId") Long correctionId); + + /** 정정 단건 조회 (VO, 도메인 로직용) */ + SettleCorrectionVO selectVoById(@Param("correctionId") Long correctionId); + + /** 원본 정산 기준 정정 목록 */ + List selectByOriginalSettleId(@Param("originalSettleId") Long originalSettleId); + + /** 정정 등록 */ + int insert(SettleCorrectionVO vo); + + /** 상태 변경 (APPROVED/APPLIED/CANCELLED) */ + int updateStatus(@Param("correctionId") Long correctionId, + @Param("status") String status, + @Param("userId") Long userId); + + /** 정정 취소 */ + int cancel(@Param("correctionId") Long correctionId, + @Param("cancelReason") String cancelReason, + @Param("cancelledBy") Long cancelledBy); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/settle/SettleMasterMapper.java b/ga-core/src/main/java/com/ga/core/mapper/settle/SettleMasterMapper.java index d4ba9e9..1d2a83b 100644 --- a/ga-core/src/main/java/com/ga/core/mapper/settle/SettleMasterMapper.java +++ b/ga-core/src/main/java/com/ga/core/mapper/settle/SettleMasterMapper.java @@ -25,7 +25,8 @@ public interface SettleMasterMapper { @Param("status") String status, @Param("userId") Long userId); int updateHold(@Param("settleId") Long settleId, - @Param("reason") String reason); + @Param("reason") String reason, + @Param("reasonCode") String reasonCode); /** 월 요약 (전체) */ Map selectMonthSummary(@Param("settleMonth") String settleMonth); diff --git a/ga-core/src/main/java/com/ga/core/mapper/settle/TaxInvoiceMapper.java b/ga-core/src/main/java/com/ga/core/mapper/settle/TaxInvoiceMapper.java new file mode 100644 index 0000000..e47cdbb --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/settle/TaxInvoiceMapper.java @@ -0,0 +1,43 @@ +package com.ga.core.mapper.settle; + +import com.ga.core.vo.settle.TaxInvoiceResp; +import com.ga.core.vo.settle.TaxInvoiceSearchParam; +import com.ga.core.vo.settle.TaxInvoiceVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** + * 세금계산서 발행 이력 Mapper + * tax_invoice 테이블 CRUD + 취소 처리. + */ +@Mapper +public interface TaxInvoiceMapper { + + /** 세금계산서 목록 조회 (페이징) */ + List selectList(TaxInvoiceSearchParam param); + + /** 세금계산서 단건 조회 (응답 조인 포함) */ + TaxInvoiceResp selectById(@Param("invoiceId") Long invoiceId); + + /** 세금계산서 단건 조회 (발행 번호) */ + TaxInvoiceVO selectByInvoiceNo(@Param("invoiceNo") String invoiceNo); + + /** + * 정산월별 매출세금계산서 합계금액 조회. + * 부가세 정산 집계 시 사용. + */ + BigDecimal sumSupplyAmountByMonth(@Param("settleMonth") String settleMonth, + @Param("invoiceType") String invoiceType); + + /** 세금계산서 발행 등록 */ + int insert(TaxInvoiceVO vo); + + /** 세금계산서 취소 처리 */ + int cancel(@Param("invoiceId") Long invoiceId, + @Param("cancelDate") LocalDate cancelDate, + @Param("cancelReason") String cancelReason); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/settle/TerminationSettlementMapper.java b/ga-core/src/main/java/com/ga/core/mapper/settle/TerminationSettlementMapper.java new file mode 100644 index 0000000..d34cfd3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/settle/TerminationSettlementMapper.java @@ -0,0 +1,44 @@ +package com.ga.core.mapper.settle; + +import com.ga.core.vo.settle.TerminationSettlementResp; +import com.ga.core.vo.settle.TerminationSettlementSearchParam; +import com.ga.core.vo.settle.TerminationSettlementVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 해촉 정산 Mapper + * termination_settlement 테이블 CRUD + 상태 변경. + */ +@Mapper +public interface TerminationSettlementMapper { + + /** 해촉 정산 목록 조회 (페이징) */ + List selectList(TerminationSettlementSearchParam param); + + /** 해촉 정산 단건 조회 (응답 조인 포함) */ + TerminationSettlementResp selectById(@Param("termSettleId") Long termSettleId); + + /** 해촉 정산 단건 조회 (VO, 도메인 로직용) */ + TerminationSettlementVO selectVoById(@Param("termSettleId") Long termSettleId); + + /** 설계사별 해촉 정산 조회 */ + TerminationSettlementVO selectByAgent(@Param("agentId") Long agentId); + + /** 해촉 정산 등록 */ + int insert(TerminationSettlementVO vo); + + /** 해촉 정산 수정 (금액 산출 후 업데이트) */ + int update(TerminationSettlementVO vo); + + /** 상태 변경 (CONFIRMED/PAID/CANCELLED) */ + int updateStatus(@Param("termSettleId") Long termSettleId, + @Param("status") String status, + @Param("userId") Long userId); + + /** 지급 처리 완료 (payment_id 연결) */ + int linkPayment(@Param("termSettleId") Long termSettleId, + @Param("paymentId") Long paymentId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/training/AgentTrainingMapper.java b/ga-core/src/main/java/com/ga/core/mapper/training/AgentTrainingMapper.java new file mode 100644 index 0000000..5beadd2 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/training/AgentTrainingMapper.java @@ -0,0 +1,34 @@ +package com.ga.core.mapper.training; + +import com.ga.core.vo.training.AgentTrainingResp; +import com.ga.core.vo.training.AgentTrainingSearchParam; +import com.ga.core.vo.training.AgentTrainingVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +/** + * agent_training Mapper. + */ +@Mapper +public interface AgentTrainingMapper { + + AgentTrainingResp selectById(@Param("trainingId") Long trainingId); + + List selectList(AgentTrainingSearchParam param); + + /** + * 설계사 + 연도 기준 연간 이수 시간 합계. + * 의무 12시간 충족 여부 검사 시 호출. + */ + BigDecimal selectAnnualHoursByAgent(@Param("agentId") Long agentId, + @Param("year") int year); + + int insert(AgentTrainingVO vo); + + int update(AgentTrainingVO vo); + + int delete(@Param("trainingId") Long trainingId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/training/TrainingRequirementMapper.java b/ga-core/src/main/java/com/ga/core/mapper/training/TrainingRequirementMapper.java new file mode 100644 index 0000000..ac52824 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/training/TrainingRequirementMapper.java @@ -0,0 +1,17 @@ +package com.ga.core.mapper.training; + +import com.ga.core.vo.training.TrainingRequirementResp; +import com.ga.core.vo.training.TrainingRequirementSearchParam; +import com.ga.core.vo.training.TrainingRequirementVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TrainingRequirementMapper { + TrainingRequirementResp selectByYear(@Param("targetYear") int targetYear); + List selectList(TrainingRequirementSearchParam param); + int insert(TrainingRequirementVO vo); + int update(TrainingRequirementVO vo); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/withdraw/WithdrawRequestMapper.java b/ga-core/src/main/java/com/ga/core/mapper/withdraw/WithdrawRequestMapper.java new file mode 100644 index 0000000..500bf8b --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/withdraw/WithdrawRequestMapper.java @@ -0,0 +1,23 @@ +package com.ga.core.mapper.withdraw; + +import com.ga.core.vo.withdraw.WithdrawRequestResp; +import com.ga.core.vo.withdraw.WithdrawRequestSearchParam; +import com.ga.core.vo.withdraw.WithdrawRequestVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface WithdrawRequestMapper { + WithdrawRequestResp selectById(@Param("requestId") Long requestId); + /** account_no 복호화 포함 — 펌뱅킹 송신 배치 전용 */ + WithdrawRequestVO selectVOById(@Param("requestId") Long requestId); + List selectList(WithdrawRequestSearchParam param); + int insert(WithdrawRequestVO vo); + int updateStatus(@Param("requestId") Long requestId, @Param("status") String status, + @Param("failReason") String failReason); + int markSent(@Param("requestId") Long requestId); + int markCompleted(@Param("requestId") Long requestId); + int cancel(@Param("requestId") Long requestId); +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalAction.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalAction.java new file mode 100644 index 0000000..7a54834 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalAction.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.approval; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 결재 액션 (V50 approval_history.action) + */ +public enum ApprovalAction { + APPROVE("승인"), + REJECT("반려"), + RETURN("반송"); + + private final String label; + + ApprovalAction(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistoryResp.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistoryResp.java new file mode 100644 index 0000000..47da5b2 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistoryResp.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.approval; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class ApprovalHistoryResp { + private Long historyId; + private Long requestId; + private Integer stepNo; + private Long approverId; + private String approverName; + private String action; + private String actionName; + private String comment; + private LocalDateTime processedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistorySaveReq.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistorySaveReq.java new file mode 100644 index 0000000..0fd3461 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistorySaveReq.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.approval; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class ApprovalHistorySaveReq { + @NotNull + private Long requestId; + @NotNull + private Integer stepNo; + @NotNull + private Long approverId; + @NotBlank + private String action; + private String comment; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistorySearchParam.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistorySearchParam.java new file mode 100644 index 0000000..76f678b --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistorySearchParam.java @@ -0,0 +1,12 @@ +package com.ga.core.vo.approval; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ApprovalHistorySearchParam extends SearchParam { + private Long requestId; + private Long approverId; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistoryVO.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistoryVO.java new file mode 100644 index 0000000..a2e973f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalHistoryVO.java @@ -0,0 +1,21 @@ +package com.ga.core.vo.approval; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * approval_history — 결재 처리 이력 (V50) + */ +@Data +public class ApprovalHistoryVO { + private Long historyId; + private Long requestId; + private Integer stepNo; + private Long approverId; + /** ApprovalAction: APPROVE/REJECT/RETURN */ + private String action; + private String comment; + private LocalDateTime processedAt; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineResp.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineResp.java new file mode 100644 index 0000000..9e2ed48 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineResp.java @@ -0,0 +1,17 @@ +package com.ga.core.vo.approval; + +import lombok.Data; + +import java.util.List; + +@Data +public class ApprovalLineResp { + private Long lineId; + private String lineCode; + private String lineName; + private Integer maxStep; + private String targetType; + private Boolean isActive; + /** 결재선 단계 목록 (1:N) */ + private List steps; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineSaveReq.java new file mode 100644 index 0000000..2c8504d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineSaveReq.java @@ -0,0 +1,21 @@ +package com.ga.core.vo.approval; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class ApprovalLineSaveReq { + @NotBlank + private String lineCode; + @NotBlank + private String lineName; + @NotNull + @Min(1) + @Max(5) + private Integer maxStep; + @NotBlank + private String targetType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineSearchParam.java new file mode 100644 index 0000000..c57d375 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineSearchParam.java @@ -0,0 +1,12 @@ +package com.ga.core.vo.approval; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ApprovalLineSearchParam extends SearchParam { + private String targetType; + private Boolean isActive; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepResp.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepResp.java new file mode 100644 index 0000000..2c0f5b1 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepResp.java @@ -0,0 +1,12 @@ +package com.ga.core.vo.approval; + +import lombok.Data; + +@Data +public class ApprovalLineStepResp { + private Long stepId; + private Long lineId; + private String lineCode; + private Integer stepNo; + private String roleCode; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepSaveReq.java new file mode 100644 index 0000000..1252be3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepSaveReq.java @@ -0,0 +1,17 @@ +package com.ga.core.vo.approval; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class ApprovalLineStepSaveReq { + @NotNull + private Long lineId; + @NotNull + @Min(1) + private Integer stepNo; + @NotBlank + private String roleCode; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepSearchParam.java new file mode 100644 index 0000000..28fb5e8 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepSearchParam.java @@ -0,0 +1,11 @@ +package com.ga.core.vo.approval; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ApprovalLineStepSearchParam extends SearchParam { + private Long lineId; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepVO.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepVO.java new file mode 100644 index 0000000..311d3a7 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineStepVO.java @@ -0,0 +1,17 @@ +package com.ga.core.vo.approval; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * approval_line_step — 결재선 단계별 역할 정의 (V50) + */ +@Data +public class ApprovalLineStepVO { + private Long stepId; + private Long lineId; + private Integer stepNo; + private String roleCode; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineVO.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineVO.java new file mode 100644 index 0000000..6acfe41 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalLineVO.java @@ -0,0 +1,22 @@ +package com.ga.core.vo.approval; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * approval_line — 결재선 마스터 (V50) + */ +@Data +public class ApprovalLineVO { + private Long lineId; + private String lineCode; + private String lineName; + private Integer maxStep; + /** SETTLE/DISPUTE/INCENTIVE/WITHDRAW/COMPLAINT */ + private String targetType; + private Boolean isActive; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestResp.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestResp.java new file mode 100644 index 0000000..de1f595 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestResp.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.approval; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +public class ApprovalRequestResp { + private Long requestId; + private String targetType; + private Long targetId; + private Long lineId; + private String lineCode; + private String lineName; + private Long requestedBy; + private LocalDateTime requestedAt; + private Integer currentStep; + private Integer maxStep; + private String status; + private String statusName; + private LocalDateTime completedAt; + /** 결재 이력 목록 (1:N) */ + private List histories; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestSaveReq.java new file mode 100644 index 0000000..36a6f93 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestSaveReq.java @@ -0,0 +1,17 @@ +package com.ga.core.vo.approval; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class ApprovalRequestSaveReq { + @NotBlank + private String targetType; + @NotNull + private Long targetId; + @NotNull + private Long lineId; + @NotNull + private Long requestedBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestSearchParam.java new file mode 100644 index 0000000..9b34706 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestSearchParam.java @@ -0,0 +1,12 @@ +package com.ga.core.vo.approval; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ApprovalRequestSearchParam extends SearchParam { + private String targetType; + private Long requestedBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestVO.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestVO.java new file mode 100644 index 0000000..6201428 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalRequestVO.java @@ -0,0 +1,26 @@ +package com.ga.core.vo.approval; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * approval_request — 결재 요청 (V50) + */ +@Data +public class ApprovalRequestVO { + private Long requestId; + /** SETTLE/DISPUTE/INCENTIVE/WITHDRAW/COMPLAINT */ + private String targetType; + /** 결재 대상 레코드 PK */ + private Long targetId; + private Long lineId; + private Long requestedBy; + private LocalDateTime requestedAt; + private Integer currentStep; + /** ApprovalStatus: PENDING/APPROVED/REJECTED/CANCEL */ + private String status; + private LocalDateTime completedAt; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalStatus.java b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalStatus.java new file mode 100644 index 0000000..067ea70 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/approval/ApprovalStatus.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.approval; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 결재 요청 상태 (V50 approval_request.status) + */ +public enum ApprovalStatus { + PENDING("진행중"), + APPROVED("최종승인"), + REJECTED("반려"), + CANCEL("취소"); + + private final String label; + + ApprovalStatus(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentRefType.java b/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentRefType.java new file mode 100644 index 0000000..716667a --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentRefType.java @@ -0,0 +1,33 @@ +package com.ga.core.vo.attachment; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 첨부파일 참조 도메인 유형 (V52 attachment.ref_type) + */ +public enum AttachmentRefType { + AGENT_CONTRACT("위촉계약"), + COMPLAINT("민원"), + APPROVAL_REQUEST("결재요청"), + WITHDRAW_REQUEST("출금신청"), + NOTICE("공지사항"), + DISPUTE("이의신청"), + INCENTIVE("시책"), + AGENT_LICENSE("자격증"), + AGENT_TRAINING("보수교육"); + + private final String label; + + AttachmentRefType(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentResp.java b/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentResp.java new file mode 100644 index 0000000..5bc7c6f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentResp.java @@ -0,0 +1,19 @@ +package com.ga.core.vo.attachment; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class AttachmentResp { + private Long attachmentId; + private String refType; + private Long refId; + private String fileName; + private String filePath; + private Long fileSize; + private String mimeType; + private Long uploadedBy; + private String uploaderName; + private LocalDateTime uploadedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentSaveReq.java new file mode 100644 index 0000000..f4561df --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentSaveReq.java @@ -0,0 +1,24 @@ +package com.ga.core.vo.attachment; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +@Data +public class AttachmentSaveReq { + @NotBlank + private String refType; + @NotNull + private Long refId; + @NotBlank + private String fileName; + @NotBlank + private String filePath; + @NotNull + @Positive + private Long fileSize; + private String mimeType; + @NotNull + private Long uploadedBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentSearchParam.java new file mode 100644 index 0000000..1eeaa51 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentSearchParam.java @@ -0,0 +1,13 @@ +package com.ga.core.vo.attachment; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AttachmentSearchParam extends SearchParam { + private String refType; + private Long refId; + private Long uploadedBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentVO.java b/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentVO.java new file mode 100644 index 0000000..53e9e95 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/attachment/AttachmentVO.java @@ -0,0 +1,26 @@ +package com.ga.core.vo.attachment; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * attachment — 공통 첨부파일 (V52) + * ref_type + ref_id 조합으로 모든 도메인에 첨부 연결. + * 파일 실체는 스토리지(로컬/S3). file_path에 경로 저장. + */ +@Data +public class AttachmentVO { + private Long attachmentId; + /** AttachmentRefType */ + private String refType; + /** 첨부 대상 레코드 PK */ + private Long refId; + private String fileName; + private String filePath; + /** 파일 크기 (bytes) */ + private Long fileSize; + private String mimeType; + private Long uploadedBy; + private LocalDateTime uploadedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/BonusType.java b/ga-core/src/main/java/com/ga/core/vo/commission/BonusType.java new file mode 100644 index 0000000..e99fbf3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/BonusType.java @@ -0,0 +1,26 @@ +package com.ga.core.vo.commission; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 유지보수수당 지급 방식 (V43 persistency_bonus_rule.bonus_type) + */ +public enum BonusType { + RATE("모집수수료 대비 비율"), + FIXED("건당 고정금액"); + + private final String label; + + BonusType(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerResp.java new file mode 100644 index 0000000..51894cf --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerResp.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * collection_commission_ledger 조회 응답 — 조인 필드(agentName, contractNo 등) 포함. + */ +@Data +public class CollectionCommissionLedgerResp { + private Long ledgerId; + private Long contractId; + private String policyNo; + private Long agentId; + private String agentName; + private String orgName; + private String settleMonth; + private BigDecimal premiumAmount; + private BigDecimal commissionRate; + private BigDecimal commissionAmount; + private String paymentCycle; + private String paymentCycleName; + private LocalDate paidAt; + private String status; + private String statusName; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerSaveReq.java new file mode 100644 index 0000000..463f058 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerSaveReq.java @@ -0,0 +1,34 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class CollectionCommissionLedgerSaveReq { + @NotNull + private Long contractId; + + @NotNull + private Long agentId; + + @NotBlank + private String settleMonth; + + @NotNull + @Positive + private BigDecimal premiumAmount; + + @NotNull + private BigDecimal commissionRate; + + @NotBlank + private String paymentCycle; + + @NotNull + private LocalDate paidAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerSearchParam.java new file mode 100644 index 0000000..7681a0b --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerSearchParam.java @@ -0,0 +1,14 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class CollectionCommissionLedgerSearchParam extends SearchParam { + private Long agentId; + private Long contractId; + private String settleMonth; + private String paymentCycle; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerVO.java new file mode 100644 index 0000000..26ee9eb --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionLedgerVO.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * collection_commission_ledger — 수금수수료 원장 (V41) + */ +@Data +public class CollectionCommissionLedgerVO { + private Long ledgerId; + private Long contractId; + private Long agentId; + private String settleMonth; + private BigDecimal premiumAmount; + private BigDecimal commissionRate; + private BigDecimal commissionAmount; + /** PaymentCycle */ + private String paymentCycle; + private LocalDate paidAt; + /** CALCULATED / CONFIRMED / PAID / CANCELLED */ + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleResp.java new file mode 100644 index 0000000..ab22408 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleResp.java @@ -0,0 +1,21 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * collection_commission_rule 조회 응답 — 상품명 조인 포함. + */ +@Data +public class CollectionCommissionRuleResp { + private Long ruleId; + private Long productId; + private String productName; + private String paymentCycle; + private String paymentCycleName; + private BigDecimal commissionRate; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleSaveReq.java new file mode 100644 index 0000000..2c6de21 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleSaveReq.java @@ -0,0 +1,32 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * collection_commission_rule 등록/수정 요청. + */ +@Data +public class CollectionCommissionRuleSaveReq { + @NotNull + private Long productId; + + @NotBlank + private String paymentCycle; + + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal commissionRate; + + @NotNull + private LocalDate effectiveFrom; + + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleSearchParam.java new file mode 100644 index 0000000..0c919a8 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleSearchParam.java @@ -0,0 +1,12 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class CollectionCommissionRuleSearchParam extends SearchParam { + private Long productId; + private String paymentCycle; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleVO.java new file mode 100644 index 0000000..204c4ca --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CollectionCommissionRuleVO.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * collection_commission_rule — 납입주기별 수금수수료율 마스터 (V41) + */ +@Data +public class CollectionCommissionRuleVO { + private Long ruleId; + private Long productId; + /** PaymentCycle: MONTHLY/QUARTERLY/SEMI/ANNUAL */ + private String paymentCycle; + /** 수금수수료율 (소수, 예: 0.005 = 0.5%) */ + private BigDecimal commissionRate; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterResp.java new file mode 100644 index 0000000..8b510c7 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterResp.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * commission_master 조회 응답 — 조인 필드(agentName, orgName 등) 포함. + */ +@Data +public class CommissionMasterResp { + private Long masterId; + private Long agentId; + private String agentName; + private String orgName; + private String commissionType; + private String commissionTypeName; + private String settleMonth; + private String sourceTable; + private Long sourceId; + private BigDecimal grossAmount; + private BigDecimal taxAmount; + private BigDecimal netAmount; + private String status; + private String statusName; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterSaveReq.java new file mode 100644 index 0000000..ab8600f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterSaveReq.java @@ -0,0 +1,42 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * commission_master 등록/수정 요청 DTO. + * ID, 상태, 일시 필드는 제외. 배치 upsert 시에도 이 필드 셋을 사용. + */ +@Data +public class CommissionMasterSaveReq { + @NotNull + private Long agentId; + + @NotBlank + private String commissionType; + + @NotBlank + private String settleMonth; + + @NotBlank + private String sourceTable; + + @NotNull + private Long sourceId; + + @NotNull + @PositiveOrZero + private BigDecimal grossAmount; + + @NotNull + @PositiveOrZero + private BigDecimal taxAmount; + + @NotNull + @PositiveOrZero + private BigDecimal netAmount; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterSearchParam.java new file mode 100644 index 0000000..e6a1ebb --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterSearchParam.java @@ -0,0 +1,17 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * commission_master 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CommissionMasterSearchParam extends SearchParam { + private Long agentId; + private String commissionType; + private String settleMonth; + private String sourceTable; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterVO.java new file mode 100644 index 0000000..d1406e2 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionMasterVO.java @@ -0,0 +1,32 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * commission_master — 전 수수료 종류 통합 마스터 (V40) + * 모든 수수료 원장(recruit_ledger / collection_commission_ledger 등)에서 + * 산출된 결과를 단일 허브로 집계. INSERT/UPDATE 전용. + */ +@Data +public class CommissionMasterVO { + private Long masterId; + private Long agentId; + /** COMMISSION_TYPE 코드 (CommissionType enum) */ + private String commissionType; + /** 정산 귀속월 YYYYMM */ + private String settleMonth; + /** 원본 원장 테이블명 (예: recruit_ledger) */ + private String sourceTable; + /** 원본 원장 PK */ + private Long sourceId; + private BigDecimal grossAmount; + private BigDecimal taxAmount; + private BigDecimal netAmount; + /** CALCULATED / CONFIRMED / PAID / CANCELLED */ + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CommissionType.java b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionType.java new file mode 100644 index 0000000..2541b75 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionType.java @@ -0,0 +1,34 @@ +package com.ga.core.vo.commission; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 수수료 종류 코드 (COMMISSION_TYPE 그룹, V40) + * commission_master.commission_type, override_ledger.source_commission_type 등에서 사용. + */ +public enum CommissionType { + RECRUIT("모집수수료"), + MAINTAIN("유지수수료"), + COLLECT("수금수수료"), + OVERRIDE("오버라이드"), + RENEWAL("갱신수수료"), + REINSTATE("부활수수료"), + PERSIST("유지보수수당"), + INCENTIVE("시책수수료"), + ONBOARD("위촉수수료"); + + private final String label; + + CommissionType(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerResp.java new file mode 100644 index 0000000..42d840a --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerResp.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class OverrideLedgerResp { + private Long ledgerId; + private Long fromAgentId; + private String fromAgentName; + private Long toAgentId; + private String toAgentName; + private String orgName; + private String settleMonth; + private String sourceCommissionType; + private String sourceCommissionTypeName; + private String sourceLedgerTable; + private Long sourceLedgerId; + private BigDecimal sourceAmount; + private BigDecimal overrideRate; + private BigDecimal overrideAmount; + private String status; + private String statusName; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerSaveReq.java new file mode 100644 index 0000000..4dbfed8 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerSaveReq.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class OverrideLedgerSaveReq { + @NotNull + private Long fromAgentId; + @NotNull + private Long toAgentId; + private Long orgId; + @NotBlank + private String settleMonth; + @NotBlank + private String sourceCommissionType; + private String sourceLedgerTable; + private Long sourceLedgerId; + @NotNull + @Positive + private BigDecimal sourceAmount; + @NotNull + private BigDecimal overrideRate; + @NotNull + private BigDecimal overrideAmount; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerSearchParam.java new file mode 100644 index 0000000..2397ea3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerSearchParam.java @@ -0,0 +1,15 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class OverrideLedgerSearchParam extends SearchParam { + private Long toAgentId; + private Long fromAgentId; + private Long orgId; + private String settleMonth; + private String sourceCommissionType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerVO.java new file mode 100644 index 0000000..58b7cc1 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideLedgerVO.java @@ -0,0 +1,32 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * override_ledger — 오버라이드 수수료 상세 원장 (V44) + * calcOverride 결과를 수수료 종류 단위로 추적. + */ +@Data +public class OverrideLedgerVO { + private Long ledgerId; + /** 원천 설계사 (오버라이드 기준 수수료 발생 하위 설계사) */ + private Long fromAgentId; + /** 오버라이드 수령 설계사 (상위 직급) */ + private Long toAgentId; + private Long orgId; + private String settleMonth; + /** 원천 수수료 종류 (COMMISSION_TYPE 코드) */ + private String sourceCommissionType; + private String sourceLedgerTable; + private Long sourceLedgerId; + private BigDecimal sourceAmount; + private BigDecimal overrideRate; + private BigDecimal overrideAmount; + /** CALCULATED / CONFIRMED / PAID / CANCELLED */ + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummaryResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummaryResp.java new file mode 100644 index 0000000..47e0c90 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummaryResp.java @@ -0,0 +1,21 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class OverrideSummaryResp { + private Long summaryId; + private Long toAgentId; + private String toAgentName; + private String orgName; + private String settleMonth; + private BigDecimal totalSourceAmount; + private BigDecimal totalOverrideAmount; + private Integer ledgerCount; + private String status; + private String statusName; + private LocalDateTime calcDate; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummarySaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummarySaveReq.java new file mode 100644 index 0000000..5f3cf31 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummarySaveReq.java @@ -0,0 +1,21 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class OverrideSummarySaveReq { + @NotNull + private Long toAgentId; + @NotBlank + private String settleMonth; + @NotNull + private BigDecimal totalSourceAmount; + @NotNull + private BigDecimal totalOverrideAmount; + @NotNull + private Integer ledgerCount; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummarySearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummarySearchParam.java new file mode 100644 index 0000000..133aad8 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummarySearchParam.java @@ -0,0 +1,12 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class OverrideSummarySearchParam extends SearchParam { + private Long toAgentId; + private String settleMonth; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummaryVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummaryVO.java new file mode 100644 index 0000000..b3ddc65 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/OverrideSummaryVO.java @@ -0,0 +1,24 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * override_summary — agent+settle_month 단위 오버라이드 합계 (V44) + * UNIQUE(to_agent_id, settle_month) — 배치 완료 후 UPSERT. + */ +@Data +public class OverrideSummaryVO { + private Long summaryId; + private Long toAgentId; + private String settleMonth; + private BigDecimal totalSourceAmount; + private BigDecimal totalOverrideAmount; + private Integer ledgerCount; + /** CALCULATED / CONFIRMED / PAID */ + private String status; + private LocalDateTime calcDate; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/PaymentCycle.java b/ga-core/src/main/java/com/ga/core/vo/commission/PaymentCycle.java new file mode 100644 index 0000000..12c6f4c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/PaymentCycle.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.commission; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 납입주기 코드 (V41 collection_commission_rule.payment_cycle) + */ +public enum PaymentCycle { + MONTHLY("월납"), + QUARTERLY("분기납"), + SEMI("반기납"), + ANNUAL("연납"); + + private final String label; + + PaymentCycle(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusResp.java new file mode 100644 index 0000000..45eb85a --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusResp.java @@ -0,0 +1,21 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class PersistencyBonusResp { + private Long bonusId; + private Long agentId; + private String agentName; + private String orgName; + private Long ruleId; + private String baseMonth; + private Integer retentionMonth; + private String settleMonth; + private BigDecimal retentionRate; + private BigDecimal bonusAmount; + private String status; + private String statusName; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleResp.java new file mode 100644 index 0000000..ad2bc32 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleResp.java @@ -0,0 +1,20 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class PersistencyBonusRuleResp { + private Long ruleId; + private Long gradeId; + private String gradeName; + private Integer retentionMonth; + private BigDecimal retentionRateThreshold; + private String bonusType; + private String bonusTypeName; + private BigDecimal bonusValue; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleSaveReq.java new file mode 100644 index 0000000..d1049b2 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleSaveReq.java @@ -0,0 +1,34 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class PersistencyBonusRuleSaveReq { + private Long gradeId; + + @NotNull + private Integer retentionMonth; + + @NotNull + @DecimalMin(value = "0", inclusive = false) + @DecimalMax("1") + private BigDecimal retentionRateThreshold; + + @NotBlank + private String bonusType; + + @NotNull + private BigDecimal bonusValue; + + @NotNull + private LocalDate effectiveFrom; + + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleSearchParam.java new file mode 100644 index 0000000..b17691e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleSearchParam.java @@ -0,0 +1,13 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class PersistencyBonusRuleSearchParam extends SearchParam { + private Long gradeId; + private Integer retentionMonth; + private String bonusType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleVO.java new file mode 100644 index 0000000..7b04495 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusRuleVO.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * persistency_bonus_rule — 유지보수수당 룰 마스터 (V43) + */ +@Data +public class PersistencyBonusRuleVO { + private Long ruleId; + /** 적용 직급 FK (NULL=전 직급 공통) */ + private Long gradeId; + /** 유지 기준 개월수 (13/25/37/49) */ + private Integer retentionMonth; + /** 유지율 최소 기준 (0초과~1이하, 예: 0.95) */ + private BigDecimal retentionRateThreshold; + /** RATE / FIXED */ + private String bonusType; + /** RATE: 비율값 / FIXED: 지급 금액 */ + private BigDecimal bonusValue; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusSaveReq.java new file mode 100644 index 0000000..cb7a0cc --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusSaveReq.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class PersistencyBonusSaveReq { + @NotNull + private Long agentId; + private Long ruleId; + @NotBlank + private String baseMonth; + @NotNull + private Integer retentionMonth; + @NotBlank + private String settleMonth; + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal retentionRate; + @NotNull + private BigDecimal bonusAmount; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusSearchParam.java new file mode 100644 index 0000000..80ebcd4 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusSearchParam.java @@ -0,0 +1,13 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class PersistencyBonusSearchParam extends SearchParam { + private Long agentId; + private String settleMonth; + private Integer retentionMonth; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusVO.java new file mode 100644 index 0000000..96f08a9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/PersistencyBonusVO.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * persistency_bonus — 유지보수수당 지급 이력 (V43) + * UNIQUE(agent_id, base_month, retention_month) — 동일 기준월·개월수 중복 방지. + */ +@Data +public class PersistencyBonusVO { + private Long bonusId; + private Long agentId; + private Long ruleId; + /** 계약 체결 기준 정산월 (YYYYMM) */ + private String baseMonth; + /** 평가 기준 개월수 (13/25/37/49) */ + private Integer retentionMonth; + /** 실제 지급 귀속 정산월 (YYYYMM) */ + private String settleMonth; + /** 평가 시점 실제 유지율 */ + private BigDecimal retentionRate; + private BigDecimal bonusAmount; + /** CALCULATED / CONFIRMED / PAID / CANCELLED */ + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerResp.java new file mode 100644 index 0000000..7d83db5 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerResp.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class ReinstatementCommissionLedgerResp { + private Long ledgerId; + private Long contractId; + private String policyNo; + private Long agentId; + private String agentName; + private String orgName; + private String settleMonth; + private LocalDate reinstatementDate; + private BigDecimal reinstatementPremium; + private BigDecimal commissionAmount; + private BigDecimal chargebackReversal; + private BigDecimal netCommission; + private String payType; + private String status; + private String statusName; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerSaveReq.java new file mode 100644 index 0000000..75c7c62 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerSaveReq.java @@ -0,0 +1,32 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class ReinstatementCommissionLedgerSaveReq { + @NotNull + private Long contractId; + @NotNull + private Long agentId; + private Long ruleId; + @NotBlank + private String settleMonth; + @NotNull + private LocalDate reinstatementDate; + @NotNull + @PositiveOrZero + private BigDecimal reinstatementPremium; + @NotNull + private BigDecimal commissionAmount; + @NotNull + @PositiveOrZero + private BigDecimal chargebackReversal; + @NotBlank + private String payType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerSearchParam.java new file mode 100644 index 0000000..f6bcc1b --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerSearchParam.java @@ -0,0 +1,13 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ReinstatementCommissionLedgerSearchParam extends SearchParam { + private Long agentId; + private Long contractId; + private String settleMonth; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerVO.java new file mode 100644 index 0000000..d42d92f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionLedgerVO.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * reinstatement_commission_ledger — 부활수수료 원장 (V42) + */ +@Data +public class ReinstatementCommissionLedgerVO { + private Long ledgerId; + private Long contractId; + private Long agentId; + private Long ruleId; + private String settleMonth; + private LocalDate reinstatementDate; + private BigDecimal reinstatementPremium; + private BigDecimal commissionAmount; + /** 부활로 인한 환수액 환원금 */ + private BigDecimal chargebackReversal; + /** 최종 수수료 = commissionAmount + chargebackReversal */ + private BigDecimal netCommission; + private String payType; + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleResp.java new file mode 100644 index 0000000..b07cfe9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleResp.java @@ -0,0 +1,19 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class ReinstatementCommissionRuleResp { + private Long ruleId; + private Long productId; + private String productName; + private String payType; + private BigDecimal rateValue; + private BigDecimal fixedAmount; + private BigDecimal chargebackReversalRate; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleSaveReq.java new file mode 100644 index 0000000..75f42cc --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleSaveReq.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class ReinstatementCommissionRuleSaveReq { + @NotNull + private Long productId; + @NotBlank + private String payType; + private BigDecimal rateValue; + private BigDecimal fixedAmount; + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal chargebackReversalRate; + @NotNull + private LocalDate effectiveFrom; + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleSearchParam.java new file mode 100644 index 0000000..88d936c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleSearchParam.java @@ -0,0 +1,12 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ReinstatementCommissionRuleSearchParam extends SearchParam { + private Long productId; + private String payType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleVO.java new file mode 100644 index 0000000..937caf3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReinstatementCommissionRuleVO.java @@ -0,0 +1,26 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * reinstatement_commission_rule — 부활수수료율 마스터 (V42) + */ +@Data +public class ReinstatementCommissionRuleVO { + private Long ruleId; + private Long productId; + private String payType; + private BigDecimal rateValue; + private BigDecimal fixedAmount; + /** 부활 시 기존 환수액 중 환원 비율 (0~1) */ + private BigDecimal chargebackReversalRate; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerResp.java new file mode 100644 index 0000000..45fb1e8 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerResp.java @@ -0,0 +1,23 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class RenewalCommissionLedgerResp { + private Long ledgerId; + private Long contractId; + private String policyNo; + private Long agentId; + private String agentName; + private String orgName; + private String settleMonth; + private LocalDate renewalDate; + private BigDecimal renewalPremium; + private BigDecimal commissionAmount; + private String payType; + private String status; + private String statusName; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerSaveReq.java new file mode 100644 index 0000000..46973af --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerSaveReq.java @@ -0,0 +1,29 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class RenewalCommissionLedgerSaveReq { + @NotNull + private Long contractId; + @NotNull + private Long agentId; + private Long ruleId; + @NotBlank + private String settleMonth; + @NotNull + private LocalDate renewalDate; + @NotNull + @Positive + private BigDecimal renewalPremium; + @NotNull + private BigDecimal commissionAmount; + @NotBlank + private String payType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerSearchParam.java new file mode 100644 index 0000000..ce170f4 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerSearchParam.java @@ -0,0 +1,13 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class RenewalCommissionLedgerSearchParam extends SearchParam { + private Long agentId; + private Long contractId; + private String settleMonth; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerVO.java new file mode 100644 index 0000000..ce4a284 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionLedgerVO.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * renewal_commission_ledger — 갱신수수료 원장 (V42) + */ +@Data +public class RenewalCommissionLedgerVO { + private Long ledgerId; + private Long contractId; + private Long agentId; + private Long ruleId; + private String settleMonth; + private LocalDate renewalDate; + private BigDecimal renewalPremium; + private BigDecimal commissionAmount; + /** RATE / FIXED */ + private String payType; + /** CALCULATED / CONFIRMED / PAID / CANCELLED */ + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleResp.java new file mode 100644 index 0000000..6d63865 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleResp.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class RenewalCommissionRuleResp { + private Long ruleId; + private Long productId; + private String productName; + private String payType; + private BigDecimal rateValue; + private BigDecimal fixedAmount; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleSaveReq.java new file mode 100644 index 0000000..76545a9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleSaveReq.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class RenewalCommissionRuleSaveReq { + @NotNull + private Long productId; + + @NotBlank + private String payType; + + private BigDecimal rateValue; + private BigDecimal fixedAmount; + + @NotNull + private LocalDate effectiveFrom; + + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleSearchParam.java new file mode 100644 index 0000000..5b59943 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleSearchParam.java @@ -0,0 +1,12 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class RenewalCommissionRuleSearchParam extends SearchParam { + private Long productId; + private String payType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleVO.java new file mode 100644 index 0000000..7d534d0 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RenewalCommissionRuleVO.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * renewal_commission_rule — 갱신수수료율 마스터 (V42) + */ +@Data +public class RenewalCommissionRuleVO { + private Long ruleId; + private Long productId; + /** RATE / FIXED */ + private String payType; + private BigDecimal rateValue; + private BigDecimal fixedAmount; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RetentionMonth.java b/ga-core/src/main/java/com/ga/core/vo/commission/RetentionMonth.java new file mode 100644 index 0000000..fe199fd --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RetentionMonth.java @@ -0,0 +1,32 @@ +package com.ga.core.vo.commission; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 유지보수수당 기준 개월수 (V43 persistency_bonus_rule.retention_month) + * 13/25/37/49개월 유지율 충족 시 각각 1회 지급. + */ +public enum RetentionMonth { + M13(13), + M25(25), + M37(37), + M49(49); + + private final int months; + + RetentionMonth(int months) { + this.months = months; + } + + @JsonValue + public int getMonths() { + return months; + } + + public static RetentionMonth ofMonths(int months) { + for (RetentionMonth rm : values()) { + if (rm.months == months) return rm; + } + throw new IllegalArgumentException("Unsupported retention month: " + months); + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintChannel.java b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintChannel.java new file mode 100644 index 0000000..7dcf437 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintChannel.java @@ -0,0 +1,29 @@ +package com.ga.core.vo.complaint; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 민원 접수 채널 (V49 complaint.channel) + */ +public enum ComplaintChannel { + PHONE("전화"), + EMAIL("이메일"), + VISIT("방문"), + ONLINE("온라인"), + FAX("팩스"); + + private final String label; + + ComplaintChannel(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintResp.java b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintResp.java new file mode 100644 index 0000000..08b6dd9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintResp.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.complaint; + +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +public class ComplaintResp { + private Long complaintId; + private Long contractId; + private String policyNo; + private Long agentId; + private String agentName; + private String orgName; + private String customerName; + private LocalDate complaintDate; + private String channel; + private String channelName; + private String complaintType; + private String complaintTypeName; + private String severity; + private String severityName; + private String status; + private String statusName; + private String resolution; + private Boolean chargebackTriggered; + private LocalDateTime settledAt; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintSaveReq.java new file mode 100644 index 0000000..9e00591 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintSaveReq.java @@ -0,0 +1,36 @@ +package com.ga.core.vo.complaint; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class ComplaintSaveReq { + private Long contractId; + + @NotNull + private Long agentId; + + @NotBlank + @Size(max = 100) + private String customerName; + + @NotNull + private LocalDate complaintDate; + + @NotBlank + private String channel; + + @NotBlank + private String complaintType; + + private String severity = "MID"; + + private String resolution; + + @NotNull + private Boolean chargebackTriggered; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintSearchParam.java new file mode 100644 index 0000000..55cea40 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintSearchParam.java @@ -0,0 +1,14 @@ +package com.ga.core.vo.complaint; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ComplaintSearchParam extends SearchParam { + private Long agentId; + private String complaintType; + private String severity; + private Boolean chargebackTriggered; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintSeverity.java b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintSeverity.java new file mode 100644 index 0000000..50b97a1 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintSeverity.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.complaint; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 민원 심각도 (V49 complaint.severity) + */ +public enum ComplaintSeverity { + LOW("낮음"), + MID("보통"), + HIGH("높음"); + + private final String label; + + ComplaintSeverity(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintStatus.java b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintStatus.java new file mode 100644 index 0000000..04c6c0e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintStatus.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.complaint; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 민원 처리 상태 (V49 complaint.status) + */ +public enum ComplaintStatus { + OPEN("접수"), + IN_PROGRESS("처리중"), + CLOSED("완료"), + WITHDRAWN("취하"); + + private final String label; + + ComplaintStatus(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintVO.java b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintVO.java new file mode 100644 index 0000000..01f0c93 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/complaint/ComplaintVO.java @@ -0,0 +1,34 @@ +package com.ga.core.vo.complaint; + +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * complaint — 민원 관리 (V49) + * chargeback_triggered=true 이면 환수 자동 생성 대상. + */ +@Data +public class ComplaintVO { + private Long complaintId; + private Long contractId; + private Long agentId; + private String customerName; + private LocalDate complaintDate; + /** ComplaintChannel: PHONE/EMAIL/VISIT/ONLINE/FAX */ + private String channel; + /** MIS_SELLING/DELAY/UNFAIR/CANCELLATION/ETC */ + private String complaintType; + /** ComplaintSeverity: LOW/MID/HIGH */ + private String severity; + /** ComplaintStatus: OPEN/IN_PROGRESS/CLOSED/WITHDRAWN */ + private String status; + private String resolution; + private Boolean chargebackTriggered; + private LocalDateTime settledAt; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; + private Long updatedBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractResp.java b/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractResp.java new file mode 100644 index 0000000..9d4fb37 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractResp.java @@ -0,0 +1,26 @@ +package com.ga.core.vo.contract; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +public class AgentContractResp { + private Long contractId; + private Long agentId; + private String agentName; + private Long carrierId; + private String carrierName; + private String contractNo; + private LocalDate signedDate; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + private String status; + private String statusName; + private BigDecimal onboardingBonusAmount; + private LocalDateTime terminatedAt; + private String terminatedReason; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractSaveReq.java new file mode 100644 index 0000000..c4f630d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractSaveReq.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.contract; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class AgentContractSaveReq { + @NotNull + private Long agentId; + @NotNull + private Long carrierId; + @NotBlank + private String contractNo; + @NotNull + private LocalDate signedDate; + @NotNull + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + @NotNull + @PositiveOrZero + private BigDecimal onboardingBonusAmount; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractSearchParam.java new file mode 100644 index 0000000..40a515f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractSearchParam.java @@ -0,0 +1,12 @@ +package com.ga.core.vo.contract; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AgentContractSearchParam extends SearchParam { + private Long agentId; + private Long carrierId; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractStatus.java b/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractStatus.java new file mode 100644 index 0000000..eeabd4f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractStatus.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.contract; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 위촉계약 상태 (V45 agent_contract.status) + */ +public enum AgentContractStatus { + ACTIVE("유효"), + TERMINATED("해촉"), + SUSPENDED("정지"); + + private final String label; + + AgentContractStatus(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractVO.java b/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractVO.java new file mode 100644 index 0000000..69f4a17 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/contract/AgentContractVO.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.contract; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * agent_contract — 보험사-설계사 위촉계약 (V45) + */ +@Data +public class AgentContractVO { + private Long contractId; + private Long agentId; + private Long carrierId; + private String contractNo; + private LocalDate signedDate; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + /** AgentContractStatus: ACTIVE/TERMINATED/SUSPENDED */ + private String status; + private BigDecimal onboardingBonusAmount; + private LocalDateTime terminatedAt; + private String terminatedReason; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; + private Long updatedBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementResp.java b/ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementResp.java new file mode 100644 index 0000000..3b37b55 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementResp.java @@ -0,0 +1,23 @@ +package com.ga.core.vo.endorse; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class ContractEndorsementResp { + private Long endorsementId; + private Long contractId; + private String policyNo; + private Long agentId; + private String agentName; + private String endorsementType; + private String endorsementTypeName; + private LocalDateTime changedAt; + private String oldValue; + private String newValue; + private String reason; + private Boolean requiresCommissionRecalc; + private Boolean processed; + private LocalDateTime processedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementSaveReq.java new file mode 100644 index 0000000..813a7d3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementSaveReq.java @@ -0,0 +1,20 @@ +package com.ga.core.vo.endorse; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class ContractEndorsementSaveReq { + @NotNull + private Long contractId; + @NotNull + private Long agentId; + @NotBlank + private String endorsementType; + private String oldValue; + private String newValue; + private String reason; + @NotNull + private Boolean requiresCommissionRecalc; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementSearchParam.java new file mode 100644 index 0000000..a8adf23 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementSearchParam.java @@ -0,0 +1,15 @@ +package com.ga.core.vo.endorse; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ContractEndorsementSearchParam extends SearchParam { + private Long contractId; + private Long agentId; + private String endorsementType; + private Boolean requiresCommissionRecalc; + private Boolean processed; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementVO.java b/ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementVO.java new file mode 100644 index 0000000..23ab306 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/endorse/ContractEndorsementVO.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.endorse; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * contract_endorsement — 계약 변경 이력 (V48) + * requires_commission_recalc=true 이면 배치에서 수수료 재계산 트리거. + */ +@Data +public class ContractEndorsementVO { + private Long endorsementId; + private Long contractId; + private Long agentId; + /** EndorsementType: PREMIUM_CHANGE/BENEFICIARY/INSURED/ADDRESS/OTHER */ + private String endorsementType; + private LocalDateTime changedAt; + /** 변경 전 값 (JSON 또는 텍스트) */ + private String oldValue; + /** 변경 후 값 (JSON 또는 텍스트) */ + private String newValue; + private String reason; + private Boolean requiresCommissionRecalc; + private Boolean processed; + private LocalDateTime processedAt; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/endorse/EndorsementType.java b/ga-core/src/main/java/com/ga/core/vo/endorse/EndorsementType.java new file mode 100644 index 0000000..f806fed --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/endorse/EndorsementType.java @@ -0,0 +1,29 @@ +package com.ga.core.vo.endorse; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 계약 변경 유형 (V48 contract_endorsement.endorsement_type) + */ +public enum EndorsementType { + PREMIUM_CHANGE("보험료변경"), + BENEFICIARY("수익자변경"), + INSURED("피보험자변경"), + ADDRESS("주소변경"), + OTHER("기타"); + + private final String label; + + EndorsementType(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/kpi/KpiCumulativeResp.java b/ga-core/src/main/java/com/ga/core/vo/kpi/KpiCumulativeResp.java new file mode 100644 index 0000000..096f96e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/kpi/KpiCumulativeResp.java @@ -0,0 +1,29 @@ +package com.ga.core.vo.kpi; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 설계사/조직별 누계 수수료 KPI 응답 VO (mv_cumulative 기반) + * V37 Materialized View 조회 전용. + * level_type=AGENT 또는 ORG 두 레벨 혼재. + */ +@Data +public class KpiCumulativeResp { + private String levelType; // AGENT / ORG + private Long entityId; // AGENT이면 agent_id, ORG이면 org_id + private String entityName; // 설계사명 또는 조직명 + private Long orgId; + private String orgName; + private String firstSettleMonth; // 첫 정산월 + private String lastSettleMonth; // 최근 정산월 + private Long settleMonthCount; // 정산 처리 월 수 + private BigDecimal cumGross; // 누적 총 수수료 (세전) + private BigDecimal cumNet; // 누적 순지급액 (세후) + private BigDecimal cumChargeback; // 누적 환수 합계 + private BigDecimal cumOverride; // 누적 조직 수수료 + private BigDecimal cumIncentive; // 누적 시책 지급액 + private LocalDateTime refreshedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/kpi/KpiMonthlySummaryResp.java b/ga-core/src/main/java/com/ga/core/vo/kpi/KpiMonthlySummaryResp.java new file mode 100644 index 0000000..e45549c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/kpi/KpiMonthlySummaryResp.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.kpi; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 월별 정산 요약 KPI 응답 VO (mv_monthly_summary 기반) + * V37 Materialized View 조회 전용 — INSERT/UPDATE 없음. + */ +@Data +public class KpiMonthlySummaryResp { + private String yyyymm; // 정산년월 (YYYYMM) + private Long agentCount; // 확정/지급 완료 설계사 수 + private BigDecimal totalRecruit; // 신계약 수수료 합계 + private BigDecimal totalMaintain; // 유지 수수료 합계 + private BigDecimal totalOverride; // 조직 수수료 합계 + private BigDecimal totalChargeback; // 환수 합계 + private BigDecimal totalGross; // 총 수수료 (세전) + private BigDecimal totalTax; // 총 원천세 + private BigDecimal totalNet; // 총 순지급액 (세후) + private BigDecimal totalIncentive; // 지급 완료 시책 합계 + private LocalDateTime refreshedAt; // MV 마지막 갱신 시각 +} diff --git a/ga-core/src/main/java/com/ga/core/vo/kpi/KpiOrgSummaryResp.java b/ga-core/src/main/java/com/ga/core/vo/kpi/KpiOrgSummaryResp.java new file mode 100644 index 0000000..2184404 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/kpi/KpiOrgSummaryResp.java @@ -0,0 +1,26 @@ +package com.ga.core.vo.kpi; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 조직별 월별 요약 KPI 응답 VO (mv_org_summary 기반) + * V37 Materialized View 조회 전용. + */ +@Data +public class KpiOrgSummaryResp { + private Long orgId; + private String orgName; + private String orgType; + private String yyyymm; // 정산년월 (YYYYMM) + private Long agentCount; // 조직의 월 확정/지급 설계사 수 + private BigDecimal totalRecruit; + private BigDecimal totalMaintain; + private BigDecimal totalOverride; + private BigDecimal totalChargeback; + private BigDecimal totalGross; // 조직 소속 총 수수료 합계 (세전) + private BigDecimal totalNet; // 조직 소속 총 순지급액 합계 + private LocalDateTime refreshedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/kpi/KpiRetentionResp.java b/ga-core/src/main/java/com/ga/core/vo/kpi/KpiRetentionResp.java new file mode 100644 index 0000000..1c0c686 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/kpi/KpiRetentionResp.java @@ -0,0 +1,24 @@ +package com.ga.core.vo.kpi; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 계약 유지율 KPI 응답 VO (mv_retention 기반) + * V37 Materialized View 조회 전용. + * 13개월/25개월 유지율 지표. + */ +@Data +public class KpiRetentionResp { + private String contractMonth; // 계약 기준월 (YYYYMM) + private Long carrierId; // 보험사 ID + private String carrierName; // 보험사명 + private Long totalContracts; // 기준월 전체 유효 계약 수 + private Long retained13m; // 13개월 시점 유지 계약 수 + private Long retained25m; // 25개월 시점 유지 계약 수 + private BigDecimal retentionRate13m; // 13개월 유지율 (%) + private BigDecimal retentionRate25m; // 25개월 유지율 (%) + private LocalDateTime refreshedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/kpi/KpiSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/kpi/KpiSearchParam.java new file mode 100644 index 0000000..a606e39 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/kpi/KpiSearchParam.java @@ -0,0 +1,20 @@ +package com.ga.core.vo.kpi; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * KPI 공통 검색 파라미터 + * 4개 MV 조회 메서드가 공통으로 사용. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class KpiSearchParam extends SearchParam { + private String yyyymmFrom; // 정산년월 시작 (YYYYMM) + private String yyyymmTo; // 정산년월 종료 (YYYYMM) + private Long orgId; + private Long agentId; + private Long carrierId; // 유지율 조회 시 보험사 필터 + private String levelType; // 누계 조회 시 AGENT / ORG +} diff --git a/ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseResp.java b/ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseResp.java new file mode 100644 index 0000000..5267514 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseResp.java @@ -0,0 +1,23 @@ +package com.ga.core.vo.license; + +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class AgentLicenseResp { + private Long licenseId; + private Long agentId; + private String agentName; + private String orgName; + private String licenseType; + private String licenseTypeName; + private String licenseNo; + private LocalDate issuedDate; + private LocalDate expiresDate; + private Long renewedFrom; + private String status; + private String statusName; + /** 만료까지 남은 일수 (뷰 계산 필드) */ + private Integer daysUntilExpiry; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseSaveReq.java new file mode 100644 index 0000000..0ed7d33 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseSaveReq.java @@ -0,0 +1,22 @@ +package com.ga.core.vo.license; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class AgentLicenseSaveReq { + @NotNull + private Long agentId; + @NotBlank + private String licenseType; + @NotBlank + private String licenseNo; + @NotNull + private LocalDate issuedDate; + @NotNull + private LocalDate expiresDate; + private Long renewedFrom; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseSearchParam.java new file mode 100644 index 0000000..d062a18 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseSearchParam.java @@ -0,0 +1,12 @@ +package com.ga.core.vo.license; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AgentLicenseSearchParam extends SearchParam { + private Long agentId; + private String licenseType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseVO.java b/ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseVO.java new file mode 100644 index 0000000..6ef0cd5 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/license/AgentLicenseVO.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.license; + +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * agent_license — 설계사 자격 관리 (V46) + * 갱신 주기 5년 기준. expires_date 기준 만료 임박 알림 연동. + */ +@Data +public class AgentLicenseVO { + private Long licenseId; + private Long agentId; + /** LicenseType: LIFE/NONLIFE/VARIABLE */ + private String licenseType; + private String licenseNo; + private LocalDate issuedDate; + private LocalDate expiresDate; + /** 갱신 전 자격증 ID (자기 참조 FK) */ + private Long renewedFrom; + /** LicenseStatus: ACTIVE/EXPIRED/SUSPENDED */ + private String status; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/license/LicenseStatus.java b/ga-core/src/main/java/com/ga/core/vo/license/LicenseStatus.java new file mode 100644 index 0000000..7637d38 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/license/LicenseStatus.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.license; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 자격증 상태 (V46 agent_license.status) + */ +public enum LicenseStatus { + ACTIVE("유효"), + EXPIRED("만료"), + SUSPENDED("정지"); + + private final String label; + + LicenseStatus(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/license/LicenseType.java b/ga-core/src/main/java/com/ga/core/vo/license/LicenseType.java new file mode 100644 index 0000000..3da6cfb --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/license/LicenseType.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.license; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 자격증 종류 (V46 agent_license.license_type) + */ +public enum LicenseType { + LIFE("생명보험"), + NONLIFE("손해보험"), + VARIABLE("변액보험"); + + private final String label; + + LicenseType(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/notice/NoticeResp.java b/ga-core/src/main/java/com/ga/core/vo/notice/NoticeResp.java new file mode 100644 index 0000000..6ea8b35 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/notice/NoticeResp.java @@ -0,0 +1,19 @@ +package com.ga.core.vo.notice; + +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +public class NoticeResp { + private Long noticeId; + private String title; + private String content; + private String noticeType; + private String noticeTypeName; + private LocalDate startDate; + private LocalDate endDate; + private Boolean isActive; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/notice/NoticeSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/notice/NoticeSaveReq.java new file mode 100644 index 0000000..a51b676 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/notice/NoticeSaveReq.java @@ -0,0 +1,20 @@ +package com.ga.core.vo.notice; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class NoticeSaveReq { + @NotBlank + private String title; + @NotBlank + private String content; + @NotBlank + private String noticeType; + @NotNull + private LocalDate startDate; + private LocalDate endDate; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/notice/NoticeSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/notice/NoticeSearchParam.java new file mode 100644 index 0000000..9e8ffa4 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/notice/NoticeSearchParam.java @@ -0,0 +1,12 @@ +package com.ga.core.vo.notice; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeSearchParam extends SearchParam { + private String noticeType; + private Boolean isActive; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/notice/NoticeType.java b/ga-core/src/main/java/com/ga/core/vo/notice/NoticeType.java new file mode 100644 index 0000000..11ddb9e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/notice/NoticeType.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.notice; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 공지사항 유형 (V51 notice.notice_type) + */ +public enum NoticeType { + SYSTEM("시스템공지"), + POLICY("정책공지"), + EVENT("이벤트"); + + private final String label; + + NoticeType(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/notice/NoticeVO.java b/ga-core/src/main/java/com/ga/core/vo/notice/NoticeVO.java new file mode 100644 index 0000000..c28a0c3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/notice/NoticeVO.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.notice; + +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * notice — 공지사항 (V51) + */ +@Data +public class NoticeVO { + private Long noticeId; + private String title; + private String content; + /** NoticeType: SYSTEM/POLICY/EVENT */ + private String noticeType; + private LocalDate startDate; + private LocalDate endDate; + private Boolean isActive; + private Long createdBy; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private Long updatedBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/notice/NotificationRefType.java b/ga-core/src/main/java/com/ga/core/vo/notice/NotificationRefType.java new file mode 100644 index 0000000..d998556 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/notice/NotificationRefType.java @@ -0,0 +1,31 @@ +package com.ga.core.vo.notice; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 사용자 알림 참조 업무 유형 (V51 user_notification.ref_type) + */ +public enum NotificationRefType { + SETTLE("정산"), + DISPUTE("이의신청"), + APPROVAL("결재"), + COMPLAINT("민원"), + INCENTIVE("시책"), + WITHDRAW("출금신청"), + NOTICE("공지"); + + private final String label; + + NotificationRefType(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationResp.java b/ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationResp.java new file mode 100644 index 0000000..b0598b3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationResp.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.notice; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class UserNotificationResp { + private Long notificationId; + private Long userId; + private String title; + private String content; + private String refType; + private Long refId; + private Boolean isRead; + private LocalDateTime readAt; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationSaveReq.java new file mode 100644 index 0000000..814369e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationSaveReq.java @@ -0,0 +1,16 @@ +package com.ga.core.vo.notice; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class UserNotificationSaveReq { + @NotNull + private Long userId; + @NotBlank + private String title; + private String content; + private String refType; + private Long refId; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationSearchParam.java new file mode 100644 index 0000000..da91490 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationSearchParam.java @@ -0,0 +1,13 @@ +package com.ga.core.vo.notice; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class UserNotificationSearchParam extends SearchParam { + private Long userId; + private Boolean isRead; + private String refType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationVO.java b/ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationVO.java new file mode 100644 index 0000000..17c2d86 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/notice/UserNotificationVO.java @@ -0,0 +1,22 @@ +package com.ga.core.vo.notice; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * user_notification — 사용자 개인 알림 (V51) + */ +@Data +public class UserNotificationVO { + private Long notificationId; + private Long userId; + private String title; + private String content; + /** NotificationRefType */ + private String refType; + private Long refId; + private Boolean isRead; + private LocalDateTime readAt; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutResp.java b/ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutResp.java new file mode 100644 index 0000000..67a821c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutResp.java @@ -0,0 +1,33 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 보험사 회신 API 응답 VO + * 보험사명, 상태명 조인 포함. + */ +@Data +public class CarrierReconciliationOutResp { + private Long reconOutId; + private Long carrierId; + private String carrierName; // insurance_company 조인 + private String settleMonth; + private String filePath; + private String fileName; + private Long fileSize; + private Integer recordCount; + private String status; + private String statusName; // common_code 조인 + private LocalDateTime sentAt; + private LocalDateTime ackAt; + private String ackMessage; + private String failReason; + private Integer retryCount; + private LocalDateTime lastRetryAt; + private LocalDateTime createdAt; + private Long createdBy; + private String createdByName; // users 조인 + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutSaveReq.java new file mode 100644 index 0000000..16d6056 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutSaveReq.java @@ -0,0 +1,31 @@ +package com.ga.core.vo.settle; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 보험사 회신 전송 등록 요청 VO. + * 배치에서 파일 생성 후 전송 이력 등록 시 사용. + */ +@Data +public class CarrierReconciliationOutSaveReq { + + @NotNull(message = "보험사 ID는 필수입니다") + private Long carrierId; + + @NotBlank(message = "정산월은 필수입니다") + @Pattern(regexp = "\\d{6}", message = "정산월은 6자리 숫자(YYYYMM)여야 합니다") + private String settleMonth; + + @Size(max = 1000) + private String filePath; + + @Size(max = 300) + private String fileName; + + private Long fileSize; + private Integer recordCount; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutSearchParam.java new file mode 100644 index 0000000..272f52e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutSearchParam.java @@ -0,0 +1,16 @@ +package com.ga.core.vo.settle; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 보험사 회신 전송 이력 검색 파라미터 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CarrierReconciliationOutSearchParam extends SearchParam { + private Long carrierId; + private String settleMonth; + // status (READY/SENT/ACK/FAIL): SearchParam.status 사용 +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutVO.java b/ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutVO.java new file mode 100644 index 0000000..40b2256 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/CarrierReconciliationOutVO.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 보험사 회신 아웃바운드 VO (carrier_reconciliation_out 테이블 1:1) + * INSERT/UPDATE 전용. + */ +@Data +public class CarrierReconciliationOutVO { + private Long reconOutId; + private Long carrierId; // insurance_company.company_id + private String settleMonth; // YYYYMM + private String filePath; // 전송 파일 경로 (SFTP/S3) + private String fileName; // 파일명 + private Long fileSize; // 파일 크기 (bytes) + private Integer recordCount; // 레코드 수 + private String status; // READY/SENT/ACK/FAIL + private LocalDateTime sentAt; // 전송 일시 + private LocalDateTime ackAt; // 보험사 수신 확인 일시 + private String ackMessage; // 보험사 회신 메시지 + private String failReason; // 실패 사유 + private Integer retryCount; // 재시도 횟수 + private LocalDateTime lastRetryAt; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/CloseStatus.java b/ga-core/src/main/java/com/ga/core/vo/settle/CloseStatus.java new file mode 100644 index 0000000..c58597d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/CloseStatus.java @@ -0,0 +1,14 @@ +package com.ga.core.vo.settle; + +/** + * 정산 마감 상태 코드 (common_code_group: CLOSE_STATUS, V29 기준) + * DB period_close: CLOSED / REOPENED (reopen_at NULL 여부로 판단) + */ +public enum CloseStatus { + CLOSED, // 마감 완료 — 수정 불가 + REOPENED; // 재오픈 — 수정 가능 + + public String code() { + return this.name(); + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeResp.java b/ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeResp.java new file mode 100644 index 0000000..0cc82d0 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeResp.java @@ -0,0 +1,37 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 이의신청 API 응답 VO + * 설계사명, 담당자명, 상태명 등 조인 결과 포함. + */ +@Data +public class CommissionDisputeResp { + private Long disputeId; + private Long agentId; + private String agentName; // agent 조인 + private String orgName; // organization 조인 + private String settleMonth; + private Long settleId; + private String status; + private String statusName; // common_code 조인 + private BigDecimal disputedAmount; + private BigDecimal claimAmount; + private String reason; + private String evidenceUrl; + private Long reviewerId; + private String reviewerName; // users 조인 + private LocalDateTime reviewedAt; + private Long resolvedBy; + private String resolvedByName; // users 조인 + private LocalDateTime resolvedAt; + private String resolution; + private BigDecimal adjustmentAmount; + private Long correctionId; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeSaveReq.java new file mode 100644 index 0000000..d22e253 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeSaveReq.java @@ -0,0 +1,37 @@ +package com.ga.core.vo.settle; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 이의신청 등록 요청 VO. + * 설계사가 직접 제출하는 최소 입력 필드. + */ +@Data +public class CommissionDisputeSaveReq { + + @NotNull(message = "설계사 ID는 필수입니다") + private Long agentId; + + @NotBlank(message = "정산월은 필수입니다") + @Pattern(regexp = "\\d{6}", message = "정산월은 6자리 숫자(YYYYMM)여야 합니다") + private String settleMonth; + + private Long settleId; // 연관 정산 (선택) + + private BigDecimal disputedAmount; // 이의 제기 금액 (NULL=전체) + + private BigDecimal claimAmount; // 설계사 주장 금액 + + @NotBlank(message = "이의 신청 사유는 필수입니다") + @Size(max = 2000, message = "이의 신청 사유는 2000자 이하여야 합니다") + private String reason; + + @Size(max = 500) + private String evidenceUrl; // 증빙 자료 URL +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeSearchParam.java new file mode 100644 index 0000000..abb14d0 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeSearchParam.java @@ -0,0 +1,17 @@ +package com.ga.core.vo.settle; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 이의신청 검색 파라미터 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CommissionDisputeSearchParam extends SearchParam { + private Long agentId; + private Long orgId; + private String settleMonth; + // status (OPEN/IN_REVIEW/APPROVED/REJECTED): SearchParam.status 사용 +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeVO.java b/ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeVO.java new file mode 100644 index 0000000..a15b0e9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/CommissionDisputeVO.java @@ -0,0 +1,32 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 수수료 이의신청 VO (commission_dispute 테이블 1:1) + * INSERT/UPDATE 전용. + */ +@Data +public class CommissionDisputeVO { + private Long disputeId; + private Long agentId; + private String settleMonth; // YYYYMM + private Long settleId; // 연관 정산 (선택) + private String status; // OPEN/IN_REVIEW/APPROVED/REJECTED + private BigDecimal disputedAmount; // 이의 제기 금액 (NULL=전체) + private BigDecimal claimAmount; // 설계사 주장 금액 + private String reason; // 이의 신청 사유 + private String evidenceUrl; // 증빙 자료 URL + private Long reviewerId; // 검토 담당자 + private LocalDateTime reviewedAt; // 검토 시작 일시 + private Long resolvedBy; // 최종 처리자 + private LocalDateTime resolvedAt; // 최종 처리 일시 + private String resolution; // 처리 결과 상세 + private BigDecimal adjustmentAmount; // 인정된 조정 금액 (APPROVED 시) + private Long correctionId; // 연계 정정건 + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/CorrectionStatus.java b/ga-core/src/main/java/com/ga/core/vo/settle/CorrectionStatus.java new file mode 100644 index 0000000..fdf7d9c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/CorrectionStatus.java @@ -0,0 +1,16 @@ +package com.ga.core.vo.settle; + +/** + * 정산 정정 상태 코드 (common_code_group: CORRECTION_STATUS, V30 기준) + * DB settle_correction.status: DRAFT / APPROVED / APPLIED / CANCELLED + */ +public enum CorrectionStatus { + DRAFT, // 정정 신청 초안 + APPROVED, // 정정 승인 완료 + APPLIED, // 정정 차액 정산 반영 완료 + CANCELLED; // 정정 신청 취소 + + public String code() { + return this.name(); + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/DisputeStatus.java b/ga-core/src/main/java/com/ga/core/vo/settle/DisputeStatus.java new file mode 100644 index 0000000..51ed848 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/DisputeStatus.java @@ -0,0 +1,16 @@ +package com.ga.core.vo.settle; + +/** + * 이의신청 상태 코드 (common_code_group: DISPUTE_STATUS, V31 기준) + * DB commission_dispute.status: OPEN / IN_REVIEW / APPROVED / REJECTED + */ +public enum DisputeStatus { + OPEN, // 접수 — 검토 전 + IN_REVIEW, // 담당자 검토 진행 중 + APPROVED, // 인정 — 정정 처리 진행 + REJECTED; // 기각 — 원 정산 유지 + + public String code() { + return this.name(); + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePayStatus.java b/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePayStatus.java new file mode 100644 index 0000000..eb77143 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePayStatus.java @@ -0,0 +1,16 @@ +package com.ga.core.vo.settle; + +/** + * 시책 지급 상태 (incentive_payment.status, V33 기준) + * CALCULATED / CONFIRMED / PAID / CANCELLED + */ +public enum IncentivePayStatus { + CALCULATED, // 시책 금액 산출 완료 + CONFIRMED, // 지급 확정 + PAID, // 정산 반영 완료 + CANCELLED; // 시책 지급 취소 + + public String code() { + return this.name(); + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePayType.java b/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePayType.java new file mode 100644 index 0000000..d0f7551 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePayType.java @@ -0,0 +1,14 @@ +package com.ga.core.vo.settle; + +/** + * 시책 지급 방식 (incentive_program.pay_type, V33 기준) + * RATE=수수료 대비 비율 / FIXED_AMOUNT=건당 고정액 + */ +public enum IncentivePayType { + RATE, // 수수료 대비 비율 지급 + FIXED_AMOUNT; // 건당 고정액 지급 + + public String code() { + return this.name(); + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentResp.java b/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentResp.java new file mode 100644 index 0000000..55f64b3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentResp.java @@ -0,0 +1,34 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 시책 지급 API 응답 VO + * 프로그램명, 설계사명, 상태명 등 조인 포함. + */ +@Data +public class IncentivePaymentResp { + private Long incentivePayId; + private Long programId; + private String programName; // incentive_program 조인 + private String payType; // incentive_program 조인 + private Long agentId; + private String agentName; // agent 조인 + private String orgName; // organization 조인 + private String settleMonth; + private BigDecimal baseAmount; + private BigDecimal incentiveAmount; + private String status; + private String statusName; // common_code 조인 + private LocalDateTime calcDate; + private Long confirmedBy; + private String confirmedByName; // users 조인 + private LocalDateTime confirmedAt; + private Long settleId; + private String cancelReason; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentSaveReq.java new file mode 100644 index 0000000..04c32d3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentSaveReq.java @@ -0,0 +1,31 @@ +package com.ga.core.vo.settle; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 시책 지급 등록 요청 VO. + * 배치/서비스에서 산출 후 등록 시 사용. + */ +@Data +public class IncentivePaymentSaveReq { + + @NotNull(message = "시책 프로그램 ID는 필수입니다") + private Long programId; + + @NotNull(message = "설계사 ID는 필수입니다") + private Long agentId; + + @NotBlank(message = "정산월은 필수입니다") + @Pattern(regexp = "\\d{6}", message = "정산월은 6자리 숫자(YYYYMM)여야 합니다") + private String settleMonth; + + private BigDecimal baseAmount; // RATE 방식 시 기준 수수료 + + @NotNull(message = "시책 지급액은 필수입니다") + private BigDecimal incentiveAmount; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentSearchParam.java new file mode 100644 index 0000000..d82d3ad --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentSearchParam.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.settle; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 시책 지급 검색 파라미터 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class IncentivePaymentSearchParam extends SearchParam { + private Long programId; + private Long agentId; + private Long orgId; + private String settleMonth; + // status (CALCULATED/CONFIRMED/PAID/CANCELLED): SearchParam.status 사용 +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentVO.java b/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentVO.java new file mode 100644 index 0000000..332d83f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/IncentivePaymentVO.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 시책 지급 이력 VO (incentive_payment 테이블 1:1) + * INSERT/UPDATE 전용. + */ +@Data +public class IncentivePaymentVO { + private Long incentivePayId; + private Long programId; + private Long agentId; + private String settleMonth; // YYYYMM + private BigDecimal baseAmount; // 기준 수수료 (RATE 방식) + private BigDecimal incentiveAmount; // 시책 지급액 + private String status; // CALCULATED/CONFIRMED/PAID/CANCELLED + private LocalDateTime calcDate; // 산출 일시 + private Long confirmedBy; + private LocalDateTime confirmedAt; + private Long settleId; // 반영된 settle_master 참조 + private String cancelReason; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramResp.java b/ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramResp.java new file mode 100644 index 0000000..b8236c6 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramResp.java @@ -0,0 +1,36 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 시책 프로그램 API 응답 VO + * 직급명, 조직명, 코드명 조인 포함. + */ +@Data +public class IncentiveProgramResp { + private Long programId; + private String programName; + private String programType; + private String programTypeName; // common_code 조인 + private String targetType; + private Long targetGradeId; + private String targetGradeName; // grade 조인 + private Long targetOrgId; + private String targetOrgName; // organization 조인 + private String productCategory; + private String startMonth; + private String endMonth; + private String conditionDesc; + private BigDecimal payRate; + private BigDecimal payAmountFixed; + private String payType; + private BigDecimal budgetAmount; + private Boolean isActive; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; + private Long updatedBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramSaveReq.java new file mode 100644 index 0000000..20c0024 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramSaveReq.java @@ -0,0 +1,49 @@ +package com.ga.core.vo.settle; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 시책 프로그램 등록/수정 요청 VO + */ +@Data +public class IncentiveProgramSaveReq { + + @NotBlank(message = "시책명은 필수입니다") + @Size(max = 200) + private String programName; + + @NotBlank(message = "시책 유형은 필수입니다") + private String programType; + + @NotBlank(message = "지급 대상 구분은 필수입니다") + private String targetType; // AGENT/ORG/GRADE + + private Long targetGradeId; + private Long targetOrgId; + private String productCategory; + + @NotBlank(message = "시책 시작월은 필수입니다") + @Pattern(regexp = "\\d{6}", message = "시작월은 6자리 숫자(YYYYMM)여야 합니다") + private String startMonth; + + @NotBlank(message = "시책 종료월은 필수입니다") + @Pattern(regexp = "\\d{6}", message = "종료월은 6자리 숫자(YYYYMM)여야 합니다") + private String endMonth; + + @Size(max = 2000) + private String conditionDesc; + + private BigDecimal payRate; // RATE 방식 시 지급률 + private BigDecimal payAmountFixed; // FIXED_AMOUNT 방식 시 고정액 + + @NotBlank(message = "지급 방식은 필수입니다") + private String payType; // RATE / FIXED_AMOUNT + + private BigDecimal budgetAmount; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramSearchParam.java new file mode 100644 index 0000000..532930c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramSearchParam.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.settle; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 시책 프로그램 검색 파라미터 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class IncentiveProgramSearchParam extends SearchParam { + private String programType; + private String targetType; + private String settleMonth; // 이 월이 start_month~end_month 범위에 포함되는 프로그램 조회 + private Boolean isActive; + // status 불필요 (is_active Boolean 으로 관리) +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramVO.java b/ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramVO.java new file mode 100644 index 0000000..7e9362a --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/IncentiveProgramVO.java @@ -0,0 +1,33 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 시책 프로그램 마스터 VO (incentive_program 테이블 1:1) + * INSERT/UPDATE 전용. + */ +@Data +public class IncentiveProgramVO { + private Long programId; + private String programName; // 시책명 + private String programType; // INCENTIVE_TYPE 코드 + private String targetType; // AGENT/ORG/GRADE + private Long targetGradeId; // 대상 직급 (NULL=전체) + private Long targetOrgId; // 대상 조직 (NULL=전체) + private String productCategory; // 적용 상품 카테고리 (NULL=전체) + private String startMonth; // YYYYMM — 시책 시작월 + private String endMonth; // YYYYMM — 시책 종료월 + private String conditionDesc; // 지급 조건 상세 + private BigDecimal payRate; // 지급률 (%) — RATE 방식 + private BigDecimal payAmountFixed; // 고정 지급액 — FIXED_AMOUNT 방식 + private String payType; // RATE / FIXED_AMOUNT + private BigDecimal budgetAmount; // 시책 예산 (NULL=무제한) + private Boolean isActive; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; + private Long updatedBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseResp.java b/ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseResp.java new file mode 100644 index 0000000..bf0b926 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseResp.java @@ -0,0 +1,26 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 정산 마감 API 응답 VO + * 조인 결과(담당자명) 포함, 화면 표시용. + */ +@Data +public class PeriodCloseResp { + private Long closeId; + private String closeMonth; // YYYYMM + private LocalDateTime closedAt; + private Long closedBy; + private String closedByName; // users.user_name 조인 + private LocalDateTime reopenAt; + private Long reopenBy; + private String reopenByName; // users.user_name 조인 + private String reopenReason; + private String note; + private String closeStatus; // CLOSED / REOPENED (reopen_at IS NULL → CLOSED) + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseSaveReq.java new file mode 100644 index 0000000..ac22ff5 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseSaveReq.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.settle; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 마감 등록/재오픈 요청 VO. + * closeMonth 필수, 나머지는 재오픈 시에만 사용. + */ +@Data +public class PeriodCloseSaveReq { + + @NotBlank(message = "정산월은 필수입니다") + @Pattern(regexp = "\\d{6}", message = "정산월은 6자리 숫자(YYYYMM)여야 합니다") + private String closeMonth; + + @Size(max = 500, message = "비고는 500자 이하여야 합니다") + private String note; + + // 재오픈 시에만 사용 + @Size(max = 500, message = "재오픈 사유는 500자 이하여야 합니다") + private String reopenReason; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseSearchParam.java new file mode 100644 index 0000000..2d6424e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseSearchParam.java @@ -0,0 +1,17 @@ +package com.ga.core.vo.settle; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 마감 마스터 검색 파라미터 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PeriodCloseSearchParam extends SearchParam { + private String closeMonthFrom; // YYYYMM 시작 + private String closeMonthTo; // YYYYMM 종료 + // 마감 상태: CLOSED / REOPENED — 부모의 status 필드 사용 + // (SearchParam.status 그대로 활용) +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseVO.java b/ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseVO.java new file mode 100644 index 0000000..ece4a6a --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/PeriodCloseVO.java @@ -0,0 +1,23 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 정산 마감 마스터 VO (period_close 테이블 1:1) + * INSERT/UPDATE 전용. 조인 결과 미포함. + */ +@Data +public class PeriodCloseVO { + private Long closeId; + private String closeMonth; // YYYYMM — 마감 정산년월 + private LocalDateTime closedAt; // 마감 처리 일시 + private Long closedBy; // 마감 처리자 user_id + private LocalDateTime reopenAt; // 재오픈 일시 (NULL = 현재 마감) + private Long reopenBy; // 재오픈 처리자 user_id + private String reopenReason; // 재오픈 사유 (감사 추적) + private String note; // 마감 비고 + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/ReconStatus.java b/ga-core/src/main/java/com/ga/core/vo/settle/ReconStatus.java new file mode 100644 index 0000000..d480fa2 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/ReconStatus.java @@ -0,0 +1,16 @@ +package com.ga.core.vo.settle; + +/** + * 보험사 회신 전송 상태 (carrier_reconciliation_out.status, V36 기준) + * READY / SENT / ACK / FAIL + */ +public enum ReconStatus { + READY, // 파일 생성 완료, 전송 대기 중 + SENT, // 보험사로 파일 전송 완료 (확인 전) + ACK, // 보험사 수신 확인 완료 + FAIL; // 파일 전송 실패 — 재시도 필요 + + public String code() { + return this.name(); + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionResp.java b/ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionResp.java new file mode 100644 index 0000000..5f6736a --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionResp.java @@ -0,0 +1,39 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 정산 정정 API 응답 VO + * 설계사명, 승인자명 등 조인 결과 포함. + */ +@Data +public class SettleCorrectionResp { + private Long correctionId; + private Long originalSettleId; + private Long agentId; + private String agentName; // agent 조인 + private String orgName; // organization 조인 + private String settleMonth; + private BigDecimal diffAmount; + private BigDecimal beforeAmount; + private BigDecimal afterAmount; + private String reasonCode; + private String reasonCodeName; // common_code 조인 + private String reasonDetail; + private String status; + private String statusName; // common_code 조인 + private Long requestedBy; + private String requestedByName; // users 조인 + private LocalDateTime requestedAt; + private Long approvedBy; + private String approvedByName; // users 조인 + private LocalDateTime approvedAt; + private Long appliedSettleId; + private String cancelReason; + private LocalDateTime cancelledAt; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionSaveReq.java new file mode 100644 index 0000000..d180354 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionSaveReq.java @@ -0,0 +1,39 @@ +package com.ga.core.vo.settle; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 정산 정정 등록 요청 VO. + * ID, 상태, 일시 필드는 제외. + */ +@Data +public class SettleCorrectionSaveReq { + + @NotNull(message = "원본 정산 ID는 필수입니다") + private Long originalSettleId; + + @NotNull(message = "설계사 ID는 필수입니다") + private Long agentId; + + @NotBlank(message = "정산월은 필수입니다") + @Pattern(regexp = "\\d{6}", message = "정산월은 6자리 숫자(YYYYMM)여야 합니다") + private String settleMonth; + + @NotNull(message = "정정 전 금액은 필수입니다") + private BigDecimal beforeAmount; + + @NotNull(message = "정정 후 금액은 필수입니다") + private BigDecimal afterAmount; + + @NotBlank(message = "정정 사유 코드는 필수입니다") + private String reasonCode; + + @Size(max = 1000, message = "정정 사유 상세는 1000자 이하여야 합니다") + private String reasonDetail; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionSearchParam.java new file mode 100644 index 0000000..2c3045d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionSearchParam.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.settle; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 정산 정정 검색 파라미터 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SettleCorrectionSearchParam extends SearchParam { + private Long agentId; + private Long orgId; + private String settleMonth; + // status (DRAFT/APPROVED/APPLIED/CANCELLED): SearchParam.status 사용 + private String reasonCode; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionVO.java b/ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionVO.java new file mode 100644 index 0000000..ed84bc0 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/SettleCorrectionVO.java @@ -0,0 +1,34 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 정산 정정 이력 VO (settle_correction 테이블 1:1) + * INSERT/UPDATE 전용. + */ +@Data +public class SettleCorrectionVO { + private Long correctionId; + private Long originalSettleId; // 원본 settle_master 참조 + private Long agentId; + private String settleMonth; // YYYYMM + private BigDecimal diffAmount; // 차액 (양수=추가, 음수=환수) + private BigDecimal beforeAmount; // 정정 전 순지급액 + private BigDecimal afterAmount; // 정정 후 순지급액 + private String reasonCode; // CORRECTION_REASON 코드 + private String reasonDetail; // 정정 사유 상세 + private String status; // DRAFT/APPROVED/APPLIED/CANCELLED + private Long requestedBy; // 정정 신청자 + private LocalDateTime requestedAt; + private Long approvedBy; // 정정 승인자 + private LocalDateTime approvedAt; + private Long appliedSettleId; // 정정이 반영된 settle_id + private String cancelReason; + private Long cancelledBy; + private LocalDateTime cancelledAt; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/SettleMasterResp.java b/ga-core/src/main/java/com/ga/core/vo/settle/SettleMasterResp.java index 2b8514b..ff0298d 100644 --- a/ga-core/src/main/java/com/ga/core/vo/settle/SettleMasterResp.java +++ b/ga-core/src/main/java/com/ga/core/vo/settle/SettleMasterResp.java @@ -29,4 +29,6 @@ public class SettleMasterResp { private String confirmedByName; private LocalDateTime confirmedAt; private String holdReason; + private String holdReasonCode; // V32 추가: HOLD_REASON 코드 + private String holdReasonName; // common_code 조인: 보류 사유 코드명 } diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/SettleMasterVO.java b/ga-core/src/main/java/com/ga/core/vo/settle/SettleMasterVO.java index 806b6f3..eed2ba9 100644 --- a/ga-core/src/main/java/com/ga/core/vo/settle/SettleMasterVO.java +++ b/ga-core/src/main/java/com/ga/core/vo/settle/SettleMasterVO.java @@ -24,4 +24,5 @@ public class SettleMasterVO { private Long confirmedBy; private LocalDateTime confirmedAt; private String holdReason; + private String holdReasonCode; // V32 추가: HOLD_REASON 코드 (NEED_DOC/UNDER_REVIEW/DISPUTE_OPEN 등) } diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceResp.java b/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceResp.java new file mode 100644 index 0000000..80acb89 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceResp.java @@ -0,0 +1,42 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 세금계산서 API 응답 VO + * 설계사명, 발행자명 조인 포함. + */ +@Data +public class TaxInvoiceResp { + private Long invoiceId; + private String invoiceNo; + private String invoiceType; + private String invoiceTypeName; // common_code 조인 + private LocalDate issueDate; + private BigDecimal supplyAmount; + private BigDecimal taxAmount; + private BigDecimal totalAmount; + + private String counterpartName; + private String counterpartBizNo; + private String counterpartRep; + private String counterpartAddr; + + private Long agentId; + private String agentName; // agent 조인 + private String settleMonth; + private Long settleId; + + private String description; + private LocalDate cancelDate; + private String cancelReason; + private Long issuedBy; + private String issuedByName; // users 조인 + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceSaveReq.java new file mode 100644 index 0000000..163f941 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceSaveReq.java @@ -0,0 +1,57 @@ +package com.ga.core.vo.settle; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 세금계산서 발행 요청 VO + */ +@Data +public class TaxInvoiceSaveReq { + + @NotBlank(message = "발행 번호는 필수입니다") + @Size(max = 50) + private String invoiceNo; + + @NotBlank(message = "세금계산서 유형은 필수입니다") + private String invoiceType; // SUPPLY / PURCHASE + + @NotNull(message = "발행일은 필수입니다") + private LocalDate issueDate; + + @NotNull(message = "공급가액은 필수입니다") + private BigDecimal supplyAmount; + + @NotNull(message = "세액은 필수입니다") + private BigDecimal taxAmount; + + @NotBlank(message = "거래 상대방 상호는 필수입니다") + @Size(max = 200) + private String counterpartName; + + @NotBlank(message = "거래 상대방 사업자번호는 필수입니다") + @Size(max = 20) + private String counterpartBizNo; + + @Size(max = 100) + private String counterpartRep; + + @Size(max = 500) + private String counterpartAddr; + + private Long agentId; + + @Pattern(regexp = "\\d{6}", message = "정산월은 6자리 숫자(YYYYMM)여야 합니다") + private String settleMonth; + + private Long settleId; + + @Size(max = 500) + private String description; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceSearchParam.java new file mode 100644 index 0000000..a12be19 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceSearchParam.java @@ -0,0 +1,22 @@ +package com.ga.core.vo.settle; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDate; + +/** + * 세금계산서 검색 파라미터 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class TaxInvoiceSearchParam extends SearchParam { + private String invoiceType; // SUPPLY / PURCHASE + private String settleMonth; + private Long agentId; + private LocalDate issueDateFrom; + private LocalDate issueDateTo; + private String counterpartBizNo; + private Boolean cancelledOnly; // true=취소된 것만, false=유효한 것만, null=전체 +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceType.java b/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceType.java new file mode 100644 index 0000000..e14a2be --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceType.java @@ -0,0 +1,14 @@ +package com.ga.core.vo.settle; + +/** + * 세금계산서 유형 (tax_invoice.invoice_type, V35 기준) + * SUPPLY=매출(GA가 발행) / PURCHASE=매입(GA가 수취) + */ +public enum TaxInvoiceType { + SUPPLY, // 매출 세금계산서 — GA가 발행 + PURCHASE; // 매입 세금계산서 — GA가 수취 + + public String code() { + return this.name(); + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceVO.java b/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceVO.java new file mode 100644 index 0000000..c563b4b --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/TaxInvoiceVO.java @@ -0,0 +1,42 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 세금계산서 발행 이력 VO (tax_invoice 테이블 1:1) + * INSERT/UPDATE 전용. + */ +@Data +public class TaxInvoiceVO { + private Long invoiceId; + private String invoiceNo; // 발행 번호 (국세청 승인번호) + private String invoiceType; // SUPPLY / PURCHASE + private LocalDate issueDate; // 발행일 + private BigDecimal supplyAmount; // 공급가액 + private BigDecimal taxAmount; // 세액 (공급가 × 10%) + private BigDecimal totalAmount; // 합계액 (공급가 + 세액) + + // 거래 상대방 + private String counterpartName; // 거래 상대방 상호 + private String counterpartBizNo; // 거래 상대방 사업자번호 + private String counterpartRep; // 대표자명 + private String counterpartAddr; // 주소 + + // 내부 참조 + private Long agentId; // 관련 설계사 (선택) + private String settleMonth; // YYYYMM (선택) + private Long settleId; + + // 발행 메타 + private String description; // 품목/비고 + private LocalDate cancelDate; // 취소일 (NULL=유효) + private String cancelReason; + private Long issuedBy; // 발행 처리자 + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/TermSettleStatus.java b/ga-core/src/main/java/com/ga/core/vo/settle/TermSettleStatus.java new file mode 100644 index 0000000..26ae736 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/TermSettleStatus.java @@ -0,0 +1,16 @@ +package com.ga.core.vo.settle; + +/** + * 해촉 정산 상태 (termination_settlement.status, V34 기준) + * CALCULATING / CONFIRMED / PAID / CANCELLED + */ +public enum TermSettleStatus { + CALCULATING, // 해촉 정산 금액 산출 중 + CONFIRMED, // 해촉 정산 금액 확정 + PAID, // 최종 지급 완료 + CANCELLED; // 해촉 정산 취소 + + public String code() { + return this.name(); + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementResp.java b/ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementResp.java new file mode 100644 index 0000000..340103c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementResp.java @@ -0,0 +1,45 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 해촉 정산 API 응답 VO + * 설계사명, 조직명, 상태명 조인 포함. + */ +@Data +public class TerminationSettlementResp { + private Long termSettleId; + private Long agentId; + private String agentName; // agent 조인 + private String orgName; // organization 조인 + private LocalDate terminationDate; + private Long orgHistoryId; + private String calcMonth; + + private BigDecimal unpaidRecruitAmount; + private BigDecimal unpaidInstallmentAmount; + private BigDecimal unpaidIncentiveAmount; + private BigDecimal unpaidTotalAmount; // 미지급 합계 (계산값) + private BigDecimal chargebackExpectedAmount; + private BigDecimal chargebackWaivedAmount; + private BigDecimal deductionAmount; + private BigDecimal finalPayAmount; + + private String status; + private String statusName; // common_code 조인 + private String note; + private Long confirmedBy; + private String confirmedByName; // users 조인 + private LocalDateTime confirmedAt; + private LocalDateTime paidAt; + private Long paymentId; + + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; + private Long updatedBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementSaveReq.java new file mode 100644 index 0000000..34b0dbb --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementSaveReq.java @@ -0,0 +1,41 @@ +package com.ga.core.vo.settle; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 해촉 정산 등록 요청 VO. + * 배치/서비스에서 해촉 처리 시 사용. + */ +@Data +public class TerminationSettlementSaveReq { + + @NotNull(message = "설계사 ID는 필수입니다") + private Long agentId; + + @NotNull(message = "해촉 일자는 필수입니다") + private LocalDate terminationDate; + + private Long orgHistoryId; + + @NotBlank(message = "정산 처리 월은 필수입니다") + @Pattern(regexp = "\\d{6}", message = "정산월은 6자리 숫자(YYYYMM)여야 합니다") + private String calcMonth; + + private BigDecimal unpaidRecruitAmount; + private BigDecimal unpaidInstallmentAmount; + private BigDecimal unpaidIncentiveAmount; + private BigDecimal chargebackExpectedAmount; + private BigDecimal chargebackWaivedAmount; + private BigDecimal deductionAmount; + private BigDecimal finalPayAmount; + + @Size(max = 1000) + private String note; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementSearchParam.java new file mode 100644 index 0000000..ab1823d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementSearchParam.java @@ -0,0 +1,17 @@ +package com.ga.core.vo.settle; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 해촉 정산 검색 파라미터 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class TerminationSettlementSearchParam extends SearchParam { + private Long agentId; + private Long orgId; + private String calcMonth; + // status (CALCULATING/CONFIRMED/PAID/CANCELLED): SearchParam.status 사용 +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementVO.java b/ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementVO.java new file mode 100644 index 0000000..6a8725c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/settle/TerminationSettlementVO.java @@ -0,0 +1,47 @@ +package com.ga.core.vo.settle; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 해촉 정산 VO (termination_settlement 테이블 1:1) + * INSERT/UPDATE 전용. + */ +@Data +public class TerminationSettlementVO { + private Long termSettleId; + private Long agentId; + private LocalDate terminationDate; // 해촉 일자 + private Long orgHistoryId; // agent_org_history LEAVE 이력 참조 + private String calcMonth; // YYYYMM — 해촉 정산 처리 월 + + // 미지급 수수료 + private BigDecimal unpaidRecruitAmount; // 미지급 신계약 수수료 + private BigDecimal unpaidInstallmentAmount; // 미지급 분급 잔액 + private BigDecimal unpaidIncentiveAmount; // 미지급 시책 + + // 환수 + private BigDecimal chargebackExpectedAmount; // 환수 예정 총액 + private BigDecimal chargebackWaivedAmount; // 면제 환수액 + + // 공제 + private BigDecimal deductionAmount; // 공제 차감액 + + // 최종 지급 + private BigDecimal finalPayAmount; // 최종 지급액 + + private String status; // CALCULATING/CONFIRMED/PAID/CANCELLED + private String note; + private Long confirmedBy; + private LocalDateTime confirmedAt; + private LocalDateTime paidAt; + private Long paymentId; // payment 참조 + + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; + private Long updatedBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingResp.java b/ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingResp.java new file mode 100644 index 0000000..3c84e0e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingResp.java @@ -0,0 +1,21 @@ +package com.ga.core.vo.training; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class AgentTrainingResp { + private Long trainingId; + private Long agentId; + private String agentName; + private String orgName; + private String courseName; + private String courseType; + private String courseTypeName; + private LocalDate trainingDate; + private BigDecimal hours; + private String certificateNo; + private String instructor; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingSaveReq.java new file mode 100644 index 0000000..2b4acf5 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingSaveReq.java @@ -0,0 +1,26 @@ +package com.ga.core.vo.training; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class AgentTrainingSaveReq { + @NotNull + private Long agentId; + @NotBlank + private String courseName; + @NotBlank + private String courseType; + @NotNull + private LocalDate trainingDate; + @NotNull + @DecimalMin(value = "0", inclusive = false) + private BigDecimal hours; + private String certificateNo; + private String instructor; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingSearchParam.java new file mode 100644 index 0000000..8985247 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingSearchParam.java @@ -0,0 +1,13 @@ +package com.ga.core.vo.training; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AgentTrainingSearchParam extends SearchParam { + private Long agentId; + private String courseType; + private Integer year; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingVO.java b/ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingVO.java new file mode 100644 index 0000000..ec3930b --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/training/AgentTrainingVO.java @@ -0,0 +1,26 @@ +package com.ga.core.vo.training; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * agent_training — 설계사 보수교육 이수 이력 (V47) + * course_type: REGULAR/COMPLIANCE/PRODUCT + */ +@Data +public class AgentTrainingVO { + private Long trainingId; + private Long agentId; + private String courseName; + private String courseType; + private LocalDate trainingDate; + private BigDecimal hours; + private String certificateNo; + private String instructor; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementResp.java b/ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementResp.java new file mode 100644 index 0000000..23d86cf --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementResp.java @@ -0,0 +1,11 @@ +package com.ga.core.vo.training; + +import lombok.Data; + +@Data +public class TrainingRequirementResp { + private Integer reqId; + private Integer targetYear; + private Integer requiredHours; + private String description; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementSaveReq.java new file mode 100644 index 0000000..6b8181d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementSaveReq.java @@ -0,0 +1,15 @@ +package com.ga.core.vo.training; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class TrainingRequirementSaveReq { + @NotNull + private Integer targetYear; + @NotNull + @Min(1) + private Integer requiredHours; + private String description; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementSearchParam.java new file mode 100644 index 0000000..c8c1850 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementSearchParam.java @@ -0,0 +1,11 @@ +package com.ga.core.vo.training; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class TrainingRequirementSearchParam extends SearchParam { + private Integer targetYear; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementVO.java b/ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementVO.java new file mode 100644 index 0000000..0570c2f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/training/TrainingRequirementVO.java @@ -0,0 +1,21 @@ +package com.ga.core.vo.training; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * training_requirement — 연도별 의무 교육 시간 마스터 (V47) + * 보험업법 시행령 제17조 기준 12시간/년 기본값. + */ +@Data +public class TrainingRequirementVO { + private Integer reqId; + private Integer targetYear; + /** 연간 의무 교육 시간 (기본 12) */ + private Integer requiredHours; + private String description; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestResp.java b/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestResp.java new file mode 100644 index 0000000..812ea7a --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestResp.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.withdraw; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class WithdrawRequestResp { + private Long requestId; + private Long settleMasterId; + private String settleMonth; + private Long agentId; + private String agentName; + private BigDecimal amount; + private Long requestedBy; + private LocalDateTime requestedAt; + private Long approvalRequestId; + private String bankCode; + /** 마스킹 처리 권장 — 뒤 4자리만 노출 */ + private String accountNo; + private String status; + private String statusName; + private LocalDateTime sentAt; + private LocalDateTime completedAt; + private String failReason; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestSaveReq.java new file mode 100644 index 0000000..d5a318f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestSaveReq.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.withdraw; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class WithdrawRequestSaveReq { + @NotNull + private Long settleMasterId; + @NotNull + private Long agentId; + @NotNull + @Positive + private BigDecimal amount; + @NotNull + private Long requestedBy; + @NotBlank + private String bankCode; + @NotBlank + private String accountNo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestSearchParam.java new file mode 100644 index 0000000..1c21390 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestSearchParam.java @@ -0,0 +1,12 @@ +package com.ga.core.vo.withdraw; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class WithdrawRequestSearchParam extends SearchParam { + private Long agentId; + private Long settleMasterId; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestVO.java b/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestVO.java new file mode 100644 index 0000000..2a2f14f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawRequestVO.java @@ -0,0 +1,31 @@ +package com.ga.core.vo.withdraw; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * withdraw_request — 출금 신청 (V53) + * account_no 는 AES 암호화 대상 — Mapper XML에서 EncryptTypeHandler 적용. + */ +@Data +public class WithdrawRequestVO { + private Long requestId; + private Long settleMasterId; + private Long agentId; + private BigDecimal amount; + private Long requestedBy; + private LocalDateTime requestedAt; + private Long approvalRequestId; + private String bankCode; + /** AES 암호화 대상 (EncryptTypeHandler) */ + private String accountNo; + /** WithdrawStatus: REQUESTED/APPROVED/SENT/COMPLETED/FAILED/CANCELLED */ + private String status; + private LocalDateTime sentAt; + private LocalDateTime completedAt; + private String failReason; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawStatus.java b/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawStatus.java new file mode 100644 index 0000000..28c7cce --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/withdraw/WithdrawStatus.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.withdraw; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 출금 신청 상태 (V53 withdraw_request.status) + */ +public enum WithdrawStatus { + REQUESTED("신청"), + APPROVED("결재완료"), + SENT("펌뱅킹송신"), + COMPLETED("입금완료"), + FAILED("실패"), + CANCELLED("취소"); + + private final String label; + + WithdrawStatus(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/resources/mapper/approval/ApprovalHistoryMapper.xml b/ga-core/src/main/resources/mapper/approval/ApprovalHistoryMapper.xml new file mode 100644 index 0000000..edc89e7 --- /dev/null +++ b/ga-core/src/main/resources/mapper/approval/ApprovalHistoryMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + ah.history_id, ah.request_id, ah.step_no, + ah.approver_id, u.user_name AS approver_name, + ah.action, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'APPROVAL_ACTION' AND cc.code = ah.action) AS action_name, + ah.comment, ah.processed_at + + + + + + + + INSERT INTO approval_history + (request_id, step_no, approver_id, action, comment, processed_at) + VALUES + (#{requestId}, #{stepNo}, #{approverId}, #{action}, #{comment}, + COALESCE(#{processedAt}, NOW())) + + + diff --git a/ga-core/src/main/resources/mapper/approval/ApprovalLineMapper.xml b/ga-core/src/main/resources/mapper/approval/ApprovalLineMapper.xml new file mode 100644 index 0000000..c965dbe --- /dev/null +++ b/ga-core/src/main/resources/mapper/approval/ApprovalLineMapper.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO approval_line (line_code, line_name, max_step, target_type, is_active, created_by) + VALUES (#{lineCode}, #{lineName}, #{maxStep}, #{targetType}, TRUE, #{createdBy}) + + + + UPDATE approval_line + SET line_name = #{lineName}, + max_step = #{maxStep}, + updated_at = NOW() + WHERE line_id = #{lineId} + + + + UPDATE approval_line SET is_active = #{isActive}, updated_at = NOW() WHERE line_id = #{lineId} + + + diff --git a/ga-core/src/main/resources/mapper/approval/ApprovalLineStepMapper.xml b/ga-core/src/main/resources/mapper/approval/ApprovalLineStepMapper.xml new file mode 100644 index 0000000..1fad4bf --- /dev/null +++ b/ga-core/src/main/resources/mapper/approval/ApprovalLineStepMapper.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + INSERT INTO approval_line_step (line_id, step_no, role_code) + VALUES (#{lineId}, #{stepNo}, #{roleCode}) + ON CONFLICT (line_id, step_no) DO UPDATE SET role_code = EXCLUDED.role_code + + + + DELETE FROM approval_line_step WHERE step_id = #{stepId} + + + diff --git a/ga-core/src/main/resources/mapper/approval/ApprovalRequestMapper.xml b/ga-core/src/main/resources/mapper/approval/ApprovalRequestMapper.xml new file mode 100644 index 0000000..972e921 --- /dev/null +++ b/ga-core/src/main/resources/mapper/approval/ApprovalRequestMapper.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + ar.request_id, ar.target_type, ar.target_id, + ar.line_id, al.line_code, al.line_name, al.max_step, + ar.requested_by, ar.requested_at, ar.current_step, + ar.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'APPROVAL_STATUS' AND cc.code = ar.status) AS status_name, + ar.completed_at + + + + + + + + INSERT INTO approval_request + (target_type, target_id, line_id, requested_by, requested_at, current_step, status) + VALUES + (#{targetType}, #{targetId}, #{lineId}, #{requestedBy}, + COALESCE(#{requestedAt}, NOW()), 1, 'PENDING') + + + + + UPDATE approval_request + SET current_step = current_step + 1, + updated_at = NOW() + WHERE request_id = #{requestId} + AND status = 'PENDING' + + + + UPDATE approval_request + SET status = #{status}, + completed_at = CASE WHEN #{status} IN ('APPROVED','REJECTED','CANCEL') THEN NOW() ELSE completed_at END, + updated_at = NOW() + WHERE request_id = #{requestId} + + + + UPDATE approval_request + SET status = 'CANCEL', + completed_at = NOW(), + updated_at = NOW() + WHERE request_id = #{requestId} + AND status = 'PENDING' + + + diff --git a/ga-core/src/main/resources/mapper/attachment/AttachmentMapper.xml b/ga-core/src/main/resources/mapper/attachment/AttachmentMapper.xml new file mode 100644 index 0000000..82969ca --- /dev/null +++ b/ga-core/src/main/resources/mapper/attachment/AttachmentMapper.xml @@ -0,0 +1,58 @@ + + + + + + + + + a.attachment_id, a.ref_type, a.ref_id, + a.file_name, a.file_path, a.file_size, a.mime_type, + a.uploaded_by, u.user_name AS uploader_name, + a.uploaded_at + + + + + + + + + + + INSERT INTO attachment (ref_type, ref_id, file_name, file_path, file_size, mime_type, uploaded_by) + VALUES (#{refType}, #{refId}, #{fileName}, #{filePath}, #{fileSize}, #{mimeType}, #{uploadedBy}) + + + + DELETE FROM attachment WHERE attachment_id = #{attachmentId} + + + + DELETE FROM attachment WHERE ref_type = #{refType} AND ref_id = #{refId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/CollectionCommissionLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/CollectionCommissionLedgerMapper.xml new file mode 100644 index 0000000..e568841 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/CollectionCommissionLedgerMapper.xml @@ -0,0 +1,78 @@ + + + + + + + + + l.ledger_id, l.contract_id, ct.policy_no, + l.agent_id, a.agent_name, o.org_name, + l.settle_month, + l.premium_amount, l.commission_rate, l.commission_amount, + l.payment_cycle, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'PAYMENT_CYCLE' AND cc.code = l.payment_cycle) AS payment_cycle_name, + l.paid_at, + l.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'MASTER_STATUS' AND cc.code = l.status) AS status_name + + + + FROM collection_commission_ledger l + LEFT JOIN agent a ON a.agent_id = l.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + LEFT JOIN contract ct ON ct.contract_id = l.contract_id + + + + + + + + + + + INSERT INTO collection_commission_ledger + (contract_id, agent_id, settle_month, premium_amount, commission_rate, + commission_amount, payment_cycle, paid_at, status) + VALUES + (#{contractId}, #{agentId}, #{settleMonth}, #{premiumAmount}, #{commissionRate}, + #{commissionAmount}, #{paymentCycle}, #{paidAt}, + COALESCE(#{status}, 'CALCULATED')) + + + + UPDATE collection_commission_ledger + SET status = #{status}, updated_at = NOW() + WHERE ledger_id = #{ledgerId} + + + + DELETE FROM collection_commission_ledger WHERE ledger_id = #{ledgerId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/CollectionCommissionRuleMapper.xml b/ga-core/src/main/resources/mapper/commission/CollectionCommissionRuleMapper.xml new file mode 100644 index 0000000..076ea35 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/CollectionCommissionRuleMapper.xml @@ -0,0 +1,68 @@ + + + + + + + + + r.rule_id, r.product_id, p.product_name, + r.payment_cycle, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'PAYMENT_CYCLE' AND cc.code = r.payment_cycle) AS payment_cycle_name, + r.commission_rate, + r.effective_from, r.effective_to + + + + + + + + + + + INSERT INTO collection_commission_rule + (product_id, payment_cycle, commission_rate, effective_from, effective_to, created_by) + VALUES + (#{productId}, #{paymentCycle}, #{commissionRate}, #{effectiveFrom}, #{effectiveTo}, #{createdBy}) + + + + UPDATE collection_commission_rule + SET commission_rate = #{commissionRate}, + effective_to = #{effectiveTo}, + updated_at = NOW() + WHERE rule_id = #{ruleId} + + + + DELETE FROM collection_commission_rule WHERE rule_id = #{ruleId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/CommissionMasterMapper.xml b/ga-core/src/main/resources/mapper/commission/CommissionMasterMapper.xml new file mode 100644 index 0000000..fdda620 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/CommissionMasterMapper.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + cm.master_id, cm.agent_id, a.agent_name, + o.org_name, + cm.commission_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'COMMISSION_TYPE' AND cc.code = cm.commission_type) AS commission_type_name, + cm.settle_month, + cm.source_table, cm.source_id, + cm.gross_amount, cm.tax_amount, cm.net_amount, + cm.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'MASTER_STATUS' AND cc.code = cm.status) AS status_name, + cm.created_at + + + + + FROM commission_master cm + LEFT JOIN agent a ON a.agent_id = cm.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + + + + + + AND cm.agent_id = #{agentId} + AND cm.commission_type = #{commissionType} + AND cm.settle_month = #{settleMonth} + AND cm.source_table = #{sourceTable} + AND cm.status = #{status} + + + + + + + + + + + + + + + + + + INSERT INTO commission_master + (agent_id, commission_type, settle_month, source_table, source_id, + gross_amount, tax_amount, net_amount, status) + VALUES + (#{agentId}, #{commissionType}, #{settleMonth}, #{sourceTable}, #{sourceId}, + #{grossAmount}, #{taxAmount}, #{netAmount}, + COALESCE(#{status}, 'CALCULATED')) + + + + + INSERT INTO commission_master + (agent_id, commission_type, settle_month, source_table, source_id, + gross_amount, tax_amount, net_amount, status) + VALUES + (#{agentId}, #{commissionType}, #{settleMonth}, #{sourceTable}, #{sourceId}, + #{grossAmount}, #{taxAmount}, #{netAmount}, + COALESCE(#{status}, 'CALCULATED')) + ON CONFLICT (agent_id, commission_type, settle_month, source_id) + DO UPDATE SET + gross_amount = EXCLUDED.gross_amount, + tax_amount = EXCLUDED.tax_amount, + net_amount = EXCLUDED.net_amount, + status = EXCLUDED.status, + updated_at = NOW() + + + + + UPDATE commission_master + SET status = #{status}, updated_at = NOW() + WHERE master_id = #{masterId} + + + + + DELETE FROM commission_master WHERE master_id = #{masterId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/OverrideLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/OverrideLedgerMapper.xml new file mode 100644 index 0000000..e5431c3 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/OverrideLedgerMapper.xml @@ -0,0 +1,76 @@ + + + + + + + + + ol.ledger_id, + ol.from_agent_id, fa.agent_name AS from_agent_name, + ol.to_agent_id, ta.agent_name AS to_agent_name, + o.org_name, + ol.settle_month, + ol.source_commission_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'COMMISSION_TYPE' AND cc.code = ol.source_commission_type) AS source_commission_type_name, + ol.source_ledger_table, ol.source_ledger_id, + ol.source_amount, ol.override_rate, ol.override_amount, + ol.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'MASTER_STATUS' AND cc.code = ol.status) AS status_name + + + + FROM override_ledger ol + LEFT JOIN agent fa ON fa.agent_id = ol.from_agent_id + LEFT JOIN agent ta ON ta.agent_id = ol.to_agent_id + LEFT JOIN organization o ON o.org_id = ol.org_id + + + + + + + + INSERT INTO override_ledger + (from_agent_id, to_agent_id, org_id, settle_month, + source_commission_type, source_ledger_table, source_ledger_id, + source_amount, override_rate, override_amount, status) + VALUES + (#{fromAgentId}, #{toAgentId}, #{orgId}, #{settleMonth}, + #{sourceCommissionType}, #{sourceLedgerTable}, #{sourceLedgerId}, + #{sourceAmount}, #{overrideRate}, #{overrideAmount}, + COALESCE(#{status}, 'CALCULATED')) + + + + UPDATE override_ledger + SET status = #{status}, updated_at = NOW() + WHERE ledger_id = #{ledgerId} + + + + DELETE FROM override_ledger WHERE ledger_id = #{ledgerId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/OverrideSummaryMapper.xml b/ga-core/src/main/resources/mapper/commission/OverrideSummaryMapper.xml new file mode 100644 index 0000000..63e44c5 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/OverrideSummaryMapper.xml @@ -0,0 +1,76 @@ + + + + + + + + + os.summary_id, os.to_agent_id, + a.agent_name AS to_agent_name, org.org_name, + os.settle_month, + os.total_source_amount, os.total_override_amount, os.ledger_count, + os.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'MASTER_STATUS' AND cc.code = os.status) AS status_name, + os.calc_date + + + + + + + + + + + INSERT INTO override_summary + (to_agent_id, settle_month, total_source_amount, total_override_amount, ledger_count, status) + VALUES + (#{toAgentId}, #{settleMonth}, #{totalSourceAmount}, #{totalOverrideAmount}, #{ledgerCount}, + COALESCE(#{status}, 'CALCULATED')) + ON CONFLICT (to_agent_id, settle_month) + DO UPDATE SET + total_source_amount = EXCLUDED.total_source_amount, + total_override_amount = EXCLUDED.total_override_amount, + ledger_count = EXCLUDED.ledger_count, + status = EXCLUDED.status, + calc_date = NOW(), + updated_at = NOW() + + + + UPDATE override_summary + SET status = #{status}, updated_at = NOW() + WHERE summary_id = #{summaryId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/PersistencyBonusMapper.xml b/ga-core/src/main/resources/mapper/commission/PersistencyBonusMapper.xml new file mode 100644 index 0000000..3449ae1 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/PersistencyBonusMapper.xml @@ -0,0 +1,79 @@ + + + + + + + + + pb.bonus_id, pb.agent_id, a.agent_name, o.org_name, + pb.rule_id, pb.base_month, pb.retention_month, pb.settle_month, + pb.retention_rate, pb.bonus_amount, pb.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'MASTER_STATUS' AND cc.code = pb.status) AS status_name + + + + + + + + + INSERT INTO persistency_bonus + (agent_id, rule_id, base_month, retention_month, settle_month, + retention_rate, bonus_amount, status) + VALUES + (#{agentId}, #{ruleId}, #{baseMonth}, #{retentionMonth}, #{settleMonth}, + #{retentionRate}, #{bonusAmount}, COALESCE(#{status}, 'CALCULATED')) + ON CONFLICT (agent_id, base_month, retention_month) + DO UPDATE SET + rule_id = EXCLUDED.rule_id, + settle_month = EXCLUDED.settle_month, + retention_rate = EXCLUDED.retention_rate, + bonus_amount = EXCLUDED.bonus_amount, + status = EXCLUDED.status, + updated_at = NOW() + + + + INSERT INTO persistency_bonus + (agent_id, rule_id, base_month, retention_month, settle_month, + retention_rate, bonus_amount, status) + VALUES + (#{agentId}, #{ruleId}, #{baseMonth}, #{retentionMonth}, #{settleMonth}, + #{retentionRate}, #{bonusAmount}, COALESCE(#{status}, 'CALCULATED')) + + + + UPDATE persistency_bonus + SET status = #{status}, updated_at = NOW() + WHERE bonus_id = #{bonusId} + + + + DELETE FROM persistency_bonus WHERE bonus_id = #{bonusId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/PersistencyBonusRuleMapper.xml b/ga-core/src/main/resources/mapper/commission/PersistencyBonusRuleMapper.xml new file mode 100644 index 0000000..e69ce80 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/PersistencyBonusRuleMapper.xml @@ -0,0 +1,80 @@ + + + + + + + + + r.rule_id, r.grade_id, g.grade_name, + r.retention_month, + r.retention_rate_threshold, + r.bonus_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'BONUS_TYPE' AND cc.code = r.bonus_type) AS bonus_type_name, + r.bonus_value, + r.effective_from, r.effective_to + + + + + + + + + + + INSERT INTO persistency_bonus_rule + (grade_id, retention_month, retention_rate_threshold, bonus_type, bonus_value, + effective_from, effective_to, created_by) + VALUES + (#{gradeId}, #{retentionMonth}, #{retentionRateThreshold}, #{bonusType}, #{bonusValue}, + #{effectiveFrom}, #{effectiveTo}, #{createdBy}) + + + + UPDATE persistency_bonus_rule + SET retention_rate_threshold = #{retentionRateThreshold}, + bonus_type = #{bonusType}, + bonus_value = #{bonusValue}, + effective_to = #{effectiveTo}, + updated_at = NOW() + WHERE rule_id = #{ruleId} + + + + DELETE FROM persistency_bonus_rule WHERE rule_id = #{ruleId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/ReinstatementCommissionLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/ReinstatementCommissionLedgerMapper.xml new file mode 100644 index 0000000..154d645 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/ReinstatementCommissionLedgerMapper.xml @@ -0,0 +1,73 @@ + + + + + + + + + l.ledger_id, l.contract_id, ct.policy_no, + l.agent_id, a.agent_name, o.org_name, + l.settle_month, l.reinstatement_date, l.reinstatement_premium, + l.commission_amount, l.chargeback_reversal, l.net_commission, + l.pay_type, l.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'MASTER_STATUS' AND cc.code = l.status) AS status_name + + + + FROM reinstatement_commission_ledger l + LEFT JOIN agent a ON a.agent_id = l.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + LEFT JOIN contract ct ON ct.contract_id = l.contract_id + + + + + + + + + + INSERT INTO reinstatement_commission_ledger + (contract_id, agent_id, rule_id, settle_month, reinstatement_date, + reinstatement_premium, commission_amount, chargeback_reversal, net_commission, + pay_type, status) + VALUES + (#{contractId}, #{agentId}, #{ruleId}, #{settleMonth}, #{reinstatementDate}, + #{reinstatementPremium}, #{commissionAmount}, #{chargebackReversal}, #{netCommission}, + #{payType}, COALESCE(#{status}, 'CALCULATED')) + + + + UPDATE reinstatement_commission_ledger + SET status = #{status}, updated_at = NOW() + WHERE ledger_id = #{ledgerId} + + + + DELETE FROM reinstatement_commission_ledger WHERE ledger_id = #{ledgerId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/ReinstatementCommissionRuleMapper.xml b/ga-core/src/main/resources/mapper/commission/ReinstatementCommissionRuleMapper.xml new file mode 100644 index 0000000..8d5648c --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/ReinstatementCommissionRuleMapper.xml @@ -0,0 +1,70 @@ + + + + + + + + + r.rule_id, r.product_id, p.product_name, + r.pay_type, r.rate_value, r.fixed_amount, + r.chargeback_reversal_rate, + r.effective_from, r.effective_to + + + + + + + + + + INSERT INTO reinstatement_commission_rule + (product_id, pay_type, rate_value, fixed_amount, chargeback_reversal_rate, + effective_from, effective_to, created_by) + VALUES + (#{productId}, #{payType}, #{rateValue}, #{fixedAmount}, #{chargebackReversalRate}, + #{effectiveFrom}, #{effectiveTo}, #{createdBy}) + + + + UPDATE reinstatement_commission_rule + SET pay_type = #{payType}, + rate_value = #{rateValue}, + fixed_amount = #{fixedAmount}, + chargeback_reversal_rate = #{chargebackReversalRate}, + effective_to = #{effectiveTo}, + updated_at = NOW() + WHERE rule_id = #{ruleId} + + + + DELETE FROM reinstatement_commission_rule WHERE rule_id = #{ruleId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/RenewalCommissionLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/RenewalCommissionLedgerMapper.xml new file mode 100644 index 0000000..b6711be --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/RenewalCommissionLedgerMapper.xml @@ -0,0 +1,71 @@ + + + + + + + + + l.ledger_id, l.contract_id, ct.policy_no, + l.agent_id, a.agent_name, o.org_name, + l.settle_month, l.renewal_date, l.renewal_premium, l.commission_amount, + l.pay_type, l.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'MASTER_STATUS' AND cc.code = l.status) AS status_name + + + + FROM renewal_commission_ledger l + LEFT JOIN agent a ON a.agent_id = l.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + LEFT JOIN contract ct ON ct.contract_id = l.contract_id + + + + + + + + + + INSERT INTO renewal_commission_ledger + (contract_id, agent_id, rule_id, settle_month, renewal_date, + renewal_premium, commission_amount, pay_type, status) + VALUES + (#{contractId}, #{agentId}, #{ruleId}, #{settleMonth}, #{renewalDate}, + #{renewalPremium}, #{commissionAmount}, #{payType}, + COALESCE(#{status}, 'CALCULATED')) + + + + UPDATE renewal_commission_ledger + SET status = #{status}, updated_at = NOW() + WHERE ledger_id = #{ledgerId} + + + + DELETE FROM renewal_commission_ledger WHERE ledger_id = #{ledgerId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/RenewalCommissionRuleMapper.xml b/ga-core/src/main/resources/mapper/commission/RenewalCommissionRuleMapper.xml new file mode 100644 index 0000000..6f8f75d --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/RenewalCommissionRuleMapper.xml @@ -0,0 +1,66 @@ + + + + + + + + + r.rule_id, r.product_id, p.product_name, + r.pay_type, r.rate_value, r.fixed_amount, + r.effective_from, r.effective_to + + + + + + + + + + + INSERT INTO renewal_commission_rule + (product_id, pay_type, rate_value, fixed_amount, effective_from, effective_to, created_by) + VALUES + (#{productId}, #{payType}, #{rateValue}, #{fixedAmount}, #{effectiveFrom}, #{effectiveTo}, #{createdBy}) + + + + UPDATE renewal_commission_rule + SET pay_type = #{payType}, + rate_value = #{rateValue}, + fixed_amount = #{fixedAmount}, + effective_to = #{effectiveTo}, + updated_at = NOW() + WHERE rule_id = #{ruleId} + + + + DELETE FROM renewal_commission_rule WHERE rule_id = #{ruleId} + + + diff --git a/ga-core/src/main/resources/mapper/complaint/ComplaintMapper.xml b/ga-core/src/main/resources/mapper/complaint/ComplaintMapper.xml new file mode 100644 index 0000000..e967fdc --- /dev/null +++ b/ga-core/src/main/resources/mapper/complaint/ComplaintMapper.xml @@ -0,0 +1,97 @@ + + + + + + + + + c.complaint_id, c.contract_id, ct.policy_no, + c.agent_id, a.agent_name, o.org_name, + c.customer_name, c.complaint_date, + c.channel, + (SELECT cc.code_name FROM common_code cc WHERE cc.group_code='COMPLAINT_CHANNEL' AND cc.code=c.channel) AS channel_name, + c.complaint_type, + (SELECT cc.code_name FROM common_code cc WHERE cc.group_code='COMPLAINT_TYPE' AND cc.code=c.complaint_type) AS complaint_type_name, + c.severity, + (SELECT cc.code_name FROM common_code cc WHERE cc.group_code='COMPLAINT_SEVERITY' AND cc.code=c.severity) AS severity_name, + c.status, + (SELECT cc.code_name FROM common_code cc WHERE cc.group_code='COMPLAINT_STATUS' AND cc.code=c.status) AS status_name, + c.resolution, c.chargeback_triggered, c.settled_at, c.created_at + + + + FROM complaint c + LEFT JOIN contract ct ON ct.contract_id = c.contract_id + LEFT JOIN agent a ON a.agent_id = c.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + + + + + + + + + + + INSERT INTO complaint + (contract_id, agent_id, customer_name, complaint_date, channel, + complaint_type, severity, status, resolution, chargeback_triggered, created_by) + VALUES + (#{contractId}, #{agentId}, #{customerName}, #{complaintDate}, #{channel}, + #{complaintType}, COALESCE(#{severity}, 'MID'), COALESCE(#{status}, 'OPEN'), + #{resolution}, #{chargebackTriggered}, #{createdBy}) + + + + UPDATE complaint + SET complaint_type = #{complaintType}, + severity = #{severity}, + resolution = #{resolution}, + chargeback_triggered = #{chargebackTriggered}, + updated_at = NOW(), + updated_by = #{updatedBy} + WHERE complaint_id = #{complaintId} + + + + UPDATE complaint + SET status = #{status}, + resolution = #{resolution}, + settled_at = CASE WHEN #{status} IN ('CLOSED', 'WITHDRAWN') THEN NOW() ELSE settled_at END, + updated_at = NOW() + WHERE complaint_id = #{complaintId} + + + + DELETE FROM complaint WHERE complaint_id = #{complaintId} + + + diff --git a/ga-core/src/main/resources/mapper/contract/AgentContractMapper.xml b/ga-core/src/main/resources/mapper/contract/AgentContractMapper.xml new file mode 100644 index 0000000..a730d6f --- /dev/null +++ b/ga-core/src/main/resources/mapper/contract/AgentContractMapper.xml @@ -0,0 +1,81 @@ + + + + + + + + + ac.contract_id, ac.agent_id, a.agent_name, + ac.carrier_id, ic.company_name AS carrier_name, + ac.contract_no, ac.signed_date, + ac.effective_from, ac.effective_to, + ac.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'AGENT_CONTRACT_STATUS' AND cc.code = ac.status) AS status_name, + ac.onboarding_bonus_amount, + ac.terminated_at, ac.terminated_reason, + ac.created_at + + + + + + + + INSERT INTO agent_contract + (agent_id, carrier_id, contract_no, signed_date, effective_from, effective_to, + status, onboarding_bonus_amount, created_by) + VALUES + (#{agentId}, #{carrierId}, #{contractNo}, #{signedDate}, #{effectiveFrom}, #{effectiveTo}, + COALESCE(#{status}, 'ACTIVE'), #{onboardingBonusAmount}, #{createdBy}) + + + + UPDATE agent_contract + SET contract_no = #{contractNo}, + signed_date = #{signedDate}, + effective_from = #{effectiveFrom}, + effective_to = #{effectiveTo}, + onboarding_bonus_amount = #{onboardingBonusAmount}, + updated_at = NOW(), + updated_by = #{updatedBy} + WHERE contract_id = #{contractId} + + + + UPDATE agent_contract + SET status = #{status}, + terminated_at = CASE WHEN #{status} = 'TERMINATED' THEN NOW() ELSE terminated_at END, + terminated_reason = #{terminatedReason}, + updated_at = NOW() + WHERE contract_id = #{contractId} + + + + DELETE FROM agent_contract WHERE contract_id = #{contractId} + + + diff --git a/ga-core/src/main/resources/mapper/endorse/ContractEndorsementMapper.xml b/ga-core/src/main/resources/mapper/endorse/ContractEndorsementMapper.xml new file mode 100644 index 0000000..8555cc5 --- /dev/null +++ b/ga-core/src/main/resources/mapper/endorse/ContractEndorsementMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + ce.endorsement_id, ce.contract_id, ct.policy_no, + ce.agent_id, a.agent_name, + ce.endorsement_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'ENDORSEMENT_TYPE' AND cc.code = ce.endorsement_type) AS endorsement_type_name, + ce.changed_at, ce.old_value, ce.new_value, ce.reason, + ce.requires_commission_recalc, ce.processed, ce.processed_at + + + + + + + + + + + INSERT INTO contract_endorsement + (contract_id, agent_id, endorsement_type, changed_at, old_value, new_value, + reason, requires_commission_recalc, processed, created_by) + VALUES + (#{contractId}, #{agentId}, #{endorsementType}, + COALESCE(#{changedAt}, NOW()), #{oldValue}, #{newValue}, + #{reason}, #{requiresCommissionRecalc}, FALSE, #{createdBy}) + + + + UPDATE contract_endorsement + SET processed = TRUE, + processed_at = NOW(), + updated_at = NOW() + WHERE endorsement_id = #{endorsementId} + + + + DELETE FROM contract_endorsement WHERE endorsement_id = #{endorsementId} + + + diff --git a/ga-core/src/main/resources/mapper/installment/InstallmentPlanMapper.xml b/ga-core/src/main/resources/mapper/installment/InstallmentPlanMapper.xml index ef0713b..d39c81e 100644 --- a/ga-core/src/main/resources/mapper/installment/InstallmentPlanMapper.xml +++ b/ga-core/src/main/resources/mapper/installment/InstallmentPlanMapper.xml @@ -188,4 +188,9 @@ WHERE plan_id = #{planId} + + + DELETE FROM installment_plan WHERE contract_id = #{contractId} + + diff --git a/ga-core/src/main/resources/mapper/kpi/KpiMapper.xml b/ga-core/src/main/resources/mapper/kpi/KpiMapper.xml new file mode 100644 index 0000000..8f5f03b --- /dev/null +++ b/ga-core/src/main/resources/mapper/kpi/KpiMapper.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ga-core/src/main/resources/mapper/license/AgentLicenseMapper.xml b/ga-core/src/main/resources/mapper/license/AgentLicenseMapper.xml new file mode 100644 index 0000000..00363fc --- /dev/null +++ b/ga-core/src/main/resources/mapper/license/AgentLicenseMapper.xml @@ -0,0 +1,86 @@ + + + + + + + + + al.license_id, al.agent_id, a.agent_name, o.org_name, + al.license_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'LICENSE_TYPE' AND cc.code = al.license_type) AS license_type_name, + al.license_no, + al.issued_date, al.expires_date, + al.renewed_from, al.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'LICENSE_STATUS' AND cc.code = al.status) AS status_name, + (al.expires_date - CURRENT_DATE) AS days_until_expiry + + + + + + + + + + + INSERT INTO agent_license + (agent_id, license_type, license_no, issued_date, expires_date, + renewed_from, status, created_by) + VALUES + (#{agentId}, #{licenseType}, #{licenseNo}, #{issuedDate}, #{expiresDate}, + #{renewedFrom}, COALESCE(#{status}, 'ACTIVE'), #{createdBy}) + + + + UPDATE agent_license + SET license_no = #{licenseNo}, + issued_date = #{issuedDate}, + expires_date = #{expiresDate}, + updated_at = NOW() + WHERE license_id = #{licenseId} + + + + UPDATE agent_license + SET status = #{status}, updated_at = NOW() + WHERE license_id = #{licenseId} + + + + DELETE FROM agent_license WHERE license_id = #{licenseId} + + + diff --git a/ga-core/src/main/resources/mapper/notice/NoticeMapper.xml b/ga-core/src/main/resources/mapper/notice/NoticeMapper.xml new file mode 100644 index 0000000..44ba074 --- /dev/null +++ b/ga-core/src/main/resources/mapper/notice/NoticeMapper.xml @@ -0,0 +1,66 @@ + + + + + + + + + n.notice_id, n.title, n.content, n.notice_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'NOTICE_TYPE' AND cc.code = n.notice_type) AS notice_type_name, + n.start_date, n.end_date, n.is_active, n.created_at + + + + + + + + + + + INSERT INTO notice (title, content, notice_type, start_date, end_date, is_active, created_by) + VALUES (#{title}, #{content}, #{noticeType}, #{startDate}, #{endDate}, TRUE, #{createdBy}) + + + + UPDATE notice + SET title = #{title}, + content = #{content}, + notice_type = #{noticeType}, + start_date = #{startDate}, + end_date = #{endDate}, + updated_at = NOW(), + updated_by = #{updatedBy} + WHERE notice_id = #{noticeId} + + + + UPDATE notice SET is_active = #{isActive}, updated_at = NOW() WHERE notice_id = #{noticeId} + + + + DELETE FROM notice WHERE notice_id = #{noticeId} + + + diff --git a/ga-core/src/main/resources/mapper/notice/UserNotificationMapper.xml b/ga-core/src/main/resources/mapper/notice/UserNotificationMapper.xml new file mode 100644 index 0000000..5ae7274 --- /dev/null +++ b/ga-core/src/main/resources/mapper/notice/UserNotificationMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + UPDATE user_notification + SET is_read = TRUE, read_at = NOW() + WHERE notification_id = #{notificationId} + AND user_id = #{userId} + AND is_read = FALSE + + + + + UPDATE user_notification + SET is_read = TRUE, read_at = NOW() + WHERE user_id = #{userId} + AND is_read = FALSE + + + + INSERT INTO user_notification (user_id, title, content, ref_type, ref_id, is_read) + VALUES (#{userId}, #{title}, #{content}, #{refType}, #{refId}, FALSE) + + + + DELETE FROM user_notification WHERE notification_id = #{notificationId} + + + diff --git a/ga-core/src/main/resources/mapper/org/OrganizationMapper.xml b/ga-core/src/main/resources/mapper/org/OrganizationMapper.xml index 7742af8..ec42a12 100644 --- a/ga-core/src/main/resources/mapper/org/OrganizationMapper.xml +++ b/ga-core/src/main/resources/mapper/org/OrganizationMapper.xml @@ -31,7 +31,7 @@ AND o.org_type = #{orgType} AND o.is_active = #{isActive} - ORDER BY o.org_level, o.sort_order, o.org_id + ORDER BY o.org_level, o.org_id + SELECT + + + AND cro.carrier_id = #{carrierId} + + AND cro.settle_month = #{settleMonth} + + AND cro.status = #{status} + + ORDER BY cro.settle_month DESC, cro.carrier_id + + + + + + + + + + + + + + INSERT INTO carrier_reconciliation_out ( + carrier_id, settle_month, + file_path, file_name, file_size, record_count, + status, retry_count, created_at, created_by + ) VALUES ( + #{carrierId}, #{settleMonth}, + #{filePath}, #{fileName}, #{fileSize}, #{recordCount}, + 'READY', 0, NOW(), #{createdBy} + ) + + + + + UPDATE carrier_reconciliation_out SET + status = #{status}, + updated_at = NOW() + WHERE recon_out_id = #{reconOutId} + + + + + UPDATE carrier_reconciliation_out SET + status = 'SENT', + sent_at = NOW(), + updated_at = NOW() + WHERE recon_out_id = #{reconOutId} + AND status IN ('READY', 'FAIL') + + + + + UPDATE carrier_reconciliation_out SET + status = 'ACK', + ack_at = NOW(), + ack_message = #{ackMessage}, + updated_at = NOW() + WHERE recon_out_id = #{reconOutId} + AND status = 'SENT' + + + + + UPDATE carrier_reconciliation_out SET + status = 'FAIL', + fail_reason = #{failReason}, + retry_count = retry_count + 1, + last_retry_at = NOW(), + updated_at = NOW() + WHERE recon_out_id = #{reconOutId} + + + diff --git a/ga-core/src/main/resources/mapper/settle/CommissionDisputeMapper.xml b/ga-core/src/main/resources/mapper/settle/CommissionDisputeMapper.xml new file mode 100644 index 0000000..264fb54 --- /dev/null +++ b/ga-core/src/main/resources/mapper/settle/CommissionDisputeMapper.xml @@ -0,0 +1,118 @@ + + + + + + + + + + cd.dispute_id, + cd.agent_id, + a.agent_name, + o.org_name, + cd.settle_month, + cd.settle_id, + cd.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'DISPUTE_STATUS' AND cc.code = cd.status) AS status_name, + cd.disputed_amount, + cd.claim_amount, + cd.reason, + cd.evidence_url, + cd.reviewer_id, + ur.user_name AS reviewer_name, + cd.reviewed_at, + cd.resolved_by, + urs.user_name AS resolved_by_name, + cd.resolved_at, + cd.resolution, + cd.adjustment_amount, + cd.correction_id, + cd.created_at, + cd.updated_at + + + + + FROM commission_dispute cd + LEFT JOIN agent a ON a.agent_id = cd.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + LEFT JOIN users ur ON ur.user_id = cd.reviewer_id + LEFT JOIN users urs ON urs.user_id = cd.resolved_by + + + + + + + + + + + + + + INSERT INTO commission_dispute ( + agent_id, settle_month, settle_id, + status, disputed_amount, claim_amount, + reason, evidence_url, created_at + ) VALUES ( + #{agentId}, #{settleMonth}, #{settleId}, + 'OPEN', #{disputedAmount}, #{claimAmount}, + #{reason}, #{evidenceUrl}, NOW() + ) + + + + + UPDATE commission_dispute SET + status = 'IN_REVIEW', + reviewer_id = #{reviewerId}, + reviewed_at = NOW(), + updated_at = NOW() + WHERE dispute_id = #{disputeId} + AND status = 'OPEN' + + + + + UPDATE commission_dispute SET + status = #{status}, + resolution = #{resolution}, + adjustment_amount = #{adjustmentAmount}, + correction_id = #{correctionId}, + resolved_by = #{resolvedBy}, + resolved_at = NOW(), + updated_at = NOW() + WHERE dispute_id = #{disputeId} + AND status = 'IN_REVIEW' + + + diff --git a/ga-core/src/main/resources/mapper/settle/IncentivePaymentMapper.xml b/ga-core/src/main/resources/mapper/settle/IncentivePaymentMapper.xml new file mode 100644 index 0000000..c3d80ec --- /dev/null +++ b/ga-core/src/main/resources/mapper/settle/IncentivePaymentMapper.xml @@ -0,0 +1,124 @@ + + + + + + + + + + ip.incentive_pay_id, + ip.program_id, + prog.program_name, + prog.pay_type, + ip.agent_id, + a.agent_name, + o.org_name, + ip.settle_month, + ip.base_amount, + ip.incentive_amount, + ip.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'INCENTIVE_PAY_STATUS' AND cc.code = ip.status) AS status_name, + ip.calc_date, + ip.confirmed_by, + u.user_name AS confirmed_by_name, + ip.confirmed_at, + ip.settle_id, + ip.cancel_reason, + ip.created_at, + ip.updated_at + + + + + FROM incentive_payment ip + LEFT JOIN incentive_program prog ON prog.program_id = ip.program_id + LEFT JOIN agent a ON a.agent_id = ip.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + LEFT JOIN users u ON u.user_id = ip.confirmed_by + + + + + + + + + + + + + + + + + INSERT INTO incentive_payment ( + program_id, agent_id, settle_month, + base_amount, incentive_amount, status, + calc_date, created_at + ) VALUES ( + #{programId}, #{agentId}, #{settleMonth}, + #{baseAmount}, #{incentiveAmount}, 'CALCULATED', + NOW(), NOW() + ) + ON CONFLICT (program_id, agent_id, settle_month) DO UPDATE SET + base_amount = EXCLUDED.base_amount, + incentive_amount = EXCLUDED.incentive_amount, + calc_date = NOW(), + updated_at = NOW() + WHERE incentive_payment.status = 'CALCULATED' + + + + + UPDATE incentive_payment SET + status = #{status}, + confirmed_by = CASE WHEN #{status} = 'CONFIRMED' THEN #{userId} ELSE confirmed_by END, + confirmed_at = CASE WHEN #{status} = 'CONFIRMED' THEN NOW() ELSE confirmed_at END, + updated_at = NOW() + WHERE incentive_pay_id = #{incentivePayId} + + + + + UPDATE incentive_payment SET + settle_id = #{settleId}, + status = 'PAID', + updated_at = NOW() + WHERE incentive_pay_id = #{incentivePayId} + AND status = 'CONFIRMED' + + + diff --git a/ga-core/src/main/resources/mapper/settle/IncentiveProgramMapper.xml b/ga-core/src/main/resources/mapper/settle/IncentiveProgramMapper.xml new file mode 100644 index 0000000..180fe52 --- /dev/null +++ b/ga-core/src/main/resources/mapper/settle/IncentiveProgramMapper.xml @@ -0,0 +1,145 @@ + + + + + + + + + + ip.program_id, + ip.program_name, + ip.program_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'INCENTIVE_TYPE' AND cc.code = ip.program_type) AS program_type_name, + ip.target_type, + ip.target_grade_id, + g.grade_name AS target_grade_name, + ip.target_org_id, + o.org_name AS target_org_name, + ip.product_category, + ip.start_month, + ip.end_month, + ip.condition_desc, + ip.pay_rate, + ip.pay_amount_fixed, + ip.pay_type, + ip.budget_amount, + ip.is_active, + ip.created_at, + ip.created_by, + ip.updated_at, + ip.updated_by + + + + + FROM incentive_program ip + LEFT JOIN grade g ON g.grade_id = ip.target_grade_id + LEFT JOIN organization o ON o.org_id = ip.target_org_id + + + + + + + + + + + + + + + + + INSERT INTO incentive_program ( + program_name, program_type, target_type, + target_grade_id, target_org_id, product_category, + start_month, end_month, condition_desc, + pay_rate, pay_amount_fixed, pay_type, + budget_amount, is_active, created_at, created_by + ) VALUES ( + #{programName}, #{programType}, #{targetType}, + #{targetGradeId}, #{targetOrgId}, #{productCategory}, + #{startMonth}, #{endMonth}, #{conditionDesc}, + #{payRate}, #{payAmountFixed}, #{payType}, + #{budgetAmount}, COALESCE(#{isActive}, TRUE), NOW(), #{createdBy} + ) + + + + + UPDATE incentive_program SET + program_name = #{programName}, + program_type = #{programType}, + target_type = #{targetType}, + target_grade_id = #{targetGradeId}, + target_org_id = #{targetOrgId}, + product_category = #{productCategory}, + start_month = #{startMonth}, + end_month = #{endMonth}, + condition_desc = #{conditionDesc}, + pay_rate = #{payRate}, + pay_amount_fixed = #{payAmountFixed}, + pay_type = #{payType}, + budget_amount = #{budgetAmount}, + updated_at = NOW(), + updated_by = #{updatedBy} + WHERE program_id = #{programId} + + + + + UPDATE incentive_program SET + is_active = #{isActive}, + updated_at = NOW(), + updated_by = #{updatedBy} + WHERE program_id = #{programId} + + + diff --git a/ga-core/src/main/resources/mapper/settle/PeriodCloseMapper.xml b/ga-core/src/main/resources/mapper/settle/PeriodCloseMapper.xml new file mode 100644 index 0000000..21e0d19 --- /dev/null +++ b/ga-core/src/main/resources/mapper/settle/PeriodCloseMapper.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + pc.close_id, + pc.close_month, + pc.closed_at, + pc.closed_by, + uc.user_name AS closed_by_name, + pc.reopen_at, + pc.reopen_by, + ur.user_name AS reopen_by_name, + pc.reopen_reason, + pc.note, + -- 마감 상태: reopen_at IS NULL이면 CLOSED, 아니면 REOPENED + CASE WHEN pc.reopen_at IS NULL THEN 'CLOSED' ELSE 'REOPENED' END AS close_status, + pc.created_at, + pc.updated_at + + + + + FROM period_close pc + LEFT JOIN users uc ON uc.user_id = pc.closed_by + LEFT JOIN users ur ON ur.user_id = pc.reopen_by + + + + + + + + + + + + + + + + + INSERT INTO period_close (close_month, closed_at, closed_by, note, created_at) + VALUES (#{closeMonth}, NOW(), #{closedBy}, #{note}, NOW()) + + + + + UPDATE period_close SET + reopen_at = NOW(), + reopen_by = #{reopenBy}, + reopen_reason = #{reopenReason}, + updated_at = NOW() + WHERE close_id = #{closeId} + AND reopen_at IS NULL -- 이미 재오픈 상태면 실행 안 됨 + + + diff --git a/ga-core/src/main/resources/mapper/settle/SettleCorrectionMapper.xml b/ga-core/src/main/resources/mapper/settle/SettleCorrectionMapper.xml new file mode 100644 index 0000000..fba7848 --- /dev/null +++ b/ga-core/src/main/resources/mapper/settle/SettleCorrectionMapper.xml @@ -0,0 +1,132 @@ + + + + + + + + + + sc.correction_id, + sc.original_settle_id, + sc.agent_id, + a.agent_name, + o.org_name, + sc.settle_month, + sc.diff_amount, + sc.before_amount, + sc.after_amount, + sc.reason_code, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'CORRECTION_REASON' AND cc.code = sc.reason_code) AS reason_code_name, + sc.reason_detail, + sc.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'CORRECTION_STATUS' AND cc.code = sc.status) AS status_name, + sc.requested_by, + ur.user_name AS requested_by_name, + sc.requested_at, + sc.approved_by, + ua.user_name AS approved_by_name, + sc.approved_at, + sc.applied_settle_id, + sc.cancel_reason, + sc.cancelled_at, + sc.created_at, + sc.updated_at + + + + + FROM settle_correction sc + LEFT JOIN agent a ON a.agent_id = sc.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + LEFT JOIN users ur ON ur.user_id = sc.requested_by + LEFT JOIN users ua ON ua.user_id = sc.approved_by + + + + + + + + + + + + + + + + + INSERT INTO settle_correction ( + original_settle_id, agent_id, settle_month, + diff_amount, before_amount, after_amount, + reason_code, reason_detail, status, + requested_by, requested_at, created_at + ) VALUES ( + #{originalSettleId}, #{agentId}, #{settleMonth}, + #{afterAmount} - #{beforeAmount}, #{beforeAmount}, #{afterAmount}, + #{reasonCode}, #{reasonDetail}, 'DRAFT', + #{requestedBy}, NOW(), NOW() + ) + + + + + UPDATE settle_correction SET + status = #{status}, + approved_by = CASE WHEN #{status} = 'APPROVED' THEN #{userId} ELSE approved_by END, + approved_at = CASE WHEN #{status} = 'APPROVED' THEN NOW() ELSE approved_at END, + updated_at = NOW() + WHERE correction_id = #{correctionId} + + + + + UPDATE settle_correction SET + status = 'CANCELLED', + cancel_reason = #{cancelReason}, + cancelled_by = #{cancelledBy}, + cancelled_at = NOW(), + updated_at = NOW() + WHERE correction_id = #{correctionId} + AND status NOT IN ('APPLIED', 'CANCELLED') + + + diff --git a/ga-core/src/main/resources/mapper/settle/SettleMasterMapper.xml b/ga-core/src/main/resources/mapper/settle/SettleMasterMapper.xml index bbebb36..6483440 100644 --- a/ga-core/src/main/resources/mapper/settle/SettleMasterMapper.xml +++ b/ga-core/src/main/resources/mapper/settle/SettleMasterMapper.xml @@ -14,7 +14,8 @@ s.status, (SELECT cc.code_name FROM common_code cc WHERE cc.group_code='SETTLE_STATUS' AND cc.code=s.status) AS status_name, s.calc_date, s.confirmed_by, u.user_name AS confirmed_by_name, - s.confirmed_at, s.hold_reason + s.confirmed_at, s.hold_reason, s.hold_reason_code, + (SELECT cc.code_name FROM common_code cc WHERE cc.group_code='HOLD_REASON' AND cc.code=s.hold_reason_code) AS hold_reason_name @@ -101,7 +102,11 @@ - UPDATE settle_master SET status = 'HOLD', hold_reason = #{reason} WHERE settle_id = #{settleId} + UPDATE settle_master SET + status = 'HOLD', + hold_reason = #{reason}, + hold_reason_code = #{reasonCode} + WHERE settle_id = #{settleId} + SELECT + + + + AND ti.invoice_type = #{invoiceType} + + + AND ti.settle_month = #{settleMonth} + + AND ti.agent_id = #{agentId} + + AND ti.issue_date >= #{issueDateFrom} + + + AND ti.issue_date <= #{issueDateTo} + + + AND ti.counterpart_biz_no = #{counterpartBizNo} + + + + + AND ti.cancel_date IS NOT NULL + AND ti.cancel_date IS NULL + + + + ORDER BY ti.issue_date DESC, ti.invoice_id DESC + + + + + + + + + + + + + + INSERT INTO tax_invoice ( + invoice_no, invoice_type, issue_date, + supply_amount, tax_amount, total_amount, + counterpart_name, counterpart_biz_no, counterpart_rep, counterpart_addr, + agent_id, settle_month, settle_id, + description, issued_by, created_at + ) VALUES ( + #{invoiceNo}, #{invoiceType}, #{issueDate}, + #{supplyAmount}, #{taxAmount}, #{supplyAmount} + #{taxAmount}, + #{counterpartName}, #{counterpartBizNo}, #{counterpartRep}, #{counterpartAddr}, + #{agentId}, #{settleMonth}, #{settleId}, + #{description}, #{issuedBy}, NOW() + ) + + + + + UPDATE tax_invoice SET + cancel_date = #{cancelDate}, + cancel_reason = #{cancelReason}, + updated_at = NOW() + WHERE invoice_id = #{invoiceId} + AND cancel_date IS NULL -- 이미 취소된 건은 재취소 불가 + + + diff --git a/ga-core/src/main/resources/mapper/settle/TerminationSettlementMapper.xml b/ga-core/src/main/resources/mapper/settle/TerminationSettlementMapper.xml new file mode 100644 index 0000000..cced981 --- /dev/null +++ b/ga-core/src/main/resources/mapper/settle/TerminationSettlementMapper.xml @@ -0,0 +1,159 @@ + + + + + + + + + + ts.term_settle_id, + ts.agent_id, + a.agent_name, + o.org_name, + ts.termination_date, + ts.org_history_id, + ts.calc_month, + ts.unpaid_recruit_amount, + ts.unpaid_installment_amount, + ts.unpaid_incentive_amount, + -- 미지급 합계 (계산값) + ts.unpaid_recruit_amount + ts.unpaid_installment_amount + ts.unpaid_incentive_amount + AS unpaid_total_amount, + ts.chargeback_expected_amount, + ts.chargeback_waived_amount, + ts.deduction_amount, + ts.final_pay_amount, + ts.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'TERM_SETTLE_STATUS' AND cc.code = ts.status) AS status_name, + ts.note, + ts.confirmed_by, + u.user_name AS confirmed_by_name, + ts.confirmed_at, + ts.paid_at, + ts.payment_id, + ts.created_at, + ts.created_by, + ts.updated_at, + ts.updated_by + + + + + FROM termination_settlement ts + LEFT JOIN agent a ON a.agent_id = ts.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + LEFT JOIN users u ON u.user_id = ts.confirmed_by + + + + + + + + + + + + + + + + + INSERT INTO termination_settlement ( + agent_id, termination_date, org_history_id, calc_month, + unpaid_recruit_amount, unpaid_installment_amount, unpaid_incentive_amount, + chargeback_expected_amount, chargeback_waived_amount, + deduction_amount, final_pay_amount, + status, note, created_at, created_by + ) VALUES ( + #{agentId}, #{terminationDate}, #{orgHistoryId}, #{calcMonth}, + COALESCE(#{unpaidRecruitAmount}, 0), + COALESCE(#{unpaidInstallmentAmount}, 0), + COALESCE(#{unpaidIncentiveAmount}, 0), + COALESCE(#{chargebackExpectedAmount}, 0), + COALESCE(#{chargebackWaivedAmount}, 0), + COALESCE(#{deductionAmount}, 0), + COALESCE(#{finalPayAmount}, 0), + 'CALCULATING', #{note}, NOW(), #{createdBy} + ) + + + + + UPDATE termination_settlement SET + unpaid_recruit_amount = #{unpaidRecruitAmount}, + unpaid_installment_amount = #{unpaidInstallmentAmount}, + unpaid_incentive_amount = #{unpaidIncentiveAmount}, + chargeback_expected_amount = #{chargebackExpectedAmount}, + chargeback_waived_amount = #{chargebackWaivedAmount}, + deduction_amount = #{deductionAmount}, + final_pay_amount = #{finalPayAmount}, + note = #{note}, + updated_at = NOW(), + updated_by = #{updatedBy} + WHERE term_settle_id = #{termSettleId} + AND status = 'CALCULATING' + + + + + UPDATE termination_settlement SET + status = #{status}, + confirmed_by = CASE WHEN #{status} = 'CONFIRMED' THEN #{userId} ELSE confirmed_by END, + confirmed_at = CASE WHEN #{status} = 'CONFIRMED' THEN NOW() ELSE confirmed_at END, + paid_at = CASE WHEN #{status} = 'PAID' THEN NOW() ELSE paid_at END, + updated_at = NOW() + WHERE term_settle_id = #{termSettleId} + + + + + UPDATE termination_settlement SET + payment_id = #{paymentId}, + status = 'PAID', + paid_at = NOW(), + updated_at = NOW() + WHERE term_settle_id = #{termSettleId} + AND status = 'CONFIRMED' + + + diff --git a/ga-core/src/main/resources/mapper/training/AgentTrainingMapper.xml b/ga-core/src/main/resources/mapper/training/AgentTrainingMapper.xml new file mode 100644 index 0000000..68ae976 --- /dev/null +++ b/ga-core/src/main/resources/mapper/training/AgentTrainingMapper.xml @@ -0,0 +1,70 @@ + + + + + + + + + at2.training_id, at2.agent_id, a.agent_name, o.org_name, + at2.course_name, at2.course_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'COURSE_TYPE' AND cc.code = at2.course_type) AS course_type_name, + at2.training_date, at2.hours, at2.certificate_no, at2.instructor + + + + + + + + + + + INSERT INTO agent_training + (agent_id, course_name, course_type, training_date, hours, certificate_no, instructor, created_by) + VALUES + (#{agentId}, #{courseName}, #{courseType}, #{trainingDate}, #{hours}, + #{certificateNo}, #{instructor}, #{createdBy}) + + + + UPDATE agent_training + SET course_name = #{courseName}, + course_type = #{courseType}, + training_date = #{trainingDate}, + hours = #{hours}, + certificate_no = #{certificateNo}, + instructor = #{instructor}, + updated_at = NOW() + WHERE training_id = #{trainingId} + + + + DELETE FROM agent_training WHERE training_id = #{trainingId} + + + diff --git a/ga-core/src/main/resources/mapper/training/TrainingRequirementMapper.xml b/ga-core/src/main/resources/mapper/training/TrainingRequirementMapper.xml new file mode 100644 index 0000000..640e5c8 --- /dev/null +++ b/ga-core/src/main/resources/mapper/training/TrainingRequirementMapper.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + INSERT INTO training_requirement (target_year, required_hours, description, created_by) + VALUES (#{targetYear}, #{requiredHours}, #{description}, #{createdBy}) + ON CONFLICT (target_year) DO NOTHING + + + + UPDATE training_requirement + SET required_hours = #{requiredHours}, + description = #{description}, + updated_at = NOW() + WHERE target_year = #{targetYear} + + + diff --git a/ga-core/src/main/resources/mapper/withdraw/WithdrawRequestMapper.xml b/ga-core/src/main/resources/mapper/withdraw/WithdrawRequestMapper.xml new file mode 100644 index 0000000..9e08331 --- /dev/null +++ b/ga-core/src/main/resources/mapper/withdraw/WithdrawRequestMapper.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wr.request_id, wr.settle_master_id, + sm.settle_month, + wr.agent_id, a.agent_name, + wr.amount, wr.requested_by, wr.requested_at, + wr.approval_request_id, wr.bank_code, wr.account_no, + wr.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'WITHDRAW_STATUS' AND cc.code = wr.status) AS status_name, + wr.sent_at, wr.completed_at, wr.fail_reason + + + + + + + + + + + + INSERT INTO withdraw_request + (settle_master_id, agent_id, amount, requested_by, requested_at, + approval_request_id, bank_code, account_no, status) + VALUES + (#{settleMasterId}, #{agentId}, #{amount}, #{requestedBy}, + COALESCE(#{requestedAt}, NOW()), + #{approvalRequestId}, #{bankCode}, + #{accountNo, typeHandler=com.ga.common.mybatis.EncryptTypeHandler}, + COALESCE(#{status}, 'REQUESTED')) + + + + UPDATE withdraw_request + SET status = #{status}, + fail_reason = #{failReason}, + updated_at = NOW() + WHERE request_id = #{requestId} + + + + + UPDATE withdraw_request + SET status = 'SENT', sent_at = NOW(), updated_at = NOW() + WHERE request_id = #{requestId} + AND status = 'APPROVED' + + + + + UPDATE withdraw_request + SET status = 'COMPLETED', completed_at = NOW(), updated_at = NOW() + WHERE request_id = #{requestId} + AND status = 'SENT' + + + + + UPDATE withdraw_request + SET status = 'CANCELLED', updated_at = NOW() + WHERE request_id = #{requestId} + AND status IN ('REQUESTED', 'APPROVED') + + + diff --git a/ga-frontend/src/App.tsx b/ga-frontend/src/App.tsx index c453e50..1de0f55 100644 --- a/ga-frontend/src/App.tsx +++ b/ga-frontend/src/App.tsx @@ -3,6 +3,30 @@ import LoginPage from '@/pages/LoginPage'; import MainLayout from '@/layouts/MainLayout'; import Dashboard from '@/pages/Dashboard'; +// ── P4: 수수료 종류 ────────────────────────────────── +import CommissionMaster from '@/pages/commission/CommissionMaster'; +import CollectionCommission from '@/pages/commission/CollectionCommission'; +import RenewalCommission from '@/pages/commission/RenewalCommission'; +import ReinstatementCommission from '@/pages/commission/ReinstatementCommission'; +import PersistencyBonus from '@/pages/commission/PersistencyBonus'; +import OverrideLedger from '@/pages/commission/OverrideLedger'; + +// ── P4: 설계사 라이프사이클 ─────────────────────────── +import AgentContracts from '@/pages/agent/AgentContracts'; +import AgentLicenses from '@/pages/agent/AgentLicenses'; +import AgentTrainings from '@/pages/agent/AgentTrainings'; + +// ── P4: 계약 운영 ──────────────────────────────────── +import ContractEndorsements from '@/pages/contracts/ContractEndorsements'; +import Complaints from '@/pages/contracts/Complaints'; + +// ── P4: 시스템 ────────────────────────────────────── +import Approvals from '@/pages/system/Approvals'; +import Notices from '@/pages/system/Notices'; + +// ── P4: 출금 ──────────────────────────────────────── +import WithdrawRequest from '@/pages/payments/WithdrawRequest'; + import OrgTree from '@/pages/org/OrgTree'; import AgentList from '@/pages/org/AgentList'; import AgentDetail from '@/pages/org/AgentDetail'; @@ -29,6 +53,15 @@ import SettleList from '@/pages/settle/SettleList'; import BatchRun from '@/pages/settle/BatchRun'; import PaymentList from '@/pages/settle/PaymentList'; +import PerformanceReport from '@/pages/report/PerformanceReport'; +import OrgReport from '@/pages/report/OrgReport'; +import ChargebackRiskReport from '@/pages/report/ChargebackRiskReport'; + +import KpiMonthlySummary from '@/pages/kpi/KpiMonthlySummary'; +import KpiOrgSummary from '@/pages/kpi/KpiOrgSummary'; +import KpiRetention from '@/pages/kpi/KpiRetention'; +import KpiCumulative from '@/pages/kpi/KpiCumulative'; + import UserList from '@/pages/system/UserList'; import RoleList from '@/pages/system/RoleList'; import MenuManage from '@/pages/system/MenuManage'; @@ -84,6 +117,42 @@ export default function App() { } /> } /> + {/* 리포트 */} + } /> + } /> + } /> + + {/* KPI 대시보드 */} + } /> + } /> + } /> + } /> + + {/* P4: 수수료 종류 */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + {/* P4: 설계사 라이프사이클 */} + } /> + } /> + } /> + + {/* P4: 계약 운영 */} + } /> + } /> + + {/* P4: 시스템 */} + } /> + } /> + + {/* P4: 출금 */} + } /> + {/* 시스템관리 */} } /> } /> diff --git a/ga-frontend/src/api/agent-contract.ts b/ga-frontend/src/api/agent-contract.ts new file mode 100644 index 0000000..a661661 --- /dev/null +++ b/ga-frontend/src/api/agent-contract.ts @@ -0,0 +1,50 @@ +import api, { PageResponse, unwrap } from './request'; + +export interface AgentContractRow extends Record { + contractId: number; + agentId: number; + agentName: string; + carrierId: number; + carrierName: string; + contractType: string; + effectiveFrom: string; + effectiveTo?: string; + commissionRate: number; + status: string; + terminatedAt?: string; + terminationReason?: string; + createdAt: string; +} + +export interface AgentContractSaveReq { + agentId: number; + carrierId: number; + contractType: string; + effectiveFrom: string; + effectiveTo?: string; + commissionRate: number; +} + +export interface AgentContractSearchParam { + agentId?: number; + agentName?: string; + carrierId?: number; + status?: string; + pageNum?: number; + pageSize?: number; +} + +export const agentContractApi = { + list: (p: AgentContractSearchParam) => + unwrap>( + api.get('/api/agent-contracts', { params: p }), + ), + create: (body: AgentContractSaveReq) => + unwrap(api.post('/api/agent-contracts', body)), + update: (id: number, body: Partial) => + unwrap(api.put(`/api/agent-contracts/${id}`, body)), + terminate: (id: number, reason: string) => + unwrap(api.post(`/api/agent-contracts/${id}/terminate`, { reason })), + delete: (id: number) => + unwrap(api.delete(`/api/agent-contracts/${id}`)), +}; diff --git a/ga-frontend/src/api/approval.ts b/ga-frontend/src/api/approval.ts new file mode 100644 index 0000000..8598e0d --- /dev/null +++ b/ga-frontend/src/api/approval.ts @@ -0,0 +1,101 @@ +import api, { PageResponse, unwrap } from './request'; + +export interface ApprovalLineRow extends Record { + lineId: number; + lineName: string; + description: string; + stepCount: number; + status: string; + createdAt: string; + steps?: ApprovalStep[]; +} + +export interface ApprovalStep extends Record { + stepId: number; + lineId: number; + stepOrder: number; + approverId: number; + approverName: string; + approverTitle?: string; + role: string; +} + +export interface ApprovalRequestRow extends Record { + requestId: number; + lineId: number; + lineName: string; + requesterId: number; + requesterName: string; + title: string; + content: string; + currentStep: number; + totalSteps: number; + status: string; // PENDING / IN_PROGRESS / APPROVED / REJECTED + currentApproverName?: string; + requestedAt: string; + completedAt?: string; + steps?: ApprovalRequestStep[]; +} + +export interface ApprovalRequestStep extends Record { + stepId: number; + stepOrder: number; + approverId: number; + approverName: string; + status: string; // PENDING / APPROVED / REJECTED + comment?: string; + processedAt?: string; +} + +export interface ApprovalLineSaveReq { + lineName: string; + description: string; + steps: { stepOrder: number; approverId: number; role: string }[]; +} + +export interface ApprovalRequestSaveReq { + lineId: number; + title: string; + content: string; +} + +export interface ApprovalSearchParam { + status?: string; + requesterId?: number; + pageNum?: number; + pageSize?: number; +} + +export const approvalApi = { + // Lines + lines: (p?: ApprovalSearchParam) => + unwrap>( + api.get('/api/approvals/lines', { params: p }), + ), + lineCreate: (body: ApprovalLineSaveReq) => + unwrap(api.post('/api/approvals/lines', body)), + lineUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/approvals/lines/${id}`, body)), + lineDelete: (id: number) => + unwrap(api.delete(`/api/approvals/lines/${id}`)), + + // Requests + requests: (p?: ApprovalSearchParam) => + unwrap>( + api.get('/api/approvals/requests', { params: p }), + ), + myRequests: (p?: ApprovalSearchParam) => + unwrap>( + api.get('/api/approvals/requests/mine', { params: p }), + ), + pendingRequests: (p?: ApprovalSearchParam) => + unwrap>( + api.get('/api/approvals/requests/pending', { params: p }), + ), + requestCreate: (body: ApprovalRequestSaveReq) => + unwrap(api.post('/api/approvals/requests', body)), + requestDetail: (id: number) => + unwrap(api.get(`/api/approvals/requests/${id}`)), + advance: (id: number, body: { approved: boolean; comment?: string }) => + unwrap(api.post(`/api/approvals/requests/${id}/advance`, body)), +}; diff --git a/ga-frontend/src/api/attachment.ts b/ga-frontend/src/api/attachment.ts new file mode 100644 index 0000000..b0df29a --- /dev/null +++ b/ga-frontend/src/api/attachment.ts @@ -0,0 +1,39 @@ +import api, { unwrap } from './request'; + +export interface AttachmentMeta extends Record { + attachmentId: number; + refType: string; + refId: number; + originalName: string; + storedPath: string; + fileSize: number; + mimeType: string; + uploadedBy: number; + uploaderName: string; + uploadedAt: string; +} + +export const attachmentApi = { + list: (refType: string, refId: number) => + unwrap( + api.get('/api/attachments', { params: { refType, refId } }), + ), + upload: (refType: string, refId: number, file: File, onProgress?: (pct: number) => void) => { + const form = new FormData(); + form.append('file', file); + form.append('refType', refType); + form.append('refId', String(refId)); + return unwrap( + api.post('/api/attachments', form, { + headers: { 'Content-Type': 'multipart/form-data' }, + onUploadProgress: (evt) => { + if (onProgress && evt.total) { + onProgress(Math.round((evt.loaded / evt.total) * 100)); + } + }, + }), + ); + }, + delete: (id: number) => + unwrap(api.delete(`/api/attachments/${id}`)), +}; diff --git a/ga-frontend/src/api/commission.ts b/ga-frontend/src/api/commission.ts new file mode 100644 index 0000000..65e571f --- /dev/null +++ b/ga-frontend/src/api/commission.ts @@ -0,0 +1,230 @@ +import api, { PageResponse, unwrap } from './request'; + +// ── 공통 ────────────────────────────────────────── +export interface CommissionSearchParam { + agentId?: number; + agentName?: string; + settleMonth?: string; // YYYYMM + commissionType?: string; + pageNum?: number; + pageSize?: number; +} + +// ── CommissionMaster ────────────────────────────── +export interface CommissionMasterRow extends Record { + masterId: number; + agentId: number; + agentName: string; + settleMonth: string; + commissionType: string; + commissionTypeName: string; + grossAmount: number; + netAmount: number; + status: string; +} + +// ── CollectionCommission ────────────────────────── +export interface CollectionRuleRow extends Record { + ruleId: number; + carrierId: number; + carrierName: string; + productType: string; + paymentCycle: string; + ratePercent: number; + effectiveFrom: string; + effectiveTo?: string; + status: string; +} + +export interface CollectionLedgerRow extends Record { + ledgerId: number; + agentId: number; + agentName: string; + contractNo: string; + settleMonth: string; + collectionAmount: number; + commissionAmount: number; + ratePercent: number; + status: string; +} + +// ── RenewalCommission ───────────────────────────── +export interface RenewalRuleRow extends Record { + ruleId: number; + carrierId: number; + carrierName: string; + productType: string; + ratePercent: number; + effectiveFrom: string; + effectiveTo?: string; + status: string; +} + +export interface RenewalLedgerRow extends Record { + ledgerId: number; + agentId: number; + agentName: string; + contractNo: string; + settleMonth: string; + renewalPremium: number; + commissionAmount: number; + status: string; +} + +// ── ReinstatementCommission ─────────────────────── +export interface ReinstatementRuleRow extends Record { + ruleId: number; + carrierId: number; + carrierName: string; + productType: string; + ratePercent: number; + effectiveFrom: string; + effectiveTo?: string; + status: string; +} + +export interface ReinstatementLedgerRow extends Record { + ledgerId: number; + agentId: number; + agentName: string; + contractNo: string; + settleMonth: string; + reinstatementPremium: number; + commissionAmount: number; + status: string; +} + +// ── PersistencyBonus ────────────────────────────── +export interface PersistencyRuleRow extends Record { + ruleId: number; + bonusType: string; // 13M/25M/37M/49M + minRate: number; + bonusAmount?: number; + bonusRate?: number; + effectiveFrom: string; + effectiveTo?: string; + status: string; +} + +export interface PersistencyBonusRow extends Record { + bonusId: number; + agentId: number; + agentName: string; + bonusType: string; + evaluationMonth: string; + retentionRate: number; + bonusAmount: number; + paidYn: string; + status: string; +} + +// ── OverrideLedger ──────────────────────────────── +export interface OverrideLedgerRow extends Record { + ledgerId: number; + fromAgentId: number; + fromAgentName: string; + toAgentId: number; + toAgentName: string; + settleMonth: string; + baseCommission: number; + overrideRate: number; + overrideAmount: number; + orgId: number; + orgName: string; + status: string; +} + +export interface OverrideLedgerSearchParam { + fromAgentId?: number; + toAgentId?: number; + agentName?: string; + settleMonth?: string; + orgId?: number; + pageNum?: number; + pageSize?: number; +} + +// ── API ─────────────────────────────────────────── +export const commissionApi = { + // CommissionMaster + masterList: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/commissions', { params: p }), + ), + masterByAgent: (agentId: number, yyyymm: string) => + unwrap( + api.get(`/api/commissions/agent/${agentId}`, { params: { yyyymm } }), + ), + + // CollectionCommission + collectionRules: (p?: Partial) => + unwrap>( + api.get('/api/collection-commissions/rules', { params: p }), + ), + collectionRuleCreate: (body: Partial) => + unwrap(api.post('/api/collection-commissions/rules', body)), + collectionRuleUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/collection-commissions/rules/${id}`, body)), + collectionRuleDelete: (id: number) => + unwrap(api.delete(`/api/collection-commissions/rules/${id}`)), + collectionLedgers: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/collection-commissions/ledgers', { params: p }), + ), + + // RenewalCommission + renewalRules: (p?: Partial) => + unwrap>( + api.get('/api/renewal-commissions/rules', { params: p }), + ), + renewalRuleCreate: (body: Partial) => + unwrap(api.post('/api/renewal-commissions/rules', body)), + renewalRuleUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/renewal-commissions/rules/${id}`, body)), + renewalRuleDelete: (id: number) => + unwrap(api.delete(`/api/renewal-commissions/rules/${id}`)), + renewalLedgers: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/renewal-commissions/ledgers', { params: p }), + ), + + // ReinstatementCommission + reinstatementRules: (p?: Partial) => + unwrap>( + api.get('/api/reinstatement-commissions/rules', { params: p }), + ), + reinstatementRuleCreate: (body: Partial) => + unwrap(api.post('/api/reinstatement-commissions/rules', body)), + reinstatementRuleUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/reinstatement-commissions/rules/${id}`, body)), + reinstatementRuleDelete: (id: number) => + unwrap(api.delete(`/api/reinstatement-commissions/rules/${id}`)), + reinstatementLedgers: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/reinstatement-commissions/ledgers', { params: p }), + ), + + // PersistencyBonus + persistencyRules: (p?: Partial) => + unwrap>( + api.get('/api/persistency-bonus/rules', { params: p }), + ), + persistencyRuleCreate: (body: Partial) => + unwrap(api.post('/api/persistency-bonus/rules', body)), + persistencyRuleUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/persistency-bonus/rules/${id}`, body)), + persistencyRuleDelete: (id: number) => + unwrap(api.delete(`/api/persistency-bonus/rules/${id}`)), + persistencyBonuses: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/persistency-bonus/bonuses', { params: p }), + ), + persistencyBonusUpsert: (body: Partial) => + unwrap(api.post('/api/persistency-bonus/bonuses', body)), + + // OverrideLedger + overrideLedgers: (p: OverrideLedgerSearchParam) => + unwrap>( + api.get('/api/override-ledgers', { params: p }), + ), +}; diff --git a/ga-frontend/src/api/complaint.ts b/ga-frontend/src/api/complaint.ts new file mode 100644 index 0000000..ce421c5 --- /dev/null +++ b/ga-frontend/src/api/complaint.ts @@ -0,0 +1,53 @@ +import api, { PageResponse, unwrap } from './request'; + +export interface ComplaintRow extends Record { + complaintId: number; + contractNo: string; + agentId: number; + agentName: string; + complaintType: string; + content: string; + receivedDate: string; + status: string; + closedAt?: string; + chargebackTriggered: boolean; + chargebackAmount?: number; + processNote?: string; + createdAt: string; +} + +export interface ComplaintSaveReq { + contractNo: string; + agentId: number; + complaintType: string; + content: string; + receivedDate: string; + chargebackTriggered: boolean; +} + +export interface ComplaintSearchParam { + contractNo?: string; + agentId?: number; + agentName?: string; + complaintType?: string; + status?: string; + startDate?: string; + endDate?: string; + pageNum?: number; + pageSize?: number; +} + +export const complaintApi = { + list: (p: ComplaintSearchParam) => + unwrap>( + api.get('/api/complaints', { params: p }), + ), + create: (body: ComplaintSaveReq) => + unwrap(api.post('/api/complaints', body)), + update: (id: number, body: Partial) => + unwrap(api.put(`/api/complaints/${id}`, body)), + close: (id: number, processNote: string) => + unwrap(api.post(`/api/complaints/${id}/close`, { processNote })), + delete: (id: number) => + unwrap(api.delete(`/api/complaints/${id}`)), +}; diff --git a/ga-frontend/src/api/endorsement.ts b/ga-frontend/src/api/endorsement.ts new file mode 100644 index 0000000..30fc7d0 --- /dev/null +++ b/ga-frontend/src/api/endorsement.ts @@ -0,0 +1,53 @@ +import api, { PageResponse, unwrap } from './request'; + +export interface ContractEndorsementRow extends Record { + endorsementId: number; + contractNo: string; + agentId: number; + agentName: string; + endorsementType: string; // PREMIUM / INSURED / BENEFICIARY / ETC + endorsementTypeName: string; + changeDescription: string; + endorsementDate: string; + recalculateRequired: boolean; + recalculatedAt?: string; + status: string; + processedAt?: string; + createdAt: string; +} + +export interface ContractEndorsementSaveReq { + contractNo: string; + agentId: number; + endorsementType: string; + changeDescription: string; + endorsementDate: string; + recalculateRequired: boolean; +} + +export interface EndorsementSearchParam { + contractNo?: string; + agentId?: number; + agentName?: string; + endorsementType?: string; + status?: string; + startDate?: string; + endDate?: string; + pageNum?: number; + pageSize?: number; +} + +export const endorsementApi = { + list: (p: EndorsementSearchParam) => + unwrap>( + api.get('/api/contract-endorsements', { params: p }), + ), + create: (body: ContractEndorsementSaveReq) => + unwrap(api.post('/api/contract-endorsements', body)), + update: (id: number, body: Partial) => + unwrap(api.put(`/api/contract-endorsements/${id}`, body)), + process: (id: number) => + unwrap(api.post(`/api/contract-endorsements/${id}/process`)), + delete: (id: number) => + unwrap(api.delete(`/api/contract-endorsements/${id}`)), +}; diff --git a/ga-frontend/src/api/kpi.ts b/ga-frontend/src/api/kpi.ts new file mode 100644 index 0000000..fa6f3b1 --- /dev/null +++ b/ga-frontend/src/api/kpi.ts @@ -0,0 +1,102 @@ +import api, { PageResponse, unwrap } from './request'; + +// ──────────────────────────────────────────────── +// 공통 검색 파라미터 +// ──────────────────────────────────────────────── + +export interface KpiSearchParam { + fromMonth?: string; // YYYYMM + toMonth?: string; // YYYYMM + orgId?: number; + agentId?: number; + carrierId?: number; + levelType?: 'AGENT' | 'ORG'; + pageNum?: number; + pageSize?: number; +} + +// ──────────────────────────────────────────────── +// 응답 타입 +// ──────────────────────────────────────────────── + +export interface MonthlySummaryRow extends Record { + yyyymm: string; + agentCount: number; + totalRecruit: number; + totalMaintain: number; + totalOverride: number; + totalChargeback: number; + totalGross: number; + totalTax: number; + totalNet: number; + totalIncentive: number; + refreshedAt?: string; +} + +export interface OrgSummaryRow extends Record { + orgId: number; + orgName: string; + orgType?: string; + yyyymm: string; + agentCount: number; + totalRecruit: number; + totalMaintain: number; + totalGross: number; + totalNet: number; + refreshedAt?: string; +} + +export interface RetentionRow extends Record { + contractMonth: string; + carrierId?: number; + carrierName?: string; + totalContracts: number; + retained13m: number; + retained25m: number; + retentionRate13m: number; + retentionRate25m: number; + refreshedAt?: string; +} + +export interface CumulativeRow extends Record { + levelType: 'AGENT' | 'ORG'; + entityId: number; + entityName: string; + orgId?: number; + orgName?: string; + firstSettleMonth?: string; + lastSettleMonth?: string; + settleMonthCount: number; + cumGross: number; + cumNet: number; + cumChargeback: number; + cumOverride: number; + cumIncentive: number; + refreshedAt?: string; +} + +// ──────────────────────────────────────────────── +// API 함수 +// ──────────────────────────────────────────────── + +export const kpiApi = { + monthlySummary: (p: KpiSearchParam) => + unwrap>( + api.get('/api/kpi/monthly-summary', { params: p }), + ), + + orgSummary: (p: KpiSearchParam) => + unwrap>( + api.get('/api/kpi/org-summary', { params: p }), + ), + + retention: (p: KpiSearchParam) => + unwrap>( + api.get('/api/kpi/retention', { params: p }), + ), + + cumulative: (p: KpiSearchParam) => + unwrap>( + api.get('/api/kpi/cumulative', { params: p }), + ), +}; diff --git a/ga-frontend/src/api/license.ts b/ga-frontend/src/api/license.ts new file mode 100644 index 0000000..7869f23 --- /dev/null +++ b/ga-frontend/src/api/license.ts @@ -0,0 +1,49 @@ +import api, { PageResponse, unwrap } from './request'; + +export interface AgentLicenseRow extends Record { + licenseId: number; + agentId: number; + agentName: string; + licenseType: string; // LIFE / NONLIFE / VARIABLE + licenseTypeName: string; + licenseNo: string; + issuedAt: string; + expiresAt: string; + renewedAt?: string; + status: string; + daysUntilExpiry?: number; +} + +export interface AgentLicenseSaveReq { + agentId: number; + licenseType: string; + licenseNo: string; + issuedAt: string; + expiresAt: string; +} + +export interface AgentLicenseSearchParam { + agentId?: number; + agentName?: string; + licenseType?: string; + status?: string; + pageNum?: number; + pageSize?: number; +} + +export const licenseApi = { + list: (p: AgentLicenseSearchParam) => + unwrap>( + api.get('/api/agent-licenses', { params: p }), + ), + expiring: (days = 30) => + unwrap( + api.get('/api/agent-licenses/expiring', { params: { days } }), + ), + create: (body: AgentLicenseSaveReq) => + unwrap(api.post('/api/agent-licenses', body)), + update: (id: number, body: Partial) => + unwrap(api.put(`/api/agent-licenses/${id}`, body)), + delete: (id: number) => + unwrap(api.delete(`/api/agent-licenses/${id}`)), +}; diff --git a/ga-frontend/src/api/notice.ts b/ga-frontend/src/api/notice.ts new file mode 100644 index 0000000..e3943f1 --- /dev/null +++ b/ga-frontend/src/api/notice.ts @@ -0,0 +1,48 @@ +import api, { PageResponse, unwrap } from './request'; + +export interface NoticeRow extends Record { + noticeId: number; + title: string; + content: string; + noticeType: string; // GENERAL / IMPORTANT / SYSTEM + isPinned: boolean; + authorId: number; + authorName: string; + viewCount: number; + publishedAt?: string; + expiresAt?: string; + status: string; + createdAt: string; +} + +export interface NoticeSaveReq { + title: string; + content: string; + noticeType: string; + isPinned: boolean; + publishedAt?: string; + expiresAt?: string; +} + +export interface NoticeSearchParam { + title?: string; + noticeType?: string; + status?: string; + pageNum?: number; + pageSize?: number; +} + +export const noticeApi = { + list: (p?: NoticeSearchParam) => + unwrap>( + api.get('/api/notices', { params: p }), + ), + detail: (id: number) => + unwrap(api.get(`/api/notices/${id}`)), + create: (body: NoticeSaveReq) => + unwrap(api.post('/api/notices', body)), + update: (id: number, body: Partial) => + unwrap(api.put(`/api/notices/${id}`, body)), + delete: (id: number) => + unwrap(api.delete(`/api/notices/${id}`)), +}; diff --git a/ga-frontend/src/api/notification.ts b/ga-frontend/src/api/notification.ts new file mode 100644 index 0000000..190437f --- /dev/null +++ b/ga-frontend/src/api/notification.ts @@ -0,0 +1,27 @@ +import api, { PageResponse, unwrap } from './request'; + +export interface UserNotificationRow extends Record { + notificationId: number; + userId: number; + title: string; + content: string; + notificationType: string; + refType?: string; + refId?: number; + isRead: boolean; + readAt?: string; + createdAt: string; +} + +export const notificationApi = { + list: (p?: { isRead?: boolean; pageNum?: number; pageSize?: number }) => + unwrap>( + api.get('/api/user-notifications', { params: p }), + ), + unreadCount: () => + unwrap(api.get('/api/user-notifications/unread-count')), + markRead: (id: number) => + unwrap(api.post(`/api/user-notifications/${id}/read`)), + markAllRead: () => + unwrap(api.post('/api/user-notifications/read-all')), +}; diff --git a/ga-frontend/src/api/training.ts b/ga-frontend/src/api/training.ts new file mode 100644 index 0000000..90d80d6 --- /dev/null +++ b/ga-frontend/src/api/training.ts @@ -0,0 +1,70 @@ +import api, { PageResponse, unwrap } from './request'; + +export interface AgentTrainingRow extends Record { + trainingId: number; + agentId: number; + agentName: string; + trainingName: string; + trainingType: string; + completedHours: number; + trainingDate: string; + provider: string; + status: string; + year: number; +} + +export interface TrainingRequirement extends Record { + requirementId: number; + trainingType: string; + requiredHours: number; + year: number; +} + +export interface AnnualHoursSummary extends Record { + agentId: number; + agentName: string; + year: number; + completedHours: number; + requiredHours: number; + satisfactionRate: number; + satisfied: boolean; +} + +export interface AgentTrainingSaveReq { + agentId: number; + trainingName: string; + trainingType: string; + completedHours: number; + trainingDate: string; + provider: string; +} + +export interface AgentTrainingSearchParam { + agentId?: number; + agentName?: string; + year?: number; + trainingType?: string; + pageNum?: number; + pageSize?: number; +} + +export const trainingApi = { + list: (p: AgentTrainingSearchParam) => + unwrap>( + api.get('/api/agent-trainings', { params: p }), + ), + annualHours: (agentId: number, year: number) => + unwrap( + api.get('/api/agent-trainings/annual-hours', { params: { agentId, year } }), + ), + requirements: () => + unwrap( + api.get('/api/training-requirements'), + ), + create: (body: AgentTrainingSaveReq) => + unwrap(api.post('/api/agent-trainings', body)), + update: (id: number, body: Partial) => + unwrap(api.put(`/api/agent-trainings/${id}`, body)), + delete: (id: number) => + unwrap(api.delete(`/api/agent-trainings/${id}`)), +}; diff --git a/ga-frontend/src/api/withdraw.ts b/ga-frontend/src/api/withdraw.ts new file mode 100644 index 0000000..1c86ef6 --- /dev/null +++ b/ga-frontend/src/api/withdraw.ts @@ -0,0 +1,45 @@ +import api, { PageResponse, unwrap } from './request'; + +export interface WithdrawRequestRow extends Record { + withdrawId: number; + agentId: number; + agentName: string; + settleMonth: string; + requestAmount: number; + bankCode: string; + accountNo: string; + accountHolder: string; + status: string; // PENDING / IN_APPROVAL / APPROVED / SENT / COMPLETED / FAILED + approvalRequestId?: number; + approvalStatus?: string; + requestedAt: string; + completedAt?: string; + failReason?: string; +} + +export interface WithdrawRequestSaveReq { + settleMonth: string; + requestAmount: number; + bankCode: string; + accountNo: string; + accountHolder: string; +} + +export interface WithdrawSearchParam { + agentId?: number; + agentName?: string; + settleMonth?: string; + status?: string; + viewAll?: boolean; + pageNum?: number; + pageSize?: number; +} + +export const withdrawApi = { + list: (p: WithdrawSearchParam) => + unwrap>( + api.get('/api/withdraw-requests', { params: p }), + ), + create: (body: WithdrawRequestSaveReq) => + unwrap(api.post('/api/withdraw-requests', body)), +}; diff --git a/ga-frontend/src/components/biz/ApprovalProgress.tsx b/ga-frontend/src/components/biz/ApprovalProgress.tsx new file mode 100644 index 0000000..e4108d5 --- /dev/null +++ b/ga-frontend/src/components/biz/ApprovalProgress.tsx @@ -0,0 +1,59 @@ +import { Steps, Typography } from 'antd'; +import { IconCheck, IconX, IconClock } from '@tabler/icons-react'; +import { ApprovalRequestStep } from '@/api/approval'; +import { COLOR_SUCCESS, COLOR_ERROR, COLOR_PRIMARY, GRAY } from '@/theme/tokens'; + +const { Text } = Typography; + +interface Props { + steps: ApprovalRequestStep[]; + currentStep: number; +} + +function stepIcon(status: string) { + if (status === 'APPROVED') return ; + if (status === 'REJECTED') return ; + return ; +} + +function stepStatus(status: string): 'finish' | 'error' | 'process' | 'wait' { + if (status === 'APPROVED') return 'finish'; + if (status === 'REJECTED') return 'error'; + if (status === 'IN_PROGRESS') return 'process'; + return 'wait'; +} + +/** + * 결재선 진행 시각화 컴포넌트. + * Steps 수평 렌더링 + 단계별 상태 색상. + */ +export default function ApprovalProgress({ steps, currentStep }: Props) { + const items = steps.map((s) => ({ + title: ( + + {s.approverName} + + ), + description: ( +
+ {s.comment &&
{s.comment}
} + {s.processedAt && ( +
+ {s.processedAt.slice(0, 10)} +
+ )} +
+ ), + icon: stepIcon(s.status), + status: stepStatus(s.status), + })); + + return ( + + ); +} diff --git a/ga-frontend/src/components/biz/AttachmentUpload.tsx b/ga-frontend/src/components/biz/AttachmentUpload.tsx new file mode 100644 index 0000000..d344ce8 --- /dev/null +++ b/ga-frontend/src/components/biz/AttachmentUpload.tsx @@ -0,0 +1,122 @@ +import { useState } from 'react'; +import { Button, List, Progress, Space, Typography, Upload, message } from 'antd'; +import { IconFile, IconTrash, IconUpload } from '@tabler/icons-react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { attachmentApi, AttachmentMeta } from '@/api/attachment'; +import { GRAY, RADIUS, SHADOW, COLOR_ERROR } from '@/theme/tokens'; + +const { Text } = Typography; +const { Dragger } = Upload; + +interface Props { + refType: string; + refId: number; +} + +/** + * 공용 첨부파일 업로드/목록/삭제 컴포넌트. + * refType + refId 로 연결. Drag+Drop 지원. + */ +export default function AttachmentUpload({ refType, refId }: Props) { + const qc = useQueryClient(); + const [uploading, setUploading] = useState(false); + const [progress, setProgress] = useState(0); + + const { data: files = [], isLoading } = useQuery({ + queryKey: ['attachments', refType, refId], + queryFn: () => attachmentApi.list(refType, refId), + enabled: !!refId, + }); + + const handleUpload = async (file: File) => { + setUploading(true); + setProgress(0); + try { + await attachmentApi.upload(refType, refId, file, setProgress); + message.success(`${file.name} 업로드 완료`); + qc.invalidateQueries({ queryKey: ['attachments', refType, refId] }); + } catch { + message.error('업로드 실패'); + } finally { + setUploading(false); + setProgress(0); + } + return false; // prevent antd auto-upload + }; + + const handleDelete = async (id: number, name: string) => { + try { + await attachmentApi.delete(id); + message.success(`${name} 삭제 완료`); + qc.invalidateQueries({ queryKey: ['attachments', refType, refId] }); + } catch { + message.error('삭제 실패'); + } + }; + + const fmtSize = (bytes: number) => { + if (bytes < 1024) return `${bytes}B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`; + return `${(bytes / 1024 / 1024).toFixed(1)}MB`; + }; + + return ( +
+ +

+ + 파일을 드래그하거나 클릭하여 업로드 (최대 50MB) +

+
+ + {uploading && ( + + )} + + {!isLoading && files.length > 0 && ( + ( + } + style={{ color: COLOR_ERROR }} + onClick={() => handleDelete(f.attachmentId, f.originalName)} + />, + ]} + > + + + {f.originalName} + ({fmtSize(f.fileSize)}) + + + )} + /> + )} +
+ ); +} diff --git a/ga-frontend/src/components/biz/NotificationBell.tsx b/ga-frontend/src/components/biz/NotificationBell.tsx new file mode 100644 index 0000000..1553947 --- /dev/null +++ b/ga-frontend/src/components/biz/NotificationBell.tsx @@ -0,0 +1,116 @@ +import { Badge, Button, Dropdown, Empty, Space, Typography } from 'antd'; +import { IconBell } from '@tabler/icons-react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { notificationApi, UserNotificationRow } from '@/api/notification'; +import { GRAY, COLOR_PRIMARY, RADIUS, SHADOW } from '@/theme/tokens'; + +const { Text } = Typography; + +/** + * MainLayout 우측 상단 알림 벨. + * 미읽음 카운트 빨간 dot + Dropdown 최근 10개 리스트. + */ +export default function NotificationBell() { + const qc = useQueryClient(); + + const { data: count = 0 } = useQuery({ + queryKey: ['notifications', 'unread-count'], + queryFn: notificationApi.unreadCount, + refetchInterval: 30_000, + }); + + const { data: page } = useQuery({ + queryKey: ['notifications', 'list'], + queryFn: () => notificationApi.list({ pageSize: 10 }), + }); + + const notifications = page?.list ?? []; + + const handleRead = async (id: number) => { + await notificationApi.markRead(id); + qc.invalidateQueries({ queryKey: ['notifications'] }); + }; + + const handleReadAll = async () => { + await notificationApi.markAllRead(); + qc.invalidateQueries({ queryKey: ['notifications'] }); + }; + + const overlay = ( +
+
+ 알림 + {count > 0 && ( + + )} +
+ + {notifications.length === 0 ? ( + + ) : ( +
+ {notifications.map((n: UserNotificationRow) => ( +
!n.isRead && handleRead(n.notificationId)} + style={{ + padding: '10px 16px', + cursor: n.isRead ? 'default' : 'pointer', + background: n.isRead ? '#fff' : '#EBF3FF', + borderBottom: `1px solid ${GRAY[100]}`, + }} + > + + + {n.title} + + {n.content} + + {n.createdAt?.slice(0, 16)} + + +
+ ))} +
+ )} +
+ ); + + return ( + overlay} + placement="bottomRight" + trigger={['click']} + > + + + } + /> + ); + } + return this.props.children; + } +} diff --git a/ga-frontend/src/components/common/PageContainer.tsx b/ga-frontend/src/components/common/PageContainer.tsx index ade568b..f0b02fc 100644 --- a/ga-frontend/src/components/common/PageContainer.tsx +++ b/ga-frontend/src/components/common/PageContainer.tsx @@ -1,5 +1,9 @@ import { ReactNode } from 'react'; import { PageContainer as ProPageContainer } from '@ant-design/pro-components'; +import { Typography } from 'antd'; +import { GRAY } from '@/theme/tokens'; + +const { Text } = Typography; interface Props { title: string; @@ -9,22 +13,33 @@ interface Props { } /** - * Ant Design Pro 의 PageContainer 래퍼. - * - 자동 Breadcrumb / Title / Tabs / Footer 슬롯 제공 - * - extra 는 우측 상단 액션 영역 + * 모든 페이지 공통 래퍼. + * - 타이틀 + 설명 + 우측 액션 버튼 슬롯 + * - breadcrumb 자동 표시 (ProPageContainer 기본) + * - 배경 / 간격 일관성 */ export default function PageContainer({ title, description, extra, children }: Props) { return ( + {title} + + ), breadcrumb: {}, - ghost: false, + ghost: true, extra, + style: { + paddingBottom: description ? 8 : 0, + }, }} content={ - description ? {description} : undefined + description ? ( + {description} + ) : undefined } + style={{ minHeight: 'calc(100vh - 56px)' }} > {children} diff --git a/ga-frontend/src/components/common/SearchForm.tsx b/ga-frontend/src/components/common/SearchForm.tsx index 30ac9ff..9c7557e 100644 --- a/ga-frontend/src/components/common/SearchForm.tsx +++ b/ga-frontend/src/components/common/SearchForm.tsx @@ -1,16 +1,20 @@ -import { Button, Col, DatePicker, Form, Input, Row, Space } from 'antd'; +import { useState } from 'react'; +import { Button, Col, DatePicker, Form, Input, Row, Space, Typography } from 'antd'; +import { IconChevronDown, IconChevronUp, IconSearch, IconX } from '@tabler/icons-react'; import dayjs from 'dayjs'; import CodeSelect from './CodeSelect'; +import { GRAY, COLOR_PRIMARY } from '@/theme/tokens'; +const { Text } = Typography; const { RangePicker } = DatePicker; export interface SearchCondition { type: 'text' | 'code' | 'dateRange' | 'month'; name: string; label: string; - groupCode?: string; // type=code 일 때 + groupCode?: string; // type=code 일 때 placeholder?: string; - span?: number; // 24그리드 기준 너비 (default 6) + span?: number; // 24그리드 기준 너비 (default 6) } interface Props { @@ -18,14 +22,25 @@ interface Props { initialValues?: Record; onSearch: (values: Record) => void; onReset?: () => void; + /** 초기에 접힘 상태로 시작할지 (default: false = 펼침) */ + defaultCollapsed?: boolean; } /** * 조건 배열만 넘기면 자동 렌더링되는 검색 폼. - * dateRange → startDate/endDate 로 분해, month → settleMonth(yyyyMM) + * - 펼침/접힘 토글 지원 + * - dateRange → startDate/endDate 분해 + * - month → settleMonth (yyyyMM) */ -export default function SearchForm({ conditions, initialValues, onSearch, onReset }: Props) { - const [form] = Form.useForm(); +export default function SearchForm({ + conditions, + initialValues, + onSearch, + onReset, + defaultCollapsed = false, +}: Props) { + const [form] = Form.useForm(); + const [collapsed, setCollapsed] = useState(defaultCollapsed); const handleSearch = (raw: Record) => { const out: Record = {}; @@ -50,35 +65,95 @@ export default function SearchForm({ conditions, initialValues, onSearch, onRese }; return ( -
- - {conditions.map((c) => ( - - - {renderField(c)} - - - ))} - - - - - - - -
+ {/* 헤더 — 클릭해서 펼침/접힘 */} +
setCollapsed(!collapsed)} + style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '10px 16px', + cursor: 'pointer', + background: GRAY[50], + borderBottom: collapsed ? 'none' : `1px solid ${GRAY[100]}`, + userSelect: 'none', + }} + > + + + 검색 조건 + {collapsed && ( + + (클릭하여 펼치기) + + )} + + {collapsed + ? + : + } +
+ + {/* 폼 본체 */} + {!collapsed && ( +
+ + {conditions.map((c) => ( + + + {c.label} + + } + style={{ marginBottom: 14 }} + > + {renderField(c)} + + + ))} + + {/* 버튼 컬럼 */} + + + + + + + +
+ )} +
); } diff --git a/ga-frontend/src/index.css b/ga-frontend/src/index.css index 96be04b..e02d4e6 100644 --- a/ga-frontend/src/index.css +++ b/ga-frontend/src/index.css @@ -1,24 +1,340 @@ @import './styles/variables.css'; -html, body, #root { height: 100%; margin: 0; padding: 0; } +/* ──────────────────────────────────────────────── + 기본 리셋 +──────────────────────────────────────────────── */ +html, body, #root { + height: 100%; + margin: 0; + padding: 0; +} body { - font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", - "Malgun Gothic", "맑은 고딕", system-ui, sans-serif; - background: #f5f5f5; - font-size: 13px; + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, + 'Apple SD Gothic Neo', 'Malgun Gothic', '맑은 고딕', + system-ui, sans-serif; + font-size: 15px; + background: var(--ga-gray-50); + color: var(--ga-gray-800); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -/* 숫자 셀에 등폭 숫자 (정렬 일관) */ -.ga-tabular { font-variant-numeric: tabular-nums; } +/* ──────────────────────────────────────────────── + 숫자 등폭 — 금액/날짜 컬럼 정렬 +──────────────────────────────────────────────── */ +.ga-tabular { + font-variant-numeric: tabular-nums; + font-feature-settings: 'tnum'; +} -/* 레이아웃 콘테이너 */ -.ga-page-container { max-width: var(--ga-page-max-width); margin: 0 auto; } +/* 금액 표시 공통 */ +.ga-amount { + font-feature-settings: 'tnum'; + font-variant-numeric: tabular-nums; +} + +/* ──────────────────────────────────────────────── + 페이지 컨테이너 최대 폭 +──────────────────────────────────────────────── */ +.ga-page-container { + max-width: var(--ga-page-max-width); + margin: 0 auto; +} + +/* ──────────────────────────────────────────────── + 사이드바 — 토스 활성 메뉴 인디케이터 +──────────────────────────────────────────────── */ +.ant-menu-item-selected { + position: relative; +} + +.ant-menu-item-selected::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + height: 60%; + width: 4px; + background: var(--ga-primary); + border-radius: 0 4px 4px 0; +} + +/* ──────────────────────────────────────────────── + Ant Design Table — 토스 스타일 +──────────────────────────────────────────────── */ +.ant-table-thead > tr > th { + position: sticky; + top: 0; + z-index: 2; + background: var(--ga-gray-50) !important; + font-weight: 600; + font-size: 13px; + color: var(--ga-gray-600); + border-bottom: 1px solid var(--ga-gray-200) !important; +} + +/* zebra 줄무늬 제거 — 토스는 hover 만 */ +.ant-table-tbody > tr > td { + height: 56px; + font-size: 14px; + color: var(--ga-gray-800); +} + +.ant-table-tbody > tr:hover > td { + background: var(--ga-gray-50) !important; +} + +/* ──────────────────────────────────────────────── + Ant Design Pagination — 토스풍 +──────────────────────────────────────────────── */ +.ant-table-pagination.ant-pagination { + justify-content: flex-end !important; + margin-top: 16px !important; +} + +.ant-pagination-item { + border-radius: 8px !important; + border-color: var(--ga-gray-200) !important; +} + +.ant-pagination-item-active { + background: var(--ga-primary) !important; + border-color: var(--ga-primary) !important; +} + +.ant-pagination-item-active a { + color: #ffffff !important; +} + +.ant-pagination-prev .ant-pagination-item-link, +.ant-pagination-next .ant-pagination-item-link { + border-radius: 8px !important; +} + +/* ──────────────────────────────────────────────── + AG Grid — quartz 테마 토스 커스터마이징 +──────────────────────────────────────────────── */ +.ag-theme-quartz { + --ag-font-family: 'Pretendard', -apple-system, system-ui, sans-serif; + --ag-font-size: 14px; + --ag-header-height: 44px; + --ag-row-height: 56px; + --ag-header-background-color: var(--ga-gray-50); + --ag-header-foreground-color: var(--ga-gray-600); + --ag-header-column-separator-color: var(--ga-gray-200); + --ag-odd-row-background-color: #ffffff; + --ag-even-row-background-color: #ffffff; + --ag-row-hover-color: var(--ga-gray-50); + --ag-selected-row-background-color: var(--ga-primary-light); + --ag-border-color: var(--ga-gray-200); + --ag-cell-horizontal-padding: 16px; + --ag-borders: solid 1px; + --ag-border-radius: 0; +} /* AG Grid 합계행 강조 */ .ag-theme-quartz .ag-row-pinned { - background: #fafafa !important; - font-weight: 500; + background: var(--ga-primary-light) !important; + font-weight: 700; + color: var(--ga-gray-800); +} + +/* AG Grid 헤더 폰트 통일 */ +.ag-theme-quartz .ag-header-cell-label { + font-weight: 600; + font-size: 13px; + letter-spacing: 0; +} + +/* ──────────────────────────────────────────────── + Ant Design Card — 토스 카드 스타일 +──────────────────────────────────────────────── */ +.ant-card { + box-shadow: var(--ga-shadow-sm) !important; + border: 1px solid var(--ga-gray-100) !important; + border-radius: 16px !important; + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +.ant-card-head { + border-bottom-color: var(--ga-gray-100) !important; + font-weight: 600 !important; + font-size: 15px !important; +} + +/* ──────────────────────────────────────────────── + 검색 폼 영역 — filled 톤 +──────────────────────────────────────────────── */ +.ant-pro-table-search { + background: #ffffff !important; + border: 1px solid var(--ga-gray-100) !important; + border-radius: var(--ga-radius-lg) !important; + box-shadow: var(--ga-shadow-sm) !important; + margin-bottom: var(--ga-space-4) !important; +} + +/* ──────────────────────────────────────────────── + 입력창 — filled 배경 (토스 스타일 옵션) +──────────────────────────────────────────────── */ +.ga-input-filled .ant-input, +.ga-input-filled .ant-input-affix-wrapper, +.ga-input-filled .ant-select-selector, +.ga-input-filled .ant-picker { + background: var(--ga-gray-100) !important; + border-color: transparent !important; +} + +.ga-input-filled .ant-input:focus, +.ga-input-filled .ant-input-affix-wrapper-focused, +.ga-input-filled .ant-select-focused .ant-select-selector, +.ga-input-filled .ant-picker-focused { + background: #ffffff !important; + border-color: var(--ga-primary) !important; +} + +/* ──────────────────────────────────────────────── + 버튼 — 토스 느낌 (hover 어두워짐) +──────────────────────────────────────────────── */ +.ant-btn { + transition: all 0.2s ease !important; + font-weight: 600 !important; +} + +.ant-btn-primary:hover { + filter: brightness(0.93) !important; +} + +.ant-btn:not(.ant-btn-primary):hover { + border-color: var(--ga-gray-300) !important; + color: var(--ga-gray-800) !important; +} + +/* ──────────────────────────────────────────────── + 모달 — 큰 라운드 +──────────────────────────────────────────────── */ +.ant-modal-content { + border-radius: 20px !important; + overflow: hidden; +} + +.ant-modal { + max-width: 720px; +} + +/* ──────────────────────────────────────────────── + Drawer +──────────────────────────────────────────────── */ +.ant-drawer-content-wrapper { + max-width: 720px; +} + +.ant-drawer-content { + border-radius: 0; +} + +/* ──────────────────────────────────────────────── + Toast 메시지 위치 통일 +──────────────────────────────────────────────── */ +.ant-message { + top: 16px !important; + right: 16px !important; + left: auto !important; +} + +/* ──────────────────────────────────────────────── + 폼 라벨 +──────────────────────────────────────────────── */ +.ant-form-item-label > label { + font-size: 13px !important; + font-weight: 500 !important; + color: var(--ga-gray-600) !important; +} + +/* ──────────────────────────────────────────────── + 태그/칩 — 토스 둥글둥글 +──────────────────────────────────────────────── */ +.ant-tag { + border-radius: 999px !important; + font-weight: 500; + border: none !important; + padding: 2px 10px !important; +} + +/* ──────────────────────────────────────────────── + Skeleton 로딩 공통 +──────────────────────────────────────────────── */ +.ga-skeleton-card { + padding: 24px; + background: #ffffff; + border-radius: var(--ga-radius-lg); + border: 1px solid var(--ga-gray-100); +} + +/* ──────────────────────────────────────────────── + 빈 상태 (EmptyState) +──────────────────────────────────────────────── */ +.ga-empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 64px 24px; + color: var(--ga-gray-400); +} + +/* ──────────────────────────────────────────────── + KPI 카드 트렌드 화살표 +──────────────────────────────────────────────── */ +.ga-kpi-trend-up { color: var(--ga-success); } +.ga-kpi-trend-down { color: var(--ga-danger); } +.ga-kpi-trend-flat { color: var(--ga-gray-500); } + +/* ──────────────────────────────────────────────── + 링크 색상 +──────────────────────────────────────────────── */ +a { + color: var(--ga-primary); + transition: color 0.2s ease; +} +a:hover { + color: var(--ga-primary-hover); +} + +/* ──────────────────────────────────────────────── + 페이지 헤더 (ProPageContainer) +──────────────────────────────────────────────── */ +.ant-pro-page-container-warp-page-header { + background: var(--ga-gray-50) !important; + border-bottom: 1px solid var(--ga-gray-100) !important; +} + +/* ──────────────────────────────────────────────── + 스크롤바 +──────────────────────────────────────────────── */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: var(--ga-gray-50); +} + +::-webkit-scrollbar-thumb { + background: var(--ga-gray-300); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--ga-gray-400); +} + +/* ──────────────────────────────────────────────── + 전환 효과 통일 +──────────────────────────────────────────────── */ +.ant-modal, +.ant-drawer { + transition: all 0.2s ease; } diff --git a/ga-frontend/src/layouts/MainLayout.tsx b/ga-frontend/src/layouts/MainLayout.tsx index f32cf97..a01ffee 100644 --- a/ga-frontend/src/layouts/MainLayout.tsx +++ b/ga-frontend/src/layouts/MainLayout.tsx @@ -1,38 +1,58 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { ProLayout, type MenuDataItem } from '@ant-design/pro-components'; -import { Badge, Button, DatePicker, Dropdown, Space } from 'antd'; +import { Avatar, Button, DatePicker, Dropdown, Space, Typography } from 'antd'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; -import { IconBell, IconLogout, IconUser } from '@tabler/icons-react'; +import { IconChevronDown, IconLogout, IconSettings, IconUser } from '@tabler/icons-react'; import dayjs from 'dayjs'; import { menuApi, type MenuResp } from '@/api/menu'; import { authApi } from '@/api/auth'; import { useAuthStore } from '@/stores/auth'; import { getMenuIcon } from '@/components/common/MenuIcon'; +import ErrorBoundary from '@/components/common/ErrorBoundary'; +import NotificationBell from '@/components/biz/NotificationBell'; +import { + COLOR_PRIMARY, COLOR_PRIMARY_LIGHT, GRAY, LAYOUT, +} from '@/theme/tokens'; + +const { Text } = Typography; + +const COLLAPSE_KEY = 'ga-sider-collapsed'; /** 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), + 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; }); } +/** 로컬스토리지에서 collapse 상태 읽기 */ +function readCollapsed(): boolean { + try { return localStorage.getItem(COLLAPSE_KEY) === 'true'; } catch { return false; } +} + export default function MainLayout() { - const navigate = useNavigate(); - const location = useLocation(); - const auth = useAuthStore(); + const navigate = useNavigate(); + const location = useLocation(); + const auth = useAuthStore(); const [settleMonth, setSettleMonth] = useState(dayjs()); + const [collapsed, setCollapsed] = useState(readCollapsed); + + // collapse 상태 localStorage 저장 + useEffect(() => { + try { localStorage.setItem(COLLAPSE_KEY, String(collapsed)); } catch { /* ignore */ } + }, [collapsed]); const { data: menus = [] } = useQuery({ queryKey: ['menu', 'my'], - queryFn: menuApi.myMenus, + queryFn: menuApi.myMenus, }); const handleLogout = async () => { @@ -43,21 +63,62 @@ export default function MainLayout() { navigate('/login'); }; + const displayName = auth.userName ?? auth.loginId ?? '관리자'; + const roleLabel = auth.roles?.[0] ?? 'USER'; + + const userMenuItems = [ + { + key: 'profile', + label: '내 프로필', + icon: , + onClick: () => navigate('/system/users'), + }, + { + key: 'config', + label: '시스템 설정', + icon: , + onClick: () => navigate('/system/config'), + }, + { type: 'divider' as const }, + { + key: 'logout', + label: '로그아웃', + icon: , + danger: true, + onClick: handleLogout, + }, + ]; + return ( + GA + + } layout="mix" navTheme="light" contentWidth="Fluid" fixedHeader fixSiderbar - siderWidth={220} + siderWidth={LAYOUT.siderWidth} + collapsed={collapsed} + onCollapse={setCollapsed} menu={{ locale: false }} menuDataRender={() => toProMenu(menus)} location={{ pathname: location.pathname }} menuItemRender={(item, dom) => ( -
item.path && !item.path.startsWith('/_grp/') && navigate(item.path)}> +
+ item.path && !item.path.startsWith('/_grp/') && navigate(item.path) + } + > {dom}
)} @@ -65,21 +126,27 @@ export default function MainLayout() { onMenuHeaderClick={() => navigate('/')} token={{ sider: { - colorMenuBackground: '#ffffff', - colorTextMenuSelected: '#185FA5', - colorBgMenuItemSelected: '#E6F1FB', - colorTextMenuItemHover: '#1677FF', + colorMenuBackground: '#ffffff', + colorTextMenuSelected: COLOR_PRIMARY, + colorBgMenuItemSelected: COLOR_PRIMARY_LIGHT, + colorTextMenuItemHover: GRAY[800], + colorTextMenu: GRAY[600], + colorTextMenuSecondary: GRAY[500], + colorTextSubMenuSelected: COLOR_PRIMARY, }, header: { - colorBgHeader: '#ffffff', - colorHeaderTitle: '#185FA5', - heightLayoutHeader: 56, + colorBgHeader: '#ffffff', + colorHeaderTitle: GRAY[800], + heightLayoutHeader: LAYOUT.headerHeight, + colorBgRightActionsItemHover: GRAY[100], }, - bgLayout: '#f5f7fa', + bgLayout: GRAY[50], + colorTextAppListIconHover: COLOR_PRIMARY, }} actionsRender={() => [ - - 정산월 + // 정산월 선택 + + 정산월 , - - - - +
+
+ {/* 상단 브랜드 헤더 */} +
+
+ GA +
+ + GA 정산시스템 + + + GA Commission Management System + +
+ + {/* 로그인 폼 */} +
+
+ 아이디} + name="loginId" + rules={[{ required: true, message: '아이디를 입력하세요' }]} + style={{ marginBottom: 16 }} + > + } + placeholder="아이디 입력" + size="large" + autoFocus + style={{ height: 52, borderRadius: 12, fontSize: 15 }} + /> + + + 비밀번호} + name="password" + rules={[{ required: true, message: '비밀번호를 입력하세요' }]} + style={{ marginBottom: 28 }} + > + } + placeholder="비밀번호 입력" + size="large" + style={{ height: 52, borderRadius: 12, fontSize: 15 }} + /> + + + +
+ + + + + 문의: 시스템 관리자 · 내선 1234 + +
+
); } diff --git a/ga-frontend/src/pages/agent/AgentContracts.tsx b/ga-frontend/src/pages/agent/AgentContracts.tsx new file mode 100644 index 0000000..f9fb353 --- /dev/null +++ b/ga-frontend/src/pages/agent/AgentContracts.tsx @@ -0,0 +1,173 @@ +import { useState } from 'react'; +import { Alert, Form, Input, InputNumber, Modal, Space, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import CodeBadge from '@/components/common/CodeBadge'; +import { + agentContractApi, + AgentContractRow, + AgentContractSaveReq, + AgentContractSearchParam, +} from '@/api/agent-contract'; +import { GRAY, RADIUS, SHADOW, COLOR_WARNING_BG, COLOR_WARNING_BORDER } from '@/theme/tokens'; + +const COLUMNS: ColDef[] = [ + { field: 'agentName', headerName: '설계사', width: 120, pinned: 'left' }, + { field: 'carrierName', headerName: '보험사', width: 140 }, + { field: 'contractType', headerName: '계약유형', width: 120 }, + { field: 'effectiveFrom', headerName: '효력시작', width: 120 }, + { field: 'effectiveTo', headerName: '효력종료', width: 120 }, + { field: 'commissionRate', headerName: '위촉수수료율(%)', width: 140, type: 'numericColumn' }, + { + field: 'status', headerName: '상태', width: 100, + cellRenderer: (p: { value: string }) => , + }, + { field: 'terminatedAt', headerName: '해촉일', width: 120 }, +]; + +export default function AgentContracts() { + const qc = useQueryClient(); + const [params, setParams] = useState({ pageSize: 100 }); + const [createOpen, setCreateOpen] = useState(false); + const [terminateId, setTerminateId] = useState(null); + const [terminateReason, setTerminateReason] = useState(''); + const [form] = Form.useForm(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['agent-contracts', params], + queryFn: () => agentContractApi.list(params), + retry: false, + }); + + const rows = data?.list ?? []; + + const handleCreate = async () => { + const values = await form.validateFields() as AgentContractSaveReq; + try { + await agentContractApi.create(values); + message.success('위촉계약 등록 완료'); + qc.invalidateQueries({ queryKey: ['agent-contracts'] }); + setCreateOpen(false); + form.resetFields(); + } catch { /* handled */ } + }; + + const handleTerminate = async () => { + if (!terminateId) return; + try { + await agentContractApi.terminate(terminateId, terminateReason); + message.success('해촉 처리 완료'); + qc.invalidateQueries({ queryKey: ['agent-contracts'] }); + setTerminateId(null); + setTerminateReason(''); + } catch { /* handled */ } + }; + + return ( + setCreateOpen(true)}> + + 위촉계약 등록 + + } + > + {isError && ( + + )} + + setParams({ ...v as AgentContractSearchParam, pageSize: 100 })} + onReset={() => setParams({ pageSize: 100 })} + /> + + + + rows={rows} + columns={[ + ...COLUMNS, + { + headerName: '액션', width: 180, pinned: 'right', + cellRenderer: (p: { data: AgentContractRow }) => ( + + { setTerminateId(p.data.contractId); setTerminateReason(''); }} + > + 해촉 + + + ), + }, + ]} + loading={isLoading} + height={600} + rowKey="contractId" + getRowStyle={(params) => { + if (params.data?.status === 'TERMINATED') { + return { background: COLOR_WARNING_BG, borderLeft: `3px solid ${COLOR_WARNING_BORDER}` }; + } + return undefined; + }} + /> + + + {/* 등록 모달 */} + setCreateOpen(false)} okText="등록" cancelText="취소"> +
+ + + + + + + + + + + + + + + + + + +
+
+ + {/* 해촉 모달 */} + setTerminateId(null)} + okText="해촉 확정" + okButtonProps={{ danger: true }} + cancelText="취소" + > +
+ + setTerminateReason(e.target.value)} + placeholder="해촉 사유를 입력하세요" + /> + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/agent/AgentLicenses.tsx b/ga-frontend/src/pages/agent/AgentLicenses.tsx new file mode 100644 index 0000000..2b539be --- /dev/null +++ b/ga-frontend/src/pages/agent/AgentLicenses.tsx @@ -0,0 +1,148 @@ +import { useState } from 'react'; +import { Alert, Form, Input, Modal, Space, Tag, Typography, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef, RowClassParams } from '@ag-grid-community/core'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { licenseApi, AgentLicenseRow, AgentLicenseSaveReq, AgentLicenseSearchParam } from '@/api/license'; +import { GRAY, RADIUS, SHADOW, COLOR_WARNING, COLOR_WARNING_BG, COLOR_WARNING_BORDER, COLOR_ERROR, COLOR_ERROR_BG } from '@/theme/tokens'; + +const { Text } = Typography; + +const COLUMNS: ColDef[] = [ + { field: 'agentName', headerName: '설계사', width: 120, pinned: 'left' }, + { field: 'licenseTypeName', headerName: '자격 유형', width: 120 }, + { field: 'licenseNo', headerName: '자격증번호', width: 160 }, + { field: 'issuedAt', headerName: '발급일', width: 120 }, + { field: 'expiresAt', headerName: '만료일', width: 120 }, + { field: 'renewedAt', headerName: '갱신일', width: 120 }, + { + field: 'daysUntilExpiry', headerName: '만료까지', width: 110, + cellRenderer: (p: { value?: number }) => { + const d = p.value; + if (d == null) return '-'; + const color = d <= 0 ? COLOR_ERROR : d <= 30 ? COLOR_WARNING : GRAY[600]; + return {d <= 0 ? '만료' : `D-${d}`}; + }, + }, + { field: 'status', headerName: '상태', width: 90 }, +]; + +export default function AgentLicenses() { + const qc = useQueryClient(); + const [params, setParams] = useState({ pageSize: 100 }); + const [createOpen, setCreateOpen] = useState(false); + const [form] = Form.useForm(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['agent-licenses', params], + queryFn: () => licenseApi.list(params), + retry: false, + }); + + const { data: expiring = [] } = useQuery({ + queryKey: ['agent-licenses', 'expiring'], + queryFn: () => licenseApi.expiring(30), + retry: false, + }); + + const rows = data?.list ?? []; + + const handleCreate = async () => { + const values = await form.validateFields() as AgentLicenseSaveReq; + try { + await licenseApi.create(values); + message.success('자격증 등록 완료'); + qc.invalidateQueries({ queryKey: ['agent-licenses'] }); + setCreateOpen(false); + form.resetFields(); + } catch { /* handled */ } + }; + + const getRowStyle = (p: RowClassParams): Record | undefined => { + const d = p.data?.daysUntilExpiry; + if (d != null && d <= 0) return { background: COLOR_ERROR_BG }; + if (d != null && d <= 30) return { background: COLOR_WARNING_BG, borderLeft: `3px solid ${COLOR_WARNING_BORDER}` }; + return undefined; + }; + + return ( + setCreateOpen(true)}> + + 자격증 등록 + + } + > + {isError && ( + + )} + + {/* 만료 임박 헤더 카드 */} + {expiring.length > 0 && ( +
+ + 30일 내 만료 {expiring.length}건 + + + {expiring.slice(0, 3).map((e) => `${e.agentName} (${e.licenseTypeName})`).join(', ')} + {expiring.length > 3 ? ` 외 ${expiring.length - 3}건` : ''} + +
+ )} + + setParams({ ...v as AgentLicenseSearchParam, pageSize: 100 })} + onReset={() => setParams({ pageSize: 100 })} + /> + + + + rows={rows} + columns={COLUMNS} + loading={isLoading} + height={600} + rowKey="licenseId" + getRowStyle={getRowStyle} + /> + + + setCreateOpen(false)} okText="등록" cancelText="취소"> +
+ + + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/agent/AgentTrainings.tsx b/ga-frontend/src/pages/agent/AgentTrainings.tsx new file mode 100644 index 0000000..be4e436 --- /dev/null +++ b/ga-frontend/src/pages/agent/AgentTrainings.tsx @@ -0,0 +1,205 @@ +import { useState } from 'react'; +import { Alert, Col, Form, Input, InputNumber, Modal, Progress, Row, Select, Space, Typography, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, +} from 'recharts'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { trainingApi, AgentTrainingRow, AgentTrainingSaveReq, AgentTrainingSearchParam } from '@/api/training'; +import { GRAY, RADIUS, SHADOW, COLOR_PRIMARY, COLOR_SUCCESS, COLOR_ERROR } from '@/theme/tokens'; + +const { Text } = Typography; + +const COLUMNS: ColDef[] = [ + { field: 'agentName', headerName: '설계사', width: 120, pinned: 'left' }, + { field: 'trainingName', headerName: '교육명', flex: 1 }, + { field: 'trainingType', headerName: '교육유형', width: 120 }, + { field: 'completedHours', headerName: '이수시간(H)', width: 120, type: 'numericColumn' }, + { field: 'trainingDate', headerName: '교육일', width: 120 }, + { field: 'provider', headerName: '교육기관', width: 140 }, + { field: 'year', headerName: '연도', width: 90 }, + { field: 'status', headerName: '상태', width: 90 }, +]; + +const REQUIRED_HOURS = 12; // 의무 12시간/년 + +export default function AgentTrainings() { + const qc = useQueryClient(); + const currentYear = dayjs().year(); + const [params, setParams] = useState({ year: currentYear, pageSize: 200 }); + const [selectedAgentId, setSelectedAgentId] = useState(null); + const [selectedAgentName, setSelectedAgentName] = useState(''); + const [createOpen, setCreateOpen] = useState(false); + const [form] = Form.useForm(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['agent-trainings', params], + queryFn: () => trainingApi.list(params), + retry: false, + }); + + const { data: annualHours } = useQuery({ + queryKey: ['agent-trainings', 'annual', selectedAgentId, currentYear], + queryFn: () => trainingApi.annualHours(selectedAgentId!, currentYear), + enabled: !!selectedAgentId, + retry: false, + }); + + const rows = data?.list ?? []; + + // 설계사별 연간 합계 계산 (클라이언트사이드) + const agentHoursMap: Record = {}; + rows.forEach((r) => { + agentHoursMap[r.agentId] = (agentHoursMap[r.agentId] ?? 0) + r.completedHours; + }); + + const chartData = selectedAgentId && annualHours ? [ + { label: '이수', hours: annualHours.completedHours }, + { label: '의무', hours: annualHours.requiredHours }, + ] : []; + + const handleCreate = async () => { + const values = await form.validateFields() as AgentTrainingSaveReq; + try { + await trainingApi.create(values); + message.success('교육 이수 등록 완료'); + qc.invalidateQueries({ queryKey: ['agent-trainings'] }); + setCreateOpen(false); + form.resetFields(); + } catch { /* handled */ } + }; + + const satisfactionRate = annualHours + ? Math.min(100, Math.round((annualHours.completedHours / REQUIRED_HOURS) * 100)) + : 0; + + return ( + setCreateOpen(true)}> + + 교육 이수 등록 + + } + > + {isError && ( + + )} + + setParams({ ...v as AgentTrainingSearchParam, pageSize: 200 })} + onReset={() => setParams({ year: currentYear, pageSize: 200 })} + /> + + + {/* 설계사 선택 → 연간 차트 */} + + 설계사별 연간 이수 현황} + style={{ background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm, height: 300 }} + > + + + + + + + + + + + + + + + + + + ); +} diff --git a/ga-frontend/src/pages/commission/CollectionCommission.tsx b/ga-frontend/src/pages/commission/CollectionCommission.tsx new file mode 100644 index 0000000..bbda506 --- /dev/null +++ b/ga-frontend/src/pages/commission/CollectionCommission.tsx @@ -0,0 +1,199 @@ +import { useState } from 'react'; +import { Alert, Button, Form, Input, InputNumber, Modal, Space, Tabs, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { + commissionApi, + CollectionRuleRow, CollectionLedgerRow, + CommissionSearchParam, +} from '@/api/commission'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; + +const RULE_COLS: ColDef[] = [ + { field: 'carrierName', headerName: '보험사', width: 140 }, + { field: 'productType', headerName: '상품유형', width: 120 }, + { field: 'paymentCycle', headerName: '납입주기', width: 100 }, + { field: 'ratePercent', headerName: '수수료율(%)', width: 120, type: 'numericColumn' }, + { field: 'effectiveFrom', headerName: '적용시작', width: 120 }, + { field: 'effectiveTo', headerName: '적용종료', width: 120 }, + { field: 'status', headerName: '상태', width: 90 }, +]; + +const LEDGER_COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 120 }, + { field: 'contractNo', headerName: '계약번호', width: 140 }, + { field: 'collectionAmount', headerName: '수납금액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'ratePercent', headerName: '수수료율(%)', width: 110, type: 'numericColumn' }, + { field: 'commissionAmount', headerName: '수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'status', headerName: '상태', width: 90 }, +]; + +export default function CollectionCommission() { + const qc = useQueryClient(); + const [ledgerParams, setLedgerParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), pageSize: 200, + }); + const [ruleModalOpen, setRuleModalOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); + const [form] = Form.useForm(); + + const { data: rulesData, isLoading: rulesLoading, isError: rulesError } = useQuery({ + queryKey: ['collection', 'rules'], + queryFn: () => commissionApi.collectionRules(), + retry: false, + }); + + const { data: ledgersData, isLoading: ledgersLoading } = useQuery({ + queryKey: ['collection', 'ledgers', ledgerParams], + queryFn: () => commissionApi.collectionLedgers(ledgerParams), + retry: false, + }); + + const rules = rulesData?.list ?? []; + const ledgers = ledgersData?.list ?? []; + + const openCreate = () => { setEditingRule(null); form.resetFields(); setRuleModalOpen(true); }; + const openEdit = (row: CollectionRuleRow) => { setEditingRule(row); form.setFieldsValue(row); setRuleModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + try { + if (editingRule) { + await commissionApi.collectionRuleUpdate(editingRule.ruleId, values); + message.success('수정 완료'); + } else { + await commissionApi.collectionRuleCreate(values); + message.success('등록 완료'); + } + qc.invalidateQueries({ queryKey: ['collection', 'rules'] }); + setRuleModalOpen(false); + } catch { /* request.ts 처리 */ } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '룰 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await commissionApi.collectionRuleDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['collection', 'rules'] }); + }, + }); + + const pinnedLedger = ledgers.length > 0 ? { + agentName: '합계', + collectionAmount: ledgers.reduce((s, r) => s + (r.collectionAmount ?? 0), 0), + commissionAmount: ledgers.reduce((s, r) => s + (r.commissionAmount ?? 0), 0), + } as Partial : undefined; + + const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + + return ( + + {rulesError && ( + + )} + + +
+ + + 룰 등록 + +
+ + rows={rules} + columns={[ + ...RULE_COLS, + { + headerName: '액션', width: 160, pinned: 'right', + cellRenderer: (p: { data: CollectionRuleRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.ruleId)}>삭제 + + ), + }, + ]} + loading={rulesLoading} + height={520} + rowKey="ruleId" + /> + + ), + }, + { + key: 'ledgers', + label: '수금 원장', + children: ( + <> + setLedgerParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setLedgerParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + + rows={ledgers} + columns={LEDGER_COLS} + loading={ledgersLoading} + height={520} + rowKey="ledgerId" + pinnedBottomRow={pinnedLedger} + /> + + + ), + }, + ]} + /> + + setRuleModalOpen(false)} + okText="저장" + cancelText="취소" + > +
+ + + + + + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/commission/CommissionMaster.tsx b/ga-frontend/src/pages/commission/CommissionMaster.tsx new file mode 100644 index 0000000..008c4bf --- /dev/null +++ b/ga-frontend/src/pages/commission/CommissionMaster.tsx @@ -0,0 +1,80 @@ +import { useState } from 'react'; +import { Alert } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import { commissionApi, CommissionMasterRow, CommissionSearchParam } from '@/api/commission'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; + +const COLUMNS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 120, pinned: 'left' }, + { field: 'commissionTypeName', headerName: '수수료 종류', width: 140 }, + { field: 'grossAmount', headerName: '총수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'netAmount', headerName: '실지급', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'status', headerName: '상태', width: 100 }, +]; + +export default function CommissionMaster() { + const [params, setParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), + pageSize: 200, + }); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['commission', 'master', params], + queryFn: () => commissionApi.masterList(params), + retry: false, + }); + + const rows = data?.list ?? []; + + const pinnedRow = rows.length > 0 ? { + agentName: '합계', + commissionTypeName: '', + grossAmount: rows.reduce((s, r) => s + (r.grossAmount ?? 0), 0), + netAmount: rows.reduce((s, r) => s + (r.netAmount ?? 0), 0), + } as Partial : undefined; + + return ( + + setParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + {isError && ( + + )} + + + + rows={rows} + columns={COLUMNS} + loading={isLoading} + height={600} + rowKey="masterId" + pinnedBottomRow={pinnedRow} + /> + + + ); +} diff --git a/ga-frontend/src/pages/commission/OverrideLedger.tsx b/ga-frontend/src/pages/commission/OverrideLedger.tsx new file mode 100644 index 0000000..cec8d2c --- /dev/null +++ b/ga-frontend/src/pages/commission/OverrideLedger.tsx @@ -0,0 +1,104 @@ +import { useState } from 'react'; +import { Alert, Radio, Space, Typography } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import { commissionApi, OverrideLedgerRow, OverrideLedgerSearchParam } from '@/api/commission'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const { Text } = Typography; +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; +const fmtPct = (v: unknown) => `${((v as number) ?? 0).toFixed(2)}%`; + +const COLUMNS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'fromAgentName', headerName: '원천 설계사(From)', width: 150 }, + { field: 'toAgentName', headerName: '수령 설계사(To)', width: 150 }, + { field: 'orgName', headerName: '조직', width: 130 }, + { field: 'baseCommission', headerName: '기준 수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'overrideRate', headerName: '오버라이드율', width: 130, type: 'numericColumn', valueFormatter: (p) => fmtPct(p.value) }, + { field: 'overrideAmount', headerName: '오버라이드액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'status', headerName: '상태', width: 90 }, +]; + +type FilterMode = 'both' | 'from' | 'to'; + +export default function OverrideLedger() { + const [mode, setMode] = useState('both'); + const [params, setParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), + pageSize: 200, + }); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['override', 'ledgers', params, mode], + queryFn: () => commissionApi.overrideLedgers(params), + retry: false, + }); + + const allRows = data?.list ?? []; + + // 클라이언트사이드 방향 필터 + const rows = mode === 'from' + ? allRows.filter((r) => r.fromAgentId === params.fromAgentId) + : mode === 'to' + ? allRows.filter((r) => r.toAgentId === params.toAgentId) + : allRows; + + const pinnedRow = rows.length > 0 ? { + fromAgentName: '합계', + baseCommission: rows.reduce((s, r) => s + (r.baseCommission ?? 0), 0), + overrideAmount: rows.reduce((s, r) => s + (r.overrideAmount ?? 0), 0), + } as Partial : undefined; + + return ( + + 방향 필터: + setMode(e.target.value)} + optionType="button" + buttonStyle="solid" + size="small" + > + 전체 + From 기준 + To 기준 + +
+ } + > + {isError && ( + + )} + + setParams({ ...v as OverrideLedgerSearchParam, pageSize: 200 })} + onReset={() => setParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + + + rows={rows} + columns={COLUMNS} + loading={isLoading} + height={600} + rowKey="ledgerId" + pinnedBottomRow={pinnedRow} + /> + + + ); +} diff --git a/ga-frontend/src/pages/commission/PersistencyBonus.tsx b/ga-frontend/src/pages/commission/PersistencyBonus.tsx new file mode 100644 index 0000000..c648a67 --- /dev/null +++ b/ga-frontend/src/pages/commission/PersistencyBonus.tsx @@ -0,0 +1,241 @@ +import { useState } from 'react'; +import { Alert, Col, Form, Input, InputNumber, Modal, Row, Space, Tabs, Tag, Typography, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { + commissionApi, + PersistencyRuleRow, PersistencyBonusRow, + CommissionSearchParam, +} from '@/api/commission'; +import { GRAY, RADIUS, SHADOW, COLOR_PRIMARY, COLOR_SUCCESS, COLOR_WARNING } from '@/theme/tokens'; + +const { Text } = Typography; +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; +const fmtPct = (v: unknown) => `${((v as number) ?? 0).toFixed(1)}%`; + +// 유지보수수당 등급별 색상 +const BONUS_TYPE_COLOR: Record = { + '13M': COLOR_PRIMARY, + '25M': COLOR_SUCCESS, + '37M': COLOR_WARNING, + '49M': '#7C3AED', +}; + +const RULE_COLS: ColDef[] = [ + { + field: 'bonusType', headerName: '유지 기준', width: 100, + cellRenderer: (p: { value: string }) => ( + + {p.value} + + ), + }, + { field: 'minRate', headerName: '최저유지율(%)', width: 130, type: 'numericColumn', valueFormatter: (p) => fmtPct(p.value) }, + { field: 'bonusAmount', headerName: '정액지급', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'bonusRate', headerName: '정률지급(%)', flex: 1, type: 'numericColumn', valueFormatter: (p) => p.value != null ? fmtPct(p.value) : '-' }, + { field: 'effectiveFrom', headerName: '적용시작', width: 120 }, + { field: 'effectiveTo', headerName: '적용종료', width: 120 }, + { field: 'status', headerName: '상태', width: 90 }, +]; + +const BONUS_COLS: ColDef[] = [ + { field: 'evaluationMonth', headerName: '평가월', width: 110, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 120 }, + { + field: 'bonusType', headerName: '기준', width: 90, + cellRenderer: (p: { value: string }) => ( + + {p.value} + + ), + }, + { field: 'retentionRate', headerName: '유지율(%)', width: 110, type: 'numericColumn', valueFormatter: (p) => fmtPct(p.value) }, + { field: 'bonusAmount', headerName: '지급액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'paidYn', headerName: '지급여부', width: 90 }, + { field: 'status', headerName: '상태', width: 90 }, +]; + +export default function PersistencyBonus() { + const qc = useQueryClient(); + const [bonusParams, setBonusParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), pageSize: 200, + }); + const [ruleModalOpen, setRuleModalOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); + const [form] = Form.useForm(); + + const { data: rulesData, isLoading: rulesLoading, isError } = useQuery({ + queryKey: ['persistency', 'rules'], + queryFn: () => commissionApi.persistencyRules(), + retry: false, + }); + + const { data: bonusData, isLoading: bonusLoading } = useQuery({ + queryKey: ['persistency', 'bonuses', bonusParams], + queryFn: () => commissionApi.persistencyBonuses(bonusParams), + retry: false, + }); + + const rules = rulesData?.list ?? []; + const bonuses = bonusData?.list ?? []; + + // 등급별 통계 카드 + const byType = ['13M', '25M', '37M', '49M'].map((t) => ({ + type: t, + count: bonuses.filter((b) => b.bonusType === t).length, + total: bonuses.filter((b) => b.bonusType === t).reduce((s, b) => s + (b.bonusAmount ?? 0), 0), + })); + + const openCreate = () => { setEditingRule(null); form.resetFields(); setRuleModalOpen(true); }; + const openEdit = (row: PersistencyRuleRow) => { setEditingRule(row); form.setFieldsValue(row); setRuleModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + try { + if (editingRule) { + await commissionApi.persistencyRuleUpdate(editingRule.ruleId, values); + } else { + await commissionApi.persistencyRuleCreate(values); + } + message.success('저장 완료'); + qc.invalidateQueries({ queryKey: ['persistency', 'rules'] }); + setRuleModalOpen(false); + } catch { /* handled */ } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '룰 삭제', onOk: async () => { + await commissionApi.persistencyRuleDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['persistency', 'rules'] }); + }, + }); + + const pinnedBonus = bonuses.length > 0 ? { + agentName: '합계', + bonusAmount: bonuses.reduce((s, b) => s + (b.bonusAmount ?? 0), 0), + } as Partial : undefined; + + const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + + return ( + + {isError && ( + + )} + + +
+ + + 룰 등록 + +
+ + rows={rules} + columns={[ + ...RULE_COLS, + { + headerName: '액션', width: 160, pinned: 'right', + cellRenderer: (p: { data: PersistencyRuleRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.ruleId)}>삭제 + + ), + }, + ]} + loading={rulesLoading} + height={420} + rowKey="ruleId" + /> + + ), + }, + { + key: 'bonuses', + label: '지급 이력', + children: ( + <> + setBonusParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setBonusParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + {/* 등급별 통계 카드 */} + + {byType.map((t) => ( + +
+ {t.type} 유지 +
+ {t.count}건 +
+ {t.total.toLocaleString()}원 +
+ + ))} +
+ + + + rows={bonuses} + columns={BONUS_COLS} + loading={bonusLoading} + height={480} + rowKey="bonusId" + pinnedBottomRow={pinnedBonus} + /> + + + ), + }, + ]} + /> + + setRuleModalOpen(false)} + okText="저장" cancelText="취소" + > +
+ + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/commission/ReinstatementCommission.tsx b/ga-frontend/src/pages/commission/ReinstatementCommission.tsx new file mode 100644 index 0000000..0edc0fc --- /dev/null +++ b/ga-frontend/src/pages/commission/ReinstatementCommission.tsx @@ -0,0 +1,184 @@ +import { useState } from 'react'; +import { Alert, Form, Input, InputNumber, Modal, Space, Tabs, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { + commissionApi, + ReinstatementRuleRow, ReinstatementLedgerRow, + CommissionSearchParam, +} from '@/api/commission'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; + +const RULE_COLS: ColDef[] = [ + { field: 'carrierName', headerName: '보험사', width: 140 }, + { field: 'productType', headerName: '상품유형', width: 120 }, + { field: 'ratePercent', headerName: '수수료율(%)', width: 120, type: 'numericColumn' }, + { field: 'effectiveFrom', headerName: '적용시작', width: 120 }, + { field: 'effectiveTo', headerName: '적용종료', width: 120 }, + { field: 'status', headerName: '상태', width: 90 }, +]; + +const LEDGER_COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 120 }, + { field: 'contractNo', headerName: '계약번호', width: 140 }, + { field: 'reinstatementPremium', headerName: '부활보험료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'commissionAmount', headerName: '수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'status', headerName: '상태', width: 90 }, +]; + +export default function ReinstatementCommission() { + const qc = useQueryClient(); + const [ledgerParams, setLedgerParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), pageSize: 200, + }); + const [ruleModalOpen, setRuleModalOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); + const [form] = Form.useForm(); + + const { data: rulesData, isLoading: rulesLoading, isError } = useQuery({ + queryKey: ['reinstatement', 'rules'], + queryFn: () => commissionApi.reinstatementRules(), + retry: false, + }); + + const { data: ledgersData, isLoading: ledgersLoading } = useQuery({ + queryKey: ['reinstatement', 'ledgers', ledgerParams], + queryFn: () => commissionApi.reinstatementLedgers(ledgerParams), + retry: false, + }); + + const rules = rulesData?.list ?? []; + const ledgers = ledgersData?.list ?? []; + + const openCreate = () => { setEditingRule(null); form.resetFields(); setRuleModalOpen(true); }; + const openEdit = (row: ReinstatementRuleRow) => { setEditingRule(row); form.setFieldsValue(row); setRuleModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + try { + if (editingRule) { + await commissionApi.reinstatementRuleUpdate(editingRule.ruleId, values); + } else { + await commissionApi.reinstatementRuleCreate(values); + } + message.success('저장 완료'); + qc.invalidateQueries({ queryKey: ['reinstatement', 'rules'] }); + setRuleModalOpen(false); + } catch { /* handled */ } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '룰 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await commissionApi.reinstatementRuleDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['reinstatement', 'rules'] }); + }, + }); + + const pinnedLedger = ledgers.length > 0 ? { + agentName: '합계', + reinstatementPremium: ledgers.reduce((s, r) => s + (r.reinstatementPremium ?? 0), 0), + commissionAmount: ledgers.reduce((s, r) => s + (r.commissionAmount ?? 0), 0), + } as Partial : undefined; + + const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + + return ( + + {isError && ( + + )} + + +
+ + + 룰 등록 + +
+ + rows={rules} + columns={[ + ...RULE_COLS, + { + headerName: '액션', width: 160, pinned: 'right', + cellRenderer: (p: { data: ReinstatementRuleRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.ruleId)}>삭제 + + ), + }, + ]} + loading={rulesLoading} + height={480} + rowKey="ruleId" + /> + + ), + }, + { + key: 'ledgers', + label: '부활 원장', + children: ( + <> + setLedgerParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setLedgerParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + + rows={ledgers} + columns={LEDGER_COLS} + loading={ledgersLoading} + height={480} + rowKey="ledgerId" + pinnedBottomRow={pinnedLedger} + /> + + + ), + }, + ]} + /> + + setRuleModalOpen(false)} + okText="저장" cancelText="취소" + > +
+ + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/commission/RenewalCommission.tsx b/ga-frontend/src/pages/commission/RenewalCommission.tsx new file mode 100644 index 0000000..1b5c039 --- /dev/null +++ b/ga-frontend/src/pages/commission/RenewalCommission.tsx @@ -0,0 +1,184 @@ +import { useState } from 'react'; +import { Alert, Button, Form, Input, InputNumber, Modal, Space, Tabs, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { + commissionApi, + RenewalRuleRow, RenewalLedgerRow, + CommissionSearchParam, +} from '@/api/commission'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; + +const RULE_COLS: ColDef[] = [ + { field: 'carrierName', headerName: '보험사', width: 140 }, + { field: 'productType', headerName: '상품유형', width: 120 }, + { field: 'ratePercent', headerName: '수수료율(%)', width: 120, type: 'numericColumn' }, + { field: 'effectiveFrom', headerName: '적용시작', width: 120 }, + { field: 'effectiveTo', headerName: '적용종료', width: 120 }, + { field: 'status', headerName: '상태', width: 90 }, +]; + +const LEDGER_COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 120 }, + { field: 'contractNo', headerName: '계약번호', width: 140 }, + { field: 'renewalPremium', headerName: '갱신보험료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'commissionAmount', headerName: '수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'status', headerName: '상태', width: 90 }, +]; + +export default function RenewalCommission() { + const qc = useQueryClient(); + const [ledgerParams, setLedgerParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), pageSize: 200, + }); + const [ruleModalOpen, setRuleModalOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); + const [form] = Form.useForm(); + + const { data: rulesData, isLoading: rulesLoading, isError } = useQuery({ + queryKey: ['renewal', 'rules'], + queryFn: () => commissionApi.renewalRules(), + retry: false, + }); + + const { data: ledgersData, isLoading: ledgersLoading } = useQuery({ + queryKey: ['renewal', 'ledgers', ledgerParams], + queryFn: () => commissionApi.renewalLedgers(ledgerParams), + retry: false, + }); + + const rules = rulesData?.list ?? []; + const ledgers = ledgersData?.list ?? []; + + const openCreate = () => { setEditingRule(null); form.resetFields(); setRuleModalOpen(true); }; + const openEdit = (row: RenewalRuleRow) => { setEditingRule(row); form.setFieldsValue(row); setRuleModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + try { + if (editingRule) { + await commissionApi.renewalRuleUpdate(editingRule.ruleId, values); + } else { + await commissionApi.renewalRuleCreate(values); + } + message.success('저장 완료'); + qc.invalidateQueries({ queryKey: ['renewal', 'rules'] }); + setRuleModalOpen(false); + } catch { /* handled */ } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '룰 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await commissionApi.renewalRuleDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['renewal', 'rules'] }); + }, + }); + + const pinnedLedger = ledgers.length > 0 ? { + agentName: '합계', + renewalPremium: ledgers.reduce((s, r) => s + (r.renewalPremium ?? 0), 0), + commissionAmount: ledgers.reduce((s, r) => s + (r.commissionAmount ?? 0), 0), + } as Partial : undefined; + + const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + + return ( + + {isError && ( + + )} + + +
+ + + 룰 등록 + +
+ + rows={rules} + columns={[ + ...RULE_COLS, + { + headerName: '액션', width: 160, pinned: 'right', + cellRenderer: (p: { data: RenewalRuleRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.ruleId)}>삭제 + + ), + }, + ]} + loading={rulesLoading} + height={480} + rowKey="ruleId" + /> + + ), + }, + { + key: 'ledgers', + label: '갱신 원장', + children: ( + <> + setLedgerParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setLedgerParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + + rows={ledgers} + columns={LEDGER_COLS} + loading={ledgersLoading} + height={480} + rowKey="ledgerId" + pinnedBottomRow={pinnedLedger} + /> + + + ), + }, + ]} + /> + + setRuleModalOpen(false)} + okText="저장" cancelText="취소" + > +
+ + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/contracts/Complaints.tsx b/ga-frontend/src/pages/contracts/Complaints.tsx new file mode 100644 index 0000000..d0aaed9 --- /dev/null +++ b/ga-frontend/src/pages/contracts/Complaints.tsx @@ -0,0 +1,197 @@ +import { useState } from 'react'; +import { Alert, Checkbox, Form, Input, Modal, Typography, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import CodeBadge from '@/components/common/CodeBadge'; +import AttachmentUpload from '@/components/biz/AttachmentUpload'; +import { complaintApi, ComplaintRow, ComplaintSaveReq, ComplaintSearchParam } from '@/api/complaint'; +import { GRAY, RADIUS, SHADOW, COLOR_ERROR_BG, COLOR_ERROR_BORDER } from '@/theme/tokens'; + +const { Text } = Typography; +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; + +const COLUMNS: ColDef[] = [ + { field: 'contractNo', headerName: '계약번호', width: 150, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 120 }, + { field: 'complaintType', headerName: '민원유형', width: 120 }, + { field: 'content', headerName: '민원내용', flex: 1 }, + { field: 'receivedDate', headerName: '접수일', width: 120 }, + { + field: 'status', headerName: '상태', width: 100, + cellRenderer: (p: { value: string }) => , + }, + { + field: 'chargebackTriggered', headerName: '환수', width: 80, + cellRenderer: (p: { value: boolean }) => + p.value ? 환수 : -, + }, + { field: 'chargebackAmount', headerName: '환수금액', width: 120, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'closedAt', headerName: '처리일', width: 120 }, +]; + +export default function Complaints() { + const qc = useQueryClient(); + const [params, setParams] = useState({ pageSize: 100 }); + const [createOpen, setCreateOpen] = useState(false); + const [closeTarget, setCloseTarget] = useState(null); + const [processNote, setProcessNote] = useState(''); + const [createdId, setCreatedId] = useState(null); + const [form] = Form.useForm(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['complaints', params], + queryFn: () => complaintApi.list(params), + retry: false, + }); + + const rows = data?.list ?? []; + const chargebackCount = rows.filter((r) => r.chargebackTriggered).length; + + const handleCreate = async () => { + const values = await form.validateFields() as ComplaintSaveReq; + try { + const created = await complaintApi.create(values); + message.success('민원 등록 완료'); + qc.invalidateQueries({ queryKey: ['complaints'] }); + setCreatedId(created.complaintId); + setCreateOpen(false); + form.resetFields(); + } catch { /* handled */ } + }; + + const handleClose = async () => { + if (!closeTarget) return; + try { + await complaintApi.close(closeTarget, processNote); + message.success('처리완료'); + qc.invalidateQueries({ queryKey: ['complaints'] }); + setCloseTarget(null); + setProcessNote(''); + } catch { /* handled */ } + }; + + return ( + setCreateOpen(true)}> + + 민원 등록 + + } + > + {isError && ( + + )} + + {chargebackCount > 0 && ( +
+ 환수 민원 {chargebackCount}건 — 정산 시 자동 반영 +
+ )} + + setParams({ ...v as ComplaintSearchParam, pageSize: 100 })} + onReset={() => setParams({ pageSize: 100 })} + /> + + + + rows={rows} + columns={[ + ...COLUMNS, + { + headerName: '액션', width: 120, pinned: 'right', + cellRenderer: (p: { data: ComplaintRow }) => ( + { setCloseTarget(p.data.complaintId); setProcessNote(''); }} + > + 처리완료 + + ), + }, + ]} + loading={isLoading} + height={600} + rowKey="complaintId" + getRowStyle={(params) => { + if (params.data?.chargebackTriggered) { + return { background: '#FEF0F1' }; + } + return undefined; + }} + /> + + + {/* 등록 모달 */} + setCreateOpen(false)} okText="등록" cancelText="취소" width={560}> +
+ + + + + + + + + + + 환수 트리거 (13M 이내 민원) + +
+
+ + {/* 처리완료 모달 */} + setCloseTarget(null)} + okText="처리완료" + cancelText="취소" + > +
+ + setProcessNote(e.target.value)} + placeholder="처리 내용을 입력하세요" + /> + +
+
+ + {/* 첨부파일 (마지막 등록 건) */} + {createdId && ( + + + + )} +
+ ); +} diff --git a/ga-frontend/src/pages/contracts/ContractEndorsements.tsx b/ga-frontend/src/pages/contracts/ContractEndorsements.tsx new file mode 100644 index 0000000..eb204ac --- /dev/null +++ b/ga-frontend/src/pages/contracts/ContractEndorsements.tsx @@ -0,0 +1,160 @@ +import { useState } from 'react'; +import { Alert, Checkbox, Form, Input, Modal, Space, Tag, Typography, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import CodeBadge from '@/components/common/CodeBadge'; +import { endorsementApi, ContractEndorsementRow, ContractEndorsementSaveReq, EndorsementSearchParam } from '@/api/endorsement'; +import { GRAY, RADIUS, SHADOW, COLOR_WARNING_BG, COLOR_WARNING_BORDER } from '@/theme/tokens'; + +const { Text } = Typography; + +const COLUMNS: ColDef[] = [ + { field: 'contractNo', headerName: '계약번호', width: 150, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 120 }, + { field: 'endorsementTypeName', headerName: '변경유형', width: 130 }, + { field: 'changeDescription', headerName: '변경내용', flex: 1 }, + { field: 'endorsementDate', headerName: '변경일', width: 120 }, + { + field: 'recalculateRequired', headerName: '재계산', width: 90, + cellRenderer: (p: { value: boolean }) => p.value + ? 필요 + : 불필요, + }, + { + field: 'status', headerName: '상태', width: 100, + cellRenderer: (p: { value: string }) => , + }, + { field: 'processedAt', headerName: '처리일', width: 120 }, +]; + +export default function ContractEndorsements() { + const qc = useQueryClient(); + const [params, setParams] = useState({ pageSize: 100 }); + const [createOpen, setCreateOpen] = useState(false); + const [form] = Form.useForm(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['endorsements', params], + queryFn: () => endorsementApi.list(params), + retry: false, + }); + + const rows = data?.list ?? []; + const recalcCount = rows.filter((r) => r.recalculateRequired && r.status !== 'PROCESSED').length; + + const handleCreate = async () => { + const values = await form.validateFields() as ContractEndorsementSaveReq; + try { + await endorsementApi.create(values); + message.success('계약 변경 등록 완료'); + qc.invalidateQueries({ queryKey: ['endorsements'] }); + setCreateOpen(false); + form.resetFields(); + } catch { /* handled */ } + }; + + const handleProcess = (id: number) => Modal.confirm({ + title: '처리 확정', + content: '변경사항을 처리 완료로 변경합니다. 재계산이 필요한 경우 정산 배치를 다시 실행하세요.', + onOk: async () => { + await endorsementApi.process(id); + message.success('처리 완료'); + qc.invalidateQueries({ queryKey: ['endorsements'] }); + }, + }); + + return ( + setCreateOpen(true)}> + + 변경 등록 + + } + > + {isError && ( + + )} + + {recalcCount > 0 && ( +
+ 재계산 필요 {recalcCount}건 + 정산 배치 실행 전 확인이 필요합니다. +
+ )} + + setParams({ ...v as EndorsementSearchParam, pageSize: 100 })} + onReset={() => setParams({ pageSize: 100 })} + /> + + + + rows={rows} + columns={[ + ...COLUMNS, + { + headerName: '액션', width: 120, pinned: 'right', + cellRenderer: (p: { data: ContractEndorsementRow }) => ( + handleProcess(p.data.endorsementId)} + > + 처리 + + ), + }, + ]} + loading={isLoading} + height={600} + rowKey="endorsementId" + getRowStyle={(params) => { + if (params.data?.recalculateRequired && params.data?.status !== 'PROCESSED') { + return { background: COLOR_WARNING_BG }; + } + return undefined; + }} + /> + + + setCreateOpen(false)} okText="등록" cancelText="취소"> +
+ + + + + + + + + + + + + 재계산 필요 + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/kpi/KpiCumulative.tsx b/ga-frontend/src/pages/kpi/KpiCumulative.tsx new file mode 100644 index 0000000..cdf1ebc --- /dev/null +++ b/ga-frontend/src/pages/kpi/KpiCumulative.tsx @@ -0,0 +1,249 @@ +import { useMemo, useState } from 'react'; +import { Alert, Col, Row, Tabs, Typography } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery } from '@tanstack/react-query'; +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, + Tooltip, ResponsiveContainer, Cell, +} from 'recharts'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import { kpiApi, CumulativeRow, KpiSearchParam } from '@/api/kpi'; +import { + COLOR_PRIMARY, COLOR_SUCCESS, COLOR_ERROR, GRAY, SHADOW, RADIUS, +} from '@/theme/tokens'; + +const { Text } = Typography; + +const fmt = (v: number) => v.toLocaleString(); +const fmtM = (v: number) => `${(v / 1_000_000).toFixed(1)}M`; + +// ──────────────────────────────────────────────────────── +// 컬럼 (AGENT / ORG 공통 구조) +// ──────────────────────────────────────────────────────── + +function buildColumns(levelType: 'AGENT' | 'ORG'): ColDef[] { + return [ + { + field: 'entityName', + headerName: levelType === 'AGENT' ? '설계사명' : '조직명', + width: 150, pinned: 'left', + }, + { + field: 'orgName', + headerName: '소속 조직', + width: 140, + hide: levelType === 'ORG', + }, + { field: 'settleMonthCount', headerName: '정산 월수', width: 100, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { field: 'cumGross', headerName: '누적 수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { field: 'cumChargeback', headerName: '누적 환수', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { field: 'cumIncentive', headerName: '누적 시책', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { + headerName: '순수익', + flex: 1, + type: 'numericColumn', + sortable: true, + valueGetter: (p) => (p.data?.cumGross ?? 0) - (p.data?.cumChargeback ?? 0), + valueFormatter: (p) => fmt(p.value ?? 0), + cellStyle: (p) => ({ + color: p.value > 0 ? COLOR_SUCCESS : p.value < 0 ? COLOR_ERROR : GRAY[500], + fontWeight: 600, + }), + }, + ]; +} + +// ──────────────────────────────────────────────────────── +// 합계행 계산 +// ──────────────────────────────────────────────────────── + +function buildPinned(rows: CumulativeRow[]): Partial | undefined { + if (!rows.length) return undefined; + return { + entityName: '합계', + cumGross: rows.reduce((s, r) => s + r.cumGross, 0), + cumChargeback: rows.reduce((s, r) => s + r.cumChargeback, 0), + cumIncentive: rows.reduce((s, r) => s + r.cumIncentive, 0), + cumNet: rows.reduce((s, r) => s + r.cumNet, 0), + settleMonthCount: rows.reduce((s, r) => s + r.settleMonthCount, 0), + }; +} + +// ──────────────────────────────────────────────────────── +// Top10 차트 +// ──────────────────────────────────────────────────────── + +const PALETTE = [ + COLOR_PRIMARY, COLOR_SUCCESS, '#F59E0B', '#8B5CF6', '#06B6D4', + '#EC4899', '#14B8A6', '#F97316', '#6366F1', '#84CC16', +]; + +function Top10Chart({ rows }: { rows: CumulativeRow[] }) { + const chartData = useMemo(() => + [...rows] + .map((r) => ({ + name: r.entityName, + 순수익: Math.round((r.cumGross - r.cumChargeback) / 1_000_000), + })) + .sort((a, b) => b.순수익 - a.순수익) + .slice(0, 10), + [rows]); + + return ( + 순수익 상위 10} + subTitle={단위: 백만원 (수수료 - 환수)} + headerBordered + style={{ + background: '#ffffff', borderRadius: RADIUS.lg, + border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm, + marginBottom: 20, + }} + > + + + + `${v}M`} + /> + + [`${fmtM(v * 1_000_000)}`, '순수익']} + /> + + {chartData.map((_, i) => ( + + ))} + + + + + ); +} + +// ──────────────────────────────────────────────────────── +// 탭 패널 +// ──────────────────────────────────────────────────────── + +interface PanelProps { + levelType: 'AGENT' | 'ORG'; + params: KpiSearchParam; +} + +function CumulativePanel({ levelType, params }: PanelProps) { + const { data, isLoading, isError } = useQuery({ + queryKey: ['kpi', 'cumulative', levelType, params], + queryFn: () => kpiApi.cumulative({ ...params, levelType }), + retry: false, + }); + + const rows = data?.list ?? []; + const columns = useMemo(() => buildColumns(levelType), [levelType]); + const pinnedRow = buildPinned(rows); + + return ( + <> + {isError && ( + + )} + + {/* 요약 카드 3개 */} + + {[ + { label: '누적 수수료 합계', value: rows.reduce((s, r) => s + r.cumGross, 0), color: COLOR_PRIMARY, bg: '#EBF3FF' }, + { label: '누적 환수 합계', value: rows.reduce((s, r) => s + r.cumChargeback, 0), color: COLOR_ERROR, bg: '#FEF0F1' }, + { label: '누적 순수익', value: rows.reduce((s, r) => s + r.cumGross - r.cumChargeback, 0), color: COLOR_SUCCESS, bg: '#E8FAF3' }, + ].map((card) => ( + +
+ {card.label} +
+ + {fmtM(card.value)} + + +
+
+ + ))} +
+ + + + + + rows={rows} + columns={columns} + loading={isLoading} + height={500} + pinnedBottomRow={pinnedRow} + /> + + + ); +} + +// ──────────────────────────────────────────────────────── +// 메인 +// ──────────────────────────────────────────────────────── + +export default function KpiCumulative() { + const [activeTab, setActiveTab] = useState<'AGENT' | 'ORG'>('AGENT'); + const [params, setParams] = useState({ + toMonth: dayjs().format('YYYYMM'), + pageSize: 500, + }); + + const handleSearch = (values: Record) => { + setParams({ + toMonth: values.toMonth as string | undefined, + orgId: values.orgId as number | undefined, + pageSize: 500, + }); + }; + + return ( + + setParams({ toMonth: dayjs().format('YYYYMM'), pageSize: 500 })} + /> + + setActiveTab(k as 'AGENT' | 'ORG')} + items={[ + { key: 'AGENT', label: '설계사별' }, + { key: 'ORG', label: '조직별' }, + ]} + style={{ marginBottom: 16 }} + /> + + + + ); +} diff --git a/ga-frontend/src/pages/kpi/KpiMonthlySummary.tsx b/ga-frontend/src/pages/kpi/KpiMonthlySummary.tsx new file mode 100644 index 0000000..565d028 --- /dev/null +++ b/ga-frontend/src/pages/kpi/KpiMonthlySummary.tsx @@ -0,0 +1,318 @@ +import { useMemo, useState } from 'react'; +import { Alert, Button, Col, Row, Space, Typography } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery } from '@tanstack/react-query'; +import { + AreaChart, Area, BarChart, Bar, XAxis, YAxis, + CartesianGrid, Tooltip, ResponsiveContainer, Legend, +} from 'recharts'; +import { + IconTrendingUp, IconTrendingDown, IconMinus, IconDownload, +} from '@tabler/icons-react'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import { kpiApi, MonthlySummaryRow, KpiSearchParam } from '@/api/kpi'; +import { + COLOR_PRIMARY, COLOR_ERROR, COLOR_SUCCESS, + GRAY, SHADOW, RADIUS, +} from '@/theme/tokens'; + +const { Text } = Typography; + +// ──────────────────────────────────────────────────────── +// 유틸 +// ──────────────────────────────────────────────────────── + +const fmt = (v: number) => v.toLocaleString(); +const fmtM = (v: number) => `${(v / 1_000_000).toFixed(1)}M`; + +function calcPct(curr: number, prev: number): number | null { + if (prev === 0) return null; + return Math.round(((curr - prev) / Math.abs(prev)) * 1000) / 10; +} + +// ──────────────────────────────────────────────────────── +// 서브 컴포넌트 — KPI 카드 +// ──────────────────────────────────────────────────────── + +interface KpiCardProps { + label: string; + value: number; + unit?: string; + delta?: number | null; + deltaLabel?: string; + color?: string; + bg?: string; +} + +function KpiCard({ label, value, unit = '원', delta, deltaLabel, color = COLOR_PRIMARY, bg = '#EBF3FF' }: KpiCardProps) { + const renderDelta = () => { + if (delta === null || delta === undefined) return null; + if (delta > 0) return ( + + +{delta.toFixed(1)}% + + ); + if (delta < 0) return ( + + {delta.toFixed(1)}% + + ); + return ( + + 변동없음 + + ); + }; + + return ( +
+
+ {label} +
+ +
+
+
+ + {fmtM(value)} + + {unit} +
+ {(delta !== null && delta !== undefined) && ( +
+ {renderDelta()} + {deltaLabel && {deltaLabel}} +
+ )} +
+ ); +} + +// ──────────────────────────────────────────────────────── +// AG Grid 컬럼 정의 +// ──────────────────────────────────────────────────────── + +const COLUMNS: ColDef[] = [ + { field: 'yyyymm', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'agentCount', headerName: '설계사수', width: 100, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { field: 'totalGross', headerName: '총수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { field: 'totalTax', headerName: '총공제(세)', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { field: 'totalNet', headerName: '총지급', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { field: 'totalChargeback', headerName: '환수', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { field: 'totalIncentive', headerName: '시책', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, +]; + +// ──────────────────────────────────────────────────────── +// 클라이언트 사이드 엑셀 내보내기 (xlsx 없음 — CSV 대체) +// ──────────────────────────────────────────────────────── + +function exportCsv(rows: MonthlySummaryRow[]) { + const headers = ['정산월', '설계사수', '총수수료', '총공제', '총지급', '환수', '시책']; + const lines = [ + headers.join(','), + ...rows.map((r) => + [r.yyyymm, r.agentCount, r.totalGross, r.totalTax, r.totalNet, r.totalChargeback, r.totalIncentive].join(','), + ), + ]; + const blob = new Blob(['' + lines.join('\n')], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `월별정산요약_${dayjs().format('YYYYMMDD')}.csv`; + link.click(); + URL.revokeObjectURL(link.href); +} + +// ──────────────────────────────────────────────────────── +// 메인 컴포넌트 +// ──────────────────────────────────────────────────────── + +export default function KpiMonthlySummary() { + const [params, setParams] = useState({ + fromMonth: dayjs().subtract(11, 'month').format('YYYYMM'), + toMonth: dayjs().format('YYYYMM'), + pageSize: 100, + }); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['kpi', 'monthly', params], + queryFn: () => kpiApi.monthlySummary(params), + retry: false, + }); + + const rows = data?.list ?? []; + + // KPI 카드: 최신월 / 전월 / YTD 합산 + const thisMonth = rows[rows.length - 1]; + const prevMonth = rows.length >= 2 ? rows[rows.length - 2] : undefined; + const ytdGross = rows.reduce((s, r) => s + (r.totalGross ?? 0), 0); + const grossDelta = thisMonth && prevMonth + ? calcPct(thisMonth.totalGross, prevMonth.totalGross) + : null; + + // 합계행 + const pinnedRow = rows.length > 0 ? { + yyyymm: '합계', + agentCount: rows.reduce((s, r) => s + r.agentCount, 0), + totalGross: rows.reduce((s, r) => s + r.totalGross, 0), + totalTax: rows.reduce((s, r) => s + r.totalTax, 0), + totalNet: rows.reduce((s, r) => s + r.totalNet, 0), + totalChargeback: rows.reduce((s, r) => s + r.totalChargeback, 0), + totalIncentive: rows.reduce((s, r) => s + r.totalIncentive, 0), + } as Partial : undefined; + + // 차트 데이터 — 최근 12개월 + const chartData = useMemo(() => + rows.slice(-12).map((r) => ({ + month: r.yyyymm.slice(4, 6) + '월', + 총수수료: Math.round(r.totalGross / 1_000_000), + 총지급: Math.round(r.totalNet / 1_000_000), + 환수: Math.round(r.totalChargeback / 1_000_000), + })), + [rows]); + + const handleSearch = (values: Record) => { + setParams({ + fromMonth: values.fromMonth as string | undefined, + toMonth: values.toMonth as string | undefined, + pageSize: 100, + }); + }; + + return ( + } + onClick={() => rows.length && exportCsv(rows)} + disabled={!rows.length} + style={{ display: 'flex', alignItems: 'center', gap: 4 }} + > + CSV 다운로드 + + } + > + {/* 검색 */} + setParams({ fromMonth: dayjs().subtract(11, 'month').format('YYYYMM'), toMonth: dayjs().format('YYYYMM'), pageSize: 100 })} + /> + + {isError && ( + + )} + + {/* KPI 카드 3개 */} + + + + + + + + + + + + + {/* 차트 */} + 최근 12개월 수수료 추이} + subTitle={단위: 백만원} + headerBordered + style={{ + background: '#ffffff', borderRadius: RADIUS.lg, + border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm, + marginBottom: 20, + }} + > + + {chartData.length > 0 ? ( + + + + + [`${v}백만`, '']} + /> + + + + + + ) : ( + + + + )} + + + + {/* AG Grid */} + + + rows={rows} + columns={COLUMNS} + loading={isLoading} + height={480} + pinnedBottomRow={pinnedRow} + /> + + + ); +} diff --git a/ga-frontend/src/pages/kpi/KpiOrgSummary.tsx b/ga-frontend/src/pages/kpi/KpiOrgSummary.tsx new file mode 100644 index 0000000..548fbc9 --- /dev/null +++ b/ga-frontend/src/pages/kpi/KpiOrgSummary.tsx @@ -0,0 +1,199 @@ +import { useMemo, useState } from 'react'; +import { Alert, Col, Row, Select, Typography } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery } from '@tanstack/react-query'; +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell, +} from 'recharts'; +import type { ColDef } from '@ag-grid-community/core'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import { kpiApi, OrgSummaryRow, KpiSearchParam } from '@/api/kpi'; +import { + COLOR_PRIMARY, COLOR_SUCCESS, GRAY, SHADOW, RADIUS, +} from '@/theme/tokens'; +import dayjs from 'dayjs'; + +const { Text } = Typography; + +const fmt = (v: number) => v.toLocaleString(); + +// AG Grid 컬럼 +const COLUMNS: ColDef[] = [ + { field: 'orgName', headerName: '조직명', width: 160, pinned: 'left' }, + { field: 'orgType', headerName: '조직유형', width: 110 }, + { field: 'yyyymm', headerName: '정산월', width: 110 }, + { field: 'agentCount', headerName: '설계사수', width: 100, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { field: 'totalGross', headerName: '총수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { field: 'totalNet', headerName: '총지급', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { field: 'totalRecruit', headerName: '모집', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, + { field: 'totalMaintain',headerName: '유지', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) }, +]; + +// 차트 팔레트 +const PALETTE = [ + COLOR_PRIMARY, COLOR_SUCCESS, '#F59E0B', '#8B5CF6', '#06B6D4', + '#EC4899', '#14B8A6', '#F97316', '#6366F1', '#84CC16', +]; + +export default function KpiOrgSummary() { + const [params, setParams] = useState({ + fromMonth: dayjs().format('YYYYMM'), + toMonth: dayjs().format('YYYYMM'), + pageSize: 200, + }); + const [selectedOrg, setSelectedOrg] = useState(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['kpi', 'org', params], + queryFn: () => kpiApi.orgSummary({ ...params, orgId: selectedOrg }), + retry: false, + }); + + const rows = data?.list ?? []; + + // 합계행 + const pinnedRow = rows.length > 0 ? { + orgName: '합계', + agentCount: rows.reduce((s, r) => s + r.agentCount, 0), + totalGross: rows.reduce((s, r) => s + r.totalGross, 0), + totalNet: rows.reduce((s, r) => s + r.totalNet, 0), + totalRecruit: rows.reduce((s, r) => s + (r.totalRecruit ?? 0), 0), + totalMaintain: rows.reduce((s, r) => s + (r.totalMaintain ?? 0), 0), + } as Partial : undefined; + + // 조직 목록 (드롭다운) + const orgOptions = useMemo(() => { + const seen = new Set(); + return rows + .filter((r) => { if (seen.has(r.orgId)) return false; seen.add(r.orgId); return true; }) + .map((r) => ({ value: r.orgId, label: r.orgName })); + }, [rows]); + + // 차트 — 조직별 총수수료 상위 10개 + const chartData = useMemo(() => { + const byOrg: Record = {}; + for (const r of rows) { + if (!byOrg[r.orgId]) byOrg[r.orgId] = { orgName: r.orgName, totalGross: 0 }; + byOrg[r.orgId].totalGross += r.totalGross; + } + return Object.values(byOrg) + .sort((a, b) => b.totalGross - a.totalGross) + .slice(0, 10) + .map((x) => ({ name: x.orgName, 수수료: Math.round(x.totalGross / 1_000_000) })); + }, [rows]); + + const handleSearch = (values: Record) => { + setParams({ + fromMonth: values.fromMonth as string | undefined, + toMonth: values.toMonth as string | undefined, + pageSize: 200, + }); + }; + + return ( + + { + setSelectedOrg(undefined); + setParams({ fromMonth: dayjs().format('YYYYMM'), toMonth: dayjs().format('YYYYMM'), pageSize: 200 }); + }} + /> + + {isError && ( + + )} + + + {/* 좌측: 조직 필터 Select */} + + 조직 선택} + style={{ + background: '#ffffff', borderRadius: RADIUS.lg, + border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm, + height: '100%', + }} + > + + + + `${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')} /> + + + + + + + + + + + + + + ); +} diff --git a/ga-frontend/src/pages/report/ChargebackRiskReport.tsx b/ga-frontend/src/pages/report/ChargebackRiskReport.tsx new file mode 100644 index 0000000..ac69033 --- /dev/null +++ b/ga-frontend/src/pages/report/ChargebackRiskReport.tsx @@ -0,0 +1,103 @@ +import { Alert } from 'antd'; +import { ProTable, type ProColumns } from '@ant-design/pro-components'; +import { useQuery } from '@tanstack/react-query'; +import PageContainer from '@/components/common/PageContainer'; +import MoneyText from '@/components/common/MoneyText'; +import ExcelExportButton from '@/components/common/ExcelExportButton'; +import api, { unwrap } from '@/api/request'; + +interface RiskRow { + agentId: number; + agentName: string; + orgName: string; + policyNo: string; + companyName: string; + recruitMonth: string; + monthsElapsed: number; + recruitAmount: number; + chargebackRate: number; + estimatedChargeback: number; + riskLevel: 'HIGH' | 'MEDIUM' | 'LOW'; +} + +function fetchRisk() { + return unwrap(api.get('/api/report/chargeback-risk')); +} + +const riskColor: Record = { + HIGH: '#f5222d', + MEDIUM: '#fa8c16', + LOW: '#52c41a', +}; + +const riskLabel: Record = { + HIGH: '고위험', + MEDIUM: '중위험', + LOW: '저위험', +}; + +const columns: ProColumns[] = [ + { title: '설계사', dataIndex: 'agentName', width: 100, fixed: 'left' }, + { title: '소속', dataIndex: 'orgName', width: 160 }, + { title: '증권번호', dataIndex: 'policyNo', width: 130 }, + { title: '보험사', dataIndex: 'companyName', width: 110 }, + { title: '모집월', dataIndex: 'recruitMonth', width: 90 }, + { title: '경과월', dataIndex: 'monthsElapsed', width: 80, align: 'right', render: (_, r) => `${r.monthsElapsed}개월` }, + { + title: '모집수수료', dataIndex: 'recruitAmount', width: 120, align: 'right', + render: (_, r) => , + }, + { + title: '환수율(%)', dataIndex: 'chargebackRate', width: 90, align: 'right', + render: (_, r) => `${r.chargebackRate}%`, + }, + { + title: '예상환수액', dataIndex: 'estimatedChargeback', width: 130, align: 'right', + render: (_, r) => , + }, + { + title: '위험등급', dataIndex: 'riskLevel', width: 90, align: 'center', fixed: 'right', + render: (_, r) => ( + + {riskLabel[r.riskLevel] ?? r.riskLevel} + + ), + }, +]; + +export default function ChargebackRiskReport() { + const { data, isLoading, isError } = useQuery({ + queryKey: ['report', 'chargeback-risk'], + queryFn: fetchRisk, + retry: false, + }); + + return ( + } + > + {isError && ( + + )} + + + rowKey={(r) => `${r.agentId}-${r.policyNo}`} + columns={columns} + dataSource={data ?? []} + loading={isLoading} + search={false} + pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}건` }} + options={{ density: false, reload: false }} + scroll={{ x: 1100 }} + /> + + ); +} diff --git a/ga-frontend/src/pages/report/OrgReport.tsx b/ga-frontend/src/pages/report/OrgReport.tsx new file mode 100644 index 0000000..1bfb102 --- /dev/null +++ b/ga-frontend/src/pages/report/OrgReport.tsx @@ -0,0 +1,98 @@ +import { useState } from 'react'; +import { Alert, DatePicker } from 'antd'; +import { ProCard, ProTable, type ProColumns } from '@ant-design/pro-components'; +import { useQuery } from '@tanstack/react-query'; +import dayjs, { type Dayjs } from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import MoneyText from '@/components/common/MoneyText'; +import ExcelExportButton from '@/components/common/ExcelExportButton'; +import api, { unwrap } from '@/api/request'; + +interface OrgRow { + orgId: number; + orgName: string; + orgType: string; + agentCount: number; + recruitTotal: number; + maintainTotal: number; + netAmount: number; +} + +function fetchOrgReport(settleMonth: string) { + return unwrap(api.get('/api/report/org', { params: { settleMonth } })); +} + +const columns: ProColumns[] = [ + { title: '조직명', dataIndex: 'orgName', width: 180, fixed: 'left' }, + { title: '유형', dataIndex: 'orgType', width: 80 }, + { title: '설계사 수', dataIndex: 'agentCount', width: 90, align: 'right' }, + { + title: '모집수수료', dataIndex: 'recruitTotal', width: 130, align: 'right', + render: (_, r) => , + }, + { + title: '유지수수료', dataIndex: 'maintainTotal', width: 130, align: 'right', + render: (_, r) => , + }, + { + title: '실지급 합계', dataIndex: 'netAmount', width: 140, align: 'right', + render: (_, r) => , + }, +]; + +export default function OrgReport() { + const [settleMonth, setSettleMonth] = useState(dayjs()); + + const monthStr = settleMonth.format('YYYYMM'); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['report', 'org', monthStr], + queryFn: () => fetchOrgReport(monthStr), + retry: false, + }); + + return ( + + } + > + {isError && ( + + )} + + + d && setSettleMonth(d)} + allowClear={false} + format="YYYY-MM" + /> + + + + rowKey="orgId" + columns={columns} + dataSource={data ?? []} + loading={isLoading} + search={false} + pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}건` }} + options={{ density: false, reload: false }} + scroll={{ x: 800 }} + /> + + ); +} diff --git a/ga-frontend/src/pages/report/PerformanceReport.tsx b/ga-frontend/src/pages/report/PerformanceReport.tsx new file mode 100644 index 0000000..3beca22 --- /dev/null +++ b/ga-frontend/src/pages/report/PerformanceReport.tsx @@ -0,0 +1,96 @@ +import { useState } from 'react'; +import { Alert, Col, Empty, Row, Spin } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery } from '@tanstack/react-query'; +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, +} from 'recharts'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import ExcelExportButton from '@/components/common/ExcelExportButton'; +import api, { unwrap } from '@/api/request'; + +interface PerfRow { + settleMonth: string; + agentCount: number; + recruitTotal: number; + maintainTotal: number; + netAmount: number; +} + +function fetchPerf(year: string) { + return unwrap(api.get('/api/report/performance', { params: { year } })); +} + +export default function PerformanceReport() { + const [year] = useState(() => dayjs().format('YYYY')); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['report', 'performance', year], + queryFn: () => fetchPerf(year), + retry: false, + }); + + return ( + } + > + {isError && ( + + )} + + + + {data && data.length > 0 ? ( + + + + + `${(Number(v) / 1_000_000).toFixed(0)}백만`} /> + Number(v).toLocaleString()} /> + + + + + + + ) : ( + + )} + + + + {data && data.length > 0 && ( + + + {data.map((row) => ( + + +
{row.settleMonth}
+
설계사: {row.agentCount}명
+
+ 모집: {(row.recruitTotal ?? 0).toLocaleString()} +
+
+ 유지: {(row.maintainTotal ?? 0).toLocaleString()} +
+
+ 실지급: {(row.netAmount ?? 0).toLocaleString()} +
+
+ + ))} +
+
+ )} +
+ ); +} diff --git a/ga-frontend/src/pages/system/Approvals.tsx b/ga-frontend/src/pages/system/Approvals.tsx new file mode 100644 index 0000000..ac42348 --- /dev/null +++ b/ga-frontend/src/pages/system/Approvals.tsx @@ -0,0 +1,261 @@ +import { useState } from 'react'; +import { Alert, Button, Drawer, Form, Input, InputNumber, Modal, Space, Tabs, Tag, Typography, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import PageContainer from '@/components/common/PageContainer'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import CodeBadge from '@/components/common/CodeBadge'; +import ApprovalProgress from '@/components/biz/ApprovalProgress'; +import { + approvalApi, + ApprovalLineRow, ApprovalRequestRow, + ApprovalRequestSaveReq, +} from '@/api/approval'; +import { GRAY, RADIUS, SHADOW, COLOR_SUCCESS, COLOR_ERROR, COLOR_PRIMARY } from '@/theme/tokens'; + +const { Text } = Typography; +const { TextArea } = Input; + +const LINE_COLS: ColDef[] = [ + { field: 'lineName', headerName: '결재선명', flex: 1 }, + { field: 'description', headerName: '설명', flex: 1 }, + { field: 'stepCount', headerName: '단계수', width: 80, type: 'numericColumn' }, + { field: 'status', headerName: '상태', width: 90 }, + { field: 'createdAt', headerName: '생성일', width: 120 }, +]; + +const REQ_COLS: ColDef[] = [ + { field: 'title', headerName: '제목', flex: 1 }, + { field: 'lineName', headerName: '결재선', width: 140 }, + { field: 'requesterName', headerName: '요청자', width: 110 }, + { field: 'currentApproverName', headerName: '현재 결재자', width: 130 }, + { + headerName: '진행', width: 120, + cellRenderer: (p: { data: ApprovalRequestRow }) => ( + {p.data.currentStep} / {p.data.totalSteps} 단계 + ), + }, + { + field: 'status', headerName: '상태', width: 100, + cellRenderer: (p: { value: string }) => , + }, + { field: 'requestedAt', headerName: '요청일', width: 120 }, +]; + +export default function Approvals() { + const qc = useQueryClient(); + const [drawerOpen, setDrawerOpen] = useState(false); + const [selectedRequest, setSelectedRequest] = useState(null); + const [approveComment, setApproveComment] = useState(''); + const [createReqOpen, setCreateReqOpen] = useState(false); + const [form] = Form.useForm(); + + const { data: linesData, isLoading: linesLoading, isError } = useQuery({ + queryKey: ['approval', 'lines'], + queryFn: () => approvalApi.lines(), + retry: false, + }); + + const { data: myReqData, isLoading: myReqLoading } = useQuery({ + queryKey: ['approval', 'mine'], + queryFn: () => approvalApi.myRequests(), + retry: false, + }); + + const { data: pendingData, isLoading: pendingLoading } = useQuery({ + queryKey: ['approval', 'pending'], + queryFn: () => approvalApi.pendingRequests(), + retry: false, + }); + + const lines = linesData?.list ?? []; + const myRequests = myReqData?.list ?? []; + const pendingRequests = pendingData?.list ?? []; + + const openDetail = async (req: ApprovalRequestRow) => { + try { + const detail = await approvalApi.requestDetail(req.requestId); + setSelectedRequest(detail); + setDrawerOpen(true); + } catch { /* handled */ } + }; + + const handleAdvance = async (approved: boolean) => { + if (!selectedRequest) return; + try { + await approvalApi.advance(selectedRequest.requestId, { approved, comment: approveComment }); + message.success(approved ? '승인 완료' : '반려 완료'); + qc.invalidateQueries({ queryKey: ['approval'] }); + setDrawerOpen(false); + setApproveComment(''); + } catch { /* handled */ } + }; + + const handleCreateReq = async () => { + const values = await form.validateFields() as ApprovalRequestSaveReq; + try { + await approvalApi.requestCreate(values); + message.success('결재 요청 완료'); + qc.invalidateQueries({ queryKey: ['approval'] }); + setCreateReqOpen(false); + form.resetFields(); + } catch { /* handled */ } + }; + + const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + + return ( + setCreateReqOpen(true)}> + + 결재 요청 + + } + > + {isError && ( + + )} + + + + rows={lines} + columns={LINE_COLS} + loading={linesLoading} + height={480} + rowKey="lineId" + /> + + ), + }, + { + key: 'mine', + label: `내 결재 요청 (${myRequests.length})`, + children: ( + + + rows={myRequests} + columns={REQ_COLS} + loading={myReqLoading} + height={480} + rowKey="requestId" + /> + + ), + }, + { + key: 'pending', + label: ( + + 결재 대기{' '} + {pendingRequests.length > 0 && ( + + {pendingRequests.length} + + )} + + ), + children: ( + + + rows={pendingRequests} + columns={[ + ...REQ_COLS, + { + headerName: '상세', width: 80, pinned: 'right', + cellRenderer: (p: { data: ApprovalRequestRow }) => ( + + ), + }, + ]} + loading={pendingLoading} + height={480} + rowKey="requestId" + /> + + ), + }, + ]} + /> + + {/* 상세 Drawer */} + setDrawerOpen(false)} + width={480} + footer={ + + + + + } + > + {selectedRequest && ( + +
+ 요청자 +
{selectedRequest.requesterName}
+
+
+ 내용 +
{selectedRequest.content}
+
+ + {selectedRequest.steps && selectedRequest.steps.length > 0 && ( +
+ 결재 진행 현황 + +
+ )} + +
+ 코멘트 +