From ac53b3c4404c785ec2b5313cba513f264da163d8 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 16 Oct 2025 16:05:12 +0900 Subject: [PATCH] =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=ED=81=AC=EA=B8=B0=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B9=84=EC=9C=A8=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/screens/[screenId]/page.tsx | 498 ++++++++++-------- frontend/components/screen/ScreenList.tsx | 7 +- 2 files changed, 277 insertions(+), 228 deletions(-) diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 27adafe3..72356cf0 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { useParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Loader2 } from "lucide-react"; @@ -34,6 +34,9 @@ export default function ScreenViewPage() { // 테이블 새로고침을 위한 키 상태 const [refreshKey, setRefreshKey] = useState(0); + // 스케일 상태 + const [scale, setScale] = useState(1); + // 편집 모달 상태 const [editModalOpen, setEditModalOpen] = useState(false); const [editModalConfig, setEditModalConfig] = useState<{ @@ -119,6 +122,36 @@ export default function ScreenViewPage() { } }, [screenId]); + // 가로폭 기준 자동 스케일 계산 + useEffect(() => { + const updateScale = () => { + if (layout) { + // main 요소의 실제 너비를 직접 사용 + const mainElement = document.querySelector("main"); + const mainWidth = mainElement ? mainElement.clientWidth : window.innerWidth - 288; + + // 좌우 마진 16px씩 제외 + const margin = 32; // 16px * 2 + const availableWidth = mainWidth - margin; + + const screenWidth = layout?.screenResolution?.width || 1200; + const newScale = availableWidth / screenWidth; + + console.log("🎯 스케일 계산 (마진 포함):", { + mainWidth, + margin, + availableWidth, + screenWidth, + newScale, + }); + + setScale(newScale); + } + }; + + updateScale(); + }, [layout]); + if (loading) { return (
@@ -152,258 +185,277 @@ export default function ScreenViewPage() { const screenHeight = layout?.screenResolution?.height || 800; return ( -
+
{layout && layout.components.length > 0 ? ( - // 캔버스 컴포넌트들을 정확한 해상도로 표시 + // 스케일링된 화면을 감싸는 래퍼 (실제 크기 조정 + 좌우 마진 16px)
- {layout.components - .filter((comp) => !comp.parentId) // 최상위 컴포넌트만 렌더링 (그룹 포함) - .map((component) => { - // 그룹 컴포넌트인 경우 특별 처리 - if (component.type === "group") { - const groupChildren = layout.components.filter((child) => child.parentId === component.id); + {/* 캔버스 컴포넌트들을 가로폭에 맞춰 스케일링하여 표시 */} +
+ {layout.components + .filter((comp) => !comp.parentId) // 최상위 컴포넌트만 렌더링 (그룹 포함) + .map((component) => { + // 그룹 컴포넌트인 경우 특별 처리 + if (component.type === "group") { + const groupChildren = layout.components.filter((child) => child.parentId === component.id); + return ( +
+ {/* 그룹 제목 */} + {(component as any).title && ( +
+ {(component as any).title} +
+ )} + + {/* 그룹 내 자식 컴포넌트들 렌더링 */} + {groupChildren.map((child) => ( +
+ { + console.log("📝 폼 데이터 변경:", { fieldName, value }); + setFormData((prev) => { + const newFormData = { + ...prev, + [fieldName]: value, + }; + console.log("📊 전체 폼 데이터:", newFormData); + return newFormData; + }); + }} + screenInfo={{ + id: screenId, + tableName: screen?.tableName, + }} + /> +
+ ))} +
+ ); + } + + // 라벨 표시 여부 계산 + const templateTypes = ["datatable"]; + const shouldShowLabel = + component.style?.labelDisplay !== false && + (component.label || component.style?.labelText) && + !templateTypes.includes(component.type); + + const labelText = component.style?.labelText || component.label || ""; + const labelStyle = { + fontSize: component.style?.labelFontSize || "14px", + color: component.style?.labelColor || "#212121", + fontWeight: component.style?.labelFontWeight || "500", + backgroundColor: component.style?.labelBackgroundColor || "transparent", + padding: component.style?.labelPadding || "0", + borderRadius: component.style?.labelBorderRadius || "0", + marginBottom: component.style?.labelMarginBottom || "4px", + }; + + // 일반 컴포넌트 렌더링 return ( -
- {/* 그룹 제목 */} - {(component as any).title && ( -
- {(component as any).title} +
+ {/* 라벨을 외부에 별도로 렌더링 */} + {shouldShowLabel && ( +
+ {labelText} + {component.required && *}
)} - {/* 그룹 내 자식 컴포넌트들 렌더링 */} - {groupChildren.map((child) => ( -
- { - console.log("📝 폼 데이터 변경:", { fieldName, value }); - setFormData((prev) => { - const newFormData = { - ...prev, - [fieldName]: value, - }; - console.log("📊 전체 폼 데이터:", newFormData); - return newFormData; - }); - }} - screenInfo={{ - id: screenId, - tableName: screen?.tableName, - }} - /> -
- ))} -
- ); - } - - // 라벨 표시 여부 계산 - const templateTypes = ["datatable"]; - const shouldShowLabel = - component.style?.labelDisplay !== false && - (component.label || component.style?.labelText) && - !templateTypes.includes(component.type); - - const labelText = component.style?.labelText || component.label || ""; - const labelStyle = { - fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#212121", - fontWeight: component.style?.labelFontWeight || "500", - backgroundColor: component.style?.labelBackgroundColor || "transparent", - padding: component.style?.labelPadding || "0", - borderRadius: component.style?.labelBorderRadius || "0", - marginBottom: component.style?.labelMarginBottom || "4px", - }; - - // 일반 컴포넌트 렌더링 - return ( -
- {/* 라벨을 외부에 별도로 렌더링 */} - {shouldShowLabel && ( + {/* 실제 컴포넌트 */}
{ + // console.log("🎯 할당된 화면 컴포넌트:", { + // id: component.id, + // type: component.type, + // position: component.position, + // size: component.size, + // styleWidth: component.style?.width, + // styleHeight: component.style?.height, + // finalWidth: `${component.size.width}px`, + // finalHeight: `${component.size.height}px`, + // }); }} > - {labelText} - {component.required && *} -
- )} - - {/* 실제 컴포넌트 */} -
{ - // console.log("🎯 할당된 화면 컴포넌트:", { - // id: component.id, - // type: component.type, - // position: component.position, - // size: component.size, - // styleWidth: component.style?.width, - // styleHeight: component.style?.height, - // finalWidth: `${component.size.width}px`, - // finalHeight: `${component.size.height}px`, - // }); - }} - > - {/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */} - {component.type !== "widget" ? ( - { - setFormData((prev) => ({ - ...prev, - [fieldName]: value, - })); - }} - screenId={screenId} - tableName={screen?.tableName} - onRefresh={() => { - console.log("화면 새로고침 요청"); - // 테이블 컴포넌트 강제 새로고침을 위한 키 업데이트 - setRefreshKey((prev) => prev + 1); - // 선택된 행 상태도 초기화 - setSelectedRows([]); - setSelectedRowsData([]); - }} - onClose={() => { - console.log("화면 닫기 요청"); - }} - // 테이블 선택된 행 정보 전달 - selectedRows={selectedRows} - selectedRowsData={selectedRowsData} - onSelectedRowsChange={(newSelectedRows, newSelectedRowsData) => { - setSelectedRows(newSelectedRows); - setSelectedRowsData(newSelectedRowsData); - }} - // 테이블 새로고침 키 전달 - refreshKey={refreshKey} - /> - ) : ( - { - // 유틸리티 함수로 파일 컴포넌트 감지 - if (isFileComponent(component)) { - console.log('🎯 page.tsx - 파일 컴포넌트 감지 → webType: "file"', { - componentId: component.id, - componentType: component.type, - originalWebType: component.webType, - }); - return "file"; - } - // 다른 컴포넌트는 유틸리티 함수로 webType 결정 - return getComponentWebType(component) || "text"; - })()} - config={component.webTypeConfig} - props={{ - component: component, - value: formData[component.columnName || component.id] || "", - onChange: (value: any) => { - const fieldName = component.columnName || component.id; + {/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */} + {component.type !== "widget" ? ( + { setFormData((prev) => ({ ...prev, [fieldName]: value, })); - }, - onFormDataChange: (fieldName, value) => { - console.log(`🎯 page.tsx onFormDataChange 호출: ${fieldName} = "${value}"`); - console.log("📋 현재 formData:", formData); - setFormData((prev) => { - const newFormData = { + }} + screenId={screenId} + tableName={screen?.tableName} + onRefresh={() => { + console.log("화면 새로고침 요청"); + // 테이블 컴포넌트 강제 새로고침을 위한 키 업데이트 + setRefreshKey((prev) => prev + 1); + // 선택된 행 상태도 초기화 + setSelectedRows([]); + setSelectedRowsData([]); + }} + onClose={() => { + console.log("화면 닫기 요청"); + }} + // 테이블 선택된 행 정보 전달 + selectedRows={selectedRows} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={(newSelectedRows, newSelectedRowsData) => { + setSelectedRows(newSelectedRows); + setSelectedRowsData(newSelectedRowsData); + }} + // 테이블 새로고침 키 전달 + refreshKey={refreshKey} + /> + ) : ( + { + // 유틸리티 함수로 파일 컴포넌트 감지 + if (isFileComponent(component)) { + console.log('🎯 page.tsx - 파일 컴포넌트 감지 → webType: "file"', { + componentId: component.id, + componentType: component.type, + originalWebType: component.webType, + }); + return "file"; + } + // 다른 컴포넌트는 유틸리티 함수로 webType 결정 + return getComponentWebType(component) || "text"; + })()} + config={component.webTypeConfig} + props={{ + component: component, + value: formData[component.columnName || component.id] || "", + onChange: (value: any) => { + const fieldName = component.columnName || component.id; + setFormData((prev) => ({ ...prev, [fieldName]: value, - }; - console.log("📝 업데이트된 formData:", newFormData); - return newFormData; - }); - }, - isInteractive: true, - formData: formData, - readonly: component.readonly, - required: component.required, - placeholder: component.placeholder, - className: "w-full h-full", - }} - /> - )} + })); + }, + onFormDataChange: (fieldName, value) => { + console.log(`🎯 page.tsx onFormDataChange 호출: ${fieldName} = "${value}"`); + console.log("📋 현재 formData:", formData); + setFormData((prev) => { + const newFormData = { + ...prev, + [fieldName]: value, + }; + console.log("📝 업데이트된 formData:", newFormData); + return newFormData; + }); + }, + isInteractive: true, + formData: formData, + readonly: component.readonly, + required: component.required, + placeholder: component.placeholder, + className: "w-full h-full", + }} + /> + )} +
-
- ); - })} + ); + })} +
) : ( - // 빈 화면일 때도 깔끔하게 표시 + // 빈 화면일 때도 같은 스케일로 표시 + 좌우 마진 16px
-
-
- 📄 +
+
+
+ 📄 +
+

화면이 비어있습니다

+

이 화면에는 아직 설계된 컴포넌트가 없습니다.

-

화면이 비어있습니다

-

이 화면에는 아직 설계된 컴포넌트가 없습니다.

)} diff --git a/frontend/components/screen/ScreenList.tsx b/frontend/components/screen/ScreenList.tsx index b8e6b2ca..ea7afd6c 100644 --- a/frontend/components/screen/ScreenList.tsx +++ b/frontend/components/screen/ScreenList.tsx @@ -900,12 +900,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr // 모달 내부 가용 공간 계산 (헤더, 푸터, 패딩 제외) const availableWidth = typeof window !== "undefined" ? window.innerWidth * 0.95 - 100 : 1800; // 95vw - 패딩 - const availableHeight = typeof window !== "undefined" ? window.innerHeight * 0.95 - 200 : 800; // 95vh - 헤더/푸터 - // 축소 비율 계산 (가로, 세로 중 더 작은 비율 사용) - const scaleX = availableWidth / screenWidth; - const scaleY = availableHeight / screenHeight; - const scale = Math.min(scaleX, scaleY, 1); // 최대 1 (확대하지 않음) + // 가로폭 기준으로 스케일 계산 (가로폭에 맞춤) + const scale = availableWidth / screenWidth; return (