From 25cd23c1fbf9bb3afa347d69224fb5e74ace3813 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 24 Oct 2025 16:34:21 +0900 Subject: [PATCH] =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B9=84=EC=9C=A8?= =?UTF-8?q?=EC=A1=B0=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/screens/[screenId]/page.tsx | 43 +-- frontend/components/layout/AppLayout.tsx | 2 +- .../screen/InteractiveScreenViewer.tsx | 2 +- .../screen/RealtimePreviewDynamic.tsx | 28 +- .../components/screen/widgets/FlowWidget.tsx | 330 ++++++++++-------- 5 files changed, 218 insertions(+), 187 deletions(-) diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 21987359..d365ebbd 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -52,9 +52,8 @@ export default function ScreenViewPage() { modalDescription?: string; }>({}); - // 자동 스케일 조정 (사용자 화면 크기에 맞춤) - const [scale, setScale] = useState(1); const containerRef = React.useRef(null); + const [scale, setScale] = useState(1); useEffect(() => { const initComponents = async () => { @@ -140,32 +139,37 @@ export default function ScreenViewPage() { } }, [screenId]); - // 자동 스케일 조정 useEffect (항상 화면에 꽉 차게) + // 캔버스 비율 조정 (사용자 화면에 맞게 자동 스케일) useEffect(() => { const updateScale = () => { if (containerRef.current && layout) { - const screenWidth = layout?.screenResolution?.width || 1200; + const designWidth = layout?.screenResolution?.width || 1200; + const designHeight = layout?.screenResolution?.height || 800; + const containerWidth = containerRef.current.offsetWidth; - const availableWidth = containerWidth - 32; // 좌우 패딩 16px * 2 + const containerHeight = containerRef.current.offsetHeight; - // 항상 화면에 맞춰서 스케일 조정 (늘리거나 줄임) - const newScale = availableWidth / screenWidth; + // 가로/세로 비율 중 작은 것을 선택 (화면에 맞게) + const scaleX = containerWidth / designWidth; + const scaleY = containerHeight / designHeight; + const newScale = Math.min(scaleX, scaleY); - console.log("📏 스케일 계산 (화면 꽉 차게):", { - screenWidth, + console.log("📏 캔버스 스케일 계산:", { + designWidth, + designHeight, containerWidth, - availableWidth, - scale: newScale, + containerHeight, + scaleX, + scaleY, + finalScale: newScale, }); setScale(newScale); } }; - // 초기 측정 (DOM이 완전히 렌더링된 후) - const timer = setTimeout(() => { - updateScale(); - }, 100); + // 초기 측정 + const timer = setTimeout(updateScale, 100); window.addEventListener("resize", updateScale); return () => { @@ -207,17 +211,16 @@ export default function ScreenViewPage() { const screenHeight = layout?.screenResolution?.height || 800; return ( -
+
{/* 절대 위치 기반 렌더링 */} {layout && layout.components.length > 0 ? (
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */} -
{children}
+
{children}
{/* 프로필 수정 모달 */} diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 71447d4f..cafd611a 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -334,7 +334,7 @@ export const InteractiveScreenViewer: React.FC = ( console.log("🔍 InteractiveScreenViewer 최종 flowComponent:", flowComponent); return ( -
+
); diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index a00f972f..0cc6ca7f 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -34,7 +34,7 @@ interface RealtimePreviewProps { onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러 onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러 onConfigChange?: (config: any) => void; // 설정 변경 핸들러 - + // 버튼 액션을 위한 props screenId?: number; tableName?: string; @@ -47,7 +47,7 @@ interface RealtimePreviewProps { onRefresh?: () => void; flowRefreshKey?: number; onFlowRefresh?: () => void; - + // 폼 데이터 관련 props formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; @@ -115,24 +115,24 @@ export const RealtimePreviewDynamic: React.FC = ({ // 플로우 위젯의 실제 높이 측정 React.useEffect(() => { const isFlowWidget = component.type === "component" && (component as any).componentType === "flow-widget"; - + if (isFlowWidget && contentRef.current) { const measureHeight = () => { if (contentRef.current) { // getBoundingClientRect()로 실제 렌더링된 높이 측정 const rect = contentRef.current.getBoundingClientRect(); const measured = rect.height; - + // scrollHeight도 함께 확인하여 더 큰 값 사용 const scrollHeight = contentRef.current.scrollHeight; const rawHeight = Math.max(measured, scrollHeight); - + // 40px 단위로 올림 const finalHeight = Math.ceil(rawHeight / 40) * 40; - + if (finalHeight > 0 && Math.abs(finalHeight - (actualHeight || 0)) > 10) { setActualHeight(finalHeight); - + // 컴포넌트의 실제 size.height도 업데이트 (중복 업데이트 방지) if (onConfigChange && finalHeight !== lastUpdatedHeight.current && finalHeight !== component.size?.height) { lastUpdatedHeight.current = finalHeight; @@ -142,11 +142,11 @@ export const RealtimePreviewDynamic: React.FC = ({ newHeight: finalHeight, }); // size는 별도 속성이므로 직접 업데이트 - const event = new CustomEvent('updateComponentSize', { + const event = new CustomEvent("updateComponentSize", { detail: { componentId: component.id, - height: finalHeight - } + height: finalHeight, + }, }); window.dispatchEvent(event); } @@ -276,10 +276,10 @@ export const RealtimePreviewDynamic: React.FC = ({ > {/* 동적 컴포넌트 렌더링 */}
([]); @@ -385,7 +386,7 @@ export function FlowWidget({ : "flex flex-col items-center gap-4"; return ( -
+
{/* 플로우 제목 */}
@@ -647,8 +648,8 @@ export function FlowWidget({ {/* 선택된 스텝의 데이터 리스트 */} {selectedStepId !== null && ( -
- {/* 헤더 */} +
+ {/* 헤더 - 자동 높이 */}

{steps.find((s) => s.id === selectedStepId)?.stepName} @@ -661,34 +662,34 @@ export function FlowWidget({

- {/* 데이터 영역 - 스크롤 가능 */} -
- {stepDataLoading ? ( -
- - 데이터 로딩 중... -
- ) : stepData.length === 0 ? ( -
- - - - 데이터가 없습니다 -
- ) : ( - <> - {/* 모바일: 카드 뷰 */} -
+ {/* 데이터 영역 - 고정 높이 + 스크롤 */} + {stepDataLoading ? ( +
+ + 데이터 로딩 중... +
+ ) : stepData.length === 0 ? ( +
+ + + + 데이터가 없습니다 +
+ ) : ( + <> + {/* 모바일: 카드 뷰 - 고정 높이 + 스크롤 */} +
+
{paginatedStepData.map((row, pageIndex) => { const actualIndex = (stepDataPage - 1) * stepDataPageSize + pageIndex; return ( @@ -725,132 +726,159 @@ export function FlowWidget({ ); })}
+
- {/* 데스크톱: 테이블 뷰 */} -
- - - - {allowDataMove && ( - - 0} - onCheckedChange={toggleAllRows} - /> - - )} - {stepDataColumns.map((col) => ( - - {col} - - ))} - - - - {paginatedStepData.map((row, pageIndex) => { - const actualIndex = (stepDataPage - 1) * stepDataPageSize + pageIndex; - return ( - - {allowDataMove && ( - - toggleRowSelection(actualIndex)} - /> - - )} - {stepDataColumns.map((col) => ( - - {row[col] !== null && row[col] !== undefined ? ( - String(row[col]) - ) : ( - - - )} - - ))} - - ); - })} - -
-
- - )} -
+ {/* 데스크톱: 테이블 뷰 - 고정 높이 + 스크롤 */} +
+ + + + {allowDataMove && ( + + 0} + onCheckedChange={toggleAllRows} + /> + + )} + {stepDataColumns.map((col) => ( + + {col} + + ))} + + + + {paginatedStepData.map((row, pageIndex) => { + const actualIndex = (stepDataPage - 1) * stepDataPageSize + pageIndex; + return ( + + {allowDataMove && ( + + toggleRowSelection(actualIndex)} + /> + + )} + {stepDataColumns.map((col) => ( + + {row[col] !== null && row[col] !== undefined ? ( + String(row[col]) + ) : ( + - + )} + + ))} + + ); + })} + +
+
+ + )} - {/* 페이지네이션 푸터 */} - {!stepDataLoading && stepData.length > 0 && totalStepDataPages > 1 && ( + {/* 페이지네이션 - 항상 하단에 고정 */} + {!stepDataLoading && stepData.length > 0 && (
-
- 페이지 {stepDataPage} / {totalStepDataPages} (총 {stepData.length}건) + {/* 왼쪽: 페이지 정보 + 페이지 크기 선택 */} +
+
+ 페이지 {stepDataPage} / {totalStepDataPages} (총 {stepData.length}건) +
+
+ 표시 개수: + +
- - - - setStepDataPage((p) => Math.max(1, p - 1))} - className={stepDataPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} - /> - - {totalStepDataPages <= 7 ? ( - Array.from({ length: totalStepDataPages }, (_, i) => i + 1).map((page) => ( - - setStepDataPage(page)} - isActive={stepDataPage === page} - className="cursor-pointer" - > - {page} - - - )) - ) : ( - <> - {Array.from({ length: totalStepDataPages }, (_, i) => i + 1) - .filter((page) => { - return ( - page === 1 || - page === totalStepDataPages || - (page >= stepDataPage - 2 && page <= stepDataPage + 2) - ); - }) - .map((page, idx, arr) => ( - - {idx > 0 && arr[idx - 1] !== page - 1 && ( + + {/* 오른쪽: 페이지네이션 */} + {totalStepDataPages > 1 && ( + + + + setStepDataPage((p) => Math.max(1, p - 1))} + className={stepDataPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + {totalStepDataPages <= 7 ? ( + Array.from({ length: totalStepDataPages }, (_, i) => i + 1).map((page) => ( + + setStepDataPage(page)} + isActive={stepDataPage === page} + className="cursor-pointer" + > + {page} + + + )) + ) : ( + <> + {Array.from({ length: totalStepDataPages }, (_, i) => i + 1) + .filter((page) => { + return ( + page === 1 || + page === totalStepDataPages || + (page >= stepDataPage - 2 && page <= stepDataPage + 2) + ); + }) + .map((page, idx, arr) => ( + + {idx > 0 && arr[idx - 1] !== page - 1 && ( + + ... + + )} - ... + setStepDataPage(page)} + isActive={stepDataPage === page} + className="cursor-pointer" + > + {page} + - )} - - setStepDataPage(page)} - isActive={stepDataPage === page} - className="cursor-pointer" - > - {page} - - - - ))} - - )} - - setStepDataPage((p) => Math.min(totalStepDataPages, p + 1))} - className={ - stepDataPage === totalStepDataPages ? "pointer-events-none opacity-50" : "cursor-pointer" - } - /> - - - + + ))} + + )} + + setStepDataPage((p) => Math.min(totalStepDataPages, p + 1))} + className={ + stepDataPage === totalStepDataPages ? "pointer-events-none opacity-50" : "cursor-pointer" + } + /> + + + + )}
)}