# 03_CORE_SPEC — ga-core (도메인 VO + Mapper + SQL) > 22 개 테이블의 용도별 VO + Mapper Interface + Mapper XML. > ga-common 의 표준 패턴(`SearchParam`, `PageResponse`, `BaseMapper`) 을 사용한다. ## 패키지 구조 ``` ga-core/src/main/java/com/ga/core/ ├── enums/ # AgentStatus, ContractStatus, SettleStatus 등 ├── mapper/ │ ├── org/ # GradeMapper, AgentMapper │ ├── product/ # InsuranceCompanyMapper, ProductMapper, ContractMapper │ ├── rule/ # RuleMapper (5 테이블 통합) │ ├── receive/ # ReceiveMapper (5 테이블 통합) │ ├── ledger/ # RecruitLedgerMapper, MaintainLedgerMapper, ExceptionLedgerMapper │ ├── settle/ # SettleMasterMapper, OverrideSettleMapper, ChargebackMapper, PaymentMapper │ ├── batch/ # BatchJobLogMapper, ReconciliationMapper │ └── user/ # UserMapper, RoleMapper ├── vo/ │ ├── org/ # GradeVO, OrganizationVO/Resp, AgentVO/Resp/SaveReq/SearchParam/ExcelVO, AgentOrgHistoryVO │ ├── product/ # InsuranceCompanyVO, ProductVO/Resp, ContractVO/Resp/SaveReq/SearchParam │ ├── rule/ # CommissionRateVO, PayoutRuleVO, OverrideRuleVO, ChargebackRuleVO, ExceptionTypeCodeVO │ ├── receive/ # CompanyProfileVO, CompanyFieldMappingVO, CodeMappingVO, RawCommissionDataVO, ParseErrorLogVO │ ├── ledger/ # LedgerVO, LedgerResp, LedgerSearchParam, LedgerExcelVO (3 테이블 공유) │ ├── settle/ # SettleMasterVO/Resp/SearchParam, OverrideSettleVO, ChargebackVO, PaymentVO │ ├── batch/ # BatchJobLogVO, ReconciliationVO │ └── user/ # UserVO/Resp/SaveReq/SearchParam, RoleVO └── mapstruct/ # AgentMapstruct, ContractMapstruct (SaveReq ↔ VO) ga-core/src/main/resources/mapper/ ├── org/ # GradeMapper.xml, AgentMapper.xml ├── product/ # ContractMapper.xml, ProductMapper.xml, InsuranceCompanyMapper.xml ├── rule/ # RuleMapper.xml ├── receive/ # ReceiveMapper.xml ├── ledger/ # RecruitLedgerMapper.xml, MaintainLedgerMapper.xml, ExceptionLedgerMapper.xml ├── settle/ # SettleMasterMapper.xml, PaymentMapper.xml, OverrideSettleMapper.xml, ChargebackMapper.xml ├── batch/ # BatchJobLogMapper.xml └── user/ # UserMapper.xml, RoleMapper.xml ``` build.gradle: `implementation project(':ga-common')`. --- ## 1. VO 설계 원칙 ★ 핵심 > **VO 는 테이블 1:1 이 아니라, 하나의 업무 기능당 용도별 3~4 개를 만든다.** | VO 종류 | 용도 | 네이밍 예 | 특징 | |---|---|---|---| | `XxxVO` | 테이블 1:1 매핑 | `AgentVO` | INSERT/UPDATE 전용, 모든 컬럼 | | `XxxResp` | API 응답 | `AgentResp` | 조인 결과 포함 (조직명/직급명/집계) — 화면 표시 전용 | | `XxxSaveReq` | API 요청 | `AgentSaveReq` | 사용자 입력 필드만 + `@Valid` (ID/상태/일시 없음) | | `XxxSearchParam` | 검색 조건 | `AgentSearchParam` | `SearchParam` 상속, 화면별 필터 | | `XxxExcelVO` | 엑셀 전용 | `AgentExcelVO` | `@ExcelColumn` 어노테이션, 목록과 다른 구성 | ### 예시: 설계사 (`org/`) ```java // AgentVO — 테이블 1:1 @Data public class AgentVO { private Long agentId, orgId, gradeId; private String agentName, residentNo, licenseNo, phone, email, bankCode, accountNo, status; private LocalDate joinDate, leaveDate; private LocalDateTime createdAt, updatedAt; } // AgentResp — 조인 결과 (화면 표시용) @Data public class AgentResp { private Long agentId, orgId, gradeId; private String agentName, status; private String statusName; // 공통코드 변환 private String orgName, gradeName;// 조인 private int contractCount; // 집계 private BigDecimal totalAmt; // settle_master 집계 private String phone; // 마스킹 private String licenseNo; private LocalDate joinDate; } // AgentSaveReq — 등록/수정 입력 @Data public class AgentSaveReq { @NotBlank private String agentName; @NotNull private Long orgId; @NotNull private Long gradeId; private String licenseNo, phone, email, bankCode; private String residentNo, accountNo; // 서버에서 EncryptTypeHandler 로 암호화 private LocalDate joinDate; public AgentVO toVO() { ... MapStruct ... } } // AgentSearchParam — 검색 @Data public class AgentSearchParam extends SearchParam { private Long orgId, gradeId; private String licenseNo; // status 는 SearchParam 에 이미 있음 } // AgentExcelVO — 엑셀 다운로드 전용 public class AgentExcelVO { @ExcelColumn(header = "설계사번호", order = 1) private Long agentId; @ExcelColumn(header = "성명", order = 2) private String agentName; @ExcelColumn(header = "조직", order = 3) private String orgName; @ExcelColumn(header = "직급", order = 4) private String gradeName; @ExcelColumn(header = "상태", order = 5, codeGroup = "AGENT_STATUS") private String status; @ExcelColumn(header = "주민번호", order = 6, masked = true) private String residentNo; } ``` ### Map 사용이 적절한 경우 (VO 대신) - 통계/리포트 (동적 컬럼) - 대시보드 집계 (1회성) - 동적 피벗 결과 - 공통코드 캐시 (key-value) --- ## 2. Mapper Interface 패턴 ```java @Mapper public interface AgentMapper { // 조회 AgentResp selectDetailById(@Param("agentId") Long agentId); AgentVO selectById(@Param("agentId") Long agentId); List selectList(AgentSearchParam param); Cursor selectCursorForExcel(AgentSearchParam param); // ★ 엑셀 100만건 // 검증 int existsByLicenseNo(@Param("licenseNo") String licenseNo); // CUD int insert(AgentVO vo); int update(AgentVO vo); int updateStatus(@Param("agentId") Long id, @Param("status") String status); int updateOrgGrade(@Param("agentId") Long id, @Param("orgId") Long o, @Param("gradeId") Long g); // 이력 List selectHistory(@Param("agentId") Long agentId); int insertHistory(AgentOrgHistoryVO vo); } ``` ### `BaseMapper` 상속 (선택) - 단순 CRUD 만 필요한 Mapper 는 `extends BaseMapper` 로 짧게. - 복잡한 조인/집계 가 필요하면 직접 정의. --- ## 3. Mapper XML 패턴 (필수 규칙) ```xml INSERT INTO agent (org_id, grade_id, agent_name, resident_no, license_no, phone, email, bank_code, account_no, status, join_date) VALUES (#{orgId}, #{gradeId}, #{agentName}, #{residentNo,typeHandler=com.ga.common.mybatis.EncryptTypeHandler}, #{licenseNo}, #{phone}, #{email}, #{bankCode}, #{accountNo,typeHandler=com.ga.common.mybatis.EncryptTypeHandler}, #{status}, #{joinDate}) ``` ### 필수 규칙 1. **`SELECT *` 금지** — 모든 컬럼 명시 2. **목록 SELECT 는 XxxResp 에 매핑** (조인 포함) 3. **INSERT/UPDATE 는 XxxVO 의 필드만** 사용 4. **암호화 컬럼은 `typeHandler` 명시** (resident_no, account_no, payment.account_no) 5. **동적 WHERE** — `` / `` 적극 사용 6. **신입 학습용 주석** — 복잡한 쿼리에 한글 주석 7. **CTE 재귀** — 조직 트리 (organization), 메뉴 트리 (menu) ### CTE 재귀 예시 (organization) ```xml ``` --- ## 4. 모듈별 Mapper 요약 ### `org/` - **`GradeMapper`** : selectAll, selectById, insert/update/delete - **`AgentMapper`** : (위 예시 — 22 개 메서드) ### `product/` - **`InsuranceCompanyMapper`** : 5 개 보험사 CRUD - **`ProductMapper`** : 보험사별 상품, insurance_type 별 통계 - **`ContractMapper`** : 검색 (agent/product/status), `existsByPolicyNo`, `countByAgent(agentId, status)` ### `rule/` — `RuleMapper` (단일 인터페이스, 5 테이블 통합) ```java // 핵심 조회 (배치 계산에서 호출) BigDecimal findCompanyRate(@Param("productId") Long, @Param("year") Integer, @Param("baseDate") LocalDate); BigDecimal findPayoutRate(@Param("gradeId") Long, @Param("insuranceType") String, @Param("year") Integer, @Param("baseDate") LocalDate); BigDecimal findChargebackRate(@Param("companyCode") String, @Param("insuranceType") String, @Param("lapseMonth") Integer, @Param("baseDate") LocalDate); List selectOverrideRules(); List selectExceptionCodes(); // + 5 개 테이블 각각의 CRUD ``` `effective_from <= baseDate < effective_to` 조건 (NULL = 무한). 같은 base 에 여러 행이면 `version DESC` 최신. ### `receive/` — `ReceiveMapper` (5 테이블 통합) ```java List selectFieldMappings(companyCode, targetTable); String findTargetCode(companyCode, codeType, sourceCode); int insertRaw(RawCommissionDataVO); int insertRawBatch(List); int updateRawStatus(rawId, parseStatus, mappedLedgerId, mappedTable); int insertError(ParseErrorLogVO); int resolveError(errorId, status, note, userId); ``` ### `ledger/` — 3 개 Mapper (구조 동일) - `RecruitLedgerMapper`, `MaintainLedgerMapper`, `ExceptionLedgerMapper` - 모두 `LedgerVO` 사용 (테이블 컬럼 동일) - 핵심: `selectList(LedgerSearchParam)`, `selectCursorForExcel(...)`, `batchInsert(List)`, `aggregateByAgent(settleMonth)`, `sumByAgentMonth(agentId, settleMonth)`, `deleteBySettleMonth(settleMonth)` - `ExceptionLedgerMapper` 추가: `approve(ledgerId, approverId)`, `reject(ledgerId, reason)` ### `settle/` - **`SettleMasterMapper`** : `upsert(SettleMasterVO)` (PG `ON CONFLICT(agent_id, settle_month) DO UPDATE`), `confirm(settleId, userId)`, `hold(settleId, reason)`, `selectMonthlySummary(month)` - **`OverrideSettleMapper`** : 정산별 오버라이드 내역 - **`ChargebackMapper`** : 환수 내역, 분할 회차 (`installment_seq`) - **`PaymentMapper`** : 이체 상태 추적, `account_no` AES 암호화 ### `batch/` - **`BatchJobLogMapper`** : `request(jobName, settleMonth)`, `start/complete/fail`, `selectLatest(jobName)`, `existsRunning(jobName)` ### `user/` - **`UserMapper`** : `selectByLoginId`, `selectMyMenus`, BCrypt 비밀번호 검증, 잠금/실패 카운트 - **`RoleMapper`** : 역할 목록, 사용자-역할 매핑, 역할-메뉴-권한 매트릭스 --- ## 5. Enum + MapStruct ### Enum (코드그룹 대응) ```java public enum AgentStatus { ACTIVE, LEAVE, SUSPEND; public boolean isActive() { return this == ACTIVE; } } public enum ContractStatus { ACTIVE, LAPSE, REVIVED, EXPIRED, CANCELED; } public enum SettleStatus { DRAFT, CONFIRMED, HOLD, PAID; } public enum ParseStatus { PENDING, OK, ERROR; } public enum ApproveStatus { PENDING, APPROVED, REJECTED; } ``` VO 의 String 필드는 enum 으로 받지 말고 **String 그대로** 둔다 (DB 와 1:1, MyBatis 호환성). 서비스 레이어에서 enum 으로 변환해서 사용. ### MapStruct (`mapstruct/`) ```java @Mapper(componentModel = "spring") public interface AgentMapstruct { AgentVO toVO(AgentSaveReq req); @Mapping(target = "createdAt", ignore = true) void update(@MappingTarget AgentVO target, AgentSaveReq req); } ``` `SaveReq → VO` 변환 정도만 사용. `Resp` 는 SQL ResultMap 이 채우므로 MapStruct 불필요. --- ## 6. 테스트 시나리오 (선택) `ga-core` 의 Mapper XML 검증은 통상 H2 호환 안되어 **Testcontainers PostgreSQL** 또는 운영 DB 의 read-only 트랜잭션으로 통합 테스트. (현재 미구현, 향후 작업.)