# 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<AgentResp> selectList(AgentSearchParam param);
    Cursor<AgentExcelVO> 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<AgentOrgHistoryVO> selectHistory(@Param("agentId") Long agentId);
    int insertHistory(AgentOrgHistoryVO vo);
}
```

### `BaseMapper<T>` 상속 (선택)
- 단순 CRUD 만 필요한 Mapper 는 `extends BaseMapper<XxxVO>` 로 짧게.
- 복잡한 조인/집계 가 필요하면 직접 정의.

---

## 3. Mapper XML 패턴 (필수 규칙)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.org.AgentMapper">

    <!-- 컬럼 암호화는 명시적으로 -->
    <resultMap id="AgentVoMap" type="com.ga.core.vo.org.AgentVO">
        <id     property="agentId" column="agent_id"/>
        <result property="orgId"      column="org_id"/>
        <result property="gradeId"    column="grade_id"/>
        <result property="agentName"  column="agent_name"/>
        <result property="residentNo" column="resident_no"
                typeHandler="com.ga.common.mybatis.EncryptTypeHandler"/>
        <result property="accountNo"  column="account_no"
                typeHandler="com.ga.common.mybatis.EncryptTypeHandler"/>
        <!-- 나머지는 mapUnderscoreToCamelCase 자동 -->
    </resultMap>

    <resultMap id="AgentRespMap" type="com.ga.core.vo.org.AgentResp">
        <id     property="agentId"   column="agent_id"/>
        <result property="orgName"   column="org_name"/>
        <result property="gradeName" column="grade_name"/>
        <result property="residentNo" column="resident_no"
                typeHandler="com.ga.common.mybatis.EncryptTypeHandler"/>
    </resultMap>

    <!-- 목록: Resp 매핑, 조인 포함 -->
    <select id="selectList" parameterType="com.ga.core.vo.org.AgentSearchParam"
            resultMap="AgentRespMap">
        SELECT a.agent_id, a.agent_name, a.status, a.phone, a.license_no, a.join_date,
               o.org_name, g.grade_name,
               (SELECT COUNT(*) FROM contract c WHERE c.agent_id = a.agent_id) AS contract_count
          FROM agent a
          JOIN organization o ON a.org_id = o.org_id
          JOIN grade        g ON a.grade_id = g.grade_id
         WHERE 1=1
        <if test="orgId != null">      AND a.org_id     = #{orgId}</if>
        <if test="gradeId != null">    AND a.grade_id   = #{gradeId}</if>
        <if test="status != null">     AND a.status     = #{status}</if>
        <if test="licenseNo != null">  AND a.license_no = #{licenseNo}</if>
        <if test="searchKeyword != null and searchKeyword != ''">
            AND a.agent_name LIKE CONCAT('%', #{searchKeyword}, '%')
        </if>
         ORDER BY a.agent_id DESC
    </select>

    <!-- INSERT: VO 만 받음, 조인 필드는 절대 사용 X -->
    <insert id="insert" parameterType="com.ga.core.vo.org.AgentVO"
            useGeneratedKeys="true" keyProperty="agentId">
        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})
    </insert>

    <!-- 엑셀용 Cursor (절대 List 로 받지 말 것) -->
    <select id="selectCursorForExcel" resultType="com.ga.core.vo.org.AgentExcelVO">
        SELECT a.agent_id, a.agent_name, o.org_name, g.grade_name, a.status,
               a.resident_no
          FROM agent a JOIN organization o ON a.org_id = o.org_id
                       JOIN grade        g ON a.grade_id = g.grade_id
         ORDER BY a.agent_id
    </select>
</mapper>
```

### 필수 규칙
1. **`SELECT *` 금지** — 모든 컬럼 명시
2. **목록 SELECT 는 XxxResp 에 매핑** (조인 포함)
3. **INSERT/UPDATE 는 XxxVO 의 필드만** 사용
4. **암호화 컬럼은 `typeHandler` 명시** (resident_no, account_no, payment.account_no)
5. **동적 WHERE** — `<if>` / `<choose>` 적극 사용
6. **신입 학습용 주석** — 복잡한 쿼리에 한글 주석
7. **CTE 재귀** — 조직 트리 (organization), 메뉴 트리 (menu)

### CTE 재귀 예시 (organization)
```xml
<select id="selectTree" resultType="com.ga.core.vo.org.OrganizationResp">
    WITH RECURSIVE org_tree AS (
        SELECT org_id, parent_org_id, org_name, org_type, org_level, 1 AS depth
          FROM organization WHERE parent_org_id IS NULL AND is_active = 'Y'
        UNION ALL
        SELECT o.org_id, o.parent_org_id, o.org_name, o.org_type, o.org_level, t.depth + 1
          FROM organization o JOIN org_tree t ON o.parent_org_id = t.org_id
         WHERE o.is_active = 'Y'
    )
    SELECT org_id, parent_org_id, org_name, org_type, org_level
      FROM org_tree ORDER BY org_level, org_name
</select>
```

---

## 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<OverrideRuleVO> selectOverrideRules();
List<ExceptionTypeCodeVO> selectExceptionCodes();
// + 5 개 테이블 각각의 CRUD
```

`effective_from <= baseDate < effective_to` 조건 (NULL = 무한). 같은 base 에 여러 행이면 `version DESC` 최신.

### `receive/` — `ReceiveMapper` (5 테이블 통합)
```java
List<CompanyFieldMappingVO> selectFieldMappings(companyCode, targetTable);
String findTargetCode(companyCode, codeType, sourceCode);
int insertRaw(RawCommissionDataVO);
int insertRawBatch(List<RawCommissionDataVO>);
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<LedgerVO>)`, `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 트랜잭션으로 통합 테스트. (현재 미구현, 향후 작업.)
