feat(WEB): CEO 대시보드 Phase 2 API 연동 완료
- StatusBoard(현황판) API Hook 및 타입 추가 - TodayIssue(오늘의 이슈) API Hook 및 타입 추가 - Calendar(캘린더) API Hook 및 타입 추가 - Vat(부가세) API Hook 및 타입 추가 - Entertainment(접대비) API Hook 및 타입 추가 - Welfare(복리후생비) API Hook 및 타입 추가 - CEODashboard.tsx에 모든 Phase 2 Hook 통합 - API 응답 → Frontend 타입 변환 transformer 추가 - WelfareCalculationType 'percentage' → 'ratio' 타입 일치 수정
This commit is contained in:
@@ -25,7 +25,7 @@ import { DEFAULT_DASHBOARD_SETTINGS } from './types';
|
||||
import { ScheduleDetailModal, DetailModal } from './modals';
|
||||
import { DashboardSettingsDialog } from './dialogs/DashboardSettingsDialog';
|
||||
import { mockData } from './mockData';
|
||||
import { useCEODashboard } from '@/hooks/useCEODashboard';
|
||||
import { useCEODashboard, useTodayIssue, useCalendar, useVat, useEntertainment, useWelfare } from '@/hooks/useCEODashboard';
|
||||
import {
|
||||
getMonthlyExpenseModalConfig,
|
||||
getCardManagementModalConfig,
|
||||
@@ -42,6 +42,21 @@ export function CEODashboard() {
|
||||
cardManagementFallback: mockData.cardManagement,
|
||||
});
|
||||
|
||||
// TodayIssue API Hook (Phase 2)
|
||||
const todayIssueData = useTodayIssue(30);
|
||||
|
||||
// Calendar API Hook (Phase 2)
|
||||
const calendarData = useCalendar();
|
||||
|
||||
// Vat API Hook (Phase 2)
|
||||
const vatData = useVat();
|
||||
|
||||
// Entertainment API Hook (Phase 2)
|
||||
const entertainmentData = useEntertainment();
|
||||
|
||||
// Welfare API Hook (Phase 2)
|
||||
const welfareData = useWelfare();
|
||||
|
||||
// 전체 로딩 상태 (모든 API 호출 중일 때)
|
||||
const isLoading = useMemo(() => {
|
||||
return (
|
||||
@@ -49,9 +64,15 @@ export function CEODashboard() {
|
||||
apiData.receivable.loading &&
|
||||
apiData.debtCollection.loading &&
|
||||
apiData.monthlyExpense.loading &&
|
||||
apiData.cardManagement.loading
|
||||
apiData.cardManagement.loading &&
|
||||
apiData.statusBoard.loading &&
|
||||
todayIssueData.loading &&
|
||||
calendarData.loading &&
|
||||
vatData.loading &&
|
||||
entertainmentData.loading &&
|
||||
welfareData.loading
|
||||
);
|
||||
}, [apiData]);
|
||||
}, [apiData, todayIssueData.loading, calendarData.loading, vatData.loading, entertainmentData.loading, welfareData.loading]);
|
||||
|
||||
// API 데이터와 mockData를 병합 (API 우선, 실패 시 fallback)
|
||||
const data = useMemo<CEODashboardData>(() => ({
|
||||
@@ -62,9 +83,14 @@ export function CEODashboard() {
|
||||
debtCollection: apiData.debtCollection.data ?? mockData.debtCollection,
|
||||
monthlyExpense: apiData.monthlyExpense.data ?? mockData.monthlyExpense,
|
||||
cardManagement: apiData.cardManagement.data ?? mockData.cardManagement,
|
||||
// Phase 2 섹션들: 아직 mockData 사용
|
||||
// todayIssue, todayIssueList, entertainment, welfare, vat, calendarSchedules
|
||||
}), [apiData, mockData]);
|
||||
// Phase 2 섹션들
|
||||
todayIssue: apiData.statusBoard.data ?? mockData.todayIssue,
|
||||
todayIssueList: todayIssueData.data?.items ?? mockData.todayIssueList,
|
||||
calendarSchedules: calendarData.data?.items ?? mockData.calendarSchedules,
|
||||
vat: vatData.data ?? mockData.vat,
|
||||
entertainment: entertainmentData.data ?? mockData.entertainment,
|
||||
welfare: welfareData.data ?? mockData.welfare,
|
||||
}), [apiData, todayIssueData.data, calendarData.data, vatData.data, entertainmentData.data, welfareData.data, mockData]);
|
||||
|
||||
// 일정 상세 모달 상태
|
||||
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { DetailModalConfig } from '../types';
|
||||
/**
|
||||
* 복리후생비 현황 모달 설정
|
||||
* 모든 카드가 동일한 상세 모달
|
||||
* @param calculationType - 계산 방식 ('fixed': 직원당 정액 금액/월, 'percentage': 연봉 총액 비율)
|
||||
* @param calculationType - 계산 방식 ('fixed': 직원당 정액 금액/월, 'ratio': 연봉 총액 비율)
|
||||
*/
|
||||
export function getWelfareModalConfig(calculationType: 'fixed' | 'percentage'): DetailModalConfig {
|
||||
export function getWelfareModalConfig(calculationType: 'fixed' | 'ratio'): DetailModalConfig {
|
||||
// 계산 방식에 따른 조건부 calculationCards 생성
|
||||
const calculationCards = calculationType === 'fixed'
|
||||
? {
|
||||
|
||||
@@ -15,6 +15,12 @@ import type {
|
||||
BadDebtApiResponse,
|
||||
ExpectedExpenseApiResponse,
|
||||
CardTransactionApiResponse,
|
||||
StatusBoardApiResponse,
|
||||
TodayIssueApiResponse,
|
||||
CalendarApiResponse,
|
||||
VatApiResponse,
|
||||
EntertainmentApiResponse,
|
||||
WelfareApiResponse,
|
||||
} from '@/lib/api/dashboard/types';
|
||||
|
||||
import {
|
||||
@@ -23,6 +29,12 @@ import {
|
||||
transformDebtCollectionResponse,
|
||||
transformMonthlyExpenseResponse,
|
||||
transformCardManagementResponse,
|
||||
transformStatusBoardResponse,
|
||||
transformTodayIssueResponse,
|
||||
transformCalendarResponse,
|
||||
transformVatResponse,
|
||||
transformEntertainmentResponse,
|
||||
transformWelfareResponse,
|
||||
} from '@/lib/api/dashboard/transformers';
|
||||
|
||||
import type {
|
||||
@@ -31,6 +43,12 @@ import type {
|
||||
DebtCollectionData,
|
||||
MonthlyExpenseData,
|
||||
CardManagementData,
|
||||
TodayIssueItem,
|
||||
TodayIssueListItem,
|
||||
CalendarScheduleItem,
|
||||
VatData,
|
||||
EntertainmentData,
|
||||
WelfareData,
|
||||
} from '@/components/business/CEODashboard/types';
|
||||
|
||||
// ============================================
|
||||
@@ -223,6 +241,318 @@ export function useCardManagement(fallbackData?: CardManagementData) {
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 6. StatusBoard Hook
|
||||
// ============================================
|
||||
|
||||
export function useStatusBoard() {
|
||||
const [data, setData] = useState<TodayIssueItem[] | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const apiData = await fetchApi<StatusBoardApiResponse>('status-board/summary');
|
||||
const transformed = transformStatusBoardResponse(apiData);
|
||||
setData(transformed);
|
||||
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
|
||||
setError(errorMessage);
|
||||
console.error('StatusBoard API Error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 7. TodayIssue Hook
|
||||
// ============================================
|
||||
|
||||
export interface TodayIssueData {
|
||||
items: TodayIssueListItem[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export function useTodayIssue(limit: number = 30) {
|
||||
const [data, setData] = useState<TodayIssueData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const apiData = await fetchApi<TodayIssueApiResponse>(`today-issues/summary?limit=${limit}`);
|
||||
const transformed = transformTodayIssueResponse(apiData);
|
||||
setData(transformed);
|
||||
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
|
||||
setError(errorMessage);
|
||||
console.error('TodayIssue API Error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [limit]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 8. Calendar Hook
|
||||
// ============================================
|
||||
|
||||
export interface CalendarData {
|
||||
items: CalendarScheduleItem[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface UseCalendarOptions {
|
||||
/** 조회 시작일 (Y-m-d, 기본: 이번 달 1일) */
|
||||
startDate?: string;
|
||||
/** 조회 종료일 (Y-m-d, 기본: 이번 달 말일) */
|
||||
endDate?: string;
|
||||
/** 일정 타입 필터 (schedule|order|construction|other|null=전체) */
|
||||
type?: 'schedule' | 'order' | 'construction' | 'other' | null;
|
||||
/** 부서 필터 (all|department|personal) */
|
||||
departmentFilter?: 'all' | 'department' | 'personal';
|
||||
}
|
||||
|
||||
export function useCalendar(options: UseCalendarOptions = {}) {
|
||||
const { startDate, endDate, type, departmentFilter = 'all' } = options;
|
||||
const [data, setData] = useState<CalendarData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 쿼리 파라미터 구성
|
||||
const params = new URLSearchParams();
|
||||
if (startDate) params.append('start_date', startDate);
|
||||
if (endDate) params.append('end_date', endDate);
|
||||
if (type) params.append('type', type);
|
||||
if (departmentFilter) params.append('department_filter', departmentFilter);
|
||||
|
||||
const queryString = params.toString();
|
||||
const endpoint = queryString ? `calendar/schedules?${queryString}` : 'calendar/schedules';
|
||||
|
||||
const apiData = await fetchApi<CalendarApiResponse>(endpoint);
|
||||
const transformed = transformCalendarResponse(apiData);
|
||||
setData(transformed);
|
||||
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
|
||||
setError(errorMessage);
|
||||
console.error('Calendar API Error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [startDate, endDate, type, departmentFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 9. Vat Hook
|
||||
// ============================================
|
||||
|
||||
export interface UseVatOptions {
|
||||
/** 기간 타입 (quarter: 분기, half: 반기, year: 연간) */
|
||||
periodType?: 'quarter' | 'half' | 'year';
|
||||
/** 연도 (기본: 현재 연도) */
|
||||
year?: number;
|
||||
/** 기간 번호 (quarter: 1-4, half: 1-2) */
|
||||
period?: number;
|
||||
}
|
||||
|
||||
export function useVat(options: UseVatOptions = {}) {
|
||||
const { periodType = 'quarter', year, period } = options;
|
||||
const [data, setData] = useState<VatData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 쿼리 파라미터 구성
|
||||
const params = new URLSearchParams();
|
||||
params.append('period_type', periodType);
|
||||
if (year) params.append('year', year.toString());
|
||||
if (period) params.append('period', period.toString());
|
||||
|
||||
const queryString = params.toString();
|
||||
const endpoint = `vat/summary?${queryString}`;
|
||||
|
||||
const apiData = await fetchApi<VatApiResponse>(endpoint);
|
||||
const transformed = transformVatResponse(apiData);
|
||||
setData(transformed);
|
||||
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
|
||||
setError(errorMessage);
|
||||
console.error('Vat API Error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [periodType, year, period]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 10. Entertainment Hook (접대비)
|
||||
// ============================================
|
||||
|
||||
export interface UseEntertainmentOptions {
|
||||
/** 기간 타입 (annual: 연간, quarterly: 분기) */
|
||||
limitType?: 'annual' | 'quarterly';
|
||||
/** 기업 유형 (large: 대기업, medium: 중견기업, small: 중소기업) */
|
||||
companyType?: 'large' | 'medium' | 'small';
|
||||
/** 연도 (기본: 현재 연도) */
|
||||
year?: number;
|
||||
/** 분기 번호 (1-4, 기본: 현재 분기) */
|
||||
quarter?: number;
|
||||
}
|
||||
|
||||
export function useEntertainment(options: UseEntertainmentOptions = {}) {
|
||||
const { limitType = 'quarterly', companyType = 'medium', year, quarter } = options;
|
||||
const [data, setData] = useState<EntertainmentData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 쿼리 파라미터 구성
|
||||
const params = new URLSearchParams();
|
||||
params.append('limit_type', limitType);
|
||||
params.append('company_type', companyType);
|
||||
if (year) params.append('year', year.toString());
|
||||
if (quarter) params.append('quarter', quarter.toString());
|
||||
|
||||
const queryString = params.toString();
|
||||
const endpoint = `entertainment/summary?${queryString}`;
|
||||
|
||||
const apiData = await fetchApi<EntertainmentApiResponse>(endpoint);
|
||||
const transformed = transformEntertainmentResponse(apiData);
|
||||
setData(transformed);
|
||||
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
|
||||
setError(errorMessage);
|
||||
console.error('Entertainment API Error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [limitType, companyType, year, quarter]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 11. Welfare Hook (복리후생비)
|
||||
// ============================================
|
||||
|
||||
export interface UseWelfareOptions {
|
||||
/** 기간 타입 (annual: 연간, quarterly: 분기) */
|
||||
limitType?: 'annual' | 'quarterly';
|
||||
/** 계산 방식 (fixed: 1인당 정액, ratio: 급여 대비 비율) */
|
||||
calculationType?: 'fixed' | 'ratio';
|
||||
/** 1인당 월 정액 (calculation_type=fixed일 때 사용, 기본: 200000) */
|
||||
fixedAmountPerMonth?: number;
|
||||
/** 급여 대비 비율 (calculation_type=ratio일 때 사용, 기본: 0.05) */
|
||||
ratio?: number;
|
||||
/** 연도 (기본: 현재 연도) */
|
||||
year?: number;
|
||||
/** 분기 번호 (1-4, 기본: 현재 분기) */
|
||||
quarter?: number;
|
||||
}
|
||||
|
||||
export function useWelfare(options: UseWelfareOptions = {}) {
|
||||
const {
|
||||
limitType = 'quarterly',
|
||||
calculationType = 'fixed',
|
||||
fixedAmountPerMonth,
|
||||
ratio,
|
||||
year,
|
||||
quarter,
|
||||
} = options;
|
||||
const [data, setData] = useState<WelfareData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 쿼리 파라미터 구성
|
||||
const params = new URLSearchParams();
|
||||
params.append('limit_type', limitType);
|
||||
params.append('calculation_type', calculationType);
|
||||
if (fixedAmountPerMonth) params.append('fixed_amount_per_month', fixedAmountPerMonth.toString());
|
||||
if (ratio) params.append('ratio', ratio.toString());
|
||||
if (year) params.append('year', year.toString());
|
||||
if (quarter) params.append('quarter', quarter.toString());
|
||||
|
||||
const queryString = params.toString();
|
||||
const endpoint = `welfare/summary?${queryString}`;
|
||||
|
||||
const apiData = await fetchApi<WelfareApiResponse>(endpoint);
|
||||
const transformed = transformWelfareResponse(apiData);
|
||||
setData(transformed);
|
||||
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
|
||||
setError(errorMessage);
|
||||
console.error('Welfare API Error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [limitType, calculationType, fixedAmountPerMonth, ratio, year, quarter]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 통합 Dashboard Hook (선택적 사용)
|
||||
// ============================================
|
||||
@@ -240,6 +570,8 @@ export interface UseCEODashboardOptions {
|
||||
cardManagement?: boolean;
|
||||
/** CardManagement fallback 데이터 */
|
||||
cardManagementFallback?: CardManagementData;
|
||||
/** StatusBoard 섹션 활성화 */
|
||||
statusBoard?: boolean;
|
||||
}
|
||||
|
||||
export interface CEODashboardState {
|
||||
@@ -268,6 +600,11 @@ export interface CEODashboardState {
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
statusBoard: {
|
||||
data: TodayIssueItem[] | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
refetchAll: () => void;
|
||||
}
|
||||
|
||||
@@ -283,6 +620,7 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
|
||||
monthlyExpense: enableMonthlyExpense = true,
|
||||
cardManagement: enableCardManagement = true,
|
||||
cardManagementFallback,
|
||||
statusBoard: enableStatusBoard = true,
|
||||
} = options;
|
||||
|
||||
// 각 섹션별 상태
|
||||
@@ -306,6 +644,10 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
|
||||
const [cardManagementLoading, setCardManagementLoading] = useState(enableCardManagement);
|
||||
const [cardManagementError, setCardManagementError] = useState<string | null>(null);
|
||||
|
||||
const [statusBoardData, setStatusBoardData] = useState<TodayIssueItem[] | null>(null);
|
||||
const [statusBoardLoading, setStatusBoardLoading] = useState(enableStatusBoard);
|
||||
const [statusBoardError, setStatusBoardError] = useState<string | null>(null);
|
||||
|
||||
// 개별 fetch 함수들
|
||||
const fetchDailyReport = useCallback(async () => {
|
||||
if (!enableDailyReport) return;
|
||||
@@ -377,6 +719,20 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
|
||||
}
|
||||
}, [enableCardManagement, cardManagementFallback]);
|
||||
|
||||
const fetchStatusBoard = useCallback(async () => {
|
||||
if (!enableStatusBoard) return;
|
||||
try {
|
||||
setStatusBoardLoading(true);
|
||||
setStatusBoardError(null);
|
||||
const apiData = await fetchApi<StatusBoardApiResponse>('status-board/summary');
|
||||
setStatusBoardData(transformStatusBoardResponse(apiData));
|
||||
} catch (err) {
|
||||
setStatusBoardError(err instanceof Error ? err.message : '데이터 로딩 실패');
|
||||
} finally {
|
||||
setStatusBoardLoading(false);
|
||||
}
|
||||
}, [enableStatusBoard]);
|
||||
|
||||
// 전체 refetch
|
||||
const refetchAll = useCallback(() => {
|
||||
fetchDailyReport();
|
||||
@@ -384,7 +740,8 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
|
||||
fetchDebtCollection();
|
||||
fetchMonthlyExpense();
|
||||
fetchCardManagement();
|
||||
}, [fetchDailyReport, fetchReceivable, fetchDebtCollection, fetchMonthlyExpense, fetchCardManagement]);
|
||||
fetchStatusBoard();
|
||||
}, [fetchDailyReport, fetchReceivable, fetchDebtCollection, fetchMonthlyExpense, fetchCardManagement, fetchStatusBoard]);
|
||||
|
||||
// 초기 로드
|
||||
useEffect(() => {
|
||||
@@ -417,6 +774,11 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
|
||||
loading: cardManagementLoading,
|
||||
error: cardManagementError,
|
||||
},
|
||||
statusBoard: {
|
||||
data: statusBoardData,
|
||||
loading: statusBoardLoading,
|
||||
error: statusBoardError,
|
||||
},
|
||||
refetchAll,
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,12 @@ import type {
|
||||
BadDebtApiResponse,
|
||||
ExpectedExpenseApiResponse,
|
||||
CardTransactionApiResponse,
|
||||
StatusBoardApiResponse,
|
||||
TodayIssueApiResponse,
|
||||
CalendarApiResponse,
|
||||
VatApiResponse,
|
||||
EntertainmentApiResponse,
|
||||
WelfareApiResponse,
|
||||
} from './types';
|
||||
|
||||
import type {
|
||||
@@ -18,8 +24,16 @@ import type {
|
||||
DebtCollectionData,
|
||||
MonthlyExpenseData,
|
||||
CardManagementData,
|
||||
TodayIssueItem,
|
||||
TodayIssueListItem,
|
||||
TodayIssueListBadgeType,
|
||||
CalendarScheduleItem,
|
||||
CheckPoint,
|
||||
CheckPointType,
|
||||
VatData,
|
||||
EntertainmentData,
|
||||
WelfareData,
|
||||
HighlightColor,
|
||||
} from '@/components/business/CEODashboard/types';
|
||||
|
||||
// ============================================
|
||||
@@ -428,4 +442,201 @@ export function transformCardManagementResponse(
|
||||
],
|
||||
checkPoints: generateCardManagementCheckPoints(api),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 6. StatusBoard 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* StatusBoard API 응답 → Frontend 타입 변환
|
||||
* API 응답 형식이 TodayIssueItem과 거의 동일하므로 단순 매핑
|
||||
*/
|
||||
export function transformStatusBoardResponse(api: StatusBoardApiResponse): TodayIssueItem[] {
|
||||
return api.items.map((item) => ({
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
count: item.count,
|
||||
path: item.path,
|
||||
isHighlighted: item.isHighlighted,
|
||||
}));
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 7. TodayIssue 변환
|
||||
// ============================================
|
||||
|
||||
/** 유효한 뱃지 타입 목록 */
|
||||
const VALID_BADGE_TYPES: TodayIssueListBadgeType[] = [
|
||||
'수주 성공',
|
||||
'주식 이슈',
|
||||
'직정 제고',
|
||||
'지출예상내역서',
|
||||
'세금 신고',
|
||||
'결재 요청',
|
||||
'기타',
|
||||
];
|
||||
|
||||
/**
|
||||
* API 뱃지 문자열 → Frontend 뱃지 타입 변환
|
||||
* 유효하지 않은 뱃지는 '기타'로 폴백
|
||||
*/
|
||||
function validateBadgeType(badge: string): TodayIssueListBadgeType {
|
||||
if (VALID_BADGE_TYPES.includes(badge as TodayIssueListBadgeType)) {
|
||||
return badge as TodayIssueListBadgeType;
|
||||
}
|
||||
return '기타';
|
||||
}
|
||||
|
||||
/**
|
||||
* TodayIssue API 응답 → Frontend 타입 변환
|
||||
* 오늘의 이슈 리스트 데이터 변환
|
||||
*/
|
||||
export function transformTodayIssueResponse(api: TodayIssueApiResponse): {
|
||||
items: TodayIssueListItem[];
|
||||
totalCount: number;
|
||||
} {
|
||||
return {
|
||||
items: api.items.map((item) => ({
|
||||
id: item.id,
|
||||
badge: validateBadgeType(item.badge),
|
||||
content: item.content,
|
||||
time: item.time,
|
||||
date: item.date,
|
||||
needsApproval: item.needsApproval ?? false,
|
||||
path: item.path,
|
||||
})),
|
||||
totalCount: api.total_count,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 8. Calendar 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Calendar API 응답 → Frontend 타입 변환
|
||||
* API 응답 형식이 CalendarScheduleItem과 동일하므로 단순 매핑
|
||||
*/
|
||||
export function transformCalendarResponse(api: CalendarApiResponse): {
|
||||
items: CalendarScheduleItem[];
|
||||
totalCount: number;
|
||||
} {
|
||||
return {
|
||||
items: api.items.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
startDate: item.startDate,
|
||||
endDate: item.endDate,
|
||||
startTime: item.startTime,
|
||||
endTime: item.endTime,
|
||||
isAllDay: item.isAllDay,
|
||||
type: item.type,
|
||||
department: item.department,
|
||||
personName: item.personName,
|
||||
color: item.color,
|
||||
})),
|
||||
totalCount: api.total_count,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 9. Vat 변환
|
||||
// ============================================
|
||||
|
||||
/** 유효한 하이라이트 색상 목록 */
|
||||
const VALID_HIGHLIGHT_COLORS: HighlightColor[] = ['red', 'green', 'blue'];
|
||||
|
||||
/**
|
||||
* API 색상 문자열 → Frontend 하이라이트 색상 변환
|
||||
* 유효하지 않은 색상은 'blue'로 폴백
|
||||
*/
|
||||
function validateHighlightColor(color: string): HighlightColor {
|
||||
if (VALID_HIGHLIGHT_COLORS.includes(color as HighlightColor)) {
|
||||
return color as HighlightColor;
|
||||
}
|
||||
return 'blue';
|
||||
}
|
||||
|
||||
/**
|
||||
* Vat API 응답 → Frontend 타입 변환
|
||||
* 부가세 현황 데이터 변환
|
||||
*/
|
||||
export function transformVatResponse(api: VatApiResponse): VatData {
|
||||
return {
|
||||
cards: api.cards.map((card) => ({
|
||||
id: card.id,
|
||||
label: card.label,
|
||||
amount: card.amount,
|
||||
subLabel: card.subLabel,
|
||||
unit: card.unit,
|
||||
})),
|
||||
checkPoints: api.check_points.map((cp) => ({
|
||||
id: cp.id,
|
||||
type: cp.type as CheckPointType,
|
||||
message: cp.message,
|
||||
highlights: cp.highlights?.map((h) => ({
|
||||
text: h.text,
|
||||
color: validateHighlightColor(h.color),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 10. Entertainment 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Entertainment API 응답 → Frontend 타입 변환
|
||||
* 접대비 현황 데이터 변환
|
||||
*/
|
||||
export function transformEntertainmentResponse(api: EntertainmentApiResponse): EntertainmentData {
|
||||
return {
|
||||
cards: api.cards.map((card) => ({
|
||||
id: card.id,
|
||||
label: card.label,
|
||||
amount: card.amount,
|
||||
subLabel: card.subLabel,
|
||||
unit: card.unit,
|
||||
})),
|
||||
checkPoints: api.check_points.map((cp) => ({
|
||||
id: cp.id,
|
||||
type: cp.type as CheckPointType,
|
||||
message: cp.message,
|
||||
highlights: cp.highlights?.map((h) => ({
|
||||
text: h.text,
|
||||
color: validateHighlightColor(h.color),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 11. Welfare 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Welfare API 응답 → Frontend 타입 변환
|
||||
* 복리후생비 현황 데이터 변환
|
||||
*/
|
||||
export function transformWelfareResponse(api: WelfareApiResponse): WelfareData {
|
||||
return {
|
||||
cards: api.cards.map((card) => ({
|
||||
id: card.id,
|
||||
label: card.label,
|
||||
amount: card.amount,
|
||||
subLabel: card.subLabel,
|
||||
unit: card.unit,
|
||||
})),
|
||||
checkPoints: api.check_points.map((cp) => ({
|
||||
id: cp.id,
|
||||
type: cp.type as CheckPointType,
|
||||
message: cp.message,
|
||||
highlights: cp.highlights?.map((h) => ({
|
||||
text: h.text,
|
||||
color: validateHighlightColor(h.color),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -117,4 +117,170 @@ export interface DashboardErrorState {
|
||||
debtCollection: string | null;
|
||||
monthlyExpense: string | null;
|
||||
cardManagement: string | null;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 6. StatusBoard (현황판) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 현황판 카드 아이템 */
|
||||
export interface StatusBoardItemApiResponse {
|
||||
id: string; // 카드 ID (orders, bad_debts, etc.)
|
||||
label: string; // 카드 라벨
|
||||
count: number | string; // 건수 또는 텍스트 (예: "부가세 신고 D-15")
|
||||
path: string; // 이동 경로
|
||||
isHighlighted: boolean; // 강조 표시 여부
|
||||
}
|
||||
|
||||
/** GET /api/proxy/status-board/summary 응답 */
|
||||
export interface StatusBoardApiResponse {
|
||||
items: StatusBoardItemApiResponse[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 7. TodayIssue (오늘의 이슈 리스트) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 오늘의 이슈 아이템 */
|
||||
export interface TodayIssueItemApiResponse {
|
||||
id: string; // 항목 고유 ID
|
||||
badge: string; // 이슈 카테고리 뱃지
|
||||
content: string; // 이슈 내용
|
||||
time: string; // 상대 시간 (예: "10분 전")
|
||||
date?: string; // 날짜 (ISO 형식)
|
||||
needsApproval?: boolean; // 승인/반려 버튼 표시 여부
|
||||
path?: string; // 클릭 시 이동할 경로
|
||||
}
|
||||
|
||||
/** GET /api/proxy/today-issues/summary 응답 */
|
||||
export interface TodayIssueApiResponse {
|
||||
items: TodayIssueItemApiResponse[];
|
||||
total_count: number; // 전체 이슈 건수
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 8. Calendar (캘린더) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 캘린더 일정 타입 */
|
||||
export type CalendarScheduleType = 'schedule' | 'order' | 'construction' | 'other';
|
||||
|
||||
/** 캘린더 일정 아이템 */
|
||||
export interface CalendarScheduleItemApiResponse {
|
||||
id: string; // 일정 ID (타입_ID 형식, 예: "wo_123")
|
||||
title: string; // 일정 제목
|
||||
startDate: string; // 시작일 (Y-m-d)
|
||||
endDate: string; // 종료일 (Y-m-d)
|
||||
startTime: string | null; // 시작 시간 (HH:mm) 또는 null
|
||||
endTime: string | null; // 종료 시간 (HH:mm) 또는 null
|
||||
isAllDay: boolean; // 종일 여부
|
||||
type: CalendarScheduleType; // 일정 타입
|
||||
department: string | null; // 부서명
|
||||
personName: string | null; // 담당자명
|
||||
color: string | null; // 일정 색상
|
||||
}
|
||||
|
||||
/** GET /api/proxy/calendar/schedules 응답 */
|
||||
export interface CalendarApiResponse {
|
||||
items: CalendarScheduleItemApiResponse[];
|
||||
total_count: number; // 총 일정 수
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 9. Vat (부가세) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 부가세 금액 카드 아이템 */
|
||||
export interface VatAmountCardApiResponse {
|
||||
id: string; // 카드 ID (vat_sales_tax, vat_purchases_tax, etc.)
|
||||
label: string; // 카드 라벨
|
||||
amount: number; // 금액 또는 건수
|
||||
subLabel?: string; // 부가 라벨 (예: "환급")
|
||||
unit?: string; // 단위 (예: "건")
|
||||
}
|
||||
|
||||
/** 부가세 체크포인트 하이라이트 아이템 */
|
||||
export interface VatHighlightItemApiResponse {
|
||||
text: string; // 하이라이트 텍스트
|
||||
color: string; // 색상 (red, blue, green 등)
|
||||
}
|
||||
|
||||
/** 부가세 체크포인트 아이템 */
|
||||
export interface VatCheckPointApiResponse {
|
||||
id: string; // 체크포인트 ID
|
||||
type: string; // 타입 (success, warning, error)
|
||||
message: string; // 메시지
|
||||
highlights?: VatHighlightItemApiResponse[]; // 하이라이트 아이템 목록
|
||||
}
|
||||
|
||||
/** GET /api/proxy/vat/summary 응답 */
|
||||
export interface VatApiResponse {
|
||||
cards: VatAmountCardApiResponse[];
|
||||
check_points: VatCheckPointApiResponse[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 10. Entertainment (접대비) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 접대비 금액 카드 아이템 */
|
||||
export interface EntertainmentAmountCardApiResponse {
|
||||
id: string; // 카드 ID (et_sales, et_limit, etc.)
|
||||
label: string; // 카드 라벨
|
||||
amount: number; // 금액
|
||||
subLabel?: string; // 부가 라벨
|
||||
unit?: string; // 단위
|
||||
}
|
||||
|
||||
/** 접대비 체크포인트 하이라이트 아이템 */
|
||||
export interface EntertainmentHighlightItemApiResponse {
|
||||
text: string; // 하이라이트 텍스트
|
||||
color: string; // 색상 (red, green, orange 등)
|
||||
}
|
||||
|
||||
/** 접대비 체크포인트 아이템 */
|
||||
export interface EntertainmentCheckPointApiResponse {
|
||||
id: string; // 체크포인트 ID
|
||||
type: string; // 타입 (success, warning, error)
|
||||
message: string; // 메시지
|
||||
highlights?: EntertainmentHighlightItemApiResponse[]; // 하이라이트 아이템 목록
|
||||
}
|
||||
|
||||
/** GET /api/proxy/entertainment/summary 응답 */
|
||||
export interface EntertainmentApiResponse {
|
||||
cards: EntertainmentAmountCardApiResponse[];
|
||||
check_points: EntertainmentCheckPointApiResponse[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 11. Welfare (복리후생비) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 복리후생비 금액 카드 아이템 */
|
||||
export interface WelfareAmountCardApiResponse {
|
||||
id: string; // 카드 ID (wf_annual_limit, wf_period_limit, etc.)
|
||||
label: string; // 카드 라벨
|
||||
amount: number; // 금액
|
||||
subLabel?: string; // 부가 라벨
|
||||
unit?: string; // 단위
|
||||
}
|
||||
|
||||
/** 복리후생비 체크포인트 하이라이트 아이템 */
|
||||
export interface WelfareHighlightItemApiResponse {
|
||||
text: string; // 하이라이트 텍스트
|
||||
color: string; // 색상 (red, green, orange 등)
|
||||
}
|
||||
|
||||
/** 복리후생비 체크포인트 아이템 */
|
||||
export interface WelfareCheckPointApiResponse {
|
||||
id: string; // 체크포인트 ID
|
||||
type: string; // 타입 (success, warning, error)
|
||||
message: string; // 메시지
|
||||
highlights?: WelfareHighlightItemApiResponse[]; // 하이라이트 아이템 목록
|
||||
}
|
||||
|
||||
/** GET /api/proxy/welfare/summary 응답 */
|
||||
export interface WelfareApiResponse {
|
||||
cards: WelfareAmountCardApiResponse[];
|
||||
check_points: WelfareCheckPointApiResponse[];
|
||||
}
|
||||
Reference in New Issue
Block a user