From c005ad705aad3170e16f702281c04dba8071a34a Mon Sep 17 00:00:00 2001 From: GA Pro Date: Sun, 24 May 2026 01:27:47 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B6=84=EA=B8=89=20=EB=B9=84=EC=9C=A8?= =?UTF-8?q?=20=ED=9A=8C=EC=B0=A8(=EC=9B=94)=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=A7=80=EC=9B=90=20(V81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - installment_ratio.plan_month INT NULL 컬럼 추가 - NULL = 연 단위(연차 합계, 기존 동작) - 1~12 = 해당 회차(월) 단위 비율 - UNIQUE 재정의: NULLS NOT DISTINCT (product_category, plan_year, plan_month, effective_from) - InstallmentPlanGenerator: planMonth=NULL이면 1, NOT NULL이면 그대로 사용해 settle_month 계산 - 분급비율 화면: 회차(월) 컬럼·입력 추가, "(연차-1)*12+월" 통합 회차 표기 --- .../installment/InstallmentPlanGenerator.java | 9 ++++- .../installment/InstallmentService.java | 1 + .../migration/V81__분급비율_회차단위_지원.sql | 26 +++++++++++++ .../vo/installment/InstallmentRatioResp.java | 1 + .../installment/InstallmentRatioSaveReq.java | 5 +++ .../InstallmentRatioSearchParam.java | 1 + .../vo/installment/InstallmentRatioVO.java | 1 + .../installment/InstallmentRatioMapper.xml | 24 +++++++----- ga-frontend/src/api/installment.ts | 4 ++ .../src/pages/rule/InstallmentRatioList.tsx | 37 ++++++++++++++++++- 10 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 ga-common/src/main/resources/db/migration/V81__분급비율_회차단위_지원.sql diff --git a/ga-api/src/main/java/com/ga/api/service/installment/InstallmentPlanGenerator.java b/ga-api/src/main/java/com/ga/api/service/installment/InstallmentPlanGenerator.java index 287a389..5ef75f4 100644 --- a/ga-api/src/main/java/com/ga/api/service/installment/InstallmentPlanGenerator.java +++ b/ga-api/src/main/java/com/ga/api/service/installment/InstallmentPlanGenerator.java @@ -34,11 +34,16 @@ public class InstallmentPlanGenerator { List plans = new ArrayList<>(); for (InstallmentRatioVO ratio : ratios) { + // 마스터의 plan_month 가 NULL 이면 연 단위(첫 달 지급), NOT NULL 이면 그 회차(월) 그대로 사용. + int planMonth = ratio.getPlanMonth() != null ? ratio.getPlanMonth() : 1; + InstallmentPlanVO plan = new InstallmentPlanVO(); plan.setContractId(contractId); plan.setPlanYear(ratio.getPlanYear()); - plan.setPlanMonth(1); // 단순화: 연 1회 지급 (plan_month=1) - plan.setSettleMonth(contractDate.plusYears(ratio.getPlanYear()).format(MONTH_FMT)); + plan.setPlanMonth(planMonth); + plan.setSettleMonth( + contractDate.plusYears(ratio.getPlanYear()).plusMonths(planMonth - 1L).format(MONTH_FMT) + ); plan.setExpectedAmount( totalCommission.multiply(ratio.getRatioPercent()) .divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP) diff --git a/ga-api/src/main/java/com/ga/api/service/installment/InstallmentService.java b/ga-api/src/main/java/com/ga/api/service/installment/InstallmentService.java index 1f7d2d7..07cd9b5 100644 --- a/ga-api/src/main/java/com/ga/api/service/installment/InstallmentService.java +++ b/ga-api/src/main/java/com/ga/api/service/installment/InstallmentService.java @@ -121,6 +121,7 @@ public class InstallmentService { InstallmentRatioVO vo = new InstallmentRatioVO(); vo.setProductCategory(req.getProductCategory()); vo.setPlanYear(req.getPlanYear()); + vo.setPlanMonth(req.getPlanMonth()); // null=연 단위, 1~12=회차(월) 단위 vo.setRatioPercent(req.getRatioPercent()); vo.setEffectiveFrom(req.getEffectiveFrom()); vo.setEffectiveTo(req.getEffectiveTo()); diff --git a/ga-common/src/main/resources/db/migration/V81__분급비율_회차단위_지원.sql b/ga-common/src/main/resources/db/migration/V81__분급비율_회차단위_지원.sql new file mode 100644 index 0000000..2e49f1b --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V81__분급비율_회차단위_지원.sql @@ -0,0 +1,26 @@ +-- V81: installment_ratio 에 plan_month(회차/월) 컬럼 추가. +-- NULL = 연 단위 (기존 동작, 1차년도 등 합계 비율) +-- 1~12 = 해당 회차(월) 단위 비율 +-- +-- 기존 7건(NULL) 은 그대로 두고 새 UNIQUE 키만 재정의한다. + +ALTER TABLE installment_ratio + ADD COLUMN IF NOT EXISTS plan_month INT + CHECK (plan_month IS NULL OR (plan_month BETWEEN 1 AND 12)); + +COMMENT ON COLUMN installment_ratio.plan_month IS + 'NULL=연 단위(연차 합계), 1~12=해당 회차(월) 단위 비율'; + +-- 기존 UNIQUE (product_category, plan_year, effective_from) 제거 후 +-- plan_month 포함 UNIQUE 재정의. NULL 도 동일하게 취급되어야 하므로 NULLS NOT DISTINCT 사용 (PG15+). +ALTER TABLE installment_ratio + DROP CONSTRAINT IF EXISTS installment_ratio_product_category_plan_year_effective_fr_key; + +ALTER TABLE installment_ratio + ADD CONSTRAINT installment_ratio_unique + UNIQUE NULLS NOT DISTINCT (product_category, plan_year, plan_month, effective_from); + +-- 조회 인덱스 보강 — 특정 카테고리·연차·회차의 활성 비율 빠른 조회 +DROP INDEX IF EXISTS idx_ir_category_year; +CREATE INDEX IF NOT EXISTS idx_ir_category_year_month + ON installment_ratio(product_category, plan_year, plan_month, effective_from DESC); diff --git a/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioResp.java b/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioResp.java index df00ed8..c7d521d 100644 --- a/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioResp.java +++ b/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioResp.java @@ -11,6 +11,7 @@ public class InstallmentRatioResp { private String productCategory; private String productCategoryName; private Integer planYear; + private Integer planMonth; private BigDecimal ratioPercent; private LocalDate effectiveFrom; private LocalDate effectiveTo; diff --git a/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioSaveReq.java index 88eb5f0..fdf89fd 100644 --- a/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioSaveReq.java +++ b/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioSaveReq.java @@ -19,6 +19,11 @@ public class InstallmentRatioSaveReq { @Max(value = 7, message = "분급 연차는 7 이하여야 합니다") private Integer planYear; + // null = 연 단위(연차 합계 비율), 1~12 = 회차(월) 단위 + @Min(value = 1, message = "회차(월)는 1 이상이어야 합니다") + @Max(value = 12, message = "회차(월)는 12 이하여야 합니다") + private Integer planMonth; + @NotNull(message = "비율은 필수입니다") @Positive(message = "비율은 양수여야 합니다") private BigDecimal ratioPercent; diff --git a/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioSearchParam.java index 06e4143..18024f8 100644 --- a/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioSearchParam.java +++ b/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioSearchParam.java @@ -9,5 +9,6 @@ import lombok.EqualsAndHashCode; public class InstallmentRatioSearchParam extends SearchParam { private String productCategory; private Integer planYear; + private Integer planMonth; private String effectiveDate; // YYYY-MM-DD — 해당 날짜 유효 비율 필터 } diff --git a/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioVO.java b/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioVO.java index 6e3a678..ed4cda6 100644 --- a/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioVO.java +++ b/ga-core/src/main/java/com/ga/core/vo/installment/InstallmentRatioVO.java @@ -11,6 +11,7 @@ public class InstallmentRatioVO { private Long ratioId; private String productCategory; // NULL = ALL 상품 private Integer planYear; // 1~7 + private Integer planMonth; // NULL = 연 단위(연차 합계), 1~12 = 회차(월) 단위 private BigDecimal ratioPercent; private LocalDate effectiveFrom; private LocalDate effectiveTo; // NULL = 현재 유효 diff --git a/ga-core/src/main/resources/mapper/installment/InstallmentRatioMapper.xml b/ga-core/src/main/resources/mapper/installment/InstallmentRatioMapper.xml index 964ae50..55e6086 100644 --- a/ga-core/src/main/resources/mapper/installment/InstallmentRatioMapper.xml +++ b/ga-core/src/main/resources/mapper/installment/InstallmentRatioMapper.xml @@ -10,6 +10,7 @@ + @@ -26,6 +27,7 @@ + @@ -40,6 +42,7 @@ (SELECT cc.code_name FROM common_code cc WHERE cc.group_code = 'PRODUCT_CATEGORY' AND cc.code = r.product_category) AS product_category_name, r.plan_year, + r.plan_month, r.ratio_percent, r.effective_from, r.effective_to, @@ -60,12 +63,15 @@ AND r.plan_year = #{planYear} + + AND r.plan_month = #{planMonth} + AND r.effective_from <= #{effectiveDate}::DATE AND (r.effective_to IS NULL OR r.effective_to >= #{effectiveDate}::DATE) - ORDER BY r.plan_year, r.effective_from DESC + ORDER BY r.plan_year, r.plan_month NULLS FIRST, r.effective_from DESC @@ -77,7 +83,7 @@ - SELECT ratio_id, product_category, plan_year, ratio_percent, + SELECT ratio_id, product_category, plan_year, plan_month, ratio_percent, effective_from, effective_to, is_active, created_at, created_by, updated_at, updated_by FROM installment_ratio @@ -107,7 +113,7 @@ @@ -124,13 +130,13 @@ INSERT INTO installment_ratio ( - product_category, plan_year, ratio_percent, + product_category, plan_year, plan_month, ratio_percent, effective_from, effective_to, is_active, created_at, created_by ) VALUES ( - #{productCategory}, #{planYear}, #{ratioPercent}, - #{effectiveFrom}, #{effectiveTo}, COALESCE(#{isActive}, TRUE), - NOW(), #{createdBy} + #{productCategory,jdbcType=VARCHAR}, #{planYear}, #{planMonth,jdbcType=INTEGER}, #{ratioPercent}, + #{effectiveFrom}, #{effectiveTo,jdbcType=DATE}, COALESCE(#{isActive}, TRUE), + NOW(), #{createdBy,jdbcType=BIGINT} ) diff --git a/ga-frontend/src/api/installment.ts b/ga-frontend/src/api/installment.ts index 9504fde..6404b83 100644 --- a/ga-frontend/src/api/installment.ts +++ b/ga-frontend/src/api/installment.ts @@ -5,6 +5,7 @@ export interface InstallmentRatioResp { productCategory: string | null; productCategoryName: string | null; planYear: number; + planMonth: number | null; ratioPercent: number; effectiveFrom: string; effectiveTo: string | null; @@ -14,6 +15,7 @@ export interface InstallmentRatioResp { export interface InstallmentRatioSaveReq { productCategory?: string | null; planYear: number; + planMonth?: number | null; ratioPercent: number; effectiveFrom: string; effectiveTo?: string | null; @@ -21,6 +23,8 @@ export interface InstallmentRatioSaveReq { export interface InstallmentRatioSearchParam { productCategory?: string; + planYear?: number; + planMonth?: number; pageNum?: number; pageSize?: number; } diff --git a/ga-frontend/src/pages/rule/InstallmentRatioList.tsx b/ga-frontend/src/pages/rule/InstallmentRatioList.tsx index 1dd71a8..7747445 100644 --- a/ga-frontend/src/pages/rule/InstallmentRatioList.tsx +++ b/ga-frontend/src/pages/rule/InstallmentRatioList.tsx @@ -37,10 +37,25 @@ const PLAN_YEAR_OPTIONS = [1, 2, 3, 4, 5, 6, 7].map((y) => ({ label: `${y}차년도`, })); +const PLAN_MONTH_OPTIONS = [ + { value: null as number | null, label: '연 단위(연차 합계)' }, + ...Array.from({ length: 12 }, (_, i) => ({ + value: i + 1, + label: `${i + 1}회차(월)`, + })), +]; + function planYearLabel(y: number): string { return `${y}차년도`; } +/** 회차 표기: NULL=연 단위, 아니면 "(연차-1)*12 + 월" 통합 회차 + 월 */ +function planMonthLabel(planYear: number, planMonth: number | null): string { + if (planMonth == null) return '연 단위'; + const seq = (planYear - 1) * 12 + planMonth; + return `${seq}회 (${planMonth}월)`; +} + export default function InstallmentRatioList() { const actionRef = useRef(); const [searchParam, setSearchParam] = useState({ @@ -57,6 +72,7 @@ export default function InstallmentRatioList() { const req: InstallmentRatioSaveReq = { productCategory: v.productCategory || null, planYear: v.planYear, + planMonth: v.planMonth ?? null, ratioPercent: v.ratioPercent, effectiveFrom: dayjs(v.effectiveFrom).format('YYYY-MM-DD'), effectiveTo: v.effectiveTo ? dayjs(v.effectiveTo).format('YYYY-MM-DD') : null, @@ -84,6 +100,14 @@ export default function InstallmentRatioList() { search: false, render: (_, r) => planYearLabel(r.planYear), }, + { + title: '회차(월)', + dataIndex: 'planMonth', + width: 130, + align: 'center', + search: false, + render: (_, r) => planMonthLabel(r.planYear, r.planMonth), + }, { title: '비율(%)', dataIndex: 'ratioPercent', @@ -168,7 +192,7 @@ export default function InstallmentRatioList() { return ( actionRef={actionRef} @@ -236,6 +260,17 @@ export default function InstallmentRatioList() { options={PLAN_YEAR_OPTIONS} /> + + +