1486 lines
59 KiB
TypeScript
1486 lines
59 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useEffect } from "react";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Separator } from "@/components/ui/separator";
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
import { Checkbox } from "@/components/ui/checkbox";
|
||
import { 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>("");
|
||
|
||
// 🆕 전체 테이블 목록 (selected-items-detail-input 등에서 사용)
|
||
const [allTables, setAllTables] = useState<Array<{ tableName: string; displayName?: string }>>([]);
|
||
|
||
// 🆕 전체 테이블 목록 로드
|
||
useEffect(() => {
|
||
const loadAllTables = async () => {
|
||
try {
|
||
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
||
const response = await tableManagementApi.getTableList();
|
||
if (response.success && response.data) {
|
||
setAllTables(response.data);
|
||
}
|
||
} catch (error) {
|
||
console.error("전체 테이블 목록 로드 실패:", error);
|
||
}
|
||
};
|
||
loadAllTables();
|
||
}, []);
|
||
|
||
// 새로운 컴포넌트 시스템의 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) => {
|
||
// 기존 config와 병합하여 다른 속성 유지
|
||
const currentConfig = selectedComponent.componentConfig?.config || {};
|
||
const mergedConfig = { ...currentConfig, ...newConfig };
|
||
onUpdateProperty(selectedComponent.id, "componentConfig.config", mergedConfig);
|
||
};
|
||
|
||
// 🆕 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} // 테이블 정보 전달
|
||
allTables={allTables} // 🆕 전체 테이블 목록 전달 (selected-items-detail-input 등에서 사용)
|
||
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} // 🔧 화면 테이블명 전달
|
||
tableColumns={currentTable?.columns || []} // 🔧 테이블 컬럼 정보 전달
|
||
/>
|
||
</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;
|