feat: 1200%룰 이연 분급 도래월 지급(PayInstallmentStep) + 코드베이스 신입교육 주석
PayInstallmentStep(Step 4.5): 1200%룰로 이연된 분급(installment_plan SCHEDULED)을 도래월(settle_month=정산월)에 recruit_ledger로 편입해 실제 지급. 이전엔 이연만 되고 도래월 지급 절차가 없어 차액이 영구 미지급되던 갭을 메움. - BatchConfig job flow에 step4b(calcRecruit→payInstallment→calcMaintain) 등록 - InstallmentPlanMapper.resetPaidToScheduledByMonth(+XML)로 재실행 멱등 (진입 시 PAID→SCHEDULED 역산, Step4 deleteBySettleMonth가 원장 정리) - RecruitLedgerMapper insert에 reconcile_status 컬럼, SettlementContext.installmentPaidCount - 단위테스트 3건(정상지급/멱등/계약미존재) GREEN 신입교육 주석: 컨트롤러/서비스/공통/프론트 전반에 도메인·코드 설명 주석 추가 (동작 변경 없음 — 빌드/테스트/타입체크 전부 GREEN으로 무회귀 확인). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,13 @@ import { Spin } from 'antd';
|
||||
import LoginPage from '@/pages/LoginPage';
|
||||
import MainLayout from '@/layouts/MainLayout';
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// 앱의 라우팅(주소 → 화면) 정의 파일.
|
||||
// 모든 페이지는 React.lazy 로 "필요할 때만" 불러오도록(코드 스플리팅) 처리한다.
|
||||
// → 첫 로딩 속도를 빠르게 하고, 안 쓰는 화면 코드는 나중에 받는다.
|
||||
// 로그인('/login')만 공개이고, 나머지는 MainLayout 아래 RequireAuth 로 보호된다.
|
||||
// ──────────────────────────────────────────────────────
|
||||
|
||||
// ── 페이지 레이지 로딩 ──────────────────────────────────
|
||||
const Dashboard = React.lazy(() => import('@/pages/Dashboard'));
|
||||
|
||||
@@ -154,6 +161,7 @@ const PageFallback = (
|
||||
</div>
|
||||
);
|
||||
|
||||
// 인증 가드 — accessToken 이 없으면 로그인 페이지로 보낸다(보호 라우트 진입 차단)
|
||||
function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
return token ? <>{children}</> : <Navigate to="/login" replace />;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// ============================================================================
|
||||
// 설계사 API — 백엔드 /api/agents (AgentController)
|
||||
// 설계사(agent): 보험을 모집·관리하는 영업 인력. 조직(org)/등급(grade)에 소속.
|
||||
// ============================================================================
|
||||
import api, { PageResponse, unwrap } from './request';
|
||||
|
||||
// 설계사 조회 응답. 조직/등급/입사일 + 계약수/누적수수료 등 요약 포함.
|
||||
export interface AgentResp {
|
||||
agentId: number;
|
||||
agentName: string;
|
||||
@@ -26,6 +31,7 @@ export interface AgentSearchParam {
|
||||
gradeId?: number;
|
||||
}
|
||||
|
||||
// 설계사 등록/수정 요청. residentNo(주민번호)·계좌정보 등 개인정보 포함.
|
||||
export interface AgentSaveReq {
|
||||
agentName: string;
|
||||
orgId: number;
|
||||
@@ -40,11 +46,16 @@ export interface AgentSaveReq {
|
||||
}
|
||||
|
||||
export const agentApi = {
|
||||
// 설계사 목록 — GET /api/agents
|
||||
list: (p: AgentSearchParam) =>
|
||||
unwrap<PageResponse<AgentResp>>(api.get('/api/agents', { params: p })),
|
||||
// 설계사 상세 — GET /api/agents/{id}
|
||||
detail: (id: number) => unwrap<AgentResp>(api.get(`/api/agents/${id}`)),
|
||||
// 설계사 등록(생성된 id 반환) — POST /api/agents
|
||||
create: (req: AgentSaveReq) => unwrap<number>(api.post('/api/agents', req)),
|
||||
// 설계사 수정 — PUT /api/agents/{id}
|
||||
update: (id: number, req: AgentSaveReq) => unwrap<void>(api.put(`/api/agents/${id}`, req)),
|
||||
// 상태 변경(재직/휴직/해촉 등) — PUT /api/agents/{id}/status
|
||||
changeStatus: (id: number, status: string) =>
|
||||
unwrap<void>(api.put(`/api/agents/${id}/status`, { status })),
|
||||
};
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
// ============================================================================
|
||||
// 인증/로그인 API — 백엔드 /api/auth/* (AuthController)
|
||||
// 로그인 후 받은 accessToken 을 localStorage 에 저장하면 request.ts 인터셉터가
|
||||
// 모든 요청에 자동으로 붙여준다.
|
||||
// ============================================================================
|
||||
import api, { unwrap } from './request';
|
||||
|
||||
// 로그인 요청 본문. loginId(사번/아이디) + password.
|
||||
export interface LoginReq {
|
||||
loginId: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// 로그인 응답. accessToken(JWT) + 사용자 기본정보 + 권한(roles).
|
||||
export interface LoginResp {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
@@ -15,6 +22,7 @@ export interface LoginResp {
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
// 내 정보(me) 응답. 로그인 사용자와 연결된 설계사(agent)/조직 정보 포함.
|
||||
export interface UserResp {
|
||||
userId: number;
|
||||
loginId: string;
|
||||
@@ -29,11 +37,16 @@ export interface UserResp {
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
// 로그인: 아이디/비밀번호로 토큰 발급 — POST /api/auth/login
|
||||
login: (req: LoginReq) => unwrap<LoginResp>(api.post('/api/auth/login', req)),
|
||||
// 토큰 갱신: refreshToken 으로 새 accessToken 발급 — POST /api/auth/refresh
|
||||
refresh: (refreshToken: string) =>
|
||||
unwrap<LoginResp>(api.post('/api/auth/refresh', { refreshToken })),
|
||||
// 내 정보 조회 — GET /api/auth/me
|
||||
me: () => unwrap<UserResp>(api.get('/api/auth/me')),
|
||||
// 로그아웃 — POST /api/auth/logout
|
||||
logout: () => unwrap<void>(api.post('/api/auth/logout')),
|
||||
// 비밀번호 변경 — POST /api/auth/password
|
||||
changePassword: (oldPassword: string, newPassword: string) =>
|
||||
unwrap<void>(api.post('/api/auth/password', { oldPassword, newPassword })),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
// ============================================================================
|
||||
// 수수료 종합 API — 백엔드 /api/commissions 외 다수 수수료 컨트롤러
|
||||
// ----------------------------------------------------------------------------
|
||||
// 이 파일 하나가 "수수료 계산"과 관련된 여러 도메인을 한데 모아 다룬다.
|
||||
// 큰 흐름: 각 수수료 종류마다 (1) 규칙(Rule) = 요율/금액 정의, (2) 원장(Ledger) =
|
||||
// 설계사별 실제 산출 결과. 화면은 보통 규칙 CRUD + 원장 목록조회로 구성된다.
|
||||
//
|
||||
// [보험 수수료 용어 풀이 — 신입 필독]
|
||||
// - 수수료(commission): 보험계약 체결/유지로 설계사가 받는 보수.
|
||||
// - 수금수수료(collection): 보험료가 실제로 수금될 때 지급되는 수수료.
|
||||
// - 갱신수수료(renewal): 계약 갱신 시 발생하는 수수료.
|
||||
// - 부활수수료(reinstatement): 실효된 계약이 부활될 때의 수수료.
|
||||
// - 유지보너스(persistency): 계약이 일정 개월(13/25/37/49개월) 유지되면 주는 보너스.
|
||||
// - 오버라이드(override): 하위 설계사 실적의 일부를 상위 관리자/조직이 받는 수수료.
|
||||
// - 환수(clawback): 이미 지급한 수수료를 사후에 회수하는 것(조기 해지 등).
|
||||
// - 정산(settle): 한 달 치 여러 수수료를 합산해 실지급액을 확정하는 절차.
|
||||
// - 분급/선지급(advance): 미래 수수료를 미리 당겨 지급하는 것.
|
||||
// - 마진(margin): 회사가 보험사에서 받은 수입 - 설계사에게 준 지급액.
|
||||
// ============================================================================
|
||||
import api, { PageResponse, unwrap } from './request';
|
||||
|
||||
// ── 공통 ──────────────────────────────────────────
|
||||
// 수수료 화면 공통 검색 파라미터(설계사/정산월/수수료종류/페이지).
|
||||
export interface CommissionSearchParam {
|
||||
agentId?: number;
|
||||
agentName?: string;
|
||||
@@ -11,6 +31,9 @@ export interface CommissionSearchParam {
|
||||
}
|
||||
|
||||
// ── CommissionMaster ──────────────────────────────
|
||||
// 수수료 마스터: 설계사 1명의 한 달치 수수료 집계 행.
|
||||
// - grossAmount: 세전 총액, netAmount: 공제/세금 반영 후 실지급액
|
||||
// - status: 처리상태(산출/확정 등)
|
||||
export interface CommissionMasterRow extends Record<string, unknown> {
|
||||
masterId: number;
|
||||
agentId: number;
|
||||
@@ -23,7 +46,8 @@ export interface CommissionMasterRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ── CollectionCommission ──────────────────────────
|
||||
// ── CollectionCommission(수금수수료) ──────────────
|
||||
// Rule: 보험사/상품유형/납입주기별 수금수수료 요율(ratePercent)과 적용기간.
|
||||
export interface CollectionRuleRow extends Record<string, unknown> {
|
||||
ruleId: number;
|
||||
carrierId: number;
|
||||
@@ -36,19 +60,21 @@ export interface CollectionRuleRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// Ledger: 계약별 실제 수금액(collectionAmount)에 요율을 적용한 수수료 산출 결과.
|
||||
export interface CollectionLedgerRow extends Record<string, unknown> {
|
||||
ledgerId: number;
|
||||
agentId: number;
|
||||
agentName: string;
|
||||
contractNo: string;
|
||||
settleMonth: string;
|
||||
collectionAmount: number;
|
||||
commissionAmount: number;
|
||||
collectionAmount: number; // 실제 수금된 보험료
|
||||
commissionAmount: number; // 산출된 수수료액
|
||||
ratePercent: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ── RenewalCommission ─────────────────────────────
|
||||
// ── RenewalCommission(갱신수수료) ──────────────────
|
||||
// 계약 갱신 시 갱신보험료(renewalPremium)에 요율을 곱해 산출.
|
||||
export interface RenewalRuleRow extends Record<string, unknown> {
|
||||
ruleId: number;
|
||||
carrierId: number;
|
||||
@@ -71,7 +97,8 @@ export interface RenewalLedgerRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ── ReinstatementCommission ───────────────────────
|
||||
// ── ReinstatementCommission(부활수수료) ────────────
|
||||
// 실효된 계약이 부활될 때 부활보험료(reinstatementPremium) 기준으로 산출.
|
||||
export interface ReinstatementRuleRow extends Record<string, unknown> {
|
||||
ruleId: number;
|
||||
carrierId: number;
|
||||
@@ -94,7 +121,8 @@ export interface ReinstatementLedgerRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ── PersistencyBonus ──────────────────────────────
|
||||
// ── PersistencyBonus(유지보너스) ───────────────────
|
||||
// 계약 유지율(retentionRate)이 기준(minRate) 이상이면 13/25/37/49개월 시점에 지급.
|
||||
export interface PersistencyRuleRow extends Record<string, unknown> {
|
||||
ruleId: number;
|
||||
bonusType: string; // 13M/25M/37M/49M
|
||||
@@ -118,7 +146,9 @@ export interface PersistencyBonusRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ── OverrideLedger ────────────────────────────────
|
||||
// ── OverrideLedger(오버라이드 원장) ────────────────
|
||||
// 하위 설계사(fromAgent)의 기준수수료(baseCommission)에 오버라이드 요율을 곱해
|
||||
// 상위 관리자/조직(toAgent)에게 귀속되는 금액(overrideAmount)을 기록.
|
||||
export interface OverrideLedgerRow extends Record<string, unknown> {
|
||||
ledgerId: number;
|
||||
fromAgentId: number;
|
||||
@@ -144,7 +174,8 @@ export interface OverrideLedgerSearchParam {
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
// ── P7: IncomeCommission ──────────────────────────
|
||||
// ── P7: IncomeCommission(수입수수료) ───────────────
|
||||
// 회사가 보험사로부터 받는 수입수수료. premiumAmount×companyRate=incomeAmount.
|
||||
export interface IncomeLedgerRow extends Record<string, unknown> {
|
||||
incomeId: number;
|
||||
contractId: number;
|
||||
@@ -159,6 +190,7 @@ export interface IncomeLedgerRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// 마진 요약: 수입(incomeTotal) - 지급(payoutTotal) = 마진(marginAmount).
|
||||
export interface MarginSummaryRow extends Record<string, unknown> {
|
||||
marginId: number;
|
||||
settleMonth: string;
|
||||
@@ -170,7 +202,8 @@ export interface MarginSummaryRow extends Record<string, unknown> {
|
||||
marginRate?: number;
|
||||
}
|
||||
|
||||
// ── P7: ReferralCommission ────────────────────────
|
||||
// ── P7: ReferralCommission(소개수수료) ─────────────
|
||||
// 계약을 소개한 설계사(referrer)에게 지급. calcType: RATE(요율) 또는 FIXED(정액).
|
||||
export interface ReferralRuleRow extends Record<string, unknown> {
|
||||
ruleId: number;
|
||||
referralType: string;
|
||||
@@ -195,7 +228,9 @@ export interface ReferralLedgerRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ── P7: SettlingSupport ───────────────────────────
|
||||
// ── P7: SettlingSupport(정착지원금) ────────────────
|
||||
// 신입/도입 설계사에게 일정기간(durationMonths) 지급하는 정착 지원금.
|
||||
// 조기 이탈 시 clawbackMonths 기준으로 환수(clawbackAmount)될 수 있다.
|
||||
export interface SettlingSupportRuleRow extends Record<string, unknown> {
|
||||
ruleId: number;
|
||||
agentGrade: string;
|
||||
@@ -217,7 +252,8 @@ export interface SettlingSupportLedgerRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ── P7: RecognizedPerformance ─────────────────────
|
||||
// ── P7: RecognizedPerformance(인정실적) ────────────
|
||||
// 상품별 환산율(conversionRate)로 보험료를 환산하고, 인정율을 적용해 인정실적 산출.
|
||||
export interface ConversionRateRow extends Record<string, unknown> {
|
||||
rateId: number;
|
||||
productId: number;
|
||||
@@ -242,7 +278,8 @@ export interface RecognizedLedgerRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ── P7: Award ─────────────────────────────────────
|
||||
// ── P7: Award(시상/포상금) ─────────────────────────
|
||||
// 실적이 기준(thresholdAmount)을 넘으면 지급하는 시상금. 요율(awardRate) 또는 정액(awardFixed).
|
||||
export interface AwardRuleRow extends Record<string, unknown> {
|
||||
ruleId: number;
|
||||
awardType: string;
|
||||
@@ -267,12 +304,14 @@ export interface AwardLedgerRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// 시상금 환수 요청: 지급된 시상금 일부/전부를 회수(환수)할 때 금액과 사유.
|
||||
export interface AwardClawbackReq {
|
||||
clawbackAmount: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
// ── P7: LifeSupport ───────────────────────────────
|
||||
// ── P7: LifeSupport(생활지원금) ────────────────────
|
||||
// 설계사 등급(agentGrade)별 요율로 지급하는 생활지원금. 환수 발생 가능.
|
||||
export interface LifeSupportRuleRow extends Record<string, unknown> {
|
||||
ruleId: number;
|
||||
agentGrade: string;
|
||||
@@ -294,7 +333,8 @@ export interface LifeSupportLedgerRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ── P7: BranchAllowance ───────────────────────────
|
||||
// ── P7: BranchAllowance(지점장수당) ────────────────
|
||||
// 직책(positionCode)별 수당. 요율 적용하되 상한(capAmount)이 있으면 그 이하로 제한.
|
||||
export interface BranchAllowanceRuleRow extends Record<string, unknown> {
|
||||
ruleId: number;
|
||||
positionCode: string;
|
||||
@@ -318,7 +358,8 @@ export interface BranchAllowanceLedgerRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ── MOD-1: RecruiterCommission ────────────────────
|
||||
// ── MOD-1: RecruiterCommission(리크루팅 수수료) ────
|
||||
// 다른 설계사를 도입(리크루팅)한 설계사(recruiter)에게 지급하는 수수료.
|
||||
export interface RecruiterRuleRow extends Record<string, unknown> {
|
||||
ruleId: number;
|
||||
recruiterType: string;
|
||||
@@ -342,7 +383,8 @@ export interface RecruiterLedgerRow extends Record<string, unknown> {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ── MOD-3: ClawbackInstallment ────────────────────
|
||||
// ── MOD-3: ClawbackInstallment(환수 분할상환) ──────
|
||||
// 환수금(receivable)을 한 번에 회수하지 않고 여러 회차(installment)로 나눠 상환하는 계획.
|
||||
export interface ClawbackInstallmentRow extends Record<string, unknown> {
|
||||
planId: number;
|
||||
receivableId: number;
|
||||
@@ -359,7 +401,8 @@ export interface ClawbackInstallmentGenerateReq {
|
||||
startMonth: string;
|
||||
}
|
||||
|
||||
// ── MOD-4: LapsedReinstatement ────────────────────
|
||||
// ── MOD-4: LapsedReinstatement(실효-부활 수수료 회복) ──
|
||||
// 계약이 실효(lapsed)되면 수수료를 보류(withheld)했다가, 부활(reinstated)되면 다시 지급(repaid).
|
||||
export interface LapsedReinstatementRow extends Record<string, unknown> {
|
||||
recoveryId: number;
|
||||
contractId: number;
|
||||
@@ -381,7 +424,8 @@ export interface LapsedReinstatementSaveReq {
|
||||
withheldAmount: number;
|
||||
}
|
||||
|
||||
// ── MOD-6: StatuteConfig ──────────────────────────
|
||||
// ── MOD-6: StatuteConfig(소멸시효 설정) ────────────
|
||||
// 채권(환수금 등)의 소멸 기준 개월수(expirationMonths) 설정. 기간 경과 시 소멸 처리.
|
||||
export interface StatuteConfigRow extends Record<string, unknown> {
|
||||
configId: number;
|
||||
entityType: string;
|
||||
@@ -389,7 +433,8 @@ export interface StatuteConfigRow extends Record<string, unknown> {
|
||||
isActive: string;
|
||||
}
|
||||
|
||||
// ── MOD-7: ChargebackWaiver ───────────────────────
|
||||
// ── MOD-7: ChargebackWaiver(환수 면제) ─────────────
|
||||
// 원래 환수해야 할 금액(originalChargeback) 중 일부/전부를 면제(waiverAmount)하는 결재 건.
|
||||
export interface ChargebackWaiverRow extends Record<string, unknown> {
|
||||
requestId: number;
|
||||
contractId: number;
|
||||
@@ -415,8 +460,11 @@ export interface ChargebackWaiverSaveReq {
|
||||
}
|
||||
|
||||
// ── API ───────────────────────────────────────────
|
||||
// 수수료 도메인 호출 모음. 각 수수료 종류별로 규칙(rules) CRUD + 원장(ledgers) 조회.
|
||||
// 패턴: xxxRules(목록) / xxxRuleCreate(등록) / xxxRuleUpdate(수정) /
|
||||
// xxxRuleDelete(삭제) / xxxLedgers(원장 목록조회).
|
||||
export const commissionApi = {
|
||||
// CommissionMaster
|
||||
// CommissionMaster: 수수료 마스터 목록 — GET /api/commissions
|
||||
masterList: (p: CommissionSearchParam) =>
|
||||
unwrap<PageResponse<CommissionMasterRow>>(
|
||||
api.get('/api/commissions', { params: p }),
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
// ============================================================================
|
||||
// 보험계약 API — 백엔드 /api/contracts (ContractController)
|
||||
// 계약(contract): 고객이 가입한 보험증권 1건. 모든 수수료 계산의 출발점.
|
||||
// - premium(보험료): 계약자가 내는 보험료. payCycle(납입주기): 월납/연납 등.
|
||||
// - policyNo(증권번호): 계약 식별 번호. contractorName(계약자)/insuredName(피보험자).
|
||||
// ============================================================================
|
||||
import api, { PageResponse, unwrap } from './request';
|
||||
|
||||
// 계약 조회 응답. 설계사/상품/보험사/보험료/상태 등 계약 요약.
|
||||
export interface ContractResp {
|
||||
contractId: number;
|
||||
agentId: number;
|
||||
@@ -35,6 +42,7 @@ export interface ContractSearchParam {
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
// 계약 등록/수정 요청. payPeriod(납입기간)/coveragePeriod(보장기간) 등 포함.
|
||||
export interface ContractSaveReq {
|
||||
agentId: number;
|
||||
productId: number;
|
||||
@@ -51,9 +59,13 @@ export interface ContractSaveReq {
|
||||
}
|
||||
|
||||
export const contractApi = {
|
||||
// 계약 목록 — GET /api/contracts
|
||||
list: (p: ContractSearchParam) =>
|
||||
unwrap<PageResponse<ContractResp>>(api.get('/api/contracts', { params: p })),
|
||||
// 계약 상세 — GET /api/contracts/{id}
|
||||
detail: (id: number) => unwrap<ContractResp>(api.get(`/api/contracts/${id}`)),
|
||||
// 계약 등록(생성된 id 반환) — POST /api/contracts
|
||||
create: (req: ContractSaveReq) => unwrap<number>(api.post('/api/contracts', req)),
|
||||
// 계약 수정 — PUT /api/contracts/{id}
|
||||
update: (id: number, req: ContractSaveReq) => unwrap<void>(api.put(`/api/contracts/${id}`, req)),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
// ============================================================================
|
||||
// [공통] HTTP 통신 기반 모듈 — axios 인스턴스 + 응답 래퍼
|
||||
// ----------------------------------------------------------------------------
|
||||
// 이 파일은 모든 API 모듈(commission.ts, settle.ts 등)이 공유하는 공통 토대다.
|
||||
// - 백엔드는 항상 { success, code, message, data } 형태의 JSON(ApiResponse)으로 응답한다.
|
||||
// - 신입 가이드: 다른 *.ts 의 함수들은 여기서 만든 api 인스턴스와 unwrap() 을 import 해 사용한다.
|
||||
// ============================================================================
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { message } from 'antd';
|
||||
|
||||
// 백엔드 표준 응답 봉투(envelope). 실제 데이터는 data 필드 안에 들어 있다.
|
||||
// - success: 성공 여부(false 면 비즈니스 오류)
|
||||
// - code : 응답/오류 코드 문자열
|
||||
// - message: 사용자에게 보여줄 메시지
|
||||
// - data : 실제 페이로드(제네릭 T)
|
||||
// - timestamp: 서버 응답 시각
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
code: string;
|
||||
@@ -9,6 +22,9 @@ export interface ApiResponse<T> {
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// 목록 조회용 페이지네이션 응답. list(현재 페이지 데이터) + 페이지 메타.
|
||||
// - pageNum: 현재 페이지 번호, pageSize: 페이지당 건수
|
||||
// - total : 전체 건수, pages: 전체 페이지 수
|
||||
export interface PageResponse<T> {
|
||||
list: T[];
|
||||
pageNum: number;
|
||||
@@ -17,17 +33,20 @@ export interface PageResponse<T> {
|
||||
pages: number;
|
||||
}
|
||||
|
||||
// axios 인스턴스 생성. baseURL 은 환경변수(VITE_API_URL), 타임아웃 60초.
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL ?? '',
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
// [요청 인터셉터] 모든 요청에 로그인 토큰(JWT)을 Authorization 헤더로 자동 첨부.
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
});
|
||||
|
||||
// [응답 인터셉터] 성공 응답이라도 success=false 면 비즈니스 오류로 간주해 토스트 표시.
|
||||
api.interceptors.response.use(
|
||||
(res) => {
|
||||
const body = res.data;
|
||||
@@ -37,6 +56,7 @@ api.interceptors.response.use(
|
||||
}
|
||||
return res;
|
||||
},
|
||||
// [HTTP 오류 처리] 401: 토큰 만료/미인증 → 로그인 화면으로. 403: 권한 없음. 그 외: 서버 오류.
|
||||
(err: AxiosError<ApiResponse<unknown>>) => {
|
||||
const status = err.response?.status;
|
||||
const body = err.response?.data;
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
// ============================================================================
|
||||
// 정산 API — 백엔드 /api/settle (SettleController) 외 분급/대사/배치
|
||||
// ----------------------------------------------------------------------------
|
||||
// 정산(settle): 한 달치 모든 수수료(모집/유지/오버라이드 - 환수 등)를 설계사별로
|
||||
// 합산해 실지급액(netAmount)을 확정하는 절차. 흐름: 산출 → 확정(confirm) /
|
||||
// 보류(hold) / 보류해제(release). 이 파일엔 정산 외에 선지급(분급), 보험사 대사,
|
||||
// 정산 배치 실행 API 도 함께 들어 있다.
|
||||
// ============================================================================
|
||||
import api, { PageResponse, unwrap } from './request';
|
||||
|
||||
// 정산 결과 행: 설계사 1명의 한 달치 정산 명세.
|
||||
// - recruitTotal: 모집수수료 합, maintainTotal: 유지수수료 합
|
||||
// - exceptionPlus/Minus: 예외 가감액(+/-), overrideTotal: 오버라이드 합
|
||||
// - chargebackTotal: 환수 합, grossAmount: 세전 총액
|
||||
// - taxAmount: 세금, netAmount: 실지급액, status: 정산상태, holdReason: 보류사유
|
||||
export interface SettleResp {
|
||||
settleId: number;
|
||||
agentId: number;
|
||||
@@ -31,21 +44,29 @@ export interface SettleSearchParam {
|
||||
}
|
||||
|
||||
export const settleApi = {
|
||||
// 정산 목록 조회 — GET /api/settle
|
||||
list: (p: SettleSearchParam) =>
|
||||
unwrap<PageResponse<SettleResp>>(api.get('/api/settle', { params: p })),
|
||||
// 월 정산 요약(항목별 합계) — GET /api/settle/summary
|
||||
summary: (settleMonth: string) =>
|
||||
unwrap<Record<string, number>>(api.get('/api/settle/summary', { params: { settleMonth } })),
|
||||
// 조직별 정산 요약 — GET /api/settle/org-summary
|
||||
orgSummary: (settleMonth: string) =>
|
||||
unwrap<Array<Record<string, unknown>>>(api.get('/api/settle/org-summary', { params: { settleMonth } })),
|
||||
// 정산 확정(지급 확정) — PUT /api/settle/{id}/confirm
|
||||
confirm: (settleId: number) =>
|
||||
unwrap<void>(api.put(`/api/settle/${settleId}/confirm`)),
|
||||
// 정산 보류(지급 보류, 사유 필수) — PUT /api/settle/{id}/hold
|
||||
hold: (settleId: number, reason: string) =>
|
||||
unwrap<void>(api.put(`/api/settle/${settleId}/hold`, { reason })),
|
||||
// 보류 해제 — PUT /api/settle/{id}/release
|
||||
release: (settleId: number) =>
|
||||
unwrap<void>(api.put(`/api/settle/${settleId}/release`)),
|
||||
};
|
||||
|
||||
// ── MOD-2: AdvancePayment ─────────────────────────
|
||||
// ── MOD-2: AdvancePayment(선지급/분급) ─────────────
|
||||
// 미래 수수료를 미리 당겨 지급(principalAmount)하고, 이후 정산에서 조금씩 회수
|
||||
// (recoveredAmount)한다. balance = 미회수 잔액.
|
||||
export interface AdvancePaymentRow extends Record<string, unknown> {
|
||||
advanceId: number;
|
||||
agentId: number;
|
||||
@@ -81,7 +102,9 @@ export interface AdvancePaymentSearchParam {
|
||||
advanceMonth?: string;
|
||||
}
|
||||
|
||||
// ── MOD-5: CarrierReconIn ─────────────────────────
|
||||
// ── MOD-5: CarrierReconIn(보험사 수수료 대사) ──────
|
||||
// 보험사가 보고한 수수료총액(reportedTotal)과 우리 시스템 산출총액(systemTotal)을
|
||||
// 대조(reconcile)해 차이(varianceAmount)를 찾아내는 대사 작업.
|
||||
export interface CarrierReconInRow extends Record<string, unknown> {
|
||||
reconId: number;
|
||||
companyId: number;
|
||||
@@ -120,30 +143,42 @@ export interface CarrierReconSearchParam {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
// 선지급(분급) API — /api/advance-payments
|
||||
export const advancePaymentApi = {
|
||||
// 선지급 목록 — GET /api/advance-payments
|
||||
list: (p: AdvancePaymentSearchParam) =>
|
||||
unwrap<PageResponse<AdvancePaymentRow>>(api.get('/api/advance-payments', { params: p })),
|
||||
// 선지급 등록 — POST /api/advance-payments
|
||||
create: (body: AdvancePaymentSaveReq) =>
|
||||
unwrap<AdvancePaymentRow>(api.post('/api/advance-payments', body)),
|
||||
// 잔액 면제(회수 포기) — PUT /api/advance-payments/{id}/waive
|
||||
waive: (advanceId: number) =>
|
||||
unwrap<void>(api.put(`/api/advance-payments/${advanceId}/waive`)),
|
||||
// 회수 이력 조회 — GET /api/advance-payments/{id}/recoveries
|
||||
recoveries: (advanceId: number) =>
|
||||
unwrap<AdvanceRecoveryRow[]>(api.get(`/api/advance-payments/${advanceId}/recoveries`)),
|
||||
};
|
||||
|
||||
// 보험사 수수료 대사 API — /api/carrier-recon-in
|
||||
export const carrierReconInApi = {
|
||||
// 대사 목록 — GET /api/carrier-recon-in
|
||||
list: (p: CarrierReconSearchParam) =>
|
||||
unwrap<PageResponse<CarrierReconInRow>>(api.get('/api/carrier-recon-in', { params: p })),
|
||||
// 대사 건 등록(보고 데이터 업로드) — POST /api/carrier-recon-in
|
||||
create: (body: CarrierReconInSaveReq) =>
|
||||
unwrap<CarrierReconInRow>(api.post('/api/carrier-recon-in', body)),
|
||||
// 대사 실행(차이 산출) — POST /api/carrier-recon-in/{id}/reconcile
|
||||
reconcile: (reconId: number) =>
|
||||
unwrap<CarrierReconInRow>(api.post(`/api/carrier-recon-in/${reconId}/reconcile`)),
|
||||
// 대사 결과 승인 — PUT /api/carrier-recon-in/{id}/approve
|
||||
approve: (reconId: number) =>
|
||||
unwrap<void>(api.put(`/api/carrier-recon-in/${reconId}/approve`)),
|
||||
// 차이 내역 조회 — GET /api/carrier-recon-in/{id}/variances
|
||||
variances: (reconId: number) =>
|
||||
unwrap<ReconciliationVarianceRow[]>(api.get(`/api/carrier-recon-in/${reconId}/variances`)),
|
||||
};
|
||||
|
||||
// 정산 배치 작업 로그: 진행률(progressPct)/현재단계(currentStep)/소요시간 등.
|
||||
export interface BatchJobLog {
|
||||
jobId: number;
|
||||
jobName: string;
|
||||
@@ -157,9 +192,12 @@ export interface BatchJobLog {
|
||||
durationSec?: number;
|
||||
}
|
||||
|
||||
// 정산 배치 API — /api/batch
|
||||
export const batchApi = {
|
||||
// 정산 배치 실행(해당 월 전체 재계산) — POST /api/batch/settlement/run
|
||||
run: (settleMonth: string) =>
|
||||
unwrap<number>(api.post('/api/batch/settlement/run', { settleMonth })),
|
||||
// 배치 진행 상태 조회 — GET /api/batch/status
|
||||
status: () =>
|
||||
unwrap<BatchJobLog>(api.get('/api/batch/status')),
|
||||
};
|
||||
|
||||
@@ -5,17 +5,22 @@ import { COLOR_SUCCESS, COLOR_ERROR, COLOR_PRIMARY, GRAY } from '@/theme/tokens'
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// Props
|
||||
// - steps: 결재선 단계 목록(각 단계의 결재자/상태/처리시각/의견)
|
||||
// - currentStep: 현재 진행 중인 단계 번호(1부터 시작)
|
||||
interface Props {
|
||||
steps: ApprovalRequestStep[];
|
||||
currentStep: number;
|
||||
}
|
||||
|
||||
// 단계 상태값을 단계 아이콘(승인=체크, 반려=X, 그 외=시계)으로 변환
|
||||
function stepIcon(status: string) {
|
||||
if (status === 'APPROVED') return <IconCheck size={14} style={{ color: COLOR_SUCCESS }} />;
|
||||
if (status === 'REJECTED') return <IconX size={14} style={{ color: COLOR_ERROR }} />;
|
||||
return <IconClock size={14} style={{ color: COLOR_PRIMARY }} />;
|
||||
}
|
||||
|
||||
// 단계 상태값을 antd Steps 가 이해하는 상태(finish/error/process/wait)로 변환
|
||||
function stepStatus(status: string): 'finish' | 'error' | 'process' | 'wait' {
|
||||
if (status === 'APPROVED') return 'finish';
|
||||
if (status === 'REJECTED') return 'error';
|
||||
|
||||
@@ -8,6 +8,9 @@ import { GRAY, RADIUS, SHADOW, COLOR_ERROR } from '@/theme/tokens';
|
||||
const { Text } = Typography;
|
||||
const { Dragger } = Upload;
|
||||
|
||||
// Props: 첨부파일이 어떤 대상에 묶이는지 식별
|
||||
// - refType: 대상 종류(예: 'CONTRACT', 'WITHDRAW' 등 업무 구분)
|
||||
// - refId: 그 대상의 PK
|
||||
interface Props {
|
||||
refType: string;
|
||||
refId: number;
|
||||
@@ -22,12 +25,14 @@ export default function AttachmentUpload({ refType, refId }: Props) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
// 현재 대상(refType+refId)에 달린 첨부파일 목록 조회 (refId 가 있어야 실행)
|
||||
const { data: files = [], isLoading } = useQuery({
|
||||
queryKey: ['attachments', refType, refId],
|
||||
queryFn: () => attachmentApi.list(refType, refId),
|
||||
enabled: !!refId,
|
||||
});
|
||||
|
||||
// 파일 업로드 — 진행률 콜백으로 Progress 갱신, 완료 후 목록 캐시 무효화로 재조회
|
||||
const handleUpload = async (file: File) => {
|
||||
setUploading(true);
|
||||
setProgress(0);
|
||||
@@ -44,6 +49,7 @@ export default function AttachmentUpload({ refType, refId }: Props) {
|
||||
return false; // prevent antd auto-upload
|
||||
};
|
||||
|
||||
// 첨부파일 삭제 후 목록 재조회
|
||||
const handleDelete = async (id: number, name: string) => {
|
||||
try {
|
||||
await attachmentApi.delete(id);
|
||||
@@ -54,6 +60,7 @@ export default function AttachmentUpload({ refType, refId }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
// 바이트 크기를 사람이 읽기 쉬운 단위(B/KB/MB)로 변환
|
||||
const fmtSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes}B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
||||
|
||||
@@ -13,6 +13,7 @@ const { Text } = Typography;
|
||||
export default function NotificationBell() {
|
||||
const qc = useQueryClient();
|
||||
|
||||
// 미읽음 개수 — 30초마다 자동 재조회(refetchInterval)하여 뱃지 숫자를 갱신
|
||||
const { data: count = 0 } = useQuery({
|
||||
queryKey: ['notifications', 'unread-count'],
|
||||
queryFn: notificationApi.unreadCount,
|
||||
@@ -26,11 +27,13 @@ export default function NotificationBell() {
|
||||
|
||||
const notifications = page?.list ?? [];
|
||||
|
||||
// 알림 1건 읽음 처리 후, 캐시 무효화로 카운트/목록을 다시 불러온다
|
||||
const handleRead = async (id: number) => {
|
||||
await notificationApi.markRead(id);
|
||||
qc.invalidateQueries({ queryKey: ['notifications'] });
|
||||
};
|
||||
|
||||
// 전체 읽음 처리
|
||||
const handleReadAll = async () => {
|
||||
await notificationApi.markAllRead();
|
||||
qc.invalidateQueries({ queryKey: ['notifications'] });
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { Select, SelectProps } from 'antd';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
|
||||
// Props: antd Select props(options 제외 — 내부에서 자동 생성) + 공통코드 관련 옵션
|
||||
// - groupCode: 불러올 공통코드 그룹(예: 'INSURANCE_TYPE')
|
||||
// - includeAll: 맨 앞에 '전체' 선택지를 넣을지 여부(주로 검색 필터에서 사용)
|
||||
// - allLabel: '전체' 선택지의 라벨 텍스트
|
||||
interface Props extends Omit<SelectProps, 'options'> {
|
||||
groupCode: string;
|
||||
includeAll?: boolean;
|
||||
allLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 드롭다운 — 보험종류/계약상태 등 코드성 값을 고르는 곳마다 재사용된다.
|
||||
* useCommonCodes 로 그룹별 코드 목록을 받아 Select 의 options 로 자동 변환한다.
|
||||
* 화면마다 코드 목록을 직접 fetch/가공하지 않도록 공통화했다.
|
||||
*/
|
||||
export default function CodeSelect({ groupCode, includeAll, allLabel = '전체', ...rest }: Props) {
|
||||
const { data = [], isLoading } = useCommonCodes(groupCode);
|
||||
const options = [
|
||||
|
||||
@@ -5,11 +5,17 @@ interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// State: 하위 렌더링 중 오류가 났는지(hasError)와 그 오류 객체(error)를 보관
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 경계(Error Boundary) — MainLayout 의 <Outlet> 을 감싸 화면 렌더링 중 예외를 잡는다.
|
||||
* 한 페이지에서 에러가 나도 앱 전체가 하얗게 죽지 않고 "다시 시도" 화면을 보여준다.
|
||||
* React 의 에러 경계는 함수 컴포넌트로 만들 수 없어 클래스 컴포넌트로 작성한다.
|
||||
*/
|
||||
export default class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
@@ -2,6 +2,10 @@ import { Button, ButtonProps, message } from 'antd';
|
||||
import { DownloadOutlined } from '@ant-design/icons';
|
||||
import api from '@/api/request';
|
||||
|
||||
// Props: antd Button props(onClick 제외) + 다운로드에 필요한 값
|
||||
// - url: 엑셀을 내려주는 백엔드 API 경로
|
||||
// - params: 현재 검색조건(어떤 SearchParam VO 든 받을 수 있도록 unknown record)
|
||||
// - fileName: 저장될 파일 이름(확장자 .xlsx 는 자동으로 붙음)
|
||||
interface Props extends Omit<ButtonProps, 'onClick'> {
|
||||
url: string;
|
||||
// 어떤 SearchParam VO 든 받을 수 있도록 unknown record
|
||||
@@ -10,9 +14,12 @@ interface Props extends Omit<ButtonProps, 'onClick'> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 대용량 엑셀 다운로드. axios responseType=blob, content-disposition 무시하고 fileName 강제.
|
||||
* 엑셀 내보내기 공통 버튼 — 목록 화면들의 "엑셀" 다운로드 버튼에서 재사용된다.
|
||||
* 응답을 blob 으로 받아(브라우저 메모리상 임시 파일) <a download> 클릭을 흉내내 저장한다.
|
||||
* 서버가 내려주는 content-disposition 파일명을 무시하고 props 의 fileName 으로 강제 통일한다.
|
||||
*/
|
||||
export default function ExcelExportButton({ url, params, fileName, ...rest }: Props) {
|
||||
// 버튼 클릭 시: 검색조건을 그대로 쿼리스트링으로 보내 엑셀 blob 을 받아 내려받는다
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
const res = await api.get(url, { params, responseType: 'blob' });
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { Button, ButtonProps } from 'antd';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
|
||||
// Props: antd Button 의 모든 props 를 그대로 받으면서(menuCode/permCode 추가)
|
||||
// - menuCode: 권한을 확인할 메뉴 식별자 (예: 'PRODUCT_COMPANY')
|
||||
// - permCode: 이 버튼이 요구하는 권한 종류
|
||||
interface Props extends ButtonProps {
|
||||
menuCode: string;
|
||||
permCode: 'READ' | 'CREATE' | 'UPDATE' | 'DELETE' | 'APPROVE' | 'EXPORT' | 'EXECUTE';
|
||||
}
|
||||
|
||||
/** 권한 없으면 렌더링 자체를 안 함 */
|
||||
/**
|
||||
* 권한 기반 버튼 — 거의 모든 화면의 등록/수정/삭제/실행 버튼에서 재사용된다.
|
||||
* usePermission 으로 현재 사용자가 해당 menuCode 에 permCode 권한을 가졌는지 확인하고,
|
||||
* 권한이 없으면 null 을 반환해 버튼 자체를 화면에 그리지 않는다(숨김 처리).
|
||||
* 권한 체크를 화면마다 if 문으로 흩뿌리지 않으려고 공통화했다.
|
||||
*/
|
||||
export default function PermissionButton({ menuCode, permCode, children, ...rest }: Props) {
|
||||
const perms = usePermission(menuCode);
|
||||
const allowed =
|
||||
|
||||
@@ -15,7 +15,14 @@ interface Props {
|
||||
onComplete?: (result: UploadResultResp) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 기반 엑셀/CSV 업로드 공통 컴포넌트 — 대량 데이터 적재 화면에서 재사용된다.
|
||||
* 흐름: 업로드 템플릿 선택 → 파일 드래그/선택 → 서버 업로드 → 결과(성공/오류 건수) 모달 표시.
|
||||
* 업로드 UI/진행률/결과 표시 로직을 매 화면에 중복 작성하지 않으려고 공통화했다.
|
||||
*/
|
||||
export default function TemplateUpload({ category, onComplete }: Props) {
|
||||
// templateCode: 선택한 업로드 템플릿 / uploading: 업로드 진행 중 여부
|
||||
// progress: 진행률(0~100) / result: 서버가 준 업로드 결과 / resultVisible: 결과 모달 표시 여부
|
||||
const [templateCode, setTemplateCode] = useState<string | undefined>();
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
@@ -41,6 +48,7 @@ export default function TemplateUpload({ category, onComplete }: Props) {
|
||||
label: `[${t.category}] ${t.templateName}`,
|
||||
}));
|
||||
|
||||
// 파일을 골랐을 때 실행 — 템플릿 미선택이면 막고, 선택됐으면 서버로 업로드한다
|
||||
const handleUpload = async (file: RcFile): Promise<false> => {
|
||||
if (!templateCode) {
|
||||
message.warning('템플릿을 먼저 선택하세요');
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// 좌측 메뉴 접힘 상태를 저장할 localStorage 키(새로고침 후에도 유지)
|
||||
const COLLAPSE_KEY = 'ga-sider-collapsed';
|
||||
|
||||
/** API 메뉴 트리 → ProLayout MenuDataItem */
|
||||
@@ -38,6 +39,11 @@ function readCollapsed(): boolean {
|
||||
try { return localStorage.getItem(COLLAPSE_KEY) === 'true'; } catch { return false; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 메인 레이아웃 — 로그인 후 모든 화면을 감싸는 공통 틀.
|
||||
* 좌측 사이드 메뉴 + 상단 헤더(정산월 선택/알림벨/사용자 메뉴) + 본문(<Outlet/>) 구성.
|
||||
* 메뉴 트리는 /menu/my API 로 받아오므로 사용자 권한에 따라 보이는 메뉴가 달라진다.
|
||||
*/
|
||||
export default function MainLayout() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@@ -50,11 +56,13 @@ export default function MainLayout() {
|
||||
try { localStorage.setItem(COLLAPSE_KEY, String(collapsed)); } catch { /* ignore */ }
|
||||
}, [collapsed]);
|
||||
|
||||
// 현재 사용자가 접근 가능한 메뉴 트리 조회
|
||||
const { data: menus = [] } = useQuery({
|
||||
queryKey: ['menu', 'my'],
|
||||
queryFn: menuApi.myMenus,
|
||||
});
|
||||
|
||||
// 로그아웃 — 서버 세션 종료 시도 후 토큰/스토어 정리하고 로그인 페이지로 이동
|
||||
const handleLogout = async () => {
|
||||
try { await authApi.logout(); } catch { /* ignore */ }
|
||||
localStorage.removeItem('accessToken');
|
||||
|
||||
@@ -13,13 +13,24 @@ import {
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// 앱의 진입점(entry point). 여기서 React 트리를 실제 DOM 에 붙인다.
|
||||
// 전역 Provider 들을 한 곳에 모아 감싼다:
|
||||
// ConfigProvider(antd 한국어 locale + 디자인 테마)
|
||||
// → QueryClientProvider(React Query 서버상태 캐시)
|
||||
// → BrowserRouter(주소 기반 라우팅)
|
||||
// → App(실제 라우트 정의)
|
||||
// ──────────────────────────────────────────────────────
|
||||
|
||||
// 토스트 메시지 전역 설정 (top-right, 최대 5개, 3초)
|
||||
message.config({ top: 16, maxCount: 5, duration: 3 });
|
||||
|
||||
// React Query 전역 설정 — 실패 시 1회 재시도, 30초간은 캐시를 신선한 것으로 간주
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: 1, staleTime: 30_000 } },
|
||||
});
|
||||
|
||||
// antd 전역 테마 — theme/tokens.ts 의 색상/폰트/반경 값을 antd 디자인 토큰에 주입한다
|
||||
const theme: ThemeConfig = {
|
||||
token: {
|
||||
// ── 브랜드 — 토스 블루 ──
|
||||
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
} from '@/api/accounting-close';
|
||||
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||
|
||||
// 숫자 → 천단위 콤마 문자열(값 없으면 '-'). AG-Grid valueFormatter/Table render 에 공용
|
||||
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||
|
||||
// AG-Grid 메인 그리드(결산 목록) 컬럼 정의
|
||||
const COLUMNS: ColDef<AccountingCloseResp>[] = [
|
||||
{ field: 'closePeriod', headerName: '결산기간', width: 120, pinned: 'left' },
|
||||
{ field: 'closeTypeName', headerName: '결산구분', width: 100 },
|
||||
@@ -40,8 +42,13 @@ const SNAPSHOT_COLUMNS = [
|
||||
|
||||
const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm };
|
||||
|
||||
/**
|
||||
* 회계 결산 화면 — 월/분기/연 결산 생성·마감·재오픈, 계정별 잔액 스냅샷 조회.
|
||||
* API: accountingCloseApi.list/createDraft/close/reopen/snapshots
|
||||
* params(검색조건)가 바뀌면 useQuery 가 자동으로 목록을 다시 불러온다.
|
||||
*/
|
||||
export default function AccountingCloseList() {
|
||||
const qc = useQueryClient();
|
||||
const qc = useQueryClient(); // 액션 성공 후 invalidateQueries 로 목록 갱신용
|
||||
const [params, setParams] = useState<AccountingCloseSearchParam>({ pageSize: 200 });
|
||||
const [selectedRows, setSelectedRows] = useState<AccountingCloseResp[]>([]);
|
||||
const [draftOpen, setDraftOpen] = useState(false);
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
/**
|
||||
* [시상] 화면
|
||||
* - 업무: 설계사에게 주는 시상(인센티브)의 "룰"을 관리하고, 실제 지급된 "원장"을 조회/환수하는 화면.
|
||||
* 상단 탭 2개: (1) 시상 룰(등록/수정/삭제) (2) 원장(월별 지급 내역 조회 + 환수처리).
|
||||
* - 호출 API: commissionApi.awardRules / awardRuleCreate·Update·Delete (룰 CRUD),
|
||||
* awardLedgers (원장 조회), awardLedgerClawback (원장 환수처리).
|
||||
* - 주요 상태: ledgerParams(원장 검색조건), ruleModalOpen/editingRule(룰 모달), clawbackTarget(환수 대상 원장).
|
||||
* - 도메인 용어:
|
||||
* - 시상: 일정 실적/유지율을 달성하면 추가로 주는 포상금.
|
||||
* - 환수(clawback): 이미 지급한 시상금을 사후 사유(계약 해지 등)로 일부/전부 회수하는 것.
|
||||
* - 정률(RATE)/정액(FIXED): 시상금을 실적의 비율로 줄지, 고정 금액으로 줄지 계산 방식.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { Alert, Form, Input, InputNumber, Modal, Select, Space, Tabs, message } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
@@ -17,8 +29,10 @@ import {
|
||||
} from '@/api/commission';
|
||||
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||
|
||||
// 숫자 천단위 콤마, 없으면 '-'
|
||||
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||
|
||||
// 시상유형 선택 옵션(룰 등록 모달의 드롭다운)
|
||||
const AWARD_TYPE_OPTIONS = [
|
||||
{ value: 'NEW_RECRUIT', label: '신인도입' },
|
||||
{ value: 'CORPORATE', label: '법인' },
|
||||
@@ -26,6 +40,7 @@ const AWARD_TYPE_OPTIONS = [
|
||||
{ value: 'PERFORMANCE', label: '실적' },
|
||||
];
|
||||
|
||||
// "시상 룰" 탭 그리드 컬럼: 어떤 조건(기준실적)일 때 얼마(시상율/시상정액)를 주는지의 규칙 목록
|
||||
const RULE_COLS: ColDef<AwardRuleRow>[] = [
|
||||
{ field: 'awardType', headerName: '시상유형', width: 130 },
|
||||
{ field: 'calcType', headerName: '계산방식', width: 110 },
|
||||
@@ -36,6 +51,7 @@ const RULE_COLS: ColDef<AwardRuleRow>[] = [
|
||||
{ field: 'effectiveTo', headerName: '적용종료', width: 120 },
|
||||
];
|
||||
|
||||
// "원장" 탭 그리드 컬럼: 설계사별·월별 실제 시상 지급 내역(시상금 - 환수액 = 실지급)
|
||||
const LEDGER_COLS: ColDef<AwardLedgerRow>[] = [
|
||||
{ field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' },
|
||||
{ field: 'agentName', headerName: '설계사', width: 130 },
|
||||
@@ -59,6 +75,7 @@ export default function Award() {
|
||||
const [clawbackTarget, setClawbackTarget] = useState<AwardLedgerRow | null>(null);
|
||||
const [clawbackForm] = Form.useForm();
|
||||
|
||||
// 원장 환수처리 mutation: 성공 시 토스트 + 원장 목록 새로고침 + 모달 닫기
|
||||
const clawbackMutation = useMutation({
|
||||
mutationFn: ({ ledgerId, body }: { ledgerId: number; body: AwardClawbackReq }) =>
|
||||
commissionApi.awardLedgerClawback(ledgerId, body),
|
||||
@@ -70,17 +87,20 @@ export default function Award() {
|
||||
},
|
||||
});
|
||||
|
||||
// 환수 모달의 "처리" 버튼: 폼 검증 통과 후 환수 mutation 실행
|
||||
const handleClawbackOk = async () => {
|
||||
const values = await clawbackForm.validateFields();
|
||||
clawbackMutation.mutate({ ledgerId: clawbackTarget!.ledgerId, body: values });
|
||||
};
|
||||
|
||||
// 시상 룰 목록 조회(읽기)
|
||||
const { data: rulesData, isLoading: rulesLoading, isError: rulesError } = useQuery({
|
||||
queryKey: ['award', 'rules'],
|
||||
queryFn: () => commissionApi.awardRules(),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// 원장(지급 내역) 목록 조회. 검색조건(ledgerParams) 변경 시 자동 재조회
|
||||
const { data: ledgersData, isLoading: ledgersLoading } = useQuery({
|
||||
queryKey: ['award', 'ledgers', ledgerParams],
|
||||
queryFn: () => commissionApi.awardLedgers(ledgerParams),
|
||||
@@ -90,9 +110,11 @@ export default function Award() {
|
||||
const rules = rulesData?.list ?? [];
|
||||
const ledgers = ledgersData?.list ?? [];
|
||||
|
||||
// 룰 등록 모달 열기(빈 폼) / 룰 수정 모달 열기(선택 행 값 채움)
|
||||
const openCreate = () => { setEditingRule(null); form.resetFields(); setRuleModalOpen(true); };
|
||||
const openEdit = (row: AwardRuleRow) => { setEditingRule(row); form.setFieldsValue(row); setRuleModalOpen(true); };
|
||||
|
||||
// 룰 저장: editingRule 유무로 수정/등록 분기. 완료 후 룰 목록 새로고침
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields();
|
||||
try {
|
||||
@@ -108,6 +130,7 @@ export default function Award() {
|
||||
} catch { /* request.ts 처리 */ }
|
||||
};
|
||||
|
||||
// 룰 삭제: 확인창 후 삭제 API 호출, 목록 새로고침
|
||||
const handleDelete = (id: number) => Modal.confirm({
|
||||
title: '룰 삭제', content: '삭제하시겠습니까?',
|
||||
onOk: async () => {
|
||||
@@ -117,6 +140,7 @@ export default function Award() {
|
||||
},
|
||||
});
|
||||
|
||||
// 원장 그리드 하단 고정 합계 행(기준실적/시상금/환수액/실지급 각 합계)
|
||||
const pinnedLedger = ledgers.length > 0 ? {
|
||||
agentName: '합계',
|
||||
baseAmount: ledgers.reduce((s, r) => s + (r.baseAmount ?? 0), 0),
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
/**
|
||||
* [지점장수당] 화면
|
||||
* - 업무: 지점장(조직장)에게 조직 실적에 비례해 주는 수당의 "룰"을 관리하고, 월별 지급 "원장"을 조회하는 화면.
|
||||
* 상단 탭 2개: (1) 수당 룰(등록/수정/삭제) (2) 원장(조회 전용).
|
||||
* - 호출 API: commissionApi.branchAllowanceRules / ...RuleCreate·Update·Delete (룰 CRUD),
|
||||
* branchAllowanceLedgers (원장 조회).
|
||||
* - 주요 상태: ledgerParams(원장 검색조건), ruleModalOpen/editingRule(룰 모달).
|
||||
* - 도메인 용어:
|
||||
* - 지점장수당: 본인 계약이 아니라 자기 조직(지점)의 전체 실적에 대해 직책자가 받는 수당.
|
||||
* - 수당율(allowanceRate): 조직실적에 곱하는 비율(0~1, 화면엔 %로 표시).
|
||||
* - 상한(capAmount): 수당의 최대 한도. 미입력이면 무제한.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { Alert, Form, Input, InputNumber, Modal, Space, Tabs, message } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
@@ -16,9 +28,12 @@ import {
|
||||
} from '@/api/commission';
|
||||
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||
|
||||
// 숫자 천단위 콤마, 없으면 '-'
|
||||
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||
// 비율(0~1)을 퍼센트 문자열로(예: 0.035 -> "3.50%")
|
||||
const fmtRate = (v: unknown) => v != null ? `${((v as number) * 100).toFixed(2)}%` : '-';
|
||||
|
||||
// "수당 룰" 탭 그리드 컬럼: 직책별 수당율/상한/적용기간 규칙
|
||||
const RULE_COLS: ColDef<BranchAllowanceRuleRow>[] = [
|
||||
{ field: 'positionCode', headerName: '직책코드', width: 130 },
|
||||
{ field: 'allowanceRate', headerName: '수당율', width: 110, type: 'numericColumn', valueFormatter: (p) => fmtRate(p.value) },
|
||||
@@ -27,6 +42,7 @@ const RULE_COLS: ColDef<BranchAllowanceRuleRow>[] = [
|
||||
{ field: 'effectiveTo', headerName: '적용종료', width: 120 },
|
||||
];
|
||||
|
||||
// "원장" 탭 그리드 컬럼: 지점장별·월별 실제 수당 지급 내역(조직실적 x 수당율 = 수당금액, 환수 반영)
|
||||
const LEDGER_COLS: ColDef<BranchAllowanceLedgerRow>[] = [
|
||||
{ field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' },
|
||||
{ field: 'managerAgentName',headerName: '지점장', width: 130 },
|
||||
@@ -47,12 +63,14 @@ export default function BranchAllowance() {
|
||||
const [editingRule, setEditingRule] = useState<BranchAllowanceRuleRow | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 수당 룰 목록 조회(읽기)
|
||||
const { data: rulesData, isLoading: rulesLoading, isError: rulesError } = useQuery({
|
||||
queryKey: ['branchAllowance', 'rules'],
|
||||
queryFn: () => commissionApi.branchAllowanceRules(),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// 원장(지급 내역) 목록 조회. 검색조건(ledgerParams) 변경 시 자동 재조회
|
||||
const { data: ledgersData, isLoading: ledgersLoading } = useQuery({
|
||||
queryKey: ['branchAllowance', 'ledgers', ledgerParams],
|
||||
queryFn: () => commissionApi.branchAllowanceLedgers(ledgerParams),
|
||||
@@ -62,9 +80,11 @@ export default function BranchAllowance() {
|
||||
const rules = rulesData?.list ?? [];
|
||||
const ledgers = ledgersData?.list ?? [];
|
||||
|
||||
// 룰 등록 모달 열기(빈 폼) / 룰 수정 모달 열기(선택 행 값 채움)
|
||||
const openCreate = () => { setEditingRule(null); form.resetFields(); setRuleModalOpen(true); };
|
||||
const openEdit = (row: BranchAllowanceRuleRow) => { setEditingRule(row); form.setFieldsValue(row); setRuleModalOpen(true); };
|
||||
|
||||
// 룰 저장: editingRule 유무로 수정/등록 분기. 완료 후 룰 목록 새로고침
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields();
|
||||
try {
|
||||
@@ -80,6 +100,7 @@ export default function BranchAllowance() {
|
||||
} catch { /* request.ts 처리 */ }
|
||||
};
|
||||
|
||||
// 룰 삭제: 확인창 후 삭제 API 호출, 목록 새로고침
|
||||
const handleDelete = (id: number) => Modal.confirm({
|
||||
title: '룰 삭제', content: '삭제하시겠습니까?',
|
||||
onOk: async () => {
|
||||
@@ -89,6 +110,7 @@ export default function BranchAllowance() {
|
||||
},
|
||||
});
|
||||
|
||||
// 원장 그리드 하단 고정 합계 행(조직실적/수당금액/환수액 각 합계)
|
||||
const pinnedLedger = ledgers.length > 0 ? {
|
||||
managerAgentName: '합계',
|
||||
baseAmount: ledgers.reduce((s, r) => s + (r.baseAmount ?? 0), 0),
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/**
|
||||
* [환수감면신청] 화면
|
||||
* - 업무: 설계사에게 부과된 "환수액"의 일부/전부를 감면해 달라는 신청을 등록하고, 관리자가 승인/반려하는 워크플로 화면.
|
||||
* - 호출 API: commissionApi.chargebackWaiverList(조회), chargebackWaiverCreate(신청),
|
||||
* chargebackWaiverApprove(승인), chargebackWaiverReject(반려).
|
||||
* - 주요 상태: searchParams(검색조건), createOpen(신청 모달), rejectTarget/rejectReason(반려 모달).
|
||||
* - 도메인 용어:
|
||||
* - 환수: 계약 해지 등으로 이미 지급한 수수료를 회수하는 것.
|
||||
* - 감면(waiver): 환수액 중 일부를 사정 참작해 면제해 주는 것.
|
||||
* - 승인상태: 신청(REQUESTED) -> 승인(APPROVED) 또는 반려(REJECTED).
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { Alert, Form, Input, InputNumber, Modal, Select, Space, message } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
@@ -24,6 +35,7 @@ const STATUS_OPTIONS = [
|
||||
{ value: 'REJECTED', label: '반려' },
|
||||
];
|
||||
|
||||
// 그리드 컬럼: 감면 신청 목록(원환수액 대비 감면액, 사유, 승인상태)
|
||||
const COLS: ColDef<ChargebackWaiverRow>[] = [
|
||||
{ field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' },
|
||||
{ field: 'agentName', headerName: '설계사', width: 130 },
|
||||
@@ -45,6 +57,7 @@ export default function ChargebackWaiver() {
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [form] = Form.useForm<ChargebackWaiverSaveReq>();
|
||||
|
||||
// 감면 신청 목록 조회(읽기). 검색조건 변경 시 자동 재조회
|
||||
const { data: listData, isLoading, isError } = useQuery({
|
||||
queryKey: ['chargebackWaiver', 'list', searchParams],
|
||||
queryFn: () => commissionApi.chargebackWaiverList(searchParams),
|
||||
@@ -53,6 +66,7 @@ export default function ChargebackWaiver() {
|
||||
|
||||
const rows = listData?.list ?? [];
|
||||
|
||||
// 감면 신청 등록 mutation: 성공 시 목록 새로고침 + 모달 닫기
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (body: ChargebackWaiverSaveReq) => commissionApi.chargebackWaiverCreate(body),
|
||||
onSuccess: () => {
|
||||
@@ -63,6 +77,7 @@ export default function ChargebackWaiver() {
|
||||
},
|
||||
});
|
||||
|
||||
// 신청 승인 mutation
|
||||
const approveMutation = useMutation({
|
||||
mutationFn: (requestId: number) => commissionApi.chargebackWaiverApprove(requestId),
|
||||
onSuccess: () => {
|
||||
@@ -71,6 +86,7 @@ export default function ChargebackWaiver() {
|
||||
},
|
||||
});
|
||||
|
||||
// 신청 반려 mutation(사유 필요)
|
||||
const rejectMutation = useMutation({
|
||||
mutationFn: ({ requestId, reason }: { requestId: number; reason: string }) =>
|
||||
commissionApi.chargebackWaiverReject(requestId, reason),
|
||||
@@ -82,22 +98,26 @@ export default function ChargebackWaiver() {
|
||||
},
|
||||
});
|
||||
|
||||
// 신청 모달 "신청" 버튼: 폼 검증 후 등록
|
||||
const handleCreate = async () => {
|
||||
const values = await form.validateFields();
|
||||
createMutation.mutate(values);
|
||||
};
|
||||
|
||||
// 승인 버튼: 확인창 후 승인 처리
|
||||
const handleApprove = (row: ChargebackWaiverRow) => Modal.confirm({
|
||||
title: '승인',
|
||||
content: `${row.agentName} 감면 신청(감면액: ${fmt(row.waiverAmount)})을 승인하시겠습니까?`,
|
||||
onOk: () => approveMutation.mutateAsync(row.requestId),
|
||||
});
|
||||
|
||||
// 반려 모달 열기(대상 행 지정)
|
||||
const openReject = (row: ChargebackWaiverRow) => {
|
||||
setRejectTarget(row);
|
||||
setRejectReason('');
|
||||
};
|
||||
|
||||
// 반려 확정: 사유 필수 검증 후 반려 처리
|
||||
const handleReject = () => {
|
||||
if (!rejectReason.trim()) { message.warning('반려 사유를 입력하세요'); return; }
|
||||
rejectMutation.mutate({ requestId: rejectTarget!.requestId, reason: rejectReason });
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/**
|
||||
* [환수분할상환] 화면
|
||||
* - 업무: 금액이 큰 환수채권을 한 번에 갚기 어려울 때 여러 회차로 나눠 납부하는 "분할 계획"을 생성/조회/취소하는 화면.
|
||||
* - 호출 API: commissionApi.clawbackInstallmentList(조회), clawbackInstallmentGenerate(분할계획 생성),
|
||||
* clawbackInstallmentCancel(회차 취소).
|
||||
* - 주요 상태: searchParams(검색조건), generateOpen(분할계획 생성 모달).
|
||||
* - 도메인 용어:
|
||||
* - 환수채권(receivable): 설계사가 GA에 갚아야 할 환수 잔액.
|
||||
* - 분할상환: 채권을 N회차로 나눠 매월 일정액(분할금액)을 회수.
|
||||
* - 회차 상태: SCHEDULED(예정) -> 회수 완료. SCHEDULED 상태만 취소 가능.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { Alert, Form, Input, InputNumber, Modal, Space, message } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
@@ -22,6 +33,7 @@ interface SearchParam {
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
// 그리드 컬럼: 채권별 분할 회차 목록(회차/분할금액/납부월/상태)
|
||||
const COLS: ColDef<ClawbackInstallmentRow>[] = [
|
||||
{ field: 'receivableId', headerName: '채권ID', width: 110, pinned: 'left' },
|
||||
{ field: 'installmentNo', headerName: '회차', width: 80, type: 'numericColumn' },
|
||||
@@ -37,6 +49,7 @@ export default function ClawbackInstallment() {
|
||||
const [generateOpen, setGenerateOpen] = useState(false);
|
||||
const [form] = Form.useForm<ClawbackInstallmentGenerateReq>();
|
||||
|
||||
// 분할 회차 목록 조회(읽기)
|
||||
const { data: listData, isLoading, isError } = useQuery({
|
||||
queryKey: ['clawbackInstallment', 'list', searchParams],
|
||||
queryFn: () => commissionApi.clawbackInstallmentList(searchParams),
|
||||
@@ -45,6 +58,7 @@ export default function ClawbackInstallment() {
|
||||
|
||||
const rows = listData?.list ?? [];
|
||||
|
||||
// 분할 계획 생성 mutation: 채권을 N회차로 쪼개 일괄 생성. 성공 시 생성 건수 안내 + 목록 새로고침
|
||||
const generateMutation = useMutation({
|
||||
mutationFn: (body: ClawbackInstallmentGenerateReq) =>
|
||||
commissionApi.clawbackInstallmentGenerate(body),
|
||||
@@ -56,6 +70,7 @@ export default function ClawbackInstallment() {
|
||||
},
|
||||
});
|
||||
|
||||
// 개별 회차 취소 mutation
|
||||
const cancelMutation = useMutation({
|
||||
mutationFn: (planId: number) => commissionApi.clawbackInstallmentCancel(planId),
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/**
|
||||
* [환수채권관리] 화면
|
||||
* - 업무: 회수하지 못한 환수금(미수금) 채권을 목록/상세로 보고, 직접 상환 완료나 대손(회수 포기) 처리를 하는 화면.
|
||||
* 행 클릭 시 오른쪽 Drawer로 상세(회수 이력 타임라인 포함)를 본다.
|
||||
* - 호출 API: clawbackReceivableApi.list(목록), detail(상세), writeOff(대손/상환 처리), export(엑셀 다운로드).
|
||||
* - 주요 상태: params(검색조건), selectedRow/drawerOpen(상세 Drawer), writeOffOpen(처리 모달).
|
||||
* - 도메인 용어:
|
||||
* - 환수채권: 설계사에게 받아야 할 환수 잔액. 상태: 미회수/회수중/상환완료/대손.
|
||||
* - 연체이자: 미회수 기간에 대해 가산되는 이자. 잔액 = 원금 - 회수액 + 연체이자.
|
||||
* - 대손(write-off): 회수 불가능으로 판단해 채권을 손실로 처리하는 것.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Drawer, Form, Input, Modal, Select, Space, Tag, Timeline, Typography, message,
|
||||
@@ -37,6 +48,7 @@ const STATUS_LABEL: Record<string, string> = {
|
||||
WRITTEN_OFF: '대손',
|
||||
};
|
||||
|
||||
// 상태 값을 색상 칩(Tag)으로 표시. 색상은 STATUS_COLOR, 라벨은 서버값(statusName) 우선
|
||||
function StatusChip({ status, statusName }: { status: string; statusName?: string }) {
|
||||
const color = STATUS_COLOR[status] ?? GRAY[400];
|
||||
const label = statusName ?? STATUS_LABEL[status] ?? status;
|
||||
@@ -153,6 +165,7 @@ export default function ClawbackReceivable() {
|
||||
setDrawerOpen(true);
|
||||
};
|
||||
|
||||
// 엑셀 다운로드: 현재 검색조건으로 받은 Blob을 a 태그 클릭으로 파일 저장
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const res = await clawbackReceivableApi.export(params);
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/**
|
||||
* [수금수수료] 화면
|
||||
* - 업무: 보험료 수납(수금) 실적에 대해 주는 수수료의 "룰"을 관리하고, 월별 지급 "원장"을 조회하는 화면.
|
||||
* 상단 탭 2개: (1) 수수료 룰(등록/수정/삭제) (2) 수금 원장(조회 전용).
|
||||
* - 호출 API: commissionApi.collectionRules / ...RuleCreate·Update·Delete (룰 CRUD),
|
||||
* collectionLedgers (원장 조회).
|
||||
* - 주요 상태: ledgerParams(원장 검색조건), ruleModalOpen/editingRule(룰 모달).
|
||||
* - 도메인 용어:
|
||||
* - 수금수수료: 계약 체결이 아니라 보험료를 실제로 거둬들인(수납) 것에 대해 지급하는 수수료.
|
||||
* - 룰은 보험사/상품유형/납입주기별로 수수료율을 정의한다.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { Alert, Button, Form, Input, InputNumber, Modal, Space, Tabs, message } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/**
|
||||
* [수수료 통합 마스터] 화면
|
||||
* - 업무: 한 설계사가 한 정산월에 받은 9종(신계약/유지/소개 등) 수수료를 한 줄로 합쳐 보여주는 통합 조회 화면.
|
||||
* - 호출 API: GET /api/commissions (commissionApi.masterList) — 조건(설계사명/정산월/수수료종류)으로 조회만 한다(읽기 전용).
|
||||
* - 주요 상태: params(검색조건). 화면 진입 시 이번 달 정산월로 200건 조회.
|
||||
* - 도메인 용어:
|
||||
* - 정산월(settleMonth): 수수료를 정산·지급하는 기준 월(YYYYMM).
|
||||
* - 총수수료(grossAmount): 세금/공제 전 받을 수수료 합.
|
||||
* - 실지급(netAmount): 환수·세금·공제를 뺀 실제 지급액.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { Alert } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
@@ -10,8 +20,10 @@ import DataGrid from '@/components/common/DataGrid';
|
||||
import { commissionApi, CommissionMasterRow, CommissionSearchParam } from '@/api/commission';
|
||||
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||
|
||||
// 숫자에 천단위 콤마(예: 1234567 -> "1,234,567"), 값이 없으면 '-' 표시
|
||||
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||
|
||||
// AG Grid 컬럼 정의: pinned: 'left'는 가로 스크롤해도 왼쪽에 고정. type:'numericColumn'은 우측 정렬 숫자 컬럼.
|
||||
const COLUMNS: ColDef<CommissionMasterRow>[] = [
|
||||
{ field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' },
|
||||
{ field: 'agentName', headerName: '설계사', width: 120, pinned: 'left' },
|
||||
@@ -27,6 +39,7 @@ export default function CommissionMaster() {
|
||||
pageSize: 200,
|
||||
});
|
||||
|
||||
// 서버에서 통합 마스터 목록 조회. params가 바뀌면 react-query가 자동 재조회. retry:false = 실패 시 재시도 안 함(에러 즉시 노출).
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['commission', 'master', params],
|
||||
queryFn: () => commissionApi.masterList(params),
|
||||
@@ -35,6 +48,7 @@ export default function CommissionMaster() {
|
||||
|
||||
const rows = data?.list ?? [];
|
||||
|
||||
// 그리드 맨 아래에 고정으로 붙는 합계 행(총수수료/실지급의 전체 합). 데이터가 있을 때만 만든다.
|
||||
const pinnedRow = rows.length > 0 ? {
|
||||
agentName: '합계',
|
||||
commissionTypeName: '',
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/**
|
||||
* [수수료 시뮬레이터] 화면
|
||||
* - 업무: 보험료/보험사요율/지급률/회차를 입력하면 설계사가 받을 수수료·세금·실지급·예상환수를 미리 계산해보는 화면.
|
||||
* 계산 결과는 상단 통계 카드로, 과거 계산 이력은 하단 그리드로 보여준다.
|
||||
* - 호출 API: operationApi.simulate (계산 요청), operationApi.simulationList (저장된 시뮬 목록 조회).
|
||||
* - 주요 상태: form(입력 폼), result(가장 최근 계산 결과 1건).
|
||||
* - 도메인 용어:
|
||||
* - 보험사요율(companyRate): 보험사가 GA에 주는 수수료 비율.
|
||||
* - 지급률(payoutRate): GA가 받은 금액 중 설계사에게 지급하는 비율(분급률).
|
||||
* - 원천세 3.3%: 사업소득 원천징수 세율. 실지급 = 설계사수수료 - 원천세.
|
||||
* - 예상환수: 향후 계약 해지 시 되돌려받을 수 있는 추정 금액.
|
||||
* - 주의: 화면 입력은 % 단위지만 백엔드에는 0~1로 환산해 보낸다(handleSimulate 참고).
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { Alert, Card, Col, Form, Input, InputNumber, Row, Statistic, message } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
@@ -12,6 +25,7 @@ import { GRAY, RADIUS, SHADOW, COLOR_PRIMARY, COLOR_ERROR, COLOR_SUCCESS } from
|
||||
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||
const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm };
|
||||
|
||||
// 하단 "저장된 시뮬레이션 목록" 그리드 컬럼: 과거 계산 시나리오들의 입력값과 결과
|
||||
const HIST_COLS: ColDef<SimulationRow>[] = [
|
||||
{ field: 'simName', headerName: '시나리오명', width: 150 },
|
||||
{ field: 'premiumAmount', headerName: '보험료', width: 130, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
@@ -41,6 +55,7 @@ export default function CommissionSimulator() {
|
||||
|
||||
const rows = listData?.list ?? [];
|
||||
|
||||
// "계산" 버튼: 폼 검증 후 % 입력을 0~1로 환산해 백엔드 계산 요청 → 결과 카드 갱신 + 이력 새로고침
|
||||
const handleSimulate = async () => {
|
||||
const values = await form.validateFields();
|
||||
// UI: 율은 % 단위 → 백엔드는 0~1
|
||||
|
||||
@@ -15,6 +15,23 @@ import {
|
||||
} from '@/api/forecast-kpi';
|
||||
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||
|
||||
/**
|
||||
* [화면] 차월 KPI 예측 (ForecastKpiList)
|
||||
*
|
||||
* 용도: 과거 실적을 바탕으로 다음 달(차월)의 보험료/지급/유지율 같은 KPI를 예측한 결과를 보여준다.
|
||||
* 이동평균 등 통계 기법으로 산출하며, 예측값과 함께 신뢰구간(상·하한)을 제공한다.
|
||||
*
|
||||
* 도메인 용어:
|
||||
* - KPI: 핵심성과지표(매출/지급/유지율 등 경영지표).
|
||||
* - 예측 기법(method): SMA3/SMA6(3·6개월 이동평균), NAIVE(직전월 그대로).
|
||||
* - 상·하한(±1σ): 예측의 불확실성 범위(표준편차 1배). 표본수(sampleCount)가 많을수록 신뢰도 상승.
|
||||
* - 대상조직: 비어있으면 전사(全社) 기준.
|
||||
*
|
||||
* 호출 API (api/forecast-kpi.ts): forecastKpiApi.list(검색) / regenerate(재산출).
|
||||
* 주요 상태: 검색 파라미터, 재산출 모달.
|
||||
*/
|
||||
|
||||
// 숫자 표시 포맷터: null 이면 '-', 아니면 천단위 콤마.
|
||||
const fmt = (v: unknown) => (v == null ? '-' : (v as number).toLocaleString());
|
||||
|
||||
const COLUMNS: ColDef<ForecastKpiResp>[] = [
|
||||
|
||||
@@ -9,13 +9,21 @@ import PageContainer from '@/components/common/PageContainer';
|
||||
import PermissionButton from '@/components/common/PermissionButton';
|
||||
import { companyApi, CompanyResp, CompanySaveReq, CompanySearchParam } from '@/api/company';
|
||||
|
||||
// 보험사 구분 드롭다운 선택지(고정값이라 공통코드 대신 상수로 둠)
|
||||
const COMPANY_TYPE_OPTIONS = [
|
||||
{ value: 'LIFE', label: '생명보험' },
|
||||
{ value: 'NON_LIFE', label: '손해보험' },
|
||||
{ value: 'OTHER', label: '기타' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 보험사 관리 화면.
|
||||
* API: companyApi.list(목록) / create(등록) / update(수정)
|
||||
* ProTable 로 검색+목록을 그리고, ModalForm 으로 등록/수정 모달을 띄운다.
|
||||
*/
|
||||
export default function CompanyList() {
|
||||
// actionRef: ProTable 을 코드에서 제어(reload 등)하기 위한 참조
|
||||
// modal: 등록/수정 모달 열림 여부와 수정 대상 id(editId 있으면 수정, 없으면 신규)
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [modal, setModal] = useState<{ open: boolean; editId?: number }>({ open: false });
|
||||
|
||||
@@ -30,16 +38,19 @@ export default function CompanyList() {
|
||||
});
|
||||
const [editRecord, setEditRecord] = useState<CompanyResp | null>(null);
|
||||
|
||||
// 등록 모달 열기(수정 대상 없음)
|
||||
const openCreate = () => {
|
||||
setEditRecord(null);
|
||||
setModal({ open: true });
|
||||
};
|
||||
|
||||
// 수정 모달 열기(선택한 행을 초기값으로)
|
||||
const openEdit = (record: CompanyResp) => {
|
||||
setEditRecord(record);
|
||||
setModal({ open: true, editId: record.companyId });
|
||||
};
|
||||
|
||||
// 모달 제출 — editId 유무로 수정/등록을 분기, 성공 시 목록 새로고침
|
||||
const onSubmit = async (v: any) => {
|
||||
const req: CompanySaveReq = {
|
||||
...v,
|
||||
@@ -57,6 +68,7 @@ export default function CompanyList() {
|
||||
return true;
|
||||
};
|
||||
|
||||
// ProTable 컬럼 정의(search:false 면 검색폼에서 제외, 표에만 표시)
|
||||
const columns: ProColumns<CompanyResp>[] = [
|
||||
{ title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
|
||||
fieldProps: { placeholder: '회사명 / 사업자번호' } },
|
||||
|
||||
@@ -15,9 +15,14 @@ import { contractApi, ContractResp, ContractSaveReq, ContractSearchParam } from
|
||||
import { productApi } from '@/api/product';
|
||||
import { agentApi } from '@/api/agent';
|
||||
|
||||
/**
|
||||
* 계약 관리 화면.
|
||||
* API: contractApi.list/create, 등록 모달용 productApi.list / agentApi.list
|
||||
* 보험종류/계약상태/납입주기/판매채널은 공통코드로 불러와 CodeBadge·CodeSelect 에 사용한다.
|
||||
*/
|
||||
export default function ContractList() {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [open, setOpen] = useState(false); // 계약 등록 모달 열림 여부
|
||||
const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
|
||||
const { data: statusCodes = [] } = useCommonCodes('CONTRACT_STATUS');
|
||||
const { data: payCycleCodes = [] } = useCommonCodes('PAY_CYCLE');
|
||||
@@ -35,6 +40,7 @@ export default function ContractList() {
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
// 계약 등록 제출 — 날짜를 문자열로 변환 후 생성 API 호출, 성공 시 목록 새로고침
|
||||
const onSubmit = async (v: any) => {
|
||||
const req: ContractSaveReq = {
|
||||
...v,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
import { productApi, ProductResp, ProductSaveReq, ProductSearchParam } from '@/api/product';
|
||||
import { companyApi } from '@/api/company';
|
||||
|
||||
// 납입주기 드롭다운 선택지(고정값)
|
||||
const PAY_PERIOD_OPTIONS = [
|
||||
{ value: 'MONTHLY', label: '월납' },
|
||||
{ value: 'QUARTERLY', label: '분기납' },
|
||||
@@ -20,12 +21,18 @@ const PAY_PERIOD_OPTIONS = [
|
||||
{ value: 'SINGLE', label: '일시납' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 상품 관리 화면.
|
||||
* API: productApi.list/create/update/remove, companyApi.list(보험사 선택용)
|
||||
* 보험종류는 공통코드(INSURANCE_TYPE)로 불러온다.
|
||||
*/
|
||||
export default function ProductList() {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
|
||||
const [modal, setModal] = useState<{ open: boolean; editId?: number }>({ open: false });
|
||||
const [editRecord, setEditRecord] = useState<ProductResp | null>(null);
|
||||
|
||||
// 등록/수정 모달의 보험사 선택지 — 모달이 열렸을 때만(enabled) 조회
|
||||
const { data: companies } = useQuery({
|
||||
queryKey: ['companies', 'all'],
|
||||
queryFn: () => companyApi.list({ pageSize: 999 }),
|
||||
@@ -42,6 +49,7 @@ export default function ProductList() {
|
||||
setModal({ open: true, editId: record.productId });
|
||||
};
|
||||
|
||||
// 삭제 — 확인창 후 삭제 API 호출, 성공 시 목록 새로고침
|
||||
const onDelete = (record: ProductResp) => {
|
||||
Modal.confirm({
|
||||
title: '상품 삭제',
|
||||
@@ -57,6 +65,7 @@ export default function ProductList() {
|
||||
});
|
||||
};
|
||||
|
||||
// 모달 제출 — 날짜는 문자열로, 활성여부는 Y/N 으로 변환 후 editId 유무로 수정/등록 분기
|
||||
const onSubmit = async (v: any) => {
|
||||
const req: ProductSaveReq = {
|
||||
...v,
|
||||
|
||||
@@ -18,6 +18,29 @@ import { GRAY, RADIUS, SHADOW, COLOR_SUCCESS, COLOR_ERROR, COLOR_PRIMARY } from
|
||||
const { Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
/**
|
||||
* [화면] 결재 관리 (Approvals)
|
||||
*
|
||||
* 용도: 사내 전자결재 처리 화면. 결재선(승인 단계의 묶음) 조회, 내가 올린 결재 요청 확인,
|
||||
* 나에게 온 결재 대기 건의 승인/반려를 한 곳에서 처리한다.
|
||||
*
|
||||
* 도메인 용어:
|
||||
* - 결재선(Approval Line): "팀장 → 본부장 → 대표" 처럼 순서대로 승인하는 단계 정의.
|
||||
* - 결재 요청(Approval Request): 어떤 안건을 특정 결재선에 태워 올린 1건.
|
||||
* - 단계(Step): 결재선 안의 개별 승인자. currentStep/totalSteps 로 진행률 표시.
|
||||
*
|
||||
* 호출 API (api/approval.ts):
|
||||
* - approvalApi.lines() 결재선 목록
|
||||
* - approvalApi.myRequests() 내가 요청한 결재 목록
|
||||
* - approvalApi.pendingRequests() 내 결재 대기 목록
|
||||
* - approvalApi.requestDetail() 결재 상세(단계 진행 현황 포함)
|
||||
* - approvalApi.advance() 승인/반려 처리(다음 단계로 진행)
|
||||
* - approvalApi.requestCreate() 신규 결재 요청 등록
|
||||
*
|
||||
* 주요 상태: 상세 Drawer 열림 여부, 선택된 결재 건, 코멘트 입력값, 결재요청 모달 열림 여부.
|
||||
*/
|
||||
|
||||
// AG Grid 컬럼 정의: 결재선 목록 테이블의 표시 열(헤더명/너비/정렬).
|
||||
const LINE_COLS: ColDef<ApprovalLineRow>[] = [
|
||||
{ field: 'lineName', headerName: '결재선명', flex: 1 },
|
||||
{ field: 'description', headerName: '설명', flex: 1 },
|
||||
@@ -26,6 +49,7 @@ const LINE_COLS: ColDef<ApprovalLineRow>[] = [
|
||||
{ field: 'createdAt', headerName: '생성일', width: 120 },
|
||||
];
|
||||
|
||||
// AG Grid 컬럼 정의: 결재 요청(내 요청/대기 공용) 테이블의 표시 열. 진행/상태는 cellRenderer 로 커스텀.
|
||||
const REQ_COLS: ColDef<ApprovalRequestRow>[] = [
|
||||
{ field: 'title', headerName: '제목', flex: 1 },
|
||||
{ field: 'lineName', headerName: '결재선', width: 140 },
|
||||
@@ -70,10 +94,12 @@ export default function Approvals() {
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// 서버 응답이 없을 때를 대비해 빈 배열로 기본값 처리(undefined 방지).
|
||||
const lines = linesData?.list ?? [];
|
||||
const myRequests = myReqData?.list ?? [];
|
||||
const pendingRequests = pendingData?.list ?? [];
|
||||
|
||||
// 결재 대기 건을 클릭 → 상세를 다시 조회해 단계 진행현황까지 채운 뒤 Drawer 를 연다.
|
||||
const openDetail = async (req: ApprovalRequestRow) => {
|
||||
try {
|
||||
const detail = await approvalApi.requestDetail(req.requestId);
|
||||
@@ -82,6 +108,7 @@ export default function Approvals() {
|
||||
} catch { /* handled */ }
|
||||
};
|
||||
|
||||
// 승인/반려 버튼 처리: approved=true 승인, false 반려. 처리 후 결재 관련 캐시를 갱신한다.
|
||||
const handleAdvance = async (approved: boolean) => {
|
||||
if (!selectedRequest) return;
|
||||
try {
|
||||
@@ -93,6 +120,7 @@ export default function Approvals() {
|
||||
} catch { /* handled */ }
|
||||
};
|
||||
|
||||
// 신규 결재 요청 등록: 모달 폼 검증 통과 시 결재선 ID/제목/내용을 서버에 보낸다.
|
||||
const handleCreateReq = async () => {
|
||||
const values = await form.validateFields() as ApprovalRequestSaveReq;
|
||||
try {
|
||||
|
||||
@@ -12,8 +12,29 @@ import PermissionButton from '@/components/common/PermissionButton';
|
||||
import { commonCodeApi, CommonCodeGroup, CommonCode } from '@/api/user';
|
||||
|
||||
const { Text } = Typography;
|
||||
// 메뉴 코드: 권한(RBAC) 체크 기준. PermissionButton 이 이 코드+CREATE/UPDATE/DELETE 권한 보유 여부로 노출/비활성 결정.
|
||||
const MENU = 'SYSTEM_CODE';
|
||||
|
||||
/**
|
||||
* [화면] 공통코드 관리 (CodeList)
|
||||
*
|
||||
* 용도: 시스템 전반에서 쓰는 "공통코드"를 관리한다. 좌측에 코드 그룹, 우측에 그 그룹에 속한 코드 목록.
|
||||
* 예) 그룹 AGENT_STATUS(설계사 상태) → 코드 ACTIVE(재직)/RESIGN(해촉) 등.
|
||||
* 드롭다운·상태 뱃지 등 화면 곳곳의 라벨이 모두 이 공통코드에서 나온다.
|
||||
*
|
||||
* 도메인 용어:
|
||||
* - 코드 그룹(Group): 코드의 분류 단위. 시스템(isSystem=Y) 그룹은 식별자 변경/삭제가 제한된다.
|
||||
* - 코드(Code): 그룹 안의 개별 값. code(영문키)+codeName(한글 표시명)+isActive(사용여부).
|
||||
* - 캐시 갱신(refreshCache): 코드 변경 시 서버 캐시를 비워 즉시 반영되게 한다.
|
||||
*
|
||||
* 호출 API (api/user.ts → commonCodeApi):
|
||||
* - groups / codes 그룹·코드 목록 조회
|
||||
* - createGroup/updateGroup/deleteGroup, createCode/updateCode/deleteCode CRUD
|
||||
* - refreshCache 변경 후 서버 코드 캐시 무효화
|
||||
*
|
||||
* 주요 상태: 선택된 그룹, 검색어, 그룹/코드 모달(생성·수정 구분 union 타입), 저장 로딩.
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 내부 상태 타입 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -57,6 +78,7 @@ export default function CodeList() {
|
||||
queryFn: () => commonCodeApi.groups(keyword || undefined),
|
||||
});
|
||||
|
||||
// 우측 코드 목록: 그룹이 선택됐을 때만(enabled) 해당 그룹의 코드를 조회한다.
|
||||
const { data: codes = [], isFetching: codeLoading } = useQuery({
|
||||
queryKey: ['code', 'codes', selected?.groupCode],
|
||||
queryFn: () => commonCodeApi.codes(selected!.groupCode),
|
||||
|
||||
@@ -17,7 +17,27 @@ import {
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
/**
|
||||
* [화면] 데이터 사전 (DataDictionary)
|
||||
*
|
||||
* 용도: DB 테이블/컬럼의 "뜻"을 정리하는 메타데이터 화면. 좌측에서 테이블을 고르면 우측에
|
||||
* 그 테이블 컬럼들의 한글명·도메인·DB타입·마스킹/암호화 정책·샘플값을 보여주고 편집한다.
|
||||
* 개발자·기획자가 "이 컬럼이 무슨 데이터인가"를 빠르게 파악하기 위한 사전.
|
||||
*
|
||||
* 도메인 용어:
|
||||
* - 도메인(Domain): 컬럼의 의미 유형(IDENTIFIER/PII/MONEY/RATE 등). PII는 개인정보로 마스킹/암호화 대상.
|
||||
* - 마스킹(maskingType): 화면 표시 시 가림 규칙(NAME/PHONE/RRN 등). 암호화(encrypted): 저장 시 암호화 여부.
|
||||
* - 미등록 행: 한글명/도메인/업무규칙이 비어있는 컬럼. 회색 배경으로 구분(dict-row-unregistered).
|
||||
*
|
||||
* 호출 API (api/dict.ts):
|
||||
* - dataDictApi.tables / full(table) / create / remove 테이블·컬럼사전 조회 및 CRUD
|
||||
* - dataDomainApi.options 도메인 선택 옵션
|
||||
*
|
||||
* 구조: TableList(좌측 목록·카테고리 그룹핑) + ColumnDictTable(우측 컬럼표) + DictFormModal(등록/수정).
|
||||
*/
|
||||
|
||||
// ─── 테이블명 → 카테고리 자동 분류 ──────────────────────────────────────────
|
||||
// 테이블 접두사로 좌측 목록의 카테고리(조직/계약/원장/정산/시스템)를 자동 분류한다.
|
||||
|
||||
const TABLE_CATEGORY_PREFIX: Array<{ prefix: string; label: string }> = [
|
||||
{ prefix: 'org_', label: '조직' },
|
||||
@@ -158,6 +178,7 @@ function DictFormModal({
|
||||
}
|
||||
|
||||
// ─── 우측: 컬럼 사전 테이블 ──────────────────────────────────────────────────
|
||||
// 선택된 테이블의 컬럼 목록을 표로 보여주고, 행별로 사전 정보 추가/수정/삭제를 제공한다.
|
||||
|
||||
function ColumnDictTable({ tableName }: { tableName: string }) {
|
||||
const qc = useQueryClient();
|
||||
@@ -331,6 +352,7 @@ function ColumnDictTable({ tableName }: { tableName: string }) {
|
||||
}
|
||||
|
||||
// ─── 좌측: 테이블 목록 ────────────────────────────────────────────────────────
|
||||
// 검색어로 필터링 후 카테고리별로 그룹핑해 목록을 그린다. 클릭 시 onSelect 로 우측 표를 전환.
|
||||
|
||||
function TableList({
|
||||
selected,
|
||||
|
||||
@@ -11,7 +11,27 @@ import PageContainer from '@/components/common/PageContainer';
|
||||
import PermissionButton from '@/components/common/PermissionButton';
|
||||
import { dataDomainApi, DataDomainResp, DataDomainSaveReq } from '@/api/dict';
|
||||
|
||||
/**
|
||||
* [화면] 데이터 도메인 사전 (DataDomainList)
|
||||
*
|
||||
* 용도: 컬럼의 "의미 유형(도메인)"을 정의·관리하는 마스터 화면. 데이터 사전(DataDictionary)에서
|
||||
* 각 컬럼에 연결하는 도메인의 원본 목록이다. 여기서 정의한 마스킹/암호화 정책이 보안 처리의 기준.
|
||||
*
|
||||
* 도메인 용어:
|
||||
* - 카테고리(domainCategory): IDENTIFIER(식별자)/PII(개인정보)/MONEY(금액)/RATE(요율) 등 분류.
|
||||
* - 마스킹 유형(maskingType): 화면 노출 시 가림 규칙(NAME/PHONE/RRN=주민번호/ACCOUNT=계좌 등).
|
||||
* - 암호화(encrypted): 저장 시 암호화 여부. PII/계좌 등 민감 항목에 적용.
|
||||
* - 길이/정밀도: precision(전체 자릿수)·scale(소수 자릿수) 또는 length(문자 길이).
|
||||
*
|
||||
* 호출 API (api/dict.ts → dataDomainApi):
|
||||
* - list(검색 파라미터) 목록 조회(ProTable request 로 페이징/검색)
|
||||
* - create / update / remove 도메인 CRUD (사용 중 컬럼 있으면 삭제 시 오류)
|
||||
*
|
||||
* 구조: DomainFormModal(등록/수정 모달) + ProTable(목록·검색·페이징).
|
||||
*/
|
||||
|
||||
// ─── 카테고리 색상 ────────────────────────────────────────────────────────────
|
||||
// 카테고리별 태그 색상 매핑. PII/계좌(RRN/ACCOUNT)는 민감도가 높아 red 로 강조.
|
||||
|
||||
const CATEGORY_COLOR: Record<string, string> = {
|
||||
IDENTIFIER: 'blue',
|
||||
@@ -142,8 +162,10 @@ function DomainFormModal({
|
||||
|
||||
export default function DataDomainList() {
|
||||
const qc = useQueryClient();
|
||||
// ProTable 제어용 ref: reload() 로 목록을 강제 재요청한다.
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
||||
// 캐시 무효화 + 테이블 새로고침을 함께 수행(저장/삭제 직후 호출).
|
||||
const refresh = () => {
|
||||
qc.invalidateQueries({ queryKey: ['dataDomain'] });
|
||||
actionRef.current?.reload();
|
||||
|
||||
@@ -4,6 +4,21 @@ import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import { externalCallLogApi, ExternalCallLogResp } from '@/api/externalCallLog';
|
||||
|
||||
/**
|
||||
* [화면] 외부연동 호출이력 (ExternalCallLogList)
|
||||
*
|
||||
* 용도: 펌뱅킹(은행 이체)·SMS/카카오 등 외부 시스템 어댑터 호출의 감사 로그를 조회한다.
|
||||
* 서버 AOP가 모든 외부 호출을 자동으로 기록(영속화)하므로, 성공/실패·소요시간·요청/응답
|
||||
* 원문을 추적해 장애 원인 분석에 사용한다. (읽기 전용 화면)
|
||||
*
|
||||
* 도메인 용어:
|
||||
* - 어댑터(adapter): 외부 시스템과 통신하는 구현 클래스(adapterClass)와 메서드(methodName).
|
||||
* - 대상(targetRef): 이 호출이 연관된 업무 객체(예: 정산건 ID). "타입#ID" 로 표시.
|
||||
* - 결과: success(true/false), resultCode, durationMs(소요 밀리초), 예외 발생 시 errorClass/Message.
|
||||
*
|
||||
* 호출 API: externalCallLogApi.list (검색 파라미터 + 페이징).
|
||||
* 주요 상태: 클릭한 행의 상세를 Drawer 로 보여주기 위한 detail.
|
||||
*/
|
||||
export default function ExternalCallLogList() {
|
||||
const [detail, setDetail] = useState<ExternalCallLogResp | null>(null);
|
||||
|
||||
|
||||
@@ -5,8 +5,22 @@ import { IconCategory } from '@tabler/icons-react';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import { menuApi, MenuResp } from '@/api/menu';
|
||||
|
||||
/**
|
||||
* [화면] 메뉴 관리 (MenuManage)
|
||||
*
|
||||
* 용도: 시스템 좌측 네비게이션을 구성하는 메뉴 계층을 트리로 보여준다(읽기 전용 표시).
|
||||
* 메뉴는 권한(RBAC)의 단위이기도 해서, 역할/권한 화면에서 메뉴별로 권한을 부여한다.
|
||||
*
|
||||
* 도메인 용어:
|
||||
* - 메뉴 유형(menuType): DIRECTORY(폴더·하위 묶음) / PAGE(실제 화면) / LINK(외부 링크).
|
||||
* - menuCode: 권한 체크 키. menuPath: 라우팅 경로.
|
||||
*
|
||||
* 호출 API: menuApi.tree (메뉴 트리 조회).
|
||||
*/
|
||||
|
||||
interface TreeNode { key: string; title: any; children?: TreeNode[]; }
|
||||
|
||||
// 서버 메뉴 응답(MenuResp 계층)을 Ant Design Tree 가 쓰는 노드 구조로 재귀 변환한다.
|
||||
function toTree(menus: MenuResp[]): TreeNode[] {
|
||||
return menus.map((m) => ({
|
||||
key: String(m.menuId),
|
||||
@@ -50,6 +64,7 @@ export default function MenuManage() {
|
||||
);
|
||||
}
|
||||
|
||||
// 트리 전체 메뉴 개수를 재귀로 합산(상단 태그의 "전체 N개" 표시용).
|
||||
function countMenus(menus: MenuResp[]): number {
|
||||
return menus.reduce((sum, m) => sum + 1 + (m.children ? countMenus(m.children) : 0), 0);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,21 @@ import { GRAY, RADIUS, SHADOW, COLOR_ERROR } from '@/theme/tokens';
|
||||
const { Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
/**
|
||||
* [화면] 공지사항 관리 (Notices)
|
||||
*
|
||||
* 용도: 시스템 공지사항을 등록/검색/수정/삭제한다. 상단 고정(isPinned), 유형/상태별 분류,
|
||||
* 게시일·만료일 기간 관리를 지원한다.
|
||||
*
|
||||
* 도메인 용어:
|
||||
* - 유형(NOTICE_TYPE)·상태(NOTICE_STATUS): 공통코드. CodeBadge 로 한글 라벨/색상 표시.
|
||||
* - 고정(isPinned): 목록 상단에 우선 노출되는 공지.
|
||||
*
|
||||
* 호출 API (api/notice.ts): noticeApi.list / create / update / delete.
|
||||
* 주요 상태: 검색 파라미터(params), 등록/수정 모달, 수정 대상(editingNotice·null이면 신규).
|
||||
*/
|
||||
|
||||
// AG Grid 컬럼 정의: 공지 목록 표시 열. 고정/유형/상태는 cellRenderer 로 뱃지 렌더링.
|
||||
const COLUMNS: ColDef<NoticeRow>[] = [
|
||||
{
|
||||
field: 'isPinned', headerName: '고정', width: 70,
|
||||
@@ -50,9 +65,11 @@ export default function Notices() {
|
||||
|
||||
const rows = data?.list ?? [];
|
||||
|
||||
// 신규 등록 모달 열기: 수정 대상 비우고 폼 초기화. / 수정 모달 열기: 선택 행 값으로 폼 채움.
|
||||
const openCreate = () => { setEditingNotice(null); form.resetFields(); setModalOpen(true); };
|
||||
const openEdit = (row: NoticeRow) => { setEditingNotice(row); form.setFieldsValue(row); setModalOpen(true); };
|
||||
|
||||
// 저장: editingNotice 유무로 수정/등록 분기. 성공 시 목록 캐시 갱신.
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields() as NoticeSaveReq;
|
||||
try {
|
||||
|
||||
@@ -17,6 +17,26 @@ import PageContainer from '@/components/common/PageContainer';
|
||||
import { roleApi, RoleVO } from '@/api/user';
|
||||
import { menuApi, MenuResp } from '@/api/menu';
|
||||
|
||||
/**
|
||||
* [화면] 역할 / 권한 (RoleList)
|
||||
*
|
||||
* 용도: RBAC(역할 기반 접근제어)의 핵심 화면. 좌측에서 역할을 고르면, 우측에 "메뉴 x 권한" 매트릭스가
|
||||
* 체크박스로 펼쳐진다. 체크된 조합만 그 역할에게 허용된다(예: 정산 메뉴의 조회/엑셀만 허용).
|
||||
*
|
||||
* 도메인 용어:
|
||||
* - 역할(Role): 권한 묶음. 사용자에게 역할을 부여해 화면/기능 접근을 통제한다.
|
||||
* - 권한(Perm): READ(조회)/CREATE(등록)/UPDATE(수정)/DELETE(삭제)/APPROVE(승인)/EXPORT(엑셀).
|
||||
* - 매트릭스: 각 메뉴별로 6개 권한 체크. DIRECTORY(폴더) 행은 권한 없이 분류만.
|
||||
* - 권한 키 표현: "menuId:permCode" 문자열로 Set 에 담아 부여 여부를 관리한다.
|
||||
*
|
||||
* 호출 API:
|
||||
* - roleApi.list / permissions(roleId) / savePermissions(roleId, list)
|
||||
* - menuApi.tree (권한을 매길 메뉴 트리)
|
||||
*
|
||||
* 주요 상태: 선택 역할, granted(부여된 권한 키 Set), 저장 로딩.
|
||||
*/
|
||||
|
||||
// 매트릭스 열로 표시할 권한 코드와 한글 라벨.
|
||||
const PERMS = ['READ', 'CREATE', 'UPDATE', 'DELETE', 'APPROVE', 'EXPORT'] as const;
|
||||
const PERM_LABELS: Record<string, string> = {
|
||||
READ: '조회',
|
||||
@@ -37,6 +57,7 @@ interface FlatMenuItem {
|
||||
indent: number; // 0 = top-level directory, 1 = child page
|
||||
}
|
||||
|
||||
// 계층형 메뉴 트리를 들여쓰기(indent) 정보가 있는 평면 배열로 펼친다(매트릭스 행으로 쓰기 위함).
|
||||
function flattenMenu(nodes: MenuResp[], indent = 0): FlatMenuItem[] {
|
||||
const out: FlatMenuItem[] = [];
|
||||
for (const n of nodes) {
|
||||
@@ -72,6 +93,7 @@ export default function RoleList() {
|
||||
|
||||
const flatMenu = flattenMenu(tree);
|
||||
|
||||
// 역할 클릭 시: 그 역할의 부여 권한을 조회해 "menuId:permCode" 키 Set 으로 매트릭스에 반영.
|
||||
const selectRole = async (r: RoleVO) => {
|
||||
setSelected(r);
|
||||
const perms = await roleApi.permissions(r.roleId);
|
||||
@@ -82,6 +104,7 @@ export default function RoleList() {
|
||||
setGranted(set);
|
||||
};
|
||||
|
||||
// 단일 체크박스 토글: 해당 메뉴-권한 키를 Set 에 넣거나 뺀다.
|
||||
const toggle = (menuId: number, permCode: string) => {
|
||||
const key = `${menuId}:${permCode}`;
|
||||
const next = new Set(granted);
|
||||
@@ -89,6 +112,7 @@ export default function RoleList() {
|
||||
setGranted(next);
|
||||
};
|
||||
|
||||
// 전체 선택/해제: 폴더가 아닌 모든 페이지의 모든 권한을 한꺼번에 켜거나 끈다.
|
||||
const toggleAll = () => {
|
||||
const pages = flatMenu.filter((m) => m.menuType !== 'DIRECTORY');
|
||||
const allKeys = pages.flatMap((m) => PERMS.map((p) => `${m.menuId}:${p}`));
|
||||
@@ -100,6 +124,7 @@ export default function RoleList() {
|
||||
}
|
||||
};
|
||||
|
||||
// 저장: 부여 키 Set 을 {menuId, permCode, isGranted:'Y'} 목록으로 변환해 서버에 전송.
|
||||
const save = () => {
|
||||
if (!selected) return;
|
||||
Modal.confirm({
|
||||
|
||||
@@ -6,8 +6,24 @@ import PageContainer from '@/components/common/PageContainer';
|
||||
import PermissionButton from '@/components/common/PermissionButton';
|
||||
import { configApi, SystemConfigResp } from '@/api/config';
|
||||
|
||||
// 권한(RBAC) 체크용 메뉴 코드.
|
||||
const MENU = 'SYSTEM_CONFIG';
|
||||
|
||||
/**
|
||||
* [화면] 시스템 설정 (SystemConfig)
|
||||
*
|
||||
* 용도: 시스템 동작을 좌우하는 키-값(key-value) 설정을 조회/수정한다. 그룹별(일반/배치/보안/메일/세금)
|
||||
* 로 분류되며, 일부는 잠금(isEditable=N)·암호화(isEncrypted=Y) 항목이라 다루는 방식이 다르다.
|
||||
*
|
||||
* 도메인 용어:
|
||||
* - 설정 키(configKey): 코드에서 참조하는 식별자(예: tax.rate). configValue: 그 값.
|
||||
* - 암호화 항목: 화면에는 *** 로만 표시. 수정 시 새 값을 입력하면 서버에서 암호화해 저장.
|
||||
* - 잠금 항목: isEditable=N. 화면에서 수정 불가(작업 버튼 미노출).
|
||||
*
|
||||
* 호출 API (api/config.ts): configApi.list(group) / update(key, value).
|
||||
* 주요 상태: 수정 모달(editState union — 닫힘/열림+행), 저장 로딩.
|
||||
*/
|
||||
|
||||
const GROUP_COLOR: Record<string, string> = {
|
||||
GENERAL: 'blue',
|
||||
BATCH: 'cyan',
|
||||
|
||||
@@ -4,6 +4,17 @@ import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import { logApi, ApiAccessLogResp, DataChangeLogResp, LoginLogResp } from '@/api/log';
|
||||
|
||||
/**
|
||||
* [화면] 시스템 로그 (SystemLog)
|
||||
*
|
||||
* 용도: 감사(audit) 목적의 3종 로그를 탭으로 보여준다(모두 읽기 전용).
|
||||
* - 로그인 이력(LoginTable): 로그인/로그아웃/실패. 실패 사유·IP·User-Agent 추적.
|
||||
* - API 호출(ApiTable): 요청 메서드/URL/응답상태/소요시간. 상태코드별 색상(4xx 경고, 5xx 오류).
|
||||
* - 변경 이력(ChangeTable): 어떤 테이블의 어떤 레코드가 CREATE/UPDATE/DELETE 되었는지.
|
||||
*
|
||||
* 호출 API (api/log.ts): logApi.login / api / change (각각 페이징+검색).
|
||||
* 주요 상태: 활성 탭(tab).
|
||||
*/
|
||||
export default function SystemLog() {
|
||||
const [tab, setTab] = useState('login');
|
||||
|
||||
|
||||
@@ -25,7 +25,26 @@ import {
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
/**
|
||||
* [화면] 업로드 템플릿 관리 (UploadTemplateManager)
|
||||
*
|
||||
* 용도: 보험사에서 받은 엑셀/CSV 파일을 우리 DB 테이블로 적재할 때, "엑셀의 어느 열을 DB의 어느 컬럼에
|
||||
* 넣을지"를 정의하는 매핑 템플릿을 만든다. 좌측에 템플릿 목록, 우측에 선택 템플릿의 컬럼 매핑 편집기.
|
||||
*
|
||||
* 도메인 용어:
|
||||
* - 카테고리: 적재 대상 원장 구분(모집/유지/예외/지급/기타). 대상 테이블(targetTable)로 흐른다.
|
||||
* - 소스(source): 엑셀 쪽 출처 — 열 인덱스(A,B,C)/헤더명/고정값.
|
||||
* - 대상 필드(targetField): DB 컬럼. 변환 규칙(transformRule)으로 값 가공(TRIM/DATE/DIVIDE/CODE_MAP 등).
|
||||
* - 샘플 엑셀 자동 채우기: 견본 파일 헤더를 읽어 매핑 행을 자동 생성(extractHeaders).
|
||||
*
|
||||
* 호출 API (api/uploadTemplate.ts): list/create/update/remove, getColumns/saveColumns,
|
||||
* targetTables/targetFields, extractHeaders, preview, history.
|
||||
*
|
||||
* 구조: TemplateFormModal(템플릿 CRUD) + ColumnEditor(컬럼 매핑 편집) + PreviewModal(미리보기) + HistoryDrawer(업로드 이력).
|
||||
*/
|
||||
|
||||
// transformRule → color tag
|
||||
// 변환 규칙별 태그 색상/라벨. 단순(TRIM 등)/숫자(DATE·DIVIDE)/조회(LOOKUP·CODE_MAP)/문자(REPLACE 등)로 색 구분.
|
||||
const RULE_TAG: Record<string, { color: string; label: string }> = {
|
||||
TRIM: { color: 'default', label: 'TRIM' },
|
||||
UPPER: { color: 'default', label: 'UPPER' },
|
||||
@@ -94,6 +113,7 @@ function toRows(cols: UploadTemplateColumnVO[]): ColumnRow[] {
|
||||
}
|
||||
|
||||
// ─── 변환 규칙에 따라 동적 파라미터 입력 렌더 ──────────────────────────────
|
||||
// 선택된 변환 규칙(DATE/DIVIDE/LOOKUP 등)에 맞는 파라미터 입력 위젯을 그려준다.
|
||||
function RuleParamCell({ row, onChange }: {
|
||||
row: ColumnRow;
|
||||
onChange: (key: string, field: keyof UploadTemplateColumnVO, value: unknown) => void;
|
||||
@@ -295,6 +315,7 @@ function HistoryDrawer({ templateId, onClose }: { templateId: number; onClose: (
|
||||
}
|
||||
|
||||
// ─── 컬럼 매핑 편집기 ─────────────────────────────────────────────────────
|
||||
// 선택 템플릿의 매핑 행을 로컬 상태(rows)로 편집한다. 행 추가/삭제/순서이동·샘플 자동채우기·저장 제공.
|
||||
function ColumnEditor({ template, onSaved }: {
|
||||
template: UploadTemplateResp;
|
||||
onSaved: () => void;
|
||||
@@ -364,6 +385,7 @@ function ColumnEditor({ template, onSaved }: {
|
||||
}
|
||||
};
|
||||
|
||||
// 특정 매핑 행(_key)의 한 필드 값을 갱신.
|
||||
const updateCell = (key: string, field: keyof UploadTemplateColumnVO, value: unknown) => {
|
||||
setRows((prev) =>
|
||||
prev.map((r) => (r._key === key ? { ...r, [field]: value } : r)),
|
||||
@@ -387,6 +409,7 @@ function ColumnEditor({ template, onSaved }: {
|
||||
setRows((prev) => prev.filter((r) => r._key !== key).map((r, i) => ({ ...r, sortOrder: i + 1 })));
|
||||
};
|
||||
|
||||
// 행 순서 위/아래 이동 후 sortOrder 재계산.
|
||||
const moveRow = (key: string, dir: 'up' | 'down') => {
|
||||
setRows((prev) => {
|
||||
const idx = prev.findIndex((r) => r._key === key);
|
||||
@@ -400,6 +423,7 @@ function ColumnEditor({ template, onSaved }: {
|
||||
});
|
||||
};
|
||||
|
||||
// 저장: 로컬 편집용 _key 를 제거한 매핑 목록을 서버에 일괄 저장.
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
@@ -732,6 +756,7 @@ function TemplateFormModal({ editTarget, onSuccess, trigger }: {
|
||||
}
|
||||
|
||||
// ─── 메인 페이지 ──────────────────────────────────────────────────────────
|
||||
// 좌측 템플릿 목록(선택) + 우측 컬럼 매핑 편집기를 조합. 이력 Drawer 는 선택 시 표시.
|
||||
export default function UploadTemplateManager() {
|
||||
const qc = useQueryClient();
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
||||
@@ -8,6 +8,21 @@ import PermissionButton from '@/components/common/PermissionButton';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
import { userApi, UserResp } from '@/api/user';
|
||||
|
||||
/**
|
||||
* [화면] 사용자 관리 (UserList)
|
||||
*
|
||||
* 용도: 시스템 로그인 계정을 관리한다. 검색/조회 후 계정별로 전화번호 등록, 비밀번호 초기화,
|
||||
* 잠긴 계정(로그인 실패 누적 등) 잠금 해제를 수행한다.
|
||||
*
|
||||
* 도메인 용어:
|
||||
* - 연결 설계사(agentName): 이 계정이 매핑된 설계사. GA에서 사용자 계정과 설계사 인사정보는 별개라 연결 관계로 표현.
|
||||
* - 상태(status): 공통코드 AGENT_STATUS. LOCKED 면 잠금해제 버튼 노출.
|
||||
* - 실패횟수(failCount): 로그인 실패 누적. 한도 초과 시 계정 잠김.
|
||||
* - 전화번호: SMS 결재 알림 발송에 사용. 비우면 SMS 스킵.
|
||||
*
|
||||
* 호출 API (api/user.ts): userApi.list / resetPassword / unlock / updatePhone.
|
||||
* 주요 상태: ProTable 제어 ref, 전화번호 수정 모달(phoneEdit).
|
||||
*/
|
||||
export default function UserList() {
|
||||
const qc = useQueryClient();
|
||||
const actionRef = useRef<ActionType>();
|
||||
@@ -18,6 +33,7 @@ export default function UserList() {
|
||||
actionRef.current?.reload();
|
||||
};
|
||||
|
||||
// 비밀번호 초기화: 임시 비밀번호로 재설정(확인 모달 후 실행).
|
||||
const onReset = (id: number) =>
|
||||
Modal.confirm({
|
||||
title: '비밀번호 초기화',
|
||||
@@ -25,6 +41,7 @@ export default function UserList() {
|
||||
onOk: async () => { await userApi.resetPassword(id); message.success('초기화 완료'); refresh(); },
|
||||
});
|
||||
|
||||
// 계정 잠금 해제: 잠긴 계정의 실패횟수/잠금상태를 풀어 다시 로그인 가능하게 한다.
|
||||
const onUnlock = (id: number) =>
|
||||
Modal.confirm({
|
||||
title: '계정 잠금 해제',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
// 로그인한 사용자 정보 + 조작 함수
|
||||
// - userId/loginId/userName/roles: 현재 사용자 정보
|
||||
// - setAuth: 로그인 성공 시 사용자 정보 저장 / clear: 로그아웃 시 초기화
|
||||
interface AuthState {
|
||||
userId?: number;
|
||||
loginId?: string;
|
||||
@@ -10,6 +13,10 @@ interface AuthState {
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증 전역 스토어(zustand) — 어느 컴포넌트에서나 useAuthStore() 로 사용자 정보를 읽는다.
|
||||
* persist 미들웨어로 localStorage('ga-auth')에 저장되어 새로고침해도 로그인 상태가 유지된다.
|
||||
*/
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
|
||||
Reference in New Issue
Block a user