feat: 리포트 3종 백엔드 구현(실적/조직별/환수위험) + Excel export 트랜잭션 수정
- ReportController/Service/Mapper(.xml) + VO 3종(PerfReportRow/OrgReportRow/ChargebackRiskRow) - /api/report/performance: settle_month별 settle_master 집계(설계사수/모집/유지/실지급) - /api/report/org: 정산월 기준 organization 조인 조직별 집계 - /api/report/chargeback-risk: recruit_ledger 계약별 경과월 + chargeback_grade 환수율/위험등급 - 각 read(List) + export(Cursor→xlsx). 신규 테이블 없음(읽기전용 집계) - ExcelService.exportLargeExcel @Transactional(readOnly=true) 추가 — Cursor 가 세션 밖에서 닫혀 모든 export(기존 agent 포함)가 500 나던 잠복버그 수정. 1줄로 export 인프라 전체 복구 검증: 3 read 200(실데이터), 3 export 200(유효 xlsx), agent export 복구 확인. Playwright 84화면 재실행 FAIL 0 (PASS59/WARN25, WARN은 차트/폼 등 탐지한계) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package com.ga.core.mapper.report;
|
||||
|
||||
import com.ga.core.vo.report.ChargebackRiskRow;
|
||||
import com.ga.core.vo.report.OrgReportRow;
|
||||
import com.ga.core.vo.report.PerfReportRow;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.cursor.Cursor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 리포트 Mapper — 기존 정산 데이터 위 읽기전용 집계 (신규 테이블 없음).
|
||||
* 실적/조직별 = settle_master 집계, 환수위험 = recruit_ledger + chargeback_grade.
|
||||
* 각 select 는 조회용(List)과 엑셀 export 용(Cursor) 두 변형을 제공.
|
||||
*/
|
||||
@Mapper
|
||||
public interface ReportMapper {
|
||||
|
||||
List<PerfReportRow> selectPerformance(@Param("year") String year);
|
||||
|
||||
Cursor<PerfReportRow> selectPerformanceCursor(@Param("year") String year);
|
||||
|
||||
List<OrgReportRow> selectOrg(@Param("settleMonth") String settleMonth);
|
||||
|
||||
Cursor<OrgReportRow> selectOrgCursor(@Param("settleMonth") String settleMonth);
|
||||
|
||||
List<ChargebackRiskRow> selectChargebackRisk();
|
||||
|
||||
Cursor<ChargebackRiskRow> selectChargebackRiskCursor();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.ga.core.vo.report;
|
||||
|
||||
import com.ga.common.annotation.ExcelColumn;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 환수위험 리포트 행 — 계약별 경과월 기준 환수 가능성 분석. */
|
||||
@Data
|
||||
public class ChargebackRiskRow {
|
||||
|
||||
private Long agentId;
|
||||
|
||||
@ExcelColumn(header = "설계사", order = 1, width = 12)
|
||||
private String agentName;
|
||||
|
||||
@ExcelColumn(header = "소속", order = 2, width = 18)
|
||||
private String orgName;
|
||||
|
||||
@ExcelColumn(header = "증권번호", order = 3, width = 16)
|
||||
private String policyNo;
|
||||
|
||||
@ExcelColumn(header = "보험사", order = 4, width = 14)
|
||||
private String companyName;
|
||||
|
||||
@ExcelColumn(header = "모집월", order = 5, width = 10)
|
||||
private String recruitMonth;
|
||||
|
||||
@ExcelColumn(header = "경과월", order = 6, width = 8)
|
||||
private Integer monthsElapsed;
|
||||
|
||||
@ExcelColumn(header = "모집수수료", order = 7, width = 14)
|
||||
private BigDecimal recruitAmount;
|
||||
|
||||
@ExcelColumn(header = "환수율", order = 8, width = 8)
|
||||
private BigDecimal chargebackRate;
|
||||
|
||||
@ExcelColumn(header = "예상환수액", order = 9, width = 14)
|
||||
private BigDecimal estimatedChargeback;
|
||||
|
||||
@ExcelColumn(header = "위험등급", order = 10, width = 10)
|
||||
private String riskLevel;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ga.core.vo.report;
|
||||
|
||||
import com.ga.common.annotation.ExcelColumn;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 조직별 리포트 행 — 정산월 기준 조직 단위 집계. */
|
||||
@Data
|
||||
public class OrgReportRow {
|
||||
|
||||
private Long orgId;
|
||||
|
||||
@ExcelColumn(header = "조직명", order = 1, width = 18)
|
||||
private String orgName;
|
||||
|
||||
@ExcelColumn(header = "유형", order = 2, width = 10)
|
||||
private String orgType;
|
||||
|
||||
@ExcelColumn(header = "설계사수", order = 3, width = 10)
|
||||
private Long agentCount;
|
||||
|
||||
@ExcelColumn(header = "모집수수료", order = 4, width = 16)
|
||||
private BigDecimal recruitTotal;
|
||||
|
||||
@ExcelColumn(header = "유지수수료", order = 5, width = 16)
|
||||
private BigDecimal maintainTotal;
|
||||
|
||||
@ExcelColumn(header = "실지급합계", order = 6, width = 16)
|
||||
private BigDecimal netAmount;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ga.core.vo.report;
|
||||
|
||||
import com.ga.common.annotation.ExcelColumn;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 실적 리포트 행 — 정산월별 집계 (settle_master). */
|
||||
@Data
|
||||
public class PerfReportRow {
|
||||
|
||||
@ExcelColumn(header = "정산월", order = 1, width = 12)
|
||||
private String settleMonth;
|
||||
|
||||
@ExcelColumn(header = "설계사수", order = 2, width = 10)
|
||||
private Long agentCount;
|
||||
|
||||
@ExcelColumn(header = "모집수수료", order = 3, width = 16)
|
||||
private BigDecimal recruitTotal;
|
||||
|
||||
@ExcelColumn(header = "유지수수료", order = 4, width = 16)
|
||||
private BigDecimal maintainTotal;
|
||||
|
||||
@ExcelColumn(header = "실지급", order = 5, width = 16)
|
||||
private BigDecimal netAmount;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ga.core.mapper.report.ReportMapper">
|
||||
|
||||
<!--
|
||||
리포트 Mapper — 읽기전용 집계 (신규 테이블 없음).
|
||||
mapUnderscoreToCamelCase=true 전제: 집계 컬럼에 snake alias 부여.
|
||||
-->
|
||||
|
||||
<!-- 1. 실적 리포트 — 정산월별 settle_master 집계 -->
|
||||
<sql id="performanceSql">
|
||||
SELECT settle_month,
|
||||
COUNT(DISTINCT agent_id) AS agent_count,
|
||||
COALESCE(SUM(recruit_total), 0) AS recruit_total,
|
||||
COALESCE(SUM(maintain_total), 0) AS maintain_total,
|
||||
COALESCE(SUM(net_amount), 0) AS net_amount
|
||||
FROM settle_master
|
||||
WHERE settle_month LIKE #{year} || '%'
|
||||
GROUP BY settle_month
|
||||
ORDER BY settle_month
|
||||
</sql>
|
||||
|
||||
<select id="selectPerformance" resultType="com.ga.core.vo.report.PerfReportRow">
|
||||
<include refid="performanceSql"/>
|
||||
</select>
|
||||
<select id="selectPerformanceCursor" resultType="com.ga.core.vo.report.PerfReportRow" fetchSize="500">
|
||||
<include refid="performanceSql"/>
|
||||
</select>
|
||||
|
||||
<!-- 2. 조직별 리포트 — 정산월 기준 조직 단위 집계 -->
|
||||
<sql id="orgSql">
|
||||
SELECT o.org_id,
|
||||
o.org_name,
|
||||
o.org_type,
|
||||
COUNT(DISTINCT sm.agent_id) AS agent_count,
|
||||
COALESCE(SUM(sm.recruit_total), 0) AS recruit_total,
|
||||
COALESCE(SUM(sm.maintain_total), 0) AS maintain_total,
|
||||
COALESCE(SUM(sm.net_amount), 0) AS net_amount
|
||||
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.settle_month = #{settleMonth}
|
||||
GROUP BY o.org_id, o.org_name, o.org_type
|
||||
ORDER BY net_amount DESC
|
||||
</sql>
|
||||
|
||||
<select id="selectOrg" resultType="com.ga.core.vo.report.OrgReportRow">
|
||||
<include refid="orgSql"/>
|
||||
</select>
|
||||
<select id="selectOrgCursor" resultType="com.ga.core.vo.report.OrgReportRow" fetchSize="500">
|
||||
<include refid="orgSql"/>
|
||||
</select>
|
||||
|
||||
<!-- 3. 환수위험 리포트 — 계약별 경과월 기준 환수율 적용 -->
|
||||
<sql id="chargebackRiskSql">
|
||||
WITH rl AS (
|
||||
SELECT contract_id,
|
||||
agent_id,
|
||||
MIN(policy_no) AS policy_no,
|
||||
MIN(company_code) AS company_code,
|
||||
MIN(settle_month) AS recruit_month,
|
||||
SUM(agent_amount) AS recruit_amount
|
||||
FROM recruit_ledger
|
||||
GROUP BY contract_id, agent_id
|
||||
)
|
||||
SELECT rl.agent_id,
|
||||
a.agent_name,
|
||||
o.org_name,
|
||||
rl.policy_no,
|
||||
ic.company_name,
|
||||
rl.recruit_month,
|
||||
elapsed.months_elapsed,
|
||||
rl.recruit_amount,
|
||||
COALESCE(cg.chargeback_percent, 0) AS chargeback_rate,
|
||||
ROUND(rl.recruit_amount * COALESCE(cg.chargeback_percent, 0) / 100.0) AS estimated_chargeback,
|
||||
CASE WHEN COALESCE(cg.chargeback_percent, 0) >= 50 THEN 'HIGH'
|
||||
WHEN COALESCE(cg.chargeback_percent, 0) >= 10 THEN 'MEDIUM'
|
||||
ELSE 'LOW' END AS risk_level
|
||||
FROM rl
|
||||
JOIN agent a ON a.agent_id = rl.agent_id
|
||||
LEFT JOIN organization o ON o.org_id = a.org_id
|
||||
LEFT JOIN insurance_company ic ON ic.company_code = rl.company_code
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT (EXTRACT(YEAR FROM CURRENT_DATE)::int * 12 + EXTRACT(MONTH FROM CURRENT_DATE)::int)
|
||||
- (substr(rl.recruit_month, 1, 4)::int * 12 + substr(rl.recruit_month, 5, 2)::int) AS months_elapsed
|
||||
) elapsed
|
||||
LEFT JOIN chargeback_grade cg
|
||||
ON cg.is_active = 'Y'
|
||||
AND elapsed.months_elapsed >= cg.months_from
|
||||
AND (cg.months_to IS NULL OR elapsed.months_elapsed <= cg.months_to)
|
||||
ORDER BY estimated_chargeback DESC
|
||||
</sql>
|
||||
|
||||
<select id="selectChargebackRisk" resultType="com.ga.core.vo.report.ChargebackRiskRow">
|
||||
<include refid="chargebackRiskSql"/>
|
||||
</select>
|
||||
<select id="selectChargebackRiskCursor" resultType="com.ga.core.vo.report.ChargebackRiskRow" fetchSize="500">
|
||||
<include refid="chargebackRiskSql"/>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user