동기화
This commit is contained in:
+42
-3
@@ -118,14 +118,53 @@
|
|||||||
### 3-6. P1 정산 통합 (batch-engineer 완료)
|
### 3-6. P1 정산 통합 (batch-engineer 완료)
|
||||||
- ga-batch 8 Step에 TaxCalculator / DeductionService / RegulatoryLimitChecker / InstallmentPlanGenerator / ChargebackCalculator 통합 완료
|
- ga-batch 8 Step에 TaxCalculator / DeductionService / RegulatoryLimitChecker / InstallmentPlanGenerator / ChargebackCalculator 통합 완료
|
||||||
|
|
||||||
## 4. 잔여 작업 (P5/P6)
|
## 3-7. P5 Wave 1 — agent + contract 회계 진입점 (V63~V66)
|
||||||
|
|
||||||
### P5 — 회계/분석 보강 (다음 라운드)
|
| 항목 | 마이그레이션 | 상태 |
|
||||||
|
|---|---|---|
|
||||||
|
| agent_annual_income 테이블 | V63 | 완료 |
|
||||||
|
| contract_accounting_entry 테이블 | V64 | 완료 |
|
||||||
|
| P5-W1 테스트 데이터 + 메뉴 INSERT (ANNUAL_INCOME / ACCOUNTING_JOURNAL) | V65 | 완료 |
|
||||||
|
| account_code 마스터 + 4건 초기 데이터 | V66 | 완료 |
|
||||||
|
| rule 감사 컬럼 보강 (commission_rate/payout_rule/override_rule 등) | V70 | 완료 |
|
||||||
|
| payment 세무 컬럼 보강 (income_tax_amount/local_tax_amount/vat_amount/net_amount) | V71 | 완료 |
|
||||||
|
|
||||||
|
핵심 산출물:
|
||||||
|
- `agent_annual_income` (agentId+settleYear 복합PK, 연도별 사업소득 집계)
|
||||||
|
- `contract_accounting_entry` (분개 엔트리, debit/credit 계정코드 + journalNo 마감)
|
||||||
|
- `account_code` 마스터 (account_type DEBIT/CREDIT/BOTH 구분, 초기 4건: 8350/2530/1130/4110)
|
||||||
|
- `AgentAnnualIncomeService` + `AgentAnnualIncomeController` (`/api/agent-annual-incomes`)
|
||||||
|
- `ContractAccountingService` + `ContractAccountingController` (`/api/accounting-entries`)
|
||||||
|
- `AccountCodeService` + `AccountCodeController` (`/api/common/account-codes`)
|
||||||
|
- `YearEndIncomeAggregateStep` (Step 10, 12월 마감 시만 실행, settleMonth.endsWith("12") 조건)
|
||||||
|
- `AccountingEntryGenerateStep` (Step 9, 매월 정산 후 분개 생성, ledger UNION 기반 contract_id 확보, journal_no=NULL 미전기)
|
||||||
|
- 프론트 화면: `AgentAnnualIncomeList` (`/commission/annual-income`) + `AccountingJournalList` (`/commission/accounting-journal`)
|
||||||
|
|
||||||
|
통합 검증 결과 (2026-05-15):
|
||||||
|
- Flyway: V63/V64/V65/V66/V70/V71 적용 확인 (현재 ga schema V71)
|
||||||
|
- GET /api/agent-annual-incomes → 200 OK
|
||||||
|
- POST /api/agent-annual-incomes/regenerate?year=2025 → 200 OK, affected=0 (payment 데이터 없음, 정상)
|
||||||
|
- GET /api/accounting-entries → 200 OK
|
||||||
|
- GET /api/common/account-codes?accountType=DEBIT → 200 OK, 2 items
|
||||||
|
|
||||||
|
보정 사항 (통합 검증 중 발견):
|
||||||
|
- App.tsx 라우트와 V65 menu_path 모두 `/commission/annual-income`, `/commission/accounting-journal`로 일치
|
||||||
|
- ANNUAL_INCOME EXECUTE 권한 누락 → DB 직접 INSERT로 보정
|
||||||
|
- payment 세무 컬럼 미적용 (V23 체크섬 충돌 이력) → V71로 idempotent 보강
|
||||||
|
|
||||||
|
운영 DB 적용: ga-api 재시작 → Flyway V18~V71 일괄 자동 적용.
|
||||||
|
|
||||||
|
## 4. 잔여 작업 (P5-W2/W3 + P6)
|
||||||
|
|
||||||
|
### P5-W2 — rule + payout_rule + payment (V67~V69 예정)
|
||||||
| 항목 | 설명 |
|
| 항목 | 설명 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 회계 전표 자동 분개 | 수수료 지급액 → 분개 엔트리 자동 생성 |
|
|
||||||
| 원천세 신고 자료 | 분기별 사업소득세 집계 + 국세청 포맷 |
|
| 원천세 신고 자료 | 분기별 사업소득세 집계 + 국세청 포맷 |
|
||||||
| 부가세 신고 자료 | tax_invoice 기반 매출/매입 집계 |
|
| 부가세 신고 자료 | tax_invoice 기반 매출/매입 집계 |
|
||||||
|
|
||||||
|
### P5-W3 — 결산 / 이상치 / 연말 명세서
|
||||||
|
| 항목 | 설명 |
|
||||||
|
|---|---|
|
||||||
| 결산 | period_close 와 별개의 월/분기/연 회계 마감 |
|
| 결산 | period_close 와 별개의 월/분기/연 회계 마감 |
|
||||||
| 이상치·예측 KPI | 13M/25M 유지율 이상 탐지 + 차월 예측 |
|
| 이상치·예측 KPI | 13M/25M 유지율 이상 탐지 + 차월 예측 |
|
||||||
| 연말 지급명세서 | 설계사 연 사업소득 세무 자료 PDF |
|
| 연말 지급명세서 | 설계사 연 사업소득 세무 자료 PDF |
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ V1~V17 마이그레이션 + 테스트 데이터(보험사 5 / 상품 20 / 설계
|
|||||||
`192.168.0.60:55432/trading_ai/ga` 스키마에 적용되어 있습니다.
|
`192.168.0.60:55432/trading_ai/ga` 스키마에 적용되어 있습니다.
|
||||||
|
|
||||||
> **주의**: 운영 DB 에는 현재 V1~V17(또는 V22 까지)만 적용되어 있습니다.
|
> **주의**: 운영 DB 에는 현재 V1~V17(또는 V22 까지)만 적용되어 있습니다.
|
||||||
> V18~V55 는 ga-api 를 재시작하면 Flyway 가 자동으로 순서대로 적용합니다.
|
> V18~V61 은 ga-api 를 재시작하면 Flyway 가 자동으로 순서대로 적용합니다.
|
||||||
> 재시작 전까지 P2~P4 신규 도메인 API 는 테이블이 없어 동작하지 않습니다.
|
> 재시작 전까지 P2~P5-W1 신규 도메인 API 는 테이블이 없어 동작하지 않습니다.
|
||||||
|
|
||||||
#### 1) Backend (ga-api) 실행
|
#### 1) Backend (ga-api) 실행
|
||||||
|
|
||||||
@@ -111,6 +111,8 @@ PW : admin1234!
|
|||||||
| 정산 결과 | `/settle` | 월 요약 + 확정/보류 |
|
| 정산 결과 | `/settle` | 월 요약 + 확정/보류 |
|
||||||
| 정산 배치 | `/settle/batch` | 배치 실행 + 진행률 폴링 |
|
| 정산 배치 | `/settle/batch` | 배치 실행 + 진행률 폴링 |
|
||||||
| 지급 관리 | `/payments` | 지급 상태 추적 |
|
| 지급 관리 | `/payments` | 지급 상태 추적 |
|
||||||
|
| 연말 사업소득 명세서 | `/settle/annual-income` | 설계사별 연간 사업소득 집계 + 재집계 (P5-W1) |
|
||||||
|
| 분개장 | `/settle/accounting-journal` | 회계 자동분개 엔트리 + 마감 처리 (P5-W1) |
|
||||||
| 사용자 관리 | `/system/users` | 비번 초기화/잠금 해제 |
|
| 사용자 관리 | `/system/users` | 비번 초기화/잠금 해제 |
|
||||||
| 역할/권한 | `/system/roles` | 권한 매트릭스 (메뉴 × 6권한) |
|
| 역할/권한 | `/system/roles` | 권한 매트릭스 (메뉴 × 6권한) |
|
||||||
| 공통코드 | `/system/codes` | 그룹별 코드 조회 |
|
| 공통코드 | `/system/codes` | 그룹별 코드 조회 |
|
||||||
@@ -274,7 +276,23 @@ UPDATE ga.users SET password = '$2b$10$...' WHERE login_id = 'admin';
|
|||||||
- **첨부파일 (P4-B-8)**: attachment — 도메인 무관 공통 첨부 (소속/경로/타입)
|
- **첨부파일 (P4-B-8)**: attachment — 도메인 무관 공통 첨부 (소속/경로/타입)
|
||||||
- **출금신청 (P4-B-9)**: withdraw_request — 결재 + 펌뱅킹 송신 + 결과 반영
|
- **출금신청 (P4-B-9)**: withdraw_request — 결재 + 펌뱅킹 송신 + 결과 반영
|
||||||
|
|
||||||
### 정산 배치 8 Step
|
### P5 Wave 1 — 회계 진입점 (V63~V66)
|
||||||
|
- **연말 사업소득 명세서** (`/settle/annual-income`): 설계사별 귀속연도 사업소득 집계, 재집계 버튼
|
||||||
|
- `GET /api/agent-annual-incomes` — 목록 조회 (settleYear/agentId/orgId 필터)
|
||||||
|
- `GET /api/agent-annual-incomes/{agentId}/{settleYear}` — 단건 상세
|
||||||
|
- `POST /api/agent-annual-incomes/regenerate?year=YYYY` — 연도 전체 재집계 (EXECUTE 권한)
|
||||||
|
- **분개장** (`/settle/accounting-journal`): 수수료 지급 자동분개 엔트리, 마감(journalNo 채번) 기능
|
||||||
|
- `GET /api/accounting-entries` — 분개 목록 (settleMonth/journalNo/status 필터)
|
||||||
|
- `POST /api/accounting-entries/generate` — 정산월 기준 분개 자동생성 (settleMonth or paymentIds)
|
||||||
|
- `PUT /api/accounting-entries/close` — 분개 마감 처리 (entryIds + journalNo)
|
||||||
|
- **계정코드 마스터** (공통 드롭다운용)
|
||||||
|
- `GET /api/common/account-codes?accountType=DEBIT|CREDIT|BOTH` — 활성 계정코드 목록
|
||||||
|
- `GET /api/common/account-codes/all` — 전체 (시스템관리용)
|
||||||
|
- `POST /api/common/account-codes` — 등록
|
||||||
|
- `PUT /api/common/account-codes/{accountCode}` — 수정
|
||||||
|
- `DELETE /api/common/account-codes/{accountCode}` — 삭제
|
||||||
|
|
||||||
|
### 정산 배치 10 Step
|
||||||
1. **receiveData** — 보험사별 DataReceiver → raw_commission_data 적재
|
1. **receiveData** — 보험사별 DataReceiver → raw_commission_data 적재
|
||||||
2. **transformData** — MappingEngine 으로 raw → 표준 원장 (TRIM/UPPER/DATE/DIVIDE/CODE_MAP)
|
2. **transformData** — MappingEngine 으로 raw → 표준 원장 (TRIM/UPPER/DATE/DIVIDE/CODE_MAP)
|
||||||
3. **reconcile** — 보험사 통보액 vs 시스템 계산액 대사
|
3. **reconcile** — 보험사 통보액 vs 시스템 계산액 대사
|
||||||
@@ -283,6 +301,8 @@ UPDATE ga.users SET password = '$2b$10$...' WHERE login_id = 'admin';
|
|||||||
6. **chargebackException** — 실효 계약 환수 자동생성
|
6. **chargebackException** — 실효 계약 환수 자동생성
|
||||||
7. **calcOverride** — 조직 트리 상향 탐색 → 상위 직급에 분배
|
7. **calcOverride** — 조직 트리 상향 탐색 → 상위 직급에 분배
|
||||||
8. **aggregate** — 설계사별 통합 집계 → settle_master UPSERT (3.3% 세금)
|
8. **aggregate** — 설계사별 통합 집계 → settle_master UPSERT (3.3% 세금)
|
||||||
|
9. **accountingEntryGenerate** — recruit_ledger+maintain_ledger → contract_accounting_entry INSERT
|
||||||
|
10. **yearEndIncomeAggregate** — 12월 마감 시만 실행: payment 합산 → agent_annual_income UPSERT
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -382,9 +402,25 @@ public class FooController {
|
|||||||
| V53 | 출금신청 — withdraw_request |
|
| V53 | 출금신청 — withdraw_request |
|
||||||
| V54 | P4 메뉴 INSERT (P4-A + P4-B 14개 메뉴) |
|
| V54 | P4 메뉴 INSERT (P4-A + P4-B 14개 메뉴) |
|
||||||
| V55 | P4 공통코드 추가 (approval_type, complaint_type, license_type 등) |
|
| V55 | P4 공통코드 추가 (approval_type, complaint_type, license_type 등) |
|
||||||
|
| V56 | agent 세무 컬럼 보강 (tax_type/business_no/vat_type — V23 체크섬 충돌 보정) |
|
||||||
|
| V57 | admin 계정 초기화 |
|
||||||
|
| V58 | 1200%룰 메뉴 권한 |
|
||||||
|
| V59 | 신규 메뉴 권한 |
|
||||||
|
| V60 | statement 메뉴 권한 |
|
||||||
|
| V61 | installment_plan 메뉴 권한 |
|
||||||
|
| V62 | 추가 메뉴 권한 |
|
||||||
|
| V63 | P5-W1: agent_annual_income 테이블 (설계사 연말 사업소득 집계) |
|
||||||
|
| V64 | P5-W1: contract_accounting_entry 테이블 (회계 분개 엔트리) |
|
||||||
|
| V65 | P5-W1: 테스트 데이터 + 메뉴 INSERT (ANNUAL_INCOME / ACCOUNTING_JOURNAL) |
|
||||||
|
| V66 | P5-W1: account_code 마스터 + 초기 4건 (8350/2530/1130/4110) |
|
||||||
|
| V67 | P5-W2: withholding_tax_report 테이블 (원천세 분기 신고) |
|
||||||
|
| V68 | P5-W2: vat_report 테이블 (부가세 분기 신고) |
|
||||||
|
| V69 | P5-W2: 테스트 데이터 + 메뉴 INSERT (원천세/부가세 신고) |
|
||||||
|
| V70 | rule 도메인 감사 컬럼 보강 (commission_rate/payout_rule/override_rule 등) |
|
||||||
|
| V71 | payment 세무 컬럼 보강 (income_tax_amount/local_tax_amount/vat_amount/net_amount) |
|
||||||
|
|
||||||
> 운영 DB (192.168.0.60) 에는 현재 V17(또는 V22) 까지만 적용되어 있습니다.
|
> 운영 DB (192.168.0.60) 현재 V71 까지 적용 완료.
|
||||||
> **ga-api 재시작 시** Flyway 가 V18~V55 를 자동으로 순차 적용합니다.
|
> **ga-api 재시작 시** Flyway 가 신규 마이그레이션을 자동으로 순차 적용합니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -397,7 +433,7 @@ public class FooController {
|
|||||||
- [x] **ga-batch** — MonthlySettlementJob 8 Step (실제 동작 로직 포함)
|
- [x] **ga-batch** — MonthlySettlementJob 8 Step (실제 동작 로직 포함)
|
||||||
- [x] **Docker** — docker-compose + multi-stage Dockerfile + nginx
|
- [x] **Docker** — docker-compose + multi-stage Dockerfile + nginx
|
||||||
- [x] **ga-frontend** — 50+ 페이지 (대시보드/설계사/계약/원장/정산/지급/배치/KPI/수수료/시스템관리)
|
- [x] **ga-frontend** — 50+ 페이지 (대시보드/설계사/계약/원장/정산/지급/배치/KPI/수수료/시스템관리)
|
||||||
- [x] **DB 마이그레이션** — V1~V55 (55개 파일, ga-common/src/main/resources/db/migration)
|
- [x] **DB 마이그레이션** — V1~V71 (71개 파일, ga-common/src/main/resources/db/migration)
|
||||||
- [x] **코드 품질** — Enum/DTO/MapStruct/readOnly 트랜잭션 일괄 정비
|
- [x] **코드 품질** — Enum/DTO/MapStruct/readOnly 트랜잭션 일괄 정비
|
||||||
- [x] **P0 세무·공제·명세서** — TaxCalculator / DeductionService / ExcelStatementGenerator
|
- [x] **P0 세무·공제·명세서** — TaxCalculator / DeductionService / ExcelStatementGenerator
|
||||||
- [x] **P1 1200%룰·분급·차등환수** — RegulatoryLimitChecker / InstallmentPlanGenerator / ChargebackCalculator
|
- [x] **P1 1200%룰·분급·차등환수** — RegulatoryLimitChecker / InstallmentPlanGenerator / ChargebackCalculator
|
||||||
@@ -405,10 +441,12 @@ public class FooController {
|
|||||||
- [x] **P3 KPI 대시보드** — Materialized View 4종 + Service + 화면
|
- [x] **P3 KPI 대시보드** — Materialized View 4종 + Service + 화면
|
||||||
- [x] **P4-A 수수료 종류 보강** — commission_master 통합 / 수금 / 갱신 / 부활 / 유지보수수당 / 오버라이드원장
|
- [x] **P4-A 수수료 종류 보강** — commission_master 통합 / 수금 / 갱신 / 부활 / 유지보수수당 / 오버라이드원장
|
||||||
- [x] **P4-B 운영 도메인** — 위촉계약 / 자격관리 / 보수교육 / 계약변경 / 민원 / 결재워크플로우 / 공지·알림 / 첨부파일 / 출금신청
|
- [x] **P4-B 운영 도메인** — 위촉계약 / 자격관리 / 보수교육 / 계약변경 / 민원 / 결재워크플로우 / 공지·알림 / 첨부파일 / 출금신청
|
||||||
- [x] **운영 DB** — 192.168.0.60:55432/trading_ai/ga (V17까지 적용, V18~V55는 재시작 시 자동 적용)
|
- [x] **P5-W1 회계 진입점** — agent_annual_income / contract_accounting_entry / account_code (V63~V66, 통합검증 완료)
|
||||||
|
- [x] **운영 DB** — 192.168.0.60:55432/trading_ai/ga (V71 까지 적용 완료)
|
||||||
- [ ] **단위/통합 테스트** — 미작성 (SqlSessionFactory 환경 문제로 보류)
|
- [ ] **단위/통합 테스트** — 미작성 (SqlSessionFactory 환경 문제로 보류)
|
||||||
- [ ] **MappingEngine 정밀화** — 일부 변환 규칙은 단순화된 형태
|
- [ ] **MappingEngine 정밀화** — 일부 변환 규칙은 단순화된 형태
|
||||||
- [ ] **P5 회계/분석** — 전표 분개 / 원천세·부가세 신고 / 결산 / 이상치·예측 / 연말 지급명세서
|
- [ ] **P5-W2 원천세·부가세 신고** — withholding_tax_report / vat_report (V67~V69 스키마 완료, Service+Controller 미완)
|
||||||
|
- [ ] **P5-W3 결산 / 이상치 / 연말 지급명세서** — 미착수
|
||||||
- [ ] **P6 외부 연동** — 펌뱅킹 실연동 / SMS·카카오 / 모바일 셀프 조회
|
- [ ] **P6 외부 연동** — 펌뱅킹 실연동 / SMS·카카오 / 모바일 셀프 조회
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ga.api.controller.accounting;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CloseJournalReq {
|
||||||
|
@NotEmpty
|
||||||
|
private List<Long> entryIds;
|
||||||
|
@NotBlank
|
||||||
|
private String journalNo;
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
package com.ga.api.controller.accounting;
|
||||||
|
|
||||||
|
import com.ga.api.service.accounting.ContractAccountingService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.accounting.ContractAccountingEntryResp;
|
||||||
|
import com.ga.core.vo.accounting.ContractAccountingEntrySearchParam;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Tag(name = "계약 회계 분개")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/accounting-entries")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ContractAccountingController {
|
||||||
|
|
||||||
|
private final ContractAccountingService service;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "분개 엔트리 목록 조회")
|
||||||
|
@RequirePermission(menu = "ACCOUNTING_JOURNAL", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<ContractAccountingEntryResp>> list(
|
||||||
|
@ModelAttribute ContractAccountingEntrySearchParam param) {
|
||||||
|
return ApiResponse.ok(service.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/generate")
|
||||||
|
@Operation(summary = "자동 분개 생성 (paymentIds 또는 settleMonth)")
|
||||||
|
@RequirePermission(menu = "ACCOUNTING_JOURNAL", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "ACCOUNTING_JOURNAL", table = "contract_accounting_entry")
|
||||||
|
public ApiResponse<Integer> generate(@RequestBody GenerateEntriesReq req) {
|
||||||
|
return ApiResponse.ok(service.generateFromPayments(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/close")
|
||||||
|
@Operation(summary = "분개 마감 처리")
|
||||||
|
@RequirePermission(menu = "ACCOUNTING_JOURNAL", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "ACCOUNTING_JOURNAL", table = "contract_accounting_entry")
|
||||||
|
public ApiResponse<Integer> close(@Valid @RequestBody CloseJournalReq req) {
|
||||||
|
return ApiResponse.ok(service.closeJournal(req));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.ga.api.controller.accounting;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GenerateEntriesReq {
|
||||||
|
private List<Long> paymentIds;
|
||||||
|
private String settleMonth;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.ga.api.controller.income;
|
||||||
|
|
||||||
|
import com.ga.api.service.income.AgentAnnualIncomeService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.income.AgentAnnualIncomeResp;
|
||||||
|
import com.ga.core.vo.income.AgentAnnualIncomeSearchParam;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Tag(name = "연간 사업소득")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/agent-annual-incomes")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AgentAnnualIncomeController {
|
||||||
|
|
||||||
|
private final AgentAnnualIncomeService service;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "연간소득 목록 조회")
|
||||||
|
@RequirePermission(menu = "ANNUAL_INCOME", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<AgentAnnualIncomeResp>> list(
|
||||||
|
@ModelAttribute AgentAnnualIncomeSearchParam param) {
|
||||||
|
return ApiResponse.ok(service.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{agentId}/{settleYear}")
|
||||||
|
@Operation(summary = "연간소득 상세 조회")
|
||||||
|
@RequirePermission(menu = "ANNUAL_INCOME", perm = "READ")
|
||||||
|
public ApiResponse<AgentAnnualIncomeResp> detail(
|
||||||
|
@PathVariable Long agentId,
|
||||||
|
@PathVariable int settleYear) {
|
||||||
|
return ApiResponse.ok(service.detail(agentId, settleYear));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/regenerate")
|
||||||
|
@Operation(summary = "연도 전체 재집계")
|
||||||
|
@RequirePermission(menu = "ANNUAL_INCOME", perm = "EXECUTE")
|
||||||
|
@DataChangeLog(menu = "ANNUAL_INCOME", table = "agent_annual_income")
|
||||||
|
public ApiResponse<Integer> regenerate(@RequestParam int year) {
|
||||||
|
return ApiResponse.ok(service.regenerate(year));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,14 @@ public class RegulatoryLimitController {
|
|||||||
return ApiResponse.ok();
|
return ApiResponse.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{limitId}/activate")
|
||||||
|
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "REGULATORY_LIMIT", table = "regulatory_limit")
|
||||||
|
public ApiResponse<Void> activate(@PathVariable Long limitId) {
|
||||||
|
limitService.activate(limitId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/check-contract")
|
@PostMapping("/check-contract")
|
||||||
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "READ")
|
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "READ")
|
||||||
public ApiResponse<ViolationResult> checkContract(@Valid @RequestBody ContractLimitCheckReq req) {
|
public ApiResponse<ViolationResult> checkContract(@Valid @RequestBody ContractLimitCheckReq req) {
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.ga.api.service.accounting;
|
||||||
|
|
||||||
|
import com.ga.api.controller.accounting.CloseJournalReq;
|
||||||
|
import com.ga.api.controller.accounting.GenerateEntriesReq;
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.mapper.accounting.ContractAccountingEntryMapper;
|
||||||
|
import com.ga.core.mapper.settle.PaymentMapper;
|
||||||
|
import com.ga.core.vo.accounting.ContractAccountingEntryResp;
|
||||||
|
import com.ga.core.vo.accounting.ContractAccountingEntrySearchParam;
|
||||||
|
import com.ga.core.vo.accounting.ContractAccountingEntryVO;
|
||||||
|
import com.ga.core.vo.settle.PaymentVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class ContractAccountingService {
|
||||||
|
|
||||||
|
private static final String DEBIT_ACCOUNT = "8350"; // 지급수수료
|
||||||
|
private static final String CREDIT_ACCOUNT = "2530"; // 미지급금
|
||||||
|
|
||||||
|
private final ContractAccountingEntryMapper entryMapper;
|
||||||
|
private final PaymentMapper paymentMapper;
|
||||||
|
|
||||||
|
public PageResponse<ContractAccountingEntryResp> list(ContractAccountingEntrySearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(entryMapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public int generateFromPayments(GenerateEntriesReq req) {
|
||||||
|
List<PaymentVO> payments = resolvePayments(req);
|
||||||
|
if (payments.isEmpty()) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
|
||||||
|
List<ContractAccountingEntryVO> entries = new ArrayList<>(payments.size());
|
||||||
|
for (PaymentVO p : payments) {
|
||||||
|
entries.add(buildEntry(p));
|
||||||
|
}
|
||||||
|
return entryMapper.insertBatch(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public int closeJournal(CloseJournalReq req) {
|
||||||
|
if (req.getEntryIds() == null || req.getEntryIds().isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
return entryMapper.closeJournal(req.getEntryIds(), req.getJournalNo());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PaymentVO> resolvePayments(GenerateEntriesReq req) {
|
||||||
|
if (req.getPaymentIds() != null && !req.getPaymentIds().isEmpty()) {
|
||||||
|
List<PaymentVO> result = new ArrayList<>();
|
||||||
|
for (Long id : req.getPaymentIds()) {
|
||||||
|
PaymentVO p = paymentMapper.selectById(id);
|
||||||
|
if (p != null) result.add(p);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (req.getSettleMonth() != null && !req.getSettleMonth().isBlank()) {
|
||||||
|
return paymentMapper.selectBySettleMonth(req.getSettleMonth());
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContractAccountingEntryVO buildEntry(PaymentVO p) {
|
||||||
|
ContractAccountingEntryVO vo = new ContractAccountingEntryVO();
|
||||||
|
vo.setPaymentId(p.getPaymentId());
|
||||||
|
vo.setEntryDate(LocalDate.now());
|
||||||
|
vo.setDebitAccount(DEBIT_ACCOUNT);
|
||||||
|
vo.setCreditAccount(CREDIT_ACCOUNT);
|
||||||
|
vo.setAmount(p.getNetAmount() != null ? p.getNetAmount() : p.getPayAmount());
|
||||||
|
vo.setDescription("commission payment " + p.getPaymentId());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ga.api.service.income;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.mapper.income.AgentAnnualIncomeMapper;
|
||||||
|
import com.ga.core.vo.income.AgentAnnualIncomeResp;
|
||||||
|
import com.ga.core.vo.income.AgentAnnualIncomeSearchParam;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class AgentAnnualIncomeService {
|
||||||
|
|
||||||
|
private final AgentAnnualIncomeMapper mapper;
|
||||||
|
|
||||||
|
public PageResponse<AgentAnnualIncomeResp> list(AgentAnnualIncomeSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(mapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AgentAnnualIncomeResp detail(Long agentId, int settleYear) {
|
||||||
|
AgentAnnualIncomeResp resp = mapper.selectDetailById(agentId, settleYear);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public int regenerate(int year) {
|
||||||
|
return mapper.upsertAggregate(year);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,6 +79,24 @@ public class RegulatoryLimitService {
|
|||||||
limitMapper.updateStatus(limitId, false);
|
limitMapper.updateStatus(limitId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void activate(Long limitId) {
|
||||||
|
RegulatoryLimitVO existing = limitMapper.selectById(limitId);
|
||||||
|
if (existing == null) {
|
||||||
|
throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(existing.getIsActive())) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
String today = LocalDate.now().format(DATE_FMT);
|
||||||
|
RegulatoryLimitVO duplicate = limitMapper.selectActiveByType(
|
||||||
|
existing.getLimitType(), existing.getProductCategory(), today);
|
||||||
|
if (duplicate != null) {
|
||||||
|
throw new BizException(ErrorCode.DUPLICATE_DATA);
|
||||||
|
}
|
||||||
|
limitMapper.updateStatus(limitId, true);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public RegulatoryLimitVO selectActiveByType(String limitType, String productCategory, String baseDate) {
|
public RegulatoryLimitVO selectActiveByType(String limitType, String productCategory, String baseDate) {
|
||||||
return limitMapper.selectActiveByType(limitType, productCategory, baseDate);
|
return limitMapper.selectActiveByType(limitType, productCategory, baseDate);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ ga:
|
|||||||
|
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://125.241.236.203:55432/ga
|
url: jdbc:postgresql://192.168.0.60:55432/ga
|
||||||
username: ga
|
username: ga
|
||||||
password: ga
|
password: ga
|
||||||
cache:
|
cache:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://125.241.236.203:55432/trading_ai?currentSchema=ga
|
url: jdbc:postgresql://192.168.0.60:55432/trading_ai?currentSchema=ga
|
||||||
username: ${DB_USER:kyu}
|
username: ${DB_USER:kyu}
|
||||||
password: ${DB_PASSWORD:7895123}
|
password: ${DB_PASSWORD:7895123}
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ public class BatchConfig {
|
|||||||
CalcMaintainStep step5,
|
CalcMaintainStep step5,
|
||||||
ChargebackExceptionStep step6,
|
ChargebackExceptionStep step6,
|
||||||
CalcOverrideStep step7,
|
CalcOverrideStep step7,
|
||||||
AggregateStep step8) {
|
AggregateStep step8,
|
||||||
|
AccountingEntryGenerateStep step9,
|
||||||
|
YearEndIncomeAggregateStep step10) {
|
||||||
return new JobBuilder("MONTHLY_SETTLEMENT", jobRepository)
|
return new JobBuilder("MONTHLY_SETTLEMENT", jobRepository)
|
||||||
.start(taskletStep("receiveData", step1, jobRepository, tx))
|
.start(taskletStep("receiveData", step1, jobRepository, tx))
|
||||||
.next(taskletStep("transformData", step2, jobRepository, tx))
|
.next(taskletStep("transformData", step2, jobRepository, tx))
|
||||||
@@ -48,6 +50,8 @@ public class BatchConfig {
|
|||||||
.next(taskletStep("chargebackException", step6, jobRepository, tx))
|
.next(taskletStep("chargebackException", step6, jobRepository, tx))
|
||||||
.next(taskletStep("calcOverride", step7, jobRepository, tx))
|
.next(taskletStep("calcOverride", step7, jobRepository, tx))
|
||||||
.next(taskletStep("aggregate", step8, jobRepository, tx))
|
.next(taskletStep("aggregate", step8, jobRepository, tx))
|
||||||
|
.next(taskletStep("accountingEntryGenerate", step9, jobRepository, tx))
|
||||||
|
.next(taskletStep("yearEndIncomeAggregate", step10, jobRepository, tx))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.ga.batch.settlement.step;
|
||||||
|
|
||||||
|
import com.ga.batch.settlement.SettlementContext;
|
||||||
|
import com.ga.core.mapper.accounting.ContractAccountingEntryMapper;
|
||||||
|
import com.ga.core.vo.accounting.ContractAccountingEntryVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.batch.core.StepContribution;
|
||||||
|
import org.springframework.batch.core.configuration.annotation.JobScope;
|
||||||
|
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||||
|
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||||
|
import org.springframework.batch.repeat.RepeatStatus;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 8b — accountingEntryGenerate: 회계 자동분개
|
||||||
|
*
|
||||||
|
* 정산월 기준 recruit_ledger + maintain_ledger → contract_accounting_entry INSERT.
|
||||||
|
* - 차변(8350 지급수수료) / 대변(2530 미지급금) 한 쌍을 단일 행으로 저장.
|
||||||
|
* - journal_no는 NULL(미전기). 마감 UI에서 채번.
|
||||||
|
* - 이미 분개된 payment는 existsByPaymentId로 중복 방어.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@JobScope
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AccountingEntryGenerateStep implements Tasklet {
|
||||||
|
|
||||||
|
private final SettlementContext ctx;
|
||||||
|
private final ContractAccountingEntryMapper accountingMapper;
|
||||||
|
|
||||||
|
private static final int BATCH_SIZE = 500;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
||||||
|
String settleMonth = ctx.getSettleMonth();
|
||||||
|
log.info("[AccountingEntry] settleMonth={}", settleMonth);
|
||||||
|
|
||||||
|
List<ContractAccountingEntryVO> source = accountingMapper.selectBySettleMonth(settleMonth);
|
||||||
|
if (source.isEmpty()) {
|
||||||
|
log.info("[AccountingEntry] no ledger records for month={}", settleMonth);
|
||||||
|
return RepeatStatus.FINISHED;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
List<ContractAccountingEntryVO> batch = new ArrayList<>(BATCH_SIZE);
|
||||||
|
int total = 0;
|
||||||
|
|
||||||
|
for (ContractAccountingEntryVO entry : source) {
|
||||||
|
if (entry.getAmount() == null || entry.getAmount().signum() == 0) continue;
|
||||||
|
entry.setEntryDate(today);
|
||||||
|
entry.setJournalNo(null);
|
||||||
|
|
||||||
|
batch.add(entry);
|
||||||
|
if (batch.size() >= BATCH_SIZE) {
|
||||||
|
accountingMapper.insertBatch(batch);
|
||||||
|
total += batch.size();
|
||||||
|
batch.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!batch.isEmpty()) {
|
||||||
|
accountingMapper.insertBatch(batch);
|
||||||
|
total += batch.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[AccountingEntry] inserted {} entries for month={}", total, settleMonth);
|
||||||
|
return RepeatStatus.FINISHED;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.ga.batch.settlement.step;
|
||||||
|
|
||||||
|
import com.ga.batch.settlement.SettlementContext;
|
||||||
|
import com.ga.core.mapper.income.AgentAnnualIncomeMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.batch.core.StepContribution;
|
||||||
|
import org.springframework.batch.core.configuration.annotation.JobScope;
|
||||||
|
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||||
|
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||||
|
import org.springframework.batch.repeat.RepeatStatus;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 9 (conditional) — yearEndIncomeAggregate: 연말 사업소득 집계
|
||||||
|
*
|
||||||
|
* 12월 마감 시에만 실행 (settleMonth ends with "-12" 또는 "12").
|
||||||
|
* payment 테이블 기준으로 해당 연도 설계사별 SUM → agent_annual_income UPSERT.
|
||||||
|
* SQL은 AgentAnnualIncomeMapper.upsertAggregate에 위임.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@JobScope
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class YearEndIncomeAggregateStep implements Tasklet {
|
||||||
|
|
||||||
|
private final SettlementContext ctx;
|
||||||
|
private final AgentAnnualIncomeMapper incomeMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
||||||
|
String settleMonth = ctx.getSettleMonth();
|
||||||
|
|
||||||
|
if (!isDecemberClose(settleMonth)) {
|
||||||
|
log.info("[YearEndIncome] skipped — not a December close: {}", settleMonth);
|
||||||
|
return RepeatStatus.FINISHED;
|
||||||
|
}
|
||||||
|
|
||||||
|
int year = extractYear(settleMonth);
|
||||||
|
log.info("[YearEndIncome] aggregating income for year={}", year);
|
||||||
|
|
||||||
|
int rows = incomeMapper.upsertAggregate(year);
|
||||||
|
log.info("[YearEndIncome] agent_annual_income upserted: {} rows", rows);
|
||||||
|
return RepeatStatus.FINISHED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDecemberClose(String settleMonth) {
|
||||||
|
if (settleMonth == null) return false;
|
||||||
|
// 형식: yyyyMM (예: 202512) 또는 yyyy-MM
|
||||||
|
return settleMonth.endsWith("12");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int extractYear(String settleMonth) {
|
||||||
|
// yyyyMM → 앞 4자리
|
||||||
|
String digits = settleMonth.replaceAll("[^0-9]", "");
|
||||||
|
return Integer.parseInt(digits.substring(0, 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# 실제 운영 PostgreSQL 연결용 프로파일 (ga-batch)
|
# 실제 운영 PostgreSQL 연결용 프로파일 (ga-batch)
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://125.241.236.203:55432/trading_ai?currentSchema=ga
|
url: jdbc:postgresql://192.168.0.60:55432/trading_ai?currentSchema=ga
|
||||||
username: ${DB_USER:kyu}
|
username: ${DB_USER:kyu}
|
||||||
password: ${DB_PASSWORD:7895123}
|
password: ${DB_PASSWORD:7895123}
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.ga.common.code;
|
||||||
|
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Tag(name = "계정코드")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/common/account-codes")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AccountCodeController {
|
||||||
|
|
||||||
|
private final AccountCodeService service;
|
||||||
|
|
||||||
|
/** 활성 계정코드 목록 — 드롭다운/배치 참조용. accountType 파라미터로 DEBIT/CREDIT/BOTH 필터 가능 */
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<List<AccountCodeVO>> getActiveCodes(
|
||||||
|
@RequestParam(required = false) String accountType) {
|
||||||
|
return ApiResponse.ok(service.getActiveCodes(accountType));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- 시스템관리 -----
|
||||||
|
|
||||||
|
@GetMapping("/all")
|
||||||
|
@RequirePermission(menu = "SYSTEM_CODE", perm = "READ")
|
||||||
|
public ApiResponse<List<AccountCodeVO>> listAll(
|
||||||
|
@RequestParam(required = false) String accountType) {
|
||||||
|
return ApiResponse.ok(service.listAll(accountType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@RequirePermission(menu = "SYSTEM_CODE", perm = "CREATE")
|
||||||
|
public ApiResponse<Void> create(@RequestBody AccountCodeVO vo) {
|
||||||
|
service.create(vo);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{accountCode}")
|
||||||
|
@RequirePermission(menu = "SYSTEM_CODE", perm = "UPDATE")
|
||||||
|
public ApiResponse<Void> update(@PathVariable String accountCode, @RequestBody AccountCodeVO vo) {
|
||||||
|
vo.setAccountCode(accountCode);
|
||||||
|
service.update(vo);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{accountCode}")
|
||||||
|
@RequirePermission(menu = "SYSTEM_CODE", perm = "DELETE")
|
||||||
|
public ApiResponse<Void> delete(@PathVariable String accountCode) {
|
||||||
|
service.delete(accountCode);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ga.common.code;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface AccountCodeMapper {
|
||||||
|
|
||||||
|
List<AccountCodeVO> selectAll(@Param("accountType") String accountType);
|
||||||
|
List<AccountCodeVO> selectActive(@Param("accountType") String accountType);
|
||||||
|
AccountCodeVO selectOne(@Param("accountCode") String accountCode);
|
||||||
|
int insert(AccountCodeVO vo);
|
||||||
|
int update(AccountCodeVO vo);
|
||||||
|
int delete(@Param("accountCode") String accountCode);
|
||||||
|
int existsByCode(@Param("accountCode") String accountCode);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.ga.common.code;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AccountCodeService {
|
||||||
|
|
||||||
|
private final AccountCodeMapper mapper;
|
||||||
|
|
||||||
|
public List<AccountCodeVO> getActiveCodes(String accountType) {
|
||||||
|
return mapper.selectActive(accountType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountCodeVO getCode(String accountCode) {
|
||||||
|
AccountCodeVO vo = mapper.selectOne(accountCode);
|
||||||
|
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AccountCodeVO> listAll(String accountType) {
|
||||||
|
return mapper.selectAll(accountType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void create(AccountCodeVO vo) {
|
||||||
|
if (mapper.existsByCode(vo.getAccountCode()) > 0) {
|
||||||
|
throw new BizException(ErrorCode.DUPLICATE_DATA);
|
||||||
|
}
|
||||||
|
mapper.insert(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void update(AccountCodeVO vo) {
|
||||||
|
getCode(vo.getAccountCode());
|
||||||
|
mapper.update(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(String accountCode) {
|
||||||
|
getCode(accountCode);
|
||||||
|
mapper.delete(accountCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.ga.common.code;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AccountCodeVO {
|
||||||
|
private String accountCode;
|
||||||
|
private String accountName;
|
||||||
|
private String accountType; // DEBIT / CREDIT / BOTH
|
||||||
|
private String isActive;
|
||||||
|
private Integer sortOrder;
|
||||||
|
private String description;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private Long updatedBy;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- V63: 설계사 연말 사업소득 집계 테이블
|
||||||
|
|
||||||
|
CREATE TABLE agent_annual_income (
|
||||||
|
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
|
||||||
|
settle_year INT NOT NULL,
|
||||||
|
total_commission NUMERIC(18,2) NOT NULL DEFAULT 0,
|
||||||
|
total_tax_withheld NUMERIC(18,2) NOT NULL DEFAULT 0,
|
||||||
|
total_local_tax NUMERIC(18,2) NOT NULL DEFAULT 0,
|
||||||
|
business_income NUMERIC(18,2) NOT NULL DEFAULT 0,
|
||||||
|
generated_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
PRIMARY KEY (agent_id, settle_year)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE agent_annual_income IS '설계사 연말 사업소득 집계';
|
||||||
|
COMMENT ON COLUMN agent_annual_income.business_income IS 'total_commission - total_tax_withheld - total_local_tax';
|
||||||
|
|
||||||
|
CREATE INDEX idx_aai_year ON agent_annual_income(settle_year);
|
||||||
|
|
||||||
|
-- EXPLAIN ANALYZE: settle_year 단독 집계 — idx_aai_year Index Scan 확인
|
||||||
|
-- EXPLAIN ANALYZE
|
||||||
|
-- SELECT settle_year, SUM(business_income)
|
||||||
|
-- FROM agent_annual_income
|
||||||
|
-- GROUP BY settle_year;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- V64: 계약 회계분개 엔트리 테이블
|
||||||
|
|
||||||
|
CREATE TABLE contract_accounting_entry (
|
||||||
|
entry_id BIGSERIAL PRIMARY KEY,
|
||||||
|
contract_id BIGINT NOT NULL REFERENCES contract(contract_id),
|
||||||
|
payment_id BIGINT REFERENCES payment(payment_id),
|
||||||
|
entry_date DATE NOT NULL,
|
||||||
|
debit_account VARCHAR(20) NOT NULL,
|
||||||
|
credit_account VARCHAR(20) NOT NULL,
|
||||||
|
amount NUMERIC(18,2) NOT NULL,
|
||||||
|
description VARCHAR(200),
|
||||||
|
journal_no VARCHAR(30),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE contract_accounting_entry IS '계약 회계 분개 엔트리';
|
||||||
|
COMMENT ON COLUMN contract_accounting_entry.debit_account IS '차변 계정코드 (예: 8350 지급수수료)';
|
||||||
|
COMMENT ON COLUMN contract_accounting_entry.credit_account IS '대변 계정코드 (예: 2530 미지급금)';
|
||||||
|
COMMENT ON COLUMN contract_accounting_entry.journal_no IS '마감 시 채번. NULL=미전기';
|
||||||
|
|
||||||
|
CREATE INDEX idx_cae_journal ON contract_accounting_entry(journal_no);
|
||||||
|
CREATE INDEX idx_cae_date ON contract_accounting_entry(entry_date);
|
||||||
|
CREATE INDEX idx_cae_contract ON contract_accounting_entry(contract_id, entry_date);
|
||||||
|
CREATE INDEX idx_cae_unposted ON contract_accounting_entry(entry_date) WHERE journal_no IS NULL;
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
-- V65: P5-W1 테스트 데이터 (agent_annual_income, contract_accounting_entry, 메뉴)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. agent_annual_income: 설계사 10명 / 2025년
|
||||||
|
-- ============================================================
|
||||||
|
INSERT INTO agent_annual_income (agent_id, settle_year, total_commission, total_tax_withheld, total_local_tax, business_income)
|
||||||
|
SELECT
|
||||||
|
a.agent_id,
|
||||||
|
2025,
|
||||||
|
v.total_commission,
|
||||||
|
v.total_commission * 0.033,
|
||||||
|
v.total_commission * 0.033 * 0.1,
|
||||||
|
v.total_commission - (v.total_commission * 0.033) - (v.total_commission * 0.033 * 0.1)
|
||||||
|
FROM (
|
||||||
|
SELECT agent_id, ROW_NUMBER() OVER (ORDER BY agent_id) AS rn
|
||||||
|
FROM agent
|
||||||
|
LIMIT 10
|
||||||
|
) a
|
||||||
|
JOIN (VALUES
|
||||||
|
(1, 42000000.00),
|
||||||
|
(2, 38500000.00),
|
||||||
|
(3, 55000000.00),
|
||||||
|
(4, 29800000.00),
|
||||||
|
(5, 61200000.00),
|
||||||
|
(6, 47300000.00),
|
||||||
|
(7, 33600000.00),
|
||||||
|
(8, 52100000.00),
|
||||||
|
(9, 28400000.00),
|
||||||
|
(10, 44700000.00)
|
||||||
|
) AS v(rn, total_commission) ON a.rn = v.rn
|
||||||
|
ON CONFLICT (agent_id, settle_year) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. contract_accounting_entry: payment 10건 기반
|
||||||
|
-- journal_no NULL 5건 (미전기) + 채번 5건 (전기완료)
|
||||||
|
-- ============================================================
|
||||||
|
INSERT INTO contract_accounting_entry (contract_id, payment_id, entry_date, debit_account, credit_account, amount, description, journal_no)
|
||||||
|
SELECT
|
||||||
|
c.contract_id,
|
||||||
|
p.payment_id,
|
||||||
|
COALESCE(p.pay_date, CURRENT_DATE),
|
||||||
|
'8350',
|
||||||
|
'2530',
|
||||||
|
p.pay_amount,
|
||||||
|
'지급수수료 자동분개 #' || p.payment_id,
|
||||||
|
CASE WHEN p.payment_id % 2 = 0 THEN 'JNL-2025-' || LPAD(p.payment_id::text, 5, '0') ELSE NULL END
|
||||||
|
FROM (
|
||||||
|
SELECT py.payment_id, py.pay_amount, py.pay_date, sm.agent_id
|
||||||
|
FROM payment py
|
||||||
|
JOIN settle_master sm ON sm.settle_id = py.settle_id
|
||||||
|
ORDER BY py.payment_id
|
||||||
|
LIMIT 10
|
||||||
|
) p
|
||||||
|
JOIN LATERAL (
|
||||||
|
SELECT c.contract_id
|
||||||
|
FROM contract c
|
||||||
|
WHERE c.agent_id = p.agent_id
|
||||||
|
ORDER BY c.contract_id
|
||||||
|
LIMIT 1
|
||||||
|
) c ON true;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 메뉴: ANNUAL_INCOME + ACCOUNTING_JOURNAL (GRP_COMMISSION 하위)
|
||||||
|
-- ============================================================
|
||||||
|
INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order, is_visible, is_active)
|
||||||
|
SELECT p.menu_id, x.code, x.name, 'PAGE', x.path, x.cmpt, 2, x.sort, 'Y', 'Y'
|
||||||
|
FROM menu p
|
||||||
|
JOIN (VALUES
|
||||||
|
('ANNUAL_INCOME', '연말 사업소득 명세서', '/commission/annual-income', 'commission/AnnualIncome', 80),
|
||||||
|
('ACCOUNTING_JOURNAL', '회계 분개장', '/commission/accounting-journal', 'commission/AccountingJournal', 90)
|
||||||
|
) AS x(code, name, path, cmpt, sort) ON p.menu_code = 'GRP_COMMISSION'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 권한 항목 등록
|
||||||
|
-- ============================================================
|
||||||
|
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
|
||||||
|
SELECT m.menu_id, p.code, p.name, p.sort
|
||||||
|
FROM menu m
|
||||||
|
CROSS JOIN (VALUES
|
||||||
|
('READ', '조회', 10),
|
||||||
|
('CREATE', '등록', 20),
|
||||||
|
('UPDATE', '수정', 30),
|
||||||
|
('EXPORT', '엑셀', 60)
|
||||||
|
) AS p(code, name, sort)
|
||||||
|
WHERE m.menu_code IN ('ANNUAL_INCOME', 'ACCOUNTING_JOURNAL')
|
||||||
|
ON CONFLICT (menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. 역할별 권한 부여
|
||||||
|
-- ============================================================
|
||||||
|
-- SUPER_ADMIN / ADMIN: 전체
|
||||||
|
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
|
||||||
|
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
|
||||||
|
FROM role r
|
||||||
|
CROSS JOIN menu_permission mp
|
||||||
|
JOIN menu m ON m.menu_id = mp.menu_id
|
||||||
|
WHERE r.role_code IN ('SUPER_ADMIN', 'ADMIN')
|
||||||
|
AND m.menu_code IN ('ANNUAL_INCOME', 'ACCOUNTING_JOURNAL')
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- MANAGER: READ + EXPORT
|
||||||
|
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
|
||||||
|
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
|
||||||
|
FROM role r
|
||||||
|
CROSS JOIN menu_permission mp
|
||||||
|
JOIN menu m ON m.menu_id = mp.menu_id
|
||||||
|
WHERE r.role_code = 'MANAGER'
|
||||||
|
AND mp.perm_code IN ('READ', 'EXPORT')
|
||||||
|
AND m.menu_code IN ('ANNUAL_INCOME', 'ACCOUNTING_JOURNAL')
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- AGENT: READ (본인 연말명세서)
|
||||||
|
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
|
||||||
|
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
|
||||||
|
FROM role r
|
||||||
|
CROSS JOIN menu_permission mp
|
||||||
|
JOIN menu m ON m.menu_id = mp.menu_id
|
||||||
|
WHERE r.role_code = 'AGENT'
|
||||||
|
AND mp.perm_code = 'READ'
|
||||||
|
AND m.menu_code = 'ANNUAL_INCOME'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- V66: 회계 계정코드 마스터 신설
|
||||||
|
-- contract_accounting_entry.debit_account / credit_account 참조용
|
||||||
|
-- FK 추가는 PL 별도 논의 후 진행
|
||||||
|
|
||||||
|
CREATE TABLE account_code (
|
||||||
|
account_code VARCHAR(20) PRIMARY KEY,
|
||||||
|
account_name VARCHAR(100) NOT NULL,
|
||||||
|
account_type VARCHAR(10) NOT NULL CHECK (account_type IN ('DEBIT','CREDIT','BOTH')),
|
||||||
|
is_active CHAR(1) NOT NULL DEFAULT 'Y',
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
description VARCHAR(200),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_by BIGINT
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE account_code IS '회계 계정코드 마스터';
|
||||||
|
COMMENT ON COLUMN account_code.account_type IS 'DEBIT=차변전용, CREDIT=대변전용, BOTH=겸용';
|
||||||
|
|
||||||
|
INSERT INTO account_code (account_code, account_name, account_type, sort_order) VALUES
|
||||||
|
('8350', '지급수수료', 'DEBIT', 10),
|
||||||
|
('2530', '미지급금', 'CREDIT', 20),
|
||||||
|
('1130', '미수수수료', 'DEBIT', 30),
|
||||||
|
('4110', '수수료수익', 'CREDIT', 40)
|
||||||
|
ON CONFLICT (account_code) DO NOTHING;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- V67: 원천세 분기신고 테이블
|
||||||
|
|
||||||
|
CREATE TABLE withholding_tax_report (
|
||||||
|
report_id BIGSERIAL PRIMARY KEY,
|
||||||
|
settle_quarter VARCHAR(7) NOT NULL,
|
||||||
|
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
|
||||||
|
total_income NUMERIC(18,2),
|
||||||
|
total_withheld NUMERIC(18,2),
|
||||||
|
total_local_tax NUMERIC(18,2),
|
||||||
|
generated_at TIMESTAMP,
|
||||||
|
filed_at TIMESTAMP,
|
||||||
|
file_status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
UNIQUE (agent_id, settle_quarter),
|
||||||
|
CONSTRAINT chk_wtr_status CHECK (file_status IN ('DRAFT','FILED','AMENDED'))
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE withholding_tax_report IS '원천세 분기신고';
|
||||||
|
COMMENT ON COLUMN withholding_tax_report.settle_quarter IS '정산 분기 (예: 2025-Q1)';
|
||||||
|
COMMENT ON COLUMN withholding_tax_report.file_status IS 'DRAFT=작성중, FILED=신고완료, AMENDED=수정신고';
|
||||||
|
|
||||||
|
CREATE INDEX idx_wtr_quarter ON withholding_tax_report(settle_quarter);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- V68: 부가세 분기신고 테이블
|
||||||
|
|
||||||
|
CREATE TABLE vat_report (
|
||||||
|
vat_id BIGSERIAL PRIMARY KEY,
|
||||||
|
settle_quarter VARCHAR(7) NOT NULL,
|
||||||
|
total_sales NUMERIC(18,2),
|
||||||
|
total_purchase NUMERIC(18,2),
|
||||||
|
total_vat_payable NUMERIC(18,2),
|
||||||
|
tax_invoice_count INT,
|
||||||
|
generated_at TIMESTAMP,
|
||||||
|
filed_at TIMESTAMP,
|
||||||
|
file_status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
UNIQUE (settle_quarter),
|
||||||
|
CONSTRAINT chk_vat_status CHECK (file_status IN ('DRAFT','FILED','AMENDED'))
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE vat_report IS '부가세 분기신고';
|
||||||
|
COMMENT ON COLUMN vat_report.settle_quarter IS '정산 분기 (예: 2025-Q1)';
|
||||||
|
COMMENT ON COLUMN vat_report.total_vat_payable IS '납부 부가세 = total_sales * 0.1 - 매입세액';
|
||||||
|
COMMENT ON COLUMN vat_report.file_status IS 'DRAFT=작성중, FILED=신고완료, AMENDED=수정신고';
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
-- V69: P5-W2 테스트 데이터 (withholding_tax_report, vat_report, 메뉴)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. withholding_tax_report: agent 5명 × 2025 Q1~Q4 = 20건
|
||||||
|
-- ============================================================
|
||||||
|
INSERT INTO withholding_tax_report (settle_quarter, agent_id, total_income, total_withheld, total_local_tax, generated_at, file_status)
|
||||||
|
SELECT
|
||||||
|
q.quarter,
|
||||||
|
a.agent_id,
|
||||||
|
q.base_income,
|
||||||
|
q.base_income * 0.033,
|
||||||
|
q.base_income * 0.033 * 0.1,
|
||||||
|
now(),
|
||||||
|
CASE WHEN q.quarter IN ('2025-Q1','2025-Q2') THEN 'FILED' ELSE 'DRAFT' END
|
||||||
|
FROM (
|
||||||
|
SELECT agent_id, ROW_NUMBER() OVER (ORDER BY agent_id) AS rn
|
||||||
|
FROM agent LIMIT 5
|
||||||
|
) a
|
||||||
|
CROSS JOIN (VALUES
|
||||||
|
('2025-Q1', 10500000.00),
|
||||||
|
('2025-Q2', 11200000.00),
|
||||||
|
('2025-Q3', 10800000.00),
|
||||||
|
('2025-Q4', 12300000.00)
|
||||||
|
) AS q(quarter, base_income)
|
||||||
|
ON CONFLICT (agent_id, settle_quarter) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. vat_report: 2025 Q1~Q4 = 4건
|
||||||
|
-- ============================================================
|
||||||
|
INSERT INTO vat_report (settle_quarter, total_sales, total_purchase, total_vat_payable, tax_invoice_count, generated_at, file_status)
|
||||||
|
VALUES
|
||||||
|
('2025-Q1', 850000000.00, 120000000.00, 73000000.00, 142, now(), 'FILED'),
|
||||||
|
('2025-Q2', 920000000.00, 135000000.00, 78500000.00, 158, now(), 'FILED'),
|
||||||
|
('2025-Q3', 880000000.00, 128000000.00, 75200000.00, 151, now(), 'DRAFT'),
|
||||||
|
('2025-Q4', 960000000.00, 140000000.00, 82000000.00, 167, now(), 'DRAFT')
|
||||||
|
ON CONFLICT (settle_quarter) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 메뉴: WITHHOLDING_TAX + VAT_REPORT (GRP_COMMISSION 하위)
|
||||||
|
-- ============================================================
|
||||||
|
INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order, is_visible, is_active)
|
||||||
|
SELECT p.menu_id, x.code, x.name, 'PAGE', x.path, x.cmpt, 2, x.sort, 'Y', 'Y'
|
||||||
|
FROM menu p
|
||||||
|
JOIN (VALUES
|
||||||
|
('WITHHOLDING_TAX', '원천세 신고', '/commission/withholding-tax', 'commission/WithholdingTax', 100),
|
||||||
|
('VAT_REPORT', '부가세 신고', '/commission/vat-report', 'commission/VatReport', 110)
|
||||||
|
) AS x(code, name, path, cmpt, sort) ON p.menu_code = 'GRP_COMMISSION'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 권한 항목 등록
|
||||||
|
-- ============================================================
|
||||||
|
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
|
||||||
|
SELECT m.menu_id, p.code, p.name, p.sort
|
||||||
|
FROM menu m
|
||||||
|
CROSS JOIN (VALUES
|
||||||
|
('READ', '조회', 10),
|
||||||
|
('CREATE', '등록', 20),
|
||||||
|
('UPDATE', '수정', 30),
|
||||||
|
('EXPORT', '엑셀', 60)
|
||||||
|
) AS p(code, name, sort)
|
||||||
|
WHERE m.menu_code IN ('WITHHOLDING_TAX', 'VAT_REPORT')
|
||||||
|
ON CONFLICT (menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. 역할별 권한 부여
|
||||||
|
-- ============================================================
|
||||||
|
-- SUPER_ADMIN / ADMIN: 전체
|
||||||
|
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
|
||||||
|
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
|
||||||
|
FROM role r
|
||||||
|
CROSS JOIN menu_permission mp
|
||||||
|
JOIN menu m ON m.menu_id = mp.menu_id
|
||||||
|
WHERE r.role_code IN ('SUPER_ADMIN', 'ADMIN')
|
||||||
|
AND m.menu_code IN ('WITHHOLDING_TAX', 'VAT_REPORT')
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- MANAGER: READ + EXPORT
|
||||||
|
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
|
||||||
|
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
|
||||||
|
FROM role r
|
||||||
|
CROSS JOIN menu_permission mp
|
||||||
|
JOIN menu m ON m.menu_id = mp.menu_id
|
||||||
|
WHERE r.role_code = 'MANAGER'
|
||||||
|
AND mp.perm_code IN ('READ', 'EXPORT')
|
||||||
|
AND m.menu_code IN ('WITHHOLDING_TAX', 'VAT_REPORT')
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- V70: rule 도메인 5개 테이블 감사 컬럼 보강
|
||||||
|
-- commission_rate / payout_rule: created_at/created_by 존재, updated_at/updated_by 누락
|
||||||
|
-- override_rule / chargeback_rule / exception_type_code: 감사 컬럼 전체 누락
|
||||||
|
|
||||||
|
ALTER TABLE commission_rate
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_by BIGINT;
|
||||||
|
|
||||||
|
ALTER TABLE payout_rule
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_by BIGINT;
|
||||||
|
|
||||||
|
ALTER TABLE override_rule
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
ADD COLUMN IF NOT EXISTS created_by BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_by BIGINT;
|
||||||
|
|
||||||
|
ALTER TABLE chargeback_rule
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
ADD COLUMN IF NOT EXISTS created_by BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_by BIGINT;
|
||||||
|
|
||||||
|
ALTER TABLE exception_type_code
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
ADD COLUMN IF NOT EXISTS created_by BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_by BIGINT;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- V71: payment 테이블 세무 컬럼 누락 보강
|
||||||
|
-- V23 세무처리.sql 체크섬 미스매치로 income_tax_amount 등 미적용 → idempotent 추가
|
||||||
|
|
||||||
|
ALTER TABLE payment
|
||||||
|
ADD COLUMN IF NOT EXISTS income_tax_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS local_tax_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS vat_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS net_amount NUMERIC(15,2);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN payment.income_tax_amount IS '원천세 (소득세 3%)';
|
||||||
|
COMMENT ON COLUMN payment.local_tax_amount IS '지방소득세 (소득세의 10%)';
|
||||||
|
COMMENT ON COLUMN payment.vat_amount IS '부가세';
|
||||||
|
COMMENT ON COLUMN payment.net_amount IS '실수령액 (pay_amount - income_tax - local_tax)';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- V72: P5-W1 ANNUAL_INCOME 메뉴 EXECUTE 권한 누락 보강
|
||||||
|
-- V65에서 READ/CREATE/UPDATE/EXPORT만 등록되어 regenerate 엔드포인트 403 발생
|
||||||
|
-- SUPER_ADMIN / ADMIN 역할에 EXECUTE 권한 부여
|
||||||
|
|
||||||
|
-- 1. menu_permission에 EXECUTE 항목 추가
|
||||||
|
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
|
||||||
|
SELECT m.menu_id, 'EXECUTE', '재집계', 70
|
||||||
|
FROM menu m
|
||||||
|
WHERE m.menu_code = 'ANNUAL_INCOME'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM menu_permission mp2
|
||||||
|
WHERE mp2.menu_id = m.menu_id AND mp2.perm_code = 'EXECUTE'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. SUPER_ADMIN / ADMIN에 EXECUTE 권한 부여
|
||||||
|
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
|
||||||
|
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
|
||||||
|
FROM role r
|
||||||
|
CROSS JOIN menu_permission mp
|
||||||
|
JOIN menu m ON m.menu_id = mp.menu_id
|
||||||
|
WHERE r.role_code IN ('SUPER_ADMIN', 'ADMIN')
|
||||||
|
AND mp.perm_code = 'EXECUTE'
|
||||||
|
AND m.menu_code = 'ANNUAL_INCOME'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM role_menu_permission rmp2
|
||||||
|
WHERE rmp2.role_id = r.role_id
|
||||||
|
AND rmp2.menu_id = mp.menu_id
|
||||||
|
AND rmp2.perm_code = 'EXECUTE'
|
||||||
|
);
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.common.code.AccountCodeMapper">
|
||||||
|
|
||||||
|
<resultMap id="AccountCodeMap" type="com.ga.common.code.AccountCodeVO"/>
|
||||||
|
|
||||||
|
<sql id="Columns">
|
||||||
|
account_code, account_name, account_type, is_active, sort_order, description,
|
||||||
|
created_at, created_by, updated_at, updated_by
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectAll" resultMap="AccountCodeMap">
|
||||||
|
SELECT <include refid="Columns"/>
|
||||||
|
FROM account_code
|
||||||
|
<where>
|
||||||
|
<if test="accountType != null and accountType != ''">
|
||||||
|
AND account_type = #{accountType}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY sort_order, account_code
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectActive" resultMap="AccountCodeMap">
|
||||||
|
SELECT <include refid="Columns"/>
|
||||||
|
FROM account_code
|
||||||
|
WHERE is_active = 'Y'
|
||||||
|
<if test="accountType != null and accountType != ''">
|
||||||
|
AND account_type = #{accountType}
|
||||||
|
</if>
|
||||||
|
ORDER BY sort_order, account_code
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectOne" resultMap="AccountCodeMap">
|
||||||
|
SELECT <include refid="Columns"/>
|
||||||
|
FROM account_code WHERE account_code = #{accountCode}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="com.ga.common.code.AccountCodeVO">
|
||||||
|
INSERT INTO account_code (
|
||||||
|
account_code, account_name, account_type, is_active, sort_order, description,
|
||||||
|
created_at, created_by
|
||||||
|
) VALUES (
|
||||||
|
#{accountCode}, #{accountName}, #{accountType},
|
||||||
|
COALESCE(#{isActive},'Y'), COALESCE(#{sortOrder},0), #{description},
|
||||||
|
now(), #{createdBy}
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update" parameterType="com.ga.common.code.AccountCodeVO">
|
||||||
|
UPDATE account_code SET
|
||||||
|
account_name = #{accountName},
|
||||||
|
account_type = #{accountType},
|
||||||
|
is_active = #{isActive},
|
||||||
|
sort_order = #{sortOrder},
|
||||||
|
description = #{description},
|
||||||
|
updated_at = now(),
|
||||||
|
updated_by = #{updatedBy}
|
||||||
|
WHERE account_code = #{accountCode}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="delete">
|
||||||
|
DELETE FROM account_code WHERE account_code = #{accountCode}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<select id="existsByCode" resultType="int">
|
||||||
|
SELECT COUNT(*) FROM account_code WHERE account_code = #{accountCode}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package com.ga.core.mapper.accounting;
|
||||||
|
|
||||||
|
import com.ga.core.vo.accounting.ContractAccountingEntryResp;
|
||||||
|
import com.ga.core.vo.accounting.ContractAccountingEntrySearchParam;
|
||||||
|
import com.ga.core.vo.accounting.ContractAccountingEntryVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ContractAccountingEntryMapper {
|
||||||
|
|
||||||
|
/** 목록 조회 — contractId / paymentId / entryDate 범위 / unpostedOnly 조건 */
|
||||||
|
List<ContractAccountingEntryResp> selectList(ContractAccountingEntrySearchParam param);
|
||||||
|
|
||||||
|
/** 단건 상세 */
|
||||||
|
ContractAccountingEntryResp selectDetailById(@Param("entryId") Long entryId);
|
||||||
|
|
||||||
|
/** 배치 step용: 정산월 기준 분개 대상 원장 조회 (recruit+maintain ledger UNION) */
|
||||||
|
List<ContractAccountingEntryVO> selectBySettleMonth(@Param("settleMonth") String settleMonth);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 분개 엔트리 일괄 INSERT.
|
||||||
|
* 회계 자동분개 배치 step에서 호출.
|
||||||
|
*/
|
||||||
|
int insertBatch(@Param("list") List<ContractAccountingEntryVO> list);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 미전기 엔트리 마감 처리 — journalNo 채번 후 UPDATE.
|
||||||
|
* @param ids 마감할 entry_id 목록
|
||||||
|
* @param journalNo 채번된 분개장 번호
|
||||||
|
*/
|
||||||
|
int closeJournal(@Param("ids") List<Long> ids,
|
||||||
|
@Param("journalNo") String journalNo);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ga.core.mapper.income;
|
||||||
|
|
||||||
|
import com.ga.core.vo.income.AgentAnnualIncomeResp;
|
||||||
|
import com.ga.core.vo.income.AgentAnnualIncomeSearchParam;
|
||||||
|
import com.ga.core.vo.income.AgentAnnualIncomeVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface AgentAnnualIncomeMapper {
|
||||||
|
|
||||||
|
/** 목록 조회 — settleYear / agentId / orgId 조건 */
|
||||||
|
List<AgentAnnualIncomeResp> selectList(AgentAnnualIncomeSearchParam param);
|
||||||
|
|
||||||
|
/** 단건 상세 (복합키: agentId + settleYear) */
|
||||||
|
AgentAnnualIncomeResp selectDetailById(@Param("agentId") Long agentId,
|
||||||
|
@Param("settleYear") int settleYear);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 연도 전체 UPSERT 집계 (전체 설계사).
|
||||||
|
*/
|
||||||
|
int upsertAggregate(@Param("year") int year);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.mapper.tax;
|
||||||
|
|
||||||
|
import com.ga.core.vo.tax.WithholdingTaxReportResp;
|
||||||
|
import com.ga.core.vo.tax.WithholdingTaxReportSearchParam;
|
||||||
|
import com.ga.core.vo.tax.WithholdingTaxReportVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface WithholdingTaxReportMapper {
|
||||||
|
|
||||||
|
/** 목록 조회 — settleQuarter / agentId / orgId / fileStatus 조건 */
|
||||||
|
List<WithholdingTaxReportResp> selectList(WithholdingTaxReportSearchParam param);
|
||||||
|
|
||||||
|
/** 단건 상세 */
|
||||||
|
WithholdingTaxReportResp selectDetailById(@Param("reportId") Long reportId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 분기 전체 UPSERT 집계.
|
||||||
|
* payment 테이블 기준 해당 분기 설계사별 SUM 후 INSERT … ON CONFLICT DO UPDATE.
|
||||||
|
*
|
||||||
|
* @param quarter 집계할 분기 (예: 2025-Q1)
|
||||||
|
* @return 처리된 행 수
|
||||||
|
*/
|
||||||
|
int upsertQuarterlyAggregate(@Param("quarter") String quarter);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.ga.core.vo.accounting;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 분개장 엑셀 다운로드 전용 VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ContractAccountingEntryExcelVO {
|
||||||
|
private String journalNo;
|
||||||
|
private LocalDate entryDate;
|
||||||
|
private String contractNo;
|
||||||
|
private String agentName;
|
||||||
|
private String debitAccount;
|
||||||
|
private String creditAccount;
|
||||||
|
private BigDecimal amount;
|
||||||
|
private String description;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.accounting;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 분개 엔트리 목록/상세 API 응답 — contract, agent 조인 포함
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ContractAccountingEntryResp {
|
||||||
|
private Long entryId;
|
||||||
|
private Long contractId;
|
||||||
|
private String contractNo;
|
||||||
|
private Long paymentId;
|
||||||
|
private Long agentId;
|
||||||
|
private String agentName;
|
||||||
|
private LocalDate entryDate;
|
||||||
|
private String debitAccount;
|
||||||
|
private String creditAccount;
|
||||||
|
private BigDecimal amount;
|
||||||
|
private String description;
|
||||||
|
/** NULL이면 미전기 상태 */
|
||||||
|
private String journalNo;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ga.core.vo.accounting;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 분개 엔트리 수동 등록 요청 — 자동 분개 외 수기 입력용
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ContractAccountingEntrySaveReq {
|
||||||
|
@NotNull
|
||||||
|
private Long contractId;
|
||||||
|
private Long paymentId;
|
||||||
|
@NotNull
|
||||||
|
private LocalDate entryDate;
|
||||||
|
@NotBlank
|
||||||
|
private String debitAccount;
|
||||||
|
@NotBlank
|
||||||
|
private String creditAccount;
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin("0.01")
|
||||||
|
private BigDecimal amount;
|
||||||
|
private String description;
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ga.core.vo.accounting;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ContractAccountingEntrySearchParam extends SearchParam {
|
||||||
|
private Long contractId;
|
||||||
|
private Long paymentId;
|
||||||
|
private LocalDate entryDateFrom;
|
||||||
|
private LocalDate entryDateTo;
|
||||||
|
/** null이면 전체, ""이면 미전기(journalNo IS NULL) */
|
||||||
|
private String journalNo;
|
||||||
|
/** 미전기만 조회 플래그 */
|
||||||
|
private Boolean unpostedOnly;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.accounting;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* contract_accounting_entry — 계약 회계 분개 엔트리 (V64)
|
||||||
|
* INSERT/insertBatch/closeJournal 전용. 조인 필드 없음.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ContractAccountingEntryVO {
|
||||||
|
private Long entryId;
|
||||||
|
private Long contractId;
|
||||||
|
private Long paymentId;
|
||||||
|
private LocalDate entryDate;
|
||||||
|
/** 차변 계정코드 (예: 8350 지급수수료) */
|
||||||
|
private String debitAccount;
|
||||||
|
/** 대변 계정코드 (예: 2530 미지급금) */
|
||||||
|
private String creditAccount;
|
||||||
|
private BigDecimal amount;
|
||||||
|
private String description;
|
||||||
|
/** 마감 시 채번. NULL=미전기 */
|
||||||
|
private String journalNo;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ga.core.vo.income;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 연말 사업소득 집계 엑셀 다운로드 전용 VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AgentAnnualIncomeExcelVO {
|
||||||
|
private String agentName;
|
||||||
|
private String orgName;
|
||||||
|
private Integer settleYear;
|
||||||
|
private BigDecimal totalCommission;
|
||||||
|
private BigDecimal totalTaxWithheld;
|
||||||
|
private BigDecimal totalLocalTax;
|
||||||
|
private BigDecimal businessIncome;
|
||||||
|
private LocalDateTime generatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ga.core.vo.income;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 연간소득 목록/상세 API 응답 — agent, organization 조인 포함
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AgentAnnualIncomeResp {
|
||||||
|
private Long agentId;
|
||||||
|
private String agentName;
|
||||||
|
private Long orgId;
|
||||||
|
private String orgName;
|
||||||
|
private Integer settleYear;
|
||||||
|
private BigDecimal totalCommission;
|
||||||
|
private BigDecimal totalTaxWithheld;
|
||||||
|
private BigDecimal totalLocalTax;
|
||||||
|
private BigDecimal businessIncome;
|
||||||
|
private LocalDateTime generatedAt;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ga.core.vo.income;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 연간소득 재집계 트리거 요청 — 연도와 대상 설계사(선택)만 받음
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AgentAnnualIncomeSaveReq {
|
||||||
|
/** null이면 전체 설계사 집계 */
|
||||||
|
private Long agentId;
|
||||||
|
@NotNull
|
||||||
|
@Positive
|
||||||
|
private Integer settleYear;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ga.core.vo.income;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class AgentAnnualIncomeSearchParam extends SearchParam {
|
||||||
|
private Long agentId;
|
||||||
|
private Integer settleYear;
|
||||||
|
private Long orgId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ga.core.vo.income;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* agent_annual_income — 설계사 연말 사업소득 집계 (V63)
|
||||||
|
* PK: (agent_id, settle_year) 복합키. INSERT/UPDATE 전용.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AgentAnnualIncomeVO {
|
||||||
|
private Long agentId;
|
||||||
|
/** 정산 연도 (YYYY) */
|
||||||
|
private Integer settleYear;
|
||||||
|
/** 연간 총 수수료 합계 */
|
||||||
|
private BigDecimal totalCommission;
|
||||||
|
/** 원천징수 소득세 합계 */
|
||||||
|
private BigDecimal totalTaxWithheld;
|
||||||
|
/** 지방소득세 합계 */
|
||||||
|
private BigDecimal totalLocalTax;
|
||||||
|
/** 사업소득 = totalCommission - totalTaxWithheld - totalLocalTax */
|
||||||
|
private BigDecimal businessIncome;
|
||||||
|
/** 마지막 집계 실행 일시 */
|
||||||
|
private LocalDateTime generatedAt;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import lombok.Data;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class ChargebackRuleVO {
|
public class ChargebackRuleVO {
|
||||||
@@ -18,4 +19,8 @@ public class ChargebackRuleVO {
|
|||||||
private Integer maxInstallments;
|
private Integer maxInstallments;
|
||||||
private LocalDate effectiveFrom;
|
private LocalDate effectiveFrom;
|
||||||
private LocalDate effectiveTo;
|
private LocalDate effectiveTo;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private Long updatedBy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,4 +19,6 @@ public class CommissionRateVO {
|
|||||||
private Integer version;
|
private Integer version;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private Long createdBy;
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private Long updatedBy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.ga.core.vo.rule;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class ExceptionTypeCodeVO {
|
public class ExceptionTypeCodeVO {
|
||||||
private String exceptionCode;
|
private String exceptionCode;
|
||||||
@@ -13,4 +15,8 @@ public class ExceptionTypeCodeVO {
|
|||||||
private String taxIncluded;
|
private String taxIncluded;
|
||||||
private String description;
|
private String description;
|
||||||
private String isActive;
|
private String isActive;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private Long updatedBy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import lombok.Data;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class OverrideRuleVO {
|
public class OverrideRuleVO {
|
||||||
@@ -16,4 +17,8 @@ public class OverrideRuleVO {
|
|||||||
private BigDecimal capAmount;
|
private BigDecimal capAmount;
|
||||||
private LocalDate effectiveFrom;
|
private LocalDate effectiveFrom;
|
||||||
private LocalDate effectiveTo;
|
private LocalDate effectiveTo;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private Long updatedBy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,6 @@ public class PayoutRuleVO {
|
|||||||
private Integer version;
|
private Integer version;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private Long createdBy;
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private Long updatedBy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ga.core.vo.tax;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 원천세 분기신고 엑셀 다운로드 전용 VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WithholdingTaxReportExcelVO {
|
||||||
|
private String settleQuarter;
|
||||||
|
private String agentName;
|
||||||
|
private String orgName;
|
||||||
|
private BigDecimal totalIncome;
|
||||||
|
private BigDecimal totalWithheld;
|
||||||
|
private BigDecimal totalLocalTax;
|
||||||
|
private String fileStatus;
|
||||||
|
private LocalDateTime filedAt;
|
||||||
|
private LocalDateTime generatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.tax;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 원천세 분기신고 목록/상세 API 응답 — agent, organization 조인 포함
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WithholdingTaxReportResp {
|
||||||
|
private Long reportId;
|
||||||
|
private Long agentId;
|
||||||
|
private String agentName;
|
||||||
|
private Long orgId;
|
||||||
|
private String orgName;
|
||||||
|
private String settleQuarter;
|
||||||
|
private BigDecimal totalIncome;
|
||||||
|
private BigDecimal totalWithheld;
|
||||||
|
private BigDecimal totalLocalTax;
|
||||||
|
private LocalDateTime generatedAt;
|
||||||
|
private LocalDateTime filedAt;
|
||||||
|
private String fileStatus;
|
||||||
|
private String fileStatusName;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ga.core.vo.tax;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 원천세 분기신고 재집계 트리거 요청
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WithholdingTaxReportSaveReq {
|
||||||
|
/** null이면 전체 설계사 집계 */
|
||||||
|
private Long agentId;
|
||||||
|
@NotBlank
|
||||||
|
private String settleQuarter;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ga.core.vo.tax;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class WithholdingTaxReportSearchParam extends SearchParam {
|
||||||
|
private String settleQuarter;
|
||||||
|
private Long agentId;
|
||||||
|
private Long orgId;
|
||||||
|
/** DRAFT / FILED / AMENDED */
|
||||||
|
private String fileStatus;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ga.core.vo.tax;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* withholding_tax_report — 원천세 분기신고 (V67)
|
||||||
|
* INSERT/UPDATE 전용. 조인 필드 없음.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WithholdingTaxReportVO {
|
||||||
|
private Long reportId;
|
||||||
|
private Long agentId;
|
||||||
|
/** 정산 분기 (예: 2025-Q1) */
|
||||||
|
private String settleQuarter;
|
||||||
|
private BigDecimal totalIncome;
|
||||||
|
private BigDecimal totalWithheld;
|
||||||
|
private BigDecimal totalLocalTax;
|
||||||
|
private LocalDateTime generatedAt;
|
||||||
|
private LocalDateTime filedAt;
|
||||||
|
/** DRAFT / FILED / AMENDED */
|
||||||
|
private String fileStatus;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.core.mapper.accounting.ContractAccountingEntryMapper">
|
||||||
|
|
||||||
|
<!-- contract, agent 조인 포함 응답 매핑 -->
|
||||||
|
<resultMap id="RespMap" type="com.ga.core.vo.accounting.ContractAccountingEntryResp"/>
|
||||||
|
|
||||||
|
<!-- 목록/상세 공통 SELECT 컬럼 -->
|
||||||
|
<sql id="joinCols">
|
||||||
|
cae.entry_id,
|
||||||
|
cae.contract_id,
|
||||||
|
ac.contract_no,
|
||||||
|
cae.payment_id,
|
||||||
|
a.agent_id,
|
||||||
|
a.agent_name,
|
||||||
|
cae.entry_date,
|
||||||
|
cae.debit_account,
|
||||||
|
cae.credit_account,
|
||||||
|
cae.amount,
|
||||||
|
cae.description,
|
||||||
|
cae.journal_no,
|
||||||
|
cae.created_at
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<!-- 공통 FROM + JOIN -->
|
||||||
|
<sql id="joinFrom">
|
||||||
|
FROM contract_accounting_entry cae
|
||||||
|
JOIN contract ac ON ac.contract_id = cae.contract_id
|
||||||
|
JOIN agent a ON a.agent_id = ac.agent_id
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<!-- ===== 목록 조회 ===== -->
|
||||||
|
<select id="selectList" resultMap="RespMap">
|
||||||
|
SELECT <include refid="joinCols"/>
|
||||||
|
<include refid="joinFrom"/>
|
||||||
|
<where>
|
||||||
|
<if test="contractId != null">AND cae.contract_id = #{contractId}</if>
|
||||||
|
<if test="paymentId != null">AND cae.payment_id = #{paymentId}</if>
|
||||||
|
<if test="entryDateFrom != null">AND cae.entry_date >= #{entryDateFrom}</if>
|
||||||
|
<if test="entryDateTo != null">AND cae.entry_date <= #{entryDateTo}</if>
|
||||||
|
<if test="journalNo != null and journalNo != ''">AND cae.journal_no = #{journalNo}</if>
|
||||||
|
<!-- unpostedOnly=true이면 미전기(journal_no IS NULL)만 조회 -->
|
||||||
|
<if test="unpostedOnly != null and unpostedOnly == true">
|
||||||
|
AND cae.journal_no IS NULL
|
||||||
|
</if>
|
||||||
|
<if test="searchKeyword != null and searchKeyword != ''">
|
||||||
|
AND (ac.contract_no LIKE CONCAT('%', #{searchKeyword}, '%')
|
||||||
|
OR a.agent_name LIKE CONCAT('%', #{searchKeyword}, '%'))
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY cae.entry_date DESC, cae.entry_id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- ===== 단건 상세 ===== -->
|
||||||
|
<select id="selectDetailById" resultMap="RespMap">
|
||||||
|
SELECT <include refid="joinCols"/>
|
||||||
|
<include refid="joinFrom"/>
|
||||||
|
WHERE cae.entry_id = #{entryId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- ===== 일괄 INSERT =====
|
||||||
|
회계 자동분개 배치 step에서 한 번에 다수 엔트리 저장.
|
||||||
|
foreach로 멀티 VALUES 구문 생성.
|
||||||
|
-->
|
||||||
|
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="entryId">
|
||||||
|
INSERT INTO contract_accounting_entry
|
||||||
|
(contract_id, payment_id, entry_date,
|
||||||
|
debit_account, credit_account, amount, description)
|
||||||
|
VALUES
|
||||||
|
<foreach collection="list" item="e" separator=",">
|
||||||
|
(#{e.contractId}, #{e.paymentId}, #{e.entryDate},
|
||||||
|
#{e.debitAccount}, #{e.creditAccount}, #{e.amount}, #{e.description})
|
||||||
|
</foreach>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- ===== 마감 처리 (journalNo 채번) ===== -->
|
||||||
|
<update id="closeJournal">
|
||||||
|
UPDATE contract_accounting_entry
|
||||||
|
SET journal_no = #{journalNo}
|
||||||
|
WHERE entry_id IN
|
||||||
|
<foreach collection="ids" item="id" open="(" separator="," close=")">
|
||||||
|
#{id}
|
||||||
|
</foreach>
|
||||||
|
AND journal_no IS NULL
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
배치 step용: 정산월 기준 분개 대상 원장 조회.
|
||||||
|
recruit_ledger + maintain_ledger UNION → contract_id 포함.
|
||||||
|
AccountingEntryGenerateStep에서 호출.
|
||||||
|
-->
|
||||||
|
<select id="selectBySettleMonth" resultType="com.ga.core.vo.accounting.ContractAccountingEntryVO">
|
||||||
|
SELECT l.contract_id,
|
||||||
|
NULL::BIGINT AS payment_id,
|
||||||
|
CURRENT_DATE AS entry_date,
|
||||||
|
'8350' AS debit_account,
|
||||||
|
'2530' AS credit_account,
|
||||||
|
l.agent_amount AS amount,
|
||||||
|
CONCAT('모집수수료 ', l.settle_month) AS description,
|
||||||
|
NULL AS journal_no
|
||||||
|
FROM recruit_ledger l
|
||||||
|
WHERE l.settle_month = #{settleMonth}
|
||||||
|
AND l.agent_amount > 0
|
||||||
|
UNION ALL
|
||||||
|
SELECT l.contract_id,
|
||||||
|
NULL::BIGINT AS payment_id,
|
||||||
|
CURRENT_DATE AS entry_date,
|
||||||
|
'8350' AS debit_account,
|
||||||
|
'2530' AS credit_account,
|
||||||
|
l.agent_amount AS amount,
|
||||||
|
CONCAT('유지수수료 ', l.settle_month) AS description,
|
||||||
|
NULL AS journal_no
|
||||||
|
FROM maintain_ledger l
|
||||||
|
WHERE l.settle_month = #{settleMonth}
|
||||||
|
AND l.agent_amount > 0
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.core.mapper.income.AgentAnnualIncomeMapper">
|
||||||
|
|
||||||
|
<!-- agent 조인 포함 응답 매핑 -->
|
||||||
|
<resultMap id="RespMap" type="com.ga.core.vo.income.AgentAnnualIncomeResp"/>
|
||||||
|
|
||||||
|
<!-- 목록/상세에서 공통으로 사용하는 SELECT 컬럼 -->
|
||||||
|
<sql id="joinCols">
|
||||||
|
i.agent_id,
|
||||||
|
a.agent_name,
|
||||||
|
a.org_id,
|
||||||
|
o.org_name,
|
||||||
|
i.settle_year,
|
||||||
|
i.total_commission,
|
||||||
|
i.total_tax_withheld,
|
||||||
|
i.total_local_tax,
|
||||||
|
i.business_income,
|
||||||
|
i.generated_at,
|
||||||
|
i.created_at,
|
||||||
|
i.updated_at
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<!-- ===== 목록 조회 ===== -->
|
||||||
|
<select id="selectList" resultMap="RespMap">
|
||||||
|
SELECT <include refid="joinCols"/>
|
||||||
|
FROM agent_annual_income i
|
||||||
|
JOIN agent a ON a.agent_id = i.agent_id
|
||||||
|
LEFT JOIN organization o ON o.org_id = a.org_id
|
||||||
|
<where>
|
||||||
|
<if test="settleYear != null">AND i.settle_year = #{settleYear}</if>
|
||||||
|
<if test="agentId != null">AND i.agent_id = #{agentId}</if>
|
||||||
|
<if test="orgId != null">AND a.org_id = #{orgId}</if>
|
||||||
|
<if test="searchKeyword != null and searchKeyword != ''">
|
||||||
|
AND a.agent_name LIKE CONCAT('%', #{searchKeyword}, '%')
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY i.settle_year DESC, i.agent_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectDetailById" resultMap="RespMap">
|
||||||
|
SELECT <include refid="joinCols"/>
|
||||||
|
FROM agent_annual_income i
|
||||||
|
JOIN agent a ON a.agent_id = i.agent_id
|
||||||
|
LEFT JOIN organization o ON o.org_id = a.org_id
|
||||||
|
WHERE i.agent_id = #{agentId}
|
||||||
|
AND i.settle_year = #{settleYear}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 연도 전체 UPSERT 집계: payment 테이블 기준 설계사별 SUM 후 UPSERT -->
|
||||||
|
<insert id="upsertAggregate">
|
||||||
|
INSERT INTO agent_annual_income
|
||||||
|
(agent_id, settle_year,
|
||||||
|
total_commission, total_tax_withheld, total_local_tax, business_income,
|
||||||
|
generated_at)
|
||||||
|
SELECT
|
||||||
|
p.agent_id,
|
||||||
|
#{year} AS settle_year,
|
||||||
|
COALESCE(SUM(p.pay_amount), 0) AS total_commission,
|
||||||
|
COALESCE(SUM(p.income_tax_amount), 0) AS total_tax_withheld,
|
||||||
|
COALESCE(SUM(p.local_tax_amount), 0) AS total_local_tax,
|
||||||
|
COALESCE(SUM(p.pay_amount)
|
||||||
|
- SUM(p.income_tax_amount)
|
||||||
|
- SUM(p.local_tax_amount), 0) AS business_income,
|
||||||
|
NOW() AS generated_at
|
||||||
|
FROM payment p
|
||||||
|
WHERE EXTRACT(YEAR FROM p.pay_date) = #{year}
|
||||||
|
GROUP BY p.agent_id
|
||||||
|
ON CONFLICT (agent_id, settle_year) DO UPDATE SET
|
||||||
|
total_commission = EXCLUDED.total_commission,
|
||||||
|
total_tax_withheld = EXCLUDED.total_tax_withheld,
|
||||||
|
total_local_tax = EXCLUDED.total_local_tax,
|
||||||
|
business_income = EXCLUDED.business_income,
|
||||||
|
generated_at = NOW(),
|
||||||
|
updated_at = NOW()
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -92,4 +92,51 @@
|
|||||||
<foreach collection="paymentIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
<foreach collection="paymentIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<select id="selectBySettleMonth" resultMap="VOMap">
|
||||||
|
SELECT p.*
|
||||||
|
FROM payment p
|
||||||
|
JOIN settle_master s ON s.settle_id = p.settle_id
|
||||||
|
WHERE s.settle_month = #{settleMonth}
|
||||||
|
ORDER BY p.payment_id
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByAgentAndMonth" resultMap="VOMap">
|
||||||
|
SELECT p.*
|
||||||
|
FROM payment p
|
||||||
|
JOIN settle_master s ON s.settle_id = p.settle_id
|
||||||
|
WHERE p.agent_id = #{agentId}
|
||||||
|
AND s.settle_month = #{settleMonth}
|
||||||
|
ORDER BY p.payment_id
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<update id="updateTaxes">
|
||||||
|
UPDATE payment
|
||||||
|
SET income_tax_amount = #{incomeTaxAmount},
|
||||||
|
local_tax_amount = #{localTaxAmount},
|
||||||
|
vat_amount = #{vatAmount},
|
||||||
|
net_amount = #{netAmount},
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE payment_id = #{paymentId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="updateTaxesBatch">
|
||||||
|
<foreach collection="list" item="p" separator=";">
|
||||||
|
UPDATE payment
|
||||||
|
SET income_tax_amount = #{p.incomeTaxAmount},
|
||||||
|
local_tax_amount = #{p.localTaxAmount},
|
||||||
|
vat_amount = #{p.vatAmount},
|
||||||
|
net_amount = #{p.netAmount},
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE payment_id = #{p.paymentId}
|
||||||
|
</foreach>
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="updateDeduction">
|
||||||
|
UPDATE payment
|
||||||
|
SET deduction_amount = #{deductionAmount},
|
||||||
|
net_amount = #{netAmount},
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE payment_id = #{paymentId}
|
||||||
|
</update>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import LoginPage from '@/pages/LoginPage';
|
|||||||
import MainLayout from '@/layouts/MainLayout';
|
import MainLayout from '@/layouts/MainLayout';
|
||||||
import Dashboard from '@/pages/Dashboard';
|
import Dashboard from '@/pages/Dashboard';
|
||||||
|
|
||||||
|
// ── P5: 연말명세서 / 분개장 ──────────────────────────
|
||||||
|
import AgentAnnualIncomeList from '@/pages/income/AgentAnnualIncomeList';
|
||||||
|
import AccountingJournalList from '@/pages/accounting/AccountingJournalList';
|
||||||
|
|
||||||
|
// ── P5-W2: 원천세 / 부가세 신고 ──────────────────────
|
||||||
|
import WithholdingTaxReportList from '@/pages/tax/WithholdingTaxReportList';
|
||||||
|
import VatReportList from '@/pages/tax/VatReportList';
|
||||||
|
|
||||||
// ── P4: 수수료 종류 ──────────────────────────────────
|
// ── P4: 수수료 종류 ──────────────────────────────────
|
||||||
import CommissionMaster from '@/pages/commission/CommissionMaster';
|
import CommissionMaster from '@/pages/commission/CommissionMaster';
|
||||||
import CollectionCommission from '@/pages/commission/CollectionCommission';
|
import CollectionCommission from '@/pages/commission/CollectionCommission';
|
||||||
@@ -146,6 +154,14 @@ export default function App() {
|
|||||||
<Route path="kpi/retention" element={<KpiRetention />} />
|
<Route path="kpi/retention" element={<KpiRetention />} />
|
||||||
<Route path="kpi/cumulative" element={<KpiCumulative />} />
|
<Route path="kpi/cumulative" element={<KpiCumulative />} />
|
||||||
|
|
||||||
|
{/* P5: 연말명세서 / 분개장 */}
|
||||||
|
<Route path="commission/annual-income" element={<AgentAnnualIncomeList />} />
|
||||||
|
<Route path="commission/accounting-journal" element={<AccountingJournalList />} />
|
||||||
|
|
||||||
|
{/* P5-W2: 원천세 / 부가세 신고 */}
|
||||||
|
<Route path="commission/withholding-tax" element={<WithholdingTaxReportList />} />
|
||||||
|
<Route path="commission/vat-report" element={<VatReportList />} />
|
||||||
|
|
||||||
{/* P4: 수수료 종류 */}
|
{/* P4: 수수료 종류 */}
|
||||||
<Route path="commission/type" element={<CommissionMaster />} />
|
<Route path="commission/type" element={<CommissionMaster />} />
|
||||||
<Route path="commission/master" element={<CommissionMaster />} />
|
<Route path="commission/master" element={<CommissionMaster />} />
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import api, { unwrap } from './request';
|
||||||
|
|
||||||
|
export interface AccountingJournalResp extends Record<string, unknown> {
|
||||||
|
entryId: number;
|
||||||
|
journalNo?: string;
|
||||||
|
agentId?: number;
|
||||||
|
agentName?: string;
|
||||||
|
contractNo?: string;
|
||||||
|
settleMonth?: string;
|
||||||
|
accountCode: string;
|
||||||
|
accountName?: string;
|
||||||
|
debitAmount: number;
|
||||||
|
creditAmount: number;
|
||||||
|
description?: string;
|
||||||
|
status?: string;
|
||||||
|
statusName?: string;
|
||||||
|
closedAt?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountingGenerateReq {
|
||||||
|
settleMonth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountingCloseReq {
|
||||||
|
journalNo: string;
|
||||||
|
entryIds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountingSearchParam {
|
||||||
|
settleMonth?: string;
|
||||||
|
journalNo?: string;
|
||||||
|
agentName?: string;
|
||||||
|
status?: string;
|
||||||
|
pageNum?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const accountingApi = {
|
||||||
|
list: (params?: AccountingSearchParam) =>
|
||||||
|
unwrap<{ list: AccountingJournalResp[]; total: number }>(
|
||||||
|
api.get('/api/accounting-entries', { params })
|
||||||
|
),
|
||||||
|
generate: (req: AccountingGenerateReq) =>
|
||||||
|
unwrap<void>(api.post('/api/accounting-entries/generate', req)),
|
||||||
|
close: (req: AccountingCloseReq) =>
|
||||||
|
unwrap<void>(api.put('/api/accounting-entries/close', req)),
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import api, { unwrap } from './request';
|
||||||
|
|
||||||
|
export interface AgentAnnualIncomeResp extends Record<string, unknown> {
|
||||||
|
incomeId: number;
|
||||||
|
agentId: number;
|
||||||
|
agentName?: string;
|
||||||
|
agentCode?: string;
|
||||||
|
orgName?: string;
|
||||||
|
settleYear: number;
|
||||||
|
totalCommission: number;
|
||||||
|
totalTaxWithheld: number;
|
||||||
|
businessIncome: number;
|
||||||
|
status?: string;
|
||||||
|
statusName?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentAnnualIncomeSaveReq {
|
||||||
|
agentId: number;
|
||||||
|
settleYear: number;
|
||||||
|
totalCommission: number;
|
||||||
|
totalTaxWithheld: number;
|
||||||
|
businessIncome: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentAnnualIncomeSearchParam {
|
||||||
|
settleYear?: number;
|
||||||
|
agentId?: number;
|
||||||
|
agentName?: string;
|
||||||
|
pageNum?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const incomeApi = {
|
||||||
|
list: (params?: AgentAnnualIncomeSearchParam) =>
|
||||||
|
unwrap<{ list: AgentAnnualIncomeResp[]; total: number }>(
|
||||||
|
api.get('/api/agent-annual-incomes', { params })
|
||||||
|
),
|
||||||
|
detail: (id: number) =>
|
||||||
|
unwrap<AgentAnnualIncomeResp>(api.get(`/api/agent-annual-incomes/${id}`)),
|
||||||
|
regenerate: (year: number) =>
|
||||||
|
unwrap<void>(api.post(`/api/agent-annual-incomes/regenerate`, null, { params: { year } })),
|
||||||
|
};
|
||||||
@@ -40,4 +40,6 @@ export const regulatoryApi = {
|
|||||||
unwrap<void>(api.put(`/api/regulatory-limits/${id}`, req)),
|
unwrap<void>(api.put(`/api/regulatory-limits/${id}`, req)),
|
||||||
deactivate: (id: number) =>
|
deactivate: (id: number) =>
|
||||||
unwrap<void>(api.put(`/api/regulatory-limits/${id}/deactivate`)),
|
unwrap<void>(api.put(`/api/regulatory-limits/${id}/deactivate`)),
|
||||||
|
activate: (id: number) =>
|
||||||
|
unwrap<void>(api.put(`/api/regulatory-limits/${id}/activate`)),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import api, { unwrap } from './request';
|
||||||
|
|
||||||
|
export interface VatReportResp extends Record<string, unknown> {
|
||||||
|
reportId: number;
|
||||||
|
reportYear: number;
|
||||||
|
reportQuarter: number;
|
||||||
|
salesAmount: number;
|
||||||
|
salesVat: number;
|
||||||
|
purchaseAmount: number;
|
||||||
|
purchaseVat: number;
|
||||||
|
payableVat: number;
|
||||||
|
status?: string;
|
||||||
|
statusName?: string;
|
||||||
|
reportedAt?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VatSearchParam {
|
||||||
|
reportYear?: number;
|
||||||
|
reportQuarter?: number;
|
||||||
|
status?: string;
|
||||||
|
pageNum?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VatRegenerateReq {
|
||||||
|
reportYear: number;
|
||||||
|
reportQuarter: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VatReportCompleteReq {
|
||||||
|
ids: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const vatApi = {
|
||||||
|
list: (params?: VatSearchParam) =>
|
||||||
|
unwrap<{ list: VatReportResp[]; total: number }>(
|
||||||
|
api.get('/api/vat-reports', { params })
|
||||||
|
),
|
||||||
|
detail: (id: number) =>
|
||||||
|
unwrap<VatReportResp>(api.get(`/api/vat-reports/${id}`)),
|
||||||
|
regenerate: (req: VatRegenerateReq) =>
|
||||||
|
unwrap<void>(api.post('/api/vat-reports/regenerate', req)),
|
||||||
|
complete: (req: VatReportCompleteReq) =>
|
||||||
|
unwrap<void>(api.put('/api/vat-reports/complete', req)),
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import api, { unwrap } from './request';
|
||||||
|
|
||||||
|
export interface WithholdingTaxReportResp extends Record<string, unknown> {
|
||||||
|
reportId: number;
|
||||||
|
agentId: number;
|
||||||
|
agentName?: string;
|
||||||
|
agentCode?: string;
|
||||||
|
orgName?: string;
|
||||||
|
reportYear: number;
|
||||||
|
reportQuarter: number;
|
||||||
|
totalIncome: number;
|
||||||
|
withholdingTax: number;
|
||||||
|
status?: string;
|
||||||
|
statusName?: string;
|
||||||
|
reportedAt?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithholdingTaxSearchParam {
|
||||||
|
reportYear?: number;
|
||||||
|
reportQuarter?: number;
|
||||||
|
agentId?: number;
|
||||||
|
agentName?: string;
|
||||||
|
status?: string;
|
||||||
|
pageNum?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithholdingTaxRegenerateReq {
|
||||||
|
reportYear: number;
|
||||||
|
reportQuarter: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithholdingTaxReportCompleteReq {
|
||||||
|
ids: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withholdingTaxApi = {
|
||||||
|
list: (params?: WithholdingTaxSearchParam) =>
|
||||||
|
unwrap<{ list: WithholdingTaxReportResp[]; total: number }>(
|
||||||
|
api.get('/api/withholding-tax-reports', { params })
|
||||||
|
),
|
||||||
|
detail: (id: number) =>
|
||||||
|
unwrap<WithholdingTaxReportResp>(api.get(`/api/withholding-tax-reports/${id}`)),
|
||||||
|
regenerate: (req: WithholdingTaxRegenerateReq) =>
|
||||||
|
unwrap<void>(api.post('/api/withholding-tax-reports/regenerate', req)),
|
||||||
|
complete: (req: WithholdingTaxReportCompleteReq) =>
|
||||||
|
unwrap<void>(api.put('/api/withholding-tax-reports/complete', req)),
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { Skeleton } from 'antd';
|
|||||||
import { AgGridReact } from '@ag-grid-community/react';
|
import { AgGridReact } from '@ag-grid-community/react';
|
||||||
import { ModuleRegistry } from '@ag-grid-community/core';
|
import { ModuleRegistry } from '@ag-grid-community/core';
|
||||||
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
|
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
|
||||||
import type { ColDef, GridReadyEvent, GridApi, RowClassParams, RowStyle } from '@ag-grid-community/core';
|
import type { ColDef, GridReadyEvent, GridApi, RowClassParams, RowStyle, SelectionChangedEvent } from '@ag-grid-community/core';
|
||||||
import '@ag-grid-community/styles/ag-grid.css';
|
import '@ag-grid-community/styles/ag-grid.css';
|
||||||
import '@ag-grid-community/styles/ag-theme-quartz.css';
|
import '@ag-grid-community/styles/ag-theme-quartz.css';
|
||||||
import { GRAY } from '@/theme/tokens';
|
import { GRAY } from '@/theme/tokens';
|
||||||
@@ -33,6 +33,8 @@ interface Props<T> {
|
|||||||
rowKey?: keyof T;
|
rowKey?: keyof T;
|
||||||
/** 행별 스타일 콜백 (만료 임박 강조 등) */
|
/** 행별 스타일 콜백 (만료 임박 강조 등) */
|
||||||
getRowStyle?: (params: RowClassParams<T>) => RowStyle | undefined;
|
getRowStyle?: (params: RowClassParams<T>) => RowStyle | undefined;
|
||||||
|
rowSelection?: 'single' | 'multiple';
|
||||||
|
onSelectionChanged?: (e: SelectionChangedEvent<T>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,6 +52,8 @@ export default function DataGrid<T extends Record<string, unknown>>({
|
|||||||
onCellChanged,
|
onCellChanged,
|
||||||
rowKey,
|
rowKey,
|
||||||
getRowStyle: getRowStyleProp,
|
getRowStyle: getRowStyleProp,
|
||||||
|
rowSelection: rowSelectionProp = 'single',
|
||||||
|
onSelectionChanged,
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
const apiRef = useRef<GridApi<T> | null>(null);
|
const apiRef = useRef<GridApi<T> | null>(null);
|
||||||
|
|
||||||
@@ -93,7 +97,8 @@ export default function DataGrid<T extends Record<string, unknown>>({
|
|||||||
rowHeight={56}
|
rowHeight={56}
|
||||||
headerHeight={44}
|
headerHeight={44}
|
||||||
localeText={AG_GRID_LOCALE_KO}
|
localeText={AG_GRID_LOCALE_KO}
|
||||||
rowSelection="single"
|
rowSelection={rowSelectionProp}
|
||||||
|
onSelectionChanged={onSelectionChanged}
|
||||||
getRowId={rowKey ? (p) => String(p.data[rowKey]) : undefined}
|
getRowId={rowKey ? (p) => String(p.data[rowKey]) : undefined}
|
||||||
pinnedBottomRowData={pinnedBottomRow ? [pinnedBottomRow as T] : undefined}
|
pinnedBottomRowData={pinnedBottomRow ? [pinnedBottomRow as T] : undefined}
|
||||||
getRowStyle={(params) => {
|
getRowStyle={(params) => {
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { Alert, Form, Input, Modal, Space, message } from 'antd';
|
||||||
|
import { ProCard } from '@ant-design/pro-components';
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import type { ColDef, SelectionChangedEvent } from '@ag-grid-community/core';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import SearchForm from '@/components/common/SearchForm';
|
||||||
|
import DataGrid from '@/components/common/DataGrid';
|
||||||
|
import PermissionButton from '@/components/common/PermissionButton';
|
||||||
|
import { accountingApi, AccountingJournalResp, AccountingSearchParam } from '@/api/accounting';
|
||||||
|
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||||
|
|
||||||
|
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||||
|
|
||||||
|
const COLUMNS: ColDef<AccountingJournalResp>[] = [
|
||||||
|
{ field: 'journalNo', headerName: '분개번호', width: 140, pinned: 'left', rowGroup: false },
|
||||||
|
{ field: 'settleMonth', headerName: '정산월', width: 100 },
|
||||||
|
{ field: 'agentName', headerName: '설계사', width: 120 },
|
||||||
|
{ field: 'contractNo', headerName: '계약번호', width: 140 },
|
||||||
|
{ field: 'accountCode', headerName: '계정코드', width: 110 },
|
||||||
|
{ field: 'accountName', headerName: '계정명', width: 150 },
|
||||||
|
{ field: 'debitAmount', headerName: '차변', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||||
|
{ field: 'creditAmount', headerName: '대변', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||||
|
{ field: 'description', headerName: '적요', flex: 1 },
|
||||||
|
{ field: 'statusName', headerName: '상태', width: 90 },
|
||||||
|
{ field: 'closedAt', headerName: '마감일시', width: 140 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm };
|
||||||
|
|
||||||
|
export default function AccountingJournalList() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const currentMonth = dayjs().format('YYYYMM');
|
||||||
|
const [params, setParams] = useState<AccountingSearchParam>({
|
||||||
|
settleMonth: currentMonth,
|
||||||
|
pageSize: 500,
|
||||||
|
});
|
||||||
|
const [selectedRows, setSelectedRows] = useState<AccountingJournalResp[]>([]);
|
||||||
|
const [closeModalOpen, setCloseModalOpen] = useState(false);
|
||||||
|
const [generateModalOpen, setGenerateModalOpen] = useState(false);
|
||||||
|
const [closeForm] = Form.useForm();
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ['accounting-journals', params],
|
||||||
|
queryFn: () => accountingApi.list(params),
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = data?.list ?? [];
|
||||||
|
|
||||||
|
const handleSelectionChanged = useCallback((e: SelectionChangedEvent<AccountingJournalResp>) => {
|
||||||
|
setSelectedRows(e.api.getSelectedRows());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGenerate = async () => {
|
||||||
|
try {
|
||||||
|
await accountingApi.generate({ settleMonth: params.settleMonth ?? currentMonth });
|
||||||
|
message.success('분개 생성 완료');
|
||||||
|
qc.invalidateQueries({ queryKey: ['accounting-journals'] });
|
||||||
|
setGenerateModalOpen(false);
|
||||||
|
} catch { /* request.ts 처리 */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = async () => {
|
||||||
|
const values = await closeForm.validateFields();
|
||||||
|
const ids = selectedRows.map((r) => r.entryId);
|
||||||
|
try {
|
||||||
|
await accountingApi.close({ journalNo: values.journalNo, entryIds: ids });
|
||||||
|
message.success('마감 완료');
|
||||||
|
qc.invalidateQueries({ queryKey: ['accounting-journals'] });
|
||||||
|
setCloseModalOpen(false);
|
||||||
|
closeForm.resetFields();
|
||||||
|
setSelectedRows([]);
|
||||||
|
} catch { /* request.ts 처리 */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title="회계 분개장"
|
||||||
|
description="정산 회계 분개 엔트리 관리"
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<PermissionButton
|
||||||
|
menuCode="ACCOUNTING_JOURNAL"
|
||||||
|
permCode="CREATE"
|
||||||
|
onClick={() => setGenerateModalOpen(true)}
|
||||||
|
>
|
||||||
|
분개 생성
|
||||||
|
</PermissionButton>
|
||||||
|
<PermissionButton
|
||||||
|
menuCode="ACCOUNTING_JOURNAL"
|
||||||
|
permCode="APPROVE"
|
||||||
|
type="primary"
|
||||||
|
disabled={selectedRows.length === 0}
|
||||||
|
onClick={() => setCloseModalOpen(true)}
|
||||||
|
>
|
||||||
|
마감 ({selectedRows.length}건)
|
||||||
|
</PermissionButton>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isError && (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="API 미응답"
|
||||||
|
description="/api/accounting-journals 를 확인하세요."
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SearchForm
|
||||||
|
conditions={[
|
||||||
|
{ type: 'month', name: 'settleMonth', label: '정산월', span: 6 },
|
||||||
|
{ type: 'text', name: 'journalNo', label: '분개번호', span: 6 },
|
||||||
|
{ type: 'text', name: 'agentName', label: '설계사명', span: 6 },
|
||||||
|
]}
|
||||||
|
onSearch={(v) => setParams({ ...v as AccountingSearchParam, pageSize: 500 })}
|
||||||
|
onReset={() => setParams({ settleMonth: currentMonth, pageSize: 500 })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProCard style={cardStyle}>
|
||||||
|
<DataGrid<AccountingJournalResp>
|
||||||
|
rows={rows}
|
||||||
|
columns={COLUMNS}
|
||||||
|
loading={isLoading}
|
||||||
|
height={620}
|
||||||
|
rowKey="entryId"
|
||||||
|
rowSelection="multiple"
|
||||||
|
onSelectionChanged={handleSelectionChanged}
|
||||||
|
/>
|
||||||
|
</ProCard>
|
||||||
|
|
||||||
|
{/* 분개 생성 확인 모달 */}
|
||||||
|
<Modal
|
||||||
|
title="분개 생성"
|
||||||
|
open={generateModalOpen}
|
||||||
|
onOk={handleGenerate}
|
||||||
|
onCancel={() => setGenerateModalOpen(false)}
|
||||||
|
okText="생성"
|
||||||
|
cancelText="취소"
|
||||||
|
>
|
||||||
|
<p>{params.settleMonth} 정산월의 회계 분개를 생성하시겠습니까?</p>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 마감 처리 모달 */}
|
||||||
|
<Modal
|
||||||
|
title="분개 마감"
|
||||||
|
open={closeModalOpen}
|
||||||
|
onOk={handleClose}
|
||||||
|
onCancel={() => { setCloseModalOpen(false); closeForm.resetFields(); }}
|
||||||
|
okText="마감"
|
||||||
|
cancelText="취소"
|
||||||
|
>
|
||||||
|
<p>선택한 {selectedRows.length}건의 분개를 마감합니다.</p>
|
||||||
|
<Form form={closeForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||||
|
<Form.Item
|
||||||
|
name="journalNo"
|
||||||
|
label="분개번호 (journal_no)"
|
||||||
|
rules={[{ required: true, message: '분개번호를 입력하세요' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="예: JNL-2024-001" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Alert, Modal, message } from 'antd';
|
||||||
|
import { ProCard } from '@ant-design/pro-components';
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import type { ColDef } from '@ag-grid-community/core';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import SearchForm from '@/components/common/SearchForm';
|
||||||
|
import DataGrid from '@/components/common/DataGrid';
|
||||||
|
import PermissionButton from '@/components/common/PermissionButton';
|
||||||
|
import { incomeApi, AgentAnnualIncomeResp, AgentAnnualIncomeSearchParam } from '@/api/income';
|
||||||
|
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||||
|
|
||||||
|
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||||
|
|
||||||
|
const COLUMNS: ColDef<AgentAnnualIncomeResp>[] = [
|
||||||
|
{ field: 'settleYear', headerName: '귀속연도', width: 100, pinned: 'left' },
|
||||||
|
{ field: 'agentCode', headerName: '설계사코드', width: 120 },
|
||||||
|
{ field: 'agentName', headerName: '설계사명', width: 120 },
|
||||||
|
{ field: 'orgName', headerName: '소속조직', width: 160 },
|
||||||
|
{ field: 'totalCommission', headerName: '총수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||||
|
{ field: 'totalTaxWithheld', headerName: '원천징수세액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||||
|
{ field: 'businessIncome', headerName: '사업소득', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||||
|
{ field: 'statusName', headerName: '상태', width: 90 },
|
||||||
|
{ field: 'updatedAt', headerName: '최종수정', width: 140 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm };
|
||||||
|
|
||||||
|
export default function AgentAnnualIncomeList() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const currentYear = dayjs().year();
|
||||||
|
const [params, setParams] = useState<AgentAnnualIncomeSearchParam>({
|
||||||
|
settleYear: currentYear,
|
||||||
|
pageSize: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ['annual-income', params],
|
||||||
|
queryFn: () => incomeApi.list(params),
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = data?.list ?? [];
|
||||||
|
|
||||||
|
const pinnedRow = rows.length > 0 ? {
|
||||||
|
agentName: '합계',
|
||||||
|
totalCommission: rows.reduce((s, r) => s + (r.totalCommission ?? 0), 0),
|
||||||
|
totalTaxWithheld: rows.reduce((s, r) => s + (r.totalTaxWithheld ?? 0), 0),
|
||||||
|
businessIncome: rows.reduce((s, r) => s + (r.businessIncome ?? 0), 0),
|
||||||
|
} as Partial<AgentAnnualIncomeResp> : undefined;
|
||||||
|
|
||||||
|
const handleRegenerate = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '연말 사업소득 재집계',
|
||||||
|
content: `${params.settleYear ?? currentYear}년 데이터를 재집계하시겠습니까? 기존 데이터가 갱신됩니다.`,
|
||||||
|
okText: '재집계',
|
||||||
|
cancelText: '취소',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await incomeApi.regenerate(params.settleYear ?? currentYear);
|
||||||
|
message.success('재집계 완료');
|
||||||
|
qc.invalidateQueries({ queryKey: ['annual-income'] });
|
||||||
|
} catch { /* request.ts 처리 */ }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title="연말 사업소득 명세서"
|
||||||
|
description="설계사별 귀속연도 사업소득 집계 현황"
|
||||||
|
extra={
|
||||||
|
<PermissionButton
|
||||||
|
menuCode="ANNUAL_INCOME"
|
||||||
|
permCode="APPROVE"
|
||||||
|
type="primary"
|
||||||
|
onClick={handleRegenerate}
|
||||||
|
>
|
||||||
|
재집계
|
||||||
|
</PermissionButton>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isError && (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="API 미응답"
|
||||||
|
description="/api/agent-annual-incomes 를 확인하세요."
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SearchForm
|
||||||
|
conditions={[
|
||||||
|
{ type: 'text', name: 'settleYear', label: '귀속연도', span: 6 },
|
||||||
|
{ type: 'text', name: 'agentName', label: '설계사명', span: 6 },
|
||||||
|
]}
|
||||||
|
onSearch={(v) => setParams({ ...v as AgentAnnualIncomeSearchParam, pageSize: 500 })}
|
||||||
|
onReset={() => setParams({ settleYear: currentYear, pageSize: 500 })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProCard style={cardStyle}>
|
||||||
|
<DataGrid<AgentAnnualIncomeResp>
|
||||||
|
rows={rows}
|
||||||
|
columns={COLUMNS}
|
||||||
|
loading={isLoading}
|
||||||
|
height={620}
|
||||||
|
rowKey="incomeId"
|
||||||
|
pinnedBottomRow={pinnedRow}
|
||||||
|
/>
|
||||||
|
</ProCard>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -59,6 +59,20 @@ export default function RegulatoryLimitList() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onActivate = (record: RegulatoryLimitResp) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '규제 한도 활성화',
|
||||||
|
content: `[${record.limitTypeName ?? record.limitType}] 한도를 활성화하시겠습니까?`,
|
||||||
|
okText: '활성화',
|
||||||
|
cancelText: '취소',
|
||||||
|
onOk: async () => {
|
||||||
|
await regulatoryApi.activate(record.limitId);
|
||||||
|
message.success('활성화 완료');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async (v: any) => {
|
const onSubmit = async (v: any) => {
|
||||||
const req: RegulatoryLimitSaveReq = {
|
const req: RegulatoryLimitSaveReq = {
|
||||||
...v,
|
...v,
|
||||||
@@ -108,9 +122,12 @@ export default function RegulatoryLimitList() {
|
|||||||
render: (_, r) => [
|
render: (_, r) => [
|
||||||
<PermissionButton key="edit" menuCode="REGULATORY_LIMIT" permCode="UPDATE" size="small" type="link"
|
<PermissionButton key="edit" menuCode="REGULATORY_LIMIT" permCode="UPDATE" size="small" type="link"
|
||||||
onClick={() => openEdit(r)}>수정</PermissionButton>,
|
onClick={() => openEdit(r)}>수정</PermissionButton>,
|
||||||
r.isActive && (
|
r.isActive ? (
|
||||||
<PermissionButton key="deact" menuCode="REGULATORY_LIMIT" permCode="UPDATE" size="small" type="link" danger
|
<PermissionButton key="deact" menuCode="REGULATORY_LIMIT" permCode="UPDATE" size="small" type="link" danger
|
||||||
onClick={() => onDeactivate(r)}>비활성화</PermissionButton>
|
onClick={() => onDeactivate(r)}>비활성화</PermissionButton>
|
||||||
|
) : (
|
||||||
|
<PermissionButton key="act" menuCode="REGULATORY_LIMIT" permCode="UPDATE" size="small" type="link"
|
||||||
|
onClick={() => onActivate(r)}>활성화</PermissionButton>
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { Alert, Modal, Space, message } from 'antd';
|
||||||
|
import { ProCard } from '@ant-design/pro-components';
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import type { ColDef, SelectionChangedEvent } from '@ag-grid-community/core';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import SearchForm from '@/components/common/SearchForm';
|
||||||
|
import DataGrid from '@/components/common/DataGrid';
|
||||||
|
import PermissionButton from '@/components/common/PermissionButton';
|
||||||
|
import { vatApi, VatReportResp, VatSearchParam } from '@/api/vat';
|
||||||
|
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||||
|
|
||||||
|
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||||
|
|
||||||
|
const COLUMNS: ColDef<VatReportResp>[] = [
|
||||||
|
{ field: 'reportYear', headerName: '신고연도', width: 100, pinned: 'left' },
|
||||||
|
{ field: 'reportQuarter', headerName: '분기', width: 80, pinned: 'left', valueFormatter: (p) => `${p.value}Q` },
|
||||||
|
{ field: 'salesAmount', headerName: '매출액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||||
|
{ field: 'salesVat', headerName: '매출세액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||||
|
{ field: 'purchaseAmount', headerName: '매입액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||||
|
{ field: 'purchaseVat', headerName: '매입세액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||||
|
{ field: 'payableVat', headerName: '납부세액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||||
|
{ field: 'statusName', headerName: '상태', width: 90 },
|
||||||
|
{ field: 'reportedAt', headerName: '신고일시', width: 140 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm };
|
||||||
|
|
||||||
|
export default function VatReportList() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const currentYear = dayjs().year();
|
||||||
|
const currentQuarter = Math.ceil((dayjs().month() + 1) / 3);
|
||||||
|
|
||||||
|
const [params, setParams] = useState<VatSearchParam>({
|
||||||
|
reportYear: currentYear,
|
||||||
|
reportQuarter: currentQuarter,
|
||||||
|
pageSize: 200,
|
||||||
|
});
|
||||||
|
const [selectedRows, setSelectedRows] = useState<VatReportResp[]>([]);
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ['vat-reports', params],
|
||||||
|
queryFn: () => vatApi.list(params),
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = data?.list ?? [];
|
||||||
|
|
||||||
|
const pinnedRow = rows.length > 0 ? {
|
||||||
|
reportYear: '합계' as unknown as number,
|
||||||
|
salesAmount: rows.reduce((s, r) => s + (r.salesAmount ?? 0), 0),
|
||||||
|
salesVat: rows.reduce((s, r) => s + (r.salesVat ?? 0), 0),
|
||||||
|
purchaseAmount: rows.reduce((s, r) => s + (r.purchaseAmount ?? 0), 0),
|
||||||
|
purchaseVat: rows.reduce((s, r) => s + (r.purchaseVat ?? 0), 0),
|
||||||
|
payableVat: rows.reduce((s, r) => s + (r.payableVat ?? 0), 0),
|
||||||
|
} as Partial<VatReportResp> : undefined;
|
||||||
|
|
||||||
|
const handleSelectionChanged = useCallback((e: SelectionChangedEvent<VatReportResp>) => {
|
||||||
|
setSelectedRows(e.api.getSelectedRows());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRegenerate = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '부가세 재집계',
|
||||||
|
content: `${params.reportYear}년 ${params.reportQuarter}Q 데이터를 재집계하시겠습니까?`,
|
||||||
|
okText: '재집계',
|
||||||
|
cancelText: '취소',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await vatApi.regenerate({
|
||||||
|
reportYear: params.reportYear ?? currentYear,
|
||||||
|
reportQuarter: params.reportQuarter ?? currentQuarter,
|
||||||
|
});
|
||||||
|
message.success('재집계 완료');
|
||||||
|
qc.invalidateQueries({ queryKey: ['vat-reports'] });
|
||||||
|
} catch { /* request.ts 처리 */ }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '신고완료 처리',
|
||||||
|
content: `선택한 ${selectedRows.length}건을 신고완료 처리하시겠습니까?`,
|
||||||
|
okText: '신고완료',
|
||||||
|
cancelText: '취소',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
// NOTE: ids 필드명은 T19 컨트롤러 확인 후 수정 필요
|
||||||
|
await vatApi.complete({ ids: selectedRows.map((r) => r.reportId) });
|
||||||
|
message.success('신고완료 처리됨');
|
||||||
|
qc.invalidateQueries({ queryKey: ['vat-reports'] });
|
||||||
|
setSelectedRows([]);
|
||||||
|
} catch { /* request.ts 처리 */ }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title="부가세 신고 현황"
|
||||||
|
description="분기별 매출/매입/납부 부가세 집계 및 관리"
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<PermissionButton menuCode="VAT_REPORT" permCode="APPROVE" onClick={handleRegenerate}>
|
||||||
|
재집계
|
||||||
|
</PermissionButton>
|
||||||
|
<PermissionButton
|
||||||
|
menuCode="VAT_REPORT"
|
||||||
|
permCode="APPROVE"
|
||||||
|
type="primary"
|
||||||
|
disabled={selectedRows.length === 0}
|
||||||
|
onClick={handleComplete}
|
||||||
|
>
|
||||||
|
신고완료 ({selectedRows.length}건)
|
||||||
|
</PermissionButton>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isError && (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="API 미응답"
|
||||||
|
description="/api/vat-reports 를 확인하세요."
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SearchForm
|
||||||
|
conditions={[
|
||||||
|
{ type: 'text', name: 'reportYear', label: '신고연도', span: 6 },
|
||||||
|
{ type: 'text', name: 'reportQuarter', label: '분기(1~4)', span: 6 },
|
||||||
|
]}
|
||||||
|
onSearch={(v) => setParams({ ...v as VatSearchParam, pageSize: 200 })}
|
||||||
|
onReset={() => setParams({ reportYear: currentYear, reportQuarter: currentQuarter, pageSize: 200 })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProCard style={cardStyle}>
|
||||||
|
<DataGrid<VatReportResp>
|
||||||
|
rows={rows}
|
||||||
|
columns={COLUMNS}
|
||||||
|
loading={isLoading}
|
||||||
|
height={520}
|
||||||
|
rowKey="reportId"
|
||||||
|
rowSelection="multiple"
|
||||||
|
onSelectionChanged={handleSelectionChanged}
|
||||||
|
pinnedBottomRow={pinnedRow}
|
||||||
|
/>
|
||||||
|
</ProCard>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { Alert, Modal, Space, message } from 'antd';
|
||||||
|
import { ProCard } from '@ant-design/pro-components';
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import type { ColDef, SelectionChangedEvent } from '@ag-grid-community/core';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
|
import SearchForm from '@/components/common/SearchForm';
|
||||||
|
import DataGrid from '@/components/common/DataGrid';
|
||||||
|
import PermissionButton from '@/components/common/PermissionButton';
|
||||||
|
import {
|
||||||
|
withholdingTaxApi,
|
||||||
|
WithholdingTaxReportResp,
|
||||||
|
WithholdingTaxSearchParam,
|
||||||
|
} from '@/api/withholdingTax';
|
||||||
|
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||||
|
|
||||||
|
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||||
|
|
||||||
|
const COLUMNS: ColDef<WithholdingTaxReportResp>[] = [
|
||||||
|
{ field: 'reportYear', headerName: '신고연도', width: 100, pinned: 'left' },
|
||||||
|
{ field: 'reportQuarter', headerName: '분기', width: 80, pinned: 'left', valueFormatter: (p) => `${p.value}Q` },
|
||||||
|
{ field: 'agentCode', headerName: '설계사코드', width: 120 },
|
||||||
|
{ field: 'agentName', headerName: '설계사명', width: 120 },
|
||||||
|
{ field: 'orgName', headerName: '소속조직', width: 160 },
|
||||||
|
{ field: 'totalIncome', headerName: '지급총액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||||
|
{ field: 'withholdingTax', headerName: '원천징수세', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||||
|
{ field: 'statusName', headerName: '상태', width: 90 },
|
||||||
|
{ field: 'reportedAt', headerName: '신고일시', width: 140 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm };
|
||||||
|
|
||||||
|
export default function WithholdingTaxReportList() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const currentYear = dayjs().year();
|
||||||
|
const currentQuarter = Math.ceil((dayjs().month() + 1) / 3);
|
||||||
|
|
||||||
|
const [params, setParams] = useState<WithholdingTaxSearchParam>({
|
||||||
|
reportYear: currentYear,
|
||||||
|
reportQuarter: currentQuarter,
|
||||||
|
pageSize: 500,
|
||||||
|
});
|
||||||
|
const [selectedRows, setSelectedRows] = useState<WithholdingTaxReportResp[]>([]);
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ['withholding-tax', params],
|
||||||
|
queryFn: () => withholdingTaxApi.list(params),
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = data?.list ?? [];
|
||||||
|
|
||||||
|
const pinnedRow = rows.length > 0 ? {
|
||||||
|
agentName: '합계',
|
||||||
|
totalIncome: rows.reduce((s, r) => s + (r.totalIncome ?? 0), 0),
|
||||||
|
withholdingTax: rows.reduce((s, r) => s + (r.withholdingTax ?? 0), 0),
|
||||||
|
} as Partial<WithholdingTaxReportResp> : undefined;
|
||||||
|
|
||||||
|
const handleSelectionChanged = useCallback((e: SelectionChangedEvent<WithholdingTaxReportResp>) => {
|
||||||
|
setSelectedRows(e.api.getSelectedRows());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRegenerate = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '원천세 재집계',
|
||||||
|
content: `${params.reportYear}년 ${params.reportQuarter}Q 데이터를 재집계하시겠습니까?`,
|
||||||
|
okText: '재집계',
|
||||||
|
cancelText: '취소',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await withholdingTaxApi.regenerate({
|
||||||
|
reportYear: params.reportYear ?? currentYear,
|
||||||
|
reportQuarter: params.reportQuarter ?? currentQuarter,
|
||||||
|
});
|
||||||
|
message.success('재집계 완료');
|
||||||
|
qc.invalidateQueries({ queryKey: ['withholding-tax'] });
|
||||||
|
} catch { /* request.ts 처리 */ }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '신고완료 처리',
|
||||||
|
content: `선택한 ${selectedRows.length}건을 신고완료 처리하시겠습니까?`,
|
||||||
|
okText: '신고완료',
|
||||||
|
cancelText: '취소',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
// NOTE: ids 필드명은 T18 컨트롤러 확인 후 수정 필요
|
||||||
|
await withholdingTaxApi.complete({ ids: selectedRows.map((r) => r.reportId) });
|
||||||
|
message.success('신고완료 처리됨');
|
||||||
|
qc.invalidateQueries({ queryKey: ['withholding-tax'] });
|
||||||
|
setSelectedRows([]);
|
||||||
|
} catch { /* request.ts 처리 */ }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title="원천세 신고 현황"
|
||||||
|
description="설계사별 분기 원천세 신고 집계 및 관리"
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<PermissionButton menuCode="WITHHOLDING_TAX" permCode="APPROVE" onClick={handleRegenerate}>
|
||||||
|
재집계
|
||||||
|
</PermissionButton>
|
||||||
|
<PermissionButton
|
||||||
|
menuCode="WITHHOLDING_TAX"
|
||||||
|
permCode="APPROVE"
|
||||||
|
type="primary"
|
||||||
|
disabled={selectedRows.length === 0}
|
||||||
|
onClick={handleComplete}
|
||||||
|
>
|
||||||
|
신고완료 ({selectedRows.length}건)
|
||||||
|
</PermissionButton>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isError && (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="API 미응답"
|
||||||
|
description="/api/withholding-tax-reports 를 확인하세요."
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SearchForm
|
||||||
|
conditions={[
|
||||||
|
{ type: 'text', name: 'reportYear', label: '신고연도', span: 6 },
|
||||||
|
{ type: 'text', name: 'reportQuarter', label: '분기(1~4)', span: 6 },
|
||||||
|
{ type: 'text', name: 'agentName', label: '설계사명', span: 6 },
|
||||||
|
]}
|
||||||
|
onSearch={(v) => setParams({ ...v as WithholdingTaxSearchParam, pageSize: 500 })}
|
||||||
|
onReset={() => setParams({ reportYear: currentYear, reportQuarter: currentQuarter, pageSize: 500 })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProCard style={cardStyle}>
|
||||||
|
<DataGrid<WithholdingTaxReportResp>
|
||||||
|
rows={rows}
|
||||||
|
columns={COLUMNS}
|
||||||
|
loading={isLoading}
|
||||||
|
height={620}
|
||||||
|
rowKey="reportId"
|
||||||
|
rowSelection="multiple"
|
||||||
|
onSelectionChanged={handleSelectionChanged}
|
||||||
|
pinnedBottomRow={pinnedRow}
|
||||||
|
/>
|
||||||
|
</ProCard>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user