feat: Phase 2 프론트엔드 타입 및 API 연동
- API 타입 정의 추가 (LoanDashboard, TaxSimulation) - API 엔드포인트 함수 추가 (endpoints.ts) - 모달 데이터 훅 생성 (useCardManagementModals.ts) 관련: docs/plans/card-management-section-plan.md
This commit is contained in:
229
src/hooks/useCardManagementModals.ts
Normal file
229
src/hooks/useCardManagementModals.ts
Normal file
@@ -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<void>;
|
||||
/** 모든 카드 데이터 조회 */
|
||||
fetchAllData: () => Promise<void>;
|
||||
/** 데이터 초기화 */
|
||||
clearData: (cardId?: CardManagementCardId) => void;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Hook 구현
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 카드/가지급금 관리 섹션 모달 데이터 관리 Hook
|
||||
*
|
||||
* @example
|
||||
* const { cm1Data, cm2Data, loading, fetchModalData } = useCardManagementModals();
|
||||
*
|
||||
* // 카드 클릭 시
|
||||
* await fetchModalData('cm1');
|
||||
*/
|
||||
export function useCardManagementModals(): UseCardManagementModalsReturn {
|
||||
// 각 카드별 데이터 상태
|
||||
const [cm1Data, setCm1Data] = useState<CardDashboardDetailApiResponse | null>(null);
|
||||
const [cm2Data, setCm2Data] = useState<LoanDashboardApiResponse | null>(null);
|
||||
const [cm3Data, setCm3Data] = useState<TaxSimulationApiResponse | null>(null);
|
||||
const [cm4Data, setCm4Data] = useState<TaxSimulationApiResponse | null>(null);
|
||||
|
||||
// 공통 상태
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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,
|
||||
};
|
||||
}
|
||||
242
src/lib/api/dashboard/endpoints.ts
Normal file
242
src/lib/api/dashboard/endpoints.ts
Normal file
@@ -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<ApiResponse<CardDashboardDetailApiResponse>> {
|
||||
try {
|
||||
const response = await apiClient.get<ApiResponse<CardDashboardDetailApiResponse>>(
|
||||
'/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<ApiResponse<LoanDashboardApiResponse>> {
|
||||
try {
|
||||
const response = await apiClient.get<ApiResponse<LoanDashboardApiResponse>>(
|
||||
'/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<ApiResponse<TaxSimulationApiResponse>> {
|
||||
try {
|
||||
const targetYear = year || new Date().getFullYear();
|
||||
const response = await apiClient.get<ApiResponse<TaxSimulationApiResponse>>(
|
||||
'/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 },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,5 @@
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './transformers';
|
||||
export * from './transformers';
|
||||
export * from './endpoints';
|
||||
@@ -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; // 소득세 비교
|
||||
}
|
||||
Reference in New Issue
Block a user