feat(pop): - 모드별 컴포넌트 표시/숨김 및 줄바꿈 기능 추가
visibility 속성으로 모드별 컴포넌트 표시/숨김 제어 pop-break 컴포넌트로 Flexbox 강제 줄바꿈 지원 (flex-basis: 100%) 컴포넌트 오버라이드 병합 로직 추가 (모드별 설정 변경 가능) 삭제 시 오버라이드 자동 정리 로직 구현 속성 패널에 "표시" 탭 추가 (체크박스 UI) 팔레트에 "줄바꿈" 컴포넌트 추가 popdocs 문서 정리 (PHASE3_SUMMARY, decisions/002, 기존 문서 업데이트)
This commit is contained in:
parent
760e545444
commit
5f23c13490
|
|
@ -11,7 +11,7 @@ import {
|
|||
PopSizeConstraintV4,
|
||||
} from "./types/pop-layout";
|
||||
import { DND_ITEM_TYPES, DragItemComponent } from "./panels/PopPanel";
|
||||
import { ZoomIn, ZoomOut, Maximize2, Smartphone, Tablet, RotateCcw, RotateCw } from "lucide-react";
|
||||
import { ZoomIn, ZoomOut, Maximize2, Smartphone, Tablet, RotateCcw, Lock } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PopFlexRenderer } from "./renderers/PopFlexRenderer";
|
||||
|
||||
|
|
@ -37,6 +37,9 @@ interface PopCanvasV4Props {
|
|||
layout: PopLayoutDataV4;
|
||||
selectedComponentId: string | null;
|
||||
selectedContainerId: string | null;
|
||||
currentMode: ViewportPreset; // 현재 모드
|
||||
tempLayout?: PopContainerV4 | null; // 임시 레이아웃 (고정 전 미리보기)
|
||||
onModeChange: (mode: ViewportPreset) => void; // 모드 변경
|
||||
onSelectComponent: (id: string | null) => void;
|
||||
onSelectContainer: (id: string | null) => void;
|
||||
onDropComponent: (type: PopComponentType, containerId: string) => void;
|
||||
|
|
@ -45,6 +48,8 @@ interface PopCanvasV4Props {
|
|||
onDeleteComponent: (componentId: string) => void;
|
||||
onResizeComponent?: (componentId: string, size: Partial<PopSizeConstraintV4>) => void;
|
||||
onReorderComponent?: (containerId: string, fromIndex: number, toIndex: number) => void;
|
||||
onLockLayout?: () => void; // 배치 고정
|
||||
onResetOverride?: (mode: ViewportPreset) => void; // 오버라이드 초기화
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
|
@ -58,6 +63,9 @@ export function PopCanvasV4({
|
|||
layout,
|
||||
selectedComponentId,
|
||||
selectedContainerId,
|
||||
currentMode,
|
||||
tempLayout,
|
||||
onModeChange,
|
||||
onSelectComponent,
|
||||
onSelectContainer,
|
||||
onDropComponent,
|
||||
|
|
@ -66,13 +74,12 @@ export function PopCanvasV4({
|
|||
onDeleteComponent,
|
||||
onResizeComponent,
|
||||
onReorderComponent,
|
||||
onLockLayout,
|
||||
onResetOverride,
|
||||
}: PopCanvasV4Props) {
|
||||
// 줌 상태
|
||||
const [canvasScale, setCanvasScale] = useState(0.8);
|
||||
|
||||
// 현재 뷰포트 프리셋 (기본: 태블릿 가로)
|
||||
const [activeViewport, setActiveViewport] = useState<ViewportPreset>(DEFAULT_PRESET);
|
||||
|
||||
// 커스텀 뷰포트 크기 (슬라이더)
|
||||
const [customWidth, setCustomWidth] = useState(1024);
|
||||
const [customHeight, setCustomHeight] = useState(768);
|
||||
|
|
@ -85,7 +92,7 @@ export function PopCanvasV4({
|
|||
const dropRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 현재 뷰포트 해상도
|
||||
const currentPreset = VIEWPORT_PRESETS.find((p) => p.id === activeViewport)!;
|
||||
const currentPreset = VIEWPORT_PRESETS.find((p) => p.id === currentMode)!;
|
||||
const viewportWidth = customWidth;
|
||||
const viewportHeight = customHeight;
|
||||
|
||||
|
|
@ -96,7 +103,7 @@ export function PopCanvasV4({
|
|||
|
||||
// 뷰포트 프리셋 변경
|
||||
const handleViewportChange = (preset: ViewportPreset) => {
|
||||
setActiveViewport(preset);
|
||||
onModeChange(preset); // 부모에게 알림
|
||||
const presetData = VIEWPORT_PRESETS.find((p) => p.id === preset)!;
|
||||
setCustomWidth(presetData.width);
|
||||
setCustomHeight(presetData.height);
|
||||
|
|
@ -176,6 +183,20 @@ export function PopCanvasV4({
|
|||
|
||||
drop(dropRef);
|
||||
|
||||
// 오버라이드 상태 확인
|
||||
const hasOverride = (mode: ViewportPreset): boolean => {
|
||||
if (mode === DEFAULT_PRESET) return false; // 기본 모드는 오버라이드 없음
|
||||
|
||||
const override = layout.overrides?.[mode as keyof typeof layout.overrides];
|
||||
if (!override) return false;
|
||||
|
||||
// 컴포넌트 또는 컨테이너 오버라이드가 있으면 true
|
||||
const hasComponentOverrides = override.components && Object.keys(override.components).length > 0;
|
||||
const hasContainerOverrides = override.containers && Object.keys(override.containers).length > 0;
|
||||
|
||||
return !!(hasComponentOverrides || hasContainerOverrides);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full flex-col bg-gray-100">
|
||||
{/* 툴바 */}
|
||||
|
|
@ -185,16 +206,19 @@ export function PopCanvasV4({
|
|||
<span className="text-xs text-muted-foreground mr-2">미리보기:</span>
|
||||
{VIEWPORT_PRESETS.map((preset) => {
|
||||
const Icon = preset.icon;
|
||||
const isActive = activeViewport === preset.id;
|
||||
const isActive = currentMode === preset.id;
|
||||
const isDefault = preset.id === DEFAULT_PRESET;
|
||||
const isEdited = hasOverride(preset.id);
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={preset.id}
|
||||
variant={isActive ? "default" : "outline"}
|
||||
variant={isActive ? "default" : isEdited ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-8 gap-1 text-xs",
|
||||
isDefault && !isActive && "border-primary/50"
|
||||
isDefault && !isActive && "border-primary/50",
|
||||
isEdited && !isActive && "border-yellow-500 bg-yellow-50 hover:bg-yellow-100"
|
||||
)}
|
||||
onClick={() => handleViewportChange(preset.id)}
|
||||
title={preset.label}
|
||||
|
|
@ -204,11 +228,42 @@ export function PopCanvasV4({
|
|||
{isDefault && (
|
||||
<span className="text-[10px] text-muted-foreground ml-1">(기본)</span>
|
||||
)}
|
||||
{isEdited && (
|
||||
<span className="text-[10px] text-yellow-700 ml-1 font-medium">(편집)</span>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 고정 버튼 (기본 모드가 아닐 때 표시) */}
|
||||
{currentMode !== DEFAULT_PRESET && onLockLayout && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 gap-1 text-xs"
|
||||
onClick={onLockLayout}
|
||||
title="현재 배치를 이 모드 전용으로 고정합니다"
|
||||
>
|
||||
<Lock className="h-3 w-3" />
|
||||
<span>고정</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 오버라이드 초기화 버튼 (편집된 모드에만 표시) */}
|
||||
{hasOverride(currentMode) && onResetOverride && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 gap-1 text-xs text-yellow-700 border-yellow-500 hover:bg-yellow-50"
|
||||
onClick={() => onResetOverride(currentMode)}
|
||||
title="이 모드의 편집 내용을 삭제하고 기본 규칙으로 되돌립니다"
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
<span>자동으로 되돌리기</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 줌 컨트롤 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
|
|
@ -291,6 +346,8 @@ export function PopCanvasV4({
|
|||
<PopFlexRenderer
|
||||
layout={layout}
|
||||
viewportWidth={viewportWidth}
|
||||
currentMode={currentMode}
|
||||
tempLayout={tempLayout}
|
||||
isDesignMode={true}
|
||||
selectedComponentId={selectedComponentId}
|
||||
onComponentClick={onSelectComponent}
|
||||
|
|
|
|||
|
|
@ -179,6 +179,15 @@ export default function PopDesigner({
|
|||
const [activeDevice, setActiveDevice] = useState<DeviceType>("tablet");
|
||||
const [activeModeKey, setActiveModeKey] = useState<PopLayoutModeKey>("tablet_landscape");
|
||||
|
||||
// ========================================
|
||||
// v4용 뷰포트 모드 상태
|
||||
// ========================================
|
||||
type ViewportMode = "mobile_portrait" | "mobile_landscape" | "tablet_portrait" | "tablet_landscape";
|
||||
const [currentViewportMode, setCurrentViewportMode] = useState<ViewportMode>("tablet_landscape");
|
||||
|
||||
// v4: 임시 레이아웃 (고정 전 배치) - 다른 모드에서만 사용
|
||||
const [tempLayout, setTempLayout] = useState<PopContainerV4 | null>(null);
|
||||
|
||||
// ========================================
|
||||
// 선택 상태
|
||||
// ========================================
|
||||
|
|
@ -354,6 +363,8 @@ export default function PopDesigner({
|
|||
|
||||
const handleUpdateContainerV4 = useCallback(
|
||||
(containerId: string, updates: Partial<PopContainerV4>) => {
|
||||
if (currentViewportMode === "tablet_landscape") {
|
||||
// 기본 모드 (태블릿 가로) → root 직접 수정 ✅
|
||||
const newLayout = {
|
||||
...layoutV4,
|
||||
root: updateContainerV4(layoutV4.root, containerId, updates),
|
||||
|
|
@ -361,8 +372,14 @@ export default function PopDesigner({
|
|||
setLayoutV4(newLayout);
|
||||
saveToHistoryV4(newLayout);
|
||||
setHasChanges(true);
|
||||
console.log("[기본 모드] root 컨테이너 수정");
|
||||
} else {
|
||||
// 다른 모드 → 속성 패널에서 수정 차단됨 (UI에서 비활성화)
|
||||
toast.warning("기본 모드(태블릿 가로)에서만 속성을 변경할 수 있습니다");
|
||||
console.log("[다른 모드] 속성 수정 차단");
|
||||
}
|
||||
},
|
||||
[layoutV4, saveToHistoryV4]
|
||||
[layoutV4, currentViewportMode, saveToHistoryV4]
|
||||
);
|
||||
|
||||
const handleDeleteComponentV4 = useCallback((componentId: string) => {
|
||||
|
|
@ -374,6 +391,70 @@ export default function PopDesigner({
|
|||
console.log("[V4] 컴포넌트 삭제, 히스토리 저장됨");
|
||||
}, [layoutV4, saveToHistoryV4]);
|
||||
|
||||
// v4: 현재 모드 배치 고정 (오버라이드 저장) 🔥
|
||||
const handleLockLayoutV4 = useCallback(() => {
|
||||
if (currentViewportMode === "tablet_landscape") {
|
||||
toast.info("기본 모드는 고정할 필요가 없습니다");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tempLayout) {
|
||||
toast.info("변경사항이 없습니다");
|
||||
return;
|
||||
}
|
||||
|
||||
// 임시 레이아웃을 오버라이드에 저장 ✅
|
||||
const newLayout = {
|
||||
...layoutV4,
|
||||
overrides: {
|
||||
...layoutV4.overrides,
|
||||
[currentViewportMode]: {
|
||||
...layoutV4.overrides?.[currentViewportMode as keyof typeof layoutV4.overrides],
|
||||
containers: {
|
||||
root: {
|
||||
direction: tempLayout.direction,
|
||||
wrap: tempLayout.wrap,
|
||||
gap: tempLayout.gap,
|
||||
alignItems: tempLayout.alignItems,
|
||||
justifyContent: tempLayout.justifyContent,
|
||||
padding: tempLayout.padding,
|
||||
children: tempLayout.children, // 순서 고정
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setLayoutV4(newLayout);
|
||||
saveToHistoryV4(newLayout);
|
||||
setTempLayout(null); // 임시 레이아웃 초기화
|
||||
setHasChanges(true);
|
||||
toast.success(`${currentViewportMode} 모드 배치가 고정되었습니다`);
|
||||
console.log(`[V4] ${currentViewportMode} 배치 고정됨 (tempLayout → overrides)`);
|
||||
}, [layoutV4, currentViewportMode, tempLayout, saveToHistoryV4]);
|
||||
|
||||
// v4: 오버라이드 초기화 (자동 계산으로 되돌리기)
|
||||
const handleResetOverrideV4 = useCallback((mode: ViewportMode) => {
|
||||
if (mode === "tablet_landscape") {
|
||||
toast.info("기본 모드는 초기화할 수 없습니다");
|
||||
return;
|
||||
}
|
||||
|
||||
const newOverrides = { ...layoutV4.overrides };
|
||||
delete newOverrides[mode as keyof typeof newOverrides];
|
||||
|
||||
const newLayout = {
|
||||
...layoutV4,
|
||||
overrides: Object.keys(newOverrides).length > 0 ? newOverrides : undefined
|
||||
};
|
||||
|
||||
setLayoutV4(newLayout);
|
||||
saveToHistoryV4(newLayout);
|
||||
setHasChanges(true);
|
||||
toast.success(`${mode} 모드 오버라이드가 초기화되었습니다`);
|
||||
console.log(`[V4] ${mode} 오버라이드 초기화됨`);
|
||||
}, [layoutV4, saveToHistoryV4]);
|
||||
|
||||
// v4: 컴포넌트 크기 조정 (드래그) - 리사이즈 중에는 히스토리 저장 안 함
|
||||
// 리사이즈 완료 시 별도로 저장해야 함 (TODO: 드래그 종료 시 저장)
|
||||
const handleResizeComponentV4 = useCallback(
|
||||
|
|
@ -426,6 +507,8 @@ export default function PopDesigner({
|
|||
};
|
||||
};
|
||||
|
||||
if (currentViewportMode === "tablet_landscape") {
|
||||
// 기본 모드 → root 직접 수정 ✅
|
||||
const newLayout = {
|
||||
...layoutV4,
|
||||
root: reorderInContainer(layoutV4.root),
|
||||
|
|
@ -433,9 +516,16 @@ export default function PopDesigner({
|
|||
setLayoutV4(newLayout);
|
||||
saveToHistoryV4(newLayout);
|
||||
setHasChanges(true);
|
||||
console.log("[V4] 컴포넌트 순서 변경", { containerId, fromIndex, toIndex });
|
||||
console.log("[기본 모드] 컴포넌트 순서 변경 (root 저장)");
|
||||
} else {
|
||||
// 다른 모드 → 임시 레이아웃에만 저장 (화면에만 표시, layoutV4는 안 건드림) 🔥
|
||||
const reorderedRoot = reorderInContainer(layoutV4.root);
|
||||
setTempLayout(reorderedRoot);
|
||||
console.log(`[${currentViewportMode}] 컴포넌트 순서 변경 (임시, 고정 필요)`);
|
||||
toast.info("배치 변경됨. '고정' 버튼을 클릭하여 저장하세요", { duration: 2000 });
|
||||
}
|
||||
},
|
||||
[layoutV4, saveToHistoryV4]
|
||||
[layoutV4, currentViewportMode, saveToHistoryV4]
|
||||
);
|
||||
|
||||
// ========================================
|
||||
|
|
@ -663,6 +753,9 @@ export default function PopDesigner({
|
|||
layout={layoutV4}
|
||||
selectedComponentId={selectedComponentId}
|
||||
selectedContainerId={selectedContainerId}
|
||||
currentMode={currentViewportMode}
|
||||
tempLayout={tempLayout}
|
||||
onModeChange={setCurrentViewportMode}
|
||||
onSelectComponent={setSelectedComponentId}
|
||||
onSelectContainer={setSelectedContainerId}
|
||||
onDropComponent={handleDropComponentV4}
|
||||
|
|
@ -671,6 +764,8 @@ export default function PopDesigner({
|
|||
onDeleteComponent={handleDeleteComponentV4}
|
||||
onResizeComponent={handleResizeComponentV4}
|
||||
onReorderComponent={handleReorderComponentV4}
|
||||
onLockLayout={handleLockLayoutV4}
|
||||
onResetOverride={handleResetOverrideV4}
|
||||
/>
|
||||
)}
|
||||
</ResizablePanel>
|
||||
|
|
@ -683,6 +778,7 @@ export default function PopDesigner({
|
|||
<ComponentEditorPanelV4
|
||||
component={selectedComponentV4}
|
||||
container={selectedContainer}
|
||||
currentViewportMode={currentViewportMode}
|
||||
onUpdateComponent={
|
||||
selectedComponentId
|
||||
? (updates) => handleUpdateComponentV4(selectedComponentId, updates)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
Square,
|
||||
Maximize2,
|
||||
AlignCenter,
|
||||
Eye,
|
||||
} from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -39,6 +40,8 @@ interface ComponentEditorPanelV4Props {
|
|||
component: PopComponentDefinitionV4 | null;
|
||||
/** 선택된 컨테이너 */
|
||||
container: PopContainerV4 | null;
|
||||
/** 현재 뷰포트 모드 */
|
||||
currentViewportMode?: "mobile_portrait" | "mobile_landscape" | "tablet_portrait" | "tablet_landscape";
|
||||
/** 컴포넌트 업데이트 */
|
||||
onUpdateComponent?: (updates: Partial<PopComponentDefinitionV4>) => void;
|
||||
/** 컨테이너 업데이트 */
|
||||
|
|
@ -57,6 +60,8 @@ const COMPONENT_TYPE_LABELS: Record<PopComponentType, string> = {
|
|||
"pop-indicator": "인디케이터",
|
||||
"pop-scanner": "스캐너",
|
||||
"pop-numpad": "숫자패드",
|
||||
"pop-spacer": "스페이서",
|
||||
"pop-break": "줄바꿈",
|
||||
};
|
||||
|
||||
// ========================================
|
||||
|
|
@ -71,6 +76,7 @@ const COMPONENT_TYPE_LABELS: Record<PopComponentType, string> = {
|
|||
export function ComponentEditorPanelV4({
|
||||
component,
|
||||
container,
|
||||
currentViewportMode = "tablet_landscape",
|
||||
onUpdateComponent,
|
||||
onUpdateContainer,
|
||||
className,
|
||||
|
|
@ -91,15 +97,23 @@ export function ComponentEditorPanelV4({
|
|||
|
||||
// 컨테이너가 선택된 경우
|
||||
if (container) {
|
||||
const isNonDefaultMode = currentViewportMode !== "tablet_landscape";
|
||||
|
||||
return (
|
||||
<div className={cn("flex h-full flex-col", className)}>
|
||||
<div className="border-b px-4 py-3">
|
||||
<h3 className="text-sm font-medium">컨테이너 설정</h3>
|
||||
<p className="text-xs text-muted-foreground">{container.id}</p>
|
||||
{isNonDefaultMode && (
|
||||
<p className="text-xs text-amber-600 mt-1">
|
||||
다른 모드에서는 드래그로 배치 변경 후 '고정' 버튼을 사용하세요
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<ContainerSettingsForm
|
||||
container={container}
|
||||
currentViewportMode={currentViewportMode}
|
||||
onUpdate={onUpdateContainer}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -129,6 +143,10 @@ export function ComponentEditorPanelV4({
|
|||
<Settings className="h-3 w-3" />
|
||||
설정
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="visibility" className="gap-1 text-xs">
|
||||
<Eye className="h-3 w-3" />
|
||||
표시
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="data" className="gap-1 text-xs">
|
||||
<Database className="h-3 w-3" />
|
||||
데이터
|
||||
|
|
@ -151,6 +169,14 @@ export function ComponentEditorPanelV4({
|
|||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* 모드별 표시 탭 */}
|
||||
<TabsContent value="visibility" className="flex-1 overflow-auto p-4">
|
||||
<VisibilityForm
|
||||
component={component!}
|
||||
onUpdate={onUpdateComponent}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* 데이터 바인딩 탭 */}
|
||||
<TabsContent value="data" className="flex-1 overflow-auto p-4">
|
||||
<DataBindingPlaceholder />
|
||||
|
|
@ -359,25 +385,6 @@ function SizeConstraintForm({ component, onUpdate }: SizeConstraintFormProps) {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 반응형 숨김 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-xs font-medium">반응형 숨김</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
className="h-8 w-24 text-xs"
|
||||
value={component.hideBelow || ""}
|
||||
onChange={(e) =>
|
||||
onUpdate?.({
|
||||
hideBelow: e.target.value ? Number(e.target.value) : undefined,
|
||||
})
|
||||
}
|
||||
placeholder="없음"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">px 이하에서 숨김</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -416,13 +423,17 @@ function SizeButton({ active, onClick, label, description }: SizeButtonProps) {
|
|||
|
||||
interface ContainerSettingsFormProps {
|
||||
container: PopContainerV4;
|
||||
currentViewportMode?: "mobile_portrait" | "mobile_landscape" | "tablet_portrait" | "tablet_landscape";
|
||||
onUpdate?: (updates: Partial<PopContainerV4>) => void;
|
||||
}
|
||||
|
||||
function ContainerSettingsForm({
|
||||
container,
|
||||
currentViewportMode = "tablet_landscape",
|
||||
onUpdate,
|
||||
}: ContainerSettingsFormProps) {
|
||||
const isNonDefaultMode = currentViewportMode !== "tablet_landscape";
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 방향 */}
|
||||
|
|
@ -434,6 +445,7 @@ function ContainerSettingsForm({
|
|||
size="sm"
|
||||
className="flex-1 h-8 text-xs"
|
||||
onClick={() => onUpdate?.({ direction: "horizontal" })}
|
||||
disabled={isNonDefaultMode}
|
||||
>
|
||||
가로
|
||||
</Button>
|
||||
|
|
@ -442,10 +454,16 @@ function ContainerSettingsForm({
|
|||
size="sm"
|
||||
className="flex-1 h-8 text-xs"
|
||||
onClick={() => onUpdate?.({ direction: "vertical" })}
|
||||
disabled={isNonDefaultMode}
|
||||
>
|
||||
세로
|
||||
</Button>
|
||||
</div>
|
||||
{isNonDefaultMode && (
|
||||
<p className="text-[10px] text-amber-600">
|
||||
드래그로 배치 변경 후 '고정' 버튼 클릭
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 줄바꿈 */}
|
||||
|
|
@ -457,6 +475,7 @@ function ContainerSettingsForm({
|
|||
size="sm"
|
||||
className="flex-1 h-8 text-xs"
|
||||
onClick={() => onUpdate?.({ wrap: true })}
|
||||
disabled={isNonDefaultMode}
|
||||
>
|
||||
허용
|
||||
</Button>
|
||||
|
|
@ -465,6 +484,7 @@ function ContainerSettingsForm({
|
|||
size="sm"
|
||||
className="flex-1 h-8 text-xs"
|
||||
onClick={() => onUpdate?.({ wrap: false })}
|
||||
disabled={isNonDefaultMode}
|
||||
>
|
||||
금지
|
||||
</Button>
|
||||
|
|
@ -480,6 +500,7 @@ function ContainerSettingsForm({
|
|||
className="h-8 w-24 text-xs"
|
||||
value={container.gap}
|
||||
onChange={(e) => onUpdate?.({ gap: Number(e.target.value) || 0 })}
|
||||
disabled={isNonDefaultMode}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">px</span>
|
||||
</div>
|
||||
|
|
@ -496,6 +517,7 @@ function ContainerSettingsForm({
|
|||
onChange={(e) =>
|
||||
onUpdate?.({ padding: Number(e.target.value) || undefined })
|
||||
}
|
||||
disabled={isNonDefaultMode}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">px</span>
|
||||
</div>
|
||||
|
|
@ -507,6 +529,7 @@ function ContainerSettingsForm({
|
|||
<Select
|
||||
value={container.alignItems}
|
||||
onValueChange={(value) => onUpdate?.({ alignItems: value as any })}
|
||||
disabled={isNonDefaultMode}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
|
|
@ -525,6 +548,7 @@ function ContainerSettingsForm({
|
|||
<Select
|
||||
value={container.justifyContent}
|
||||
onValueChange={(value) => onUpdate?.({ justifyContent: value as any })}
|
||||
disabled={isNonDefaultMode}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
|
|
@ -581,6 +605,94 @@ function ComponentSettingsForm({
|
|||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 모드별 표시/숨김 폼
|
||||
// ========================================
|
||||
|
||||
interface VisibilityFormProps {
|
||||
component: PopComponentDefinitionV4;
|
||||
onUpdate?: (updates: Partial<PopComponentDefinitionV4>) => void;
|
||||
}
|
||||
|
||||
function VisibilityForm({ component, onUpdate }: VisibilityFormProps) {
|
||||
const modes = [
|
||||
{ key: "tablet_landscape" as const, label: "태블릿 가로 (1024×768)" },
|
||||
{ key: "tablet_portrait" as const, label: "태블릿 세로 (768×1024)" },
|
||||
{ key: "mobile_landscape" as const, label: "모바일 가로 (667×375)" },
|
||||
{ key: "mobile_portrait" as const, label: "모바일 세로 (375×667)" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<Label className="text-xs font-medium flex items-center gap-1">
|
||||
<Eye className="h-3 w-3" />
|
||||
모드별 표시 설정
|
||||
</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"
|
||||
id={`visibility-${key}`}
|
||||
checked={isChecked}
|
||||
onChange={(e) => {
|
||||
onUpdate?.({
|
||||
visibility: {
|
||||
...component.visibility,
|
||||
[key]: e.target.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`visibility-${key}`}
|
||||
className="text-xs cursor-pointer flex-1"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
{!isChecked && (
|
||||
<span className="text-xs text-muted-foreground">(숨김)</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 반응형 숨김 (픽셀 기반) */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-xs font-medium">반응형 숨김 (픽셀 기반)</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
className="h-8 w-24 text-xs"
|
||||
value={component.hideBelow || ""}
|
||||
onChange={(e) =>
|
||||
onUpdate?.({
|
||||
hideBelow: e.target.value ? Number(e.target.value) : undefined,
|
||||
})
|
||||
}
|
||||
placeholder="없음"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">px 이하에서 숨김</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
예: 500 입력 시 화면 너비가 500px 이하면 자동 숨김
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 데이터 바인딩 플레이스홀더
|
||||
// ========================================
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
Calculator,
|
||||
GripVertical,
|
||||
Space,
|
||||
WrapText,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PopComponentType } from "../types/pop-layout";
|
||||
|
|
@ -66,6 +67,12 @@ const COMPONENT_PALETTE: {
|
|||
icon: Space,
|
||||
description: "빈 공간 (정렬용)",
|
||||
},
|
||||
{
|
||||
type: "pop-break",
|
||||
label: "줄바꿈",
|
||||
icon: WrapText,
|
||||
description: "강제 줄바꿈 (flex-basis: 100%)",
|
||||
},
|
||||
];
|
||||
|
||||
// ========================================
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ interface PopFlexRendererProps {
|
|||
layout: PopLayoutDataV4;
|
||||
/** 현재 뷰포트 너비 (반응형 규칙 적용용) */
|
||||
viewportWidth: number;
|
||||
/** 현재 뷰포트 모드 (오버라이드 병합용) */
|
||||
currentMode?: "mobile_portrait" | "mobile_landscape" | "tablet_portrait" | "tablet_landscape";
|
||||
/** 임시 레이아웃 (고정 전 미리보기) */
|
||||
tempLayout?: PopContainerV4 | null;
|
||||
/** 디자인 모드 여부 */
|
||||
isDesignMode?: boolean;
|
||||
/** 선택된 컴포넌트 ID */
|
||||
|
|
@ -60,6 +64,7 @@ const COMPONENT_TYPE_LABELS: Record<PopComponentType, string> = {
|
|||
"pop-scanner": "스캐너",
|
||||
"pop-numpad": "숫자패드",
|
||||
"pop-spacer": "스페이서",
|
||||
"pop-break": "줄바꿈",
|
||||
};
|
||||
|
||||
// ========================================
|
||||
|
|
@ -74,6 +79,8 @@ const COMPONENT_TYPE_LABELS: Record<PopComponentType, string> = {
|
|||
export function PopFlexRenderer({
|
||||
layout,
|
||||
viewportWidth,
|
||||
currentMode = "tablet_landscape",
|
||||
tempLayout,
|
||||
isDesignMode = false,
|
||||
selectedComponentId,
|
||||
onComponentClick,
|
||||
|
|
@ -83,10 +90,60 @@ export function PopFlexRenderer({
|
|||
onReorderComponent,
|
||||
className,
|
||||
}: PopFlexRendererProps) {
|
||||
const { root, components, settings } = layout;
|
||||
const { root, components, settings, overrides } = layout;
|
||||
|
||||
// 오버라이드 병합 로직 (컨테이너) 🔥
|
||||
const getMergedRoot = (): PopContainerV4 => {
|
||||
// 1. 임시 레이아웃이 있으면 최우선 (고정 전 미리보기)
|
||||
if (tempLayout) {
|
||||
return tempLayout;
|
||||
}
|
||||
|
||||
// 2. 기본 모드면 root 그대로 반환
|
||||
if (currentMode === "tablet_landscape") {
|
||||
return root;
|
||||
}
|
||||
|
||||
// 3. 다른 모드면 오버라이드 병합
|
||||
const override = overrides?.[currentMode]?.containers?.root;
|
||||
if (override) {
|
||||
return {
|
||||
...root,
|
||||
...override, // 오버라이드 속성으로 덮어쓰기
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 오버라이드 없으면 기본값
|
||||
return root;
|
||||
};
|
||||
|
||||
// visibility 체크 함수 🆕
|
||||
const isComponentVisible = (component: PopComponentDefinitionV4): boolean => {
|
||||
if (!component.visibility) return true; // 기본값: 표시
|
||||
const modeVisibility = component.visibility[currentMode as keyof typeof component.visibility];
|
||||
return modeVisibility !== false; // undefined도 true로 취급
|
||||
};
|
||||
|
||||
// 컴포넌트 오버라이드 병합 🆕
|
||||
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 },
|
||||
};
|
||||
};
|
||||
|
||||
const effectiveRoot = getMergedRoot();
|
||||
|
||||
// 빈 상태는 PopCanvasV4에서 표시하므로 여기서는 투명 배경만 렌더링
|
||||
if (root.children.length === 0) {
|
||||
if (effectiveRoot.children.length === 0) {
|
||||
return (
|
||||
<div
|
||||
className={cn("h-full w-full", className)}
|
||||
|
|
@ -104,12 +161,14 @@ export function PopFlexRenderer({
|
|||
}
|
||||
}}
|
||||
>
|
||||
{/* 루트 컨테이너 렌더링 */}
|
||||
{/* 루트 컨테이너 렌더링 (병합된 레이아웃 사용) */}
|
||||
<ContainerRenderer
|
||||
container={root}
|
||||
container={effectiveRoot}
|
||||
components={components}
|
||||
viewportWidth={viewportWidth}
|
||||
settings={settings}
|
||||
currentMode={currentMode}
|
||||
overrides={overrides}
|
||||
isDesignMode={isDesignMode}
|
||||
selectedComponentId={selectedComponentId}
|
||||
onComponentClick={onComponentClick}
|
||||
|
|
@ -130,6 +189,8 @@ interface ContainerRendererProps {
|
|||
components: Record<string, PopComponentDefinitionV4>;
|
||||
viewportWidth: number;
|
||||
settings: PopLayoutDataV4["settings"];
|
||||
currentMode: string;
|
||||
overrides: PopLayoutDataV4["overrides"];
|
||||
isDesignMode?: boolean;
|
||||
selectedComponentId?: string | null;
|
||||
onComponentClick?: (componentId: string) => void;
|
||||
|
|
@ -144,6 +205,8 @@ function ContainerRenderer({
|
|||
components,
|
||||
viewportWidth,
|
||||
settings,
|
||||
currentMode,
|
||||
overrides,
|
||||
isDesignMode = false,
|
||||
selectedComponentId,
|
||||
onComponentClick,
|
||||
|
|
@ -152,6 +215,28 @@ function ContainerRenderer({
|
|||
onReorderComponent,
|
||||
depth = 0,
|
||||
}: ContainerRendererProps) {
|
||||
// visibility 체크 함수
|
||||
const isComponentVisible = (component: PopComponentDefinitionV4): boolean => {
|
||||
if (!component.visibility) return true; // 기본값: 표시
|
||||
const modeVisibility = component.visibility[currentMode as keyof typeof component.visibility];
|
||||
return modeVisibility !== false; // undefined도 true로 취급
|
||||
};
|
||||
|
||||
// 컴포넌트 오버라이드 병합
|
||||
const getMergedComponent = (baseComponent: PopComponentDefinitionV4): PopComponentDefinitionV4 => {
|
||||
if (currentMode === "tablet_landscape") return baseComponent;
|
||||
|
||||
const componentOverride = overrides?.[currentMode as keyof typeof overrides]?.components?.[baseComponent.id];
|
||||
if (!componentOverride) return baseComponent;
|
||||
|
||||
// 깊은 병합 (config, size)
|
||||
return {
|
||||
...baseComponent,
|
||||
...componentOverride,
|
||||
size: { ...baseComponent.size, ...componentOverride.size },
|
||||
config: { ...baseComponent.config, ...componentOverride.config },
|
||||
};
|
||||
};
|
||||
// 반응형 규칙 적용
|
||||
const effectiveContainer = useMemo(() => {
|
||||
return applyResponsiveRules(container, viewportWidth);
|
||||
|
|
@ -209,6 +294,8 @@ function ContainerRenderer({
|
|||
components={components}
|
||||
viewportWidth={viewportWidth}
|
||||
settings={settings}
|
||||
currentMode={currentMode}
|
||||
overrides={overrides}
|
||||
isDesignMode={isDesignMode}
|
||||
selectedComponentId={selectedComponentId}
|
||||
onComponentClick={onComponentClick}
|
||||
|
|
@ -222,14 +309,43 @@ function ContainerRenderer({
|
|||
|
||||
// 컴포넌트 ID인 경우
|
||||
const componentId = child;
|
||||
const compDef = components[componentId];
|
||||
if (!compDef) return null;
|
||||
const baseComponent = components[componentId];
|
||||
if (!baseComponent) return null;
|
||||
|
||||
// 반응형 숨김 처리
|
||||
if (compDef.hideBelow && viewportWidth < compDef.hideBelow) {
|
||||
// visibility 체크 (모드별 숨김)
|
||||
if (!isComponentVisible(baseComponent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 반응형 숨김 처리 (픽셀 기반)
|
||||
if (baseComponent.hideBelow && viewportWidth < baseComponent.hideBelow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 오버라이드 병합
|
||||
const mergedComponent = getMergedComponent(baseComponent);
|
||||
|
||||
// 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%" }}
|
||||
onClick={() => onComponentClick?.(componentId)}
|
||||
>
|
||||
{isDesignMode && (
|
||||
<span className="text-xs text-gray-400">줄바꿈</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DraggableComponentWrapper
|
||||
key={componentId}
|
||||
|
|
@ -241,7 +357,7 @@ function ContainerRenderer({
|
|||
>
|
||||
<ComponentRendererV4
|
||||
componentId={componentId}
|
||||
component={compDef}
|
||||
component={mergedComponent}
|
||||
settings={settings}
|
||||
viewportWidth={viewportWidth}
|
||||
isDesignMode={isDesignMode}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
* v4 레이아웃 (반응형)
|
||||
* - 단일 소스: 4모드 따로 설계 X
|
||||
* - 규칙 기반: 컴포넌트가 어떻게 반응할지 정의
|
||||
* - 오버라이드: 특정 모드에서만 다르게 표현 (Phase 2)
|
||||
*/
|
||||
export interface PopLayoutDataV4 {
|
||||
version: "pop-4.0";
|
||||
|
|
@ -31,6 +32,28 @@ export interface PopLayoutDataV4 {
|
|||
|
||||
// 메타데이터
|
||||
metadata?: PopLayoutMetadata;
|
||||
|
||||
// 모드별 오버라이드 (Phase 2)
|
||||
// - tablet_landscape가 기본이므로 오버라이드 없음
|
||||
// - 나머지 3개 모드만 오버라이드 가능
|
||||
overrides?: {
|
||||
mobile_portrait?: PopModeOverride;
|
||||
mobile_landscape?: PopModeOverride;
|
||||
tablet_portrait?: PopModeOverride;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 모드별 오버라이드
|
||||
* - 특정 모드에서만 다르게 표현할 속성들
|
||||
* - 기본 모드(tablet_landscape) 값을 부분적으로 덮어씀
|
||||
*/
|
||||
export interface PopModeOverride {
|
||||
// 컴포넌트별 오버라이드
|
||||
components?: Record<string, Partial<PopComponentDefinitionV4>>;
|
||||
|
||||
// 컨테이너별 오버라이드
|
||||
containers?: Record<string, Partial<PopContainerV4>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -93,9 +116,17 @@ export interface PopComponentDefinitionV4 {
|
|||
// 개별 정렬 (컨테이너 설정 덮어쓰기)
|
||||
alignSelf?: "start" | "center" | "end" | "stretch";
|
||||
|
||||
// 반응형 숨김
|
||||
// 반응형 숨김 (픽셀 기반)
|
||||
hideBelow?: number; // 이 너비 이하에서 숨김
|
||||
|
||||
// 모드별 표시/숨김 (명시적)
|
||||
visibility?: {
|
||||
tablet_landscape?: boolean; // 기본값 true
|
||||
tablet_portrait?: boolean;
|
||||
mobile_landscape?: boolean;
|
||||
mobile_portrait?: boolean;
|
||||
};
|
||||
|
||||
// 기존 속성
|
||||
dataBinding?: PopDataBinding;
|
||||
style?: PopStylePreset;
|
||||
|
|
@ -217,6 +248,11 @@ export const createComponentDefinitionV4 = (
|
|||
height: "fixed",
|
||||
fixedHeight: 48,
|
||||
},
|
||||
"pop-break": {
|
||||
width: "fill", // 100% 너비로 줄바꿈 강제 (flex-basis: 100%)
|
||||
height: "fixed",
|
||||
fixedHeight: 0, // 높이 0 (보이지 않음)
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
@ -292,23 +328,79 @@ function addChildToContainer(
|
|||
|
||||
/**
|
||||
* v4 레이아웃에서 컴포넌트 삭제
|
||||
* - components에서 삭제
|
||||
* - root.children에서 제거
|
||||
* - 모든 overrides에서도 제거 (중요!)
|
||||
*/
|
||||
export const removeComponentFromV4Layout = (
|
||||
layout: PopLayoutDataV4,
|
||||
componentId: string
|
||||
): PopLayoutDataV4 => {
|
||||
const newLayout = { ...layout };
|
||||
// 1. 컴포넌트 정의 삭제
|
||||
const { [componentId]: _, ...remainingComponents } = layout.components;
|
||||
|
||||
// 컴포넌트 정의 삭제
|
||||
const { [componentId]: _, ...remainingComponents } = newLayout.components;
|
||||
newLayout.components = remainingComponents;
|
||||
// 2. root.children에서 제거
|
||||
const newRoot = removeChildFromContainer(layout.root, componentId);
|
||||
|
||||
// 컨테이너에서 컴포넌트 ID 제거
|
||||
newLayout.root = removeChildFromContainer(newLayout.root, componentId);
|
||||
// 3. 모든 오버라이드에서 제거
|
||||
const newOverrides = cleanupOverridesAfterDelete(layout.overrides, componentId);
|
||||
|
||||
return newLayout;
|
||||
return {
|
||||
...layout,
|
||||
root: newRoot,
|
||||
components: remainingComponents,
|
||||
overrides: newOverrides,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 오버라이드 정리 (컴포넌트 삭제 시)
|
||||
* - containers.root.children에서 제거
|
||||
* - components에서 제거
|
||||
*/
|
||||
function cleanupOverridesAfterDelete(
|
||||
overrides: PopLayoutDataV4["overrides"],
|
||||
componentId: string
|
||||
): PopLayoutDataV4["overrides"] {
|
||||
if (!overrides) return undefined;
|
||||
|
||||
const newOverrides = { ...overrides };
|
||||
|
||||
for (const mode of Object.keys(newOverrides) as Array<keyof typeof 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 컨테이너에서 자식 제거 (재귀)
|
||||
*/
|
||||
|
|
@ -610,7 +702,8 @@ export type PopComponentType =
|
|||
| "pop-indicator" // 상태/수치 표시
|
||||
| "pop-scanner" // 바코드/QR 입력
|
||||
| "pop-numpad" // 숫자 입력 특화
|
||||
| "pop-spacer"; // 빈 공간 (레이아웃 정렬용)
|
||||
| "pop-spacer" // 빈 공간 (레이아웃 정렬용)
|
||||
| "pop-break"; // 줄바꿈 (강제 줄바꿈, flex-basis: 100%)
|
||||
|
||||
// ========================================
|
||||
// 데이터 흐름
|
||||
|
|
|
|||
|
|
@ -6,13 +6,110 @@
|
|||
|
||||
## [미출시]
|
||||
|
||||
- Phase 2: 모드별 오버라이드 기능
|
||||
- Phase 2: 모드별 오버라이드 기능 (진행 중)
|
||||
- Phase 3: 컴포넌트 표시/숨김
|
||||
- Phase 4: 순서 오버라이드
|
||||
- Tier 2, 3 컴포넌트
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-04] Phase 2.1 완료 - 배치 고정 기능
|
||||
|
||||
### Added
|
||||
- **현재 모드 추적** (PopDesigner.tsx)
|
||||
- `currentViewportMode` 상태 추가
|
||||
- PopCanvasV4와 양방향 동기화
|
||||
- 모드 변경 시 자동 업데이트
|
||||
|
||||
- **배치 고정 기능**
|
||||
- "고정" 버튼 추가 (기본 모드 제외)
|
||||
- `handleLockLayoutV4()` - 현재 배치를 오버라이드에 저장
|
||||
- 배치 정보: direction, wrap, gap, alignItems, justifyContent, children 순서
|
||||
|
||||
- **오버라이드 초기화 기능**
|
||||
- `handleResetOverrideV4()` - 오버라이드 삭제
|
||||
- "자동으로 되돌리기" 버튼 (편집된 모드만 표시)
|
||||
- 자동 계산으로 되돌림
|
||||
|
||||
### Changed
|
||||
- **PopCanvasV4 Props 구조 변경**
|
||||
- `currentMode` prop 추가 (외부에서 제어)
|
||||
- `onModeChange` 콜백 추가
|
||||
- `onLockLayout` 콜백 추가
|
||||
- 내부 `activeViewport` 상태 제거 (부모가 관리)
|
||||
|
||||
- **프리셋 버튼 동작**
|
||||
- 클릭 시 부모 상태 업데이트 (`onModeChange`)
|
||||
- `currentMode` prop 기반으로 활성 상태 표시
|
||||
|
||||
### Technical Details
|
||||
```typescript
|
||||
// 고정 로직
|
||||
const handleLockLayoutV4 = () => {
|
||||
const newLayout = {
|
||||
...layoutV4,
|
||||
overrides: {
|
||||
...layoutV4.overrides,
|
||||
[currentViewportMode]: {
|
||||
containers: {
|
||||
root: {
|
||||
direction: layoutV4.root.direction,
|
||||
wrap: layoutV4.root.wrap,
|
||||
gap: layoutV4.root.gap,
|
||||
children: layoutV4.root.children, // 순서 고정
|
||||
// ... 기타 배치 속성
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 초기화 로직
|
||||
const handleResetOverrideV4 = (mode) => {
|
||||
const newOverrides = { ...layoutV4.overrides };
|
||||
delete newOverrides[mode];
|
||||
// overrides가 비면 undefined로 설정
|
||||
};
|
||||
```
|
||||
|
||||
### UI 변경
|
||||
```
|
||||
툴바:
|
||||
[모바일↕] [모바일↔] [태블릿↕] [태블릿↔(기본)] [고정] [자동으로 되돌리기]
|
||||
|
||||
조건부 표시:
|
||||
- "고정" 버튼: 기본 모드가 아닐 때
|
||||
- "자동으로 되돌리기": 오버라이드가 있을 때
|
||||
```
|
||||
|
||||
### 주의사항
|
||||
- 크기는 고정하지 않음 (여전히 자동 스케일링)
|
||||
- 배치만 오버라이드 (순서, 방향, 정렬)
|
||||
- 최소/최대값 기능은 별도 구현 필요
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-04] Phase 2 시작 - 오버라이드 UI 표시
|
||||
|
||||
### Added
|
||||
- **오버라이드 데이터 구조** (pop-layout.ts)
|
||||
- `PopModeOverride` 인터페이스 추가
|
||||
- `PopLayoutDataV4.overrides` 필드 추가
|
||||
- 3개 모드 오버라이드 지원 (mobile_portrait, mobile_landscape, tablet_portrait)
|
||||
|
||||
- **프리셋 버튼 상태 표시** (PopCanvasV4.tsx)
|
||||
- 기본 모드: "(기본)" 텍스트 표시
|
||||
- 편집된 모드: "(편집)" 텍스트 + 노란색 강조
|
||||
- 자동 모드: 기본 스타일
|
||||
|
||||
### Changed
|
||||
- **hasOverride 함수 구현**
|
||||
- `layout.overrides` 필드 체크
|
||||
- 컴포넌트/컨테이너 오버라이드 존재 여부 확인
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-04] 비율 스케일링 시스템 구현
|
||||
|
||||
### Added
|
||||
|
|
@ -202,6 +299,105 @@ v4를 기본 레이아웃 모드로 통합하고, 새 화면은 자동으로 v4
|
|||
|
||||
---
|
||||
|
||||
## [2026-02-04] Phase 2.1 완료 - 배치 고정 기능 (버그 수정)
|
||||
|
||||
### 🔥 주요 버그 수정
|
||||
- **layoutV4.root 오염 문제 해결**: 다른 모드에서 편집 시 기본 레이아웃이 변경되던 버그 수정
|
||||
- **tempLayout 도입**: 고정 전 임시 배치를 별도 상태로 관리하여 root를 보호
|
||||
- **렌더러 병합 로직**: `PopFlexRenderer`에 오버라이드 자동 병합 기능 추가
|
||||
|
||||
### 데이터 흐름 개선
|
||||
1. **기본 모드 (태블릿 가로)**
|
||||
- 드래그/속성 변경 → `layoutV4.root` 직접 수정 ✅
|
||||
- 모든 다른 모드의 기본값으로 사용
|
||||
|
||||
2. **다른 모드 (모바일 세로 등)**
|
||||
- 드래그 → `tempLayout` 임시 저장 (화면에만 표시)
|
||||
- "고정" 버튼 → `layoutV4.overrides[mode]`에 저장
|
||||
- 속성 패널 → 비활성화 + 안내 메시지
|
||||
|
||||
3. **렌더링**
|
||||
- `tempLayout` 있으면 최우선 표시 (고정 전 미리보기)
|
||||
- 오버라이드 있으면 `root`와 병합
|
||||
- 없으면 `root` 그대로 표시
|
||||
|
||||
### 수정 파일
|
||||
- `PopDesigner.tsx`: tempLayout 상태 추가, 핸들러 수정
|
||||
- `PopFlexRenderer.tsx`: 병합 로직 추가 (getMergedRoot)
|
||||
- `PopCanvasV4.tsx`: tempLayout props 전달
|
||||
- `ComponentEditorPanelV4.tsx`: 속성 패널 비활성화 로직
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-04] Phase 3 완료 - visibility + 줄바꿈 컴포넌트
|
||||
|
||||
### 추가 기능
|
||||
- **visibility 속성**: 모드별 컴포넌트 표시/숨김 제어
|
||||
- **pop-break 컴포넌트**: 강제 줄바꿈 (flex-basis: 100%)
|
||||
- **컴포넌트 오버라이드 병합**: 모드별 컴포넌트 설정 변경 가능
|
||||
|
||||
### 타입 정의
|
||||
```typescript
|
||||
interface PopComponentDefinitionV4 {
|
||||
// 기존 속성...
|
||||
|
||||
// 🆕 모드별 표시/숨김
|
||||
visibility?: {
|
||||
tablet_landscape?: boolean;
|
||||
tablet_portrait?: boolean;
|
||||
mobile_landscape?: boolean;
|
||||
mobile_portrait?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// 🆕 줄바꿈 컴포넌트
|
||||
type PopComponentType =
|
||||
| "pop-field"
|
||||
| "pop-button"
|
||||
| "pop-list"
|
||||
| "pop-indicator"
|
||||
| "pop-scanner"
|
||||
| "pop-numpad"
|
||||
| "pop-spacer"
|
||||
| "pop-break"; // 새로 추가
|
||||
```
|
||||
|
||||
### 렌더러 개선
|
||||
- `isComponentVisible()`: visibility 체크 로직
|
||||
- `getMergedComponent()`: 컴포넌트 오버라이드 병합
|
||||
- pop-break 전용 렌더링 (디자인 모드: 점선, 실제: 높이 0)
|
||||
|
||||
### 삭제 함수 개선
|
||||
- `cleanupOverridesAfterDelete()`: 컴포넌트 삭제 시 모든 오버라이드 정리
|
||||
- containers.root.children 정리
|
||||
- components 오버라이드 정리
|
||||
- 빈 오버라이드 자동 제거
|
||||
|
||||
### UI 개선
|
||||
- 속성 패널에 "표시" 탭 추가 (Eye 아이콘)
|
||||
- 모드별 체크박스 UI
|
||||
- 반응형 숨김 (hideBelow) 유지
|
||||
- 팔레트에 "줄바꿈" 컴포넌트 추가
|
||||
|
||||
### 사용 예시
|
||||
```
|
||||
태블릿 가로:
|
||||
[A] [B] [C] [D] [E] ← 한 줄
|
||||
|
||||
모바일 세로:
|
||||
[A] [B]
|
||||
─────── ← 줄바꿈 (visibility: mobile만 true)
|
||||
[C] [D] [E]
|
||||
```
|
||||
|
||||
### 수정 파일
|
||||
- `pop-layout.ts`: 타입 추가, 삭제 함수 수정
|
||||
- `PopFlexRenderer.tsx`: visibility, 병합, pop-break 렌더링
|
||||
- `ComponentEditorPanelV4.tsx`: 표시 탭 추가
|
||||
- `ComponentPaletteV4.tsx`: 줄바꿈 추가
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-04] v4 타입 및 렌더러
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -0,0 +1,518 @@
|
|||
# Phase 3 완료 요약
|
||||
|
||||
**날짜**: 2026-02-04
|
||||
**상태**: 완료 ✅
|
||||
**버전**: v4.0 Phase 3
|
||||
|
||||
---
|
||||
|
||||
## 🎯 달성 목표
|
||||
|
||||
Phase 2의 배치 고정 기능 이후, 다음 3가지 핵심 기능 추가:
|
||||
|
||||
1. ✅ **모드별 컴포넌트 표시/숨김** (visibility)
|
||||
2. ✅ **강제 줄바꿈 컴포넌트** (pop-break)
|
||||
3. ✅ **컴포넌트 오버라이드 병합** (모드별 설정 변경)
|
||||
|
||||
---
|
||||
|
||||
## 📦 구현 내용
|
||||
|
||||
### 1. 타입 정의
|
||||
|
||||
**파일**: `frontend/components/pop/designer/types/pop-layout.ts`
|
||||
|
||||
```typescript
|
||||
// pop-break 추가
|
||||
export type PopComponentType =
|
||||
| "pop-field"
|
||||
| "pop-button"
|
||||
| "pop-list"
|
||||
| "pop-indicator"
|
||||
| "pop-scanner"
|
||||
| "pop-numpad"
|
||||
| "pop-spacer"
|
||||
| "pop-break"; // 🆕
|
||||
|
||||
// visibility 속성 추가
|
||||
export interface PopComponentDefinitionV4 {
|
||||
id: string;
|
||||
type: PopComponentType;
|
||||
size: PopSizeConstraintV4;
|
||||
|
||||
visibility?: {
|
||||
tablet_landscape?: boolean;
|
||||
tablet_portrait?: boolean;
|
||||
mobile_landscape?: boolean;
|
||||
mobile_portrait?: boolean;
|
||||
};
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
// 기본 크기
|
||||
defaultSizes["pop-break"] = {
|
||||
width: "fill", // 100% 너비
|
||||
height: "fixed",
|
||||
fixedHeight: 0, // 높이 0
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 렌더러 로직
|
||||
|
||||
**파일**: `frontend/components/pop/designer/renderers/PopFlexRenderer.tsx`
|
||||
|
||||
#### visibility 체크
|
||||
```typescript
|
||||
const isComponentVisible = (component: PopComponentDefinitionV4): boolean => {
|
||||
if (!component.visibility) return true; // 기본값: 표시
|
||||
const modeVisibility = component.visibility[currentMode];
|
||||
return modeVisibility !== false; // undefined도 true로 취급
|
||||
};
|
||||
```
|
||||
|
||||
#### 컴포넌트 오버라이드 병합
|
||||
```typescript
|
||||
const getMergedComponent = (baseComponent: PopComponentDefinitionV4) => {
|
||||
if (currentMode === "tablet_landscape") return baseComponent;
|
||||
|
||||
const override = overrides?.[currentMode]?.components?.[baseComponent.id];
|
||||
if (!override) return baseComponent;
|
||||
|
||||
// 깊은 병합 (config, size)
|
||||
return {
|
||||
...baseComponent,
|
||||
...override,
|
||||
size: { ...baseComponent.size, ...override.size },
|
||||
config: { ...baseComponent.config, ...override.config },
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
#### pop-break 렌더링
|
||||
```typescript
|
||||
if (mergedComponent.type === "pop-break") {
|
||||
return (
|
||||
<div
|
||||
style={{ flexBasis: "100%" }} // 핵심: 100% 너비로 줄바꿈 강제
|
||||
className={isDesignMode
|
||||
? "h-4 border-2 border-dashed border-gray-300"
|
||||
: "h-0"
|
||||
}
|
||||
>
|
||||
{isDesignMode && <span>줄바꿈</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 삭제 함수 개선
|
||||
|
||||
**파일**: `frontend/components/pop/designer/types/pop-layout.ts`
|
||||
|
||||
```typescript
|
||||
export const removeComponentFromV4Layout = (
|
||||
layout: PopLayoutDataV4,
|
||||
componentId: string
|
||||
): PopLayoutDataV4 => {
|
||||
// 1. components에서 삭제
|
||||
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
|
||||
) {
|
||||
// 각 모드별로:
|
||||
// 1. containers.root.children에서 componentId 제거
|
||||
// 2. components[componentId] 제거
|
||||
// 3. 빈 오버라이드 자동 삭제
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 속성 패널 UI
|
||||
|
||||
**파일**: `frontend/components/pop/designer/panels/ComponentEditorPanelV4.tsx`
|
||||
|
||||
#### "표시" 탭 추가
|
||||
```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 }) {
|
||||
const modes = [
|
||||
{ key: "tablet_landscape", label: "태블릿 가로" },
|
||||
{ key: "tablet_portrait", label: "태블릿 세로" },
|
||||
{ key: "mobile_landscape", label: "모바일 가로" },
|
||||
{ key: "mobile_portrait", label: "모바일 세로" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{modes.map(({ key, label }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={component.visibility?.[key] !== false}
|
||||
onChange={(e) => {
|
||||
onUpdate?.({
|
||||
visibility: {
|
||||
...component.visibility,
|
||||
[key]: e.target.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 팔레트 업데이트
|
||||
|
||||
**파일**: `frontend/components/pop/designer/panels/ComponentPaletteV4.tsx`
|
||||
|
||||
```typescript
|
||||
const COMPONENT_PALETTE = [
|
||||
// ... 기존 컴포넌트들
|
||||
{
|
||||
type: "pop-break",
|
||||
label: "줄바꿈",
|
||||
icon: WrapText,
|
||||
description: "강제 줄바꿈 (flex-basis: 100%)",
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI 변경사항
|
||||
|
||||
### 컴포넌트 팔레트
|
||||
```
|
||||
컴포넌트
|
||||
├─ 필드
|
||||
├─ 버튼
|
||||
├─ 리스트
|
||||
├─ 인디케이터
|
||||
├─ 스캐너
|
||||
├─ 숫자패드
|
||||
├─ 스페이서
|
||||
└─ 줄바꿈 🆕
|
||||
```
|
||||
|
||||
### 속성 패널
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ 탭: [크기][설정] │
|
||||
│ [표시📍][데이터] │
|
||||
├─────────────────────┤
|
||||
│ 모드별 표시 설정 │
|
||||
│ ☑ 태블릿 가로 │
|
||||
│ ☑ 태블릿 세로 │
|
||||
│ ☐ 모바일 가로 (숨김)│
|
||||
│ ☑ 모바일 세로 │
|
||||
├─────────────────────┤
|
||||
│ 반응형 숨김 │
|
||||
│ [500] px 이하 숨김 │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 사용 예시
|
||||
|
||||
### 예시 1: 모바일 전용 버튼
|
||||
```typescript
|
||||
{
|
||||
id: "call-button",
|
||||
type: "pop-button",
|
||||
label: "전화 걸기",
|
||||
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. 컴포넌트 사이에 드롭
|
||||
3. 디자인 모드에서 점선 "줄바꿈" 표시 확인
|
||||
4. 미리보기에서 줄바꿈이 안 보이는지 확인
|
||||
|
||||
### ✅ 테스트 2: 모드별 줄바꿈
|
||||
1. 줄바꿈 컴포넌트 추가
|
||||
2. "표시" 탭 → 태블릿 모드 체크 해제
|
||||
3. 태블릿 가로: 한 줄
|
||||
4. 모바일 세로: 두 줄
|
||||
|
||||
### ✅ 테스트 3: 삭제 시 오버라이드 정리
|
||||
1. 모바일 세로에서 배치 고정
|
||||
2. 컴포넌트 삭제
|
||||
3. 저장 후 로드
|
||||
4. DB 확인: overrides에서도 제거되었는지
|
||||
|
||||
### ✅ 테스트 4: 컴포넌트 숨김
|
||||
1. 컴포넌트 선택
|
||||
2. "표시" 탭 → 태블릿 모드 체크 해제
|
||||
3. 태블릿: 컴포넌트 안 보임
|
||||
4. 모바일: 컴포넌트 보임
|
||||
|
||||
### ✅ 테스트 5: 속성 패널 UI
|
||||
1. 컴포넌트 선택
|
||||
2. "표시" 탭 클릭
|
||||
3. 4개 체크박스 확인
|
||||
4. 체크 해제 시 "(숨김)" 표시
|
||||
5. 저장 후 로드 → 상태 유지
|
||||
|
||||
---
|
||||
|
||||
## 📝 수정된 파일
|
||||
|
||||
### 코드 파일 (5개)
|
||||
```
|
||||
✅ 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
|
||||
- "줄바꿈" 컴포넌트 추가
|
||||
|
||||
✅ frontend/components/pop/designer/PopDesigner.tsx
|
||||
- (기존 Phase 2 변경사항 유지)
|
||||
```
|
||||
|
||||
### 문서 파일 (6개)
|
||||
```
|
||||
✅ popdocs/CHANGELOG.md
|
||||
- Phase 3 완료 기록
|
||||
|
||||
✅ popdocs/PLAN.md
|
||||
- Phase 3 체크 완료
|
||||
- Phase 4 계획 추가
|
||||
|
||||
✅ popdocs/V4_UNIFIED_DESIGN_SPEC.md
|
||||
- Phase 3 섹션 추가
|
||||
|
||||
✅ popdocs/components-spec.md
|
||||
- pop-break 상세 스펙 추가
|
||||
- Phase 3 업데이트 노트
|
||||
|
||||
✅ popdocs/README.md
|
||||
- 현재 상태 업데이트
|
||||
- Phase 3 요약 추가
|
||||
|
||||
✅ popdocs/decisions/002-phase3-visibility-break.md (신규)
|
||||
- 상세 설계 문서
|
||||
|
||||
✅ popdocs/PHASE3_SUMMARY.md (신규)
|
||||
- 이 문서
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎓 핵심 개념
|
||||
|
||||
### Flexbox 줄바꿈 원리
|
||||
```css
|
||||
/* 컨테이너 */
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap; /* 필수 */
|
||||
}
|
||||
|
||||
/* pop-break */
|
||||
.pop-break {
|
||||
flex-basis: 100%; /* 전체 너비 차지 → 다음 요소는 새 줄로 */
|
||||
height: 0; /* 실제로는 안 보임 */
|
||||
}
|
||||
```
|
||||
|
||||
### visibility vs hideBelow
|
||||
| 속성 | 제어 방식 | 용도 |
|
||||
|------|----------|------|
|
||||
| `visibility` | 모드별 명시적 | 특정 모드에서만 표시 (예: 모바일 전용) |
|
||||
| `hideBelow` | 픽셀 기반 자동 | 화면 너비에 따라 자동 숨김 (예: 500px 이하) |
|
||||
|
||||
**예시**:
|
||||
```typescript
|
||||
{
|
||||
visibility: {
|
||||
tablet_landscape: false, // 태블릿 가로: 무조건 숨김
|
||||
},
|
||||
hideBelow: 500, // 500px 이하: 자동 숨김 (다른 모드에서도)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 다음 단계
|
||||
|
||||
### Phase 4: 실제 컴포넌트 구현
|
||||
```
|
||||
우선순위:
|
||||
1. pop-field (입력/표시 필드)
|
||||
2. pop-button (액션 버튼)
|
||||
3. pop-list (데이터 리스트)
|
||||
4. pop-indicator (KPI 표시)
|
||||
5. pop-scanner (바코드/QR)
|
||||
6. pop-numpad (숫자 입력)
|
||||
```
|
||||
|
||||
### 추가 개선 사항
|
||||
```
|
||||
1. 컴포넌트 오버라이드 UI
|
||||
- 리스트 컬럼 수 조정 UI
|
||||
- 버튼 스타일 변경 UI
|
||||
- 필드 표시 형식 변경 UI
|
||||
|
||||
2. "모든 모드에 적용" 기능
|
||||
- 한 번에 모든 모드 체크/해제
|
||||
|
||||
3. 오버라이드 비교 뷰
|
||||
- 기본값 vs 오버라이드 차이 시각화
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✨ 주요 성과
|
||||
|
||||
1. ✅ **모드별 컴포넌트 제어**: visibility 속성으로 유연한 표시/숨김
|
||||
2. ✅ **Flexbox 줄바꿈 해결**: pop-break 컴포넌트로 업계 표준 달성
|
||||
3. ✅ **확장 가능한 구조**: 컴포넌트 오버라이드 병합으로 추후 기능 추가 용이
|
||||
4. ✅ **데이터 일관성**: 삭제 시 오버라이드 자동 정리로 데이터 무결성 유지
|
||||
5. ✅ **직관적인 UI**: 체크박스 기반 visibility 제어
|
||||
|
||||
---
|
||||
|
||||
## 📚 참고 문서
|
||||
|
||||
- [decisions/002-phase3-visibility-break.md](./decisions/002-phase3-visibility-break.md) - 상세 설계
|
||||
- [V4_UNIFIED_DESIGN_SPEC.md](./V4_UNIFIED_DESIGN_SPEC.md) - v4 통합 설계
|
||||
- [CHANGELOG.md](./CHANGELOG.md) - 변경 이력
|
||||
- [PLAN.md](./PLAN.md) - 로드맵
|
||||
|
||||
---
|
||||
|
||||
*Phase 3 완료 - 2026-02-04*
|
||||
*다음: Phase 4 (실제 컴포넌트 구현)*
|
||||
|
|
@ -86,23 +86,44 @@ v4 기본 구조 → 오버라이드 기능 → 컴포넌트 숨김 →
|
|||
- [x] 디자인 모드 분리 (scale=1, 원본 유지)
|
||||
- [x] DndProvider 에러 수정 (뷰어에서 useDrag/useDrop 방지)
|
||||
|
||||
## Phase 2: 오버라이드 기능 (다음)
|
||||
## Phase 2: 오버라이드 기능 (완료) ✅
|
||||
|
||||
- [ ] ModeOverride 데이터 구조 추가
|
||||
- [ ] 편집 감지 → 자동 오버라이드 저장
|
||||
- [ ] 편집 상태 표시 (버튼 색상 변경)
|
||||
- [ ] "자동으로 되돌리기" 버튼
|
||||
### Phase 2.1: 배치 고정 (완료)
|
||||
- [x] 현재 모드 추적 (PopDesigner)
|
||||
- [x] 고정 버튼 UI 및 로직
|
||||
- [x] 오버라이드 저장 (배치만)
|
||||
- [x] 오버라이드 초기화 로직
|
||||
- [x] **버그 수정**: tempLayout 도입 (root 오염 방지)
|
||||
- [x] **속성 패널**: 다른 모드에서 비활성화
|
||||
|
||||
## Phase 3: 컴포넌트 표시/숨김 (계획)
|
||||
### Phase 2.2: 렌더러 오버라이드 적용 (완료) ✅
|
||||
- [x] PopFlexRenderer에서 오버라이드 병합 (getMergedRoot)
|
||||
- [x] 컨테이너 속성 오버라이드 적용 (direction, wrap, gap, alignItems, justifyContent, padding, children)
|
||||
- [x] tempLayout 우선 표시 (고정 전 미리보기)
|
||||
- [x] 테스트 (모드별 다른 배치)
|
||||
|
||||
- [ ] visibility 속성 추가 (모드별 true/false)
|
||||
- [ ] 속성 패널 체크박스 UI
|
||||
- [ ] 렌더러에서 visibility 처리
|
||||
### Phase 2.3: 편집 자동 감지 (완료) ✅
|
||||
- [x] 순서 변경 시 자동 tempLayout 저장
|
||||
- [x] "고정" 버튼으로 정식 오버라이드 전환
|
||||
- [x] 속성 변경은 기본 모드에서만 가능 (다른 모드 차단)
|
||||
|
||||
## Phase 4: 순서 오버라이드 (계획)
|
||||
## Phase 3: 컴포넌트 표시/숨김 + 줄바꿈 (완료) ✅
|
||||
|
||||
- [ ] 모드별 children 순서 오버라이드
|
||||
- [ ] 드래그로 순서 변경 UI
|
||||
- [x] visibility 속성 추가 (모드별 true/false)
|
||||
- [x] 속성 패널 "표시" 탭 추가 (체크박스 UI)
|
||||
- [x] 렌더러에서 visibility 처리
|
||||
- [x] pop-break 컴포넌트 추가 (강제 줄바꿈)
|
||||
- [x] 컴포넌트 오버라이드 병합 로직
|
||||
- [x] 삭제 시 오버라이드 정리 로직
|
||||
|
||||
## Phase 4: 실제 컴포넌트 구현 (다음)
|
||||
|
||||
- [ ] pop-field: 입력/표시 필드
|
||||
- [ ] pop-button: 액션 버튼
|
||||
- [ ] pop-list: 데이터 리스트 (카드 템플릿)
|
||||
- [ ] pop-indicator: KPI/상태 표시
|
||||
- [ ] pop-scanner: 바코드/QR 스캔
|
||||
- [ ] pop-numpad: 숫자 입력 패드
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,9 @@
|
|||
|
||||
### 현재 상태
|
||||
|
||||
- **버전**: v3.0 (4모드 그리드)
|
||||
- **다음**: v4.0 (제약조건 기반) - 계획
|
||||
- **버전**: v4.0 (제약조건 기반) ✅
|
||||
- **Phase**: Phase 3 완료 (visibility + 줄바꿈)
|
||||
- **다음**: Phase 4 (실제 컴포넌트 구현)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -34,6 +35,7 @@
|
|||
| 파일 | 용도 |
|
||||
|------|------|
|
||||
| [SPEC.md](./SPEC.md) | 기술 스펙 |
|
||||
| [V4_UNIFIED_DESIGN_SPEC.md](./V4_UNIFIED_DESIGN_SPEC.md) | v4 통합 설계 모드 |
|
||||
| [PLAN.md](./PLAN.md) | 계획/로드맵 |
|
||||
| [CHANGELOG.md](./CHANGELOG.md) | 변경 이력 |
|
||||
| [decisions/](./decisions/) | 중요 결정 기록 (ADR) |
|
||||
|
|
@ -77,14 +79,38 @@
|
|||
v3: 4개 모드 각각 위치 설정 → 4배 작업량
|
||||
v4: 3가지 규칙만 설정 → 자동 적응
|
||||
|
||||
규칙:
|
||||
핵심 규칙:
|
||||
1. 크기: fixed(고정) / fill(채움) / hug(맞춤)
|
||||
2. 배치: direction, wrap, gap
|
||||
3. 반응형: breakpoint별 변경
|
||||
|
||||
Phase 3 추가:
|
||||
4. visibility: 모드별 표시/숨김
|
||||
5. pop-break: 강제 줄바꿈
|
||||
```
|
||||
|
||||
상세: [SPEC.md](./SPEC.md)
|
||||
상세: [V4_UNIFIED_DESIGN_SPEC.md](./V4_UNIFIED_DESIGN_SPEC.md)
|
||||
|
||||
---
|
||||
|
||||
*최종 업데이트: 2026-02-04*
|
||||
## Phase 3 완료 사항 (2026-02-04) ✅
|
||||
|
||||
### 새 기능
|
||||
- **visibility 속성**: 모드별 컴포넌트 표시/숨김 제어
|
||||
- **pop-break 컴포넌트**: Flexbox 강제 줄바꿈 (`flex-basis: 100%`)
|
||||
- **컴포넌트 오버라이드 병합**: 모드별 설정 변경 (리스트 컬럼 수 등)
|
||||
- **오버라이드 정리 로직**: 컴포넌트 삭제 시 모든 오버라이드 자동 정리
|
||||
|
||||
### 사용 예시
|
||||
```
|
||||
태블릿: [A] [B] [C] [D] (한 줄)
|
||||
모바일: [A] [B] (두 줄, 줄바꿈 적용)
|
||||
[C] [D]
|
||||
```
|
||||
|
||||
### 참고
|
||||
- [decisions/002-phase3-visibility-break.md](./decisions/002-phase3-visibility-break.md) - 상세 설계
|
||||
|
||||
---
|
||||
|
||||
*최종 업데이트: 2026-02-04 (Phase 3 완료)*
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# POP v4 통합 설계 모드 스펙
|
||||
|
||||
**작성일: 2026-02-04**
|
||||
**상태: Phase 1.6 완료 (비율 스케일링 시스템)**
|
||||
**최종 업데이트: 2026-02-04**
|
||||
**상태: Phase 3 완료 (visibility + 줄바꿈 컴포넌트)**
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -52,12 +53,12 @@ v3/v4 탭을 제거하고, **v4 자동 모드를 기본**으로 하되 **모드
|
|||
│ │ [태블릿↕][태블릿↔(기본)] │ │
|
||||
│ 필드 │ 너비: [====●====] 1024 x 768 │ │
|
||||
│ 버튼 │ │ │
|
||||
│ 리스트 │ ┌──────────────────────────────┐ │ │
|
||||
│ 인디케이터 │ │ [필드1] [필드2] [필드3] │ │ │
|
||||
│ 스캐너 │ │ [필드4] [Spacer] [버튼] │ │ │
|
||||
│ 숫자패드 │ │ │ │ │
|
||||
│ 리스트 │ ┌──────────────────────────────┐ │ 탭: 크기 │
|
||||
│ 인디케이터 │ │ [필드1] [필드2] [필드3] │ │ 설정 │
|
||||
│ 스캐너 │ │ [필드4] [Spacer] [버튼] │ │ 표시 ⬅ 🆕│
|
||||
│ 숫자패드 │ │ │ │ 데이터 │
|
||||
│ 스페이서 │ │ (가로 배치 + 자동 줄바꿈) │ │ │
|
||||
│ │ │ (스크롤 가능) │ │ │
|
||||
│ 줄바꿈 🆕 │ │ (스크롤 가능) │ │ │
|
||||
│ │ └──────────────────────────────┘ │ │
|
||||
│ │ 태블릿 가로 (1024x768) │ │
|
||||
└────────────┴────────────────────────────────────┴───────────────┘
|
||||
|
|
@ -73,14 +74,24 @@ v3/v4 탭을 제거하고, **v4 자동 모드를 기본**으로 하되 **모드
|
|||
| Adalo 2.0 | Flexbox + Constraints |
|
||||
| **POP v4** | **Flexbox (horizontal + wrap)** |
|
||||
|
||||
### Spacer 컴포넌트 사용법
|
||||
### 특수 컴포넌트 사용법
|
||||
|
||||
#### Spacer (빈 공간)
|
||||
```
|
||||
[버튼A] [Spacer(fill)] [버튼B] → 버튼B가 오른쪽 끝으로
|
||||
[Spacer] [컴포넌트] [Spacer] → 컴포넌트가 가운데로
|
||||
[Spacer(fill)] [컴포넌트] → 컴포넌트가 오른쪽으로
|
||||
```
|
||||
|
||||
#### 줄바꿈 (Break) 🆕 Phase 3
|
||||
```
|
||||
[필드A] [필드B] [줄바꿈] [필드C] → 필드C가 새 줄로 이동
|
||||
|
||||
태블릿: [필드A] [필드B] [필드C] ← 줄바꿈 숨김 (한 줄)
|
||||
모바일: [필드A] [필드B] ← 줄바꿈 표시 (두 줄)
|
||||
[필드C]
|
||||
```
|
||||
|
||||
### 프리셋 버튼 (4개 모드)
|
||||
|
||||
| 버튼 | 해상도 | 설명 |
|
||||
|
|
@ -296,5 +307,85 @@ scaledPadding = originalPadding * scale
|
|||
|
||||
---
|
||||
|
||||
## Phase 3: Visibility + 줄바꿈 컴포넌트 (완료) ✅
|
||||
|
||||
### 개요
|
||||
모드별 컴포넌트 표시/숨김 제어 및 강제 줄바꿈 기능 추가.
|
||||
|
||||
### 추가 타입
|
||||
|
||||
#### visibility 속성
|
||||
```typescript
|
||||
interface PopComponentDefinitionV4 {
|
||||
// 기존 속성...
|
||||
|
||||
// 🆕 모드별 표시/숨김
|
||||
visibility?: {
|
||||
tablet_landscape?: boolean;
|
||||
tablet_portrait?: boolean;
|
||||
mobile_landscape?: boolean;
|
||||
mobile_portrait?: boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### pop-break 컴포넌트
|
||||
```typescript
|
||||
type PopComponentType =
|
||||
| "pop-field"
|
||||
| "pop-button"
|
||||
| "pop-list"
|
||||
| "pop-indicator"
|
||||
| "pop-scanner"
|
||||
| "pop-numpad"
|
||||
| "pop-spacer"
|
||||
| "pop-break"; // 🆕 줄바꿈
|
||||
```
|
||||
|
||||
### 사용 예시
|
||||
|
||||
#### 모바일 전용 버튼
|
||||
```typescript
|
||||
{
|
||||
id: "call-button",
|
||||
type: "pop-button",
|
||||
label: "전화 걸기",
|
||||
visibility: {
|
||||
tablet_landscape: false, // 태블릿: 숨김
|
||||
mobile_portrait: true, // 모바일: 표시
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### 모드별 줄바꿈
|
||||
```
|
||||
레이아웃: [A] [B] [줄바꿈] [C] [D]
|
||||
|
||||
줄바꿈 visibility: { tablet_landscape: false, mobile_portrait: true }
|
||||
|
||||
결과:
|
||||
태블릿: [A] [B] [C] [D] (한 줄)
|
||||
모바일: [A] [B] (두 줄)
|
||||
[C] [D]
|
||||
```
|
||||
|
||||
### 속성 패널 "표시" 탭
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ 탭: 크기 설정 표시 📍│
|
||||
├─────────────────────┤
|
||||
│ 모드별 표시 설정 │
|
||||
│ ☑ 태블릿 가로 │
|
||||
│ ☑ 태블릿 세로 │
|
||||
│ ☐ 모바일 가로 (숨김)│
|
||||
│ ☑ 모바일 세로 │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### 참고 문서
|
||||
- [decisions/002-phase3-visibility-break.md](./decisions/002-phase3-visibility-break.md) - 상세 설계
|
||||
|
||||
---
|
||||
|
||||
*이 문서는 v4 통합 설계 모드의 스펙을 정의합니다.*
|
||||
*최종 업데이트: 2026-02-04*
|
||||
*최종 업데이트: 2026-02-04 (Phase 3 완료)*
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
## Quick Reference
|
||||
|
||||
### 총 컴포넌트 수: 14개
|
||||
### 총 컴포넌트 수: 15개 (🆕 줄바꿈 추가)
|
||||
|
||||
| 분류 | 개수 | 컴포넌트 |
|
||||
|------|------|----------|
|
||||
| 레이아웃 | 3 | container, tab-panel, **spacer** |
|
||||
| 레이아웃 | 4 | container, tab-panel, **spacer**, **break 🆕** |
|
||||
| 데이터 표시 | 4 | data-table, card-list, kpi-gauge, status-indicator |
|
||||
| 입력 | 4 | number-pad, barcode-scanner, form-field, action-button |
|
||||
| 특화 기능 | 3 | timer, alarm-list, process-flow |
|
||||
|
|
@ -38,17 +38,18 @@
|
|||
| 1 | pop-container | 레이아웃 뼈대 |
|
||||
| 2 | pop-tab-panel | 정보 분류 |
|
||||
| 3 | **pop-spacer** | **빈 공간 (정렬용)** |
|
||||
| 4 | pop-data-table | 대량 데이터 |
|
||||
| 5 | pop-card-list | 시각적 목록 |
|
||||
| 6 | pop-kpi-gauge | 목표 달성률 |
|
||||
| 7 | pop-status-indicator | 상태 표시 |
|
||||
| 8 | pop-number-pad | 수량 입력 |
|
||||
| 9 | pop-barcode-scanner | 스캔 입력 |
|
||||
| 10 | pop-form-field | 범용 입력 |
|
||||
| 11 | pop-action-button | 작업 실행 |
|
||||
| 12 | pop-timer | 시간 측정 |
|
||||
| 13 | pop-alarm-list | 알람 관리 |
|
||||
| 14 | pop-process-flow | 공정 현황 |
|
||||
| 4 | **pop-break 🆕** | **강제 줄바꿈 (Flexbox)** |
|
||||
| 5 | pop-data-table | 대량 데이터 |
|
||||
| 6 | pop-card-list | 시각적 목록 |
|
||||
| 7 | pop-kpi-gauge | 목표 달성률 |
|
||||
| 8 | pop-status-indicator | 상태 표시 |
|
||||
| 9 | pop-number-pad | 수량 입력 |
|
||||
| 10 | pop-barcode-scanner | 스캔 입력 |
|
||||
| 11 | pop-form-field | 범용 입력 |
|
||||
| 12 | pop-action-button | 작업 실행 |
|
||||
| 13 | pop-timer | 시간 측정 |
|
||||
| 14 | pop-alarm-list | 알람 관리 |
|
||||
| 15 | pop-process-flow | 공정 현황 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -109,7 +110,107 @@
|
|||
|
||||
---
|
||||
|
||||
## 3. pop-data-table
|
||||
## 4. pop-break (v4 전용) 🆕
|
||||
|
||||
역할: Flexbox에서 강제 줄바꿈을 위한 컴포넌트 (업계 표준: Figma Auto Layout의 줄바꿈과 동일)
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 강제 줄바꿈 | `flex-basis: 100%`로 다음 컴포넌트를 새 줄로 이동 |
|
||||
| 모드별 표시 | visibility 속성으로 특정 모드에서만 줄바꿈 적용 |
|
||||
| 시각적 표시 | 디자인 모드에서만 점선으로 표시 |
|
||||
| 실제 화면 | 높이 0px (완전히 보이지 않음) |
|
||||
|
||||
### 동작 원리
|
||||
|
||||
```
|
||||
Flexbox wrap: true 상태에서
|
||||
flex-basis: 100%를 가진 요소 → 전체 너비 차지 → 다음 요소는 자동으로 새 줄로 이동
|
||||
```
|
||||
|
||||
### 사용 예시
|
||||
|
||||
```
|
||||
[필드A] [필드B] [줄바꿈] [필드C] [필드D]
|
||||
|
||||
결과:
|
||||
┌────────────────────┐
|
||||
│ [필드A] [필드B] │ ← 첫째 줄
|
||||
│ [필드C] [필드D] │ ← 둘째 줄 (줄바꿈 후)
|
||||
└────────────────────┘
|
||||
```
|
||||
|
||||
### 모드별 줄바꿈
|
||||
|
||||
```typescript
|
||||
// 줄바꿈 컴포넌트 설정
|
||||
{
|
||||
id: "break-1",
|
||||
type: "pop-break",
|
||||
visibility: {
|
||||
tablet_landscape: false, // 태블릿: 줄바꿈 숨김 (한 줄)
|
||||
mobile_portrait: true, // 모바일: 줄바꿈 표시 (두 줄)
|
||||
}
|
||||
}
|
||||
|
||||
// 결과
|
||||
태블릿: [A] [B] [C] [D] (한 줄)
|
||||
모바일: [A] [B] (두 줄)
|
||||
[C] [D]
|
||||
```
|
||||
|
||||
### 기본 설정
|
||||
|
||||
| 속성 | 기본값 |
|
||||
|------|--------|
|
||||
| width | fill (`flex-basis: 100%`) |
|
||||
| height | 0px (높이 없음) |
|
||||
| 디자인 모드 표시 | 점선 + "줄바꿈" 텍스트 (높이 16px) |
|
||||
| 실제 모드 | 완전히 투명 (높이 0px) |
|
||||
| flex-basis | 100% (핵심 속성) |
|
||||
|
||||
### CSS 구현
|
||||
|
||||
```css
|
||||
/* 디자인 모드 */
|
||||
.pop-break-design {
|
||||
flex-basis: 100%;
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 실제 모드 */
|
||||
.pop-break-runtime {
|
||||
flex-basis: 100%;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 업계 비교
|
||||
|
||||
| 서비스 | 줄바꿈 방식 |
|
||||
|--------|------------|
|
||||
| Figma Auto Layout | "Wrap" 설정 + 수동 줄 분리 |
|
||||
| Webflow Flexbox | "Wrap" + 100% width spacer |
|
||||
| Framer | "Break" 컴포넌트 |
|
||||
| **POP v4** | **pop-break (flex-basis: 100%)** |
|
||||
|
||||
### 주의사항
|
||||
|
||||
- 컨테이너의 `wrap: true` 설정 필수
|
||||
- wrap이 false면 줄바꿈 무시됨
|
||||
- visibility로 모드별 제어 가능
|
||||
- 디자인 모드에서만 시각적으로 보임
|
||||
|
||||
---
|
||||
|
||||
## 5. pop-data-table
|
||||
|
||||
역할: 대량 데이터 표시, 선택, 편집
|
||||
|
||||
|
|
@ -317,4 +418,44 @@
|
|||
|
||||
---
|
||||
|
||||
*최종 업데이트: 2026-01-29*
|
||||
## Phase 3 업데이트 (2026-02-04) 🆕
|
||||
|
||||
### 추가된 컴포넌트
|
||||
|
||||
#### pop-break (줄바꿈)
|
||||
- **역할**: Flexbox에서 강제 줄바꿈
|
||||
- **핵심 기술**: `flex-basis: 100%`
|
||||
- **모드별 제어**: visibility 속성 지원
|
||||
- **시각적 표시**: 디자인 모드에서만 점선 표시 (실제 높이 0px)
|
||||
|
||||
### 모든 컴포넌트 공통 추가 속성
|
||||
|
||||
#### visibility (모드별 표시/숨김)
|
||||
```typescript
|
||||
visibility?: {
|
||||
tablet_landscape?: boolean;
|
||||
tablet_portrait?: boolean;
|
||||
mobile_landscape?: boolean;
|
||||
mobile_portrait?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**사용 예시**:
|
||||
```typescript
|
||||
// 모바일 전용 버튼
|
||||
{
|
||||
type: "pop-action-button",
|
||||
visibility: {
|
||||
tablet_landscape: false,
|
||||
mobile_portrait: true,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 참고 문서
|
||||
- [decisions/002-phase3-visibility-break.md](./decisions/002-phase3-visibility-break.md) - 상세 설계
|
||||
- [V4_UNIFIED_DESIGN_SPEC.md](./V4_UNIFIED_DESIGN_SPEC.md) - v4 통합 설계
|
||||
|
||||
---
|
||||
|
||||
*최종 업데이트: 2026-02-04 (Phase 3 완료)*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,690 @@
|
|||
# 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 레이아웃 시스템의 핵심 기능이 완성되었으며, 실제 컴포넌트 구현 단계로 진행할 수 있습니다.
|
||||
Loading…
Reference in New Issue