- Changed color schemes for various screen types and roles to align with the new design guidelines, enhancing visual consistency across the application. - Updated background colors for components based on their types, such as changing 'bg-slate-400' to 'bg-muted-foreground' and adjusting other color mappings for better clarity. - Improved the styling of the ScreenNode and V2PropertiesPanel components to ensure a more cohesive user experience. - Enhanced the DynamicComponentRenderer to support dynamic loading of column metadata with cache invalidation for better performance. These changes aim to refine the UI and improve the overall aesthetic of the application, ensuring a more modern and user-friendly interface.
1322 lines
55 KiB
TypeScript
1322 lines
55 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, Suspense } 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 } 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 { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent";
|
|
|
|
// ComponentRegistry import (동적 ConfigPanel 가져오기용)
|
|
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
|
|
import { columnMetaCache } from "@/lib/registry/DynamicComponentRenderer";
|
|
import { DynamicComponentConfigPanel, hasComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
|
|
import StyleEditor from "../StyleEditor";
|
|
import { Slider } from "@/components/ui/slider";
|
|
import { Zap } from "lucide-react";
|
|
import { ConditionalConfigPanel } from "@/components/v2/ConditionalConfigPanel";
|
|
import { ConditionalConfig } from "@/types/v2-components";
|
|
|
|
interface V2PropertiesPanelProps {
|
|
selectedComponent?: ComponentData;
|
|
tables: TableInfo[];
|
|
onUpdateProperty: (componentId: string, path: string, value: any) => void;
|
|
onDeleteComponent?: (componentId: string) => void;
|
|
onCopyComponent?: (componentId: string) => void;
|
|
currentTable?: TableInfo;
|
|
currentTableName?: string;
|
|
dragState?: any;
|
|
// 스타일 관련
|
|
onStyleChange?: (style: any) => void;
|
|
// 🆕 플로우 위젯 감지용
|
|
allComponents?: ComponentData[];
|
|
// 🆕 메뉴 OBJID (코드/카테고리 스코프용)
|
|
menuObjid?: number;
|
|
// 🆕 현재 편집 중인 화면의 회사 코드
|
|
currentScreenCompanyCode?: string;
|
|
}
|
|
|
|
export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
|
|
selectedComponent,
|
|
tables,
|
|
onUpdateProperty,
|
|
onDeleteComponent,
|
|
onCopyComponent,
|
|
currentTable,
|
|
currentTableName,
|
|
currentScreenCompanyCode,
|
|
dragState,
|
|
onStyleChange,
|
|
menuObjid,
|
|
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]);
|
|
|
|
// 컴포넌트가 선택되지 않았을 때는 안내 메시지만 표시
|
|
if (!selectedComponent) {
|
|
return (
|
|
<div className="flex h-full flex-col overflow-x-auto bg-white">
|
|
<div className="flex-1 overflow-x-auto overflow-y-auto p-2">
|
|
<div className="space-y-4 text-xs">
|
|
{/* 안내 메시지 */}
|
|
<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;
|
|
|
|
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기 시도
|
|
const componentId =
|
|
selectedComponent.componentType || // ⭐ section-card 등
|
|
selectedComponent.componentConfig?.type ||
|
|
selectedComponent.componentConfig?.id ||
|
|
(selectedComponent.type === "component" ? selectedComponent.id : null); // 🆕 독립 컴포넌트 (table-search-widget 등)
|
|
|
|
// 🆕 V2 컴포넌트 직접 감지 및 설정 패널 렌더링
|
|
if (componentId?.startsWith("v2-")) {
|
|
const v2ConfigPanels: Record<string, React.FC<{ config: any; onChange: (config: any) => void }>> = {
|
|
"v2-input": require("@/components/v2/config-panels/V2FieldConfigPanel").V2FieldConfigPanel,
|
|
"v2-select": require("@/components/v2/config-panels/V2FieldConfigPanel").V2FieldConfigPanel,
|
|
"v2-date": require("@/components/v2/config-panels/V2DateConfigPanel").V2DateConfigPanel,
|
|
"v2-list": require("@/components/v2/config-panels/V2ListConfigPanel").V2ListConfigPanel,
|
|
"v2-layout": require("@/components/v2/config-panels/V2LayoutConfigPanel").V2LayoutConfigPanel,
|
|
"v2-group": require("@/components/v2/config-panels/V2GroupConfigPanel").V2GroupConfigPanel,
|
|
"v2-media": require("@/components/v2/config-panels/V2MediaConfigPanel").V2MediaConfigPanel,
|
|
"v2-biz": require("@/components/v2/config-panels/V2BizConfigPanel").V2BizConfigPanel,
|
|
"v2-hierarchy": require("@/components/v2/config-panels/V2HierarchyConfigPanel").V2HierarchyConfigPanel,
|
|
"v2-bom-item-editor": require("@/components/v2/config-panels/V2BomItemEditorConfigPanel")
|
|
.V2BomItemEditorConfigPanel,
|
|
"v2-bom-tree": require("@/components/v2/config-panels/V2BomTreeConfigPanel").V2BomTreeConfigPanel,
|
|
};
|
|
|
|
const V2ConfigPanel = v2ConfigPanels[componentId];
|
|
if (V2ConfigPanel) {
|
|
const currentConfig = selectedComponent.componentConfig || {};
|
|
const handleV2ConfigChange = (newConfig: any) => {
|
|
onUpdateProperty(selectedComponent.id, "componentConfig", { ...currentConfig, ...newConfig });
|
|
};
|
|
|
|
// 현재 화면의 테이블명 가져오기
|
|
const currentTableName = tables?.[0]?.tableName;
|
|
|
|
// DB input_type 가져오기 (columnMetaCache에서 최신값 조회)
|
|
const colName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName;
|
|
const tblName = selectedComponent.tableName || currentTable?.tableName || currentTableName;
|
|
const dbMeta = colName && tblName && !colName.includes(".") ? columnMetaCache[tblName]?.[colName] : undefined;
|
|
const dbInputType = dbMeta ? (() => { const raw = dbMeta.input_type || dbMeta.inputType; return raw === "direct" || raw === "auto" ? undefined : raw; })() : undefined;
|
|
const inputType = dbInputType || currentConfig.inputType || currentConfig.webType || (selectedComponent as any).inputType;
|
|
|
|
// 컴포넌트별 추가 props
|
|
const extraProps: Record<string, any> = {};
|
|
const resolvedTableName = selectedComponent.tableName || currentTable?.tableName || currentTableName;
|
|
const resolvedColumnName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName;
|
|
|
|
if (componentId === "v2-input" || componentId === "v2-select") {
|
|
extraProps.inputType = inputType;
|
|
extraProps.tableName = resolvedTableName;
|
|
extraProps.columnName = resolvedColumnName;
|
|
extraProps.screenTableName = resolvedTableName;
|
|
}
|
|
if (componentId === "v2-input") {
|
|
extraProps.allComponents = allComponents;
|
|
}
|
|
if (componentId === "v2-list") {
|
|
extraProps.currentTableName = currentTableName;
|
|
}
|
|
if (componentId === "v2-bom-item-editor" || componentId === "v2-bom-tree") {
|
|
extraProps.currentTableName = currentTableName;
|
|
extraProps.screenTableName = resolvedTableName;
|
|
}
|
|
|
|
return (
|
|
<div key={selectedComponent.id} className="space-y-4">
|
|
<V2ConfigPanel config={currentConfig} onChange={handleV2ConfigChange} {...extraProps} />
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
if (componentId) {
|
|
const definition = ComponentRegistry.getComponent(componentId);
|
|
|
|
if (definition?.configPanel) {
|
|
const ConfigPanelComponent = definition.configPanel;
|
|
const currentConfig = selectedComponent.componentConfig || {};
|
|
|
|
// 🔧 ConfigPanelWrapper를 인라인 함수 대신 직접 JSX 반환 (리마운트 방지)
|
|
const config = currentConfig || definition.defaultProps?.componentConfig || {};
|
|
|
|
const handlePanelConfigChange = (newConfig: any) => {
|
|
// 🔧 Partial 업데이트: 기존 componentConfig를 유지하면서 새 설정만 병합
|
|
const mergedConfig = {
|
|
...currentConfig, // 기존 설정 유지
|
|
...newConfig, // 새 설정 병합
|
|
};
|
|
onUpdateProperty(selectedComponent.id, "componentConfig", mergedConfig);
|
|
};
|
|
|
|
return (
|
|
<div key={selectedComponent.id} className="space-y-4">
|
|
<Suspense
|
|
fallback={
|
|
<div className="flex items-center justify-center py-8">
|
|
<div className="text-muted-foreground text-sm">설정 패널 로딩 중...</div>
|
|
</div>
|
|
}
|
|
>
|
|
<ConfigPanelComponent
|
|
config={config}
|
|
onChange={handlePanelConfigChange}
|
|
onConfigChange={handlePanelConfigChange}
|
|
tables={tables}
|
|
allTables={allTables}
|
|
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
|
tableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
|
columnName={
|
|
(selectedComponent as any).columnName || currentConfig?.columnName || currentConfig?.fieldName
|
|
}
|
|
inputType={(selectedComponent as any).inputType || currentConfig?.inputType}
|
|
componentType={componentType}
|
|
tableColumns={currentTable?.columns || []}
|
|
allComponents={allComponents}
|
|
currentComponent={selectedComponent}
|
|
menuObjid={menuObjid}
|
|
screenComponents={allComponents.map((comp: any) => ({
|
|
id: comp.id,
|
|
componentType: comp.componentType || comp.type,
|
|
label: comp.label || comp.name || comp.id,
|
|
tableName: comp.componentConfig?.tableName || comp.tableName,
|
|
columnName: comp.columnName || comp.componentConfig?.columnName || comp.componentConfig?.fieldName,
|
|
}))}
|
|
/>
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|
|
// ConfigPanel이 없으면 DynamicComponentConfigPanel fallback으로 처리
|
|
}
|
|
|
|
// DynamicComponentConfigPanel을 통한 동적 로드 (CONFIG_PANEL_MAP 기반)
|
|
const fallbackId = componentId || componentType;
|
|
if (fallbackId && hasComponentConfigPanel(fallbackId)) {
|
|
const handleDynamicConfigChange = (newConfig: any) => {
|
|
const currentConfig = selectedComponent.componentConfig || {};
|
|
const mergedConfig = { ...currentConfig, ...newConfig };
|
|
onUpdateProperty(selectedComponent.id, "componentConfig", mergedConfig);
|
|
};
|
|
|
|
return (
|
|
<DynamicComponentConfigPanel
|
|
componentId={fallbackId}
|
|
componentType={componentType}
|
|
config={selectedComponent.componentConfig || {}}
|
|
onChange={handleDynamicConfigChange}
|
|
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
|
tableColumns={currentTable?.columns || []}
|
|
tables={tables}
|
|
menuObjid={menuObjid}
|
|
allComponents={allComponents}
|
|
currentComponent={selectedComponent}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
|
|
<Settings className="text-muted-foreground mb-4 h-12 w-12" />
|
|
<h3 className="mb-2 text-base font-medium">설정 패널 없음</h3>
|
|
<p className="text-muted-foreground text-sm">
|
|
컴포넌트 "{fallbackId || componentType}"에 대한 설정 패널이 없습니다.
|
|
</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 기본 정보 탭
|
|
const renderBasicTab = () => {
|
|
const widget = selectedComponent as WidgetComponent;
|
|
const group = selectedComponent as GroupComponent;
|
|
const area = selectedComponent as AreaComponent;
|
|
|
|
// 라벨 설정이 표시될 입력 필드 타입들
|
|
const inputFieldTypes = [
|
|
"text",
|
|
"number",
|
|
"decimal",
|
|
"date",
|
|
"datetime",
|
|
"time",
|
|
"email",
|
|
"tel",
|
|
"url",
|
|
"password",
|
|
"textarea",
|
|
"select",
|
|
"dropdown",
|
|
"entity",
|
|
"code",
|
|
"checkbox",
|
|
"radio",
|
|
"boolean",
|
|
"file",
|
|
"autocomplete",
|
|
"text-input",
|
|
"number-input",
|
|
"date-input",
|
|
"textarea-basic",
|
|
"select-basic",
|
|
"checkbox-basic",
|
|
"radio-basic",
|
|
"entity-search-input",
|
|
"autocomplete-search-input",
|
|
// 새로운 통합 입력 컴포넌트
|
|
"v2-input",
|
|
"v2-select",
|
|
"v2-entity-select",
|
|
"v2-checkbox",
|
|
"v2-radio",
|
|
"v2-textarea",
|
|
"v2-date",
|
|
"v2-datetime",
|
|
"v2-time",
|
|
"v2-file",
|
|
];
|
|
|
|
// 현재 컴포넌트가 입력 필드인지 확인
|
|
const componentType = widget.widgetType || (widget as any).componentId || (widget as any).componentType;
|
|
const isInputField = inputFieldTypes.includes(componentType);
|
|
|
|
return (
|
|
<div className="space-y-1">
|
|
{/* DIMENSIONS 섹션 */}
|
|
<div className="border-border/50 mb-3 border-b pb-3">
|
|
<h4 className="text-muted-foreground py-2 text-[10px] font-semibold tracking-wider uppercase">DIMENSIONS</h4>
|
|
<div className="flex gap-2">
|
|
<div className="flex-1">
|
|
<Label className="text-muted-foreground text-[10px]">너비</Label>
|
|
<Input
|
|
type="number"
|
|
min={10}
|
|
max={3840}
|
|
step="1"
|
|
value={localWidth}
|
|
onChange={(e) => {
|
|
setLocalWidth(e.target.value);
|
|
}}
|
|
onBlur={(e) => {
|
|
const value = parseInt(e.target.value) || 0;
|
|
if (value >= 10) {
|
|
const snappedValue = Math.round(value / 10) * 10;
|
|
handleUpdate("size.width", snappedValue);
|
|
setLocalWidth(String(snappedValue));
|
|
}
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
const value = parseInt(e.currentTarget.value) || 0;
|
|
if (value >= 10) {
|
|
const snappedValue = Math.round(value / 10) * 10;
|
|
handleUpdate("size.width", snappedValue);
|
|
setLocalWidth(String(snappedValue));
|
|
}
|
|
e.currentTarget.blur();
|
|
}
|
|
}}
|
|
placeholder="100"
|
|
className="h-7 w-full text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<Label className="text-muted-foreground text-[10px]">높이</Label>
|
|
<Input
|
|
type="number"
|
|
value={localHeight}
|
|
onChange={(e) => {
|
|
setLocalHeight(e.target.value);
|
|
}}
|
|
onBlur={(e) => {
|
|
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) => {
|
|
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-7 w-full text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<Label className="text-muted-foreground text-[10px]">Z-Index</Label>
|
|
<Input
|
|
type="number"
|
|
step="1"
|
|
value={currentPosition.z || 1}
|
|
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
|
|
className="h-7 w-full text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Title (group/area) */}
|
|
{(selectedComponent.type === "group" || selectedComponent.type === "area") && (
|
|
<div className="border-border/50 mb-3 border-b pb-3">
|
|
<h4 className="text-muted-foreground py-2 text-[10px] font-semibold tracking-wider uppercase">CONTENT</h4>
|
|
<div className="flex items-center justify-between py-1.5">
|
|
<span className="text-muted-foreground text-xs">제목</span>
|
|
<div className="w-[160px]">
|
|
<Input
|
|
value={group.title || area.title || ""}
|
|
onChange={(e) => handleUpdate("title", e.target.value)}
|
|
placeholder="제목"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{selectedComponent.type === "area" && (
|
|
<div className="flex items-center justify-between py-1.5">
|
|
<span className="text-muted-foreground text-xs">설명</span>
|
|
<div className="w-[160px]">
|
|
<Input
|
|
value={area.description || ""}
|
|
onChange={(e) => handleUpdate("description", e.target.value)}
|
|
placeholder="설명"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* OPTIONS 섹션 */}
|
|
<div className="border-border/50 mb-3 border-b pb-3">
|
|
<h4 className="text-muted-foreground py-2 text-[10px] font-semibold tracking-wider uppercase">OPTIONS</h4>
|
|
{(isInputField || widget.required !== undefined) &&
|
|
(() => {
|
|
const colName = widget.columnName || selectedComponent?.columnName;
|
|
const colMeta = colName
|
|
? currentTable?.columns?.find(
|
|
(c: any) => (c.columnName || c.column_name || "").toLowerCase() === colName.toLowerCase(),
|
|
)
|
|
: null;
|
|
const isNotNull =
|
|
colMeta &&
|
|
((colMeta as any).isNullable === "NO" ||
|
|
(colMeta as any).isNullable === "N" ||
|
|
(colMeta as any).is_nullable === "NO" ||
|
|
(colMeta as any).is_nullable === "N");
|
|
return (
|
|
<div className="flex items-center justify-between py-1.5">
|
|
<span className="text-muted-foreground text-xs">
|
|
필수
|
|
{isNotNull && <span className="text-muted-foreground/60 ml-1">(NOT NULL)</span>}
|
|
</span>
|
|
<Checkbox
|
|
checked={
|
|
isNotNull || widget.required === true || selectedComponent.componentConfig?.required === true
|
|
}
|
|
onCheckedChange={(checked) => {
|
|
if (isNotNull) return;
|
|
handleUpdate("required", checked);
|
|
handleUpdate("componentConfig.required", checked);
|
|
}}
|
|
disabled={!!isNotNull}
|
|
className="h-4 w-4"
|
|
/>
|
|
</div>
|
|
);
|
|
})()}
|
|
{(isInputField || widget.readonly !== undefined) && (
|
|
<div className="flex items-center justify-between py-1.5">
|
|
<span className="text-muted-foreground text-xs">읽기전용</span>
|
|
<Checkbox
|
|
checked={widget.readonly === true || selectedComponent.componentConfig?.readonly === true}
|
|
onCheckedChange={(checked) => {
|
|
handleUpdate("readonly", checked);
|
|
handleUpdate("componentConfig.readonly", checked);
|
|
}}
|
|
className="h-4 w-4"
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center justify-between py-1.5">
|
|
<span className="text-muted-foreground text-xs">숨김</span>
|
|
<Checkbox
|
|
checked={selectedComponent.hidden === true || selectedComponent.componentConfig?.hidden === true}
|
|
onCheckedChange={(checked) => {
|
|
handleUpdate("hidden", checked);
|
|
handleUpdate("componentConfig.hidden", checked);
|
|
}}
|
|
className="h-4 w-4"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* LABEL 섹션 - 입력 필드에서만 표시 */}
|
|
{isInputField && (
|
|
<Collapsible>
|
|
<CollapsibleTrigger className="flex w-full items-center justify-between py-0.5 text-left">
|
|
<span className="text-muted-foreground text-[10px] font-semibold tracking-wider uppercase">LABEL</span>
|
|
<ChevronDown className="text-muted-foreground/50 h-3 w-3 shrink-0" />
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent className="mt-1.5 space-y-1">
|
|
{/* 라벨 텍스트 */}
|
|
<div className="flex items-center justify-between py-1.5">
|
|
<span className="text-muted-foreground text-xs">텍스트</span>
|
|
<div className="w-[160px]">
|
|
<Input
|
|
value={
|
|
selectedComponent.style?.labelText !== undefined
|
|
? selectedComponent.style.labelText
|
|
: selectedComponent.label || selectedComponent.componentConfig?.label || ""
|
|
}
|
|
onChange={(e) => {
|
|
handleUpdate("style.labelText", e.target.value);
|
|
handleUpdate("label", e.target.value);
|
|
}}
|
|
placeholder="라벨"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/* 위치 + 간격 */}
|
|
<div className="flex gap-2">
|
|
<div className="flex-1">
|
|
<Label className="text-muted-foreground text-[10px]">위치</Label>
|
|
<Select
|
|
value={selectedComponent.style?.labelPosition || "top"}
|
|
onValueChange={(value) => handleUpdate("style.labelPosition", value)}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="top">위</SelectItem>
|
|
<SelectItem value="bottom">아래</SelectItem>
|
|
<SelectItem value="left">왼쪽</SelectItem>
|
|
<SelectItem value="right">오른쪽</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex-1">
|
|
<Label className="text-muted-foreground text-[10px]">간격</Label>
|
|
<Input
|
|
value={
|
|
selectedComponent.style?.labelPosition === "left" ||
|
|
selectedComponent.style?.labelPosition === "right"
|
|
? selectedComponent.style?.labelGap || "8px"
|
|
: selectedComponent.style?.labelMarginBottom || "4px"
|
|
}
|
|
onChange={(e) => {
|
|
const pos = selectedComponent.style?.labelPosition;
|
|
if (pos === "left" || pos === "right") {
|
|
handleUpdate("style.labelGap", e.target.value);
|
|
} else {
|
|
handleUpdate("style.labelMarginBottom", e.target.value);
|
|
}
|
|
}}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/* 크기 + 색상 */}
|
|
<div className="flex gap-2">
|
|
<div className="flex-1">
|
|
<Label className="text-muted-foreground text-[10px]">크기</Label>
|
|
<Input
|
|
value={selectedComponent.style?.labelFontSize || "12px"}
|
|
onChange={(e) => handleUpdate("style.labelFontSize", e.target.value)}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<Label className="text-muted-foreground text-[10px]">색상</Label>
|
|
<ColorPickerWithTransparent
|
|
value={selectedComponent.style?.labelColor}
|
|
onChange={(value) => handleUpdate("style.labelColor", value)}
|
|
defaultColor="#212121"
|
|
placeholder="#212121"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/* 굵기 */}
|
|
<div className="flex items-center justify-between py-1.5">
|
|
<span className="text-muted-foreground text-xs">굵기</span>
|
|
<div className="w-[160px]">
|
|
<Select
|
|
value={selectedComponent.style?.labelFontWeight || "500"}
|
|
onValueChange={(value) => handleUpdate("style.labelFontWeight", value)}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="400">보통</SelectItem>
|
|
<SelectItem value="500">중간</SelectItem>
|
|
<SelectItem value="600">굵게</SelectItem>
|
|
<SelectItem value="700">매우 굵게</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
{/* 표시 */}
|
|
<div className="flex items-center justify-between py-1.5">
|
|
<span className="text-muted-foreground text-xs">표시</span>
|
|
<Checkbox
|
|
checked={selectedComponent.style?.labelDisplay === true || selectedComponent.labelDisplay === true}
|
|
onCheckedChange={(checked) => {
|
|
const boolValue = checked === true;
|
|
handleUpdate("style.labelDisplay", boolValue);
|
|
handleUpdate("labelDisplay", boolValue);
|
|
if (boolValue && !selectedComponent.style?.labelText) {
|
|
const labelValue = selectedComponent.label || selectedComponent.componentConfig?.label || "";
|
|
if (labelValue) {
|
|
handleUpdate("style.labelText", labelValue);
|
|
}
|
|
}
|
|
}}
|
|
className="h-4 w-4"
|
|
/>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)}
|
|
</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}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// 🆕 3.5. V2 컴포넌트 - 반드시 다른 체크보다 먼저 처리
|
|
const v2ComponentType = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type || "";
|
|
if (v2ComponentType.startsWith("v2-")) {
|
|
const configPanel = renderComponentConfigPanel();
|
|
if (configPanel) {
|
|
return <div className="space-y-4">{configPanel}</div>;
|
|
}
|
|
}
|
|
|
|
// 4. 새로운 컴포넌트 시스템 (button, card 등)
|
|
const componentType = selectedComponent.componentConfig?.type || selectedComponent.type;
|
|
const hasNewConfigPanel =
|
|
componentType &&
|
|
[
|
|
"button",
|
|
"button-primary",
|
|
"button-secondary",
|
|
"v2-button-primary",
|
|
"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">
|
|
<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 전달
|
|
allComponents={allComponents} // 🆕 연쇄 드롭다운 부모 감지용
|
|
currentComponent={selectedComponent} // 🆕 현재 컴포넌트 정보
|
|
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") {
|
|
const widget = selectedComponent as WidgetComponent;
|
|
|
|
// 새로운 컴포넌트 시스템 (widgetType이 button, card 등) - 먼저 체크
|
|
if (
|
|
widget.widgetType &&
|
|
["button", "card", "dashboard", "stats-card", "progress-bar", "chart", "alert", "badge"].includes(
|
|
widget.widgetType,
|
|
)
|
|
) {
|
|
return (
|
|
<DynamicComponentConfigPanel
|
|
componentId={widget.widgetType}
|
|
config={widget.componentConfig || {}}
|
|
screenTableName={widget.tableName || currentTable?.tableName || currentTableName}
|
|
tableColumns={currentTable?.columns || []}
|
|
tables={tables}
|
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
|
allComponents={allComponents} // 🆕 연쇄 드롭다운 부모 감지용
|
|
currentComponent={selectedComponent} // 🆕 현재 컴포넌트 정보
|
|
onChange={(newConfig) => {
|
|
handleUpdate("componentConfig", newConfig);
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// 일반 위젯 (webType 기반)
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 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">
|
|
<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="border-destructive space-y-3 border-4 bg-amber-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-x-auto overflow-y-auto p-2">
|
|
<div className="space-y-4 text-xs">
|
|
{/* 기본 설정 */}
|
|
{renderBasicTab()}
|
|
|
|
{/* 상세 설정 */}
|
|
<Separator className="my-2" />
|
|
{renderDetailTab()}
|
|
|
|
{/* 조건부 표시 설정 */}
|
|
{selectedComponent && (
|
|
<>
|
|
<Separator className="my-2" />
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-1.5">
|
|
<Zap className="text-primary h-3 w-3" />
|
|
<h4 className="text-xs font-semibold">조건부 표시</h4>
|
|
</div>
|
|
<div className="border-border rounded-md border p-2">
|
|
<ConditionalConfigPanel
|
|
config={
|
|
(selectedComponent as any).conditional || {
|
|
enabled: false,
|
|
field: "",
|
|
operator: "=",
|
|
value: "",
|
|
action: "show",
|
|
}
|
|
}
|
|
onChange={(newConfig: ConditionalConfig | undefined) => {
|
|
handleUpdate("conditional", newConfig);
|
|
}}
|
|
availableFields={
|
|
allComponents
|
|
?.filter((c) => {
|
|
// 자기 자신 제외
|
|
if (c.id === selectedComponent.id) return false;
|
|
// widget 타입 또는 component 타입 (V2 컴포넌트 포함)
|
|
return c.type === "widget" || c.type === "component";
|
|
})
|
|
.map((c) => {
|
|
const widgetType = (c as any).widgetType || (c as any).componentType || "text";
|
|
const config = (c as any).componentConfig || (c as any).webTypeConfig || {};
|
|
const detailSettings = (c as any).detailSettings || {};
|
|
|
|
// 정적 옵션 추출 (select, dropdown, radio, entity 등)
|
|
let options: Array<{ value: string; label: string }> | undefined;
|
|
|
|
// V2 컴포넌트의 경우
|
|
if (config.options && Array.isArray(config.options)) {
|
|
options = config.options;
|
|
}
|
|
// 레거시 컴포넌트의 경우
|
|
else if ((c as any).options && Array.isArray((c as any).options)) {
|
|
options = (c as any).options;
|
|
}
|
|
|
|
// 엔티티 정보 추출 (config > detailSettings > 직접 속성 순으로 우선순위)
|
|
const entityTable =
|
|
config.entityTable ||
|
|
detailSettings.referenceTable ||
|
|
(c as any).entityTable ||
|
|
(c as any).referenceTable;
|
|
const entityValueColumn =
|
|
config.entityValueColumn ||
|
|
detailSettings.referenceColumn ||
|
|
(c as any).entityValueColumn ||
|
|
(c as any).referenceColumn;
|
|
const entityLabelColumn =
|
|
config.entityLabelColumn ||
|
|
detailSettings.displayColumn ||
|
|
(c as any).entityLabelColumn ||
|
|
(c as any).displayColumn;
|
|
|
|
// 공통코드 정보 추출
|
|
const codeGroup = config.codeGroup || detailSettings.codeGroup || (c as any).codeGroup;
|
|
|
|
return {
|
|
id: (c as any).columnName || c.id,
|
|
label: (c as any).label || config.label || c.id,
|
|
type: widgetType,
|
|
options,
|
|
entityTable,
|
|
entityValueColumn,
|
|
entityLabelColumn,
|
|
codeGroup,
|
|
};
|
|
}) || []
|
|
}
|
|
currentComponentId={selectedComponent.id}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 스타일 설정 */}
|
|
{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 V2PropertiesPanel;
|