2026-01-20 18:51:05 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* CEO Dashboard API 응답 → Frontend 타입 변환 함수
|
|
|
|
|
|
*
|
|
|
|
|
|
* 참조: docs/plans/AI_리포트_키워드_색상체계_가이드_v1.4.md
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import type {
|
|
|
|
|
|
DailyReportApiResponse,
|
|
|
|
|
|
ReceivablesApiResponse,
|
|
|
|
|
|
BadDebtApiResponse,
|
|
|
|
|
|
ExpectedExpenseApiResponse,
|
|
|
|
|
|
CardTransactionApiResponse,
|
2026-01-21 10:38:09 +09:00
|
|
|
|
StatusBoardApiResponse,
|
|
|
|
|
|
TodayIssueApiResponse,
|
|
|
|
|
|
CalendarApiResponse,
|
|
|
|
|
|
VatApiResponse,
|
|
|
|
|
|
EntertainmentApiResponse,
|
|
|
|
|
|
WelfareApiResponse,
|
2026-01-22 22:48:29 +09:00
|
|
|
|
WelfareDetailApiResponse,
|
|
|
|
|
|
PurchaseDashboardDetailApiResponse,
|
|
|
|
|
|
CardDashboardDetailApiResponse,
|
|
|
|
|
|
BillDashboardDetailApiResponse,
|
|
|
|
|
|
ExpectedExpenseDashboardDetailApiResponse,
|
2026-01-23 09:04:56 +09:00
|
|
|
|
LoanDashboardApiResponse,
|
|
|
|
|
|
TaxSimulationApiResponse,
|
2026-01-20 18:51:05 +09:00
|
|
|
|
} from './types';
|
|
|
|
|
|
|
|
|
|
|
|
import type {
|
|
|
|
|
|
DailyReportData,
|
|
|
|
|
|
ReceivableData,
|
|
|
|
|
|
DebtCollectionData,
|
|
|
|
|
|
MonthlyExpenseData,
|
|
|
|
|
|
CardManagementData,
|
2026-01-21 10:38:09 +09:00
|
|
|
|
TodayIssueItem,
|
|
|
|
|
|
TodayIssueListItem,
|
|
|
|
|
|
TodayIssueListBadgeType,
|
2026-01-28 15:48:17 +09:00
|
|
|
|
TodayIssueNotificationType,
|
2026-01-21 10:38:09 +09:00
|
|
|
|
CalendarScheduleItem,
|
2026-01-20 18:51:05 +09:00
|
|
|
|
CheckPoint,
|
|
|
|
|
|
CheckPointType,
|
2026-01-21 10:38:09 +09:00
|
|
|
|
VatData,
|
|
|
|
|
|
EntertainmentData,
|
|
|
|
|
|
WelfareData,
|
|
|
|
|
|
HighlightColor,
|
2026-01-20 18:51:05 +09:00
|
|
|
|
} from '@/components/business/CEODashboard/types';
|
2026-02-20 10:45:47 +09:00
|
|
|
|
import { formatNumber } from '@/lib/utils/amount';
|
2026-01-20 18:51:05 +09:00
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 헬퍼 함수
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
2026-01-25 15:05:24 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 네비게이션 경로 정규화
|
|
|
|
|
|
* - /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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 18:51:05 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 금액 포맷팅
|
|
|
|
|
|
* @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) {
|
2026-02-20 10:45:47 +09:00
|
|
|
|
return `${formatNumber(Math.round(amount / 10000))}만원`;
|
2026-01-20 18:51:05 +09:00
|
|
|
|
}
|
2026-02-20 10:45:47 +09:00
|
|
|
|
return `${formatNumber(amount)}원`;
|
2026-01-20 18:51:05 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 날짜 포맷팅 (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 변환
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
2026-01-22 09:47:05 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 운영자금 안정성에 따른 색상 반환
|
|
|
|
|
|
* 참조: 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}이 확보되어 있습니다.`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 18:51:05 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 일일 일보 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 },
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 09:47:05 +09:00
|
|
|
|
// 현금성 자산 + 운영자금 안정성 현황
|
2026-01-20 18:51:05 +09:00
|
|
|
|
const cashAsset = api.cash_asset_total;
|
2026-01-22 09:47:05 +09:00
|
|
|
|
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' });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 18:51:05 +09:00
|
|
|
|
checkPoints.push({
|
|
|
|
|
|
id: 'dr-cash-asset',
|
2026-01-22 09:47:05 +09:00
|
|
|
|
type: isDeficit || isAlmostEmpty ? 'warning' as CheckPointType : 'info' as CheckPointType,
|
|
|
|
|
|
message: `총 현금성 자산이 ${formatAmount(cashAsset)}입니다. ${stabilityMessage}`,
|
|
|
|
|
|
highlights,
|
2026-01-20 18:51:05 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return checkPoints;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 14:16:17 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 변동률 → changeRate/changeDirection 변환 헬퍼
|
|
|
|
|
|
*/
|
|
|
|
|
|
function toChangeFields(rate?: number): { changeRate?: string; changeDirection?: 'up' | 'down' } {
|
|
|
|
|
|
if (rate === undefined || rate === null) return {};
|
|
|
|
|
|
const direction = rate >= 0 ? 'up' as const : 'down' as const;
|
|
|
|
|
|
const sign = rate >= 0 ? '+' : '';
|
|
|
|
|
|
return {
|
|
|
|
|
|
changeRate: `${sign}${rate.toFixed(1)}%`,
|
|
|
|
|
|
changeDirection: direction,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 18:51:05 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* DailyReport API 응답 → Frontend 타입 변환
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function transformDailyReportResponse(api: DailyReportApiResponse): DailyReportData {
|
2026-01-30 14:16:17 +09:00
|
|
|
|
const change = api.daily_change;
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: 백엔드 daily_change 필드 제공 시 더미값 제거
|
|
|
|
|
|
const FALLBACK_CHANGES = {
|
|
|
|
|
|
cash_asset: { changeRate: '+5.2%', changeDirection: 'up' as const },
|
|
|
|
|
|
foreign_currency: { changeRate: '+2.1%', changeDirection: 'up' as const },
|
|
|
|
|
|
income: { changeRate: '+12.0%', changeDirection: 'up' as const },
|
|
|
|
|
|
expense: { changeRate: '-8.0%', changeDirection: 'down' as const },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-20 18:51:05 +09:00
|
|
|
|
return {
|
|
|
|
|
|
date: formatDate(api.date, api.day_of_week),
|
|
|
|
|
|
cards: [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'dr1',
|
|
|
|
|
|
label: '현금성 자산 합계',
|
|
|
|
|
|
amount: api.cash_asset_total,
|
2026-01-30 14:16:17 +09:00
|
|
|
|
...(change?.cash_asset_change_rate !== undefined
|
|
|
|
|
|
? toChangeFields(change.cash_asset_change_rate)
|
|
|
|
|
|
: FALLBACK_CHANGES.cash_asset),
|
2026-01-20 18:51:05 +09:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'dr2',
|
|
|
|
|
|
label: '외국환(USD) 합계',
|
|
|
|
|
|
amount: api.foreign_currency_total,
|
|
|
|
|
|
currency: 'USD',
|
2026-01-30 14:16:17 +09:00
|
|
|
|
...(change?.foreign_currency_change_rate !== undefined
|
|
|
|
|
|
? toChangeFields(change.foreign_currency_change_rate)
|
|
|
|
|
|
: FALLBACK_CHANGES.foreign_currency),
|
2026-01-20 18:51:05 +09:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'dr3',
|
|
|
|
|
|
label: '입금 합계',
|
|
|
|
|
|
amount: api.krw_totals.income,
|
2026-01-30 14:16:17 +09:00
|
|
|
|
...(change?.income_change_rate !== undefined
|
|
|
|
|
|
? toChangeFields(change.income_change_rate)
|
|
|
|
|
|
: FALLBACK_CHANGES.income),
|
2026-01-20 18:51:05 +09:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'dr4',
|
|
|
|
|
|
label: '출금 합계',
|
|
|
|
|
|
amount: api.krw_totals.expense,
|
2026-01-30 14:16:17 +09:00
|
|
|
|
...(change?.expense_change_rate !== undefined
|
|
|
|
|
|
? toChangeFields(change.expense_change_rate)
|
|
|
|
|
|
: FALLBACK_CHANGES.expense),
|
2026-01-20 18:51:05 +09:00
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
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),
|
2026-01-28 16:02:32 +09:00
|
|
|
|
//detailButtonLabel: '미수금 상세',
|
2026-01-25 15:05:24 +09:00
|
|
|
|
detailButtonPath: normalizePath('/accounting/receivables-status'),
|
2026-01-20 18:51:05 +09:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 15:23:35 +09:00
|
|
|
|
// TODO: 백엔드 per-card sub_label/count 제공 시 더미값 제거
|
|
|
|
|
|
// 채권추심 카드별 더미 서브라벨 (회사명 + 건수)
|
|
|
|
|
|
const DEBT_COLLECTION_FALLBACK_SUB_LABELS: Record<string, { company: string; count: number }> = {
|
|
|
|
|
|
dc1: { company: '(주)부산화학 외', count: 5 },
|
|
|
|
|
|
dc2: { company: '(주)삼성테크 외', count: 3 },
|
|
|
|
|
|
dc3: { company: '(주)대한전자 외', count: 2 },
|
|
|
|
|
|
dc4: { company: '(주)한국정밀 외', count: 3 },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 채권추심 subLabel 생성 헬퍼
|
|
|
|
|
|
* dc1(누적)은 API client_count 사용, 나머지는 더미값
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buildDebtSubLabel(cardId: string, clientCount?: number): string | undefined {
|
|
|
|
|
|
const fallback = DEBT_COLLECTION_FALLBACK_SUB_LABELS[cardId];
|
|
|
|
|
|
if (!fallback) return undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const count = cardId === 'dc1' && clientCount !== undefined ? clientCount : fallback.count;
|
|
|
|
|
|
if (count <= 0) return undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const remaining = count - 1;
|
|
|
|
|
|
if (remaining > 0) {
|
|
|
|
|
|
return `${fallback.company} ${remaining}건`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return fallback.company.replace(/ 외$/, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 18:51:05 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* BadDebt API 응답 → Frontend 타입 변환
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function transformDebtCollectionResponse(api: BadDebtApiResponse): DebtCollectionData {
|
|
|
|
|
|
return {
|
|
|
|
|
|
cards: [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'dc1',
|
|
|
|
|
|
label: '누적 악성채권',
|
|
|
|
|
|
amount: api.total_amount,
|
2026-01-30 15:23:35 +09:00
|
|
|
|
subLabel: buildDebtSubLabel('dc1', api.client_count),
|
2026-01-20 18:51:05 +09:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'dc2',
|
|
|
|
|
|
label: '추심중',
|
|
|
|
|
|
amount: api.collecting_amount,
|
2026-01-30 15:23:35 +09:00
|
|
|
|
subLabel: buildDebtSubLabel('dc2'),
|
2026-01-20 18:51:05 +09:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'dc3',
|
|
|
|
|
|
label: '법적조치',
|
|
|
|
|
|
amount: api.legal_action_amount,
|
2026-01-30 15:23:35 +09:00
|
|
|
|
subLabel: buildDebtSubLabel('dc3'),
|
2026-01-20 18:51:05 +09:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'dc4',
|
|
|
|
|
|
label: '회수완료',
|
|
|
|
|
|
amount: api.recovered_amount,
|
2026-01-30 15:23:35 +09:00
|
|
|
|
subLabel: buildDebtSubLabel('dc4'),
|
2026-01-20 18:51:05 +09:00
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
checkPoints: generateDebtCollectionCheckPoints(api),
|
2026-01-25 15:05:24 +09:00
|
|
|
|
detailButtonPath: normalizePath('/accounting/bad-debt-collection'),
|
2026-01-20 18:51:05 +09:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 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 타입 변환
|
2026-01-23 09:04:56 +09:00
|
|
|
|
* 4개 카드 구조:
|
|
|
|
|
|
* - cm1: 카드 사용액 (CardTransaction API)
|
|
|
|
|
|
* - cm2: 가지급금 (LoanDashboard API)
|
|
|
|
|
|
* - cm3: 법인세 예상 가중 (TaxSimulation API - corporate_tax.difference)
|
|
|
|
|
|
* - cm4: 대표자 종합세 예상 가중 (TaxSimulation API - income_tax.difference)
|
2026-01-20 18:51:05 +09:00
|
|
|
|
*/
|
|
|
|
|
|
export function transformCardManagementResponse(
|
2026-01-22 22:48:29 +09:00
|
|
|
|
summaryApi: CardTransactionApiResponse,
|
2026-01-23 09:04:56 +09:00
|
|
|
|
loanApi?: LoanDashboardApiResponse | null,
|
|
|
|
|
|
taxApi?: TaxSimulationApiResponse | null,
|
2026-01-20 18:51:05 +09:00
|
|
|
|
fallbackData?: CardManagementData
|
|
|
|
|
|
): CardManagementData {
|
2026-01-22 22:48:29 +09:00
|
|
|
|
const changeRate = calculateChangeRate(summaryApi.current_month_total, summaryApi.previous_month_total);
|
2026-01-20 18:51:05 +09:00
|
|
|
|
|
2026-01-23 09:04:56 +09:00
|
|
|
|
// 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;
|
|
|
|
|
|
|
2026-01-20 18:51:05 +09:00
|
|
|
|
return {
|
2026-01-23 09:04:56 +09:00
|
|
|
|
// 가지급금 관련 경고 배너 (가지급금 있을 때만 표시)
|
|
|
|
|
|
warningBanner: hasLoanWarning ? fallbackData?.warningBanner : undefined,
|
2026-01-20 18:51:05 +09:00
|
|
|
|
cards: [
|
2026-01-23 09:04:56 +09:00
|
|
|
|
// cm1: 카드 사용액 (CardTransaction API)
|
2026-01-20 18:51:05 +09:00
|
|
|
|
{
|
|
|
|
|
|
id: 'cm1',
|
|
|
|
|
|
label: '카드',
|
2026-01-22 22:48:29 +09:00
|
|
|
|
amount: summaryApi.current_month_total,
|
2026-01-20 18:51:05 +09:00
|
|
|
|
previousLabel: `전월 대비 ${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}%`,
|
|
|
|
|
|
},
|
2026-01-23 09:04:56 +09:00
|
|
|
|
// cm2: 가지급금 (LoanDashboard API)
|
|
|
|
|
|
{
|
2026-01-20 18:51:05 +09:00
|
|
|
|
id: 'cm2',
|
|
|
|
|
|
label: '가지급금',
|
2026-01-23 09:04:56 +09:00
|
|
|
|
amount: loanAmount,
|
2026-01-20 18:51:05 +09:00
|
|
|
|
},
|
2026-01-23 09:04:56 +09:00
|
|
|
|
// cm3: 법인세 예상 가중 (TaxSimulation API)
|
|
|
|
|
|
{
|
2026-01-20 18:51:05 +09:00
|
|
|
|
id: 'cm3',
|
|
|
|
|
|
label: '법인세 예상 가중',
|
2026-01-23 09:04:56 +09:00
|
|
|
|
amount: corporateTaxDifference,
|
2026-01-20 18:51:05 +09:00
|
|
|
|
},
|
2026-01-23 09:04:56 +09:00
|
|
|
|
// cm4: 대표자 종합세 예상 가중 (TaxSimulation API)
|
|
|
|
|
|
{
|
2026-01-20 18:51:05 +09:00
|
|
|
|
id: 'cm4',
|
|
|
|
|
|
label: '대표자 종합세 예상 가중',
|
2026-01-23 09:04:56 +09:00
|
|
|
|
amount: incomeTaxDifference,
|
2026-01-20 18:51:05 +09:00
|
|
|
|
},
|
|
|
|
|
|
],
|
2026-01-22 22:48:29 +09:00
|
|
|
|
checkPoints: generateCardManagementCheckPoints(summaryApi),
|
2026-01-20 18:51:05 +09:00
|
|
|
|
};
|
2026-01-21 10:38:09 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 6. StatusBoard 변환
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
2026-01-30 15:23:35 +09:00
|
|
|
|
// TODO: 백엔드 sub_label 필드 제공 시 더미값 제거
|
|
|
|
|
|
// API id 기준: orders, bad_debts, safety_stock, tax_deadline, new_clients, leaves, purchases, approvals
|
|
|
|
|
|
const STATUS_BOARD_FALLBACK_SUB_LABELS: Record<string, string> = {
|
|
|
|
|
|
orders: '(주)삼성전자 외',
|
|
|
|
|
|
bad_debts: '주식회사 부산화학 외',
|
|
|
|
|
|
safety_stock: '',
|
|
|
|
|
|
tax_deadline: '',
|
|
|
|
|
|
new_clients: '대한철강 외',
|
|
|
|
|
|
leaves: '',
|
|
|
|
|
|
purchases: '(유)한국정밀 외',
|
|
|
|
|
|
approvals: '구매 결재 외',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 현황판 subLabel 생성 헬퍼
|
|
|
|
|
|
* API sub_label이 있으면 사용, 없으면 더미값 + 건수로 조합
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buildStatusSubLabel(item: { id: string; count: number | string; sub_label?: string }): string | undefined {
|
|
|
|
|
|
// API에서 sub_label 제공 시 우선 사용
|
|
|
|
|
|
if (item.sub_label) return item.sub_label;
|
|
|
|
|
|
|
|
|
|
|
|
// 건수가 0이거나 문자열이면 subLabel 불필요
|
|
|
|
|
|
const count = typeof item.count === 'number' ? item.count : parseInt(String(item.count), 10);
|
|
|
|
|
|
if (isNaN(count) || count <= 0) return undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const fallback = STATUS_BOARD_FALLBACK_SUB_LABELS[item.id];
|
|
|
|
|
|
if (!fallback) return undefined;
|
|
|
|
|
|
|
|
|
|
|
|
// "대한철강 외" + 나머지 건수
|
|
|
|
|
|
const remaining = count - 1;
|
|
|
|
|
|
if (remaining > 0) {
|
|
|
|
|
|
return `${fallback} ${remaining}건`;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 1건이면 "외" 제거하고 이름만
|
|
|
|
|
|
return fallback.replace(/ 외$/, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 10:38:09 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 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,
|
2026-01-30 15:23:35 +09:00
|
|
|
|
subLabel: buildStatusSubLabel(item),
|
2026-01-25 15:05:24 +09:00
|
|
|
|
path: normalizePath(item.path, { addViewMode: true }),
|
2026-01-21 10:38:09 +09:00
|
|
|
|
isHighlighted: item.isHighlighted,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 7. TodayIssue 변환
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
2026-01-28 15:48:17 +09:00
|
|
|
|
/** 유효한 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',
|
2026-01-21 10:38:09 +09:00
|
|
|
|
];
|
|
|
|
|
|
|
2026-01-28 15:58:27 +09:00
|
|
|
|
/** notification_type → 한글 badge 변환 매핑
|
|
|
|
|
|
* 백엔드 TodayIssue.php BADGE 상수와 동기화!
|
|
|
|
|
|
*/
|
2026-01-28 15:48:17 +09:00
|
|
|
|
const NOTIFICATION_TYPE_TO_BADGE: Record<TodayIssueNotificationType, TodayIssueListBadgeType> = {
|
2026-01-28 15:58:27 +09:00
|
|
|
|
sales_order: '수주등록',
|
|
|
|
|
|
bad_debt: '추심이슈',
|
|
|
|
|
|
safety_stock: '안전재고',
|
|
|
|
|
|
expected_expense: '지출 승인대기',
|
2026-01-28 15:48:17 +09:00
|
|
|
|
vat_report: '세금 신고',
|
|
|
|
|
|
approval_request: '결재 요청',
|
|
|
|
|
|
new_vendor: '신규거래처',
|
|
|
|
|
|
deposit: '입금',
|
|
|
|
|
|
withdrawal: '출금',
|
|
|
|
|
|
other: '기타',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-28 15:58:27 +09:00
|
|
|
|
/** 한글 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',
|
2026-01-28 16:02:32 +09:00
|
|
|
|
'신규업체': 'new_vendor', // 변형
|
2026-01-28 15:58:27 +09:00
|
|
|
|
'입금': 'deposit',
|
|
|
|
|
|
'출금': 'withdrawal',
|
|
|
|
|
|
// === 혹시 모를 변형 (안전장치) ===
|
|
|
|
|
|
'수주 등록': 'sales_order',
|
|
|
|
|
|
'추심 이슈': 'bad_debt',
|
|
|
|
|
|
'안전 재고': 'safety_stock',
|
|
|
|
|
|
'지출승인대기': 'expected_expense',
|
|
|
|
|
|
'세금신고': 'vat_report',
|
|
|
|
|
|
'결재요청': 'approval_request',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-21 10:38:09 +09:00
|
|
|
|
/**
|
2026-01-28 15:48:17 +09:00
|
|
|
|
* API notification_type → Frontend notificationType 변환
|
2026-01-28 15:58:27 +09:00
|
|
|
|
* notification_type이 없으면 badge에서 추론
|
2026-01-21 10:38:09 +09:00
|
|
|
|
*/
|
2026-01-28 15:58:27 +09:00
|
|
|
|
function validateNotificationType(notificationType: string | null, badge?: string): TodayIssueNotificationType {
|
|
|
|
|
|
// 1. notification_type이 유효하면 그대로 사용
|
2026-01-28 15:48:17 +09:00
|
|
|
|
if (notificationType && VALID_NOTIFICATION_TYPES.includes(notificationType as TodayIssueNotificationType)) {
|
|
|
|
|
|
return notificationType as TodayIssueNotificationType;
|
2026-01-21 10:38:09 +09:00
|
|
|
|
}
|
2026-01-28 15:58:27 +09:00
|
|
|
|
// 2. notification_type이 없으면 badge에서 추론
|
|
|
|
|
|
if (badge && BADGE_TO_NOTIFICATION_TYPE[badge]) {
|
|
|
|
|
|
return BADGE_TO_NOTIFICATION_TYPE[badge];
|
|
|
|
|
|
}
|
2026-01-28 15:48:17 +09:00
|
|
|
|
return 'other';
|
2026-01-21 10:38:09 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* TodayIssue API 응답 → Frontend 타입 변환
|
|
|
|
|
|
* 오늘의 이슈 리스트 데이터 변환
|
2026-01-28 15:48:17 +09:00
|
|
|
|
* notification_type 코드값 기반으로 색상 매핑 지원
|
2026-01-21 10:38:09 +09:00
|
|
|
|
*/
|
|
|
|
|
|
export function transformTodayIssueResponse(api: TodayIssueApiResponse): {
|
|
|
|
|
|
items: TodayIssueListItem[];
|
|
|
|
|
|
totalCount: number;
|
|
|
|
|
|
} {
|
|
|
|
|
|
return {
|
2026-01-28 15:48:17 +09:00
|
|
|
|
items: api.items.map((item) => {
|
2026-01-28 15:58:27 +09:00
|
|
|
|
// notification_type이 없으면 badge에서 추론
|
|
|
|
|
|
const notificationType = validateNotificationType(item.notification_type, item.badge);
|
2026-01-28 15:48:17 +09:00
|
|
|
|
// 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 }),
|
|
|
|
|
|
};
|
|
|
|
|
|
}),
|
2026-01-21 10:38:09 +09:00
|
|
|
|
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,
|
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
|
|
|
|
startTime: item.startTime ?? undefined,
|
|
|
|
|
|
endTime: item.endTime ?? undefined,
|
2026-01-21 10:38:09 +09:00
|
|
|
|
isAllDay: item.isAllDay,
|
|
|
|
|
|
type: item.type,
|
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
|
|
|
|
department: item.department ?? undefined,
|
|
|
|
|
|
personName: item.personName ?? undefined,
|
|
|
|
|
|
color: item.color ?? undefined,
|
2026-01-21 10:38:09 +09:00
|
|
|
|
})),
|
|
|
|
|
|
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 {
|
2026-01-30 15:23:35 +09:00
|
|
|
|
// 사용금액(et_used)을 잔여한도(et_remaining) 앞으로 재배치
|
|
|
|
|
|
const reordered = [...api.cards];
|
|
|
|
|
|
const usedIdx = reordered.findIndex((c) => c.id === 'et_used');
|
|
|
|
|
|
const remainIdx = reordered.findIndex((c) => c.id === 'et_remaining');
|
|
|
|
|
|
if (usedIdx > remainIdx && remainIdx >= 0) {
|
|
|
|
|
|
const [used] = reordered.splice(usedIdx, 1);
|
|
|
|
|
|
reordered.splice(remainIdx, 0, used);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 10:38:09 +09:00
|
|
|
|
return {
|
2026-01-30 15:23:35 +09:00
|
|
|
|
cards: reordered.map((card) => ({
|
2026-01-21 10:38:09 +09:00
|
|
|
|
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 {
|
2026-01-30 15:23:35 +09:00
|
|
|
|
// 사용금액(wf_used)을 잔여한도(wf_remaining) 앞으로 재배치
|
|
|
|
|
|
const reordered = [...api.cards];
|
|
|
|
|
|
const usedIdx = reordered.findIndex((c) => c.id === 'wf_used');
|
|
|
|
|
|
const remainIdx = reordered.findIndex((c) => c.id === 'wf_remaining');
|
|
|
|
|
|
if (usedIdx > remainIdx && remainIdx >= 0) {
|
|
|
|
|
|
const [used] = reordered.splice(usedIdx, 1);
|
|
|
|
|
|
reordered.splice(remainIdx, 0, used);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 10:38:09 +09:00
|
|
|
|
return {
|
2026-01-30 15:23:35 +09:00
|
|
|
|
cards: reordered.map((card) => ({
|
2026-01-21 10:38:09 +09:00
|
|
|
|
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),
|
|
|
|
|
|
})),
|
|
|
|
|
|
})),
|
|
|
|
|
|
};
|
2026-01-22 22:48:29 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 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: '복리후생비 계산',
|
2026-02-20 10:45:47 +09:00
|
|
|
|
subtitle: `직원당 정액 금액/월 ${formatNumber(calculation.fixed_amount_per_month ?? 200000)}원`,
|
2026-01-22 22:48:29 +09:00
|
|
|
|
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) || '',
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
2026-01-22 23:16:56 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 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 통합)
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
2026-01-23 13:31:44 +09:00
|
|
|
|
// 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,
|
|
|
|
|
|
},
|
2026-01-22 23:16:56 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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 {
|
2026-01-23 13:31:44 +09:00
|
|
|
|
const { summary, monthly_trend, vendor_distribution, items, footer_summary } = api;
|
2026-01-22 23:16:56 +09:00
|
|
|
|
const changeRateText = summary.change_rate >= 0
|
|
|
|
|
|
? `+${summary.change_rate.toFixed(1)}%`
|
|
|
|
|
|
: `${summary.change_rate.toFixed(1)}%`;
|
|
|
|
|
|
|
2026-01-23 13:31:44 +09:00
|
|
|
|
// cardId별 설정 가져오기 (기본값: me4)
|
|
|
|
|
|
const config = EXPENSE_CARD_CONFIG[cardId] || EXPENSE_CARD_CONFIG.me4;
|
2026-01-22 23:16:56 +09:00
|
|
|
|
|
2026-01-23 13:31:44 +09:00
|
|
|
|
// 거래처 필터 옵션 생성
|
|
|
|
|
|
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,
|
2026-01-22 23:16:56 +09:00
|
|
|
|
summaryCards: [
|
2026-01-23 13:31:44 +09:00
|
|
|
|
{ label: config.summaryLabel, value: summary.total_amount, unit: '원' },
|
2026-01-22 23:16:56 +09:00
|
|
|
|
{ label: '전월 대비', value: changeRateText },
|
|
|
|
|
|
{ label: '지출 후 예상 잔액', value: summary.remaining_balance, unit: '원' },
|
|
|
|
|
|
],
|
|
|
|
|
|
table: {
|
2026-01-23 13:31:44 +09:00
|
|
|
|
title: config.tableTitle,
|
2026-01-22 23:16:56 +09:00
|
|
|
|
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',
|
2026-01-23 13:31:44 +09:00
|
|
|
|
options: vendorOptions,
|
2026-01-22 23:16:56 +09:00
|
|
|
|
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',
|
2026-01-23 13:31:44 +09:00
|
|
|
|
footerSummary: [
|
|
|
|
|
|
{ label: `총 ${footer_summary.item_count}건`, value: footer_summary.total_amount },
|
|
|
|
|
|
],
|
2026-01-22 23:16:56 +09:00
|
|
|
|
},
|
|
|
|
|
|
};
|
2026-01-23 13:31:44 +09:00
|
|
|
|
|
|
|
|
|
|
// 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;
|
2026-01-20 18:51:05 +09:00
|
|
|
|
}
|