730 lines
21 KiB
Markdown
730 lines
21 KiB
Markdown
|
|
# Flow 기반 반응형 레이아웃 설계서
|
||
|
|
|
||
|
|
> 작성일: 2026-01-30
|
||
|
|
> 목표: 진정한 반응형 구현 (PC/태블릿/모바일 전체 대응)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. 핵심 결론
|
||
|
|
|
||
|
|
### 1.1 현재 방식 vs 반응형 표준
|
||
|
|
|
||
|
|
| 항목 | 현재 시스템 | 웹 표준 (2025) |
|
||
|
|
|------|-------------|----------------|
|
||
|
|
| 배치 방식 | `position: absolute` | **Flexbox / CSS Grid** |
|
||
|
|
| 좌표 | 픽셀 고정 (x, y) | **Flow 기반 (순서)** |
|
||
|
|
| 화면 축소 시 | 그대로 (잘림) | **자동 재배치** |
|
||
|
|
| 용도 | 툴팁, 오버레이 | **전체 레이아웃** |
|
||
|
|
|
||
|
|
> **결론**: `position: absolute`는 전체 레이아웃에 사용하면 안 됨 (웹 표준)
|
||
|
|
|
||
|
|
### 1.2 구현 방향
|
||
|
|
|
||
|
|
```
|
||
|
|
절대 좌표 (x, y 픽셀)
|
||
|
|
↓ 변환
|
||
|
|
Flow 기반 배치 (Flexbox + Grid)
|
||
|
|
↓ 결과
|
||
|
|
화면 크기에 따라 자동 재배치
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. 실제 화면 데이터 분석
|
||
|
|
|
||
|
|
### 2.1 분석 대상
|
||
|
|
|
||
|
|
```
|
||
|
|
총 레이아웃: 1,250개
|
||
|
|
총 컴포넌트: 5,236개
|
||
|
|
분석 샘플: 6개 화면 (23, 20, 18, 16, 18, 5개 컴포넌트)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.2 화면 68 (수주 목록) - 가로 배치 패턴
|
||
|
|
|
||
|
|
```
|
||
|
|
y=88: [분리] [저장] [수정] [삭제] ← 같은 행에 버튼 4개
|
||
|
|
x=1277 x=1436 x=1594 x=1753
|
||
|
|
|
||
|
|
y=128: [────────── 테이블 ──────────]
|
||
|
|
x=8, width=1904
|
||
|
|
```
|
||
|
|
|
||
|
|
**변환 후**:
|
||
|
|
```html
|
||
|
|
<div class="flex flex-wrap justify-end gap-2"> <!-- Row 1 -->
|
||
|
|
<button>분리</button>
|
||
|
|
<button>저장</button>
|
||
|
|
<button>수정</button>
|
||
|
|
<button>삭제</button>
|
||
|
|
</div>
|
||
|
|
<div class="w-full"> <!-- Row 2 -->
|
||
|
|
<Table />
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**반응형 동작**:
|
||
|
|
```
|
||
|
|
1920px: [분리] [저장] [수정] [삭제] ← 가로 배치
|
||
|
|
1280px: [분리] [저장] [수정] [삭제] ← 가로 배치 (공간 충분)
|
||
|
|
768px: [분리] [저장] ← 줄바꿈 발생
|
||
|
|
[수정] [삭제]
|
||
|
|
375px: [분리] ← 세로 배치
|
||
|
|
[저장]
|
||
|
|
[수정]
|
||
|
|
[삭제]
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.3 화면 119 (장치 관리) - 2열 폼 패턴
|
||
|
|
|
||
|
|
```
|
||
|
|
y=80: [장치 코드 ] [시리얼넘버 ]
|
||
|
|
x=136, w=256 x=408, w=256
|
||
|
|
|
||
|
|
y=160: [제조사 ]
|
||
|
|
x=136, w=528
|
||
|
|
|
||
|
|
y=240: [품번 ] [모델명 ]
|
||
|
|
x=136, w=256 x=408, w=256
|
||
|
|
|
||
|
|
y=320: [구매일 ] [상태 ]
|
||
|
|
y=400: [공급사 ] [구매 가격 ]
|
||
|
|
y=480: [계약 번호 ] [공급사 전화 ]
|
||
|
|
... (2열 반복)
|
||
|
|
|
||
|
|
y=840: [저장]
|
||
|
|
x=544
|
||
|
|
```
|
||
|
|
|
||
|
|
**변환 후**:
|
||
|
|
```html
|
||
|
|
<div class="grid grid-cols-2 gap-4"> <!-- 2열 그리드 -->
|
||
|
|
<Input label="장치 코드" />
|
||
|
|
<Input label="시리얼넘버" />
|
||
|
|
</div>
|
||
|
|
<div class="w-full"> <!-- 전체 너비 -->
|
||
|
|
<Input label="제조사" />
|
||
|
|
</div>
|
||
|
|
<div class="grid grid-cols-2 gap-4">
|
||
|
|
<Input label="품번" />
|
||
|
|
<Select label="모델명" />
|
||
|
|
</div>
|
||
|
|
<!-- ... 반복 ... -->
|
||
|
|
<div class="flex justify-center">
|
||
|
|
<Button>저장</Button>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**반응형 동작**:
|
||
|
|
```
|
||
|
|
1920px: [장치 코드] [시리얼넘버] ← 2열
|
||
|
|
1280px: [장치 코드] [시리얼넘버] ← 2열
|
||
|
|
768px: [장치 코드] ← 1열
|
||
|
|
[시리얼넘버]
|
||
|
|
375px: [장치 코드] ← 1열
|
||
|
|
[시리얼넘버]
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.4 화면 4103 (수주 등록) - 섹션 기반 패턴
|
||
|
|
|
||
|
|
```
|
||
|
|
y=20: [섹션: 옵션 설정 ]
|
||
|
|
y=35: [입력방식▼] [판매유형▼] [단가방식▼] [☑ 단가수정]
|
||
|
|
|
||
|
|
y=110: [섹션: 거래처 정보 ]
|
||
|
|
y=190: [거래처 * ] [담당자 ] [납품처 ] [납품장소 ]
|
||
|
|
|
||
|
|
y=260: [섹션: 추가된 품목 ]
|
||
|
|
y=360: [리피터 테이블 ]
|
||
|
|
|
||
|
|
y=570: [섹션: 무역 정보 ]
|
||
|
|
y=690: [인코텀즈▼] [결제조건▼] [통화▼ ]
|
||
|
|
y=740: [선적항 ] [도착항 ] [HS Code ]
|
||
|
|
|
||
|
|
y=890: [섹션: 추가 정보 ]
|
||
|
|
y=935: [메모 ]
|
||
|
|
|
||
|
|
y=1080: [저장]
|
||
|
|
```
|
||
|
|
|
||
|
|
**변환 후**:
|
||
|
|
```html
|
||
|
|
<Card title="옵션 설정">
|
||
|
|
<div class="flex flex-wrap gap-4">
|
||
|
|
<Select label="입력방식" />
|
||
|
|
<Select label="판매유형" />
|
||
|
|
<Select label="단가방식" />
|
||
|
|
<Checkbox label="단가수정 허용" />
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card title="거래처 정보">
|
||
|
|
<div class="grid grid-cols-4 gap-4"> <!-- 4열 그리드 -->
|
||
|
|
<Select label="거래처 *" />
|
||
|
|
<Input label="담당자" />
|
||
|
|
<Input label="납품처" />
|
||
|
|
<Input label="납품장소" class="col-span-2" />
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<!-- ... 섹션 반복 ... -->
|
||
|
|
|
||
|
|
<div class="flex justify-end">
|
||
|
|
<Button>저장</Button>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**반응형 동작**:
|
||
|
|
```
|
||
|
|
1920px: [입력방식] [판매유형] [단가방식] [단가수정] ← 4열
|
||
|
|
1280px: [입력방식] [판매유형] [단가방식] ← 3열
|
||
|
|
[단가수정]
|
||
|
|
768px: [입력방식] [판매유형] ← 2열
|
||
|
|
[단가방식] [단가수정]
|
||
|
|
375px: [입력방식] ← 1열
|
||
|
|
[판매유형]
|
||
|
|
[단가방식]
|
||
|
|
[단가수정]
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. 변환 규칙
|
||
|
|
|
||
|
|
### 3.1 Row 그룹화 알고리즘
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const ROW_THRESHOLD = 40; // px
|
||
|
|
|
||
|
|
function groupByRows(components: Component[]): Row[] {
|
||
|
|
// 1. y 좌표로 정렬
|
||
|
|
const sorted = [...components].sort((a, b) => a.position.y - b.position.y);
|
||
|
|
|
||
|
|
const rows: Row[] = [];
|
||
|
|
let currentRow: Component[] = [];
|
||
|
|
let currentY = -Infinity;
|
||
|
|
|
||
|
|
for (const comp of sorted) {
|
||
|
|
if (comp.position.y - currentY > ROW_THRESHOLD) {
|
||
|
|
// 새로운 Row 시작
|
||
|
|
if (currentRow.length > 0) {
|
||
|
|
rows.push({
|
||
|
|
y: currentY,
|
||
|
|
components: currentRow.sort((a, b) => a.position.x - b.position.x)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
currentRow = [comp];
|
||
|
|
currentY = comp.position.y;
|
||
|
|
} else {
|
||
|
|
// 같은 Row에 추가
|
||
|
|
currentRow.push(comp);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 마지막 Row 추가
|
||
|
|
if (currentRow.length > 0) {
|
||
|
|
rows.push({
|
||
|
|
y: currentY,
|
||
|
|
components: currentRow.sort((a, b) => a.position.x - b.position.x)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return rows;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 화면 68 적용 예시
|
||
|
|
|
||
|
|
**입력**:
|
||
|
|
```json
|
||
|
|
[
|
||
|
|
{ "id": "comp_1899", "position": { "x": 1277, "y": 88 }, "text": "분리" },
|
||
|
|
{ "id": "comp_1898", "position": { "x": 1436, "y": 88 }, "text": "저장" },
|
||
|
|
{ "id": "comp_1897", "position": { "x": 1594, "y": 88 }, "text": "수정" },
|
||
|
|
{ "id": "comp_1896", "position": { "x": 1753, "y": 88 }, "text": "삭제" },
|
||
|
|
{ "id": "comp_1895", "position": { "x": 8, "y": 128 }, "type": "table" }
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
**변환 결과**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"rows": [
|
||
|
|
{
|
||
|
|
"y": 88,
|
||
|
|
"justify": "end",
|
||
|
|
"components": ["comp_1899", "comp_1898", "comp_1897", "comp_1896"]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"y": 128,
|
||
|
|
"justify": "start",
|
||
|
|
"components": ["comp_1895"]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.3 정렬 방향 결정
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function determineJustify(row: Row, screenWidth: number): string {
|
||
|
|
const firstX = row.components[0].position.x;
|
||
|
|
const lastComp = row.components[row.components.length - 1];
|
||
|
|
const lastEnd = lastComp.position.x + lastComp.size.width;
|
||
|
|
|
||
|
|
// 왼쪽 여백 vs 오른쪽 여백 비교
|
||
|
|
const leftMargin = firstX;
|
||
|
|
const rightMargin = screenWidth - lastEnd;
|
||
|
|
|
||
|
|
if (leftMargin > rightMargin * 2) {
|
||
|
|
return "end"; // 오른쪽 정렬
|
||
|
|
} else if (rightMargin > leftMargin * 2) {
|
||
|
|
return "start"; // 왼쪽 정렬
|
||
|
|
} else {
|
||
|
|
return "center"; // 중앙 정렬
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 화면 68 버튼 그룹:
|
||
|
|
// leftMargin = 1277, rightMargin = 1920 - 1912 = 8
|
||
|
|
// → "end" (오른쪽 정렬)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. 렌더링 구현
|
||
|
|
|
||
|
|
### 4.1 새로운 FlowLayout 컴포넌트
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// frontend/lib/registry/layouts/flow/FlowLayout.tsx
|
||
|
|
|
||
|
|
interface FlowLayoutProps {
|
||
|
|
layout: LayoutData;
|
||
|
|
renderer: DynamicComponentRenderer;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function FlowLayout({ layout, renderer }: FlowLayoutProps) {
|
||
|
|
// 1. Row 그룹화
|
||
|
|
const rows = useMemo(() => {
|
||
|
|
return groupByRows(layout.components);
|
||
|
|
}, [layout.components]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex flex-col gap-4 w-full">
|
||
|
|
{rows.map((row, index) => (
|
||
|
|
<FlowRow
|
||
|
|
key={index}
|
||
|
|
row={row}
|
||
|
|
renderer={renderer}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function FlowRow({ row, renderer }: { row: Row; renderer: any }) {
|
||
|
|
const justify = determineJustify(row, 1920);
|
||
|
|
|
||
|
|
const justifyClass = {
|
||
|
|
start: "justify-start",
|
||
|
|
center: "justify-center",
|
||
|
|
end: "justify-end",
|
||
|
|
}[justify];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={`flex flex-wrap gap-2 ${justifyClass}`}>
|
||
|
|
{row.components.map((comp) => (
|
||
|
|
<div
|
||
|
|
key={comp.id}
|
||
|
|
style={{
|
||
|
|
minWidth: comp.size.width,
|
||
|
|
// width는 고정하지 않음 (flex로 자동 조정)
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{renderer.renderChild(comp)}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.2 기존 코드 수정 위치
|
||
|
|
|
||
|
|
**현재 (RealtimePreviewDynamic.tsx 라인 524-536)**:
|
||
|
|
```tsx
|
||
|
|
const baseStyle = {
|
||
|
|
left: `${adjustedPositionX}px`, // ❌ 절대 좌표
|
||
|
|
top: `${position.y}px`, // ❌ 절대 좌표
|
||
|
|
position: "absolute", // ❌ 절대 위치
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
**변경 후**:
|
||
|
|
```tsx
|
||
|
|
// FlowLayout 사용 시 position 관련 스타일 제거
|
||
|
|
const baseStyle = isFlowMode ? {
|
||
|
|
// position, left, top 없음
|
||
|
|
minWidth: size.width,
|
||
|
|
height: size.height,
|
||
|
|
} : {
|
||
|
|
left: `${adjustedPositionX}px`,
|
||
|
|
top: `${position.y}px`,
|
||
|
|
position: "absolute",
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. 가상 시뮬레이션
|
||
|
|
|
||
|
|
### 5.1 시나리오 1: 화면 68 (버튼 4개 + 테이블)
|
||
|
|
|
||
|
|
**렌더링 결과 (1920px)**:
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────────────────┐
|
||
|
|
│ [분리] [저장] [수정] [삭제] │
|
||
|
|
│ flex-wrap, justify-end │
|
||
|
|
├────────────────────────────────────────────────────────────────────────┤
|
||
|
|
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ 테이블 (w-full) │ │
|
||
|
|
│ └────────────────────────────────────────────────────────────────────┘ │
|
||
|
|
└────────────────────────────────────────────────────────────────────────┘
|
||
|
|
✅ 정상: 버튼 오른쪽 정렬, 테이블 전체 너비
|
||
|
|
```
|
||
|
|
|
||
|
|
**렌더링 결과 (1280px)**:
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────┐
|
||
|
|
│ [분리] [저장] [수정] [삭제] │
|
||
|
|
│ flex-wrap, justify-end │
|
||
|
|
├─────────────────────────────────────────────┤
|
||
|
|
│ ┌─────────────────────────────────────────┐ │
|
||
|
|
│ │ 테이블 (w-full) │ │
|
||
|
|
│ └─────────────────────────────────────────┘ │
|
||
|
|
└─────────────────────────────────────────────┘
|
||
|
|
✅ 정상: 버튼 크기 유지, 테이블 너비 조정
|
||
|
|
```
|
||
|
|
|
||
|
|
**렌더링 결과 (768px)**:
|
||
|
|
```
|
||
|
|
┌──────────────────────────┐
|
||
|
|
│ [분리] [저장] │
|
||
|
|
│ [수정] [삭제] │ ← 자동 줄바꿈!
|
||
|
|
├──────────────────────────┤
|
||
|
|
│ ┌──────────────────────┐ │
|
||
|
|
│ │ 테이블 (w-full) │ │
|
||
|
|
│ └──────────────────────┘ │
|
||
|
|
└──────────────────────────┘
|
||
|
|
✅ 정상: 버튼 줄바꿈, 테이블 너비 조정
|
||
|
|
```
|
||
|
|
|
||
|
|
**렌더링 결과 (375px)**:
|
||
|
|
```
|
||
|
|
┌─────────────┐
|
||
|
|
│ [분리] │
|
||
|
|
│ [저장] │
|
||
|
|
│ [수정] │
|
||
|
|
│ [삭제] │ ← 세로 배치
|
||
|
|
├─────────────┤
|
||
|
|
│ ┌─────────┐ │
|
||
|
|
│ │ 테이블 │ │ (가로 스크롤)
|
||
|
|
│ └─────────┘ │
|
||
|
|
└─────────────┘
|
||
|
|
✅ 정상: 버튼 세로 배치, 테이블 가로 스크롤
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5.2 시나리오 2: 화면 119 (2열 폼)
|
||
|
|
|
||
|
|
**렌더링 결과 (1920px)**:
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────────────────┐
|
||
|
|
│ [장치 코드 ] [시리얼넘버 ] │
|
||
|
|
│ grid-cols-2 │
|
||
|
|
├────────────────────────────────────────────────────────────────────────┤
|
||
|
|
│ [제조사 ] │
|
||
|
|
│ col-span-2 (전체 너비) │
|
||
|
|
├────────────────────────────────────────────────────────────────────────┤
|
||
|
|
│ [품번 ] [모델명▼ ] │
|
||
|
|
│ ... │
|
||
|
|
└────────────────────────────────────────────────────────────────────────┘
|
||
|
|
✅ 정상: 2열 그리드
|
||
|
|
```
|
||
|
|
|
||
|
|
**렌더링 결과 (768px)**:
|
||
|
|
```
|
||
|
|
┌──────────────────────────┐
|
||
|
|
│ [장치 코드 ] │
|
||
|
|
│ [시리얼넘버 ] │ ← 1열로 변경
|
||
|
|
├──────────────────────────┤
|
||
|
|
│ [제조사 ] │
|
||
|
|
├──────────────────────────┤
|
||
|
|
│ [품번 ] │
|
||
|
|
│ [모델명▼ ] │
|
||
|
|
│ ... │
|
||
|
|
└──────────────────────────┘
|
||
|
|
✅ 정상: 1열 그리드
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5.3 시나리오 3: 분할 패널
|
||
|
|
|
||
|
|
**현재 SplitPanelLayout 동작**:
|
||
|
|
```
|
||
|
|
좌측 60% | 우측 40% ← 이미 퍼센트 기반
|
||
|
|
```
|
||
|
|
|
||
|
|
**변경 후 (768px 이하)**:
|
||
|
|
```
|
||
|
|
┌────────────────────┐
|
||
|
|
│ 좌측 100% │
|
||
|
|
├────────────────────┤
|
||
|
|
│ 우측 100% │
|
||
|
|
└────────────────────┘
|
||
|
|
← 세로 배치로 전환
|
||
|
|
```
|
||
|
|
|
||
|
|
**구현**:
|
||
|
|
```tsx
|
||
|
|
// SplitPanelLayoutComponent.tsx
|
||
|
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={isMobile ? "flex-col" : "flex-row"}>
|
||
|
|
<div className={isMobile ? "w-full" : "w-[60%]"}>
|
||
|
|
{/* 좌측 패널 */}
|
||
|
|
</div>
|
||
|
|
<div className={isMobile ? "w-full" : "w-[40%]"}>
|
||
|
|
{/* 우측 패널 */}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. 엣지 케이스 검증
|
||
|
|
|
||
|
|
### 6.1 겹치는 컴포넌트
|
||
|
|
|
||
|
|
**현재 데이터 (화면 74)**:
|
||
|
|
```json
|
||
|
|
{ "id": "comp_2606", "position": { "x": 161, "y": 400 } }, // 분할 패널
|
||
|
|
{ "id": "comp_fkk75q08", "position": { "x": 161, "y": 400 } } // 라디오 버튼
|
||
|
|
```
|
||
|
|
|
||
|
|
**문제**: 같은 위치에 두 컴포넌트 → z-index로 겹쳐서 표시
|
||
|
|
|
||
|
|
**해결**:
|
||
|
|
- z-index가 높은 컴포넌트 우선
|
||
|
|
- 또는 parent-child 관계면 중첩 처리
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function resolveOverlaps(row: Row): Row {
|
||
|
|
// z-index로 정렬하여 높은 것만 표시
|
||
|
|
// 또는 parentId 확인하여 중첩 처리
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6.2 조건부 표시 컴포넌트
|
||
|
|
|
||
|
|
**현재 데이터 (화면 4103)**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"id": "section-customer-info",
|
||
|
|
"conditionalConfig": {
|
||
|
|
"field": "input_method",
|
||
|
|
"value": "customer_first",
|
||
|
|
"action": "show"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**동작**: 조건에 따라 show/hide
|
||
|
|
**Flow 레이아웃에서**: 숨겨지면 공간도 사라짐 (flex 자동 조정)
|
||
|
|
|
||
|
|
✅ 문제없음
|
||
|
|
|
||
|
|
### 6.3 테이블 + 버튼 조합
|
||
|
|
|
||
|
|
**패턴**:
|
||
|
|
```
|
||
|
|
[버튼 그룹] ← flex-wrap, justify-end
|
||
|
|
[테이블] ← w-full
|
||
|
|
```
|
||
|
|
|
||
|
|
**테이블 가로 스크롤**:
|
||
|
|
- 테이블 내부는 가로 스크롤 지원
|
||
|
|
- 외부 컨테이너는 w-full
|
||
|
|
|
||
|
|
✅ 문제없음
|
||
|
|
|
||
|
|
### 6.4 섹션 카드 내부 컴포넌트
|
||
|
|
|
||
|
|
**현재**: 섹션 카드와 내부 컴포넌트가 별도로 저장됨
|
||
|
|
|
||
|
|
**변환 시**:
|
||
|
|
1. 섹션 카드의 y 범위 파악
|
||
|
|
2. 해당 y 범위 내 컴포넌트들을 섹션 자식으로 그룹화
|
||
|
|
3. 섹션 내부에서 다시 Row 그룹화
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function groupWithinSection(
|
||
|
|
section: Component,
|
||
|
|
allComponents: Component[]
|
||
|
|
): Component[] {
|
||
|
|
const sectionTop = section.position.y;
|
||
|
|
const sectionBottom = section.position.y + section.size.height;
|
||
|
|
|
||
|
|
return allComponents.filter(comp => {
|
||
|
|
return comp.id !== section.id &&
|
||
|
|
comp.position.y >= sectionTop &&
|
||
|
|
comp.position.y < sectionBottom;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. 호환성 검증
|
||
|
|
|
||
|
|
### 7.1 기존 기능 호환
|
||
|
|
|
||
|
|
| 기능 | 호환 여부 | 설명 |
|
||
|
|
|------|----------|------|
|
||
|
|
| 디자인 모드 | ⚠️ 수정 필요 | 드래그 앤 드롭 로직 수정 |
|
||
|
|
| 미리보기 | ✅ 호환 | Flow 레이아웃으로 렌더링 |
|
||
|
|
| 조건부 표시 | ✅ 호환 | flex로 자동 조정 |
|
||
|
|
| 분할 패널 | ⚠️ 수정 필요 | 반응형 전환 로직 추가 |
|
||
|
|
| 테이블 | ✅ 호환 | w-full 적용 |
|
||
|
|
| 모달 | ✅ 호환 | 모달 내부도 Flow 적용 |
|
||
|
|
|
||
|
|
### 7.2 디자인 모드 수정
|
||
|
|
|
||
|
|
**현재**: 드래그하면 x, y 픽셀 저장
|
||
|
|
**변경 후**: 드래그하면 x, y 픽셀 저장 (동일) → 렌더링 시 변환
|
||
|
|
|
||
|
|
```
|
||
|
|
저장: 픽셀 좌표 (기존 유지)
|
||
|
|
렌더링: Flow 기반으로 변환
|
||
|
|
```
|
||
|
|
|
||
|
|
**장점**: DB 마이그레이션 불필요
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. 구현 계획
|
||
|
|
|
||
|
|
### Phase 1: 핵심 변환 로직 (1일)
|
||
|
|
|
||
|
|
1. `groupByRows()` 함수 구현
|
||
|
|
2. `determineJustify()` 함수 구현
|
||
|
|
3. `FlowLayout` 컴포넌트 생성
|
||
|
|
|
||
|
|
### Phase 2: 렌더링 적용 (1일)
|
||
|
|
|
||
|
|
1. `DynamicComponentRenderer`에 Flow 모드 추가
|
||
|
|
2. `RealtimePreviewDynamic` 수정
|
||
|
|
3. 기존 absolute 스타일 조건부 적용
|
||
|
|
|
||
|
|
### Phase 3: 특수 케이스 처리 (1일)
|
||
|
|
|
||
|
|
1. 섹션 카드 내부 그룹화
|
||
|
|
2. 겹치는 컴포넌트 처리
|
||
|
|
3. 분할 패널 반응형 전환
|
||
|
|
|
||
|
|
### Phase 4: 테스트 (1일)
|
||
|
|
|
||
|
|
1. 화면 68 (버튼 + 테이블) 테스트
|
||
|
|
2. 화면 119 (2열 폼) 테스트
|
||
|
|
3. 화면 4103 (복잡한 폼) 테스트
|
||
|
|
4. PC 1920px → 1280px 테스트
|
||
|
|
5. 태블릿 768px 테스트
|
||
|
|
6. 모바일 375px 테스트
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. 예상 이슈
|
||
|
|
|
||
|
|
### 9.1 디자이너 의도 손실
|
||
|
|
|
||
|
|
**문제**: 디자이너가 의도적으로 배치한 위치가 변경될 수 있음
|
||
|
|
|
||
|
|
**해결**:
|
||
|
|
- 기본 Flow 레이아웃 적용
|
||
|
|
- 필요시 `flexOrder` 속성으로 순서 조정 가능
|
||
|
|
- 또는 `fixedPosition: true` 옵션으로 절대 좌표 유지
|
||
|
|
|
||
|
|
### 9.2 복잡한 레이아웃
|
||
|
|
|
||
|
|
**문제**: 일부 화면은 자유 배치가 필요할 수 있음
|
||
|
|
|
||
|
|
**해결**:
|
||
|
|
- 화면별 `layoutMode` 설정
|
||
|
|
- `"flow"`: Flow 기반 (기본값)
|
||
|
|
- `"absolute"`: 기존 절대 좌표
|
||
|
|
|
||
|
|
### 9.3 성능
|
||
|
|
|
||
|
|
**문제**: 매 렌더링마다 Row 그룹화 계산
|
||
|
|
|
||
|
|
**해결**:
|
||
|
|
- `useMemo`로 캐싱
|
||
|
|
- 컴포넌트 목록 변경 시에만 재계산
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 10. 최종 체크리스트
|
||
|
|
|
||
|
|
### 구현 전
|
||
|
|
|
||
|
|
- [ ] 현재 동작하는 화면 스크린샷 (비교용)
|
||
|
|
- [ ] 테스트 화면 목록 확정 (68, 119, 4103)
|
||
|
|
|
||
|
|
### 구현 중
|
||
|
|
|
||
|
|
- [ ] `groupByRows()` 구현
|
||
|
|
- [ ] `determineJustify()` 구현
|
||
|
|
- [ ] `FlowLayout` 컴포넌트 생성
|
||
|
|
- [ ] `DynamicComponentRenderer` 수정
|
||
|
|
- [ ] `RealtimePreviewDynamic` 수정
|
||
|
|
|
||
|
|
### 테스트
|
||
|
|
|
||
|
|
- [ ] 1920px 테스트
|
||
|
|
- [ ] 1280px 테스트
|
||
|
|
- [ ] 768px 테스트
|
||
|
|
- [ ] 375px 테스트
|
||
|
|
- [ ] 디자인 모드 테스트
|
||
|
|
- [ ] 분할 패널 테스트
|
||
|
|
- [ ] 조건부 표시 테스트
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 11. 결론
|
||
|
|
|
||
|
|
### 11.1 구현 가능 여부
|
||
|
|
|
||
|
|
**✅ 가능**
|
||
|
|
|
||
|
|
- 기존 데이터 구조 유지 (DB 변경 없음)
|
||
|
|
- 렌더링 레벨에서만 변환
|
||
|
|
- 모든 화면 패턴 분석 완료
|
||
|
|
- 엣지 케이스 해결책 확보
|
||
|
|
|
||
|
|
### 11.2 핵심 변경 사항
|
||
|
|
|
||
|
|
```
|
||
|
|
Before: position: absolute + left/top 픽셀
|
||
|
|
After: Flexbox + flex-wrap + justify-*
|
||
|
|
```
|
||
|
|
|
||
|
|
### 11.3 예상 효과
|
||
|
|
|
||
|
|
| 화면 크기 | Before | After |
|
||
|
|
|-----------|--------|-------|
|
||
|
|
| 1920px | 정상 | 정상 |
|
||
|
|
| 1280px | 버튼 잘림 | **자동 조정** |
|
||
|
|
| 768px | 레이아웃 깨짐 | **자동 재배치** |
|
||
|
|
| 375px | 사용 불가 | **자동 세로 배치** |
|