디자이너 캔버스 UX 개선: 헤더 제거 + 실제 데이터 렌더링 + 컴포넌트 목록

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
SeongHyun Kim 2026-02-10 18:02:30 +09:00
parent f825d65bfc
commit 1116fb350a
3 changed files with 75 additions and 35 deletions

View File

@ -652,6 +652,9 @@ export default function PopDesigner({
? (updates) => handleUpdateComponent(selectedComponentId, updates) ? (updates) => handleUpdateComponent(selectedComponentId, updates)
: undefined : undefined
} }
allComponents={Object.values(layout.components)}
onSelectComponent={setSelectedComponentId}
selectedComponentId={selectedComponentId}
/> />
</ResizablePanel> </ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>

View File

@ -7,7 +7,6 @@ import {
PopGridPosition, PopGridPosition,
GridMode, GridMode,
GRID_BREAKPOINTS, GRID_BREAKPOINTS,
PopComponentType,
} from "../types/pop-layout"; } from "../types/pop-layout";
import { import {
Settings, Settings,
@ -16,6 +15,7 @@ import {
Grid3x3, Grid3x3,
MoveHorizontal, MoveHorizontal,
MoveVertical, MoveVertical,
Layers,
} from "lucide-react"; } from "lucide-react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@ -36,12 +36,21 @@ interface ComponentEditorPanelProps {
onUpdateComponent?: (updates: Partial<PopComponentDefinitionV5>) => void; onUpdateComponent?: (updates: Partial<PopComponentDefinitionV5>) => void;
/** 추가 className */ /** 추가 className */
className?: string; className?: string;
/** 그리드에 배치된 모든 컴포넌트 */
allComponents?: PopComponentDefinitionV5[];
/** 컴포넌트 선택 콜백 */
onSelectComponent?: (componentId: string) => void;
/** 현재 선택된 컴포넌트 ID */
selectedComponentId?: string | null;
} }
// ======================================== // ========================================
// 컴포넌트 타입별 라벨 // 컴포넌트 타입별 라벨
// ======================================== // ========================================
const COMPONENT_TYPE_LABELS: Record<PopComponentType, string> = { const COMPONENT_TYPE_LABELS: Record<string, string> = {
"pop-sample": "샘플",
"pop-text": "텍스트",
"pop-dashboard": "대시보드",
"pop-field": "필드", "pop-field": "필드",
"pop-button": "버튼", "pop-button": "버튼",
"pop-list": "리스트", "pop-list": "리스트",
@ -61,6 +70,9 @@ export default function ComponentEditorPanel({
currentMode, currentMode,
onUpdateComponent, onUpdateComponent,
className, className,
allComponents,
onSelectComponent,
selectedComponentId,
}: ComponentEditorPanelProps) { }: ComponentEditorPanelProps) {
const breakpoint = GRID_BREAKPOINTS[currentMode]; const breakpoint = GRID_BREAKPOINTS[currentMode];
@ -118,7 +130,44 @@ export default function ComponentEditorPanel({
</TabsList> </TabsList>
{/* 위치 탭 */} {/* 위치 탭 */}
<TabsContent value="position" className="flex-1 overflow-auto p-4"> <TabsContent value="position" className="min-h-0 flex-1 overflow-auto p-4">
{/* 배치된 컴포넌트 목록 */}
{allComponents && allComponents.length > 0 && (
<div className="mb-4">
<div className="flex items-center gap-1 mb-2">
<Layers className="h-3 w-3 text-muted-foreground" />
<span className="text-xs font-medium text-muted-foreground">
({allComponents.length})
</span>
</div>
<div className="space-y-1">
{allComponents.map((comp) => {
const label = comp.label
|| COMPONENT_TYPE_LABELS[comp.type]
|| comp.type;
const isActive = comp.id === selectedComponentId;
return (
<button
key={comp.id}
onClick={() => onSelectComponent?.(comp.id)}
className={cn(
"flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs transition-colors",
isActive
? "bg-primary/10 text-primary font-medium"
: "hover:bg-gray-100 text-gray-600"
)}
>
<span className="truncate flex-1">{label}</span>
<span className="shrink-0 text-[10px] text-gray-400">
({comp.position.col},{comp.position.row})
</span>
</button>
);
})}
</div>
<div className="h-px bg-gray-200 mt-3" />
</div>
)}
<PositionForm <PositionForm
component={component} component={component}
currentMode={currentMode} currentMode={currentMode}

View File

@ -505,41 +505,29 @@ function ComponentContent({ component, effectivePosition, isDesignMode, isSelect
const registeredComp = PopComponentRegistry.getComponent(component.type); const registeredComp = PopComponentRegistry.getComponent(component.type);
const PreviewComponent = registeredComp?.preview; const PreviewComponent = registeredComp?.preview;
// 디자인 모드: 미리보기 컴포넌트 또는 플레이스홀더 표시 // 디자인 모드: 실제 컴포넌트 또는 미리보기 표시 (헤더 없음 - 뷰어와 동일하게)
if (isDesignMode) { if (isDesignMode) {
const ActualComp = registeredComp?.component;
// 실제 컴포넌트가 등록되어 있으면 실제 데이터로 렌더링 (대시보드 등)
if (ActualComp) {
return (
<div className="h-full w-full overflow-hidden pointer-events-none">
<ActualComp config={component.config} label={component.label} />
</div>
);
}
// 미등록: preview 컴포넌트 또는 기본 플레이스홀더
return ( return (
<div className="flex h-full w-full flex-col"> <div className="flex h-full w-full items-center justify-center overflow-hidden">
{/* 헤더 */} {PreviewComponent ? (
<div <PreviewComponent config={component.config} />
className={cn( ) : (
"flex h-5 shrink-0 items-center border-b px-2", <span className="text-xs text-gray-400 p-2">
isSelected ? "bg-primary/10 border-primary" : "bg-gray-50 border-gray-200" {typeLabel}
)}
>
<span className={cn(
"text-[10px] font-medium truncate",
isSelected ? "text-primary" : "text-gray-600"
)}>
{component.label || typeLabel}
</span> </span>
</div> )}
{/* 내용: 등록된 preview 컴포넌트 또는 기본 플레이스홀더 */}
<div className="flex flex-1 items-center justify-center overflow-hidden">
{PreviewComponent ? (
<PreviewComponent config={component.config} />
) : (
<span className="text-xs text-gray-400 p-2">
{typeLabel}
</span>
)}
</div>
{/* 위치 정보 표시 (유효 위치 사용) */}
<div className="absolute bottom-1 right-1 text-[9px] text-gray-400 bg-white/80 px-1 rounded">
{effectivePosition.col},{effectivePosition.row}
({effectivePosition.colSpan}×{effectivePosition.rowSpan})
</div>
</div> </div>
); );
} }