1 Commits

Author SHA1 Message Date
GA Pro 169e0ca2ef 동기화 2026-05-12 22:16:28 +09:00
5 changed files with 435 additions and 0 deletions
@@ -0,0 +1,83 @@
package com.ga.batch.settlement.receive;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.core.mapper.receive.ReceiveMapper;
import com.ga.core.vo.receive.CompanyProfileVO;
import com.ga.core.vo.receive.RawCommissionDataVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* 보험사 OpenAPI 호출하여 데이터 수신.
* - api_url 은 company_profile 에서 가져옴. {settleMonth} placeholder 치환 지원.
* - 응답은 JSON array 가정. 각 element 를 raw_content 에 적재.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiReceiver implements DataReceiver {
private final ReceiveMapper mapper;
private final RestClient restClient = RestClient.builder().build();
private static final ObjectMapper JSON = new ObjectMapper();
@Override
public boolean supports(String companyCode) {
CompanyProfileVO p = mapper.selectProfile(companyCode);
return p != null && "API".equalsIgnoreCase(p.getDataFormat());
}
@Override
public int receive(String companyCode, String settleMonth) {
CompanyProfileVO profile = mapper.selectProfile(companyCode);
if (profile == null || profile.getApiUrl() == null) {
log.warn("API profile not configured: {}", companyCode);
return 0;
}
String url = profile.getApiUrl().replace("{settleMonth}", settleMonth);
try {
String body = restClient.get().uri(url).retrieve().body(String.class);
if (body == null || body.isBlank()) return 0;
JsonNode arr = JSON.readTree(body);
if (!arr.isArray()) {
log.warn("API response is not array: {}", url);
return 0;
}
String batchId = UUID.randomUUID().toString();
List<RawCommissionDataVO> buf = new ArrayList<>(arr.size());
int rowNum = 0;
for (JsonNode item : arr) {
rowNum++;
RawCommissionDataVO raw = new RawCommissionDataVO();
raw.setCompanyCode(companyCode);
raw.setBatchId(batchId);
raw.setSettleMonth(settleMonth);
raw.setDataType("API");
raw.setRawContent(item.toString());
raw.setParseStatus("PENDING");
raw.setFileName(url);
raw.setRowNumber(rowNum);
buf.add(raw);
}
if (!buf.isEmpty()) mapper.insertRawBatch(buf);
log.info("ApiReceiver: company={}, url={}, rows={}", companyCode, url, buf.size());
return buf.size();
} catch (Exception e) {
log.error("API receive error: {}", url, e);
throw new BizException(ErrorCode.EXTERNAL_API_FAIL, e.getMessage());
}
}
}
@@ -0,0 +1,103 @@
package com.ga.batch.settlement.receive;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.core.mapper.receive.ReceiveMapper;
import com.ga.core.vo.receive.CompanyProfileVO;
import com.ga.core.vo.receive.RawCommissionDataVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* CSV/TSV 파일 수신.
* file_encoding, delimiter 는 company_profile 에서 가져옴.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CsvReceiver implements DataReceiver {
private final ReceiveMapper mapper;
@Value("${ga.batch.receive.csv-dir:./data/receive/csv}")
private String csvDir;
@Override
public boolean supports(String companyCode) {
CompanyProfileVO p = mapper.selectProfile(companyCode);
return p != null && "CSV".equalsIgnoreCase(p.getDataFormat());
}
@Override
public int receive(String companyCode, String settleMonth) {
CompanyProfileVO profile = mapper.selectProfile(companyCode);
Path file = Paths.get(csvDir, settleMonth, companyCode + ".csv");
if (!Files.exists(file)) {
log.warn("CSV file not found: {}", file);
return 0;
}
Charset cs = profile.getFileEncoding() != null ? Charset.forName(profile.getFileEncoding()) : Charset.forName("UTF-8");
String delim = profile.getDelimiter() != null ? profile.getDelimiter() : ",";
int dataStart = profile.getDataStartRow() == null ? 2 : profile.getDataStartRow();
String batchId = UUID.randomUUID().toString();
int batchSize = 1000;
int total = 0;
List<RawCommissionDataVO> buf = new ArrayList<>(batchSize);
try (BufferedReader reader = Files.newBufferedReader(file, cs)) {
String line;
int rowNum = 0;
while ((line = reader.readLine()) != null) {
rowNum++;
if (rowNum < dataStart) continue;
if (line.isBlank()) continue;
RawCommissionDataVO raw = new RawCommissionDataVO();
raw.setCompanyCode(companyCode);
raw.setBatchId(batchId);
raw.setSettleMonth(settleMonth);
raw.setDataType("CSV");
raw.setRawContent(toJsonArray(line.split(java.util.regex.Pattern.quote(delim), -1)));
raw.setParseStatus("PENDING");
raw.setFileName(file.getFileName().toString());
raw.setRowNumber(rowNum);
buf.add(raw);
if (buf.size() >= batchSize) {
mapper.insertRawBatch(buf);
total += buf.size();
buf.clear();
}
}
if (!buf.isEmpty()) {
mapper.insertRawBatch(buf);
total += buf.size();
}
log.info("CsvReceiver: company={}, file={}, rows={}", companyCode, file, total);
return total;
} catch (Exception e) {
log.error("CSV receive error", e);
throw new BizException(ErrorCode.EXCEL_PARSE_FAIL, e.getMessage());
}
}
private String toJsonArray(String[] cols) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < cols.length; i++) {
if (i > 0) sb.append(',');
sb.append('"').append(cols[i].replace("\"", "\\\"")).append('"');
}
sb.append(']');
return sb.toString();
}
}
@@ -0,0 +1,95 @@
package com.ga.batch.settlement.receive;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.core.mapper.receive.ReceiveMapper;
import com.ga.core.vo.receive.CompanyProfileVO;
import com.ga.core.vo.receive.RawCommissionDataVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 보험사별 EDI(고정 폭) 텍스트 파일 수신.
* 각 행을 그대로 raw_content 에 적재 — 매핑은 transform 단계에서 company_field_mapping 의 sourceColIndex(시작/길이)로 분해.
*
* 보험사별 EDI 양식이 매우 다양하므로 여기서는 line 자체를 raw로 보존하고
* MappingEngine 이 sourceColIndex 기준으로 substring 처리.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class EdiReceiver implements DataReceiver {
private final ReceiveMapper mapper;
@Value("${ga.batch.receive.edi-dir:./data/receive/edi}")
private String ediDir;
@Override
public boolean supports(String companyCode) {
CompanyProfileVO p = mapper.selectProfile(companyCode);
return p != null && "EDI".equalsIgnoreCase(p.getDataFormat());
}
@Override
public int receive(String companyCode, String settleMonth) {
CompanyProfileVO profile = mapper.selectProfile(companyCode);
Path file = Paths.get(ediDir, settleMonth, companyCode + ".dat");
if (!Files.exists(file)) {
log.warn("EDI file not found: {}", file);
return 0;
}
Charset cs = profile.getFileEncoding() != null ? Charset.forName(profile.getFileEncoding()) : Charset.forName("EUC-KR");
int dataStart = profile.getDataStartRow() == null ? 1 : profile.getDataStartRow();
String batchId = UUID.randomUUID().toString();
int batchSize = 1000;
int total = 0;
List<RawCommissionDataVO> buf = new ArrayList<>(batchSize);
try (BufferedReader reader = Files.newBufferedReader(file, cs)) {
String line;
int rowNum = 0;
while ((line = reader.readLine()) != null) {
rowNum++;
if (rowNum < dataStart) continue;
if (line.isBlank()) continue;
RawCommissionDataVO raw = new RawCommissionDataVO();
raw.setCompanyCode(companyCode);
raw.setBatchId(batchId);
raw.setSettleMonth(settleMonth);
raw.setDataType("EDI");
raw.setRawContent(line); // 고정 폭 라인 그대로
raw.setParseStatus("PENDING");
raw.setFileName(file.getFileName().toString());
raw.setRowNumber(rowNum);
buf.add(raw);
if (buf.size() >= batchSize) {
mapper.insertRawBatch(buf);
total += buf.size();
buf.clear();
}
}
if (!buf.isEmpty()) {
mapper.insertRawBatch(buf);
total += buf.size();
}
log.info("EdiReceiver: company={}, file={}, rows={}", companyCode, file, total);
return total;
} catch (Exception e) {
log.error("EDI receive error", e);
throw new BizException(ErrorCode.EXCEL_PARSE_FAIL, e.getMessage());
}
}
}
@@ -0,0 +1,116 @@
package com.ga.batch.settlement.receive;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.core.mapper.receive.ReceiveMapper;
import com.ga.core.vo.receive.CompanyProfileVO;
import com.ga.core.vo.receive.RawCommissionDataVO;
import com.monitorjbl.xlsx.StreamingReader;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 엑셀 파일을 읽어 raw_commission_data 에 적재.
* 보험사별 FTP 다운로드 폴더 또는 운영자가 미리 옮겨둔 폴더에서 읽음.
*
* - SAX 스트리밍으로 70만건 안전 처리
* - 컬럼 매핑은 ReceiveMapper.selectFieldMappings(companyCode, "raw") 의 정의를 따름
* - data_format = 'EXCEL' 인 보험사 지원
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ExcelReceiver implements DataReceiver {
private final ReceiveMapper mapper;
@Value("${ga.batch.receive.excel-dir:./data/receive/excel}")
private String excelDir;
@Override
public boolean supports(String companyCode) {
CompanyProfileVO p = mapper.selectProfile(companyCode);
return p != null && "EXCEL".equalsIgnoreCase(p.getDataFormat());
}
@Override
public int receive(String companyCode, String settleMonth) {
CompanyProfileVO profile = mapper.selectProfile(companyCode);
Path file = resolveFile(companyCode, settleMonth);
if (!Files.exists(file)) {
log.warn("Excel file not found: {}", file);
return 0;
}
String batchId = UUID.randomUUID().toString();
int header = profile.getHeaderRow() == null ? 1 : profile.getHeaderRow();
int dataStart = profile.getDataStartRow() == null ? 2 : profile.getDataStartRow();
int batchSize = 1000;
int total = 0;
List<RawCommissionDataVO> buf = new ArrayList<>(batchSize);
try (var is = Files.newInputStream(file);
Workbook wb = StreamingReader.builder().rowCacheSize(100).bufferSize(4096).open(is)) {
Sheet sheet = profile.getSheetName() != null ? wb.getSheet(profile.getSheetName()) : wb.getSheetAt(0);
int rowNum = 0;
for (Row row : sheet) {
rowNum++;
if (rowNum < dataStart) continue;
RawCommissionDataVO raw = new RawCommissionDataVO();
raw.setCompanyCode(companyCode);
raw.setBatchId(batchId);
raw.setSettleMonth(settleMonth);
raw.setDataType("EXCEL");
raw.setRawContent(rowToJson(row));
raw.setParseStatus("PENDING");
raw.setFileName(file.getFileName().toString());
raw.setRowNumber(rowNum);
buf.add(raw);
if (buf.size() >= batchSize) {
mapper.insertRawBatch(buf);
total += buf.size();
buf.clear();
}
}
if (!buf.isEmpty()) {
mapper.insertRawBatch(buf);
total += buf.size();
}
log.info("ExcelReceiver: company={}, file={}, rows={}", companyCode, file, total);
return total;
} catch (Exception e) {
log.error("Excel receive error", e);
throw new BizException(ErrorCode.EXCEL_PARSE_FAIL, e.getMessage());
}
}
private Path resolveFile(String companyCode, String settleMonth) {
return Paths.get(excelDir, settleMonth, companyCode + ".xlsx");
}
/** Row 의 셀 값을 단순 JSON 배열 문자열로 직렬화. */
private String rowToJson(Row row) {
StringBuilder sb = new StringBuilder("[");
boolean first = true;
for (var cell : row) {
if (!first) sb.append(',');
sb.append('"').append(cell == null ? "" : cell.toString().replace("\"", "\\\"")).append('"');
first = false;
}
sb.append(']');
return sb.toString();
}
}
@@ -0,0 +1,38 @@
package com.ga.batch.settlement.receive;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 보험사별 적절한 Receiver 를 선택해 호출.
* 모든 DataReceiver 가 자동 주입되며, supports(companyCode) 가 true 인 첫 구현체를 사용.
*
* 우선순위는 List 주입 순서. ManualReceiver 는 fallback 역할이므로 마지막에 두는 것을 권장
* (현재는 supports 가 NULL 프로파일도 true로 하므로 우선순위 위에 있을 수 없음).
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ReceiverDispatcher {
private final List<DataReceiver> receivers;
public int dispatch(String companyCode, String settleMonth) {
for (DataReceiver r : receivers) {
if (r == null) continue;
try {
if (r.supports(companyCode)) {
log.info("Dispatching {} -> {}", companyCode, r.getClass().getSimpleName());
return r.receive(companyCode, settleMonth);
}
} catch (Exception e) {
log.warn("Receiver {} supports() error", r.getClass().getSimpleName(), e);
}
}
log.warn("No receiver supports company: {}", companyCode);
return 0;
}
}