docs: v2-timeline-scheduler 구현 완료 및 상태 업데이트
- v2-timeline-scheduler의 구현 상태를 체크리스트에 반영하였으며, 관련 문서화 작업을 완료하였습니다. - 각 구성 요소의 구현 완료 상태를 명시하고, 향후 작업 계획을 업데이트하였습니다. - 타임라인 스케줄러 컴포넌트를 레지스트리에 추가하여 통합하였습니다.
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
"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;
|
||||
|
||||
// 필드 매핑
|
||||
const fieldMapping = config.fieldMapping || defaultTimelineSchedulerConfig.fieldMapping!;
|
||||
const resourceFieldMapping =
|
||||
config.resourceFieldMapping || defaultTimelineSchedulerConfig.resourceFieldMapping!;
|
||||
|
||||
// 스케줄 데이터 로드
|
||||
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,
|
||||
search: {
|
||||
// 표시 범위 내의 스케줄만 조회
|
||||
[fieldMapping.startDate]: {
|
||||
value: toDateString(viewEndDate),
|
||||
operator: "lte",
|
||||
},
|
||||
[fieldMapping.endDate]: {
|
||||
value: toDateString(viewStartDate),
|
||||
operator: "gte",
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}, [
|
||||
tableName,
|
||||
externalSchedules,
|
||||
fieldMapping,
|
||||
viewStartDate,
|
||||
viewEndDate,
|
||||
]);
|
||||
|
||||
// 리소스 데이터 로드
|
||||
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([]);
|
||||
}
|
||||
}, [resourceTableName, externalResources, resourceFieldMapping]);
|
||||
|
||||
// 초기 로드
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user