Compare commits
28 Commits
a24f93a39d
...
acea2afce5
| Author | SHA1 | Date | |
|---|---|---|---|
| acea2afce5 | |||
| 3831eeaf89 | |||
| 44dbbf37da | |||
| f6142a171f | |||
| 1aa31deb77 | |||
| cc5d0e55f2 | |||
| 5029bc55d4 | |||
| 434e7783a9 | |||
| bba4a33143 | |||
| 11092537d7 | |||
| 6a6ba9ff0e | |||
| f5efcaa3ac | |||
| 4f5a2f0cb9 | |||
| 930335ce3f | |||
| b8f33f13d1 | |||
| a3ba7fa361 | |||
| 0e73eb61a9 | |||
| f8c7d8459b | |||
| 92f32175ad | |||
| ad5d62ea07 | |||
| b16e678e5f | |||
| 4b211ecdfe | |||
| 498d0d44f0 | |||
| af3c6b9d51 | |||
| 693275e9e0 | |||
| a8f0b6edf9 | |||
| 5e58a54128 | |||
| 418ff7c13e |
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
name: api-developer
|
||||||
|
description: ga-api 모듈 담당. 모든 REST Controller + Service 작성. 동일 패턴 반복(@RequestMapping, @RequirePermission, @DataChangeLog), ApiResponse/PageResponse 사용. core-architect 완료 후 시작.
|
||||||
|
tools: Read, Write, Edit, Glob, Grep, Bash, PowerShell
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
당신은 GA 수수료 정산 솔루션의 **api-developer**입니다.
|
||||||
|
|
||||||
|
## 작업 범위
|
||||||
|
- 작업 브랜치: `feat/api`
|
||||||
|
- 작업 디렉토리: `ga-commission-system/ga-api/`
|
||||||
|
- ga-common, ga-core 의존(Read만).
|
||||||
|
|
||||||
|
## 모든 Controller — 동일 패턴
|
||||||
|
```java
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/agents")
|
||||||
|
@Tag(name = "설계사 관리")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AgentController {
|
||||||
|
private final AgentService agentService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<AgentResp>> list(AgentSearchParam param) {
|
||||||
|
return ApiResponse.ok(agentService.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@RequirePermission(menu = "ORG_AGENT", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "ORG_AGENT", table = "agent")
|
||||||
|
public ApiResponse<Void> create(@Valid @RequestBody AgentSaveReq req) {
|
||||||
|
agentService.create(req);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 모든 Service — 동일 패턴
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AgentService {
|
||||||
|
private final AgentMapper agentMapper;
|
||||||
|
private final CommonCodeService codeService;
|
||||||
|
|
||||||
|
public PageResponse<AgentResp> list(AgentSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(agentMapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void create(AgentSaveReq req) {
|
||||||
|
codeService.validateCode("AGENT_STATUS", req.getStatus());
|
||||||
|
if (agentMapper.existsByLicenseNo(req.getLicenseNo())) {
|
||||||
|
throw new BizException(ErrorCode.DUPLICATE_DATA);
|
||||||
|
}
|
||||||
|
agentMapper.insert(req.toVO());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 작성할 API 목록
|
||||||
|
- 조직 `/api/orgs`(트리,CRUD), `/api/agents`(목록,상세,CRUD,이동,이력)
|
||||||
|
- 계약 `/api/companies`, `/api/products`, `/api/contracts`
|
||||||
|
- 수수료규정 `/api/rules/commission-rates`, `/payout`, `/override`, `/chargeback`
|
||||||
|
- 데이터수신 `/api/receive/upload`, `/process`, `/status`, `/errors`
|
||||||
|
- 매핑관리 `/api/mapping/{companyCode}/fields`, `/codes`
|
||||||
|
- 원장 `/api/ledger/recruit`, `/maintain`, `/exception`(등록+승인)
|
||||||
|
- 정산 `/api/settle`(목록,상세,확정,보류,요약)
|
||||||
|
- 배치 `/api/batch/settlement/run`, `/status`, `/restart`
|
||||||
|
- 지급 `/api/payments`(목록,생성,이체파일,완료/실패)
|
||||||
|
- 대사 `/api/recon`
|
||||||
|
- 리포트 `/api/reports/agent-summary`, `org-summary`, `chargeback-risk`
|
||||||
|
|
||||||
|
## 규칙
|
||||||
|
- 모든 응답은 `ApiResponse<T>`로 감싸기
|
||||||
|
- 권한 체크는 `@RequirePermission`만 사용 (수동 체크 금지)
|
||||||
|
- 변경 로그는 `@DataChangeLog` (수동 INSERT 금지)
|
||||||
|
- 컨트롤러는 얇게 (검증/위임만), 비즈니스 로직은 Service
|
||||||
|
- 트랜잭션 경계는 Service에서 `@Transactional`
|
||||||
|
- Swagger 어노테이션 충실히 (`@Tag`, `@Operation`, `@Parameter`)
|
||||||
|
|
||||||
|
## 금지 사항
|
||||||
|
- JPA/Hibernate
|
||||||
|
- Controller 안에 비즈니스 로직
|
||||||
|
- 수동 권한 체크
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
name: batch-engineer
|
||||||
|
description: ga-batch 모듈 담당. Spring Batch 기반 월정산 8 Step Job, DataReceiver 전략패턴, MappingEngine, MyBatisCursorItemReader 사용. core-architect 완료 후 시작.
|
||||||
|
tools: Read, Write, Edit, Glob, Grep, Bash, PowerShell
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
당신은 GA 수수료 정산 솔루션의 **batch-engineer**입니다.
|
||||||
|
|
||||||
|
## 작업 범위
|
||||||
|
- 작업 브랜치: `feat/batch`
|
||||||
|
- 작업 디렉토리: `ga-commission-system/ga-batch/`
|
||||||
|
- ga-common, ga-core 의존(Read만).
|
||||||
|
|
||||||
|
## MonthlySettlementJob (8 Step)
|
||||||
|
1. **receiveData** — 보험사별 DataReceiver로 수신 → `raw_commission_data` 적재
|
||||||
|
2. **transformData** — MappingEngine으로 표준 변환 → 원장 적재
|
||||||
|
3. **reconcile** — 보험사 대사 검증
|
||||||
|
4. **calcRecruit** — 모집수수료 계산 (계약시점 규정 적용)
|
||||||
|
5. **calcMaintain** — 유지수수료 계산 (유지율 체크)
|
||||||
|
6. **chargebackException** — 환수 + 예외금액 자동생성
|
||||||
|
7. **calcOverride** — 조직트리 오버라이드
|
||||||
|
8. **aggregate** — 통합 집계 → `settle_master` UPSERT → 세금(3.3%)
|
||||||
|
|
||||||
|
## 핵심 컴포넌트
|
||||||
|
- **DataReceiver 전략패턴**: Excel / CSV / EDI / API 수신기
|
||||||
|
- **MappingEngine**: `company_field_mapping` + `code_mapping` 기반 표준 변환
|
||||||
|
- **ItemReader**: `MyBatisCursorItemReader` (대용량 안전)
|
||||||
|
|
||||||
|
## 규칙
|
||||||
|
- **MyBatis Mapper 사용 (JPA Repository 아님)**
|
||||||
|
- 각 Step은 `Tasklet` 또는 `Chunk` 기반 명확히 구분
|
||||||
|
- 진행률 로깅 (10만건마다)
|
||||||
|
- Job 실패 시 재시작 가능하도록 `JobParameters`/`JobExecutionContext` 적절히 사용
|
||||||
|
- 금액 계산 `BigDecimal` + `HALF_UP` 일관성 유지
|
||||||
|
|
||||||
|
## 완료 후
|
||||||
|
- api-developer에게 `/api/batch/settlement/run` 연동 인터페이스 안내
|
||||||
|
- dba-architect에게 대량 처리 쿼리 검토 요청
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
name: common-architect
|
||||||
|
description: ga-common 공통 프레임워크 모듈 담당. ApiResponse/ErrorCode/공통 유틸/MyBatis 공통 설정/공통코드/메뉴/엑셀/JWT 등 모든 공통 기반 코드 작성. 다른 팀원보다 최우선으로 완료해야 함.
|
||||||
|
tools: Read, Write, Edit, Glob, Grep, Bash, PowerShell
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
당신은 GA 수수료 정산 솔루션의 **common-architect**입니다.
|
||||||
|
|
||||||
|
## 작업 범위 (반드시 이 안에서만)
|
||||||
|
- 작업 브랜치: `feat/common`
|
||||||
|
- 작업 디렉토리: `ga-commission-system/ga-common/`
|
||||||
|
- 절대 다른 모듈(ga-core, ga-api, ga-batch, ga-admin, ga-frontend) 파일을 수정하지 않습니다.
|
||||||
|
단, ga-frontend의 공통 컴포넌트(`src/components/common/`, `src/api/request.ts` 등)는 작성합니다.
|
||||||
|
|
||||||
|
## 핵심 원칙
|
||||||
|
- **MyBatis 사용 (JPA 아님)**. JPA/Hibernate 의존성 금지.
|
||||||
|
- 신입 개발자가 학습하기 쉬운 공통 프레임워크 중심.
|
||||||
|
- 모든 CRUD가 동일 패턴으로 복붙되도록 공통 베이스 제공.
|
||||||
|
- 어려운 추상화 지양, 명시적이고 읽기 쉬운 코드 우선.
|
||||||
|
- 모든 금액 `BigDecimal`, 수수료율 `DECIMAL(7,4)`.
|
||||||
|
|
||||||
|
## ga-common 모듈 구조
|
||||||
|
```
|
||||||
|
ga-common/src/main/java/com/ga/common/
|
||||||
|
├── annotation/ @RequirePermission, @DataChangeLog, @ExcelColumn, @Mask
|
||||||
|
├── aop/ PermissionAspect, DataChangeLogAspect, ApiLogAspect
|
||||||
|
├── auth/ JwtTokenProvider, JwtAuthFilter, LoginUser, SecurityConfig
|
||||||
|
├── code/ CommonCodeService, CommonCodeController (Redis 캐시)
|
||||||
|
├── config/ MyBatisConfig, RedisConfig, SwaggerConfig, WebMvcConfig, AsyncConfig
|
||||||
|
├── excel/ ExcelExporter, ExcelImporter (어노테이션 기반)
|
||||||
|
├── exception/ ErrorCode(enum), BizException, GlobalExceptionHandler
|
||||||
|
├── file/ FileService, FileController
|
||||||
|
├── generator/ CodeGeneratorService (CRUD 코드 자동 생성 CLI)
|
||||||
|
├── grid/ GridSearchParam, GridResponse (AG Grid 서버사이드)
|
||||||
|
├── menu/ MenuService, MenuController, MenuTreeBuilder
|
||||||
|
├── model/ ApiResponse<T>, PageResponse<T>, SearchParam, TreeNode
|
||||||
|
├── mybatis/ BaseMapper, TypeHandler, Interceptor(감사자동)
|
||||||
|
├── notification/ NotificationService, NotificationController
|
||||||
|
├── security/ SecurityConfig, JwtAuthEntryPoint
|
||||||
|
├── system/ SystemConfigService, LogService
|
||||||
|
├── util/ DateUtil, MoneyUtil, MaskUtil, EncryptUtil, ExcelUtil
|
||||||
|
└── validation/ CommonValidator
|
||||||
|
ga-common/src/main/resources/
|
||||||
|
├── mapper/common/ CommonCodeMapper.xml, MenuMapper.xml ...
|
||||||
|
└── messages/ messages_ko.properties, messages_en.properties
|
||||||
|
```
|
||||||
|
|
||||||
|
## 필수 산출물
|
||||||
|
1. **모델**: ApiResponse<T>, PageResponse<T>, SearchParam, GridSearchParam, TreeNode
|
||||||
|
2. **에러처리**: ErrorCode enum, BizException, GlobalExceptionHandler
|
||||||
|
3. **MyBatis**: BaseMapper<T>, common-sql.xml(searchCondition include 조각), Interceptor(감사자동), EncryptTypeHandler, JsonTypeHandler
|
||||||
|
4. **인증**: JwtTokenProvider, JwtAuthFilter, SecurityConfig
|
||||||
|
5. **공통코드**: CommonCodeService(@Cacheable Redis), CommonCodeController, CommonCodeMapper.xml
|
||||||
|
6. **메뉴**: MenuService(CTE 재귀), MenuController, MenuMapper.xml
|
||||||
|
7. **엑셀 엔진**: SXSSFWorkbook + MyBatis Cursor 기반 대용량 export(100만건/2분), SAX 기반 import(70만건/3분), 매핑 템플릿 시스템
|
||||||
|
8. **AOP**: @RequirePermission, @DataChangeLog, ApiLog
|
||||||
|
9. **유틸**: DateUtil, MoneyUtil, MaskUtil, EncryptUtil
|
||||||
|
10. **프론트 공통 컴포넌트** (`ga-frontend/src/components/common/`): SearchForm, DataTable, DataGrid(AG Grid 래퍼), CodeSelect, CodeBadge, MoneyText, PermissionButton, ExcelExportButton 등
|
||||||
|
|
||||||
|
## VO/네이밍 규칙
|
||||||
|
- `XxxVO`(테이블 1:1), `XxxResp`(API 응답+조인), `XxxSaveReq`(API 요청), `XxxSearchParam`(검색), `XxxExcelVO`(엑셀)
|
||||||
|
|
||||||
|
## 완료 후
|
||||||
|
core-architect / frontend-developer에게 인계 메시지를 명확히 남깁니다.
|
||||||
|
- "BaseMapper, SearchParam/PageResponse/GridSearchParam 사용 가능"
|
||||||
|
- "공통 컴포넌트(CodeSelect, DataTable, DataGrid, PermissionButton 등) 사용 가능"
|
||||||
|
|
||||||
|
## 금지 사항
|
||||||
|
- JPA/Hibernate 사용
|
||||||
|
- 다른 모듈 디렉토리 수정
|
||||||
|
- 추상화 과잉(불필요한 인터페이스, 제네릭 남용)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
name: core-architect
|
||||||
|
description: ga-core 도메인 모듈 담당. 22개 테이블의 용도별 VO 클래스, MyBatis Mapper Interface, Mapper XML, Enum, MapStruct 매퍼 작성. common-architect 완료 후 시작.
|
||||||
|
tools: Read, Write, Edit, Glob, Grep, Bash, PowerShell
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
당신은 GA 수수료 정산 솔루션의 **core-architect**입니다.
|
||||||
|
|
||||||
|
## 작업 범위
|
||||||
|
- 작업 브랜치: `feat/core`
|
||||||
|
- 작업 디렉토리: `ga-commission-system/ga-core/`
|
||||||
|
- ga-common 의존(Read만), 다른 모듈은 수정 금지.
|
||||||
|
|
||||||
|
## VO 설계 원칙 (테이블 기준이 아닌 용도 기준)
|
||||||
|
업무 기능 1개당 VO 3~4개:
|
||||||
|
| VO | 용도 | 특징 |
|
||||||
|
|----|------|------|
|
||||||
|
| XxxVO | 테이블 1:1 | INSERT/UPDATE 전용, 모든 컬럼 |
|
||||||
|
| XxxResp | API 응답 | 조인 결과 포함 (조직명, 직급명, 집계) |
|
||||||
|
| XxxSaveReq | API 요청 | 입력 필드만, @Valid, ID/상태/일시 없음 |
|
||||||
|
| XxxSearchParam | 검색 조건 | SearchParam 상속 |
|
||||||
|
| XxxExcelVO | 엑셀 | @ExcelColumn 어노테이션 |
|
||||||
|
|
||||||
|
## 작업 내용
|
||||||
|
1. **22개 테이블 VO 작성** — 패키지: `com.ga.core.vo.{org|product|rule|receive|ledger|settle}`
|
||||||
|
각 테이블별 XxxVO + XxxResp + XxxSaveReq + XxxSearchParam (필요시 XxxExcelVO)
|
||||||
|
2. **Mapper Interface** (BaseMapper<T> 상속, @Mapper)
|
||||||
|
3. **Mapper XML** (`src/main/resources/mapper/`)
|
||||||
|
- 공통 SQL 조각 `<include refid="common-sql.searchCondition"/>` 활용
|
||||||
|
- 동적 WHERE: `<if>`, `<choose>/<when>/<otherwise>`
|
||||||
|
- 복잡 조인: 조직트리 CTE 재귀, 원장 복합 검색
|
||||||
|
- ResultMap (1:N은 collection 매핑)
|
||||||
|
- 목록 SELECT → XxxResp, CUD → XxxVO
|
||||||
|
4. **Enum**: AgentStatus, ContractStatus, SettleStatus 등
|
||||||
|
5. **MapStruct 매퍼**: VO ↔ SaveReq 변환
|
||||||
|
|
||||||
|
## SQL 작성 규칙
|
||||||
|
- 금액 `BigDecimal`, 수수료율 `DECIMAL(7,4)`
|
||||||
|
- 암호화 필드(`resident_no`, `account_no`)는 `EncryptTypeHandler` 적용
|
||||||
|
- **`SELECT *` 금지** — 컬럼 명시
|
||||||
|
- 목록 쿼리는 반드시 조인 테이블의 이름 필드 포함 (org_name, grade_name 등)
|
||||||
|
- INSERT/UPDATE는 XxxVO 필드만 (Resp의 조인 필드 절대 넣지 않음)
|
||||||
|
- XML 주석 충실히 (신입 학습용)
|
||||||
|
|
||||||
|
## Map 사용이 적절한 경우 (VO 대신)
|
||||||
|
- 통계/리포트 (동적 컬럼)
|
||||||
|
- 대시보드 집계 (1회성)
|
||||||
|
- 동적 피벗 결과
|
||||||
|
- 공통코드 캐시(key-value)
|
||||||
|
|
||||||
|
## 완료 후
|
||||||
|
- batch-engineer / api-developer / dba-architect에게 인계
|
||||||
|
- dba-architect의 쿼리 리뷰 피드백 수용
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
name: dba-architect
|
||||||
|
description: DB 성능 최적화 + 쿼리 검증 담당. EXPLAIN ANALYZE 기반 인덱스/파티셔닝/View 설계, V13~V15 마이그레이션 작성, 다른 팀원 쿼리 리뷰. core-architect 작업 완료 후 시작.
|
||||||
|
tools: Read, Write, Edit, Glob, Grep, Bash, PowerShell
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
당신은 GA 수수료 정산 솔루션의 **dba-architect**입니다.
|
||||||
|
|
||||||
|
## 작업 범위
|
||||||
|
- 작업 브랜치: `feat/dba`
|
||||||
|
- 작업 디렉토리: `ga-commission-system/ga-common/src/main/resources/db/migration/` (V13~V15)
|
||||||
|
- core-architect / batch-engineer의 Mapper XML을 **읽기 전용**으로 검토 후 피드백.
|
||||||
|
|
||||||
|
## 작업 내용
|
||||||
|
### 1. 모든 Mapper XML 쿼리 검증
|
||||||
|
- EXPLAIN ANALYZE 기반 실행계획 확인
|
||||||
|
- N+1, 풀스캔, 잘못된 인덱스 사용 진단
|
||||||
|
|
||||||
|
### 2. 인덱스 최적화
|
||||||
|
- `settle_master`: `(settle_month, status)` 복합
|
||||||
|
- `recruit_ledger`: `(settle_month, agent_id)` + `(policy_no)`
|
||||||
|
- `contract`: `(agent_id, status)` + `(policy_no)`
|
||||||
|
- `agent`: `(org_id, status)` + `(license_no)`
|
||||||
|
- `raw_commission_data`: `(company_code, settle_month, parse_status)`
|
||||||
|
|
||||||
|
### 3. 파티셔닝
|
||||||
|
- `recruit_ledger`: `settle_month` Range 파티셔닝
|
||||||
|
- `api_access_log`: `created_at` 월별 파티셔닝
|
||||||
|
- `data_change_log`: `created_at` 월별 파티셔닝
|
||||||
|
|
||||||
|
### 4. 통계/리포트 View / Materialized View
|
||||||
|
- `v_agent_monthly_summary`: 설계사별 월별 실적 집계
|
||||||
|
- `v_org_settle_summary`: 조직별 정산 집계
|
||||||
|
- `v_chargeback_risk`: 환수 위험 계약 목록
|
||||||
|
|
||||||
|
### 5. PostgreSQL 설정 권장값
|
||||||
|
- `postgresql.conf` 권장 (shared_buffers, work_mem, maintenance_work_mem 등)
|
||||||
|
|
||||||
|
### 6. 대량 배치 쿼리 튜닝
|
||||||
|
- 배치 INSERT 시 `COPY` 명령 활용 검토
|
||||||
|
- batch-engineer에 적용 제안
|
||||||
|
|
||||||
|
### 7. 마이그레이션 작성
|
||||||
|
- `V13__인덱스최적화.sql`
|
||||||
|
- `V14__파티셔닝.sql`
|
||||||
|
- `V15__뷰.sql`
|
||||||
|
|
||||||
|
### 8. 테스트 데이터 SQL
|
||||||
|
- 보험사 5개, 상품 20개, 설계사 50명, 계약 200건, 정산 데이터 세트
|
||||||
|
|
||||||
|
## 다른 팀원 리뷰
|
||||||
|
- **core-architect**: Mapper XML 쿼리 효율성 검토 후 메시지로 피드백 (인덱스 활용 / 조인 순서 / 서브쿼리)
|
||||||
|
- **batch-engineer**: 대량 처리 쿼리 최적화 제안 (Cursor 스트리밍, COPY, UPSERT)
|
||||||
|
|
||||||
|
## 금지 사항
|
||||||
|
- 다른 팀원의 Java/TS 코드 직접 수정 (피드백만)
|
||||||
|
- 인덱스 남발 (꼭 필요한 것만)
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
name: frontend-developer
|
||||||
|
description: ga-frontend 담당. React 18 + Vite + TS + Ant Design 5 + AG Grid + React Query + Zustand. 대시보드/조직/계약/수수료규정/데이터수신/원장/정산/지급/리포트 화면 구현.
|
||||||
|
tools: Read, Write, Edit, Glob, Grep, Bash, PowerShell
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
당신은 GA 수수료 정산 솔루션의 **frontend-developer**입니다.
|
||||||
|
|
||||||
|
## 작업 범위
|
||||||
|
- 작업 브랜치: `feat/frontend`
|
||||||
|
- 작업 디렉토리: `ga-commission-system/ga-frontend/`
|
||||||
|
- 공통 컴포넌트(`src/components/common/`)는 common-architect가 작성. 사용만 함.
|
||||||
|
|
||||||
|
## 기술 스택
|
||||||
|
Vite + React 18 + TypeScript + Ant Design 5.x + AG Grid Community + React Query + Zustand + dayjs + recharts
|
||||||
|
|
||||||
|
## 화면 목록
|
||||||
|
1. **대시보드** `/` — 정산요약 카드 4개, 배치현황, 대사오류알림, 월별추이(recharts)
|
||||||
|
2. **조직관리** — 조직트리(antd Tree) + 설계사목록(DataTable) + 상세(탭)
|
||||||
|
3. **계약관리** — 계약목록(DataTable) + 상세모달
|
||||||
|
4. **수수료규정** — 4개탭, 버전이력, AG Grid 셀 편집으로 수수료율 수정
|
||||||
|
5. **데이터수신** — 파일업로드(Dragger), 필드매핑편집, 코드매핑, 에러목록
|
||||||
|
6. **원장관리** — **AG Grid 사용** (DataGrid) — 모집/유지/예외 탭, 대량 데이터
|
||||||
|
7. **정산관리** — **AG Grid 사용** — 배치실행+진행폴링, 결과(설계사별/조직별), 상세, 대사
|
||||||
|
8. **지급관리** — DataTable — 목록, 생성, 이체파일, 완료/실패
|
||||||
|
9. **리포트** — recharts 차트 + DataTable
|
||||||
|
10. **시스템관리** — common-architect가 만든 화면 사용
|
||||||
|
|
||||||
|
## AG Grid 사용 기준
|
||||||
|
- 100건 이상 데이터 → AG Grid
|
||||||
|
- 셀 편집 필요 → AG Grid
|
||||||
|
- 합계행 필요 → AG Grid
|
||||||
|
- 단순 CRUD 목록 → Ant Design DataTable
|
||||||
|
|
||||||
|
## 폴더 구조
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── api/ # request.ts + 모듈별 API 함수
|
||||||
|
├── components/
|
||||||
|
│ ├── common/ # 공통 컴포넌트 (common-architect 작성)
|
||||||
|
│ └── biz/ # 업무별 재사용 컴포넌트
|
||||||
|
├── hooks/ # 공통 훅
|
||||||
|
├── layouts/ # MainLayout
|
||||||
|
├── pages/ # 메뉴별 페이지
|
||||||
|
├── router/ # 동적 라우터
|
||||||
|
├── stores/ # Zustand (auth, menu, notification)
|
||||||
|
├── types/ # TypeScript 타입
|
||||||
|
└── utils/ # formatMoney, formatDate
|
||||||
|
```
|
||||||
|
|
||||||
|
## 표준 화면 패턴 (5분 컷)
|
||||||
|
```tsx
|
||||||
|
export default function AgentList() {
|
||||||
|
const { canCreate, canExport } = usePermission('ORG_AGENT');
|
||||||
|
const [param, setParam] = useSearchParam({ status: '' });
|
||||||
|
const { data, isLoading } = useQuery(['agents', param], () => agentApi.list(param));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SearchForm conditions={[
|
||||||
|
{ type: 'text', name: 'keyword', label: '설계사명' },
|
||||||
|
{ type: 'code', name: 'status', label: '상태', groupCode: 'AGENT_STATUS' },
|
||||||
|
]} onSearch={setParam} />
|
||||||
|
<PermissionButton permCode="CREATE" type="primary"
|
||||||
|
onClick={() => navigate('/org/agents/new')}>등록</PermissionButton>
|
||||||
|
<DataTable
|
||||||
|
loading={isLoading}
|
||||||
|
dataSource={data?.list}
|
||||||
|
pagination={{ total: data?.total, current: param.pageNum }}
|
||||||
|
columns={[...]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 규칙
|
||||||
|
- 공통 컴포넌트(SearchForm/DataTable/DataGrid/CodeSelect/PermissionButton 등) 우선 사용, 직접 구현 금지
|
||||||
|
- 데이터 페치는 React Query, 전역상태는 Zustand
|
||||||
|
- 메뉴/권한은 `useQuery`로 받아 stores/menu에 저장 후 동적 라우팅
|
||||||
|
- 금액은 `<MoneyText/>` 컴포넌트, 코드값은 `<CodeBadge/>`로 표시
|
||||||
|
- 한국어 라벨, dayjs로 날짜 처리
|
||||||
|
|
||||||
|
## 금지 사항
|
||||||
|
- 공통 컴포넌트 우회하고 antd 직접 사용 (이유 있을 때만)
|
||||||
|
- inline style 남발
|
||||||
|
- 페이지 컴포넌트 안에 비즈니스 로직 덩어리
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
name: infra-reviewer
|
||||||
|
description: Docker/nginx/문서/통합검증 담당. docker-compose, Dockerfile, nginx.conf, README, DEVELOPER_GUIDE 작성 + 모든 팀원 작업 후 통합 빌드/실행 검증. 마지막에 합류.
|
||||||
|
tools: Read, Write, Edit, Glob, Grep, Bash, PowerShell
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
당신은 GA 수수료 정산 솔루션의 **infra-reviewer**입니다.
|
||||||
|
|
||||||
|
## 작업 범위
|
||||||
|
- 작업 브랜치: `feat/infra`
|
||||||
|
- 작업 디렉토리: 프로젝트 루트 (`docker-compose.yml`, `Dockerfile`, `nginx.conf`, `README.md`, `DEVELOPER_GUIDE.md`)
|
||||||
|
|
||||||
|
## 작업 내용
|
||||||
|
### 1. Docker
|
||||||
|
- `docker-compose.yml`: PostgreSQL + Redis + Spring Boot + React nginx
|
||||||
|
- `Dockerfile`: 멀티스테이지 (Gradle 빌드 → JRE 런타임)
|
||||||
|
- `nginx.conf`: React SPA + `/api` → `backend:8080` 프록시
|
||||||
|
|
||||||
|
### 2. 문서
|
||||||
|
- `README.md`: 실행방법, API 목록, 아키텍처, 폴더 구조
|
||||||
|
- `DEVELOPER_GUIDE.md`: 신입 개발자 온보딩
|
||||||
|
1. 환경 설정 (JDK 17, Node 18, PostgreSQL, Redis, IDE)
|
||||||
|
2. 프로젝트 구조 설명
|
||||||
|
3. 새 화면 만들기 Step-by-Step (메뉴 등록 → VO → Mapper → Service → Controller → React 페이지)
|
||||||
|
4. 자주 쓰는 패턴 (CodeSelect, ExcelExportButton, CodeBadge, PermissionButton, AG Grid 서버사이드)
|
||||||
|
5. 코드 컨벤션 (VO 네이밍, Mapper XML, API URL kebab-case)
|
||||||
|
6. 트러블슈팅 FAQ (권한 에러 / 캐시 / MyBatis 매핑)
|
||||||
|
|
||||||
|
### 3. 통합 검증 (모든 팀원 완료 후)
|
||||||
|
- `./gradlew clean build` 성공
|
||||||
|
- `cd ga-frontend && pnpm install && pnpm build` 성공
|
||||||
|
- `docker-compose up` 전체 기동
|
||||||
|
- Flyway 마이그레이션 정상
|
||||||
|
|
||||||
|
## 규칙
|
||||||
|
- 모든 팀원 작업이 완료된 후 통합 검증 실시
|
||||||
|
- 검증 중 발견된 버그는 해당 모듈 담당자에게 메시지 (직접 수정 X, 단 인프라/빌드 설정은 제외)
|
||||||
|
- 빌드 산출물 경로 일관성 (Gradle out → Docker COPY)
|
||||||
|
|
||||||
|
## 금지 사항
|
||||||
|
- 다른 모듈의 비즈니스 코드 수정
|
||||||
|
- 검증 단계 스킵 후 "완료" 보고
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||||
|
"env": {
|
||||||
|
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
|
||||||
|
},
|
||||||
|
"teammateMode": "auto"
|
||||||
|
}
|
||||||
+5
-2
@@ -36,8 +36,11 @@ dist/
|
|||||||
out/
|
out/
|
||||||
target/
|
target/
|
||||||
|
|
||||||
# Claude Code 개인 설정 (Agent Teams 등)
|
# Claude Code: 팀 공용 파일만 커밋, 개인 설정/로컬 데이터는 제외
|
||||||
.claude/
|
.claude/*
|
||||||
|
!.claude/settings.json
|
||||||
|
!.claude/agents/
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
# vite/tsc 빌드 산출물
|
# vite/tsc 빌드 산출물
|
||||||
ga-frontend/tsconfig.tsbuildinfo
|
ga-frontend/tsconfig.tsbuildinfo
|
||||||
|
|||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
# Handoff Note — 2026-05-12
|
||||||
|
|
||||||
|
다음 세션에서 이어가기 위한 핸드오프 문서. Agent Teams 기반 자동 체인으로 진행 중인 작업의 현재 상태와 잔여 작업, 재개 절차를 기록한다.
|
||||||
|
|
||||||
|
## 1. 핸드오프 시점 상태
|
||||||
|
|
||||||
|
- **마지막 커밋**: P1-3-c (ChargebackCalculator + ChargebackGradeService + Controller)
|
||||||
|
- **빌드 상태**: `./gradlew :ga-common:compileJava :ga-core:compileJava :ga-api:compileJava` 모두 BUILD SUCCESSFUL
|
||||||
|
- **이번 세션 누적 커밋 수**: 26개 (Agent Teams 설정 1 + 코드 품질 7 + 도메인 P0 9 + 도메인 P1 11)
|
||||||
|
- **변경 사항 없음** (working tree clean)
|
||||||
|
|
||||||
|
## 2. 완료된 작업
|
||||||
|
|
||||||
|
### 2-1. 코드 품질 자동 체인 (7 커밋)
|
||||||
|
초기 ga-api 5명 리뷰 결과를 바탕으로 P0 이슈 일괄 해결.
|
||||||
|
|
||||||
|
| 단계 | 커밋 | 내용 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `5e58a54` | OrganizationSaveReq + 상태 Enum 4종 (Agent/Contract/Settle/Approve) |
|
||||||
|
| 2 | `a8f0b6e` | Controller→Service 분리 5개 + 매직 스트링 Enum 교체 + @DataChangeLog 17곳 |
|
||||||
|
| 3a | `693275e` | UserStatus + ExceptionLedgerStatus Enum |
|
||||||
|
| 3b | `af3c6b9` | Map→SaveReq DTO 4종 + 입력 검증 + 잔여 Enum 적용 |
|
||||||
|
| 4 | `498d0d4` | LookupCache 동시성 + UploadService/TransferFile 트랜잭션 분해 + readOnly 일괄 |
|
||||||
|
| 5a | `4b211ec` | AgentOrgHistoryResp + MapStruct mapper 3종 + AgentMapper.xml 조인 |
|
||||||
|
| 5b | `b16e678` | MapStruct 적용 + Resp 적용 + PaymentService/UploadTemplateService 신설 + PageHelper 통일 |
|
||||||
|
|
||||||
|
### 2-2. 도메인 P0 — 세무·공제·명세서 (9 커밋)
|
||||||
|
|
||||||
|
| 도메인 | a (DB) | b (core) | c (api) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| P0-1 세무 | `ad5d62e` V23 | `92f3217` | `f8c7d84` TaxCalculator |
|
||||||
|
| P0-2 공제 | `0e73eb6` V24 | `a3ba7fa` | `b8f33f1` DeductionService |
|
||||||
|
| P0-3 명세서 | `930335c` V25 | `4f5a2f0` | `f5efcaa` ExcelStatementGenerator |
|
||||||
|
|
||||||
|
핵심 산출물:
|
||||||
|
- 표준 세율 system_config 외부화 (3.3%/8.8%/10%/10%)
|
||||||
|
- `agent`에 tax_type/business_no/vat_type 컬럼
|
||||||
|
- `payment`에 income_tax_amount/local_tax_amount/vat_amount/net_amount
|
||||||
|
- `agent_deduction` (월별 차감 마스터) + `payment_deduction_detail` (이체 명세)
|
||||||
|
- `commission_statement` (명세서 발급 마스터, agent+settle_month+statement_type UNIQUE)
|
||||||
|
- TaxCalculator pure 도메인 + ExcelStatementGenerator (PDF는 향후)
|
||||||
|
|
||||||
|
### 2-3. 도메인 P1 — 1200%룰·분급·차등환수 (11 커밋)
|
||||||
|
|
||||||
|
| 도메인 | a (DB) | b (core) | c (api) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| P1-1 1200%룰 | `6a6ba9f` V26 | `1109253` | `bba4a33` RegulatoryLimitChecker |
|
||||||
|
| P1-2 분급 | `434e778` V27 | `5029bc5` | (P1-2-c 커밋) InstallmentPlanGenerator |
|
||||||
|
| P1-3 차등환수 | `cc5d0e5` V28 | `44dbbf3` | (P1-3-c 커밋) ChargebackCalculator |
|
||||||
|
|
||||||
|
**P1 보강 (병렬 트랙)**:
|
||||||
|
- `1aa31de` RecruitLedger/MaintainLedger sumByContract 메서드 추가
|
||||||
|
- `f6142a1` RegulatoryLimitChecker TODO 해소 (실제 누적값 사용)
|
||||||
|
|
||||||
|
핵심 산출물:
|
||||||
|
- `regulatory_limit` 마스터 (1200%/35%/7년 한도, effective_from/to 이력 추적)
|
||||||
|
- `installment_ratio` + `installment_plan` (분급 7년 비율 35/25/15/10/7/5/3)
|
||||||
|
- `chargeback_grade` (5단계: 1~12개월 100% → 85개월~ 0%)
|
||||||
|
- RegulatoryLimitChecker / InstallmentPlanGenerator / ChargebackCalculator pure 도메인
|
||||||
|
|
||||||
|
### 2-4. 병렬화 적용 (P1 후반부)
|
||||||
|
초반 직렬 a→b→c 패턴에서 후반에 도메인 간 sub-step 병렬로 전환.
|
||||||
|
- P1-2-c(api) + P1-3-a(dba) + P1 보강(core) 3명 동시 활성화
|
||||||
|
- P1-3-b(core) + P1-1-c TODO 교체(api) 2명 동시
|
||||||
|
- 모듈이 다른 사람끼리 묶어 머지 충돌 없음
|
||||||
|
|
||||||
|
## 3. 잔여 작업
|
||||||
|
|
||||||
|
### 3-1. P1 정산 통합 (보류 권장)
|
||||||
|
- ga-batch의 8 Step 정산 잡에 TaxCalculator/DeductionService/RegulatoryLimitChecker/InstallmentService/ChargebackCalculator 통합
|
||||||
|
- batch-engineer 작업
|
||||||
|
- 운영 비즈니스 룰 의존이 커 **인프라만 완성하고 통합은 사용자 결정 후** 진행 권장
|
||||||
|
- 보류 가능 — 현 시점에 인프라/CRUD/API는 모두 동작
|
||||||
|
|
||||||
|
### 3-2. 도메인 P2 — 8개 항목 (Wave 방식)
|
||||||
|
| # | 도메인 | 마이그레이션 | 핵심 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| P2-1 | 마감 (Period Close) | V29 | period_close 테이블 + 마감 후 수정 차단 AOP |
|
||||||
|
| P2-2 | 정정 (Correction) | V30 | settle_correction + 원본 참조 + 정정 사유 |
|
||||||
|
| P2-3 | 이의 신청 (Dispute) | V31 | commission_dispute + OPEN/IN_REVIEW/APPROVED/REJECTED |
|
||||||
|
| P2-4 | 보류 사유 (Hold Reasons) | V32 | settle_master.hold_reason_code + HOLD_REASON 코드그룹 |
|
||||||
|
| P2-5 | 시책·인센티브 | V33 | incentive_program + incentive_payment |
|
||||||
|
| P2-6 | 해촉 정산 (Termination) | V34 | termination_settlement (agent_org_history 연계) |
|
||||||
|
| P2-7 | 세금계산서 (Tax Invoice) | V35 | tax_invoice 발행 이력 (SUPPLY/PURCHASE) |
|
||||||
|
| P2-8 | 외부 회신 | V36 | carrier_reconciliation_out (보험사 회신 인터페이스) |
|
||||||
|
|
||||||
|
**Wave 디스패치 계획**:
|
||||||
|
- Wave 1 (dba): V29~V36 8개 마이그레이션 한 번에
|
||||||
|
- Wave 2 (core): 8개 도메인 VO/Mapper 한 번에
|
||||||
|
- Wave 3 (api): 8개 도메인 Service/Controller 한 번에
|
||||||
|
- 디스패치 24 → 3으로 축소
|
||||||
|
|
||||||
|
### 3-3. 도메인 P3 — KPI 대시보드
|
||||||
|
| 작업 | 담당 |
|
||||||
|
|---|---|
|
||||||
|
| V37 Materialized View 4종 (월별/조직별/유지율/누계) | dba-architect |
|
||||||
|
| KPI VO/Mapper | core-architect |
|
||||||
|
| KpiService + KpiController | api-developer |
|
||||||
|
| KPI 대시보드 화면 (AG Grid 합계행, 차트) | frontend-developer |
|
||||||
|
|
||||||
|
### 3-4. 마무리 통합
|
||||||
|
| 작업 | 담당 |
|
||||||
|
|---|---|
|
||||||
|
| ga-batch 8 Step에 도메인 로직 통합 (TaxCalculator/Deduction/Regulatory/Installment/Chargeback) | batch-engineer |
|
||||||
|
| 통합 테스트 | infra-reviewer |
|
||||||
|
| README/DEVELOPER_GUIDE 업데이트 | infra-reviewer |
|
||||||
|
|
||||||
|
## 4. 재개 절차 (다음 세션)
|
||||||
|
|
||||||
|
1. **Claude Code 시작 시 cwd 확인**: `C:\Users\kyu\Desktop\ga_pro` (현재 세션과 동일)
|
||||||
|
2. **Agent Teams 활성화 확인**: `.claude/settings.json` 이미 커밋되어 있음 (`418ff7c`)
|
||||||
|
3. **팀 재생성**: 새 세션은 팀이 없음. 다음 명령 흐름:
|
||||||
|
- PL 역할로 `TeamCreate(team_name="ga-pro-team", agent_type="pl", description="...")` 호출
|
||||||
|
- 7명 팀원 spawn (common/core/api/batch/frontend/dba/infra) — 첫 세션과 동일
|
||||||
|
4. **HANDOFF.md 읽기**: 본 문서 + 최근 git log로 컨텍스트 확보
|
||||||
|
5. **권장 시작 지점**: P2 Wave 1 디스패치 (dba-architect에게 V29~V36 8개 마이그레이션)
|
||||||
|
6. **사용자 결정 필요 항목**:
|
||||||
|
- P1 정산 통합 진행 여부 (Wave 작업 전 또는 P3 후)
|
||||||
|
- P2-1 마감 도메인의 마감 후 수정 차단 AOP 구현 방식 (전역 AOP vs Service-level 가드)
|
||||||
|
|
||||||
|
## 5. PL 가이드 (다음 세션 유의사항)
|
||||||
|
|
||||||
|
- **시간차 중복 메시지**: 팀원이 SendMessage로 같은 보고를 2~3번 보내는 경우 있음. 짧게 acknowledge만.
|
||||||
|
- **idle 알림**: 팀원이 작업 끝낼 때마다 idle 알림 보냄. 정상. 액션 필요 없음.
|
||||||
|
- **책임 경계**: dba=Flyway/SQL, core=VO/Mapper/Enum, api=Service/Controller. api-developer가 가끔 ga-core Mapper에 메서드 추가하는데 작은 변경이면 통과 가능, 큰 변경은 core-architect에 위임.
|
||||||
|
- **TODO 잔존**: api-developer가 부족한 ga-core 메서드 발견 시 TODO로 남기고 보고 → PL이 후속 보강 디스패치. 같은 단계에서 멈추지 말고 흐름 유지.
|
||||||
|
- **자동 정책**:
|
||||||
|
- 각 단계 완료 후 자동 커밋 (push 안 함)
|
||||||
|
- 빌드/테스트 실패 시 담당 팀원에 재지시
|
||||||
|
- 컴파일은 PL이 직접 검증, 테스트는 환경 문제(SqlSessionFactory)로 통과 안 되는 게 알려진 상태
|
||||||
|
|
||||||
|
## 6. 표준 가정 (도메인 작업 진행 시 채택한 값)
|
||||||
|
|
||||||
|
도메인 룰은 한국 보험업감독규정 표준 가정을 외부화 가능한 구조로 등록. 실제 회사 정책과 다르면 system_config / regulatory_limit / installment_ratio / chargeback_grade의 데이터만 수정하면 됨.
|
||||||
|
|
||||||
|
| 항목 | 표준값 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 사업소득 원천징수 | 3.3% | 한국 표준 |
|
||||||
|
| 기타소득 원천징수 | 8.8% | 한국 표준 |
|
||||||
|
| 지방소득세 | 원천세의 10% | 한국 표준 |
|
||||||
|
| 부가세 | 10% | 한국 표준 |
|
||||||
|
| 1200% 룰 총 한도 | 1200% | 보험업감독규정 제5-15조의5 |
|
||||||
|
| 1차년 한도 | 35% | 2024년 기준 |
|
||||||
|
| 분급 연한 | 7년 | 2024년 기준 |
|
||||||
|
| 분급 비율 | 35/25/15/10/7/5/3 (합 100%) | 가정값 |
|
||||||
|
| 차등 환수 | 1~12: 100%, 13~24: 50%, 25~36: 25%, 37~84: 10%, 85~: 0% | 가정값 |
|
||||||
|
|
||||||
|
가정값과 실제가 다르면 운영 데이터 수정으로 해결 가능. 코드 변경 불필요.
|
||||||
@@ -8,8 +8,6 @@ import jakarta.validation.Valid;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Tag(name = "인증")
|
@Tag(name = "인증")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/auth")
|
@RequestMapping("/api/auth")
|
||||||
@@ -24,8 +22,8 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/refresh")
|
@PostMapping("/refresh")
|
||||||
public ApiResponse<LoginResp> refresh(@RequestBody Map<String, String> body) {
|
public ApiResponse<LoginResp> refresh(@Valid @RequestBody RefreshTokenReq req) {
|
||||||
return ApiResponse.ok(authService.refresh(body.get("refreshToken")));
|
return ApiResponse.ok(authService.refresh(req.getRefreshToken()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/me")
|
@GetMapping("/me")
|
||||||
@@ -34,8 +32,8 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/password")
|
@PostMapping("/password")
|
||||||
public ApiResponse<Void> changePassword(@RequestBody Map<String, String> body) {
|
public ApiResponse<Void> changePassword(@Valid @RequestBody ChangePasswordReq req) {
|
||||||
authService.changePassword(body.get("oldPassword"), body.get("newPassword"));
|
authService.changePassword(req.getOldPassword(), req.getNewPassword());
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.ga.common.system.SystemLogMapper;
|
|||||||
import com.ga.common.util.SecurityUtil;
|
import com.ga.common.util.SecurityUtil;
|
||||||
import com.ga.core.mapper.user.UserMapper;
|
import com.ga.core.mapper.user.UserMapper;
|
||||||
import com.ga.core.vo.user.UserResp;
|
import com.ga.core.vo.user.UserResp;
|
||||||
|
import com.ga.core.vo.user.UserStatus;
|
||||||
import com.ga.core.vo.user.UserVO;
|
import com.ga.core.vo.user.UserVO;
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -38,18 +39,18 @@ public class AuthService {
|
|||||||
saveLoginLog(null, req.getLoginId(), "FAIL", "USER_NOT_FOUND", ipAddress, userAgent);
|
saveLoginLog(null, req.getLoginId(), "FAIL", "USER_NOT_FOUND", ipAddress, userAgent);
|
||||||
throw new BizException(ErrorCode.LOGIN_FAIL);
|
throw new BizException(ErrorCode.LOGIN_FAIL);
|
||||||
}
|
}
|
||||||
if ("LOCKED".equals(user.getStatus())) {
|
if (UserStatus.LOCKED.name().equals(user.getStatus())) {
|
||||||
saveLoginLog(user.getUserId(), req.getLoginId(), "FAIL", "ACCOUNT_LOCKED", ipAddress, userAgent);
|
saveLoginLog(user.getUserId(), req.getLoginId(), "FAIL", "ACCOUNT_LOCKED", ipAddress, userAgent);
|
||||||
throw new BizException(ErrorCode.ACCOUNT_LOCKED);
|
throw new BizException(ErrorCode.ACCOUNT_LOCKED);
|
||||||
}
|
}
|
||||||
if ("RETIRED".equals(user.getStatus())) {
|
if (UserStatus.RETIRED.name().equals(user.getStatus())) {
|
||||||
throw new BizException(ErrorCode.ACCOUNT_RETIRED);
|
throw new BizException(ErrorCode.ACCOUNT_RETIRED);
|
||||||
}
|
}
|
||||||
if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) {
|
if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) {
|
||||||
userMapper.incrementFailCount(user.getUserId());
|
userMapper.incrementFailCount(user.getUserId());
|
||||||
int newFail = (user.getFailCount() == null ? 0 : user.getFailCount()) + 1;
|
int newFail = (user.getFailCount() == null ? 0 : user.getFailCount()) + 1;
|
||||||
if (newFail >= failLimit) {
|
if (newFail >= failLimit) {
|
||||||
userMapper.updateStatus(user.getUserId(), "LOCKED");
|
userMapper.updateStatus(user.getUserId(), UserStatus.LOCKED.name());
|
||||||
}
|
}
|
||||||
saveLoginLog(user.getUserId(), req.getLoginId(), "FAIL", "BAD_PASSWORD", ipAddress, userAgent);
|
saveLoginLog(user.getUserId(), req.getLoginId(), "FAIL", "BAD_PASSWORD", ipAddress, userAgent);
|
||||||
throw new BizException(ErrorCode.LOGIN_FAIL);
|
throw new BizException(ErrorCode.LOGIN_FAIL);
|
||||||
@@ -80,7 +81,7 @@ public class AuthService {
|
|||||||
}
|
}
|
||||||
Long userId = claims.get("uid", Long.class);
|
Long userId = claims.get("uid", Long.class);
|
||||||
UserVO user = userMapper.selectById(userId);
|
UserVO user = userMapper.selectById(userId);
|
||||||
if (user == null || !"ACTIVE".equals(user.getStatus())) {
|
if (user == null || !UserStatus.ACTIVE.name().equals(user.getStatus())) {
|
||||||
throw new BizException(ErrorCode.UNAUTHORIZED);
|
throw new BizException(ErrorCode.UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
List<String> roles = userMapper.selectRoleCodes(userId);
|
List<String> roles = userMapper.selectRoleCodes(userId);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ga.api.auth;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class ChangePasswordReq {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String oldPassword;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String newPassword;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ga.api.auth;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class RefreshTokenReq {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String refreshToken;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ga.api.controller.chargeback;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ChargebackCalculateReq {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private LocalDate contractDate;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private LocalDate terminationDate;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Positive
|
||||||
|
private BigDecimal originalCommission;
|
||||||
|
|
||||||
|
private String productCategory;
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.ga.api.controller.chargeback;
|
||||||
|
|
||||||
|
import com.ga.api.service.chargeback.ChargebackCalculator;
|
||||||
|
import com.ga.api.service.chargeback.ChargebackGradeService;
|
||||||
|
import com.ga.api.service.chargeback.ChargebackResult;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.chargeback.ChargebackGradeResp;
|
||||||
|
import com.ga.core.vo.chargeback.ChargebackGradeSaveReq;
|
||||||
|
import com.ga.core.vo.chargeback.ChargebackGradeSearchParam;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Tag(name = "환수 등급 관리")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/chargeback-grades")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChargebackGradeController {
|
||||||
|
|
||||||
|
private final ChargebackGradeService gradeService;
|
||||||
|
private final ChargebackCalculator calculator;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<ChargebackGradeResp>> list(ChargebackGradeSearchParam param) {
|
||||||
|
return ApiResponse.ok(gradeService.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{gradeId}")
|
||||||
|
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "READ")
|
||||||
|
public ApiResponse<ChargebackGradeResp> detail(@PathVariable Long gradeId) {
|
||||||
|
return ApiResponse.ok(gradeService.detail(gradeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "CHARGEBACK_GRADE", table = "chargeback_grade")
|
||||||
|
public ApiResponse<Void> create(@Valid @RequestBody ChargebackGradeSaveReq req) {
|
||||||
|
gradeService.create(req);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{gradeId}")
|
||||||
|
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "CHARGEBACK_GRADE", table = "chargeback_grade")
|
||||||
|
public ApiResponse<Void> update(@PathVariable Long gradeId,
|
||||||
|
@Valid @RequestBody ChargebackGradeSaveReq req) {
|
||||||
|
gradeService.update(gradeId, req);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{gradeId}/deactivate")
|
||||||
|
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "CHARGEBACK_GRADE", table = "chargeback_grade")
|
||||||
|
public ApiResponse<Void> deactivate(@PathVariable Long gradeId) {
|
||||||
|
gradeService.deactivate(gradeId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/calculate")
|
||||||
|
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "READ")
|
||||||
|
public ApiResponse<ChargebackResult> calculate(@Valid @RequestBody ChargebackCalculateReq req) {
|
||||||
|
ChargebackResult result = calculator.calculate(
|
||||||
|
req.getContractDate(),
|
||||||
|
req.getTerminationDate(),
|
||||||
|
req.getOriginalCommission(),
|
||||||
|
req.getProductCategory()
|
||||||
|
);
|
||||||
|
return ApiResponse.ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.ga.api.controller.deduction;
|
||||||
|
|
||||||
|
import com.ga.api.service.deduction.DeductionService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.settle.AgentDeductionResp;
|
||||||
|
import com.ga.core.vo.settle.AgentDeductionSaveReq;
|
||||||
|
import com.ga.core.vo.settle.AgentDeductionSearchParam;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Tag(name = "공제 관리")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/deductions")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeductionController {
|
||||||
|
|
||||||
|
private final DeductionService deductionService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "DEDUCTION", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<AgentDeductionResp>> list(@ModelAttribute AgentDeductionSearchParam param) {
|
||||||
|
return ApiResponse.ok(deductionService.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{deductionId}")
|
||||||
|
@RequirePermission(menu = "DEDUCTION", perm = "READ")
|
||||||
|
public ApiResponse<AgentDeductionResp> detail(@PathVariable Long deductionId) {
|
||||||
|
return ApiResponse.ok(deductionService.detail(deductionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@RequirePermission(menu = "DEDUCTION", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "DEDUCTION", table = "agent_deduction")
|
||||||
|
public ApiResponse<Long> create(@Valid @RequestBody AgentDeductionSaveReq req) {
|
||||||
|
return ApiResponse.ok(deductionService.create(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{deductionId}")
|
||||||
|
@RequirePermission(menu = "DEDUCTION", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "DEDUCTION", table = "agent_deduction")
|
||||||
|
public ApiResponse<Void> update(@PathVariable Long deductionId,
|
||||||
|
@Valid @RequestBody AgentDeductionSaveReq req) {
|
||||||
|
deductionService.update(deductionId, req);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{deductionId}/cancel")
|
||||||
|
@RequirePermission(menu = "DEDUCTION", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "DEDUCTION", table = "agent_deduction")
|
||||||
|
public ApiResponse<Void> cancel(@PathVariable Long deductionId) {
|
||||||
|
deductionService.cancel(deductionId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ga.api.controller.installment;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GeneratePlanReq {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long contractId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Positive
|
||||||
|
private BigDecimal totalCommission;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private LocalDate contractDate;
|
||||||
|
|
||||||
|
private String productCategory;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.ga.api.controller.installment;
|
||||||
|
|
||||||
|
import com.ga.api.service.installment.InstallmentService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.installment.InstallmentPlanResp;
|
||||||
|
import com.ga.core.vo.installment.InstallmentPlanSearchParam;
|
||||||
|
import com.ga.core.vo.installment.InstallmentPlanVO;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Tag(name = "분급 계획 관리")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/installment-plans")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InstallmentController {
|
||||||
|
|
||||||
|
private final InstallmentService installmentService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "INSTALLMENT", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<InstallmentPlanResp>> list(InstallmentPlanSearchParam param) {
|
||||||
|
return ApiResponse.ok(installmentService.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{planId}")
|
||||||
|
@RequirePermission(menu = "INSTALLMENT", perm = "READ")
|
||||||
|
public ApiResponse<InstallmentPlanResp> detail(@PathVariable Long planId) {
|
||||||
|
return ApiResponse.ok(installmentService.detail(planId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/generate")
|
||||||
|
@RequirePermission(menu = "INSTALLMENT", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "INSTALLMENT", table = "installment_plan")
|
||||||
|
public ApiResponse<Void> generate(@Valid @RequestBody GeneratePlanReq req) {
|
||||||
|
installmentService.createPlan(
|
||||||
|
req.getContractId(),
|
||||||
|
req.getTotalCommission(),
|
||||||
|
req.getContractDate(),
|
||||||
|
req.getProductCategory()
|
||||||
|
);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{planId}/pay")
|
||||||
|
@RequirePermission(menu = "INSTALLMENT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "INSTALLMENT", table = "installment_plan")
|
||||||
|
public ApiResponse<Void> pay(@PathVariable Long planId,
|
||||||
|
@Valid @RequestBody PayPlanReq req) {
|
||||||
|
installmentService.payPlan(planId, req.getPaymentId(), req.getPaidAmount());
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{planId}/defer")
|
||||||
|
@RequirePermission(menu = "INSTALLMENT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "INSTALLMENT", table = "installment_plan")
|
||||||
|
public ApiResponse<Void> defer(@PathVariable Long planId) {
|
||||||
|
installmentService.deferPlan(planId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{planId}/cancel")
|
||||||
|
@RequirePermission(menu = "INSTALLMENT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "INSTALLMENT", table = "installment_plan")
|
||||||
|
public ApiResponse<Void> cancel(@PathVariable Long planId) {
|
||||||
|
installmentService.cancelPlan(planId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/scheduled")
|
||||||
|
@RequirePermission(menu = "INSTALLMENT", perm = "READ")
|
||||||
|
public ApiResponse<List<InstallmentPlanVO>> scheduled(@RequestParam String settleMonth) {
|
||||||
|
return ApiResponse.ok(installmentService.selectScheduledForMonth(settleMonth));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ga.api.controller.installment;
|
||||||
|
|
||||||
|
import com.ga.api.service.installment.InstallmentService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.installment.InstallmentRatioResp;
|
||||||
|
import com.ga.core.vo.installment.InstallmentRatioSaveReq;
|
||||||
|
import com.ga.core.vo.installment.InstallmentRatioSearchParam;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Tag(name = "분급 비율 관리")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/installment-ratios")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InstallmentRatioController {
|
||||||
|
|
||||||
|
private final InstallmentService installmentService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "INSTALLMENT", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<InstallmentRatioResp>> list(InstallmentRatioSearchParam param) {
|
||||||
|
return ApiResponse.ok(installmentService.listRatios(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@RequirePermission(menu = "INSTALLMENT", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "INSTALLMENT", table = "installment_ratio")
|
||||||
|
public ApiResponse<Void> create(@Valid @RequestBody InstallmentRatioSaveReq req) {
|
||||||
|
installmentService.createRatio(req);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ga.api.controller.installment;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PayPlanReq {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long paymentId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Positive
|
||||||
|
private BigDecimal paidAmount;
|
||||||
|
}
|
||||||
@@ -6,8 +6,6 @@ import com.ga.common.annotation.RequirePermission;
|
|||||||
import com.ga.common.excel.ExcelService;
|
import com.ga.common.excel.ExcelService;
|
||||||
import com.ga.common.model.ApiResponse;
|
import com.ga.common.model.ApiResponse;
|
||||||
import com.ga.common.model.PageResponse;
|
import com.ga.common.model.PageResponse;
|
||||||
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
|
|
||||||
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
|
|
||||||
import com.ga.core.vo.ledger.*;
|
import com.ga.core.vo.ledger.*;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@@ -24,8 +22,6 @@ import java.util.Map;
|
|||||||
public class LedgerController {
|
public class LedgerController {
|
||||||
|
|
||||||
private final LedgerService service;
|
private final LedgerService service;
|
||||||
private final RecruitLedgerMapper recruitMapper;
|
|
||||||
private final MaintainLedgerMapper maintainMapper;
|
|
||||||
private final ExcelService excelService;
|
private final ExcelService excelService;
|
||||||
|
|
||||||
// ===== 모집 =====
|
// ===== 모집 =====
|
||||||
@@ -45,7 +41,7 @@ public class LedgerController {
|
|||||||
@RequirePermission(menu = "LEDGER_RECRUIT", perm = "EXPORT")
|
@RequirePermission(menu = "LEDGER_RECRUIT", perm = "EXPORT")
|
||||||
public void exportRecruit(@ModelAttribute LedgerSearchParam param, HttpServletResponse res) {
|
public void exportRecruit(@ModelAttribute LedgerSearchParam param, HttpServletResponse res) {
|
||||||
excelService.exportLargeExcel("모집수수료원장", LedgerExcelVO.class,
|
excelService.exportLargeExcel("모집수수료원장", LedgerExcelVO.class,
|
||||||
() -> recruitMapper.selectCursorForExcel(param), res);
|
() -> service.exportRecruitCursor(param), res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 유지 =====
|
// ===== 유지 =====
|
||||||
@@ -65,7 +61,7 @@ public class LedgerController {
|
|||||||
@RequirePermission(menu = "LEDGER_MAINTAIN", perm = "EXPORT")
|
@RequirePermission(menu = "LEDGER_MAINTAIN", perm = "EXPORT")
|
||||||
public void exportMaintain(@ModelAttribute LedgerSearchParam param, HttpServletResponse res) {
|
public void exportMaintain(@ModelAttribute LedgerSearchParam param, HttpServletResponse res) {
|
||||||
excelService.exportLargeExcel("유지수수료원장", LedgerExcelVO.class,
|
excelService.exportLargeExcel("유지수수료원장", LedgerExcelVO.class,
|
||||||
() -> maintainMapper.selectCursorForExcel(param), res);
|
() -> service.exportMaintainCursor(param), res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 예외 =====
|
// ===== 예외 =====
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import com.ga.common.annotation.RequirePermission;
|
|||||||
import com.ga.common.excel.ExcelService;
|
import com.ga.common.excel.ExcelService;
|
||||||
import com.ga.common.model.ApiResponse;
|
import com.ga.common.model.ApiResponse;
|
||||||
import com.ga.common.model.PageResponse;
|
import com.ga.common.model.PageResponse;
|
||||||
import com.ga.core.mapper.org.AgentMapper;
|
|
||||||
import com.ga.core.vo.org.*;
|
import com.ga.core.vo.org.*;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@@ -24,7 +23,6 @@ import java.util.Map;
|
|||||||
public class AgentController {
|
public class AgentController {
|
||||||
|
|
||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
private final AgentMapper agentMapper;
|
|
||||||
private final ExcelService excelService;
|
private final ExcelService excelService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -64,7 +62,7 @@ public class AgentController {
|
|||||||
|
|
||||||
@GetMapping("/{agentId}/history")
|
@GetMapping("/{agentId}/history")
|
||||||
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
|
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
|
||||||
public ApiResponse<List<AgentOrgHistoryVO>> history(@PathVariable Long agentId) {
|
public ApiResponse<List<AgentOrgHistoryResp>> history(@PathVariable Long agentId) {
|
||||||
return ApiResponse.ok(agentService.history(agentId));
|
return ApiResponse.ok(agentService.history(agentId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +70,6 @@ public class AgentController {
|
|||||||
@RequirePermission(menu = "ORG_AGENT", perm = "EXPORT")
|
@RequirePermission(menu = "ORG_AGENT", perm = "EXPORT")
|
||||||
public void export(@ModelAttribute AgentSearchParam param, HttpServletResponse response) {
|
public void export(@ModelAttribute AgentSearchParam param, HttpServletResponse response) {
|
||||||
excelService.exportLargeExcel("설계사목록", AgentExcelVO.class,
|
excelService.exportLargeExcel("설계사목록", AgentExcelVO.class,
|
||||||
() -> agentMapper.selectCursorForExcel(param), response);
|
() -> agentService.exportCursor(param), response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import com.ga.common.annotation.DataChangeLog;
|
|||||||
import com.ga.common.annotation.RequirePermission;
|
import com.ga.common.annotation.RequirePermission;
|
||||||
import com.ga.common.model.ApiResponse;
|
import com.ga.common.model.ApiResponse;
|
||||||
import com.ga.core.vo.org.OrganizationResp;
|
import com.ga.core.vo.org.OrganizationResp;
|
||||||
|
import com.ga.core.vo.org.OrganizationSaveReq;
|
||||||
import com.ga.core.vo.org.OrganizationVO;
|
import com.ga.core.vo.org.OrganizationVO;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -43,15 +45,15 @@ public class OrganizationController {
|
|||||||
@PostMapping
|
@PostMapping
|
||||||
@RequirePermission(menu = "ORG_TREE", perm = "CREATE")
|
@RequirePermission(menu = "ORG_TREE", perm = "CREATE")
|
||||||
@DataChangeLog(menu = "ORG_TREE", table = "organization")
|
@DataChangeLog(menu = "ORG_TREE", table = "organization")
|
||||||
public ApiResponse<Long> create(@RequestBody OrganizationVO vo) {
|
public ApiResponse<Long> create(@Valid @RequestBody OrganizationSaveReq req) {
|
||||||
return ApiResponse.ok(service.create(vo));
|
return ApiResponse.ok(service.create(req));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{orgId}")
|
@PutMapping("/{orgId}")
|
||||||
@RequirePermission(menu = "ORG_TREE", perm = "UPDATE")
|
@RequirePermission(menu = "ORG_TREE", perm = "UPDATE")
|
||||||
@DataChangeLog(menu = "ORG_TREE", table = "organization")
|
@DataChangeLog(menu = "ORG_TREE", table = "organization")
|
||||||
public ApiResponse<Void> update(@PathVariable Long orgId, @RequestBody OrganizationVO vo) {
|
public ApiResponse<Void> update(@PathVariable Long orgId, @Valid @RequestBody OrganizationSaveReq req) {
|
||||||
service.update(orgId, vo);
|
service.update(orgId, req);
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.ga.api.controller.product;
|
package com.ga.api.controller.product;
|
||||||
|
|
||||||
|
import com.ga.api.service.product.InsuranceCompanyService;
|
||||||
import com.ga.common.annotation.DataChangeLog;
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
import com.ga.common.annotation.RequirePermission;
|
import com.ga.common.annotation.RequirePermission;
|
||||||
import com.ga.common.model.ApiResponse;
|
import com.ga.common.model.ApiResponse;
|
||||||
import com.ga.core.mapper.product.InsuranceCompanyMapper;
|
|
||||||
import com.ga.core.vo.product.InsuranceCompanyVO;
|
import com.ga.core.vo.product.InsuranceCompanyVO;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -17,34 +17,32 @@ import java.util.List;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CompanyController {
|
public class CompanyController {
|
||||||
|
|
||||||
private final InsuranceCompanyMapper mapper;
|
private final InsuranceCompanyService companyService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@RequirePermission(menu = "COMPANY", perm = "READ")
|
@RequirePermission(menu = "COMPANY", perm = "READ")
|
||||||
public ApiResponse<List<InsuranceCompanyVO>> list(@RequestParam(required = false, defaultValue = "false") boolean activeOnly) {
|
public ApiResponse<List<InsuranceCompanyVO>> list(@RequestParam(required = false, defaultValue = "false") boolean activeOnly) {
|
||||||
return ApiResponse.ok(activeOnly ? mapper.selectActive() : mapper.selectAll());
|
return ApiResponse.ok(companyService.list(activeOnly));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{companyId}")
|
@GetMapping("/{companyId}")
|
||||||
@RequirePermission(menu = "COMPANY", perm = "READ")
|
@RequirePermission(menu = "COMPANY", perm = "READ")
|
||||||
public ApiResponse<InsuranceCompanyVO> detail(@PathVariable Long companyId) {
|
public ApiResponse<InsuranceCompanyVO> detail(@PathVariable Long companyId) {
|
||||||
return ApiResponse.ok(mapper.selectById(companyId));
|
return ApiResponse.ok(companyService.detail(companyId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@RequirePermission(menu = "COMPANY", perm = "CREATE")
|
@RequirePermission(menu = "COMPANY", perm = "CREATE")
|
||||||
@DataChangeLog(menu = "COMPANY", table = "insurance_company")
|
@DataChangeLog(menu = "COMPANY", table = "insurance_company")
|
||||||
public ApiResponse<Long> create(@RequestBody InsuranceCompanyVO vo) {
|
public ApiResponse<Long> create(@RequestBody InsuranceCompanyVO vo) {
|
||||||
mapper.insert(vo);
|
return ApiResponse.ok(companyService.create(vo));
|
||||||
return ApiResponse.ok(vo.getCompanyId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{companyId}")
|
@PutMapping("/{companyId}")
|
||||||
@RequirePermission(menu = "COMPANY", perm = "UPDATE")
|
@RequirePermission(menu = "COMPANY", perm = "UPDATE")
|
||||||
@DataChangeLog(menu = "COMPANY", table = "insurance_company")
|
@DataChangeLog(menu = "COMPANY", table = "insurance_company")
|
||||||
public ApiResponse<Void> update(@PathVariable Long companyId, @RequestBody InsuranceCompanyVO vo) {
|
public ApiResponse<Void> update(@PathVariable Long companyId, @RequestBody InsuranceCompanyVO vo) {
|
||||||
vo.setCompanyId(companyId);
|
companyService.update(companyId, vo);
|
||||||
mapper.update(vo);
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package com.ga.api.controller.product;
|
package com.ga.api.controller.product;
|
||||||
|
|
||||||
|
import com.ga.api.service.product.ProductService;
|
||||||
import com.ga.common.annotation.DataChangeLog;
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
import com.ga.common.annotation.RequirePermission;
|
import com.ga.common.annotation.RequirePermission;
|
||||||
import com.ga.common.exception.BizException;
|
|
||||||
import com.ga.common.exception.ErrorCode;
|
|
||||||
import com.ga.common.model.ApiResponse;
|
import com.ga.common.model.ApiResponse;
|
||||||
import com.ga.core.mapper.product.ProductMapper;
|
|
||||||
import com.ga.core.vo.product.ProductResp;
|
import com.ga.core.vo.product.ProductResp;
|
||||||
import com.ga.core.vo.product.ProductVO;
|
import com.ga.core.vo.product.ProductVO;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -20,7 +18,7 @@ import java.util.List;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ProductController {
|
public class ProductController {
|
||||||
|
|
||||||
private final ProductMapper mapper;
|
private final ProductService productService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@RequirePermission(menu = "PRODUCT", perm = "READ")
|
@RequirePermission(menu = "PRODUCT", perm = "READ")
|
||||||
@@ -28,41 +26,35 @@ public class ProductController {
|
|||||||
@RequestParam(required = false) String insuranceType,
|
@RequestParam(required = false) String insuranceType,
|
||||||
@RequestParam(required = false) String keyword,
|
@RequestParam(required = false) String keyword,
|
||||||
@RequestParam(required = false) String isActive) {
|
@RequestParam(required = false) String isActive) {
|
||||||
return ApiResponse.ok(mapper.selectList(companyId, insuranceType, keyword, isActive));
|
return ApiResponse.ok(productService.list(companyId, insuranceType, keyword, isActive));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{productId}")
|
@GetMapping("/{productId}")
|
||||||
@RequirePermission(menu = "PRODUCT", perm = "READ")
|
@RequirePermission(menu = "PRODUCT", perm = "READ")
|
||||||
public ApiResponse<ProductResp> detail(@PathVariable Long productId) {
|
public ApiResponse<ProductResp> detail(@PathVariable Long productId) {
|
||||||
ProductResp r = mapper.selectDetailById(productId);
|
return ApiResponse.ok(productService.detail(productId));
|
||||||
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
|
|
||||||
return ApiResponse.ok(r);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@RequirePermission(menu = "PRODUCT", perm = "CREATE")
|
@RequirePermission(menu = "PRODUCT", perm = "CREATE")
|
||||||
@DataChangeLog(menu = "PRODUCT", table = "product")
|
@DataChangeLog(menu = "PRODUCT", table = "product")
|
||||||
public ApiResponse<Long> create(@RequestBody ProductVO vo) {
|
public ApiResponse<Long> create(@RequestBody ProductVO vo) {
|
||||||
if (mapper.existsByCode(vo.getCompanyId(), vo.getProductCode()) > 0) {
|
return ApiResponse.ok(productService.create(vo));
|
||||||
throw new BizException(ErrorCode.DUPLICATE_DATA);
|
|
||||||
}
|
|
||||||
mapper.insert(vo);
|
|
||||||
return ApiResponse.ok(vo.getProductId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{productId}")
|
@PutMapping("/{productId}")
|
||||||
@RequirePermission(menu = "PRODUCT", perm = "UPDATE")
|
@RequirePermission(menu = "PRODUCT", perm = "UPDATE")
|
||||||
@DataChangeLog(menu = "PRODUCT", table = "product")
|
@DataChangeLog(menu = "PRODUCT", table = "product")
|
||||||
public ApiResponse<Void> update(@PathVariable Long productId, @RequestBody ProductVO vo) {
|
public ApiResponse<Void> update(@PathVariable Long productId, @RequestBody ProductVO vo) {
|
||||||
vo.setProductId(productId);
|
productService.update(productId, vo);
|
||||||
mapper.update(vo);
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{productId}")
|
@DeleteMapping("/{productId}")
|
||||||
@RequirePermission(menu = "PRODUCT", perm = "DELETE")
|
@RequirePermission(menu = "PRODUCT", perm = "DELETE")
|
||||||
|
@DataChangeLog(menu = "PRODUCT", table = "product")
|
||||||
public ApiResponse<Void> delete(@PathVariable Long productId) {
|
public ApiResponse<Void> delete(@PathVariable Long productId) {
|
||||||
mapper.deleteById(productId);
|
productService.delete(productId);
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package com.ga.api.controller.receive;
|
package com.ga.api.controller.receive;
|
||||||
|
|
||||||
|
import com.ga.api.service.receive.ReceiveService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
import com.ga.common.annotation.RequirePermission;
|
import com.ga.common.annotation.RequirePermission;
|
||||||
import com.ga.common.model.ApiResponse;
|
import com.ga.common.model.ApiResponse;
|
||||||
import com.ga.core.mapper.receive.ReceiveMapper;
|
|
||||||
import com.ga.core.vo.receive.*;
|
import com.ga.core.vo.receive.*;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Tag(name = "데이터 수신")
|
@Tag(name = "데이터 수신")
|
||||||
@RestController
|
@RestController
|
||||||
@@ -17,21 +18,20 @@ import java.util.Map;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ReceiveController {
|
public class ReceiveController {
|
||||||
|
|
||||||
private final ReceiveMapper mapper;
|
private final ReceiveService receiveService;
|
||||||
|
|
||||||
// 보험사 프로파일
|
// 보험사 프로파일
|
||||||
@GetMapping("/profiles")
|
@GetMapping("/profiles")
|
||||||
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "READ")
|
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "READ")
|
||||||
public ApiResponse<List<CompanyProfileVO>> profiles() {
|
public ApiResponse<List<CompanyProfileVO>> profiles() {
|
||||||
return ApiResponse.ok(mapper.selectAllProfiles());
|
return ApiResponse.ok(receiveService.listProfiles());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/profiles/{companyCode}")
|
@PutMapping("/profiles/{companyCode}")
|
||||||
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "UPDATE")
|
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "RECEIVE_MAPPING", table = "company_profile")
|
||||||
public ApiResponse<Void> updateProfile(@PathVariable String companyCode, @RequestBody CompanyProfileVO vo) {
|
public ApiResponse<Void> updateProfile(@PathVariable String companyCode, @RequestBody CompanyProfileVO vo) {
|
||||||
vo.setCompanyCode(companyCode);
|
receiveService.saveProfile(companyCode, vo);
|
||||||
if (mapper.selectProfile(companyCode) == null) mapper.insertProfile(vo);
|
|
||||||
else mapper.updateProfile(vo);
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,29 +40,29 @@ public class ReceiveController {
|
|||||||
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "READ")
|
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "READ")
|
||||||
public ApiResponse<List<CompanyFieldMappingVO>> fields(@PathVariable String companyCode,
|
public ApiResponse<List<CompanyFieldMappingVO>> fields(@PathVariable String companyCode,
|
||||||
@RequestParam(required = false) String targetTable) {
|
@RequestParam(required = false) String targetTable) {
|
||||||
return ApiResponse.ok(mapper.selectFieldMappings(companyCode, targetTable));
|
return ApiResponse.ok(receiveService.listFields(companyCode, targetTable));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/mapping/{companyCode}/fields")
|
@PostMapping("/mapping/{companyCode}/fields")
|
||||||
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "CREATE")
|
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "RECEIVE_MAPPING", table = "company_field_mapping")
|
||||||
public ApiResponse<Long> createField(@PathVariable String companyCode, @RequestBody CompanyFieldMappingVO vo) {
|
public ApiResponse<Long> createField(@PathVariable String companyCode, @RequestBody CompanyFieldMappingVO vo) {
|
||||||
vo.setCompanyCode(companyCode);
|
return ApiResponse.ok(receiveService.createField(companyCode, vo));
|
||||||
mapper.insertFieldMapping(vo);
|
|
||||||
return ApiResponse.ok(vo.getMappingId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/mapping/fields/{mappingId}")
|
@PutMapping("/mapping/fields/{mappingId}")
|
||||||
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "UPDATE")
|
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "RECEIVE_MAPPING", table = "company_field_mapping")
|
||||||
public ApiResponse<Void> updateField(@PathVariable Long mappingId, @RequestBody CompanyFieldMappingVO vo) {
|
public ApiResponse<Void> updateField(@PathVariable Long mappingId, @RequestBody CompanyFieldMappingVO vo) {
|
||||||
vo.setMappingId(mappingId);
|
receiveService.updateField(mappingId, vo);
|
||||||
mapper.updateFieldMapping(vo);
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/mapping/fields/{mappingId}")
|
@DeleteMapping("/mapping/fields/{mappingId}")
|
||||||
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "DELETE")
|
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "DELETE")
|
||||||
|
@DataChangeLog(menu = "RECEIVE_MAPPING", table = "company_field_mapping")
|
||||||
public ApiResponse<Void> deleteField(@PathVariable Long mappingId) {
|
public ApiResponse<Void> deleteField(@PathVariable Long mappingId) {
|
||||||
mapper.deleteFieldMapping(mappingId);
|
receiveService.deleteField(mappingId);
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,15 +71,14 @@ public class ReceiveController {
|
|||||||
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "READ")
|
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "READ")
|
||||||
public ApiResponse<List<CodeMappingVO>> codes(@PathVariable String companyCode,
|
public ApiResponse<List<CodeMappingVO>> codes(@PathVariable String companyCode,
|
||||||
@RequestParam(required = false) String codeType) {
|
@RequestParam(required = false) String codeType) {
|
||||||
return ApiResponse.ok(mapper.selectCodeMappings(companyCode, codeType));
|
return ApiResponse.ok(receiveService.listCodes(companyCode, codeType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/mapping/{companyCode}/codes")
|
@PostMapping("/mapping/{companyCode}/codes")
|
||||||
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "CREATE")
|
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "RECEIVE_MAPPING", table = "code_mapping")
|
||||||
public ApiResponse<Long> createCode(@PathVariable String companyCode, @RequestBody CodeMappingVO vo) {
|
public ApiResponse<Long> createCode(@PathVariable String companyCode, @RequestBody CodeMappingVO vo) {
|
||||||
vo.setCompanyCode(companyCode);
|
return ApiResponse.ok(receiveService.createCode(companyCode, vo));
|
||||||
mapper.insertCodeMapping(vo);
|
|
||||||
return ApiResponse.ok(vo.getCodeId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 원본 데이터 + 에러
|
// 원본 데이터 + 에러
|
||||||
@@ -88,23 +87,21 @@ public class ReceiveController {
|
|||||||
public ApiResponse<List<RawCommissionDataVO>> rawList(@RequestParam(required = false) String companyCode,
|
public ApiResponse<List<RawCommissionDataVO>> rawList(@RequestParam(required = false) String companyCode,
|
||||||
@RequestParam(required = false) String settleMonth,
|
@RequestParam(required = false) String settleMonth,
|
||||||
@RequestParam(required = false) String parseStatus) {
|
@RequestParam(required = false) String parseStatus) {
|
||||||
return ApiResponse.ok(mapper.selectRawList(companyCode, settleMonth, parseStatus));
|
return ApiResponse.ok(receiveService.listRaw(companyCode, settleMonth, parseStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/errors")
|
@GetMapping("/errors")
|
||||||
@RequirePermission(menu = "RECEIVE_DATA", perm = "READ")
|
@RequirePermission(menu = "RECEIVE_DATA", perm = "READ")
|
||||||
public ApiResponse<List<ParseErrorLogVO>> errors(@RequestParam(required = false) String companyCode,
|
public ApiResponse<List<ParseErrorLogVO>> errors(@RequestParam(required = false) String companyCode,
|
||||||
@RequestParam(required = false) String resolveStatus) {
|
@RequestParam(required = false) String resolveStatus) {
|
||||||
return ApiResponse.ok(mapper.selectErrors(companyCode, resolveStatus));
|
return ApiResponse.ok(receiveService.listErrors(companyCode, resolveStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/errors/{errorId}/resolve")
|
@PutMapping("/errors/{errorId}/resolve")
|
||||||
@RequirePermission(menu = "RECEIVE_DATA", perm = "UPDATE")
|
@RequirePermission(menu = "RECEIVE_DATA", perm = "UPDATE")
|
||||||
public ApiResponse<Void> resolveError(@PathVariable Long errorId, @RequestBody Map<String, String> body) {
|
@DataChangeLog(menu = "RECEIVE_DATA", table = "parse_error_log")
|
||||||
mapper.resolveError(errorId,
|
public ApiResponse<Void> resolveError(@PathVariable Long errorId, @Valid @RequestBody ResolveErrorReq req) {
|
||||||
body.getOrDefault("status", "RESOLVED"),
|
receiveService.resolveError(errorId, req.getStatus(), req.getNote());
|
||||||
body.get("note"),
|
|
||||||
com.ga.common.util.SecurityUtil.getCurrentUserId());
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ga.api.controller.receive;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class ResolveErrorReq {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
private String note;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.ga.api.controller.regulatory;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ContractLimitCheckReq {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long contractId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Positive
|
||||||
|
private BigDecimal additionalAmount;
|
||||||
|
|
||||||
|
/** 기준일 (null이면 오늘) */
|
||||||
|
private LocalDate baseDate;
|
||||||
|
|
||||||
|
/** TOTAL_RATIO | FIRST_YEAR_RATIO */
|
||||||
|
@NotNull
|
||||||
|
private String limitType;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.ga.api.controller.regulatory;
|
||||||
|
|
||||||
|
import com.ga.api.service.regulatory.RegulatoryLimitChecker;
|
||||||
|
import com.ga.api.service.regulatory.RegulatoryLimitService;
|
||||||
|
import com.ga.api.service.regulatory.ViolationResult;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.regulatory.RegulatoryLimitResp;
|
||||||
|
import com.ga.core.vo.regulatory.RegulatoryLimitSaveReq;
|
||||||
|
import com.ga.core.vo.regulatory.RegulatoryLimitSearchParam;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Tag(name = "규제 한도 관리")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/regulatory-limits")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RegulatoryLimitController {
|
||||||
|
|
||||||
|
private final RegulatoryLimitService limitService;
|
||||||
|
private final RegulatoryLimitChecker limitChecker;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<RegulatoryLimitResp>> list(RegulatoryLimitSearchParam param) {
|
||||||
|
return ApiResponse.ok(limitService.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{limitId}")
|
||||||
|
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "READ")
|
||||||
|
public ApiResponse<RegulatoryLimitResp> detail(@PathVariable Long limitId) {
|
||||||
|
return ApiResponse.ok(limitService.detail(limitId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "REGULATORY_LIMIT", table = "regulatory_limit")
|
||||||
|
public ApiResponse<Void> create(@Valid @RequestBody RegulatoryLimitSaveReq req) {
|
||||||
|
limitService.create(req);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{limitId}")
|
||||||
|
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "REGULATORY_LIMIT", table = "regulatory_limit")
|
||||||
|
public ApiResponse<Void> update(@PathVariable Long limitId,
|
||||||
|
@Valid @RequestBody RegulatoryLimitSaveReq req) {
|
||||||
|
limitService.update(limitId, req);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{limitId}/deactivate")
|
||||||
|
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "REGULATORY_LIMIT", table = "regulatory_limit")
|
||||||
|
public ApiResponse<Void> deactivate(@PathVariable Long limitId) {
|
||||||
|
limitService.deactivate(limitId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/check-contract")
|
||||||
|
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "READ")
|
||||||
|
public ApiResponse<ViolationResult> checkContract(@Valid @RequestBody ContractLimitCheckReq req) {
|
||||||
|
LocalDate baseDate = req.getBaseDate() != null ? req.getBaseDate() : LocalDate.now();
|
||||||
|
ViolationResult result;
|
||||||
|
if ("FIRST_YEAR_RATIO".equals(req.getLimitType())) {
|
||||||
|
result = limitChecker.checkFirstYear(req.getContractId(), req.getAdditionalAmount(), baseDate);
|
||||||
|
} else {
|
||||||
|
result = limitChecker.checkContractTotal(req.getContractId(), req.getAdditionalAmount(), baseDate);
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.ga.api.controller.rule;
|
package com.ga.api.controller.rule;
|
||||||
|
|
||||||
|
import com.ga.api.service.rule.RuleService;
|
||||||
import com.ga.common.annotation.DataChangeLog;
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
import com.ga.common.annotation.RequirePermission;
|
import com.ga.common.annotation.RequirePermission;
|
||||||
import com.ga.common.model.ApiResponse;
|
import com.ga.common.model.ApiResponse;
|
||||||
import com.ga.core.mapper.rule.RuleMapper;
|
|
||||||
import com.ga.core.vo.rule.*;
|
import com.ga.core.vo.rule.*;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -17,37 +17,36 @@ import java.util.List;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class RuleController {
|
public class RuleController {
|
||||||
|
|
||||||
private final RuleMapper mapper;
|
private final RuleService ruleService;
|
||||||
|
|
||||||
// ========== commission_rate ==========
|
// ========== commission_rate ==========
|
||||||
@GetMapping("/commission-rates")
|
@GetMapping("/commission-rates")
|
||||||
@RequirePermission(menu = "RULE_COMMISSION", perm = "READ")
|
@RequirePermission(menu = "RULE_COMMISSION", perm = "READ")
|
||||||
public ApiResponse<List<CommissionRateVO>> listCommissionRates(@RequestParam(required = false) Long productId,
|
public ApiResponse<List<CommissionRateVO>> listCommissionRates(@RequestParam(required = false) Long productId,
|
||||||
@RequestParam(required = false) Integer year) {
|
@RequestParam(required = false) Integer year) {
|
||||||
return ApiResponse.ok(mapper.selectCommissionRates(productId, year));
|
return ApiResponse.ok(ruleService.listCommissionRates(productId, year));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/commission-rates")
|
@PostMapping("/commission-rates")
|
||||||
@RequirePermission(menu = "RULE_COMMISSION", perm = "CREATE")
|
@RequirePermission(menu = "RULE_COMMISSION", perm = "CREATE")
|
||||||
@DataChangeLog(menu = "RULE_COMMISSION", table = "commission_rate")
|
@DataChangeLog(menu = "RULE_COMMISSION", table = "commission_rate")
|
||||||
public ApiResponse<Long> createCommissionRate(@RequestBody CommissionRateVO vo) {
|
public ApiResponse<Long> createCommissionRate(@RequestBody CommissionRateVO vo) {
|
||||||
mapper.insertCommissionRate(vo);
|
return ApiResponse.ok(ruleService.createCommissionRate(vo));
|
||||||
return ApiResponse.ok(vo.getRateId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/commission-rates/{rateId}")
|
@PutMapping("/commission-rates/{rateId}")
|
||||||
@RequirePermission(menu = "RULE_COMMISSION", perm = "UPDATE")
|
@RequirePermission(menu = "RULE_COMMISSION", perm = "UPDATE")
|
||||||
@DataChangeLog(menu = "RULE_COMMISSION", table = "commission_rate")
|
@DataChangeLog(menu = "RULE_COMMISSION", table = "commission_rate")
|
||||||
public ApiResponse<Void> updateCommissionRate(@PathVariable Long rateId, @RequestBody CommissionRateVO vo) {
|
public ApiResponse<Void> updateCommissionRate(@PathVariable Long rateId, @RequestBody CommissionRateVO vo) {
|
||||||
vo.setRateId(rateId);
|
ruleService.updateCommissionRate(rateId, vo);
|
||||||
mapper.updateCommissionRate(vo);
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/commission-rates/{rateId}")
|
@DeleteMapping("/commission-rates/{rateId}")
|
||||||
@RequirePermission(menu = "RULE_COMMISSION", perm = "DELETE")
|
@RequirePermission(menu = "RULE_COMMISSION", perm = "DELETE")
|
||||||
|
@DataChangeLog(menu = "RULE_COMMISSION", table = "commission_rate")
|
||||||
public ApiResponse<Void> deleteCommissionRate(@PathVariable Long rateId) {
|
public ApiResponse<Void> deleteCommissionRate(@PathVariable Long rateId) {
|
||||||
mapper.deleteCommissionRate(rateId);
|
ruleService.deleteCommissionRate(rateId);
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,30 +55,29 @@ public class RuleController {
|
|||||||
@RequirePermission(menu = "RULE_PAYOUT", perm = "READ")
|
@RequirePermission(menu = "RULE_PAYOUT", perm = "READ")
|
||||||
public ApiResponse<List<PayoutRuleVO>> listPayoutRules(@RequestParam(required = false) Long gradeId,
|
public ApiResponse<List<PayoutRuleVO>> listPayoutRules(@RequestParam(required = false) Long gradeId,
|
||||||
@RequestParam(required = false) String insuranceType) {
|
@RequestParam(required = false) String insuranceType) {
|
||||||
return ApiResponse.ok(mapper.selectPayoutRules(gradeId, insuranceType));
|
return ApiResponse.ok(ruleService.listPayoutRules(gradeId, insuranceType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/payout")
|
@PostMapping("/payout")
|
||||||
@RequirePermission(menu = "RULE_PAYOUT", perm = "CREATE")
|
@RequirePermission(menu = "RULE_PAYOUT", perm = "CREATE")
|
||||||
@DataChangeLog(menu = "RULE_PAYOUT", table = "payout_rule")
|
@DataChangeLog(menu = "RULE_PAYOUT", table = "payout_rule")
|
||||||
public ApiResponse<Long> createPayoutRule(@RequestBody PayoutRuleVO vo) {
|
public ApiResponse<Long> createPayoutRule(@RequestBody PayoutRuleVO vo) {
|
||||||
mapper.insertPayoutRule(vo);
|
return ApiResponse.ok(ruleService.createPayoutRule(vo));
|
||||||
return ApiResponse.ok(vo.getRuleId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/payout/{ruleId}")
|
@PutMapping("/payout/{ruleId}")
|
||||||
@RequirePermission(menu = "RULE_PAYOUT", perm = "UPDATE")
|
@RequirePermission(menu = "RULE_PAYOUT", perm = "UPDATE")
|
||||||
@DataChangeLog(menu = "RULE_PAYOUT", table = "payout_rule")
|
@DataChangeLog(menu = "RULE_PAYOUT", table = "payout_rule")
|
||||||
public ApiResponse<Void> updatePayoutRule(@PathVariable Long ruleId, @RequestBody PayoutRuleVO vo) {
|
public ApiResponse<Void> updatePayoutRule(@PathVariable Long ruleId, @RequestBody PayoutRuleVO vo) {
|
||||||
vo.setRuleId(ruleId);
|
ruleService.updatePayoutRule(ruleId, vo);
|
||||||
mapper.updatePayoutRule(vo);
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/payout/{ruleId}")
|
@DeleteMapping("/payout/{ruleId}")
|
||||||
@RequirePermission(menu = "RULE_PAYOUT", perm = "DELETE")
|
@RequirePermission(menu = "RULE_PAYOUT", perm = "DELETE")
|
||||||
|
@DataChangeLog(menu = "RULE_PAYOUT", table = "payout_rule")
|
||||||
public ApiResponse<Void> deletePayoutRule(@PathVariable Long ruleId) {
|
public ApiResponse<Void> deletePayoutRule(@PathVariable Long ruleId) {
|
||||||
mapper.deletePayoutRule(ruleId);
|
ruleService.deletePayoutRule(ruleId);
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,28 +85,29 @@ public class RuleController {
|
|||||||
@GetMapping("/override")
|
@GetMapping("/override")
|
||||||
@RequirePermission(menu = "RULE_OVERRIDE", perm = "READ")
|
@RequirePermission(menu = "RULE_OVERRIDE", perm = "READ")
|
||||||
public ApiResponse<List<OverrideRuleVO>> listOverrideRules() {
|
public ApiResponse<List<OverrideRuleVO>> listOverrideRules() {
|
||||||
return ApiResponse.ok(mapper.selectOverrideRules());
|
return ApiResponse.ok(ruleService.listOverrideRules());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/override")
|
@PostMapping("/override")
|
||||||
@RequirePermission(menu = "RULE_OVERRIDE", perm = "CREATE")
|
@RequirePermission(menu = "RULE_OVERRIDE", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "RULE_OVERRIDE", table = "override_rule")
|
||||||
public ApiResponse<Long> createOverrideRule(@RequestBody OverrideRuleVO vo) {
|
public ApiResponse<Long> createOverrideRule(@RequestBody OverrideRuleVO vo) {
|
||||||
mapper.insertOverrideRule(vo);
|
return ApiResponse.ok(ruleService.createOverrideRule(vo));
|
||||||
return ApiResponse.ok(vo.getOverrideId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/override/{overrideId}")
|
@PutMapping("/override/{overrideId}")
|
||||||
@RequirePermission(menu = "RULE_OVERRIDE", perm = "UPDATE")
|
@RequirePermission(menu = "RULE_OVERRIDE", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "RULE_OVERRIDE", table = "override_rule")
|
||||||
public ApiResponse<Void> updateOverrideRule(@PathVariable Long overrideId, @RequestBody OverrideRuleVO vo) {
|
public ApiResponse<Void> updateOverrideRule(@PathVariable Long overrideId, @RequestBody OverrideRuleVO vo) {
|
||||||
vo.setOverrideId(overrideId);
|
ruleService.updateOverrideRule(overrideId, vo);
|
||||||
mapper.updateOverrideRule(vo);
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/override/{overrideId}")
|
@DeleteMapping("/override/{overrideId}")
|
||||||
@RequirePermission(menu = "RULE_OVERRIDE", perm = "DELETE")
|
@RequirePermission(menu = "RULE_OVERRIDE", perm = "DELETE")
|
||||||
|
@DataChangeLog(menu = "RULE_OVERRIDE", table = "override_rule")
|
||||||
public ApiResponse<Void> deleteOverrideRule(@PathVariable Long overrideId) {
|
public ApiResponse<Void> deleteOverrideRule(@PathVariable Long overrideId) {
|
||||||
mapper.deleteOverrideRule(overrideId);
|
ruleService.deleteOverrideRule(overrideId);
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,28 +115,29 @@ public class RuleController {
|
|||||||
@GetMapping("/chargeback")
|
@GetMapping("/chargeback")
|
||||||
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "READ")
|
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "READ")
|
||||||
public ApiResponse<List<ChargebackRuleVO>> listChargebackRules(@RequestParam(required = false) String companyCode) {
|
public ApiResponse<List<ChargebackRuleVO>> listChargebackRules(@RequestParam(required = false) String companyCode) {
|
||||||
return ApiResponse.ok(mapper.selectChargebackRules(companyCode));
|
return ApiResponse.ok(ruleService.listChargebackRules(companyCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/chargeback")
|
@PostMapping("/chargeback")
|
||||||
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "CREATE")
|
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "RULE_CHARGEBACK", table = "chargeback_rule")
|
||||||
public ApiResponse<Long> createChargebackRule(@RequestBody ChargebackRuleVO vo) {
|
public ApiResponse<Long> createChargebackRule(@RequestBody ChargebackRuleVO vo) {
|
||||||
mapper.insertChargebackRule(vo);
|
return ApiResponse.ok(ruleService.createChargebackRule(vo));
|
||||||
return ApiResponse.ok(vo.getCbRuleId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/chargeback/{cbRuleId}")
|
@PutMapping("/chargeback/{cbRuleId}")
|
||||||
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "UPDATE")
|
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "RULE_CHARGEBACK", table = "chargeback_rule")
|
||||||
public ApiResponse<Void> updateChargebackRule(@PathVariable Long cbRuleId, @RequestBody ChargebackRuleVO vo) {
|
public ApiResponse<Void> updateChargebackRule(@PathVariable Long cbRuleId, @RequestBody ChargebackRuleVO vo) {
|
||||||
vo.setCbRuleId(cbRuleId);
|
ruleService.updateChargebackRule(cbRuleId, vo);
|
||||||
mapper.updateChargebackRule(vo);
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/chargeback/{cbRuleId}")
|
@DeleteMapping("/chargeback/{cbRuleId}")
|
||||||
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "DELETE")
|
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "DELETE")
|
||||||
|
@DataChangeLog(menu = "RULE_CHARGEBACK", table = "chargeback_rule")
|
||||||
public ApiResponse<Void> deleteChargebackRule(@PathVariable Long cbRuleId) {
|
public ApiResponse<Void> deleteChargebackRule(@PathVariable Long cbRuleId) {
|
||||||
mapper.deleteChargebackRule(cbRuleId);
|
ruleService.deleteChargebackRule(cbRuleId);
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,22 +145,23 @@ public class RuleController {
|
|||||||
@GetMapping("/exception-codes")
|
@GetMapping("/exception-codes")
|
||||||
@RequirePermission(menu = "RULE_EXCEPTION", perm = "READ")
|
@RequirePermission(menu = "RULE_EXCEPTION", perm = "READ")
|
||||||
public ApiResponse<List<ExceptionTypeCodeVO>> listExceptionCodes() {
|
public ApiResponse<List<ExceptionTypeCodeVO>> listExceptionCodes() {
|
||||||
return ApiResponse.ok(mapper.selectExceptionCodes());
|
return ApiResponse.ok(ruleService.listExceptionCodes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/exception-codes")
|
@PostMapping("/exception-codes")
|
||||||
@RequirePermission(menu = "RULE_EXCEPTION", perm = "CREATE")
|
@RequirePermission(menu = "RULE_EXCEPTION", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "RULE_EXCEPTION", table = "exception_type_code")
|
||||||
public ApiResponse<Void> createExceptionCode(@RequestBody ExceptionTypeCodeVO vo) {
|
public ApiResponse<Void> createExceptionCode(@RequestBody ExceptionTypeCodeVO vo) {
|
||||||
mapper.insertExceptionCode(vo);
|
ruleService.createExceptionCode(vo);
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/exception-codes/{exceptionCode}")
|
@PutMapping("/exception-codes/{exceptionCode}")
|
||||||
@RequirePermission(menu = "RULE_EXCEPTION", perm = "UPDATE")
|
@RequirePermission(menu = "RULE_EXCEPTION", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "RULE_EXCEPTION", table = "exception_type_code")
|
||||||
public ApiResponse<Void> updateExceptionCode(@PathVariable String exceptionCode,
|
public ApiResponse<Void> updateExceptionCode(@PathVariable String exceptionCode,
|
||||||
@RequestBody ExceptionTypeCodeVO vo) {
|
@RequestBody ExceptionTypeCodeVO vo) {
|
||||||
vo.setExceptionCode(exceptionCode);
|
ruleService.updateExceptionCode(exceptionCode, vo);
|
||||||
mapper.updateExceptionCode(vo);
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,41 @@
|
|||||||
package com.ga.api.controller.settle;
|
package com.ga.api.controller.settle;
|
||||||
|
|
||||||
import com.ga.api.service.transfer.TransferFileService;
|
import com.ga.api.service.settle.PaymentService;
|
||||||
import com.ga.common.annotation.DataChangeLog;
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
import com.ga.common.annotation.RequirePermission;
|
import com.ga.common.annotation.RequirePermission;
|
||||||
import com.ga.common.model.ApiResponse;
|
import com.ga.common.model.ApiResponse;
|
||||||
import com.ga.common.model.PageResponse;
|
import com.ga.common.model.PageResponse;
|
||||||
import com.ga.core.mapper.settle.PaymentMapper;
|
|
||||||
import com.ga.core.vo.settle.PaymentResp;
|
import com.ga.core.vo.settle.PaymentResp;
|
||||||
import com.ga.core.vo.settle.SettleSearchParam;
|
import com.ga.core.vo.settle.SettleSearchParam;
|
||||||
import com.ga.core.vo.settle.TransferFileResp;
|
import com.ga.core.vo.settle.TransferFileResp;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Tag(name = "지급 관리")
|
@Tag(name = "지급 관리")
|
||||||
|
@Validated
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/payments")
|
@RequestMapping("/api/payments")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PaymentController {
|
public class PaymentController {
|
||||||
|
|
||||||
private final PaymentMapper mapper;
|
private final PaymentService paymentService;
|
||||||
private final TransferFileService transferFileService;
|
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@RequirePermission(menu = "PAYMENT", perm = "READ")
|
@RequirePermission(menu = "PAYMENT", perm = "READ")
|
||||||
public ApiResponse<PageResponse<PaymentResp>> list(@ModelAttribute SettleSearchParam param) {
|
public ApiResponse<PageResponse<PaymentResp>> list(@ModelAttribute SettleSearchParam param) {
|
||||||
param.startPage();
|
return ApiResponse.ok(paymentService.list(param));
|
||||||
return ApiResponse.ok(PageResponse.of(mapper.selectList(param)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{paymentId}/status")
|
@PutMapping("/{paymentId}/status")
|
||||||
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
|
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
|
||||||
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
||||||
public ApiResponse<Void> updateStatus(@PathVariable Long paymentId, @RequestBody Map<String, String> body) {
|
public ApiResponse<Void> updateStatus(@PathVariable Long paymentId, @Valid @RequestBody PaymentStatusUpdateReq req) {
|
||||||
mapper.updateStatus(paymentId, body.get("status"), body.get("failReason"));
|
paymentService.updateStatus(paymentId, req.getStatus(), req.getFailReason());
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,8 +44,48 @@ public class PaymentController {
|
|||||||
@RequirePermission(menu = "PAYMENT", perm = "EXPORT")
|
@RequirePermission(menu = "PAYMENT", perm = "EXPORT")
|
||||||
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
||||||
public ApiResponse<TransferFileResp> generateTransferFile(
|
public ApiResponse<TransferFileResp> generateTransferFile(
|
||||||
@RequestParam String settleMonth,
|
@Pattern(regexp = "\\d{6}", message = "settleMonth는 YYYYMM 형식이어야 합니다") @RequestParam String settleMonth,
|
||||||
@RequestParam String bankCode) {
|
@RequestParam String bankCode) {
|
||||||
return ApiResponse.ok(transferFileService.generate(settleMonth, bankCode));
|
return ApiResponse.ok(paymentService.generateTransferFile(settleMonth, bankCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "세금 단건 재계산")
|
||||||
|
@PostMapping("/{paymentId}/calculate-tax")
|
||||||
|
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
||||||
|
public ApiResponse<Void> calculateTax(@PathVariable Long paymentId) {
|
||||||
|
paymentService.calculateTaxes(paymentId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "세금 월별 일괄 재계산")
|
||||||
|
@PostMapping("/calculate-taxes")
|
||||||
|
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
||||||
|
public ApiResponse<Void> calculateTaxesForMonth(
|
||||||
|
@Pattern(regexp = "\\d{6}", message = "settleMonth는 YYYYMM 형식이어야 합니다") @RequestParam String settleMonth) {
|
||||||
|
paymentService.calculateTaxesForMonth(settleMonth);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "차감 단건 적용")
|
||||||
|
@PostMapping("/{paymentId}/apply-deductions")
|
||||||
|
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
||||||
|
public ApiResponse<Void> applyDeductions(
|
||||||
|
@PathVariable Long paymentId,
|
||||||
|
@Pattern(regexp = "\\d{6}", message = "settleMonth는 YYYYMM 형식이어야 합니다") @RequestParam String settleMonth) {
|
||||||
|
paymentService.applyDeductions(paymentId, settleMonth);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "차감 월별 일괄 적용")
|
||||||
|
@PostMapping("/apply-deductions")
|
||||||
|
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
||||||
|
public ApiResponse<Void> applyDeductionsForMonth(
|
||||||
|
@Pattern(regexp = "\\d{6}", message = "settleMonth는 YYYYMM 형식이어야 합니다") @RequestParam String settleMonth) {
|
||||||
|
paymentService.applyDeductionsForMonth(settleMonth);
|
||||||
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ga.api.controller.settle;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class PaymentStatusUpdateReq {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
private String failReason;
|
||||||
|
}
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
package com.ga.api.controller.statement;
|
||||||
|
|
||||||
|
import com.ga.api.service.statement.CommissionStatementService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.settle.CommissionStatementResp;
|
||||||
|
import com.ga.core.vo.settle.CommissionStatementSaveReq;
|
||||||
|
import com.ga.core.vo.settle.CommissionStatementSearchParam;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
@Tag(name = "수수료 명세서")
|
||||||
|
@Validated
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/statements")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CommissionStatementController {
|
||||||
|
|
||||||
|
private final CommissionStatementService statementService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "STATEMENT", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<CommissionStatementResp>> list(@ModelAttribute CommissionStatementSearchParam param) {
|
||||||
|
return ApiResponse.ok(statementService.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{statementId}")
|
||||||
|
@RequirePermission(menu = "STATEMENT", perm = "READ")
|
||||||
|
public ApiResponse<CommissionStatementResp> detail(@PathVariable Long statementId) {
|
||||||
|
return ApiResponse.ok(statementService.detail(statementId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "명세서 단건 발급")
|
||||||
|
@PostMapping("/generate")
|
||||||
|
@RequirePermission(menu = "STATEMENT", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "STATEMENT", table = "commission_statement")
|
||||||
|
public ApiResponse<Long> generate(@Valid @RequestBody CommissionStatementSaveReq req) {
|
||||||
|
return ApiResponse.ok(statementService.generate(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "명세서 월별 일괄 발급")
|
||||||
|
@PostMapping("/generate-for-month")
|
||||||
|
@RequirePermission(menu = "STATEMENT", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "STATEMENT", table = "commission_statement")
|
||||||
|
public ApiResponse<Void> generateForMonth(
|
||||||
|
@Pattern(regexp = "\\d{6}", message = "settleMonth는 YYYYMM 형식이어야 합니다") @RequestParam String settleMonth,
|
||||||
|
@RequestParam String statementType) {
|
||||||
|
statementService.generateForMonth(settleMonth, statementType);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "명세서 다운로드")
|
||||||
|
@GetMapping("/{statementId}/download")
|
||||||
|
@RequirePermission(menu = "STATEMENT", perm = "READ")
|
||||||
|
public void download(@PathVariable Long statementId, HttpServletResponse response) throws IOException {
|
||||||
|
byte[] content = statementService.download(statementId);
|
||||||
|
String encoded = URLEncoder.encode("수수료명세서_" + statementId + ".xlsx", StandardCharsets.UTF_8)
|
||||||
|
.replace("+", "%20");
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encoded);
|
||||||
|
response.setContentLength(content.length);
|
||||||
|
response.getOutputStream().write(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "명세서 취소")
|
||||||
|
@PutMapping("/{statementId}/cancel")
|
||||||
|
@RequirePermission(menu = "STATEMENT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "STATEMENT", table = "commission_statement")
|
||||||
|
public ApiResponse<Void> cancel(@PathVariable Long statementId) {
|
||||||
|
statementService.cancel(statementId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
package com.ga.api.controller.system;
|
package com.ga.api.controller.system;
|
||||||
|
|
||||||
|
import com.ga.api.service.system.RoleService;
|
||||||
import com.ga.common.annotation.DataChangeLog;
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
import com.ga.common.annotation.RequirePermission;
|
import com.ga.common.annotation.RequirePermission;
|
||||||
import com.ga.common.model.ApiResponse;
|
import com.ga.common.model.ApiResponse;
|
||||||
import com.ga.core.mapper.user.RoleMapper;
|
|
||||||
import com.ga.core.vo.user.RoleVO;
|
import com.ga.core.vo.user.RoleVO;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -19,63 +18,55 @@ import java.util.Map;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class RoleController {
|
public class RoleController {
|
||||||
|
|
||||||
private final RoleMapper mapper;
|
private final RoleService roleService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@RequirePermission(menu = "SYSTEM_ROLE", perm = "READ")
|
@RequirePermission(menu = "SYSTEM_ROLE", perm = "READ")
|
||||||
public ApiResponse<List<RoleVO>> list() {
|
public ApiResponse<List<RoleVO>> list() {
|
||||||
return ApiResponse.ok(mapper.selectAll());
|
return ApiResponse.ok(roleService.list());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{roleId}")
|
@GetMapping("/{roleId}")
|
||||||
@RequirePermission(menu = "SYSTEM_ROLE", perm = "READ")
|
@RequirePermission(menu = "SYSTEM_ROLE", perm = "READ")
|
||||||
public ApiResponse<RoleVO> detail(@PathVariable Long roleId) {
|
public ApiResponse<RoleVO> detail(@PathVariable Long roleId) {
|
||||||
return ApiResponse.ok(mapper.selectById(roleId));
|
return ApiResponse.ok(roleService.detail(roleId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{roleId}/permissions")
|
@GetMapping("/{roleId}/permissions")
|
||||||
@RequirePermission(menu = "SYSTEM_ROLE", perm = "READ")
|
@RequirePermission(menu = "SYSTEM_ROLE", perm = "READ")
|
||||||
public ApiResponse<List<Map<String, Object>>> permissions(@PathVariable Long roleId) {
|
public ApiResponse<List<Map<String, Object>>> permissions(@PathVariable Long roleId) {
|
||||||
return ApiResponse.ok(mapper.selectRolePermissions(roleId));
|
return ApiResponse.ok(roleService.permissions(roleId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@RequirePermission(menu = "SYSTEM_ROLE", perm = "CREATE")
|
@RequirePermission(menu = "SYSTEM_ROLE", perm = "CREATE")
|
||||||
@DataChangeLog(menu = "SYSTEM_ROLE", table = "role")
|
@DataChangeLog(menu = "SYSTEM_ROLE", table = "role")
|
||||||
public ApiResponse<Long> create(@RequestBody RoleVO vo) {
|
public ApiResponse<Long> create(@RequestBody RoleVO vo) {
|
||||||
mapper.insert(vo);
|
return ApiResponse.ok(roleService.create(vo));
|
||||||
return ApiResponse.ok(vo.getRoleId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{roleId}")
|
@PutMapping("/{roleId}")
|
||||||
@RequirePermission(menu = "SYSTEM_ROLE", perm = "UPDATE")
|
@RequirePermission(menu = "SYSTEM_ROLE", perm = "UPDATE")
|
||||||
@DataChangeLog(menu = "SYSTEM_ROLE", table = "role")
|
@DataChangeLog(menu = "SYSTEM_ROLE", table = "role")
|
||||||
public ApiResponse<Void> update(@PathVariable Long roleId, @RequestBody RoleVO vo) {
|
public ApiResponse<Void> update(@PathVariable Long roleId, @RequestBody RoleVO vo) {
|
||||||
vo.setRoleId(roleId);
|
roleService.update(roleId, vo);
|
||||||
mapper.update(vo);
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{roleId}")
|
@DeleteMapping("/{roleId}")
|
||||||
@RequirePermission(menu = "SYSTEM_ROLE", perm = "DELETE")
|
@RequirePermission(menu = "SYSTEM_ROLE", perm = "DELETE")
|
||||||
|
@DataChangeLog(menu = "SYSTEM_ROLE", table = "role")
|
||||||
public ApiResponse<Void> delete(@PathVariable Long roleId) {
|
public ApiResponse<Void> delete(@PathVariable Long roleId) {
|
||||||
mapper.deleteById(roleId);
|
roleService.delete(roleId);
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 권한 매트릭스 일괄 저장 */
|
|
||||||
@PutMapping("/{roleId}/permissions")
|
@PutMapping("/{roleId}/permissions")
|
||||||
@RequirePermission(menu = "SYSTEM_ROLE", perm = "UPDATE")
|
@RequirePermission(menu = "SYSTEM_ROLE", perm = "UPDATE")
|
||||||
@Transactional
|
@DataChangeLog(menu = "SYSTEM_ROLE", table = "role_permission")
|
||||||
public ApiResponse<Void> savePermissions(@PathVariable Long roleId,
|
public ApiResponse<Void> savePermissions(@PathVariable Long roleId,
|
||||||
@RequestBody List<Map<String, Object>> permissions) {
|
@RequestBody List<Map<String, Object>> permissions) {
|
||||||
mapper.deleteRolePermissions(roleId);
|
roleService.savePermissions(roleId, permissions);
|
||||||
for (Map<String, Object> p : permissions) {
|
|
||||||
Long menuId = ((Number) p.get("menuId")).longValue();
|
|
||||||
String permCode = (String) p.get("permCode");
|
|
||||||
String isGranted = (String) p.getOrDefault("isGranted", "Y");
|
|
||||||
mapper.insertRolePermission(roleId, menuId, permCode, isGranted);
|
|
||||||
}
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import com.ga.common.annotation.RequirePermission;
|
|||||||
import com.ga.common.model.ApiResponse;
|
import com.ga.common.model.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@@ -17,6 +19,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Tag(name = "엑셀 업로드")
|
@Tag(name = "엑셀 업로드")
|
||||||
|
@Validated
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UploadController {
|
public class UploadController {
|
||||||
@@ -41,7 +44,7 @@ public class UploadController {
|
|||||||
public ApiResponse<List<Map<String, Object>>> preview(
|
public ApiResponse<List<Map<String, Object>>> preview(
|
||||||
@PathVariable String templateCode,
|
@PathVariable String templateCode,
|
||||||
@RequestParam("file") MultipartFile file,
|
@RequestParam("file") MultipartFile file,
|
||||||
@RequestParam(value = "limit", defaultValue = "5") int limit) {
|
@Max(100) @RequestParam(value = "limit", defaultValue = "5") int limit) {
|
||||||
List<Map<String, Object>> rows = uploadService.preview(templateCode, file, limit);
|
List<Map<String, Object>> rows = uploadService.preview(templateCode, file, limit);
|
||||||
return ApiResponse.ok(rows);
|
return ApiResponse.ok(rows);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
package com.ga.api.controller.upload;
|
package com.ga.api.controller.upload;
|
||||||
|
|
||||||
|
import com.ga.api.service.upload.UploadTemplateService;
|
||||||
import com.ga.common.annotation.DataChangeLog;
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
import com.ga.common.annotation.RequirePermission;
|
import com.ga.common.annotation.RequirePermission;
|
||||||
import com.ga.common.exception.BizException;
|
|
||||||
import com.ga.common.exception.ErrorCode;
|
|
||||||
import com.ga.common.model.ApiResponse;
|
import com.ga.common.model.ApiResponse;
|
||||||
import com.ga.common.model.PageResponse;
|
import com.ga.common.model.PageResponse;
|
||||||
import com.ga.common.util.SecurityUtil;
|
|
||||||
import com.ga.core.mapper.upload.UploadColumnMapper;
|
|
||||||
import com.ga.core.mapper.upload.UploadHistoryMapper;
|
|
||||||
import com.ga.core.mapper.upload.UploadTemplateMapper;
|
|
||||||
import com.ga.core.vo.upload.*;
|
import com.ga.core.vo.upload.*;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -26,68 +20,37 @@ import java.util.List;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UploadTemplateController {
|
public class UploadTemplateController {
|
||||||
|
|
||||||
private final UploadTemplateMapper templateMapper;
|
private final UploadTemplateService templateService;
|
||||||
private final UploadColumnMapper columnMapper;
|
|
||||||
private final UploadHistoryMapper historyMapper;
|
|
||||||
|
|
||||||
@Operation(summary = "템플릿 목록 조회")
|
@Operation(summary = "템플릿 목록 조회")
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
|
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
|
||||||
public ApiResponse<PageResponse<UploadTemplateResp>> list(@ModelAttribute UploadTemplateSearchParam param) {
|
public ApiResponse<PageResponse<UploadTemplateResp>> list(@ModelAttribute UploadTemplateSearchParam param) {
|
||||||
param.startPage();
|
return ApiResponse.ok(templateService.list(param));
|
||||||
return ApiResponse.ok(PageResponse.of(templateMapper.selectList(param)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "템플릿 상세 조회 (컬럼 포함)")
|
@Operation(summary = "템플릿 상세 조회 (컬럼 포함)")
|
||||||
@GetMapping("/{templateId}")
|
@GetMapping("/{templateId}")
|
||||||
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
|
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
|
||||||
public ApiResponse<UploadTemplateResp> detail(@PathVariable Long templateId) {
|
public ApiResponse<UploadTemplateResp> detail(@PathVariable Long templateId) {
|
||||||
UploadTemplateResp resp = templateMapper.selectById(templateId);
|
return ApiResponse.ok(templateService.detail(templateId));
|
||||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
|
||||||
resp.setColumns(columnMapper.selectByTemplateId(templateId));
|
|
||||||
return ApiResponse.ok(resp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "템플릿 등록")
|
@Operation(summary = "템플릿 등록")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "CREATE")
|
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "CREATE")
|
||||||
@DataChangeLog(menu = "SYSTEM_UPLOAD_TEMPLATE", table = "upload_template")
|
@DataChangeLog(menu = "SYSTEM_UPLOAD_TEMPLATE", table = "upload_template")
|
||||||
@Transactional
|
|
||||||
public ApiResponse<Long> create(@Valid @RequestBody UploadTemplateSaveReq req) {
|
public ApiResponse<Long> create(@Valid @RequestBody UploadTemplateSaveReq req) {
|
||||||
if (templateMapper.existsByCode(req.getTemplateCode()) > 0) {
|
return ApiResponse.ok(templateService.create(req));
|
||||||
throw new BizException(ErrorCode.DUPLICATE_DATA, "이미 존재하는 템플릿 코드입니다: " + req.getTemplateCode());
|
|
||||||
}
|
|
||||||
UploadTemplateVO vo = req.toVO();
|
|
||||||
vo.setCreatedBy(SecurityUtil.getCurrentUserId());
|
|
||||||
templateMapper.insert(vo);
|
|
||||||
|
|
||||||
if (req.getColumns() != null && !req.getColumns().isEmpty()) {
|
|
||||||
columnMapper.insertBatch(vo.getTemplateId(), req.getColumns());
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(vo.getTemplateId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "템플릿 수정")
|
@Operation(summary = "템플릿 수정")
|
||||||
@PutMapping("/{templateId}")
|
@PutMapping("/{templateId}")
|
||||||
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "UPDATE")
|
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "UPDATE")
|
||||||
@DataChangeLog(menu = "SYSTEM_UPLOAD_TEMPLATE", table = "upload_template")
|
@DataChangeLog(menu = "SYSTEM_UPLOAD_TEMPLATE", table = "upload_template")
|
||||||
@Transactional
|
|
||||||
public ApiResponse<Void> update(@PathVariable Long templateId,
|
public ApiResponse<Void> update(@PathVariable Long templateId,
|
||||||
@Valid @RequestBody UploadTemplateSaveReq req) {
|
@Valid @RequestBody UploadTemplateSaveReq req) {
|
||||||
if (templateMapper.selectById(templateId) == null) {
|
templateService.update(templateId, req);
|
||||||
throw new BizException(ErrorCode.NOT_FOUND);
|
|
||||||
}
|
|
||||||
UploadTemplateVO vo = req.toVO();
|
|
||||||
vo.setTemplateId(templateId);
|
|
||||||
vo.setUpdatedBy(SecurityUtil.getCurrentUserId());
|
|
||||||
templateMapper.update(vo);
|
|
||||||
|
|
||||||
if (req.getColumns() != null) {
|
|
||||||
columnMapper.deleteByTemplateId(templateId);
|
|
||||||
if (!req.getColumns().isEmpty()) {
|
|
||||||
columnMapper.insertBatch(templateId, req.getColumns());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,13 +58,8 @@ public class UploadTemplateController {
|
|||||||
@DeleteMapping("/{templateId}")
|
@DeleteMapping("/{templateId}")
|
||||||
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "DELETE")
|
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "DELETE")
|
||||||
@DataChangeLog(menu = "SYSTEM_UPLOAD_TEMPLATE", table = "upload_template")
|
@DataChangeLog(menu = "SYSTEM_UPLOAD_TEMPLATE", table = "upload_template")
|
||||||
@Transactional
|
|
||||||
public ApiResponse<Void> delete(@PathVariable Long templateId) {
|
public ApiResponse<Void> delete(@PathVariable Long templateId) {
|
||||||
if (templateMapper.selectById(templateId) == null) {
|
templateService.delete(templateId);
|
||||||
throw new BizException(ErrorCode.NOT_FOUND);
|
|
||||||
}
|
|
||||||
columnMapper.deleteByTemplateId(templateId);
|
|
||||||
templateMapper.delete(templateId);
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,23 +67,16 @@ public class UploadTemplateController {
|
|||||||
@GetMapping("/{templateId}/columns")
|
@GetMapping("/{templateId}/columns")
|
||||||
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
|
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
|
||||||
public ApiResponse<List<UploadTemplateColumnVO>> columns(@PathVariable Long templateId) {
|
public ApiResponse<List<UploadTemplateColumnVO>> columns(@PathVariable Long templateId) {
|
||||||
return ApiResponse.ok(columnMapper.selectByTemplateId(templateId));
|
return ApiResponse.ok(templateService.columns(templateId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "템플릿 컬럼 전체 갱신 (기존 삭제 후 재등록)")
|
@Operation(summary = "템플릿 컬럼 전체 갱신 (기존 삭제 후 재등록)")
|
||||||
@PutMapping("/{templateId}/columns")
|
@PutMapping("/{templateId}/columns")
|
||||||
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "UPDATE")
|
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "UPDATE")
|
||||||
@DataChangeLog(menu = "SYSTEM_UPLOAD_TEMPLATE", table = "upload_template_column")
|
@DataChangeLog(menu = "SYSTEM_UPLOAD_TEMPLATE", table = "upload_template_column")
|
||||||
@Transactional
|
|
||||||
public ApiResponse<Void> updateColumns(@PathVariable Long templateId,
|
public ApiResponse<Void> updateColumns(@PathVariable Long templateId,
|
||||||
@RequestBody List<UploadTemplateColumnVO> columns) {
|
@RequestBody List<UploadTemplateColumnVO> columns) {
|
||||||
if (templateMapper.selectById(templateId) == null) {
|
templateService.updateColumns(templateId, columns);
|
||||||
throw new BizException(ErrorCode.NOT_FOUND);
|
|
||||||
}
|
|
||||||
columnMapper.deleteByTemplateId(templateId);
|
|
||||||
if (columns != null && !columns.isEmpty()) {
|
|
||||||
columnMapper.insertBatch(templateId, columns);
|
|
||||||
}
|
|
||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +85,6 @@ public class UploadTemplateController {
|
|||||||
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
|
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
|
||||||
public ApiResponse<PageResponse<UploadHistoryResp>> history(@PathVariable Long templateId,
|
public ApiResponse<PageResponse<UploadHistoryResp>> history(@PathVariable Long templateId,
|
||||||
@ModelAttribute UploadTemplateSearchParam param) {
|
@ModelAttribute UploadTemplateSearchParam param) {
|
||||||
param.startPage();
|
return ApiResponse.ok(templateService.history(templateId, param));
|
||||||
return ApiResponse.ok(PageResponse.of(historyMapper.selectByTemplate(templateId, param)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.ga.api.service.chargeback;
|
||||||
|
|
||||||
|
import com.ga.core.mapper.chargeback.ChargebackGradeMapper;
|
||||||
|
import com.ga.core.vo.chargeback.ChargebackGradeVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChargebackCalculator {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
private final ChargebackGradeMapper gradeMapper;
|
||||||
|
|
||||||
|
public ChargebackResult calculate(LocalDate contractDate, LocalDate terminationDate,
|
||||||
|
BigDecimal originalCommission, String productCategory) {
|
||||||
|
int monthsElapsed = (int) ChronoUnit.MONTHS.between(contractDate, terminationDate);
|
||||||
|
String today = LocalDate.now().format(DATE_FMT);
|
||||||
|
|
||||||
|
ChargebackGradeVO grade = gradeMapper.selectByMonths(productCategory, monthsElapsed, today);
|
||||||
|
|
||||||
|
if (grade == null) {
|
||||||
|
return new ChargebackResult(BigDecimal.ZERO, monthsElapsed, BigDecimal.ZERO,
|
||||||
|
"적용 환수 구간 없음");
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal chargebackAmount = originalCommission
|
||||||
|
.multiply(grade.getChargebackPercent())
|
||||||
|
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
return new ChargebackResult(
|
||||||
|
chargebackAmount,
|
||||||
|
monthsElapsed,
|
||||||
|
grade.getChargebackPercent(),
|
||||||
|
String.format("경과월 %d개월 → 환수율 %s%%", monthsElapsed, grade.getChargebackPercent())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.ga.api.service.chargeback;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.mapper.chargeback.ChargebackGradeMapper;
|
||||||
|
import com.ga.core.vo.chargeback.ChargebackGradeResp;
|
||||||
|
import com.ga.core.vo.chargeback.ChargebackGradeSaveReq;
|
||||||
|
import com.ga.core.vo.chargeback.ChargebackGradeSearchParam;
|
||||||
|
import com.ga.core.vo.chargeback.ChargebackGradeVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChargebackGradeService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
private final ChargebackGradeMapper gradeMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<ChargebackGradeResp> list(ChargebackGradeSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(gradeMapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ChargebackGradeResp detail(Long gradeId) {
|
||||||
|
ChargebackGradeResp resp = gradeMapper.selectDetailById(gradeId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void create(ChargebackGradeSaveReq req) {
|
||||||
|
String today = LocalDate.now().format(DATE_FMT);
|
||||||
|
ChargebackGradeVO existing = gradeMapper.selectByMonths(
|
||||||
|
req.getProductCategory(), req.getMonthsFrom(), today);
|
||||||
|
if (existing != null) {
|
||||||
|
throw new BizException(ErrorCode.DUPLICATE_DATA);
|
||||||
|
}
|
||||||
|
gradeMapper.insert(toVO(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void update(Long gradeId, ChargebackGradeSaveReq req) {
|
||||||
|
ChargebackGradeVO existing = gradeMapper.selectById(gradeId);
|
||||||
|
if (existing == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
ChargebackGradeVO vo = toVO(req);
|
||||||
|
vo.setGradeId(gradeId);
|
||||||
|
vo.setIsActive(existing.getIsActive());
|
||||||
|
gradeMapper.update(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deactivate(Long gradeId) {
|
||||||
|
ChargebackGradeVO existing = gradeMapper.selectById(gradeId);
|
||||||
|
if (existing == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
if (Boolean.FALSE.equals(existing.getIsActive())) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
gradeMapper.updateStatus(gradeId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ChargebackGradeVO selectByMonths(String productCategory, Integer monthsElapsed, String baseDate) {
|
||||||
|
return gradeMapper.selectByMonths(productCategory, monthsElapsed, baseDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChargebackGradeVO toVO(ChargebackGradeSaveReq req) {
|
||||||
|
ChargebackGradeVO vo = new ChargebackGradeVO();
|
||||||
|
vo.setProductCategory(req.getProductCategory());
|
||||||
|
vo.setMonthsFrom(req.getMonthsFrom());
|
||||||
|
vo.setMonthsTo(req.getMonthsTo());
|
||||||
|
vo.setChargebackPercent(req.getChargebackPercent());
|
||||||
|
vo.setEffectiveFrom(req.getEffectiveFrom());
|
||||||
|
vo.setEffectiveTo(req.getEffectiveTo());
|
||||||
|
vo.setIsActive(true);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ga.api.service.chargeback;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public record ChargebackResult(
|
||||||
|
BigDecimal chargebackAmount,
|
||||||
|
int monthsElapsed,
|
||||||
|
BigDecimal appliedPercent,
|
||||||
|
String reason
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.ga.api.service.deduction;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.mapper.settle.AgentDeductionMapper;
|
||||||
|
import com.ga.core.vo.settle.AgentDeductionResp;
|
||||||
|
import com.ga.core.vo.settle.AgentDeductionSaveReq;
|
||||||
|
import com.ga.core.vo.settle.AgentDeductionSearchParam;
|
||||||
|
import com.ga.core.vo.settle.AgentDeductionVO;
|
||||||
|
import com.ga.core.vo.settle.DeductionStatus;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeductionService {
|
||||||
|
|
||||||
|
private final AgentDeductionMapper mapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<AgentDeductionResp> list(AgentDeductionSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(mapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public AgentDeductionResp detail(Long deductionId) {
|
||||||
|
AgentDeductionResp resp = mapper.selectDetailById(deductionId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long create(AgentDeductionSaveReq req) {
|
||||||
|
if (mapper.countByAgentMonthType(req.getAgentId(), req.getSettleMonth(), req.getDeductionType()) > 0) {
|
||||||
|
throw new BizException(ErrorCode.DUPLICATE_DATA,
|
||||||
|
"동일 설계사/정산월/차감유형 조합이 이미 존재합니다");
|
||||||
|
}
|
||||||
|
AgentDeductionVO vo = new AgentDeductionVO();
|
||||||
|
vo.setAgentId(req.getAgentId());
|
||||||
|
vo.setSettleMonth(req.getSettleMonth());
|
||||||
|
vo.setDeductionType(req.getDeductionType());
|
||||||
|
vo.setAmount(req.getAmount());
|
||||||
|
vo.setDescription(req.getDescription());
|
||||||
|
vo.setStatus(DeductionStatus.PENDING.name());
|
||||||
|
mapper.insert(vo);
|
||||||
|
return vo.getDeductionId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void update(Long deductionId, AgentDeductionSaveReq req) {
|
||||||
|
AgentDeductionVO vo = mapper.selectById(deductionId);
|
||||||
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
if (!DeductionStatus.PENDING.name().equals(vo.getStatus())) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER, "PENDING 상태만 수정 가능합니다");
|
||||||
|
}
|
||||||
|
vo.setAmount(req.getAmount());
|
||||||
|
vo.setDescription(req.getDescription());
|
||||||
|
mapper.update(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void cancel(Long deductionId) {
|
||||||
|
AgentDeductionVO vo = mapper.selectById(deductionId);
|
||||||
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
if (DeductionStatus.APPLIED.name().equals(vo.getStatus())) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER, "이미 적용된 차감은 취소할 수 없습니다");
|
||||||
|
}
|
||||||
|
mapper.updateStatus(deductionId, DeductionStatus.CANCELLED.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public BigDecimal sumPendingByAgent(Long agentId, String settleMonth) {
|
||||||
|
BigDecimal sum = mapper.sumPendingByAgent(agentId, settleMonth);
|
||||||
|
return sum != null ? sum : BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<AgentDeductionVO> selectPendingByMonth(String settleMonth) {
|
||||||
|
return mapper.selectPendingByMonth(settleMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<AgentDeductionVO> selectPendingByAgent(Long agentId, String settleMonth) {
|
||||||
|
return mapper.selectPendingByAgent(agentId, settleMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void markAsApplied(List<Long> deductionIds) {
|
||||||
|
if (deductionIds.isEmpty()) return;
|
||||||
|
mapper.updateStatusBatch(deductionIds, DeductionStatus.APPLIED.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.ga.api.service.installment;
|
||||||
|
|
||||||
|
import com.ga.core.mapper.installment.InstallmentRatioMapper;
|
||||||
|
import com.ga.core.vo.installment.InstallmentPlanVO;
|
||||||
|
import com.ga.core.vo.installment.InstallmentRatioVO;
|
||||||
|
import com.ga.core.vo.installment.InstallmentStatus;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InstallmentPlanGenerator {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyyMM");
|
||||||
|
|
||||||
|
private final InstallmentRatioMapper ratioMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계약 체결일 기준 1~7차년 분급 계획 생성.
|
||||||
|
* ratioPercent 합이 100%가 안 되는 경우 각 연차별 비율만큼 지급.
|
||||||
|
*/
|
||||||
|
public List<InstallmentPlanVO> generate(Long contractId, BigDecimal totalCommission,
|
||||||
|
LocalDate contractDate, String productCategory) {
|
||||||
|
String baseDate = contractDate.format(DATE_FMT);
|
||||||
|
List<InstallmentRatioVO> ratios = ratioMapper.selectAllActiveRatios(productCategory, baseDate);
|
||||||
|
|
||||||
|
List<InstallmentPlanVO> plans = new ArrayList<>();
|
||||||
|
for (InstallmentRatioVO ratio : ratios) {
|
||||||
|
InstallmentPlanVO plan = new InstallmentPlanVO();
|
||||||
|
plan.setContractId(contractId);
|
||||||
|
plan.setPlanYear(ratio.getPlanYear());
|
||||||
|
plan.setPlanMonth(1); // 단순화: 연 1회 지급 (plan_month=1)
|
||||||
|
plan.setSettleMonth(contractDate.plusYears(ratio.getPlanYear()).format(MONTH_FMT));
|
||||||
|
plan.setExpectedAmount(
|
||||||
|
totalCommission.multiply(ratio.getRatioPercent())
|
||||||
|
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP)
|
||||||
|
);
|
||||||
|
plan.setStatus(InstallmentStatus.SCHEDULED.code());
|
||||||
|
plans.add(plan);
|
||||||
|
}
|
||||||
|
return plans;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.ga.api.service.installment;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.mapper.installment.InstallmentPlanMapper;
|
||||||
|
import com.ga.core.mapper.installment.InstallmentRatioMapper;
|
||||||
|
import com.ga.core.vo.installment.InstallmentPlanResp;
|
||||||
|
import com.ga.core.vo.installment.InstallmentPlanSearchParam;
|
||||||
|
import com.ga.core.vo.installment.InstallmentPlanVO;
|
||||||
|
import com.ga.core.vo.installment.InstallmentRatioResp;
|
||||||
|
import com.ga.core.vo.installment.InstallmentRatioSaveReq;
|
||||||
|
import com.ga.core.vo.installment.InstallmentRatioSearchParam;
|
||||||
|
import com.ga.core.vo.installment.InstallmentRatioVO;
|
||||||
|
import com.ga.core.vo.installment.InstallmentStatus;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InstallmentService {
|
||||||
|
|
||||||
|
private final InstallmentPlanMapper planMapper;
|
||||||
|
private final InstallmentRatioMapper ratioMapper;
|
||||||
|
private final InstallmentPlanGenerator generator;
|
||||||
|
|
||||||
|
// ---- Plan 조회 ----
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<InstallmentPlanResp> list(InstallmentPlanSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(planMapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public InstallmentPlanResp detail(Long planId) {
|
||||||
|
InstallmentPlanResp resp = planMapper.selectDetailById(planId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<InstallmentPlanVO> selectScheduledForMonth(String settleMonth) {
|
||||||
|
return planMapper.selectScheduledByMonth(settleMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Ratio 조회 ----
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<InstallmentRatioResp> listRatios(InstallmentRatioSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(ratioMapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Plan 생성 ----
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void createPlan(Long contractId, BigDecimal totalCommission,
|
||||||
|
LocalDate contractDate, String productCategory) {
|
||||||
|
List<InstallmentPlanVO> plans = generator.generate(contractId, totalCommission, contractDate, productCategory);
|
||||||
|
if (!plans.isEmpty()) {
|
||||||
|
planMapper.insertBatch(plans);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Plan 상태 전이 ----
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void payPlan(Long planId, Long paymentId, BigDecimal paidAmount) {
|
||||||
|
InstallmentPlanVO plan = requirePlan(planId);
|
||||||
|
if (!InstallmentStatus.SCHEDULED.code().equals(plan.getStatus())
|
||||||
|
&& !InstallmentStatus.DEFERRED.code().equals(plan.getStatus())) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER, "지급 처리 가능한 상태가 아닙니다");
|
||||||
|
}
|
||||||
|
plan.setStatus(InstallmentStatus.PAID.code());
|
||||||
|
plan.setPaidAmount(paidAmount);
|
||||||
|
plan.setPaidAt(LocalDateTime.now());
|
||||||
|
plan.setPaymentId(paymentId);
|
||||||
|
planMapper.update(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deferPlan(Long planId) {
|
||||||
|
InstallmentPlanVO plan = requirePlan(planId);
|
||||||
|
if (!InstallmentStatus.SCHEDULED.code().equals(plan.getStatus())) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER, "SCHEDULED 상태만 유예 가능합니다");
|
||||||
|
}
|
||||||
|
planMapper.updateStatus(planId, InstallmentStatus.DEFERRED.code());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void cancelPlan(Long planId) {
|
||||||
|
InstallmentPlanVO plan = requirePlan(planId);
|
||||||
|
if (InstallmentStatus.PAID.code().equals(plan.getStatus())) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER, "지급 완료된 분급은 취소할 수 없습니다");
|
||||||
|
}
|
||||||
|
planMapper.updateStatus(planId, InstallmentStatus.CANCELLED.code());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void payScheduledForMonth(String settleMonth) {
|
||||||
|
List<InstallmentPlanVO> scheduled = planMapper.selectScheduledByMonth(settleMonth);
|
||||||
|
for (InstallmentPlanVO plan : scheduled) {
|
||||||
|
plan.setStatus(InstallmentStatus.PAID.code());
|
||||||
|
plan.setPaidAmount(plan.getExpectedAmount());
|
||||||
|
plan.setPaidAt(LocalDateTime.now());
|
||||||
|
planMapper.update(plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Ratio 생성 ----
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void createRatio(InstallmentRatioSaveReq req) {
|
||||||
|
InstallmentRatioVO vo = new InstallmentRatioVO();
|
||||||
|
vo.setProductCategory(req.getProductCategory());
|
||||||
|
vo.setPlanYear(req.getPlanYear());
|
||||||
|
vo.setRatioPercent(req.getRatioPercent());
|
||||||
|
vo.setEffectiveFrom(req.getEffectiveFrom());
|
||||||
|
vo.setEffectiveTo(req.getEffectiveTo());
|
||||||
|
vo.setIsActive(true);
|
||||||
|
ratioMapper.insert(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InstallmentPlanVO requirePlan(Long planId) {
|
||||||
|
InstallmentPlanVO plan = planMapper.selectById(planId);
|
||||||
|
if (plan == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,11 @@ import com.ga.common.util.SecurityUtil;
|
|||||||
import com.ga.core.mapper.ledger.ExceptionLedgerMapper;
|
import com.ga.core.mapper.ledger.ExceptionLedgerMapper;
|
||||||
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
|
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
|
||||||
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
|
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
|
||||||
|
import com.ga.core.mapstruct.ExceptionLedgerMapStruct;
|
||||||
import com.ga.core.vo.ledger.*;
|
import com.ga.core.vo.ledger.*;
|
||||||
|
import com.ga.core.vo.ledger.ApproveStatus;
|
||||||
|
import com.ga.core.vo.ledger.ExceptionLedgerStatus;
|
||||||
|
import org.apache.ibatis.cursor.Cursor;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -19,34 +23,41 @@ public class LedgerService {
|
|||||||
private final RecruitLedgerMapper recruitMapper;
|
private final RecruitLedgerMapper recruitMapper;
|
||||||
private final MaintainLedgerMapper maintainMapper;
|
private final MaintainLedgerMapper maintainMapper;
|
||||||
private final ExceptionLedgerMapper exceptionMapper;
|
private final ExceptionLedgerMapper exceptionMapper;
|
||||||
|
private final ExceptionLedgerMapStruct exceptionLedgerMapStruct;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public PageResponse<LedgerResp> listRecruit(LedgerSearchParam param) {
|
public PageResponse<LedgerResp> listRecruit(LedgerSearchParam param) {
|
||||||
param.startPage();
|
param.startPage();
|
||||||
return PageResponse.of(recruitMapper.selectList(param));
|
return PageResponse.of(recruitMapper.selectList(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public PageResponse<LedgerResp> listMaintain(LedgerSearchParam param) {
|
public PageResponse<LedgerResp> listMaintain(LedgerSearchParam param) {
|
||||||
param.startPage();
|
param.startPage();
|
||||||
return PageResponse.of(maintainMapper.selectList(param));
|
return PageResponse.of(maintainMapper.selectList(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public PageResponse<ExceptionLedgerResp> listException(LedgerSearchParam param) {
|
public PageResponse<ExceptionLedgerResp> listException(LedgerSearchParam param) {
|
||||||
param.startPage();
|
param.startPage();
|
||||||
return PageResponse.of(exceptionMapper.selectList(param));
|
return PageResponse.of(exceptionMapper.selectList(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public LedgerResp detailRecruit(Long ledgerId) {
|
public LedgerResp detailRecruit(Long ledgerId) {
|
||||||
LedgerResp r = recruitMapper.selectDetailById(ledgerId);
|
LedgerResp r = recruitMapper.selectDetailById(ledgerId);
|
||||||
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public LedgerResp detailMaintain(Long ledgerId) {
|
public LedgerResp detailMaintain(Long ledgerId) {
|
||||||
LedgerResp r = maintainMapper.selectDetailById(ledgerId);
|
LedgerResp r = maintainMapper.selectDetailById(ledgerId);
|
||||||
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public ExceptionLedgerResp detailException(Long ledgerId) {
|
public ExceptionLedgerResp detailException(Long ledgerId) {
|
||||||
ExceptionLedgerResp r = exceptionMapper.selectDetailById(ledgerId);
|
ExceptionLedgerResp r = exceptionMapper.selectDetailById(ledgerId);
|
||||||
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
@@ -55,27 +66,25 @@ public class LedgerService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Long createException(ExceptionLedgerSaveReq req) {
|
public Long createException(ExceptionLedgerSaveReq req) {
|
||||||
ExceptionLedgerVO vo = new ExceptionLedgerVO();
|
ExceptionLedgerVO vo = exceptionLedgerMapStruct.toVO(req);
|
||||||
vo.setAgentId(req.getAgentId());
|
|
||||||
vo.setExceptionCode(req.getExceptionCode());
|
|
||||||
vo.setSettleMonth(req.getSettleMonth());
|
|
||||||
vo.setAmount(req.getAmount());
|
|
||||||
vo.setPolicyNo(req.getPolicyNo());
|
|
||||||
vo.setCompanyCode(req.getCompanyCode());
|
|
||||||
vo.setInsuranceType(req.getInsuranceType());
|
|
||||||
vo.setDescription(req.getDescription());
|
|
||||||
vo.setRecurringYn(req.getRecurringYn());
|
|
||||||
vo.setRecurringMonths(req.getRecurringMonths());
|
|
||||||
vo.setRecurringLeft(req.getRecurringMonths());
|
vo.setRecurringLeft(req.getRecurringMonths());
|
||||||
vo.setApproveStatus("PENDING");
|
vo.setApproveStatus(ApproveStatus.PENDING.name());
|
||||||
vo.setStatus("NEW");
|
vo.setStatus(ExceptionLedgerStatus.NEW.name());
|
||||||
exceptionMapper.insert(vo);
|
exceptionMapper.insert(vo);
|
||||||
return vo.getLedgerId();
|
return vo.getLedgerId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Cursor<LedgerExcelVO> exportRecruitCursor(LedgerSearchParam param) {
|
||||||
|
return recruitMapper.selectCursorForExcel(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cursor<LedgerExcelVO> exportMaintainCursor(LedgerSearchParam param) {
|
||||||
|
return maintainMapper.selectCursorForExcel(param);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void approveException(Long ledgerId, String status) {
|
public void approveException(Long ledgerId, String status) {
|
||||||
if (!"APPROVED".equals(status) && !"REJECTED".equals(status)) {
|
if (!ApproveStatus.APPROVED.name().equals(status) && !ApproveStatus.REJECTED.name().equals(status)) {
|
||||||
throw new BizException(ErrorCode.INVALID_PARAMETER, "status는 APPROVED/REJECTED만 가능합니다");
|
throw new BizException(ErrorCode.INVALID_PARAMETER, "status는 APPROVED/REJECTED만 가능합니다");
|
||||||
}
|
}
|
||||||
Long approverId = SecurityUtil.getCurrentUserId();
|
Long approverId = SecurityUtil.getCurrentUserId();
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ import com.ga.common.exception.ErrorCode;
|
|||||||
import com.ga.common.model.PageResponse;
|
import com.ga.common.model.PageResponse;
|
||||||
import com.ga.core.mapper.org.AgentMapper;
|
import com.ga.core.mapper.org.AgentMapper;
|
||||||
import com.ga.core.vo.org.*;
|
import com.ga.core.vo.org.*;
|
||||||
|
import com.ga.core.vo.org.AgentStatus;
|
||||||
|
import com.ga.core.vo.org.TaxType;
|
||||||
|
import com.ga.core.vo.org.VatType;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.ibatis.cursor.Cursor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AgentService {
|
public class AgentService {
|
||||||
@@ -17,11 +23,13 @@ public class AgentService {
|
|||||||
private final AgentMapper mapper;
|
private final AgentMapper mapper;
|
||||||
private final CommonCodeService codeService;
|
private final CommonCodeService codeService;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public PageResponse<AgentResp> list(AgentSearchParam param) {
|
public PageResponse<AgentResp> list(AgentSearchParam param) {
|
||||||
param.startPage();
|
param.startPage();
|
||||||
return PageResponse.of(mapper.selectList(param));
|
return PageResponse.of(mapper.selectList(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public AgentResp detail(Long agentId) {
|
public AgentResp detail(Long agentId) {
|
||||||
AgentResp resp = mapper.selectDetailById(agentId);
|
AgentResp resp = mapper.selectDetailById(agentId);
|
||||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
@@ -34,7 +42,7 @@ public class AgentService {
|
|||||||
throw new BizException(ErrorCode.DUPLICATE_DATA, "이미 등록된 사번입니다");
|
throw new BizException(ErrorCode.DUPLICATE_DATA, "이미 등록된 사번입니다");
|
||||||
}
|
}
|
||||||
AgentVO vo = toVO(req, null);
|
AgentVO vo = toVO(req, null);
|
||||||
vo.setStatus("ACTIVE");
|
vo.setStatus(AgentStatus.ACTIVE.name());
|
||||||
mapper.insert(vo);
|
mapper.insert(vo);
|
||||||
// 입사 이력 기록
|
// 입사 이력 기록
|
||||||
AgentOrgHistoryVO h = new AgentOrgHistoryVO();
|
AgentOrgHistoryVO h = new AgentOrgHistoryVO();
|
||||||
@@ -76,8 +84,13 @@ public class AgentService {
|
|||||||
mapper.updateStatus(agentId, status);
|
mapper.updateStatus(agentId, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
public java.util.List<AgentOrgHistoryVO> history(Long agentId) {
|
@Transactional(readOnly = true)
|
||||||
return mapper.selectHistory(agentId);
|
public java.util.List<AgentOrgHistoryResp> history(Long agentId) {
|
||||||
|
return mapper.selectHistoryResp(agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cursor<AgentExcelVO> exportCursor(AgentSearchParam param) {
|
||||||
|
return mapper.selectCursorForExcel(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AgentVO toVO(AgentSaveReq req, Long agentId) {
|
private AgentVO toVO(AgentSaveReq req, Long agentId) {
|
||||||
@@ -93,6 +106,17 @@ public class AgentService {
|
|||||||
vo.setBankCode(req.getBankCode());
|
vo.setBankCode(req.getBankCode());
|
||||||
vo.setAccountNo(req.getAccountNo());
|
vo.setAccountNo(req.getAccountNo());
|
||||||
vo.setJoinDate(req.getJoinDate());
|
vo.setJoinDate(req.getJoinDate());
|
||||||
|
|
||||||
|
String taxType = req.getTaxType() != null ? req.getTaxType() : TaxType.BUSINESS_INCOME.name();
|
||||||
|
String vatType = req.getVatType() != null ? req.getVatType() : VatType.NONE.name();
|
||||||
|
vo.setTaxType(taxType);
|
||||||
|
vo.setVatType(vatType);
|
||||||
|
vo.setBusinessNo(req.getBusinessNo());
|
||||||
|
|
||||||
|
if (TaxType.BUSINESS_INCOME.name().equals(taxType)
|
||||||
|
&& (req.getBusinessNo() == null || req.getBusinessNo().isBlank())) {
|
||||||
|
log.warn("agentId={} taxType=BUSINESS_INCOME이지만 businessNo가 비어있습니다", agentId);
|
||||||
|
}
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.ga.common.exception.BizException;
|
|||||||
import com.ga.common.exception.ErrorCode;
|
import com.ga.common.exception.ErrorCode;
|
||||||
import com.ga.core.mapper.org.OrganizationMapper;
|
import com.ga.core.mapper.org.OrganizationMapper;
|
||||||
import com.ga.core.vo.org.OrganizationResp;
|
import com.ga.core.vo.org.OrganizationResp;
|
||||||
|
import com.ga.core.vo.org.OrganizationSaveReq;
|
||||||
import com.ga.core.vo.org.OrganizationVO;
|
import com.ga.core.vo.org.OrganizationVO;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -17,15 +18,18 @@ public class OrganizationService {
|
|||||||
|
|
||||||
private final OrganizationMapper mapper;
|
private final OrganizationMapper mapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public List<OrganizationResp> getTree() {
|
public List<OrganizationResp> getTree() {
|
||||||
List<OrganizationResp> flat = mapper.selectAllForTree();
|
List<OrganizationResp> flat = mapper.selectAllForTree();
|
||||||
return buildTree(flat);
|
return buildTree(flat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public List<OrganizationResp> list(String keyword, String orgType, String isActive) {
|
public List<OrganizationResp> list(String keyword, String orgType, String isActive) {
|
||||||
return mapper.selectList(keyword, orgType, isActive);
|
return mapper.selectList(keyword, orgType, isActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public OrganizationVO get(Long orgId) {
|
public OrganizationVO get(Long orgId) {
|
||||||
OrganizationVO vo = mapper.selectById(orgId);
|
OrganizationVO vo = mapper.selectById(orgId);
|
||||||
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
@@ -33,18 +37,33 @@ public class OrganizationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Long create(OrganizationVO vo) {
|
public Long create(OrganizationSaveReq req) {
|
||||||
|
OrganizationVO vo = toVO(req, null);
|
||||||
mapper.insert(vo);
|
mapper.insert(vo);
|
||||||
return vo.getOrgId();
|
return vo.getOrgId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void update(Long orgId, OrganizationVO vo) {
|
public void update(Long orgId, OrganizationSaveReq req) {
|
||||||
get(orgId);
|
get(orgId);
|
||||||
vo.setOrgId(orgId);
|
OrganizationVO vo = toVO(req, orgId);
|
||||||
mapper.update(vo);
|
mapper.update(vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private OrganizationVO toVO(OrganizationSaveReq req, Long orgId) {
|
||||||
|
OrganizationVO vo = new OrganizationVO();
|
||||||
|
vo.setOrgId(orgId);
|
||||||
|
vo.setOrgName(req.getOrgName());
|
||||||
|
vo.setParentOrgId(req.getParentOrgId());
|
||||||
|
vo.setOrgType(req.getOrgType());
|
||||||
|
vo.setOrgLevel(req.getOrgLevel());
|
||||||
|
vo.setRegion(req.getRegion());
|
||||||
|
vo.setEffectiveFrom(req.getEffectiveFrom());
|
||||||
|
vo.setEffectiveTo(req.getEffectiveTo());
|
||||||
|
vo.setIsActive(req.getIsActive());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void delete(Long orgId) {
|
public void delete(Long orgId) {
|
||||||
if (mapper.countChildren(orgId) > 0) {
|
if (mapper.countChildren(orgId) > 0) {
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import com.ga.common.exception.BizException;
|
|||||||
import com.ga.common.exception.ErrorCode;
|
import com.ga.common.exception.ErrorCode;
|
||||||
import com.ga.common.model.PageResponse;
|
import com.ga.common.model.PageResponse;
|
||||||
import com.ga.core.mapper.product.ContractMapper;
|
import com.ga.core.mapper.product.ContractMapper;
|
||||||
|
import com.ga.core.mapstruct.ContractMapStruct;
|
||||||
import com.ga.core.vo.product.ContractResp;
|
import com.ga.core.vo.product.ContractResp;
|
||||||
import com.ga.core.vo.product.ContractSaveReq;
|
import com.ga.core.vo.product.ContractSaveReq;
|
||||||
import com.ga.core.vo.product.ContractSearchParam;
|
import com.ga.core.vo.product.ContractSearchParam;
|
||||||
|
import com.ga.core.vo.product.ContractStatus;
|
||||||
import com.ga.core.vo.product.ContractVO;
|
import com.ga.core.vo.product.ContractVO;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -17,12 +19,15 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
public class ContractService {
|
public class ContractService {
|
||||||
|
|
||||||
private final ContractMapper mapper;
|
private final ContractMapper mapper;
|
||||||
|
private final ContractMapStruct contractMapStruct;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public PageResponse<ContractResp> list(ContractSearchParam param) {
|
public PageResponse<ContractResp> list(ContractSearchParam param) {
|
||||||
param.startPage();
|
param.startPage();
|
||||||
return PageResponse.of(mapper.selectList(param));
|
return PageResponse.of(mapper.selectList(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public ContractResp detail(Long contractId) {
|
public ContractResp detail(Long contractId) {
|
||||||
ContractResp resp = mapper.selectDetailById(contractId);
|
ContractResp resp = mapper.selectDetailById(contractId);
|
||||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
@@ -34,19 +39,8 @@ public class ContractService {
|
|||||||
if (mapper.existsByPolicyNo(req.getPolicyNo()) > 0) {
|
if (mapper.existsByPolicyNo(req.getPolicyNo()) > 0) {
|
||||||
throw new BizException(ErrorCode.DUPLICATE_DATA, "이미 등록된 증권번호입니다");
|
throw new BizException(ErrorCode.DUPLICATE_DATA, "이미 등록된 증권번호입니다");
|
||||||
}
|
}
|
||||||
ContractVO vo = new ContractVO();
|
ContractVO vo = contractMapStruct.toVO(req);
|
||||||
vo.setAgentId(req.getAgentId());
|
vo.setStatus(ContractStatus.ACTIVE.name());
|
||||||
vo.setProductId(req.getProductId());
|
|
||||||
vo.setPolicyNo(req.getPolicyNo());
|
|
||||||
vo.setContractorName(req.getContractorName());
|
|
||||||
vo.setInsuredName(req.getInsuredName());
|
|
||||||
vo.setPremium(req.getPremium());
|
|
||||||
vo.setPayCycle(req.getPayCycle());
|
|
||||||
vo.setPayPeriod(req.getPayPeriod());
|
|
||||||
vo.setCoveragePeriod(req.getCoveragePeriod());
|
|
||||||
vo.setStatus("ACTIVE");
|
|
||||||
vo.setContractDate(req.getContractDate());
|
|
||||||
vo.setEffectiveDate(req.getEffectiveDate());
|
|
||||||
mapper.insert(vo);
|
mapper.insert(vo);
|
||||||
return vo.getContractId();
|
return vo.getContractId();
|
||||||
}
|
}
|
||||||
@@ -55,15 +49,7 @@ public class ContractService {
|
|||||||
public void update(Long contractId, ContractSaveReq req) {
|
public void update(Long contractId, ContractSaveReq req) {
|
||||||
ContractVO vo = mapper.selectById(contractId);
|
ContractVO vo = mapper.selectById(contractId);
|
||||||
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
vo.setAgentId(req.getAgentId());
|
contractMapStruct.updateVO(req, vo);
|
||||||
vo.setProductId(req.getProductId());
|
|
||||||
vo.setContractorName(req.getContractorName());
|
|
||||||
vo.setInsuredName(req.getInsuredName());
|
|
||||||
vo.setPremium(req.getPremium());
|
|
||||||
vo.setPayCycle(req.getPayCycle());
|
|
||||||
vo.setPayPeriod(req.getPayPeriod());
|
|
||||||
vo.setCoveragePeriod(req.getCoveragePeriod());
|
|
||||||
vo.setEffectiveDate(req.getEffectiveDate());
|
|
||||||
mapper.update(vo);
|
mapper.update(vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.ga.api.service.product;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.core.mapper.product.InsuranceCompanyMapper;
|
||||||
|
import com.ga.core.vo.product.InsuranceCompanyVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InsuranceCompanyService {
|
||||||
|
|
||||||
|
private final InsuranceCompanyMapper mapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<InsuranceCompanyVO> list(boolean activeOnly) {
|
||||||
|
return activeOnly ? mapper.selectActive() : mapper.selectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public InsuranceCompanyVO detail(Long companyId) {
|
||||||
|
InsuranceCompanyVO vo = mapper.selectById(companyId);
|
||||||
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long create(InsuranceCompanyVO vo) {
|
||||||
|
mapper.insert(vo);
|
||||||
|
return vo.getCompanyId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void update(Long companyId, InsuranceCompanyVO vo) {
|
||||||
|
if (mapper.selectById(companyId) == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
vo.setCompanyId(companyId);
|
||||||
|
mapper.update(vo);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.ga.api.service.product;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.core.mapper.product.ProductMapper;
|
||||||
|
import com.ga.core.vo.product.ProductResp;
|
||||||
|
import com.ga.core.vo.product.ProductVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ProductService {
|
||||||
|
|
||||||
|
private final ProductMapper mapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<ProductResp> list(Long companyId, String insuranceType, String keyword, String isActive) {
|
||||||
|
return mapper.selectList(companyId, insuranceType, keyword, isActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ProductResp detail(Long productId) {
|
||||||
|
ProductResp r = mapper.selectDetailById(productId);
|
||||||
|
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long create(ProductVO vo) {
|
||||||
|
if (mapper.existsByCode(vo.getCompanyId(), vo.getProductCode()) > 0) {
|
||||||
|
throw new BizException(ErrorCode.DUPLICATE_DATA);
|
||||||
|
}
|
||||||
|
mapper.insert(vo);
|
||||||
|
return vo.getProductId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void update(Long productId, ProductVO vo) {
|
||||||
|
if (mapper.selectById(productId) == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
vo.setProductId(productId);
|
||||||
|
mapper.update(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(Long productId) {
|
||||||
|
if (mapper.selectById(productId) == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
mapper.deleteById(productId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package com.ga.api.service.receive;
|
||||||
|
|
||||||
|
import com.ga.common.util.SecurityUtil;
|
||||||
|
import com.ga.core.mapper.receive.ReceiveMapper;
|
||||||
|
import com.ga.core.vo.receive.*;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ReceiveService {
|
||||||
|
|
||||||
|
private final ReceiveMapper mapper;
|
||||||
|
|
||||||
|
// === company_profile ===
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<CompanyProfileVO> listProfiles() {
|
||||||
|
return mapper.selectAllProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void saveProfile(String companyCode, CompanyProfileVO vo) {
|
||||||
|
vo.setCompanyCode(companyCode);
|
||||||
|
if (mapper.selectProfile(companyCode) == null) mapper.insertProfile(vo);
|
||||||
|
else mapper.updateProfile(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === field_mapping ===
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<CompanyFieldMappingVO> listFields(String companyCode, String targetTable) {
|
||||||
|
return mapper.selectFieldMappings(companyCode, targetTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long createField(String companyCode, CompanyFieldMappingVO vo) {
|
||||||
|
vo.setCompanyCode(companyCode);
|
||||||
|
mapper.insertFieldMapping(vo);
|
||||||
|
return vo.getMappingId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateField(Long mappingId, CompanyFieldMappingVO vo) {
|
||||||
|
vo.setMappingId(mappingId);
|
||||||
|
mapper.updateFieldMapping(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteField(Long mappingId) {
|
||||||
|
mapper.deleteFieldMapping(mappingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === code_mapping ===
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<CodeMappingVO> listCodes(String companyCode, String codeType) {
|
||||||
|
return mapper.selectCodeMappings(companyCode, codeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long createCode(String companyCode, CodeMappingVO vo) {
|
||||||
|
vo.setCompanyCode(companyCode);
|
||||||
|
mapper.insertCodeMapping(vo);
|
||||||
|
return vo.getCodeId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === raw + errors ===
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<RawCommissionDataVO> listRaw(String companyCode, String settleMonth, String parseStatus) {
|
||||||
|
return mapper.selectRawList(companyCode, settleMonth, parseStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<ParseErrorLogVO> listErrors(String companyCode, String resolveStatus) {
|
||||||
|
return mapper.selectErrors(companyCode, resolveStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void resolveError(Long errorId, String status, String note) {
|
||||||
|
mapper.resolveError(errorId, status, note, SecurityUtil.getCurrentUserId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.ga.api.service.regulatory;
|
||||||
|
|
||||||
|
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
|
||||||
|
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
|
||||||
|
import com.ga.core.mapper.regulatory.RegulatoryLimitMapper;
|
||||||
|
import com.ga.core.vo.regulatory.RegulatoryLimitVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RegulatoryLimitChecker {
|
||||||
|
|
||||||
|
private static final String TOTAL_RATIO = "TOTAL_RATIO";
|
||||||
|
private static final String FIRST_YEAR_RATIO = "FIRST_YEAR_RATIO";
|
||||||
|
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
private final RegulatoryLimitMapper limitMapper;
|
||||||
|
private final RecruitLedgerMapper recruitLedgerMapper;
|
||||||
|
private final MaintainLedgerMapper maintainLedgerMapper;
|
||||||
|
|
||||||
|
/** 누적 수수료율 한도 체크 (1200%). 신계약 + 유지 원장 합산. */
|
||||||
|
public ViolationResult checkContractTotal(Long contractId, BigDecimal additionalAmount, LocalDate baseDate) {
|
||||||
|
String baseDateStr = baseDate.format(DATE_FMT);
|
||||||
|
RegulatoryLimitVO limit = limitMapper.selectActiveByType(TOTAL_RATIO, null, baseDateStr);
|
||||||
|
if (limit == null) {
|
||||||
|
return ViolationResult.ok(additionalAmount, null, TOTAL_RATIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal currentTotal = sumPaidByContract(contractId);
|
||||||
|
BigDecimal projected = currentTotal.add(additionalAmount);
|
||||||
|
|
||||||
|
if (projected.compareTo(limit.getLimitValue()) > 0) {
|
||||||
|
return ViolationResult.violated(
|
||||||
|
currentTotal,
|
||||||
|
limit.getLimitValue(),
|
||||||
|
TOTAL_RATIO,
|
||||||
|
String.format("누적 수수료 한도 초과: 현재 %s, 추가 %s, 한도 %s %s",
|
||||||
|
currentTotal, additionalAmount, limit.getLimitValue(), limit.getUnit())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ViolationResult.ok(currentTotal, limit.getLimitValue(), TOTAL_RATIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 1차년 수수료율 한도 체크. 신계약 원장 1차년도만 (유지는 1차년 개념 없음). */
|
||||||
|
public ViolationResult checkFirstYear(Long contractId, BigDecimal additionalAmount, LocalDate baseDate) {
|
||||||
|
String baseDateStr = baseDate.format(DATE_FMT);
|
||||||
|
RegulatoryLimitVO limit = limitMapper.selectActiveByType(FIRST_YEAR_RATIO, null, baseDateStr);
|
||||||
|
if (limit == null) {
|
||||||
|
return ViolationResult.ok(additionalAmount, null, FIRST_YEAR_RATIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal currentTotal = sumFirstYearPaidByContract(contractId);
|
||||||
|
BigDecimal projected = currentTotal.add(additionalAmount);
|
||||||
|
|
||||||
|
if (projected.compareTo(limit.getLimitValue()) > 0) {
|
||||||
|
return ViolationResult.violated(
|
||||||
|
currentTotal,
|
||||||
|
limit.getLimitValue(),
|
||||||
|
FIRST_YEAR_RATIO,
|
||||||
|
String.format("1차년 수수료 한도 초과: 현재 %s, 추가 %s, 한도 %s %s",
|
||||||
|
currentTotal, additionalAmount, limit.getLimitValue(), limit.getUnit())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ViolationResult.ok(currentTotal, limit.getLimitValue(), FIRST_YEAR_RATIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal sumPaidByContract(Long contractId) {
|
||||||
|
BigDecimal recruit = recruitLedgerMapper.sumByContract(contractId);
|
||||||
|
BigDecimal maintain = maintainLedgerMapper.sumByContract(contractId);
|
||||||
|
return nullSafe(recruit).add(nullSafe(maintain));
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal sumFirstYearPaidByContract(Long contractId) {
|
||||||
|
return nullSafe(recruitLedgerMapper.sumFirstYearByContract(contractId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal nullSafe(BigDecimal value) {
|
||||||
|
return value != null ? value : BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.ga.api.service.regulatory;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.mapper.regulatory.RegulatoryLimitMapper;
|
||||||
|
import com.ga.core.vo.regulatory.RegulatoryLimitResp;
|
||||||
|
import com.ga.core.vo.regulatory.RegulatoryLimitSaveReq;
|
||||||
|
import com.ga.core.vo.regulatory.RegulatoryLimitSearchParam;
|
||||||
|
import com.ga.core.vo.regulatory.RegulatoryLimitVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RegulatoryLimitService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
private final RegulatoryLimitMapper limitMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<RegulatoryLimitResp> list(RegulatoryLimitSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(limitMapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public RegulatoryLimitResp detail(Long limitId) {
|
||||||
|
RegulatoryLimitResp resp = limitMapper.selectDetailById(limitId);
|
||||||
|
if (resp == null) {
|
||||||
|
throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void create(RegulatoryLimitSaveReq req) {
|
||||||
|
// 동일 limitType + productCategory + 기간 중복 체크
|
||||||
|
String today = LocalDate.now().format(DATE_FMT);
|
||||||
|
RegulatoryLimitVO existing = limitMapper.selectActiveByType(
|
||||||
|
req.getLimitType(), req.getProductCategory(), today);
|
||||||
|
if (existing != null) {
|
||||||
|
throw new BizException(ErrorCode.DUPLICATE_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
RegulatoryLimitVO vo = toVO(req);
|
||||||
|
vo.setIsActive(true);
|
||||||
|
limitMapper.insert(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void update(Long limitId, RegulatoryLimitSaveReq req) {
|
||||||
|
RegulatoryLimitVO existing = limitMapper.selectById(limitId);
|
||||||
|
if (existing == null) {
|
||||||
|
throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
RegulatoryLimitVO vo = toVO(req);
|
||||||
|
vo.setLimitId(limitId);
|
||||||
|
vo.setIsActive(existing.getIsActive());
|
||||||
|
limitMapper.update(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deactivate(Long limitId) {
|
||||||
|
RegulatoryLimitVO existing = limitMapper.selectById(limitId);
|
||||||
|
if (existing == null) {
|
||||||
|
throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (Boolean.FALSE.equals(existing.getIsActive())) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
limitMapper.updateStatus(limitId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public RegulatoryLimitVO selectActiveByType(String limitType, String productCategory, String baseDate) {
|
||||||
|
return limitMapper.selectActiveByType(limitType, productCategory, baseDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<RegulatoryLimitVO> selectAllActive(String baseDate) {
|
||||||
|
return limitMapper.selectAllActive(baseDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RegulatoryLimitVO toVO(RegulatoryLimitSaveReq req) {
|
||||||
|
RegulatoryLimitVO vo = new RegulatoryLimitVO();
|
||||||
|
vo.setLimitType(req.getLimitType());
|
||||||
|
vo.setProductCategory(req.getProductCategory());
|
||||||
|
vo.setLimitValue(req.getLimitValue());
|
||||||
|
vo.setUnit(req.getUnit());
|
||||||
|
vo.setEffectiveFrom(req.getEffectiveFrom());
|
||||||
|
vo.setEffectiveTo(req.getEffectiveTo());
|
||||||
|
vo.setRegulationRef(req.getRegulationRef());
|
||||||
|
vo.setDescription(req.getDescription());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.ga.api.service.regulatory;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public record ViolationResult(
|
||||||
|
boolean violated,
|
||||||
|
BigDecimal currentTotal,
|
||||||
|
BigDecimal limitAmount,
|
||||||
|
String limitType,
|
||||||
|
String message
|
||||||
|
) {
|
||||||
|
public static ViolationResult ok(BigDecimal currentTotal, BigDecimal limitAmount, String limitType) {
|
||||||
|
return new ViolationResult(false, currentTotal, limitAmount, limitType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ViolationResult violated(BigDecimal currentTotal, BigDecimal limitAmount, String limitType, String message) {
|
||||||
|
return new ViolationResult(true, currentTotal, limitAmount, limitType, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package com.ga.api.service.rule;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.core.mapper.rule.RuleMapper;
|
||||||
|
import com.ga.core.vo.rule.*;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RuleService {
|
||||||
|
|
||||||
|
private final RuleMapper mapper;
|
||||||
|
|
||||||
|
// === commission_rate ===
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<CommissionRateVO> listCommissionRates(Long productId, Integer year) {
|
||||||
|
return mapper.selectCommissionRates(productId, year);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long createCommissionRate(CommissionRateVO vo) {
|
||||||
|
mapper.insertCommissionRate(vo);
|
||||||
|
return vo.getRateId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateCommissionRate(Long rateId, CommissionRateVO vo) {
|
||||||
|
vo.setRateId(rateId);
|
||||||
|
mapper.updateCommissionRate(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteCommissionRate(Long rateId) {
|
||||||
|
mapper.deleteCommissionRate(rateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === payout_rule ===
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<PayoutRuleVO> listPayoutRules(Long gradeId, String insuranceType) {
|
||||||
|
return mapper.selectPayoutRules(gradeId, insuranceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long createPayoutRule(PayoutRuleVO vo) {
|
||||||
|
mapper.insertPayoutRule(vo);
|
||||||
|
return vo.getRuleId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updatePayoutRule(Long ruleId, PayoutRuleVO vo) {
|
||||||
|
vo.setRuleId(ruleId);
|
||||||
|
mapper.updatePayoutRule(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deletePayoutRule(Long ruleId) {
|
||||||
|
mapper.deletePayoutRule(ruleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === override_rule ===
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<OverrideRuleVO> listOverrideRules() {
|
||||||
|
return mapper.selectOverrideRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long createOverrideRule(OverrideRuleVO vo) {
|
||||||
|
mapper.insertOverrideRule(vo);
|
||||||
|
return vo.getOverrideId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateOverrideRule(Long overrideId, OverrideRuleVO vo) {
|
||||||
|
vo.setOverrideId(overrideId);
|
||||||
|
mapper.updateOverrideRule(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteOverrideRule(Long overrideId) {
|
||||||
|
mapper.deleteOverrideRule(overrideId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === chargeback_rule ===
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<ChargebackRuleVO> listChargebackRules(String companyCode) {
|
||||||
|
return mapper.selectChargebackRules(companyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long createChargebackRule(ChargebackRuleVO vo) {
|
||||||
|
mapper.insertChargebackRule(vo);
|
||||||
|
return vo.getCbRuleId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateChargebackRule(Long cbRuleId, ChargebackRuleVO vo) {
|
||||||
|
vo.setCbRuleId(cbRuleId);
|
||||||
|
mapper.updateChargebackRule(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteChargebackRule(Long cbRuleId) {
|
||||||
|
mapper.deleteChargebackRule(cbRuleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === exception_type_code ===
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<ExceptionTypeCodeVO> listExceptionCodes() {
|
||||||
|
return mapper.selectExceptionCodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void createExceptionCode(ExceptionTypeCodeVO vo) {
|
||||||
|
if (mapper.selectExceptionCode(vo.getExceptionCode()) != null) {
|
||||||
|
throw new BizException(ErrorCode.DUPLICATE_DATA);
|
||||||
|
}
|
||||||
|
mapper.insertExceptionCode(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateExceptionCode(String exceptionCode, ExceptionTypeCodeVO vo) {
|
||||||
|
vo.setExceptionCode(exceptionCode);
|
||||||
|
mapper.updateExceptionCode(vo);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package com.ga.api.service.settle;
|
||||||
|
|
||||||
|
import com.ga.api.service.deduction.DeductionService;
|
||||||
|
import com.ga.api.service.tax.TaxBreakdown;
|
||||||
|
import com.ga.api.service.tax.TaxCalculator;
|
||||||
|
import com.ga.api.service.transfer.TransferFileService;
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.mapper.org.AgentMapper;
|
||||||
|
import com.ga.core.mapper.settle.PaymentDeductionDetailMapper;
|
||||||
|
import com.ga.core.mapper.settle.PaymentMapper;
|
||||||
|
import com.ga.core.vo.org.AgentVO;
|
||||||
|
import com.ga.core.vo.org.TaxType;
|
||||||
|
import com.ga.core.vo.org.VatType;
|
||||||
|
import com.ga.core.vo.settle.AgentDeductionVO;
|
||||||
|
import com.ga.core.vo.settle.PaymentDeductionDetailVO;
|
||||||
|
import com.ga.core.vo.settle.PaymentResp;
|
||||||
|
import com.ga.core.vo.settle.PaymentVO;
|
||||||
|
import com.ga.core.vo.settle.SettleSearchParam;
|
||||||
|
import com.ga.core.vo.settle.TransferFileResp;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PaymentService {
|
||||||
|
|
||||||
|
private final PaymentMapper mapper;
|
||||||
|
private final AgentMapper agentMapper;
|
||||||
|
private final PaymentDeductionDetailMapper deductionDetailMapper;
|
||||||
|
private final TransferFileService transferFileService;
|
||||||
|
private final TaxCalculator taxCalculator;
|
||||||
|
private final DeductionService deductionService;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<PaymentResp> list(SettleSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(mapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateStatus(Long paymentId, String status, String failReason) {
|
||||||
|
mapper.updateStatus(paymentId, status, failReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransferFileResp generateTransferFile(String settleMonth, String bankCode) {
|
||||||
|
return transferFileService.generate(settleMonth, bankCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void calculateTaxes(Long paymentId) {
|
||||||
|
PaymentVO payment = mapper.selectById(paymentId);
|
||||||
|
if (payment == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
applyTaxes(payment);
|
||||||
|
mapper.updateTaxes(payment.getPaymentId(),
|
||||||
|
payment.getIncomeTaxAmount(), payment.getLocalTaxAmount(),
|
||||||
|
payment.getVatAmount(), payment.getNetAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void calculateTaxesForMonth(String settleMonth) {
|
||||||
|
List<PaymentVO> payments = mapper.selectBySettleMonth(settleMonth);
|
||||||
|
for (PaymentVO payment : payments) {
|
||||||
|
applyTaxes(payment);
|
||||||
|
}
|
||||||
|
if (!payments.isEmpty()) {
|
||||||
|
mapper.updateTaxesBatch(payments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void applyDeductions(Long paymentId, String settleMonth) {
|
||||||
|
PaymentVO payment = mapper.selectById(paymentId);
|
||||||
|
if (payment == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
|
||||||
|
List<AgentDeductionVO> pending = deductionService.selectPendingByAgent(payment.getAgentId(), settleMonth);
|
||||||
|
if (pending.isEmpty()) return;
|
||||||
|
|
||||||
|
// 명세 기록
|
||||||
|
List<PaymentDeductionDetailVO> details = pending.stream().map(d -> {
|
||||||
|
PaymentDeductionDetailVO detail = new PaymentDeductionDetailVO();
|
||||||
|
detail.setPaymentId(paymentId);
|
||||||
|
detail.setDeductionType(d.getDeductionType());
|
||||||
|
detail.setAmount(d.getAmount());
|
||||||
|
detail.setDescription(d.getDescription());
|
||||||
|
detail.setAppliedAt(LocalDateTime.now());
|
||||||
|
return detail;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
deductionDetailMapper.insertBatch(details);
|
||||||
|
|
||||||
|
// 상태 전이
|
||||||
|
List<Long> ids = pending.stream().map(AgentDeductionVO::getDeductionId).collect(Collectors.toList());
|
||||||
|
deductionService.markAsApplied(ids);
|
||||||
|
|
||||||
|
// deduction_amount + net_amount UPDATE
|
||||||
|
// net_amount = amount − incomeTax − localTax + vat − deductionAmount
|
||||||
|
BigDecimal deductionTotal = pending.stream()
|
||||||
|
.map(AgentDeductionVO::getAmount)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
BigDecimal currentNet = payment.getNetAmount() != null ? payment.getNetAmount() : payment.getPayAmount();
|
||||||
|
BigDecimal newNet = currentNet.subtract(deductionTotal);
|
||||||
|
mapper.updateDeduction(paymentId, deductionTotal, newNet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void applyDeductionsForMonth(String settleMonth) {
|
||||||
|
List<PaymentVO> payments = mapper.selectBySettleMonth(settleMonth);
|
||||||
|
for (PaymentVO payment : payments) {
|
||||||
|
applyDeductions(payment.getPaymentId(), settleMonth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyTaxes(PaymentVO payment) {
|
||||||
|
AgentVO agent = agentMapper.selectById(payment.getAgentId());
|
||||||
|
TaxType taxType = parseTaxType(agent);
|
||||||
|
VatType vatType = parseVatType(agent);
|
||||||
|
|
||||||
|
if (taxType == TaxType.BUSINESS_INCOME && (agent == null || agent.getBusinessNo() == null || agent.getBusinessNo().isBlank())) {
|
||||||
|
log.warn("agentId={} taxType=BUSINESS_INCOME이지만 businessNo가 비어있습니다", payment.getAgentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
TaxBreakdown breakdown = taxCalculator.calculate(payment.getPayAmount(), taxType, vatType);
|
||||||
|
payment.setIncomeTaxAmount(breakdown.incomeTax());
|
||||||
|
payment.setLocalTaxAmount(breakdown.localTax());
|
||||||
|
payment.setVatAmount(breakdown.vat());
|
||||||
|
payment.setNetAmount(breakdown.netAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaxType parseTaxType(AgentVO agent) {
|
||||||
|
if (agent != null && agent.getTaxType() != null) {
|
||||||
|
try { return TaxType.valueOf(agent.getTaxType()); } catch (IllegalArgumentException ignored) {}
|
||||||
|
}
|
||||||
|
return TaxType.BUSINESS_INCOME;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VatType parseVatType(AgentVO agent) {
|
||||||
|
if (agent != null && agent.getVatType() != null) {
|
||||||
|
try { return VatType.valueOf(agent.getVatType()); } catch (IllegalArgumentException ignored) {}
|
||||||
|
}
|
||||||
|
return VatType.NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import com.ga.core.mapper.settle.SettleMasterMapper;
|
|||||||
import com.ga.core.vo.settle.SettleMasterResp;
|
import com.ga.core.vo.settle.SettleMasterResp;
|
||||||
import com.ga.core.vo.settle.SettleMasterVO;
|
import com.ga.core.vo.settle.SettleMasterVO;
|
||||||
import com.ga.core.vo.settle.SettleSearchParam;
|
import com.ga.core.vo.settle.SettleSearchParam;
|
||||||
|
import com.ga.core.vo.settle.SettleStatus;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -21,21 +22,25 @@ public class SettleService {
|
|||||||
|
|
||||||
private final SettleMasterMapper masterMapper;
|
private final SettleMasterMapper masterMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public PageResponse<SettleMasterResp> list(SettleSearchParam param) {
|
public PageResponse<SettleMasterResp> list(SettleSearchParam param) {
|
||||||
param.startPage();
|
param.startPage();
|
||||||
return PageResponse.of(masterMapper.selectList(param));
|
return PageResponse.of(masterMapper.selectList(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public SettleMasterResp detail(Long agentId, String settleMonth) {
|
public SettleMasterResp detail(Long agentId, String settleMonth) {
|
||||||
SettleMasterResp r = masterMapper.selectDetail(agentId, settleMonth);
|
SettleMasterResp r = masterMapper.selectDetail(agentId, settleMonth);
|
||||||
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public Map<String, Object> summary(String settleMonth) {
|
public Map<String, Object> summary(String settleMonth) {
|
||||||
return masterMapper.selectMonthSummary(settleMonth);
|
return masterMapper.selectMonthSummary(settleMonth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public List<Map<String, Object>> orgSummary(String settleMonth) {
|
public List<Map<String, Object>> orgSummary(String settleMonth) {
|
||||||
return masterMapper.selectOrgSummary(settleMonth);
|
return masterMapper.selectOrgSummary(settleMonth);
|
||||||
}
|
}
|
||||||
@@ -44,17 +49,17 @@ public class SettleService {
|
|||||||
public void confirm(Long settleId) {
|
public void confirm(Long settleId) {
|
||||||
SettleMasterVO vo = masterMapper.selectById(settleId);
|
SettleMasterVO vo = masterMapper.selectById(settleId);
|
||||||
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
if ("CONFIRMED".equals(vo.getStatus()) || "PAID".equals(vo.getStatus())) {
|
if (SettleStatus.CONFIRMED.name().equals(vo.getStatus()) || SettleStatus.PAID.name().equals(vo.getStatus())) {
|
||||||
throw new BizException(ErrorCode.ALREADY_CONFIRMED);
|
throw new BizException(ErrorCode.ALREADY_CONFIRMED);
|
||||||
}
|
}
|
||||||
masterMapper.updateStatus(settleId, "CONFIRMED", SecurityUtil.getCurrentUserId());
|
masterMapper.updateStatus(settleId, SettleStatus.CONFIRMED.name(), SecurityUtil.getCurrentUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void hold(Long settleId, String reason) {
|
public void hold(Long settleId, String reason) {
|
||||||
SettleMasterVO vo = masterMapper.selectById(settleId);
|
SettleMasterVO vo = masterMapper.selectById(settleId);
|
||||||
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
if ("PAID".equals(vo.getStatus())) {
|
if (SettleStatus.PAID.name().equals(vo.getStatus())) {
|
||||||
throw new BizException(ErrorCode.NOT_PERMITTED, "지급완료된 정산은 보류할 수 없습니다");
|
throw new BizException(ErrorCode.NOT_PERMITTED, "지급완료된 정산은 보류할 수 없습니다");
|
||||||
}
|
}
|
||||||
masterMapper.updateHold(settleId, reason);
|
masterMapper.updateHold(settleId, reason);
|
||||||
@@ -64,9 +69,9 @@ public class SettleService {
|
|||||||
public void release(Long settleId) {
|
public void release(Long settleId) {
|
||||||
SettleMasterVO vo = masterMapper.selectById(settleId);
|
SettleMasterVO vo = masterMapper.selectById(settleId);
|
||||||
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
if (!"HOLD".equals(vo.getStatus())) {
|
if (!SettleStatus.HOLD.name().equals(vo.getStatus())) {
|
||||||
throw new BizException(ErrorCode.INVALID_STATUS, "보류 상태가 아닙니다");
|
throw new BizException(ErrorCode.INVALID_STATUS, "보류 상태가 아닙니다");
|
||||||
}
|
}
|
||||||
masterMapper.updateStatus(settleId, "CALCULATED", SecurityUtil.getCurrentUserId());
|
masterMapper.updateStatus(settleId, SettleStatus.CALCULATED.name(), SecurityUtil.getCurrentUserId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package com.ga.api.service.statement;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.file.FileService;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.common.util.SecurityUtil;
|
||||||
|
import com.ga.core.mapper.org.AgentMapper;
|
||||||
|
import com.ga.core.mapper.settle.CommissionStatementMapper;
|
||||||
|
import com.ga.core.mapper.settle.PaymentMapper;
|
||||||
|
import com.ga.core.vo.org.AgentResp;
|
||||||
|
import com.ga.core.vo.settle.CommissionStatementResp;
|
||||||
|
import com.ga.core.vo.settle.CommissionStatementSaveReq;
|
||||||
|
import com.ga.core.vo.settle.CommissionStatementSearchParam;
|
||||||
|
import com.ga.core.vo.settle.CommissionStatementVO;
|
||||||
|
import com.ga.core.vo.settle.PaymentVO;
|
||||||
|
import com.ga.core.vo.settle.StatementStatus;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CommissionStatementService {
|
||||||
|
|
||||||
|
private final CommissionStatementMapper statementMapper;
|
||||||
|
private final AgentMapper agentMapper;
|
||||||
|
private final PaymentMapper paymentMapper;
|
||||||
|
private final StatementAggregator aggregator;
|
||||||
|
private final ExcelStatementGenerator excelGenerator;
|
||||||
|
private final FileService fileService;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<CommissionStatementResp> list(CommissionStatementSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(statementMapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public CommissionStatementResp detail(Long statementId) {
|
||||||
|
CommissionStatementResp resp = statementMapper.selectDetailById(statementId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long generate(CommissionStatementSaveReq req) {
|
||||||
|
if ("PDF".equalsIgnoreCase(req.getFileFormat())) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER, "PDF 형식은 아직 지원되지 않습니다");
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentResp agent = agentMapper.selectDetailById(req.getAgentId());
|
||||||
|
if (agent == null) throw new BizException(ErrorCode.NOT_FOUND, "설계사를 찾을 수 없습니다");
|
||||||
|
|
||||||
|
// 1. 집계
|
||||||
|
StatementSummary summary = aggregator.aggregate(req.getAgentId(), req.getSettleMonth());
|
||||||
|
|
||||||
|
// 2. 엑셀 생성
|
||||||
|
List<PaymentVO> payments = paymentMapper.selectByAgentAndMonth(req.getAgentId(), req.getSettleMonth());
|
||||||
|
byte[] excelBytes = excelGenerator.generate(agent, summary, payments);
|
||||||
|
|
||||||
|
// 3. 파일 저장
|
||||||
|
String fileName = "statement_" + req.getAgentId() + "_" + req.getSettleMonth() + ".xlsx";
|
||||||
|
Long fileId = fileService.saveBytes(excelBytes, fileName, "COMMISSION_STATEMENT",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
|
||||||
|
// 4. INSERT 또는 UPDATE (재발급)
|
||||||
|
CommissionStatementVO existing = statementMapper.selectByAgentMonth(
|
||||||
|
req.getAgentId(), req.getSettleMonth(), req.getStatementType());
|
||||||
|
|
||||||
|
Long issuedBy = SecurityUtil.getCurrentUserId();
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
if (existing == null) {
|
||||||
|
CommissionStatementVO vo = new CommissionStatementVO();
|
||||||
|
vo.setAgentId(req.getAgentId());
|
||||||
|
vo.setSettleMonth(req.getSettleMonth());
|
||||||
|
vo.setStatementType(req.getStatementType());
|
||||||
|
vo.setGrossAmount(summary.grossAmount());
|
||||||
|
vo.setIncomeTaxAmount(summary.incomeTaxAmount());
|
||||||
|
vo.setLocalTaxAmount(summary.localTaxAmount());
|
||||||
|
vo.setVatAmount(summary.vatAmount());
|
||||||
|
vo.setDeductionAmount(summary.deductionAmount());
|
||||||
|
vo.setNetAmount(summary.netAmount());
|
||||||
|
vo.setFilePath(String.valueOf(fileId));
|
||||||
|
vo.setFileFormat("EXCEL");
|
||||||
|
vo.setIssueStatus(StatementStatus.GENERATED.name());
|
||||||
|
vo.setIssuedAt(now);
|
||||||
|
vo.setIssuedBy(issuedBy != null ? issuedBy.toString() : null);
|
||||||
|
statementMapper.insert(vo);
|
||||||
|
return vo.getStatementId();
|
||||||
|
} else {
|
||||||
|
existing.setGrossAmount(summary.grossAmount());
|
||||||
|
existing.setIncomeTaxAmount(summary.incomeTaxAmount());
|
||||||
|
existing.setLocalTaxAmount(summary.localTaxAmount());
|
||||||
|
existing.setVatAmount(summary.vatAmount());
|
||||||
|
existing.setDeductionAmount(summary.deductionAmount());
|
||||||
|
existing.setNetAmount(summary.netAmount());
|
||||||
|
existing.setFilePath(String.valueOf(fileId));
|
||||||
|
existing.setFileFormat("EXCEL");
|
||||||
|
existing.setIssueStatus(StatementStatus.GENERATED.name());
|
||||||
|
existing.setIssuedAt(now);
|
||||||
|
existing.setIssuedBy(issuedBy != null ? issuedBy.toString() : null);
|
||||||
|
statementMapper.update(existing);
|
||||||
|
return existing.getStatementId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void generateForMonth(String settleMonth, String statementType) {
|
||||||
|
List<PaymentVO> allPayments = paymentMapper.selectBySettleMonth(settleMonth);
|
||||||
|
List<Long> agentIds = allPayments.stream()
|
||||||
|
.map(PaymentVO::getAgentId)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
for (Long agentId : agentIds) {
|
||||||
|
CommissionStatementSaveReq req = new CommissionStatementSaveReq();
|
||||||
|
req.setAgentId(agentId);
|
||||||
|
req.setSettleMonth(settleMonth);
|
||||||
|
req.setStatementType(statementType);
|
||||||
|
req.setFileFormat("EXCEL");
|
||||||
|
generate(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public byte[] download(Long statementId) {
|
||||||
|
CommissionStatementVO vo = statementMapper.selectById(statementId);
|
||||||
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
|
||||||
|
Long fileId = Long.parseLong(vo.getFilePath());
|
||||||
|
com.ga.common.file.FileVO fileVO = fileService.download(fileId);
|
||||||
|
|
||||||
|
// DOWNLOADED 상태 전이
|
||||||
|
statementMapper.updateStatus(statementId, StatementStatus.DOWNLOADED.name());
|
||||||
|
|
||||||
|
try {
|
||||||
|
return java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(fileVO.getFilePath()));
|
||||||
|
} catch (java.io.IOException e) {
|
||||||
|
throw new BizException(ErrorCode.FILE_NOT_FOUND, "파일을 읽을 수 없습니다");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void cancel(Long statementId) {
|
||||||
|
CommissionStatementVO vo = statementMapper.selectById(statementId);
|
||||||
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
statementMapper.updateStatus(statementId, StatementStatus.CANCELLED.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package com.ga.api.service.statement;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.core.vo.org.AgentResp;
|
||||||
|
import com.ga.core.vo.settle.PaymentVO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.poi.ss.usermodel.*;
|
||||||
|
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class ExcelStatementGenerator {
|
||||||
|
|
||||||
|
private static final DecimalFormat AMOUNT_FMT = new DecimalFormat("#,##0");
|
||||||
|
|
||||||
|
public byte[] generate(AgentResp agent, StatementSummary summary, List<PaymentVO> payments) {
|
||||||
|
try (SXSSFWorkbook wb = new SXSSFWorkbook(100)) {
|
||||||
|
wb.setCompressTempFiles(true);
|
||||||
|
Sheet sheet = wb.createSheet("수수료명세서");
|
||||||
|
|
||||||
|
int rowNum = 0;
|
||||||
|
|
||||||
|
// 상단 헤더 정보
|
||||||
|
rowNum = writeHeaderSection(sheet, wb, agent, summary, rowNum);
|
||||||
|
|
||||||
|
// 공백 행
|
||||||
|
sheet.createRow(rowNum++);
|
||||||
|
|
||||||
|
// 지급 테이블 헤더
|
||||||
|
Row tableHeader = sheet.createRow(rowNum++);
|
||||||
|
CellStyle boldStyle = boldStyle(wb);
|
||||||
|
String[] cols = {"지급ID", "지급금액", "원천세", "지방소득세", "부가세", "공제금액", "실수령액"};
|
||||||
|
for (int i = 0; i < cols.length; i++) {
|
||||||
|
Cell c = tableHeader.createCell(i);
|
||||||
|
c.setCellValue(cols[i]);
|
||||||
|
c.setCellStyle(boldStyle);
|
||||||
|
sheet.setColumnWidth(i, 18 * 256);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 지급 데이터 행
|
||||||
|
for (PaymentVO p : payments) {
|
||||||
|
Row row = sheet.createRow(rowNum++);
|
||||||
|
row.createCell(0).setCellValue(p.getPaymentId() != null ? p.getPaymentId() : 0L);
|
||||||
|
row.createCell(1).setCellValue(fmt(p.getPayAmount()));
|
||||||
|
row.createCell(2).setCellValue(fmt(p.getIncomeTaxAmount()));
|
||||||
|
row.createCell(3).setCellValue(fmt(p.getLocalTaxAmount()));
|
||||||
|
row.createCell(4).setCellValue(fmt(p.getVatAmount()));
|
||||||
|
row.createCell(5).setCellValue(fmt(p.getDeductionAmount()));
|
||||||
|
row.createCell(6).setCellValue(fmt(p.getNetAmount()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
wb.write(out);
|
||||||
|
wb.dispose();
|
||||||
|
return out.toByteArray();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("ExcelStatementGenerator error", e);
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_ERROR, "명세서 엑셀 생성 실패");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int writeHeaderSection(Sheet sheet, SXSSFWorkbook wb, AgentResp agent,
|
||||||
|
StatementSummary summary, int rowNum) {
|
||||||
|
CellStyle bold = boldStyle(wb);
|
||||||
|
writeKv(sheet, rowNum++, bold, "설계사명", agent.getAgentName());
|
||||||
|
writeKv(sheet, rowNum++, bold, "사업자번호", agent.getBusinessNo());
|
||||||
|
writeKv(sheet, rowNum++, bold, "소득세유형", agent.getTaxType());
|
||||||
|
writeKv(sheet, rowNum++, bold, "부가세유형", agent.getVatType());
|
||||||
|
sheet.createRow(rowNum++);
|
||||||
|
writeKv(sheet, rowNum++, bold, "총 지급액", fmt(summary.grossAmount()));
|
||||||
|
writeKv(sheet, rowNum++, bold, "원천세", fmt(summary.incomeTaxAmount()));
|
||||||
|
writeKv(sheet, rowNum++, bold, "지방소득세", fmt(summary.localTaxAmount()));
|
||||||
|
writeKv(sheet, rowNum++, bold, "부가세", fmt(summary.vatAmount()));
|
||||||
|
writeKv(sheet, rowNum++, bold, "공제금액", fmt(summary.deductionAmount()));
|
||||||
|
writeKv(sheet, rowNum++, bold, "실수령액", fmt(summary.netAmount()));
|
||||||
|
return rowNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeKv(Sheet sheet, int rowNum, CellStyle keyStyle, String key, String value) {
|
||||||
|
Row row = sheet.createRow(rowNum);
|
||||||
|
Cell k = row.createCell(0);
|
||||||
|
k.setCellValue(key);
|
||||||
|
k.setCellStyle(keyStyle);
|
||||||
|
row.createCell(1).setCellValue(value != null ? value : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private CellStyle boldStyle(SXSSFWorkbook wb) {
|
||||||
|
CellStyle style = wb.createCellStyle();
|
||||||
|
Font font = wb.createFont();
|
||||||
|
font.setBold(true);
|
||||||
|
style.setFont(font);
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fmt(BigDecimal val) {
|
||||||
|
if (val == null) return "0";
|
||||||
|
return AMOUNT_FMT.format(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ga.api.service.statement;
|
||||||
|
|
||||||
|
import com.ga.core.mapper.settle.PaymentMapper;
|
||||||
|
import com.ga.core.vo.settle.PaymentVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class StatementAggregator {
|
||||||
|
|
||||||
|
private final PaymentMapper paymentMapper;
|
||||||
|
|
||||||
|
public StatementSummary aggregate(Long agentId, String settleMonth) {
|
||||||
|
List<PaymentVO> payments = paymentMapper.selectByAgentAndMonth(agentId, settleMonth);
|
||||||
|
|
||||||
|
BigDecimal grossAmount = sum(payments, PaymentVO::getPayAmount);
|
||||||
|
BigDecimal incomeTax = sum(payments, PaymentVO::getIncomeTaxAmount);
|
||||||
|
BigDecimal localTax = sum(payments, PaymentVO::getLocalTaxAmount);
|
||||||
|
BigDecimal vat = sum(payments, PaymentVO::getVatAmount);
|
||||||
|
BigDecimal deduction = sum(payments, PaymentVO::getDeductionAmount);
|
||||||
|
BigDecimal netAmount = sum(payments, PaymentVO::getNetAmount);
|
||||||
|
|
||||||
|
return new StatementSummary(grossAmount, incomeTax, localTax, vat, deduction, netAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal sum(List<PaymentVO> payments, java.util.function.Function<PaymentVO, BigDecimal> getter) {
|
||||||
|
return payments.stream()
|
||||||
|
.map(p -> getter.apply(p) != null ? getter.apply(p) : BigDecimal.ZERO)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.ga.api.service.statement;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public record StatementSummary(
|
||||||
|
BigDecimal grossAmount,
|
||||||
|
BigDecimal incomeTaxAmount,
|
||||||
|
BigDecimal localTaxAmount,
|
||||||
|
BigDecimal vatAmount,
|
||||||
|
BigDecimal deductionAmount,
|
||||||
|
BigDecimal netAmount
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.ga.api.service.system;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.core.mapper.user.RoleMapper;
|
||||||
|
import com.ga.core.vo.user.RoleVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RoleService {
|
||||||
|
|
||||||
|
private final RoleMapper mapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<RoleVO> list() {
|
||||||
|
return mapper.selectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public RoleVO detail(Long roleId) {
|
||||||
|
RoleVO vo = mapper.selectById(roleId);
|
||||||
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<Map<String, Object>> permissions(Long roleId) {
|
||||||
|
return mapper.selectRolePermissions(roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long create(RoleVO vo) {
|
||||||
|
mapper.insert(vo);
|
||||||
|
return vo.getRoleId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void update(Long roleId, RoleVO vo) {
|
||||||
|
if (mapper.selectById(roleId) == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
vo.setRoleId(roleId);
|
||||||
|
mapper.update(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(Long roleId) {
|
||||||
|
if (mapper.selectById(roleId) == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
mapper.deleteById(roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void savePermissions(Long roleId, List<Map<String, Object>> permissions) {
|
||||||
|
mapper.deleteRolePermissions(roleId);
|
||||||
|
for (Map<String, Object> p : permissions) {
|
||||||
|
Long menuId = ((Number) p.get("menuId")).longValue();
|
||||||
|
String permCode = (String) p.get("permCode");
|
||||||
|
String isGranted = (String) p.getOrDefault("isGranted", "Y");
|
||||||
|
mapper.insertRolePermission(roleId, menuId, permCode, isGranted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,11 @@ import com.ga.common.exception.ErrorCode;
|
|||||||
import com.ga.common.model.PageResponse;
|
import com.ga.common.model.PageResponse;
|
||||||
import com.ga.common.system.SystemConfigService;
|
import com.ga.common.system.SystemConfigService;
|
||||||
import com.ga.core.mapper.user.UserMapper;
|
import com.ga.core.mapper.user.UserMapper;
|
||||||
|
import com.ga.core.mapstruct.UserMapStruct;
|
||||||
import com.ga.core.vo.user.UserResp;
|
import com.ga.core.vo.user.UserResp;
|
||||||
import com.ga.core.vo.user.UserSaveReq;
|
import com.ga.core.vo.user.UserSaveReq;
|
||||||
import com.ga.core.vo.user.UserSearchParam;
|
import com.ga.core.vo.user.UserSearchParam;
|
||||||
|
import com.ga.core.vo.user.UserStatus;
|
||||||
import com.ga.core.vo.user.UserVO;
|
import com.ga.core.vo.user.UserVO;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
@@ -23,12 +25,15 @@ public class UserService {
|
|||||||
private final UserMapper mapper;
|
private final UserMapper mapper;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final SystemConfigService configService;
|
private final SystemConfigService configService;
|
||||||
|
private final UserMapStruct userMapStruct;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public PageResponse<UserResp> list(UserSearchParam param) {
|
public PageResponse<UserResp> list(UserSearchParam param) {
|
||||||
param.startPage();
|
param.startPage();
|
||||||
return PageResponse.of(mapper.selectList(param));
|
return PageResponse.of(mapper.selectList(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public UserResp detail(Long userId) {
|
public UserResp detail(Long userId) {
|
||||||
UserResp r = mapper.selectDetailById(userId);
|
UserResp r = mapper.selectDetailById(userId);
|
||||||
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
@@ -45,14 +50,9 @@ public class UserService {
|
|||||||
if (initialPwd == null || initialPwd.isBlank()) {
|
if (initialPwd == null || initialPwd.isBlank()) {
|
||||||
initialPwd = "init1234!"; // 임시 — 로그인 후 변경 강제
|
initialPwd = "init1234!"; // 임시 — 로그인 후 변경 강제
|
||||||
}
|
}
|
||||||
UserVO vo = new UserVO();
|
UserVO vo = userMapStruct.toVO(req);
|
||||||
vo.setLoginId(req.getLoginId());
|
|
||||||
vo.setPassword(passwordEncoder.encode(initialPwd));
|
vo.setPassword(passwordEncoder.encode(initialPwd));
|
||||||
vo.setUserName(req.getUserName());
|
vo.setStatus(req.getStatus() != null ? req.getStatus() : UserStatus.ACTIVE.name());
|
||||||
vo.setEmail(req.getEmail());
|
|
||||||
vo.setPhone(req.getPhone());
|
|
||||||
vo.setAgentId(req.getAgentId());
|
|
||||||
vo.setStatus(req.getStatus() != null ? req.getStatus() : "ACTIVE");
|
|
||||||
mapper.insert(vo);
|
mapper.insert(vo);
|
||||||
|
|
||||||
if (req.getRoleIds() != null) {
|
if (req.getRoleIds() != null) {
|
||||||
@@ -63,15 +63,10 @@ public class UserService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void update(Long userId, UserSaveReq req) {
|
public void update(Long userId, UserSaveReq req) {
|
||||||
UserVO old = mapper.selectById(userId);
|
UserVO vo = mapper.selectById(userId);
|
||||||
if (old == null) throw new BizException(ErrorCode.NOT_FOUND);
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
UserVO vo = new UserVO();
|
userMapStruct.updateVO(req, vo);
|
||||||
vo.setUserId(userId);
|
vo.setUserId(userId);
|
||||||
vo.setUserName(req.getUserName());
|
|
||||||
vo.setEmail(req.getEmail());
|
|
||||||
vo.setPhone(req.getPhone());
|
|
||||||
vo.setAgentId(req.getAgentId());
|
|
||||||
vo.setStatus(req.getStatus());
|
|
||||||
mapper.update(vo);
|
mapper.update(vo);
|
||||||
|
|
||||||
if (req.getRoleIds() != null) {
|
if (req.getRoleIds() != null) {
|
||||||
@@ -91,9 +86,10 @@ public class UserService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void unlock(Long userId) {
|
public void unlock(Long userId) {
|
||||||
mapper.resetFailCount(userId);
|
mapper.resetFailCount(userId);
|
||||||
mapper.updateStatus(userId, "ACTIVE");
|
mapper.updateStatus(userId, UserStatus.ACTIVE.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public List<String> roleCodes(Long userId) {
|
public List<String> roleCodes(Long userId) {
|
||||||
return mapper.selectRoleCodes(userId);
|
return mapper.selectRoleCodes(userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ga.api.service.tax;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public record TaxBreakdown(
|
||||||
|
BigDecimal incomeTax,
|
||||||
|
BigDecimal localTax,
|
||||||
|
BigDecimal vat,
|
||||||
|
BigDecimal netAmount
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.ga.api.service.tax;
|
||||||
|
|
||||||
|
import com.ga.common.system.SystemConfigService;
|
||||||
|
import com.ga.core.vo.org.TaxType;
|
||||||
|
import com.ga.core.vo.org.VatType;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TaxCalculator {
|
||||||
|
|
||||||
|
private final SystemConfigService configService;
|
||||||
|
|
||||||
|
public TaxBreakdown calculate(BigDecimal amount, TaxType taxType, VatType vatType) {
|
||||||
|
BigDecimal incomeRate = resolveIncomeRate(taxType);
|
||||||
|
BigDecimal localRate = configService.getDecimal("TAX_RATE_LOCAL_INCOME", new BigDecimal("10.0"));
|
||||||
|
BigDecimal vatRate = configService.getDecimal("TAX_RATE_VAT", new BigDecimal("10.0"));
|
||||||
|
|
||||||
|
BigDecimal incomeTax = amount.multiply(incomeRate)
|
||||||
|
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP);
|
||||||
|
BigDecimal localTax = incomeTax.multiply(localRate)
|
||||||
|
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP);
|
||||||
|
BigDecimal vat = VatType.TAXABLE.equals(vatType)
|
||||||
|
? amount.multiply(vatRate).divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP)
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
BigDecimal netAmount = amount.subtract(incomeTax).subtract(localTax).add(vat);
|
||||||
|
|
||||||
|
return new TaxBreakdown(incomeTax, localTax, vat, netAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal resolveIncomeRate(TaxType taxType) {
|
||||||
|
if (TaxType.OTHER_INCOME.equals(taxType)) {
|
||||||
|
return configService.getDecimal("TAX_RATE_OTHER_INCOME", new BigDecimal("8.8"));
|
||||||
|
}
|
||||||
|
return configService.getDecimal("TAX_RATE_BUSINESS_INCOME", new BigDecimal("3.3"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,34 +26,35 @@ public class TransferFileService {
|
|||||||
private final FileService fileService;
|
private final FileService fileService;
|
||||||
private final EncryptUtil encryptUtil;
|
private final EncryptUtil encryptUtil;
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public TransferFileResp generate(String settleMonth, String bankCode) {
|
public TransferFileResp generate(String settleMonth, String bankCode) {
|
||||||
// 1) 해당 정산월 + 은행의 PENDING payment 조회 (accountNo는 EncryptTypeHandler로 자동복호화됨)
|
// 1) PENDING payment 조회 — readOnly 트랜잭션 (accountNo는 EncryptTypeHandler로 자동복호화됨)
|
||||||
List<PaymentVO> payments = paymentMapper.selectPendingByMonthAndBank(settleMonth, bankCode);
|
List<PaymentVO> payments = paymentMapper.selectPendingByMonthAndBank(settleMonth, bankCode);
|
||||||
|
|
||||||
if (payments.isEmpty()) {
|
if (payments.isEmpty()) {
|
||||||
throw new BizException(ErrorCode.NOT_FOUND, "이체 대상 지급 건이 없습니다 (settleMonth=" + settleMonth + ", bankCode=" + bankCode + ")");
|
throw new BizException(ErrorCode.NOT_FOUND, "이체 대상 지급 건이 없습니다 (settleMonth=" + settleMonth + ", bankCode=" + bankCode + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) 은행별 생성기로 byte[] 생성 (accountNo는 이미 복호화된 상태)
|
// 2) 파일 byte 생성 + 저장 — 트랜잭션 밖 (IO 작업이므로 커넥션 점유 불필요)
|
||||||
BankTransferFileGenerator generator = factory.get(bankCode);
|
BankTransferFileGenerator generator = factory.get(bankCode);
|
||||||
byte[] fileContent = generator.generate(payments, settleMonth);
|
byte[] fileContent = generator.generate(payments, settleMonth);
|
||||||
|
|
||||||
// 3) FileService로 file_storage 적재 → fileId 반환
|
|
||||||
String fileName = "transfer_" + settleMonth + "_" + bankCode + generator.getFileExtension();
|
String fileName = "transfer_" + settleMonth + "_" + bankCode + generator.getFileExtension();
|
||||||
Long fileId = fileService.saveBytes(fileContent, fileName, "TRANSFER_FILE", "application/octet-stream");
|
Long fileId = fileService.saveBytes(fileContent, fileName, "TRANSFER_FILE", "application/octet-stream");
|
||||||
|
|
||||||
// 4) payment들의 transfer_file_id 갱신 + status=SENT
|
// 3) payment 상태 갱신 — 파일 저장 성공 후 별도 트랜잭션 (파일만 잔존하는 역방향 불일치 방지)
|
||||||
List<Long> paymentIds = payments.stream()
|
List<Long> paymentIds = payments.stream()
|
||||||
.map(PaymentVO::getPaymentId)
|
.map(PaymentVO::getPaymentId)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
paymentMapper.updateTransferFile(paymentIds, fileId);
|
markPaymentsSent(paymentIds, fileId);
|
||||||
|
|
||||||
// 5) 합계 금액 계산
|
|
||||||
BigDecimal totalAmount = payments.stream()
|
BigDecimal totalAmount = payments.stream()
|
||||||
.map(p -> p.getPayAmount() != null ? p.getPayAmount() : BigDecimal.ZERO)
|
.map(p -> p.getPayAmount() != null ? p.getPayAmount() : BigDecimal.ZERO)
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
return new TransferFileResp(fileId, fileName, payments.size(), totalAmount);
|
return new TransferFileResp(fileId, fileName, payments.size(), totalAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void markPaymentsSent(List<Long> paymentIds, Long fileId) {
|
||||||
|
paymentMapper.updateTransferFile(paymentIds, fileId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import java.util.regex.Pattern;
|
|||||||
/**
|
/**
|
||||||
* LOOKUP 변환 규칙 최적화 캐시.
|
* LOOKUP 변환 규칙 최적화 캐시.
|
||||||
*
|
*
|
||||||
* processUpload() 시작 시 LOOKUP 컬럼의 참조 테이블을 한 번에 로드하여
|
* preload()가 새 Map 인스턴스를 반환한다 — 싱글톤 상태를 보유하지 않으므로 thread-safe.
|
||||||
* 행 처리마다 DB 조회하는 것을 방지한다 (70만건 × DB 조회 = 지옥 → HashMap.get() = 즉시).
|
* 호출자(UploadService)가 반환된 Map을 보유하고 TransformEngine에 전달한다.
|
||||||
*
|
*
|
||||||
* 캐시 구조: Map< "table:sourceField:targetField" → Map<sourceValue, targetValue> >
|
* 캐시 구조: Map< "table:sourceField:targetField" → Map<sourceValue, targetValue> >
|
||||||
*/
|
*/
|
||||||
@@ -40,14 +40,12 @@ public class LookupCache {
|
|||||||
private static final Pattern FIELD_PATTERN =
|
private static final Pattern FIELD_PATTERN =
|
||||||
Pattern.compile("^[A-Za-z][A-Za-z0-9_]{0,63}$");
|
Pattern.compile("^[A-Za-z][A-Za-z0-9_]{0,63}$");
|
||||||
|
|
||||||
// 요청 범위 캐시 (processUpload 한 번에만 유효)
|
|
||||||
private final Map<String, Map<String, Object>> cache = new HashMap<>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LOOKUP 컬럼 목록에서 필요한 참조 데이터를 한 번에 preload.
|
* LOOKUP 컬럼 목록에서 필요한 참조 데이터를 한 번에 로드하여 새 Map으로 반환.
|
||||||
|
* 반환된 Map은 요청 범위에서만 사용되므로 동기화 불필요.
|
||||||
*/
|
*/
|
||||||
public void preload(List<UploadTemplateColumnVO> columns) {
|
public Map<String, Map<String, Object>> preload(List<UploadTemplateColumnVO> columns) {
|
||||||
cache.clear();
|
Map<String, Map<String, Object>> cache = new HashMap<>();
|
||||||
|
|
||||||
columns.stream()
|
columns.stream()
|
||||||
.filter(c -> "LOOKUP".equalsIgnoreCase(c.getTransformRule()))
|
.filter(c -> "LOOKUP".equalsIgnoreCase(c.getTransformRule()))
|
||||||
@@ -71,12 +69,15 @@ public class LookupCache {
|
|||||||
log.debug("LookupCache preloaded: {} → {} entries", key, map.size());
|
log.debug("LookupCache preloaded: {} → {} entries", key, map.size());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* O(1) 조회.
|
* O(1) 조회. cache는 preload()의 반환값이어야 한다.
|
||||||
*/
|
*/
|
||||||
public Object lookup(String table, String sourceField, String value, String targetField) {
|
public Object lookup(Map<String, Map<String, Object>> cache,
|
||||||
|
String table, String sourceField, String value, String targetField) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
String key = buildKey(table, sourceField, targetField);
|
String key = buildKey(table, sourceField, targetField);
|
||||||
Map<String, Object> map = cache.get(key);
|
Map<String, Object> map = cache.get(key);
|
||||||
|
|||||||
@@ -37,17 +37,19 @@ public class TransformEngine {
|
|||||||
/**
|
/**
|
||||||
* 엑셀 Row 를 템플릿 컬럼 정의에 따라 변환하여 Map 으로 반환.
|
* 엑셀 Row 를 템플릿 컬럼 정의에 따라 변환하여 Map 으로 반환.
|
||||||
*
|
*
|
||||||
* @param row xlsx-streamer Row
|
* @param row xlsx-streamer Row
|
||||||
* @param columns 템플릿 컬럼 정의
|
* @param columns 템플릿 컬럼 정의
|
||||||
|
* @param lookupData preload()가 반환한 요청 범위 LOOKUP 캐시
|
||||||
* @return { targetField → 변환된 값 }
|
* @return { targetField → 변환된 값 }
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> transform(Row row, List<UploadTemplateColumnVO> columns) {
|
public Map<String, Object> transform(Row row, List<UploadTemplateColumnVO> columns,
|
||||||
|
Map<String, Map<String, Object>> lookupData) {
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
|
||||||
for (UploadTemplateColumnVO col : columns) {
|
for (UploadTemplateColumnVO col : columns) {
|
||||||
try {
|
try {
|
||||||
Object rawValue = extractRaw(row, col);
|
Object rawValue = extractRaw(row, col);
|
||||||
Object transformed = applyTransform(rawValue, col, row, columns);
|
Object transformed = applyTransform(rawValue, col, row, columns, lookupData);
|
||||||
result.put(col.getTargetField(), transformed);
|
result.put(col.getTargetField(), transformed);
|
||||||
} catch (BizException e) {
|
} catch (BizException e) {
|
||||||
throw e;
|
throw e;
|
||||||
@@ -64,7 +66,8 @@ public class TransformEngine {
|
|||||||
* 단일 컬럼에 변환 규칙을 적용.
|
* 단일 컬럼에 변환 규칙을 적용.
|
||||||
*/
|
*/
|
||||||
public Object applyTransform(Object value, UploadTemplateColumnVO col,
|
public Object applyTransform(Object value, UploadTemplateColumnVO col,
|
||||||
Row row, List<UploadTemplateColumnVO> allColumns) {
|
Row row, List<UploadTemplateColumnVO> allColumns,
|
||||||
|
Map<String, Map<String, Object>> lookupData) {
|
||||||
String rule = col.getTransformRule();
|
String rule = col.getTransformRule();
|
||||||
|
|
||||||
// FIXED 는 원본 값 무시
|
// FIXED 는 원본 값 무시
|
||||||
@@ -88,8 +91,8 @@ public class TransformEngine {
|
|||||||
case "DATE" -> applyDate(value, col);
|
case "DATE" -> applyDate(value, col);
|
||||||
case "DIVIDE" -> applyDivide(value, col);
|
case "DIVIDE" -> applyDivide(value, col);
|
||||||
case "MULTIPLY" -> applyMultiply(value, col);
|
case "MULTIPLY" -> applyMultiply(value, col);
|
||||||
case "LOOKUP" -> applyLookup(value, col);
|
case "LOOKUP" -> applyLookup(value, col, lookupData);
|
||||||
case "CODE_MAP" -> applyCodeMap(value, col);
|
case "CODE_MAP" -> applyCodeMap(value, col, lookupData);
|
||||||
case "REPLACE" -> applyReplace(value, col);
|
case "REPLACE" -> applyReplace(value, col);
|
||||||
case "SUBSTRING" -> applySubstring(value, col);
|
case "SUBSTRING" -> applySubstring(value, col);
|
||||||
case "CONCAT" -> applyConcat(row, col);
|
case "CONCAT" -> applyConcat(row, col);
|
||||||
@@ -180,9 +183,11 @@ public class TransformEngine {
|
|||||||
return bd.multiply(BigDecimal.valueOf(multiplier)).stripTrailingZeros();
|
return bd.multiply(BigDecimal.valueOf(multiplier)).stripTrailingZeros();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object applyLookup(Object value, UploadTemplateColumnVO col) {
|
private Object applyLookup(Object value, UploadTemplateColumnVO col,
|
||||||
|
Map<String, Map<String, Object>> lookupData) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
Object result = lookupCache.lookup(
|
Object result = lookupCache.lookup(
|
||||||
|
lookupData,
|
||||||
col.getLookupTable(),
|
col.getLookupTable(),
|
||||||
col.getLookupSourceField(),
|
col.getLookupSourceField(),
|
||||||
value.toString(),
|
value.toString(),
|
||||||
@@ -193,10 +198,11 @@ public class TransformEngine {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object applyCodeMap(Object value, UploadTemplateColumnVO col) {
|
private Object applyCodeMap(Object value, UploadTemplateColumnVO col,
|
||||||
|
Map<String, Map<String, Object>> lookupData) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
Object mapped = lookupCache.lookup(
|
Object mapped = lookupCache.lookup(
|
||||||
"common_code", "code", value.toString(), "code_name");
|
lookupData, "common_code", "code", value.toString(), "code_name");
|
||||||
return mapped != null ? mapped : (col.getDefaultValue() != null ? col.getDefaultValue() : value);
|
return mapped != null ? mapped : (col.getDefaultValue() != null ? col.getDefaultValue() : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import com.ga.common.exception.ErrorCode;
|
|||||||
import com.ga.common.file.FileService;
|
import com.ga.common.file.FileService;
|
||||||
import com.ga.common.util.SecurityUtil;
|
import com.ga.common.util.SecurityUtil;
|
||||||
import com.ga.core.mapper.upload.UploadColumnMapper;
|
import com.ga.core.mapper.upload.UploadColumnMapper;
|
||||||
import com.ga.core.mapper.upload.UploadHistoryMapper;
|
|
||||||
import com.ga.core.mapper.upload.UploadTemplateMapper;
|
import com.ga.core.mapper.upload.UploadTemplateMapper;
|
||||||
import com.ga.core.vo.upload.UploadHistoryVO;
|
import com.ga.core.vo.upload.UploadHistoryVO;
|
||||||
import com.ga.core.vo.upload.UploadTemplateColumnVO;
|
import com.ga.core.vo.upload.UploadTemplateColumnVO;
|
||||||
@@ -19,7 +18,6 @@ import org.apache.poi.ss.usermodel.Row;
|
|||||||
import org.apache.poi.ss.usermodel.Sheet;
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
import org.apache.poi.ss.usermodel.Workbook;
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
|
|
||||||
@@ -36,6 +34,10 @@ import java.util.regex.Pattern;
|
|||||||
*
|
*
|
||||||
* processUpload: SAX 파싱 + 변환 + 검증 + 배치 INSERT → upload_history 저장
|
* processUpload: SAX 파싱 + 변환 + 검증 + 배치 INSERT → upload_history 저장
|
||||||
* preview: 첫 N행 변환 결과만 반환 (DB INSERT 없음)
|
* preview: 첫 N행 변환 결과만 반환 (DB INSERT 없음)
|
||||||
|
*
|
||||||
|
* 트랜잭션 경계: 파일 파싱은 트랜잭션 밖. history 저장/갱신과 배치 INSERT는
|
||||||
|
* UploadTxHelper를 통해 REQUIRES_NEW 독립 트랜잭션으로 처리하여
|
||||||
|
* 롱 트랜잭션으로 인한 커넥션 풀 고갈을 방지한다.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@@ -44,11 +46,10 @@ public class UploadService {
|
|||||||
|
|
||||||
private final UploadTemplateMapper templateMapper;
|
private final UploadTemplateMapper templateMapper;
|
||||||
private final UploadColumnMapper columnMapper;
|
private final UploadColumnMapper columnMapper;
|
||||||
private final UploadHistoryMapper historyMapper;
|
|
||||||
private final TransformEngine transformEngine;
|
private final TransformEngine transformEngine;
|
||||||
private final LookupCache lookupCache;
|
private final LookupCache lookupCache;
|
||||||
private final DynamicInsertMapper dynamicInsertMapper;
|
|
||||||
private final FileService fileService;
|
private final FileService fileService;
|
||||||
|
private final UploadTxHelper txHelper;
|
||||||
|
|
||||||
/** target_table whitelist — SQL Injection 방지 */
|
/** target_table whitelist — SQL Injection 방지 */
|
||||||
private static final Set<String> ALLOWED_TARGET_TABLES = Set.of(
|
private static final Set<String> ALLOWED_TARGET_TABLES = Set.of(
|
||||||
@@ -61,7 +62,6 @@ public class UploadService {
|
|||||||
// 메인: 업로드 처리
|
// 메인: 업로드 처리
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public UploadResultResp processUpload(String templateCode, MultipartFile file) {
|
public UploadResultResp processUpload(String templateCode, MultipartFile file) {
|
||||||
long startMs = System.currentTimeMillis();
|
long startMs = System.currentTimeMillis();
|
||||||
|
|
||||||
@@ -74,12 +74,12 @@ public class UploadService {
|
|||||||
|
|
||||||
validateTargetTable(template.getTargetTable());
|
validateTargetTable(template.getTargetTable());
|
||||||
|
|
||||||
// 2. LOOKUP 캐시 사전 로드 (70만건 × DB 조회 방지)
|
// 2. LOOKUP 캐시 사전 로드 (요청 범위 Map — thread-safe)
|
||||||
lookupCache.preload(columns);
|
Map<String, Map<String, Object>> lookupData = lookupCache.preload(columns);
|
||||||
|
|
||||||
// 3. history 레코드 생성 (PROCESSING 상태)
|
// 3. history 레코드 생성 (PROCESSING 상태) — 독립 트랜잭션
|
||||||
UploadHistoryVO history = initHistory(template, file);
|
UploadHistoryVO history = initHistory(template, file);
|
||||||
historyMapper.insert(history);
|
txHelper.saveHistory(history);
|
||||||
|
|
||||||
List<ExcelErrorRow> errors = new ArrayList<>();
|
List<ExcelErrorRow> errors = new ArrayList<>();
|
||||||
int successCount = 0;
|
int successCount = 0;
|
||||||
@@ -92,6 +92,7 @@ public class UploadService {
|
|||||||
|
|
||||||
List<Map<String, Object>> batch = new ArrayList<>(batchSize);
|
List<Map<String, Object>> batch = new ArrayList<>(batchSize);
|
||||||
|
|
||||||
|
// 4. 파일 파싱 — 트랜잭션 밖에서 실행
|
||||||
try (InputStream is = file.getInputStream();
|
try (InputStream is = file.getInputStream();
|
||||||
Workbook wb = StreamingReader.builder()
|
Workbook wb = StreamingReader.builder()
|
||||||
.rowCacheSize(100)
|
.rowCacheSize(100)
|
||||||
@@ -118,7 +119,7 @@ public class UploadService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Map<String, Object> mapped = transformEngine.transform(row, columns);
|
Map<String, Object> mapped = transformEngine.transform(row, columns, lookupData);
|
||||||
validateRow(mapped, columns, rowNum, errors);
|
validateRow(mapped, columns, rowNum, errors);
|
||||||
|
|
||||||
final int currentRow = rowNum;
|
final int currentRow = rowNum;
|
||||||
@@ -126,8 +127,8 @@ public class UploadService {
|
|||||||
if (!rowHasError) {
|
if (!rowHasError) {
|
||||||
batch.add(mapped);
|
batch.add(mapped);
|
||||||
if (batch.size() >= batchSize) {
|
if (batch.size() >= batchSize) {
|
||||||
dynamicInsertMapper.insertBatch(template.getTargetTable(), batch);
|
// 청크 단위 독립 트랜잭션으로 커밋
|
||||||
successCount += batch.size();
|
successCount += txHelper.insertBatch(template.getTargetTable(), new ArrayList<>(batch));
|
||||||
batch.clear();
|
batch.clear();
|
||||||
log.debug("Batch flush: {} rows processed so far", totalCount);
|
log.debug("Batch flush: {} rows processed so far", totalCount);
|
||||||
}
|
}
|
||||||
@@ -140,17 +141,18 @@ public class UploadService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!batch.isEmpty()) {
|
if (!batch.isEmpty()) {
|
||||||
dynamicInsertMapper.insertBatch(template.getTargetTable(), batch);
|
successCount += txHelper.insertBatch(template.getTargetTable(), new ArrayList<>(batch));
|
||||||
successCount += batch.size();
|
|
||||||
batch.clear();
|
batch.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (BizException e) {
|
} catch (BizException e) {
|
||||||
updateHistoryFail(history.getHistoryId(), e.getDisplayMessage());
|
txHelper.updateHistory(history.getHistoryId(), "FAIL", e.getDisplayMessage(),
|
||||||
|
0, 0, 0, 0, null, 0);
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Upload processing error: templateCode={}", templateCode, e);
|
log.error("Upload processing error: templateCode={}", templateCode, e);
|
||||||
updateHistoryFail(history.getHistoryId(), e.getMessage());
|
txHelper.updateHistory(history.getHistoryId(), "FAIL", e.getMessage(),
|
||||||
|
0, 0, 0, 0, null, 0);
|
||||||
throw new BizException(ErrorCode.EXCEL_PARSE_FAIL, e.getMessage());
|
throw new BizException(ErrorCode.EXCEL_PARSE_FAIL, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +165,8 @@ public class UploadService {
|
|||||||
errorFileId = createErrorFile(errors, file.getOriginalFilename());
|
errorFileId = createErrorFile(errors, file.getOriginalFilename());
|
||||||
}
|
}
|
||||||
|
|
||||||
historyMapper.updateStatus(history.getHistoryId(), status, null,
|
// 5. history 완료 갱신 — 독립 트랜잭션
|
||||||
|
txHelper.updateHistory(history.getHistoryId(), status, null,
|
||||||
successCount, errorCount, skipCount, totalCount, errorFileId, durationSec);
|
successCount, errorCount, skipCount, totalCount, errorFileId, durationSec);
|
||||||
|
|
||||||
return UploadResultResp.builder()
|
return UploadResultResp.builder()
|
||||||
@@ -186,7 +189,7 @@ public class UploadService {
|
|||||||
UploadTemplateVO template = loadTemplate(templateCode);
|
UploadTemplateVO template = loadTemplate(templateCode);
|
||||||
List<UploadTemplateColumnVO> columns = columnMapper.selectByTemplateId(template.getTemplateId());
|
List<UploadTemplateColumnVO> columns = columnMapper.selectByTemplateId(template.getTemplateId());
|
||||||
|
|
||||||
lookupCache.preload(columns);
|
Map<String, Map<String, Object>> lookupData = lookupCache.preload(columns);
|
||||||
|
|
||||||
List<Map<String, Object>> result = new ArrayList<>();
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
int dataStartRow = template.getDataStartRow() != null ? template.getDataStartRow() : 2;
|
int dataStartRow = template.getDataStartRow() != null ? template.getDataStartRow() : 2;
|
||||||
@@ -207,7 +210,7 @@ public class UploadService {
|
|||||||
if ("Y".equalsIgnoreCase(template.getSkipEmptyRow()) && isEmptyRow(row)) continue;
|
if ("Y".equalsIgnoreCase(template.getSkipEmptyRow()) && isEmptyRow(row)) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result.add(transformEngine.transform(row, columns));
|
result.add(transformEngine.transform(row, columns, lookupData));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
result.add(Map.of("_error", "행 " + rowNum + ": " + e.getMessage()));
|
result.add(Map.of("_error", "행 " + rowNum + ": " + e.getMessage()));
|
||||||
}
|
}
|
||||||
@@ -337,11 +340,6 @@ public class UploadService {
|
|||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateHistoryFail(Long historyId, String message) {
|
|
||||||
historyMapper.updateStatus(historyId, "FAIL", message,
|
|
||||||
0, 0, 0, 0, null, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateRow(Map<String, Object> row, List<UploadTemplateColumnVO> columns,
|
private void validateRow(Map<String, Object> row, List<UploadTemplateColumnVO> columns,
|
||||||
int rowNum, List<ExcelErrorRow> errors) {
|
int rowNum, List<ExcelErrorRow> errors) {
|
||||||
for (UploadTemplateColumnVO col : columns) {
|
for (UploadTemplateColumnVO col : columns) {
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.ga.api.service.upload;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.common.util.SecurityUtil;
|
||||||
|
import com.ga.core.mapper.upload.UploadColumnMapper;
|
||||||
|
import com.ga.core.mapper.upload.UploadHistoryMapper;
|
||||||
|
import com.ga.core.mapper.upload.UploadTemplateMapper;
|
||||||
|
import com.ga.core.vo.upload.*;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UploadTemplateService {
|
||||||
|
|
||||||
|
private final UploadTemplateMapper templateMapper;
|
||||||
|
private final UploadColumnMapper columnMapper;
|
||||||
|
private final UploadHistoryMapper historyMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<UploadTemplateResp> list(UploadTemplateSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(templateMapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public UploadTemplateResp detail(Long templateId) {
|
||||||
|
UploadTemplateResp resp = templateMapper.selectById(templateId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
resp.setColumns(columnMapper.selectByTemplateId(templateId));
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long create(UploadTemplateSaveReq req) {
|
||||||
|
if (templateMapper.existsByCode(req.getTemplateCode()) > 0) {
|
||||||
|
throw new BizException(ErrorCode.DUPLICATE_DATA, "이미 존재하는 템플릿 코드입니다: " + req.getTemplateCode());
|
||||||
|
}
|
||||||
|
UploadTemplateVO vo = req.toVO();
|
||||||
|
vo.setCreatedBy(SecurityUtil.getCurrentUserId());
|
||||||
|
templateMapper.insert(vo);
|
||||||
|
if (req.getColumns() != null && !req.getColumns().isEmpty()) {
|
||||||
|
columnMapper.insertBatch(vo.getTemplateId(), req.getColumns());
|
||||||
|
}
|
||||||
|
return vo.getTemplateId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void update(Long templateId, UploadTemplateSaveReq req) {
|
||||||
|
if (templateMapper.selectById(templateId) == null) {
|
||||||
|
throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
}
|
||||||
|
UploadTemplateVO vo = req.toVO();
|
||||||
|
vo.setTemplateId(templateId);
|
||||||
|
vo.setUpdatedBy(SecurityUtil.getCurrentUserId());
|
||||||
|
templateMapper.update(vo);
|
||||||
|
if (req.getColumns() != null) {
|
||||||
|
columnMapper.deleteByTemplateId(templateId);
|
||||||
|
if (!req.getColumns().isEmpty()) {
|
||||||
|
columnMapper.insertBatch(templateId, req.getColumns());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(Long templateId) {
|
||||||
|
if (templateMapper.selectById(templateId) == null) {
|
||||||
|
throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
}
|
||||||
|
columnMapper.deleteByTemplateId(templateId);
|
||||||
|
templateMapper.delete(templateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<UploadTemplateColumnVO> columns(Long templateId) {
|
||||||
|
return columnMapper.selectByTemplateId(templateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateColumns(Long templateId, List<UploadTemplateColumnVO> columns) {
|
||||||
|
if (templateMapper.selectById(templateId) == null) {
|
||||||
|
throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
}
|
||||||
|
columnMapper.deleteByTemplateId(templateId);
|
||||||
|
if (columns != null && !columns.isEmpty()) {
|
||||||
|
columnMapper.insertBatch(templateId, columns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<UploadHistoryResp> history(Long templateId, UploadTemplateSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(historyMapper.selectByTemplate(templateId, param));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.ga.api.service.upload;
|
||||||
|
|
||||||
|
import com.ga.core.mapper.upload.UploadHistoryMapper;
|
||||||
|
import com.ga.core.vo.upload.UploadHistoryVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UploadService의 트랜잭션 경계를 분리한 헬퍼.
|
||||||
|
*
|
||||||
|
* UploadService 내에서 self-invocation으로는 프록시가 적용되지 않으므로
|
||||||
|
* 별도 Bean으로 분리하여 REQUIRES_NEW 트랜잭션을 보장한다.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UploadTxHelper {
|
||||||
|
|
||||||
|
private final UploadHistoryMapper historyMapper;
|
||||||
|
private final DynamicInsertMapper dynamicInsertMapper;
|
||||||
|
|
||||||
|
/** history INSERT를 독립 트랜잭션으로 커밋 — 본 작업 실패 시에도 기록이 남는다. */
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
public void saveHistory(UploadHistoryVO history) {
|
||||||
|
historyMapper.insert(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 배치 INSERT를 독립 트랜잭션으로 커밋 — 청크 단위 커밋으로 커넥션 점유 시간 단축. */
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
public int insertBatch(String targetTable, List<Map<String, Object>> batch) {
|
||||||
|
dynamicInsertMapper.insertBatch(targetTable, batch);
|
||||||
|
return batch.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** history 상태 갱신을 독립 트랜잭션으로 커밋. */
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
public void updateHistory(Long historyId, String status, String errorMessage,
|
||||||
|
int successCount, int errorCount, int skipCount,
|
||||||
|
int totalCount, Long errorFileId, int durationSec) {
|
||||||
|
historyMapper.updateStatus(historyId, status, errorMessage,
|
||||||
|
successCount, errorCount, skipCount, totalCount, errorFileId, durationSec);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
-- V23: 세무 처리 스키마 확장 + 세율 설정 + 공통코드
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. agent 테이블 세무 컬럼 추가
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE agent
|
||||||
|
ADD COLUMN tax_type VARCHAR(20) NOT NULL DEFAULT 'BUSINESS_INCOME',
|
||||||
|
ADD COLUMN business_no VARCHAR(20),
|
||||||
|
ADD COLUMN vat_type VARCHAR(20) NOT NULL DEFAULT 'NONE';
|
||||||
|
|
||||||
|
ALTER TABLE agent
|
||||||
|
ADD CONSTRAINT chk_agent_tax_type
|
||||||
|
CHECK (tax_type IN ('BUSINESS_INCOME', 'OTHER_INCOME')),
|
||||||
|
ADD CONSTRAINT chk_agent_vat_type
|
||||||
|
CHECK (vat_type IN ('NONE', 'EXEMPT', 'TAXABLE'));
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. payment 테이블 세무 컬럼 추가
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE payment
|
||||||
|
ADD COLUMN income_tax_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN local_tax_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN vat_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN net_amount NUMERIC(15,2);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 표준 세율 system_config 추가
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO system_config (config_key, config_value, config_group, value_type, description) VALUES
|
||||||
|
('TAX_RATE_BUSINESS_INCOME', '3.3', 'TAX', 'NUMBER', '사업소득 원천징수율(%) — 2024년 기준'),
|
||||||
|
('TAX_RATE_OTHER_INCOME', '8.8', 'TAX', 'NUMBER', '기타소득 원천징수율(%) — 2024년 기준'),
|
||||||
|
('TAX_RATE_LOCAL_INCOME', '10.0', 'TAX', 'NUMBER', '지방소득세율(%) — 원천세 기준'),
|
||||||
|
('TAX_RATE_VAT', '10.0', 'TAX', 'NUMBER', '부가세율(%) — 일반과세자 적용')
|
||||||
|
ON CONFLICT (config_key) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 공통코드 그룹 TAX_TYPE / VAT_TYPE 추가
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('TAX_TYPE', '소득세유형', 'BUSINESS_INCOME(사업소득) / OTHER_INCOME(기타소득)', 'Y', 190),
|
||||||
|
('VAT_TYPE', '부가세유형', 'NONE / EXEMPT(면세) / TAXABLE(일반과세)', 'Y', 200)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES
|
||||||
|
('TAX_TYPE', 'BUSINESS_INCOME', '사업소득', '원천징수 3.3% 적용', 10),
|
||||||
|
('TAX_TYPE', 'OTHER_INCOME', '기타소득', '원천징수 8.8% 적용', 20),
|
||||||
|
('VAT_TYPE', 'NONE', '미지정', '부가세 처리 없음', 10),
|
||||||
|
('VAT_TYPE', 'EXEMPT', '면세사업자','부가세 면세 — 계산서 발행', 20),
|
||||||
|
('VAT_TYPE', 'TAXABLE', '일반과세자','부가세 10% — 세금계산서', 30)
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
-- V24: 공제/차감 도메인 (agent_deduction, payment_deduction_detail, payment 확장, 공통코드)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. agent_deduction — 월별 차감 마스터
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE agent_deduction (
|
||||||
|
deduction_id BIGSERIAL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
|
||||||
|
settle_month CHAR(6) NOT NULL,
|
||||||
|
deduction_type VARCHAR(20) NOT NULL,
|
||||||
|
amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
description VARCHAR(255),
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by BIGINT,
|
||||||
|
UNIQUE (agent_id, settle_month, deduction_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_ad_month_agent ON agent_deduction(settle_month, agent_id);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. payment_deduction_detail — 이체 시 적용된 차감 명세
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE payment_deduction_detail (
|
||||||
|
detail_id BIGSERIAL PRIMARY KEY,
|
||||||
|
payment_id BIGINT NOT NULL REFERENCES payment(payment_id),
|
||||||
|
deduction_type VARCHAR(20) NOT NULL,
|
||||||
|
amount NUMERIC(15,2) NOT NULL,
|
||||||
|
description VARCHAR(255),
|
||||||
|
applied_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_pdd_payment ON payment_deduction_detail(payment_id);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. payment 테이블 차감 합계 컬럼 추가
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE payment
|
||||||
|
ADD COLUMN deduction_amount NUMERIC(15,2) NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 공통코드 그룹 DEDUCTION_TYPE 추가
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('DEDUCTION_TYPE', '차감유형', 'LOAN/ADVANCE/GUARANTEE/DUES/AD/CHARGEBACK_PENDING/OTHER', 'Y', 210),
|
||||||
|
('DEDUCTION_STATUS', '차감상태', 'PENDING/APPLIED/CANCELLED', 'Y', 220)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES
|
||||||
|
('DEDUCTION_TYPE', 'LOAN', '대출잔액', '설계사 대출 미상환 잔액', 10),
|
||||||
|
('DEDUCTION_TYPE', 'ADVANCE', '가불금', '선지급 후 정산 차감', 20),
|
||||||
|
('DEDUCTION_TYPE', 'GUARANTEE', '신원보증보험료', '신원보증보험 납부액', 30),
|
||||||
|
('DEDUCTION_TYPE', 'DUES', '협회비', '보험협회 등 협회비', 40),
|
||||||
|
('DEDUCTION_TYPE', 'AD', '광고비/판촉비', '광고 및 판촉 활동 비용', 50),
|
||||||
|
('DEDUCTION_TYPE', 'CHARGEBACK_PENDING', '미회수환수금', '환수 분할 미처리 잔액', 60),
|
||||||
|
('DEDUCTION_TYPE', 'OTHER', '기타', '기타 차감 항목', 70),
|
||||||
|
('DEDUCTION_STATUS', 'PENDING', '대기', '차감 등록 후 미적용 상태', 10),
|
||||||
|
('DEDUCTION_STATUS', 'APPLIED', '적용됨', '이체 시 차감 완료', 20),
|
||||||
|
('DEDUCTION_STATUS', 'CANCELLED', '취소', '차감 취소 처리됨', 30)
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
-- V25: 수수료 명세서 (commission_statement, 공통코드)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. commission_statement — 명세서 발급 마스터
|
||||||
|
--
|
||||||
|
-- 재발급 정책: UNIQUE(agent_id, settle_month, statement_type) 유지.
|
||||||
|
-- 재발급은 기존 row를 PUT으로 갱신(issued_at, file_path 등 업데이트)
|
||||||
|
-- — 재발급 빈도가 낮고 명세서 이력 추적보다 최신본이 중요하기 때문.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE commission_statement (
|
||||||
|
statement_id BIGSERIAL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
|
||||||
|
settle_month CHAR(6) NOT NULL,
|
||||||
|
statement_type VARCHAR(20) NOT NULL,
|
||||||
|
|
||||||
|
-- 발급 시점 집계 스냅샷 (동결값)
|
||||||
|
gross_amount NUMERIC(15,2) NOT NULL,
|
||||||
|
income_tax_amount NUMERIC(15,2) NOT NULL,
|
||||||
|
local_tax_amount NUMERIC(15,2) NOT NULL,
|
||||||
|
vat_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
deduction_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
net_amount NUMERIC(15,2) NOT NULL,
|
||||||
|
|
||||||
|
-- 발급 정보
|
||||||
|
issued_at TIMESTAMP NOT NULL,
|
||||||
|
issued_by VARCHAR(50),
|
||||||
|
file_path VARCHAR(500),
|
||||||
|
file_format VARCHAR(10),
|
||||||
|
issue_status VARCHAR(20) NOT NULL DEFAULT 'GENERATED',
|
||||||
|
|
||||||
|
-- 감사 4종
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by BIGINT,
|
||||||
|
|
||||||
|
UNIQUE (agent_id, settle_month, statement_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_cs_agent_month ON commission_statement(agent_id, settle_month, statement_type DESC);
|
||||||
|
CREATE INDEX idx_cs_issued_at ON commission_statement(issued_at DESC);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. 공통코드 그룹 STATEMENT_TYPE / STATEMENT_STATUS 추가
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('STATEMENT_TYPE', '명세서유형', 'MONTHLY/ANNUAL/PROOF', 'Y', 230),
|
||||||
|
('STATEMENT_STATUS', '명세서상태', 'GENERATED/SENT/DOWNLOADED/CANCELLED', 'Y', 240)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES
|
||||||
|
('STATEMENT_TYPE', 'MONTHLY', '월별', '월별 수수료 명세서', 10),
|
||||||
|
('STATEMENT_TYPE', 'ANNUAL', '연간누계', '연간 수수료 누계 명세서', 20),
|
||||||
|
('STATEMENT_TYPE', 'PROOF', '소득증빙', '소득증빙용 발급 명세서', 30),
|
||||||
|
('STATEMENT_STATUS', 'GENERATED', '생성됨', '명세서 파일 생성 완료', 10),
|
||||||
|
('STATEMENT_STATUS', 'SENT', '발송', '이메일 등으로 발송 완료', 20),
|
||||||
|
('STATEMENT_STATUS', 'DOWNLOADED', '다운로드됨', '설계사 다운로드 확인', 30),
|
||||||
|
('STATEMENT_STATUS', 'CANCELLED', '취소', '명세서 취소 처리', 40)
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
-- V26: 규제 한도 마스터 + 1200%룰 기본 데이터 (보험업감독규정 2024년 기준)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. regulatory_limit — 감독규정 한도 마스터 (개정 이력 추적)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE regulatory_limit (
|
||||||
|
limit_id BIGSERIAL PRIMARY KEY,
|
||||||
|
limit_type VARCHAR(30) NOT NULL,
|
||||||
|
product_category VARCHAR(30), -- NULL = ALL 상품 적용
|
||||||
|
limit_value NUMERIC(10,2) NOT NULL,
|
||||||
|
unit VARCHAR(10) NOT NULL, -- PERCENT / YEAR
|
||||||
|
effective_from DATE NOT NULL,
|
||||||
|
effective_to DATE, -- NULL = 현재 유효
|
||||||
|
regulation_ref VARCHAR(100),
|
||||||
|
description VARCHAR(500),
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by BIGINT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_rl_type_category ON regulatory_limit(limit_type, product_category, effective_from DESC);
|
||||||
|
CREATE INDEX idx_rl_active ON regulatory_limit(is_active) WHERE is_active = TRUE;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. 기본 데이터 INSERT — 2024년 한국 보험업감독규정 표준
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO regulatory_limit
|
||||||
|
(limit_type, product_category, limit_value, unit, effective_from, effective_to, regulation_ref, description)
|
||||||
|
VALUES
|
||||||
|
('TOTAL_RATIO', NULL, 1200, 'PERCENT', '2024-01-01', NULL,
|
||||||
|
'보험업감독규정 제5-15조의5', '총 모집수수료 한도 — 첫 1년 영업보험료의 1200% 이내'),
|
||||||
|
|
||||||
|
('FIRST_YEAR_RATIO', NULL, 35, 'PERCENT', '2024-01-01', NULL,
|
||||||
|
'보험업감독규정 제5-15조의5', '1차년도 지급 한도'),
|
||||||
|
|
||||||
|
('INSTALLMENT_YEARS', NULL, 7, 'YEAR', '2024-01-01', NULL,
|
||||||
|
'보험업감독규정 제5-15조의5', '분급 연한');
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 공통코드 그룹
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('LIMIT_TYPE', '한도유형', 'TOTAL_RATIO/FIRST_YEAR_RATIO/INSTALLMENT_YEARS', 'Y', 250),
|
||||||
|
('LIMIT_UNIT', '한도단위', 'PERCENT/YEAR', 'Y', 260),
|
||||||
|
('PRODUCT_CATEGORY', '상품카테고리', 'ALL/LIFE/ANNUITY/SAVINGS/EXCLUDED_AUTO/EXCLUDED_HEALTH', 'Y', 270)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES
|
||||||
|
('LIMIT_TYPE', 'TOTAL_RATIO', '총한도비율', '총 모집수수료 한도율 (1200%)', 10),
|
||||||
|
('LIMIT_TYPE', 'FIRST_YEAR_RATIO', '1차년도한도', '1차년도 지급 한도율 (35%)', 20),
|
||||||
|
('LIMIT_TYPE', 'INSTALLMENT_YEARS', '분급연한', '수수료 분급 기간 (7년)', 30),
|
||||||
|
('LIMIT_UNIT', 'PERCENT', '퍼센트', '%', 10),
|
||||||
|
('LIMIT_UNIT', 'YEAR', '년', '연도', 20),
|
||||||
|
('PRODUCT_CATEGORY', 'ALL', '전체', '모든 상품 공통 적용', 10),
|
||||||
|
('PRODUCT_CATEGORY', 'LIFE', '종신', '종신보험', 20),
|
||||||
|
('PRODUCT_CATEGORY', 'ANNUITY', '연금', '연금보험', 30),
|
||||||
|
('PRODUCT_CATEGORY', 'SAVINGS', '저축', '저축보험', 40),
|
||||||
|
('PRODUCT_CATEGORY', 'EXCLUDED_AUTO', '자동차제외', '1200%룰 적용 제외 — 자동차보험', 50),
|
||||||
|
('PRODUCT_CATEGORY', 'EXCLUDED_HEALTH', '실손제외', '1200%룰 적용 제외 — 실손의료비', 60)
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
-- V27: 분급 계획 (installment_ratio, installment_plan, 공통코드)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. installment_ratio — 분급 비율 마스터 (개정 이력 추적)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE installment_ratio (
|
||||||
|
ratio_id BIGSERIAL PRIMARY KEY,
|
||||||
|
product_category VARCHAR(30), -- NULL = ALL 상품
|
||||||
|
plan_year INT NOT NULL, -- 1~7
|
||||||
|
ratio_percent NUMERIC(5,2) NOT NULL,
|
||||||
|
effective_from DATE NOT NULL,
|
||||||
|
effective_to DATE, -- NULL = 현재 유효
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by BIGINT,
|
||||||
|
UNIQUE (product_category, plan_year, effective_from)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_ir_category_year ON installment_ratio(product_category, plan_year, effective_from DESC);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. installment_plan — 계약별 분급 계획
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE installment_plan (
|
||||||
|
plan_id BIGSERIAL PRIMARY KEY,
|
||||||
|
contract_id BIGINT NOT NULL REFERENCES contract(contract_id),
|
||||||
|
plan_year INT NOT NULL, -- 1~7
|
||||||
|
plan_month INT NOT NULL, -- 1~12
|
||||||
|
settle_month CHAR(6), -- YYYYMM, 지급 예정월
|
||||||
|
expected_amount NUMERIC(15,2) NOT NULL,
|
||||||
|
paid_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
paid_at TIMESTAMP,
|
||||||
|
payment_id BIGINT REFERENCES payment(payment_id),
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'SCHEDULED',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by BIGINT,
|
||||||
|
UNIQUE (contract_id, plan_year, plan_month)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_ip_contract ON installment_plan(contract_id, plan_year, plan_month);
|
||||||
|
CREATE INDEX idx_ip_settle ON installment_plan(settle_month, status);
|
||||||
|
CREATE INDEX idx_ip_payment ON installment_plan(payment_id);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 기본 분급 비율 데이터 (2024-01-01, NULL=ALL 상품)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO installment_ratio (product_category, plan_year, ratio_percent, effective_from) VALUES
|
||||||
|
(NULL, 1, 35, '2024-01-01'),
|
||||||
|
(NULL, 2, 25, '2024-01-01'),
|
||||||
|
(NULL, 3, 15, '2024-01-01'),
|
||||||
|
(NULL, 4, 10, '2024-01-01'),
|
||||||
|
(NULL, 5, 7, '2024-01-01'),
|
||||||
|
(NULL, 6, 5, '2024-01-01'),
|
||||||
|
(NULL, 7, 3, '2024-01-01')
|
||||||
|
ON CONFLICT (product_category, plan_year, effective_from) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 공통코드 INSTALLMENT_STATUS
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('INSTALLMENT_STATUS', '분급상태', 'SCHEDULED/PAID/DEFERRED/CANCELLED', 'Y', 280)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, code_desc, sort_order) VALUES
|
||||||
|
('INSTALLMENT_STATUS', 'SCHEDULED', '지급예정', '분급 지급 예정 상태', 10),
|
||||||
|
('INSTALLMENT_STATUS', 'PAID', '지급완료', '분급 지급 완료', 20),
|
||||||
|
('INSTALLMENT_STATUS', 'DEFERRED', '이월', '당월 미지급 — 다음 월로 이월', 30),
|
||||||
|
('INSTALLMENT_STATUS', 'CANCELLED', '취소', '분급 계획 취소', 40)
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
-- V28: 차등 환수 룰 (chargeback_grade — 해약 경과월별 단계별 환수 비율)
|
||||||
|
-- 기존 chargeback_rule(V3)은 계약별 일반 룰 — 본 테이블은 해약 시점별 차등 비율 마스터
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. chargeback_grade — 해약 경과월별 환수 비율 마스터
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE chargeback_grade (
|
||||||
|
grade_id BIGSERIAL PRIMARY KEY,
|
||||||
|
product_category VARCHAR(30), -- NULL = ALL 상품
|
||||||
|
months_from INT NOT NULL, -- 해약 경과월 시작 (이상)
|
||||||
|
months_to INT, -- 해약 경과월 종료 (이하), NULL = 무한대
|
||||||
|
chargeback_percent NUMERIC(5,2) NOT NULL, -- 환수율 %
|
||||||
|
effective_from DATE NOT NULL,
|
||||||
|
effective_to DATE, -- NULL = 현재 유효
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by BIGINT,
|
||||||
|
CHECK (months_from >= 0 AND (months_to IS NULL OR months_to >= months_from))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_cbg_category_months ON chargeback_grade(product_category, months_from, effective_from DESC);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. 기본 데이터 (2024-01-01, NULL=ALL 상품, 표준 차등 환수 룰)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO chargeback_grade
|
||||||
|
(product_category, months_from, months_to, chargeback_percent, effective_from)
|
||||||
|
VALUES
|
||||||
|
(NULL, 1, 12, 100, '2024-01-01'), -- 1~12개월: 전액 환수
|
||||||
|
(NULL, 13, 24, 50, '2024-01-01'), -- 13~24개월: 50%
|
||||||
|
(NULL, 25, 36, 25, '2024-01-01'), -- 25~36개월: 25%
|
||||||
|
(NULL, 37, 84, 10, '2024-01-01'), -- 37~84개월: 10%
|
||||||
|
(NULL, 85, NULL, 0, '2024-01-01') -- 85개월 이상: 환수 없음
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ga.core.mapper.chargeback;
|
||||||
|
|
||||||
|
import com.ga.core.vo.chargeback.ChargebackGradeResp;
|
||||||
|
import com.ga.core.vo.chargeback.ChargebackGradeSearchParam;
|
||||||
|
import com.ga.core.vo.chargeback.ChargebackGradeVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ChargebackGradeMapper {
|
||||||
|
|
||||||
|
List<ChargebackGradeResp> selectList(ChargebackGradeSearchParam param);
|
||||||
|
|
||||||
|
ChargebackGradeResp selectDetailById(@Param("gradeId") Long gradeId);
|
||||||
|
|
||||||
|
ChargebackGradeVO selectById(@Param("gradeId") Long gradeId);
|
||||||
|
|
||||||
|
/** 해약 경과월에 적용되는 환수 비율 구간 1건 조회 (차등 환수 계산 핵심) */
|
||||||
|
ChargebackGradeVO selectByMonths(@Param("productCategory") String productCategory,
|
||||||
|
@Param("monthsElapsed") Integer monthsElapsed,
|
||||||
|
@Param("baseDate") String baseDate);
|
||||||
|
|
||||||
|
/** 특정 시점 활성 전체 구간 조회 (months_from ASC) */
|
||||||
|
List<ChargebackGradeVO> selectAll(@Param("baseDate") String baseDate);
|
||||||
|
|
||||||
|
/** 카테고리별 활성 구간 조회 */
|
||||||
|
List<ChargebackGradeVO> selectByCategory(@Param("productCategory") String productCategory,
|
||||||
|
@Param("baseDate") String baseDate);
|
||||||
|
|
||||||
|
int insert(ChargebackGradeVO vo);
|
||||||
|
|
||||||
|
int update(ChargebackGradeVO vo);
|
||||||
|
|
||||||
|
int updateStatus(@Param("gradeId") Long gradeId, @Param("isActive") Boolean isActive);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.ga.core.mapper.installment;
|
||||||
|
|
||||||
|
import com.ga.core.vo.installment.InstallmentPlanResp;
|
||||||
|
import com.ga.core.vo.installment.InstallmentPlanSearchParam;
|
||||||
|
import com.ga.core.vo.installment.InstallmentPlanVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface InstallmentPlanMapper {
|
||||||
|
|
||||||
|
List<InstallmentPlanResp> selectList(InstallmentPlanSearchParam param);
|
||||||
|
|
||||||
|
InstallmentPlanResp selectDetailById(@Param("planId") Long planId);
|
||||||
|
|
||||||
|
InstallmentPlanVO selectById(@Param("planId") Long planId);
|
||||||
|
|
||||||
|
/** 계약의 전체 분급 계획 조회 (계약 상세 화면용) */
|
||||||
|
List<InstallmentPlanVO> selectByContract(@Param("contractId") Long contractId);
|
||||||
|
|
||||||
|
/** 정산월의 SCHEDULED 분급 계획 조회 (배치 지급 처리용) */
|
||||||
|
List<InstallmentPlanVO> selectScheduledByMonth(@Param("settleMonth") String settleMonth);
|
||||||
|
|
||||||
|
int insert(InstallmentPlanVO vo);
|
||||||
|
|
||||||
|
/** 7년치 분급 계획 일괄 생성 */
|
||||||
|
int insertBatch(@Param("list") List<InstallmentPlanVO> list);
|
||||||
|
|
||||||
|
int update(InstallmentPlanVO vo);
|
||||||
|
|
||||||
|
int updateStatus(@Param("planId") Long planId, @Param("status") String status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.ga.core.mapper.installment;
|
||||||
|
|
||||||
|
import com.ga.core.vo.installment.InstallmentRatioResp;
|
||||||
|
import com.ga.core.vo.installment.InstallmentRatioSearchParam;
|
||||||
|
import com.ga.core.vo.installment.InstallmentRatioVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface InstallmentRatioMapper {
|
||||||
|
|
||||||
|
List<InstallmentRatioResp> selectList(InstallmentRatioSearchParam param);
|
||||||
|
|
||||||
|
InstallmentRatioResp selectDetailById(@Param("ratioId") Long ratioId);
|
||||||
|
|
||||||
|
InstallmentRatioVO selectById(@Param("ratioId") Long ratioId);
|
||||||
|
|
||||||
|
/** 특정 날짜에 유효한 분급 비율 1건 (planYear + productCategory 조합) */
|
||||||
|
InstallmentRatioVO selectActive(@Param("productCategory") String productCategory,
|
||||||
|
@Param("planYear") Integer planYear,
|
||||||
|
@Param("baseDate") String baseDate);
|
||||||
|
|
||||||
|
/** 특정 날짜에 유효한 1~7년 차 전체 비율 조회 (분급 계획 일괄 생성 시 사용) */
|
||||||
|
List<InstallmentRatioVO> selectAllActiveRatios(@Param("productCategory") String productCategory,
|
||||||
|
@Param("baseDate") String baseDate);
|
||||||
|
|
||||||
|
int insert(InstallmentRatioVO vo);
|
||||||
|
|
||||||
|
int update(InstallmentRatioVO vo);
|
||||||
|
}
|
||||||
@@ -26,4 +26,7 @@ public interface MaintainLedgerMapper {
|
|||||||
BigDecimal sumByMonth(@Param("settleMonth") String settleMonth);
|
BigDecimal sumByMonth(@Param("settleMonth") String settleMonth);
|
||||||
BigDecimal sumByAgentMonth(@Param("agentId") Long agentId,
|
BigDecimal sumByAgentMonth(@Param("agentId") Long agentId,
|
||||||
@Param("settleMonth") String settleMonth);
|
@Param("settleMonth") String settleMonth);
|
||||||
|
|
||||||
|
/** 계약별 유지 원장 수수료 합산 (1200%룰 누적 검증용) */
|
||||||
|
BigDecimal sumByContract(@Param("contractId") Long contractId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,4 +30,10 @@ public interface RecruitLedgerMapper {
|
|||||||
/** 정산월 + 설계사 합계 */
|
/** 정산월 + 설계사 합계 */
|
||||||
BigDecimal sumByAgentMonth(@Param("agentId") Long agentId,
|
BigDecimal sumByAgentMonth(@Param("agentId") Long agentId,
|
||||||
@Param("settleMonth") String settleMonth);
|
@Param("settleMonth") String settleMonth);
|
||||||
|
|
||||||
|
/** 계약별 신계약 원장 수수료 합산 (1200%룰 누적 검증용) */
|
||||||
|
BigDecimal sumByContract(@Param("contractId") Long contractId);
|
||||||
|
|
||||||
|
/** 계약별 1차년도 수수료 합산 (1차년도 한도 검증용) */
|
||||||
|
BigDecimal sumFirstYearByContract(@Param("contractId") Long contractId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ga.core.mapper.org;
|
package com.ga.core.mapper.org;
|
||||||
|
|
||||||
import com.ga.core.vo.org.AgentExcelVO;
|
import com.ga.core.vo.org.AgentExcelVO;
|
||||||
|
import com.ga.core.vo.org.AgentOrgHistoryResp;
|
||||||
import com.ga.core.vo.org.AgentOrgHistoryVO;
|
import com.ga.core.vo.org.AgentOrgHistoryVO;
|
||||||
import com.ga.core.vo.org.AgentResp;
|
import com.ga.core.vo.org.AgentResp;
|
||||||
import com.ga.core.vo.org.AgentSearchParam;
|
import com.ga.core.vo.org.AgentSearchParam;
|
||||||
@@ -28,5 +29,6 @@ public interface AgentMapper {
|
|||||||
|
|
||||||
/** 이력 */
|
/** 이력 */
|
||||||
List<AgentOrgHistoryVO> selectHistory(@Param("agentId") Long agentId);
|
List<AgentOrgHistoryVO> selectHistory(@Param("agentId") Long agentId);
|
||||||
|
List<AgentOrgHistoryResp> selectHistoryResp(@Param("agentId") Long agentId);
|
||||||
int insertHistory(AgentOrgHistoryVO vo);
|
int insertHistory(AgentOrgHistoryVO vo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.ga.core.mapper.regulatory;
|
||||||
|
|
||||||
|
import com.ga.core.vo.regulatory.RegulatoryLimitResp;
|
||||||
|
import com.ga.core.vo.regulatory.RegulatoryLimitSearchParam;
|
||||||
|
import com.ga.core.vo.regulatory.RegulatoryLimitVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface RegulatoryLimitMapper {
|
||||||
|
|
||||||
|
List<RegulatoryLimitResp> selectList(RegulatoryLimitSearchParam param);
|
||||||
|
|
||||||
|
RegulatoryLimitResp selectDetailById(@Param("limitId") Long limitId);
|
||||||
|
|
||||||
|
RegulatoryLimitVO selectById(@Param("limitId") Long limitId);
|
||||||
|
|
||||||
|
/** 특정 날짜에 유효한 한도 1건 조회 (limitType + productCategory 조합) */
|
||||||
|
RegulatoryLimitVO selectActiveByType(@Param("limitType") String limitType,
|
||||||
|
@Param("productCategory") String productCategory,
|
||||||
|
@Param("baseDate") String baseDate);
|
||||||
|
|
||||||
|
/** 특정 날짜에 유효한 모든 활성 한도 조회 (배치/검증용) */
|
||||||
|
List<RegulatoryLimitVO> selectAllActive(@Param("baseDate") String baseDate);
|
||||||
|
|
||||||
|
int insert(RegulatoryLimitVO vo);
|
||||||
|
|
||||||
|
int update(RegulatoryLimitVO vo);
|
||||||
|
|
||||||
|
/** is_active 플래그 변경 */
|
||||||
|
int updateStatus(@Param("limitId") Long limitId, @Param("isActive") Boolean isActive);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.ga.core.mapper.settle;
|
||||||
|
|
||||||
|
import com.ga.core.vo.settle.AgentDeductionResp;
|
||||||
|
import com.ga.core.vo.settle.AgentDeductionSearchParam;
|
||||||
|
import com.ga.core.vo.settle.AgentDeductionVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface AgentDeductionMapper {
|
||||||
|
List<AgentDeductionResp> selectList(AgentDeductionSearchParam param);
|
||||||
|
AgentDeductionResp selectDetailById(@Param("deductionId") Long deductionId);
|
||||||
|
AgentDeductionVO selectById(@Param("deductionId") Long deductionId);
|
||||||
|
|
||||||
|
int insert(AgentDeductionVO vo);
|
||||||
|
int update(AgentDeductionVO vo);
|
||||||
|
int updateStatus(@Param("deductionId") Long deductionId, @Param("status") String status);
|
||||||
|
|
||||||
|
/** 정산월 + 설계사의 PENDING 차감 합계 */
|
||||||
|
BigDecimal sumPendingByAgent(@Param("agentId") Long agentId,
|
||||||
|
@Param("settleMonth") String settleMonth);
|
||||||
|
|
||||||
|
/** 정산월 전체 PENDING 목록 (배치용) */
|
||||||
|
List<AgentDeductionVO> selectPendingByMonth(@Param("settleMonth") String settleMonth);
|
||||||
|
|
||||||
|
/** 설계사의 PENDING 목록 (단건 차감 적용용) */
|
||||||
|
List<AgentDeductionVO> selectPendingByAgent(@Param("agentId") Long agentId,
|
||||||
|
@Param("settleMonth") String settleMonth);
|
||||||
|
|
||||||
|
/** 일괄 상태 전이 */
|
||||||
|
int updateStatusBatch(@Param("deductionIds") List<Long> deductionIds,
|
||||||
|
@Param("status") String status);
|
||||||
|
|
||||||
|
/** 중복 검증 */
|
||||||
|
int countByAgentMonthType(@Param("agentId") Long agentId,
|
||||||
|
@Param("settleMonth") String settleMonth,
|
||||||
|
@Param("deductionType") String deductionType);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.ga.core.mapper.settle;
|
||||||
|
|
||||||
|
import com.ga.core.vo.settle.CommissionStatementResp;
|
||||||
|
import com.ga.core.vo.settle.CommissionStatementSearchParam;
|
||||||
|
import com.ga.core.vo.settle.CommissionStatementVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface CommissionStatementMapper {
|
||||||
|
|
||||||
|
List<CommissionStatementResp> selectList(CommissionStatementSearchParam param);
|
||||||
|
|
||||||
|
CommissionStatementResp selectDetailById(@Param("statementId") Long statementId);
|
||||||
|
|
||||||
|
CommissionStatementVO selectById(@Param("statementId") Long statementId);
|
||||||
|
|
||||||
|
/** UNIQUE(agent_id, settle_month, statement_type) 조회 — 재발급 판단용 */
|
||||||
|
CommissionStatementVO selectByAgentMonth(@Param("agentId") Long agentId,
|
||||||
|
@Param("settleMonth") String settleMonth,
|
||||||
|
@Param("statementType") String statementType);
|
||||||
|
|
||||||
|
int insert(CommissionStatementVO vo);
|
||||||
|
|
||||||
|
int update(CommissionStatementVO vo);
|
||||||
|
|
||||||
|
int updateStatus(@Param("statementId") Long statementId, @Param("issueStatus") String issueStatus);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ga.core.mapper.settle;
|
||||||
|
|
||||||
|
import com.ga.core.vo.settle.PaymentDeductionDetailVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface PaymentDeductionDetailMapper {
|
||||||
|
List<PaymentDeductionDetailVO> selectByPaymentId(@Param("paymentId") Long paymentId);
|
||||||
|
int insert(PaymentDeductionDetailVO vo);
|
||||||
|
int insertBatch(@Param("list") List<PaymentDeductionDetailVO> list);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import com.ga.core.vo.settle.SettleSearchParam;
|
|||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
@@ -19,7 +20,20 @@ public interface PaymentMapper {
|
|||||||
@Param("failReason") String failReason);
|
@Param("failReason") String failReason);
|
||||||
int updateTransferFile(@Param("paymentIds") List<Long> paymentIds,
|
int updateTransferFile(@Param("paymentIds") List<Long> paymentIds,
|
||||||
@Param("transferFileId") Long transferFileId);
|
@Param("transferFileId") Long transferFileId);
|
||||||
|
int updateTaxes(@Param("paymentId") Long paymentId,
|
||||||
|
@Param("incomeTaxAmount") BigDecimal incomeTaxAmount,
|
||||||
|
@Param("localTaxAmount") BigDecimal localTaxAmount,
|
||||||
|
@Param("vatAmount") BigDecimal vatAmount,
|
||||||
|
@Param("netAmount") BigDecimal netAmount);
|
||||||
|
int updateTaxesBatch(@Param("list") List<PaymentVO> list);
|
||||||
|
|
||||||
|
int updateDeduction(@Param("paymentId") Long paymentId,
|
||||||
|
@Param("deductionAmount") BigDecimal deductionAmount,
|
||||||
|
@Param("netAmount") BigDecimal netAmount);
|
||||||
|
|
||||||
List<PaymentVO> selectPendingByMonthAndBank(@Param("settleMonth") String settleMonth,
|
List<PaymentVO> selectPendingByMonthAndBank(@Param("settleMonth") String settleMonth,
|
||||||
@Param("bankCode") String bankCode);
|
@Param("bankCode") String bankCode);
|
||||||
|
List<PaymentVO> selectBySettleMonth(@Param("settleMonth") String settleMonth);
|
||||||
|
List<PaymentVO> selectByAgentAndMonth(@Param("agentId") Long agentId,
|
||||||
|
@Param("settleMonth") String settleMonth);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ga.core.mapstruct;
|
||||||
|
|
||||||
|
import com.ga.core.vo.product.ContractSaveReq;
|
||||||
|
import com.ga.core.vo.product.ContractVO;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
import org.mapstruct.MappingTarget;
|
||||||
|
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
public interface ContractMapStruct {
|
||||||
|
|
||||||
|
@Mapping(target = "contractId", ignore = true)
|
||||||
|
@Mapping(target = "status", ignore = true)
|
||||||
|
@Mapping(target = "lapseDate", ignore = true)
|
||||||
|
@Mapping(target = "revivalDate", ignore = true)
|
||||||
|
@Mapping(target = "createdAt", ignore = true)
|
||||||
|
@Mapping(target = "updatedAt", ignore = true)
|
||||||
|
ContractVO toVO(ContractSaveReq req);
|
||||||
|
|
||||||
|
@Mapping(target = "contractId", ignore = true)
|
||||||
|
@Mapping(target = "status", ignore = true)
|
||||||
|
@Mapping(target = "lapseDate", ignore = true)
|
||||||
|
@Mapping(target = "revivalDate", ignore = true)
|
||||||
|
@Mapping(target = "createdAt", ignore = true)
|
||||||
|
@Mapping(target = "updatedAt", ignore = true)
|
||||||
|
void updateVO(ContractSaveReq req, @MappingTarget ContractVO vo);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ga.core.mapstruct;
|
||||||
|
|
||||||
|
import com.ga.core.vo.ledger.ExceptionLedgerSaveReq;
|
||||||
|
import com.ga.core.vo.ledger.ExceptionLedgerVO;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
public interface ExceptionLedgerMapStruct {
|
||||||
|
|
||||||
|
@Mapping(target = "ledgerId", ignore = true)
|
||||||
|
@Mapping(target = "contractId", ignore = true)
|
||||||
|
@Mapping(target = "commissionYear", ignore = true)
|
||||||
|
@Mapping(target = "taxAmount", ignore = true)
|
||||||
|
@Mapping(target = "approveStatus", ignore = true)
|
||||||
|
@Mapping(target = "approverId", ignore = true)
|
||||||
|
@Mapping(target = "approveDate", ignore = true)
|
||||||
|
@Mapping(target = "recurringLeft", ignore = true)
|
||||||
|
@Mapping(target = "status", ignore = true)
|
||||||
|
@Mapping(target = "version", ignore = true)
|
||||||
|
@Mapping(target = "createdAt", ignore = true)
|
||||||
|
@Mapping(target = "updatedAt", ignore = true)
|
||||||
|
ExceptionLedgerVO toVO(ExceptionLedgerSaveReq req);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.ga.core.mapstruct;
|
||||||
|
|
||||||
|
import com.ga.core.vo.user.UserSaveReq;
|
||||||
|
import com.ga.core.vo.user.UserVO;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
import org.mapstruct.MappingTarget;
|
||||||
|
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
public interface UserMapStruct {
|
||||||
|
|
||||||
|
@Mapping(target = "userId", ignore = true)
|
||||||
|
@Mapping(target = "password", ignore = true)
|
||||||
|
@Mapping(target = "failCount", ignore = true)
|
||||||
|
@Mapping(target = "lastLoginAt", ignore = true)
|
||||||
|
@Mapping(target = "pwdChangedAt", ignore = true)
|
||||||
|
@Mapping(target = "createdAt", ignore = true)
|
||||||
|
@Mapping(target = "createdBy", ignore = true)
|
||||||
|
@Mapping(target = "updatedAt", ignore = true)
|
||||||
|
@Mapping(target = "updatedBy", ignore = true)
|
||||||
|
UserVO toVO(UserSaveReq req);
|
||||||
|
|
||||||
|
@Mapping(target = "userId", ignore = true)
|
||||||
|
@Mapping(target = "loginId", ignore = true)
|
||||||
|
@Mapping(target = "password", ignore = true)
|
||||||
|
@Mapping(target = "failCount", ignore = true)
|
||||||
|
@Mapping(target = "lastLoginAt", ignore = true)
|
||||||
|
@Mapping(target = "pwdChangedAt", ignore = true)
|
||||||
|
@Mapping(target = "createdAt", ignore = true)
|
||||||
|
@Mapping(target = "createdBy", ignore = true)
|
||||||
|
@Mapping(target = "updatedAt", ignore = true)
|
||||||
|
@Mapping(target = "updatedBy", ignore = true)
|
||||||
|
void updateVO(UserSaveReq req, @MappingTarget UserVO vo);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ga.core.vo.chargeback;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ChargebackGradeResp {
|
||||||
|
private Long gradeId;
|
||||||
|
private String productCategory;
|
||||||
|
private String productCategoryName;
|
||||||
|
private Integer monthsFrom;
|
||||||
|
private Integer monthsTo;
|
||||||
|
private String monthsRange; // "1~12개월", "85개월 이상" 형태 표시용
|
||||||
|
private BigDecimal chargebackPercent;
|
||||||
|
private LocalDate effectiveFrom;
|
||||||
|
private LocalDate effectiveTo;
|
||||||
|
private Boolean isActive;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.ga.core.vo.chargeback;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ChargebackGradeSaveReq {
|
||||||
|
|
||||||
|
private String productCategory; // NULL = 전체 상품
|
||||||
|
|
||||||
|
@NotNull(message = "경과월 시작은 필수입니다")
|
||||||
|
@Min(value = 0, message = "경과월 시작은 0 이상이어야 합니다")
|
||||||
|
private Integer monthsFrom;
|
||||||
|
|
||||||
|
private Integer monthsTo; // NULL = 무한대
|
||||||
|
|
||||||
|
@NotNull(message = "환수율은 필수입니다")
|
||||||
|
@Min(value = 0, message = "환수율은 0 이상이어야 합니다")
|
||||||
|
private BigDecimal chargebackPercent;
|
||||||
|
|
||||||
|
@NotNull(message = "적용 시작일은 필수입니다")
|
||||||
|
private LocalDate effectiveFrom;
|
||||||
|
|
||||||
|
private LocalDate effectiveTo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ga.core.vo.chargeback;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ChargebackGradeSearchParam extends SearchParam {
|
||||||
|
private String productCategory;
|
||||||
|
private Integer monthsElapsed; // 해당 경과월에 해당하는 구간 필터
|
||||||
|
private String effectiveDate; // YYYY-MM-DD — 해당 날짜 유효 구간 필터
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ga.core.vo.chargeback;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ChargebackGradeVO {
|
||||||
|
private Long gradeId;
|
||||||
|
private String productCategory; // NULL = ALL 상품
|
||||||
|
private Integer monthsFrom; // 해약 경과월 시작 (이상)
|
||||||
|
private Integer monthsTo; // 해약 경과월 종료 (이하), NULL = 무한대
|
||||||
|
private BigDecimal chargebackPercent;
|
||||||
|
private LocalDate effectiveFrom;
|
||||||
|
private LocalDate effectiveTo; // NULL = 현재 유효
|
||||||
|
private Boolean isActive;
|
||||||
|
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private Long updatedBy;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ga.core.vo.installment;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class InstallmentPlanResp {
|
||||||
|
private Long planId;
|
||||||
|
private Long contractId;
|
||||||
|
private String policyNo;
|
||||||
|
private String productName;
|
||||||
|
private Long agentId;
|
||||||
|
private String agentName;
|
||||||
|
private Integer planYear;
|
||||||
|
private Integer planMonth;
|
||||||
|
private String settleMonth;
|
||||||
|
private BigDecimal expectedAmount;
|
||||||
|
private BigDecimal paidAmount;
|
||||||
|
private LocalDateTime paidAt;
|
||||||
|
private Long paymentId;
|
||||||
|
private String status;
|
||||||
|
private String statusName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ga.core.vo.installment;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class InstallmentPlanSearchParam extends SearchParam {
|
||||||
|
private Long contractId;
|
||||||
|
private String settleMonth;
|
||||||
|
private String status;
|
||||||
|
private Integer planYear;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ga.core.vo.installment;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class InstallmentPlanVO {
|
||||||
|
private Long planId;
|
||||||
|
private Long contractId;
|
||||||
|
private Integer planYear; // 1~7
|
||||||
|
private Integer planMonth; // 1~12
|
||||||
|
private String settleMonth; // YYYYMM
|
||||||
|
private BigDecimal expectedAmount;
|
||||||
|
private BigDecimal paidAmount;
|
||||||
|
private LocalDateTime paidAt;
|
||||||
|
private Long paymentId;
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private Long updatedBy;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ga.core.vo.installment;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class InstallmentRatioResp {
|
||||||
|
private Long ratioId;
|
||||||
|
private String productCategory;
|
||||||
|
private String productCategoryName;
|
||||||
|
private Integer planYear;
|
||||||
|
private BigDecimal ratioPercent;
|
||||||
|
private LocalDate effectiveFrom;
|
||||||
|
private LocalDate effectiveTo;
|
||||||
|
private Boolean isActive;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.ga.core.vo.installment;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class InstallmentRatioSaveReq {
|
||||||
|
|
||||||
|
private String productCategory; // NULL = 전체 상품
|
||||||
|
|
||||||
|
@NotNull(message = "분급 연차는 필수입니다")
|
||||||
|
@Min(value = 1, message = "분급 연차는 1 이상이어야 합니다")
|
||||||
|
@Max(value = 7, message = "분급 연차는 7 이하여야 합니다")
|
||||||
|
private Integer planYear;
|
||||||
|
|
||||||
|
@NotNull(message = "비율은 필수입니다")
|
||||||
|
@Positive(message = "비율은 양수여야 합니다")
|
||||||
|
private BigDecimal ratioPercent;
|
||||||
|
|
||||||
|
@NotNull(message = "적용 시작일은 필수입니다")
|
||||||
|
private LocalDate effectiveFrom;
|
||||||
|
|
||||||
|
private LocalDate effectiveTo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ga.core.vo.installment;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class InstallmentRatioSearchParam extends SearchParam {
|
||||||
|
private String productCategory;
|
||||||
|
private Integer planYear;
|
||||||
|
private String effectiveDate; // YYYY-MM-DD — 해당 날짜 유효 비율 필터
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user