From 49c8f9a2ddd512a9705dbb6a56fe1b2f42088d33 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 9 Sep 2025 15:42:04 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9B=B9=20=ED=83=80=EC=9E=85=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=ED=8C=A8=EB=84=90=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/prisma/schema.prisma | 1 + .../controllers/webTypeStandardController.ts | 44 +++- .../admin/standards/[webType]/edit/page.tsx | 41 +++ .../app/(main)/admin/standards/new/page.tsx | 41 +++ frontend/app/(main)/admin/standards/page.tsx | 14 +- frontend/components/screen/ScreenDesigner.tsx | 1 + .../screen/panels/DetailSettingsPanel.tsx | 242 +++++------------- .../screen/panels/PropertiesPanel.tsx | 36 +-- frontend/hooks/admin/useWebTypes.ts | 2 + frontend/lib/registry/DynamicConfigPanel.tsx | 95 +++++-- frontend/lib/utils/availableConfigPanels.ts | 22 ++ frontend/lib/utils/getConfigPanelComponent.ts | 61 +++++ 12 files changed, 357 insertions(+), 243 deletions(-) create mode 100644 frontend/lib/utils/availableConfigPanels.ts create mode 100644 frontend/lib/utils/getConfigPanelComponent.ts diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index 9944035f..421ecbcb 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -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? diff --git a/backend-node/src/controllers/webTypeStandardController.ts b/backend-node/src/controllers/webTypeStandardController.ts index de3cea09..a62e892e 100644 --- a/backend-node/src/controllers/webTypeStandardController.ts +++ b/backend-node/src/controllers/webTypeStandardController.ts @@ -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({ diff --git a/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx b/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx index 9ab967cd..be7dd3f5 100644 --- a/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx +++ b/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx @@ -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() { )} + {/* 설정 패널 */} +
+ + + {formData.config_panel && ( +
+
+
+ + 현재 선택: {getConfigPanelInfo(formData.config_panel)?.label || formData.config_panel} + +
+ {getConfigPanelInfo(formData.config_panel)?.description && ( +

+ {getConfigPanelInfo(formData.config_panel)?.description} +

+ )} +
+ )} +
+ {/* 설명 */}
diff --git a/frontend/app/(main)/admin/standards/new/page.tsx b/frontend/app/(main)/admin/standards/new/page.tsx index 95a069dd..77df8a74 100644 --- a/frontend/app/(main)/admin/standards/new/page.tsx +++ b/frontend/app/(main)/admin/standards/new/page.tsx @@ -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() { )}
+ {/* 설정 패널 */} +
+ + + {formData.config_panel && ( +
+
+
+ + 현재 선택: {getConfigPanelInfo(formData.config_panel)?.label || formData.config_panel} + +
+ {getConfigPanelInfo(formData.config_panel)?.description && ( +

+ {getConfigPanelInfo(formData.config_panel)?.description} +

+ )} +
+ )} +
+ {/* 설명 */}
diff --git a/frontend/app/(main)/admin/standards/page.tsx b/frontend/app/(main)/admin/standards/page.tsx index 8f683ee0..c21266ab 100644 --- a/frontend/app/(main)/admin/standards/page.tsx +++ b/frontend/app/(main)/admin/standards/page.tsx @@ -245,6 +245,13 @@ export default function WebTypesManagePage() { (sortDirection === "asc" ? : )}
+ handleSort("config_panel")}> +
+ 설정 패널 + {sortField === "config_panel" && + (sortDirection === "asc" ? : )} +
+
handleSort("is_active")}>
상태 @@ -265,7 +272,7 @@ export default function WebTypesManagePage() { {filteredAndSortedWebTypes.length === 0 ? ( - + 조건에 맞는 웹타입이 없습니다. @@ -289,6 +296,11 @@ export default function WebTypesManagePage() { {webType.component_name || "TextWidget"} + + + {webType.config_panel === "none" || !webType.config_panel ? "기본 설정" : webType.config_panel} + + {webType.is_active === "Y" ? "활성화" : "비활성화"} diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 43436307..b7894863 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -3063,6 +3063,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD autoHeight={true} > { updateComponentProperty(componentId, path, value); diff --git a/frontend/components/screen/panels/DetailSettingsPanel.tsx b/frontend/components/screen/panels/DetailSettingsPanel.tsx index 37f8c94e..0ae34682 100644 --- a/frontend/components/screen/panels/DetailSettingsPanel.tsx +++ b/frontend/components/screen/panels/DetailSettingsPanel.tsx @@ -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 = ({ }) => { // 데이터베이스에서 입력 가능한 웹타입들을 동적으로 가져오기 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 ( - - ); + 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 ( - - ); - - case "select": - case "dropdown": - return ( - - ); - - case "text": - case "email": - case "tel": - return ( - - ); - - case "textarea": - return ( - - ); - - case "checkbox": - case "boolean": - return ( - - ); - - case "radio": - return ( - - ); - - case "file": - return ( - - ); - - case "code": - return ( - - ); - - case "rating": - case "star": - case "score": - return ( - - ); - - case "entity": - return ( - - ); - - case "button": - return ( - { - Object.entries(updates).forEach(([key, value]) => { - onUpdateProperty(widget.id, key, value); - }); - }} - /> - ); - - default: - return
해당 웹타입의 상세 설정이 지원되지 않습니다.
; + if (ConfigPanelComponent) { + console.log(`🎨 ✅ ConfigPanelComponent 렌더링 시작`); + return ; + } else { + console.log(`🎨 ❌ ConfigPanelComponent가 null - 기본 설정 표시`); + return ( +
+ ⚙️ 기본 설정 +
+ 웹타입 "{widget.widgetType}"은 추가 설정이 없습니다. +
+ ); } - }, - [onUpdateProperty], - ); + } else { + console.log(`🎨 config_panel이 없음 - 기본 설정 표시`); + return ( +
+ ⚙️ 기본 설정 +
+ 웹타입 "{widget.widgetType}"은 추가 설정이 없습니다. +
+ ); + } + }; if (!selectedComponent) { return ( diff --git a/frontend/components/screen/panels/PropertiesPanel.tsx b/frontend/components/screen/panels/PropertiesPanel.tsx index 8664f7bd..13c44181 100644 --- a/frontend/components/screen/panels/PropertiesPanel.tsx +++ b/frontend/components/screen/panels/PropertiesPanel.tsx @@ -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 = ({
- {/* 동적 웹타입 설정 패널 */} - {selectedComponent.widgetType && ( -
- -
- -
- { - // 컴포넌트 전체 업데이트 - 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); - } - }} - /> -
-
- -
- )} -