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

1173 lines
52 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. 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 { Settings } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useWebTypes } from "@/hooks/admin/useWebTypes";
import { getConfigPanelComponent } from "@/lib/utils/getConfigPanelComponent";
import {
ComponentData,
WidgetComponent,
FileComponent,
WebTypeConfig,
TableInfo,
LayoutComponent,
} from "@/types/screen";
// 레거시 ButtonConfigPanel 제거됨
import { FileComponentConfigPanel } from "./FileComponentConfigPanel";
import { WebTypeConfigPanel } from "./WebTypeConfigPanel";
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
import { BaseInputType, getBaseInputType, getDetailTypes, DetailTypeOption } from "@/types/input-type-mapping";
// 새로운 컴포넌트 설정 패널들 import
import { ButtonConfigPanel as NewButtonConfigPanel } from "../config-panels/ButtonConfigPanel";
import { CardConfigPanel } from "../config-panels/CardConfigPanel";
import { DashboardConfigPanel } from "../config-panels/DashboardConfigPanel";
import { StatsCardConfigPanel } from "../config-panels/StatsCardConfigPanel";
import { ProgressBarConfigPanel } from "../config-panels/ProgressBarConfigPanel";
import { ChartConfigPanel } from "../config-panels/ChartConfigPanel";
import { AlertConfigPanel } from "../config-panels/AlertConfigPanel";
import { BadgeConfigPanel } from "../config-panels/BadgeConfigPanel";
// 동적 컴포넌트 설정 패널
import { DynamicComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
interface DetailSettingsPanelProps {
selectedComponent?: ComponentData;
onUpdateProperty: (componentId: string, path: string, value: any) => void;
currentTable?: TableInfo; // 현재 화면의 테이블 정보
currentTableName?: string; // 현재 화면의 테이블명
tables?: TableInfo[]; // 전체 테이블 목록
}
export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
selectedComponent,
onUpdateProperty,
currentTable,
currentTableName,
tables = [], // 기본값 빈 배열
}) => {
// 데이터베이스에서 입력 가능한 웹타입들을 동적으로 가져오기
const { webTypes } = useWebTypes({ active: "Y" });
// console.log(`🔍 DetailSettingsPanel props:`, {
// selectedComponent: selectedComponent?.id,
// componentType: selectedComponent?.type,
// currentTableName,
// currentTable: currentTable?.tableName,
// selectedComponentTableName: selectedComponent?.tableName,
// });
// console.log(`🔍 DetailSettingsPanel webTypes 로드됨:`, webTypes?.length, "개");
// console.log(`🔍 webTypes:`, webTypes);
// console.log(`🔍 DetailSettingsPanel selectedComponent:`, selectedComponent);
// console.log(`🔍 DetailSettingsPanel selectedComponent.widgetType:`, selectedComponent?.widgetType);
const inputableWebTypes = webTypes.map((wt) => wt.web_type);
// 새로운 컴포넌트 시스템용 로컬 상태
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
// 새로운 컴포넌트 시스템의 webType 동기화
useEffect(() => {
if (selectedComponent?.type === "component") {
const webType = selectedComponent.componentConfig?.webType;
if (webType) {
setLocalComponentDetailType(webType);
}
}
}, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]);
// 레이아웃 컴포넌트 설정 렌더링 함수
const renderLayoutConfig = (layoutComponent: LayoutComponent) => {
return (
<div className="flex h-full flex-col">
{/* 헤더 */}
<div className="border-b p-4">
<div className="flex items-center gap-2">
<Settings className="text-muted-foreground h-4 w-4" />
<h3 className="text-sm font-semibold"> </h3>
</div>
<div className="mt-2 flex items-center gap-2">
<span className="text-muted-foreground text-xs">:</span>
<span className="bg-primary/10 text-primary rounded-md px-2 py-0.5 text-xs font-medium">
{layoutComponent.layoutType}
</span>
</div>
<div className="text-muted-foreground mt-1 text-xs">ID: {layoutComponent.id}</div>
</div>
{/* 레이아웃 설정 영역 */}
<div className="flex-1 space-y-3 overflow-y-auto p-4">
{/* 기본 정보 */}
<div className="space-y-1.5">
<label className="text-xs font-medium"> </label>
<input
type="text"
value={layoutComponent.label || ""}
onChange={(e) => onUpdateProperty(layoutComponent.id, "label", e.target.value)}
className="border-input bg-background focus-visible:ring-ring h-8 w-full rounded-md border px-3 text-xs focus-visible:ring-1 focus-visible:outline-none"
placeholder="레이아웃 이름을 입력하세요"
/>
</div>
{/* 그리드 레이아웃 설정 */}
{layoutComponent.layoutType === "grid" && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"> </label>
<input
type="number"
min="1"
max="10"
value={layoutComponent.layoutConfig?.grid?.rows || 2}
onChange={(e) => {
const newRows = parseInt(e.target.value);
const newCols = layoutComponent.layoutConfig?.grid?.columns || 2;
// 그리드 설정 업데이트
onUpdateProperty(layoutComponent.id, "layoutConfig.grid.rows", newRows);
// 존 개수 자동 업데이트 (행 × 열)
const totalZones = newRows * newCols;
const currentZones = layoutComponent.zones || [];
if (totalZones !== currentZones.length) {
const newZones = [];
for (let row = 0; row < newRows; row++) {
for (let col = 0; col < newCols; col++) {
const zoneIndex = row * newCols + col;
newZones.push({
id: `zone${zoneIndex + 1}`,
name: `${zoneIndex + 1}`,
position: { row, column: col },
size: { width: "100%", height: "100%" },
});
}
}
onUpdateProperty(layoutComponent.id, "zones", newZones);
}
}}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
/>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"> </label>
<input
type="number"
min="1"
max="10"
value={layoutComponent.layoutConfig?.grid?.columns || 2}
onChange={(e) => {
const newCols = parseInt(e.target.value);
const newRows = layoutComponent.layoutConfig?.grid?.rows || 2;
// 그리드 설정 업데이트
onUpdateProperty(layoutComponent.id, "layoutConfig.grid.columns", newCols);
// 존 개수 자동 업데이트 (행 × 열)
const totalZones = newRows * newCols;
const currentZones = layoutComponent.zones || [];
if (totalZones !== currentZones.length) {
const newZones = [];
for (let row = 0; row < newRows; row++) {
for (let col = 0; col < newCols; col++) {
const zoneIndex = row * newCols + col;
newZones.push({
id: `zone${zoneIndex + 1}`,
name: `${zoneIndex + 1}`,
position: { row, column: col },
size: { width: "100%", height: "100%" },
});
}
}
onUpdateProperty(layoutComponent.id, "zones", newZones);
}
}}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
/>
</div>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"> (px)</label>
<input
type="number"
min="0"
max="50"
value={layoutComponent.layoutConfig?.grid?.gap || 16}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, "layoutConfig.grid.gap", parseInt(e.target.value))
}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
/>
</div>
</div>
)}
{/* 플렉스박스 레이아웃 설정 */}
{layoutComponent.layoutType === "flexbox" && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"></label>
<select
value={layoutComponent.layoutConfig?.flexbox?.direction || "row"}
onChange={(e) => {
const newDirection = e.target.value;
// console.log("🔄 플렉스박스 방향 변경:", newDirection);
// 방향 설정 업데이트
onUpdateProperty(layoutComponent.id, "layoutConfig.flexbox.direction", newDirection);
// 방향 변경 시 존 크기 자동 조정
const currentZones = layoutComponent.zones || [];
const zoneCount = currentZones.length;
if (zoneCount > 0) {
const updatedZones = currentZones.map((zone, index) => ({
...zone,
size: {
...zone.size,
width: newDirection === "row" ? `${100 / zoneCount}%` : "100%",
height: newDirection === "column" ? `${100 / zoneCount}%` : "auto",
},
}));
// console.log("🔄 존 크기 자동 조정:", {
// direction: newDirection,
// zoneCount,
// updatedZones: updatedZones.map((z) => ({ id: z.id, size: z.size })),
// });
onUpdateProperty(layoutComponent.id, "zones", updatedZones);
}
}}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
>
<option value="row"> (row)</option>
<option value="column"> (column)</option>
<option value="row-reverse"> </option>
<option value="column-reverse"> </option>
</select>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"> </label>
<div className="flex items-center space-x-2">
<input
type="number"
min="1"
max="10"
value={layoutComponent.zones?.length || 2}
onChange={(e) => {
const newZoneCount = parseInt(e.target.value);
const currentZones = layoutComponent.zones || [];
const direction = layoutComponent.layoutConfig?.flexbox?.direction || "row";
if (newZoneCount > currentZones.length) {
// 존 추가
const newZones = [...currentZones];
for (let i = currentZones.length; i < newZoneCount; i++) {
newZones.push({
id: `zone${i + 1}`,
name: `${i + 1}`,
position: {},
size: {
width: direction === "row" ? `${100 / newZoneCount}%` : "100%",
height: direction === "column" ? `${100 / newZoneCount}%` : "100%",
},
});
}
// 기존 존들의 크기도 조정
newZones.forEach((zone, index) => {
if (direction === "row") {
zone.size.width = `${100 / newZoneCount}%`;
} else {
zone.size.height = `${100 / newZoneCount}%`;
}
});
onUpdateProperty(layoutComponent.id, "zones", newZones);
} else if (newZoneCount < currentZones.length) {
// 존 제거
const newZones = currentZones.slice(0, newZoneCount);
// 남은 존들의 크기 재조정
newZones.forEach((zone, index) => {
if (direction === "row") {
zone.size.width = `${100 / newZoneCount}%`;
} else {
zone.size.height = `${100 / newZoneCount}%`;
}
});
onUpdateProperty(layoutComponent.id, "zones", newZones);
}
}}
className="w-20 rounded border border-gray-300 px-2 py-1 text-sm"
/>
<span className="text-xs text-gray-500"></span>
</div>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"> (px)</label>
<input
type="number"
min="0"
max="50"
value={layoutComponent.layoutConfig?.flexbox?.gap || 16}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, "layoutConfig.flexbox.gap", parseInt(e.target.value))
}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
/>
</div>
</div>
)}
{/* 분할 레이아웃 설정 */}
{layoutComponent.layoutType === "split" && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"> </label>
<select
value={layoutComponent.layoutConfig?.split?.direction || "horizontal"}
onChange={(e) => onUpdateProperty(layoutComponent.id, "layoutConfig.split.direction", e.target.value)}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
>
<option value="horizontal"> </option>
<option value="vertical"> </option>
</select>
</div>
</div>
)}
{/* 카드 레이아웃 설정 */}
{layoutComponent.layoutType === "card-layout" && (
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-900"> </h4>
{/* 테이블 컬럼 매핑 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h5 className="text-xs font-medium text-gray-700"> </h5>
{currentTable && (
<span className="bg-accent text-primary rounded px-2 py-1 text-xs">
: {currentTable.table_name}
</span>
)}
</div>
{/* 테이블이 선택되지 않은 경우 안내 */}
{!currentTable && (
<div className="rounded-lg bg-yellow-50 p-3 text-center">
<p className="text-sm text-yellow-700"> </p>
<p className="mt-1 text-xs text-yellow-600">
</p>
</div>
)}
{/* 테이블이 선택된 경우 컬럼 드롭다운 */}
{currentTable && (
<>
<div>
<label className="text-muted-foreground mb-1 block text-xs font-medium"> </label>
<select
value={layoutComponent.layoutConfig?.card?.columnMapping?.titleColumn || ""}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.titleColumn",
e.target.value,
)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
>
<option value=""> </option>
{currentTable.columns?.map((column) => (
<option key={column.columnName} value={column.columnName}>
{column.columnLabel || column.columnName} ({column.dataType})
</option>
))}
</select>
</div>
<div>
<label className="text-muted-foreground mb-1 block text-xs font-medium"> </label>
<select
value={layoutComponent.layoutConfig?.card?.columnMapping?.subtitleColumn || ""}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.subtitleColumn",
e.target.value,
)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
>
<option value=""> </option>
{currentTable.columns?.map((column) => (
<option key={column.columnName} value={column.columnName}>
{column.columnLabel || column.columnName} ({column.dataType})
</option>
))}
</select>
</div>
<div>
<label className="text-muted-foreground mb-1 block text-xs font-medium"> </label>
<select
value={layoutComponent.layoutConfig?.card?.columnMapping?.descriptionColumn || ""}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.descriptionColumn",
e.target.value,
)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
>
<option value=""> </option>
{currentTable.columns?.map((column) => (
<option key={column.columnName} value={column.columnName}>
{column.columnLabel || column.columnName} ({column.dataType})
</option>
))}
</select>
</div>
<div>
<label className="text-muted-foreground mb-1 block text-xs font-medium"> </label>
<select
value={layoutComponent.layoutConfig?.card?.columnMapping?.imageColumn || ""}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.imageColumn",
e.target.value,
)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
>
<option value=""> </option>
{currentTable.columns?.map((column) => (
<option key={column.columnName} value={column.columnName}>
{column.columnLabel || column.columnName} ({column.dataType})
</option>
))}
</select>
</div>
{/* 동적 표시 컬럼 추가 */}
<div>
<div className="mb-2 flex items-center justify-between">
<label className="text-muted-foreground text-xs font-medium"> </label>
<button
type="button"
onClick={() => {
const currentColumns =
layoutComponent.layoutConfig?.card?.columnMapping?.displayColumns || [];
const newColumns = [...currentColumns, ""];
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.displayColumns",
newColumns,
);
}}
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded px-2 py-1 text-xs"
>
+
</button>
</div>
<div className="space-y-2">
{(layoutComponent.layoutConfig?.card?.columnMapping?.displayColumns || []).map(
(column, index) => (
<div key={index} className="flex items-center space-x-2">
<select
value={column}
onChange={(e) => {
const currentColumns = [
...(layoutComponent.layoutConfig?.card?.columnMapping?.displayColumns || []),
];
currentColumns[index] = e.target.value;
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.displayColumns",
currentColumns,
);
}}
className="flex-1 rounded border border-gray-300 px-2 py-1 text-sm"
>
<option value=""> </option>
{currentTable.columns?.map((col) => (
<option key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName} ({col.dataType})
</option>
))}
</select>
<button
type="button"
onClick={() => {
const currentColumns = [
...(layoutComponent.layoutConfig?.card?.columnMapping?.displayColumns || []),
];
currentColumns.splice(index, 1);
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.displayColumns",
currentColumns,
);
}}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded px-2 py-1 text-xs"
>
</button>
</div>
),
)}
{(!layoutComponent.layoutConfig?.card?.columnMapping?.displayColumns ||
layoutComponent.layoutConfig.card.columnMapping.displayColumns.length === 0) && (
<div className="rounded border border-dashed border-gray-300 py-2 text-center text-xs text-gray-500">
"컬럼 추가"
</div>
)}
</div>
</div>
</>
)}
</div>
{/* 카드 스타일 설정 */}
<div className="space-y-3">
<h5 className="text-xs font-medium text-gray-700"> </h5>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-muted-foreground mb-1 block text-xs font-medium"> </label>
<input
type="number"
min="1"
max="6"
value={layoutComponent.layoutConfig?.card?.cardsPerRow || 3}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardsPerRow", parseInt(e.target.value))
}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
/>
</div>
<div>
<label className="text-muted-foreground mb-1 block text-xs font-medium"> (px)</label>
<input
type="number"
min="0"
max="50"
value={layoutComponent.layoutConfig?.card?.cardSpacing || 16}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardSpacing", parseInt(e.target.value))
}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="showTitle"
checked={layoutComponent.layoutConfig?.card?.cardStyle?.showTitle ?? true}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardStyle.showTitle", e.target.checked)
}
className="rounded border-gray-300"
/>
<label htmlFor="showTitle" className="text-muted-foreground text-xs">
</label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="showSubtitle"
checked={layoutComponent.layoutConfig?.card?.cardStyle?.showSubtitle ?? true}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.cardStyle.showSubtitle",
e.target.checked,
)
}
className="rounded border-gray-300"
/>
<label htmlFor="showSubtitle" className="text-muted-foreground text-xs">
</label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="showDescription"
checked={layoutComponent.layoutConfig?.card?.cardStyle?.showDescription ?? true}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.cardStyle.showDescription",
e.target.checked,
)
}
className="rounded border-gray-300"
/>
<label htmlFor="showDescription" className="text-muted-foreground text-xs">
</label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="showImage"
checked={layoutComponent.layoutConfig?.card?.cardStyle?.showImage ?? false}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardStyle.showImage", e.target.checked)
}
className="rounded border-gray-300"
/>
<label htmlFor="showImage" className="text-muted-foreground text-xs">
</label>
</div>
</div>
<div>
<label className="text-muted-foreground mb-1 block text-xs font-medium"> </label>
<input
type="number"
min="10"
max="500"
value={layoutComponent.layoutConfig?.card?.cardStyle?.maxDescriptionLength || 100}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.cardStyle.maxDescriptionLength",
parseInt(e.target.value),
)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
/>
</div>
</div>
</div>
)}
{/* 존 목록 - 카드 레이아웃은 데이터 기반이므로 존 관리 불필요 */}
{layoutComponent.layoutType !== "card-layout" && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<div className="space-y-2">
{layoutComponent.zones?.map((zone, index) => (
<div key={zone.id} className="rounded-lg bg-gray-50 p-3">
<div className="mb-2 flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">{zone.name}</span>
<span className="text-xs text-gray-500">ID: {zone.id}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-muted-foreground mb-1 block text-xs"></label>
<input
type="text"
value={zone.size?.width || "100%"}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, `zones.${index}.size.width`, e.target.value)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
placeholder="100%"
/>
</div>
<div>
<label className="text-muted-foreground mb-1 block text-xs"></label>
<input
type="text"
value={zone.size?.height || "auto"}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, `zones.${index}.size.height`, e.target.value)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
placeholder="auto"
/>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
};
// 웹타입별 상세 설정 렌더링 함수 - useCallback 제거하여 항상 최신 widget 사용
const renderWebTypeConfig = (widget: WidgetComponent) => {
const currentConfig = widget.webTypeConfig || {};
// console.log("🎨 DetailSettingsPanel renderWebTypeConfig 호출:", {
// componentId: widget.id,
// widgetType: widget.widgetType,
// currentConfig,
// configExists: !!currentConfig,
// configKeys: Object.keys(currentConfig),
// configStringified: JSON.stringify(currentConfig),
// widgetWebTypeConfig: widget.webTypeConfig,
// widgetWebTypeConfigExists: !!widget.webTypeConfig,
// timestamp: new Date().toISOString(),
// });
// console.log("🎨 selectedComponent 전체:", selectedComponent);
const handleConfigChange = (newConfig: WebTypeConfig) => {
// console.log("🔧 WebTypeConfig 업데이트:", {
// widgetType: widget.widgetType,
// oldConfig: currentConfig,
// newConfig,
// componentId: widget.id,
// isEqual: JSON.stringify(currentConfig) === JSON.stringify(newConfig),
// });
// 강제 새 객체 생성으로 React 변경 감지 보장
const freshConfig = { ...newConfig };
onUpdateProperty(widget.id, "webTypeConfig", freshConfig);
};
// 1순위: DB에서 지정된 설정 패널 사용
const dbWebType = webTypes.find((wt) => wt.web_type === widget.widgetType);
// console.log(`🎨 웹타입 "${widget.widgetType}" DB 조회 결과:`, dbWebType);
if (dbWebType?.config_panel) {
// console.log(`🎨 웹타입 "${widget.widgetType}" → DB 지정 설정 패널 "${dbWebType.config_panel}" 사용`);
const ConfigPanelComponent = getConfigPanelComponent(dbWebType.config_panel);
// console.log(`🎨 getConfigPanelComponent 결과:`, ConfigPanelComponent);
if (ConfigPanelComponent) {
// console.log(`🎨 ✅ ConfigPanelComponent 렌더링 시작`);
return <ConfigPanelComponent config={currentConfig} onConfigChange={handleConfigChange} />;
} else {
// console.log(`🎨 ❌ ConfigPanelComponent가 null - WebTypeConfigPanel 사용`);
return (
<WebTypeConfigPanel
webType={widget.widgetType as any}
config={currentConfig}
onUpdateConfig={handleConfigChange}
/>
);
}
} else {
// console.log(`🎨 config_panel이 없음 - WebTypeConfigPanel 사용`);
return (
<WebTypeConfigPanel
webType={widget.widgetType as any}
config={currentConfig}
onUpdateConfig={handleConfigChange}
/>
);
}
};
if (!selectedComponent) {
return (
<div className="flex h-full flex-col border-r border-gray-200/60 bg-gradient-to-br from-slate-50 to-orange-50/30 shadow-sm">
<div className="p-6">
{/* 헤더 */}
<div className="mb-6">
<h2 className="mb-1 text-lg font-semibold text-gray-900"> </h2>
<p className="text-sm text-gray-500"> </p>
</div>
</div>
{/* 빈 상태 */}
<div className="flex flex-1 flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-gray-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900"> </h3>
<p className="text-sm text-gray-500"> .</p>
</div>
</div>
);
}
// 컴포넌트 타입별 설정 패널 렌더링
const renderComponentConfigPanel = () => {
// console.log("🔍 renderComponentConfigPanel - selectedComponent:", selectedComponent);
if (!selectedComponent) {
// console.error("❌ selectedComponent가 undefined입니다!");
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-red-400" />
<h3 className="mb-2 text-lg font-medium text-red-900"></h3>
<p className="text-sm text-red-500"> .</p>
</div>
);
}
const componentType = selectedComponent.componentConfig?.type || selectedComponent.type;
const handleUpdateProperty = (path: string, value: any) => {
onUpdateProperty(selectedComponent.id, path, value);
};
switch (componentType) {
case "button":
case "button-primary":
case "button-secondary":
// 🔧 component.id만 key로 사용 (unmount 방지)
return <NewButtonConfigPanel key={selectedComponent.id} component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "card":
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "dashboard":
return <DashboardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "stats":
case "stats-card":
return <StatsCardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "progress":
case "progress-bar":
return <ProgressBarConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "chart":
case "chart-basic":
return <ChartConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "alert":
case "alert-info":
return <AlertConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "badge":
case "badge-status":
return <BadgeConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
default:
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-gray-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900"> </h3>
<p className="text-sm text-gray-500"> "{componentType}" .</p>
</div>
);
}
};
// 새로운 컴포넌트 타입들에 대한 설정 패널 확인
const componentType = selectedComponent?.componentConfig?.type || selectedComponent?.type;
// console.log("🔍 DetailSettingsPanel componentType 확인:", {
// selectedComponentType: selectedComponent?.type,
// componentConfigType: selectedComponent?.componentConfig?.type,
// finalComponentType: componentType,
// });
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);
// console.log("🔍 hasNewConfigPanel:", hasNewConfigPanel);
if (hasNewConfigPanel) {
return (
<div className="flex h-full flex-col">
{/* 헤더 */}
<div className="border-b border-gray-200 p-4">
<div className="flex items-center space-x-2">
<Settings className="text-muted-foreground h-4 w-4" />
<h3 className="font-medium text-gray-900"> </h3>
</div>
<div className="mt-2 flex items-center space-x-2">
<span className="text-muted-foreground text-sm">:</span>
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentType}</span>
</div>
</div>
{/* 설정 패널 영역 */}
<div className="flex-1 overflow-y-auto p-4">{renderComponentConfigPanel()}</div>
</div>
);
}
// 레이아웃 컴포넌트 처리
if (selectedComponent.type === "layout") {
return renderLayoutConfig(selectedComponent as LayoutComponent);
}
if (
selectedComponent.type !== "widget" &&
selectedComponent.type !== "file" &&
selectedComponent.type !== "button" &&
selectedComponent.type !== "component"
) {
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-gray-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900"> </h3>
<p className="text-sm text-gray-500">
, , , , .
<br />
: {selectedComponent.type}
</p>
</div>
);
}
// 파일 컴포넌트인 경우 FileComponentConfigPanel 렌더링
if (isFileComponent(selectedComponent)) {
const fileComponent = selectedComponent as FileComponent;
return (
<div className="flex h-full flex-col">
{/* 헤더 */}
<div className="border-b border-gray-200 p-4">
<div className="flex items-center space-x-2">
<Settings className="text-muted-foreground h-4 w-4" />
<h3 className="font-medium text-gray-900"> </h3>
</div>
<div className="mt-2 flex items-center space-x-2">
<span className="text-muted-foreground text-sm">:</span>
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800"> </span>
</div>
<div className="mt-1 text-xs text-gray-500">
{selectedComponent.type === "widget"
? `위젯타입: ${selectedComponent.widgetType}`
: `문서 타입: ${fileComponent.fileConfig?.docTypeName || "일반 문서"}`}
</div>
</div>
{/* 파일 컴포넌트 설정 영역 */}
<div className="flex-1 overflow-y-auto p-4">
<FileComponentConfigPanel
component={fileComponent}
onUpdateProperty={onUpdateProperty}
currentTable={currentTable}
currentTableName={currentTableName}
/>
</div>
</div>
);
}
// 레거시 버튼을 새로운 컴포넌트 시스템으로 강제 변환
if (selectedComponent.type === "button") {
// console.log("🔄 레거시 버튼을 새로운 컴포넌트 시스템으로 변환:", selectedComponent);
// 레거시 버튼을 새로운 시스템으로 변환
const convertedComponent = {
...selectedComponent,
type: "component" as const,
componentConfig: {
type: "button-primary",
webType: "button",
...selectedComponent.componentConfig,
},
};
// 변환된 컴포넌트로 DB 업데이트
onUpdateProperty(selectedComponent.id, "type", "component");
onUpdateProperty(selectedComponent.id, "componentConfig", convertedComponent.componentConfig);
// 변환된 컴포넌트로 처리 계속
selectedComponent = convertedComponent;
}
// 새로운 컴포넌트 시스템 처리 (type: "component")
if (selectedComponent.type === "component") {
const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type;
const webType = selectedComponent.componentConfig?.webType;
// console.log("🔧 새로운 컴포넌트 시스템 설정 패널:", { componentId, webType });
if (!componentId) {
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-gray-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900"> ID가 </h3>
<p className="text-sm text-gray-500">componentConfig.type이 .</p>
</div>
);
}
// 현재 웹타입의 기본 입력 타입 추출
const currentBaseInputType = webType ? getBaseInputType(webType as any) : null;
// 선택 가능한 세부 타입 목록
const availableDetailTypes = currentBaseInputType ? getDetailTypes(currentBaseInputType) : [];
// 세부 타입 변경 핸들러
const handleDetailTypeChange = (newDetailType: string) => {
setLocalComponentDetailType(newDetailType);
onUpdateProperty(selectedComponent.id, "componentConfig.webType", newDetailType);
};
return (
<div className="flex h-full flex-col border-r border-gray-200/60 bg-gradient-to-br from-slate-50 to-orange-50/30 shadow-sm">
<div className="p-6">
{/* 헤더 */}
<div className="mb-6">
<h2 className="mb-1 text-lg font-semibold text-gray-900"> </h2>
<p className="text-sm text-gray-500"> </p>
</div>
{/* 컴포넌트 정보 */}
<div className="mb-4 space-y-2">
<div className="flex items-center space-x-2">
<span className="text-muted-foreground text-sm">:</span>
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentId}</span>
</div>
{webType && currentBaseInputType && (
<div className="flex items-center space-x-2">
<span className="text-muted-foreground text-sm"> :</span>
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
{currentBaseInputType}
</span>
</div>
)}
{selectedComponent.columnName && (
<div className="flex items-center space-x-2">
<span className="text-muted-foreground text-sm">:</span>
<span className="text-xs text-gray-700">{selectedComponent.columnName}</span>
</div>
)}
</div>
</div>
{/* 컴포넌트 설정 패널 */}
<div className="flex-1 overflow-y-auto px-6 pb-6">
<div className="space-y-6">
{/* DynamicComponentConfigPanel */}
<DynamicComponentConfigPanel
componentId={componentId}
config={(() => {
const config = selectedComponent.componentConfig || {};
// console.log("🔍 DetailSettingsPanel에서 전달하는 config:", config);
// console.log("🔍 selectedComponent 전체:", selectedComponent);
return config;
})()}
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
tableColumns={(() => {
// console.log("🔍 DetailSettingsPanel tableColumns 전달:", {
// currentTable,
// columns: currentTable?.columns,
// columnsLength: currentTable?.columns?.length,
// sampleColumn: currentTable?.columns?.[0],
// deptCodeColumn: currentTable?.columns?.find((col) => col.columnName === "dept_code"),
// });
return currentTable?.columns || [];
})()}
tables={tables} // 전체 테이블 목록 전달
onChange={(newConfig) => {
// console.log("🔧 컴포넌트 설정 변경:", newConfig);
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
Object.entries(newConfig).forEach(([key, value]) => {
onUpdateProperty(selectedComponent.id, `componentConfig.${key}`, value);
});
}}
/>
</div>
</div>
</div>
);
}
// 기존 위젯 시스템 처리 (type: "widget")
const widget = selectedComponent as WidgetComponent;
// 현재 웹타입의 기본 입력 타입 추출
const currentBaseInputType = getBaseInputType(widget.widgetType);
// 선택 가능한 세부 타입 목록
const availableDetailTypes = getDetailTypes(currentBaseInputType);
// 로컬 상태: 세부 타입 선택
const [localDetailType, setLocalDetailType] = useState(widget.widgetType);
// 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => {
setLocalDetailType(widget.widgetType);
}, [widget.widgetType, widget.id]);
// 세부 타입 변경 핸들러
const handleDetailTypeChange = (newDetailType: string) => {
setLocalDetailType(newDetailType);
onUpdateProperty(widget.id, "widgetType", newDetailType);
// 웹타입 변경 시 기존 설정 초기화 (선택적)
// onUpdateProperty(widget.id, "webTypeConfig", {});
};
return (
<div className="flex h-full flex-col">
{/* 헤더 */}
<div className="border-b border-gray-200 p-4">
<div className="flex items-center space-x-2">
<Settings className="text-muted-foreground h-4 w-4" />
<h3 className="font-medium text-gray-900"> </h3>
</div>
<div className="mt-2 flex items-center space-x-2">
<span className="text-muted-foreground text-sm"> :</span>
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
{currentBaseInputType}
</span>
</div>
<div className="mt-1 text-xs text-gray-500">: {widget.columnName}</div>
</div>
{/* 세부 타입 선택 영역 */}
<div className="border-b border-gray-200 bg-gray-50 p-4">
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> </label>
<Select value={localDetailType} onValueChange={handleDetailTypeChange}>
<SelectTrigger className="w-full bg-white">
<SelectValue placeholder="세부 타입을 선택하세요" />
</SelectTrigger>
<SelectContent>
{availableDetailTypes.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex flex-col">
<span className="font-medium">{option.label}</span>
<span className="text-xs text-gray-500">{option.description}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-gray-500">
"{currentBaseInputType}"
</p>
</div>
</div>
{/* 상세 설정 영역 */}
<div className="flex-1 overflow-y-auto p-4">{renderWebTypeConfig(widget)}</div>
</div>
);
};
export default DetailSettingsPanel;