Files
GA Pro 035175e895 docs: Agent Teams 실행 가이드 + 8개 도메인 spec 추가
00_PROMPTS.md  — Claude Code 실행 프롬프트 1~6
01_DB_SCHEMA.md — V1~V17 Flyway 스키마 + 프로젝트 뼈대
02_COMMON_SPEC.md — ga-common 공통 프레임워크 (모델/예외/MyBatis/AOP/엑셀/공통컴포넌트)
03_CORE_SPEC.md — ga-core VO 설계 원칙 + Mapper 패턴
04_BATCH_SPEC.md — ga-batch 정산 8 Step + DataReceiver/MappingEngine
05_API_SPEC.md — ga-api Controller/Service 표준 패턴 + 엔드포인트 목록
06_FRONTEND_SPEC.md — ga-frontend 13개 화면 + 공통 컴포넌트 사용법
07_INFRA_SPEC.md — Docker/Nginx + README + DEVELOPER_GUIDE
08_DBA_SPEC.md — V13~V15 인덱스/파티셔닝/뷰

실제 구현된 ga-commission-system 기준으로 작성. 코드는 변경 없음.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:15:13 +09:00

11 KiB
Raw Permalink Blame History

04_BATCH_SPEC — ga-batch (정산 배치 8 Step)

Spring Batch 기반 월별 정산 잡. ga-core 의 Mapper 를 사용. 별도 실행 모듈 (포트 8081).

패키지 구조

ga-batch/src/main/java/com/ga/batch/
├── GaBatchApplication.java                    # @SpringBootApplication, scanBasePackages = "com.ga"
├── config/
│   └── BatchConfig.java                       # MonthlySettlementJob 정의
├── controller/
│   └── JobLauncherController.java             # POST /run, GET /status
└── settlement/
    ├── SettlementContext.java                 # @JobScope 공유 컨텍스트
    ├── calc/
    │   └── CommissionCalculator.java          # 모집/유지 수수료 계산기
    ├── receive/
    │   ├── DataReceiver.java                  # 전략 패턴 인터페이스
    │   └── ManualReceiver.java                # 기본 구현 (수동 업로드)
    ├── transform/
    │   └── MappingEngine.java                 # raw → 표준 원장
    └── step/
        ├── ReceiveDataStep.java               # Step 1
        ├── TransformDataStep.java             # Step 2
        ├── ReconcileStep.java                 # Step 3
        ├── CalcRecruitStep.java               # Step 4
        ├── CalcMaintainStep.java              # Step 5
        ├── ChargebackExceptionStep.java       # Step 6
        ├── CalcOverrideStep.java              # Step 7
        └── AggregateStep.java                 # Step 8

build.gradle:

apply plugin: 'org.springframework.boot'
bootJar { archiveFileName = 'ga-batch.jar'; mainClass = 'com.ga.batch.GaBatchApplication' }
jar.enabled = false
dependencies {
    implementation project(':ga-core')
    implementation 'org.springframework.boot:spring-boot-starter-batch'
}

application.yml:

server.port: 8081
spring:
  batch:
    jdbc:
      initialize-schema: always       # BATCH_JOB_* 메타테이블
    job: { enabled: false }           # 자동 실행 끄기, 컨트롤러로만 트리거

1. SettlementContext (@JobScope)

@Component @JobScope @Getter @Setter
public class SettlementContext {
    @Value("#{jobParameters['settleMonth']}") private String settleMonth;   // "202605"
    @Value("#{jobParameters['companyCode']}") private String companyCode;   // 선택 (없으면 전체)
    @Value("#{jobParameters['jobLogId']}")    private Long   jobLogId;

    // Step 간 공유 카운트
    private int receiveCount, transformOk, transformErr, reconcileOk, reconcileDiff;
    private int recruitCount, maintainCount, chargebackCount, exceptionCount, overrideCount;
    private int aggregateCount;
}

2. Job 정의 (BatchConfig)

@Configuration @RequiredArgsConstructor
public class BatchConfig {
    private final JobRepository jobRepo;
    private final PlatformTransactionManager txMgr;

    @Bean
    public Job monthlySettlementJob(
            ReceiveDataStep s1, TransformDataStep s2, ReconcileStep s3,
            CalcRecruitStep s4, CalcMaintainStep s5,
            ChargebackExceptionStep s6, CalcOverrideStep s7, AggregateStep s8) {
        return new JobBuilder("monthlySettlementJob", jobRepo)
            .start(toStep("receive",       s1))
            .next(toStep("transform",     s2))
            .next(toStep("reconcile",     s3))
            .next(toStep("calcRecruit",   s4))
            .next(toStep("calcMaintain",  s5))
            .next(toStep("chargeback",    s6))
            .next(toStep("calcOverride",  s7))
            .next(toStep("aggregate",     s8))
            .build();
    }

    private Step toStep(String name, Tasklet t) {
        return new StepBuilder(name, jobRepo).tasklet(t, txMgr).build();
    }
}

각 Step 은 Tasklet (단순 단계) 또는 Chunk 기반. 대용량 (Step 4, 5) 은 MyBatisCursorItemReader + ItemWriter 청크 처리.


3. 8 Step 명세

Step 1 — receiveData

  • 입력: companyCode (또는 전체 활성 보험사)
  • 처리: DataReceiver 전략 패턴
    • Excel/CSV (수동 업로드 — 기본): ManualReceiver
    • EDI (FTP), API (HTTP) 는 향후 확장
    • 각 receiver 가 raw_commission_data 에 1 행씩 (raw_content JSON) 저장
    • 우선 구현: ManualReceiver 만. 다른 receiver 는 인터페이스만 두고 stub.
  • 출력: ctx.receiveCount
public interface DataReceiver {
    String getMethod();   // EXCEL / CSV / EDI / API / MANUAL
    int receive(String companyCode, String settleMonth);
}

Step 2 — transformData (★ 핵심)

  • 입력: raw_commission_data 에서 parse_status='PENDING' 인 행
  • 처리: MappingEngine.transform(raw) 호출
    • company_field_mapping 로 source → target 매핑
    • 변환 규칙: TRIM / UPPER / LOWER / DATE / DIVIDE / MULTIPLY / CODE_MAP
    • policy_nocontract 매칭 → contract_id, agent_id 채움
    • data_type 에 따라 recruit_ledger / maintain_ledger 에 INSERT
    • 실패 시 parse_error_log 에 적재
  • 9 가지 에러 유형: INVALID_INPUT / UNKNOWN_TYPE / NO_MAPPING / JSON_PARSE / REQUIRED / TRANSFORM_FAIL / NO_POLICY / CONTRACT_NOT_FOUND / CODE_MAP_FAIL
  • 출력: ctx.transformOk, ctx.transformErr

MappingEngine 의 핵심 메서드:

@Transactional
public Long transform(RawCommissionDataVO raw) {
    // 1) companyCode/rawContent null check
    // 2) data_type → target_table (RECRUIT/MAINTAIN)
    // 3) selectFieldMappings(companyCode, target_table)
    // 4) JSON 파싱 (ObjectMapper)
    // 5) 각 매핑별 applyTransform(value, mapping, companyCode) → mapped Map
    // 6) policy_no 로 contract 매칭
    // 7) buildLedger(mapped, contract, raw) → recruit/maintain mapper insert
}

Step 3 — reconcile

  • 입력: 보험사 통보 합계 vs 시스템 계산 합계 (raw_commission_data 기준)
  • 처리: reconciliation 테이블에 expected/actual/diff 적재
    • 차이 임계값(시스템설정 RECON_DIFF_THRESHOLD) 초과 시 recon_status='DIFF'
  • 출력: ctx.reconcileOk, ctx.reconcileDiff

Step 4 — calcRecruit

  • 입력: recruit_ledger 의 status='CALCULATED' 또는 신규 계약 (계약일이 정산월 이내)
  • 처리: CommissionCalculator.calcRecruitLedger(contract, settleMonth)
    • contractDate 기준 commission_rate (commission_year=1) → companyAmount
    • gradeId + insuranceType + commission_year=1 기준 payout_rateagentAmount
    • tax = agentAmount × 3.3% (HALF_UP)
  • 출력: ctx.recruitCount
public LedgerVO calcRecruitLedger(ContractResp c, String settleMonth) {
    AgentVO agent = agentMapper.selectById(c.getAgentId());
    if (agent == null) return null;
    LocalDate base = c.getContractDate() != null ? c.getContractDate() : LocalDate.now();

    BigDecimal companyRate = ruleMapper.findCompanyRate(c.getProductId(), 1, base);
    if (companyRate == null) companyRate = BigDecimal.ZERO;
    BigDecimal payoutRate  = ruleMapper.findPayoutRate(agent.getGradeId(),
                                  c.getInsuranceType(), 1, base);
    if (payoutRate == null) payoutRate = BigDecimal.ZERO;

    BigDecimal premium = MoneyUtil.zero(c.getPremium());
    BigDecimal companyAmt = MoneyUtil.pct(premium,    companyRate);
    BigDecimal agentAmt   = MoneyUtil.pct(companyAmt, payoutRate);
    BigDecimal tax        = MoneyUtil.tax(agentAmt);
    // LedgerVO 필드 채워서 반환
}

Step 5 — calcMaintain

  • 입력: 유지 계약 (status='ACTIVE', effective_date 가 1~5 년차 도래)
  • 처리: CommissionCalculator.calcMaintainLedger(contract, settleMonth, year)
    • commission_year 만 1 → year (2~5) 로 다름
    • 같은 공식 적용
  • 출력: ctx.maintainCount

Step 6 — chargebackException

  • 입력: 정산월 직전 실효 계약 (status='LAPSE', lapse_date 가 정산월 범위)
  • 처리:
    • ruleMapper.findChargebackRate(companyCode, insuranceType, lapseMonthDiff, base)
    • 환수 계산: cb_amount = base × cb_rate
    • 분할 회차(installment_seq) 정의 시 N 회로 분할
    • 결과를 chargeback 에 적재
  • 예외 자동 생성: exception_type_codeauto_calc_yn='Y' 인 코드별로 exception_ledger 에 기록 (수동 승인 대기)
  • 출력: ctx.chargebackCount, ctx.exceptionCount

Step 7 — calcOverride

  • 입력: Step 4/5 에서 생성된 모집/유지 수수료 합계 (per agent)
  • 처리:
    • 조직 트리 상향 탐색 (CTE): 설계사 → 팀장 → 지점장 → 본부장
    • 각 단계마다 override_rule (from_grade → to_grade) 의 override_pct 적용
    • cap_amount 가 있으면 상한 적용
    • override_settle 에 (from_agent, to_agent, base, pct, amount) 적재
  • 출력: ctx.overrideCount

Step 8 — aggregate

  • 입력: 모든 원장의 설계사별 합계 + 환수 + 오버라이드 + 예외(PLUS/MINUS)
  • 처리:
    gross = recruit + maintain + override + excPlus - chargeback - excMinus
    tax   = gross × TAX_RATE   (system_config 의 TAX_RATE, 기본 3.3%)
    net   = gross - tax
    
  • settle_master(agent_id, settle_month) UPSERT (ON CONFLICT DO UPDATE)
    • status = 'CALCULATED' (확정은 별도 API)
  • 활성 설계사 누락 보충: agentMapper.selectList(status=ACTIVE) 로 0 행도 빈 settle 생성
  • 출력: ctx.aggregateCount

4. JobLauncherController

@RestController @RequestMapping("/api/batch/settlement")
public class JobLauncherController {
    private final JobLauncher launcher;
    private final Job monthlySettlementJob;
    private final BatchJobLogMapper logMapper;

    @PostMapping("/run")
    public ApiResponse<Long> run(@RequestParam String settleMonth,
                                 @RequestParam(required=false) String companyCode) {
        if (logMapper.existsRunning("monthlySettlementJob") > 0)
            throw new BizException(ErrorCode.BATCH_RUNNING);
        Long jobLogId = logMapper.request("monthlySettlementJob", settleMonth);
        JobParameters p = new JobParametersBuilder()
            .addString("settleMonth", settleMonth)
            .addString("companyCode", companyCode)
            .addLong  ("jobLogId",   jobLogId)
            .addLong  ("ts", System.currentTimeMillis())   // 동일 파라미터 재실행
            .toJobParameters();
        new Thread(() -> launcher.run(monthlySettlementJob, p)).start();
        return ApiResponse.ok(jobLogId);
    }

    @GetMapping("/status")
    public ApiResponse<BatchJobLogVO> status(@RequestParam Long jobLogId) {
        return ApiResponse.ok(logMapper.selectById(jobLogId));
    }
}

ga-api 도 동일 엔드포인트 노출 가능 — 배치 서버가 따로 떠 있을 때는 ga-api 가 batch_job_log.status='REQUESTED' 로 적재만 하고, ga-batch 가 폴링/직접 호출.


5. 단위 테스트 (현재 작성됨)

ga-batch/src/test/java/com/ga/batch/:

  • settlement/calc/CommissionCalculatorTest (6) — 모집/유지/폴백/agent 누락
  • settlement/transform/MappingEngineTest (13) — RECRUIT/MAINTAIN 라우팅, TRIM/UPPER/LOWER/DATE/DIVIDE/MULTIPLY/CODE_MAP, 9 가지 에러

6. 향후 확장 (현재 단순화 부분)

  1. DataReceiver: EDI / FTP / API 구현 (현재 ManualReceiver 만)
  2. MappingEngine: REPLACE / SUBSTRING / CONCAT / LOOKUP 등 추가 변환 규칙
  3. 이체파일: 은행별 EBCDIC / CSV 출력 어댑터 (Step 9 으로 분리)
  4. 재실행: 특정 Step 부터 재실행 (/api/batch/settlement/restart?fromStep=4)
  5. 병렬 처리: Step 4/5 를 partitioning 으로 멀티스레드화