674 lines
24 KiB
TypeScript
674 lines
24 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react";
|
|
import {
|
|
ComponentData,
|
|
WebType,
|
|
WidgetComponent,
|
|
GroupComponent,
|
|
DataTableComponent,
|
|
TableInfo,
|
|
LayoutComponent,
|
|
FileComponent,
|
|
AreaComponent,
|
|
} from "@/types/screen";
|
|
import { ColumnSpanPreset, COLUMN_SPAN_PRESETS } from "@/lib/constants/columnSpans";
|
|
|
|
// 컬럼 스팬 숫자 배열 (1~12)
|
|
const COLUMN_NUMBERS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
|
import { cn } from "@/lib/utils";
|
|
import DataTableConfigPanel from "./DataTableConfigPanel";
|
|
import { WebTypeConfigPanel } from "./WebTypeConfigPanel";
|
|
import { FileComponentConfigPanel } from "./FileComponentConfigPanel";
|
|
import { useWebTypes } from "@/hooks/admin/useWebTypes";
|
|
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
|
|
import {
|
|
BaseInputType,
|
|
BASE_INPUT_TYPE_OPTIONS,
|
|
getBaseInputType,
|
|
getDefaultDetailType,
|
|
getDetailTypes,
|
|
DetailTypeOption,
|
|
} from "@/types/input-type-mapping";
|
|
|
|
// 새로운 컴포넌트 설정 패널들
|
|
import { ButtonConfigPanel } from "../config-panels/ButtonConfigPanel";
|
|
import { CardConfigPanel } from "../config-panels/CardConfigPanel";
|
|
import { DashboardConfigPanel } from "../config-panels/DashboardConfigPanel";
|
|
import { StatsCardConfigPanel } from "../config-panels/StatsCardConfigPanel";
|
|
import { ProgressBarConfigPanel } from "../config-panels/ProgressBarConfigPanel";
|
|
import { ChartConfigPanel } from "../config-panels/ChartConfigPanel";
|
|
import { AlertConfigPanel } from "../config-panels/AlertConfigPanel";
|
|
import { BadgeConfigPanel } from "../config-panels/BadgeConfigPanel";
|
|
import { DynamicComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
|
|
import StyleEditor from "../StyleEditor";
|
|
import ResolutionPanel from "./ResolutionPanel";
|
|
|
|
interface UnifiedPropertiesPanelProps {
|
|
selectedComponent?: ComponentData;
|
|
tables: TableInfo[];
|
|
onUpdateProperty: (componentId: string, path: string, value: any) => void;
|
|
onDeleteComponent?: (componentId: string) => void;
|
|
onCopyComponent?: (componentId: string) => void;
|
|
currentTable?: TableInfo;
|
|
currentTableName?: string;
|
|
dragState?: any;
|
|
// 스타일 관련
|
|
onStyleChange?: (style: any) => void;
|
|
// 해상도 관련
|
|
currentResolution?: { name: string; width: number; height: number };
|
|
onResolutionChange?: (resolution: { name: string; width: number; height: number }) => void;
|
|
}
|
|
|
|
export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|
selectedComponent,
|
|
tables,
|
|
onUpdateProperty,
|
|
onDeleteComponent,
|
|
onCopyComponent,
|
|
currentTable,
|
|
currentTableName,
|
|
dragState,
|
|
onStyleChange,
|
|
currentResolution,
|
|
onResolutionChange,
|
|
}) => {
|
|
const { webTypes } = useWebTypes({ active: "Y" });
|
|
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
|
|
|
|
// 새로운 컴포넌트 시스템의 webType 동기화
|
|
useEffect(() => {
|
|
if (selectedComponent?.type === "component") {
|
|
const webType = selectedComponent.componentConfig?.webType;
|
|
if (webType) {
|
|
setLocalComponentDetailType(webType);
|
|
}
|
|
}
|
|
}, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]);
|
|
|
|
// 컴포넌트가 선택되지 않았을 때
|
|
if (!selectedComponent) {
|
|
return (
|
|
<div className="flex h-full flex-col items-center justify-center p-4 text-center">
|
|
<Settings className="mb-2 h-8 w-8 text-gray-300" />
|
|
<p className="text-[10px] text-gray-500">컴포넌트를 선택하여</p>
|
|
<p className="text-[10px] text-gray-500">속성을 편집하세요</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const handleUpdate = (path: string, value: any) => {
|
|
onUpdateProperty(selectedComponent.id, path, value);
|
|
};
|
|
|
|
// 드래그 중일 때 실시간 위치 표시
|
|
const currentPosition =
|
|
dragState?.isDragging && dragState?.draggedComponent?.id === selectedComponent.id
|
|
? dragState.currentPosition
|
|
: selectedComponent.position;
|
|
|
|
// 컴포넌트별 설정 패널 렌더링 함수 (DetailSettingsPanel의 로직)
|
|
const renderComponentConfigPanel = () => {
|
|
if (!selectedComponent) return null;
|
|
|
|
const componentType = selectedComponent.componentConfig?.type || selectedComponent.type;
|
|
|
|
const handleUpdateProperty = (path: string, value: any) => {
|
|
onUpdateProperty(selectedComponent.id, path, value);
|
|
};
|
|
|
|
switch (componentType) {
|
|
case "button":
|
|
case "button-primary":
|
|
case "button-secondary":
|
|
// 🔧 component.id만 key로 사용 (unmount 방지)
|
|
return <ButtonConfigPanel key={selectedComponent.id} component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
|
|
|
case "card":
|
|
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
|
|
|
case "dashboard":
|
|
return <DashboardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
|
|
|
case "stats":
|
|
case "stats-card":
|
|
return <StatsCardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
|
|
|
case "progress":
|
|
case "progress-bar":
|
|
return <ProgressBarConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
|
|
|
case "chart":
|
|
case "chart-basic":
|
|
return <ChartConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
|
|
|
case "alert":
|
|
case "alert-info":
|
|
return <AlertConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
|
|
|
case "badge":
|
|
case "badge-status":
|
|
return <BadgeConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// 기본 정보 탭
|
|
const renderBasicTab = () => {
|
|
const widget = selectedComponent as WidgetComponent;
|
|
const group = selectedComponent as GroupComponent;
|
|
const area = selectedComponent as AreaComponent;
|
|
|
|
return (
|
|
<div className="space-y-1.5">
|
|
{/* 컴포넌트 정보 - 간소화 */}
|
|
<div className="flex items-center justify-between rounded bg-muted px-2 py-1">
|
|
<div className="flex items-center gap-1">
|
|
<Info className="h-2.5 w-2.5 text-muted-foreground" />
|
|
<span className="text-[10px] font-medium text-foreground">{selectedComponent.type}</span>
|
|
</div>
|
|
<span className="text-[9px] text-muted-foreground">{selectedComponent.id.slice(0, 8)}</span>
|
|
</div>
|
|
|
|
{/* 라벨 + 최소 높이 (같은 행) */}
|
|
<div className="grid grid-cols-2 gap-1.5">
|
|
<div className="space-y-0.5">
|
|
<Label className="text-[10px]">라벨</Label>
|
|
<Input
|
|
value={widget.label || ""}
|
|
onChange={(e) => handleUpdate("label", e.target.value)}
|
|
placeholder="라벨"
|
|
className="h-6 text-[10px]"
|
|
/>
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<Label className="text-[10px]">높이</Label>
|
|
<Input
|
|
type="number"
|
|
value={selectedComponent.size?.height || 0}
|
|
onChange={(e) => {
|
|
const value = parseInt(e.target.value) || 0;
|
|
const roundedValue = Math.max(40, Math.round(value / 40) * 40);
|
|
handleUpdate("size.height", roundedValue);
|
|
}}
|
|
step={40}
|
|
placeholder="40"
|
|
className="h-6 text-[10px]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Placeholder (widget만) */}
|
|
{selectedComponent.type === "widget" && (
|
|
<div className="space-y-0.5">
|
|
<Label className="text-[10px]">Placeholder</Label>
|
|
<Input
|
|
value={widget.placeholder || ""}
|
|
onChange={(e) => handleUpdate("placeholder", e.target.value)}
|
|
placeholder="입력 안내 텍스트"
|
|
className="h-6 text-[10px]"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Title (group/area) */}
|
|
{(selectedComponent.type === "group" || selectedComponent.type === "area") && (
|
|
<div className="space-y-0.5">
|
|
<Label className="text-[10px]">제목</Label>
|
|
<Input
|
|
value={group.title || area.title || ""}
|
|
onChange={(e) => handleUpdate("title", e.target.value)}
|
|
placeholder="제목"
|
|
className="h-6 text-[10px]"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Description (area만) */}
|
|
{selectedComponent.type === "area" && (
|
|
<div className="space-y-0.5">
|
|
<Label className="text-[10px]">설명</Label>
|
|
<Input
|
|
value={area.description || ""}
|
|
onChange={(e) => handleUpdate("description", e.target.value)}
|
|
placeholder="설명"
|
|
className="h-6 text-[10px]"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Grid Columns */}
|
|
{(selectedComponent as any).gridColumns !== undefined && (
|
|
<div className="space-y-0.5">
|
|
<Label className="text-[10px]">Grid Columns</Label>
|
|
<Select
|
|
value={((selectedComponent as any).gridColumns || 12).toString()}
|
|
onValueChange={(value) => handleUpdate("gridColumns", parseInt(value))}
|
|
>
|
|
<SelectTrigger className="h-6 text-[10px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{COLUMN_NUMBERS.map((span) => (
|
|
<SelectItem key={span} value={span.toString()}>
|
|
{span} 컬럼 ({Math.round((span / 12) * 100)}%)
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
|
|
{/* 위치 */}
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<div>
|
|
<Label>X {dragState?.isDragging && <Badge variant="secondary">드래그중</Badge>}</Label>
|
|
<Input type="number" value={Math.round(currentPosition.x || 0)} disabled />
|
|
</div>
|
|
<div>
|
|
<Label>Y</Label>
|
|
<Input type="number" value={Math.round(currentPosition.y || 0)} disabled />
|
|
</div>
|
|
<div>
|
|
<Label>Z</Label>
|
|
<Input
|
|
type="number"
|
|
value={currentPosition.z || 1}
|
|
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 라벨 스타일 */}
|
|
<Collapsible>
|
|
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg bg-slate-50 p-2 text-sm font-medium hover:bg-slate-100">
|
|
라벨 스타일
|
|
<ChevronDown className="h-4 w-4" />
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent className="mt-2 space-y-2">
|
|
<div>
|
|
<Label>라벨 텍스트</Label>
|
|
<Input
|
|
value={selectedComponent.style?.labelText || selectedComponent.label || ""}
|
|
onChange={(e) => handleUpdate("style.labelText", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<Label>폰트 크기</Label>
|
|
<Input
|
|
value={selectedComponent.style?.labelFontSize || "12px"}
|
|
onChange={(e) => handleUpdate("style.labelFontSize", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>색상</Label>
|
|
<Input
|
|
type="color"
|
|
value={selectedComponent.style?.labelColor || "#212121"}
|
|
onChange={(e) => handleUpdate("style.labelColor", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label>하단 여백</Label>
|
|
<Input
|
|
value={selectedComponent.style?.labelMarginBottom || "4px"}
|
|
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
checked={selectedComponent.style?.labelDisplay !== false}
|
|
onCheckedChange={(checked) => handleUpdate("style.labelDisplay", checked)}
|
|
/>
|
|
<Label>라벨 표시</Label>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* 옵션 */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
checked={widget.visible !== false}
|
|
onCheckedChange={(checked) => handleUpdate("visible", checked)}
|
|
/>
|
|
<Label>표시</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
checked={widget.disabled === true}
|
|
onCheckedChange={(checked) => handleUpdate("disabled", checked)}
|
|
/>
|
|
<Label>비활성화</Label>
|
|
</div>
|
|
{widget.required !== undefined && (
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
checked={widget.required === true}
|
|
onCheckedChange={(checked) => handleUpdate("required", checked)}
|
|
/>
|
|
<Label>필수 입력</Label>
|
|
</div>
|
|
)}
|
|
{widget.readonly !== undefined && (
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
checked={widget.readonly === true}
|
|
onCheckedChange={(checked) => handleUpdate("readonly", checked)}
|
|
/>
|
|
<Label>읽기 전용</Label>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 액션 버튼 */}
|
|
<Separator />
|
|
<div className="flex gap-2">
|
|
{onCopyComponent && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onCopyComponent(selectedComponent.id)}
|
|
className="flex-1"
|
|
>
|
|
<Copy className="mr-2 h-4 w-4" />
|
|
복사
|
|
</Button>
|
|
)}
|
|
{onDeleteComponent && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onDeleteComponent(selectedComponent.id)}
|
|
className="flex-1 text-red-600 hover:bg-red-50 hover:text-red-700"
|
|
>
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
삭제
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 상세 설정 탭 (DetailSettingsPanel의 전체 로직 통합)
|
|
const renderDetailTab = () => {
|
|
// 1. DataTable 컴포넌트
|
|
if (selectedComponent.type === "datatable") {
|
|
return (
|
|
<DataTableConfigPanel
|
|
component={selectedComponent as DataTableComponent}
|
|
tables={tables}
|
|
onUpdateComponent={(updates) => {
|
|
Object.entries(updates).forEach(([key, value]) => {
|
|
handleUpdate(key, value);
|
|
});
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// 3. 파일 컴포넌트
|
|
if (isFileComponent(selectedComponent)) {
|
|
return (
|
|
<FileComponentConfigPanel
|
|
component={selectedComponent as FileComponent}
|
|
onUpdateProperty={onUpdateProperty}
|
|
currentTable={currentTable}
|
|
currentTableName={currentTableName}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// 4. 새로운 컴포넌트 시스템 (button, card 등)
|
|
const componentType = selectedComponent.componentConfig?.type || selectedComponent.type;
|
|
const hasNewConfigPanel =
|
|
componentType &&
|
|
[
|
|
"button",
|
|
"button-primary",
|
|
"button-secondary",
|
|
"card",
|
|
"dashboard",
|
|
"stats",
|
|
"stats-card",
|
|
"progress",
|
|
"progress-bar",
|
|
"chart",
|
|
"chart-basic",
|
|
"alert",
|
|
"alert-info",
|
|
"badge",
|
|
"badge-status",
|
|
].includes(componentType);
|
|
|
|
if (hasNewConfigPanel) {
|
|
const configPanel = renderComponentConfigPanel();
|
|
if (configPanel) {
|
|
return <div className="space-y-4">{configPanel}</div>;
|
|
}
|
|
}
|
|
|
|
// 5. 새로운 컴포넌트 시스템 (type: "component")
|
|
if (selectedComponent.type === "component") {
|
|
const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type;
|
|
const webType = selectedComponent.componentConfig?.webType;
|
|
|
|
// 테이블 패널에서 드래그한 컴포넌트인지 확인
|
|
const isFromTablePanel = !!(selectedComponent.tableName && selectedComponent.columnName);
|
|
|
|
if (!componentId) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center p-8 text-center">
|
|
<p className="text-sm text-gray-500">컴포넌트 ID가 설정되지 않았습니다</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 현재 웹타입의 기본 입력 타입 추출
|
|
const currentBaseInputType = webType ? getBaseInputType(webType as any) : null;
|
|
|
|
// 선택 가능한 세부 타입 목록
|
|
const availableDetailTypes = currentBaseInputType ? getDetailTypes(currentBaseInputType) : [];
|
|
|
|
// 세부 타입 변경 핸들러
|
|
const handleDetailTypeChange = (newDetailType: string) => {
|
|
setLocalComponentDetailType(newDetailType);
|
|
handleUpdate("componentConfig.webType", newDetailType);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 세부 타입 선택 - 테이블 패널에서 드래그한 컴포넌트만 표시 */}
|
|
{isFromTablePanel && webType && availableDetailTypes.length > 1 && (
|
|
<div>
|
|
<Label>세부 타입</Label>
|
|
<Select value={localComponentDetailType || webType} onValueChange={handleDetailTypeChange}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="세부 타입 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableDetailTypes.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
<div>
|
|
<div className="font-medium">{option.label}</div>
|
|
<div className="text-xs text-gray-500">{option.description}</div>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
|
|
{/* DynamicComponentConfigPanel */}
|
|
<DynamicComponentConfigPanel
|
|
componentId={componentId}
|
|
config={selectedComponent.componentConfig || {}}
|
|
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
|
tableColumns={currentTable?.columns || []}
|
|
tables={tables}
|
|
onChange={(newConfig) => {
|
|
console.log("🔄 DynamicComponentConfigPanel onChange:", newConfig);
|
|
// 전체 componentConfig를 업데이트
|
|
handleUpdate("componentConfig", newConfig);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 6. Widget 컴포넌트
|
|
if (selectedComponent.type === "widget") {
|
|
const widget = selectedComponent as WidgetComponent;
|
|
|
|
// Widget에 webType이 있는 경우
|
|
if (widget.webType) {
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* WebType 선택 */}
|
|
<div>
|
|
<Label>입력 타입</Label>
|
|
<Select value={widget.webType} onValueChange={(value) => handleUpdate("webType", value)}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{webTypes.map((wt) => (
|
|
<SelectItem key={wt.web_type} value={wt.web_type}>
|
|
{wt.web_type_name_kor || wt.web_type}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 새로운 컴포넌트 시스템 (widgetType이 button, card 등)
|
|
if (
|
|
widget.widgetType &&
|
|
["button", "card", "dashboard", "stats-card", "progress-bar", "chart", "alert", "badge"].includes(
|
|
widget.widgetType,
|
|
)
|
|
) {
|
|
return (
|
|
<DynamicComponentConfigPanel
|
|
componentId={widget.widgetType}
|
|
config={widget.componentConfig || {}}
|
|
screenTableName={widget.tableName || currentTable?.tableName || currentTableName}
|
|
tableColumns={currentTable?.columns || []}
|
|
tables={tables}
|
|
onChange={(newConfig) => {
|
|
console.log("🔄 DynamicComponentConfigPanel onChange (widget):", newConfig);
|
|
// 전체 componentConfig를 업데이트
|
|
handleUpdate("componentConfig", newConfig);
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
// 기본 메시지
|
|
return (
|
|
<div className="flex h-full items-center justify-center p-8 text-center">
|
|
<p className="text-sm text-gray-500">이 컴포넌트는 추가 설정이 없습니다</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-full flex-col bg-white">
|
|
{/* 헤더 - 간소화 */}
|
|
<div className="border-b border-gray-200 px-3 py-2">
|
|
{selectedComponent.type === "widget" && (
|
|
<div className="text-[10px] text-gray-600 truncate">
|
|
{(selectedComponent as WidgetComponent).label || selectedComponent.id}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 탭 컨텐츠 */}
|
|
<Tabs defaultValue="properties" className="flex flex-1 flex-col overflow-hidden">
|
|
<TabsList className="grid h-7 w-full flex-shrink-0 grid-cols-2">
|
|
<TabsTrigger value="properties" className="text-[10px]">
|
|
편집
|
|
</TabsTrigger>
|
|
<TabsTrigger value="styles" className="text-[10px]">
|
|
<Palette className="mr-0.5 h-2.5 w-2.5" />
|
|
스타일 & 해상도
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* 속성 탭 */}
|
|
<TabsContent value="properties" className="mt-0 flex-1 overflow-y-auto p-2">
|
|
<div className="space-y-2 text-xs">
|
|
{/* 기본 설정 */}
|
|
{renderBasicTab()}
|
|
|
|
{/* 상세 설정 통합 */}
|
|
<Separator className="my-2" />
|
|
{renderDetailTab()}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* 스타일 & 해상도 탭 */}
|
|
<TabsContent value="styles" className="mt-0 flex-1 overflow-y-auto">
|
|
<div className="space-y-2">
|
|
{/* 해상도 설정 */}
|
|
{currentResolution && onResolutionChange && (
|
|
<div className="border-b pb-2 px-2">
|
|
<ResolutionPanel
|
|
currentResolution={currentResolution}
|
|
onResolutionChange={onResolutionChange}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* 스타일 설정 */}
|
|
{selectedComponent ? (
|
|
<div>
|
|
<div className="mb-1.5 flex items-center gap-1.5 px-2">
|
|
<Palette className="h-3 w-3 text-primary" />
|
|
<h4 className="text-xs font-semibold">컴포넌트 스타일</h4>
|
|
</div>
|
|
<StyleEditor
|
|
style={selectedComponent.style || {}}
|
|
onStyleChange={(style) => {
|
|
if (onStyleChange) {
|
|
onStyleChange(style);
|
|
} else {
|
|
handleUpdate("style", style);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="flex h-full items-center justify-center text-muted-foreground text-xs">
|
|
컴포넌트를 선택하여 스타일을 편집하세요
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default UnifiedPropertiesPanel;
|