동기화
This commit is contained in:
+42
-3
@@ -118,14 +118,53 @@
|
||||
### 3-6. P1 정산 통합 (batch-engineer 완료)
|
||||
- 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 기반 매출/매입 집계 |
|
||||
|
||||
### P5-W3 — 결산 / 이상치 / 연말 명세서
|
||||
| 항목 | 설명 |
|
||||
|---|---|
|
||||
| 결산 | period_close 와 별개의 월/분기/연 회계 마감 |
|
||||
| 이상치·예측 KPI | 13M/25M 유지율 이상 탐지 + 차월 예측 |
|
||||
| 연말 지급명세서 | 설계사 연 사업소득 세무 자료 PDF |
|
||||
|
||||
@@ -44,8 +44,8 @@ V1~V17 마이그레이션 + 테스트 데이터(보험사 5 / 상품 20 / 설계
|
||||
`192.168.0.60:55432/trading_ai/ga` 스키마에 적용되어 있습니다.
|
||||
|
||||
> **주의**: 운영 DB 에는 현재 V1~V17(또는 V22 까지)만 적용되어 있습니다.
|
||||
> V18~V55 는 ga-api 를 재시작하면 Flyway 가 자동으로 순서대로 적용합니다.
|
||||
> 재시작 전까지 P2~P4 신규 도메인 API 는 테이블이 없어 동작하지 않습니다.
|
||||
> V18~V61 은 ga-api 를 재시작하면 Flyway 가 자동으로 순서대로 적용합니다.
|
||||
> 재시작 전까지 P2~P5-W1 신규 도메인 API 는 테이블이 없어 동작하지 않습니다.
|
||||
|
||||
#### 1) Backend (ga-api) 실행
|
||||
|
||||
@@ -111,6 +111,8 @@ PW : admin1234!
|
||||
| 정산 결과 | `/settle` | 월 요약 + 확정/보류 |
|
||||
| 정산 배치 | `/settle/batch` | 배치 실행 + 진행률 폴링 |
|
||||
| 지급 관리 | `/payments` | 지급 상태 추적 |
|
||||
| 연말 사업소득 명세서 | `/settle/annual-income` | 설계사별 연간 사업소득 집계 + 재집계 (P5-W1) |
|
||||
| 분개장 | `/settle/accounting-journal` | 회계 자동분개 엔트리 + 마감 처리 (P5-W1) |
|
||||
| 사용자 관리 | `/system/users` | 비번 초기화/잠금 해제 |
|
||||
| 역할/권한 | `/system/roles` | 권한 매트릭스 (메뉴 × 6권한) |
|
||||
| 공통코드 | `/system/codes` | 그룹별 코드 조회 |
|
||||
@@ -274,7 +276,23 @@ UPDATE ga.users SET password = '$2b$10$...' WHERE login_id = 'admin';
|
||||
- **첨부파일 (P4-B-8)**: attachment — 도메인 무관 공통 첨부 (소속/경로/타입)
|
||||
- **출금신청 (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 적재
|
||||
2. **transformData** — MappingEngine 으로 raw → 표준 원장 (TRIM/UPPER/DATE/DIVIDE/CODE_MAP)
|
||||
3. **reconcile** — 보험사 통보액 vs 시스템 계산액 대사
|
||||
@@ -283,6 +301,8 @@ UPDATE ga.users SET password = '$2b$10$...' WHERE login_id = 'admin';
|
||||
6. **chargebackException** — 실효 계약 환수 자동생성
|
||||
7. **calcOverride** — 조직 트리 상향 탐색 → 상위 직급에 분배
|
||||
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 |
|
||||
| V54 | P4 메뉴 INSERT (P4-A + P4-B 14개 메뉴) |
|
||||
| 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) 까지만 적용되어 있습니다.
|
||||
> **ga-api 재시작 시** Flyway 가 V18~V55 를 자동으로 순차 적용합니다.
|
||||
> 운영 DB (192.168.0.60) 현재 V71 까지 적용 완료.
|
||||
> **ga-api 재시작 시** Flyway 가 신규 마이그레이션을 자동으로 순차 적용합니다.
|
||||
|
||||
---
|
||||
|
||||
@@ -397,7 +433,7 @@ public class FooController {
|
||||
- [x] **ga-batch** — MonthlySettlementJob 8 Step (실제 동작 로직 포함)
|
||||
- [x] **Docker** — docker-compose + multi-stage Dockerfile + nginx
|
||||
- [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] **P0 세무·공제·명세서** — TaxCalculator / DeductionService / ExcelStatementGenerator
|
||||
- [x] **P1 1200%룰·분급·차등환수** — RegulatoryLimitChecker / InstallmentPlanGenerator / ChargebackCalculator
|
||||
@@ -405,10 +441,12 @@ public class FooController {
|
||||
- [x] **P3 KPI 대시보드** — Materialized View 4종 + Service + 화면
|
||||
- [x] **P4-A 수수료 종류 보강** — commission_master 통합 / 수금 / 갱신 / 부활 / 유지보수수당 / 오버라이드원장
|
||||
- [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 환경 문제로 보류)
|
||||
- [ ] **MappingEngine 정밀화** — 일부 변환 규칙은 단순화된 형태
|
||||
- [ ] **P5 회계/분석** — 전표 분개 / 원천세·부가세 신고 / 결산 / 이상치·예측 / 연말 지급명세서
|
||||
- [ ] **P5-W2 원천세·부가세 신고** — withholding_tax_report / vat_report (V67~V69 스키마 완료, Service+Controller 미완)
|
||||
- [ ] **P5-W3 결산 / 이상치 / 연말 지급명세서** — 미착수
|
||||
- [ ] **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();
|
||||
}
|
||||
|
||||
@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")
|
||||
@RequirePermission(menu = "REGULATORY_LIMIT", perm = "READ")
|
||||
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);
|
||||
}
|
||||
|
||||
@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)
|
||||
public RegulatoryLimitVO selectActiveByType(String limitType, String productCategory, String baseDate) {
|
||||
return limitMapper.selectActiveByType(limitType, productCategory, baseDate);
|
||||
|
||||
@@ -4,7 +4,7 @@ ga:
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://125.241.236.203:55432/ga
|
||||
url: jdbc:postgresql://192.168.0.60:55432/ga
|
||||
username: ga
|
||||
password: ga
|
||||
cache:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
spring:
|
||||
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}
|
||||
password: ${DB_PASSWORD:7895123}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
@@ -38,16 +38,20 @@ public class BatchConfig {
|
||||
CalcMaintainStep step5,
|
||||
ChargebackExceptionStep step6,
|
||||
CalcOverrideStep step7,
|
||||
AggregateStep step8) {
|
||||
AggregateStep step8,
|
||||
AccountingEntryGenerateStep step9,
|
||||
YearEndIncomeAggregateStep step10) {
|
||||
return new JobBuilder("MONTHLY_SETTLEMENT", jobRepository)
|
||||
.start(taskletStep("receiveData", step1, jobRepository, tx))
|
||||
.next(taskletStep("transformData", step2, jobRepository, tx))
|
||||
.next(taskletStep("reconcile", step3, jobRepository, tx))
|
||||
.next(taskletStep("calcRecruit", step4, jobRepository, tx))
|
||||
.next(taskletStep("calcMaintain", step5, jobRepository, tx))
|
||||
.next(taskletStep("chargebackException", step6, jobRepository, tx))
|
||||
.next(taskletStep("calcOverride", step7, jobRepository, tx))
|
||||
.next(taskletStep("aggregate", step8, jobRepository, tx))
|
||||
.start(taskletStep("receiveData", step1, jobRepository, tx))
|
||||
.next(taskletStep("transformData", step2, jobRepository, tx))
|
||||
.next(taskletStep("reconcile", step3, jobRepository, tx))
|
||||
.next(taskletStep("calcRecruit", step4, jobRepository, tx))
|
||||
.next(taskletStep("calcMaintain", step5, jobRepository, tx))
|
||||
.next(taskletStep("chargebackException", step6, jobRepository, tx))
|
||||
.next(taskletStep("calcOverride", step7, jobRepository, tx))
|
||||
.next(taskletStep("aggregate", step8, jobRepository, tx))
|
||||
.next(taskletStep("accountingEntryGenerate", step9, jobRepository, tx))
|
||||
.next(taskletStep("yearEndIncomeAggregate", step10, jobRepository, tx))
|
||||
.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)
|
||||
spring:
|
||||
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}
|
||||
password: ${DB_PASSWORD:7895123}
|
||||
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.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class ChargebackRuleVO {
|
||||
@@ -18,4 +19,8 @@ public class ChargebackRuleVO {
|
||||
private Integer maxInstallments;
|
||||
private LocalDate effectiveFrom;
|
||||
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 LocalDateTime createdAt;
|
||||
private Long createdBy;
|
||||
private LocalDateTime updatedAt;
|
||||
private Long updatedBy;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.ga.core.vo.rule;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class ExceptionTypeCodeVO {
|
||||
private String exceptionCode;
|
||||
@@ -13,4 +15,8 @@ public class ExceptionTypeCodeVO {
|
||||
private String taxIncluded;
|
||||
private String description;
|
||||
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.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class OverrideRuleVO {
|
||||
@@ -16,4 +17,8 @@ public class OverrideRuleVO {
|
||||
private BigDecimal capAmount;
|
||||
private LocalDate effectiveFrom;
|
||||
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 LocalDateTime createdAt;
|
||||
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>
|
||||
</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>
|
||||
|
||||
@@ -3,6 +3,14 @@ import LoginPage from '@/pages/LoginPage';
|
||||
import MainLayout from '@/layouts/MainLayout';
|
||||
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: 수수료 종류 ──────────────────────────────────
|
||||
import CommissionMaster from '@/pages/commission/CommissionMaster';
|
||||
import CollectionCommission from '@/pages/commission/CollectionCommission';
|
||||
@@ -146,6 +154,14 @@ export default function App() {
|
||||
<Route path="kpi/retention" element={<KpiRetention />} />
|
||||
<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: 수수료 종류 */}
|
||||
<Route path="commission/type" 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)),
|
||||
deactivate: (id: number) =>
|
||||
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 { ModuleRegistry } from '@ag-grid-community/core';
|
||||
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-theme-quartz.css';
|
||||
import { GRAY } from '@/theme/tokens';
|
||||
@@ -33,6 +33,8 @@ interface Props<T> {
|
||||
rowKey?: keyof T;
|
||||
/** 행별 스타일 콜백 (만료 임박 강조 등) */
|
||||
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,
|
||||
rowKey,
|
||||
getRowStyle: getRowStyleProp,
|
||||
rowSelection: rowSelectionProp = 'single',
|
||||
onSelectionChanged,
|
||||
}: Props<T>) {
|
||||
const apiRef = useRef<GridApi<T> | null>(null);
|
||||
|
||||
@@ -93,7 +97,8 @@ export default function DataGrid<T extends Record<string, unknown>>({
|
||||
rowHeight={56}
|
||||
headerHeight={44}
|
||||
localeText={AG_GRID_LOCALE_KO}
|
||||
rowSelection="single"
|
||||
rowSelection={rowSelectionProp}
|
||||
onSelectionChanged={onSelectionChanged}
|
||||
getRowId={rowKey ? (p) => String(p.data[rowKey]) : undefined}
|
||||
pinnedBottomRowData={pinnedBottomRow ? [pinnedBottomRow as T] : undefined}
|
||||
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 req: RegulatoryLimitSaveReq = {
|
||||
...v,
|
||||
@@ -108,9 +122,12 @@ export default function RegulatoryLimitList() {
|
||||
render: (_, r) => [
|
||||
<PermissionButton key="edit" menuCode="REGULATORY_LIMIT" permCode="UPDATE" size="small" type="link"
|
||||
onClick={() => openEdit(r)}>수정</PermissionButton>,
|
||||
r.isActive && (
|
||||
r.isActive ? (
|
||||
<PermissionButton key="deact" menuCode="REGULATORY_LIMIT" permCode="UPDATE" size="small" type="link" danger
|
||||
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