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

1163 lines
46 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 { 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)
// 동적으로 컬럼 수 배열 생성 (gridSettings.columns 기반)
const generateColumnNumbers = (maxColumns: number) => {
return Array.from({ length: maxColumns }, (_, i) => i + 1);
};
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";
// ComponentRegistry import (동적 ConfigPanel 가져오기용)
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
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";
import { Slider } from "@/components/ui/slider";
import { Grid3X3, Eye, EyeOff, Zap } from "lucide-react";
interface UnifiedPropertiesPanelProps {
selectedComponent?: ComponentData;
tables: TableInfo[];
gridSettings?: {
columns: number;
gap: number;
padding: number;
snapToGrid: boolean;
showGrid: boolean;
gridColor?: string;
gridOpacity?: number;
};
onUpdateProperty: (componentId: string, path: string, value: any) => void;
onGridSettingsChange?: (settings: 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;
// 🆕 플로우 위젯 감지용
allComponents?: ComponentData[];
// 🆕 메뉴 OBJID (코드/카테고리 스코프용)
menuObjid?: number;
// 🆕 현재 편집 중인 화면의 회사 코드
currentScreenCompanyCode?: string;
}
export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
selectedComponent,
tables,
gridSettings,
onUpdateProperty,
onGridSettingsChange,
onDeleteComponent,
onCopyComponent,
currentTable,
currentTableName,
currentScreenCompanyCode,
dragState,
onStyleChange,
menuObjid,
currentResolution,
onResolutionChange,
allComponents = [], // 🆕 기본값 빈 배열
}) => {
const { webTypes } = useWebTypes({ active: "Y" });
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
// 높이/너비 입력 로컬 상태 (자유 입력 허용)
const [localHeight, setLocalHeight] = useState<string>("");
const [localWidth, setLocalWidth] = useState<string>("");
// 새로운 컴포넌트 시스템의 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]);
// 너비 값 동기화
useEffect(() => {
if (selectedComponent?.size?.width !== undefined) {
setLocalWidth(String(selectedComponent.size.width));
}
}, [selectedComponent?.size?.width, selectedComponent?.id]);
// 격자 설정 업데이트 함수 (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>
{/* 10px 단위 스냅 안내 */}
<div className="bg-muted/50 rounded-md p-2">
<p className="text-[10px] text-muted-foreground">
10px .
</p>
</div>
</div>
</div>
);
};
// 컴포넌트가 선택되지 않았을 때도 해상도 설정과 격자 설정은 표시
if (!selectedComponent) {
return (
<div className="flex h-full flex-col bg-white">
{/* 해상도 설정과 격자 설정 표시 */}
<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">
<Monitor className="text-primary h-3 w-3" />
<h4 className="text-xs font-semibold"> </h4>
</div>
<ResolutionPanel currentResolution={currentResolution} onResolutionChange={onResolutionChange} />
</div>
<Separator className="my-2" />
</>
)}
{/* 격자 설정 */}
{renderGridSettings()}
{/* 안내 메시지 */}
<Separator className="my-4" />
<div className="flex flex-col items-center justify-center py-8 text-center">
<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>
</div>
</div>
</div>
</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);
};
const handleConfigChange = (newConfig: any) => {
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
};
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기
const componentId = selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.id;
if (componentId) {
const definition = ComponentRegistry.getComponent(componentId);
if (definition?.configPanel) {
const ConfigPanelComponent = definition.configPanel;
const currentConfig = selectedComponent.componentConfig || {};
console.log("✅ ConfigPanel 표시:", {
componentId,
definitionName: definition.name,
hasConfigPanel: !!definition.configPanel,
currentConfig,
});
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
const ConfigPanelWrapper = () => {
const config = currentConfig.config || definition.defaultConfig || {};
const handleConfigChange = (newConfig: any) => {
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
};
return (
<div className="space-y-4">
<div className="flex items-center gap-2 border-b pb-2">
<Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3>
</div>
<ConfigPanelComponent config={config} onConfigChange={handleConfigChange} />
</div>
);
};
return <ConfigPanelWrapper key={selectedComponent.id} />;
} else {
console.warn("⚠️ ConfigPanel 없음:", {
componentId,
definitionName: definition?.name,
hasDefinition: !!definition,
});
}
}
// 기존 하드코딩된 설정 패널들 (레거시)
switch (componentType) {
case "button":
case "button-primary":
case "button-secondary":
// 🔧 component.id만 key로 사용 (unmount 방지)
return (
<ButtonConfigPanel
key={selectedComponent.id}
component={selectedComponent}
onUpdateProperty={handleUpdateProperty}
allComponents={allComponents}
currentTableName={currentTableName}
currentScreenCompanyCode={currentScreenCompanyCode}
/>
);
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:
// ConfigPanel이 없는 경우 경고 표시
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-muted-foreground" />
<h3 className="mb-2 text-base font-medium"> </h3>
<p className="text-sm text-muted-foreground">
"{componentId || componentType}" .
</p>
</div>
);
}
};
// 기본 정보 탭
const renderBasicTab = () => {
const widget = selectedComponent as WidgetComponent;
const group = selectedComponent as GroupComponent;
const area = selectedComponent as AreaComponent;
return (
<div className="space-y-2">
{/* 라벨 + 최소 높이 (같은 행) */}
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={widget.label || ""}
onChange={(e) => handleUpdate("label", e.target.value)}
placeholder="라벨"
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
type="number"
value={localHeight}
onChange={(e) => {
// 입력 중에는 로컬 상태만 업데이트 (자유 입력)
setLocalHeight(e.target.value);
}}
onBlur={(e) => {
// 포커스를 잃을 때 10px 단위로 스냅
const value = parseInt(e.target.value) || 0;
if (value >= 10) {
const snappedValue = Math.round(value / 10) * 10;
handleUpdate("size.height", snappedValue);
setLocalHeight(String(snappedValue));
}
}}
onKeyDown={(e) => {
// Enter 키를 누르면 즉시 적용 (10px 단위로 스냅)
if (e.key === "Enter") {
const value = parseInt(e.currentTarget.value) || 0;
if (value >= 10) {
const snappedValue = Math.round(value / 10) * 10;
handleUpdate("size.height", snappedValue);
setLocalHeight(String(snappedValue));
}
e.currentTarget.blur(); // 포커스 제거
}
}}
step={1}
placeholder="10"
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
{/* Placeholder (widget만) */}
{selectedComponent.type === "widget" && (
<div className="space-y-1">
<Label className="text-xs">Placeholder</Label>
<Input
value={widget.placeholder || ""}
onChange={(e) => handleUpdate("placeholder", e.target.value)}
placeholder="입력 안내 텍스트"
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
</div>
)}
{/* Title (group/area) */}
{(selectedComponent.type === "group" || selectedComponent.type === "area") && (
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={group.title || area.title || ""}
onChange={(e) => handleUpdate("title", e.target.value)}
placeholder="제목"
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
</div>
)}
{/* Description (area만) */}
{selectedComponent.type === "area" && (
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={area.description || ""}
onChange={(e) => handleUpdate("description", e.target.value)}
placeholder="설명"
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
</div>
)}
{/* Width + Z-Index (같은 행) */}
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs"> (px)</Label>
<div className="flex items-center gap-1">
<Input
type="number"
min={10}
max={3840}
step="1"
value={localWidth}
onChange={(e) => {
// 입력 중에는 로컬 상태만 업데이트 (자유 입력)
setLocalWidth(e.target.value);
}}
onBlur={(e) => {
// 포커스를 잃을 때 10px 단위로 스냅
const value = parseInt(e.target.value, 10);
if (!isNaN(value) && value >= 10) {
const snappedValue = Math.round(value / 10) * 10;
handleUpdate("size.width", snappedValue);
setLocalWidth(String(snappedValue));
}
}}
onKeyDown={(e) => {
// Enter 키를 누르면 즉시 적용 (10px 단위로 스냅)
if (e.key === "Enter") {
const value = parseInt(e.currentTarget.value, 10);
if (!isNaN(value) && value >= 10) {
const snappedValue = Math.round(value / 10) * 10;
handleUpdate("size.width", snappedValue);
setLocalWidth(String(snappedValue));
}
e.currentTarget.blur(); // 포커스 제거
}
}}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs">Z-Index</Label>
<Input
type="number"
step="1"
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" }}
/>
</div>
</div>
{/* 라벨 스타일 */}
<Collapsible>
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg bg-slate-50 p-2 text-xs font-medium hover:bg-slate-100">
<ChevronDown className="h-3.5 w-3.5" />
</CollapsibleTrigger>
<CollapsibleContent className="mt-2 space-y-2">
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
value={selectedComponent.style?.labelText || selectedComponent.label || ""}
onChange={(e) => handleUpdate("style.labelText", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={selectedComponent.style?.labelFontSize || "12px"}
onChange={(e) => handleUpdate("style.labelFontSize", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
type="color"
value={selectedComponent.style?.labelColor || "#212121"}
onChange={(e) => handleUpdate("style.labelColor", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
</div>
</div>
<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>
</div>
</CollapsibleContent>
</Collapsible>
{/* 옵션 */}
<div className="grid grid-cols-2 gap-2">
{widget.required !== undefined && (
<div className="flex items-center space-x-2">
<Checkbox
checked={widget.required === true || selectedComponent.componentConfig?.required === true}
onCheckedChange={(checked) => {
handleUpdate("required", checked);
handleUpdate("componentConfig.required", checked);
}}
className="h-4 w-4"
/>
<Label className="text-xs"></Label>
</div>
)}
{widget.readonly !== undefined && (
<div className="flex items-center space-x-2">
<Checkbox
checked={widget.readonly === true || selectedComponent.componentConfig?.readonly === true}
onCheckedChange={(checked) => {
handleUpdate("readonly", checked);
handleUpdate("componentConfig.readonly", checked);
}}
className="h-4 w-4"
/>
<Label className="text-xs"></Label>
</div>
)}
</div>
</div>
);
};
// 상세 설정 탭 (DetailSettingsPanel의 전체 로직 통합)
const renderDetailTab = () => {
console.log("🔍 [renderDetailTab] selectedComponent.type:", selectedComponent.type);
// 1. DataTable 컴포넌트
if (selectedComponent.type === "datatable") {
console.log("✅ [renderDetailTab] 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") {
console.log("✅ [renderDetailTab] 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-muted-foreground text-sm"> 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 className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<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-muted-foreground text-xs">{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}
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
onChange={(newConfig) => {
console.log("🔄 DynamicComponentConfigPanel onChange:", newConfig);
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
Object.entries(newConfig).forEach(([key, value]) => {
handleUpdate(`componentConfig.${key}`, value);
});
}}
/>
{/* 🆕 테이블 데이터 자동 입력 (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>
</div>
);
}
// 6. Widget 컴포넌트
if (selectedComponent.type === "widget") {
console.log("✅ [renderDetailTab] Widget 타입");
const widget = selectedComponent as WidgetComponent;
console.log("🔍 [renderDetailTab] widget.widgetType:", widget.widgetType);
// 새로운 컴포넌트 시스템 (widgetType이 button, card 등) - 먼저 체크
if (
widget.widgetType &&
["button", "card", "dashboard", "stats-card", "progress-bar", "chart", "alert", "badge"].includes(
widget.widgetType,
)
) {
console.log("✅ [renderDetailTab] DynamicComponent 반환 (widgetType)");
return (
<DynamicComponentConfigPanel
componentId={widget.widgetType}
config={widget.componentConfig || {}}
screenTableName={widget.tableName || currentTable?.tableName || currentTableName}
tableColumns={currentTable?.columns || []}
tables={tables}
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
onChange={(newConfig) => {
console.log("🔄 DynamicComponentConfigPanel onChange (widget):", newConfig);
// 전체 componentConfig를 업데이트
handleUpdate("componentConfig", newConfig);
}}
/>
);
}
// 일반 위젯 (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>
);
}
// 기본 메시지
return (
<div className="flex h-full items-center justify-center p-8 text-center">
<p className="text-muted-foreground text-sm"> </p>
</div>
);
};
return (
<div className="flex h-full flex-col bg-white">
{/* 헤더 - 간소화 */}
<div className="border-border border-b px-3 py-2">
{selectedComponent.type === "widget" && (
<div className="text-muted-foreground truncate text-[10px]">
{(selectedComponent as WidgetComponent).label || selectedComponent.id}
</div>
)}
</div>
{/* 통합 컨텐츠 (탭 제거) */}
<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">
<Monitor className="text-primary h-3 w-3" />
<h4 className="text-xs font-semibold"> </h4>
</div>
<ResolutionPanel currentResolution={currentResolution} onResolutionChange={onResolutionChange} />
</div>
<Separator className="my-2" />
</>
)}
{/* 격자 설정 - 해상도 설정 아래 표시 */}
{renderGridSettings()}
{gridSettings && onGridSettingsChange && <Separator className="my-2" />}
{/* 기본 설정 */}
{renderBasicTab()}
{/* 상세 설정 */}
<Separator className="my-2" />
{renderDetailTab()}
{/* 스타일 설정 */}
{selectedComponent && (
<>
<Separator className="my-2" />
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Palette className="text-primary h-3 w-3" />
<h4 className="text-xs font-semibold"> </h4>
</div>
<StyleEditor
style={selectedComponent.style || {}}
onStyleChange={(style) => {
if (onStyleChange) {
onStyleChange(style);
} else {
handleUpdate("style", style);
}
}}
/>
</div>
</>
)}
</div>
</div>
</div>
);
};
export default UnifiedPropertiesPanel;