237 lines
5.1 KiB
Markdown
237 lines
5.1 KiB
Markdown
# POP 기술 스펙
|
|
|
|
**버전: v5 (CSS Grid 기반)**
|
|
|
|
---
|
|
|
|
## v5 핵심 규칙
|
|
|
|
### 1. 그리드 시스템
|
|
|
|
| 모드 | 화면 너비 | 칸 수 | 대상 기기 |
|
|
|------|----------|-------|----------|
|
|
| mobile_portrait | ~479px | 4칸 | 아이폰 SE ~ 갤럭시 S (세로) |
|
|
| mobile_landscape | 480~767px | 6칸 | 스마트폰 가로, 작은 태블릿 |
|
|
| tablet_portrait | 768~1023px | 8칸 | iPad Mini ~ iPad Pro (세로) |
|
|
| tablet_landscape | 1024px~ | 12칸 | 10~14인치 태블릿 가로 (기본) |
|
|
|
|
> **브레이크포인트 기준**: 실제 기기 CSS 뷰포트 너비 기반 (2026-02-06 재설계)
|
|
|
|
### 2. 위치 지정
|
|
|
|
```typescript
|
|
interface PopGridPosition {
|
|
col: number; // 시작 열 (1부터)
|
|
row: number; // 시작 행 (1부터)
|
|
colSpan: number; // 열 크기 (1~12)
|
|
rowSpan: number; // 행 크기 (1~)
|
|
}
|
|
```
|
|
|
|
### 3. 브레이크포인트 설정
|
|
|
|
```typescript
|
|
const GRID_BREAKPOINTS = {
|
|
mobile_portrait: {
|
|
columns: 4,
|
|
rowHeight: 48,
|
|
gap: 8,
|
|
padding: 12,
|
|
maxWidth: 479, // 아이폰 SE (375px) ~ 갤럭시 S (360px)
|
|
},
|
|
mobile_landscape: {
|
|
columns: 6,
|
|
rowHeight: 44,
|
|
gap: 8,
|
|
padding: 16,
|
|
minWidth: 480,
|
|
maxWidth: 767, // 스마트폰 가로
|
|
},
|
|
tablet_portrait: {
|
|
columns: 8,
|
|
rowHeight: 52,
|
|
gap: 12,
|
|
padding: 20,
|
|
minWidth: 768, // iPad Mini 세로 (768px)
|
|
maxWidth: 1023,
|
|
},
|
|
tablet_landscape: {
|
|
columns: 12,
|
|
rowHeight: 56,
|
|
gap: 12,
|
|
padding: 24,
|
|
minWidth: 1024, // iPad Pro 11 가로 (1194px), 12.9 가로 (1366px)
|
|
},
|
|
};
|
|
```
|
|
|
|
### 4. 세로 자동 확장
|
|
|
|
```typescript
|
|
// 캔버스 높이 동적 계산
|
|
const MIN_CANVAS_HEIGHT = 600; // 최소 높이 (px)
|
|
const CANVAS_EXTRA_ROWS = 3; // 항상 유지되는 여유 행 수
|
|
|
|
const dynamicCanvasHeight = useMemo(() => {
|
|
// 가장 아래 컴포넌트 위치 계산
|
|
const maxRowEnd = visibleComps.reduce((max, comp) => {
|
|
const rowEnd = pos.row + pos.rowSpan;
|
|
return Math.max(max, rowEnd);
|
|
}, 1);
|
|
|
|
// 여유 행 추가하여 높이 계산
|
|
const totalRows = maxRowEnd + CANVAS_EXTRA_ROWS;
|
|
return Math.max(MIN_CANVAS_HEIGHT, totalRows * rowHeight + padding);
|
|
}, [layout.components, ...]);
|
|
```
|
|
|
|
**특징**:
|
|
- 디자이너: 세로 무한 확장 (컴포넌트 추가에 제한 없음)
|
|
- 뷰어: 터치 스크롤로 아래 컴포넌트 접근 가능
|
|
|
|
---
|
|
|
|
## 데이터 구조
|
|
|
|
### v5 레이아웃
|
|
|
|
```typescript
|
|
interface PopLayoutDataV5 {
|
|
version: "pop-5.0";
|
|
metadata: {
|
|
screenId: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
gridConfig: {
|
|
defaultMode: GridMode;
|
|
maxRows: number;
|
|
};
|
|
components: PopComponentDefinitionV5[];
|
|
globalSettings: {
|
|
backgroundColor: string;
|
|
padding: number;
|
|
};
|
|
}
|
|
```
|
|
|
|
### v5 컴포넌트
|
|
|
|
```typescript
|
|
interface PopComponentDefinitionV5 {
|
|
id: string;
|
|
type: PopComponentType; // "pop-label" | "pop-button" | ...
|
|
label: string;
|
|
gridPosition: PopGridPosition;
|
|
config: PopComponentConfig;
|
|
visibility: Record<GridMode, boolean>; // 모드별 표시/숨김
|
|
modeOverrides?: Record<GridMode, PopModeOverrideV5>; // 모드별 오버라이드
|
|
}
|
|
```
|
|
|
|
### 컴포넌트 타입
|
|
|
|
```typescript
|
|
type PopComponentType =
|
|
| "pop-label" // 텍스트 라벨
|
|
| "pop-button" // 버튼
|
|
| "pop-input" // 입력 필드
|
|
| "pop-select" // 선택 박스
|
|
| "pop-grid" // 데이터 그리드
|
|
| "pop-container"; // 컨테이너
|
|
```
|
|
|
|
---
|
|
|
|
## 크기 프리셋
|
|
|
|
### 터치 요소
|
|
|
|
| 요소 | 일반 | 산업용 |
|
|
|------|-----|-------|
|
|
| 버튼 높이 | 48px | 60px |
|
|
| 입력창 높이 | 48px | 56px |
|
|
| 터치 영역 | 48px | 60px |
|
|
|
|
### 폰트 (clamp)
|
|
|
|
| 용도 | 범위 | CSS |
|
|
|------|-----|-----|
|
|
| 본문 | 14-18px | `clamp(14px, 1.5vw, 18px)` |
|
|
| 제목 | 18-28px | `clamp(18px, 2.5vw, 28px)` |
|
|
|
|
### 간격
|
|
|
|
| 이름 | 값 | 용도 |
|
|
|------|---|-----|
|
|
| sm | 8px | 요소 내부 |
|
|
| md | 16px | 컴포넌트 간 |
|
|
| lg | 24px | 섹션 간 |
|
|
|
|
---
|
|
|
|
## 반응형 원칙
|
|
|
|
```
|
|
누르는 것 → 고정 (48px) - 버튼, 터치 영역
|
|
읽는 것 → 범위 (clamp) - 텍스트
|
|
담는 것 → 칸 (colSpan) - 컨테이너
|
|
```
|
|
|
|
---
|
|
|
|
## 위치 변환
|
|
|
|
12칸 기준으로 설계 → 다른 모드에서 자동 변환
|
|
|
|
```typescript
|
|
// 12칸 → 4칸 변환 예시
|
|
const ratio = 4 / 12; // = 0.333
|
|
|
|
original: { col: 1, colSpan: 6 } // 12칸에서 절반
|
|
converted: { col: 1, colSpan: 2 } // 4칸에서 절반
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### 컴포넌트가 얇게 보임
|
|
|
|
- **증상**: rowSpan이 적용 안됨
|
|
- **원인**: gridTemplateRows 고정 px
|
|
- **해결**: `1fr` 사용
|
|
|
|
### 모드 전환 안 됨
|
|
|
|
- **증상**: 화면 크기 변경해도 레이아웃 유지
|
|
- **해결**: `detectGridMode()` 사용
|
|
|
|
### 겹침 발생
|
|
|
|
- **증상**: 컴포넌트끼리 겹침
|
|
- **해결**: `resolveOverlaps()` 호출
|
|
|
|
---
|
|
|
|
## 타입 가드
|
|
|
|
```typescript
|
|
// v5 레이아웃 판별
|
|
function isV5Layout(data: any): data is PopLayoutDataV5 {
|
|
return data?.version === "pop-5.0";
|
|
}
|
|
|
|
// 사용 예시
|
|
if (isV5Layout(savedData)) {
|
|
setLayout(savedData);
|
|
} else {
|
|
setLayout(createEmptyPopLayoutV5());
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
*상세 아키텍처: [ARCHITECTURE.md](./ARCHITECTURE.md)*
|
|
*파일 목록: [FILES.md](./FILES.md)*
|