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 레이아웃 시스템의 핵심 기능이 완성되었으며, 실제 컴포넌트 구현 단계로 진행할 수 있습니다.
|