feat: 중첩 구조 지원을 위한 컴포넌트 선택 및 기본값 적용 기능 추가
- ScreenManagementService에서 company_code 저장 로직을 개선하여 SUPER_ADMIN의 경우 화면 정의에 따라 company_code를 저장하도록 수정하였습니다. - ScreenDesigner에서 중첩 구조를 지원하는 탭 내부 컴포넌트 선택 상태 및 핸들러를 추가하였습니다. - SplitPanelLayoutComponent에서 분할 패널 내부 컴포넌트의 기본값을 재귀적으로 적용하는 헬퍼 함수를 구현하였습니다. - TimelineSchedulerConfigPanel에서 필드 매핑 업데이트 로직을 개선하여 이전 형식과 새 형식을 모두 지원하도록 하였습니다. - useTimelineData 훅에서 필드 매핑을 JSON 문자열로 안정화하여 객체 참조 변경 방지를 위한 메모이제이션을 적용하였습니다.
This commit is contained in:
parent
4e7aa0c3b9
commit
7043f26ac8
|
|
@ -5040,6 +5040,18 @@ export class ScreenManagementService {
|
||||||
console.log(
|
console.log(
|
||||||
`V2 레이아웃 로드 완료: ${layout.layout_data?.components?.length || 0}개 컴포넌트`,
|
`V2 레이아웃 로드 완료: ${layout.layout_data?.components?.length || 0}개 컴포넌트`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 🐛 디버깅: finished_timeline의 fieldMapping 확인
|
||||||
|
const splitPanel = layout.layout_data?.components?.find((c: any) =>
|
||||||
|
c.url?.includes("v2-split-panel-layout")
|
||||||
|
);
|
||||||
|
const finishedTimeline = splitPanel?.overrides?.rightPanel?.components?.find(
|
||||||
|
(c: any) => c.id === "finished_timeline"
|
||||||
|
);
|
||||||
|
if (finishedTimeline) {
|
||||||
|
console.log("🐛 [Backend] finished_timeline fieldMapping:", JSON.stringify(finishedTimeline.componentConfig?.fieldMapping));
|
||||||
|
}
|
||||||
|
|
||||||
return layout.layout_data;
|
return layout.layout_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5079,16 +5091,20 @@ export class ScreenManagementService {
|
||||||
...layoutData
|
...layoutData
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SUPER_ADMIN인 경우 화면 정의의 company_code로 저장 (로드와 일관성 유지)
|
||||||
|
const saveCompanyCode = companyCode === "*" ? existingScreen.company_code : companyCode;
|
||||||
|
console.log(`저장할 company_code: ${saveCompanyCode} (원본: ${companyCode}, 화면 정의: ${existingScreen.company_code})`);
|
||||||
|
|
||||||
// UPSERT (있으면 업데이트, 없으면 삽입)
|
// UPSERT (있으면 업데이트, 없으면 삽입)
|
||||||
await query(
|
await query(
|
||||||
`INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, created_at, updated_at)
|
`INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, created_at, updated_at)
|
||||||
VALUES ($1, $2, $3, NOW(), NOW())
|
VALUES ($1, $2, $3, NOW(), NOW())
|
||||||
ON CONFLICT (screen_id, company_code)
|
ON CONFLICT (screen_id, company_code)
|
||||||
DO UPDATE SET layout_data = $3, updated_at = NOW()`,
|
DO UPDATE SET layout_data = $3, updated_at = NOW()`,
|
||||||
[screenId, companyCode, JSON.stringify(dataToSave)],
|
[screenId, saveCompanyCode, JSON.stringify(dataToSave)],
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`V2 레이아웃 저장 완료`);
|
console.log(`V2 레이아웃 저장 완료 (company_code: ${saveCompanyCode})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
# 078 마이그레이션 실행 가이드
|
||||||
|
|
||||||
|
## 실행할 파일 (순서대로)
|
||||||
|
|
||||||
|
1. **078_create_production_plan_tables.sql** - 테이블 생성
|
||||||
|
2. **078b_insert_production_plan_sample_data.sql** - 샘플 데이터
|
||||||
|
3. **078c_insert_production_plan_screen.sql** - 화면 정의 및 레이아웃
|
||||||
|
|
||||||
|
## 실행 방법
|
||||||
|
|
||||||
|
### 방법 1: psql 명령어 (터미널)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 테이블 생성
|
||||||
|
psql -h localhost -U postgres -d wace -f db/migrations/078_create_production_plan_tables.sql
|
||||||
|
|
||||||
|
# 샘플 데이터 입력
|
||||||
|
psql -h localhost -U postgres -d wace -f db/migrations/078b_insert_production_plan_sample_data.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 방법 2: DBeaver / pgAdmin에서 실행
|
||||||
|
|
||||||
|
1. DB 연결 후 SQL 에디터 열기
|
||||||
|
2. `078_create_production_plan_tables.sql` 내용 복사 & 실행
|
||||||
|
3. `078b_insert_production_plan_sample_data.sql` 내용 복사 & 실행
|
||||||
|
|
||||||
|
### 방법 3: Docker 환경
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker 컨테이너 내부에서 실행
|
||||||
|
docker exec -i <container_name> psql -U postgres -d wace < db/migrations/078_create_production_plan_tables.sql
|
||||||
|
docker exec -i <container_name> psql -U postgres -d wace < db/migrations/078b_insert_production_plan_sample_data.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## 생성되는 테이블
|
||||||
|
|
||||||
|
| 테이블명 | 설명 |
|
||||||
|
|---------|------|
|
||||||
|
| `equipment_info` | 설비 정보 마스터 |
|
||||||
|
| `production_plan_mng` | 생산계획 관리 |
|
||||||
|
| `production_plan_order_rel` | 생산계획-수주 연결 |
|
||||||
|
|
||||||
|
## 생성되는 화면
|
||||||
|
|
||||||
|
| 화면 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| 생산계획관리 (메인) | 생산계획 목록 조회/등록/수정/삭제 |
|
||||||
|
| 생산계획 등록/수정 (모달) | 생산계획 상세 입력 폼 |
|
||||||
|
|
||||||
|
## 확인 쿼리
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 테이블 생성 확인
|
||||||
|
SELECT table_name FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name IN ('equipment_info', 'production_plan_mng', 'production_plan_order_rel');
|
||||||
|
|
||||||
|
-- 샘플 데이터 확인
|
||||||
|
SELECT * FROM equipment_info;
|
||||||
|
SELECT * FROM production_plan_mng;
|
||||||
|
|
||||||
|
-- 화면 생성 확인
|
||||||
|
SELECT id, screen_name, screen_code, table_name
|
||||||
|
FROM screen_definitions
|
||||||
|
WHERE screen_code LIKE '%PP%';
|
||||||
|
|
||||||
|
-- 레이아웃 확인
|
||||||
|
SELECT sl.id, sd.screen_name, sl.layout_name
|
||||||
|
FROM screen_layouts_v2 sl
|
||||||
|
JOIN screen_definitions sd ON sl.screen_id = sd.id
|
||||||
|
WHERE sd.screen_code LIKE '%PP%';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 메뉴 연결 (수동 작업 필요)
|
||||||
|
|
||||||
|
화면 생성 후, 메뉴에 연결하려면 `menu_info` 테이블에서 해당 메뉴의 `screen_id`를 업데이트하세요:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 예시: 생산관리 > 생산계획관리 메뉴에 연결
|
||||||
|
UPDATE menu_info
|
||||||
|
SET screen_id = (SELECT id FROM screen_definitions WHERE screen_code = 'TOPSEAL_PP_MAIN')
|
||||||
|
WHERE menu_name = '생산계획관리' AND company_code = 'TOPSEAL';
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -67,6 +67,20 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const componentConfig = (component.componentConfig || {}) as SplitPanelLayoutConfig;
|
const componentConfig = (component.componentConfig || {}) as SplitPanelLayoutConfig;
|
||||||
|
|
||||||
|
// 🐛 디버깅: 로드 시 rightPanel.components 확인
|
||||||
|
const rightComps = componentConfig.rightPanel?.components || [];
|
||||||
|
const finishedTimeline = rightComps.find((c: any) => c.id === "finished_timeline");
|
||||||
|
if (finishedTimeline) {
|
||||||
|
const fm = finishedTimeline.componentConfig?.fieldMapping;
|
||||||
|
console.log("🔍 [SplitPanelLayout] finished_timeline fieldMapping:", {
|
||||||
|
componentId: finishedTimeline.id,
|
||||||
|
fieldMapping: fm ? JSON.stringify(fm) : "undefined",
|
||||||
|
fieldMappingKeys: fm ? Object.keys(fm) : [],
|
||||||
|
fieldMappingId: fm?.id,
|
||||||
|
fullComponentConfig: JSON.stringify(finishedTimeline.componentConfig || {}, null, 2),
|
||||||
|
});
|
||||||
|
}
|
||||||
// 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만 사용 가능)
|
// 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만 사용 가능)
|
||||||
const companyCode = (props as any).companyCode as string | undefined;
|
const companyCode = (props as any).companyCode as string | undefined;
|
||||||
|
|
||||||
|
|
@ -231,6 +245,33 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
[component, componentConfig, onUpdateComponent]
|
[component, componentConfig, onUpdateComponent]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 🆕 중첩된 컴포넌트 업데이트 핸들러 (탭 컴포넌트 내부 위치 변경 등)
|
||||||
|
const handleNestedComponentUpdate = useCallback(
|
||||||
|
(panelSide: "left" | "right", compId: string, updatedNestedComponent: any) => {
|
||||||
|
if (!onUpdateComponent) return;
|
||||||
|
|
||||||
|
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||||
|
const panelConfig = componentConfig[panelKey] || {};
|
||||||
|
const panelComponents = panelConfig.components || [];
|
||||||
|
|
||||||
|
const updatedComponents = panelComponents.map((c: PanelInlineComponent) =>
|
||||||
|
c.id === compId ? { ...c, ...updatedNestedComponent, id: c.id } : c
|
||||||
|
);
|
||||||
|
|
||||||
|
onUpdateComponent({
|
||||||
|
...component,
|
||||||
|
componentConfig: {
|
||||||
|
...componentConfig,
|
||||||
|
[panelKey]: {
|
||||||
|
...panelConfig,
|
||||||
|
components: updatedComponents,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[component, componentConfig, onUpdateComponent]
|
||||||
|
);
|
||||||
|
|
||||||
// 🆕 커스텀 모드: 드래그 시작 핸들러
|
// 🆕 커스텀 모드: 드래그 시작 핸들러
|
||||||
const handlePanelDragStart = useCallback(
|
const handlePanelDragStart = useCallback(
|
||||||
(e: React.MouseEvent, panelSide: "left" | "right", comp: PanelInlineComponent) => {
|
(e: React.MouseEvent, panelSide: "left" | "right", comp: PanelInlineComponent) => {
|
||||||
|
|
@ -2293,6 +2334,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
)}
|
)}
|
||||||
<CardContent className="flex-1 overflow-auto p-4">
|
<CardContent className="flex-1 overflow-auto p-4">
|
||||||
{/* 좌측 데이터 목록/테이블/커스텀 */}
|
{/* 좌측 데이터 목록/테이블/커스텀 */}
|
||||||
|
{console.log("🔍 [SplitPanel] 왼쪽 패널 displayMode:", componentConfig.leftPanel?.displayMode, "isDesignMode:", isDesignMode)}
|
||||||
{componentConfig.leftPanel?.displayMode === "custom" ? (
|
{componentConfig.leftPanel?.displayMode === "custom" ? (
|
||||||
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치
|
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치
|
||||||
<div
|
<div
|
||||||
|
|
@ -2398,11 +2440,42 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
height: displayHeight,
|
height: displayHeight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="pointer-events-none h-full w-full">
|
{/* 🆕 컨테이너 컴포넌트(탭, 분할 패널)는 드롭 이벤트를 받을 수 있어야 함 */}
|
||||||
|
<div className={cn(
|
||||||
|
"h-full w-full",
|
||||||
|
// 탭/분할 패널 같은 컨테이너 컴포넌트는 pointer-events 활성화
|
||||||
|
(comp.componentType === "v2-tabs-widget" ||
|
||||||
|
comp.componentType === "tabs-widget" ||
|
||||||
|
comp.componentType === "v2-split-panel-layout" ||
|
||||||
|
comp.componentType === "split-panel-layout")
|
||||||
|
? ""
|
||||||
|
: "pointer-events-none"
|
||||||
|
)}>
|
||||||
<DynamicComponentRenderer
|
<DynamicComponentRenderer
|
||||||
component={componentData as any}
|
component={componentData as any}
|
||||||
isDesignMode={true}
|
isDesignMode={true}
|
||||||
formData={{}}
|
formData={{}}
|
||||||
|
// 🆕 중첩된 컴포넌트 업데이트 핸들러 전달
|
||||||
|
onUpdateComponent={(updatedComp: any) => {
|
||||||
|
handleNestedComponentUpdate("left", comp.id, updatedComp);
|
||||||
|
}}
|
||||||
|
// 🆕 중첩된 탭 내부 컴포넌트 선택 핸들러 - 부모 분할 패널 정보 포함
|
||||||
|
onSelectTabComponent={(tabId: string, compId: string, tabComp: any) => {
|
||||||
|
console.log("🔍 [SplitPanel-Left] onSelectTabComponent 호출:", { tabId, compId, tabComp, parentSplitPanelId: component.id });
|
||||||
|
// 부모 분할 패널 정보와 함께 전역 이벤트 발생
|
||||||
|
const event = new CustomEvent("nested-tab-component-select", {
|
||||||
|
detail: {
|
||||||
|
tabsComponentId: comp.id,
|
||||||
|
tabId,
|
||||||
|
componentId: compId,
|
||||||
|
component: tabComp,
|
||||||
|
parentSplitPanelId: component.id,
|
||||||
|
parentPanelSide: "left",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
}}
|
||||||
|
selectedTabComponentId={undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -3079,11 +3152,42 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
height: displayHeight,
|
height: displayHeight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="pointer-events-none h-full w-full">
|
{/* 🆕 컨테이너 컴포넌트(탭, 분할 패널)는 드롭 이벤트를 받을 수 있어야 함 */}
|
||||||
|
<div className={cn(
|
||||||
|
"h-full w-full",
|
||||||
|
// 탭/분할 패널 같은 컨테이너 컴포넌트는 pointer-events 활성화
|
||||||
|
(comp.componentType === "v2-tabs-widget" ||
|
||||||
|
comp.componentType === "tabs-widget" ||
|
||||||
|
comp.componentType === "v2-split-panel-layout" ||
|
||||||
|
comp.componentType === "split-panel-layout")
|
||||||
|
? ""
|
||||||
|
: "pointer-events-none"
|
||||||
|
)}>
|
||||||
<DynamicComponentRenderer
|
<DynamicComponentRenderer
|
||||||
component={componentData as any}
|
component={componentData as any}
|
||||||
isDesignMode={true}
|
isDesignMode={true}
|
||||||
formData={{}}
|
formData={{}}
|
||||||
|
// 🆕 중첩된 컴포넌트 업데이트 핸들러 전달
|
||||||
|
onUpdateComponent={(updatedComp: any) => {
|
||||||
|
handleNestedComponentUpdate("right", comp.id, updatedComp);
|
||||||
|
}}
|
||||||
|
// 🆕 중첩된 탭 내부 컴포넌트 선택 핸들러 - 부모 분할 패널 정보 포함
|
||||||
|
onSelectTabComponent={(tabId: string, compId: string, tabComp: any) => {
|
||||||
|
console.log("🔍 [SplitPanel-Right] onSelectTabComponent 호출:", { tabId, compId, tabComp, parentSplitPanelId: component.id });
|
||||||
|
// 부모 분할 패널 정보와 함께 전역 이벤트 발생
|
||||||
|
const event = new CustomEvent("nested-tab-component-select", {
|
||||||
|
detail: {
|
||||||
|
tabsComponentId: comp.id,
|
||||||
|
tabId,
|
||||||
|
componentId: compId,
|
||||||
|
component: tabComp,
|
||||||
|
parentSplitPanelId: component.id,
|
||||||
|
parentPanelSide: "right",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
}}
|
||||||
|
selectedTabComponentId={undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -365,6 +365,7 @@ const TabsDesignEditor: React.FC<{
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
console.log("🔍 [탭 컴포넌트] 클릭:", { activeTabId, compId: comp.id, hasOnSelectTabComponent: !!onSelectTabComponent });
|
||||||
onSelectTabComponent?.(activeTabId, comp.id, comp);
|
onSelectTabComponent?.(activeTabId, comp.id, comp);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,13 @@ export function TimelineSchedulerConfigPanel({
|
||||||
config,
|
config,
|
||||||
onChange,
|
onChange,
|
||||||
}: TimelineSchedulerConfigPanelProps) {
|
}: TimelineSchedulerConfigPanelProps) {
|
||||||
|
// 🐛 디버깅: 받은 config 출력
|
||||||
|
console.log("🐛 [TimelineSchedulerConfigPanel] config:", {
|
||||||
|
selectedTable: config.selectedTable,
|
||||||
|
fieldMapping: config.fieldMapping,
|
||||||
|
fieldMappingKeys: config.fieldMapping ? Object.keys(config.fieldMapping) : [],
|
||||||
|
});
|
||||||
|
|
||||||
const [tables, setTables] = useState<TableInfo[]>([]);
|
const [tables, setTables] = useState<TableInfo[]>([]);
|
||||||
const [tableColumns, setTableColumns] = useState<ColumnInfo[]>([]);
|
const [tableColumns, setTableColumns] = useState<ColumnInfo[]>([]);
|
||||||
const [resourceColumns, setResourceColumns] = useState<ColumnInfo[]>([]);
|
const [resourceColumns, setResourceColumns] = useState<ColumnInfo[]>([]);
|
||||||
|
|
@ -141,13 +148,40 @@ export function TimelineSchedulerConfigPanel({
|
||||||
onChange({ ...config, ...updates });
|
onChange({ ...config, ...updates });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 필드 매핑 업데이트
|
// 🆕 이전 형식(idField)과 새 형식(id) 모두 지원하는 헬퍼 함수
|
||||||
|
const getFieldMappingValue = (newKey: string, oldKey: string): string => {
|
||||||
|
const mapping = config.fieldMapping as Record<string, any> | undefined;
|
||||||
|
if (!mapping) return "";
|
||||||
|
return mapping[newKey] || mapping[oldKey] || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필드 매핑 업데이트 (새 형식으로 저장하고, 이전 형식 키 삭제)
|
||||||
const updateFieldMapping = (field: string, value: string) => {
|
const updateFieldMapping = (field: string, value: string) => {
|
||||||
|
const currentMapping = { ...config.fieldMapping } as Record<string, any>;
|
||||||
|
|
||||||
|
// 이전 형식 키 매핑
|
||||||
|
const oldKeyMap: Record<string, string> = {
|
||||||
|
id: "idField",
|
||||||
|
resourceId: "resourceIdField",
|
||||||
|
title: "titleField",
|
||||||
|
startDate: "startDateField",
|
||||||
|
endDate: "endDateField",
|
||||||
|
status: "statusField",
|
||||||
|
progress: "progressField",
|
||||||
|
color: "colorField",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 새 형식으로 저장
|
||||||
|
currentMapping[field] = value;
|
||||||
|
|
||||||
|
// 이전 형식 키가 있으면 삭제
|
||||||
|
const oldKey = oldKeyMap[field];
|
||||||
|
if (oldKey && currentMapping[oldKey]) {
|
||||||
|
delete currentMapping[oldKey];
|
||||||
|
}
|
||||||
|
|
||||||
updateConfig({
|
updateConfig({
|
||||||
fieldMapping: {
|
fieldMapping: currentMapping,
|
||||||
...config.fieldMapping,
|
|
||||||
[field]: value,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -345,7 +379,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">ID</Label>
|
<Label className="text-[10px]">ID</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.fieldMapping?.id || ""}
|
value={getFieldMappingValue("id", "idField")}
|
||||||
onValueChange={(v) => updateFieldMapping("id", v)}
|
onValueChange={(v) => updateFieldMapping("id", v)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 text-xs">
|
<SelectTrigger className="h-7 text-xs">
|
||||||
|
|
@ -365,7 +399,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">리소스 ID</Label>
|
<Label className="text-[10px]">리소스 ID</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.fieldMapping?.resourceId || ""}
|
value={getFieldMappingValue("resourceId", "resourceIdField")}
|
||||||
onValueChange={(v) => updateFieldMapping("resourceId", v)}
|
onValueChange={(v) => updateFieldMapping("resourceId", v)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 text-xs">
|
<SelectTrigger className="h-7 text-xs">
|
||||||
|
|
@ -385,7 +419,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">제목</Label>
|
<Label className="text-[10px]">제목</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.fieldMapping?.title || ""}
|
value={getFieldMappingValue("title", "titleField")}
|
||||||
onValueChange={(v) => updateFieldMapping("title", v)}
|
onValueChange={(v) => updateFieldMapping("title", v)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 text-xs">
|
<SelectTrigger className="h-7 text-xs">
|
||||||
|
|
@ -405,7 +439,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">시작일</Label>
|
<Label className="text-[10px]">시작일</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.fieldMapping?.startDate || ""}
|
value={getFieldMappingValue("startDate", "startDateField")}
|
||||||
onValueChange={(v) => updateFieldMapping("startDate", v)}
|
onValueChange={(v) => updateFieldMapping("startDate", v)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 text-xs">
|
<SelectTrigger className="h-7 text-xs">
|
||||||
|
|
@ -425,7 +459,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">종료일</Label>
|
<Label className="text-[10px]">종료일</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.fieldMapping?.endDate || ""}
|
value={getFieldMappingValue("endDate", "endDateField")}
|
||||||
onValueChange={(v) => updateFieldMapping("endDate", v)}
|
onValueChange={(v) => updateFieldMapping("endDate", v)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 text-xs">
|
<SelectTrigger className="h-7 text-xs">
|
||||||
|
|
@ -445,7 +479,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">상태 (선택)</Label>
|
<Label className="text-[10px]">상태 (선택)</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.fieldMapping?.status || "__none__"}
|
value={getFieldMappingValue("status", "statusField") || "__none__"}
|
||||||
onValueChange={(v) => updateFieldMapping("status", v === "__none__" ? "" : v)}
|
onValueChange={(v) => updateFieldMapping("status", v === "__none__" ? "" : v)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 text-xs">
|
<SelectTrigger className="h-7 text-xs">
|
||||||
|
|
|
||||||
|
|
@ -67,10 +67,38 @@ export function useTimelineData(
|
||||||
|
|
||||||
const resourceTableName = config.resourceTable;
|
const resourceTableName = config.resourceTable;
|
||||||
|
|
||||||
// 필드 매핑
|
// 필드 매핑을 JSON 문자열로 안정화 (객체 참조 변경 방지)
|
||||||
const fieldMapping = config.fieldMapping || defaultTimelineSchedulerConfig.fieldMapping!;
|
const fieldMappingKey = useMemo(() => {
|
||||||
const resourceFieldMapping =
|
return JSON.stringify(config.fieldMapping || {});
|
||||||
config.resourceFieldMapping || defaultTimelineSchedulerConfig.resourceFieldMapping!;
|
}, [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 () => {
|
const fetchSchedules = useCallback(async () => {
|
||||||
|
|
@ -125,13 +153,10 @@ export function useTimelineData(
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [
|
// fieldMappingKey를 의존성으로 사용하여 객체 참조 변경 방지
|
||||||
tableName,
|
// viewStartDate, viewEndDate는 API 호출에 사용되지 않으므로 제거
|
||||||
externalSchedules,
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
fieldMapping,
|
}, [tableName, externalSchedules, fieldMappingKey]);
|
||||||
viewStartDate,
|
|
||||||
viewEndDate,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 리소스 데이터 로드
|
// 리소스 데이터 로드
|
||||||
const fetchResources = useCallback(async () => {
|
const fetchResources = useCallback(async () => {
|
||||||
|
|
@ -173,7 +198,9 @@ export function useTimelineData(
|
||||||
console.error("리소스 로드 오류:", err);
|
console.error("리소스 로드 오류:", err);
|
||||||
setResources([]);
|
setResources([]);
|
||||||
}
|
}
|
||||||
}, [resourceTableName, externalResources, resourceFieldMapping]);
|
// resourceFieldMappingKey를 의존성으로 사용하여 객체 참조 변경 방지
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [resourceTableName, externalResources, resourceFieldMappingKey]);
|
||||||
|
|
||||||
// 초기 로드
|
// 초기 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,105 @@ interface LegacyLayoutData {
|
||||||
metadata?: any;
|
metadata?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 중첩 컴포넌트 기본값 적용 헬퍼 함수 (재귀적)
|
||||||
|
// ============================================
|
||||||
|
function applyDefaultsToNestedComponents(components: any[]): any[] {
|
||||||
|
if (!Array.isArray(components)) return components;
|
||||||
|
|
||||||
|
return components.map((nestedComp: any) => {
|
||||||
|
if (!nestedComp) return nestedComp;
|
||||||
|
|
||||||
|
// 중첩 컴포넌트의 타입 확인 (componentType 또는 url에서 추출)
|
||||||
|
let nestedComponentType = nestedComp.componentType;
|
||||||
|
if (!nestedComponentType && nestedComp.url) {
|
||||||
|
nestedComponentType = getComponentTypeFromUrl(nestedComp.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 결과 객체 초기화 (원본 복사)
|
||||||
|
let result = { ...nestedComp };
|
||||||
|
|
||||||
|
// 🆕 탭 위젯인 경우 재귀적으로 탭 내부 컴포넌트도 처리
|
||||||
|
if (nestedComponentType === "v2-tabs-widget") {
|
||||||
|
const config = result.componentConfig || {};
|
||||||
|
if (config.tabs && Array.isArray(config.tabs)) {
|
||||||
|
result.componentConfig = {
|
||||||
|
...config,
|
||||||
|
tabs: config.tabs.map((tab: any) => {
|
||||||
|
if (tab?.components && Array.isArray(tab.components)) {
|
||||||
|
return {
|
||||||
|
...tab,
|
||||||
|
components: applyDefaultsToNestedComponents(tab.components),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return tab;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 분할 패널인 경우 재귀적으로 내부 컴포넌트도 처리
|
||||||
|
if (nestedComponentType === "v2-split-panel-layout") {
|
||||||
|
const config = result.componentConfig || {};
|
||||||
|
result.componentConfig = {
|
||||||
|
...config,
|
||||||
|
leftPanel: config.leftPanel ? {
|
||||||
|
...config.leftPanel,
|
||||||
|
components: applyDefaultsToNestedComponents(config.leftPanel.components || []),
|
||||||
|
} : config.leftPanel,
|
||||||
|
rightPanel: config.rightPanel ? {
|
||||||
|
...config.rightPanel,
|
||||||
|
components: applyDefaultsToNestedComponents(config.rightPanel.components || []),
|
||||||
|
} : config.rightPanel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컴포넌트 타입이 없으면 그대로 반환
|
||||||
|
if (!nestedComponentType) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 중첩 컴포넌트의 기본값 가져오기
|
||||||
|
const nestedDefaults = getDefaultsByUrl(`registry://${nestedComponentType}`);
|
||||||
|
|
||||||
|
// componentConfig가 있으면 기본값과 병합
|
||||||
|
if (result.componentConfig && Object.keys(nestedDefaults).length > 0) {
|
||||||
|
const mergedNestedConfig = mergeComponentConfig(nestedDefaults, result.componentConfig);
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
componentConfig: mergedNestedConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 분할 패널 내부 컴포넌트 기본값 적용
|
||||||
|
// ============================================
|
||||||
|
function applyDefaultsToSplitPanelComponents(mergedConfig: Record<string, any>): Record<string, any> {
|
||||||
|
const result = { ...mergedConfig };
|
||||||
|
|
||||||
|
// leftPanel.components 처리
|
||||||
|
if (result.leftPanel?.components) {
|
||||||
|
result.leftPanel = {
|
||||||
|
...result.leftPanel,
|
||||||
|
components: applyDefaultsToNestedComponents(result.leftPanel.components),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// rightPanel.components 처리
|
||||||
|
if (result.rightPanel?.components) {
|
||||||
|
result.rightPanel = {
|
||||||
|
...result.rightPanel,
|
||||||
|
components: applyDefaultsToNestedComponents(result.rightPanel.components),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// V2 → Legacy 변환 (로드 시)
|
// V2 → Legacy 변환 (로드 시)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -44,7 +143,28 @@ export function convertV2ToLegacy(v2Layout: LayoutV2 | null): LegacyLayoutData |
|
||||||
const components: LegacyComponentData[] = v2Layout.components.map((comp) => {
|
const components: LegacyComponentData[] = v2Layout.components.map((comp) => {
|
||||||
const componentType = getComponentTypeFromUrl(comp.url);
|
const componentType = getComponentTypeFromUrl(comp.url);
|
||||||
const defaults = getDefaultsByUrl(comp.url);
|
const defaults = getDefaultsByUrl(comp.url);
|
||||||
const mergedConfig = mergeComponentConfig(defaults, comp.overrides);
|
let mergedConfig = mergeComponentConfig(defaults, comp.overrides);
|
||||||
|
|
||||||
|
// 🆕 분할 패널인 경우 내부 컴포넌트에도 기본값 적용
|
||||||
|
if (componentType === "v2-split-panel-layout") {
|
||||||
|
mergedConfig = applyDefaultsToSplitPanelComponents(mergedConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 탭 위젯인 경우 탭 내부 컴포넌트에도 기본값 적용
|
||||||
|
if (componentType === "v2-tabs-widget" && mergedConfig.tabs) {
|
||||||
|
mergedConfig = {
|
||||||
|
...mergedConfig,
|
||||||
|
tabs: mergedConfig.tabs.map((tab: any) => {
|
||||||
|
if (tab?.components) {
|
||||||
|
return {
|
||||||
|
...tab,
|
||||||
|
components: applyDefaultsToNestedComponents(tab.components),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return tab;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 🆕 overrides에서 상위 레벨 속성들 추출
|
// 🆕 overrides에서 상위 레벨 속성들 추출
|
||||||
const overrides = comp.overrides || {};
|
const overrides = comp.overrides || {};
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
|
@ -301,7 +300,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
|
|
@ -335,7 +333,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/accessibility": "^3.1.1",
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
|
@ -2666,7 +2663,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz",
|
||||||
"integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==",
|
"integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.17.8",
|
"@babel/runtime": "^7.17.8",
|
||||||
"@types/react-reconciler": "^0.32.0",
|
"@types/react-reconciler": "^0.32.0",
|
||||||
|
|
@ -3320,7 +3316,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz",
|
||||||
"integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==",
|
"integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/query-core": "5.90.6"
|
"@tanstack/query-core": "5.90.6"
|
||||||
},
|
},
|
||||||
|
|
@ -3388,7 +3383,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.1.tgz",
|
||||||
"integrity": "sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==",
|
"integrity": "sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
|
@ -3702,7 +3696,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.1.tgz",
|
||||||
"integrity": "sha512-ijKo3+kIjALthYsnBmkRXAuw2Tswd9gd7BUR5OMfIcjGp8v576vKxOxrRfuYiUM78GPt//P0sVc1WV82H5N0PQ==",
|
"integrity": "sha512-ijKo3+kIjALthYsnBmkRXAuw2Tswd9gd7BUR5OMfIcjGp8v576vKxOxrRfuYiUM78GPt//P0sVc1WV82H5N0PQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-changeset": "^2.3.0",
|
"prosemirror-changeset": "^2.3.0",
|
||||||
"prosemirror-collab": "^1.3.1",
|
"prosemirror-collab": "^1.3.1",
|
||||||
|
|
@ -6203,7 +6196,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
|
|
@ -6214,7 +6206,6 @@
|
||||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
|
|
@ -6248,7 +6239,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz",
|
||||||
"integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==",
|
"integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
"@tweenjs/tween.js": "~23.1.3",
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
|
|
@ -6331,7 +6321,6 @@
|
||||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.46.2",
|
"@typescript-eslint/scope-manager": "8.46.2",
|
||||||
"@typescript-eslint/types": "8.46.2",
|
"@typescript-eslint/types": "8.46.2",
|
||||||
|
|
@ -6964,7 +6953,6 @@
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -8115,8 +8103,7 @@
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/d3": {
|
"node_modules/d3": {
|
||||||
"version": "7.9.0",
|
"version": "7.9.0",
|
||||||
|
|
@ -8438,7 +8425,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -9198,7 +9184,6 @@
|
||||||
"integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
|
"integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
|
|
@ -9287,7 +9272,6 @@
|
||||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"eslint-config-prettier": "bin/cli.js"
|
"eslint-config-prettier": "bin/cli.js"
|
||||||
},
|
},
|
||||||
|
|
@ -9389,7 +9373,6 @@
|
||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
|
|
@ -10540,7 +10523,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||||
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/immer"
|
"url": "https://opencollective.com/immer"
|
||||||
|
|
@ -11322,8 +11304,7 @@
|
||||||
"version": "1.9.4",
|
"version": "1.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
|
|
@ -12622,7 +12603,6 @@
|
||||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
|
|
@ -12918,7 +12898,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"orderedmap": "^2.0.0"
|
"orderedmap": "^2.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -12948,7 +12927,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.0.0",
|
"prosemirror-model": "^1.0.0",
|
||||||
"prosemirror-transform": "^1.0.0",
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
|
@ -12997,7 +12975,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz",
|
||||||
"integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==",
|
"integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.20.0",
|
"prosemirror-model": "^1.20.0",
|
||||||
"prosemirror-state": "^1.0.0",
|
"prosemirror-state": "^1.0.0",
|
||||||
|
|
@ -13124,7 +13101,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
|
@ -13194,7 +13170,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
|
|
@ -13213,7 +13188,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
|
||||||
"integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==",
|
"integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -13540,7 +13514,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/use-sync-external-store": "^0.0.6",
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"use-sync-external-store": "^1.4.0"
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
|
@ -13563,8 +13536,7 @@
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/recharts/node_modules/redux-thunk": {
|
"node_modules/recharts/node_modules/redux-thunk": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
|
|
@ -14588,8 +14560,7 @@
|
||||||
"version": "0.180.0",
|
"version": "0.180.0",
|
||||||
"resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
|
"resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
|
||||||
"integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==",
|
"integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/three-mesh-bvh": {
|
"node_modules/three-mesh-bvh": {
|
||||||
"version": "0.8.3",
|
"version": "0.8.3",
|
||||||
|
|
@ -14677,7 +14648,6 @@
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -15026,7 +14996,6 @@
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue