# 반응형 그리드 시스템 아키텍처 > 최종 업데이트: 2026-01-30 ## 1. 개요 ### 1.1 문제 정의 **현재 상황**: 컴포넌트 위치/크기가 픽셀 단위로 고정되어 반응형 미지원 ```json // 현재 저장 방식 (screen_layouts_v2.layout_data) { "position": { "x": 1753, "y": 88 }, "size": { "width": 158, "height": 40 } } ``` **발생 문제**: - 1920px 기준 설계 → 1280px 화면에서 버튼이 화면 밖으로 나감 - 모바일/태블릿에서 레이아웃 완전히 깨짐 - 화면 축소해도 컴포넌트 위치/크기 그대로 ### 1.2 목표 | 목표 | 설명 | |------|------| | **PC 대응** | 1280px ~ 1920px 화면에서 정상 동작 | | **태블릿 대응** | 768px ~ 1024px 화면에서 레이아웃 재배치 | | **모바일 대응** | 320px ~ 767px 화면에서 세로 스택 | | **shadcn/Tailwind 활용** | 반응형 브레이크포인트 시스템 사용 | ### 1.3 핵심 원칙 ``` 현재: 픽셀 좌표 → position: absolute → 고정 레이아웃 변경: 그리드 셀 번호 → CSS Grid + Tailwind → 반응형 레이아웃 ``` --- ## 2. 현재 시스템 분석 ### 2.1 기존 그리드 설정 (이미 존재) ```typescript // frontend/components/screen/ScreenDesigner.tsx gridSettings: { columns: 12, // ✅ 이미 12컬럼 그리드 있음 gap: 16, // ✅ 간격 설정 있음 padding: 0, snapToGrid: true, // ✅ 스냅 기능 있음 showGrid: false, gridColor: "#d1d5db", gridOpacity: 0.5, } ``` ### 2.2 현재 저장 방식 ```typescript // 드래그 후 저장되는 데이터 { "id": "comp_1896", "url": "@/lib/registry/components/v2-button-primary", "position": { "x": 1753.33, "y": 88, "z": 1 }, // 픽셀 좌표 "size": { "width": 158.67, "height": 40 }, // 픽셀 크기 "overrides": { ... } } ``` ### 2.3 현재 렌더링 방식 ```tsx // frontend/lib/registry/layouts/flexbox/FlexboxLayout.tsx (라인 234-248)
``` ### 2.4 문제점 요약 | 현재 | 문제 | |------|------| | 12컬럼 그리드 있음 | 스냅용으로만 사용, 저장은 픽셀 | | position: 픽셀 좌표 | 화면 크기 변해도 위치 고정 | | size: 픽셀 크기 | 화면 작아지면 넘침 | | absolute 포지션 | 반응형 불가 | --- ## 3. 신규 데이터 구조 ### 3.1 layout_data 구조 변경 **현재 구조**: ```json { "version": "2.0", "components": [{ "id": "comp_xxx", "url": "@/lib/registry/components/v2-button-primary", "position": { "x": 1753, "y": 88, "z": 1 }, "size": { "width": 158, "height": 40 }, "overrides": { ... } }] } ``` **변경 후 구조**: ```json { "version": "3.0", "layoutMode": "grid", "components": [{ "id": "comp_xxx", "url": "@/lib/registry/components/v2-button-primary", "grid": { "col": 11, "row": 2, "colSpan": 2, "rowSpan": 1 }, "responsive": { "sm": { "col": 1, "colSpan": 12 }, "md": { "col": 7, "colSpan": 6 }, "lg": { "col": 11, "colSpan": 2 } }, "overrides": { ... } }], "gridSettings": { "columns": 12, "rowHeight": 80, "gap": 16 } } ``` ### 3.2 필드 설명 | 필드 | 타입 | 설명 | |------|------|------| | `version` | string | "3.0" (반응형 그리드 버전) | | `layoutMode` | string | "grid" (그리드 레이아웃 사용) | | `grid.col` | number | 시작 컬럼 (1-12) | | `grid.row` | number | 시작 행 (1부터) | | `grid.colSpan` | number | 차지하는 컬럼 수 | | `grid.rowSpan` | number | 차지하는 행 수 | | `responsive.sm` | object | 모바일 (< 768px) 설정 | | `responsive.md` | object | 태블릿 (768px ~ 1024px) 설정 | | `responsive.lg` | object | 데스크톱 (> 1024px) 설정 | ### 3.3 반응형 브레이크포인트 | 브레이크포인트 | 화면 크기 | 기본 동작 | |----------------|-----------|-----------| | `sm` | < 768px | 모든 컴포넌트 12컬럼 (세로 스택) | | `md` | 768px ~ 1024px | 컬럼 수 2배로 확장 | | `lg` | > 1024px | 원본 그리드 위치 유지 | --- ## 4. 변환 로직 ### 4.1 픽셀 → 그리드 변환 함수 ```typescript // frontend/lib/utils/gridConverter.ts const DESIGN_WIDTH = 1920; const COLUMNS = 12; const COLUMN_WIDTH = DESIGN_WIDTH / COLUMNS; // 160px const ROW_HEIGHT = 80; interface PixelPosition { x: number; y: number; } interface PixelSize { width: number; height: number; } interface GridPosition { col: number; row: number; colSpan: number; rowSpan: number; } interface ResponsiveConfig { sm: { col: number; colSpan: number }; md: { col: number; colSpan: number }; lg: { col: number; colSpan: number }; } /** * 픽셀 좌표를 그리드 셀 번호로 변환 */ export function pixelToGrid( position: PixelPosition, size: PixelSize ): GridPosition { // 컬럼 계산 (1-based) const col = Math.max(1, Math.min(12, Math.round(position.x / COLUMN_WIDTH) + 1)); // 행 계산 (1-based) const row = Math.max(1, Math.round(position.y / ROW_HEIGHT) + 1); // 컬럼 스팬 계산 const colSpan = Math.max(1, Math.min(12 - col + 1, Math.round(size.width / COLUMN_WIDTH))); // 행 스팬 계산 const rowSpan = Math.max(1, Math.round(size.height / ROW_HEIGHT)); return { col, row, colSpan, rowSpan }; } /** * 그리드 셀 번호를 픽셀 좌표로 변환 (디자인 모드용) */ export function gridToPixel( grid: GridPosition ): { position: PixelPosition; size: PixelSize } { return { position: { x: (grid.col - 1) * COLUMN_WIDTH, y: (grid.row - 1) * ROW_HEIGHT, }, size: { width: grid.colSpan * COLUMN_WIDTH, height: grid.rowSpan * ROW_HEIGHT, }, }; } /** * 기본 반응형 설정 생성 */ export function getDefaultResponsive( grid: GridPosition ): ResponsiveConfig { return { // 모바일: 전체 너비, 원래 순서대로 스택 sm: { col: 1, colSpan: 12 }, // 태블릿: 컬럼 스팬 2배 (최대 12) md: { col: Math.max(1, Math.round((grid.col - 1) / 2) + 1), colSpan: Math.min(grid.colSpan * 2, 12) }, // 데스크톱: 원본 유지 lg: { col: grid.col, colSpan: grid.colSpan }, }; } ``` ### 4.2 Tailwind 클래스 생성 함수 ```typescript // frontend/lib/utils/gridClassGenerator.ts /** * 그리드 위치/크기를 Tailwind 클래스로 변환 */ export function generateGridClasses( grid: GridPosition, responsive: ResponsiveConfig ): string { const classes: string[] = []; // 모바일 (기본) classes.push(`col-start-${responsive.sm.col}`); classes.push(`col-span-${responsive.sm.colSpan}`); // 태블릿 classes.push(`md:col-start-${responsive.md.col}`); classes.push(`md:col-span-${responsive.md.colSpan}`); // 데스크톱 classes.push(`lg:col-start-${responsive.lg.col}`); classes.push(`lg:col-span-${responsive.lg.colSpan}`); return classes.join(' '); } ``` **주의**: Tailwind는 빌드 타임에 클래스를 결정하므로, 동적 클래스 생성 시 safelist 설정 필요 ```javascript // tailwind.config.js module.exports = { safelist: [ // 그리드 컬럼 시작 { pattern: /col-start-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, { pattern: /md:col-start-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, { pattern: /lg:col-start-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, // 그리드 컬럼 스팬 { pattern: /col-span-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, { pattern: /md:col-span-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, { pattern: /lg:col-span-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, ], } ``` --- ## 5. 렌더링 컴포넌트 수정 ### 5.1 ResponsiveGridLayout 컴포넌트 ```tsx // frontend/lib/registry/layouts/responsive-grid/ResponsiveGridLayout.tsx import { cn } from "@/lib/utils"; import { generateGridClasses } from "@/lib/utils/gridClassGenerator"; interface ResponsiveGridLayoutProps { layout: LayoutData; isDesignMode: boolean; renderer: ComponentRenderer; } export function ResponsiveGridLayout({ layout, isDesignMode, renderer, }: ResponsiveGridLayoutProps) { const { gridSettings, components } = layout; return (
{components .sort((a, b) => (a.grid?.row || 0) - (b.grid?.row || 0)) .map((component) => { const gridClasses = generateGridClasses( component.grid, component.responsive ); return (
{renderer.renderChild(component)}
); })}
); } ``` ### 5.2 렌더링 결과 예시 **데스크톱 (lg: 1024px+)**: ``` ┌─────────────────────────────────────────────────────────┐ │ [분리] [저장] [수정] [삭제] │ ← 버튼들 오른쪽 정렬 ├─────────────────────────────────────────────────────────┤ │ │ │ 테이블 컴포넌트 │ │ │ └─────────────────────────────────────────────────────────┘ ``` **태블릿 (md: 768px ~ 1024px)**: ``` ┌───────────────────────────────┐ │ [분리] [저장] [수정] [삭제] │ ← 버튼들 2개씩 ├───────────────────────────────┤ │ │ │ 테이블 컴포넌트 │ │ │ └───────────────────────────────┘ ``` **모바일 (sm: < 768px)**: ``` ┌─────────────────┐ │ [분리] │ │ [저장] │ │ [수정] │ ← 세로 스택 │ [삭제] │ ├─────────────────┤ │ 테이블 컴포넌트 │ │ (스크롤) │ └─────────────────┘ ``` --- ## 6. 마이그레이션 계획 ### 6.1 데이터 마이그레이션 스크립트 ```sql -- 기존 데이터를 V3 구조로 변환하는 함수 CREATE OR REPLACE FUNCTION migrate_layout_to_v3(layout_data JSONB) RETURNS JSONB AS $$ DECLARE result JSONB; component JSONB; new_components JSONB := '[]'::JSONB; grid_col INT; grid_row INT; col_span INT; row_span INT; BEGIN -- 각 컴포넌트 변환 FOR component IN SELECT * FROM jsonb_array_elements(layout_data->'components') LOOP -- 픽셀 → 그리드 변환 (160px = 1컬럼, 80px = 1행) grid_col := GREATEST(1, LEAST(12, ROUND((component->'position'->>'x')::NUMERIC / 160) + 1)); grid_row := GREATEST(1, ROUND((component->'position'->>'y')::NUMERIC / 80) + 1); col_span := GREATEST(1, LEAST(13 - grid_col, ROUND((component->'size'->>'width')::NUMERIC / 160))); row_span := GREATEST(1, ROUND((component->'size'->>'height')::NUMERIC / 80)); -- 새 컴포넌트 구조 생성 component := component || jsonb_build_object( 'grid', jsonb_build_object( 'col', grid_col, 'row', grid_row, 'colSpan', col_span, 'rowSpan', row_span ), 'responsive', jsonb_build_object( 'sm', jsonb_build_object('col', 1, 'colSpan', 12), 'md', jsonb_build_object('col', GREATEST(1, ROUND(grid_col / 2.0)), 'colSpan', LEAST(col_span * 2, 12)), 'lg', jsonb_build_object('col', grid_col, 'colSpan', col_span) ) ); -- position, size 필드 제거 (선택사항 - 호환성 위해 유지 가능) -- component := component - 'position' - 'size'; new_components := new_components || component; END LOOP; -- 결과 생성 result := jsonb_build_object( 'version', '3.0', 'layoutMode', 'grid', 'components', new_components, 'gridSettings', COALESCE(layout_data->'gridSettings', '{"columns": 12, "rowHeight": 80, "gap": 16}'::JSONB) ); RETURN result; END; $$ LANGUAGE plpgsql; -- 마이그레이션 실행 UPDATE screen_layouts_v2 SET layout_data = migrate_layout_to_v3(layout_data) WHERE (layout_data->>'version') = '2.0'; ``` ### 6.2 백워드 호환성 V2 ↔ V3 호환을 위한 변환 레이어: ```typescript // frontend/lib/utils/layoutVersionConverter.ts export function normalizeLayout(layout: any): NormalizedLayout { const version = layout.version || "2.0"; if (version === "2.0") { // V2 → V3 변환 (렌더링 시) return { ...layout, version: "3.0", layoutMode: "grid", components: layout.components.map((comp: any) => ({ ...comp, grid: pixelToGrid(comp.position, comp.size), responsive: getDefaultResponsive(pixelToGrid(comp.position, comp.size)), })), }; } return layout; // V3는 그대로 } ``` --- ## 7. 디자인 모드 수정 ### 7.1 그리드 편집 UI 디자인 모드에서 그리드 셀 선택 방식 추가: ```tsx // 기존: 픽셀 좌표 입력 updatePosition({ x })} /> // 변경: 그리드 셀 선택
{Array.from({ length: 12 }).map((_, col) => (
setGridCol(col + 1)} /> ))}
``` ### 7.2 반응형 미리보기 ```tsx // 화면 크기 미리보기 버튼
// 미리보기 컨테이너
``` --- ## 8. 작업 목록 ### Phase 1: 핵심 유틸리티 (1일) | 작업 | 파일 | 상태 | |------|------|------| | 그리드 변환 함수 | `lib/utils/gridConverter.ts` | ⬜ | | 클래스 생성 함수 | `lib/utils/gridClassGenerator.ts` | ⬜ | | Tailwind safelist 설정 | `tailwind.config.js` | ⬜ | ### Phase 2: 렌더링 수정 (1일) | 작업 | 파일 | 상태 | |------|------|------| | ResponsiveGridLayout 생성 | `lib/registry/layouts/responsive-grid/` | ⬜ | | 레이아웃 버전 분기 처리 | `lib/registry/DynamicComponentRenderer.tsx` | ⬜ | ### Phase 3: 저장 로직 수정 (1일) | 작업 | 파일 | 상태 | |------|------|------| | 저장 시 그리드 변환 | `components/screen/ScreenDesigner.tsx` | ⬜ | | V3 레이아웃 변환기 | `lib/utils/layoutV3Converter.ts` | ⬜ | ### Phase 4: 디자인 모드 UI (1일) | 작업 | 파일 | 상태 | |------|------|------| | 그리드 셀 편집 UI | `components/screen/panels/V2PropertiesPanel.tsx` | ⬜ | | 반응형 미리보기 | `components/screen/ScreenDesigner.tsx` | ⬜ | ### Phase 5: 마이그레이션 (0.5일) | 작업 | 파일 | 상태 | |------|------|------| | 마이그레이션 스크립트 | `db/migrations/xxx_migrate_to_v3.sql` | ⬜ | | 백워드 호환성 테스트 | - | ⬜ | --- ## 9. 예상 일정 | 단계 | 기간 | 완료 기준 | |------|------|-----------| | Phase 1 | 1일 | 유틸리티 함수 테스트 통과 | | Phase 2 | 1일 | 그리드 렌더링 정상 동작 | | Phase 3 | 1일 | 저장/로드 정상 동작 | | Phase 4 | 1일 | 디자인 모드 UI 완성 | | Phase 5 | 0.5일 | 기존 데이터 마이그레이션 완료 | | 테스트 | 0.5일 | 모든 화면 반응형 테스트 | | **합계** | **5일** | | --- ## 10. 리스크 및 대응 | 리스크 | 영향 | 대응 방안 | |--------|------|-----------| | 기존 레이아웃 깨짐 | 높음 | position/size 필드 유지하여 폴백 | | Tailwind 동적 클래스 | 중간 | safelist로 모든 클래스 사전 정의 | | 디자인 모드 혼란 | 낮음 | 그리드 가이드라인 시각화 | --- ## 11. 참고 자료 - [COMPONENT_LAYOUT_V2_ARCHITECTURE.md](./COMPONENT_LAYOUT_V2_ARCHITECTURE.md) - V2 아키텍처 - [Tailwind CSS Grid](https://tailwindcss.com/docs/grid-template-columns) - 그리드 시스템 - [shadcn/ui](https://ui.shadcn.com/) - 컴포넌트 라이브러리