From c2a92d13cb634c92a466b849dd609222a379c9b3 Mon Sep 17 00:00:00 2001 From: GA Pro Date: Fri, 22 May 2026 00:48:08 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=A0=95=EC=82=B0=20=EB=B0=B0=EC=B9=98?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EC=A0=95?= =?UTF-8?q?=ED=95=A9=20=E2=80=94=20=EB=A7=A4=ED=8D=BC=20NULL=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20+=20override=5Fsettle=20=EC=A0=9C?= =?UTF-8?q?=EC=95=BD=20+=20=EC=A0=95=EC=82=B0=EC=A7=80=EA=B8=89=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 정산/지급 화면이 전부 비어있던 원인: 운영 DB에서 정산 배치가 실행된 적 없음. MONTHLY_SETTLEMENT 배치를 돌리는 과정에서 발견한 버그 수정 + 데이터 구성. 매퍼 버그 (PG: could not determine data type of parameter): - RegulatoryLimitMapper/ChargebackGradeMapper/InstallmentRatioMapper: #{productCategory} 가 IS NULL 비교에 쓰여 타입 추론 실패 -> jdbcType=VARCHAR 명시 - ForecastKpiMonthlyMapper: #{targetOrgId} 동일 -> jdbcType=BIGINT 명시 스키마 (V79): - override_settle.settle_id NOT NULL 완화 — calcOverride(Step7)는 settle_master 생성(Step8) 전 단계라 settle_id 를 채울 수 없음 데이터 (V80): - 배치 실행 결과: settle_master 150 / 원장 433 / 명세서 112 / 분개 433 (2026-02~04) - payment 는 settle_master 파생 생성, 운영성 도메인(공제/마감/세금계산서/이의신청/ 시책/정정)은 대표 샘플 시드 (멱등 NOT EXISTS) 검증: 배치 12스텝 COMPLETED, 정산/지급 10개 엔드포인트 200 OK. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../V79__정산배치_override_settle_보정.sql | 13 ++ .../db/migration/V80__정산지급_샘플데이터.sql | 134 ++++++++++++++++++ .../chargeback/ChargebackGradeMapper.xml | 8 +- .../installment/InstallmentRatioMapper.xml | 8 +- .../mapper/kpi/ForecastKpiMonthlyMapper.xml | 4 +- .../regulatory/RegulatoryLimitMapper.xml | 4 +- 6 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 ga-common/src/main/resources/db/migration/V79__정산배치_override_settle_보정.sql create mode 100644 ga-common/src/main/resources/db/migration/V80__정산지급_샘플데이터.sql diff --git a/ga-common/src/main/resources/db/migration/V79__정산배치_override_settle_보정.sql b/ga-common/src/main/resources/db/migration/V79__정산배치_override_settle_보정.sql new file mode 100644 index 0000000..b039110 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V79__정산배치_override_settle_보정.sql @@ -0,0 +1,13 @@ +-- V79: 정산 배치 파이프라인 정합 보정 — override_settle.settle_id NOT NULL 완화 +-- +-- 배경: +-- MONTHLY_SETTLEMENT 배치는 Step7(calcOverride) 에서 override_settle 을 적재한 뒤 +-- Step8(aggregate) 에서 settle_master 를 생성한다. 즉 오버라이드 계산 시점에는 +-- 아직 settle_master 행이 없어 settle_id 를 채울 수 없다. +-- override_settle 은 (settle_month, upper_agent_id) 로 식별되며 AggregateStep 도 +-- 해당 키로 집계(aggregateByUpper)하므로 settle_id 는 선택적 역참조 컬럼이다. +-- +-- 조치: settle_id 의 NOT NULL 제약을 해제 (FK 는 유지, NULL 허용). +ALTER TABLE override_settle ALTER COLUMN settle_id DROP NOT NULL; + +COMMENT ON COLUMN override_settle.settle_id IS '연결 settle_master (선택적 역참조 — 정산 집계 이후 백필 가능, 배치 적재 시점엔 NULL)'; diff --git a/ga-common/src/main/resources/db/migration/V80__정산지급_샘플데이터.sql b/ga-common/src/main/resources/db/migration/V80__정산지급_샘플데이터.sql new file mode 100644 index 0000000..0ffd826 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V80__정산지급_샘플데이터.sql @@ -0,0 +1,134 @@ +-- V80: 정산/지급 영역 데이터 구성 +-- +-- MONTHLY_SETTLEMENT 배치(2026-02~04)로 settle_master / 원장 / 명세서 / 분개는 적재 완료. +-- payment(지급) 는 settle_master 에서 파생 생성하고, 배치 파이프라인이 없는 +-- 운영성 도메인(공제/마감/세금계산서/이의신청/시책/정정)은 대표 샘플을 시드한다. +-- 모든 블록은 멱등 (NOT EXISTS 가드). + +-- ============================================================ +-- 1. payment — settle_master 파생 (정산 1건 → 지급 1건) +-- 202602 = DONE(지급완료), 202603/202604 = PENDING(지급대기) +-- ============================================================ +INSERT INTO payment (settle_id, agent_id, pay_amount, bank_code, account_no, + pay_status, pay_date, income_tax_amount, local_tax_amount, + vat_amount, deduction_amount, net_amount, created_at) +SELECT sm.settle_id, sm.agent_id, sm.gross_amount, a.bank_code, a.account_no, + CASE WHEN sm.settle_month = '202602' THEN 'DONE' ELSE 'PENDING' END, + CASE WHEN sm.settle_month = '202602' THEN DATE '2026-03-10' END, + ROUND(sm.tax_amount * 0.909, 0), -- 소득세분 (지방세 제외 근사) + ROUND(sm.tax_amount * 0.091, 0), -- 지방소득세분 + 0, 0, sm.net_amount, now() +FROM settle_master sm +JOIN agent a ON a.agent_id = sm.agent_id +WHERE NOT EXISTS (SELECT 1 FROM payment p WHERE p.settle_id = sm.settle_id); + +-- ============================================================ +-- 2. agent_deduction — 공제 샘플 (settle_master 설계사 기준 12건) +-- ============================================================ +INSERT INTO agent_deduction (agent_id, settle_month, deduction_type, amount, description, status, created_by) +SELECT r.agent_id, '202604', t.dtype, t.amt, t.descr, t.st, 1 +FROM ( + SELECT agent_id, row_number() OVER (ORDER BY agent_id) AS rn + FROM settle_master WHERE settle_month = '202604' +) r +JOIN (VALUES + (1,'LOAN', 800000, '단기 대출 상환분', 'APPLIED'), + (2,'ADVANCE', 500000, '선지급 수수료 환수', 'APPLIED'), + (3,'GUARANTEE',300000, '영업보증금 분할 납입', 'PENDING'), + (4,'DUES', 50000, '협회비', 'APPLIED'), + (5,'AD', 120000, '공동 광고비 분담', 'PENDING'), + (6,'CHARGEBACK_PENDING', 650000, '환수 보류분', 'PENDING'), + (7,'LOAN', 400000, '대출 상환분', 'APPLIED'), + (8,'OTHER', 90000, '기타 공제', 'PENDING'), + (9,'ADVANCE', 350000, '선지급 환수', 'APPLIED'), + (10,'DUES', 50000, '협회비', 'APPLIED'), + (11,'GUARANTEE',300000,'보증금 납입', 'PENDING'), + (12,'LOAN', 220000, '대출 상환분', 'APPLIED') +) AS t(rn, dtype, amt, descr, st) ON t.rn = r.rn +WHERE NOT EXISTS (SELECT 1 FROM agent_deduction); + +-- ============================================================ +-- 3. period_close — 마감 (2026-01 ~ 2026-03 마감 완료) +-- ============================================================ +INSERT INTO period_close (close_month, closed_at, closed_by, note) +SELECT m, ts, 1, note FROM (VALUES + ('202601', TIMESTAMP '2026-02-05 18:00:00', '2026년 1월 정산 마감'), + ('202602', TIMESTAMP '2026-03-05 18:00:00', '2026년 2월 정산 마감'), + ('202603', TIMESTAMP '2026-04-06 18:00:00', '2026년 3월 정산 마감') +) AS v(m, ts, note) +WHERE NOT EXISTS (SELECT 1 FROM period_close); + +-- ============================================================ +-- 4. tax_invoice — 세금계산서 (보험사 대상 매출 5건) +-- ============================================================ +INSERT INTO tax_invoice (invoice_no, invoice_type, issue_date, supply_amount, tax_amount, + total_amount, counterpart_name, counterpart_biz_no, counterpart_rep, + settle_month, description, issued_by, created_at) +SELECT v.no, 'SUPPLY', v.dt, v.supply, ROUND(v.supply*0.1,0), v.supply+ROUND(v.supply*0.1,0), + v.name, v.bizno, v.rep, v.sm, v.descr, 1, now() +FROM (VALUES + ('2026-S-0001', DATE '2026-03-10', 11968527::numeric, '삼성생명', '124-81-00001', '홍길동', '202602', '2026년 2월 수수료 세금계산서'), + ('2026-S-0002', DATE '2026-04-10', 16019916::numeric, '한화생명', '124-81-00002', '김영수', '202603', '2026년 3월 수수료 세금계산서'), + ('2026-S-0003', DATE '2026-05-10', 18944495::numeric, '교보생명', '124-81-00003', '이철수', '202604', '2026년 4월 수수료 세금계산서'), + ('2026-S-0004', DATE '2026-04-10', 4200000::numeric, '메리츠화재','124-81-00004', '박민지', '202603', '2026년 3월 손보 수수료'), + ('2026-S-0005', DATE '2026-05-10', 3850000::numeric, 'DB손해보험','124-81-00005', '최지훈', '202604', '2026년 4월 손보 수수료') +) AS v(no, dt, supply, name, bizno, rep, sm, descr) +WHERE NOT EXISTS (SELECT 1 FROM tax_invoice); + +-- ============================================================ +-- 5. commission_dispute — 수수료 이의신청 (6건) +-- ============================================================ +INSERT INTO commission_dispute (agent_id, settle_month, settle_id, status, + disputed_amount, claim_amount, reason, created_at) +SELECT r.agent_id, r.settle_month, r.settle_id, t.st, t.damt, t.camt, t.reason, now() +FROM ( + SELECT settle_id, agent_id, settle_month, + row_number() OVER (ORDER BY settle_id) AS rn + FROM settle_master WHERE settle_month = '202604' +) r +JOIN (VALUES + (1,'OPEN', 300000, 300000, '모집수수료 누락 — 3월 신계약 2건 미반영 주장'), + (2,'IN_REVIEW', 150000, 150000, '유지수수료 요율 오적용 이의'), + (3,'OPEN', 500000, 450000, '환수 금액 과다 산정 이의'), + (4,'APPROVED', 220000, 220000, '오버라이드 누락분 — 검토 후 인정'), + (5,'REJECTED', 100000, 0, '예외 차감 이의 — 근거 부족으로 기각'), + (6,'IN_REVIEW', 280000, 280000, '세금 공제액 과다 이의') +) AS t(rn, st, damt, camt, reason) ON t.rn = r.rn +WHERE NOT EXISTS (SELECT 1 FROM commission_dispute); + +-- ============================================================ +-- 6. incentive_program — 시책 프로그램 (4건) +-- ============================================================ +INSERT INTO incentive_program (program_name, program_type, target_type, start_month, end_month, + condition_desc, pay_rate, pay_amount_fixed, pay_type, + budget_amount, is_active, created_by, created_at) +SELECT v.name, v.ptype, 'AGENT', v.sm, v.em, v.cond, v.rate, v.fixed, v.paytype, + v.budget, v.active, 1, now() +FROM (VALUES + ('2026 상반기 신인 모집왕', 'RECRUIT_BONUS', '202601','202606','월 신계약 5건 이상 시 모집수수료의 추가 지급', 0.10::numeric, NULL::numeric, 'RATE', 50000000::numeric, TRUE), + ('2분기 유지율 우수상', 'MAINTAIN_BONUS','202604','202606','13M 유지율 90% 이상 달성', NULL::numeric, 1000000::numeric, 'FIXED_AMOUNT', 30000000::numeric, TRUE), + ('봄맞이 특별 콘테스트', 'CONTEST', '202603','202605','기간 내 보장성 신계약 환산 300%P 달성', NULL::numeric, 2000000::numeric, 'FIXED_AMOUNT', 40000000::numeric, TRUE), + ('2025 하반기 정착 지원', 'SPECIAL', '202507','202512','신인 정착 지원금 (종료)', NULL::numeric, 500000::numeric, 'FIXED_AMOUNT', 20000000::numeric, FALSE) +) AS v(name, ptype, sm, em, cond, rate, fixed, paytype, budget, active) +WHERE NOT EXISTS (SELECT 1 FROM incentive_program); + +-- ============================================================ +-- 7. settle_correction — 정산 정정 (4건, 실제 settle_master 참조) +-- ============================================================ +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) +SELECT r.settle_id, r.agent_id, r.settle_month, t.diff, + r.net_amount, r.net_amount + t.diff, t.rcode, t.detail, t.st, 1, now() +FROM ( + SELECT settle_id, agent_id, settle_month, net_amount, + row_number() OVER (ORDER BY settle_id) AS rn + FROM settle_master WHERE settle_month = '202603' +) r +JOIN (VALUES + (1, 200000::numeric, 'DATA_ERROR', '신계약 데이터 누락 — 모집수수료 추가 반영', 'APPROVED'), + (2, -150000::numeric, 'POLICY_CHANGE','요율 정책 변경 소급 적용', 'APPROVED'), + (3, 80000::numeric, 'LATE_DATA', '지연 수신 데이터 반영', 'DRAFT'), + (4, -50000::numeric, 'MANUAL_ADJ', '수기 조정 — 중복 지급분 회수', 'DRAFT') +) AS t(rn, diff, rcode, detail, st) ON t.rn = r.rn +WHERE NOT EXISTS (SELECT 1 FROM settle_correction); diff --git a/ga-core/src/main/resources/mapper/chargeback/ChargebackGradeMapper.xml b/ga-core/src/main/resources/mapper/chargeback/ChargebackGradeMapper.xml index d951388..4dc47d4 100644 --- a/ga-core/src/main/resources/mapper/chargeback/ChargebackGradeMapper.xml +++ b/ga-core/src/main/resources/mapper/chargeback/ChargebackGradeMapper.xml @@ -113,8 +113,8 @@ FROM chargeback_grade g WHERE g.months_from <= #{monthsElapsed} AND (g.months_to IS NULL OR g.months_to >= #{monthsElapsed}) - AND (g.product_category = #{productCategory} - OR (#{productCategory} IS NULL AND g.product_category IS NULL)) + AND (g.product_category = #{productCategory,jdbcType=VARCHAR} + OR (#{productCategory,jdbcType=VARCHAR} IS NULL AND g.product_category IS NULL)) ORDER BY g.effective_from DESC LIMIT 1 @@ -137,8 +137,8 @@ chargeback_percent, effective_from, effective_to, is_active, created_at, created_by, updated_at, updated_by FROM chargeback_grade g - WHERE (g.product_category = #{productCategory} - OR (#{productCategory} IS NULL AND g.product_category IS NULL)) + WHERE (g.product_category = #{productCategory,jdbcType=VARCHAR} + OR (#{productCategory,jdbcType=VARCHAR} IS NULL AND g.product_category IS NULL)) ORDER BY g.months_from diff --git a/ga-core/src/main/resources/mapper/installment/InstallmentRatioMapper.xml b/ga-core/src/main/resources/mapper/installment/InstallmentRatioMapper.xml index 1eb0415..964ae50 100644 --- a/ga-core/src/main/resources/mapper/installment/InstallmentRatioMapper.xml +++ b/ga-core/src/main/resources/mapper/installment/InstallmentRatioMapper.xml @@ -96,8 +96,8 @@ created_at, created_by, updated_at, updated_by FROM installment_ratio WHERE plan_year = #{planYear} - AND (product_category = #{productCategory} - OR (#{productCategory} IS NULL AND product_category IS NULL)) + AND (product_category = #{productCategory,jdbcType=VARCHAR} + OR (#{productCategory,jdbcType=VARCHAR} IS NULL AND product_category IS NULL)) AND is_active = TRUE AND effective_from <= #{baseDate}::DATE AND (effective_to IS NULL OR effective_to >= #{baseDate}::DATE) @@ -111,8 +111,8 @@ effective_from, effective_to, is_active, created_at, created_by, updated_at, updated_by FROM installment_ratio - WHERE (product_category = #{productCategory} - OR (#{productCategory} IS NULL AND product_category IS NULL)) + WHERE (product_category = #{productCategory,jdbcType=VARCHAR} + OR (#{productCategory,jdbcType=VARCHAR} IS NULL AND product_category IS NULL)) AND is_active = TRUE AND effective_from <= #{baseDate}::DATE AND (effective_to IS NULL OR effective_to >= #{baseDate}::DATE) diff --git a/ga-core/src/main/resources/mapper/kpi/ForecastKpiMonthlyMapper.xml b/ga-core/src/main/resources/mapper/kpi/ForecastKpiMonthlyMapper.xml index 3287b8b..b154cd8 100644 --- a/ga-core/src/main/resources/mapper/kpi/ForecastKpiMonthlyMapper.xml +++ b/ga-core/src/main/resources/mapper/kpi/ForecastKpiMonthlyMapper.xml @@ -52,8 +52,8 @@ DELETE FROM forecast_kpi_monthly WHERE forecast_month = #{forecastMonth} AND metric_code = #{metricCode} - AND ((#{targetOrgId} IS NULL AND target_org_id IS NULL) - OR target_org_id = #{targetOrgId}) + AND ((#{targetOrgId,jdbcType=BIGINT} IS NULL AND target_org_id IS NULL) + OR target_org_id = #{targetOrgId,jdbcType=BIGINT}) diff --git a/ga-core/src/main/resources/mapper/regulatory/RegulatoryLimitMapper.xml b/ga-core/src/main/resources/mapper/regulatory/RegulatoryLimitMapper.xml index d5509a7..daa8b2b 100644 --- a/ga-core/src/main/resources/mapper/regulatory/RegulatoryLimitMapper.xml +++ b/ga-core/src/main/resources/mapper/regulatory/RegulatoryLimitMapper.xml @@ -111,8 +111,8 @@ created_at, created_by, updated_at, updated_by FROM regulatory_limit WHERE limit_type = #{limitType} - AND (product_category = #{productCategory} - OR (#{productCategory} IS NULL AND product_category IS NULL)) + AND (product_category = #{productCategory,jdbcType=VARCHAR} + OR (#{productCategory,jdbcType=VARCHAR} IS NULL AND product_category IS NULL)) AND is_active = TRUE AND effective_from <= #{baseDate}::DATE AND (effective_to IS NULL OR effective_to >= #{baseDate}::DATE)