feat: [CEO 대시보드] API 연동 + 섹션 확장 + SummaryNavBar
- 접대비/복리후생비/매출채권/캘린더 섹션 API 연동 - SummaryNavBar 추가 + mockData/modalConfigs 대규모 리팩토링 - Dashboard transformers 도메인별 분리 - 상세 모달 ScheduleDetailModal 추가
This commit is contained in:
@@ -102,12 +102,17 @@ export async function fetchCardTransactionDashboard(): Promise<ApiResponse<CardD
|
||||
* 가지급금 대시보드 데이터 조회
|
||||
* GET /api/v1/loans/dashboard
|
||||
*
|
||||
* @returns 가지급금 요약, 월별 추이, 사용자별 분포, 거래 목록
|
||||
* @param params - 날짜 필터 (선택)
|
||||
* @returns 가지급금 요약, 카테고리 집계, 거래 목록
|
||||
*/
|
||||
export async function fetchLoanDashboard(): Promise<ApiResponse<LoanDashboardApiResponse>> {
|
||||
export async function fetchLoanDashboard(params?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}): Promise<ApiResponse<LoanDashboardApiResponse>> {
|
||||
try {
|
||||
const response = await apiClient.get<ApiResponse<LoanDashboardApiResponse>>(
|
||||
'/loans/dashboard'
|
||||
'/loans/dashboard',
|
||||
params ? { params } : undefined
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
@@ -126,13 +131,10 @@ export async function fetchLoanDashboard(): Promise<ApiResponse<LoanDashboardApi
|
||||
data: {
|
||||
summary: {
|
||||
total_outstanding: 0,
|
||||
settled_amount: 0,
|
||||
recognized_interest: 0,
|
||||
pending_count: 0,
|
||||
outstanding_count: 0,
|
||||
},
|
||||
monthly_trend: [],
|
||||
user_distribution: [],
|
||||
items: [],
|
||||
loans: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -144,13 +146,10 @@ export async function fetchLoanDashboard(): Promise<ApiResponse<LoanDashboardApi
|
||||
data: {
|
||||
summary: {
|
||||
total_outstanding: 0,
|
||||
settled_amount: 0,
|
||||
recognized_interest: 0,
|
||||
pending_count: 0,
|
||||
outstanding_count: 0,
|
||||
},
|
||||
monthly_trend: [],
|
||||
user_distribution: [],
|
||||
items: [],
|
||||
loans: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,5 +10,8 @@ export { transformReceivableResponse, transformDebtCollectionResponse } from './
|
||||
export { transformMonthlyExpenseResponse, transformCardManagementResponse } from './transformers/expense';
|
||||
export { transformStatusBoardResponse, transformTodayIssueResponse } from './transformers/status-issue';
|
||||
export { transformCalendarResponse } from './transformers/calendar';
|
||||
export { transformVatResponse, transformEntertainmentResponse, transformWelfareResponse, transformWelfareDetailResponse } from './transformers/tax-benefits';
|
||||
export { transformPurchaseDetailResponse, transformCardDetailResponse, transformBillDetailResponse, transformExpectedExpenseDetailResponse } from './transformers/expense-detail';
|
||||
export { transformVatResponse, transformVatDetailResponse, transformEntertainmentResponse, transformEntertainmentDetailResponse, transformWelfareResponse, transformWelfareDetailResponse } from './transformers/tax-benefits';
|
||||
export { transformPurchaseDetailResponse, transformCardDetailResponse, transformBillDetailResponse, transformExpectedExpenseDetailResponse, transformPurchaseRecordsToModal, transformCardTransactionsToModal, transformBillRecordsToModal, transformAllExpensesToModal } from './transformers/expense-detail';
|
||||
export { transformSalesStatusResponse, transformPurchaseStatusResponse } from './transformers/sales-purchase';
|
||||
export { transformDailyProductionResponse, transformUnshippedResponse, transformConstructionResponse } from './transformers/production-logistics';
|
||||
export { transformDailyAttendanceResponse } from './transformers/hr';
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
CheckPoint,
|
||||
CheckPointType,
|
||||
} from '@/components/business/CEODashboard/types';
|
||||
import { formatAmount, formatDate, toChangeFields } from './common';
|
||||
import { formatAmount, formatDate } from './common';
|
||||
|
||||
/**
|
||||
* 운영자금 안정성에 따른 색상 반환
|
||||
@@ -137,51 +137,32 @@ function generateDailyReportCheckPoints(api: DailyReportApiResponse): CheckPoint
|
||||
* DailyReport API 응답 → Frontend 타입 변환
|
||||
*/
|
||||
export function transformDailyReportResponse(api: DailyReportApiResponse): DailyReportData {
|
||||
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 },
|
||||
};
|
||||
|
||||
return {
|
||||
date: formatDate(api.date, api.day_of_week),
|
||||
cards: [
|
||||
{
|
||||
id: 'dr1',
|
||||
label: '현금성 자산 합계',
|
||||
label: '일일일보',
|
||||
amount: api.cash_asset_total,
|
||||
...(change?.cash_asset_change_rate !== undefined
|
||||
? toChangeFields(change.cash_asset_change_rate)
|
||||
: FALLBACK_CHANGES.cash_asset),
|
||||
path: '/ko/accounting/daily-report',
|
||||
},
|
||||
{
|
||||
id: 'dr2',
|
||||
label: '외국환(USD) 합계',
|
||||
amount: api.foreign_currency_total,
|
||||
currency: 'USD',
|
||||
...(change?.foreign_currency_change_rate !== undefined
|
||||
? toChangeFields(change.foreign_currency_change_rate)
|
||||
: FALLBACK_CHANGES.foreign_currency),
|
||||
label: '미수금 잔액',
|
||||
amount: api.receivable_balance ?? 0,
|
||||
path: '/ko/accounting/receivables-status',
|
||||
},
|
||||
{
|
||||
id: 'dr3',
|
||||
label: '입금 합계',
|
||||
amount: api.krw_totals.income,
|
||||
...(change?.income_change_rate !== undefined
|
||||
? toChangeFields(change.income_change_rate)
|
||||
: FALLBACK_CHANGES.income),
|
||||
label: '미지급금 잔액',
|
||||
amount: api.payable_balance ?? 0,
|
||||
// 클릭 이동 없음
|
||||
},
|
||||
{
|
||||
id: 'dr4',
|
||||
label: '출금 합계',
|
||||
amount: api.krw_totals.expense,
|
||||
...(change?.expense_change_rate !== undefined
|
||||
? toChangeFields(change.expense_change_rate)
|
||||
: FALLBACK_CHANGES.expense),
|
||||
label: '당월 예상 지출 합계',
|
||||
amount: api.monthly_expense_total ?? 0,
|
||||
// 클릭 시 당월 예상 지출 상세 팝업 (UI에서 처리)
|
||||
},
|
||||
],
|
||||
checkPoints: generateDailyReportCheckPoints(api),
|
||||
|
||||
@@ -8,7 +8,15 @@ import type {
|
||||
BillDashboardDetailApiResponse,
|
||||
ExpectedExpenseDashboardDetailApiResponse,
|
||||
} from '../types';
|
||||
import type { DetailModalConfig } from '@/components/business/CEODashboard/types';
|
||||
import type { DateFilterConfig, DetailModalConfig } from '@/components/business/CEODashboard/types';
|
||||
import type { PurchaseRecord } from '@/components/accounting/PurchaseManagement/types';
|
||||
import type { CardTransaction } from '@/components/accounting/CardTransactionInquiry/types';
|
||||
import type { BillRecord } from '@/components/accounting/BillManagement/types';
|
||||
import { PURCHASE_TYPE_LABELS } from '@/components/accounting/PurchaseManagement/types';
|
||||
import { getBillStatusLabel } from '@/components/accounting/BillManagement/types';
|
||||
|
||||
// 차트 색상 팔레트
|
||||
const CHART_COLORS = ['#60A5FA', '#34D399', '#F59E0B', '#F87171', '#A78BFA', '#94A3B8'];
|
||||
|
||||
// ============================================
|
||||
// Purchase Dashboard Detail 변환 (me1)
|
||||
@@ -268,6 +276,7 @@ const EXPENSE_CARD_CONFIG: Record<string, {
|
||||
hasBarChart: boolean;
|
||||
hasPieChart: boolean;
|
||||
hasHorizontalBarChart: boolean;
|
||||
dateFilter?: DateFilterConfig;
|
||||
}> = {
|
||||
me1: {
|
||||
title: '당월 매입 상세',
|
||||
@@ -278,6 +287,7 @@ const EXPENSE_CARD_CONFIG: Record<string, {
|
||||
hasBarChart: true,
|
||||
hasPieChart: true,
|
||||
hasHorizontalBarChart: false,
|
||||
dateFilter: { enabled: true, defaultPreset: '당월', showSearch: true },
|
||||
},
|
||||
me2: {
|
||||
title: '당월 카드 상세',
|
||||
@@ -288,6 +298,7 @@ const EXPENSE_CARD_CONFIG: Record<string, {
|
||||
hasBarChart: true,
|
||||
hasPieChart: true,
|
||||
hasHorizontalBarChart: false,
|
||||
dateFilter: { enabled: true, defaultPreset: '당월', showSearch: true },
|
||||
},
|
||||
me3: {
|
||||
title: '당월 발행어음 상세',
|
||||
@@ -298,6 +309,7 @@ const EXPENSE_CARD_CONFIG: Record<string, {
|
||||
hasBarChart: true,
|
||||
hasPieChart: false,
|
||||
hasHorizontalBarChart: true,
|
||||
dateFilter: { enabled: true, presets: ['당해년도', '전전월', '전월', '당월', '어제'], defaultPreset: '당월', showSearch: true },
|
||||
},
|
||||
me4: {
|
||||
title: '당월 지출 예상 상세',
|
||||
@@ -339,6 +351,7 @@ export function transformExpectedExpenseDetailResponse(
|
||||
// 결과 객체 생성
|
||||
const result: DetailModalConfig = {
|
||||
title: config.title,
|
||||
...(config.dateFilter && { dateFilter: config.dateFilter }),
|
||||
summaryCards: [
|
||||
{ label: config.summaryLabel, value: summary.total_amount, unit: '원' },
|
||||
{ label: '전월 대비', value: changeRateText },
|
||||
@@ -420,3 +433,294 @@ export function transformExpectedExpenseDetailResponse(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 기존 페이지 서버 액션 응답 → DetailModalConfig 변환
|
||||
// (dashboard-detail API 대신 실제 페이지 API 사용)
|
||||
// ============================================
|
||||
|
||||
/** 거래처별 그룹핑 → 상위 5개 + 기타 */
|
||||
function groupByVendor(
|
||||
records: { vendorName: string; amount: number }[],
|
||||
limit: number = 5,
|
||||
): { name: string; value: number; percentage: number; color: string }[] {
|
||||
const vendorMap = new Map<string, number>();
|
||||
let total = 0;
|
||||
for (const r of records) {
|
||||
const name = r.vendorName || '미지정';
|
||||
vendorMap.set(name, (vendorMap.get(name) || 0) + r.amount);
|
||||
total += r.amount;
|
||||
}
|
||||
const sorted = [...vendorMap.entries()].sort((a, b) => b[1] - a[1]);
|
||||
const top = sorted.slice(0, limit);
|
||||
const otherTotal = sorted.slice(limit).reduce((sum, [, v]) => sum + v, 0);
|
||||
if (otherTotal > 0) top.push(['기타', otherTotal]);
|
||||
|
||||
return top.map(([name, value], idx) => ({
|
||||
name,
|
||||
value,
|
||||
percentage: total > 0 ? Math.round((value / total) * 1000) / 10 : 0,
|
||||
color: CHART_COLORS[idx % CHART_COLORS.length],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* PurchaseRecord[] → DetailModalConfig (me1 매입)
|
||||
*/
|
||||
export function transformPurchaseRecordsToModal(
|
||||
records: PurchaseRecord[],
|
||||
dateFilter?: DateFilterConfig,
|
||||
): DetailModalConfig {
|
||||
const totalAmount = records.reduce((sum, r) => sum + r.totalAmount, 0);
|
||||
const totalSupply = records.reduce((sum, r) => sum + r.supplyAmount, 0);
|
||||
const totalVat = records.reduce((sum, r) => sum + r.vat, 0);
|
||||
|
||||
const vendorData = groupByVendor(
|
||||
records.map(r => ({ vendorName: r.vendorName, amount: r.totalAmount })),
|
||||
);
|
||||
|
||||
return {
|
||||
title: '당월 매입 상세',
|
||||
dateFilter: dateFilter ?? { enabled: true, defaultPreset: '당월', showSearch: true },
|
||||
summaryCards: [
|
||||
{ label: '총 매입액', value: totalAmount, unit: '원' },
|
||||
{ label: '공급가액', value: totalSupply, unit: '원' },
|
||||
{ label: '부가세', value: totalVat, unit: '원' },
|
||||
{ label: '건수', value: records.length, unit: '건' },
|
||||
],
|
||||
pieChart: vendorData.length > 0 ? {
|
||||
title: '거래처별 매입 비율',
|
||||
data: vendorData,
|
||||
} : undefined,
|
||||
table: {
|
||||
title: '매입 내역',
|
||||
columns: [
|
||||
{ key: 'no', label: 'No.', align: 'center' },
|
||||
{ key: 'purchaseDate', label: '매입일자', align: 'center', format: 'date' },
|
||||
{ key: 'vendorName', label: '거래처명', align: 'left' },
|
||||
{ key: 'supplyAmount', label: '공급가액', align: 'right', format: 'currency' },
|
||||
{ key: 'vat', label: '부가세', align: 'right', format: 'currency' },
|
||||
{ key: 'totalAmount', label: '합계', align: 'right', format: 'currency' },
|
||||
{ key: 'purchaseType', label: '유형', align: 'center' },
|
||||
],
|
||||
data: records.map((r, idx) => ({
|
||||
no: idx + 1,
|
||||
purchaseDate: r.purchaseDate,
|
||||
vendorName: r.vendorName,
|
||||
supplyAmount: r.supplyAmount,
|
||||
vat: r.vat,
|
||||
totalAmount: r.totalAmount,
|
||||
purchaseType: PURCHASE_TYPE_LABELS[r.purchaseType] || r.purchaseType,
|
||||
})),
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: totalAmount,
|
||||
totalColumnKey: 'totalAmount',
|
||||
footerSummary: [
|
||||
{ label: `총 ${records.length}건`, value: totalAmount, format: 'currency' },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* CardTransaction[] → DetailModalConfig (me2 카드)
|
||||
*/
|
||||
export function transformCardTransactionsToModal(
|
||||
records: CardTransaction[],
|
||||
dateFilter?: DateFilterConfig,
|
||||
): DetailModalConfig {
|
||||
const totalAmount = records.reduce((sum, r) => sum + r.totalAmount, 0);
|
||||
|
||||
const vendorData = groupByVendor(
|
||||
records.map(r => ({ vendorName: r.merchantName || r.vendorName, amount: r.totalAmount })),
|
||||
);
|
||||
|
||||
return {
|
||||
title: '당월 카드 상세',
|
||||
dateFilter: dateFilter ?? { enabled: true, defaultPreset: '당월', showSearch: true },
|
||||
summaryCards: [
|
||||
{ label: '총 사용액', value: totalAmount, unit: '원' },
|
||||
{ label: '건수', value: records.length, unit: '건' },
|
||||
],
|
||||
pieChart: vendorData.length > 0 ? {
|
||||
title: '가맹점별 카드 사용 비율',
|
||||
data: vendorData,
|
||||
} : undefined,
|
||||
table: {
|
||||
title: '카드 사용 내역',
|
||||
columns: [
|
||||
{ key: 'no', label: 'No.', align: 'center' },
|
||||
{ key: 'usedAt', label: '사용일자', align: 'center', format: 'date' },
|
||||
{ key: 'cardName', label: '카드명', align: 'left' },
|
||||
{ key: 'user', label: '사용자', align: 'center' },
|
||||
{ key: 'merchantName', label: '가맹점명', align: 'left' },
|
||||
{ key: 'totalAmount', label: '사용금액', align: 'right', format: 'currency' },
|
||||
],
|
||||
data: records.map((r, idx) => ({
|
||||
no: idx + 1,
|
||||
usedAt: r.usedAt,
|
||||
cardName: r.cardName,
|
||||
user: r.user,
|
||||
merchantName: r.merchantName,
|
||||
totalAmount: r.totalAmount,
|
||||
})),
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: totalAmount,
|
||||
totalColumnKey: 'totalAmount',
|
||||
footerSummary: [
|
||||
{ label: `총 ${records.length}건`, value: totalAmount, format: 'currency' },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* BillRecord[] → DetailModalConfig (me3 발행어음)
|
||||
*/
|
||||
export function transformBillRecordsToModal(
|
||||
records: BillRecord[],
|
||||
dateFilter?: DateFilterConfig,
|
||||
): DetailModalConfig {
|
||||
const totalAmount = records.reduce((sum, r) => sum + r.amount, 0);
|
||||
|
||||
const vendorBarData = groupByVendor(
|
||||
records.map(r => ({ vendorName: r.vendorName, amount: r.amount })),
|
||||
);
|
||||
|
||||
return {
|
||||
title: '당월 발행어음 상세',
|
||||
dateFilter: dateFilter ?? {
|
||||
enabled: true,
|
||||
presets: ['당해년도', '전전월', '전월', '당월', '어제'],
|
||||
defaultPreset: '당월',
|
||||
showSearch: true,
|
||||
},
|
||||
summaryCards: [
|
||||
{ label: '총 발행어음', value: totalAmount, unit: '원' },
|
||||
{ label: '건수', value: records.length, unit: '건' },
|
||||
],
|
||||
horizontalBarChart: vendorBarData.length > 0 ? {
|
||||
title: '거래처별 발행어음',
|
||||
data: vendorBarData.map(d => ({ name: d.name, value: d.value })),
|
||||
dataKey: 'value',
|
||||
yAxisKey: 'name',
|
||||
color: '#8B5CF6',
|
||||
} : undefined,
|
||||
table: {
|
||||
title: '발행어음 내역',
|
||||
columns: [
|
||||
{ key: 'no', label: 'No.', align: 'center' },
|
||||
{ key: 'billNumber', label: '어음번호', align: 'center' },
|
||||
{ key: 'vendorName', label: '거래처명', align: 'left' },
|
||||
{ key: 'issueDate', label: '발행일', align: 'center', format: 'date' },
|
||||
{ key: 'maturityDate', label: '만기일', align: 'center', format: 'date' },
|
||||
{ key: 'amount', label: '금액', align: 'right', format: 'currency' },
|
||||
{ key: 'status', label: '상태', align: 'center' },
|
||||
],
|
||||
data: records.map((r, idx) => ({
|
||||
no: idx + 1,
|
||||
billNumber: r.billNumber,
|
||||
vendorName: r.vendorName,
|
||||
issueDate: r.issueDate,
|
||||
maturityDate: r.maturityDate,
|
||||
amount: r.amount,
|
||||
status: getBillStatusLabel(r.billType, r.status),
|
||||
})),
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: totalAmount,
|
||||
totalColumnKey: 'amount',
|
||||
footerSummary: [
|
||||
{ label: `총 ${records.length}건`, value: totalAmount, format: 'currency' },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 3개 합산 → DetailModalConfig (me4 총 예상 지출)
|
||||
*/
|
||||
export function transformAllExpensesToModal(
|
||||
purchases: PurchaseRecord[],
|
||||
cards: CardTransaction[],
|
||||
bills: BillRecord[],
|
||||
): DetailModalConfig {
|
||||
const purchaseTotal = purchases.reduce((sum, r) => sum + r.totalAmount, 0);
|
||||
const cardTotal = cards.reduce((sum, r) => sum + r.totalAmount, 0);
|
||||
const billTotal = bills.reduce((sum, r) => sum + r.amount, 0);
|
||||
const grandTotal = purchaseTotal + cardTotal + billTotal;
|
||||
const totalCount = purchases.length + cards.length + bills.length;
|
||||
|
||||
// 3개 소스를 하나의 테이블로 합침
|
||||
type UnifiedRow = { date: string; type: string; vendorName: string; amount: number };
|
||||
const allRows: UnifiedRow[] = [
|
||||
...purchases.map(r => ({
|
||||
date: r.purchaseDate,
|
||||
type: '매입',
|
||||
vendorName: r.vendorName,
|
||||
amount: r.totalAmount,
|
||||
})),
|
||||
...cards.map(r => ({
|
||||
date: r.usedAt?.split(' ')[0] || r.usedAt, // 'YYYY-MM-DD HH:mm' → 'YYYY-MM-DD'
|
||||
type: '카드',
|
||||
vendorName: r.merchantName || r.vendorName,
|
||||
amount: r.totalAmount,
|
||||
})),
|
||||
...bills.map(r => ({
|
||||
date: r.issueDate,
|
||||
type: '어음',
|
||||
vendorName: r.vendorName,
|
||||
amount: r.amount,
|
||||
})),
|
||||
];
|
||||
// 날짜순 정렬
|
||||
allRows.sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
return {
|
||||
title: '당월 지출 예상 상세',
|
||||
summaryCards: [
|
||||
{ label: '총 지출 예상액', value: grandTotal, unit: '원' },
|
||||
{ label: '매입', value: purchaseTotal, unit: '원' },
|
||||
{ label: '카드', value: cardTotal, unit: '원' },
|
||||
{ label: '어음', value: billTotal, unit: '원' },
|
||||
],
|
||||
table: {
|
||||
title: '당월 지출 승인 내역서',
|
||||
columns: [
|
||||
{ key: 'no', label: 'No.', align: 'center' },
|
||||
{ key: 'date', label: '일자', align: 'center', format: 'date' },
|
||||
{ key: 'type', label: '유형', align: 'center' },
|
||||
{ key: 'vendorName', label: '거래처명', align: 'left' },
|
||||
{ key: 'amount', label: '금액', align: 'right', format: 'currency' },
|
||||
],
|
||||
data: allRows.map((r, idx) => ({
|
||||
no: idx + 1,
|
||||
date: r.date,
|
||||
type: r.type,
|
||||
vendorName: r.vendorName,
|
||||
amount: r.amount,
|
||||
})),
|
||||
filters: [
|
||||
{
|
||||
key: 'type',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '매입', label: '매입' },
|
||||
{ value: '카드', label: '카드' },
|
||||
{ value: '어음', label: '어음' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
],
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: grandTotal,
|
||||
totalColumnKey: 'amount',
|
||||
footerSummary: [
|
||||
{ label: `총 ${totalCount}건`, value: grandTotal, format: 'currency' },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
CheckPoint,
|
||||
CheckPointType,
|
||||
} from '@/components/business/CEODashboard/types';
|
||||
import { formatAmount, calculateChangeRate } from './common';
|
||||
import { formatAmount } from './common';
|
||||
|
||||
// ============================================
|
||||
// 월 예상 지출 (MonthlyExpense)
|
||||
@@ -78,49 +78,84 @@ export function transformMonthlyExpenseResponse(api: ExpectedExpenseApiResponse)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 카드/가지급금 (CardManagement)
|
||||
// 카드/가지급금 (CardManagement) — D1.7 5장 카드 구조
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 카드/가지급금 CheckPoints 생성
|
||||
* 카드/가지급금 CheckPoints 생성 (D1.7)
|
||||
*
|
||||
* CP1: 법인카드 → 가지급금 전환 경고
|
||||
* CP2: 인정이자 발생 현황
|
||||
* CP3: 접대비 불인정 항목 감지
|
||||
* CP4: 주말 카드 사용 감지 (향후 확장)
|
||||
*/
|
||||
function generateCardManagementCheckPoints(api: CardTransactionApiResponse): CheckPoint[] {
|
||||
function generateCardManagementCheckPoints(
|
||||
loanApi?: LoanDashboardApiResponse | null,
|
||||
taxApi?: TaxSimulationApiResponse | null,
|
||||
cardApi?: CardTransactionApiResponse | null,
|
||||
): 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';
|
||||
const totalOutstanding = loanApi?.summary?.total_outstanding ?? 0;
|
||||
const interestRate = taxApi?.loan_summary?.interest_rate ?? 4.6;
|
||||
const recognizedInterest = taxApi?.loan_summary?.recognized_interest ?? 0;
|
||||
|
||||
// CP1: 법인카드 사용 중 가지급금 전환 경고
|
||||
if (totalOutstanding > 0) {
|
||||
checkPoints.push({
|
||||
id: 'cm-change',
|
||||
type,
|
||||
message: `당월 카드 사용액이 전월 대비 ${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}% 변동했습니다.`,
|
||||
id: 'cm-cp1',
|
||||
type: 'success' as CheckPointType,
|
||||
message: `법인카드 사용 중 ${formatAmount(totalOutstanding)}이 가지급금으로 전환되었습니다. 연 ${interestRate}% 인정이자가 발생합니다.`,
|
||||
highlights: [
|
||||
{ text: `${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}%`, color: changeRate > 0 ? 'red' as const : 'green' as const },
|
||||
{ text: formatAmount(totalOutstanding), color: 'red' as const },
|
||||
{ text: '가지급금', color: 'red' as const },
|
||||
{ text: `연 ${interestRate}% 인정이자`, color: 'red' 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 },
|
||||
],
|
||||
});
|
||||
// CP2: 인정이자 발생 현황
|
||||
if (totalOutstanding > 0 && recognizedInterest > 0) {
|
||||
checkPoints.push({
|
||||
id: 'cm-cp2',
|
||||
type: 'success' as CheckPointType,
|
||||
message: `현재 가지급금 ${formatAmount(totalOutstanding)} × ${interestRate}% = 연간 약 ${formatAmount(recognizedInterest)}의 인정이자가 발생 중입니다.`,
|
||||
highlights: [
|
||||
{ text: `연간 약 ${formatAmount(recognizedInterest)}의 인정이자`, color: 'red' as const },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// CP3: 접대비 불인정 항목 감지
|
||||
const entertainmentCount = loanApi?.category_breakdown?.entertainment?.unverified_count ?? 0;
|
||||
if (entertainmentCount > 0) {
|
||||
checkPoints.push({
|
||||
id: 'cm-cp3',
|
||||
type: 'success' as CheckPointType,
|
||||
message: '상품권/귀금속 등 접대비 불인정 항목 결제 감지. 가지급금 처리 예정입니다.',
|
||||
highlights: [
|
||||
{ text: '불인정 항목 결제 감지', color: 'red' as const },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// CP4: 주말 카드 사용 감지 (향후 card-transactions API 확장 시)
|
||||
// 현재는 cardApi 데이터에서 주말 사용 정보가 없으므로 placeholder
|
||||
if (cardApi && cardApi.current_month_total > 0) {
|
||||
// 향후 weekend_amount 필드 추가 시 활성화
|
||||
}
|
||||
|
||||
return checkPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* CardTransaction API 응답 → Frontend 타입 변환
|
||||
* 4개 카드 구조:
|
||||
* - cm1: 카드 사용액 (CardTransaction API)
|
||||
* - cm2: 가지급금 (LoanDashboard API)
|
||||
* - cm3: 법인세 예상 가중 (TaxSimulation API - corporate_tax.difference)
|
||||
* - cm4: 대표자 종합세 예상 가중 (TaxSimulation API - income_tax.difference)
|
||||
* D1.7 5장 카드 구조:
|
||||
* - cm1: 카드 (loans category_breakdown.card)
|
||||
* - cm2: 경조사 (loans category_breakdown.congratulatory)
|
||||
* - cm3: 상품권 (loans category_breakdown.gift_certificate)
|
||||
* - cm4: 접대비 (loans category_breakdown.entertainment)
|
||||
* - cm_total: 총 가지급금 합계 (loans summary.total_outstanding)
|
||||
*/
|
||||
export function transformCardManagementResponse(
|
||||
summaryApi: CardTransactionApiResponse,
|
||||
@@ -128,50 +163,77 @@ export function transformCardManagementResponse(
|
||||
taxApi?: TaxSimulationApiResponse | null,
|
||||
fallbackData?: CardManagementData
|
||||
): CardManagementData {
|
||||
const changeRate = calculateChangeRate(summaryApi.current_month_total, summaryApi.previous_month_total);
|
||||
const breakdown = loanApi?.category_breakdown;
|
||||
const totalOutstanding = loanApi?.summary?.total_outstanding ?? 0;
|
||||
|
||||
// cm2: 가지급금 금액 (LoanDashboard API 또는 fallback)
|
||||
const loanAmount = loanApi?.summary?.total_outstanding ?? fallbackData?.cards[1]?.amount ?? 0;
|
||||
// 카테고리별 금액 추출
|
||||
const cardAmount = breakdown?.card?.outstanding_amount ?? fallbackData?.cards[0]?.amount ?? 0;
|
||||
const congratulatoryAmount = breakdown?.congratulatory?.outstanding_amount ?? fallbackData?.cards[1]?.amount ?? 0;
|
||||
const giftCertificateAmount = breakdown?.gift_certificate?.outstanding_amount ?? fallbackData?.cards[2]?.amount ?? 0;
|
||||
const entertainmentAmount = breakdown?.entertainment?.outstanding_amount ?? fallbackData?.cards[3]?.amount ?? 0;
|
||||
|
||||
// cm3: 법인세 예상 가중 (TaxSimulation API 또는 fallback)
|
||||
const corporateTaxDifference = taxApi?.corporate_tax?.difference ?? fallbackData?.cards[2]?.amount ?? 0;
|
||||
// 카테고리별 미증빙/미정리 건수
|
||||
const cardUnverified = breakdown?.card?.unverified_count ?? 0;
|
||||
const congratulatoryUnverified = breakdown?.congratulatory?.unverified_count ?? 0;
|
||||
const giftCertificateUnverified = breakdown?.gift_certificate?.unverified_count ?? 0;
|
||||
const entertainmentUnverified = breakdown?.entertainment?.unverified_count ?? 0;
|
||||
|
||||
// cm4: 대표자 종합세 예상 가중 (TaxSimulation API 또는 fallback)
|
||||
const incomeTaxDifference = taxApi?.income_tax?.difference ?? fallbackData?.cards[3]?.amount ?? 0;
|
||||
// 총 합계 (API summary 또는 카테고리 합산)
|
||||
const totalAmount = totalOutstanding > 0
|
||||
? totalOutstanding
|
||||
: cardAmount + congratulatoryAmount + giftCertificateAmount + entertainmentAmount;
|
||||
|
||||
// 가지급금 경고 배너 표시 여부 결정 (가지급금 잔액 > 0이면 표시)
|
||||
const hasLoanWarning = loanAmount > 0;
|
||||
// 가지급금 경고 배너 표시 여부 (가지급금 잔액 > 0이면 표시)
|
||||
const hasLoanWarning = totalAmount > 0;
|
||||
|
||||
return {
|
||||
// 가지급금 관련 경고 배너 (가지급금 있을 때만 표시)
|
||||
warningBanner: hasLoanWarning ? fallbackData?.warningBanner : undefined,
|
||||
warningBanner: hasLoanWarning
|
||||
? (fallbackData?.warningBanner ?? '가지급금 인정이자 4.6%, 법인세 및 연말정산 시 대표자 종합세 가중 주의')
|
||||
: undefined,
|
||||
cards: [
|
||||
// cm1: 카드 사용액 (CardTransaction API)
|
||||
// cm1: 카드
|
||||
{
|
||||
id: 'cm1',
|
||||
label: '카드',
|
||||
amount: summaryApi.current_month_total,
|
||||
previousLabel: `전월 대비 ${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}%`,
|
||||
amount: cardAmount,
|
||||
subLabel: cardUnverified > 0 ? `미정리 ${cardUnverified}건` : undefined,
|
||||
subAmount: cardUnverified > 0 ? cardAmount : undefined,
|
||||
isHighlighted: cardUnverified > 0,
|
||||
},
|
||||
// cm2: 가지급금 (LoanDashboard API)
|
||||
// cm2: 경조사
|
||||
{
|
||||
id: 'cm2',
|
||||
label: '가지급금',
|
||||
amount: loanAmount,
|
||||
label: '경조사',
|
||||
amount: congratulatoryAmount,
|
||||
subLabel: congratulatoryUnverified > 0 ? `미증빙 ${congratulatoryUnverified}건` : undefined,
|
||||
subAmount: congratulatoryUnverified > 0 ? congratulatoryAmount : undefined,
|
||||
isHighlighted: congratulatoryUnverified > 0,
|
||||
},
|
||||
// cm3: 법인세 예상 가중 (TaxSimulation API)
|
||||
// cm3: 상품권
|
||||
{
|
||||
id: 'cm3',
|
||||
label: '법인세 예상 가중',
|
||||
amount: corporateTaxDifference,
|
||||
label: '상품권',
|
||||
amount: giftCertificateAmount,
|
||||
subLabel: giftCertificateUnverified > 0 ? `미증빙 ${giftCertificateUnverified}건` : undefined,
|
||||
subAmount: giftCertificateUnverified > 0 ? giftCertificateAmount : undefined,
|
||||
isHighlighted: giftCertificateUnverified > 0,
|
||||
},
|
||||
// cm4: 대표자 종합세 예상 가중 (TaxSimulation API)
|
||||
// cm4: 접대비
|
||||
{
|
||||
id: 'cm4',
|
||||
label: '대표자 종합세 예상 가중',
|
||||
amount: incomeTaxDifference,
|
||||
label: '접대비',
|
||||
amount: entertainmentAmount,
|
||||
subLabel: entertainmentUnverified > 0 ? `미증빙 ${entertainmentUnverified}건` : undefined,
|
||||
subAmount: entertainmentUnverified > 0 ? entertainmentAmount : undefined,
|
||||
isHighlighted: entertainmentUnverified > 0,
|
||||
},
|
||||
// cm_total: 총 가지급금 합계
|
||||
{
|
||||
id: 'cm_total',
|
||||
label: '총 가지급금 합계',
|
||||
amount: totalAmount,
|
||||
},
|
||||
],
|
||||
checkPoints: generateCardManagementCheckPoints(summaryApi),
|
||||
checkPoints: generateCardManagementCheckPoints(loanApi, taxApi, summaryApi),
|
||||
};
|
||||
}
|
||||
|
||||
32
src/lib/api/dashboard/transformers/hr.ts
Normal file
32
src/lib/api/dashboard/transformers/hr.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 근태 현황 (HR/Attendance) 변환
|
||||
*/
|
||||
|
||||
import type { DailyAttendanceApiResponse } from '../types';
|
||||
import type { DailyAttendanceData } from '@/components/business/CEODashboard/types';
|
||||
|
||||
const ATTENDANCE_STATUS_MAP: Record<string, '출근' | '휴가' | '지각' | '결근'> = {
|
||||
present: '출근',
|
||||
on_leave: '휴가',
|
||||
late: '지각',
|
||||
absent: '결근',
|
||||
};
|
||||
|
||||
/**
|
||||
* DailyAttendance API 응답 → Frontend DailyAttendanceData 변환
|
||||
*/
|
||||
export function transformDailyAttendanceResponse(api: DailyAttendanceApiResponse): DailyAttendanceData {
|
||||
return {
|
||||
present: api.present,
|
||||
onLeave: api.on_leave,
|
||||
late: api.late,
|
||||
absent: api.absent,
|
||||
employees: api.employees.map((emp) => ({
|
||||
id: emp.id,
|
||||
department: emp.department,
|
||||
position: emp.position,
|
||||
name: emp.name,
|
||||
status: ATTENDANCE_STATUS_MAP[emp.status] ?? '출근',
|
||||
})),
|
||||
};
|
||||
}
|
||||
105
src/lib/api/dashboard/transformers/production-logistics.ts
Normal file
105
src/lib/api/dashboard/transformers/production-logistics.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 생산/물류 현황 (Production/Logistics) 변환
|
||||
*/
|
||||
|
||||
import type {
|
||||
DailyProductionApiResponse,
|
||||
UnshippedApiResponse,
|
||||
ConstructionApiResponse,
|
||||
} from '../types';
|
||||
import type {
|
||||
DailyProductionData,
|
||||
UnshippedData,
|
||||
ConstructionData,
|
||||
} from '@/components/business/CEODashboard/types';
|
||||
|
||||
const WORK_STATUS_MAP: Record<string, '진행중' | '대기' | '완료'> = {
|
||||
in_progress: '진행중',
|
||||
pending: '대기',
|
||||
completed: '완료',
|
||||
};
|
||||
|
||||
const CONSTRUCTION_STATUS_MAP: Record<string, '진행중' | '예정' | '완료'> = {
|
||||
in_progress: '진행중',
|
||||
scheduled: '예정',
|
||||
completed: '완료',
|
||||
};
|
||||
|
||||
/**
|
||||
* DailyProduction API 응답 → Frontend DailyProductionData 변환
|
||||
*/
|
||||
export function transformDailyProductionResponse(api: DailyProductionApiResponse): DailyProductionData {
|
||||
const dateObj = new Date(api.date);
|
||||
const dayNames = ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'];
|
||||
const formattedDate = `${dateObj.getFullYear()}년 ${dateObj.getMonth() + 1}월 ${dateObj.getDate()}일 ${api.day_of_week || dayNames[dateObj.getDay()]}`;
|
||||
|
||||
return {
|
||||
date: formattedDate,
|
||||
processes: api.processes.map((proc) => ({
|
||||
processName: proc.process_name,
|
||||
totalWork: proc.total_work,
|
||||
todo: proc.todo,
|
||||
inProgress: proc.in_progress,
|
||||
completed: proc.completed,
|
||||
urgent: proc.urgent,
|
||||
subLine: proc.sub_line,
|
||||
regular: proc.regular,
|
||||
workerCount: proc.worker_count,
|
||||
workItems: proc.work_items.map((item) => ({
|
||||
id: item.id,
|
||||
orderNo: item.order_no,
|
||||
client: item.client,
|
||||
product: item.product,
|
||||
quantity: item.quantity,
|
||||
status: WORK_STATUS_MAP[item.status] ?? '대기',
|
||||
})),
|
||||
workers: proc.workers.map((w) => ({
|
||||
name: w.name,
|
||||
assigned: w.assigned,
|
||||
completed: w.completed,
|
||||
rate: w.rate,
|
||||
})),
|
||||
})),
|
||||
shipment: {
|
||||
expectedAmount: api.shipment.expected_amount,
|
||||
expectedCount: api.shipment.expected_count,
|
||||
actualAmount: api.shipment.actual_amount,
|
||||
actualCount: api.shipment.actual_count,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unshipped API 응답 → Frontend UnshippedData 변환
|
||||
*/
|
||||
export function transformUnshippedResponse(api: UnshippedApiResponse): UnshippedData {
|
||||
return {
|
||||
items: api.items.map((item) => ({
|
||||
id: item.id,
|
||||
portNo: item.port_no,
|
||||
siteName: item.site_name,
|
||||
orderClient: item.order_client,
|
||||
dueDate: item.due_date,
|
||||
daysLeft: item.days_left,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Construction API 응답 → Frontend ConstructionData 변환
|
||||
*/
|
||||
export function transformConstructionResponse(api: ConstructionApiResponse): ConstructionData {
|
||||
return {
|
||||
thisMonth: api.this_month,
|
||||
completed: api.completed,
|
||||
items: api.items.map((item) => ({
|
||||
id: item.id,
|
||||
siteName: item.site_name,
|
||||
client: item.client,
|
||||
startDate: item.start_date,
|
||||
endDate: item.end_date,
|
||||
progress: item.progress,
|
||||
status: CONSTRUCTION_STATUS_MAP[item.status] ?? '진행중',
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -9,83 +9,48 @@ import type {
|
||||
CheckPoint,
|
||||
CheckPointType,
|
||||
} from '@/components/business/CEODashboard/types';
|
||||
import { formatAmount, normalizePath } from './common';
|
||||
import { formatAmount, normalizePath, validateHighlightColor } from './common';
|
||||
|
||||
// ============================================
|
||||
// 미수금 (Receivable)
|
||||
// 미수금 (Receivable) — D1.7 cards + check_points 구조
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 미수금 현황 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 타입 변환
|
||||
* 백엔드에서 cards[] + check_points[] 구조로 직접 전달
|
||||
*/
|
||||
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: '미수금 상세',
|
||||
cards: api.cards.map((card) => ({
|
||||
id: card.id,
|
||||
label: card.label,
|
||||
amount: card.amount,
|
||||
subLabel: card.subLabel,
|
||||
unit: card.unit,
|
||||
// sub_items → subItems 매핑
|
||||
subItems: card.sub_items?.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
})),
|
||||
// top_items → subItems 매핑 (Top 3 거래처)
|
||||
...(card.top_items && card.top_items.length > 0
|
||||
? {
|
||||
subItems: card.top_items.map((item, idx) => ({
|
||||
label: `${idx + 1}. ${item.name}`,
|
||||
value: item.amount,
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
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),
|
||||
})),
|
||||
})),
|
||||
detailButtonPath: normalizePath('/accounting/receivables-status'),
|
||||
};
|
||||
}
|
||||
|
||||
75
src/lib/api/dashboard/transformers/sales-purchase.ts
Normal file
75
src/lib/api/dashboard/transformers/sales-purchase.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 매출/매입 현황 (Sales/Purchase) 변환
|
||||
*/
|
||||
|
||||
import type { SalesStatusApiResponse, PurchaseStatusApiResponse } from '../types';
|
||||
import type { SalesStatusData, PurchaseStatusData } from '@/components/business/CEODashboard/types';
|
||||
|
||||
const STATUS_MAP_SALES: Record<string, '입금완료' | '미입금' | '부분입금'> = {
|
||||
deposited: '입금완료',
|
||||
unpaid: '미입금',
|
||||
partial: '부분입금',
|
||||
};
|
||||
|
||||
const STATUS_MAP_PURCHASE: Record<string, '결제완료' | '미결제' | '부분결제'> = {
|
||||
paid: '결제완료',
|
||||
unpaid: '미결제',
|
||||
partial: '부분결제',
|
||||
};
|
||||
|
||||
/**
|
||||
* Sales Summary API 응답 → Frontend SalesStatusData 변환
|
||||
*/
|
||||
export function transformSalesStatusResponse(api: SalesStatusApiResponse): SalesStatusData {
|
||||
return {
|
||||
cumulativeSales: api.cumulative_sales,
|
||||
achievementRate: api.achievement_rate,
|
||||
yoyChange: api.yoy_change,
|
||||
monthlySales: api.monthly_sales,
|
||||
monthlyTrend: api.monthly_trend.map((item) => ({
|
||||
month: item.label,
|
||||
amount: item.amount,
|
||||
})),
|
||||
clientSales: api.client_sales.map((item) => ({
|
||||
name: item.name,
|
||||
amount: item.amount,
|
||||
})),
|
||||
dailyItems: api.daily_items.map((item) => ({
|
||||
date: item.date,
|
||||
client: item.client,
|
||||
item: item.item,
|
||||
amount: item.amount,
|
||||
status: STATUS_MAP_SALES[item.status] ?? '미입금',
|
||||
})),
|
||||
dailyTotal: api.daily_total,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Purchase Summary API 응답 → Frontend PurchaseStatusData 변환
|
||||
*/
|
||||
export function transformPurchaseStatusResponse(api: PurchaseStatusApiResponse): PurchaseStatusData {
|
||||
return {
|
||||
cumulativePurchase: api.cumulative_purchase,
|
||||
unpaidAmount: api.unpaid_amount,
|
||||
yoyChange: api.yoy_change,
|
||||
monthlyTrend: api.monthly_trend.map((item) => ({
|
||||
month: item.label,
|
||||
amount: item.amount,
|
||||
})),
|
||||
materialRatio: api.material_ratio.map((item) => ({
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
percentage: item.percentage,
|
||||
color: item.color,
|
||||
})),
|
||||
dailyItems: api.daily_items.map((item) => ({
|
||||
date: item.date,
|
||||
supplier: item.supplier,
|
||||
item: item.item,
|
||||
amount: item.amount,
|
||||
status: STATUS_MAP_PURCHASE[item.status] ?? '미결제',
|
||||
})),
|
||||
dailyTotal: api.daily_total,
|
||||
};
|
||||
}
|
||||
@@ -24,7 +24,7 @@ const STATUS_BOARD_FALLBACK_SUB_LABELS: Record<string, string> = {
|
||||
tax_deadline: '',
|
||||
new_clients: '대한철강 외',
|
||||
leaves: '',
|
||||
purchases: '(유)한국정밀 외',
|
||||
// purchases: '(유)한국정밀 외', // [2026-03-03] 비활성화 — 백엔드 path 오류 + 데이터 정합성 이슈 (N4 참조)
|
||||
approvals: '구매 결재 외',
|
||||
};
|
||||
|
||||
@@ -52,19 +52,42 @@ function buildStatusSubLabel(item: { id: string; count: number | string; sub_lab
|
||||
return fallback.replace(/ 외$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 프론트 path 오버라이드: 백엔드 path가 잘못되거나 링크 불필요한 항목
|
||||
* - 값이 빈 문자열: 클릭 비활성화 (일정 표시 등 링크 불필요)
|
||||
* - 값이 경로 문자열: 백엔드 path 대신 사용
|
||||
*/
|
||||
const STATUS_BOARD_PATH_OVERRIDE: Record<string, string> = {
|
||||
tax_deadline: '/accounting/tax-invoices', // 백엔드 /accounting/tax → 실제 페이지
|
||||
// purchases: '/accounting/purchase', // [2026-03-03] 비활성화 — purchases 항목 자체를 숨김 (N4 참조)
|
||||
};
|
||||
|
||||
/**
|
||||
* [2026-03-03] 비활성화 항목: 백엔드 이슈 해결 전까지 현황판에서 숨김
|
||||
* - purchases: path 오류(건설경로 하드코딩) + 데이터 정합성 미확인 (API-SPEC N4 참조)
|
||||
*/
|
||||
const STATUS_BOARD_HIDDEN_ITEMS = new Set(['purchases']);
|
||||
|
||||
/**
|
||||
* 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,
|
||||
subLabel: buildStatusSubLabel(item),
|
||||
path: normalizePath(item.path, { addViewMode: true }),
|
||||
isHighlighted: item.isHighlighted,
|
||||
}));
|
||||
return api.items.filter((item) => !STATUS_BOARD_HIDDEN_ITEMS.has(item.id)).map((item) => {
|
||||
const overridePath = STATUS_BOARD_PATH_OVERRIDE[item.id];
|
||||
const path = overridePath !== undefined
|
||||
? overridePath
|
||||
: normalizePath(item.path, { addViewMode: true });
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
count: item.count,
|
||||
subLabel: buildStatusSubLabel(item),
|
||||
path,
|
||||
isHighlighted: item.isHighlighted,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
import type {
|
||||
VatApiResponse,
|
||||
VatDetailApiResponse,
|
||||
EntertainmentApiResponse,
|
||||
EntertainmentDetailApiResponse,
|
||||
WelfareApiResponse,
|
||||
WelfareDetailApiResponse,
|
||||
} from '../types';
|
||||
@@ -47,6 +49,90 @@ export function transformVatResponse(api: VatApiResponse): VatData {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 부가세 상세 (VatDetail)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* VatDetail API 응답 → DetailModalConfig 변환
|
||||
* 부가세 상세 모달 설정 생성
|
||||
*/
|
||||
export function transformVatDetailResponse(api: VatDetailApiResponse): DetailModalConfig {
|
||||
const { period_label, period_options, summary, reference_table, unissued_invoices } = api;
|
||||
|
||||
// 참조 테이블 행 구성: direction(invoice_type) 형식 + 납부세액 행
|
||||
const refRows = reference_table.map(row => ({
|
||||
category: `${row.direction_label}(${row.invoice_type_label})`,
|
||||
supplyAmount: formatNumber(row.supply_amount) + '원',
|
||||
taxAmount: formatNumber(row.tax_amount) + '원',
|
||||
}));
|
||||
|
||||
// 납부세액 행 추가
|
||||
const paymentLabel = summary.is_refund ? '환급세액' : '납부세액';
|
||||
refRows.push({
|
||||
category: paymentLabel,
|
||||
supplyAmount: '',
|
||||
taxAmount: formatNumber(summary.estimated_payment) + '원',
|
||||
});
|
||||
|
||||
return {
|
||||
title: '예상 납부세액',
|
||||
periodSelect: {
|
||||
enabled: true,
|
||||
options: period_options.map(opt => ({ value: opt.value, label: opt.label })),
|
||||
defaultValue: period_options[0]?.value,
|
||||
},
|
||||
summaryCards: [
|
||||
{ label: '매출 공급가액', value: summary.sales_supply_amount, unit: '원' },
|
||||
{ label: '매입 공급가액', value: summary.purchases_supply_amount, unit: '원' },
|
||||
{ label: summary.is_refund ? '예상 환급세액' : '예상 납부세액', value: summary.estimated_payment, unit: '원' },
|
||||
],
|
||||
referenceTable: {
|
||||
title: `${period_label} 부가세 요약`,
|
||||
columns: [
|
||||
{ key: 'category', label: '구분', align: 'left' },
|
||||
{ key: 'supplyAmount', label: '공급가액', align: 'right' },
|
||||
{ key: 'taxAmount', label: '세액', align: 'right' },
|
||||
],
|
||||
data: refRows,
|
||||
},
|
||||
table: {
|
||||
title: '세금계산서 미발행/미수취 내역',
|
||||
columns: [
|
||||
{ key: 'no', label: 'No.', align: 'center' },
|
||||
{ key: 'type', label: '구분', align: 'center' },
|
||||
{ key: 'issueDate', label: '발생일자', align: 'center', format: 'date' },
|
||||
{ key: 'vendor', label: '거래처', align: 'left' },
|
||||
{ key: 'vat', label: '부가세', align: 'right', format: 'currency' },
|
||||
{ key: 'invoiceStatus', label: '세금계산서 미발행/미수취', align: 'center' },
|
||||
],
|
||||
data: unissued_invoices.map((inv, idx) => ({
|
||||
no: idx + 1,
|
||||
type: inv.direction_label,
|
||||
issueDate: inv.issue_date,
|
||||
vendor: inv.vendor_name,
|
||||
vat: inv.tax_amount,
|
||||
invoiceStatus: inv.status,
|
||||
})),
|
||||
filters: [
|
||||
{
|
||||
key: 'type',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '매출', label: '매출' },
|
||||
{ value: '매입', label: '매입' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
],
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: unissued_invoices.reduce((sum, inv) => sum + inv.tax_amount, 0),
|
||||
totalColumnKey: 'vat',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 접대비 (Entertainment)
|
||||
// ============================================
|
||||
@@ -56,17 +142,8 @@ export function transformVatResponse(api: VatApiResponse): VatData {
|
||||
* 접대비 현황 데이터 변환
|
||||
*/
|
||||
export function transformEntertainmentResponse(api: EntertainmentApiResponse): EntertainmentData {
|
||||
// 사용금액(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);
|
||||
}
|
||||
|
||||
return {
|
||||
cards: reordered.map((card) => ({
|
||||
cards: api.cards.map((card) => ({
|
||||
id: card.id,
|
||||
label: card.label,
|
||||
amount: card.amount,
|
||||
@@ -94,17 +171,8 @@ export function transformEntertainmentResponse(api: EntertainmentApiResponse): E
|
||||
* 복리후생비 현황 데이터 변환
|
||||
*/
|
||||
export function transformWelfareResponse(api: WelfareApiResponse): WelfareData {
|
||||
// 사용금액(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);
|
||||
}
|
||||
|
||||
return {
|
||||
cards: reordered.map((card) => ({
|
||||
cards: api.cards.map((card) => ({
|
||||
id: card.id,
|
||||
label: card.label,
|
||||
amount: card.amount,
|
||||
@@ -131,6 +199,183 @@ export function transformWelfareResponse(api: WelfareApiResponse): WelfareData {
|
||||
* WelfareDetail API 응답 → DetailModalConfig 변환
|
||||
* 복리후생비 상세 모달 설정 생성
|
||||
*/
|
||||
// ============================================
|
||||
// 접대비 상세 (EntertainmentDetail)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* EntertainmentDetail API 응답 → DetailModalConfig 변환
|
||||
* 접대비 상세 모달 설정 생성
|
||||
*/
|
||||
export function transformEntertainmentDetailResponse(api: EntertainmentDetailApiResponse): DetailModalConfig {
|
||||
const { summary, risk_review, monthly_usage, user_distribution, transactions, calculation, quarterly } = api;
|
||||
|
||||
// 법인 유형 라벨
|
||||
const companyTypeLabel = calculation.company_type === 'large' ? '일반법인' : '중소기업';
|
||||
|
||||
return {
|
||||
title: '접대비 상세',
|
||||
dateFilter: {
|
||||
enabled: true,
|
||||
defaultPreset: '당월',
|
||||
showSearch: true,
|
||||
},
|
||||
summaryCards: [
|
||||
{ label: '당해년도 접대비 총 한도', value: summary.annual_limit, unit: '원' },
|
||||
{ label: '당해년도 접대비 잔여한도', value: summary.annual_remaining, unit: '원' },
|
||||
{ label: '당해년도 접대비 사용금액', value: summary.annual_used, unit: '원' },
|
||||
{ label: '당해년도 접대비 초과 금액', value: summary.annual_exceeded, unit: '원' },
|
||||
],
|
||||
reviewCards: {
|
||||
title: '접대비 검토 필요',
|
||||
cards: risk_review.map(r => ({
|
||||
label: r.label,
|
||||
amount: r.amount,
|
||||
subLabel: r.label === '기피업종'
|
||||
? (r.count > 0 ? `불인정 ${r.count}건` : '0건')
|
||||
: (r.count > 0 ? `미증빙 ${r.count}건` : '0건'),
|
||||
})),
|
||||
},
|
||||
barChart: {
|
||||
title: '월별 접대비 사용 추이',
|
||||
data: monthly_usage.map(item => ({
|
||||
name: item.label,
|
||||
value: item.amount,
|
||||
})),
|
||||
dataKey: 'value',
|
||||
xAxisKey: 'name',
|
||||
color: '#60A5FA',
|
||||
},
|
||||
pieChart: {
|
||||
title: '사용자별 접대비 사용 비율',
|
||||
data: user_distribution.map(item => ({
|
||||
name: item.user_name,
|
||||
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: 'useDate', label: '사용일시', align: 'center', format: 'date' },
|
||||
{ key: 'store', label: '가맹점명', align: 'left' },
|
||||
{ key: 'amount', label: '사용금액', align: 'right', format: 'currency' },
|
||||
{ key: 'riskType', label: '리스크', align: 'center' },
|
||||
],
|
||||
data: transactions.map((tx, idx) => ({
|
||||
no: idx + 1,
|
||||
cardName: tx.card_name,
|
||||
user: tx.user_name,
|
||||
useDate: tx.expense_date,
|
||||
store: tx.vendor_name,
|
||||
amount: tx.amount,
|
||||
riskType: tx.risk_type,
|
||||
})),
|
||||
filters: [
|
||||
{
|
||||
key: 'riskType',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '주말/심야', label: '주말/심야' },
|
||||
{ value: '기피업종', label: '기피업종' },
|
||||
{ value: '고액 결제', label: '고액 결제' },
|
||||
{ value: '증빙 미비', label: '증빙 미비' },
|
||||
{ value: '정상', label: '정상' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
],
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: transactions.reduce((sum, tx) => sum + tx.amount, 0),
|
||||
totalColumnKey: 'amount',
|
||||
},
|
||||
referenceTables: [
|
||||
{
|
||||
title: '접대비 손금한도 계산 - 기본한도',
|
||||
columns: [
|
||||
{ key: 'type', label: '법인 유형', align: 'left' },
|
||||
{ key: 'annualLimit', label: '연간 기본한도', align: 'right' },
|
||||
{ key: 'monthlyLimit', label: '월 환산', align: 'right' },
|
||||
],
|
||||
data: [
|
||||
{ type: '일반법인', annualLimit: '12,000,000원', monthlyLimit: '1,000,000원' },
|
||||
{ type: '중소기업', annualLimit: '36,000,000원', monthlyLimit: '3,000,000원' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '수입금액별 추가한도',
|
||||
columns: [
|
||||
{ key: 'range', label: '수입금액 구간', align: 'left' },
|
||||
{ key: 'formula', label: '추가한도 계산식', align: 'left' },
|
||||
],
|
||||
data: [
|
||||
{ range: '100억원 이하', formula: '수입금액 × 0.2%' },
|
||||
{ range: '100억 초과 ~ 500억 이하', formula: '2,000만원 + (수입금액 - 100억) × 0.1%' },
|
||||
{ range: '500억원 초과', formula: '6,000만원 + (수입금액 - 500억) × 0.03%' },
|
||||
],
|
||||
},
|
||||
],
|
||||
calculationCards: {
|
||||
title: '접대비 계산',
|
||||
cards: [
|
||||
{ label: `${companyTypeLabel} 연간 기본한도`, value: calculation.base_limit },
|
||||
{ label: '당해년도 수입금액별 추가한도', value: calculation.revenue_additional, operator: '+' as const },
|
||||
{ label: '당해년도 접대비 총 한도', value: calculation.annual_limit, operator: '=' as const },
|
||||
],
|
||||
},
|
||||
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) || '',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function transformWelfareDetailResponse(api: WelfareDetailApiResponse): DetailModalConfig {
|
||||
const { summary, monthly_usage, category_distribution, transactions, calculation, quarterly } = api;
|
||||
|
||||
|
||||
@@ -40,23 +40,59 @@ export interface DailyReportApiResponse {
|
||||
monthly_operating_expense: number; // 월 운영비 (직전 3개월 평균)
|
||||
operating_months: number | null; // 운영 가능 개월 수
|
||||
operating_stability: OperatingStability; // 안정성 상태
|
||||
// 어제 대비 변동률 (optional - 백엔드에서 제공 시)
|
||||
// 기획서 D1.7 자금현황 카드용 필드
|
||||
receivable_balance: number; // 미수금 잔액
|
||||
payable_balance: number; // 미지급금 잔액
|
||||
monthly_expense_total: number; // 당월 예상 지출 합계
|
||||
// 어제 대비 변동률 (optional - 백엔드에서 제공 시, 현재 주석 처리)
|
||||
daily_change?: DailyChangeRate;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 2. Receivables API 응답 타입
|
||||
// 2. Receivables API 응답 타입 (D1.7 cards + check_points 구조)
|
||||
// ============================================
|
||||
|
||||
/** 미수금 카드 서브 아이템 */
|
||||
export interface ReceivablesCardSubItem {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
/** 미수금 Top 거래처 아이템 */
|
||||
export interface ReceivablesTopItem {
|
||||
name: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
/** 미수금 금액 카드 아이템 */
|
||||
export interface ReceivablesAmountCardApiResponse {
|
||||
id: string;
|
||||
label: string;
|
||||
amount: number;
|
||||
subLabel?: string;
|
||||
unit?: string;
|
||||
sub_items?: ReceivablesCardSubItem[];
|
||||
top_items?: ReceivablesTopItem[];
|
||||
}
|
||||
|
||||
/** 미수금 체크포인트 하이라이트 아이템 */
|
||||
export interface ReceivablesHighlightItemApiResponse {
|
||||
text: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
/** 미수금 체크포인트 아이템 */
|
||||
export interface ReceivablesCheckPointApiResponse {
|
||||
id: string;
|
||||
type: string;
|
||||
message: string;
|
||||
highlights?: ReceivablesHighlightItemApiResponse[];
|
||||
}
|
||||
|
||||
/** GET /api/proxy/receivables/summary 응답 */
|
||||
export interface ReceivablesApiResponse {
|
||||
total_carry_forward: number; // 이월 미수금
|
||||
total_sales: number; // 당월 매출
|
||||
total_deposits: number; // 당월 입금
|
||||
total_bills: number; // 당월 어음
|
||||
total_receivables: number; // 미수금 잔액
|
||||
vendor_count: number; // 거래처 수
|
||||
overdue_vendor_count: number; // 연체 거래처 수
|
||||
cards: ReceivablesAmountCardApiResponse[];
|
||||
check_points: ReceivablesCheckPointApiResponse[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -276,6 +312,56 @@ export interface VatApiResponse {
|
||||
check_points: VatCheckPointApiResponse[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 9-1. Vat Detail (부가세 상세) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 부가세 상세 요약 */
|
||||
export interface VatDetailSummaryApiResponse {
|
||||
sales_supply_amount: number; // 매출 공급가액
|
||||
sales_tax_amount: number; // 매출 세액
|
||||
purchases_supply_amount: number; // 매입 공급가액
|
||||
purchases_tax_amount: number; // 매입 세액
|
||||
estimated_payment: number; // 예상 납부세액
|
||||
is_refund: boolean; // 환급 여부
|
||||
}
|
||||
|
||||
/** 부가세 요약 테이블 행 */
|
||||
export interface VatReferenceTableRowApiResponse {
|
||||
direction: string; // sales | purchases
|
||||
direction_label: string; // 매출 | 매입
|
||||
invoice_type: string; // tax_invoice | invoice | modified
|
||||
invoice_type_label: string; // 전자세금계산서 | 계산서 | 수정세금계산서
|
||||
supply_amount: number; // 공급가액
|
||||
tax_amount: number; // 세액
|
||||
}
|
||||
|
||||
/** 미발행/미수취 세금계산서 */
|
||||
export interface VatUnissuedInvoiceApiResponse {
|
||||
id: number;
|
||||
direction: string;
|
||||
direction_label: string; // 매출 | 매입
|
||||
issue_date: string; // 발생일자
|
||||
vendor_name: string; // 거래처명
|
||||
tax_amount: number; // 부가세
|
||||
status: string; // 미발행 | 미수취
|
||||
}
|
||||
|
||||
/** 신고기간 옵션 */
|
||||
export interface VatPeriodOptionApiResponse {
|
||||
value: string; // "2026-quarter-1"
|
||||
label: string; // "2026년 1기 예정신고"
|
||||
}
|
||||
|
||||
/** GET /api/proxy/vat/detail 응답 */
|
||||
export interface VatDetailApiResponse {
|
||||
period_label: string;
|
||||
period_options: VatPeriodOptionApiResponse[];
|
||||
summary: VatDetailSummaryApiResponse;
|
||||
reference_table: VatReferenceTableRowApiResponse[];
|
||||
unissued_invoices: VatUnissuedInvoiceApiResponse[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 10. Entertainment (접대비) API 응답 타입
|
||||
// ============================================
|
||||
@@ -342,6 +428,81 @@ export interface WelfareApiResponse {
|
||||
check_points: WelfareCheckPointApiResponse[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 11-1. Entertainment Detail (접대비 상세) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 접대비 상세 요약 */
|
||||
export interface EntertainmentDetailSummaryApiResponse {
|
||||
annual_limit: number; // 당해년도 접대비 총 한도
|
||||
annual_remaining: number; // 당해년도 접대비 잔여한도
|
||||
annual_used: number; // 당해년도 접대비 사용금액
|
||||
annual_exceeded: number; // 당해년도 접대비 초과 금액
|
||||
}
|
||||
|
||||
/** 접대비 리스크 검토 카드 */
|
||||
export interface EntertainmentRiskReviewApiResponse {
|
||||
label: string; // 주말/심야, 기피업종, 고액 결제, 증빙 미비
|
||||
amount: number; // 금액
|
||||
count: number; // 건수
|
||||
}
|
||||
|
||||
/** 접대비 월별 사용 추이 */
|
||||
export interface EntertainmentMonthlyUsageApiResponse {
|
||||
month: number; // 1~12
|
||||
label: string; // "1월"
|
||||
amount: number; // 사용 금액
|
||||
}
|
||||
|
||||
/** 접대비 사용자별 분포 */
|
||||
export interface EntertainmentUserDistributionApiResponse {
|
||||
user_name: string; // 사용자명
|
||||
amount: number; // 금액
|
||||
percentage: number; // 비율 (%)
|
||||
color: string; // 차트 색상
|
||||
}
|
||||
|
||||
/** 접대비 거래 내역 */
|
||||
export interface EntertainmentTransactionApiResponse {
|
||||
id: number;
|
||||
card_name: string; // 카드명
|
||||
user_name: string; // 사용자명
|
||||
expense_date: string; // 사용일자
|
||||
vendor_name: string; // 가맹점명
|
||||
amount: number; // 사용금액
|
||||
risk_type: string; // 리스크 유형 (주말/심야, 기피업종, 고액 결제, 증빙 미비, 정상)
|
||||
}
|
||||
|
||||
/** 접대비 손금한도 계산 정보 */
|
||||
export interface EntertainmentCalculationApiResponse {
|
||||
company_type: string; // 법인 유형 (large|medium|small)
|
||||
base_limit: number; // 기본한도
|
||||
revenue: number; // 수입금액
|
||||
revenue_additional: number; // 수입금액별 추가한도
|
||||
annual_limit: number; // 연간 총 한도
|
||||
}
|
||||
|
||||
/** 접대비 분기별 현황 */
|
||||
export interface EntertainmentQuarterlyStatusApiResponse {
|
||||
quarter: number; // 분기 (1-4)
|
||||
limit: number; // 한도금액
|
||||
carryover: number; // 이월금액
|
||||
used: number; // 사용금액
|
||||
remaining: number; // 잔여한도
|
||||
exceeded: number; // 초과금액
|
||||
}
|
||||
|
||||
/** GET /api/proxy/entertainment/detail 응답 */
|
||||
export interface EntertainmentDetailApiResponse {
|
||||
summary: EntertainmentDetailSummaryApiResponse;
|
||||
risk_review: EntertainmentRiskReviewApiResponse[];
|
||||
monthly_usage: EntertainmentMonthlyUsageApiResponse[];
|
||||
user_distribution: EntertainmentUserDistributionApiResponse[];
|
||||
transactions: EntertainmentTransactionApiResponse[];
|
||||
calculation: EntertainmentCalculationApiResponse;
|
||||
quarterly: EntertainmentQuarterlyStatusApiResponse[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 12. Welfare Detail (복리후생비 상세) API 응답 타입
|
||||
// ============================================
|
||||
@@ -613,9 +774,8 @@ export interface ExpectedExpenseDashboardDetailApiResponse {
|
||||
/** 가지급금 대시보드 요약 */
|
||||
export interface LoanDashboardSummaryApiResponse {
|
||||
total_outstanding: number; // 미정산 잔액
|
||||
settled_amount: number; // 정산 완료 금액
|
||||
recognized_interest: number; // 인정이자
|
||||
pending_count: number; // 미정산 건수
|
||||
outstanding_count: number; // 미정산 건수
|
||||
}
|
||||
|
||||
/** 가지급금 월별 추이 */
|
||||
@@ -640,17 +800,24 @@ export interface LoanItemApiResponse {
|
||||
user_name: string; // 사용자명
|
||||
loan_date: string; // 가지급일
|
||||
amount: number; // 금액
|
||||
description: string; // 설명
|
||||
content: string; // 내용 (백엔드 필드명)
|
||||
category: string; // 카테고리 라벨 (카드/경조사/상품권/접대비)
|
||||
status: string; // 상태 코드
|
||||
status_label: string; // 상태 라벨
|
||||
status_label?: string; // 상태 라벨 (optional - dashboard()에서 미반환 가능)
|
||||
}
|
||||
|
||||
/** 가지급금 카테고리별 집계 (D1.7) */
|
||||
export interface LoanCategoryBreakdown {
|
||||
outstanding_amount: number;
|
||||
total_count: number;
|
||||
unverified_count: number;
|
||||
}
|
||||
|
||||
/** GET /api/v1/loans/dashboard 응답 */
|
||||
export interface LoanDashboardApiResponse {
|
||||
summary: LoanDashboardSummaryApiResponse;
|
||||
monthly_trend: LoanMonthlyTrendApiResponse[];
|
||||
user_distribution: LoanUserDistributionApiResponse[];
|
||||
items: LoanItemApiResponse[];
|
||||
category_breakdown?: Record<string, LoanCategoryBreakdown>;
|
||||
loans: LoanItemApiResponse[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -706,4 +873,198 @@ export interface TaxSimulationApiResponse {
|
||||
loan_summary: TaxSimulationLoanSummaryApiResponse; // 가지급금 요약
|
||||
corporate_tax: CorporateTaxComparisonApiResponse; // 법인세 비교
|
||||
income_tax: IncomeTaxComparisonApiResponse; // 소득세 비교
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 19. SalesStatus (매출 현황) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 매출 월별 추이 */
|
||||
export interface SalesMonthlyTrendApiResponse {
|
||||
month: string; // "2026-08"
|
||||
label: string; // "8월"
|
||||
amount: number; // 금액
|
||||
}
|
||||
|
||||
/** 거래처별 매출 */
|
||||
export interface SalesClientApiResponse {
|
||||
name: string; // 거래처명
|
||||
amount: number; // 금액
|
||||
}
|
||||
|
||||
/** 일별 매출 아이템 */
|
||||
export interface SalesDailyItemApiResponse {
|
||||
date: string; // "2026-02-01"
|
||||
client: string; // 거래처명
|
||||
item: string; // 품목명
|
||||
amount: number; // 금액
|
||||
status: 'deposited' | 'unpaid' | 'partial'; // 입금상태
|
||||
}
|
||||
|
||||
/** GET /api/v1/dashboard/sales/summary 응답 */
|
||||
export interface SalesStatusApiResponse {
|
||||
cumulative_sales: number; // 누적 매출
|
||||
achievement_rate: number; // 달성률 (%)
|
||||
yoy_change: number; // 전년 동월 대비 변화율 (%)
|
||||
monthly_sales: number; // 당월 매출
|
||||
monthly_trend: SalesMonthlyTrendApiResponse[];
|
||||
client_sales: SalesClientApiResponse[];
|
||||
daily_items: SalesDailyItemApiResponse[];
|
||||
daily_total: number; // 일별 합계
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 20. PurchaseStatus (매입 현황) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 매입 월별 추이 */
|
||||
export interface PurchaseMonthlyTrendDashboardApiResponse {
|
||||
month: string; // "2026-08"
|
||||
label: string; // "8월"
|
||||
amount: number; // 금액
|
||||
}
|
||||
|
||||
/** 자재 구성 비율 */
|
||||
export interface PurchaseMaterialRatioApiResponse {
|
||||
name: string; // "원자재", "부자재", "소모품"
|
||||
value: number; // 금액
|
||||
percentage: number; // 비율 (%)
|
||||
color: string; // 차트 색상
|
||||
}
|
||||
|
||||
/** 일별 매입 아이템 */
|
||||
export interface PurchaseDailyItemApiResponse {
|
||||
date: string; // "2026-02-01"
|
||||
supplier: string; // 거래처명
|
||||
item: string; // 품목명
|
||||
amount: number; // 금액
|
||||
status: 'paid' | 'unpaid' | 'partial'; // 결제상태
|
||||
}
|
||||
|
||||
/** GET /api/v1/dashboard/purchases/summary 응답 */
|
||||
export interface PurchaseStatusApiResponse {
|
||||
cumulative_purchase: number; // 누적 매입
|
||||
unpaid_amount: number; // 미결제 금액
|
||||
yoy_change: number; // 전년 동월 대비 변화율 (%)
|
||||
monthly_trend: PurchaseMonthlyTrendDashboardApiResponse[];
|
||||
material_ratio: PurchaseMaterialRatioApiResponse[];
|
||||
daily_items: PurchaseDailyItemApiResponse[];
|
||||
daily_total: number; // 일별 합계
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 21. DailyProduction (생산 현황) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 공정별 작업 아이템 */
|
||||
export interface ProductionWorkItemApiResponse {
|
||||
id: string;
|
||||
order_no: string; // 수주번호
|
||||
client: string; // 거래처명
|
||||
product: string; // 제품명
|
||||
quantity: number; // 수량
|
||||
status: 'in_progress' | 'pending' | 'completed';
|
||||
}
|
||||
|
||||
/** 공정별 작업자 */
|
||||
export interface ProductionWorkerApiResponse {
|
||||
name: string;
|
||||
assigned: number; // 배정 건수
|
||||
completed: number; // 완료 건수
|
||||
rate: number; // 완료율 (%)
|
||||
}
|
||||
|
||||
/** 공정별 데이터 */
|
||||
export interface ProductionProcessApiResponse {
|
||||
process_name: string; // "스크린", "슬랫", "절곡"
|
||||
total_work: number;
|
||||
todo: number;
|
||||
in_progress: number;
|
||||
completed: number;
|
||||
urgent: number; // 긴급 건수
|
||||
sub_line: number;
|
||||
regular: number;
|
||||
worker_count: number;
|
||||
work_items: ProductionWorkItemApiResponse[];
|
||||
workers: ProductionWorkerApiResponse[];
|
||||
}
|
||||
|
||||
/** 출고 현황 */
|
||||
export interface ShipmentApiResponse {
|
||||
expected_amount: number;
|
||||
expected_count: number;
|
||||
actual_amount: number;
|
||||
actual_count: number;
|
||||
}
|
||||
|
||||
/** GET /api/v1/dashboard/production/summary 응답 */
|
||||
export interface DailyProductionApiResponse {
|
||||
date: string; // "2026-02-23"
|
||||
day_of_week: string; // "월요일"
|
||||
processes: ProductionProcessApiResponse[];
|
||||
shipment: ShipmentApiResponse;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 22. Unshipped (미출고 내역) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 미출고 아이템 */
|
||||
export interface UnshippedItemApiResponse {
|
||||
id: string;
|
||||
port_no: string; // 로트번호
|
||||
site_name: string; // 현장명
|
||||
order_client: string; // 수주처
|
||||
due_date: string; // 납기일
|
||||
days_left: number; // 잔여일수
|
||||
}
|
||||
|
||||
/** GET /api/v1/dashboard/unshipped/summary 응답 */
|
||||
export interface UnshippedApiResponse {
|
||||
items: UnshippedItemApiResponse[];
|
||||
total_count: number;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 23. Construction (시공 현황) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 시공 아이템 */
|
||||
export interface ConstructionItemApiResponse {
|
||||
id: string;
|
||||
site_name: string; // 현장명
|
||||
client: string; // 거래처명
|
||||
start_date: string; // 시작일
|
||||
end_date: string; // 종료일
|
||||
progress: number; // 진행률 (%)
|
||||
status: 'in_progress' | 'scheduled' | 'completed';
|
||||
}
|
||||
|
||||
/** GET /api/v1/dashboard/construction/summary 응답 */
|
||||
export interface ConstructionApiResponse {
|
||||
this_month: number; // 이번 달 시공 건수
|
||||
completed: number; // 완료 건수
|
||||
items: ConstructionItemApiResponse[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 24. DailyAttendance (근태 현황) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 직원 근태 아이템 */
|
||||
export interface AttendanceEmployeeApiResponse {
|
||||
id: string;
|
||||
department: string; // 부서명
|
||||
position: string; // 직급
|
||||
name: string; // 이름
|
||||
status: 'present' | 'on_leave' | 'late' | 'absent';
|
||||
}
|
||||
|
||||
/** GET /api/v1/dashboard/attendance/summary 응답 */
|
||||
export interface DailyAttendanceApiResponse {
|
||||
present: number; // 출근
|
||||
on_leave: number; // 휴가
|
||||
late: number; // 지각
|
||||
absent: number; // 결근
|
||||
employees: AttendanceEmployeeApiResponse[];
|
||||
}
|
||||
Reference in New Issue
Block a user