사용자 화면 크기에 따라 화면 비율 조정

This commit is contained in:
kjs 2025-10-16 16:05:12 +09:00
parent a0dde51109
commit ac53b3c440
2 changed files with 277 additions and 228 deletions

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState, useRef } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
@ -34,6 +34,9 @@ export default function ScreenViewPage() {
// 테이블 새로고침을 위한 키 상태 // 테이블 새로고침을 위한 키 상태
const [refreshKey, setRefreshKey] = useState(0); const [refreshKey, setRefreshKey] = useState(0);
// 스케일 상태
const [scale, setScale] = useState(1);
// 편집 모달 상태 // 편집 모달 상태
const [editModalOpen, setEditModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false);
const [editModalConfig, setEditModalConfig] = useState<{ const [editModalConfig, setEditModalConfig] = useState<{
@ -119,6 +122,36 @@ export default function ScreenViewPage() {
} }
}, [screenId]); }, [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) { if (loading) {
return ( return (
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br from-gray-50 to-slate-100"> <div className="flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br from-gray-50 to-slate-100">
@ -152,258 +185,277 @@ export default function ScreenViewPage() {
const screenHeight = layout?.screenResolution?.height || 800; const screenHeight = layout?.screenResolution?.height || 800;
return ( return (
<div className="h-full w-full overflow-auto bg-gradient-to-br from-gray-50 to-slate-100 p-10"> <div className="h-full w-full overflow-auto bg-white" style={{ padding: "16px 0" }}>
{layout && layout.components.length > 0 ? ( {layout && layout.components.length > 0 ? (
// 캔버스 컴포넌트들을 정확한 해상도로 표시 // 스케일링된 화면을 감싸는 래퍼 (실제 크기 조정 + 좌우 마진 16px)
<div <div
className="relative mx-auto rounded-xl border border-gray-200/60 bg-white shadow-lg shadow-gray-900/5"
style={{ style={{
width: `${screenWidth}px`, width: `${screenWidth * scale}px`,
height: `${screenHeight}px`, height: `${screenHeight * scale}px`,
minWidth: `${screenWidth}px`, marginLeft: "16px",
minHeight: `${screenHeight}px`, marginRight: "16px",
}} }}
> >
{layout.components {/* 캔버스 컴포넌트들을 가로폭에 맞춰 스케일링하여 표시 */}
.filter((comp) => !comp.parentId) // 최상위 컴포넌트만 렌더링 (그룹 포함) <div
.map((component) => { className="relative bg-white"
// 그룹 컴포넌트인 경우 특별 처리 style={{
if (component.type === "group") { width: `${screenWidth}px`,
const groupChildren = layout.components.filter((child) => child.parentId === component.id); height: `${screenHeight}px`,
transform: `scale(${scale})`,
transformOrigin: "top left",
}}
>
{layout.components
.filter((comp) => !comp.parentId) // 최상위 컴포넌트만 렌더링 (그룹 포함)
.map((component) => {
// 그룹 컴포넌트인 경우 특별 처리
if (component.type === "group") {
const groupChildren = layout.components.filter((child) => child.parentId === component.id);
return (
<div
key={component.id}
style={{
position: "absolute",
left: `${component.position.x}px`,
top: `${component.position.y}px`,
width: component.style?.width || `${component.size.width}px`,
height: component.style?.height || `${component.size.height}px`,
zIndex: component.position.z || 1,
backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.05)",
border: (component as any).border || "1px solid rgba(59, 130, 246, 0.2)",
borderRadius: (component as any).borderRadius || "12px",
padding: "20px",
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
}}
>
{/* 그룹 제목 */}
{(component as any).title && (
<div className="mb-3 inline-block rounded-lg bg-blue-50 px-3 py-1 text-sm font-semibold text-blue-700">
{(component as any).title}
</div>
)}
{/* 그룹 내 자식 컴포넌트들 렌더링 */}
{groupChildren.map((child) => (
<div
key={child.id}
style={{
position: "absolute",
left: `${child.position.x}px`,
top: `${child.position.y}px`,
width: child.style?.width || `${child.size.width}px`,
height: child.style?.height || `${child.size.height}px`,
zIndex: child.position.z || 1,
}}
>
<InteractiveScreenViewer
component={child}
allComponents={layout.components}
formData={formData}
onFormDataChange={(fieldName, value) => {
console.log("📝 폼 데이터 변경:", { fieldName, value });
setFormData((prev) => {
const newFormData = {
...prev,
[fieldName]: value,
};
console.log("📊 전체 폼 데이터:", newFormData);
return newFormData;
});
}}
screenInfo={{
id: screenId,
tableName: screen?.tableName,
}}
/>
</div>
))}
</div>
);
}
// 라벨 표시 여부 계산
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 ( return (
<div <div key={component.id}>
key={component.id} {/* 라벨을 외부에 별도로 렌더링 */}
style={{ {shouldShowLabel && (
position: "absolute", <div
left: `${component.position.x}px`, style={{
top: `${component.position.y}px`, position: "absolute",
width: component.style?.width || `${component.size.width}px`, left: `${component.position.x}px`,
height: component.style?.height || `${component.size.height}px`, top: `${component.position.y - 25}px`, // 컴포넌트 위쪽에 라벨 배치
zIndex: component.position.z || 1, zIndex: (component.position.z || 1) + 1,
backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.05)", ...labelStyle,
border: (component as any).border || "1px solid rgba(59, 130, 246, 0.2)", }}
borderRadius: (component as any).borderRadius || "12px", >
padding: "20px", {labelText}
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", {component.required && <span style={{ color: "#f97316", marginLeft: "2px" }}>*</span>}
}}
>
{/* 그룹 제목 */}
{(component as any).title && (
<div className="mb-3 inline-block rounded-lg bg-blue-50 px-3 py-1 text-sm font-semibold text-blue-700">
{(component as any).title}
</div> </div>
)} )}
{/* 그룹 내 자식 컴포넌트들 렌더링 */} {/* 실제 컴포넌트 */}
{groupChildren.map((child) => (
<div
key={child.id}
style={{
position: "absolute",
left: `${child.position.x}px`,
top: `${child.position.y}px`,
width: child.style?.width || `${child.size.width}px`,
height: child.style?.height || `${child.size.height}px`,
zIndex: child.position.z || 1,
}}
>
<InteractiveScreenViewer
component={child}
allComponents={layout.components}
formData={formData}
onFormDataChange={(fieldName, value) => {
console.log("📝 폼 데이터 변경:", { fieldName, value });
setFormData((prev) => {
const newFormData = {
...prev,
[fieldName]: value,
};
console.log("📊 전체 폼 데이터:", newFormData);
return newFormData;
});
}}
screenInfo={{
id: screenId,
tableName: screen?.tableName,
}}
/>
</div>
))}
</div>
);
}
// 라벨 표시 여부 계산
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 (
<div key={component.id}>
{/* 라벨을 외부에 별도로 렌더링 */}
{shouldShowLabel && (
<div <div
style={{ style={{
position: "absolute", position: "absolute",
left: `${component.position.x}px`, left: `${component.position.x}px`,
top: `${component.position.y - 25}px`, // 컴포넌트 위쪽에 라벨 배치 top: `${component.position.y}px`,
zIndex: (component.position.z || 1) + 1, width: component.style?.width || `${component.size.width}px`,
...labelStyle, height: component.style?.height || `${component.size.height}px`,
zIndex: component.position.z || 1,
}}
onMouseEnter={() => {
// 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} {/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */}
{component.required && <span style={{ color: "#f97316", marginLeft: "2px" }}>*</span>} {component.type !== "widget" ? (
</div> <DynamicComponentRenderer
)} component={{
...component,
{/* 실제 컴포넌트 */} style: {
<div ...component.style,
style={{ labelDisplay: shouldShowLabel ? false : (component.style?.labelDisplay ?? true), // 상위에서 라벨을 표시했으면 컴포넌트 내부에서는 숨김
position: "absolute", },
left: `${component.position.x}px`, }}
top: `${component.position.y}px`, isInteractive={true}
width: component.style?.width || `${component.size.width}px`, formData={formData}
height: component.style?.height || `${component.size.height}px`, onFormDataChange={(fieldName, value) => {
zIndex: component.position.z || 1,
}}
onMouseEnter={() => {
// 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" ? (
<DynamicComponentRenderer
component={{
...component,
style: {
...component.style,
labelDisplay: shouldShowLabel ? false : (component.style?.labelDisplay ?? true), // 상위에서 라벨을 표시했으면 컴포넌트 내부에서는 숨김
},
}}
isInteractive={true}
formData={formData}
onFormDataChange={(fieldName, value) => {
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}
/>
) : (
<DynamicWebTypeRenderer
webType={(() => {
// 유틸리티 함수로 파일 컴포넌트 감지
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) => ({ setFormData((prev) => ({
...prev, ...prev,
[fieldName]: value, [fieldName]: value,
})); }));
}, }}
onFormDataChange: (fieldName, value) => { screenId={screenId}
console.log(`🎯 page.tsx onFormDataChange 호출: ${fieldName} = "${value}"`); tableName={screen?.tableName}
console.log("📋 현재 formData:", formData); onRefresh={() => {
setFormData((prev) => { console.log("화면 새로고침 요청");
const newFormData = { // 테이블 컴포넌트 강제 새로고침을 위한 키 업데이트
setRefreshKey((prev) => prev + 1);
// 선택된 행 상태도 초기화
setSelectedRows([]);
setSelectedRowsData([]);
}}
onClose={() => {
console.log("화면 닫기 요청");
}}
// 테이블 선택된 행 정보 전달
selectedRows={selectedRows}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={(newSelectedRows, newSelectedRowsData) => {
setSelectedRows(newSelectedRows);
setSelectedRowsData(newSelectedRowsData);
}}
// 테이블 새로고침 키 전달
refreshKey={refreshKey}
/>
) : (
<DynamicWebTypeRenderer
webType={(() => {
// 유틸리티 함수로 파일 컴포넌트 감지
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, ...prev,
[fieldName]: value, [fieldName]: value,
}; }));
console.log("📝 업데이트된 formData:", newFormData); },
return newFormData; onFormDataChange: (fieldName, value) => {
}); console.log(`🎯 page.tsx onFormDataChange 호출: ${fieldName} = "${value}"`);
}, console.log("📋 현재 formData:", formData);
isInteractive: true, setFormData((prev) => {
formData: formData, const newFormData = {
readonly: component.readonly, ...prev,
required: component.required, [fieldName]: value,
placeholder: component.placeholder, };
className: "w-full h-full", console.log("📝 업데이트된 formData:", newFormData);
}} return newFormData;
/> });
)} },
isInteractive: true,
formData: formData,
readonly: component.readonly,
required: component.required,
placeholder: component.placeholder,
className: "w-full h-full",
}}
/>
)}
</div>
</div> </div>
</div> );
); })}
})} </div>
</div> </div>
) : ( ) : (
// 빈 화면일 때도 깔끔하게 표시 // 빈 화면일 때도 같은 스케일로 표시 + 좌우 마진 16px
<div <div
className="mx-auto flex items-center justify-center rounded-xl border border-gray-200/60 bg-white shadow-lg shadow-gray-900/5"
style={{ style={{
width: `${screenWidth}px`, width: `${screenWidth * scale}px`,
height: `${screenHeight}px`, height: `${screenHeight * scale}px`,
minWidth: `${screenWidth}px`, marginLeft: "16px",
minHeight: `${screenHeight}px`, marginRight: "16px",
}} }}
> >
<div className="text-center"> <div
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-white shadow-sm"> className="flex items-center justify-center bg-white"
<span className="text-2xl">📄</span> style={{
width: `${screenWidth}px`,
height: `${screenHeight}px`,
transform: `scale(${scale})`,
transformOrigin: "top left",
}}
>
<div className="text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-white shadow-sm">
<span className="text-2xl">📄</span>
</div>
<h2 className="mb-2 text-xl font-semibold text-gray-900"> </h2>
<p className="text-gray-600"> .</p>
</div> </div>
<h2 className="mb-2 text-xl font-semibold text-gray-900"> </h2>
<p className="text-gray-600"> .</p>
</div> </div>
</div> </div>
)} )}

View File

@ -900,12 +900,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
// 모달 내부 가용 공간 계산 (헤더, 푸터, 패딩 제외) // 모달 내부 가용 공간 계산 (헤더, 푸터, 패딩 제외)
const availableWidth = typeof window !== "undefined" ? window.innerWidth * 0.95 - 100 : 1800; // 95vw - 패딩 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 scale = availableWidth / screenWidth;
const scaleY = availableHeight / screenHeight;
const scale = Math.min(scaleX, scaleY, 1); // 최대 1 (확대하지 않음)
return ( return (
<div <div