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:
GA Pro
2026-06-02 00:51:37 +09:00
parent 81871615cd
commit 1d77671ea0
8 changed files with 369 additions and 0 deletions
@@ -0,0 +1,84 @@
package com.ga.api.controller.report;
import com.ga.api.service.report.ReportService;
import com.ga.common.annotation.RequirePermission;
import com.ga.common.excel.ExcelService;
import com.ga.common.model.ApiResponse;
import com.ga.core.vo.report.ChargebackRiskRow;
import com.ga.core.vo.report.OrgReportRow;
import com.ga.core.vo.report.PerfReportRow;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* 리포트 — 실적 / 조직별 / 환수위험. 읽기전용 집계 (신규 테이블 없음).
* 각 화면의 조회 + 엑셀 export 엔드포인트 제공.
*/
@Tag(name = "리포트")
@RestController
@RequestMapping("/api/report")
@RequiredArgsConstructor
public class ReportController {
private final ReportService service;
private final ExcelService excelService;
private String defaultYear(String year) {
return (year != null && !year.isBlank()) ? year : String.valueOf(LocalDate.now().getYear());
}
private String defaultMonth(String settleMonth) {
return (settleMonth != null && !settleMonth.isBlank())
? settleMonth : LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
}
// ── 실적 리포트 ──
@GetMapping("/performance")
@RequirePermission(menu = "REPORT_PERFORMANCE", perm = "READ")
public ApiResponse<List<PerfReportRow>> performance(@RequestParam(required = false) String year) {
return ApiResponse.ok(service.performance(defaultYear(year)));
}
@GetMapping("/performance/export")
@RequirePermission(menu = "REPORT_PERFORMANCE", perm = "READ")
public void performanceExport(@RequestParam(required = false) String year, HttpServletResponse response) {
String y = defaultYear(year);
excelService.exportLargeExcel("실적리포트", PerfReportRow.class,
() -> service.performanceCursor(y), response);
}
// ── 조직별 리포트 ──
@GetMapping("/org")
@RequirePermission(menu = "REPORT_ORG", perm = "READ")
public ApiResponse<List<OrgReportRow>> org(@RequestParam(required = false) String settleMonth) {
return ApiResponse.ok(service.org(defaultMonth(settleMonth)));
}
@GetMapping("/org/export")
@RequirePermission(menu = "REPORT_ORG", perm = "READ")
public void orgExport(@RequestParam(required = false) String settleMonth, HttpServletResponse response) {
String m = defaultMonth(settleMonth);
excelService.exportLargeExcel("조직별리포트", OrgReportRow.class,
() -> service.orgCursor(m), response);
}
// ── 환수위험 리포트 ──
@GetMapping("/chargeback-risk")
@RequirePermission(menu = "REPORT_CHARGEBACK", perm = "READ")
public ApiResponse<List<ChargebackRiskRow>> chargebackRisk() {
return ApiResponse.ok(service.chargebackRisk());
}
@GetMapping("/chargeback-risk/export")
@RequirePermission(menu = "REPORT_CHARGEBACK", perm = "READ")
public void chargebackRiskExport(HttpServletResponse response) {
excelService.exportLargeExcel("환수위험리포트", ChargebackRiskRow.class,
() -> service.chargebackRiskCursor(), response);
}
}
@@ -0,0 +1,50 @@
package com.ga.api.service.report;
import com.ga.core.mapper.report.ReportMapper;
import com.ga.core.vo.report.ChargebackRiskRow;
import com.ga.core.vo.report.OrgReportRow;
import com.ga.core.vo.report.PerfReportRow;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.cursor.Cursor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 리포트 서비스 — 기존 정산 데이터 위 읽기전용 집계.
* Cursor 변형은 엑셀 export 용 (ExcelService.exportLargeExcel 가 트랜잭션 내에서 소비).
*/
@Service
@RequiredArgsConstructor
public class ReportService {
private final ReportMapper mapper;
@Transactional(readOnly = true)
public List<PerfReportRow> performance(String year) {
return mapper.selectPerformance(year);
}
public Cursor<PerfReportRow> performanceCursor(String year) {
return mapper.selectPerformanceCursor(year);
}
@Transactional(readOnly = true)
public List<OrgReportRow> org(String settleMonth) {
return mapper.selectOrg(settleMonth);
}
public Cursor<OrgReportRow> orgCursor(String settleMonth) {
return mapper.selectOrgCursor(settleMonth);
}
@Transactional(readOnly = true)
public List<ChargebackRiskRow> chargebackRisk() {
return mapper.selectChargebackRisk();
}
public Cursor<ChargebackRiskRow> chargebackRiskCursor() {
return mapper.selectChargebackRiskCursor();
}
}