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

1461 lines
58 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 { Textarea } from "@/components/ui/textarea";
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;
// 🎯 Section Card, Section Paper 등 신규 컴포넌트는 componentType에서 감지
const componentType =
selectedComponent.componentType || // ⭐ 1순위: ScreenDesigner가 설정한 componentType (section-card 등)
selectedComponent.componentConfig?.type ||
selectedComponent.componentConfig?.id ||
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.componentType || // ⭐ section-card 등
selectedComponent.componentConfig?.type ||
selectedComponent.componentConfig?.id ||
(selectedComponent.type === "component" ? selectedComponent.id : null); // 🆕 독립 컴포넌트 (table-search-widget 등)
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 = () => {
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
const config = currentConfig || definition.defaultProps?.componentConfig || {};
const handleConfigChange = (newConfig: any) => {
// componentConfig 전체를 업데이트
onUpdateProperty(selectedComponent.id, "componentConfig", 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}
onChange={handleConfigChange}
tables={tables} // 테이블 정보 전달
/>
</div>
);
};
return <ConfigPanelWrapper key={selectedComponent.id} />;
} else {
console.warn("⚠️ ComponentRegistry에서 ConfigPanel을 찾을 수 없음 - switch case로 이동:", {
componentId,
definitionName: definition?.name,
hasDefinition: !!definition,
});
// ConfigPanel이 없으면 아래 switch case로 넘어감
}
}
// 기존 하드코딩된 설정 패널들 (레거시)
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} />;
case "section-card":
return (
<div className="space-y-4 p-4">
<div className="space-y-2">
<h3 className="text-sm font-semibold">Section Card </h3>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 헤더 표시 */}
<div className="flex items-center space-x-2">
<Checkbox
id="showHeader"
checked={selectedComponent.componentConfig?.showHeader !== false}
onCheckedChange={(checked) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.showHeader", checked);
}}
/>
<Label htmlFor="showHeader" className="text-xs cursor-pointer">
</Label>
</div>
{/* 제목 */}
{selectedComponent.componentConfig?.showHeader !== false && (
<div className="space-y-2">
<Label className="text-xs"></Label>
<Input
value={selectedComponent.componentConfig?.title || ""}
onChange={(e) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.title", e.target.value);
}}
placeholder="섹션 제목 입력"
className="h-9 text-xs"
/>
</div>
)}
{/* 설명 */}
{selectedComponent.componentConfig?.showHeader !== false && (
<div className="space-y-2">
<Label className="text-xs"> ()</Label>
<Textarea
value={selectedComponent.componentConfig?.description || ""}
onChange={(e) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.description", e.target.value);
}}
placeholder="섹션 설명 입력"
className="text-xs resize-none"
rows={2}
/>
</div>
)}
{/* 패딩 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={selectedComponent.componentConfig?.padding || "md"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.padding", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="sm"> (12px)</SelectItem>
<SelectItem value="md"> (24px)</SelectItem>
<SelectItem value="lg"> (32px)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 배경색 */}
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select
value={selectedComponent.componentConfig?.backgroundColor || "default"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.backgroundColor", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"> ()</SelectItem>
<SelectItem value="muted"></SelectItem>
<SelectItem value="transparent"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 테두리 스타일 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={selectedComponent.componentConfig?.borderStyle || "solid"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.borderStyle", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="solid"></SelectItem>
<SelectItem value="dashed"></SelectItem>
<SelectItem value="none"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 접기/펼치기 기능 */}
<div className="space-y-2 pt-2 border-t">
<div className="flex items-center space-x-2">
<Checkbox
id="collapsible"
checked={selectedComponent.componentConfig?.collapsible || false}
onCheckedChange={(checked) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.collapsible", checked);
}}
/>
<Label htmlFor="collapsible" className="text-xs cursor-pointer">
/
</Label>
</div>
{selectedComponent.componentConfig?.collapsible && (
<div className="flex items-center space-x-2 ml-6">
<Checkbox
id="defaultOpen"
checked={selectedComponent.componentConfig?.defaultOpen !== false}
onCheckedChange={(checked) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.defaultOpen", checked);
}}
/>
<Label htmlFor="defaultOpen" className="text-xs cursor-pointer">
</Label>
</div>
)}
</div>
</div>
);
case "section-paper":
return (
<div className="space-y-4 p-4">
<div className="space-y-2">
<h3 className="text-sm font-semibold">Section Paper </h3>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 배경색 */}
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select
value={selectedComponent.componentConfig?.backgroundColor || "default"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.backgroundColor", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"> ( )</SelectItem>
<SelectItem value="muted"></SelectItem>
<SelectItem value="accent"> ( )</SelectItem>
<SelectItem value="primary"> </SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 커스텀 색상 */}
{selectedComponent.componentConfig?.backgroundColor === "custom" && (
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
type="color"
value={selectedComponent.componentConfig?.customColor || "#f0f0f0"}
onChange={(e) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.customColor", e.target.value);
}}
className="h-9"
/>
</div>
)}
{/* 패딩 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={selectedComponent.componentConfig?.padding || "md"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.padding", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="sm"> (12px)</SelectItem>
<SelectItem value="md"> (16px)</SelectItem>
<SelectItem value="lg"> (24px)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 둥근 모서리 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={selectedComponent.componentConfig?.roundedCorners || "md"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.roundedCorners", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="sm"> (2px)</SelectItem>
<SelectItem value="md"> (6px)</SelectItem>
<SelectItem value="lg"> (8px)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 그림자 */}
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select
value={selectedComponent.componentConfig?.shadow || "none"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.shadow", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="sm"></SelectItem>
<SelectItem value="md"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 테두리 표시 */}
<div className="flex items-center space-x-2">
<Checkbox
id="showBorder"
checked={selectedComponent.componentConfig?.showBorder || false}
onCheckedChange={(checked) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.showBorder", checked);
}}
/>
<Label htmlFor="showBorder" className="text-xs cursor-pointer">
</Label>
</div>
</div>
);
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 = () => {
// 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-muted-foreground text-sm"> ID가 </p>
</div>
);
}
// 🆕 ComponentRegistry에서 전용 ConfigPanel이 있는지 먼저 확인
const definition = ComponentRegistry.getComponent(componentId);
if (definition?.configPanel) {
// 전용 ConfigPanel이 있으면 renderComponentConfigPanel 호출
const configPanelContent = renderComponentConfigPanel();
if (configPanelContent) {
return configPanelContent;
}
}
// 현재 웹타입의 기본 입력 타입 추출
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) => {
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
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;