From db84d6796bbb27a26425bccf0acc193c0f0bd73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Sun, 1 Mar 2026 12:19:53 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=EA=B3=B5=ED=86=B5]=20Sidebar,=20?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=ED=9B=85,=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sidebar 레이아웃 개선 - useCEODashboard 최적화, useDashboardFetch 훅 신규 - amount, status-config 유틸 개선 - dashboard transformers 개선 Co-Authored-By: Claude Opus 4.6 --- src/components/layout/Sidebar.tsx | 12 +- src/hooks/useCEODashboard.ts | 1098 ++++------------- src/hooks/useDashboardFetch.ts | 156 +++ .../dashboard/transformers/expense-detail.ts | 40 - .../dashboard/transformers/tax-benefits.ts | 15 +- src/lib/utils/amount.ts | 13 + src/lib/utils/status-config.ts | 3 +- 7 files changed, 423 insertions(+), 914 deletions(-) create mode 100644 src/hooks/useDashboardFetch.ts diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 4282a1ad..6df741aa 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -153,7 +153,9 @@ function MenuItemComponent({ className={`flex-shrink-0 p-1 rounded transition-all duration-200 ${ isFav ? 'opacity-100 text-yellow-500' - : 'opacity-0 group-hover/row:opacity-100 text-muted-foreground hover:text-yellow-500' + : isMobile + ? 'opacity-50 text-muted-foreground active:text-yellow-500' + : 'opacity-0 group-hover/row:opacity-100 text-muted-foreground hover:text-yellow-500' }`} title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'} > @@ -216,7 +218,9 @@ function MenuItemComponent({ className={`flex-shrink-0 p-0.5 rounded transition-all duration-200 ${ isFav ? 'opacity-100 text-yellow-500' - : 'opacity-0 group-hover/row:opacity-100 text-muted-foreground hover:text-yellow-500' + : isMobile + ? 'opacity-50 text-muted-foreground active:text-yellow-500' + : 'opacity-0 group-hover/row:opacity-100 text-muted-foreground hover:text-yellow-500' }`} title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'} > @@ -281,7 +285,9 @@ function MenuItemComponent({ className={`flex-shrink-0 p-0.5 rounded transition-all duration-200 ${ isFav ? 'opacity-100 text-yellow-500' - : 'opacity-0 group-hover/row:opacity-100 text-muted-foreground hover:text-yellow-500' + : isMobile + ? 'opacity-50 text-muted-foreground active:text-yellow-500' + : 'opacity-0 group-hover/row:opacity-100 text-muted-foreground hover:text-yellow-500' }`} title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'} > diff --git a/src/hooks/useCEODashboard.ts b/src/hooks/useCEODashboard.ts index b09c9df6..2e48ab67 100644 --- a/src/hooks/useCEODashboard.ts +++ b/src/hooks/useCEODashboard.ts @@ -1,12 +1,18 @@ - /** * CEO Dashboard API 연동 Hook * * 각 섹션별 API 호출 및 데이터 변환 담당 - * 참조 패턴: useClientList.ts + * 제네릭 useDashboardFetch 훅을 활용하여 보일러플레이트 최소화 */ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useMemo } from 'react'; + +import { useDashboardFetch } from './useDashboardFetch'; + +import { + fetchLoanDashboard, + fetchTaxSimulation, +} from '@/lib/api/dashboard/endpoints'; import type { DailyReportApiResponse, @@ -22,15 +28,8 @@ import type { WelfareApiResponse, WelfareDetailApiResponse, ExpectedExpenseDashboardDetailApiResponse, - LoanDashboardApiResponse, - TaxSimulationApiResponse, } from '@/lib/api/dashboard/types'; -import { - fetchLoanDashboard, - fetchTaxSimulation, -} from '@/lib/api/dashboard/endpoints'; - import { transformDailyReportResponse, transformReceivableResponse, @@ -45,9 +44,6 @@ import { transformWelfareResponse, transformWelfareDetailResponse, transformExpectedExpenseDetailResponse, - transformPurchaseDetailResponse, - transformCardDetailResponse, - transformBillDetailResponse, } from '@/lib/api/dashboard/transformers'; import type { @@ -66,163 +62,79 @@ import type { } from '@/components/business/CEODashboard/types'; // ============================================ -// 공통 fetch 유틸리티 +// 쿼리 파라미터 빌더 유틸리티 // ============================================ -async function fetchApi(endpoint: string): Promise { - const response = await fetch(`/api/proxy/${endpoint}`); - - if (!response.ok) { - throw new Error(`API 오류: ${response.status}`); +function buildEndpoint( + base: string, + params: Record, +): string { + const searchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value != null && value !== '') { + searchParams.append(key, String(value)); + } } - - const result = await response.json(); - - if (!result.success) { - throw new Error(result.message || '데이터 조회 실패'); - } - - return result.data; + const qs = searchParams.toString(); + return qs ? `${base}?${qs}` : base; } // ============================================ -// 1. DailyReport Hook +// CardManagement 전용 fetch 유틸리티 +// ============================================ + +async function fetchCardManagementData(fallbackData?: CardManagementData) { + const [cardApiData, loanResponse, taxResponse] = await Promise.all([ + fetch('/api/proxy/card-transactions/summary').then(async (r) => { + if (!r.ok) throw new Error(`API 오류: ${r.status}`); + const json = await r.json(); + if (!json.success) throw new Error(json.message || '데이터 조회 실패'); + return json.data as CardTransactionApiResponse; + }), + fetchLoanDashboard(), + fetchTaxSimulation(), + ]); + + const loanData = loanResponse.success ? loanResponse.data : null; + const taxData = taxResponse.success ? taxResponse.data : null; + + return transformCardManagementResponse(cardApiData, loanData, taxData, fallbackData); +} + +// ============================================ +// 1~4. 단순 섹션 Hooks (파라미터 없음) // ============================================ export function useDailyReport() { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const apiData = await fetchApi('daily-report/summary'); - const transformed = transformDailyReportResponse(apiData); - setData(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('DailyReport API Error:', err); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { data, loading, error, refetch: fetchData }; + return useDashboardFetch( + 'daily-report/summary', + transformDailyReportResponse, + ); } -// ============================================ -// 2. Receivable Hook -// ============================================ - export function useReceivable() { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const apiData = await fetchApi('receivables/summary'); - const transformed = transformReceivableResponse(apiData); - setData(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('Receivable API Error:', err); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { data, loading, error, refetch: fetchData }; + return useDashboardFetch( + 'receivables/summary', + transformReceivableResponse, + ); } -// ============================================ -// 3. DebtCollection Hook -// ============================================ - export function useDebtCollection() { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const apiData = await fetchApi('bad-debts/summary'); - const transformed = transformDebtCollectionResponse(apiData); - setData(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('DebtCollection API Error:', err); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { data, loading, error, refetch: fetchData }; + return useDashboardFetch( + 'bad-debts/summary', + transformDebtCollectionResponse, + ); } -// ============================================ -// 4. MonthlyExpense Hook -// ============================================ - export function useMonthlyExpense() { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const apiData = await fetchApi('expected-expenses/summary'); - const transformed = transformMonthlyExpenseResponse(apiData); - setData(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('MonthlyExpense API Error:', err); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { data, loading, error, refetch: fetchData }; + return useDashboardFetch( + 'expected-expenses/summary', + transformMonthlyExpenseResponse, + ); } // ============================================ -// 5. CardManagement Hook +// 5. CardManagement Hook (커스텀: 3개 API 병렬 호출) // ============================================ export function useCardManagement(fallbackData?: CardManagementData) { @@ -234,24 +146,10 @@ export function useCardManagement(fallbackData?: CardManagementData) { try { setLoading(true); setError(null); - - // 3개 API 병렬 호출: 카드거래, 가지급금, 세금 시뮬레이션 - const [cardApiData, loanResponse, taxResponse] = await Promise.all([ - fetchApi('card-transactions/summary'), - fetchLoanDashboard(), - fetchTaxSimulation(), - ]); - - // LoanDashboard와 TaxSimulation은 ApiResponse wrapper가 있으므로 data 추출 - const loanData = loanResponse.success ? loanResponse.data : null; - const taxData = taxResponse.success ? taxResponse.data : null; - - const transformed = transformCardManagementResponse(cardApiData, loanData, taxData, fallbackData); - setData(transformed); - + const result = await fetchCardManagementData(fallbackData); + setData(result); } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); + setError(err instanceof Error ? err.message : '데이터 로딩 실패'); console.error('CardManagement API Error:', err); } finally { setLoading(false); @@ -270,33 +168,10 @@ export function useCardManagement(fallbackData?: CardManagementData) { // ============================================ export function useStatusBoard() { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const apiData = await fetchApi('status-board/summary'); - const transformed = transformStatusBoardResponse(apiData); - setData(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('StatusBoard API Error:', err); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { data, loading, error, refetch: fetchData }; + return useDashboardFetch( + 'status-board/summary', + transformStatusBoardResponse, + ); } // ============================================ @@ -309,33 +184,14 @@ export interface TodayIssueData { } export function useTodayIssue(limit: number = 30) { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const apiData = await fetchApi(`today-issues/summary?limit=${limit}`); - const transformed = transformTodayIssueResponse(apiData); - setData(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('TodayIssue API Error:', err); - } finally { - setLoading(false); - } - }, [limit]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { data, loading, error, refetch: fetchData }; + const endpoint = useMemo( + () => buildEndpoint('today-issues/summary', { limit }), + [limit], + ); + return useDashboardFetch( + endpoint, + transformTodayIssueResponse, + ); } // ============================================ @@ -343,37 +199,15 @@ export function useTodayIssue(limit: number = 30) { // ============================================ export function usePastIssue(date: string | null, limit: number = 30) { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - if (!date) return; - - try { - setLoading(true); - setError(null); - - const apiData = await fetchApi( - `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 }; + const endpoint = useMemo( + () => (date ? buildEndpoint('today-issues/summary', { limit, date }) : null), + [date, limit], + ); + return useDashboardFetch( + endpoint, + transformTodayIssueResponse, + { initialLoading: false }, + ); } // ============================================ @@ -386,55 +220,30 @@ export interface CalendarData { } export interface UseCalendarOptions { - /** 조회 시작일 (Y-m-d, 기본: 이번 달 1일) */ startDate?: string; - /** 조회 종료일 (Y-m-d, 기본: 이번 달 말일) */ endDate?: string; - /** 일정 타입 필터 (schedule|order|construction|other|null=전체) */ type?: 'schedule' | 'order' | 'construction' | 'other' | null; - /** 부서 필터 (all|department|personal) */ departmentFilter?: 'all' | 'department' | 'personal'; } export function useCalendar(options: UseCalendarOptions = {}) { const { startDate, endDate, type, departmentFilter = 'all' } = options; - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); + const endpoint = useMemo( + () => + buildEndpoint('calendar/schedules', { + start_date: startDate, + end_date: endDate, + type: type ?? undefined, + department_filter: departmentFilter, + }), + [startDate, endDate, type, departmentFilter], + ); - // 쿼리 파라미터 구성 - const params = new URLSearchParams(); - if (startDate) params.append('start_date', startDate); - if (endDate) params.append('end_date', endDate); - if (type) params.append('type', type); - if (departmentFilter) params.append('department_filter', departmentFilter); - - const queryString = params.toString(); - const endpoint = queryString ? `calendar/schedules?${queryString}` : 'calendar/schedules'; - - const apiData = await fetchApi(endpoint); - const transformed = transformCalendarResponse(apiData); - setData(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('Calendar API Error:', err); - } finally { - setLoading(false); - } - }, [startDate, endDate, type, departmentFilter]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { data, loading, error, refetch: fetchData }; + return useDashboardFetch( + endpoint, + transformCalendarResponse, + ); } // ============================================ @@ -442,52 +251,25 @@ export function useCalendar(options: UseCalendarOptions = {}) { // ============================================ export interface UseVatOptions { - /** 기간 타입 (quarter: 분기, half: 반기, year: 연간) */ periodType?: 'quarter' | 'half' | 'year'; - /** 연도 (기본: 현재 연도) */ year?: number; - /** 기간 번호 (quarter: 1-4, half: 1-2) */ period?: number; } export function useVat(options: UseVatOptions = {}) { const { periodType = 'quarter', year, period } = options; - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); + const endpoint = useMemo( + () => + buildEndpoint('vat/summary', { + period_type: periodType, + year, + period, + }), + [periodType, year, period], + ); - // 쿼리 파라미터 구성 - const params = new URLSearchParams(); - params.append('period_type', periodType); - if (year) params.append('year', year.toString()); - if (period) params.append('period', period.toString()); - - const queryString = params.toString(); - const endpoint = `vat/summary?${queryString}`; - - const apiData = await fetchApi(endpoint); - const transformed = transformVatResponse(apiData); - setData(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('Vat API Error:', err); - } finally { - setLoading(false); - } - }, [periodType, year, period]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { data, loading, error, refetch: fetchData }; + return useDashboardFetch(endpoint, transformVatResponse); } // ============================================ @@ -495,55 +277,30 @@ export function useVat(options: UseVatOptions = {}) { // ============================================ export interface UseEntertainmentOptions { - /** 기간 타입 (annual: 연간, quarterly: 분기) */ limitType?: 'annual' | 'quarterly'; - /** 기업 유형 (large: 대기업, medium: 중견기업, small: 중소기업) */ companyType?: 'large' | 'medium' | 'small'; - /** 연도 (기본: 현재 연도) */ year?: number; - /** 분기 번호 (1-4, 기본: 현재 분기) */ quarter?: number; } export function useEntertainment(options: UseEntertainmentOptions = {}) { const { limitType = 'quarterly', companyType = 'medium', year, quarter } = options; - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); + const endpoint = useMemo( + () => + buildEndpoint('entertainment/summary', { + limit_type: limitType, + company_type: companyType, + year, + quarter, + }), + [limitType, companyType, year, quarter], + ); - // 쿼리 파라미터 구성 - const params = new URLSearchParams(); - params.append('limit_type', limitType); - params.append('company_type', companyType); - if (year) params.append('year', year.toString()); - if (quarter) params.append('quarter', quarter.toString()); - - const queryString = params.toString(); - const endpoint = `entertainment/summary?${queryString}`; - - const apiData = await fetchApi(endpoint); - const transformed = transformEntertainmentResponse(apiData); - setData(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('Entertainment API Error:', err); - } finally { - setLoading(false); - } - }, [limitType, companyType, year, quarter]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { data, loading, error, refetch: fetchData }; + return useDashboardFetch( + endpoint, + transformEntertainmentResponse, + ); } // ============================================ @@ -551,17 +308,11 @@ export function useEntertainment(options: UseEntertainmentOptions = {}) { // ============================================ export interface UseWelfareOptions { - /** 기간 타입 (annual: 연간, quarterly: 분기) */ limitType?: 'annual' | 'quarterly'; - /** 계산 방식 (fixed: 1인당 정액, ratio: 급여 대비 비율) */ calculationType?: 'fixed' | 'ratio'; - /** 1인당 월 정액 (calculation_type=fixed일 때 사용, 기본: 200000) */ fixedAmountPerMonth?: number; - /** 급여 대비 비율 (calculation_type=ratio일 때 사용, 기본: 0.05) */ ratio?: number; - /** 연도 (기본: 현재 연도) */ year?: number; - /** 분기 번호 (1-4, 기본: 현재 분기) */ quarter?: number; } @@ -574,45 +325,24 @@ export function useWelfare(options: UseWelfareOptions = {}) { year, quarter, } = options; - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); + const endpoint = useMemo( + () => + buildEndpoint('welfare/summary', { + limit_type: limitType, + calculation_type: calculationType, + fixed_amount_per_month: fixedAmountPerMonth, + ratio, + year, + quarter, + }), + [limitType, calculationType, fixedAmountPerMonth, ratio, year, quarter], + ); - // 쿼리 파라미터 구성 - const params = new URLSearchParams(); - params.append('limit_type', limitType); - params.append('calculation_type', calculationType); - if (fixedAmountPerMonth) params.append('fixed_amount_per_month', fixedAmountPerMonth.toString()); - if (ratio) params.append('ratio', ratio.toString()); - if (year) params.append('year', year.toString()); - if (quarter) params.append('quarter', quarter.toString()); - - const queryString = params.toString(); - const endpoint = `welfare/summary?${queryString}`; - - const apiData = await fetchApi(endpoint); - const transformed = transformWelfareResponse(apiData); - setData(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('Welfare API Error:', err); - } finally { - setLoading(false); - } - }, [limitType, calculationType, fixedAmountPerMonth, ratio, year, quarter]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { data, loading, error, refetch: fetchData }; + return useDashboardFetch( + endpoint, + transformWelfareResponse, + ); } // ============================================ @@ -620,22 +350,13 @@ export function useWelfare(options: UseWelfareOptions = {}) { // ============================================ export interface UseWelfareDetailOptions { - /** 계산 방식 (fixed: 1인당 정액, ratio: 급여 대비 비율) */ calculationType?: 'fixed' | 'ratio'; - /** 1인당 월 정액 (calculation_type=fixed일 때 사용, 기본: 200000) */ fixedAmountPerMonth?: number; - /** 급여 대비 비율 (calculation_type=ratio일 때 사용, 기본: 0.05) */ ratio?: number; - /** 연도 (기본: 현재 연도) */ year?: number; - /** 분기 번호 (1-4, 기본: 현재 분기) */ quarter?: number; } -/** - * 복리후생비 상세 데이터 Hook (모달용) - * API에서 상세 데이터를 가져와 DetailModalConfig로 변환 - */ export function useWelfareDetail(options: UseWelfareDetailOptions = {}) { const { calculationType = 'fixed', @@ -644,224 +365,39 @@ export function useWelfareDetail(options: UseWelfareDetailOptions = {}) { year, quarter, } = options; - const [modalConfig, setModalConfig] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); + const endpoint = useMemo( + () => + buildEndpoint('welfare/detail', { + calculation_type: calculationType, + fixed_amount_per_month: fixedAmountPerMonth, + ratio, + year, + quarter, + }), + [calculationType, fixedAmountPerMonth, ratio, year, quarter], + ); - // 쿼리 파라미터 구성 - const params = new URLSearchParams(); - params.append('calculation_type', calculationType); - if (fixedAmountPerMonth) params.append('fixed_amount_per_month', fixedAmountPerMonth.toString()); - if (ratio) params.append('ratio', ratio.toString()); - if (year) params.append('year', year.toString()); - if (quarter) params.append('quarter', quarter.toString()); + const result = useDashboardFetch( + endpoint, + transformWelfareDetailResponse, + { lazy: true }, + ); - const queryString = params.toString(); - const endpoint = `welfare/detail?${queryString}`; - - const apiData = await fetchApi(endpoint); - const transformed = transformWelfareDetailResponse(apiData); - setModalConfig(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('WelfareDetail API Error:', err); - } finally { - setLoading(false); - } - }, [calculationType, fixedAmountPerMonth, ratio, year, quarter]); - - return { modalConfig, loading, error, refetch: fetchData }; + return { + modalConfig: result.data, + loading: result.loading, + error: result.error, + refetch: result.refetch, + }; } // ============================================ -// 13. PurchaseDetail Hook (매입 상세 - me1 모달용) -// ============================================ - -/** - * 매입 상세 데이터 Hook (me1 모달용) - * API에서 상세 데이터를 가져와 DetailModalConfig로 변환 - */ -export function usePurchaseDetail() { - const [modalConfig, setModalConfig] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const response = await fetch('/api/v1/purchases/dashboard-detail'); - if (!response.ok) { - throw new Error(`API 오류: ${response.status}`); - } - const result = await response.json(); - if (!result.success) { - throw new Error(result.message || '데이터 조회 실패'); - } - - const transformed = transformPurchaseDetailResponse(result.data); - setModalConfig(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('PurchaseDetail API Error:', err); - } finally { - setLoading(false); - } - }, []); - - return { modalConfig, loading, error, refetch: fetchData }; -} - -// ============================================ -// 14. CardDetail Hook (카드 상세 - me2 모달용) -// ============================================ - -/** - * 카드 상세 데이터 Hook (me2 모달용) - * API에서 상세 데이터를 가져와 DetailModalConfig로 변환 - */ -export function useCardDetail() { - const [modalConfig, setModalConfig] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const response = await fetch('/api/v1/card-transactions/dashboard'); - if (!response.ok) { - throw new Error(`API 오류: ${response.status}`); - } - const result = await response.json(); - if (!result.success) { - throw new Error(result.message || '데이터 조회 실패'); - } - - const transformed = transformCardDetailResponse(result.data); - setModalConfig(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('CardDetail API Error:', err); - } finally { - setLoading(false); - } - }, []); - - return { modalConfig, loading, error, refetch: fetchData }; -} - -// ============================================ -// 15. BillDetail Hook (발행어음 상세 - me3 모달용) -// ============================================ - -/** - * 발행어음 상세 데이터 Hook (me3 모달용) - * API에서 상세 데이터를 가져와 DetailModalConfig로 변환 - */ -export function useBillDetail() { - const [modalConfig, setModalConfig] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const response = await fetch('/api/v1/bills/dashboard-detail'); - if (!response.ok) { - throw new Error(`API 오류: ${response.status}`); - } - const result = await response.json(); - if (!result.success) { - throw new Error(result.message || '데이터 조회 실패'); - } - - const transformed = transformBillDetailResponse(result.data); - setModalConfig(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('BillDetail API Error:', err); - } finally { - setLoading(false); - } - }, []); - - return { modalConfig, loading, error, refetch: fetchData }; -} - -// ============================================ -// 16. ExpectedExpenseDetail Hook (지출예상 상세 - me4 모달용) -// ============================================ - -/** - * 지출예상 상세 데이터 Hook (me4 모달용) - * API에서 상세 데이터를 가져와 DetailModalConfig로 변환 - */ -export function useExpectedExpenseDetail() { - const [modalConfig, setModalConfig] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const response = await fetch('/api/v1/expected-expenses/dashboard-detail'); - if (!response.ok) { - throw new Error(`API 오류: ${response.status}`); - } - const result = await response.json(); - if (!result.success) { - throw new Error(result.message || '데이터 조회 실패'); - } - - const transformed = transformExpectedExpenseDetailResponse(result.data); - setModalConfig(transformed); - - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; - setError(errorMessage); - console.error('ExpectedExpenseDetail API Error:', err); - } finally { - setLoading(false); - } - }, []); - - return { modalConfig, loading, error, refetch: fetchData }; -} - -// ============================================ -// 17. MonthlyExpenseDetail Hook (당월 예상 지출 상세 - 통합 모달용) +// 13. MonthlyExpenseDetail Hook (당월 예상 지출 상세 - 통합 모달용) // ============================================ export type MonthlyExpenseCardId = 'me1' | 'me2' | 'me3' | 'me4'; -/** - * 당월 예상 지출 상세 데이터 Hook (통합 모달용) - * cardId에 따라 다른 API를 호출하고 DetailModalConfig로 변환 - * - * @example - * const { modalConfig, loading, error, fetchData } = useMonthlyExpenseDetail(); - * await fetchData('me1'); // 매입 상세 API 호출 - */ export function useMonthlyExpenseDetail() { const [modalConfig, setModalConfig] = useState(null); const [loading, setLoading] = useState(false); @@ -872,48 +408,28 @@ export function useMonthlyExpenseDetail() { setLoading(true); setError(null); - // 모든 카드가 expected-expenses API를 사용하여 데이터 일관성 보장 - // transaction_type: me1=purchase, me2=card, me3=bill, me4=전체 - let endpoint: string; - let transactionType: string | null = null; + const transactionTypeMap: Record = { + me1: 'purchase', + me2: 'card', + me3: 'bill', + me4: null, + }; + const transactionType = transactionTypeMap[cardId]; - switch (cardId) { - case 'me1': - transactionType = 'purchase'; - break; - case 'me2': - transactionType = 'card'; - break; - case 'me3': - transactionType = 'bill'; - break; - case 'me4': - transactionType = null; // 전체 조회 - break; - default: - throw new Error(`Unknown cardId: ${cardId}`); - } - - // 단일 API 엔드포인트 사용 (transaction_type으로 필터링) - endpoint = transactionType + const endpoint = transactionType ? `/api/proxy/expected-expenses/dashboard-detail?transaction_type=${transactionType}` : '/api/proxy/expected-expenses/dashboard-detail'; - const transformer = (data: unknown) => - transformExpectedExpenseDetailResponse(data as ExpectedExpenseDashboardDetailApiResponse, cardId); - const response = await fetch(endpoint); - if (!response.ok) { - throw new Error(`API 오류: ${response.status}`); - } + if (!response.ok) throw new Error(`API 오류: ${response.status}`); const result = await response.json(); - if (!result.success) { - throw new Error(result.message || '데이터 조회 실패'); - } + if (!result.success) throw new Error(result.message || '데이터 조회 실패'); - const transformed = transformer(result.data); + const transformed = transformExpectedExpenseDetailResponse( + result.data as ExpectedExpenseDashboardDetailApiResponse, + cardId, + ); setModalConfig(transformed); - return transformed; } catch (err) { const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; @@ -929,64 +445,29 @@ export function useMonthlyExpenseDetail() { } // ============================================ -// 통합 Dashboard Hook (선택적 사용) +// 통합 Dashboard Hook // ============================================ export interface UseCEODashboardOptions { - /** DailyReport 섹션 활성화 */ dailyReport?: boolean; - /** Receivable 섹션 활성화 */ receivable?: boolean; - /** DebtCollection 섹션 활성화 */ debtCollection?: boolean; - /** MonthlyExpense 섹션 활성화 */ monthlyExpense?: boolean; - /** CardManagement 섹션 활성화 */ cardManagement?: boolean; - /** CardManagement fallback 데이터 (가지급금, 법인세, 종합세 등) */ cardManagementFallback?: CardManagementData; - /** StatusBoard 섹션 활성화 */ statusBoard?: boolean; } export interface CEODashboardState { - dailyReport: { - data: DailyReportData | null; - loading: boolean; - error: string | null; - }; - receivable: { - data: ReceivableData | null; - loading: boolean; - error: string | null; - }; - debtCollection: { - data: DebtCollectionData | null; - loading: boolean; - error: string | null; - }; - monthlyExpense: { - data: MonthlyExpenseData | null; - loading: boolean; - error: string | null; - }; - cardManagement: { - data: CardManagementData | null; - loading: boolean; - error: string | null; - }; - statusBoard: { - data: TodayIssueItem[] | null; - loading: boolean; - error: string | null; - }; + dailyReport: { data: DailyReportData | null; loading: boolean; error: string | null }; + receivable: { data: ReceivableData | null; loading: boolean; error: string | null }; + debtCollection: { data: DebtCollectionData | null; loading: boolean; error: string | null }; + monthlyExpense: { data: MonthlyExpenseData | null; loading: boolean; error: string | null }; + cardManagement: { data: CardManagementData | null; loading: boolean; error: string | null }; + statusBoard: { data: TodayIssueItem[] | null; loading: boolean; error: string | null }; refetchAll: () => void; } -/** - * 통합 CEO Dashboard Hook - * 여러 섹션의 API를 병렬로 호출하여 성능 최적화 - */ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashboardState { const { dailyReport: enableDailyReport = true, @@ -998,175 +479,74 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo statusBoard: enableStatusBoard = true, } = options; - // 각 섹션별 상태 - const [dailyReportData, setDailyReportData] = useState(null); - const [dailyReportLoading, setDailyReportLoading] = useState(enableDailyReport); - const [dailyReportError, setDailyReportError] = useState(null); + // 비활성 섹션은 endpoint를 null로 → useDashboardFetch가 skip + const dr = useDashboardFetch( + enableDailyReport ? 'daily-report/summary' : null, + transformDailyReportResponse, + { initialLoading: enableDailyReport }, + ); + const rv = useDashboardFetch( + enableReceivable ? 'receivables/summary' : null, + transformReceivableResponse, + { initialLoading: enableReceivable }, + ); + const dc = useDashboardFetch( + enableDebtCollection ? 'bad-debts/summary' : null, + transformDebtCollectionResponse, + { initialLoading: enableDebtCollection }, + ); + const me = useDashboardFetch( + enableMonthlyExpense ? 'expected-expenses/summary' : null, + transformMonthlyExpenseResponse, + { initialLoading: enableMonthlyExpense }, + ); + const sb = useDashboardFetch( + enableStatusBoard ? 'status-board/summary' : null, + transformStatusBoardResponse, + { initialLoading: enableStatusBoard }, + ); - const [receivableData, setReceivableData] = useState(null); - const [receivableLoading, setReceivableLoading] = useState(enableReceivable); - const [receivableError, setReceivableError] = useState(null); + // CardManagement: 커스텀 (3개 API 병렬) + const [cmData, setCmData] = useState(null); + const [cmLoading, setCmLoading] = useState(enableCardManagement); + const [cmError, setCmError] = useState(null); - const [debtCollectionData, setDebtCollectionData] = useState(null); - const [debtCollectionLoading, setDebtCollectionLoading] = useState(enableDebtCollection); - const [debtCollectionError, setDebtCollectionError] = useState(null); - - const [monthlyExpenseData, setMonthlyExpenseData] = useState(null); - const [monthlyExpenseLoading, setMonthlyExpenseLoading] = useState(enableMonthlyExpense); - const [monthlyExpenseError, setMonthlyExpenseError] = useState(null); - - const [cardManagementData, setCardManagementData] = useState(null); - const [cardManagementLoading, setCardManagementLoading] = useState(enableCardManagement); - const [cardManagementError, setCardManagementError] = useState(null); - - const [statusBoardData, setStatusBoardData] = useState(null); - const [statusBoardLoading, setStatusBoardLoading] = useState(enableStatusBoard); - const [statusBoardError, setStatusBoardError] = useState(null); - - // 개별 fetch 함수들 - const fetchDailyReport = useCallback(async () => { - if (!enableDailyReport) return; - try { - setDailyReportLoading(true); - setDailyReportError(null); - const apiData = await fetchApi('daily-report/summary'); - setDailyReportData(transformDailyReportResponse(apiData)); - } catch (err) { - setDailyReportError(err instanceof Error ? err.message : '데이터 로딩 실패'); - } finally { - setDailyReportLoading(false); - } - }, [enableDailyReport]); - - const fetchReceivable = useCallback(async () => { - if (!enableReceivable) return; - try { - setReceivableLoading(true); - setReceivableError(null); - const apiData = await fetchApi('receivables/summary'); - setReceivableData(transformReceivableResponse(apiData)); - } catch (err) { - setReceivableError(err instanceof Error ? err.message : '데이터 로딩 실패'); - } finally { - setReceivableLoading(false); - } - }, [enableReceivable]); - - const fetchDebtCollection = useCallback(async () => { - if (!enableDebtCollection) return; - try { - setDebtCollectionLoading(true); - setDebtCollectionError(null); - const apiData = await fetchApi('bad-debts/summary'); - setDebtCollectionData(transformDebtCollectionResponse(apiData)); - } catch (err) { - setDebtCollectionError(err instanceof Error ? err.message : '데이터 로딩 실패'); - } finally { - setDebtCollectionLoading(false); - } - }, [enableDebtCollection]); - - const fetchMonthlyExpense = useCallback(async () => { - if (!enableMonthlyExpense) return; - try { - setMonthlyExpenseLoading(true); - setMonthlyExpenseError(null); - const apiData = await fetchApi('expected-expenses/summary'); - setMonthlyExpenseData(transformMonthlyExpenseResponse(apiData)); - } catch (err) { - setMonthlyExpenseError(err instanceof Error ? err.message : '데이터 로딩 실패'); - } finally { - setMonthlyExpenseLoading(false); - } - }, [enableMonthlyExpense]); - - const fetchCardManagement = useCallback(async () => { + const fetchCM = useCallback(async () => { if (!enableCardManagement) return; try { - setCardManagementLoading(true); - setCardManagementError(null); - - // 3개 API 병렬 호출: 카드거래, 가지급금, 세금 시뮬레이션 - const [cardApiData, loanResponse, taxResponse] = await Promise.all([ - fetchApi('card-transactions/summary'), - fetchLoanDashboard(), - fetchTaxSimulation(), - ]); - - // LoanDashboard와 TaxSimulation은 ApiResponse wrapper가 있으므로 data 추출 - const loanData = loanResponse.success ? loanResponse.data : null; - const taxData = taxResponse.success ? taxResponse.data : null; - - setCardManagementData( - transformCardManagementResponse(cardApiData, loanData, taxData, cardManagementFallback) - ); + setCmLoading(true); + setCmError(null); + const result = await fetchCardManagementData(cardManagementFallback); + setCmData(result); } catch (err) { - setCardManagementError(err instanceof Error ? err.message : '데이터 로딩 실패'); + setCmError(err instanceof Error ? err.message : '데이터 로딩 실패'); + console.error('CardManagement API Error:', err); } finally { - setCardManagementLoading(false); + setCmLoading(false); } }, [enableCardManagement, cardManagementFallback]); - const fetchStatusBoard = useCallback(async () => { - if (!enableStatusBoard) return; - try { - setStatusBoardLoading(true); - setStatusBoardError(null); - const apiData = await fetchApi('status-board/summary'); - setStatusBoardData(transformStatusBoardResponse(apiData)); - } catch (err) { - setStatusBoardError(err instanceof Error ? err.message : '데이터 로딩 실패'); - } finally { - setStatusBoardLoading(false); - } - }, [enableStatusBoard]); - - // 전체 refetch - const refetchAll = useCallback(() => { - fetchDailyReport(); - fetchReceivable(); - fetchDebtCollection(); - fetchMonthlyExpense(); - fetchCardManagement(); - fetchStatusBoard(); - }, [fetchDailyReport, fetchReceivable, fetchDebtCollection, fetchMonthlyExpense, fetchCardManagement, fetchStatusBoard]); - - // 초기 로드 useEffect(() => { - refetchAll(); - }, [refetchAll]); + fetchCM(); + }, [fetchCM]); + + const refetchAll = useCallback(() => { + dr.refetch(); + rv.refetch(); + dc.refetch(); + me.refetch(); + fetchCM(); + sb.refetch(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dr.refetch, rv.refetch, dc.refetch, me.refetch, fetchCM, sb.refetch]); return { - dailyReport: { - data: dailyReportData, - loading: dailyReportLoading, - error: dailyReportError, - }, - receivable: { - data: receivableData, - loading: receivableLoading, - error: receivableError, - }, - debtCollection: { - data: debtCollectionData, - loading: debtCollectionLoading, - error: debtCollectionError, - }, - monthlyExpense: { - data: monthlyExpenseData, - loading: monthlyExpenseLoading, - error: monthlyExpenseError, - }, - cardManagement: { - data: cardManagementData, - loading: cardManagementLoading, - error: cardManagementError, - }, - statusBoard: { - data: statusBoardData, - loading: statusBoardLoading, - error: statusBoardError, - }, + dailyReport: { data: dr.data, loading: dr.loading, error: dr.error }, + receivable: { data: rv.data, loading: rv.loading, error: rv.error }, + debtCollection: { data: dc.data, loading: dc.loading, error: dc.error }, + monthlyExpense: { data: me.data, loading: me.loading, error: me.error }, + cardManagement: { data: cmData, loading: cmLoading, error: cmError }, + statusBoard: { data: sb.data, loading: sb.loading, error: sb.error }, refetchAll, }; } \ No newline at end of file diff --git a/src/hooks/useDashboardFetch.ts b/src/hooks/useDashboardFetch.ts new file mode 100644 index 00000000..22b71b58 --- /dev/null +++ b/src/hooks/useDashboardFetch.ts @@ -0,0 +1,156 @@ +import { useState, useCallback, useEffect } from 'react'; + +/** + * CEO Dashboard API 호출을 위한 제네릭 훅 + * + * @param endpoint - API 엔드포인트 (예: 'daily-report/summary') + * @param transformer - API 응답을 프론트엔드 데이터로 변환하는 함수 + * @param options - 추가 옵션 + * + * @example + * // 자동 fetch (마운트 시 즉시 호출) + * const { data, loading, error, refetch } = useDashboardFetch( + * 'daily-report/summary', + * transformDailyReportResponse, + * ); + * + * // 수동 fetch (lazy: true → 마운트 시 호출하지 않음) + * const { data, loading, error, refetch } = useDashboardFetch( + * 'welfare/detail', + * transformWelfareDetailResponse, + * { lazy: true }, + * ); + * // 필요할 때 수동 호출 + * await refetch(); + */ +export function useDashboardFetch( + endpoint: string | null, + transformer: (data: TApi) => TResult, + options?: { + /** true이면 마운트 시 자동 호출하지 않음 */ + lazy?: boolean; + /** 초기 로딩 상태 (기본: !lazy) */ + initialLoading?: boolean; + }, +) { + const lazy = options?.lazy ?? false; + const [data, setData] = useState(null); + const [loading, setLoading] = useState(options?.initialLoading ?? !lazy); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + if (!endpoint) return; + try { + setLoading(true); + setError(null); + + const response = await fetch(`/api/proxy/${endpoint}`); + if (!response.ok) { + throw new Error(`API 오류: ${response.status}`); + } + const result = await response.json(); + if (!result.success) { + throw new Error(result.message || '데이터 조회 실패'); + } + + const transformed = transformer(result.data); + setData(transformed); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; + setError(errorMessage); + console.error(`Dashboard API Error [${endpoint}]:`, err); + } finally { + setLoading(false); + } + }, [endpoint, transformer]); + + useEffect(() => { + if (!lazy && endpoint) { + fetchData(); + } + }, [lazy, endpoint, fetchData]); + + return { data, loading, error, refetch: fetchData }; +} + +/** + * 여러 API를 병렬 호출하는 제네릭 훅 + * + * @example + * const { data, loading, error, refetch } = useDashboardMultiFetch( + * [ + * { endpoint: 'card-transactions/summary' }, + * { endpoint: 'loans/dashboard', fetchFn: fetchLoanDashboard }, + * ], + * ([cardData, loanData]) => transformCardManagementResponse(cardData, loanData), + * ); + */ +export function useDashboardMultiFetch( + sources: Array<{ + endpoint: string; + /** 커스텀 fetch 함수 (기본: fetchApi 패턴) */ + fetchFn?: () => Promise; + }>, + transformer: (results: unknown[]) => TResult, + options?: { + lazy?: boolean; + initialLoading?: boolean; + }, +) { + const lazy = options?.lazy ?? false; + const [data, setData] = useState(null); + const [loading, setLoading] = useState(options?.initialLoading ?? !lazy); + const [error, setError] = useState(null); + + // sources를 JSON으로 비교하여 안정적인 의존성 확보 + const sourcesKey = JSON.stringify(sources.map((s) => s.endpoint)); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const results = await Promise.all( + sources.map(async (source) => { + if (source.fetchFn) { + const result = await source.fetchFn(); + // fetchFn이 { success, data } 형태를 반환할 수 있음 + if (result && typeof result === 'object' && 'success' in result) { + const r = result as { success: boolean; data: unknown }; + return r.success ? r.data : null; + } + return result; + } + + const response = await fetch(`/api/proxy/${source.endpoint}`); + if (!response.ok) { + throw new Error(`API 오류: ${response.status}`); + } + const json = await response.json(); + if (!json.success) { + throw new Error(json.message || '데이터 조회 실패'); + } + return json.data; + }), + ); + + const transformed = transformer(results); + setData(transformed); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; + setError(errorMessage); + console.error('Dashboard MultiFetch Error:', err); + } finally { + setLoading(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sourcesKey, transformer]); + + useEffect(() => { + if (!lazy) { + fetchData(); + } + }, [lazy, fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/src/lib/api/dashboard/transformers/expense-detail.ts b/src/lib/api/dashboard/transformers/expense-detail.ts index c9c33afd..d32e9c56 100644 --- a/src/lib/api/dashboard/transformers/expense-detail.ts +++ b/src/lib/api/dashboard/transformers/expense-detail.ts @@ -74,16 +74,6 @@ export function transformPurchaseDetailResponse(api: PurchaseDashboardDetailApiR ], defaultValue: 'all', }, - { - key: 'sortOrder', - options: [ - { value: 'latest', label: '최신순' }, - { value: 'oldest', label: '등록순' }, - { value: 'amountDesc', label: '금액 높은순' }, - { value: 'amountAsc', label: '금액 낮은순' }, - ], - defaultValue: 'latest', - }, ], showTotal: true, totalLabel: '합계', @@ -165,16 +155,6 @@ export function transformCardDetailResponse(api: CardDashboardDetailApiResponse) ], defaultValue: 'all', }, - { - key: 'sortOrder', - options: [ - { value: 'latest', label: '최신순' }, - { value: 'oldest', label: '등록순' }, - { value: 'amountDesc', label: '금액 높은순' }, - { value: 'amountAsc', label: '금액 낮은순' }, - ], - defaultValue: 'latest', - }, ], showTotal: true, totalLabel: '합계', @@ -264,16 +244,6 @@ export function transformBillDetailResponse(api: BillDashboardDetailApiResponse) ], defaultValue: 'all', }, - { - key: 'sortOrder', - options: [ - { value: 'latest', label: '최신순' }, - { value: 'oldest', label: '등록순' }, - { value: 'amountDesc', label: '금액 높은순' }, - { value: 'amountAsc', label: '금액 낮은순' }, - ], - defaultValue: 'latest', - }, ], showTotal: true, totalLabel: '합계', @@ -398,16 +368,6 @@ export function transformExpectedExpenseDetailResponse( options: vendorOptions, defaultValue: 'all', }, - { - key: 'sortOrder', - options: [ - { value: 'latest', label: '최신순' }, - { value: 'oldest', label: '등록순' }, - { value: 'amountDesc', label: '금액 높은순' }, - { value: 'amountAsc', label: '금액 낮은순' }, - ], - defaultValue: 'latest', - }, ], showTotal: true, totalLabel: '합계', diff --git a/src/lib/api/dashboard/transformers/tax-benefits.ts b/src/lib/api/dashboard/transformers/tax-benefits.ts index cbe76614..85e444ae 100644 --- a/src/lib/api/dashboard/transformers/tax-benefits.ts +++ b/src/lib/api/dashboard/transformers/tax-benefits.ts @@ -161,6 +161,11 @@ export function transformWelfareDetailResponse(api: WelfareDetailApiResponse): D return { title: '복리후생비 상세', + dateFilter: { + enabled: true, + defaultPreset: '당월', + showSearch: true, + }, summaryCards: [ // 1행: 당해년도 기준 { label: '당해년도 복리후생비 계정', value: summary.annual_account, unit: '원' }, @@ -224,16 +229,6 @@ export function transformWelfareDetailResponse(api: WelfareDetailApiResponse): D ], defaultValue: 'all', }, - { - key: 'sortOrder', - options: [ - { value: 'latest', label: '최신순' }, - { value: 'oldest', label: '등록순' }, - { value: 'amountDesc', label: '금액 높은순' }, - { value: 'amountAsc', label: '금액 낮은순' }, - ], - defaultValue: 'latest', - }, ], showTotal: true, totalLabel: '합계', diff --git a/src/lib/utils/amount.ts b/src/lib/utils/amount.ts index 8ff070f0..5bd2d3bb 100644 --- a/src/lib/utils/amount.ts +++ b/src/lib/utils/amount.ts @@ -94,6 +94,19 @@ export function formatAmountManwon(amount: number): string { return `${manwon.toLocaleString("ko-KR")}만원`; } +/** + * 차트 축 레이블용 축약 포맷 + * + * - 1억 이상: "1.5억" + * - 1만 이상: "5320만" + * - 1만 미만: "5,000" + */ +export function formatCompactAmount(value: number): string { + if (value >= 100000000) return `${(value / 100000000).toFixed(1)}억`; + if (value >= 10000) return `${(value / 10000).toFixed(0)}만`; + return value.toLocaleString(); +} + /** * 한국식 금액 축약 포맷 * diff --git a/src/lib/utils/status-config.ts b/src/lib/utils/status-config.ts index b92e09ae..460133a6 100644 --- a/src/lib/utils/status-config.ts +++ b/src/lib/utils/status-config.ts @@ -324,8 +324,7 @@ export const RECEIVING_STATUS_CONFIG = createStatusConfig({ export const BAD_DEBT_COLLECTION_STATUS_CONFIG = createStatusConfig({ collecting: { label: '추심중', style: 'border-orange-300 text-orange-600 bg-orange-50' }, legalAction: { label: '법적조치', style: 'border-red-300 text-red-600 bg-red-50' }, - recovered: { label: '회수완료', style: 'border-green-300 text-green-600 bg-green-50' }, - badDebt: { label: '대손처리', style: 'border-gray-300 text-gray-600 bg-gray-50' }, + collectionEnd: { label: '추심종료', style: 'border-green-300 text-green-600 bg-green-50' }, }, { includeAll: true }); /**