# 반응형 레이아웃 시스템 구현 계획서 ## 📋 프로젝트 개요 ### 목표 화면 디자이너는 절대 위치 기반으로 유지하되, 실제 화면 표시는 반응형으로 동작하도록 전환 ### 핵심 원칙 - ✅ 화면 디자이너의 절대 위치 기반 드래그앤드롭은 그대로 유지 - ✅ 실제 화면 표시만 반응형으로 전환 - ✅ 데이터 마이그레이션 불필요 (신규 화면부터 적용) - ✅ 기존 화면은 불러올 때 스마트 기본값 자동 생성 --- ## 🎯 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부터 순차적으로 구현을 시작합니다.