691 lines
17 KiB
Markdown
691 lines
17 KiB
Markdown
# Phase 3: Visibility + 줄바꿈 컴포넌트 구현
|
||
|
||
**날짜**: 2026-02-04
|
||
**상태**: 구현 완료 ✅
|
||
**관련 이슈**: 모드별 컴포넌트 표시/숨김, 강제 줄바꿈
|
||
|
||
---
|
||
|
||
## 📋 목표
|
||
|
||
Phase 2의 배치 고정 기능에 이어, 다음 기능들을 추가:
|
||
1. **모드별 컴포넌트 표시/숨김** (visibility)
|
||
2. **강제 줄바꿈 컴포넌트** (pop-break)
|
||
3. **컴포넌트 오버라이드 병합** (모드별 설정 변경)
|
||
|
||
---
|
||
|
||
## 🔍 문제 정의
|
||
|
||
### 문제 1: 모드별 컴포넌트 추가/삭제 불가
|
||
```
|
||
현재 상황:
|
||
- 모든 모드에서 같은 컴포넌트만 표시 가능
|
||
- 모바일 전용 버튼(예: "전화 걸기")을 추가할 수 없음
|
||
|
||
요구사항:
|
||
- 특정 모드에서만 컴포넌트 표시
|
||
- 다른 모드에서는 자동 숨김
|
||
```
|
||
|
||
### 문제 2: Flexbox에서 강제 줄바꿈 불가
|
||
```
|
||
현재 상황:
|
||
- wrap: true여도 컴포넌트가 공간을 채워야 줄바꿈
|
||
- [A] [B] [C] → 강제로 [A] [B] / [C] 불가능
|
||
|
||
요구사항:
|
||
- 사용자가 원하는 위치에서 강제 줄바꿈
|
||
- 디자인 모드에서 시각적으로 표시
|
||
```
|
||
|
||
### 문제 3: 컴포넌트 설정을 모드별로 변경 불가
|
||
```
|
||
현재 상황:
|
||
- 컨테이너 배치만 오버라이드 가능
|
||
- 리스트 컬럼 수, 버튼 스타일 등은 모든 모드 동일
|
||
|
||
요구사항 (확장성):
|
||
- 태블릿: 리스트 7개 컬럼
|
||
- 모바일: 리스트 3개 컬럼
|
||
```
|
||
|
||
---
|
||
|
||
## 💡 해결 방안
|
||
|
||
### 방안 A: children 배열 오버라이드 (추가/삭제)
|
||
```typescript
|
||
overrides: {
|
||
mobile_portrait: {
|
||
containers: {
|
||
root: {
|
||
children: ["comp1", "comp2", "mobile-only-button"] // 컴포넌트 추가
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**장점**:
|
||
- 모드별로 완전히 다른 컴포넌트 구성 가능
|
||
- 유연성 극대화
|
||
|
||
**단점**:
|
||
- 데이터 동기화 복잡
|
||
- 삭제/추가 시 다른 모드에도 영향
|
||
- 순서 변경 시 충돌 가능
|
||
|
||
---
|
||
|
||
### 방안 B: visibility 속성 (표시/숨김) ✅ 채택
|
||
```typescript
|
||
interface PopComponentDefinitionV4 {
|
||
visibility?: {
|
||
tablet_landscape?: boolean;
|
||
tablet_portrait?: boolean;
|
||
mobile_landscape?: boolean;
|
||
mobile_portrait?: boolean;
|
||
};
|
||
}
|
||
```
|
||
|
||
**장점**:
|
||
- 단순하고 명확
|
||
- 컴포넌트는 항상 존재 (숨김만)
|
||
- 데이터 일관성 유지
|
||
|
||
**단점**:
|
||
- 완전히 다른 컴포넌트 추가는 불가능
|
||
- 많은 모드 전용 컴포넌트는 비효율적
|
||
|
||
---
|
||
|
||
### 최종 결정: 하이브리드 접근 ⭐
|
||
```typescript
|
||
1. visibility: 기본 기능 (Phase 3)
|
||
- 간단한 표시/숨김
|
||
- 줄바꿈 컴포넌트 제어
|
||
|
||
2. components 오버라이드: 고급 기능 (Phase 3 기반)
|
||
- 컴포넌트 설정 변경 (리스트 컬럼 수 등)
|
||
- 스타일 변경
|
||
|
||
3. children 오버라이드: 추후 고려
|
||
- 모드별 완전히 다른 구성 필요 시
|
||
```
|
||
|
||
---
|
||
|
||
## 🛠️ 구현 내용
|
||
|
||
### 1. 타입 정의 확장
|
||
|
||
#### pop-break 컴포넌트 추가
|
||
```typescript
|
||
export type PopComponentType =
|
||
| "pop-field"
|
||
| "pop-button"
|
||
| "pop-list"
|
||
| "pop-indicator"
|
||
| "pop-scanner"
|
||
| "pop-numpad"
|
||
| "pop-spacer"
|
||
| "pop-break"; // 🆕 줄바꿈
|
||
```
|
||
|
||
#### visibility 속성 추가
|
||
```typescript
|
||
export interface PopComponentDefinitionV4 {
|
||
id: string;
|
||
type: PopComponentType;
|
||
size: PopSizeConstraintV4;
|
||
|
||
// 🆕 모드별 표시/숨김
|
||
visibility?: {
|
||
tablet_landscape?: boolean; // undefined = true (기본 표시)
|
||
tablet_portrait?: boolean;
|
||
mobile_landscape?: boolean;
|
||
mobile_portrait?: boolean;
|
||
};
|
||
|
||
// 기존: 픽셀 기반 반응형
|
||
hideBelow?: number;
|
||
|
||
// 기타...
|
||
}
|
||
```
|
||
|
||
#### 기본 크기 설정
|
||
```typescript
|
||
const defaultSizes: Record<PopComponentType, PopSizeConstraintV4> = {
|
||
// ...
|
||
"pop-break": {
|
||
width: "fill", // 100% 너비 (flex-basis: 100%)
|
||
height: "fixed",
|
||
fixedHeight: 0, // 높이 0 (보이지 않음)
|
||
},
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 렌더러 로직 개선
|
||
|
||
#### visibility 체크 함수
|
||
```typescript
|
||
const isComponentVisible = (component: PopComponentDefinitionV4): boolean => {
|
||
if (!component.visibility) return true; // 기본값: 표시
|
||
|
||
const modeVisibility = component.visibility[currentMode];
|
||
return modeVisibility !== false; // undefined도 true로 취급
|
||
};
|
||
```
|
||
|
||
**로직 설명**:
|
||
- `visibility` 속성이 없으면 → 모든 모드에서 표시
|
||
- `visibility.mobile_portrait === false` → 모바일 세로에서 숨김
|
||
- `visibility.mobile_portrait === undefined` → 모바일 세로에서 표시 (기본값)
|
||
|
||
---
|
||
|
||
#### 컴포넌트 오버라이드 병합
|
||
```typescript
|
||
const getMergedComponent = (
|
||
baseComponent: PopComponentDefinitionV4
|
||
): PopComponentDefinitionV4 => {
|
||
if (currentMode === "tablet_landscape") return baseComponent;
|
||
|
||
const componentOverride = overrides?.[currentMode]?.components?.[baseComponent.id];
|
||
if (!componentOverride) return baseComponent;
|
||
|
||
// 깊은 병합 (config, size)
|
||
return {
|
||
...baseComponent,
|
||
...componentOverride,
|
||
size: { ...baseComponent.size, ...componentOverride.size },
|
||
config: { ...baseComponent.config, ...componentOverride.config },
|
||
};
|
||
};
|
||
```
|
||
|
||
**병합 우선순위**:
|
||
1. `baseComponent` (기본값)
|
||
2. `overrides[currentMode].components[id]` (모드별 오버라이드)
|
||
3. 중첩 객체는 깊은 병합 (`size`, `config`)
|
||
|
||
**확장 가능성**:
|
||
- 리스트 컬럼 수 변경
|
||
- 버튼 스타일 변경
|
||
- 필드 표시 형식 변경
|
||
|
||
---
|
||
|
||
#### pop-break 전용 렌더링
|
||
```typescript
|
||
// pop-break 특수 처리
|
||
if (mergedComponent.type === "pop-break") {
|
||
return (
|
||
<div
|
||
key={componentId}
|
||
className={cn(
|
||
"w-full",
|
||
isDesignMode
|
||
? "h-4 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-gray-400"
|
||
: "h-0"
|
||
)}
|
||
style={{ flexBasis: "100%" }} // 핵심: 100% 너비로 줄바꿈 강제
|
||
onClick={() => onComponentClick?.(componentId)}
|
||
>
|
||
{isDesignMode && (
|
||
<span className="text-xs text-gray-400">줄바꿈</span>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
**동작 방식**:
|
||
- `flex-basis: 100%` → 컨테이너 전체 너비 차지
|
||
- 다음 컴포넌트는 자동으로 새 줄로 이동
|
||
- 디자인 모드: 점선 표시 (높이 16px)
|
||
- 실제 화면: 높이 0 (안 보임)
|
||
|
||
---
|
||
|
||
### 3. 삭제 함수 개선
|
||
|
||
#### 오버라이드 정리 로직
|
||
```typescript
|
||
export const removeComponentFromV4Layout = (
|
||
layout: PopLayoutDataV4,
|
||
componentId: string
|
||
): PopLayoutDataV4 => {
|
||
// 1. 컴포넌트 정의 삭제
|
||
const { [componentId]: _, ...remainingComponents } = layout.components;
|
||
|
||
// 2. root.children에서 제거
|
||
const newRoot = removeChildFromContainer(layout.root, componentId);
|
||
|
||
// 3. 🆕 모든 오버라이드에서 제거
|
||
const newOverrides = cleanupOverridesAfterDelete(layout.overrides, componentId);
|
||
|
||
return {
|
||
...layout,
|
||
root: newRoot,
|
||
components: remainingComponents,
|
||
overrides: newOverrides,
|
||
};
|
||
};
|
||
```
|
||
|
||
#### 오버라이드 정리 상세
|
||
```typescript
|
||
function cleanupOverridesAfterDelete(
|
||
overrides: PopLayoutDataV4["overrides"],
|
||
componentId: string
|
||
): PopLayoutDataV4["overrides"] {
|
||
if (!overrides) return undefined;
|
||
|
||
const newOverrides = { ...overrides };
|
||
|
||
for (const mode of Object.keys(newOverrides)) {
|
||
const override = newOverrides[mode];
|
||
if (!override) continue;
|
||
|
||
const updated = { ...override };
|
||
|
||
// containers.root.children에서 제거
|
||
if (updated.containers?.root?.children) {
|
||
updated.containers = {
|
||
...updated.containers,
|
||
root: {
|
||
...updated.containers.root,
|
||
children: updated.containers.root.children.filter(id => id !== componentId),
|
||
},
|
||
};
|
||
}
|
||
|
||
// components에서 제거
|
||
if (updated.components?.[componentId]) {
|
||
const { [componentId]: _, ...rest } = updated.components;
|
||
updated.components = Object.keys(rest).length > 0 ? rest : undefined;
|
||
}
|
||
|
||
// 빈 오버라이드 정리
|
||
if (!updated.containers && !updated.components) {
|
||
delete newOverrides[mode];
|
||
} else {
|
||
newOverrides[mode] = updated;
|
||
}
|
||
}
|
||
|
||
// 모든 오버라이드가 비었으면 undefined 반환
|
||
return Object.keys(newOverrides).length > 0 ? newOverrides : undefined;
|
||
}
|
||
```
|
||
|
||
**정리 항목**:
|
||
1. `overrides[mode].containers.root.children` - 컴포넌트 ID 제거
|
||
2. `overrides[mode].components[componentId]` - 컴포넌트 설정 제거
|
||
3. 빈 오버라이드 객체 삭제 (메모리 절약)
|
||
|
||
---
|
||
|
||
### 4. 속성 패널 UI
|
||
|
||
#### "표시" 탭 추가
|
||
```typescript
|
||
<TabsList>
|
||
<TabsTrigger value="size">크기</TabsTrigger>
|
||
<TabsTrigger value="settings">설정</TabsTrigger>
|
||
<TabsTrigger value="visibility">
|
||
<Eye className="h-3 w-3" />
|
||
표시
|
||
</TabsTrigger>
|
||
<TabsTrigger value="data">데이터</TabsTrigger>
|
||
</TabsList>
|
||
```
|
||
|
||
#### VisibilityForm 컴포넌트
|
||
```typescript
|
||
function VisibilityForm({ component, onUpdate }: VisibilityFormProps) {
|
||
const modes = [
|
||
{ key: "tablet_landscape", label: "태블릿 가로 (1024×768)" },
|
||
{ key: "tablet_portrait", label: "태블릿 세로 (768×1024)" },
|
||
{ key: "mobile_landscape", label: "모바일 가로 (667×375)" },
|
||
{ key: "mobile_portrait", label: "모바일 세로 (375×667)" },
|
||
];
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<Label>모드별 표시 설정</Label>
|
||
<p className="text-xs text-muted-foreground">
|
||
체크 해제하면 해당 모드에서 컴포넌트가 숨겨집니다
|
||
</p>
|
||
|
||
<div className="space-y-2 rounded-lg border p-3">
|
||
{modes.map(({ key, label }) => {
|
||
const isChecked = component.visibility?.[key] !== false;
|
||
|
||
return (
|
||
<div key={key} className="flex items-center gap-2">
|
||
<input
|
||
type="checkbox"
|
||
checked={isChecked}
|
||
onChange={(e) => {
|
||
onUpdate?.({
|
||
visibility: {
|
||
...component.visibility,
|
||
[key]: e.target.checked,
|
||
},
|
||
});
|
||
}}
|
||
/>
|
||
<label>{label}</label>
|
||
{!isChecked && <span>(숨김)</span>}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* 기존: 반응형 숨김 (픽셀 기반) */}
|
||
<div className="space-y-3">
|
||
<Label>반응형 숨김 (픽셀 기반)</Label>
|
||
<Input
|
||
type="number"
|
||
value={component.hideBelow || ""}
|
||
onChange={(e) =>
|
||
onUpdate?.({
|
||
hideBelow: e.target.value ? Number(e.target.value) : undefined,
|
||
})
|
||
}
|
||
placeholder="없음"
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
예: 500 입력 시 화면 너비가 500px 이하면 자동 숨김
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
**UI 특징**:
|
||
- 체크박스로 직관적인 표시/숨김 제어
|
||
- 기본값은 모든 모드 체크 (표시)
|
||
- `hideBelow` (픽셀 기반)와 별도 유지
|
||
|
||
---
|
||
|
||
### 5. 팔레트 업데이트
|
||
|
||
```typescript
|
||
const COMPONENT_PALETTE = [
|
||
// ... 기존 컴포넌트들
|
||
{
|
||
type: "pop-break",
|
||
label: "줄바꿈",
|
||
icon: WrapText,
|
||
description: "강제 줄바꿈 (flex-basis: 100%)",
|
||
},
|
||
];
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 사용 예시
|
||
|
||
### 예시 1: 모바일 전용 버튼
|
||
```typescript
|
||
{
|
||
id: "call-button",
|
||
type: "pop-button",
|
||
label: "전화 걸기",
|
||
size: { width: "fixed", height: "fixed", fixedWidth: 120, fixedHeight: 48 },
|
||
visibility: {
|
||
tablet_landscape: false, // 태블릿 가로: 숨김
|
||
tablet_portrait: false, // 태블릿 세로: 숨김
|
||
mobile_landscape: true, // 모바일 가로: 표시
|
||
mobile_portrait: true, // 모바일 세로: 표시
|
||
},
|
||
}
|
||
```
|
||
|
||
**결과**:
|
||
- 태블릿: "전화 걸기" 버튼 안 보임
|
||
- 모바일: "전화 걸기" 버튼 보임
|
||
|
||
---
|
||
|
||
### 예시 2: 모드별 줄바꿈
|
||
```typescript
|
||
레이아웃:
|
||
[필드A] [필드B] [줄바꿈] [필드C] [필드D]
|
||
|
||
줄바꿈 컴포넌트 설정:
|
||
{
|
||
id: "break-1",
|
||
type: "pop-break",
|
||
visibility: {
|
||
tablet_landscape: false, // 태블릿: 줄바꿈 숨김 (한 줄)
|
||
mobile_portrait: true, // 모바일: 줄바꿈 표시 (두 줄)
|
||
},
|
||
}
|
||
```
|
||
|
||
**결과**:
|
||
```
|
||
태블릿 가로 (1024px):
|
||
┌─────────────────────────────────┐
|
||
│ [필드A] [필드B] [필드C] [필드D] │ ← 한 줄
|
||
└─────────────────────────────────┘
|
||
|
||
모바일 세로 (375px):
|
||
┌─────────────────┐
|
||
│ [필드A] [필드B] │ ← 첫 줄
|
||
│ [필드C] [필드D] │ ← 둘째 줄 (줄바꿈 적용)
|
||
└─────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
### 예시 3: 리스트 컬럼 수 변경 (확장 가능)
|
||
```typescript
|
||
// 기본 (태블릿 가로)
|
||
{
|
||
id: "product-list",
|
||
type: "pop-list",
|
||
config: {
|
||
columns: 7, // 7개 컬럼
|
||
}
|
||
}
|
||
|
||
// 오버라이드 (모바일 세로)
|
||
overrides: {
|
||
mobile_portrait: {
|
||
components: {
|
||
"product-list": {
|
||
config: {
|
||
columns: 3, // 3개 컬럼
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**결과**:
|
||
- 태블릿: 7개 컬럼 표시
|
||
- 모바일: 3개 컬럼 표시 (병합됨)
|
||
|
||
---
|
||
|
||
## ✅ 테스트 시나리오
|
||
|
||
### 테스트 1: 줄바꿈 기본 동작
|
||
```
|
||
1. 팔레트에서 "줄바꿈" 드래그
|
||
2. [A] [B] [C] 사이에 드롭
|
||
3. 예상 결과: [A] [B] / [C]
|
||
4. 디자인 모드에서 점선 "줄바꿈" 표시 확인
|
||
5. 미리보기에서 줄바꿈이 안 보이는지 확인
|
||
```
|
||
|
||
### 테스트 2: 모드별 줄바꿈 표시
|
||
```
|
||
1. 줄바꿈 컴포넌트 추가
|
||
2. "표시" 탭 → 태블릿 모드 체크 해제
|
||
3. 태블릿 가로 모드: [A] [B] [C] (한 줄)
|
||
4. 모바일 세로 모드: [A] [B] / [C] (두 줄)
|
||
```
|
||
|
||
### 테스트 3: 컴포넌트 삭제 시 오버라이드 정리
|
||
```
|
||
1. 모바일 세로 모드에서 배치 고정
|
||
2. 컴포넌트 삭제
|
||
3. 저장 후 로드
|
||
4. DB 확인: overrides에서도 제거되었는지
|
||
```
|
||
|
||
### 테스트 4: 모드별 컴포넌트 숨김
|
||
```
|
||
1. "전화 걸기" 버튼 추가
|
||
2. "표시" 탭 → 태블릿 모드 체크 해제
|
||
3. 태블릿 가로: 버튼 안 보임
|
||
4. 모바일 세로: 버튼 보임
|
||
```
|
||
|
||
### 테스트 5: 속성 패널 UI
|
||
```
|
||
1. 컴포넌트 선택
|
||
2. "표시" 탭 클릭
|
||
3. 4개 체크박스 확인 (모두 체크됨)
|
||
4. 체크 해제 시 "(숨김)" 표시 확인
|
||
5. 저장 후 로드 → 체크 상태 유지
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 기술적 고려사항
|
||
|
||
### 1. 데이터 일관성
|
||
```
|
||
문제: 컴포넌트 삭제 시 오버라이드 잔여물
|
||
|
||
해결:
|
||
- cleanupOverridesAfterDelete() 함수
|
||
- containers.root.children 정리
|
||
- components 오버라이드 정리
|
||
- 빈 오버라이드 자동 삭제
|
||
```
|
||
|
||
### 2. 병합 우선순위
|
||
```
|
||
우선순위 (높음 → 낮음):
|
||
1. tempLayout (고정 전 미리보기)
|
||
2. overrides[currentMode].containers.root
|
||
3. overrides[currentMode].components[id]
|
||
4. layout.root (기본값)
|
||
5. layout.components[id] (기본값)
|
||
```
|
||
|
||
### 3. 성능 최적화
|
||
```typescript
|
||
// useMemo로 병합 결과 캐싱
|
||
const effectiveRoot = useMemo(() => getMergedRoot(), [tempLayout, overrides, currentMode]);
|
||
const mergedComponent = useMemo(() => getMergedComponent(baseComponent), [baseComponent, overrides, currentMode]);
|
||
```
|
||
|
||
### 4. 타입 안전성
|
||
```typescript
|
||
// visibility 키는 ViewportPreset에서만 허용
|
||
visibility?: {
|
||
[K in ViewportPreset]?: boolean;
|
||
};
|
||
|
||
// 컴파일 타임에 오타 방지
|
||
visibility.tablet_landspace = false; // ❌ 오타 감지!
|
||
visibility.tablet_landscape = false; // ✅ 정상
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 영향 받는 파일
|
||
|
||
### 코드 파일
|
||
```
|
||
✅ frontend/components/pop/designer/types/pop-layout.ts
|
||
- PopComponentType 확장 (pop-break)
|
||
- PopComponentDefinitionV4.visibility 추가
|
||
- cleanupOverridesAfterDelete() 추가
|
||
|
||
✅ frontend/components/pop/designer/renderers/PopFlexRenderer.tsx
|
||
- isComponentVisible() 추가
|
||
- getMergedComponent() 추가
|
||
- pop-break 렌더링 추가
|
||
- ContainerRenderer props 확장
|
||
|
||
✅ frontend/components/pop/designer/panels/ComponentEditorPanelV4.tsx
|
||
- "표시" 탭 추가
|
||
- VisibilityForm 컴포넌트 추가
|
||
- COMPONENT_TYPE_LABELS 업데이트
|
||
|
||
✅ frontend/components/pop/designer/panels/ComponentPaletteV4.tsx
|
||
- "줄바꿈" 컴포넌트 추가
|
||
```
|
||
|
||
### 문서 파일
|
||
```
|
||
✅ popdocs/CHANGELOG.md
|
||
- Phase 3 완료 기록
|
||
|
||
✅ popdocs/PLAN.md
|
||
- Phase 3 체크 완료
|
||
- Phase 4 계획 추가
|
||
|
||
✅ popdocs/decisions/002-phase3-visibility-break.md (이 문서)
|
||
- 설계 결정 및 구현 상세
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 다음 단계
|
||
|
||
### Phase 4: 실제 컴포넌트 구현
|
||
```
|
||
우선순위:
|
||
1. pop-field (입력/표시 필드)
|
||
2. pop-button (액션 버튼)
|
||
3. pop-list (데이터 리스트)
|
||
4. pop-indicator (KPI 표시)
|
||
5. pop-scanner (바코드/QR)
|
||
6. pop-numpad (숫자 입력)
|
||
```
|
||
|
||
### 추가 개선 사항
|
||
```
|
||
1. 컴포넌트 오버라이드 UI
|
||
- 리스트 컬럼 수 조정
|
||
- 버튼 스타일 변경
|
||
- 필드 표시 형식 변경
|
||
|
||
2. "모든 모드에 적용" 기능
|
||
- 한 번에 모든 모드 체크/해제
|
||
|
||
3. 오버라이드 비교 뷰
|
||
- 기본값 vs 오버라이드 차이 표시
|
||
```
|
||
|
||
---
|
||
|
||
## 📝 결론
|
||
|
||
Phase 3를 통해 다음을 달성:
|
||
1. ✅ 모드별 컴포넌트 표시/숨김 제어
|
||
2. ✅ 강제 줄바꿈 컴포넌트 (Flexbox 한계 극복)
|
||
3. ✅ 컴포넌트 오버라이드 병합 (확장성 확보)
|
||
4. ✅ 데이터 일관성 유지 (삭제 시 정리)
|
||
|
||
이제 v4 레이아웃 시스템의 핵심 기능이 완성되었으며, 실제 컴포넌트 구현 단계로 진행할 수 있습니다.
|