feat: CEO 대시보드 API 연동 강화 및 회계/결재/HR 개선

- CEO 대시보드: 예상비용, 현황이슈, 일별매출/매입 등 모달 API 연동 확대
- dashboard transformers 리팩토링 (hr, sales-purchase, production-logistics 분리)
- useCEODashboard 훅 대폭 확장 (모달 데이터 fetching 로직)
- DailyReport: USD 섹션 추가 및 레이아웃 개선
- VendorManagement/ApprovalBox: 소폭 개선
- VacationManagement: 소폭 수정
- component-registry previews 업데이트
- claudedocs: 대시보드 API 스펙, 분석 문서 추가
This commit is contained in:
유병철
2026-03-03 22:18:48 +09:00
parent 7bb8699403
commit cde9333652
30 changed files with 2852 additions and 269 deletions

View File

@@ -11,4 +11,7 @@ export { transformMonthlyExpenseResponse, transformCardManagementResponse } from
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 { 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';

View File

@@ -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),

View File

@@ -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' },
],
},
};
}

View 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] ?? '출근',
})),
};
}

View 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] ?? '진행중',
})),
};
}

View 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,
};
}

View File

@@ -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,
};
});
}
// ============================================

View File

@@ -40,7 +40,11 @@ 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;
}
@@ -706,4 +710,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[];
}