From 169e0ca2ef2b56c0d529e0fb6210de5eeec072e7 Mon Sep 17 00:00:00 2001 From: GA Pro Date: Tue, 12 May 2026 22:16:28 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../batch/settlement/receive/ApiReceiver.java | 83 +++++++++++++ .../batch/settlement/receive/CsvReceiver.java | 103 ++++++++++++++++ .../batch/settlement/receive/EdiReceiver.java | 95 ++++++++++++++ .../settlement/receive/ExcelReceiver.java | 116 ++++++++++++++++++ .../receive/ReceiverDispatcher.java | 38 ++++++ 5 files changed, 435 insertions(+) create mode 100644 ga-batch/src/main/java/com/ga/batch/settlement/receive/ApiReceiver.java create mode 100644 ga-batch/src/main/java/com/ga/batch/settlement/receive/CsvReceiver.java create mode 100644 ga-batch/src/main/java/com/ga/batch/settlement/receive/EdiReceiver.java create mode 100644 ga-batch/src/main/java/com/ga/batch/settlement/receive/ExcelReceiver.java create mode 100644 ga-batch/src/main/java/com/ga/batch/settlement/receive/ReceiverDispatcher.java diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/receive/ApiReceiver.java b/ga-batch/src/main/java/com/ga/batch/settlement/receive/ApiReceiver.java new file mode 100644 index 0000000..e857029 --- /dev/null +++ b/ga-batch/src/main/java/com/ga/batch/settlement/receive/ApiReceiver.java @@ -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 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()); + } + } +} diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/receive/CsvReceiver.java b/ga-batch/src/main/java/com/ga/batch/settlement/receive/CsvReceiver.java new file mode 100644 index 0000000..8f2de75 --- /dev/null +++ b/ga-batch/src/main/java/com/ga/batch/settlement/receive/CsvReceiver.java @@ -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 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(); + } +} diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/receive/EdiReceiver.java b/ga-batch/src/main/java/com/ga/batch/settlement/receive/EdiReceiver.java new file mode 100644 index 0000000..2860c99 --- /dev/null +++ b/ga-batch/src/main/java/com/ga/batch/settlement/receive/EdiReceiver.java @@ -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 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()); + } + } +} diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/receive/ExcelReceiver.java b/ga-batch/src/main/java/com/ga/batch/settlement/receive/ExcelReceiver.java new file mode 100644 index 0000000..45f57c1 --- /dev/null +++ b/ga-batch/src/main/java/com/ga/batch/settlement/receive/ExcelReceiver.java @@ -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 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(); + } +} diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/receive/ReceiverDispatcher.java b/ga-batch/src/main/java/com/ga/batch/settlement/receive/ReceiverDispatcher.java new file mode 100644 index 0000000..3c0999a --- /dev/null +++ b/ga-batch/src/main/java/com/ga/batch/settlement/receive/ReceiverDispatcher.java @@ -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 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; + } +}