feat: Phase 2 프론트엔드 타입 및 API 연동

- API 타입 정의 추가 (LoanDashboard, TaxSimulation)
- API 엔드포인트 함수 추가 (endpoints.ts)
- 모달 데이터 훅 생성 (useCardManagementModals.ts)

관련: docs/plans/card-management-section-plan.md
This commit is contained in:
2026-01-22 23:01:22 +09:00
parent f8d93e2851
commit fe845227df
4 changed files with 575 additions and 1 deletions

View 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,
};
}

View 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 },
},
},
};
}
}

View File

@@ -3,4 +3,5 @@
*/
export * from './types';
export * from './transformers';
export * from './transformers';
export * from './endpoints';

View File

@@ -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; // 소득세 비교
}