From 0bb314f8e5d6a973915ad1dc231b8b40126954e1 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 31 Oct 2025 10:41:45 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=99=94=EB=A9=B4=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EB=B0=8F=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EB=B7=B0?= =?UTF-8?q?=EC=96=B4=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EB=84=88=EB=B9=84=20=ED=99=9C=EC=9A=A9=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면 관리 페이지에서 position.x === 0인 컴포넌트가 100% 너비로 표시되도록 수정 - 대시보드 뷰어에서 부모 컨테이너의 maxWidth 제한 제거하여 화면 전체 너비 활용 - AppLayout의 main 영역에 16px 내부 패딩 적용 - RealtimePreview 및 RealtimePreviewDynamic 컴포넌트에서 좌측 정렬 컴포넌트 너비 자동 조정 - 모바일 환경에서 화면 스케일링 비활성화 (반응형만 작동) - table-mobile-fixed CSS 클래스 추가로 모바일 테이블 레이아웃 개선 - useResponsive 훅 추가로 반응형 감지 기능 구현 --- .../app/(main)/screens/[screenId]/page.tsx | 32 +++++--- frontend/app/globals.css | 14 ++++ .../components/dashboard/DashboardViewer.tsx | 5 +- frontend/components/layout/AppLayout.tsx | 4 +- .../components/screen/RealtimePreview.tsx | 19 ++++- .../screen/RealtimePreviewDynamic.tsx | 8 +- .../table-list/SingleTableWithSticky.tsx | 14 ++-- .../table-list/TableListComponent.tsx | 79 ++++++++----------- 8 files changed, 107 insertions(+), 68 deletions(-) diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index f33a93bf..dac590d6 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -17,6 +17,7 @@ import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext"; import { useAuth } from "@/hooks/useAuth"; // 🆕 사용자 정보 +import { useResponsive } from "@/lib/hooks/useResponsive"; // 🆕 반응형 감지 export default function ScreenViewPage() { const params = useParams(); @@ -25,6 +26,9 @@ export default function ScreenViewPage() { // 🆕 현재 로그인한 사용자 정보 const { user, userName, companyCode } = useAuth(); + + // 🆕 모바일 환경 감지 + const { isMobile } = useResponsive(); const [screen, setScreen] = useState(null); const [layout, setLayout] = useState(null); @@ -59,6 +63,7 @@ export default function ScreenViewPage() { const containerRef = React.useRef(null); const [scale, setScale] = useState(1); + const [containerWidth, setContainerWidth] = useState(0); useEffect(() => { const initComponents = async () => { @@ -142,22 +147,33 @@ export default function ScreenViewPage() { } }, [screenId]); - // 캔버스 비율 조정 (사용자 화면에 맞게 자동 스케일) + // 캔버스 비율 조정 (사용자 화면에 맞게 자동 스케일) - 모바일에서는 비활성화 useEffect(() => { + // 모바일 환경에서는 스케일 조정 비활성화 (반응형만 작동) + if (isMobile) { + setScale(1); + return; + } + const updateScale = () => { if (containerRef.current && layout) { const designWidth = layout?.screenResolution?.width || 1200; const designHeight = layout?.screenResolution?.height || 800; + // containerRef는 이미 패딩이 적용된 영역 내부이므로 offsetWidth는 패딩을 제외한 크기입니다 const containerWidth = containerRef.current.offsetWidth; const containerHeight = containerRef.current.offsetHeight; - // 가로/세로 비율 중 작은 것을 선택 (화면에 맞게) + // 가로/세로 비율 중 작은 것을 선택하여 화면에 맞게 스케일 조정 + // 하지만 화면이 컨테이너 전체 너비를 차지하도록 하기 위해 가로를 우선시 const scaleX = containerWidth / designWidth; const scaleY = containerHeight / designHeight; + // 가로를 우선으로 하되, 세로가 넘치지 않도록 제한 const newScale = Math.min(scaleX, scaleY); setScale(newScale); + // 컨테이너 너비 업데이트 + setContainerWidth(containerWidth); } }; @@ -169,7 +185,7 @@ export default function ScreenViewPage() { clearTimeout(timer); window.removeEventListener("resize", updateScale); }; - }, [layout]); + }, [layout, isMobile]); if (loading) { return ( @@ -205,18 +221,16 @@ export default function ScreenViewPage() { return ( -
+
{/* 절대 위치 기반 렌더링 */} {layout && layout.components.length > 0 ? (
0 ? `${containerWidth / scale}px` : "100%", + minWidth: containerWidth > 0 ? `${containerWidth / scale}px` : "100%", }} > {/* 최상위 컴포넌트들 렌더링 */} diff --git a/frontend/app/globals.css b/frontend/app/globals.css index f5e35eca..7818c287 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -342,4 +342,18 @@ select { /* 필요시 특정 컴포넌트에 대한 스타일 오버라이드를 여기에 추가 */ /* 예: Calendar, Table 등의 미세 조정 */ +/* 모바일에서 테이블 레이아웃 고정 (화면 밖으로 넘어가지 않도록) */ +@media (max-width: 639px) { + .table-mobile-fixed { + table-layout: fixed; + } +} + +/* 데스크톱에서 테이블 레이아웃 자동 (기본값이지만 명시적으로 설정) */ +@media (min-width: 640px) { + .table-mobile-fixed { + table-layout: auto; + } +} + /* ===== End of Global Styles ===== */ diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx index 5828bea9..2b21b5f4 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -542,7 +542,7 @@ export function DashboardViewer({ {/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */}
-
+
{/* 다운로드 버튼 */}
@@ -700,12 +700,13 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi // 데스크톱: 디자이너에서 설정한 위치 그대로 absolute positioning // 단, 너비는 화면 크기에 따라 비율로 조정 const widthPercentage = (element.size.width / canvasWidth) * 100; + const leftPercentage = (element.position.x / canvasWidth) * 100; return (
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */} -
{children}
+
+
{children}
+
{/* 프로필 수정 모달 */} diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index 7cf02aa0..12c300de 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -389,14 +389,27 @@ export const RealtimePreviewDynamic: React.FC = ({ finalHeight = actualHeight; } + // 🔍 디버깅: position.x 값 확인 + const positionX = position?.x || 0; + console.log("🔍 RealtimePreview componentStyle 설정:", { + componentId: id, + positionX, + sizeWidth: size?.width, + styleWidth: style?.width, + willUse100Percent: positionX === 0, + }); + const componentStyle = { position: "absolute" as const, - left: position?.x || 0, + ...style, // 먼저 적용하고 + left: positionX, top: position?.y || 0, - width: size?.width || 200, + // 🆕 left가 0이면 부모 너비를 100% 채우도록 수정 (우측 여백 제거) + width: positionX === 0 ? "100%" : (size?.width || 200), height: finalHeight, zIndex: position?.z || 1, - ...style, + // right 속성 강제 제거 + right: undefined, }; // 선택된 컴포넌트 스타일 diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index df14084b..f4e0fec9 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -238,11 +238,15 @@ export const RealtimePreviewDynamic: React.FC = ({ const baseStyle = { left: `${position.x}px`, top: `${position.y}px`, - width: getWidth(), + // 🆕 left가 0이면 부모 너비를 100% 채우도록 수정 (우측 여백 제거) + width: position.x === 0 ? "100%" : getWidth(), height: getHeight(), // 모든 컴포넌트 고정 높이로 변경 zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상 ...componentStyle, - // style.width와 style.height는 이미 getWidth/getHeight에서 처리했으므로 중복 적용됨 + // style.width가 있어도 position.x === 0이면 100%로 강제 + ...(position.x === 0 && { width: "100%" }), + // right 속성 강제 제거 + right: undefined, }; const handleClick = (e: React.MouseEvent) => { diff --git a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx index cbfe678c..d429fbf4 100644 --- a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx @@ -91,8 +91,8 @@ export const SingleTableWithSticky: React.FC = ({ key={column.columnName} className={cn( column.columnName === "__checkbox__" - ? "h-12 border-0 px-6 py-3 text-center align-middle" - : "h-12 cursor-pointer border-0 px-6 py-3 text-left align-middle font-semibold whitespace-nowrap text-foreground transition-all duration-200 select-none hover:text-foreground", + ? "h-10 border-0 px-3 py-2 text-center align-middle sm:h-12 sm:px-6 sm:py-3" + : "h-10 cursor-pointer border-0 px-3 py-2 text-left align-middle font-semibold whitespace-nowrap text-xs text-foreground transition-all duration-200 select-none hover:text-foreground sm:h-12 sm:px-6 sm:py-3 sm:text-sm", `text-${column.align}`, column.sortable && "hover:bg-primary/10", // 고정 컬럼 스타일 @@ -133,11 +133,11 @@ export const SingleTableWithSticky: React.FC = ({ {columnLabels[column.columnName] || column.displayName || column.columnName} {column.sortable && sortColumn === column.columnName && ( - + {sortDirection === "asc" ? ( - + ) : ( - + )} )} @@ -177,7 +177,7 @@ export const SingleTableWithSticky: React.FC = ({ handleRowClick(row)} @@ -201,7 +201,7 @@ export const SingleTableWithSticky: React.FC = ({ = ({ return (
{/* 중앙 페이지네이션 컨트롤 */}
- + {currentPage} / {totalPages || 1} @@ -819,19 +806,21 @@ export const TableListComponent: React.FC = ({ size="sm" onClick={() => handlePageChange(currentPage + 1)} disabled={currentPage >= totalPages || loading} + className="h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3" > - + - + 전체 {totalItems.toLocaleString()}개
@@ -842,9 +831,9 @@ export const TableListComponent: React.FC = ({ size="sm" onClick={handleRefresh} disabled={loading} - style={{ position: "absolute", right: "24px" }} + className="absolute right-2 h-8 w-8 p-0 sm:right-6 sm:h-9 sm:w-auto sm:px-3" > - +
); @@ -884,14 +873,14 @@ export const TableListComponent: React.FC = ({ return (
{tableConfig.showHeader && ( -
-

{tableConfig.title || tableLabel}

+
+

{tableConfig.title || tableLabel}

)} {tableConfig.filter?.enabled && ( -
-
+
+
= ({ variant="outline" size="sm" onClick={() => setIsFilterSettingOpen(true)} - className="flex-shrink-0 mt-1" + className="flex-shrink-0 w-full sm:w-auto sm:mt-1" > 필터 설정 @@ -946,16 +935,16 @@ export const TableListComponent: React.FC = ({
{/* 헤더 */} {tableConfig.showHeader && ( -
-

{tableConfig.title || tableLabel}

+
+

{tableConfig.title || tableLabel}

)} {/* 필터 */} {tableConfig.filter?.enabled && ( -
-
-
+
+
+
= ({ variant="outline" size="sm" onClick={() => setIsFilterSettingOpen(true)} - style={{ flexShrink: 0, marginTop: "4px" }} + className="flex-shrink-0 w-full sm:w-auto sm:mt-1" > 필터 설정 @@ -978,33 +967,34 @@ export const TableListComponent: React.FC = ({ )} {/* 테이블 컨테이너 */} -
+
{/* 스크롤 영역 */}
{/* 테이블 */} {/* 헤더 (sticky) */} - + {visibleColumns.map((column) => (
column.sortable && handleSort(column.columnName)} > @@ -1063,7 +1053,7 @@ export const TableListComponent: React.FC = ({ onDragStart={(e) => handleRowDragStart(e, row, index)} onDragEnd={handleRowDragEnd} className={cn( - "h-16 border-b transition-colors bg-background hover:bg-muted/50 cursor-pointer" + "h-14 border-b transition-colors bg-background hover:bg-muted/50 cursor-pointer sm:h-16" )} onClick={() => handleRowClick(row)} > @@ -1075,10 +1065,11 @@ export const TableListComponent: React.FC = ({ {column.columnName === "__checkbox__"