# 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 | 사용 불가 | **자동 세로 배치** |
