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