ERP-node/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx

714 lines
25 KiB
TypeScript
Raw Normal View History

2025-10-15 10:24:33 +09:00
"use client";
import React, { useState, useEffect } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
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 { ChevronDown, Settings, Info, Database, Trash2, Copy } 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";
2025-10-16 18:16:57 +09:00
import { ResponsiveConfigPanel } from "./ResponsiveConfigPanel";
2025-10-15 10:24:33 +09:00
import { AlertConfigPanel } from "../config-panels/AlertConfigPanel";
import { BadgeConfigPanel } from "../config-panels/BadgeConfigPanel";
import { DynamicComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
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;
}
export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
selectedComponent,
tables,
onUpdateProperty,
onDeleteComponent,
onCopyComponent,
currentTable,
currentTableName,
dragState,
}) => {
const { webTypes } = useWebTypes({ active: "Y" });
const [activeTab, setActiveTab] = useState("basic");
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-8 text-center">
<Settings className="mb-4 h-12 w-12 text-gray-300" />
<p className="text-sm text-gray-500"> </p>
<p className="text-sm 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":
2025-10-21 17:32:54 +09:00
// 🔧 component.id만 key로 사용 (unmount 방지)
return <ButtonConfigPanel key={selectedComponent.id} component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
2025-10-15 10:24:33 +09:00
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-4">
{/* 컴포넌트 정보 */}
<div className="rounded-lg bg-slate-50 p-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Info className="h-4 w-4 text-slate-500" />
<span className="text-sm font-medium text-slate-700"> </span>
</div>
<Badge variant="secondary" className="text-xs">
{selectedComponent.type}
</Badge>
</div>
<div className="mt-2 space-y-1 text-xs text-slate-600">
<div>ID: {selectedComponent.id}</div>
{widget.widgetType && <div>: {widget.widgetType}</div>}
</div>
</div>
{/* 라벨 */}
<div>
<Label></Label>
<Input
value={widget.label || ""}
onChange={(e) => handleUpdate("label", e.target.value)}
placeholder="컴포넌트 라벨"
/>
</div>
{/* Placeholder (widget만) */}
{selectedComponent.type === "widget" && (
<div>
<Label>Placeholder</Label>
<Input
value={widget.placeholder || ""}
onChange={(e) => handleUpdate("placeholder", e.target.value)}
placeholder="입력 안내 텍스트"
/>
</div>
)}
{/* Title (group/area) */}
{(selectedComponent.type === "group" || selectedComponent.type === "area") && (
<div>
<Label></Label>
<Input
value={group.title || area.title || ""}
onChange={(e) => handleUpdate("title", e.target.value)}
placeholder="제목"
/>
</div>
)}
{/* Description (area만) */}
{selectedComponent.type === "area" && (
<div>
<Label></Label>
<Input
value={area.description || ""}
onChange={(e) => handleUpdate("description", e.target.value)}
placeholder="설명"
/>
</div>
)}
{/* 크기 */}
<div>
<Label> (px)</Label>
<Input
type="number"
value={selectedComponent.size?.height || 0}
onChange={(e) => {
const value = parseInt(e.target.value) || 0;
// 40 단위로 반올림
const roundedValue = Math.max(40, Math.round(value / 40) * 40);
handleUpdate("size.height", roundedValue);
}}
step={40}
placeholder="40 단위로 입력"
/>
<p className="mt-1 text-xs text-gray-500">40 </p>
2025-10-15 10:24:33 +09:00
</div>
{/* 컬럼 스팬 */}
{widget.columnSpan !== undefined && (
<div>
<Label> </Label>
<Select
value={widget.columnSpan?.toString() || "12"}
onValueChange={(value) => handleUpdate("columnSpan", parseInt(value))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{COLUMN_NUMBERS.map((span) => (
<SelectItem key={span} value={span.toString()}>
{span} ({Math.round((span / 12) * 100)}%)
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* Grid Columns */}
{(selectedComponent as any).gridColumns !== undefined && (
<div>
<Label>Grid Columns</Label>
<Select
value={((selectedComponent as any).gridColumns || 12).toString()}
onValueChange={(value) => handleUpdate("gridColumns", parseInt(value))}
>
<SelectTrigger>
<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}
activeTab={activeTab}
onTabChange={setActiveTab}
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);
2025-10-15 10:24:33 +09:00
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 && (
2025-10-15 10:24:33 +09:00
<div>
<Label> </Label>
2025-10-15 10:24:33 +09:00
<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 || []}
2025-10-15 17:25:38 +09:00
tables={tables}
2025-10-15 10:24:33 +09:00
onChange={(newConfig) => {
2025-10-15 17:25:38 +09:00
console.log("🔄 DynamicComponentConfigPanel onChange:", newConfig);
// 전체 componentConfig를 업데이트
handleUpdate("componentConfig", newConfig);
2025-10-15 10:24:33 +09:00
}}
/>
</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 || []}
2025-10-15 17:25:38 +09:00
tables={tables}
2025-10-15 10:24:33 +09:00
onChange={(newConfig) => {
2025-10-15 17:25:38 +09:00
console.log("🔄 DynamicComponentConfigPanel onChange (widget):", newConfig);
// 전체 componentConfig를 업데이트
handleUpdate("componentConfig", newConfig);
2025-10-15 10:24:33 +09:00
}}
/>
);
}
}
// 기본 메시지
return (
<div className="flex h-full items-center justify-center p-8 text-center">
<p className="text-sm text-gray-500"> </p>
</div>
);
};
// 데이터 바인딩 탭
const renderDataTab = () => {
if (selectedComponent.type !== "widget") {
return (
<div className="flex h-full items-center justify-center p-8 text-center">
<p className="text-sm text-gray-500"> </p>
</div>
);
}
const widget = selectedComponent as WidgetComponent;
return (
<div className="space-y-4">
<div className="rounded-lg bg-blue-50 p-3">
<div className="flex items-center space-x-2">
<Database className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium text-blue-900"> </span>
</div>
</div>
{/* 테이블 컬럼 */}
<div>
<Label> </Label>
<Input
value={widget.columnName || ""}
onChange={(e) => handleUpdate("columnName", e.target.value)}
placeholder="컬럼명 입력"
/>
</div>
{/* 기본값 */}
<div>
<Label></Label>
<Input
value={widget.defaultValue || ""}
onChange={(e) => handleUpdate("defaultValue", e.target.value)}
placeholder="기본값 입력"
/>
</div>
</div>
);
};
return (
<div className="flex h-full flex-col bg-white">
{/* 헤더 */}
<div className="border-b border-gray-200 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Settings className="h-5 w-5 text-blue-600" />
<h3 className="font-semibold text-gray-900"> </h3>
</div>
<Badge variant="outline">{selectedComponent.type}</Badge>
</div>
{selectedComponent.type === "widget" && (
<div className="mt-2 text-xs text-gray-600">
{(selectedComponent as WidgetComponent).label || selectedComponent.id}
</div>
)}
</div>
{/* 탭 컨텐츠 */}
<div className="flex-1 overflow-hidden">
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex h-full flex-col">
<TabsList className="w-full justify-start rounded-none border-b px-4">
<TabsTrigger value="basic"></TabsTrigger>
<TabsTrigger value="detail"></TabsTrigger>
<TabsTrigger value="data"></TabsTrigger>
2025-10-16 18:16:57 +09:00
<TabsTrigger value="responsive"></TabsTrigger>
2025-10-15 10:24:33 +09:00
</TabsList>
<div className="flex-1 overflow-y-auto">
<TabsContent value="basic" className="m-0 p-4">
{renderBasicTab()}
</TabsContent>
<TabsContent value="detail" className="m-0 p-4">
{renderDetailTab()}
</TabsContent>
<TabsContent value="data" className="m-0 p-4">
{renderDataTab()}
</TabsContent>
2025-10-16 18:16:57 +09:00
<TabsContent value="responsive" className="m-0 p-4">
<ResponsiveConfigPanel
component={selectedComponent}
onUpdate={(config) => {
onUpdateProperty(selectedComponent.id, "responsiveConfig", config);
}}
/>
</TabsContent>
2025-10-15 10:24:33 +09:00
</div>
</Tabs>
</div>
</div>
);
};
export default UnifiedPropertiesPanel;