From d7a845ad9fa0d6960b52eee709aebc2ff697b33a Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 16 Oct 2025 18:16:57 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=98=EC=9D=91=ED=98=95=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHASE_RESPONSIVE_LAYOUT.md | 998 ++++++++++++++++++ .../app/(main)/screens/[screenId]/page.tsx | 370 +------ .../screen/ResponsiveLayoutEngine.tsx | 143 +++ .../screen/panels/ResponsiveConfigPanel.tsx | 156 +++ .../screen/panels/UnifiedPropertiesPanel.tsx | 10 + frontend/hooks/useBreakpoint.ts | 45 + .../lib/registry/DynamicComponentRenderer.tsx | 4 + .../SplitPanelLayoutComponent.tsx | 30 +- frontend/lib/utils/responsiveDefaults.ts | 154 +++ frontend/types/responsive.ts | 69 ++ frontend/types/screen-management.ts | 5 + 11 files changed, 1623 insertions(+), 361 deletions(-) create mode 100644 PHASE_RESPONSIVE_LAYOUT.md create mode 100644 frontend/components/screen/ResponsiveLayoutEngine.tsx create mode 100644 frontend/components/screen/panels/ResponsiveConfigPanel.tsx create mode 100644 frontend/hooks/useBreakpoint.ts create mode 100644 frontend/lib/utils/responsiveDefaults.ts create mode 100644 frontend/types/responsive.ts diff --git a/PHASE_RESPONSIVE_LAYOUT.md b/PHASE_RESPONSIVE_LAYOUT.md new file mode 100644 index 00000000..3dde3d49 --- /dev/null +++ b/PHASE_RESPONSIVE_LAYOUT.md @@ -0,0 +1,998 @@ +# 반응형 레이아웃 시스템 구현 계획서 + +## 📋 프로젝트 개요 + +### 목표 + +화면 디자이너는 절대 위치 기반으로 유지하되, 실제 화면 표시는 반응형으로 동작하도록 전환 + +### 핵심 원칙 + +- ✅ 화면 디자이너의 절대 위치 기반 드래그앤드롭은 그대로 유지 +- ✅ 실제 화면 표시만 반응형으로 전환 +- ✅ 데이터 마이그레이션 불필요 (신규 화면부터 적용) +- ✅ 기존 화면은 불러올 때 스마트 기본값 자동 생성 + +--- + +## 🎯 Phase 1: 기본 반응형 시스템 구축 (2-3일) + +### 1.1 타입 정의 (2시간) + +#### 파일: `frontend/types/responsive.ts` + +```typescript +/** + * 브레이크포인트 타입 정의 + */ +export type Breakpoint = "desktop" | "tablet" | "mobile"; + +/** + * 브레이크포인트별 설정 + */ +export interface BreakpointConfig { + minWidth: number; // 최소 너비 (px) + maxWidth?: number; // 최대 너비 (px) + columns: number; // 그리드 컬럼 수 +} + +/** + * 기본 브레이크포인트 설정 + */ +export const BREAKPOINTS: Record = { + desktop: { + minWidth: 1200, + columns: 12, + }, + tablet: { + minWidth: 768, + maxWidth: 1199, + columns: 8, + }, + mobile: { + minWidth: 0, + maxWidth: 767, + columns: 4, + }, +}; + +/** + * 브레이크포인트별 반응형 설정 + */ +export interface ResponsiveBreakpointConfig { + gridColumns?: number; // 차지할 컬럼 수 (1-12) + order?: number; // 정렬 순서 + hide?: boolean; // 숨김 여부 +} + +/** + * 컴포넌트별 반응형 설정 + */ +export interface ResponsiveComponentConfig { + // 기본값 (디자이너에서 설정한 절대 위치) + designerPosition: { + x: number; + y: number; + width: number; + height: number; + }; + + // 반응형 설정 (선택적) + responsive?: { + desktop?: ResponsiveBreakpointConfig; + tablet?: ResponsiveBreakpointConfig; + mobile?: ResponsiveBreakpointConfig; + }; + + // 스마트 기본값 사용 여부 + useSmartDefaults?: boolean; +} +``` + +### 1.2 스마트 기본값 생성기 (3시간) + +#### 파일: `frontend/lib/utils/responsiveDefaults.ts` + +```typescript +import { ComponentData } from "@/types/screen-management"; +import { ResponsiveComponentConfig, BREAKPOINTS } from "@/types/responsive"; + +/** + * 컴포넌트 크기에 따른 스마트 기본값 생성 + * + * 로직: + * - 작은 컴포넌트 (너비 25% 이하): 모바일에서도 같은 너비 유지 + * - 중간 컴포넌트 (너비 25-50%): 모바일에서 전체 너비로 확장 + * - 큰 컴포넌트 (너비 50% 이상): 모든 디바이스에서 전체 너비 + */ +export function generateSmartDefaults( + component: ComponentData, + screenWidth: number = 1920 +): ResponsiveComponentConfig["responsive"] { + const componentWidthPercent = (component.size.width / screenWidth) * 100; + + // 작은 컴포넌트 (25% 이하) + if (componentWidthPercent <= 25) { + return { + desktop: { + gridColumns: 3, // 12컬럼 중 3개 (25%) + order: 1, + hide: false, + }, + tablet: { + gridColumns: 2, // 8컬럼 중 2개 (25%) + order: 1, + hide: false, + }, + mobile: { + gridColumns: 1, // 4컬럼 중 1개 (25%) + order: 1, + hide: false, + }, + }; + } + // 중간 컴포넌트 (25-50%) + else if (componentWidthPercent <= 50) { + return { + desktop: { + gridColumns: 6, // 12컬럼 중 6개 (50%) + order: 1, + hide: false, + }, + tablet: { + gridColumns: 4, // 8컬럼 중 4개 (50%) + order: 1, + hide: false, + }, + mobile: { + gridColumns: 4, // 4컬럼 전체 (100%) + order: 1, + hide: false, + }, + }; + } + // 큰 컴포넌트 (50% 이상) + else { + return { + desktop: { + gridColumns: 12, // 전체 너비 + order: 1, + hide: false, + }, + tablet: { + gridColumns: 8, // 전체 너비 + order: 1, + hide: false, + }, + mobile: { + gridColumns: 4, // 전체 너비 + order: 1, + hide: false, + }, + }; + } +} + +/** + * 컴포넌트에 반응형 설정이 없을 경우 자동 생성 + */ +export function ensureResponsiveConfig( + component: ComponentData, + screenWidth?: number +): ComponentData { + if (component.responsiveConfig) { + return component; + } + + return { + ...component, + responsiveConfig: { + designerPosition: { + x: component.position.x, + y: component.position.y, + width: component.size.width, + height: component.size.height, + }, + useSmartDefaults: true, + responsive: generateSmartDefaults(component, screenWidth), + }, + }; +} +``` + +### 1.3 브레이크포인트 감지 훅 (1시간) + +#### 파일: `frontend/hooks/useBreakpoint.ts` + +```typescript +import { useState, useEffect } from "react"; +import { Breakpoint, BREAKPOINTS } from "@/types/responsive"; + +/** + * 현재 윈도우 크기에 따른 브레이크포인트 반환 + */ +export function useBreakpoint(): Breakpoint { + const [breakpoint, setBreakpoint] = useState("desktop"); + + useEffect(() => { + const updateBreakpoint = () => { + const width = window.innerWidth; + + if (width >= BREAKPOINTS.desktop.minWidth) { + setBreakpoint("desktop"); + } else if (width >= BREAKPOINTS.tablet.minWidth) { + setBreakpoint("tablet"); + } else { + setBreakpoint("mobile"); + } + }; + + // 초기 실행 + updateBreakpoint(); + + // 리사이즈 이벤트 리스너 등록 + window.addEventListener("resize", updateBreakpoint); + + return () => window.removeEventListener("resize", updateBreakpoint); + }, []); + + return breakpoint; +} + +/** + * 현재 브레이크포인트의 컬럼 수 반환 + */ +export function useGridColumns(): number { + const breakpoint = useBreakpoint(); + return BREAKPOINTS[breakpoint].columns; +} +``` + +### 1.4 반응형 레이아웃 엔진 (6시간) + +#### 파일: `frontend/components/screen/ResponsiveLayoutEngine.tsx` + +```typescript +import React, { useMemo } from "react"; +import { ComponentData } from "@/types/screen-management"; +import { Breakpoint, BREAKPOINTS } from "@/types/responsive"; +import { + generateSmartDefaults, + ensureResponsiveConfig, +} from "@/lib/utils/responsiveDefaults"; +import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; + +interface ResponsiveLayoutEngineProps { + components: ComponentData[]; + breakpoint: Breakpoint; + containerWidth: number; + screenWidth?: number; +} + +/** + * 반응형 레이아웃 엔진 + * + * 절대 위치로 배치된 컴포넌트들을 반응형 그리드로 변환 + * + * 변환 로직: + * 1. Y 위치 기준으로 행(row)으로 그룹화 + * 2. 각 행 내에서 X 위치 기준으로 정렬 + * 3. 반응형 설정 적용 (order, gridColumns, hide) + * 4. CSS Grid로 렌더링 + */ +export const ResponsiveLayoutEngine: React.FC = ({ + components, + breakpoint, + containerWidth, + screenWidth = 1920, +}) => { + // 1단계: 컴포넌트들을 Y 위치 기준으로 행(row)으로 그룹화 + const rows = useMemo(() => { + const sortedComponents = [...components].sort( + (a, b) => a.position.y - b.position.y + ); + + const rows: ComponentData[][] = []; + let currentRow: ComponentData[] = []; + let currentRowY = 0; + const ROW_THRESHOLD = 50; // 같은 행으로 간주할 Y 오차 범위 (px) + + sortedComponents.forEach((comp) => { + if (currentRow.length === 0) { + currentRow.push(comp); + currentRowY = comp.position.y; + } else if (Math.abs(comp.position.y - currentRowY) < ROW_THRESHOLD) { + currentRow.push(comp); + } else { + rows.push(currentRow); + currentRow = [comp]; + currentRowY = comp.position.y; + } + }); + + if (currentRow.length > 0) { + rows.push(currentRow); + } + + return rows; + }, [components]); + + // 2단계: 각 행 내에서 X 위치 기준으로 정렬 + const sortedRows = useMemo(() => { + return rows.map((row) => + [...row].sort((a, b) => a.position.x - b.position.x) + ); + }, [rows]); + + // 3단계: 반응형 설정 적용 + const responsiveComponents = useMemo(() => { + return sortedRows.flatMap((row) => + row.map((comp) => { + // 반응형 설정이 없으면 자동 생성 + const compWithConfig = ensureResponsiveConfig(comp, screenWidth); + + // 현재 브레이크포인트의 설정 가져오기 + const config = compWithConfig.responsiveConfig!.useSmartDefaults + ? generateSmartDefaults(comp, screenWidth)[breakpoint] + : compWithConfig.responsiveConfig!.responsive?.[breakpoint]; + + return { + ...compWithConfig, + responsiveDisplay: + config || generateSmartDefaults(comp, screenWidth)[breakpoint], + }; + }) + ); + }, [sortedRows, breakpoint, screenWidth]); + + // 4단계: 필터링 및 정렬 + const visibleComponents = useMemo(() => { + return responsiveComponents + .filter((comp) => !comp.responsiveDisplay?.hide) + .sort( + (a, b) => + (a.responsiveDisplay?.order || 0) - (b.responsiveDisplay?.order || 0) + ); + }, [responsiveComponents]); + + const gridColumns = BREAKPOINTS[breakpoint].columns; + + return ( +
+ {visibleComponents.map((comp) => ( +
+ +
+ ))} +
+ ); +}; +``` + +### 1.5 화면 표시 페이지 수정 (4시간) + +#### 파일: `frontend/app/(main)/screens/[screenId]/page.tsx` + +```typescript +// 기존 import 유지 +import { ResponsiveLayoutEngine } from "@/components/screen/ResponsiveLayoutEngine"; +import { useBreakpoint } from "@/hooks/useBreakpoint"; + +export default function ScreenViewPage({ + params, +}: { + params: { screenId: string }; +}) { + const [layout, setLayout] = useState(null); + const breakpoint = useBreakpoint(); + + // 반응형 모드 토글 (사용자 설정 또는 화면 설정에 따라) + const [useResponsive, setUseResponsive] = useState(true); + + // 기존 로직 유지... + + if (!layout) { + return
로딩 중...
; + } + + const screenWidth = layout.screenResolution?.width || 1920; + const screenHeight = layout.screenResolution?.height || 1080; + + return ( +
+ {useResponsive ? ( + // 반응형 모드 + + ) : ( + // 기존 스케일 모드 (하위 호환성) +
+
+
+ {layout.components?.map((component) => ( +
+ +
+ ))} +
+
+
+ )} +
+ ); +} +``` + +--- + +## 🎨 Phase 2: 디자이너 통합 (1-2일) + +### 2.1 반응형 설정 패널 (5시간) + +#### 파일: `frontend/components/screen/panels/ResponsiveConfigPanel.tsx` + +```typescript +import React, { useState } from "react"; +import { ComponentData } from "@/types/screen-management"; +import { + Breakpoint, + BREAKPOINTS, + ResponsiveComponentConfig, +} from "@/types/responsive"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Checkbox } from "@/components/ui/checkbox"; + +interface ResponsiveConfigPanelProps { + component: ComponentData; + onUpdate: (config: ResponsiveComponentConfig) => void; +} + +export const ResponsiveConfigPanel: React.FC = ({ + component, + onUpdate, +}) => { + const [activeTab, setActiveTab] = useState("desktop"); + + const config = component.responsiveConfig || { + designerPosition: { + x: component.position.x, + y: component.position.y, + width: component.size.width, + height: component.size.height, + }, + useSmartDefaults: true, + }; + + return ( + + + 반응형 설정 + + + {/* 스마트 기본값 토글 */} +
+ { + onUpdate({ + ...config, + useSmartDefaults: checked as boolean, + }); + }} + /> + +
+ + {/* 수동 설정 */} + {!config.useSmartDefaults && ( + setActiveTab(v as Breakpoint)} + > + + 데스크톱 + 태블릿 + 모바일 + + + + {/* 그리드 컬럼 수 */} +
+ + +
+ + {/* 표시 순서 */} +
+ + { + onUpdate({ + ...config, + responsive: { + ...config.responsive, + [activeTab]: { + ...config.responsive?.[activeTab], + order: parseInt(e.target.value), + }, + }, + }); + }} + /> +
+ + {/* 숨김 */} +
+ { + onUpdate({ + ...config, + responsive: { + ...config.responsive, + [activeTab]: { + ...config.responsive?.[activeTab], + hide: checked as boolean, + }, + }, + }); + }} + /> + +
+
+
+ )} +
+
+ ); +}; +``` + +### 2.2 속성 패널 통합 (1시간) + +#### 파일: `frontend/components/screen/panels/UnifiedPropertiesPanel.tsx` 수정 + +```typescript +// 기존 import에 추가 +import { ResponsiveConfigPanel } from './ResponsiveConfigPanel'; + +// 컴포넌트 내부에 추가 +return ( +
+ {/* 기존 패널들 */} + + + + {/* 반응형 설정 패널 추가 */} + { + onUpdateComponent({ + ...selectedComponent, + responsiveConfig: config + }); + }} + /> + + {/* 기존 세부 설정 패널 */} + +
+); +``` + +### 2.3 미리보기 모드 (3시간) + +#### 파일: `frontend/components/screen/ScreenDesigner.tsx` 수정 + +```typescript +// 추가 import +import { Breakpoint } from '@/types/responsive'; +import { ResponsiveLayoutEngine } from './ResponsiveLayoutEngine'; +import { useBreakpoint } from '@/hooks/useBreakpoint'; +import { Button } from '@/components/ui/button'; + +export const ScreenDesigner: React.FC = () => { + // 미리보기 모드: 'design' | 'desktop' | 'tablet' | 'mobile' + const [previewMode, setPreviewMode] = useState<'design' | Breakpoint>('design'); + const currentBreakpoint = useBreakpoint(); + + // ... 기존 로직 ... + + return ( +
+ {/* 상단 툴바 */} +
+ + + + +
+ + {/* 캔버스 영역 */} +
+ {previewMode === 'design' ? ( + // 기존 절대 위치 기반 디자이너 + + ) : ( + // 반응형 미리보기 +
+ +
+ )} +
+
+ ); +}; +``` + +--- + +## 💾 Phase 3: 저장/불러오기 (1일) + +### 3.1 타입 업데이트 (2시간) + +#### 파일: `frontend/types/screen-management.ts` 수정 + +```typescript +import { ResponsiveComponentConfig } from "./responsive"; + +export interface ComponentData { + // ... 기존 필드들 ... + + // 반응형 설정 추가 + responsiveConfig?: ResponsiveComponentConfig; +} +``` + +### 3.2 저장 로직 (2시간) + +#### 파일: `frontend/components/screen/ScreenDesigner.tsx` 수정 + +```typescript +// 저장 함수 수정 +const handleSave = async () => { + try { + const layoutData: LayoutData = { + screenResolution: { + width: 1920, + height: 1080, + }, + components: components.map((comp) => ({ + ...comp, + // 반응형 설정이 없으면 자동 생성 + responsiveConfig: comp.responsiveConfig || { + designerPosition: { + x: comp.position.x, + y: comp.position.y, + width: comp.size.width, + height: comp.size.height, + }, + useSmartDefaults: true, + }, + })), + }; + + await screenApi.updateLayout(selectedScreen.id, layoutData); + // ... 기존 로직 ... + } catch (error) { + console.error("저장 실패:", error); + } +}; +``` + +### 3.3 불러오기 로직 (2시간) + +#### 파일: `frontend/components/screen/ScreenDesigner.tsx` 수정 + +```typescript +import { ensureResponsiveConfig } from "@/lib/utils/responsiveDefaults"; + +// 화면 불러오기 +useEffect(() => { + const loadScreen = async () => { + if (!selectedScreenId) return; + + const screen = await screenApi.getScreenById(selectedScreenId); + const layout = await screenApi.getLayout(selectedScreenId); + + // 반응형 설정이 없는 컴포넌트에 자동 생성 + const componentsWithResponsive = layout.components.map((comp) => + ensureResponsiveConfig(comp, layout.screenResolution?.width) + ); + + setSelectedScreen(screen); + setComponents(componentsWithResponsive); + }; + + loadScreen(); +}, [selectedScreenId]); +``` + +--- + +## 🧪 Phase 4: 테스트 및 최적화 (1일) + +### 4.1 기능 테스트 체크리스트 (3시간) + +- [ ] 브레이크포인트 전환 테스트 + - [ ] 윈도우 크기 변경 시 자동 전환 + - [ ] desktop → tablet → mobile 순차 테스트 +- [ ] 스마트 기본값 생성 테스트 + - [ ] 작은 컴포넌트 (25% 이하) + - [ ] 중간 컴포넌트 (25-50%) + - [ ] 큰 컴포넌트 (50% 이상) +- [ ] 수동 설정 적용 테스트 + - [ ] 그리드 컬럼 변경 + - [ ] 표시 순서 변경 + - [ ] 디바이스별 숨김 +- [ ] 미리보기 모드 테스트 + - [ ] 디자인 모드 ↔ 미리보기 모드 전환 + - [ ] 각 브레이크포인트 미리보기 +- [ ] 저장/불러오기 테스트 + - [ ] 반응형 설정 저장 + - [ ] 기존 화면 불러오기 시 자동 변환 + +### 4.2 성능 최적화 (3시간) + +#### 레이아웃 계산 메모이제이션 + +```typescript +// ResponsiveLayoutEngine.tsx +const memoizedLayout = useMemo(() => { + // 레이아웃 계산 로직 +}, [components, breakpoint, screenWidth]); +``` + +#### ResizeObserver 최적화 + +```typescript +// useBreakpoint.ts +// debounce 적용 +const debouncedResize = debounce(updateBreakpoint, 150); +window.addEventListener("resize", debouncedResize); +``` + +#### 불필요한 리렌더링 방지 + +```typescript +// React.memo 적용 +export const ResponsiveLayoutEngine = React.memo(({...}) => { + // ... +}); +``` + +### 4.3 UI/UX 개선 (2시간) + +- [ ] 반응형 설정 패널 툴팁 추가 +- [ ] 미리보기 모드 전환 애니메이션 +- [ ] 로딩 상태 표시 +- [ ] 에러 처리 및 사용자 피드백 + +--- + +## 📅 최종 타임라인 + +| Phase | 작업 내용 | 소요 시간 | 누적 시간 | +| ------- | --------------------- | --------- | ------------ | +| Phase 1 | 타입 정의 및 유틸리티 | 6시간 | 6시간 | +| Phase 1 | 반응형 레이아웃 엔진 | 6시간 | 12시간 | +| Phase 1 | 화면 표시 페이지 수정 | 4시간 | 16시간 (2일) | +| Phase 2 | 반응형 설정 패널 | 5시간 | 21시간 | +| Phase 2 | 디자이너 통합 | 4시간 | 25시간 (3일) | +| Phase 3 | 저장/불러오기 | 6시간 | 31시간 (4일) | +| Phase 4 | 테스트 및 최적화 | 8시간 | 39시간 (5일) | + +**총 예상 시간: 39시간 (약 5일)** + +--- + +## 🎯 구현 우선순위 + +### 1단계: 핵심 기능 (필수) + +1. ✅ 타입 정의 +2. ✅ 스마트 기본값 생성기 +3. ✅ 브레이크포인트 훅 +4. ✅ 반응형 레이아웃 엔진 +5. ✅ 화면 표시 페이지 수정 + +### 2단계: 디자이너 UI (중요) + +6. ✅ 반응형 설정 패널 +7. ✅ 속성 패널 통합 +8. ✅ 미리보기 모드 + +### 3단계: 데이터 처리 (중요) + +9. ✅ 타입 업데이트 +10. ✅ 저장/불러오기 로직 + +### 4단계: 완성도 (선택) + +11. 테스트 +12. 최적화 +13. UI/UX 개선 + +--- + +## ✅ 완료 체크리스트 + +### Phase 1: 기본 시스템 + +- [ ] `frontend/types/responsive.ts` 생성 +- [ ] `frontend/lib/utils/responsiveDefaults.ts` 생성 +- [ ] `frontend/hooks/useBreakpoint.ts` 생성 +- [ ] `frontend/components/screen/ResponsiveLayoutEngine.tsx` 생성 +- [ ] `frontend/app/(main)/screens/[screenId]/page.tsx` 수정 + +### Phase 2: 디자이너 통합 + +- [ ] `frontend/components/screen/panels/ResponsiveConfigPanel.tsx` 생성 +- [ ] `frontend/components/screen/panels/UnifiedPropertiesPanel.tsx` 수정 +- [ ] `frontend/components/screen/ScreenDesigner.tsx` 수정 + +### Phase 3: 데이터 처리 + +- [ ] `frontend/types/screen-management.ts` 수정 +- [ ] 저장 로직 수정 +- [ ] 불러오기 로직 수정 + +### Phase 4: 테스트 + +- [ ] 기능 테스트 완료 +- [ ] 성능 최적화 완료 +- [ ] UI/UX 개선 완료 + +--- + +## 🚀 시작 준비 완료 + +이제 Phase 1부터 순차적으로 구현을 시작합니다. diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 81e7ee49..20484a2c 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -1,20 +1,17 @@ "use client"; -import React, { useEffect, useState, useRef, useMemo } from "react"; +import React, { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Loader2 } from "lucide-react"; import { screenApi } from "@/lib/api/screen"; import { ScreenDefinition, LayoutData } from "@/types/screen"; -import { InteractiveScreenViewer } from "@/components/screen/InteractiveScreenViewerDynamic"; -import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; -import { DynamicWebTypeRenderer } from "@/lib/registry"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { initializeComponents } from "@/lib/registry/components"; import { EditModal } from "@/components/screen/EditModal"; -import { isFileComponent, getComponentWebType } from "@/lib/utils/componentTypeUtils"; -// import { ResponsiveScreenContainer } from "@/components/screen/ResponsiveScreenContainer"; // 컨테이너 제거 +import { ResponsiveLayoutEngine } from "@/components/screen/ResponsiveLayoutEngine"; +import { useBreakpoint } from "@/hooks/useBreakpoint"; export default function ScreenViewPage() { const params = useParams(); @@ -27,15 +24,7 @@ export default function ScreenViewPage() { const [error, setError] = useState(null); const [formData, setFormData] = useState>({}); - // 테이블 선택된 행 상태 (화면 레벨에서 관리) - const [selectedRows, setSelectedRows] = useState([]); - const [selectedRowsData, setSelectedRowsData] = useState([]); - - // 테이블 새로고침을 위한 키 상태 - const [refreshKey, setRefreshKey] = useState(0); - - // 스케일 상태 - const [scale, setScale] = useState(1); + const breakpoint = useBreakpoint(); // 편집 모달 상태 const [editModalOpen, setEditModalOpen] = useState(false); @@ -122,73 +111,6 @@ 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]); - - // 실제 컨텐츠의 동적 높이 상태 - const [actualContentHeight, setActualContentHeight] = useState(layout?.screenResolution?.height || 800); - const contentRef = useRef(null); - - // ResizeObserver로 컨텐츠 높이 실시간 모니터링 - useEffect(() => { - if (!contentRef.current) return; - - const resizeObserver = new ResizeObserver(() => { - if (!contentRef.current) return; - - // 모든 컴포넌트의 실제 높이를 측정 - const components = contentRef.current.querySelectorAll("[data-component-id]"); - let maxBottom = layout?.screenResolution?.height || 800; - - components.forEach((element) => { - const rect = element.getBoundingClientRect(); - const parentRect = contentRef.current!.getBoundingClientRect(); - const relativeTop = rect.top - parentRect.top; - const bottom = relativeTop + rect.height; - maxBottom = Math.max(maxBottom, bottom); - }); - - setActualContentHeight(maxBottom); - }); - - resizeObserver.observe(contentRef.current); - - // 모든 자식 요소도 관찰 - const childElements = contentRef.current.querySelectorAll("[data-component-id]"); - childElements.forEach((child) => resizeObserver.observe(child)); - - return () => { - resizeObserver.disconnect(); - }; - }, [layout]); - if (loading) { return (
@@ -219,275 +141,21 @@ export default function ScreenViewPage() { // 화면 해상도 정보가 있으면 해당 크기로, 없으면 기본 크기 사용 const screenWidth = layout?.screenResolution?.width || 1200; - 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); - - 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 ( -
- {/* 라벨을 외부에 별도로 렌더링 */} - {shouldShowLabel && ( -
- {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; - setFormData((prev) => ({ - ...prev, - [fieldName]: value, - })); - }, - 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 -
-
+
+
+ {/* 항상 반응형 모드로 렌더링 */} + {layout && layout.components.length > 0 ? ( + + ) : ( + // 빈 화면일 때 +
📄 @@ -496,8 +164,8 @@ export default function ScreenViewPage() {

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

-
- )} + )} +
{/* 편집 모달 */} = ({ + components, + breakpoint, + containerWidth, + screenWidth = 1920, +}) => { + // 1단계: 컴포넌트들을 Y 위치 기준으로 행(row)으로 그룹화 + const rows = useMemo(() => { + const sortedComponents = [...components].sort((a, b) => a.position.y - b.position.y); + + const rows: ComponentData[][] = []; + let currentRow: ComponentData[] = []; + let currentRowY = 0; + const ROW_THRESHOLD = 150; // 같은 행으로 간주할 Y 오차 범위 (px) - 여유있게 설정 + + sortedComponents.forEach((comp) => { + if (currentRow.length === 0) { + currentRow.push(comp); + currentRowY = comp.position.y; + } else if (Math.abs(comp.position.y - currentRowY) < ROW_THRESHOLD) { + currentRow.push(comp); + } else { + rows.push(currentRow); + currentRow = [comp]; + currentRowY = comp.position.y; + } + }); + + if (currentRow.length > 0) { + rows.push(currentRow); + } + + return rows; + }, [components]); + + // 2단계: 각 행 내에서 X 위치 기준으로 정렬 + const sortedRows = useMemo(() => { + return rows.map((row) => [...row].sort((a, b) => a.position.x - b.position.x)); + }, [rows]); + + // 3단계: 반응형 설정 적용 + const responsiveComponents = useMemo(() => { + const result = sortedRows.flatMap((row, rowIndex) => + row.map((comp, compIndex) => { + // 반응형 설정이 없으면 자동 생성 + const compWithConfig = ensureResponsiveConfig(comp, screenWidth); + + // 현재 브레이크포인트의 설정 가져오기 (같은 행의 컴포넌트 개수 전달) + const config = compWithConfig.responsiveConfig!.useSmartDefaults + ? generateSmartDefaults(comp, screenWidth, row.length)[breakpoint] + : compWithConfig.responsiveConfig!.responsive?.[breakpoint]; + + const finalConfig = config || generateSmartDefaults(comp, screenWidth, row.length)[breakpoint]; + + return { + ...compWithConfig, + responsiveDisplay: finalConfig, + }; + }), + ); + + return result; + }, [sortedRows, breakpoint, screenWidth]); + + // 4단계: 필터링 및 정렬 + const visibleComponents = useMemo(() => { + return responsiveComponents + .filter((comp) => !comp.responsiveDisplay?.hide) + .sort((a, b) => (a.responsiveDisplay?.order || 0) - (b.responsiveDisplay?.order || 0)); + }, [responsiveComponents]); + + const gridColumns = BREAKPOINTS[breakpoint].columns; + + // 각 행의 Y 위치를 추적 + const rowsWithYPosition = useMemo(() => { + return sortedRows.map((row) => ({ + components: row, + yPosition: Math.min(...row.map((c) => c.position.y)), // 행의 최소 Y 위치 + })); + }, [sortedRows]); + + return ( +
+ {rowsWithYPosition.map((row, rowIndex) => { + const rowComponents = visibleComponents.filter((vc) => row.components.some((rc) => rc.id === vc.id)); + + return ( +
+ {rowComponents.map((comp) => ( +
+ +
+ ))} +
+ ); + })} +
+ ); +}; diff --git a/frontend/components/screen/panels/ResponsiveConfigPanel.tsx b/frontend/components/screen/panels/ResponsiveConfigPanel.tsx new file mode 100644 index 00000000..1565b58e --- /dev/null +++ b/frontend/components/screen/panels/ResponsiveConfigPanel.tsx @@ -0,0 +1,156 @@ +/** + * 반응형 설정 패널 + * + * 컴포넌트별로 브레이크포인트마다 다른 레이아웃 설정 가능 + */ + +import React, { useState } from "react"; +import { ComponentData } from "@/types/screen-management"; +import { Breakpoint, BREAKPOINTS, ResponsiveComponentConfig } from "@/types/responsive"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Checkbox } from "@/components/ui/checkbox"; + +interface ResponsiveConfigPanelProps { + component: ComponentData; + onUpdate: (config: ResponsiveComponentConfig) => void; +} + +export const ResponsiveConfigPanel: React.FC = ({ component, onUpdate }) => { + const [activeTab, setActiveTab] = useState("desktop"); + + const config = component.responsiveConfig || { + designerPosition: { + x: component.position.x, + y: component.position.y, + width: component.size.width, + height: component.size.height, + }, + useSmartDefaults: true, + }; + + return ( + + + 반응형 설정 + + + {/* 스마트 기본값 토글 */} +
+ { + onUpdate({ + ...config, + useSmartDefaults: checked as boolean, + }); + }} + /> + +
+ +
+ 스마트 기본값은 컴포넌트 크기에 따라 자동으로 반응형 레이아웃을 생성합니다. +
+ + {/* 수동 설정 */} + {!config.useSmartDefaults && ( + setActiveTab(v as Breakpoint)}> + + 데스크톱 + 태블릿 + 모바일 + + + + {/* 그리드 컬럼 수 */} +
+ + +
+ + {/* 표시 순서 */} +
+ + { + onUpdate({ + ...config, + responsive: { + ...config.responsive, + [activeTab]: { + ...config.responsive?.[activeTab], + order: parseInt(e.target.value), + }, + }, + }); + }} + /> +
작은 숫자가 먼저 표시됩니다.
+
+ + {/* 숨김 */} +
+ { + onUpdate({ + ...config, + responsive: { + ...config.responsive, + [activeTab]: { + ...config.responsive?.[activeTab], + hide: checked as boolean, + }, + }, + }); + }} + /> + +
+
+
+ )} +
+
+ ); +}; diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index b04dd0a4..cc4d3ef1 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -48,6 +48,7 @@ import { DashboardConfigPanel } from "../config-panels/DashboardConfigPanel"; import { StatsCardConfigPanel } from "../config-panels/StatsCardConfigPanel"; import { ProgressBarConfigPanel } from "../config-panels/ProgressBarConfigPanel"; import { ChartConfigPanel } from "../config-panels/ChartConfigPanel"; +import { ResponsiveConfigPanel } from "./ResponsiveConfigPanel"; import { AlertConfigPanel } from "../config-panels/AlertConfigPanel"; import { BadgeConfigPanel } from "../config-panels/BadgeConfigPanel"; import { DynamicComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel"; @@ -707,6 +708,7 @@ export const UnifiedPropertiesPanel: React.FC = ({ 기본 상세 데이터 + 반응형
@@ -719,6 +721,14 @@ export const UnifiedPropertiesPanel: React.FC = ({ {renderDataTab()} + + { + onUpdateProperty(selectedComponent.id, "responsiveConfig", config); + }} + /> +
diff --git a/frontend/hooks/useBreakpoint.ts b/frontend/hooks/useBreakpoint.ts new file mode 100644 index 00000000..28e1ad71 --- /dev/null +++ b/frontend/hooks/useBreakpoint.ts @@ -0,0 +1,45 @@ +/** + * 반응형 브레이크포인트 감지 훅 + */ + +import { useState, useEffect } from "react"; +import { Breakpoint, BREAKPOINTS } from "@/types/responsive"; + +/** + * 현재 윈도우 크기에 따른 브레이크포인트 반환 + */ +export function useBreakpoint(): Breakpoint { + const [breakpoint, setBreakpoint] = useState("desktop"); + + useEffect(() => { + const updateBreakpoint = () => { + const width = window.innerWidth; + + if (width >= BREAKPOINTS.desktop.minWidth) { + setBreakpoint("desktop"); + } else if (width >= BREAKPOINTS.tablet.minWidth) { + setBreakpoint("tablet"); + } else { + setBreakpoint("mobile"); + } + }; + + // 초기 실행 + updateBreakpoint(); + + // 리사이즈 이벤트 리스너 등록 + window.addEventListener("resize", updateBreakpoint); + + return () => window.removeEventListener("resize", updateBreakpoint); + }, []); + + return breakpoint; +} + +/** + * 현재 브레이크포인트의 컬럼 수 반환 + */ +export function useGridColumns(): number { + const breakpoint = useBreakpoint(); + return BREAKPOINTS[breakpoint].columns; +} diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 79c085e5..4f6ff820 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -76,6 +76,7 @@ export const componentRegistry = legacyComponentRegistry; export interface DynamicComponentRendererProps { component: ComponentData; isSelected?: boolean; + isPreview?: boolean; // 반응형 모드 플래그 onClick?: (e?: React.MouseEvent) => void; onDragStart?: (e: React.DragEvent) => void; onDragEnd?: () => void; @@ -105,6 +106,7 @@ export interface DynamicComponentRendererProps { export const DynamicComponentRenderer: React.FC = ({ component, isSelected = false, + isPreview = false, onClick, onDragStart, onDragEnd, @@ -233,6 +235,8 @@ export const DynamicComponentRenderer: React.FC = // 설정 변경 핸들러 전달 onConfigChange, refreshKey, + // 반응형 모드 플래그 전달 + isPreview, }; // 렌더러가 클래스인지 함수인지 확인 diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 822bf651..b4bc3ba5 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -23,6 +23,7 @@ export const SplitPanelLayoutComponent: React.FC component, isDesignMode = false, isSelected = false, + isPreview = false, onClick, ...props }) => { @@ -52,16 +53,25 @@ export const SplitPanelLayoutComponent: React.FC const containerRef = React.useRef(null); // 컴포넌트 스타일 - const componentStyle: React.CSSProperties = { - position: "absolute", - left: `${component.style?.positionX || 0}px`, - top: `${component.style?.positionY || 0}px`, - width: `${component.style?.width || 1000}px`, - height: `${component.style?.height || 600}px`, - zIndex: component.style?.positionZ || 1, - cursor: isDesignMode ? "pointer" : "default", - border: isSelected ? "2px solid #3b82f6" : "1px solid #e5e7eb", - }; + const componentStyle: React.CSSProperties = isPreview + ? { + // 반응형 모드: position relative, width/height 100% + position: "relative", + width: "100%", + height: `${component.style?.height || 600}px`, + border: "1px solid #e5e7eb", + } + : { + // 디자이너 모드: position absolute + position: "absolute", + left: `${component.style?.positionX || 0}px`, + top: `${component.style?.positionY || 0}px`, + width: `${component.style?.width || 1000}px`, + height: `${component.style?.height || 600}px`, + zIndex: component.style?.positionZ || 1, + cursor: isDesignMode ? "pointer" : "default", + border: isSelected ? "2px solid #3b82f6" : "1px solid #e5e7eb", + }; // 좌측 데이터 로드 const loadLeftData = useCallback(async () => { diff --git a/frontend/lib/utils/responsiveDefaults.ts b/frontend/lib/utils/responsiveDefaults.ts new file mode 100644 index 00000000..cc180cee --- /dev/null +++ b/frontend/lib/utils/responsiveDefaults.ts @@ -0,0 +1,154 @@ +/** + * 반응형 스마트 기본값 생성 유틸리티 + */ + +import { ComponentData } from "@/types/screen-management"; +import { ResponsiveComponentConfig, BREAKPOINTS } from "@/types/responsive"; + +/** + * 컴포넌트 크기에 따른 스마트 기본값 생성 + * + * 로직: + * - 작은 컴포넌트 (너비 25% 이하): 모바일에서도 같은 너비 유지 + * - 중간 컴포넌트 (너비 25-50%): 모바일에서 전체 너비로 확장 + * - 큰 컴포넌트 (너비 50% 이상): 모든 디바이스에서 전체 너비 + */ +export function generateSmartDefaults( + component: ComponentData, + screenWidth: number = 1920, + rowComponentCount: number = 1, // 같은 행에 있는 컴포넌트 개수 +): ResponsiveComponentConfig["responsive"] { + // 특정 컴포넌트는 항상 전체 너비 (split-panel-layout, datatable 등) + const fullWidthComponents = ["split-panel-layout", "datatable", "data-table"]; + const componentType = (component as any).componentType || component.type; + + if (fullWidthComponents.includes(componentType)) { + return { + desktop: { + gridColumns: 12, // 전체 너비 + order: 1, + hide: false, + }, + tablet: { + gridColumns: 8, // 전체 너비 + order: 1, + hide: false, + }, + mobile: { + gridColumns: 4, // 전체 너비 + order: 1, + hide: false, + }, + }; + } + + const componentWidthPercent = (component.size.width / screenWidth) * 100; + + // 같은 행에 여러 컴포넌트가 있으면 컬럼을 나눔 + if (rowComponentCount > 1) { + const desktopColumns = Math.round(12 / rowComponentCount); + const tabletColumns = Math.round(8 / rowComponentCount); + const mobileColumns = 4; // 모바일에서는 항상 전체 너비 + + return { + desktop: { + gridColumns: desktopColumns, + order: 1, + hide: false, + }, + tablet: { + gridColumns: tabletColumns, + order: 1, + hide: false, + }, + mobile: { + gridColumns: mobileColumns, + order: 1, + hide: false, + }, + }; + } + // 작은 컴포넌트 (25% 이하) + else if (componentWidthPercent <= 25) { + return { + desktop: { + gridColumns: 3, // 12컬럼 중 3개 (25%) + order: 1, + hide: false, + }, + tablet: { + gridColumns: 2, // 8컬럼 중 2개 (25%) + order: 1, + hide: false, + }, + mobile: { + gridColumns: 1, // 4컬럼 중 1개 (25%) + order: 1, + hide: false, + }, + }; + } + // 중간 컴포넌트 (25-50%) + else if (componentWidthPercent <= 50) { + return { + desktop: { + gridColumns: 6, // 12컬럼 중 6개 (50%) + order: 1, + hide: false, + }, + tablet: { + gridColumns: 4, // 8컬럼 중 4개 (50%) + order: 1, + hide: false, + }, + mobile: { + gridColumns: 4, // 4컬럼 전체 (100%) + order: 1, + hide: false, + }, + }; + } + // 큰 컴포넌트 (50% 이상) + else { + return { + desktop: { + gridColumns: 12, // 전체 너비 + order: 1, + hide: false, + }, + tablet: { + gridColumns: 8, // 전체 너비 + order: 1, + hide: false, + }, + mobile: { + gridColumns: 4, // 전체 너비 + order: 1, + hide: false, + }, + }; + } +} + +/** + * 컴포넌트에 반응형 설정이 없을 경우 자동 생성 + */ +export function ensureResponsiveConfig(component: ComponentData, screenWidth?: number): ComponentData { + if (component.responsiveConfig) { + return component; + } + + return { + ...component, + responsiveConfig: { + designerPosition: { + x: component.position.x, + y: component.position.y, + width: component.size.width, + height: component.size.height, + }, + useSmartDefaults: true, + responsive: generateSmartDefaults(component, screenWidth), + }, + }; +} diff --git a/frontend/types/responsive.ts b/frontend/types/responsive.ts new file mode 100644 index 00000000..3ff8f2b9 --- /dev/null +++ b/frontend/types/responsive.ts @@ -0,0 +1,69 @@ +/** + * 반응형 레이아웃 시스템 타입 정의 + */ + +/** + * 브레이크포인트 타입 정의 + */ +export type Breakpoint = "desktop" | "tablet" | "mobile"; + +/** + * 브레이크포인트별 설정 + */ +export interface BreakpointConfig { + minWidth: number; // 최소 너비 (px) + maxWidth?: number; // 최대 너비 (px) + columns: number; // 그리드 컬럼 수 +} + +/** + * 기본 브레이크포인트 설정 + */ +export const BREAKPOINTS: Record = { + desktop: { + minWidth: 1200, + columns: 12, + }, + tablet: { + minWidth: 768, + maxWidth: 1199, + columns: 8, + }, + mobile: { + minWidth: 0, + maxWidth: 767, + columns: 4, + }, +}; + +/** + * 브레이크포인트별 반응형 설정 + */ +export interface ResponsiveBreakpointConfig { + gridColumns?: number; // 차지할 컬럼 수 (1-12) + order?: number; // 정렬 순서 + hide?: boolean; // 숨김 여부 +} + +/** + * 컴포넌트별 반응형 설정 + */ +export interface ResponsiveComponentConfig { + // 기본값 (디자이너에서 설정한 절대 위치) + designerPosition: { + x: number; + y: number; + width: number; + height: number; + }; + + // 반응형 설정 (선택적) + responsive?: { + desktop?: ResponsiveBreakpointConfig; + tablet?: ResponsiveBreakpointConfig; + mobile?: ResponsiveBreakpointConfig; + }; + + // 스마트 기본값 사용 여부 + useSmartDefaults?: boolean; +} diff --git a/frontend/types/screen-management.ts b/frontend/types/screen-management.ts index 8cf37bf1..7f29f22b 100644 --- a/frontend/types/screen-management.ts +++ b/frontend/types/screen-management.ts @@ -18,6 +18,7 @@ import { isWebType, } from "./unified-core"; import { ColumnSpanPreset } from "@/lib/constants/columnSpans"; +import { ResponsiveComponentConfig } from "./responsive"; // ===== 기본 컴포넌트 인터페이스 ===== @@ -50,6 +51,10 @@ export interface BaseComponent { componentConfig?: any; // 컴포넌트별 설정 componentType?: string; // 새 컴포넌트 시스템의 ID webTypeConfig?: WebTypeConfig; // 웹타입별 설정 + + // 반응형 설정 + responsiveConfig?: ResponsiveComponentConfig; + responsiveDisplay?: any; // 런타임에 추가되는 임시 필드 } /**