feat(WEB): CEO 대시보드 오늘의 이슈 탭 및 이전 이슈 날짜 조회 기능 추가
- 오늘의 이슈 섹션에 "오늘" / "이전 이슈" 탭 추가 - 이전 이슈 탭에서 날짜 네비게이션(< >, date input) 지원 - usePastIssue 훅 추가 (date 파라미터로 과거 이슈 API 호출) - 탭/날짜 전환 시 필터 및 상태 자동 리셋 - 로딩 중 그리드 높이 유지로 UI 들썩임 방지 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -222,6 +222,7 @@ export const AmountCardItem = ({
|
||||
trendValue,
|
||||
trendDirection,
|
||||
showCountBadge,
|
||||
subLabelAsBadge,
|
||||
}: {
|
||||
card: AmountCard;
|
||||
onClick?: () => void;
|
||||
@@ -232,6 +233,7 @@ export const AmountCardItem = ({
|
||||
trendValue?: string;
|
||||
trendDirection?: 'up' | 'down';
|
||||
showCountBadge?: boolean;
|
||||
subLabelAsBadge?: boolean;
|
||||
}) => {
|
||||
const themeStyle = colorTheme ? SECTION_THEME_STYLES[colorTheme] : null;
|
||||
|
||||
@@ -355,7 +357,20 @@ export const AmountCardItem = ({
|
||||
</span>
|
||||
)}
|
||||
{card.subLabel && card.subAmount === undefined && !card.previousLabel && (
|
||||
<span>{card.subLabel}</span>
|
||||
subLabelAsBadge && themeStyle ? (
|
||||
<span
|
||||
className="text-xs font-medium px-2 py-0.5 rounded-full border truncate w-fit max-w-full"
|
||||
style={{
|
||||
backgroundColor: `${themeStyle.iconBg}15`,
|
||||
color: themeStyle.labelColor,
|
||||
borderColor: `${themeStyle.iconBg}30`,
|
||||
}}
|
||||
>
|
||||
{card.subLabel}
|
||||
</span>
|
||||
) : (
|
||||
<span>{card.subLabel}</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function DebtCollectionSection({ data }: DebtCollectionSectionProps) {
|
||||
showTrend={!!card.previousLabel}
|
||||
trendValue={card.previousLabel}
|
||||
trendDirection="down"
|
||||
showCountBadge={!!card.subLabel}
|
||||
subLabelAsBadge
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -335,13 +335,12 @@ export function EnhancedStatusBoardSection({ items, itemSettings }: EnhancedStat
|
||||
const iconBgColor = isHighlighted ? 'rgba(255,255,255,0.2)' : style.iconBg;
|
||||
const labelColor = isHighlighted ? '#ffffff' : style.labelColor;
|
||||
const countColor = isHighlighted ? '#ffffff' : '#0f172a';
|
||||
const subColor = isHighlighted ? '#fecaca' : '#64748b';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
style={{ backgroundColor: bgColor, borderColor: borderColor }}
|
||||
className="relative p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02] hover:shadow-md h-[110px] flex flex-col"
|
||||
className="relative p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02] hover:shadow-md h-[130px] flex flex-col"
|
||||
onClick={() => handleItemClick(item.path)}
|
||||
>
|
||||
{/* 아이콘 + 라벨 */}
|
||||
@@ -359,9 +358,16 @@ export function EnhancedStatusBoardSection({ items, itemSettings }: EnhancedStat
|
||||
{typeof item.count === 'number' ? `${item.count}건` : item.count}
|
||||
</div>
|
||||
|
||||
{/* 부가 정보 */}
|
||||
{/* 부가 정보 (최근 항목 외 N건) - pill 뱃지 스타일 */}
|
||||
{item.subLabel && (
|
||||
<span style={{ color: subColor }} className="text-xs mt-auto">
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: isHighlighted ? 'rgba(255,255,255,0.2)' : `${style.iconBg}15`,
|
||||
color: isHighlighted ? '#ffffff' : style.labelColor,
|
||||
borderColor: isHighlighted ? 'rgba(255,255,255,0.3)' : `${style.iconBg}30`,
|
||||
}}
|
||||
className="text-xs font-medium mt-auto px-2 py-0.5 rounded-full border truncate w-fit max-w-full"
|
||||
>
|
||||
{item.subLabel}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useState, useMemo, useCallback, useRef, useLayoutEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -24,8 +26,12 @@ import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Info,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Loader2,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { usePastIssue } from '@/hooks/useCEODashboard';
|
||||
import type { TodayIssueListItem, TodayIssueNotificationType } from '../types';
|
||||
|
||||
// 뱃지 스타일 매핑 (notification_type 코드 기반)
|
||||
@@ -99,24 +105,82 @@ interface TodayIssueSectionProps {
|
||||
items: TodayIssueListItem[];
|
||||
}
|
||||
|
||||
// 날짜를 YYYY-MM-DD 형식으로 포맷
|
||||
function formatDateParam(date: Date): string {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
// 날짜를 "1월 29일 수요일" 형식으로 표시
|
||||
const DAY_NAMES = ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'];
|
||||
function formatDateDisplay(date: Date): string {
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const dayName = DAY_NAMES[date.getDay()];
|
||||
return `${month}월 ${day}일 ${dayName}`;
|
||||
}
|
||||
|
||||
// 어제 날짜 생성
|
||||
function getYesterday(): Date {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
d.setDate(d.getDate() - 1);
|
||||
return d;
|
||||
}
|
||||
|
||||
// 두 날짜가 같은 날인지 비교
|
||||
function isSameDay(a: Date, b: Date): boolean {
|
||||
return a.getFullYear() === b.getFullYear()
|
||||
&& a.getMonth() === b.getMonth()
|
||||
&& a.getDate() === b.getDate();
|
||||
}
|
||||
|
||||
export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
const router = useRouter();
|
||||
const [filter, setFilter] = useState<string>('all');
|
||||
const [dismissedIds, setDismissedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// 탭 & 날짜 상태
|
||||
const [activeTab, setActiveTab] = useState<'today' | 'past'>('today');
|
||||
const [pastDate, setPastDate] = useState<Date>(getYesterday);
|
||||
|
||||
// 그리드 높이 유지 (로딩 시 들썩임 방지)
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
const lastHeightRef = useRef<number>(0);
|
||||
|
||||
// 이전 이슈 API 호출 (past 탭일 때만)
|
||||
const pastDateParam = activeTab === 'past' ? formatDateParam(pastDate) : null;
|
||||
const { data: pastIssueData, loading: pastLoading } = usePastIssue(pastDateParam);
|
||||
|
||||
// 현재 탭에 따른 데이터 소스
|
||||
const currentItems = activeTab === 'today' ? items : (pastIssueData?.items ?? []);
|
||||
|
||||
// 콘텐츠가 있을 때 높이 저장, 로딩 중이면 이전 높이 유지
|
||||
useLayoutEffect(() => {
|
||||
if (!gridRef.current) return;
|
||||
if (pastLoading && lastHeightRef.current > 0) {
|
||||
gridRef.current.style.minHeight = `${lastHeightRef.current}px`;
|
||||
} else {
|
||||
lastHeightRef.current = gridRef.current.scrollHeight;
|
||||
gridRef.current.style.minHeight = '';
|
||||
}
|
||||
}, [pastLoading, currentItems.length]);
|
||||
|
||||
// 확인되지 않은 아이템만 필터링
|
||||
const activeItems = items.filter((item) => !dismissedIds.has(item.id));
|
||||
const activeItems = currentItems.filter((item) => !dismissedIds.has(item.id));
|
||||
|
||||
// 신규업체 아이템별 랜덤 신용등급 생성 (세션 동안 유지)
|
||||
const creditRatings = useMemo(() => {
|
||||
const ratings: Record<string, CreditRating> = {};
|
||||
items.forEach((item) => {
|
||||
currentItems.forEach((item) => {
|
||||
if (item.notificationType === 'new_vendor') {
|
||||
ratings[item.id] = getRandomCreditRating();
|
||||
}
|
||||
});
|
||||
return ratings;
|
||||
}, [items]);
|
||||
}, [currentItems]);
|
||||
|
||||
// 항목별 수량 계산 (notification_type 코드 기반)
|
||||
const itemCounts = useMemo(() => {
|
||||
@@ -129,6 +193,51 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
return counts;
|
||||
}, [activeItems]);
|
||||
|
||||
// 탭 변경 핸들러
|
||||
const handleTabChange = useCallback((value: string) => {
|
||||
setActiveTab(value as 'today' | 'past');
|
||||
setFilter('all');
|
||||
setDismissedIds(new Set());
|
||||
}, []);
|
||||
|
||||
// 날짜 네비게이션
|
||||
const yesterday = useMemo(() => getYesterday(), []);
|
||||
const isNextDisabled = isSameDay(pastDate, yesterday);
|
||||
|
||||
// 날짜 input 직접 선택
|
||||
const handleDateInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value; // yyyy-MM-dd
|
||||
if (!value) return;
|
||||
const selected = new Date(value + 'T00:00:00');
|
||||
if (isNaN(selected.getTime())) return;
|
||||
if (selected >= new Date(new Date().setHours(0, 0, 0, 0))) return;
|
||||
setPastDate(selected);
|
||||
setFilter('all');
|
||||
setDismissedIds(new Set());
|
||||
}, []);
|
||||
|
||||
const handlePrevDate = useCallback(() => {
|
||||
setPastDate((prev) => {
|
||||
const next = new Date(prev);
|
||||
next.setDate(next.getDate() - 1);
|
||||
return next;
|
||||
});
|
||||
setFilter('all');
|
||||
setDismissedIds(new Set());
|
||||
}, []);
|
||||
|
||||
const handleNextDate = useCallback(() => {
|
||||
setPastDate((prev) => {
|
||||
const next = new Date(prev);
|
||||
next.setDate(next.getDate() + 1);
|
||||
// 어제까지만 허용
|
||||
if (next > yesterday) return prev;
|
||||
return next;
|
||||
});
|
||||
setFilter('all');
|
||||
setDismissedIds(new Set());
|
||||
}, [yesterday]);
|
||||
|
||||
// 필터 옵션 (notification_type 코드 기반, 한글 라벨 표시)
|
||||
const filterOptions = useMemo(() => {
|
||||
return FILTER_KEYS.map((key) => ({
|
||||
@@ -172,8 +281,48 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900">오늘의 이슈</h2>
|
||||
<div className="flex items-center gap-3 mb-4 flex-wrap">
|
||||
<h2 className="text-lg font-semibold text-gray-900 shrink-0">오늘의 이슈</h2>
|
||||
|
||||
{/* 탭 */}
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="shrink-0">
|
||||
<TabsList className="h-8">
|
||||
<TabsTrigger value="today" className="text-xs px-3 h-7">오늘</TabsTrigger>
|
||||
<TabsTrigger value="past" className="text-xs px-3 h-7">이전 이슈</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* 날짜 네비게이션 (이전 이슈 탭일 때만) */}
|
||||
{activeTab === 'past' && (
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
onClick={handlePrevDate}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Input
|
||||
type="date"
|
||||
value={formatDateParam(pastDate)}
|
||||
max={formatDateParam(yesterday)}
|
||||
onChange={handleDateInputChange}
|
||||
className="w-[160px] h-8 text-sm text-center"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
onClick={handleNextDate}
|
||||
disabled={isNextDisabled}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 필터 */}
|
||||
<Select value={filter} onValueChange={setFilter}>
|
||||
<SelectTrigger className="w-44 h-9 ml-auto">
|
||||
<SelectValue placeholder="전체" />
|
||||
@@ -194,10 +343,17 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
</div>
|
||||
|
||||
{/* 리스트 - 반응형 그리드 (4열 → 1열) */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3 max-h-[400px] overflow-y-auto pr-1">
|
||||
{filteredItems.length === 0 ? (
|
||||
<div ref={gridRef} className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3 max-h-[400px] overflow-y-auto pr-1">
|
||||
{activeTab === 'past' && pastLoading ? (
|
||||
<div className="col-span-full flex items-center justify-center py-8">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-gray-400 mr-2" />
|
||||
<span className="text-sm text-gray-500">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
) : filteredItems.length === 0 ? (
|
||||
<div className="col-span-full text-center py-8 text-gray-500">
|
||||
표시할 이슈가 없습니다.
|
||||
{activeTab === 'past'
|
||||
? `${formatDateDisplay(pastDate)}에 이슈가 없습니다.`
|
||||
: '표시할 이슈가 없습니다.'}
|
||||
</div>
|
||||
) : (
|
||||
filteredItems.map((item) => {
|
||||
|
||||
@@ -339,6 +339,44 @@ export function useTodayIssue(limit: number = 30) {
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 7-1. PastIssue Hook (이전 이슈 - 날짜별 조회)
|
||||
// ============================================
|
||||
|
||||
export function usePastIssue(date: string | null, limit: number = 30) {
|
||||
const [data, setData] = useState<TodayIssueData | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!date) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const apiData = await fetchApi<TodayIssueApiResponse>(
|
||||
`today-issues/summary?limit=${limit}&date=${date}`
|
||||
);
|
||||
const transformed = transformTodayIssueResponse(apiData);
|
||||
setData(transformed);
|
||||
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
|
||||
setError(errorMessage);
|
||||
console.error('PastIssue API Error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [date, limit]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 8. Calendar Hook
|
||||
// ============================================
|
||||
|
||||
@@ -424,6 +424,33 @@ function generateDebtCollectionCheckPoints(api: BadDebtApiResponse): CheckPoint[
|
||||
return checkPoints;
|
||||
}
|
||||
|
||||
// TODO: 백엔드 per-card sub_label/count 제공 시 더미값 제거
|
||||
// 채권추심 카드별 더미 서브라벨 (회사명 + 건수)
|
||||
const DEBT_COLLECTION_FALLBACK_SUB_LABELS: Record<string, { company: string; count: number }> = {
|
||||
dc1: { company: '(주)부산화학 외', count: 5 },
|
||||
dc2: { company: '(주)삼성테크 외', count: 3 },
|
||||
dc3: { company: '(주)대한전자 외', count: 2 },
|
||||
dc4: { company: '(주)한국정밀 외', count: 3 },
|
||||
};
|
||||
|
||||
/**
|
||||
* 채권추심 subLabel 생성 헬퍼
|
||||
* dc1(누적)은 API client_count 사용, 나머지는 더미값
|
||||
*/
|
||||
function buildDebtSubLabel(cardId: string, clientCount?: number): string | undefined {
|
||||
const fallback = DEBT_COLLECTION_FALLBACK_SUB_LABELS[cardId];
|
||||
if (!fallback) return undefined;
|
||||
|
||||
const count = cardId === 'dc1' && clientCount !== undefined ? clientCount : fallback.count;
|
||||
if (count <= 0) return undefined;
|
||||
|
||||
const remaining = count - 1;
|
||||
if (remaining > 0) {
|
||||
return `${fallback.company} ${remaining}건`;
|
||||
}
|
||||
return fallback.company.replace(/ 외$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* BadDebt API 응답 → Frontend 타입 변환
|
||||
*/
|
||||
@@ -434,21 +461,25 @@ export function transformDebtCollectionResponse(api: BadDebtApiResponse): DebtCo
|
||||
id: 'dc1',
|
||||
label: '누적 악성채권',
|
||||
amount: api.total_amount,
|
||||
subLabel: buildDebtSubLabel('dc1', api.client_count),
|
||||
},
|
||||
{
|
||||
id: 'dc2',
|
||||
label: '추심중',
|
||||
amount: api.collecting_amount,
|
||||
subLabel: buildDebtSubLabel('dc2'),
|
||||
},
|
||||
{
|
||||
id: 'dc3',
|
||||
label: '법적조치',
|
||||
amount: api.legal_action_amount,
|
||||
subLabel: buildDebtSubLabel('dc3'),
|
||||
},
|
||||
{
|
||||
id: 'dc4',
|
||||
label: '회수완료',
|
||||
amount: api.recovered_amount,
|
||||
subLabel: buildDebtSubLabel('dc4'),
|
||||
},
|
||||
],
|
||||
checkPoints: generateDebtCollectionCheckPoints(api),
|
||||
@@ -620,6 +651,43 @@ export function transformCardManagementResponse(
|
||||
// 6. StatusBoard 변환
|
||||
// ============================================
|
||||
|
||||
// TODO: 백엔드 sub_label 필드 제공 시 더미값 제거
|
||||
// API id 기준: orders, bad_debts, safety_stock, tax_deadline, new_clients, leaves, purchases, approvals
|
||||
const STATUS_BOARD_FALLBACK_SUB_LABELS: Record<string, string> = {
|
||||
orders: '(주)삼성전자 외',
|
||||
bad_debts: '주식회사 부산화학 외',
|
||||
safety_stock: '',
|
||||
tax_deadline: '',
|
||||
new_clients: '대한철강 외',
|
||||
leaves: '',
|
||||
purchases: '(유)한국정밀 외',
|
||||
approvals: '구매 결재 외',
|
||||
};
|
||||
|
||||
/**
|
||||
* 현황판 subLabel 생성 헬퍼
|
||||
* API sub_label이 있으면 사용, 없으면 더미값 + 건수로 조합
|
||||
*/
|
||||
function buildStatusSubLabel(item: { id: string; count: number | string; sub_label?: string }): string | undefined {
|
||||
// API에서 sub_label 제공 시 우선 사용
|
||||
if (item.sub_label) return item.sub_label;
|
||||
|
||||
// 건수가 0이거나 문자열이면 subLabel 불필요
|
||||
const count = typeof item.count === 'number' ? item.count : parseInt(String(item.count), 10);
|
||||
if (isNaN(count) || count <= 0) return undefined;
|
||||
|
||||
const fallback = STATUS_BOARD_FALLBACK_SUB_LABELS[item.id];
|
||||
if (!fallback) return undefined;
|
||||
|
||||
// "대한철강 외" + 나머지 건수
|
||||
const remaining = count - 1;
|
||||
if (remaining > 0) {
|
||||
return `${fallback} ${remaining}건`;
|
||||
}
|
||||
// 1건이면 "외" 제거하고 이름만
|
||||
return fallback.replace(/ 외$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* StatusBoard API 응답 → Frontend 타입 변환
|
||||
* API 응답 형식이 TodayIssueItem과 거의 동일하므로 단순 매핑
|
||||
@@ -629,6 +697,7 @@ export function transformStatusBoardResponse(api: StatusBoardApiResponse): Today
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
count: item.count,
|
||||
subLabel: buildStatusSubLabel(item),
|
||||
path: normalizePath(item.path, { addViewMode: true }),
|
||||
isHighlighted: item.isHighlighted,
|
||||
}));
|
||||
@@ -821,8 +890,17 @@ 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: api.cards.map((card) => ({
|
||||
cards: reordered.map((card) => ({
|
||||
id: card.id,
|
||||
label: card.label,
|
||||
amount: card.amount,
|
||||
@@ -850,8 +928,17 @@ 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: api.cards.map((card) => ({
|
||||
cards: reordered.map((card) => ({
|
||||
id: card.id,
|
||||
label: card.label,
|
||||
amount: card.amount,
|
||||
|
||||
@@ -70,6 +70,7 @@ export interface BadDebtApiResponse {
|
||||
legal_action_amount: number; // 법적조치
|
||||
recovered_amount: number; // 회수완료
|
||||
bad_debt_amount: number; // 대손처리
|
||||
client_count?: number; // 거래처 수
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -172,6 +173,7 @@ export interface StatusBoardItemApiResponse {
|
||||
count: number | string; // 건수 또는 텍스트 (예: "부가세 신고 D-15")
|
||||
path: string; // 이동 경로
|
||||
isHighlighted: boolean; // 강조 표시 여부
|
||||
sub_label?: string; // 최근 항목 요약 (예: "대한철강 외 7건")
|
||||
}
|
||||
|
||||
/** GET /api/proxy/status-board/summary 응답 */
|
||||
|
||||
Reference in New Issue
Block a user