feat: CEO 대시보드 접대비/복리후생비/매출채권/캘린더 섹션 개선 및 회계 페이지 확장
- 접대비/복리후생비 섹션: 리스크감지형 구조로 변경 - 매출채권 섹션: transformer/타입 정비 - 캘린더 섹션: ScheduleDetailModal 개선 - 카드관리 모달 transformer 확장 - useCEODashboard 훅 리팩토링 및 정리 - dashboard endpoints/types/transformers (expense, receivable, tax-benefits) 대폭 확장 - 회계 5개 페이지(은행거래, 카드거래, 매출채권, 세금계산서, 거래처원장) 기능 개선 - ApprovalBox 소폭 수정 - CLAUDE.md 업데이트
This commit is contained in:
@@ -18,14 +18,18 @@ import type {
|
||||
DailyReportApiResponse,
|
||||
ReceivablesApiResponse,
|
||||
BadDebtApiResponse,
|
||||
ExpectedExpenseApiResponse,
|
||||
CardTransactionApiResponse,
|
||||
StatusBoardApiResponse,
|
||||
TodayIssueApiResponse,
|
||||
CalendarApiResponse,
|
||||
VatApiResponse,
|
||||
VatDetailApiResponse,
|
||||
EntertainmentApiResponse,
|
||||
EntertainmentDetailApiResponse,
|
||||
WelfareApiResponse,
|
||||
WelfareDetailApiResponse,
|
||||
ExpectedExpenseDashboardDetailApiResponse,
|
||||
SalesStatusApiResponse,
|
||||
PurchaseStatusApiResponse,
|
||||
DailyProductionApiResponse,
|
||||
@@ -38,18 +42,18 @@ import {
|
||||
transformDailyReportResponse,
|
||||
transformReceivableResponse,
|
||||
transformDebtCollectionResponse,
|
||||
transformMonthlyExpenseResponse,
|
||||
transformCardManagementResponse,
|
||||
transformStatusBoardResponse,
|
||||
transformTodayIssueResponse,
|
||||
transformCalendarResponse,
|
||||
transformVatResponse,
|
||||
transformVatDetailResponse,
|
||||
transformEntertainmentResponse,
|
||||
transformEntertainmentDetailResponse,
|
||||
transformWelfareResponse,
|
||||
transformWelfareDetailResponse,
|
||||
transformPurchaseRecordsToModal,
|
||||
transformCardTransactionsToModal,
|
||||
transformBillRecordsToModal,
|
||||
transformAllExpensesToModal,
|
||||
transformExpectedExpenseDetailResponse,
|
||||
transformSalesStatusResponse,
|
||||
transformPurchaseStatusResponse,
|
||||
transformDailyProductionResponse,
|
||||
@@ -58,11 +62,6 @@ import {
|
||||
transformDailyAttendanceResponse,
|
||||
} from '@/lib/api/dashboard/transformers';
|
||||
|
||||
import { getPurchases } from '@/components/accounting/PurchaseManagement/actions';
|
||||
import { getCardTransactionList } from '@/components/accounting/CardTransactionInquiry/actions';
|
||||
import { getBills } from '@/components/accounting/BillManagement/actions';
|
||||
import { formatAmount } from '@/lib/api/dashboard/transformers/common';
|
||||
|
||||
import type {
|
||||
DailyReportData,
|
||||
ReceivableData,
|
||||
@@ -124,6 +123,20 @@ async function fetchCardManagementData(fallbackData?: CardManagementData) {
|
||||
return transformCardManagementResponse(cardApiData, loanData, taxData, fallbackData);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 당월 날짜 범위 유틸리티
|
||||
// ============================================
|
||||
|
||||
function getCurrentMonthEndpoint(base: string): string {
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth() + 1;
|
||||
const startDate = `${y}-${String(m).padStart(2, '0')}-01`;
|
||||
const lastDay = new Date(y, m, 0).getDate();
|
||||
const endDate = `${y}-${String(m).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
|
||||
return buildEndpoint(base, { start_date: startDate, end_date: endDate });
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 1~4. 단순 섹션 Hooks (파라미터 없음)
|
||||
// ============================================
|
||||
@@ -150,65 +163,10 @@ export function useDebtCollection() {
|
||||
}
|
||||
|
||||
export function useMonthlyExpense() {
|
||||
const [data, setData] = useState<MonthlyExpenseData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 당월 날짜 범위
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth() + 1;
|
||||
const startDate = `${y}-${String(m).padStart(2, '0')}-01`;
|
||||
const lastDay = new Date(y, m, 0).getDate();
|
||||
const endDate = `${y}-${String(m).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
|
||||
const commonParams = { perPage: 9999, page: 1 };
|
||||
|
||||
const [purchaseResult, cardResult, billResult] = await Promise.all([
|
||||
getPurchases({ ...commonParams, startDate, endDate }),
|
||||
getCardTransactionList({ ...commonParams, startDate, endDate }),
|
||||
getBills({ ...commonParams, issueStartDate: startDate, issueEndDate: endDate }),
|
||||
]);
|
||||
|
||||
const purchases = purchaseResult.success ? purchaseResult.data : [];
|
||||
const cards = cardResult.success ? cardResult.data : [];
|
||||
const bills = billResult.success ? billResult.data : [];
|
||||
|
||||
const purchaseTotal = purchases.reduce((sum, r) => sum + r.totalAmount, 0);
|
||||
const cardTotal = cards.reduce((sum, r) => sum + r.totalAmount, 0);
|
||||
const billTotal = bills.reduce((sum, r) => sum + r.amount, 0);
|
||||
const grandTotal = purchaseTotal + cardTotal + billTotal;
|
||||
|
||||
const result: MonthlyExpenseData = {
|
||||
cards: [
|
||||
{ id: 'me1', label: '매입', amount: purchaseTotal },
|
||||
{ id: 'me2', label: '카드', amount: cardTotal },
|
||||
{ id: 'me3', label: '발행어음', amount: billTotal },
|
||||
{ id: 'me4', label: '총 예상 지출 합계', amount: grandTotal },
|
||||
],
|
||||
checkPoints: grandTotal > 0
|
||||
? [{ id: 'me-total', type: 'info' as const, message: `이번 달 예상 지출은 ${formatAmount(grandTotal)}입니다.`, highlights: [{ text: formatAmount(grandTotal), color: 'blue' as const }] }]
|
||||
: [{ id: 'me-total', type: 'info' as const, message: '이번 달 예상 지출이 없습니다.' }],
|
||||
};
|
||||
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '데이터 로딩 실패');
|
||||
console.error('MonthlyExpense API Error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
return useDashboardFetch<ExpectedExpenseApiResponse, MonthlyExpenseData>(
|
||||
getCurrentMonthEndpoint('expected-expenses/summary'),
|
||||
transformMonthlyExpenseResponse,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -350,6 +308,27 @@ export function useVat(options: UseVatOptions = {}) {
|
||||
return useDashboardFetch<VatApiResponse, VatData>(endpoint, transformVatResponse);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 9-1. VatDetail Hook (부가세 상세 - 모달용)
|
||||
// ============================================
|
||||
|
||||
export function useVatDetail() {
|
||||
const endpoint = useMemo(() => 'vat/detail', []);
|
||||
|
||||
const result = useDashboardFetch<VatDetailApiResponse, DetailModalConfig>(
|
||||
endpoint,
|
||||
transformVatDetailResponse,
|
||||
{ lazy: true },
|
||||
);
|
||||
|
||||
return {
|
||||
modalConfig: result.data,
|
||||
loading: result.loading,
|
||||
error: result.error,
|
||||
refetch: result.refetch,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 10. Entertainment Hook (접대비)
|
||||
// ============================================
|
||||
@@ -423,6 +402,43 @@ export function useWelfare(options: UseWelfareOptions = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 11-1. EntertainmentDetail Hook (접대비 상세 - 모달용)
|
||||
// ============================================
|
||||
|
||||
export interface UseEntertainmentDetailOptions {
|
||||
companyType?: 'large' | 'medium' | 'small';
|
||||
year?: number;
|
||||
quarter?: number;
|
||||
}
|
||||
|
||||
export function useEntertainmentDetail(options: UseEntertainmentDetailOptions = {}) {
|
||||
const { companyType = 'medium', year, quarter } = options;
|
||||
|
||||
const endpoint = useMemo(
|
||||
() =>
|
||||
buildEndpoint('entertainment/detail', {
|
||||
company_type: companyType,
|
||||
year,
|
||||
quarter,
|
||||
}),
|
||||
[companyType, year, quarter],
|
||||
);
|
||||
|
||||
const result = useDashboardFetch<EntertainmentDetailApiResponse, DetailModalConfig>(
|
||||
endpoint,
|
||||
transformEntertainmentDetailResponse,
|
||||
{ lazy: true },
|
||||
);
|
||||
|
||||
return {
|
||||
modalConfig: result.data,
|
||||
loading: result.loading,
|
||||
error: result.error,
|
||||
refetch: result.refetch,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 12. WelfareDetail Hook (복리후생비 상세 - 모달용)
|
||||
// ============================================
|
||||
@@ -547,44 +563,42 @@ export function useMonthlyExpenseDetail() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// cardId → transaction_type 매핑
|
||||
const CARD_TRANSACTION_TYPE: Record<MonthlyExpenseCardId, string | undefined> = {
|
||||
me1: 'purchase',
|
||||
me2: 'card',
|
||||
me3: 'bill',
|
||||
me4: undefined, // 전체
|
||||
};
|
||||
|
||||
const fetchData = useCallback(async (cardId: MonthlyExpenseCardId, filterParams?: { startDate?: string; endDate?: string; search?: string }) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 당월 기본 날짜 범위
|
||||
const now = new Date();
|
||||
const startDate = filterParams?.startDate || `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
|
||||
const endDate = filterParams?.endDate || `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()).padStart(2, '0')}`;
|
||||
const search = filterParams?.search;
|
||||
// 대시보드 전용 API 엔드포인트 구성
|
||||
const transactionType = CARD_TRANSACTION_TYPE[cardId];
|
||||
const params: Record<string, string | undefined> = {
|
||||
transaction_type: transactionType,
|
||||
start_date: filterParams?.startDate,
|
||||
end_date: filterParams?.endDate,
|
||||
search: filterParams?.search,
|
||||
};
|
||||
const endpoint = buildEndpoint('/api/proxy/expected-expenses/dashboard-detail', params);
|
||||
|
||||
// 전체 데이터 가져오기 (perPage 크게 설정)
|
||||
const commonParams = { perPage: 9999, page: 1 };
|
||||
|
||||
let transformed: DetailModalConfig;
|
||||
|
||||
if (cardId === 'me1') {
|
||||
const result = await getPurchases({ ...commonParams, startDate, endDate, search });
|
||||
transformed = transformPurchaseRecordsToModal(result.success ? result.data : []);
|
||||
} else if (cardId === 'me2') {
|
||||
const result = await getCardTransactionList({ ...commonParams, startDate, endDate, search });
|
||||
transformed = transformCardTransactionsToModal(result.success ? result.data : []);
|
||||
} else if (cardId === 'me3') {
|
||||
const result = await getBills({ ...commonParams, issueStartDate: startDate, issueEndDate: endDate, search });
|
||||
transformed = transformBillRecordsToModal(result.success ? result.data : []);
|
||||
} else {
|
||||
// me4: 3개 모두 호출 후 합산
|
||||
const [purchaseResult, cardResult, billResult] = await Promise.all([
|
||||
getPurchases({ ...commonParams, startDate, endDate, search }),
|
||||
getCardTransactionList({ ...commonParams, startDate, endDate, search }),
|
||||
getBills({ ...commonParams, issueStartDate: startDate, issueEndDate: endDate, search }),
|
||||
]);
|
||||
transformed = transformAllExpensesToModal(
|
||||
purchaseResult.success ? purchaseResult.data : [],
|
||||
cardResult.success ? cardResult.data : [],
|
||||
billResult.success ? billResult.data : [],
|
||||
);
|
||||
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 = transformExpectedExpenseDetailResponse(
|
||||
result.data as ExpectedExpenseDashboardDetailApiResponse,
|
||||
cardId,
|
||||
);
|
||||
|
||||
setModalConfig(transformed);
|
||||
return transformed;
|
||||
@@ -712,57 +726,12 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
|
||||
{ initialLoading: enableDailyAttendance },
|
||||
);
|
||||
|
||||
// MonthlyExpense: 커스텀 (3개 페이지 API 병렬)
|
||||
const [meData, setMeData] = useState<MonthlyExpenseData | null>(null);
|
||||
const [meLoading, setMeLoading] = useState(enableMonthlyExpense);
|
||||
const [meError, setMeError] = useState<string | null>(null);
|
||||
|
||||
const fetchME = useCallback(async () => {
|
||||
if (!enableMonthlyExpense) return;
|
||||
try {
|
||||
setMeLoading(true);
|
||||
setMeError(null);
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth() + 1;
|
||||
const startDate = `${y}-${String(m).padStart(2, '0')}-01`;
|
||||
const lastDay = new Date(y, m, 0).getDate();
|
||||
const endDate = `${y}-${String(m).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
|
||||
const commonParams = { perPage: 9999, page: 1 };
|
||||
|
||||
const [purchaseResult, cardResult, billResult] = await Promise.all([
|
||||
getPurchases({ ...commonParams, startDate, endDate }),
|
||||
getCardTransactionList({ ...commonParams, startDate, endDate }),
|
||||
getBills({ ...commonParams, issueStartDate: startDate, issueEndDate: endDate }),
|
||||
]);
|
||||
|
||||
const purchases = purchaseResult.success ? purchaseResult.data : [];
|
||||
const cards = cardResult.success ? cardResult.data : [];
|
||||
const bills = billResult.success ? billResult.data : [];
|
||||
|
||||
const purchaseTotal = purchases.reduce((sum, r) => sum + r.totalAmount, 0);
|
||||
const cardTotal = cards.reduce((sum, r) => sum + r.totalAmount, 0);
|
||||
const billTotal = bills.reduce((sum, r) => sum + r.amount, 0);
|
||||
const grandTotal = purchaseTotal + cardTotal + billTotal;
|
||||
|
||||
setMeData({
|
||||
cards: [
|
||||
{ id: 'me1', label: '매입', amount: purchaseTotal },
|
||||
{ id: 'me2', label: '카드', amount: cardTotal },
|
||||
{ id: 'me3', label: '발행어음', amount: billTotal },
|
||||
{ id: 'me4', label: '총 예상 지출 합계', amount: grandTotal },
|
||||
],
|
||||
checkPoints: grandTotal > 0
|
||||
? [{ id: 'me-total', type: 'info' as const, message: `이번 달 예상 지출은 ${formatAmount(grandTotal)}입니다.`, highlights: [{ text: formatAmount(grandTotal), color: 'blue' as const }] }]
|
||||
: [{ id: 'me-total', type: 'info' as const, message: '이번 달 예상 지출이 없습니다.' }],
|
||||
});
|
||||
} catch (err) {
|
||||
setMeError(err instanceof Error ? err.message : '데이터 로딩 실패');
|
||||
console.error('MonthlyExpense API Error:', err);
|
||||
} finally {
|
||||
setMeLoading(false);
|
||||
}
|
||||
}, [enableMonthlyExpense]);
|
||||
// MonthlyExpense: 대시보드 전용 API (당월 필터)
|
||||
const me = useDashboardFetch<ExpectedExpenseApiResponse, MonthlyExpenseData>(
|
||||
enableMonthlyExpense ? getCurrentMonthEndpoint('expected-expenses/summary') : null,
|
||||
transformMonthlyExpenseResponse,
|
||||
{ initialLoading: enableMonthlyExpense },
|
||||
);
|
||||
|
||||
// CardManagement: 커스텀 (3개 API 병렬)
|
||||
const [cmData, setCmData] = useState<CardManagementData | null>(null);
|
||||
@@ -785,15 +754,14 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
|
||||
}, [enableCardManagement, cardManagementFallback]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchME();
|
||||
fetchCM();
|
||||
}, [fetchME, fetchCM]);
|
||||
}, [fetchCM]);
|
||||
|
||||
const refetchAll = useCallback(() => {
|
||||
dr.refetch();
|
||||
rv.refetch();
|
||||
dc.refetch();
|
||||
fetchME();
|
||||
me.refetch();
|
||||
fetchCM();
|
||||
sb.refetch();
|
||||
ss.refetch();
|
||||
@@ -803,13 +771,13 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
|
||||
cs.refetch();
|
||||
da.refetch();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dr.refetch, rv.refetch, dc.refetch, fetchME, fetchCM, sb.refetch, ss.refetch, ps.refetch, dp.refetch, us.refetch, cs.refetch, da.refetch]);
|
||||
}, [dr.refetch, rv.refetch, dc.refetch, me.refetch, fetchCM, sb.refetch, ss.refetch, ps.refetch, dp.refetch, us.refetch, cs.refetch, da.refetch]);
|
||||
|
||||
return {
|
||||
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: meData, loading: meLoading, error: meError },
|
||||
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 },
|
||||
salesStatus: { data: ss.data, loading: ss.loading, error: ss.error },
|
||||
|
||||
@@ -52,7 +52,7 @@ export interface UseCardManagementModalsReturn {
|
||||
/** 에러 메시지 */
|
||||
error: string | null;
|
||||
/** 특정 카드의 모달 데이터 조회 - 데이터 직접 반환 */
|
||||
fetchModalData: (cardId: CardManagementCardId) => Promise<CardManagementModalData>;
|
||||
fetchModalData: (cardId: CardManagementCardId, params?: { start_date?: string; end_date?: string }) => Promise<CardManagementModalData>;
|
||||
/** 모든 카드 데이터 조회 */
|
||||
fetchAllData: () => Promise<void>;
|
||||
/** 데이터 초기화 */
|
||||
@@ -105,11 +105,15 @@ export function useCardManagementModals(): UseCardManagementModalsReturn {
|
||||
|
||||
/**
|
||||
* cm2: 가지급금 상세 데이터 조회
|
||||
* @param params - 날짜 필터 (선택)
|
||||
* @returns 조회된 데이터 (실패 시 null)
|
||||
*/
|
||||
const fetchCm2Data = useCallback(async (): Promise<LoanDashboardApiResponse | null> => {
|
||||
const fetchCm2Data = useCallback(async (params?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}): Promise<LoanDashboardApiResponse | null> => {
|
||||
try {
|
||||
const response = await fetchLoanDashboard();
|
||||
const response = await fetchLoanDashboard(params);
|
||||
if (response.success && response.data) {
|
||||
setCm2Data(response.data);
|
||||
return response.data;
|
||||
@@ -148,10 +152,14 @@ export function useCardManagementModals(): UseCardManagementModalsReturn {
|
||||
|
||||
/**
|
||||
* 특정 카드의 모달 데이터 조회
|
||||
* @param params - cm2용 날짜 필터 (선택)
|
||||
* @returns 조회된 모달 데이터 객체 (카드 ID에 해당하는 데이터만 포함)
|
||||
*/
|
||||
const fetchModalData = useCallback(
|
||||
async (cardId: CardManagementCardId): Promise<CardManagementModalData> => {
|
||||
async (cardId: CardManagementCardId, params?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}): Promise<CardManagementModalData> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
@@ -163,7 +171,7 @@ export function useCardManagementModals(): UseCardManagementModalsReturn {
|
||||
result.cm1Data = await fetchCm1Data();
|
||||
break;
|
||||
case 'cm2':
|
||||
result.cm2Data = await fetchCm2Data();
|
||||
result.cm2Data = await fetchCm2Data(params);
|
||||
break;
|
||||
case 'cm3':
|
||||
case 'cm4': {
|
||||
|
||||
@@ -38,8 +38,8 @@ export function useDashboardFetch<TApi, TResult>(
|
||||
const [loading, setLoading] = useState(options?.initialLoading ?? !lazy);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!endpoint) return;
|
||||
const fetchData = useCallback(async (): Promise<TResult | null> => {
|
||||
if (!endpoint) return null;
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -55,10 +55,12 @@ export function useDashboardFetch<TApi, TResult>(
|
||||
|
||||
const transformed = transformer(result.data);
|
||||
setData(transformed);
|
||||
return transformed;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
|
||||
setError(errorMessage);
|
||||
console.error(`Dashboard API Error [${endpoint}]:`, err);
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -70,7 +72,7 @@ export function useDashboardFetch<TApi, TResult>(
|
||||
}
|
||||
}, [lazy, endpoint, fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
return { data, loading, error, refetch: fetchData, setData };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user