ERP-node/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts

348 lines
11 KiB
TypeScript
Raw Normal View History

"use client";
import { useState, useCallback, useEffect, useMemo } from "react";
import { apiClient } from "@/lib/api/client";
import {
TimelineSchedulerConfig,
ScheduleItem,
Resource,
ZoomLevel,
UseTimelineDataResult,
} from "../types";
import { zoomLevelDays, defaultTimelineSchedulerConfig } from "../config";
/**
* ISO ( )
*/
const toDateString = (date: Date): string => {
return date.toISOString().split("T")[0];
};
/**
*
*/
const addDays = (date: Date, days: number): Date => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
};
/**
*
*/
export function useTimelineData(
config: TimelineSchedulerConfig,
externalSchedules?: ScheduleItem[],
externalResources?: Resource[]
): UseTimelineDataResult {
// 상태
const [schedules, setSchedules] = useState<ScheduleItem[]>([]);
const [resources, setResources] = useState<Resource[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [zoomLevel, setZoomLevel] = useState<ZoomLevel>(
config.defaultZoomLevel || "day"
);
const [viewStartDate, setViewStartDate] = useState<Date>(() => {
if (config.initialDate) {
return new Date(config.initialDate);
}
// 오늘 기준 1주일 전부터 시작
const today = new Date();
today.setDate(today.getDate() - 7);
today.setHours(0, 0, 0, 0);
return today;
});
// 표시 종료일 계산
const viewEndDate = useMemo(() => {
const days = zoomLevelDays[zoomLevel];
return addDays(viewStartDate, days);
}, [viewStartDate, zoomLevel]);
// 테이블명
const tableName = config.useCustomTable
? config.customTableName
: config.selectedTable;
const resourceTableName = config.resourceTable;
// 필드 매핑을 JSON 문자열로 안정화 (객체 참조 변경 방지)
const fieldMappingKey = useMemo(() => {
return JSON.stringify(config.fieldMapping || {});
}, [config.fieldMapping]);
const resourceFieldMappingKey = useMemo(() => {
return JSON.stringify(config.resourceFieldMapping || {});
}, [config.resourceFieldMapping]);
// 🆕 필드 매핑 정규화 (이전 형식 → 새 형식 변환) - useMemo로 메모이제이션
const fieldMapping = useMemo(() => {
const mapping = config.fieldMapping;
if (!mapping) return defaultTimelineSchedulerConfig.fieldMapping!;
return {
id: mapping.id || mapping.idField || "id",
resourceId: mapping.resourceId || mapping.resourceIdField || "resource_id",
title: mapping.title || mapping.titleField || "title",
startDate: mapping.startDate || mapping.startDateField || "start_date",
endDate: mapping.endDate || mapping.endDateField || "end_date",
status: mapping.status || mapping.statusField || undefined,
progress: mapping.progress || mapping.progressField || undefined,
color: mapping.color || mapping.colorField || undefined,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fieldMappingKey]);
// 리소스 필드 매핑 - useMemo로 메모이제이션
const resourceFieldMapping = useMemo(() => {
return config.resourceFieldMapping || defaultTimelineSchedulerConfig.resourceFieldMapping!;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resourceFieldMappingKey]);
// 스케줄 데이터 로드
const fetchSchedules = useCallback(async () => {
if (externalSchedules) {
setSchedules(externalSchedules);
return;
}
if (!tableName) {
setSchedules([]);
return;
}
setIsLoading(true);
setError(null);
try {
const response = await apiClient.post(
`/table-management/tables/${tableName}/data`,
{
page: 1,
size: 10000,
autoFilter: true,
}
);
const responseData =
response.data?.data?.data || response.data?.data || [];
const rawData = Array.isArray(responseData) ? responseData : [];
// 데이터를 ScheduleItem 형태로 변환
const mappedSchedules: ScheduleItem[] = rawData.map((row: any) => ({
id: String(row[fieldMapping.id] || ""),
resourceId: String(row[fieldMapping.resourceId] || ""),
title: String(row[fieldMapping.title] || ""),
startDate: row[fieldMapping.startDate] || "",
endDate: row[fieldMapping.endDate] || "",
status: fieldMapping.status
? row[fieldMapping.status] || "planned"
: "planned",
progress: fieldMapping.progress
? Number(row[fieldMapping.progress]) || 0
: undefined,
color: fieldMapping.color ? row[fieldMapping.color] : undefined,
data: row,
}));
setSchedules(mappedSchedules);
} catch (err: any) {
setError(err.message || "스케줄 데이터 로드 중 오류 발생");
setSchedules([]);
} finally {
setIsLoading(false);
}
// fieldMappingKey를 의존성으로 사용하여 객체 참조 변경 방지
// viewStartDate, viewEndDate는 API 호출에 사용되지 않으므로 제거
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tableName, externalSchedules, fieldMappingKey]);
// 리소스 데이터 로드
const fetchResources = useCallback(async () => {
if (externalResources) {
setResources(externalResources);
return;
}
if (!resourceTableName) {
setResources([]);
return;
}
try {
const response = await apiClient.post(
`/table-management/tables/${resourceTableName}/data`,
{
page: 1,
size: 1000,
autoFilter: true,
}
);
const responseData =
response.data?.data?.data || response.data?.data || [];
const rawData = Array.isArray(responseData) ? responseData : [];
// 데이터를 Resource 형태로 변환
const mappedResources: Resource[] = rawData.map((row: any) => ({
id: String(row[resourceFieldMapping.id] || ""),
name: String(row[resourceFieldMapping.name] || ""),
group: resourceFieldMapping.group
? row[resourceFieldMapping.group]
: undefined,
}));
setResources(mappedResources);
} catch (err: any) {
console.error("리소스 로드 오류:", err);
setResources([]);
}
// resourceFieldMappingKey를 의존성으로 사용하여 객체 참조 변경 방지
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resourceTableName, externalResources, resourceFieldMappingKey]);
// 초기 로드
useEffect(() => {
fetchSchedules();
}, [fetchSchedules]);
useEffect(() => {
fetchResources();
}, [fetchResources]);
// 네비게이션 함수들
const goToPrevious = useCallback(() => {
const days = zoomLevelDays[zoomLevel];
setViewStartDate((prev) => addDays(prev, -days));
}, [zoomLevel]);
const goToNext = useCallback(() => {
const days = zoomLevelDays[zoomLevel];
setViewStartDate((prev) => addDays(prev, days));
}, [zoomLevel]);
const goToToday = useCallback(() => {
const today = new Date();
today.setDate(today.getDate() - 7);
today.setHours(0, 0, 0, 0);
setViewStartDate(today);
}, []);
const goToDate = useCallback((date: Date) => {
const newDate = new Date(date);
newDate.setDate(newDate.getDate() - 7);
newDate.setHours(0, 0, 0, 0);
setViewStartDate(newDate);
}, []);
// 스케줄 업데이트
const updateSchedule = useCallback(
async (id: string, updates: Partial<ScheduleItem>) => {
if (!tableName || !config.editable) return;
try {
// 필드 매핑 역변환
const updateData: Record<string, any> = {};
if (updates.startDate) updateData[fieldMapping.startDate] = updates.startDate;
if (updates.endDate) updateData[fieldMapping.endDate] = updates.endDate;
if (updates.resourceId) updateData[fieldMapping.resourceId] = updates.resourceId;
if (updates.title) updateData[fieldMapping.title] = updates.title;
if (updates.status && fieldMapping.status)
updateData[fieldMapping.status] = updates.status;
if (updates.progress !== undefined && fieldMapping.progress)
updateData[fieldMapping.progress] = updates.progress;
await apiClient.put(`/table-management/tables/${tableName}/data/${id}`, updateData);
// 로컬 상태 업데이트
setSchedules((prev) =>
prev.map((s) => (s.id === id ? { ...s, ...updates } : s))
);
} catch (err: any) {
console.error("스케줄 업데이트 오류:", err);
throw err;
}
},
[tableName, fieldMapping, config.editable]
);
// 스케줄 추가
const addSchedule = useCallback(
async (schedule: Omit<ScheduleItem, "id">) => {
if (!tableName || !config.editable) return;
try {
// 필드 매핑 역변환
const insertData: Record<string, any> = {
[fieldMapping.resourceId]: schedule.resourceId,
[fieldMapping.title]: schedule.title,
[fieldMapping.startDate]: schedule.startDate,
[fieldMapping.endDate]: schedule.endDate,
};
if (fieldMapping.status) insertData[fieldMapping.status] = schedule.status;
if (fieldMapping.progress && schedule.progress !== undefined)
insertData[fieldMapping.progress] = schedule.progress;
const response = await apiClient.post(
`/table-management/tables/${tableName}/data`,
insertData
);
const newId = response.data?.data?.id || Date.now().toString();
// 로컬 상태 업데이트
setSchedules((prev) => [...prev, { ...schedule, id: newId }]);
} catch (err: any) {
console.error("스케줄 추가 오류:", err);
throw err;
}
},
[tableName, fieldMapping, config.editable]
);
// 스케줄 삭제
const deleteSchedule = useCallback(
async (id: string) => {
if (!tableName || !config.editable) return;
try {
await apiClient.delete(`/table-management/tables/${tableName}/data/${id}`);
// 로컬 상태 업데이트
setSchedules((prev) => prev.filter((s) => s.id !== id));
} catch (err: any) {
console.error("스케줄 삭제 오류:", err);
throw err;
}
},
[tableName, config.editable]
);
// 새로고침
const refresh = useCallback(() => {
fetchSchedules();
fetchResources();
}, [fetchSchedules, fetchResources]);
return {
schedules,
resources,
isLoading,
error,
zoomLevel,
setZoomLevel,
viewStartDate,
viewEndDate,
goToPrevious,
goToNext,
goToToday,
goToDate,
updateSchedule,
addSchedule,
deleteSchedule,
refresh,
};
}