feat: 분급 비율 회차(월) 단위 등록 지원 (V81)
- 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+월" 통합 회차 표기
This commit is contained in:
@@ -34,11 +34,16 @@ public class InstallmentPlanGenerator {
|
|||||||
|
|
||||||
List<InstallmentPlanVO> plans = new ArrayList<>();
|
List<InstallmentPlanVO> plans = new ArrayList<>();
|
||||||
for (InstallmentRatioVO ratio : ratios) {
|
for (InstallmentRatioVO ratio : ratios) {
|
||||||
|
// 마스터의 plan_month 가 NULL 이면 연 단위(첫 달 지급), NOT NULL 이면 그 회차(월) 그대로 사용.
|
||||||
|
int planMonth = ratio.getPlanMonth() != null ? ratio.getPlanMonth() : 1;
|
||||||
|
|
||||||
InstallmentPlanVO plan = new InstallmentPlanVO();
|
InstallmentPlanVO plan = new InstallmentPlanVO();
|
||||||
plan.setContractId(contractId);
|
plan.setContractId(contractId);
|
||||||
plan.setPlanYear(ratio.getPlanYear());
|
plan.setPlanYear(ratio.getPlanYear());
|
||||||
plan.setPlanMonth(1); // 단순화: 연 1회 지급 (plan_month=1)
|
plan.setPlanMonth(planMonth);
|
||||||
plan.setSettleMonth(contractDate.plusYears(ratio.getPlanYear()).format(MONTH_FMT));
|
plan.setSettleMonth(
|
||||||
|
contractDate.plusYears(ratio.getPlanYear()).plusMonths(planMonth - 1L).format(MONTH_FMT)
|
||||||
|
);
|
||||||
plan.setExpectedAmount(
|
plan.setExpectedAmount(
|
||||||
totalCommission.multiply(ratio.getRatioPercent())
|
totalCommission.multiply(ratio.getRatioPercent())
|
||||||
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP)
|
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP)
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ public class InstallmentService {
|
|||||||
InstallmentRatioVO vo = new InstallmentRatioVO();
|
InstallmentRatioVO vo = new InstallmentRatioVO();
|
||||||
vo.setProductCategory(req.getProductCategory());
|
vo.setProductCategory(req.getProductCategory());
|
||||||
vo.setPlanYear(req.getPlanYear());
|
vo.setPlanYear(req.getPlanYear());
|
||||||
|
vo.setPlanMonth(req.getPlanMonth()); // null=연 단위, 1~12=회차(월) 단위
|
||||||
vo.setRatioPercent(req.getRatioPercent());
|
vo.setRatioPercent(req.getRatioPercent());
|
||||||
vo.setEffectiveFrom(req.getEffectiveFrom());
|
vo.setEffectiveFrom(req.getEffectiveFrom());
|
||||||
vo.setEffectiveTo(req.getEffectiveTo());
|
vo.setEffectiveTo(req.getEffectiveTo());
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -11,6 +11,7 @@ public class InstallmentRatioResp {
|
|||||||
private String productCategory;
|
private String productCategory;
|
||||||
private String productCategoryName;
|
private String productCategoryName;
|
||||||
private Integer planYear;
|
private Integer planYear;
|
||||||
|
private Integer planMonth;
|
||||||
private BigDecimal ratioPercent;
|
private BigDecimal ratioPercent;
|
||||||
private LocalDate effectiveFrom;
|
private LocalDate effectiveFrom;
|
||||||
private LocalDate effectiveTo;
|
private LocalDate effectiveTo;
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ public class InstallmentRatioSaveReq {
|
|||||||
@Max(value = 7, message = "분급 연차는 7 이하여야 합니다")
|
@Max(value = 7, message = "분급 연차는 7 이하여야 합니다")
|
||||||
private Integer planYear;
|
private Integer planYear;
|
||||||
|
|
||||||
|
// null = 연 단위(연차 합계 비율), 1~12 = 회차(월) 단위
|
||||||
|
@Min(value = 1, message = "회차(월)는 1 이상이어야 합니다")
|
||||||
|
@Max(value = 12, message = "회차(월)는 12 이하여야 합니다")
|
||||||
|
private Integer planMonth;
|
||||||
|
|
||||||
@NotNull(message = "비율은 필수입니다")
|
@NotNull(message = "비율은 필수입니다")
|
||||||
@Positive(message = "비율은 양수여야 합니다")
|
@Positive(message = "비율은 양수여야 합니다")
|
||||||
private BigDecimal ratioPercent;
|
private BigDecimal ratioPercent;
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ import lombok.EqualsAndHashCode;
|
|||||||
public class InstallmentRatioSearchParam extends SearchParam {
|
public class InstallmentRatioSearchParam extends SearchParam {
|
||||||
private String productCategory;
|
private String productCategory;
|
||||||
private Integer planYear;
|
private Integer planYear;
|
||||||
|
private Integer planMonth;
|
||||||
private String effectiveDate; // YYYY-MM-DD — 해당 날짜 유효 비율 필터
|
private String effectiveDate; // YYYY-MM-DD — 해당 날짜 유효 비율 필터
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public class InstallmentRatioVO {
|
|||||||
private Long ratioId;
|
private Long ratioId;
|
||||||
private String productCategory; // NULL = ALL 상품
|
private String productCategory; // NULL = ALL 상품
|
||||||
private Integer planYear; // 1~7
|
private Integer planYear; // 1~7
|
||||||
|
private Integer planMonth; // NULL = 연 단위(연차 합계), 1~12 = 회차(월) 단위
|
||||||
private BigDecimal ratioPercent;
|
private BigDecimal ratioPercent;
|
||||||
private LocalDate effectiveFrom;
|
private LocalDate effectiveFrom;
|
||||||
private LocalDate effectiveTo; // NULL = 현재 유효
|
private LocalDate effectiveTo; // NULL = 현재 유효
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<id property="ratioId" column="ratio_id"/>
|
<id property="ratioId" column="ratio_id"/>
|
||||||
<result property="productCategory" column="product_category"/>
|
<result property="productCategory" column="product_category"/>
|
||||||
<result property="planYear" column="plan_year"/>
|
<result property="planYear" column="plan_year"/>
|
||||||
|
<result property="planMonth" column="plan_month"/>
|
||||||
<result property="ratioPercent" column="ratio_percent"/>
|
<result property="ratioPercent" column="ratio_percent"/>
|
||||||
<result property="effectiveFrom" column="effective_from"/>
|
<result property="effectiveFrom" column="effective_from"/>
|
||||||
<result property="effectiveTo" column="effective_to"/>
|
<result property="effectiveTo" column="effective_to"/>
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
<result property="productCategory" column="product_category"/>
|
<result property="productCategory" column="product_category"/>
|
||||||
<result property="productCategoryName" column="product_category_name"/>
|
<result property="productCategoryName" column="product_category_name"/>
|
||||||
<result property="planYear" column="plan_year"/>
|
<result property="planYear" column="plan_year"/>
|
||||||
|
<result property="planMonth" column="plan_month"/>
|
||||||
<result property="ratioPercent" column="ratio_percent"/>
|
<result property="ratioPercent" column="ratio_percent"/>
|
||||||
<result property="effectiveFrom" column="effective_from"/>
|
<result property="effectiveFrom" column="effective_from"/>
|
||||||
<result property="effectiveTo" column="effective_to"/>
|
<result property="effectiveTo" column="effective_to"/>
|
||||||
@@ -40,6 +42,7 @@
|
|||||||
(SELECT cc.code_name FROM common_code cc
|
(SELECT cc.code_name FROM common_code cc
|
||||||
WHERE cc.group_code = 'PRODUCT_CATEGORY' AND cc.code = r.product_category) AS product_category_name,
|
WHERE cc.group_code = 'PRODUCT_CATEGORY' AND cc.code = r.product_category) AS product_category_name,
|
||||||
r.plan_year,
|
r.plan_year,
|
||||||
|
r.plan_month,
|
||||||
r.ratio_percent,
|
r.ratio_percent,
|
||||||
r.effective_from,
|
r.effective_from,
|
||||||
r.effective_to,
|
r.effective_to,
|
||||||
@@ -60,12 +63,15 @@
|
|||||||
<if test="planYear != null">
|
<if test="planYear != null">
|
||||||
AND r.plan_year = #{planYear}
|
AND r.plan_year = #{planYear}
|
||||||
</if>
|
</if>
|
||||||
|
<if test="planMonth != null">
|
||||||
|
AND r.plan_month = #{planMonth}
|
||||||
|
</if>
|
||||||
<if test="effectiveDate != null and effectiveDate != ''">
|
<if test="effectiveDate != null and effectiveDate != ''">
|
||||||
AND r.effective_from <= #{effectiveDate}::DATE
|
AND r.effective_from <= #{effectiveDate}::DATE
|
||||||
AND (r.effective_to IS NULL OR r.effective_to >= #{effectiveDate}::DATE)
|
AND (r.effective_to IS NULL OR r.effective_to >= #{effectiveDate}::DATE)
|
||||||
</if>
|
</if>
|
||||||
</where>
|
</where>
|
||||||
ORDER BY r.plan_year, r.effective_from DESC
|
ORDER BY r.plan_year, r.plan_month NULLS FIRST, r.effective_from DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 단건 조회 (Resp: 코드명 포함) -->
|
<!-- 단건 조회 (Resp: 코드명 포함) -->
|
||||||
@@ -77,7 +83,7 @@
|
|||||||
|
|
||||||
<!-- 단건 조회 (VO: CUD용) -->
|
<!-- 단건 조회 (VO: CUD용) -->
|
||||||
<select id="selectById" resultMap="VOMap">
|
<select id="selectById" resultMap="VOMap">
|
||||||
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,
|
effective_from, effective_to, is_active,
|
||||||
created_at, created_by, updated_at, updated_by
|
created_at, created_by, updated_at, updated_by
|
||||||
FROM installment_ratio
|
FROM installment_ratio
|
||||||
@@ -91,7 +97,7 @@
|
|||||||
effective_from DESC 정렬로 최신 개정본 우선 반환.
|
effective_from DESC 정렬로 최신 개정본 우선 반환.
|
||||||
-->
|
-->
|
||||||
<select id="selectActive" resultMap="VOMap">
|
<select id="selectActive" resultMap="VOMap">
|
||||||
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,
|
effective_from, effective_to, is_active,
|
||||||
created_at, created_by, updated_at, updated_by
|
created_at, created_by, updated_at, updated_by
|
||||||
FROM installment_ratio
|
FROM installment_ratio
|
||||||
@@ -107,7 +113,7 @@
|
|||||||
|
|
||||||
<!-- 1~7년 차 전체 비율 조회 (분급 계획 일괄 생성 시 사용) -->
|
<!-- 1~7년 차 전체 비율 조회 (분급 계획 일괄 생성 시 사용) -->
|
||||||
<select id="selectAllActiveRatios" resultMap="VOMap">
|
<select id="selectAllActiveRatios" resultMap="VOMap">
|
||||||
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,
|
effective_from, effective_to, is_active,
|
||||||
created_at, created_by, updated_at, updated_by
|
created_at, created_by, updated_at, updated_by
|
||||||
FROM installment_ratio
|
FROM installment_ratio
|
||||||
@@ -116,7 +122,7 @@
|
|||||||
AND is_active = TRUE
|
AND is_active = TRUE
|
||||||
AND effective_from <= #{baseDate}::DATE
|
AND effective_from <= #{baseDate}::DATE
|
||||||
AND (effective_to IS NULL OR effective_to >= #{baseDate}::DATE)
|
AND (effective_to IS NULL OR effective_to >= #{baseDate}::DATE)
|
||||||
ORDER BY plan_year
|
ORDER BY plan_year, plan_month NULLS FIRST
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- ==================== INSERT ==================== -->
|
<!-- ==================== INSERT ==================== -->
|
||||||
@@ -124,13 +130,13 @@
|
|||||||
<insert id="insert" parameterType="com.ga.core.vo.installment.InstallmentRatioVO"
|
<insert id="insert" parameterType="com.ga.core.vo.installment.InstallmentRatioVO"
|
||||||
useGeneratedKeys="true" keyProperty="ratioId">
|
useGeneratedKeys="true" keyProperty="ratioId">
|
||||||
INSERT INTO installment_ratio (
|
INSERT INTO installment_ratio (
|
||||||
product_category, plan_year, ratio_percent,
|
product_category, plan_year, plan_month, ratio_percent,
|
||||||
effective_from, effective_to, is_active,
|
effective_from, effective_to, is_active,
|
||||||
created_at, created_by
|
created_at, created_by
|
||||||
) VALUES (
|
) VALUES (
|
||||||
#{productCategory}, #{planYear}, #{ratioPercent},
|
#{productCategory,jdbcType=VARCHAR}, #{planYear}, #{planMonth,jdbcType=INTEGER}, #{ratioPercent},
|
||||||
#{effectiveFrom}, #{effectiveTo}, COALESCE(#{isActive}, TRUE),
|
#{effectiveFrom}, #{effectiveTo,jdbcType=DATE}, COALESCE(#{isActive}, TRUE),
|
||||||
NOW(), #{createdBy}
|
NOW(), #{createdBy,jdbcType=BIGINT}
|
||||||
)
|
)
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface InstallmentRatioResp {
|
|||||||
productCategory: string | null;
|
productCategory: string | null;
|
||||||
productCategoryName: string | null;
|
productCategoryName: string | null;
|
||||||
planYear: number;
|
planYear: number;
|
||||||
|
planMonth: number | null;
|
||||||
ratioPercent: number;
|
ratioPercent: number;
|
||||||
effectiveFrom: string;
|
effectiveFrom: string;
|
||||||
effectiveTo: string | null;
|
effectiveTo: string | null;
|
||||||
@@ -14,6 +15,7 @@ export interface InstallmentRatioResp {
|
|||||||
export interface InstallmentRatioSaveReq {
|
export interface InstallmentRatioSaveReq {
|
||||||
productCategory?: string | null;
|
productCategory?: string | null;
|
||||||
planYear: number;
|
planYear: number;
|
||||||
|
planMonth?: number | null;
|
||||||
ratioPercent: number;
|
ratioPercent: number;
|
||||||
effectiveFrom: string;
|
effectiveFrom: string;
|
||||||
effectiveTo?: string | null;
|
effectiveTo?: string | null;
|
||||||
@@ -21,6 +23,8 @@ export interface InstallmentRatioSaveReq {
|
|||||||
|
|
||||||
export interface InstallmentRatioSearchParam {
|
export interface InstallmentRatioSearchParam {
|
||||||
productCategory?: string;
|
productCategory?: string;
|
||||||
|
planYear?: number;
|
||||||
|
planMonth?: number;
|
||||||
pageNum?: number;
|
pageNum?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,25 @@ const PLAN_YEAR_OPTIONS = [1, 2, 3, 4, 5, 6, 7].map((y) => ({
|
|||||||
label: `${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 {
|
function planYearLabel(y: number): string {
|
||||||
return `${y}차년도`;
|
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() {
|
export default function InstallmentRatioList() {
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
const [searchParam, setSearchParam] = useState<InstallmentRatioSearchParam>({
|
const [searchParam, setSearchParam] = useState<InstallmentRatioSearchParam>({
|
||||||
@@ -57,6 +72,7 @@ export default function InstallmentRatioList() {
|
|||||||
const req: InstallmentRatioSaveReq = {
|
const req: InstallmentRatioSaveReq = {
|
||||||
productCategory: v.productCategory || null,
|
productCategory: v.productCategory || null,
|
||||||
planYear: v.planYear,
|
planYear: v.planYear,
|
||||||
|
planMonth: v.planMonth ?? null,
|
||||||
ratioPercent: v.ratioPercent,
|
ratioPercent: v.ratioPercent,
|
||||||
effectiveFrom: dayjs(v.effectiveFrom).format('YYYY-MM-DD'),
|
effectiveFrom: dayjs(v.effectiveFrom).format('YYYY-MM-DD'),
|
||||||
effectiveTo: v.effectiveTo ? dayjs(v.effectiveTo).format('YYYY-MM-DD') : null,
|
effectiveTo: v.effectiveTo ? dayjs(v.effectiveTo).format('YYYY-MM-DD') : null,
|
||||||
@@ -84,6 +100,14 @@ export default function InstallmentRatioList() {
|
|||||||
search: false,
|
search: false,
|
||||||
render: (_, r) => planYearLabel(r.planYear),
|
render: (_, r) => planYearLabel(r.planYear),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '회차(월)',
|
||||||
|
dataIndex: 'planMonth',
|
||||||
|
width: 130,
|
||||||
|
align: 'center',
|
||||||
|
search: false,
|
||||||
|
render: (_, r) => planMonthLabel(r.planYear, r.planMonth),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '비율(%)',
|
title: '비율(%)',
|
||||||
dataIndex: 'ratioPercent',
|
dataIndex: 'ratioPercent',
|
||||||
@@ -168,7 +192,7 @@ export default function InstallmentRatioList() {
|
|||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
title="분급비율 설정"
|
title="분급비율 설정"
|
||||||
description="분급 비율 합계가 100%가 되도록 1~7차년도 각 비율 설정"
|
description="1~7차년도 각 비율 설정. 회차(월) 미지정 = 연 단위(첫 달 지급), 1~12 지정 = 해당 월만 지급"
|
||||||
>
|
>
|
||||||
<ProTable<InstallmentRatioResp>
|
<ProTable<InstallmentRatioResp>
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
@@ -236,6 +260,17 @@ export default function InstallmentRatioList() {
|
|||||||
options={PLAN_YEAR_OPTIONS}
|
options={PLAN_YEAR_OPTIONS}
|
||||||
/>
|
/>
|
||||||
</ProForm.Group>
|
</ProForm.Group>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormSelect
|
||||||
|
name="planMonth"
|
||||||
|
label="회차(월)"
|
||||||
|
width="md"
|
||||||
|
placeholder="비워두면 연 단위"
|
||||||
|
tooltip="비우면 연차 합계, 1~12 선택 시 그 회차(월)만 적용"
|
||||||
|
options={PLAN_MONTH_OPTIONS}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
<ProForm.Group>
|
<ProForm.Group>
|
||||||
<ProFormDigit
|
<ProFormDigit
|
||||||
name="ratioPercent"
|
name="ratioPercent"
|
||||||
|
|||||||
Reference in New Issue
Block a user