722 lines
22 KiB
TypeScript
722 lines
22 KiB
TypeScript
"use client";
|
||
|
||
import React from "react";
|
||
import { cn } from "@/lib/utils";
|
||
import {
|
||
PopComponentDefinitionV4,
|
||
PopSizeConstraintV4,
|
||
PopContainerV4,
|
||
PopComponentType,
|
||
} from "../types/pop-layout";
|
||
import {
|
||
Settings,
|
||
Database,
|
||
Link2,
|
||
MoveHorizontal,
|
||
MoveVertical,
|
||
Square,
|
||
Maximize2,
|
||
AlignCenter,
|
||
Eye,
|
||
} from "lucide-react";
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Input } from "@/components/ui/input";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
|
||
// ========================================
|
||
// Props 정의
|
||
// ========================================
|
||
|
||
interface ComponentEditorPanelV4Props {
|
||
/** 선택된 컴포넌트 */
|
||
component: PopComponentDefinitionV4 | null;
|
||
/** 선택된 컨테이너 */
|
||
container: PopContainerV4 | null;
|
||
/** 현재 뷰포트 모드 */
|
||
currentViewportMode?: "mobile_portrait" | "mobile_landscape" | "tablet_portrait" | "tablet_landscape";
|
||
/** 컴포넌트 업데이트 */
|
||
onUpdateComponent?: (updates: Partial<PopComponentDefinitionV4>) => void;
|
||
/** 컨테이너 업데이트 */
|
||
onUpdateContainer?: (updates: Partial<PopContainerV4>) => void;
|
||
/** 추가 className */
|
||
className?: string;
|
||
}
|
||
|
||
// ========================================
|
||
// 컴포넌트 타입별 라벨
|
||
// ========================================
|
||
const COMPONENT_TYPE_LABELS: Record<PopComponentType, string> = {
|
||
"pop-field": "필드",
|
||
"pop-button": "버튼",
|
||
"pop-list": "리스트",
|
||
"pop-indicator": "인디케이터",
|
||
"pop-scanner": "스캐너",
|
||
"pop-numpad": "숫자패드",
|
||
"pop-spacer": "스페이서",
|
||
"pop-break": "줄바꿈",
|
||
};
|
||
|
||
// ========================================
|
||
// v4 컴포넌트 편집 패널
|
||
//
|
||
// 핵심:
|
||
// - 크기 제약 편집 (fixed/fill/hug)
|
||
// - 반응형 숨김 설정
|
||
// - 개별 정렬 설정
|
||
// ========================================
|
||
|
||
export function ComponentEditorPanelV4({
|
||
component,
|
||
container,
|
||
currentViewportMode = "tablet_landscape",
|
||
onUpdateComponent,
|
||
onUpdateContainer,
|
||
className,
|
||
}: ComponentEditorPanelV4Props) {
|
||
// 아무것도 선택되지 않은 경우
|
||
if (!component && !container) {
|
||
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>
|
||
</div>
|
||
<div className="flex flex-1 items-center justify-center p-4 text-sm text-muted-foreground">
|
||
컴포넌트 또는 컨테이너를 선택하세요
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 컨테이너가 선택된 경우
|
||
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>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 컴포넌트가 선택된 경우
|
||
return (
|
||
<div className={cn("flex h-full flex-col", className)}>
|
||
{/* 헤더 */}
|
||
<div className="border-b px-4 py-3">
|
||
<h3 className="text-sm font-medium">
|
||
{component!.label || COMPONENT_TYPE_LABELS[component!.type]}
|
||
</h3>
|
||
<p className="text-xs text-muted-foreground">{component!.type}</p>
|
||
</div>
|
||
|
||
{/* 탭 컨텐츠 */}
|
||
<Tabs defaultValue="size" className="flex-1">
|
||
<TabsList className="w-full justify-start rounded-none border-b bg-transparent px-2">
|
||
<TabsTrigger value="size" className="gap-1 text-xs">
|
||
<Maximize2 className="h-3 w-3" />
|
||
크기
|
||
</TabsTrigger>
|
||
<TabsTrigger value="settings" className="gap-1 text-xs">
|
||
<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" />
|
||
데이터
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
|
||
{/* 크기 제약 탭 */}
|
||
<TabsContent value="size" className="flex-1 overflow-auto p-4">
|
||
<SizeConstraintForm
|
||
component={component!}
|
||
onUpdate={onUpdateComponent}
|
||
/>
|
||
</TabsContent>
|
||
|
||
{/* 기본 설정 탭 */}
|
||
<TabsContent value="settings" className="flex-1 overflow-auto p-4">
|
||
<ComponentSettingsForm
|
||
component={component!}
|
||
onUpdate={onUpdateComponent}
|
||
/>
|
||
</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 />
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ========================================
|
||
// 크기 제약 폼
|
||
// ========================================
|
||
|
||
interface SizeConstraintFormProps {
|
||
component: PopComponentDefinitionV4;
|
||
onUpdate?: (updates: Partial<PopComponentDefinitionV4>) => void;
|
||
}
|
||
|
||
function SizeConstraintForm({ component, onUpdate }: SizeConstraintFormProps) {
|
||
const { size } = component;
|
||
|
||
const handleSizeChange = (
|
||
field: keyof PopSizeConstraintV4,
|
||
value: string | number | undefined
|
||
) => {
|
||
onUpdate?.({
|
||
size: {
|
||
...size,
|
||
[field]: value,
|
||
},
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* 너비 설정 */}
|
||
<div className="space-y-3">
|
||
<Label className="text-xs font-medium flex items-center gap-1">
|
||
<MoveHorizontal className="h-3 w-3" />
|
||
너비
|
||
</Label>
|
||
|
||
<div className="flex gap-1">
|
||
<SizeButton
|
||
active={size.width === "fixed"}
|
||
onClick={() => handleSizeChange("width", "fixed")}
|
||
label="고정"
|
||
description="px"
|
||
/>
|
||
<SizeButton
|
||
active={size.width === "fill"}
|
||
onClick={() => handleSizeChange("width", "fill")}
|
||
label="채움"
|
||
description="flex"
|
||
/>
|
||
<SizeButton
|
||
active={size.width === "hug"}
|
||
onClick={() => handleSizeChange("width", "hug")}
|
||
label="맞춤"
|
||
description="auto"
|
||
/>
|
||
</div>
|
||
|
||
{/* 고정 너비 입력 */}
|
||
{size.width === "fixed" && (
|
||
<div className="flex items-center gap-2">
|
||
<Input
|
||
type="number"
|
||
className="h-8 w-24 text-xs"
|
||
value={size.fixedWidth || ""}
|
||
onChange={(e) =>
|
||
handleSizeChange(
|
||
"fixedWidth",
|
||
e.target.value ? Number(e.target.value) : undefined
|
||
)
|
||
}
|
||
placeholder="너비"
|
||
/>
|
||
<span className="text-xs text-muted-foreground">px</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* 채움일 때 최소/최대 */}
|
||
{size.width === "fill" && (
|
||
<div className="flex items-center gap-2">
|
||
<Input
|
||
type="number"
|
||
className="h-8 w-20 text-xs"
|
||
value={size.minWidth || ""}
|
||
onChange={(e) =>
|
||
handleSizeChange(
|
||
"minWidth",
|
||
e.target.value ? Number(e.target.value) : undefined
|
||
)
|
||
}
|
||
placeholder="최소"
|
||
/>
|
||
<span className="text-xs text-muted-foreground">~</span>
|
||
<Input
|
||
type="number"
|
||
className="h-8 w-20 text-xs"
|
||
value={size.maxWidth || ""}
|
||
onChange={(e) =>
|
||
handleSizeChange(
|
||
"maxWidth",
|
||
e.target.value ? Number(e.target.value) : undefined
|
||
)
|
||
}
|
||
placeholder="최대"
|
||
/>
|
||
<span className="text-xs text-muted-foreground">px</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 높이 설정 */}
|
||
<div className="space-y-3">
|
||
<Label className="text-xs font-medium flex items-center gap-1">
|
||
<MoveVertical className="h-3 w-3" />
|
||
높이
|
||
</Label>
|
||
|
||
<div className="flex gap-1">
|
||
<SizeButton
|
||
active={size.height === "fixed"}
|
||
onClick={() => handleSizeChange("height", "fixed")}
|
||
label="고정"
|
||
description="px"
|
||
/>
|
||
<SizeButton
|
||
active={size.height === "fill"}
|
||
onClick={() => handleSizeChange("height", "fill")}
|
||
label="채움"
|
||
description="flex"
|
||
/>
|
||
<SizeButton
|
||
active={size.height === "hug"}
|
||
onClick={() => handleSizeChange("height", "hug")}
|
||
label="맞춤"
|
||
description="auto"
|
||
/>
|
||
</div>
|
||
|
||
{/* 고정 높이 입력 */}
|
||
{size.height === "fixed" && (
|
||
<div className="flex items-center gap-2">
|
||
<Input
|
||
type="number"
|
||
className="h-8 w-24 text-xs"
|
||
value={size.fixedHeight || ""}
|
||
onChange={(e) =>
|
||
handleSizeChange(
|
||
"fixedHeight",
|
||
e.target.value ? Number(e.target.value) : undefined
|
||
)
|
||
}
|
||
placeholder="높이"
|
||
/>
|
||
<span className="text-xs text-muted-foreground">px</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* 채움일 때 최소 */}
|
||
{size.height === "fill" && (
|
||
<div className="flex items-center gap-2">
|
||
<Input
|
||
type="number"
|
||
className="h-8 w-24 text-xs"
|
||
value={size.minHeight || ""}
|
||
onChange={(e) =>
|
||
handleSizeChange(
|
||
"minHeight",
|
||
e.target.value ? Number(e.target.value) : undefined
|
||
)
|
||
}
|
||
placeholder="최소 높이"
|
||
/>
|
||
<span className="text-xs text-muted-foreground">px</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 개별 정렬 */}
|
||
<div className="space-y-3">
|
||
<Label className="text-xs font-medium flex items-center gap-1">
|
||
<AlignCenter className="h-3 w-3" />
|
||
개별 정렬
|
||
</Label>
|
||
<Select
|
||
value={component.alignSelf || "none"}
|
||
onValueChange={(value) =>
|
||
onUpdate?.({
|
||
alignSelf: value === "none" ? undefined : (value as any),
|
||
})
|
||
}
|
||
>
|
||
<SelectTrigger className="h-8 text-xs">
|
||
<SelectValue placeholder="컨테이너 설정 따름" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="none">컨테이너 설정 따름</SelectItem>
|
||
<SelectItem value="start">시작</SelectItem>
|
||
<SelectItem value="center">가운데</SelectItem>
|
||
<SelectItem value="end">끝</SelectItem>
|
||
<SelectItem value="stretch">늘이기</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ========================================
|
||
// 크기 버튼 컴포넌트
|
||
// ========================================
|
||
|
||
interface SizeButtonProps {
|
||
active: boolean;
|
||
onClick: () => void;
|
||
label: string;
|
||
description: string;
|
||
}
|
||
|
||
function SizeButton({ active, onClick, label, description }: SizeButtonProps) {
|
||
return (
|
||
<button
|
||
className={cn(
|
||
"flex-1 flex flex-col items-center gap-0.5 rounded-md border p-2 text-xs transition-colors",
|
||
active
|
||
? "border-primary bg-primary/10 text-primary"
|
||
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
|
||
)}
|
||
onClick={onClick}
|
||
>
|
||
<span className="font-medium">{label}</span>
|
||
<span className="text-[10px] text-muted-foreground">{description}</span>
|
||
</button>
|
||
);
|
||
}
|
||
|
||
// ========================================
|
||
// 컨테이너 설정 폼
|
||
// ========================================
|
||
|
||
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">
|
||
{/* 방향 */}
|
||
<div className="space-y-3">
|
||
<Label className="text-xs font-medium">방향</Label>
|
||
<div className="flex gap-1">
|
||
<Button
|
||
variant={container.direction === "horizontal" ? "default" : "outline"}
|
||
size="sm"
|
||
className="flex-1 h-8 text-xs"
|
||
onClick={() => onUpdate?.({ direction: "horizontal" })}
|
||
disabled={isNonDefaultMode}
|
||
>
|
||
가로
|
||
</Button>
|
||
<Button
|
||
variant={container.direction === "vertical" ? "default" : "outline"}
|
||
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>
|
||
|
||
{/* 줄바꿈 */}
|
||
<div className="space-y-3">
|
||
<Label className="text-xs font-medium">줄바꿈</Label>
|
||
<div className="flex gap-1">
|
||
<Button
|
||
variant={container.wrap ? "default" : "outline"}
|
||
size="sm"
|
||
className="flex-1 h-8 text-xs"
|
||
onClick={() => onUpdate?.({ wrap: true })}
|
||
disabled={isNonDefaultMode}
|
||
>
|
||
허용
|
||
</Button>
|
||
<Button
|
||
variant={!container.wrap ? "default" : "outline"}
|
||
size="sm"
|
||
className="flex-1 h-8 text-xs"
|
||
onClick={() => onUpdate?.({ wrap: false })}
|
||
disabled={isNonDefaultMode}
|
||
>
|
||
금지
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 간격 */}
|
||
<div className="space-y-3">
|
||
<Label className="text-xs font-medium">간격 (gap)</Label>
|
||
<div className="flex items-center gap-2">
|
||
<Input
|
||
type="number"
|
||
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>
|
||
</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={container.padding || 0}
|
||
onChange={(e) =>
|
||
onUpdate?.({ padding: Number(e.target.value) || undefined })
|
||
}
|
||
disabled={isNonDefaultMode}
|
||
/>
|
||
<span className="text-xs text-muted-foreground">px</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 정렬 */}
|
||
<div className="space-y-3">
|
||
<Label className="text-xs font-medium">교차축 정렬</Label>
|
||
<Select
|
||
value={container.alignItems}
|
||
onValueChange={(value) => onUpdate?.({ alignItems: value as any })}
|
||
disabled={isNonDefaultMode}
|
||
>
|
||
<SelectTrigger className="h-8 text-xs">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="start">시작</SelectItem>
|
||
<SelectItem value="center">가운데</SelectItem>
|
||
<SelectItem value="end">끝</SelectItem>
|
||
<SelectItem value="stretch">늘이기</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<Label className="text-xs font-medium">주축 정렬</Label>
|
||
<Select
|
||
value={container.justifyContent}
|
||
onValueChange={(value) => onUpdate?.({ justifyContent: value as any })}
|
||
disabled={isNonDefaultMode}
|
||
>
|
||
<SelectTrigger className="h-8 text-xs">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="start">시작</SelectItem>
|
||
<SelectItem value="center">가운데</SelectItem>
|
||
<SelectItem value="end">끝</SelectItem>
|
||
<SelectItem value="space-between">균등 배치</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ========================================
|
||
// 컴포넌트 설정 폼
|
||
// ========================================
|
||
|
||
interface ComponentSettingsFormProps {
|
||
component: PopComponentDefinitionV4;
|
||
onUpdate?: (updates: Partial<PopComponentDefinitionV4>) => void;
|
||
}
|
||
|
||
function ComponentSettingsForm({
|
||
component,
|
||
onUpdate,
|
||
}: ComponentSettingsFormProps) {
|
||
return (
|
||
<div className="space-y-4">
|
||
{/* 라벨 입력 */}
|
||
<div className="space-y-1.5">
|
||
<Label className="text-xs font-medium">라벨</Label>
|
||
<Input
|
||
type="text"
|
||
className="h-8 text-xs"
|
||
value={component.label || ""}
|
||
onChange={(e) => onUpdate?.({ label: e.target.value })}
|
||
placeholder="컴포넌트 라벨"
|
||
/>
|
||
</div>
|
||
|
||
{/* 타입별 설정 (TODO: 상세 구현) */}
|
||
<div className="rounded-lg border border-dashed border-gray-300 bg-gray-50 p-4">
|
||
<p className="text-center text-xs text-muted-foreground">
|
||
{COMPONENT_TYPE_LABELS[component.type]} 상세 설정
|
||
</p>
|
||
<p className="mt-1 text-center text-xs text-muted-foreground">
|
||
(추후 구현 예정)
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ========================================
|
||
// 모드별 표시/숨김 폼
|
||
// ========================================
|
||
|
||
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>
|
||
);
|
||
}
|
||
|
||
// ========================================
|
||
// 데이터 바인딩 플레이스홀더
|
||
// ========================================
|
||
|
||
function DataBindingPlaceholder() {
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="rounded-lg border border-dashed border-gray-300 bg-gray-50 p-4">
|
||
<div className="flex flex-col items-center gap-2">
|
||
<Database className="h-8 w-8 text-gray-400" />
|
||
<p className="text-center text-xs text-muted-foreground">
|
||
데이터 바인딩 설정
|
||
</p>
|
||
<p className="text-center text-xs text-muted-foreground">
|
||
테이블 선택 - 칼럼 선택 - 조인 설정
|
||
</p>
|
||
<p className="mt-2 text-center text-xs text-gray-400">
|
||
(추후 구현 예정)
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default ComponentEditorPanelV4;
|