# 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 = { // ... "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 (
onComponentClick?.(componentId)} > {isDesignMode && ( 줄바꿈 )}
); } ``` **동작 방식**: - `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 크기 설정 표시 데이터 ``` #### 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 (

체크 해제하면 해당 모드에서 컴포넌트가 숨겨집니다

{modes.map(({ key, label }) => { const isChecked = component.visibility?.[key] !== false; return (
{ onUpdate?.({ visibility: { ...component.visibility, [key]: e.target.checked, }, }); }} /> {!isChecked && (숨김)}
); })}
{/* 기존: 반응형 숨김 (픽셀 기반) */}
onUpdate?.({ hideBelow: e.target.value ? Number(e.target.value) : undefined, }) } placeholder="없음" />

예: 500 입력 시 화면 너비가 500px 이하면 자동 숨김

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