Files
sam-react-prod/src/lib/api/dashboard/transformers.ts
유병철 a1f4c82cec fix: 프로젝트 전체 TypeScript 타입에러 408개 수정 (tsc --noEmit 0 errors)
- 공통 템플릿 타입 수정 (IntegratedDetailTemplate, UniversalListPage)
- 페이지(app/[locale]) 타입 호환성 수정 (80개)
- 재고/자재 모듈 타입 수정 (StockStatus, ReceivingManagement)
- 생산 모듈 타입 수정 (WorkOrders, WorkerScreen, WorkResults)
- 주문/출고 모듈 타입 수정 (ShipmentManagement, Orders)
- 견적/단가 모듈 타입 수정 (Quotes, Pricing)
- 건설 모듈 타입 수정 (49개, 17개 하위 모듈)
- HR 모듈 타입 수정 (CardManagement, VacationManagement 등)
- 설정 모듈 타입 수정 (PermissionManagement, AccountManagement 등)
- 게시판 모듈 타입 수정 (BoardManagement, BoardList 등)
- 회계 모듈 타입 수정 (VendorManagement, BadDebtCollection 등)
- 기타 모듈 타입 수정 (CEODashboard, clients, vehicle 등)
- 유틸/훅/API 타입 수정 (hooks, contexts, lib)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:07:58 +09:00

1455 lines
46 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* CEO Dashboard API 응답 → Frontend 타입 변환 함수
*
* 참조: docs/plans/AI_리포트_키워드_색상체계_가이드_v1.4.md
*/
import type {
DailyReportApiResponse,
ReceivablesApiResponse,
BadDebtApiResponse,
ExpectedExpenseApiResponse,
CardTransactionApiResponse,
StatusBoardApiResponse,
TodayIssueApiResponse,
CalendarApiResponse,
VatApiResponse,
EntertainmentApiResponse,
WelfareApiResponse,
WelfareDetailApiResponse,
PurchaseDashboardDetailApiResponse,
CardDashboardDetailApiResponse,
BillDashboardDetailApiResponse,
ExpectedExpenseDashboardDetailApiResponse,
LoanDashboardApiResponse,
TaxSimulationApiResponse,
} from './types';
import type {
DailyReportData,
ReceivableData,
DebtCollectionData,
MonthlyExpenseData,
CardManagementData,
TodayIssueItem,
TodayIssueListItem,
TodayIssueListBadgeType,
TodayIssueNotificationType,
CalendarScheduleItem,
CheckPoint,
CheckPointType,
VatData,
EntertainmentData,
WelfareData,
HighlightColor,
} from '@/components/business/CEODashboard/types';
// ============================================
// 헬퍼 함수
// ============================================
/**
* 네비게이션 경로 정규화
* - /ko prefix 추가 (없는 경우)
* - 상세 페이지에 ?mode=view 추가 (필요시)
* @example normalizePath('/accounting/deposits/73') → '/ko/accounting/deposits/73?mode=view'
*/
function normalizePath(path: string | undefined, options?: { addViewMode?: boolean }): string {
if (!path) return '';
let normalizedPath = path;
// /ko prefix 추가 (없는 경우)
if (!normalizedPath.startsWith('/ko')) {
normalizedPath = `/ko${normalizedPath}`;
}
// 상세 페이지에 ?mode=view 추가 (숫자 ID가 있고 mode 파라미터가 없는 경우)
if (options?.addViewMode) {
const hasIdPattern = /\/\d+($|\?)/.test(normalizedPath);
const hasMode = /[?&]mode=/.test(normalizedPath);
const hasModal = /[?&]openModal=/.test(normalizedPath);
// ID가 있고, mode 파라미터가 없고, openModal도 없는 경우에만 ?mode=view 추가
if (hasIdPattern && !hasMode && !hasModal) {
normalizedPath = normalizedPath.includes('?')
? `${normalizedPath}&mode=view`
: `${normalizedPath}?mode=view`;
}
}
return normalizedPath;
}
/**
* 금액 포맷팅
* @example formatAmount(3050000000) → "30.5억원"
*/
function formatAmount(amount: number): string {
const absAmount = Math.abs(amount);
if (absAmount >= 100000000) {
return `${(amount / 100000000).toFixed(1)}억원`;
} else if (absAmount >= 10000) {
return `${Math.round(amount / 10000).toLocaleString()}만원`;
}
return `${amount.toLocaleString()}`;
}
/**
* 날짜 포맷팅 (API → 한국어 형식)
* @example formatDate("2026-01-20", "월요일") → "2026년 1월 20일 월요일"
*/
function formatDate(dateStr: string, dayOfWeek: string): string {
const date = new Date(dateStr);
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}${dayOfWeek}`;
}
/**
* 퍼센트 변화율 계산
*/
function calculateChangeRate(current: number, previous: number): number {
if (previous === 0) return current > 0 ? 100 : 0;
return ((current - previous) / previous) * 100;
}
// ============================================
// 1. DailyReport 변환
// ============================================
/**
* 운영자금 안정성에 따른 색상 반환
* 참조: AI 리포트 색상 체계 가이드 - 섹션 2.3
*/
function getStabilityColor(stability: string): 'red' | 'green' | 'blue' {
switch (stability) {
case 'stable':
return 'blue'; // 6개월 이상 - 안정적
case 'caution':
return 'green'; // 3-6개월 - 주의 (주황 대신 green 사용, 기존 타입 호환)
case 'warning':
return 'red'; // 3개월 미만 - 경고
default:
return 'blue';
}
}
/**
* 운영자금 안정성 메시지 생성
* - 음수: 현금성 자산 적자 상태
* - 0~3개월: 자금 부족 우려
* - 3~6개월: 자금 관리 필요
* - 6개월 이상: 안정적
*/
function getStabilityMessage(months: number | null, stability: string, cashAsset: number): string {
if (months === null) {
return '월 운영비 데이터가 없어 안정성을 판단할 수 없습니다.';
}
// 현금성 자산이 음수인 경우 (적자 상태)
if (cashAsset < 0 || months < 0) {
return '현금성 자산이 부족한 상태입니다. 긴급 자금 확보가 필요합니다.';
}
// 운영 가능 기간이 거의 없는 경우 (1개월 미만)
if (months < 1) {
return '운영 자금이 거의 소진된 상태입니다. 즉시 자금 확보가 필요합니다.';
}
const monthsText = `${months}개월분`;
switch (stability) {
case 'stable':
return `월 운영비용 대비 ${monthsText}이 확보되어 안정적입니다.`;
case 'caution':
return `월 운영비용 대비 ${monthsText}이 확보되어 있습니다. 자금 관리가 필요합니다.`;
case 'warning':
return `월 운영비용 대비 ${monthsText}만 확보되어 자금 부족 우려가 있습니다.`;
default:
return `월 운영비용 대비 ${monthsText}이 확보되어 있습니다.`;
}
}
/**
* 일일 일보 CheckPoints 생성
* 참조: AI 리포트 색상 체계 가이드 - 섹션 2
*/
function generateDailyReportCheckPoints(api: DailyReportApiResponse): CheckPoint[] {
const checkPoints: CheckPoint[] = [];
// 출금 정보
const withdrawal = api.krw_totals.expense;
if (withdrawal > 0) {
checkPoints.push({
id: 'dr-withdrawal',
type: 'info' as CheckPointType,
message: `어제 ${formatAmount(withdrawal)} 출금했습니다.`,
highlights: [
{ text: formatAmount(withdrawal), color: 'red' as const },
],
});
}
// 입금 정보
const deposit = api.krw_totals.income;
if (deposit > 0) {
checkPoints.push({
id: 'dr-deposit',
type: 'success' as CheckPointType,
message: `어제 ${formatAmount(deposit)}이 입금되었습니다.`,
highlights: [
{ text: formatAmount(deposit), color: 'green' as const },
{ text: '입금', color: 'green' as const },
],
});
}
// 현금성 자산 + 운영자금 안정성 현황
const cashAsset = api.cash_asset_total;
const operatingMonths = api.operating_months;
const operatingStability = api.operating_stability;
const stabilityColor = getStabilityColor(operatingStability);
const stabilityMessage = getStabilityMessage(operatingMonths, operatingStability, cashAsset);
// 하이라이트 생성 (음수/적자 상태일 때는 "X개월분" 대신 다른 메시지)
const isDeficit = cashAsset < 0 || (operatingMonths !== null && operatingMonths < 0);
const isAlmostEmpty = operatingMonths !== null && operatingMonths >= 0 && operatingMonths < 1;
const highlights: Array<{ text: string; color: 'red' | 'green' | 'blue' }> = [];
if (isDeficit) {
highlights.push({ text: '긴급 자금 확보 필요', color: 'red' });
} else if (isAlmostEmpty) {
highlights.push({ text: '즉시 자금 확보 필요', color: 'red' });
} else if (operatingMonths !== null && operatingMonths >= 1) {
highlights.push({ text: `${operatingMonths}개월분`, color: stabilityColor });
if (operatingStability === 'stable') {
highlights.push({ text: '안정적', color: 'blue' });
} else if (operatingStability === 'warning') {
highlights.push({ text: '자금 부족 우려', color: 'red' });
}
}
checkPoints.push({
id: 'dr-cash-asset',
type: isDeficit || isAlmostEmpty ? 'warning' as CheckPointType : 'info' as CheckPointType,
message: `총 현금성 자산이 ${formatAmount(cashAsset)}입니다. ${stabilityMessage}`,
highlights,
});
return checkPoints;
}
/**
* DailyReport API 응답 → Frontend 타입 변환
*/
export function transformDailyReportResponse(api: DailyReportApiResponse): DailyReportData {
return {
date: formatDate(api.date, api.day_of_week),
cards: [
{
id: 'dr1',
label: '현금성 자산 합계',
amount: api.cash_asset_total,
},
{
id: 'dr2',
label: '외국환(USD) 합계',
amount: api.foreign_currency_total,
currency: 'USD',
},
{
id: 'dr3',
label: '입금 합계',
amount: api.krw_totals.income,
},
{
id: 'dr4',
label: '출금 합계',
amount: api.krw_totals.expense,
},
],
checkPoints: generateDailyReportCheckPoints(api),
};
}
// ============================================
// 2. Receivable 변환
// ============================================
/**
* 미수금 현황 CheckPoints 생성
*/
function generateReceivableCheckPoints(api: ReceivablesApiResponse): CheckPoint[] {
const checkPoints: CheckPoint[] = [];
// 연체 거래처 경고
if (api.overdue_vendor_count > 0) {
checkPoints.push({
id: 'rv-overdue',
type: 'warning' as CheckPointType,
message: `연체 거래처 ${api.overdue_vendor_count}곳. 회수 조치가 필요합니다.`,
highlights: [
{ text: `연체 거래처 ${api.overdue_vendor_count}`, color: 'red' as const },
],
});
}
// 미수금 현황
if (api.total_receivables > 0) {
checkPoints.push({
id: 'rv-total',
type: 'info' as CheckPointType,
message: `총 미수금 ${formatAmount(api.total_receivables)}입니다.`,
highlights: [
{ text: formatAmount(api.total_receivables), color: 'blue' as const },
],
});
}
return checkPoints;
}
/**
* Receivables API 응답 → Frontend 타입 변환
*/
export function transformReceivableResponse(api: ReceivablesApiResponse): ReceivableData {
// 누적 미수금 = 이월 + 매출 - 입금
const cumulativeReceivable = api.total_carry_forward + api.total_sales - api.total_deposits;
return {
cards: [
{
id: 'rv1',
label: '누적 미수금',
amount: cumulativeReceivable,
subItems: [
{ label: '이월', value: api.total_carry_forward },
{ label: '매출', value: api.total_sales },
{ label: '입금', value: api.total_deposits },
],
},
{
id: 'rv2',
label: '당월 미수금',
amount: api.total_receivables,
subItems: [
{ label: '매출', value: api.total_sales },
{ label: '입금', value: api.total_deposits },
],
},
{
id: 'rv3',
label: '거래처 현황',
amount: api.vendor_count,
unit: '곳',
subLabel: `연체 ${api.overdue_vendor_count}`,
},
],
checkPoints: generateReceivableCheckPoints(api),
//detailButtonLabel: '미수금 상세',
detailButtonPath: normalizePath('/accounting/receivables-status'),
};
}
// ============================================
// 3. DebtCollection 변환
// ============================================
/**
* 채권추심 CheckPoints 생성
*/
function generateDebtCollectionCheckPoints(api: BadDebtApiResponse): CheckPoint[] {
const checkPoints: CheckPoint[] = [];
// 법적조치 진행 중
if (api.legal_action_amount > 0) {
checkPoints.push({
id: 'dc-legal',
type: 'warning' as CheckPointType,
message: `법적조치 진행 중 ${formatAmount(api.legal_action_amount)}입니다.`,
highlights: [
{ text: formatAmount(api.legal_action_amount), color: 'red' as const },
],
});
}
// 회수 완료
if (api.recovered_amount > 0) {
checkPoints.push({
id: 'dc-recovered',
type: 'success' as CheckPointType,
message: `${formatAmount(api.recovered_amount)}을 회수 완료했습니다.`,
highlights: [
{ text: formatAmount(api.recovered_amount), color: 'green' as const },
{ text: '회수 완료', color: 'green' as const },
],
});
}
return checkPoints;
}
/**
* BadDebt API 응답 → Frontend 타입 변환
*/
export function transformDebtCollectionResponse(api: BadDebtApiResponse): DebtCollectionData {
return {
cards: [
{
id: 'dc1',
label: '누적 악성채권',
amount: api.total_amount,
},
{
id: 'dc2',
label: '추심중',
amount: api.collecting_amount,
},
{
id: 'dc3',
label: '법적조치',
amount: api.legal_action_amount,
},
{
id: 'dc4',
label: '회수완료',
amount: api.recovered_amount,
},
],
checkPoints: generateDebtCollectionCheckPoints(api),
detailButtonPath: normalizePath('/accounting/bad-debt-collection'),
};
}
// ============================================
// 4. MonthlyExpense 변환
// ============================================
/**
* 당월 예상 지출 CheckPoints 생성
*/
function generateMonthlyExpenseCheckPoints(api: ExpectedExpenseApiResponse): CheckPoint[] {
const checkPoints: CheckPoint[] = [];
// 총 예상 지출
checkPoints.push({
id: 'me-total',
type: 'info' as CheckPointType,
message: `이번 달 예상 지출은 ${formatAmount(api.total_amount)}입니다.`,
highlights: [
{ text: formatAmount(api.total_amount), color: 'blue' as const },
],
});
return checkPoints;
}
/**
* ExpectedExpense API 응답 → Frontend 타입 변환
* 주의: 실제 API는 상세 분류(매입/카드/어음 등)를 제공하지 않음
* by_transaction_type에서 추출하거나 기본값 사용
*/
export function transformMonthlyExpenseResponse(api: ExpectedExpenseApiResponse): MonthlyExpenseData {
// transaction_type별 금액 추출
const purchaseTotal = api.by_transaction_type['purchase']?.total ?? 0;
const cardTotal = api.by_transaction_type['card']?.total ?? 0;
const billTotal = api.by_transaction_type['bill']?.total ?? 0;
return {
cards: [
{
id: 'me1',
label: '매입',
amount: purchaseTotal,
},
{
id: 'me2',
label: '카드',
amount: cardTotal,
},
{
id: 'me3',
label: '발행어음',
amount: billTotal,
},
{
id: 'me4',
label: '총 예상 지출 합계',
amount: api.total_amount,
},
],
checkPoints: generateMonthlyExpenseCheckPoints(api),
};
}
// ============================================
// 5. CardManagement 변환
// ============================================
/**
* 카드/가지급금 CheckPoints 생성
*/
function generateCardManagementCheckPoints(api: CardTransactionApiResponse): CheckPoint[] {
const checkPoints: CheckPoint[] = [];
// 전월 대비 변화
const changeRate = calculateChangeRate(api.current_month_total, api.previous_month_total);
if (Math.abs(changeRate) > 10) {
const type: CheckPointType = changeRate > 0 ? 'warning' : 'info';
checkPoints.push({
id: 'cm-change',
type,
message: `당월 카드 사용액이 전월 대비 ${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}% 변동했습니다.`,
highlights: [
{ text: `${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}%`, color: changeRate > 0 ? 'red' as const : 'green' as const },
],
});
}
// 당월 사용액
checkPoints.push({
id: 'cm-current',
type: 'info' as CheckPointType,
message: `당월 카드 사용 총 ${formatAmount(api.current_month_total)}입니다.`,
highlights: [
{ text: formatAmount(api.current_month_total), color: 'blue' as const },
],
});
return checkPoints;
}
/**
* CardTransaction API 응답 → Frontend 타입 변환
* 4개 카드 구조:
* - cm1: 카드 사용액 (CardTransaction API)
* - cm2: 가지급금 (LoanDashboard API)
* - cm3: 법인세 예상 가중 (TaxSimulation API - corporate_tax.difference)
* - cm4: 대표자 종합세 예상 가중 (TaxSimulation API - income_tax.difference)
*/
export function transformCardManagementResponse(
summaryApi: CardTransactionApiResponse,
loanApi?: LoanDashboardApiResponse | null,
taxApi?: TaxSimulationApiResponse | null,
fallbackData?: CardManagementData
): CardManagementData {
const changeRate = calculateChangeRate(summaryApi.current_month_total, summaryApi.previous_month_total);
// cm2: 가지급금 금액 (LoanDashboard API 또는 fallback)
const loanAmount = loanApi?.summary?.total_outstanding ?? fallbackData?.cards[1]?.amount ?? 0;
// cm3: 법인세 예상 가중 (TaxSimulation API 또는 fallback)
const corporateTaxDifference = taxApi?.corporate_tax?.difference ?? fallbackData?.cards[2]?.amount ?? 0;
// cm4: 대표자 종합세 예상 가중 (TaxSimulation API 또는 fallback)
const incomeTaxDifference = taxApi?.income_tax?.difference ?? fallbackData?.cards[3]?.amount ?? 0;
// 가지급금 경고 배너 표시 여부 결정 (가지급금 잔액 > 0이면 표시)
const hasLoanWarning = loanAmount > 0;
return {
// 가지급금 관련 경고 배너 (가지급금 있을 때만 표시)
warningBanner: hasLoanWarning ? fallbackData?.warningBanner : undefined,
cards: [
// cm1: 카드 사용액 (CardTransaction API)
{
id: 'cm1',
label: '카드',
amount: summaryApi.current_month_total,
previousLabel: `전월 대비 ${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}%`,
},
// cm2: 가지급금 (LoanDashboard API)
{
id: 'cm2',
label: '가지급금',
amount: loanAmount,
},
// cm3: 법인세 예상 가중 (TaxSimulation API)
{
id: 'cm3',
label: '법인세 예상 가중',
amount: corporateTaxDifference,
},
// cm4: 대표자 종합세 예상 가중 (TaxSimulation API)
{
id: 'cm4',
label: '대표자 종합세 예상 가중',
amount: incomeTaxDifference,
},
],
checkPoints: generateCardManagementCheckPoints(summaryApi),
};
}
// ============================================
// 6. StatusBoard 변환
// ============================================
/**
* StatusBoard API 응답 → Frontend 타입 변환
* API 응답 형식이 TodayIssueItem과 거의 동일하므로 단순 매핑
*/
export function transformStatusBoardResponse(api: StatusBoardApiResponse): TodayIssueItem[] {
return api.items.map((item) => ({
id: item.id,
label: item.label,
count: item.count,
path: normalizePath(item.path, { addViewMode: true }),
isHighlighted: item.isHighlighted,
}));
}
// ============================================
// 7. TodayIssue 변환
// ============================================
/** 유효한 notification_type 목록 (API TodayIssue 모델과 동기화) */
const VALID_NOTIFICATION_TYPES: TodayIssueNotificationType[] = [
'sales_order',
'bad_debt',
'safety_stock',
'expected_expense',
'vat_report',
'approval_request',
'new_vendor',
'deposit',
'withdrawal',
'other',
];
/** notification_type → 한글 badge 변환 매핑
* 백엔드 TodayIssue.php BADGE 상수와 동기화!
*/
const NOTIFICATION_TYPE_TO_BADGE: Record<TodayIssueNotificationType, TodayIssueListBadgeType> = {
sales_order: '수주등록',
bad_debt: '추심이슈',
safety_stock: '안전재고',
expected_expense: '지출 승인대기',
vat_report: '세금 신고',
approval_request: '결재 요청',
new_vendor: '신규거래처',
deposit: '입금',
withdrawal: '출금',
other: '기타',
};
/** 한글 badge → notification_type 추론 매핑 (fallback용)
* 백엔드 TodayIssue.php BADGE 상수와 동기화 필수!
*/
const BADGE_TO_NOTIFICATION_TYPE: Record<string, TodayIssueNotificationType> = {
// === 백엔드 실제 값 (TodayIssue.php 상수) ===
'수주등록': 'sales_order',
'추심이슈': 'bad_debt',
'안전재고': 'safety_stock',
'지출 승인대기': 'expected_expense',
'세금 신고': 'vat_report',
'결재 요청': 'approval_request',
'신규거래처': 'new_vendor',
'신규업체': 'new_vendor', // 변형
'입금': 'deposit',
'출금': 'withdrawal',
// === 혹시 모를 변형 (안전장치) ===
'수주 등록': 'sales_order',
'추심 이슈': 'bad_debt',
'안전 재고': 'safety_stock',
'지출승인대기': 'expected_expense',
'세금신고': 'vat_report',
'결재요청': 'approval_request',
};
/**
* API notification_type → Frontend notificationType 변환
* notification_type이 없으면 badge에서 추론
*/
function validateNotificationType(notificationType: string | null, badge?: string): TodayIssueNotificationType {
// 1. notification_type이 유효하면 그대로 사용
if (notificationType && VALID_NOTIFICATION_TYPES.includes(notificationType as TodayIssueNotificationType)) {
return notificationType as TodayIssueNotificationType;
}
// 2. notification_type이 없으면 badge에서 추론
if (badge && BADGE_TO_NOTIFICATION_TYPE[badge]) {
return BADGE_TO_NOTIFICATION_TYPE[badge];
}
return 'other';
}
/**
* TodayIssue API 응답 → Frontend 타입 변환
* 오늘의 이슈 리스트 데이터 변환
* notification_type 코드값 기반으로 색상 매핑 지원
*/
export function transformTodayIssueResponse(api: TodayIssueApiResponse): {
items: TodayIssueListItem[];
totalCount: number;
} {
return {
items: api.items.map((item) => {
// notification_type이 없으면 badge에서 추론
const notificationType = validateNotificationType(item.notification_type, item.badge);
// badge는 API 응답 그대로 사용하되, 없으면 notification_type에서 변환
const badge = item.badge || NOTIFICATION_TYPE_TO_BADGE[notificationType];
return {
id: item.id,
badge: badge as TodayIssueListBadgeType,
notificationType,
content: item.content,
time: item.time,
date: item.date,
needsApproval: item.needsApproval ?? false,
path: normalizePath(item.path, { addViewMode: true }),
};
}),
totalCount: api.total_count,
};
}
// ============================================
// 8. Calendar 변환
// ============================================
/**
* Calendar API 응답 → Frontend 타입 변환
* API 응답 형식이 CalendarScheduleItem과 동일하므로 단순 매핑
*/
export function transformCalendarResponse(api: CalendarApiResponse): {
items: CalendarScheduleItem[];
totalCount: number;
} {
return {
items: api.items.map((item) => ({
id: item.id,
title: item.title,
startDate: item.startDate,
endDate: item.endDate,
startTime: item.startTime ?? undefined,
endTime: item.endTime ?? undefined,
isAllDay: item.isAllDay,
type: item.type,
department: item.department ?? undefined,
personName: item.personName ?? undefined,
color: item.color ?? undefined,
})),
totalCount: api.total_count,
};
}
// ============================================
// 9. Vat 변환
// ============================================
/** 유효한 하이라이트 색상 목록 */
const VALID_HIGHLIGHT_COLORS: HighlightColor[] = ['red', 'green', 'blue'];
/**
* API 색상 문자열 → Frontend 하이라이트 색상 변환
* 유효하지 않은 색상은 'blue'로 폴백
*/
function validateHighlightColor(color: string): HighlightColor {
if (VALID_HIGHLIGHT_COLORS.includes(color as HighlightColor)) {
return color as HighlightColor;
}
return 'blue';
}
/**
* Vat API 응답 → Frontend 타입 변환
* 부가세 현황 데이터 변환
*/
export function transformVatResponse(api: VatApiResponse): VatData {
return {
cards: api.cards.map((card) => ({
id: card.id,
label: card.label,
amount: card.amount,
subLabel: card.subLabel,
unit: card.unit,
})),
checkPoints: api.check_points.map((cp) => ({
id: cp.id,
type: cp.type as CheckPointType,
message: cp.message,
highlights: cp.highlights?.map((h) => ({
text: h.text,
color: validateHighlightColor(h.color),
})),
})),
};
}
// ============================================
// 10. Entertainment 변환
// ============================================
/**
* Entertainment API 응답 → Frontend 타입 변환
* 접대비 현황 데이터 변환
*/
export function transformEntertainmentResponse(api: EntertainmentApiResponse): EntertainmentData {
return {
cards: api.cards.map((card) => ({
id: card.id,
label: card.label,
amount: card.amount,
subLabel: card.subLabel,
unit: card.unit,
})),
checkPoints: api.check_points.map((cp) => ({
id: cp.id,
type: cp.type as CheckPointType,
message: cp.message,
highlights: cp.highlights?.map((h) => ({
text: h.text,
color: validateHighlightColor(h.color),
})),
})),
};
}
// ============================================
// 11. Welfare 변환
// ============================================
/**
* Welfare API 응답 → Frontend 타입 변환
* 복리후생비 현황 데이터 변환
*/
export function transformWelfareResponse(api: WelfareApiResponse): WelfareData {
return {
cards: api.cards.map((card) => ({
id: card.id,
label: card.label,
amount: card.amount,
subLabel: card.subLabel,
unit: card.unit,
})),
checkPoints: api.check_points.map((cp) => ({
id: cp.id,
type: cp.type as CheckPointType,
message: cp.message,
highlights: cp.highlights?.map((h) => ({
text: h.text,
color: validateHighlightColor(h.color),
})),
})),
};
}
// ============================================
// 12. Welfare Detail 변환
// ============================================
import type { DetailModalConfig } from '@/components/business/CEODashboard/types';
/**
* WelfareDetail API 응답 → DetailModalConfig 변환
* 복리후생비 상세 모달 설정 생성
*/
export function transformWelfareDetailResponse(api: WelfareDetailApiResponse): DetailModalConfig {
const { summary, monthly_usage, category_distribution, transactions, calculation, quarterly } = api;
// 계산 방식에 따른 calculationCards 생성
const calculationCards = calculation.type === 'fixed'
? {
title: '복리후생비 계산',
subtitle: `직원당 정액 금액/월 ${(calculation.fixed_amount_per_month ?? 200000).toLocaleString()}`,
cards: [
{ label: '직원 수', value: calculation.employee_count, unit: '명' },
{ label: '연간 직원당 월급 금액', value: calculation.annual_amount_per_employee ?? 0, unit: '원', operator: '×' as const },
{ label: '당해년도 복리후생비 총 한도', value: calculation.annual_limit, unit: '원', operator: '=' as const },
],
}
: {
title: '복리후생비 계산',
subtitle: `연봉 총액 기준 비율 ${((calculation.ratio ?? 0.05) * 100).toFixed(1)}%`,
cards: [
{ label: '연봉 총액', value: calculation.total_salary ?? 0, unit: '원' },
{ label: '비율', value: (calculation.ratio ?? 0.05) * 100, unit: '%', operator: '×' as const },
{ label: '당해년도 복리후생비 총 한도', value: calculation.annual_limit, unit: '원', operator: '=' as const },
],
};
// 분기 라벨 가져오기 (현재 분기 기준)
const currentQuarter = quarterly.find(q => q.used !== null)?.quarter ?? 1;
const quarterLabel = `${currentQuarter}사분기`;
return {
title: '복리후생비 상세',
summaryCards: [
// 1행: 당해년도 기준
{ label: '당해년도 복리후생비 계정', value: summary.annual_account, unit: '원' },
{ label: '당해년도 복리후생비 한도', value: summary.annual_limit, unit: '원' },
{ label: '당해년도 복리후생비 사용', value: summary.annual_used, unit: '원' },
{ label: '당해년도 잔여한도', value: summary.annual_remaining, unit: '원' },
// 2행: 분기 기준
{ label: `${quarterLabel} 복리후생비 총 한도`, value: summary.quarterly_limit, unit: '원' },
{ label: `${quarterLabel} 복리후생비 잔여한도`, value: summary.quarterly_remaining, unit: '원' },
{ label: `${quarterLabel} 복리후생비 사용금액`, value: summary.quarterly_used, unit: '원' },
{ label: `${quarterLabel} 복리후생비 초과 금액`, value: summary.quarterly_exceeded, unit: '원' },
],
barChart: {
title: '월별 복리후생비 사용 추이',
data: monthly_usage.map(item => ({
name: item.label,
value: item.amount,
})),
dataKey: 'value',
xAxisKey: 'name',
color: '#60A5FA',
},
pieChart: {
title: '항목별 사용 비율',
data: category_distribution.map(item => ({
name: item.category_label,
value: item.amount,
percentage: item.percentage,
color: item.color,
})),
},
table: {
title: '일별 복리후생비 사용 내역',
columns: [
{ key: 'no', label: 'No.', align: 'center' },
{ key: 'cardName', label: '카드명', align: 'left' },
{ key: 'user', label: '사용자', align: 'center' },
{ key: 'date', label: '사용일자', align: 'center', format: 'date' },
{ key: 'store', label: '가맹점명', align: 'left' },
{ key: 'amount', label: '사용금액', align: 'right', format: 'currency' },
{ key: 'usageType', label: '사용항목', align: 'center' },
],
data: transactions.map((tx, idx) => ({
no: idx + 1,
cardName: tx.card_name,
user: tx.user_name,
date: tx.transaction_date,
store: tx.merchant_name,
amount: tx.amount,
usageType: tx.usage_type_label,
})),
filters: [
{
key: 'usageType',
options: [
{ value: 'all', label: '전체' },
{ value: '식비', label: '식비' },
{ value: '건강검진', label: '건강검진' },
{ value: '경조사비', label: '경조사비' },
{ value: '기타', label: '기타' },
],
defaultValue: 'all',
},
{
key: 'sortOrder',
options: [
{ value: 'latest', label: '최신순' },
{ value: 'oldest', label: '등록순' },
{ value: 'amountDesc', label: '금액 높은순' },
{ value: 'amountAsc', label: '금액 낮은순' },
],
defaultValue: 'latest',
},
],
showTotal: true,
totalLabel: '합계',
totalValue: transactions.reduce((sum, tx) => sum + tx.amount, 0),
totalColumnKey: 'amount',
},
calculationCards,
quarterlyTable: {
title: '복리후생비 현황',
rows: [
{
label: '한도금액',
q1: quarterly[0]?.limit ?? 0,
q2: quarterly[1]?.limit ?? 0,
q3: quarterly[2]?.limit ?? 0,
q4: quarterly[3]?.limit ?? 0,
total: quarterly.reduce((sum, q) => sum + (q.limit ?? 0), 0),
},
{
label: '이월금액',
q1: quarterly[0]?.carryover ?? 0,
q2: quarterly[1]?.carryover ?? '',
q3: quarterly[2]?.carryover ?? '',
q4: quarterly[3]?.carryover ?? '',
total: '',
},
{
label: '사용금액',
q1: quarterly[0]?.used ?? '',
q2: quarterly[1]?.used ?? '',
q3: quarterly[2]?.used ?? '',
q4: quarterly[3]?.used ?? '',
total: quarterly.reduce((sum, q) => sum + (q.used ?? 0), 0) || '',
},
{
label: '잔여한도',
q1: quarterly[0]?.remaining ?? '',
q2: quarterly[1]?.remaining ?? '',
q3: quarterly[2]?.remaining ?? '',
q4: quarterly[3]?.remaining ?? '',
total: '',
},
{
label: '초과금액',
q1: quarterly[0]?.exceeded ?? '',
q2: quarterly[1]?.exceeded ?? '',
q3: quarterly[2]?.exceeded ?? '',
q4: quarterly[3]?.exceeded ?? '',
total: quarterly.reduce((sum, q) => sum + (q.exceeded ?? 0), 0) || '',
},
],
},
};
}
// ============================================
// 13. Purchase Dashboard Detail 변환 (me1)
// ============================================
/**
* Purchase Dashboard Detail API 응답 → DetailModalConfig 변환
* 매입 상세 모달 설정 생성 (me1)
*/
export function transformPurchaseDetailResponse(api: PurchaseDashboardDetailApiResponse): DetailModalConfig {
const { summary, monthly_trend, by_type, items } = api;
const changeRateText = summary.change_rate >= 0
? `+${summary.change_rate.toFixed(1)}%`
: `${summary.change_rate.toFixed(1)}%`;
return {
title: '매입 상세',
summaryCards: [
{ label: '당월 매입액', value: summary.current_month_amount, unit: '원' },
{ label: '전월 대비', value: changeRateText },
],
barChart: {
title: '월별 매입 추이',
data: monthly_trend.map(item => ({
name: item.label,
value: item.amount,
})),
dataKey: 'value',
xAxisKey: 'name',
color: '#60A5FA',
},
pieChart: {
title: '유형별 매입 비율',
data: by_type.map(item => ({
name: item.type_label,
value: item.amount,
percentage: 0, // API에서 계산하거나 프론트에서 계산
color: item.color,
})),
},
table: {
title: '매입 내역',
columns: [
{ key: 'no', label: 'No.', align: 'center' },
{ key: 'date', label: '매입일자', align: 'center', format: 'date' },
{ key: 'vendor', label: '거래처명', align: 'left' },
{ key: 'amount', label: '금액', align: 'right', format: 'currency' },
{ key: 'type', label: '유형', align: 'center' },
],
data: items.map((item, idx) => ({
no: idx + 1,
date: item.purchase_date,
vendor: item.vendor_name,
amount: item.amount,
type: item.type_label,
})),
filters: [
{
key: 'type',
options: [
{ value: 'all', label: '전체' },
...by_type.map(t => ({ value: t.type, label: t.type_label })),
],
defaultValue: 'all',
},
{
key: 'sortOrder',
options: [
{ value: 'latest', label: '최신순' },
{ value: 'oldest', label: '등록순' },
{ value: 'amountDesc', label: '금액 높은순' },
{ value: 'amountAsc', label: '금액 낮은순' },
],
defaultValue: 'latest',
},
],
showTotal: true,
totalLabel: '합계',
totalValue: items.reduce((sum, item) => sum + item.amount, 0),
totalColumnKey: 'amount',
},
};
}
// ============================================
// 14. Card Dashboard Detail 변환 (me2)
// ============================================
/**
* Card Dashboard Detail API 응답 → DetailModalConfig 변환
* 카드 상세 모달 설정 생성 (me2)
*/
export function transformCardDetailResponse(api: CardDashboardDetailApiResponse): DetailModalConfig {
const { summary, monthly_trend, by_user, items } = api;
const changeRate = summary.previous_month_total > 0
? ((summary.current_month_total - summary.previous_month_total) / summary.previous_month_total * 100)
: 0;
const changeRateText = changeRate >= 0
? `+${changeRate.toFixed(1)}%`
: `${changeRate.toFixed(1)}%`;
return {
title: '카드 사용 상세',
summaryCards: [
{ label: '당월 사용액', value: summary.current_month_total, unit: '원' },
{ label: '전월 대비', value: changeRateText },
{ label: '당월 건수', value: summary.current_month_count, unit: '건' },
],
barChart: {
title: '월별 카드 사용 추이',
data: monthly_trend.map(item => ({
name: item.label,
value: item.amount,
})),
dataKey: 'value',
xAxisKey: 'name',
color: '#34D399',
},
pieChart: {
title: '사용자별 사용 비율',
data: by_user.map(item => ({
name: item.user_name,
value: item.amount,
percentage: 0,
color: item.color,
})),
},
table: {
title: '카드 사용 내역',
columns: [
{ key: 'no', label: 'No.', align: 'center' },
{ key: 'cardName', label: '카드명', align: 'left' },
{ key: 'user', label: '사용자', align: 'center' },
{ key: 'date', label: '사용일자', align: 'center', format: 'date' },
{ key: 'store', label: '가맹점명', align: 'left' },
{ key: 'amount', label: '사용금액', align: 'right', format: 'currency' },
{ key: 'usageType', label: '사용항목', align: 'center' },
],
data: items.map((item, idx) => ({
no: idx + 1,
cardName: item.card_name,
user: item.user_name,
date: item.transaction_date,
store: item.merchant_name,
amount: item.amount,
usageType: item.usage_type,
})),
filters: [
{
key: 'user',
options: [
{ value: 'all', label: '전체' },
...by_user.map(u => ({ value: u.user_name, label: u.user_name })),
],
defaultValue: 'all',
},
{
key: 'sortOrder',
options: [
{ value: 'latest', label: '최신순' },
{ value: 'oldest', label: '등록순' },
{ value: 'amountDesc', label: '금액 높은순' },
{ value: 'amountAsc', label: '금액 낮은순' },
],
defaultValue: 'latest',
},
],
showTotal: true,
totalLabel: '합계',
totalValue: items.reduce((sum, item) => sum + item.amount, 0),
totalColumnKey: 'amount',
},
};
}
// ============================================
// 15. Bill Dashboard Detail 변환 (me3)
// ============================================
/**
* Bill Dashboard Detail API 응답 → DetailModalConfig 변환
* 발행어음 상세 모달 설정 생성 (me3)
*/
export function transformBillDetailResponse(api: BillDashboardDetailApiResponse): DetailModalConfig {
const { summary, monthly_trend, by_vendor, items } = api;
const changeRateText = summary.change_rate >= 0
? `+${summary.change_rate.toFixed(1)}%`
: `${summary.change_rate.toFixed(1)}%`;
// 거래처별 가로 막대 차트 데이터
const horizontalBarData = by_vendor.map(item => ({
name: item.vendor_name,
value: item.amount,
}));
return {
title: '발행어음 상세',
summaryCards: [
{ label: '당월 발행어음', value: summary.current_month_amount, unit: '원' },
{ label: '전월 대비', value: changeRateText },
],
barChart: {
title: '월별 발행어음 추이',
data: monthly_trend.map(item => ({
name: item.label,
value: item.amount,
})),
dataKey: 'value',
xAxisKey: 'name',
color: '#F59E0B',
},
horizontalBarChart: {
title: '거래처별 발행어음',
data: horizontalBarData,
dataKey: 'value',
yAxisKey: 'name',
color: '#8B5CF6',
},
table: {
title: '발행어음 내역',
columns: [
{ key: 'no', label: 'No.', align: 'center' },
{ key: 'vendor', label: '거래처명', align: 'left' },
{ key: 'issueDate', label: '발행일', align: 'center', format: 'date' },
{ key: 'dueDate', label: '만기일', align: 'center', format: 'date' },
{ key: 'amount', label: '금액', align: 'right', format: 'currency' },
{ key: 'status', label: '상태', align: 'center' },
],
data: items.map((item, idx) => ({
no: idx + 1,
vendor: item.vendor_name,
issueDate: item.issue_date,
dueDate: item.due_date,
amount: item.amount,
status: item.status_label,
})),
filters: [
{
key: 'vendor',
options: [
{ value: 'all', label: '전체' },
...by_vendor.map(v => ({ value: v.vendor_name, label: v.vendor_name })),
],
defaultValue: 'all',
},
{
key: 'status',
options: [
{ value: 'all', label: '전체' },
{ value: 'pending', label: '대기중' },
{ value: 'paid', label: '결제완료' },
{ value: 'overdue', label: '연체' },
],
defaultValue: 'all',
},
{
key: 'sortOrder',
options: [
{ value: 'latest', label: '최신순' },
{ value: 'oldest', label: '등록순' },
{ value: 'amountDesc', label: '금액 높은순' },
{ value: 'amountAsc', label: '금액 낮은순' },
],
defaultValue: 'latest',
},
],
showTotal: true,
totalLabel: '합계',
totalValue: items.reduce((sum, item) => sum + item.amount, 0),
totalColumnKey: 'amount',
},
};
}
// ============================================
// 16. Expected Expense Dashboard Detail 변환 (me1~me4 통합)
// ============================================
// cardId별 제목 및 차트 설정 매핑
const EXPENSE_CARD_CONFIG: Record<string, {
title: string;
tableTitle: string;
summaryLabel: string;
barChartTitle: string;
pieChartTitle?: string;
horizontalBarChartTitle?: string;
hasBarChart: boolean;
hasPieChart: boolean;
hasHorizontalBarChart: boolean;
}> = {
me1: {
title: '당월 매입 상세',
tableTitle: '일별 매입 내역',
summaryLabel: '당월 매입',
barChartTitle: '월별 매입 추이',
pieChartTitle: '거래처별 매입 비율',
hasBarChart: true,
hasPieChart: true,
hasHorizontalBarChart: false,
},
me2: {
title: '당월 카드 상세',
tableTitle: '일별 카드 사용 내역',
summaryLabel: '당월 카드 사용',
barChartTitle: '월별 카드 사용 추이',
pieChartTitle: '거래처별 카드 사용 비율',
hasBarChart: true,
hasPieChart: true,
hasHorizontalBarChart: false,
},
me3: {
title: '당월 발행어음 상세',
tableTitle: '일별 발행어음 내역',
summaryLabel: '당월 발행어음 사용',
barChartTitle: '월별 발행어음 추이',
horizontalBarChartTitle: '당월 거래처별 발행어음',
hasBarChart: true,
hasPieChart: false,
hasHorizontalBarChart: true,
},
me4: {
title: '당월 지출 예상 상세',
tableTitle: '당월 지출 승인 내역서',
summaryLabel: '당월 지출 예상',
barChartTitle: '',
hasBarChart: false,
hasPieChart: false,
hasHorizontalBarChart: false,
},
};
/**
* ExpectedExpense Dashboard Detail API 응답 → DetailModalConfig 변환
* 카드별 지출 상세 모달 설정 생성 (me1: 매입, me2: 카드, me3: 발행어음, me4: 전체)
*
* @param api API 응답 데이터
* @param cardId 카드 ID (me1~me4), 기본값 me4
*/
export function transformExpectedExpenseDetailResponse(
api: ExpectedExpenseDashboardDetailApiResponse,
cardId: string = 'me4'
): DetailModalConfig {
const { summary, monthly_trend, vendor_distribution, items, footer_summary } = api;
const changeRateText = summary.change_rate >= 0
? `+${summary.change_rate.toFixed(1)}%`
: `${summary.change_rate.toFixed(1)}%`;
// cardId별 설정 가져오기 (기본값: me4)
const config = EXPENSE_CARD_CONFIG[cardId] || EXPENSE_CARD_CONFIG.me4;
// 거래처 필터 옵션 생성
const vendorOptions = [{ value: 'all', label: '전체' }];
const uniqueVendors = [...new Set(items.map(item => item.vendor_name).filter(Boolean))];
uniqueVendors.forEach(vendor => {
vendorOptions.push({ value: vendor, label: vendor });
});
// 결과 객체 생성
const result: DetailModalConfig = {
title: config.title,
summaryCards: [
{ label: config.summaryLabel, value: summary.total_amount, unit: '원' },
{ label: '전월 대비', value: changeRateText },
{ label: '지출 후 예상 잔액', value: summary.remaining_balance, unit: '원' },
],
table: {
title: config.tableTitle,
columns: [
{ key: 'no', label: 'No.', align: 'center' },
{ key: 'paymentDate', label: '결제예정일', align: 'center', format: 'date' },
{ key: 'item', label: '항목', align: 'left' },
{ key: 'amount', label: '금액', align: 'right', format: 'currency' },
{ key: 'vendor', label: '거래처', align: 'left' },
{ key: 'account', label: '계정과목', align: 'center' },
],
data: items.map((item, idx) => ({
no: idx + 1,
paymentDate: item.payment_date,
item: item.item_name,
amount: item.amount,
vendor: item.vendor_name,
account: item.account_title,
})),
filters: [
{
key: 'vendor',
options: vendorOptions,
defaultValue: 'all',
},
{
key: 'sortOrder',
options: [
{ value: 'latest', label: '최신순' },
{ value: 'oldest', label: '등록순' },
{ value: 'amountDesc', label: '금액 높은순' },
{ value: 'amountAsc', label: '금액 낮은순' },
],
defaultValue: 'latest',
},
],
showTotal: true,
totalLabel: '합계',
totalValue: footer_summary.total_amount,
totalColumnKey: 'amount',
footerSummary: [
{ label: `${footer_summary.item_count}`, value: footer_summary.total_amount },
],
},
};
// barChart 추가 (me1, me2, me3)
if (config.hasBarChart && monthly_trend && monthly_trend.length > 0) {
result.barChart = {
title: config.barChartTitle,
data: monthly_trend.map(item => ({
name: item.label,
value: item.amount,
})),
dataKey: 'value',
xAxisKey: 'name',
color: '#60A5FA',
};
}
// pieChart 추가 (me1, me2)
if (config.hasPieChart && vendor_distribution && vendor_distribution.length > 0) {
result.pieChart = {
title: config.pieChartTitle || '분포',
data: vendor_distribution.map(item => ({
name: item.name,
value: item.value,
percentage: item.percentage,
color: item.color,
})),
};
}
// horizontalBarChart 추가 (me3)
if (config.hasHorizontalBarChart && vendor_distribution && vendor_distribution.length > 0) {
result.horizontalBarChart = {
title: config.horizontalBarChartTitle || '거래처별 분포',
data: vendor_distribution.map(item => ({
name: item.name,
value: item.value,
})),
color: '#60A5FA',
};
}
return result;
}