From 7a1358484ba3f5334b4802cebcafec0c203be460 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 18 Sep 2025 21:33:04 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=ED=99=94=EB=A9=B4=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20UI=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/prisma/schema.prisma | 21 +++++++++++++++++++ frontend/components/screen/EditModal.tsx | 1 + frontend/components/screen/FloatingPanel.tsx | 4 ++-- .../screen/InteractiveScreenViewer.tsx | 2 +- .../screen/RealtimePreviewDynamic.tsx | 4 ++-- frontend/components/ui/dialog.tsx | 4 ++-- frontend/components/ui/dropdown-menu.tsx | 4 ++-- frontend/components/ui/popover.tsx | 2 +- frontend/components/ui/select.tsx | 2 +- 9 files changed, 33 insertions(+), 11 deletions(-) diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index e198576b..169adbf1 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -4080,3 +4080,24 @@ model table_relationships_backup { @@ignore } + +model test_sales_info { + sales_no String @id @db.VarChar(20) + contract_type String? @db.VarChar(50) + order_seq Int? + domestic_foreign String? @db.VarChar(20) + customer_name String? @db.VarChar(200) + product_type String? @db.VarChar(100) + machine_type String? @db.VarChar(100) + customer_project_name String? @db.VarChar(200) + expected_delivery_date DateTime? @db.Date + receiving_location String? @db.VarChar(200) + setup_location String? @db.VarChar(200) + equipment_direction String? @db.VarChar(100) + equipment_count Int? @default(0) + equipment_type String? @db.VarChar(100) + equipment_length Decimal? @db.Decimal(10,2) + manager_name String? @db.VarChar(100) + reg_date DateTime? @default(now()) @db.Timestamp(6) + status String? @default("진행중") @db.VarChar(50) +} diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 2e736840..026bd9f3 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -230,6 +230,7 @@ export const EditModal: React.FC = ({ minHeight: dynamicSize.height, maxWidth: "95vw", maxHeight: "95vh", + zIndex: 9999, // 모든 컴포넌트보다 위에 표시 }} > diff --git a/frontend/components/screen/FloatingPanel.tsx b/frontend/components/screen/FloatingPanel.tsx index 2de7ed12..e6d3241e 100644 --- a/frontend/components/screen/FloatingPanel.tsx +++ b/frontend/components/screen/FloatingPanel.tsx @@ -227,7 +227,7 @@ export const FloatingPanel: React.FC = ({
= ({ height: `${panelSize.height}px`, transform: isDragging ? "scale(1.01)" : "scale(1)", transition: isDragging ? "none" : "transform 0.1s ease-out, box-shadow 0.1s ease-out", - zIndex: isDragging ? 9999 : 50, // 드래그 중 최상위 표시 + zIndex: isDragging ? 9999 : 9998, // 항상 컴포넌트보다 위에 표시 }} > {/* 헤더 */} diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index aa038662..006072fc 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -1683,7 +1683,7 @@ export const InteractiveScreenViewer: React.FC = ( top: `${popupComponent.position.y}px`, width: `${popupComponent.size.width}px`, height: `${popupComponent.size.height}px`, - zIndex: popupComponent.position.z || 1, + zIndex: Math.min(popupComponent.position.z || 1, 20), // 최대 z-index 20으로 제한 }} > {/* 🎯 핵심 수정: 팝업 전용 formData 사용 */} diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 087335ac..409c6056 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -76,12 +76,12 @@ export const RealtimePreviewDynamic: React.FC = ({ }) => { const { id, type, position, size, style: componentStyle } = component; - // 선택 상태에 따른 스타일 + // 선택 상태에 따른 스타일 (z-index 낮춤 - 패널과 모달보다 아래) const selectionStyle = isSelected ? { outline: "2px solid #3b82f6", outlineOffset: "2px", - zIndex: 1000, + zIndex: 30, // 패널(z-50)과 모달(z-50)보다 낮게 설정 } : {}; diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx index c0ef81cc..4afeb373 100644 --- a/frontend/components/ui/dialog.tsx +++ b/frontend/components/ui/dialog.tsx @@ -38,13 +38,13 @@ const DialogContent = React.forwardRef< {children} - + Close diff --git a/frontend/components/ui/dropdown-menu.tsx b/frontend/components/ui/dropdown-menu.tsx index 7aaa6d81..97a61ea2 100644 --- a/frontend/components/ui/dropdown-menu.tsx +++ b/frontend/components/ui/dropdown-menu.tsx @@ -46,7 +46,7 @@ const DropdownMenuSubContent = React.forwardRef< Date: Fri, 19 Sep 2025 02:15:21 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/dynamicFormService.ts | 108 +++++++++- frontend/components/common/ScreenModal.tsx | 79 ++++++-- frontend/components/screen/EditModal.tsx | 92 ++++++--- .../screen/InteractiveScreenViewer.tsx | 94 ++++++--- .../screen/InteractiveScreenViewerDynamic.tsx | 6 +- frontend/components/ui/alert-dialog.tsx | 6 +- .../lib/hooks/useEntityJoinOptimization.ts | 33 ++- .../lib/registry/DynamicComponentRenderer.tsx | 113 +++++++++-- .../lib/registry/DynamicLayoutRenderer.tsx | 6 +- .../button-primary/ButtonPrimaryComponent.tsx | 18 +- .../date-input/DateInputComponent.tsx | 142 +++++++++++-- .../number-input/NumberInputComponent.tsx | 65 ++++-- .../select-basic/SelectBasicComponent.tsx | 107 +++++++++- .../table-list/TableListComponent.tsx | 18 +- .../text-input/TextInputComponent.tsx | 14 +- .../registry/layouts/BaseLayoutRenderer.tsx | 12 ++ .../layouts/flexbox/FlexboxLayout.tsx | 16 +- .../lib/registry/layouts/grid/GridLayout.tsx | 16 +- frontend/lib/utils/buttonActions.ts | 70 ++++++- frontend/lib/utils/domPropsFilter.ts | 189 ++++++++++++++++++ 20 files changed, 1024 insertions(+), 180 deletions(-) create mode 100644 frontend/lib/utils/domPropsFilter.ts diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 56b0c42c..3e61b69e 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -87,6 +87,48 @@ export class DynamicFormService { return Boolean(value); } + // 날짜/시간 타입 처리 + if ( + lowerDataType.includes("date") || + lowerDataType.includes("timestamp") || + lowerDataType.includes("time") + ) { + if (typeof value === "string") { + // 빈 문자열이면 null 반환 + if (value.trim() === "") { + return null; + } + + try { + // YYYY-MM-DD 형식인 경우 시간 추가해서 Date 객체 생성 + if (/^\d{4}-\d{2}-\d{2}$/.test(value)) { + console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`); + return new Date(value + "T00:00:00"); + } + // 다른 날짜 형식도 Date 객체로 변환 + else { + console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`); + return new Date(value); + } + } catch (error) { + console.error(`❌ 날짜 변환 실패: ${value}`, error); + return null; + } + } + + // 이미 Date 객체인 경우 그대로 반환 + if (value instanceof Date) { + return value; + } + + // 숫자인 경우 timestamp로 처리 + if (typeof value === "number") { + return new Date(value); + } + + return null; + } + // 기본적으로 문자열로 반환 return value; } @@ -479,7 +521,7 @@ export class DynamicFormService { const updateQuery = ` UPDATE ${tableName} SET ${setClause} - WHERE ${primaryKeyColumn} = $${values.length} + WHERE ${primaryKeyColumn} = $${values.length}::text RETURNING * `; @@ -552,6 +594,31 @@ export class DynamicFormService { } }); + // 컬럼 타입에 맞는 데이터 변환 (UPDATE용) + const columnInfo = await this.getTableColumnInfo(tableName); + console.log(`📊 테이블 ${tableName}의 컬럼 타입 정보:`, columnInfo); + + // 각 컬럼의 타입에 맞게 데이터 변환 + Object.keys(dataToUpdate).forEach((columnName) => { + const column = columnInfo.find((col) => col.column_name === columnName); + if (column) { + const originalValue = dataToUpdate[columnName]; + const convertedValue = this.convertValueForPostgreSQL( + originalValue, + column.data_type + ); + + if (originalValue !== convertedValue) { + console.log( + `🔄 UPDATE 타입 변환: ${columnName} (${column.data_type}) = "${originalValue}" -> ${convertedValue}` + ); + dataToUpdate[columnName] = convertedValue; + } + } + }); + + console.log("✅ UPDATE 타입 변환 완료된 데이터:", dataToUpdate); + console.log("🎯 실제 테이블에서 업데이트할 데이터:", { tableName, id, @@ -650,12 +717,15 @@ export class DynamicFormService { tableName, }); - // 1. 먼저 테이블의 기본키 컬럼명을 동적으로 조회 + // 1. 먼저 테이블의 기본키 컬럼명과 데이터 타입을 동적으로 조회 const primaryKeyQuery = ` - SELECT kcu.column_name + SELECT kcu.column_name, c.data_type FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name + JOIN information_schema.columns c + ON kcu.column_name = c.column_name + AND kcu.table_name = c.table_name WHERE tc.table_name = $1 AND tc.constraint_type = 'PRIMARY KEY' LIMIT 1 @@ -677,13 +747,37 @@ export class DynamicFormService { throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다.`); } - const primaryKeyColumn = (primaryKeyResult[0] as any).column_name; - console.log("🔑 발견된 기본키 컬럼:", primaryKeyColumn); + const primaryKeyInfo = primaryKeyResult[0] as any; + const primaryKeyColumn = primaryKeyInfo.column_name; + const primaryKeyDataType = primaryKeyInfo.data_type; + console.log("🔑 발견된 기본키:", { + column: primaryKeyColumn, + dataType: primaryKeyDataType, + }); - // 2. 동적으로 발견된 기본키를 사용한 DELETE SQL 생성 + // 2. 데이터 타입에 맞는 타입 캐스팅 적용 + let typeCastSuffix = ""; + if ( + primaryKeyDataType.includes("character") || + primaryKeyDataType.includes("text") + ) { + typeCastSuffix = "::text"; + } else if ( + primaryKeyDataType.includes("integer") || + primaryKeyDataType.includes("bigint") + ) { + typeCastSuffix = "::bigint"; + } else if ( + primaryKeyDataType.includes("numeric") || + primaryKeyDataType.includes("decimal") + ) { + typeCastSuffix = "::numeric"; + } + + // 3. 동적으로 발견된 기본키와 타입 캐스팅을 사용한 DELETE SQL 생성 const deleteQuery = ` DELETE FROM ${tableName} - WHERE ${primaryKeyColumn} = $1 + WHERE ${primaryKeyColumn} = $1${typeCastSuffix} RETURNING * `; diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 92194b7a..453ddfe8 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState, useEffect } from "react"; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic"; import { screenApi } from "@/lib/api/screen"; import { ComponentData } from "@/types/screen"; @@ -45,24 +45,60 @@ export const ScreenModal: React.FC = ({ className }) => { let maxWidth = 800; // 최소 너비 let maxHeight = 600; // 최소 높이 - components.forEach((component) => { - const x = parseFloat(component.style?.positionX || "0"); - const y = parseFloat(component.style?.positionY || "0"); - const width = parseFloat(component.style?.width || "100"); - const height = parseFloat(component.style?.height || "40"); + console.log("🔍 화면 크기 계산 시작:", { componentsCount: components.length }); + + components.forEach((component, index) => { + // position과 size는 BaseComponent에서 별도 속성으로 관리 + const x = parseFloat(component.position?.x?.toString() || "0"); + const y = parseFloat(component.position?.y?.toString() || "0"); + const width = parseFloat(component.size?.width?.toString() || "100"); + const height = parseFloat(component.size?.height?.toString() || "40"); // 컴포넌트의 오른쪽 끝과 아래쪽 끝 계산 const rightEdge = x + width; const bottomEdge = y + height; - maxWidth = Math.max(maxWidth, rightEdge + 50); // 여백 추가 - maxHeight = Math.max(maxHeight, bottomEdge + 50); // 여백 추가 + console.log( + `📏 컴포넌트 ${index + 1} (${component.id}): x=${x}, y=${y}, w=${width}, h=${height}, rightEdge=${rightEdge}, bottomEdge=${bottomEdge}`, + ); + + const newMaxWidth = Math.max(maxWidth, rightEdge + 100); // 여백 증가 + const newMaxHeight = Math.max(maxHeight, bottomEdge + 100); // 여백 증가 + + if (newMaxWidth > maxWidth || newMaxHeight > maxHeight) { + console.log(`🔄 크기 업데이트: ${maxWidth}×${maxHeight} → ${newMaxWidth}×${newMaxHeight}`); + maxWidth = newMaxWidth; + maxHeight = newMaxHeight; + } }); - return { - width: Math.min(maxWidth, window.innerWidth * 0.9), // 화면의 90%를 넘지 않도록 - height: Math.min(maxHeight, window.innerHeight * 0.8), // 화면의 80%를 넘지 않도록 + console.log("📊 컴포넌트 기반 계산 결과:", { maxWidth, maxHeight }); + + // 브라우저 크기 제한 확인 (더욱 관대하게 설정) + const maxAllowedWidth = window.innerWidth * 0.98; // 95% -> 98% + const maxAllowedHeight = window.innerHeight * 0.95; // 90% -> 95% + + console.log("📐 크기 제한 정보:", { + 계산된크기: { maxWidth, maxHeight }, + 브라우저제한: { maxAllowedWidth, maxAllowedHeight }, + 브라우저크기: { width: window.innerWidth, height: window.innerHeight }, + }); + + // 컴포넌트 기반 크기를 우선 적용하되, 브라우저 제한을 고려 + const finalDimensions = { + width: Math.min(maxWidth, maxAllowedWidth), + height: Math.min(maxHeight, maxAllowedHeight), }; + + console.log("✅ 최종 화면 크기:", finalDimensions); + console.log("🔧 크기 적용 분석:", { + width적용: maxWidth <= maxAllowedWidth ? "컴포넌트기준" : "브라우저제한", + height적용: maxHeight <= maxAllowedHeight ? "컴포넌트기준" : "브라우저제한", + 컴포넌트크기: { maxWidth, maxHeight }, + 최종크기: finalDimensions, + }); + + return finalDimensions; }; // 전역 모달 이벤트 리스너 @@ -154,17 +190,17 @@ export const ScreenModal: React.FC = ({ className }) => { }; } - // 헤더 높이와 패딩을 고려한 전체 높이 계산 - const headerHeight = 60; // DialogHeader + 패딩 + // 헤더 높이와 패딩을 고려한 전체 높이 계산 (실제 측정값 기반) + const headerHeight = 80; // DialogHeader + 패딩 (더 정확한 값) const totalHeight = screenDimensions.height + headerHeight; return { className: "overflow-hidden p-0", style: { - width: `${screenDimensions.width + 48}px`, // 헤더 패딩과 여백 고려 - height: `${Math.min(totalHeight, window.innerHeight * 0.8)}px`, - maxWidth: "90vw", - maxHeight: "80vh", + width: `${Math.min(screenDimensions.width + 48, window.innerWidth * 0.98)}px`, // 브라우저 제한 적용 + height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`, // 브라우저 제한 적용 + maxWidth: "98vw", // 안전장치 + maxHeight: "95vh", // 안전장치 }, }; }; @@ -176,9 +212,10 @@ export const ScreenModal: React.FC = ({ className }) => { {modalState.title} + {loading ? "화면을 불러오는 중입니다..." : "화면 내용을 표시합니다."} -
+
{loading ? (
@@ -188,7 +225,7 @@ export const ScreenModal: React.FC = ({ className }) => {
) : screenData ? (
= ({ className }) => { formData={formData} onFormDataChange={(fieldName, value) => { console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`); - console.log(`📋 현재 formData:`, formData); + console.log("📋 현재 formData:", formData); setFormData((prev) => { const newFormData = { ...prev, [fieldName]: value, }; - console.log(`📝 ScreenModal 업데이트된 formData:`, newFormData); + console.log("📝 ScreenModal 업데이트된 formData:", newFormData); return newFormData; }); }} diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 026bd9f3..b0cc0c2f 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"; import { X, Save, RotateCcw } from "lucide-react"; import { toast } from "sonner"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; +import { InteractiveScreenViewer } from "./InteractiveScreenViewer"; import { screenApi } from "@/lib/api/screen"; import { ComponentData } from "@/lib/types/screen"; @@ -145,7 +146,19 @@ export const EditModal: React.FC = ({ layoutData.components.forEach((comp) => { if (comp.columnName) { const formValue = formData[comp.columnName]; - console.log(` - ${comp.columnName}: "${formValue}" (컴포넌트 ID: ${comp.id})`); + console.log( + ` - ${comp.columnName}: "${formValue}" (타입: ${comp.type}, 웹타입: ${(comp as any).widgetType})`, + ); + + // 코드 타입인 경우 특별히 로깅 + if ((comp as any).widgetType === "code") { + console.log(` 🔍 코드 타입 세부정보:`, { + columnName: comp.columnName, + componentId: comp.id, + formValue, + webTypeConfig: (comp as any).webTypeConfig, + }); + } } }); } else { @@ -270,30 +283,61 @@ export const EditModal: React.FC = ({ zIndex: 1, }} > - { - console.log("📝 폼 데이터 변경:", fieldName, value); - const newFormData = { ...formData, [fieldName]: value }; - setFormData(newFormData); + {/* 위젯 컴포넌트는 InteractiveScreenViewer 사용 (라벨 표시를 위해) */} + {component.type === "widget" ? ( + { + console.log("📝 폼 데이터 변경:", fieldName, value); + const newFormData = { ...formData, [fieldName]: value }; + setFormData(newFormData); - // 변경된 데이터를 즉시 부모로 전달 - if (onDataChange) { - console.log("📤 EditModal -> 부모로 데이터 전달:", newFormData); - onDataChange(newFormData); - } - }} - // 편집 모드로 설정 - mode="edit" - // 모달 내에서 렌더링되고 있음을 표시 - isInModal={true} - // 인터랙티브 모드 활성화 (formData 사용을 위해 필수) - isInteractive={true} - /> + // 변경된 데이터를 즉시 부모로 전달 + if (onDataChange) { + console.log("📤 EditModal -> 부모로 데이터 전달:", newFormData); + onDataChange(newFormData); + } + }} + screenInfo={{ + id: screenId || 0, + tableName: screenData.tableName, + }} + /> + ) : ( + { + console.log("📝 폼 데이터 변경:", fieldName, value); + const newFormData = { ...formData, [fieldName]: value }; + setFormData(newFormData); + + // 변경된 데이터를 즉시 부모로 전달 + if (onDataChange) { + console.log("📤 EditModal -> 부모로 데이터 전달:", newFormData); + onDataChange(newFormData); + } + }} + // 편집 모드로 설정 + mode="edit" + // 모달 내에서 렌더링되고 있음을 표시 + isInModal={true} + // 인터랙티브 모드 활성화 (formData 사용을 위해 필수) + isInteractive={true} + /> + )}
))}
diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 006072fc..bb6d2eac 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -37,6 +37,7 @@ import { FileUpload } from "./widgets/FileUpload"; import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm"; import { useParams } from "next/navigation"; import { screenApi } from "@/lib/api/screen"; +import { DynamicWebTypeRenderer } from "@/lib/registry/DynamicWebTypeRenderer"; interface InteractiveScreenViewerProps { component: ComponentData; @@ -936,41 +937,64 @@ export const InteractiveScreenViewer: React.FC = ( const widget = comp as WidgetComponent; const config = widget.webTypeConfig as CodeTypeConfig | undefined; - console.log("💻 InteractiveScreenViewer - Code 위젯:", { + console.log("🔍 InteractiveScreenViewer - Code 위젯 (공통코드 선택):", { componentId: widget.id, widgetType: widget.widgetType, + columnName: widget.columnName, + fieldName, + currentValue, + formData, config, - appliedSettings: { - language: config?.language, - theme: config?.theme, - fontSize: config?.fontSize, - defaultValue: config?.defaultValue, - wordWrap: config?.wordWrap, - tabSize: config?.tabSize, - }, + codeCategory: config?.codeCategory, }); - const finalPlaceholder = config?.placeholder || "코드를 입력하세요..."; - const rows = config?.rows || 4; - - return applyStyles( -