Files
sam-react-prod/src/lib/api/dashboard/transformers.ts
권혁성 d824c913e8 fix: 당월 예상 지출 모달 API 통합
- 모든 카드(me1~me4)가 expected-expenses/dashboard-detail API 사용
- transaction_type 파라미터로 필터링 (me1=purchase, me2=card, me3=bill)
- cardId별 모달 제목 동적 설정 추가
- 불필요한 import 정리
2026-01-22 23:16:56 +09:00

1255 lines
39 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,
} from './types';
import type {
DailyReportData,
ReceivableData,
DebtCollectionData,
MonthlyExpenseData,
CardManagementData,
TodayIssueItem,
TodayIssueListItem,
TodayIssueListBadgeType,
CalendarScheduleItem,
CheckPoint,
CheckPointType,
VatData,
EntertainmentData,
WelfareData,
HighlightColor,
} from '@/components/business/CEODashboard/types';
// ============================================
// 헬퍼 함수
// ============================================
/**
* 금액 포맷팅
* @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: '/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: '/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개 카드 구조 유지: 카드, 가지급금, 법인세 예상 가중, 대표자 종합세 예상 가중
* 주의: 가지급금, 법인세, 종합세 관련 데이터는 별도 API 필요 (현재 목업 유지)
*/
export function transformCardManagementResponse(
summaryApi: CardTransactionApiResponse,
fallbackData?: CardManagementData
): CardManagementData {
const changeRate = calculateChangeRate(summaryApi.current_month_total, summaryApi.previous_month_total);
return {
// 가지급금 관련 경고 배너 (별도 API 필요)
warningBanner: fallbackData?.warningBanner,
cards: [
// cm1: 카드 사용액 (API 데이터)
{
id: 'cm1',
label: '카드',
amount: summaryApi.current_month_total,
previousLabel: `전월 대비 ${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}%`,
},
// cm2: 가지급금 (별도 API 필요 - 현재 fallback)
fallbackData?.cards[1] ?? {
id: 'cm2',
label: '가지급금',
amount: 0,
},
// cm3: 법인세 예상 가중 (별도 API 필요 - 현재 fallback)
fallbackData?.cards[2] ?? {
id: 'cm3',
label: '법인세 예상 가중',
amount: 0,
},
// cm4: 대표자 종합세 예상 가중 (별도 API 필요 - 현재 fallback)
fallbackData?.cards[3] ?? {
id: 'cm4',
label: '대표자 종합세 예상 가중',
amount: 0,
},
],
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: item.path,
isHighlighted: item.isHighlighted,
}));
}
// ============================================
// 7. TodayIssue 변환
// ============================================
/** 유효한 뱃지 타입 목록 */
const VALID_BADGE_TYPES: TodayIssueListBadgeType[] = [
'수주 성공',
'주식 이슈',
'직정 제고',
'지출예상내역서',
'세금 신고',
'결재 요청',
'기타',
];
/**
* API 뱃지 문자열 → Frontend 뱃지 타입 변환
* 유효하지 않은 뱃지는 '기타'로 폴백
*/
function validateBadgeType(badge: string): TodayIssueListBadgeType {
if (VALID_BADGE_TYPES.includes(badge as TodayIssueListBadgeType)) {
return badge as TodayIssueListBadgeType;
}
return '기타';
}
/**
* TodayIssue API 응답 → Frontend 타입 변환
* 오늘의 이슈 리스트 데이터 변환
*/
export function transformTodayIssueResponse(api: TodayIssueApiResponse): {
items: TodayIssueListItem[];
totalCount: number;
} {
return {
items: api.items.map((item) => ({
id: item.id,
badge: validateBadgeType(item.badge),
content: item.content,
time: item.time,
date: item.date,
needsApproval: item.needsApproval ?? false,
path: item.path,
})),
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,
endTime: item.endTime,
isAllDay: item.isAllDay,
type: item.type,
department: item.department,
personName: item.personName,
color: item.color,
})),
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_TITLES: Record<string, { title: string; tableTitle: string; summaryLabel: string }> = {
me1: { title: '당월 매입 상세', tableTitle: '매입 내역', summaryLabel: '당월 총 매입' },
me2: { title: '당월 카드결제 상세', tableTitle: '카드결제 내역', summaryLabel: '당월 총 카드결제' },
me3: { title: '당월 발행어음 상세', tableTitle: '발행어음 내역', summaryLabel: '당월 총 발행어음' },
me4: { title: '지출예상 상세', tableTitle: '지출예상 내역', summaryLabel: '당월 총 지출예상' },
};
/**
* 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, items } = api;
const changeRateText = summary.change_rate >= 0
? `+${summary.change_rate.toFixed(1)}%`
: `${summary.change_rate.toFixed(1)}%`;
// cardId별 제목 가져오기 (기본값: me4)
const titles = EXPENSE_CARD_TITLES[cardId] || EXPENSE_CARD_TITLES.me4;
return {
title: titles.title,
summaryCards: [
{ label: titles.summaryLabel, value: summary.total_amount, unit: '원' },
{ label: '전월 대비', value: changeRateText },
{ label: '지출 후 예상 잔액', value: summary.remaining_balance, unit: '원' },
],
// 차트 없음
table: {
title: titles.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: [
{ value: 'all', label: '전체' },
],
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,
},
},
};
}