Compare commits
3 Commits
5d5c2d53aa
...
1d77671ea0
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d77671ea0 | |||
| 81871615cd | |||
| 93ac94558f |
@@ -4,14 +4,13 @@ import com.ga.api.service.product.ProductService;
|
||||
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.product.ProductResp;
|
||||
import com.ga.core.vo.product.ProductVO;
|
||||
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/products")
|
||||
@@ -22,11 +21,13 @@ public class ProductController {
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "PRODUCT", perm = "READ")
|
||||
public ApiResponse<List<ProductResp>> list(@RequestParam(required = false) Long companyId,
|
||||
@RequestParam(required = false) String insuranceType,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String isActive) {
|
||||
return ApiResponse.ok(productService.list(companyId, insuranceType, keyword, isActive));
|
||||
public ApiResponse<PageResponse<ProductResp>> list(@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) Long companyId,
|
||||
@RequestParam(required = false) String insuranceType,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String isActive) {
|
||||
return ApiResponse.ok(productService.list(pageNum, pageSize, companyId, insuranceType, keyword, isActive));
|
||||
}
|
||||
|
||||
@GetMapping("/{productId}")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,15 @@ package com.ga.api.service.product;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.mapper.product.ProductMapper;
|
||||
import com.ga.core.vo.product.ProductResp;
|
||||
import com.ga.core.vo.product.ProductVO;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProductService {
|
||||
@@ -18,8 +18,9 @@ public class ProductService {
|
||||
private final ProductMapper mapper;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ProductResp> list(Long companyId, String insuranceType, String keyword, String isActive) {
|
||||
return mapper.selectList(companyId, insuranceType, keyword, isActive);
|
||||
public PageResponse<ProductResp> list(int pageNum, int pageSize, Long companyId, String insuranceType, String keyword, String isActive) {
|
||||
PageHelper.startPage(pageNum, pageSize);
|
||||
return PageResponse.of(mapper.selectList(companyId, insuranceType, keyword, isActive));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.streaming.SXSSFSheet;
|
||||
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
@@ -42,6 +43,7 @@ public class ExcelService {
|
||||
/* =========================================================
|
||||
다운로드 (Export) — SXSSF + Cursor 스트리밍
|
||||
========================================================= */
|
||||
@Transactional(readOnly = true)
|
||||
public <T> void exportLargeExcel(String fileName, Class<T> clazz,
|
||||
Supplier<Cursor<T>> cursorSupplier,
|
||||
HttpServletResponse response) {
|
||||
|
||||
@@ -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>
|
||||
@@ -29,19 +29,60 @@ export interface BatchHistoryResp {
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 batch_job_log VO (실제 응답 형태) */
|
||||
interface BatchJobVO {
|
||||
jobId: number;
|
||||
jobName: string;
|
||||
status: string; // REQUESTED | STARTED | COMPLETED | FAILED
|
||||
currentStep?: string;
|
||||
totalCount?: number;
|
||||
processedCount?: number;
|
||||
errorCount?: number;
|
||||
errorMessage?: string;
|
||||
startedAt?: number[] | string;
|
||||
finishedAt?: number[] | string;
|
||||
durationSec?: number;
|
||||
}
|
||||
|
||||
/** LocalDateTime 배열([y,M,d,h,m,s,..]) → 'YYYY-MM-DD HH:mm:ss' */
|
||||
function fmtTs(v?: number[] | string): string | undefined {
|
||||
if (v == null) return undefined;
|
||||
if (typeof v === 'string') return v;
|
||||
const [y, mo, d, h = 0, mi = 0, s = 0] = v;
|
||||
const p = (n: number) => String(n).padStart(2, '0');
|
||||
return `${y}-${p(mo)}-${p(d)} ${p(h)}:${p(mi)}:${p(s)}`;
|
||||
}
|
||||
|
||||
function mapStatus(s: string): BatchJobLogResp['status'] {
|
||||
if (s === 'STARTED') return 'RUNNING';
|
||||
if (s === 'REQUESTED' || s === 'RUNNING' || s === 'COMPLETED' || s === 'FAILED') return s;
|
||||
return 'RUNNING';
|
||||
}
|
||||
|
||||
/** 백엔드 VO → 프론트 BatchJobLogResp 어댑터 */
|
||||
function toJobLog(v: BatchJobVO): BatchJobLogResp {
|
||||
return {
|
||||
jobLogId: v.jobId,
|
||||
jobName: v.jobName,
|
||||
status: mapStatus(v.status),
|
||||
stepName: v.currentStep,
|
||||
totalCount: v.totalCount,
|
||||
successCount: v.processedCount,
|
||||
errorCount: v.errorCount,
|
||||
startedAt: fmtTs(v.startedAt),
|
||||
endedAt: fmtTs(v.finishedAt),
|
||||
durationMs: v.durationSec != null ? v.durationSec * 1000 : undefined,
|
||||
errorMessage: v.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
export const batchRunApi = {
|
||||
run: (settleMonth: string, companyCode?: string) =>
|
||||
unwrap<number>(
|
||||
api.post('/api/batch/settlement/run', null, {
|
||||
params: { settleMonth, companyCode },
|
||||
}),
|
||||
),
|
||||
unwrap<number>(api.post('/api/batch/settlement/run', { settleMonth, companyCode })),
|
||||
status: (jobLogId: number) =>
|
||||
unwrap<BatchJobLogResp>(
|
||||
api.get('/api/batch/settlement/status', { params: { jobLogId } }),
|
||||
),
|
||||
unwrap<BatchJobVO>(api.get(`/api/batch/jobs/${jobLogId}`)).then(toJobLog),
|
||||
history: (size = 10) =>
|
||||
unwrap<BatchHistoryResp[]>(
|
||||
api.get('/api/batch/settlement/history', { params: { size } }),
|
||||
unwrap<BatchJobVO[]>(api.get('/api/batch/jobs')).then((rows) =>
|
||||
rows.slice(0, size).map(toJobLog) as BatchHistoryResp[],
|
||||
),
|
||||
};
|
||||
|
||||
@@ -153,9 +153,9 @@ export const operationApi = {
|
||||
hqSettleDelete: (id: number) =>
|
||||
unwrap<void>(api.delete(`/api/hq-settles/${id}`)),
|
||||
|
||||
// GradeEval Rules
|
||||
// GradeEval Rules — 백엔드는 PageResponse({list}) 반환이므로 .list 추출
|
||||
gradeEvalRules: () =>
|
||||
unwrap<GradeEvalRuleRow[]>(api.get('/api/grade-evaluations/rules')),
|
||||
unwrap<{ list: GradeEvalRuleRow[] }>(api.get('/api/grade-evaluations/rules')).then((r) => r.list ?? []),
|
||||
gradeEvalRuleGet: (id: number) =>
|
||||
unwrap<GradeEvalRuleRow>(api.get(`/api/grade-evaluations/rules/${id}`)),
|
||||
gradeEvalRuleCreate: (body: Partial<GradeEvalRuleRow>) =>
|
||||
|
||||
Generated
+1037
-4
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
||||
"antd-mobile-icons": "^0.3.0",
|
||||
"axios": "^1.7.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"firebase": "^12.14.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.23.1",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/* FCM 백그라운드 메시지 수신 서비스워커 (vite-plugin-pwa SW 와 별개).
|
||||
* Firebase 웹 config 는 정적 파일에 박지 않고, initPush 등록 시 쿼리스트링으로 전달받는다. */
|
||||
importScripts('https://www.gstatic.com/firebasejs/12.14.0/firebase-app-compat.js');
|
||||
importScripts('https://www.gstatic.com/firebasejs/12.14.0/firebase-messaging-compat.js');
|
||||
|
||||
const p = new URL(self.location).searchParams;
|
||||
firebase.initializeApp({
|
||||
apiKey: p.get('apiKey'),
|
||||
projectId: p.get('projectId'),
|
||||
appId: p.get('appId'),
|
||||
messagingSenderId: p.get('messagingSenderId'),
|
||||
});
|
||||
|
||||
firebase.messaging().onBackgroundMessage((payload) => {
|
||||
const n = payload.notification || {};
|
||||
self.registration.showNotification(n.title || '알림', {
|
||||
body: n.body || '',
|
||||
icon: '/pwa-192x192.png',
|
||||
data: payload.data || {},
|
||||
});
|
||||
});
|
||||
@@ -6,11 +6,10 @@ import { pushApi } from '@/api/push';
|
||||
* 활성 조건: 빌드 환경변수 `VITE_FCM_*` 가 모두 설정된 경우에만 동작한다.
|
||||
* 미설정 시 no-op (개발/미연동 환경에서 안전).
|
||||
*
|
||||
* <b>운영 활성화 절차</b>:
|
||||
* 1) Firebase 프로젝트 생성 → 웹 앱 등록 → 설정값을 .env 의 VITE_FCM_* 로 주입
|
||||
* 2) `npm i firebase` 후 아래 TODO 블록의 firebase/messaging 초기화 활성화
|
||||
* 3) getToken(VAPID key)으로 FCM 토큰 발급 → pushApi.register(token) 호출
|
||||
* 4) public/firebase-messaging-sw.js 서비스워커 배치
|
||||
* firebase SDK 통합 완료 — 활성 조건은 `.env(.local)` 의 VITE_FCM_* 주입뿐이다.
|
||||
* 1) Firebase 콘솔 → 웹 앱 등록 → 설정값을 .env.local 의 VITE_FCM_* 로 주입
|
||||
* 2) getToken(VAPID key)으로 FCM 토큰 발급 → pushApi.register(token) 자동 호출
|
||||
* 3) public/firebase-messaging-sw.js 가 백그라운드 수신 처리
|
||||
*
|
||||
* 서버측(PushAdapter=fcm + FCM_PROJECT_ID/FCM_ACCESS_TOKEN)과 함께 활성화해야 실제 발송된다.
|
||||
*/
|
||||
@@ -38,14 +37,38 @@ export async function initPush(): Promise<void> {
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission !== 'granted') return;
|
||||
|
||||
// TODO(운영): `npm i firebase` 후 활성화.
|
||||
// const { initializeApp } = await import('firebase/app');
|
||||
// const { getMessaging, getToken } = await import('firebase/messaging');
|
||||
// const appFb = initializeApp(cfg);
|
||||
// const messaging = getMessaging(appFb);
|
||||
// const token = await getToken(messaging, { vapidKey: cfg.vapidKey });
|
||||
// if (token) await pushApi.register(token, 'WEB');
|
||||
void pushApi; // 등록 API 는 토큰 발급 후 호출 (위 TODO 활성화 시)
|
||||
const { isSupported, getMessaging, getToken } = await import('firebase/messaging');
|
||||
if (!(await isSupported())) {
|
||||
if (import.meta.env.DEV) console.info('[push] 이 브라우저는 FCM 미지원 — skip.');
|
||||
return;
|
||||
}
|
||||
|
||||
const { initializeApp, getApps } = await import('firebase/app');
|
||||
const appFb = getApps().length
|
||||
? getApps()[0]
|
||||
: initializeApp({
|
||||
apiKey: cfg.apiKey,
|
||||
projectId: cfg.projectId,
|
||||
appId: cfg.appId,
|
||||
messagingSenderId: cfg.messagingSenderId,
|
||||
});
|
||||
|
||||
// FCM 백그라운드 수신용 서비스워커 (vite-plugin-pwa SW 와 별개).
|
||||
// 웹 config 는 정적 SW 파일에 박지 않고 쿼리스트링으로 전달.
|
||||
const swQuery = new URLSearchParams({
|
||||
apiKey: cfg.apiKey,
|
||||
projectId: cfg.projectId,
|
||||
appId: cfg.appId,
|
||||
messagingSenderId: cfg.messagingSenderId ?? '',
|
||||
}).toString();
|
||||
const swReg = await navigator.serviceWorker.register(`/firebase-messaging-sw.js?${swQuery}`);
|
||||
|
||||
const messaging = getMessaging(appFb);
|
||||
const token = await getToken(messaging, {
|
||||
vapidKey: cfg.vapidKey,
|
||||
serviceWorkerRegistration: swReg,
|
||||
});
|
||||
if (token) await pushApi.register(token, 'WEB');
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('[push] init 실패', e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# 외부연동 어댑터 키를 주입하여 ga-api 를 기동한다.
|
||||
# adapter.env.local 의 값을 환경변수로 로드한 뒤 bootRun.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [[ ! -f adapter.env.local ]]; then
|
||||
echo "adapter.env.local 이 없습니다. 키를 먼저 채우세요." >&2
|
||||
exit 1
|
||||
fi
|
||||
set -a; source adapter.env.local; set +a
|
||||
|
||||
echo "[adapters] bank=$ADAPTER_BANK message=$ADAPTER_MESSAGE push=$ADAPTER_PUSH"
|
||||
./gradlew :ga-api:bootRun --args='--spring.profiles.active=trading_ai'
|
||||
Reference in New Issue
Block a user