From d824c913e8a816ea4fb1601143cffd78aaa7aaee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Thu, 22 Jan 2026 23:16:56 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=8B=B9=EC=9B=94=20=EC=98=88=EC=83=81?= =?UTF-8?q?=20=EC=A7=80=EC=B6=9C=20=EB=AA=A8=EB=8B=AC=20API=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모든 카드(me1~me4)가 expected-expenses/dashboard-detail API 사용 - transaction_type 파라미터로 필터링 (me1=purchase, me2=card, me3=bill) - cardId별 모달 제목 동적 설정 추가 - 불필요한 import 정리 --- src/hooks/useCEODashboard.ts | 250 ++++++++++++++++++ src/lib/api/dashboard/transformers.ts | 361 ++++++++++++++++++++++++++ 2 files changed, 611 insertions(+) diff --git a/src/hooks/useCEODashboard.ts b/src/hooks/useCEODashboard.ts index 77d6c8a4..d5ede586 100644 --- a/src/hooks/useCEODashboard.ts +++ b/src/hooks/useCEODashboard.ts @@ -22,6 +22,7 @@ import type { EntertainmentApiResponse, WelfareApiResponse, WelfareDetailApiResponse, + ExpectedExpenseDashboardDetailApiResponse, } from '@/lib/api/dashboard/types'; import { @@ -37,6 +38,7 @@ import { transformEntertainmentResponse, transformWelfareResponse, transformWelfareDetailResponse, + transformExpectedExpenseDetailResponse, } from '@/lib/api/dashboard/transformers'; import type { @@ -621,6 +623,254 @@ export function useWelfareDetail(options: UseWelfareDetailOptions = {}) { return { modalConfig, loading, error, refetch: fetchData }; } +// ============================================ +// 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 (당월 예상 지출 상세 - 통합 모달용) +// ============================================ + +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); + const [error, setError] = useState(null); + + const fetchData = useCallback(async (cardId: MonthlyExpenseCardId) => { + try { + setLoading(true); + setError(null); + + // 모든 카드가 expected-expenses API를 사용하여 데이터 일관성 보장 + // transaction_type: me1=purchase, me2=card, me3=bill, me4=전체 + let endpoint: string; + let transactionType: string | null = null; + + 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 + ? `/api/v1/expected-expenses/dashboard-detail?transaction_type=${transactionType}` + : '/api/v1/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}`); + } + const result = await response.json(); + if (!result.success) { + throw new Error(result.message || '데이터 조회 실패'); + } + + const transformed = transformer(result.data); + setModalConfig(transformed); + + return transformed; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; + setError(errorMessage); + console.error('MonthlyExpenseDetail API Error:', err); + return null; + } finally { + setLoading(false); + } + }, []); + + return { modalConfig, loading, error, fetchData }; +} + // ============================================ // 통합 Dashboard Hook (선택적 사용) // ============================================ diff --git a/src/lib/api/dashboard/transformers.ts b/src/lib/api/dashboard/transformers.ts index e7fb91e4..fb2b5acd 100644 --- a/src/lib/api/dashboard/transformers.ts +++ b/src/lib/api/dashboard/transformers.ts @@ -891,4 +891,365 @@ export function transformWelfareDetailResponse(api: WelfareDetailApiResponse): D ], }, }; +} + +// ============================================ +// 13. Purchase Dashboard Detail 변환 (me1) +// ============================================ + +/** + * Purchase Dashboard Detail API 응답 → DetailModalConfig 변환 + * 매입 상세 모달 설정 생성 (me1) + */ +export function transformPurchaseDetailResponse(api: PurchaseDashboardDetailApiResponse): DetailModalConfig { + const { summary, monthly_trend, by_type, items } = api; + const changeRateText = summary.change_rate >= 0 + ? `+${summary.change_rate.toFixed(1)}%` + : `${summary.change_rate.toFixed(1)}%`; + + return { + title: '매입 상세', + summaryCards: [ + { label: '당월 매입액', value: summary.current_month_amount, unit: '원' }, + { label: '전월 대비', value: changeRateText }, + ], + barChart: { + title: '월별 매입 추이', + data: monthly_trend.map(item => ({ + name: item.label, + value: item.amount, + })), + dataKey: 'value', + xAxisKey: 'name', + color: '#60A5FA', + }, + pieChart: { + title: '유형별 매입 비율', + data: by_type.map(item => ({ + name: item.type_label, + value: item.amount, + percentage: 0, // API에서 계산하거나 프론트에서 계산 + color: item.color, + })), + }, + table: { + title: '매입 내역', + columns: [ + { key: 'no', label: 'No.', align: 'center' }, + { key: 'date', label: '매입일자', align: 'center', format: 'date' }, + { key: 'vendor', label: '거래처명', align: 'left' }, + { key: 'amount', label: '금액', align: 'right', format: 'currency' }, + { key: 'type', label: '유형', align: 'center' }, + ], + data: items.map((item, idx) => ({ + no: idx + 1, + date: item.purchase_date, + vendor: item.vendor_name, + amount: item.amount, + type: item.type_label, + })), + filters: [ + { + key: 'type', + options: [ + { value: 'all', label: '전체' }, + ...by_type.map(t => ({ value: t.type, label: t.type_label })), + ], + defaultValue: 'all', + }, + { + key: 'sortOrder', + options: [ + { value: 'latest', label: '최신순' }, + { value: 'oldest', label: '등록순' }, + { value: 'amountDesc', label: '금액 높은순' }, + { value: 'amountAsc', label: '금액 낮은순' }, + ], + defaultValue: 'latest', + }, + ], + showTotal: true, + totalLabel: '합계', + totalValue: items.reduce((sum, item) => sum + item.amount, 0), + totalColumnKey: 'amount', + }, + }; +} + +// ============================================ +// 14. Card Dashboard Detail 변환 (me2) +// ============================================ + +/** + * Card Dashboard Detail API 응답 → DetailModalConfig 변환 + * 카드 상세 모달 설정 생성 (me2) + */ +export function transformCardDetailResponse(api: CardDashboardDetailApiResponse): DetailModalConfig { + const { summary, monthly_trend, by_user, items } = api; + const changeRate = summary.previous_month_total > 0 + ? ((summary.current_month_total - summary.previous_month_total) / summary.previous_month_total * 100) + : 0; + const changeRateText = changeRate >= 0 + ? `+${changeRate.toFixed(1)}%` + : `${changeRate.toFixed(1)}%`; + + return { + title: '카드 사용 상세', + summaryCards: [ + { label: '당월 사용액', value: summary.current_month_total, unit: '원' }, + { label: '전월 대비', value: changeRateText }, + { label: '당월 건수', value: summary.current_month_count, unit: '건' }, + ], + barChart: { + title: '월별 카드 사용 추이', + data: monthly_trend.map(item => ({ + name: item.label, + value: item.amount, + })), + dataKey: 'value', + xAxisKey: 'name', + color: '#34D399', + }, + pieChart: { + title: '사용자별 사용 비율', + data: by_user.map(item => ({ + name: item.user_name, + value: item.amount, + percentage: 0, + color: item.color, + })), + }, + table: { + title: '카드 사용 내역', + columns: [ + { key: 'no', label: 'No.', align: 'center' }, + { key: 'cardName', label: '카드명', align: 'left' }, + { key: 'user', label: '사용자', align: 'center' }, + { key: 'date', label: '사용일자', align: 'center', format: 'date' }, + { key: 'store', label: '가맹점명', align: 'left' }, + { key: 'amount', label: '사용금액', align: 'right', format: 'currency' }, + { key: 'usageType', label: '사용항목', align: 'center' }, + ], + data: items.map((item, idx) => ({ + no: idx + 1, + cardName: item.card_name, + user: item.user_name, + date: item.transaction_date, + store: item.merchant_name, + amount: item.amount, + usageType: item.usage_type, + })), + filters: [ + { + key: 'user', + options: [ + { value: 'all', label: '전체' }, + ...by_user.map(u => ({ value: u.user_name, label: u.user_name })), + ], + defaultValue: 'all', + }, + { + key: 'sortOrder', + options: [ + { value: 'latest', label: '최신순' }, + { value: 'oldest', label: '등록순' }, + { value: 'amountDesc', label: '금액 높은순' }, + { value: 'amountAsc', label: '금액 낮은순' }, + ], + defaultValue: 'latest', + }, + ], + showTotal: true, + totalLabel: '합계', + totalValue: items.reduce((sum, item) => sum + item.amount, 0), + totalColumnKey: 'amount', + }, + }; +} + +// ============================================ +// 15. Bill Dashboard Detail 변환 (me3) +// ============================================ + +/** + * Bill Dashboard Detail API 응답 → DetailModalConfig 변환 + * 발행어음 상세 모달 설정 생성 (me3) + */ +export function transformBillDetailResponse(api: BillDashboardDetailApiResponse): DetailModalConfig { + const { summary, monthly_trend, by_vendor, items } = api; + const changeRateText = summary.change_rate >= 0 + ? `+${summary.change_rate.toFixed(1)}%` + : `${summary.change_rate.toFixed(1)}%`; + + // 거래처별 가로 막대 차트 데이터 + const horizontalBarData = by_vendor.map(item => ({ + name: item.vendor_name, + value: item.amount, + })); + + return { + title: '발행어음 상세', + summaryCards: [ + { label: '당월 발행어음', value: summary.current_month_amount, unit: '원' }, + { label: '전월 대비', value: changeRateText }, + ], + barChart: { + title: '월별 발행어음 추이', + data: monthly_trend.map(item => ({ + name: item.label, + value: item.amount, + })), + dataKey: 'value', + xAxisKey: 'name', + color: '#F59E0B', + }, + horizontalBarChart: { + title: '거래처별 발행어음', + data: horizontalBarData, + dataKey: 'value', + yAxisKey: 'name', + color: '#8B5CF6', + }, + table: { + title: '발행어음 내역', + columns: [ + { key: 'no', label: 'No.', align: 'center' }, + { key: 'vendor', label: '거래처명', align: 'left' }, + { key: 'issueDate', label: '발행일', align: 'center', format: 'date' }, + { key: 'dueDate', label: '만기일', align: 'center', format: 'date' }, + { key: 'amount', label: '금액', align: 'right', format: 'currency' }, + { key: 'status', label: '상태', align: 'center' }, + ], + data: items.map((item, idx) => ({ + no: idx + 1, + vendor: item.vendor_name, + issueDate: item.issue_date, + dueDate: item.due_date, + amount: item.amount, + status: item.status_label, + })), + filters: [ + { + key: 'vendor', + options: [ + { value: 'all', label: '전체' }, + ...by_vendor.map(v => ({ value: v.vendor_name, label: v.vendor_name })), + ], + defaultValue: 'all', + }, + { + key: 'status', + options: [ + { value: 'all', label: '전체' }, + { value: 'pending', label: '대기중' }, + { value: 'paid', label: '결제완료' }, + { value: 'overdue', label: '연체' }, + ], + defaultValue: 'all', + }, + { + key: 'sortOrder', + options: [ + { value: 'latest', label: '최신순' }, + { value: 'oldest', label: '등록순' }, + { value: 'amountDesc', label: '금액 높은순' }, + { value: 'amountAsc', label: '금액 낮은순' }, + ], + defaultValue: 'latest', + }, + ], + showTotal: true, + totalLabel: '합계', + totalValue: items.reduce((sum, item) => sum + item.amount, 0), + totalColumnKey: 'amount', + }, + }; +} + +// ============================================ +// 16. Expected Expense Dashboard Detail 변환 (me1~me4 통합) +// ============================================ + +// cardId별 제목 매핑 +const EXPENSE_CARD_TITLES: Record = { + me1: { title: '당월 매입 상세', tableTitle: '매입 내역', summaryLabel: '당월 총 매입' }, + me2: { title: '당월 카드결제 상세', tableTitle: '카드결제 내역', summaryLabel: '당월 총 카드결제' }, + me3: { title: '당월 발행어음 상세', tableTitle: '발행어음 내역', summaryLabel: '당월 총 발행어음' }, + me4: { title: '지출예상 상세', tableTitle: '지출예상 내역', summaryLabel: '당월 총 지출예상' }, +}; + +/** + * ExpectedExpense Dashboard Detail API 응답 → DetailModalConfig 변환 + * 카드별 지출 상세 모달 설정 생성 (me1: 매입, me2: 카드, me3: 발행어음, me4: 전체) + * + * @param api API 응답 데이터 + * @param cardId 카드 ID (me1~me4), 기본값 me4 + */ +export function transformExpectedExpenseDetailResponse( + api: ExpectedExpenseDashboardDetailApiResponse, + cardId: string = 'me4' +): DetailModalConfig { + const { summary, items } = api; + const changeRateText = summary.change_rate >= 0 + ? `+${summary.change_rate.toFixed(1)}%` + : `${summary.change_rate.toFixed(1)}%`; + + // cardId별 제목 가져오기 (기본값: me4) + const titles = EXPENSE_CARD_TITLES[cardId] || EXPENSE_CARD_TITLES.me4; + + return { + title: titles.title, + summaryCards: [ + { label: titles.summaryLabel, value: summary.total_amount, unit: '원' }, + { label: '전월 대비', value: changeRateText }, + { label: '지출 후 예상 잔액', value: summary.remaining_balance, unit: '원' }, + ], + // 차트 없음 + table: { + title: titles.tableTitle, + columns: [ + { key: 'no', label: 'No.', align: 'center' }, + { key: 'paymentDate', label: '결제예정일', align: 'center', format: 'date' }, + { key: 'item', label: '항목', align: 'left' }, + { key: 'amount', label: '금액', align: 'right', format: 'currency' }, + { key: 'vendor', label: '거래처', align: 'left' }, + { key: 'account', label: '계정과목', align: 'center' }, + ], + data: items.map((item, idx) => ({ + no: idx + 1, + paymentDate: item.payment_date, + item: item.item_name, + amount: item.amount, + vendor: item.vendor_name, + account: item.account_title, + })), + filters: [ + { + key: 'vendor', + options: [ + { value: 'all', label: '전체' }, + ], + defaultValue: 'all', + }, + { + key: 'sortOrder', + options: [ + { value: 'latest', label: '최신순' }, + { value: 'oldest', label: '등록순' }, + { value: 'amountDesc', label: '금액 높은순' }, + { value: 'amountAsc', label: '금액 낮은순' }, + ], + defaultValue: 'latest', + }, + ], + showTotal: true, + totalLabel: '합계', + totalValue: footer_summary.total_amount, + totalColumnKey: 'amount', + footerSummary: { + label: `총 ${footer_summary.item_count}건`, + value: footer_summary.total_amount, + }, + }, + }; } \ No newline at end of file