feat: 계약 승계(contract succession) — 유지수수료 귀속 후임 전환 (V86)
설계사 해촉/이관 시 보험계약의 유지수수료 귀속을 승계월 기준 후임 설계사로 전환. 모집수수료는 원 모집설계사 귀속 유지(고아계약 승계 표준). - DB V86: contract_succession 테이블 + 인덱스 + 메뉴/권한(GRP_PRD) + 공통코드 2그룹 + 테스트데이터 1건 - core: ContractSuccession VO/Resp/SaveReq/SearchParam + Mapper(.java/.xml) selectEffectiveSuccessions(settleMonth): 계약별 최근 ACTIVE 승계 1건 (DISTINCT ON) - api: ContractSuccessionService/Controller (/api/contract-successions, CRUD+cancel) - batch: CalcMaintainStep 이 effective 승계 사전로드(N+1 회피) 후 effectiveAgentId 결정, CommissionCalculator.calcMaintainLedger 오버로드로 후임 등급 payout + ledger agent_id 후임 기록. AggregateStep 은 ledger agent_id 집계라 무변경. - frontend: api/succession.ts + ContractSuccessions.tsx + App.tsx 라우트 검증: 5모듈 compileJava SUCCESS / ga-frontend tsc 0 / Flyway v86 적용 / API CRUD+cancel+from==to 가드(E411) 라이브 PASS / resolver 쿼리 라이브 검증(계약1→후임 agent2). 배치 풀잡 e2e 는 정산월 원장 재생성(파괴적)이라 미실행. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package com.ga.api.controller.succession;
|
||||||
|
|
||||||
|
import com.ga.api.service.succession.ContractSuccessionService;
|
||||||
|
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.succession.ContractSuccessionResp;
|
||||||
|
import com.ga.core.vo.succession.ContractSuccessionSaveReq;
|
||||||
|
import com.ga.core.vo.succession.ContractSuccessionSearchParam;
|
||||||
|
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/contract-successions")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ContractSuccessionController {
|
||||||
|
|
||||||
|
private final ContractSuccessionService service;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<ContractSuccessionResp>> list(
|
||||||
|
@ModelAttribute ContractSuccessionSearchParam param) {
|
||||||
|
return ApiResponse.ok(service.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{successionId}")
|
||||||
|
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "READ")
|
||||||
|
public ApiResponse<ContractSuccessionResp> detail(@PathVariable Long successionId) {
|
||||||
|
return ApiResponse.ok(service.detail(successionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "CONTRACT_SUCCESSION", table = "contract_succession")
|
||||||
|
public ApiResponse<Long> create(@Valid @RequestBody ContractSuccessionSaveReq req) {
|
||||||
|
return ApiResponse.ok(service.create(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{successionId}/cancel")
|
||||||
|
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "CONTRACT_SUCCESSION", table = "contract_succession")
|
||||||
|
public ApiResponse<Void> cancel(@PathVariable Long successionId) {
|
||||||
|
service.cancel(successionId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{successionId}")
|
||||||
|
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "DELETE")
|
||||||
|
@DataChangeLog(menu = "CONTRACT_SUCCESSION", table = "contract_succession")
|
||||||
|
public ApiResponse<Void> delete(@PathVariable Long successionId) {
|
||||||
|
service.delete(successionId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.ga.api.service.succession;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.common.util.SecurityUtil;
|
||||||
|
import com.ga.core.mapper.succession.ContractSuccessionMapper;
|
||||||
|
import com.ga.core.vo.succession.ContractSuccessionResp;
|
||||||
|
import com.ga.core.vo.succession.ContractSuccessionSaveReq;
|
||||||
|
import com.ga.core.vo.succession.ContractSuccessionSearchParam;
|
||||||
|
import com.ga.core.vo.succession.ContractSuccessionVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class ContractSuccessionService {
|
||||||
|
|
||||||
|
private final ContractSuccessionMapper mapper;
|
||||||
|
|
||||||
|
public PageResponse<ContractSuccessionResp> list(ContractSuccessionSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(mapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContractSuccessionResp detail(Long successionId) {
|
||||||
|
ContractSuccessionResp resp = mapper.selectById(successionId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long create(ContractSuccessionSaveReq req) {
|
||||||
|
if (req.getFromAgentId().equals(req.getToAgentId())) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER, "전임과 후임 설계사가 같을 수 없습니다");
|
||||||
|
}
|
||||||
|
ContractSuccessionVO vo = new ContractSuccessionVO();
|
||||||
|
vo.setContractId(req.getContractId());
|
||||||
|
vo.setFromAgentId(req.getFromAgentId());
|
||||||
|
vo.setToAgentId(req.getToAgentId());
|
||||||
|
vo.setSuccessionDate(req.getSuccessionDate());
|
||||||
|
vo.setReason(req.getReason());
|
||||||
|
vo.setMemo(req.getMemo());
|
||||||
|
vo.setCreatedBy(SecurityUtil.getCurrentUserId());
|
||||||
|
mapper.insert(vo);
|
||||||
|
return vo.getSuccessionId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 승계 취소 — status=CANCELED. 이후 정산월부터 유지수수료는 다시 직전 귀속 설계사에게. */
|
||||||
|
@Transactional
|
||||||
|
public void cancel(Long successionId) {
|
||||||
|
ContractSuccessionResp existing = mapper.selectById(successionId);
|
||||||
|
if (existing == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
mapper.cancel(successionId, SecurityUtil.getCurrentUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(Long successionId) {
|
||||||
|
ContractSuccessionResp existing = mapper.selectById(successionId);
|
||||||
|
if (existing == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
mapper.delete(successionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,15 +26,25 @@ public class CommissionCalculator {
|
|||||||
private final AgentMapper agentMapper;
|
private final AgentMapper agentMapper;
|
||||||
|
|
||||||
public LedgerVO calcRecruitLedger(ContractResp c, String settleMonth) {
|
public LedgerVO calcRecruitLedger(ContractResp c, String settleMonth) {
|
||||||
return buildLedger(c, settleMonth, 1);
|
return buildLedger(c, settleMonth, 1, c.getAgentId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public LedgerVO calcMaintainLedger(ContractResp c, String settleMonth, int commissionYear) {
|
public LedgerVO calcMaintainLedger(ContractResp c, String settleMonth, int commissionYear) {
|
||||||
return buildLedger(c, settleMonth, commissionYear);
|
return buildLedger(c, settleMonth, commissionYear, c.getAgentId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private LedgerVO buildLedger(ContractResp c, String settleMonth, int year) {
|
/**
|
||||||
AgentVO agent = agentMapper.selectById(c.getAgentId());
|
* 계약 승계 반영 유지수수료 계산.
|
||||||
|
* effectiveAgentId 가 후임 설계사이면 해당 설계사 등급의 payout 비율로 계산되고
|
||||||
|
* 원장 agent_id 도 후임으로 기록된다. null 이면 원 모집설계사로 동작.
|
||||||
|
*/
|
||||||
|
public LedgerVO calcMaintainLedger(ContractResp c, String settleMonth, int commissionYear, Long effectiveAgentId) {
|
||||||
|
return buildLedger(c, settleMonth, commissionYear,
|
||||||
|
effectiveAgentId != null ? effectiveAgentId : c.getAgentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private LedgerVO buildLedger(ContractResp c, String settleMonth, int year, Long agentId) {
|
||||||
|
AgentVO agent = agentMapper.selectById(agentId);
|
||||||
if (agent == null) return null;
|
if (agent == null) return null;
|
||||||
LocalDate baseDate = c.getContractDate() != null ? c.getContractDate() : LocalDate.now();
|
LocalDate baseDate = c.getContractDate() != null ? c.getContractDate() : LocalDate.now();
|
||||||
|
|
||||||
@@ -52,7 +62,7 @@ public class CommissionCalculator {
|
|||||||
|
|
||||||
LedgerVO led = new LedgerVO();
|
LedgerVO led = new LedgerVO();
|
||||||
led.setContractId(c.getContractId());
|
led.setContractId(c.getContractId());
|
||||||
led.setAgentId(c.getAgentId());
|
led.setAgentId(agentId);
|
||||||
led.setPolicyNo(c.getPolicyNo());
|
led.setPolicyNo(c.getPolicyNo());
|
||||||
led.setCompanyCode(c.getCompanyCode());
|
led.setCompanyCode(c.getCompanyCode());
|
||||||
led.setProductCode(c.getProductCode());
|
led.setProductCode(c.getProductCode());
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import com.ga.common.util.DateUtil;
|
|||||||
import com.ga.common.util.MoneyUtil;
|
import com.ga.common.util.MoneyUtil;
|
||||||
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
|
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
|
||||||
import com.ga.core.mapper.product.ContractMapper;
|
import com.ga.core.mapper.product.ContractMapper;
|
||||||
|
import com.ga.core.mapper.succession.ContractSuccessionMapper;
|
||||||
import com.ga.core.vo.ledger.LedgerVO;
|
import com.ga.core.vo.ledger.LedgerVO;
|
||||||
import com.ga.core.vo.product.ContractResp;
|
import com.ga.core.vo.product.ContractResp;
|
||||||
import com.ga.core.vo.product.ContractSearchParam;
|
import com.ga.core.vo.product.ContractSearchParam;
|
||||||
|
import com.ga.core.vo.succession.ContractSuccessionVO;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.batch.core.StepContribution;
|
import org.springframework.batch.core.StepContribution;
|
||||||
@@ -20,7 +22,9 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 5 — calcMaintain: 유지수수료 계산 (2회차~)
|
* Step 5 — calcMaintain: 유지수수료 계산 (2회차~)
|
||||||
@@ -37,6 +41,7 @@ public class CalcMaintainStep implements Tasklet {
|
|||||||
private final SettlementContext ctx;
|
private final SettlementContext ctx;
|
||||||
private final ContractMapper contractMapper;
|
private final ContractMapper contractMapper;
|
||||||
private final MaintainLedgerMapper maintainMapper;
|
private final MaintainLedgerMapper maintainMapper;
|
||||||
|
private final ContractSuccessionMapper successionMapper;
|
||||||
private final CommissionCalculator calculator;
|
private final CommissionCalculator calculator;
|
||||||
|
|
||||||
private static final int BATCH_SIZE = 1000;
|
private static final int BATCH_SIZE = 1000;
|
||||||
@@ -52,6 +57,15 @@ public class CalcMaintainStep implements Tasklet {
|
|||||||
|
|
||||||
maintainMapper.deleteBySettleMonth(ctx.getSettleMonth());
|
maintainMapper.deleteBySettleMonth(ctx.getSettleMonth());
|
||||||
|
|
||||||
|
// 계약 승계 반영: 정산월 기준 유효 승계(계약→후임설계사) 사전 로드 (N+1 회피)
|
||||||
|
Map<Long, Long> successorByContract = new HashMap<>();
|
||||||
|
for (ContractSuccessionVO s : successionMapper.selectEffectiveSuccessions(ctx.getSettleMonth())) {
|
||||||
|
successorByContract.put(s.getContractId(), s.getToAgentId());
|
||||||
|
}
|
||||||
|
if (!successorByContract.isEmpty()) {
|
||||||
|
log.info("[Step5] active successions applied: {}", successorByContract.size());
|
||||||
|
}
|
||||||
|
|
||||||
List<LedgerVO> batch = new ArrayList<>(BATCH_SIZE);
|
List<LedgerVO> batch = new ArrayList<>(BATCH_SIZE);
|
||||||
int total = 0;
|
int total = 0;
|
||||||
|
|
||||||
@@ -66,7 +80,9 @@ public class CalcMaintainStep implements Tasklet {
|
|||||||
// 납입주기 매칭 (월납이면 매월, 분기납이면 3개월마다 ...)
|
// 납입주기 매칭 (월납이면 매월, 분기납이면 3개월마다 ...)
|
||||||
if (!matchesPayCycle(c.getPayCycle(), monthsElapsed)) continue;
|
if (!matchesPayCycle(c.getPayCycle(), monthsElapsed)) continue;
|
||||||
|
|
||||||
LedgerVO led = calculator.calcMaintainLedger(c, ctx.getSettleMonth(), year);
|
// 승계된 계약이면 유지수수료 귀속을 후임 설계사로 (없으면 원 모집설계사)
|
||||||
|
Long effectiveAgentId = successorByContract.getOrDefault(c.getContractId(), c.getAgentId());
|
||||||
|
LedgerVO led = calculator.calcMaintainLedger(c, ctx.getSettleMonth(), year, effectiveAgentId);
|
||||||
if (led == null || MoneyUtil.isZero(led.getAgentAmount())) continue;
|
if (led == null || MoneyUtil.isZero(led.getAgentAmount())) continue;
|
||||||
batch.add(led);
|
batch.add(led);
|
||||||
if (batch.size() >= BATCH_SIZE) {
|
if (batch.size() >= BATCH_SIZE) {
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
-- V86: 계약 승계 (contract_succession)
|
||||||
|
-- 설계사 해촉/이관 시 보험계약의 "유지수수료" 귀속 설계사를 후임에게 전환.
|
||||||
|
-- - 모집수수료(1회차)는 원 모집설계사 귀속이므로 승계 대상 아님.
|
||||||
|
-- - 유지수수료는 승계월(succession_date의 YYYYMM) 이후 정산월부터 후임 설계사에게 귀속.
|
||||||
|
-- - 후임 설계사는 본인 등급의 payout 비율로 수령(CommissionCalculator 가 effectiveAgentId 등급 조회).
|
||||||
|
-- - 한 계약에 시차 승계(A→B→C) 가능: 정산월 기준 가장 최근 ACTIVE 승계가 유효.
|
||||||
|
|
||||||
|
CREATE TABLE contract_succession (
|
||||||
|
succession_id BIGSERIAL PRIMARY KEY,
|
||||||
|
contract_id BIGINT NOT NULL REFERENCES contract(contract_id),
|
||||||
|
from_agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
|
||||||
|
to_agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
|
||||||
|
succession_date DATE NOT NULL,
|
||||||
|
reason VARCHAR(20) NOT NULL DEFAULT 'TERMINATION',
|
||||||
|
memo VARCHAR(500),
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by BIGINT,
|
||||||
|
CHECK (reason IN ('TERMINATION','TRANSFER','REASSIGN')),
|
||||||
|
CHECK (status IN ('ACTIVE','CANCELED')),
|
||||||
|
CHECK (from_agent_id <> to_agent_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE contract_succession IS '계약 승계 — 유지수수료 귀속 설계사 전환 이력';
|
||||||
|
COMMENT ON COLUMN contract_succession.contract_id IS '대상 보험계약 FK';
|
||||||
|
COMMENT ON COLUMN contract_succession.from_agent_id IS '전임 설계사 (관두거나 이관한 설계사)';
|
||||||
|
COMMENT ON COLUMN contract_succession.to_agent_id IS '후임 설계사 (유지수수료를 이어받는 설계사)';
|
||||||
|
COMMENT ON COLUMN contract_succession.succession_date IS '승계 효력일 (이 날짜의 YYYYMM 이상 정산월부터 후임 귀속)';
|
||||||
|
COMMENT ON COLUMN contract_succession.reason IS 'TERMINATION=해촉 / TRANSFER=이관 / REASSIGN=재배정';
|
||||||
|
COMMENT ON COLUMN contract_succession.status IS 'ACTIVE=유효 / CANCELED=취소';
|
||||||
|
|
||||||
|
-- 배치 effective 조회용: contract_id + 효력일 역순
|
||||||
|
CREATE INDEX idx_cs_contract ON contract_succession(contract_id, status, succession_date DESC);
|
||||||
|
CREATE INDEX idx_cs_to_agent ON contract_succession(to_agent_id, status);
|
||||||
|
CREATE INDEX idx_cs_from_agent ON contract_succession(from_agent_id, status);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 메뉴 (GRP_PRD 하위, ENDORSE 옆)
|
||||||
|
-- ============================================================
|
||||||
|
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
|
||||||
|
('CONTRACT_SUCCESSION', '계약 승계', '/contracts/successions', 'contracts/ContractSuccessions', 50)
|
||||||
|
) AS x(code, name, path, cmpt, sort) ON p.menu_code = 'GRP_PRD'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code);
|
||||||
|
|
||||||
|
-- 권한 항목 (READ/CREATE/UPDATE/DELETE)
|
||||||
|
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),
|
||||||
|
('DELETE', '삭제', 40)
|
||||||
|
) AS p(code, name, sort)
|
||||||
|
WHERE m.menu_code = 'CONTRACT_SUCCESSION'
|
||||||
|
ON CONFLICT (menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- 역할 부여: 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 = 'CONTRACT_SUCCESSION'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- MANAGER: 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 = 'MANAGER'
|
||||||
|
AND mp.perm_code = 'READ'
|
||||||
|
AND m.menu_code = 'CONTRACT_SUCCESSION'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 공통코드
|
||||||
|
-- ============================================================
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('SUCCESSION_REASON', '승계사유', 'TERMINATION/TRANSFER/REASSIGN', 'Y', 470),
|
||||||
|
('SUCCESSION_STATUS', '승계상태', 'ACTIVE/CANCELED', 'Y', 480)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, sort_order, is_active) VALUES
|
||||||
|
('SUCCESSION_REASON', 'TERMINATION', '해촉', 10, 'Y'),
|
||||||
|
('SUCCESSION_REASON', 'TRANSFER', '이관', 20, 'Y'),
|
||||||
|
('SUCCESSION_REASON', 'REASSIGN', '재배정', 30, 'Y'),
|
||||||
|
('SUCCESSION_STATUS', 'ACTIVE', '유효', 10, 'Y'),
|
||||||
|
('SUCCESSION_STATUS', 'CANCELED', '취소', 20, 'Y')
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 테스트 데이터 (소량) — 테이블이 비어있고 활성 계약/타 설계사가 존재할 때만 1건
|
||||||
|
-- ============================================================
|
||||||
|
INSERT INTO contract_succession (contract_id, from_agent_id, to_agent_id, succession_date, reason, memo, status, created_at)
|
||||||
|
SELECT c.contract_id, c.agent_id, b.agent_id, DATE '2025-01-01', 'TERMINATION', '테스트 승계 데이터', 'ACTIVE', NOW()
|
||||||
|
FROM contract c
|
||||||
|
CROSS JOIN LATERAL (
|
||||||
|
SELECT agent_id FROM agent WHERE agent_id <> c.agent_id AND status = 'ACTIVE' ORDER BY agent_id LIMIT 1
|
||||||
|
) b
|
||||||
|
WHERE c.status = 'ACTIVE'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM contract_succession)
|
||||||
|
ORDER BY c.contract_id
|
||||||
|
LIMIT 1;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ga.core.mapper.succession;
|
||||||
|
|
||||||
|
import com.ga.core.vo.succession.ContractSuccessionResp;
|
||||||
|
import com.ga.core.vo.succession.ContractSuccessionSearchParam;
|
||||||
|
import com.ga.core.vo.succession.ContractSuccessionVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ContractSuccessionMapper {
|
||||||
|
ContractSuccessionResp selectById(@Param("successionId") Long successionId);
|
||||||
|
List<ContractSuccessionResp> selectList(ContractSuccessionSearchParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 배치 유지수수료 계산용 — 정산월 기준 유효한 승계의 (계약, 후임설계사) 매핑.
|
||||||
|
* 계약별로 succession_date 의 YYYYMM 이 정산월 이하인 가장 최근 ACTIVE 승계 1건만.
|
||||||
|
*/
|
||||||
|
List<ContractSuccessionVO> selectEffectiveSuccessions(@Param("settleMonth") String settleMonth);
|
||||||
|
|
||||||
|
int insert(ContractSuccessionVO vo);
|
||||||
|
int cancel(@Param("successionId") Long successionId, @Param("updatedBy") Long updatedBy);
|
||||||
|
int delete(@Param("successionId") Long successionId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ga.core.vo.succession;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ContractSuccessionResp {
|
||||||
|
private Long successionId;
|
||||||
|
private Long contractId;
|
||||||
|
private String policyNo;
|
||||||
|
private Long fromAgentId;
|
||||||
|
private String fromAgentName;
|
||||||
|
private Long toAgentId;
|
||||||
|
private String toAgentName;
|
||||||
|
private LocalDate successionDate;
|
||||||
|
private String reason;
|
||||||
|
private String reasonName;
|
||||||
|
private String memo;
|
||||||
|
private String status;
|
||||||
|
private String statusName;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ga.core.vo.succession;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ContractSuccessionSaveReq {
|
||||||
|
@NotNull
|
||||||
|
private Long contractId;
|
||||||
|
@NotNull
|
||||||
|
private Long fromAgentId;
|
||||||
|
@NotNull
|
||||||
|
private Long toAgentId;
|
||||||
|
@NotNull
|
||||||
|
private LocalDate successionDate;
|
||||||
|
/** TERMINATION/TRANSFER/REASSIGN (기본 TERMINATION) */
|
||||||
|
private String reason;
|
||||||
|
private String memo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ga.core.vo.succession;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ContractSuccessionSearchParam extends SearchParam {
|
||||||
|
private Long contractId;
|
||||||
|
private Long fromAgentId;
|
||||||
|
private Long toAgentId;
|
||||||
|
private String reason;
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.succession;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* contract_succession — 계약 승계 (V86)
|
||||||
|
* 유지수수료 귀속 설계사를 승계일 기준으로 전임→후임 전환.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ContractSuccessionVO {
|
||||||
|
private Long successionId;
|
||||||
|
private Long contractId;
|
||||||
|
private Long fromAgentId;
|
||||||
|
private Long toAgentId;
|
||||||
|
private LocalDate successionDate;
|
||||||
|
/** SUCCESSION_REASON: TERMINATION/TRANSFER/REASSIGN */
|
||||||
|
private String reason;
|
||||||
|
private String memo;
|
||||||
|
/** SUCCESSION_STATUS: ACTIVE/CANCELED */
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private Long updatedBy;
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?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.succession.ContractSuccessionMapper">
|
||||||
|
|
||||||
|
<resultMap id="VOMap" type="com.ga.core.vo.succession.ContractSuccessionVO"/>
|
||||||
|
<resultMap id="RespMap" type="com.ga.core.vo.succession.ContractSuccessionResp"/>
|
||||||
|
|
||||||
|
<sql id="joinCols">
|
||||||
|
cs.succession_id, cs.contract_id, ct.policy_no,
|
||||||
|
cs.from_agent_id, fa.agent_name AS from_agent_name,
|
||||||
|
cs.to_agent_id, ta.agent_name AS to_agent_name,
|
||||||
|
cs.succession_date,
|
||||||
|
cs.reason,
|
||||||
|
(SELECT cc.code_name FROM common_code cc
|
||||||
|
WHERE cc.group_code = 'SUCCESSION_REASON' AND cc.code = cs.reason) AS reason_name,
|
||||||
|
cs.memo,
|
||||||
|
cs.status,
|
||||||
|
(SELECT cc.code_name FROM common_code cc
|
||||||
|
WHERE cc.group_code = 'SUCCESSION_STATUS' AND cc.code = cs.status) AS status_name,
|
||||||
|
cs.created_at
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="RespMap">
|
||||||
|
SELECT <include refid="joinCols"/>
|
||||||
|
FROM contract_succession cs
|
||||||
|
LEFT JOIN contract ct ON ct.contract_id = cs.contract_id
|
||||||
|
LEFT JOIN agent fa ON fa.agent_id = cs.from_agent_id
|
||||||
|
LEFT JOIN agent ta ON ta.agent_id = cs.to_agent_id
|
||||||
|
WHERE cs.succession_id = #{successionId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="RespMap">
|
||||||
|
SELECT <include refid="joinCols"/>
|
||||||
|
FROM contract_succession cs
|
||||||
|
LEFT JOIN contract ct ON ct.contract_id = cs.contract_id
|
||||||
|
LEFT JOIN agent fa ON fa.agent_id = cs.from_agent_id
|
||||||
|
LEFT JOIN agent ta ON ta.agent_id = cs.to_agent_id
|
||||||
|
<where>
|
||||||
|
<if test="contractId != null">AND cs.contract_id = #{contractId}</if>
|
||||||
|
<if test="fromAgentId != null">AND cs.from_agent_id = #{fromAgentId}</if>
|
||||||
|
<if test="toAgentId != null">AND cs.to_agent_id = #{toAgentId}</if>
|
||||||
|
<if test="reason != null and reason != ''">AND cs.reason = #{reason}</if>
|
||||||
|
<if test="status != null and status != ''">AND cs.status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY cs.succession_date DESC, cs.succession_id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 정산월 기준 계약별 유효 승계 (가장 최근 ACTIVE 1건) -->
|
||||||
|
<select id="selectEffectiveSuccessions" resultMap="VOMap">
|
||||||
|
SELECT DISTINCT ON (cs.contract_id)
|
||||||
|
cs.contract_id, cs.to_agent_id, cs.from_agent_id, cs.succession_date, cs.status
|
||||||
|
FROM contract_succession cs
|
||||||
|
WHERE cs.status = 'ACTIVE'
|
||||||
|
AND TO_CHAR(cs.succession_date, 'YYYYMM') <= #{settleMonth}
|
||||||
|
ORDER BY cs.contract_id, cs.succession_date DESC, cs.succession_id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="successionId">
|
||||||
|
INSERT INTO contract_succession
|
||||||
|
(contract_id, from_agent_id, to_agent_id, succession_date, reason, memo, status, created_by)
|
||||||
|
VALUES
|
||||||
|
(#{contractId}, #{fromAgentId}, #{toAgentId}, #{successionDate},
|
||||||
|
COALESCE(#{reason}, 'TERMINATION'), #{memo}, 'ACTIVE', #{createdBy})
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="cancel">
|
||||||
|
UPDATE contract_succession
|
||||||
|
SET status = 'CANCELED',
|
||||||
|
updated_at = NOW(),
|
||||||
|
updated_by = #{updatedBy}
|
||||||
|
WHERE succession_id = #{successionId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="delete">
|
||||||
|
DELETE FROM contract_succession WHERE succession_id = #{successionId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -32,6 +32,7 @@ import AgentTrainings from '@/pages/agent/AgentTrainings';
|
|||||||
|
|
||||||
// ── P4: 계약 운영 ────────────────────────────────────
|
// ── P4: 계약 운영 ────────────────────────────────────
|
||||||
import ContractEndorsements from '@/pages/contracts/ContractEndorsements';
|
import ContractEndorsements from '@/pages/contracts/ContractEndorsements';
|
||||||
|
import ContractSuccessions from '@/pages/contracts/ContractSuccessions';
|
||||||
import Complaints from '@/pages/contracts/Complaints';
|
import Complaints from '@/pages/contracts/Complaints';
|
||||||
|
|
||||||
// ── P4: 시스템 ──────────────────────────────────────
|
// ── P4: 시스템 ──────────────────────────────────────
|
||||||
@@ -191,6 +192,7 @@ export default function App() {
|
|||||||
|
|
||||||
{/* P4: 계약 운영 */}
|
{/* P4: 계약 운영 */}
|
||||||
<Route path="contracts/endorsements" element={<ContractEndorsements />} />
|
<Route path="contracts/endorsements" element={<ContractEndorsements />} />
|
||||||
|
<Route path="contracts/successions" element={<ContractSuccessions />} />
|
||||||
<Route path="contracts/complaints" element={<Complaints />} />
|
<Route path="contracts/complaints" element={<Complaints />} />
|
||||||
|
|
||||||
{/* P4: 시스템 */}
|
{/* P4: 시스템 */}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import api, { PageResponse, unwrap } from './request';
|
||||||
|
|
||||||
|
export interface ContractSuccessionRow extends Record<string, unknown> {
|
||||||
|
successionId: number;
|
||||||
|
contractId: number;
|
||||||
|
policyNo: string;
|
||||||
|
fromAgentId: number;
|
||||||
|
fromAgentName: string;
|
||||||
|
toAgentId: number;
|
||||||
|
toAgentName: string;
|
||||||
|
successionDate: string;
|
||||||
|
reason: string; // TERMINATION / TRANSFER / REASSIGN
|
||||||
|
reasonName: string;
|
||||||
|
memo?: string;
|
||||||
|
status: string; // ACTIVE / CANCELED
|
||||||
|
statusName: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContractSuccessionSaveReq {
|
||||||
|
contractId: number;
|
||||||
|
fromAgentId: number;
|
||||||
|
toAgentId: number;
|
||||||
|
successionDate: string;
|
||||||
|
reason?: string;
|
||||||
|
memo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuccessionSearchParam {
|
||||||
|
contractId?: number;
|
||||||
|
fromAgentId?: number;
|
||||||
|
toAgentId?: number;
|
||||||
|
reason?: string;
|
||||||
|
status?: string;
|
||||||
|
pageNum?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const successionApi = {
|
||||||
|
list: (p: SuccessionSearchParam) =>
|
||||||
|
unwrap<PageResponse<ContractSuccessionRow>>(
|
||||||
|
api.get('/api/contract-successions', { params: p }),
|
||||||
|
),
|
||||||
|
create: (body: ContractSuccessionSaveReq) =>
|
||||||
|
unwrap<number>(api.post('/api/contract-successions', body)),
|
||||||
|
cancel: (id: number) =>
|
||||||
|
unwrap<void>(api.post(`/api/contract-successions/${id}/cancel`)),
|
||||||
|
delete: (id: number) =>
|
||||||
|
unwrap<void>(api.delete(`/api/contract-successions/${id}`)),
|
||||||
|
};
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Alert, Form, Input, InputNumber, Modal, Select, Tag, 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 PageContainer from '@/components/common/PageContainer';
|
||||||
|
import SearchForm from '@/components/common/SearchForm';
|
||||||
|
import DataGrid from '@/components/common/DataGrid';
|
||||||
|
import PermissionButton from '@/components/common/PermissionButton';
|
||||||
|
import CodeBadge from '@/components/common/CodeBadge';
|
||||||
|
import { successionApi, ContractSuccessionRow, ContractSuccessionSaveReq, SuccessionSearchParam } from '@/api/succession';
|
||||||
|
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||||
|
|
||||||
|
const COLUMNS: ColDef<ContractSuccessionRow>[] = [
|
||||||
|
{ field: 'policyNo', headerName: '증권번호', width: 150, pinned: 'left' },
|
||||||
|
{ field: 'fromAgentName', headerName: '전임 설계사', width: 130 },
|
||||||
|
{ field: 'toAgentName', headerName: '후임 설계사', width: 130 },
|
||||||
|
{ field: 'successionDate', headerName: '승계일', width: 120 },
|
||||||
|
{
|
||||||
|
field: 'reason', headerName: '사유', width: 110,
|
||||||
|
cellRenderer: (p: { value: string }) => <CodeBadge groupCode="SUCCESSION_REASON" value={p.value} />,
|
||||||
|
},
|
||||||
|
{ field: 'memo', headerName: '메모', flex: 1 },
|
||||||
|
{
|
||||||
|
field: 'status', headerName: '상태', width: 100,
|
||||||
|
cellRenderer: (p: { value: string }) =>
|
||||||
|
p.value === 'CANCELED'
|
||||||
|
? <Tag color="default" style={{ margin: 0 }}>취소</Tag>
|
||||||
|
: <Tag color="green" style={{ margin: 0, fontWeight: 700 }}>유효</Tag>,
|
||||||
|
},
|
||||||
|
{ field: 'createdAt', headerName: '등록일', width: 120 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ContractSuccessions() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const [params, setParams] = useState<SuccessionSearchParam>({ pageSize: 100 });
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ['successions', params],
|
||||||
|
queryFn: () => successionApi.list(params),
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = data?.list ?? [];
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
const values = await form.validateFields() as ContractSuccessionSaveReq;
|
||||||
|
try {
|
||||||
|
await successionApi.create(values);
|
||||||
|
message.success('계약 승계 등록 완료');
|
||||||
|
qc.invalidateQueries({ queryKey: ['successions'] });
|
||||||
|
setCreateOpen(false);
|
||||||
|
form.resetFields();
|
||||||
|
} catch { /* handled by interceptor */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = (id: number) => Modal.confirm({
|
||||||
|
title: '승계 취소',
|
||||||
|
content: '이 승계를 취소합니다. 이후 정산월부터 유지수수료는 다시 직전 귀속 설계사에게 계산됩니다.',
|
||||||
|
okText: '취소 처리',
|
||||||
|
cancelText: '닫기',
|
||||||
|
onOk: async () => {
|
||||||
|
await successionApi.cancel(id);
|
||||||
|
message.success('승계 취소 완료');
|
||||||
|
qc.invalidateQueries({ queryKey: ['successions'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title="계약 승계 관리"
|
||||||
|
description="설계사 해촉·이관 시 보험계약의 유지수수료 귀속 설계사를 후임으로 전환 (모집수수료는 원 설계사 귀속 유지)"
|
||||||
|
extra={
|
||||||
|
<PermissionButton menuCode="CONTRACT_SUCCESSION" permCode="CREATE" type="primary" onClick={() => setCreateOpen(true)}>
|
||||||
|
+ 승계 등록
|
||||||
|
</PermissionButton>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isError && (
|
||||||
|
<Alert type="warning" showIcon message="API 미응답" description="/api/contract-successions 를 확인하세요." style={{ marginBottom: 16 }} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SearchForm
|
||||||
|
conditions={[
|
||||||
|
{ type: 'code', name: 'reason', label: '사유', groupCode: 'SUCCESSION_REASON', span: 6 },
|
||||||
|
{ type: 'code', name: 'status', label: '상태', groupCode: 'SUCCESSION_STATUS', span: 6 },
|
||||||
|
]}
|
||||||
|
onSearch={(v) => setParams({ ...v as SuccessionSearchParam, pageSize: 100 })}
|
||||||
|
onReset={() => setParams({ pageSize: 100 })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProCard style={{ background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }}>
|
||||||
|
<DataGrid<ContractSuccessionRow>
|
||||||
|
rows={rows}
|
||||||
|
columns={[
|
||||||
|
...COLUMNS,
|
||||||
|
{
|
||||||
|
headerName: '액션', width: 110, pinned: 'right',
|
||||||
|
cellRenderer: (p: { data: ContractSuccessionRow }) => (
|
||||||
|
<PermissionButton
|
||||||
|
menuCode="CONTRACT_SUCCESSION" permCode="UPDATE" size="small" danger
|
||||||
|
disabled={p.data.status === 'CANCELED'}
|
||||||
|
onClick={() => handleCancel(p.data.successionId)}
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</PermissionButton>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
loading={isLoading}
|
||||||
|
height={600}
|
||||||
|
rowKey="successionId"
|
||||||
|
/>
|
||||||
|
</ProCard>
|
||||||
|
|
||||||
|
<Modal title="계약 승계 등록" open={createOpen} onOk={handleCreate} onCancel={() => setCreateOpen(false)} okText="등록" cancelText="취소">
|
||||||
|
<Form form={form} layout="vertical" initialValues={{ reason: 'TERMINATION' }}>
|
||||||
|
<Form.Item name="contractId" label="계약 ID" rules={[{ required: true }]}>
|
||||||
|
<InputNumber style={{ width: '100%' }} placeholder="contract_id" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="fromAgentId" label="전임 설계사 ID" rules={[{ required: true }]}>
|
||||||
|
<InputNumber style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="toAgentId" label="후임 설계사 ID" rules={[{ required: true }]}>
|
||||||
|
<InputNumber style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="successionDate" label="승계일" rules={[{ required: true }]}>
|
||||||
|
<Input placeholder="YYYY-MM-DD" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="reason" label="사유" rules={[{ required: true }]}>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: 'TERMINATION', label: '해촉' },
|
||||||
|
{ value: 'TRANSFER', label: '이관' },
|
||||||
|
{ value: 'REASSIGN', label: '재배정' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="memo" label="메모">
|
||||||
|
<Input.TextArea rows={3} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user