diff --git a/backend-node/src/services/scheduleService.ts b/backend-node/src/services/scheduleService.ts index 62eecb59..0ce378d5 100644 --- a/backend-node/src/services/scheduleService.ts +++ b/backend-node/src/services/scheduleService.ts @@ -119,17 +119,14 @@ export class ScheduleService { companyCode ); toCreate.push(...schedules); - totalQty += schedules.reduce( - (sum, s) => sum + (s.plan_qty || 0), - 0 - ); + totalQty += schedules.reduce((sum, s) => sum + (s.plan_qty || 0), 0); } // 3. 기존 스케줄 조회 (삭제 대상) // 그룹 키에서 리소스 ID만 추출 ("리소스ID|날짜" 형식에서 "리소스ID"만) - const resourceIds = [...new Set( - Object.keys(groupedData).map((key) => key.split("|")[0]) - )]; + const resourceIds = [ + ...new Set(Object.keys(groupedData).map((key) => key.split("|")[0])), + ]; const toDelete = await this.getExistingSchedules( config.scheduleType, resourceIds, @@ -369,7 +366,9 @@ export class ScheduleService { let groupKey = resourceId; if (dueDateField && item[dueDateField]) { // 날짜를 YYYY-MM-DD 형식으로 정규화 - const dueDate = new Date(item[dueDateField]).toISOString().split("T")[0]; + const dueDate = new Date(item[dueDateField]) + .toISOString() + .split("T")[0]; groupKey = `${resourceId}|${dueDate}`; } @@ -403,8 +402,7 @@ export class ScheduleService { // 그룹 키에서 리소스ID와 기준일 분리 const [resourceId, groupDueDate] = groupKey.split("|"); - const resourceName = - items[0]?.[config.resource.nameField] || resourceId; + const resourceName = items[0]?.[config.resource.nameField] || resourceId; // 총 수량 계산 const totalQty = items.reduce((sum, item) => { @@ -469,7 +467,9 @@ export class ScheduleService { plan_qty: totalQty, status: "PLANNED", source_table: config.source.tableName, - source_id: items.map((i) => i.id || i.order_no || i.sales_order_no).join(","), + source_id: items + .map((i) => i.id || i.order_no || i.sales_order_no) + .join(","), source_group_key: resourceId, metadata: { sourceCount: items.length, diff --git a/docs/v2-sales-order-modal-layout.json b/docs/v2-sales-order-modal-layout.json index 6c8287e0..13e929e0 100644 --- a/docs/v2-sales-order-modal-layout.json +++ b/docs/v2-sales-order-modal-layout.json @@ -302,13 +302,29 @@ { "field": "spec", "header": "규격", "width": 100 }, { "field": "unit", "header": "단위", "width": 80 }, { "field": "qty", "header": "수량", "width": 100, "editable": true }, - { "field": "unit_price", "header": "단가", "width": 100, "editable": true }, + { + "field": "unit_price", + "header": "단가", + "width": 100, + "editable": true + }, { "field": "amount", "header": "금액", "width": 100 }, - { "field": "due_date", "header": "납기일", "width": 120, "editable": true } + { + "field": "due_date", + "header": "납기일", + "width": 120, + "editable": true + } ], "modal": { "sourceTable": "item_info", - "sourceColumns": ["part_code", "part_name", "spec", "material", "unit_price"], + "sourceColumns": [ + "part_code", + "part_name", + "spec", + "material", + "unit_price" + ], "filterCondition": {} }, "features": { diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 14230b14..9f043adf 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -161,7 +161,6 @@ function ScreenViewPage() { // V2 레이아웃: Zod 기반 변환 (기본값 병합) const convertedLayout = convertV2ToLegacy(v2Response); if (convertedLayout) { - console.log("📦 V2 레이아웃 로드 (Zod 기반):", v2Response.components?.length || 0, "개 컴포넌트"); setLayout({ ...convertedLayout, screenResolution: v2Response.screenResolution || convertedLayout.screenResolution, @@ -227,7 +226,6 @@ function ScreenViewPage() { ); if (hasTableWidget) { - console.log("📋 테이블 위젯이 있어 자동 로드 건너뜀 (행 선택으로 데이터 로드)"); return; } diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 746c85f1..49fb3355 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -372,7 +372,6 @@ export const ScreenModal: React.FC = ({ className }) => { // V2 레이아웃이 없으면 기존 API로 fallback if (!layoutData) { - console.log("📦 V2 레이아웃 없음, 기존 API로 fallback"); layoutData = await screenApi.getLayout(screenId); } @@ -385,8 +384,6 @@ export const ScreenModal: React.FC = ({ className }) => { const groupByColumnsParam = urlParams.get("groupByColumns"); const primaryKeyColumn = urlParams.get("primaryKeyColumn"); // 🆕 Primary Key 컬럼명 - console.log("📋 URL 파라미터 확인:", { mode, editId, tableName, groupByColumnsParam, primaryKeyColumn }); - // 수정 모드이고 editId가 있으면 해당 레코드 조회 if (mode === "edit" && editId && tableName) { try { @@ -411,14 +408,8 @@ export const ScreenModal: React.FC = ({ className }) => { // 🆕 Primary Key 컬럼명 전달 (백엔드 자동 감지 실패 시 사용) if (primaryKeyColumn) { params.primaryKeyColumn = primaryKeyColumn; - console.log("✅ [ScreenModal] primaryKeyColumn을 params에 추가:", primaryKeyColumn); } - console.log("📡 [ScreenModal] 실제 API 요청:", { - url: `/data/${tableName}/${editId}`, - params, - }); - const apiResponse = await apiClient.get(`/data/${tableName}/${editId}`, { params }); const response = apiResponse.data; @@ -751,9 +742,6 @@ export const ScreenModal: React.FC = ({ className }) => { .map(([bottom, gap]) => ({ bottom, gap })) .sort((a, b) => a.bottom - b.bottom); - console.log('🔍 [Y조정] visibleRanges:', visibleRanges.filter(r => r.bottom - r.y > 50).map(r => `${r.y}~${r.bottom}`)); - console.log('🔍 [Y조정] hiddenGaps:', sortedGaps); - // 각 컴포넌트의 y 조정값 계산 함수 const getYOffset = (compY: number, compId?: string) => { let offset = 0; @@ -763,9 +751,6 @@ export const ScreenModal: React.FC = ({ className }) => { offset += gap; } } - if (offset > 0 && compId) { - console.log(`🔍 [Y조정] ${compId}: y=${compY} → ${compY - offset} (offset=${offset})`); - } return offset; }; diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 389e8366..c6ad7437 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -17,7 +17,11 @@ import { SCREEN_RESOLUTIONS, } from "@/types/screen"; import { generateComponentId } from "@/lib/utils/generateId"; -import { getComponentIdFromWebType, createV2ConfigFromColumn, getV2ConfigFromWebType } from "@/lib/utils/webTypeMapping"; +import { + getComponentIdFromWebType, + createV2ConfigFromColumn, + getV2ConfigFromWebType, +} from "@/lib/utils/webTypeMapping"; import { createGroupComponent, calculateBoundingBox, @@ -209,20 +213,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // 🆕 탭 내부 컴포넌트 선택 핸들러 (중첩 구조 지원) const handleSelectTabComponent = useCallback( ( - tabsComponentId: string, - tabId: string, - compId: string, + tabsComponentId: string, + tabId: string, + compId: string, comp: any, // 🆕 중첩 구조용: 부모 분할 패널 정보 (선택적) parentSplitPanelId?: string | null, - parentPanelSide?: "left" | "right" | null + parentPanelSide?: "left" | "right" | null, ) => { if (!compId) { // 탭 영역 빈 공간 클릭 시 선택 해제 setSelectedTabComponentInfo(null); return; } - + setSelectedTabComponentInfo({ tabsComponentId, tabId, @@ -250,13 +254,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU fieldMapping: comp?.componentConfig?.fieldMapping, fieldMappingKeys: comp?.componentConfig?.fieldMapping ? Object.keys(comp.componentConfig.fieldMapping) : [], }); - + if (!compId) { // 패널 영역 빈 공간 클릭 시 선택 해제 setSelectedPanelComponentInfo(null); return; } - + setSelectedPanelComponentInfo({ splitPanelId, panelSide, @@ -275,14 +279,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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, @@ -295,15 +299,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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([]); @@ -517,9 +520,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const handleUpdateTabComponentConfig = useCallback( (path: string, value: any) => { if (!selectedTabComponentInfo) return; - + const { tabsComponentId, tabId, componentId, parentSplitPanelId, parentPanelSide } = selectedTabComponentInfo; - + // 탭 컴포넌트 업데이트 함수 (재사용) const updateTabsComponent = (tabsComponent: any) => { const currentConfig = tabsComponent.componentConfig || {}; @@ -556,11 +559,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU return { ...tabsComponent, componentConfig: { ...currentConfig, tabs: updatedTabs } }; }; - + setLayout((prevLayout) => { let newLayout; let updatedTabs; - + if (parentSplitPanelId && parentPanelSide) { // 🆕 중첩 구조: 분할 패널 안의 탭 업데이트 newLayout = { @@ -571,13 +574,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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: { @@ -585,7 +588,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [panelKey]: { ...panelConfig, components: panelComponents.map((pc: any) => - pc.id === tabsComponentId ? updatedTabsComponent : pc + pc.id === tabsComponentId ? updatedTabsComponent : pc, ), }, }, @@ -598,15 +601,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // 일반 구조: 최상위 탭 업데이트 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 - ), + components: prevLayout.components.map((c) => (c.id === tabsComponentId ? updatedTabsComponent : c)), }; } @@ -616,9 +617,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU .find((t: any) => t.id === tabId) ?.components?.find((c: any) => c.id === componentId); if (updatedComp) { - setSelectedTabComponentInfo((prev) => - prev ? { ...prev, component: updatedComp } : null - ); + setSelectedTabComponentInfo((prev) => (prev ? { ...prev, component: updatedComp } : null)); } } @@ -786,7 +785,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU pathIncludesLabelDisplay: path.includes("labelDisplay"), }); } - + // 🆕 labelDisplay 변경 시 강제 리렌더링 트리거 (조건문 밖으로 이동) if (path === "style.labelDisplay") { console.log("⏰⏰⏰ labelDisplay 변경 감지! forceRenderTrigger 실행 예정"); @@ -796,7 +795,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU if (path === "size.width" || path === "size.height" || path === "size") { // 🔧 style 객체를 새로 복사하여 불변성 유지 newComp.style = { ...(newComp.style || {}) }; - + if (path === "size.width") { newComp.style.width = `${value}px`; } else if (path === "size.height") { @@ -810,7 +809,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU newComp.style.height = `${value.height}px`; } } - + console.log("🔄 size 변경 → style 동기화:", { componentId: newComp.id, path, @@ -1115,18 +1114,19 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // 전체 selectedScreen 객체도 출력 fullScreen: selectedScreen, }); - + // REST API 데이터 소스인 경우 // 1. dataSourceType이 "restapi"인 경우 // 2. tableName이 restapi_ 또는 _restapi_로 시작하는 경우 // 3. restApiConnectionId가 있는 경우 - const isRestApi = selectedScreen?.dataSourceType === "restapi" || - selectedScreen?.tableName?.startsWith("restapi_") || - selectedScreen?.tableName?.startsWith("_restapi_") || - !!selectedScreen?.restApiConnectionId; - + const isRestApi = + selectedScreen?.dataSourceType === "restapi" || + selectedScreen?.tableName?.startsWith("restapi_") || + selectedScreen?.tableName?.startsWith("_restapi_") || + !!selectedScreen?.restApiConnectionId; + console.log("🔍 [ScreenDesigner] REST API 여부:", { isRestApi }); - + if (isRestApi && (selectedScreen?.restApiConnectionId || selectedScreen?.tableName)) { try { // 연결 ID 추출 (restApiConnectionId가 없으면 tableName에서 추출) @@ -1135,13 +1135,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const match = selectedScreen.tableName.match(/restapi_(\d+)/); connectionId = match ? parseInt(match[1]) : undefined; } - + if (!connectionId) { throw new Error("REST API 연결 ID를 찾을 수 없습니다."); } - + console.log("🌐 [ScreenDesigner] REST API 데이터 로드:", { connectionId }); - + const restApiData = await ExternalRestApiConnectionAPI.fetchData( connectionId, selectedScreen?.restApiEndpoint, @@ -1166,12 +1166,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU tableLabel: restApiData.connectionInfo.connectionName || "REST API 데이터", columns, }; - + console.log("✅ [ScreenDesigner] REST API 컬럼 로드 완료:", { tableName: tableInfo.tableName, tableLabel: tableInfo.tableLabel, columnsCount: columns.length, - columns: columns.map(c => c.columnName), + columns: columns.map((c) => c.columnName), }); setTables([tableInfo]); @@ -1278,95 +1278,107 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }; loadScreenDataSource(); - }, [selectedScreen?.tableName, selectedScreen?.screenName, selectedScreen?.dataSourceType, selectedScreen?.restApiConnectionId, selectedScreen?.restApiEndpoint, selectedScreen?.restApiJsonPath]); + }, [ + selectedScreen?.tableName, + selectedScreen?.screenName, + selectedScreen?.dataSourceType, + selectedScreen?.restApiConnectionId, + selectedScreen?.restApiEndpoint, + selectedScreen?.restApiJsonPath, + ]); // 테이블 선택 핸들러 - 사이드바에서 테이블 선택 시 호출 - const handleTableSelect = useCallback(async (tableName: string) => { - console.log("📊 테이블 선택:", tableName); - - try { - // 테이블 라벨 조회 - const tableListResponse = await tableManagementApi.getTableList(); - const currentTable = - tableListResponse.success && tableListResponse.data - ? tableListResponse.data.find((t: any) => (t.tableName || t.table_name) === tableName) - : null; - const tableLabel = currentTable?.displayName || currentTable?.table_label || tableName; + const handleTableSelect = useCallback( + async (tableName: string) => { + console.log("📊 테이블 선택:", tableName); - // 테이블 컬럼 정보 조회 - const columnsResponse = await tableTypeApi.getColumns(tableName, true); + try { + // 테이블 라벨 조회 + const tableListResponse = await tableManagementApi.getTableList(); + const currentTable = + tableListResponse.success && tableListResponse.data + ? tableListResponse.data.find((t: any) => (t.tableName || t.table_name) === tableName) + : null; + const tableLabel = currentTable?.displayName || currentTable?.table_label || tableName; - const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => { - const inputType = col.inputType || col.input_type; - const widgetType = inputType || col.widgetType || col.widget_type || col.webType || col.web_type; + // 테이블 컬럼 정보 조회 + const columnsResponse = await tableTypeApi.getColumns(tableName, true); - let detailSettings = col.detailSettings || col.detail_settings; - if (typeof detailSettings === "string") { - try { - detailSettings = JSON.parse(detailSettings); - } catch (e) { - detailSettings = {}; - } - } + const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => { + const inputType = col.inputType || col.input_type; + const widgetType = inputType || col.widgetType || col.widget_type || col.webType || col.web_type; - return { - tableName: col.tableName || tableName, - columnName: col.columnName || col.column_name, - columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, - dataType: col.dataType || col.data_type || col.dbType, - webType: col.webType || col.web_type, - input_type: inputType, - inputType: inputType, - widgetType, - isNullable: col.isNullable || col.is_nullable, - required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO", - columnDefault: col.columnDefault || col.column_default, - characterMaximumLength: col.characterMaximumLength || col.character_maximum_length, - codeCategory: col.codeCategory || col.code_category, - codeValue: col.codeValue || col.code_value, - referenceTable: detailSettings?.referenceTable || col.referenceTable || col.reference_table, - referenceColumn: detailSettings?.referenceColumn || col.referenceColumn || col.reference_column, - displayColumn: detailSettings?.displayColumn || col.displayColumn || col.display_column, - detailSettings, - }; - }); - - const tableInfo: TableInfo = { - tableName, - tableLabel, - columns, - }; - - setTables([tableInfo]); - toast.success(`테이블 "${tableLabel}" 선택됨`); - - // 기존 테이블과 다른 테이블 선택 시, 기존 컴포넌트 중 다른 테이블 컬럼은 제거 - if (tables.length > 0 && tables[0].tableName !== tableName) { - setLayout((prev) => { - const newComponents = prev.components.filter((comp) => { - // 테이블 컬럼 기반 컴포넌트인지 확인 - if (comp.tableName && comp.tableName !== tableName) { - console.log("🗑️ 다른 테이블 컴포넌트 제거:", comp.tableName, comp.columnName); - return false; + let detailSettings = col.detailSettings || col.detail_settings; + if (typeof detailSettings === "string") { + try { + detailSettings = JSON.parse(detailSettings); + } catch (e) { + detailSettings = {}; } - return true; - }); - - if (newComponents.length < prev.components.length) { - toast.info(`이전 테이블(${tables[0].tableName})의 컴포넌트가 ${prev.components.length - newComponents.length}개 제거되었습니다.`); } return { - ...prev, - components: newComponents, + tableName: col.tableName || tableName, + columnName: col.columnName || col.column_name, + columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, + dataType: col.dataType || col.data_type || col.dbType, + webType: col.webType || col.web_type, + input_type: inputType, + inputType: inputType, + widgetType, + isNullable: col.isNullable || col.is_nullable, + required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO", + columnDefault: col.columnDefault || col.column_default, + characterMaximumLength: col.characterMaximumLength || col.character_maximum_length, + codeCategory: col.codeCategory || col.code_category, + codeValue: col.codeValue || col.code_value, + referenceTable: detailSettings?.referenceTable || col.referenceTable || col.reference_table, + referenceColumn: detailSettings?.referenceColumn || col.referenceColumn || col.reference_column, + displayColumn: detailSettings?.displayColumn || col.displayColumn || col.display_column, + detailSettings, }; }); + + const tableInfo: TableInfo = { + tableName, + tableLabel, + columns, + }; + + setTables([tableInfo]); + toast.success(`테이블 "${tableLabel}" 선택됨`); + + // 기존 테이블과 다른 테이블 선택 시, 기존 컴포넌트 중 다른 테이블 컬럼은 제거 + if (tables.length > 0 && tables[0].tableName !== tableName) { + setLayout((prev) => { + const newComponents = prev.components.filter((comp) => { + // 테이블 컬럼 기반 컴포넌트인지 확인 + if (comp.tableName && comp.tableName !== tableName) { + console.log("🗑️ 다른 테이블 컴포넌트 제거:", comp.tableName, comp.columnName); + return false; + } + return true; + }); + + if (newComponents.length < prev.components.length) { + toast.info( + `이전 테이블(${tables[0].tableName})의 컴포넌트가 ${prev.components.length - newComponents.length}개 제거되었습니다.`, + ); + } + + return { + ...prev, + components: newComponents, + }; + }); + } + } catch (error) { + console.error("테이블 정보 로드 실패:", error); + toast.error("테이블 정보를 불러오는데 실패했습니다."); } - } catch (error) { - console.error("테이블 정보 로드 실패:", error); - toast.error("테이블 정보를 불러오는데 실패했습니다."); - } - }, [tables]); + }, + [tables], + ); // 화면 레이아웃 로드 useEffect(() => { @@ -1391,43 +1403,27 @@ 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 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" + (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) : [], + 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); } - + if (response) { // 🔄 마이그레이션 필요 여부 확인 (V2는 스킵) let layoutToUse = response; @@ -1469,15 +1465,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } // 🔍 디버깅: 로드된 버튼 컴포넌트의 action 확인 - const buttonComponents = layoutWithDefaultGrid.components.filter( - (c: any) => c.componentType?.startsWith("button") + const buttonComponents = layoutWithDefaultGrid.components.filter((c: any) => + c.componentType?.startsWith("button"), + ); + console.log( + "🔍 [로드] 버튼 컴포넌트 action 확인:", + buttonComponents.map((c: any) => ({ + id: c.id, + type: c.componentType, + actionType: c.componentConfig?.action?.type, + fullAction: c.componentConfig?.action, + })), ); - console.log("🔍 [로드] 버튼 컴포넌트 action 확인:", buttonComponents.map((c: any) => ({ - id: c.id, - type: c.componentType, - actionType: c.componentConfig?.action?.type, - fullAction: c.componentConfig?.action, - }))); setLayout(layoutWithDefaultGrid); setHistory([layoutWithDefaultGrid]); @@ -1503,8 +1502,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU if ( activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement || - activeElement?.getAttribute('contenteditable') === 'true' || - activeElement?.getAttribute('role') === 'textbox' + activeElement?.getAttribute("contenteditable") === "true" || + activeElement?.getAttribute("role") === "textbox" ) { return; } @@ -1530,8 +1529,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU if ( activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement || - activeElement?.getAttribute('contenteditable') === 'true' || - activeElement?.getAttribute('role') === 'textbox' + activeElement?.getAttribute("contenteditable") === "true" || + activeElement?.getAttribute("role") === "textbox" ) { return; } @@ -1682,7 +1681,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU setLayout(updatedLayout); saveToHistory(updatedLayout); - toast.success(`해상도가 변경되었습니다.`, { + toast.success("해상도가 변경되었습니다.", { description: `${oldWidth}×${oldHeight} → ${newWidth}×${newHeight}`, }); @@ -1798,7 +1797,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // 해상도 정보를 포함한 레이아웃 데이터 생성 // 현재 선택된 테이블을 화면의 기본 테이블로 저장 const currentMainTableName = tables.length > 0 ? tables[0].tableName : null; - + const layoutWithResolution = { ...layout, components: updatedComponents, @@ -1907,14 +1906,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU if (response.success && response.data) { // 자동 매핑 적용 const updatedComponents = applyMultilangMappings(layout.components, response.data); - + // 레이아웃 업데이트 const updatedLayout = { ...layout, components: updatedComponents, screenResolution: screenResolution, }; - + setLayout(updatedLayout); // 자동 저장 (매핑 정보가 손실되지 않도록) @@ -2518,7 +2517,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU if (targetComponent && (compType === "repeat-container" || compType === "v2-repeat-container")) { const currentConfig = (targetComponent as any).componentConfig || {}; const currentChildren = currentConfig.children || []; - + // 새 자식 컴포넌트 생성 const newChild = { id: `slot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, @@ -2529,7 +2528,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU size: component.defaultSize || { width: 200, height: 32 }, componentConfig: component.defaultConfig || {}, }; - + // 컴포넌트 업데이트 const updatedComponent = { ...targetComponent, @@ -2538,14 +2537,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU children: [...currentChildren, newChild], }, }; - + const newLayout = { ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedComponent : c - ), + components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)), }; - + setLayout(newLayout); saveToHistory(newLayout); return; // 리피터 컨테이너 처리 완료 @@ -2563,14 +2560,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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); @@ -2580,7 +2577,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU parentPanelSide = "left"; break; } - + // 우측 패널에서 찾기 const rightComponents = config.rightPanel?.components || []; const foundInRight = rightComponents.find((c: any) => c.id === containerId); @@ -2593,20 +2590,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } } } - + const compType = (targetComponent as any)?.componentType; if (targetComponent && (compType === "tabs-widget" || compType === "v2-tabs-widget")) { const currentConfig = (targetComponent as any).componentConfig || {}; const tabs = currentConfig.tabs || []; - + // 활성 탭의 드롭 위치 계산 const tabContentRect = tabsContainer.getBoundingClientRect(); const dropX = (e.clientX - tabContentRect.left) / zoomLevel; const dropY = (e.clientY - tabContentRect.top) / zoomLevel; - + // 새 컴포넌트 생성 const componentType = component.id || component.componentType || "v2-text-display"; - + console.log("🎯 탭에 컴포넌트 드롭:", { componentId: component.id, componentType: componentType, @@ -2621,7 +2618,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU zoomLevel, calculatedPosition: { x: dropX, y: dropY }, }); - + const newTabComponent = { id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, componentType: componentType, @@ -2630,7 +2627,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU size: component.defaultSize || { width: 200, height: 100 }, componentConfig: component.defaultConfig || {}, }; - + // 해당 탭에 컴포넌트 추가 const updatedTabs = tabs.map((tab: any) => { if (tab.id === activeTabId) { @@ -2641,7 +2638,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } return tab; }); - + const updatedTabsComponent = { ...targetComponent, componentConfig: { @@ -2649,9 +2646,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU tabs: updatedTabs, }, }; - + let newLayout; - + if (parentSplitPanelId && parentPanelSide) { // 🆕 중첩 구조: 분할 패널 안의 탭 업데이트 newLayout = { @@ -2662,7 +2659,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; const panelConfig = splitConfig[panelKey] || {}; const panelComponents = panelConfig.components || []; - + return { ...c, componentConfig: { @@ -2670,7 +2667,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [panelKey]: { ...panelConfig, components: panelComponents.map((pc: any) => - pc.id === containerId ? updatedTabsComponent : pc + pc.id === containerId ? updatedTabsComponent : pc, ), }, }, @@ -2684,13 +2681,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // 일반 구조: 최상위 탭 업데이트 newLayout = { ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedTabsComponent : c - ), + components: layout.components.map((c) => (c.id === containerId ? updatedTabsComponent : c)), }; toast.success("컴포넌트가 탭에 추가되었습니다"); } - + setLayout(newLayout); saveToHistory(newLayout); return; // 탭 컨테이너 처리 완료 @@ -2711,22 +2706,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel"; const panelConfig = currentConfig[panelKey] || {}; const currentComponents = panelConfig.components || []; - + // 드롭 위치 계산 const panelRect = splitPanelContainer.getBoundingClientRect(); const dropX = (e.clientX - panelRect.left) / zoomLevel; const dropY = (e.clientY - panelRect.top) / zoomLevel; - + // 새 컴포넌트 생성 const componentType = component.id || component.componentType || "v2-text-display"; - + console.log("🎯 분할 패널에 컴포넌트 드롭:", { componentId: component.id, componentType: componentType, panelSide: panelSide, dropPosition: { x: dropX, y: dropY }, }); - + const newPanelComponent = { id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, componentType: componentType, @@ -2735,12 +2730,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU size: component.defaultSize || { width: 200, height: 100 }, componentConfig: component.defaultConfig || {}, }; - + const updatedPanelConfig = { ...panelConfig, components: [...currentComponents, newPanelComponent], }; - + const updatedComponent = { ...targetComponent, componentConfig: { @@ -2748,14 +2743,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [panelKey]: updatedPanelConfig, }, }; - + const newLayout = { ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedComponent : c - ), + components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)), }; - + setLayout(newLayout); saveToHistory(newLayout); toast.success(`컴포넌트가 ${panelSide === "left" ? "좌측" : "우측"} 패널에 추가되었습니다`); @@ -3114,7 +3107,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU if (targetComponent && (rcType === "repeat-container" || rcType === "v2-repeat-container")) { const currentConfig = (targetComponent as any).componentConfig || {}; const currentChildren = currentConfig.children || []; - + // 새 자식 컴포넌트 생성 (컬럼 기반) const newChild = { id: `slot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, @@ -3125,7 +3118,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU size: { width: 200, height: 32 }, componentConfig: {}, }; - + const updatedComponent = { ...targetComponent, componentConfig: { @@ -3133,14 +3126,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU children: [...currentChildren, newChild], }, }; - + const newLayout = { ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedComponent : c - ), + components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)), }; - + setLayout(newLayout); saveToHistory(newLayout); return; @@ -3158,14 +3149,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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); @@ -3175,7 +3166,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU parentPanelSide = "left"; break; } - + // 우측 패널에서 찾기 const rightComponents = config.rightPanel?.components || []; const foundInRight = rightComponents.find((c: any) => c.id === containerId); @@ -3188,17 +3179,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } } } - + const compType = (targetComponent as any)?.componentType; if (targetComponent && (compType === "tabs-widget" || compType === "v2-tabs-widget")) { const currentConfig = (targetComponent as any).componentConfig || {}; const tabs = currentConfig.tabs || []; - + // 드롭 위치 계산 const tabContentRect = tabsContainer.getBoundingClientRect(); const dropX = (e.clientX - tabContentRect.left) / zoomLevel; const dropY = (e.clientY - tabContentRect.top) / zoomLevel; - + // 🆕 V2 컴포넌트 매핑 사용 (일반 캔버스와 동일) const v2Mapping = createV2ConfigFromColumn({ widgetType: column.widgetType, @@ -3212,7 +3203,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU referenceColumn: column.referenceColumn, displayColumn: column.displayColumn, }); - + // 웹타입별 기본 크기 계산 const getTabComponentSize = (widgetType: string) => { const sizeMap: Record = { @@ -3232,9 +3223,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }; return sizeMap[widgetType] || { width: 200, height: 36 }; }; - + const componentSize = getTabComponentSize(column.widgetType); - + const newTabComponent = { id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, componentType: v2Mapping.componentType, @@ -3250,7 +3241,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU inputType: column.inputType || column.widgetType, }, }; - + // 해당 탭에 컴포넌트 추가 const updatedTabs = tabs.map((tab: any) => { if (tab.id === activeTabId) { @@ -3261,7 +3252,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } return tab; }); - + const updatedTabsComponent = { ...targetComponent, componentConfig: { @@ -3269,9 +3260,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU tabs: updatedTabs, }, }; - + let newLayout; - + if (parentSplitPanelId && parentPanelSide) { // 🆕 중첩 구조: 분할 패널 안의 탭 업데이트 newLayout = { @@ -3282,7 +3273,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; const panelConfig = splitConfig[panelKey] || {}; const panelComponents = panelConfig.components || []; - + return { ...c, componentConfig: { @@ -3290,7 +3281,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [panelKey]: { ...panelConfig, components: panelComponents.map((pc: any) => - pc.id === containerId ? updatedTabsComponent : pc + pc.id === containerId ? updatedTabsComponent : pc, ), }, }, @@ -3304,13 +3295,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // 일반 구조: 최상위 탭 업데이트 newLayout = { ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedTabsComponent : c - ), + components: layout.components.map((c) => (c.id === containerId ? updatedTabsComponent : c)), }; toast.success("컬럼이 탭에 추가되었습니다"); } - + setLayout(newLayout); saveToHistory(newLayout); return; @@ -3331,12 +3320,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel"; const panelConfig = currentConfig[panelKey] || {}; const currentComponents = panelConfig.components || []; - + // 드롭 위치 계산 const panelRect = splitPanelContainer.getBoundingClientRect(); const dropX = (e.clientX - panelRect.left) / zoomLevel; const dropY = (e.clientY - panelRect.top) / zoomLevel; - + // V2 컴포넌트 매핑 사용 const v2Mapping = createV2ConfigFromColumn({ widgetType: column.widgetType, @@ -3350,7 +3339,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU referenceColumn: column.referenceColumn, displayColumn: column.displayColumn, }); - + // 웹타입별 기본 크기 계산 const getPanelComponentSize = (widgetType: string) => { const sizeMap: Record = { @@ -3370,9 +3359,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }; return sizeMap[widgetType] || { width: 200, height: 36 }; }; - + const componentSize = getPanelComponentSize(column.widgetType); - + const newPanelComponent = { id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, componentType: v2Mapping.componentType, @@ -3388,12 +3377,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU inputType: column.inputType || column.widgetType, }, }; - + const updatedPanelConfig = { ...panelConfig, components: [...currentComponents, newPanelComponent], }; - + const updatedComponent = { ...targetComponent, componentConfig: { @@ -3401,14 +3390,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [panelKey]: updatedPanelConfig, }, }; - + const newLayout = { ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedComponent : c - ), + components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)), }; - + setLayout(newLayout); saveToHistory(newLayout); toast.success(`컬럼이 ${panelSide === "left" ? "좌측" : "우측"} 패널에 추가되었습니다`); @@ -3694,7 +3681,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU isEntityJoin: true, entityJoinTable: column.entityJoinTable, entityJoinColumn: column.entityJoinColumn, - }), + }), style: { labelDisplay: true, // 🆕 라벨 기본 표시 labelFontSize: "12px", @@ -3760,7 +3747,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU isEntityJoin: true, entityJoinTable: column.entityJoinTable, entityJoinColumn: column.entityJoinColumn, - }), + }), style: { labelDisplay: true, // 🆕 라벨 기본 표시 labelFontSize: "14px", @@ -4030,322 +4017,325 @@ 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"]'); - - if (tabsContainer) { - const containerId = tabsContainer.getAttribute("data-component-id"); - const activeTabId = tabsContainer.getAttribute("data-active-tab-id"); - - if (containerId && activeTabId) { - // 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 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"]'); + + if (tabsContainer) { + const containerId = tabsContainer.getAttribute("data-component-id"); + const activeTabId = tabsContainer.getAttribute("data-active-tab-id"); + + if (containerId && activeTabId) { + // 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 && + + const compType = (targetComponent as any)?.componentType; + + // 자기 자신을 자신에게 드롭하는 것 방지 + if ( + targetComponent && (compType === "tabs-widget" || compType === "v2-tabs-widget") && - dragState.draggedComponent !== containerId) { - - const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent); - if (draggedComponent) { - const currentConfig = (targetComponent as any).componentConfig || {}; - const tabs = currentConfig.tabs || []; - - // 탭 컨텐츠 영역 기준 드롭 위치 계산 - const tabContentRect = tabsContainer.getBoundingClientRect(); - const dropX = (mouseEvent.clientX - tabContentRect.left) / zoomLevel; - const dropY = (mouseEvent.clientY - tabContentRect.top) / zoomLevel; - - // 기존 컴포넌트를 탭 내부 컴포넌트로 변환 - const newTabComponent = { - id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - componentType: (draggedComponent as any).componentType || draggedComponent.type, - label: (draggedComponent as any).label || "컴포넌트", - position: { x: Math.max(0, dropX), y: Math.max(0, dropY) }, - size: draggedComponent.size || { width: 200, height: 100 }, - componentConfig: (draggedComponent as any).componentConfig || {}, - style: (draggedComponent as any).style || {}, - }; - - // 해당 탭에 컴포넌트 추가 - const updatedTabs = tabs.map((tab: any) => { - if (tab.id === activeTabId) { - return { - ...tab, - components: [...(tab.components || []), newTabComponent], - }; - } - return tab; - }); - - 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 - ), + dragState.draggedComponent !== containerId + ) { + const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent); + if (draggedComponent) { + const currentConfig = (targetComponent as any).componentConfig || {}; + const tabs = currentConfig.tabs || []; + + // 탭 컨텐츠 영역 기준 드롭 위치 계산 + const tabContentRect = tabsContainer.getBoundingClientRect(); + const dropX = (mouseEvent.clientX - tabContentRect.left) / zoomLevel; + const dropY = (mouseEvent.clientY - tabContentRect.top) / zoomLevel; + + // 기존 컴포넌트를 탭 내부 컴포넌트로 변환 + const newTabComponent = { + id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + componentType: (draggedComponent as any).componentType || draggedComponent.type, + label: (draggedComponent as any).label || "컴포넌트", + position: { x: Math.max(0, dropX), y: Math.max(0, dropY) }, + size: draggedComponent.size || { width: 200, height: 100 }, + componentConfig: (draggedComponent as any).componentConfig || {}, + style: (draggedComponent as any).style || {}, + }; + + // 해당 탭에 컴포넌트 추가 + const updatedTabs = tabs.map((tab: any) => { + if (tab.id === activeTabId) { + return { + ...tab, + components: [...(tab.components || []), newTabComponent], + }; + } + return tab; + }); + + 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("컴포넌트가 탭으로 이동되었습니다"); + }; + } + 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); + + // 드래그 상태 초기화 후 종료 + setDragState({ + isDragging: false, + draggedComponent: null, + draggedComponents: [], + originalPosition: { x: 0, y: 0, z: 1 }, + currentPosition: { x: 0, y: 0, z: 1 }, + grabOffset: { x: 0, y: 0 }, + justFinishedDrag: true, + }); + + setTimeout(() => { + setDragState((prev) => ({ ...prev, justFinishedDrag: false })); + }, 100); + + return; // 탭으로 이동 완료, 일반 드래그 종료 로직 스킵 } - - setLayout(newLayout); - saveToHistory(newLayout); - setSelectedComponent(null); - - // 드래그 상태 초기화 후 종료 - setDragState({ - isDragging: false, - draggedComponent: null, - draggedComponents: [], - originalPosition: { x: 0, y: 0, z: 1 }, - currentPosition: { x: 0, y: 0, z: 1 }, - grabOffset: { x: 0, y: 0 }, - justFinishedDrag: true, - }); - - setTimeout(() => { - setDragState((prev) => ({ ...prev, justFinishedDrag: false })); - }, 100); - - return; // 탭으로 이동 완료, 일반 드래그 종료 로직 스킵 } } } } - } - - // 주 드래그 컴포넌트의 최종 위치 계산 - const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent); - let finalPosition = dragState.currentPosition; - // 현재 해상도에 맞는 격자 정보 계산 - const currentGridInfo = layout.gridSettings - ? calculateGridInfo(screenResolution.width, screenResolution.height, { - columns: layout.gridSettings.columns, - gap: layout.gridSettings.gap, - padding: layout.gridSettings.padding, - snapToGrid: layout.gridSettings.snapToGrid || false, - }) - : null; + // 주 드래그 컴포넌트의 최종 위치 계산 + const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent); + let finalPosition = dragState.currentPosition; - // 일반 컴포넌트 및 플로우 버튼 그룹에 격자 스냅 적용 (일반 그룹 제외) - if (draggedComponent?.type !== "group" && layout.gridSettings?.snapToGrid && currentGridInfo) { - finalPosition = snapPositionTo10px( - { - x: dragState.currentPosition.x, - y: dragState.currentPosition.y, - z: dragState.currentPosition.z ?? 1, - }, - currentGridInfo, - { - columns: layout.gridSettings.columns, - gap: layout.gridSettings.gap, - padding: layout.gridSettings.padding, - snapToGrid: layout.gridSettings.snapToGrid || false, - }, - ); + // 현재 해상도에 맞는 격자 정보 계산 + const currentGridInfo = layout.gridSettings + ? calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }) + : null; - } + // 일반 컴포넌트 및 플로우 버튼 그룹에 격자 스냅 적용 (일반 그룹 제외) + if (draggedComponent?.type !== "group" && layout.gridSettings?.snapToGrid && currentGridInfo) { + finalPosition = snapPositionTo10px( + { + x: dragState.currentPosition.x, + y: dragState.currentPosition.y, + z: dragState.currentPosition.z ?? 1, + }, + currentGridInfo, + { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }, + ); + } - // 스냅으로 인한 추가 이동 거리 계산 - const snapDeltaX = finalPosition.x - dragState.currentPosition.x; - const snapDeltaY = finalPosition.y - dragState.currentPosition.y; + // 스냅으로 인한 추가 이동 거리 계산 + const snapDeltaX = finalPosition.x - dragState.currentPosition.x; + const snapDeltaY = finalPosition.y - dragState.currentPosition.y; - // 원래 이동 거리 + 스냅 조정 거리 - const totalDeltaX = dragState.currentPosition.x - dragState.originalPosition.x + snapDeltaX; - const totalDeltaY = dragState.currentPosition.y - dragState.originalPosition.y + snapDeltaY; + // 원래 이동 거리 + 스냅 조정 거리 + const totalDeltaX = dragState.currentPosition.x - dragState.originalPosition.x + snapDeltaX; + const totalDeltaY = dragState.currentPosition.y - dragState.originalPosition.y + snapDeltaY; - // 다중 컴포넌트들의 최종 위치 업데이트 - const updatedComponents = layout.components.map((comp) => { - const isDraggedComponent = dragState.draggedComponents.some((dragComp) => dragComp.id === comp.id); - if (isDraggedComponent) { - const originalComponent = dragState.draggedComponents.find((dragComp) => dragComp.id === comp.id)!; - let newPosition = { - x: originalComponent.position.x + totalDeltaX, - y: originalComponent.position.y + totalDeltaY, - z: originalComponent.position.z || 1, - }; - - // 캔버스 경계 제한 (컴포넌트가 화면 밖으로 나가지 않도록) - const componentWidth = comp.size?.width || 100; - const componentHeight = comp.size?.height || 40; - - // 최소 위치: 0, 최대 위치: 캔버스 크기 - 컴포넌트 크기 - newPosition.x = Math.max(0, Math.min(newPosition.x, screenResolution.width - componentWidth)); - newPosition.y = Math.max(0, Math.min(newPosition.y, screenResolution.height - componentHeight)); - - // 그룹 내부 컴포넌트인 경우 패딩을 고려한 격자 스냅 적용 - if (comp.parentId && layout.gridSettings?.snapToGrid && gridInfo) { - const { columnWidth } = gridInfo; - const { gap } = layout.gridSettings; - - // 그룹 내부 패딩 고려한 격자 정렬 - const padding = 16; - const effectiveX = newPosition.x - padding; - const columnIndex = Math.round(effectiveX / (columnWidth + (gap || 16))); - const snappedX = padding + columnIndex * (columnWidth + (gap || 16)); - - // Y 좌표는 20px 단위로 스냅 - const effectiveY = newPosition.y - padding; - const rowIndex = Math.round(effectiveY / 20); - const snappedY = padding + rowIndex * 20; - - // 크기도 외부 격자와 동일하게 스냅 - const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기 - const widthInColumns = Math.max(1, Math.round(comp.size.width / fullColumnWidth)); - const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap 제거하여 실제 컴포넌트 크기 - // 높이는 사용자가 입력한 값 그대로 사용 (스냅 제거) - const snappedHeight = Math.max(40, comp.size.height); - - newPosition = { - x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보 - y: Math.max(padding, snappedY), - z: newPosition.z, + // 다중 컴포넌트들의 최종 위치 업데이트 + const updatedComponents = layout.components.map((comp) => { + const isDraggedComponent = dragState.draggedComponents.some((dragComp) => dragComp.id === comp.id); + if (isDraggedComponent) { + const originalComponent = dragState.draggedComponents.find((dragComp) => dragComp.id === comp.id)!; + let newPosition = { + x: originalComponent.position.x + totalDeltaX, + y: originalComponent.position.y + totalDeltaY, + z: originalComponent.position.z || 1, }; - // 크기도 업데이트 - const newSize = { - width: snappedWidth, - height: snappedHeight, - }; + // 캔버스 경계 제한 (컴포넌트가 화면 밖으로 나가지 않도록) + const componentWidth = comp.size?.width || 100; + const componentHeight = comp.size?.height || 40; + + // 최소 위치: 0, 최대 위치: 캔버스 크기 - 컴포넌트 크기 + newPosition.x = Math.max(0, Math.min(newPosition.x, screenResolution.width - componentWidth)); + newPosition.y = Math.max(0, Math.min(newPosition.y, screenResolution.height - componentHeight)); + + // 그룹 내부 컴포넌트인 경우 패딩을 고려한 격자 스냅 적용 + if (comp.parentId && layout.gridSettings?.snapToGrid && gridInfo) { + const { columnWidth } = gridInfo; + const { gap } = layout.gridSettings; + + // 그룹 내부 패딩 고려한 격자 정렬 + const padding = 16; + const effectiveX = newPosition.x - padding; + const columnIndex = Math.round(effectiveX / (columnWidth + (gap || 16))); + const snappedX = padding + columnIndex * (columnWidth + (gap || 16)); + + // Y 좌표는 20px 단위로 스냅 + const effectiveY = newPosition.y - padding; + const rowIndex = Math.round(effectiveY / 20); + const snappedY = padding + rowIndex * 20; + + // 크기도 외부 격자와 동일하게 스냅 + const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기 + const widthInColumns = Math.max(1, Math.round(comp.size.width / fullColumnWidth)); + const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap 제거하여 실제 컴포넌트 크기 + // 높이는 사용자가 입력한 값 그대로 사용 (스냅 제거) + const snappedHeight = Math.max(40, comp.size.height); + + newPosition = { + x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보 + y: Math.max(padding, snappedY), + z: newPosition.z, + }; + + // 크기도 업데이트 + const newSize = { + width: snappedWidth, + height: snappedHeight, + }; + + return { + ...comp, + position: newPosition as Position, + size: newSize, + }; + } return { ...comp, position: newPosition as Position, - size: newSize, }; } + return comp; + }); - return { - ...comp, - position: newPosition as Position, - }; + const newLayout = { ...layout, components: updatedComponents }; + setLayout(newLayout); + + // 선택된 컴포넌트도 업데이트 (PropertiesPanel 동기화용) + if (selectedComponent && dragState.draggedComponents.some((c) => c.id === selectedComponent.id)) { + const updatedSelectedComponent = updatedComponents.find((c) => c.id === selectedComponent.id); + if (updatedSelectedComponent) { + console.log("🔄 ScreenDesigner: 선택된 컴포넌트 위치 업데이트", { + componentId: selectedComponent.id, + oldPosition: selectedComponent.position, + newPosition: updatedSelectedComponent.position, + }); + setSelectedComponent(updatedSelectedComponent); + } } - return comp; - }); - const newLayout = { ...layout, components: updatedComponents }; - setLayout(newLayout); - - // 선택된 컴포넌트도 업데이트 (PropertiesPanel 동기화용) - if (selectedComponent && dragState.draggedComponents.some((c) => c.id === selectedComponent.id)) { - const updatedSelectedComponent = updatedComponents.find((c) => c.id === selectedComponent.id); - if (updatedSelectedComponent) { - console.log("🔄 ScreenDesigner: 선택된 컴포넌트 위치 업데이트", { - componentId: selectedComponent.id, - oldPosition: selectedComponent.position, - newPosition: updatedSelectedComponent.position, - }); - setSelectedComponent(updatedSelectedComponent); - } + // 히스토리에 저장 + saveToHistory(newLayout); } - // 히스토리에 저장 - saveToHistory(newLayout); - } + setDragState({ + isDragging: false, + draggedComponent: null, + draggedComponents: [], + originalPosition: { x: 0, y: 0, z: 1 }, + currentPosition: { x: 0, y: 0, z: 1 }, + grabOffset: { x: 0, y: 0 }, + justFinishedDrag: true, + }); - setDragState({ - isDragging: false, - draggedComponent: null, - draggedComponents: [], - originalPosition: { x: 0, y: 0, z: 1 }, - currentPosition: { x: 0, y: 0, z: 1 }, - grabOffset: { x: 0, y: 0 }, - justFinishedDrag: true, - }); - - // 짧은 시간 후 justFinishedDrag 플래그 해제 - setTimeout(() => { - setDragState((prev) => ({ - ...prev, - justFinishedDrag: false, - })); - }, 100); - }, [dragState, layout, saveToHistory]); + // 짧은 시간 후 justFinishedDrag 플래그 해제 + setTimeout(() => { + setDragState((prev) => ({ + ...prev, + justFinishedDrag: false, + })); + }, 100); + }, + [dragState, layout, saveToHistory], + ); // 드래그 선택 시작 const startSelectionDrag = useCallback( @@ -5419,15 +5409,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU onGenerateMultilang={handleGenerateMultilang} isGeneratingMultilang={isGeneratingMultilang} onOpenMultilangSettings={() => setShowMultilangSettingsModal(true)} - isPanelOpen={panelStates.v2?.isOpen || false} - onTogglePanel={() => togglePanel("v2")} + isPanelOpen={panelStates.v2?.isOpen || false} + onTogglePanel={() => togglePanel("v2")} /> {/* 메인 컨테이너 (패널들 + 캔버스) */}
{/* 통합 패널 - 좌측 사이드바 제거 후 너비 300px로 확장 */} {panelStates.v2?.isOpen && ( -
-
+
+

패널

@@ -164,9 +162,9 @@ export const ImageWidget: React.FC {/* 필수 필드 경고 */} - {required && !imageUrl && ( -
* 이미지를 업로드해야 합니다
- )} + {required && !imageUrl &&
* 이미지를 업로드해야 합니다
}
); }; diff --git a/frontend/components/v2/V2Input.tsx b/frontend/components/v2/V2Input.tsx index 8e27e38f..a284f26e 100644 --- a/frontend/components/v2/V2Input.tsx +++ b/frontend/components/v2/V2Input.tsx @@ -551,10 +551,6 @@ export const V2Input = forwardRef((props, ref) => // 🆕 채번 규칙 ID를 formData에 저장 (저장 시 allocateCode 호출을 위해) if (parsed.numberingRuleId && onFormDataChange && columnName) { onFormDataChange(`${columnName}_numberingRuleId`, parsed.numberingRuleId); - console.log("🔧 채번 규칙 ID를 formData에 저장:", { - key: `${columnName}_numberingRuleId`, - value: parsed.numberingRuleId, - }); } } catch { // JSON 파싱 실패 @@ -571,11 +567,6 @@ export const V2Input = forwardRef((props, ref) => // 채번 코드 생성 (formDataRef.current 사용하여 최신 formData 전달) const currentFormData = formDataRef.current; - console.log("🔍 [V2Input] 채번 미리보기 호출:", { - numberingRuleId, - formDataKeys: Object.keys(currentFormData), - materialValue: currentFormData.material // 재질 값 로깅 - }); const previewResponse = await previewNumberingCode(numberingRuleId, currentFormData); if (previewResponse.success && previewResponse.data?.generatedCode) { @@ -655,11 +646,6 @@ export const V2Input = forwardRef((props, ref) => // formData에 직접 주입 if (event.detail?.formData && columnName) { event.detail.formData[columnName] = currentValue; - console.log("🔧 [V2Input] beforeFormSave에서 채번 값 주입:", { - columnName, - manualInputValue, - currentValue, - }); } }; diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx index 84dd0d3c..a1bf35f9 100644 --- a/frontend/components/v2/V2Select.tsx +++ b/frontend/components/v2/V2Select.tsx @@ -758,16 +758,6 @@ export const V2Select = forwardRef( const componentWidth = size?.width || style?.width; const componentHeight = size?.height || style?.height; - // 🔍 디버깅: 높이값 확인 (warn으로 변경하여 캡처되도록) - console.warn("🔍 [V2Select] 높이 디버깅:", { - id, - "size?.height": size?.height, - "style?.height": style?.height, - componentHeight, - size, - style, - }); - // 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정) const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14; const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4; diff --git a/frontend/components/v2/config-panels/V2SelectConfigPanel.tsx b/frontend/components/v2/config-panels/V2SelectConfigPanel.tsx index 056facac..62aa9246 100644 --- a/frontend/components/v2/config-panels/V2SelectConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2SelectConfigPanel.tsx @@ -27,11 +27,7 @@ interface V2SelectConfigPanelProps { inputType?: string; } -export const V2SelectConfigPanel: React.FC = ({ - config, - onChange, - inputType, -}) => { +export const V2SelectConfigPanel: React.FC = ({ config, onChange, inputType }) => { // 엔티티 타입인지 확인 const isEntityType = inputType === "entity"; // 엔티티 테이블의 컬럼 목록 @@ -55,18 +51,18 @@ export const V2SelectConfigPanel: React.FC = ({ const response = await apiClient.get(`/table-management/tables/${tableName}/columns?size=500`); const data = response.data.data || response.data; const columns = data.columns || data || []; - + const columnOptions: ColumnOption[] = columns.map((col: any) => { const name = col.columnName || col.column_name || col.name; // displayName 우선 사용 const label = col.displayName || col.display_name || col.columnLabel || col.column_label || name; - + return { columnName: name, columnLabel: label, }; }); - + setEntityColumns(columnOptions); } catch (error) { console.error("컬럼 목록 조회 실패:", error); @@ -85,7 +81,7 @@ export const V2SelectConfigPanel: React.FC = ({ // 정적 옵션 관리 const options = config.options || []; - + const addOption = () => { const newOptions = [...options, { value: "", label: "" }]; updateConfig("options", newOptions); @@ -107,10 +103,7 @@ export const V2SelectConfigPanel: React.FC = ({ {/* 선택 모드 */}
- updateConfig("mode", value)}> @@ -130,10 +123,7 @@ export const V2SelectConfigPanel: React.FC = ({ {/* 데이터 소스 */}
- updateConfig("source", value)}> @@ -151,59 +141,51 @@ export const V2SelectConfigPanel: React.FC = ({
-
-
+
{options.map((option: any, index: number) => (
updateOption(index, "value", e.target.value)} placeholder="값" - className="h-7 text-xs flex-1" + className="h-7 flex-1 text-xs" /> updateOption(index, "label", e.target.value)} placeholder="표시 텍스트" - className="h-7 text-xs flex-1" + className="h-7 flex-1 text-xs" />
))} {options.length === 0 && ( -

- 옵션을 추가해주세요 -

+

옵션을 추가해주세요

)}
- + {/* 기본값 설정 */} {options.length > 0 && ( -
+
-

- 화면 로드 시 자동 선택될 값 -

+

화면 로드 시 자동 선택될 값

)}
@@ -228,16 +208,13 @@ export const V2SelectConfigPanel: React.FC = ({
{config.codeGroup ? ( -

{config.codeGroup}

+

{config.codeGroup}

) : ( -

- 테이블 타입 관리에서 코드 그룹을 설정해주세요 -

+

테이블 타입 관리에서 코드 그룹을 설정해주세요

)}
)} - {/* 엔티티(참조 테이블) 설정 */} {config.source === "entity" && (
@@ -248,16 +225,16 @@ export const V2SelectConfigPanel: React.FC = ({ readOnly disabled placeholder="테이블 타입 관리에서 설정" - className="h-8 text-xs bg-muted" + className="bg-muted h-8 text-xs" /> -

+

조인할 테이블명 (테이블 타입 관리에서 설정된 경우 자동 입력됨)

{/* 컬럼 로딩 중 표시 */} {loadingColumns && ( -
+
컬럼 목록 로딩 중...
@@ -291,7 +268,7 @@ export const V2SelectConfigPanel: React.FC = ({ className="h-8 text-xs" /> )} -

저장될 값

+

저장될 값

@@ -319,7 +296,7 @@ export const V2SelectConfigPanel: React.FC = ({ className="h-8 text-xs" /> )} -

화면에 표시될 값

+

화면에 표시될 값

@@ -337,14 +314,16 @@ export const V2SelectConfigPanel: React.FC = ({ {/* 추가 옵션 */}
- +
updateConfig("multiple", checked)} /> - +
@@ -353,7 +332,9 @@ export const V2SelectConfigPanel: React.FC = ({ checked={config.searchable || false} onCheckedChange={(checked) => updateConfig("searchable", checked)} /> - +
@@ -362,7 +343,9 @@ export const V2SelectConfigPanel: React.FC = ({ checked={config.allowClear !== false} onCheckedChange={(checked) => updateConfig("allowClear", checked)} /> - +
diff --git a/frontend/lib/registry/ComponentRegistry.ts b/frontend/lib/registry/ComponentRegistry.ts index 00866c68..f6065ff5 100644 --- a/frontend/lib/registry/ComponentRegistry.ts +++ b/frontend/lib/registry/ComponentRegistry.ts @@ -31,10 +31,7 @@ export class ComponentRegistry { throw new Error(`컴포넌트 등록 실패 (${definition.id}): ${validation.errors.join(", ")}`); } - // 중복 등록 체크 - if (this.components.has(definition.id)) { - console.warn(`⚠️ 컴포넌트 중복 등록: ${definition.id} - 기존 정의를 덮어씁니다.`); - } + // 중복 등록 체크 (기존 정의를 덮어씀) // 타임스탬프 추가 const enhancedDefinition = { @@ -64,7 +61,6 @@ export class ComponentRegistry { static unregisterComponent(id: string): void { const definition = this.components.get(id); if (!definition) { - console.warn(`⚠️ 등록되지 않은 컴포넌트 해제 시도: ${id}`); return; } @@ -76,8 +72,6 @@ export class ComponentRegistry { data: definition, timestamp: new Date(), }); - - console.log(`🗑️ 컴포넌트 해제: ${id}`); } /** @@ -355,7 +349,6 @@ export class ComponentRegistry { }, force: async () => { // hotReload 기능 비활성화 (불필요) - console.log("⚠️ 강제 Hot Reload는 더 이상 필요하지 않습니다"); }, }, diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 7c6470fa..570a82a7 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -220,8 +220,8 @@ export function RepeaterTable({ columns .filter((col) => !col.hidden) .forEach((col) => { - widths[col.field] = col.width ? parseInt(col.width) : 120; - }); + widths[col.field] = col.width ? parseInt(col.width) : 120; + }); return widths; }); @@ -404,10 +404,10 @@ export function RepeaterTable({ // 데이터가 있으면 데이터 기반 자동 맞춤, 없으면 균등 분배 const timer = setTimeout(() => { if (data.length > 0) { - applyAutoFitWidths(); - } else { - applyEqualizeWidths(); - } + applyAutoFitWidths(); + } else { + applyEqualizeWidths(); + } }, 50); return () => clearTimeout(timer); @@ -654,11 +654,17 @@ export function RepeaterTable({ {/* 드래그 핸들 헤더 - 좌측 고정 */} - + 순서 {/* 체크박스 헤더 - 좌측 고정 */} - + @@ -810,7 +816,7 @@ export function RepeaterTable({ diff --git a/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx b/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx index 78e58bfe..e7917dd9 100644 --- a/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx @@ -90,7 +90,7 @@ export function SimpleRepeaterTableComponent({ const newRowDefaults = componentConfig?.newRowDefaults || {}; const summaryConfig = componentConfig?.summaryConfig; const maxHeight = componentConfig?.maxHeight || propMaxHeight || "240px"; - + // 🆕 컴포넌트 레벨의 저장 테이블 설정 const componentTargetTable = componentConfig?.targetTable || componentConfig?.saveTable; const componentFkColumn = componentConfig?.fkColumn; @@ -149,14 +149,11 @@ export function SimpleRepeaterTableComponent({ } // API 호출 - const response = await apiClient.post( - `/table-management/tables/${initialConfig.sourceTable}/data`, - { - search: filters, - page: 1, - size: 1000, // 대량 조회 - } - ); + const response = await apiClient.post(`/table-management/tables/${initialConfig.sourceTable}/data`, { + search: filters, + page: 1, + size: 1000, // 대량 조회 + }); if (response.data.success && response.data.data?.data) { const loadedData = response.data.data.data; @@ -182,7 +179,7 @@ export function SimpleRepeaterTableComponent({ // 2. 조인 데이터 처리 const joinColumns = columns.filter( - (col) => col.sourceConfig?.type === "join" && col.sourceConfig.joinTable && col.sourceConfig.joinKey + (col) => col.sourceConfig?.type === "join" && col.sourceConfig.joinTable && col.sourceConfig.joinKey, ); if (joinColumns.length > 0) { @@ -208,25 +205,20 @@ export function SimpleRepeaterTableComponent({ const [tableName] = groupKey.split(":"); // 조인 키 값 수집 (중복 제거) - const keyValues = Array.from(new Set( - baseMappedData - .map((row: any) => row[key]) - .filter((v: any) => v !== undefined && v !== null) - )); + const keyValues = Array.from( + new Set(baseMappedData.map((row: any) => row[key]).filter((v: any) => v !== undefined && v !== null)), + ); if (keyValues.length === 0) return; try { // 조인 테이블 조회 // refKey(타겟 테이블 컬럼)로 검색 - const response = await apiClient.post( - `/table-management/tables/${tableName}/data`, - { - search: { [refKey]: keyValues }, // { id: [1, 2, 3] } - page: 1, - size: 1000, - } - ); + const response = await apiClient.post(`/table-management/tables/${tableName}/data`, { + search: { [refKey]: keyValues }, // { id: [1, 2, 3] } + page: 1, + size: 1000, + }); if (response.data.success && response.data.data?.data) { const joinedRows = response.data.data.data; @@ -251,7 +243,7 @@ export function SimpleRepeaterTableComponent({ console.error(`조인 실패 (${tableName}):`, error); // 실패 시 무시하고 진행 (값은 undefined) } - }) + }), ); } @@ -296,7 +288,7 @@ export function SimpleRepeaterTableComponent({ // 🆕 컴포넌트 레벨의 targetTable이 설정되어 있으면 우선 사용 if (componentTargetTable) { console.log("✅ [SimpleRepeaterTable] 컴포넌트 레벨 저장 테이블 사용:", componentTargetTable); - + // 모든 행을 해당 테이블에 저장 const dataToSave = value.map((row: any) => { // 메타데이터 필드 제외 (_, _rowIndex 등) @@ -399,9 +391,12 @@ export function SimpleRepeaterTableComponent({ // 기존 onFormDataChange도 호출 (호환성) if (onFormDataChange && columnName) { // 테이블별 데이터를 통합하여 전달 - onFormDataChange(columnName, Object.entries(dataByTable).flatMap(([table, rows]) => - rows.map((row: any) => ({ ...row, _targetTable: table })) - )); + onFormDataChange( + columnName, + Object.entries(dataByTable).flatMap(([table, rows]) => + rows.map((row: any) => ({ ...row, _targetTable: table })), + ), + ); } }; @@ -543,24 +538,14 @@ export function SimpleRepeaterTableComponent({ if (!allowAdd || readOnly || value.length >= maxRows) return null; return ( - ); }; - const renderCell = ( - row: any, - column: SimpleRepeaterColumnConfig, - rowIndex: number - ) => { + const renderCell = (row: any, column: SimpleRepeaterColumnConfig, rowIndex: number) => { const cellValue = row[column.field]; // 계산 필드는 편집 불가 @@ -583,9 +568,7 @@ export function SimpleRepeaterTableComponent({ - handleCellEdit(rowIndex, column.field, parseFloat(e.target.value) || 0) - } + onChange={(e) => handleCellEdit(rowIndex, column.field, parseFloat(e.target.value) || 0)} className="h-7 text-xs" /> ); @@ -604,19 +587,19 @@ export function SimpleRepeaterTableComponent({ return ( ); @@ -636,11 +619,11 @@ export function SimpleRepeaterTableComponent({ // 로딩 중일 때 if (isLoading) { return ( -
+
- -

데이터를 불러오는 중...

+ +

데이터를 불러오는 중...

@@ -650,14 +633,14 @@ export function SimpleRepeaterTableComponent({ // 에러 발생 시 if (loadError) { return ( -
+
-
- +
+
-

데이터 로드 실패

-

{loadError}

+

데이터 로드 실패

+

{loadError}

@@ -668,30 +651,27 @@ export function SimpleRepeaterTableComponent({ const totalColumns = columns.length + (showRowNumber ? 1 : 0) + (allowDelete && !readOnly ? 1 : 0); return ( -
+
{/* 상단 행 추가 버튼 */} {allowAdd && addButtonPosition !== "bottom" && ( -
+
)} -
+
{showRowNumber && ( - )} {columns.map((col) => ( ))} {!readOnly && allowDelete && ( - )} @@ -708,11 +688,7 @@ export function SimpleRepeaterTableComponent({ {value.length === 0 ? ( - ) : ( value.map((row, rowIndex) => ( - + {showRowNumber && ( - )} @@ -743,7 +719,7 @@ export function SimpleRepeaterTableComponent({ size="sm" onClick={() => handleRowDelete(rowIndex)} disabled={value.length <= minRows} - className="h-7 w-7 p-0 text-destructive hover:text-destructive disabled:opacity-50" + className="text-destructive hover:text-destructive h-7 w-7 p-0 disabled:opacity-50" > @@ -758,35 +734,29 @@ export function SimpleRepeaterTableComponent({ {/* 합계 표시 */} {summaryConfig?.enabled && summaryValues && ( -
-
+
+
{summaryConfig.title && ( -
- {summaryConfig.title} -
+
{summaryConfig.title}
)} -
+
{summaryConfig.fields.map((field) => (
- {field.label} - + {field.label} + {formatSummaryValue(field, summaryValues[field.field] || 0)}
@@ -798,10 +768,10 @@ export function SimpleRepeaterTableComponent({ {/* 하단 행 추가 버튼 */} {allowAdd && addButtonPosition !== "top" && value.length > 0 && ( -
+
{maxRows !== Infinity && ( - + {value.length} / {maxRows} )} @@ -810,4 +780,3 @@ export function SimpleRepeaterTableComponent({
); } - diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index fcf2e97f..f8b154d6 100644 --- a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx @@ -1098,28 +1098,10 @@ export const ButtonPrimaryComponent: React.FC = ({ const screenContextFormData = screenContext?.formData || {}; const propsFormData = formData || {}; - // 🔧 디버그: formData 소스 확인 - console.log("🔍 [v2-button-primary] formData 소스 확인:", { - propsFormDataKeys: Object.keys(propsFormData), - screenContextFormDataKeys: Object.keys(screenContextFormData), - propsHasCompanyImage: "company_image" in propsFormData, - propsHasCompanyLogo: "company_logo" in propsFormData, - screenHasCompanyImage: "company_image" in screenContextFormData, - screenHasCompanyLogo: "company_logo" in screenContextFormData, - }); - // 병합: splitPanelParentData를 기본으로, props.formData, screenContext.formData 순으로 오버라이드 // (일반 폼 필드는 props.formData, RepeaterFieldGroup은 screenContext.formData에 있음) let effectiveFormData = { ...propsFormData, ...screenContextFormData }; - console.log("🔍 [v2-button-primary] effectiveFormData 병합 결과:", { - keys: Object.keys(effectiveFormData), - hasCompanyImage: "company_image" in effectiveFormData, - hasCompanyLogo: "company_logo" in effectiveFormData, - companyImageValue: effectiveFormData.company_image, - companyLogoValue: effectiveFormData.company_logo, - }); - // 분할 패널 우측이고 formData가 비어있으면 splitPanelParentData 사용 if (splitPanelPosition === "right" && Object.keys(effectiveFormData).length === 0 && splitPanelParentData) { effectiveFormData = { ...splitPanelParentData }; @@ -1289,20 +1271,18 @@ export const ButtonPrimaryComponent: React.FC = ({ // 🔧 component.style에서 background/backgroundColor 충돌 방지 (width/height는 허용) const userStyle = component.style ? Object.fromEntries( - Object.entries(component.style).filter( - ([key]) => !["background", "backgroundColor"].includes(key), - ), + Object.entries(component.style).filter(([key]) => !["background", "backgroundColor"].includes(key)), ) : {}; // 🔧 사용자가 설정한 크기 우선 사용, 없으면 100% - const buttonWidth = component.size?.width ? `${component.size.width}px` : (style?.width || "100%"); - const buttonHeight = component.size?.height ? `${component.size.height}px` : (style?.height || "100%"); + const buttonWidth = component.size?.width ? `${component.size.width}px` : style?.width || "100%"; + const buttonHeight = component.size?.height ? `${component.size.height}px` : style?.height || "100%"; const buttonElementStyle: React.CSSProperties = { width: buttonWidth, height: buttonHeight, - minHeight: "32px", // 🔧 최소 높이를 32px로 줄임 + minHeight: "32px", // 🔧 최소 높이를 32px로 줄임 border: "none", borderRadius: "0.5rem", backgroundColor: finalDisabled ? "#e5e7eb" : buttonColor, @@ -1328,26 +1308,26 @@ export const ButtonPrimaryComponent: React.FC = ({ // 버튼 텍스트 결정 (다양한 소스에서 가져옴) // "기본 버튼"은 컴포넌트 생성 시 기본값이므로 무시 const labelValue = component.label === "기본 버튼" ? undefined : component.label; - + // 액션 타입에 따른 기본 텍스트 (modal 액션과 동일하게) const actionType = processedConfig.action?.type || component.componentConfig?.action?.type; const actionDefaultText: Record = { save: "저장", - delete: "삭제", + delete: "삭제", modal: "등록", edit: "수정", copy: "복사", close: "닫기", cancel: "취소", }; - - const buttonContent = - processedConfig.text || - component.webTypeConfig?.text || - component.componentConfig?.text || - component.config?.text || + + const buttonContent = + processedConfig.text || + component.webTypeConfig?.text || + component.componentConfig?.text || + component.config?.text || component.style?.labelText || - labelValue || + labelValue || actionDefaultText[actionType as string] || "버튼"; diff --git a/frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx index cc2a8ea3..1f8232d8 100644 --- a/frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx @@ -123,34 +123,16 @@ const FileUploadComponent: React.FC = ({ }, [isRecordMode, recordTableName, recordId, columnName]); // 🔑 레코드별 고유 키 생성 (localStorage, 전역 상태용) + // 🆕 columnName을 포함하여 같은 화면의 여러 파일 업로드 컴포넌트 구분 const getUniqueKey = useCallback(() => { if (isRecordMode && recordTableName && recordId) { - // 레코드 모드: 테이블명:레코드ID:컴포넌트ID 형태로 고유 키 생성 - return `fileUpload_${recordTableName}_${recordId}_${component.id}`; + // 레코드 모드: 테이블명:레코드ID:컴포넌트ID:컬럼명 형태로 고유 키 생성 + return `fileUpload_${recordTableName}_${recordId}_${component.id}_${columnName}`; } - // 기본 모드: 컴포넌트 ID만 사용 - return `fileUpload_${component.id}`; - }, [isRecordMode, recordTableName, recordId, component.id]); + // 기본 모드: 컴포넌트 ID + 컬럼명 사용 + return `fileUpload_${component.id}_${columnName}`; + }, [isRecordMode, recordTableName, recordId, component.id, columnName]); - // 🔍 디버깅: 레코드 모드 상태 로깅 - useEffect(() => { - console.log("📎 [FileUploadComponent] 모드 확인:", { - isRecordMode, - recordTableName, - recordId, - columnName, - targetObjid: getRecordTargetObjid(), - uniqueKey: getUniqueKey(), - formDataKeys: formData ? Object.keys(formData) : [], - // 🔍 추가 디버깅: formData.id 확인 (수정 모드 판단에 사용됨) - "formData.id": formData?.id, - "formData.tableName": formData?.tableName, - "formData.image": formData?.image, - "component.tableName": component.tableName, - "component.columnName": component.columnName, - "component.id": component.id, - }); - }, [isRecordMode, recordTableName, recordId, columnName, getRecordTargetObjid, getUniqueKey, formData, component.tableName, component.columnName, component.id]); // 🆕 레코드 ID 변경 시 파일 목록 초기화 및 새로 로드 const prevRecordIdRef = useRef(null); @@ -160,19 +142,12 @@ const FileUploadComponent: React.FC = ({ const modeChanged = prevIsRecordModeRef.current !== null && prevIsRecordModeRef.current !== isRecordMode; if (recordIdChanged || modeChanged) { - console.log("📎 [FileUploadComponent] 레코드 상태 변경 감지:", { - prevRecordId: prevRecordIdRef.current, - currentRecordId: recordId, - prevIsRecordMode: prevIsRecordModeRef.current, - currentIsRecordMode: isRecordMode, - }); prevRecordIdRef.current = recordId; prevIsRecordModeRef.current = isRecordMode; // 레코드 ID가 변경되거나 등록 모드(isRecordMode=false)로 전환되면 파일 목록 초기화 // 등록 모드에서는 항상 빈 상태로 시작해야 함 if (isRecordMode || !recordId) { - console.log("📎 [FileUploadComponent] 파일 목록 초기화 (새 레코드 또는 레코드 변경)"); setUploadedFiles([]); setRepresentativeImageUrl(null); } @@ -189,7 +164,6 @@ const FileUploadComponent: React.FC = ({ // 등록 모드(새 레코드)인 경우 파일 복원 스킵 - 빈 상태 유지 if (!isRecordMode || !recordId) { - console.log("📎 [FileUploadComponent] 등록 모드: 파일 복원 스킵 (빈 상태 유지)"); return; } @@ -200,13 +174,6 @@ const FileUploadComponent: React.FC = ({ if (backupFiles) { const parsedFiles = JSON.parse(backupFiles); if (parsedFiles.length > 0) { - console.log("🚀 컴포넌트 마운트 시 파일 즉시 복원:", { - uniqueKey: backupKey, - componentId: component.id, - recordId: recordId, - restoredFiles: parsedFiles.length, - files: parsedFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })), - }); setUploadedFiles(parsedFiles); // 전역 상태에도 복원 (레코드별 고유 키 사용) @@ -224,26 +191,20 @@ const FileUploadComponent: React.FC = ({ }, [component.id, getUniqueKey, recordId, isRecordMode]); // 레코드별 고유 키 변경 시 재실행 // 🔑 수정 모드: formData[columnName]에 저장된 objid로 이미지 로드 - // 이 로직은 isRecordMode와 상관없이 formData에 이미지 objid가 있으면 표시 + // 🆕 formData 전체가 아닌 특정 컬럼 값만 의존하도록 수정 (다른 컴포넌트 영향 방지) + const imageObjidFromFormData = formData?.[columnName]; + useEffect(() => { - const imageObjid = formData?.[columnName]; - // 이미지 objid가 있고, 숫자 문자열인 경우에만 처리 - if (imageObjid && /^\d+$/.test(String(imageObjid))) { - console.log("🖼️ [FileUploadComponent] formData에서 이미지 objid 발견:", { - columnName, - imageObjid, - currentFilesCount: uploadedFiles.length, - }); + if (imageObjidFromFormData && /^\d+$/.test(String(imageObjidFromFormData))) { + const objidStr = String(imageObjidFromFormData); // 이미 같은 objid의 파일이 로드되어 있으면 스킵 - const alreadyLoaded = uploadedFiles.some(f => String(f.objid) === String(imageObjid)); + const alreadyLoaded = uploadedFiles.some(f => String(f.objid) === objidStr); if (alreadyLoaded) { - console.log("🖼️ [FileUploadComponent] 이미 로드된 이미지, 스킵"); return; } - const objidStr = String(imageObjid); const previewUrl = `/api/files/preview/${objidStr}`; // 🔑 실제 파일 정보 조회 @@ -254,12 +215,6 @@ const FileUploadComponent: React.FC = ({ if (fileInfoResponse.success && fileInfoResponse.data) { const { realFileName, fileSize, fileExt, regdate, isRepresentative } = fileInfoResponse.data; - console.log("🖼️ [FileUploadComponent] 파일 정보 조회 성공:", { - objid: objidStr, - realFileName, - fileExt, - }); - const fileInfo = { objid: objidStr, realFileName: realFileName, @@ -296,46 +251,39 @@ const FileUploadComponent: React.FC = ({ } })(); } - }, [formData, columnName, uploadedFiles]); + }, [imageObjidFromFormData, columnName, component.id]); // 🆕 formData 대신 특정 컬럼 값만 의존 // 🎯 화면설계 모드에서 실제 화면으로의 실시간 동기화 이벤트 리스너 + // 🆕 columnName도 체크하여 같은 화면의 다른 파일 업로드 컴포넌트와 구분 useEffect(() => { const handleDesignModeFileChange = (event: CustomEvent) => { - console.log("🎯🎯🎯 FileUploadComponent 화면설계 모드 파일 변경 이벤트 수신:", { - eventComponentId: event.detail.componentId, - currentComponentId: component.id, - isMatch: event.detail.componentId === component.id, - filesCount: event.detail.files?.length || 0, - action: event.detail.action, - source: event.detail.source, - eventDetail: event.detail, - }); + const eventColumnName = event.detail.eventColumnName || event.detail.columnName; + + // 🆕 고유 키 또는 (컴포넌트ID + 컬럼명) 조합으로 체크 + const isForThisComponent = + (event.detail.uniqueKey && event.detail.uniqueKey === currentUniqueKey) || + (event.detail.componentId === component.id && eventColumnName === columnName) || + (event.detail.componentId === component.id && !eventColumnName); // 이전 호환성 - // 현재 컴포넌트와 일치하고 화면설계 모드에서 온 이벤트인 경우 - if (event.detail.componentId === component.id && event.detail.source === "designMode") { + // 🆕 현재 컴포넌트와 일치하고 화면설계 모드에서 온 이벤트인 경우 + if (isForThisComponent && event.detail.source === "designMode") { // 파일 상태 업데이트 const newFiles = event.detail.files || []; setUploadedFiles(newFiles); // localStorage 백업 업데이트 (레코드별 고유 키 사용) try { - const backupKey = getUniqueKey(); + const backupKey = currentUniqueKey; localStorage.setItem(backupKey, JSON.stringify(newFiles)); - console.log("💾 화면설계 모드 동기화 후 localStorage 백업 업데이트:", { - uniqueKey: backupKey, - componentId: component.id, - recordId: recordId, - fileCount: newFiles.length, - }); } catch (e) { console.warn("localStorage 백업 업데이트 실패:", e); } - // 전역 상태 업데이트 + // 전역 상태 업데이트 (🆕 고유 키 사용) if (typeof window !== "undefined") { (window as any).globalFileState = { ...(window as any).globalFileState, - [component.id]: newFiles, + [currentUniqueKey]: newFiles, }; } @@ -346,11 +294,6 @@ const FileUploadComponent: React.FC = ({ lastFileUpdate: event.detail.timestamp, }); } - - console.log("🎉🎉🎉 화면설계 모드 → 실제 화면 동기화 완료:", { - componentId: component.id, - finalFileCount: newFiles.length, - }); } }; @@ -369,25 +312,10 @@ const FileUploadComponent: React.FC = ({ // 🔑 등록 모드(새 레코드)인 경우 파일 조회 스킵 - 빈 상태 유지 if (!isRecordMode || !recordId) { - console.log("📂 [FileUploadComponent] 등록 모드: 파일 조회 스킵 (빈 상태 유지)", { - isRecordMode, - recordId, - componentId: component.id, - }); return false; } try { - // 🔑 레코드 모드: 해당 행의 파일만 조회 - if (isRecordMode && recordTableName && recordId) { - console.log("📂 [FileUploadComponent] 레코드 모드 파일 조회:", { - tableName: recordTableName, - recordId: recordId, - columnName: columnName, - targetObjid: getRecordTargetObjid(), - }); - } - // 1. formData에서 screenId 가져오기 let screenId = formData?.screenId; @@ -424,8 +352,6 @@ const FileUploadComponent: React.FC = ({ columnName: columnName, // 🔑 레코드 모드에서 사용하는 columnName }; - console.log("📂 [FileUploadComponent] 파일 조회 파라미터:", params); - const response = await getComponentFiles(params); if (response.success) { @@ -457,12 +383,6 @@ const FileUploadComponent: React.FC = ({ const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid)); finalFiles = [...formattedFiles, ...additionalFiles]; - console.log("📂 [FileUploadComponent] 파일 병합 완료:", { - uniqueKey, - serverFiles: formattedFiles.length, - localFiles: parsedBackupFiles.length, - finalFiles: finalFiles.length, - }); } } catch (e) { console.warn("파일 병합 중 오류:", e); @@ -505,16 +425,6 @@ const FileUploadComponent: React.FC = ({ const componentFiles = (component as any)?.uploadedFiles || []; const lastUpdate = (component as any)?.lastFileUpdate; - console.log("🔄 FileUploadComponent 파일 동기화 시작:", { - componentId: component.id, - componentFiles: componentFiles.length, - formData: formData, - screenId: formData?.screenId, - tableName: formData?.tableName, // 🔍 테이블명 확인 - recordId: formData?.id, // 🔍 레코드 ID 확인 - currentUploadedFiles: uploadedFiles.length, - }); - // 🔒 항상 DB에서 최신 파일 목록을 조회 (멀티테넌시 격리) loadComponentFiles().then((dbLoadSuccess) => { if (dbLoadSuccess) { @@ -523,9 +433,10 @@ const FileUploadComponent: React.FC = ({ // DB 로드 실패 시에만 기존 로직 사용 (하위 호환성) - // 전역 상태에서 최신 파일 정보 가져오기 + // 전역 상태에서 최신 파일 정보 가져오기 (🆕 고유 키 사용) const globalFileState = typeof window !== "undefined" ? (window as any).globalFileState || {} : {}; - const globalFiles = globalFileState[component.id] || []; + const uniqueKeyForFallback = getUniqueKey(); + const globalFiles = globalFileState[uniqueKeyForFallback] || globalFileState[component.id] || []; // 최신 파일 정보 사용 (전역 상태 > 컴포넌트 속성) const currentFiles = globalFiles.length > 0 ? globalFiles : componentFiles; @@ -540,36 +451,27 @@ const FileUploadComponent: React.FC = ({ }, [loadComponentFiles, component.id, (component as any)?.uploadedFiles, (component as any)?.lastFileUpdate]); // 전역 상태 변경 감지 (모든 파일 컴포넌트 동기화 + 화면 복원) + // 🆕 columnName을 포함한 고유 키로 구분하여 다른 파일 업로드 컴포넌트에 영향 방지 + const currentUniqueKey = getUniqueKey(); + useEffect(() => { const handleGlobalFileStateChange = (event: CustomEvent) => { - const { componentId, files, fileCount, timestamp, isRestore } = event.detail; + const { componentId, files, fileCount, timestamp, isRestore, uniqueKey: eventUniqueKey, eventColumnName } = event.detail; - console.log("🔄 FileUploadComponent 전역 상태 변경 감지:", { - currentComponentId: component.id, - eventComponentId: componentId, - isForThisComponent: componentId === component.id, - newFileCount: fileCount, - currentFileCount: uploadedFiles.length, - timestamp, - isRestore: !!isRestore, - }); + // 🆕 고유 키 또는 (컴포넌트ID + 컬럼명) 조합으로 체크 + const isForThisComponent = + (eventUniqueKey && eventUniqueKey === currentUniqueKey) || + (componentId === component.id && eventColumnName === columnName); - // 같은 컴포넌트 ID인 경우에만 업데이트 - if (componentId === component.id) { - const logMessage = isRestore ? "🔄 화면 복원으로 파일 상태 동기화" : "✅ 파일 상태 동기화 적용"; - console.log(logMessage, { - componentId: component.id, - 이전파일수: uploadedFiles?.length || 0, - 새파일수: files?.length || 0, - files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName })) || [], - }); + // 🆕 같은 고유 키인 경우에만 업데이트 (componentId + columnName 조합) + if (isForThisComponent) { setUploadedFiles(files); setForceUpdate((prev) => prev + 1); // localStorage 백업도 업데이트 (레코드별 고유 키 사용) try { - const backupKey = getUniqueKey(); + const backupKey = currentUniqueKey; localStorage.setItem(backupKey, JSON.stringify(files)); } catch (e) { console.warn("localStorage 백업 실패:", e); @@ -584,7 +486,7 @@ const FileUploadComponent: React.FC = ({ window.removeEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener); }; } - }, [component.id, uploadedFiles.length]); + }, [component.id, columnName, currentUniqueKey, uploadedFiles.length]); // 파일 업로드 설정 - componentConfig가 undefined일 수 있으므로 안전하게 처리 const safeComponentConfig = componentConfig || {}; @@ -598,18 +500,8 @@ const FileUploadComponent: React.FC = ({ // 파일 선택 핸들러 const handleFileSelect = useCallback(() => { - console.log("🎯 handleFileSelect 호출됨:", { - hasFileInputRef: !!fileInputRef.current, - fileInputRef: fileInputRef.current, - fileInputType: fileInputRef.current?.type, - fileInputHidden: fileInputRef.current?.className, - }); - if (fileInputRef.current) { - console.log("✅ fileInputRef.current.click() 호출"); fileInputRef.current.click(); - } else { - console.log("❌ fileInputRef.current가 null입니다"); } }, []); @@ -680,34 +572,17 @@ const FileUploadComponent: React.FC = ({ if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) { // 🎯 레코드 모드: 특정 행에 파일 연결 targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`; - console.log("📁 [레코드 모드] 파일 업로드:", { - targetObjid, - tableName: effectiveTableName, - recordId: effectiveRecordId, - columnName: effectiveColumnName, - }); } else if (screenId) { // 🔑 템플릿 파일 (백엔드 조회 형식과 동일하게) targetObjid = `screen_files:${screenId}:${component.id}:${effectiveColumnName}`; - console.log("📝 [템플릿 모드] 파일 업로드:", targetObjid); } else { // 기본값 (화면관리에서 사용) targetObjid = `temp_${component.id}`; - console.log("📝 [기본 모드] 파일 업로드:", targetObjid); } // 🔒 현재 사용자의 회사 코드 가져오기 (멀티테넌시 격리) const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode; - console.log("📤 [FileUploadComponent] 파일 업로드 준비:", { - userCompanyCode, - isRecordMode: effectiveIsRecordMode, - tableName: effectiveTableName, - recordId: effectiveRecordId, - columnName: effectiveColumnName, - targetObjid, - }); - // 🔑 레코드 모드일 때는 effectiveTableName을 우선 사용 // formData.linkedTable이 'screen_files' 같은 기본값일 수 있으므로 레코드 모드에서는 무시 const finalLinkedTable = effectiveIsRecordMode @@ -732,27 +607,11 @@ const FileUploadComponent: React.FC = ({ isRecordMode: effectiveIsRecordMode, }; - console.log("📤 [FileUploadComponent] uploadData 최종:", { - isRecordMode: effectiveIsRecordMode, - linkedTable: finalLinkedTable, - recordId: effectiveRecordId, - columnName: effectiveColumnName, - targetObjid, - }); - - - console.log("🚀 [FileUploadComponent] uploadFiles API 호출 직전:", { - filesCount: filesToUpload.length, - uploadData, - }); - const response = await uploadFiles({ files: filesToUpload, ...uploadData, }); - console.log("📥 [FileUploadComponent] uploadFiles API 응답:", response); - if (response.success) { // FileUploadResponse 타입에 맞게 files 배열 사용 const fileData = response.files || (response as any).data || []; @@ -811,9 +670,11 @@ const FileUploadComponent: React.FC = ({ }); // 모든 파일 컴포넌트에 동기화 이벤트 발생 + // 🆕 columnName 추가하여 같은 화면의 다른 파일 업로드 컴포넌트와 구분 const syncEvent = new CustomEvent("globalFileStateChanged", { detail: { componentId: component.id, + eventColumnName: columnName, // 🆕 컬럼명 추가 uniqueKey: uniqueKey, // 🆕 고유 키 추가 recordId: recordId, // 🆕 레코드 ID 추가 files: updatedFiles, @@ -822,25 +683,11 @@ const FileUploadComponent: React.FC = ({ }, }); window.dispatchEvent(syncEvent); - - console.log("🌐 전역 파일 상태 업데이트 및 동기화 이벤트 발생:", { - componentId: component.id, - fileCount: updatedFiles.length, - globalState: Object.keys(globalFileState).map((id) => ({ - id, - fileCount: globalFileState[id]?.length || 0, - })), - }); } // 컴포넌트 업데이트 if (onUpdate) { const timestamp = Date.now(); - console.log("🔄 onUpdate 호출:", { - componentId: component.id, - uploadedFiles: updatedFiles.length, - timestamp: timestamp, - }); onUpdate({ uploadedFiles: updatedFiles, lastFileUpdate: timestamp, @@ -858,15 +705,6 @@ const FileUploadComponent: React.FC = ({ ? fileObjids.join(',') // 복수 파일: 콤마 구분 : (fileObjids[0] || ''); // 단일 파일: 첫 번째 파일 ID - console.log("📎 [파일 업로드] 컬럼 데이터 동기화:", { - tableName: effectiveTableName, - recordId: effectiveRecordId, - columnName: effectiveColumnName, - columnValue, - fileCount: updatedFiles.length, - isMultiple: fileConfig.multiple, - }); - // onFormDataChange를 (fieldName, value) 형태로 호출 (SaveModal 호환) onFormDataChange(effectiveColumnName, columnValue); } @@ -883,13 +721,6 @@ const FileUploadComponent: React.FC = ({ }, }); window.dispatchEvent(refreshEvent); - console.log("🔄 그리드 파일 상태 새로고침 이벤트 발생:", { - tableName: effectiveTableName, - recordId: effectiveRecordId, - columnName: effectiveColumnName, - targetObjid, - fileCount: updatedFiles.length, - }); } // 컴포넌트 설정 콜백 @@ -972,9 +803,11 @@ const FileUploadComponent: React.FC = ({ (window as any).globalFileState = globalFileState; // 모든 파일 컴포넌트에 동기화 이벤트 발생 + // 🆕 columnName 추가하여 같은 화면의 다른 파일 업로드 컴포넌트와 구분 const syncEvent = new CustomEvent("globalFileStateChanged", { detail: { componentId: component.id, + eventColumnName: columnName, // 🆕 컬럼명 추가 uniqueKey: uniqueKey, // 🆕 고유 키 추가 recordId: recordId, // 🆕 레코드 ID 추가 files: updatedFiles, @@ -985,12 +818,6 @@ const FileUploadComponent: React.FC = ({ }, }); window.dispatchEvent(syncEvent); - - console.log("🗑️ 파일 삭제 후 전역 상태 동기화:", { - componentId: component.id, - deletedFile: fileName, - remainingFiles: updatedFiles.length, - }); } // 컴포넌트 업데이트 @@ -1010,14 +837,6 @@ const FileUploadComponent: React.FC = ({ ? fileObjids.join(',') : (fileObjids[0] || ''); - console.log("📎 [파일 삭제] 컬럼 데이터 동기화:", { - tableName: recordTableName, - recordId: recordId, - columnName: columnName, - columnValue, - remainingFiles: updatedFiles.length, - }); - // onFormDataChange를 (fieldName, value) 형태로 호출 (SaveModal 호환) onFormDataChange(columnName, columnValue); } @@ -1053,16 +872,10 @@ const FileUploadComponent: React.FC = ({ // 🔑 이미 previewUrl이 설정된 경우 바로 사용 (API 호출 스킵) if (file.previewUrl) { - console.log("🖼️ 대표 이미지: previewUrl 사용:", file.previewUrl); setRepresentativeImageUrl(file.previewUrl); return; } - console.log("🖼️ 대표 이미지 로드 시작:", { - objid: file.objid, - fileName: file.realFileName, - }); - // API 클라이언트를 통해 Blob으로 다운로드 (인증 토큰 포함) // 🔑 download 대신 preview 사용 (공개 접근) const response = await apiClient.get(`/files/preview/${file.objid}`, { @@ -1082,7 +895,6 @@ const FileUploadComponent: React.FC = ({ } setRepresentativeImageUrl(url); - console.log("✅ 대표 이미지 로드 성공:", url); } catch (error: any) { console.error("❌ 대표 이미지 로드 실패:", { file: file.realFileName, @@ -1113,12 +925,6 @@ const FileUploadComponent: React.FC = ({ // 대표 이미지 로드 loadRepresentativeImage(file); - - console.log("✅ 대표 파일 설정 완료:", { - componentId: component.id, - representativeFile: file.realFileName, - objid: file.objid, - }); } catch (e) { console.error("❌ 대표 파일 설정 실패:", e); } @@ -1146,22 +952,13 @@ const FileUploadComponent: React.FC = ({ // 드래그 앤 드롭 핸들러 const handleDragOver = useCallback( (e: React.DragEvent) => { - console.log("🎯 드래그 오버 이벤트 감지:", { - readonly: safeComponentConfig.readonly, - disabled: safeComponentConfig.disabled, - dragOver: dragOver, - }); - e.preventDefault(); e.stopPropagation(); if (!safeComponentConfig.readonly && !safeComponentConfig.disabled) { setDragOver(true); - console.log("✅ 드래그 오버 활성화"); - } else { - console.log("❌ 드래그 차단됨: readonly 또는 disabled"); } }, - [safeComponentConfig.readonly, safeComponentConfig.disabled, dragOver], + [safeComponentConfig.readonly, safeComponentConfig.disabled], ); const handleDragLeave = useCallback((e: React.DragEvent) => { @@ -1189,19 +986,10 @@ const FileUploadComponent: React.FC = ({ // 클릭 핸들러 const handleClick = useCallback( (e: React.MouseEvent) => { - console.log("🖱️ 파일 업로드 영역 클릭:", { - readonly: safeComponentConfig.readonly, - disabled: safeComponentConfig.disabled, - hasHandleFileSelect: !!handleFileSelect, - }); - e.preventDefault(); e.stopPropagation(); if (!safeComponentConfig.readonly && !safeComponentConfig.disabled) { - console.log("✅ 파일 선택 함수 호출"); handleFileSelect(); - } else { - console.log("❌ 클릭 차단됨: readonly 또는 disabled"); } onClick?.(); }, diff --git a/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx b/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx index 4f5da129..c9b738a1 100644 --- a/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx +++ b/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx @@ -23,9 +23,15 @@ export class V2SelectRenderer extends AutoRegisteringComponentRenderer { // formData에서 현재 값 가져오기 (기본값 지원) const defaultValue = config.defaultValue || ""; let currentValue = formData?.[columnName] ?? component.value ?? ""; - + // 🆕 formData에 값이 없고 기본값이 설정된 경우, 기본값 적용 - if ((currentValue === "" || currentValue === undefined || currentValue === null) && defaultValue && isInteractive && onFormDataChange && columnName) { + if ( + (currentValue === "" || currentValue === undefined || currentValue === null) && + defaultValue && + isInteractive && + onFormDataChange && + columnName + ) { // 초기 렌더링 시 기본값을 formData에 설정 setTimeout(() => { if (!formData?.[columnName]) { diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index 7f2f1fa3..b820d370 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -1033,7 +1033,6 @@ export const TableListComponent: React.FC = ({ // localStorage에 저장된 정렬이 없으면 defaultSort 설정 적용 if (tableConfig.defaultSort?.columnName) { - console.log("📊 기본 정렬 설정 적용:", tableConfig.defaultSort); setSortColumn(tableConfig.defaultSort.columnName); setSortDirection(tableConfig.defaultSort.direction || "asc"); hasInitializedSort.current = true; @@ -1139,16 +1138,6 @@ export const TableListComponent: React.FC = ({ }); } - // 🔍 디버깅: 캐시 사용 시 로그 - console.log("📊 [TableListComponent] 캐시에서 inputTypes 로드:", { - tableName: tableConfig.selectedTable, - cacheKey: cacheKey, - hasInputTypes: !!cached.inputTypes, - inputTypesLength: cached.inputTypes?.length || 0, - imageInputType: inputTypeMap["image"], - cacheAge: Date.now() - cached.timestamp, - }); - cached.columns.forEach((col: any) => { labels[col.columnName] = col.displayName || col.comment || col.columnName; meta[col.columnName] = { @@ -1172,14 +1161,6 @@ export const TableListComponent: React.FC = ({ inputTypeMap[col.columnName] = col.inputType; }); - // 🔍 디버깅: inputTypes 확인 - console.log("📊 [TableListComponent] inputTypes 조회 결과:", { - tableName: tableConfig.selectedTable, - inputTypes: inputTypes, - inputTypeMap: inputTypeMap, - imageColumn: inputTypes.find((col: any) => col.columnName === "image"), - }); - tableColumnCache.set(cacheKey, { columns, inputTypes, @@ -4079,17 +4060,6 @@ export const TableListComponent: React.FC = ({ // inputType 기반 포맷팅 (columnMeta에서 가져온 inputType 우선) const inputType = meta?.inputType || column.inputType; - // 🔍 디버깅: image 컬럼인 경우 로그 출력 - if (column.columnName === "image") { - console.log("🖼️ [formatCellValue] image 컬럼 처리:", { - columnName: column.columnName, - value: value, - meta: meta, - inputType: inputType, - columnInputType: column.inputType, - }); - } - // 🖼️ 이미지 타입: 작은 썸네일 표시 if (inputType === "image" && value) { // value가 objid (숫자 또는 숫자 문자열)인 경우 파일 API URL 사용 diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx index d297f860..a715e408 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx @@ -5,32 +5,10 @@ import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { tableTypeApi } from "@/lib/api/screen"; @@ -52,10 +30,7 @@ interface ColumnInfo { displayName: string; } -export function TimelineSchedulerConfigPanel({ - config, - onChange, -}: TimelineSchedulerConfigPanelProps) { +export function TimelineSchedulerConfigPanel({ config, onChange }: TimelineSchedulerConfigPanelProps) { const [tables, setTables] = useState([]); const [sourceColumns, setSourceColumns] = useState([]); const [resourceColumns, setResourceColumns] = useState([]); @@ -74,7 +49,7 @@ export function TimelineSchedulerConfigPanel({ tableList.map((t: any) => ({ tableName: t.table_name || t.tableName, displayName: t.display_name || t.displayName || t.table_name || t.tableName, - })) + })), ); } } catch (err) { @@ -100,7 +75,7 @@ export function TimelineSchedulerConfigPanel({ columns.map((col: any) => ({ columnName: col.column_name || col.columnName, displayName: col.display_name || col.displayName || col.column_name || col.columnName, - })) + })), ); } } catch (err) { @@ -125,7 +100,7 @@ export function TimelineSchedulerConfigPanel({ columns.map((col: any) => ({ columnName: col.column_name || col.columnName, displayName: col.display_name || col.displayName || col.column_name || col.columnName, - })) + })), ); } } catch (err) { @@ -168,11 +143,9 @@ export function TimelineSchedulerConfigPanel({ {/* 소스 데이터 설정 (스케줄 생성 기준) */} - - 스케줄 생성 설정 - + 스케줄 생성 설정 -

+

스케줄 자동 생성 시 참조할 원본 데이터 설정 (저장: schedule_mng)

@@ -208,20 +181,14 @@ export function TimelineSchedulerConfigPanel({ className="h-8 w-full justify-between text-xs" disabled={loading} > - {config.sourceConfig?.tableName ? ( - tables.find((t) => t.tableName === config.sourceConfig?.tableName) - ?.displayName || config.sourceConfig.tableName - ) : ( - "소스 테이블 선택..." - )} + {config.sourceConfig?.tableName + ? tables.find((t) => t.tableName === config.sourceConfig?.tableName)?.displayName || + config.sourceConfig.tableName + : "소스 테이블 선택..."} - + { const lowerSearch = search.toLowerCase(); @@ -233,9 +200,7 @@ export function TimelineSchedulerConfigPanel({ > - - 테이블을 찾을 수 없습니다. - + 테이블을 찾을 수 없습니다. {tables.map((table) => (
{table.displayName} - - {table.tableName} - + {table.tableName}
))} @@ -272,11 +233,11 @@ export function TimelineSchedulerConfigPanel({ {/* 소스 필드 매핑 */} {config.sourceConfig?.tableName && ( -
+
{/* 기준일 필드 */} -
+
-

- 스케줄 종료일로 사용됩니다 -

+

스케줄 종료일로 사용됩니다

{/* 수량 필드 */} @@ -339,7 +298,7 @@ export function TimelineSchedulerConfigPanel({
{/* 그룹명 필드 */} -
+
- updateConfig({ defaultZoomLevel: v as any }) - } + onValueChange={(v) => updateConfig({ defaultZoomLevel: v as any })} > @@ -534,9 +469,7 @@ export function TimelineSchedulerConfigPanel({ - updateConfig({ height: parseInt(e.target.value) || 500 }) - } + onChange={(e) => updateConfig({ height: parseInt(e.target.value) || 500 })} className="h-8 text-xs" />
@@ -547,9 +480,7 @@ export function TimelineSchedulerConfigPanel({ - updateConfig({ rowHeight: parseInt(e.target.value) || 50 }) - } + onChange={(e) => updateConfig({ rowHeight: parseInt(e.target.value) || 50 })} className="h-8 text-xs" />
@@ -558,26 +489,17 @@ export function TimelineSchedulerConfigPanel({
- updateConfig({ editable: v })} - /> + updateConfig({ editable: v })} />
- updateConfig({ draggable: v })} - /> + updateConfig({ draggable: v })} />
- updateConfig({ resizable: v })} - /> + updateConfig({ resizable: 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 7ce7a9d6..94c001d4 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts +++ b/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts @@ -3,13 +3,7 @@ import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { apiClient } from "@/lib/api/client"; import { v2EventBus, V2_EVENTS } from "@/lib/v2-core"; -import { - TimelineSchedulerConfig, - ScheduleItem, - Resource, - ZoomLevel, - UseTimelineDataResult, -} from "../types"; +import { TimelineSchedulerConfig, ScheduleItem, Resource, ZoomLevel, UseTimelineDataResult } from "../types"; import { zoomLevelDays, defaultTimelineSchedulerConfig } from "../config"; // schedule_mng 테이블 고정 (공통 스케줄 테이블) @@ -37,16 +31,14 @@ const addDays = (date: Date, days: number): Date => { export function useTimelineData( config: TimelineSchedulerConfig, externalSchedules?: ScheduleItem[], - externalResources?: Resource[] + externalResources?: Resource[], ): UseTimelineDataResult { // 상태 const [schedules, setSchedules] = useState([]); const [resources, setResources] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const [zoomLevel, setZoomLevel] = useState( - config.defaultZoomLevel || "day" - ); + const [zoomLevel, setZoomLevel] = useState(config.defaultZoomLevel || "day"); const [viewStartDate, setViewStartDate] = useState(() => { if (config.initialDate) { return new Date(config.initialDate); @@ -69,9 +61,7 @@ export function useTimelineData( }, [viewStartDate, zoomLevel]); // 테이블명: 기본적으로 schedule_mng 사용, 커스텀 테이블 설정 시 해당 테이블 사용 - const tableName = config.useCustomTable && config.customTableName - ? config.customTableName - : SCHEDULE_TABLE; + const tableName = config.useCustomTable && config.customTableName ? config.customTableName : SCHEDULE_TABLE; const resourceTableName = config.resourceTable; @@ -88,7 +78,7 @@ export function useTimelineData( 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", @@ -134,17 +124,13 @@ export function useTimelineData( sourceKeys: currentSourceKeys, }); - const response = await apiClient.post( - `/table-management/tables/${tableName}/data`, - { - page: 1, - size: 10000, - autoFilter: true, - } - ); + const response = await apiClient.post(`/table-management/tables/${tableName}/data`, { + page: 1, + size: 10000, + autoFilter: true, + }); - const responseData = - response.data?.data?.data || response.data?.data || []; + const responseData = response.data?.data?.data || response.data?.data || []; let rawData = Array.isArray(responseData) ? responseData : []; // 클라이언트 측 필터링 적용 (schedule_mng 테이블인 경우) @@ -156,9 +142,7 @@ export function useTimelineData( // 선택된 품목 필터 (source_group_key 기준) if (currentSourceKeys.length > 0) { - rawData = rawData.filter((row: any) => - currentSourceKeys.includes(row.source_group_key) - ); + rawData = rawData.filter((row: any) => currentSourceKeys.includes(row.source_group_key)); } console.log("[useTimelineData] 필터링 후 스케줄:", rawData.length, "건"); @@ -194,9 +178,7 @@ export function useTimelineData( title: String(row[effectiveMapping.title] || ""), startDate: row[effectiveMapping.startDate] || "", endDate: row[effectiveMapping.endDate] || "", - status: effectiveMapping.status - ? row[effectiveMapping.status] || "planned" - : "planned", + status: effectiveMapping.status ? row[effectiveMapping.status] || "planned" : "planned", progress, color: fieldMapping.color ? row[fieldMapping.color] : undefined, data: row, @@ -228,26 +210,20 @@ export function useTimelineData( } try { - const response = await apiClient.post( - `/table-management/tables/${resourceTableName}/data`, - { - page: 1, - size: 1000, - autoFilter: true, - } - ); + const response = await apiClient.post(`/table-management/tables/${resourceTableName}/data`, { + page: 1, + size: 1000, + autoFilter: true, + }); - const responseData = - response.data?.data?.data || response.data?.data || []; + const responseData = response.data?.data?.data || response.data?.data || []; const rawData = Array.isArray(responseData) ? responseData : []; // 데이터를 Resource 형태로 변환 const mappedResources: Resource[] = rawData.map((row: any) => ({ id: String(row[resourceFieldMapping.id] || ""), name: String(row[resourceFieldMapping.name] || ""), - group: resourceFieldMapping.group - ? row[resourceFieldMapping.group] - : undefined, + group: resourceFieldMapping.group ? row[resourceFieldMapping.group] : undefined, })); setResources(mappedResources); @@ -270,44 +246,41 @@ export function useTimelineData( // 이벤트 버스 리스너 - 테이블 선택 변경 (품목 선택 시 해당 스케줄만 표시) useEffect(() => { - const unsubscribeSelection = v2EventBus.subscribe( - V2_EVENTS.TABLE_SELECTION_CHANGE, - (payload) => { - console.log("[useTimelineData] TABLE_SELECTION_CHANGE 수신:", { - tableName: payload.tableName, - selectedCount: payload.selectedCount, - }); + const unsubscribeSelection = v2EventBus.subscribe(V2_EVENTS.TABLE_SELECTION_CHANGE, (payload) => { + console.log("[useTimelineData] TABLE_SELECTION_CHANGE 수신:", { + tableName: payload.tableName, + selectedCount: payload.selectedCount, + }); - // 설정된 그룹 필드명 사용 (없으면 기본값들 fallback) - const groupByField = config.sourceConfig?.groupByField; + // 설정된 그룹 필드명 사용 (없으면 기본값들 fallback) + const groupByField = config.sourceConfig?.groupByField; - // 선택된 데이터에서 source_group_key 추출 - const sourceKeys: string[] = []; - for (const row of payload.selectedRows || []) { - // 설정된 필드명 우선, 없으면 일반적인 필드명 fallback - let key: string | undefined; - if (groupByField && row[groupByField]) { - key = row[groupByField]; - } else { - // fallback: 일반적으로 사용되는 필드명들 - key = row.part_code || row.source_group_key || row.item_code; - } - - if (key && !sourceKeys.includes(key)) { - sourceKeys.push(key); - } + // 선택된 데이터에서 source_group_key 추출 + const sourceKeys: string[] = []; + for (const row of payload.selectedRows || []) { + // 설정된 필드명 우선, 없으면 일반적인 필드명 fallback + let key: string | undefined; + if (groupByField && row[groupByField]) { + key = row[groupByField]; + } else { + // fallback: 일반적으로 사용되는 필드명들 + key = row.part_code || row.source_group_key || row.item_code; } - console.log("[useTimelineData] 선택된 그룹 키:", { - groupByField, - keys: sourceKeys, - }); - - // 상태 업데이트 및 ref 동기화 - selectedSourceKeysRef.current = sourceKeys; - setSelectedSourceKeys(sourceKeys); + if (key && !sourceKeys.includes(key)) { + sourceKeys.push(key); + } } - ); + + console.log("[useTimelineData] 선택된 그룹 키:", { + groupByField, + keys: sourceKeys, + }); + + // 상태 업데이트 및 ref 동기화 + selectedSourceKeysRef.current = sourceKeys; + setSelectedSourceKeys(sourceKeys); + }); return () => { unsubscribeSelection(); @@ -325,27 +298,21 @@ export function useTimelineData( // 이벤트 버스 리스너 - 스케줄 생성 완료 및 테이블 새로고침 useEffect(() => { // TABLE_REFRESH 이벤트 수신 - 스케줄 새로고침 - const unsubscribeRefresh = v2EventBus.subscribe( - V2_EVENTS.TABLE_REFRESH, - (payload) => { - // schedule_mng 또는 해당 테이블에 대한 새로고침 - if (payload.tableName === tableName || payload.tableName === SCHEDULE_TABLE) { - console.log("[useTimelineData] TABLE_REFRESH 수신, 스케줄 새로고침:", payload); - fetchSchedules(); - } + const unsubscribeRefresh = v2EventBus.subscribe(V2_EVENTS.TABLE_REFRESH, (payload) => { + // schedule_mng 또는 해당 테이블에 대한 새로고침 + if (payload.tableName === tableName || payload.tableName === SCHEDULE_TABLE) { + console.log("[useTimelineData] TABLE_REFRESH 수신, 스케줄 새로고침:", payload); + fetchSchedules(); } - ); + }); // SCHEDULE_GENERATE_COMPLETE 이벤트 수신 - 스케줄 자동 생성 완료 시 새로고침 - const unsubscribeComplete = v2EventBus.subscribe( - V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, - (payload) => { - if (payload.success) { - console.log("[useTimelineData] SCHEDULE_GENERATE_COMPLETE 수신, 스케줄 새로고침:", payload); - fetchSchedules(); - } + const unsubscribeComplete = v2EventBus.subscribe(V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, (payload) => { + if (payload.success) { + console.log("[useTimelineData] SCHEDULE_GENERATE_COMPLETE 수신, 스케줄 새로고침:", payload); + fetchSchedules(); } - ); + }); return () => { unsubscribeRefresh(); @@ -390,23 +357,20 @@ export function useTimelineData( if (updates.endDate) updateData[fieldMapping.endDate] = updates.endDate; if (updates.resourceId) updateData[fieldMapping.resourceId] = updates.resourceId; if (updates.title) updateData[fieldMapping.title] = updates.title; - if (updates.status && fieldMapping.status) - updateData[fieldMapping.status] = updates.status; + if (updates.status && fieldMapping.status) updateData[fieldMapping.status] = updates.status; if (updates.progress !== undefined && fieldMapping.progress) updateData[fieldMapping.progress] = updates.progress; await apiClient.put(`/table-management/tables/${tableName}/data/${id}`, updateData); // 로컬 상태 업데이트 - setSchedules((prev) => - prev.map((s) => (s.id === id ? { ...s, ...updates } : s)) - ); + setSchedules((prev) => prev.map((s) => (s.id === id ? { ...s, ...updates } : s))); } catch (err: any) { console.error("스케줄 업데이트 오류:", err); throw err; } }, - [tableName, fieldMapping, config.editable] + [tableName, fieldMapping, config.editable], ); // 스케줄 추가 @@ -427,10 +391,7 @@ export function useTimelineData( if (fieldMapping.progress && schedule.progress !== undefined) insertData[fieldMapping.progress] = schedule.progress; - const response = await apiClient.post( - `/table-management/tables/${tableName}/data`, - insertData - ); + const response = await apiClient.post(`/table-management/tables/${tableName}/data`, insertData); const newId = response.data?.data?.id || Date.now().toString(); @@ -441,7 +402,7 @@ export function useTimelineData( throw err; } }, - [tableName, fieldMapping, config.editable] + [tableName, fieldMapping, config.editable], ); // 스케줄 삭제 @@ -459,7 +420,7 @@ export function useTimelineData( throw err; } }, - [tableName, config.editable] + [tableName, config.editable], ); // 새로고침 diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/types.ts b/frontend/lib/registry/components/v2-timeline-scheduler/types.ts index b7a836a6..baf59741 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/types.ts +++ b/frontend/lib/registry/components/v2-timeline-scheduler/types.ts @@ -10,12 +10,7 @@ export type ZoomLevel = "day" | "week" | "month"; /** * 스케줄 상태 */ -export type ScheduleStatus = - | "planned" - | "in_progress" - | "completed" - | "delayed" - | "cancelled"; +export type ScheduleStatus = "planned" | "in_progress" | "completed" | "delayed" | "cancelled"; /** * 스케줄 항목 (간트 바) @@ -107,10 +102,10 @@ export interface ResourceFieldMapping { * 스케줄 타입 (schedule_mng.schedule_type) */ export type ScheduleType = - | "PRODUCTION" // 생산계획 - | "MAINTENANCE" // 정비계획 - | "SHIPPING" // 배차계획 - | "WORK_ASSIGN"; // 작업배정 + | "PRODUCTION" // 생산계획 + | "MAINTENANCE" // 정비계획 + | "SHIPPING" // 배차계획 + | "WORK_ASSIGN"; // 작업배정 /** * 소스 데이터 설정 (스케줄 생성 기준이 되는 원본 데이터) diff --git a/frontend/lib/utils/layoutV2Converter.ts b/frontend/lib/utils/layoutV2Converter.ts index b8485487..fff56bf9 100644 --- a/frontend/lib/utils/layoutV2Converter.ts +++ b/frontend/lib/utils/layoutV2Converter.ts @@ -38,19 +38,19 @@ interface LegacyLayoutData { // ============================================ 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 }; - + const result = { ...nestedComp }; + // 🆕 탭 위젯인 경우 재귀적으로 탭 내부 컴포넌트도 처리 if (nestedComponentType === "v2-tabs-widget") { const config = result.componentConfig || {}; @@ -69,31 +69,35 @@ function applyDefaultsToNestedComponents(components: any[]): any[] { }; } } - + // 🆕 분할 패널인 경우 재귀적으로 내부 컴포넌트도 처리 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, + 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); @@ -102,7 +106,7 @@ function applyDefaultsToNestedComponents(components: any[]): any[] { componentConfig: mergedNestedConfig, }; } - + return result; }); } @@ -112,7 +116,7 @@ function applyDefaultsToNestedComponents(components: any[]): any[] { // ============================================ function applyDefaultsToSplitPanelComponents(mergedConfig: Record): Record { const result = { ...mergedConfig }; - + // leftPanel.components 처리 if (result.leftPanel?.components) { result.leftPanel = { @@ -120,7 +124,7 @@ function applyDefaultsToSplitPanelComponents(mergedConfig: Record): components: applyDefaultsToNestedComponents(result.leftPanel.components), }; } - + // rightPanel.components 처리 if (result.rightPanel?.components) { result.rightPanel = { @@ -128,7 +132,7 @@ function applyDefaultsToSplitPanelComponents(mergedConfig: Record): components: applyDefaultsToNestedComponents(result.rightPanel.components), }; } - + return result; } @@ -149,7 +153,7 @@ export function convertV2ToLegacy(v2Layout: LayoutV2 | null): LegacyLayoutData | if (componentType === "v2-split-panel-layout") { mergedConfig = applyDefaultsToSplitPanelComponents(mergedConfig); } - + // 🆕 탭 위젯인 경우 탭 내부 컴포넌트에도 기본값 적용 if (componentType === "v2-tabs-widget" && mergedConfig.tabs) { mergedConfig = { @@ -273,15 +277,15 @@ export function convertLegacyToV2(legacyLayout: LegacyLayoutData): LayoutV2 { ...(configOverrides.style || {}), ...(topLevelProps.style || {}), }; - + // 🔧 webTypeConfig도 병합 (topLevelProps가 우선, dataflowConfig 등 보존) const mergedWebTypeConfig = { ...(configOverrides.webTypeConfig || {}), ...(topLevelProps.webTypeConfig || {}), }; - - const overrides = { - ...topLevelProps, + + const overrides = { + ...topLevelProps, ...configOverrides, // 🆕 병합된 style 사용 (comp.style 값이 최종 우선) ...(Object.keys(mergedStyle).length > 0 ? { style: mergedStyle } : {}), diff --git a/frontend/lib/v2-core/services/ScheduleGeneratorService.ts b/frontend/lib/v2-core/services/ScheduleGeneratorService.ts index d73dd3a3..5d693005 100644 --- a/frontend/lib/v2-core/services/ScheduleGeneratorService.ts +++ b/frontend/lib/v2-core/services/ScheduleGeneratorService.ts @@ -10,11 +10,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { v2EventBus } from "../events/EventBus"; import { V2_EVENTS } from "../events/types"; -import type { - ScheduleType, - V2ScheduleGenerateRequestEvent, - V2ScheduleGenerateApplyEvent, -} from "../events/types"; +import type { ScheduleType, V2ScheduleGenerateRequestEvent, V2ScheduleGenerateApplyEvent } from "../events/types"; import { apiClient } from "@/lib/api/client"; import { toast } from "sonner"; @@ -122,13 +118,10 @@ function getDefaultPeriod(): { start: string; end: string } { * const { showConfirmDialog, previewResult, handleConfirm } = useScheduleGenerator(config); * ``` */ -export function useScheduleGenerator( - scheduleConfig?: ScheduleGenerationConfig | null -): UseScheduleGeneratorReturn { +export function useScheduleGenerator(scheduleConfig?: ScheduleGenerationConfig | null): UseScheduleGeneratorReturn { // 상태 const [selectedData, setSelectedData] = useState([]); - const [previewResult, setPreviewResult] = - useState(null); + const [previewResult, setPreviewResult] = useState(null); const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [isLoading, setIsLoading] = useState(false); const currentRequestIdRef = useRef(""); @@ -136,57 +129,53 @@ export function useScheduleGenerator( // 1. 테이블 선택 데이터 추적 (TABLE_SELECTION_CHANGE 이벤트 수신) useEffect(() => { - const unsubscribe = v2EventBus.subscribe( - V2_EVENTS.TABLE_SELECTION_CHANGE, - (payload) => { - // scheduleConfig가 있으면 해당 테이블만, 없으면 모든 테이블의 선택 데이터 저장 - if (scheduleConfig?.source?.tableName) { - if (payload.tableName === scheduleConfig.source.tableName) { - setSelectedData(payload.selectedRows); - console.log("[useScheduleGenerator] 선택 데이터 업데이트 (특정 테이블):", payload.selectedCount, "건"); - } - } else { - // scheduleConfig가 없으면 모든 테이블의 선택 데이터를 저장 + const unsubscribe = v2EventBus.subscribe(V2_EVENTS.TABLE_SELECTION_CHANGE, (payload) => { + // scheduleConfig가 있으면 해당 테이블만, 없으면 모든 테이블의 선택 데이터 저장 + if (scheduleConfig?.source?.tableName) { + if (payload.tableName === scheduleConfig.source.tableName) { setSelectedData(payload.selectedRows); - console.log("[useScheduleGenerator] 선택 데이터 업데이트 (모든 테이블):", payload.selectedCount, "건"); + console.log("[useScheduleGenerator] 선택 데이터 업데이트 (특정 테이블):", payload.selectedCount, "건"); } + } else { + // scheduleConfig가 없으면 모든 테이블의 선택 데이터를 저장 + setSelectedData(payload.selectedRows); + console.log("[useScheduleGenerator] 선택 데이터 업데이트 (모든 테이블):", payload.selectedCount, "건"); } - ); + }); return unsubscribe; }, [scheduleConfig?.source?.tableName]); // 2. 스케줄 생성 요청 처리 (SCHEDULE_GENERATE_REQUEST 수신) useEffect(() => { - console.log("[useScheduleGenerator] 이벤트 구독 시작"); - const unsubscribe = v2EventBus.subscribe( V2_EVENTS.SCHEDULE_GENERATE_REQUEST, async (payload: V2ScheduleGenerateRequestEvent) => { console.log("[useScheduleGenerator] SCHEDULE_GENERATE_REQUEST 수신:", payload); // 이벤트에서 config가 오면 사용, 없으면 기존 scheduleConfig 또는 기본 config 사용 - const configToUse = (payload as any).config || scheduleConfig || { - // 기본 설정 (생산계획 화면용) - scheduleType: payload.scheduleType || "PRODUCTION", - source: { - tableName: "sales_order_mng", - groupByField: "part_code", - quantityField: "balance_qty", - dueDateField: "delivery_date", // 기준일 필드 (납기일) - }, - resource: { - type: "ITEM", - idField: "part_code", - nameField: "part_name", - }, - rules: { - leadTimeDays: 3, - dailyCapacity: 100, - }, - target: { - tableName: "schedule_mng", - }, - }; + const configToUse = (payload as any).config || + scheduleConfig || { + // 기본 설정 (생산계획 화면용) + scheduleType: payload.scheduleType || "PRODUCTION", + source: { + tableName: "sales_order_mng", + groupByField: "part_code", + quantityField: "balance_qty", + dueDateField: "delivery_date", // 기준일 필드 (납기일) + }, + resource: { + type: "ITEM", + idField: "part_code", + nameField: "part_name", + }, + rules: { + leadTimeDays: 3, + dailyCapacity: 100, + }, + target: { + tableName: "schedule_mng", + }, + }; console.log("[useScheduleGenerator] 사용할 config:", configToUse); @@ -250,7 +239,7 @@ export function useScheduleGenerator( } finally { setIsLoading(false); } - } + }, ); return unsubscribe; }, [selectedData, scheduleConfig]); @@ -299,10 +288,9 @@ export function useScheduleGenerator( tableName: configToUse?.target?.tableName || "schedule_mng", }); - toast.success( - `${response.data.applied?.created || 0}건의 스케줄이 생성되었습니다.`, - { id: "schedule-apply" } - ); + toast.success(`${response.data.applied?.created || 0}건의 스케줄이 생성되었습니다.`, { + id: "schedule-apply", + }); setShowConfirmDialog(false); setPreviewResult(null); } catch (error: any) { @@ -311,7 +299,7 @@ export function useScheduleGenerator( } finally { setIsLoading(false); } - } + }, ); return unsubscribe; }, [previewResult, scheduleConfig]);
+ # {col.label} @@ -699,7 +679,7 @@ export function SimpleRepeaterTableComponent({ + 삭제
+ {allowAdd ? (
표시할 데이터가 없습니다 @@ -725,9 +701,9 @@ export function SimpleRepeaterTableComponent({
+ {rowIndex + 1}