764 lines
19 KiB
Markdown
764 lines
19 KiB
Markdown
# POP 그리드 시스템 코딩 계획
|
|
|
|
> 작성일: 2026-02-05
|
|
> 상태: 코딩 준비 완료
|
|
|
|
---
|
|
|
|
## 작업 목록
|
|
|
|
```
|
|
Phase 5.1: 타입 정의 ─────────────────────────────
|
|
[ ] 1. v5 타입 정의 (PopLayoutDataV5, PopGridConfig 등)
|
|
[ ] 2. 브레이크포인트 상수 정의
|
|
[ ] 3. v5 생성/변환 함수
|
|
|
|
Phase 5.2: 그리드 렌더러 ─────────────────────────
|
|
[ ] 4. PopGridRenderer.tsx 생성
|
|
[ ] 5. 위치 변환 로직 (12칸→4칸)
|
|
|
|
Phase 5.3: 디자이너 UI ───────────────────────────
|
|
[ ] 6. PopCanvasV5.tsx 생성
|
|
[ ] 7. 드래그 스냅 기능
|
|
[ ] 8. ComponentEditorPanelV5.tsx
|
|
|
|
Phase 5.4: 통합 ──────────────────────────────────
|
|
[ ] 9. 자동 변환 알고리즘
|
|
[ ] 10. PopDesigner.tsx 통합
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5.1: 타입 정의
|
|
|
|
### 작업 1: v5 타입 정의
|
|
|
|
**파일**: `frontend/components/pop/designer/types/pop-layout.ts`
|
|
|
|
**추가할 코드**:
|
|
|
|
```typescript
|
|
// ========================================
|
|
// v5.0 그리드 기반 레이아웃
|
|
// ========================================
|
|
// 핵심: CSS Grid로 정확한 위치 지정
|
|
// - 열/행 좌표로 배치 (col, row)
|
|
// - 칸 단위 크기 (colSpan, rowSpan)
|
|
|
|
/**
|
|
* v5 레이아웃 (그리드 기반)
|
|
*/
|
|
export interface PopLayoutDataV5 {
|
|
version: "pop-5.0";
|
|
|
|
// 그리드 설정
|
|
gridConfig: PopGridConfig;
|
|
|
|
// 컴포넌트 정의 (ID → 정의)
|
|
components: Record<string, PopComponentDefinitionV5>;
|
|
|
|
// 데이터 흐름 (기존과 동일)
|
|
dataFlow: PopDataFlow;
|
|
|
|
// 전역 설정
|
|
settings: PopGlobalSettingsV5;
|
|
|
|
// 메타데이터
|
|
metadata?: PopLayoutMetadata;
|
|
|
|
// 모드별 오버라이드 (위치 변경용)
|
|
overrides?: {
|
|
mobile_portrait?: PopModeOverrideV5;
|
|
mobile_landscape?: PopModeOverrideV5;
|
|
tablet_portrait?: PopModeOverrideV5;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 그리드 설정
|
|
*/
|
|
export interface PopGridConfig {
|
|
// 행 높이 (px) - 1행의 기본 높이
|
|
rowHeight: number; // 기본 48px
|
|
|
|
// 간격 (px)
|
|
gap: number; // 기본 8px
|
|
|
|
// 패딩 (px)
|
|
padding: number; // 기본 16px
|
|
}
|
|
|
|
/**
|
|
* v5 컴포넌트 정의
|
|
*/
|
|
export interface PopComponentDefinitionV5 {
|
|
id: string;
|
|
type: PopComponentType;
|
|
label?: string;
|
|
|
|
// 위치 (열/행 좌표) - 기본 모드(태블릿 가로 12칸) 기준
|
|
position: PopGridPosition;
|
|
|
|
// 모드별 표시/숨김
|
|
visibility?: {
|
|
tablet_landscape?: boolean;
|
|
tablet_portrait?: boolean;
|
|
mobile_landscape?: boolean;
|
|
mobile_portrait?: boolean;
|
|
};
|
|
|
|
// 기존 속성
|
|
dataBinding?: PopDataBinding;
|
|
style?: PopStylePreset;
|
|
config?: PopComponentConfig;
|
|
}
|
|
|
|
/**
|
|
* 그리드 위치
|
|
*/
|
|
export interface PopGridPosition {
|
|
col: number; // 시작 열 (1부터, 최대 12)
|
|
row: number; // 시작 행 (1부터)
|
|
colSpan: number; // 차지할 열 수 (1~12)
|
|
rowSpan: number; // 차지할 행 수 (1~)
|
|
}
|
|
|
|
/**
|
|
* v5 전역 설정
|
|
*/
|
|
export interface PopGlobalSettingsV5 {
|
|
// 터치 최소 크기 (px)
|
|
touchTargetMin: number; // 기본 48
|
|
|
|
// 모드
|
|
mode: "normal" | "industrial";
|
|
}
|
|
|
|
/**
|
|
* v5 모드별 오버라이드
|
|
*/
|
|
export interface PopModeOverrideV5 {
|
|
// 컴포넌트별 위치 오버라이드
|
|
positions?: Record<string, Partial<PopGridPosition>>;
|
|
|
|
// 컴포넌트별 숨김
|
|
hidden?: string[];
|
|
}
|
|
```
|
|
|
|
### 작업 2: 브레이크포인트 상수
|
|
|
|
**파일**: `frontend/components/pop/designer/types/pop-layout.ts`
|
|
|
|
```typescript
|
|
// ========================================
|
|
// 그리드 브레이크포인트
|
|
// ========================================
|
|
|
|
export type GridMode =
|
|
| "mobile_portrait"
|
|
| "mobile_landscape"
|
|
| "tablet_portrait"
|
|
| "tablet_landscape";
|
|
|
|
export const GRID_BREAKPOINTS: Record<GridMode, {
|
|
minWidth?: number;
|
|
maxWidth?: number;
|
|
columns: number;
|
|
rowHeight: number;
|
|
gap: number;
|
|
padding: number;
|
|
label: string;
|
|
}> = {
|
|
// 4~6인치 모바일 세로
|
|
mobile_portrait: {
|
|
maxWidth: 599,
|
|
columns: 4,
|
|
rowHeight: 40,
|
|
gap: 8,
|
|
padding: 12,
|
|
label: "모바일 세로 (4칸)",
|
|
},
|
|
|
|
// 6~8인치 모바일 가로 / 작은 태블릿
|
|
mobile_landscape: {
|
|
minWidth: 600,
|
|
maxWidth: 839,
|
|
columns: 6,
|
|
rowHeight: 44,
|
|
gap: 8,
|
|
padding: 16,
|
|
label: "모바일 가로 (6칸)",
|
|
},
|
|
|
|
// 8~10인치 태블릿 세로
|
|
tablet_portrait: {
|
|
minWidth: 840,
|
|
maxWidth: 1023,
|
|
columns: 8,
|
|
rowHeight: 48,
|
|
gap: 12,
|
|
padding: 16,
|
|
label: "태블릿 세로 (8칸)",
|
|
},
|
|
|
|
// 10~14인치 태블릿 가로 (기본)
|
|
tablet_landscape: {
|
|
minWidth: 1024,
|
|
columns: 12,
|
|
rowHeight: 48,
|
|
gap: 16,
|
|
padding: 24,
|
|
label: "태블릿 가로 (12칸)",
|
|
},
|
|
};
|
|
|
|
// 기본 모드
|
|
export const DEFAULT_GRID_MODE: GridMode = "tablet_landscape";
|
|
|
|
// 뷰포트 너비로 모드 감지
|
|
export function detectGridMode(viewportWidth: number): GridMode {
|
|
if (viewportWidth < 600) return "mobile_portrait";
|
|
if (viewportWidth < 840) return "mobile_landscape";
|
|
if (viewportWidth < 1024) return "tablet_portrait";
|
|
return "tablet_landscape";
|
|
}
|
|
```
|
|
|
|
### 작업 3: v5 생성/변환 함수
|
|
|
|
**파일**: `frontend/components/pop/designer/types/pop-layout.ts`
|
|
|
|
```typescript
|
|
// ========================================
|
|
// v5 유틸리티 함수
|
|
// ========================================
|
|
|
|
/**
|
|
* 빈 v5 레이아웃 생성
|
|
*/
|
|
export const createEmptyPopLayoutV5 = (): PopLayoutDataV5 => ({
|
|
version: "pop-5.0",
|
|
gridConfig: {
|
|
rowHeight: 48,
|
|
gap: 8,
|
|
padding: 16,
|
|
},
|
|
components: {},
|
|
dataFlow: { connections: [] },
|
|
settings: {
|
|
touchTargetMin: 48,
|
|
mode: "normal",
|
|
},
|
|
});
|
|
|
|
/**
|
|
* v5 레이아웃 여부 확인
|
|
*/
|
|
export const isV5Layout = (layout: any): layout is PopLayoutDataV5 => {
|
|
return layout?.version === "pop-5.0";
|
|
};
|
|
|
|
/**
|
|
* v5 컴포넌트 정의 생성
|
|
*/
|
|
export const createComponentDefinitionV5 = (
|
|
id: string,
|
|
type: PopComponentType,
|
|
position: PopGridPosition,
|
|
label?: string
|
|
): PopComponentDefinitionV5 => ({
|
|
id,
|
|
type,
|
|
label,
|
|
position,
|
|
});
|
|
|
|
/**
|
|
* 컴포넌트 타입별 기본 크기 (칸 단위)
|
|
*/
|
|
export const DEFAULT_COMPONENT_SIZE: Record<PopComponentType, { colSpan: number; rowSpan: number }> = {
|
|
"pop-field": { colSpan: 3, rowSpan: 1 },
|
|
"pop-button": { colSpan: 2, rowSpan: 1 },
|
|
"pop-list": { colSpan: 12, rowSpan: 4 },
|
|
"pop-indicator": { colSpan: 3, rowSpan: 2 },
|
|
"pop-scanner": { colSpan: 6, rowSpan: 2 },
|
|
"pop-numpad": { colSpan: 4, rowSpan: 5 },
|
|
"pop-spacer": { colSpan: 1, rowSpan: 1 },
|
|
"pop-break": { colSpan: 12, rowSpan: 0 },
|
|
};
|
|
|
|
/**
|
|
* v4 → v5 마이그레이션
|
|
*/
|
|
export const migrateV4ToV5 = (layoutV4: PopLayoutDataV4): PopLayoutDataV5 => {
|
|
const componentsV4 = Object.values(layoutV4.components);
|
|
const componentsV5: Record<string, PopComponentDefinitionV5> = {};
|
|
|
|
// Flexbox 순서 → Grid 위치 변환
|
|
let currentRow = 1;
|
|
let currentCol = 1;
|
|
const columns = 12;
|
|
|
|
componentsV4.forEach((comp) => {
|
|
// 픽셀 → 칸 변환 (대략적)
|
|
const colSpan = comp.size.width === "fill"
|
|
? columns
|
|
: Math.max(1, Math.min(12, 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 += 1;
|
|
currentCol = 1;
|
|
}
|
|
|
|
componentsV5[comp.id] = {
|
|
id: comp.id,
|
|
type: comp.type,
|
|
label: comp.label,
|
|
position: {
|
|
col: currentCol,
|
|
row: currentRow,
|
|
colSpan,
|
|
rowSpan,
|
|
},
|
|
visibility: comp.visibility,
|
|
dataBinding: comp.dataBinding,
|
|
config: comp.config,
|
|
};
|
|
|
|
currentCol += colSpan;
|
|
});
|
|
|
|
return {
|
|
version: "pop-5.0",
|
|
gridConfig: {
|
|
rowHeight: 48,
|
|
gap: layoutV4.settings.defaultGap,
|
|
padding: layoutV4.settings.defaultPadding,
|
|
},
|
|
components: componentsV5,
|
|
dataFlow: layoutV4.dataFlow,
|
|
settings: {
|
|
touchTargetMin: layoutV4.settings.touchTargetMin,
|
|
mode: layoutV4.settings.mode,
|
|
},
|
|
};
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5.2: 그리드 렌더러
|
|
|
|
### 작업 4: PopGridRenderer.tsx
|
|
|
|
**파일**: `frontend/components/pop/designer/renderers/PopGridRenderer.tsx`
|
|
|
|
```typescript
|
|
"use client";
|
|
|
|
import React, { useMemo } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
PopLayoutDataV5,
|
|
PopComponentDefinitionV5,
|
|
PopGridPosition,
|
|
GridMode,
|
|
GRID_BREAKPOINTS,
|
|
detectGridMode,
|
|
} from "../types/pop-layout";
|
|
|
|
interface PopGridRendererProps {
|
|
layout: PopLayoutDataV5;
|
|
viewportWidth: number;
|
|
currentMode?: GridMode;
|
|
isDesignMode?: boolean;
|
|
selectedComponentId?: string | null;
|
|
onComponentClick?: (componentId: string) => void;
|
|
onBackgroundClick?: () => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function PopGridRenderer({
|
|
layout,
|
|
viewportWidth,
|
|
currentMode,
|
|
isDesignMode = false,
|
|
selectedComponentId,
|
|
onComponentClick,
|
|
onBackgroundClick,
|
|
className,
|
|
}: PopGridRendererProps) {
|
|
const { gridConfig, components, overrides } = layout;
|
|
|
|
// 현재 모드 (자동 감지 또는 지정)
|
|
const mode = currentMode || detectGridMode(viewportWidth);
|
|
const breakpoint = GRID_BREAKPOINTS[mode];
|
|
|
|
// CSS Grid 스타일
|
|
const gridStyle = useMemo((): React.CSSProperties => ({
|
|
display: "grid",
|
|
gridTemplateColumns: `repeat(${breakpoint.columns}, 1fr)`,
|
|
gridAutoRows: `${breakpoint.rowHeight}px`,
|
|
gap: `${breakpoint.gap}px`,
|
|
padding: `${breakpoint.padding}px`,
|
|
minHeight: "100%",
|
|
}), [breakpoint]);
|
|
|
|
// visibility 체크
|
|
const isVisible = (comp: PopComponentDefinitionV5): boolean => {
|
|
if (!comp.visibility) return true;
|
|
return comp.visibility[mode] !== false;
|
|
};
|
|
|
|
// 위치 변환 (12칸 기준 → 현재 모드 칸 수)
|
|
const convertPosition = (position: PopGridPosition): React.CSSProperties => {
|
|
const sourceColumns = 12; // 항상 12칸 기준으로 저장
|
|
const targetColumns = breakpoint.columns;
|
|
|
|
if (sourceColumns === targetColumns) {
|
|
return {
|
|
gridColumn: `${position.col} / span ${position.colSpan}`,
|
|
gridRow: `${position.row} / span ${position.rowSpan}`,
|
|
};
|
|
}
|
|
|
|
// 비율 계산
|
|
const ratio = targetColumns / sourceColumns;
|
|
let newCol = Math.max(1, Math.ceil((position.col - 1) * ratio) + 1);
|
|
let newColSpan = Math.max(1, Math.round(position.colSpan * ratio));
|
|
|
|
// 범위 초과 방지
|
|
if (newCol + newColSpan - 1 > targetColumns) {
|
|
newColSpan = targetColumns - newCol + 1;
|
|
}
|
|
|
|
return {
|
|
gridColumn: `${newCol} / span ${Math.max(1, newColSpan)}`,
|
|
gridRow: `${position.row} / span ${position.rowSpan}`,
|
|
};
|
|
};
|
|
|
|
// 오버라이드 적용
|
|
const getEffectivePosition = (comp: PopComponentDefinitionV5): PopGridPosition => {
|
|
const override = overrides?.[mode]?.positions?.[comp.id];
|
|
if (override) {
|
|
return { ...comp.position, ...override };
|
|
}
|
|
return comp.position;
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={cn("relative min-h-full w-full bg-white", className)}
|
|
style={gridStyle}
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget) {
|
|
onBackgroundClick?.();
|
|
}
|
|
}}
|
|
>
|
|
{Object.values(components).map((comp) => {
|
|
if (!isVisible(comp)) return null;
|
|
|
|
const position = getEffectivePosition(comp);
|
|
const positionStyle = convertPosition(position);
|
|
|
|
return (
|
|
<div
|
|
key={comp.id}
|
|
className={cn(
|
|
"relative rounded-lg border-2 bg-white transition-all overflow-hidden",
|
|
selectedComponentId === comp.id
|
|
? "border-primary ring-2 ring-primary/30 z-10"
|
|
: "border-gray-200",
|
|
isDesignMode && "cursor-pointer hover:border-gray-300"
|
|
)}
|
|
style={positionStyle}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onComponentClick?.(comp.id);
|
|
}}
|
|
>
|
|
{/* 컴포넌트 내용 */}
|
|
<ComponentContent component={comp} isDesignMode={isDesignMode} />
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 컴포넌트 내용 렌더링
|
|
function ComponentContent({
|
|
component,
|
|
isDesignMode
|
|
}: {
|
|
component: PopComponentDefinitionV5;
|
|
isDesignMode: boolean;
|
|
}) {
|
|
const typeLabels: Record<string, string> = {
|
|
"pop-field": "필드",
|
|
"pop-button": "버튼",
|
|
"pop-list": "리스트",
|
|
"pop-indicator": "인디케이터",
|
|
"pop-scanner": "스캐너",
|
|
"pop-numpad": "숫자패드",
|
|
"pop-spacer": "스페이서",
|
|
"pop-break": "줄바꿈",
|
|
};
|
|
|
|
if (isDesignMode) {
|
|
return (
|
|
<div className="flex h-full w-full flex-col">
|
|
<div className="flex h-5 shrink-0 items-center border-b bg-gray-50 px-2">
|
|
<span className="text-[10px] font-medium text-gray-600">
|
|
{component.label || typeLabels[component.type] || component.type}
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-1 items-center justify-center p-2">
|
|
<span className="text-xs text-gray-400">
|
|
{typeLabels[component.type]}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 실제 컴포넌트 렌더링 (Phase 4에서 구현)
|
|
return (
|
|
<div className="flex h-full w-full items-center justify-center p-2">
|
|
<span className="text-xs text-gray-500">
|
|
{component.label || typeLabels[component.type]}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default PopGridRenderer;
|
|
```
|
|
|
|
### 작업 5: 위치 변환 유틸리티
|
|
|
|
**파일**: `frontend/components/pop/designer/utils/gridUtils.ts`
|
|
|
|
```typescript
|
|
import { PopGridPosition, GridMode, GRID_BREAKPOINTS } from "../types/pop-layout";
|
|
|
|
/**
|
|
* 12칸 기준 위치를 다른 모드로 변환
|
|
*/
|
|
export function convertPositionToMode(
|
|
position: PopGridPosition,
|
|
targetMode: GridMode
|
|
): PopGridPosition {
|
|
const sourceColumns = 12;
|
|
const targetColumns = GRID_BREAKPOINTS[targetMode].columns;
|
|
|
|
if (sourceColumns === targetColumns) {
|
|
return position;
|
|
}
|
|
|
|
const ratio = targetColumns / sourceColumns;
|
|
|
|
// 열 위치 변환
|
|
let newCol = Math.max(1, Math.ceil((position.col - 1) * ratio) + 1);
|
|
let newColSpan = Math.max(1, Math.round(position.colSpan * ratio));
|
|
|
|
// 범위 초과 방지
|
|
if (newCol > targetColumns) {
|
|
newCol = 1;
|
|
}
|
|
if (newCol + newColSpan - 1 > targetColumns) {
|
|
newColSpan = targetColumns - newCol + 1;
|
|
}
|
|
|
|
return {
|
|
col: newCol,
|
|
row: position.row,
|
|
colSpan: Math.max(1, newColSpan),
|
|
rowSpan: position.rowSpan,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 두 위치가 겹치는지 확인
|
|
*/
|
|
export function isOverlapping(a: PopGridPosition, b: PopGridPosition): boolean {
|
|
// 열 겹침
|
|
const colOverlap = !(a.col + a.colSpan - 1 < b.col || b.col + b.colSpan - 1 < a.col);
|
|
// 행 겹침
|
|
const rowOverlap = !(a.row + a.rowSpan - 1 < b.row || b.row + b.rowSpan - 1 < a.row);
|
|
|
|
return colOverlap && rowOverlap;
|
|
}
|
|
|
|
/**
|
|
* 겹침 해결 (아래로 밀기)
|
|
*/
|
|
export function resolveOverlaps(
|
|
positions: Array<{ id: string; position: PopGridPosition }>,
|
|
columns: number
|
|
): Array<{ id: string; position: PopGridPosition }> {
|
|
// row, col 순으로 정렬
|
|
const sorted = [...positions].sort((a, b) =>
|
|
a.position.row - b.position.row || a.position.col - b.position.col
|
|
);
|
|
|
|
const resolved: Array<{ id: string; position: PopGridPosition }> = [];
|
|
|
|
sorted.forEach((item) => {
|
|
let { row, col, colSpan, rowSpan } = item.position;
|
|
|
|
// 기존 배치와 겹치면 아래로 이동
|
|
let attempts = 0;
|
|
while (attempts < 100) {
|
|
const currentPos: PopGridPosition = { col, row, colSpan, rowSpan };
|
|
const hasOverlap = resolved.some(r => isOverlapping(currentPos, r.position));
|
|
|
|
if (!hasOverlap) break;
|
|
|
|
row++;
|
|
attempts++;
|
|
}
|
|
|
|
resolved.push({
|
|
id: item.id,
|
|
position: { col, row, colSpan, rowSpan },
|
|
});
|
|
});
|
|
|
|
return resolved;
|
|
}
|
|
|
|
/**
|
|
* 마우스 좌표 → 그리드 좌표 변환
|
|
*/
|
|
export function mouseToGridPosition(
|
|
mouseX: number,
|
|
mouseY: number,
|
|
canvasRect: DOMRect,
|
|
columns: number,
|
|
rowHeight: number,
|
|
gap: number,
|
|
padding: number
|
|
): { col: number; row: number } {
|
|
// 캔버스 내 상대 위치
|
|
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;
|
|
|
|
// 그리드 좌표 계산 (1부터 시작)
|
|
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 };
|
|
}
|
|
|
|
/**
|
|
* 그리드 좌표 → 픽셀 좌표 변환
|
|
*/
|
|
export function gridToPixelPosition(
|
|
col: number,
|
|
row: number,
|
|
canvasWidth: number,
|
|
columns: number,
|
|
rowHeight: number,
|
|
gap: number,
|
|
padding: number
|
|
): { x: number; y: number; width: number; height: number } {
|
|
const totalGap = gap * (columns - 1);
|
|
const colWidth = (canvasWidth - padding * 2 - totalGap) / columns;
|
|
|
|
return {
|
|
x: padding + (col - 1) * (colWidth + gap),
|
|
y: padding + (row - 1) * (rowHeight + gap),
|
|
width: colWidth,
|
|
height: rowHeight,
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5.3: 디자이너 UI
|
|
|
|
### 작업 6-7: PopCanvasV5.tsx
|
|
|
|
**파일**: `frontend/components/pop/designer/PopCanvasV5.tsx`
|
|
|
|
핵심 기능:
|
|
- 그리드 배경 표시 (바둑판)
|
|
- 4개 모드 프리셋 버튼
|
|
- 드래그 앤 드롭 (칸에 스냅)
|
|
- 컴포넌트 리사이즈 (칸 단위)
|
|
|
|
### 작업 8: ComponentEditorPanelV5.tsx
|
|
|
|
**파일**: `frontend/components/pop/designer/panels/ComponentEditorPanelV5.tsx`
|
|
|
|
핵심 기능:
|
|
- 위치 편집 (col, row 입력)
|
|
- 크기 편집 (colSpan, rowSpan 입력)
|
|
- visibility 체크박스
|
|
|
|
---
|
|
|
|
## Phase 5.4: 통합
|
|
|
|
### 작업 9: 자동 변환 알고리즘
|
|
|
|
이미 `gridUtils.ts`에 포함
|
|
|
|
### 작업 10: PopDesigner.tsx 통합
|
|
|
|
**수정 파일**: `frontend/components/pop/designer/PopDesigner.tsx`
|
|
|
|
변경 사항:
|
|
- v5 레이아웃 상태 추가
|
|
- v3/v4/v5 자동 판별
|
|
- 새 화면 → v5로 시작
|
|
- v4 → v5 마이그레이션 옵션
|
|
|
|
---
|
|
|
|
## 파일 목록
|
|
|
|
| 상태 | 파일 | 작업 |
|
|
|------|------|------|
|
|
| 수정 | `types/pop-layout.ts` | v5 타입, 상수, 함수 추가 |
|
|
| 생성 | `renderers/PopGridRenderer.tsx` | 그리드 렌더러 |
|
|
| 생성 | `utils/gridUtils.ts` | 유틸리티 함수 |
|
|
| 생성 | `PopCanvasV5.tsx` | 그리드 캔버스 |
|
|
| 생성 | `panels/ComponentEditorPanelV5.tsx` | 속성 패널 |
|
|
| 수정 | `PopDesigner.tsx` | v5 통합 |
|
|
|
|
---
|
|
|
|
## 시작 순서
|
|
|
|
```
|
|
1. pop-layout.ts에 v5 타입 추가 (작업 1-3)
|
|
↓
|
|
2. PopGridRenderer.tsx 생성 (작업 4)
|
|
↓
|
|
3. gridUtils.ts 생성 (작업 5)
|
|
↓
|
|
4. PopCanvasV5.tsx 생성 (작업 6-7)
|
|
↓
|
|
5. ComponentEditorPanelV5.tsx 생성 (작업 8)
|
|
↓
|
|
6. PopDesigner.tsx 수정 (작업 9-10)
|
|
↓
|
|
7. 테스트
|
|
```
|
|
|
|
---
|
|
|
|
*다음 단계: Phase 5.1 작업 1 시작 (v5 타입 정의)*
|