diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 8cd6d4e0..1eb3ce7e 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -5040,6 +5040,18 @@ export class ScreenManagementService { console.log( `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; } @@ -5079,16 +5091,20 @@ export class ScreenManagementService { ...layoutData }; + // SUPER_ADMIN인 경우 화면 정의의 company_code로 저장 (로드와 일관성 유지) + const saveCompanyCode = companyCode === "*" ? existingScreen.company_code : companyCode; + console.log(`저장할 company_code: ${saveCompanyCode} (원본: ${companyCode}, 화면 정의: ${existingScreen.company_code})`); + // UPSERT (있으면 업데이트, 없으면 삽입) await query( `INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, created_at, updated_at) VALUES ($1, $2, $3, NOW(), NOW()) ON CONFLICT (screen_id, company_code) 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})`); } } diff --git a/db/migrations/RUN_078_MIGRATION.md b/db/migrations/RUN_078_MIGRATION.md new file mode 100644 index 00000000..05669d0c --- /dev/null +++ b/db/migrations/RUN_078_MIGRATION.md @@ -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 psql -U postgres -d wace < db/migrations/078_create_production_plan_tables.sql +docker exec -i 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'; +``` diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index a0ed5574..c73e6598 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -169,12 +169,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const [selectedComponent, setSelectedComponent] = useState(null); - // 🆕 탭 내부 컴포넌트 선택 상태 + // 🆕 탭 내부 컴포넌트 선택 상태 (중첩 구조 지원) const [selectedTabComponentInfo, setSelectedTabComponentInfo] = useState<{ tabsComponentId: string; // 탭 컴포넌트 ID tabId: string; // 탭 ID componentId: string; // 탭 내부 컴포넌트 ID component: any; // 탭 내부 컴포넌트 데이터 + // 🆕 중첩 구조용: 부모 분할 패널 정보 + parentSplitPanelId?: string | null; + parentPanelSide?: "left" | "right" | null; } | null>(null); // 🆕 분할 패널 내부 컴포넌트 선택 상태 @@ -203,9 +206,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [openPanel], ); - // 🆕 탭 내부 컴포넌트 선택 핸들러 + // 🆕 탭 내부 컴포넌트 선택 핸들러 (중첩 구조 지원) const handleSelectTabComponent = useCallback( - (tabsComponentId: string, tabId: string, compId: string, comp: any) => { + ( + tabsComponentId: string, + tabId: string, + compId: string, + comp: any, + // 🆕 중첩 구조용: 부모 분할 패널 정보 (선택적) + parentSplitPanelId?: string | null, + parentPanelSide?: "left" | "right" | null + ) => { if (!compId) { // 탭 영역 빈 공간 클릭 시 선택 해제 setSelectedTabComponentInfo(null); @@ -217,6 +228,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU tabId, componentId: compId, component: comp, + parentSplitPanelId: parentSplitPanelId || null, + parentPanelSide: parentPanelSide || null, }); // 탭 내부 컴포넌트 선택 시 일반 컴포넌트/분할 패널 내부 컴포넌트 선택 해제 setSelectedComponent(null); @@ -229,6 +242,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // 🆕 분할 패널 내부 컴포넌트 선택 핸들러 const handleSelectPanelComponent = useCallback( (splitPanelId: string, panelSide: "left" | "right", compId: string, comp: any) => { + // 🐛 디버깅: 전달받은 comp 확인 + console.log("🐛 [handleSelectPanelComponent] comp:", { + compId, + componentType: comp?.componentType, + selectedTable: comp?.componentConfig?.selectedTable, + fieldMapping: comp?.componentConfig?.fieldMapping, + fieldMappingKeys: comp?.componentConfig?.fieldMapping ? Object.keys(comp.componentConfig.fieldMapping) : [], + }); + if (!compId) { // 패널 영역 빈 공간 클릭 시 선택 해제 setSelectedPanelComponentInfo(null); @@ -249,6 +271,38 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [openPanel], ); + // 🆕 중첩된 탭 컴포넌트 선택 이벤트 리스너 (분할 패널 안의 탭 안의 컴포넌트) + useEffect(() => { + const handleNestedTabComponentSelect = (event: CustomEvent) => { + const { tabsComponentId, tabId, componentId, component, parentSplitPanelId, parentPanelSide } = event.detail; + + if (!componentId) { + setSelectedTabComponentInfo(null); + return; + } + + console.log("🎯 중첩된 탭 컴포넌트 선택:", event.detail); + + setSelectedTabComponentInfo({ + tabsComponentId, + tabId, + componentId, + component, + parentSplitPanelId, + parentPanelSide, + }); + setSelectedComponent(null); + setSelectedPanelComponentInfo(null); + openPanel("v2"); + }; + + window.addEventListener("nested-tab-component-select", handleNestedTabComponentSelect as EventListener); + + return () => { + window.removeEventListener("nested-tab-component-select", handleNestedTabComponentSelect as EventListener); + }; + }, [openPanel]); + // 클립보드 상태 const [clipboard, setClipboard] = useState([]); @@ -453,18 +507,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [historyIndex], ); - // 🆕 탭 내부 컴포넌트 설정 업데이트 핸들러 + // 🆕 탭 내부 컴포넌트 설정 업데이트 핸들러 (중첩 구조 지원) const handleUpdateTabComponentConfig = useCallback( (path: string, value: any) => { if (!selectedTabComponentInfo) return; - const { tabsComponentId, tabId, componentId } = selectedTabComponentInfo; + const { tabsComponentId, tabId, componentId, parentSplitPanelId, parentPanelSide } = selectedTabComponentInfo; - setLayout((prevLayout) => { - const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); - if (!tabsComponent) return prevLayout; - - const currentConfig = (tabsComponent as any).componentConfig || {}; + // 탭 컴포넌트 업데이트 함수 (재사용) + const updateTabsComponent = (tabsComponent: any) => { + const currentConfig = tabsComponent.componentConfig || {}; const tabs = currentConfig.tabs || []; const updatedTabs = tabs.map((tab: any) => { @@ -473,34 +525,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU ...tab, components: (tab.components || []).map((comp: any) => { if (comp.id === componentId) { - // path에 따라 적절한 속성 업데이트 if (path.startsWith("componentConfig.")) { const configPath = path.replace("componentConfig.", ""); return { ...comp, - componentConfig: { - ...comp.componentConfig, - [configPath]: value, - }, + componentConfig: { ...comp.componentConfig, [configPath]: value }, }; } else if (path.startsWith("style.")) { const stylePath = path.replace("style.", ""); - return { - ...comp, - style: { - ...comp.style, - [stylePath]: value, - }, - }; + return { ...comp, style: { ...comp.style, [stylePath]: value } }; } else if (path.startsWith("size.")) { const sizePath = path.replace("size.", ""); - return { - ...comp, - size: { - ...comp.size, - [sizePath]: value, - }, - }; + return { ...comp, size: { ...comp.size, [sizePath]: value } }; } else { return { ...comp, [path]: value }; } @@ -512,29 +548,72 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU return tab; }); - const updatedComponent = { - ...tabsComponent, - componentConfig: { - ...currentConfig, - tabs: updatedTabs, - }, - }; - - const newLayout = { - ...prevLayout, - components: prevLayout.components.map((c) => - c.id === tabsComponentId ? updatedComponent : c - ), - }; + return { ...tabsComponent, componentConfig: { ...currentConfig, tabs: updatedTabs } }; + }; + + setLayout((prevLayout) => { + let newLayout; + let updatedTabs; + + if (parentSplitPanelId && parentPanelSide) { + // 🆕 중첩 구조: 분할 패널 안의 탭 업데이트 + newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => { + if (c.id === parentSplitPanelId) { + const splitConfig = (c as any).componentConfig || {}; + const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = splitConfig[panelKey] || {}; + const panelComponents = panelConfig.components || []; + + const tabsComponent = panelComponents.find((pc: any) => pc.id === tabsComponentId); + if (!tabsComponent) return c; + + const updatedTabsComponent = updateTabsComponent(tabsComponent); + updatedTabs = updatedTabsComponent.componentConfig.tabs; + + return { + ...c, + componentConfig: { + ...splitConfig, + [panelKey]: { + ...panelConfig, + components: panelComponents.map((pc: any) => + pc.id === tabsComponentId ? updatedTabsComponent : pc + ), + }, + }, + }; + } + return c; + }), + }; + } else { + // 일반 구조: 최상위 탭 업데이트 + const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); + if (!tabsComponent) return prevLayout; + + const updatedTabsComponent = updateTabsComponent(tabsComponent); + updatedTabs = updatedTabsComponent.componentConfig.tabs; + + newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => + c.id === tabsComponentId ? updatedTabsComponent : c + ), + }; + } // 선택된 컴포넌트 정보도 업데이트 - const updatedComp = updatedTabs - .find((t: any) => t.id === tabId) - ?.components?.find((c: any) => c.id === componentId); - if (updatedComp) { - setSelectedTabComponentInfo((prev) => - prev ? { ...prev, component: updatedComp } : null - ); + if (updatedTabs) { + const updatedComp = updatedTabs + .find((t: any) => t.id === tabId) + ?.components?.find((c: any) => c.id === componentId); + if (updatedComp) { + setSelectedTabComponentInfo((prev) => + prev ? { ...prev, component: updatedComp } : null + ); + } } return newLayout; @@ -1290,7 +1369,38 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU let response: any; if (USE_V2_API) { const v2Response = await screenApi.getLayoutV2(selectedScreen.screenId); + + // 🐛 디버깅: API 응답에서 fieldMapping.id 확인 + const splitPanelInV2 = v2Response?.components?.find((c: any) => + c.url?.includes("v2-split-panel-layout") + ); + const finishedTimelineInV2 = splitPanelInV2?.overrides?.rightPanel?.components?.find( + (c: any) => c.id === "finished_timeline" + ); + console.log("🐛 [API 응답 RAW] finished_timeline:", JSON.stringify(finishedTimelineInV2, null, 2)); + console.log("🐛 [API 응답] finished_timeline fieldMapping:", { + fieldMapping: JSON.stringify(finishedTimelineInV2?.componentConfig?.fieldMapping), + fieldMappingKeys: finishedTimelineInV2?.componentConfig?.fieldMapping ? Object.keys(finishedTimelineInV2?.componentConfig?.fieldMapping) : [], + hasId: !!finishedTimelineInV2?.componentConfig?.fieldMapping?.id, + idValue: finishedTimelineInV2?.componentConfig?.fieldMapping?.id, + }); + response = v2Response ? convertV2ToLegacy(v2Response) : null; + + // 🐛 디버깅: convertV2ToLegacy 후 fieldMapping.id 확인 + const splitPanelInLegacy = response?.components?.find((c: any) => + c.componentType === "v2-split-panel-layout" + ); + const finishedTimelineInLegacy = splitPanelInLegacy?.componentConfig?.rightPanel?.components?.find( + (c: any) => c.id === "finished_timeline" + ); + console.log("🐛 [변환 후] finished_timeline fieldMapping:", { + fieldMapping: JSON.stringify(finishedTimelineInLegacy?.componentConfig?.fieldMapping), + fieldMappingKeys: finishedTimelineInLegacy?.componentConfig?.fieldMapping ? Object.keys(finishedTimelineInLegacy?.componentConfig?.fieldMapping) : [], + hasId: !!finishedTimelineInLegacy?.componentConfig?.fieldMapping?.id, + idValue: finishedTimelineInLegacy?.componentConfig?.fieldMapping?.id, + }); + console.log("📦 V2 레이아웃 로드:", v2Response?.components?.length || 0, "개 컴포넌트"); } else { response = await screenApi.getLayout(selectedScreen.screenId); @@ -1731,9 +1841,75 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU })), }); + // 🔍 디버그: 분할 패널 내부의 탭 및 컴포넌트 설정 확인 + const splitPanels = layoutWithResolution.components.filter( + (c: any) => c.componentType === "v2-split-panel-layout" || c.componentType === "split-panel-layout" + ); + splitPanels.forEach((sp: any) => { + console.log("🔍 [저장] 분할 패널 설정:", { + id: sp.id, + leftPanel: sp.componentConfig?.leftPanel, + rightPanel: sp.componentConfig?.rightPanel, + }); + // 🆕 분할 패널 내 모든 컴포넌트의 componentConfig 로그 + const rightComponents = sp.componentConfig?.rightPanel?.components || []; + console.log("🔍 [저장] 오른쪽 패널 컴포넌트들:", rightComponents.map((c: any) => ({ + id: c.id, + componentType: c.componentType, + hasComponentConfig: !!c.componentConfig, + componentConfig: JSON.parse(JSON.stringify(c.componentConfig || {})), + }))); + // 왼쪽 패널의 탭 컴포넌트 확인 + const leftTabs = sp.componentConfig?.leftPanel?.components?.filter( + (c: any) => c.componentType === "v2-tabs-widget" + ); + leftTabs?.forEach((tabWidget: any) => { + console.log("🔍 [저장] 왼쪽 패널 탭 위젯 전체 componentConfig:", { + tabWidgetId: tabWidget.id, + fullComponentConfig: JSON.parse(JSON.stringify(tabWidget.componentConfig || {})), + }); + console.log("🔍 [저장] 왼쪽 패널 탭 내부 컴포넌트:", { + tabId: tabWidget.id, + tabs: tabWidget.componentConfig?.tabs?.map((t: any) => ({ + id: t.id, + label: t.label, + componentsCount: t.components?.length || 0, + components: t.components, + })), + }); + }); + // 오른쪽 패널의 탭 컴포넌트 확인 + const rightTabs = sp.componentConfig?.rightPanel?.components?.filter( + (c: any) => c.componentType === "v2-tabs-widget" + ); + rightTabs?.forEach((tabWidget: any) => { + console.log("🔍 [저장] 오른쪽 패널 탭 위젯 전체 componentConfig:", { + tabWidgetId: tabWidget.id, + fullComponentConfig: JSON.parse(JSON.stringify(tabWidget.componentConfig || {})), + }); + console.log("🔍 [저장] 오른쪽 패널 탭 내부 컴포넌트:", { + tabId: tabWidget.id, + tabs: tabWidget.componentConfig?.tabs?.map((t: any) => ({ + id: t.id, + label: t.label, + componentsCount: t.components?.length || 0, + components: t.components, + })), + }); + }); + }); + // V2 API 사용 여부에 따라 분기 if (USE_V2_API) { const v2Layout = convertLegacyToV2(layoutWithResolution); + console.log("📦 V2 변환 결과 (분할 패널 overrides):", v2Layout.components + .filter((c: any) => c.url?.includes("split-panel")) + .map((c: any) => ({ + id: c.id, + url: c.url, + overrides: c.overrides, + })) + ); await screenApi.saveLayoutV2(selectedScreen.screenId, v2Layout); console.log("📦 V2 레이아웃 저장:", v2Layout.components.length, "개 컴포넌트"); } else { @@ -2470,13 +2646,47 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } } - // 🎯 탭 컨테이너 내부 드롭 처리 + // 🎯 탭 컨테이너 내부 드롭 처리 (중첩 구조 지원) const tabsContainer = dropTarget.closest('[data-tabs-container="true"]'); if (tabsContainer) { const containerId = tabsContainer.getAttribute("data-component-id"); const activeTabId = tabsContainer.getAttribute("data-active-tab-id"); if (containerId && activeTabId) { - const targetComponent = layout.components.find((c) => c.id === containerId); + // 1. 먼저 최상위 레이아웃에서 탭 컴포넌트 찾기 + let targetComponent = layout.components.find((c) => c.id === containerId); + let parentSplitPanelId: string | null = null; + let parentPanelSide: "left" | "right" | null = null; + + // 2. 최상위에 없으면 분할 패널 안에서 중첩된 탭 찾기 + if (!targetComponent) { + for (const comp of layout.components) { + const compType = (comp as any)?.componentType; + if (compType === "split-panel-layout" || compType === "v2-split-panel-layout") { + const config = (comp as any).componentConfig || {}; + + // 좌측 패널에서 찾기 + const leftComponents = config.leftPanel?.components || []; + const foundInLeft = leftComponents.find((c: any) => c.id === containerId); + if (foundInLeft) { + targetComponent = foundInLeft; + parentSplitPanelId = comp.id; + parentPanelSide = "left"; + break; + } + + // 우측 패널에서 찾기 + const rightComponents = config.rightPanel?.components || []; + const foundInRight = rightComponents.find((c: any) => c.id === containerId); + if (foundInRight) { + targetComponent = foundInRight; + parentSplitPanelId = comp.id; + parentPanelSide = "right"; + break; + } + } + } + } + const compType = (targetComponent as any)?.componentType; if (targetComponent && (compType === "tabs-widget" || compType === "v2-tabs-widget")) { const currentConfig = (targetComponent as any).componentConfig || {}; @@ -2487,16 +2697,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const dropX = (e.clientX - tabContentRect.left) / zoomLevel; const dropY = (e.clientY - tabContentRect.top) / zoomLevel; - // 새 컴포넌트 생성 - 드롭된 컴포넌트의 id를 그대로 사용 - // component.id는 ComponentDefinition의 id (예: "v2-table-list", "v2-button-primary") + // 새 컴포넌트 생성 const componentType = component.id || component.componentType || "v2-text-display"; console.log("🎯 탭에 컴포넌트 드롭:", { componentId: component.id, componentType: componentType, componentName: component.name, - defaultConfig: component.defaultConfig, - defaultSize: component.defaultSize, + isNested: !!parentSplitPanelId, + parentSplitPanelId, + parentPanelSide, + // 🆕 위치 디버깅 + clientX: e.clientX, + clientY: e.clientY, + tabContentRect: { left: tabContentRect.left, top: tabContentRect.top }, + zoomLevel, + calculatedPosition: { x: dropX, y: dropY }, }); const newTabComponent = { @@ -2519,7 +2735,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU return tab; }); - const updatedComponent = { + const updatedTabsComponent = { ...targetComponent, componentConfig: { ...currentConfig, @@ -2527,16 +2743,49 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }, }; - const newLayout = { - ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedComponent : c - ), - }; + let newLayout; + + if (parentSplitPanelId && parentPanelSide) { + // 🆕 중첩 구조: 분할 패널 안의 탭 업데이트 + newLayout = { + ...layout, + components: layout.components.map((c) => { + if (c.id === parentSplitPanelId) { + const splitConfig = (c as any).componentConfig || {}; + const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = splitConfig[panelKey] || {}; + const panelComponents = panelConfig.components || []; + + return { + ...c, + componentConfig: { + ...splitConfig, + [panelKey]: { + ...panelConfig, + components: panelComponents.map((pc: any) => + pc.id === containerId ? updatedTabsComponent : pc + ), + }, + }, + }; + } + return c; + }), + }; + toast.success("컴포넌트가 중첩된 탭에 추가되었습니다"); + } else { + // 일반 구조: 최상위 탭 업데이트 + newLayout = { + ...layout, + components: layout.components.map((c) => + c.id === containerId ? updatedTabsComponent : c + ), + }; + toast.success("컴포넌트가 탭에 추가되었습니다"); + } setLayout(newLayout); saveToHistory(newLayout); - toast.success("컴포넌트가 탭에 추가되었습니다"); return; // 탭 컨테이너 처리 완료 } } @@ -2992,13 +3241,47 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } } - // 🎯 탭 컨테이너 내부에 컬럼 드롭 시 처리 + // 🎯 탭 컨테이너 내부에 컬럼 드롭 시 처리 (중첩 구조 지원) const tabsContainer = dropTarget.closest('[data-tabs-container="true"]'); if (tabsContainer && type === "column" && column) { const containerId = tabsContainer.getAttribute("data-component-id"); const activeTabId = tabsContainer.getAttribute("data-active-tab-id"); if (containerId && activeTabId) { - const targetComponent = layout.components.find((c) => c.id === containerId); + // 1. 먼저 최상위 레이아웃에서 탭 컴포넌트 찾기 + let targetComponent = layout.components.find((c) => c.id === containerId); + let parentSplitPanelId: string | null = null; + let parentPanelSide: "left" | "right" | null = null; + + // 2. 최상위에 없으면 분할 패널 안에서 중첩된 탭 찾기 + if (!targetComponent) { + for (const comp of layout.components) { + const compType = (comp as any)?.componentType; + if (compType === "split-panel-layout" || compType === "v2-split-panel-layout") { + const config = (comp as any).componentConfig || {}; + + // 좌측 패널에서 찾기 + const leftComponents = config.leftPanel?.components || []; + const foundInLeft = leftComponents.find((c: any) => c.id === containerId); + if (foundInLeft) { + targetComponent = foundInLeft; + parentSplitPanelId = comp.id; + parentPanelSide = "left"; + break; + } + + // 우측 패널에서 찾기 + const rightComponents = config.rightPanel?.components || []; + const foundInRight = rightComponents.find((c: any) => c.id === containerId); + if (foundInRight) { + targetComponent = foundInRight; + parentSplitPanelId = comp.id; + parentPanelSide = "right"; + break; + } + } + } + } + const compType = (targetComponent as any)?.componentType; if (targetComponent && (compType === "tabs-widget" || compType === "v2-tabs-widget")) { const currentConfig = (targetComponent as any).componentConfig || {}; @@ -3047,17 +3330,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const newTabComponent = { id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - componentType: v2Mapping.componentType, // v2-input, v2-select 등 + componentType: v2Mapping.componentType, label: column.columnLabel || column.columnName, position: { x: Math.max(0, dropX), y: Math.max(0, dropY) }, size: componentSize, - inputType: column.inputType || column.widgetType, // 🆕 inputType 저장 (설정 패널용) - widgetType: column.widgetType, // 🆕 widgetType 저장 + inputType: column.inputType || column.widgetType, + widgetType: column.widgetType, componentConfig: { - ...v2Mapping.componentConfig, // V2 컴포넌트 기본 설정 + ...v2Mapping.componentConfig, columnName: column.columnName, tableName: column.tableName, - inputType: column.inputType || column.widgetType, // 🆕 componentConfig에도 저장 + inputType: column.inputType || column.widgetType, }, }; @@ -3072,7 +3355,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU return tab; }); - const updatedComponent = { + const updatedTabsComponent = { ...targetComponent, componentConfig: { ...currentConfig, @@ -3080,16 +3363,49 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }, }; - const newLayout = { - ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedComponent : c - ), - }; + let newLayout; + + if (parentSplitPanelId && parentPanelSide) { + // 🆕 중첩 구조: 분할 패널 안의 탭 업데이트 + newLayout = { + ...layout, + components: layout.components.map((c) => { + if (c.id === parentSplitPanelId) { + const splitConfig = (c as any).componentConfig || {}; + const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = splitConfig[panelKey] || {}; + const panelComponents = panelConfig.components || []; + + return { + ...c, + componentConfig: { + ...splitConfig, + [panelKey]: { + ...panelConfig, + components: panelComponents.map((pc: any) => + pc.id === containerId ? updatedTabsComponent : pc + ), + }, + }, + }; + } + return c; + }), + }; + toast.success("컬럼이 중첩된 탭에 추가되었습니다"); + } else { + // 일반 구조: 최상위 탭 업데이트 + newLayout = { + ...layout, + components: layout.components.map((c) => + c.id === containerId ? updatedTabsComponent : c + ), + }; + toast.success("컬럼이 탭에 추가되었습니다"); + } setLayout(newLayout); saveToHistory(newLayout); - toast.success("컬럼이 탭에 추가되었습니다"); return; } } @@ -3809,7 +4125,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // 드래그 종료 const endDrag = useCallback((mouseEvent?: MouseEvent) => { if (dragState.isDragging && dragState.draggedComponent) { - // 🎯 탭 컨테이너로의 드롭 처리 (기존 컴포넌트 이동) + // 🎯 탭 컨테이너로의 드롭 처리 (기존 컴포넌트 이동, 중첩 구조 지원) if (mouseEvent) { const dropTarget = document.elementFromPoint(mouseEvent.clientX, mouseEvent.clientY) as HTMLElement; const tabsContainer = dropTarget?.closest('[data-tabs-container="true"]'); @@ -3819,7 +4135,41 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const activeTabId = tabsContainer.getAttribute("data-active-tab-id"); if (containerId && activeTabId) { - const targetComponent = layout.components.find((c) => c.id === containerId); + // 1. 먼저 최상위 레이아웃에서 탭 컴포넌트 찾기 + let targetComponent = layout.components.find((c) => c.id === containerId); + let parentSplitPanelId: string | null = null; + let parentPanelSide: "left" | "right" | null = null; + + // 2. 최상위에 없으면 분할 패널 안에서 중첩된 탭 찾기 + if (!targetComponent) { + for (const comp of layout.components) { + const compType = (comp as any)?.componentType; + if (compType === "split-panel-layout" || compType === "v2-split-panel-layout") { + const config = (comp as any).componentConfig || {}; + + // 좌측 패널에서 찾기 + const leftComponents = config.leftPanel?.components || []; + const foundInLeft = leftComponents.find((c: any) => c.id === containerId); + if (foundInLeft) { + targetComponent = foundInLeft; + parentSplitPanelId = comp.id; + parentPanelSide = "left"; + break; + } + + // 우측 패널에서 찾기 + const rightComponents = config.rightPanel?.components || []; + const foundInRight = rightComponents.find((c: any) => c.id === containerId); + if (foundInRight) { + targetComponent = foundInRight; + parentSplitPanelId = comp.id; + parentPanelSide = "right"; + break; + } + } + } + } + const compType = (targetComponent as any)?.componentType; // 자기 자신을 자신에게 드롭하는 것 방지 @@ -3859,29 +4209,65 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU return tab; }); - // 탭 컴포넌트 업데이트 + 원래 컴포넌트 캔버스에서 제거 - const newLayout = { - ...layout, - components: layout.components - .filter((c) => c.id !== dragState.draggedComponent) // 캔버스에서 제거 - .map((c) => { - if (c.id === containerId) { - return { - ...c, - componentConfig: { - ...currentConfig, - tabs: updatedTabs, - }, - }; - } - return c; - }), + const updatedTabsComponent = { + ...targetComponent, + componentConfig: { + ...currentConfig, + tabs: updatedTabs, + }, }; + let newLayout; + + if (parentSplitPanelId && parentPanelSide) { + // 🆕 중첩 구조: 분할 패널 안의 탭 업데이트 + newLayout = { + ...layout, + components: layout.components + .filter((c) => c.id !== dragState.draggedComponent) + .map((c) => { + if (c.id === parentSplitPanelId) { + const splitConfig = (c as any).componentConfig || {}; + const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = splitConfig[panelKey] || {}; + const panelComponents = panelConfig.components || []; + + return { + ...c, + componentConfig: { + ...splitConfig, + [panelKey]: { + ...panelConfig, + components: panelComponents.map((pc: any) => + pc.id === containerId ? updatedTabsComponent : pc + ), + }, + }, + }; + } + return c; + }), + }; + toast.success("컴포넌트가 중첩된 탭으로 이동되었습니다"); + } else { + // 일반 구조: 최상위 탭 업데이트 + newLayout = { + ...layout, + components: layout.components + .filter((c) => c.id !== dragState.draggedComponent) + .map((c) => { + if (c.id === containerId) { + return updatedTabsComponent; + } + return c; + }), + }; + toast.success("컴포넌트가 탭으로 이동되었습니다"); + } + setLayout(newLayout); saveToHistory(newLayout); setSelectedComponent(null); - toast.success("컴포넌트가 탭으로 이동되었습니다"); // 드래그 상태 초기화 후 종료 setDragState({ @@ -5189,15 +5575,33 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU style: tabComp.style || {}, } as ComponentData; - // 탭 내부 컴포넌트용 속성 업데이트 핸들러 + // 탭 내부 컴포넌트용 속성 업데이트 핸들러 (중첩 구조 지원) const updateTabComponentProperty = (componentId: string, path: string, value: any) => { - const { tabsComponentId, tabId } = selectedTabComponentInfo; + const { tabsComponentId, tabId, parentSplitPanelId, parentPanelSide } = selectedTabComponentInfo; - setLayout((prevLayout) => { - const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); - if (!tabsComponent) return prevLayout; - - const currentConfig = (tabsComponent as any).componentConfig || {}; + console.log("🔧 updateTabComponentProperty 호출:", { componentId, path, value, parentSplitPanelId, parentPanelSide }); + + // 🆕 안전한 깊은 경로 업데이트 헬퍼 함수 + const setNestedValue = (obj: any, pathStr: string, val: any): any => { + // 깊은 복사로 시작 + const result = JSON.parse(JSON.stringify(obj)); + const parts = pathStr.split("."); + let current = result; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!current[part] || typeof current[part] !== "object") { + current[part] = {}; + } + current = current[part]; + } + current[parts[parts.length - 1]] = val; + return result; + }; + + // 탭 컴포넌트 업데이트 함수 + const updateTabsComponent = (tabsComponent: any) => { + const currentConfig = JSON.parse(JSON.stringify(tabsComponent.componentConfig || {})); const tabs = currentConfig.tabs || []; const updatedTabs = tabs.map((tab: any) => { @@ -5207,62 +5611,98 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU components: (tab.components || []).map((comp: any) => { if (comp.id !== componentId) return comp; - // path를 파싱하여 중첩 속성 업데이트 - const pathParts = path.split("."); - const newComp = { ...comp }; - let current: any = newComp; - - for (let i = 0; i < pathParts.length - 1; i++) { - const part = pathParts[i]; - if (!current[part]) { - current[part] = {}; - } else { - current[part] = { ...current[part] }; - } - current = current[part]; - } - current[pathParts[pathParts.length - 1]] = value; - - return newComp; + // 🆕 안전한 깊은 경로 업데이트 사용 + const updatedComp = setNestedValue(comp, path, value); + console.log("🔧 컴포넌트 업데이트 결과:", updatedComp); + return updatedComp; }), }; } return tab; }); - const updatedComponent = { + return { ...tabsComponent, componentConfig: { ...currentConfig, tabs: updatedTabs }, }; - - // 선택된 컴포넌트 정보 업데이트 - const updatedComp = updatedTabs - .find((t: any) => t.id === tabId) - ?.components?.find((c: any) => c.id === componentId); - if (updatedComp) { - setSelectedTabComponentInfo((prev) => - prev ? { ...prev, component: updatedComp } : null - ); + }; + + setLayout((prevLayout) => { + let newLayout; + let updatedTabs; + + if (parentSplitPanelId && parentPanelSide) { + // 🆕 중첩 구조: 분할 패널 안의 탭 업데이트 + newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => { + if (c.id === parentSplitPanelId) { + const splitConfig = (c as any).componentConfig || {}; + const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = splitConfig[panelKey] || {}; + const panelComponents = panelConfig.components || []; + + const tabsComponent = panelComponents.find((pc: any) => pc.id === tabsComponentId); + if (!tabsComponent) return c; + + const updatedTabsComponent = updateTabsComponent(tabsComponent); + updatedTabs = updatedTabsComponent.componentConfig.tabs; + + return { + ...c, + componentConfig: { + ...splitConfig, + [panelKey]: { + ...panelConfig, + components: panelComponents.map((pc: any) => + pc.id === tabsComponentId ? updatedTabsComponent : pc + ), + }, + }, + }; + } + return c; + }), + }; + } else { + // 일반 구조: 최상위 탭 업데이트 + const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); + if (!tabsComponent) return prevLayout; + + const updatedTabsComponent = updateTabsComponent(tabsComponent); + updatedTabs = updatedTabsComponent.componentConfig.tabs; + + newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => + c.id === tabsComponentId ? updatedTabsComponent : c + ), + }; } - return { - ...prevLayout, - components: prevLayout.components.map((c) => - c.id === tabsComponentId ? updatedComponent : c - ), - }; + // 선택된 컴포넌트 정보 업데이트 + if (updatedTabs) { + const updatedComp = updatedTabs + .find((t: any) => t.id === tabId) + ?.components?.find((c: any) => c.id === componentId); + if (updatedComp) { + setSelectedTabComponentInfo((prev) => + prev ? { ...prev, component: updatedComp } : null + ); + } + } + + return newLayout; }); }; - // 탭 내부 컴포넌트 삭제 핸들러 + // 탭 내부 컴포넌트 삭제 핸들러 (중첩 구조 지원) const deleteTabComponent = (componentId: string) => { - const { tabsComponentId, tabId } = selectedTabComponentInfo; + const { tabsComponentId, tabId, parentSplitPanelId, parentPanelSide } = selectedTabComponentInfo; - setLayout((prevLayout) => { - const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); - if (!tabsComponent) return prevLayout; - - const currentConfig = (tabsComponent as any).componentConfig || {}; + // 탭 컴포넌트에서 특정 컴포넌트 삭제 + const updateTabsComponentForDelete = (tabsComponent: any) => { + const currentConfig = tabsComponent.componentConfig || {}; const tabs = currentConfig.tabs || []; const updatedTabs = tabs.map((tab: any) => { @@ -5275,19 +5715,64 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU return tab; }); - const updatedComponent = { + return { ...tabsComponent, componentConfig: { ...currentConfig, tabs: updatedTabs }, }; + }; + + setLayout((prevLayout) => { + let newLayout; + + if (parentSplitPanelId && parentPanelSide) { + // 🆕 중첩 구조: 분할 패널 안의 탭에서 삭제 + newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => { + if (c.id === parentSplitPanelId) { + const splitConfig = (c as any).componentConfig || {}; + const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = splitConfig[panelKey] || {}; + const panelComponents = panelConfig.components || []; + + const tabsComponent = panelComponents.find((pc: any) => pc.id === tabsComponentId); + if (!tabsComponent) return c; + + const updatedTabsComponent = updateTabsComponentForDelete(tabsComponent); + + return { + ...c, + componentConfig: { + ...splitConfig, + [panelKey]: { + ...panelConfig, + components: panelComponents.map((pc: any) => + pc.id === tabsComponentId ? updatedTabsComponent : pc + ), + }, + }, + }; + } + return c; + }), + }; + } else { + // 일반 구조: 최상위 탭에서 삭제 + const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); + if (!tabsComponent) return prevLayout; + + const updatedTabsComponent = updateTabsComponentForDelete(tabsComponent); + + newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => + c.id === tabsComponentId ? updatedTabsComponent : c + ), + }; + } setSelectedTabComponentInfo(null); - - return { - ...prevLayout, - components: prevLayout.components.map((c) => - c.id === tabsComponentId ? updatedComponent : c - ), - }; + return newLayout; }); }; @@ -5343,6 +5828,25 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const { splitPanelId, panelSide } = selectedPanelComponentInfo; const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel"; + console.log("🔧 updatePanelComponentProperty 호출:", { componentId, path, value, splitPanelId, panelSide }); + + // 🆕 안전한 깊은 경로 업데이트 헬퍼 함수 + const setNestedValue = (obj: any, pathStr: string, val: any): any => { + const result = JSON.parse(JSON.stringify(obj)); + const parts = pathStr.split("."); + let current = result; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!current[part] || typeof current[part] !== "object") { + current[part] = {}; + } + current = current[part]; + } + current[parts[parts.length - 1]] = val; + return result; + }; + setLayout((prevLayout) => { const splitPanelComponent = prevLayout.components.find((c) => c.id === splitPanelId); if (!splitPanelComponent) return prevLayout; @@ -5355,23 +5859,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const targetCompIndex = components.findIndex((c: any) => c.id === componentId); if (targetCompIndex === -1) return prevLayout; - // 컴포넌트 속성 업데이트 + // 🆕 안전한 깊은 경로 업데이트 사용 const targetComp = components[targetCompIndex]; const updatedComp = path === "style" ? { ...targetComp, style: value } - : path.includes(".") - ? (() => { - const parts = path.split("."); - let obj = { ...targetComp }; - let current: any = obj; - for (let i = 0; i < parts.length - 1; i++) { - current[parts[i]] = { ...current[parts[i]] }; - current = current[parts[i]]; - } - current[parts[parts.length - 1]] = value; - return obj; - })() - : { ...targetComp, [path]: value }; + : setNestedValue(targetComp, path, value); + + console.log("🔧 분할 패널 컴포넌트 업데이트 결과:", updatedComp); const updatedComponents = [ ...components.slice(0, targetCompIndex), diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index 8a5898e0..ae77e105 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -67,6 +67,20 @@ export const SplitPanelLayoutComponent: React.FC ...props }) => { 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; @@ -231,6 +245,33 @@ export const SplitPanelLayoutComponent: React.FC [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( (e: React.MouseEvent, panelSide: "left" | "right", comp: PanelInlineComponent) => { @@ -2293,6 +2334,7 @@ export const SplitPanelLayoutComponent: React.FC )} {/* 좌측 데이터 목록/테이블/커스텀 */} + {console.log("🔍 [SplitPanel] 왼쪽 패널 displayMode:", componentConfig.leftPanel?.displayMode, "isDesignMode:", isDesignMode)} {componentConfig.leftPanel?.displayMode === "custom" ? ( // 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치
height: displayHeight, }} > -
+ {/* 🆕 컨테이너 컴포넌트(탭, 분할 패널)는 드롭 이벤트를 받을 수 있어야 함 */} +
{ + 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} />
@@ -3079,11 +3152,42 @@ export const SplitPanelLayoutComponent: React.FC height: displayHeight, }} > -
+ {/* 🆕 컨테이너 컴포넌트(탭, 분할 패널)는 드롭 이벤트를 받을 수 있어야 함 */} +
{ + 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} />
diff --git a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx index 91965aa8..8645fed9 100644 --- a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx +++ b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx @@ -365,6 +365,7 @@ const TabsDesignEditor: React.FC<{ }} onClick={(e) => { e.stopPropagation(); + console.log("🔍 [탭 컴포넌트] 클릭:", { activeTabId, compId: comp.id, hasOnSelectTabComponent: !!onSelectTabComponent }); onSelectTabComponent?.(activeTabId, comp.id, comp); }} > diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx index 3371d425..f62c3b34 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx @@ -56,6 +56,13 @@ export function TimelineSchedulerConfigPanel({ config, onChange, }: TimelineSchedulerConfigPanelProps) { + // 🐛 디버깅: 받은 config 출력 + console.log("🐛 [TimelineSchedulerConfigPanel] config:", { + selectedTable: config.selectedTable, + fieldMapping: config.fieldMapping, + fieldMappingKeys: config.fieldMapping ? Object.keys(config.fieldMapping) : [], + }); + const [tables, setTables] = useState([]); const [tableColumns, setTableColumns] = useState([]); const [resourceColumns, setResourceColumns] = useState([]); @@ -141,13 +148,40 @@ export function TimelineSchedulerConfigPanel({ onChange({ ...config, ...updates }); }; - // 필드 매핑 업데이트 + // 🆕 이전 형식(idField)과 새 형식(id) 모두 지원하는 헬퍼 함수 + const getFieldMappingValue = (newKey: string, oldKey: string): string => { + const mapping = config.fieldMapping as Record | undefined; + if (!mapping) return ""; + return mapping[newKey] || mapping[oldKey] || ""; + }; + + // 필드 매핑 업데이트 (새 형식으로 저장하고, 이전 형식 키 삭제) const updateFieldMapping = (field: string, value: string) => { + const currentMapping = { ...config.fieldMapping } as Record; + + // 이전 형식 키 매핑 + const oldKeyMap: Record = { + 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({ - fieldMapping: { - ...config.fieldMapping, - [field]: value, - }, + fieldMapping: currentMapping, }); }; @@ -345,7 +379,7 @@ export function TimelineSchedulerConfigPanel({
updateFieldMapping("resourceId", v)} > @@ -385,7 +419,7 @@ export function TimelineSchedulerConfigPanel({
updateFieldMapping("startDate", v)} > @@ -425,7 +459,7 @@ export function TimelineSchedulerConfigPanel({
updateFieldMapping("status", v === "__none__" ? "" : v)} > diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts b/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts index 6039cce4..2e56f5c2 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts +++ b/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts @@ -67,10 +67,38 @@ export function useTimelineData( const resourceTableName = config.resourceTable; - // 필드 매핑 - const fieldMapping = config.fieldMapping || defaultTimelineSchedulerConfig.fieldMapping!; - const resourceFieldMapping = - config.resourceFieldMapping || defaultTimelineSchedulerConfig.resourceFieldMapping!; + // 필드 매핑을 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 () => { @@ -125,13 +153,10 @@ export function useTimelineData( } finally { setIsLoading(false); } - }, [ - tableName, - externalSchedules, - fieldMapping, - viewStartDate, - viewEndDate, - ]); + // fieldMappingKey를 의존성으로 사용하여 객체 참조 변경 방지 + // viewStartDate, viewEndDate는 API 호출에 사용되지 않으므로 제거 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tableName, externalSchedules, fieldMappingKey]); // 리소스 데이터 로드 const fetchResources = useCallback(async () => { @@ -173,7 +198,9 @@ export function useTimelineData( console.error("리소스 로드 오류:", err); setResources([]); } - }, [resourceTableName, externalResources, resourceFieldMapping]); + // resourceFieldMappingKey를 의존성으로 사용하여 객체 참조 변경 방지 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [resourceTableName, externalResources, resourceFieldMappingKey]); // 초기 로드 useEffect(() => { diff --git a/frontend/lib/utils/layoutV2Converter.ts b/frontend/lib/utils/layoutV2Converter.ts index 2c65189e..c6cf9522 100644 --- a/frontend/lib/utils/layoutV2Converter.ts +++ b/frontend/lib/utils/layoutV2Converter.ts @@ -33,6 +33,105 @@ interface LegacyLayoutData { 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): Record { + 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 변환 (로드 시) // ============================================ @@ -44,7 +143,28 @@ export function convertV2ToLegacy(v2Layout: LayoutV2 | null): LegacyLayoutData | const components: LegacyComponentData[] = v2Layout.components.map((comp) => { const componentType = getComponentTypeFromUrl(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에서 상위 레벨 속성들 추출 const overrides = comp.overrides || {}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1558865e..12709ddf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -259,7 +259,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -301,7 +300,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -335,7 +333,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -2666,7 +2663,6 @@ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz", "integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/react-reconciler": "^0.32.0", @@ -3320,7 +3316,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz", "integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.90.6" }, @@ -3388,7 +3383,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.1.tgz", "integrity": "sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -3702,7 +3696,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.1.tgz", "integrity": "sha512-ijKo3+kIjALthYsnBmkRXAuw2Tswd9gd7BUR5OMfIcjGp8v576vKxOxrRfuYiUM78GPt//P0sVc1WV82H5N0PQ==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -6203,7 +6196,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -6214,7 +6206,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6248,7 +6239,6 @@ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz", "integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==", "license": "MIT", - "peer": true, "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", @@ -6331,7 +6321,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -6964,7 +6953,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8115,8 +8103,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3": { "version": "7.9.0", @@ -8438,7 +8425,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -9198,7 +9184,6 @@ "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9287,7 +9272,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9389,7 +9373,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10540,7 +10523,6 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -11322,8 +11304,7 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/levn": { "version": "0.4.1", @@ -12622,7 +12603,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12918,7 +12898,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", - "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -12948,7 +12927,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-model": "^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", "integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -13124,7 +13101,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13194,7 +13170,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -13213,7 +13188,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -13540,7 +13514,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -13563,8 +13536,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/recharts/node_modules/redux-thunk": { "version": "3.1.0", @@ -14588,8 +14560,7 @@ "version": "0.180.0", "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/three-mesh-bvh": { "version": "0.8.3", @@ -14677,7 +14648,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15026,7 +14996,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver"