2025-09-03 11:32:09 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
2025-10-14 11:48:04 +09:00
|
|
|
|
import React, { useState, useEffect } from "react";
|
2025-09-03 11:32:09 +09:00
|
|
|
|
import { Settings } from "lucide-react";
|
2025-09-04 14:23:35 +09:00
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
2025-09-09 14:29:04 +09:00
|
|
|
|
import { useWebTypes } from "@/hooks/admin/useWebTypes";
|
2025-09-09 15:42:04 +09:00
|
|
|
|
import { getConfigPanelComponent } from "@/lib/utils/getConfigPanelComponent";
|
2025-09-11 12:22:39 +09:00
|
|
|
|
import {
|
|
|
|
|
|
ComponentData,
|
|
|
|
|
|
WidgetComponent,
|
|
|
|
|
|
FileComponent,
|
|
|
|
|
|
WebTypeConfig,
|
|
|
|
|
|
TableInfo,
|
|
|
|
|
|
LayoutComponent,
|
|
|
|
|
|
} from "@/types/screen";
|
2025-09-12 14:24:25 +09:00
|
|
|
|
// 레거시 ButtonConfigPanel 제거됨
|
2025-09-05 21:52:19 +09:00
|
|
|
|
import { FileComponentConfigPanel } from "./FileComponentConfigPanel";
|
2025-10-14 16:45:30 +09:00
|
|
|
|
import { WebTypeConfigPanel } from "./WebTypeConfigPanel";
|
2025-09-29 13:29:03 +09:00
|
|
|
|
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
|
2025-10-14 11:48:04 +09:00
|
|
|
|
import { BaseInputType, getBaseInputType, getDetailTypes, DetailTypeOption } from "@/types/input-type-mapping";
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
2025-09-10 14:09:32 +09:00
|
|
|
|
// 새로운 컴포넌트 설정 패널들 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";
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
// 동적 컴포넌트 설정 패널
|
|
|
|
|
|
import { DynamicComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
|
|
|
|
|
|
|
2025-09-03 11:32:09 +09:00
|
|
|
|
interface DetailSettingsPanelProps {
|
|
|
|
|
|
selectedComponent?: ComponentData;
|
|
|
|
|
|
onUpdateProperty: (componentId: string, path: string, value: any) => void;
|
2025-09-06 00:16:27 +09:00
|
|
|
|
currentTable?: TableInfo; // 현재 화면의 테이블 정보
|
|
|
|
|
|
currentTableName?: string; // 현재 화면의 테이블명
|
2025-10-16 15:05:24 +09:00
|
|
|
|
tables?: TableInfo[]; // 전체 테이블 목록
|
2025-09-03 11:32:09 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-06 00:16:27 +09:00
|
|
|
|
export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
|
|
|
|
|
selectedComponent,
|
|
|
|
|
|
onUpdateProperty,
|
|
|
|
|
|
currentTable,
|
|
|
|
|
|
currentTableName,
|
2025-10-16 15:05:24 +09:00
|
|
|
|
tables = [], // 기본값 빈 배열
|
2025-09-06 00:16:27 +09:00
|
|
|
|
}) => {
|
2025-09-09 14:29:04 +09:00
|
|
|
|
// 데이터베이스에서 입력 가능한 웹타입들을 동적으로 가져오기
|
|
|
|
|
|
const { webTypes } = useWebTypes({ active: "Y" });
|
2025-09-04 15:20:26 +09:00
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(`🔍 DetailSettingsPanel props:`, {
|
2025-10-14 11:48:04 +09:00
|
|
|
|
// selectedComponent: selectedComponent?.id,
|
|
|
|
|
|
// componentType: selectedComponent?.type,
|
|
|
|
|
|
// currentTableName,
|
|
|
|
|
|
// currentTable: currentTable?.tableName,
|
|
|
|
|
|
// selectedComponentTableName: selectedComponent?.tableName,
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// });
|
|
|
|
|
|
// console.log(`🔍 DetailSettingsPanel webTypes 로드됨:`, webTypes?.length, "개");
|
|
|
|
|
|
// console.log(`🔍 webTypes:`, webTypes);
|
|
|
|
|
|
// console.log(`🔍 DetailSettingsPanel selectedComponent:`, selectedComponent);
|
|
|
|
|
|
// console.log(`🔍 DetailSettingsPanel selectedComponent.widgetType:`, selectedComponent?.widgetType);
|
2025-09-09 15:42:04 +09:00
|
|
|
|
const inputableWebTypes = webTypes.map((wt) => wt.web_type);
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
2025-10-14 11:48:04 +09:00
|
|
|
|
// 새로운 컴포넌트 시스템용 로컬 상태
|
|
|
|
|
|
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]);
|
|
|
|
|
|
|
2025-09-11 12:22:39 +09:00
|
|
|
|
// 레이아웃 컴포넌트 설정 렌더링 함수
|
|
|
|
|
|
const renderLayoutConfig = (layoutComponent: LayoutComponent) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-full flex-col">
|
|
|
|
|
|
{/* 헤더 */}
|
2025-10-17 16:21:08 +09:00
|
|
|
|
<div className="border-b p-4">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<Settings className="text-muted-foreground h-4 w-4" />
|
2025-10-17 16:21:08 +09:00
|
|
|
|
<h3 className="text-sm font-semibold">레이아웃 설정</h3>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
</div>
|
2025-10-17 16:21:08 +09:00
|
|
|
|
<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">
|
2025-09-11 12:22:39 +09:00
|
|
|
|
{layoutComponent.layoutType}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2025-10-17 16:21:08 +09:00
|
|
|
|
<div className="text-muted-foreground mt-1 text-xs">ID: {layoutComponent.id}</div>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 레이아웃 설정 영역 */}
|
2025-10-17 16:21:08 +09:00
|
|
|
|
<div className="flex-1 space-y-3 overflow-y-auto p-4">
|
2025-09-11 12:22:39 +09:00
|
|
|
|
{/* 기본 정보 */}
|
2025-10-17 16:21:08 +09:00
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
|
<label className="text-xs font-medium">레이아웃 이름</label>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={layoutComponent.label || ""}
|
|
|
|
|
|
onChange={(e) => onUpdateProperty(layoutComponent.id, "label", e.target.value)}
|
2025-10-17 16:21:08 +09:00
|
|
|
|
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"
|
2025-09-11 12:22:39 +09:00
|
|
|
|
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;
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 플렉스박스 방향 변경:", newDirection);
|
2025-09-11 12:22:39 +09:00
|
|
|
|
|
|
|
|
|
|
// 방향 설정 업데이트
|
|
|
|
|
|
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",
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 존 크기 자동 조정:", {
|
2025-10-14 11:48:04 +09:00
|
|
|
|
// direction: newDirection,
|
|
|
|
|
|
// zoneCount,
|
|
|
|
|
|
// updatedZones: updatedZones.map((z) => ({ id: z.id, size: z.size })),
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// });
|
2025-09-11 12:22:39 +09:00
|
|
|
|
|
|
|
|
|
|
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 && (
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<span className="bg-accent text-primary rounded px-2 py-1 text-xs">
|
2025-09-11 12:22:39 +09:00
|
|
|
|
테이블: {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>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label className="text-muted-foreground mb-1 block text-xs font-medium">타이틀 컬럼</label>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
<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>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label className="text-muted-foreground mb-1 block text-xs font-medium">서브타이틀 컬럼</label>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
<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>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label className="text-muted-foreground mb-1 block text-xs font-medium">설명 컬럼</label>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
<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>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label className="text-muted-foreground mb-1 block text-xs font-medium">이미지 컬럼</label>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
<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">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label className="text-muted-foreground text-xs font-medium">표시 컬럼들</label>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const currentColumns =
|
|
|
|
|
|
layoutComponent.layoutConfig?.card?.columnMapping?.displayColumns || [];
|
|
|
|
|
|
const newColumns = [...currentColumns, ""];
|
|
|
|
|
|
onUpdateProperty(
|
|
|
|
|
|
layoutComponent.id,
|
|
|
|
|
|
"layoutConfig.card.columnMapping.displayColumns",
|
|
|
|
|
|
newColumns,
|
|
|
|
|
|
);
|
|
|
|
|
|
}}
|
2025-10-14 11:48:04 +09:00
|
|
|
|
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded px-2 py-1 text-xs"
|
2025-09-11 12:22:39 +09:00
|
|
|
|
>
|
|
|
|
|
|
+ 컬럼 추가
|
|
|
|
|
|
</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,
|
|
|
|
|
|
);
|
|
|
|
|
|
}}
|
2025-10-14 11:48:04 +09:00
|
|
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded px-2 py-1 text-xs"
|
2025-09-11 12:22:39 +09:00
|
|
|
|
>
|
|
|
|
|
|
삭제
|
|
|
|
|
|
</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>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label className="text-muted-foreground mb-1 block text-xs font-medium">한 행당 카드 수</label>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
<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>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label className="text-muted-foreground mb-1 block text-xs font-medium">카드 간격 (px)</label>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
<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"
|
|
|
|
|
|
/>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label htmlFor="showTitle" className="text-muted-foreground text-xs">
|
2025-09-11 12:22:39 +09:00
|
|
|
|
타이틀 표시
|
|
|
|
|
|
</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"
|
|
|
|
|
|
/>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label htmlFor="showSubtitle" className="text-muted-foreground text-xs">
|
2025-09-11 12:22:39 +09:00
|
|
|
|
서브타이틀 표시
|
|
|
|
|
|
</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"
|
|
|
|
|
|
/>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label htmlFor="showDescription" className="text-muted-foreground text-xs">
|
2025-09-11 12:22:39 +09:00
|
|
|
|
설명 표시
|
|
|
|
|
|
</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"
|
|
|
|
|
|
/>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label htmlFor="showImage" className="text-muted-foreground text-xs">
|
2025-09-11 12:22:39 +09:00
|
|
|
|
이미지 표시
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label className="text-muted-foreground mb-1 block text-xs font-medium">설명 최대 길이</label>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
<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>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label className="text-muted-foreground mb-1 block text-xs">너비</label>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
<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>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<label className="text-muted-foreground mb-1 block text-xs">높이</label>
|
2025-09-11 12:22:39 +09:00
|
|
|
|
<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>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-09 15:42:04 +09:00
|
|
|
|
// 웹타입별 상세 설정 렌더링 함수 - useCallback 제거하여 항상 최신 widget 사용
|
|
|
|
|
|
const renderWebTypeConfig = (widget: WidgetComponent) => {
|
|
|
|
|
|
const currentConfig = widget.webTypeConfig || {};
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🎨 DetailSettingsPanel renderWebTypeConfig 호출:", {
|
2025-10-14 11:48:04 +09:00
|
|
|
|
// 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(),
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// });
|
|
|
|
|
|
// console.log("🎨 selectedComponent 전체:", selectedComponent);
|
2025-09-09 15:42:04 +09:00
|
|
|
|
|
|
|
|
|
|
const handleConfigChange = (newConfig: WebTypeConfig) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔧 WebTypeConfig 업데이트:", {
|
2025-10-14 11:48:04 +09:00
|
|
|
|
// widgetType: widget.widgetType,
|
|
|
|
|
|
// oldConfig: currentConfig,
|
|
|
|
|
|
// newConfig,
|
|
|
|
|
|
// componentId: widget.id,
|
|
|
|
|
|
// isEqual: JSON.stringify(currentConfig) === JSON.stringify(newConfig),
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// });
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
2025-09-09 15:42:04 +09:00
|
|
|
|
// 강제 새 객체 생성으로 React 변경 감지 보장
|
|
|
|
|
|
const freshConfig = { ...newConfig };
|
|
|
|
|
|
onUpdateProperty(widget.id, "webTypeConfig", freshConfig);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 1순위: DB에서 지정된 설정 패널 사용
|
|
|
|
|
|
const dbWebType = webTypes.find((wt) => wt.web_type === widget.widgetType);
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(`🎨 웹타입 "${widget.widgetType}" DB 조회 결과:`, dbWebType);
|
2025-09-09 15:42:04 +09:00
|
|
|
|
|
|
|
|
|
|
if (dbWebType?.config_panel) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(`🎨 웹타입 "${widget.widgetType}" → DB 지정 설정 패널 "${dbWebType.config_panel}" 사용`);
|
2025-09-09 15:42:04 +09:00
|
|
|
|
const ConfigPanelComponent = getConfigPanelComponent(dbWebType.config_panel);
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(`🎨 getConfigPanelComponent 결과:`, ConfigPanelComponent);
|
2025-09-09 15:42:04 +09:00
|
|
|
|
|
|
|
|
|
|
if (ConfigPanelComponent) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(`🎨 ✅ ConfigPanelComponent 렌더링 시작`);
|
2025-09-09 15:42:04 +09:00
|
|
|
|
return <ConfigPanelComponent config={currentConfig} onConfigChange={handleConfigChange} />;
|
|
|
|
|
|
} else {
|
2025-10-14 16:45:30 +09:00
|
|
|
|
// console.log(`🎨 ❌ ConfigPanelComponent가 null - WebTypeConfigPanel 사용`);
|
2025-09-09 15:42:04 +09:00
|
|
|
|
return (
|
2025-10-14 16:45:30 +09:00
|
|
|
|
<WebTypeConfigPanel
|
|
|
|
|
|
webType={widget.widgetType as any}
|
|
|
|
|
|
config={currentConfig}
|
|
|
|
|
|
onUpdateConfig={handleConfigChange}
|
|
|
|
|
|
/>
|
2025-09-09 15:42:04 +09:00
|
|
|
|
);
|
2025-09-03 11:32:09 +09:00
|
|
|
|
}
|
2025-09-09 15:42:04 +09:00
|
|
|
|
} else {
|
2025-10-14 16:45:30 +09:00
|
|
|
|
// console.log(`🎨 config_panel이 없음 - WebTypeConfigPanel 사용`);
|
2025-09-09 15:42:04 +09:00
|
|
|
|
return (
|
2025-10-14 16:45:30 +09:00
|
|
|
|
<WebTypeConfigPanel
|
|
|
|
|
|
webType={widget.widgetType as any}
|
|
|
|
|
|
config={currentConfig}
|
|
|
|
|
|
onUpdateConfig={handleConfigChange}
|
|
|
|
|
|
/>
|
2025-09-09 15:42:04 +09:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
|
|
|
|
|
if (!selectedComponent) {
|
|
|
|
|
|
return (
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<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">
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<div className="p-6">
|
|
|
|
|
|
{/* 헤더 */}
|
|
|
|
|
|
<div className="mb-6">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<h2 className="mb-1 text-lg font-semibold text-gray-900">상세 설정</h2>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<p className="text-sm text-gray-500">컴포넌트를 선택하여 상세 설정을 편집하세요</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
|
{/* 빈 상태 */}
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<div className="flex flex-1 flex-col items-center justify-center p-6 text-center">
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<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>
|
2025-09-03 11:32:09 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 14:09:32 +09:00
|
|
|
|
// 컴포넌트 타입별 설정 패널 렌더링
|
|
|
|
|
|
const renderComponentConfigPanel = () => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔍 renderComponentConfigPanel - selectedComponent:", selectedComponent);
|
2025-09-10 14:09:32 +09:00
|
|
|
|
|
|
|
|
|
|
if (!selectedComponent) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("❌ selectedComponent가 undefined입니다!");
|
2025-09-10 14:09:32 +09:00
|
|
|
|
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":
|
|
|
|
|
|
return <NewButtonConfigPanel 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;
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔍 DetailSettingsPanel componentType 확인:", {
|
2025-10-14 11:48:04 +09:00
|
|
|
|
// selectedComponentType: selectedComponent?.type,
|
|
|
|
|
|
// componentConfigType: selectedComponent?.componentConfig?.type,
|
|
|
|
|
|
// finalComponentType: componentType,
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// });
|
2025-09-10 14:09:32 +09:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔍 hasNewConfigPanel:", hasNewConfigPanel);
|
2025-09-10 14:09:32 +09:00
|
|
|
|
|
|
|
|
|
|
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">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<Settings className="text-muted-foreground h-4 w-4" />
|
2025-09-10 14:09:32 +09:00
|
|
|
|
<h3 className="font-medium text-gray-900">컴포넌트 설정</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-2 flex items-center space-x-2">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<span className="text-muted-foreground text-sm">타입:</span>
|
2025-09-10 14:09:32 +09:00
|
|
|
|
<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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-11 12:22:39 +09:00
|
|
|
|
// 레이아웃 컴포넌트 처리
|
|
|
|
|
|
if (selectedComponent.type === "layout") {
|
|
|
|
|
|
return renderLayoutConfig(selectedComponent as LayoutComponent);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
if (
|
|
|
|
|
|
selectedComponent.type !== "widget" &&
|
|
|
|
|
|
selectedComponent.type !== "file" &&
|
|
|
|
|
|
selectedComponent.type !== "button" &&
|
|
|
|
|
|
selectedComponent.type !== "component"
|
|
|
|
|
|
) {
|
2025-09-03 11:32:09 +09:00
|
|
|
|
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" />
|
2025-09-05 21:52:19 +09:00
|
|
|
|
<h3 className="mb-2 text-lg font-medium text-gray-900">설정할 수 없는 컴포넌트입니다</h3>
|
2025-09-03 11:32:09 +09:00
|
|
|
|
<p className="text-sm text-gray-500">
|
2025-09-12 14:24:25 +09:00
|
|
|
|
상세 설정은 위젯, 파일, 버튼, 컴포넌트, 레이아웃에서만 사용할 수 있습니다.
|
2025-09-03 11:32:09 +09:00
|
|
|
|
<br />
|
|
|
|
|
|
현재 선택된 컴포넌트: {selectedComponent.type}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-05 21:52:19 +09:00
|
|
|
|
// 파일 컴포넌트인 경우 FileComponentConfigPanel 렌더링
|
2025-09-29 13:29:03 +09:00
|
|
|
|
if (isFileComponent(selectedComponent)) {
|
2025-09-05 21:52:19 +09:00
|
|
|
|
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">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<Settings className="text-muted-foreground h-4 w-4" />
|
2025-09-05 21:52:19 +09:00
|
|
|
|
<h3 className="font-medium text-gray-900">파일 컴포넌트 설정</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-2 flex items-center space-x-2">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<span className="text-muted-foreground text-sm">타입:</span>
|
2025-09-05 21:52:19 +09:00
|
|
|
|
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800">파일 업로드</span>
|
|
|
|
|
|
</div>
|
2025-09-26 13:11:34 +09:00
|
|
|
|
<div className="mt-1 text-xs text-gray-500">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
{selectedComponent.type === "widget"
|
|
|
|
|
|
? `위젯타입: ${selectedComponent.widgetType}`
|
|
|
|
|
|
: `문서 타입: ${fileComponent.fileConfig?.docTypeName || "일반 문서"}`}
|
2025-09-26 13:11:34 +09:00
|
|
|
|
</div>
|
2025-09-05 21:52:19 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 파일 컴포넌트 설정 영역 */}
|
|
|
|
|
|
<div className="flex-1 overflow-y-auto p-4">
|
2025-09-06 00:16:27 +09:00
|
|
|
|
<FileComponentConfigPanel
|
|
|
|
|
|
component={fileComponent}
|
|
|
|
|
|
onUpdateProperty={onUpdateProperty}
|
|
|
|
|
|
currentTable={currentTable}
|
|
|
|
|
|
currentTableName={currentTableName}
|
|
|
|
|
|
/>
|
2025-09-05 21:52:19 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
// 레거시 버튼을 새로운 컴포넌트 시스템으로 강제 변환
|
2025-09-09 17:42:23 +09:00
|
|
|
|
if (selectedComponent.type === "button") {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 레거시 버튼을 새로운 컴포넌트 시스템으로 변환:", selectedComponent);
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
|
|
// 레거시 버튼을 새로운 시스템으로 변환
|
|
|
|
|
|
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") {
|
2025-09-12 16:47:02 +09:00
|
|
|
|
const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type;
|
2025-09-12 14:24:25 +09:00
|
|
|
|
const webType = selectedComponent.componentConfig?.webType;
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔧 새로운 컴포넌트 시스템 설정 패널:", { componentId, webType });
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
2025-10-14 11:48:04 +09:00
|
|
|
|
// 현재 웹타입의 기본 입력 타입 추출
|
|
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-09 17:42:23 +09:00
|
|
|
|
return (
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<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">
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<div className="p-6">
|
|
|
|
|
|
{/* 헤더 */}
|
|
|
|
|
|
<div className="mb-6">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<h2 className="mb-1 text-lg font-semibold text-gray-900">상세 설정</h2>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<p className="text-sm text-gray-500">선택한 컴포넌트의 속성을 편집하세요</p>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
</div>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 컴포넌트 정보 */}
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<div className="mb-4 space-y-2">
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<div className="flex items-center space-x-2">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<span className="text-muted-foreground text-sm">컴포넌트:</span>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentId}</span>
|
2025-09-12 14:24:25 +09:00
|
|
|
|
</div>
|
2025-10-14 11:48:04 +09:00
|
|
|
|
{webType && currentBaseInputType && (
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<div className="flex items-center space-x-2">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<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>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{selectedComponent.columnName && (
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<span className="text-muted-foreground text-sm">컬럼:</span>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<span className="text-xs text-gray-700">{selectedComponent.columnName}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-14 11:48:04 +09:00
|
|
|
|
{/* 세부 타입 선택 영역 */}
|
|
|
|
|
|
{webType && availableDetailTypes.length > 1 && (
|
|
|
|
|
|
<div className="border-b border-gray-200 bg-gray-50 p-6 pt-0">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700">세부 타입 선택</label>
|
|
|
|
|
|
<Select value={localComponentDetailType || webType} 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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
{/* 컴포넌트 설정 패널 */}
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<div className="flex-1 overflow-y-auto px-6 pb-6">
|
2025-10-14 16:45:30 +09:00
|
|
|
|
<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 || [];
|
|
|
|
|
|
})()}
|
2025-10-16 15:05:24 +09:00
|
|
|
|
tables={tables} // 전체 테이블 목록 전달
|
2025-10-14 16:45:30 +09:00
|
|
|
|
onChange={(newConfig) => {
|
|
|
|
|
|
// console.log("🔧 컴포넌트 설정 변경:", newConfig);
|
|
|
|
|
|
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
|
|
|
|
|
Object.entries(newConfig).forEach(([key, value]) => {
|
|
|
|
|
|
onUpdateProperty(selectedComponent.id, `componentConfig.${key}`, value);
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 웹타입별 특화 설정 */}
|
|
|
|
|
|
{webType && (
|
|
|
|
|
|
<div className="border-t pt-6">
|
|
|
|
|
|
<h4 className="mb-4 text-sm font-semibold text-gray-900">웹타입 설정</h4>
|
|
|
|
|
|
<WebTypeConfigPanel
|
|
|
|
|
|
webType={webType as any}
|
|
|
|
|
|
config={selectedComponent.componentConfig || {}}
|
|
|
|
|
|
onUpdateConfig={(newConfig) => {
|
|
|
|
|
|
// 기존 설정과 병합하여 업데이트
|
|
|
|
|
|
Object.entries(newConfig).forEach(([key, value]) => {
|
|
|
|
|
|
onUpdateProperty(selectedComponent.id, `componentConfig.${key}`, value);
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
// 기존 위젯 시스템 처리 (type: "widget")
|
2025-09-03 11:32:09 +09:00
|
|
|
|
const widget = selectedComponent as WidgetComponent;
|
|
|
|
|
|
|
2025-10-14 11:48:04 +09:00
|
|
|
|
// 현재 웹타입의 기본 입력 타입 추출
|
|
|
|
|
|
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", {});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-03 11:32:09 +09:00
|
|
|
|
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">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<Settings className="text-muted-foreground h-4 w-4" />
|
2025-09-03 11:32:09 +09:00
|
|
|
|
<h3 className="font-medium text-gray-900">상세 설정</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-2 flex items-center space-x-2">
|
2025-10-14 11:48:04 +09:00
|
|
|
|
<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>
|
2025-09-03 11:32:09 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-1 text-xs text-gray-500">컬럼: {widget.columnName}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-14 11:48:04 +09:00
|
|
|
|
{/* 세부 타입 선택 영역 */}
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2025-09-03 11:32:09 +09:00
|
|
|
|
{/* 상세 설정 영역 */}
|
|
|
|
|
|
<div className="flex-1 overflow-y-auto p-4">{renderWebTypeConfig(widget)}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default DetailSettingsPanel;
|