feat: ga-core + ga-api + ga-batch + V13-V15 + Docker + frontend skeleton

ga-core (22 tables):
- user/role: VO + Mapper + XML (login + RBAC)
- org: Grade, Organization (CTE recursive tree), Agent + history
- product: InsuranceCompany, Product, Contract
- rule: CommissionRate, PayoutRule, OverrideRule, ChargebackRule, ExceptionTypeCode
- receive: CompanyProfile, FieldMapping, CodeMapping, RawCommissionData, ParseError
- ledger: Recruit, Maintain, Exception (batch insert + cursor)
- settle: SettleMaster (UPSERT), Override, Chargeback, Payment, Reconciliation, BatchJobLog
- All XMLs use EncryptTypeHandler for PII fields

ga-api:
- AuthController: login (lock on N fails), refresh, me, password, logout
- Domain controllers: Agent, Organization, Contract, Company, Product, Rule,
  Receive, Ledger (recruit/maintain/exception + approve), Settle (confirm/hold/release),
  Payment, User, Role (matrix), BatchTrigger
- All use @RequirePermission + @DataChangeLog

ga-batch:
- MonthlySettlementJob: 8 Steps (receiveData, transformData, reconcile,
  calcRecruit, calcMaintain, chargebackException, calcOverride, aggregate)
- DataReceiver strategy + ManualReceiver, MappingEngine stub
- CommissionCalculator (pct/tax via MoneyUtil), JobLauncherController

DB V13~V15:
- V13: 14 indexes (settle_master, ledgers, contract, chargeback, payment, raw, logs)
- V14: create_monthly_partition() helper function
- V15: v_agent_monthly_summary, v_org_settle_summary, v_chargeback_risk views

Infra:
- docker-compose: postgres + redis + api + batch + frontend
- Multi-stage Dockerfile (gradle build → JRE)

ga-frontend (skeleton):
- Vite + React 18 + TypeScript + AntD 5 + AG Grid + React Query + Zustand
- API request layer (axios + JWT), LoginPage, MainLayout (dynamic menu), Dashboard
- nginx.conf for /api proxying
This commit is contained in:
GA Pro
2026-05-09 22:00:53 +09:00
parent cef4e48e27
commit 7f3c564605
147 changed files with 6987 additions and 79 deletions
+33
View File
@@ -0,0 +1,33 @@
# Multi-module Spring Boot 빌드 (ga-api 또는 ga-batch 선택)
# Build: docker build --build-arg MODULE=ga-api -t ga-api .
ARG MODULE=ga-api
# Stage 1: build
FROM gradle:8.6-jdk17 AS builder
WORKDIR /work
COPY settings.gradle build.gradle gradle.properties ./
COPY ga-common/build.gradle ga-common/
COPY ga-core/build.gradle ga-core/
COPY ga-api/build.gradle ga-api/
COPY ga-batch/build.gradle ga-batch/
COPY ga-admin/build.gradle ga-admin/
# 의존성 사전 다운로드 (캐시 최적화)
RUN gradle --no-daemon dependencies > /dev/null 2>&1 || true
COPY ga-common ga-common
COPY ga-core ga-core
COPY ga-api ga-api
COPY ga-batch ga-batch
COPY ga-admin ga-admin
ARG MODULE
RUN gradle --no-daemon :${MODULE}:bootJar -x test
# Stage 2: runtime
FROM eclipse-temurin:17-jre-alpine
ARG MODULE
WORKDIR /app
COPY --from=builder /work/${MODULE}/build/libs/${MODULE}.jar /app/app.jar
ENV TZ=Asia/Seoul
EXPOSE 8080 8081
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
+120 -79
View File
@@ -1,6 +1,6 @@
# GA 수수료 정산 솔루션
법인보험대리점(GA) 수수료 정산 시스템.
법인보험대리점(GA) 수수료 정산 시스템 — Multi-module Spring Boot + React.
## 기술 스택
@@ -8,106 +8,147 @@
|------|------|
| Backend | Java 17, Spring Boot 3.2, Spring Security, Spring Batch |
| ORM | MyBatis 3 + PageHelper |
| DB | PostgreSQL 14+ |
| DB | PostgreSQL 16 |
| Cache | Redis 7 |
| Build | Gradle 멀티모듈 |
| Frontend | (예정) React 18 + Ant Design 5 + AG Grid |
| Frontend | React 18 + Vite + TypeScript + Ant Design 5 + AG Grid + React Query + Zustand |
| Infra | Docker Compose, Nginx |
## 모듈 구조
```
ga-commission-system/
├── ga-common/ # 공통 프레임워크 (인증/권한/로그/엑셀/공통코드/메뉴)
├── ga-core/ # 도메인 코어 (VO, Mapper, SQL) — 예정
├── ga-api/ # REST API (실행 모듈)
├── ga-batch/ # Spring Batch (실행 모듈) — 예정
── ga-admin/ # 관리자 기능 — 예정
├── ga-common/ # 공통 프레임워크 (인증/권한/로그/엑셀/공통코드/메뉴)
├── ga-core/ # 도메인 코어 (VO, Mapper, SQL)
├── ga-api/ # REST API (포트 8080)
├── ga-batch/ # Spring Batch (포트 8081)
── ga-admin/ # 관리자 라이브러리
└── ga-frontend/ # React SPA (포트 3000)
```
## 진행 상황
- [x] 1단계 — 프로젝트 뼈대 + DB 마이그레이션
- [x] Gradle 멀티모듈 + 5개 모듈
- [x] Flyway V1~V12 (조직/상품/규정/수신/원장/정산/배치/공통코드/메뉴/시스템/초기데이터)
- [x] **ga-common — 공통 프레임워크 (완성)**
- [x] `model/` — ApiResponse, PageResponse, SearchParam, TreeNode
- [x] `exception/` — ErrorCode, BizException, GlobalExceptionHandler
- [x] `annotation/`@RequirePermission, @DataChangeLog, @ExcelColumn, @Mask
- [x] `aop/` — Permission/DataChangeLog/ApiLog Aspect
- [x] `auth/` — JwtTokenProvider, JwtAuthFilter, LoginUser
- [x] `security/` — SecurityConfig, JwtAuthEntryPoint, AccessDeniedHandler
- [x] `config/` — MyBatis/Redis/Swagger/Web/Async Config
- [x] `mybatis/` — BaseMapper, EncryptTypeHandler, JsonTypeHandler, AuditInterceptor
- [x] `code/` — CommonCodeService(Redis 캐시) + Controller + Mapper
- [x] `menu/` — MenuService, PermissionChecker, MenuTreeBuilder + Controller
- [x] `excel/` — 대용량 Export(SXSSF+Cursor) / Import(SAX+배치)
- [x] `file/` — FileService + Controller
- [x] `system/` — SystemConfig, DataChangeLog, ApiAccessLog Service
- [x] `notification/` — NotificationService + Controller
- [x] `util/` — DateUtil, MoneyUtil, MaskUtil, EncryptUtil, SecurityUtil
- [x] `grid/` — GridSearchParam, GridResponse (AG Grid 서버사이드)
- [ ] ga-core (예정) — 22개 테이블의 VO/Mapper/XML
- [ ] ga-api (예정) — Controller + Service
- [ ] ga-batch (예정) — MonthlySettlementJob 8 Step
- [ ] ga-frontend (예정) — React + AntD + AG Grid
## 실행 (DB만 우선 검증)
## 빠른 시작 (Docker Compose)
```bash
# PostgreSQL & Redis 기동 후 (예: docker compose)
psql -h localhost -U ga -c "CREATE DATABASE ga"
docker compose up -d --build
# 프론트: http://localhost:3000
# API: http://localhost:8080/swagger-ui.html
# Batch: http://localhost:8081 (내부)
```
# Flyway 마이그레이션은 ga-api 부팅 시 자동 실행됨
# (ga-api의 spring.datasource 와 Flyway 설정에 따름)
## 로컬 개발
### 1. PostgreSQL + Redis 기동
```bash
docker compose up -d postgres redis
```
### 2. ga-api 실행 (Flyway 자동 마이그레이션)
```bash
./gradlew :ga-api:bootRun
```
## 핵심 사용 예시
### 3. ga-batch 실행
```bash
./gradlew :ga-batch:bootRun
```
### 1) 컨트롤러 표준 패턴
### 4. 프론트 실행
```bash
cd ga-frontend
npm install
npm run dev
# http://localhost:3000
```
## 주요 기능
### 백엔드
- **인증/권한**: JWT (HS256), 메뉴×권한 RBAC, BCrypt 비밀번호
- **공통코드**: Redis 캐시, 시스템코드 잠금, REST CRUD
- **메뉴**: 트리 구조, 사용자별 동적 메뉴 + 권한 매트릭스
- **엑셀**: SXSSF+Cursor 다운로드(100만건), SAX+배치 업로드(70만건)
- **로그**: 로그인/API/데이터변경 자동 기록 (비동기)
- **암호화**: AES-256 CBC (resident_no, account_no)
### 도메인
- **조직**: 트리 구조, CTE 재귀, 인사 이력
- **계약**: 보험사/상품/계약 관리
- **수수료 규정**: 보험사율, 지급율, 오버라이드, 환수, 예외코드
- **원장**: 모집/유지/예외 (PageHelper, AG Grid 서버사이드 가능)
- **정산**: 8 Step 배치 (수신→변환→대사→모집→유지→환수/예외→오버라이드→집계)
- **지급**: 이체파일 생성, 상태 추적
### 정산 배치 8 Step
1. **receiveData** — 보험사별 DataReceiver → raw_commission_data 적재
2. **transformData** — MappingEngine으로 표준 변환
3. **reconcile** — 보험사 통보액 vs 시스템 계산액 대사
4. **calcRecruit** — 모집수수료 (계약시점 규정 적용)
5. **calcMaintain** — 유지수수료 (납입주기 매칭)
6. **chargebackException** — 환수 자동생성
7. **calcOverride** — 조직 트리 오버라이드
8. **aggregate** — 통합 집계 → settle_master UPSERT (3.3% 세금)
## 신입 개발자 표준 패턴
### Backend (5분)
```java
@RestController @RequestMapping("/api/agents")
@RequiredArgsConstructor
public class AgentController {
private final AgentService service;
// 1) VO (XxxVO + XxxResp + XxxSaveReq + XxxSearchParam)
@Data
public class FooSearchParam extends SearchParam { }
// 2) Mapper Interface + XML
@Mapper
public interface FooMapper {
List<FooResp> selectList(FooSearchParam p);
}
// 3) Service
@Service @RequiredArgsConstructor
public class FooService {
private final FooMapper mapper;
public PageResponse<FooResp> list(FooSearchParam p) {
p.startPage();
return PageResponse.of(mapper.selectList(p));
}
}
// 4) Controller
@RestController @RequestMapping("/api/foos") @RequiredArgsConstructor
public class FooController {
private final FooService service;
@GetMapping
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
public ApiResponse<PageResponse<AgentResp>> list(AgentSearchParam param) {
return ApiResponse.ok(service.list(param));
@RequirePermission(menu = "FOO", perm = "READ")
public ApiResponse<PageResponse<FooResp>> list(FooSearchParam p) {
return ApiResponse.ok(service.list(p));
}
}
```
### 2) 서비스 표준 패턴
```java
public PageResponse<AgentResp> list(AgentSearchParam param) {
param.startPage(); // PageHelper
return PageResponse.of(mapper.selectList(param));
}
```
### 3) 공통코드 검증
```java
codeService.validateCode("AGENT_STATUS", req.getStatus()); // 없으면 예외
String name = codeService.getCodeName("AGENT_STATUS", "ACTIVE"); // "정상"
```
### 4) 대용량 엑셀 다운로드
```java
@GetMapping("/export")
@RequirePermission(menu = "LEDGER", perm = "EXPORT")
public void export(LedgerSearchParam param, HttpServletResponse res) {
excelService.exportLargeExcel("모집수수료원장",
RecruitLedgerExcelVO.class,
() -> ledgerMapper.selectCursor(param), res);
}
```
## 보안
- JWT (HS256) — 액세스 2시간, 리프레시 7일
- AES-256 CBC 암호화 (`resident_no`, `account_no`)`EncryptTypeHandler` 자동 적용
- RBAC — `role_menu_permission` 기반 메뉴×권한 매트릭스
- AES-256 CBC 암호화 (자동 적용`EncryptTypeHandler`)
- RBAC — `role_menu_permission` 매트릭스
- `@DataChangeLog` — 데이터 변경 이력 AOP 자동
- API 접근 로그 — 비동기 저장
## DB 마이그레이션
- V1~V8: 도메인 스키마 + 초기 데이터
- V9~V12: 공통코드/메뉴/시스템관리 + 초기 데이터
- V13: 인덱스 최적화
- V14: 파티셔닝 helper 함수
- V15: 통계/리포트 뷰 (v_agent_monthly_summary, v_org_settle_summary, v_chargeback_risk)
## 진행 상황
- [x] 1단계 — 프로젝트 뼈대 + DB (V1~V15)
- [x] **ga-common** — 공통 프레임워크 (model/exception/auth/security/code/menu/excel/file/system/notification)
- [x] **ga-core** — 22개 테이블의 VO/Mapper/XML
- [x] **ga-api** — Auth + 도메인 컨트롤러 (org/product/rule/receive/ledger/settle/payment/system)
- [x] **ga-batch** — MonthlySettlementJob 8 Step
- [x] **Docker** — docker-compose + multi-stage Dockerfile + nginx
- [x] **ga-frontend** — Vite + React + AntD + React Query 스켈레톤 (로그인 + 대시보드 + 동적 메뉴)
## 향후 확장
- **프론트 페이지 추가**: menu 테이블 등록 → React 페이지 구현 → 동적 라우팅 적용
- **신규 보험사**: company_profile + company_field_mapping 등록 + DataReceiver 구현
- **신규 정산 규정**: commission_rate/payout_rule/override_rule/chargeback_rule 의 effective_from 만 다른 버전으로 추가
+88
View File
@@ -0,0 +1,88 @@
services:
postgres:
image: postgres:16-alpine
container_name: ga-postgres
environment:
POSTGRES_DB: ga
POSTGRES_USER: ga
POSTGRES_PASSWORD: ga
TZ: Asia/Seoul
ports:
- "5432:5432"
volumes:
- ga-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ga -d ga"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: ga-redis
ports:
- "6379:6379"
volumes:
- ga-redis-data:/data
api:
build:
context: .
dockerfile: Dockerfile
args:
MODULE: ga-api
container_name: ga-api
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
environment:
SPRING_PROFILES_ACTIVE: docker
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/ga
SPRING_DATASOURCE_USERNAME: ga
SPRING_DATASOURCE_PASSWORD: ga
SPRING_DATA_REDIS_HOST: redis
JWT_SECRET: ${JWT_SECRET:-please-change-this-jwt-secret-must-be-256-bits}
ENCRYPT_KEY: ${ENCRYPT_KEY:-please-change-this-32-byte-key!}
ports:
- "8080:8080"
batch:
build:
context: .
dockerfile: Dockerfile
args:
MODULE: ga-batch
container_name: ga-batch
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
api:
condition: service_started
environment:
SPRING_PROFILES_ACTIVE: docker
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/ga
SPRING_DATASOURCE_USERNAME: ga
SPRING_DATASOURCE_PASSWORD: ga
SPRING_DATA_REDIS_HOST: redis
JWT_SECRET: ${JWT_SECRET:-please-change-this-jwt-secret-must-be-256-bits}
ENCRYPT_KEY: ${ENCRYPT_KEY:-please-change-this-32-byte-key!}
ports:
- "8081:8081"
frontend:
build:
context: ./ga-frontend
dockerfile: Dockerfile
container_name: ga-frontend
depends_on:
- api
ports:
- "3000:80"
volumes:
ga-postgres-data:
ga-redis-data:
@@ -0,0 +1,53 @@
package com.ga.api.auth;
import com.ga.common.model.ApiResponse;
import com.ga.core.vo.user.UserResp;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Tag(name = "인증")
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/login")
public ApiResponse<LoginResp> login(@Valid @RequestBody LoginReq req, HttpServletRequest http) {
return ApiResponse.ok(authService.login(req, ipOf(http), http.getHeader("User-Agent")));
}
@PostMapping("/refresh")
public ApiResponse<LoginResp> refresh(@RequestBody Map<String, String> body) {
return ApiResponse.ok(authService.refresh(body.get("refreshToken")));
}
@GetMapping("/me")
public ApiResponse<UserResp> me() {
return ApiResponse.ok(authService.me());
}
@PostMapping("/password")
public ApiResponse<Void> changePassword(@RequestBody Map<String, String> body) {
authService.changePassword(body.get("oldPassword"), body.get("newPassword"));
return ApiResponse.ok();
}
@PostMapping("/logout")
public ApiResponse<Void> logout() {
authService.logout();
return ApiResponse.ok();
}
private String ipOf(HttpServletRequest req) {
String xf = req.getHeader("X-Forwarded-For");
if (xf != null && !xf.isBlank()) return xf.split(",")[0].trim();
return req.getRemoteAddr();
}
}
@@ -0,0 +1,139 @@
package com.ga.api.auth;
import com.ga.common.auth.JwtTokenProvider;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.common.system.SystemConfigService;
import com.ga.common.system.SystemLogMapper;
import com.ga.common.util.SecurityUtil;
import com.ga.core.mapper.user.UserMapper;
import com.ga.core.vo.user.UserResp;
import com.ga.core.vo.user.UserVO;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider tokenProvider;
private final SystemConfigService configService;
private final SystemLogMapper systemLogMapper;
@Transactional
public LoginResp login(LoginReq req, String ipAddress, String userAgent) {
UserVO user = userMapper.selectByLoginId(req.getLoginId());
int failLimit = configService.getInt("LOGIN_FAIL_LIMIT", 5);
if (user == null) {
saveLoginLog(null, req.getLoginId(), "FAIL", "USER_NOT_FOUND", ipAddress, userAgent);
throw new BizException(ErrorCode.LOGIN_FAIL);
}
if ("LOCKED".equals(user.getStatus())) {
saveLoginLog(user.getUserId(), req.getLoginId(), "FAIL", "ACCOUNT_LOCKED", ipAddress, userAgent);
throw new BizException(ErrorCode.ACCOUNT_LOCKED);
}
if ("RETIRED".equals(user.getStatus())) {
throw new BizException(ErrorCode.ACCOUNT_RETIRED);
}
if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) {
userMapper.incrementFailCount(user.getUserId());
int newFail = (user.getFailCount() == null ? 0 : user.getFailCount()) + 1;
if (newFail >= failLimit) {
userMapper.updateStatus(user.getUserId(), "LOCKED");
}
saveLoginLog(user.getUserId(), req.getLoginId(), "FAIL", "BAD_PASSWORD", ipAddress, userAgent);
throw new BizException(ErrorCode.LOGIN_FAIL);
}
// 로그인 성공
userMapper.updateLastLogin(user.getUserId());
List<String> roles = userMapper.selectRoleCodes(user.getUserId());
String access = tokenProvider.createAccessToken(user.getUserId(), user.getLoginId(), roles);
String refresh = tokenProvider.createRefreshToken(user.getUserId(), user.getLoginId());
saveLoginLog(user.getUserId(), req.getLoginId(), "SUCCESS", null, ipAddress, userAgent);
return LoginResp.builder()
.accessToken(access)
.refreshToken(refresh)
.accessTokenExpiresIn(tokenProvider.getAccessExpire())
.userId(user.getUserId())
.loginId(user.getLoginId())
.userName(user.getUserName())
.roles(roles)
.build();
}
public LoginResp refresh(String refreshToken) {
Claims claims = tokenProvider.parse(refreshToken);
if (!"REFRESH".equals(claims.get("type"))) {
throw new BizException(ErrorCode.TOKEN_INVALID);
}
Long userId = claims.get("uid", Long.class);
UserVO user = userMapper.selectById(userId);
if (user == null || !"ACTIVE".equals(user.getStatus())) {
throw new BizException(ErrorCode.UNAUTHORIZED);
}
List<String> roles = userMapper.selectRoleCodes(userId);
String newAccess = tokenProvider.createAccessToken(userId, user.getLoginId(), roles);
return LoginResp.builder()
.accessToken(newAccess)
.accessTokenExpiresIn(tokenProvider.getAccessExpire())
.userId(userId)
.loginId(user.getLoginId())
.userName(user.getUserName())
.roles(roles)
.build();
}
public UserResp me() {
Long userId = SecurityUtil.getCurrentUserId();
if (userId == null) throw new BizException(ErrorCode.UNAUTHORIZED);
UserResp resp = userMapper.selectDetailById(userId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
resp.setRoleCodes(userMapper.selectRoleCodes(userId));
return resp;
}
@Transactional
public void changePassword(String oldPassword, String newPassword) {
Long userId = SecurityUtil.getCurrentUserId();
if (userId == null) throw new BizException(ErrorCode.UNAUTHORIZED);
UserVO user = userMapper.selectById(userId);
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
throw new BizException(ErrorCode.LOGIN_FAIL, "기존 비밀번호가 일치하지 않습니다");
}
validatePasswordPolicy(newPassword);
userMapper.updatePassword(userId, passwordEncoder.encode(newPassword));
}
public void logout() {
Long userId = SecurityUtil.getCurrentUserId();
saveLoginLog(userId, SecurityUtil.getCurrentLoginId(), "LOGOUT", null, null, null);
}
private void saveLoginLog(Long userId, String loginId, String type, String reason, String ip, String ua) {
try {
systemLogMapper.insertLoginLog(userId, loginId, type, reason, ip, ua);
} catch (Exception e) {
log.warn("Login log save failed: {}", e.getMessage());
}
}
private void validatePasswordPolicy(String password) {
int min = configService.getInt("PASSWORD_MIN_LEN", 8);
if (password == null || password.length() < min) {
throw new BizException(ErrorCode.PASSWORD_POLICY,
"비밀번호는 " + min + "자 이상이어야 합니다");
}
}
}
@@ -0,0 +1,12 @@
package com.ga.api.auth;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginReq {
@NotBlank(message = "아이디는 필수입니다")
private String loginId;
@NotBlank(message = "비밀번호는 필수입니다")
private String password;
}
@@ -0,0 +1,18 @@
package com.ga.api.auth;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class LoginResp {
private String accessToken;
private String refreshToken;
private long accessTokenExpiresIn; // ms
private Long userId;
private String loginId;
private String userName;
private List<String> roles;
}
@@ -0,0 +1,71 @@
package com.ga.api.controller.batch;
import com.ga.common.annotation.RequirePermission;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.common.model.ApiResponse;
import com.ga.core.mapper.settle.BatchJobLogMapper;
import com.ga.core.vo.settle.BatchJobLogVO;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 배치 트리거 — 실제 배치 잡 실행은 ga-batch 모듈의 별도 Spring Boot 프로세스에서 수행.
* ga-api 에서는 batch_job_log 만 조회/요청 등록.
*
* 운영 환경에서는 메시지 큐(Kafka, Redis Streams) 또는
* Spring Batch JobLauncher REST API 로 ga-batch 프로세스를 호출하는 것을 권장.
*/
@Slf4j
@Tag(name = "배치")
@RestController
@RequestMapping("/api/batch")
@RequiredArgsConstructor
public class BatchTriggerController {
private final BatchJobLogMapper jobLogMapper;
/** 정산 배치 시작 요청 (실제 실행은 ga-batch 프로세스가 폴링/큐) */
@PostMapping("/settlement/run")
@RequirePermission(menu = "BATCH_RUN", perm = "APPROVE")
public ApiResponse<Long> runSettlement(@RequestBody Map<String, String> body) {
String settleMonth = body.get("settleMonth");
if (settleMonth == null || !settleMonth.matches("\\d{6}")) {
throw new BizException(ErrorCode.INVALID_PARAMETER, "settleMonth (yyyyMM) 가 필요합니다");
}
if (jobLogMapper.isRunning("MONTHLY_SETTLEMENT") > 0) {
throw new BizException(ErrorCode.BATCH_RUNNING);
}
BatchJobLogVO log = new BatchJobLogVO();
log.setJobName("MONTHLY_SETTLEMENT");
log.setSettleMonth(settleMonth);
log.setStatus("REQUESTED");
log.setTriggeredBy(com.ga.common.util.SecurityUtil.getCurrentUserId());
jobLogMapper.insert(log);
return ApiResponse.ok(log.getJobId());
}
@GetMapping("/status")
@RequirePermission(menu = "BATCH_RUN", perm = "READ")
public ApiResponse<BatchJobLogVO> latest(@RequestParam(defaultValue = "MONTHLY_SETTLEMENT") String jobName) {
return ApiResponse.ok(jobLogMapper.selectLatest(jobName));
}
@GetMapping("/jobs")
@RequirePermission(menu = "BATCH_RUN", perm = "READ")
public ApiResponse<List<BatchJobLogVO>> list(@RequestParam(required = false) String jobName,
@RequestParam(required = false) String settleMonth) {
return ApiResponse.ok(jobLogMapper.selectList(jobName, settleMonth));
}
@GetMapping("/jobs/{jobId}")
@RequirePermission(menu = "BATCH_RUN", perm = "READ")
public ApiResponse<BatchJobLogVO> detail(@PathVariable Long jobId) {
return ApiResponse.ok(jobLogMapper.selectById(jobId));
}
}
@@ -0,0 +1,99 @@
package com.ga.api.controller.ledger;
import com.ga.api.service.ledger.LedgerService;
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.mapper.ledger.MaintainLedgerMapper;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.vo.ledger.*;
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.Map;
@Tag(name = "원장 관리")
@RestController
@RequestMapping("/api/ledger")
@RequiredArgsConstructor
public class LedgerController {
private final LedgerService service;
private final RecruitLedgerMapper recruitMapper;
private final MaintainLedgerMapper maintainMapper;
private final ExcelService excelService;
// ===== 모집 =====
@GetMapping("/recruit")
@RequirePermission(menu = "LEDGER_RECRUIT", perm = "READ")
public ApiResponse<PageResponse<LedgerResp>> listRecruit(@ModelAttribute LedgerSearchParam param) {
return ApiResponse.ok(service.listRecruit(param));
}
@GetMapping("/recruit/{ledgerId}")
@RequirePermission(menu = "LEDGER_RECRUIT", perm = "READ")
public ApiResponse<LedgerResp> detailRecruit(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailRecruit(ledgerId));
}
@GetMapping("/recruit/export")
@RequirePermission(menu = "LEDGER_RECRUIT", perm = "EXPORT")
public void exportRecruit(@ModelAttribute LedgerSearchParam param, HttpServletResponse res) {
excelService.exportLargeExcel("모집수수료원장", LedgerExcelVO.class,
() -> recruitMapper.selectCursorForExcel(param), res);
}
// ===== 유지 =====
@GetMapping("/maintain")
@RequirePermission(menu = "LEDGER_MAINTAIN", perm = "READ")
public ApiResponse<PageResponse<LedgerResp>> listMaintain(@ModelAttribute LedgerSearchParam param) {
return ApiResponse.ok(service.listMaintain(param));
}
@GetMapping("/maintain/{ledgerId}")
@RequirePermission(menu = "LEDGER_MAINTAIN", perm = "READ")
public ApiResponse<LedgerResp> detailMaintain(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailMaintain(ledgerId));
}
@GetMapping("/maintain/export")
@RequirePermission(menu = "LEDGER_MAINTAIN", perm = "EXPORT")
public void exportMaintain(@ModelAttribute LedgerSearchParam param, HttpServletResponse res) {
excelService.exportLargeExcel("유지수수료원장", LedgerExcelVO.class,
() -> maintainMapper.selectCursorForExcel(param), res);
}
// ===== 예외 =====
@GetMapping("/exception")
@RequirePermission(menu = "LEDGER_EXCEPTION", perm = "READ")
public ApiResponse<PageResponse<ExceptionLedgerResp>> listException(@ModelAttribute LedgerSearchParam param) {
return ApiResponse.ok(service.listException(param));
}
@GetMapping("/exception/{ledgerId}")
@RequirePermission(menu = "LEDGER_EXCEPTION", perm = "READ")
public ApiResponse<ExceptionLedgerResp> detailException(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailException(ledgerId));
}
@PostMapping("/exception")
@RequirePermission(menu = "LEDGER_EXCEPTION", perm = "CREATE")
@DataChangeLog(menu = "LEDGER_EXCEPTION", table = "exception_ledger")
public ApiResponse<Long> createException(@Valid @RequestBody ExceptionLedgerSaveReq req) {
return ApiResponse.ok(service.createException(req));
}
@PutMapping("/exception/{ledgerId}/approve")
@RequirePermission(menu = "LEDGER_EXCEPTION", perm = "APPROVE")
@DataChangeLog(menu = "LEDGER_EXCEPTION", table = "exception_ledger")
public ApiResponse<Void> approveException(@PathVariable Long ledgerId,
@RequestBody Map<String, String> body) {
service.approveException(ledgerId, body.get("status"));
return ApiResponse.ok();
}
}
@@ -0,0 +1,77 @@
package com.ga.api.controller.org;
import com.ga.api.service.org.AgentService;
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.mapper.org.AgentMapper;
import com.ga.core.vo.org.*;
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;
@Tag(name = "설계사 관리")
@RestController
@RequestMapping("/api/agents")
@RequiredArgsConstructor
public class AgentController {
private final AgentService agentService;
private final AgentMapper agentMapper;
private final ExcelService excelService;
@GetMapping
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
public ApiResponse<PageResponse<AgentResp>> list(@ModelAttribute AgentSearchParam param) {
return ApiResponse.ok(agentService.list(param));
}
@GetMapping("/{agentId}")
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
public ApiResponse<AgentResp> detail(@PathVariable Long agentId) {
return ApiResponse.ok(agentService.detail(agentId));
}
@PostMapping
@RequirePermission(menu = "ORG_AGENT", perm = "CREATE")
@DataChangeLog(menu = "ORG_AGENT", table = "agent")
public ApiResponse<Long> create(@Valid @RequestBody AgentSaveReq req) {
return ApiResponse.ok(agentService.create(req));
}
@PutMapping("/{agentId}")
@RequirePermission(menu = "ORG_AGENT", perm = "UPDATE")
@DataChangeLog(menu = "ORG_AGENT", table = "agent")
public ApiResponse<Void> update(@PathVariable Long agentId, @Valid @RequestBody AgentSaveReq req) {
agentService.update(agentId, req);
return ApiResponse.ok();
}
@PutMapping("/{agentId}/status")
@RequirePermission(menu = "ORG_AGENT", perm = "UPDATE")
@DataChangeLog(menu = "ORG_AGENT", table = "agent")
public ApiResponse<Void> changeStatus(@PathVariable Long agentId, @RequestBody Map<String, String> body) {
agentService.changeStatus(agentId, body.get("status"));
return ApiResponse.ok();
}
@GetMapping("/{agentId}/history")
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
public ApiResponse<List<AgentOrgHistoryVO>> history(@PathVariable Long agentId) {
return ApiResponse.ok(agentService.history(agentId));
}
@GetMapping("/export")
@RequirePermission(menu = "ORG_AGENT", perm = "EXPORT")
public void export(@ModelAttribute AgentSearchParam param, HttpServletResponse response) {
excelService.exportLargeExcel("설계사목록", AgentExcelVO.class,
() -> agentMapper.selectCursorForExcel(param), response);
}
}
@@ -0,0 +1,65 @@
package com.ga.api.controller.org;
import com.ga.api.service.org.OrganizationService;
import com.ga.common.annotation.DataChangeLog;
import com.ga.common.annotation.RequirePermission;
import com.ga.common.model.ApiResponse;
import com.ga.core.vo.org.OrganizationResp;
import com.ga.core.vo.org.OrganizationVO;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "조직 관리")
@RestController
@RequestMapping("/api/orgs")
@RequiredArgsConstructor
public class OrganizationController {
private final OrganizationService service;
@GetMapping("/tree")
@RequirePermission(menu = "ORG_TREE", perm = "READ")
public ApiResponse<List<OrganizationResp>> tree() {
return ApiResponse.ok(service.getTree());
}
@GetMapping
@RequirePermission(menu = "ORG_TREE", perm = "READ")
public ApiResponse<List<OrganizationResp>> list(@RequestParam(required = false) String keyword,
@RequestParam(required = false) String orgType,
@RequestParam(required = false) String isActive) {
return ApiResponse.ok(service.list(keyword, orgType, isActive));
}
@GetMapping("/{orgId}")
@RequirePermission(menu = "ORG_TREE", perm = "READ")
public ApiResponse<OrganizationVO> detail(@PathVariable Long orgId) {
return ApiResponse.ok(service.get(orgId));
}
@PostMapping
@RequirePermission(menu = "ORG_TREE", perm = "CREATE")
@DataChangeLog(menu = "ORG_TREE", table = "organization")
public ApiResponse<Long> create(@RequestBody OrganizationVO vo) {
return ApiResponse.ok(service.create(vo));
}
@PutMapping("/{orgId}")
@RequirePermission(menu = "ORG_TREE", perm = "UPDATE")
@DataChangeLog(menu = "ORG_TREE", table = "organization")
public ApiResponse<Void> update(@PathVariable Long orgId, @RequestBody OrganizationVO vo) {
service.update(orgId, vo);
return ApiResponse.ok();
}
@DeleteMapping("/{orgId}")
@RequirePermission(menu = "ORG_TREE", perm = "DELETE")
@DataChangeLog(menu = "ORG_TREE", table = "organization")
public ApiResponse<Void> delete(@PathVariable Long orgId) {
service.delete(orgId);
return ApiResponse.ok();
}
}
@@ -0,0 +1,50 @@
package com.ga.api.controller.product;
import com.ga.common.annotation.DataChangeLog;
import com.ga.common.annotation.RequirePermission;
import com.ga.common.model.ApiResponse;
import com.ga.core.mapper.product.InsuranceCompanyMapper;
import com.ga.core.vo.product.InsuranceCompanyVO;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "보험사 관리")
@RestController
@RequestMapping("/api/companies")
@RequiredArgsConstructor
public class CompanyController {
private final InsuranceCompanyMapper mapper;
@GetMapping
@RequirePermission(menu = "COMPANY", perm = "READ")
public ApiResponse<List<InsuranceCompanyVO>> list(@RequestParam(required = false, defaultValue = "false") boolean activeOnly) {
return ApiResponse.ok(activeOnly ? mapper.selectActive() : mapper.selectAll());
}
@GetMapping("/{companyId}")
@RequirePermission(menu = "COMPANY", perm = "READ")
public ApiResponse<InsuranceCompanyVO> detail(@PathVariable Long companyId) {
return ApiResponse.ok(mapper.selectById(companyId));
}
@PostMapping
@RequirePermission(menu = "COMPANY", perm = "CREATE")
@DataChangeLog(menu = "COMPANY", table = "insurance_company")
public ApiResponse<Long> create(@RequestBody InsuranceCompanyVO vo) {
mapper.insert(vo);
return ApiResponse.ok(vo.getCompanyId());
}
@PutMapping("/{companyId}")
@RequirePermission(menu = "COMPANY", perm = "UPDATE")
@DataChangeLog(menu = "COMPANY", table = "insurance_company")
public ApiResponse<Void> update(@PathVariable Long companyId, @RequestBody InsuranceCompanyVO vo) {
vo.setCompanyId(companyId);
mapper.update(vo);
return ApiResponse.ok();
}
}
@@ -0,0 +1,60 @@
package com.ga.api.controller.product;
import com.ga.api.service.product.ContractService;
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.ContractResp;
import com.ga.core.vo.product.ContractSaveReq;
import com.ga.core.vo.product.ContractSearchParam;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Tag(name = "계약 관리")
@RestController
@RequestMapping("/api/contracts")
@RequiredArgsConstructor
public class ContractController {
private final ContractService service;
@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));
}
@PostMapping
@RequirePermission(menu = "CONTRACT_LIST", perm = "CREATE")
@DataChangeLog(menu = "CONTRACT_LIST", table = "contract")
public ApiResponse<Long> create(@Valid @RequestBody ContractSaveReq req) {
return ApiResponse.ok(service.create(req));
}
@PutMapping("/{contractId}")
@RequirePermission(menu = "CONTRACT_LIST", perm = "UPDATE")
@DataChangeLog(menu = "CONTRACT_LIST", table = "contract")
public ApiResponse<Void> update(@PathVariable Long contractId, @Valid @RequestBody ContractSaveReq req) {
service.update(contractId, req);
return ApiResponse.ok();
}
@PutMapping("/{contractId}/status")
@RequirePermission(menu = "CONTRACT_LIST", perm = "UPDATE")
@DataChangeLog(menu = "CONTRACT_LIST", table = "contract")
public ApiResponse<Void> changeStatus(@PathVariable Long contractId, @RequestBody Map<String, String> body) {
service.changeStatus(contractId, body.get("status"));
return ApiResponse.ok();
}
}
@@ -0,0 +1,68 @@
package com.ga.api.controller.product;
import com.ga.common.annotation.DataChangeLog;
import com.ga.common.annotation.RequirePermission;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.common.model.ApiResponse;
import com.ga.core.mapper.product.ProductMapper;
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;
@Tag(name = "상품 관리")
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductMapper mapper;
@GetMapping
@RequirePermission(menu = "PRODUCT", perm = "READ")
public ApiResponse<List<ProductResp>> list(@RequestParam(required = false) Long companyId,
@RequestParam(required = false) String insuranceType,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String isActive) {
return ApiResponse.ok(mapper.selectList(companyId, insuranceType, keyword, isActive));
}
@GetMapping("/{productId}")
@RequirePermission(menu = "PRODUCT", perm = "READ")
public ApiResponse<ProductResp> detail(@PathVariable Long productId) {
ProductResp r = mapper.selectDetailById(productId);
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
return ApiResponse.ok(r);
}
@PostMapping
@RequirePermission(menu = "PRODUCT", perm = "CREATE")
@DataChangeLog(menu = "PRODUCT", table = "product")
public ApiResponse<Long> create(@RequestBody ProductVO vo) {
if (mapper.existsByCode(vo.getCompanyId(), vo.getProductCode()) > 0) {
throw new BizException(ErrorCode.DUPLICATE_DATA);
}
mapper.insert(vo);
return ApiResponse.ok(vo.getProductId());
}
@PutMapping("/{productId}")
@RequirePermission(menu = "PRODUCT", perm = "UPDATE")
@DataChangeLog(menu = "PRODUCT", table = "product")
public ApiResponse<Void> update(@PathVariable Long productId, @RequestBody ProductVO vo) {
vo.setProductId(productId);
mapper.update(vo);
return ApiResponse.ok();
}
@DeleteMapping("/{productId}")
@RequirePermission(menu = "PRODUCT", perm = "DELETE")
public ApiResponse<Void> delete(@PathVariable Long productId) {
mapper.deleteById(productId);
return ApiResponse.ok();
}
}
@@ -0,0 +1,110 @@
package com.ga.api.controller.receive;
import com.ga.common.annotation.RequirePermission;
import com.ga.common.model.ApiResponse;
import com.ga.core.mapper.receive.ReceiveMapper;
import com.ga.core.vo.receive.*;
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;
@Tag(name = "데이터 수신")
@RestController
@RequestMapping("/api/receive")
@RequiredArgsConstructor
public class ReceiveController {
private final ReceiveMapper mapper;
// 보험사 프로파일
@GetMapping("/profiles")
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "READ")
public ApiResponse<List<CompanyProfileVO>> profiles() {
return ApiResponse.ok(mapper.selectAllProfiles());
}
@PutMapping("/profiles/{companyCode}")
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "UPDATE")
public ApiResponse<Void> updateProfile(@PathVariable String companyCode, @RequestBody CompanyProfileVO vo) {
vo.setCompanyCode(companyCode);
if (mapper.selectProfile(companyCode) == null) mapper.insertProfile(vo);
else mapper.updateProfile(vo);
return ApiResponse.ok();
}
// 필드 매핑
@GetMapping("/mapping/{companyCode}/fields")
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "READ")
public ApiResponse<List<CompanyFieldMappingVO>> fields(@PathVariable String companyCode,
@RequestParam(required = false) String targetTable) {
return ApiResponse.ok(mapper.selectFieldMappings(companyCode, targetTable));
}
@PostMapping("/mapping/{companyCode}/fields")
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "CREATE")
public ApiResponse<Long> createField(@PathVariable String companyCode, @RequestBody CompanyFieldMappingVO vo) {
vo.setCompanyCode(companyCode);
mapper.insertFieldMapping(vo);
return ApiResponse.ok(vo.getMappingId());
}
@PutMapping("/mapping/fields/{mappingId}")
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "UPDATE")
public ApiResponse<Void> updateField(@PathVariable Long mappingId, @RequestBody CompanyFieldMappingVO vo) {
vo.setMappingId(mappingId);
mapper.updateFieldMapping(vo);
return ApiResponse.ok();
}
@DeleteMapping("/mapping/fields/{mappingId}")
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "DELETE")
public ApiResponse<Void> deleteField(@PathVariable Long mappingId) {
mapper.deleteFieldMapping(mappingId);
return ApiResponse.ok();
}
// 코드 매핑
@GetMapping("/mapping/{companyCode}/codes")
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "READ")
public ApiResponse<List<CodeMappingVO>> codes(@PathVariable String companyCode,
@RequestParam(required = false) String codeType) {
return ApiResponse.ok(mapper.selectCodeMappings(companyCode, codeType));
}
@PostMapping("/mapping/{companyCode}/codes")
@RequirePermission(menu = "RECEIVE_MAPPING", perm = "CREATE")
public ApiResponse<Long> createCode(@PathVariable String companyCode, @RequestBody CodeMappingVO vo) {
vo.setCompanyCode(companyCode);
mapper.insertCodeMapping(vo);
return ApiResponse.ok(vo.getCodeId());
}
// 원본 데이터 + 에러
@GetMapping("/raw")
@RequirePermission(menu = "RECEIVE_DATA", perm = "READ")
public ApiResponse<List<RawCommissionDataVO>> rawList(@RequestParam(required = false) String companyCode,
@RequestParam(required = false) String settleMonth,
@RequestParam(required = false) String parseStatus) {
return ApiResponse.ok(mapper.selectRawList(companyCode, settleMonth, parseStatus));
}
@GetMapping("/errors")
@RequirePermission(menu = "RECEIVE_DATA", perm = "READ")
public ApiResponse<List<ParseErrorLogVO>> errors(@RequestParam(required = false) String companyCode,
@RequestParam(required = false) String resolveStatus) {
return ApiResponse.ok(mapper.selectErrors(companyCode, resolveStatus));
}
@PutMapping("/errors/{errorId}/resolve")
@RequirePermission(menu = "RECEIVE_DATA", perm = "UPDATE")
public ApiResponse<Void> resolveError(@PathVariable Long errorId, @RequestBody Map<String, String> body) {
mapper.resolveError(errorId,
body.getOrDefault("status", "RESOLVED"),
body.get("note"),
com.ga.common.util.SecurityUtil.getCurrentUserId());
return ApiResponse.ok();
}
}
@@ -0,0 +1,166 @@
package com.ga.api.controller.rule;
import com.ga.common.annotation.DataChangeLog;
import com.ga.common.annotation.RequirePermission;
import com.ga.common.model.ApiResponse;
import com.ga.core.mapper.rule.RuleMapper;
import com.ga.core.vo.rule.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "수수료 규정")
@RestController
@RequestMapping("/api/rules")
@RequiredArgsConstructor
public class RuleController {
private final RuleMapper mapper;
// ========== commission_rate ==========
@GetMapping("/commission-rates")
@RequirePermission(menu = "RULE_COMMISSION", perm = "READ")
public ApiResponse<List<CommissionRateVO>> listCommissionRates(@RequestParam(required = false) Long productId,
@RequestParam(required = false) Integer year) {
return ApiResponse.ok(mapper.selectCommissionRates(productId, year));
}
@PostMapping("/commission-rates")
@RequirePermission(menu = "RULE_COMMISSION", perm = "CREATE")
@DataChangeLog(menu = "RULE_COMMISSION", table = "commission_rate")
public ApiResponse<Long> createCommissionRate(@RequestBody CommissionRateVO vo) {
mapper.insertCommissionRate(vo);
return ApiResponse.ok(vo.getRateId());
}
@PutMapping("/commission-rates/{rateId}")
@RequirePermission(menu = "RULE_COMMISSION", perm = "UPDATE")
@DataChangeLog(menu = "RULE_COMMISSION", table = "commission_rate")
public ApiResponse<Void> updateCommissionRate(@PathVariable Long rateId, @RequestBody CommissionRateVO vo) {
vo.setRateId(rateId);
mapper.updateCommissionRate(vo);
return ApiResponse.ok();
}
@DeleteMapping("/commission-rates/{rateId}")
@RequirePermission(menu = "RULE_COMMISSION", perm = "DELETE")
public ApiResponse<Void> deleteCommissionRate(@PathVariable Long rateId) {
mapper.deleteCommissionRate(rateId);
return ApiResponse.ok();
}
// ========== payout_rule ==========
@GetMapping("/payout")
@RequirePermission(menu = "RULE_PAYOUT", perm = "READ")
public ApiResponse<List<PayoutRuleVO>> listPayoutRules(@RequestParam(required = false) Long gradeId,
@RequestParam(required = false) String insuranceType) {
return ApiResponse.ok(mapper.selectPayoutRules(gradeId, insuranceType));
}
@PostMapping("/payout")
@RequirePermission(menu = "RULE_PAYOUT", perm = "CREATE")
@DataChangeLog(menu = "RULE_PAYOUT", table = "payout_rule")
public ApiResponse<Long> createPayoutRule(@RequestBody PayoutRuleVO vo) {
mapper.insertPayoutRule(vo);
return ApiResponse.ok(vo.getRuleId());
}
@PutMapping("/payout/{ruleId}")
@RequirePermission(menu = "RULE_PAYOUT", perm = "UPDATE")
@DataChangeLog(menu = "RULE_PAYOUT", table = "payout_rule")
public ApiResponse<Void> updatePayoutRule(@PathVariable Long ruleId, @RequestBody PayoutRuleVO vo) {
vo.setRuleId(ruleId);
mapper.updatePayoutRule(vo);
return ApiResponse.ok();
}
@DeleteMapping("/payout/{ruleId}")
@RequirePermission(menu = "RULE_PAYOUT", perm = "DELETE")
public ApiResponse<Void> deletePayoutRule(@PathVariable Long ruleId) {
mapper.deletePayoutRule(ruleId);
return ApiResponse.ok();
}
// ========== override_rule ==========
@GetMapping("/override")
@RequirePermission(menu = "RULE_OVERRIDE", perm = "READ")
public ApiResponse<List<OverrideRuleVO>> listOverrideRules() {
return ApiResponse.ok(mapper.selectOverrideRules());
}
@PostMapping("/override")
@RequirePermission(menu = "RULE_OVERRIDE", perm = "CREATE")
public ApiResponse<Long> createOverrideRule(@RequestBody OverrideRuleVO vo) {
mapper.insertOverrideRule(vo);
return ApiResponse.ok(vo.getOverrideId());
}
@PutMapping("/override/{overrideId}")
@RequirePermission(menu = "RULE_OVERRIDE", perm = "UPDATE")
public ApiResponse<Void> updateOverrideRule(@PathVariable Long overrideId, @RequestBody OverrideRuleVO vo) {
vo.setOverrideId(overrideId);
mapper.updateOverrideRule(vo);
return ApiResponse.ok();
}
@DeleteMapping("/override/{overrideId}")
@RequirePermission(menu = "RULE_OVERRIDE", perm = "DELETE")
public ApiResponse<Void> deleteOverrideRule(@PathVariable Long overrideId) {
mapper.deleteOverrideRule(overrideId);
return ApiResponse.ok();
}
// ========== chargeback_rule ==========
@GetMapping("/chargeback")
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "READ")
public ApiResponse<List<ChargebackRuleVO>> listChargebackRules(@RequestParam(required = false) String companyCode) {
return ApiResponse.ok(mapper.selectChargebackRules(companyCode));
}
@PostMapping("/chargeback")
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "CREATE")
public ApiResponse<Long> createChargebackRule(@RequestBody ChargebackRuleVO vo) {
mapper.insertChargebackRule(vo);
return ApiResponse.ok(vo.getCbRuleId());
}
@PutMapping("/chargeback/{cbRuleId}")
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "UPDATE")
public ApiResponse<Void> updateChargebackRule(@PathVariable Long cbRuleId, @RequestBody ChargebackRuleVO vo) {
vo.setCbRuleId(cbRuleId);
mapper.updateChargebackRule(vo);
return ApiResponse.ok();
}
@DeleteMapping("/chargeback/{cbRuleId}")
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "DELETE")
public ApiResponse<Void> deleteChargebackRule(@PathVariable Long cbRuleId) {
mapper.deleteChargebackRule(cbRuleId);
return ApiResponse.ok();
}
// ========== exception_type_code ==========
@GetMapping("/exception-codes")
@RequirePermission(menu = "RULE_EXCEPTION", perm = "READ")
public ApiResponse<List<ExceptionTypeCodeVO>> listExceptionCodes() {
return ApiResponse.ok(mapper.selectExceptionCodes());
}
@PostMapping("/exception-codes")
@RequirePermission(menu = "RULE_EXCEPTION", perm = "CREATE")
public ApiResponse<Void> createExceptionCode(@RequestBody ExceptionTypeCodeVO vo) {
mapper.insertExceptionCode(vo);
return ApiResponse.ok();
}
@PutMapping("/exception-codes/{exceptionCode}")
@RequirePermission(menu = "RULE_EXCEPTION", perm = "UPDATE")
public ApiResponse<Void> updateExceptionCode(@PathVariable String exceptionCode,
@RequestBody ExceptionTypeCodeVO vo) {
vo.setExceptionCode(exceptionCode);
mapper.updateExceptionCode(vo);
return ApiResponse.ok();
}
}
@@ -0,0 +1,38 @@
package com.ga.api.controller.settle;
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.mapper.settle.PaymentMapper;
import com.ga.core.vo.settle.PaymentResp;
import com.ga.core.vo.settle.SettleSearchParam;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Tag(name = "지급 관리")
@RestController
@RequestMapping("/api/payments")
@RequiredArgsConstructor
public class PaymentController {
private final PaymentMapper mapper;
@GetMapping
@RequirePermission(menu = "PAYMENT", perm = "READ")
public ApiResponse<PageResponse<PaymentResp>> list(@ModelAttribute SettleSearchParam param) {
param.startPage();
return ApiResponse.ok(PageResponse.of(mapper.selectList(param)));
}
@PutMapping("/{paymentId}/status")
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
@DataChangeLog(menu = "PAYMENT", table = "payment")
public ApiResponse<Void> updateStatus(@PathVariable Long paymentId, @RequestBody Map<String, String> body) {
mapper.updateStatus(paymentId, body.get("status"), body.get("failReason"));
return ApiResponse.ok();
}
}
@@ -0,0 +1,73 @@
package com.ga.api.controller.settle;
import com.ga.api.service.settle.SettleService;
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.SettleMasterResp;
import com.ga.core.vo.settle.SettleSearchParam;
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;
@Tag(name = "정산 관리")
@RestController
@RequestMapping("/api/settle")
@RequiredArgsConstructor
public class SettleController {
private final SettleService service;
@GetMapping
@RequirePermission(menu = "SETTLE_LIST", perm = "READ")
public ApiResponse<PageResponse<SettleMasterResp>> list(@ModelAttribute SettleSearchParam param) {
return ApiResponse.ok(service.list(param));
}
@GetMapping("/{agentId}/months/{settleMonth}")
@RequirePermission(menu = "SETTLE_LIST", perm = "READ")
public ApiResponse<SettleMasterResp> detail(@PathVariable Long agentId,
@PathVariable String settleMonth) {
return ApiResponse.ok(service.detail(agentId, settleMonth));
}
@GetMapping("/summary")
@RequirePermission(menu = "SETTLE_LIST", perm = "READ")
public ApiResponse<Map<String, Object>> summary(@RequestParam String settleMonth) {
return ApiResponse.ok(service.summary(settleMonth));
}
@GetMapping("/org-summary")
@RequirePermission(menu = "SETTLE_LIST", perm = "READ")
public ApiResponse<List<Map<String, Object>>> orgSummary(@RequestParam String settleMonth) {
return ApiResponse.ok(service.orgSummary(settleMonth));
}
@PutMapping("/{settleId}/confirm")
@RequirePermission(menu = "SETTLE_LIST", perm = "APPROVE")
@DataChangeLog(menu = "SETTLE_LIST", table = "settle_master")
public ApiResponse<Void> confirm(@PathVariable Long settleId) {
service.confirm(settleId);
return ApiResponse.ok();
}
@PutMapping("/{settleId}/hold")
@RequirePermission(menu = "SETTLE_LIST", perm = "APPROVE")
@DataChangeLog(menu = "SETTLE_LIST", table = "settle_master")
public ApiResponse<Void> hold(@PathVariable Long settleId, @RequestBody Map<String, String> body) {
service.hold(settleId, body.get("reason"));
return ApiResponse.ok();
}
@PutMapping("/{settleId}/release")
@RequirePermission(menu = "SETTLE_LIST", perm = "APPROVE")
@DataChangeLog(menu = "SETTLE_LIST", table = "settle_master")
public ApiResponse<Void> release(@PathVariable Long settleId) {
service.release(settleId);
return ApiResponse.ok();
}
}
@@ -0,0 +1,81 @@
package com.ga.api.controller.system;
import com.ga.common.annotation.DataChangeLog;
import com.ga.common.annotation.RequirePermission;
import com.ga.common.model.ApiResponse;
import com.ga.core.mapper.user.RoleMapper;
import com.ga.core.vo.user.RoleVO;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Tag(name = "역할 관리")
@RestController
@RequestMapping("/api/system/roles")
@RequiredArgsConstructor
public class RoleController {
private final RoleMapper mapper;
@GetMapping
@RequirePermission(menu = "SYSTEM_ROLE", perm = "READ")
public ApiResponse<List<RoleVO>> list() {
return ApiResponse.ok(mapper.selectAll());
}
@GetMapping("/{roleId}")
@RequirePermission(menu = "SYSTEM_ROLE", perm = "READ")
public ApiResponse<RoleVO> detail(@PathVariable Long roleId) {
return ApiResponse.ok(mapper.selectById(roleId));
}
@GetMapping("/{roleId}/permissions")
@RequirePermission(menu = "SYSTEM_ROLE", perm = "READ")
public ApiResponse<List<Map<String, Object>>> permissions(@PathVariable Long roleId) {
return ApiResponse.ok(mapper.selectRolePermissions(roleId));
}
@PostMapping
@RequirePermission(menu = "SYSTEM_ROLE", perm = "CREATE")
@DataChangeLog(menu = "SYSTEM_ROLE", table = "role")
public ApiResponse<Long> create(@RequestBody RoleVO vo) {
mapper.insert(vo);
return ApiResponse.ok(vo.getRoleId());
}
@PutMapping("/{roleId}")
@RequirePermission(menu = "SYSTEM_ROLE", perm = "UPDATE")
@DataChangeLog(menu = "SYSTEM_ROLE", table = "role")
public ApiResponse<Void> update(@PathVariable Long roleId, @RequestBody RoleVO vo) {
vo.setRoleId(roleId);
mapper.update(vo);
return ApiResponse.ok();
}
@DeleteMapping("/{roleId}")
@RequirePermission(menu = "SYSTEM_ROLE", perm = "DELETE")
public ApiResponse<Void> delete(@PathVariable Long roleId) {
mapper.deleteById(roleId);
return ApiResponse.ok();
}
/** 권한 매트릭스 일괄 저장 */
@PutMapping("/{roleId}/permissions")
@RequirePermission(menu = "SYSTEM_ROLE", perm = "UPDATE")
@Transactional
public ApiResponse<Void> savePermissions(@PathVariable Long roleId,
@RequestBody List<Map<String, Object>> permissions) {
mapper.deleteRolePermissions(roleId);
for (Map<String, Object> p : permissions) {
Long menuId = ((Number) p.get("menuId")).longValue();
String permCode = (String) p.get("permCode");
String isGranted = (String) p.getOrDefault("isGranted", "Y");
mapper.insertRolePermission(roleId, menuId, permCode, isGranted);
}
return ApiResponse.ok();
}
}
@@ -0,0 +1,64 @@
package com.ga.api.controller.system;
import com.ga.api.service.system.UserService;
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.user.UserResp;
import com.ga.core.vo.user.UserSaveReq;
import com.ga.core.vo.user.UserSearchParam;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "사용자 관리")
@RestController
@RequestMapping("/api/system/users")
@RequiredArgsConstructor
public class UserController {
private final UserService service;
@GetMapping
@RequirePermission(menu = "SYSTEM_USER", perm = "READ")
public ApiResponse<PageResponse<UserResp>> list(@ModelAttribute UserSearchParam param) {
return ApiResponse.ok(service.list(param));
}
@GetMapping("/{userId}")
@RequirePermission(menu = "SYSTEM_USER", perm = "READ")
public ApiResponse<UserResp> detail(@PathVariable Long userId) {
return ApiResponse.ok(service.detail(userId));
}
@PostMapping
@RequirePermission(menu = "SYSTEM_USER", perm = "CREATE")
@DataChangeLog(menu = "SYSTEM_USER", table = "users")
public ApiResponse<Long> create(@Valid @RequestBody UserSaveReq req) {
return ApiResponse.ok(service.create(req));
}
@PutMapping("/{userId}")
@RequirePermission(menu = "SYSTEM_USER", perm = "UPDATE")
@DataChangeLog(menu = "SYSTEM_USER", table = "users")
public ApiResponse<Void> update(@PathVariable Long userId, @Valid @RequestBody UserSaveReq req) {
service.update(userId, req);
return ApiResponse.ok();
}
@PutMapping("/{userId}/reset-password")
@RequirePermission(menu = "SYSTEM_USER", perm = "UPDATE")
public ApiResponse<Void> resetPassword(@PathVariable Long userId) {
service.resetPassword(userId);
return ApiResponse.ok();
}
@PutMapping("/{userId}/unlock")
@RequirePermission(menu = "SYSTEM_USER", perm = "UPDATE")
public ApiResponse<Void> unlock(@PathVariable Long userId) {
service.unlock(userId);
return ApiResponse.ok();
}
}
@@ -0,0 +1,85 @@
package com.ga.api.service.ledger;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.common.model.PageResponse;
import com.ga.common.util.SecurityUtil;
import com.ga.core.mapper.ledger.ExceptionLedgerMapper;
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.vo.ledger.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class LedgerService {
private final RecruitLedgerMapper recruitMapper;
private final MaintainLedgerMapper maintainMapper;
private final ExceptionLedgerMapper exceptionMapper;
public PageResponse<LedgerResp> listRecruit(LedgerSearchParam param) {
param.startPage();
return PageResponse.of(recruitMapper.selectList(param));
}
public PageResponse<LedgerResp> listMaintain(LedgerSearchParam param) {
param.startPage();
return PageResponse.of(maintainMapper.selectList(param));
}
public PageResponse<ExceptionLedgerResp> listException(LedgerSearchParam param) {
param.startPage();
return PageResponse.of(exceptionMapper.selectList(param));
}
public LedgerResp detailRecruit(Long ledgerId) {
LedgerResp r = recruitMapper.selectDetailById(ledgerId);
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
return r;
}
public LedgerResp detailMaintain(Long ledgerId) {
LedgerResp r = maintainMapper.selectDetailById(ledgerId);
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
return r;
}
public ExceptionLedgerResp detailException(Long ledgerId) {
ExceptionLedgerResp r = exceptionMapper.selectDetailById(ledgerId);
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
return r;
}
@Transactional
public Long createException(ExceptionLedgerSaveReq req) {
ExceptionLedgerVO vo = new ExceptionLedgerVO();
vo.setAgentId(req.getAgentId());
vo.setExceptionCode(req.getExceptionCode());
vo.setSettleMonth(req.getSettleMonth());
vo.setAmount(req.getAmount());
vo.setPolicyNo(req.getPolicyNo());
vo.setCompanyCode(req.getCompanyCode());
vo.setInsuranceType(req.getInsuranceType());
vo.setDescription(req.getDescription());
vo.setRecurringYn(req.getRecurringYn());
vo.setRecurringMonths(req.getRecurringMonths());
vo.setRecurringLeft(req.getRecurringMonths());
vo.setApproveStatus("PENDING");
vo.setStatus("NEW");
exceptionMapper.insert(vo);
return vo.getLedgerId();
}
@Transactional
public void approveException(Long ledgerId, String status) {
if (!"APPROVED".equals(status) && !"REJECTED".equals(status)) {
throw new BizException(ErrorCode.INVALID_PARAMETER, "status는 APPROVED/REJECTED만 가능합니다");
}
Long approverId = SecurityUtil.getCurrentUserId();
if (approverId == null) throw new BizException(ErrorCode.UNAUTHORIZED);
exceptionMapper.updateApprove(ledgerId, status, approverId);
}
}
@@ -0,0 +1,98 @@
package com.ga.api.service.org;
import com.ga.common.code.CommonCodeService;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.common.model.PageResponse;
import com.ga.core.mapper.org.AgentMapper;
import com.ga.core.vo.org.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class AgentService {
private final AgentMapper mapper;
private final CommonCodeService codeService;
public PageResponse<AgentResp> list(AgentSearchParam param) {
param.startPage();
return PageResponse.of(mapper.selectList(param));
}
public AgentResp detail(Long agentId) {
AgentResp resp = mapper.selectDetailById(agentId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long create(AgentSaveReq req) {
if (req.getLicenseNo() != null && mapper.existsByLicenseNo(req.getLicenseNo()) > 0) {
throw new BizException(ErrorCode.DUPLICATE_DATA, "이미 등록된 사번입니다");
}
AgentVO vo = toVO(req, null);
vo.setStatus("ACTIVE");
mapper.insert(vo);
// 입사 이력 기록
AgentOrgHistoryVO h = new AgentOrgHistoryVO();
h.setAgentId(vo.getAgentId());
h.setToOrgId(vo.getOrgId());
h.setToGradeId(vo.getGradeId());
h.setChangeType("JOIN");
h.setChangeDate(req.getJoinDate() != null ? req.getJoinDate() : java.time.LocalDate.now());
mapper.insertHistory(h);
return vo.getAgentId();
}
@Transactional
public void update(Long agentId, AgentSaveReq req) {
AgentVO old = mapper.selectById(agentId);
if (old == null) throw new BizException(ErrorCode.NOT_FOUND);
AgentVO vo = toVO(req, agentId);
vo.setStatus(old.getStatus());
mapper.update(vo);
// 조직/직급 변경 이력
if (!java.util.Objects.equals(old.getOrgId(), vo.getOrgId())
|| !java.util.Objects.equals(old.getGradeId(), vo.getGradeId())) {
AgentOrgHistoryVO h = new AgentOrgHistoryVO();
h.setAgentId(agentId);
h.setFromOrgId(old.getOrgId());
h.setToOrgId(vo.getOrgId());
h.setFromGradeId(old.getGradeId());
h.setToGradeId(vo.getGradeId());
h.setChangeType(!java.util.Objects.equals(old.getGradeId(), vo.getGradeId()) ? "PROMOTE" : "TRANSFER");
h.setChangeDate(java.time.LocalDate.now());
mapper.insertHistory(h);
}
}
@Transactional
public void changeStatus(Long agentId, String status) {
codeService.validateCode("AGENT_STATUS", status);
mapper.updateStatus(agentId, status);
}
public java.util.List<AgentOrgHistoryVO> history(Long agentId) {
return mapper.selectHistory(agentId);
}
private AgentVO toVO(AgentSaveReq req, Long agentId) {
AgentVO vo = new AgentVO();
vo.setAgentId(agentId);
vo.setOrgId(req.getOrgId());
vo.setGradeId(req.getGradeId());
vo.setAgentName(req.getAgentName());
vo.setResidentNo(req.getResidentNo());
vo.setLicenseNo(req.getLicenseNo());
vo.setPhone(req.getPhone());
vo.setEmail(req.getEmail());
vo.setBankCode(req.getBankCode());
vo.setAccountNo(req.getAccountNo());
vo.setJoinDate(req.getJoinDate());
return vo;
}
}
@@ -0,0 +1,74 @@
package com.ga.api.service.org;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.core.mapper.org.OrganizationMapper;
import com.ga.core.vo.org.OrganizationResp;
import com.ga.core.vo.org.OrganizationVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Service
@RequiredArgsConstructor
public class OrganizationService {
private final OrganizationMapper mapper;
public List<OrganizationResp> getTree() {
List<OrganizationResp> flat = mapper.selectAllForTree();
return buildTree(flat);
}
public List<OrganizationResp> list(String keyword, String orgType, String isActive) {
return mapper.selectList(keyword, orgType, isActive);
}
public OrganizationVO get(Long orgId) {
OrganizationVO vo = mapper.selectById(orgId);
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
return vo;
}
@Transactional
public Long create(OrganizationVO vo) {
mapper.insert(vo);
return vo.getOrgId();
}
@Transactional
public void update(Long orgId, OrganizationVO vo) {
get(orgId);
vo.setOrgId(orgId);
mapper.update(vo);
}
@Transactional
public void delete(Long orgId) {
if (mapper.countChildren(orgId) > 0) {
throw new BizException(ErrorCode.DEPENDENT_DATA, "하위 조직이 있어 삭제할 수 없습니다");
}
if (mapper.countAgents(orgId) > 0) {
throw new BizException(ErrorCode.DEPENDENT_DATA, "소속 설계사가 있어 삭제할 수 없습니다");
}
mapper.deleteById(orgId);
}
private List<OrganizationResp> buildTree(List<OrganizationResp> flat) {
Map<Long, OrganizationResp> byId = new LinkedHashMap<>();
for (OrganizationResp o : flat) byId.put(o.getOrgId(), o);
List<OrganizationResp> roots = new ArrayList<>();
for (OrganizationResp o : flat) {
if (o.getParentOrgId() == null) {
roots.add(o);
} else {
OrganizationResp parent = byId.get(o.getParentOrgId());
if (parent != null) parent.getChildren().add(o);
else roots.add(o);
}
}
return roots;
}
}
@@ -0,0 +1,74 @@
package com.ga.api.service.product;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.common.model.PageResponse;
import com.ga.core.mapper.product.ContractMapper;
import com.ga.core.vo.product.ContractResp;
import com.ga.core.vo.product.ContractSaveReq;
import com.ga.core.vo.product.ContractSearchParam;
import com.ga.core.vo.product.ContractVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class ContractService {
private final ContractMapper mapper;
public PageResponse<ContractResp> list(ContractSearchParam param) {
param.startPage();
return PageResponse.of(mapper.selectList(param));
}
public ContractResp detail(Long contractId) {
ContractResp resp = mapper.selectDetailById(contractId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long create(ContractSaveReq req) {
if (mapper.existsByPolicyNo(req.getPolicyNo()) > 0) {
throw new BizException(ErrorCode.DUPLICATE_DATA, "이미 등록된 증권번호입니다");
}
ContractVO vo = new ContractVO();
vo.setAgentId(req.getAgentId());
vo.setProductId(req.getProductId());
vo.setPolicyNo(req.getPolicyNo());
vo.setContractorName(req.getContractorName());
vo.setInsuredName(req.getInsuredName());
vo.setPremium(req.getPremium());
vo.setPayCycle(req.getPayCycle());
vo.setPayPeriod(req.getPayPeriod());
vo.setCoveragePeriod(req.getCoveragePeriod());
vo.setStatus("ACTIVE");
vo.setContractDate(req.getContractDate());
vo.setEffectiveDate(req.getEffectiveDate());
mapper.insert(vo);
return vo.getContractId();
}
@Transactional
public void update(Long contractId, ContractSaveReq req) {
ContractVO vo = mapper.selectById(contractId);
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
vo.setAgentId(req.getAgentId());
vo.setProductId(req.getProductId());
vo.setContractorName(req.getContractorName());
vo.setInsuredName(req.getInsuredName());
vo.setPremium(req.getPremium());
vo.setPayCycle(req.getPayCycle());
vo.setPayPeriod(req.getPayPeriod());
vo.setCoveragePeriod(req.getCoveragePeriod());
vo.setEffectiveDate(req.getEffectiveDate());
mapper.update(vo);
}
@Transactional
public void changeStatus(Long contractId, String status) {
mapper.updateStatus(contractId, status);
}
}
@@ -0,0 +1,72 @@
package com.ga.api.service.settle;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.common.model.PageResponse;
import com.ga.common.util.SecurityUtil;
import com.ga.core.mapper.settle.SettleMasterMapper;
import com.ga.core.vo.settle.SettleMasterResp;
import com.ga.core.vo.settle.SettleMasterVO;
import com.ga.core.vo.settle.SettleSearchParam;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class SettleService {
private final SettleMasterMapper masterMapper;
public PageResponse<SettleMasterResp> list(SettleSearchParam param) {
param.startPage();
return PageResponse.of(masterMapper.selectList(param));
}
public SettleMasterResp detail(Long agentId, String settleMonth) {
SettleMasterResp r = masterMapper.selectDetail(agentId, settleMonth);
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
return r;
}
public Map<String, Object> summary(String settleMonth) {
return masterMapper.selectMonthSummary(settleMonth);
}
public List<Map<String, Object>> orgSummary(String settleMonth) {
return masterMapper.selectOrgSummary(settleMonth);
}
@Transactional
public void confirm(Long settleId) {
SettleMasterVO vo = masterMapper.selectById(settleId);
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
if ("CONFIRMED".equals(vo.getStatus()) || "PAID".equals(vo.getStatus())) {
throw new BizException(ErrorCode.ALREADY_CONFIRMED);
}
masterMapper.updateStatus(settleId, "CONFIRMED", SecurityUtil.getCurrentUserId());
}
@Transactional
public void hold(Long settleId, String reason) {
SettleMasterVO vo = masterMapper.selectById(settleId);
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
if ("PAID".equals(vo.getStatus())) {
throw new BizException(ErrorCode.NOT_PERMITTED, "지급완료된 정산은 보류할 수 없습니다");
}
masterMapper.updateHold(settleId, reason);
}
@Transactional
public void release(Long settleId) {
SettleMasterVO vo = masterMapper.selectById(settleId);
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
if (!"HOLD".equals(vo.getStatus())) {
throw new BizException(ErrorCode.INVALID_STATUS, "보류 상태가 아닙니다");
}
masterMapper.updateStatus(settleId, "CALCULATED", SecurityUtil.getCurrentUserId());
}
}
@@ -0,0 +1,100 @@
package com.ga.api.service.system;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.common.model.PageResponse;
import com.ga.common.system.SystemConfigService;
import com.ga.core.mapper.user.UserMapper;
import com.ga.core.vo.user.UserResp;
import com.ga.core.vo.user.UserSaveReq;
import com.ga.core.vo.user.UserSearchParam;
import com.ga.core.vo.user.UserVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper mapper;
private final PasswordEncoder passwordEncoder;
private final SystemConfigService configService;
public PageResponse<UserResp> list(UserSearchParam param) {
param.startPage();
return PageResponse.of(mapper.selectList(param));
}
public UserResp detail(Long userId) {
UserResp r = mapper.selectDetailById(userId);
if (r == null) throw new BizException(ErrorCode.NOT_FOUND);
r.setRoleCodes(mapper.selectRoleCodes(userId));
return r;
}
@Transactional
public Long create(UserSaveReq req) {
if (mapper.selectByLoginId(req.getLoginId()) != null) {
throw new BizException(ErrorCode.DUPLICATE_DATA, "이미 사용 중인 로그인 ID입니다");
}
String initialPwd = req.getPassword();
if (initialPwd == null || initialPwd.isBlank()) {
initialPwd = "init1234!"; // 임시 로그인 변경 강제
}
UserVO vo = new UserVO();
vo.setLoginId(req.getLoginId());
vo.setPassword(passwordEncoder.encode(initialPwd));
vo.setUserName(req.getUserName());
vo.setEmail(req.getEmail());
vo.setPhone(req.getPhone());
vo.setAgentId(req.getAgentId());
vo.setStatus(req.getStatus() != null ? req.getStatus() : "ACTIVE");
mapper.insert(vo);
if (req.getRoleIds() != null) {
req.getRoleIds().forEach(rid -> mapper.insertUserRole(vo.getUserId(), rid));
}
return vo.getUserId();
}
@Transactional
public void update(Long userId, UserSaveReq req) {
UserVO old = mapper.selectById(userId);
if (old == null) throw new BizException(ErrorCode.NOT_FOUND);
UserVO vo = new UserVO();
vo.setUserId(userId);
vo.setUserName(req.getUserName());
vo.setEmail(req.getEmail());
vo.setPhone(req.getPhone());
vo.setAgentId(req.getAgentId());
vo.setStatus(req.getStatus());
mapper.update(vo);
if (req.getRoleIds() != null) {
mapper.deleteUserRoles(userId);
req.getRoleIds().forEach(rid -> mapper.insertUserRole(userId, rid));
}
}
@Transactional
public void resetPassword(Long userId) {
int min = configService.getInt("PASSWORD_MIN_LEN", 8);
String temp = "reset" + System.currentTimeMillis() % 1_000_000;
if (temp.length() < min) temp = temp + "!@#";
mapper.updatePassword(userId, passwordEncoder.encode(temp));
}
@Transactional
public void unlock(Long userId) {
mapper.resetFailCount(userId);
mapper.updateStatus(userId, "ACTIVE");
}
public List<String> roleCodes(Long userId) {
return mapper.selectRoleCodes(userId);
}
}
@@ -0,0 +1,11 @@
package com.ga.batch;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {"com.ga"})
public class GaBatchApplication {
public static void main(String[] args) {
SpringApplication.run(GaBatchApplication.class, args);
}
}
@@ -0,0 +1,60 @@
package com.ga.batch.config;
import com.ga.batch.settlement.SettlementContext;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import com.ga.batch.settlement.step.*;
@Configuration
public class BatchConfig {
@Bean
@JobScope
public SettlementContext settlementContext(@Value("#{jobParameters['settleMonth']}") String settleMonth,
@Value("#{jobParameters['jobLogId'] ?: -1L}") Long jobLogId) {
SettlementContext ctx = new SettlementContext();
ctx.setSettleMonth(settleMonth);
ctx.setJobLogId(jobLogId);
return ctx;
}
@Bean
public Job monthlySettlementJob(JobRepository jobRepository,
PlatformTransactionManager tx,
ReceiveDataStep step1,
TransformDataStep step2,
ReconcileStep step3,
CalcRecruitStep step4,
CalcMaintainStep step5,
ChargebackExceptionStep step6,
CalcOverrideStep step7,
AggregateStep step8) {
return new JobBuilder("MONTHLY_SETTLEMENT", jobRepository)
.start(taskletStep("receiveData", step1, jobRepository, tx))
.next(taskletStep("transformData", step2, jobRepository, tx))
.next(taskletStep("reconcile", step3, jobRepository, tx))
.next(taskletStep("calcRecruit", step4, jobRepository, tx))
.next(taskletStep("calcMaintain", step5, jobRepository, tx))
.next(taskletStep("chargebackException", step6, jobRepository, tx))
.next(taskletStep("calcOverride", step7, jobRepository, tx))
.next(taskletStep("aggregate", step8, jobRepository, tx))
.build();
}
private Step taskletStep(String name, Tasklet tasklet,
JobRepository jobRepository, PlatformTransactionManager tx) {
return new StepBuilder(name, jobRepository)
.tasklet(tasklet, tx)
.build();
}
}
@@ -0,0 +1,53 @@
package com.ga.batch.controller;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import com.ga.common.model.ApiResponse;
import com.ga.core.mapper.settle.BatchJobLogMapper;
import com.ga.core.vo.settle.BatchJobLogVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* ga-batch 프로세스 내부 트리거. ga-api HTTP 호출.
*/
@Slf4j
@RestController
@RequestMapping("/internal/jobs")
@RequiredArgsConstructor
public class JobLauncherController {
private final JobLauncher jobLauncher;
private final Job monthlySettlementJob;
private final BatchJobLogMapper jobLogMapper;
@PostMapping("/monthly-settlement")
public ApiResponse<Long> runMonthlySettlement(@RequestBody Map<String, String> body) throws Exception {
String settleMonth = body.get("settleMonth");
if (settleMonth == null || !settleMonth.matches("\\d{6}")) {
throw new BizException(ErrorCode.INVALID_PARAMETER, "settleMonth (yyyyMM)");
}
BatchJobLogVO log = new BatchJobLogVO();
log.setJobName("MONTHLY_SETTLEMENT");
log.setSettleMonth(settleMonth);
log.setStatus("STARTED");
jobLogMapper.insert(log);
JobParameters params = new JobParametersBuilder()
.addString("settleMonth", settleMonth)
.addLong("jobLogId", log.getJobId())
.addLong("ts", System.currentTimeMillis())
.toJobParameters();
jobLauncher.run(monthlySettlementJob, params);
return ApiResponse.ok(log.getJobId());
}
}
@@ -0,0 +1,20 @@
package com.ga.batch.settlement;
import lombok.Data;
/**
* Job 단위 컨텍스트. Step 공유 상태.
*/
@Data
public class SettlementContext {
private String settleMonth; // yyyyMM
private Long jobLogId;
private int receivedCount;
private int transformedCount;
private int reconcileDiffCount;
private int recruitCount;
private int maintainCount;
private int chargebackCount;
private int overrideCount;
private int aggregateCount;
}
@@ -0,0 +1,72 @@
package com.ga.batch.settlement.calc;
import com.ga.common.util.MoneyUtil;
import com.ga.core.mapper.org.AgentMapper;
import com.ga.core.mapper.rule.RuleMapper;
import com.ga.core.vo.ledger.LedgerVO;
import com.ga.core.vo.org.AgentVO;
import com.ga.core.vo.product.ContractResp;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 수수료 계산 핵심 로직.
* - 모집수수료: premium × company_rate × payout_rate
* - 유지수수료: 동일 공식, 회차에 따른 commission_year 다름
* - 모든 금액은 BigDecimal HALF_UP, 소수점 2자리
*/
@Component
@RequiredArgsConstructor
public class CommissionCalculator {
private final RuleMapper ruleMapper;
private final AgentMapper agentMapper;
public LedgerVO calcRecruitLedger(ContractResp c, String settleMonth) {
return buildLedger(c, settleMonth, 1);
}
public LedgerVO calcMaintainLedger(ContractResp c, String settleMonth, int commissionYear) {
return buildLedger(c, settleMonth, commissionYear);
}
private LedgerVO buildLedger(ContractResp c, String settleMonth, int year) {
AgentVO agent = agentMapper.selectById(c.getAgentId());
if (agent == null) return null;
LocalDate baseDate = c.getContractDate() != null ? c.getContractDate() : LocalDate.now();
BigDecimal companyRate = ruleMapper.findCompanyRate(c.getProductId(), year, baseDate);
if (companyRate == null) companyRate = BigDecimal.ZERO;
BigDecimal payoutRate = ruleMapper.findPayoutRate(
agent.getGradeId(), c.getInsuranceType(), year, baseDate);
if (payoutRate == null) payoutRate = BigDecimal.ZERO;
BigDecimal premium = MoneyUtil.zero(c.getPremium());
BigDecimal companyAmount = MoneyUtil.pct(premium, companyRate);
BigDecimal agentAmount = MoneyUtil.pct(companyAmount, payoutRate);
BigDecimal taxAmount = MoneyUtil.tax(agentAmount);
LedgerVO led = new LedgerVO();
led.setContractId(c.getContractId());
led.setAgentId(c.getAgentId());
led.setPolicyNo(c.getPolicyNo());
led.setCompanyCode(c.getCompanyCode());
led.setProductCode(c.getProductCode());
led.setInsuranceType(c.getInsuranceType());
led.setCommissionYear(year);
led.setPremium(premium);
led.setPayMethod(c.getPayCycle());
led.setCompanyRate(companyRate);
led.setCompanyAmount(companyAmount);
led.setPayoutRate(payoutRate);
led.setAgentAmount(agentAmount);
led.setTaxAmount(taxAmount);
led.setSettleMonth(settleMonth);
led.setStatus("CALCULATED");
return led;
}
}
@@ -0,0 +1,12 @@
package com.ga.batch.settlement.receive;
/**
* 보험사별 데이터 수신 전략. (Excel/CSV/EDI/API)
*/
public interface DataReceiver {
/** 이 receiver 가 해당 보험사를 지원하는지 */
boolean supports(String companyCode);
/** 데이터 수신 → raw_commission_data 적재. 처리한 행 수 반환 */
int receive(String companyCode, String settleMonth);
}
@@ -0,0 +1,33 @@
package com.ga.batch.settlement.receive;
import com.ga.core.mapper.receive.ReceiveMapper;
import com.ga.core.vo.receive.CompanyProfileVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 수동 업로드된 데이터를 사용하는 기본 Receiver.
* (이미 화면에서 업로드된 raw_commission_data 있으면 그대로 사용)
*
* 운영에서 FTP/API Receiver 추가 인터페이스 구현체로 추가.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ManualReceiver implements DataReceiver {
private final ReceiveMapper mapper;
@Override
public boolean supports(String companyCode) {
CompanyProfileVO p = mapper.selectProfile(companyCode);
return p == null || "MANUAL".equalsIgnoreCase(p.getReceiveMethod());
}
@Override
public int receive(String companyCode, String settleMonth) {
// 수동 업로드는 별도 작업 없음. 기존 raw 데이터 건수만 반환.
return mapper.selectRawList(companyCode, settleMonth, "PENDING").size();
}
}
@@ -0,0 +1,131 @@
package com.ga.batch.settlement.step;
import com.ga.batch.settlement.SettlementContext;
import com.ga.common.system.SystemConfigService;
import com.ga.common.util.MoneyUtil;
import com.ga.core.mapper.ledger.ExceptionLedgerMapper;
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.mapper.org.AgentMapper;
import com.ga.core.mapper.settle.ChargebackMapper;
import com.ga.core.mapper.settle.OverrideSettleMapper;
import com.ga.core.mapper.settle.SettleMasterMapper;
import com.ga.core.vo.org.AgentResp;
import com.ga.core.vo.org.AgentSearchParam;
import com.ga.core.vo.settle.SettleMasterVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Step 8 aggregate: 설계사별 통합 집계 settle_master UPSERT (세금 자동 계산)
*/
@Slf4j
@Component
@JobScope
@RequiredArgsConstructor
public class AggregateStep implements Tasklet {
private final SettlementContext ctx;
private final AgentMapper agentMapper;
private final RecruitLedgerMapper recruitMapper;
private final MaintainLedgerMapper maintainMapper;
private final ExceptionLedgerMapper exceptionMapper;
private final OverrideSettleMapper overrideMapper;
private final ChargebackMapper chargebackMapper;
private final SettleMasterMapper settleMapper;
private final SystemConfigService configService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("[Step8 aggregate] settleMonth={}", ctx.getSettleMonth());
Map<Long, BigDecimal> recruitMap = toMap(recruitMapper.aggregateByAgent(ctx.getSettleMonth()));
Map<Long, BigDecimal> maintainMap = toMap(maintainMapper.aggregateByAgent(ctx.getSettleMonth()));
Map<Long, BigDecimal> chargebackMap= toMap(chargebackMapper.aggregateByAgent(ctx.getSettleMonth()));
Map<Long, BigDecimal> overrideMap = toMap(overrideMapper.aggregateByUpper(ctx.getSettleMonth()));
// 예외는 PLUS/MINUS 분리
Map<Long, BigDecimal> excPlus = new HashMap<>();
Map<Long, BigDecimal> excMinus = new HashMap<>();
for (Map<String, Object> row : exceptionMapper.aggregateByAgent(ctx.getSettleMonth())) {
Long agentId = ((Number) row.get("agentId")).longValue();
excPlus.put(agentId, toBd(row.get("plus")));
excMinus.put(agentId, toBd(row.get("minus")));
}
BigDecimal taxRate = configService.getDecimal("TAX_RATE", new BigDecimal("3.3"));
// 합집합 agent_id
java.util.Set<Long> agentIds = new java.util.HashSet<>();
agentIds.addAll(recruitMap.keySet());
agentIds.addAll(maintainMap.keySet());
agentIds.addAll(chargebackMap.keySet());
agentIds.addAll(overrideMap.keySet());
agentIds.addAll(excPlus.keySet());
agentIds.addAll(excMinus.keySet());
// 활성 설계사 누락 보충
AgentSearchParam ap = new AgentSearchParam();
ap.setStatus("ACTIVE"); ap.setPageSize(Integer.MAX_VALUE);
for (AgentResp a : agentMapper.selectList(ap)) agentIds.add(a.getAgentId());
int count = 0;
for (Long agentId : agentIds) {
BigDecimal recruit = recruitMap.getOrDefault(agentId, BigDecimal.ZERO);
BigDecimal maintain = maintainMap.getOrDefault(agentId, BigDecimal.ZERO);
BigDecimal cb = chargebackMap.getOrDefault(agentId, BigDecimal.ZERO);
BigDecimal ov = overrideMap.getOrDefault(agentId, BigDecimal.ZERO);
BigDecimal ep = excPlus.getOrDefault(agentId, BigDecimal.ZERO);
BigDecimal em = excMinus.getOrDefault(agentId, BigDecimal.ZERO);
BigDecimal gross = MoneyUtil.add(MoneyUtil.add(MoneyUtil.add(recruit, maintain), ov), ep)
.subtract(cb).subtract(em);
BigDecimal tax = MoneyUtil.tax(gross, taxRate);
BigDecimal net = MoneyUtil.sub(gross, tax);
SettleMasterVO vo = new SettleMasterVO();
vo.setAgentId(agentId);
vo.setSettleMonth(ctx.getSettleMonth());
vo.setRecruitTotal(recruit);
vo.setMaintainTotal(maintain);
vo.setExceptionPlus(ep);
vo.setExceptionMinus(em);
vo.setOverrideTotal(ov);
vo.setChargebackTotal(cb);
vo.setGrossAmount(gross);
vo.setTaxAmount(tax);
vo.setNetAmount(net);
vo.setStatus("CALCULATED");
settleMapper.upsert(vo);
count++;
}
ctx.setAggregateCount(count);
log.info("[Step8] settle master upserted: {}", count);
return RepeatStatus.FINISHED;
}
private Map<Long, BigDecimal> toMap(List<Map<String, Object>> rows) {
Map<Long, BigDecimal> m = new HashMap<>();
for (Map<String, Object> r : rows) {
Long id = ((Number) r.get("agentId")).longValue();
m.put(id, toBd(r.get("total")));
}
return m;
}
private BigDecimal toBd(Object v) {
if (v == null) return BigDecimal.ZERO;
if (v instanceof BigDecimal bd) return bd;
return new BigDecimal(v.toString());
}
}
@@ -0,0 +1,98 @@
package com.ga.batch.settlement.step;
import com.ga.batch.settlement.SettlementContext;
import com.ga.batch.settlement.calc.CommissionCalculator;
import com.ga.common.util.DateUtil;
import com.ga.common.util.MoneyUtil;
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
import com.ga.core.mapper.product.ContractMapper;
import com.ga.core.vo.ledger.LedgerVO;
import com.ga.core.vo.product.ContractResp;
import com.ga.core.vo.product.ContractSearchParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* Step 5 calcMaintain: 유지수수료 계산 (2회차~)
*
* 유지수수료: 계약일로부터 N개월 경과한 활동 계약 (status=ACTIVE) 대상.
* - 1회차는 모집수수료에서 처리하므로 N >= 1 계약 대상.
*/
@Slf4j
@Component
@JobScope
@RequiredArgsConstructor
public class CalcMaintainStep implements Tasklet {
private final SettlementContext ctx;
private final ContractMapper contractMapper;
private final MaintainLedgerMapper maintainMapper;
private final CommissionCalculator calculator;
private static final int BATCH_SIZE = 1000;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("[Step5 calcMaintain] settleMonth={}", ctx.getSettleMonth());
ContractSearchParam param = new ContractSearchParam();
param.setStatus("ACTIVE");
param.setPageSize(Integer.MAX_VALUE);
List<ContractResp> contracts = contractMapper.selectList(param);
maintainMapper.deleteBySettleMonth(ctx.getSettleMonth());
List<LedgerVO> batch = new ArrayList<>(BATCH_SIZE);
int total = 0;
for (ContractResp c : contracts) {
if (c.getContractDate() == null) continue;
String contractMonth = DateUtil.getSettleMonth(c.getContractDate());
int monthsElapsed = DateUtil.calcMonthDiff(contractMonth, ctx.getSettleMonth());
if (monthsElapsed < 1) continue; // 모집회차는 step4에서 처리
int year = (monthsElapsed / 12) + 1; // 1년차/2년차/...
if (year > 5) continue; // 통상 5년차까지
// 납입주기 매칭 (월납이면 매월, 분기납이면 3개월마다 ...)
if (!matchesPayCycle(c.getPayCycle(), monthsElapsed)) continue;
LedgerVO led = calculator.calcMaintainLedger(c, ctx.getSettleMonth(), year);
if (led == null || MoneyUtil.isZero(led.getAgentAmount())) continue;
batch.add(led);
if (batch.size() >= BATCH_SIZE) {
maintainMapper.batchInsert(batch);
total += batch.size();
batch.clear();
}
}
if (!batch.isEmpty()) {
maintainMapper.batchInsert(batch);
total += batch.size();
}
ctx.setMaintainCount(total);
log.info("[Step5] maintain ledger inserted: {}", total);
return RepeatStatus.FINISHED;
}
private boolean matchesPayCycle(String cycle, int monthsElapsed) {
if (cycle == null) return true;
return switch (cycle) {
case "MONTHLY" -> true;
case "QUARTERLY" -> monthsElapsed % 3 == 0;
case "ANNUAL" -> monthsElapsed % 12 == 0;
case "LUMP" -> false; // 일시납은 모집회차만
default -> true;
};
}
}
@@ -0,0 +1,110 @@
package com.ga.batch.settlement.step;
import com.ga.batch.settlement.SettlementContext;
import com.ga.common.util.MoneyUtil;
import com.ga.core.mapper.org.AgentMapper;
import com.ga.core.mapper.org.OrganizationMapper;
import com.ga.core.mapper.rule.RuleMapper;
import com.ga.core.mapper.settle.OverrideSettleMapper;
import com.ga.core.vo.org.AgentVO;
import com.ga.core.vo.rule.OverrideRuleVO;
import com.ga.core.vo.settle.OverrideSettleVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Step 7 calcOverride: 조직 트리 기반 오버라이드 계산
*
* 단순화: 정산월의 모집+유지 합계 × override_pct 상위 직급에게 분배.
* 실제로는 from_grade to_grade 매트릭스로 다단계 분배해야 .
*/
@Slf4j
@Component
@JobScope
@RequiredArgsConstructor
public class CalcOverrideStep implements Tasklet {
private final SettlementContext ctx;
private final RuleMapper ruleMapper;
private final AgentMapper agentMapper;
private final OrganizationMapper organizationMapper;
private final OverrideSettleMapper overrideMapper;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("[Step7 calcOverride] settleMonth={}", ctx.getSettleMonth());
overrideMapper.deleteBySettleMonth(ctx.getSettleMonth());
List<OverrideRuleVO> rules = ruleMapper.selectOverrideRules();
// 정산월의 활성 오버라이드 규칙만 (effective_from 기준)
LocalDate baseDate = LocalDate.parse(ctx.getSettleMonth().substring(0,4) + "-" + ctx.getSettleMonth().substring(4) + "-01");
rules.removeIf(r -> r.getEffectiveFrom() != null && r.getEffectiveFrom().isAfter(baseDate));
List<OverrideSettleVO> records = new ArrayList<>();
// 단순화: 모든 ACTIVE 설계사를 조회하고, 본인의 grade from_grade 규칙에 대해 상위 조직장 검색
com.ga.core.vo.org.AgentSearchParam ap = new com.ga.core.vo.org.AgentSearchParam();
ap.setStatus("ACTIVE");
ap.setPageSize(Integer.MAX_VALUE);
List<com.ga.core.vo.org.AgentResp> agents = agentMapper.selectList(ap);
for (com.ga.core.vo.org.AgentResp lower : agents) {
for (OverrideRuleVO rule : rules) {
if (!java.util.Objects.equals(rule.getFromGrade(), lower.getGradeId())) continue;
Long upperAgentId = findUpperAgentByGrade(lower.getOrgId(), rule.getToGrade());
if (upperAgentId == null) continue;
BigDecimal base = sumLowerCommission(lower.getAgentId(), ctx.getSettleMonth());
BigDecimal amount = MoneyUtil.pct(base, rule.getOverridePct());
if (rule.getCapAmount() != null && amount.compareTo(rule.getCapAmount()) > 0) {
amount = rule.getCapAmount();
}
if (MoneyUtil.isZero(amount)) continue;
OverrideSettleVO os = new OverrideSettleVO();
os.setUpperAgentId(upperAgentId);
os.setLowerAgentId(lower.getAgentId());
os.setSettleMonth(ctx.getSettleMonth());
os.setBaseAmount(base);
os.setOverridePct(rule.getOverridePct());
os.setOverrideAmount(amount);
records.add(os);
}
}
if (!records.isEmpty()) overrideMapper.insertBatch(records);
ctx.setOverrideCount(records.size());
log.info("[Step7] override inserted: {}", records.size());
return RepeatStatus.FINISHED;
}
/** 하위 설계사의 정산월 모집+유지 합계 (단순화: 모집 원장 합으로 대체) */
private BigDecimal sumLowerCommission(Long agentId, String settleMonth) {
// 실제로는 recruit + maintain agent별 합계 필요. 여기서는 0 반환 (실제 운영 별도 sumByAgentMonth 추가)
return BigDecimal.ZERO;
}
/** 동일 조직 트리에서 to_grade 직급의 상위 설계사 찾기 (단순화: org 트리 상위 첫 번째) */
private Long findUpperAgentByGrade(Long orgId, Long toGrade) {
if (orgId == null) return null;
// 조직 트리 상향 탐색은 운영 단계에서 추가. 여기서는 동일 조직 toGrade 1명 반환.
com.ga.core.vo.org.AgentSearchParam p = new com.ga.core.vo.org.AgentSearchParam();
p.setOrgId(orgId);
p.setGradeId(toGrade);
p.setStatus("ACTIVE");
p.setPageSize(1);
List<com.ga.core.vo.org.AgentResp> upper = agentMapper.selectList(p);
return upper.isEmpty() ? null : upper.get(0).getAgentId();
}
}
@@ -0,0 +1,89 @@
package com.ga.batch.settlement.step;
import com.ga.batch.settlement.SettlementContext;
import com.ga.batch.settlement.calc.CommissionCalculator;
import com.ga.common.util.MoneyUtil;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.mapper.product.ContractMapper;
import com.ga.core.vo.ledger.LedgerVO;
import com.ga.core.vo.product.ContractResp;
import com.ga.core.vo.product.ContractSearchParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* Step 4 calcRecruit: 모집 수수료 계산 (계약 회차)
*
* 원칙: 계약시점의 commission_rate / payout_rule 적용 (effective_from 기준)
*/
@Slf4j
@Component
@JobScope
@RequiredArgsConstructor
public class CalcRecruitStep implements Tasklet {
private final SettlementContext ctx;
private final ContractMapper contractMapper;
private final RecruitLedgerMapper recruitMapper;
private final CommissionCalculator calculator;
private static final int BATCH_SIZE = 1000;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("[Step4 calcRecruit] settleMonth={}", ctx.getSettleMonth());
// 모집수수료 대상: 계약일이 정산월에 해당하는 계약
ContractSearchParam param = new ContractSearchParam();
param.setStartDate(ctx.getSettleMonth().substring(0, 4) + "-" + ctx.getSettleMonth().substring(4) + "-01");
param.setEndDate(param.getStartDate());
param.setPageSize(Integer.MAX_VALUE);
List<ContractResp> contracts = contractMapper.selectList(param);
// 정산월 모집원장 초기화 (재계산 대비)
recruitMapper.deleteBySettleMonth(ctx.getSettleMonth());
List<LedgerVO> batch = new ArrayList<>(BATCH_SIZE);
int total = 0;
for (ContractResp c : contracts) {
LedgerVO led = calculator.calcRecruitLedger(c, ctx.getSettleMonth());
if (led == null || MoneyUtil.isZero(led.getAgentAmount())) continue;
batch.add(led);
if (batch.size() >= BATCH_SIZE) {
recruitMapper.batchInsert(batch);
total += batch.size();
batch.clear();
}
}
if (!batch.isEmpty()) {
recruitMapper.batchInsert(batch);
total += batch.size();
}
ctx.setRecruitCount(total);
log.info("[Step4] recruit ledger inserted: {}", total);
return RepeatStatus.FINISHED;
}
/** 정산월의 첫날 / 마지막날 — calcRecruit 대상 기간 */
private static String[] monthRange(String yyyymm) {
int year = Integer.parseInt(yyyymm.substring(0, 4));
int month = Integer.parseInt(yyyymm.substring(4));
java.time.YearMonth ym = java.time.YearMonth.of(year, month);
return new String[]{ym.atDay(1).toString(), ym.atEndOfMonth().toString()};
}
/** 외부에서 사용할 수도 있는 헬퍼 */
@SuppressWarnings("unused")
private BigDecimal premiumOrZero(ContractResp c) { return MoneyUtil.zero(c.getPremium()); }
}
@@ -0,0 +1,90 @@
package com.ga.batch.settlement.step;
import com.ga.batch.settlement.SettlementContext;
import com.ga.common.util.DateUtil;
import com.ga.common.util.MoneyUtil;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.mapper.product.ContractMapper;
import com.ga.core.mapper.rule.RuleMapper;
import com.ga.core.mapper.settle.ChargebackMapper;
import com.ga.core.vo.product.ContractResp;
import com.ga.core.vo.product.ContractSearchParam;
import com.ga.core.vo.settle.ChargebackVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* Step 6 chargebackException: 환수 자동생성 + 예외금액 자동계산
*
* 환수 대상: 정산월에 LAPSE(실효) 계약 계약일~실효까지 경과월수로 환수율 결정.
*/
@Slf4j
@Component
@JobScope
@RequiredArgsConstructor
public class ChargebackExceptionStep implements Tasklet {
private final SettlementContext ctx;
private final ContractMapper contractMapper;
private final RecruitLedgerMapper recruitMapper;
private final RuleMapper ruleMapper;
private final ChargebackMapper chargebackMapper;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("[Step6 chargebackException] settleMonth={}", ctx.getSettleMonth());
ContractSearchParam param = new ContractSearchParam();
param.setStatus("LAPSE");
param.setPageSize(Integer.MAX_VALUE);
List<ContractResp> lapsed = contractMapper.selectList(param);
List<ChargebackVO> chargebacks = new ArrayList<>();
for (ContractResp c : lapsed) {
if (c.getLapseDate() == null) continue;
String lapseMonth = DateUtil.getSettleMonth(c.getLapseDate());
if (!ctx.getSettleMonth().equals(lapseMonth)) continue;
if (c.getContractDate() == null) continue;
int monthsActive = DateUtil.calcMonthDiff(
DateUtil.getSettleMonth(c.getContractDate()), lapseMonth);
BigDecimal cbRate = ruleMapper.findChargebackRate(
c.getCompanyCode(), c.getInsuranceType(), monthsActive, c.getLapseDate());
if (cbRate == null || cbRate.signum() <= 0) continue;
// 모집수수료 합계 × 환수율
BigDecimal originalAmount = MoneyUtil.zero(c.getPremium())
.multiply(cbRate)
.divide(MoneyUtil.HUNDRED, 2, java.math.RoundingMode.HALF_UP);
ChargebackVO cb = new ChargebackVO();
cb.setContractId(c.getContractId());
cb.setAgentId(c.getAgentId());
cb.setPolicyNo(c.getPolicyNo());
cb.setLapseMonth(monthsActive);
cb.setCbRate(cbRate);
cb.setCbAmount(originalAmount);
cb.setRemainAmount(originalAmount);
cb.setSettleMonth(ctx.getSettleMonth());
cb.setStatus("NEW");
chargebacks.add(cb);
}
if (!chargebacks.isEmpty()) {
chargebackMapper.insertBatch(chargebacks);
}
ctx.setChargebackCount(chargebacks.size());
log.info("[Step6] chargeback inserted: {}", chargebacks.size());
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,49 @@
package com.ga.batch.settlement.step;
import com.ga.batch.settlement.SettlementContext;
import com.ga.batch.settlement.receive.DataReceiver;
import com.ga.core.mapper.product.InsuranceCompanyMapper;
import com.ga.core.vo.product.InsuranceCompanyVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Step 1 receiveData: 보험사별 DataReceiver 호출 raw_commission_data 적재
*/
@Slf4j
@Component
@JobScope
@RequiredArgsConstructor
public class ReceiveDataStep implements Tasklet {
private final SettlementContext ctx;
private final InsuranceCompanyMapper companyMapper;
private final List<DataReceiver> receivers;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("[Step1 receiveData] settleMonth={}", ctx.getSettleMonth());
int total = 0;
for (InsuranceCompanyVO c : companyMapper.selectActive()) {
for (DataReceiver r : receivers) {
if (r.supports(c.getCompanyCode())) {
int n = r.receive(c.getCompanyCode(), ctx.getSettleMonth());
log.info(" - {} via {}: {} rows", c.getCompanyCode(), r.getClass().getSimpleName(), n);
total += n;
break;
}
}
}
ctx.setReceivedCount(total);
log.info("[Step1] received {} rows", total);
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,58 @@
package com.ga.batch.settlement.step;
import com.ga.batch.settlement.SettlementContext;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.mapper.product.InsuranceCompanyMapper;
import com.ga.core.mapper.settle.ReconciliationMapper;
import com.ga.core.vo.product.InsuranceCompanyVO;
import com.ga.core.vo.settle.ReconciliationVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* Step 3 reconcile: 보험사 통보액 vs 시스템 계산액 대사
*/
@Slf4j
@Component
@JobScope
@RequiredArgsConstructor
public class ReconcileStep implements Tasklet {
private final SettlementContext ctx;
private final InsuranceCompanyMapper companyMapper;
private final RecruitLedgerMapper recruitMapper;
private final ReconciliationMapper reconMapper;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("[Step3 reconcile] settleMonth={}", ctx.getSettleMonth());
int diff = 0;
for (InsuranceCompanyVO c : companyMapper.selectActive()) {
BigDecimal systemTotal = recruitMapper.sumByMonth(ctx.getSettleMonth());
// 회사 통보액 실제로는 raw_commission_data 합계로 계산. 여기선 systemTotal 동일 처리.
BigDecimal companyTotal = systemTotal;
ReconciliationVO r = new ReconciliationVO();
r.setCompanyCode(c.getCompanyCode());
r.setSettleMonth(ctx.getSettleMonth());
r.setCompanyTotal(companyTotal);
r.setSystemTotal(systemTotal);
r.setDiffAmount(companyTotal.subtract(systemTotal));
r.setDiffCount(0);
r.setStatus(r.getDiffAmount().signum() == 0 ? "MATCHED" : "DIFF");
reconMapper.upsert(r);
if ("DIFF".equals(r.getStatus())) diff++;
}
ctx.setReconcileDiffCount(diff);
log.info("[Step3] reconcile diff companies: {}", diff);
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,50 @@
package com.ga.batch.settlement.step;
import com.ga.batch.settlement.SettlementContext;
import com.ga.batch.settlement.transform.MappingEngine;
import com.ga.core.mapper.receive.ReceiveMapper;
import com.ga.core.vo.receive.RawCommissionDataVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Step 2 transformData: company_field_mapping + code_mapping 으로 표준 변환 원장 적재
*/
@Slf4j
@Component
@JobScope
@RequiredArgsConstructor
public class TransformDataStep implements Tasklet {
private final SettlementContext ctx;
private final ReceiveMapper receiveMapper;
private final MappingEngine mappingEngine;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("[Step2 transformData] settleMonth={}", ctx.getSettleMonth());
List<RawCommissionDataVO> raws = receiveMapper.selectRawList(null, ctx.getSettleMonth(), "PENDING");
int ok = 0;
for (RawCommissionDataVO raw : raws) {
try {
Long ledgerId = mappingEngine.transform(raw);
receiveMapper.updateRawStatus(raw.getRawId(), "OK", ledgerId, raw.getMappedTable());
ok++;
} catch (Exception e) {
log.warn("transform fail rawId={}: {}", raw.getRawId(), e.getMessage());
receiveMapper.updateRawStatus(raw.getRawId(), "ERROR", null, null);
}
}
ctx.setTransformedCount(ok);
log.info("[Step2] transformed {} / {}", ok, raws.size());
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,28 @@
package com.ga.batch.settlement.transform;
import com.ga.core.vo.receive.RawCommissionDataVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 보험사 raw 데이터 표준 원장(recruit_ledger / maintain_ledger / exception_ledger) 으로 변환.
*
* 구현은 단순화된 stub. 실제로는:
* - company_field_mapping 으로 raw_content(JSON ) 에서 필드를 추출
* - code_mapping 으로 보험사 코드 표준 코드 변환
* - data_type 따라 모집/유지/예외 원장 선택
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MappingEngine {
/** 변환 후 ledger_id 반환 (없으면 null) */
public Long transform(RawCommissionDataVO raw) {
// TODO: 실제 매핑 로직 company_field_mapping 기반 동적 변환
// 현재는 ID 미지정으로 처리 (Step4/5 에서 contract 기반으로 재계산)
log.debug("transform raw={}, type={}", raw.getRawId(), raw.getDataType());
return null;
}
}
@@ -0,0 +1,44 @@
spring:
application:
name: ga-batch
profiles:
active: local
datasource:
url: jdbc:postgresql://localhost:5432/ga
username: ga
password: ga
driver-class-name: org.postgresql.Driver
flyway:
enabled: false # ga-api 가 마이그레이션 담당
data:
redis:
host: localhost
port: 6379
batch:
jdbc:
initialize-schema: always
job:
enabled: false # 자동 실행 비활성. JobLauncher 로 명시 실행
server:
port: 8081
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.ga.core.vo,com.ga.common.model
pagehelper:
helper-dialect: postgresql
reasonable: true
ga:
jwt:
secret: ${JWT_SECRET:change-me-please-this-must-be-at-least-256-bits-long-secret-key}
encrypt:
key: ${ENCRYPT_KEY:0123456789abcdef0123456789abcdef}
logging:
level:
com.ga: INFO
org.springframework.batch: INFO
@@ -0,0 +1,31 @@
-- V13: 추가 인덱스 (성능 최적화)
-- settle_master: 정산월 기준 빈번한 조회
CREATE INDEX IF NOT EXISTS idx_sm_month_status ON settle_master(settle_month, status);
CREATE INDEX IF NOT EXISTS idx_sm_agent_month ON settle_master(agent_id, settle_month);
-- recruit_ledger / maintain_ledger: 정산월 + 설계사
CREATE INDEX IF NOT EXISTS idx_rl_month_agent ON recruit_ledger(settle_month, agent_id);
CREATE INDEX IF NOT EXISTS idx_ml_month_agent ON maintain_ledger(settle_month, agent_id);
CREATE INDEX IF NOT EXISTS idx_rl_company ON recruit_ledger(company_code, settle_month);
CREATE INDEX IF NOT EXISTS idx_ml_company ON maintain_ledger(company_code, settle_month);
-- exception_ledger: 정산월 + 승인상태
CREATE INDEX IF NOT EXISTS idx_el_month_approve ON exception_ledger(settle_month, approve_status);
-- contract: 만기/실효 처리용
CREATE INDEX IF NOT EXISTS idx_contract_lapse ON contract(lapse_date) WHERE lapse_date IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_contract_status_agent ON contract(status, agent_id);
-- chargeback / payment 인덱스 보강
CREATE INDEX IF NOT EXISTS idx_cb_month ON chargeback(settle_month, status);
CREATE INDEX IF NOT EXISTS idx_pay_month ON payment(pay_date, pay_status);
-- raw / 에러 / 매핑
CREATE INDEX IF NOT EXISTS idx_raw_status ON raw_commission_data(parse_status, settle_month);
CREATE INDEX IF NOT EXISTS idx_pe_company ON parse_error_log(company_code, resolve_status);
CREATE INDEX IF NOT EXISTS idx_codemap_lookup ON code_mapping(company_code, code_type, source_code) WHERE is_active = 'Y';
-- 로그 테이블 부분 인덱스 (날짜 검색)
CREATE INDEX IF NOT EXISTS idx_dcl_table_recent ON data_change_log(table_name, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_aal_url_recent ON api_access_log(url, created_at DESC);
@@ -0,0 +1,30 @@
-- V14: 파티셔닝 (대용량 로그 테이블)
--
-- 주의: 운영에서 기존 테이블을 파티셔닝하려면 데이터 마이그레이션이 필요.
-- 본 마이그레이션은 신규 테이블만 파티셔닝 적용.
-- 기존 api_access_log / data_change_log 는 별도 마이그레이션으로 전환 권장.
-- 본 V14 는 파티션 자동 생성을 위한 함수 + cron 형태의 helper.
-- 월별 파티션 테이블을 자동 생성하는 함수 (운영 시 cron 또는 pg_cron 으로 매월 호출)
CREATE OR REPLACE FUNCTION create_monthly_partition(
base_table TEXT,
target_month DATE
) RETURNS void AS $$
DECLARE
partition_name TEXT;
start_date DATE;
end_date DATE;
BEGIN
start_date := date_trunc('month', target_month);
end_date := start_date + interval '1 month';
partition_name := base_table || '_' || to_char(start_date, 'YYYYMM');
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',
partition_name, base_table, start_date, end_date
);
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION create_monthly_partition IS
'파티션 테이블에 월별 파티션을 자동 생성. 예: SELECT create_monthly_partition(''api_access_log'', NOW())';
@@ -0,0 +1,63 @@
-- V15: 통계/리포트용 뷰
-- 설계사별 월 실적 요약
CREATE OR REPLACE VIEW v_agent_monthly_summary AS
SELECT
sm.agent_id,
a.agent_name,
o.org_id,
o.org_name,
g.grade_name,
sm.settle_month,
sm.recruit_total,
sm.maintain_total,
sm.exception_plus,
sm.exception_minus,
sm.override_total,
sm.chargeback_total,
sm.gross_amount,
sm.tax_amount,
sm.net_amount,
sm.status
FROM settle_master sm
JOIN agent a ON a.agent_id = sm.agent_id
LEFT JOIN organization o ON o.org_id = a.org_id
LEFT JOIN grade g ON g.grade_id = a.grade_id;
-- 조직별 월 정산 합계
CREATE OR REPLACE VIEW v_org_settle_summary AS
SELECT
a.org_id,
o.org_name,
o.org_type,
sm.settle_month,
COUNT(sm.settle_id) AS agent_count,
COALESCE(SUM(sm.recruit_total), 0) AS recruit_total,
COALESCE(SUM(sm.maintain_total), 0) AS maintain_total,
COALESCE(SUM(sm.gross_amount), 0) AS gross_amount,
COALESCE(SUM(sm.tax_amount), 0) AS tax_amount,
COALESCE(SUM(sm.net_amount), 0) AS net_amount
FROM settle_master sm
JOIN agent a ON a.agent_id = sm.agent_id
JOIN organization o ON o.org_id = a.org_id
GROUP BY a.org_id, o.org_name, o.org_type, sm.settle_month;
-- 환수 위험 계약 (실효 + 환수 발생)
CREATE OR REPLACE VIEW v_chargeback_risk AS
SELECT
cb.cb_id,
cb.contract_id,
cb.agent_id,
a.agent_name,
o.org_name,
cb.policy_no,
cb.lapse_month,
cb.cb_rate,
cb.cb_amount,
cb.paid_amount,
cb.remain_amount,
cb.settle_month,
cb.status
FROM chargeback cb
JOIN agent a ON a.agent_id = cb.agent_id
LEFT JOIN organization o ON o.org_id = a.org_id;
@@ -0,0 +1,29 @@
package com.ga.core.mapper.ledger;
import com.ga.core.vo.ledger.ExceptionLedgerResp;
import com.ga.core.vo.ledger.ExceptionLedgerVO;
import com.ga.core.vo.ledger.LedgerSearchParam;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@Mapper
public interface ExceptionLedgerMapper {
List<ExceptionLedgerResp> selectList(LedgerSearchParam param);
ExceptionLedgerResp selectDetailById(@Param("ledgerId") Long ledgerId);
ExceptionLedgerVO selectById(@Param("ledgerId") Long ledgerId);
int insert(ExceptionLedgerVO vo);
int update(ExceptionLedgerVO vo);
int updateApprove(@Param("ledgerId") Long ledgerId,
@Param("status") String status,
@Param("approverId") Long approverId);
int decrementRecurring(@Param("ledgerId") Long ledgerId);
/** 정산월별 PLUS/MINUS 합계 */
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
BigDecimal sumPlusByMonth(@Param("settleMonth") String settleMonth);
BigDecimal sumMinusByMonth(@Param("settleMonth") String settleMonth);
}
@@ -0,0 +1,27 @@
package com.ga.core.mapper.ledger;
import com.ga.core.vo.ledger.LedgerExcelVO;
import com.ga.core.vo.ledger.LedgerResp;
import com.ga.core.vo.ledger.LedgerSearchParam;
import com.ga.core.vo.ledger.LedgerVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.cursor.Cursor;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@Mapper
public interface MaintainLedgerMapper {
List<LedgerResp> selectList(LedgerSearchParam param);
LedgerResp selectDetailById(@Param("ledgerId") Long ledgerId);
LedgerVO selectById(@Param("ledgerId") Long ledgerId);
Cursor<LedgerExcelVO> selectCursorForExcel(LedgerSearchParam param);
int insert(LedgerVO vo);
int batchInsert(@Param("list") List<LedgerVO> list);
int update(LedgerVO vo);
int deleteBySettleMonth(@Param("settleMonth") String settleMonth);
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
BigDecimal sumByMonth(@Param("settleMonth") String settleMonth);
}
@@ -0,0 +1,30 @@
package com.ga.core.mapper.ledger;
import com.ga.core.vo.ledger.LedgerExcelVO;
import com.ga.core.vo.ledger.LedgerResp;
import com.ga.core.vo.ledger.LedgerSearchParam;
import com.ga.core.vo.ledger.LedgerVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.cursor.Cursor;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@Mapper
public interface RecruitLedgerMapper {
List<LedgerResp> selectList(LedgerSearchParam param);
LedgerResp selectDetailById(@Param("ledgerId") Long ledgerId);
LedgerVO selectById(@Param("ledgerId") Long ledgerId);
Cursor<LedgerExcelVO> selectCursorForExcel(LedgerSearchParam param);
int insert(LedgerVO vo);
int batchInsert(@Param("list") List<LedgerVO> list);
int update(LedgerVO vo);
int deleteBySettleMonth(@Param("settleMonth") String settleMonth);
/** 정산월 기준 설계사별 합계 (정산 마스터용) */
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
/** 정산월 합계 */
BigDecimal sumByMonth(@Param("settleMonth") String settleMonth);
}
@@ -0,0 +1,32 @@
package com.ga.core.mapper.org;
import com.ga.core.vo.org.AgentExcelVO;
import com.ga.core.vo.org.AgentOrgHistoryVO;
import com.ga.core.vo.org.AgentResp;
import com.ga.core.vo.org.AgentSearchParam;
import com.ga.core.vo.org.AgentVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.cursor.Cursor;
import java.util.List;
@Mapper
public interface AgentMapper {
AgentResp selectDetailById(@Param("agentId") Long agentId);
AgentVO selectById(@Param("agentId") Long agentId);
List<AgentResp> selectList(AgentSearchParam param);
Cursor<AgentExcelVO> selectCursorForExcel(AgentSearchParam param);
int existsByLicenseNo(@Param("licenseNo") String licenseNo);
int insert(AgentVO vo);
int update(AgentVO vo);
int updateStatus(@Param("agentId") Long agentId, @Param("status") String status);
int updateOrgGrade(@Param("agentId") Long agentId,
@Param("orgId") Long orgId,
@Param("gradeId") Long gradeId);
/** 이력 */
List<AgentOrgHistoryVO> selectHistory(@Param("agentId") Long agentId);
int insertHistory(AgentOrgHistoryVO vo);
}
@@ -0,0 +1,17 @@
package com.ga.core.mapper.org;
import com.ga.core.vo.org.GradeVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface GradeMapper {
List<GradeVO> selectAll();
List<GradeVO> selectActive();
GradeVO selectById(@Param("gradeId") Long gradeId);
int insert(GradeVO vo);
int update(GradeVO vo);
int deleteById(@Param("gradeId") Long gradeId);
}
@@ -0,0 +1,24 @@
package com.ga.core.mapper.org;
import com.ga.core.vo.org.OrganizationResp;
import com.ga.core.vo.org.OrganizationVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface OrganizationMapper {
List<OrganizationResp> selectAllForTree();
List<OrganizationResp> selectList(@Param("keyword") String keyword,
@Param("orgType") String orgType,
@Param("isActive") String isActive);
OrganizationVO selectById(@Param("orgId") Long orgId);
/** 자기 자신 + 모든 하위 조직 ID (CTE 재귀) */
List<Long> selectDescendantIds(@Param("orgId") Long orgId);
int insert(OrganizationVO vo);
int update(OrganizationVO vo);
int deleteById(@Param("orgId") Long orgId);
int countChildren(@Param("orgId") Long orgId);
int countAgents(@Param("orgId") Long orgId);
}
@@ -0,0 +1,21 @@
package com.ga.core.mapper.product;
import com.ga.core.vo.product.ContractResp;
import com.ga.core.vo.product.ContractSearchParam;
import com.ga.core.vo.product.ContractVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ContractMapper {
List<ContractResp> selectList(ContractSearchParam param);
ContractResp selectDetailById(@Param("contractId") Long contractId);
ContractVO selectById(@Param("contractId") Long contractId);
int existsByPolicyNo(@Param("policyNo") String policyNo);
int insert(ContractVO vo);
int update(ContractVO vo);
int updateStatus(@Param("contractId") Long contractId, @Param("status") String status);
int countByAgent(@Param("agentId") Long agentId, @Param("status") String status);
}
@@ -0,0 +1,18 @@
package com.ga.core.mapper.product;
import com.ga.core.vo.product.InsuranceCompanyVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface InsuranceCompanyMapper {
List<InsuranceCompanyVO> selectAll();
List<InsuranceCompanyVO> selectActive();
InsuranceCompanyVO selectById(@Param("companyId") Long companyId);
InsuranceCompanyVO selectByCode(@Param("companyCode") String companyCode);
int insert(InsuranceCompanyVO vo);
int update(InsuranceCompanyVO vo);
int deleteById(@Param("companyId") Long companyId);
}
@@ -0,0 +1,22 @@
package com.ga.core.mapper.product;
import com.ga.core.vo.product.ProductResp;
import com.ga.core.vo.product.ProductVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ProductMapper {
List<ProductResp> selectList(@Param("companyId") Long companyId,
@Param("insuranceType") String insuranceType,
@Param("keyword") String keyword,
@Param("isActive") String isActive);
ProductResp selectDetailById(@Param("productId") Long productId);
ProductVO selectById(@Param("productId") Long productId);
int existsByCode(@Param("companyId") Long companyId, @Param("productCode") String productCode);
int insert(ProductVO vo);
int update(ProductVO vo);
int deleteById(@Param("productId") Long productId);
}
@@ -0,0 +1,54 @@
package com.ga.core.mapper.receive;
import com.ga.core.vo.receive.*;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ReceiveMapper {
// company_profile
List<CompanyProfileVO> selectAllProfiles();
CompanyProfileVO selectProfile(@Param("companyCode") String companyCode);
int insertProfile(CompanyProfileVO vo);
int updateProfile(CompanyProfileVO vo);
// field_mapping
List<CompanyFieldMappingVO> selectFieldMappings(@Param("companyCode") String companyCode,
@Param("targetTable") String targetTable);
int insertFieldMapping(CompanyFieldMappingVO vo);
int updateFieldMapping(CompanyFieldMappingVO vo);
int deleteFieldMapping(@Param("mappingId") Long mappingId);
// code_mapping
List<CodeMappingVO> selectCodeMappings(@Param("companyCode") String companyCode,
@Param("codeType") String codeType);
String findTargetCode(@Param("companyCode") String companyCode,
@Param("codeType") String codeType,
@Param("sourceCode") String sourceCode);
int insertCodeMapping(CodeMappingVO vo);
int updateCodeMapping(CodeMappingVO vo);
int deleteCodeMapping(@Param("codeId") Long codeId);
// raw_commission_data
int insertRaw(RawCommissionDataVO vo);
int insertRawBatch(@Param("list") List<RawCommissionDataVO> list);
List<RawCommissionDataVO> selectRawList(@Param("companyCode") String companyCode,
@Param("settleMonth") String settleMonth,
@Param("parseStatus") String parseStatus);
int updateRawStatus(@Param("rawId") Long rawId,
@Param("parseStatus") String parseStatus,
@Param("mappedLedgerId") Long mappedLedgerId,
@Param("mappedTable") String mappedTable);
// parse_error_log
List<ParseErrorLogVO> selectErrors(@Param("companyCode") String companyCode,
@Param("resolveStatus") String resolveStatus);
int insertError(ParseErrorLogVO vo);
int resolveError(@Param("errorId") Long errorId,
@Param("status") String status,
@Param("note") String note,
@Param("userId") Long userId);
}
@@ -0,0 +1,60 @@
package com.ga.core.mapper.rule;
import com.ga.core.vo.rule.*;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
/**
* 수수료 규정 Mapper. 5개 테이블을 통합한 인터페이스.
*/
@Mapper
public interface RuleMapper {
// === commission_rate ===
List<CommissionRateVO> selectCommissionRates(@Param("productId") Long productId,
@Param("year") Integer year);
/** 계약일에 유효한 보험사 수수료율 */
BigDecimal findCompanyRate(@Param("productId") Long productId,
@Param("year") Integer year,
@Param("baseDate") LocalDate baseDate);
int insertCommissionRate(CommissionRateVO vo);
int updateCommissionRate(CommissionRateVO vo);
int deleteCommissionRate(@Param("rateId") Long rateId);
// === payout_rule ===
List<PayoutRuleVO> selectPayoutRules(@Param("gradeId") Long gradeId,
@Param("insuranceType") String insuranceType);
BigDecimal findPayoutRate(@Param("gradeId") Long gradeId,
@Param("insuranceType") String insuranceType,
@Param("year") Integer year,
@Param("baseDate") LocalDate baseDate);
int insertPayoutRule(PayoutRuleVO vo);
int updatePayoutRule(PayoutRuleVO vo);
int deletePayoutRule(@Param("ruleId") Long ruleId);
// === override_rule ===
List<OverrideRuleVO> selectOverrideRules();
int insertOverrideRule(OverrideRuleVO vo);
int updateOverrideRule(OverrideRuleVO vo);
int deleteOverrideRule(@Param("overrideId") Long overrideId);
// === chargeback_rule ===
List<ChargebackRuleVO> selectChargebackRules(@Param("companyCode") String companyCode);
BigDecimal findChargebackRate(@Param("companyCode") String companyCode,
@Param("insuranceType") String insuranceType,
@Param("lapseMonth") Integer lapseMonth,
@Param("baseDate") LocalDate baseDate);
int insertChargebackRule(ChargebackRuleVO vo);
int updateChargebackRule(ChargebackRuleVO vo);
int deleteChargebackRule(@Param("cbRuleId") Long cbRuleId);
// === exception_type_code ===
List<ExceptionTypeCodeVO> selectExceptionCodes();
ExceptionTypeCodeVO selectExceptionCode(@Param("exceptionCode") String exceptionCode);
int insertExceptionCode(ExceptionTypeCodeVO vo);
int updateExceptionCode(ExceptionTypeCodeVO vo);
}
@@ -0,0 +1,18 @@
package com.ga.core.mapper.settle;
import com.ga.core.vo.settle.BatchJobLogVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface BatchJobLogMapper {
int insert(BatchJobLogVO vo);
int update(BatchJobLogVO vo);
BatchJobLogVO selectById(@Param("jobId") Long jobId);
BatchJobLogVO selectLatest(@Param("jobName") String jobName);
List<BatchJobLogVO> selectList(@Param("jobName") String jobName,
@Param("settleMonth") String settleMonth);
int isRunning(@Param("jobName") String jobName);
}
@@ -0,0 +1,23 @@
package com.ga.core.mapper.settle;
import com.ga.core.vo.settle.ChargebackVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@Mapper
public interface ChargebackMapper {
int insert(ChargebackVO vo);
int insertBatch(@Param("list") List<ChargebackVO> list);
List<ChargebackVO> selectByAgent(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth);
int updatePaid(@Param("cbId") Long cbId,
@Param("paidAmount") BigDecimal paidAmount,
@Param("status") String status);
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
BigDecimal sumByMonth(@Param("settleMonth") String settleMonth);
}
@@ -0,0 +1,21 @@
package com.ga.core.mapper.settle;
import com.ga.core.vo.settle.OverrideSettleVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@Mapper
public interface OverrideSettleMapper {
int insert(OverrideSettleVO vo);
int insertBatch(@Param("list") List<OverrideSettleVO> list);
List<OverrideSettleVO> selectByUpper(@Param("upperAgentId") Long upperAgentId,
@Param("settleMonth") String settleMonth);
int deleteBySettleMonth(@Param("settleMonth") String settleMonth);
/** 정산월 + 상위설계사별 override 합계 */
List<Map<String, Object>> aggregateByUpper(@Param("settleMonth") String settleMonth);
BigDecimal sumByMonth(@Param("settleMonth") String settleMonth);
}
@@ -0,0 +1,22 @@
package com.ga.core.mapper.settle;
import com.ga.core.vo.settle.PaymentResp;
import com.ga.core.vo.settle.PaymentVO;
import com.ga.core.vo.settle.SettleSearchParam;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface PaymentMapper {
List<PaymentResp> selectList(SettleSearchParam param);
PaymentVO selectById(@Param("paymentId") Long paymentId);
int insert(PaymentVO vo);
int insertBatch(@Param("list") List<PaymentVO> list);
int updateStatus(@Param("paymentId") Long paymentId,
@Param("status") String status,
@Param("failReason") String failReason);
int updateTransferFile(@Param("paymentIds") List<Long> paymentIds,
@Param("transferFileId") Long transferFileId);
}
@@ -0,0 +1,16 @@
package com.ga.core.mapper.settle;
import com.ga.core.vo.settle.ReconciliationVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ReconciliationMapper {
List<ReconciliationVO> selectByMonth(@Param("settleMonth") String settleMonth);
int upsert(ReconciliationVO vo);
int resolve(@Param("reconId") Long reconId,
@Param("note") String note,
@Param("userId") Long userId);
}
@@ -0,0 +1,35 @@
package com.ga.core.mapper.settle;
import com.ga.core.vo.settle.SettleMasterResp;
import com.ga.core.vo.settle.SettleMasterVO;
import com.ga.core.vo.settle.SettleSearchParam;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
@Mapper
public interface SettleMasterMapper {
List<SettleMasterResp> selectList(SettleSearchParam param);
SettleMasterResp selectDetail(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth);
SettleMasterVO selectByAgentAndMonth(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth);
SettleMasterVO selectById(@Param("settleId") Long settleId);
/** 정산 마스터 UPSERT (agent_id + settle_month UNIQUE) */
int upsert(SettleMasterVO vo);
int updateStatus(@Param("settleId") Long settleId,
@Param("status") String status,
@Param("userId") Long userId);
int updateHold(@Param("settleId") Long settleId,
@Param("reason") String reason);
/** 월 요약 (전체) */
Map<String, Object> selectMonthSummary(@Param("settleMonth") String settleMonth);
/** 조직별 월 합계 */
List<Map<String, Object>> selectOrgSummary(@Param("settleMonth") String settleMonth);
}
@@ -0,0 +1,24 @@
package com.ga.core.mapper.user;
import com.ga.core.vo.user.RoleVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface RoleMapper {
List<RoleVO> selectAll();
RoleVO selectById(@Param("roleId") Long roleId);
int insert(RoleVO vo);
int update(RoleVO vo);
int deleteById(@Param("roleId") Long roleId);
/** 역할의 (menu_id, perm_code) 권한 목록 */
List<java.util.Map<String, Object>> selectRolePermissions(@Param("roleId") Long roleId);
int deleteRolePermissions(@Param("roleId") Long roleId);
int insertRolePermission(@Param("roleId") Long roleId,
@Param("menuId") Long menuId,
@Param("permCode") String permCode,
@Param("isGranted") String isGranted);
}
@@ -0,0 +1,33 @@
package com.ga.core.mapper.user;
import com.ga.core.vo.user.UserResp;
import com.ga.core.vo.user.UserSearchParam;
import com.ga.core.vo.user.UserVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface UserMapper {
UserVO selectById(@Param("userId") Long userId);
UserVO selectByLoginId(@Param("loginId") String loginId);
UserResp selectDetailById(@Param("userId") Long userId);
List<UserResp> selectList(UserSearchParam param);
int countByCondition(UserSearchParam param);
int insert(UserVO vo);
int update(UserVO vo);
int updatePassword(@Param("userId") Long userId, @Param("password") String password);
int updateLastLogin(@Param("userId") Long userId);
int incrementFailCount(@Param("userId") Long userId);
int resetFailCount(@Param("userId") Long userId);
int updateStatus(@Param("userId") Long userId, @Param("status") String status);
List<String> selectRoleCodes(@Param("userId") Long userId);
int insertUserRole(@Param("userId") Long userId, @Param("roleId") Long roleId);
int deleteUserRoles(@Param("userId") Long userId);
}
@@ -0,0 +1,33 @@
package com.ga.core.vo.ledger;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class ExceptionLedgerResp {
private Long ledgerId;
private Long contractId;
private Long agentId;
private String agentName;
private String orgName;
private String exceptionCode;
private String exceptionName;
private String direction;
private String policyNo;
private String companyCode;
private String insuranceType;
private BigDecimal amount;
private BigDecimal taxAmount;
private String settleMonth;
private String description;
private String approveStatus;
private String approveStatusName;
private Long approverId;
private String approverName;
private LocalDateTime approveDate;
private String recurringYn;
private Integer recurringLeft;
private String status;
}
@@ -0,0 +1,21 @@
package com.ga.core.vo.ledger;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ExceptionLedgerSaveReq {
@NotNull private Long agentId;
@NotBlank private String exceptionCode;
@NotBlank private String settleMonth;
@NotNull private BigDecimal amount;
private String policyNo;
private String companyCode;
private String insuranceType;
private String description;
private String recurringYn;
private Integer recurringMonths;
}
@@ -0,0 +1,32 @@
package com.ga.core.vo.ledger;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class ExceptionLedgerVO {
private Long ledgerId;
private Long contractId;
private Long agentId;
private String exceptionCode;
private String policyNo;
private String companyCode;
private String insuranceType;
private Integer commissionYear;
private BigDecimal amount;
private BigDecimal taxAmount;
private String settleMonth;
private String description;
private String approveStatus; // PENDING / APPROVED / REJECTED
private Long approverId;
private LocalDateTime approveDate;
private String recurringYn;
private Integer recurringMonths;
private Integer recurringLeft;
private String status;
private Integer version;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,36 @@
package com.ga.core.vo.ledger;
import com.ga.common.annotation.ExcelColumn;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class LedgerExcelVO {
@ExcelColumn(header = "정산월", order = 1, width = 10)
private String settleMonth;
@ExcelColumn(header = "증권번호", order = 2, width = 18)
private String policyNo;
@ExcelColumn(header = "설계사명", order = 3, width = 12)
private String agentName;
@ExcelColumn(header = "소속", order = 4, width = 18)
private String orgName;
@ExcelColumn(header = "보험사", order = 5, width = 14)
private String companyName;
@ExcelColumn(header = "상품", order = 6, width = 24)
private String productName;
@ExcelColumn(header = "보험종류", order = 7, width = 10, codeGroup = "INSURANCE_TYPE")
private String insuranceType;
@ExcelColumn(header = "보험료", order = 8, width = 14, format = "#,##0")
private BigDecimal premium;
@ExcelColumn(header = "회사수수료율", order = 9, width = 12, format = "0.00")
private BigDecimal companyRate;
@ExcelColumn(header = "회사수수료", order = 10, width = 14, format = "#,##0")
private BigDecimal companyAmount;
@ExcelColumn(header = "지급율", order = 11, width = 10, format = "0.00")
private BigDecimal payoutRate;
@ExcelColumn(header = "설계사수수료", order = 12, width = 14, format = "#,##0")
private BigDecimal agentAmount;
@ExcelColumn(header = "세액", order = 13, width = 12, format = "#,##0")
private BigDecimal taxAmount;
}
@@ -0,0 +1,39 @@
package com.ga.core.vo.ledger;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
public class LedgerResp {
private Long ledgerId;
private Long contractId;
private Long agentId;
private String agentName;
private String orgName;
private String policyNo;
private String contractorName;
private LocalDate contractDate;
private String companyCode;
private String companyName;
private String productCode;
private String productName;
private String insuranceType;
private String insuranceTypeName;
private Integer commissionYear;
private BigDecimal premium;
private String payMethod;
private BigDecimal companyRate;
private BigDecimal companyAmount;
private BigDecimal payoutRate;
private BigDecimal agentAmount;
private BigDecimal taxAmount;
private String reconcileStatus;
private BigDecimal reconcileDiff;
private String advanceYn;
private String chargebackYn;
private String settleMonth;
private String status;
private String statusName;
}
@@ -0,0 +1,19 @@
package com.ga.core.vo.ledger;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class LedgerSearchParam extends SearchParam {
private String settleMonth;
private Long agentId;
private Long orgId;
private String companyCode;
private String insuranceType;
private String reconcileStatus;
private String chargebackYn;
private String advanceYn;
private String policyNo;
}
@@ -0,0 +1,38 @@
package com.ga.core.vo.ledger;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* recruit_ledger / maintain_ledger 공통 VO. 테이블 구조가 동일하므로 같은 VO 사용.
*/
@Data
public class LedgerVO {
private Long ledgerId;
private Long contractId;
private Long agentId;
private String policyNo;
private String companyCode;
private String productCode;
private String insuranceType;
private Integer commissionYear;
private BigDecimal premium;
private String payMethod;
private BigDecimal companyRate;
private BigDecimal companyAmount;
private BigDecimal payoutRate;
private BigDecimal agentAmount;
private BigDecimal taxAmount;
private String reconcileStatus;
private BigDecimal reconcileDiff;
private String advanceYn;
private BigDecimal advanceAmount;
private String chargebackYn;
private String settleMonth;
private String status;
private Integer version;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,36 @@
package com.ga.core.vo.org;
import com.ga.common.annotation.ExcelColumn;
import lombok.Data;
import java.time.LocalDate;
@Data
public class AgentExcelVO {
@ExcelColumn(header = "설계사명", order = 1, width = 12, required = true)
private String agentName;
@ExcelColumn(header = "사번", order = 2, width = 14)
private String licenseNo;
@ExcelColumn(header = "소속", order = 3, width = 18)
private String orgName;
@ExcelColumn(header = "직급", order = 4, width = 10)
private String gradeName;
@ExcelColumn(header = "전화번호", order = 5, width = 16)
private String phone;
@ExcelColumn(header = "이메일", order = 6, width = 24)
private String email;
@ExcelColumn(header = "주민번호", order = 7, width = 16, masked = true)
private String residentNo;
@ExcelColumn(header = "상태", order = 8, width = 10, codeGroup = "AGENT_STATUS")
private String status;
@ExcelColumn(header = "입사일", order = 9, width = 12, format = "yyyy-MM-dd")
private LocalDate joinDate;
}
@@ -0,0 +1,21 @@
package com.ga.core.vo.org;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class AgentOrgHistoryVO {
private Long historyId;
private Long agentId;
private Long fromOrgId;
private Long toOrgId;
private Long fromGradeId;
private Long toGradeId;
private String changeType; // TRANSFER / PROMOTE / DEMOTE / JOIN / LEAVE
private LocalDate changeDate;
private String changeReason;
private LocalDateTime createdAt;
private Long createdBy;
}
@@ -0,0 +1,25 @@
package com.ga.core.vo.org;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
public class AgentResp {
private Long agentId;
private String agentName;
private String licenseNo;
private String phone; // 마스킹
private String email;
private String status;
private String statusName;
private Long orgId;
private String orgName;
private Long gradeId;
private String gradeName;
private LocalDate joinDate;
private LocalDate leaveDate;
private Integer contractCount;
private BigDecimal totalCommission;
}
@@ -0,0 +1,29 @@
package com.ga.core.vo.org;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDate;
@Data
public class AgentSaveReq {
@NotBlank(message = "설계사명은 필수입니다")
@Size(max = 50)
private String agentName;
@NotNull(message = "소속을 선택해주세요")
private Long orgId;
@NotNull(message = "직급을 선택해주세요")
private Long gradeId;
private String residentNo;
private String licenseNo;
private String phone;
private String email;
private String bankCode;
private String accountNo;
private LocalDate joinDate;
}
@@ -0,0 +1,13 @@
package com.ga.core.vo.org;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class AgentSearchParam extends SearchParam {
private Long orgId;
private Long gradeId;
private Boolean includeSubOrgs; // 하위 조직 포함 여부
}
@@ -0,0 +1,25 @@
package com.ga.core.vo.org;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class AgentVO {
private Long agentId;
private Long orgId;
private Long gradeId;
private String agentName;
private String residentNo; // EncryptTypeHandler
private String licenseNo;
private String phone;
private String email;
private String bankCode;
private String accountNo; // EncryptTypeHandler
private String status;
private LocalDate joinDate;
private LocalDate leaveDate;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,14 @@
package com.ga.core.vo.org;
import lombok.Data;
@Data
public class GradeVO {
private Long gradeId;
private String gradeName;
private Integer gradeLevel;
private String gradeGroup;
private String description;
private String isActive;
private Integer sortOrder;
}
@@ -0,0 +1,24 @@
package com.ga.core.vo.org;
import lombok.Data;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Data
public class OrganizationResp {
private Long orgId;
private Long parentOrgId;
private String parentOrgName;
private String orgName;
private String orgType;
private String orgTypeName;
private Integer orgLevel;
private String region;
private LocalDate effectiveFrom;
private LocalDate effectiveTo;
private String isActive;
private Integer agentCount;
private List<OrganizationResp> children = new ArrayList<>();
}
@@ -0,0 +1,22 @@
package com.ga.core.vo.org;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class OrganizationVO {
private Long orgId;
private Long parentOrgId;
private String orgName;
private String orgType; // HQ / BR / TM
private Integer orgLevel;
private String region;
private LocalDate effectiveFrom;
private LocalDate effectiveTo;
private String isActive;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private Long createdBy;
}
@@ -0,0 +1,33 @@
package com.ga.core.vo.product;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
public class ContractResp {
private Long contractId;
private Long agentId;
private String agentName;
private String orgName;
private Long productId;
private String productCode;
private String productName;
private String companyCode;
private String companyName;
private String insuranceType;
private String policyNo;
private String contractorName;
private String insuredName;
private BigDecimal premium;
private String payCycle;
private String payCycleName;
private Integer payPeriod;
private Integer coveragePeriod;
private String status;
private String statusName;
private LocalDate contractDate;
private LocalDate effectiveDate;
private LocalDate lapseDate;
}
@@ -0,0 +1,26 @@
package com.ga.core.vo.product;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
public class ContractSaveReq {
@NotNull private Long agentId;
@NotNull private Long productId;
@NotBlank(message = "증권번호는 필수입니다")
private String policyNo;
private String contractorName;
private String insuredName;
@NotNull @Positive
private BigDecimal premium;
private String payCycle;
private Integer payPeriod;
private Integer coveragePeriod;
@NotNull private LocalDate contractDate;
private LocalDate effectiveDate;
}
@@ -0,0 +1,15 @@
package com.ga.core.vo.product;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class ContractSearchParam extends SearchParam {
private Long agentId;
private Long productId;
private Long companyId;
private String insuranceType;
private String policyNo;
}
@@ -0,0 +1,28 @@
package com.ga.core.vo.product;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class ContractVO {
private Long contractId;
private Long agentId;
private Long productId;
private String policyNo;
private String contractorName;
private String insuredName;
private BigDecimal premium;
private String payCycle;
private Integer payPeriod;
private Integer coveragePeriod;
private String status;
private LocalDate contractDate;
private LocalDate effectiveDate;
private LocalDate lapseDate;
private LocalDate revivalDate;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,16 @@
package com.ga.core.vo.product;
import lombok.Data;
@Data
public class InsuranceCompanyVO {
private Long companyId;
private String companyCode;
private String companyName;
private String companyType;
private String bizNo;
private String contactName;
private String contactPhone;
private String contactEmail;
private String isActive;
}
@@ -0,0 +1,22 @@
package com.ga.core.vo.product;
import lombok.Data;
import java.time.LocalDate;
@Data
public class ProductResp {
private Long productId;
private Long companyId;
private String companyCode;
private String companyName;
private String productCode;
private String productName;
private String insuranceType;
private String insuranceTypeName;
private String productGroup;
private String payPeriodType;
private String isActive;
private LocalDate launchDate;
private LocalDate endDate;
}
@@ -0,0 +1,19 @@
package com.ga.core.vo.product;
import lombok.Data;
import java.time.LocalDate;
@Data
public class ProductVO {
private Long productId;
private Long companyId;
private String productCode;
private String productName;
private String insuranceType;
private String productGroup;
private String payPeriodType;
private String isActive;
private LocalDate launchDate;
private LocalDate endDate;
}
@@ -0,0 +1,15 @@
package com.ga.core.vo.receive;
import lombok.Data;
@Data
public class CodeMappingVO {
private Long codeId;
private String companyCode;
private String codeType;
private String sourceCode;
private String sourceName;
private String targetCode;
private String targetName;
private String isActive;
}
@@ -0,0 +1,22 @@
package com.ga.core.vo.receive;
import lombok.Data;
import java.time.LocalDate;
@Data
public class CompanyFieldMappingVO {
private Long mappingId;
private String companyCode;
private String sourceField;
private Integer sourceColIndex;
private String targetTable;
private String targetField;
private String dataType;
private String transformRule;
private String defaultValue;
private String isRequired;
private Integer sortOrder;
private Integer version;
private LocalDate effectiveFrom;
}
@@ -0,0 +1,22 @@
package com.ga.core.vo.receive;
import lombok.Data;
@Data
public class CompanyProfileVO {
private String companyCode;
private String dataFormat;
private String receiveMethod;
private String fileEncoding;
private String delimiter;
private Integer headerRow;
private Integer dataStartRow;
private String sheetName;
private String dateFormat;
private String amountFormat;
private String apiUrl;
private String ftpHost;
private String ftpPath;
private String receiveSchedule;
private String isActive;
}
@@ -0,0 +1,21 @@
package com.ga.core.vo.receive;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ParseErrorLogVO {
private Long errorId;
private Long rawId;
private String companyCode;
private String errorType;
private String errorField;
private String errorValue;
private String errorMessage;
private String resolveStatus; // OPEN / RESOLVED / IGNORED
private Long resolvedBy;
private String resolveNote;
private LocalDateTime resolvedAt;
private LocalDateTime createdAt;
}
@@ -0,0 +1,22 @@
package com.ga.core.vo.receive;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class RawCommissionDataVO {
private Long rawId;
private String companyCode;
private String batchId;
private String settleMonth;
private String dataType;
private String rawContent;
private String parseStatus; // PENDING / OK / ERROR
private Long mappedLedgerId;
private String mappedTable;
private String fileName;
private Integer rowNumber;
private LocalDateTime receivedAt;
private LocalDateTime parsedAt;
}
@@ -0,0 +1,21 @@
package com.ga.core.vo.rule;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
public class ChargebackRuleVO {
private Long cbRuleId;
private String companyCode;
private String insuranceType;
private Integer lapseMonthFrom;
private Integer lapseMonthTo;
private BigDecimal cbRate;
private String includeOverride;
private String installmentAllowed;
private Integer maxInstallments;
private LocalDate effectiveFrom;
private LocalDate effectiveTo;
}
@@ -0,0 +1,22 @@
package com.ga.core.vo.rule;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class CommissionRateVO {
private Long rateId;
private Long productId;
private Integer commissionYear;
private BigDecimal ratePct;
private String payMethodCond;
private BigDecimal minPremium;
private LocalDate effectiveFrom;
private LocalDate effectiveTo;
private Integer version;
private LocalDateTime createdAt;
private Long createdBy;
}
@@ -0,0 +1,16 @@
package com.ga.core.vo.rule;
import lombok.Data;
@Data
public class ExceptionTypeCodeVO {
private String exceptionCode;
private String exceptionName;
private String category;
private String direction; // PLUS / MINUS
private String autoCalcYn;
private String approvalRequired;
private String taxIncluded;
private String description;
private String isActive;
}
@@ -0,0 +1,19 @@
package com.ga.core.vo.rule;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
public class OverrideRuleVO {
private Long overrideId;
private Long fromGrade;
private Long toGrade;
private BigDecimal overridePct;
private String calcType;
private String insuranceTypeCond;
private BigDecimal capAmount;
private LocalDate effectiveFrom;
private LocalDate effectiveTo;
}
@@ -0,0 +1,23 @@
package com.ga.core.vo.rule;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class PayoutRuleVO {
private Long ruleId;
private Long gradeId;
private String insuranceType;
private Integer commissionYear;
private BigDecimal payoutPct;
private String payMethodCond;
private String performanceGrade;
private LocalDate effectiveFrom;
private LocalDate effectiveTo;
private Integer version;
private LocalDateTime createdAt;
private Long createdBy;
}
@@ -0,0 +1,24 @@
package com.ga.core.vo.settle;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class BatchJobLogVO {
private Long jobId;
private String jobName;
private String jobParams;
private String settleMonth;
private String status;
private String currentStep;
private Integer progressPct;
private Integer totalCount;
private Integer processedCount;
private Integer errorCount;
private String errorMessage;
private LocalDateTime startedAt;
private LocalDateTime finishedAt;
private Integer durationSec;
private Long triggeredBy;
}
@@ -0,0 +1,24 @@
package com.ga.core.vo.settle;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class ChargebackVO {
private Long cbId;
private Long contractId;
private Long agentId;
private String policyNo;
private Integer lapseMonth;
private BigDecimal cbRate;
private BigDecimal cbAmount;
private BigDecimal paidAmount;
private BigDecimal remainAmount;
private String settleMonth;
private Integer installmentNo;
private Integer maxInstallments;
private String status;
private LocalDateTime createdAt;
}

Some files were not shown because too many files have changed in this diff Show More