From fe845227df97dd70361dc8a966aff05ae8450d4b 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:01:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=202=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=EC=97=94=EB=93=9C=20=ED=83=80=EC=9E=85=20=EB=B0=8F=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API 타입 정의 추가 (LoanDashboard, TaxSimulation) - API 엔드포인트 함수 추가 (endpoints.ts) - 모달 데이터 훅 생성 (useCardManagementModals.ts) 관련: docs/plans/card-management-section-plan.md --- src/hooks/useCardManagementModals.ts | 229 +++++++++++++++++++++++++ src/lib/api/dashboard/endpoints.ts | 242 +++++++++++++++++++++++++++ src/lib/api/dashboard/index.ts | 3 +- src/lib/api/dashboard/types.ts | 102 +++++++++++ 4 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useCardManagementModals.ts create mode 100644 src/lib/api/dashboard/endpoints.ts diff --git a/src/hooks/useCardManagementModals.ts b/src/hooks/useCardManagementModals.ts new file mode 100644 index 00000000..d2384e28 --- /dev/null +++ b/src/hooks/useCardManagementModals.ts @@ -0,0 +1,229 @@ +'use client'; + +/** + * Card Management Modals Hook + * + * CEO 대시보드 카드/가지급금 관리 섹션 (cm1-cm4) 모달 데이터 관리 + * + * cm1: 카드 사용액 상세 → CardTransactionDashboard API + * cm2: 가지급금 상세 → LoanDashboard API + * cm3: 법인세 예상 가중 → TaxSimulation API (corporate_tax) + * cm4: 대표자 종합세 예상 가중 → TaxSimulation API (income_tax) + */ + +import { useState, useCallback } from 'react'; +import { + fetchCardTransactionDashboard, + fetchLoanDashboard, + fetchTaxSimulation, +} from '@/lib/api/dashboard/endpoints'; +import type { + CardDashboardDetailApiResponse, + LoanDashboardApiResponse, + TaxSimulationApiResponse, +} from '@/lib/api/dashboard/types'; + +// ============================================ +// 타입 정의 +// ============================================ + +/** 카드 ID 타입 */ +export type CardManagementCardId = 'cm1' | 'cm2' | 'cm3' | 'cm4'; + +/** Hook 반환 타입 */ +export interface UseCardManagementModalsReturn { + /** cm1: 카드 사용액 상세 데이터 */ + cm1Data: CardDashboardDetailApiResponse | null; + /** cm2: 가지급금 상세 데이터 */ + cm2Data: LoanDashboardApiResponse | null; + /** cm3: 법인세 시뮬레이션 데이터 */ + cm3Data: TaxSimulationApiResponse | null; + /** cm4: 소득세 시뮬레이션 데이터 (cm3와 동일 소스, 다른 표시) */ + cm4Data: TaxSimulationApiResponse | null; + /** 로딩 상태 */ + loading: boolean; + /** 에러 메시지 */ + error: string | null; + /** 특정 카드의 모달 데이터 조회 */ + fetchModalData: (cardId: CardManagementCardId) => Promise; + /** 모든 카드 데이터 조회 */ + fetchAllData: () => Promise; + /** 데이터 초기화 */ + clearData: (cardId?: CardManagementCardId) => void; +} + +// ============================================ +// Hook 구현 +// ============================================ + +/** + * 카드/가지급금 관리 섹션 모달 데이터 관리 Hook + * + * @example + * const { cm1Data, cm2Data, loading, fetchModalData } = useCardManagementModals(); + * + * // 카드 클릭 시 + * await fetchModalData('cm1'); + */ +export function useCardManagementModals(): UseCardManagementModalsReturn { + // 각 카드별 데이터 상태 + const [cm1Data, setCm1Data] = useState(null); + const [cm2Data, setCm2Data] = useState(null); + const [cm3Data, setCm3Data] = useState(null); + const [cm4Data, setCm4Data] = useState(null); + + // 공통 상태 + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + /** + * cm1: 카드 사용액 상세 데이터 조회 + */ + const fetchCm1Data = useCallback(async () => { + try { + const response = await fetchCardTransactionDashboard(); + if (response.success) { + setCm1Data(response.data); + } else { + throw new Error(response.message || '카드 거래 데이터 조회 실패'); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : '카드 거래 데이터 조회 실패'; + console.error('[useCardManagementModals] cm1 error:', err); + throw new Error(errorMessage); + } + }, []); + + /** + * cm2: 가지급금 상세 데이터 조회 + */ + const fetchCm2Data = useCallback(async () => { + try { + const response = await fetchLoanDashboard(); + if (response.success) { + setCm2Data(response.data); + } else { + throw new Error(response.message || '가지급금 데이터 조회 실패'); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : '가지급금 데이터 조회 실패'; + console.error('[useCardManagementModals] cm2 error:', err); + throw new Error(errorMessage); + } + }, []); + + /** + * cm3 & cm4: 세금 시뮬레이션 데이터 조회 + * cm3은 법인세 (corporate_tax), cm4는 소득세 (income_tax) 사용 + */ + const fetchTaxData = useCallback(async () => { + try { + const response = await fetchTaxSimulation(); + if (response.success) { + // cm3, cm4 모두 같은 데이터 소스 사용 (표시만 다름) + setCm3Data(response.data); + setCm4Data(response.data); + } else { + throw new Error(response.message || '세금 시뮬레이션 데이터 조회 실패'); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : '세금 시뮬레이션 데이터 조회 실패'; + console.error('[useCardManagementModals] tax simulation error:', err); + throw new Error(errorMessage); + } + }, []); + + /** + * 특정 카드의 모달 데이터 조회 + */ + const fetchModalData = useCallback( + async (cardId: CardManagementCardId) => { + setLoading(true); + setError(null); + + try { + switch (cardId) { + case 'cm1': + await fetchCm1Data(); + break; + case 'cm2': + await fetchCm2Data(); + break; + case 'cm3': + case 'cm4': + // cm3, cm4는 같은 API 사용 + await fetchTaxData(); + break; + default: + throw new Error(`알 수 없는 카드 ID: ${cardId}`); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : '데이터 조회 실패'; + setError(errorMessage); + } finally { + setLoading(false); + } + }, + [fetchCm1Data, fetchCm2Data, fetchTaxData] + ); + + /** + * 모든 카드 데이터 조회 (초기 로드용) + */ + const fetchAllData = useCallback(async () => { + setLoading(true); + setError(null); + + try { + // 병렬로 모든 데이터 조회 + await Promise.all([fetchCm1Data(), fetchCm2Data(), fetchTaxData()]); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : '데이터 조회 실패'; + setError(errorMessage); + } finally { + setLoading(false); + } + }, [fetchCm1Data, fetchCm2Data, fetchTaxData]); + + /** + * 데이터 초기화 + */ + const clearData = useCallback((cardId?: CardManagementCardId) => { + if (!cardId) { + // 전체 초기화 + setCm1Data(null); + setCm2Data(null); + setCm3Data(null); + setCm4Data(null); + setError(null); + } else { + // 특정 카드만 초기화 + switch (cardId) { + case 'cm1': + setCm1Data(null); + break; + case 'cm2': + setCm2Data(null); + break; + case 'cm3': + setCm3Data(null); + break; + case 'cm4': + setCm4Data(null); + break; + } + } + }, []); + + return { + cm1Data, + cm2Data, + cm3Data, + cm4Data, + loading, + error, + fetchModalData, + fetchAllData, + clearData, + }; +} diff --git a/src/lib/api/dashboard/endpoints.ts b/src/lib/api/dashboard/endpoints.ts new file mode 100644 index 00000000..93d02b9b --- /dev/null +++ b/src/lib/api/dashboard/endpoints.ts @@ -0,0 +1,242 @@ +'use server'; + +/** + * CEO Dashboard API 엔드포인트 함수 + * + * 카드/가지급금 관리 섹션 (cm1-cm4) 데이터 조회 + */ + +import { apiClient } from '../index'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import type { + ApiResponse, + CardDashboardDetailApiResponse, + LoanDashboardApiResponse, + TaxSimulationApiResponse, +} from './types'; + +// ============================================ +// 에러 핸들링 헬퍼 +// ============================================ + +/** + * 인증 에러인지 확인 + */ +function isAuthenticationError(error: unknown): boolean { + if (error && typeof error === 'object') { + const err = error as { status?: number; code?: string; message?: string }; + if (err.status === 401) return true; + if (err.code === 'AUTH_ERROR') return true; + if (err.message?.includes('회원정보') || err.message?.includes('인증')) return true; + } + return false; +} + +// ============================================ +// 카드 거래 대시보드 API +// ============================================ + +/** + * 카드 거래 대시보드 데이터 조회 + * GET /api/v1/card-transactions/dashboard + * + * @returns 카드 거래 요약, 월별 추이, 사용자별 분포, 거래 목록 + */ +export async function fetchCardTransactionDashboard(): Promise> { + try { + const response = await apiClient.get>( + '/card-transactions/dashboard' + ); + return response; + } catch (error) { + // Next.js redirect 에러는 전파 + if (isNextRedirectError(error)) { + throw error; + } + + console.error('[Dashboard] fetchCardTransactionDashboard error:', error); + + // 인증 에러인 경우 + if (isAuthenticationError(error)) { + return { + success: false, + message: '인증이 만료되었습니다.', + data: { + summary: { + current_month_total: 0, + previous_month_total: 0, + total_count: 0, + current_month_count: 0, + }, + monthly_trend: [], + by_user: [], + items: [], + }, + }; + } + + // 기타 에러 + return { + success: false, + message: error instanceof Error ? error.message : 'API 요청 실패', + data: { + summary: { + current_month_total: 0, + previous_month_total: 0, + total_count: 0, + current_month_count: 0, + }, + monthly_trend: [], + by_user: [], + items: [], + }, + }; + } +} + +// ============================================ +// 가지급금 대시보드 API +// ============================================ + +/** + * 가지급금 대시보드 데이터 조회 + * GET /api/v1/loans/dashboard + * + * @returns 가지급금 요약, 월별 추이, 사용자별 분포, 거래 목록 + */ +export async function fetchLoanDashboard(): Promise> { + try { + const response = await apiClient.get>( + '/loans/dashboard' + ); + return response; + } catch (error) { + // Next.js redirect 에러는 전파 + if (isNextRedirectError(error)) { + throw error; + } + + console.error('[Dashboard] fetchLoanDashboard error:', error); + + // 인증 에러인 경우 + if (isAuthenticationError(error)) { + return { + success: false, + message: '인증이 만료되었습니다.', + data: { + summary: { + total_outstanding: 0, + settled_amount: 0, + recognized_interest: 0, + pending_count: 0, + }, + monthly_trend: [], + user_distribution: [], + items: [], + }, + }; + } + + // 기타 에러 + return { + success: false, + message: error instanceof Error ? error.message : 'API 요청 실패', + data: { + summary: { + total_outstanding: 0, + settled_amount: 0, + recognized_interest: 0, + pending_count: 0, + }, + monthly_trend: [], + user_distribution: [], + items: [], + }, + }; + } +} + +// ============================================ +// 세금 시뮬레이션 API +// ============================================ + +/** + * 세금 시뮬레이션 데이터 조회 + * GET /api/v1/loans/tax-simulation?year={year} + * + * @param year - 시뮬레이션 연도 (기본값: 현재 연도) + * @returns 가지급금 요약, 법인세 비교, 소득세 비교 + */ +export async function fetchTaxSimulation(year?: number): Promise> { + try { + const targetYear = year || new Date().getFullYear(); + const response = await apiClient.get>( + '/loans/tax-simulation', + { params: { year: String(targetYear) } } + ); + return response; + } catch (error) { + // Next.js redirect 에러는 전파 + if (isNextRedirectError(error)) { + throw error; + } + + console.error('[Dashboard] fetchTaxSimulation error:', error); + + const currentYear = year || new Date().getFullYear(); + + // 인증 에러인 경우 + if (isAuthenticationError(error)) { + return { + success: false, + message: '인증이 만료되었습니다.', + data: { + year: currentYear, + loan_summary: { + total_outstanding: 0, + recognized_interest: 0, + interest_rate: 0.046, + }, + corporate_tax: { + without_loan: { taxable_income: 0, tax_amount: 0 }, + with_loan: { taxable_income: 0, tax_amount: 0 }, + difference: 0, + rate_info: '법인세 19% 적용', + }, + income_tax: { + without_loan: { taxable_income: 0, tax_rate: '0%', tax_amount: 0 }, + with_loan: { taxable_income: 0, tax_rate: '35%', tax_amount: 0 }, + difference: 0, + breakdown: { income_tax: 0, local_tax: 0, insurance: 0 }, + }, + }, + }; + } + + // 기타 에러 + return { + success: false, + message: error instanceof Error ? error.message : 'API 요청 실패', + data: { + year: currentYear, + loan_summary: { + total_outstanding: 0, + recognized_interest: 0, + interest_rate: 0.046, + }, + corporate_tax: { + without_loan: { taxable_income: 0, tax_amount: 0 }, + with_loan: { taxable_income: 0, tax_amount: 0 }, + difference: 0, + rate_info: '법인세 19% 적용', + }, + income_tax: { + without_loan: { taxable_income: 0, tax_rate: '0%', tax_amount: 0 }, + with_loan: { taxable_income: 0, tax_rate: '35%', tax_amount: 0 }, + difference: 0, + breakdown: { income_tax: 0, local_tax: 0, insurance: 0 }, + }, + }, + }; + } +} \ No newline at end of file diff --git a/src/lib/api/dashboard/index.ts b/src/lib/api/dashboard/index.ts index c831c140..f26d2ff7 100644 --- a/src/lib/api/dashboard/index.ts +++ b/src/lib/api/dashboard/index.ts @@ -3,4 +3,5 @@ */ export * from './types'; -export * from './transformers'; \ No newline at end of file +export * from './transformers'; +export * from './endpoints'; \ No newline at end of file diff --git a/src/lib/api/dashboard/types.ts b/src/lib/api/dashboard/types.ts index c4154163..2d5c9d8b 100644 --- a/src/lib/api/dashboard/types.ts +++ b/src/lib/api/dashboard/types.ts @@ -561,4 +561,106 @@ export interface ExpectedExpenseDashboardDetailApiResponse { summary: ExpectedExpenseDashboardSummaryApiResponse; items: ExpectedExpenseItemApiResponse[]; footer_summary: ExpectedExpenseFooterSummaryApiResponse; +} + +// ============================================ +// 17. Loan Dashboard Detail (가지급금 상세) API 응답 타입 +// ============================================ + +/** 가지급금 대시보드 요약 */ +export interface LoanDashboardSummaryApiResponse { + total_outstanding: number; // 미정산 잔액 + settled_amount: number; // 정산 완료 금액 + recognized_interest: number; // 인정이자 + pending_count: number; // 미정산 건수 +} + +/** 가지급금 월별 추이 */ +export interface LoanMonthlyTrendApiResponse { + month: string; // "2026-01" + label: string; // "1월" + amount: number; // 금액 +} + +/** 가지급금 사용자별 분포 */ +export interface LoanUserDistributionApiResponse { + user_name: string; // 사용자명 + total_outstanding: number; // 미정산 잔액 + settled_amount: number; // 정산 완료 + count: number; // 건수 + color: string; // 차트 색상 +} + +/** 가지급금 거래 내역 */ +export interface LoanItemApiResponse { + id: number; + user_name: string; // 사용자명 + loan_date: string; // 가지급일 + amount: number; // 금액 + description: string; // 설명 + status: string; // 상태 코드 + status_label: string; // 상태 라벨 +} + +/** GET /api/v1/loans/dashboard 응답 */ +export interface LoanDashboardApiResponse { + summary: LoanDashboardSummaryApiResponse; + monthly_trend: LoanMonthlyTrendApiResponse[]; + user_distribution: LoanUserDistributionApiResponse[]; + items: LoanItemApiResponse[]; +} + +// ============================================ +// 18. Tax Simulation (세금 시뮬레이션) API 응답 타입 +// ============================================ + +/** 세금 시뮬레이션 - 가지급금 요약 */ +export interface TaxSimulationLoanSummaryApiResponse { + total_outstanding: number; // 가지급금 잔액 + recognized_interest: number; // 인정이자 + interest_rate: number; // 이자율 (4.6%) +} + +/** 세금 계산 결과 - 기본 */ +export interface TaxCalculationBaseApiResponse { + taxable_income: number; // 과세소득 + tax_amount: number; // 세금액 +} + +/** 세금 계산 결과 - 소득세용 (세율 포함) */ +export interface TaxCalculationWithRateApiResponse { + taxable_income: number; // 과세소득 + tax_rate: string; // 세율 (예: "35%") + tax_amount: number; // 세금액 +} + +/** 법인세 비교 */ +export interface CorporateTaxComparisonApiResponse { + without_loan: TaxCalculationBaseApiResponse; // 가지급금 없는 경우 + with_loan: TaxCalculationBaseApiResponse; // 가지급금 있는 경우 + difference: number; // 추가 법인세 + rate_info: string; // "법인세 19% 적용" +} + +/** 소득세 내역 */ +export interface IncomeTaxBreakdownApiResponse { + income_tax: number; // 소득세 (35%) + local_tax: number; // 지방소득세 (소득세의 10%) + insurance: number; // 4대보험 추정 (9%) +} + +/** 소득세 비교 */ +export interface IncomeTaxComparisonApiResponse { + without_loan: TaxCalculationWithRateApiResponse; // 가지급금 없는 경우 + with_loan: TaxCalculationWithRateApiResponse; // 가지급금 있는 경우 + difference: number; // 추가 소득세 + breakdown: IncomeTaxBreakdownApiResponse; // 세금 내역 +} + +/** GET /api/v1/loans/tax-simulation 응답 */ +export interface TaxSimulationApiResponse { + year: number; // 시뮬레이션 연도 + loan_summary: TaxSimulationLoanSummaryApiResponse; // 가지급금 요약 + corporate_tax: CorporateTaxComparisonApiResponse; // 법인세 비교 + income_tax: IncomeTaxComparisonApiResponse; // 소득세 비교 } \ No newline at end of file