feat(WEB): CEO 대시보드 오늘의 이슈 탭 및 이전 이슈 날짜 조회 기능 추가

- 오늘의 이슈 섹션에 "오늘" / "이전 이슈" 탭 추가
- 이전 이슈 탭에서 날짜 네비게이션(< >, date input) 지원
- usePastIssue 훅 추가 (date 파라미터로 과거 이슈 API 호출)
- 탭/날짜 전환 시 필터 및 상태 자동 리셋
- 로딩 중 그리드 높이 유지로 UI 들썩임 방지

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-30 15:23:35 +09:00
parent 103a2b9f03
commit 9f7f55aeff
7 changed files with 321 additions and 17 deletions

View File

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

View File

@@ -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 응답 */