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

15 KiB

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/)

// 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 패턴

@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 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)

<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 테이블 통합)

// 핵심 조회 (배치 계산에서 호출)
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 테이블 통합)

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 (코드그룹 대응)

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/)

@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 트랜잭션으로 통합 테스트. (현재 미구현, 향후 작업.)