feat: 타임라인 스케줄러 컴포넌트 리소스 처리 개선

- 리소스가 없을 경우 스케줄의 resourceId를 기반으로 자동으로 리소스를 생성하는 기능을 추가하였습니다.
- 리소스별 스케줄 그룹화를 위해 effectiveResources를 도입하여 코드의 가독성을 향상시켰습니다.
- 스케줄 데이터가 없을 경우 사용자에게 적절한 메시지를 표시하도록 수정하였습니다.
- useTimelineData 훅에서 불필요한 검색 조건을 제거하여 성능을 개선하였습니다.
This commit is contained in:
kjs 2026-02-02 13:41:11 +09:00
parent 4447911892
commit 4e7aa0c3b9
2 changed files with 33 additions and 24 deletions

View File

@ -85,11 +85,31 @@ export function TimelineSchedulerComponent({
const cellWidthConfig = config.cellWidth || defaultTimelineSchedulerConfig.cellWidth!; const cellWidthConfig = config.cellWidth || defaultTimelineSchedulerConfig.cellWidth!;
const cellWidth = cellWidthConfig[zoomLevel] || 60; const cellWidth = cellWidthConfig[zoomLevel] || 60;
// 리소스가 없으면 스케줄의 resourceId로 자동 생성
const effectiveResources = useMemo(() => {
if (resources.length > 0) {
return resources;
}
// 스케줄에서 고유한 resourceId 추출하여 자동 리소스 생성
const uniqueResourceIds = new Set<string>();
schedules.forEach((schedule) => {
if (schedule.resourceId) {
uniqueResourceIds.add(schedule.resourceId);
}
});
return Array.from(uniqueResourceIds).map((id) => ({
id,
name: id, // resourceId를 이름으로 사용
}));
}, [resources, schedules]);
// 리소스별 스케줄 그룹화 // 리소스별 스케줄 그룹화
const schedulesByResource = useMemo(() => { const schedulesByResource = useMemo(() => {
const grouped = new Map<string, ScheduleItem[]>(); const grouped = new Map<string, ScheduleItem[]>();
resources.forEach((resource) => { effectiveResources.forEach((resource) => {
grouped.set(resource.id, []); grouped.set(resource.id, []);
}); });
@ -99,7 +119,7 @@ export function TimelineSchedulerComponent({
list.push(schedule); list.push(schedule);
} else { } else {
// 리소스가 없는 스케줄은 첫 번째 리소스에 할당 // 리소스가 없는 스케줄은 첫 번째 리소스에 할당
const firstResource = resources[0]; const firstResource = effectiveResources[0];
if (firstResource) { if (firstResource) {
const firstList = grouped.get(firstResource.id); const firstList = grouped.get(firstResource.id);
if (firstList) { if (firstList) {
@ -110,7 +130,7 @@ export function TimelineSchedulerComponent({
}); });
return grouped; return grouped;
}, [schedules, resources]); }, [schedules, effectiveResources]);
// 줌 레벨 변경 // 줌 레벨 변경
const handleZoomIn = useCallback(() => { const handleZoomIn = useCallback(() => {
@ -132,12 +152,12 @@ export function TimelineSchedulerComponent({
// 스케줄 클릭 핸들러 // 스케줄 클릭 핸들러
const handleScheduleClick = useCallback( const handleScheduleClick = useCallback(
(schedule: ScheduleItem) => { (schedule: ScheduleItem) => {
const resource = resources.find((r) => r.id === schedule.resourceId); const resource = effectiveResources.find((r) => r.id === schedule.resourceId);
if (resource && onScheduleClick) { if (resource && onScheduleClick) {
onScheduleClick({ schedule, resource }); onScheduleClick({ schedule, resource });
} }
}, },
[resources, onScheduleClick] [effectiveResources, onScheduleClick]
); );
// 빈 셀 클릭 핸들러 // 빈 셀 클릭 핸들러
@ -195,13 +215,13 @@ export function TimelineSchedulerComponent({
// 추가 버튼 클릭 // 추가 버튼 클릭
const handleAddClick = useCallback(() => { const handleAddClick = useCallback(() => {
if (onAddSchedule && resources.length > 0) { if (onAddSchedule && effectiveResources.length > 0) {
onAddSchedule( onAddSchedule(
resources[0].id, effectiveResources[0].id,
new Date().toISOString().split("T")[0] new Date().toISOString().split("T")[0]
); );
} }
}, [onAddSchedule, resources]); }, [onAddSchedule, effectiveResources]);
// 디자인 모드 플레이스홀더 // 디자인 모드 플레이스홀더
if (isDesignMode) { if (isDesignMode) {
@ -250,8 +270,8 @@ export function TimelineSchedulerComponent({
); );
} }
// 리소스 없음 // 리소스 없음 (스케줄도 없는 경우에만 표시)
if (resources.length === 0) { if (effectiveResources.length === 0) {
return ( return (
<div <div
className="w-full flex items-center justify-center bg-muted/10 rounded-lg" className="w-full flex items-center justify-center bg-muted/10 rounded-lg"
@ -259,8 +279,8 @@ export function TimelineSchedulerComponent({
> >
<div className="text-center text-muted-foreground"> <div className="text-center text-muted-foreground">
<Calendar className="h-8 w-8 mx-auto mb-2" /> <Calendar className="h-8 w-8 mx-auto mb-2" />
<p className="text-sm font-medium"> </p> <p className="text-sm font-medium"> </p>
<p className="text-xs mt-1"> </p> <p className="text-xs mt-1"> </p>
</div> </div>
</div> </div>
); );
@ -385,7 +405,7 @@ export function TimelineSchedulerComponent({
{/* 리소스 행들 */} {/* 리소스 행들 */}
<div> <div>
{resources.map((resource) => ( {effectiveResources.map((resource) => (
<ResourceRow <ResourceRow
key={resource.id} key={resource.id}
resource={resource} resource={resource}

View File

@ -94,17 +94,6 @@ export function useTimelineData(
page: 1, page: 1,
size: 10000, size: 10000,
autoFilter: true, autoFilter: true,
search: {
// 표시 범위 내의 스케줄만 조회
[fieldMapping.startDate]: {
value: toDateString(viewEndDate),
operator: "lte",
},
[fieldMapping.endDate]: {
value: toDateString(viewStartDate),
operator: "gte",
},
},
} }
); );