481 lines
13 KiB
Markdown
481 lines
13 KiB
Markdown
|
|
# POP 그리드 시스템 도입 계획
|
||
|
|
|
||
|
|
> 작성일: 2026-02-05
|
||
|
|
> 상태: 계획 승인, 구현 대기
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 개요
|
||
|
|
|
||
|
|
### 목표
|
||
|
|
현재 Flexbox 기반 v4 시스템을 **CSS Grid 기반 v5 시스템**으로 전환하여
|
||
|
|
4~14인치 화면에서 일관된 배치와 예측 가능한 반응형 레이아웃 구현
|
||
|
|
|
||
|
|
### 핵심 변경점
|
||
|
|
|
||
|
|
| 항목 | v4 (현재) | v5 (그리드) |
|
||
|
|
|------|----------|-------------|
|
||
|
|
| 배치 방식 | Flexbox 흐름 | **Grid 위치 지정** |
|
||
|
|
| 크기 단위 | 픽셀 (200px) | **칸 (col, row)** |
|
||
|
|
| 위치 지정 | 순서대로 자동 | **열/행 좌표** |
|
||
|
|
| 줄바꿈 | 자동 (넘치면) | **명시적 (row 지정)** |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 구조
|
||
|
|
|
||
|
|
```
|
||
|
|
[Phase 5.1] [Phase 5.2] [Phase 5.3] [Phase 5.4]
|
||
|
|
그리드 타입 정의 → 그리드 렌더러 → 디자이너 UI → 반응형 자동화
|
||
|
|
1주 1주 1~2주 1주
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 5.1: 그리드 타입 정의
|
||
|
|
|
||
|
|
### 목표
|
||
|
|
v5 레이아웃 데이터 구조 설계
|
||
|
|
|
||
|
|
### 작업 항목
|
||
|
|
|
||
|
|
- [ ] `PopLayoutDataV5` 인터페이스 정의
|
||
|
|
- [ ] `PopGridConfig` 인터페이스 (그리드 설정)
|
||
|
|
- [ ] `PopComponentPositionV5` 인터페이스 (위치: col, row, colSpan, rowSpan)
|
||
|
|
- [ ] `PopSizeConstraintV5` 인터페이스 (칸 기반 크기)
|
||
|
|
- [ ] 브레이크포인트 상수 정의
|
||
|
|
- [ ] `createEmptyPopLayoutV5()` 생성 함수
|
||
|
|
- [ ] `isV5Layout()` 타입 가드
|
||
|
|
|
||
|
|
### 데이터 구조 설계
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// v5 레이아웃
|
||
|
|
interface PopLayoutDataV5 {
|
||
|
|
version: "pop-5.0";
|
||
|
|
|
||
|
|
// 그리드 설정
|
||
|
|
gridConfig: PopGridConfig;
|
||
|
|
|
||
|
|
// 컴포넌트 목록
|
||
|
|
components: Record<string, PopComponentDefinitionV5>;
|
||
|
|
|
||
|
|
// 모드별 오버라이드
|
||
|
|
overrides?: {
|
||
|
|
mobile_portrait?: PopModeOverrideV5;
|
||
|
|
mobile_landscape?: PopModeOverrideV5;
|
||
|
|
tablet_portrait?: PopModeOverrideV5;
|
||
|
|
};
|
||
|
|
|
||
|
|
// 기존 호환
|
||
|
|
dataFlow: PopDataFlow;
|
||
|
|
settings: PopGlobalSettingsV5;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 그리드 설정
|
||
|
|
interface PopGridConfig {
|
||
|
|
// 모드별 칸 수
|
||
|
|
columns: {
|
||
|
|
tablet_landscape: 12; // 기본 (10~14인치)
|
||
|
|
tablet_portrait: 8; // 8~10인치 세로
|
||
|
|
mobile_landscape: 6; // 6~8인치 가로
|
||
|
|
mobile_portrait: 4; // 4~6인치 세로
|
||
|
|
};
|
||
|
|
|
||
|
|
// 행 높이 (px) - 1행의 기본 높이
|
||
|
|
rowHeight: number; // 기본 48px
|
||
|
|
|
||
|
|
// 간격
|
||
|
|
gap: number; // 기본 8px
|
||
|
|
padding: number; // 기본 16px
|
||
|
|
}
|
||
|
|
|
||
|
|
// 컴포넌트 정의
|
||
|
|
interface PopComponentDefinitionV5 {
|
||
|
|
id: string;
|
||
|
|
type: PopComponentType;
|
||
|
|
label?: string;
|
||
|
|
|
||
|
|
// 위치 (열/행 좌표) - 기본 모드(태블릿 가로) 기준
|
||
|
|
position: {
|
||
|
|
col: number; // 시작 열 (1부터)
|
||
|
|
row: number; // 시작 행 (1부터)
|
||
|
|
colSpan: number; // 차지할 열 수 (1~12)
|
||
|
|
rowSpan: number; // 차지할 행 수 (1~)
|
||
|
|
};
|
||
|
|
|
||
|
|
// 모드별 표시/숨김
|
||
|
|
visibility?: {
|
||
|
|
tablet_landscape?: boolean;
|
||
|
|
tablet_portrait?: boolean;
|
||
|
|
mobile_landscape?: boolean;
|
||
|
|
mobile_portrait?: boolean;
|
||
|
|
};
|
||
|
|
|
||
|
|
// 기존 속성
|
||
|
|
dataBinding?: PopDataBinding;
|
||
|
|
config?: PopComponentConfig;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 브레이크포인트 정의
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 브레이크포인트 상수
|
||
|
|
const GRID_BREAKPOINTS = {
|
||
|
|
// 4~6인치 모바일 세로
|
||
|
|
mobile_portrait: {
|
||
|
|
maxWidth: 599,
|
||
|
|
columns: 4,
|
||
|
|
rowHeight: 40,
|
||
|
|
gap: 8,
|
||
|
|
padding: 12,
|
||
|
|
},
|
||
|
|
|
||
|
|
// 6~8인치 모바일 가로 / 작은 태블릿
|
||
|
|
mobile_landscape: {
|
||
|
|
minWidth: 600,
|
||
|
|
maxWidth: 839,
|
||
|
|
columns: 6,
|
||
|
|
rowHeight: 44,
|
||
|
|
gap: 8,
|
||
|
|
padding: 16,
|
||
|
|
},
|
||
|
|
|
||
|
|
// 8~10인치 태블릿 세로
|
||
|
|
tablet_portrait: {
|
||
|
|
minWidth: 840,
|
||
|
|
maxWidth: 1023,
|
||
|
|
columns: 8,
|
||
|
|
rowHeight: 48,
|
||
|
|
gap: 12,
|
||
|
|
padding: 16,
|
||
|
|
},
|
||
|
|
|
||
|
|
// 10~14인치 태블릿 가로 (기본)
|
||
|
|
tablet_landscape: {
|
||
|
|
minWidth: 1024,
|
||
|
|
columns: 12,
|
||
|
|
rowHeight: 48,
|
||
|
|
gap: 16,
|
||
|
|
padding: 24,
|
||
|
|
},
|
||
|
|
} as const;
|
||
|
|
```
|
||
|
|
|
||
|
|
### 산출물
|
||
|
|
- `frontend/components/pop/designer/types/pop-layout-v5.ts`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 5.2: 그리드 렌더러
|
||
|
|
|
||
|
|
### 목표
|
||
|
|
CSS Grid 기반 렌더러 구현
|
||
|
|
|
||
|
|
### 작업 항목
|
||
|
|
|
||
|
|
- [ ] `PopGridRenderer.tsx` 생성
|
||
|
|
- [ ] CSS Grid 스타일 계산 로직
|
||
|
|
- [ ] 브레이크포인트 감지 및 칸 수 자동 변경
|
||
|
|
- [ ] 컴포넌트 위치 렌더링 (grid-column, grid-row)
|
||
|
|
- [ ] 모드별 자동 위치 재계산 (12칸→4칸 변환)
|
||
|
|
- [ ] visibility 처리
|
||
|
|
- [ ] 기존 PopFlexRenderer와 공존
|
||
|
|
|
||
|
|
### 렌더링 로직
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// CSS Grid 스타일 생성
|
||
|
|
function calculateGridStyle(config: PopGridConfig, mode: string): React.CSSProperties {
|
||
|
|
const columns = config.columns[mode];
|
||
|
|
const { rowHeight, gap, padding } = config;
|
||
|
|
|
||
|
|
return {
|
||
|
|
display: 'grid',
|
||
|
|
gridTemplateColumns: `repeat(${columns}, 1fr)`,
|
||
|
|
gridAutoRows: `${rowHeight}px`,
|
||
|
|
gap: `${gap}px`,
|
||
|
|
padding: `${padding}px`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// 컴포넌트 위치 스타일
|
||
|
|
function calculatePositionStyle(
|
||
|
|
position: PopComponentPositionV5['position'],
|
||
|
|
sourceColumns: number, // 원본 모드 칸 수 (12)
|
||
|
|
targetColumns: number // 현재 모드 칸 수 (4)
|
||
|
|
): React.CSSProperties {
|
||
|
|
// 12칸 → 4칸 변환 예시
|
||
|
|
// col: 7, colSpan: 3 → col: 3, colSpan: 1
|
||
|
|
const ratio = targetColumns / sourceColumns;
|
||
|
|
const newCol = Math.max(1, Math.ceil(position.col * ratio));
|
||
|
|
const newColSpan = Math.max(1, Math.round(position.colSpan * ratio));
|
||
|
|
|
||
|
|
return {
|
||
|
|
gridColumn: `${newCol} / span ${Math.min(newColSpan, targetColumns - newCol + 1)}`,
|
||
|
|
gridRow: `${position.row} / span ${position.rowSpan}`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 산출물
|
||
|
|
- `frontend/components/pop/designer/renderers/PopGridRenderer.tsx`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 5.3: 디자이너 UI
|
||
|
|
|
||
|
|
### 목표
|
||
|
|
그리드 기반 편집 UI 구현
|
||
|
|
|
||
|
|
### 작업 항목
|
||
|
|
|
||
|
|
- [ ] `PopCanvasV5.tsx` 생성 (그리드 캔버스)
|
||
|
|
- [ ] 그리드 배경 표시 (바둑판 모양)
|
||
|
|
- [ ] 컴포넌트 드래그 배치 (칸에 스냅)
|
||
|
|
- [ ] 컴포넌트 리사이즈 (칸 단위)
|
||
|
|
- [ ] 위치 편집 패널 (col, row, colSpan, rowSpan)
|
||
|
|
- [ ] 모드 전환 시 그리드 칸 수 변경 표시
|
||
|
|
- [ ] v4/v5 자동 판별 및 전환
|
||
|
|
|
||
|
|
### UI 구조
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────────┐
|
||
|
|
│ ← 목록 화면명 *변경됨 [↶][↷] 그리드 레이아웃 (v5) [저장] │
|
||
|
|
├─────────────────────────────────────────────────────────────────┤
|
||
|
|
│ 미리보기: [모바일↕ 4칸] [모바일↔ 6칸] [태블릿↕ 8칸] [태블릿↔ 12칸] │
|
||
|
|
├────────────┬────────────────────────────────────┬───────────────┤
|
||
|
|
│ │ 1 2 3 4 5 6 ... 12 │ │
|
||
|
|
│ 컴포넌트 │ ┌───────────┬───────────┐ │ 위치 │
|
||
|
|
│ │1│ A │ B │ │ 열: [1-12] │
|
||
|
|
│ 필드 │ ├───────────┴───────────┤ │ 행: [1-99] │
|
||
|
|
│ 버튼 │2│ C │ │ 너비: [1-12]│
|
||
|
|
│ 리스트 │ ├───────────┬───────────┤ │ 높이: [1-10]│
|
||
|
|
│ 인디케이터 │3│ D │ E │ │ │
|
||
|
|
│ ... │ └───────────┴───────────┘ │ 표시 설정 │
|
||
|
|
│ │ │ [x] 태블릿↔ │
|
||
|
|
│ │ (그리드 배경 표시) │ [x] 모바일↕ │
|
||
|
|
└────────────┴────────────────────────────────────┴───────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### 드래그 앤 드롭 로직
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 마우스 위치 → 그리드 좌표 변환
|
||
|
|
function mouseToGridPosition(
|
||
|
|
mouseX: number,
|
||
|
|
mouseY: number,
|
||
|
|
gridConfig: PopGridConfig,
|
||
|
|
canvasRect: DOMRect
|
||
|
|
): { col: number; row: number } {
|
||
|
|
const { columns, rowHeight, gap, padding } = gridConfig;
|
||
|
|
|
||
|
|
// 캔버스 내 상대 위치
|
||
|
|
const relX = mouseX - canvasRect.left - padding;
|
||
|
|
const relY = mouseY - canvasRect.top - padding;
|
||
|
|
|
||
|
|
// 칸 너비 계산
|
||
|
|
const totalGap = gap * (columns - 1);
|
||
|
|
const colWidth = (canvasRect.width - padding * 2 - totalGap) / columns;
|
||
|
|
|
||
|
|
// 그리드 좌표 계산
|
||
|
|
const col = Math.max(1, Math.min(columns, Math.floor(relX / (colWidth + gap)) + 1));
|
||
|
|
const row = Math.max(1, Math.floor(relY / (rowHeight + gap)) + 1);
|
||
|
|
|
||
|
|
return { col, row };
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 산출물
|
||
|
|
- `frontend/components/pop/designer/PopCanvasV5.tsx`
|
||
|
|
- `frontend/components/pop/designer/panels/ComponentEditorPanelV5.tsx`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 5.4: 반응형 자동화
|
||
|
|
|
||
|
|
### 목표
|
||
|
|
모드 전환 시 자동 레이아웃 조정
|
||
|
|
|
||
|
|
### 작업 항목
|
||
|
|
|
||
|
|
- [ ] 12칸 → 4칸 자동 변환 알고리즘
|
||
|
|
- [ ] 겹침 감지 및 자동 재배치
|
||
|
|
- [ ] 모드별 오버라이드 저장
|
||
|
|
- [ ] "자동 배치" vs "수동 고정" 선택
|
||
|
|
- [ ] 변환 미리보기
|
||
|
|
|
||
|
|
### 자동 변환 알고리즘
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 12칸 → 4칸 변환 전략
|
||
|
|
function convertLayoutToMode(
|
||
|
|
components: PopComponentDefinitionV5[],
|
||
|
|
sourceMode: 'tablet_landscape', // 12칸
|
||
|
|
targetMode: 'mobile_portrait' // 4칸
|
||
|
|
): PopComponentDefinitionV5[] {
|
||
|
|
const sourceColumns = 12;
|
||
|
|
const targetColumns = 4;
|
||
|
|
const ratio = targetColumns / sourceColumns; // 0.333
|
||
|
|
|
||
|
|
// 1. 각 컴포넌트 위치 변환
|
||
|
|
const converted = components.map(comp => {
|
||
|
|
const newCol = Math.max(1, Math.ceil(comp.position.col * ratio));
|
||
|
|
const newColSpan = Math.max(1, Math.round(comp.position.colSpan * ratio));
|
||
|
|
|
||
|
|
return {
|
||
|
|
...comp,
|
||
|
|
position: {
|
||
|
|
...comp.position,
|
||
|
|
col: newCol,
|
||
|
|
colSpan: Math.min(newColSpan, targetColumns),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
// 2. 겹침 감지 및 해결
|
||
|
|
return resolveOverlaps(converted, targetColumns);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 겹침 해결 (아래로 밀기)
|
||
|
|
function resolveOverlaps(
|
||
|
|
components: PopComponentDefinitionV5[],
|
||
|
|
columns: number
|
||
|
|
): PopComponentDefinitionV5[] {
|
||
|
|
// 행 단위로 그리드 점유 상태 추적
|
||
|
|
const grid: boolean[][] = [];
|
||
|
|
|
||
|
|
// row 순서대로 처리
|
||
|
|
const sorted = [...components].sort((a, b) =>
|
||
|
|
a.position.row - b.position.row || a.position.col - b.position.col
|
||
|
|
);
|
||
|
|
|
||
|
|
return sorted.map(comp => {
|
||
|
|
let { row, col, colSpan, rowSpan } = comp.position;
|
||
|
|
|
||
|
|
// 배치 가능한 위치 찾기
|
||
|
|
while (isOccupied(grid, row, col, colSpan, rowSpan, columns)) {
|
||
|
|
row++; // 아래로 이동
|
||
|
|
}
|
||
|
|
|
||
|
|
// 그리드에 표시
|
||
|
|
markOccupied(grid, row, col, colSpan, rowSpan);
|
||
|
|
|
||
|
|
return {
|
||
|
|
...comp,
|
||
|
|
position: { row, col, colSpan, rowSpan },
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 산출물
|
||
|
|
- `frontend/components/pop/designer/utils/gridLayoutUtils.ts`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 마이그레이션 전략
|
||
|
|
|
||
|
|
### v4 → v5 변환
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function migrateV4ToV5(layoutV4: PopLayoutDataV4): PopLayoutDataV5 {
|
||
|
|
const componentsList = Object.values(layoutV4.components);
|
||
|
|
|
||
|
|
// Flexbox 순서 → Grid 위치 변환
|
||
|
|
let currentRow = 1;
|
||
|
|
let currentCol = 1;
|
||
|
|
const columns = 12;
|
||
|
|
|
||
|
|
const componentsV5: Record<string, PopComponentDefinitionV5> = {};
|
||
|
|
|
||
|
|
componentsList.forEach((comp, index) => {
|
||
|
|
// 기본 크기 추정 (픽셀 → 칸)
|
||
|
|
const colSpan = Math.max(1, Math.round((comp.size.fixedWidth || 100) / 85));
|
||
|
|
const rowSpan = Math.max(1, Math.round((comp.size.fixedHeight || 48) / 48));
|
||
|
|
|
||
|
|
// 줄바꿈 체크
|
||
|
|
if (currentCol + colSpan - 1 > columns) {
|
||
|
|
currentRow++;
|
||
|
|
currentCol = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
componentsV5[comp.id] = {
|
||
|
|
...comp,
|
||
|
|
position: {
|
||
|
|
col: currentCol,
|
||
|
|
row: currentRow,
|
||
|
|
colSpan,
|
||
|
|
rowSpan,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
currentCol += colSpan;
|
||
|
|
});
|
||
|
|
|
||
|
|
return {
|
||
|
|
version: "pop-5.0",
|
||
|
|
gridConfig: { /* 기본값 */ },
|
||
|
|
components: componentsV5,
|
||
|
|
dataFlow: layoutV4.dataFlow,
|
||
|
|
settings: { /* 변환 */ },
|
||
|
|
};
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 하위 호환
|
||
|
|
|
||
|
|
| 버전 | 처리 방식 |
|
||
|
|
|------|----------|
|
||
|
|
| v1~v2 | v3로 변환 후 v5로 |
|
||
|
|
| v3 | v5로 직접 변환 |
|
||
|
|
| v4 | v5로 직접 변환 |
|
||
|
|
| v5 | 그대로 사용 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 일정 (예상)
|
||
|
|
|
||
|
|
| Phase | 작업 | 예상 기간 |
|
||
|
|
|-------|------|----------|
|
||
|
|
| 5.1 | 타입 정의 | 2~3일 |
|
||
|
|
| 5.2 | 그리드 렌더러 | 3~5일 |
|
||
|
|
| 5.3 | 디자이너 UI | 5~7일 |
|
||
|
|
| 5.4 | 반응형 자동화 | 3~5일 |
|
||
|
|
| - | 테스트 및 버그 수정 | 2~3일 |
|
||
|
|
| **총** | | **약 2~3주** |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 리스크 및 대응
|
||
|
|
|
||
|
|
| 리스크 | 영향 | 대응 |
|
||
|
|
|--------|------|------|
|
||
|
|
| 기존 v4 화면 깨짐 | 높음 | 하위 호환 유지, v4 렌더러 보존 |
|
||
|
|
| 자동 변환 품질 | 중간 | 수동 오버라이드로 보완 |
|
||
|
|
| 드래그 UX 복잡 | 중간 | 스냅 기능으로 편의성 확보 |
|
||
|
|
| 성능 저하 | 낮음 | CSS Grid는 네이티브 성능 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 성공 기준
|
||
|
|
|
||
|
|
1. **배치 예측 가능**: "2열 3행"이라고 하면 정확히 그 위치에 표시
|
||
|
|
2. **일관된 디자인**: 12칸 → 4칸 전환 시 비율 유지
|
||
|
|
3. **쉬운 편집**: 드래그로 칸에 스냅되어 배치
|
||
|
|
4. **하위 호환**: 기존 v4 화면이 정상 동작
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 관련 문서
|
||
|
|
|
||
|
|
- [GRID_SYSTEM_DESIGN.md](./GRID_SYSTEM_DESIGN.md) - 그리드 시스템 설계 상세
|
||
|
|
- [PLAN.md](./PLAN.md) - 전체 POP 개발 계획
|
||
|
|
- [V4_UNIFIED_DESIGN_SPEC.md](./V4_UNIFIED_DESIGN_SPEC.md) - 현재 v4 스펙
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
*최종 업데이트: 2026-02-05*
|