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>
This commit is contained in:
GA Pro
2026-05-10 21:15:13 +09:00
parent 936d376794
commit 035175e895
9 changed files with 2756 additions and 0 deletions
+284
View File
@@ -0,0 +1,284 @@
# 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:
```groovy
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`:
```yaml
server.port: 8081
spring:
batch:
jdbc:
initialize-schema: always # BATCH_JOB_* 메타테이블
job: { enabled: false } # 자동 실행 끄기, 컨트롤러로만 트리거
```
---
## 1. SettlementContext (`@JobScope`)
```java
@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`)
```java
@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`
```java
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` 의 핵심 메서드:
```java
@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_rate``agentAmount`
- `tax = agentAmount × 3.3%` (HALF_UP)
- 출력: `ctx.recruitCount`
```java
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
```java
@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` 으로 멀티스레드화