2 Commits

Author SHA1 Message Date
GA Pro 2a27aa31a0 fix: 모집정산 재실행 시 1200% clip 분급 UNIQUE 충돌 — 멱등성 보강
CalcRecruitStep(Step4)이 1200%룰 clip 초과분을 installment_plan(7년)으로 생성할 때,
재계산을 위해 recruit_ledger는 deleteBySettleMonth로 지우면서 분급은 정리하지 않아
같은 정산월을 재실행하면 동일 계약이 또 clip되어 동일 분급키(contract_id,plan_year,
plan_month)로 INSERT → UNIQUE 충돌로 Job 실패하던 버그.

수정: insertBatch 직전에 clip된 계약들의 SCHEDULED(미지급) 분급을 먼저 삭제
(InstallmentPlanMapper.deleteScheduledByContractIds). PAID 분급 이력은 보존하여
PayInstallmentStep 지급분과 충돌하지 않음.

검증: CalcRecruitStepTest 멱등 테스트(clip 시 delete→insert 순서/대상계약 검증) 추가,
전체 build SUCCESS. 라이브 E2E: 202605 정산을 연속 2회 실행 → 둘 다 COMPLETED,
분급 196건 불변(중복 0), settle_master gross 동일, 무결성 유지.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 00:09:35 +09:00
GA Pro 17ab1098ec feat: 1200%룰 이연 분급 도래월 지급(PayInstallmentStep) + 코드베이스 신입교육 주석
PayInstallmentStep(Step 4.5): 1200%룰로 이연된 분급(installment_plan SCHEDULED)을
도래월(settle_month=정산월)에 recruit_ledger로 편입해 실제 지급. 이전엔 이연만 되고
도래월 지급 절차가 없어 차액이 영구 미지급되던 갭을 메움.
- BatchConfig job flow에 step4b(calcRecruit→payInstallment→calcMaintain) 등록
- InstallmentPlanMapper.resetPaidToScheduledByMonth(+XML)로 재실행 멱등
  (진입 시 PAID→SCHEDULED 역산, Step4 deleteBySettleMonth가 원장 정리)
- RecruitLedgerMapper insert에 reconcile_status 컬럼, SettlementContext.installmentPaidCount
- 단위테스트 3건(정상지급/멱등/계약미존재) GREEN

신입교육 주석: 컨트롤러/서비스/공통/프론트 전반에 도메인·코드 설명 주석 추가
(동작 변경 없음 — 빌드/테스트/타입체크 전부 GREEN으로 무회귀 확인).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 23:40:28 +09:00
346 changed files with 4811 additions and 441 deletions
@@ -18,6 +18,15 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 회계 결산(accounting_close) 컨트롤러.
*
* <p>"결산"은 일정 기간(월/분기/연)의 회계 분개를 마감하고 계정과목별 잔액을 확정 저장하는 작업입니다.
* 흐름: DRAFT 생성(createDraft) → 마감 실행(close, 미전기 분개에 전표번호 부여 + 잔액 스냅샷 생성) → 필요 시 재오픈(reopen).
*
* <p>상태: DRAFT / REOPENED 에서만 마감 가능. 잔액 스냅샷(snapshots)은 계정과목별 차변·대변 합계입니다.
* 권한(menu=ACCOUNTING_CLOSE): 조회 READ, DRAFT생성 CREATE, 마감 EXECUTE, 재오픈 APPROVE.
*/
@Tag(name = "회계 결산")
@RestController
@RequestMapping("/api/accounting-closes")
@@ -6,6 +6,10 @@ import lombok.Data;
import java.util.List;
/**
* 분개 마감 요청 DTO.
* entryIds: 마감할 분개 ID 목록(필수, 1건 이상). journalNo: 부여할 전표번호(필수).
*/
@Data
public class CloseJournalReq {
@NotEmpty
@@ -13,6 +13,16 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 계약 회계 분개(contract_accounting_entry) 컨트롤러.
*
* <p>"분개"는 회계의 기본 기록 단위로, 거래 하나를 차변(빌린 곳)/대변(빌려준 곳) 두 계정으로 적는 것입니다.
* 여기서는 수수료 지급(payment) 건을 회계 분개로 자동 변환합니다.
* (차변: 8350 지급수수료 / 대변: 2530 미지급금)
*
* <p>generate: paymentIds 또는 settleMonth 기준으로 분개를 일괄 생성. close: 분개에 전표번호를 붙여 마감 처리.
* 권한(menu=ACCOUNTING_JOURNAL): 조회 READ, 분개생성 CREATE, 마감 UPDATE.
*/
@Tag(name = "계약 회계 분개")
@RestController
@RequestMapping("/api/accounting-entries")
@@ -4,6 +4,10 @@ import lombok.Data;
import java.util.List;
/**
* 자동 분개 생성 요청 DTO.
* paymentIds 가 있으면 해당 지급건들로, 없으면 settleMonth(정산월) 전체 지급건으로 분개를 생성합니다.
*/
@Data
public class GenerateEntriesReq {
private List<Long> paymentIds;
@@ -4,12 +4,20 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 결재 단계 진행(승인/반려) 요청 DTO.
*
* <p>결재자(approverId)가 현재 단계에 대해 action(APPROVE=승인 / REJECT=반려)을 처리할 때 전달된다.
* comment는 선택 입력이다.</p>
*/
@Data
public class AdvanceReq {
/** 결재 처리자(user_id). */
@NotNull
private Long approverId;
/** APPROVE / REJECT */
@NotBlank
private String action;
/** 결재 코멘트(선택). */
private String comment;
}
@@ -13,6 +13,17 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 전자결재 관리 API.
*
* <p>두 개념을 다룬다.
* <ul>
* <li>결재선(line): 결재 단계(step) 시퀀스의 템플릿. 각 단계는 결재 권한 역할(roleCode)로 정의된다.</li>
* <li>결재 요청(request): 특정 대상(출금/정산/이의신청/시책 등 targetType)에 대한 상신 건.
* 결재선을 따라 단계별로 진행(advance)되며, 최종 승인 시 대상 도메인에 후처리 콜백이 일어난다.</li>
* </ul>
* 메뉴 APPROVAL 권한으로 보호되며, 단계 진행만 APPROVE 권한을 요구한다.</p>
*/
@Tag(name = "결재 관리")
@RestController
@RequestMapping("/api/approvals")
@@ -23,6 +34,7 @@ public class ApprovalController {
// ── 결재선 ─────────────────────────────────────────────────────────────────
/** 결재선 목록 조회(페이징). READ 권한 필요. */
@GetMapping("/lines")
@RequirePermission(menu = "APPROVAL", perm = "READ")
public ApiResponse<PageResponse<ApprovalLineResp>> listLines(
@@ -30,18 +42,21 @@ public class ApprovalController {
return ApiResponse.ok(service.listLines(param));
}
/** 결재선 단건 상세 조회. READ 권한 필요. */
@GetMapping("/lines/{lineId}")
@RequirePermission(menu = "APPROVAL", perm = "READ")
public ApiResponse<ApprovalLineResp> detailLine(@PathVariable Long lineId) {
return ApiResponse.ok(service.detailLine(lineId));
}
/** 특정 결재선의 단계(step) 목록 조회. READ 권한 필요. */
@GetMapping("/lines/{lineId}/steps")
@RequirePermission(menu = "APPROVAL", perm = "READ")
public ApiResponse<List<ApprovalLineStepResp>> stepsOfLine(@PathVariable Long lineId) {
return ApiResponse.ok(service.stepsOfLine(lineId));
}
/** 결재선 신규 등록. CREATE 권한 필요. */
@PostMapping("/lines")
@RequirePermission(menu = "APPROVAL", perm = "CREATE")
@DataChangeLog(menu = "APPROVAL", table = "approval_line")
@@ -49,6 +64,7 @@ public class ApprovalController {
return ApiResponse.ok(service.createLine(req));
}
/** 결재선 수정. UPDATE 권한 필요. */
@PutMapping("/lines/{lineId}")
@RequirePermission(menu = "APPROVAL", perm = "UPDATE")
@DataChangeLog(menu = "APPROVAL", table = "approval_line")
@@ -58,6 +74,7 @@ public class ApprovalController {
return ApiResponse.ok();
}
/** 결재선에 단계(step) 추가. 경로의 lineId를 요청에 주입한다. UPDATE 권한 필요. */
@PostMapping("/lines/{lineId}/steps")
@RequirePermission(menu = "APPROVAL", perm = "UPDATE")
@DataChangeLog(menu = "APPROVAL", table = "approval_line_step")
@@ -70,6 +87,7 @@ public class ApprovalController {
// ── 결재 요청 ──────────────────────────────────────────────────────────────
/** 결재 요청 전체 목록 조회(페이징). READ 권한 필요. */
@GetMapping("/requests")
@RequirePermission(menu = "APPROVAL", perm = "READ")
public ApiResponse<PageResponse<ApprovalRequestResp>> listRequests(
@@ -77,6 +95,7 @@ public class ApprovalController {
return ApiResponse.ok(service.listRequests(param));
}
/** 내가 상신한 결재 요청 목록. 현재 로그인 사용자로 필터된다. READ 권한 필요. */
@GetMapping("/requests/mine")
@RequirePermission(menu = "APPROVAL", perm = "READ")
public ApiResponse<PageResponse<ApprovalRequestResp>> myRequests(
@@ -84,6 +103,7 @@ public class ApprovalController {
return ApiResponse.ok(service.myRequests(param));
}
/** 진행 중(PENDING) 결재 요청 목록. READ 권한 필요. */
@GetMapping("/requests/pending")
@RequirePermission(menu = "APPROVAL", perm = "READ")
public ApiResponse<PageResponse<ApprovalRequestResp>> pendingRequests(
@@ -91,18 +111,21 @@ public class ApprovalController {
return ApiResponse.ok(service.pendingRequests(param));
}
/** 결재 요청 단건 상세 조회. READ 권한 필요. */
@GetMapping("/requests/{requestId}")
@RequirePermission(menu = "APPROVAL", perm = "READ")
public ApiResponse<ApprovalRequestResp> detailRequest(@PathVariable Long requestId) {
return ApiResponse.ok(service.detailRequest(requestId));
}
/** 결재 요청의 단계별 처리 이력 조회. READ 권한 필요. */
@GetMapping("/requests/{requestId}/history")
@RequirePermission(menu = "APPROVAL", perm = "READ")
public ApiResponse<List<ApprovalHistoryResp>> historyOfRequest(@PathVariable Long requestId) {
return ApiResponse.ok(service.historyOfRequest(requestId));
}
/** 결재 상신(요청 생성). PENDING 상태로 시작하며 1단계 결재자에게 알림이 발송된다. CREATE 권한 필요. */
@PostMapping("/requests")
@RequirePermission(menu = "APPROVAL", perm = "CREATE")
@DataChangeLog(menu = "APPROVAL", table = "approval_request")
@@ -110,6 +133,7 @@ public class ApprovalController {
return ApiResponse.ok(service.createRequest(req));
}
/** 결재 단계 진행(승인/반려). 마지막 단계 승인 시 대상 도메인에 후처리 콜백이 일어난다. APPROVE 권한 필요. */
@PostMapping("/requests/{requestId}/advance")
@RequirePermission(menu = "APPROVAL", perm = "APPROVE")
@DataChangeLog(menu = "APPROVAL", table = "approval_request")
@@ -119,6 +143,7 @@ public class ApprovalController {
return ApiResponse.ok();
}
/** 결재 요청 취소. 진행 중(PENDING)인 건만 취소 가능. UPDATE 권한 필요. */
@PostMapping("/requests/{requestId}/cancel")
@RequirePermission(menu = "APPROVAL", perm = "UPDATE")
@DataChangeLog(menu = "APPROVAL", table = "approval_request")
@@ -23,6 +23,15 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
/**
* 첨부파일 관리 API.
*
* <p>각종 도메인(refType + refId 조합)에 파일을 첨부·다운로드·삭제한다.
* refType은 "어떤 종류의 대상인지"(예: 계약/민원 등), refId는 그 대상의 PK다.
* 파일은 서버 디스크에 저장되고, 메타 정보(파일명/경로/크기/MIME)는 DB로 관리된다.</p>
*
* <p>메뉴 ATTACHMENT 권한으로 보호된다.</p>
*/
@Tag(name = "첨부파일")
@RestController
@RequestMapping("/api/attachments")
@@ -31,12 +40,14 @@ public class AttachmentController {
private final AttachmentService service;
/** 첨부파일 메타 목록 조회(페이징). READ 권한 필요. */
@GetMapping
@RequirePermission(menu = "ATTACHMENT", perm = "READ")
public ApiResponse<PageResponse<AttachmentResp>> list(@ModelAttribute AttachmentSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 특정 대상(refType+refId)에 달린 첨부파일 목록 조회. READ 권한 필요. */
@GetMapping("/by-ref")
@RequirePermission(menu = "ATTACHMENT", perm = "READ")
public ApiResponse<List<AttachmentResp>> listByRef(@RequestParam String refType,
@@ -44,12 +55,14 @@ public class AttachmentController {
return ApiResponse.ok(service.listByRef(refType, refId));
}
/** 첨부파일 메타 단건 조회. READ 권한 필요. */
@GetMapping("/{attachmentId}/meta")
@RequirePermission(menu = "ATTACHMENT", perm = "READ")
public ApiResponse<AttachmentResp> detail(@PathVariable Long attachmentId) {
return ApiResponse.ok(service.detail(attachmentId));
}
/** 파일 업로드(multipart). 대상(refType+refId)에 연결해 저장한다. CREATE 권한 필요. */
@PostMapping(consumes = "multipart/form-data")
@RequirePermission(menu = "ATTACHMENT", perm = "CREATE")
@DataChangeLog(menu = "ATTACHMENT", table = "attachment")
@@ -59,6 +72,7 @@ public class AttachmentController {
return ApiResponse.ok(service.upload(file, refType, refId));
}
/** 파일 다운로드. 메타의 MIME/파일명으로 응답 헤더를 구성하고 파일 스트림을 내려준다. READ 권한 필요. */
@GetMapping("/{attachmentId}/download")
@RequirePermission(menu = "ATTACHMENT", perm = "READ")
public void download(@PathVariable Long attachmentId, HttpServletResponse response) throws IOException {
@@ -75,6 +89,7 @@ public class AttachmentController {
}
}
/** 첨부파일 삭제(메타 + 실제 파일). DELETE 권한 필요. */
@DeleteMapping("/{attachmentId}")
@RequirePermission(menu = "ATTACHMENT", perm = "DELETE")
@DataChangeLog(menu = "ATTACHMENT", table = "attachment")
@@ -50,12 +50,14 @@ public class BatchTriggerController {
return ApiResponse.ok(log.getJobId());
}
/** 특정 배치 잡의 최신 실행 상태 조회(기본 월정산). READ 권한 필요. */
@GetMapping("/status")
@RequirePermission(menu = "BATCH_RUN", perm = "READ")
public ApiResponse<BatchJobLogVO> latest(@RequestParam(defaultValue = "MONTHLY_SETTLEMENT") String jobName) {
return ApiResponse.ok(jobLogMapper.selectLatest(jobName));
}
/** 배치 실행 이력 목록 조회(잡명/정산월 필터). READ 권한 필요. */
@GetMapping("/jobs")
@RequirePermission(menu = "BATCH_RUN", perm = "READ")
public ApiResponse<List<BatchJobLogVO>> list(@RequestParam(required = false) String jobName,
@@ -63,6 +65,7 @@ public class BatchTriggerController {
return ApiResponse.ok(jobLogMapper.selectList(jobName, settleMonth));
}
/** 배치 실행 이력 단건 상세 조회. READ 권한 필요. */
@GetMapping("/jobs/{jobId}")
@RequirePermission(menu = "BATCH_RUN", perm = "READ")
public ApiResponse<BatchJobLogVO> detail(@PathVariable Long jobId) {
@@ -7,18 +7,27 @@ import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 차등환수 금액 계산(시뮬레이션) 요청 DTO.
* 차등환수란 고객이 계약을 조기 해지하면 이미 지급한 수수료의 일부를 경과기간에 따라 차등 회수하는 것이다.
* (계약일~해지일 경과개월수가 짧을수록 환수율이 높다)
*/
@Data
public class ChargebackCalculateReq {
/** 계약 체결일. 경과개월수 계산의 시작점. */
@NotNull
private LocalDate contractDate;
/** 해지일. 계약일과의 차이로 경과개월수를 구한다. */
@NotNull
private LocalDate terminationDate;
/** 당초 지급했던 수수료 원금(양수). 여기에 환수율을 곱해 환수액을 산출한다. */
@NotNull
@Positive
private BigDecimal originalCommission;
/** 상품 분류. 분류별로 적용 환수율 구간(chargeback_grade)이 다르다. */
private String productCategory;
}
@@ -15,6 +15,12 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 차등환수 등급(chargeback_grade) 관리 REST 컨트롤러.
* 환수 등급표는 "경과개월 구간별 환수율(%)"을 정의하는 기준표이며, 조기해지 시 환수액 계산에 쓰인다.
* - gradeService : 환수 등급표 CRUD + 비활성화
* - calculator : 실제 계약 정보로 환수액을 계산(시뮬레이션)
*/
@Tag(name = "환수 등급 관리")
@RestController
@RequestMapping("/api/chargeback-grades")
@@ -24,18 +30,21 @@ public class ChargebackGradeController {
private final ChargebackGradeService gradeService;
private final ChargebackCalculator calculator;
/** 환수 등급표 목록 페이징 조회. */
@GetMapping
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "READ")
public ApiResponse<PageResponse<ChargebackGradeResp>> list(ChargebackGradeSearchParam param) {
return ApiResponse.ok(gradeService.list(param));
}
/** 환수 등급 단건 상세 조회. */
@GetMapping("/{gradeId}")
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "READ")
public ApiResponse<ChargebackGradeResp> detail(@PathVariable Long gradeId) {
return ApiResponse.ok(gradeService.detail(gradeId));
}
/** 환수 등급 신규 등록. */
@PostMapping
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "CREATE")
@DataChangeLog(menu = "CHARGEBACK_GRADE", table = "chargeback_grade")
@@ -44,6 +53,7 @@ public class ChargebackGradeController {
return ApiResponse.ok();
}
/** 환수 등급 수정. */
@PutMapping("/{gradeId}")
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "UPDATE")
@DataChangeLog(menu = "CHARGEBACK_GRADE", table = "chargeback_grade")
@@ -53,6 +63,7 @@ public class ChargebackGradeController {
return ApiResponse.ok();
}
/** 환수 등급 비활성화(사용 중지). 비활성 등급은 환수액 계산에서 제외된다. */
@PutMapping("/{gradeId}/deactivate")
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "UPDATE")
@DataChangeLog(menu = "CHARGEBACK_GRADE", table = "chargeback_grade")
@@ -61,6 +72,7 @@ public class ChargebackGradeController {
return ApiResponse.ok();
}
/** 계약일·해지일·원수수료를 입력받아 환수액을 계산(시뮬레이션)한다. */
@PostMapping("/calculate")
@RequirePermission(menu = "CHARGEBACK_GRADE", perm = "READ")
public ApiResponse<ChargebackResult> calculate(@Valid @RequestBody ChargebackCalculateReq req) {
@@ -13,6 +13,16 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 선지급정산 API.
*
* 선지급(advance payment): 아직 확정되지 않은 미래 수수료를 설계사에게 미리 지급(가불)하는 것.
* 이후 실제 수수료가 발생하면 그 금액에서 차감(상계)하여 회수한다.
* - principalAmount(선지급 원금) / recoveredAmount(회수 누계) / balance(미상환 잔액)
* - status: OUTSTANDING(미상환) → PARTIAL(일부회수) → CLEARED(완전회수) 또는 WAIVED(면제)
*
* 권한: @RequirePermission(menu="ADVANCE_PAYMENT"), perm=READ/CREATE/UPDATE. 변경은 @DataChangeLog 로 감사 기록.
*/
@Tag(name = "선지급정산")
@RestController
@RequestMapping("/api/advance-payments")
@@ -21,6 +31,7 @@ public class AdvancePaymentController {
private final AdvancePaymentService service;
/** 선지급 목록 조회(검색 + 페이징). 권한: ADVANCE_PAYMENT READ. */
@GetMapping
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "READ")
public ApiResponse<PageResponse<AdvancePaymentResp>> list(
@@ -28,12 +39,14 @@ public class AdvancePaymentController {
return ApiResponse.ok(service.list(param));
}
/** 선지급 단건 상세 조회. 권한: ADVANCE_PAYMENT READ. */
@GetMapping("/{advanceId}")
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "READ")
public ApiResponse<AdvancePaymentResp> detail(@PathVariable Long advanceId) {
return ApiResponse.ok(service.detail(advanceId));
}
/** 선지급 신규 등록. 등록 시 잔액=원금, 상태=OUTSTANDING. 응답: 생성된 advanceId. 권한: ADVANCE_PAYMENT CREATE. */
@PostMapping
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "CREATE")
@DataChangeLog(menu = "ADVANCE_PAYMENT", table = "advance_payment")
@@ -41,6 +54,7 @@ public class AdvancePaymentController {
return ApiResponse.ok(service.create(req));
}
/** 선지급 면제 처리. 남은 잔액 회수를 포기(상태=WAIVED, 잔액=0). 권한: ADVANCE_PAYMENT UPDATE. */
@PostMapping("/{advanceId}/waive")
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "UPDATE")
@DataChangeLog(menu = "ADVANCE_PAYMENT", table = "advance_payment")
@@ -7,14 +7,19 @@ import lombok.Data;
import java.math.BigDecimal;
/**
* 시상 원장 환수 처리 요청 (PUT /api/awards/ledgers/{id}/clawback).
* 시상(award) 원장 환수 처리 요청 바디 (PUT /api/awards/ledgers/{id}/clawback).
*
* 환수(clawback): 이미 지급한 시상금을 일정 사유로 되돌려 받는 것.
* clawbackAmount(환수 금액, 0 이상 필수) + reason(환수 사유).
*/
@Data
public class AwardClawbackReq {
/** 환수 금액. 0 이상이어야 하며(@DecimalMin("0")) 필수값(@NotNull). */
@NotNull
@DecimalMin("0")
private BigDecimal clawbackAmount;
/** 환수 사유(선택). */
private String reason;
}
@@ -11,6 +11,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 시상(award) API.
*
* 시상: 일정 실적 조건을 달성한 설계사에게 지급하는 포상금/인센티브.
* - Rule(시상 규칙): 어떤 조건일 때 얼마를 지급하는지 정의(생성/수정/삭제 가능).
* - Ledger(시상 원장): 규칙에 따라 실제 산출/지급된 시상금 내역. clawback(환수)로 회수 가능.
*
* 권한: @RequirePermission(menu="AWARD"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 로 감사 기록.
*/
@Tag(name = "시상")
@RestController
@RequestMapping("/api/awards")
@@ -19,8 +28,9 @@ public class AwardController {
private final AwardService service;
// ── Rule ──────────────────────────────────────────────────────────────────
// ── Rule (시상 규칙) ────────────────────────────────────────────────────
/** 시상 규칙 목록 조회. 권한: AWARD READ. */
@GetMapping("/rules")
@RequirePermission(menu = "AWARD", perm = "READ")
public ApiResponse<PageResponse<AwardRuleResp>> listRules(
@@ -28,12 +38,14 @@ public class AwardController {
return ApiResponse.ok(service.listRules(param));
}
/** 시상 규칙 단건 상세. 권한: AWARD READ. */
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "AWARD", perm = "READ")
public ApiResponse<AwardRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
/** 시상 규칙 신규 등록. 응답: 생성된 ruleId. 권한: AWARD CREATE. */
@PostMapping("/rules")
@RequirePermission(menu = "AWARD", perm = "CREATE")
@DataChangeLog(menu = "AWARD", table = "award_rule")
@@ -41,6 +53,7 @@ public class AwardController {
return ApiResponse.ok(service.createRule(req));
}
/** 시상 규칙 수정. 권한: AWARD UPDATE. */
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "AWARD", perm = "UPDATE")
@DataChangeLog(menu = "AWARD", table = "award_rule")
@@ -50,6 +63,7 @@ public class AwardController {
return ApiResponse.ok();
}
/** 시상 규칙 삭제. 권한: AWARD DELETE. */
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "AWARD", perm = "DELETE")
@DataChangeLog(menu = "AWARD", table = "award_rule")
@@ -58,8 +72,9 @@ public class AwardController {
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
// ── Ledger (시상 원장) ──────────────────────────────────────────────────
/** 시상 원장 목록 조회. 권한: AWARD READ. */
@GetMapping("/ledgers")
@RequirePermission(menu = "AWARD", perm = "READ")
public ApiResponse<PageResponse<AwardLedgerResp>> listLedgers(
@@ -67,12 +82,14 @@ public class AwardController {
return ApiResponse.ok(service.listLedgers(param));
}
/** 시상 원장 단건 상세. 권한: AWARD READ. */
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "AWARD", perm = "READ")
public ApiResponse<AwardLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
/** 시상 원장 신규 등록(시상금 산출/지급 기록). 응답: 생성된 ledgerId. 권한: AWARD CREATE. */
@PostMapping("/ledgers")
@RequirePermission(menu = "AWARD", perm = "CREATE")
@DataChangeLog(menu = "AWARD", table = "award_ledger")
@@ -80,6 +97,7 @@ public class AwardController {
return ApiResponse.ok(service.createLedger(req));
}
/** 시상금 환수(clawback) 적용. 이미 지급한 시상금을 일부/전부 회수한다. 권한: AWARD UPDATE. */
@PutMapping("/ledgers/{ledgerId}/clawback")
@RequirePermission(menu = "AWARD", perm = "UPDATE")
@DataChangeLog(menu = "AWARD", table = "award_ledger")
@@ -11,6 +11,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 지점장수당 API.
*
* 지점장수당(branch manager allowance): 지점에 소속된 설계사들의 실적 합계에 따라
* 지점장에게 추가로 지급하는 수당. 상한(cap)이 적용될 수 있다.
* - Rule(수당 규칙): 지점 실적 대비 지급률/상한 정의. - Ledger(수당 원장): 실제 산출/지급 내역.
*
* 권한: @RequirePermission(menu="BRANCH_MGR_ALLOWANCE"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "지점장수당")
@RestController
@RequestMapping("/api/branch-allowances")
@@ -19,8 +28,9 @@ public class BranchAllowanceController {
private final BranchAllowanceService service;
// ── Rule ──────────────────────────────────────────────────────────────────
// ── Rule (수당 규칙) ────────────────────────────────────────────────────
/** 지점장수당 규칙 목록 조회. 권한: BRANCH_MGR_ALLOWANCE READ. */
@GetMapping("/rules")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ")
public ApiResponse<PageResponse<BranchMgrAllowanceRuleResp>> listRules(
@@ -34,6 +44,7 @@ public class BranchAllowanceController {
return ApiResponse.ok(service.detailRule(ruleId));
}
/** 지점장수당 규칙 신규 등록. 응답: ruleId. 권한: BRANCH_MGR_ALLOWANCE CREATE. */
@PostMapping("/rules")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "CREATE")
@DataChangeLog(menu = "BRANCH_MGR_ALLOWANCE", table = "branch_mgr_allowance_rule")
@@ -41,6 +52,7 @@ public class BranchAllowanceController {
return ApiResponse.ok(service.createRule(req));
}
/** 지점장수당 규칙 수정. 권한: BRANCH_MGR_ALLOWANCE UPDATE. */
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "UPDATE")
@DataChangeLog(menu = "BRANCH_MGR_ALLOWANCE", table = "branch_mgr_allowance_rule")
@@ -50,6 +62,7 @@ public class BranchAllowanceController {
return ApiResponse.ok();
}
/** 지점장수당 규칙 삭제. 권한: BRANCH_MGR_ALLOWANCE DELETE. */
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "DELETE")
@DataChangeLog(menu = "BRANCH_MGR_ALLOWANCE", table = "branch_mgr_allowance_rule")
@@ -58,8 +71,9 @@ public class BranchAllowanceController {
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
// ── Ledger (수당 원장) ──────────────────────────────────────────────────
/** 지점장수당 원장 목록 조회. 권한: BRANCH_MGR_ALLOWANCE READ. */
@GetMapping("/ledgers")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ")
public ApiResponse<PageResponse<BranchMgrAllowanceLedgerResp>> listLedgers(
@@ -67,12 +81,14 @@ public class BranchAllowanceController {
return ApiResponse.ok(service.listLedgers(param));
}
/** 지점장수당 원장 단건 상세. 권한: BRANCH_MGR_ALLOWANCE READ. */
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ")
public ApiResponse<BranchMgrAllowanceLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
/** 지점장수당 원장 신규 등록(수당 산출/지급 기록). 응답: ledgerId. 권한: BRANCH_MGR_ALLOWANCE CREATE. */
@PostMapping("/ledgers")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "CREATE")
@DataChangeLog(menu = "BRANCH_MGR_ALLOWANCE", table = "branch_mgr_allowance_ledger")
@@ -13,6 +13,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 정산대사(입수) API.
*
* 정산대사(reconciliation): 보험사로부터 받은 정산 명세(입수 자료)와 우리 시스템이 계산한 금액을
* 서로 맞춰 보아(대사) 차이를 확인/확정하는 작업.
* - reconcile: 대사 실행(차이 비교) - approve: 대사 결과 승인(확정)
*
* 권한: @RequirePermission(menu="CARRIER_RECON_IN"), perm=READ/CREATE/UPDATE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "정산대사입수")
@RestController
@RequestMapping("/api/carrier-recon-in")
@@ -21,6 +30,7 @@ public class CarrierReconciliationInController {
private final CarrierReconciliationInService service;
/** 대사 입수 자료 목록 조회. 권한: CARRIER_RECON_IN READ. */
@GetMapping
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "READ")
public ApiResponse<PageResponse<CarrierReconciliationInResp>> list(
@@ -28,12 +38,14 @@ public class CarrierReconciliationInController {
return ApiResponse.ok(service.list(param));
}
/** 대사 입수 단건 상세. 권한: CARRIER_RECON_IN READ. */
@GetMapping("/{reconId}")
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "READ")
public ApiResponse<CarrierReconciliationInResp> detail(@PathVariable Long reconId) {
return ApiResponse.ok(service.detail(reconId));
}
/** 대사 입수 자료 신규 등록. 응답: reconId. 권한: CARRIER_RECON_IN CREATE. */
@PostMapping
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "CREATE")
@DataChangeLog(menu = "CARRIER_RECON_IN", table = "carrier_reconciliation_in")
@@ -41,6 +53,7 @@ public class CarrierReconciliationInController {
return ApiResponse.ok(service.create(req));
}
/** 대사 실행. 보험사 명세와 시스템 계산액을 비교해 차이를 계산한다. 권한: CARRIER_RECON_IN UPDATE. */
@PostMapping("/{reconId}/reconcile")
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "UPDATE")
@DataChangeLog(menu = "CARRIER_RECON_IN", table = "carrier_reconciliation_in")
@@ -49,6 +62,7 @@ public class CarrierReconciliationInController {
return ApiResponse.ok();
}
/** 대사 결과 승인(확정). 권한: CARRIER_RECON_IN UPDATE. */
@PostMapping("/{reconId}/approve")
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "UPDATE")
@DataChangeLog(menu = "CARRIER_RECON_IN", table = "carrier_reconciliation_in")
@@ -13,6 +13,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 환수감면신청 API.
*
* 환수(chargeback): 계약 해지/실효 등으로 이미 지급한 수수료를 설계사로부터 다시 회수하는 것.
* 환수감면(waiver): 특별한 사유가 인정되면 그 환수 금액을 줄이거나 면제해 달라고 신청하는 것.
* - approve: 감면 신청 승인 - reject: 감면 신청 반려
*
* 권한: @RequirePermission(menu="CHARGEBACK_WAIVER"), perm=READ/CREATE/UPDATE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "환수감면신청")
@RestController
@RequestMapping("/api/chargeback-waivers")
@@ -21,6 +30,7 @@ public class ChargebackWaiverController {
private final ChargebackWaiverService service;
/** 환수감면 신청 목록 조회. 권한: CHARGEBACK_WAIVER READ. */
@GetMapping
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "READ")
public ApiResponse<PageResponse<ChargebackWaiverRequestResp>> list(
@@ -28,12 +38,14 @@ public class ChargebackWaiverController {
return ApiResponse.ok(service.list(param));
}
/** 환수감면 신청 단건 상세. 권한: CHARGEBACK_WAIVER READ. */
@GetMapping("/{requestId}")
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "READ")
public ApiResponse<ChargebackWaiverRequestResp> detail(@PathVariable Long requestId) {
return ApiResponse.ok(service.detail(requestId));
}
/** 환수감면 신청 등록. 응답: requestId. 권한: CHARGEBACK_WAIVER CREATE. */
@PostMapping
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "CREATE")
@DataChangeLog(menu = "CHARGEBACK_WAIVER", table = "chargeback_waiver_request")
@@ -41,6 +53,7 @@ public class ChargebackWaiverController {
return ApiResponse.ok(service.create(req));
}
/** 환수감면 신청 승인. 권한: CHARGEBACK_WAIVER UPDATE. */
@PostMapping("/{requestId}/approve")
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "UPDATE")
@DataChangeLog(menu = "CHARGEBACK_WAIVER", table = "chargeback_waiver_request")
@@ -49,6 +62,7 @@ public class ChargebackWaiverController {
return ApiResponse.ok();
}
/** 환수감면 신청 반려. 권한: CHARGEBACK_WAIVER UPDATE. */
@PostMapping("/{requestId}/reject")
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "UPDATE")
@DataChangeLog(menu = "CHARGEBACK_WAIVER", table = "chargeback_waiver_request")
@@ -15,6 +15,15 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 환수분할상환 API.
*
* 환수채권(받아야 할 환수 금액)을 한 번에 못 갚을 때, 여러 달에 나눠 갚도록(분할상환) 계획을 만든다.
* - generate: 환수채권(receivableId) 하나를 N개월(count)로 나눠 baseMonth 부터 매월 상환 계획을 생성
* - cancel: 특정 회차 계획 취소
*
* 권한: @RequirePermission(menu="CLAWBACK_INSTALLMENT"), perm=READ/CREATE/UPDATE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "환수분할상환")
@RestController
@RequestMapping("/api/clawback-installments")
@@ -23,6 +32,7 @@ public class ClawbackInstallmentController {
private final ClawbackInstallmentService service;
/** 분할상환 계획 목록 조회. 권한: CLAWBACK_INSTALLMENT READ. */
@GetMapping
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "READ")
public ApiResponse<PageResponse<ClawbackInstallmentPlanResp>> list(
@@ -30,12 +40,14 @@ public class ClawbackInstallmentController {
return ApiResponse.ok(service.list(param));
}
/** 분할상환 계획 단건 상세. 권한: CLAWBACK_INSTALLMENT READ. */
@GetMapping("/{planId}")
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "READ")
public ApiResponse<ClawbackInstallmentPlanResp> detail(@PathVariable Long planId) {
return ApiResponse.ok(service.detail(planId));
}
/** 특정 환수채권에 묶인 분할상환 회차 전체 조회. 권한: CLAWBACK_INSTALLMENT READ. */
@GetMapping("/by-receivable/{receivableId}")
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "READ")
public ApiResponse<List<ClawbackInstallmentPlanVO>> listByReceivable(
@@ -44,20 +56,23 @@ public class ClawbackInstallmentController {
}
/**
* 분할상환 계획 생성.
* Request body: { "count": N, "baseMonth": "YYYYMM" }
* 분할상환 계획 생성. 환수채권을 N개월로 균등 분할해 baseMonth 부터 회차를 만든다.
* Request body: { "count": N(분할 개월 수), "baseMonth": "YYYYMM"(첫 회차 월) }
* 권한: CLAWBACK_INSTALLMENT CREATE.
*/
@PostMapping("/generate/{receivableId}")
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "CREATE")
@DataChangeLog(menu = "CLAWBACK_INSTALLMENT", table = "clawback_installment_plan")
public ApiResponse<Void> generate(@PathVariable Long receivableId,
@RequestBody Map<String, Object> body) {
// body 의 count/baseMonth 를 꺼내 서비스로 전달 (JSON 값은 문자열로 변환 후 파싱)
int count = Integer.parseInt(body.get("count").toString());
String baseMonth = body.get("baseMonth").toString();
service.generate(receivableId, count, baseMonth);
return ApiResponse.ok();
}
/** 분할상환 회차 취소. 권한: CLAWBACK_INSTALLMENT UPDATE. */
@PostMapping("/{planId}/cancel")
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "UPDATE")
@DataChangeLog(menu = "CLAWBACK_INSTALLMENT", table = "clawback_installment_plan")
@@ -18,6 +18,16 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 환수채권 관리 API.
*
* 환수채권(clawback receivable): 환수가 확정되어 설계사로부터 "받아야 할 돈"(미수금).
* 발생액(original) 중 회수액(recovered)을 빼면 잔액(balance)이 남는다.
* - write-off(대손상각): 회수가 불가능하다고 판단된 잔액을 손실로 털어내는 처리.
* - summary: 상태/설계사별 집계, export: 엑셀 다운로드.
*
* 권한: @RequirePermission(menu="CLAWBACK_RECEIVABLE"), perm=READ/UPDATE/EXPORT. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "환수채권 관리")
@RestController
@RequestMapping("/api/clawback-receivables")
@@ -26,6 +36,7 @@ public class ClawbackReceivableController {
private final ClawbackReceivableService service;
/** 환수채권 목록 조회(검색 + 페이징). 권한: CLAWBACK_RECEIVABLE READ. */
@GetMapping
@RequirePermission(menu = "CLAWBACK_RECEIVABLE", perm = "READ")
public ApiResponse<PageResponse<ClawbackReceivableResp>> list(
@@ -33,6 +44,7 @@ public class ClawbackReceivableController {
return ApiResponse.ok(service.list(param));
}
/** 환수채권 집계 요약(상태/그룹별 합계). 권한: CLAWBACK_RECEIVABLE READ. */
@GetMapping("/summary")
@RequirePermission(menu = "CLAWBACK_RECEIVABLE", perm = "READ")
public ApiResponse<List<Map<String, Object>>> summary(
@@ -40,12 +52,14 @@ public class ClawbackReceivableController {
return ApiResponse.ok(service.summary(param));
}
/** 환수채권 단건 상세. 권한: CLAWBACK_RECEIVABLE READ. */
@GetMapping("/{id}")
@RequirePermission(menu = "CLAWBACK_RECEIVABLE", perm = "READ")
public ApiResponse<ClawbackReceivableResp> detail(@PathVariable Long id) {
return ApiResponse.ok(service.detail(id));
}
/** 대손상각(write-off) 처리. 잔액을 손실 처리한다. 처리자(현재 로그인 사용자 ID)를 함께 기록. 권한: CLAWBACK_RECEIVABLE UPDATE. */
@PutMapping("/{id}/write-off")
@RequirePermission(menu = "CLAWBACK_RECEIVABLE", perm = "UPDATE")
@DataChangeLog(menu = "CLAWBACK_RECEIVABLE", table = "clawback_receivable")
@@ -55,6 +69,7 @@ public class ClawbackReceivableController {
return ApiResponse.ok();
}
/** 환수채권 목록 엑셀 다운로드. 응답 스트림(response)에 직접 파일을 쓴다. 권한: CLAWBACK_RECEIVABLE EXPORT. */
@GetMapping("/export")
@RequirePermission(menu = "CLAWBACK_RECEIVABLE", perm = "EXPORT")
public void export(@ModelAttribute ClawbackReceivableSearchParam param,
@@ -11,6 +11,14 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 수금수수료 API.
*
* 수금수수료(collection commission): 설계사가 보험료를 정상적으로 수금(납입 유도/관리)한 것에 대해
* 지급하는 수수료. 규칙(Rule)에 따라 산출되어 원장(Ledger)에 기록된다.
*
* 권한: @RequirePermission(menu="COLLECT_COMM"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "수금수수료")
@RestController
@RequestMapping("/api/collection-commissions")
@@ -19,8 +27,9 @@ public class CollectionCommissionController {
private final CollectionCommissionService service;
// ── Rule ──────────────────────────────────────────────────────────────────
// ── Rule (수금수수료 규칙) ──────────────────────────────────────────────
/** 수금수수료 규칙 목록 조회. 권한: COLLECT_COMM READ. */
@GetMapping("/rules")
@RequirePermission(menu = "COLLECT_COMM", perm = "READ")
public ApiResponse<PageResponse<CollectionCommissionRuleResp>> listRules(
@@ -28,12 +37,14 @@ public class CollectionCommissionController {
return ApiResponse.ok(service.listRules(param));
}
/** 수금수수료 규칙 단건 상세. 권한: COLLECT_COMM READ. */
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "COLLECT_COMM", perm = "READ")
public ApiResponse<CollectionCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
/** 수금수수료 규칙 신규 등록. 응답: ruleId. 권한: COLLECT_COMM CREATE. */
@PostMapping("/rules")
@RequirePermission(menu = "COLLECT_COMM", perm = "CREATE")
@DataChangeLog(menu = "COLLECT_COMM", table = "collection_commission_rule")
@@ -41,6 +52,7 @@ public class CollectionCommissionController {
return ApiResponse.ok(service.createRule(req));
}
/** 수금수수료 규칙 수정. 권한: COLLECT_COMM UPDATE. */
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "COLLECT_COMM", perm = "UPDATE")
@DataChangeLog(menu = "COLLECT_COMM", table = "collection_commission_rule")
@@ -50,6 +62,7 @@ public class CollectionCommissionController {
return ApiResponse.ok();
}
/** 수금수수료 규칙 삭제. 권한: COLLECT_COMM DELETE. */
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "COLLECT_COMM", perm = "DELETE")
@DataChangeLog(menu = "COLLECT_COMM", table = "collection_commission_rule")
@@ -58,8 +71,9 @@ public class CollectionCommissionController {
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
// ── Ledger (수금수수료 원장) ────────────────────────────────────────────
/** 수금수수료 원장 목록 조회. 권한: COLLECT_COMM READ. */
@GetMapping("/ledgers")
@RequirePermission(menu = "COLLECT_COMM", perm = "READ")
public ApiResponse<PageResponse<CollectionCommissionLedgerResp>> listLedgers(
@@ -67,12 +81,14 @@ public class CollectionCommissionController {
return ApiResponse.ok(service.listLedgers(param));
}
/** 수금수수료 원장 단건 상세. 권한: COLLECT_COMM READ. */
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "COLLECT_COMM", perm = "READ")
public ApiResponse<CollectionCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
/** 수금수수료 원장 신규 등록. 응답: ledgerId. 권한: COLLECT_COMM CREATE. */
@PostMapping("/ledgers")
@RequirePermission(menu = "COLLECT_COMM", perm = "CREATE")
@DataChangeLog(menu = "COLLECT_COMM", table = "collection_commission_ledger")
@@ -14,6 +14,15 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 수수료 마스터 API.
*
* 수수료 마스터(commission master): 설계사·정산월 단위로 각종 수수료(도입/소개/유지/시상 등)를
* 한데 모아 최종 지급액을 관리하는 핵심 원장. 여러 세부 원장이 이 마스터로 집계된다.
* - by-agent: 설계사+정산월의 수수료들, aggregated: 정산월 전체 집계, status: 정산 상태 변경(확정/지급 등).
*
* 권한: @RequirePermission(menu="COMM_MASTER"), perm=READ/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "수수료 마스터")
@RestController
@RequestMapping("/api/commissions")
@@ -22,18 +31,21 @@ public class CommissionMasterController {
private final CommissionMasterService service;
/** 수수료 마스터 목록 조회(검색 + 페이징). 권한: COMM_MASTER READ. */
@GetMapping
@RequirePermission(menu = "COMM_MASTER", perm = "READ")
public ApiResponse<PageResponse<CommissionMasterResp>> list(@ModelAttribute CommissionMasterSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 수수료 마스터 단건 상세. 권한: COMM_MASTER READ. */
@GetMapping("/{masterId}")
@RequirePermission(menu = "COMM_MASTER", perm = "READ")
public ApiResponse<CommissionMasterResp> detail(@PathVariable Long masterId) {
return ApiResponse.ok(service.detail(masterId));
}
/** 특정 설계사(agentId)의 특정 정산월(settleMonth) 수수료 내역 조회. 권한: COMM_MASTER READ. */
@GetMapping("/by-agent")
@RequirePermission(menu = "COMM_MASTER", perm = "READ")
public ApiResponse<List<CommissionMasterResp>> byAgent(@RequestParam Long agentId,
@@ -41,12 +53,14 @@ public class CommissionMasterController {
return ApiResponse.ok(service.listByAgentAndMonth(agentId, settleMonth));
}
/** 정산월 단위 전체 집계 조회. 권한: COMM_MASTER READ. */
@GetMapping("/aggregated")
@RequirePermission(menu = "COMM_MASTER", perm = "READ")
public ApiResponse<List<CommissionMasterResp>> aggregated(@RequestParam String settleMonth) {
return ApiResponse.ok(service.aggregatedByMonth(settleMonth));
}
/** 정산 상태 변경(예: 확정/지급 등). body의 "status" 값으로 갱신. 권한: COMM_MASTER UPDATE. */
@PutMapping("/{masterId}/status")
@RequirePermission(menu = "COMM_MASTER", perm = "UPDATE")
@DataChangeLog(menu = "COMM_MASTER", table = "commission_master")
@@ -56,6 +70,7 @@ public class CommissionMasterController {
return ApiResponse.ok();
}
/** 수수료 마스터 삭제. 권한: COMM_MASTER DELETE. */
@DeleteMapping("/{masterId}")
@RequirePermission(menu = "COMM_MASTER", perm = "DELETE")
@DataChangeLog(menu = "COMM_MASTER", table = "commission_master")
@@ -13,6 +13,14 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 수수료 시뮬레이터 API.
*
* 시뮬레이션(simulation): 실제 정산을 확정하기 전에 "이 조건이면 수수료가 얼마 나올까"를 미리 계산해 보는 기능.
* 결과는 저장되어 이력으로 조회할 수 있다(실제 지급에는 영향 없음).
*
* 권한: @RequirePermission(menu="COMM_SIMULATOR"), perm=READ/CREATE.
*/
@Tag(name = "수수료 시뮬레이터")
@RestController
@RequestMapping("/api/commission-simulations")
@@ -21,6 +29,7 @@ public class CommissionSimulationController {
private final CommissionSimulationService service;
/** 시뮬레이션 이력 목록 조회. 권한: COMM_SIMULATOR READ. */
@GetMapping
@RequirePermission(menu = "COMM_SIMULATOR", perm = "READ")
public ApiResponse<PageResponse<CommissionSimulationResp>> list(
@@ -28,12 +37,14 @@ public class CommissionSimulationController {
return ApiResponse.ok(service.list(param));
}
/** 시뮬레이션 단건 상세. 권한: COMM_SIMULATOR READ. */
@GetMapping("/{simId}")
@RequirePermission(menu = "COMM_SIMULATOR", perm = "READ")
public ApiResponse<CommissionSimulationResp> detail(@PathVariable Long simId) {
return ApiResponse.ok(service.detail(simId));
}
/** 시뮬레이션 실행. 요청 조건으로 예상 수수료를 계산해 결과를 반환(및 이력 저장). 권한: COMM_SIMULATOR CREATE. */
@PostMapping("/simulate")
@RequirePermission(menu = "COMM_SIMULATOR", perm = "CREATE")
@DataChangeLog(menu = "COMM_SIMULATOR", table = "commission_simulation")
@@ -16,6 +16,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 등급평가 API.
*
* 등급평가(grade evaluation): 설계사의 실적 등을 기준 규칙(Rule)에 따라 평가해 등급을 매기는 기능.
* 등급은 수수료율·시상 등에 영향을 준다.
* - Rule(평가 규칙) CRUD - Evaluation(평가 결과): 생성 후 apply(적용)하면 실제 설계사 등급에 반영된다.
*
* 권한: @RequirePermission(menu="GRADE_EVAL"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "등급평가")
@RestController
@RequestMapping("/api/grade-evaluations")
@@ -24,20 +33,23 @@ public class GradeEvaluationController {
private final GradeEvaluationService service;
// ── Rule ──────────────────────────────────────────────────────────────────
// ── Rule (평가 규칙) ────────────────────────────────────────────────────
/** 등급평가 규칙 목록 조회. 권한: GRADE_EVAL READ. */
@GetMapping("/rules")
@RequirePermission(menu = "GRADE_EVAL", perm = "READ")
public ApiResponse<PageResponse<GradeEvalRuleResp>> listRules(@ModelAttribute GradeEvalRuleSearchParam param) {
return ApiResponse.ok(service.listRules(param));
}
/** 등급평가 규칙 단건 상세. 권한: GRADE_EVAL READ. */
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "GRADE_EVAL", perm = "READ")
public ApiResponse<GradeEvalRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
/** 등급평가 규칙 신규 등록. 응답: ruleId. 권한: GRADE_EVAL CREATE. */
@PostMapping("/rules")
@RequirePermission(menu = "GRADE_EVAL", perm = "CREATE")
@DataChangeLog(menu = "GRADE_EVAL", table = "grade_eval_rule")
@@ -45,6 +57,7 @@ public class GradeEvaluationController {
return ApiResponse.ok(service.createRule(req));
}
/** 등급평가 규칙 수정. 권한: GRADE_EVAL UPDATE. */
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "GRADE_EVAL", perm = "UPDATE")
@DataChangeLog(menu = "GRADE_EVAL", table = "grade_eval_rule")
@@ -54,6 +67,7 @@ public class GradeEvaluationController {
return ApiResponse.ok();
}
/** 등급평가 규칙 삭제. 권한: GRADE_EVAL DELETE. */
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "GRADE_EVAL", perm = "DELETE")
@DataChangeLog(menu = "GRADE_EVAL", table = "grade_eval_rule")
@@ -62,8 +76,9 @@ public class GradeEvaluationController {
return ApiResponse.ok();
}
// ── Evaluation ────────────────────────────────────────────────────────────
// ── Evaluation (평가 결과) ──────────────────────────────────────────────
/** 등급평가 결과 목록 조회. 권한: GRADE_EVAL READ. */
@GetMapping
@RequirePermission(menu = "GRADE_EVAL", perm = "READ")
public ApiResponse<PageResponse<AgentGradeEvaluationResp>> listEvals(
@@ -71,12 +86,14 @@ public class GradeEvaluationController {
return ApiResponse.ok(service.listEvals(param));
}
/** 등급평가 결과 단건 상세. 권한: GRADE_EVAL READ. */
@GetMapping("/{evalId}")
@RequirePermission(menu = "GRADE_EVAL", perm = "READ")
public ApiResponse<AgentGradeEvaluationResp> detailEval(@PathVariable Long evalId) {
return ApiResponse.ok(service.detailEval(evalId));
}
/** 등급평가 결과 신규 생성(아직 미적용 상태). 응답: evalId. 권한: GRADE_EVAL CREATE. */
@PostMapping
@RequirePermission(menu = "GRADE_EVAL", perm = "CREATE")
@DataChangeLog(menu = "GRADE_EVAL", table = "agent_grade_evaluation")
@@ -84,6 +101,7 @@ public class GradeEvaluationController {
return ApiResponse.ok(service.createEval(req));
}
/** 등급평가 결과 적용. 평가된 등급을 실제 설계사 등급에 반영한다. 권한: GRADE_EVAL UPDATE. */
@PostMapping("/{evalId}/apply")
@RequirePermission(menu = "GRADE_EVAL", perm = "UPDATE")
@DataChangeLog(menu = "GRADE_EVAL", table = "agent_grade_evaluation")
@@ -92,6 +110,7 @@ public class GradeEvaluationController {
return ApiResponse.ok();
}
/** 등급평가 결과 삭제. 권한: GRADE_EVAL DELETE. */
@DeleteMapping("/{evalId}")
@RequirePermission(menu = "GRADE_EVAL", perm = "DELETE")
@DataChangeLog(menu = "GRADE_EVAL", table = "agent_grade_evaluation")
@@ -13,6 +13,14 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 보증보험 API.
*
* 보증보험(guarantee insurance): 설계사가 선지급금 등을 갚지 못할 경우에 대비해 가입하는 보험.
* 설계사별 가입 정보(보험사/증권번호/보장한도/기간 등)를 관리한다.
*
* 권한: @RequirePermission(menu="GUARANTEE_INS"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "보증보험")
@RestController
@RequestMapping("/api/guarantee-insurances")
@@ -21,6 +29,7 @@ public class GuaranteeInsuranceController {
private final GuaranteeInsuranceService service;
/** 보증보험 가입 목록 조회. 권한: GUARANTEE_INS READ. */
@GetMapping
@RequirePermission(menu = "GUARANTEE_INS", perm = "READ")
public ApiResponse<PageResponse<AgentGuaranteeInsuranceResp>> list(
@@ -28,12 +37,14 @@ public class GuaranteeInsuranceController {
return ApiResponse.ok(service.list(param));
}
/** 보증보험 단건 상세. 권한: GUARANTEE_INS READ. */
@GetMapping("/{guaranteeId}")
@RequirePermission(menu = "GUARANTEE_INS", perm = "READ")
public ApiResponse<AgentGuaranteeInsuranceResp> detail(@PathVariable Long guaranteeId) {
return ApiResponse.ok(service.detail(guaranteeId));
}
/** 보증보험 가입 등록. 응답: guaranteeId. 권한: GUARANTEE_INS CREATE. */
@PostMapping
@RequirePermission(menu = "GUARANTEE_INS", perm = "CREATE")
@DataChangeLog(menu = "GUARANTEE_INS", table = "agent_guarantee_insurance")
@@ -41,6 +52,7 @@ public class GuaranteeInsuranceController {
return ApiResponse.ok(service.create(req));
}
/** 보증보험 정보 수정. 권한: GUARANTEE_INS UPDATE. */
@PutMapping("/{guaranteeId}")
@RequirePermission(menu = "GUARANTEE_INS", perm = "UPDATE")
@DataChangeLog(menu = "GUARANTEE_INS", table = "agent_guarantee_insurance")
@@ -50,6 +62,7 @@ public class GuaranteeInsuranceController {
return ApiResponse.ok();
}
/** 보증보험 삭제. 권한: GUARANTEE_INS DELETE. */
@DeleteMapping("/{guaranteeId}")
@RequirePermission(menu = "GUARANTEE_INS", perm = "DELETE")
@DataChangeLog(menu = "GUARANTEE_INS", table = "agent_guarantee_insurance")
@@ -14,6 +14,14 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 본사정산 API.
*
* 본사정산(HQ settle): 본사 차원에서 발생하는 정산 항목(item)을 정산월 단위로 관리하고 합계(summary)를 낸다.
* 설계사 지급과 별개로 본사가 직접 가감하는 비용/수입 항목 등을 다룬다.
*
* 권한: @RequirePermission(menu="HQ_SETTLE"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "본사정산")
@RestController
@RequestMapping("/api/hq-settles")
@@ -22,24 +30,28 @@ public class HqSettleController {
private final HqSettleService service;
/** 본사정산 항목 목록 조회. 권한: HQ_SETTLE READ. */
@GetMapping
@RequirePermission(menu = "HQ_SETTLE", perm = "READ")
public ApiResponse<PageResponse<HqSettleItemResp>> list(@ModelAttribute HqSettleItemSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 특정 정산월의 본사정산 합계 요약. 권한: HQ_SETTLE READ. */
@GetMapping("/summary")
@RequirePermission(menu = "HQ_SETTLE", perm = "READ")
public ApiResponse<HqSummaryResp> summary(@RequestParam String settleMonth) {
return ApiResponse.ok(service.summary(settleMonth));
}
/** 본사정산 항목 단건 상세. 권한: HQ_SETTLE READ. */
@GetMapping("/{itemId}")
@RequirePermission(menu = "HQ_SETTLE", perm = "READ")
public ApiResponse<HqSettleItemResp> detail(@PathVariable Long itemId) {
return ApiResponse.ok(service.detail(itemId));
}
/** 본사정산 항목 신규 등록. 응답: itemId. 권한: HQ_SETTLE CREATE. */
@PostMapping
@RequirePermission(menu = "HQ_SETTLE", perm = "CREATE")
@DataChangeLog(menu = "HQ_SETTLE", table = "hq_settle_item")
@@ -47,6 +59,7 @@ public class HqSettleController {
return ApiResponse.ok(service.create(req));
}
/** 본사정산 항목 수정. 권한: HQ_SETTLE UPDATE. */
@PutMapping("/{itemId}")
@RequirePermission(menu = "HQ_SETTLE", perm = "UPDATE")
@DataChangeLog(menu = "HQ_SETTLE", table = "hq_settle_item")
@@ -56,6 +69,7 @@ public class HqSettleController {
return ApiResponse.ok();
}
/** 본사정산 항목 삭제. 권한: HQ_SETTLE DELETE. */
@DeleteMapping("/{itemId}")
@RequirePermission(menu = "HQ_SETTLE", perm = "DELETE")
@DataChangeLog(menu = "HQ_SETTLE", table = "hq_settle_item")
@@ -11,6 +11,17 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 수입수수료/수지차 API.
*
* - 수입수수료(income commission): GA가 보험사로부터 받는 수수료. "보험료 x 보험사 요율"로 계산되며,
* 설계사에게 지급하는 수수료(지출)의 재원이 된다.
* - 수지차(margin): 같은 정산월의 수입수수료 합계에서 설계사 지급액 합계를 뺀 GA의 마진.
* 양수면 흑자, 음수면 적자를 뜻한다.
*
* 모든 엔드포인트는 @RequirePermission(menu="INCOME_COMM")로 보호되며, perm 은 READ(조회)/CREATE(생성) 권한을 의미한다.
* 데이터 변경(POST)에는 @DataChangeLog 로 변경 이력(감사 로그)이 자동 기록된다.
*/
@Tag(name = "수입수수료/수지차")
@RestController
@RequestMapping("/api/income-commissions")
@@ -19,8 +30,9 @@ public class IncomeCommissionController {
private final IncomeCommissionService service;
// ── Income Ledger ────────────────────────────────────────────────────────
// ── Income Ledger (수입수수료 원장) ───────────────────────────────────────
/** 수입수수료 원장 목록 조회(검색조건 + 페이징). 권한: INCOME_COMM READ. */
@GetMapping("/ledgers")
@RequirePermission(menu = "INCOME_COMM", perm = "READ")
public ApiResponse<PageResponse<IncomeCommissionLedgerResp>> listLedgers(
@@ -28,12 +40,14 @@ public class IncomeCommissionController {
return ApiResponse.ok(service.listLedgers(param));
}
/** 수입수수료 원장 단건 상세 조회. 권한: INCOME_COMM READ. */
@GetMapping("/ledgers/{incomeId}")
@RequirePermission(menu = "INCOME_COMM", perm = "READ")
public ApiResponse<IncomeCommissionLedgerResp> detailLedger(@PathVariable Long incomeId) {
return ApiResponse.ok(service.detailLedger(incomeId));
}
/** 수입수수료 원장 신규 등록. 등록 시 "보험료 x 보험사요율"로 수입액이 계산된다. 응답: 생성된 incomeId. 권한: INCOME_COMM CREATE. */
@PostMapping("/ledgers")
@RequirePermission(menu = "INCOME_COMM", perm = "CREATE")
@DataChangeLog(menu = "INCOME_COMM", table = "income_commission_ledger")
@@ -41,8 +55,9 @@ public class IncomeCommissionController {
return ApiResponse.ok(service.createLedger(req));
}
// ── Margin Summary ───────────────────────────────────────────────────────
// ── Margin Summary (수지차 집계) ──────────────────────────────────────────
/** 수지차 집계 목록 조회. 권한: INCOME_COMM READ. */
@GetMapping("/margins")
@RequirePermission(menu = "INCOME_COMM", perm = "READ")
public ApiResponse<PageResponse<MarginSummaryResp>> listMargins(
@@ -50,12 +65,17 @@ public class IncomeCommissionController {
return ApiResponse.ok(service.listMargins(param));
}
/** 수지차 집계 단건 상세 조회. 권한: INCOME_COMM READ. */
@GetMapping("/margins/{marginId}")
@RequirePermission(menu = "INCOME_COMM", perm = "READ")
public ApiResponse<MarginSummaryResp> detailMargin(@PathVariable Long marginId) {
return ApiResponse.ok(service.detailMargin(marginId));
}
/**
* 수지차 재계산. 정산월(settleMonth) 기준으로 수입수수료 합 - 설계사 지급액 합을 다시 산출해 저장한다.
* agentId 가 주어지면 해당 설계사 지급분만 좁혀 집계한다(미지정 시 GA 월 전체). 응답: marginId. 권한: INCOME_COMM CREATE.
*/
@PostMapping("/margin/recalc")
@RequirePermission(menu = "INCOME_COMM", perm = "CREATE")
@DataChangeLog(menu = "INCOME_COMM", table = "margin_summary")
@@ -13,6 +13,14 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 공동계약 API.
*
* 공동계약(joint contract): 한 계약을 두 명 이상의 설계사가 함께 모집한 경우, 그 수수료를
* 약정 비율(지분)대로 나눠 갖도록 관리하는 기능. 비율 합이 100%가 되도록 등록한다.
*
* 권한: @RequirePermission(menu="JOINT_CONTRACT"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "공동계약")
@RestController
@RequestMapping("/api/joint-contracts")
@@ -21,18 +29,21 @@ public class JointContractController {
private final JointContractService service;
/** 공동계약 목록 조회. 권한: JOINT_CONTRACT READ. */
@GetMapping
@RequirePermission(menu = "JOINT_CONTRACT", perm = "READ")
public ApiResponse<PageResponse<JointContractResp>> list(@ModelAttribute JointContractSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 공동계약 단건 상세. 권한: JOINT_CONTRACT READ. */
@GetMapping("/{jointId}")
@RequirePermission(menu = "JOINT_CONTRACT", perm = "READ")
public ApiResponse<JointContractResp> detail(@PathVariable Long jointId) {
return ApiResponse.ok(service.detail(jointId));
}
/** 공동계약 신규 등록(설계사별 지분 비율 포함). 응답: jointId. 권한: JOINT_CONTRACT CREATE. */
@PostMapping
@RequirePermission(menu = "JOINT_CONTRACT", perm = "CREATE")
@DataChangeLog(menu = "JOINT_CONTRACT", table = "joint_contract")
@@ -40,6 +51,7 @@ public class JointContractController {
return ApiResponse.ok(service.create(req));
}
/** 공동계약 수정(지분 비율 변경 등). 권한: JOINT_CONTRACT UPDATE. */
@PutMapping("/{jointId}")
@RequirePermission(menu = "JOINT_CONTRACT", perm = "UPDATE")
@DataChangeLog(menu = "JOINT_CONTRACT", table = "joint_contract")
@@ -49,6 +61,7 @@ public class JointContractController {
return ApiResponse.ok();
}
/** 공동계약 삭제. 권한: JOINT_CONTRACT DELETE. */
@DeleteMapping("/{jointId}")
@RequirePermission(menu = "JOINT_CONTRACT", perm = "DELETE")
@DataChangeLog(menu = "JOINT_CONTRACT", table = "joint_contract")
@@ -13,6 +13,16 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 익월부활재지급 API.
*
* 실효(lapsed): 보험료 미납 등으로 계약 효력이 일시 정지된 상태. 실효가 나면 그 달에 지급했던 수수료를 환수한다.
* 부활(reinstatement): 실효된 계약이 다시 살아나는 것. 다음 달(익월)에 부활하면 앞서 환수했던 수수료를
* 다시 지급(재지급)해 준다. 이 도메인은 그 "환수 → 익월 부활 시 재지급(회수)" 흐름을 관리한다.
* - repay: 부활에 따른 재지급(회수) 확정 - cancel: 재지급 취소
*
* 권한: @RequirePermission(menu="LAPSED_REINSTATEMENT"), perm=READ/CREATE/UPDATE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "익월부활재지급")
@RestController
@RequestMapping("/api/lapsed-reinstatements")
@@ -21,6 +31,7 @@ public class LapsedReinstatementController {
private final LapsedReinstatementService service;
/** 익월부활 재지급 대상 목록 조회. 권한: LAPSED_REINSTATEMENT READ. */
@GetMapping
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "READ")
public ApiResponse<PageResponse<LapsedReinstatementRecoveryResp>> list(
@@ -28,12 +39,14 @@ public class LapsedReinstatementController {
return ApiResponse.ok(service.list(param));
}
/** 재지급 건 단건 상세. 권한: LAPSED_REINSTATEMENT READ. */
@GetMapping("/{recoveryId}")
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "READ")
public ApiResponse<LapsedReinstatementRecoveryResp> detail(@PathVariable Long recoveryId) {
return ApiResponse.ok(service.detail(recoveryId));
}
/** 재지급 건 신규 등록. 응답: recoveryId. 권한: LAPSED_REINSTATEMENT CREATE. */
@PostMapping
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "CREATE")
@DataChangeLog(menu = "LAPSED_REINSTATEMENT", table = "lapsed_reinstatement_recovery")
@@ -41,6 +54,7 @@ public class LapsedReinstatementController {
return ApiResponse.ok(service.create(req));
}
/** 부활에 따른 재지급(회수) 확정 처리. 권한: LAPSED_REINSTATEMENT UPDATE. */
@PostMapping("/{recoveryId}/repay")
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "UPDATE")
@DataChangeLog(menu = "LAPSED_REINSTATEMENT", table = "lapsed_reinstatement_recovery")
@@ -49,6 +63,7 @@ public class LapsedReinstatementController {
return ApiResponse.ok();
}
/** 재지급 취소 처리. 권한: LAPSED_REINSTATEMENT UPDATE. */
@PostMapping("/{recoveryId}/cancel")
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "UPDATE")
@DataChangeLog(menu = "LAPSED_REINSTATEMENT", table = "lapsed_reinstatement_recovery")
@@ -11,6 +11,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 생보운영지원수수료 API.
*
* 생명보험사 운영지원수수료(life operation support): 생보 상품 운영/관리 활동에 대해 지급하는 지원성 수수료.
* 규칙(Rule)에 정의된 기준에 따라 산출되어 원장(Ledger)에 기록된다.
*
* 구조는 "규칙(Rule) CRUD + 원장(Ledger) 조회/등록"으로, 다른 수수료 도메인과 동일한 패턴이다.
* 권한: @RequirePermission(menu="LIFE_OP_SUPPORT"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "생보운영지원수수료")
@RestController
@RequestMapping("/api/life-supports")
@@ -19,8 +28,9 @@ public class LifeSupportController {
private final LifeSupportService service;
// ── Rule ──────────────────────────────────────────────────────────────────
// ── Rule (지원수수료 규칙) ──────────────────────────────────────────────
/** 규칙 목록 조회. 권한: LIFE_OP_SUPPORT READ. */
@GetMapping("/rules")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ")
public ApiResponse<PageResponse<LifeSupportRuleResp>> listRules(
@@ -28,12 +38,14 @@ public class LifeSupportController {
return ApiResponse.ok(service.listRules(param));
}
/** 규칙 단건 상세. 권한: LIFE_OP_SUPPORT READ. */
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ")
public ApiResponse<LifeSupportRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
/** 규칙 신규 등록. 응답: ruleId. 권한: LIFE_OP_SUPPORT CREATE. */
@PostMapping("/rules")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "CREATE")
@DataChangeLog(menu = "LIFE_OP_SUPPORT", table = "life_support_rule")
@@ -41,6 +53,7 @@ public class LifeSupportController {
return ApiResponse.ok(service.createRule(req));
}
/** 규칙 수정. 권한: LIFE_OP_SUPPORT UPDATE. */
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "UPDATE")
@DataChangeLog(menu = "LIFE_OP_SUPPORT", table = "life_support_rule")
@@ -50,6 +63,7 @@ public class LifeSupportController {
return ApiResponse.ok();
}
/** 규칙 삭제. 권한: LIFE_OP_SUPPORT DELETE. */
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "DELETE")
@DataChangeLog(menu = "LIFE_OP_SUPPORT", table = "life_support_rule")
@@ -58,8 +72,9 @@ public class LifeSupportController {
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
// ── Ledger (지원수수료 원장) ────────────────────────────────────────────
/** 원장 목록 조회. 권한: LIFE_OP_SUPPORT READ. */
@GetMapping("/ledgers")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ")
public ApiResponse<PageResponse<LifeSupportLedgerResp>> listLedgers(
@@ -67,12 +82,14 @@ public class LifeSupportController {
return ApiResponse.ok(service.listLedgers(param));
}
/** 원장 단건 상세. 권한: LIFE_OP_SUPPORT READ. */
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ")
public ApiResponse<LifeSupportLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
/** 원장 신규 등록(지원수수료 산출/지급 기록). 응답: ledgerId. 권한: LIFE_OP_SUPPORT CREATE. */
@PostMapping("/ledgers")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "CREATE")
@DataChangeLog(menu = "LIFE_OP_SUPPORT", table = "life_support_ledger")
@@ -11,6 +11,16 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 오버라이드 원장 API.
*
* 오버라이드(override): 상위 직급자(예: 팀장/지점장)가 자기 산하 하위 설계사의 실적에 대해
* 일정 비율로 추가로 받는 수수료. "하위가 판 만큼 상위에게도 얹어주는" 구조라 override 라 부른다.
* 이 원장은 누가(fromAgent) 누구에게(toAgentId) 얼마를 넘겨주는지를 기록한다.
* - summary: 수령자(상위자) 기준 집계.
*
* 권한: @RequirePermission(menu="OVERRIDE_LEDGER"), perm=READ. (조회 전용 - 생성/배치는 별도 계산 로직에서 처리)
*/
@Tag(name = "오버라이드 원장")
@RestController
@RequestMapping("/api/override-ledgers")
@@ -19,24 +29,28 @@ public class OverrideLedgerController {
private final OverrideLedgerService service;
/** 오버라이드 원장 목록 조회. 권한: OVERRIDE_LEDGER READ. */
@GetMapping
@RequirePermission(menu = "OVERRIDE_LEDGER", perm = "READ")
public ApiResponse<PageResponse<OverrideLedgerResp>> list(@ModelAttribute OverrideLedgerSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 오버라이드 원장 단건 상세. 권한: OVERRIDE_LEDGER READ. */
@GetMapping("/{ledgerId}")
@RequirePermission(menu = "OVERRIDE_LEDGER", perm = "READ")
public ApiResponse<OverrideLedgerResp> detail(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detail(ledgerId));
}
/** 오버라이드 집계 목록(수령자별 합계 등). 권한: OVERRIDE_LEDGER READ. */
@GetMapping("/summary")
@RequirePermission(menu = "OVERRIDE_LEDGER", perm = "READ")
public ApiResponse<List<OverrideSummaryResp>> summaryList(@ModelAttribute OverrideSummarySearchParam param) {
return ApiResponse.ok(service.summaryList(param));
}
/** 특정 수령자(toAgentId)의 특정 정산월(settleMonth) 오버라이드 합계. 권한: OVERRIDE_LEDGER READ. */
@GetMapping("/summary/by-agent")
@RequirePermission(menu = "OVERRIDE_LEDGER", perm = "READ")
public ApiResponse<OverrideSummaryResp> summaryByAgent(@RequestParam Long toAgentId,
@@ -11,6 +11,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 유지보너스(유지보수수당) API.
*
* 유지율 보너스(persistency bonus): 계약이 일정 기간(예: 13회차/25회차)까지 해지 없이 잘 "유지"되면
* 그 유지 성과에 대해 설계사에게 지급하는 보너스. 유지율이 높을수록 보너스가 커진다.
* - Rule(보너스 규칙) CRUD - Bonus(보너스 산출 내역): upsert(있으면 갱신, 없으면 생성)로 등록.
*
* 권한: @RequirePermission(menu="PERSIST_BONUS"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "유지보수수당")
@RestController
@RequestMapping("/api/persistency-bonus")
@@ -19,6 +28,7 @@ public class PersistencyBonusController {
private final PersistencyBonusService service;
/** 유지보너스 규칙 목록 조회. 권한: PERSIST_BONUS READ. */
@GetMapping("/rules")
@RequirePermission(menu = "PERSIST_BONUS", perm = "READ")
public ApiResponse<PageResponse<PersistencyBonusRuleResp>> listRules(
@@ -26,12 +36,14 @@ public class PersistencyBonusController {
return ApiResponse.ok(service.listRules(param));
}
/** 유지보너스 규칙 단건 상세. 권한: PERSIST_BONUS READ. */
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "PERSIST_BONUS", perm = "READ")
public ApiResponse<PersistencyBonusRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
/** 유지보너스 규칙 신규 등록. 응답: ruleId. 권한: PERSIST_BONUS CREATE. */
@PostMapping("/rules")
@RequirePermission(menu = "PERSIST_BONUS", perm = "CREATE")
@DataChangeLog(menu = "PERSIST_BONUS", table = "persistency_bonus_rule")
@@ -39,6 +51,7 @@ public class PersistencyBonusController {
return ApiResponse.ok(service.createRule(req));
}
/** 유지보너스 규칙 수정. 권한: PERSIST_BONUS UPDATE. */
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "PERSIST_BONUS", perm = "UPDATE")
@DataChangeLog(menu = "PERSIST_BONUS", table = "persistency_bonus_rule")
@@ -48,6 +61,7 @@ public class PersistencyBonusController {
return ApiResponse.ok();
}
/** 유지보너스 규칙 삭제. 권한: PERSIST_BONUS DELETE. */
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "PERSIST_BONUS", perm = "DELETE")
@DataChangeLog(menu = "PERSIST_BONUS", table = "persistency_bonus_rule")
@@ -56,6 +70,7 @@ public class PersistencyBonusController {
return ApiResponse.ok();
}
/** 유지보너스 산출 내역 목록 조회. 권한: PERSIST_BONUS READ. */
@GetMapping("/bonuses")
@RequirePermission(menu = "PERSIST_BONUS", perm = "READ")
public ApiResponse<PageResponse<PersistencyBonusResp>> listBonuses(
@@ -63,12 +78,14 @@ public class PersistencyBonusController {
return ApiResponse.ok(service.listBonuses(param));
}
/** 유지보너스 산출 내역 단건 상세. 권한: PERSIST_BONUS READ. */
@GetMapping("/bonuses/{bonusId}")
@RequirePermission(menu = "PERSIST_BONUS", perm = "READ")
public ApiResponse<PersistencyBonusResp> detailBonus(@PathVariable Long bonusId) {
return ApiResponse.ok(service.detailBonus(bonusId));
}
/** 유지보너스 산출 내역 upsert(동일 키가 있으면 갱신, 없으면 신규 생성). 권한: PERSIST_BONUS CREATE. */
@PostMapping("/bonuses")
@RequirePermission(menu = "PERSIST_BONUS", perm = "CREATE")
@DataChangeLog(menu = "PERSIST_BONUS", table = "persistency_bonus")
@@ -11,6 +11,16 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 인정실적 API.
*
* 인정실적(recognized performance): 실제 보험료를 그대로 실적으로 보지 않고, 상품 종류에 따른
* 환산율(product conversion rate)을 곱해 "회사가 인정해 주는 실적"으로 환산한 값.
* (예: 보장성 100%, 저축성 30% 등. 같은 보험료라도 상품에 따라 인정실적이 달라진다.)
* - rates(환산율 규칙) CRUD - ledgers(인정실적 원장) 조회/등록.
*
* 권한: @RequirePermission(menu="RECOGNIZED_PERF"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "인정실적")
@RestController
@RequestMapping("/api/recognized-performances")
@@ -19,8 +29,9 @@ public class RecognizedPerformanceController {
private final RecognizedPerformanceService service;
// ── Product Conversion Rate (rule) ────────────────────────────────────────
// ── Product Conversion Rate (상품 환산율 규칙) ──────────────────────────
/** 상품 환산율 목록 조회. 권한: RECOGNIZED_PERF READ. */
@GetMapping("/rates")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ")
public ApiResponse<PageResponse<ProductConversionRateResp>> listRates(
@@ -28,12 +39,14 @@ public class RecognizedPerformanceController {
return ApiResponse.ok(service.listRates(param));
}
/** 상품 환산율 단건 상세. 권한: RECOGNIZED_PERF READ. */
@GetMapping("/rates/{rateId}")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ")
public ApiResponse<ProductConversionRateResp> detailRate(@PathVariable Long rateId) {
return ApiResponse.ok(service.detailRate(rateId));
}
/** 상품 환산율 신규 등록. 응답: rateId. 권한: RECOGNIZED_PERF CREATE. */
@PostMapping("/rates")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "CREATE")
@DataChangeLog(menu = "RECOGNIZED_PERF", table = "product_conversion_rate")
@@ -41,6 +54,7 @@ public class RecognizedPerformanceController {
return ApiResponse.ok(service.createRate(req));
}
/** 상품 환산율 수정. 권한: RECOGNIZED_PERF UPDATE. */
@PutMapping("/rates/{rateId}")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "UPDATE")
@DataChangeLog(menu = "RECOGNIZED_PERF", table = "product_conversion_rate")
@@ -50,6 +64,7 @@ public class RecognizedPerformanceController {
return ApiResponse.ok();
}
/** 상품 환산율 삭제. 권한: RECOGNIZED_PERF DELETE. */
@DeleteMapping("/rates/{rateId}")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "DELETE")
@DataChangeLog(menu = "RECOGNIZED_PERF", table = "product_conversion_rate")
@@ -58,8 +73,9 @@ public class RecognizedPerformanceController {
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
// ── Ledger (인정실적 원장) ──────────────────────────────────────────────
/** 인정실적 원장 목록 조회. 권한: RECOGNIZED_PERF READ. */
@GetMapping("/ledgers")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ")
public ApiResponse<PageResponse<RecognizedPerformanceLedgerResp>> listLedgers(
@@ -67,12 +83,14 @@ public class RecognizedPerformanceController {
return ApiResponse.ok(service.listLedgers(param));
}
/** 인정실적 원장 단건 상세. 권한: RECOGNIZED_PERF READ. */
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ")
public ApiResponse<RecognizedPerformanceLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
/** 인정실적 원장 신규 등록(환산율 적용해 인정실적 산출). 응답: ledgerId. 권한: RECOGNIZED_PERF CREATE. */
@PostMapping("/ledgers")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "CREATE")
@DataChangeLog(menu = "RECOGNIZED_PERF", table = "recognized_performance_ledger")
@@ -11,6 +11,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 도입수수료 API.
*
* 도입수수료(recruiter commission): 새 설계사를 회사에 데려온(도입/리크루팅) 사람에게 지급하는 수수료.
* 도입한 설계사가 실적을 내면, 그 실적에 연동해 도입자에게도 일정 수수료가 발생한다.
* - Rule(도입수수료 규칙) CRUD - Ledger(도입수수료 원장) 조회/등록.
*
* 권한: @RequirePermission(menu="RECRUITER_COMM"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "도입수수료")
@RestController
@RequestMapping("/api/recruiter-commissions")
@@ -19,8 +28,9 @@ public class RecruiterCommissionController {
private final RecruiterCommissionService service;
// ── Rule ──────────────────────────────────────────────────────────────────
// ── Rule (도입수수료 규칙) ──────────────────────────────────────────────
/** 도입수수료 규칙 목록 조회. 권한: RECRUITER_COMM READ. */
@GetMapping("/rules")
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
public ApiResponse<PageResponse<RecruiterCommissionRuleResp>> listRules(
@@ -28,12 +38,14 @@ public class RecruiterCommissionController {
return ApiResponse.ok(service.listRules(param));
}
/** 도입수수료 규칙 단건 상세. 권한: RECRUITER_COMM READ. */
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
public ApiResponse<RecruiterCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
/** 도입수수료 규칙 신규 등록. 응답: ruleId. 권한: RECRUITER_COMM CREATE. */
@PostMapping("/rules")
@RequirePermission(menu = "RECRUITER_COMM", perm = "CREATE")
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_rule")
@@ -41,6 +53,7 @@ public class RecruiterCommissionController {
return ApiResponse.ok(service.createRule(req));
}
/** 도입수수료 규칙 수정. 권한: RECRUITER_COMM UPDATE. */
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "RECRUITER_COMM", perm = "UPDATE")
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_rule")
@@ -50,6 +63,7 @@ public class RecruiterCommissionController {
return ApiResponse.ok();
}
/** 도입수수료 규칙 삭제. 권한: RECRUITER_COMM DELETE. */
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "RECRUITER_COMM", perm = "DELETE")
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_rule")
@@ -58,8 +72,9 @@ public class RecruiterCommissionController {
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
// ── Ledger (도입수수료 원장) ────────────────────────────────────────────
/** 도입수수료 원장 목록 조회. 권한: RECRUITER_COMM READ. */
@GetMapping("/ledgers")
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
public ApiResponse<PageResponse<RecruiterCommissionLedgerResp>> listLedgers(
@@ -67,12 +82,14 @@ public class RecruiterCommissionController {
return ApiResponse.ok(service.listLedgers(param));
}
/** 도입수수료 원장 단건 상세. 권한: RECRUITER_COMM READ. */
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
public ApiResponse<RecruiterCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
/** 도입수수료 원장 신규 등록. 응답: ledgerId. 권한: RECRUITER_COMM CREATE. */
@PostMapping("/ledgers")
@RequirePermission(menu = "RECRUITER_COMM", perm = "CREATE")
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_ledger")
@@ -11,6 +11,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 소개·이관수수료 API.
*
* 소개수수료(referral commission): 고객이나 계약을 다른 설계사에게 "소개·이관"해 준 사람에게 지급하는 수수료.
* 소개 받은 설계사가 계약을 성사시키면, 소개해 준 사람에게 규칙(Rule)에 따라 정액(FIXED) 또는 정률(RATE)로 지급한다.
* - Rule(소개수수료 규칙) CRUD - Ledger(소개수수료 원장) 조회/등록.
*
* 권한: @RequirePermission(menu="REFERRAL_COMM"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "소개·이관수수료")
@RestController
@RequestMapping("/api/referral-commissions")
@@ -19,8 +28,9 @@ public class ReferralCommissionController {
private final ReferralCommissionService service;
// ── Rule ──────────────────────────────────────────────────────────────────
// ── Rule (소개수수료 규칙) ──────────────────────────────────────────────
/** 소개수수료 규칙 목록 조회. 권한: REFERRAL_COMM READ. */
@GetMapping("/rules")
@RequirePermission(menu = "REFERRAL_COMM", perm = "READ")
public ApiResponse<PageResponse<ReferralCommissionRuleResp>> listRules(
@@ -28,12 +38,14 @@ public class ReferralCommissionController {
return ApiResponse.ok(service.listRules(param));
}
/** 소개수수료 규칙 단건 상세. 권한: REFERRAL_COMM READ. */
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "REFERRAL_COMM", perm = "READ")
public ApiResponse<ReferralCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
/** 소개수수료 규칙 신규 등록. 응답: ruleId. 권한: REFERRAL_COMM CREATE. */
@PostMapping("/rules")
@RequirePermission(menu = "REFERRAL_COMM", perm = "CREATE")
@DataChangeLog(menu = "REFERRAL_COMM", table = "referral_commission_rule")
@@ -41,6 +53,7 @@ public class ReferralCommissionController {
return ApiResponse.ok(service.createRule(req));
}
/** 소개수수료 규칙 수정. 권한: REFERRAL_COMM UPDATE. */
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "REFERRAL_COMM", perm = "UPDATE")
@DataChangeLog(menu = "REFERRAL_COMM", table = "referral_commission_rule")
@@ -50,6 +63,7 @@ public class ReferralCommissionController {
return ApiResponse.ok();
}
/** 소개수수료 규칙 삭제. 권한: REFERRAL_COMM DELETE. */
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "REFERRAL_COMM", perm = "DELETE")
@DataChangeLog(menu = "REFERRAL_COMM", table = "referral_commission_rule")
@@ -58,8 +72,9 @@ public class ReferralCommissionController {
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
// ── Ledger (소개수수료 원장) ────────────────────────────────────────────
/** 소개수수료 원장 목록 조회. 권한: REFERRAL_COMM READ. */
@GetMapping("/ledgers")
@RequirePermission(menu = "REFERRAL_COMM", perm = "READ")
public ApiResponse<PageResponse<ReferralCommissionLedgerResp>> listLedgers(
@@ -67,12 +82,14 @@ public class ReferralCommissionController {
return ApiResponse.ok(service.listLedgers(param));
}
/** 소개수수료 원장 단건 상세. 권한: REFERRAL_COMM READ. */
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "REFERRAL_COMM", perm = "READ")
public ApiResponse<ReferralCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
/** 소개수수료 원장 신규 등록. 응답: ledgerId. 권한: REFERRAL_COMM CREATE. */
@PostMapping("/ledgers")
@RequirePermission(menu = "REFERRAL_COMM", perm = "CREATE")
@DataChangeLog(menu = "REFERRAL_COMM", table = "referral_commission_ledger")
@@ -11,6 +11,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 부활수수료 API.
*
* 부활(reinstatement): 실효(보험료 미납 등으로 효력 정지)됐던 계약이 밀린 보험료 납입 등으로 다시 살아나는 것.
* 부활수수료: 계약을 부활시킨 설계사에게 규칙(Rule)에 따라 지급하는 수수료.
* - Rule(부활수수료 규칙) CRUD - Ledger(부활수수료 원장) 조회/등록.
*
* 권한: @RequirePermission(menu="REINSTATE_COMM"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "부활수수료")
@RestController
@RequestMapping("/api/reinstatement-commissions")
@@ -19,6 +28,7 @@ public class ReinstatementCommissionController {
private final ReinstatementCommissionService service;
/** 부활수수료 규칙 목록 조회. 권한: REINSTATE_COMM READ. */
@GetMapping("/rules")
@RequirePermission(menu = "REINSTATE_COMM", perm = "READ")
public ApiResponse<PageResponse<ReinstatementCommissionRuleResp>> listRules(
@@ -26,12 +36,14 @@ public class ReinstatementCommissionController {
return ApiResponse.ok(service.listRules(param));
}
/** 부활수수료 규칙 단건 상세. 권한: REINSTATE_COMM READ. */
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "REINSTATE_COMM", perm = "READ")
public ApiResponse<ReinstatementCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
/** 부활수수료 규칙 신규 등록. 응답: ruleId. 권한: REINSTATE_COMM CREATE. */
@PostMapping("/rules")
@RequirePermission(menu = "REINSTATE_COMM", perm = "CREATE")
@DataChangeLog(menu = "REINSTATE_COMM", table = "reinstatement_commission_rule")
@@ -39,6 +51,7 @@ public class ReinstatementCommissionController {
return ApiResponse.ok(service.createRule(req));
}
/** 부활수수료 규칙 수정. 권한: REINSTATE_COMM UPDATE. */
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "REINSTATE_COMM", perm = "UPDATE")
@DataChangeLog(menu = "REINSTATE_COMM", table = "reinstatement_commission_rule")
@@ -48,6 +61,7 @@ public class ReinstatementCommissionController {
return ApiResponse.ok();
}
/** 부활수수료 규칙 삭제. 권한: REINSTATE_COMM DELETE. */
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "REINSTATE_COMM", perm = "DELETE")
@DataChangeLog(menu = "REINSTATE_COMM", table = "reinstatement_commission_rule")
@@ -56,6 +70,7 @@ public class ReinstatementCommissionController {
return ApiResponse.ok();
}
/** 부활수수료 원장 목록 조회. 권한: REINSTATE_COMM READ. */
@GetMapping("/ledgers")
@RequirePermission(menu = "REINSTATE_COMM", perm = "READ")
public ApiResponse<PageResponse<ReinstatementCommissionLedgerResp>> listLedgers(
@@ -63,12 +78,14 @@ public class ReinstatementCommissionController {
return ApiResponse.ok(service.listLedgers(param));
}
/** 부활수수료 원장 단건 상세. 권한: REINSTATE_COMM READ. */
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "REINSTATE_COMM", perm = "READ")
public ApiResponse<ReinstatementCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
/** 부활수수료 원장 신규 등록. 응답: ledgerId. 권한: REINSTATE_COMM CREATE. */
@PostMapping("/ledgers")
@RequirePermission(menu = "REINSTATE_COMM", perm = "CREATE")
@DataChangeLog(menu = "REINSTATE_COMM", table = "reinstatement_commission_ledger")
@@ -11,6 +11,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 갱신수수료 API.
*
* 갱신(renewal): 보험 기간이 끝난 계약을 같은 조건으로 다시 이어가는 것(예: 1년 갱신형 보험).
* 갱신수수료: 계약이 갱신될 때 그 유지·관리 대가로 설계사에게 지급하는 수수료.
* - Rule(갱신수수료 규칙) CRUD - Ledger(갱신수수료 원장) 조회/등록.
*
* 권한: @RequirePermission(menu="RENEWAL_COMM"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "갱신수수료")
@RestController
@RequestMapping("/api/renewal-commissions")
@@ -19,6 +28,7 @@ public class RenewalCommissionController {
private final RenewalCommissionService service;
/** 갱신수수료 규칙 목록 조회. 권한: RENEWAL_COMM READ. */
@GetMapping("/rules")
@RequirePermission(menu = "RENEWAL_COMM", perm = "READ")
public ApiResponse<PageResponse<RenewalCommissionRuleResp>> listRules(
@@ -26,12 +36,14 @@ public class RenewalCommissionController {
return ApiResponse.ok(service.listRules(param));
}
/** 갱신수수료 규칙 단건 상세. 권한: RENEWAL_COMM READ. */
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "RENEWAL_COMM", perm = "READ")
public ApiResponse<RenewalCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
/** 갱신수수료 규칙 신규 등록. 응답: ruleId. 권한: RENEWAL_COMM CREATE. */
@PostMapping("/rules")
@RequirePermission(menu = "RENEWAL_COMM", perm = "CREATE")
@DataChangeLog(menu = "RENEWAL_COMM", table = "renewal_commission_rule")
@@ -39,6 +51,7 @@ public class RenewalCommissionController {
return ApiResponse.ok(service.createRule(req));
}
/** 갱신수수료 규칙 수정. 권한: RENEWAL_COMM UPDATE. */
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "RENEWAL_COMM", perm = "UPDATE")
@DataChangeLog(menu = "RENEWAL_COMM", table = "renewal_commission_rule")
@@ -48,6 +61,7 @@ public class RenewalCommissionController {
return ApiResponse.ok();
}
/** 갱신수수료 규칙 삭제. 권한: RENEWAL_COMM DELETE. */
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "RENEWAL_COMM", perm = "DELETE")
@DataChangeLog(menu = "RENEWAL_COMM", table = "renewal_commission_rule")
@@ -56,6 +70,7 @@ public class RenewalCommissionController {
return ApiResponse.ok();
}
/** 갱신수수료 원장 목록 조회. 권한: RENEWAL_COMM READ. */
@GetMapping("/ledgers")
@RequirePermission(menu = "RENEWAL_COMM", perm = "READ")
public ApiResponse<PageResponse<RenewalCommissionLedgerResp>> listLedgers(
@@ -63,12 +78,14 @@ public class RenewalCommissionController {
return ApiResponse.ok(service.listLedgers(param));
}
/** 갱신수수료 원장 단건 상세. 권한: RENEWAL_COMM READ. */
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "RENEWAL_COMM", perm = "READ")
public ApiResponse<RenewalCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
/** 갱신수수료 원장 신규 등록. 응답: ledgerId. 권한: RENEWAL_COMM CREATE. */
@PostMapping("/ledgers")
@RequirePermission(menu = "RENEWAL_COMM", perm = "CREATE")
@DataChangeLog(menu = "RENEWAL_COMM", table = "renewal_commission_ledger")
@@ -13,6 +13,14 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 적립금 API.
*
* 적립금(reserve fund): 향후 발생할 수 있는 환수 등에 대비해, 설계사 수수료 중 일부를 떼어 모아 두는 돈.
* 환수가 생기면 이 적립금에서 우선 충당할 수 있다. 설계사별 적립 내역을 관리한다.
*
* 권한: @RequirePermission(menu="RESERVE_FUND"), perm=READ/CREATE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "적립금")
@RestController
@RequestMapping("/api/reserve-funds")
@@ -21,18 +29,21 @@ public class ReserveFundController {
private final ReserveFundService service;
/** 적립금 내역 목록 조회. 권한: RESERVE_FUND READ. */
@GetMapping
@RequirePermission(menu = "RESERVE_FUND", perm = "READ")
public ApiResponse<PageResponse<AgentReserveFundResp>> list(@ModelAttribute AgentReserveFundSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 적립금 단건 상세. 권한: RESERVE_FUND READ. */
@GetMapping("/{reserveId}")
@RequirePermission(menu = "RESERVE_FUND", perm = "READ")
public ApiResponse<AgentReserveFundResp> detail(@PathVariable Long reserveId) {
return ApiResponse.ok(service.detail(reserveId));
}
/** 적립금 내역 신규 등록(적립/사용 기록). 응답: reserveId. 권한: RESERVE_FUND CREATE. */
@PostMapping
@RequirePermission(menu = "RESERVE_FUND", perm = "CREATE")
@DataChangeLog(menu = "RESERVE_FUND", table = "agent_reserve_fund")
@@ -11,6 +11,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 정착지원금 API.
*
* 정착지원금(settling support): 새로 들어온 설계사가 안정적으로 자리 잡을 때까지 일정 기간 지급하는 지원금.
* 보통 실적이 충분치 않은 초기 기간 동안 최소 수입을 보전해 준다(조건 미달 시 환수될 수도 있음).
* - Rule(지원금 규칙) CRUD - Ledger(지원금 원장) 조회/등록.
*
* 권한: @RequirePermission(menu="SETTLING_SUPPORT"), perm=READ/CREATE/UPDATE/DELETE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "정착지원금")
@RestController
@RequestMapping("/api/settling-supports")
@@ -19,8 +28,9 @@ public class SettlingSupportController {
private final SettlingSupportService service;
// ── Rule ──────────────────────────────────────────────────────────────────
// ── Rule (정착지원금 규칙) ──────────────────────────────────────────────
/** 정착지원금 규칙 목록 조회. 권한: SETTLING_SUPPORT READ. */
@GetMapping("/rules")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ")
public ApiResponse<PageResponse<SettlingSupportRuleResp>> listRules(
@@ -28,12 +38,14 @@ public class SettlingSupportController {
return ApiResponse.ok(service.listRules(param));
}
/** 정착지원금 규칙 단건 상세. 권한: SETTLING_SUPPORT READ. */
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ")
public ApiResponse<SettlingSupportRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
/** 정착지원금 규칙 신규 등록. 응답: ruleId. 권한: SETTLING_SUPPORT CREATE. */
@PostMapping("/rules")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "CREATE")
@DataChangeLog(menu = "SETTLING_SUPPORT", table = "settling_support_rule")
@@ -41,6 +53,7 @@ public class SettlingSupportController {
return ApiResponse.ok(service.createRule(req));
}
/** 정착지원금 규칙 수정. 권한: SETTLING_SUPPORT UPDATE. */
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "UPDATE")
@DataChangeLog(menu = "SETTLING_SUPPORT", table = "settling_support_rule")
@@ -50,6 +63,7 @@ public class SettlingSupportController {
return ApiResponse.ok();
}
/** 정착지원금 규칙 삭제. 권한: SETTLING_SUPPORT DELETE. */
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "DELETE")
@DataChangeLog(menu = "SETTLING_SUPPORT", table = "settling_support_rule")
@@ -58,8 +72,9 @@ public class SettlingSupportController {
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
// ── Ledger (정착지원금 원장) ────────────────────────────────────────────
/** 정착지원금 원장 목록 조회. 권한: SETTLING_SUPPORT READ. */
@GetMapping("/ledgers")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ")
public ApiResponse<PageResponse<SettlingSupportLedgerResp>> listLedgers(
@@ -67,12 +82,14 @@ public class SettlingSupportController {
return ApiResponse.ok(service.listLedgers(param));
}
/** 정착지원금 원장 단건 상세. 권한: SETTLING_SUPPORT READ. */
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ")
public ApiResponse<SettlingSupportLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
/** 정착지원금 원장 신규 등록. 응답: ledgerId. 권한: SETTLING_SUPPORT CREATE. */
@PostMapping("/ledgers")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "CREATE")
@DataChangeLog(menu = "SETTLING_SUPPORT", table = "settling_support_ledger")
@@ -15,6 +15,14 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 환수시효관리 API.
*
* 시효(statute of limitations): 환수채권을 받아낼 수 있는 법적 기한. 기한이 지나면(시효 만료) 더 이상 회수할 수 없어
* 손실(대손)로 처리해야 한다. 이 도메인은 시효 기준 설정(config)과 만료 건 일괄 정리를 담당한다.
*
* 권한: @RequirePermission(menu="STATUTE_CONFIG"), perm=READ/CREATE/UPDATE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "환수시효관리")
@RestController
@RequestMapping("/api/statute")
@@ -23,18 +31,21 @@ public class StatuteController {
private final StatuteService service;
/** 시효 설정 전체 목록 조회. 권한: STATUTE_CONFIG READ. */
@GetMapping
@RequirePermission(menu = "STATUTE_CONFIG", perm = "READ")
public ApiResponse<List<StatuteConfigResp>> list() {
return ApiResponse.ok(service.listConfigs());
}
/** 시효 설정 단건 상세. 권한: STATUTE_CONFIG READ. */
@GetMapping("/{configId}")
@RequirePermission(menu = "STATUTE_CONFIG", perm = "READ")
public ApiResponse<StatuteConfigResp> detail(@PathVariable Long configId) {
return ApiResponse.ok(service.detailConfig(configId));
}
/** 시효 설정 신규 등록. 응답: configId. 권한: STATUTE_CONFIG CREATE. */
@PostMapping
@RequirePermission(menu = "STATUTE_CONFIG", perm = "CREATE")
@DataChangeLog(menu = "STATUTE_CONFIG", table = "statute_config")
@@ -42,6 +53,7 @@ public class StatuteController {
return ApiResponse.ok(service.createConfig(req));
}
/** 시효 설정 수정. 권한: STATUTE_CONFIG UPDATE. */
@PutMapping("/{configId}")
@RequirePermission(menu = "STATUTE_CONFIG", perm = "UPDATE")
@DataChangeLog(menu = "STATUTE_CONFIG", table = "statute_config")
@@ -52,13 +64,16 @@ public class StatuteController {
}
/**
* 만료 대상 환수채권 일괄 대손 처리.
* 만료 대상 환수채권 일괄 대손 처리. 기준월(baseMonth)까지 시효가 지난 환수채권을 한꺼번에 손실 처리하고
* 처리 건수를 반환한다. baseMonth 미지정 시 현재 정산월을 사용한다.
* Request body: { "baseMonth": "YYYYMM" }
* 권한: STATUTE_CONFIG UPDATE.
*/
@PostMapping("/expire")
@RequirePermission(menu = "STATUTE_CONFIG", perm = "UPDATE")
@DataChangeLog(menu = "STATUTE_CONFIG", table = "clawback_receivable")
public ApiResponse<Integer> expire(@RequestBody Map<String, String> body) {
// baseMonth 가 비어 있으면 현재 정산월로 대체
String baseMonth = body.get("baseMonth");
if (baseMonth == null || baseMonth.isBlank()) {
baseMonth = com.ga.common.util.DateUtil.getSettleMonth();
@@ -15,6 +15,15 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 단계마감 API.
*
* 단계마감(step close): 월 정산을 여러 단계로 나눠, 각 단계가 끝나면 "마감"해서 더 이상 수정 못 하도록 잠그는 것.
* 마감(close) 후 수정이 필요하면 다시 reopen(마감 해제)할 수 있다. 마감/해제는 승인(APPROVE) 권한이 필요하다.
* - close: 단계 마감(잠금) - reopen: 마감 해제(다시 열기)
*
* 권한: @RequirePermission(menu="STEP_CLOSE"), perm=READ/UPDATE/APPROVE. 변경은 @DataChangeLog 기록.
*/
@Tag(name = "단계마감")
@RestController
@RequestMapping("/api/step-closes")
@@ -23,24 +32,28 @@ public class StepCloseController {
private final StepCloseService service;
/** 단계마감 목록 조회. 권한: STEP_CLOSE READ. */
@GetMapping
@RequirePermission(menu = "STEP_CLOSE", perm = "READ")
public ApiResponse<PageResponse<StepCloseResp>> list(@ModelAttribute StepCloseSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 특정 정산월(settleMonth)의 단계마감 현황 조회. 권한: STEP_CLOSE READ. */
@GetMapping("/by-month")
@RequirePermission(menu = "STEP_CLOSE", perm = "READ")
public ApiResponse<List<StepCloseResp>> listByMonth(@RequestParam String settleMonth) {
return ApiResponse.ok(service.listByMonth(settleMonth));
}
/** 단계마감 단건 상세. 권한: STEP_CLOSE READ. */
@GetMapping("/{stepCloseId}")
@RequirePermission(menu = "STEP_CLOSE", perm = "READ")
public ApiResponse<StepCloseResp> detail(@PathVariable Long stepCloseId) {
return ApiResponse.ok(service.detail(stepCloseId));
}
/** 단계마감 항목 신규 생성(아직 미마감 상태). 응답: stepCloseId. 권한: STEP_CLOSE UPDATE. */
@PostMapping
@RequirePermission(menu = "STEP_CLOSE", perm = "UPDATE")
@DataChangeLog(menu = "STEP_CLOSE", table = "settlement_step_close")
@@ -48,6 +61,7 @@ public class StepCloseController {
return ApiResponse.ok(service.create(req));
}
/** 단계 마감 확정(해당 단계 잠금). 승인 권한 필요. 권한: STEP_CLOSE APPROVE. */
@PostMapping("/{stepCloseId}/close")
@RequirePermission(menu = "STEP_CLOSE", perm = "APPROVE")
@DataChangeLog(menu = "STEP_CLOSE", table = "settlement_step_close")
@@ -56,6 +70,7 @@ public class StepCloseController {
return ApiResponse.ok();
}
/** 단계 마감 해제(다시 수정 가능 상태로). 승인 권한 필요. 권한: STEP_CLOSE APPROVE. */
@PostMapping("/{stepCloseId}/reopen")
@RequirePermission(menu = "STEP_CLOSE", perm = "APPROVE")
@DataChangeLog(menu = "STEP_CLOSE", table = "settlement_step_close")
@@ -15,6 +15,15 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 민원 관리 API.
*
* <p>고객 민원을 접수·처리하는 도메인이다. 민원은 특정 보험계약(contractId)·담당 설계사(agentId)와 연결되며,
* 상태는 OPEN(접수) → CLOSED(처리완료)로 전이된다. 환수 유발(chargebackTriggered=true) 민원은
* 처리 후 정산 배치(P5)가 실제 수수료 환수(chargeback)를 생성한다(여기서는 플래그만 관리).</p>
*
* <p>메뉴 COMPLAINT 권한으로 보호된다.</p>
*/
@Tag(name = "민원 관리")
@RestController
@RequestMapping("/api/complaints")
@@ -23,18 +32,21 @@ public class ComplaintController {
private final ComplaintService service;
/** 민원 목록 조회(페이징). READ 권한 필요. */
@GetMapping
@RequirePermission(menu = "COMPLAINT", perm = "READ")
public ApiResponse<PageResponse<ComplaintResp>> list(@ModelAttribute ComplaintSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 민원 단건 상세 조회. READ 권한 필요. */
@GetMapping("/{complaintId}")
@RequirePermission(menu = "COMPLAINT", perm = "READ")
public ApiResponse<ComplaintResp> detail(@PathVariable Long complaintId) {
return ApiResponse.ok(service.detail(complaintId));
}
/** 민원 신규 접수. 등록 시 OPEN 상태로 시작한다. CREATE 권한 필요. */
@PostMapping
@RequirePermission(menu = "COMPLAINT", perm = "CREATE")
@DataChangeLog(menu = "COMPLAINT", table = "complaint")
@@ -42,6 +54,7 @@ public class ComplaintController {
return ApiResponse.ok(service.create(req));
}
/** 민원 내용 수정. 상태는 변경하지 않는다(처리완료는 별도 엔드포인트). UPDATE 권한 필요. */
@PutMapping("/{complaintId}")
@RequirePermission(menu = "COMPLAINT", perm = "UPDATE")
@DataChangeLog(menu = "COMPLAINT", table = "complaint")
@@ -51,6 +64,7 @@ public class ComplaintController {
return ApiResponse.ok();
}
/** 민원 처리완료(status=CLOSED). 처리결과(resolution)를 기록하며 이미 완료된 건은 거부된다. UPDATE 권한 필요. */
@PostMapping("/{complaintId}/close")
@RequirePermission(menu = "COMPLAINT", perm = "UPDATE")
@DataChangeLog(menu = "COMPLAINT", table = "complaint")
@@ -60,6 +74,7 @@ public class ComplaintController {
return ApiResponse.ok();
}
/** 민원 레코드 삭제. DELETE 권한 필요. */
@DeleteMapping("/{complaintId}")
@RequirePermission(menu = "COMPLAINT", perm = "DELETE")
@DataChangeLog(menu = "COMPLAINT", table = "complaint")
@@ -15,6 +15,16 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 위촉계약 관리 API.
*
* <p>위촉계약(agent_contract)은 설계사와 보험사(carrier) 사이의 모집 위탁 계약이다.
* 어떤 설계사가 어떤 보험사의 상품을 팔 수 있는지를 정의하며, 유효기간(effectiveFrom~To)과
* 정착지원금(onboardingBonusAmount) 등을 포함한다. 상태는 ACTIVE(유효)에서 TERMINATED(해촉)로 전이된다.</p>
*
* <p>주의: 여기서 말하는 계약은 "설계사 위촉계약"이며, 고객이 가입한 "보험계약"과는 다르다.
* 메뉴 AGENT_CONTRACT 권한으로 보호된다.</p>
*/
@Tag(name = "위촉계약 관리")
@RestController
@RequestMapping("/api/agent-contracts")
@@ -23,18 +33,21 @@ public class AgentContractController {
private final AgentContractService service;
/** 위촉계약 목록 조회(페이징). READ 권한 필요. */
@GetMapping
@RequirePermission(menu = "AGENT_CONTRACT", perm = "READ")
public ApiResponse<PageResponse<AgentContractResp>> list(@ModelAttribute AgentContractSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 위촉계약 단건 상세 조회. READ 권한 필요. */
@GetMapping("/{contractId}")
@RequirePermission(menu = "AGENT_CONTRACT", perm = "READ")
public ApiResponse<AgentContractResp> detail(@PathVariable Long contractId) {
return ApiResponse.ok(service.detail(contractId));
}
/** 위촉계약 신규 등록. 등록 시 ACTIVE 상태로 시작한다. CREATE 권한 필요. */
@PostMapping
@RequirePermission(menu = "AGENT_CONTRACT", perm = "CREATE")
@DataChangeLog(menu = "AGENT_CONTRACT", table = "agent_contract")
@@ -42,6 +55,7 @@ public class AgentContractController {
return ApiResponse.ok(service.create(req));
}
/** 위촉계약 수정. 상태는 변경하지 않는다(해촉은 별도 엔드포인트). UPDATE 권한 필요. */
@PutMapping("/{contractId}")
@RequirePermission(menu = "AGENT_CONTRACT", perm = "UPDATE")
@DataChangeLog(menu = "AGENT_CONTRACT", table = "agent_contract")
@@ -51,6 +65,7 @@ public class AgentContractController {
return ApiResponse.ok();
}
/** 위촉계약 해촉(status=TERMINATED). 사유(reason)를 함께 기록하며 이미 해촉된 건은 거부된다. UPDATE 권한 필요. */
@PostMapping("/{contractId}/terminate")
@RequirePermission(menu = "AGENT_CONTRACT", perm = "UPDATE")
@DataChangeLog(menu = "AGENT_CONTRACT", table = "agent_contract")
@@ -60,6 +75,7 @@ public class AgentContractController {
return ApiResponse.ok();
}
/** 위촉계약 레코드 삭제. DELETE 권한 필요. */
@DeleteMapping("/{contractId}")
@RequirePermission(menu = "AGENT_CONTRACT", perm = "DELETE")
@DataChangeLog(menu = "AGENT_CONTRACT", table = "agent_contract")
@@ -13,6 +13,18 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 공제/차감(agent_deduction) 관리 컨트롤러.
*
* <p>"차감"은 설계사에게 줄 수수료에서 미리 빼야 할 금액(예: 대출 상환, 가지급금 회수 등)을 등록해 두는 것입니다.
* 여기서 PENDING(대기) 상태로 등록해 두면, 지급 처리 단계에서 실지급액(netAmount)에서 자동으로 차감됩니다.
*
* <p>상태 흐름(DeductionStatus): PENDING(대기) → APPLIED(적용완료), 또는 CANCELLED(취소).
* 수정·취소는 아직 적용(APPLIED)되지 않은 PENDING 건만 가능합니다.
* 같은 설계사+정산월+차감유형 조합은 중복 등록이 거부됩니다.
*
* <p>권한(menu=DEDUCTION): 조회 READ, 등록 CREATE, 수정/취소 UPDATE.
*/
@Tag(name = "공제 관리")
@RestController
@RequestMapping("/api/deductions")
@@ -21,18 +33,21 @@ public class DeductionController {
private final DeductionService deductionService;
/** 차감 목록 조회(검색 + 페이징). */
@GetMapping
@RequirePermission(menu = "DEDUCTION", perm = "READ")
public ApiResponse<PageResponse<AgentDeductionResp>> list(@ModelAttribute AgentDeductionSearchParam param) {
return ApiResponse.ok(deductionService.list(param));
}
/** 차감 단건 상세 조회. */
@GetMapping("/{deductionId}")
@RequirePermission(menu = "DEDUCTION", perm = "READ")
public ApiResponse<AgentDeductionResp> detail(@PathVariable Long deductionId) {
return ApiResponse.ok(deductionService.detail(deductionId));
}
/** 차감 등록(PENDING 상태). 동일 설계사+정산월+차감유형 중복은 거부됩니다. */
@PostMapping
@RequirePermission(menu = "DEDUCTION", perm = "CREATE")
@DataChangeLog(menu = "DEDUCTION", table = "agent_deduction")
@@ -40,6 +55,7 @@ public class DeductionController {
return ApiResponse.ok(deductionService.create(req));
}
/** 차감 금액·설명 수정. PENDING 상태에서만 가능합니다. */
@PutMapping("/{deductionId}")
@RequirePermission(menu = "DEDUCTION", perm = "UPDATE")
@DataChangeLog(menu = "DEDUCTION", table = "agent_deduction")
@@ -49,6 +65,7 @@ public class DeductionController {
return ApiResponse.ok();
}
/** 차감 취소(→ CANCELLED). 이미 적용(APPLIED)된 차감은 취소할 수 없습니다. */
@PutMapping("/{deductionId}/cancel")
@RequirePermission(menu = "DEDUCTION", perm = "UPDATE")
@DataChangeLog(menu = "DEDUCTION", table = "agent_deduction")
@@ -16,6 +16,13 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 데이터 사전(Data Dictionary) 관리 API.
*
* <p>DB 테이블/컬럼의 의미·도메인 코드 매핑 등을 문서화해 관리하는 메타데이터 도구다.
* 각 항목(dictId)은 특정 테이블의 컬럼을 설명하며, 도메인 코드(domainCode)와 연결될 수 있다.
* 메뉴 SYSTEM_DATA_DICT 권한으로 보호된다.</p>
*/
@Tag(name = "데이터 사전")
@RestController
@RequestMapping("/api/data-dictionary")
@@ -24,18 +31,21 @@ public class DataDictionaryController {
private final DataDictionaryService dataDictionaryService;
/** 데이터 사전 항목 목록 조회(페이징). READ 권한 필요. */
@GetMapping
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "READ")
public ApiResponse<PageResponse<DataDictionaryResp>> list(@ModelAttribute DataDictionarySearchParam param) {
return ApiResponse.ok(dataDictionaryService.list(param));
}
/** 사전에 등록된 테이블명 목록 조회. READ 권한 필요. */
@GetMapping("/tables")
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "READ")
public ApiResponse<List<String>> tables() {
return ApiResponse.ok(dataDictionaryService.tables());
}
/** 사전 뷰 기반 통합 조회(테이블명 선택 필터). 컬럼+도메인 결합 정보를 반환한다. READ 권한 필요. */
@GetMapping("/full")
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "READ")
public ApiResponse<List<DataDictionaryFullResp>> full(
@@ -43,12 +53,14 @@ public class DataDictionaryController {
return ApiResponse.ok(dataDictionaryService.full(tableName));
}
/** 데이터 사전 항목 단건 조회. READ 권한 필요. */
@GetMapping("/{dictId}")
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "READ")
public ApiResponse<DataDictionaryResp> detail(@PathVariable Long dictId) {
return ApiResponse.ok(dataDictionaryService.detail(dictId));
}
/** 데이터 사전 항목 등록. 도메인 코드를 지정하면 실존 여부를 검증한다. CREATE 권한 필요. */
@PostMapping
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "CREATE")
@DataChangeLog(menu = "SYSTEM_DATA_DICT", table = "data_dictionary")
@@ -57,6 +69,7 @@ public class DataDictionaryController {
return ApiResponse.ok();
}
/** 데이터 사전 항목 수정. UPDATE 권한 필요. */
@PutMapping("/{dictId}")
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "UPDATE")
@DataChangeLog(menu = "SYSTEM_DATA_DICT", table = "data_dictionary")
@@ -66,6 +79,7 @@ public class DataDictionaryController {
return ApiResponse.ok();
}
/** 데이터 사전 항목 삭제. DELETE 권한 필요. */
@DeleteMapping("/{dictId}")
@RequirePermission(menu = "SYSTEM_DATA_DICT", perm = "DELETE")
@DataChangeLog(menu = "SYSTEM_DATA_DICT", table = "data_dictionary")
@@ -16,6 +16,13 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 데이터 도메인 사전 관리 API.
*
* <p>도메인(domain)은 컬럼들이 공통으로 따르는 데이터 타입/형식 규칙의 정의다(예: 금액형, 날짜형 등).
* 데이터 사전(data_dictionary)의 컬럼들이 이 도메인 코드를 참조한다. 식별자는 문자열 도메인 코드(domainCode)다.
* 메뉴 SYSTEM_DATA_DOMAIN 권한으로 보호된다.</p>
*/
@Tag(name = "데이터 도메인 사전")
@RestController
@RequestMapping("/api/data-domains")
@@ -24,24 +31,28 @@ public class DataDomainController {
private final DataDomainService dataDomainService;
/** 도메인 목록 조회(페이징). READ 권한 필요. */
@GetMapping
@RequirePermission(menu = "SYSTEM_DATA_DOMAIN", perm = "READ")
public ApiResponse<PageResponse<DataDomainResp>> list(@ModelAttribute DataDomainSearchParam param) {
return ApiResponse.ok(dataDomainService.list(param));
}
/** 선택 옵션용 도메인 전체 목록(코드/명칭 등 경량 정보). READ 권한 필요. */
@GetMapping("/options")
@RequirePermission(menu = "SYSTEM_DATA_DOMAIN", perm = "READ")
public ApiResponse<List<DataDomainOptionResp>> options() {
return ApiResponse.ok(dataDomainService.options());
}
/** 도메인 단건 상세 조회(도메인 코드 기준). READ 권한 필요. */
@GetMapping("/{domainCode}")
@RequirePermission(menu = "SYSTEM_DATA_DOMAIN", perm = "READ")
public ApiResponse<DataDomainResp> detail(@PathVariable String domainCode) {
return ApiResponse.ok(dataDomainService.detail(domainCode));
}
/** 도메인 등록. 코드 중복이면 거부된다. CREATE 권한 필요. */
@PostMapping
@RequirePermission(menu = "SYSTEM_DATA_DOMAIN", perm = "CREATE")
@DataChangeLog(menu = "SYSTEM_DATA_DOMAIN", table = "data_domain")
@@ -50,6 +61,7 @@ public class DataDomainController {
return ApiResponse.ok();
}
/** 도메인 수정. UPDATE 권한 필요. */
@PutMapping("/{domainCode}")
@RequirePermission(menu = "SYSTEM_DATA_DOMAIN", perm = "UPDATE")
@DataChangeLog(menu = "SYSTEM_DATA_DOMAIN", table = "data_domain")
@@ -59,6 +71,7 @@ public class DataDomainController {
return ApiResponse.ok();
}
/** 도메인 삭제. 이 도메인을 참조하는 컬럼이 있으면 삭제 불가(서비스에서 검증). DELETE 권한 필요. */
@DeleteMapping("/{domainCode}")
@RequirePermission(menu = "SYSTEM_DATA_DOMAIN", perm = "DELETE")
@DataChangeLog(menu = "SYSTEM_DATA_DOMAIN", table = "data_domain")
@@ -13,6 +13,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 계약 변경(배서, Endorsement) 관리 API.
*
* <p>배서란 이미 체결된 계약의 내용을 바꾸는 것을 말한다(예: 보험료 증감, 피보험자/수익자 변경, 담보 추가 등).
* 변경 유형(endorsementType)과 변경 전/후 값(oldValue/newValue)을 기록하며,
* 보험료가 바뀌어 수수료 재계산이 필요한 경우(requiresCommissionRecalc) 정산 배치가 이를 반영한다.</p>
*
* <p>메뉴 ENDORSE 권한으로 보호된다.</p>
*/
@Tag(name = "계약 변경")
@RestController
@RequestMapping("/api/contract-endorsements")
@@ -21,6 +30,7 @@ public class ContractEndorsementController {
private final ContractEndorsementService service;
/** 배서 목록 조회(페이징). READ 권한 필요. */
@GetMapping
@RequirePermission(menu = "ENDORSE", perm = "READ")
public ApiResponse<PageResponse<ContractEndorsementResp>> list(
@@ -28,12 +38,14 @@ public class ContractEndorsementController {
return ApiResponse.ok(service.list(param));
}
/** 배서 단건 상세 조회. READ 권한 필요. */
@GetMapping("/{endorsementId}")
@RequirePermission(menu = "ENDORSE", perm = "READ")
public ApiResponse<ContractEndorsementResp> detail(@PathVariable Long endorsementId) {
return ApiResponse.ok(service.detail(endorsementId));
}
/** 배서 등록. 등록 직후엔 미처리(processed=false) 상태다. CREATE 권한 필요. */
@PostMapping
@RequirePermission(menu = "ENDORSE", perm = "CREATE")
@DataChangeLog(menu = "ENDORSE", table = "contract_endorsement")
@@ -41,6 +53,7 @@ public class ContractEndorsementController {
return ApiResponse.ok(service.create(req));
}
/** 배서 처리 완료 표시(processed=true). 실제 수수료 재계산은 배치(P5)가 담당한다. UPDATE 권한 필요. */
@PostMapping("/{endorsementId}/process")
@RequirePermission(menu = "ENDORSE", perm = "UPDATE")
@DataChangeLog(menu = "ENDORSE", table = "contract_endorsement")
@@ -49,6 +62,7 @@ public class ContractEndorsementController {
return ApiResponse.ok();
}
/** 배서 레코드 삭제. DELETE 권한 필요. */
@DeleteMapping("/{endorsementId}")
@RequirePermission(menu = "ENDORSE", perm = "DELETE")
@DataChangeLog(menu = "ENDORSE", table = "contract_endorsement")
@@ -10,6 +10,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 외부연동 호출 이력 조회 API.
*
* <p>SMS/알림톡, 펌뱅킹, 푸시 등 외부 시스템 호출 결과를 감사용으로 기록한 이력을 조회한다(읽기 전용).
* 이력 적재 자체는 AOP가 자동 수행하며, 여기서는 운영/장애 추적용 조회만 제공한다.
* 메뉴 SYSTEM_EXTERNAL_CALL 권한으로 보호된다.</p>
*/
@Tag(name = "외부연동 호출이력")
@RestController
@RequiredArgsConstructor
@@ -17,12 +24,14 @@ public class ExternalCallLogController {
private final ExternalCallLogService service;
/** 외부 호출 이력 목록 조회(페이징). READ 권한 필요. */
@GetMapping("/api/external-call-logs")
@RequirePermission(menu = "SYSTEM_EXTERNAL_CALL", perm = "READ")
public ApiResponse<PageResponse<ExternalCallLogResp>> list(@ModelAttribute ExternalCallLogSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 외부 호출 이력 단건 상세 조회. READ 권한 필요. */
@GetMapping("/api/external-call-logs/{logId}")
@RequirePermission(menu = "SYSTEM_EXTERNAL_CALL", perm = "READ")
public ApiResponse<ExternalCallLogResp> detail(@PathVariable Long logId) {
@@ -12,6 +12,14 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* [연간 사업소득 API]
*
* 신입 개발자 안내:
* - 설계사별로 한 해(연도) 동안의 총 사업소득과 떼인 원천세 등을 합산해 보관하는 자료다(연말정산/지급명세서의 기초).
* - 월별 지급/정산 데이터를 연 단위로 집계(regenerate)하여 agent_annual_income 에 UPSERT 한다.
* - 상세는 (설계사ID, 정산연도) 조합으로 1건을 조회한다.
*/
@Tag(name = "연간 사업소득")
@RestController
@RequestMapping("/api/agent-annual-incomes")
@@ -37,6 +45,7 @@ public class AgentAnnualIncomeController {
return ApiResponse.ok(service.detail(agentId, settleYear));
}
// 지정 연도의 모든 설계사 연간소득을 다시 집계(UPSERT). 월별 데이터가 바뀌었을 때 다시 돌린다.
@PostMapping("/regenerate")
@Operation(summary = "연도 전체 재집계")
@RequirePermission(menu = "ANNUAL_INCOME", perm = "EXECUTE")
@@ -18,6 +18,15 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* [연말 지급명세서 API]
*
* 신입 개발자 안내:
* - 연말정산을 위해 "한 해 동안 이 설계사에게 얼마를 지급하고 세금을 얼마 뗐는지"를 국세청 제출용으로 정리한 명세서다.
* - 데이터 출처: 연간 사업소득(agent_annual_income) 집계 결과를 기반으로 한다.
* - 흐름: regenerate(연도별 UPSERT 생성) → generate(단건 파일 발급) → send(설계사에게 발송 처리) → export(엑셀 다운로드).
* - export 는 데이터가 많을 수 있어 Cursor 스트리밍(ExcelService.exportLargeExcel)으로 내려보낸다.
*/
@Tag(name = "연말 지급명세서")
@RestController
@RequestMapping("/api/year-end-statements")
@@ -68,6 +77,7 @@ public class YearEndStatementController {
return ApiResponse.ok();
}
// 대용량 엑셀 다운로드: 조회 결과를 Cursor 로 스트리밍하며 한 번에 메모리에 올리지 않고 파일로 내려쓴다.
@GetMapping("/export")
@Operation(summary = "엑셀 다운로드")
@RequirePermission(menu = "YEAR_END_STMT", perm = "EXPORT")
@@ -7,18 +7,27 @@ import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 분급(分給) 계획 생성 요청 DTO.
* 분급이란 총수수료를 한 번에 주지 않고 여러 회차로 나눠 지급하는 것으로,
* 이 요청을 받아 회차별 지급 스케줄(installment_plan)을 자동 생성한다.
*/
@Data
public class GeneratePlanReq {
/** 분급 대상 계약 ID. */
@NotNull
private Long contractId;
/** 나눠서 지급할 총수수료 금액(양수). 분급 비율에 따라 회차별로 쪼개진다. */
@NotNull
@Positive
private BigDecimal totalCommission;
/** 계약일. 회차별 지급 예정월 계산의 기준이 된다. */
@NotNull
private LocalDate contractDate;
/** 상품 분류. 분류에 따라 적용할 분급 비율표(installment_ratio)가 달라진다. */
private String productCategory;
}
@@ -15,6 +15,10 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 분급(分給) 계획 관리 REST 컨트롤러.
* 총수수료를 여러 회차로 나눠 지급하는 분급 계획(installment_plan)의 조회·생성·지급/이연/취소를 처리한다.
*/
@Tag(name = "분급 계획 관리")
@RestController
@RequestMapping("/api/installment-plans")
@@ -23,18 +27,21 @@ public class InstallmentController {
private final InstallmentService installmentService;
/** 분급 계획(회차) 목록 페이징 조회. */
@GetMapping
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "READ")
public ApiResponse<PageResponse<InstallmentPlanResp>> list(InstallmentPlanSearchParam param) {
return ApiResponse.ok(installmentService.list(param));
}
/** 분급 계획 단건 상세 조회. */
@GetMapping("/{planId}")
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "READ")
public ApiResponse<InstallmentPlanResp> detail(@PathVariable Long planId) {
return ApiResponse.ok(installmentService.detail(planId));
}
/** 분급 계획 생성. 총수수료를 분급 비율표에 따라 회차별 지급 스케줄로 자동 분해한다. */
@PostMapping("/generate")
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "CREATE")
@DataChangeLog(menu = "INSTALLMENT_PLAN", table = "installment_plan")
@@ -48,6 +55,7 @@ public class InstallmentController {
return ApiResponse.ok();
}
/** 특정 회차 지급 완료 처리(지급건 ID와 지급액 기록). */
@PutMapping("/{planId}/pay")
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "UPDATE")
@DataChangeLog(menu = "INSTALLMENT_PLAN", table = "installment_plan")
@@ -57,6 +65,7 @@ public class InstallmentController {
return ApiResponse.ok();
}
/** 특정 회차 지급 이연(다음 기회로 미룸) 처리. */
@PutMapping("/{planId}/defer")
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "UPDATE")
@DataChangeLog(menu = "INSTALLMENT_PLAN", table = "installment_plan")
@@ -65,6 +74,7 @@ public class InstallmentController {
return ApiResponse.ok();
}
/** 특정 회차 지급 취소(예: 계약 해지로 향후 분급 중단). */
@PutMapping("/{planId}/cancel")
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "UPDATE")
@DataChangeLog(menu = "INSTALLMENT_PLAN", table = "installment_plan")
@@ -73,6 +83,7 @@ public class InstallmentController {
return ApiResponse.ok();
}
/** 특정 정산월(settleMonth)에 지급 예정인 분급 회차 목록 조회. 월 정산 배치가 이 목록을 사용한다. */
@GetMapping("/scheduled")
@RequirePermission(menu = "INSTALLMENT_PLAN", perm = "READ")
public ApiResponse<List<InstallmentPlanVO>> scheduled(@RequestParam String settleMonth) {
@@ -13,6 +13,11 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 분급 비율(installment_ratio) 관리 REST 컨트롤러.
* 분급 비율표는 "총수수료를 몇 회차에 걸쳐, 각 회차 몇 %씩 지급할지"를 정의하는 기준표이며,
* 분급 계획(installment_plan) 생성 시 이 비율이 적용된다.
*/
@Tag(name = "분급 비율 관리")
@RestController
@RequestMapping("/api/installment-ratios")
@@ -21,12 +26,14 @@ public class InstallmentRatioController {
private final InstallmentService installmentService;
/** 분급 비율표 목록 페이징 조회. */
@GetMapping
@RequirePermission(menu = "INSTALLMENT_RATIO", perm = "READ")
public ApiResponse<PageResponse<InstallmentRatioResp>> list(InstallmentRatioSearchParam param) {
return ApiResponse.ok(installmentService.listRatios(param));
}
/** 분급 비율표 신규 등록. */
@PostMapping
@RequirePermission(menu = "INSTALLMENT_RATIO", perm = "CREATE")
@DataChangeLog(menu = "INSTALLMENT_RATIO", table = "installment_ratio")
@@ -6,12 +6,18 @@ import lombok.Data;
import java.math.BigDecimal;
/**
* 분급 회차 지급 처리 요청 DTO.
* 특정 회차(plan)를 실제로 지급 완료 처리할 때, 어떤 지급건(paymentId)으로 얼마(paidAmount)를 지급했는지 전달한다.
*/
@Data
public class PayPlanReq {
/** 실제 지급이 일어난 지급(payment) 레코드 ID. */
@NotNull
private Long paymentId;
/** 이번 회차에 지급한 금액(양수). */
@NotNull
@Positive
private BigDecimal paidAmount;
@@ -21,6 +21,7 @@ public class BankRetryController {
private final WithdrawBankRetryService bankRetryService;
/** 펌뱅킹 이체 실패(FAILED) 건을 즉시 1회 재시도하고 처리 건수를 반환한다. WITHDRAW EXECUTE 권한 필요. */
@PostMapping
@RequirePermission(menu = "WITHDRAW", perm = "EXECUTE")
public ApiResponse<Integer> retry() {
@@ -21,6 +21,7 @@ public class BankSettleController {
private final WithdrawBankSettleService bankSettleService;
/** 펌뱅킹 입금 확정(SETTLED) 상태를 즉시 1회 동기화하고 처리 건수를 반환한다. WITHDRAW EXECUTE 권한 필요. */
@PostMapping
@RequirePermission(menu = "WITHDRAW", perm = "EXECUTE")
public ApiResponse<Integer> sync() {
@@ -14,6 +14,10 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 차월(다음 달) KPI 예측 REST 컨트롤러.
* 과거 실적 데이터를 이동평균(SMA) 등으로 분석해 다음 달 실적 KPI를 미리 예측·관리한다.
*/
@Tag(name = "차월 KPI 예측")
@RestController
@RequestMapping("/api/forecast-kpi")
@@ -22,6 +26,7 @@ public class ForecastKpiMonthlyController {
private final ForecastKpiMonthlyService service;
/** 예측값 목록 페이징 조회. */
@GetMapping
@Operation(summary = "예측 목록")
@RequirePermission(menu = "FORECAST_KPI", perm = "READ")
@@ -29,6 +34,7 @@ public class ForecastKpiMonthlyController {
return ApiResponse.ok(service.list(p));
}
/** 차월 예측 재산출(실적 변동 시 다시 계산). 갱신된 행 수를 반환한다. */
@PostMapping("/regenerate")
@Operation(summary = "차월 예측 재산출")
@RequirePermission(menu = "FORECAST_KPI", perm = "EXECUTE")
@@ -16,6 +16,14 @@ import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* KPI 대시보드 REST 컨트롤러.
* 실적 집계(KPI)를 여러 관점으로 조회하는 읽기 전용 화면용 API다.
* - monthly-summary : 월별 실적 요약
* - org-summary : 조직(지점/본부)별 실적 요약
* - retention : 계약 유지율 지표
* - cumulative : 누적 실적 추이
*/
@Tag(name = "KPI 대시보드")
@RestController
@RequestMapping("/api/kpi")
@@ -24,24 +32,28 @@ public class KpiController {
private final KpiService service;
/** 월별 실적 요약 조회. */
@GetMapping("/monthly-summary")
@RequirePermission(menu = "KPI_MONTHLY", perm = "READ")
public ApiResponse<PageResponse<KpiMonthlySummaryResp>> monthlySummary(@ModelAttribute KpiSearchParam param) {
return ApiResponse.ok(service.monthlySummary(param));
}
/** 조직별 실적 요약 조회. */
@GetMapping("/org-summary")
@RequirePermission(menu = "KPI_ORG", perm = "READ")
public ApiResponse<PageResponse<KpiOrgSummaryResp>> orgSummary(@ModelAttribute KpiSearchParam param) {
return ApiResponse.ok(service.orgSummary(param));
}
/** 계약 유지율 지표 조회. */
@GetMapping("/retention")
@RequirePermission(menu = "KPI_RETENTION", perm = "READ")
public ApiResponse<PageResponse<KpiRetentionResp>> retention(@ModelAttribute KpiSearchParam param) {
return ApiResponse.ok(service.retention(param));
}
/** 누적 실적 추이 조회. */
@GetMapping("/cumulative")
@RequirePermission(menu = "KPI_CUMULATIVE", perm = "READ")
public ApiResponse<PageResponse<KpiCumulativeResp>> cumulative(@ModelAttribute KpiSearchParam param) {
@@ -15,6 +15,11 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 유지율 이상치(retention anomaly) 알림 REST 컨트롤러.
* 특정 조직/설계사의 계약 유지율이 평소 대비 비정상적으로 떨어진 경우를 탐지해 알림으로 관리한다.
* (유지율 급락은 부실모집/불완전판매 신호일 수 있어 모니터링 대상이다)
*/
@Tag(name = "유지율 이상치")
@RestController
@RequestMapping("/api/retention-anomalies")
@@ -23,6 +28,7 @@ public class RetentionAnomalyAlertController {
private final RetentionAnomalyAlertService service;
/** 이상치 알림 목록 페이징 조회. */
@GetMapping
@Operation(summary = "이상치 목록")
@RequirePermission(menu = "RETENTION_ANOMALY", perm = "READ")
@@ -30,6 +36,7 @@ public class RetentionAnomalyAlertController {
return ApiResponse.ok(service.list(p));
}
/** 이상치 알림 단건 상세 조회. */
@GetMapping("/{id}")
@Operation(summary = "이상치 단건 상세")
@RequirePermission(menu = "RETENTION_ANOMALY", perm = "READ")
@@ -37,6 +44,7 @@ public class RetentionAnomalyAlertController {
return ApiResponse.ok(service.detail(id));
}
/** 지정한 월(alertMonth)에 대해 이상치를 다시 탐지·저장한다. 새로 탐지된 건수를 반환. */
@PostMapping("/detect")
@Operation(summary = "이상치 재탐지 (월 단위)")
@RequirePermission(menu = "RETENTION_ANOMALY", perm = "UPDATE")
@@ -45,6 +53,7 @@ public class RetentionAnomalyAlertController {
return ApiResponse.ok(service.detect(body.get("alertMonth")));
}
/** 탐지된 이상치를 검토/완료 상태로 처리(담당자 확인 결과 기록). */
@PutMapping("/review")
@Operation(summary = "이상치 검토/완료 처리")
@RequirePermission(menu = "RETENTION_ANOMALY", perm = "UPDATE")
@@ -15,6 +15,14 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 수수료 원장(ledger) 관리 REST 컨트롤러.
* 원장은 설계사에게 지급된/지급될 수수료 내역을 종류별로 기록하는 장부다.
* - 모집(recruit) : 신계약 체결로 발생한 모집수수료 원장
* - 유지(maintain) : 계약 유지에 따라 매월 발생하는 유지수수료 원장
* - 예외(exception) : 정상 계산에 안 잡히는 수기 조정 수수료(승인 절차 필요)
* 조회/상세 외에 대용량 엑셀 다운로드(exportLargeExcel: 커서 스트리밍)를 제공한다.
*/
@Tag(name = "원장 관리")
@RestController
@RequestMapping("/api/ledger")
@@ -24,7 +32,7 @@ public class LedgerController {
private final LedgerService service;
private final ExcelService excelService;
// ===== 모집 =====
// ===== 모집수수료 원장 =====
@GetMapping("/recruit")
@RequirePermission(menu = "LEDGER_RECRUIT", perm = "READ")
public ApiResponse<PageResponse<LedgerResp>> listRecruit(@ModelAttribute LedgerSearchParam param) {
@@ -37,6 +45,7 @@ public class LedgerController {
return ApiResponse.ok(service.detailRecruit(ledgerId));
}
/** 모집수수료 원장 엑셀 다운로드. 대용량 대비 커서로 한 건씩 흘려보내 메모리 폭증을 막는다. */
@GetMapping("/recruit/export")
@RequirePermission(menu = "LEDGER_RECRUIT", perm = "EXPORT")
public void exportRecruit(@ModelAttribute LedgerSearchParam param, HttpServletResponse res) {
@@ -44,7 +53,7 @@ public class LedgerController {
() -> service.exportRecruitCursor(param), res);
}
// ===== 유지 =====
// ===== 유지수수료 원장 =====
@GetMapping("/maintain")
@RequirePermission(menu = "LEDGER_MAINTAIN", perm = "READ")
public ApiResponse<PageResponse<LedgerResp>> listMaintain(@ModelAttribute LedgerSearchParam param) {
@@ -57,6 +66,7 @@ public class LedgerController {
return ApiResponse.ok(service.detailMaintain(ledgerId));
}
/** 유지수수료 원장 엑셀 다운로드(커서 스트리밍). */
@GetMapping("/maintain/export")
@RequirePermission(menu = "LEDGER_MAINTAIN", perm = "EXPORT")
public void exportMaintain(@ModelAttribute LedgerSearchParam param, HttpServletResponse res) {
@@ -64,7 +74,7 @@ public class LedgerController {
() -> service.exportMaintainCursor(param), res);
}
// ===== 예외 =====
// ===== 예외수수료 원장(수기 등록 + 승인) =====
@GetMapping("/exception")
@RequirePermission(menu = "LEDGER_EXCEPTION", perm = "READ")
public ApiResponse<PageResponse<ExceptionLedgerResp>> listException(@ModelAttribute LedgerSearchParam param) {
@@ -77,6 +87,7 @@ public class LedgerController {
return ApiResponse.ok(service.detailException(ledgerId));
}
/** 예외수수료 원장 수기 등록. 등록 직후에는 승인대기(PENDING) 상태다. */
@PostMapping("/exception")
@RequirePermission(menu = "LEDGER_EXCEPTION", perm = "CREATE")
@DataChangeLog(menu = "LEDGER_EXCEPTION", table = "exception_ledger")
@@ -84,6 +95,7 @@ public class LedgerController {
return ApiResponse.ok(service.createException(req));
}
/** 예외수수료 원장 승인/반려 처리(body 의 status: APPROVED 또는 REJECTED). */
@PutMapping("/exception/{ledgerId}/approve")
@RequirePermission(menu = "LEDGER_EXCEPTION", perm = "APPROVE")
@DataChangeLog(menu = "LEDGER_EXCEPTION", table = "exception_ledger")
@@ -15,6 +15,13 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 설계사 자격(License) 관리 API.
*
* <p>보험설계사는 판매 가능한 상품군별로 자격(생명/손해 등 licenseType)을 보유해야 한다.
* 각 자격은 발급일(issuedDate)~만료일(expiresDate)을 가지며 갱신(renewedFrom) 이력이 연결될 수 있다.
* 만료 임박 자격을 조회해 갱신을 독려하는 기능이 핵심이다. 메뉴 AGENT_LICENSE 권한으로 보호된다.</p>
*/
@Tag(name = "자격 관리")
@RestController
@RequestMapping("/api/agent-licenses")
@@ -23,18 +30,21 @@ public class AgentLicenseController {
private final AgentLicenseService service;
/** 자격 목록 조회(페이징). READ 권한 필요. */
@GetMapping
@RequirePermission(menu = "AGENT_LICENSE", perm = "READ")
public ApiResponse<PageResponse<AgentLicenseResp>> list(@ModelAttribute AgentLicenseSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 자격 단건 상세 조회. READ 권한 필요. */
@GetMapping("/{licenseId}")
@RequirePermission(menu = "AGENT_LICENSE", perm = "READ")
public ApiResponse<AgentLicenseResp> detail(@PathVariable Long licenseId) {
return ApiResponse.ok(service.detail(licenseId));
}
/** 만료 임박 자격 조회(기본 30일 이내). 갱신 알림/관리에 사용한다. READ 권한 필요. */
@GetMapping("/expiring")
@RequirePermission(menu = "AGENT_LICENSE", perm = "READ")
public ApiResponse<List<AgentLicenseResp>> expiring(
@@ -42,6 +52,7 @@ public class AgentLicenseController {
return ApiResponse.ok(service.expiring(days));
}
/** 자격 등록. 등록 시 ACTIVE 상태로 시작한다. CREATE 권한 필요. */
@PostMapping
@RequirePermission(menu = "AGENT_LICENSE", perm = "CREATE")
@DataChangeLog(menu = "AGENT_LICENSE", table = "agent_license")
@@ -49,6 +60,7 @@ public class AgentLicenseController {
return ApiResponse.ok(service.create(req));
}
/** 자격 수정. UPDATE 권한 필요. */
@PutMapping("/{licenseId}")
@RequirePermission(menu = "AGENT_LICENSE", perm = "UPDATE")
@DataChangeLog(menu = "AGENT_LICENSE", table = "agent_license")
@@ -58,6 +70,7 @@ public class AgentLicenseController {
return ApiResponse.ok();
}
/** 자격 레코드 삭제. DELETE 권한 필요. */
@DeleteMapping("/{licenseId}")
@RequirePermission(menu = "AGENT_LICENSE", perm = "DELETE")
@DataChangeLog(menu = "AGENT_LICENSE", table = "agent_license")
@@ -38,22 +38,26 @@ public class MobileMeController {
private final MobileMeService service;
/** 내 홈 요약(명세서 수/출금 신청 수/미읽음 알림 수). */
@GetMapping("/summary")
public ApiResponse<MeSummary> summary() {
return ApiResponse.ok(service.summary());
}
/** 내 수수료 명세서 목록(서버가 본인 agentId로 강제 필터). */
@GetMapping("/statements")
public ApiResponse<PageResponse<CommissionStatementResp>> statements(
@ModelAttribute CommissionStatementSearchParam param) {
return ApiResponse.ok(service.statements(param));
}
/** 내 명세서 단건 상세. 본인 소유가 아니면 FORBIDDEN. */
@GetMapping("/statements/{statementId}")
public ApiResponse<CommissionStatementResp> statementDetail(@PathVariable Long statementId) {
return ApiResponse.ok(service.statementDetail(statementId));
}
/** 내 명세서 엑셀 다운로드. 본인 소유 검증 후 파일 스트림을 내려준다. */
@GetMapping("/statements/{statementId}/download")
public void downloadStatement(@PathVariable Long statementId, HttpServletResponse response) throws IOException {
byte[] content = service.downloadStatement(statementId);
@@ -65,33 +69,39 @@ public class MobileMeController {
response.getOutputStream().write(content);
}
/** 출금 신청 가능한 내 정산 마스터 목록(출금 폼 드롭다운용). */
@GetMapping("/settle-masters")
public ApiResponse<List<SettleMasterResp>> withdrawableSettleMasters() {
return ApiResponse.ok(service.withdrawableSettleMasters());
}
/** 특정 정산 마스터의 출금 가능 잔액 조회(출금 폼 미리 표시용). 본인 소유 검증 포함. */
@GetMapping("/settle-masters/{settleMasterId}/withdrawable-balance")
public ApiResponse<WithdrawableBalance> withdrawableBalance(@PathVariable Long settleMasterId) {
return ApiResponse.ok(service.withdrawableBalance(settleMasterId));
}
/** 내 출금 신청 목록(서버가 본인 agentId로 강제 필터). */
@GetMapping("/withdraw-requests")
public ApiResponse<PageResponse<WithdrawRequestResp>> withdrawRequests(
@ModelAttribute WithdrawRequestSearchParam param) {
return ApiResponse.ok(service.withdrawRequests(param));
}
/** 출금 신청 등록. agentId/신청자는 토큰에서 강제 결정하고 잔액 한도를 검증한다. */
@PostMapping("/withdraw-requests")
public ApiResponse<Long> createWithdrawRequest(@Valid @RequestBody MobileWithdrawSaveReq req) {
return ApiResponse.ok(service.createWithdrawRequest(req));
}
/** 내 출금 신청 취소. 본인 소유 검증 후 취소한다. */
@PostMapping("/withdraw-requests/{requestId}/cancel")
public ApiResponse<Void> cancelWithdrawRequest(@PathVariable Long requestId) {
service.cancelMyWithdrawRequest(requestId);
return ApiResponse.ok();
}
/** 내 전화번호 직접 수정(관리자 거치지 않음). */
@PutMapping("/profile/phone")
public ApiResponse<Void> updateMyPhone(@RequestBody PhoneReq req) {
service.updateMyPhone(req.getPhone());
@@ -103,18 +113,21 @@ public class MobileMeController {
private String phone;
}
/** 내 알림 목록(서버가 본인 userId로 강제 필터). */
@GetMapping("/notifications")
public ApiResponse<PageResponse<UserNotificationResp>> notifications(
@ModelAttribute UserNotificationSearchParam param) {
return ApiResponse.ok(service.notifications(param));
}
/** 내 알림 단건 읽음 처리. */
@PostMapping("/notifications/{notificationId}/read")
public ApiResponse<Void> markRead(@PathVariable Long notificationId) {
service.markNotificationRead(notificationId);
return ApiResponse.ok();
}
/** 내 알림 전체 읽음 처리(처리 건수 반환). */
@PostMapping("/notifications/read-all")
public ApiResponse<Integer> markAllRead() {
return ApiResponse.ok(service.markAllNotificationsRead());
@@ -14,6 +14,16 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 공지사항 및 사용자 알림 API.
*
* <p>두 가지 도메인을 함께 다룬다.
* <ul>
* <li>공지사항(notice): 관리자가 등록하는 게시물. 기간(startDate~endDate)과 활성여부로 노출을 제어한다.</li>
* <li>사용자 알림(user-notification): 개인에게 발송되는 알림. 읽음 처리/미읽음 개수 조회 등을 제공한다.</li>
* </ul>
* 모두 메뉴 NOTICE 권한으로 보호된다.</p>
*/
@Tag(name = "공지사항 & 알림")
@RestController
@RequiredArgsConstructor
@@ -23,24 +33,28 @@ public class NoticeController {
// ── Notice ────────────────────────────────────────────────────────────────
/** 공지사항 목록 조회(페이징). READ 권한 필요. */
@GetMapping("/api/notices")
@RequirePermission(menu = "NOTICE", perm = "READ")
public ApiResponse<PageResponse<NoticeResp>> list(@ModelAttribute NoticeSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 현재 노출 기간이고 활성 상태인 공지만 조회. READ 권한 필요. */
@GetMapping("/api/notices/active")
@RequirePermission(menu = "NOTICE", perm = "READ")
public ApiResponse<List<NoticeResp>> activeList() {
return ApiResponse.ok(service.activeList());
}
/** 공지사항 단건 상세 조회. READ 권한 필요. */
@GetMapping("/api/notices/{noticeId}")
@RequirePermission(menu = "NOTICE", perm = "READ")
public ApiResponse<NoticeResp> detail(@PathVariable Long noticeId) {
return ApiResponse.ok(service.detail(noticeId));
}
/** 공지사항 등록. 등록 시 활성(isActive=true) 상태로 시작한다. CREATE 권한 필요. */
@PostMapping("/api/notices")
@RequirePermission(menu = "NOTICE", perm = "CREATE")
@DataChangeLog(menu = "NOTICE", table = "notice")
@@ -48,6 +62,7 @@ public class NoticeController {
return ApiResponse.ok(service.create(req));
}
/** 공지사항 수정. UPDATE 권한 필요. */
@PutMapping("/api/notices/{noticeId}")
@RequirePermission(menu = "NOTICE", perm = "UPDATE")
@DataChangeLog(menu = "NOTICE", table = "notice")
@@ -57,6 +72,7 @@ public class NoticeController {
return ApiResponse.ok();
}
/** 공지사항 삭제. DELETE 권한 필요. */
@DeleteMapping("/api/notices/{noticeId}")
@RequirePermission(menu = "NOTICE", perm = "DELETE")
@DataChangeLog(menu = "NOTICE", table = "notice")
@@ -67,6 +83,7 @@ public class NoticeController {
// ── UserNotification ──────────────────────────────────────────────────────
/** 사용자 알림 목록 조회(페이징). READ 권한 필요. */
@GetMapping("/api/user-notifications")
@RequirePermission(menu = "NOTICE", perm = "READ")
public ApiResponse<PageResponse<UserNotificationResp>> listNotifications(
@@ -74,6 +91,7 @@ public class NoticeController {
return ApiResponse.ok(service.listNotifications(param));
}
/** 미읽음 알림 개수 조회. userId 미지정 시 현재 로그인 사용자 기준. READ 권한 필요. */
@GetMapping("/api/user-notifications/unread-count")
@RequirePermission(menu = "NOTICE", perm = "READ")
public ApiResponse<Integer> unreadCount(@RequestParam(required = false) Long userId) {
@@ -81,6 +99,7 @@ public class NoticeController {
return ApiResponse.ok(service.unreadCount(target));
}
/** 알림 단건 읽음 처리. userId 미지정 시 현재 로그인 사용자 기준. READ 권한 필요. */
@PostMapping("/api/user-notifications/{notificationId}/read")
@RequirePermission(menu = "NOTICE", perm = "READ")
public ApiResponse<Void> markRead(@PathVariable Long notificationId,
@@ -90,6 +109,7 @@ public class NoticeController {
return ApiResponse.ok();
}
/** 해당 사용자의 모든 알림을 읽음 처리. READ 권한 필요. */
@PostMapping("/api/user-notifications/read-all")
@RequirePermission(menu = "NOTICE", perm = "READ")
public ApiResponse<Void> markAllRead(@RequestParam Long userId) {
@@ -16,6 +16,15 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 설계사(보험모집인) 관리 API.
*
* <p>설계사는 GA(보험대리점)에 소속되어 보험계약을 모집하는 사람이며, 수수료 정산의 기본 단위다.
* 각 설계사는 특정 조직(지점/본부)과 직급(grade)에 소속되며, 입사/전배/승진/퇴사 등의 이력이 관리된다.</p>
*
* <p>모든 엔드포인트는 {@code @RequirePermission}으로 메뉴(ORG_AGENT)별 권한(READ/CREATE/UPDATE/EXPORT)을
* 검증하며, 데이터 변경 작업은 {@code @DataChangeLog}로 변경 이력을 자동 적재한다.</p>
*/
@Tag(name = "설계사 관리")
@RestController
@RequestMapping("/api/agents")
@@ -25,18 +34,21 @@ public class AgentController {
private final AgentService agentService;
private final ExcelService excelService;
/** 설계사 목록 조회(검색조건 + 페이징). READ 권한 필요. */
@GetMapping
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
public ApiResponse<PageResponse<AgentResp>> list(@ModelAttribute AgentSearchParam param) {
return ApiResponse.ok(agentService.list(param));
}
/** 설계사 단건 상세 조회. READ 권한 필요. */
@GetMapping("/{agentId}")
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
public ApiResponse<AgentResp> detail(@PathVariable Long agentId) {
return ApiResponse.ok(agentService.detail(agentId));
}
/** 신규 설계사 등록(입사). 등록 시 입사 이력이 함께 생성된다. CREATE 권한 필요. */
@PostMapping
@RequirePermission(menu = "ORG_AGENT", perm = "CREATE")
@DataChangeLog(menu = "ORG_AGENT", table = "agent")
@@ -44,6 +56,7 @@ public class AgentController {
return ApiResponse.ok(agentService.create(req));
}
/** 설계사 정보 수정. 조직/직급이 바뀌면 전배/승진 이력이 자동 기록된다. UPDATE 권한 필요. */
@PutMapping("/{agentId}")
@RequirePermission(menu = "ORG_AGENT", perm = "UPDATE")
@DataChangeLog(menu = "ORG_AGENT", table = "agent")
@@ -52,6 +65,7 @@ public class AgentController {
return ApiResponse.ok();
}
/** 설계사 상태 변경(예: ACTIVE 재직 → 퇴사/휴직 등). body의 "status" 값을 코드로 검증한다. UPDATE 권한 필요. */
@PutMapping("/{agentId}/status")
@RequirePermission(menu = "ORG_AGENT", perm = "UPDATE")
@DataChangeLog(menu = "ORG_AGENT", table = "agent")
@@ -60,12 +74,14 @@ public class AgentController {
return ApiResponse.ok();
}
/** 설계사 조직/직급 변경 이력(입사·전배·승진) 조회. READ 권한 필요. */
@GetMapping("/{agentId}/history")
@RequirePermission(menu = "ORG_AGENT", perm = "READ")
public ApiResponse<List<AgentOrgHistoryResp>> history(@PathVariable Long agentId) {
return ApiResponse.ok(agentService.history(agentId));
}
/** 설계사 목록 엑셀 다운로드. 대용량 대비 커서 스트리밍으로 출력한다. EXPORT 권한 필요. */
@GetMapping("/export")
@RequirePermission(menu = "ORG_AGENT", perm = "EXPORT")
public void export(@ModelAttribute AgentSearchParam param, HttpServletResponse response) {
@@ -14,6 +14,14 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 조직(본부/지점 등) 관리 API.
*
* <p>조직은 부모-자식 관계의 계층(트리) 구조를 가진다. 설계사는 말단 조직에 소속되며,
* 수수료 정산·실적 집계는 이 조직 계층을 따라 상위로 롤업된다.</p>
*
* <p>메뉴 ORG_TREE 권한으로 보호되며, 삭제는 하위 조직/소속 설계사가 없을 때만 허용된다(아래 서비스 참고).</p>
*/
@Tag(name = "조직 관리")
@RestController
@RequestMapping("/api/orgs")
@@ -22,12 +30,14 @@ public class OrganizationController {
private final OrganizationService service;
/** 조직 전체를 트리(계층) 구조로 조회. READ 권한 필요. */
@GetMapping("/tree")
@RequirePermission(menu = "ORG_TREE", perm = "READ")
public ApiResponse<List<OrganizationResp>> tree() {
return ApiResponse.ok(service.getTree());
}
/** 조직 목록을 평면(flat) 리스트로 조회(검색어/조직유형/활성여부 필터). READ 권한 필요. */
@GetMapping
@RequirePermission(menu = "ORG_TREE", perm = "READ")
public ApiResponse<List<OrganizationResp>> list(@RequestParam(required = false) String keyword,
@@ -36,12 +46,14 @@ public class OrganizationController {
return ApiResponse.ok(service.list(keyword, orgType, isActive));
}
/** 조직 단건 상세 조회. READ 권한 필요. */
@GetMapping("/{orgId}")
@RequirePermission(menu = "ORG_TREE", perm = "READ")
public ApiResponse<OrganizationVO> detail(@PathVariable Long orgId) {
return ApiResponse.ok(service.get(orgId));
}
/** 신규 조직 등록. CREATE 권한 필요. */
@PostMapping
@RequirePermission(menu = "ORG_TREE", perm = "CREATE")
@DataChangeLog(menu = "ORG_TREE", table = "organization")
@@ -49,6 +61,7 @@ public class OrganizationController {
return ApiResponse.ok(service.create(req));
}
/** 조직 정보 수정. UPDATE 권한 필요. */
@PutMapping("/{orgId}")
@RequirePermission(menu = "ORG_TREE", perm = "UPDATE")
@DataChangeLog(menu = "ORG_TREE", table = "organization")
@@ -57,6 +70,7 @@ public class OrganizationController {
return ApiResponse.ok();
}
/** 조직 삭제. 하위 조직 또는 소속 설계사가 있으면 삭제 불가(서비스에서 검증). DELETE 권한 필요. */
@DeleteMapping("/{orgId}")
@RequirePermission(menu = "ORG_TREE", perm = "DELETE")
@DataChangeLog(menu = "ORG_TREE", table = "organization")
@@ -11,6 +11,14 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 보험사(원수사) 관리 REST 컨트롤러.
* 우리 GA가 상품을 위탁판매하는 보험회사(예: 삼성생명, KB손해보험 등) 마스터를 관리한다.
* 모든 상품/요율/수수료 계산의 출발점이 되는 기준 데이터이다.
*
* - @RequirePermission: 메뉴/권한(READ·CREATE·UPDATE) 체크. 권한 없으면 호출 거부.
* - @DataChangeLog: 데이터 변경 시 감사 로그(누가/언제/무엇을 바꿨는지) 자동 기록.
*/
@Tag(name = "보험사 관리")
@RestController
@RequestMapping("/api/companies")
@@ -19,18 +27,21 @@ public class CompanyController {
private final InsuranceCompanyService companyService;
/** 보험사 목록 조회. activeOnly=true 면 사용중(활성) 보험사만 반환. */
@GetMapping
@RequirePermission(menu = "COMPANY", perm = "READ")
public ApiResponse<List<InsuranceCompanyVO>> list(@RequestParam(required = false, defaultValue = "false") boolean activeOnly) {
return ApiResponse.ok(companyService.list(activeOnly));
}
/** 보험사 단건 상세 조회 (URL 경로의 companyId 기준). */
@GetMapping("/{companyId}")
@RequirePermission(menu = "COMPANY", perm = "READ")
public ApiResponse<InsuranceCompanyVO> detail(@PathVariable Long companyId) {
return ApiResponse.ok(companyService.detail(companyId));
}
/** 보험사 신규 등록. 생성된 PK(companyId)를 반환. */
@PostMapping
@RequirePermission(menu = "COMPANY", perm = "CREATE")
@DataChangeLog(menu = "COMPANY", table = "insurance_company")
@@ -38,6 +49,7 @@ public class CompanyController {
return ApiResponse.ok(companyService.create(vo));
}
/** 보험사 정보 수정 (경로 companyId 대상). */
@PutMapping("/{companyId}")
@RequirePermission(menu = "COMPANY", perm = "UPDATE")
@DataChangeLog(menu = "COMPANY", table = "insurance_company")
@@ -15,6 +15,11 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 보험계약 관리 REST 컨트롤러.
* 계약(contract)은 "어떤 설계사가 어떤 고객에게 어떤 상품을 팔았는가"를 나타내는 핵심 거래 데이터로,
* 이후 수수료 계산·분급(installment)·차등환수(chargeback)의 기준이 된다.
*/
@Tag(name = "계약 관리")
@RestController
@RequestMapping("/api/contracts")
@@ -23,18 +28,21 @@ public class ContractController {
private final ContractService service;
/** 계약 목록 조회. @ModelAttribute 로 검색조건(증권번호·기간 등)을 받아 페이징 결과를 반환. */
@GetMapping
@RequirePermission(menu = "CONTRACT_LIST", perm = "READ")
public ApiResponse<PageResponse<ContractResp>> list(@ModelAttribute ContractSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 계약 단건 상세 조회. */
@GetMapping("/{contractId}")
@RequirePermission(menu = "CONTRACT_LIST", perm = "READ")
public ApiResponse<ContractResp> detail(@PathVariable Long contractId) {
return ApiResponse.ok(service.detail(contractId));
}
/** 계약 신규 등록. @Valid 로 필수값 검증 후 생성된 PK(contractId) 반환. */
@PostMapping
@RequirePermission(menu = "CONTRACT_LIST", perm = "CREATE")
@DataChangeLog(menu = "CONTRACT_LIST", table = "contract")
@@ -42,6 +50,7 @@ public class ContractController {
return ApiResponse.ok(service.create(req));
}
/** 계약 정보 수정. */
@PutMapping("/{contractId}")
@RequirePermission(menu = "CONTRACT_LIST", perm = "UPDATE")
@DataChangeLog(menu = "CONTRACT_LIST", table = "contract")
@@ -50,6 +59,7 @@ public class ContractController {
return ApiResponse.ok();
}
/** 계약 상태 변경(예: 정상 ACTIVE → 해지 CANCELLED). body 의 "status" 값만 사용. 해지 시 차등환수 계산 대상이 됨. */
@PutMapping("/{contractId}/status")
@RequirePermission(menu = "CONTRACT_LIST", perm = "UPDATE")
@DataChangeLog(menu = "CONTRACT_LIST", table = "contract")
@@ -11,6 +11,12 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 보험상품 관리 REST 컨트롤러.
* 상품(product)은 보험사가 판매하는 개별 상품으로, 각 상품에는
* company_rate(보험사가 GA에 주는 요율)·payout_rate(GA가 설계사에게 지급하는 요율) 등
* 수수료 계산의 핵심 요율 정보가 붙는다.
*/
@Tag(name = "상품 관리")
@RestController
@RequestMapping("/api/products")
@@ -19,6 +25,7 @@ public class ProductController {
private final ProductService productService;
/** 상품 목록 조회. 보험사/보험종류/키워드/활성여부로 필터링하며 페이징 처리(pageNum/pageSize)된다. */
@GetMapping
@RequirePermission(menu = "PRODUCT", perm = "READ")
public ApiResponse<PageResponse<ProductResp>> list(@RequestParam(defaultValue = "1") int pageNum,
@@ -30,12 +37,14 @@ public class ProductController {
return ApiResponse.ok(productService.list(pageNum, pageSize, companyId, insuranceType, keyword, isActive));
}
/** 상품 단건 상세 조회. */
@GetMapping("/{productId}")
@RequirePermission(menu = "PRODUCT", perm = "READ")
public ApiResponse<ProductResp> detail(@PathVariable Long productId) {
return ApiResponse.ok(productService.detail(productId));
}
/** 상품 신규 등록. 생성된 PK(productId) 반환. */
@PostMapping
@RequirePermission(menu = "PRODUCT", perm = "CREATE")
@DataChangeLog(menu = "PRODUCT", table = "product")
@@ -43,6 +52,7 @@ public class ProductController {
return ApiResponse.ok(productService.create(vo));
}
/** 상품 정보(요율 등) 수정. */
@PutMapping("/{productId}")
@RequirePermission(menu = "PRODUCT", perm = "UPDATE")
@DataChangeLog(menu = "PRODUCT", table = "product")
@@ -51,6 +61,7 @@ public class ProductController {
return ApiResponse.ok();
}
/** 상품 삭제. */
@DeleteMapping("/{productId}")
@RequirePermission(menu = "PRODUCT", perm = "DELETE")
@DataChangeLog(menu = "PRODUCT", table = "product")
@@ -12,6 +12,19 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* [데이터 수신 매핑/원본 관리 API]
*
* 신입 개발자 안내:
* - 보험사마다 보내주는 수수료 데이터의 컬럼명/코드값이 제각각이다. 이를 우리 표준으로 바꾸기 위한 설정이 "매핑"이다.
* - 구성 요소:
* company_profile : 보험사별 수신 설정(파일 형식, 헤더 위치 등)
* company_field_mapping : 보험사 컬럼 → 우리 DB 필드 매핑
* code_mapping : 보험사 코드값 → 우리 표준 코드값 매핑
* raw_commission_data : 수신한 원본 데이터(파싱 상태 포함)
* parse_error_log : 파싱 실패 건과 그 처리(resolve) 상태
* - 업로드 템플릿(upload)이 "엑셀 수동 적재"라면, 여기 매핑은 "보험사별 자동 수신/파싱 규칙"에 가깝다.
*/
@Tag(name = "데이터 수신")
@RestController
@RequestMapping("/api/receive")
@@ -82,6 +95,7 @@ public class ReceiveController {
}
// 원본 데이터 + 에러
// raw: 수신한 원본 행 목록(보험사/정산월/파싱상태로 필터). errors: 파싱 실패 로그. resolve: 실패 건을 처리완료로 표시.
@GetMapping("/raw")
@RequirePermission(menu = "RECEIVE_DATA", perm = "READ")
public ApiResponse<List<RawCommissionDataVO>> rawList(@RequestParam(required = false) String companyCode,
@@ -4,6 +4,11 @@ import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
/**
* 파싱 에러 처리(resolve) 요청 본문.
* status: 처리 결과 상태(예: 해결됨/보류 등) — 필수.
* note : 처리 사유/메모 — 선택.
*/
@Getter
@Setter
public class ResolveErrorReq {
@@ -7,12 +7,18 @@ import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 계약 단위 규제 한도(예: 1200%룰) 위반 여부 체크 요청 DTO.
* "이 계약에 수수료를 additionalAmount 만큼 더 지급해도 규제 한도를 넘지 않는가?"를 묻는다.
*/
@Data
public class ContractLimitCheckReq {
/** 한도 체크 대상 계약 ID. */
@NotNull
private Long contractId;
/** 추가로 지급하려는 수수료 금액(양수만 허용). 이 금액을 더했을 때 한도 초과 여부를 검사한다. */
@NotNull
@Positive
private BigDecimal additionalAmount;
@@ -17,6 +17,13 @@ import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
/**
* 규제 한도 관리 REST 컨트롤러.
* 금융당국 규제(대표적으로 보험모집 수수료의 '1200%룰': 초년도 수수료를 월납보험료의 12배 이내로 제한)를
* 한도마스터(regulatory_limit) 테이블로 관리하고, 계약별 위반 여부를 점검한다.
* - limitService : 한도 마스터 CRUD + 활성/비활성 토글
* - limitChecker : 실제 계약에 적용해 한도 초과 여부 계산(별도 상세 주석 파일)
*/
@Tag(name = "규제 한도 관리")
@RestController
@RequestMapping("/api/regulatory-limits")
@@ -26,18 +33,21 @@ public class RegulatoryLimitController {
private final RegulatoryLimitService limitService;
private final RegulatoryLimitChecker limitChecker;
/** 한도 마스터 목록 페이징 조회. */
@GetMapping
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "READ")
public ApiResponse<PageResponse<RegulatoryLimitResp>> list(RegulatoryLimitSearchParam param) {
return ApiResponse.ok(limitService.list(param));
}
/** 한도 마스터 단건 상세 조회. */
@GetMapping("/{limitId}")
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "READ")
public ApiResponse<RegulatoryLimitResp> detail(@PathVariable Long limitId) {
return ApiResponse.ok(limitService.detail(limitId));
}
/** 한도 마스터 신규 등록. */
@PostMapping
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "CREATE")
@DataChangeLog(menu = "REGULATORY_LIMIT", table = "regulatory_limit")
@@ -46,6 +56,7 @@ public class RegulatoryLimitController {
return ApiResponse.ok();
}
/** 한도 마스터 수정. */
@PutMapping("/{limitId}")
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "UPDATE")
@DataChangeLog(menu = "REGULATORY_LIMIT", table = "regulatory_limit")
@@ -55,6 +66,7 @@ public class RegulatoryLimitController {
return ApiResponse.ok();
}
/** 한도 마스터 비활성화(삭제 대신 사용 중지 처리). 비활성 한도는 위반 체크에서 제외된다. */
@PutMapping("/{limitId}/deactivate")
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "UPDATE")
@DataChangeLog(menu = "REGULATORY_LIMIT", table = "regulatory_limit")
@@ -63,6 +75,7 @@ public class RegulatoryLimitController {
return ApiResponse.ok();
}
/** 한도 마스터 재활성화. 같은 유형의 활성 한도가 이미 있으면 중복 오류. */
@PutMapping("/{limitId}/activate")
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "UPDATE")
@DataChangeLog(menu = "REGULATORY_LIMIT", table = "regulatory_limit")
@@ -71,6 +84,11 @@ public class RegulatoryLimitController {
return ApiResponse.ok();
}
/**
* 특정 계약에 수수료를 더 지급했을 때 규제 한도 위반 여부를 점검한다.
* limitType 이 FIRST_YEAR_RATIO 면 초년도(첫 1년) 기준, 그 외(TOTAL_RATIO)면 계약 전체 누적 기준으로 검사.
* baseDate 가 없으면 오늘 날짜를 기준일로 사용한다.
*/
@PostMapping("/check-contract")
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "READ")
public ApiResponse<ViolationResult> checkContract(@Valid @RequestBody ContractLimitCheckReq req) {
@@ -20,6 +20,16 @@ import java.util.List;
/**
* 리포트 — 실적 / 조직별 / 환수위험. 읽기전용 집계 (신규 테이블 없음).
* 각 화면의 조회 + 엑셀 export 엔드포인트 제공.
*
* [신입 개발자 안내]
* - 모든 리포트는 기존 정산/지급 데이터를 SQL 로 모아 보여주는 "읽기 전용 집계"다(저장 테이블을 새로 만들지 않음).
* - 각 리포트마다 화면 조회용(JSON 목록)과 엑셀 다운로드용(/export, Cursor 스트리밍) 두 개가 한 쌍이다.
* - year/settleMonth 파라미터가 없으면 defaultYear/defaultMonth 로 "올해/이번 달"을 기본 적용한다.
* - 리포트 종류:
* performance : 설계사/연도별 실적
* org : 조직(지점)별 월 집계
* chargeback-risk : 환수(이미 준 수수료를 도로 회수) 위험 목록
* simplified-payment : 사업소득 간이지급명세서(국세청 제출용 월별 자료)
*/
@Tag(name = "리포트")
@RestController
@@ -11,6 +11,16 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 수수료 규정(룰) 관리 REST 컨트롤러.
* 수수료 계산에 쓰이는 각종 기준표를 CRUD 한다. 다섯 가지 룰 그룹으로 구성된다.
* - commission_rate : 상품별 보험사 지급요율(company_rate) 기준표
* - payout_rule : 설계사 등급(grade)별 지급요율(payout_rate) 규칙
* - override_rule : 상위 관리자에게 주는 오버라이드(육성) 수당 규칙
* - chargeback_rule : 조기해지 시 환수율(차등환수) 규칙
* - exception_type_code : 예외 처리 유형 코드
* 이 기준표들을 잘못 등록하면 전 직원 수수료 계산이 틀어지므로 권한·감사로그가 엄격히 걸려 있다.
*/
@Tag(name = "수수료 규정")
@RestController
@RequestMapping("/api/rules")
@@ -19,7 +29,7 @@ public class RuleController {
private final RuleService ruleService;
// ========== commission_rate ==========
// ========== commission_rate : 상품별 보험사 지급요율(company_rate) 기준표 CRUD ==========
@GetMapping("/commission-rates")
@RequirePermission(menu = "RULE_COMMISSION", perm = "READ")
public ApiResponse<List<CommissionRateVO>> listCommissionRates(@RequestParam(required = false) Long productId,
@@ -50,7 +60,7 @@ public class RuleController {
return ApiResponse.ok();
}
// ========== payout_rule ==========
// ========== payout_rule : 설계사 등급(grade)별 지급요율(payout_rate) 규칙 CRUD ==========
@GetMapping("/payout")
@RequirePermission(menu = "RULE_PAYOUT", perm = "READ")
public ApiResponse<List<PayoutRuleVO>> listPayoutRules(@RequestParam(required = false) Long gradeId,
@@ -81,7 +91,7 @@ public class RuleController {
return ApiResponse.ok();
}
// ========== override_rule ==========
// ========== override_rule : 상위 관리자 오버라이드(육성) 수당 규칙 CRUD ==========
@GetMapping("/override")
@RequirePermission(menu = "RULE_OVERRIDE", perm = "READ")
public ApiResponse<List<OverrideRuleVO>> listOverrideRules() {
@@ -111,7 +121,7 @@ public class RuleController {
return ApiResponse.ok();
}
// ========== chargeback_rule ==========
// ========== chargeback_rule : 조기해지 환수율(차등환수) 규칙 CRUD ==========
@GetMapping("/chargeback")
@RequirePermission(menu = "RULE_CHARGEBACK", perm = "READ")
public ApiResponse<List<ChargebackRuleVO>> listChargebackRules(@RequestParam(required = false) String companyCode) {
@@ -141,7 +151,7 @@ public class RuleController {
return ApiResponse.ok();
}
// ========== exception_type_code ==========
// ========== exception_type_code : 예외 처리 유형 코드 관리(코드값 기반) ==========
@GetMapping("/exception-codes")
@RequirePermission(menu = "RULE_EXCEPTION", perm = "READ")
public ApiResponse<List<ExceptionTypeCodeVO>> listExceptionCodes() {
@@ -13,6 +13,14 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 보험사 회신(carrier_reconciliation_out) 관리 컨트롤러.
*
* <p>GA가 보험사로 보낸 정산 자료에 대해 "어떤 파일을 언제 회신/전송했는지" 이력을 관리하는 화면입니다.
* 한 건은 "보험사 1곳 × 정산월 1개"로 유일하며, 전송 실패(FAIL) 건은 retry 로 다시 READY 로 돌려 배치가 재전송합니다.
*
* <p>상태: READY(전송대기) → (배치 처리) → 성공/FAIL. 권한(menu=RECON_OUT): 조회 READ, 등록 CREATE, 재전송 UPDATE.
*/
@Tag(name = "보험사 회신 관리")
@RestController
@RequestMapping("/api/carrier-reconciliation")
@@ -21,18 +29,21 @@ public class CarrierReconciliationOutController {
private final CarrierReconciliationOutService service;
/** 보험사 회신 이력 목록 조회(검색 + 페이징). */
@GetMapping
@RequirePermission(menu = "RECON_OUT", perm = "READ")
public ApiResponse<PageResponse<CarrierReconciliationOutResp>> list(@ModelAttribute CarrierReconciliationOutSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 회신 이력 단건 상세 조회. */
@GetMapping("/{reconOutId}")
@RequirePermission(menu = "RECON_OUT", perm = "READ")
public ApiResponse<CarrierReconciliationOutResp> detail(@PathVariable Long reconOutId) {
return ApiResponse.ok(service.detail(reconOutId));
}
/** 회신 이력 신규 등록(READY 상태로 시작). 같은 보험사+정산월 중복 등록은 거부됩니다. */
@PostMapping
@RequirePermission(menu = "RECON_OUT", perm = "CREATE")
@DataChangeLog(menu = "RECON_OUT", table = "carrier_reconciliation_out")
@@ -40,6 +51,7 @@ public class CarrierReconciliationOutController {
return ApiResponse.ok(service.create(req));
}
/** 전송 실패(FAIL) 건 재전송. 상태를 READY 로 되돌려 배치가 다시 처리하도록 합니다. */
@PostMapping("/{reconOutId}/retry")
@RequirePermission(menu = "RECON_OUT", perm = "UPDATE")
@DataChangeLog(menu = "RECON_OUT", table = "carrier_reconciliation_out")
@@ -17,6 +17,18 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 환수(chargeback) 자료 관리 컨트롤러.
*
* <p>"환수"란 이미 설계사에게 지급한 수수료를 도로 거둬들이는 것입니다. 계약이 일찍 해지/실효되면
* 보험사가 GA에게 수수료를 돌려달라 하고, GA는 그만큼을 설계사 정산에서 회수합니다.
*
* <p>주 업무는 보험사가 보내준 환수 엑셀(증권번호·환수금액·실효월·환수율)을 업로드(upload)해
* chargeback 테이블에 적재하는 것입니다. 존재하지 않는 증권번호나 잘못된 금액은 에러행으로 돌려주고,
* 같은 계약+정산월 중복은 건너뜁니다(스킵).
*
* <p>권한(menu=CHARGEBACK_DATA): 조회 READ, 엑셀 EXPORT, 업로드 CREATE.
*/
@Tag(name = "환수자료 관리")
@Validated
@RestController
@@ -16,6 +16,18 @@ import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.Map;
/**
* 수수료 이의신청(commission_dispute) 관리 컨트롤러.
*
* <p>설계사가 "내 수수료 정산 금액이 이상하다"고 문제를 제기하면 이의신청 건이 만들어집니다.
* 담당자는 검토를 시작(startReview)하고, 타당하면 승인(approve)하며 보정 금액을 정정(correction)과 연결하거나,
* 타당하지 않으면 기각(reject)합니다.
*
* <p>상태 흐름(DisputeStatus): OPEN(접수) → IN_REVIEW(검토중) → APPROVED(승인)/REJECTED(기각).
* 승인/기각은 IN_REVIEW 상태에서만 가능합니다.
*
* <p>권한(menu=DISPUTE): 조회 READ, 접수 CREATE, 검토시작 UPDATE, 승인/기각 APPROVE.
*/
@Tag(name = "수수료 이의신청 관리")
@RestController
@RequestMapping("/api/disputes")
@@ -24,18 +36,21 @@ public class CommissionDisputeController {
private final CommissionDisputeService service;
/** 이의신청 목록 조회(검색 + 페이징). */
@GetMapping
@RequirePermission(menu = "DISPUTE", perm = "READ")
public ApiResponse<PageResponse<CommissionDisputeResp>> list(@ModelAttribute CommissionDisputeSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 이의신청 단건 상세 조회. */
@GetMapping("/{disputeId}")
@RequirePermission(menu = "DISPUTE", perm = "READ")
public ApiResponse<CommissionDisputeResp> detail(@PathVariable Long disputeId) {
return ApiResponse.ok(service.detail(disputeId));
}
/** 이의신청 접수(OPEN 상태로 생성). */
@PostMapping
@RequirePermission(menu = "DISPUTE", perm = "CREATE")
@DataChangeLog(menu = "DISPUTE", table = "commission_dispute")
@@ -43,6 +58,7 @@ public class CommissionDisputeController {
return ApiResponse.ok(service.create(req));
}
/** 검토 시작(OPEN → IN_REVIEW). */
@PostMapping("/{disputeId}/review")
@RequirePermission(menu = "DISPUTE", perm = "UPDATE")
@DataChangeLog(menu = "DISPUTE", table = "commission_dispute")
@@ -51,6 +67,10 @@ public class CommissionDisputeController {
return ApiResponse.ok();
}
/**
* 이의신청 승인(IN_REVIEW → APPROVED).
* body 의 resolution(처리내용), adjustmentAmount(보정금액, 선택), correctionId(연결된 정산정정ID, 선택)를 받습니다.
*/
@PostMapping("/{disputeId}/approve")
@RequirePermission(menu = "DISPUTE", perm = "APPROVE")
@DataChangeLog(menu = "DISPUTE", table = "commission_dispute")
@@ -64,6 +84,7 @@ public class CommissionDisputeController {
return ApiResponse.ok();
}
/** 이의신청 기각(IN_REVIEW → REJECTED). body 의 resolution 에 기각 사유를 담습니다. */
@PostMapping("/{disputeId}/reject")
@RequirePermission(menu = "DISPUTE", perm = "APPROVE")
@DataChangeLog(menu = "DISPUTE", table = "commission_dispute")
@@ -15,6 +15,17 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 시책 지급(incentive_payment) 관리 컨트롤러.
*
* <p>"시책"은 보험사/GA가 특정 기간·상품에 거는 추가 인센티브(판매 장려금)입니다.
* 시책 프로그램(incentive_program) 조건을 충족한 설계사에게 지급할 금액을 건별로 만들어 확정/취소합니다.
*
* <p>상태 흐름(IncentivePayStatus): CALCULATED(산출) → CONFIRMED(확정) → PAID(지급완료), 또는 CANCELLED(취소).
* 확정은 CALCULATED 상태에서만, 취소는 PAID 가 아닐 때만 가능합니다. 생성·확정 시 정산월 마감 여부를 확인합니다.
*
* <p>권한(menu=INCENTIVE_PAY): 조회 READ, 생성 CREATE, 확정 APPROVE, 취소 UPDATE.
*/
@Tag(name = "시책 지급 관리")
@RestController
@RequestMapping("/api/incentive-payments")
@@ -23,18 +34,21 @@ public class IncentivePaymentController {
private final IncentivePaymentService service;
/** 시책 지급 목록 조회(검색 + 페이징). */
@GetMapping
@RequirePermission(menu = "INCENTIVE_PAY", perm = "READ")
public ApiResponse<PageResponse<IncentivePaymentResp>> list(@ModelAttribute IncentivePaymentSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 시책 지급 단건 상세 조회. */
@GetMapping("/{incentivePayId}")
@RequirePermission(menu = "INCENTIVE_PAY", perm = "READ")
public ApiResponse<IncentivePaymentResp> detail(@PathVariable Long incentivePayId) {
return ApiResponse.ok(service.detail(incentivePayId));
}
/** 시책 지급 생성(CALCULATED 상태). 해당 정산월이 마감되어 있으면 거부됩니다. */
@PostMapping
@RequirePermission(menu = "INCENTIVE_PAY", perm = "CREATE")
@DataChangeLog(menu = "INCENTIVE_PAY", table = "incentive_payment")
@@ -42,6 +56,7 @@ public class IncentivePaymentController {
return ApiResponse.ok(service.create(req));
}
/** 시책 지급 확정(CALCULATED → CONFIRMED). 마감 가드 적용. APPROVE 권한 필요. */
@PostMapping("/{incentivePayId}/confirm")
@RequirePermission(menu = "INCENTIVE_PAY", perm = "APPROVE")
@DataChangeLog(menu = "INCENTIVE_PAY", table = "incentive_payment")
@@ -50,6 +65,7 @@ public class IncentivePaymentController {
return ApiResponse.ok();
}
/** 시책 지급 취소(→ CANCELLED). body 의 cancelReason 에 취소 사유를 담습니다. 지급완료건은 취소 불가. */
@PostMapping("/{incentivePayId}/cancel")
@RequirePermission(menu = "INCENTIVE_PAY", perm = "UPDATE")
@DataChangeLog(menu = "INCENTIVE_PAY", table = "incentive_payment")
@@ -15,6 +15,16 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 시책 프로그램(incentive_program) 관리 컨트롤러.
*
* <p>시책 "프로그램"은 인센티브 지급의 규칙 정의입니다. 대상(등급/조직/상품군), 기간(시작월~종료월),
* 지급 방식(정률 payRate 또는 정액 payAmountFixed), 예산(budgetAmount) 등을 설정합니다.
* 실제 지급(incentive_payment)은 이 프로그램 조건에 따라 산출됩니다.
*
* <p>활성/비활성(isActive)은 toggleActive 로 전환합니다. 비활성 프로그램은 신규 지급 대상에서 제외됩니다.
* 권한(menu=INCENTIVE_PROG): 조회 READ, 생성 CREATE, 수정/활성토글 UPDATE.
*/
@Tag(name = "시책 프로그램 관리")
@RestController
@RequestMapping("/api/incentive-programs")
@@ -23,18 +33,21 @@ public class IncentiveProgramController {
private final IncentiveProgramService service;
/** 시책 프로그램 목록 조회(검색 + 페이징). */
@GetMapping
@RequirePermission(menu = "INCENTIVE_PROG", perm = "READ")
public ApiResponse<PageResponse<IncentiveProgramResp>> list(@ModelAttribute IncentiveProgramSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 시책 프로그램 단건 상세 조회. */
@GetMapping("/{programId}")
@RequirePermission(menu = "INCENTIVE_PROG", perm = "READ")
public ApiResponse<IncentiveProgramResp> detail(@PathVariable Long programId) {
return ApiResponse.ok(service.detail(programId));
}
/** 시책 프로그램 신규 등록(기본 활성 상태로 생성). */
@PostMapping
@RequirePermission(menu = "INCENTIVE_PROG", perm = "CREATE")
@DataChangeLog(menu = "INCENTIVE_PROG", table = "incentive_program")
@@ -42,6 +55,7 @@ public class IncentiveProgramController {
return ApiResponse.ok(service.create(req));
}
/** 시책 프로그램 수정(전체 필드 갱신). */
@PutMapping("/{programId}")
@RequirePermission(menu = "INCENTIVE_PROG", perm = "UPDATE")
@DataChangeLog(menu = "INCENTIVE_PROG", table = "incentive_program")
@@ -50,6 +64,7 @@ public class IncentiveProgramController {
return ApiResponse.ok();
}
/** 활성/비활성 토글. body 의 isActive(true/false)로 사용 여부를 전환합니다. */
@PutMapping("/{programId}/active")
@RequirePermission(menu = "INCENTIVE_PROG", perm = "UPDATE")
@DataChangeLog(menu = "INCENTIVE_PROG", table = "incentive_program")
@@ -16,6 +16,18 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 지급대상 배치(payment_batch) 관리 컨트롤러.
*
* <p>"배치"는 한 정산월에 실제로 돈을 보낼 대상 설계사들을 한 묶음으로 모은 작업 단위입니다.
* 담당자는 배치를 만들어(create) 대상 명단을 확인하고, 빼야 할 사람은 제외(updateItem)한 뒤,
* 최종 확정(finalize)하여 이후 이체 처리로 넘깁니다.
*
* <p>상태 흐름(PaymentBatchStatus): DRAFT(작성중) → FINALIZED(확정) / CANCELLED(취소).
* 수정(updateItem)은 DRAFT 상태에서만 가능합니다.
*
* <p>권한(menu=PAYMENT_TARGET): 생성 CREATE, 조회/엑셀 READ, 항목수정 UPDATE, 확정 EXECUTE.
*/
@Tag(name = "지급대상 배치 관리")
@RestController
@RequestMapping("/api/payment-batches")
@@ -16,6 +16,19 @@ import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 지급(payment) 관리 컨트롤러.
*
* <p>정산 마스터가 "이번 달 줄 금액"을 확정하면, 실제 계좌로 보낼 한 건 한 건이 payment(지급) 레코드가 됩니다.
* 이 컨트롤러는 지급 건 목록 조회, 상태 변경, 세금 계산, 차감 적용, 은행 이체파일 생성을 담당합니다.
*
* <p>지급 한 건의 금액 계산 순서(도메인):
* payAmount(지급원금) → 세금(소득세·지방세·부가세) 차감 → 각종 공제(차감) 차감 → netAmount(실지급액).
* 이 netAmount 가 최종적으로 설계사 통장에 들어가는 금액입니다.
*
* <p>@Validated + @Pattern 으로 settleMonth 는 YYYYMM(숫자 6자리) 형식만 허용합니다.
* 권한(menu=PAYMENT): 조회 READ, 상태/세금/차감 변경 UPDATE, 이체파일 생성 EXPORT.
*/
@Tag(name = "지급 관리")
@Validated
@RestController
@@ -25,12 +38,14 @@ public class PaymentController {
private final PaymentService paymentService;
/** 지급 건 목록 조회(검색 + 페이징). */
@GetMapping
@RequirePermission(menu = "PAYMENT", perm = "READ")
public ApiResponse<PageResponse<PaymentResp>> list(@ModelAttribute SettleSearchParam param) {
return ApiResponse.ok(paymentService.list(param));
}
/** 지급 상태 수동 변경(예: 실패 처리). 실패 시 req.failReason 에 사유를 담아 기록합니다. */
@PutMapping("/{paymentId}/status")
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
@DataChangeLog(menu = "PAYMENT", table = "payment")
@@ -4,6 +4,10 @@ import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
/**
* 지급 상태 변경 요청 DTO.
* status: 변경할 상태값(필수). failReason: 실패 처리 시 사유(선택).
*/
@Getter
@Setter
public class PaymentStatusUpdateReq {
@@ -15,6 +15,15 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 정산 마감(period_close) 관리 컨트롤러.
*
* <p>"마감"은 특정 정산월의 정산 데이터를 잠그는 작업입니다. 마감된 월에는 정정·시책지급 등 금액을 바꾸는
* 작업이 막혀(마감 가드), 이미 확정된 숫자가 사후에 흔들리는 것을 방지합니다.
*
* <p>마감(close) 후 정정이 필요하면 재오픈(reopen)으로 잠금을 풀 수 있으며, 재오픈 사유를 함께 기록합니다.
* 권한(menu=PERIOD_CLOSE): 조회 READ, 마감 CREATE, 재오픈 UPDATE.
*/
@Tag(name = "정산 마감 관리")
@RestController
@RequestMapping("/api/period-close")
@@ -23,18 +32,21 @@ public class PeriodCloseController {
private final PeriodCloseService service;
/** 마감 이력 목록 조회(검색 + 페이징). */
@GetMapping
@RequirePermission(menu = "PERIOD_CLOSE", perm = "READ")
public ApiResponse<PageResponse<PeriodCloseResp>> list(@ModelAttribute PeriodCloseSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 마감 이력 단건 상세 조회. */
@GetMapping("/{closeId}")
@RequirePermission(menu = "PERIOD_CLOSE", perm = "READ")
public ApiResponse<PeriodCloseResp> detail(@PathVariable Long closeId) {
return ApiResponse.ok(service.detail(closeId));
}
/** 정산월 마감 실행. 이미 마감된 월이면 거부됩니다. */
@PostMapping
@RequirePermission(menu = "PERIOD_CLOSE", perm = "CREATE")
@DataChangeLog(menu = "PERIOD_CLOSE", table = "period_close")
@@ -43,6 +55,7 @@ public class PeriodCloseController {
return ApiResponse.ok();
}
/** 마감 재오픈(잠금 해제). body 의 reopenReason 에 재오픈 사유를 담습니다. */
@PostMapping("/{closeId}/reopen")
@RequirePermission(menu = "PERIOD_CLOSE", perm = "UPDATE")
@DataChangeLog(menu = "PERIOD_CLOSE", table = "period_close")
@@ -14,6 +14,18 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 정산 마스터(settle_master) 관리 컨트롤러.
*
* <p>정산 마스터는 "설계사 1명 × 정산월 1개" 단위로 그 달에 줄 수수료 합계를 모아둔 표(원장)입니다.
* 매달 배치가 각종 원장을 집계해 settle_master 한 행을 만들고, 담당자가 이 화면에서 확인/승인합니다.
*
* <p>상태 흐름(SettleStatus): CALCULATED(산출완료) → CONFIRMED(확정) → PAID(지급완료).
* 중간에 문제가 있으면 HOLD(보류)로 묶었다가 release 로 다시 CALCULATED 로 풀 수 있습니다.
*
* <p>권한(menu=SETTLE_LIST): 조회는 READ, 확정/보류/해제 같은 상태변경은 APPROVE 권한이 필요합니다.
* 상태변경 엔드포인트에는 @DataChangeLog 가 붙어 누가 무엇을 바꿨는지 변경이력이 자동 기록됩니다.
*/
@Tag(name = "정산 관리")
@RestController
@RequestMapping("/api/settle")
@@ -22,12 +34,14 @@ public class SettleController {
private final SettleService service;
/** 정산 마스터 목록 조회(검색조건 + 페이징). 요청: SettleSearchParam, 응답: 페이지 형태의 정산 요약 목록. */
@GetMapping
@RequirePermission(menu = "SETTLE_LIST", perm = "READ")
public ApiResponse<PageResponse<SettleMasterResp>> list(@ModelAttribute SettleSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 정산 마스터 단건 상세 조회. 키는 "설계사ID + 정산월(YYYYMM)" 조합입니다. */
@GetMapping("/{agentId}/months/{settleMonth}")
@RequirePermission(menu = "SETTLE_LIST", perm = "READ")
public ApiResponse<SettleMasterResp> detail(@PathVariable Long agentId,
@@ -35,18 +49,21 @@ public class SettleController {
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));
}
/** 특정 정산월의 조직(지점)별 합계 요약. summary 를 조직 단위로 쪼갠 목록입니다. */
@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));
}
/** 정산 확정(CONFIRMED). 확정 후에는 금액이 잠겨 지급 단계로 넘어갑니다. APPROVE 권한 필요. */
@PutMapping("/{settleId}/confirm")
@RequirePermission(menu = "SETTLE_LIST", perm = "APPROVE")
@DataChangeLog(menu = "SETTLE_LIST", table = "settle_master")
@@ -55,6 +72,7 @@ public class SettleController {
return ApiResponse.ok();
}
/** 정산 보류(HOLD). body 의 reason(사유)·reasonCode(사유코드)를 함께 기록합니다. 지급완료건은 보류 불가. */
@PutMapping("/{settleId}/hold")
@RequirePermission(menu = "SETTLE_LIST", perm = "APPROVE")
@DataChangeLog(menu = "SETTLE_LIST", table = "settle_master")
@@ -63,6 +81,7 @@ public class SettleController {
return ApiResponse.ok();
}
/** 보류 해제(release). HOLD 상태를 다시 CALCULATED 로 되돌려 정상 흐름에 복귀시킵니다. */
@PutMapping("/{settleId}/release")
@RequirePermission(menu = "SETTLE_LIST", perm = "APPROVE")
@DataChangeLog(menu = "SETTLE_LIST", table = "settle_master")
@@ -16,6 +16,18 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 정산 정정(settle_correction) 관리 컨트롤러.
*
* <p>이미 산출된 정산 금액을 사후에 바로잡는 화면입니다. 원래 정산(originalSettleId)을 기준으로
* 변경 전(beforeAmount)·후(afterAmount) 금액과 차액(diffAmount), 사유를 기록합니다.
*
* <p>상태 흐름(CorrectionStatus): DRAFT(작성) → APPROVED(승인) → APPLIED(반영완료), 또는 취소.
* 승인은 DRAFT 상태에서만, 취소는 이미 반영(APPLIED)되지 않은 건만 가능합니다.
* 생성 시 해당 정산월이 마감되어 있으면 거부됩니다(마감 가드).
*
* <p>권한(menu=SETTLE_CORRECTION): 조회 READ, 생성 CREATE, 승인 APPROVE, 취소 UPDATE.
*/
@Tag(name = "정산 정정 관리")
@RestController
@RequestMapping("/api/settle-corrections")
@@ -24,24 +36,28 @@ public class SettleCorrectionController {
private final SettleCorrectionService service;
/** 정정 목록 조회(검색 + 페이징). */
@GetMapping
@RequirePermission(menu = "SETTLE_CORRECTION", perm = "READ")
public ApiResponse<PageResponse<SettleCorrectionResp>> list(@ModelAttribute SettleCorrectionSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 정정 단건 상세 조회. */
@GetMapping("/{correctionId}")
@RequirePermission(menu = "SETTLE_CORRECTION", perm = "READ")
public ApiResponse<SettleCorrectionResp> detail(@PathVariable Long correctionId) {
return ApiResponse.ok(service.detail(correctionId));
}
/** 특정 원본 정산(originalSettleId)에 달린 정정 이력 전체 조회. */
@GetMapping("/by-settle/{originalSettleId}")
@RequirePermission(menu = "SETTLE_CORRECTION", perm = "READ")
public ApiResponse<List<SettleCorrectionResp>> listByOriginalSettle(@PathVariable Long originalSettleId) {
return ApiResponse.ok(service.listByOriginalSettle(originalSettleId));
}
/** 정정 생성(DRAFT 상태). 차액(diffAmount)은 후-전 금액으로 자동 계산됩니다. 마감 가드 적용. */
@PostMapping
@RequirePermission(menu = "SETTLE_CORRECTION", perm = "CREATE")
@DataChangeLog(menu = "SETTLE_CORRECTION", table = "settle_correction")
@@ -49,6 +65,7 @@ public class SettleCorrectionController {
return ApiResponse.ok(service.create(req));
}
/** 정정 승인(DRAFT → APPROVED). APPROVE 권한 필요. */
@PostMapping("/{correctionId}/approve")
@RequirePermission(menu = "SETTLE_CORRECTION", perm = "APPROVE")
@DataChangeLog(menu = "SETTLE_CORRECTION", table = "settle_correction")
@@ -57,6 +74,7 @@ public class SettleCorrectionController {
return ApiResponse.ok();
}
/** 정정 취소. body 의 cancelReason 에 사유를 담습니다. 이미 반영(APPLIED)된 정정은 취소 불가. */
@PostMapping("/{correctionId}/cancel")
@RequirePermission(menu = "SETTLE_CORRECTION", perm = "UPDATE")
@DataChangeLog(menu = "SETTLE_CORRECTION", table = "settle_correction")
@@ -15,6 +15,15 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 세금계산서(tax_invoice) 관리 컨트롤러.
*
* <p>수수료 지급/거래에 대한 세금계산서 발행 이력을 관리합니다. 공급가액(supplyAmount) + 세액(taxAmount) =
* 합계(totalAmount)로 구성되며, 거래 상대방(상호·사업자번호·대표·주소) 정보를 함께 보관합니다.
*
* <p>발행번호(invoiceNo)는 유일해야 하며 중복 시 등록이 거부됩니다. 잘못 발행한 계산서는 취소(cancel)로 처리합니다.
* 권한(menu=TAX_INVOICE): 조회 READ, 발행 CREATE, 취소 UPDATE.
*/
@Tag(name = "세금계산서 관리")
@RestController
@RequestMapping("/api/tax-invoices")
@@ -23,18 +32,21 @@ public class TaxInvoiceController {
private final TaxInvoiceService service;
/** 세금계산서 목록 조회(검색 + 페이징). */
@GetMapping
@RequirePermission(menu = "TAX_INVOICE", perm = "READ")
public ApiResponse<PageResponse<TaxInvoiceResp>> list(@ModelAttribute TaxInvoiceSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 세금계산서 단건 상세 조회. */
@GetMapping("/{invoiceId}")
@RequirePermission(menu = "TAX_INVOICE", perm = "READ")
public ApiResponse<TaxInvoiceResp> detail(@PathVariable Long invoiceId) {
return ApiResponse.ok(service.detail(invoiceId));
}
/** 세금계산서 발행(등록). 발행번호 중복은 거부됩니다. 합계금액은 공급가액+세액으로 자동 계산됩니다. */
@PostMapping
@RequirePermission(menu = "TAX_INVOICE", perm = "CREATE")
@DataChangeLog(menu = "TAX_INVOICE", table = "tax_invoice")
@@ -42,6 +54,7 @@ public class TaxInvoiceController {
return ApiResponse.ok(service.create(req));
}
/** 세금계산서 취소. body 의 cancelReason 에 취소 사유를 담습니다(취소일은 서버 기준 오늘). */
@PostMapping("/{invoiceId}/cancel")
@RequirePermission(menu = "TAX_INVOICE", perm = "UPDATE")
@DataChangeLog(menu = "TAX_INVOICE", table = "tax_invoice")
@@ -13,6 +13,17 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 해촉 정산(termination_settlement) 관리 컨트롤러.
*
* <p>"해촉"은 설계사 위촉계약이 끝나는 것입니다. 그만둘 때 미지급 수수료(모집/분할/시책)는 정산해 주고,
* 향후 발생할 환수 예상액 등은 차감하여 최종 지급액(finalPayAmount)을 산출하는 일회성 정산입니다.
*
* <p>상태 흐름(TermSettleStatus): CALCULATING(산출중) → CONFIRMED(확정), 또는 CANCELLED(취소).
* 수정·확정은 CALCULATING 상태에서만 가능합니다. 한 설계사에 진행 중인 해촉 정산이 이미 있으면 중복 생성이 거부됩니다.
*
* <p>권한(menu=TERM_SETTLE): 조회 READ, 생성/process CREATE, 수정 UPDATE, 확정 APPROVE.
*/
@Tag(name = "해촉 정산 관리")
@RestController
@RequestMapping("/api/termination-settlements")
@@ -21,18 +32,21 @@ public class TerminationSettlementController {
private final TerminationSettlementService service;
/** 해촉 정산 목록 조회(검색 + 페이징). */
@GetMapping
@RequirePermission(menu = "TERM_SETTLE", perm = "READ")
public ApiResponse<PageResponse<TerminationSettlementResp>> list(@ModelAttribute TerminationSettlementSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 해촉 정산 단건 상세 조회. */
@GetMapping("/{termSettleId}")
@RequirePermission(menu = "TERM_SETTLE", perm = "READ")
public ApiResponse<TerminationSettlementResp> detail(@PathVariable Long termSettleId) {
return ApiResponse.ok(service.detail(termSettleId));
}
/** 해촉 정산 생성(CALCULATING 상태). 진행 중인 해촉 정산이 있으면 거부됩니다. */
@PostMapping
@RequirePermission(menu = "TERM_SETTLE", perm = "CREATE")
@DataChangeLog(menu = "TERM_SETTLE", table = "termination_settlement")
@@ -40,6 +54,7 @@ public class TerminationSettlementController {
return ApiResponse.ok(service.create(req));
}
/** 해촉 정산 금액 수정. 산출 중(CALCULATING) 상태에서만 가능합니다. */
@PutMapping("/{termSettleId}")
@RequirePermission(menu = "TERM_SETTLE", perm = "UPDATE")
@DataChangeLog(menu = "TERM_SETTLE", table = "termination_settlement")
@@ -48,6 +63,7 @@ public class TerminationSettlementController {
return ApiResponse.ok();
}
/** 해촉 발생 시 자동 처리 진입점. 내부적으로 create 와 동일하게 CALCULATING 레코드를 만듭니다. */
@PostMapping("/process")
@RequirePermission(menu = "TERM_SETTLE", perm = "CREATE")
@DataChangeLog(menu = "TERM_SETTLE", table = "termination_settlement")
@@ -55,6 +71,7 @@ public class TerminationSettlementController {
return ApiResponse.ok(service.process(req));
}
/** 해촉 정산 확정(CALCULATING → CONFIRMED). APPROVE 권한 필요. */
@PostMapping("/{termSettleId}/confirm")
@RequirePermission(menu = "TERM_SETTLE", perm = "APPROVE")
@DataChangeLog(menu = "TERM_SETTLE", table = "termination_settlement")
@@ -21,6 +21,15 @@ import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* [수수료 명세서 API]
*
* 신입 개발자 안내:
* - 설계사에게 "이번 달 얼마를 어떻게 지급했는지"를 정리한 문서가 수수료 명세서다.
* - 데이터 출처: 지급(payment) 내역을 설계사+정산월 단위로 합산(StatementAggregator)하여
* 총지급액/원천세/지방소득세/부가세/공제/실수령액을 산출하고, 엑셀 파일로 만들어 보관한다.
* - 발급: 단건(generate) 또는 월별 일괄(generateForMonth). 다운로드 시 상태가 DOWNLOADED 로 바뀐다.
*/
@Tag(name = "수수료 명세서")
@Validated
@RestController
@@ -30,12 +39,14 @@ public class CommissionStatementController {
private final CommissionStatementService statementService;
// 명세서 목록 (검색/페이징)
@GetMapping
@RequirePermission(menu = "STATEMENT", perm = "READ")
public ApiResponse<PageResponse<CommissionStatementResp>> list(@ModelAttribute CommissionStatementSearchParam param) {
return ApiResponse.ok(statementService.list(param));
}
// 명세서 1건 상세
@GetMapping("/{statementId}")
@RequirePermission(menu = "STATEMENT", perm = "READ")
public ApiResponse<CommissionStatementResp> detail(@PathVariable Long statementId) {
@@ -61,6 +72,7 @@ public class CommissionStatementController {
return ApiResponse.ok();
}
// 발급된 명세서 엑셀 파일을 내려준다. 한글 파일명은 RFC5987(filename*=UTF-8'')로 인코딩해 깨짐을 방지.
@Operation(summary = "명세서 다운로드")
@GetMapping("/{statementId}/download")
@RequirePermission(menu = "STATEMENT", perm = "READ")
@@ -13,6 +13,15 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 계약 승계 관리 API.
*
* <p>승계란 한 계약의 담당(귀속) 설계사를 전임(fromAgent)에서 후임(toAgent)으로 넘기는 것을 말한다.
* 퇴사·조직이동 등으로 담당자가 바뀌면, 승계일(successionDate) 이후의 유지수수료는 후임 설계사에게 귀속된다.
* 정산 배치가 이 승계 정보를 참고해 원장의 agent를 이관한다.</p>
*
* <p>메뉴 CONTRACT_SUCCESSION 권한으로 보호된다.</p>
*/
@Tag(name = "계약 승계")
@RestController
@RequestMapping("/api/contract-successions")
@@ -21,6 +30,7 @@ public class ContractSuccessionController {
private final ContractSuccessionService service;
/** 승계 내역 목록 조회(페이징). READ 권한 필요. */
@GetMapping
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "READ")
public ApiResponse<PageResponse<ContractSuccessionResp>> list(
@@ -28,12 +38,14 @@ public class ContractSuccessionController {
return ApiResponse.ok(service.list(param));
}
/** 승계 단건 상세 조회. READ 권한 필요. */
@GetMapping("/{successionId}")
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "READ")
public ApiResponse<ContractSuccessionResp> detail(@PathVariable Long successionId) {
return ApiResponse.ok(service.detail(successionId));
}
/** 승계 등록(담당 설계사 이관 예약). 전임/후임이 같으면 거부된다. CREATE 권한 필요. */
@PostMapping
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "CREATE")
@DataChangeLog(menu = "CONTRACT_SUCCESSION", table = "contract_succession")
@@ -41,6 +53,7 @@ public class ContractSuccessionController {
return ApiResponse.ok(service.create(req));
}
/** 승계 취소(status=CANCELED). 취소 후 정산월부터 유지수수료는 다시 직전 귀속 설계사에게 돌아간다. UPDATE 권한 필요. */
@PostMapping("/{successionId}/cancel")
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "UPDATE")
@DataChangeLog(menu = "CONTRACT_SUCCESSION", table = "contract_succession")
@@ -49,6 +62,7 @@ public class ContractSuccessionController {
return ApiResponse.ok();
}
/** 승계 레코드 물리 삭제. DELETE 권한 필요. */
@DeleteMapping("/{successionId}")
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "DELETE")
@DataChangeLog(menu = "CONTRACT_SUCCESSION", table = "contract_succession")
@@ -12,6 +12,13 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 역할(Role) 및 권한 관리 API.
*
* <p>역할은 메뉴별 권한(menuId + permCode)의 묶음이다. 사용자에게 역할을 부여해
* 접근 권한을 통제한다(RBAC). 역할별 권한 매핑은 savePermissions에서 전체 교체 방식으로 저장된다.
* 메뉴 SYSTEM_ROLE 권한으로 보호된다.</p>
*/
@Tag(name = "역할 관리")
@RestController
@RequestMapping("/api/system/roles")
@@ -20,24 +27,28 @@ public class RoleController {
private final RoleService roleService;
/** 역할 전체 목록 조회. READ 권한 필요. */
@GetMapping
@RequirePermission(menu = "SYSTEM_ROLE", perm = "READ")
public ApiResponse<List<RoleVO>> list() {
return ApiResponse.ok(roleService.list());
}
/** 역할 단건 상세 조회. READ 권한 필요. */
@GetMapping("/{roleId}")
@RequirePermission(menu = "SYSTEM_ROLE", perm = "READ")
public ApiResponse<RoleVO> detail(@PathVariable Long roleId) {
return ApiResponse.ok(roleService.detail(roleId));
}
/** 역할에 부여된 메뉴별 권한 목록 조회. READ 권한 필요. */
@GetMapping("/{roleId}/permissions")
@RequirePermission(menu = "SYSTEM_ROLE", perm = "READ")
public ApiResponse<List<Map<String, Object>>> permissions(@PathVariable Long roleId) {
return ApiResponse.ok(roleService.permissions(roleId));
}
/** 역할 신규 등록. CREATE 권한 필요. */
@PostMapping
@RequirePermission(menu = "SYSTEM_ROLE", perm = "CREATE")
@DataChangeLog(menu = "SYSTEM_ROLE", table = "role")
@@ -45,6 +56,7 @@ public class RoleController {
return ApiResponse.ok(roleService.create(vo));
}
/** 역할 정보 수정. UPDATE 권한 필요. */
@PutMapping("/{roleId}")
@RequirePermission(menu = "SYSTEM_ROLE", perm = "UPDATE")
@DataChangeLog(menu = "SYSTEM_ROLE", table = "role")
@@ -53,6 +65,7 @@ public class RoleController {
return ApiResponse.ok();
}
/** 역할 삭제. DELETE 권한 필요. */
@DeleteMapping("/{roleId}")
@RequirePermission(menu = "SYSTEM_ROLE", perm = "DELETE")
@DataChangeLog(menu = "SYSTEM_ROLE", table = "role")
@@ -61,6 +74,7 @@ public class RoleController {
return ApiResponse.ok();
}
/** 역할의 권한 매핑 일괄 저장(기존 전체 삭제 후 재삽입). UPDATE 권한 필요. */
@PutMapping("/{roleId}/permissions")
@RequirePermission(menu = "SYSTEM_ROLE", perm = "UPDATE")
@DataChangeLog(menu = "SYSTEM_ROLE", table = "role_permission")
@@ -13,6 +13,13 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 시스템 사용자(로그인 계정) 관리 API.
*
* <p>사용자 계정의 생성·수정·비밀번호 초기화·잠금 해제·전화번호 정정을 제공한다.
* 사용자는 역할(role)을 통해 권한을 부여받는다(roleIds). 로그인 ID는 중복 불가다.
* 메뉴 SYSTEM_USER 권한으로 보호된다.</p>
*/
@Tag(name = "사용자 관리")
@RestController
@RequestMapping("/api/system/users")
@@ -21,18 +28,21 @@ public class UserController {
private final UserService service;
/** 사용자 목록 조회(페이징). READ 권한 필요. */
@GetMapping
@RequirePermission(menu = "SYSTEM_USER", perm = "READ")
public ApiResponse<PageResponse<UserResp>> list(@ModelAttribute UserSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 사용자 단건 상세 조회(역할 코드 포함). READ 권한 필요. */
@GetMapping("/{userId}")
@RequirePermission(menu = "SYSTEM_USER", perm = "READ")
public ApiResponse<UserResp> detail(@PathVariable Long userId) {
return ApiResponse.ok(service.detail(userId));
}
/** 사용자 신규 등록. 비밀번호 미지정 시 임시 비밀번호로 생성하고 역할을 부여한다. CREATE 권한 필요. */
@PostMapping
@RequirePermission(menu = "SYSTEM_USER", perm = "CREATE")
@DataChangeLog(menu = "SYSTEM_USER", table = "users")
@@ -40,6 +50,7 @@ public class UserController {
return ApiResponse.ok(service.create(req));
}
/** 사용자 정보 수정. roleIds가 오면 역할을 전체 교체한다. UPDATE 권한 필요. */
@PutMapping("/{userId}")
@RequirePermission(menu = "SYSTEM_USER", perm = "UPDATE")
@DataChangeLog(menu = "SYSTEM_USER", table = "users")
@@ -48,6 +59,7 @@ public class UserController {
return ApiResponse.ok();
}
/** 비밀번호 초기화(임시 비밀번호 발급). UPDATE 권한 필요. */
@PutMapping("/{userId}/reset-password")
@RequirePermission(menu = "SYSTEM_USER", perm = "UPDATE")
public ApiResponse<Void> resetPassword(@PathVariable Long userId) {
@@ -55,6 +67,7 @@ public class UserController {
return ApiResponse.ok();
}
/** 계정 잠금 해제(로그인 실패 횟수 초기화 + ACTIVE 복귀). UPDATE 권한 필요. */
@PutMapping("/{userId}/unlock")
@RequirePermission(menu = "SYSTEM_USER", perm = "UPDATE")
public ApiResponse<Void> unlock(@PathVariable Long userId) {
@@ -17,6 +17,14 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* [부가세 분기신고 API]
*
* 신입 개발자 안내:
* - 부가세(VAT)는 분기 단위(1~4분기)로 신고한다. 과세사업자인 설계사의 지급분에 붙은 부가세를 분기별로 모은다.
* - 흐름: regenerate(분기 재집계 → vat_report UPSERT) → 목록/상세 확인 → complete(신고 제출 완료 표시).
* - settleQuarter 형식은 보통 "YYYYQ"(예: 2026년 1분기)처럼 분기를 식별하는 값이다.
*/
@Tag(name = "부가세 분기신고")
@RestController
@RequestMapping("/api/vat-reports")
@@ -48,6 +56,7 @@ public class VatReportController {
return ApiResponse.ok(service.regenerate(req));
}
// 선택한 신고건들의 상태를 "신고 완료(FILED)"로 변경 (실제 국세청 제출은 외부 절차, 여기선 상태만 갱신)
@PutMapping("/complete")
@Operation(summary = "부가세 분기신고 제출 완료 처리")
@RequirePermission(menu = "VAT_REPORT", perm = "EXECUTE")
@@ -17,6 +17,14 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* [원천세 분기신고 API]
*
* 신입 개발자 안내:
* - 원천세(원천징수세)는 설계사에게 지급할 때 회사가 미리 떼어 국세청에 대신 납부하는 세금이다.
* 사업소득은 3.3%(소득세 3% + 지방소득세 0.3%), 기타소득은 8.8%(소득세 8% + 지방소득세 0.8%)가 일반적이다.
* - 분기별로 떼어둔 원천세를 모아 신고한다. 흐름: regenerate(분기 재집계) → 목록/상세 → complete(제출 완료 표시).
*/
@Tag(name = "원천세 분기신고")
@RestController
@RequestMapping("/api/withholding-tax-reports")
@@ -48,6 +56,7 @@ public class WithholdingTaxReportController {
return ApiResponse.ok(service.regenerate(req));
}
// 선택한 신고건들의 상태를 "신고 완료(FILED)"로 변경 (상태 갱신만, 실제 제출은 외부 절차)
@PutMapping("/complete")
@Operation(summary = "원천세 분기신고 제출 완료 처리")
@RequirePermission(menu = "WITHHOLDING_TAX", perm = "EXECUTE")
@@ -14,6 +14,17 @@ import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
/**
* 보수교육(자격 유지교육) 관리 API.
*
* <p>보험설계사는 법정 의무로 매년 일정 시간의 교육을 이수해야 한다. 이 도메인은 두 부분으로 나뉜다.
* <ul>
* <li>교육 이수 내역(agent_training): 설계사가 들은 개별 교육(과정/일자/시간/수료증 등).</li>
* <li>연도별 의무 기준(training_requirement): 해당 연도에 요구되는 최소 이수 시간.</li>
* </ul>
* annual-hours로 설계사의 연간 누적 이수 시간을 조회해 의무 충족 여부를 판단한다.
* 메뉴 AGENT_TRAINING 권한으로 보호된다.</p>
*/
@Tag(name = "보수교육 관리")
@RestController
@RequiredArgsConstructor
@@ -23,18 +34,21 @@ public class AgentTrainingController {
// ── Training ──────────────────────────────────────────────────────────────
/** 교육 이수 내역 목록 조회(페이징). READ 권한 필요. */
@GetMapping("/api/agent-trainings")
@RequirePermission(menu = "AGENT_TRAINING", perm = "READ")
public ApiResponse<PageResponse<AgentTrainingResp>> list(@ModelAttribute AgentTrainingSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 교육 이수 단건 상세 조회. READ 권한 필요. */
@GetMapping("/api/agent-trainings/{trainingId}")
@RequirePermission(menu = "AGENT_TRAINING", perm = "READ")
public ApiResponse<AgentTrainingResp> detail(@PathVariable Long trainingId) {
return ApiResponse.ok(service.detail(trainingId));
}
/** 설계사의 특정 연도 누적 이수 시간 조회(의무 충족 판단용). READ 권한 필요. */
@GetMapping("/api/agent-trainings/annual-hours")
@RequirePermission(menu = "AGENT_TRAINING", perm = "READ")
public ApiResponse<BigDecimal> annualHours(@RequestParam Long agentId,
@@ -42,6 +56,7 @@ public class AgentTrainingController {
return ApiResponse.ok(service.annualHours(agentId, year));
}
/** 교육 이수 내역 등록. CREATE 권한 필요. */
@PostMapping("/api/agent-trainings")
@RequirePermission(menu = "AGENT_TRAINING", perm = "CREATE")
@DataChangeLog(menu = "AGENT_TRAINING", table = "agent_training")
@@ -49,6 +64,7 @@ public class AgentTrainingController {
return ApiResponse.ok(service.create(req));
}
/** 교육 이수 내역 수정. UPDATE 권한 필요. */
@PutMapping("/api/agent-trainings/{trainingId}")
@RequirePermission(menu = "AGENT_TRAINING", perm = "UPDATE")
@DataChangeLog(menu = "AGENT_TRAINING", table = "agent_training")
@@ -58,6 +74,7 @@ public class AgentTrainingController {
return ApiResponse.ok();
}
/** 교육 이수 내역 삭제. DELETE 권한 필요. */
@DeleteMapping("/api/agent-trainings/{trainingId}")
@RequirePermission(menu = "AGENT_TRAINING", perm = "DELETE")
@DataChangeLog(menu = "AGENT_TRAINING", table = "agent_training")
@@ -68,6 +85,7 @@ public class AgentTrainingController {
// ── Requirement ───────────────────────────────────────────────────────────
/** 연도별 의무 교육시간 기준 목록 조회. READ 권한 필요. */
@GetMapping("/api/training-requirements")
@RequirePermission(menu = "AGENT_TRAINING", perm = "READ")
public ApiResponse<List<TrainingRequirementResp>> listRequirements(
@@ -75,12 +93,14 @@ public class AgentTrainingController {
return ApiResponse.ok(service.listRequirements(param));
}
/** 특정 연도의 의무 교육시간 기준 조회. READ 권한 필요. */
@GetMapping("/api/training-requirements/{year}")
@RequirePermission(menu = "AGENT_TRAINING", perm = "READ")
public ApiResponse<TrainingRequirementResp> requirementByYear(@PathVariable int year) {
return ApiResponse.ok(service.requirementByYear(year));
}
/** 연도별 의무 교육시간 기준 저장(있으면 수정, 없으면 신규 = upsert). UPDATE 권한 필요. */
@PostMapping("/api/training-requirements")
@RequirePermission(menu = "AGENT_TRAINING", perm = "UPDATE")
@DataChangeLog(menu = "AGENT_TRAINING", table = "training_requirement")
@@ -18,15 +18,27 @@ import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
/**
* [엑셀 업로드 API]
*
* 신입 개발자 안내:
* - 보험사/내부에서 받은 엑셀을 "업로드 템플릿(templateCode)"의 규칙대로 파싱·변환하여 DB에 적재한다.
* - 템플릿은 "엑셀의 어느 컬럼(A,B,C..)을 어느 DB 필드(target_field)에 어떻게 변환해 넣을지"를 정의한다.
* - 흐름: (1) 샘플 헤더 추출 → (2) 미리보기로 변환 결과 확인 → (3) 실제 업로드(DB INSERT).
* - 실제 적재/검증 로직은 UploadService 가 담당하고, 여기서는 HTTP 입출력만 연결한다.
*/
@Tag(name = "엑셀 업로드")
@Validated
@RestController
@RequiredArgsConstructor
public class UploadController {
// 업로드 파싱·변환·적재 핵심 서비스
private final UploadService uploadService;
// target_table 별 사용 가능한 컬럼 메타(화이트리스트 + 프론트 옵션) 제공 서비스
private final TargetFieldMetaService metaService;
// [실제 업로드] 파일을 템플릿 규칙대로 파싱·변환·검증 후 배치 INSERT 하고 처리 결과(성공/오류 건수 등)를 돌려준다.
@Operation(summary = "엑셀 업로드 처리",
description = "templateCode 에 해당하는 템플릿으로 파일을 파싱 · 변환 · 배치 INSERT 한다.")
@PostMapping("/api/upload/{templateCode}")
@@ -37,6 +49,7 @@ public class UploadController {
return ApiResponse.ok(result);
}
// [미리보기] DB에 저장하지 않고 상위 N행을 템플릿대로 변환만 해 보여준다. 매핑이 맞는지 사전 확인용.
@Operation(summary = "업로드 미리보기 (상위 5행)",
description = "DB 에 저장하지 않고 변환 결과만 반환한다.")
@PostMapping("/api/upload-templates/{templateCode}/preview")
@@ -49,6 +62,7 @@ public class UploadController {
return ApiResponse.ok(rows);
}
// [헤더 추출] 템플릿을 만들기 전 단계. 샘플 엑셀의 헤더 행과 몇 줄을 읽어 프론트가 컬럼 매핑 UI를 자동 구성하게 한다.
@Operation(summary = "샘플 엑셀 헤더 추출",
description = "템플릿 등록 전에 샘플 파일 첫 행 헤더 + 미리보기 행을 추출한다. " +
"프론트는 이 결과로 sourceColumn(A,B,C..) + sourceHeader 를 자동 채운다.")
@@ -62,6 +76,7 @@ public class UploadController {
return ApiResponse.ok(uploadService.extractHeaders(file, headerRow, sheetIndex, previewRows));
}
// [컬럼 메타] 특정 대상 테이블에 넣을 수 있는 DB 필드 목록(한글명/타입/필수여부)을 반환. 매핑 시 어떤 필드를 고를지 안내.
@Operation(summary = "target_table 별 사용 가능한 컬럼 메타",
description = "프론트의 target_field Select 옵션 + 타입 자동 채우기에 사용한다.")
@GetMapping("/api/upload-templates/target-fields/{targetTable}")
@@ -70,6 +85,7 @@ public class UploadController {
return ApiResponse.ok(metaService.getFields(targetTable));
}
// [대상 테이블 목록] 업로드로 적재 가능한 테이블 이름들(화이트리스트)을 반환.
@Operation(summary = "사용 가능한 target_table 목록")
@GetMapping("/api/upload-templates/target-tables")
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
@@ -14,6 +14,16 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* [업로드 템플릿 관리 API]
*
* 신입 개발자 안내:
* - "업로드 템플릿"이란 엑셀 → DB 적재 규칙의 묶음이다.
* 템플릿 본체(upload_template) + 컬럼 매핑 정의(upload_template_column) + 업로드 이력(upload_history)으로 구성된다.
* - 컬럼 매핑은 "엑셀 컬럼(sourceColumn) → 변환규칙(transformRule) → DB 필드(targetField)"를 한 줄씩 정의한다.
* - 실제 엑셀 파싱/적재는 UploadController + UploadService 가 담당하고, 여기는 템플릿 CRUD/이력 조회만 다룬다.
* - @DataChangeLog: 변경(등록/수정/삭제)은 감사 로그로 남긴다.
*/
@Tag(name = "업로드 템플릿 관리")
@RestController
@RequestMapping("/api/upload-templates")
@@ -22,6 +32,7 @@ public class UploadTemplateController {
private final UploadTemplateService templateService;
// 템플릿 목록 (검색/페이징)
@Operation(summary = "템플릿 목록 조회")
@GetMapping
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
@@ -29,6 +40,7 @@ public class UploadTemplateController {
return ApiResponse.ok(templateService.list(param));
}
// 템플릿 1건 + 그 컬럼 매핑 정의까지 함께 조회
@Operation(summary = "템플릿 상세 조회 (컬럼 포함)")
@GetMapping("/{templateId}")
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
@@ -36,6 +48,7 @@ public class UploadTemplateController {
return ApiResponse.ok(templateService.detail(templateId));
}
// 템플릿 신규 등록 (templateCode 중복 불가). 컬럼 정의를 함께 보내면 같이 저장된다.
@Operation(summary = "템플릿 등록")
@PostMapping
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "CREATE")
@@ -44,6 +57,7 @@ public class UploadTemplateController {
return ApiResponse.ok(templateService.create(req));
}
// 템플릿 수정. 컬럼 정의가 함께 오면 기존 컬럼을 지우고 재등록한다(전체 교체).
@Operation(summary = "템플릿 수정")
@PutMapping("/{templateId}")
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "UPDATE")
@@ -54,6 +68,7 @@ public class UploadTemplateController {
return ApiResponse.ok();
}
// 템플릿 삭제 (컬럼 정의도 함께 삭제)
@Operation(summary = "템플릿 삭제")
@DeleteMapping("/{templateId}")
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "DELETE")
@@ -63,6 +78,7 @@ public class UploadTemplateController {
return ApiResponse.ok();
}
// 템플릿의 컬럼 매핑 정의만 따로 조회
@Operation(summary = "템플릿 컬럼 목록 조회")
@GetMapping("/{templateId}/columns")
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
@@ -70,6 +86,7 @@ public class UploadTemplateController {
return ApiResponse.ok(templateService.columns(templateId));
}
// 컬럼 매핑만 통째로 교체(기존 전체 삭제 후 새로 등록). 매핑 편집 화면 저장에 사용.
@Operation(summary = "템플릿 컬럼 전체 갱신 (기존 삭제 후 재등록)")
@PutMapping("/{templateId}/columns")
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "UPDATE")
@@ -80,6 +97,7 @@ public class UploadTemplateController {
return ApiResponse.ok();
}
// 이 템플릿으로 수행한 과거 업로드 이력(성공/오류 건수, 상태 등) 조회
@Operation(summary = "템플릿 업로드 이력 조회")
@GetMapping("/{templateId}/history")
@RequirePermission(menu = "SYSTEM_UPLOAD_TEMPLATE", perm = "READ")
@@ -13,6 +13,17 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 출금 신청(withdraw_request) 컨트롤러.
*
* <p>설계사가 자기 정산 잔액에서 돈을 빼달라고 요청하는 화면입니다. 신청하면 결재(승인) 절차가 자동으로 연동되고,
* 승인 후 펌뱅킹(은행 이체)으로 실제 입금이 처리됩니다.
*
* <p>상태 흐름(WithdrawStatus): REQUESTED(신청) → APPROVED(승인) → SENT(이체요청) → COMPLETED(입금완료).
* 취소는 아직 신청(REQUESTED) 상태일 때만 가능합니다. 응답의 계좌번호(accountNo)는 뒤 4자리만 보이도록 마스킹됩니다.
*
* <p>권한(menu=WITHDRAW): 조회 READ, 신청 CREATE, 취소 UPDATE.
*/
@Tag(name = "출금 신청")
@RestController
@RequestMapping("/api/withdraw-requests")
@@ -21,18 +32,21 @@ public class WithdrawRequestController {
private final WithdrawRequestService service;
/** 출금 신청 목록 조회(검색 + 페이징). 계좌번호는 마스킹되어 반환됩니다. */
@GetMapping
@RequirePermission(menu = "WITHDRAW", perm = "READ")
public ApiResponse<PageResponse<WithdrawRequestResp>> list(@ModelAttribute WithdrawRequestSearchParam param) {
return ApiResponse.ok(service.list(param));
}
/** 출금 신청 단건 상세 조회. 계좌번호는 마스킹되어 반환됩니다. */
@GetMapping("/{requestId}")
@RequirePermission(menu = "WITHDRAW", perm = "READ")
public ApiResponse<WithdrawRequestResp> detail(@PathVariable Long requestId) {
return ApiResponse.ok(service.detail(requestId));
}
/** 출금 신청 등록(REQUESTED 상태). 등록과 동시에 결재 요청이 자동 생성됩니다. */
@PostMapping
@RequirePermission(menu = "WITHDRAW", perm = "CREATE")
@DataChangeLog(menu = "WITHDRAW", table = "withdraw_request")
@@ -40,6 +54,7 @@ public class WithdrawRequestController {
return ApiResponse.ok(service.create(req));
}
/** 출금 신청 취소. 신청(REQUESTED) 상태에서만 가능합니다. */
@PostMapping("/{requestId}/cancel")
@RequirePermission(menu = "WITHDRAW", perm = "UPDATE")
@DataChangeLog(menu = "WITHDRAW", table = "withdraw_request")
@@ -19,6 +19,13 @@ import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* 계약 회계 분개(contract_accounting_entry) 서비스.
*
* <p>수수료 지급(payment) 건을 복식부기 분개로 자동 변환합니다.
* 분개 1건 = 차변(8350 지급수수료) / 대변(2530 미지급금), 금액은 실지급액(netAmount, 없으면 payAmount)을 사용합니다.
* generateFromPayments 로 분개를 일괄 생성하고, closeJournal 로 전표번호를 부여해 마감합니다.
*/
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@@ -30,11 +37,13 @@ public class ContractAccountingService {
private final ContractAccountingEntryMapper entryMapper;
private final PaymentMapper paymentMapper;
/** 분개 엔트리 목록 조회(페이징). */
public PageResponse<ContractAccountingEntryResp> list(ContractAccountingEntrySearchParam param) {
param.startPage();
return PageResponse.of(entryMapper.selectList(param));
}
/** 지급건들을 분개로 변환해 일괄 INSERT. 대상이 하나도 없으면 NOT_FOUND. 반환값은 생성된 분개 수. */
@Transactional
public int generateFromPayments(GenerateEntriesReq req) {
List<PaymentVO> payments = resolvePayments(req);
@@ -47,6 +56,7 @@ public class ContractAccountingService {
return entryMapper.insertBatch(entries);
}
/** 지정한 분개들에 전표번호를 부여해 마감. 반환값은 마감 처리된 분개 수. */
@Transactional
public int closeJournal(CloseJournalReq req) {
if (req.getEntryIds() == null || req.getEntryIds().isEmpty()) {
@@ -55,6 +65,7 @@ public class ContractAccountingService {
return entryMapper.closeJournal(req.getEntryIds(), req.getJournalNo());
}
/** 분개 대상 지급건 결정: paymentIds 우선, 없으면 settleMonth 전체, 둘 다 없으면 빈 목록. */
private List<PaymentVO> resolvePayments(GenerateEntriesReq req) {
if (req.getPaymentIds() != null && !req.getPaymentIds().isEmpty()) {
List<PaymentVO> result = new ArrayList<>();
@@ -70,6 +81,7 @@ public class ContractAccountingService {
return List.of();
}
/** 지급 1건 → 분개 VO 변환(분개일=오늘, 금액=netAmount 우선). */
private ContractAccountingEntryVO buildEntry(PaymentVO p) {
ContractAccountingEntryVO vo = new ContractAccountingEntryVO();
vo.setPaymentId(p.getPaymentId());
@@ -33,6 +33,21 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
* 전자결재 도메인 비즈니스 로직.
*
* <p>결재선(line)과 결재 단계(step) 템플릿을 관리하고, 결재 요청(request)을 단계별로 진행시킨다.
* 핵심 흐름:
* <ul>
* <li>상신: createRequest → 상태 PENDING, currentStep=1, 1단계 결재자 알림.</li>
* <li>진행: advance → 이력 INSERT 후 APPROVE/REJECT 처리. REJECT면 즉시 REJECTED 종료,
* APPROVE면 다음 단계로 진행하거나(현재단계 &lt; maxStep) 마지막 단계면 APPROVED 후 콜백.</li>
* <li>최종 승인 콜백(handleApprovalCallback): targetType별로 출금/정산/이의신청/시책 도메인 상태를 갱신.
* 출금(WITHDRAW)은 추가로 펌뱅킹 어댑터에 이체를 요청한다(어댑터 실패해도 결재 흐름은 깨지지 않음).</li>
* </ul>
* 외부 발송(SMS/펌뱅킹)은 비핵심(non-critical)으로 처리해 실패 시 로깅만 하고 결재 트랜잭션은 진행한다.
* 클래스 전체는 읽기 전용이며 쓰기 메서드만 별도 트랜잭션을 건다.</p>
*/
@Slf4j
@Service
@RequiredArgsConstructor
@@ -58,21 +73,25 @@ public class ApprovalService {
// ── Line ──────────────────────────────────────────────────────────────────
/** 결재선 목록 조회(페이징). */
public PageResponse<ApprovalLineResp> listLines(ApprovalLineSearchParam param) {
param.startPage();
return PageResponse.of(lineMapper.selectList(param));
}
/** 결재선 상세 조회. 없으면 NOT_FOUND. */
public ApprovalLineResp detailLine(Long lineId) {
ApprovalLineResp resp = lineMapper.selectById(lineId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
/** 특정 결재선의 단계 목록 조회. */
public List<ApprovalLineStepResp> stepsOfLine(Long lineId) {
return stepMapper.selectByLine(lineId);
}
/** 결재선 등록. 활성 상태로 시작하고 작성자를 기록한다. */
@Transactional
public Long createLine(ApprovalLineSaveReq req) {
ApprovalLineVO vo = new ApprovalLineVO();
@@ -86,6 +105,7 @@ public class ApprovalService {
return vo.getLineId();
}
/** 결재선 수정. 존재 검증 후 갱신한다. */
@Transactional
public void updateLine(Long lineId, ApprovalLineSaveReq req) {
ApprovalLineResp existing = lineMapper.selectById(lineId);
@@ -99,6 +119,7 @@ public class ApprovalService {
lineMapper.update(vo);
}
/** 결재선에 단계(step) 추가. 단계 순번(stepNo)과 결재 역할(roleCode)을 저장한다. */
@Transactional
public void addStep(ApprovalLineStepSaveReq req) {
ApprovalLineStepVO vo = new ApprovalLineStepVO();
@@ -110,6 +131,7 @@ public class ApprovalService {
// ── Request ───────────────────────────────────────────────────────────────
/** 결재 요청 전체 목록 조회(페이징). */
public PageResponse<ApprovalRequestResp> listRequests(ApprovalRequestSearchParam param) {
param.startPage();
return PageResponse.of(requestMapper.selectList(param));
@@ -129,12 +151,14 @@ public class ApprovalService {
return PageResponse.of(requestMapper.selectList(param));
}
/** 결재 요청 상세 조회. 없으면 NOT_FOUND. */
public ApprovalRequestResp detailRequest(Long requestId) {
ApprovalRequestResp resp = requestMapper.selectById(requestId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
/** 결재 요청의 단계별 처리 이력 조회. */
public List<ApprovalHistoryResp> historyOfRequest(Long requestId) {
return historyMapper.selectByRequest(requestId);
}
@@ -22,6 +22,13 @@ import java.nio.file.Paths;
import java.util.List;
import java.util.UUID;
/**
* 첨부파일 도메인 비즈니스 로직.
*
* <p>파일은 {@code app.attachment.path}(기본 ./uploads) 아래 refType별 폴더에 UUID 접두 파일명으로 저장하고,
* 메타는 DB에 적재한다. 파일 저장이 실패해도 메타는 남기되 경로를 {@code [STORAGE_FAILED]} 로 표시해
* 다운로드를 막는다(데이터 유실 추적 가능). 클래스 전체는 읽기 전용이며 쓰기 메서드만 별도 트랜잭션을 건다.</p>
*/
@Slf4j
@Service
@RequiredArgsConstructor
@@ -33,15 +40,18 @@ public class AttachmentService {
@Value("${app.attachment.path:./uploads}")
private String uploadBasePath;
/** 첨부 메타 목록 조회(페이징). */
public PageResponse<AttachmentResp> list(AttachmentSearchParam param) {
param.startPage();
return PageResponse.of(mapper.selectList(param));
}
/** 특정 대상(refType+refId)에 연결된 첨부 목록 조회. */
public List<AttachmentResp> listByRef(String refType, Long refId) {
return mapper.selectByRef(refType, refId);
}
/** 첨부 메타 단건 조회. 없으면 NOT_FOUND. */
public AttachmentResp detail(Long attachmentId) {
AttachmentResp resp = mapper.selectById(attachmentId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
@@ -11,26 +11,41 @@ import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
/**
* 차등환수 금액 계산기.
* 조기해지 시 "경과개월수 → 적용 환수율 → 환수액" 순으로 계산한다.
* 핵심 공식: 환수액 = 원수수료 × 환수율(%) / 100 (소수점 반올림).
*/
@Component
@RequiredArgsConstructor
public class ChargebackCalculator {
/** 환수 등급의 유효기간 비교에 쓰는 날짜 포맷. */
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private final ChargebackGradeMapper gradeMapper;
/**
* 환수액을 계산한다.
* 1) 계약일~해지일의 경과개월수(monthsElapsed)를 구한다.
* 2) 상품분류·경과월·기준일에 맞는 환수 등급(환수율 구간)을 조회한다.
* 3) 해당 구간이 없으면 환수 0원, 있으면 원수수료 × 환수율로 환수액을 산출한다.
*/
public ChargebackResult calculate(LocalDate contractDate, LocalDate terminationDate,
BigDecimal originalCommission, String productCategory) {
int monthsElapsed = (int) ChronoUnit.MONTHS.between(contractDate, terminationDate);
String today = LocalDate.now().format(DATE_FMT);
// 상품분류 + 경과월이 속한 환수율 구간을 조회(오늘 기준 유효한 등급)
ChargebackGradeVO grade = gradeMapper.selectByMonths(productCategory, monthsElapsed, today);
if (grade == null) {
// 적용할 환수 구간이 없으면 환수하지 않음(0원)
return new ChargebackResult(BigDecimal.ZERO, monthsElapsed, BigDecimal.ZERO,
"적용 환수 구간 없음");
}
// 환수액 = 원수수료 × 환수율 / 100, 원 단위 반올림(HALF_UP)
BigDecimal chargebackAmount = originalCommission
.multiply(grade.getChargebackPercent())
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP);
@@ -15,20 +15,28 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* 차등환수 등급(chargeback_grade) 마스터 비즈니스 로직 서비스.
* "경과개월 구간별 환수율(%)" 기준표를 CRUD 하고 활성/비활성을 관리한다.
* 이 기준표가 ChargebackCalculator 의 환수율 조회 대상이 된다.
*/
@Service
@RequiredArgsConstructor
public class ChargebackGradeService {
/** 등급 유효기간 비교용 날짜 포맷. */
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private final ChargebackGradeMapper gradeMapper;
/** 환수 등급표 목록 페이징 조회. */
@Transactional(readOnly = true)
public PageResponse<ChargebackGradeResp> list(ChargebackGradeSearchParam param) {
param.startPage();
return PageResponse.of(gradeMapper.selectList(param));
}
/** 환수 등급 상세 조회. 없으면 NOT_FOUND. */
@Transactional(readOnly = true)
public ChargebackGradeResp detail(Long gradeId) {
ChargebackGradeResp resp = gradeMapper.selectDetailById(gradeId);
@@ -36,6 +44,7 @@ public class ChargebackGradeService {
return resp;
}
/** 환수 등급 신규 등록. 같은 상품분류의 시작개월(monthsFrom) 구간이 이미 활성이면 중복 오류. */
@Transactional
public void create(ChargebackGradeSaveReq req) {
String today = LocalDate.now().format(DATE_FMT);
@@ -47,6 +56,7 @@ public class ChargebackGradeService {
gradeMapper.insert(toVO(req));
}
/** 환수 등급 수정. 활성 여부(isActive)는 기존 값을 유지(수정 API로는 변경하지 않음). */
@Transactional
public void update(Long gradeId, ChargebackGradeSaveReq req) {
ChargebackGradeVO existing = gradeMapper.selectById(gradeId);
@@ -57,6 +67,7 @@ public class ChargebackGradeService {
gradeMapper.update(vo);
}
/** 환수 등급 비활성화. 이미 비활성이면 잘못된 요청 처리. */
@Transactional
public void deactivate(Long gradeId) {
ChargebackGradeVO existing = gradeMapper.selectById(gradeId);
@@ -67,11 +78,13 @@ public class ChargebackGradeService {
gradeMapper.updateStatus(gradeId, false);
}
/** 상품분류·경과월·기준일에 해당하는 환수율 구간 1건 조회(계산기가 사용). */
@Transactional(readOnly = true)
public ChargebackGradeVO selectByMonths(String productCategory, Integer monthsElapsed, String baseDate) {
return gradeMapper.selectByMonths(productCategory, monthsElapsed, baseDate);
}
/** 저장 요청 DTO(SaveReq)를 DB 엔티티(VO)로 변환하는 내부 헬퍼. 신규는 항상 활성(true). */
private ChargebackGradeVO toVO(ChargebackGradeSaveReq req) {
ChargebackGradeVO vo = new ChargebackGradeVO();
vo.setProductCategory(req.getProductCategory());

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