761 lines
22 KiB
Markdown
761 lines
22 KiB
Markdown
|
|
# POP v4.0 제약조건 기반 시스템 구현 계획
|
||
|
|
|
||
|
|
## 1. 현재 시스템 분석
|
||
|
|
|
||
|
|
### 1.1 현재 구조 (v3.0)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 4개 모드별 그리드 위치 기반
|
||
|
|
interface PopLayoutDataV3 {
|
||
|
|
version: "pop-3.0";
|
||
|
|
layouts: {
|
||
|
|
tablet_landscape: { componentPositions: Record<string, GridPosition> };
|
||
|
|
tablet_portrait: { componentPositions: Record<string, GridPosition> };
|
||
|
|
mobile_landscape: { componentPositions: Record<string, GridPosition> };
|
||
|
|
mobile_portrait: { componentPositions: Record<string, GridPosition> };
|
||
|
|
};
|
||
|
|
components: Record<string, PopComponentDefinition>;
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
|
||
|
|
interface GridPosition {
|
||
|
|
col: number; // 1-based
|
||
|
|
row: number; // 1-based
|
||
|
|
colSpan: number;
|
||
|
|
rowSpan: number;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.2 현재 문제점
|
||
|
|
|
||
|
|
1. **4배 작업량**: 4개 모드 각각 설계 필요
|
||
|
|
2. **수동 동기화**: 컴포넌트 추가/삭제 시 4모드 수동 동기화
|
||
|
|
3. **그리드 한계**: col/row 기반이라 자동 재배치 불가
|
||
|
|
4. **반응형 미흡**: 화면 크기 변화에 자동 적응 불가
|
||
|
|
5. **디바이스 차이 무시**: 태블릿/모바일 물리적 크기 차이 고려 안됨
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. 새로운 시스템 설계 (v4.0)
|
||
|
|
|
||
|
|
### 2.1 핵심 철학
|
||
|
|
|
||
|
|
```
|
||
|
|
"하나의 레이아웃 설계 → 제약조건 설정 → 모든 화면 자동 적응"
|
||
|
|
```
|
||
|
|
|
||
|
|
- **단일 소스**: 1개 레이아웃만 설계
|
||
|
|
- **제약조건 기반**: 컴포넌트가 "어떻게 반응할지" 규칙 정의
|
||
|
|
- **Flexbox 렌더링**: CSS Grid에서 Flexbox 기반으로 전환
|
||
|
|
- **자동 줄바꿈**: 공간 부족 시 자동 재배치
|
||
|
|
|
||
|
|
### 2.2 새로운 데이터 구조
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// v4.0 레이아웃
|
||
|
|
interface PopLayoutDataV4 {
|
||
|
|
version: "pop-4.0";
|
||
|
|
|
||
|
|
// 루트 컨테이너
|
||
|
|
root: PopContainer;
|
||
|
|
|
||
|
|
// 컴포넌트 정의 (ID → 정의)
|
||
|
|
components: Record<string, PopComponentDefinitionV4>;
|
||
|
|
|
||
|
|
// 데이터 흐름
|
||
|
|
dataFlow: PopDataFlow;
|
||
|
|
|
||
|
|
// 전역 설정
|
||
|
|
settings: PopGlobalSettingsV4;
|
||
|
|
|
||
|
|
// 메타데이터
|
||
|
|
metadata?: PopLayoutMetadata;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.3 컨테이너 (스택)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 컨테이너: 컴포넌트들을 담는 그룹
|
||
|
|
interface PopContainer {
|
||
|
|
id: string;
|
||
|
|
type: "stack";
|
||
|
|
|
||
|
|
// 스택 방향
|
||
|
|
direction: "horizontal" | "vertical";
|
||
|
|
|
||
|
|
// 줄바꿈 허용
|
||
|
|
wrap: boolean;
|
||
|
|
|
||
|
|
// 요소 간 간격
|
||
|
|
gap: number;
|
||
|
|
|
||
|
|
// 정렬
|
||
|
|
alignItems: "start" | "center" | "end" | "stretch";
|
||
|
|
justifyContent: "start" | "center" | "end" | "space-between" | "space-around";
|
||
|
|
|
||
|
|
// 패딩
|
||
|
|
padding?: {
|
||
|
|
top: number;
|
||
|
|
right: number;
|
||
|
|
bottom: number;
|
||
|
|
left: number;
|
||
|
|
};
|
||
|
|
|
||
|
|
// 반응형 규칙 (선택)
|
||
|
|
responsive?: {
|
||
|
|
// 브레이크포인트 (이 너비 이하에서 적용)
|
||
|
|
breakpoint: number;
|
||
|
|
// 변경할 방향
|
||
|
|
direction?: "horizontal" | "vertical";
|
||
|
|
// 변경할 간격
|
||
|
|
gap?: number;
|
||
|
|
}[];
|
||
|
|
|
||
|
|
// 자식 요소 (컴포넌트 ID 또는 중첩 컨테이너)
|
||
|
|
children: (string | PopContainer)[];
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.4 컴포넌트 제약조건
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface PopComponentDefinitionV4 {
|
||
|
|
id: string;
|
||
|
|
type: PopComponentType;
|
||
|
|
label?: string;
|
||
|
|
|
||
|
|
// ===== 크기 제약 (핵심) =====
|
||
|
|
size: {
|
||
|
|
// 너비 모드
|
||
|
|
width: "fixed" | "fill" | "hug";
|
||
|
|
// 높이 모드
|
||
|
|
height: "fixed" | "fill" | "hug";
|
||
|
|
|
||
|
|
// 고정 크기 (width/height가 fixed일 때)
|
||
|
|
fixedWidth?: number;
|
||
|
|
fixedHeight?: number;
|
||
|
|
|
||
|
|
// 최소/최대 크기
|
||
|
|
minWidth?: number;
|
||
|
|
maxWidth?: number;
|
||
|
|
minHeight?: number;
|
||
|
|
maxHeight?: number;
|
||
|
|
|
||
|
|
// 비율 (fill일 때, 같은 컨테이너 내 다른 요소와의 비율)
|
||
|
|
flexGrow?: number; // 기본 1
|
||
|
|
flexShrink?: number; // 기본 1
|
||
|
|
};
|
||
|
|
|
||
|
|
// ===== 정렬 =====
|
||
|
|
alignSelf?: "start" | "center" | "end" | "stretch";
|
||
|
|
|
||
|
|
// ===== 여백 =====
|
||
|
|
margin?: {
|
||
|
|
top: number;
|
||
|
|
right: number;
|
||
|
|
bottom: number;
|
||
|
|
left: number;
|
||
|
|
};
|
||
|
|
|
||
|
|
// ===== 모바일 스케일 (선택) =====
|
||
|
|
// 모바일에서 컴포넌트를 더 크게 표시
|
||
|
|
mobileScale?: number; // 기본 1.0, 예: 1.2 = 20% 더 크게
|
||
|
|
|
||
|
|
// ===== 기존 속성 =====
|
||
|
|
dataBinding?: PopDataBinding;
|
||
|
|
style?: PopStylePreset;
|
||
|
|
config?: PopComponentConfig;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.5 크기 모드 설명
|
||
|
|
|
||
|
|
| 모드 | 설명 | CSS 변환 |
|
||
|
|
|------|------|----------|
|
||
|
|
| `fixed` | 고정 크기 (px) | `width: {fixedWidth}px` |
|
||
|
|
| `fill` | 부모 공간 채우기 | `flex: {flexGrow} {flexShrink} 0` |
|
||
|
|
| `hug` | 내용에 맞춤 | `flex: 0 0 auto` |
|
||
|
|
|
||
|
|
### 2.6 전역 설정
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface PopGlobalSettingsV4 {
|
||
|
|
// 기본 터치 타겟 크기
|
||
|
|
touchTargetMin: number; // 48px
|
||
|
|
|
||
|
|
// 모드 (일반/산업현장)
|
||
|
|
mode: "normal" | "industrial";
|
||
|
|
|
||
|
|
// 기본 간격
|
||
|
|
defaultGap: number; // 8px
|
||
|
|
|
||
|
|
// 기본 패딩
|
||
|
|
defaultPadding: number; // 16px
|
||
|
|
|
||
|
|
// 반응형 브레이크포인트 (전역)
|
||
|
|
breakpoints: {
|
||
|
|
tablet: number; // 768px
|
||
|
|
mobile: number; // 480px
|
||
|
|
};
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. 디자이너 UI 변경
|
||
|
|
|
||
|
|
### 3.1 기존 디자이너 vs 새 디자이너
|
||
|
|
|
||
|
|
```
|
||
|
|
기존 (그리드 기반):
|
||
|
|
┌──────────────────────────────────────────────┐
|
||
|
|
│ [태블릿 가로] [태블릿 세로] [모바일 가로] [모바일 세로] │
|
||
|
|
│ │
|
||
|
|
│ 24x24 그리드에 컴포넌트 드래그 배치 │
|
||
|
|
│ │
|
||
|
|
└──────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
새로운 (제약조건 기반):
|
||
|
|
┌──────────────────────────────────────────────┐
|
||
|
|
│ [단일 캔버스] 미리보기: [태블릿▼] │
|
||
|
|
│ │
|
||
|
|
│ 스택(컨테이너)에 컴포넌트 배치 │
|
||
|
|
│ + 우측 패널에서 제약조건 설정 │
|
||
|
|
│ │
|
||
|
|
└──────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 새로운 디자이너 레이아웃
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────────┐
|
||
|
|
│ POP 화면 디자이너 v4 [저장] [미리보기] │
|
||
|
|
├────────────────┬────────────────────────┬───────────────────────┤
|
||
|
|
│ │ │ │
|
||
|
|
│ 컴포넌트 │ 캔버스 │ 속성 패널 │
|
||
|
|
│ │ │ │
|
||
|
|
│ ▼ 기본 │ ┌──────────────────┐ │ ▼ 선택됨: 입력창 │
|
||
|
|
│ [필드] │ │ ┌──────────────┐ │ │ │
|
||
|
|
│ [버튼] │ │ │입력창 │ │ │ ▼ 크기 │
|
||
|
|
│ [리스트] │ │ └──────────────┘ │ │ 너비: [채우기 ▼] │
|
||
|
|
│ [인디케이터] │ │ │ │ 최소: [100] px │
|
||
|
|
│ │ │ ┌─────┐ ┌─────┐ │ │ 최대: [없음] │
|
||
|
|
│ ▼ 입력 │ │ │버튼1│ │버튼2│ │ │ │
|
||
|
|
│ [스캐너] │ │ └─────┘ └─────┘ │ │ 높이: [고정 ▼] │
|
||
|
|
│ [숫자패드] │ │ │ │ 값: [48] px │
|
||
|
|
│ │ └──────────────────┘ │ │
|
||
|
|
│ ───────── │ │ ▼ 정렬 │
|
||
|
|
│ │ 미리보기: │ [늘이기 ▼] │
|
||
|
|
│ ▼ 레이아웃 │ ┌──────────────────┐ │ │
|
||
|
|
│ [스택 (가로)] │ │[태블릿 가로 ▼] │ │ ▼ 여백 │
|
||
|
|
│ [스택 (세로)] │ │[768px] │ │ 상[8] 우[0] 하[8] 좌[0]│
|
||
|
|
│ │ └──────────────────┘ │ │
|
||
|
|
│ │ │ ▼ 반응형 │
|
||
|
|
│ │ │ 모바일 스케일: [1.2] │
|
||
|
|
│ │ │ │
|
||
|
|
└────────────────┴────────────────────────┴───────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.3 컨테이너(스택) 편집
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─ 스택 속성 ─────────────────────┐
|
||
|
|
│ │
|
||
|
|
│ 방향: [가로 ▼] │
|
||
|
|
│ 줄바꿈: [허용 ☑] │
|
||
|
|
│ 간격: [8] px │
|
||
|
|
│ │
|
||
|
|
│ 정렬 (가로): [가운데 ▼] │
|
||
|
|
│ 정렬 (세로): [늘이기 ▼] │
|
||
|
|
│ │
|
||
|
|
│ ▼ 반응형 규칙 │
|
||
|
|
│ ┌─────────────────────────────┐ │
|
||
|
|
│ │ 768px 이하: 세로 방향 │ │
|
||
|
|
│ │ [+ 규칙 추가] │ │
|
||
|
|
│ └─────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
└─────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. 렌더링 로직 변경
|
||
|
|
|
||
|
|
### 4.1 기존 렌더링 (CSS Grid)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// v3: CSS Grid 기반
|
||
|
|
<div style={{
|
||
|
|
display: "grid",
|
||
|
|
gridTemplateColumns: `repeat(24, 1fr)`,
|
||
|
|
gridTemplateRows: `repeat(24, 1fr)`,
|
||
|
|
gap: "4px",
|
||
|
|
}}>
|
||
|
|
{componentIds.map(id => (
|
||
|
|
<div style={{
|
||
|
|
gridColumn: `${pos.col} / span ${pos.colSpan}`,
|
||
|
|
gridRow: `${pos.row} / span ${pos.rowSpan}`,
|
||
|
|
}}>
|
||
|
|
<Component />
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.2 새로운 렌더링 (Flexbox)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// v4: Flexbox 기반
|
||
|
|
function renderContainer(container: PopContainer, components: Record<string, PopComponentDefinitionV4>) {
|
||
|
|
const direction = useResponsiveValue(container, 'direction');
|
||
|
|
const gap = useResponsiveValue(container, 'gap');
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{
|
||
|
|
display: "flex",
|
||
|
|
flexDirection: direction === "horizontal" ? "row" : "column",
|
||
|
|
flexWrap: container.wrap ? "wrap" : "nowrap",
|
||
|
|
gap: `${gap}px`,
|
||
|
|
alignItems: container.alignItems,
|
||
|
|
justifyContent: container.justifyContent,
|
||
|
|
padding: container.padding ?
|
||
|
|
`${container.padding.top}px ${container.padding.right}px ${container.padding.bottom}px ${container.padding.left}px`
|
||
|
|
: undefined,
|
||
|
|
}}>
|
||
|
|
{container.children.map(child => {
|
||
|
|
if (typeof child === "string") {
|
||
|
|
// 컴포넌트 렌더링
|
||
|
|
return renderComponent(components[child]);
|
||
|
|
} else {
|
||
|
|
// 중첩 컨테이너 렌더링
|
||
|
|
return renderContainer(child, components);
|
||
|
|
}
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderComponent(component: PopComponentDefinitionV4) {
|
||
|
|
const { size, margin, mobileScale } = component;
|
||
|
|
const isMobile = useIsMobile();
|
||
|
|
const scale = isMobile && mobileScale ? mobileScale : 1;
|
||
|
|
|
||
|
|
// 크기 계산
|
||
|
|
let width: string;
|
||
|
|
let flex: string;
|
||
|
|
|
||
|
|
if (size.width === "fixed") {
|
||
|
|
width = `${(size.fixedWidth || 100) * scale}px`;
|
||
|
|
flex = "0 0 auto";
|
||
|
|
} else if (size.width === "fill") {
|
||
|
|
width = "auto";
|
||
|
|
flex = `${size.flexGrow || 1} ${size.flexShrink || 1} 0`;
|
||
|
|
} else { // hug
|
||
|
|
width = "auto";
|
||
|
|
flex = "0 0 auto";
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{
|
||
|
|
flex,
|
||
|
|
width,
|
||
|
|
minWidth: size.minWidth ? `${size.minWidth * scale}px` : undefined,
|
||
|
|
maxWidth: size.maxWidth ? `${size.maxWidth * scale}px` : undefined,
|
||
|
|
height: size.height === "fixed" ? `${(size.fixedHeight || 48) * scale}px` : "auto",
|
||
|
|
minHeight: size.minHeight ? `${size.minHeight * scale}px` : undefined,
|
||
|
|
maxHeight: size.maxHeight ? `${size.maxHeight * scale}px` : undefined,
|
||
|
|
margin: margin ?
|
||
|
|
`${margin.top}px ${margin.right}px ${margin.bottom}px ${margin.left}px`
|
||
|
|
: undefined,
|
||
|
|
alignSelf: component.alignSelf,
|
||
|
|
}}>
|
||
|
|
<ActualComponent {...component} />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.3 반응형 훅
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function useResponsiveValue<T>(
|
||
|
|
container: PopContainer,
|
||
|
|
property: keyof PopContainer
|
||
|
|
): T {
|
||
|
|
const windowWidth = useWindowWidth();
|
||
|
|
|
||
|
|
// 기본값
|
||
|
|
let value = container[property] as T;
|
||
|
|
|
||
|
|
// 반응형 규칙 적용 (작은 브레이크포인트 우선)
|
||
|
|
if (container.responsive) {
|
||
|
|
const sortedRules = [...container.responsive].sort((a, b) => b.breakpoint - a.breakpoint);
|
||
|
|
for (const rule of sortedRules) {
|
||
|
|
if (windowWidth <= rule.breakpoint && rule[property] !== undefined) {
|
||
|
|
value = rule[property] as T;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. 구현 단계
|
||
|
|
|
||
|
|
### Phase 1: 데이터 구조 (1-2일)
|
||
|
|
|
||
|
|
**파일**: `frontend/components/pop/designer/types/pop-layout.ts`
|
||
|
|
|
||
|
|
1. `PopLayoutDataV4` 인터페이스 정의
|
||
|
|
2. `PopContainer` 인터페이스 정의
|
||
|
|
3. `PopComponentDefinitionV4` 인터페이스 정의
|
||
|
|
4. `createEmptyPopLayoutV4()` 함수
|
||
|
|
5. `migrateV3ToV4()` 마이그레이션 함수
|
||
|
|
6. `ensureV4Layout()` 함수
|
||
|
|
7. 타입 가드 함수들
|
||
|
|
|
||
|
|
### Phase 2: 렌더러 (2-3일)
|
||
|
|
|
||
|
|
**파일**: `frontend/components/pop/designer/renderers/PopLayoutRendererV4.tsx`
|
||
|
|
|
||
|
|
1. `renderContainer()` 함수
|
||
|
|
2. `renderComponent()` 함수
|
||
|
|
3. `useResponsiveValue()` 훅
|
||
|
|
4. `useWindowWidth()` 훅
|
||
|
|
5. CSS 스타일 계산 로직
|
||
|
|
6. 반응형 브레이크포인트 처리
|
||
|
|
|
||
|
|
### Phase 3: 디자이너 UI (3-4일)
|
||
|
|
|
||
|
|
**파일**: `frontend/components/pop/designer/PopDesignerV4.tsx`
|
||
|
|
|
||
|
|
1. 캔버스 영역 (드래그 앤 드롭)
|
||
|
|
2. 컴포넌트 팔레트 (기존 + 스택)
|
||
|
|
3. 속성 패널
|
||
|
|
- 크기 제약 편집
|
||
|
|
- 정렬 편집
|
||
|
|
- 여백 편집
|
||
|
|
- 반응형 규칙 편집
|
||
|
|
4. 미리보기 모드 (다양한 화면 크기)
|
||
|
|
5. 컨테이너(스택) 관리
|
||
|
|
- 컨테이너 추가/삭제
|
||
|
|
- 컨테이너 설정 편집
|
||
|
|
- 컴포넌트 이동 (컨테이너 간)
|
||
|
|
|
||
|
|
### Phase 4: 뷰어 통합 (1-2일)
|
||
|
|
|
||
|
|
**파일**: `frontend/app/(pop)/pop/screens/[screenId]/page.tsx`
|
||
|
|
|
||
|
|
1. v4 레이아웃 감지 및 렌더링
|
||
|
|
2. 기존 v3 호환 유지
|
||
|
|
3. 반응형 모드 감지 연동
|
||
|
|
4. 성능 최적화
|
||
|
|
|
||
|
|
### Phase 5: 백엔드 수정 (1일)
|
||
|
|
|
||
|
|
**파일**: `backend-node/src/services/screenManagementService.ts`
|
||
|
|
|
||
|
|
1. `saveLayoutPop` - v4 버전 감지 및 저장
|
||
|
|
2. `getLayoutPop` - v4 버전 반환
|
||
|
|
3. 버전 마이그레이션 로직
|
||
|
|
|
||
|
|
### Phase 6: 테스트 및 마이그레이션 (2-3일)
|
||
|
|
|
||
|
|
1. 단위 테스트
|
||
|
|
2. 통합 테스트
|
||
|
|
3. 기존 v3 레이아웃 마이그레이션 도구
|
||
|
|
4. 크로스 디바이스 테스트
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. 마이그레이션 전략
|
||
|
|
|
||
|
|
### 6.1 v3 → v4 자동 변환
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function migrateV3ToV4(v3: PopLayoutDataV3): PopLayoutDataV4 {
|
||
|
|
// 태블릿 가로 모드 기준으로 변환
|
||
|
|
const baseLayout = v3.layouts.tablet_landscape;
|
||
|
|
const componentIds = Object.keys(baseLayout.componentPositions);
|
||
|
|
|
||
|
|
// 컴포넌트를 row, col 순으로 정렬
|
||
|
|
const sortedIds = componentIds.sort((a, b) => {
|
||
|
|
const posA = baseLayout.componentPositions[a];
|
||
|
|
const posB = baseLayout.componentPositions[b];
|
||
|
|
if (posA.row !== posB.row) return posA.row - posB.row;
|
||
|
|
return posA.col - posB.col;
|
||
|
|
});
|
||
|
|
|
||
|
|
// 같은 row에 있는 컴포넌트들을 가로 스택으로 그룹화
|
||
|
|
const rowGroups = groupByRow(sortedIds, baseLayout.componentPositions);
|
||
|
|
|
||
|
|
// 루트 컨테이너 (세로 스택)
|
||
|
|
const rootContainer: PopContainer = {
|
||
|
|
id: "root",
|
||
|
|
type: "stack",
|
||
|
|
direction: "vertical",
|
||
|
|
wrap: false,
|
||
|
|
gap: v3.settings.canvasGrid.gap,
|
||
|
|
alignItems: "stretch",
|
||
|
|
justifyContent: "start",
|
||
|
|
children: [],
|
||
|
|
};
|
||
|
|
|
||
|
|
// 각 행을 가로 스택으로 변환
|
||
|
|
for (const [row, ids] of rowGroups) {
|
||
|
|
if (ids.length === 1) {
|
||
|
|
// 단일 컴포넌트면 직접 추가
|
||
|
|
rootContainer.children.push(ids[0]);
|
||
|
|
} else {
|
||
|
|
// 여러 컴포넌트면 가로 스택으로 감싸기
|
||
|
|
const rowStack: PopContainer = {
|
||
|
|
id: `row-${row}`,
|
||
|
|
type: "stack",
|
||
|
|
direction: "horizontal",
|
||
|
|
wrap: true,
|
||
|
|
gap: v3.settings.canvasGrid.gap,
|
||
|
|
alignItems: "center",
|
||
|
|
justifyContent: "start",
|
||
|
|
children: ids,
|
||
|
|
};
|
||
|
|
rootContainer.children.push(rowStack);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 컴포넌트 정의 변환
|
||
|
|
const components: Record<string, PopComponentDefinitionV4> = {};
|
||
|
|
for (const id of componentIds) {
|
||
|
|
const v3Comp = v3.components[id];
|
||
|
|
const pos = baseLayout.componentPositions[id];
|
||
|
|
|
||
|
|
components[id] = {
|
||
|
|
...v3Comp,
|
||
|
|
size: {
|
||
|
|
// colSpan을 기반으로 크기 모드 결정
|
||
|
|
width: pos.colSpan >= 20 ? "fill" : "fixed",
|
||
|
|
height: "fixed",
|
||
|
|
fixedWidth: pos.colSpan * (1024 / 24), // 대략적인 픽셀 변환
|
||
|
|
fixedHeight: pos.rowSpan * (768 / 24),
|
||
|
|
minWidth: 100,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
version: "pop-4.0",
|
||
|
|
root: rootContainer,
|
||
|
|
components,
|
||
|
|
dataFlow: v3.dataFlow,
|
||
|
|
settings: {
|
||
|
|
touchTargetMin: v3.settings.touchTargetMin,
|
||
|
|
mode: v3.settings.mode,
|
||
|
|
defaultGap: v3.settings.canvasGrid.gap,
|
||
|
|
defaultPadding: 16,
|
||
|
|
breakpoints: {
|
||
|
|
tablet: 768,
|
||
|
|
mobile: 480,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
metadata: v3.metadata,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6.2 하위 호환
|
||
|
|
|
||
|
|
- v3 레이아웃은 계속 지원
|
||
|
|
- 디자이너에서 v3 → v4 업그레이드 버튼 제공
|
||
|
|
- 새로 생성하는 레이아웃은 v4
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. 예상 효과
|
||
|
|
|
||
|
|
### 7.1 사용자 경험
|
||
|
|
|
||
|
|
| 항목 | 기존 (v3) | 새로운 (v4) |
|
||
|
|
|------|-----------|-------------|
|
||
|
|
| 설계 개수 | 4개 | 1개 |
|
||
|
|
| 작업 시간 | 4배 | 1배 |
|
||
|
|
| 반응형 | 수동 | 자동 |
|
||
|
|
| 디바이스 대응 | 각각 설정 | mobileScale |
|
||
|
|
|
||
|
|
### 7.2 개발자 경험
|
||
|
|
|
||
|
|
| 항목 | 기존 (v3) | 새로운 (v4) |
|
||
|
|
|------|-----------|-------------|
|
||
|
|
| 렌더링 | CSS Grid | Flexbox |
|
||
|
|
| 위치 계산 | col/row | 자동 |
|
||
|
|
| 반응형 로직 | 4모드 분기 | 브레이크포인트 |
|
||
|
|
| 유지보수 | 복잡 | 단순 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. 일정 (예상)
|
||
|
|
|
||
|
|
| Phase | 내용 | 기간 |
|
||
|
|
|-------|------|------|
|
||
|
|
| 1 | 데이터 구조 | 1-2일 |
|
||
|
|
| 2 | 렌더러 | 2-3일 |
|
||
|
|
| 3 | 디자이너 UI | 3-4일 |
|
||
|
|
| 4 | 뷰어 통합 | 1-2일 |
|
||
|
|
| 5 | 백엔드 수정 | 1일 |
|
||
|
|
| 6 | 테스트/마이그레이션 | 2-3일 |
|
||
|
|
| **총계** | | **10-15일** |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. 리스크 및 대응
|
||
|
|
|
||
|
|
### 9.1 기존 레이아웃 호환성
|
||
|
|
|
||
|
|
- **리스크**: v3 → v4 자동 변환이 완벽하지 않을 수 있음
|
||
|
|
- **대응**:
|
||
|
|
- 마이그레이션 미리보기 기능
|
||
|
|
- 수동 조정 도구 제공
|
||
|
|
- v3 유지 옵션
|
||
|
|
|
||
|
|
### 9.2 학습 곡선
|
||
|
|
|
||
|
|
- **리스크**: 제약조건 개념이 익숙하지 않을 수 있음
|
||
|
|
- **대응**:
|
||
|
|
- 프리셋 제공 (예: "화면 전체 채우기", "고정 크기")
|
||
|
|
- 툴팁/도움말
|
||
|
|
- 예제 템플릿
|
||
|
|
|
||
|
|
### 9.3 성능
|
||
|
|
|
||
|
|
- **리스크**: Flexbox 중첩으로 렌더링 성능 저하
|
||
|
|
- **대응**:
|
||
|
|
- 컨테이너 중첩 깊이 제한 (최대 3-4)
|
||
|
|
- React.memo 활용
|
||
|
|
- 가상화 (리스트 컴포넌트)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 10. 결론
|
||
|
|
|
||
|
|
v4.0 제약조건 기반 시스템은 업계 표준(Figma, Flutter, SwiftUI)을 따르며, 사용자의 작업량을 75% 줄이고 자동 반응형을 제공합니다.
|
||
|
|
|
||
|
|
구현 후 POP 디자이너는:
|
||
|
|
- **1개 레이아웃**만 설계
|
||
|
|
- **모든 화면 크기**에 자동 적응
|
||
|
|
- **모바일 특화 설정** (mobileScale)으로 세밀한 제어 가능
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 11. 추가 설정 (2026-02-03 업데이트)
|
||
|
|
|
||
|
|
### 11.1 확장된 전역 설정
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface PopGlobalSettingsV4 {
|
||
|
|
// 기존
|
||
|
|
touchTargetMin: number; // 48 (normal) / 60 (industrial)
|
||
|
|
mode: "normal" | "industrial";
|
||
|
|
defaultGap: number;
|
||
|
|
defaultPadding: number;
|
||
|
|
breakpoints: {
|
||
|
|
tablet: number; // 768
|
||
|
|
mobile: number; // 480
|
||
|
|
};
|
||
|
|
|
||
|
|
// 신규 추가
|
||
|
|
environment: "indoor" | "outdoor"; // 야외면 대비 높임
|
||
|
|
|
||
|
|
typography: {
|
||
|
|
body: { min: number; max: number }; // 14-18px
|
||
|
|
heading: { min: number; max: number }; // 18-28px
|
||
|
|
caption: { min: number; max: number }; // 12-14px
|
||
|
|
};
|
||
|
|
|
||
|
|
contrast: "normal" | "high"; // outdoor면 자동 high
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 11.2 컴포넌트 기본값 프리셋
|
||
|
|
|
||
|
|
컴포넌트 추가 시 자동 적용되는 안전한 기본값:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const COMPONENT_DEFAULTS = {
|
||
|
|
"pop-button": {
|
||
|
|
minWidth: 80,
|
||
|
|
minHeight: 48,
|
||
|
|
height: "fixed",
|
||
|
|
fixedHeight: 48,
|
||
|
|
},
|
||
|
|
"pop-field": {
|
||
|
|
minWidth: 120,
|
||
|
|
minHeight: 40,
|
||
|
|
height: "fixed",
|
||
|
|
fixedHeight: 48,
|
||
|
|
},
|
||
|
|
"pop-list": {
|
||
|
|
minHeight: 200,
|
||
|
|
itemHeight: 48,
|
||
|
|
},
|
||
|
|
// ...
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 11.3 리스트 반응형 컬럼
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface PopListConfig {
|
||
|
|
// 기존
|
||
|
|
listType: PopListType;
|
||
|
|
displayColumns?: string[];
|
||
|
|
|
||
|
|
// 신규 추가
|
||
|
|
responsiveColumns?: {
|
||
|
|
tablet: string[]; // 전체 컬럼
|
||
|
|
mobile: string[]; // 주요 컬럼만
|
||
|
|
};
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 11.4 라벨 배치 자동화
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface PopContainer {
|
||
|
|
// 기존
|
||
|
|
direction: "horizontal" | "vertical";
|
||
|
|
|
||
|
|
// 신규 추가
|
||
|
|
labelPlacement?: "auto" | "above" | "beside";
|
||
|
|
// auto: 모바일 세로=위, 태블릿 가로=옆
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 12. 관련 문서
|
||
|
|
|
||
|
|
- [v4 핵심 규칙 가이드](./V4_CORE_RULES.md) - **3가지 핵심 규칙 (필독)**
|
||
|
|
- [반응형 디자인 가이드](./RESPONSIVE_DESIGN_GUIDE.md)
|
||
|
|
- [컴포넌트 로드맵](./COMPONENT_ROADMAP.md)
|
||
|
|
- [크기 프리셋 가이드](./SIZE_PRESETS.md)
|
||
|
|
- [컴포넌트 상세 스펙](./components-spec.md)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 13. 현재 상태 (2026-02-03)
|
||
|
|
|
||
|
|
**구현 대기**: 컴포넌트가 아직 없어서 레이아웃 시스템보다 컴포넌트 개발이 선행되어야 함.
|
||
|
|
|
||
|
|
**권장 진행 순서**:
|
||
|
|
1. 기초 컴포넌트 개발 (PopButton, PopInput 등)
|
||
|
|
2. 조합 컴포넌트 개발 (PopFormField, PopCard 등)
|
||
|
|
3. 복합 컴포넌트 개발 (PopDataTable, PopCardList 등)
|
||
|
|
4. v4 레이아웃 시스템 구현
|
||
|
|
5. 디자이너 UI 개발
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
*최종 업데이트: 2026-02-03*
|