035175e895
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>
11 KiB
11 KiB
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(수동 업로드 — 기본):ManualReceiverEDI(FTP),API(HTTP) 는 향후 확장- 각 receiver 가
raw_commission_data에 1 행씩 (raw_contentJSON) 저장 - 우선 구현:
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_no로contract매칭 →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) →companyAmountgradeId+insuranceType+commission_year=1기준payout_rate→agentAmounttax = 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_code의auto_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. 향후 확장 (현재 단순화 부분)
- DataReceiver: EDI / FTP / API 구현 (현재 ManualReceiver 만)
- MappingEngine: REPLACE / SUBSTRING / CONCAT / LOOKUP 등 추가 변환 규칙
- 이체파일: 은행별 EBCDIC / CSV 출력 어댑터 (Step 9 으로 분리)
- 재실행: 특정 Step 부터 재실행 (
/api/batch/settlement/restart?fromStep=4) - 병렬 처리: Step 4/5 를
partitioning으로 멀티스레드화