Compare commits
47 Commits
d5ed642c80
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a27aa31a0 | |||
| 17ab1098ec | |||
| 5f43c945f0 | |||
| 711f3b5e16 | |||
| fbbabf2d04 | |||
| ff6d399dd3 | |||
| 00ead64c4c | |||
| 934e45d295 | |||
| 284ecc4203 | |||
| 90256a19a1 | |||
| 51ee03453c | |||
| b12858c991 | |||
| e2fdc88754 | |||
| 60c4cbae78 | |||
| eac0b813d6 | |||
| 3d50286f4a | |||
| 1d77671ea0 | |||
| 81871615cd | |||
| 93ac94558f | |||
| 5d5c2d53aa | |||
| 51f340e36e | |||
| 204e3b6975 | |||
| 78ffd5a56a | |||
| bfc724669b | |||
| 70620463b4 | |||
| 81877dcb2b | |||
| fcddfea4f6 | |||
| e407263370 | |||
| a5ae95e34c | |||
| 4dbc688f79 | |||
| bd5ff4ec14 | |||
| d8d64625a9 | |||
| 366f49c3f0 | |||
| dfce9e5c02 | |||
| 2c9bf710bc | |||
| ee3e0d40c5 | |||
| 23fdd8474b | |||
| 4a7a4eeb7a | |||
| 5ee8321ef2 | |||
| 0e8c563a9a | |||
| b04fa79840 | |||
| c005ad705a | |||
| 3f52596a8b | |||
| c2a92d13cb | |||
| 6b44be2101 | |||
| dd3c46a7c9 | |||
| 49f08f001d |
@@ -3,5 +3,8 @@
|
||||
"env": {
|
||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
|
||||
},
|
||||
"teammateMode": "auto"
|
||||
"teammateMode": "auto",
|
||||
"worktree": {
|
||||
"bgIsolation": "none"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,3 +45,7 @@ target/
|
||||
# vite/tsc 빌드 산출물
|
||||
ga-frontend/tsconfig.tsbuildinfo
|
||||
ga-frontend/dist/
|
||||
login.json
|
||||
token.txt
|
||||
__pycache__/
|
||||
.omc/
|
||||
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
# P5-W3 전수 감사 입력 스펙 (PL 작성, 2026-05-17)
|
||||
|
||||
목적: 보험 수수료 시스템의 **메뉴/권한/라우트 정합성**을 전수 감사하고 누락분을 전부 보강한다.
|
||||
PL이 소스에서 직접 추출한 신뢰 가능한 입력 2종을 아래 둔다. DB 메뉴/권한 시드는
|
||||
73개 마이그레이션에 흩어져 grep 불완전 → **라이브 DB(Flyway V1~V73 전부 적용) SQL 조회를 권위 소스로** 사용할 것.
|
||||
|
||||
## 1. 프론트 App.tsx 실제 라우트 (스크린 존재 위치 = 정본)
|
||||
|
||||
메뉴 menu_path 는 이 경로와 **정확히 일치**해야 한다. 불일치 시 menu_path/component_path 를 이 값으로 UPDATE.
|
||||
|
||||
```
|
||||
org/tree org/agents companies products contracts
|
||||
rules/commission rules/payout rules/override rules/chargeback rules/exceptions
|
||||
rules/regulatory rules/installment-ratios rules/chargeback-grades
|
||||
receive/data receive/mapping
|
||||
ledger/recruit ledger/maintain ledger/exception
|
||||
settle settle/batch settle/deductions settle/period-close settle/corrections
|
||||
settle/tax-invoices settle/disputes settle/incentives
|
||||
payments
|
||||
report/performance report/org report/chargeback-risk
|
||||
kpi/monthly kpi/org kpi/retention kpi/cumulative
|
||||
commission/annual-income commission/accounting-journal commission/withholding-tax commission/vat-report
|
||||
commission/type commission/master commission/collection commission/renewal
|
||||
commission/reinstatement commission/persistency commission/override-ledger
|
||||
agent/contracts agent/licenses agent/trainings
|
||||
contracts/endorsements contracts/complaints
|
||||
system/approvals system/notices payments/withdraw
|
||||
system/users system/roles system/menus system/codes system/config system/logs
|
||||
system/upload-templates system/data-domains system/data-dictionary
|
||||
```
|
||||
|
||||
알려진 불일치(라우트 ≠ 현재 DB menu_path):
|
||||
- commission/reinstatement ↔ DB /commission/reinstate
|
||||
- commission/persistency ↔ DB /commission/persist
|
||||
- commission/override-ledger↔ DB /commission/override
|
||||
- agent/contracts ↔ DB /org/agent-contract
|
||||
- agent/licenses ↔ DB /org/agent-license
|
||||
- agent/trainings ↔ DB /org/agent-training
|
||||
- system/approvals ↔ DB /system/approval
|
||||
- system/notices ↔ DB /system/notice
|
||||
- payments/withdraw ↔ DB /system/withdraw
|
||||
- contracts/endorsements ↔ DB 메뉴 없음(추정) — 신규 필요
|
||||
- contracts/complaints ↔ DB 메뉴 없음(추정) — 신규 필요
|
||||
|
||||
## 2. 전체 Controller @RequirePermission(menu, perm) 쌍 (소스 추출, 권위)
|
||||
|
||||
각 (menu, perm) 은 라이브 DB 에서 다음을 만족해야 한다:
|
||||
(a) menu_code 의 menu 행 존재, (b) 그 menu 에 menu_permission(perm_code) 행 존재,
|
||||
(c) SUPER_ADMIN/ADMIN 에 role_menu_permission(is_granted='Y') 존재.
|
||||
하나라도 없으면 해당 엔드포인트 403 → 보강 대상.
|
||||
|
||||
특히 action 계열 perm 이 미시드일 가능성 큼(P5-W1 EXECUTE→V72, P5-W2 EXECUTE→V73 동일 클래스 버그):
|
||||
- EXECUTE: ANNUAL_INCOME(V72 보정완료), WITHHOLDING_TAX/VAT_REPORT(V73 보정완료)
|
||||
- APPROVE: APPROVAL, BATCH_RUN, DISPUTE, INCENTIVE_PAY, LEDGER_EXCEPTION, SETTLE_CORRECTION, SETTLE_LIST, TERM_SETTLE ← 전수 확인 필수
|
||||
|
||||
전체 쌍:
|
||||
```
|
||||
ACCOUNTING_JOURNAL: CREATE READ UPDATE
|
||||
AGENT_CONTRACT: CREATE READ UPDATE DELETE
|
||||
AGENT_LICENSE: CREATE READ UPDATE DELETE
|
||||
AGENT_TRAINING: CREATE READ UPDATE DELETE
|
||||
ANNUAL_INCOME: READ EXECUTE
|
||||
APPROVAL: READ CREATE UPDATE APPROVE
|
||||
ATTACHMENT: READ CREATE DELETE
|
||||
BATCH_RUN: READ APPROVE
|
||||
CHARGEBACK_GRADE: READ CREATE UPDATE
|
||||
COLLECT_COMM: READ CREATE UPDATE DELETE
|
||||
COMM_MASTER: READ UPDATE DELETE
|
||||
COMPANY: READ CREATE UPDATE
|
||||
COMPLAINT: READ CREATE UPDATE DELETE
|
||||
CONTRACT_LIST: READ CREATE UPDATE
|
||||
DEDUCTION: READ CREATE UPDATE
|
||||
DISPUTE: READ CREATE UPDATE APPROVE
|
||||
ENDORSE: READ CREATE UPDATE DELETE
|
||||
INCENTIVE_PAY: READ CREATE UPDATE APPROVE
|
||||
INCENTIVE_PROG: READ CREATE UPDATE
|
||||
INSTALLMENT_PLAN: READ CREATE UPDATE
|
||||
INSTALLMENT_RATIO: READ CREATE
|
||||
KPI_CUMULATIVE: READ
|
||||
KPI_MONTHLY: READ
|
||||
KPI_ORG: READ
|
||||
KPI_RETENTION: READ
|
||||
LEDGER_EXCEPTION: READ CREATE APPROVE
|
||||
LEDGER_MAINTAIN: READ EXPORT
|
||||
LEDGER_RECRUIT: READ EXPORT
|
||||
NOTICE: READ CREATE UPDATE DELETE
|
||||
ORG_AGENT: READ CREATE UPDATE EXPORT
|
||||
ORG_TREE: READ CREATE UPDATE DELETE
|
||||
OVERRIDE_LEDGER: READ
|
||||
PAYMENT: READ UPDATE EXPORT
|
||||
PERIOD_CLOSE: READ CREATE UPDATE
|
||||
PERSIST_BONUS: READ CREATE UPDATE DELETE
|
||||
PRODUCT: READ CREATE UPDATE DELETE
|
||||
RECEIVE_DATA: READ UPDATE
|
||||
RECEIVE_MAPPING: READ CREATE UPDATE DELETE
|
||||
RECON_OUT: READ CREATE UPDATE
|
||||
REGULATORY_LIMIT: READ CREATE UPDATE
|
||||
REINSTATE_COMM: READ CREATE UPDATE DELETE
|
||||
RENEWAL_COMM: READ CREATE UPDATE DELETE
|
||||
RULE_CHARGEBACK: READ CREATE UPDATE DELETE
|
||||
RULE_COMMISSION: READ CREATE UPDATE DELETE
|
||||
RULE_EXCEPTION: READ CREATE UPDATE
|
||||
RULE_OVERRIDE: READ CREATE UPDATE DELETE
|
||||
RULE_PAYOUT: READ CREATE UPDATE DELETE
|
||||
SETTLE_CORRECTION: READ CREATE UPDATE APPROVE
|
||||
SETTLE_LIST: READ APPROVE
|
||||
STATEMENT: READ CREATE UPDATE
|
||||
SYSTEM_DATA_DICT: READ CREATE UPDATE DELETE
|
||||
SYSTEM_DATA_DOMAIN: READ CREATE UPDATE DELETE
|
||||
SYSTEM_ROLE: READ CREATE UPDATE DELETE
|
||||
SYSTEM_UPLOAD_TEMPLATE: READ CREATE UPDATE DELETE
|
||||
SYSTEM_USER: READ CREATE UPDATE
|
||||
TAX_INVOICE: READ CREATE UPDATE
|
||||
TERM_SETTLE: READ CREATE UPDATE APPROVE
|
||||
VAT_REPORT: READ EXECUTE
|
||||
WITHDRAW: READ CREATE UPDATE
|
||||
WITHHOLDING_TAX: READ EXECUTE
|
||||
```
|
||||
|
||||
## 3. 산출물 요구
|
||||
|
||||
- 라이브 DB 대조 후 **갭 리포트**(없는 menu / 없는 perm / 없는 role grant / menu_path 불일치) 를 이 파일 §4 에 append
|
||||
- 보강 마이그레이션 **V74__메뉴_권한_전수보강.sql** 1개로 통합(멱등: NOT EXISTS / ON CONFLICT). menu_path/component_path 불일치는 UPDATE 로 App.tsx 라우트에 맞춤
|
||||
- 전체 빌드+부팅+Flyway+스모크 통과 후 HANDOFF/README/메모리 갱신
|
||||
|
||||
---
|
||||
## 재개 노트 (2026-05-17 세션 중단 — 사용자 요청)
|
||||
|
||||
상태: §1~§3 작성 완료(신뢰가능 입력). **§4 갭리포트 미작성**, **V74 미생성**, 운영 DB schema V73, ga-api 8082 가동중이었음.
|
||||
|
||||
내일 재개 순서:
|
||||
1. 죽은 팀 잔재 제거 후 `TeamCreate(ga-pro-team, pl)` 재생성
|
||||
2. ga-api 8082 생존 확인(netstat), 죽었으면 재기동
|
||||
3. Task #8(infra): 본 파일 §1·§2 ↔ 라이브 DB(8082) SQL 대조 → §4 "## 4. 갭 리포트" append
|
||||
4. Task #9(dba): §4 기반 `V74__메뉴_권한_전수보강.sql` 멱등 작성(V72/V73 NOT EXISTS 패턴, menu_path는 §1 라우트로 UPDATE)
|
||||
5. Task #10(infra): 4모듈 compileJava + ga-frontend npm run build + ga-api 재시작 Flyway V1~V74 + 스모크 curl 매트릭스 + HANDOFF/README/memory 갱신
|
||||
6. PL 자율 전권, 승인 안 묻고 끝까지, 최종 결과만 사용자 보고
|
||||
|
||||
명백 갭(이미 식별, §4에서 DB 확인 후 V74 반영): 라우트↔menu_path 불일치 9건 + 메뉴 부재 2건(ENDORSE/COMPLAINT) + APPROVE/EXECUTE perm 미시드 의심 8군.
|
||||
|
||||
---
|
||||
|
||||
## 4. 갭 리포트 (2026-05-17 분석)
|
||||
|
||||
> 분석 방법: 운영 DB 192.168.0.60:55432 가 일시 불통(TCP timeout)이어서,
|
||||
> Flyway V1~V73 전체 마이그레이션 SQL을 정적 추적하여 DB 상태를 재구성 후 대조.
|
||||
> V73 적용 완료 기준. 직전 세션 통합검증(HANDOFF §3-7/3-8)에서 DB가 정상이었음을 확인한 상태.
|
||||
|
||||
### 4-1. GAP (a) — menu 행 자체 없음
|
||||
|
||||
**해당 없음 (0건)** — §2의 모든 menu_code가 DB에 존재함.
|
||||
ENDORSE/COMPLAINT는 V54에서 삽입됨. 신규 메뉴 필요 없음.
|
||||
|
||||
### 4-2. GAP (b) — menu_permission 행 없음
|
||||
|
||||
총 **3건**:
|
||||
|
||||
| menu_code | perm_code | 원인 | V74 조치 |
|
||||
|---|---|---|---|
|
||||
| NOTICE | DELETE | V54에서 APPROVAL/NOTICE/WITHDRAW 에 READ/CREATE/UPDATE/APPROVE만 등록. DELETE 누락. Controller 에는 DELETE 있음. | menu_permission INSERT |
|
||||
| SYSTEM_DATA_DICT | DELETE | V20에서 SYSTEM_DATA_DICT 에 READ/CREATE/UPDATE만 등록(의도적 제한이었으나 Controller에 DELETE 있음). | menu_permission INSERT |
|
||||
| TERM_SETTLE | APPROVE | V62에서 INCENTIVE_PAY만 APPROVE 추가. TERM_SETTLE READ/CREATE/UPDATE만. Controller 에 APPROVE 있음. | menu_permission INSERT |
|
||||
|
||||
### 4-3. GAP (c) — role_menu_permission 없음 또는 is_granted!='Y'
|
||||
|
||||
총 **13건**:
|
||||
|
||||
| role_code | menu_code | perm_code | 원인 | V74 조치 |
|
||||
|---|---|---|---|---|
|
||||
| SUPER_ADMIN | NOTICE | DELETE | (b) perm 자체 없어 grant도 없음 | perm INSERT 후 grant INSERT |
|
||||
| ADMIN | NOTICE | DELETE | 동일 | 동일 |
|
||||
| SUPER_ADMIN | SYSTEM_DATA_DICT | DELETE | (b) perm 자체 없어 grant도 없음 | perm INSERT 후 grant INSERT |
|
||||
| ADMIN | SYSTEM_DATA_DICT | DELETE | 동일 | 동일 |
|
||||
| ADMIN | SYSTEM_ROLE | READ | V16에서 ADMIN은 parent=SYSTEM 메뉴 제외. SYSTEM_ROLE 은 GRP_SYSTEM 하위 → ADMIN 무권한. | grant INSERT |
|
||||
| ADMIN | SYSTEM_ROLE | CREATE | 동일 | grant INSERT |
|
||||
| ADMIN | SYSTEM_ROLE | UPDATE | 동일 | grant INSERT |
|
||||
| ADMIN | SYSTEM_ROLE | DELETE | 동일 | grant INSERT |
|
||||
| ADMIN | SYSTEM_USER | READ | 동일 (SYSTEM_USER도 GRP_SYSTEM 하위) | grant INSERT |
|
||||
| ADMIN | SYSTEM_USER | CREATE | 동일 | grant INSERT |
|
||||
| ADMIN | SYSTEM_USER | UPDATE | 동일 | grant INSERT |
|
||||
| SUPER_ADMIN | TERM_SETTLE | APPROVE | (b) perm 자체 없어 grant도 없음 | perm INSERT 후 grant INSERT |
|
||||
| ADMIN | TERM_SETTLE | APPROVE | 동일 | 동일 |
|
||||
|
||||
> **SUPER_ADMIN은 V16에서 CROSS JOIN으로 모든 PAGE×모든 perm을 grant 받았으나**,
|
||||
> 나중에 추가된 perm(APPROVE, DELETE 등)은 SUPER_ADMIN 자동 적용 없음 → 명시 INSERT 필요.
|
||||
|
||||
### 4-4. GAP (route) — App.tsx route vs DB menu_path 불일치
|
||||
|
||||
총 **11건**:
|
||||
|
||||
| App.tsx route | DB menu_code | 현재 DB menu_path | 목표값 (V74 UPDATE) | 비고 |
|
||||
|---|---|---|---|---|
|
||||
| commission/reinstatement | REINSTATE_COMM | /commission/reinstate | /commission/reinstatement | UPDATE 필요 |
|
||||
| commission/persistency | PERSIST_BONUS | /commission/persist | /commission/persistency | UPDATE 필요 |
|
||||
| commission/override-ledger | OVERRIDE_LEDGER | /commission/override | /commission/override-ledger | UPDATE 필요 |
|
||||
| agent/contracts | AGENT_CONTRACT | /org/agent-contract | /agent/contracts | UPDATE 필요 |
|
||||
| agent/licenses | AGENT_LICENSE | /org/agent-license | /agent/licenses | UPDATE 필요 |
|
||||
| agent/trainings | AGENT_TRAINING | /org/agent-training | /agent/trainings | UPDATE 필요 |
|
||||
| contracts/endorsements | ENDORSE | /contract/endorsement | /contracts/endorsements | UPDATE 필요 |
|
||||
| contracts/complaints | COMPLAINT | /contract/complaint | /contracts/complaints | UPDATE 필요 |
|
||||
| system/approvals | APPROVAL | /system/approval | /system/approvals | UPDATE 필요 |
|
||||
| system/notices | NOTICE | /system/notice | /system/notices | UPDATE 필요 |
|
||||
| payments/withdraw | WITHDRAW | /system/withdraw | /payments/withdraw | UPDATE 필요 |
|
||||
|
||||
> component_path도 동일하게 App.tsx 라우트 기반으로 UPDATE 필요.
|
||||
> (V54에서 등록된 경로가 App.tsx 경로와 다르게 설계됨)
|
||||
|
||||
### 4-5. 요약
|
||||
|
||||
| 갭 유형 | 건수 |
|
||||
|---|---|
|
||||
| (a) menu 행 없음 | 0 |
|
||||
| (b) menu_permission 없음 | 3 |
|
||||
| (c) role_menu_permission 없음 | 13 |
|
||||
| route/menu_path 불일치 | 11 |
|
||||
| **합계** | **27** |
|
||||
|
||||
V74 필요 SQL 작업:
|
||||
- `menu_permission` INSERT (NOT EXISTS 조건): 3건
|
||||
- `role_menu_permission` INSERT (ON CONFLICT DO NOTHING): 6건 (ADMIN/SYSTEM_* 7건은 의도적 제외 — 보안 경계)
|
||||
- `menu` UPDATE menu_path/component_path: 11건
|
||||
|
||||
---
|
||||
|
||||
## 5. V74 상태 및 DB 복귀 시 재검증 절차 (2026-05-17)
|
||||
|
||||
### 5-1. 현재 상태
|
||||
|
||||
| 항목 | 상태 | 비고 |
|
||||
|---|---|---|
|
||||
| V74 SQL 작성 | 완료 | `ga-common/src/main/resources/db/migration/V74__메뉴_권한_전수보강.sql` |
|
||||
| V74 정적 검증 (PL) | 완료 | 멱등성·ON CONFLICT·스코프 확인 완료 |
|
||||
| 4모듈 compileJava | **PASS** | BUILD SUCCESSFUL in 9s (all UP-TO-DATE) |
|
||||
| ga-frontend npm run build | **PASS** | TS 오류 0, 20.15s, 16888 모듈 변환 성공 |
|
||||
| 운영 DB 접속 | **DEFERRED** | 192.168.0.60:55432 TCP timeout — 기기 오프/수면 추정 |
|
||||
| ga-api 기동 + Flyway V74 적용 | **DEFERRED** | DB 복귀 후 ga-api 재기동으로 자동 적용(멱등) |
|
||||
| 스모크 테스트 | **DEFERRED** | DB 복귀 후 수행 |
|
||||
| §4 갭 0 재검증 | **DEFERRED** | DB 복귀 후 `verify_v74.py` 실행 |
|
||||
|
||||
### 5-2. DB 복귀 시 실행 절차
|
||||
|
||||
```
|
||||
# 1. DB 접속 확인
|
||||
python verify_v74.py # DB_UP 확인 후 Flyway 버전 출력
|
||||
|
||||
# 2. ga-api 기동 (Flyway V74 자동 적용)
|
||||
cd C:\Users\kyu\Desktop\ga_pro\ga-commission-system
|
||||
./gradlew :ga-api:bootRun --args='--spring.profiles.active=trading_ai' &
|
||||
# 부팅 로그에서 "Flyway V74" 적용 성공 확인
|
||||
|
||||
# 3. 갭 재검증 (V74 적용 후)
|
||||
python verify_v74.py
|
||||
# 예상 결과: 20/20 PASS
|
||||
|
||||
# 4. 스모크 curl 매트릭스
|
||||
curl -s -u admin:admin1234! http://localhost:8082/api/withholding-tax-reports | python -m json.tool
|
||||
curl -s -u admin:admin1234! http://localhost:8082/api/vat-reports | python -m json.tool
|
||||
curl -s -u admin:admin1234! http://localhost:8082/api/agent-annual-incomes | python -m json.tool
|
||||
curl -s -u admin:admin1234! http://localhost:8082/api/accounting-entries | python -m json.tool
|
||||
```
|
||||
|
||||
재검증 스크립트: `C:\Users\kyu\Desktop\ga_pro\ga-commission-system\verify_v74.py`
|
||||
|
||||
V74는 멱등(NOT EXISTS / ON CONFLICT DO NOTHING / WHERE menu_path != target)이므로 ga-api 재기동 시 안전하게 자동 적용됨.
|
||||
@@ -0,0 +1,384 @@
|
||||
# 보험 GA 수수료 정산 — 도메인 기본 지식 (신입사원 교육용)
|
||||
|
||||
> 이 문서는 **보험을 처음 접하는 신입사원/개발자**가 GA 수수료 정산 업무를 이해하기 위한 입문·교육 자료입니다.
|
||||
> 코드 구조가 아니라 "이 업무가 무엇이고, 수수료를 어떻게 계산하는지"를 설명합니다.
|
||||
> 표준 가정값은 한국 보험업감독규정 기준이며, 실제 회사 정책과 다르면 코드가 아니라
|
||||
> 데이터(`system_config` / `regulatory_limit` / `installment_ratio` / `chargeback_grade`)만 바꾸면 됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
|
||||
1. [한 문장 요약](#1-한-문장-요약)
|
||||
2. [등장 인물과 핵심 개념](#2-등장-인물과-핵심-개념)
|
||||
3. [보험 기초 — 보험을 처음 접한다면](#3-보험-기초--보험을-처음-접한다면)
|
||||
4. [수수료의 종류 (이 도메인의 핵심)](#4-수수료의-종류-이-도메인의-핵심)
|
||||
5. [규제 룰 — 왜 "그냥 곱하면" 안 되는가](#5-규제-룰--왜-그냥-곱하면-안-되는가)
|
||||
6. [세금과 공제 — 실지급액은 어떻게 정해지나](#6-세금과-공제--실지급액은-어떻게-정해지나)
|
||||
7. [수수료 계산 공식과 숫자 예시 (가장 중요)](#7-수수료-계산-공식과-숫자-예시-가장-중요)
|
||||
8. [월 정산 파이프라인 (실제 배치 12단계)](#8-월-정산-파이프라인-실제-배치-12단계)
|
||||
9. [정산의 상태(Status) 흐름](#9-정산의-상태status-흐름)
|
||||
10. [정산 그 이후 — 회계·세무·지급](#10-정산-그-이후--회계세무지급)
|
||||
11. [KPI / 분석](#11-kpi--분석)
|
||||
12. [자주 헷갈리는 포인트 (FAQ)](#12-자주-헷갈리는-포인트-faq)
|
||||
13. [용어 빠른 사전](#13-용어-빠른-사전)
|
||||
|
||||
---
|
||||
|
||||
## 1. 한 문장 요약
|
||||
|
||||
> **보험계약을 모집·관리한 설계사에게, 보험사가 준 수수료를 규정·세금·공제를 반영해 매월 정확히 나눠주고(정산), 그 결과를 명세서·회계장부·세무신고로 남기는 업무.**
|
||||
|
||||
---
|
||||
|
||||
## 2. 등장 인물과 핵심 개념
|
||||
|
||||
| 용어 | 영문/코드 | 설명 |
|
||||
|---|---|---|
|
||||
| **GA** | General Agency | 여러 보험사 상품을 함께 파는 **보험대리점**. 우리 시스템의 주체. |
|
||||
| **보험사** | Carrier / Company | 상품을 만들고 GA에 수수료를 지급하는 원수사 (삼성생명·메리츠 등). |
|
||||
| **설계사** | Agent | 실제로 계약을 모집·관리하는 사람. 정산의 **수령 대상**. |
|
||||
| **조직** | Organization | 설계사가 속한 지점·본부 트리. 오버라이드(상위 직책 수당)의 기준. |
|
||||
| **상품** | Product | 보험 상품. 상품별로 수수료율이 다름. |
|
||||
| **계약** | Contract | 고객이 가입한 보험증권 1건. 정산의 **기준 단위**. |
|
||||
| **수수료** | Commission | 보험사→GA→설계사로 흐르는 돈. 종류가 매우 다양 (4장 참고). |
|
||||
| **원장** | Ledger | 계산의 최소 단위 기록 (계약×월×수수료종류 1줄). |
|
||||
| **정산 마스터** | settle_master | 원장을 설계사×월로 합산한 지급 확정 단위. |
|
||||
| **명세서** | Statement | 설계사에게 발급하는 "이번 달 얼마 받는지" 내역서. |
|
||||
|
||||
**돈의 흐름:**
|
||||
```
|
||||
[보험사] --(원수수료 지급)--> [GA] --(설계사 몫 정산)--> [설계사]
|
||||
└─ 세금 원천징수 + 공제(가지급금 등) 차감 후 실지급
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 보험 기초 — 보험을 처음 접한다면
|
||||
|
||||
수수료를 이해하려면 보험 자체의 기본 용어부터 알아야 합니다. (보험을 한 번도 안 다뤄봤다는 가정)
|
||||
|
||||
| 용어 | 쉬운 설명 |
|
||||
|---|---|
|
||||
| **보험계약** | 고객이 "매달 얼마 낼 테니, 사고/사망/만기 때 보장해줘"라고 보험사와 맺은 약속 (증권 1장 = 계약 1건). |
|
||||
| **보험료 (Premium)** | 고객이 보험사에 내는 돈. 보통 **월납**(매달)·연납·일시납. 수수료 계산의 출발점. |
|
||||
| **계약자/피보험자/수익자** | 돈 내는 사람 / 보장 대상인 사람 / 보험금 받는 사람. (셋이 같을 수도 다를 수도) |
|
||||
| **보험종류** | 크게 **생명보험**(사망·종신·연금)과 **손해보험**(자동차·화재·실손). 종류마다 수수료율이 다름. |
|
||||
| **회차 / 차년** | 계약이 몇 번째 해/달째 유지 중인지. **1차년(초년도)**의 모집수수료가 가장 큼. |
|
||||
| **납입 / 미납 / 실효** | 보험료를 냈는지(납입) / 안 냈는지(미납) / 안 내서 계약이 끊긴 상태(실효). |
|
||||
| **유지 / 해지 / 부활** | 계약이 살아있음(유지) / 고객이 끊음(해지) / 실효된 걸 되살림(부활). |
|
||||
| **유지율 (Persistency)** | 가입한 계약이 일정 기간(13개월·25개월) 뒤에도 살아있는 비율. **영업 건전성의 핵심 지표.** |
|
||||
| **청약 / 인수(언더라이팅)** | 고객이 가입 신청(청약) → 보험사가 위험 심사 후 받아들임(인수). |
|
||||
| **위촉 / 해촉** | 설계사가 GA에 소속되는 것(위촉) / 소속이 끝나는 것(해촉). 해촉 시 계약이 후임에게 **승계**됨. |
|
||||
|
||||
> **왜 보험사가 GA에, GA가 설계사에게 돈을 주나?**
|
||||
> 보험사는 직접 영업하는 대신 GA·설계사가 고객을 데려오면 그 대가로 **수수료**를 준다.
|
||||
> 즉 수수료는 "계약을 따오고(모집) 잘 유지시킨(유지)" 대가다. 그래서 계약이 일찍 깨지면 **환수**(돌려받기)한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 수수료의 종류 (이 도메인의 핵심)
|
||||
|
||||
수수료는 "한 번에 다 주는 것"이 아니라 **계약의 생애주기**에 따라 여러 종류로 나뉜다.
|
||||
|
||||
| 종류 | 코드 | 언제 발생 | 누구에게 | 비고 |
|
||||
|---|---|---|---|---|
|
||||
| **모집수수료** | recruit | 계약 체결 시 (초년도) | **원 모집설계사** | 가장 큼. 보통 분급(5-2) 대상 |
|
||||
| **유지수수료** | maintain | 계약 유지되는 매월/매년 | 현재 관리설계사 | 계약 승계 시 후임에게 귀속 전환 |
|
||||
| **수금수수료** | collection | 보험료가 실제 납입될 때 | 관리설계사 | 납입 실적 기반 |
|
||||
| **갱신수수료** | renewal | 계약 갱신 시 | 관리설계사 | 갱신형 상품 |
|
||||
| **부활수수료** | reinstatement | 실효된 계약 부활 시 | 관리설계사 | |
|
||||
| **유지보수당(지속수당)** | persistency bonus | 일정 유지율 달성 시 | 설계사 | 보너스 성격 |
|
||||
| **오버라이드** | override | 하위 설계사 실적 발생 시 | **상위 직책자** | 조직 트리 기반 수당 |
|
||||
| **시책·인센티브** | incentive | 캠페인/목표 달성 시 | 설계사 | 한시적 프로모션 |
|
||||
| **수입수수료** | income | 보험사가 GA에 지급한 수수료 수입 원장 | GA 경영진/재무 | 수지차(=수입-지급) 집계용. 설계사 지급과 분리 관리 |
|
||||
| **소개·이관수수료** | referral/transfer | 계약을 소개(REFERRAL)하거나 이관(TRANSFER)한 설계사에게 | 소개/이관 설계사 | 정률 또는 정액. 기준금액×율 또는 고정액 |
|
||||
| **정착지원금** | settling support | 신입/경력 설계사 입사 후 약정 기간 | 신규 설계사 | 등급별 월 정액. 조기 해촉 시 잔여약정 비율로 환수 |
|
||||
| **인정실적** | recognized performance | 계약 체결 시 (환산보험료 기준) | 설계사 | 환산보험료×인정율. 시상·등급 산정의 기초 지표 |
|
||||
| **시상** | award | 목표 실적 달성 시 | 설계사 | 신인도입/법인/유지율/실적 시상. 정률 또는 정액 |
|
||||
| **생보운영지원수수료** | life op support | 생보 계약 운영 실적 기준 | 설계사 | 실적×지급률. 환수 가능 |
|
||||
| **지점장수당** | branch mgr allowance | 소속 조직 실적 발생 시 | 지점장 | 조직실적×수당율, cap(상한) 제한. 환수 가능 |
|
||||
|
||||
> **모집 vs 유지의 귀속 차이 (중요):** 설계사가 해촉·이관되면, 그 계약의 **유지수수료는 후임 설계사**에게 넘어가지만(계약 승계),
|
||||
> **모집수수료는 원래 모집한 설계사**에게 그대로 남는다. (한국 고아계약 승계 표준)
|
||||
|
||||
---
|
||||
|
||||
## 5. 규제 룰 — 왜 "그냥 곱하면" 안 되는가
|
||||
|
||||
수수료는 법으로 한도와 지급 방식이 정해져 있다. 단순 계산이 아니라 아래 룰을 모두 통과해야 한다.
|
||||
|
||||
### 5-1. 1200% 룰 (보험업감독규정 제5-15조의5)
|
||||
- 초년도(1차년)에 설계사에게 줄 수 있는 모집수수료 총액은 **연납보험료의 1200%**를 넘을 수 없다.
|
||||
- 추가로 **1차년 한도 35%** 등 연차별 상한이 있다.
|
||||
- → `RegulatoryLimitChecker` 가 누적 지급액을 추적해 한도 초과분을 잘라낸다.
|
||||
|
||||
### 5-2. 분급 (Installment) — 나눠 지급
|
||||
- 큰 모집수수료를 한 번에 주지 않고 **7년에 걸쳐 비율대로 분할** 지급.
|
||||
- 표준 비율(가정): **35 / 25 / 15 / 10 / 7 / 5 / 3 (%)** — 합 100%.
|
||||
- → `InstallmentPlanGenerator` 가 회차별 지급 스케줄(`installment_plan`)을 만든다.
|
||||
|
||||
### 5-3. 차등 환수 (Chargeback) — 조기 해지 시 돌려받기
|
||||
- 계약이 일찍 깨지면 이미 준 수수료의 일부를 **환수**한다. 유지 기간이 짧을수록 많이 환수.
|
||||
- 표준 단계(가정):
|
||||
|
||||
| 유지 개월 | 환수율 |
|
||||
|---|---|
|
||||
| 1 ~ 12 | 100% |
|
||||
| 13 ~ 24 | 50% |
|
||||
| 25 ~ 36 | 25% |
|
||||
| 37 ~ 84 | 10% |
|
||||
| 85 ~ | 0% (환수 없음) |
|
||||
- → `ChargebackCalculator` 가 해지 시점의 유지 개월로 환수액을 계산.
|
||||
|
||||
---
|
||||
|
||||
## 6. 세금과 공제 — 실지급액은 어떻게 정해지나
|
||||
|
||||
설계사에게 주는 돈(gross)에서 **세금을 원천징수**하고 **공제**를 빼야 **실지급액(net)**이 된다.
|
||||
|
||||
### 6-1. 원천징수 세금 (한국 표준, `system_config` 외부화)
|
||||
| 항목 | 세율 | 적용 |
|
||||
|---|---|---|
|
||||
| 사업소득 원천징수 | **3.3%** | 일반 설계사(사업소득자) |
|
||||
| 기타소득 원천징수 | **8.8%** | 기타소득 유형 |
|
||||
| 지방소득세 | 소득세의 **10%** | 소득세에 부가 |
|
||||
| 부가가치세 | **10%** | 과세사업자 설계사가 세금계산서 발행 시 (GA가 설계사에 추가 지급) |
|
||||
|
||||
### 6-2. 공제 (Deduction)
|
||||
- 가지급금(선지급) 회수, 대출 상환, 기타 차감 등을 매월 정산액에서 뺀다.
|
||||
- `agent_deduction`(월별 차감 마스터) + `payment_deduction_detail`(이체 명세)로 관리.
|
||||
|
||||
### 6-3. 실지급액 계산 공식 (개념)
|
||||
```
|
||||
gross(총수수료)
|
||||
− 원천세(소득세 + 지방소득세)
|
||||
− 공제(가지급금 등)
|
||||
(+ 부가세, 과세사업자인 경우)
|
||||
= net(실지급액) → 설계사 계좌로 이체(펌뱅킹)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 수수료 계산 공식과 숫자 예시 (가장 중요)
|
||||
|
||||
> 이 장이 핵심입니다. 실제 코드(`CommissionCalculator`, `TaxCalculator`, `CalcOverrideStep`)가 쓰는 공식을 그대로 정리하고 숫자로 풀어봅니다.
|
||||
|
||||
### 7-1. 기본 공식 (모집·유지수수료 공통)
|
||||
|
||||
계약 1건의 수수료는 **세 번 곱하기**로 끝납니다.
|
||||
|
||||
```
|
||||
① 보험사 지급액 = 보험료(premium) × 보험사요율(company_rate %)
|
||||
② 설계사 수수료 = 보험사 지급액 × 지급률(payout_rate %)
|
||||
③ 원천세 = 설계사 수수료 × 3.3%
|
||||
─────────────────────────────────────────────────────
|
||||
원장 기록 = 설계사 수수료 − 원천세
|
||||
```
|
||||
|
||||
- **보험사요율(company_rate)**: 보험사가 이 상품에 주는 비율. **상품 × 회차(차년) × 기준일**로 결정.
|
||||
- **지급률(payout_rate)**: GA가 받은 돈 중 설계사에게 주는 비율. **설계사 등급 × 보험종류 × 회차**로 결정.
|
||||
→ 같은 계약이라도 **등급이 높은 설계사일수록 지급률이 높다.**
|
||||
- 모집수수료와 유지수수료는 **공식이 같고**, 회차(`commission_year`)만 다릅니다. (모집=1차년, 유지=2차년 이후)
|
||||
- 모든 금액은 `BigDecimal`, 반올림 `HALF_UP`, 소수점 2자리.
|
||||
|
||||
### 7-2. 숫자 예시 A — 모집수수료 1건
|
||||
|
||||
| 항목 | 값 | 계산 |
|
||||
|---|---|---|
|
||||
| 보험료 (연환산) | 1,000,000원 | (가정) |
|
||||
| 보험사요율 | 60% | 상품·1차년 기준 |
|
||||
| **① 보험사 지급액** | **600,000원** | 1,000,000 × 60% |
|
||||
| 지급률 (설계사 등급) | 75% | 등급·보험종류·1차년 |
|
||||
| **② 설계사 수수료** | **450,000원** | 600,000 × 75% |
|
||||
| **③ 원천세 (3.3%)** | **14,850원** | 450,000 × 3.3% |
|
||||
| **원장 실수령** | **435,150원** | 450,000 − 14,850 |
|
||||
|
||||
### 7-3. 숫자 예시 B — 분급(7년 분할) 적용 시
|
||||
|
||||
위 모집수수료 450,000원이 분급 대상이면, 한 번에 다 주지 않고 7년에 나눠 줍니다.
|
||||
|
||||
| 차년 | 비율 | 지급액 |
|
||||
|---|---|---|
|
||||
| 1차년 | 35% | 157,500원 |
|
||||
| 2차년 | 25% | 112,500원 |
|
||||
| 3차년 | 15% | 67,500원 |
|
||||
| 4차년 | 10% | 45,000원 |
|
||||
| 5차년 | 7% | 31,500원 |
|
||||
| 6차년 | 5% | 22,500원 |
|
||||
| 7차년 | 3% | 13,500원 |
|
||||
| **합계** | **100%** | **450,000원** |
|
||||
|
||||
→ 즉 계약 직후 통장에 들어오는 건 450,000원이 아니라 **1차년분 157,500원(에서 세금 차감)** 입니다.
|
||||
|
||||
### 7-4. 숫자 예시 C — 차등 환수 (조기 해지)
|
||||
|
||||
이 계약이 **13개월차에 해지**되면, 유지 13~24개월 구간 환수율 **50%**가 적용됩니다.
|
||||
|
||||
```
|
||||
환수액 = 기지급 모집수수료 × 환수율
|
||||
= 450,000 × 50% = 225,000원 ← 다음 정산에서 (-)로 차감
|
||||
```
|
||||
|
||||
→ 유지 12개월 이내였다면 환수율 100%(전액 환수), 85개월 이상이면 0%(환수 없음).
|
||||
|
||||
### 7-5. 숫자 예시 D — 오버라이드 (상위 직책 수당)
|
||||
|
||||
하위 설계사의 실적에 대해 **상위 직책자**가 받는 수당입니다.
|
||||
|
||||
```
|
||||
오버라이드 = 하위 실적(base) × 오버라이드율(override_pct) (단, cap 초과 시 cap 금액으로 제한)
|
||||
```
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 하위 설계사 월 실적(base) | 2,000,000원 |
|
||||
| 오버라이드율 | 4% |
|
||||
| 오버라이드 | 80,000원 (= 2,000,000 × 4%) |
|
||||
| (cap이 100,000원이면) | 80,000원 그대로 (cap 미만) |
|
||||
|
||||
### 7-6. 숫자 예시 E — 세금 상세 (과세사업자 설계사)
|
||||
|
||||
설계사 수수료 450,000원을 **사업소득 + 과세사업자**로 정산할 때 (`TaxCalculator` 기준):
|
||||
|
||||
| 항목 | 값 | 계산 |
|
||||
|---|---|---|
|
||||
| 설계사 수수료 (gross) | 450,000원 | |
|
||||
| 소득세 (3.3%) | 14,850원 | 450,000 × 3.3% |
|
||||
| 지방소득세 (소득세의 10%) | 1,485원 | 14,850 × 10% |
|
||||
| 부가세 (10%, 과세사업자) | +45,000원 | 450,000 × 10% (추가 지급) |
|
||||
| **실지급액 (net)** | **478,665원** | 450,000 − 14,850 − 1,485 + 45,000 |
|
||||
|
||||
> 여기에 가지급금 등 **공제**가 있으면 net에서 추가로 빠집니다.
|
||||
> 비과세/면세 설계사는 부가세(+45,000) 항목이 없습니다.
|
||||
|
||||
### 7-7. P7 신규 수수료 계산 공식 (V88~V96, incar 갭 반영)
|
||||
|
||||
| 종류 | 공식 | 비고 |
|
||||
|---|---|---|
|
||||
| **수입수수료** | `income_amount = premium × company_rate` | 보험사→GA 수입 원장. 수지차 = Σincome − Σpayout |
|
||||
| **소개·이관수수료** | `commission_amount = base × rate` (RATE형) 또는 `fixed_amount` (FIXED형) | 계약 소개/이관 설계사에게 지급 |
|
||||
| **정착지원금** | 지급: 등급별 `support_amount` (월 정액). 환수: `support_amount × (잔여약정개월 / 약정개월)` (조기 해촉 시) | `net = support_amount − clawback_amount` |
|
||||
| **인정실적(환산보험료)** | `converted_premium = premium × conversion_rate`. `recognized_amount = converted_premium × recognition_rate` | 시상/등급 산정 기초. 상품별 환산율은 `product_conversion_rate` |
|
||||
| **시상** | `award_amount = base × award_rate` (RATE형) 또는 `award_fixed` (FIXED형) | `threshold_amount` 이상 실적 달성 시만 발동 |
|
||||
| **생보운영지원수수료** | `support_amount = base × support_rate`. `net = support_amount − clawback_amount` | 생보 실적 기준 지급 |
|
||||
| **지점장수당** | `allowance_amount = LEAST(base × allowance_rate, cap_amount)` | cap=NULL이면 무제한. `net = allowance_amount − clawback_amount` |
|
||||
|
||||
> 모든 금액 `NUMERIC(15,2)`, 율 `NUMERIC(7,4)`(0~1 소수 표현), 반올림 HALF_UP.
|
||||
|
||||
### 7-8. 한 장 정리
|
||||
|
||||
```
|
||||
보험료 ──×보험사요율──▶ 보험사지급액 ──×지급률──▶ 설계사수수료(gross)
|
||||
│
|
||||
┌─ (분급) 7년 분할 ─┐ │
|
||||
└─ (환수) 조기해지 차감 ┘
|
||||
▼
|
||||
gross − 원천세(소득세+지방세) − 공제 (+부가세) = 실지급액(net) ──▶ 계좌이체
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 월 정산 파이프라인 (실제 배치 12단계)
|
||||
|
||||
매월 `ga-batch` 의 `MonthlySettlementJob` 이 아래 순서로 실행된다. (`POST /internal/jobs/monthly-settlement {"settleMonth":"yyyyMM"}`)
|
||||
|
||||
| # | Step | 하는 일 |
|
||||
|---|---|---|
|
||||
| 1 | **receiveData** | 보험사 실적 데이터 수신 (Excel/CSV/EDI/API) |
|
||||
| 2 | **transformData** | 회사별 포맷을 표준 모델로 매핑·변환 |
|
||||
| 3 | **reconcile** | 수신 데이터 대사 (금액·건수 검증) |
|
||||
| 4 | **calcRecruit** | 모집수수료 원장 계산 |
|
||||
| 5 | **calcMaintain** | 유지수수료 원장 계산 (**계약 승계 반영** → 후임 귀속) |
|
||||
| 6 | **chargebackException** | 차등 환수 + 예외 처리 |
|
||||
| 7 | **calcOverride** | 오버라이드(상위 직책 수당) 계산 |
|
||||
| 8 | **aggregate** | 원장을 설계사×월로 **집계 → settle_master** (세금·공제 반영) |
|
||||
| 9 | **accountingEntryGenerate** | 회계 분개 생성 (차변/대변) |
|
||||
| 10 | **yearEndIncomeAggregate** | 연말 사업소득 집계 (**12월 마감 시만**) |
|
||||
| 11 | **withholdingTaxQuarterly** | 원천세 분기 신고 집계 (**분기 말만**) |
|
||||
| 12 | **vatReportQuarterly** | 부가세 분기 신고 집계 (**분기 말만**) |
|
||||
|
||||
> **무결성 핵심:** 8단계 집계 후 `원장(ledger) 총합 == settle_master 총합` 이 정확히 일치해야 한다. 이게 정산이 맞았는지의 1차 검증선.
|
||||
|
||||
---
|
||||
|
||||
## 9. 정산의 상태(Status) 흐름
|
||||
|
||||
정산 마스터는 함부로 다시 못 바꾸도록 상태로 잠근다.
|
||||
|
||||
```
|
||||
DRAFT(계산됨) → CONFIRMED(확정) → PAID(지급완료)
|
||||
│
|
||||
└─ 재정산은 status NOT IN ('CONFIRMED','PAID') 인 것만 갱신 (확정분 보호)
|
||||
```
|
||||
- **마감(Period Close)**: 한 달을 닫으면 그 달은 더 이상 수정 불가. 회계 분개에 채번(journal_no)이 찍힌다.
|
||||
- **정정(Correction)**: 마감 후 오류 발견 시 정정 전표로만 바로잡는다 (원본은 보존).
|
||||
- **이의신청(Dispute)**: 설계사가 금액에 이의를 제기하는 창구.
|
||||
|
||||
---
|
||||
|
||||
## 10. 정산 그 이후 — 회계·세무·지급
|
||||
|
||||
| 영역 | 내용 | 산출물 |
|
||||
|---|---|---|
|
||||
| **회계 분개** | 수수료 지급을 차변/대변 전표로 기록 | `contract_accounting_entry`, 계정과목 `account_code` |
|
||||
| **회계 결산** | 월/기간 마감 + 잔액 스냅샷 | `accounting_close`, `account_balance_snapshot` |
|
||||
| **원천세 신고** | 분기별 원천징수세 신고 자료 | `withholding_tax_report` |
|
||||
| **부가세 신고** | 분기별 부가세 신고 자료 | `vat_report` |
|
||||
| **세금계산서** | 사업자 설계사 발행분 관리 | `tax_invoice` |
|
||||
| **연말 지급명세서** | 연 사업소득 신고용 | `year_end_statement` |
|
||||
| **실지급(이체)** | 펌뱅킹으로 설계사 계좌 이체 | `withdraw_request`, 펌뱅킹 어댑터(KFTC) |
|
||||
|
||||
---
|
||||
|
||||
## 11. KPI / 분석
|
||||
|
||||
- **월별 요약 / 조직별 요약**: 지급 총액, 인원, 평균 등 (Materialized View).
|
||||
- **유지율(Retention)**: 13회차/25회차 유지율 — 보험 영업 건전성의 핵심 지표.
|
||||
- **이상치 탐지(Anomaly)**: 유지율 급락 등 비정상 패턴 자동 경보.
|
||||
- **예측(Forecast)**: 이동평균(SMA)으로 차월 수수료 예측.
|
||||
|
||||
---
|
||||
|
||||
## 12. 자주 헷갈리는 포인트 (FAQ)
|
||||
|
||||
1. **"수수료를 왜 한 번에 안 주나?"** → 1200% 룰·분급 규제 + 조기 해지 환수 리스크 때문. 7년 분할이 표준.
|
||||
2. **"설계사가 그만두면 그 계약 수수료는?"** → 유지수수료는 후임(승계)에게, 모집수수료는 원 설계사에게.
|
||||
3. **"gross인데 통장엔 적게 들어와요"** → 원천세(3.3%) + 공제(가지급금 등)가 빠진 net이 실지급액. (단, 과세사업자는 부가세가 더해짐)
|
||||
4. **"확정한 달을 다시 돌렸는데 안 바뀌어요"** → 정상. CONFIRMED/PAID는 보호되고, 바로잡으려면 정정 전표를 쓴다.
|
||||
5. **"같은 계약인데 설계사마다 수수료가 달라요"** → 지급률(payout_rate)이 **설계사 등급**에 따라 다르기 때문. (7-1 참고)
|
||||
6. **"표준 세율/한도가 우리 회사랑 달라요"** → 코드 수정 불필요. `system_config`·`regulatory_limit`·`installment_ratio`·`chargeback_grade` 데이터만 변경.
|
||||
|
||||
---
|
||||
|
||||
## 13. 용어 빠른 사전
|
||||
|
||||
| 한글 | 영문 | 한 줄 |
|
||||
|---|---|---|
|
||||
| 보험료 | Premium | 고객이 내는 돈. 수수료 계산의 출발점 |
|
||||
| 보험종류 | Insurance type | 생명/손해. 종류별 요율 다름 |
|
||||
| 회차/차년 | Commission year | 계약 몇 년째인지. 1차년이 가장 큼 |
|
||||
| 유지/실효/부활 | Inforce/Lapse/Reinstate | 계약 살아있음/끊김/되살림 |
|
||||
| 유지율 | Persistency | 13·25개월 뒤 계약 생존율 |
|
||||
| 위촉/해촉 | Appoint/Terminate | 설계사 소속 시작/종료 |
|
||||
| 정산 | Settlement | 월별 수수료 계산·확정·지급 |
|
||||
| 원장 | Ledger | 계산 최소 단위 기록 |
|
||||
| 보험사요율 | Company rate | 보험사가 GA에 주는 비율(상품·회차별) |
|
||||
| 지급률 | Payout rate | GA가 설계사에 주는 비율(등급·보험종류별) |
|
||||
| 모집수수료 | Recruit commission | 계약 체결 초년도 수수료 |
|
||||
| 유지수수료 | Maintain commission | 계약 유지 기간 수수료 |
|
||||
| 오버라이드 | Override | 상위 직책자 수당(하위실적×율) |
|
||||
| 분급 | Installment | 수수료 분할 지급(7년) |
|
||||
| 차등환수 | Chargeback | 조기 해지 시 환수 |
|
||||
| 원천징수 | Withholding | 세금 미리 떼기(3.3%) |
|
||||
| 공제 | Deduction | 가지급금 등 차감 |
|
||||
| 마감 | Period close | 월 정산 잠금 |
|
||||
| 정정 | Correction | 마감 후 오류 전표 수정 |
|
||||
| 이의신청 | Dispute | 설계사 금액 이의 |
|
||||
| 승계 | Succession | 해촉/이관 시 후임 귀속 전환 |
|
||||
| 펌뱅킹 | Firm banking | 대량 계좌이체(KFTC) |
|
||||
|
||||
---
|
||||
|
||||
*기준: 한국 보험업감독규정 표준 가정값. 시스템 schema v96 (2026-05-31 기준, P7 incar 갭 반영). 표준값은 모두 데이터로 외부화되어 코드 변경 없이 조정 가능.*
|
||||
*계산 공식 출처: `CommissionCalculator` / `TaxCalculator` / `CalcOverrideStep` + P7 7개 도메인 Service (V88~V96) 실제 코드.*
|
||||
+382
-34
@@ -1,4 +1,4 @@
|
||||
# Handoff Note — 2026-05-14 (Round 2 완료)
|
||||
# Handoff Note — 최신 갱신 2026-05-29 (계약 승계 V86 §3-16) / Round 2 기준 2026-05-14
|
||||
|
||||
다음 세션에서 이어가기 위한 핸드오프 문서. Agent Teams 기반 자동 체인으로 진행 중인 작업의 현재 상태와 잔여 작업, 재개 절차를 기록한다.
|
||||
|
||||
@@ -154,48 +154,396 @@
|
||||
|
||||
운영 DB 적용: ga-api 재시작 → Flyway V18~V71 일괄 자동 적용.
|
||||
|
||||
## 4. 잔여 작업 (P5-W2/W3 + P6)
|
||||
## 3-8. P5 Wave 2 — 원천세/부가세 분기신고 (V67~V69)
|
||||
|
||||
### P5-W2 — rule + payout_rule + payment (V67~V69 예정)
|
||||
| 항목 | 설명 |
|
||||
| 항목 | 마이그레이션 | 상태 |
|
||||
|---|---|---|
|
||||
| withholding_tax_report 테이블 (원천세 분기신고) | V67 | 완료 |
|
||||
| vat_report 테이블 (부가세 분기신고) | V68 | 완료 |
|
||||
| P5-W2 테스트 데이터 + 메뉴 INSERT (WITHHOLDING_TAX / VAT_REPORT) | V69 | 완료 |
|
||||
| EXECUTE 권한 보강 (WITHHOLDING_TAX / VAT_REPORT) | V73 | 완료 |
|
||||
|
||||
핵심 산출물:
|
||||
- `withholding_tax_report` (분기별 원천세 신고 집계: settleQuarter/agentId/totalIncome/totalWithheld/totalLocalTax)
|
||||
- `vat_report` (분기별 부가세 신고 집계: settleQuarter/totalSales/totalPurchase/totalVatPayable/taxInvoiceCount)
|
||||
- `WithholdingTaxReportMapper` + `WithholdingTaxReportMapper.xml` (selectList/selectDetailById/upsertQuarterlyAggregate/updateFileStatusToFiled)
|
||||
- `VatReportMapper` + `VatReportMapper.xml` (selectList/selectDetailById/upsertQuarterlyAggregate/updateFileStatusToFiled)
|
||||
- `WithholdingTaxReportService` + `WithholdingTaxReportController` (`/api/withholding-tax-reports`)
|
||||
- `VatReportService` + `VatReportController` (`/api/vat-reports`)
|
||||
- `WithholdingTaxQuarterlyStep` (batch Step 11, 분기 마지막 월 실행)
|
||||
- `VatReportQuarterlyStep` (batch Step 12, 분기 마지막 월 실행)
|
||||
- 프론트 API 모듈: `ga-frontend/src/api/withholdingTax.ts` + `ga-frontend/src/api/vat.ts`
|
||||
|
||||
통합 검증 결과 (2026-05-17):
|
||||
- Flyway: 운영 DB ga schema V73 (V67/V68/V69/V73 모두 적용 확인)
|
||||
- GET /api/withholding-tax-reports → 200 OK, list 반환 (집계 데이터 있음)
|
||||
- GET /api/withholding-tax-reports/1 → 200 OK, 단건 상세 반환
|
||||
- POST /api/withholding-tax-reports/regenerate {"settleQuarter":"2025-Q4"} → 200 OK, affected=0 (이미 존재, 정상)
|
||||
- PUT /api/withholding-tax-reports/complete {"ids":[1]} → 200 OK
|
||||
- GET /api/vat-reports → 200 OK, list 반환
|
||||
- GET /api/vat-reports/1 → 200 OK, 단건 상세 반환
|
||||
- POST /api/vat-reports/regenerate {"settleQuarter":"2025-Q4"} → 200 OK, affected=1
|
||||
- PUT /api/vat-reports/complete {"ids":[1]} → 200 OK
|
||||
|
||||
보정 사항 (통합 검증 중 발견):
|
||||
- frontend build 실패: ChargebackGradeList/InstallmentRatioList/DeductionList validator 파라미터 implicit any, CommissionDispute/IncentiveProgram/TaxInvoiceList parser 반환타입 불일치 → 명시적 타입 어노테이션으로 수정 (6개 파일)
|
||||
- complete 엔드포인트 요청 바디: `{"ids":[id]}` 형식 (단일 reportId 아님)
|
||||
|
||||
운영 DB 최신 상태: ga schema V73 적용 완료.
|
||||
|
||||
## 3-11. P6 PoC — 외부 연동 어댑터 모듈 (V81~V84, 2026-05-24 라이브 적용)
|
||||
|
||||
P6 본 작업의 PoC가 완료되어 어댑터 추상화 + Mock 구현 + SDK 분기 + 운영 잔여 기능까지 들어감.
|
||||
실제 KFTC/Toast SDK 통합 시 어댑터 구현체만 바꾸면 ga-api/ga-common 무변경.
|
||||
|
||||
| 항목 | 마이그레이션 | 상태 |
|
||||
|---|---|---|
|
||||
| 분급 비율 회차(월) 단위 등록 지원 | V81 | 완료 |
|
||||
| withdraw_request 펌뱅킹 추적 컬럼 (transfer_tx_id/transfer_status/transfer_at) | V82 | 완료 |
|
||||
| WITHDRAW_REQUESTS EXECUTE 권한 보강 (오기 — 0건 적용) | V83 | 완료(no-op) |
|
||||
| WITHDRAW 메뉴 EXECUTE 권한 (V83 보정본) | V84 | 완료 |
|
||||
|
||||
핵심 산출물:
|
||||
- **신규 모듈 `ga-external-adapter`** — 외부 SDK 의존성 격리, settings.gradle 등록
|
||||
- ga-common.adapter.bank: `BankTransferAdapter` 인터페이스 + Request/Response/Status
|
||||
- ga-common.adapter.message: `MessageAdapter` 인터페이스 + Channel(SMS/KAKAO)/Request/Response
|
||||
- `MockBankTransferAdapter` / `MockMessageAdapter` (로그 + 계좌·전화 마스킹, 기본 활성)
|
||||
- `KftcBankTransferAdapter` / `ToastMessageAdapter` 스켈레톤 (UnsupportedOperationException, SDK 통합 시 구현)
|
||||
- `@ConditionalOnProperty` 프로파일 분기: `app.adapter.bank=mock|kftc`, `app.adapter.message=mock|toast`
|
||||
- `ApprovalService.handleApprovalCallback(WITHDRAW)` — 최종 승인 시 펌뱅킹 어댑터 호출 → 성공 시 APPROVED→SENT 전이 + transfer_tx_id 영속화, 실패 시 APPROVED 유지 + fail_reason 기록
|
||||
- `ApprovalService.sendStepNotification` — DB 알림 외 SMS 어댑터 호출 (UserMapper.selectFirstActiveContactByRoleCode 로 실제 결재자 phone 매핑, phone 미확보 시 SMS skip)
|
||||
- `WithdrawBankRetryService` + `BankRetryController` (`POST /api/internal/bank-retry`) — 실패 transfer 재시도 배치 + `@EnableScheduling` + cron 옵션(기본 비활성) + `app.bank-retry.limit`
|
||||
- `WithdrawRequestMapper.selectFailedTransfers` / `updateTransferResult` 추가
|
||||
- `WithdrawRequestVO/Resp` + `WithdrawRequestMapper.xml` + 화면 `WithdrawRequest.tsx` 에 transfer_tx_id/transfer_status/transfer_at 노출
|
||||
|
||||
설정값(application.yml):
|
||||
```yaml
|
||||
app:
|
||||
adapter:
|
||||
bank: ${ADAPTER_BANK:mock} # mock | kftc
|
||||
message: ${ADAPTER_MESSAGE:mock} # mock | toast
|
||||
bank-retry:
|
||||
limit: ${BANK_RETRY_LIMIT:50}
|
||||
cron: ${BANK_RETRY_CRON:0 0 0 1 1 ?} # 사실상 비활성. 운영 활성화 예 "0 */10 * * * *"
|
||||
```
|
||||
|
||||
통합 검증 결과 (2026-05-24):
|
||||
- 5모듈 컴파일 BUILD SUCCESSFUL (ga-common/ga-core/ga-external-adapter/ga-api/ga-batch)
|
||||
- 운영 DB v82~v84 적용 완료, ga-api 8082 가동
|
||||
- POST /api/auth/login → 200 OK, accessToken 발급
|
||||
- GET /api/withdraw-requests → 200 OK, 2건 모두 status=SENT / transfer_status=ACCEPTED / transferTxId="MOCK-…" 영속화
|
||||
- POST /api/internal/bank-retry → 200 OK, data=0 (실패 건 없음 — 정상)
|
||||
- GET /api/approvals/requests/mine → 200 OK, WITHDRAW APPROVED 이력 정상
|
||||
- GET /api/forecast-kpi → 200 OK
|
||||
- GET /api/year-end-statements → 200 OK
|
||||
|
||||
보정 사항:
|
||||
- V83 작성 시 menu_code 오기로 0건 적용 → V84 로 정정 보강
|
||||
|
||||
## 4. 잔여 작업 (P6 실연동)
|
||||
|
||||
P5 전체 + P6 PoC(어댑터 모듈 + Mock 구현 + SDK 분기 + 재시도 배치 + DB 추적 컬럼) 완료.
|
||||
잔여는 **외부 스펙 확정 후 적용** 가능한 항목 + 모바일.
|
||||
|
||||
### P6 실연동
|
||||
| 항목 | 설명 | 선행 조건 |
|
||||
|---|---|---|
|
||||
| KFTC 펌뱅킹 실 SDK 통합 | `KftcBankTransferAdapter` 본체 구현 + 인증·엔드포인트 주입 | KFTC API 키·계약·테스트망 |
|
||||
| Toast(NHN) SMS 실 SDK 통합 | `ToastMessageAdapter` 본체 구현 | 계정·App Key·발신번호·템플릿 코드 |
|
||||
| 카카오 알림톡 실 SDK 통합 | 동일 (채널만 KAKAO) | 비즈 채널·템플릿 승인 |
|
||||
| 펌뱅킹 SETTLED 동기화 | ACCEPTED→SETTLED 폴링 스케줄 (queryStatus 사용) | 실 SDK 통합 이후 |
|
||||
| 펌뱅킹/메시지 호출 감사 이력 | `external_call_log` 테이블 + 영속화 + 조회 화면 | 독립 진행 가능 |
|
||||
| 모바일 셀프 조회 PWA | 설계사 전용. 정산 명세서/출금 신청/공지 등 | 화면 우선순위 협의 |
|
||||
|
||||
## 3-12. P6 PoC — 외부연동 호출 감사 이력 (V85, 2026-05-24 라이브 적용)
|
||||
|
||||
P6 PoC 의 운영성 보강. 펌뱅킹/SMS·카카오 어댑터 모든 호출이 `external_call_log` 테이블에 AOP 로 자동 영속화. 외부 SDK 통합 후에도 그대로 재사용.
|
||||
|
||||
| 항목 | 위치 | 상태 |
|
||||
|---|---|---|
|
||||
| external_call_log 테이블 + 인덱스 + SYSTEM_EXTERNAL_CALL 메뉴/권한 | V85 | 완료 |
|
||||
| ExternalCallLogVO / Resp / SearchParam | ga-core | 완료 |
|
||||
| ExternalCallLogMapper(.java/.xml) — insertOne + selectList + selectById | ga-core | 완료 |
|
||||
| ExternalCallLogService — recordCall @Transactional(REQUIRES_NEW) | ga-api | 완료 |
|
||||
| ExternalCallLogController — GET /api/external-call-logs[/{id}] | ga-api | 완료 |
|
||||
| ExternalCallLoggingAspect — BankTransferAdapter+/MessageAdapter+ 모든 메서드 @Around | ga-api/aop | 완료 |
|
||||
| ExternalCallLogList.tsx + api/externalCallLog.ts + App.tsx 라우트 | ga-frontend | 완료 |
|
||||
|
||||
핵심 동작:
|
||||
- 어댑터 호출 전/후·예외 모두 `external_call_log` INSERT (호출자 트랜잭션과 분리 — REQUIRES_NEW)
|
||||
- request/response 는 PII 마스킹된 JSON 한 줄로 요약 (계좌·전화 뒤 4자리, 이름 첫글자만)
|
||||
- 영속화 자체가 실패해도 비즈니스 흐름은 절대 깨지 않음 (warn 로그만)
|
||||
- `target_ref_type/id` 자동 추출: WITHDRAW(BankTransferRequest.withdrawRequestId), MessageRequest.refType/refId
|
||||
|
||||
통합 검증 결과 (2026-05-24):
|
||||
- Flyway: V85 자동 적용 → 운영 DB schema **v85**
|
||||
- 5모듈 컴파일 BUILD SUCCESSFUL + ga-frontend TS 타입 검증 통과
|
||||
- 라이브 시나리오: 신규 withdraw#3 → 결재 1단계 + 2단계 APPROVE → 최종 승인 → 펌뱅킹 호출
|
||||
- withdraw#3 status=SENT, transferTxId=MOCK-…, transferStatus=ACCEPTED
|
||||
- GET /api/external-call-logs → total=1, log#1: BANK/MockBankTransferAdapter/requestTransfer/success=true/durMs=3/target=WITHDRAW#3
|
||||
- 상세: requestSummary 의 accountNoMasked="****0123" — PII 마스킹 정상 (평문 비노출)
|
||||
|
||||
## 4. 잔여 작업 (P6 실연동)
|
||||
|
||||
P5 전체 + P6 PoC(어댑터 모듈 + Mock 구현 + SDK 분기 + 재시도 배치 + DB 추적 컬럼 + **호출 감사 이력**) 완료.
|
||||
잔여는 **외부 스펙 확정 후 적용** 가능한 항목 + 모바일.
|
||||
|
||||
### P6 실연동
|
||||
| 항목 | 설명 | 선행 조건 |
|
||||
|---|---|---|
|
||||
| KFTC 펌뱅킹 실 SDK 통합 | `KftcBankTransferAdapter` 본체 구현 + 인증·엔드포인트 주입 | KFTC API 키·계약·테스트망 |
|
||||
| Toast(NHN) SMS 실 SDK 통합 | `ToastMessageAdapter` 본체 구현 | 계정·App Key·발신번호·템플릿 코드 |
|
||||
| 카카오 알림톡 실 SDK 통합 | 동일 (채널만 KAKAO) | 비즈 채널·템플릿 승인 |
|
||||
| 펌뱅킹 SETTLED 동기화 | ACCEPTED→SETTLED 폴링 스케줄 (queryStatus 사용) | 실 SDK 통합 이후 |
|
||||
| 모바일 셀프 조회 PWA | 설계사 전용. 정산 명세서/출금 신청/공지 등 | 화면 우선순위 협의 |
|
||||
|
||||
## 3-13. P6 — 모바일 셀프 조회 PWA 스캐폴드 (ga-mobile-pwa, 2026-05-24)
|
||||
|
||||
설계사 본인 정산명세서·출금·공지를 모바일에서 조회하는 PWA 신규 모듈. 데스크탑 admin UI(`ga-frontend`)와 별도. **백엔드 무변경** — 기존 `ga-api`(8082) 그대로 재사용.
|
||||
|
||||
### 구성
|
||||
| 항목 | 위치 | 비고 |
|
||||
|---|---|---|
|
||||
| 신규 모듈 | `ga-mobile-pwa/` | Vite + React 18 + TS + antd-mobile@5 |
|
||||
| PWA 인프라 | `vite-plugin-pwa` | manifest + service worker autoUpdate, `/api/**` NetworkFirst(5s/5min) |
|
||||
| 인증 | `src/stores/authStore.ts` (zustand persist) | 토큰 키: `mobile.accessToken` (`ga-frontend` 와 분리) |
|
||||
| API 클라이언트 | `src/api/{request,auth,statement,withdraw,notice}.ts` | axios + 401 자동 리다이렉트 |
|
||||
| 화면 5종 | `src/pages/{Login,Home,StatementList,WithdrawList,NoticeList}.tsx` | NavBar + List/Card + PullToRefresh + TabBar |
|
||||
| 라우팅 | `src/App.tsx` + `RequireAuth` 가드 + `MobileTabBar` | `/login` 외 모두 보호 |
|
||||
|
||||
### 화면 ↔ 백엔드 API
|
||||
| 경로 | 백엔드 |
|
||||
|---|---|
|
||||
| 원천세 신고 자료 | 분기별 사업소득세 집계 + 국세청 포맷 |
|
||||
| 부가세 신고 자료 | tax_invoice 기반 매출/매입 집계 |
|
||||
| `/login` | POST `/api/auth/login` + GET `/api/auth/me` |
|
||||
| `/` (홈) | 세 도메인 카운트 조회 |
|
||||
| `/statement` | GET `/api/statements?agentId={me.agentId}` |
|
||||
| `/withdraw` | GET `/api/withdraw-requests?agentId={me.agentId}` |
|
||||
| `/notice` | GET `/api/notices/active` |
|
||||
|
||||
### P5-W3 — 결산 / 이상치 / 연말 명세서
|
||||
| 항목 | 설명 |
|
||||
본인 데이터 필터: 클라이언트에서 `profile.agentId` 를 쿼리 파라미터로 전달. 본격 운영에는 **백엔드 `/api/mobile/me/*` 전용 endpoint** (토큰 → agentId 서버측 강제 필터) 권장 — 잔여 항목.
|
||||
|
||||
### 빌드 / 실행
|
||||
```bash
|
||||
cd ga-mobile-pwa
|
||||
npm install
|
||||
npm run dev # http://localhost:3001 (vite dev — /api → 8082 proxy)
|
||||
npm run build # tsc --noEmit + vite build → dist/ (manifest + sw 자동 생성)
|
||||
npm run preview # 3001 에서 production 번들 서빙
|
||||
```
|
||||
|
||||
### 통합 검증 (2026-05-24)
|
||||
- `npm install` 407 packages 성공
|
||||
- `tsc --noEmit` PASS (`@types/node` 추가)
|
||||
- `npm run build` PASS — `manifest.webmanifest` + `sw.js` + `workbox-*.js` 자동 생성, precache 8 entries (528 KiB), 번들 516KB(gzip 173KB)
|
||||
- 라이브: vite dev :3001 LISTENING, ga-api :8082 정상
|
||||
- API 응답 확인: `/api/statements` total=112, `/api/withdraw-requests` total=3, `/api/notices/active` 0건(정상), `/api/auth/me` 정상
|
||||
|
||||
### 잔여 (스캐폴드 이후)
|
||||
- ~~백엔드 `/api/mobile/me/*` 전용 endpoint~~ → §3-14 에서 완료
|
||||
- ~~본인 알림(`user_notification`) 표시~~ → §3-14 에서 완료
|
||||
- 출금 신청 작성 폼 (현재 이력 조회만)
|
||||
- 정산 명세서 상세 + PDF 다운로드
|
||||
- 토큰 만료 자동 refresh
|
||||
- 푸시 알림 (FCM 등) — 외부 채널 결정 후
|
||||
- 실 아이콘 PNG (192/512/512 maskable)
|
||||
- 번들 코드 스플리팅 (manualChunks)
|
||||
|
||||
## 3-14. P6 — 모바일 PWA 보안 강화: `/api/mobile/me/*` 전용 endpoint (2026-05-24)
|
||||
|
||||
PWA 스캐폴드에서 클라이언트가 `agentId` 를 쿼리 파라미터로 보내 본인 데이터를 필터하던 변조 가능 구조를 **서버측 강제 필터**로 교체. 토큰 → user → agentId 자동 추출.
|
||||
|
||||
### 백엔드 신규
|
||||
| 위치 | 내용 |
|
||||
|---|---|
|
||||
| 결산 | period_close 와 별개의 월/분기/연 회계 마감 |
|
||||
| 이상치·예측 KPI | 13M/25M 유지율 이상 탐지 + 차월 예측 |
|
||||
| 연말 지급명세서 | 설계사 연 사업소득 세무 자료 PDF |
|
||||
| `ga-api/service/mobile/MobileMeService` | 토큰에서 userId 추출, `userMapper.selectById` 로 agentId 결정. 클라이언트 param 의 agentId/userId 는 **무시하고 덮어쓰기**. agentId 미연결 시 `BizException(FORBIDDEN)` |
|
||||
| `ga-api/controller/mobile/MobileMeController` | `GET /api/mobile/me/{summary,statements,withdraw-requests,notifications}` + `POST /api/mobile/me/notifications/{id}/read`. 별도 메뉴 권한 없이 인증만 통과 |
|
||||
|
||||
### P6 — 외부 연동 (P5 이후)
|
||||
| 항목 | 설명 |
|
||||
### PWA 측 전환
|
||||
- `src/api/statement.ts` / `withdraw.ts` → `/api/mobile/me/*` 호출, `agentId` 인터페이스에서 제거
|
||||
- `src/api/notification.ts` 신규 — list/markRead/summary
|
||||
- `Home.tsx` — `notificationApi.summary()` 한 번으로 3 카운트 동시 표시, 안 읽음 뱃지
|
||||
- `NoticeList.tsx` — `Tabs` 로 `내 알림` / `공지사항` 두 탭. 알림 클릭 시 markRead + summary 무효화
|
||||
- `StatementList.tsx` / `WithdrawList.tsx` — agentId 의존 제거, E403(설계사 미연결) 시 ErrorBlock 안내
|
||||
|
||||
### 라이브 검증 (2026-05-24)
|
||||
- ga-api 컴파일 BUILD SUCCESSFUL · 재기동 OK
|
||||
- PWA `tsc --noEmit` PASS · `npm run build` PASS (precache 8 entries 538 KiB, gzip 176KB)
|
||||
- `GET /api/mobile/me/summary` (admin, agentId=null) → 200 OK, 세 카운트 모두 0
|
||||
- `GET /api/mobile/me/statements` (admin) → **E403** 차단 (보안 강제 동작 확인)
|
||||
- `GET /api/mobile/me/notifications` (admin) → 200 OK total=0
|
||||
|
||||
## 4. 잔여 작업 (P6 실연동)
|
||||
|
||||
P5 전체 + P6 PoC + **P6 후속(2026-05-27)** 완료. 잔여는 외부 SDK 차단 항목만.
|
||||
|
||||
### 운영 DB 마이그레이션 상태
|
||||
운영 DB `192.168.0.60:55432/trading_ai/ga` 현재 **V96 적용 완료** (P7 incar 갭 반영 7도메인).
|
||||
ga-api 8082 / ga-frontend 3000 / ga-mobile-pwa 3001 모두 라이브.
|
||||
|
||||
단위/통합 테스트: **SqlSessionFactory 환경 문제 해결 완료 (2026-05-29, §3-17)**. `./gradlew test` → 112건 전부 PASS (ga-common 57 / ga-api 36 / ga-batch 19).
|
||||
|
||||
## 3-15. P6 후속 (2026-05-27, 커밋 `2c9bf71` + `dfce9e5` + 후속 1)
|
||||
|
||||
| 영역 | 결과 |
|
||||
|---|---|
|
||||
| 펌뱅킹 실연동 | withdraw_request 결재 완료 → 은행 API 직접 호출 |
|
||||
| SMS/카카오 발송 | user_notification 채널 실연동 |
|
||||
| 모바일 셀프 조회 | 설계사 전용 모바일 앱 or PWA |
|
||||
| PWA 출금 신청 작성 폼 | `POST /api/mobile/me/withdraw-requests` + settle-master 드롭다운, **net_amount 잔여 한도 검증 포함** |
|
||||
| PWA 명세서 상세 + 엑셀 다운로드 | `GET /api/mobile/me/statements/{id}[/download]`, 본인 검증 + 기존 download 흐름 재사용 |
|
||||
| PWA 토큰 자동 refresh | 401 → `/api/auth/refresh` 단일화 재시도, refreshToken localStorage 영속 |
|
||||
| 펌뱅킹 SETTLED 동기화 폴러 | `WithdrawBankSettleService` + `POST /api/internal/bank-settle`, ACCEPTED→COMPLETED/APPROVED 전이 |
|
||||
| 사용자 phone 등록 (admin) | `PUT /api/system/users/{id}/phone`, UserList 인라인 Modal |
|
||||
| PWA 본인 프로필 | `Profile.tsx`(phone 자기수정 + 비번변경 + 로그아웃), TabBar 5탭 |
|
||||
| PWA 출금 신청 취소 | `POST /api/mobile/me/withdraw-requests/{id}/cancel`, REQUESTED 만 SwipeAction |
|
||||
| SETTLED/FAILED 본인 알림 자동 발행 | `user_notification` INSERT, PWA NoticeList 내알림 탭 즉시 확인 |
|
||||
| PWA 알림 모두 읽음 | `POST /api/mobile/me/notifications/read-all`, NoticeList 헤더 버튼 |
|
||||
| 출금 잔여 한도 검증 | `settle_master.net_amount - sum(활성 출금합계)` 초과 시 E411 |
|
||||
|
||||
### 운영 DB 마이그레이션 (중요 공지)
|
||||
운영 DB `192.168.0.60:55432/trading_ai/ga` 에는 현재 **V17(또는 V22) 까지만** 적용되어 있습니다.
|
||||
`ga-api 를 재시작`하면 Flyway 가 V18~V55 를 자동 순차 적용합니다.
|
||||
재시작 전까지 P2~P4 신규 도메인 API 는 테이블이 없어 동작하지 않습니다.
|
||||
**e2e 검증 (2026-05-27 23:40, agent01/userId=4/agentId=1)**:
|
||||
- 로그인 → AGENT role → /api/mobile/me/summary statementCount=3 / withdrawRequestCount=3
|
||||
- /api/mobile/me/settle-masters → 3건 응답 (admin은 E403이었음)
|
||||
- POST /api/mobile/me/withdraw-requests → 성공 (requestId=4)
|
||||
- /api/mobile/me/statements/1 → 200 (본인) · /statements/2 → **E403 (타인)** ← 본인 검증 강제
|
||||
- PUT /api/mobile/me/profile/phone → 200, /api/auth/me 즉시 반영
|
||||
- POST /api/internal/bank-settle → SETTLED 동기화 + user_notification 자동 발행 코드 경로 검증
|
||||
- POST /api/auth/refresh → 새 accessToken 발급
|
||||
|
||||
단위/통합 테스트는 SqlSessionFactory 환경 설정 문제로 아직 미작성 상태입니다.
|
||||
**잔여 — 외부 SDK 차단**:
|
||||
| 항목 | 차단 사유 |
|
||||
|---|---|
|
||||
| KftcBankTransferAdapter 본체 | API 키·계약·테스트망 |
|
||||
| ToastMessageAdapter 본체 | App Key·발신번호·템플릿 코드 |
|
||||
| 카카오 알림톡 본체 | 비즈채널·템플릿 승인 |
|
||||
| 푸시 알림 (FCM 등) | 외부 채널 결정 |
|
||||
|
||||
## 5. 재개 절차 (다음 세션 — P5 라운드)
|
||||
## 3-18. P7 — incar 갭 반영 수수료 계산 7개 도메인 풀스택 (V88~V96 — 완료, 2026-05-31 라이브 적용)
|
||||
|
||||
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. **권장 시작 지점**: P5 Wave 1 — dba-architect에게 V56~V61 (회계 전표 / 원천세 / 부가세 / 결산 / 이상치 / 연말 지급명세서)
|
||||
6. **ga-api 재시작 필요**: 운영 DB 에 V18~V55 적용하려면 ga-api 재시작 후 Flyway 자동 실행 확인
|
||||
7. **사용자 결정 필요 항목**:
|
||||
- P5 시작 전 ga-api 재시작해서 V18~V55 운영 DB 반영 여부
|
||||
- P2-1 마감 도메인의 마감 후 수정 차단 AOP 구현 방식 (전역 AOP vs Service-level 가드)
|
||||
incar CMS 갭 분석 결과 GA 시스템에 없던 수수료 계산 7개 도메인을 DB→Core→API→Frontend 전 레이어로 추가. 운영 DB schema **v96**.
|
||||
|
||||
| 도메인 | 마이그레이션 | 테이블 | API 경로 |
|
||||
|---|---|---|---|
|
||||
| 수입수수료+수지차 (INCOME_COMM) | V88 | income_commission_ledger, margin_summary | `/api/income-commissions/*` |
|
||||
| 소개·이관수수료 (REFERRAL_COMM) | V89 | referral_commission_rule, referral_commission_ledger | `/api/referral-commissions/*` |
|
||||
| 정착지원금 (SETTLING_SUPPORT) | V90 | settling_support_rule, settling_support_ledger | `/api/settling-supports/*` |
|
||||
| 인정실적+환산보험료 (RECOGNIZED_PERF) | V91 | product_conversion_rate, recognized_performance_ledger | `/api/recognized-performances/*` |
|
||||
| 시상 (AWARD) | V92 | award_rule, award_ledger | `/api/awards/*` |
|
||||
| 생보운영지원수수료 (LIFE_OP_SUPPORT) | V93 | life_support_rule, life_support_ledger | `/api/life-supports/*` |
|
||||
| 지점장수당 (BRANCH_MGR_ALLOWANCE) | V94 | branch_mgr_allowance_rule, branch_mgr_allowance_ledger | `/api/branch-allowances/*` |
|
||||
| 메뉴 7개 | V95 | — | GRP_COMMISSION 하위 sort 80~140 |
|
||||
| 공통코드 5그룹 | V96 | — | INCOME_STATUS/REFERRAL_TYPE/CALC_TYPE/SUPPORT_STATUS/AWARD_TYPE |
|
||||
|
||||
통합 검증 (2026-05-31):
|
||||
- `./gradlew build -x test` BUILD SUCCESSFUL
|
||||
- Flyway V88~V96 9건 모두 success=True, 운영 DB schema v96
|
||||
- 신규 14테이블 전부 존재 확인, 신규 메뉴 7건 등록 확인
|
||||
- 기존 smoke_allmenus.py 67엔드포인트: PASS=66 FAIL=0 SKIP=1(settle/summary 필수파라미터, 기존 동일)
|
||||
- P7 신규 8엔드포인트(인정실적은 /rates+/ledgers 분리): PASS=8 FAIL=0
|
||||
- 주의: `/api/recognized-performances/rules` 는 없음. D4는 `/rates`(환산율)와 `/ledgers`(원장) 두 경로로 분리됨.
|
||||
|
||||
## 3-16. 계약 승계 (contract_succession, V86 — 완료, 2026-05-29 라이브 적용)
|
||||
|
||||
설계사 해촉/이관 시 보험계약의 **유지수수료(maintain) 귀속 설계사를 승계월 기준 후임으로 전환**.
|
||||
모집수수료(1회차)는 원 모집설계사 귀속 유지 — 한국 고아계약 승계 표준. 후임 설계사는 본인 등급의 payout 비율로 수령.
|
||||
|
||||
| 레이어 | 산출물 |
|
||||
|---|---|
|
||||
| DB (V86) | `contract_succession`(contract_id/from_agent_id/to_agent_id/succession_date/reason/status) + 인덱스 3 + 메뉴 `CONTRACT_SUCCESSION`(GRP_PRD, path `/contracts/successions`) + 권한 READ/CREATE/UPDATE/DELETE + 역할부여(SUPER_ADMIN·ADMIN 전체 / MANAGER READ) + 공통코드 SUCCESSION_REASON·SUCCESSION_STATUS + 테스트데이터 1건 |
|
||||
| core | `ContractSuccession` VO/Resp/SaveReq/SearchParam + Mapper(.java/.xml). 핵심 `selectEffectiveSuccessions(settleMonth)` = 계약별 최근 ACTIVE 승계 1건(DISTINCT ON, `TO_CHAR(succession_date,'YYYYMM') <= settleMonth`) |
|
||||
| api | `ContractSuccessionService/Controller` (`/api/contract-successions`) CRUD + `/{id}/cancel`. from==to 가드(E411) |
|
||||
| batch | `CalcMaintainStep`: effective 승계 Map 사전로드(N+1 회피) → `effectiveAgentId = getOrDefault(contractId, c.agentId)` → `CommissionCalculator.calcMaintainLedger(c, month, year, effectiveAgentId)` 오버로드(후임 등급 payout + `led.setAgentId(effectiveAgentId)`). **AggregateStep 무변경** — ledger의 agent_id로 집계하므로 후임 귀속 자동 전파 |
|
||||
| frontend | `api/succession.ts` + `pages/contracts/ContractSuccessions.tsx` + App.tsx 라우트 `contracts/successions` |
|
||||
|
||||
핵심 산출물 커밋: `bd5ff4e`.
|
||||
|
||||
**부수 버그 수정 (커밋 `4dbc688`, 승계 무관)**:
|
||||
- `AgentDeductionMapper.updateStatusBatch` 가 인터페이스 선언만 있고 XML 구문 누락 → AggregateStep(Step8) 이 PENDING 차감 일괄 APPLIED 전이 시 `Invalid bound statement (not found)` 로 aggregate 전체 FAILED 되던 잠복 버그(P0-2부터 존재). `updateStatus` 동일 패턴 + foreach IN 으로 보강. e2e 도중 발견.
|
||||
|
||||
통합 검증 (2026-05-29):
|
||||
- 5모듈 compileJava BUILD SUCCESSFUL + ga-frontend tsc 0 오류
|
||||
- Flyway V86 자동 적용 → 운영 DB schema **v86**
|
||||
- API CRUD + cancel + from==to 가드(E411) 라이브 PASS
|
||||
- **배치 풀잡 e2e**: ga-batch 8081 기동, `POST /internal/jobs/monthly-settlement`(JWT 필요 — ga-api admin 토큰 재사용). 테스트 승계(계약59 agent39→agent2, 2026-01-01) 등록 후 202605 정산 실행:
|
||||
- maintain_ledger 계약59 = **agent2(후임)** / 5520(후임 등급 재계산, 원래 agent39/19320)
|
||||
- settle_master agent2 maintain = 142700 = ledger 합 / agent39 = 191100 = ledger 합
|
||||
- 무결성 **ledger 총합 == settle_master maintain 총합 = 17,556,135 정확 일치**
|
||||
- (202604는 전 행 status=CONFIRMED → upsert 불변 가드 `WHERE status NOT IN ('CONFIRMED','PAID')` 로 settle_master 미갱신 = 정상 동작)
|
||||
- e2e 테스트 DB 부산물(승계 레코드 / 202605 정산데이터 / 회계분개 / 차감 전이 / batch_job_log)은 단일 트랜잭션으로 전부 롤백 완료, pre-e2e 상태 복원 검증.
|
||||
- **주의**: AccountingEntryGenerateStep(Step8b)은 멱등이 아님 — 동일 월 재실행 시 분개 중복 INSERT(주석상 existsByPaymentId 중복방어 언급되나 코드 미구현). 동일 월 재정산 전 기존 분개 정리 필요.
|
||||
|
||||
## 3-17. 테스트 환경 — SqlSessionFactory 문제 해결 (2026-05-29, 완료)
|
||||
|
||||
증상: `./gradlew :ga-api:test` 시 Spring 컨텍스트 로딩 실패 — `Error creating bean 'externalCallLogMapper' ... Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required`.
|
||||
|
||||
원인:
|
||||
- `application-test.yml` 이 MybatisAutoConfiguration / DataSourceAutoConfiguration 을 제외 → 컨텍스트에 SqlSessionFactory 없음.
|
||||
- 그러나 `MyBatisConfig` 의 `@MapperScan(basePackages = "com.ga")` 가 모든 @Mapper 의 MapperFactoryBean 을 생성, 이들은 SqlSessionFactory 를 요구.
|
||||
- `AbstractControllerTest` 는 매퍼를 하나씩 `@MockBean` 으로 막아 회피했으나, 이후 추가된 매퍼(ExternalCallLog/Succession/AccountingEntry 등)가 목록에 누락 → 실제 MapperFactoryBean 생성 시도 → 깨짐. **매퍼 추가될 때마다 목록 갱신해야 하는 취약 구조.**
|
||||
- (부수) `SettleControllerTest` 가 구버전 `hold(id, reason)` 2인자 stub 사용 → 현재 `hold(id, reason, reasonCode)` 3인자와 불일치로 testCompile 실패.
|
||||
|
||||
해결:
|
||||
- `TestMyBatisConfig`(@TestConfiguration) 신규 — 미접속 `PGSimpleDataSource` + `SqlSessionFactory` 한 쌍 제공. 모든 MapperFactoryBean 이 생성되므로 매퍼 목록 갱신 불필요. 컨트롤러 테스트는 Service 를 mock 하므로 실제 쿼리 미실행(연결 안 함).
|
||||
- `AbstractControllerTest` `@Import` 에 `TestMyBatisConfig` 추가.
|
||||
- `SettleControllerTest` hold stub 3인자로 정정.
|
||||
|
||||
검증: `./gradlew test --rerun-tasks` → **BUILD SUCCESSFUL, 112건 PASS** (ga-common 57 / ga-api 36 / ga-batch 19, 실패·에러 0). DB 무관(미접속 DataSource).
|
||||
|
||||
## 3-9. P5-W3 메뉴/권한 전수 정합 보강 (V74 — 완료, 2026-05-21 라이브 적용)
|
||||
|
||||
| 항목 | 상태 |
|
||||
|---|---|
|
||||
| AUDIT_P5W3.md §4 갭 리포트 작성 | 완료 (2026-05-17, infra) |
|
||||
| V74 SQL 작성 | 완료 (2026-05-17, dba) |
|
||||
| V74 정적검증 (PL) | 완료 (멱등·ON CONFLICT·스코프 확인) |
|
||||
| 4모듈 compileJava | **PASS** (BUILD SUCCESSFUL) |
|
||||
| ga-frontend npm run build | **PASS** (TS 오류 0) |
|
||||
| 운영 DB 접속·Flyway V74 적용 | **완료** (2026-05-21, ga-api 재기동 시 자동 적용) |
|
||||
| 스모크 검증 | **완료** (verify_v74.py 20/20 PASS) |
|
||||
|
||||
갭 요약 (총 27건):
|
||||
- (a) menu 없음: 0건
|
||||
- (b) menu_permission 없음: 3건 (NOTICE/DELETE, SYSTEM_DATA_DICT/DELETE, TERM_SETTLE/APPROVE)
|
||||
- (c) grant 없음: 6건 (위 3 perm × SUPER_ADMIN+ADMIN, ADMIN/SYSTEM_* 7건은 보안상 의도적 제외)
|
||||
- route/menu_path 불일치: 11건 (V74에서 UPDATE 처리)
|
||||
|
||||
갭 재검증 (V74 적용 후): `python verify_v74.py` → **20/20 PASS** 확인 완료 (2026-05-21).
|
||||
|
||||
## 3-10. P5-W3 본론 — 결산 / 이상치·예측 / 연말명세서 (V75~V78, 2026-05-21 완료)
|
||||
|
||||
| 항목 | 마이그레이션 | 상태 |
|
||||
|---|---|---|
|
||||
| accounting_close + account_balance_snapshot (회계 결산) | V75 | 완료 |
|
||||
| retention_anomaly_alert + forecast_kpi_monthly (이상치·예측 KPI) | V76 | 완료 |
|
||||
| year_end_statement (연말 지급명세서) | V77 | 완료 |
|
||||
| P5-W3 본론 메뉴 4개 + 권한 + 공통코드 7그룹 + 테스트데이터 | V78 | 완료 |
|
||||
|
||||
핵심 산출물:
|
||||
- `AccountingCloseService/Controller` (`/api/accounting-closes`) — DRAFT 생성 / 마감 실행(분개 채번+잔액 스냅샷) / 재오픈
|
||||
- `RetentionAnomalyAlertService/Controller` (`/api/retention-anomalies`) — 13M/25M 이상 탐지 / 검토 처리
|
||||
- `ForecastKpiMonthlyService/Controller` (`/api/forecast-kpi`) — 이동평균(SMA3/6/NAIVE) 차월 예측
|
||||
- `YearEndStatementService/Controller` (`/api/year-end-statements`) — 연 사업소득 명세서 재집계/발급/발송/엑셀
|
||||
- 배치: BatchConfig 에 Step 11(WithholdingTaxQuarterly) / Step 12(VatReportQuarterly) 연결
|
||||
- 프론트: `AccountingCloseList` / `YearEndStatementList` / `RetentionAnomalyList` / `ForecastKpiList` 4화면 + API 모듈 4개 + App.tsx 라우트 4개
|
||||
- 공통: `usePermission`/`PermissionButton` 에 `EXECUTE` 권한 누락 보강 (canExecute)
|
||||
|
||||
통합 검증 (2026-05-21):
|
||||
- Flyway: 운영 DB V74~V78 자동 적용 → 현재 **v78**
|
||||
- 4모듈 compileJava + ga-frontend npm run build **PASS** (TS 오류 0)
|
||||
- 스모크: 신규 4 + 회귀 4 = **GET 8/8 PASS**, 쓰기(regenerate/detect/draft) 4/4 PASS
|
||||
- 보정: YearEndStatementMapper.xml 의 `a.agent_code` 미존재 컬럼 참조 제거 (agent 테이블에 agent_code 없음)
|
||||
|
||||
## 5. 재개 절차 (다음 세션)
|
||||
|
||||
1. **Claude Code 시작 시 cwd 확인**: `C:\Users\kyu\Desktop\ga_pro`
|
||||
2. **HANDOFF.md 읽기**: 본 문서 §3-16(계약 승계) + §3-15(P6 후속) + 최근 git log
|
||||
3. **운영 DB 상태**: **V96 적용 완료** (P7 incar 7도메인). ga-api 8082 가동. ga-frontend 3000 / ga-mobile-pwa 3001 은 필요 시 기동. origin/main 동기화 완료(HEAD `4dbc688`, 2026-05-29 push). P7 코드(V88~V96)는 로컬 커밋 아직 없음.
|
||||
4. **재기동이 필요한 경우**:
|
||||
- `cd ga-commission-system && nohup ./gradlew :ga-api:bootRun --args='--spring.profiles.active=trading_ai' > ../ga-api-bootrun.log 2>&1 & disown`
|
||||
- `cd ga-commission-system/ga-frontend && npm run dev` (3000)
|
||||
- `cd ga-commission-system/ga-mobile-pwa && npm run dev` (3001)
|
||||
- 정산 배치: `nohup ./gradlew :ga-batch:bootRun --args='--spring.profiles.active=trading_ai' > ../ga-batch-bootrun.log 2>&1 &` (8081). 트리거 `POST :8081/internal/jobs/monthly-settlement {"settleMonth":"yyyyMM"}` — **JWT 필요(ga-api `/api/auth/login` admin 토큰 재사용)**. 동기 실행.
|
||||
5. **테스트 계정 / DB**: `admin`/`admin1234!`(SUPER_ADMIN), `agent01`/`agent1234!`(userId=4·agentId=1·AGENT). 로그인 필드명 `loginId`. DB: psql17 `192.168.0.60:55432/trading_ai` user `kyu`/pw `7895123`, `SET search_path TO ga`.
|
||||
6. **사용자 결정 필요 항목 (외부 SDK 차단 사유)**:
|
||||
- KFTC 펌뱅킹 API 계약·인증 정보 → `KftcBankTransferAdapter` 본체 구현
|
||||
- NHN Toast / 카카오 비즈채널 계정 + 템플릿 코드 → `ToastMessageAdapter` 본체
|
||||
- 푸시 알림 채널 (FCM 등) 결정 → PWA 통합
|
||||
|
||||
## 6. PL 가이드 (다음 세션 유의사항)
|
||||
|
||||
@@ -206,7 +554,7 @@
|
||||
- **자동 정책**:
|
||||
- 각 단계 완료 후 자동 커밋 (push 안 함)
|
||||
- 빌드/테스트 실패 시 담당 팀원에 재지시
|
||||
- 컴파일은 PL이 직접 검증, 테스트는 환경 문제(SqlSessionFactory)로 통과 안 되는 게 알려진 상태
|
||||
- 컴파일은 PL이 직접 검증. 테스트는 SqlSessionFactory 환경 문제 해결 후 `./gradlew test` 112건 PASS (§3-17)
|
||||
|
||||
## 7. 표준 가정 (도메인 작업 진행 시 채택한 값)
|
||||
|
||||
|
||||
@@ -43,9 +43,9 @@ ga-commission-system/
|
||||
V1~V17 마이그레이션 + 테스트 데이터(보험사 5 / 상품 20 / 설계사 50 / 계약 200)가
|
||||
`192.168.0.60:55432/trading_ai/ga` 스키마에 적용되어 있습니다.
|
||||
|
||||
> **주의**: 운영 DB 에는 현재 V1~V17(또는 V22 까지)만 적용되어 있습니다.
|
||||
> V18~V61 은 ga-api 를 재시작하면 Flyway 가 자동으로 순서대로 적용합니다.
|
||||
> 재시작 전까지 P2~P5-W1 신규 도메인 API 는 테이블이 없어 동작하지 않습니다.
|
||||
> **운영 DB 상태 (2026-05-17 기준)**: V73 적용 완료. V74(메뉴/권한 전수보강) 작성 완료, 대기 중.
|
||||
> ga-api 를 재시작하면 Flyway 가 V74 를 자동 적용합니다(멱등).
|
||||
> DB 복귀 후 `python verify_v74.py` 로 보강 결과를 검증하세요.
|
||||
|
||||
#### 1) Backend (ga-api) 실행
|
||||
|
||||
@@ -113,6 +113,8 @@ PW : admin1234!
|
||||
| 지급 관리 | `/payments` | 지급 상태 추적 |
|
||||
| 연말 사업소득 명세서 | `/settle/annual-income` | 설계사별 연간 사업소득 집계 + 재집계 (P5-W1) |
|
||||
| 분개장 | `/settle/accounting-journal` | 회계 자동분개 엔트리 + 마감 처리 (P5-W1) |
|
||||
| 원천세 신고 | `/tax/withholding-tax` | 분기별 원천세 신고 집계 + 재집계 + 제출완료 처리 (P5-W2) |
|
||||
| 부가세 신고 | `/tax/vat` | 분기별 부가세 신고 집계 + 재집계 + 제출완료 처리 (P5-W2) |
|
||||
| 사용자 관리 | `/system/users` | 비번 초기화/잠금 해제 |
|
||||
| 역할/권한 | `/system/roles` | 권한 매트릭스 (메뉴 × 6권한) |
|
||||
| 공통코드 | `/system/codes` | 그룹별 코드 조회 |
|
||||
@@ -276,6 +278,18 @@ UPDATE ga.users SET password = '$2b$10$...' WHERE login_id = 'admin';
|
||||
- **첨부파일 (P4-B-8)**: attachment — 도메인 무관 공통 첨부 (소속/경로/타입)
|
||||
- **출금신청 (P4-B-9)**: withdraw_request — 결재 + 펌뱅킹 송신 + 결과 반영
|
||||
|
||||
### P5 Wave 2 — 원천세/부가세 분기신고 (V67~V69)
|
||||
- **원천세 신고** (`/tax/withholding-tax`): 분기별 설계사 사업소득 원천세 신고 집계, 재집계 + 제출완료 처리
|
||||
- `GET /api/withholding-tax-reports` — 목록 조회 (settleQuarter/agentId/orgId/fileStatus 필터)
|
||||
- `GET /api/withholding-tax-reports/{id}` — 단건 상세
|
||||
- `POST /api/withholding-tax-reports/regenerate {"settleQuarter":"YYYY-Qn"}` — 분기 재집계 (EXECUTE 권한)
|
||||
- `PUT /api/withholding-tax-reports/complete {"ids":[...]}` — 제출완료 처리 (EXECUTE 권한)
|
||||
- **부가세 신고** (`/tax/vat`): tax_invoice 기반 분기 매출/매입 VAT 집계, 재집계 + 제출완료 처리
|
||||
- `GET /api/vat-reports` — 목록 조회 (settleQuarter/fileStatus 필터)
|
||||
- `GET /api/vat-reports/{id}` — 단건 상세
|
||||
- `POST /api/vat-reports/regenerate {"settleQuarter":"YYYY-Qn"}` — 분기 재집계 (EXECUTE 권한)
|
||||
- `PUT /api/vat-reports/complete {"ids":[...]}` — 제출완료 처리 (EXECUTE 권한)
|
||||
|
||||
### P5 Wave 1 — 회계 진입점 (V63~V66)
|
||||
- **연말 사업소득 명세서** (`/settle/annual-income`): 설계사별 귀속연도 사업소득 집계, 재집계 버튼
|
||||
- `GET /api/agent-annual-incomes` — 목록 조회 (settleYear/agentId/orgId 필터)
|
||||
@@ -418,8 +432,10 @@ public class FooController {
|
||||
| V69 | P5-W2: 테스트 데이터 + 메뉴 INSERT (원천세/부가세 신고) |
|
||||
| V70 | rule 도메인 감사 컬럼 보강 (commission_rate/payout_rule/override_rule 등) |
|
||||
| V71 | payment 세무 컬럼 보강 (income_tax_amount/local_tax_amount/vat_amount/net_amount) |
|
||||
| V72 | P5-W1 EXECUTE 권한 보강 (ANNUAL_INCOME / ACCOUNTING_JOURNAL) |
|
||||
| V73 | P5-W2 EXECUTE 권한 보강 (WITHHOLDING_TAX / VAT_REPORT) |
|
||||
|
||||
> 운영 DB (192.168.0.60) 현재 V71 까지 적용 완료.
|
||||
> 운영 DB (192.168.0.60) 현재 V73 까지 적용 완료.
|
||||
> **ga-api 재시작 시** Flyway 가 신규 마이그레이션을 자동으로 순차 적용합니다.
|
||||
|
||||
---
|
||||
@@ -442,10 +458,10 @@ public class FooController {
|
||||
- [x] **P4-A 수수료 종류 보강** — commission_master 통합 / 수금 / 갱신 / 부활 / 유지보수수당 / 오버라이드원장
|
||||
- [x] **P4-B 운영 도메인** — 위촉계약 / 자격관리 / 보수교육 / 계약변경 / 민원 / 결재워크플로우 / 공지·알림 / 첨부파일 / 출금신청
|
||||
- [x] **P5-W1 회계 진입점** — agent_annual_income / contract_accounting_entry / account_code (V63~V66, 통합검증 완료)
|
||||
- [x] **운영 DB** — 192.168.0.60:55432/trading_ai/ga (V71 까지 적용 완료)
|
||||
- [x] **P5-W2 원천세·부가세 분기신고** — withholding_tax_report / vat_report (V67~V69, 통합검증 완료 2026-05-17)
|
||||
- [x] **운영 DB** — 192.168.0.60:55432/trading_ai/ga (V73 까지 적용 완료)
|
||||
- [ ] **단위/통합 테스트** — 미작성 (SqlSessionFactory 환경 문제로 보류)
|
||||
- [ ] **MappingEngine 정밀화** — 일부 변환 규칙은 단순화된 형태
|
||||
- [ ] **P5-W2 원천세·부가세 신고** — withholding_tax_report / vat_report (V67~V69 스키마 완료, Service+Controller 미완)
|
||||
- [ ] **P5-W3 결산 / 이상치 / 연말 지급명세서** — 미착수
|
||||
- [ ] **P6 외부 연동** — 펌뱅킹 실연동 / SMS·카카오 / 모바일 셀프 조회
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# DOMAIN_GAP_MOD1-8 — 수수료 갭 8종 신규모듈 권위 스펙
|
||||
|
||||
> 2026-06-06 갭분석 결과 "개념적으로 빠진" 수수료 모듈 8종. 업무 수치는 **데모 기본값**(운영 시 룰테이블 조정).
|
||||
> 모든 레이어 에이전트는 이 문서를 단일 진실원으로 따른다. 테이블/컬럼/Enum/경로/메뉴코드명을 **그대로** 사용.
|
||||
|
||||
## 공통 규약 (기존 패턴 — V92/V95/V96 미러)
|
||||
- PK `BIGSERIAL`, 금액 `NUMERIC(15,2)`, 율 `NUMERIC(7,4)`(0~1 소수), 정산월 `CHAR(6)`(YYYYMM).
|
||||
- 감사: `created_at TIMESTAMP NOT NULL DEFAULT NOW()`, `created_by BIGINT`, `updated_at TIMESTAMP`.
|
||||
- 모든 테이블 `COMMENT ON TABLE/COLUMN`, 인덱스 `idx_<약어>_...`.
|
||||
- 메뉴: `menu`(parent=GRP_*, menu_type='PAGE', menu_level=2) + `menu_permission`(READ/CREATE/UPDATE/DELETE/EXPORT) + `role_menu_permission`(SUPER_ADMIN/ADMIN 전체, MANAGER READ/CREATE/UPDATE/EXPORT, AGENT READ). NOT EXISTS/ON CONFLICT 가드.
|
||||
- 공통코드: `common_code_group` + `common_code`.
|
||||
- core: `XxxVO`(전컬럼)/`XxxResp`(+조인명)/`XxxSaveReq`(입력)/`XxxSearchParam`(extends PageParam) + Mapper(.java/.xml, selectList/selectById/insert/update/delete + 도메인쿼리) + Enum(상태) + MapStruct(필요시). Mapper는 BaseMapper 상속 안 함(개별 정의, 기존 commission Mapper 미러).
|
||||
- api: `XxxService`(@Service @Transactional(readOnly=true), 쓰기 @Transactional) + `XxxController`(@RestController @RequestMapping("/api/...") @RequirePermission, @DataChangeLog 쓰기, ApiResponse/PageResponse 반환). 기존 commission Controller 미러.
|
||||
- frontend: `ga-frontend/src/pages/<area>/Xxx.tsx` + `api/<area>.ts` 함수 + `App.tsx` 라우트. 100건↑/셀편집/합계는 AG Grid, 단순 CRUD는 Ant Design DataTable.
|
||||
- 마이그레이션 번호: MOD-1=V116, MOD-2=V117, MOD-3=V118, MOD-4=V119, MOD-5=V120, MOD-6=V121, MOD-7=V122, MOD-8=V123. (필요 시 모듈당 2파일 가능 — 예 V116_a 테이블 / 단일 파일에 메뉴·코드 포함 권장)
|
||||
|
||||
---
|
||||
|
||||
## MOD-1 — 슈퍼바이저/도입수수료 (recruiter/development commission) [V116]
|
||||
설계사를 GA에 **도입(리크루팅)**한 상위 설계사에게 지급하는 별도 트랙(오버라이드와 무관, recruiter 관계 기반).
|
||||
|
||||
### DB V116
|
||||
- `ALTER TABLE agent ADD COLUMN recruiter_agent_id BIGINT REFERENCES agent(agent_id);` (도입자, NULL 허용)
|
||||
- `recruiter_commission_rule`(rule_id PK, recruiter_type VARCHAR(20) CHECK IN('NEW_RECRUIT','DEVELOPMENT','TRAINING'), calc_type VARCHAR(10) CHECK IN('RATE','FIXED'), commission_rate NUMERIC(7,4), fixed_amount NUMERIC(15,2), effective_from DATE NOT NULL, effective_to DATE, 감사컬럼). CHECK rate 0~1.
|
||||
- `recruiter_commission_ledger`(ledger_id PK, recruiter_agent_id BIGINT NOT NULL REFERENCES agent, recruit_agent_id BIGINT NOT NULL REFERENCES agent, settle_month CHAR(6) NOT NULL, recruiter_type VARCHAR(20) NOT NULL, base_amount NUMERIC(15,2) NOT NULL, commission_amount NUMERIC(15,2) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED' CHECK IN('CALCULATED','CONFIRMED','PAID','CANCELLED'), 감사). idx (recruiter_agent_id, settle_month).
|
||||
- 메뉴 `RECRUITER_COMM`('도입수수료', '/commission/recruiter', 'commission/RecruiterCommission', GRP_COMMISSION, sort 150) + 권한 + 공통코드 `RECRUITER_TYPE`(NEW_RECRUIT 신인도입/DEVELOPMENT 육성/TRAINING 교육).
|
||||
- 공식(데모): commission = base_amount × rule.commission_rate (RATE) / fixed_amount (FIXED). base_amount=도입 설계사의 당월 모집수수료 합(데모 입력 허용).
|
||||
|
||||
### core/api
|
||||
- `RecruiterCommissionRule*` + `RecruiterCommissionLedger*` VO/Resp/SaveReq/SearchParam, Mapper(.java/.xml: selectList/selectById/insert/update/delete + `aggregateByAgent(settleMonth)` = SUM(commission_amount) GROUP BY recruiter_agent_id). Enum `RecruiterType`.
|
||||
- `RecruiterCommissionService`: rule CRUD + ledger list/detail/createLedger(룰 calc_type 권위, ReferralCommissionService 미러) + aggregate. Controller `/api/recruiter-commissions` (rules/ledgers).
|
||||
- **정산 반영**: `AggregateStep`에 recruiterCommissionMapper.aggregateByAgent 추가 → other_commission_total 합산(기존 9종 패턴 동일, recruiter_agent_id 기준 agentId). (지급성이므로 gross 가산.)
|
||||
|
||||
### frontend
|
||||
- `pages/commission/RecruiterCommission.tsx`(룰 탭 + 원장 탭, 등록 모달), `api/commission.ts`에 함수 추가, App.tsx 라우트.
|
||||
|
||||
---
|
||||
|
||||
## MOD-2 — 선지급/차익정산 (advance payment recovery + delta settlement) [V117]
|
||||
신입/램프업 설계사에 미리 지급한 **선지급금**을 확정수수료로 우선 회수, 차액만 실지급.
|
||||
|
||||
### DB V117 (GRP_SETTLE)
|
||||
- `advance_payment`(advance_id PK, agent_id BIGINT NOT NULL REFERENCES agent, advance_month CHAR(6) NOT NULL, principal_amount NUMERIC(15,2) NOT NULL CHECK>0, recovered_amount NUMERIC(15,2) NOT NULL DEFAULT 0, balance NUMERIC(15,2) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'OUTSTANDING' CHECK IN('OUTSTANDING','PARTIAL','CLEARED','WAIVED'), memo VARCHAR(200), 감사). CHECK balance>=0. idx(agent_id, status).
|
||||
- `advance_recovery_ledger`(recovery_id PK, advance_id BIGINT NOT NULL REFERENCES advance_payment, settle_month CHAR(6) NOT NULL, recovery_amount NUMERIC(15,2) NOT NULL CHECK>0, 감사). idx(advance_id), idx(settle_month).
|
||||
- 메뉴 `ADVANCE_PAYMENT`('선지급정산','/settle/advance-payment','settle/AdvancePayment', GRP_SETTLE, sort 적절) + 권한 + 공통코드 `ADVANCE_STATUS`(OUTSTANDING 미상환/PARTIAL 부분/CLEARED 완료/WAIVED 면제).
|
||||
|
||||
### core/api/batch
|
||||
- `AdvancePayment*` + `AdvanceRecoveryLedger*` VO/Resp/SaveReq/SearchParam, Mapper. 도메인쿼리: `outstandingByAgent(agentId)`=SUM(balance) WHERE status IN('OUTSTANDING','PARTIAL'); aging은 advance_month 기준.
|
||||
- `AdvancePaymentService`: 선지급 등록(balance=principal)/조회/aging 리포트/면제(waive). Controller `/api/advance-payments`.
|
||||
- **정산 연계(AggregateStep)**: net(=tax후) 계산 직후, P9 환수상계와 **동일 위치/유사 패턴**으로 선지급 잔액 FIFO 우선상계. recovered_advance = min(payable, outstanding). 차익정산 delta = payable − recovered_advance. payable 최종 = delta. advance_recovery_ledger 기록 + advance_payment.recovered/balance/status 갱신. **멱등**: 당월 recovery reverse 후 재적용(P9 reverseClawbackRecovery 패턴). 환수상계와 선지급상계 순서: 환수(채권) → 선지급. 둘 다 payable 차감.
|
||||
- 주의: 기존 settle_master payable_amount 의미에 선지급 차감 반영. 새 컬럼 불필요(payable에 통합) — 단 advance_recovery_ledger로 추적.
|
||||
|
||||
### frontend
|
||||
- `pages/settle/AdvancePayment.tsx`(선지급 목록+잔액/상태, 등록·면제 모달, 상계내역 Drawer), `api/settle.ts` 또는 `api/operation.ts` 함수.
|
||||
|
||||
---
|
||||
|
||||
## MOD-3 — 환수 분할상환 (installment clawback) [V118]
|
||||
대액 환수채권을 N개월 분할 납부 계획으로.
|
||||
|
||||
### DB V118 (GRP_COMMISSION)
|
||||
- `clawback_installment_plan`(plan_id PK, receivable_id BIGINT NOT NULL REFERENCES clawback_receivable(receivable_id), installment_no INT NOT NULL, installment_amount NUMERIC(15,2) NOT NULL CHECK>0, due_month CHAR(6) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'SCHEDULED' CHECK IN('SCHEDULED','RECOVERED','CANCELLED'), recovered_at TIMESTAMP, 감사). UNIQUE(receivable_id, installment_no). idx(due_month, status).
|
||||
- 메뉴 `CLAWBACK_INSTALLMENT`('환수분할상환','/commission/clawback-installment','commission/ClawbackInstallment', GRP_COMMISSION, sort) + 권한. 공통코드 `CB_INSTALLMENT_STATUS`(SCHEDULED 예정/RECOVERED 회수/CANCELLED 취소).
|
||||
|
||||
### core/api
|
||||
- `ClawbackInstallmentPlan*` VO/Resp/SaveReq/SearchParam + Mapper(selectByReceivable, insertBatch, updateStatus).
|
||||
- `ClawbackInstallmentService`: receivable 1건 + 회차수 N → 균등분할(마지막 회차 잔차흡수, InstallmentPlanGenerator 잔차패턴) 계획 생성. due_month=기준월+회차. list/detail/generate/cancel. Controller `/api/clawback-installments`.
|
||||
- 월별 상계 연계는 데모 수준(계획 생성·조회·상태전이). (AggregateStep 통합은 선택 — 미연계 시 문서화.)
|
||||
|
||||
### frontend
|
||||
- `pages/commission/ClawbackInstallment.tsx`(채권 선택→분할 생성, 회차 목록), `api/commission.ts`.
|
||||
|
||||
---
|
||||
|
||||
## MOD-4 — 익월 부활 재지급 (lapsed reinstatement repayment) [V119]
|
||||
실효(보류)된 계약이 익월 부활 시 보류했던 수수료를 make-good 재지급.
|
||||
|
||||
### DB V119 (GRP_COMMISSION)
|
||||
- `lapsed_reinstatement_recovery`(recovery_id PK, contract_id BIGINT NOT NULL REFERENCES contract(contract_id), agent_id BIGINT NOT NULL REFERENCES agent, lapsed_month CHAR(6) NOT NULL, reinstated_month CHAR(6) NOT NULL, withheld_amount NUMERIC(15,2) NOT NULL CHECK>=0, repaid_amount NUMERIC(15,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'PENDING' CHECK IN('PENDING','REPAID','CANCELLED'), 감사). idx(agent_id, reinstated_month), idx(contract_id).
|
||||
- 메뉴 `LAPSED_REINSTATEMENT`('익월부활재지급','/commission/lapsed-reinstatement','commission/LapsedReinstatement', GRP_COMMISSION, sort) + 권한. 공통코드 `LAPSE_RECOVERY_STATUS`(PENDING 대기/REPAID 재지급완료/CANCELLED 취소).
|
||||
|
||||
### core/api
|
||||
- `LapsedReinstatementRecovery*` VO/Resp/SaveReq/SearchParam + Mapper(selectList/selectById/insert/updateStatus + aggregateByAgent=SUM(repaid_amount) WHERE status='REPAID' GROUP BY agent_id).
|
||||
- `LapsedReinstatementService`: 부활 등록(withheld_amount=보류액, 데모 입력)/repay(상태 REPAID, repaid=withheld)/cancel. Controller `/api/lapsed-reinstatements`.
|
||||
- **정산 반영(선택)**: aggregateByAgent를 AggregateStep other_commission_total에 가산(REPAID만). 미연계 시 문서화.
|
||||
|
||||
### frontend
|
||||
- `pages/commission/LapsedReinstatement.tsx`, `api/commission.ts`.
|
||||
|
||||
---
|
||||
|
||||
## MOD-5 — 정산대사 입수 (carrier reconciliation IN) [V120]
|
||||
보험사 통보 정산자료 vs GA 원장 대사 차이 산출.
|
||||
|
||||
### DB V120 (GRP_SETTLE)
|
||||
- `carrier_reconciliation_in`(recon_id PK, company_id BIGINT NOT NULL REFERENCES insurance_company(company_id), settle_month CHAR(6) NOT NULL, file_name VARCHAR(200), record_count INT NOT NULL DEFAULT 0, reported_total NUMERIC(15,2) NOT NULL DEFAULT 0, system_total NUMERIC(15,2) NOT NULL DEFAULT 0, variance_amount NUMERIC(15,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'RECEIVED' CHECK IN('RECEIVED','MATCHED','VARIANCE','APPROVED'), 감사). UNIQUE(company_id, settle_month). idx(settle_month).
|
||||
- `reconciliation_variance`(variance_id PK, recon_id BIGINT NOT NULL REFERENCES carrier_reconciliation_in, policy_no VARCHAR(50), expected_amount NUMERIC(15,2), reported_amount NUMERIC(15,2), variance_reason VARCHAR(100), 감사). idx(recon_id).
|
||||
- 메뉴 `CARRIER_RECON_IN`('정산대사입수','/settle/carrier-recon-in','settle/CarrierReconIn', GRP_SETTLE, sort) + 권한. 공통코드 `RECON_IN_STATUS`(RECEIVED 입수/MATCHED 일치/VARIANCE 차이/APPROVED 승인).
|
||||
- insurance_company PK 컬럼명 확인 후 사용(company_id 가정, 다르면 실제명).
|
||||
|
||||
### core/api
|
||||
- `CarrierReconciliationIn*` + `ReconciliationVariance*` VO/Resp/SaveReq/SearchParam + Mapper.
|
||||
- `CarrierReconciliationInService`: 대사 등록(reported_total 입력)/reconcile(system_total=settle_master 또는 income_commission 집계, variance=reported−system, status 자동 MATCHED/VARIANCE)/approve. Controller `/api/carrier-recon-in`.
|
||||
|
||||
### frontend
|
||||
- `pages/settle/CarrierReconIn.tsx`(대사 목록 + 차이내역 Drawer), `api/settle.ts`/`operation.ts`.
|
||||
|
||||
---
|
||||
|
||||
## MOD-6 — 환수 시효/소멸 (statute of limitations) [V121]
|
||||
오래된 미결 환수채권을 만료개월 경과 시 자동 대손(WRITTEN_OFF/EXPIRED).
|
||||
|
||||
### DB V121 (GRP_COMMISSION)
|
||||
- `statute_config`(config_id PK, entity_type VARCHAR(30) NOT NULL UNIQUE CHECK IN('CLAWBACK_RECEIVABLE'), expiration_months INT NOT NULL CHECK>0, is_active CHAR(1) NOT NULL DEFAULT 'Y', 감사). 시드 1행: ('CLAWBACK_RECEIVABLE', 36, 'Y').
|
||||
- 메뉴 `STATUTE_CONFIG`('환수시효관리','/commission/statute','commission/StatuteConfig', GRP_COMMISSION, sort) + 권한.
|
||||
- (별도 status 코드 불필요 — 기존 clawback_receivable status에 WRITTEN_OFF 존재 가정. 없으면 사유코드만 추가.)
|
||||
|
||||
### core/api
|
||||
- `StatuteConfig*` VO/Resp/SaveReq + Mapper. clawback_receivable에 만료대상 조회/대손처리 쿼리(`selectExpired(beforeMonth)`, `writeOffExpired`) 추가(기존 ClawbackReceivableMapper에).
|
||||
- `StatuteService`: config CRUD + `expireOverdue(baseMonth)` = origin_settle_month <= baseMonth−expiration_months AND status='OUTSTANDING' → WRITTEN_OFF(reason EXPIRED) + 카운트. Controller `/api/statute` (config + POST expire).
|
||||
|
||||
### frontend
|
||||
- `pages/commission/StatuteConfig.tsx`(설정 + 만료실행 버튼 + 결과), `api/commission.ts`.
|
||||
|
||||
---
|
||||
|
||||
## MOD-7 — 환수 감면/면제 (chargeback waiver/reduction) [V122]
|
||||
재직 설계사 환수에 대한 감면 신청·승인 워크플로.
|
||||
|
||||
### DB V122 (GRP_COMMISSION)
|
||||
- `chargeback_waiver_request`(request_id PK, contract_id BIGINT NOT NULL REFERENCES contract, agent_id BIGINT NOT NULL REFERENCES agent, settle_month CHAR(6) NOT NULL, original_chargeback NUMERIC(15,2) NOT NULL CHECK>=0, waiver_amount NUMERIC(15,2) NOT NULL CHECK>=0, reason VARCHAR(200), approval_status VARCHAR(20) NOT NULL DEFAULT 'REQUESTED' CHECK IN('REQUESTED','APPROVED','REJECTED'), approver_id BIGINT, approved_at TIMESTAMP, 감사). CHECK waiver_amount<=original_chargeback. idx(agent_id, settle_month).
|
||||
- 메뉴 `CHARGEBACK_WAIVER`('환수감면신청','/commission/chargeback-waiver','commission/ChargebackWaiver', GRP_COMMISSION, sort) + 권한. 공통코드 `WAIVER_STATUS`(REQUESTED 신청/APPROVED 승인/REJECTED 반려).
|
||||
|
||||
### core/api
|
||||
- `ChargebackWaiverRequest*` VO/Resp/SaveReq/SearchParam + Mapper.
|
||||
- `ChargebackWaiverService`: 신청(create)/approve(승인 시 approval_status, approver/approved_at; 환수액 조정은 데모 수준 — 승인분 추적)/reject. waiver<=original 검증. Controller `/api/chargeback-waivers`.
|
||||
|
||||
### frontend
|
||||
- `pages/commission/ChargebackWaiver.tsx`(신청 목록 + 승인/반려 액션), `api/commission.ts`.
|
||||
|
||||
---
|
||||
|
||||
## MOD-8 — 채널별 수수료 차등 (channel-specific commission) [V123]
|
||||
판매채널(직판/방카/홈쇼핑/제휴)별 요율 차등. **하위호환 필수**(channel NULL = 기존 동작).
|
||||
|
||||
### DB V123
|
||||
- `ALTER TABLE contract ADD COLUMN sales_channel VARCHAR(20);` (NULL 허용, NULL=DIRECT 취급)
|
||||
- 공통코드 `SALES_CHANNEL`(DIRECT 직판/BANCASSURANCE 방카/HOME_SHOPPING 홈쇼핑/PARTNERSHIP 제휴).
|
||||
- commission_rate / payout_rule 룰 테이블에 `sales_channel VARCHAR(20)` 컬럼 추가(NULL=모든채널). **실제 룰 테이블명·조회 SQL 확인 필수**(CommissionCalculator가 쓰는 rate 조회). 조회 우선순위: channel 일치 룰 우선, 없으면 channel IS NULL 룰 폴백.
|
||||
- 메뉴 신규 불필요(계약 화면에서 노출). 단 계약 등록/조회에 sales_channel 추가.
|
||||
|
||||
### core/api/batch
|
||||
- ContractVO/Resp/SaveReq에 salesChannel. ContractMapper.xml insert/update/select에 sales_channel.
|
||||
- rate 조회 Mapper SQL에 channel 파라미터 추가(`AND (sales_channel = #{channel} OR sales_channel IS NULL) ORDER BY sales_channel NULLS LAST` 패턴으로 channel 우선). CommissionCalculator가 contract.salesChannel 전달.
|
||||
- **하위호환 회귀 필수 검증**: channel NULL 계약은 기존과 동일 rate. 기존 CommissionCalculatorTest 통과 유지.
|
||||
|
||||
### frontend
|
||||
- 계약 페이지(`pages/contracts/...` 또는 product)에서 sales_channel select 노출(공통코드 SALES_CHANNEL).
|
||||
|
||||
---
|
||||
|
||||
## 실행 순서 (5-Wave, 레이어별 병렬)
|
||||
1. **DBA**: V116~V123 작성 (dba-architect). → SQL 파싱/리뷰.
|
||||
2. **CORE**: 8모듈 VO/Mapper/Enum/MapStruct (core-architect). → `:ga-core:compileJava` GREEN.
|
||||
3. **API+BATCH**: Service/Controller (api-developer) ‖ AggregateStep/배치 연계 (batch-engineer). → `:ga-api:compileJava :ga-batch:compileJava` GREEN.
|
||||
4. **FRONTEND**: 화면+api+라우트 (frontend-developer). → `tsc` GREEN.
|
||||
5. **INFRA/검증**: `./gradlew build`(test) GREEN + Flyway 적용 + 스모크 (PL + infra-reviewer).
|
||||
- 각 Wave 후 PL이 빌드로 직접 검증. 컬럼/이름 불일치는 PL이 즉시 수정.
|
||||
@@ -0,0 +1,46 @@
|
||||
# P10 — 환수 보험사자료 업로드 (#3)
|
||||
|
||||
작성: PL, 2026-06-04. 사용자 "후속 갭 3종 진행" 중 첫 번째.
|
||||
근거: incar `환수보험사자료 업로드`. GA는 LAPSE 계약 기반 자동환수(ChargebackExceptionStep)만 있고,
|
||||
보험사가 통보한 환수자료(증권번호+환수금액)를 직접 인입하는 경로가 없음.
|
||||
|
||||
범위: 풀스택(DB→Core→API→Frontend). 신규 테이블 없음 — 기존 `chargeback`(V6) 재사용.
|
||||
Flyway: **V112 최신 → V113**(메뉴/권한만). 금액 NUMERIC(15,2).
|
||||
|
||||
## 1. chargeback 테이블 (기존, 재사용)
|
||||
cb_id PK / contract_id FK(nullable) / agent_id FK NN / policy_no / lapse_month INT /
|
||||
cb_rate / cb_amount NN / paid_amount(0) / remain_amount NN / settle_month CHAR(6) NN /
|
||||
installment_no / max_installments / status('NEW') / created_at.
|
||||
|
||||
## 2. 업로드 설계
|
||||
- **1 업로드 = 1 settleMonth**(요청 파라미터). 파일 각 행 = 계약별 환수 통보.
|
||||
- 엑셀 헤더(ChargebackUploadExcelVO, @ExcelColumn): `증권번호`(policyNo, order1) / `환수금액`(cbAmount, order2) / `실효월`(lapseMonth YYYYMM 문자열, order3, 선택) / `환수율`(cbRate, order4, 선택).
|
||||
- **agent/contract 해소**: policy_no → contract 조회 → contract_id + agent_id. 미발견 행은 에러행으로 수집(스킵, 결과에 행번호+사유 반환). ContractMapper에 `selectByPolicyNo(policyNo)` 없으면 core가 추가(contract 테이블 policy_no 컬럼 확인).
|
||||
- 생성 ChargebackVO: contractId/agentId(해소), policyNo, lapseMonth(YYYYMM→INT 변환, '202401'→202401 또는 경과월? **chargeback.lapse_month는 INT**: 기존 의미가 경과월(month elapsed)인지 실효연월인지 ChargebackExceptionStep 확인 후 동일 의미로 — 자동환수와 일관성. 불명확하면 실효연월 YYYYMM의 끝2자리(월)이 아니라 **경과월**로: contract_date~lapse_date 개월수 계산. 단순화를 위해 파일에 없으면 null 허용), cbRate, cbAmount, paidAmount=0, remainAmount=cbAmount, settleMonth(param), status='NEW'.
|
||||
- **주의**: 자동환수(ChargebackExceptionStep)와 동일 (contract, settleMonth) 중복 생성 방지 — 동일 contract_id+settle_month NEW 존재 시 스킵 또는 갱신(중복 인입 방지). core에 `existsByContractAndMonth` 또는 service에서 selectByAgent로 확인. 최소: 같은 정산월 재업로드 시 중복 INSERT 경고만(운영자 책임). **권장: contract_id+settle_month UNIQUE 부분 보장 위해 service가 사전 조회 후 skip + 결과 리포트.**
|
||||
- ExcelService.importLargeExcel(file, ChargebackUploadExcelVO.class, 500, consumer) 사용. consumer에서 해소+검증+chargebackMapper.insertBatch. 트랜잭션: @Transactional.
|
||||
|
||||
## 3. 조회(List) — 신규
|
||||
chargeback 목록 화면이 현재 없음. 추가:
|
||||
- core: ChargebackResp(+agentName/contractNo 조인) / ChargebackSearchParam(settleMonth/agentId/status/policyNo) / ChargebackMapper.selectList + countByCondition. (기존 ChargebackVO 유지)
|
||||
- api: ChargebackService.list(PageHelper) / detail. 업로드는 ChargebackUploadService.upload(settleMonth, file)→ExcelImportResult-유사(성공/에러행).
|
||||
|
||||
## 4. API
|
||||
- `POST /api/chargebacks/upload?settleMonth=YYYYMM` (multipart file) — @RequirePermission(CHARGEBACK_DATA, CREATE), @DataChangeLog. 반환: {successCount, errorCount, errors[]}.
|
||||
- `GET /api/chargebacks` — 목록(@RequirePermission READ, PageResponse).
|
||||
- `GET /api/chargebacks/export` — 엑셀(@Transactional).
|
||||
컨트롤러 위치 `controller/settle/ChargebackController` 또는 기존 chargeback 패키지 확인.
|
||||
|
||||
## 5. DB (V113)
|
||||
- 메뉴 `CHARGEBACK_DATA`(환수자료관리), 상위 GRP_SETTLE, path `/settle/chargeback-data`, component `settle/ChargebackData`. (V97 단계마감 등 GRP_SETTLE 패턴)
|
||||
- 권한 READ/CREATE/EXPORT + 역할부여 SUPER_ADMIN·ADMIN 전체, MANAGER READ.
|
||||
- 공통코드 불필요(chargeback status는 기존 코드 재사용 여부 확인 — 없으면 생략).
|
||||
|
||||
## 6. Frontend
|
||||
- `src/api/chargeback.ts` — list/upload(FormData)/export.
|
||||
- `src/pages/settle/ChargebackData.tsx` — 상단 정산월 선택 + 파일 업로드 버튼(업로드 결과 성공/에러행 표시) + AG Grid 목록(증권번호/설계사/환수금액/잔여/상태/정산월) + 엑셀. 기존 업로드 화면(UploadPage 등) 결과표시 패턴 참고.
|
||||
- App.tsx 라우트 `settle/chargeback-data`.
|
||||
|
||||
## 7. 검증(PL)
|
||||
build+test, Flyway V113, GET 목록 200, **샘플 엑셀 업로드 실호출**(정상행 INSERT + 미존재 증권번호 에러행 리포트) 라이브 확인 후 테스트 chargeback 롤백/삭제.
|
||||
</content>
|
||||
@@ -0,0 +1,62 @@
|
||||
# P11 — 시상/신인시상 환수 (#1)
|
||||
|
||||
작성: PL, 2026-06-04. 후속 갭 3종 중 두 번째.
|
||||
근거: incar `유지율시상환수`/`신인도입유치자시상환수`/`시상내역 조회`. GA award_ledger(V92)는
|
||||
지급만 있고 환수(clawback) 처리가 없음. 정착지원금/생보운영지원/지점장수당은 이미
|
||||
`clawback_amount`+`net_amount` 패턴 보유 → **award_ledger도 동일 패턴으로 통일**.
|
||||
|
||||
신인시상은 award_type='NEW_RECRUIT'로 이미 존재 → 별도 테이블 불필요. 모든 award_type에 환수 적용.
|
||||
Flyway: **V113 최신 → V114**. 금액 NUMERIC(15,2), HALF_UP.
|
||||
|
||||
## 1. 현재 award_ledger (V92)
|
||||
ledger_id / agent_id / settle_month / award_type(NEW_RECRUIT/CORPORATE/RETENTION/PERFORMANCE) /
|
||||
base_amount / award_amount / status(CALCULATED/CONFIRMED/PAID/CANCELLED) / created_at / updated_at.
|
||||
aggregateByAgent = SUM(award_amount) WHERE status!=CANCELLED.
|
||||
|
||||
**문제**: clawback_amount/net_amount 없음. settling/life/branch는 net=support−clawback, aggregate=SUM(net).
|
||||
|
||||
## 2. 스키마 변경 (V114) — settling_support_ledger 패턴 미러
|
||||
```sql
|
||||
ALTER TABLE award_ledger
|
||||
ADD COLUMN clawback_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN net_amount NUMERIC(15,2) NOT NULL DEFAULT 0;
|
||||
-- 기존 행 백필: 환수 없던 과거 = net = award
|
||||
UPDATE award_ledger SET net_amount = award_amount WHERE net_amount = 0 AND clawback_amount = 0;
|
||||
-- clawback 음수 방지 + 상태에 CLAWED_BACK 추가(settling 패턴과 통일)
|
||||
ALTER TABLE award_ledger ADD CONSTRAINT chk_award_clawback CHECK (clawback_amount >= 0);
|
||||
ALTER TABLE award_ledger DROP CONSTRAINT <기존 status CHECK 이름>; -- \d 로 확인 (예: award_ledger_status_check)
|
||||
ALTER TABLE award_ledger ADD CONSTRAINT chk_award_status
|
||||
CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED','CLAWED_BACK'));
|
||||
```
|
||||
> 기존 status CHECK 제약명은 `\d award_ledger` 또는 information_schema 로 정확히 확인 후 DROP. 추정명 사용 금지.
|
||||
> net_amount는 음수 허용(환수>시상 시 음수 가능)이나, **§4 검증에서 clawback ≤ award_amount 강제**하므로 실제 net≥0. (지급성이므로 음수 차단 — settling 동일)
|
||||
|
||||
메뉴 변경 없음(award 화면 기존). 공통코드 AWARD_STATUS에 CLAWED_BACK 있으면 추가(없으면 생략).
|
||||
|
||||
## 3. Core
|
||||
- AwardLedgerVO/Resp/SaveReq에 `clawbackAmount`, `netAmount` 추가.
|
||||
- `AwardLedgerMapper.aggregateByAgent` XML: `SUM(award_amount)` → **`SUM(net_amount)`** (net=award−clawback). status!=CANCELLED 유지.
|
||||
- 신규 Mapper 메서드 `applyClawback(@Param ledgerId, @Param clawbackAmount, @Param netAmount, @Param status)` (+XML): clawback_amount/net_amount/status='CLAWED_BACK'/updated_at 갱신.
|
||||
- createLedger 시 net_amount = award_amount − clawback_amount 저장(insert XML에 두 컬럼 포함).
|
||||
|
||||
## 4. API (AwardService 확장)
|
||||
- `createLedger`: clawback_amount(SaveReq, 기본0) 받아 **clawback ≤ award_amount 검증(초과 시 E411)**, net = award − clawback 계산해 저장.
|
||||
- **신규 액션** `applyClawback(ledgerId, AwardClawbackReq{clawbackAmount, reason})` @Transactional:
|
||||
- 원장 조회(없으면 NOT_FOUND). 이미 CANCELLED면 E411.
|
||||
- clawbackAmount > award_amount 면 E411("환수액이 시상액 초과").
|
||||
- net = award_amount − clawbackAmount. mapper.applyClawback(ledgerId, clawback, net, 'CLAWED_BACK').
|
||||
- @DataChangeLog.
|
||||
- Controller: `PUT /api/awards/ledgers/{id}/clawback` (@RequirePermission(menu=AWARD, action=UPDATE)). 기존 award 컨트롤러 경로 패턴 확인.
|
||||
|
||||
> **AggregateStep 무변경**: awardMapper.aggregateByAgent가 SUM(net_amount) 반환하므로 환수가 자동으로 other_commission 차감에 반영. net<0이면 P9 환수채권 이월에 자연 연계.
|
||||
|
||||
## 5. Frontend (기존 Award 화면 확장)
|
||||
- award 원장 그리드에 환수액/실지급(net) 컬럼 추가.
|
||||
- 행 액션 "환수처리"(PermissionButton UPDATE) → 모달(환수액 입력 + 사유) → PUT /clawback. 성공 시 invalidate.
|
||||
- api 모듈에 applyClawback 추가. 기존 award api/페이지 파일명 확인 후 확장.
|
||||
|
||||
## 6. 검증(PL)
|
||||
build+test, Flyway V114(award_ledger 컬럼/제약/백필), 라이브: award 원장 1건 생성(net=award) →
|
||||
applyClawback(부분환수) → net 감소·status CLAWED_BACK 확인 → clawback>award E411 → aggregateByAgent
|
||||
가 net 반영하는지 SQL 확인. 테스트 데이터 정리.
|
||||
</content>
|
||||
@@ -0,0 +1,65 @@
|
||||
# P12 — 지급대상건 구성·수정 (#2)
|
||||
|
||||
작성: PL, 2026-06-04. 후속 갭 3종 중 마지막.
|
||||
근거: incar `지급대상건구성`/`지급대상건수정`/`지급대상건조회`. GA는 settle_master를 건별
|
||||
confirm/hold/release만 가능, **정산월 단위 지급대상 배치 구성(일괄 포함/제외 후 확정)** 레이어 부재.
|
||||
|
||||
범위: 풀스택. 기존 settle_master 위의 **구성/스냅샷 레이어**(settle_master 상태는 본 범위에서 변경 안 함 — 결합 최소화). Flyway: **V114 최신 → V115**. 금액 NUMERIC(15,2).
|
||||
|
||||
## 1. 개념
|
||||
- 정산월의 지급대상(settle_master, payable_amount>0)을 모아 **배치(payment_batch)** 로 구성.
|
||||
- 각 건(payment_batch_item)을 **포함/제외**(사유) 편집 → 합계 재계산.
|
||||
- **확정(finalize)**: 배치 잠금(FINALIZED). 확정된 포함건 목록 = 지급대상 확정본(스냅샷).
|
||||
- AccountingClose(V75 DRAFT/마감/재오픈) 패턴 준용. **settle_master.status는 본 범위에서 미변경**(향후 finalize→지급실행 연계는 별도).
|
||||
|
||||
## 2. 스키마 (V115)
|
||||
### payment_batch (지급대상 배치)
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| batch_id | BIGSERIAL PK | |
|
||||
| settle_month | CHAR(6) NN | |
|
||||
| batch_name | VARCHAR(100) NN | |
|
||||
| status | VARCHAR(20) NN DEFAULT 'DRAFT' | DRAFT/FINALIZED/CANCELLED |
|
||||
| total_count | INT NN DEFAULT 0 | 포함건수 |
|
||||
| total_payable | NUMERIC(15,2) NN DEFAULT 0 | 포함건 payable 합 |
|
||||
| created_at/created_by/finalized_at/finalized_by/updated_at | | |
|
||||
- idx(settle_month, status). CHECK status IN('DRAFT','FINALIZED','CANCELLED').
|
||||
|
||||
### payment_batch_item (지급대상건)
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| item_id | BIGSERIAL PK | |
|
||||
| batch_id | BIGINT NN FK payment_batch | |
|
||||
| settle_id | BIGINT NN FK settle_master | |
|
||||
| agent_id | BIGINT NN FK agent | |
|
||||
| payable_amount | NUMERIC(15,2) NN | settle_master.payable_amount 스냅샷 |
|
||||
| included | BOOLEAN NN DEFAULT true | 포함여부 |
|
||||
| exclude_reason | VARCHAR(300) | 제외사유 |
|
||||
| created_at | | |
|
||||
- UNIQUE(batch_id, settle_id). idx(batch_id).
|
||||
|
||||
### 메뉴/권한/코드
|
||||
- 메뉴 `PAYMENT_TARGET`(지급대상관리), GRP_SETTLE, `/settle/payment-target`, `settle/PaymentTarget`.
|
||||
- 권한 READ/CREATE/UPDATE/EXECUTE(확정) + 역할(SUPER_ADMIN·ADMIN 전체, MANAGER READ).
|
||||
- 공통코드 PAYMENT_BATCH_STATUS(DRAFT=작성중/FINALIZED=확정/CANCELLED=취소).
|
||||
|
||||
## 3. API (PaymentBatchService, `/api/payment-batches`)
|
||||
- `POST` create(settleMonth, batchName) @Transactional: payment_batch(DRAFT) INSERT → settle_master 조회(settle_month=, payable_amount>0, status != 'PAID') → 각 행 payment_batch_item(included=true, payable 스냅샷) INSERT → total_count/total_payable 집계 UPDATE. 동일월 DRAFT 중복 생성 허용(이름으로 구분).
|
||||
- `GET` list(settleMonth/status 필터, PageResponse) / `GET /{id}` detail(배치 + items).
|
||||
- `PUT /{id}/items/{itemId}` updateItem(included, excludeReason) @Transactional: DRAFT 배치만(FINALIZED/CANCELLED 시 E411). 항목 갱신 후 배치 total 재집계.
|
||||
- `PUT /{id}/finalize` @Transactional, @RequirePermission EXECUTE: DRAFT→FINALIZED, finalized_at/by. 포함건 0이면 E411.
|
||||
- `PUT /{id}/cancel`: DRAFT/FINALIZED→CANCELLED.
|
||||
- `GET /{id}/export` 엑셀(@Transactional).
|
||||
- @RequirePermission(PAYMENT_TARGET, *) + @DataChangeLog(쓰기).
|
||||
|
||||
총계 재집계: `total_count = COUNT(included=true)`, `total_payable = SUM(payable WHERE included)` — Mapper에 recalcTotals(batchId) 제공.
|
||||
|
||||
## 4. Frontend (PaymentTarget.tsx)
|
||||
- 배치 목록(정산월/상태 필터) + "구성" 버튼(정산월+배치명 입력 → create).
|
||||
- 배치 행 클릭 → 상세: 배치 헤더(상태/포함건수/합계) + items AG Grid(설계사/payable/포함체크/제외사유). DRAFT일 때 포함토글·사유 편집(PermissionButton UPDATE) → updateItem. 합계행.
|
||||
- "확정" 버튼(PermissionButton EXECUTE, DRAFT만) → finalize. "취소" 버튼. 엑셀.
|
||||
- React Query, 변경 후 invalidate.
|
||||
|
||||
## 5. 검증(PL)
|
||||
build+test, Flyway V115, 라이브: create(특정월) → items 자동생성 확인 → updateItem 제외 → total 재집계 확인 → finalize → 잠금(이후 updateItem E411) → cancel. 테스트 데이터 정리.
|
||||
</content>
|
||||
@@ -0,0 +1,255 @@
|
||||
# P7 — incar 갭 반영: 수수료 계산 7개 도메인
|
||||
|
||||
작성: PL (자율 모드), 2026-05-31
|
||||
근거: incar CMS 116메뉴 vs ga-pro 96메뉴 갭 분석. 사용자 지시 "필요한 메뉴 적용 + 수수료 공식 다 적용".
|
||||
범위: 🅰 수수료 계산 7개 도메인 풀스택 (DB→Core→API→Frontend→Infra).
|
||||
|
||||
기존 패턴 100% 준수 (template = 수금수수료 V41 / CollectionCommission*). 모든 금액 `NUMERIC(15,2)`, 율 `NUMERIC(7,4)`(소수, 0~1), 반올림 HALF_UP.
|
||||
|
||||
Flyway: **V87이 최신 → V88부터.** 각 도메인은 rule(+) ledger 2테이블 패턴. 메뉴는 GRP_COMMISSION 하위.
|
||||
|
||||
---
|
||||
|
||||
## 도메인별 설계
|
||||
|
||||
### D1. 수입수수료 + 수지차 (INCOME_COMM) — V88
|
||||
보험사가 GA에 지급한 **수입** 측. 기존 원장(LEDGER_*)은 설계사 **지급** 측. 수지차 = 수입 − 지급.
|
||||
|
||||
**income_commission_ledger** (수입수수료 원장)
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| income_id | BIGSERIAL PK | |
|
||||
| contract_id | BIGINT NN FK contract | |
|
||||
| carrier_id | BIGINT NN FK insurance_company | 보험사 |
|
||||
| settle_month | CHAR(6) NN | |
|
||||
| premium_amount | NUMERIC(15,2) NN | 보험료 |
|
||||
| company_rate | NUMERIC(7,4) NN | 보험사요율 |
|
||||
| income_amount | NUMERIC(15,2) NN | **= premium_amount × company_rate** |
|
||||
| commission_type | VARCHAR(20) NN | RECRUIT/MAINTAIN 등 |
|
||||
| status | VARCHAR(20) NN DEFAULT 'CALCULATED' | CALCULATED/CONFIRMED/CANCELLED |
|
||||
| created_at/updated_at | | |
|
||||
idx: (carrier_id, settle_month), (contract_id), (status, settle_month)
|
||||
|
||||
**margin_summary** (수지차 집계 — recalc 액션으로 채움)
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| margin_id | BIGSERIAL PK | |
|
||||
| settle_month | CHAR(6) NN | |
|
||||
| agent_id | BIGINT FK agent (NULL이면 전체) | |
|
||||
| income_total | NUMERIC(15,2) NN DEFAULT 0 | 수입 합 |
|
||||
| payout_total | NUMERIC(15,2) NN DEFAULT 0 | 지급 합(settle_master 기준) |
|
||||
| margin_amount | NUMERIC(15,2) NN DEFAULT 0 | **= income_total − payout_total** |
|
||||
| margin_rate | NUMERIC(7,4) | = margin/income (income>0) |
|
||||
| created_at | | |
|
||||
UNIQUE(settle_month, agent_id)
|
||||
|
||||
**공식**: income_amount = premium × company_rate. margin = Σincome − Σpayout.
|
||||
|
||||
### D2. 소개·이관수수료 (REFERRAL_COMM) — V89
|
||||
계약을 소개(REFERRAL)하거나 이관(TRANSFER)한 설계사에게 주는 수수료.
|
||||
|
||||
**referral_commission_rule**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| rule_id | BIGSERIAL PK | |
|
||||
| referral_type | VARCHAR(20) NN | REFERRAL/TRANSFER |
|
||||
| calc_type | VARCHAR(10) NN | RATE/FIXED |
|
||||
| commission_rate | NUMERIC(7,4) | calc_type=RATE 일 때 |
|
||||
| fixed_amount | NUMERIC(15,2) | calc_type=FIXED 일 때 |
|
||||
| effective_from DATE NN / effective_to DATE | | |
|
||||
CHECK referral_type IN ('REFERRAL','TRANSFER'); CHECK calc_type IN ('RATE','FIXED')
|
||||
|
||||
**referral_commission_ledger**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| ledger_id | BIGSERIAL PK | |
|
||||
| contract_id | BIGINT NN FK | |
|
||||
| referrer_agent_id | BIGINT NN FK agent | 소개/이관한 설계사 |
|
||||
| settle_month | CHAR(6) NN | |
|
||||
| referral_type | VARCHAR(20) NN | |
|
||||
| base_amount | NUMERIC(15,2) NN | 기준금액(수수료/보험료) |
|
||||
| commission_rate | NUMERIC(7,4) | |
|
||||
| commission_amount | NUMERIC(15,2) NN | **= base×rate (RATE) 또는 fixed_amount (FIXED)** |
|
||||
| status | VARCHAR(20) NN DEFAULT 'CALCULATED' | |
|
||||
idx: (referrer_agent_id, settle_month),(contract_id)
|
||||
|
||||
### D3. 정착지원금 (SETTLING_SUPPORT) — V90
|
||||
신입/경력 설계사 정착 지원금. 등급별 정액 지급 + 조기 해촉 시 환수.
|
||||
|
||||
**settling_support_rule**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| rule_id BIGSERIAL PK | | |
|
||||
| agent_grade | VARCHAR(20) NN | 설계사 등급 |
|
||||
| support_amount | NUMERIC(15,2) NN | 월 정액 지원금 |
|
||||
| duration_months | INT NN | 약정 지원 개월수 |
|
||||
| clawback_months | INT NN | 이 개월 내 해촉 시 환수 대상 |
|
||||
| effective_from DATE NN / effective_to DATE | | |
|
||||
|
||||
**settling_support_ledger**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| ledger_id BIGSERIAL PK | | |
|
||||
| agent_id BIGINT NN FK agent | | |
|
||||
| settle_month CHAR(6) NN | | |
|
||||
| support_amount NUMERIC(15,2) NN | 지급액 | |
|
||||
| clawback_amount NUMERIC(15,2) NN DEFAULT 0 | 환수액 | |
|
||||
| net_amount NUMERIC(15,2) NN | **= support_amount − clawback_amount** | |
|
||||
| status VARCHAR(20) NN DEFAULT 'CALCULATED' | PAID/CLAWED_BACK 포함 | |
|
||||
idx: (agent_id, settle_month)
|
||||
|
||||
**공식**: 지급 = 등급별 support_amount. 환수 = 조기해촉시 support_amount × (잔여약정개월/약정개월) (clawback service 계산).
|
||||
|
||||
### D4. 인정실적 + 환산보험료 (RECOGNIZED_PERF) — V91
|
||||
환산보험료 = 보험료 × 상품별환산율. 인정실적 = 환산보험료 × 인정율. 시상/등급의 기초.
|
||||
|
||||
**product_conversion_rate** (상품별 환산율)
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| rate_id BIGSERIAL PK | | |
|
||||
| product_id BIGINT NN FK product | | |
|
||||
| conversion_rate NUMERIC(7,4) NN | 환산율 (예: 0.5=50%) | |
|
||||
| effective_from DATE NN / effective_to DATE | | |
|
||||
UNIQUE(product_id, effective_from)
|
||||
|
||||
**recognized_performance_ledger**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| ledger_id BIGSERIAL PK | | |
|
||||
| agent_id BIGINT NN FK agent | | |
|
||||
| contract_id BIGINT NN FK contract | | |
|
||||
| settle_month CHAR(6) NN | | |
|
||||
| premium_amount NUMERIC(15,2) NN | 보험료 | |
|
||||
| conversion_rate NUMERIC(7,4) NN | 적용 환산율 | |
|
||||
| converted_premium NUMERIC(15,2) NN | **= premium × conversion_rate** | |
|
||||
| recognition_rate NUMERIC(7,4) NN | 인정율 | |
|
||||
| recognized_amount NUMERIC(15,2) NN | **= converted_premium × recognition_rate** | |
|
||||
| status VARCHAR(20) NN DEFAULT 'CALCULATED' | | |
|
||||
idx: (agent_id, settle_month),(contract_id)
|
||||
|
||||
### D5. 시상 (AWARD) — V92
|
||||
신인도입/법인/유지율 등 시상. 조건 충족 시 정액 또는 정률.
|
||||
|
||||
**award_rule**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| rule_id BIGSERIAL PK | | |
|
||||
| award_type VARCHAR(20) NN | NEW_RECRUIT/CORPORATE/RETENTION/PERFORMANCE | |
|
||||
| calc_type VARCHAR(10) NN | RATE/FIXED | |
|
||||
| threshold_amount NUMERIC(15,2) | 충족 기준 실적(이상) | |
|
||||
| award_rate NUMERIC(7,4) | RATE | |
|
||||
| award_fixed NUMERIC(15,2) | FIXED | |
|
||||
| effective_from DATE NN / effective_to DATE | | |
|
||||
CHECK award_type IN (...); CHECK calc_type IN ('RATE','FIXED')
|
||||
|
||||
**award_ledger**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| ledger_id BIGSERIAL PK | | |
|
||||
| agent_id BIGINT NN FK agent | | |
|
||||
| settle_month CHAR(6) NN | | |
|
||||
| award_type VARCHAR(20) NN | | |
|
||||
| base_amount NUMERIC(15,2) NN | 기준 실적 | |
|
||||
| award_amount NUMERIC(15,2) NN | **= base×award_rate (RATE) 또는 award_fixed (FIXED)** | |
|
||||
| status VARCHAR(20) NN DEFAULT 'CALCULATED' | | |
|
||||
idx: (agent_id, settle_month),(award_type, settle_month)
|
||||
|
||||
### D6. 생보운영지원수수료 (LIFE_OP_SUPPORT) — V93
|
||||
생보 계약 운영지원 수수료. 실적 기준 지급률 + 환수.
|
||||
|
||||
**life_support_rule**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| rule_id BIGSERIAL PK | | |
|
||||
| agent_grade VARCHAR(20) NN | | |
|
||||
| support_rate NUMERIC(7,4) NN | 지급률 | |
|
||||
| effective_from DATE NN / effective_to DATE | | |
|
||||
|
||||
**life_support_ledger**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| ledger_id BIGSERIAL PK | | |
|
||||
| agent_id BIGINT NN FK agent | | |
|
||||
| settle_month CHAR(6) NN | | |
|
||||
| base_amount NUMERIC(15,2) NN | 생보 실적 | |
|
||||
| support_rate NUMERIC(7,4) NN | | |
|
||||
| support_amount NUMERIC(15,2) NN | **= base × support_rate** | |
|
||||
| clawback_amount NUMERIC(15,2) NN DEFAULT 0 | 환수 | |
|
||||
| net_amount NUMERIC(15,2) NN | = support − clawback | |
|
||||
| status VARCHAR(20) NN DEFAULT 'CALCULATED' | | |
|
||||
idx: (agent_id, settle_month)
|
||||
|
||||
### D7. 지점장수당 (BRANCH_MGR_ALLOWANCE) — V94
|
||||
지점장이 소속 조직 실적에 대해 받는 수당. cap 제한 + 환수.
|
||||
|
||||
**branch_mgr_allowance_rule**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| rule_id BIGSERIAL PK | | |
|
||||
| position_code VARCHAR(20) NN | 직책(지점장 등) | |
|
||||
| allowance_rate NUMERIC(7,4) NN | 수당율 | |
|
||||
| cap_amount NUMERIC(15,2) | 상한(NULL=무제한) | |
|
||||
| effective_from DATE NN / effective_to DATE | | |
|
||||
|
||||
**branch_mgr_allowance_ledger**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| ledger_id BIGSERIAL PK | | |
|
||||
| manager_agent_id BIGINT NN FK agent | 지점장 | |
|
||||
| org_id BIGINT NN FK organization | 대상 조직 | |
|
||||
| settle_month CHAR(6) NN | | |
|
||||
| base_amount NUMERIC(15,2) NN | 조직 실적 | |
|
||||
| allowance_rate NUMERIC(7,4) NN | | |
|
||||
| allowance_amount NUMERIC(15,2) NN | **= LEAST(base×rate, cap)** | |
|
||||
| clawback_amount NUMERIC(15,2) NN DEFAULT 0 | | |
|
||||
| status VARCHAR(20) NN DEFAULT 'CALCULATED' | | |
|
||||
idx: (manager_agent_id, settle_month),(org_id, settle_month)
|
||||
|
||||
> **확정 FK (DB 실검증):** contract(contract_id) · agent(agent_id) · product(product_id) · organization(org_id) · **insurance_company(company_id)** · settle_master. 보험사 FK는 `carrier_id BIGINT REFERENCES insurance_company(company_id)`. 수지차 payout은 settle_master 집계로 산출(dba가 컬럼 확인).
|
||||
|
||||
---
|
||||
|
||||
## 메뉴 (V95) — GRP_COMMISSION 하위 7개 PAGE
|
||||
| menu_code | menu_name | menu_path | component_path | sort |
|
||||
|---|---|---|---|---|
|
||||
| INCOME_COMM | 수입수수료/수지차 | /commission/income | commission/IncomeCommission | 80 |
|
||||
| REFERRAL_COMM | 소개·이관수수료 | /commission/referral | commission/ReferralCommission | 90 |
|
||||
| SETTLING_SUPPORT | 정착지원금 | /commission/settling-support | commission/SettlingSupport | 100 |
|
||||
| RECOGNIZED_PERF | 인정실적 | /commission/recognized | commission/RecognizedPerformance | 110 |
|
||||
| AWARD | 시상 | /commission/award | commission/Award | 120 |
|
||||
| LIFE_OP_SUPPORT | 생보운영지원수수료 | /commission/life-support | commission/LifeSupport | 130 |
|
||||
| BRANCH_MGR_ALLOWANCE | 지점장수당 | /commission/branch-allowance | commission/BranchAllowance | 140 |
|
||||
|
||||
권한: READ/CREATE/UPDATE/DELETE/EXPORT. 역할부여: SUPER_ADMIN/ADMIN 전체, MANAGER READ/CREATE/UPDATE/EXPORT, AGENT READ(INCOME_COMM 제외 — 본인 관련만). V54 패턴 그대로.
|
||||
|
||||
## 공통코드 (V96)
|
||||
| group_code | 코드 |
|
||||
|---|---|
|
||||
| INCOME_STATUS | CALCULATED/CONFIRMED/CANCELLED |
|
||||
| REFERRAL_TYPE | REFERRAL=소개/TRANSFER=이관 |
|
||||
| CALC_TYPE | RATE=정률/FIXED=정액 |
|
||||
| SUPPORT_STATUS | CALCULATED/PAID/CLAWED_BACK |
|
||||
| AWARD_TYPE | NEW_RECRUIT=신인도입/CORPORATE=법인/RETENTION=유지율/PERFORMANCE=실적 |
|
||||
|
||||
CommissionType enum 확장: INCOME/REFERRAL/TRANSFER/SETTLING/RECOGNIZED/AWARD/LIFE_SUPPORT/BRANCH_ALLOWANCE 추가 + V40 COMMISSION_TYPE 공통코드 동기화.
|
||||
|
||||
---
|
||||
|
||||
## Wave 실행
|
||||
| Wave | 담당 | 산출물 |
|
||||
|---|---|---|
|
||||
| 1 | dba-architect | V88~V96 (도메인 7 + 메뉴 + 공통코드). FK 테이블명 실검증. |
|
||||
| 2 | core-architect | 14테이블 VO/Resp/SaveReq/SearchParam + Mapper IF+XML + CommissionType enum 확장 |
|
||||
| 3 | api-developer | 7 Service + 7 Controller (@RequirePermission/@DataChangeLog, 공식 계산 Service 내장) |
|
||||
| 4 | frontend-developer | 7 페이지 + api/commission.ts 확장 + App.tsx 라우트 |
|
||||
| 5 | infra-reviewer | `./gradlew build` + 프론트 빌드 + smoke. README/HANDOFF/DOMAIN_KNOWLEDGE 갱신 |
|
||||
|
||||
---
|
||||
|
||||
## 완료 상태 (2026-05-31)
|
||||
schema **v96** 적용(Flyway V88~V96 success), 전체 `./gradlew build` SUCCESSFUL, 프론트 빌드 OK, ga-api 8082 라이브. P7 14개 GET + 수지차 recalc POST 전수 호출 5xx 0건. 수입수수료(보험료×요율=6,000,000)·수지차(수입−지급) 공식 런타임 실측 검증 후 테스트데이터 정리.
|
||||
|
||||
**PL 통합검증에서 잡은 버그 3건(팀원 GET-only smoke가 놓침) — 수정 완료:**
|
||||
1. 프론트 인정실적 경로 `/rules` → 백엔드 `/rates` 불일치 → `api/commission.ts` 수정.
|
||||
2. 프론트 수지차 경로 `/margin` → 백엔드 `/margins` 불일치 → 수정.
|
||||
3. 수지차 recalc 3중 결함: 컨트롤러 `@RequestBody`(프론트는 쿼리파라미터)→`@RequestParam`, 서비스 `payoutTotal=0` 하드코딩 → settle_master.net_amount 실집계(`sumPayoutByMonth`)+`sumIncomeByMonth` 신규, `selectByMonthAndAgent`의 `#{agentId} IS NULL` Postgres typed-null 오류 → `<choose>`로 교체.
|
||||
@@ -0,0 +1,169 @@
|
||||
# P8 — incar 갭 반영: 운영/정산 프로세스 7개 도메인 (🅱)
|
||||
|
||||
작성: PL (자율 모드), 2026-05-31. 사용자 지시 "다 적용". P7(🅰 수수료계산 7)에 이어 🅱 운영 7개로 incar 갭 14개 완성.
|
||||
범위: 풀스택(DB→Core→API→Frontend→Infra). 패턴 100% 준수(template = 수금수수료 V41 / CollectionCommission*, P7과 동일).
|
||||
Flyway: **V96이 최신 → V97부터.** 메뉴는 적절한 상위그룹 하위. 금액 NUMERIC(15,2), 율 NUMERIC(7,4), HALF_UP.
|
||||
|
||||
**확정 FK**: agent(agent_id), contract(contract_id), organization(org_id), insurance_company(company_id), grade(grade_id), settle_master.
|
||||
|
||||
---
|
||||
|
||||
## 도메인별 설계
|
||||
|
||||
### D8. 단계마감 (STEP_CLOSE) — V97
|
||||
월 정산을 단계별로 마감(수신→계산→검증→지급). 기존 PERIOD_CLOSE(단일 잠금) 보강.
|
||||
|
||||
**settlement_step_close**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| step_close_id | BIGSERIAL PK | |
|
||||
| settle_month | CHAR(6) NN | |
|
||||
| step_code | VARCHAR(20) NN | RECEIVE/CALC/VERIFY/PAY |
|
||||
| step_seq | INT NN | 단계 순서 1~4 |
|
||||
| status | VARCHAR(20) NN DEFAULT 'OPEN' | OPEN/CLOSED |
|
||||
| closed_at TIMESTAMP / closed_by BIGINT / created_at | | |
|
||||
UNIQUE(settle_month, step_code); CHECK step_code IN(...); CHECK status IN('OPEN','CLOSED')
|
||||
공식 없음(워크플로우). 액션: close(단계마감), reopen.
|
||||
|
||||
### D9. 본사정산 (HQ_SETTLE) — V98
|
||||
GA 본사 자체의 수입/비용 정산 항목(설계사 지급과 별개의 GA 손익).
|
||||
|
||||
**hq_settle_item**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| item_id | BIGSERIAL PK | |
|
||||
| settle_month | CHAR(6) NN | |
|
||||
| item_type | VARCHAR(20) NN | INCOME(수입)/EXPENSE(비용) |
|
||||
| item_name | VARCHAR(100) NN | 항목명 |
|
||||
| amount | NUMERIC(15,2) NN | 금액 |
|
||||
| memo | VARCHAR(300) | |
|
||||
| status | VARCHAR(20) NN DEFAULT 'REGISTERED' | REGISTERED/CONFIRMED |
|
||||
| created_at/created_by/updated_at | | |
|
||||
idx: (settle_month, item_type). CHECK item_type IN('INCOME','EXPENSE')
|
||||
공식: 본사손익 = Σ(INCOME amount) − Σ(EXPENSE amount). 조회 집계 엔드포인트 제공.
|
||||
|
||||
### D10. 등급평가 (GRADE_EVAL) — V99
|
||||
설계사 실적 점수로 등급 산정. rule(점수구간→등급) + 평가원장.
|
||||
|
||||
**grade_eval_rule**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| rule_id BIGSERIAL PK | | |
|
||||
| grade_id | BIGINT NN FK grade | 부여 등급 |
|
||||
| min_score | NUMERIC(7,2) NN | 이 점수 이상이면 해당 등급 |
|
||||
| effective_from DATE NN / effective_to DATE | | |
|
||||
|
||||
**agent_grade_evaluation**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| eval_id BIGSERIAL PK | | |
|
||||
| agent_id BIGINT NN FK agent | | |
|
||||
| eval_period CHAR(6) NN | 평가기준월 | |
|
||||
| performance_score NUMERIC(7,2) NN | 실적점수 | |
|
||||
| prev_grade_id BIGINT FK grade | 기존 등급 | |
|
||||
| evaluated_grade_id BIGINT FK grade | 산정 등급 | |
|
||||
| status VARCHAR(20) NN DEFAULT 'EVALUATED' | EVALUATED/APPLIED | |
|
||||
idx: (agent_id, eval_period). UNIQUE(agent_id, eval_period)
|
||||
공식: evaluated_grade = score 이상인 최대 min_score 룰의 grade. Service가 grade_eval_rule 조회해 결정.
|
||||
|
||||
### D11. 공동계약 (JOINT_CONTRACT) — V100
|
||||
한 계약을 여러 설계사가 공동 모집 → 지분율(share_rate)로 수수료 배분.
|
||||
|
||||
**joint_contract**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| joint_id BIGSERIAL PK | | |
|
||||
| contract_id BIGINT NN FK contract | | |
|
||||
| agent_id BIGINT NN FK agent | | |
|
||||
| role VARCHAR(10) NN | LEAD/SUB | |
|
||||
| share_rate NUMERIC(7,4) NN | 지분율(합=1) | |
|
||||
| created_at/created_by | | |
|
||||
UNIQUE(contract_id, agent_id); CHECK role IN('LEAD','SUB'); CHECK share_rate>=0 AND share_rate<=1
|
||||
공식: 설계사몫 = 계약수수료 × share_rate. (검증: 계약별 share_rate 합=1 — Service 가드)
|
||||
|
||||
### D12. 보증보험 (GUARANTEE_INS) — V101
|
||||
설계사가 가입하는 신원보증보험 가입현황 추적(공식 없음, 관리).
|
||||
|
||||
**agent_guarantee_insurance**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| guarantee_id BIGSERIAL PK | | |
|
||||
| agent_id BIGINT NN FK agent | | |
|
||||
| policy_no VARCHAR(50) NN | 증권번호 | |
|
||||
| carrier_id BIGINT FK insurance_company(company_id) | 보증보험사 | |
|
||||
| guarantee_amount NUMERIC(15,2) NN | 보증금액 | |
|
||||
| start_date DATE NN / end_date DATE NN | 보증기간 | |
|
||||
| status VARCHAR(20) NN DEFAULT 'ACTIVE' | ACTIVE/EXPIRED/CANCELLED | |
|
||||
idx: (agent_id, status),(end_date)
|
||||
|
||||
### D13. 적립금 (RESERVE_FUND) — V102
|
||||
환수 대비 수수료 일부 적립 + 잔액 관리.
|
||||
|
||||
**agent_reserve_fund**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| reserve_id BIGSERIAL PK | | |
|
||||
| agent_id BIGINT NN FK agent | | |
|
||||
| settle_month CHAR(6) NN | | |
|
||||
| reserve_in NUMERIC(15,2) NN DEFAULT 0 | 적립액 | |
|
||||
| reserve_out NUMERIC(15,2) NN DEFAULT 0 | 인출/사용액 | |
|
||||
| balance NUMERIC(15,2) NN | **= 전월잔액 + reserve_in − reserve_out** | |
|
||||
| created_at | | |
|
||||
UNIQUE(agent_id, settle_month). idx (agent_id, settle_month)
|
||||
공식: balance = (직전월 balance) + reserve_in − reserve_out. Service가 직전월 조회해 누적.
|
||||
|
||||
### D14. 수수료 시뮬레이터 (COMM_SIMULATOR) — V103
|
||||
보험료/요율/지급률 입력 → 수수료·세금·실지급·분급·환수예상 산출. 시나리오 저장.
|
||||
|
||||
**commission_simulation**
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| sim_id BIGSERIAL PK | | |
|
||||
| sim_name VARCHAR(100) NN | 시나리오명 | |
|
||||
| premium_amount NUMERIC(15,2) NN | 입력 보험료 | |
|
||||
| company_rate NUMERIC(7,4) NN | 보험사요율 | |
|
||||
| payout_rate NUMERIC(7,4) NN | 지급률 | |
|
||||
| commission_year INT NN | 회차 | |
|
||||
| company_payment NUMERIC(15,2) NN | = 보험료×요율 | |
|
||||
| agent_commission NUMERIC(15,2) NN | = 보험사지급액×지급률 | |
|
||||
| withholding_tax NUMERIC(15,2) NN | = 설계사수수료×3.3% | |
|
||||
| net_amount NUMERIC(15,2) NN | = 수수료−원천세 | |
|
||||
| expected_chargeback NUMERIC(15,2) | 예상 환수(13개월 가정 50% 등) | |
|
||||
| created_at/created_by | | |
|
||||
공식(DOMAIN_KNOWLEDGE 7장과 동일): company_payment=premium×company_rate, agent_commission=company_payment×payout_rate, withholding_tax=agent_commission×0.033, net=agent_commission−withholding_tax. Service의 simulate 액션이 입력만 받아 계산 후 저장/반환.
|
||||
|
||||
---
|
||||
|
||||
## 메뉴 (V104)
|
||||
| menu_code | menu_name | 상위그룹 | menu_path | component_path |
|
||||
|---|---|---|---|---|
|
||||
| STEP_CLOSE | 단계마감 | GRP_SETTLE | /settle/step-close | settle/StepClose |
|
||||
| HQ_SETTLE | 본사정산 | GRP_SETTLE | /settle/hq-settle | settle/HqSettle |
|
||||
| GRADE_EVAL | 등급평가 | GRP_ORG | /org/grade-eval | org/GradeEvaluation |
|
||||
| JOINT_CONTRACT | 공동계약 | GRP_PRD | /contracts/joint | contracts/JointContract |
|
||||
| GUARANTEE_INS | 보증보험 | GRP_ORG | /org/guarantee | org/GuaranteeInsurance |
|
||||
| RESERVE_FUND | 적립금 | GRP_COMMISSION | /commission/reserve | commission/ReserveFund |
|
||||
| COMM_SIMULATOR | 수수료 시뮬레이터 | GRP_COMMISSION | /commission/simulator | commission/CommissionSimulator |
|
||||
|
||||
권한 READ/CREATE/UPDATE/DELETE/EXPORT(STEP_CLOSE는 READ/UPDATE/APPROVE). 역할부여 V54/V95 패턴.
|
||||
|
||||
## 공통코드 (V105)
|
||||
| group_code | 코드 |
|
||||
|---|---|
|
||||
| STEP_CODE | RECEIVE=수신마감/CALC=계산마감/VERIFY=검증마감/PAY=지급마감 |
|
||||
| STEP_STATUS | OPEN=열림/CLOSED=마감 |
|
||||
| HQ_ITEM_TYPE | INCOME=수입/EXPENSE=비용 |
|
||||
| JOINT_ROLE | LEAD=주설계사/SUB=공동설계사 |
|
||||
| GUARANTEE_STATUS | ACTIVE=유효/EXPIRED=만료/CANCELLED=해지 |
|
||||
| GRADE_EVAL_STATUS | EVALUATED=평가완료/APPLIED=반영완료 |
|
||||
|
||||
---
|
||||
|
||||
## Wave 실행 (P7과 동일 팀/패턴)
|
||||
1 dba: V97~V105 · 2 core: VO/Mapper/Enum · 3 api: Service/Controller(공식·집계 내장) · 4 front: 7페이지+commission/settle/org api+App.tsx · 5 infra: build+smoke
|
||||
**PL 검증**: GET뿐 아니라 액션 POST(close/reopen/simulate/recalc balance/등급산정)까지 실호출로 5xx 0 확인 — P7에서 GET-only smoke가 버그를 놓친 교훈 반영.
|
||||
|
||||
---
|
||||
|
||||
## 완료 상태 (2026-05-31)
|
||||
schema **v105** 적용(Flyway V97~V105 success), 전체 `./gradlew build` SUCCESSFUL, 프론트 빌드 OK, ga-api 8082 라이브. 8테이블·7메뉴 등록. GET 9 + 액션 POST 5(simulate/hq-settles/step-closes/close/reserve upsert) **전부 200, 5xx 0건**. 시뮬레이터 공식 실측: 보험료 1,000,000×0.6=600,000 → ×0.75=450,000 → 원천세 14,850 → net 435,150 → 예상환수 225,000 ✓. 테스트데이터 정리 완료. (P8은 P7과 달리 액션까지 PL이 직접 호출 검증 → 숨은 버그 0.)
|
||||
@@ -0,0 +1,168 @@
|
||||
# P9 — 환수채권 이월/미수금 관리 (Clawback Receivable Carry-forward)
|
||||
|
||||
작성: PL, 2026-06-04. 사용자 지시 "수수료 빠진 업무 찾아서 기획·설계 후 적용".
|
||||
근거: incar CMS 메뉴(`환수이월관리`/`환수계약리스트`/`환수리스트`/`위촉중환수`) ↔ GA 부재 + 코드 갭
|
||||
(`AggregateStep.java:197-199` — net<0 시 경고 로그만, 미회수 환수 채권화·차월상계·대손 생애주기 전무).
|
||||
|
||||
범위: 풀스택(DB→Core→API→Batch→Frontend). 패턴 100% 준수(template = 정착지원금 V90 / SettlingSupport*).
|
||||
Flyway: **V110이 최신 → V111부터.** 금액 NUMERIC(15,2), HALF_UP.
|
||||
|
||||
**확정 FK**: agent(agent_id), settle_master(agent_id, settle_month).
|
||||
|
||||
---
|
||||
|
||||
## 1. 업무 정의 (표준 한국 GA 관행 — 데이터로 외부화)
|
||||
|
||||
조기해지 차등환수(chargeback)·공제가 당월 지급수수료(gross)를 초과하면 `net < 0`. 이 미회수분은 **설계사가 GA에 갚아야 할 미수채권(환수채권)** 이다. 표준 처리:
|
||||
|
||||
1. **이월(carryover)**: 당월 net<0 → 음수분(|net|)을 환수채권으로 신규 등록, 당월 실지급(payable)=0.
|
||||
2. **상계 회수(recovery)**: 차월 이후 net>0이면 미결 채권을 **FIFO(오래된 origin월 우선)** 로 상계 회수. payable = net − 회수액.
|
||||
3. **대손/직접상환(write-off / direct settle)**: 장기 미회수 채권은 관리자가 대손(WRITTEN_OFF) 또는 외부 직접상환(CLEARED) 처리.
|
||||
|
||||
표준 가정(회사 정책 다르면 데이터/룰만 수정, 코드 불변):
|
||||
- 상계 우선순위 = origin_settle_month ASC (FIFO).
|
||||
- 회수는 당월 양수 net 한도 내에서만(설계사를 음수로 만들지 않음).
|
||||
- 이자·연체료 없음(필요 시 후속).
|
||||
|
||||
---
|
||||
|
||||
## 2. 스키마 (V111)
|
||||
|
||||
### 2-1. clawback_receivable (환수채권 마스터)
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| receivable_id | BIGSERIAL PK | |
|
||||
| agent_id | BIGINT NN FK agent | |
|
||||
| origin_settle_month | CHAR(6) NN | 미수 발생 정산월 |
|
||||
| original_amount | NUMERIC(15,2) NN | 발생액(양수, =당월 |net|) |
|
||||
| recovered_amount | NUMERIC(15,2) NN DEFAULT 0 | 누적 상계 회수액 |
|
||||
| balance | NUMERIC(15,2) NN | = original_amount − recovered_amount |
|
||||
| status | VARCHAR(20) NN DEFAULT 'OUTSTANDING' | OUTSTANDING/RECOVERING/CLEARED/WRITTEN_OFF |
|
||||
| last_recovery_month | CHAR(6) | 최근 회수 정산월 |
|
||||
| writeoff_reason | VARCHAR(300) | 대손/직접상환 사유 |
|
||||
| written_off_at | TIMESTAMP | |
|
||||
| written_off_by | BIGINT | |
|
||||
| created_at | TIMESTAMP NN DEFAULT now() | |
|
||||
| created_by | BIGINT | |
|
||||
| updated_at | TIMESTAMP | |
|
||||
|
||||
- UNIQUE(agent_id, origin_settle_month) — 설계사·발생월당 1건(배치 upsert 멱등 키)
|
||||
- CHECK status IN('OUTSTANDING','RECOVERING','CLEARED','WRITTEN_OFF')
|
||||
- CHECK original_amount >= 0, balance >= 0
|
||||
- idx: (agent_id, status), (status), (origin_settle_month)
|
||||
|
||||
### 2-2. clawback_recovery (상계 회수 이력)
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| recovery_id | BIGSERIAL PK | |
|
||||
| receivable_id | BIGINT NN FK clawback_receivable | |
|
||||
| settle_month | CHAR(6) NN | 회수(상계) 정산월 |
|
||||
| recovered_amount | NUMERIC(15,2) NN | 이 월 이 채권에서 회수한 액 |
|
||||
| created_at | TIMESTAMP NN DEFAULT now() | |
|
||||
|
||||
- idx: (receivable_id), (settle_month)
|
||||
- **멱등 키**: 배치 재실행 시 동일 settle_month 회수분을 reverse(삭제+balance 복원) 후 재적용.
|
||||
|
||||
### 2-3. settle_master 컬럼 추가 (기존 의미 불변)
|
||||
```sql
|
||||
ALTER TABLE settle_master
|
||||
ADD COLUMN clawback_recovered NUMERIC(15,2) NOT NULL DEFAULT 0, -- 당월 채권 상계 회수액
|
||||
ADD COLUMN clawback_carryover NUMERIC(15,2) NOT NULL DEFAULT 0, -- 당월 발생→이월된 미수액
|
||||
ADD COLUMN payable_amount NUMERIC(15,2) NOT NULL DEFAULT 0; -- 실지급(이체대상) = max(0,net) − recovered
|
||||
```
|
||||
- `net_amount`(=gross−소득세−지방세+부가세−공제) 의미·계산 **불변**(무결성 Σ원장==Σsettle_master 보존).
|
||||
- 실제 이체/출금 가용액은 `payable_amount` 사용(후속: withdraw 한도 검증을 payable_amount 기준으로 전환은 본 P9 범위 외, 문서화만).
|
||||
|
||||
### 2-4. 메뉴 / 권한 / 공통코드
|
||||
- 메뉴 `CLAWBACK_RECEIVABLE` (환수채권관리), 상위 GRP_COMMISSION, path `/commission/clawback-receivable`, component `commission/ClawbackReceivable`.
|
||||
- 권한 READ/UPDATE/EXPORT + 역할부여(SUPER_ADMIN·ADMIN 전체, MANAGER READ) — V54/V95 패턴.
|
||||
- 공통코드 `CLAWBACK_RCV_STATUS`: OUTSTANDING=미회수/RECOVERING=회수중/CLEARED=상환완료/WRITTEN_OFF=대손.
|
||||
|
||||
---
|
||||
|
||||
## 3. 배치 통합 (AggregateStep, Step 8) — **핵심**
|
||||
|
||||
설계사 루프에서 `rawNet = tax.netAmount() − deductionTotal` 계산 직후(현 196행), settle_master.upsert **이전**에 삽입:
|
||||
|
||||
```
|
||||
priorBalance = receivableMapper.outstandingBalance(agentId, settleMonth)
|
||||
// SUM(balance) WHERE agent_id=? AND status IN('OUTSTANDING','RECOVERING')
|
||||
// AND origin_settle_month < settleMonth
|
||||
recovered = 0; carryover = 0; payable = 0
|
||||
if (rawNet.signum() >= 0) {
|
||||
recovered = min(rawNet, priorBalance) // 양수 net에서 기존 채권 FIFO 상계
|
||||
payable = rawNet - recovered
|
||||
} else {
|
||||
payable = 0
|
||||
carryover = rawNet.negate() // 음수분 신규 채권화
|
||||
}
|
||||
vo.setNetAmount(rawNet) // 불변
|
||||
vo.setClawbackRecovered(recovered)
|
||||
vo.setClawbackCarryover(carryover)
|
||||
vo.setPayableAmount(payable)
|
||||
settleMapper.upsert(vo)
|
||||
|
||||
// settle_master upsert 가드(status NOT IN CONFIRMED/PAID)와 동일 행만 채권 부수효과 적용
|
||||
if (settle_master 행이 갱신가능 상태였다면) {
|
||||
clawbackService.applyCarryoverAndRecovery(agentId, settleMonth, carryover, recovered)
|
||||
}
|
||||
```
|
||||
|
||||
**applyCarryoverAndRecovery (배치 멱등 보장)** — ga-batch 내 헬퍼 또는 Mapper 직접:
|
||||
1. **reverse**: 이 settleMonth의 기존 clawback_recovery 행 조회 → 각 receivable.balance += recovered_amount, recovered_amount -= , status 재계산(CLEARED→RECOVERING/OUTSTANDING) → recovery 행 삭제. (재실행 안전)
|
||||
2. **carryover**: carryover>0 → upsert clawback_receivable (agent_id, origin=settleMonth) {original_amount=carryover, balance=carryover, recovered_amount=0, status=OUTSTANDING}. (ON CONFLICT 갱신)
|
||||
3. **recovery**: recovered>0 → outstanding 채권 origin_month ASC FIFO 순회:
|
||||
- take = min(recovered_remaining, rcv.balance); rcv.recovered_amount += take; rcv.balance -= take;
|
||||
status = balance==0 ? CLEARED : RECOVERING; last_recovery_month=settleMonth;
|
||||
INSERT clawback_recovery(receivable_id, settleMonth, take); recovered_remaining -= take;
|
||||
- recovered_remaining==0 종료.
|
||||
|
||||
> reverse를 먼저 하므로 동일 월 재정산 시 회수 중복 없음. carryover는 UNIQUE upsert로 멱등.
|
||||
> **주의**: priorBalance 조회는 `origin_settle_month < settleMonth` 로 당월 발생분 자기상계 방지.
|
||||
|
||||
배치 Step 순서: AggregateStep(8) 내부에서 처리(별도 Step 불필요). 회계분개(8b)·세금은 net_amount 기준 불변.
|
||||
|
||||
---
|
||||
|
||||
## 4. API (ga-api)
|
||||
|
||||
`ClawbackReceivableService` + `ClawbackReceivableController` (`/api/clawback-receivables`):
|
||||
- `GET /api/clawback-receivables` — 목록(agentId/status/originMonth 필터, PageHelper, @RequirePermission READ)
|
||||
- `GET /api/clawback-receivables/{id}` — 상세 + 회수이력(clawback_recovery 리스트)
|
||||
- `GET /api/clawback-receivables/summary` — 설계사별 미결 잔액 집계(status OUTSTANDING/RECOVERING)
|
||||
- `PUT /api/clawback-receivables/{id}/write-off` — 대손/직접상환 처리(@RequirePermission UPDATE, @DataChangeLog).
|
||||
body {action: WRITTEN_OFF|CLEARED, reason}. balance→0, status 전이, writeoff_* 기록. **OUTSTANDING/RECOVERING만 허용**(CLEARED/WRITTEN_OFF 재처리 E411).
|
||||
- `GET /api/clawback-receivables/export` — 엑셀(ExcelService, @Transactional).
|
||||
- 표준 ApiResponse/PageResponse, MapStruct, BaseMapper 상속.
|
||||
|
||||
회수(recovery)는 **배치만** 생성(수동 회수 엔드포인트 없음 — 정산 상계로만 발생).
|
||||
|
||||
---
|
||||
|
||||
## 5. Frontend (ga-frontend)
|
||||
|
||||
- `src/api/clawbackReceivable.ts` — list/detail/summary/writeOff/export.
|
||||
- `src/pages/commission/ClawbackReceivable.tsx` — AG Grid(목록 100건+ / status 컬러칩 / 합계행: original/recovered/balance) + 행 클릭 상세 Drawer(회수이력 타임라인) + 대손/상환 처리 버튼(PermissionButton UPDATE) + 엑셀 export.
|
||||
- `App.tsx` 라우트 `commission/clawback-receivable` (lazy).
|
||||
|
||||
---
|
||||
|
||||
## 6. 검증 (PL 직접)
|
||||
1. `./gradlew build`(test 포함) SUCCESSFUL, ga-frontend `npm run build` PASS.
|
||||
2. Flyway V111 success, schema v111. 테이블 2개·컬럼 3개·메뉴 1·권한·공통코드 확인.
|
||||
3. GET 3종 + write-off PUT 실호출 5xx 0.
|
||||
4. **배치 e2e (핵심)**: 단일 트랜잭션 롤백 시나리오로
|
||||
- (a) 특정 설계사에 chargeback>gross 인 net<0 상황 구성 → 202611 정산 → clawback_carryover 발생 + clawback_receivable OUTSTANDING 1건 + payable=0 확인.
|
||||
- (b) 익월 202612 정산(양수 net) → clawback_recovered 상계 + receivable balance 감소/CLEARED + clawback_recovery 행 + payable=net−recovered 확인.
|
||||
- (c) 동일월 재실행 → 회수 중복 없음(reverse 멱등) 확인.
|
||||
- 부산물 전부 롤백, pre-e2e 복원.
|
||||
|
||||
---
|
||||
|
||||
## 7. 후속 갭 (본 P9 범위 외, 기록)
|
||||
- 시상/인정실적 환수 ledger (incar `인정실적수수료환수`/`신인도입유치자시상환수`).
|
||||
- 지급대상건 구성·수정(수동 포함/제외 워크플로우, incar `지급대상건구성/수정/조회`).
|
||||
- 환수 보험사자료 업로드(incar `환수보험사자료 업로드` — 보험사 통보 기반 환수 인입).
|
||||
- withdraw 가용한도 검증을 net_amount→payable_amount 로 전환.
|
||||
</content>
|
||||
</invoke>
|
||||
@@ -0,0 +1,182 @@
|
||||
# 수수료 계산 공식 전체 레퍼런스 (코드 기준)
|
||||
|
||||
> 모든 수수료 종류의 **실제 구현 계산식**을 정리한 문서입니다.
|
||||
> 출처는 각 `*Service` / `*Calculator` / Step 코드이며, 2026-06 수수료 감사 수정분이 반영돼 있습니다.
|
||||
> 공통 규칙: 모든 금액 `BigDecimal`, 반올림 `HALF_UP`. 금액 `NUMERIC(15,2)`(소수 2자리),
|
||||
> 율 `NUMERIC(7,4)`. 원천세·환수 등 일부 집계는 원단위(scale 0).
|
||||
|
||||
---
|
||||
|
||||
## 0. 가장 기본 — 모집/유지 "세 번 곱하기"
|
||||
|
||||
`CommissionCalculator` (모집·유지 공통, 회차만 다름)
|
||||
|
||||
```
|
||||
① 보험사지급액 = 보험료(premium) × 보험사요율(company_rate %) / 100
|
||||
② 설계사수수료 = 보험사지급액 × 지급률(payout_rate %) / 100 ← gross(agent_amount)
|
||||
③ 원장 원천세 = 설계사수수료 × 3.3% ← tax_amount
|
||||
```
|
||||
|
||||
- `company_rate`: 상품 × 회차(commission_year) × 기준일로 조회
|
||||
- `payout_rate` : 설계사등급 × 보험종류 × 회차로 조회 (등급 높을수록 ↑)
|
||||
- 율이 NULL이면 0 폴백 + **경보 로그**(과소지급 추적). 금액 pct는 scale 2.
|
||||
- **숫자 예시**: 보험료 1,000,000 × 60% = 600,000 → × 75% = 450,000 → 세금 14,850
|
||||
|
||||
| 종류 | 코드 | 회차(commission_year) | 귀속 |
|
||||
|---|---|---|---|
|
||||
| **모집수수료** | recruit | 1 (초년도) | 원 모집설계사 (승계돼도 잔류) |
|
||||
| **유지수수료** | maintain | 2~ (`monthsElapsed/12 + 1`) | 현재 관리설계사 (승계 시 후임) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 설계사 지급성 수수료 (settle_master gross 에 합산되는 9종 + 본 2종)
|
||||
|
||||
| # | 종류 | 코드 | 계산 공식 | 비고 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 모집수수료 | recruit | `premium × company_rate × payout_rate` | §0 |
|
||||
| 2 | 유지수수료 | maintain | 〃 (회차만 다름) | §0 |
|
||||
| 3 | **수금수수료** | collection | `commission = 보험료(premium_amount) × 수수료율(commission_rate)` | `CollectionCommissionService:89` |
|
||||
| 4 | **갱신수수료** | renewal | `commission = req.commissionAmount` (룰의 rateValue/fixedAmount 참조, 원장은 입력액) | `RenewalCommissionService:91` |
|
||||
| 5 | **부활수수료** | reinstatement | `net = commissionAmount + chargebackReversal` (부활 시 기환수액 일부 복원) | `ReinstatementCommissionService:88,100` |
|
||||
| 6 | **지속수당(유지보너스)** | persistency_bonus | 유지율 ≥ `retention_rate_threshold` 일 때 `bonus_value` (RATE형 base×율 / FIXED형 정액) | 룰: 등급별 임계유지율 |
|
||||
| 7 | **소개·이관수수료** | referral | RATE형 `base × rate` / FIXED형 `fixed_amount`. (RATE는 룰 rate 우선, 없으면 req) | `ReferralCommissionService:103,110,114` |
|
||||
| 8 | **정착지원금** | settling_support | `net = support_amount − clawback`. **환수액 ≤ 지원금 검증**(net 음수 차단) | `SettlingSupportService` |
|
||||
| 9 | **시상** | award | `threshold_amount 이상`일 때만: RATE `base × award_rate` / FIXED `award_fixed` | `AwardService` (threshold 게이트) |
|
||||
| 10 | **생보운영지원수수료** | life_support | `support = base × support_rate`, `net = support − clawback`. **환수 ≤ support 검증** | `LifeSupportService` |
|
||||
| 11 | **지점장수당** | branch_allowance | `allowance = min(base × allowance_rate, cap)`. cap=NULL이면 무제한. `net = allowance − clawback` | `BranchAllowanceService:85-93` |
|
||||
|
||||
> settle_master 집계 시 9종(3~11, 모집·유지 제외)은 `other_commission_total` 로 합산돼 gross 에 더해진다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 조직·실적 기반
|
||||
|
||||
| 종류 | 코드 | 계산 공식 | 비고 |
|
||||
|---|---|---|---|
|
||||
| **오버라이드** | override | `amount = min(base × override_pct, cap)`. **base = 하위설계사 모집+유지 합** (룰에 `insurance_type_cond` 있으면 해당 보험종류만 집계) | `CalcOverrideStep`. cap NULL=무제한. 조직트리 상향 탐색해 가장 가까운 `to_grade` 1명에 귀속 |
|
||||
| **인정실적(환산보험료)** | recognized | `converted = premium × conversion_rate`, `recognized = converted × recognition_rate` | `RecognizedPerformanceService:85,89`. 시상·등급 산정 기초 |
|
||||
| **수입수수료** | income | `income = premium × company_rate`. 수지차 `margin = Σincome − Σpayout`, `marginRate = margin / Σincome` | `IncomeCommissionService:47,83,86`. 보험사→GA 수입 원장 |
|
||||
| **등급평가** | grade_eval | `performance_score`로 `selectActiveRuleByScore` → 매칭 룰의 `gradeId`로 등급 결정 (없으면 기존등급 유지) | `GradeEvaluationService:95-97` |
|
||||
| **공동계약 지분** | joint_contract | 계약별 `Σ share_rate ≤ 1.0` 검증 (초과 시 E411). 각 설계사 몫 = 수수료 × 본인 share_rate | `JointContractService` |
|
||||
| **준비금** | reserve_fund | `balance = (직전월 balance or 0) + reserve_in − reserve_out` | `ReserveFundService:50` |
|
||||
| **수수료 시뮬레이션** | simulation | `사수 = premium×company_rate×payout_rate`, `원천세 = 사수×3.3%`, `net = 사수−원천세`, `예상환수 = 사수×기본환수율(or 입력)` | `CommissionSimulationService` (조회 전용 미리보기) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 횡단(cross-cutting) 규제·정산 계산
|
||||
|
||||
### 3-1. 1200% 룰 (RegulatoryLimitChecker) — **단위 환산 + clip+이연**
|
||||
```
|
||||
한도(KRW) = 연환산보험료 × 한도율(%) / 100 ← unit=PERCENT 일 때 (1200%, 1차년 35%)
|
||||
위반조건 = (기지급 누적 + 추가분) > 한도(KRW)
|
||||
```
|
||||
- 1차년 한도 초과 시: **한도잔여까지만 당월 지급(clip)**, 초과분만 분급(7년)으로 **이연**, 세액 재계산.
|
||||
전액 이연되면 당월 원장 미생성. (`CalcRecruitStep`)
|
||||
|
||||
### 3-2. 분급 (InstallmentPlanGenerator) — 7년 분할
|
||||
```
|
||||
회차별 지급 = 총수수료 × 회차비율(%) / 100 (표준 35/25/15/10/7/5/3)
|
||||
```
|
||||
- 비율 합이 정확히 100%이면 **마지막 회차가 반올림 잔차를 흡수**(분급 합 = 원금 보장).
|
||||
|
||||
### 3-3. 차등환수 (ChargebackCalculator) — 조기해지
|
||||
```
|
||||
경과월 = MONTHS_BETWEEN(계약일, 해지일)
|
||||
환수액 = 기지급 모집수수료 × 환수율(경과월 구간)
|
||||
```
|
||||
| 경과월 | 환수율 |
|
||||
|---|---|
|
||||
| 0 ~ 12 | 100% |
|
||||
| 13 ~ 24 | 50% |
|
||||
| 25 ~ 36 | 25% |
|
||||
| 37 ~ 84 | 10% |
|
||||
| 85 ~ | 0% |
|
||||
- (0개월 즉시해지도 100% 구간에 포함 — V109 보정)
|
||||
|
||||
### 3-4. 세금 (TaxCalculator / BatchTaxCalculator)
|
||||
```
|
||||
소득세 = gross × 소득세율(사업 3.3% / 기타 8.8%) (scale 0, 원단위)
|
||||
지방세 = 소득세 × 10% (scale 0)
|
||||
부가세 = gross × 10% (과세사업자 TAXABLE 만, 그 외 0)
|
||||
net = gross − 소득세 − 지방세 + 부가세
|
||||
```
|
||||
- **gross ≤ 0(환수>지급)이면 세금 0, net = gross** (음수 원천세가 신고에 전파되는 것 차단)
|
||||
|
||||
### 3-5. 집계 → 실지급 (AggregateStep, settle_master)
|
||||
```
|
||||
gross = 모집 + 유지 + 오버라이드 + 예외(+) − 환수 − 예외(−) + 기타9종(other_commission_total)
|
||||
tax = 소득세 + 지방세
|
||||
net = gross − 소득세 − 지방세 + 부가세 − 공제(deduction)
|
||||
```
|
||||
- net < 0(공제>지급)이면 경보 로그(차월 이월 검토 대상).
|
||||
- 무결성: 8단계 후 `Σ원장 == Σsettle_master`.
|
||||
|
||||
### 3-6. 회계 분개 (AccountingEntryGenerateStep) — **원천세 예수금 분리**
|
||||
```
|
||||
(차) 8350 지급수수료 gross
|
||||
(대) 2530 미지급금 net (= gross − 원천세)
|
||||
(대) 2540 예수금 원천세(tax_amount) ← 원천세>0 일 때 leg 분리
|
||||
```
|
||||
- 차변합(gross) = 대변합(net + 원천세) 균형.
|
||||
|
||||
---
|
||||
|
||||
## 3-7. 2026-06 감사 보완 4종 (V124~V126)
|
||||
|
||||
설계사 지급성 수수료 종류는 사실상 완비된 상태에서, 감사로 발견한 **실제 결함·규제 갭**만 보완.
|
||||
|
||||
### 3-7-A. 지급보류(HOLD) 지급배치 제외 — **버그 수정**
|
||||
```
|
||||
지급배치 대상 = settle_master WHERE payable_amount>0 AND status NOT IN ('PAID','HOLD')
|
||||
```
|
||||
- 종전엔 `status != 'PAID'` 만 검사 → 운영자가 지급보류(HOLD)한 설계사도 펌뱅킹 이체에 포함되던 결함.
|
||||
(`PaymentBatchItemMapper.insertFromSettleMaster`)
|
||||
|
||||
### 3-7-B. 환수 원천세 역분개 — 교차월 환수 시 기납부 원천세 보정
|
||||
```
|
||||
gross<0(상계되지 않은 환수분)인 월:
|
||||
역분개 기준액 = |gross|
|
||||
소득세 역분개 = −(|gross| × 소득세율) ← withholding_tax_adjustment (음수)
|
||||
지방세 역분개 = −(소득세 역분개액 × 10%)
|
||||
분기 원천세신고 = Σpayment(원천세) + Σadjustment(역분개, 음수) ← 과대신고 해소
|
||||
```
|
||||
- gross≥0(당월 환수가 양수분으로 상계)인 달은 당월 과세에 이미 반영 → **역분개 미발생**(이중계상 없음).
|
||||
- settle_master gross/net/carryover/payable 흐름은 **불변**(역분개는 신고보정용 별도 레코드).
|
||||
(`AggregateStep` gross<0 분기, `WithholdingTaxReportMapper.upsertQuarterlyAggregate`)
|
||||
|
||||
### 3-7-C. 환수채권 연체이자 — 미수채권 이월 시 월 단리 적립
|
||||
```
|
||||
당월 적립이자 = 채권잔액(balance) × 연체이자율(%) / 1200 (월 단리, scale 0)
|
||||
적립조건 = origin_settle_month ≤ 당월 − (거치기간+1)
|
||||
balance += 적립이자 (→ 기존 FIFO 상계가 이자까지 회수)
|
||||
```
|
||||
- 이자율 `CLAWBACK_OVERDUE_INTEREST_RATE`(기본 0=미적립 안전폴백), 거치기간 `CLAWBACK_INTEREST_GRACE_MONTHS`(기본 0).
|
||||
- 멱등: `clawback_interest` 이력으로 당월 적립분 reverse 후 재적립. (`AggregateStep.reverseAndAccrueClawbackInterest`)
|
||||
|
||||
### 3-7-D. 사업소득 간이지급명세서(월별) — 국세청 제출 대응
|
||||
```
|
||||
간이지급명세서 = commission_statement(MONTHLY) ⋈ agent
|
||||
(소득자·주민/사업자번호·지급액·소득세·지방소득세·차인지급액)
|
||||
```
|
||||
- 신규 데이터테이블 없음(읽기전용 리포트). 조회 + 엑셀 export. (`ReportMapper.selectSimplifiedPayment`)
|
||||
|
||||
---
|
||||
|
||||
## 4. 실지급 흐름 한 장 요약
|
||||
|
||||
```
|
||||
보험료 ─×보험사요율─▶ 보험사지급액 ─×지급률─▶ 설계사수수료(gross)
|
||||
│
|
||||
┌─ 1200%룰: 1차년 한도 clip + 초과분 7년 분급 이연 ─┐
|
||||
└─ 조기해지: 차등환수(−) / 부활: 환수복원(+) ┘
|
||||
▼
|
||||
gross − 소득세 − 지방세 (+부가세) − 공제 = net(실지급) ──▶ 펌뱅킹 이체
|
||||
│
|
||||
회계분개: (차)지급수수료 / (대)미지급금 net + 예수금 원천세
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*기준 코드: CommissionCalculator, TaxCalculator, ChargebackCalculator, InstallmentPlanGenerator,
|
||||
RegulatoryLimitChecker, CalcRecruit/Maintain/OverrideStep, AggregateStep, AccountingEntryGenerateStep,
|
||||
P7/P8 12개 commission Service. schema v126. 2026-06 수수료 감사 수정 + 갭보완 4종(§3-7) 반영.*
|
||||
@@ -0,0 +1,168 @@
|
||||
# E2E 시나리오 — 보험이 어떻게 들어와서 수수료가 어떻게 나가는가
|
||||
|
||||
> 이 문서는 **계약 1건이 인입되어 설계사 통장에 돈이 꽂힐 때까지**의 전체 순서를
|
||||
> "등장인물 + 숫자"로 따라가는 시나리오입니다.
|
||||
> 각 단계는 실제 계산 코드(`CommissionCalculator`, `RegulatoryLimitChecker`,
|
||||
> `InstallmentPlanGenerator`, `ChargebackCalculator`, `TaxCalculator`)와 1:1로 대응하며,
|
||||
> 이 시나리오를 그대로 검증하는 단위테스트가 `SettlementScenarioTest` 입니다.
|
||||
|
||||
---
|
||||
|
||||
## 0. 등장인물 (이 시나리오의 테스트 데이터)
|
||||
|
||||
| 역할 | 식별자 | 속성 |
|
||||
|---|---|---|
|
||||
| 원 모집설계사 **김설계** | `agentId=10`, `gradeId=5` | 사업소득(BUSINESS_INCOME) · 과세사업자(TAXABLE) |
|
||||
| 후임 승계설계사 **박승계** | `agentId=20`, `gradeId=3` | (계약 승계 단계에서 등장) |
|
||||
| 상위 직책자 **이지점** | `agentId=30` | 오버라이드 수령 |
|
||||
| 상품 **무배당 종신보험** | `productId=1`, `WHOLE_LIFE`, 카테고리 `LIFE` | |
|
||||
| 계약 | `contractId=100`, `policyNo=POL-0001` | 연납보험료 **1,000,000원**, 계약일 **2024-01-15** |
|
||||
|
||||
기준 룰(테스트에서 Mock으로 주입하는 데이터):
|
||||
|
||||
| 룰 | 값 |
|
||||
|---|---|
|
||||
| 보험사요율(company_rate) 1차년 | 60% |
|
||||
| 보험사요율 2차년 | 10% |
|
||||
| 지급률(payout_rate) 등급5·WHOLE_LIFE·1차년 | 75% |
|
||||
| 지급률 등급5·2차년 | 50% |
|
||||
| 지급률 등급3(후임)·2차년 | 40% |
|
||||
| 분급 비율(7년) | 35 / 25 / 15 / 10 / 7 / 5 / 3 (%) |
|
||||
| 차등환수 13~24개월 구간 | 50% |
|
||||
| 1200% 한도(TOTAL_RATIO) | 12,000,000원 (연납 1,000,000 × 1200%) |
|
||||
| 원천세 | 사업소득 3.3% · 지방세 = 소득세 10% · 부가세 10%(과세사업자) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 한눈에 보는 순서
|
||||
|
||||
```
|
||||
[보험사] 실적데이터 인입
|
||||
│ (Step1 receive → Step2 transform → Step3 reconcile)
|
||||
▼
|
||||
① 모집수수료 원장 계산 (Step4 calcRecruit) ← premium × company_rate × payout_rate
|
||||
│
|
||||
② 1200% 룰 한도 체크 (RegulatoryLimitChecker) ← 누적이 한도를 넘으면 잘라냄
|
||||
│
|
||||
③ 분급(7년) 계획 생성 (InstallmentPlanGenerator) ← 큰 모집수수료를 7년에 나눠 지급
|
||||
│
|
||||
④ 유지수수료 원장 계산 (Step5 calcMaintain) ← 2차년 이후 매년
|
||||
│ └ ⑤ 계약 승계 시 후임에게 귀속 전환 (모집은 원설계사 유지)
|
||||
│
|
||||
⑥ 오버라이드 계산 (Step7 calcOverride) ← 하위실적 × 율 (cap 제한)
|
||||
│
|
||||
⑦ 조기 해지 환수 (Step6 chargeback) ← 유지개월 짧을수록 많이 환수
|
||||
│
|
||||
⑧ 설계사×월 집계 (Step8 aggregate) ← gross 합산
|
||||
│ └ 세금 원천징수 + 공제 차감 → net(실지급액)
|
||||
▼
|
||||
[설계사 통장] ← 펌뱅킹 이체(KFTC)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 단계별 숫자 (시나리오 본문)
|
||||
|
||||
### ① 모집수수료 원장 계산 — 보험이 "들어오는" 첫 돈
|
||||
계약이 인입되면 설계사 김설계(등급5)의 모집수수료 원장 1줄이 생긴다.
|
||||
|
||||
```
|
||||
① 보험사지급액 = 보험료 1,000,000 × 보험사요율 60% = 600,000
|
||||
② 설계사수수료 = 600,000 × 지급률 75% = 450,000 ← gross
|
||||
③ 원장 원천세 = 450,000 × 3.3% = 14,850
|
||||
```
|
||||
→ 원장: `agent_id=10`, `commission_year=1`, `agent_amount=450,000`, `tax_amount=14,850`
|
||||
|
||||
### ② 1200% 룰 한도 체크
|
||||
초년도 모집수수료 누적이 연납보험료의 1200%(=12,000,000)를 넘으면 안 된다.
|
||||
- 현재 누적 0 + 450,000 = 450,000 ≤ 12,000,000 → **통과**
|
||||
- 만약 추가분이 한도를 넘기면 `ViolationResult.violated` 로 막고 초과분을 잘라낸다.
|
||||
|
||||
### ③ 분급(7년 분할) 계획 생성
|
||||
모집수수료 450,000을 한 번에 안 주고 7년에 나눈다.
|
||||
|
||||
| 차년 | 비율 | 지급액 |
|
||||
|---|---|---|
|
||||
| 1 | 35% | 157,500 |
|
||||
| 2 | 25% | 112,500 |
|
||||
| 3 | 15% | 67,500 |
|
||||
| 4 | 10% | 45,000 |
|
||||
| 5 | 7% | 31,500 |
|
||||
| 6 | 5% | 22,500 |
|
||||
| 7 | 3% | 13,500 |
|
||||
| **합** | **100%** | **450,000** |
|
||||
|
||||
> **무결성 규칙:** 분급액의 **합은 원수수료와 정확히 같아야 한다.**
|
||||
> 금액이 딱 나눠떨어지지 않을 때(예: 435,150원) 회차별 반올림 잔차가 생기는데,
|
||||
> 이때는 **마지막 회차가 잔차를 흡수**해서 합계를 원금에 맞춘다. (→ §4 보완)
|
||||
|
||||
### ④ 유지수수료 (2차년)
|
||||
계약이 유지되면 2차년부터 유지수수료가 매년 발생한다.
|
||||
```
|
||||
보험사지급액 = 1,000,000 × 10%(2차년 요율) = 100,000
|
||||
설계사수수료 = 100,000 × 50%(등급5 2차년) = 50,000
|
||||
```
|
||||
|
||||
### ⑤ 계약 승계 (김설계 해촉 → 박승계 후임)
|
||||
김설계가 해촉되면 그 계약의 **유지수수료만** 후임 박승계(등급3)에게 넘어간다.
|
||||
```
|
||||
유지수수료(후임) = 100,000 × 40%(등급3 2차년) = 40,000 → 원장 agent_id = 20(박승계)
|
||||
```
|
||||
- **모집수수료는 원 모집설계사 김설계(10)에게 그대로 남는다.** (고아계약 승계 표준)
|
||||
|
||||
### ⑥ 오버라이드 (상위 직책자 이지점)
|
||||
하위 김설계의 실적에 대해 상위 이지점이 받는 수당.
|
||||
```
|
||||
오버라이드 = 하위실적 2,000,000 × 4% = 80,000 (cap 100,000 미만이므로 그대로)
|
||||
```
|
||||
|
||||
### ⑦ 조기 해지 환수 — 13개월차 해지
|
||||
계약이 13개월차(2024-01-15 → 2025-02-15)에 깨지면 13~24개월 구간 환수율 50%.
|
||||
```
|
||||
환수액 = 기지급 모집수수료 450,000 × 50% = 225,000 ← 다음 정산에서 (−) 차감
|
||||
```
|
||||
|
||||
### ⑧ 집계 → 세금 → 실지급액(net)
|
||||
김설계 한 달치를 settle_master로 집계하고 세금·공제를 반영한다.
|
||||
과세사업자(TAXABLE) gross 450,000 기준:
|
||||
```
|
||||
소득세 = 450,000 × 3.3% = 14,850
|
||||
지방세 = 14,850 × 10% = 1,485
|
||||
부가세 = 450,000 × 10% = 45,000 (과세사업자만 +)
|
||||
실지급 = 450,000 − 14,850 − 1,485 + 45,000 = 478,665
|
||||
```
|
||||
- 여기에 가지급금 공제 100,000이 있으면 net = 478,665 − 100,000 = **378,665**
|
||||
- 비과세(NONE) 설계사는 부가세가 없어 net = 450,000 − 14,850 − 1,485 = **433,665**
|
||||
|
||||
---
|
||||
|
||||
## 3. 시나리오 ↔ 코드 ↔ 테스트 대응표
|
||||
|
||||
| 단계 | 코드 | 테스트 메서드 |
|
||||
|---|---|---|
|
||||
| ① 모집수수료 | `CommissionCalculator.calcRecruitLedger` | `s1_보험인입_모집수수료_원장계산` |
|
||||
| ② 1200% 룰 | `BatchRegulatoryLimitChecker.checkContractTotal` | `s2_규제_1200퍼센트_한도_통과및위반` |
|
||||
| ③ 분급 7년 | `BatchInstallmentPlanGenerator.generate` | `s3_분급_7년_분할_합계100퍼센트` |
|
||||
| ③ 분급 잔차 | 〃 | `s3b_분급_라운딩_잔차_마지막회차_흡수` |
|
||||
| ④ 유지수수료 | `CommissionCalculator.calcMaintainLedger` | `s4_유지수수료_2차년` |
|
||||
| ⑤ 계약 승계 | `CommissionCalculator.calcMaintainLedger(effectiveAgentId)` | `s5_계약승계_유지수수료_후임귀속` |
|
||||
| ⑦ 차등 환수 | `BatchChargebackCalculator.calculate` | `s6_조기해지_차등환수_13개월_50퍼센트` |
|
||||
| ⑧ 세금/실지급 | `BatchTaxCalculator.calculate` | `s7_세금_실지급액_과세사업자` / `s8_세금_비과세` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 이 시나리오 작성 중 발견·보완한 사항
|
||||
|
||||
**분급 계획 합계 불일치(반올림 잔차) — 수정함.**
|
||||
- 현상: 분급액을 회차별로 `총액 × 비율 / 100` 으로 각각 반올림하면,
|
||||
원금이 7로 나눠떨어지지 않을 때 분급 합계가 원금과 1~수원 어긋났다.
|
||||
(예: 435,150원 → 회차 합 435,153원, +3원 누수)
|
||||
- 영향: 분급 총합 ≠ 원 모집수수료 → 정산 무결성(합계 일치) 위반, 과/미지급.
|
||||
- 수정: **비율 합이 정확히 100%인 정규 스케줄**일 때 마지막 회차가
|
||||
`원금 − 직전 회차 합` 으로 잔차를 흡수하도록 보정.
|
||||
(비율 합이 100% 미만인 부분지급 스케줄은 기존 동작 유지 → 설계 의도 보존)
|
||||
- 대상: `InstallmentPlanGenerator`(ga-api), `BatchInstallmentPlanGenerator`(ga-batch) 양쪽.
|
||||
|
||||
---
|
||||
|
||||
*기준: schema v108, DOMAIN_KNOWLEDGE.md 7장 공식. 모든 금액 BigDecimal HALF_UP.*
|
||||
@@ -0,0 +1,470 @@
|
||||
# 신입사원 완전 기초 교육 — 보험과 수수료, 0부터 시작하기
|
||||
|
||||
> 대상: **보험을 한 번도 다뤄본 적 없는 신입사원/개발자.**
|
||||
> 목표: 이 문서 하나만 끝까지 읽으면 "우리 회사가 무슨 일을 하고, 수수료가 어떻게 계산돼서 설계사 통장에 꽂히는지" 머릿속에 그림이 그려지게 하는 것.
|
||||
> 읽는 법: 위에서 아래로 순서대로. 어려운 용어는 나올 때마다 바로 쉬운 말로 풀어 씁니다. 숫자 예시는 직접 손으로 따라 계산해 보세요.
|
||||
> 더 깊은 코드/규정 레벨은 [`DOMAIN_KNOWLEDGE.md`](DOMAIN_KNOWLEDGE.md) 참고.
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
1. [보험이 뭔가요? (진짜 0단계)](#1-보험이-뭔가요-진짜-0단계)
|
||||
2. [등장인물 4명 — 누가 누구에게 돈을 주나](#2-등장인물-4명--누가-누구에게-돈을-주나)
|
||||
3. [보험 기초 용어 25개 — 이것만 알면 대화가 된다](#3-보험-기초-용어-25개--이것만-알면-대화가-된다)
|
||||
4. [수수료가 뭐고 왜 종류가 많은가](#4-수수료가-뭐고-왜-종류가-많은가)
|
||||
5. [수수료 종류 한눈에 (우리 시스템이 다루는 전부)](#5-수수료-종류-한눈에-우리-시스템이-다루는-전부)
|
||||
6. [수수료 계산의 핵심 공식 — "세 번 곱하기"](#6-수수료-계산의-핵심-공식--세-번-곱하기)
|
||||
- 6-A. [수수료 종류별 계산식 — 하나씩 풀어보기](#6-a-수수료-종류별-계산식--하나씩-풀어보기--전부)
|
||||
- 6-B. [기타수수료 — 예외 가감과 그 밖의 정산](#6-b-기타수수료--예외-가감과-그-밖의-정산--전부)
|
||||
7. [숫자로 끝까지 따라가기 (한 계약의 일생)](#7-숫자로-끝까지-따라가기-한-계약의-일생)
|
||||
8. [그냥 곱하면 안 되는 이유 — 규제 3종](#8-그냥-곱하면-안-되는-이유--규제-3종)
|
||||
9. [세금과 공제 — 통장에 적게 들어오는 이유](#9-세금과-공제--통장에-적게-들어오는-이유)
|
||||
10. [한 달이 흘러가는 과정 — 월 정산 파이프라인](#10-한-달이-흘러가는-과정--월-정산-파이프라인)
|
||||
11. [수입 vs 지급, 그리고 수지차 — 회사는 어디서 남기나](#11-수입-vs-지급-그리고-수지차--회사는-어디서-남기나)
|
||||
12. [자주 나오는 질문 (FAQ)](#12-자주-나오는-질문-faq)
|
||||
13. [용어 빠른 사전](#13-용어-빠른-사전)
|
||||
|
||||
---
|
||||
|
||||
## 1. 보험이 뭔가요? (진짜 0단계)
|
||||
|
||||
**보험 = 여럿이 조금씩 돈을 모아뒀다가, 사고를 당한 사람에게 몰아주는 약속.**
|
||||
|
||||
예를 들어 100명이 매달 1만 원씩 모읍니다(월 100만 원). 그 중 한 명이 큰 병에 걸리면, 모인 돈에서 큰 금액을 그 사람에게 줍니다. 나머지 99명은 "내가 안 걸려서 다행이다" 하고 넘어갑니다. 이 "돈 모으고 → 사고나면 주기"를 전문으로 운영하는 회사가 **보험사**입니다.
|
||||
|
||||
- 고객이 매달 내는 돈 = **보험료(premium)**
|
||||
- 사고/사망/만기 때 받는 돈 = **보험금**
|
||||
- 이 약속을 적은 계약서 한 장 = **보험계약** (증권 1장 = 계약 1건)
|
||||
|
||||
> 우리 회사는 보험사가 아닙니다. 우리는 **보험을 파는 일(영업)을 대행**하고, 그 대가로 받은 돈을 **영업한 사람들에게 정확히 나눠주는 회사**입니다. 그게 다음 장에서 나오는 GA입니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 등장인물 4명 — 누가 누구에게 돈을 주나
|
||||
|
||||
| 누구 | 한 줄 설명 | 비유 |
|
||||
|---|---|---|
|
||||
| **고객** | 보험에 가입하고 보험료를 내는 사람 | 물건을 사는 손님 |
|
||||
| **보험사 (Carrier)** | 보험 상품을 만들고, 팔아준 대가로 수수료를 주는 회사 (삼성생명, 메리츠 등) | 물건을 만든 제조사 |
|
||||
| **GA (우리 회사)** | 여러 보험사 상품을 한데 모아 파는 대형 보험대리점 | 여러 브랜드를 파는 백화점 |
|
||||
| **설계사 (Agent)** | 실제로 고객을 만나 계약을 따오는 사람 | 백화점 매장 직원 |
|
||||
|
||||
**돈의 흐름 (제일 중요한 그림):**
|
||||
```
|
||||
[고객] ──보험료──▶ [보험사] ──수수료──▶ [GA(우리)] ──정산──▶ [설계사]
|
||||
└ 세금 떼고, 공제 빼고 통장으로
|
||||
```
|
||||
|
||||
- 보험사는 직접 영업하는 대신, GA·설계사가 고객을 데려와 주면 그 대가로 **수수료**를 줍니다.
|
||||
- GA는 보험사한테 받은 수수료(=**수입수수료**)에서, 영업한 설계사 몫(=**지급수수료**)을 떼어 줍니다.
|
||||
- **우리 시스템이 하는 일 = 이 "설계사 몫을 정확히 계산해서 나눠주는 것(정산)".**
|
||||
|
||||
---
|
||||
|
||||
## 3. 보험 기초 용어 25개 — 이것만 알면 대화가 된다
|
||||
|
||||
| 용어 | 쉬운 설명 |
|
||||
|---|---|
|
||||
| 보험계약 | 고객이 "매달 낼 테니 사고 때 보장해줘"라고 맺은 약속. 증권 1장 = 1건. |
|
||||
| 보험료(premium) | 고객이 내는 돈. **모든 수수료 계산의 출발점.** 월납/연납/일시납. |
|
||||
| 보험금 | 사고·만기 때 고객이 받는 돈. (수수료와는 다른 개념!) |
|
||||
| 계약자 / 피보험자 / 수익자 | 돈 내는 사람 / 보장 대상 / 보험금 받는 사람. (셋이 같을 수도 다를 수도) |
|
||||
| 생명보험 | 사람의 사망·생존·연금 관련 보험. (종신, 연금보험 등) |
|
||||
| 손해보험 | 자동차·화재·실손 등 재산·사고 관련 보험. |
|
||||
| 회차 / 차년(commission year) | 계약이 몇 년째인지. **1차년(초년도)** 수수료가 가장 큼. |
|
||||
| 납입 / 미납 / 실효 | 보험료를 냈다 / 안 냈다 / 안 내서 계약이 끊겼다. |
|
||||
| 유지 / 해지 / 부활 | 계약 살아있음 / 고객이 끊음 / 끊긴 걸 되살림. |
|
||||
| 유지율(persistency) | 가입한 계약이 13개월·25개월 뒤에도 살아있는 비율. **영업 건전성의 핵심 지표.** |
|
||||
| 청약 / 인수(언더라이팅) | 고객이 가입 신청 → 보험사가 위험 심사 후 받아들임. |
|
||||
| 위촉 / 해촉 | 설계사가 GA에 소속됨 / 소속이 끝남. |
|
||||
| 승계 | 설계사가 해촉되면 그가 관리하던 계약을 후임에게 넘기는 것. |
|
||||
| 모집 | 설계사가 새 계약을 따오는 행위. |
|
||||
| 환산보험료 | 상품마다 조건이 달라 비교가 어렵기 때문에, 정해진 "환산율"을 곱해 **공통 기준으로 환산한 보험료**. 실적 비교·시상의 기준. |
|
||||
| 인정실적 | 회사가 "이건 실적으로 인정한다"고 집계한 환산보험료 합. 등급·시상의 근거. |
|
||||
| 등급 | 설계사 레벨. 등급이 높을수록 지급률(받는 비율)이 높음. |
|
||||
| 오버라이드 | 부하 설계사 실적에 대해 상위 직책자가 받는 수당. |
|
||||
| 시상 | 목표 달성/우수자에게 주는 상금 성격의 수당. |
|
||||
| 시책(인센티브) | 한시적 캠페인 보너스. |
|
||||
| 환수(chargeback) | 계약이 일찍 깨지면 이미 준 수수료를 일부 돌려받는 것. |
|
||||
| 분급 | 큰 수수료를 한 번에 안 주고 여러 해에 나눠 주는 것. |
|
||||
| 정산 | 매월 수수료를 계산·확정·지급하는 업무 전체. |
|
||||
| 원장(ledger) | 계산의 최소 단위 기록 (계약 × 월 × 수수료종류 1줄). |
|
||||
| 명세서 | 설계사에게 "이번 달 얼마 받는지" 보여주는 내역서. |
|
||||
|
||||
---
|
||||
|
||||
## 4. 수수료가 뭐고 왜 종류가 많은가
|
||||
|
||||
**수수료 = 계약을 따오고(모집) 잘 유지시킨(유지) 대가로 보험사→GA→설계사로 흐르는 돈.**
|
||||
|
||||
왜 종류가 많을까요? 계약은 **태어나서(체결) → 유지되다가 → 갱신·부활도 하고 → 깨지기도(해지) 합니다.** 이 생애주기 단계마다, 또 영업을 도운 방식마다 다른 이름의 수수료가 붙기 때문입니다.
|
||||
|
||||
핵심 직관 하나만 기억하세요:
|
||||
> **"초년도(1차년) 모집수수료가 제일 크고, 그 다음부터는 유지·관리 대가로 잘게 나뉜다."**
|
||||
|
||||
---
|
||||
|
||||
## 5. 수수료 종류 한눈에 (우리 시스템이 다루는 전부)
|
||||
|
||||
우리 시스템은 아래 수수료/수당을 모두 계산합니다. (★ = incar 벤치마킹으로 새로 추가한 것 — P7/P8)
|
||||
|
||||
### 5-1. 계약 생애주기 기반
|
||||
| 종류 | 언제 | 누구에게 | 공식 한 줄 |
|
||||
|---|---|---|---|
|
||||
| 모집수수료 | 계약 체결(초년도) | 원 모집설계사 | 보험사지급액 × 지급률 |
|
||||
| 유지수수료 | 유지되는 매월/매년 | 현재 관리설계사(승계 시 후임) | 보험사지급액 × 지급률 |
|
||||
| 수금수수료 | 보험료가 실제 납입될 때 | 관리설계사 | 납입보험료 × 수금율 |
|
||||
| 갱신수수료 | 갱신형 상품 갱신 시 | 관리설계사 | 갱신보험료 × 갱신율 |
|
||||
| 부활수수료 | 실효 계약 부활 시 | 관리설계사 | 부활보험료 × 부활율 |
|
||||
|
||||
### 5-2. 성과·조직 기반
|
||||
| 종류 | 의미 | 공식 한 줄 |
|
||||
|---|---|---|
|
||||
| 오버라이드 | 하위 실적에 대한 상위 직책 수당 | 하위실적 × 오버라이드율 (cap 제한) |
|
||||
| 유지보수수당(지속수당) | 일정 유지율 달성 보너스 | 등급별 정액 또는 정률 |
|
||||
| 시책(인센티브) | 캠페인/목표 달성 프로모션 | 캠페인 규칙대로 |
|
||||
| ★ 시상(Award) | 신인도입/법인/유지율 등 시상 | 충족 시 실적×율 또는 정액 |
|
||||
| ★ 지점장수당 | 지점장이 소속 조직 실적에 받는 수당 | min(조직실적 × 수당율, 한도) |
|
||||
|
||||
### 5-3. 소개·이관·지원 (★ 신규)
|
||||
| 종류 | 의미 | 공식 한 줄 |
|
||||
|---|---|---|
|
||||
| ★ 소개수수료 | 계약을 소개해 준 설계사에게 | 기준금액 × 소개율 (또는 정액) |
|
||||
| ★ 이관수수료 | 계약을 다른 설계사에게 이관할 때 | 기준금액 × 이관율 |
|
||||
| ★ 정착지원금 | 신입/경력 설계사 정착 지원금(조기해촉 시 환수) | 등급별 정액 − 환수 |
|
||||
| ★ 생보운영지원수수료 | 생보 운영지원 명목 수수료 | 실적 × 지급률 − 환수 |
|
||||
|
||||
### 5-4. 회사(GA) 레벨 (★ 신규)
|
||||
| 종류 | 의미 | 공식 한 줄 |
|
||||
|---|---|---|
|
||||
| ★ 수입수수료 | 보험사가 GA에 준 돈(수입 측) | 보험료 × 보험사요율 |
|
||||
| ★ 수지차 | GA가 남긴 마진 | 수입 합 − 지급 합 |
|
||||
| ★ 인정실적 | 시상·등급의 기준이 되는 실적 | 환산보험료 × 인정율 |
|
||||
| ★ 본사정산 | GA 본사 자체 수입/비용 정산 | Σ수입 − Σ비용 |
|
||||
|
||||
### 5-5. 보조 관리 (★ 신규, 공식보다 관리 중심)
|
||||
| 항목 | 의미 |
|
||||
|---|---|
|
||||
| ★ 환산율(상품별) | 보험료를 공통 기준으로 환산하는 비율 |
|
||||
| ★ 등급평가 | 실적 점수로 설계사 등급 산정 |
|
||||
| ★ 공동계약 | 한 계약을 여러 설계사가 공동 모집, 지분율로 배분 |
|
||||
| ★ 보증보험 | 설계사 신원보증보험 가입 관리 |
|
||||
| ★ 적립금 | 환수 대비 수수료 일부 적립·잔액 관리 |
|
||||
| ★ 단계마감 | 정산을 수신→계산→검증→지급 단계로 마감 |
|
||||
| ★ 수수료 시뮬레이터 | 보험료/요율 입력 → 수수료·세금·실지급 미리 계산 |
|
||||
|
||||
> **모집 vs 유지의 귀속 차이(자주 묻는 함정):** 설계사가 해촉·이관되면 그 계약의 **유지수수료는 후임**에게 넘어가지만, **모집수수료는 원래 모집한 설계사**에게 그대로 남습니다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 수수료 계산의 핵심 공식 — "세 번 곱하기"
|
||||
|
||||
대부분의 수수료는 **곱하기 세 번**이면 끝납니다. 이것만 외우면 80%는 이해한 겁니다.
|
||||
|
||||
```
|
||||
① 보험사 지급액 = 보험료(premium) × 보험사요율(company_rate)
|
||||
② 설계사 수수료 = 보험사 지급액 × 지급률(payout_rate)
|
||||
③ 원천세 = 설계사 수수료 × 3.3%
|
||||
──────────────────────────────────────────────
|
||||
설계사 손에 = 설계사 수수료 − 원천세 (− 공제 + 부가세)
|
||||
```
|
||||
|
||||
두 가지 율의 뜻:
|
||||
- **보험사요율(company_rate)**: 보험사가 이 상품에 주기로 한 비율. **상품 × 회차(차년)**로 정해짐.
|
||||
- **지급률(payout_rate)**: GA가 받은 돈 중 설계사에게 주는 비율. **설계사 등급 × 보험종류 × 회차**로 정해짐. → 같은 계약이라도 **등급 높은 설계사가 더 많이** 받음.
|
||||
|
||||
> 모집수수료와 유지수수료는 **공식이 똑같고**, 회차(commission_year)만 다릅니다. (모집 = 1차년, 유지 = 2차년 이후)
|
||||
|
||||
---
|
||||
|
||||
## 6-A. 수수료 종류별 계산식 — 하나씩 풀어보기 (★ 전부)
|
||||
|
||||
5장 표의 "한 줄 공식"을 **실제 계산 방식 + 작은 숫자 예시**로 풀었습니다.
|
||||
대부분 **"무엇 × 율"** 한 줄이고, 일부만 환수·한도·조건이 붙습니다.
|
||||
(율은 코드에서 % 또는 소수로 들어오며, 모든 금액은 반올림 후 원 단위로 떨어집니다.)
|
||||
|
||||
### 계약 생애주기 기반
|
||||
|
||||
| 수수료 | 계산식 | 작은 예시 |
|
||||
|---|---|---|
|
||||
| **모집수수료** | 보험료 × 보험사요율 × 지급률 (세 번 곱하기, 1차년) | 100만 ×60% ×75% = **450,000** |
|
||||
| **유지수수료** | 〃 (회차만 2차년 이후, 율이 더 낮음) | 100만 ×10% ×50% = **50,000** |
|
||||
| **수금수수료** | 납입된 보험료 × 수금율 | 월납 100,000 ×5% = **5,000** |
|
||||
| **갱신수수료** | 갱신보험료 × 갱신율 (실무는 확정 금액 입력) | 갱신 80만 ×3% = **24,000** |
|
||||
| **부활수수료** | 부활 시 수수료 + **환수복원**(깎였던 환수 일부 되돌림) | 수수료 30,000 + 복원 10,000 = net **40,000** |
|
||||
|
||||
> **수금수수료가 왜 따로?** 계약만 따왔다고 끝이 아니라 **고객이 실제로 보험료를 내야** 돈이 들어옵니다. 그 "낸 보험료"에 비례해 주는 게 수금수수료입니다.
|
||||
|
||||
### 성과·조직 기반
|
||||
|
||||
| 수수료 | 계산식 | 작은 예시 |
|
||||
|---|---|---|
|
||||
| **오버라이드** | min(하위 설계사 실적 × 오버라이드율, **한도**) · 한도 없으면 무제한 | 하위 200만 ×4% = **80,000** (한도 10만 미만이라 그대로) |
|
||||
| **지속수당** | 유지율이 **기준 이상**일 때만 → 실적×율 또는 등급별 정액 | 13회차 유지율 85% ≥ 기준 → 정액 **100,000** |
|
||||
| **시상(Award)** | 실적이 **기준(threshold) 이상**일 때만 → 실적×시상율 또는 정액 | 기준 100만 **미달이면 0**, 충족 시 정액 **200,000** |
|
||||
| **지점장수당** | min(조직 실적 × 수당율, 한도) − 환수 | 조직 1,000만 ×1% = 100,000, 한도 8만 → **80,000** |
|
||||
|
||||
> **오버라이드 핵심:** "내 밑의 설계사가 잘 팔면 그 일부를 팀장/지점장이 받는다." 단, 룰에 보험종류 조건(예: 생보만)이 있으면 **그 종류 실적만** 곱합니다. 한도(cap)를 넘으면 한도까지만.
|
||||
> **시상·지속수당 핵심:** 기준(threshold·유지율)을 **넘어야만** 나옵니다. 못 넘으면 0원.
|
||||
|
||||
### 소개·이관·지원
|
||||
|
||||
| 수수료 | 계산식 | 작은 예시 |
|
||||
|---|---|---|
|
||||
| **소개수수료** | 기준금액 × 소개율 **또는** 정액 (룰이 RATE냐 FIXED냐) | 기준 100만 ×2% = **20,000** |
|
||||
| **이관수수료** | 기준금액 × 이관율 | 기준 100만 ×1% = **10,000** |
|
||||
| **정착지원금** | 등급별 월 정액 지급. 조기 해촉 시 **환수 = 정액 × (잔여약정개월 / 약정개월)**. net = 지급 − 환수 | 지급 50만, 환수 8만 → net **420,000** (환수는 지급액을 넘지 못함) |
|
||||
| **생보운영지원수수료** | 생보 실적 × 지급률 − 환수 | 실적 200만 ×3% = 60,000 − 환수 0 = **60,000** |
|
||||
|
||||
> **정착지원금 환수:** 신입에게 "정착하라"고 미리 준 돈이라, 약정 기간을 못 채우고 나가면 **남은 기간만큼 비례로 토해냅니다.** 단, 환수액이 받은 지원금보다 클 수는 없습니다(net이 마이너스가 되지 않음).
|
||||
|
||||
### 회사(GA) 레벨
|
||||
|
||||
| 항목 | 계산식 | 작은 예시 |
|
||||
|---|---|---|
|
||||
| **수입수수료** | 보험료 × 보험사요율 (보험사 → GA로 들어온 돈) | 100만 ×60% = **600,000** |
|
||||
| **수지차(마진)** | Σ수입 − Σ지급. 마진율 = 수지차 ÷ 수입 | 수입 600,000 − 지급 450,000 = **150,000** (마진율 25%) |
|
||||
| **인정실적** | 환산보험료(= 보험료 × 환산율) × 인정율 | 100만 ×환산 100% ×인정 90% = **900,000** |
|
||||
| **공동계약 배분** | 그 계약 수수료 × 본인 지분율 (지분 합은 100% 이하) | 450,000 ×지분 60% = **270,000** |
|
||||
| **적립금 잔액** | 전월 잔액 + 이번 적립 − 사용 | 100,000 + 20,000 − 5,000 = **115,000** |
|
||||
| **등급평가** | 실적 점수 → 점수 구간 룰 → 해당 등급 부여 | 점수 820 → "1등급" 구간 → 등급 상향 |
|
||||
| **수수료 시뮬레이터** | 보험료·요율 입력 → 위 ①②③ + 세금·예상환수 미리 계산 | 입력만 하면 실지급까지 미리보기 |
|
||||
|
||||
> **수입 vs 지급, 수지차:** 보험사가 GA에 준 돈(**수입**)에서 설계사에게 나간 돈(**지급**)을 빼면 GA가 남긴 마진(**수지차**)입니다. 회사가 돈을 버는 핵심 지표예요. (자세히는 11장)
|
||||
|
||||
### 한 줄 정리
|
||||
> **"무엇 × 율"이 기본.** 여기에 ① **조건**(시상·지속수당은 기준 넘어야), ② **한도**(오버라이드·지점장수당은 cap까지만), ③ **환수**(정착·생보지원은 토해내는 분 차감)만 더 붙는다고 보면 됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 6-B. 기타수수료 — 예외 가감과 그 밖의 정산 (★ 전부)
|
||||
|
||||
위 "공식이 있는" 수수료 말고도, 매월 정산액에 **그때그때 더하거나 빼는 항목**들이 있습니다.
|
||||
이걸 통틀어 **예외(가감) 항목**이라 부르고, "기타수수료"의 핵심입니다.
|
||||
|
||||
### 6-B-1. 예외 가감 (Exception) — 더하는 것(PLUS) / 빼는 것(MINUS)
|
||||
공식으로 안 나오는 1회성·수동 항목입니다. **방향(direction)**이 PLUS면 지급에 더하고, MINUS면 빼냅니다.
|
||||
|
||||
| 코드 | 이름 | 방향 | 의미 |
|
||||
|---|---|---|---|
|
||||
| ADV | 선지급 | ➕ PLUS | 나중에 받을 걸 미리 줌 |
|
||||
| PERF | 실적격려금 | ➕ PLUS | 실적 잘 낸 격려금 |
|
||||
| AWARD | 시상금 | ➕ PLUS | 시상 인센티브 |
|
||||
| SUPPORT | 운영지원금 | ➕ PLUS | 운영 보조 |
|
||||
| REFUND | 환급금 | ➕ PLUS | 자동 환급(auto) |
|
||||
| ETC | 기타 | ➕ PLUS | 그 외 잡다한 지급 |
|
||||
| PEN | 제재금 | ➖ MINUS | 징계·벌과금 |
|
||||
| PRECOL | 선차감 | ➖ MINUS | 전월 못 뗀 공제분 |
|
||||
| LOAN | 대여금공제 | ➖ MINUS | 빌려준 돈 자동 회수 |
|
||||
| TAX_ADJ | 세금조정 | ➖ MINUS | 세금 보정 |
|
||||
|
||||
- **계산:** 집계할 때 `+ 예외PLUS합 − 예외MINUS합` 으로 반영됩니다.
|
||||
- `auto_calc_yn='Y'`(REFUND·LOAN·TAX_ADJ)는 **자동 생성**, 나머지는 **수동 입력 + 승인** 후 반영됩니다.
|
||||
- `tax_included`(세금 포함 여부)에 따라 과세 대상인지 갈립니다.
|
||||
- **예시:** 이번 달 수수료 450,000 + 실적격려금(PLUS) 100,000 − 대여금공제(MINUS) 50,000 = **500,000** (세전)
|
||||
|
||||
> **예외 항목이 왜 필요?** 모든 돈이 공식으로 딱 떨어지지 않습니다. "이번 달만 격려금 준다", "지난달 빠뜨린 공제 뗀다" 같은 **1회성 가감**을 처리하는 창구가 예외 항목입니다.
|
||||
|
||||
### 6-B-2. 시책·인센티브 (Incentive)
|
||||
캠페인/목표 프로그램입니다. 프로그램 규칙대로 **기준실적(base) × 달성률** 또는 **확정 인센티브액**을 지급합니다.
|
||||
- 예시: "이달 신계약 5건 이상 시 30만 원" → 달성하면 incentive_amount **300,000**
|
||||
|
||||
### 6-B-3. 해촉정산 (설계사가 그만둘 때 마지막 계산)
|
||||
설계사가 해촉(퇴사)되면 그 시점에 한 번 **몰아서 정산**합니다.
|
||||
```
|
||||
최종 지급액 = 미지급분(미지급 모집 + 분급 잔액 + 미지급 시책)
|
||||
− 환수 예정액 (+ 면제분 제외)
|
||||
− 공제
|
||||
```
|
||||
- 미지급 분급(아직 안 준 7년치 잔액)과 환수 예정(곧 토해낼 것)을 **상계**해서 최종 한 번에 정리합니다.
|
||||
- 예시: 미지급 200,000 − 환수예정 80,000 − 공제 20,000 = 최종 **100,000**
|
||||
|
||||
### 6-B-4. 본사정산 (회사 자체 손익)
|
||||
설계사 지급과 **별개로**, GA 본사의 수입·비용을 집계합니다.
|
||||
```
|
||||
본사 손익(profit) = 수입(INCOME) 합 − 비용(EXPENSE) 합
|
||||
```
|
||||
- 설계사에게 나가는 돈이 아니라 **회사가 얼마 벌었나**를 보는 정산입니다. (수지차와 비슷한 회사 관점)
|
||||
|
||||
### 6-B-5. 보증보험 (수수료 아님 — 관리 항목)
|
||||
설계사가 횡령·사고 대비로 드는 **신원보증보험**입니다. 돈을 주는 게 아니라 **보증금액(guarantee_amount)**을 관리만 합니다.
|
||||
- 계산이 아니라 "이 설계사가 얼마짜리 보증보험에 가입돼 있나"를 기록하는 항목입니다.
|
||||
|
||||
### 한 줄 정리
|
||||
> 공식 수수료(6-A)가 **본 메뉴**라면, 기타수수료는 **양념**입니다.
|
||||
> ➕ 더하는 것(선지급·격려금·시상금·시책), ➖ 빼는 것(제재금·대여금·선차감), 그리고 **해촉 때 몰아서 정산**, **본사 손익**, **보증보험 관리**까지가 전부입니다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 숫자로 끝까지 따라가기 (한 계약의 일생)
|
||||
|
||||
연환산 보험료 **100만 원**짜리 계약 하나가 태어나서 깨질 때까지 따라가 봅시다.
|
||||
|
||||
### (A) 모집수수료 — 계약이 태어났다
|
||||
| 항목 | 값 | 계산 |
|
||||
|---|---|---|
|
||||
| 보험료(연환산) | 1,000,000원 | (가정) |
|
||||
| 보험사요율 | 60% | 상품·1차년 |
|
||||
| **① 보험사 지급액** | **600,000원** | 1,000,000 × 60% |
|
||||
| 지급률(이 설계사 등급) | 75% | 등급·1차년 |
|
||||
| **② 설계사 수수료** | **450,000원** | 600,000 × 75% |
|
||||
| **③ 원천세(3.3%)** | **14,850원** | 450,000 × 3.3% |
|
||||
| **원장에 남는 실수령** | **435,150원** | 450,000 − 14,850 |
|
||||
|
||||
### (B) 분급 — 한 번에 안 주고 7년에 나눠 준다
|
||||
큰 모집수수료(450,000원)는 규제상 7년에 걸쳐 비율대로 나눠 줍니다(35/25/15/10/7/5/3%).
|
||||
|
||||
| 차년 | 비율 | 지급액 |
|
||||
|---|---|---|
|
||||
| 1차년 | 35% | 157,500원 |
|
||||
| 2차년 | 25% | 112,500원 |
|
||||
| 3차년 | 15% | 67,500원 |
|
||||
| … | … | … |
|
||||
| **합계** | **100%** | **450,000원** |
|
||||
|
||||
→ 계약 직후 통장에 들어오는 건 450,000원이 아니라 **1차년분 157,500원(에서 세금 차감)**.
|
||||
|
||||
### (C) 환수 — 고객이 13개월 만에 해지했다
|
||||
유지 13~24개월 구간 환수율 **50%** 적용:
|
||||
```
|
||||
환수액 = 기지급 모집수수료 × 환수율 = 450,000 × 50% = 225,000원 ← 다음 정산에서 (−)로 차감
|
||||
```
|
||||
- 12개월 이내면 100%(전액 환수), 85개월 이상이면 0%(환수 없음).
|
||||
|
||||
### (D) 오버라이드 — 이 설계사의 팀장이 받는 수당
|
||||
```
|
||||
오버라이드 = 하위 실적 × 오버라이드율 = 2,000,000 × 4% = 80,000원 (cap 넘으면 cap까지만)
|
||||
```
|
||||
|
||||
### (E) 수지차 — 회사는 이 계약으로 얼마 남겼나 (★신규)
|
||||
```
|
||||
수입수수료(보험사가 GA에 준 돈) = 1,000,000 × 60% = 600,000원
|
||||
지급수수료(GA가 설계사에 준 돈) = 450,000원
|
||||
수지차(GA 마진) = 600,000 − 450,000 = 150,000원
|
||||
```
|
||||
|
||||
이 한 흐름이 머리에 들어왔다면, 나머지 수수료 종류는 "기준금액 × 율" 변형일 뿐입니다.
|
||||
|
||||
---
|
||||
|
||||
## 8. 그냥 곱하면 안 되는 이유 — 규제 3종
|
||||
|
||||
수수료는 법으로 한도·방식이 정해져 있어, 단순 곱셈 결과를 그대로 줄 수 없습니다.
|
||||
|
||||
1. **1200% 룰** (보험업감독규정): 초년도 모집수수료 총액은 **연납보험료의 1200%**를 못 넘음. 시스템이 누적 지급액을 추적해 초과분을 잘라냄.
|
||||
2. **분급**: 큰 수수료를 7년에 나눠 지급(위 7-B).
|
||||
3. **차등 환수**: 조기 해지 시 유지기간이 짧을수록 많이 환수(위 7-C).
|
||||
|
||||
> 회사마다 세율·한도가 달라도 **코드는 안 고칩니다.** 전부 데이터(`system_config`, `regulatory_limit`, `installment_ratio`, `chargeback_grade`)로 빼놨기 때문에 값만 바꾸면 됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 세금과 공제 — 통장에 적게 들어오는 이유
|
||||
|
||||
설계사에게 주는 돈(gross)에서 **세금을 먼저 떼고(원천징수)**, **공제를 빼면** 실제 입금액(net)이 됩니다.
|
||||
|
||||
| 항목 | 율 | 설명 |
|
||||
|---|---|---|
|
||||
| 사업소득 원천징수 | 3.3% | 일반 설계사(사업소득자) |
|
||||
| 기타소득 원천징수 | 8.8% | 기타소득 유형 |
|
||||
| 지방소득세 | 소득세의 10% | 소득세에 부가 |
|
||||
| 부가가치세 | 10% | 과세사업자가 세금계산서 발행 시 GA가 추가 지급(+) |
|
||||
|
||||
**공제(deduction)**: 미리 준 돈(가지급금) 회수, 대출 상환, **적립금** 등을 그 달 정산액에서 차감.
|
||||
|
||||
```
|
||||
gross − 원천세(소득세+지방세) − 공제 (+부가세, 과세사업자) = net(실지급액) → 계좌이체(펌뱅킹)
|
||||
```
|
||||
|
||||
**예시(과세사업자, 수수료 450,000원):** 소득세 14,850 + 지방세 1,485 빼고, 부가세 45,000 더하면 → **실지급 478,665원.**
|
||||
|
||||
---
|
||||
|
||||
## 10. 한 달이 흘러가는 과정 — 월 정산 파이프라인
|
||||
|
||||
매월 배치가 아래 순서로 돈다고 생각하면 됩니다.
|
||||
|
||||
```
|
||||
보험사 데이터 수신 → 표준포맷 변환 → 대사(검증) →
|
||||
모집수수료 계산 → 유지수수료 계산(승계 반영) → 환수·예외 →
|
||||
오버라이드 계산 → 집계(설계사×월, 세금·공제 반영) →
|
||||
회계 분개 → (분기/연말이면) 세무신고 자료
|
||||
```
|
||||
|
||||
마감은 **단계마감(★신규)**으로 수신→계산→검증→지급 단계별로 잠급니다. 한 달을 닫으면(마감) 더는 못 고치고, 오류는 **정정 전표**로만 바로잡습니다(원본 보존). 설계사가 금액에 이의를 제기하면 **이의신청**으로 받습니다.
|
||||
|
||||
**상태 흐름:** `DRAFT(계산됨) → CONFIRMED(확정) → PAID(지급완료)`. 확정·지급된 건은 재정산해도 안 바뀝니다(보호).
|
||||
|
||||
---
|
||||
|
||||
## 11. 수입 vs 지급, 그리고 수지차 — 회사는 어디서 남기나 (★ 신규 핵심)
|
||||
|
||||
신입이 가장 헷갈리는 지점입니다. 두 방향을 구분하세요.
|
||||
|
||||
| 방향 | 누가 → 누구 | 시스템 용어 |
|
||||
|---|---|---|
|
||||
| **수입(income)** | 보험사 → GA | 수입수수료 |
|
||||
| **지급(payout)** | GA → 설계사 | 모집/유지/… 수수료, 정산 |
|
||||
|
||||
```
|
||||
수지차(마진) = Σ수입수수료 − Σ지급수수료
|
||||
```
|
||||
|
||||
- 수지차가 (+)면 GA가 남긴 것, (−)면 그달 지급이 수입보다 많았던 것(분급·시상 등으로 일시적으로 발생 가능).
|
||||
- 여기에 **본사정산**(GA 자체 수입/비용)까지 합치면 회사 전체 손익이 보입니다.
|
||||
- 우리 시스템의 `수입수수료/수지차` 화면에서 **재계산 버튼**을 누르면 그달 수입 합과 지급 합(settle_master 기준)을 모아 수지차를 계산합니다.
|
||||
|
||||
---
|
||||
|
||||
## 12. 자주 나오는 질문 (FAQ)
|
||||
|
||||
1. **"수수료를 왜 한 번에 안 주죠?"** → 1200% 룰·분급 규제 + 조기해지 환수 리스크 때문. 7년 분할이 표준.
|
||||
2. **"설계사가 그만두면 그 계약 수수료는?"** → 유지수수료는 후임(승계)에게, 모집수수료는 원 설계사에게.
|
||||
3. **"gross인데 통장엔 적게 들어와요."** → 원천세(3.3%) + 공제를 뺀 net이 실지급액. (과세사업자는 부가세가 더해짐)
|
||||
4. **"같은 계약인데 설계사마다 수수료가 달라요."** → 지급률이 **설계사 등급**에 따라 다르기 때문.
|
||||
5. **"수입수수료랑 지급수수료가 헷갈려요."** → 수입 = 보험사가 우리에게 준 돈, 지급 = 우리가 설계사에게 준 돈. 차이가 수지차(마진).
|
||||
6. **"환산보험료는 왜 필요해요?"** → 상품마다 보험료 조건이 달라 그대로 비교하면 불공평. 환산율을 곱해 공통 기준으로 맞춘 게 환산보험료고, 그 합이 인정실적이 되어 시상·등급의 잣대가 됩니다.
|
||||
7. **"표준 세율/한도가 우리 회사랑 달라요."** → 코드 수정 불필요. 데이터(`system_config` 등)만 변경.
|
||||
|
||||
---
|
||||
|
||||
## 13. 용어 빠른 사전
|
||||
|
||||
| 한글 | 영문 | 한 줄 |
|
||||
|---|---|---|
|
||||
| 보험료 | Premium | 고객이 내는 돈. 계산의 출발점 |
|
||||
| 보험금 | Benefit | 사고·만기 때 고객이 받는 돈 |
|
||||
| 회차/차년 | Commission year | 계약 몇 년째. 1차년이 가장 큼 |
|
||||
| 유지율 | Persistency | 13·25개월 뒤 계약 생존율 |
|
||||
| 위촉/해촉 | Appoint/Terminate | 설계사 소속 시작/종료 |
|
||||
| 승계 | Succession | 해촉 시 후임 귀속 전환 |
|
||||
| 보험사요율 | Company rate | 보험사가 GA에 주는 비율 |
|
||||
| 지급률 | Payout rate | GA가 설계사에 주는 비율(등급별) |
|
||||
| 모집수수료 | Recruit | 체결 초년도 수수료(제일 큼) |
|
||||
| 유지수수료 | Maintain | 유지 기간 수수료 |
|
||||
| 수금수수료 | Collection | 보험료 납입 시 |
|
||||
| 갱신/부활수수료 | Renewal/Reinstatement | 갱신·부활 시 |
|
||||
| 오버라이드 | Override | 상위 직책 수당 |
|
||||
| 시상 | Award | 목표 달성 상금 |
|
||||
| 소개/이관수수료 | Referral/Transfer | 소개·이관 대가 |
|
||||
| 정착지원금 | Settling support | 신입 정착 지원금 |
|
||||
| 수입수수료 | Income commission | 보험사가 GA에 준 돈 |
|
||||
| 수지차 | Margin | 수입 − 지급 (GA 마진) |
|
||||
| 환산보험료 | Converted premium | 공통 기준 환산 보험료 |
|
||||
| 인정실적 | Recognized performance | 시상·등급 기준 실적 |
|
||||
| 본사정산 | HQ settlement | GA 자체 수입/비용 정산 |
|
||||
| 적립금 | Reserve fund | 환수 대비 적립·잔액 |
|
||||
| 분급 | Installment | 수수료 7년 분할 |
|
||||
| 차등환수 | Chargeback | 조기 해지 환수 |
|
||||
| 원천징수 | Withholding | 세금 미리 떼기(3.3%) |
|
||||
| 공제 | Deduction | 가지급금 등 차감 |
|
||||
| 단계마감 | Step close | 단계별 정산 잠금 |
|
||||
| 정산 | Settlement | 월별 계산·확정·지급 |
|
||||
| 원장 | Ledger | 계산 최소 단위 기록 |
|
||||
| 명세서 | Statement | 설계사 지급 내역서 |
|
||||
| 펌뱅킹 | Firm banking | 대량 계좌이체 |
|
||||
|
||||
---
|
||||
|
||||
*기준: 한국 보험업감독규정 표준 가정값. 시스템 schema (incar 갭 반영 P7+P8 14개 도메인 포함). 표준값은 데이터로 외부화되어 코드 변경 없이 조정 가능.*
|
||||
*더 깊은 코드/배치/규정 레벨은 [`DOMAIN_KNOWLEDGE.md`](DOMAIN_KNOWLEDGE.md), [`DOMAIN_GAP_P7.md`](DOMAIN_GAP_P7.md), [`DOMAIN_GAP_P8.md`](DOMAIN_GAP_P8.md) 참고.*
|
||||
@@ -0,0 +1,74 @@
|
||||
# 외부연동 활성화 가이드 (KFTC 펌뱅킹 / NHN Toast / FCM 푸시)
|
||||
|
||||
세 외부연동 어댑터는 **코드 구현 완료** 상태이며, 기본은 모두 **Mock** 으로 동작한다.
|
||||
운영 활성화는 아래 환경변수(또는 application.yml)만 주입하면 된다. 키가 비어 있으면 어댑터는 **fail-fast**(IllegalStateException) 하여 잘못된 적용을 즉시 알린다.
|
||||
|
||||
> 공통 전환 스위치: `app.adapter.{bank|message|push}` = `mock`(기본) / `kftc` / `toast` / `fcm`
|
||||
> 어댑터별 구현체는 `ga-external-adapter` 모듈, 모든 외부호출은 `external_call_log` 에 AOP 로 자동 감사된다.
|
||||
|
||||
---
|
||||
|
||||
## 1. KFTC 오픈뱅킹 입금이체 (펌뱅킹)
|
||||
|
||||
- 구현체: `KftcBankTransferAdapter` (입금이체 `POST /v2.0/transfer/deposit/acnt_num` + 이체결과조회)
|
||||
- 활성화: `ADAPTER_BANK=kftc`
|
||||
|
||||
| 환경변수 | 설명 | 예시 |
|
||||
|---|---|---|
|
||||
| `KFTC_BASE_URL` | API 베이스 (운영/테스트베드) | `https://openapi.openbanking.or.kr` |
|
||||
| `KFTC_ACCESS_TOKEN` | 이용기관 2-legged Access Token (주기 재발급) | (발급 토큰) |
|
||||
| `KFTC_CNTR_ACCOUNT_NUM` | 이용기관 약정 출금계좌 번호 | (약정계좌) |
|
||||
| `KFTC_CNTR_ACCOUNT_TYPE` | 약정계좌 구분 | `N` |
|
||||
| `KFTC_WD_PRINT` | 출금계좌 인자내역 | `수수료정산` |
|
||||
| `KFTC_NAME_CHECK` | 수취인 실명확인 옵션 | `off` |
|
||||
|
||||
**사전 조건(비개발)**: 금융결제원 이용기관 계약, 약정 출금계좌 등록, Access Token 발급 체계.
|
||||
**검증**: 테스트베드에서 출금 1건 → `withdraw_request.transfer_status` 전이 + `external_call_log` 확인.
|
||||
|
||||
---
|
||||
|
||||
## 2. NHN Cloud(Toast) SMS / 카카오 알림톡
|
||||
|
||||
- 구현체: `ToastMessageAdapter` (SMS v3.0 + 알림톡 v2.3, `MessageChannel` 분기)
|
||||
- 활성화: `ADAPTER_MESSAGE=toast`
|
||||
|
||||
| 환경변수 | 설명 |
|
||||
|---|---|
|
||||
| `TOAST_SMS_APP_KEY` / `TOAST_SMS_SECRET_KEY` | SMS 프로젝트 AppKey / Secret Key |
|
||||
| `TOAST_SENDER_NO` | 사전 등록 발신번호 |
|
||||
| `TOAST_ALIMTALK_APP_KEY` / `TOAST_ALIMTALK_SECRET_KEY` | 알림톡 AppKey / Secret Key |
|
||||
| `TOAST_SENDER_KEY` | 카카오 비즈메시지 발신프로필 SenderKey |
|
||||
|
||||
**사전 조건(비개발)**: NHN Cloud 프로젝트 + SMS/알림톡 상품 활성화, 발신번호 등록, 알림톡 **템플릿 등록**(발송 시 `templateCode` 필수).
|
||||
**검증**: 본인 번호로 SMS 1건 발송 → `header.isSuccessful=true` + `external_call_log` 확인.
|
||||
|
||||
---
|
||||
|
||||
## 3. FCM 푸시 (모바일 PWA)
|
||||
|
||||
- 서버 구현체: `FcmPushAdapter` (HTTP v1 `POST /v1/projects/{projectId}/messages:send`)
|
||||
- 활성화(서버): `ADAPTER_PUSH=fcm`
|
||||
|
||||
| 환경변수(서버) | 설명 |
|
||||
|---|---|
|
||||
| `FCM_PROJECT_ID` | Firebase 프로젝트 ID |
|
||||
| `FCM_ACCESS_TOKEN` | 서비스계정 OAuth2 토큰(scope `firebase.messaging`, 주기 재발급) |
|
||||
|
||||
**PWA(클라이언트) 활성화** — `ga-mobile-pwa`:
|
||||
1. Firebase 콘솔 → 웹 앱 등록 → 설정값을 `.env` 의 `VITE_FCM_*` 로 주입
|
||||
(`VITE_FCM_API_KEY`, `VITE_FCM_PROJECT_ID`, `VITE_FCM_APP_ID`, `VITE_FCM_SENDER_ID`, `VITE_FCM_VAPID_KEY`)
|
||||
2. `npm i firebase` 후 `src/push/initPush.ts` 의 TODO 블록(firebase/messaging `getToken`) 활성화
|
||||
3. `public/firebase-messaging-sw.js` 서비스워커 배치
|
||||
4. 로그인 시 `initPush()` 가 토큰 발급 → `POST /api/mobile/me/device-tokens` 자동 등록
|
||||
|
||||
**동작 흐름(구현 완료)**: 알림 발생(예: 출금 SETTLED) → `PushDispatchService` 가 `user_device_token` 의 활성 토큰 조회 → `PushAdapter.send`. 토큰 미등록/Mock 이면 no-op.
|
||||
**검증**: PWA 로그인 → device-token 등록 확인(`user_device_token`) → 출금 SETTLED 트리거 → 푸시 수신 + `external_call_log(call_type=PUSH)` 확인.
|
||||
|
||||
---
|
||||
|
||||
## 요약 — 남은 것은 "키 주입"뿐
|
||||
| 연동 | 코드 | 활성화에 필요한 것(비개발) |
|
||||
|---|---|---|
|
||||
| KFTC 입금이체 | ✅ 완료 | 계약 + 약정계좌 + Access Token |
|
||||
| Toast SMS/알림톡 | ✅ 완료 | AppKey/SecretKey + 발신번호 + 알림톡 템플릿 |
|
||||
| FCM 푸시 | ✅ 완료(서버) / 스캐폴드(PWA) | Firebase 프로젝트 + `firebase` SDK + VAPID 키 |
|
||||
@@ -10,5 +10,6 @@ jar.enabled = false
|
||||
|
||||
dependencies {
|
||||
implementation project(':ga-core')
|
||||
implementation project(':ga-external-adapter')
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ga.api;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* GA 수수료 정산 API 서버.
|
||||
@@ -10,6 +11,7 @@ import org.springframework.cache.annotation.EnableCaching;
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = {"com.ga"})
|
||||
@EnableCaching
|
||||
@EnableScheduling
|
||||
public class GaApiApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(GaApiApplication.class, args);
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package com.ga.api.aop;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ga.api.service.external.ExternalCallLogService;
|
||||
import com.ga.common.adapter.bank.BankTransferRequest;
|
||||
import com.ga.common.adapter.bank.BankTransferResponse;
|
||||
import com.ga.common.adapter.message.MessageRequest;
|
||||
import com.ga.common.adapter.message.MessageResponse;
|
||||
import com.ga.common.util.SecurityUtil;
|
||||
import com.ga.core.vo.external.ExternalCallLogVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 외부 연동 어댑터 호출 감사 이력 자동 영속화 (V85 external_call_log).
|
||||
*
|
||||
* <p>대상: {@link com.ga.common.adapter.bank.BankTransferAdapter},
|
||||
* {@link com.ga.common.adapter.message.MessageAdapter} 모든 메서드.
|
||||
*
|
||||
* <p>정책:
|
||||
* <ul>
|
||||
* <li>호출 전/후 (정상·예외) 모두 영속화. 예외는 그대로 propagate.
|
||||
* <li>request/response 는 마스킹된 JSON 요약만 저장 (계좌·전화 뒤 4자리만).
|
||||
* <li>영속화는 별도 트랜잭션({@link com.ga.api.service.external.ExternalCallLogService}#recordCall = REQUIRES_NEW).
|
||||
* <li>영속화 자체가 실패해도 비즈니스 흐름을 깨지 않는다 (warn 로그만).
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ExternalCallLoggingAspect {
|
||||
|
||||
private final ExternalCallLogService logService;
|
||||
private final ObjectMapper om;
|
||||
|
||||
@Around("execution(* com.ga.common.adapter.bank.BankTransferAdapter+.*(..))")
|
||||
public Object aroundBank(ProceedingJoinPoint jp) throws Throwable {
|
||||
return around(jp, "BANK");
|
||||
}
|
||||
|
||||
@Around("execution(* com.ga.common.adapter.message.MessageAdapter+.*(..))")
|
||||
public Object aroundMessage(ProceedingJoinPoint jp) throws Throwable {
|
||||
return around(jp, "MESSAGE");
|
||||
}
|
||||
|
||||
@Around("execution(* com.ga.common.adapter.push.PushAdapter+.*(..))")
|
||||
public Object aroundPush(ProceedingJoinPoint jp) throws Throwable {
|
||||
return around(jp, "PUSH");
|
||||
}
|
||||
|
||||
private Object around(ProceedingJoinPoint jp, String callType) throws Throwable {
|
||||
long start = System.currentTimeMillis();
|
||||
ExternalCallLogVO vo = new ExternalCallLogVO();
|
||||
vo.setCallType(callType);
|
||||
vo.setAdapterClass(simpleName(jp.getTarget()));
|
||||
vo.setMethodName(jp.getSignature().getName());
|
||||
vo.setCalledAt(LocalDateTime.now());
|
||||
try {
|
||||
vo.setCreatedBy(SecurityUtil.getCurrentUserId());
|
||||
} catch (Exception ignore) {
|
||||
// 스케줄러/배치 컨텍스트에서는 인증 정보 없을 수 있음
|
||||
}
|
||||
applyRequest(vo, jp.getArgs());
|
||||
|
||||
try {
|
||||
Object result = jp.proceed();
|
||||
vo.setSuccess(true);
|
||||
applyResponse(vo, result);
|
||||
return result;
|
||||
} catch (Throwable t) {
|
||||
vo.setSuccess(false);
|
||||
vo.setErrorClass(t.getClass().getName());
|
||||
vo.setErrorMessage(truncate(t.getMessage(), 4000));
|
||||
throw t;
|
||||
} finally {
|
||||
vo.setDurationMs((int) (System.currentTimeMillis() - start));
|
||||
try {
|
||||
logService.recordCall(vo);
|
||||
} catch (Exception persistEx) {
|
||||
log.warn("[external_call_log] persist failed: {}", persistEx.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyRequest(ExternalCallLogVO vo, Object[] args) {
|
||||
if (args == null || args.length == 0) return;
|
||||
Object a0 = args[0];
|
||||
if (a0 instanceof BankTransferRequest btr) {
|
||||
vo.setTargetRefType("WITHDRAW");
|
||||
vo.setTargetRefId(btr.getWithdrawRequestId());
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("withdrawRequestId", btr.getWithdrawRequestId());
|
||||
m.put("bankCode", btr.getBankCode());
|
||||
m.put("accountNoMasked", maskAccount(btr.getAccountNo()));
|
||||
m.put("holder", maskName(btr.getAccountHolderName()));
|
||||
m.put("amount", btr.getAmount());
|
||||
m.put("memo", btr.getMemo());
|
||||
vo.setRequestSummary(toJson(m));
|
||||
} else if (a0 instanceof MessageRequest mr) {
|
||||
vo.setTargetRefType(mr.getRefType());
|
||||
vo.setTargetRefId(mr.getRefId());
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("channel", mr.getChannel());
|
||||
m.put("phoneMasked", maskPhone(mr.getPhoneNumber()));
|
||||
m.put("title", mr.getTitle());
|
||||
m.put("templateCode", mr.getTemplateCode());
|
||||
m.put("refUserId", mr.getRefUserId());
|
||||
m.put("contentPreview", truncate(mr.getContent(), 80));
|
||||
vo.setRequestSummary(toJson(m));
|
||||
} else if (a0 instanceof com.ga.common.adapter.push.PushRequest pr) {
|
||||
vo.setTargetRefType(pr.getRefType());
|
||||
vo.setTargetRefId(pr.getRefId());
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("tokenMasked", maskToken(pr.getDeviceToken()));
|
||||
m.put("title", pr.getTitle());
|
||||
m.put("refUserId", pr.getRefUserId());
|
||||
m.put("bodyPreview", truncate(pr.getBody(), 80));
|
||||
vo.setRequestSummary(toJson(m));
|
||||
} else if (a0 instanceof String s) {
|
||||
// BankTransferAdapter.queryStatus(txId)
|
||||
vo.setRequestSummary("{\"arg\":\"" + safeJsonString(s) + "\"}");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyResponse(ExternalCallLogVO vo, Object result) {
|
||||
if (result instanceof BankTransferResponse btr) {
|
||||
vo.setResultCode(btr.getResultCode());
|
||||
vo.setResultMessage(truncate(btr.getResultMessage(), 1000));
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("txId", btr.getTxId());
|
||||
m.put("status", btr.getStatus());
|
||||
m.put("processedAt", btr.getProcessedAt());
|
||||
vo.setResponseSummary(toJson(m));
|
||||
} else if (result instanceof MessageResponse mr) {
|
||||
vo.setResultCode(mr.getResultCode());
|
||||
vo.setResultMessage(truncate(mr.getResultMessage(), 1000));
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("messageId", mr.getMessageId());
|
||||
m.put("sent", mr.isSent());
|
||||
m.put("sentAt", mr.getSentAt());
|
||||
vo.setResponseSummary(toJson(m));
|
||||
} else if (result instanceof com.ga.common.adapter.push.PushResponse pr) {
|
||||
vo.setResultCode(pr.getResultCode());
|
||||
vo.setResultMessage(truncate(pr.getResultMessage(), 1000));
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("messageId", pr.getMessageId());
|
||||
m.put("sent", pr.isSent());
|
||||
m.put("sentAt", pr.getSentAt());
|
||||
vo.setResponseSummary(toJson(m));
|
||||
}
|
||||
}
|
||||
|
||||
private String simpleName(Object o) {
|
||||
return o == null ? null : o.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
private String toJson(Object o) {
|
||||
try {
|
||||
return om.writeValueAsString(o);
|
||||
} catch (Exception e) {
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
private String maskAccount(String accountNo) {
|
||||
if (accountNo == null) return null;
|
||||
String d = accountNo.replaceAll("[^0-9]", "");
|
||||
if (d.length() <= 4) return "****";
|
||||
return "****" + d.substring(d.length() - 4);
|
||||
}
|
||||
|
||||
private String maskPhone(String phone) {
|
||||
if (phone == null) return null;
|
||||
String d = phone.replaceAll("[^0-9]", "");
|
||||
if (d.length() <= 4) return "****";
|
||||
return "***-****-" + d.substring(d.length() - 4);
|
||||
}
|
||||
|
||||
private String maskName(String name) {
|
||||
if (name == null || name.isEmpty()) return null;
|
||||
return name.charAt(0) + "*".repeat(Math.max(0, name.length() - 1));
|
||||
}
|
||||
|
||||
private String maskToken(String token) {
|
||||
if (token == null) return null;
|
||||
return token.length() <= 8 ? "****" : token.substring(0, 8) + "****";
|
||||
}
|
||||
|
||||
private String truncate(String s, int max) {
|
||||
if (s == null) return null;
|
||||
return s.length() <= max ? s : s.substring(0, max);
|
||||
}
|
||||
|
||||
private String safeJsonString(String s) {
|
||||
return s == null ? "" : s.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.ga.api.controller.accounting;
|
||||
|
||||
import com.ga.api.service.accounting.AccountingCloseService;
|
||||
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.accounting.AccountBalanceSnapshotVO;
|
||||
import com.ga.core.vo.accounting.AccountingCloseResp;
|
||||
import com.ga.core.vo.accounting.AccountingCloseSaveReq;
|
||||
import com.ga.core.vo.accounting.AccountingCloseSearchParam;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
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;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 회계 결산(accounting_close) 컨트롤러.
|
||||
*
|
||||
* <p>"결산"은 일정 기간(월/분기/연)의 회계 분개를 마감하고 계정과목별 잔액을 확정 저장하는 작업입니다.
|
||||
* 흐름: DRAFT 생성(createDraft) → 마감 실행(close, 미전기 분개에 전표번호 부여 + 잔액 스냅샷 생성) → 필요 시 재오픈(reopen).
|
||||
*
|
||||
* <p>상태: DRAFT / REOPENED 에서만 마감 가능. 잔액 스냅샷(snapshots)은 계정과목별 차변·대변 합계입니다.
|
||||
* 권한(menu=ACCOUNTING_CLOSE): 조회 READ, DRAFT생성 CREATE, 마감 EXECUTE, 재오픈 APPROVE.
|
||||
*/
|
||||
@Tag(name = "회계 결산")
|
||||
@RestController
|
||||
@RequestMapping("/api/accounting-closes")
|
||||
@RequiredArgsConstructor
|
||||
public class AccountingCloseController {
|
||||
|
||||
private final AccountingCloseService service;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "회계 결산 목록")
|
||||
@RequirePermission(menu = "ACCOUNTING_CLOSE", perm = "READ")
|
||||
public ApiResponse<PageResponse<AccountingCloseResp>> list(@ModelAttribute AccountingCloseSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "회계 결산 상세")
|
||||
@RequirePermission(menu = "ACCOUNTING_CLOSE", perm = "READ")
|
||||
public ApiResponse<AccountingCloseResp> detail(@PathVariable Long id) {
|
||||
return ApiResponse.ok(service.detail(id));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/snapshots")
|
||||
@Operation(summary = "계정과목별 잔액 스냅샷")
|
||||
@RequirePermission(menu = "ACCOUNTING_CLOSE", perm = "READ")
|
||||
public ApiResponse<List<AccountBalanceSnapshotVO>> snapshots(@PathVariable Long id) {
|
||||
return ApiResponse.ok(service.snapshots(id));
|
||||
}
|
||||
|
||||
@PostMapping("/draft")
|
||||
@Operation(summary = "결산 DRAFT 생성")
|
||||
@RequirePermission(menu = "ACCOUNTING_CLOSE", perm = "CREATE")
|
||||
@DataChangeLog(menu = "ACCOUNTING_CLOSE", table = "accounting_close")
|
||||
public ApiResponse<Long> createDraft(@Valid @RequestBody AccountingCloseSaveReq req) {
|
||||
return ApiResponse.ok(service.createDraft(req));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/close")
|
||||
@Operation(summary = "결산 마감 실행")
|
||||
@RequirePermission(menu = "ACCOUNTING_CLOSE", perm = "EXECUTE")
|
||||
@DataChangeLog(menu = "ACCOUNTING_CLOSE", table = "accounting_close")
|
||||
public ApiResponse<AccountingCloseResp> close(@PathVariable Long id) {
|
||||
return ApiResponse.ok(service.close(id));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reopen")
|
||||
@Operation(summary = "결산 재오픈")
|
||||
@RequirePermission(menu = "ACCOUNTING_CLOSE", perm = "APPROVE")
|
||||
@DataChangeLog(menu = "ACCOUNTING_CLOSE", table = "accounting_close")
|
||||
public ApiResponse<Void> reopen(@PathVariable Long id, @RequestBody Map<String, String> body) {
|
||||
service.reopen(id, body.get("reopenReason"));
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@ import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 분개 마감 요청 DTO.
|
||||
* entryIds: 마감할 분개 ID 목록(필수, 1건 이상). journalNo: 부여할 전표번호(필수).
|
||||
*/
|
||||
@Data
|
||||
public class CloseJournalReq {
|
||||
@NotEmpty
|
||||
|
||||
+10
@@ -13,6 +13,16 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 계약 회계 분개(contract_accounting_entry) 컨트롤러.
|
||||
*
|
||||
* <p>"분개"는 회계의 기본 기록 단위로, 거래 하나를 차변(빌린 곳)/대변(빌려준 곳) 두 계정으로 적는 것입니다.
|
||||
* 여기서는 수수료 지급(payment) 건을 회계 분개로 자동 변환합니다.
|
||||
* (차변: 8350 지급수수료 / 대변: 2530 미지급금)
|
||||
*
|
||||
* <p>generate: paymentIds 또는 settleMonth 기준으로 분개를 일괄 생성. close: 분개에 전표번호를 붙여 마감 처리.
|
||||
* 권한(menu=ACCOUNTING_JOURNAL): 조회 READ, 분개생성 CREATE, 마감 UPDATE.
|
||||
*/
|
||||
@Tag(name = "계약 회계 분개")
|
||||
@RestController
|
||||
@RequestMapping("/api/accounting-entries")
|
||||
|
||||
@@ -4,6 +4,10 @@ import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 자동 분개 생성 요청 DTO.
|
||||
* paymentIds 가 있으면 해당 지급건들로, 없으면 settleMonth(정산월) 전체 지급건으로 분개를 생성합니다.
|
||||
*/
|
||||
@Data
|
||||
public class GenerateEntriesReq {
|
||||
private List<Long> paymentIds;
|
||||
|
||||
@@ -4,12 +4,20 @@ import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 결재 단계 진행(승인/반려) 요청 DTO.
|
||||
*
|
||||
* <p>결재자(approverId)가 현재 단계에 대해 action(APPROVE=승인 / REJECT=반려)을 처리할 때 전달된다.
|
||||
* comment는 선택 입력이다.</p>
|
||||
*/
|
||||
@Data
|
||||
public class AdvanceReq {
|
||||
/** 결재 처리자(user_id). */
|
||||
@NotNull
|
||||
private Long approverId;
|
||||
/** APPROVE / REJECT */
|
||||
@NotBlank
|
||||
private String action;
|
||||
/** 결재 코멘트(선택). */
|
||||
private String comment;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,17 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 전자결재 관리 API.
|
||||
*
|
||||
* <p>두 개념을 다룬다.
|
||||
* <ul>
|
||||
* <li>결재선(line): 결재 단계(step) 시퀀스의 템플릿. 각 단계는 결재 권한 역할(roleCode)로 정의된다.</li>
|
||||
* <li>결재 요청(request): 특정 대상(출금/정산/이의신청/시책 등 targetType)에 대한 상신 건.
|
||||
* 결재선을 따라 단계별로 진행(advance)되며, 최종 승인 시 대상 도메인에 후처리 콜백이 일어난다.</li>
|
||||
* </ul>
|
||||
* 메뉴 APPROVAL 권한으로 보호되며, 단계 진행만 APPROVE 권한을 요구한다.</p>
|
||||
*/
|
||||
@Tag(name = "결재 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/approvals")
|
||||
@@ -23,6 +34,7 @@ public class ApprovalController {
|
||||
|
||||
// ── 결재선 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 결재선 목록 조회(페이징). READ 권한 필요. */
|
||||
@GetMapping("/lines")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "READ")
|
||||
public ApiResponse<PageResponse<ApprovalLineResp>> listLines(
|
||||
@@ -30,18 +42,21 @@ public class ApprovalController {
|
||||
return ApiResponse.ok(service.listLines(param));
|
||||
}
|
||||
|
||||
/** 결재선 단건 상세 조회. READ 권한 필요. */
|
||||
@GetMapping("/lines/{lineId}")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "READ")
|
||||
public ApiResponse<ApprovalLineResp> detailLine(@PathVariable Long lineId) {
|
||||
return ApiResponse.ok(service.detailLine(lineId));
|
||||
}
|
||||
|
||||
/** 특정 결재선의 단계(step) 목록 조회. READ 권한 필요. */
|
||||
@GetMapping("/lines/{lineId}/steps")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "READ")
|
||||
public ApiResponse<List<ApprovalLineStepResp>> stepsOfLine(@PathVariable Long lineId) {
|
||||
return ApiResponse.ok(service.stepsOfLine(lineId));
|
||||
}
|
||||
|
||||
/** 결재선 신규 등록. CREATE 권한 필요. */
|
||||
@PostMapping("/lines")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "CREATE")
|
||||
@DataChangeLog(menu = "APPROVAL", table = "approval_line")
|
||||
@@ -49,6 +64,7 @@ public class ApprovalController {
|
||||
return ApiResponse.ok(service.createLine(req));
|
||||
}
|
||||
|
||||
/** 결재선 수정. UPDATE 권한 필요. */
|
||||
@PutMapping("/lines/{lineId}")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "APPROVAL", table = "approval_line")
|
||||
@@ -58,6 +74,7 @@ public class ApprovalController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 결재선에 단계(step) 추가. 경로의 lineId를 요청에 주입한다. UPDATE 권한 필요. */
|
||||
@PostMapping("/lines/{lineId}/steps")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "APPROVAL", table = "approval_line_step")
|
||||
@@ -70,6 +87,7 @@ public class ApprovalController {
|
||||
|
||||
// ── 결재 요청 ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** 결재 요청 전체 목록 조회(페이징). READ 권한 필요. */
|
||||
@GetMapping("/requests")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "READ")
|
||||
public ApiResponse<PageResponse<ApprovalRequestResp>> listRequests(
|
||||
@@ -77,18 +95,37 @@ public class ApprovalController {
|
||||
return ApiResponse.ok(service.listRequests(param));
|
||||
}
|
||||
|
||||
/** 내가 상신한 결재 요청 목록. 현재 로그인 사용자로 필터된다. READ 권한 필요. */
|
||||
@GetMapping("/requests/mine")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "READ")
|
||||
public ApiResponse<PageResponse<ApprovalRequestResp>> myRequests(
|
||||
@ModelAttribute ApprovalRequestSearchParam param) {
|
||||
return ApiResponse.ok(service.myRequests(param));
|
||||
}
|
||||
|
||||
/** 진행 중(PENDING) 결재 요청 목록. READ 권한 필요. */
|
||||
@GetMapping("/requests/pending")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "READ")
|
||||
public ApiResponse<PageResponse<ApprovalRequestResp>> pendingRequests(
|
||||
@ModelAttribute ApprovalRequestSearchParam param) {
|
||||
return ApiResponse.ok(service.pendingRequests(param));
|
||||
}
|
||||
|
||||
/** 결재 요청 단건 상세 조회. READ 권한 필요. */
|
||||
@GetMapping("/requests/{requestId}")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "READ")
|
||||
public ApiResponse<ApprovalRequestResp> detailRequest(@PathVariable Long requestId) {
|
||||
return ApiResponse.ok(service.detailRequest(requestId));
|
||||
}
|
||||
|
||||
/** 결재 요청의 단계별 처리 이력 조회. READ 권한 필요. */
|
||||
@GetMapping("/requests/{requestId}/history")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "READ")
|
||||
public ApiResponse<List<ApprovalHistoryResp>> historyOfRequest(@PathVariable Long requestId) {
|
||||
return ApiResponse.ok(service.historyOfRequest(requestId));
|
||||
}
|
||||
|
||||
/** 결재 상신(요청 생성). PENDING 상태로 시작하며 1단계 결재자에게 알림이 발송된다. CREATE 권한 필요. */
|
||||
@PostMapping("/requests")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "CREATE")
|
||||
@DataChangeLog(menu = "APPROVAL", table = "approval_request")
|
||||
@@ -96,6 +133,7 @@ public class ApprovalController {
|
||||
return ApiResponse.ok(service.createRequest(req));
|
||||
}
|
||||
|
||||
/** 결재 단계 진행(승인/반려). 마지막 단계 승인 시 대상 도메인에 후처리 콜백이 일어난다. APPROVE 권한 필요. */
|
||||
@PostMapping("/requests/{requestId}/advance")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "APPROVE")
|
||||
@DataChangeLog(menu = "APPROVAL", table = "approval_request")
|
||||
@@ -105,6 +143,7 @@ public class ApprovalController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 결재 요청 취소. 진행 중(PENDING)인 건만 취소 가능. UPDATE 권한 필요. */
|
||||
@PostMapping("/requests/{requestId}/cancel")
|
||||
@RequirePermission(menu = "APPROVAL", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "APPROVAL", table = "approval_request")
|
||||
|
||||
@@ -23,6 +23,15 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 첨부파일 관리 API.
|
||||
*
|
||||
* <p>각종 도메인(refType + refId 조합)에 파일을 첨부·다운로드·삭제한다.
|
||||
* refType은 "어떤 종류의 대상인지"(예: 계약/민원 등), refId는 그 대상의 PK다.
|
||||
* 파일은 서버 디스크에 저장되고, 메타 정보(파일명/경로/크기/MIME)는 DB로 관리된다.</p>
|
||||
*
|
||||
* <p>메뉴 ATTACHMENT 권한으로 보호된다.</p>
|
||||
*/
|
||||
@Tag(name = "첨부파일")
|
||||
@RestController
|
||||
@RequestMapping("/api/attachments")
|
||||
@@ -31,12 +40,14 @@ public class AttachmentController {
|
||||
|
||||
private final AttachmentService service;
|
||||
|
||||
/** 첨부파일 메타 목록 조회(페이징). READ 권한 필요. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "ATTACHMENT", perm = "READ")
|
||||
public ApiResponse<PageResponse<AttachmentResp>> list(@ModelAttribute AttachmentSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 특정 대상(refType+refId)에 달린 첨부파일 목록 조회. READ 권한 필요. */
|
||||
@GetMapping("/by-ref")
|
||||
@RequirePermission(menu = "ATTACHMENT", perm = "READ")
|
||||
public ApiResponse<List<AttachmentResp>> listByRef(@RequestParam String refType,
|
||||
@@ -44,12 +55,14 @@ public class AttachmentController {
|
||||
return ApiResponse.ok(service.listByRef(refType, refId));
|
||||
}
|
||||
|
||||
/** 첨부파일 메타 단건 조회. READ 권한 필요. */
|
||||
@GetMapping("/{attachmentId}/meta")
|
||||
@RequirePermission(menu = "ATTACHMENT", perm = "READ")
|
||||
public ApiResponse<AttachmentResp> detail(@PathVariable Long attachmentId) {
|
||||
return ApiResponse.ok(service.detail(attachmentId));
|
||||
}
|
||||
|
||||
/** 파일 업로드(multipart). 대상(refType+refId)에 연결해 저장한다. CREATE 권한 필요. */
|
||||
@PostMapping(consumes = "multipart/form-data")
|
||||
@RequirePermission(menu = "ATTACHMENT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "ATTACHMENT", table = "attachment")
|
||||
@@ -59,6 +72,7 @@ public class AttachmentController {
|
||||
return ApiResponse.ok(service.upload(file, refType, refId));
|
||||
}
|
||||
|
||||
/** 파일 다운로드. 메타의 MIME/파일명으로 응답 헤더를 구성하고 파일 스트림을 내려준다. READ 권한 필요. */
|
||||
@GetMapping("/{attachmentId}/download")
|
||||
@RequirePermission(menu = "ATTACHMENT", perm = "READ")
|
||||
public void download(@PathVariable Long attachmentId, HttpServletResponse response) throws IOException {
|
||||
@@ -75,6 +89,7 @@ public class AttachmentController {
|
||||
}
|
||||
}
|
||||
|
||||
/** 첨부파일 삭제(메타 + 실제 파일). DELETE 권한 필요. */
|
||||
@DeleteMapping("/{attachmentId}")
|
||||
@RequirePermission(menu = "ATTACHMENT", perm = "DELETE")
|
||||
@DataChangeLog(menu = "ATTACHMENT", table = "attachment")
|
||||
|
||||
@@ -50,12 +50,14 @@ public class BatchTriggerController {
|
||||
return ApiResponse.ok(log.getJobId());
|
||||
}
|
||||
|
||||
/** 특정 배치 잡의 최신 실행 상태 조회(기본 월정산). READ 권한 필요. */
|
||||
@GetMapping("/status")
|
||||
@RequirePermission(menu = "BATCH_RUN", perm = "READ")
|
||||
public ApiResponse<BatchJobLogVO> latest(@RequestParam(defaultValue = "MONTHLY_SETTLEMENT") String jobName) {
|
||||
return ApiResponse.ok(jobLogMapper.selectLatest(jobName));
|
||||
}
|
||||
|
||||
/** 배치 실행 이력 목록 조회(잡명/정산월 필터). READ 권한 필요. */
|
||||
@GetMapping("/jobs")
|
||||
@RequirePermission(menu = "BATCH_RUN", perm = "READ")
|
||||
public ApiResponse<List<BatchJobLogVO>> list(@RequestParam(required = false) String jobName,
|
||||
@@ -63,6 +65,7 @@ public class BatchTriggerController {
|
||||
return ApiResponse.ok(jobLogMapper.selectList(jobName, settleMonth));
|
||||
}
|
||||
|
||||
/** 배치 실행 이력 단건 상세 조회. READ 권한 필요. */
|
||||
@GetMapping("/jobs/{jobId}")
|
||||
@RequirePermission(menu = "BATCH_RUN", perm = "READ")
|
||||
public ApiResponse<BatchJobLogVO> detail(@PathVariable Long jobId) {
|
||||
|
||||
@@ -7,18 +7,27 @@ import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 차등환수 금액 계산(시뮬레이션) 요청 DTO.
|
||||
* 차등환수란 고객이 계약을 조기 해지하면 이미 지급한 수수료의 일부를 경과기간에 따라 차등 회수하는 것이다.
|
||||
* (계약일~해지일 경과개월수가 짧을수록 환수율이 높다)
|
||||
*/
|
||||
@Data
|
||||
public class ChargebackCalculateReq {
|
||||
|
||||
/** 계약 체결일. 경과개월수 계산의 시작점. */
|
||||
@NotNull
|
||||
private LocalDate contractDate;
|
||||
|
||||
/** 해지일. 계약일과의 차이로 경과개월수를 구한다. */
|
||||
@NotNull
|
||||
private LocalDate terminationDate;
|
||||
|
||||
/** 당초 지급했던 수수료 원금(양수). 여기에 환수율을 곱해 환수액을 산출한다. */
|
||||
@NotNull
|
||||
@Positive
|
||||
private BigDecimal originalCommission;
|
||||
|
||||
/** 상품 분류. 분류별로 적용 환수율 구간(chargeback_grade)이 다르다. */
|
||||
private String productCategory;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 차등환수 등급(chargeback_grade) 관리 REST 컨트롤러.
|
||||
* 환수 등급표는 "경과개월 구간별 환수율(%)"을 정의하는 기준표이며, 조기해지 시 환수액 계산에 쓰인다.
|
||||
* - gradeService : 환수 등급표 CRUD + 비활성화
|
||||
* - calculator : 실제 계약 정보로 환수액을 계산(시뮬레이션)
|
||||
*/
|
||||
@Tag(name = "환수 등급 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/chargeback-grades")
|
||||
@@ -24,18 +30,21 @@ 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")
|
||||
@@ -44,6 +53,7 @@ public class ChargebackGradeController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 환수 등급 수정. */
|
||||
@PutMapping("/{gradeId}")
|
||||
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CHARGEBACK_GRADE", table = "chargeback_grade")
|
||||
@@ -53,6 +63,7 @@ public class ChargebackGradeController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 환수 등급 비활성화(사용 중지). 비활성 등급은 환수액 계산에서 제외된다. */
|
||||
@PutMapping("/{gradeId}/deactivate")
|
||||
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CHARGEBACK_GRADE", table = "chargeback_grade")
|
||||
@@ -61,6 +72,7 @@ public class ChargebackGradeController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 계약일·해지일·원수수료를 입력받아 환수액을 계산(시뮬레이션)한다. */
|
||||
@PostMapping("/calculate")
|
||||
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "READ")
|
||||
public ApiResponse<ChargebackResult> calculate(@Valid @RequestBody ChargebackCalculateReq req) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.AdvancePaymentService;
|
||||
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.commission.AdvancePaymentResp;
|
||||
import com.ga.core.vo.commission.AdvancePaymentSaveReq;
|
||||
import com.ga.core.vo.commission.AdvancePaymentSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 선지급정산 API.
|
||||
*
|
||||
* 선지급(advance payment): 아직 확정되지 않은 미래 수수료를 설계사에게 미리 지급(가불)하는 것.
|
||||
* 이후 실제 수수료가 발생하면 그 금액에서 차감(상계)하여 회수한다.
|
||||
* - principalAmount(선지급 원금) / recoveredAmount(회수 누계) / balance(미상환 잔액)
|
||||
* - status: OUTSTANDING(미상환) → PARTIAL(일부회수) → CLEARED(완전회수) 또는 WAIVED(면제)
|
||||
*
|
||||
* 권한: @RequirePermission(menu="ADVANCE_PAYMENT"), perm=READ/CREATE/UPDATE. 변경은 @DataChangeLog 로 감사 기록.
|
||||
*/
|
||||
@Tag(name = "선지급정산")
|
||||
@RestController
|
||||
@RequestMapping("/api/advance-payments")
|
||||
@RequiredArgsConstructor
|
||||
public class AdvancePaymentController {
|
||||
|
||||
private final AdvancePaymentService service;
|
||||
|
||||
/** 선지급 목록 조회(검색 + 페이징). 권한: ADVANCE_PAYMENT READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "READ")
|
||||
public ApiResponse<PageResponse<AdvancePaymentResp>> list(
|
||||
@ModelAttribute AdvancePaymentSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 선지급 단건 상세 조회. 권한: ADVANCE_PAYMENT READ. */
|
||||
@GetMapping("/{advanceId}")
|
||||
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "READ")
|
||||
public ApiResponse<AdvancePaymentResp> detail(@PathVariable Long advanceId) {
|
||||
return ApiResponse.ok(service.detail(advanceId));
|
||||
}
|
||||
|
||||
/** 선지급 신규 등록. 등록 시 잔액=원금, 상태=OUTSTANDING. 응답: 생성된 advanceId. 권한: ADVANCE_PAYMENT CREATE. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "ADVANCE_PAYMENT", table = "advance_payment")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody AdvancePaymentSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 선지급 면제 처리. 남은 잔액 회수를 포기(상태=WAIVED, 잔액=0). 권한: ADVANCE_PAYMENT UPDATE. */
|
||||
@PostMapping("/{advanceId}/waive")
|
||||
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "ADVANCE_PAYMENT", table = "advance_payment")
|
||||
public ApiResponse<Void> waive(@PathVariable Long advanceId) {
|
||||
service.waive(advanceId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 시상(award) 원장 환수 처리 요청 바디 (PUT /api/awards/ledgers/{id}/clawback).
|
||||
*
|
||||
* 환수(clawback): 이미 지급한 시상금을 일정 사유로 되돌려 받는 것.
|
||||
* clawbackAmount(환수 금액, 0 이상 필수) + reason(환수 사유).
|
||||
*/
|
||||
@Data
|
||||
public class AwardClawbackReq {
|
||||
|
||||
/** 환수 금액. 0 이상이어야 하며(@DecimalMin("0")) 필수값(@NotNull). */
|
||||
@NotNull
|
||||
@DecimalMin("0")
|
||||
private BigDecimal clawbackAmount;
|
||||
|
||||
/** 환수 사유(선택). */
|
||||
private String reason;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.AwardService;
|
||||
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.commission.*;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 시상(award) API.
|
||||
*
|
||||
* 시상: 일정 실적 조건을 달성한 설계사에게 지급하는 포상금/인센티브.
|
||||
* - Rule(시상 규칙): 어떤 조건일 때 얼마를 지급하는지 정의(생성/수정/삭제 가능).
|
||||
* - Ledger(시상 원장): 규칙에 따라 실제 산출/지급된 시상금 내역. clawback(환수)로 회수 가능.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="AWARD"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 로 감사 기록.
|
||||
*/
|
||||
@Tag(name = "시상")
|
||||
@RestController
|
||||
@RequestMapping("/api/awards")
|
||||
@RequiredArgsConstructor
|
||||
public class AwardController {
|
||||
|
||||
private final AwardService service;
|
||||
|
||||
// ── Rule (시상 규칙) ────────────────────────────────────────────────────
|
||||
|
||||
/** 시상 규칙 목록 조회. 권한: AWARD READ. */
|
||||
@GetMapping("/rules")
|
||||
@RequirePermission(menu = "AWARD", perm = "READ")
|
||||
public ApiResponse<PageResponse<AwardRuleResp>> listRules(
|
||||
@ModelAttribute AwardRuleSearchParam param) {
|
||||
return ApiResponse.ok(service.listRules(param));
|
||||
}
|
||||
|
||||
/** 시상 규칙 단건 상세. 권한: AWARD READ. */
|
||||
@GetMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "AWARD", perm = "READ")
|
||||
public ApiResponse<AwardRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||
return ApiResponse.ok(service.detailRule(ruleId));
|
||||
}
|
||||
|
||||
/** 시상 규칙 신규 등록. 응답: 생성된 ruleId. 권한: AWARD CREATE. */
|
||||
@PostMapping("/rules")
|
||||
@RequirePermission(menu = "AWARD", perm = "CREATE")
|
||||
@DataChangeLog(menu = "AWARD", table = "award_rule")
|
||||
public ApiResponse<Long> createRule(@Valid @RequestBody AwardRuleSaveReq req) {
|
||||
return ApiResponse.ok(service.createRule(req));
|
||||
}
|
||||
|
||||
/** 시상 규칙 수정. 권한: AWARD UPDATE. */
|
||||
@PutMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "AWARD", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "AWARD", table = "award_rule")
|
||||
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
|
||||
@Valid @RequestBody AwardRuleSaveReq req) {
|
||||
service.updateRule(ruleId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 시상 규칙 삭제. 권한: AWARD DELETE. */
|
||||
@DeleteMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "AWARD", perm = "DELETE")
|
||||
@DataChangeLog(menu = "AWARD", table = "award_rule")
|
||||
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
|
||||
service.deleteRule(ruleId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ── Ledger (시상 원장) ──────────────────────────────────────────────────
|
||||
|
||||
/** 시상 원장 목록 조회. 권한: AWARD READ. */
|
||||
@GetMapping("/ledgers")
|
||||
@RequirePermission(menu = "AWARD", perm = "READ")
|
||||
public ApiResponse<PageResponse<AwardLedgerResp>> listLedgers(
|
||||
@ModelAttribute AwardLedgerSearchParam param) {
|
||||
return ApiResponse.ok(service.listLedgers(param));
|
||||
}
|
||||
|
||||
/** 시상 원장 단건 상세. 권한: AWARD READ. */
|
||||
@GetMapping("/ledgers/{ledgerId}")
|
||||
@RequirePermission(menu = "AWARD", perm = "READ")
|
||||
public ApiResponse<AwardLedgerResp> detailLedger(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(service.detailLedger(ledgerId));
|
||||
}
|
||||
|
||||
/** 시상 원장 신규 등록(시상금 산출/지급 기록). 응답: 생성된 ledgerId. 권한: AWARD CREATE. */
|
||||
@PostMapping("/ledgers")
|
||||
@RequirePermission(menu = "AWARD", perm = "CREATE")
|
||||
@DataChangeLog(menu = "AWARD", table = "award_ledger")
|
||||
public ApiResponse<Long> createLedger(@Valid @RequestBody AwardLedgerSaveReq req) {
|
||||
return ApiResponse.ok(service.createLedger(req));
|
||||
}
|
||||
|
||||
/** 시상금 환수(clawback) 적용. 이미 지급한 시상금을 일부/전부 회수한다. 권한: AWARD UPDATE. */
|
||||
@PutMapping("/ledgers/{ledgerId}/clawback")
|
||||
@RequirePermission(menu = "AWARD", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "AWARD", table = "award_ledger")
|
||||
public ApiResponse<Void> applyClawback(@PathVariable Long ledgerId,
|
||||
@Valid @RequestBody AwardClawbackReq req) {
|
||||
service.applyClawback(ledgerId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.BranchAllowanceService;
|
||||
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.commission.*;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 지점장수당 API.
|
||||
*
|
||||
* 지점장수당(branch manager allowance): 지점에 소속된 설계사들의 실적 합계에 따라
|
||||
* 지점장에게 추가로 지급하는 수당. 상한(cap)이 적용될 수 있다.
|
||||
* - Rule(수당 규칙): 지점 실적 대비 지급률/상한 정의. - Ledger(수당 원장): 실제 산출/지급 내역.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="BRANCH_MGR_ALLOWANCE"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "지점장수당")
|
||||
@RestController
|
||||
@RequestMapping("/api/branch-allowances")
|
||||
@RequiredArgsConstructor
|
||||
public class BranchAllowanceController {
|
||||
|
||||
private final BranchAllowanceService service;
|
||||
|
||||
// ── Rule (수당 규칙) ────────────────────────────────────────────────────
|
||||
|
||||
/** 지점장수당 규칙 목록 조회. 권한: BRANCH_MGR_ALLOWANCE READ. */
|
||||
@GetMapping("/rules")
|
||||
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ")
|
||||
public ApiResponse<PageResponse<BranchMgrAllowanceRuleResp>> listRules(
|
||||
@ModelAttribute BranchMgrAllowanceRuleSearchParam param) {
|
||||
return ApiResponse.ok(service.listRules(param));
|
||||
}
|
||||
|
||||
@GetMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ")
|
||||
public ApiResponse<BranchMgrAllowanceRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||
return ApiResponse.ok(service.detailRule(ruleId));
|
||||
}
|
||||
|
||||
/** 지점장수당 규칙 신규 등록. 응답: ruleId. 권한: BRANCH_MGR_ALLOWANCE CREATE. */
|
||||
@PostMapping("/rules")
|
||||
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "CREATE")
|
||||
@DataChangeLog(menu = "BRANCH_MGR_ALLOWANCE", table = "branch_mgr_allowance_rule")
|
||||
public ApiResponse<Long> createRule(@Valid @RequestBody BranchMgrAllowanceRuleSaveReq req) {
|
||||
return ApiResponse.ok(service.createRule(req));
|
||||
}
|
||||
|
||||
/** 지점장수당 규칙 수정. 권한: BRANCH_MGR_ALLOWANCE UPDATE. */
|
||||
@PutMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "BRANCH_MGR_ALLOWANCE", table = "branch_mgr_allowance_rule")
|
||||
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
|
||||
@Valid @RequestBody BranchMgrAllowanceRuleSaveReq req) {
|
||||
service.updateRule(ruleId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 지점장수당 규칙 삭제. 권한: BRANCH_MGR_ALLOWANCE DELETE. */
|
||||
@DeleteMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "DELETE")
|
||||
@DataChangeLog(menu = "BRANCH_MGR_ALLOWANCE", table = "branch_mgr_allowance_rule")
|
||||
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
|
||||
service.deleteRule(ruleId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ── Ledger (수당 원장) ──────────────────────────────────────────────────
|
||||
|
||||
/** 지점장수당 원장 목록 조회. 권한: BRANCH_MGR_ALLOWANCE READ. */
|
||||
@GetMapping("/ledgers")
|
||||
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ")
|
||||
public ApiResponse<PageResponse<BranchMgrAllowanceLedgerResp>> listLedgers(
|
||||
@ModelAttribute BranchMgrAllowanceLedgerSearchParam param) {
|
||||
return ApiResponse.ok(service.listLedgers(param));
|
||||
}
|
||||
|
||||
/** 지점장수당 원장 단건 상세. 권한: BRANCH_MGR_ALLOWANCE READ. */
|
||||
@GetMapping("/ledgers/{ledgerId}")
|
||||
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ")
|
||||
public ApiResponse<BranchMgrAllowanceLedgerResp> detailLedger(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(service.detailLedger(ledgerId));
|
||||
}
|
||||
|
||||
/** 지점장수당 원장 신규 등록(수당 산출/지급 기록). 응답: ledgerId. 권한: BRANCH_MGR_ALLOWANCE CREATE. */
|
||||
@PostMapping("/ledgers")
|
||||
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "CREATE")
|
||||
@DataChangeLog(menu = "BRANCH_MGR_ALLOWANCE", table = "branch_mgr_allowance_ledger")
|
||||
public ApiResponse<Long> createLedger(@Valid @RequestBody BranchMgrAllowanceLedgerSaveReq req) {
|
||||
return ApiResponse.ok(service.createLedger(req));
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.CarrierReconciliationInService;
|
||||
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.commission.CarrierReconciliationInResp;
|
||||
import com.ga.core.vo.commission.CarrierReconciliationInSaveReq;
|
||||
import com.ga.core.vo.commission.CarrierReconciliationInSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 정산대사(입수) API.
|
||||
*
|
||||
* 정산대사(reconciliation): 보험사로부터 받은 정산 명세(입수 자료)와 우리 시스템이 계산한 금액을
|
||||
* 서로 맞춰 보아(대사) 차이를 확인/확정하는 작업.
|
||||
* - reconcile: 대사 실행(차이 비교) - approve: 대사 결과 승인(확정)
|
||||
*
|
||||
* 권한: @RequirePermission(menu="CARRIER_RECON_IN"), perm=READ/CREATE/UPDATE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "정산대사입수")
|
||||
@RestController
|
||||
@RequestMapping("/api/carrier-recon-in")
|
||||
@RequiredArgsConstructor
|
||||
public class CarrierReconciliationInController {
|
||||
|
||||
private final CarrierReconciliationInService service;
|
||||
|
||||
/** 대사 입수 자료 목록 조회. 권한: CARRIER_RECON_IN READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "READ")
|
||||
public ApiResponse<PageResponse<CarrierReconciliationInResp>> list(
|
||||
@ModelAttribute CarrierReconciliationInSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 대사 입수 단건 상세. 권한: CARRIER_RECON_IN READ. */
|
||||
@GetMapping("/{reconId}")
|
||||
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "READ")
|
||||
public ApiResponse<CarrierReconciliationInResp> detail(@PathVariable Long reconId) {
|
||||
return ApiResponse.ok(service.detail(reconId));
|
||||
}
|
||||
|
||||
/** 대사 입수 자료 신규 등록. 응답: reconId. 권한: CARRIER_RECON_IN CREATE. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "CREATE")
|
||||
@DataChangeLog(menu = "CARRIER_RECON_IN", table = "carrier_reconciliation_in")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody CarrierReconciliationInSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 대사 실행. 보험사 명세와 시스템 계산액을 비교해 차이를 계산한다. 권한: CARRIER_RECON_IN UPDATE. */
|
||||
@PostMapping("/{reconId}/reconcile")
|
||||
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CARRIER_RECON_IN", table = "carrier_reconciliation_in")
|
||||
public ApiResponse<Void> reconcile(@PathVariable Long reconId) {
|
||||
service.reconcile(reconId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 대사 결과 승인(확정). 권한: CARRIER_RECON_IN UPDATE. */
|
||||
@PostMapping("/{reconId}/approve")
|
||||
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CARRIER_RECON_IN", table = "carrier_reconciliation_in")
|
||||
public ApiResponse<Void> approve(@PathVariable Long reconId) {
|
||||
service.approve(reconId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.ChargebackWaiverService;
|
||||
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.commission.ChargebackWaiverRequestResp;
|
||||
import com.ga.core.vo.commission.ChargebackWaiverRequestSaveReq;
|
||||
import com.ga.core.vo.commission.ChargebackWaiverRequestSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 환수감면신청 API.
|
||||
*
|
||||
* 환수(chargeback): 계약 해지/실효 등으로 이미 지급한 수수료를 설계사로부터 다시 회수하는 것.
|
||||
* 환수감면(waiver): 특별한 사유가 인정되면 그 환수 금액을 줄이거나 면제해 달라고 신청하는 것.
|
||||
* - approve: 감면 신청 승인 - reject: 감면 신청 반려
|
||||
*
|
||||
* 권한: @RequirePermission(menu="CHARGEBACK_WAIVER"), perm=READ/CREATE/UPDATE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "환수감면신청")
|
||||
@RestController
|
||||
@RequestMapping("/api/chargeback-waivers")
|
||||
@RequiredArgsConstructor
|
||||
public class ChargebackWaiverController {
|
||||
|
||||
private final ChargebackWaiverService service;
|
||||
|
||||
/** 환수감면 신청 목록 조회. 권한: CHARGEBACK_WAIVER READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "READ")
|
||||
public ApiResponse<PageResponse<ChargebackWaiverRequestResp>> list(
|
||||
@ModelAttribute ChargebackWaiverRequestSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 환수감면 신청 단건 상세. 권한: CHARGEBACK_WAIVER READ. */
|
||||
@GetMapping("/{requestId}")
|
||||
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "READ")
|
||||
public ApiResponse<ChargebackWaiverRequestResp> detail(@PathVariable Long requestId) {
|
||||
return ApiResponse.ok(service.detail(requestId));
|
||||
}
|
||||
|
||||
/** 환수감면 신청 등록. 응답: requestId. 권한: CHARGEBACK_WAIVER CREATE. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "CREATE")
|
||||
@DataChangeLog(menu = "CHARGEBACK_WAIVER", table = "chargeback_waiver_request")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody ChargebackWaiverRequestSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 환수감면 신청 승인. 권한: CHARGEBACK_WAIVER UPDATE. */
|
||||
@PostMapping("/{requestId}/approve")
|
||||
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CHARGEBACK_WAIVER", table = "chargeback_waiver_request")
|
||||
public ApiResponse<Void> approve(@PathVariable Long requestId) {
|
||||
service.approve(requestId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 환수감면 신청 반려. 권한: CHARGEBACK_WAIVER UPDATE. */
|
||||
@PostMapping("/{requestId}/reject")
|
||||
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CHARGEBACK_WAIVER", table = "chargeback_waiver_request")
|
||||
public ApiResponse<Void> reject(@PathVariable Long requestId) {
|
||||
service.reject(requestId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.ClawbackInstallmentService;
|
||||
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.commission.ClawbackInstallmentPlanResp;
|
||||
import com.ga.core.vo.commission.ClawbackInstallmentPlanSearchParam;
|
||||
import com.ga.core.vo.commission.ClawbackInstallmentPlanVO;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 환수분할상환 API.
|
||||
*
|
||||
* 환수채권(받아야 할 환수 금액)을 한 번에 못 갚을 때, 여러 달에 나눠 갚도록(분할상환) 계획을 만든다.
|
||||
* - generate: 환수채권(receivableId) 하나를 N개월(count)로 나눠 baseMonth 부터 매월 상환 계획을 생성
|
||||
* - cancel: 특정 회차 계획 취소
|
||||
*
|
||||
* 권한: @RequirePermission(menu="CLAWBACK_INSTALLMENT"), perm=READ/CREATE/UPDATE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "환수분할상환")
|
||||
@RestController
|
||||
@RequestMapping("/api/clawback-installments")
|
||||
@RequiredArgsConstructor
|
||||
public class ClawbackInstallmentController {
|
||||
|
||||
private final ClawbackInstallmentService service;
|
||||
|
||||
/** 분할상환 계획 목록 조회. 권한: CLAWBACK_INSTALLMENT READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "READ")
|
||||
public ApiResponse<PageResponse<ClawbackInstallmentPlanResp>> list(
|
||||
@ModelAttribute ClawbackInstallmentPlanSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 분할상환 계획 단건 상세. 권한: CLAWBACK_INSTALLMENT READ. */
|
||||
@GetMapping("/{planId}")
|
||||
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "READ")
|
||||
public ApiResponse<ClawbackInstallmentPlanResp> detail(@PathVariable Long planId) {
|
||||
return ApiResponse.ok(service.detail(planId));
|
||||
}
|
||||
|
||||
/** 특정 환수채권에 묶인 분할상환 회차 전체 조회. 권한: CLAWBACK_INSTALLMENT READ. */
|
||||
@GetMapping("/by-receivable/{receivableId}")
|
||||
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "READ")
|
||||
public ApiResponse<List<ClawbackInstallmentPlanVO>> listByReceivable(
|
||||
@PathVariable Long receivableId) {
|
||||
return ApiResponse.ok(service.listByReceivable(receivableId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 분할상환 계획 생성. 환수채권을 N개월로 균등 분할해 baseMonth 부터 회차를 만든다.
|
||||
* Request body: { "count": N(분할 개월 수), "baseMonth": "YYYYMM"(첫 회차 월) }
|
||||
* 권한: CLAWBACK_INSTALLMENT CREATE.
|
||||
*/
|
||||
@PostMapping("/generate/{receivableId}")
|
||||
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "CLAWBACK_INSTALLMENT", table = "clawback_installment_plan")
|
||||
public ApiResponse<Void> generate(@PathVariable Long receivableId,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
// body 의 count/baseMonth 를 꺼내 서비스로 전달 (JSON 값은 문자열로 변환 후 파싱)
|
||||
int count = Integer.parseInt(body.get("count").toString());
|
||||
String baseMonth = body.get("baseMonth").toString();
|
||||
service.generate(receivableId, count, baseMonth);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 분할상환 회차 취소. 권한: CLAWBACK_INSTALLMENT UPDATE. */
|
||||
@PostMapping("/{planId}/cancel")
|
||||
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CLAWBACK_INSTALLMENT", table = "clawback_installment_plan")
|
||||
public ApiResponse<Void> cancel(@PathVariable Long planId) {
|
||||
service.cancel(planId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.ClawbackReceivableService;
|
||||
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.common.util.SecurityUtil;
|
||||
import com.ga.core.vo.commission.ClawbackReceivableResp;
|
||||
import com.ga.core.vo.commission.ClawbackReceivableSaveReq;
|
||||
import com.ga.core.vo.commission.ClawbackReceivableSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 환수채권 관리 API.
|
||||
*
|
||||
* 환수채권(clawback receivable): 환수가 확정되어 설계사로부터 "받아야 할 돈"(미수금).
|
||||
* 발생액(original) 중 회수액(recovered)을 빼면 잔액(balance)이 남는다.
|
||||
* - write-off(대손상각): 회수가 불가능하다고 판단된 잔액을 손실로 털어내는 처리.
|
||||
* - summary: 상태/설계사별 집계, export: 엑셀 다운로드.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="CLAWBACK_RECEIVABLE"), perm=READ/UPDATE/EXPORT. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "환수채권 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/clawback-receivables")
|
||||
@RequiredArgsConstructor
|
||||
public class ClawbackReceivableController {
|
||||
|
||||
private final ClawbackReceivableService service;
|
||||
|
||||
/** 환수채권 목록 조회(검색 + 페이징). 권한: CLAWBACK_RECEIVABLE READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "CLAWBACK_RECEIVABLE", perm = "READ")
|
||||
public ApiResponse<PageResponse<ClawbackReceivableResp>> list(
|
||||
@ModelAttribute ClawbackReceivableSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 환수채권 집계 요약(상태/그룹별 합계). 권한: CLAWBACK_RECEIVABLE READ. */
|
||||
@GetMapping("/summary")
|
||||
@RequirePermission(menu = "CLAWBACK_RECEIVABLE", perm = "READ")
|
||||
public ApiResponse<List<Map<String, Object>>> summary(
|
||||
@ModelAttribute ClawbackReceivableSearchParam param) {
|
||||
return ApiResponse.ok(service.summary(param));
|
||||
}
|
||||
|
||||
/** 환수채권 단건 상세. 권한: CLAWBACK_RECEIVABLE READ. */
|
||||
@GetMapping("/{id}")
|
||||
@RequirePermission(menu = "CLAWBACK_RECEIVABLE", perm = "READ")
|
||||
public ApiResponse<ClawbackReceivableResp> detail(@PathVariable Long id) {
|
||||
return ApiResponse.ok(service.detail(id));
|
||||
}
|
||||
|
||||
/** 대손상각(write-off) 처리. 잔액을 손실 처리한다. 처리자(현재 로그인 사용자 ID)를 함께 기록. 권한: CLAWBACK_RECEIVABLE UPDATE. */
|
||||
@PutMapping("/{id}/write-off")
|
||||
@RequirePermission(menu = "CLAWBACK_RECEIVABLE", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CLAWBACK_RECEIVABLE", table = "clawback_receivable")
|
||||
public ApiResponse<Void> writeOff(@PathVariable Long id,
|
||||
@Valid @RequestBody ClawbackReceivableSaveReq req) {
|
||||
service.writeOff(id, req, SecurityUtil.getCurrentUserId());
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 환수채권 목록 엑셀 다운로드. 응답 스트림(response)에 직접 파일을 쓴다. 권한: CLAWBACK_RECEIVABLE EXPORT. */
|
||||
@GetMapping("/export")
|
||||
@RequirePermission(menu = "CLAWBACK_RECEIVABLE", perm = "EXPORT")
|
||||
public void export(@ModelAttribute ClawbackReceivableSearchParam param,
|
||||
HttpServletResponse response) {
|
||||
service.export(param, response);
|
||||
}
|
||||
}
|
||||
+18
-2
@@ -11,6 +11,14 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 수금수수료 API.
|
||||
*
|
||||
* 수금수수료(collection commission): 설계사가 보험료를 정상적으로 수금(납입 유도/관리)한 것에 대해
|
||||
* 지급하는 수수료. 규칙(Rule)에 따라 산출되어 원장(Ledger)에 기록된다.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="COLLECT_COMM"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "수금수수료")
|
||||
@RestController
|
||||
@RequestMapping("/api/collection-commissions")
|
||||
@@ -19,8 +27,9 @@ public class CollectionCommissionController {
|
||||
|
||||
private final CollectionCommissionService service;
|
||||
|
||||
// ── Rule ──────────────────────────────────────────────────────────────────
|
||||
// ── Rule (수금수수료 규칙) ──────────────────────────────────────────────
|
||||
|
||||
/** 수금수수료 규칙 목록 조회. 권한: COLLECT_COMM READ. */
|
||||
@GetMapping("/rules")
|
||||
@RequirePermission(menu = "COLLECT_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<CollectionCommissionRuleResp>> listRules(
|
||||
@@ -28,12 +37,14 @@ public class CollectionCommissionController {
|
||||
return ApiResponse.ok(service.listRules(param));
|
||||
}
|
||||
|
||||
/** 수금수수료 규칙 단건 상세. 권한: COLLECT_COMM READ. */
|
||||
@GetMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "COLLECT_COMM", perm = "READ")
|
||||
public ApiResponse<CollectionCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||
return ApiResponse.ok(service.detailRule(ruleId));
|
||||
}
|
||||
|
||||
/** 수금수수료 규칙 신규 등록. 응답: ruleId. 권한: COLLECT_COMM CREATE. */
|
||||
@PostMapping("/rules")
|
||||
@RequirePermission(menu = "COLLECT_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "COLLECT_COMM", table = "collection_commission_rule")
|
||||
@@ -41,6 +52,7 @@ public class CollectionCommissionController {
|
||||
return ApiResponse.ok(service.createRule(req));
|
||||
}
|
||||
|
||||
/** 수금수수료 규칙 수정. 권한: COLLECT_COMM UPDATE. */
|
||||
@PutMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "COLLECT_COMM", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "COLLECT_COMM", table = "collection_commission_rule")
|
||||
@@ -50,6 +62,7 @@ public class CollectionCommissionController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 수금수수료 규칙 삭제. 권한: COLLECT_COMM DELETE. */
|
||||
@DeleteMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "COLLECT_COMM", perm = "DELETE")
|
||||
@DataChangeLog(menu = "COLLECT_COMM", table = "collection_commission_rule")
|
||||
@@ -58,8 +71,9 @@ public class CollectionCommissionController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ── Ledger ────────────────────────────────────────────────────────────────
|
||||
// ── Ledger (수금수수료 원장) ────────────────────────────────────────────
|
||||
|
||||
/** 수금수수료 원장 목록 조회. 권한: COLLECT_COMM READ. */
|
||||
@GetMapping("/ledgers")
|
||||
@RequirePermission(menu = "COLLECT_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<CollectionCommissionLedgerResp>> listLedgers(
|
||||
@@ -67,12 +81,14 @@ public class CollectionCommissionController {
|
||||
return ApiResponse.ok(service.listLedgers(param));
|
||||
}
|
||||
|
||||
/** 수금수수료 원장 단건 상세. 권한: COLLECT_COMM READ. */
|
||||
@GetMapping("/ledgers/{ledgerId}")
|
||||
@RequirePermission(menu = "COLLECT_COMM", perm = "READ")
|
||||
public ApiResponse<CollectionCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(service.detailLedger(ledgerId));
|
||||
}
|
||||
|
||||
/** 수금수수료 원장 신규 등록. 응답: ledgerId. 권한: COLLECT_COMM CREATE. */
|
||||
@PostMapping("/ledgers")
|
||||
@RequirePermission(menu = "COLLECT_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "COLLECT_COMM", table = "collection_commission_ledger")
|
||||
|
||||
@@ -14,6 +14,15 @@ import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 수수료 마스터 API.
|
||||
*
|
||||
* 수수료 마스터(commission master): 설계사·정산월 단위로 각종 수수료(도입/소개/유지/시상 등)를
|
||||
* 한데 모아 최종 지급액을 관리하는 핵심 원장. 여러 세부 원장이 이 마스터로 집계된다.
|
||||
* - by-agent: 설계사+정산월의 수수료들, aggregated: 정산월 전체 집계, status: 정산 상태 변경(확정/지급 등).
|
||||
*
|
||||
* 권한: @RequirePermission(menu="COMM_MASTER"), perm=READ/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "수수료 마스터")
|
||||
@RestController
|
||||
@RequestMapping("/api/commissions")
|
||||
@@ -22,18 +31,21 @@ public class CommissionMasterController {
|
||||
|
||||
private final CommissionMasterService service;
|
||||
|
||||
/** 수수료 마스터 목록 조회(검색 + 페이징). 권한: COMM_MASTER READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "COMM_MASTER", perm = "READ")
|
||||
public ApiResponse<PageResponse<CommissionMasterResp>> list(@ModelAttribute CommissionMasterSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 수수료 마스터 단건 상세. 권한: COMM_MASTER READ. */
|
||||
@GetMapping("/{masterId}")
|
||||
@RequirePermission(menu = "COMM_MASTER", perm = "READ")
|
||||
public ApiResponse<CommissionMasterResp> detail(@PathVariable Long masterId) {
|
||||
return ApiResponse.ok(service.detail(masterId));
|
||||
}
|
||||
|
||||
/** 특정 설계사(agentId)의 특정 정산월(settleMonth) 수수료 내역 조회. 권한: COMM_MASTER READ. */
|
||||
@GetMapping("/by-agent")
|
||||
@RequirePermission(menu = "COMM_MASTER", perm = "READ")
|
||||
public ApiResponse<List<CommissionMasterResp>> byAgent(@RequestParam Long agentId,
|
||||
@@ -41,12 +53,14 @@ public class CommissionMasterController {
|
||||
return ApiResponse.ok(service.listByAgentAndMonth(agentId, settleMonth));
|
||||
}
|
||||
|
||||
/** 정산월 단위 전체 집계 조회. 권한: COMM_MASTER READ. */
|
||||
@GetMapping("/aggregated")
|
||||
@RequirePermission(menu = "COMM_MASTER", perm = "READ")
|
||||
public ApiResponse<List<CommissionMasterResp>> aggregated(@RequestParam String settleMonth) {
|
||||
return ApiResponse.ok(service.aggregatedByMonth(settleMonth));
|
||||
}
|
||||
|
||||
/** 정산 상태 변경(예: 확정/지급 등). body의 "status" 값으로 갱신. 권한: COMM_MASTER UPDATE. */
|
||||
@PutMapping("/{masterId}/status")
|
||||
@RequirePermission(menu = "COMM_MASTER", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "COMM_MASTER", table = "commission_master")
|
||||
@@ -56,6 +70,7 @@ public class CommissionMasterController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 수수료 마스터 삭제. 권한: COMM_MASTER DELETE. */
|
||||
@DeleteMapping("/{masterId}")
|
||||
@RequirePermission(menu = "COMM_MASTER", perm = "DELETE")
|
||||
@DataChangeLog(menu = "COMM_MASTER", table = "commission_master")
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.CommissionSimulationService;
|
||||
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.commission.CommissionSimulationResp;
|
||||
import com.ga.core.vo.commission.CommissionSimulationSaveReq;
|
||||
import com.ga.core.vo.commission.CommissionSimulationSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 수수료 시뮬레이터 API.
|
||||
*
|
||||
* 시뮬레이션(simulation): 실제 정산을 확정하기 전에 "이 조건이면 수수료가 얼마 나올까"를 미리 계산해 보는 기능.
|
||||
* 결과는 저장되어 이력으로 조회할 수 있다(실제 지급에는 영향 없음).
|
||||
*
|
||||
* 권한: @RequirePermission(menu="COMM_SIMULATOR"), perm=READ/CREATE.
|
||||
*/
|
||||
@Tag(name = "수수료 시뮬레이터")
|
||||
@RestController
|
||||
@RequestMapping("/api/commission-simulations")
|
||||
@RequiredArgsConstructor
|
||||
public class CommissionSimulationController {
|
||||
|
||||
private final CommissionSimulationService service;
|
||||
|
||||
/** 시뮬레이션 이력 목록 조회. 권한: COMM_SIMULATOR READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "COMM_SIMULATOR", perm = "READ")
|
||||
public ApiResponse<PageResponse<CommissionSimulationResp>> list(
|
||||
@ModelAttribute CommissionSimulationSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 시뮬레이션 단건 상세. 권한: COMM_SIMULATOR READ. */
|
||||
@GetMapping("/{simId}")
|
||||
@RequirePermission(menu = "COMM_SIMULATOR", perm = "READ")
|
||||
public ApiResponse<CommissionSimulationResp> detail(@PathVariable Long simId) {
|
||||
return ApiResponse.ok(service.detail(simId));
|
||||
}
|
||||
|
||||
/** 시뮬레이션 실행. 요청 조건으로 예상 수수료를 계산해 결과를 반환(및 이력 저장). 권한: COMM_SIMULATOR CREATE. */
|
||||
@PostMapping("/simulate")
|
||||
@RequirePermission(menu = "COMM_SIMULATOR", perm = "CREATE")
|
||||
@DataChangeLog(menu = "COMM_SIMULATOR", table = "commission_simulation")
|
||||
public ApiResponse<CommissionSimulationResp> simulate(@Valid @RequestBody CommissionSimulationSaveReq req) {
|
||||
return ApiResponse.ok(service.simulate(req));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.GradeEvaluationService;
|
||||
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.commission.AgentGradeEvaluationResp;
|
||||
import com.ga.core.vo.commission.AgentGradeEvaluationSaveReq;
|
||||
import com.ga.core.vo.commission.AgentGradeEvaluationSearchParam;
|
||||
import com.ga.core.vo.commission.GradeEvalRuleResp;
|
||||
import com.ga.core.vo.commission.GradeEvalRuleSaveReq;
|
||||
import com.ga.core.vo.commission.GradeEvalRuleSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 등급평가 API.
|
||||
*
|
||||
* 등급평가(grade evaluation): 설계사의 실적 등을 기준 규칙(Rule)에 따라 평가해 등급을 매기는 기능.
|
||||
* 등급은 수수료율·시상 등에 영향을 준다.
|
||||
* - Rule(평가 규칙) CRUD - Evaluation(평가 결과): 생성 후 apply(적용)하면 실제 설계사 등급에 반영된다.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="GRADE_EVAL"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "등급평가")
|
||||
@RestController
|
||||
@RequestMapping("/api/grade-evaluations")
|
||||
@RequiredArgsConstructor
|
||||
public class GradeEvaluationController {
|
||||
|
||||
private final GradeEvaluationService service;
|
||||
|
||||
// ── Rule (평가 규칙) ────────────────────────────────────────────────────
|
||||
|
||||
/** 등급평가 규칙 목록 조회. 권한: GRADE_EVAL READ. */
|
||||
@GetMapping("/rules")
|
||||
@RequirePermission(menu = "GRADE_EVAL", perm = "READ")
|
||||
public ApiResponse<PageResponse<GradeEvalRuleResp>> listRules(@ModelAttribute GradeEvalRuleSearchParam param) {
|
||||
return ApiResponse.ok(service.listRules(param));
|
||||
}
|
||||
|
||||
/** 등급평가 규칙 단건 상세. 권한: GRADE_EVAL READ. */
|
||||
@GetMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "GRADE_EVAL", perm = "READ")
|
||||
public ApiResponse<GradeEvalRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||
return ApiResponse.ok(service.detailRule(ruleId));
|
||||
}
|
||||
|
||||
/** 등급평가 규칙 신규 등록. 응답: ruleId. 권한: GRADE_EVAL CREATE. */
|
||||
@PostMapping("/rules")
|
||||
@RequirePermission(menu = "GRADE_EVAL", perm = "CREATE")
|
||||
@DataChangeLog(menu = "GRADE_EVAL", table = "grade_eval_rule")
|
||||
public ApiResponse<Long> createRule(@Valid @RequestBody GradeEvalRuleSaveReq req) {
|
||||
return ApiResponse.ok(service.createRule(req));
|
||||
}
|
||||
|
||||
/** 등급평가 규칙 수정. 권한: GRADE_EVAL UPDATE. */
|
||||
@PutMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "GRADE_EVAL", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "GRADE_EVAL", table = "grade_eval_rule")
|
||||
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
|
||||
@Valid @RequestBody GradeEvalRuleSaveReq req) {
|
||||
service.updateRule(ruleId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 등급평가 규칙 삭제. 권한: GRADE_EVAL DELETE. */
|
||||
@DeleteMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "GRADE_EVAL", perm = "DELETE")
|
||||
@DataChangeLog(menu = "GRADE_EVAL", table = "grade_eval_rule")
|
||||
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
|
||||
service.deleteRule(ruleId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ── Evaluation (평가 결과) ──────────────────────────────────────────────
|
||||
|
||||
/** 등급평가 결과 목록 조회. 권한: GRADE_EVAL READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "GRADE_EVAL", perm = "READ")
|
||||
public ApiResponse<PageResponse<AgentGradeEvaluationResp>> listEvals(
|
||||
@ModelAttribute AgentGradeEvaluationSearchParam param) {
|
||||
return ApiResponse.ok(service.listEvals(param));
|
||||
}
|
||||
|
||||
/** 등급평가 결과 단건 상세. 권한: GRADE_EVAL READ. */
|
||||
@GetMapping("/{evalId}")
|
||||
@RequirePermission(menu = "GRADE_EVAL", perm = "READ")
|
||||
public ApiResponse<AgentGradeEvaluationResp> detailEval(@PathVariable Long evalId) {
|
||||
return ApiResponse.ok(service.detailEval(evalId));
|
||||
}
|
||||
|
||||
/** 등급평가 결과 신규 생성(아직 미적용 상태). 응답: evalId. 권한: GRADE_EVAL CREATE. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "GRADE_EVAL", perm = "CREATE")
|
||||
@DataChangeLog(menu = "GRADE_EVAL", table = "agent_grade_evaluation")
|
||||
public ApiResponse<Long> createEval(@Valid @RequestBody AgentGradeEvaluationSaveReq req) {
|
||||
return ApiResponse.ok(service.createEval(req));
|
||||
}
|
||||
|
||||
/** 등급평가 결과 적용. 평가된 등급을 실제 설계사 등급에 반영한다. 권한: GRADE_EVAL UPDATE. */
|
||||
@PostMapping("/{evalId}/apply")
|
||||
@RequirePermission(menu = "GRADE_EVAL", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "GRADE_EVAL", table = "agent_grade_evaluation")
|
||||
public ApiResponse<Void> applyEval(@PathVariable Long evalId) {
|
||||
service.applyEval(evalId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 등급평가 결과 삭제. 권한: GRADE_EVAL DELETE. */
|
||||
@DeleteMapping("/{evalId}")
|
||||
@RequirePermission(menu = "GRADE_EVAL", perm = "DELETE")
|
||||
@DataChangeLog(menu = "GRADE_EVAL", table = "agent_grade_evaluation")
|
||||
public ApiResponse<Void> deleteEval(@PathVariable Long evalId) {
|
||||
service.deleteEval(evalId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.GuaranteeInsuranceService;
|
||||
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.commission.AgentGuaranteeInsuranceResp;
|
||||
import com.ga.core.vo.commission.AgentGuaranteeInsuranceSaveReq;
|
||||
import com.ga.core.vo.commission.AgentGuaranteeInsuranceSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 보증보험 API.
|
||||
*
|
||||
* 보증보험(guarantee insurance): 설계사가 선지급금 등을 갚지 못할 경우에 대비해 가입하는 보험.
|
||||
* 설계사별 가입 정보(보험사/증권번호/보장한도/기간 등)를 관리한다.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="GUARANTEE_INS"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "보증보험")
|
||||
@RestController
|
||||
@RequestMapping("/api/guarantee-insurances")
|
||||
@RequiredArgsConstructor
|
||||
public class GuaranteeInsuranceController {
|
||||
|
||||
private final GuaranteeInsuranceService service;
|
||||
|
||||
/** 보증보험 가입 목록 조회. 권한: GUARANTEE_INS READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "GUARANTEE_INS", perm = "READ")
|
||||
public ApiResponse<PageResponse<AgentGuaranteeInsuranceResp>> list(
|
||||
@ModelAttribute AgentGuaranteeInsuranceSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 보증보험 단건 상세. 권한: GUARANTEE_INS READ. */
|
||||
@GetMapping("/{guaranteeId}")
|
||||
@RequirePermission(menu = "GUARANTEE_INS", perm = "READ")
|
||||
public ApiResponse<AgentGuaranteeInsuranceResp> detail(@PathVariable Long guaranteeId) {
|
||||
return ApiResponse.ok(service.detail(guaranteeId));
|
||||
}
|
||||
|
||||
/** 보증보험 가입 등록. 응답: guaranteeId. 권한: GUARANTEE_INS CREATE. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "GUARANTEE_INS", perm = "CREATE")
|
||||
@DataChangeLog(menu = "GUARANTEE_INS", table = "agent_guarantee_insurance")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody AgentGuaranteeInsuranceSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 보증보험 정보 수정. 권한: GUARANTEE_INS UPDATE. */
|
||||
@PutMapping("/{guaranteeId}")
|
||||
@RequirePermission(menu = "GUARANTEE_INS", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "GUARANTEE_INS", table = "agent_guarantee_insurance")
|
||||
public ApiResponse<Void> update(@PathVariable Long guaranteeId,
|
||||
@Valid @RequestBody AgentGuaranteeInsuranceSaveReq req) {
|
||||
service.update(guaranteeId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 보증보험 삭제. 권한: GUARANTEE_INS DELETE. */
|
||||
@DeleteMapping("/{guaranteeId}")
|
||||
@RequirePermission(menu = "GUARANTEE_INS", perm = "DELETE")
|
||||
@DataChangeLog(menu = "GUARANTEE_INS", table = "agent_guarantee_insurance")
|
||||
public ApiResponse<Void> delete(@PathVariable Long guaranteeId) {
|
||||
service.delete(guaranteeId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.HqSettleService;
|
||||
import com.ga.api.service.commission.HqSettleService.HqSummaryResp;
|
||||
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.commission.HqSettleItemResp;
|
||||
import com.ga.core.vo.commission.HqSettleItemSaveReq;
|
||||
import com.ga.core.vo.commission.HqSettleItemSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 본사정산 API.
|
||||
*
|
||||
* 본사정산(HQ settle): 본사 차원에서 발생하는 정산 항목(item)을 정산월 단위로 관리하고 합계(summary)를 낸다.
|
||||
* 설계사 지급과 별개로 본사가 직접 가감하는 비용/수입 항목 등을 다룬다.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="HQ_SETTLE"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "본사정산")
|
||||
@RestController
|
||||
@RequestMapping("/api/hq-settles")
|
||||
@RequiredArgsConstructor
|
||||
public class HqSettleController {
|
||||
|
||||
private final HqSettleService service;
|
||||
|
||||
/** 본사정산 항목 목록 조회. 권한: HQ_SETTLE READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "HQ_SETTLE", perm = "READ")
|
||||
public ApiResponse<PageResponse<HqSettleItemResp>> list(@ModelAttribute HqSettleItemSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 특정 정산월의 본사정산 합계 요약. 권한: HQ_SETTLE READ. */
|
||||
@GetMapping("/summary")
|
||||
@RequirePermission(menu = "HQ_SETTLE", perm = "READ")
|
||||
public ApiResponse<HqSummaryResp> summary(@RequestParam String settleMonth) {
|
||||
return ApiResponse.ok(service.summary(settleMonth));
|
||||
}
|
||||
|
||||
/** 본사정산 항목 단건 상세. 권한: HQ_SETTLE READ. */
|
||||
@GetMapping("/{itemId}")
|
||||
@RequirePermission(menu = "HQ_SETTLE", perm = "READ")
|
||||
public ApiResponse<HqSettleItemResp> detail(@PathVariable Long itemId) {
|
||||
return ApiResponse.ok(service.detail(itemId));
|
||||
}
|
||||
|
||||
/** 본사정산 항목 신규 등록. 응답: itemId. 권한: HQ_SETTLE CREATE. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "HQ_SETTLE", perm = "CREATE")
|
||||
@DataChangeLog(menu = "HQ_SETTLE", table = "hq_settle_item")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody HqSettleItemSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 본사정산 항목 수정. 권한: HQ_SETTLE UPDATE. */
|
||||
@PutMapping("/{itemId}")
|
||||
@RequirePermission(menu = "HQ_SETTLE", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "HQ_SETTLE", table = "hq_settle_item")
|
||||
public ApiResponse<Void> update(@PathVariable Long itemId,
|
||||
@Valid @RequestBody HqSettleItemSaveReq req) {
|
||||
service.update(itemId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 본사정산 항목 삭제. 권한: HQ_SETTLE DELETE. */
|
||||
@DeleteMapping("/{itemId}")
|
||||
@RequirePermission(menu = "HQ_SETTLE", perm = "DELETE")
|
||||
@DataChangeLog(menu = "HQ_SETTLE", table = "hq_settle_item")
|
||||
public ApiResponse<Void> delete(@PathVariable Long itemId) {
|
||||
service.delete(itemId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.IncomeCommissionService;
|
||||
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.commission.*;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 수입수수료/수지차 API.
|
||||
*
|
||||
* - 수입수수료(income commission): GA가 보험사로부터 받는 수수료. "보험료 x 보험사 요율"로 계산되며,
|
||||
* 설계사에게 지급하는 수수료(지출)의 재원이 된다.
|
||||
* - 수지차(margin): 같은 정산월의 수입수수료 합계에서 설계사 지급액 합계를 뺀 GA의 마진.
|
||||
* 양수면 흑자, 음수면 적자를 뜻한다.
|
||||
*
|
||||
* 모든 엔드포인트는 @RequirePermission(menu="INCOME_COMM")로 보호되며, perm 은 READ(조회)/CREATE(생성) 권한을 의미한다.
|
||||
* 데이터 변경(POST)에는 @DataChangeLog 로 변경 이력(감사 로그)이 자동 기록된다.
|
||||
*/
|
||||
@Tag(name = "수입수수료/수지차")
|
||||
@RestController
|
||||
@RequestMapping("/api/income-commissions")
|
||||
@RequiredArgsConstructor
|
||||
public class IncomeCommissionController {
|
||||
|
||||
private final IncomeCommissionService service;
|
||||
|
||||
// ── Income Ledger (수입수수료 원장) ───────────────────────────────────────
|
||||
|
||||
/** 수입수수료 원장 목록 조회(검색조건 + 페이징). 권한: INCOME_COMM READ. */
|
||||
@GetMapping("/ledgers")
|
||||
@RequirePermission(menu = "INCOME_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<IncomeCommissionLedgerResp>> listLedgers(
|
||||
@ModelAttribute IncomeCommissionLedgerSearchParam param) {
|
||||
return ApiResponse.ok(service.listLedgers(param));
|
||||
}
|
||||
|
||||
/** 수입수수료 원장 단건 상세 조회. 권한: INCOME_COMM READ. */
|
||||
@GetMapping("/ledgers/{incomeId}")
|
||||
@RequirePermission(menu = "INCOME_COMM", perm = "READ")
|
||||
public ApiResponse<IncomeCommissionLedgerResp> detailLedger(@PathVariable Long incomeId) {
|
||||
return ApiResponse.ok(service.detailLedger(incomeId));
|
||||
}
|
||||
|
||||
/** 수입수수료 원장 신규 등록. 등록 시 "보험료 x 보험사요율"로 수입액이 계산된다. 응답: 생성된 incomeId. 권한: INCOME_COMM CREATE. */
|
||||
@PostMapping("/ledgers")
|
||||
@RequirePermission(menu = "INCOME_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "INCOME_COMM", table = "income_commission_ledger")
|
||||
public ApiResponse<Long> createLedger(@Valid @RequestBody IncomeCommissionLedgerSaveReq req) {
|
||||
return ApiResponse.ok(service.createLedger(req));
|
||||
}
|
||||
|
||||
// ── Margin Summary (수지차 집계) ──────────────────────────────────────────
|
||||
|
||||
/** 수지차 집계 목록 조회. 권한: INCOME_COMM READ. */
|
||||
@GetMapping("/margins")
|
||||
@RequirePermission(menu = "INCOME_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<MarginSummaryResp>> listMargins(
|
||||
@ModelAttribute MarginSummarySearchParam param) {
|
||||
return ApiResponse.ok(service.listMargins(param));
|
||||
}
|
||||
|
||||
/** 수지차 집계 단건 상세 조회. 권한: INCOME_COMM READ. */
|
||||
@GetMapping("/margins/{marginId}")
|
||||
@RequirePermission(menu = "INCOME_COMM", perm = "READ")
|
||||
public ApiResponse<MarginSummaryResp> detailMargin(@PathVariable Long marginId) {
|
||||
return ApiResponse.ok(service.detailMargin(marginId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수지차 재계산. 정산월(settleMonth) 기준으로 수입수수료 합 - 설계사 지급액 합을 다시 산출해 저장한다.
|
||||
* agentId 가 주어지면 해당 설계사 지급분만 좁혀 집계한다(미지정 시 GA 월 전체). 응답: marginId. 권한: INCOME_COMM CREATE.
|
||||
*/
|
||||
@PostMapping("/margin/recalc")
|
||||
@RequirePermission(menu = "INCOME_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "INCOME_COMM", table = "margin_summary")
|
||||
public ApiResponse<Long> recalcMargin(@RequestParam String settleMonth,
|
||||
@RequestParam(required = false) Long agentId) {
|
||||
return ApiResponse.ok(service.recalcMargin(settleMonth, agentId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.JointContractService;
|
||||
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.commission.JointContractResp;
|
||||
import com.ga.core.vo.commission.JointContractSaveReq;
|
||||
import com.ga.core.vo.commission.JointContractSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 공동계약 API.
|
||||
*
|
||||
* 공동계약(joint contract): 한 계약을 두 명 이상의 설계사가 함께 모집한 경우, 그 수수료를
|
||||
* 약정 비율(지분)대로 나눠 갖도록 관리하는 기능. 비율 합이 100%가 되도록 등록한다.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="JOINT_CONTRACT"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "공동계약")
|
||||
@RestController
|
||||
@RequestMapping("/api/joint-contracts")
|
||||
@RequiredArgsConstructor
|
||||
public class JointContractController {
|
||||
|
||||
private final JointContractService service;
|
||||
|
||||
/** 공동계약 목록 조회. 권한: JOINT_CONTRACT READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "JOINT_CONTRACT", perm = "READ")
|
||||
public ApiResponse<PageResponse<JointContractResp>> list(@ModelAttribute JointContractSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 공동계약 단건 상세. 권한: JOINT_CONTRACT READ. */
|
||||
@GetMapping("/{jointId}")
|
||||
@RequirePermission(menu = "JOINT_CONTRACT", perm = "READ")
|
||||
public ApiResponse<JointContractResp> detail(@PathVariable Long jointId) {
|
||||
return ApiResponse.ok(service.detail(jointId));
|
||||
}
|
||||
|
||||
/** 공동계약 신규 등록(설계사별 지분 비율 포함). 응답: jointId. 권한: JOINT_CONTRACT CREATE. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "JOINT_CONTRACT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "JOINT_CONTRACT", table = "joint_contract")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody JointContractSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 공동계약 수정(지분 비율 변경 등). 권한: JOINT_CONTRACT UPDATE. */
|
||||
@PutMapping("/{jointId}")
|
||||
@RequirePermission(menu = "JOINT_CONTRACT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "JOINT_CONTRACT", table = "joint_contract")
|
||||
public ApiResponse<Void> update(@PathVariable Long jointId,
|
||||
@Valid @RequestBody JointContractSaveReq req) {
|
||||
service.update(jointId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 공동계약 삭제. 권한: JOINT_CONTRACT DELETE. */
|
||||
@DeleteMapping("/{jointId}")
|
||||
@RequirePermission(menu = "JOINT_CONTRACT", perm = "DELETE")
|
||||
@DataChangeLog(menu = "JOINT_CONTRACT", table = "joint_contract")
|
||||
public ApiResponse<Void> delete(@PathVariable Long jointId) {
|
||||
service.delete(jointId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.LapsedReinstatementService;
|
||||
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.commission.LapsedReinstatementRecoveryResp;
|
||||
import com.ga.core.vo.commission.LapsedReinstatementRecoverySaveReq;
|
||||
import com.ga.core.vo.commission.LapsedReinstatementRecoverySearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 익월부활재지급 API.
|
||||
*
|
||||
* 실효(lapsed): 보험료 미납 등으로 계약 효력이 일시 정지된 상태. 실효가 나면 그 달에 지급했던 수수료를 환수한다.
|
||||
* 부활(reinstatement): 실효된 계약이 다시 살아나는 것. 다음 달(익월)에 부활하면 앞서 환수했던 수수료를
|
||||
* 다시 지급(재지급)해 준다. 이 도메인은 그 "환수 → 익월 부활 시 재지급(회수)" 흐름을 관리한다.
|
||||
* - repay: 부활에 따른 재지급(회수) 확정 - cancel: 재지급 취소
|
||||
*
|
||||
* 권한: @RequirePermission(menu="LAPSED_REINSTATEMENT"), perm=READ/CREATE/UPDATE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "익월부활재지급")
|
||||
@RestController
|
||||
@RequestMapping("/api/lapsed-reinstatements")
|
||||
@RequiredArgsConstructor
|
||||
public class LapsedReinstatementController {
|
||||
|
||||
private final LapsedReinstatementService service;
|
||||
|
||||
/** 익월부활 재지급 대상 목록 조회. 권한: LAPSED_REINSTATEMENT READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "READ")
|
||||
public ApiResponse<PageResponse<LapsedReinstatementRecoveryResp>> list(
|
||||
@ModelAttribute LapsedReinstatementRecoverySearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 재지급 건 단건 상세. 권한: LAPSED_REINSTATEMENT READ. */
|
||||
@GetMapping("/{recoveryId}")
|
||||
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "READ")
|
||||
public ApiResponse<LapsedReinstatementRecoveryResp> detail(@PathVariable Long recoveryId) {
|
||||
return ApiResponse.ok(service.detail(recoveryId));
|
||||
}
|
||||
|
||||
/** 재지급 건 신규 등록. 응답: recoveryId. 권한: LAPSED_REINSTATEMENT CREATE. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "LAPSED_REINSTATEMENT", table = "lapsed_reinstatement_recovery")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody LapsedReinstatementRecoverySaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 부활에 따른 재지급(회수) 확정 처리. 권한: LAPSED_REINSTATEMENT UPDATE. */
|
||||
@PostMapping("/{recoveryId}/repay")
|
||||
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "LAPSED_REINSTATEMENT", table = "lapsed_reinstatement_recovery")
|
||||
public ApiResponse<Void> repay(@PathVariable Long recoveryId) {
|
||||
service.repay(recoveryId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 재지급 취소 처리. 권한: LAPSED_REINSTATEMENT UPDATE. */
|
||||
@PostMapping("/{recoveryId}/cancel")
|
||||
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "LAPSED_REINSTATEMENT", table = "lapsed_reinstatement_recovery")
|
||||
public ApiResponse<Void> cancel(@PathVariable Long recoveryId) {
|
||||
service.cancel(recoveryId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.LifeSupportService;
|
||||
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.commission.*;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 생보운영지원수수료 API.
|
||||
*
|
||||
* 생명보험사 운영지원수수료(life operation support): 생보 상품 운영/관리 활동에 대해 지급하는 지원성 수수료.
|
||||
* 규칙(Rule)에 정의된 기준에 따라 산출되어 원장(Ledger)에 기록된다.
|
||||
*
|
||||
* 구조는 "규칙(Rule) CRUD + 원장(Ledger) 조회/등록"으로, 다른 수수료 도메인과 동일한 패턴이다.
|
||||
* 권한: @RequirePermission(menu="LIFE_OP_SUPPORT"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "생보운영지원수수료")
|
||||
@RestController
|
||||
@RequestMapping("/api/life-supports")
|
||||
@RequiredArgsConstructor
|
||||
public class LifeSupportController {
|
||||
|
||||
private final LifeSupportService service;
|
||||
|
||||
// ── Rule (지원수수료 규칙) ──────────────────────────────────────────────
|
||||
|
||||
/** 규칙 목록 조회. 권한: LIFE_OP_SUPPORT READ. */
|
||||
@GetMapping("/rules")
|
||||
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ")
|
||||
public ApiResponse<PageResponse<LifeSupportRuleResp>> listRules(
|
||||
@ModelAttribute LifeSupportRuleSearchParam param) {
|
||||
return ApiResponse.ok(service.listRules(param));
|
||||
}
|
||||
|
||||
/** 규칙 단건 상세. 권한: LIFE_OP_SUPPORT READ. */
|
||||
@GetMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ")
|
||||
public ApiResponse<LifeSupportRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||
return ApiResponse.ok(service.detailRule(ruleId));
|
||||
}
|
||||
|
||||
/** 규칙 신규 등록. 응답: ruleId. 권한: LIFE_OP_SUPPORT CREATE. */
|
||||
@PostMapping("/rules")
|
||||
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "LIFE_OP_SUPPORT", table = "life_support_rule")
|
||||
public ApiResponse<Long> createRule(@Valid @RequestBody LifeSupportRuleSaveReq req) {
|
||||
return ApiResponse.ok(service.createRule(req));
|
||||
}
|
||||
|
||||
/** 규칙 수정. 권한: LIFE_OP_SUPPORT UPDATE. */
|
||||
@PutMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "LIFE_OP_SUPPORT", table = "life_support_rule")
|
||||
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
|
||||
@Valid @RequestBody LifeSupportRuleSaveReq req) {
|
||||
service.updateRule(ruleId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 규칙 삭제. 권한: LIFE_OP_SUPPORT DELETE. */
|
||||
@DeleteMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "DELETE")
|
||||
@DataChangeLog(menu = "LIFE_OP_SUPPORT", table = "life_support_rule")
|
||||
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
|
||||
service.deleteRule(ruleId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ── Ledger (지원수수료 원장) ────────────────────────────────────────────
|
||||
|
||||
/** 원장 목록 조회. 권한: LIFE_OP_SUPPORT READ. */
|
||||
@GetMapping("/ledgers")
|
||||
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ")
|
||||
public ApiResponse<PageResponse<LifeSupportLedgerResp>> listLedgers(
|
||||
@ModelAttribute LifeSupportLedgerSearchParam param) {
|
||||
return ApiResponse.ok(service.listLedgers(param));
|
||||
}
|
||||
|
||||
/** 원장 단건 상세. 권한: LIFE_OP_SUPPORT READ. */
|
||||
@GetMapping("/ledgers/{ledgerId}")
|
||||
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ")
|
||||
public ApiResponse<LifeSupportLedgerResp> detailLedger(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(service.detailLedger(ledgerId));
|
||||
}
|
||||
|
||||
/** 원장 신규 등록(지원수수료 산출/지급 기록). 응답: ledgerId. 권한: LIFE_OP_SUPPORT CREATE. */
|
||||
@PostMapping("/ledgers")
|
||||
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "LIFE_OP_SUPPORT", table = "life_support_ledger")
|
||||
public ApiResponse<Long> createLedger(@Valid @RequestBody LifeSupportLedgerSaveReq req) {
|
||||
return ApiResponse.ok(service.createLedger(req));
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,16 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 오버라이드 원장 API.
|
||||
*
|
||||
* 오버라이드(override): 상위 직급자(예: 팀장/지점장)가 자기 산하 하위 설계사의 실적에 대해
|
||||
* 일정 비율로 추가로 받는 수수료. "하위가 판 만큼 상위에게도 얹어주는" 구조라 override 라 부른다.
|
||||
* 이 원장은 누가(fromAgent) 누구에게(toAgentId) 얼마를 넘겨주는지를 기록한다.
|
||||
* - summary: 수령자(상위자) 기준 집계.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="OVERRIDE_LEDGER"), perm=READ. (조회 전용 - 생성/배치는 별도 계산 로직에서 처리)
|
||||
*/
|
||||
@Tag(name = "오버라이드 원장")
|
||||
@RestController
|
||||
@RequestMapping("/api/override-ledgers")
|
||||
@@ -19,24 +29,28 @@ public class OverrideLedgerController {
|
||||
|
||||
private final OverrideLedgerService service;
|
||||
|
||||
/** 오버라이드 원장 목록 조회. 권한: OVERRIDE_LEDGER READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "OVERRIDE_LEDGER", perm = "READ")
|
||||
public ApiResponse<PageResponse<OverrideLedgerResp>> list(@ModelAttribute OverrideLedgerSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 오버라이드 원장 단건 상세. 권한: OVERRIDE_LEDGER READ. */
|
||||
@GetMapping("/{ledgerId}")
|
||||
@RequirePermission(menu = "OVERRIDE_LEDGER", perm = "READ")
|
||||
public ApiResponse<OverrideLedgerResp> detail(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(service.detail(ledgerId));
|
||||
}
|
||||
|
||||
/** 오버라이드 집계 목록(수령자별 합계 등). 권한: OVERRIDE_LEDGER READ. */
|
||||
@GetMapping("/summary")
|
||||
@RequirePermission(menu = "OVERRIDE_LEDGER", perm = "READ")
|
||||
public ApiResponse<List<OverrideSummaryResp>> summaryList(@ModelAttribute OverrideSummarySearchParam param) {
|
||||
return ApiResponse.ok(service.summaryList(param));
|
||||
}
|
||||
|
||||
/** 특정 수령자(toAgentId)의 특정 정산월(settleMonth) 오버라이드 합계. 권한: OVERRIDE_LEDGER READ. */
|
||||
@GetMapping("/summary/by-agent")
|
||||
@RequirePermission(menu = "OVERRIDE_LEDGER", perm = "READ")
|
||||
public ApiResponse<OverrideSummaryResp> summaryByAgent(@RequestParam Long toAgentId,
|
||||
|
||||
@@ -11,6 +11,15 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 유지보너스(유지보수수당) API.
|
||||
*
|
||||
* 유지율 보너스(persistency bonus): 계약이 일정 기간(예: 13회차/25회차)까지 해지 없이 잘 "유지"되면
|
||||
* 그 유지 성과에 대해 설계사에게 지급하는 보너스. 유지율이 높을수록 보너스가 커진다.
|
||||
* - Rule(보너스 규칙) CRUD - Bonus(보너스 산출 내역): upsert(있으면 갱신, 없으면 생성)로 등록.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="PERSIST_BONUS"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "유지보수수당")
|
||||
@RestController
|
||||
@RequestMapping("/api/persistency-bonus")
|
||||
@@ -19,6 +28,7 @@ public class PersistencyBonusController {
|
||||
|
||||
private final PersistencyBonusService service;
|
||||
|
||||
/** 유지보너스 규칙 목록 조회. 권한: PERSIST_BONUS READ. */
|
||||
@GetMapping("/rules")
|
||||
@RequirePermission(menu = "PERSIST_BONUS", perm = "READ")
|
||||
public ApiResponse<PageResponse<PersistencyBonusRuleResp>> listRules(
|
||||
@@ -26,12 +36,14 @@ public class PersistencyBonusController {
|
||||
return ApiResponse.ok(service.listRules(param));
|
||||
}
|
||||
|
||||
/** 유지보너스 규칙 단건 상세. 권한: PERSIST_BONUS READ. */
|
||||
@GetMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "PERSIST_BONUS", perm = "READ")
|
||||
public ApiResponse<PersistencyBonusRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||
return ApiResponse.ok(service.detailRule(ruleId));
|
||||
}
|
||||
|
||||
/** 유지보너스 규칙 신규 등록. 응답: ruleId. 권한: PERSIST_BONUS CREATE. */
|
||||
@PostMapping("/rules")
|
||||
@RequirePermission(menu = "PERSIST_BONUS", perm = "CREATE")
|
||||
@DataChangeLog(menu = "PERSIST_BONUS", table = "persistency_bonus_rule")
|
||||
@@ -39,6 +51,7 @@ public class PersistencyBonusController {
|
||||
return ApiResponse.ok(service.createRule(req));
|
||||
}
|
||||
|
||||
/** 유지보너스 규칙 수정. 권한: PERSIST_BONUS UPDATE. */
|
||||
@PutMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "PERSIST_BONUS", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "PERSIST_BONUS", table = "persistency_bonus_rule")
|
||||
@@ -48,6 +61,7 @@ public class PersistencyBonusController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 유지보너스 규칙 삭제. 권한: PERSIST_BONUS DELETE. */
|
||||
@DeleteMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "PERSIST_BONUS", perm = "DELETE")
|
||||
@DataChangeLog(menu = "PERSIST_BONUS", table = "persistency_bonus_rule")
|
||||
@@ -56,6 +70,7 @@ public class PersistencyBonusController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 유지보너스 산출 내역 목록 조회. 권한: PERSIST_BONUS READ. */
|
||||
@GetMapping("/bonuses")
|
||||
@RequirePermission(menu = "PERSIST_BONUS", perm = "READ")
|
||||
public ApiResponse<PageResponse<PersistencyBonusResp>> listBonuses(
|
||||
@@ -63,12 +78,14 @@ public class PersistencyBonusController {
|
||||
return ApiResponse.ok(service.listBonuses(param));
|
||||
}
|
||||
|
||||
/** 유지보너스 산출 내역 단건 상세. 권한: PERSIST_BONUS READ. */
|
||||
@GetMapping("/bonuses/{bonusId}")
|
||||
@RequirePermission(menu = "PERSIST_BONUS", perm = "READ")
|
||||
public ApiResponse<PersistencyBonusResp> detailBonus(@PathVariable Long bonusId) {
|
||||
return ApiResponse.ok(service.detailBonus(bonusId));
|
||||
}
|
||||
|
||||
/** 유지보너스 산출 내역 upsert(동일 키가 있으면 갱신, 없으면 신규 생성). 권한: PERSIST_BONUS CREATE. */
|
||||
@PostMapping("/bonuses")
|
||||
@RequirePermission(menu = "PERSIST_BONUS", perm = "CREATE")
|
||||
@DataChangeLog(menu = "PERSIST_BONUS", table = "persistency_bonus")
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.RecognizedPerformanceService;
|
||||
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.commission.*;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 인정실적 API.
|
||||
*
|
||||
* 인정실적(recognized performance): 실제 보험료를 그대로 실적으로 보지 않고, 상품 종류에 따른
|
||||
* 환산율(product conversion rate)을 곱해 "회사가 인정해 주는 실적"으로 환산한 값.
|
||||
* (예: 보장성 100%, 저축성 30% 등. 같은 보험료라도 상품에 따라 인정실적이 달라진다.)
|
||||
* - rates(환산율 규칙) CRUD - ledgers(인정실적 원장) 조회/등록.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="RECOGNIZED_PERF"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "인정실적")
|
||||
@RestController
|
||||
@RequestMapping("/api/recognized-performances")
|
||||
@RequiredArgsConstructor
|
||||
public class RecognizedPerformanceController {
|
||||
|
||||
private final RecognizedPerformanceService service;
|
||||
|
||||
// ── Product Conversion Rate (상품 환산율 규칙) ──────────────────────────
|
||||
|
||||
/** 상품 환산율 목록 조회. 권한: RECOGNIZED_PERF READ. */
|
||||
@GetMapping("/rates")
|
||||
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ")
|
||||
public ApiResponse<PageResponse<ProductConversionRateResp>> listRates(
|
||||
@ModelAttribute ProductConversionRateSearchParam param) {
|
||||
return ApiResponse.ok(service.listRates(param));
|
||||
}
|
||||
|
||||
/** 상품 환산율 단건 상세. 권한: RECOGNIZED_PERF READ. */
|
||||
@GetMapping("/rates/{rateId}")
|
||||
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ")
|
||||
public ApiResponse<ProductConversionRateResp> detailRate(@PathVariable Long rateId) {
|
||||
return ApiResponse.ok(service.detailRate(rateId));
|
||||
}
|
||||
|
||||
/** 상품 환산율 신규 등록. 응답: rateId. 권한: RECOGNIZED_PERF CREATE. */
|
||||
@PostMapping("/rates")
|
||||
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "CREATE")
|
||||
@DataChangeLog(menu = "RECOGNIZED_PERF", table = "product_conversion_rate")
|
||||
public ApiResponse<Long> createRate(@Valid @RequestBody ProductConversionRateSaveReq req) {
|
||||
return ApiResponse.ok(service.createRate(req));
|
||||
}
|
||||
|
||||
/** 상품 환산율 수정. 권한: RECOGNIZED_PERF UPDATE. */
|
||||
@PutMapping("/rates/{rateId}")
|
||||
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "RECOGNIZED_PERF", table = "product_conversion_rate")
|
||||
public ApiResponse<Void> updateRate(@PathVariable Long rateId,
|
||||
@Valid @RequestBody ProductConversionRateSaveReq req) {
|
||||
service.updateRate(rateId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 상품 환산율 삭제. 권한: RECOGNIZED_PERF DELETE. */
|
||||
@DeleteMapping("/rates/{rateId}")
|
||||
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "DELETE")
|
||||
@DataChangeLog(menu = "RECOGNIZED_PERF", table = "product_conversion_rate")
|
||||
public ApiResponse<Void> deleteRate(@PathVariable Long rateId) {
|
||||
service.deleteRate(rateId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ── Ledger (인정실적 원장) ──────────────────────────────────────────────
|
||||
|
||||
/** 인정실적 원장 목록 조회. 권한: RECOGNIZED_PERF READ. */
|
||||
@GetMapping("/ledgers")
|
||||
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ")
|
||||
public ApiResponse<PageResponse<RecognizedPerformanceLedgerResp>> listLedgers(
|
||||
@ModelAttribute RecognizedPerformanceLedgerSearchParam param) {
|
||||
return ApiResponse.ok(service.listLedgers(param));
|
||||
}
|
||||
|
||||
/** 인정실적 원장 단건 상세. 권한: RECOGNIZED_PERF READ. */
|
||||
@GetMapping("/ledgers/{ledgerId}")
|
||||
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ")
|
||||
public ApiResponse<RecognizedPerformanceLedgerResp> detailLedger(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(service.detailLedger(ledgerId));
|
||||
}
|
||||
|
||||
/** 인정실적 원장 신규 등록(환산율 적용해 인정실적 산출). 응답: ledgerId. 권한: RECOGNIZED_PERF CREATE. */
|
||||
@PostMapping("/ledgers")
|
||||
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "CREATE")
|
||||
@DataChangeLog(menu = "RECOGNIZED_PERF", table = "recognized_performance_ledger")
|
||||
public ApiResponse<Long> createLedger(@Valid @RequestBody RecognizedPerformanceLedgerSaveReq req) {
|
||||
return ApiResponse.ok(service.createLedger(req));
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.RecruiterCommissionService;
|
||||
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.commission.*;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 도입수수료 API.
|
||||
*
|
||||
* 도입수수료(recruiter commission): 새 설계사를 회사에 데려온(도입/리크루팅) 사람에게 지급하는 수수료.
|
||||
* 도입한 설계사가 실적을 내면, 그 실적에 연동해 도입자에게도 일정 수수료가 발생한다.
|
||||
* - Rule(도입수수료 규칙) CRUD - Ledger(도입수수료 원장) 조회/등록.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="RECRUITER_COMM"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "도입수수료")
|
||||
@RestController
|
||||
@RequestMapping("/api/recruiter-commissions")
|
||||
@RequiredArgsConstructor
|
||||
public class RecruiterCommissionController {
|
||||
|
||||
private final RecruiterCommissionService service;
|
||||
|
||||
// ── Rule (도입수수료 규칙) ──────────────────────────────────────────────
|
||||
|
||||
/** 도입수수료 규칙 목록 조회. 권한: RECRUITER_COMM READ. */
|
||||
@GetMapping("/rules")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<RecruiterCommissionRuleResp>> listRules(
|
||||
@ModelAttribute RecruiterCommissionRuleSearchParam param) {
|
||||
return ApiResponse.ok(service.listRules(param));
|
||||
}
|
||||
|
||||
/** 도입수수료 규칙 단건 상세. 권한: RECRUITER_COMM READ. */
|
||||
@GetMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
|
||||
public ApiResponse<RecruiterCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||
return ApiResponse.ok(service.detailRule(ruleId));
|
||||
}
|
||||
|
||||
/** 도입수수료 규칙 신규 등록. 응답: ruleId. 권한: RECRUITER_COMM CREATE. */
|
||||
@PostMapping("/rules")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_rule")
|
||||
public ApiResponse<Long> createRule(@Valid @RequestBody RecruiterCommissionRuleSaveReq req) {
|
||||
return ApiResponse.ok(service.createRule(req));
|
||||
}
|
||||
|
||||
/** 도입수수료 규칙 수정. 권한: RECRUITER_COMM UPDATE. */
|
||||
@PutMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_rule")
|
||||
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
|
||||
@Valid @RequestBody RecruiterCommissionRuleSaveReq req) {
|
||||
service.updateRule(ruleId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 도입수수료 규칙 삭제. 권한: RECRUITER_COMM DELETE. */
|
||||
@DeleteMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "DELETE")
|
||||
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_rule")
|
||||
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
|
||||
service.deleteRule(ruleId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ── Ledger (도입수수료 원장) ────────────────────────────────────────────
|
||||
|
||||
/** 도입수수료 원장 목록 조회. 권한: RECRUITER_COMM READ. */
|
||||
@GetMapping("/ledgers")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<RecruiterCommissionLedgerResp>> listLedgers(
|
||||
@ModelAttribute RecruiterCommissionLedgerSearchParam param) {
|
||||
return ApiResponse.ok(service.listLedgers(param));
|
||||
}
|
||||
|
||||
/** 도입수수료 원장 단건 상세. 권한: RECRUITER_COMM READ. */
|
||||
@GetMapping("/ledgers/{ledgerId}")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
|
||||
public ApiResponse<RecruiterCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(service.detailLedger(ledgerId));
|
||||
}
|
||||
|
||||
/** 도입수수료 원장 신규 등록. 응답: ledgerId. 권한: RECRUITER_COMM CREATE. */
|
||||
@PostMapping("/ledgers")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_ledger")
|
||||
public ApiResponse<Long> createLedger(@Valid @RequestBody RecruiterCommissionLedgerSaveReq req) {
|
||||
return ApiResponse.ok(service.createLedger(req));
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.ReferralCommissionService;
|
||||
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.commission.*;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 소개·이관수수료 API.
|
||||
*
|
||||
* 소개수수료(referral commission): 고객이나 계약을 다른 설계사에게 "소개·이관"해 준 사람에게 지급하는 수수료.
|
||||
* 소개 받은 설계사가 계약을 성사시키면, 소개해 준 사람에게 규칙(Rule)에 따라 정액(FIXED) 또는 정률(RATE)로 지급한다.
|
||||
* - Rule(소개수수료 규칙) CRUD - Ledger(소개수수료 원장) 조회/등록.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="REFERRAL_COMM"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "소개·이관수수료")
|
||||
@RestController
|
||||
@RequestMapping("/api/referral-commissions")
|
||||
@RequiredArgsConstructor
|
||||
public class ReferralCommissionController {
|
||||
|
||||
private final ReferralCommissionService service;
|
||||
|
||||
// ── Rule (소개수수료 규칙) ──────────────────────────────────────────────
|
||||
|
||||
/** 소개수수료 규칙 목록 조회. 권한: REFERRAL_COMM READ. */
|
||||
@GetMapping("/rules")
|
||||
@RequirePermission(menu = "REFERRAL_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<ReferralCommissionRuleResp>> listRules(
|
||||
@ModelAttribute ReferralCommissionRuleSearchParam param) {
|
||||
return ApiResponse.ok(service.listRules(param));
|
||||
}
|
||||
|
||||
/** 소개수수료 규칙 단건 상세. 권한: REFERRAL_COMM READ. */
|
||||
@GetMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "REFERRAL_COMM", perm = "READ")
|
||||
public ApiResponse<ReferralCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||
return ApiResponse.ok(service.detailRule(ruleId));
|
||||
}
|
||||
|
||||
/** 소개수수료 규칙 신규 등록. 응답: ruleId. 권한: REFERRAL_COMM CREATE. */
|
||||
@PostMapping("/rules")
|
||||
@RequirePermission(menu = "REFERRAL_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "REFERRAL_COMM", table = "referral_commission_rule")
|
||||
public ApiResponse<Long> createRule(@Valid @RequestBody ReferralCommissionRuleSaveReq req) {
|
||||
return ApiResponse.ok(service.createRule(req));
|
||||
}
|
||||
|
||||
/** 소개수수료 규칙 수정. 권한: REFERRAL_COMM UPDATE. */
|
||||
@PutMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "REFERRAL_COMM", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "REFERRAL_COMM", table = "referral_commission_rule")
|
||||
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
|
||||
@Valid @RequestBody ReferralCommissionRuleSaveReq req) {
|
||||
service.updateRule(ruleId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 소개수수료 규칙 삭제. 권한: REFERRAL_COMM DELETE. */
|
||||
@DeleteMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "REFERRAL_COMM", perm = "DELETE")
|
||||
@DataChangeLog(menu = "REFERRAL_COMM", table = "referral_commission_rule")
|
||||
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
|
||||
service.deleteRule(ruleId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ── Ledger (소개수수료 원장) ────────────────────────────────────────────
|
||||
|
||||
/** 소개수수료 원장 목록 조회. 권한: REFERRAL_COMM READ. */
|
||||
@GetMapping("/ledgers")
|
||||
@RequirePermission(menu = "REFERRAL_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<ReferralCommissionLedgerResp>> listLedgers(
|
||||
@ModelAttribute ReferralCommissionLedgerSearchParam param) {
|
||||
return ApiResponse.ok(service.listLedgers(param));
|
||||
}
|
||||
|
||||
/** 소개수수료 원장 단건 상세. 권한: REFERRAL_COMM READ. */
|
||||
@GetMapping("/ledgers/{ledgerId}")
|
||||
@RequirePermission(menu = "REFERRAL_COMM", perm = "READ")
|
||||
public ApiResponse<ReferralCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(service.detailLedger(ledgerId));
|
||||
}
|
||||
|
||||
/** 소개수수료 원장 신규 등록. 응답: ledgerId. 권한: REFERRAL_COMM CREATE. */
|
||||
@PostMapping("/ledgers")
|
||||
@RequirePermission(menu = "REFERRAL_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "REFERRAL_COMM", table = "referral_commission_ledger")
|
||||
public ApiResponse<Long> createLedger(@Valid @RequestBody ReferralCommissionLedgerSaveReq req) {
|
||||
return ApiResponse.ok(service.createLedger(req));
|
||||
}
|
||||
}
|
||||
+17
@@ -11,6 +11,15 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 부활수수료 API.
|
||||
*
|
||||
* 부활(reinstatement): 실효(보험료 미납 등으로 효력 정지)됐던 계약이 밀린 보험료 납입 등으로 다시 살아나는 것.
|
||||
* 부활수수료: 계약을 부활시킨 설계사에게 규칙(Rule)에 따라 지급하는 수수료.
|
||||
* - Rule(부활수수료 규칙) CRUD - Ledger(부활수수료 원장) 조회/등록.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="REINSTATE_COMM"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "부활수수료")
|
||||
@RestController
|
||||
@RequestMapping("/api/reinstatement-commissions")
|
||||
@@ -19,6 +28,7 @@ public class ReinstatementCommissionController {
|
||||
|
||||
private final ReinstatementCommissionService service;
|
||||
|
||||
/** 부활수수료 규칙 목록 조회. 권한: REINSTATE_COMM READ. */
|
||||
@GetMapping("/rules")
|
||||
@RequirePermission(menu = "REINSTATE_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<ReinstatementCommissionRuleResp>> listRules(
|
||||
@@ -26,12 +36,14 @@ public class ReinstatementCommissionController {
|
||||
return ApiResponse.ok(service.listRules(param));
|
||||
}
|
||||
|
||||
/** 부활수수료 규칙 단건 상세. 권한: REINSTATE_COMM READ. */
|
||||
@GetMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "REINSTATE_COMM", perm = "READ")
|
||||
public ApiResponse<ReinstatementCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||
return ApiResponse.ok(service.detailRule(ruleId));
|
||||
}
|
||||
|
||||
/** 부활수수료 규칙 신규 등록. 응답: ruleId. 권한: REINSTATE_COMM CREATE. */
|
||||
@PostMapping("/rules")
|
||||
@RequirePermission(menu = "REINSTATE_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "REINSTATE_COMM", table = "reinstatement_commission_rule")
|
||||
@@ -39,6 +51,7 @@ public class ReinstatementCommissionController {
|
||||
return ApiResponse.ok(service.createRule(req));
|
||||
}
|
||||
|
||||
/** 부활수수료 규칙 수정. 권한: REINSTATE_COMM UPDATE. */
|
||||
@PutMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "REINSTATE_COMM", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "REINSTATE_COMM", table = "reinstatement_commission_rule")
|
||||
@@ -48,6 +61,7 @@ public class ReinstatementCommissionController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 부활수수료 규칙 삭제. 권한: REINSTATE_COMM DELETE. */
|
||||
@DeleteMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "REINSTATE_COMM", perm = "DELETE")
|
||||
@DataChangeLog(menu = "REINSTATE_COMM", table = "reinstatement_commission_rule")
|
||||
@@ -56,6 +70,7 @@ public class ReinstatementCommissionController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 부활수수료 원장 목록 조회. 권한: REINSTATE_COMM READ. */
|
||||
@GetMapping("/ledgers")
|
||||
@RequirePermission(menu = "REINSTATE_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<ReinstatementCommissionLedgerResp>> listLedgers(
|
||||
@@ -63,12 +78,14 @@ public class ReinstatementCommissionController {
|
||||
return ApiResponse.ok(service.listLedgers(param));
|
||||
}
|
||||
|
||||
/** 부활수수료 원장 단건 상세. 권한: REINSTATE_COMM READ. */
|
||||
@GetMapping("/ledgers/{ledgerId}")
|
||||
@RequirePermission(menu = "REINSTATE_COMM", perm = "READ")
|
||||
public ApiResponse<ReinstatementCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(service.detailLedger(ledgerId));
|
||||
}
|
||||
|
||||
/** 부활수수료 원장 신규 등록. 응답: ledgerId. 권한: REINSTATE_COMM CREATE. */
|
||||
@PostMapping("/ledgers")
|
||||
@RequirePermission(menu = "REINSTATE_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "REINSTATE_COMM", table = "reinstatement_commission_ledger")
|
||||
|
||||
+17
@@ -11,6 +11,15 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 갱신수수료 API.
|
||||
*
|
||||
* 갱신(renewal): 보험 기간이 끝난 계약을 같은 조건으로 다시 이어가는 것(예: 1년 갱신형 보험).
|
||||
* 갱신수수료: 계약이 갱신될 때 그 유지·관리 대가로 설계사에게 지급하는 수수료.
|
||||
* - Rule(갱신수수료 규칙) CRUD - Ledger(갱신수수료 원장) 조회/등록.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="RENEWAL_COMM"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "갱신수수료")
|
||||
@RestController
|
||||
@RequestMapping("/api/renewal-commissions")
|
||||
@@ -19,6 +28,7 @@ public class RenewalCommissionController {
|
||||
|
||||
private final RenewalCommissionService service;
|
||||
|
||||
/** 갱신수수료 규칙 목록 조회. 권한: RENEWAL_COMM READ. */
|
||||
@GetMapping("/rules")
|
||||
@RequirePermission(menu = "RENEWAL_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<RenewalCommissionRuleResp>> listRules(
|
||||
@@ -26,12 +36,14 @@ public class RenewalCommissionController {
|
||||
return ApiResponse.ok(service.listRules(param));
|
||||
}
|
||||
|
||||
/** 갱신수수료 규칙 단건 상세. 권한: RENEWAL_COMM READ. */
|
||||
@GetMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "RENEWAL_COMM", perm = "READ")
|
||||
public ApiResponse<RenewalCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||
return ApiResponse.ok(service.detailRule(ruleId));
|
||||
}
|
||||
|
||||
/** 갱신수수료 규칙 신규 등록. 응답: ruleId. 권한: RENEWAL_COMM CREATE. */
|
||||
@PostMapping("/rules")
|
||||
@RequirePermission(menu = "RENEWAL_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "RENEWAL_COMM", table = "renewal_commission_rule")
|
||||
@@ -39,6 +51,7 @@ public class RenewalCommissionController {
|
||||
return ApiResponse.ok(service.createRule(req));
|
||||
}
|
||||
|
||||
/** 갱신수수료 규칙 수정. 권한: RENEWAL_COMM UPDATE. */
|
||||
@PutMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "RENEWAL_COMM", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "RENEWAL_COMM", table = "renewal_commission_rule")
|
||||
@@ -48,6 +61,7 @@ public class RenewalCommissionController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 갱신수수료 규칙 삭제. 권한: RENEWAL_COMM DELETE. */
|
||||
@DeleteMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "RENEWAL_COMM", perm = "DELETE")
|
||||
@DataChangeLog(menu = "RENEWAL_COMM", table = "renewal_commission_rule")
|
||||
@@ -56,6 +70,7 @@ public class RenewalCommissionController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 갱신수수료 원장 목록 조회. 권한: RENEWAL_COMM READ. */
|
||||
@GetMapping("/ledgers")
|
||||
@RequirePermission(menu = "RENEWAL_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<RenewalCommissionLedgerResp>> listLedgers(
|
||||
@@ -63,12 +78,14 @@ public class RenewalCommissionController {
|
||||
return ApiResponse.ok(service.listLedgers(param));
|
||||
}
|
||||
|
||||
/** 갱신수수료 원장 단건 상세. 권한: RENEWAL_COMM READ. */
|
||||
@GetMapping("/ledgers/{ledgerId}")
|
||||
@RequirePermission(menu = "RENEWAL_COMM", perm = "READ")
|
||||
public ApiResponse<RenewalCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(service.detailLedger(ledgerId));
|
||||
}
|
||||
|
||||
/** 갱신수수료 원장 신규 등록. 응답: ledgerId. 권한: RENEWAL_COMM CREATE. */
|
||||
@PostMapping("/ledgers")
|
||||
@RequirePermission(menu = "RENEWAL_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "RENEWAL_COMM", table = "renewal_commission_ledger")
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.ReserveFundService;
|
||||
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.commission.AgentReserveFundResp;
|
||||
import com.ga.core.vo.commission.AgentReserveFundSaveReq;
|
||||
import com.ga.core.vo.commission.AgentReserveFundSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 적립금 API.
|
||||
*
|
||||
* 적립금(reserve fund): 향후 발생할 수 있는 환수 등에 대비해, 설계사 수수료 중 일부를 떼어 모아 두는 돈.
|
||||
* 환수가 생기면 이 적립금에서 우선 충당할 수 있다. 설계사별 적립 내역을 관리한다.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="RESERVE_FUND"), perm=READ/CREATE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "적립금")
|
||||
@RestController
|
||||
@RequestMapping("/api/reserve-funds")
|
||||
@RequiredArgsConstructor
|
||||
public class ReserveFundController {
|
||||
|
||||
private final ReserveFundService service;
|
||||
|
||||
/** 적립금 내역 목록 조회. 권한: RESERVE_FUND READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "RESERVE_FUND", perm = "READ")
|
||||
public ApiResponse<PageResponse<AgentReserveFundResp>> list(@ModelAttribute AgentReserveFundSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 적립금 단건 상세. 권한: RESERVE_FUND READ. */
|
||||
@GetMapping("/{reserveId}")
|
||||
@RequirePermission(menu = "RESERVE_FUND", perm = "READ")
|
||||
public ApiResponse<AgentReserveFundResp> detail(@PathVariable Long reserveId) {
|
||||
return ApiResponse.ok(service.detail(reserveId));
|
||||
}
|
||||
|
||||
/** 적립금 내역 신규 등록(적립/사용 기록). 응답: reserveId. 권한: RESERVE_FUND CREATE. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "RESERVE_FUND", perm = "CREATE")
|
||||
@DataChangeLog(menu = "RESERVE_FUND", table = "agent_reserve_fund")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody AgentReserveFundSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.SettlingSupportService;
|
||||
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.commission.*;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 정착지원금 API.
|
||||
*
|
||||
* 정착지원금(settling support): 새로 들어온 설계사가 안정적으로 자리 잡을 때까지 일정 기간 지급하는 지원금.
|
||||
* 보통 실적이 충분치 않은 초기 기간 동안 최소 수입을 보전해 준다(조건 미달 시 환수될 수도 있음).
|
||||
* - Rule(지원금 규칙) CRUD - Ledger(지원금 원장) 조회/등록.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="SETTLING_SUPPORT"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "정착지원금")
|
||||
@RestController
|
||||
@RequestMapping("/api/settling-supports")
|
||||
@RequiredArgsConstructor
|
||||
public class SettlingSupportController {
|
||||
|
||||
private final SettlingSupportService service;
|
||||
|
||||
// ── Rule (정착지원금 규칙) ──────────────────────────────────────────────
|
||||
|
||||
/** 정착지원금 규칙 목록 조회. 권한: SETTLING_SUPPORT READ. */
|
||||
@GetMapping("/rules")
|
||||
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ")
|
||||
public ApiResponse<PageResponse<SettlingSupportRuleResp>> listRules(
|
||||
@ModelAttribute SettlingSupportRuleSearchParam param) {
|
||||
return ApiResponse.ok(service.listRules(param));
|
||||
}
|
||||
|
||||
/** 정착지원금 규칙 단건 상세. 권한: SETTLING_SUPPORT READ. */
|
||||
@GetMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ")
|
||||
public ApiResponse<SettlingSupportRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||
return ApiResponse.ok(service.detailRule(ruleId));
|
||||
}
|
||||
|
||||
/** 정착지원금 규칙 신규 등록. 응답: ruleId. 권한: SETTLING_SUPPORT CREATE. */
|
||||
@PostMapping("/rules")
|
||||
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "SETTLING_SUPPORT", table = "settling_support_rule")
|
||||
public ApiResponse<Long> createRule(@Valid @RequestBody SettlingSupportRuleSaveReq req) {
|
||||
return ApiResponse.ok(service.createRule(req));
|
||||
}
|
||||
|
||||
/** 정착지원금 규칙 수정. 권한: SETTLING_SUPPORT UPDATE. */
|
||||
@PutMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "SETTLING_SUPPORT", table = "settling_support_rule")
|
||||
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
|
||||
@Valid @RequestBody SettlingSupportRuleSaveReq req) {
|
||||
service.updateRule(ruleId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 정착지원금 규칙 삭제. 권한: SETTLING_SUPPORT DELETE. */
|
||||
@DeleteMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "DELETE")
|
||||
@DataChangeLog(menu = "SETTLING_SUPPORT", table = "settling_support_rule")
|
||||
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
|
||||
service.deleteRule(ruleId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ── Ledger (정착지원금 원장) ────────────────────────────────────────────
|
||||
|
||||
/** 정착지원금 원장 목록 조회. 권한: SETTLING_SUPPORT READ. */
|
||||
@GetMapping("/ledgers")
|
||||
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ")
|
||||
public ApiResponse<PageResponse<SettlingSupportLedgerResp>> listLedgers(
|
||||
@ModelAttribute SettlingSupportLedgerSearchParam param) {
|
||||
return ApiResponse.ok(service.listLedgers(param));
|
||||
}
|
||||
|
||||
/** 정착지원금 원장 단건 상세. 권한: SETTLING_SUPPORT READ. */
|
||||
@GetMapping("/ledgers/{ledgerId}")
|
||||
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ")
|
||||
public ApiResponse<SettlingSupportLedgerResp> detailLedger(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(service.detailLedger(ledgerId));
|
||||
}
|
||||
|
||||
/** 정착지원금 원장 신규 등록. 응답: ledgerId. 권한: SETTLING_SUPPORT CREATE. */
|
||||
@PostMapping("/ledgers")
|
||||
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "SETTLING_SUPPORT", table = "settling_support_ledger")
|
||||
public ApiResponse<Long> createLedger(@Valid @RequestBody SettlingSupportLedgerSaveReq req) {
|
||||
return ApiResponse.ok(service.createLedger(req));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.StatuteService;
|
||||
import com.ga.common.annotation.DataChangeLog;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.core.vo.commission.StatuteConfigResp;
|
||||
import com.ga.core.vo.commission.StatuteConfigSaveReq;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 환수시효관리 API.
|
||||
*
|
||||
* 시효(statute of limitations): 환수채권을 받아낼 수 있는 법적 기한. 기한이 지나면(시효 만료) 더 이상 회수할 수 없어
|
||||
* 손실(대손)로 처리해야 한다. 이 도메인은 시효 기준 설정(config)과 만료 건 일괄 정리를 담당한다.
|
||||
*
|
||||
* 권한: @RequirePermission(menu="STATUTE_CONFIG"), perm=READ/CREATE/UPDATE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "환수시효관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/statute")
|
||||
@RequiredArgsConstructor
|
||||
public class StatuteController {
|
||||
|
||||
private final StatuteService service;
|
||||
|
||||
/** 시효 설정 전체 목록 조회. 권한: STATUTE_CONFIG READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "STATUTE_CONFIG", perm = "READ")
|
||||
public ApiResponse<List<StatuteConfigResp>> list() {
|
||||
return ApiResponse.ok(service.listConfigs());
|
||||
}
|
||||
|
||||
/** 시효 설정 단건 상세. 권한: STATUTE_CONFIG READ. */
|
||||
@GetMapping("/{configId}")
|
||||
@RequirePermission(menu = "STATUTE_CONFIG", perm = "READ")
|
||||
public ApiResponse<StatuteConfigResp> detail(@PathVariable Long configId) {
|
||||
return ApiResponse.ok(service.detailConfig(configId));
|
||||
}
|
||||
|
||||
/** 시효 설정 신규 등록. 응답: configId. 권한: STATUTE_CONFIG CREATE. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "STATUTE_CONFIG", perm = "CREATE")
|
||||
@DataChangeLog(menu = "STATUTE_CONFIG", table = "statute_config")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody StatuteConfigSaveReq req) {
|
||||
return ApiResponse.ok(service.createConfig(req));
|
||||
}
|
||||
|
||||
/** 시효 설정 수정. 권한: STATUTE_CONFIG UPDATE. */
|
||||
@PutMapping("/{configId}")
|
||||
@RequirePermission(menu = "STATUTE_CONFIG", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "STATUTE_CONFIG", table = "statute_config")
|
||||
public ApiResponse<Void> update(@PathVariable Long configId,
|
||||
@Valid @RequestBody StatuteConfigSaveReq req) {
|
||||
service.updateConfig(configId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 만료 대상 환수채권 일괄 대손 처리. 기준월(baseMonth)까지 시효가 지난 환수채권을 한꺼번에 손실 처리하고
|
||||
* 처리 건수를 반환한다. baseMonth 미지정 시 현재 정산월을 사용한다.
|
||||
* Request body: { "baseMonth": "YYYYMM" }
|
||||
* 권한: STATUTE_CONFIG UPDATE.
|
||||
*/
|
||||
@PostMapping("/expire")
|
||||
@RequirePermission(menu = "STATUTE_CONFIG", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "STATUTE_CONFIG", table = "clawback_receivable")
|
||||
public ApiResponse<Integer> expire(@RequestBody Map<String, String> body) {
|
||||
// baseMonth 가 비어 있으면 현재 정산월로 대체
|
||||
String baseMonth = body.get("baseMonth");
|
||||
if (baseMonth == null || baseMonth.isBlank()) {
|
||||
baseMonth = com.ga.common.util.DateUtil.getSettleMonth();
|
||||
}
|
||||
return ApiResponse.ok(service.expireOverdue(baseMonth));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.StepCloseService;
|
||||
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.commission.StepCloseResp;
|
||||
import com.ga.core.vo.commission.StepCloseSaveReq;
|
||||
import com.ga.core.vo.commission.StepCloseSearchParam;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 단계마감 API.
|
||||
*
|
||||
* 단계마감(step close): 월 정산을 여러 단계로 나눠, 각 단계가 끝나면 "마감"해서 더 이상 수정 못 하도록 잠그는 것.
|
||||
* 마감(close) 후 수정이 필요하면 다시 reopen(마감 해제)할 수 있다. 마감/해제는 승인(APPROVE) 권한이 필요하다.
|
||||
* - close: 단계 마감(잠금) - reopen: 마감 해제(다시 열기)
|
||||
*
|
||||
* 권한: @RequirePermission(menu="STEP_CLOSE"), perm=READ/UPDATE/APPROVE. 변경은 @DataChangeLog 기록.
|
||||
*/
|
||||
@Tag(name = "단계마감")
|
||||
@RestController
|
||||
@RequestMapping("/api/step-closes")
|
||||
@RequiredArgsConstructor
|
||||
public class StepCloseController {
|
||||
|
||||
private final StepCloseService service;
|
||||
|
||||
/** 단계마감 목록 조회. 권한: STEP_CLOSE READ. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "STEP_CLOSE", perm = "READ")
|
||||
public ApiResponse<PageResponse<StepCloseResp>> list(@ModelAttribute StepCloseSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 특정 정산월(settleMonth)의 단계마감 현황 조회. 권한: STEP_CLOSE READ. */
|
||||
@GetMapping("/by-month")
|
||||
@RequirePermission(menu = "STEP_CLOSE", perm = "READ")
|
||||
public ApiResponse<List<StepCloseResp>> listByMonth(@RequestParam String settleMonth) {
|
||||
return ApiResponse.ok(service.listByMonth(settleMonth));
|
||||
}
|
||||
|
||||
/** 단계마감 단건 상세. 권한: STEP_CLOSE READ. */
|
||||
@GetMapping("/{stepCloseId}")
|
||||
@RequirePermission(menu = "STEP_CLOSE", perm = "READ")
|
||||
public ApiResponse<StepCloseResp> detail(@PathVariable Long stepCloseId) {
|
||||
return ApiResponse.ok(service.detail(stepCloseId));
|
||||
}
|
||||
|
||||
/** 단계마감 항목 신규 생성(아직 미마감 상태). 응답: stepCloseId. 권한: STEP_CLOSE UPDATE. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "STEP_CLOSE", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "STEP_CLOSE", table = "settlement_step_close")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody StepCloseSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 단계 마감 확정(해당 단계 잠금). 승인 권한 필요. 권한: STEP_CLOSE APPROVE. */
|
||||
@PostMapping("/{stepCloseId}/close")
|
||||
@RequirePermission(menu = "STEP_CLOSE", perm = "APPROVE")
|
||||
@DataChangeLog(menu = "STEP_CLOSE", table = "settlement_step_close")
|
||||
public ApiResponse<Void> close(@PathVariable Long stepCloseId) {
|
||||
service.close(stepCloseId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 단계 마감 해제(다시 수정 가능 상태로). 승인 권한 필요. 권한: STEP_CLOSE APPROVE. */
|
||||
@PostMapping("/{stepCloseId}/reopen")
|
||||
@RequirePermission(menu = "STEP_CLOSE", perm = "APPROVE")
|
||||
@DataChangeLog(menu = "STEP_CLOSE", table = "settlement_step_close")
|
||||
public ApiResponse<Void> reopen(@PathVariable Long stepCloseId) {
|
||||
service.reopen(stepCloseId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,15 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 민원 관리 API.
|
||||
*
|
||||
* <p>고객 민원을 접수·처리하는 도메인이다. 민원은 특정 보험계약(contractId)·담당 설계사(agentId)와 연결되며,
|
||||
* 상태는 OPEN(접수) → CLOSED(처리완료)로 전이된다. 환수 유발(chargebackTriggered=true) 민원은
|
||||
* 처리 후 정산 배치(P5)가 실제 수수료 환수(chargeback)를 생성한다(여기서는 플래그만 관리).</p>
|
||||
*
|
||||
* <p>메뉴 COMPLAINT 권한으로 보호된다.</p>
|
||||
*/
|
||||
@Tag(name = "민원 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/complaints")
|
||||
@@ -23,18 +32,21 @@ public class ComplaintController {
|
||||
|
||||
private final ComplaintService service;
|
||||
|
||||
/** 민원 목록 조회(페이징). READ 권한 필요. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "COMPLAINT", perm = "READ")
|
||||
public ApiResponse<PageResponse<ComplaintResp>> list(@ModelAttribute ComplaintSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 민원 단건 상세 조회. READ 권한 필요. */
|
||||
@GetMapping("/{complaintId}")
|
||||
@RequirePermission(menu = "COMPLAINT", perm = "READ")
|
||||
public ApiResponse<ComplaintResp> detail(@PathVariable Long complaintId) {
|
||||
return ApiResponse.ok(service.detail(complaintId));
|
||||
}
|
||||
|
||||
/** 민원 신규 접수. 등록 시 OPEN 상태로 시작한다. CREATE 권한 필요. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "COMPLAINT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "COMPLAINT", table = "complaint")
|
||||
@@ -42,6 +54,7 @@ public class ComplaintController {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 민원 내용 수정. 상태는 변경하지 않는다(처리완료는 별도 엔드포인트). UPDATE 권한 필요. */
|
||||
@PutMapping("/{complaintId}")
|
||||
@RequirePermission(menu = "COMPLAINT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "COMPLAINT", table = "complaint")
|
||||
@@ -51,6 +64,7 @@ public class ComplaintController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 민원 처리완료(status=CLOSED). 처리결과(resolution)를 기록하며 이미 완료된 건은 거부된다. UPDATE 권한 필요. */
|
||||
@PostMapping("/{complaintId}/close")
|
||||
@RequirePermission(menu = "COMPLAINT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "COMPLAINT", table = "complaint")
|
||||
@@ -60,6 +74,7 @@ public class ComplaintController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 민원 레코드 삭제. DELETE 권한 필요. */
|
||||
@DeleteMapping("/{complaintId}")
|
||||
@RequirePermission(menu = "COMPLAINT", perm = "DELETE")
|
||||
@DataChangeLog(menu = "COMPLAINT", table = "complaint")
|
||||
|
||||
@@ -15,6 +15,16 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 위촉계약 관리 API.
|
||||
*
|
||||
* <p>위촉계약(agent_contract)은 설계사와 보험사(carrier) 사이의 모집 위탁 계약이다.
|
||||
* 어떤 설계사가 어떤 보험사의 상품을 팔 수 있는지를 정의하며, 유효기간(effectiveFrom~To)과
|
||||
* 정착지원금(onboardingBonusAmount) 등을 포함한다. 상태는 ACTIVE(유효)에서 TERMINATED(해촉)로 전이된다.</p>
|
||||
*
|
||||
* <p>주의: 여기서 말하는 계약은 "설계사 위촉계약"이며, 고객이 가입한 "보험계약"과는 다르다.
|
||||
* 메뉴 AGENT_CONTRACT 권한으로 보호된다.</p>
|
||||
*/
|
||||
@Tag(name = "위촉계약 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/agent-contracts")
|
||||
@@ -23,18 +33,21 @@ public class AgentContractController {
|
||||
|
||||
private final AgentContractService service;
|
||||
|
||||
/** 위촉계약 목록 조회(페이징). READ 권한 필요. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "AGENT_CONTRACT", perm = "READ")
|
||||
public ApiResponse<PageResponse<AgentContractResp>> list(@ModelAttribute AgentContractSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 위촉계약 단건 상세 조회. READ 권한 필요. */
|
||||
@GetMapping("/{contractId}")
|
||||
@RequirePermission(menu = "AGENT_CONTRACT", perm = "READ")
|
||||
public ApiResponse<AgentContractResp> detail(@PathVariable Long contractId) {
|
||||
return ApiResponse.ok(service.detail(contractId));
|
||||
}
|
||||
|
||||
/** 위촉계약 신규 등록. 등록 시 ACTIVE 상태로 시작한다. CREATE 권한 필요. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "AGENT_CONTRACT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "AGENT_CONTRACT", table = "agent_contract")
|
||||
@@ -42,6 +55,7 @@ public class AgentContractController {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 위촉계약 수정. 상태는 변경하지 않는다(해촉은 별도 엔드포인트). UPDATE 권한 필요. */
|
||||
@PutMapping("/{contractId}")
|
||||
@RequirePermission(menu = "AGENT_CONTRACT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "AGENT_CONTRACT", table = "agent_contract")
|
||||
@@ -51,6 +65,7 @@ public class AgentContractController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 위촉계약 해촉(status=TERMINATED). 사유(reason)를 함께 기록하며 이미 해촉된 건은 거부된다. UPDATE 권한 필요. */
|
||||
@PostMapping("/{contractId}/terminate")
|
||||
@RequirePermission(menu = "AGENT_CONTRACT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "AGENT_CONTRACT", table = "agent_contract")
|
||||
@@ -60,6 +75,7 @@ public class AgentContractController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 위촉계약 레코드 삭제. DELETE 권한 필요. */
|
||||
@DeleteMapping("/{contractId}")
|
||||
@RequirePermission(menu = "AGENT_CONTRACT", perm = "DELETE")
|
||||
@DataChangeLog(menu = "AGENT_CONTRACT", table = "agent_contract")
|
||||
|
||||
@@ -13,6 +13,18 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 공제/차감(agent_deduction) 관리 컨트롤러.
|
||||
*
|
||||
* <p>"차감"은 설계사에게 줄 수수료에서 미리 빼야 할 금액(예: 대출 상환, 가지급금 회수 등)을 등록해 두는 것입니다.
|
||||
* 여기서 PENDING(대기) 상태로 등록해 두면, 지급 처리 단계에서 실지급액(netAmount)에서 자동으로 차감됩니다.
|
||||
*
|
||||
* <p>상태 흐름(DeductionStatus): PENDING(대기) → APPLIED(적용완료), 또는 CANCELLED(취소).
|
||||
* 수정·취소는 아직 적용(APPLIED)되지 않은 PENDING 건만 가능합니다.
|
||||
* 같은 설계사+정산월+차감유형 조합은 중복 등록이 거부됩니다.
|
||||
*
|
||||
* <p>권한(menu=DEDUCTION): 조회 READ, 등록 CREATE, 수정/취소 UPDATE.
|
||||
*/
|
||||
@Tag(name = "공제 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/deductions")
|
||||
@@ -21,18 +33,21 @@ 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));
|
||||
}
|
||||
|
||||
/** 차감 등록(PENDING 상태). 동일 설계사+정산월+차감유형 중복은 거부됩니다. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "DEDUCTION", perm = "CREATE")
|
||||
@DataChangeLog(menu = "DEDUCTION", table = "agent_deduction")
|
||||
@@ -40,6 +55,7 @@ public class DeductionController {
|
||||
return ApiResponse.ok(deductionService.create(req));
|
||||
}
|
||||
|
||||
/** 차감 금액·설명 수정. PENDING 상태에서만 가능합니다. */
|
||||
@PutMapping("/{deductionId}")
|
||||
@RequirePermission(menu = "DEDUCTION", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "DEDUCTION", table = "agent_deduction")
|
||||
@@ -49,6 +65,7 @@ public class DeductionController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 차감 취소(→ CANCELLED). 이미 적용(APPLIED)된 차감은 취소할 수 없습니다. */
|
||||
@PutMapping("/{deductionId}/cancel")
|
||||
@RequirePermission(menu = "DEDUCTION", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "DEDUCTION", table = "agent_deduction")
|
||||
|
||||
@@ -16,6 +16,13 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 데이터 사전(Data Dictionary) 관리 API.
|
||||
*
|
||||
* <p>DB 테이블/컬럼의 의미·도메인 코드 매핑 등을 문서화해 관리하는 메타데이터 도구다.
|
||||
* 각 항목(dictId)은 특정 테이블의 컬럼을 설명하며, 도메인 코드(domainCode)와 연결될 수 있다.
|
||||
* 메뉴 SYSTEM_DATA_DICT 권한으로 보호된다.</p>
|
||||
*/
|
||||
@Tag(name = "데이터 사전")
|
||||
@RestController
|
||||
@RequestMapping("/api/data-dictionary")
|
||||
@@ -24,18 +31,21 @@ public class DataDictionaryController {
|
||||
|
||||
private final DataDictionaryService dataDictionaryService;
|
||||
|
||||
/** 데이터 사전 항목 목록 조회(페이징). READ 권한 필요. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "READ")
|
||||
public ApiResponse<PageResponse<DataDictionaryResp>> list(@ModelAttribute DataDictionarySearchParam param) {
|
||||
return ApiResponse.ok(dataDictionaryService.list(param));
|
||||
}
|
||||
|
||||
/** 사전에 등록된 테이블명 목록 조회. READ 권한 필요. */
|
||||
@GetMapping("/tables")
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "READ")
|
||||
public ApiResponse<List<String>> tables() {
|
||||
return ApiResponse.ok(dataDictionaryService.tables());
|
||||
}
|
||||
|
||||
/** 사전 뷰 기반 통합 조회(테이블명 선택 필터). 컬럼+도메인 결합 정보를 반환한다. READ 권한 필요. */
|
||||
@GetMapping("/full")
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "READ")
|
||||
public ApiResponse<List<DataDictionaryFullResp>> full(
|
||||
@@ -43,12 +53,14 @@ public class DataDictionaryController {
|
||||
return ApiResponse.ok(dataDictionaryService.full(tableName));
|
||||
}
|
||||
|
||||
/** 데이터 사전 항목 단건 조회. READ 권한 필요. */
|
||||
@GetMapping("/{dictId}")
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "READ")
|
||||
public ApiResponse<DataDictionaryResp> detail(@PathVariable Long dictId) {
|
||||
return ApiResponse.ok(dataDictionaryService.detail(dictId));
|
||||
}
|
||||
|
||||
/** 데이터 사전 항목 등록. 도메인 코드를 지정하면 실존 여부를 검증한다. CREATE 권한 필요. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "SYSTEM_DATA_DICT", table = "data_dictionary")
|
||||
@@ -57,6 +69,7 @@ public class DataDictionaryController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 데이터 사전 항목 수정. UPDATE 권한 필요. */
|
||||
@PutMapping("/{dictId}")
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "SYSTEM_DATA_DICT", table = "data_dictionary")
|
||||
@@ -66,6 +79,7 @@ public class DataDictionaryController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 데이터 사전 항목 삭제. DELETE 권한 필요. */
|
||||
@DeleteMapping("/{dictId}")
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "DELETE")
|
||||
@DataChangeLog(menu = "SYSTEM_DATA_DICT", table = "data_dictionary")
|
||||
|
||||
@@ -16,6 +16,13 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 데이터 도메인 사전 관리 API.
|
||||
*
|
||||
* <p>도메인(domain)은 컬럼들이 공통으로 따르는 데이터 타입/형식 규칙의 정의다(예: 금액형, 날짜형 등).
|
||||
* 데이터 사전(data_dictionary)의 컬럼들이 이 도메인 코드를 참조한다. 식별자는 문자열 도메인 코드(domainCode)다.
|
||||
* 메뉴 SYSTEM_DATA_DOMAIN 권한으로 보호된다.</p>
|
||||
*/
|
||||
@Tag(name = "데이터 도메인 사전")
|
||||
@RestController
|
||||
@RequestMapping("/api/data-domains")
|
||||
@@ -24,24 +31,28 @@ public class DataDomainController {
|
||||
|
||||
private final DataDomainService dataDomainService;
|
||||
|
||||
/** 도메인 목록 조회(페이징). READ 권한 필요. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DOMAIN", perm = "READ")
|
||||
public ApiResponse<PageResponse<DataDomainResp>> list(@ModelAttribute DataDomainSearchParam param) {
|
||||
return ApiResponse.ok(dataDomainService.list(param));
|
||||
}
|
||||
|
||||
/** 선택 옵션용 도메인 전체 목록(코드/명칭 등 경량 정보). READ 권한 필요. */
|
||||
@GetMapping("/options")
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DOMAIN", perm = "READ")
|
||||
public ApiResponse<List<DataDomainOptionResp>> options() {
|
||||
return ApiResponse.ok(dataDomainService.options());
|
||||
}
|
||||
|
||||
/** 도메인 단건 상세 조회(도메인 코드 기준). READ 권한 필요. */
|
||||
@GetMapping("/{domainCode}")
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DOMAIN", perm = "READ")
|
||||
public ApiResponse<DataDomainResp> detail(@PathVariable String domainCode) {
|
||||
return ApiResponse.ok(dataDomainService.detail(domainCode));
|
||||
}
|
||||
|
||||
/** 도메인 등록. 코드 중복이면 거부된다. CREATE 권한 필요. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DOMAIN", perm = "CREATE")
|
||||
@DataChangeLog(menu = "SYSTEM_DATA_DOMAIN", table = "data_domain")
|
||||
@@ -50,6 +61,7 @@ public class DataDomainController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 도메인 수정. UPDATE 권한 필요. */
|
||||
@PutMapping("/{domainCode}")
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DOMAIN", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "SYSTEM_DATA_DOMAIN", table = "data_domain")
|
||||
@@ -59,6 +71,7 @@ public class DataDomainController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 도메인 삭제. 이 도메인을 참조하는 컬럼이 있으면 삭제 불가(서비스에서 검증). DELETE 권한 필요. */
|
||||
@DeleteMapping("/{domainCode}")
|
||||
@RequirePermission(menu = "SYSTEM_DATA_DOMAIN", perm = "DELETE")
|
||||
@DataChangeLog(menu = "SYSTEM_DATA_DOMAIN", table = "data_domain")
|
||||
|
||||
@@ -13,6 +13,15 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 계약 변경(배서, Endorsement) 관리 API.
|
||||
*
|
||||
* <p>배서란 이미 체결된 계약의 내용을 바꾸는 것을 말한다(예: 보험료 증감, 피보험자/수익자 변경, 담보 추가 등).
|
||||
* 변경 유형(endorsementType)과 변경 전/후 값(oldValue/newValue)을 기록하며,
|
||||
* 보험료가 바뀌어 수수료 재계산이 필요한 경우(requiresCommissionRecalc) 정산 배치가 이를 반영한다.</p>
|
||||
*
|
||||
* <p>메뉴 ENDORSE 권한으로 보호된다.</p>
|
||||
*/
|
||||
@Tag(name = "계약 변경")
|
||||
@RestController
|
||||
@RequestMapping("/api/contract-endorsements")
|
||||
@@ -21,6 +30,7 @@ public class ContractEndorsementController {
|
||||
|
||||
private final ContractEndorsementService service;
|
||||
|
||||
/** 배서 목록 조회(페이징). READ 권한 필요. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "ENDORSE", perm = "READ")
|
||||
public ApiResponse<PageResponse<ContractEndorsementResp>> list(
|
||||
@@ -28,12 +38,14 @@ public class ContractEndorsementController {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 배서 단건 상세 조회. READ 권한 필요. */
|
||||
@GetMapping("/{endorsementId}")
|
||||
@RequirePermission(menu = "ENDORSE", perm = "READ")
|
||||
public ApiResponse<ContractEndorsementResp> detail(@PathVariable Long endorsementId) {
|
||||
return ApiResponse.ok(service.detail(endorsementId));
|
||||
}
|
||||
|
||||
/** 배서 등록. 등록 직후엔 미처리(processed=false) 상태다. CREATE 권한 필요. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "ENDORSE", perm = "CREATE")
|
||||
@DataChangeLog(menu = "ENDORSE", table = "contract_endorsement")
|
||||
@@ -41,6 +53,7 @@ public class ContractEndorsementController {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 배서 처리 완료 표시(processed=true). 실제 수수료 재계산은 배치(P5)가 담당한다. UPDATE 권한 필요. */
|
||||
@PostMapping("/{endorsementId}/process")
|
||||
@RequirePermission(menu = "ENDORSE", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "ENDORSE", table = "contract_endorsement")
|
||||
@@ -49,6 +62,7 @@ public class ContractEndorsementController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 배서 레코드 삭제. DELETE 권한 필요. */
|
||||
@DeleteMapping("/{endorsementId}")
|
||||
@RequirePermission(menu = "ENDORSE", perm = "DELETE")
|
||||
@DataChangeLog(menu = "ENDORSE", table = "contract_endorsement")
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.ga.api.controller.external;
|
||||
|
||||
import com.ga.api.service.external.ExternalCallLogService;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.external.ExternalCallLogResp;
|
||||
import com.ga.core.vo.external.ExternalCallLogSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 외부연동 호출 이력 조회 API.
|
||||
*
|
||||
* <p>SMS/알림톡, 펌뱅킹, 푸시 등 외부 시스템 호출 결과를 감사용으로 기록한 이력을 조회한다(읽기 전용).
|
||||
* 이력 적재 자체는 AOP가 자동 수행하며, 여기서는 운영/장애 추적용 조회만 제공한다.
|
||||
* 메뉴 SYSTEM_EXTERNAL_CALL 권한으로 보호된다.</p>
|
||||
*/
|
||||
@Tag(name = "외부연동 호출이력")
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class ExternalCallLogController {
|
||||
|
||||
private final ExternalCallLogService service;
|
||||
|
||||
/** 외부 호출 이력 목록 조회(페이징). READ 권한 필요. */
|
||||
@GetMapping("/api/external-call-logs")
|
||||
@RequirePermission(menu = "SYSTEM_EXTERNAL_CALL", perm = "READ")
|
||||
public ApiResponse<PageResponse<ExternalCallLogResp>> list(@ModelAttribute ExternalCallLogSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 외부 호출 이력 단건 상세 조회. READ 권한 필요. */
|
||||
@GetMapping("/api/external-call-logs/{logId}")
|
||||
@RequirePermission(menu = "SYSTEM_EXTERNAL_CALL", perm = "READ")
|
||||
public ApiResponse<ExternalCallLogResp> detail(@PathVariable Long logId) {
|
||||
return ApiResponse.ok(service.detail(logId));
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,14 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* [연간 사업소득 API]
|
||||
*
|
||||
* 신입 개발자 안내:
|
||||
* - 설계사별로 한 해(연도) 동안의 총 사업소득과 떼인 원천세 등을 합산해 보관하는 자료다(연말정산/지급명세서의 기초).
|
||||
* - 월별 지급/정산 데이터를 연 단위로 집계(regenerate)하여 agent_annual_income 에 UPSERT 한다.
|
||||
* - 상세는 (설계사ID, 정산연도) 조합으로 1건을 조회한다.
|
||||
*/
|
||||
@Tag(name = "연간 사업소득")
|
||||
@RestController
|
||||
@RequestMapping("/api/agent-annual-incomes")
|
||||
@@ -37,6 +45,7 @@ public class AgentAnnualIncomeController {
|
||||
return ApiResponse.ok(service.detail(agentId, settleYear));
|
||||
}
|
||||
|
||||
// 지정 연도의 모든 설계사 연간소득을 다시 집계(UPSERT). 월별 데이터가 바뀌었을 때 다시 돌린다.
|
||||
@PostMapping("/regenerate")
|
||||
@Operation(summary = "연도 전체 재집계")
|
||||
@RequirePermission(menu = "ANNUAL_INCOME", perm = "EXECUTE")
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.ga.api.controller.income;
|
||||
|
||||
import com.ga.api.service.income.YearEndStatementService;
|
||||
import com.ga.common.annotation.DataChangeLog;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.excel.ExcelService;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.income.YearEndStatementExcelVO;
|
||||
import com.ga.core.vo.income.YearEndStatementResp;
|
||||
import com.ga.core.vo.income.YearEndStatementSaveReq;
|
||||
import com.ga.core.vo.income.YearEndStatementSearchParam;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* [연말 지급명세서 API]
|
||||
*
|
||||
* 신입 개발자 안내:
|
||||
* - 연말정산을 위해 "한 해 동안 이 설계사에게 얼마를 지급하고 세금을 얼마 뗐는지"를 국세청 제출용으로 정리한 명세서다.
|
||||
* - 데이터 출처: 연간 사업소득(agent_annual_income) 집계 결과를 기반으로 한다.
|
||||
* - 흐름: regenerate(연도별 UPSERT 생성) → generate(단건 파일 발급) → send(설계사에게 발송 처리) → export(엑셀 다운로드).
|
||||
* - export 는 데이터가 많을 수 있어 Cursor 스트리밍(ExcelService.exportLargeExcel)으로 내려보낸다.
|
||||
*/
|
||||
@Tag(name = "연말 지급명세서")
|
||||
@RestController
|
||||
@RequestMapping("/api/year-end-statements")
|
||||
@RequiredArgsConstructor
|
||||
public class YearEndStatementController {
|
||||
|
||||
private final YearEndStatementService service;
|
||||
private final ExcelService excelService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "연말 지급명세서 목록")
|
||||
@RequirePermission(menu = "YEAR_END_STMT", perm = "READ")
|
||||
public ApiResponse<PageResponse<YearEndStatementResp>> list(@ModelAttribute YearEndStatementSearchParam p) {
|
||||
return ApiResponse.ok(service.list(p));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "연말 지급명세서 상세")
|
||||
@RequirePermission(menu = "YEAR_END_STMT", perm = "READ")
|
||||
public ApiResponse<YearEndStatementResp> detail(@PathVariable Long id) {
|
||||
return ApiResponse.ok(service.detail(id));
|
||||
}
|
||||
|
||||
@PostMapping("/regenerate")
|
||||
@Operation(summary = "연도별 재집계 (UPSERT)")
|
||||
@RequirePermission(menu = "YEAR_END_STMT", perm = "EXECUTE")
|
||||
@DataChangeLog(menu = "YEAR_END_STMT", table = "year_end_statement")
|
||||
public ApiResponse<Integer> regenerate(@RequestBody YearEndStatementSaveReq req) {
|
||||
return ApiResponse.ok(service.regenerate(req));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/generate")
|
||||
@Operation(summary = "단건 파일 발급")
|
||||
@RequirePermission(menu = "YEAR_END_STMT", perm = "EXECUTE")
|
||||
@DataChangeLog(menu = "YEAR_END_STMT", table = "year_end_statement")
|
||||
public ApiResponse<Void> generate(@PathVariable Long id, @RequestBody(required = false) Map<String, String> body) {
|
||||
String format = body == null ? null : body.get("fileFormat");
|
||||
service.generate(id, format);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PutMapping("/send")
|
||||
@Operation(summary = "발송 처리")
|
||||
@RequirePermission(menu = "YEAR_END_STMT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "YEAR_END_STMT", table = "year_end_statement")
|
||||
public ApiResponse<Void> send(@RequestBody YearEndStatementSaveReq req) {
|
||||
service.send(req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// 대용량 엑셀 다운로드: 조회 결과를 Cursor 로 스트리밍하며 한 번에 메모리에 올리지 않고 파일로 내려쓴다.
|
||||
@GetMapping("/export")
|
||||
@Operation(summary = "엑셀 다운로드")
|
||||
@RequirePermission(menu = "YEAR_END_STMT", perm = "EXPORT")
|
||||
public void export(@ModelAttribute YearEndStatementSearchParam p, HttpServletResponse res) {
|
||||
excelService.exportLargeExcel(
|
||||
"year-end-statement.xlsx",
|
||||
YearEndStatementExcelVO.class,
|
||||
() -> service.exportCursor(p),
|
||||
res);
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,27 @@ import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 분급(分給) 계획 생성 요청 DTO.
|
||||
* 분급이란 총수수료를 한 번에 주지 않고 여러 회차로 나눠 지급하는 것으로,
|
||||
* 이 요청을 받아 회차별 지급 스케줄(installment_plan)을 자동 생성한다.
|
||||
*/
|
||||
@Data
|
||||
public class GeneratePlanReq {
|
||||
|
||||
/** 분급 대상 계약 ID. */
|
||||
@NotNull
|
||||
private Long contractId;
|
||||
|
||||
/** 나눠서 지급할 총수수료 금액(양수). 분급 비율에 따라 회차별로 쪼개진다. */
|
||||
@NotNull
|
||||
@Positive
|
||||
private BigDecimal totalCommission;
|
||||
|
||||
/** 계약일. 회차별 지급 예정월 계산의 기준이 된다. */
|
||||
@NotNull
|
||||
private LocalDate contractDate;
|
||||
|
||||
/** 상품 분류. 분류에 따라 적용할 분급 비율표(installment_ratio)가 달라진다. */
|
||||
private String productCategory;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 분급(分給) 계획 관리 REST 컨트롤러.
|
||||
* 총수수료를 여러 회차로 나눠 지급하는 분급 계획(installment_plan)의 조회·생성·지급/이연/취소를 처리한다.
|
||||
*/
|
||||
@Tag(name = "분급 계획 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/installment-plans")
|
||||
@@ -23,18 +27,21 @@ public class InstallmentController {
|
||||
|
||||
private final InstallmentService installmentService;
|
||||
|
||||
/** 분급 계획(회차) 목록 페이징 조회. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "READ")
|
||||
public ApiResponse<PageResponse<InstallmentPlanResp>> list(InstallmentPlanSearchParam param) {
|
||||
return ApiResponse.ok(installmentService.list(param));
|
||||
}
|
||||
|
||||
/** 분급 계획 단건 상세 조회. */
|
||||
@GetMapping("/{planId}")
|
||||
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "READ")
|
||||
public ApiResponse<InstallmentPlanResp> detail(@PathVariable Long planId) {
|
||||
return ApiResponse.ok(installmentService.detail(planId));
|
||||
}
|
||||
|
||||
/** 분급 계획 생성. 총수수료를 분급 비율표에 따라 회차별 지급 스케줄로 자동 분해한다. */
|
||||
@PostMapping("/generate")
|
||||
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "CREATE")
|
||||
@DataChangeLog(menu = "INSTALLMENT_PLAN", table = "installment_plan")
|
||||
@@ -48,6 +55,7 @@ public class InstallmentController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 특정 회차 지급 완료 처리(지급건 ID와 지급액 기록). */
|
||||
@PutMapping("/{planId}/pay")
|
||||
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "INSTALLMENT_PLAN", table = "installment_plan")
|
||||
@@ -57,6 +65,7 @@ public class InstallmentController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 특정 회차 지급 이연(다음 기회로 미룸) 처리. */
|
||||
@PutMapping("/{planId}/defer")
|
||||
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "INSTALLMENT_PLAN", table = "installment_plan")
|
||||
@@ -65,6 +74,7 @@ public class InstallmentController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 특정 회차 지급 취소(예: 계약 해지로 향후 분급 중단). */
|
||||
@PutMapping("/{planId}/cancel")
|
||||
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "INSTALLMENT_PLAN", table = "installment_plan")
|
||||
@@ -73,6 +83,7 @@ public class InstallmentController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 특정 정산월(settleMonth)에 지급 예정인 분급 회차 목록 조회. 월 정산 배치가 이 목록을 사용한다. */
|
||||
@GetMapping("/scheduled")
|
||||
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "READ")
|
||||
public ApiResponse<List<InstallmentPlanVO>> scheduled(@RequestParam String settleMonth) {
|
||||
|
||||
@@ -13,6 +13,11 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 분급 비율(installment_ratio) 관리 REST 컨트롤러.
|
||||
* 분급 비율표는 "총수수료를 몇 회차에 걸쳐, 각 회차 몇 %씩 지급할지"를 정의하는 기준표이며,
|
||||
* 분급 계획(installment_plan) 생성 시 이 비율이 적용된다.
|
||||
*/
|
||||
@Tag(name = "분급 비율 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/installment-ratios")
|
||||
@@ -21,12 +26,14 @@ public class InstallmentRatioController {
|
||||
|
||||
private final InstallmentService installmentService;
|
||||
|
||||
/** 분급 비율표 목록 페이징 조회. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "INSTALLMENT_RATIO", perm = "READ")
|
||||
public ApiResponse<PageResponse<InstallmentRatioResp>> list(InstallmentRatioSearchParam param) {
|
||||
return ApiResponse.ok(installmentService.listRatios(param));
|
||||
}
|
||||
|
||||
/** 분급 비율표 신규 등록. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "INSTALLMENT_RATIO", perm = "CREATE")
|
||||
@DataChangeLog(menu = "INSTALLMENT_RATIO", table = "installment_ratio")
|
||||
|
||||
@@ -6,12 +6,18 @@ import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 분급 회차 지급 처리 요청 DTO.
|
||||
* 특정 회차(plan)를 실제로 지급 완료 처리할 때, 어떤 지급건(paymentId)으로 얼마(paidAmount)를 지급했는지 전달한다.
|
||||
*/
|
||||
@Data
|
||||
public class PayPlanReq {
|
||||
|
||||
/** 실제 지급이 일어난 지급(payment) 레코드 ID. */
|
||||
@NotNull
|
||||
private Long paymentId;
|
||||
|
||||
/** 이번 회차에 지급한 금액(양수). */
|
||||
@NotNull
|
||||
@Positive
|
||||
private BigDecimal paidAmount;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ga.api.controller.internal;
|
||||
|
||||
import com.ga.api.service.withdraw.WithdrawBankRetryService;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 운영용 내부 트리거. 펌뱅킹 재시도를 수동으로 실행한다.
|
||||
* cron 활성화 환경에서는 자동 실행되지만 운영 중 즉시 회수가 필요할 때 사용.
|
||||
*/
|
||||
@Tag(name = "내부 트리거")
|
||||
@RestController
|
||||
@RequestMapping("/api/internal/bank-retry")
|
||||
@RequiredArgsConstructor
|
||||
public class BankRetryController {
|
||||
|
||||
private final WithdrawBankRetryService bankRetryService;
|
||||
|
||||
/** 펌뱅킹 이체 실패(FAILED) 건을 즉시 1회 재시도하고 처리 건수를 반환한다. WITHDRAW EXECUTE 권한 필요. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "WITHDRAW", perm = "EXECUTE")
|
||||
public ApiResponse<Integer> retry() {
|
||||
return ApiResponse.ok(bankRetryService.retryOnce());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ga.api.controller.internal;
|
||||
|
||||
import com.ga.api.service.withdraw.WithdrawBankSettleService;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 펌뱅킹 SETTLED 동기화 수동 트리거.
|
||||
* cron 활성화 환경에서는 자동 실행되지만 운영 중 즉시 입금 확정 동기화가 필요할 때 사용.
|
||||
*/
|
||||
@Tag(name = "내부 트리거")
|
||||
@RestController
|
||||
@RequestMapping("/api/internal/bank-settle")
|
||||
@RequiredArgsConstructor
|
||||
public class BankSettleController {
|
||||
|
||||
private final WithdrawBankSettleService bankSettleService;
|
||||
|
||||
/** 펌뱅킹 입금 확정(SETTLED) 상태를 즉시 1회 동기화하고 처리 건수를 반환한다. WITHDRAW EXECUTE 권한 필요. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "WITHDRAW", perm = "EXECUTE")
|
||||
public ApiResponse<Integer> sync() {
|
||||
return ApiResponse.ok(bankSettleService.syncOnce());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ga.api.controller.kpi;
|
||||
|
||||
import com.ga.api.service.kpi.ForecastKpiMonthlyService;
|
||||
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.kpi.ForecastKpiMonthlyResp;
|
||||
import com.ga.core.vo.kpi.ForecastKpiMonthlySaveReq;
|
||||
import com.ga.core.vo.kpi.ForecastKpiMonthlySearchParam;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 차월(다음 달) KPI 예측 REST 컨트롤러.
|
||||
* 과거 실적 데이터를 이동평균(SMA) 등으로 분석해 다음 달 실적 KPI를 미리 예측·관리한다.
|
||||
*/
|
||||
@Tag(name = "차월 KPI 예측")
|
||||
@RestController
|
||||
@RequestMapping("/api/forecast-kpi")
|
||||
@RequiredArgsConstructor
|
||||
public class ForecastKpiMonthlyController {
|
||||
|
||||
private final ForecastKpiMonthlyService service;
|
||||
|
||||
/** 예측값 목록 페이징 조회. */
|
||||
@GetMapping
|
||||
@Operation(summary = "예측 목록")
|
||||
@RequirePermission(menu = "FORECAST_KPI", perm = "READ")
|
||||
public ApiResponse<PageResponse<ForecastKpiMonthlyResp>> list(@ModelAttribute ForecastKpiMonthlySearchParam p) {
|
||||
return ApiResponse.ok(service.list(p));
|
||||
}
|
||||
|
||||
/** 차월 예측 재산출(실적 변동 시 다시 계산). 갱신된 행 수를 반환한다. */
|
||||
@PostMapping("/regenerate")
|
||||
@Operation(summary = "차월 예측 재산출")
|
||||
@RequirePermission(menu = "FORECAST_KPI", perm = "EXECUTE")
|
||||
@DataChangeLog(menu = "FORECAST_KPI", table = "forecast_kpi_monthly")
|
||||
public ApiResponse<Integer> regenerate(@Valid @RequestBody ForecastKpiMonthlySaveReq req) {
|
||||
return ApiResponse.ok(service.regenerate(req));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,14 @@ import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* KPI 대시보드 REST 컨트롤러.
|
||||
* 실적 집계(KPI)를 여러 관점으로 조회하는 읽기 전용 화면용 API다.
|
||||
* - monthly-summary : 월별 실적 요약
|
||||
* - org-summary : 조직(지점/본부)별 실적 요약
|
||||
* - retention : 계약 유지율 지표
|
||||
* - cumulative : 누적 실적 추이
|
||||
*/
|
||||
@Tag(name = "KPI 대시보드")
|
||||
@RestController
|
||||
@RequestMapping("/api/kpi")
|
||||
@@ -24,24 +32,28 @@ public class KpiController {
|
||||
|
||||
private final KpiService service;
|
||||
|
||||
/** 월별 실적 요약 조회. */
|
||||
@GetMapping("/monthly-summary")
|
||||
@RequirePermission(menu = "KPI_MONTHLY", perm = "READ")
|
||||
public ApiResponse<PageResponse<KpiMonthlySummaryResp>> monthlySummary(@ModelAttribute KpiSearchParam param) {
|
||||
return ApiResponse.ok(service.monthlySummary(param));
|
||||
}
|
||||
|
||||
/** 조직별 실적 요약 조회. */
|
||||
@GetMapping("/org-summary")
|
||||
@RequirePermission(menu = "KPI_ORG", perm = "READ")
|
||||
public ApiResponse<PageResponse<KpiOrgSummaryResp>> orgSummary(@ModelAttribute KpiSearchParam param) {
|
||||
return ApiResponse.ok(service.orgSummary(param));
|
||||
}
|
||||
|
||||
/** 계약 유지율 지표 조회. */
|
||||
@GetMapping("/retention")
|
||||
@RequirePermission(menu = "KPI_RETENTION", perm = "READ")
|
||||
public ApiResponse<PageResponse<KpiRetentionResp>> retention(@ModelAttribute KpiSearchParam param) {
|
||||
return ApiResponse.ok(service.retention(param));
|
||||
}
|
||||
|
||||
/** 누적 실적 추이 조회. */
|
||||
@GetMapping("/cumulative")
|
||||
@RequirePermission(menu = "KPI_CUMULATIVE", perm = "READ")
|
||||
public ApiResponse<PageResponse<KpiCumulativeResp>> cumulative(@ModelAttribute KpiSearchParam param) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.ga.api.controller.kpi;
|
||||
|
||||
import com.ga.api.service.kpi.RetentionAnomalyAlertService;
|
||||
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.kpi.RetentionAnomalyAlertResp;
|
||||
import com.ga.core.vo.kpi.RetentionAnomalyAlertSaveReq;
|
||||
import com.ga.core.vo.kpi.RetentionAnomalyAlertSearchParam;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 유지율 이상치(retention anomaly) 알림 REST 컨트롤러.
|
||||
* 특정 조직/설계사의 계약 유지율이 평소 대비 비정상적으로 떨어진 경우를 탐지해 알림으로 관리한다.
|
||||
* (유지율 급락은 부실모집/불완전판매 신호일 수 있어 모니터링 대상이다)
|
||||
*/
|
||||
@Tag(name = "유지율 이상치")
|
||||
@RestController
|
||||
@RequestMapping("/api/retention-anomalies")
|
||||
@RequiredArgsConstructor
|
||||
public class RetentionAnomalyAlertController {
|
||||
|
||||
private final RetentionAnomalyAlertService service;
|
||||
|
||||
/** 이상치 알림 목록 페이징 조회. */
|
||||
@GetMapping
|
||||
@Operation(summary = "이상치 목록")
|
||||
@RequirePermission(menu = "RETENTION_ANOMALY", perm = "READ")
|
||||
public ApiResponse<PageResponse<RetentionAnomalyAlertResp>> list(@ModelAttribute RetentionAnomalyAlertSearchParam p) {
|
||||
return ApiResponse.ok(service.list(p));
|
||||
}
|
||||
|
||||
/** 이상치 알림 단건 상세 조회. */
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "이상치 단건 상세")
|
||||
@RequirePermission(menu = "RETENTION_ANOMALY", perm = "READ")
|
||||
public ApiResponse<RetentionAnomalyAlertResp> detail(@PathVariable Long id) {
|
||||
return ApiResponse.ok(service.detail(id));
|
||||
}
|
||||
|
||||
/** 지정한 월(alertMonth)에 대해 이상치를 다시 탐지·저장한다. 새로 탐지된 건수를 반환. */
|
||||
@PostMapping("/detect")
|
||||
@Operation(summary = "이상치 재탐지 (월 단위)")
|
||||
@RequirePermission(menu = "RETENTION_ANOMALY", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "RETENTION_ANOMALY", table = "retention_anomaly_alert")
|
||||
public ApiResponse<Integer> detect(@RequestBody Map<String, String> body) {
|
||||
return ApiResponse.ok(service.detect(body.get("alertMonth")));
|
||||
}
|
||||
|
||||
/** 탐지된 이상치를 검토/완료 상태로 처리(담당자 확인 결과 기록). */
|
||||
@PutMapping("/review")
|
||||
@Operation(summary = "이상치 검토/완료 처리")
|
||||
@RequirePermission(menu = "RETENTION_ANOMALY", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "RETENTION_ANOMALY", table = "retention_anomaly_alert")
|
||||
public ApiResponse<Void> review(@RequestBody RetentionAnomalyAlertSaveReq req) {
|
||||
service.review(req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,14 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 수수료 원장(ledger) 관리 REST 컨트롤러.
|
||||
* 원장은 설계사에게 지급된/지급될 수수료 내역을 종류별로 기록하는 장부다.
|
||||
* - 모집(recruit) : 신계약 체결로 발생한 모집수수료 원장
|
||||
* - 유지(maintain) : 계약 유지에 따라 매월 발생하는 유지수수료 원장
|
||||
* - 예외(exception) : 정상 계산에 안 잡히는 수기 조정 수수료(승인 절차 필요)
|
||||
* 조회/상세 외에 대용량 엑셀 다운로드(exportLargeExcel: 커서 스트리밍)를 제공한다.
|
||||
*/
|
||||
@Tag(name = "원장 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/ledger")
|
||||
@@ -24,7 +32,7 @@ public class LedgerController {
|
||||
private final LedgerService service;
|
||||
private final ExcelService excelService;
|
||||
|
||||
// ===== 모집 =====
|
||||
// ===== 모집수수료 원장 =====
|
||||
@GetMapping("/recruit")
|
||||
@RequirePermission(menu = "LEDGER_RECRUIT", perm = "READ")
|
||||
public ApiResponse<PageResponse<LedgerResp>> listRecruit(@ModelAttribute LedgerSearchParam param) {
|
||||
@@ -37,6 +45,7 @@ public class LedgerController {
|
||||
return ApiResponse.ok(service.detailRecruit(ledgerId));
|
||||
}
|
||||
|
||||
/** 모집수수료 원장 엑셀 다운로드. 대용량 대비 커서로 한 건씩 흘려보내 메모리 폭증을 막는다. */
|
||||
@GetMapping("/recruit/export")
|
||||
@RequirePermission(menu = "LEDGER_RECRUIT", perm = "EXPORT")
|
||||
public void exportRecruit(@ModelAttribute LedgerSearchParam param, HttpServletResponse res) {
|
||||
@@ -44,7 +53,7 @@ public class LedgerController {
|
||||
() -> service.exportRecruitCursor(param), res);
|
||||
}
|
||||
|
||||
// ===== 유지 =====
|
||||
// ===== 유지수수료 원장 =====
|
||||
@GetMapping("/maintain")
|
||||
@RequirePermission(menu = "LEDGER_MAINTAIN", perm = "READ")
|
||||
public ApiResponse<PageResponse<LedgerResp>> listMaintain(@ModelAttribute LedgerSearchParam param) {
|
||||
@@ -57,6 +66,7 @@ public class LedgerController {
|
||||
return ApiResponse.ok(service.detailMaintain(ledgerId));
|
||||
}
|
||||
|
||||
/** 유지수수료 원장 엑셀 다운로드(커서 스트리밍). */
|
||||
@GetMapping("/maintain/export")
|
||||
@RequirePermission(menu = "LEDGER_MAINTAIN", perm = "EXPORT")
|
||||
public void exportMaintain(@ModelAttribute LedgerSearchParam param, HttpServletResponse res) {
|
||||
@@ -64,7 +74,7 @@ public class LedgerController {
|
||||
() -> service.exportMaintainCursor(param), res);
|
||||
}
|
||||
|
||||
// ===== 예외 =====
|
||||
// ===== 예외수수료 원장(수기 등록 + 승인) =====
|
||||
@GetMapping("/exception")
|
||||
@RequirePermission(menu = "LEDGER_EXCEPTION", perm = "READ")
|
||||
public ApiResponse<PageResponse<ExceptionLedgerResp>> listException(@ModelAttribute LedgerSearchParam param) {
|
||||
@@ -77,6 +87,7 @@ public class LedgerController {
|
||||
return ApiResponse.ok(service.detailException(ledgerId));
|
||||
}
|
||||
|
||||
/** 예외수수료 원장 수기 등록. 등록 직후에는 승인대기(PENDING) 상태다. */
|
||||
@PostMapping("/exception")
|
||||
@RequirePermission(menu = "LEDGER_EXCEPTION", perm = "CREATE")
|
||||
@DataChangeLog(menu = "LEDGER_EXCEPTION", table = "exception_ledger")
|
||||
@@ -84,6 +95,7 @@ public class LedgerController {
|
||||
return ApiResponse.ok(service.createException(req));
|
||||
}
|
||||
|
||||
/** 예외수수료 원장 승인/반려 처리(body 의 status: APPROVED 또는 REJECTED). */
|
||||
@PutMapping("/exception/{ledgerId}/approve")
|
||||
@RequirePermission(menu = "LEDGER_EXCEPTION", perm = "APPROVE")
|
||||
@DataChangeLog(menu = "LEDGER_EXCEPTION", table = "exception_ledger")
|
||||
|
||||
@@ -15,6 +15,13 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 설계사 자격(License) 관리 API.
|
||||
*
|
||||
* <p>보험설계사는 판매 가능한 상품군별로 자격(생명/손해 등 licenseType)을 보유해야 한다.
|
||||
* 각 자격은 발급일(issuedDate)~만료일(expiresDate)을 가지며 갱신(renewedFrom) 이력이 연결될 수 있다.
|
||||
* 만료 임박 자격을 조회해 갱신을 독려하는 기능이 핵심이다. 메뉴 AGENT_LICENSE 권한으로 보호된다.</p>
|
||||
*/
|
||||
@Tag(name = "자격 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/agent-licenses")
|
||||
@@ -23,18 +30,21 @@ public class AgentLicenseController {
|
||||
|
||||
private final AgentLicenseService service;
|
||||
|
||||
/** 자격 목록 조회(페이징). READ 권한 필요. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "AGENT_LICENSE", perm = "READ")
|
||||
public ApiResponse<PageResponse<AgentLicenseResp>> list(@ModelAttribute AgentLicenseSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 자격 단건 상세 조회. READ 권한 필요. */
|
||||
@GetMapping("/{licenseId}")
|
||||
@RequirePermission(menu = "AGENT_LICENSE", perm = "READ")
|
||||
public ApiResponse<AgentLicenseResp> detail(@PathVariable Long licenseId) {
|
||||
return ApiResponse.ok(service.detail(licenseId));
|
||||
}
|
||||
|
||||
/** 만료 임박 자격 조회(기본 30일 이내). 갱신 알림/관리에 사용한다. READ 권한 필요. */
|
||||
@GetMapping("/expiring")
|
||||
@RequirePermission(menu = "AGENT_LICENSE", perm = "READ")
|
||||
public ApiResponse<List<AgentLicenseResp>> expiring(
|
||||
@@ -42,6 +52,7 @@ public class AgentLicenseController {
|
||||
return ApiResponse.ok(service.expiring(days));
|
||||
}
|
||||
|
||||
/** 자격 등록. 등록 시 ACTIVE 상태로 시작한다. CREATE 권한 필요. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "AGENT_LICENSE", perm = "CREATE")
|
||||
@DataChangeLog(menu = "AGENT_LICENSE", table = "agent_license")
|
||||
@@ -49,6 +60,7 @@ public class AgentLicenseController {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 자격 수정. UPDATE 권한 필요. */
|
||||
@PutMapping("/{licenseId}")
|
||||
@RequirePermission(menu = "AGENT_LICENSE", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "AGENT_LICENSE", table = "agent_license")
|
||||
@@ -58,6 +70,7 @@ public class AgentLicenseController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 자격 레코드 삭제. DELETE 권한 필요. */
|
||||
@DeleteMapping("/{licenseId}")
|
||||
@RequirePermission(menu = "AGENT_LICENSE", perm = "DELETE")
|
||||
@DataChangeLog(menu = "AGENT_LICENSE", table = "agent_license")
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.ga.api.controller.mobile;
|
||||
|
||||
import com.ga.api.service.mobile.MobileMeService;
|
||||
import com.ga.api.service.mobile.MobileMeService.MeSummary;
|
||||
import com.ga.api.service.mobile.MobileMeService.MobileWithdrawSaveReq;
|
||||
import com.ga.api.service.mobile.MobileMeService.WithdrawableBalance;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.notice.UserNotificationResp;
|
||||
import com.ga.core.vo.notice.UserNotificationSearchParam;
|
||||
import com.ga.core.vo.settle.CommissionStatementResp;
|
||||
import com.ga.core.vo.settle.CommissionStatementSearchParam;
|
||||
import com.ga.core.vo.settle.SettleMasterResp;
|
||||
import com.ga.core.vo.withdraw.WithdrawRequestResp;
|
||||
import com.ga.core.vo.withdraw.WithdrawRequestSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 모바일 PWA 전용 — 토큰의 사용자 본인 데이터만 반환.
|
||||
*
|
||||
* <p>모든 endpoint 는 인증만 통과하면 사용 가능 (별도 메뉴 권한 없음).
|
||||
* agentId / userId 는 서버측에서 토큰 → user 조회로 강제 결정. 클라이언트 파라미터는 무시한다.
|
||||
*/
|
||||
@Tag(name = "모바일 셀프 조회")
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/me")
|
||||
@RequiredArgsConstructor
|
||||
public class MobileMeController {
|
||||
|
||||
private final MobileMeService service;
|
||||
|
||||
/** 내 홈 요약(명세서 수/출금 신청 수/미읽음 알림 수). */
|
||||
@GetMapping("/summary")
|
||||
public ApiResponse<MeSummary> summary() {
|
||||
return ApiResponse.ok(service.summary());
|
||||
}
|
||||
|
||||
/** 내 수수료 명세서 목록(서버가 본인 agentId로 강제 필터). */
|
||||
@GetMapping("/statements")
|
||||
public ApiResponse<PageResponse<CommissionStatementResp>> statements(
|
||||
@ModelAttribute CommissionStatementSearchParam param) {
|
||||
return ApiResponse.ok(service.statements(param));
|
||||
}
|
||||
|
||||
/** 내 명세서 단건 상세. 본인 소유가 아니면 FORBIDDEN. */
|
||||
@GetMapping("/statements/{statementId}")
|
||||
public ApiResponse<CommissionStatementResp> statementDetail(@PathVariable Long statementId) {
|
||||
return ApiResponse.ok(service.statementDetail(statementId));
|
||||
}
|
||||
|
||||
/** 내 명세서 엑셀 다운로드. 본인 소유 검증 후 파일 스트림을 내려준다. */
|
||||
@GetMapping("/statements/{statementId}/download")
|
||||
public void downloadStatement(@PathVariable Long statementId, HttpServletResponse response) throws IOException {
|
||||
byte[] content = service.downloadStatement(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);
|
||||
}
|
||||
|
||||
/** 출금 신청 가능한 내 정산 마스터 목록(출금 폼 드롭다운용). */
|
||||
@GetMapping("/settle-masters")
|
||||
public ApiResponse<List<SettleMasterResp>> withdrawableSettleMasters() {
|
||||
return ApiResponse.ok(service.withdrawableSettleMasters());
|
||||
}
|
||||
|
||||
/** 특정 정산 마스터의 출금 가능 잔액 조회(출금 폼 미리 표시용). 본인 소유 검증 포함. */
|
||||
@GetMapping("/settle-masters/{settleMasterId}/withdrawable-balance")
|
||||
public ApiResponse<WithdrawableBalance> withdrawableBalance(@PathVariable Long settleMasterId) {
|
||||
return ApiResponse.ok(service.withdrawableBalance(settleMasterId));
|
||||
}
|
||||
|
||||
/** 내 출금 신청 목록(서버가 본인 agentId로 강제 필터). */
|
||||
@GetMapping("/withdraw-requests")
|
||||
public ApiResponse<PageResponse<WithdrawRequestResp>> withdrawRequests(
|
||||
@ModelAttribute WithdrawRequestSearchParam param) {
|
||||
return ApiResponse.ok(service.withdrawRequests(param));
|
||||
}
|
||||
|
||||
/** 출금 신청 등록. agentId/신청자는 토큰에서 강제 결정하고 잔액 한도를 검증한다. */
|
||||
@PostMapping("/withdraw-requests")
|
||||
public ApiResponse<Long> createWithdrawRequest(@Valid @RequestBody MobileWithdrawSaveReq req) {
|
||||
return ApiResponse.ok(service.createWithdrawRequest(req));
|
||||
}
|
||||
|
||||
/** 내 출금 신청 취소. 본인 소유 검증 후 취소한다. */
|
||||
@PostMapping("/withdraw-requests/{requestId}/cancel")
|
||||
public ApiResponse<Void> cancelWithdrawRequest(@PathVariable Long requestId) {
|
||||
service.cancelMyWithdrawRequest(requestId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 내 전화번호 직접 수정(관리자 거치지 않음). */
|
||||
@PutMapping("/profile/phone")
|
||||
public ApiResponse<Void> updateMyPhone(@RequestBody PhoneReq req) {
|
||||
service.updateMyPhone(req.getPhone());
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@lombok.Data
|
||||
public static class PhoneReq {
|
||||
private String phone;
|
||||
}
|
||||
|
||||
/** 내 알림 목록(서버가 본인 userId로 강제 필터). */
|
||||
@GetMapping("/notifications")
|
||||
public ApiResponse<PageResponse<UserNotificationResp>> notifications(
|
||||
@ModelAttribute UserNotificationSearchParam param) {
|
||||
return ApiResponse.ok(service.notifications(param));
|
||||
}
|
||||
|
||||
/** 내 알림 단건 읽음 처리. */
|
||||
@PostMapping("/notifications/{notificationId}/read")
|
||||
public ApiResponse<Void> markRead(@PathVariable Long notificationId) {
|
||||
service.markNotificationRead(notificationId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 내 알림 전체 읽음 처리(처리 건수 반환). */
|
||||
@PostMapping("/notifications/read-all")
|
||||
public ApiResponse<Integer> markAllRead() {
|
||||
return ApiResponse.ok(service.markAllNotificationsRead());
|
||||
}
|
||||
|
||||
/** 푸시용 디바이스 토큰 등록. body: {token, platform?} */
|
||||
@PostMapping("/device-tokens")
|
||||
public ApiResponse<Void> registerDeviceToken(@RequestBody java.util.Map<String, String> body) {
|
||||
service.registerDeviceToken(body.get("token"), body.get("platform"));
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 디바이스 토큰 해제(로그아웃 등). body: {token} */
|
||||
@PostMapping("/device-tokens/unregister")
|
||||
public ApiResponse<Void> unregisterDeviceToken(@RequestBody java.util.Map<String, String> body) {
|
||||
service.unregisterDeviceToken(body.get("token"));
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,16 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 공지사항 및 사용자 알림 API.
|
||||
*
|
||||
* <p>두 가지 도메인을 함께 다룬다.
|
||||
* <ul>
|
||||
* <li>공지사항(notice): 관리자가 등록하는 게시물. 기간(startDate~endDate)과 활성여부로 노출을 제어한다.</li>
|
||||
* <li>사용자 알림(user-notification): 개인에게 발송되는 알림. 읽음 처리/미읽음 개수 조회 등을 제공한다.</li>
|
||||
* </ul>
|
||||
* 모두 메뉴 NOTICE 권한으로 보호된다.</p>
|
||||
*/
|
||||
@Tag(name = "공지사항 & 알림")
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@@ -23,24 +33,28 @@ public class NoticeController {
|
||||
|
||||
// ── Notice ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 공지사항 목록 조회(페이징). READ 권한 필요. */
|
||||
@GetMapping("/api/notices")
|
||||
@RequirePermission(menu = "NOTICE", perm = "READ")
|
||||
public ApiResponse<PageResponse<NoticeResp>> list(@ModelAttribute NoticeSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 현재 노출 기간이고 활성 상태인 공지만 조회. READ 권한 필요. */
|
||||
@GetMapping("/api/notices/active")
|
||||
@RequirePermission(menu = "NOTICE", perm = "READ")
|
||||
public ApiResponse<List<NoticeResp>> activeList() {
|
||||
return ApiResponse.ok(service.activeList());
|
||||
}
|
||||
|
||||
/** 공지사항 단건 상세 조회. READ 권한 필요. */
|
||||
@GetMapping("/api/notices/{noticeId}")
|
||||
@RequirePermission(menu = "NOTICE", perm = "READ")
|
||||
public ApiResponse<NoticeResp> detail(@PathVariable Long noticeId) {
|
||||
return ApiResponse.ok(service.detail(noticeId));
|
||||
}
|
||||
|
||||
/** 공지사항 등록. 등록 시 활성(isActive=true) 상태로 시작한다. CREATE 권한 필요. */
|
||||
@PostMapping("/api/notices")
|
||||
@RequirePermission(menu = "NOTICE", perm = "CREATE")
|
||||
@DataChangeLog(menu = "NOTICE", table = "notice")
|
||||
@@ -48,6 +62,7 @@ public class NoticeController {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 공지사항 수정. UPDATE 권한 필요. */
|
||||
@PutMapping("/api/notices/{noticeId}")
|
||||
@RequirePermission(menu = "NOTICE", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "NOTICE", table = "notice")
|
||||
@@ -57,6 +72,7 @@ public class NoticeController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 공지사항 삭제. DELETE 권한 필요. */
|
||||
@DeleteMapping("/api/notices/{noticeId}")
|
||||
@RequirePermission(menu = "NOTICE", perm = "DELETE")
|
||||
@DataChangeLog(menu = "NOTICE", table = "notice")
|
||||
@@ -67,6 +83,7 @@ public class NoticeController {
|
||||
|
||||
// ── UserNotification ──────────────────────────────────────────────────────
|
||||
|
||||
/** 사용자 알림 목록 조회(페이징). READ 권한 필요. */
|
||||
@GetMapping("/api/user-notifications")
|
||||
@RequirePermission(menu = "NOTICE", perm = "READ")
|
||||
public ApiResponse<PageResponse<UserNotificationResp>> listNotifications(
|
||||
@@ -74,6 +91,7 @@ public class NoticeController {
|
||||
return ApiResponse.ok(service.listNotifications(param));
|
||||
}
|
||||
|
||||
/** 미읽음 알림 개수 조회. userId 미지정 시 현재 로그인 사용자 기준. READ 권한 필요. */
|
||||
@GetMapping("/api/user-notifications/unread-count")
|
||||
@RequirePermission(menu = "NOTICE", perm = "READ")
|
||||
public ApiResponse<Integer> unreadCount(@RequestParam(required = false) Long userId) {
|
||||
@@ -81,6 +99,7 @@ public class NoticeController {
|
||||
return ApiResponse.ok(service.unreadCount(target));
|
||||
}
|
||||
|
||||
/** 알림 단건 읽음 처리. userId 미지정 시 현재 로그인 사용자 기준. READ 권한 필요. */
|
||||
@PostMapping("/api/user-notifications/{notificationId}/read")
|
||||
@RequirePermission(menu = "NOTICE", perm = "READ")
|
||||
public ApiResponse<Void> markRead(@PathVariable Long notificationId,
|
||||
@@ -90,6 +109,7 @@ public class NoticeController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 해당 사용자의 모든 알림을 읽음 처리. READ 권한 필요. */
|
||||
@PostMapping("/api/user-notifications/read-all")
|
||||
@RequirePermission(menu = "NOTICE", perm = "READ")
|
||||
public ApiResponse<Void> markAllRead(@RequestParam Long userId) {
|
||||
|
||||
@@ -16,6 +16,15 @@ import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 설계사(보험모집인) 관리 API.
|
||||
*
|
||||
* <p>설계사는 GA(보험대리점)에 소속되어 보험계약을 모집하는 사람이며, 수수료 정산의 기본 단위다.
|
||||
* 각 설계사는 특정 조직(지점/본부)과 직급(grade)에 소속되며, 입사/전배/승진/퇴사 등의 이력이 관리된다.</p>
|
||||
*
|
||||
* <p>모든 엔드포인트는 {@code @RequirePermission}으로 메뉴(ORG_AGENT)별 권한(READ/CREATE/UPDATE/EXPORT)을
|
||||
* 검증하며, 데이터 변경 작업은 {@code @DataChangeLog}로 변경 이력을 자동 적재한다.</p>
|
||||
*/
|
||||
@Tag(name = "설계사 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/agents")
|
||||
@@ -25,18 +34,21 @@ public class AgentController {
|
||||
private final AgentService agentService;
|
||||
private final ExcelService excelService;
|
||||
|
||||
/** 설계사 목록 조회(검색조건 + 페이징). READ 권한 필요. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
|
||||
public ApiResponse<PageResponse<AgentResp>> list(@ModelAttribute AgentSearchParam param) {
|
||||
return ApiResponse.ok(agentService.list(param));
|
||||
}
|
||||
|
||||
/** 설계사 단건 상세 조회. READ 권한 필요. */
|
||||
@GetMapping("/{agentId}")
|
||||
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
|
||||
public ApiResponse<AgentResp> detail(@PathVariable Long agentId) {
|
||||
return ApiResponse.ok(agentService.detail(agentId));
|
||||
}
|
||||
|
||||
/** 신규 설계사 등록(입사). 등록 시 입사 이력이 함께 생성된다. CREATE 권한 필요. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "ORG_AGENT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "ORG_AGENT", table = "agent")
|
||||
@@ -44,6 +56,7 @@ public class AgentController {
|
||||
return ApiResponse.ok(agentService.create(req));
|
||||
}
|
||||
|
||||
/** 설계사 정보 수정. 조직/직급이 바뀌면 전배/승진 이력이 자동 기록된다. UPDATE 권한 필요. */
|
||||
@PutMapping("/{agentId}")
|
||||
@RequirePermission(menu = "ORG_AGENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "ORG_AGENT", table = "agent")
|
||||
@@ -52,6 +65,7 @@ public class AgentController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 설계사 상태 변경(예: ACTIVE 재직 → 퇴사/휴직 등). body의 "status" 값을 코드로 검증한다. UPDATE 권한 필요. */
|
||||
@PutMapping("/{agentId}/status")
|
||||
@RequirePermission(menu = "ORG_AGENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "ORG_AGENT", table = "agent")
|
||||
@@ -60,12 +74,14 @@ public class AgentController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 설계사 조직/직급 변경 이력(입사·전배·승진) 조회. READ 권한 필요. */
|
||||
@GetMapping("/{agentId}/history")
|
||||
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
|
||||
public ApiResponse<List<AgentOrgHistoryResp>> history(@PathVariable Long agentId) {
|
||||
return ApiResponse.ok(agentService.history(agentId));
|
||||
}
|
||||
|
||||
/** 설계사 목록 엑셀 다운로드. 대용량 대비 커서 스트리밍으로 출력한다. EXPORT 권한 필요. */
|
||||
@GetMapping("/export")
|
||||
@RequirePermission(menu = "ORG_AGENT", perm = "EXPORT")
|
||||
public void export(@ModelAttribute AgentSearchParam param, HttpServletResponse response) {
|
||||
|
||||
@@ -14,6 +14,14 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 조직(본부/지점 등) 관리 API.
|
||||
*
|
||||
* <p>조직은 부모-자식 관계의 계층(트리) 구조를 가진다. 설계사는 말단 조직에 소속되며,
|
||||
* 수수료 정산·실적 집계는 이 조직 계층을 따라 상위로 롤업된다.</p>
|
||||
*
|
||||
* <p>메뉴 ORG_TREE 권한으로 보호되며, 삭제는 하위 조직/소속 설계사가 없을 때만 허용된다(아래 서비스 참고).</p>
|
||||
*/
|
||||
@Tag(name = "조직 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/orgs")
|
||||
@@ -22,12 +30,14 @@ public class OrganizationController {
|
||||
|
||||
private final OrganizationService service;
|
||||
|
||||
/** 조직 전체를 트리(계층) 구조로 조회. READ 권한 필요. */
|
||||
@GetMapping("/tree")
|
||||
@RequirePermission(menu = "ORG_TREE", perm = "READ")
|
||||
public ApiResponse<List<OrganizationResp>> tree() {
|
||||
return ApiResponse.ok(service.getTree());
|
||||
}
|
||||
|
||||
/** 조직 목록을 평면(flat) 리스트로 조회(검색어/조직유형/활성여부 필터). READ 권한 필요. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "ORG_TREE", perm = "READ")
|
||||
public ApiResponse<List<OrganizationResp>> list(@RequestParam(required = false) String keyword,
|
||||
@@ -36,12 +46,14 @@ public class OrganizationController {
|
||||
return ApiResponse.ok(service.list(keyword, orgType, isActive));
|
||||
}
|
||||
|
||||
/** 조직 단건 상세 조회. READ 권한 필요. */
|
||||
@GetMapping("/{orgId}")
|
||||
@RequirePermission(menu = "ORG_TREE", perm = "READ")
|
||||
public ApiResponse<OrganizationVO> detail(@PathVariable Long orgId) {
|
||||
return ApiResponse.ok(service.get(orgId));
|
||||
}
|
||||
|
||||
/** 신규 조직 등록. CREATE 권한 필요. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "ORG_TREE", perm = "CREATE")
|
||||
@DataChangeLog(menu = "ORG_TREE", table = "organization")
|
||||
@@ -49,6 +61,7 @@ public class OrganizationController {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 조직 정보 수정. UPDATE 권한 필요. */
|
||||
@PutMapping("/{orgId}")
|
||||
@RequirePermission(menu = "ORG_TREE", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "ORG_TREE", table = "organization")
|
||||
@@ -57,6 +70,7 @@ public class OrganizationController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 조직 삭제. 하위 조직 또는 소속 설계사가 있으면 삭제 불가(서비스에서 검증). DELETE 권한 필요. */
|
||||
@DeleteMapping("/{orgId}")
|
||||
@RequirePermission(menu = "ORG_TREE", perm = "DELETE")
|
||||
@DataChangeLog(menu = "ORG_TREE", table = "organization")
|
||||
|
||||
@@ -11,6 +11,14 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 보험사(원수사) 관리 REST 컨트롤러.
|
||||
* 우리 GA가 상품을 위탁판매하는 보험회사(예: 삼성생명, KB손해보험 등) 마스터를 관리한다.
|
||||
* 모든 상품/요율/수수료 계산의 출발점이 되는 기준 데이터이다.
|
||||
*
|
||||
* - @RequirePermission: 메뉴/권한(READ·CREATE·UPDATE) 체크. 권한 없으면 호출 거부.
|
||||
* - @DataChangeLog: 데이터 변경 시 감사 로그(누가/언제/무엇을 바꿨는지) 자동 기록.
|
||||
*/
|
||||
@Tag(name = "보험사 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/companies")
|
||||
@@ -19,18 +27,21 @@ public class CompanyController {
|
||||
|
||||
private final InsuranceCompanyService companyService;
|
||||
|
||||
/** 보험사 목록 조회. activeOnly=true 면 사용중(활성) 보험사만 반환. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "COMPANY", perm = "READ")
|
||||
public ApiResponse<List<InsuranceCompanyVO>> list(@RequestParam(required = false, defaultValue = "false") boolean activeOnly) {
|
||||
return ApiResponse.ok(companyService.list(activeOnly));
|
||||
}
|
||||
|
||||
/** 보험사 단건 상세 조회 (URL 경로의 companyId 기준). */
|
||||
@GetMapping("/{companyId}")
|
||||
@RequirePermission(menu = "COMPANY", perm = "READ")
|
||||
public ApiResponse<InsuranceCompanyVO> detail(@PathVariable Long companyId) {
|
||||
return ApiResponse.ok(companyService.detail(companyId));
|
||||
}
|
||||
|
||||
/** 보험사 신규 등록. 생성된 PK(companyId)를 반환. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "COMPANY", perm = "CREATE")
|
||||
@DataChangeLog(menu = "COMPANY", table = "insurance_company")
|
||||
@@ -38,6 +49,7 @@ public class CompanyController {
|
||||
return ApiResponse.ok(companyService.create(vo));
|
||||
}
|
||||
|
||||
/** 보험사 정보 수정 (경로 companyId 대상). */
|
||||
@PutMapping("/{companyId}")
|
||||
@RequirePermission(menu = "COMPANY", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "COMPANY", table = "insurance_company")
|
||||
|
||||
@@ -15,6 +15,11 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 보험계약 관리 REST 컨트롤러.
|
||||
* 계약(contract)은 "어떤 설계사가 어떤 고객에게 어떤 상품을 팔았는가"를 나타내는 핵심 거래 데이터로,
|
||||
* 이후 수수료 계산·분급(installment)·차등환수(chargeback)의 기준이 된다.
|
||||
*/
|
||||
@Tag(name = "계약 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/contracts")
|
||||
@@ -23,18 +28,21 @@ public class ContractController {
|
||||
|
||||
private final ContractService service;
|
||||
|
||||
/** 계약 목록 조회. @ModelAttribute 로 검색조건(증권번호·기간 등)을 받아 페이징 결과를 반환. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "CONTRACT_LIST", perm = "READ")
|
||||
public ApiResponse<PageResponse<ContractResp>> list(@ModelAttribute ContractSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 계약 단건 상세 조회. */
|
||||
@GetMapping("/{contractId}")
|
||||
@RequirePermission(menu = "CONTRACT_LIST", perm = "READ")
|
||||
public ApiResponse<ContractResp> detail(@PathVariable Long contractId) {
|
||||
return ApiResponse.ok(service.detail(contractId));
|
||||
}
|
||||
|
||||
/** 계약 신규 등록. @Valid 로 필수값 검증 후 생성된 PK(contractId) 반환. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "CONTRACT_LIST", perm = "CREATE")
|
||||
@DataChangeLog(menu = "CONTRACT_LIST", table = "contract")
|
||||
@@ -42,6 +50,7 @@ public class ContractController {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 계약 정보 수정. */
|
||||
@PutMapping("/{contractId}")
|
||||
@RequirePermission(menu = "CONTRACT_LIST", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CONTRACT_LIST", table = "contract")
|
||||
@@ -50,6 +59,7 @@ public class ContractController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 계약 상태 변경(예: 정상 ACTIVE → 해지 CANCELLED). body 의 "status" 값만 사용. 해지 시 차등환수 계산 대상이 됨. */
|
||||
@PutMapping("/{contractId}/status")
|
||||
@RequirePermission(menu = "CONTRACT_LIST", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CONTRACT_LIST", table = "contract")
|
||||
|
||||
@@ -4,14 +4,19 @@ import com.ga.api.service.product.ProductService;
|
||||
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.product.ProductResp;
|
||||
import com.ga.core.vo.product.ProductVO;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 보험상품 관리 REST 컨트롤러.
|
||||
* 상품(product)은 보험사가 판매하는 개별 상품으로, 각 상품에는
|
||||
* company_rate(보험사가 GA에 주는 요율)·payout_rate(GA가 설계사에게 지급하는 요율) 등
|
||||
* 수수료 계산의 핵심 요율 정보가 붙는다.
|
||||
*/
|
||||
@Tag(name = "상품 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/products")
|
||||
@@ -20,21 +25,26 @@ public class ProductController {
|
||||
|
||||
private final ProductService productService;
|
||||
|
||||
/** 상품 목록 조회. 보험사/보험종류/키워드/활성여부로 필터링하며 페이징 처리(pageNum/pageSize)된다. */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "PRODUCT", perm = "READ")
|
||||
public ApiResponse<List<ProductResp>> list(@RequestParam(required = false) Long companyId,
|
||||
public ApiResponse<PageResponse<ProductResp>> list(@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) Long companyId,
|
||||
@RequestParam(required = false) String insuranceType,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String isActive) {
|
||||
return ApiResponse.ok(productService.list(companyId, insuranceType, keyword, isActive));
|
||||
return ApiResponse.ok(productService.list(pageNum, pageSize, companyId, insuranceType, keyword, isActive));
|
||||
}
|
||||
|
||||
/** 상품 단건 상세 조회. */
|
||||
@GetMapping("/{productId}")
|
||||
@RequirePermission(menu = "PRODUCT", perm = "READ")
|
||||
public ApiResponse<ProductResp> detail(@PathVariable Long productId) {
|
||||
return ApiResponse.ok(productService.detail(productId));
|
||||
}
|
||||
|
||||
/** 상품 신규 등록. 생성된 PK(productId) 반환. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "PRODUCT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "PRODUCT", table = "product")
|
||||
@@ -42,6 +52,7 @@ public class ProductController {
|
||||
return ApiResponse.ok(productService.create(vo));
|
||||
}
|
||||
|
||||
/** 상품 정보(요율 등) 수정. */
|
||||
@PutMapping("/{productId}")
|
||||
@RequirePermission(menu = "PRODUCT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "PRODUCT", table = "product")
|
||||
@@ -50,6 +61,7 @@ public class ProductController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 상품 삭제. */
|
||||
@DeleteMapping("/{productId}")
|
||||
@RequirePermission(menu = "PRODUCT", perm = "DELETE")
|
||||
@DataChangeLog(menu = "PRODUCT", table = "product")
|
||||
|
||||
@@ -12,6 +12,19 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* [데이터 수신 매핑/원본 관리 API]
|
||||
*
|
||||
* 신입 개발자 안내:
|
||||
* - 보험사마다 보내주는 수수료 데이터의 컬럼명/코드값이 제각각이다. 이를 우리 표준으로 바꾸기 위한 설정이 "매핑"이다.
|
||||
* - 구성 요소:
|
||||
* company_profile : 보험사별 수신 설정(파일 형식, 헤더 위치 등)
|
||||
* company_field_mapping : 보험사 컬럼 → 우리 DB 필드 매핑
|
||||
* code_mapping : 보험사 코드값 → 우리 표준 코드값 매핑
|
||||
* raw_commission_data : 수신한 원본 데이터(파싱 상태 포함)
|
||||
* parse_error_log : 파싱 실패 건과 그 처리(resolve) 상태
|
||||
* - 업로드 템플릿(upload)이 "엑셀 수동 적재"라면, 여기 매핑은 "보험사별 자동 수신/파싱 규칙"에 가깝다.
|
||||
*/
|
||||
@Tag(name = "데이터 수신")
|
||||
@RestController
|
||||
@RequestMapping("/api/receive")
|
||||
@@ -82,6 +95,7 @@ public class ReceiveController {
|
||||
}
|
||||
|
||||
// 원본 데이터 + 에러
|
||||
// raw: 수신한 원본 행 목록(보험사/정산월/파싱상태로 필터). errors: 파싱 실패 로그. resolve: 실패 건을 처리완료로 표시.
|
||||
@GetMapping("/raw")
|
||||
@RequirePermission(menu = "RECEIVE_DATA", perm = "READ")
|
||||
public ApiResponse<List<RawCommissionDataVO>> rawList(@RequestParam(required = false) String companyCode,
|
||||
|
||||
@@ -4,6 +4,11 @@ import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 파싱 에러 처리(resolve) 요청 본문.
|
||||
* status: 처리 결과 상태(예: 해결됨/보류 등) — 필수.
|
||||
* note : 처리 사유/메모 — 선택.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class ResolveErrorReq {
|
||||
|
||||
@@ -7,12 +7,18 @@ import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 계약 단위 규제 한도(예: 1200%룰) 위반 여부 체크 요청 DTO.
|
||||
* "이 계약에 수수료를 additionalAmount 만큼 더 지급해도 규제 한도를 넘지 않는가?"를 묻는다.
|
||||
*/
|
||||
@Data
|
||||
public class ContractLimitCheckReq {
|
||||
|
||||
/** 한도 체크 대상 계약 ID. */
|
||||
@NotNull
|
||||
private Long contractId;
|
||||
|
||||
/** 추가로 지급하려는 수수료 금액(양수만 허용). 이 금액을 더했을 때 한도 초과 여부를 검사한다. */
|
||||
@NotNull
|
||||
@Positive
|
||||
private BigDecimal additionalAmount;
|
||||
|
||||
@@ -17,6 +17,13 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 규제 한도 관리 REST 컨트롤러.
|
||||
* 금융당국 규제(대표적으로 보험모집 수수료의 '1200%룰': 초년도 수수료를 월납보험료의 12배 이내로 제한)를
|
||||
* 한도마스터(regulatory_limit) 테이블로 관리하고, 계약별 위반 여부를 점검한다.
|
||||
* - limitService : 한도 마스터 CRUD + 활성/비활성 토글
|
||||
* - limitChecker : 실제 계약에 적용해 한도 초과 여부 계산(별도 상세 주석 파일)
|
||||
*/
|
||||
@Tag(name = "규제 한도 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/regulatory-limits")
|
||||
@@ -26,18 +33,21 @@ 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")
|
||||
@@ -46,6 +56,7 @@ public class RegulatoryLimitController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 한도 마스터 수정. */
|
||||
@PutMapping("/{limitId}")
|
||||
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "REGULATORY_LIMIT", table = "regulatory_limit")
|
||||
@@ -55,6 +66,7 @@ public class RegulatoryLimitController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 한도 마스터 비활성화(삭제 대신 사용 중지 처리). 비활성 한도는 위반 체크에서 제외된다. */
|
||||
@PutMapping("/{limitId}/deactivate")
|
||||
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "REGULATORY_LIMIT", table = "regulatory_limit")
|
||||
@@ -63,6 +75,7 @@ public class RegulatoryLimitController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 한도 마스터 재활성화. 같은 유형의 활성 한도가 이미 있으면 중복 오류. */
|
||||
@PutMapping("/{limitId}/activate")
|
||||
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "REGULATORY_LIMIT", table = "regulatory_limit")
|
||||
@@ -71,6 +84,11 @@ public class RegulatoryLimitController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 계약에 수수료를 더 지급했을 때 규제 한도 위반 여부를 점검한다.
|
||||
* limitType 이 FIRST_YEAR_RATIO 면 초년도(첫 1년) 기준, 그 외(TOTAL_RATIO)면 계약 전체 누적 기준으로 검사.
|
||||
* baseDate 가 없으면 오늘 날짜를 기준일로 사용한다.
|
||||
*/
|
||||
@PostMapping("/check-contract")
|
||||
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "READ")
|
||||
public ApiResponse<ViolationResult> checkContract(@Valid @RequestBody ContractLimitCheckReq req) {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.ga.api.controller.report;
|
||||
|
||||
import com.ga.api.service.report.ReportService;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.excel.ExcelService;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.core.vo.report.ChargebackRiskRow;
|
||||
import com.ga.core.vo.report.OrgReportRow;
|
||||
import com.ga.core.vo.report.PerfReportRow;
|
||||
import com.ga.core.vo.report.SimplifiedPaymentRow;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 리포트 — 실적 / 조직별 / 환수위험. 읽기전용 집계 (신규 테이블 없음).
|
||||
* 각 화면의 조회 + 엑셀 export 엔드포인트 제공.
|
||||
*
|
||||
* [신입 개발자 안내]
|
||||
* - 모든 리포트는 기존 정산/지급 데이터를 SQL 로 모아 보여주는 "읽기 전용 집계"다(저장 테이블을 새로 만들지 않음).
|
||||
* - 각 리포트마다 화면 조회용(JSON 목록)과 엑셀 다운로드용(/export, Cursor 스트리밍) 두 개가 한 쌍이다.
|
||||
* - year/settleMonth 파라미터가 없으면 defaultYear/defaultMonth 로 "올해/이번 달"을 기본 적용한다.
|
||||
* - 리포트 종류:
|
||||
* performance : 설계사/연도별 실적
|
||||
* org : 조직(지점)별 월 집계
|
||||
* chargeback-risk : 환수(이미 준 수수료를 도로 회수) 위험 목록
|
||||
* simplified-payment : 사업소득 간이지급명세서(국세청 제출용 월별 자료)
|
||||
*/
|
||||
@Tag(name = "리포트")
|
||||
@RestController
|
||||
@RequestMapping("/api/report")
|
||||
@RequiredArgsConstructor
|
||||
public class ReportController {
|
||||
|
||||
private final ReportService service;
|
||||
private final ExcelService excelService;
|
||||
|
||||
private String defaultYear(String year) {
|
||||
return (year != null && !year.isBlank()) ? year : String.valueOf(LocalDate.now().getYear());
|
||||
}
|
||||
|
||||
private String defaultMonth(String settleMonth) {
|
||||
return (settleMonth != null && !settleMonth.isBlank())
|
||||
? settleMonth : LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
|
||||
}
|
||||
|
||||
// ── 실적 리포트 ──
|
||||
@GetMapping("/performance")
|
||||
@RequirePermission(menu = "REPORT_PERFORMANCE", perm = "READ")
|
||||
public ApiResponse<List<PerfReportRow>> performance(@RequestParam(required = false) String year) {
|
||||
return ApiResponse.ok(service.performance(defaultYear(year)));
|
||||
}
|
||||
|
||||
@GetMapping("/performance/export")
|
||||
@RequirePermission(menu = "REPORT_PERFORMANCE", perm = "READ")
|
||||
public void performanceExport(@RequestParam(required = false) String year, HttpServletResponse response) {
|
||||
String y = defaultYear(year);
|
||||
excelService.exportLargeExcel("실적리포트", PerfReportRow.class,
|
||||
() -> service.performanceCursor(y), response);
|
||||
}
|
||||
|
||||
// ── 조직별 리포트 ──
|
||||
@GetMapping("/org")
|
||||
@RequirePermission(menu = "REPORT_ORG", perm = "READ")
|
||||
public ApiResponse<List<OrgReportRow>> org(@RequestParam(required = false) String settleMonth) {
|
||||
return ApiResponse.ok(service.org(defaultMonth(settleMonth)));
|
||||
}
|
||||
|
||||
@GetMapping("/org/export")
|
||||
@RequirePermission(menu = "REPORT_ORG", perm = "READ")
|
||||
public void orgExport(@RequestParam(required = false) String settleMonth, HttpServletResponse response) {
|
||||
String m = defaultMonth(settleMonth);
|
||||
excelService.exportLargeExcel("조직별리포트", OrgReportRow.class,
|
||||
() -> service.orgCursor(m), response);
|
||||
}
|
||||
|
||||
// ── 환수위험 리포트 ──
|
||||
@GetMapping("/chargeback-risk")
|
||||
@RequirePermission(menu = "REPORT_CHARGEBACK", perm = "READ")
|
||||
public ApiResponse<List<ChargebackRiskRow>> chargebackRisk() {
|
||||
return ApiResponse.ok(service.chargebackRisk());
|
||||
}
|
||||
|
||||
@GetMapping("/chargeback-risk/export")
|
||||
@RequirePermission(menu = "REPORT_CHARGEBACK", perm = "READ")
|
||||
public void chargebackRiskExport(HttpServletResponse response) {
|
||||
excelService.exportLargeExcel("환수위험리포트", ChargebackRiskRow.class,
|
||||
() -> service.chargebackRiskCursor(), response);
|
||||
}
|
||||
|
||||
// ── 사업소득 간이지급명세서 (월별) ──
|
||||
@GetMapping("/simplified-payment")
|
||||
@RequirePermission(menu = "REPORT_SIMPLIFIED_PAYMENT", perm = "READ")
|
||||
public ApiResponse<List<SimplifiedPaymentRow>> simplifiedPayment(@RequestParam(required = false) String settleMonth) {
|
||||
return ApiResponse.ok(service.simplifiedPayment(defaultMonth(settleMonth)));
|
||||
}
|
||||
|
||||
@GetMapping("/simplified-payment/export")
|
||||
@RequirePermission(menu = "REPORT_SIMPLIFIED_PAYMENT", perm = "READ")
|
||||
public void simplifiedPaymentExport(@RequestParam(required = false) String settleMonth, HttpServletResponse response) {
|
||||
String m = defaultMonth(settleMonth);
|
||||
excelService.exportLargeExcel("간이지급명세서_" + m, SimplifiedPaymentRow.class,
|
||||
() -> service.simplifiedPaymentCursor(m), response);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,16 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 수수료 규정(룰) 관리 REST 컨트롤러.
|
||||
* 수수료 계산에 쓰이는 각종 기준표를 CRUD 한다. 다섯 가지 룰 그룹으로 구성된다.
|
||||
* - commission_rate : 상품별 보험사 지급요율(company_rate) 기준표
|
||||
* - payout_rule : 설계사 등급(grade)별 지급요율(payout_rate) 규칙
|
||||
* - override_rule : 상위 관리자에게 주는 오버라이드(육성) 수당 규칙
|
||||
* - chargeback_rule : 조기해지 시 환수율(차등환수) 규칙
|
||||
* - exception_type_code : 예외 처리 유형 코드
|
||||
* 이 기준표들을 잘못 등록하면 전 직원 수수료 계산이 틀어지므로 권한·감사로그가 엄격히 걸려 있다.
|
||||
*/
|
||||
@Tag(name = "수수료 규정")
|
||||
@RestController
|
||||
@RequestMapping("/api/rules")
|
||||
@@ -19,7 +29,7 @@ public class RuleController {
|
||||
|
||||
private final RuleService ruleService;
|
||||
|
||||
// ========== commission_rate ==========
|
||||
// ========== commission_rate : 상품별 보험사 지급요율(company_rate) 기준표 CRUD ==========
|
||||
@GetMapping("/commission-rates")
|
||||
@RequirePermission(menu = "RULE_COMMISSION", perm = "READ")
|
||||
public ApiResponse<List<CommissionRateVO>> listCommissionRates(@RequestParam(required = false) Long productId,
|
||||
@@ -50,7 +60,7 @@ public class RuleController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ========== payout_rule ==========
|
||||
// ========== payout_rule : 설계사 등급(grade)별 지급요율(payout_rate) 규칙 CRUD ==========
|
||||
@GetMapping("/payout")
|
||||
@RequirePermission(menu = "RULE_PAYOUT", perm = "READ")
|
||||
public ApiResponse<List<PayoutRuleVO>> listPayoutRules(@RequestParam(required = false) Long gradeId,
|
||||
@@ -81,7 +91,7 @@ public class RuleController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ========== override_rule ==========
|
||||
// ========== override_rule : 상위 관리자 오버라이드(육성) 수당 규칙 CRUD ==========
|
||||
@GetMapping("/override")
|
||||
@RequirePermission(menu = "RULE_OVERRIDE", perm = "READ")
|
||||
public ApiResponse<List<OverrideRuleVO>> listOverrideRules() {
|
||||
@@ -111,7 +121,7 @@ public class RuleController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ========== chargeback_rule ==========
|
||||
// ========== chargeback_rule : 조기해지 환수율(차등환수) 규칙 CRUD ==========
|
||||
@GetMapping("/chargeback")
|
||||
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "READ")
|
||||
public ApiResponse<List<ChargebackRuleVO>> listChargebackRules(@RequestParam(required = false) String companyCode) {
|
||||
@@ -141,7 +151,7 @@ public class RuleController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ========== exception_type_code ==========
|
||||
// ========== exception_type_code : 예외 처리 유형 코드 관리(코드값 기반) ==========
|
||||
@GetMapping("/exception-codes")
|
||||
@RequirePermission(menu = "RULE_EXCEPTION", perm = "READ")
|
||||
public ApiResponse<List<ExceptionTypeCodeVO>> listExceptionCodes() {
|
||||
|
||||
+12
@@ -13,6 +13,14 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 보험사 회신(carrier_reconciliation_out) 관리 컨트롤러.
|
||||
*
|
||||
* <p>GA가 보험사로 보낸 정산 자료에 대해 "어떤 파일을 언제 회신/전송했는지" 이력을 관리하는 화면입니다.
|
||||
* 한 건은 "보험사 1곳 × 정산월 1개"로 유일하며, 전송 실패(FAIL) 건은 retry 로 다시 READY 로 돌려 배치가 재전송합니다.
|
||||
*
|
||||
* <p>상태: READY(전송대기) → (배치 처리) → 성공/FAIL. 권한(menu=RECON_OUT): 조회 READ, 등록 CREATE, 재전송 UPDATE.
|
||||
*/
|
||||
@Tag(name = "보험사 회신 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/carrier-reconciliation")
|
||||
@@ -21,18 +29,21 @@ public class CarrierReconciliationOutController {
|
||||
|
||||
private final CarrierReconciliationOutService service;
|
||||
|
||||
/** 보험사 회신 이력 목록 조회(검색 + 페이징). */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "RECON_OUT", perm = "READ")
|
||||
public ApiResponse<PageResponse<CarrierReconciliationOutResp>> list(@ModelAttribute CarrierReconciliationOutSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 회신 이력 단건 상세 조회. */
|
||||
@GetMapping("/{reconOutId}")
|
||||
@RequirePermission(menu = "RECON_OUT", perm = "READ")
|
||||
public ApiResponse<CarrierReconciliationOutResp> detail(@PathVariable Long reconOutId) {
|
||||
return ApiResponse.ok(service.detail(reconOutId));
|
||||
}
|
||||
|
||||
/** 회신 이력 신규 등록(READY 상태로 시작). 같은 보험사+정산월 중복 등록은 거부됩니다. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "RECON_OUT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "RECON_OUT", table = "carrier_reconciliation_out")
|
||||
@@ -40,6 +51,7 @@ public class CarrierReconciliationOutController {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 전송 실패(FAIL) 건 재전송. 상태를 READY 로 되돌려 배치가 다시 처리하도록 합니다. */
|
||||
@PostMapping("/{reconOutId}/retry")
|
||||
@RequirePermission(menu = "RECON_OUT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "RECON_OUT", table = "carrier_reconciliation_out")
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.ga.api.controller.settle;
|
||||
|
||||
import com.ga.api.service.settle.ChargebackService;
|
||||
import com.ga.api.service.settle.ChargebackUploadService;
|
||||
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.ChargebackResp;
|
||||
import com.ga.core.vo.settle.ChargebackSearchParam;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 환수(chargeback) 자료 관리 컨트롤러.
|
||||
*
|
||||
* <p>"환수"란 이미 설계사에게 지급한 수수료를 도로 거둬들이는 것입니다. 계약이 일찍 해지/실효되면
|
||||
* 보험사가 GA에게 수수료를 돌려달라 하고, GA는 그만큼을 설계사 정산에서 회수합니다.
|
||||
*
|
||||
* <p>주 업무는 보험사가 보내준 환수 엑셀(증권번호·환수금액·실효월·환수율)을 업로드(upload)해
|
||||
* chargeback 테이블에 적재하는 것입니다. 존재하지 않는 증권번호나 잘못된 금액은 에러행으로 돌려주고,
|
||||
* 같은 계약+정산월 중복은 건너뜁니다(스킵).
|
||||
*
|
||||
* <p>권한(menu=CHARGEBACK_DATA): 조회 READ, 엑셀 EXPORT, 업로드 CREATE.
|
||||
*/
|
||||
@Tag(name = "환수자료 관리")
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/chargebacks")
|
||||
@RequiredArgsConstructor
|
||||
public class ChargebackController {
|
||||
|
||||
private final ChargebackService chargebackService;
|
||||
private final ChargebackUploadService chargebackUploadService;
|
||||
|
||||
@Operation(summary = "환수자료 목록 조회")
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "CHARGEBACK_DATA", perm = "READ")
|
||||
public ApiResponse<PageResponse<ChargebackResp>> list(@ModelAttribute ChargebackSearchParam param) {
|
||||
return ApiResponse.ok(chargebackService.list(param));
|
||||
}
|
||||
|
||||
@Operation(summary = "환수자료 엑셀 다운로드")
|
||||
@GetMapping("/export")
|
||||
@RequirePermission(menu = "CHARGEBACK_DATA", perm = "EXPORT")
|
||||
public void export(@ModelAttribute ChargebackSearchParam param, HttpServletResponse response) {
|
||||
chargebackService.export(param, response);
|
||||
}
|
||||
|
||||
@Operation(summary = "보험사 환수자료 업로드",
|
||||
description = "정산월(YYYYMM) + 엑셀 파일(증권번호/환수금액/실효월/환수율). " +
|
||||
"미존재 증권번호·cbAmount 오류는 에러행으로 반환. 동일 계약+정산월 중복은 스킵.")
|
||||
@PostMapping("/upload")
|
||||
@RequirePermission(menu = "CHARGEBACK_DATA", perm = "CREATE")
|
||||
@DataChangeLog(menu = "CHARGEBACK_DATA", table = "chargeback")
|
||||
public ApiResponse<ChargebackUploadService.UploadResult> upload(
|
||||
@Pattern(regexp = "\\d{6}", message = "settleMonth는 YYYYMM 형식이어야 합니다")
|
||||
@RequestParam String settleMonth,
|
||||
@RequestParam("file") MultipartFile file) {
|
||||
return ApiResponse.ok(chargebackUploadService.upload(settleMonth, file));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,18 @@ import org.springframework.web.bind.annotation.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 수수료 이의신청(commission_dispute) 관리 컨트롤러.
|
||||
*
|
||||
* <p>설계사가 "내 수수료 정산 금액이 이상하다"고 문제를 제기하면 이의신청 건이 만들어집니다.
|
||||
* 담당자는 검토를 시작(startReview)하고, 타당하면 승인(approve)하며 보정 금액을 정정(correction)과 연결하거나,
|
||||
* 타당하지 않으면 기각(reject)합니다.
|
||||
*
|
||||
* <p>상태 흐름(DisputeStatus): OPEN(접수) → IN_REVIEW(검토중) → APPROVED(승인)/REJECTED(기각).
|
||||
* 승인/기각은 IN_REVIEW 상태에서만 가능합니다.
|
||||
*
|
||||
* <p>권한(menu=DISPUTE): 조회 READ, 접수 CREATE, 검토시작 UPDATE, 승인/기각 APPROVE.
|
||||
*/
|
||||
@Tag(name = "수수료 이의신청 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/disputes")
|
||||
@@ -24,18 +36,21 @@ public class CommissionDisputeController {
|
||||
|
||||
private final CommissionDisputeService service;
|
||||
|
||||
/** 이의신청 목록 조회(검색 + 페이징). */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "DISPUTE", perm = "READ")
|
||||
public ApiResponse<PageResponse<CommissionDisputeResp>> list(@ModelAttribute CommissionDisputeSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 이의신청 단건 상세 조회. */
|
||||
@GetMapping("/{disputeId}")
|
||||
@RequirePermission(menu = "DISPUTE", perm = "READ")
|
||||
public ApiResponse<CommissionDisputeResp> detail(@PathVariable Long disputeId) {
|
||||
return ApiResponse.ok(service.detail(disputeId));
|
||||
}
|
||||
|
||||
/** 이의신청 접수(OPEN 상태로 생성). */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "DISPUTE", perm = "CREATE")
|
||||
@DataChangeLog(menu = "DISPUTE", table = "commission_dispute")
|
||||
@@ -43,6 +58,7 @@ public class CommissionDisputeController {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 검토 시작(OPEN → IN_REVIEW). */
|
||||
@PostMapping("/{disputeId}/review")
|
||||
@RequirePermission(menu = "DISPUTE", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "DISPUTE", table = "commission_dispute")
|
||||
@@ -51,6 +67,10 @@ public class CommissionDisputeController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 이의신청 승인(IN_REVIEW → APPROVED).
|
||||
* body 의 resolution(처리내용), adjustmentAmount(보정금액, 선택), correctionId(연결된 정산정정ID, 선택)를 받습니다.
|
||||
*/
|
||||
@PostMapping("/{disputeId}/approve")
|
||||
@RequirePermission(menu = "DISPUTE", perm = "APPROVE")
|
||||
@DataChangeLog(menu = "DISPUTE", table = "commission_dispute")
|
||||
@@ -64,6 +84,7 @@ public class CommissionDisputeController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 이의신청 기각(IN_REVIEW → REJECTED). body 의 resolution 에 기각 사유를 담습니다. */
|
||||
@PostMapping("/{disputeId}/reject")
|
||||
@RequirePermission(menu = "DISPUTE", perm = "APPROVE")
|
||||
@DataChangeLog(menu = "DISPUTE", table = "commission_dispute")
|
||||
|
||||
@@ -15,6 +15,17 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 시책 지급(incentive_payment) 관리 컨트롤러.
|
||||
*
|
||||
* <p>"시책"은 보험사/GA가 특정 기간·상품에 거는 추가 인센티브(판매 장려금)입니다.
|
||||
* 시책 프로그램(incentive_program) 조건을 충족한 설계사에게 지급할 금액을 건별로 만들어 확정/취소합니다.
|
||||
*
|
||||
* <p>상태 흐름(IncentivePayStatus): CALCULATED(산출) → CONFIRMED(확정) → PAID(지급완료), 또는 CANCELLED(취소).
|
||||
* 확정은 CALCULATED 상태에서만, 취소는 PAID 가 아닐 때만 가능합니다. 생성·확정 시 정산월 마감 여부를 확인합니다.
|
||||
*
|
||||
* <p>권한(menu=INCENTIVE_PAY): 조회 READ, 생성 CREATE, 확정 APPROVE, 취소 UPDATE.
|
||||
*/
|
||||
@Tag(name = "시책 지급 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/incentive-payments")
|
||||
@@ -23,18 +34,21 @@ public class IncentivePaymentController {
|
||||
|
||||
private final IncentivePaymentService service;
|
||||
|
||||
/** 시책 지급 목록 조회(검색 + 페이징). */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "INCENTIVE_PAY", perm = "READ")
|
||||
public ApiResponse<PageResponse<IncentivePaymentResp>> list(@ModelAttribute IncentivePaymentSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 시책 지급 단건 상세 조회. */
|
||||
@GetMapping("/{incentivePayId}")
|
||||
@RequirePermission(menu = "INCENTIVE_PAY", perm = "READ")
|
||||
public ApiResponse<IncentivePaymentResp> detail(@PathVariable Long incentivePayId) {
|
||||
return ApiResponse.ok(service.detail(incentivePayId));
|
||||
}
|
||||
|
||||
/** 시책 지급 생성(CALCULATED 상태). 해당 정산월이 마감되어 있으면 거부됩니다. */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "INCENTIVE_PAY", perm = "CREATE")
|
||||
@DataChangeLog(menu = "INCENTIVE_PAY", table = "incentive_payment")
|
||||
@@ -42,6 +56,7 @@ public class IncentivePaymentController {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 시책 지급 확정(CALCULATED → CONFIRMED). 마감 가드 적용. APPROVE 권한 필요. */
|
||||
@PostMapping("/{incentivePayId}/confirm")
|
||||
@RequirePermission(menu = "INCENTIVE_PAY", perm = "APPROVE")
|
||||
@DataChangeLog(menu = "INCENTIVE_PAY", table = "incentive_payment")
|
||||
@@ -50,6 +65,7 @@ public class IncentivePaymentController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 시책 지급 취소(→ CANCELLED). body 의 cancelReason 에 취소 사유를 담습니다. 지급완료건은 취소 불가. */
|
||||
@PostMapping("/{incentivePayId}/cancel")
|
||||
@RequirePermission(menu = "INCENTIVE_PAY", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "INCENTIVE_PAY", table = "incentive_payment")
|
||||
|
||||
@@ -15,6 +15,16 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 시책 프로그램(incentive_program) 관리 컨트롤러.
|
||||
*
|
||||
* <p>시책 "프로그램"은 인센티브 지급의 규칙 정의입니다. 대상(등급/조직/상품군), 기간(시작월~종료월),
|
||||
* 지급 방식(정률 payRate 또는 정액 payAmountFixed), 예산(budgetAmount) 등을 설정합니다.
|
||||
* 실제 지급(incentive_payment)은 이 프로그램 조건에 따라 산출됩니다.
|
||||
*
|
||||
* <p>활성/비활성(isActive)은 toggleActive 로 전환합니다. 비활성 프로그램은 신규 지급 대상에서 제외됩니다.
|
||||
* 권한(menu=INCENTIVE_PROG): 조회 READ, 생성 CREATE, 수정/활성토글 UPDATE.
|
||||
*/
|
||||
@Tag(name = "시책 프로그램 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/incentive-programs")
|
||||
@@ -23,18 +33,21 @@ public class IncentiveProgramController {
|
||||
|
||||
private final IncentiveProgramService service;
|
||||
|
||||
/** 시책 프로그램 목록 조회(검색 + 페이징). */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "INCENTIVE_PROG", perm = "READ")
|
||||
public ApiResponse<PageResponse<IncentiveProgramResp>> list(@ModelAttribute IncentiveProgramSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
/** 시책 프로그램 단건 상세 조회. */
|
||||
@GetMapping("/{programId}")
|
||||
@RequirePermission(menu = "INCENTIVE_PROG", perm = "READ")
|
||||
public ApiResponse<IncentiveProgramResp> detail(@PathVariable Long programId) {
|
||||
return ApiResponse.ok(service.detail(programId));
|
||||
}
|
||||
|
||||
/** 시책 프로그램 신규 등록(기본 활성 상태로 생성). */
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "INCENTIVE_PROG", perm = "CREATE")
|
||||
@DataChangeLog(menu = "INCENTIVE_PROG", table = "incentive_program")
|
||||
@@ -42,6 +55,7 @@ public class IncentiveProgramController {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
/** 시책 프로그램 수정(전체 필드 갱신). */
|
||||
@PutMapping("/{programId}")
|
||||
@RequirePermission(menu = "INCENTIVE_PROG", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "INCENTIVE_PROG", table = "incentive_program")
|
||||
@@ -50,6 +64,7 @@ public class IncentiveProgramController {
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/** 활성/비활성 토글. body 의 isActive(true/false)로 사용 여부를 전환합니다. */
|
||||
@PutMapping("/{programId}/active")
|
||||
@RequirePermission(menu = "INCENTIVE_PROG", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "INCENTIVE_PROG", table = "incentive_program")
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.ga.api.controller.settle;
|
||||
|
||||
import com.ga.api.service.settle.PaymentBatchService;
|
||||
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.PaymentBatchItemUpdateReq;
|
||||
import com.ga.core.vo.settle.PaymentBatchResp;
|
||||
import com.ga.core.vo.settle.PaymentBatchSaveReq;
|
||||
import com.ga.core.vo.settle.PaymentBatchSearchParam;
|
||||
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 lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 지급대상 배치(payment_batch) 관리 컨트롤러.
|
||||
*
|
||||
* <p>"배치"는 한 정산월에 실제로 돈을 보낼 대상 설계사들을 한 묶음으로 모은 작업 단위입니다.
|
||||
* 담당자는 배치를 만들어(create) 대상 명단을 확인하고, 빼야 할 사람은 제외(updateItem)한 뒤,
|
||||
* 최종 확정(finalize)하여 이후 이체 처리로 넘깁니다.
|
||||
*
|
||||
* <p>상태 흐름(PaymentBatchStatus): DRAFT(작성중) → FINALIZED(확정) / CANCELLED(취소).
|
||||
* 수정(updateItem)은 DRAFT 상태에서만 가능합니다.
|
||||
*
|
||||
* <p>권한(menu=PAYMENT_TARGET): 생성 CREATE, 조회/엑셀 READ, 항목수정 UPDATE, 확정 EXECUTE.
|
||||
*/
|
||||
@Tag(name = "지급대상 배치 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/payment-batches")
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentBatchController {
|
||||
|
||||
private final PaymentBatchService service;
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "PAYMENT_TARGET", perm = "CREATE")
|
||||
@DataChangeLog(menu = "PAYMENT_TARGET", table = "payment_batch")
|
||||
@Operation(summary = "배치 생성", description = "정산월의 지급대상(payable_amount>0) 건으로 DRAFT 배치를 구성합니다")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody PaymentBatchSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "PAYMENT_TARGET", perm = "READ")
|
||||
@Operation(summary = "배치 목록 조회")
|
||||
public ApiResponse<PageResponse<PaymentBatchResp>> list(@ModelAttribute PaymentBatchSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@RequirePermission(menu = "PAYMENT_TARGET", perm = "READ")
|
||||
@Operation(summary = "배치 상세 조회", description = "배치 헤더 + items 포함")
|
||||
public ApiResponse<PaymentBatchResp> detail(@PathVariable Long id) {
|
||||
return ApiResponse.ok(service.detail(id));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/items/{itemId}")
|
||||
@RequirePermission(menu = "PAYMENT_TARGET", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "PAYMENT_TARGET", table = "payment_batch_item")
|
||||
@Operation(summary = "지급대상건 포함/제외 수정", description = "DRAFT 배치만 수정 가능합니다")
|
||||
public ApiResponse<Void> updateItem(@PathVariable Long id,
|
||||
@PathVariable Long itemId,
|
||||
@Valid @RequestBody PaymentBatchItemUpdateReq req) {
|
||||
service.updateItem(id, itemId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/finalize")
|
||||
@RequirePermission(menu = "PAYMENT_TARGET", perm = "EXECUTE")
|
||||
@DataChangeLog(menu = "PAYMENT_TARGET", table = "payment_batch")
|
||||
@Operation(summary = "배치 확정", description = "DRAFT → FINALIZED. 포함건이 없으면 확정 불가")
|
||||
public ApiResponse<Void> finalize(@PathVariable Long id) {
|
||||
service.finalize(id);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/cancel")
|
||||
@RequirePermission(menu = "PAYMENT_TARGET", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "PAYMENT_TARGET", table = "payment_batch")
|
||||
@Operation(summary = "배치 취소")
|
||||
public ApiResponse<Void> cancel(@PathVariable Long id) {
|
||||
service.cancel(id);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/export")
|
||||
@RequirePermission(menu = "PAYMENT_TARGET", perm = "READ")
|
||||
@Operation(summary = "배치 items 엑셀 다운로드")
|
||||
public void export(@PathVariable Long id, HttpServletResponse response) {
|
||||
service.export(id, response);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,19 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 지급(payment) 관리 컨트롤러.
|
||||
*
|
||||
* <p>정산 마스터가 "이번 달 줄 금액"을 확정하면, 실제 계좌로 보낼 한 건 한 건이 payment(지급) 레코드가 됩니다.
|
||||
* 이 컨트롤러는 지급 건 목록 조회, 상태 변경, 세금 계산, 차감 적용, 은행 이체파일 생성을 담당합니다.
|
||||
*
|
||||
* <p>지급 한 건의 금액 계산 순서(도메인):
|
||||
* payAmount(지급원금) → 세금(소득세·지방세·부가세) 차감 → 각종 공제(차감) 차감 → netAmount(실지급액).
|
||||
* 이 netAmount 가 최종적으로 설계사 통장에 들어가는 금액입니다.
|
||||
*
|
||||
* <p>@Validated + @Pattern 으로 settleMonth 는 YYYYMM(숫자 6자리) 형식만 허용합니다.
|
||||
* 권한(menu=PAYMENT): 조회 READ, 상태/세금/차감 변경 UPDATE, 이체파일 생성 EXPORT.
|
||||
*/
|
||||
@Tag(name = "지급 관리")
|
||||
@Validated
|
||||
@RestController
|
||||
@@ -25,12 +38,14 @@ public class PaymentController {
|
||||
|
||||
private final PaymentService paymentService;
|
||||
|
||||
/** 지급 건 목록 조회(검색 + 페이징). */
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "PAYMENT", perm = "READ")
|
||||
public ApiResponse<PageResponse<PaymentResp>> list(@ModelAttribute SettleSearchParam param) {
|
||||
return ApiResponse.ok(paymentService.list(param));
|
||||
}
|
||||
|
||||
/** 지급 상태 수동 변경(예: 실패 처리). 실패 시 req.failReason 에 사유를 담아 기록합니다. */
|
||||
@PutMapping("/{paymentId}/status")
|
||||
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
||||
|
||||
@@ -4,6 +4,10 @@ import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 지급 상태 변경 요청 DTO.
|
||||
* status: 변경할 상태값(필수). failReason: 실패 처리 시 사유(선택).
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class PaymentStatusUpdateReq {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user