671 lines
20 KiB
Markdown
671 lines
20 KiB
Markdown
# 반응형 그리드 시스템 아키텍처
|
|
|
|
> 최종 업데이트: 2026-01-30
|
|
|
|
---
|
|
|
|
## 1. 개요
|
|
|
|
### 1.1 현재 문제
|
|
|
|
**컴포넌트 위치/크기가 픽셀 단위로 고정되어 반응형 미지원**
|
|
|
|
```json
|
|
// 현재 DB 저장 방식 (screen_layouts_v2.layout_data)
|
|
{
|
|
"position": { "x": 1753, "y": 88 },
|
|
"size": { "width": 158, "height": 40 }
|
|
}
|
|
```
|
|
|
|
| 화면 크기 | 결과 |
|
|
|-----------|------|
|
|
| 1920px (디자인 기준) | 정상 |
|
|
| 1280px (노트북) | 오른쪽 버튼 잘림 |
|
|
| 768px (태블릿) | 레이아웃 완전히 깨짐 |
|
|
| 375px (모바일) | 사용 불가 |
|
|
|
|
### 1.2 목표
|
|
|
|
| 목표 | 설명 |
|
|
|------|------|
|
|
| PC 대응 | 1280px ~ 1920px |
|
|
| 태블릿 대응 | 768px ~ 1024px |
|
|
| 모바일 대응 | 320px ~ 767px |
|
|
|
|
### 1.3 해결 방향
|
|
|
|
```
|
|
현재: 픽셀 좌표 → position: absolute → 고정 레이아웃
|
|
변경: 그리드 셀 번호 → CSS Grid + ResizeObserver → 반응형 레이아웃
|
|
```
|
|
|
|
---
|
|
|
|
## 2. 현재 시스템 분석
|
|
|
|
### 2.1 데이터 현황
|
|
|
|
```
|
|
총 레이아웃: 1,250개
|
|
총 컴포넌트: 5,236개
|
|
회사 수: 14개
|
|
테이블 크기: 약 3MB
|
|
```
|
|
|
|
### 2.2 컴포넌트 타입별 분포
|
|
|
|
| 컴포넌트 | 수량 | shadcn 사용 |
|
|
|----------|------|-------------|
|
|
| v2-input | 1,914 | ✅ `@/components/ui/input` |
|
|
| v2-button-primary | 1,549 | ✅ `@/components/ui/button` |
|
|
| v2-table-search-widget | 355 | ✅ shadcn 기반 |
|
|
| v2-select | 327 | ✅ `@/components/ui/select` |
|
|
| v2-table-list | 285 | ✅ `@/components/ui/table` |
|
|
| v2-media | 181 | ✅ shadcn 기반 |
|
|
| v2-date | 132 | ✅ `@/components/ui/calendar` |
|
|
| **v2-split-panel-layout** | **131** | ✅ shadcn 기반 (**반응형 필요**) |
|
|
| v2-tabs-widget | 75 | ✅ shadcn 기반 |
|
|
| 기타 | 287 | ✅ shadcn 기반 |
|
|
| **합계** | **5,236** | **전부 shadcn** |
|
|
|
|
### 2.3 현재 렌더링 방식
|
|
|
|
```tsx
|
|
// frontend/lib/registry/layouts/flexbox/FlexboxLayout.tsx (라인 234-248)
|
|
{components.map((child) => (
|
|
<div
|
|
style={{
|
|
position: "absolute", // 절대 위치
|
|
left: child.position.x, // 픽셀 고정
|
|
top: child.position.y, // 픽셀 고정
|
|
width: child.size.width, // 픽셀 고정
|
|
height: child.size.height, // 픽셀 고정
|
|
}}
|
|
>
|
|
{renderer.renderChild(child)}
|
|
</div>
|
|
))}
|
|
```
|
|
|
|
### 2.4 핵심 발견
|
|
|
|
```
|
|
✅ 이미 있는 것:
|
|
- 12컬럼 그리드 설정 (gridSettings.columns: 12)
|
|
- 그리드 스냅 기능 (snapToGrid: true)
|
|
- shadcn/ui 기반 컴포넌트 (전체)
|
|
|
|
❌ 없는 것:
|
|
- 그리드 셀 번호 저장 (현재 픽셀 저장)
|
|
- 반응형 브레이크포인트 설정
|
|
- CSS Grid 기반 렌더링
|
|
- 분할 패널 반응형 처리
|
|
```
|
|
|
|
---
|
|
|
|
## 3. 기술 결정
|
|
|
|
### 3.1 왜 Tailwind 동적 클래스가 아닌 CSS Grid + Inline Style인가?
|
|
|
|
**Tailwind 동적 클래스의 한계**:
|
|
```tsx
|
|
// ❌ 이건 안 됨 - Tailwind가 빌드 타임에 인식 못함
|
|
className={`col-start-${col} md:col-start-${mdCol}`}
|
|
|
|
// ✅ 이것만 됨 - 정적 클래스
|
|
className="col-start-1 md:col-start-3"
|
|
```
|
|
|
|
Tailwind는 **빌드 타임**에 클래스를 스캔하므로, 런타임에 동적으로 생성되는 클래스는 인식하지 못합니다.
|
|
|
|
**해결책: CSS Grid + Inline Style + ResizeObserver**:
|
|
```tsx
|
|
// ✅ 올바른 방법
|
|
<div style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(12, 1fr)',
|
|
}}>
|
|
<div style={{
|
|
gridColumn: `${col} / span ${colSpan}`, // 동적 값 가능
|
|
}}>
|
|
{component}
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
### 3.2 역할 분담
|
|
|
|
| 영역 | 기술 | 설명 |
|
|
|------|------|------|
|
|
| **UI 컴포넌트** | shadcn/ui | 버튼, 인풋, 테이블 등 (이미 적용됨) |
|
|
| **레이아웃 배치** | CSS Grid + Inline Style | 컴포넌트 위치, 크기, 반응형 |
|
|
| **반응형 감지** | ResizeObserver | 화면 크기 감지 및 브레이크포인트 변경 |
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ ResponsiveGridLayout (CSS Grid) │
|
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
│ │ shadcn │ │ shadcn │ │ shadcn │ │
|
|
│ │ Button │ │ Input │ │ Select │ │
|
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
│ ┌─────────────────────────────────────────────┐ │
|
|
│ │ shadcn Table │ │
|
|
│ └─────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 4. 데이터 구조 변경
|
|
|
|
### 4.1 현재 구조 (V2)
|
|
|
|
```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": { ... }
|
|
}]
|
|
}
|
|
```
|
|
|
|
### 4.2 변경 후 구조 (V2 + 그리드)
|
|
|
|
```json
|
|
{
|
|
"version": "2.0",
|
|
"layoutMode": "grid",
|
|
"components": [{
|
|
"id": "comp_xxx",
|
|
"url": "@/lib/registry/components/v2-button-primary",
|
|
"position": { "x": 1753, "y": 88, "z": 1 },
|
|
"size": { "width": 158, "height": 40 },
|
|
"grid": {
|
|
"col": 11,
|
|
"row": 2,
|
|
"colSpan": 1,
|
|
"rowSpan": 1
|
|
},
|
|
"responsive": {
|
|
"sm": { "col": 1, "colSpan": 12 },
|
|
"md": { "col": 7, "colSpan": 6 },
|
|
"lg": { "col": 11, "colSpan": 1 }
|
|
},
|
|
"overrides": { ... }
|
|
}],
|
|
"gridSettings": {
|
|
"columns": 12,
|
|
"rowHeight": 80,
|
|
"gap": 16
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4.3 필드 설명
|
|
|
|
| 필드 | 타입 | 설명 |
|
|
|------|------|------|
|
|
| `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) 설정 |
|
|
|
|
### 4.4 호환성
|
|
|
|
- `position`, `size` 필드는 유지 (디자인 모드 + 폴백용)
|
|
- `layoutMode`가 없으면 기존 방식(absolute) 사용
|
|
- 마이그레이션 후에도 기존 화면 정상 동작
|
|
|
|
---
|
|
|
|
## 5. 구현 상세
|
|
|
|
### 5.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;
|
|
|
|
/**
|
|
* 픽셀 좌표를 그리드 셀 번호로 변환
|
|
*/
|
|
export function pixelToGrid(
|
|
position: { x: number; y: number },
|
|
size: { width: number; height: number }
|
|
): GridPosition {
|
|
return {
|
|
col: Math.max(1, Math.min(12, Math.round(position.x / COLUMN_WIDTH) + 1)),
|
|
row: Math.max(1, Math.round(position.y / ROW_HEIGHT) + 1),
|
|
colSpan: Math.max(1, Math.round(size.width / COLUMN_WIDTH)),
|
|
rowSpan: Math.max(1, Math.round(size.height / ROW_HEIGHT)),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 기본 반응형 설정 생성
|
|
*/
|
|
export function getDefaultResponsive(grid: GridPosition): ResponsiveConfig {
|
|
return {
|
|
sm: { col: 1, colSpan: 12 }, // 모바일: 전체 너비
|
|
md: {
|
|
col: Math.max(1, Math.round(grid.col / 2)),
|
|
colSpan: Math.min(grid.colSpan * 2, 12)
|
|
}, // 태블릿: 2배 확장
|
|
lg: { col: grid.col, colSpan: grid.colSpan }, // 데스크톱: 원본
|
|
};
|
|
}
|
|
```
|
|
|
|
### 5.2 반응형 그리드 레이아웃 컴포넌트
|
|
|
|
```tsx
|
|
// frontend/lib/registry/layouts/responsive-grid/ResponsiveGridLayout.tsx
|
|
|
|
import React, { useRef, useState, useEffect } from "react";
|
|
|
|
type Breakpoint = "sm" | "md" | "lg";
|
|
|
|
interface ResponsiveGridLayoutProps {
|
|
layout: LayoutData;
|
|
isDesignMode: boolean;
|
|
renderer: ComponentRenderer;
|
|
}
|
|
|
|
export function ResponsiveGridLayout({
|
|
layout,
|
|
isDesignMode,
|
|
renderer,
|
|
}: ResponsiveGridLayoutProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [breakpoint, setBreakpoint] = useState<Breakpoint>("lg");
|
|
|
|
// 화면 크기 감지
|
|
useEffect(() => {
|
|
if (!containerRef.current) return;
|
|
|
|
const observer = new ResizeObserver((entries) => {
|
|
const width = entries[0].contentRect.width;
|
|
if (width < 768) setBreakpoint("sm");
|
|
else if (width < 1024) setBreakpoint("md");
|
|
else setBreakpoint("lg");
|
|
});
|
|
|
|
observer.observe(containerRef.current);
|
|
return () => observer.disconnect();
|
|
}, []);
|
|
|
|
const gridSettings = layout.gridSettings || { columns: 12, rowHeight: 80, gap: 16 };
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns: `repeat(${gridSettings.columns}, 1fr)`,
|
|
gridAutoRows: `${gridSettings.rowHeight}px`,
|
|
gap: `${gridSettings.gap}px`,
|
|
minHeight: isDesignMode ? "600px" : "auto",
|
|
}}
|
|
>
|
|
{layout.components
|
|
.sort((a, b) => (a.grid?.row || 0) - (b.grid?.row || 0))
|
|
.map((component) => {
|
|
// 반응형 설정 가져오기
|
|
const gridConfig = component.responsive?.[breakpoint] || component.grid;
|
|
const { col, colSpan } = gridConfig;
|
|
const rowSpan = component.grid?.rowSpan || 1;
|
|
|
|
return (
|
|
<div
|
|
key={component.id}
|
|
style={{
|
|
gridColumn: `${col} / span ${colSpan}`,
|
|
gridRow: `span ${rowSpan}`,
|
|
}}
|
|
>
|
|
{renderer.renderChild(component)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 5.3 브레이크포인트 훅
|
|
|
|
```typescript
|
|
// frontend/lib/registry/layouts/responsive-grid/useBreakpoint.ts
|
|
|
|
import { useState, useEffect, RefObject } from "react";
|
|
|
|
type Breakpoint = "sm" | "md" | "lg";
|
|
|
|
export function useBreakpoint(containerRef: RefObject<HTMLElement>): Breakpoint {
|
|
const [breakpoint, setBreakpoint] = useState<Breakpoint>("lg");
|
|
|
|
useEffect(() => {
|
|
if (!containerRef.current) return;
|
|
|
|
const observer = new ResizeObserver((entries) => {
|
|
const width = entries[0].contentRect.width;
|
|
if (width < 768) setBreakpoint("sm");
|
|
else if (width < 1024) setBreakpoint("md");
|
|
else setBreakpoint("lg");
|
|
});
|
|
|
|
observer.observe(containerRef.current);
|
|
return () => observer.disconnect();
|
|
}, [containerRef]);
|
|
|
|
return breakpoint;
|
|
}
|
|
```
|
|
|
|
### 5.4 분할 패널 반응형 수정
|
|
|
|
```tsx
|
|
// frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx
|
|
|
|
// 추가할 코드
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [isMobile, setIsMobile] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!containerRef.current) return;
|
|
|
|
const observer = new ResizeObserver((entries) => {
|
|
const width = entries[0].contentRect.width;
|
|
setIsMobile(width < 768);
|
|
});
|
|
|
|
observer.observe(containerRef.current);
|
|
return () => observer.disconnect();
|
|
}, []);
|
|
|
|
// 렌더링 부분 수정
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={cn(
|
|
"flex h-full",
|
|
isMobile ? "flex-col" : "flex-row" // 모바일: 상하, 데스크톱: 좌우
|
|
)}
|
|
>
|
|
<div style={{
|
|
width: isMobile ? "100%" : `${leftWidth}%`,
|
|
minHeight: isMobile ? "300px" : "auto"
|
|
}}>
|
|
{/* 좌측/상단 패널 */}
|
|
</div>
|
|
<div style={{
|
|
width: isMobile ? "100%" : `${100 - leftWidth}%`,
|
|
minHeight: isMobile ? "300px" : "auto"
|
|
}}>
|
|
{/* 우측/하단 패널 */}
|
|
</div>
|
|
</div>
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 6. 렌더링 분기 처리
|
|
|
|
```typescript
|
|
// frontend/lib/registry/DynamicComponentRenderer.tsx
|
|
|
|
function renderLayout(layout: LayoutData) {
|
|
// layoutMode에 따라 분기
|
|
if (layout.layoutMode === "grid") {
|
|
return <ResponsiveGridLayout layout={layout} renderer={this} />;
|
|
}
|
|
|
|
// 기존 방식 (폴백)
|
|
return <FlexboxLayout layout={layout} renderer={this} />;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 마이그레이션
|
|
|
|
### 7.1 백업
|
|
|
|
```sql
|
|
-- 마이그레이션 전 백업
|
|
CREATE TABLE screen_layouts_v2_backup_20260130 AS
|
|
SELECT * FROM screen_layouts_v2;
|
|
```
|
|
|
|
### 7.2 마이그레이션 스크립트
|
|
|
|
```sql
|
|
-- grid, responsive 필드 추가
|
|
UPDATE screen_layouts_v2
|
|
SET layout_data = (
|
|
SELECT jsonb_set(
|
|
jsonb_set(
|
|
layout_data,
|
|
'{layoutMode}',
|
|
'"grid"'
|
|
),
|
|
'{components}',
|
|
(
|
|
SELECT jsonb_agg(
|
|
comp || jsonb_build_object(
|
|
'grid', jsonb_build_object(
|
|
'col', GREATEST(1, LEAST(12, ROUND((comp->'position'->>'x')::NUMERIC / 160) + 1)),
|
|
'row', GREATEST(1, ROUND((comp->'position'->>'y')::NUMERIC / 80) + 1),
|
|
'colSpan', GREATEST(1, ROUND((comp->'size'->>'width')::NUMERIC / 160)),
|
|
'rowSpan', GREATEST(1, ROUND((comp->'size'->>'height')::NUMERIC / 80))
|
|
),
|
|
'responsive', jsonb_build_object(
|
|
'sm', jsonb_build_object('col', 1, 'colSpan', 12),
|
|
'md', jsonb_build_object(
|
|
'col', GREATEST(1, ROUND((ROUND((comp->'position'->>'x')::NUMERIC / 160) + 1) / 2.0)),
|
|
'colSpan', LEAST(ROUND((comp->'size'->>'width')::NUMERIC / 160) * 2, 12)
|
|
),
|
|
'lg', jsonb_build_object(
|
|
'col', GREATEST(1, LEAST(12, ROUND((comp->'position'->>'x')::NUMERIC / 160) + 1)),
|
|
'colSpan', GREATEST(1, ROUND((comp->'size'->>'width')::NUMERIC / 160))
|
|
)
|
|
)
|
|
)
|
|
)
|
|
FROM jsonb_array_elements(layout_data->'components') as comp
|
|
)
|
|
)
|
|
);
|
|
```
|
|
|
|
### 7.3 롤백
|
|
|
|
```sql
|
|
-- 문제 발생 시 롤백
|
|
DROP TABLE screen_layouts_v2;
|
|
ALTER TABLE screen_layouts_v2_backup_20260130 RENAME TO screen_layouts_v2;
|
|
```
|
|
|
|
---
|
|
|
|
## 8. 동작 흐름
|
|
|
|
### 8.1 데스크톱 (> 1024px)
|
|
|
|
```
|
|
┌────────────────────────────────────────────────────────────┐
|
|
│ 1 2 3 4 5 6 7 8 9 10 │ 11 12 │ │
|
|
│ │ [버튼] │ │
|
|
├────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ 테이블 (12컬럼) │
|
|
│ │
|
|
└────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 8.2 태블릿 (768px ~ 1024px)
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ 1 2 3 4 5 6 │ 7 8 9 10 11 12 │
|
|
│ │ [버튼] │
|
|
├─────────────────────────────────────┤
|
|
│ │
|
|
│ 테이블 (12컬럼) │
|
|
│ │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
### 8.3 모바일 (< 768px)
|
|
|
|
```
|
|
┌──────────────────┐
|
|
│ [버튼] │ ← 12컬럼 (전체 너비)
|
|
├──────────────────┤
|
|
│ │
|
|
│ 테이블 (스크롤) │ ← 12컬럼 (전체 너비)
|
|
│ │
|
|
└──────────────────┘
|
|
```
|
|
|
|
### 8.4 분할 패널 (반응형)
|
|
|
|
**데스크톱**:
|
|
```
|
|
┌─────────────────────────┬─────────────────────────┐
|
|
│ 좌측 패널 (60%) │ 우측 패널 (40%) │
|
|
└─────────────────────────┴─────────────────────────┘
|
|
```
|
|
|
|
**모바일**:
|
|
```
|
|
┌─────────────────────────┐
|
|
│ 상단 패널 (이전 좌측) │
|
|
├─────────────────────────┤
|
|
│ 하단 패널 (이전 우측) │
|
|
└─────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 9. 수정 파일 목록
|
|
|
|
### 9.1 새로 생성
|
|
|
|
| 파일 | 설명 |
|
|
|------|------|
|
|
| `lib/utils/gridConverter.ts` | 픽셀 → 그리드 변환 유틸리티 |
|
|
| `lib/registry/layouts/responsive-grid/ResponsiveGridLayout.tsx` | CSS Grid 레이아웃 |
|
|
| `lib/registry/layouts/responsive-grid/useBreakpoint.ts` | ResizeObserver 훅 |
|
|
| `lib/registry/layouts/responsive-grid/index.ts` | 모듈 export |
|
|
|
|
### 9.2 수정
|
|
|
|
| 파일 | 수정 내용 |
|
|
|------|-----------|
|
|
| `lib/registry/DynamicComponentRenderer.tsx` | layoutMode 분기 추가 |
|
|
| `components/screen/ScreenDesigner.tsx` | 저장 시 grid/responsive 생성 |
|
|
| `v2-split-panel-layout/SplitPanelLayoutComponent.tsx` | 반응형 처리 추가 |
|
|
|
|
### 9.3 수정 없음
|
|
|
|
| 파일 | 이유 |
|
|
|------|------|
|
|
| `v2-input/*` | 레이아웃과 무관 (shadcn 그대로) |
|
|
| `v2-button-primary/*` | 레이아웃과 무관 (shadcn 그대로) |
|
|
| `v2-table-list/*` | 레이아웃과 무관 (shadcn 그대로) |
|
|
| `v2-select/*` | 레이아웃과 무관 (shadcn 그대로) |
|
|
| **...모든 v2 컴포넌트** | **수정 불필요** |
|
|
|
|
---
|
|
|
|
## 10. 작업 일정
|
|
|
|
| Phase | 작업 | 파일 | 시간 |
|
|
|-------|------|------|------|
|
|
| **1** | 그리드 변환 유틸리티 | `gridConverter.ts` | 2시간 |
|
|
| **1** | 브레이크포인트 훅 | `useBreakpoint.ts` | 1시간 |
|
|
| **2** | ResponsiveGridLayout | `ResponsiveGridLayout.tsx` | 4시간 |
|
|
| **2** | 렌더링 분기 처리 | `DynamicComponentRenderer.tsx` | 1시간 |
|
|
| **3** | 저장 로직 수정 | `ScreenDesigner.tsx` | 2시간 |
|
|
| **3** | 분할 패널 반응형 | `SplitPanelLayoutComponent.tsx` | 3시간 |
|
|
| **4** | 마이그레이션 스크립트 | SQL | 2시간 |
|
|
| **4** | 마이그레이션 실행 | - | 1시간 |
|
|
| **5** | 테스트 및 버그 수정 | - | 4시간 |
|
|
| | **합계** | | **약 2.5일** |
|
|
|
|
---
|
|
|
|
## 11. 체크리스트
|
|
|
|
### 개발 전
|
|
|
|
- [ ] screen_layouts_v2 백업 완료
|
|
- [ ] 개발 환경에서 테스트 데이터 준비
|
|
|
|
### Phase 1: 유틸리티
|
|
|
|
- [ ] `gridConverter.ts` 생성
|
|
- [ ] `useBreakpoint.ts` 생성
|
|
- [ ] 단위 테스트 작성
|
|
|
|
### Phase 2: 레이아웃
|
|
|
|
- [ ] `ResponsiveGridLayout.tsx` 생성
|
|
- [ ] `DynamicComponentRenderer.tsx` 분기 추가
|
|
- [ ] 기존 화면 정상 동작 확인
|
|
|
|
### Phase 3: 저장/수정
|
|
|
|
- [ ] `ScreenDesigner.tsx` 저장 로직 수정
|
|
- [ ] `SplitPanelLayoutComponent.tsx` 반응형 추가
|
|
- [ ] 디자인 모드 테스트
|
|
|
|
### Phase 4: 마이그레이션
|
|
|
|
- [ ] 마이그레이션 스크립트 테스트 (개발 DB)
|
|
- [ ] 운영 DB 백업
|
|
- [ ] 마이그레이션 실행
|
|
- [ ] 검증
|
|
|
|
### Phase 5: 테스트
|
|
|
|
- [ ] PC (1920px, 1280px) 테스트
|
|
- [ ] 태블릿 (768px, 1024px) 테스트
|
|
- [ ] 모바일 (375px, 414px) 테스트
|
|
- [ ] 분할 패널 화면 테스트
|
|
|
|
---
|
|
|
|
## 12. 리스크 및 대응
|
|
|
|
| 리스크 | 영향 | 대응 |
|
|
|--------|------|------|
|
|
| 마이그레이션 실패 | 높음 | 백업 테이블에서 즉시 롤백 |
|
|
| 기존 화면 깨짐 | 중간 | `layoutMode` 없으면 기존 방식 사용 (폴백) |
|
|
| 디자인 모드 혼란 | 낮음 | position/size 필드 유지 |
|
|
|
|
---
|
|
|
|
## 13. 참고
|
|
|
|
- [COMPONENT_LAYOUT_V2_ARCHITECTURE.md](./COMPONENT_LAYOUT_V2_ARCHITECTURE.md) - V2 아키텍처
|
|
- [CSS Grid Layout - MDN](https://developer.mozilla.org/ko/docs/Web/CSS/CSS_Grid_Layout)
|
|
- [ResizeObserver - MDN](https://developer.mozilla.org/ko/docs/Web/API/ResizeObserver)
|
|
- [shadcn/ui](https://ui.shadcn.com/) - 컴포넌트 라이브러리
|