웹 타입 설정패널 분리
This commit is contained in:
@@ -5115,6 +5115,7 @@ model web_type_standards {
|
||||
description String?
|
||||
category String? @default("input") @db.VarChar(50)
|
||||
component_name String? @default("TextWidget") @db.VarChar(100)
|
||||
config_panel String? @db.VarChar(100)
|
||||
default_config Json?
|
||||
validation_rules Json?
|
||||
default_style Json?
|
||||
|
||||
@@ -2,6 +2,42 @@ import { Request, Response } from "express";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
|
||||
// 임시 타입 확장 (Prisma Client 재생성 전까지)
|
||||
interface WebTypeStandardCreateData {
|
||||
web_type: string;
|
||||
type_name: string;
|
||||
type_name_eng?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
component_name?: string;
|
||||
config_panel?: string;
|
||||
default_config?: any;
|
||||
validation_rules?: any;
|
||||
default_style?: any;
|
||||
input_properties?: any;
|
||||
sort_order?: number;
|
||||
is_active?: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
interface WebTypeStandardUpdateData {
|
||||
type_name?: string;
|
||||
type_name_eng?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
component_name?: string;
|
||||
config_panel?: string;
|
||||
default_config?: any;
|
||||
validation_rules?: any;
|
||||
default_style?: any;
|
||||
input_properties?: any;
|
||||
sort_order?: number;
|
||||
is_active?: string;
|
||||
updated_by?: string;
|
||||
updated_date?: Date;
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export class WebTypeStandardController {
|
||||
@@ -91,6 +127,7 @@ export class WebTypeStandardController {
|
||||
description,
|
||||
category = "input",
|
||||
component_name = "TextWidget",
|
||||
config_panel,
|
||||
default_config,
|
||||
validation_rules,
|
||||
default_style,
|
||||
@@ -127,6 +164,7 @@ export class WebTypeStandardController {
|
||||
description,
|
||||
category,
|
||||
component_name,
|
||||
config_panel,
|
||||
default_config,
|
||||
validation_rules,
|
||||
default_style,
|
||||
@@ -135,7 +173,7 @@ export class WebTypeStandardController {
|
||||
is_active,
|
||||
created_by: req.user?.userId || "system",
|
||||
updated_by: req.user?.userId || "system",
|
||||
},
|
||||
} as WebTypeStandardCreateData,
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
@@ -163,6 +201,7 @@ export class WebTypeStandardController {
|
||||
description,
|
||||
category,
|
||||
component_name,
|
||||
config_panel,
|
||||
default_config,
|
||||
validation_rules,
|
||||
default_style,
|
||||
@@ -191,6 +230,7 @@ export class WebTypeStandardController {
|
||||
description,
|
||||
category,
|
||||
component_name,
|
||||
config_panel,
|
||||
default_config,
|
||||
validation_rules,
|
||||
default_style,
|
||||
@@ -199,7 +239,7 @@ export class WebTypeStandardController {
|
||||
is_active,
|
||||
updated_by: req.user?.userId || "system",
|
||||
updated_date: new Date(),
|
||||
},
|
||||
} as WebTypeStandardUpdateData,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
|
||||
@@ -14,6 +14,7 @@ import { toast } from "sonner";
|
||||
import { ArrowLeft, Save, RotateCcw, Eye } from "lucide-react";
|
||||
import { useWebTypes, type WebTypeFormData } from "@/hooks/admin/useWebTypes";
|
||||
import { AVAILABLE_COMPONENTS, getComponentInfo } from "@/lib/utils/availableComponents";
|
||||
import { AVAILABLE_CONFIG_PANELS, getConfigPanelInfo } from "@/lib/utils/availableConfigPanels";
|
||||
import Link from "next/link";
|
||||
|
||||
// 기본 카테고리 목록
|
||||
@@ -57,6 +58,7 @@ export default function EditWebTypePage() {
|
||||
description: found.description || "",
|
||||
category: found.category,
|
||||
component_name: found.component_name || "TextWidget",
|
||||
config_panel: found.config_panel || "none",
|
||||
default_config: found.default_config || {},
|
||||
validation_rules: found.validation_rules || {},
|
||||
default_style: found.default_style || {},
|
||||
@@ -159,6 +161,7 @@ export default function EditWebTypePage() {
|
||||
description: originalData.description || "",
|
||||
category: originalData.category,
|
||||
component_name: originalData.component_name || "TextWidget",
|
||||
config_panel: originalData.config_panel || "none",
|
||||
default_config: originalData.default_config || {},
|
||||
validation_rules: originalData.validation_rules || {},
|
||||
default_style: originalData.default_style || {},
|
||||
@@ -311,6 +314,44 @@ export default function EditWebTypePage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 설정 패널 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="config_panel">설정 패널</Label>
|
||||
<Select
|
||||
value={formData.config_panel || "none"}
|
||||
onValueChange={(value) => handleInputChange("config_panel", value === "none" ? null : value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="설정 패널 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AVAILABLE_CONFIG_PANELS.map((panel) => (
|
||||
<SelectItem key={panel.value} value={panel.value}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{panel.label}</span>
|
||||
<span className="text-muted-foreground text-xs">{panel.description}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{formData.config_panel && (
|
||||
<div className="rounded-lg border border-green-200 bg-green-50 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
||||
<span className="text-sm font-medium text-green-700">
|
||||
현재 선택: {getConfigPanelInfo(formData.config_panel)?.label || formData.config_panel}
|
||||
</span>
|
||||
</div>
|
||||
{getConfigPanelInfo(formData.config_panel)?.description && (
|
||||
<p className="mt-1 ml-4 text-xs text-green-600">
|
||||
{getConfigPanelInfo(formData.config_panel)?.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 설명 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">설명</Label>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { toast } from "sonner";
|
||||
import { ArrowLeft, Save, RotateCcw } from "lucide-react";
|
||||
import { useWebTypes, type WebTypeFormData } from "@/hooks/admin/useWebTypes";
|
||||
import { AVAILABLE_COMPONENTS } from "@/lib/utils/availableComponents";
|
||||
import { AVAILABLE_CONFIG_PANELS, getConfigPanelInfo } from "@/lib/utils/availableConfigPanels";
|
||||
import Link from "next/link";
|
||||
|
||||
// 기본 카테고리 목록
|
||||
@@ -30,6 +31,7 @@ export default function NewWebTypePage() {
|
||||
description: "",
|
||||
category: "input",
|
||||
component_name: "TextWidget",
|
||||
config_panel: "none",
|
||||
default_config: {},
|
||||
validation_rules: {},
|
||||
default_style: {},
|
||||
@@ -139,6 +141,7 @@ export default function NewWebTypePage() {
|
||||
description: "",
|
||||
category: "input",
|
||||
component_name: "TextWidget",
|
||||
config_panel: "none",
|
||||
default_config: {},
|
||||
validation_rules: {},
|
||||
default_style: {},
|
||||
@@ -270,6 +273,44 @@ export default function NewWebTypePage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 설정 패널 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="config_panel">설정 패널</Label>
|
||||
<Select
|
||||
value={formData.config_panel || "none"}
|
||||
onValueChange={(value) => handleInputChange("config_panel", value === "none" ? null : value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="설정 패널 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AVAILABLE_CONFIG_PANELS.map((panel) => (
|
||||
<SelectItem key={panel.value} value={panel.value}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{panel.label}</span>
|
||||
<span className="text-muted-foreground text-xs">{panel.description}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{formData.config_panel && (
|
||||
<div className="rounded-lg border border-green-200 bg-green-50 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
||||
<span className="text-sm font-medium text-green-700">
|
||||
현재 선택: {getConfigPanelInfo(formData.config_panel)?.label || formData.config_panel}
|
||||
</span>
|
||||
</div>
|
||||
{getConfigPanelInfo(formData.config_panel)?.description && (
|
||||
<p className="mt-1 ml-4 text-xs text-green-600">
|
||||
{getConfigPanelInfo(formData.config_panel)?.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 설명 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">설명</Label>
|
||||
|
||||
@@ -245,6 +245,13 @@ export default function WebTypesManagePage() {
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("config_panel")}>
|
||||
<div className="flex items-center gap-2">
|
||||
설정 패널
|
||||
{sortField === "config_panel" &&
|
||||
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("is_active")}>
|
||||
<div className="flex items-center gap-2">
|
||||
상태
|
||||
@@ -265,7 +272,7 @@ export default function WebTypesManagePage() {
|
||||
<TableBody>
|
||||
{filteredAndSortedWebTypes.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="py-8 text-center">
|
||||
<TableCell colSpan={10} className="py-8 text-center">
|
||||
조건에 맞는 웹타입이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -289,6 +296,11 @@ export default function WebTypesManagePage() {
|
||||
{webType.component_name || "TextWidget"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary" className="font-mono text-xs">
|
||||
{webType.config_panel === "none" || !webType.config_panel ? "기본 설정" : webType.config_panel}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={webType.is_active === "Y" ? "default" : "secondary"}>
|
||||
{webType.is_active === "Y" ? "활성화" : "비활성화"}
|
||||
|
||||
@@ -3063,6 +3063,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
autoHeight={true}
|
||||
>
|
||||
<DetailSettingsPanel
|
||||
key={`detail-settings-${selectedComponent?.id}-${selectedComponent?.type === "widget" ? (selectedComponent as any).widgetType : "non-widget"}`}
|
||||
selectedComponent={selectedComponent || undefined}
|
||||
onUpdateProperty={(componentId: string, path: string, value: any) => {
|
||||
updateComponentProperty(componentId, path, value);
|
||||
|
||||
@@ -4,35 +4,8 @@ import React from "react";
|
||||
import { Settings } from "lucide-react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { useWebTypes } from "@/hooks/admin/useWebTypes";
|
||||
import {
|
||||
ComponentData,
|
||||
WidgetComponent,
|
||||
FileComponent,
|
||||
WebTypeConfig,
|
||||
DateTypeConfig,
|
||||
NumberTypeConfig,
|
||||
SelectTypeConfig,
|
||||
TextTypeConfig,
|
||||
TextareaTypeConfig,
|
||||
CheckboxTypeConfig,
|
||||
RadioTypeConfig,
|
||||
FileTypeConfig,
|
||||
CodeTypeConfig,
|
||||
EntityTypeConfig,
|
||||
ButtonTypeConfig,
|
||||
TableInfo,
|
||||
} from "@/types/screen";
|
||||
import { DateTypeConfigPanel } from "./webtype-configs/DateTypeConfigPanel";
|
||||
import { NumberTypeConfigPanel } from "./webtype-configs/NumberTypeConfigPanel";
|
||||
import { SelectTypeConfigPanel } from "./webtype-configs/SelectTypeConfigPanel";
|
||||
import { TextTypeConfigPanel } from "./webtype-configs/TextTypeConfigPanel";
|
||||
import { TextareaTypeConfigPanel } from "./webtype-configs/TextareaTypeConfigPanel";
|
||||
import { CheckboxTypeConfigPanel } from "./webtype-configs/CheckboxTypeConfigPanel";
|
||||
import { RadioTypeConfigPanel } from "./webtype-configs/RadioTypeConfigPanel";
|
||||
import { FileTypeConfigPanel } from "./webtype-configs/FileTypeConfigPanel";
|
||||
import { CodeTypeConfigPanel } from "./webtype-configs/CodeTypeConfigPanel";
|
||||
import { RatingTypeConfigPanel, RatingTypeConfig } from "./webtype-configs/RatingTypeConfigPanel";
|
||||
import { EntityTypeConfigPanel } from "./webtype-configs/EntityTypeConfigPanel";
|
||||
import { getConfigPanelComponent } from "@/lib/utils/getConfigPanelComponent";
|
||||
import { ComponentData, WidgetComponent, FileComponent, WebTypeConfig, TableInfo } from "@/types/screen";
|
||||
import { ButtonConfigPanel } from "./ButtonConfigPanel";
|
||||
import { FileComponentConfigPanel } from "./FileComponentConfigPanel";
|
||||
|
||||
@@ -51,166 +24,77 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
}) => {
|
||||
// 데이터베이스에서 입력 가능한 웹타입들을 동적으로 가져오기
|
||||
const { webTypes } = useWebTypes({ active: "Y" });
|
||||
|
||||
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 renderWebTypeConfig = React.useCallback(
|
||||
(widget: WidgetComponent) => {
|
||||
const currentConfig = widget.webTypeConfig || {};
|
||||
// 웹타입별 상세 설정 렌더링 함수 - useCallback 제거하여 항상 최신 widget 사용
|
||||
const renderWebTypeConfig = (widget: WidgetComponent) => {
|
||||
const currentConfig = widget.webTypeConfig || {};
|
||||
|
||||
console.log("🎨 DetailSettingsPanel renderWebTypeConfig 호출:", {
|
||||
componentId: widget.id,
|
||||
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,
|
||||
currentConfig,
|
||||
configExists: !!currentConfig,
|
||||
configKeys: Object.keys(currentConfig),
|
||||
configStringified: JSON.stringify(currentConfig),
|
||||
widgetWebTypeConfig: widget.webTypeConfig,
|
||||
widgetWebTypeConfigExists: !!widget.webTypeConfig,
|
||||
timestamp: new Date().toISOString(),
|
||||
oldConfig: currentConfig,
|
||||
newConfig,
|
||||
componentId: widget.id,
|
||||
isEqual: JSON.stringify(currentConfig) === JSON.stringify(newConfig),
|
||||
});
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// 강제 새 객체 생성으로 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);
|
||||
|
||||
switch (widget.widgetType) {
|
||||
case "date":
|
||||
case "datetime":
|
||||
return (
|
||||
<DateTypeConfigPanel
|
||||
key={`date-config-${widget.id}`}
|
||||
config={currentConfig as DateTypeConfig}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
);
|
||||
if (dbWebType?.config_panel) {
|
||||
console.log(`🎨 웹타입 "${widget.widgetType}" → DB 지정 설정 패널 "${dbWebType.config_panel}" 사용`);
|
||||
const ConfigPanelComponent = getConfigPanelComponent(dbWebType.config_panel);
|
||||
console.log(`🎨 getConfigPanelComponent 결과:`, ConfigPanelComponent);
|
||||
|
||||
case "number":
|
||||
case "decimal":
|
||||
return (
|
||||
<NumberTypeConfigPanel
|
||||
key={`${widget.id}-number`}
|
||||
config={currentConfig as NumberTypeConfig}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
);
|
||||
|
||||
case "select":
|
||||
case "dropdown":
|
||||
return (
|
||||
<SelectTypeConfigPanel
|
||||
key={`${widget.id}-select`}
|
||||
config={currentConfig as SelectTypeConfig}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
);
|
||||
|
||||
case "text":
|
||||
case "email":
|
||||
case "tel":
|
||||
return (
|
||||
<TextTypeConfigPanel
|
||||
key={`${widget.id}-text`}
|
||||
config={currentConfig as TextTypeConfig}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
);
|
||||
|
||||
case "textarea":
|
||||
return (
|
||||
<TextareaTypeConfigPanel
|
||||
key={`${widget.id}-textarea`}
|
||||
config={currentConfig as TextareaTypeConfig}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
);
|
||||
|
||||
case "checkbox":
|
||||
case "boolean":
|
||||
return (
|
||||
<CheckboxTypeConfigPanel
|
||||
key={`${widget.id}-checkbox`}
|
||||
config={currentConfig as CheckboxTypeConfig}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
);
|
||||
|
||||
case "radio":
|
||||
return (
|
||||
<RadioTypeConfigPanel
|
||||
key={`${widget.id}-radio`}
|
||||
config={currentConfig as RadioTypeConfig}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
);
|
||||
|
||||
case "file":
|
||||
return (
|
||||
<FileTypeConfigPanel
|
||||
key={`${widget.id}-file`}
|
||||
config={currentConfig as FileTypeConfig}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
);
|
||||
|
||||
case "code":
|
||||
return (
|
||||
<CodeTypeConfigPanel
|
||||
key={`${widget.id}-code`}
|
||||
config={currentConfig as CodeTypeConfig}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
);
|
||||
|
||||
case "rating":
|
||||
case "star":
|
||||
case "score":
|
||||
return (
|
||||
<RatingTypeConfigPanel
|
||||
key={`${widget.id}-rating`}
|
||||
config={currentConfig as RatingTypeConfig}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
);
|
||||
|
||||
case "entity":
|
||||
return (
|
||||
<EntityTypeConfigPanel
|
||||
key={`${widget.id}-entity`}
|
||||
config={currentConfig as EntityTypeConfig}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
);
|
||||
|
||||
case "button":
|
||||
return (
|
||||
<ButtonConfigPanel
|
||||
key={`${widget.id}-button`}
|
||||
component={widget}
|
||||
onUpdateComponent={(updates) => {
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
onUpdateProperty(widget.id, key, value);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return <div className="text-sm text-gray-500 italic">해당 웹타입의 상세 설정이 지원되지 않습니다.</div>;
|
||||
if (ConfigPanelComponent) {
|
||||
console.log(`🎨 ✅ ConfigPanelComponent 렌더링 시작`);
|
||||
return <ConfigPanelComponent config={currentConfig} onConfigChange={handleConfigChange} />;
|
||||
} else {
|
||||
console.log(`🎨 ❌ ConfigPanelComponent가 null - 기본 설정 표시`);
|
||||
return (
|
||||
<div className="py-8 text-center text-gray-500">
|
||||
⚙️ 기본 설정
|
||||
<br />
|
||||
웹타입 "{widget.widgetType}"은 추가 설정이 없습니다.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
[onUpdateProperty],
|
||||
);
|
||||
} else {
|
||||
console.log(`🎨 config_panel이 없음 - 기본 설정 표시`);
|
||||
return (
|
||||
<div className="py-8 text-center text-gray-500">
|
||||
⚙️ 기본 설정
|
||||
<br />
|
||||
웹타입 "{widget.widgetType}"은 추가 설정이 없습니다.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedComponent) {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
TableInfo,
|
||||
} from "@/types/screen";
|
||||
import DataTableConfigPanel from "./DataTableConfigPanel";
|
||||
import { DynamicConfigPanel } from "@/lib/registry";
|
||||
import { useWebTypes } from "@/hooks/admin/useWebTypes";
|
||||
|
||||
// DataTableConfigPanel을 위한 안정화된 래퍼 컴포넌트
|
||||
@@ -351,39 +350,6 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 동적 웹타입 설정 패널 */}
|
||||
{selectedComponent.widgetType && (
|
||||
<div className="space-y-3">
|
||||
<Separator />
|
||||
<div>
|
||||
<Label className="text-sm font-medium">웹타입 세부 설정</Label>
|
||||
<div className="mt-2">
|
||||
<DynamicConfigPanel
|
||||
webType={selectedComponent.widgetType}
|
||||
component={selectedComponent}
|
||||
onUpdateComponent={(updatedComponent) => {
|
||||
// 컴포넌트 전체 업데이트
|
||||
Object.keys(updatedComponent).forEach((key) => {
|
||||
if (key !== "id") {
|
||||
onUpdateProperty(key, updatedComponent[key]);
|
||||
}
|
||||
});
|
||||
}}
|
||||
onUpdateProperty={(property, value) => {
|
||||
// 웹타입 설정 업데이트
|
||||
if (property === "webTypeConfig") {
|
||||
onUpdateProperty("webTypeConfig", value);
|
||||
} else {
|
||||
onUpdateProperty(property, value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="placeholder" className="text-sm font-medium">
|
||||
플레이스홀더
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface WebTypeStandard {
|
||||
description?: string;
|
||||
category: string;
|
||||
component_name?: string;
|
||||
config_panel?: string;
|
||||
default_config?: any;
|
||||
validation_rules?: any;
|
||||
default_style?: any;
|
||||
@@ -32,6 +33,7 @@ export interface WebTypeFormData {
|
||||
description?: string;
|
||||
category: string;
|
||||
component_name?: string;
|
||||
config_panel?: string;
|
||||
default_config?: any;
|
||||
validation_rules?: any;
|
||||
default_style?: any;
|
||||
|
||||
@@ -1,58 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { WebTypeRegistry } from "./WebTypeRegistry";
|
||||
import { WebTypeConfigPanelProps } from "./types";
|
||||
import { useWebTypes } from "@/hooks/admin/useWebTypes";
|
||||
import { getConfigPanelComponent } from "@/lib/utils/getConfigPanelComponent";
|
||||
|
||||
/**
|
||||
* 동적 설정 패널 렌더러 컴포넌트
|
||||
* 레지스트리에서 웹타입을 조회하여 해당 설정 패널을 동적으로 렌더링합니다.
|
||||
* DB에서 웹타입 설정을 조회하여 해당 설정 패널을 동적으로 렌더링합니다.
|
||||
*/
|
||||
export const DynamicConfigPanel: React.FC<
|
||||
WebTypeConfigPanelProps & {
|
||||
webType: string;
|
||||
}
|
||||
> = ({ webType, component, onUpdateComponent, onUpdateProperty }) => {
|
||||
// 레지스트리에서 웹타입 정의 조회
|
||||
const webTypeDefinition = useMemo(() => {
|
||||
return WebTypeRegistry.getWebType(webType);
|
||||
}, [webType]);
|
||||
// DB에서 웹타입 목록 조회
|
||||
const { webTypes } = useWebTypes({ active: "Y" });
|
||||
|
||||
// 웹타입이 등록되지 않은 경우
|
||||
if (!webTypeDefinition) {
|
||||
console.warn(`웹타입 "${webType}"이 레지스트리에 등록되지 않았습니다.`);
|
||||
console.log(`🔧 DynamicConfigPanel: 웹타입 "${webType}" 설정 패널 로드 시작`);
|
||||
console.log(`🔧 DynamicConfigPanel: webTypes 로드됨`, webTypes?.length, "개");
|
||||
|
||||
// DB에서 웹타입 정보 조회
|
||||
const dbWebType = useMemo(() => {
|
||||
if (!webTypes) return null;
|
||||
const found = webTypes.find((wt) => wt.web_type === webType);
|
||||
console.log(`🔧 DynamicConfigPanel: 웹타입 "${webType}" DB 조회 결과:`, found);
|
||||
return found;
|
||||
}, [webTypes, webType]);
|
||||
|
||||
// 웹타입이 DB에 없는 경우
|
||||
if (!webTypes) {
|
||||
return (
|
||||
<div className="rounded-md border border-dashed border-gray-300 bg-gray-50 p-4">
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<span className="text-sm font-medium">⏳ 로딩 중...</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-500">웹타입 정보를 불러오는 중입니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!dbWebType) {
|
||||
console.warn(`웹타입 "${webType}"이 데이터베이스에 등록되지 않았습니다.`);
|
||||
return (
|
||||
<div className="rounded-md border border-dashed border-red-300 bg-red-50 p-4">
|
||||
<div className="flex items-center gap-2 text-red-600">
|
||||
<span className="text-sm font-medium">⚠️ 설정 패널 없음</span>
|
||||
<span className="text-sm font-medium">⚠️ 웹타입 없음</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-red-500">웹타입 "{webType}"의 설정 패널을 찾을 수 없습니다.</p>
|
||||
<p className="mt-1 text-xs text-red-500">웹타입 "{webType}"이 데이터베이스에 등록되지 않았습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 설정 패널 컴포넌트가 없는 경우
|
||||
if (!webTypeDefinition.configPanel) {
|
||||
// DB에서 설정 패널 조회
|
||||
const configPanelName = dbWebType.config_panel;
|
||||
console.log(`🔧 DynamicConfigPanel: config_panel="${configPanelName}"`);
|
||||
|
||||
// 설정 패널이 지정되지 않은 경우
|
||||
if (!configPanelName || configPanelName === "none") {
|
||||
console.log(`🔧 DynamicConfigPanel: 설정 패널이 없음 - 기본 설정 표시`);
|
||||
return (
|
||||
<div className="rounded-md border border-dashed border-yellow-300 bg-yellow-50 p-4">
|
||||
<div className="flex items-center gap-2 text-yellow-600">
|
||||
<span className="text-sm font-medium">⚠️ 설정 패널 미구현</span>
|
||||
<span className="text-sm font-medium">⚠️ 기본 설정</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-yellow-500">웹타입 "{webType}"에 대한 설정 패널이 구현되지 않았습니다.</p>
|
||||
<p className="mt-1 text-xs text-yellow-500">웹타입 "{webType}"에 대한 추가 설정이 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ConfigPanelComponent = webTypeDefinition.configPanel;
|
||||
// 설정 패널 컴포넌트 조회
|
||||
const ConfigPanelComponent = getConfigPanelComponent(configPanelName);
|
||||
console.log(`🔧 DynamicConfigPanel: ConfigPanelComponent=`, ConfigPanelComponent);
|
||||
|
||||
if (!ConfigPanelComponent) {
|
||||
console.warn(`설정 패널 "${configPanelName}"을 찾을 수 없습니다.`);
|
||||
return (
|
||||
<div className="rounded-md border border-dashed border-red-300 bg-red-50 p-4">
|
||||
<div className="flex items-center gap-2 text-red-600">
|
||||
<span className="text-sm font-medium">⚠️ 설정 패널 미구현</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-red-500">설정 패널 "{configPanelName}"을 찾을 수 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 설정 패널 props 구성
|
||||
const configPanelProps: WebTypeConfigPanelProps = {
|
||||
component,
|
||||
onUpdateComponent,
|
||||
onUpdateProperty,
|
||||
const configPanelProps = {
|
||||
config: component.webTypeConfig || {},
|
||||
onConfigChange: (newConfig: any) => {
|
||||
console.log(`🔧 DynamicConfigPanel: 설정 변경`, newConfig);
|
||||
onUpdateProperty("webTypeConfig", newConfig);
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
console.log(`🔧 DynamicConfigPanel: "${configPanelName}" 컴포넌트 렌더링 시작`);
|
||||
return <ConfigPanelComponent {...configPanelProps} />;
|
||||
} catch (error) {
|
||||
console.error(`웹타입 "${webType}" 설정 패널 렌더링 중 오류 발생:`, error);
|
||||
@@ -95,18 +138,18 @@ export function renderConfigPanel(
|
||||
|
||||
/**
|
||||
* 웹타입이 설정 패널을 지원하는지 확인하는 헬퍼 함수
|
||||
* @deprecated DB 기반 동적 시스템으로 대체됨. DynamicConfigPanel을 직접 사용하세요.
|
||||
*/
|
||||
export function hasConfigPanel(webType: string): boolean {
|
||||
const webTypeDefinition = WebTypeRegistry.getWebType(webType);
|
||||
return !!(webTypeDefinition && webTypeDefinition.configPanel);
|
||||
console.warn("hasConfigPanel은 deprecated됨. DynamicConfigPanel을 직접 사용하세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 웹타입의 기본 설정을 가져오는 헬퍼 함수
|
||||
* @deprecated DB 기반 동적 시스템으로 대체됨. 각 설정 패널에서 직접 관리합니다.
|
||||
*/
|
||||
export function getDefaultConfig(webType: string): Record<string, any> | null {
|
||||
const webTypeDefinition = WebTypeRegistry.getWebType(webType);
|
||||
return webTypeDefinition ? webTypeDefinition.defaultConfig : null;
|
||||
console.warn("getDefaultConfig는 deprecated됨. 각 설정 패널에서 직접 기본값을 관리하세요.");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
22
frontend/lib/utils/availableConfigPanels.ts
Normal file
22
frontend/lib/utils/availableConfigPanels.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// 사용 가능한 설정 패널 목록
|
||||
export const AVAILABLE_CONFIG_PANELS = [
|
||||
{ value: "none", label: "기본 설정", description: "기본 설정 패널 (설정 없음)" },
|
||||
{ value: "DateTypeConfigPanel", label: "날짜 설정", description: "날짜/시간 필드 설정" },
|
||||
{ value: "NumberTypeConfigPanel", label: "숫자 설정", description: "숫자 입력 필드 설정" },
|
||||
{ value: "SelectTypeConfigPanel", label: "선택 설정", description: "드롭다운/선택 필드 설정" },
|
||||
{ value: "TextTypeConfigPanel", label: "텍스트 설정", description: "텍스트 입력 필드 설정" },
|
||||
{ value: "TextareaTypeConfigPanel", label: "텍스트영역 설정", description: "여러 줄 텍스트 설정" },
|
||||
{ value: "CheckboxTypeConfigPanel", label: "체크박스 설정", description: "체크박스 필드 설정" },
|
||||
{ value: "RadioTypeConfigPanel", label: "라디오 설정", description: "라디오 버튼 설정" },
|
||||
{ value: "FileTypeConfigPanel", label: "파일 설정", description: "파일 업로드 설정" },
|
||||
{ value: "CodeTypeConfigPanel", label: "코드 설정", description: "공통코드 선택 설정" },
|
||||
{ value: "EntityTypeConfigPanel", label: "엔티티 설정", description: "엔티티 참조 설정" },
|
||||
{ value: "RatingTypeConfigPanel", label: "별점 설정", description: "별점 평가 설정" },
|
||||
] as const;
|
||||
|
||||
export type ConfigPanelName = (typeof AVAILABLE_CONFIG_PANELS)[number]["value"];
|
||||
|
||||
// 설정 패널 정보 조회 함수
|
||||
export const getConfigPanelInfo = (panelName: string) => {
|
||||
return AVAILABLE_CONFIG_PANELS.find((panel) => panel.value === panelName);
|
||||
};
|
||||
61
frontend/lib/utils/getConfigPanelComponent.ts
Normal file
61
frontend/lib/utils/getConfigPanelComponent.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// 설정 패널 컴포넌트 동적 매핑
|
||||
import React from "react";
|
||||
import { DateTypeConfigPanel } from "@/components/screen/panels/webtype-configs/DateTypeConfigPanel";
|
||||
import { NumberTypeConfigPanel } from "@/components/screen/panels/webtype-configs/NumberTypeConfigPanel";
|
||||
import { SelectTypeConfigPanel } from "@/components/screen/panels/webtype-configs/SelectTypeConfigPanel";
|
||||
import { TextTypeConfigPanel } from "@/components/screen/panels/webtype-configs/TextTypeConfigPanel";
|
||||
import { TextareaTypeConfigPanel } from "@/components/screen/panels/webtype-configs/TextareaTypeConfigPanel";
|
||||
import { CheckboxTypeConfigPanel } from "@/components/screen/panels/webtype-configs/CheckboxTypeConfigPanel";
|
||||
import { RadioTypeConfigPanel } from "@/components/screen/panels/webtype-configs/RadioTypeConfigPanel";
|
||||
import { FileTypeConfigPanel } from "@/components/screen/panels/webtype-configs/FileTypeConfigPanel";
|
||||
import { CodeTypeConfigPanel } from "@/components/screen/panels/webtype-configs/CodeTypeConfigPanel";
|
||||
import { EntityTypeConfigPanel } from "@/components/screen/panels/webtype-configs/EntityTypeConfigPanel";
|
||||
import { RatingTypeConfigPanel } from "@/components/screen/panels/webtype-configs/RatingTypeConfigPanel";
|
||||
|
||||
// 설정 패널 컴포넌트 타입
|
||||
export type ConfigPanelComponent = React.ComponentType<{
|
||||
config: any;
|
||||
onConfigChange: (config: any) => void;
|
||||
}>;
|
||||
|
||||
// 설정 패널 이름으로 직접 매핑하는 함수 (DB의 config_panel 필드용)
|
||||
export const getConfigPanelComponent = (panelName: string): ConfigPanelComponent | null => {
|
||||
console.log(`🔧 getConfigPanelComponent 호출: panelName="${panelName}"`);
|
||||
|
||||
// "none"이나 빈 값은 null 반환 (기본 설정)
|
||||
if (!panelName || panelName === "none") {
|
||||
console.log(`🔧 빈 값 또는 "none" → null 반환`);
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (panelName) {
|
||||
case "DateTypeConfigPanel":
|
||||
return DateTypeConfigPanel;
|
||||
case "NumberTypeConfigPanel":
|
||||
return NumberTypeConfigPanel;
|
||||
case "SelectTypeConfigPanel":
|
||||
return SelectTypeConfigPanel;
|
||||
case "TextTypeConfigPanel":
|
||||
return TextTypeConfigPanel;
|
||||
case "TextareaTypeConfigPanel":
|
||||
return TextareaTypeConfigPanel;
|
||||
case "CheckboxTypeConfigPanel":
|
||||
return CheckboxTypeConfigPanel;
|
||||
case "RadioTypeConfigPanel":
|
||||
return RadioTypeConfigPanel;
|
||||
case "FileTypeConfigPanel":
|
||||
return FileTypeConfigPanel;
|
||||
case "CodeTypeConfigPanel":
|
||||
return CodeTypeConfigPanel;
|
||||
case "EntityTypeConfigPanel":
|
||||
return EntityTypeConfigPanel;
|
||||
case "RatingTypeConfigPanel":
|
||||
console.log(`🔧 RatingTypeConfigPanel 컴포넌트 반환`);
|
||||
console.log(`🔧 RatingTypeConfigPanel 타입:`, typeof RatingTypeConfigPanel);
|
||||
console.log(`🔧 RatingTypeConfigPanel 내용:`, RatingTypeConfigPanel);
|
||||
return RatingTypeConfigPanel;
|
||||
default:
|
||||
console.warn(`🔧 알 수 없는 설정 패널: ${panelName}, 기본 설정 사용`);
|
||||
return null; // 기본 설정 (패널 없음)
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user