- StatusBoard(현황판) API Hook 및 타입 추가 - TodayIssue(오늘의 이슈) API Hook 및 타입 추가 - Calendar(캘린더) API Hook 및 타입 추가 - Vat(부가세) API Hook 및 타입 추가 - Entertainment(접대비) API Hook 및 타입 추가 - Welfare(복리후생비) API Hook 및 타입 추가 - CEODashboard.tsx에 모든 Phase 2 Hook 통합 - API 응답 → Frontend 타입 변환 transformer 추가 - WelfareCalculationType 'percentage' → 'ratio' 타입 일치 수정
642 lines
17 KiB
TypeScript
642 lines
17 KiB
TypeScript
/**
|
|
* CEO Dashboard API 응답 → Frontend 타입 변환 함수
|
|
*
|
|
* 참조: docs/plans/AI_리포트_키워드_색상체계_가이드_v1.4.md
|
|
*/
|
|
|
|
import type {
|
|
DailyReportApiResponse,
|
|
ReceivablesApiResponse,
|
|
BadDebtApiResponse,
|
|
ExpectedExpenseApiResponse,
|
|
CardTransactionApiResponse,
|
|
StatusBoardApiResponse,
|
|
TodayIssueApiResponse,
|
|
CalendarApiResponse,
|
|
VatApiResponse,
|
|
EntertainmentApiResponse,
|
|
WelfareApiResponse,
|
|
} 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 변환
|
|
// ============================================
|
|
|
|
/**
|
|
* 일일 일보 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;
|
|
checkPoints.push({
|
|
id: 'dr-cash-asset',
|
|
type: 'info' as CheckPointType,
|
|
message: `총 현금성 자산이 ${formatAmount(cashAsset)}입니다.`,
|
|
highlights: [
|
|
{ text: formatAmount(cashAsset), color: 'blue' as const },
|
|
],
|
|
});
|
|
|
|
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 타입 변환
|
|
* 주의: 가지급금, 법인세 예상 가중 등은 별도 API 필요 (현재 목업 유지)
|
|
*/
|
|
export function transformCardManagementResponse(
|
|
api: CardTransactionApiResponse,
|
|
fallbackData?: CardManagementData
|
|
): CardManagementData {
|
|
const changeRate = calculateChangeRate(api.current_month_total, api.previous_month_total);
|
|
|
|
return {
|
|
// 가지급금 관련 경고는 API 데이터가 없으므로 fallback 사용
|
|
warningBanner: fallbackData?.warningBanner,
|
|
cards: [
|
|
{
|
|
id: 'cm1',
|
|
label: '카드',
|
|
amount: api.current_month_total,
|
|
previousLabel: `전월 대비 ${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}%`,
|
|
},
|
|
// 아래 항목들은 API에서 제공하지 않으므로 fallback 사용
|
|
fallbackData?.cards[1] ?? {
|
|
id: 'cm2',
|
|
label: '가지급금',
|
|
amount: 0,
|
|
},
|
|
fallbackData?.cards[2] ?? {
|
|
id: 'cm3',
|
|
label: '법인세 예상 가중',
|
|
amount: 0,
|
|
},
|
|
fallbackData?.cards[3] ?? {
|
|
id: 'cm4',
|
|
label: '대표자 종합세 예상 가중',
|
|
amount: 0,
|
|
},
|
|
],
|
|
checkPoints: generateCardManagementCheckPoints(api),
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// 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),
|
|
})),
|
|
})),
|
|
};
|
|
} |