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

1126 lines
44 KiB
TypeScript
Raw Normal View History

2025-10-15 10:24:33 +09:00
"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";
2025-10-22 17:19:47 +09:00
import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react";
2025-10-15 10:24:33 +09:00
import {
ComponentData,
WebType,
WidgetComponent,
GroupComponent,
DataTableComponent,
TableInfo,
LayoutComponent,
FileComponent,
AreaComponent,
} from "@/types/screen";
import { ColumnSpanPreset, COLUMN_SPAN_PRESETS } from "@/lib/constants/columnSpans";
// 컬럼 스팬 숫자 배열 (1~12)
// 동적으로 컬럼 수 배열 생성 (gridSettings.columns 기반)
const generateColumnNumbers = (maxColumns: number) => {
return Array.from({ length: maxColumns }, (_, i) => i + 1);
};
2025-10-15 10:24:33 +09:00
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";
2025-10-22 17:19:47 +09:00
import StyleEditor from "../StyleEditor";
import ResolutionPanel from "./ResolutionPanel";
import { Slider } from "@/components/ui/slider";
import { Grid3X3, Eye, EyeOff, Zap } from "lucide-react";
2025-10-15 10:24:33 +09:00
interface UnifiedPropertiesPanelProps {
selectedComponent?: ComponentData;
tables: TableInfo[];
gridSettings?: {
columns: number;
gap: number;
padding: number;
snapToGrid: boolean;
showGrid: boolean;
gridColor?: string;
gridOpacity?: number;
};
2025-10-15 10:24:33 +09:00
onUpdateProperty: (componentId: string, path: string, value: any) => void;
onGridSettingsChange?: (settings: any) => void;
2025-10-15 10:24:33 +09:00
onDeleteComponent?: (componentId: string) => void;
onCopyComponent?: (componentId: string) => void;
currentTable?: TableInfo;
currentTableName?: string;
dragState?: any;
2025-10-22 17:19:47 +09:00
// 스타일 관련
onStyleChange?: (style: any) => void;
// 해상도 관련
currentResolution?: { name: string; width: number; height: number };
onResolutionChange?: (resolution: { name: string; width: number; height: number }) => void;
// 🆕 플로우 위젯 감지용
allComponents?: ComponentData[];
2025-10-15 10:24:33 +09:00
}
export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
selectedComponent,
tables,
gridSettings,
2025-10-15 10:24:33 +09:00
onUpdateProperty,
onGridSettingsChange,
2025-10-15 10:24:33 +09:00
onDeleteComponent,
onCopyComponent,
currentTable,
currentTableName,
dragState,
2025-10-22 17:19:47 +09:00
onStyleChange,
currentResolution,
onResolutionChange,
allComponents = [], // 🆕 기본값 빈 배열
2025-10-15 10:24:33 +09:00
}) => {
const { webTypes } = useWebTypes({ active: "Y" });
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
// 높이 입력 로컬 상태 (격자 스냅 방지)
const [localHeight, setLocalHeight] = useState<string>("");
2025-10-15 10:24:33 +09:00
// 새로운 컴포넌트 시스템의 webType 동기화
useEffect(() => {
if (selectedComponent?.type === "component") {
const webType = selectedComponent.componentConfig?.webType;
if (webType) {
setLocalComponentDetailType(webType);
}
}
}, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]);
// 높이 값 동기화
useEffect(() => {
if (selectedComponent?.size?.height !== undefined) {
setLocalHeight(String(selectedComponent.size.height));
}
}, [selectedComponent?.size?.height, selectedComponent?.id]);
2025-10-15 10:24:33 +09:00
// 격자 설정 업데이트 함수 (early return 이전에 정의)
const updateGridSetting = (key: string, value: any) => {
if (onGridSettingsChange && gridSettings) {
onGridSettingsChange({
...gridSettings,
[key]: value,
});
}
};
// 격자 설정 렌더링 (early return 이전에 정의)
const renderGridSettings = () => {
if (!gridSettings || !onGridSettingsChange) return null;
// 최대 컬럼 수 계산
const MIN_COLUMN_WIDTH = 30;
const maxColumns = currentResolution
? Math.floor((currentResolution.width - gridSettings.padding * 2 + gridSettings.gap) / (MIN_COLUMN_WIDTH + gridSettings.gap))
: 24;
const safeMaxColumns = Math.max(1, Math.min(maxColumns, 100)); // 최대 100개로 제한
return (
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Grid3X3 className="text-primary h-3 w-3" />
<h4 className="text-xs font-semibold"> </h4>
</div>
<div className="space-y-3">
{/* 토글들 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{gridSettings.showGrid ? (
<Eye className="text-primary h-3 w-3" />
) : (
<EyeOff className="text-muted-foreground h-3 w-3" />
)}
<Label htmlFor="showGrid" className="text-xs font-medium">
</Label>
</div>
<Checkbox
id="showGrid"
checked={gridSettings.showGrid}
onCheckedChange={(checked) => updateGridSetting("showGrid", checked)}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Zap className="text-primary h-3 w-3" />
<Label htmlFor="snapToGrid" className="text-xs font-medium">
</Label>
</div>
<Checkbox
id="snapToGrid"
checked={gridSettings.snapToGrid}
onCheckedChange={(checked) => updateGridSetting("snapToGrid", checked)}
/>
</div>
{/* 컬럼 수 */}
<div className="space-y-1">
<Label htmlFor="columns" className="text-xs font-medium">
</Label>
<div className="flex items-center gap-2">
<Input
id="columns"
type="number"
min={1}
max={safeMaxColumns}
step="1"
value={gridSettings.columns}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
if (!isNaN(value) && value >= 1 && value <= safeMaxColumns) {
updateGridSetting("columns", value);
}
}}
className="h-6 px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
placeholder={`1~${safeMaxColumns}`}
/>
</div>
2025-11-04 16:21:24 +09:00
<p className="text-muted-foreground text-[10px]">
{safeMaxColumns} ( {MIN_COLUMN_WIDTH}px)
2025-11-04 16:21:24 +09:00
</p>
</div>
{/* 간격 */}
<div className="space-y-1">
<Label htmlFor="gap" className="text-xs font-medium">
: <span className="text-primary">{gridSettings.gap}px</span>
</Label>
<Slider
id="gap"
min={0}
max={40}
step={2}
value={[gridSettings.gap]}
onValueChange={([value]) => updateGridSetting("gap", value)}
className="w-full"
/>
</div>
{/* 여백 */}
<div className="space-y-1">
<Label htmlFor="padding" className="text-xs font-medium">
: <span className="text-primary">{gridSettings.padding}px</span>
</Label>
<Slider
id="padding"
min={0}
max={60}
step={4}
value={[gridSettings.padding]}
onValueChange={([value]) => updateGridSetting("padding", value)}
className="w-full"
/>
</div>
</div>
</div>
);
};
// 컴포넌트가 선택되지 않았을 때도 해상도 설정과 격자 설정은 표시
2025-10-15 10:24:33 +09:00
if (!selectedComponent) {
return (
2025-10-23 15:06:00 +09:00
<div className="flex h-full flex-col bg-white">
{/* 해상도 설정과 격자 설정 표시 */}
2025-10-23 15:06:00 +09:00
<div className="flex-1 overflow-y-auto p-2">
<div className="space-y-4 text-xs">
{/* 해상도 설정 */}
2025-10-23 15:06:00 +09:00
{currentResolution && onResolutionChange && (
<>
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Monitor className="text-primary h-3 w-3" />
<h4 className="text-xs font-semibold"> </h4>
</div>
<ResolutionPanel currentResolution={currentResolution} onResolutionChange={onResolutionChange} />
2025-10-23 15:06:00 +09:00
</div>
<Separator className="my-2" />
</>
2025-10-23 15:06:00 +09:00
)}
2025-10-27 11:11:08 +09:00
{/* 격자 설정 */}
{renderGridSettings()}
2025-10-23 15:06:00 +09:00
{/* 안내 메시지 */}
<Separator className="my-4" />
<div className="flex flex-col items-center justify-center py-8 text-center">
2025-11-04 14:33:39 +09:00
<Settings className="text-muted-foreground/30 mb-2 h-8 w-8" />
<p className="text-muted-foreground text-[10px]"> </p>
<p className="text-muted-foreground text-[10px]"> </p>
2025-10-23 15:06:00 +09:00
</div>
</div>
</div>
2025-10-15 10:24:33 +09:00
</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 방지)
2025-10-27 11:11:08 +09:00
return (
<ButtonConfigPanel
key={selectedComponent.id}
component={selectedComponent}
onUpdateProperty={handleUpdateProperty}
allComponents={allComponents}
currentTableName={currentTableName}
/>
);
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 (
2025-10-28 17:33:03 +09:00
<div className="space-y-2">
2025-10-22 17:19:47 +09:00
{/* 라벨 + 최소 높이 (같은 행) */}
2025-10-28 17:33:03 +09:00
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs"></Label>
2025-10-22 17:19:47 +09:00
<Input
value={widget.label || ""}
onChange={(e) => handleUpdate("label", e.target.value)}
placeholder="라벨"
2025-10-28 17:33:03 +09:00
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
2025-10-22 17:19:47 +09:00
/>
</div>
2025-10-28 17:33:03 +09:00
<div className="space-y-1">
<Label className="text-xs"></Label>
2025-10-22 17:19:47 +09:00
<Input
type="number"
value={localHeight}
2025-10-22 17:19:47 +09:00
onChange={(e) => {
// 입력 중에는 로컬 상태만 업데이트 (격자 스냅 방지)
setLocalHeight(e.target.value);
}}
onBlur={(e) => {
// 포커스를 잃을 때만 실제로 업데이트
2025-10-22 17:19:47 +09:00
const value = parseInt(e.target.value) || 0;
if (value >= 1) {
handleUpdate("size.height", value);
}
}}
onKeyDown={(e) => {
// Enter 키를 누르면 즉시 적용
if (e.key === "Enter") {
const value = parseInt(e.currentTarget.value) || 0;
if (value >= 1) {
handleUpdate("size.height", value);
}
e.currentTarget.blur(); // 포커스 제거
}
2025-10-22 17:19:47 +09:00
}}
2025-11-05 15:23:57 +09:00
step={1}
2025-11-04 17:44:10 +09:00
placeholder="10"
2025-10-28 17:33:03 +09:00
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
2025-10-22 17:19:47 +09:00
/>
</div>
2025-10-15 10:24:33 +09:00
</div>
{/* Placeholder (widget만) */}
{selectedComponent.type === "widget" && (
2025-10-28 17:33:03 +09:00
<div className="space-y-1">
<Label className="text-xs">Placeholder</Label>
2025-10-15 10:24:33 +09:00
<Input
value={widget.placeholder || ""}
onChange={(e) => handleUpdate("placeholder", e.target.value)}
placeholder="입력 안내 텍스트"
2025-10-28 17:33:03 +09:00
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
2025-10-15 10:24:33 +09:00
/>
</div>
)}
{/* Title (group/area) */}
{(selectedComponent.type === "group" || selectedComponent.type === "area") && (
2025-10-28 17:33:03 +09:00
<div className="space-y-1">
<Label className="text-xs"></Label>
2025-10-15 10:24:33 +09:00
<Input
value={group.title || area.title || ""}
onChange={(e) => handleUpdate("title", e.target.value)}
placeholder="제목"
2025-10-28 17:33:03 +09:00
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
2025-10-15 10:24:33 +09:00
/>
</div>
)}
{/* Description (area만) */}
{selectedComponent.type === "area" && (
2025-10-28 17:33:03 +09:00
<div className="space-y-1">
<Label className="text-xs"></Label>
2025-10-15 10:24:33 +09:00
<Input
value={area.description || ""}
onChange={(e) => handleUpdate("description", e.target.value)}
placeholder="설명"
2025-10-28 17:33:03 +09:00
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
2025-10-15 10:24:33 +09:00
/>
</div>
)}
2025-10-28 17:33:03 +09:00
{/* Grid Columns + Z-Index (같은 행) */}
<div className="grid grid-cols-2 gap-2">
{(selectedComponent as any).gridColumns !== undefined && (
<div className="space-y-1">
<Label className="text-xs"> </Label>
<div className="flex items-center gap-1">
<Input
type="number"
min={1}
max={gridSettings?.columns || 12}
step="1"
value={(selectedComponent as any).gridColumns || 1}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
const maxColumns = gridSettings?.columns || 12;
if (!isNaN(value) && value >= 1 && value <= maxColumns) {
handleUpdate("gridColumns", value);
// width를 퍼센트로 계산하여 업데이트
const widthPercent = (value / maxColumns) * 100;
handleUpdate("style.width", `${widthPercent}%`);
}
}}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/>
<span className="text-muted-foreground text-[10px] whitespace-nowrap">
/{gridSettings?.columns || 12}
</span>
</div>
2025-10-28 17:33:03 +09:00
</div>
)}
<div className="space-y-1">
<Label className="text-xs">Z-Index</Label>
<Input
type="number"
step="1"
2025-10-28 17:33:03 +09:00
value={currentPosition.z || 1}
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
2025-10-15 10:24:33 +09:00
</div>
</div>
{/* 라벨 스타일 */}
<Collapsible>
2025-10-28 17:33:03 +09:00
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg bg-slate-50 p-2 text-xs font-medium hover:bg-slate-100">
2025-10-15 10:24:33 +09:00
2025-10-28 17:33:03 +09:00
<ChevronDown className="h-3.5 w-3.5" />
2025-10-15 10:24:33 +09:00
</CollapsibleTrigger>
<CollapsibleContent className="mt-2 space-y-2">
2025-10-28 17:33:03 +09:00
<div className="space-y-1">
<Label className="text-xs"> </Label>
2025-10-15 10:24:33 +09:00
<Input
value={selectedComponent.style?.labelText || selectedComponent.label || ""}
onChange={(e) => handleUpdate("style.labelText", e.target.value)}
2025-10-28 17:33:03 +09:00
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
2025-10-15 10:24:33 +09:00
/>
</div>
<div className="grid grid-cols-2 gap-2">
2025-10-28 17:33:03 +09:00
<div className="space-y-1">
<Label className="text-xs"></Label>
2025-10-15 10:24:33 +09:00
<Input
value={selectedComponent.style?.labelFontSize || "12px"}
onChange={(e) => handleUpdate("style.labelFontSize", e.target.value)}
2025-10-28 17:33:03 +09:00
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
2025-10-15 10:24:33 +09:00
/>
</div>
2025-10-28 17:33:03 +09:00
<div className="space-y-1">
<Label className="text-xs"></Label>
2025-10-15 10:24:33 +09:00
<Input
type="color"
value={selectedComponent.style?.labelColor || "#212121"}
onChange={(e) => handleUpdate("style.labelColor", e.target.value)}
2025-10-28 17:33:03 +09:00
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
2025-10-15 10:24:33 +09:00
/>
</div>
</div>
2025-10-28 17:33:03 +09:00
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={selectedComponent.style?.labelMarginBottom || "4px"}
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
</div>
<div className="flex items-center space-x-2 pt-5">
<Checkbox
checked={selectedComponent.style?.labelDisplay !== false}
onCheckedChange={(checked) => handleUpdate("style.labelDisplay", checked)}
className="h-4 w-4"
/>
<Label className="text-xs"></Label>
</div>
2025-10-15 10:24:33 +09:00
</div>
</CollapsibleContent>
</Collapsible>
{/* 옵션 */}
2025-10-23 15:06:00 +09:00
<div className="grid grid-cols-2 gap-2">
2025-10-15 10:24:33 +09:00
{widget.required !== undefined && (
<div className="flex items-center space-x-2">
<Checkbox
2025-10-23 15:06:00 +09:00
checked={widget.required === true || selectedComponent.componentConfig?.required === true}
2025-10-30 12:03:50 +09:00
onCheckedChange={(checked) => {
handleUpdate("required", checked);
handleUpdate("componentConfig.required", checked);
}}
2025-10-28 17:33:03 +09:00
className="h-4 w-4"
2025-10-15 10:24:33 +09:00
/>
2025-10-28 17:33:03 +09:00
<Label className="text-xs"></Label>
2025-10-15 10:24:33 +09:00
</div>
)}
{widget.readonly !== undefined && (
<div className="flex items-center space-x-2">
<Checkbox
2025-10-23 15:06:00 +09:00
checked={widget.readonly === true || selectedComponent.componentConfig?.readonly === true}
2025-10-30 12:03:50 +09:00
onCheckedChange={(checked) => {
handleUpdate("readonly", checked);
handleUpdate("componentConfig.readonly", checked);
}}
2025-10-28 17:33:03 +09:00
className="h-4 w-4"
2025-10-15 10:24:33 +09:00
/>
2025-10-28 17:33:03 +09:00
<Label className="text-xs"></Label>
2025-10-15 10:24:33 +09:00
</div>
)}
</div>
</div>
);
};
// 상세 설정 탭 (DetailSettingsPanel의 전체 로직 통합)
const renderDetailTab = () => {
2025-11-04 14:33:39 +09:00
console.log("🔍 [renderDetailTab] selectedComponent.type:", selectedComponent.type);
2025-10-15 10:24:33 +09:00
// 1. DataTable 컴포넌트
if (selectedComponent.type === "datatable") {
2025-11-04 14:33:39 +09:00
console.log("✅ [renderDetailTab] DataTable 컴포넌트");
2025-10-15 10:24:33 +09:00
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") {
2025-11-04 14:33:39 +09:00
console.log("✅ [renderDetailTab] Component 타입");
2025-10-15 10:24:33 +09:00
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">
2025-11-04 14:33:39 +09:00
<p className="text-muted-foreground text-sm"> ID가 </p>
2025-10-15 10:24:33 +09:00
</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}>
2025-10-28 17:33:03 +09:00
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
2025-10-15 10:24:33 +09:00
<SelectValue placeholder="세부 타입 선택" />
</SelectTrigger>
<SelectContent>
{availableDetailTypes.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div>
<div className="font-medium">{option.label}</div>
2025-11-04 14:33:39 +09:00
<div className="text-muted-foreground text-xs">{option.description}</div>
2025-10-15 10:24:33 +09:00
</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);
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
Object.entries(newConfig).forEach(([key, value]) => {
handleUpdate(`componentConfig.${key}`, value);
});
2025-10-15 10:24:33 +09:00
}}
/>
2025-11-04 14:33:39 +09:00
{/* 🆕 테이블 데이터 자동 입력 (component 타입용) */}
<Separator />
<div className="space-y-3">
<div className="flex items-center gap-2">
<Database className="text-primary h-4 w-4" />
<h4 className="text-xs font-semibold"> </h4>
</div>
{/* 활성화 체크박스 */}
<div className="flex items-center space-x-2">
<Checkbox
id="autoFill-enabled-component"
checked={selectedComponent.autoFill?.enabled || false}
onCheckedChange={(checked) => {
handleUpdate("autoFill", {
...selectedComponent.autoFill,
enabled: Boolean(checked),
});
}}
/>
<Label htmlFor="autoFill-enabled-component" className="cursor-pointer text-xs">
</Label>
</div>
{selectedComponent.autoFill?.enabled && (
<>
{/* 조회할 테이블 */}
<div className="space-y-1">
<Label htmlFor="autoFill-sourceTable-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={selectedComponent.autoFill?.sourceTable || ""}
onValueChange={(value) => {
handleUpdate("autoFill", {
...selectedComponent.autoFill,
enabled: selectedComponent.autoFill?.enabled || false,
sourceTable: value,
});
}}
>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName} className="text-xs">
{table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 필터링할 컬럼 */}
<div className="space-y-1">
<Label htmlFor="autoFill-filterColumn-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="autoFill-filterColumn-component"
value={selectedComponent.autoFill?.filterColumn || ""}
onChange={(e) => {
handleUpdate("autoFill", {
...selectedComponent.autoFill,
enabled: selectedComponent.autoFill?.enabled || false,
filterColumn: e.target.value,
});
}}
placeholder="예: company_code"
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
{/* 사용자 정보 필드 */}
<div className="space-y-1">
<Label htmlFor="autoFill-userField-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={selectedComponent.autoFill?.userField || ""}
onValueChange={(value: "companyCode" | "userId" | "deptCode") => {
handleUpdate("autoFill", {
...selectedComponent.autoFill,
enabled: selectedComponent.autoFill?.enabled || false,
userField: value,
});
}}
>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="사용자 정보 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="companyCode" className="text-xs">
</SelectItem>
<SelectItem value="userId" className="text-xs">
ID
</SelectItem>
<SelectItem value="deptCode" className="text-xs">
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 표시할 컬럼 */}
<div className="space-y-1">
<Label htmlFor="autoFill-displayColumn-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="autoFill-displayColumn-component"
value={selectedComponent.autoFill?.displayColumn || ""}
onChange={(e) => {
handleUpdate("autoFill", {
...selectedComponent.autoFill,
enabled: selectedComponent.autoFill?.enabled || false,
displayColumn: e.target.value,
});
}}
placeholder="예: company_name"
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
</>
)}
</div>
2025-10-15 10:24:33 +09:00
</div>
);
}
// 6. Widget 컴포넌트
if (selectedComponent.type === "widget") {
2025-11-04 14:33:39 +09:00
console.log("✅ [renderDetailTab] Widget 타입");
2025-10-15 10:24:33 +09:00
const widget = selectedComponent as WidgetComponent;
2025-11-04 14:33:39 +09:00
console.log("🔍 [renderDetailTab] widget.widgetType:", widget.widgetType);
2025-10-15 10:24:33 +09:00
2025-11-04 14:33:39 +09:00
// 새로운 컴포넌트 시스템 (widgetType이 button, card 등) - 먼저 체크
2025-10-15 10:24:33 +09:00
if (
widget.widgetType &&
["button", "card", "dashboard", "stats-card", "progress-bar", "chart", "alert", "badge"].includes(
widget.widgetType,
)
) {
2025-11-04 14:33:39 +09:00
console.log("✅ [renderDetailTab] DynamicComponent 반환 (widgetType)");
2025-10-15 10:24:33 +09:00
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
}}
/>
);
}
2025-11-04 14:33:39 +09:00
// 일반 위젯 (webType 기반)
console.log("✅ [renderDetailTab] 일반 위젯 렌더링 시작");
return (
<div className="space-y-4">
{console.log("🔍 [UnifiedPropertiesPanel] widget.webType:", widget.webType, "widget:", widget)}
{/* WebType 선택 (있는 경우만) */}
{widget.webType && (
<div>
<Label> </Label>
<Select value={widget.webType} onValueChange={(value) => handleUpdate("webType", value)}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<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>
)}
{/* 🆕 테이블 데이터 자동 입력 (모든 widget 컴포넌트) */}
<Separator />
<div className="space-y-3 border-4 border-red-500 bg-yellow-100 p-4">
<div className="flex items-center gap-2">
<Database className="text-primary h-4 w-4" />
<h4 className="text-xs font-semibold"> </h4>
</div>
{/* 활성화 체크박스 */}
<div className="flex items-center space-x-2">
<Checkbox
id="autoFill-enabled"
checked={widget.autoFill?.enabled || false}
onCheckedChange={(checked) => {
handleUpdate("autoFill", {
...widget.autoFill,
enabled: Boolean(checked),
});
}}
/>
<Label htmlFor="autoFill-enabled" className="cursor-pointer text-xs">
</Label>
</div>
{widget.autoFill?.enabled && (
<>
{/* 조회할 테이블 */}
<div className="space-y-1">
<Label htmlFor="autoFill-sourceTable" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={widget.autoFill?.sourceTable || ""}
onValueChange={(value) => {
handleUpdate("autoFill", {
...widget.autoFill,
enabled: widget.autoFill?.enabled || false,
sourceTable: value,
});
}}
>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName} className="text-xs">
{table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 필터링할 컬럼 */}
<div className="space-y-1">
<Label htmlFor="autoFill-filterColumn" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="autoFill-filterColumn"
value={widget.autoFill?.filterColumn || ""}
onChange={(e) => {
handleUpdate("autoFill", {
...widget.autoFill,
enabled: widget.autoFill?.enabled || false,
filterColumn: e.target.value,
});
}}
placeholder="예: company_code"
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
{/* 사용자 정보 필드 */}
<div className="space-y-1">
<Label htmlFor="autoFill-userField" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={widget.autoFill?.userField || ""}
onValueChange={(value: "companyCode" | "userId" | "deptCode") => {
handleUpdate("autoFill", {
...widget.autoFill,
enabled: widget.autoFill?.enabled || false,
userField: value,
});
}}
>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="사용자 정보 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="companyCode" className="text-xs">
</SelectItem>
<SelectItem value="userId" className="text-xs">
ID
</SelectItem>
<SelectItem value="deptCode" className="text-xs">
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 표시할 컬럼 */}
<div className="space-y-1">
<Label htmlFor="autoFill-displayColumn" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="autoFill-displayColumn"
value={widget.autoFill?.displayColumn || ""}
onChange={(e) => {
handleUpdate("autoFill", {
...widget.autoFill,
enabled: widget.autoFill?.enabled || false,
displayColumn: e.target.value,
});
}}
placeholder="예: company_name"
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
</>
)}
</div>
</div>
);
2025-10-15 10:24:33 +09:00
}
// 기본 메시지
return (
<div className="flex h-full items-center justify-center p-8 text-center">
2025-11-04 14:33:39 +09:00
<p className="text-muted-foreground text-sm"> </p>
2025-10-15 10:24:33 +09:00
</div>
);
};
return (
<div className="flex h-full flex-col bg-white">
2025-10-22 17:19:47 +09:00
{/* 헤더 - 간소화 */}
2025-11-04 14:33:39 +09:00
<div className="border-border border-b px-3 py-2">
2025-10-15 10:24:33 +09:00
{selectedComponent.type === "widget" && (
2025-11-04 14:33:39 +09:00
<div className="text-muted-foreground truncate text-[10px]">
2025-10-15 10:24:33 +09:00
{(selectedComponent as WidgetComponent).label || selectedComponent.id}
</div>
)}
</div>
2025-10-23 15:06:00 +09:00
{/* 통합 컨텐츠 (탭 제거) */}
<div className="flex-1 overflow-y-auto p-2">
<div className="space-y-4 text-xs">
{/* 해상도 설정 - 항상 맨 위에 표시 */}
{currentResolution && onResolutionChange && (
<>
<div className="space-y-2">
<div className="flex items-center gap-1.5">
2025-10-27 11:11:08 +09:00
<Monitor className="text-primary h-3 w-3" />
2025-10-23 15:06:00 +09:00
<h4 className="text-xs font-semibold"> </h4>
</div>
2025-10-27 11:11:08 +09:00
<ResolutionPanel currentResolution={currentResolution} onResolutionChange={onResolutionChange} />
2025-10-22 17:19:47 +09:00
</div>
2025-10-23 15:06:00 +09:00
<Separator className="my-2" />
</>
)}
2025-10-22 17:19:47 +09:00
{/* 격자 설정 - 해상도 설정 아래 표시 */}
{renderGridSettings()}
{gridSettings && onGridSettingsChange && <Separator className="my-2" />}
2025-10-23 15:06:00 +09:00
{/* 기본 설정 */}
{renderBasicTab()}
2025-10-27 11:11:08 +09:00
2025-10-23 15:06:00 +09:00
{/* 상세 설정 */}
<Separator className="my-2" />
{renderDetailTab()}
{/* 스타일 설정 */}
{selectedComponent && (
<>
<Separator className="my-2" />
<div className="space-y-2">
<div className="flex items-center gap-1.5">
2025-10-27 11:11:08 +09:00
<Palette className="text-primary h-3 w-3" />
2025-10-22 17:19:47 +09:00
<h4 className="text-xs font-semibold"> </h4>
</div>
<StyleEditor
style={selectedComponent.style || {}}
onStyleChange={(style) => {
if (onStyleChange) {
onStyleChange(style);
} else {
handleUpdate("style", style);
}
}}
/>
</div>
2025-10-23 15:06:00 +09:00
</>
)}
</div>
</div>
2025-10-15 10:24:33 +09:00
</div>
);
};
export default UnifiedPropertiesPanel;