diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index 35a3578c..d7c9b38d 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -4106,4 +4106,37 @@ model test_sales_info { manager_name String? @db.VarChar(100) reg_date DateTime? @default(now()) @db.Timestamp(6) status String? @default("진행중") @db.VarChar(50) + + // 관계 정의: 영업 정보에서 프로젝트로 + projects test_project_info[] +} + +model test_project_info { + project_no String @id @db.VarChar(200) + sales_no String? @db.VarChar(20) + contract_type String? @db.VarChar(50) + order_seq Int? + domestic_foreign String? @db.VarChar(20) + customer_name String? @db.VarChar(200) + + // 프로젝트 전용 컬럼들 + project_status String? @default("PLANNING") @db.VarChar(50) + project_start_date DateTime? @db.Date + project_end_date DateTime? @db.Date + project_manager String? @db.VarChar(100) + project_description String? @db.Text + + // 시스템 관리 컬럼들 + created_by String? @db.VarChar(100) + created_date DateTime? @default(now()) @db.Timestamp(6) + updated_by String? @db.VarChar(100) + updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6) + + // 관계 정의: 영업 정보 참조 + sales test_sales_info? @relation(fields: [sales_no], references: [sales_no]) + + @@index([sales_no], map: "idx_project_sales_no") + @@index([project_status], map: "idx_project_status") + @@index([customer_name], map: "idx_project_customer") + @@index([project_manager], map: "idx_project_manager") } diff --git a/backend-node/src/controllers/buttonDataflowController.ts b/backend-node/src/controllers/buttonDataflowController.ts index 82ee5b64..38ca9d4c 100644 --- a/backend-node/src/controllers/buttonDataflowController.ts +++ b/backend-node/src/controllers/buttonDataflowController.ts @@ -167,6 +167,19 @@ export async function getDiagramRelationships( const relationships = (diagram.relationships as any)?.relationships || []; + console.log("🔍 백엔드 - 관계도 데이터:", { + diagramId: diagram.diagram_id, + diagramName: diagram.diagram_name, + relationshipsRaw: diagram.relationships, + relationshipsArray: relationships, + relationshipsCount: relationships.length, + }); + + // 각 관계의 구조도 로깅 + relationships.forEach((rel: any, index: number) => { + console.log(`🔍 백엔드 - 관계 ${index + 1}:`, rel); + }); + res.json({ success: true, data: relationships, @@ -213,14 +226,77 @@ export async function getRelationshipPreview( } // 관계 정보 찾기 - const relationship = (diagram.relationships as any)?.relationships?.find( + console.log("🔍 관계 미리보기 요청:", { + diagramId, + relationshipId, + diagramRelationships: diagram.relationships, + relationshipsArray: (diagram.relationships as any)?.relationships, + }); + + const relationships = (diagram.relationships as any)?.relationships || []; + console.log( + "🔍 사용 가능한 관계 목록:", + relationships.map((rel: any) => ({ + id: rel.id, + name: rel.relationshipName || rel.name, // relationshipName 사용 + sourceTable: rel.fromTable || rel.sourceTable, // fromTable 사용 + targetTable: rel.toTable || rel.targetTable, // toTable 사용 + originalData: rel, // 디버깅용 + })) + ); + + const relationship = relationships.find( (rel: any) => rel.id === relationshipId ); + console.log("🔍 찾은 관계:", relationship); + if (!relationship) { + console.log("❌ 관계를 찾을 수 없음:", { + requestedId: relationshipId, + availableIds: relationships.map((rel: any) => rel.id), + }); + + // 🔧 임시 해결책: 첫 번째 관계를 사용하거나 기본 응답 반환 + if (relationships.length > 0) { + console.log("🔧 첫 번째 관계를 대신 사용:", relationships[0].id); + + const fallbackRelationship = relationships[0]; + + console.log("🔍 fallback 관계 선택:", fallbackRelationship); + console.log("🔍 diagram.control 전체 구조:", diagram.control); + console.log("🔍 diagram.plan 전체 구조:", diagram.plan); + + const fallbackControl = Array.isArray(diagram.control) + ? diagram.control.find((c: any) => c.id === fallbackRelationship.id) + : null; + const fallbackPlan = Array.isArray(diagram.plan) + ? diagram.plan.find((p: any) => p.id === fallbackRelationship.id) + : null; + + console.log("🔍 찾은 fallback control:", fallbackControl); + console.log("🔍 찾은 fallback plan:", fallbackPlan); + + const fallbackPreviewData = { + relationship: fallbackRelationship, + control: fallbackControl, + plan: fallbackPlan, + conditionsCount: (fallbackControl as any)?.conditions?.length || 0, + actionsCount: (fallbackPlan as any)?.actions?.length || 0, + }; + + console.log("🔍 최종 fallback 응답 데이터:", fallbackPreviewData); + + res.json({ + success: true, + data: fallbackPreviewData, + }); + return; + } + res.status(404).json({ success: false, - message: "관계를 찾을 수 없습니다.", + message: `관계를 찾을 수 없습니다. 요청된 ID: ${relationshipId}, 사용 가능한 ID: ${relationships.map((rel: any) => rel.id).join(", ")}`, }); return; } diff --git a/backend-node/src/controllers/dynamicFormController.ts b/backend-node/src/controllers/dynamicFormController.ts index 20c74714..47d99787 100644 --- a/backend-node/src/controllers/dynamicFormController.ts +++ b/backend-node/src/controllers/dynamicFormController.ts @@ -11,8 +11,8 @@ export const saveFormData = async ( const { companyCode, userId } = req.user as any; const { screenId, tableName, data } = req.body; - // 필수 필드 검증 - if (!screenId || !tableName || !data) { + // 필수 필드 검증 (screenId는 0일 수 있으므로 undefined 체크) + if (screenId === undefined || screenId === null || !tableName || !data) { return res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다. (screenId, tableName, data)", @@ -80,7 +80,7 @@ export const updateFormData = async ( }; const result = await dynamicFormService.updateFormData( - parseInt(id), + id, // parseInt 제거 - 문자열 ID 지원 tableName, formDataWithMeta ); @@ -168,7 +168,7 @@ export const deleteFormData = async ( }); } - await dynamicFormService.deleteFormData(parseInt(id), tableName); + await dynamicFormService.deleteFormData(id, tableName); // parseInt 제거 - 문자열 ID 지원 res.json({ success: true, diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 3e61b69e..8a8d1dd7 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -549,7 +549,7 @@ export class DynamicFormService { * 폼 데이터 업데이트 (실제 테이블에서 직접 업데이트) */ async updateFormData( - id: number, + id: string | number, tableName: string, data: Record ): Promise { @@ -642,10 +642,36 @@ export class DynamicFormService { const primaryKeyColumn = primaryKeys[0]; // 첫 번째 기본키 사용 console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`); + // 기본키 데이터 타입 조회하여 적절한 캐스팅 적용 + const primaryKeyInfo = (await prisma.$queryRawUnsafe(` + SELECT data_type + FROM information_schema.columns + WHERE table_name = '${tableName}' + AND column_name = '${primaryKeyColumn}' + AND table_schema = 'public' + `)) as any[]; + + let typeCastSuffix = ""; + if (primaryKeyInfo.length > 0) { + const dataType = primaryKeyInfo[0].data_type; + console.log(`🔍 기본키 ${primaryKeyColumn}의 데이터 타입: ${dataType}`); + + if (dataType.includes("character") || dataType.includes("text")) { + typeCastSuffix = "::text"; + } else if (dataType.includes("bigint")) { + typeCastSuffix = "::bigint"; + } else if ( + dataType.includes("integer") || + dataType.includes("numeric") + ) { + typeCastSuffix = "::numeric"; + } + } + const updateQuery = ` UPDATE ${tableName} SET ${setClause} - WHERE ${primaryKeyColumn} = $${values.length} + WHERE ${primaryKeyColumn} = $${values.length}${typeCastSuffix} RETURNING * `; @@ -707,7 +733,7 @@ export class DynamicFormService { * 폼 데이터 삭제 (실제 테이블에서 직접 삭제) */ async deleteFormData( - id: number, + id: string | number, tableName: string, companyCode?: string ): Promise { diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 294cfa40..2530d403 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -18,6 +18,41 @@ const prisma = new PrismaClient(); export class TableManagementService { constructor() {} + /** + * 컬럼이 코드 타입인지 확인하고 코드 카테고리 반환 + */ + private async getCodeTypeInfo( + tableName: string, + columnName: string + ): Promise<{ isCodeType: boolean; codeCategory?: string }> { + try { + // column_labels 테이블에서 해당 컬럼의 web_type이 'code'인지 확인 + const result = await prisma.$queryRaw` + SELECT web_type, code_category + FROM column_labels + WHERE table_name = ${tableName} + AND column_name = ${columnName} + AND web_type = 'code' + `; + + if (Array.isArray(result) && result.length > 0) { + const row = result[0] as any; + return { + isCodeType: true, + codeCategory: row.code_category, + }; + } + + return { isCodeType: false }; + } catch (error) { + logger.warn( + `코드 타입 컬럼 확인 중 오류: ${tableName}.${columnName}`, + error + ); + return { isCodeType: false }; + } + } + /** * 테이블 목록 조회 (PostgreSQL information_schema 활용) * 메타데이터 조회는 Prisma로 변경 불가 @@ -915,8 +950,36 @@ export class TableManagementService { const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, ""); if (typeof value === "string") { - whereConditions.push(`${safeColumn}::text ILIKE $${paramIndex}`); - searchValues.push(`%${value}%`); + // 🎯 코드 타입 컬럼의 경우 코드값과 코드명 모두로 검색 + const codeTypeInfo = await this.getCodeTypeInfo( + tableName, + safeColumn + ); + + if (codeTypeInfo.isCodeType && codeTypeInfo.codeCategory) { + // 코드 타입 컬럼: 코드값 또는 코드명으로 검색 + // 1) 컬럼 값이 직접 검색어와 일치하는 경우 + // 2) 컬럼 값이 코드값이고, 해당 코드의 코드명이 검색어와 일치하는 경우 + whereConditions.push(`( + ${safeColumn}::text ILIKE $${paramIndex} OR + EXISTS ( + SELECT 1 FROM code_info ci + WHERE ci.code_category = $${paramIndex + 1} + AND ci.code_value = ${safeColumn} + AND ci.code_name ILIKE $${paramIndex + 2} + ) + )`); + searchValues.push(`%${value}%`); // 직접 값 검색용 + searchValues.push(codeTypeInfo.codeCategory); // 코드 카테고리 + searchValues.push(`%${value}%`); // 코드명 검색용 + paramIndex += 2; // 추가 파라미터로 인해 인덱스 증가 + } else { + // 일반 컬럼: 기존 방식 + whereConditions.push( + `${safeColumn}::text ILIKE $${paramIndex}` + ); + searchValues.push(`%${value}%`); + } } else { whereConditions.push(`${safeColumn} = $${paramIndex}`); searchValues.push(value); diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 6e5f26e4..41b88030 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -1557,6 +1557,22 @@ export const InteractiveDataTable: React.FC = ({ ); + case "code": + // 코드 타입은 텍스트 검색으로 처리 (코드명으로 검색 가능) + return ( + handleSearchValueChange(filter.columnName, e.target.value)} + onKeyPress={(e) => { + if (e.key === "Enter") { + handleSearch(); + } + }} + /> + ); + default: return ( = ({ try { console.log(`🔘 Button clicked: ${component.id} (${config?.actionType})`); - // 🔥 현재 폼 데이터 수집 + // 🔥 확장된 컨텍스트 데이터 수집 const contextData = { ...formData, buttonId: component.id, @@ -69,11 +73,61 @@ export const OptimizedButtonComponent: React.FC = ({ clickCount, }; + // 🔥 확장된 제어 컨텍스트 생성 + const extendedContext = { + formData, + selectedRows: selectedRows || [], + selectedRowsData: selectedRowsData || [], + controlDataSource: config?.dataflowConfig?.controlDataSource || "form", + buttonId: component.id, + componentData: component, + timestamp: new Date().toISOString(), + clickCount, + }; + + // 🔥 제어 전용 액션인지 확인 + const isControlOnlyAction = config?.actionType === "control"; + + console.log("🎯 OptimizedButtonComponent 실행:", { + actionType: config?.actionType, + isControlOnlyAction, + enableDataflowControl: config?.enableDataflowControl, + hasDataflowConfig: !!config?.dataflowConfig, + selectedRows, + selectedRowsData, + }); + if (config?.enableDataflowControl && config?.dataflowConfig) { + // 🔥 확장된 제어 검증 먼저 실행 + const validationResult = await OptimizedButtonDataflowService.executeExtendedValidation( + config.dataflowConfig, + extendedContext as ExtendedControlContext, + ); + + if (!validationResult.success) { + toast.error(validationResult.message || "제어 조건을 만족하지 않습니다."); + return; + } + + // 🔥 제어 전용 액션이면 여기서 종료 + if (isControlOnlyAction) { + toast.success("제어 조건을 만족합니다."); + if (onActionComplete) { + onActionComplete({ success: true, message: "제어 조건 통과" }); + } + return; + } + // 🔥 최적화된 버튼 실행 (즉시 응답) await executeOptimizedButtonAction(contextData); + } else if (isControlOnlyAction) { + // 🔥 제어관리가 비활성화된 상태에서 제어 액션 + toast.warning( + "제어관리를 먼저 활성화해주세요. 제어 액션을 사용하려면 버튼 설정에서 '제어관리 활성화'를 체크하고 조건을 설정해주세요.", + ); + return; } else { - // 🔥 기존 액션만 실행 + // 🔥 기존 액션만 실행 (제어 액션 제외) await executeOriginalAction(config?.actionType || "save", contextData); } } catch (error) { @@ -332,6 +386,12 @@ export const OptimizedButtonComponent: React.FC = ({ actionType: ButtonActionType, contextData: Record, ): Promise => { + // 🔥 제어 액션은 여기서 처리하지 않음 (이미 위에서 처리됨) + if (actionType === "control") { + console.warn("제어 액션은 executeOriginalAction에서 처리되지 않아야 합니다."); + return; + } + // 간단한 mock 처리 (실제로는 API 호출) await new Promise((resolve) => setTimeout(resolve, 100)); // 100ms 시뮬레이션 @@ -369,6 +429,7 @@ export const OptimizedButtonComponent: React.FC = ({ modal: "모달", newWindow: "새 창", navigate: "페이지 이동", + control: "제어", }; return displayNames[actionType] || actionType; }; diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 61c0e154..7c819eb2 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -151,6 +151,7 @@ export const ButtonConfigPanel: React.FC = ({ component, 닫기 모달 열기 페이지 이동 + 제어 (조건 체크만) diff --git a/frontend/components/screen/config-panels/ButtonDataflowConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonDataflowConfigPanel.tsx index ca43929f..798f9812 100644 --- a/frontend/components/screen/config-panels/ButtonDataflowConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonDataflowConfigPanel.tsx @@ -6,7 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { Check, ChevronsUpDown, Search, Info, Settings } from "lucide-react"; +import { Check, ChevronsUpDown, Search, Info, Settings, FileText, Table, Layers } from "lucide-react"; import { cn } from "@/lib/utils"; import { ComponentData, ButtonDataflowConfig } from "@/types/screen"; import { apiClient } from "@/lib/api/client"; @@ -111,16 +111,38 @@ export const ButtonDataflowConfigPanel: React.FC const response = await apiClient.get(`/test-button-dataflow/diagrams/${diagramId}/relationships`); if (response.data.success && Array.isArray(response.data.data)) { - const relationshipList = response.data.data.map((rel: any) => ({ - id: rel.id, - name: rel.name || `${rel.sourceTable} → ${rel.targetTable}`, - sourceTable: rel.sourceTable, - targetTable: rel.targetTable, - category: rel.category || "data-save", - })); + console.log("🔍 백엔드에서 받은 관계 데이터:", response.data.data); + + const relationshipList = response.data.data.map((rel: any) => { + console.log("🔍 개별 관계 데이터:", rel); + + // 여러 가지 가능한 필드명 시도 (백엔드 로그 기준으로 수정) + const relationshipName = + rel.relationshipName || // 백엔드에서 이 필드 사용 + rel.name || + rel.relationship_name || + rel.label || + rel.title || + rel.description || + `${rel.fromTable || rel.sourceTable || rel.source_table} → ${rel.toTable || rel.targetTable || rel.target_table}`; + + const sourceTable = rel.fromTable || rel.sourceTable || rel.source_table || "Unknown"; // fromTable 우선 + const targetTable = rel.toTable || rel.targetTable || rel.target_table || "Unknown"; // toTable 우선 + + const mappedRel = { + id: rel.id, + name: relationshipName, + sourceTable, + targetTable, + category: rel.category || rel.type || "data-save", + }; + + console.log("🔍 매핑된 관계 데이터:", mappedRel); + return mappedRel; + }); setRelationships(relationshipList); - console.log(`✅ 관계 ${relationshipList.length}개 로딩 완료`); + console.log(`✅ 관계 ${relationshipList.length}개 로딩 완료:`, relationshipList); } } catch (error) { console.error("❌ 관계 목록 로딩 실패:", error); @@ -173,6 +195,7 @@ export const ButtonDataflowConfigPanel: React.FC close: "닫기", popup: "팝업", navigate: "페이지 이동", + control: "제어", }; return displayNames[actionType] || actionType; }; @@ -215,6 +238,50 @@ export const ButtonDataflowConfigPanel: React.FC {/* 🔥 제어관리가 활성화된 경우에만 설정 표시 */} + {config.enableDataflowControl && ( + <> + {/* 🔥 제어 데이터 소스 선택 */} +
+ + +

+ {dataflowConfig.controlDataSource === "form" && "현재 폼의 입력값으로 조건을 체크합니다"} + {dataflowConfig.controlDataSource === "table-selection" && + "테이블에서 선택된 항목의 데이터로 조건을 체크합니다"} + {dataflowConfig.controlDataSource === "both" && "폼 데이터와 선택된 항목 데이터를 모두 사용합니다"} + {!dataflowConfig.controlDataSource && "폼 데이터를 기본으로 사용합니다"} +

+
+ + )} + {config.enableDataflowControl && (
{/* 현재 액션 정보 (간소화) */} diff --git a/frontend/lib/api/dynamicForm.ts b/frontend/lib/api/dynamicForm.ts index 29524b45..992859d9 100644 --- a/frontend/lib/api/dynamicForm.ts +++ b/frontend/lib/api/dynamicForm.ts @@ -64,7 +64,7 @@ export class DynamicFormApi { * @returns 업데이트 결과 */ static async updateFormData( - id: number, + id: string | number, formData: Partial, ): Promise> { try { @@ -173,7 +173,7 @@ export class DynamicFormApi { * @param tableName 테이블명 * @returns 삭제 결과 */ - static async deleteFormDataFromTable(id: number, tableName: string): Promise> { + static async deleteFormDataFromTable(id: string | number, tableName: string): Promise> { try { console.log("🗑️ 실제 테이블에서 폼 데이터 삭제 요청:", { id, tableName }); diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index d436b44e..99c1b72e 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -185,6 +185,9 @@ export const DynamicComponentRenderer: React.FC = currentValue, hasFormData: !!formData, formDataKeys: formData ? Object.keys(formData) : [], + autoGeneration: component.autoGeneration, + hidden: component.hidden, + isInteractive, }); return ( @@ -200,6 +203,9 @@ export const DynamicComponentRenderer: React.FC = config={component.componentConfig} componentConfig={component.componentConfig} value={currentValue} // formData에서 추출한 현재 값 전달 + // 새로운 기능들 전달 + autoGeneration={component.autoGeneration} + hidden={component.hidden} // React 전용 props들은 직접 전달 (DOM에 전달되지 않음) isInteractive={isInteractive} formData={formData} diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 0c3ef3fa..1b751b7c 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { ComponentRendererProps } from "@/types/component"; import { ButtonPrimaryConfig } from "./types"; import { @@ -71,6 +71,21 @@ export const ButtonPrimaryComponent: React.FC = ({ config: any; context: ButtonActionContext; } | null>(null); + + // 토스트 정리를 위한 ref + const currentLoadingToastRef = useRef(); + + // 컴포넌트 언마운트 시 토스트 정리 + useEffect(() => { + return () => { + if (currentLoadingToastRef.current !== undefined) { + console.log("🧹 컴포넌트 언마운트 시 토스트 정리"); + toast.dismiss(currentLoadingToastRef.current); + currentLoadingToastRef.current = undefined; + } + }; + }, []); + // 컴포넌트 설정 const componentConfig = { ...config, @@ -84,13 +99,26 @@ export const ButtonPrimaryComponent: React.FC = ({ processedConfig.action = { ...DEFAULT_BUTTON_ACTIONS[actionType], type: actionType, + // 🔥 제어관리 설정 추가 (webTypeConfig에서 가져옴) + enableDataflowControl: component.webTypeConfig?.enableDataflowControl, + dataflowConfig: component.webTypeConfig?.dataflowConfig, + }; + } else if (componentConfig.action && typeof componentConfig.action === "object") { + // 🔥 이미 객체인 경우에도 제어관리 설정 추가 + processedConfig.action = { + ...componentConfig.action, + enableDataflowControl: component.webTypeConfig?.enableDataflowControl, + dataflowConfig: component.webTypeConfig?.dataflowConfig, }; } console.log("🔧 버튼 컴포넌트 설정:", { originalConfig: componentConfig, processedConfig, - component: component, + actionConfig: processedConfig.action, + webTypeConfig: component.webTypeConfig, + enableDataflowControl: component.webTypeConfig?.enableDataflowControl, + dataflowConfig: component.webTypeConfig?.dataflowConfig, screenId, tableName, onRefresh, @@ -119,13 +147,22 @@ export const ButtonPrimaryComponent: React.FC = ({ // 실제 액션 실행 함수 const executeAction = async (actionConfig: any, context: ButtonActionContext) => { console.log("🚀 executeAction 시작:", { actionConfig, context }); - let loadingToast: string | number | undefined; try { + // 기존 토스트가 있다면 먼저 제거 + if (currentLoadingToastRef.current !== undefined) { + console.log("📱 기존 토스트 제거"); + toast.dismiss(currentLoadingToastRef.current); + currentLoadingToastRef.current = undefined; + } + + // 추가 안전장치: 모든 로딩 토스트 제거 + toast.dismiss(); + // edit 액션을 제외하고만 로딩 토스트 표시 if (actionConfig.type !== "edit") { console.log("📱 로딩 토스트 표시 시작"); - loadingToast = toast.loading( + currentLoadingToastRef.current = toast.loading( actionConfig.type === "save" ? "저장 중..." : actionConfig.type === "delete" @@ -133,8 +170,11 @@ export const ButtonPrimaryComponent: React.FC = ({ : actionConfig.type === "submit" ? "제출 중..." : "처리 중...", + { + duration: Infinity, // 명시적으로 무한대로 설정 + }, ); - console.log("📱 로딩 토스트 ID:", loadingToast); + console.log("📱 로딩 토스트 ID:", currentLoadingToastRef.current); } console.log("⚡ ButtonActionExecutor.executeAction 호출 시작"); @@ -142,9 +182,10 @@ export const ButtonPrimaryComponent: React.FC = ({ console.log("⚡ ButtonActionExecutor.executeAction 완료, success:", success); // 로딩 토스트 제거 (있는 경우에만) - if (loadingToast) { - console.log("📱 로딩 토스트 제거"); - toast.dismiss(loadingToast); + if (currentLoadingToastRef.current !== undefined) { + console.log("📱 로딩 토스트 제거 시도, ID:", currentLoadingToastRef.current); + toast.dismiss(currentLoadingToastRef.current); + currentLoadingToastRef.current = undefined; } // edit 액션은 조용히 처리 (모달 열기만 하므로 토스트 불필요) @@ -170,9 +211,10 @@ export const ButtonPrimaryComponent: React.FC = ({ console.log("❌ executeAction catch 블록 진입:", error); // 로딩 토스트 제거 - if (loadingToast) { - console.log("📱 오류 시 로딩 토스트 제거"); - toast.dismiss(loadingToast); + if (currentLoadingToastRef.current !== undefined) { + console.log("📱 오류 시 로딩 토스트 제거, ID:", currentLoadingToastRef.current); + toast.dismiss(currentLoadingToastRef.current); + currentLoadingToastRef.current = undefined; } console.error("❌ 버튼 액션 실행 오류:", error); diff --git a/frontend/lib/registry/components/date-input/DateInputComponent.tsx b/frontend/lib/registry/components/date-input/DateInputComponent.tsx index 2db9d8aa..bdefbac4 100644 --- a/frontend/lib/registry/components/date-input/DateInputComponent.tsx +++ b/frontend/lib/registry/components/date-input/DateInputComponent.tsx @@ -1,13 +1,17 @@ "use client"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { ComponentRendererProps } from "@/types/component"; import { DateInputConfig } from "./types"; import { filterDOMProps } from "@/lib/utils/domPropsFilter"; +import { AutoGenerationUtils } from "@/lib/utils/autoGeneration"; +import { AutoGenerationConfig } from "@/types/screen"; export interface DateInputComponentProps extends ComponentRendererProps { config?: DateInputConfig; value?: any; // 외부에서 전달받는 값 + autoGeneration?: AutoGenerationConfig; + hidden?: boolean; } /** @@ -28,6 +32,8 @@ export const DateInputComponent: React.FC = ({ formData, onFormDataChange, value: externalValue, // 외부에서 전달받은 값 + autoGeneration, + hidden, ...props }) => { // 컴포넌트 설정 @@ -36,14 +42,130 @@ export const DateInputComponent: React.FC = ({ ...component.config, } as DateInputConfig; + // 🎯 자동생성 상태 관리 + const [autoGeneratedValue, setAutoGeneratedValue] = useState(""); + + // 🚨 컴포넌트 마운트 확인용 로그 + console.log("🚨 DateInputComponent 마운트됨!", { + componentId: component.id, + isInteractive, + isDesignMode, + autoGeneration, + componentAutoGeneration: component.autoGeneration, + externalValue, + formDataValue: formData?.[component.columnName || ""], + timestamp: new Date().toISOString(), + }); + + // 🧪 무조건 실행되는 테스트 + useEffect(() => { + console.log("🧪 DateInputComponent 무조건 실행 테스트!"); + const testDate = "2025-01-19"; // 고정된 테스트 날짜 + setAutoGeneratedValue(testDate); + console.log("🧪 autoGeneratedValue 설정 완료:", testDate); + }, []); // 빈 의존성 배열로 한 번만 실행 + + // 자동생성 설정 (props 우선, 컴포넌트 설정 폴백) + const finalAutoGeneration = autoGeneration || component.autoGeneration; + const finalHidden = hidden !== undefined ? hidden : component.hidden; + + // 🧪 테스트용 간단한 자동생성 로직 + useEffect(() => { + console.log("🔍 DateInputComponent useEffect 실행:", { + componentId: component.id, + finalAutoGeneration, + enabled: finalAutoGeneration?.enabled, + type: finalAutoGeneration?.type, + isInteractive, + isDesignMode, + hasOnFormDataChange: !!onFormDataChange, + columnName: component.columnName, + currentFormValue: formData?.[component.columnName || ""], + }); + + // 🧪 테스트: 자동생성이 활성화되어 있으면 무조건 현재 날짜 설정 + if (finalAutoGeneration?.enabled) { + const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD + console.log("🧪 테스트용 날짜 생성:", today); + + setAutoGeneratedValue(today); + + // 인터랙티브 모드에서 폼 데이터에도 설정 + if (isInteractive && onFormDataChange && component.columnName) { + console.log("📤 테스트용 폼 데이터 업데이트:", component.columnName, today); + onFormDataChange(component.columnName, today); + } + } + + // 원래 자동생성 로직 (주석 처리) + /* + if (finalAutoGeneration?.enabled && finalAutoGeneration.type !== "none") { + const fieldName = component.columnName || component.id; + const generatedValue = AutoGenerationUtils.generateValue(finalAutoGeneration, fieldName); + + console.log("🎯 DateInputComponent 자동생성 시도:", { + componentId: component.id, + fieldName, + type: finalAutoGeneration.type, + options: finalAutoGeneration.options, + generatedValue, + isInteractive, + isDesignMode, + }); + + if (generatedValue) { + console.log("✅ DateInputComponent 자동생성 성공:", generatedValue); + setAutoGeneratedValue(generatedValue); + + // 인터랙티브 모드에서 폼 데이터 업데이트 + if (isInteractive && onFormDataChange && component.columnName) { + const currentValue = formData?.[component.columnName]; + if (!currentValue) { + console.log("📤 DateInputComponent -> onFormDataChange 호출:", component.columnName, generatedValue); + onFormDataChange(component.columnName, generatedValue); + } else { + console.log("⚠️ DateInputComponent 기존 값이 있어서 자동생성 스킵:", currentValue); + } + } else { + console.log("⚠️ DateInputComponent 자동생성 조건 불만족:", { + isInteractive, + hasOnFormDataChange: !!onFormDataChange, + hasColumnName: !!component.columnName, + }); + } + } else { + console.log("❌ DateInputComponent 자동생성 실패: generatedValue가 null"); + } + } else { + console.log("⚠️ DateInputComponent 자동생성 비활성화:", { + enabled: finalAutoGeneration?.enabled, + type: finalAutoGeneration?.type, + }); + } + */ + }, [ + finalAutoGeneration?.enabled, + finalAutoGeneration?.type, + finalAutoGeneration?.options, + component.id, + component.columnName, + isInteractive, + ]); + // 날짜 값 계산 및 디버깅 const fieldName = component.columnName || component.id; - const rawValue = - externalValue !== undefined - ? externalValue - : isInteractive && formData && component.columnName - ? formData[component.columnName] - : component.value; + + // 값 우선순위: externalValue > formData > autoGeneratedValue > component.value + let rawValue: any; + if (externalValue !== undefined) { + rawValue = externalValue; + } else if (isInteractive && formData && component.columnName && formData[component.columnName]) { + rawValue = formData[component.columnName]; + } else if (autoGeneratedValue) { + rawValue = autoGeneratedValue; + } else { + rawValue = component.value; + } console.log("🔍 DateInputComponent 값 디버깅:", { componentId: component.id, @@ -196,10 +318,14 @@ export const DateInputComponent: React.FC = ({ = ({ - config, - onChange, -}) => { +export const DateInputConfigPanel: React.FC = ({ config, onChange }) => { const handleChange = (key: keyof DateInputConfig, value: any) => { + console.log("🔧 DateInputConfigPanel.handleChange:", { key, value }); onChange({ [key]: value }); }; return (
-
- date-input 설정 -
+
date-input 설정
- {/* date 관련 설정 */} + {/* date 관련 설정 */}
= ({ onCheckedChange={(checked) => handleChange("readonly", checked)} />
+ + {/* 숨김 기능 */} +
+ + handleChange("hidden", checked)} + /> +

편집기에서는 연하게 보이지만 실제 화면에서는 숨겨집니다

+
+ + {/* 자동생성 기능 */} +
+
+ + { + const newAutoGeneration: AutoGenerationConfig = { + ...config.autoGeneration, + enabled: checked as boolean, + type: config.autoGeneration?.type || "current_time", + }; + handleChange("autoGeneration", newAutoGeneration); + }} + /> +
+ + {config.autoGeneration?.enabled && ( + <> +
+ + +

+ {AutoGenerationUtils.getTypeDescription(config.autoGeneration?.type || "current_time")} +

+
+ + {config.autoGeneration?.type === "current_time" && ( +
+ + +
+ )} + + {(config.autoGeneration?.type === "sequence" || + config.autoGeneration?.type === "random_string" || + config.autoGeneration?.type === "random_number") && ( + <> + {config.autoGeneration?.type === "sequence" && ( +
+ + { + const newAutoGeneration: AutoGenerationConfig = { + ...config.autoGeneration!, + options: { + ...config.autoGeneration?.options, + startValue: parseInt(e.target.value) || 1, + }, + }; + handleChange("autoGeneration", newAutoGeneration); + }} + /> +
+ )} + + {(config.autoGeneration?.type === "random_string" || + config.autoGeneration?.type === "random_number") && ( +
+ + { + const newAutoGeneration: AutoGenerationConfig = { + ...config.autoGeneration!, + options: { + ...config.autoGeneration?.options, + length: parseInt(e.target.value) || 8, + }, + }; + handleChange("autoGeneration", newAutoGeneration); + }} + /> +
+ )} + +
+
+ + { + const newAutoGeneration: AutoGenerationConfig = { + ...config.autoGeneration!, + options: { + ...config.autoGeneration?.options, + prefix: e.target.value, + }, + }; + handleChange("autoGeneration", newAutoGeneration); + }} + /> +
+
+ + { + const newAutoGeneration: AutoGenerationConfig = { + ...config.autoGeneration!, + options: { + ...config.autoGeneration?.options, + suffix: e.target.value, + }, + }; + handleChange("autoGeneration", newAutoGeneration); + }} + /> +
+
+ + )} + +
+ 미리보기: {AutoGenerationUtils.generatePreviewValue(config.autoGeneration)} +
+ + )} +
); }; diff --git a/frontend/lib/registry/components/date-input/DateInputRenderer.tsx b/frontend/lib/registry/components/date-input/DateInputRenderer.tsx index b5438224..1f93cef3 100644 --- a/frontend/lib/registry/components/date-input/DateInputRenderer.tsx +++ b/frontend/lib/registry/components/date-input/DateInputRenderer.tsx @@ -13,17 +13,24 @@ export class DateInputRenderer extends AutoRegisteringComponentRenderer { static componentDefinition = DateInputDefinition; render(): React.ReactElement { + console.log("🎯 DateInputRenderer.render() 호출:", { + componentId: this.props.component?.id, + autoGeneration: this.props.autoGeneration, + componentAutoGeneration: this.props.component?.autoGeneration, + allProps: Object.keys(this.props), + }); + return ; } /** * 컴포넌트별 특화 메서드들 */ - + // date 타입 특화 속성 처리 protected getDateInputProps() { const baseProps = this.getWebTypeProps(); - + // date 타입에 특화된 추가 속성들 return { ...baseProps, diff --git a/frontend/lib/registry/components/date-input/index.ts b/frontend/lib/registry/components/date-input/index.ts index 56bc5d57..639e28cc 100644 --- a/frontend/lib/registry/components/date-input/index.ts +++ b/frontend/lib/registry/components/date-input/index.ts @@ -4,7 +4,7 @@ import React from "react"; import { createComponentDefinition } from "../../utils/createComponentDefinition"; import { ComponentCategory } from "@/types/component"; import type { WebType } from "@/types/screen"; -import { DateInputWrapper } from "./DateInputComponent"; +import { DateInputComponent } from "./DateInputComponent"; import { DateInputConfigPanel } from "./DateInputConfigPanel"; import { DateInputConfig } from "./types"; @@ -19,7 +19,7 @@ export const DateInputDefinition = createComponentDefinition({ description: "날짜 선택을 위한 날짜 선택기 컴포넌트", category: ComponentCategory.INPUT, webType: "date", - component: DateInputWrapper, + component: DateInputComponent, defaultConfig: { placeholder: "입력하세요", }, diff --git a/frontend/lib/registry/components/date-input/types.ts b/frontend/lib/registry/components/date-input/types.ts index c263eb6b..df6bc4eb 100644 --- a/frontend/lib/registry/components/date-input/types.ts +++ b/frontend/lib/registry/components/date-input/types.ts @@ -1,25 +1,29 @@ "use client"; import { ComponentConfig } from "@/types/component"; +import { AutoGenerationConfig } from "@/types/screen"; /** * DateInput 컴포넌트 설정 타입 */ export interface DateInputConfig extends ComponentConfig { - // date 관련 설정 + // date 관련 설정 placeholder?: string; - + // 공통 설정 disabled?: boolean; required?: boolean; readonly?: boolean; - placeholder?: string; helperText?: string; - + + // 자동생성 및 숨김 기능 + autoGeneration?: AutoGenerationConfig; + hidden?: boolean; + // 스타일 관련 variant?: "default" | "outlined" | "filled"; size?: "sm" | "md" | "lg"; - + // 이벤트 관련 onChange?: (value: any) => void; onFocus?: () => void; @@ -37,7 +41,7 @@ export interface DateInputProps { config?: DateInputConfig; className?: string; style?: React.CSSProperties; - + // 이벤트 핸들러 onChange?: (value: any) => void; onFocus?: () => void; diff --git a/frontend/lib/registry/components/number-input/NumberInputComponent.tsx b/frontend/lib/registry/components/number-input/NumberInputComponent.tsx index a202334a..154e601f 100644 --- a/frontend/lib/registry/components/number-input/NumberInputComponent.tsx +++ b/frontend/lib/registry/components/number-input/NumberInputComponent.tsx @@ -1,9 +1,10 @@ "use client"; -import React from "react"; -import { ComponentRendererProps } from "@/types/component"; +import React, { useEffect, useState } from "react"; +import { ComponentRendererProps, AutoGenerationConfig } from "@/types/component"; import { NumberInputConfig } from "./types"; import { filterDOMProps } from "@/lib/utils/domPropsFilter"; +import { AutoGenerationUtils } from "@/lib/utils/autoGeneration"; export interface NumberInputComponentProps extends ComponentRendererProps { config?: NumberInputConfig; diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index f272b71e..6bf59d30 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -539,7 +539,7 @@ export const TableListComponent: React.FC = ({ } }, [refreshKey]); - // 표시할 컬럼 계산 (Entity 조인 적용됨 + 체크박스 컬럼 추가) + // 표시할 컬럼 계산 (Entity 조인 적용됨 + 체크박스 컬럼 추가 + 숨김 기능) const visibleColumns = useMemo(() => { // 기본값 처리: checkbox 설정이 없으면 기본값 사용 const checkboxConfig = tableConfig.checkbox || { @@ -554,9 +554,27 @@ export const TableListComponent: React.FC = ({ if (!displayColumns || displayColumns.length === 0) { // displayColumns가 아직 설정되지 않은 경우 기본 컬럼 사용 if (!tableConfig.columns) return []; - columns = tableConfig.columns.filter((col) => col.visible).sort((a, b) => a.order - b.order); + columns = tableConfig.columns + .filter((col) => { + // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 + if (isDesignMode) { + return col.visible; // 디자인 모드에서는 visible만 체크 + } else { + return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 + } + }) + .sort((a, b) => a.order - b.order); } else { - columns = displayColumns.filter((col) => col.visible).sort((a, b) => a.order - b.order); + columns = displayColumns + .filter((col) => { + // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 + if (isDesignMode) { + return col.visible; // 디자인 모드에서는 visible만 체크 + } else { + return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 + } + }) + .sort((a, b) => a.order - b.order); } // 체크박스가 활성화된 경우 체크박스 컬럼을 추가 @@ -876,6 +894,8 @@ export const TableListComponent: React.FC = ({ : "h-12 cursor-pointer border-b px-4 py-3 text-left align-middle font-medium whitespace-nowrap text-gray-900 select-none", `text-${column.align}`, column.sortable && "hover:bg-gray-50", + // 숨김 컬럼 스타일 (디자인 모드에서만) + isDesignMode && column.hidden && "bg-gray-100/50 opacity-40", )} style={{ minWidth: `${getColumnWidth(column)}px`, @@ -963,13 +983,22 @@ export const TableListComponent: React.FC = ({ {columnsByPosition.normal.map((column) => ( column.sortable && handleSort(column.columnName)} > @@ -1056,6 +1085,8 @@ export const TableListComponent: React.FC = ({ : "h-12 cursor-pointer border-b px-4 py-3 text-left align-middle font-medium whitespace-nowrap text-gray-900 select-none", `text-${column.align}`, column.sortable && "hover:bg-gray-50", + // 숨김 컬럼 스타일 (디자인 모드에서만) + isDesignMode && column.hidden && "bg-gray-100/50 opacity-40", )} style={{ minWidth: `${getColumnWidth(column)}px`, diff --git a/frontend/lib/registry/components/table-list/types.ts b/frontend/lib/registry/components/table-list/types.ts index 42dbd7d3..2d6d9aee 100644 --- a/frontend/lib/registry/components/table-list/types.ts +++ b/frontend/lib/registry/components/table-list/types.ts @@ -11,6 +11,35 @@ export interface EntityJoinInfo { joinAlias: string; } +/** + * 자동생성 타입 정의 + */ +export type AutoGenerationType = + | "uuid" // UUID 생성 + | "current_user" // 현재 사용자 ID + | "current_time" // 현재 시간 + | "sequence" // 시퀀스 번호 + | "random_string" // 랜덤 문자열 + | "random_number" // 랜덤 숫자 + | "company_code" // 회사 코드 + | "department" // 부서 코드 + | "none"; // 자동생성 없음 + +/** + * 자동생성 설정 + */ +export interface AutoGenerationConfig { + type: AutoGenerationType; + enabled: boolean; + options?: { + length?: number; // 랜덤 문자열/숫자 길이 + prefix?: string; // 접두사 + suffix?: string; // 접미사 + format?: string; // 시간 형식 (current_time용) + startValue?: number; // 시퀀스 시작값 + }; +} + /** * 테이블 컬럼 설정 */ @@ -31,6 +60,10 @@ export interface ColumnConfig { // 컬럼 고정 관련 속성 fixed?: "left" | "right" | false; // 컬럼 고정 위치 (왼쪽, 오른쪽, 고정 안함) fixedOrder?: number; // 고정된 컬럼들 내에서의 순서 + + // 새로운 기능들 + hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김) + autoGeneration?: AutoGenerationConfig; // 자동생성 설정 } /** diff --git a/frontend/lib/registry/components/text-input/TextInputComponent.tsx b/frontend/lib/registry/components/text-input/TextInputComponent.tsx index 60e5c2ef..4a5aabf6 100644 --- a/frontend/lib/registry/components/text-input/TextInputComponent.tsx +++ b/frontend/lib/registry/components/text-input/TextInputComponent.tsx @@ -1,9 +1,11 @@ "use client"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { ComponentRendererProps } from "@/types/component"; +import { AutoGenerationConfig } from "@/types/screen"; import { TextInputConfig } from "./types"; import { filterDOMProps } from "@/lib/utils/domPropsFilter"; +import { AutoGenerationUtils } from "@/lib/utils/autoGeneration"; export interface TextInputComponentProps extends ComponentRendererProps { config?: TextInputConfig; @@ -34,18 +36,112 @@ export const TextInputComponent: React.FC = ({ ...component.config, } as TextInputConfig; + // 자동생성 설정 (props에서 전달받은 값 우선 사용) + const autoGeneration: AutoGenerationConfig = props.autoGeneration || + component.autoGeneration || + componentConfig.autoGeneration || { + type: "none", + enabled: false, + }; + + // 숨김 상태 (props에서 전달받은 값 우선 사용) + const isHidden = props.hidden !== undefined ? props.hidden : component.hidden || componentConfig.hidden || false; + + // 자동생성된 값 상태 + const [autoGeneratedValue, setAutoGeneratedValue] = useState(""); + + // 테스트용: 컴포넌트 라벨에 "test"가 포함되면 강제로 UUID 자동생성 활성화 + const testAutoGeneration = component.label?.toLowerCase().includes("test") + ? { + type: "uuid" as AutoGenerationType, + enabled: true, + } + : autoGeneration; + console.log("🔧 텍스트 입력 컴포넌트 설정:", { config, componentConfig, component: component, + autoGeneration, + testAutoGeneration, + isTestMode: component.label?.toLowerCase().includes("test"), + isHidden, + isInteractive, + formData, + columnName: component.columnName, + currentFormValue: formData?.[component.columnName], + componentValue: component.value, + autoGeneratedValue, }); + // 자동생성 값 생성 (컴포넌트 마운트 시 또는 폼 데이터 변경 시) + useEffect(() => { + console.log("🔄 자동생성 useEffect 실행:", { + enabled: testAutoGeneration.enabled, + type: testAutoGeneration.type, + isInteractive, + columnName: component.columnName, + hasFormData: !!formData, + hasOnFormDataChange: !!onFormDataChange, + }); + + if (testAutoGeneration.enabled && testAutoGeneration.type !== "none") { + // 폼 데이터에 이미 값이 있으면 자동생성하지 않음 + const currentFormValue = formData?.[component.columnName]; + const currentComponentValue = component.value; + + console.log("🔍 자동생성 조건 확인:", { + currentFormValue, + currentComponentValue, + hasCurrentValue: !!(currentFormValue || currentComponentValue), + autoGeneratedValue, + }); + + // 자동생성된 값이 없고, 현재 값도 없을 때만 생성 + if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) { + const generatedValue = AutoGenerationUtils.generateValue(testAutoGeneration, component.columnName); + console.log("✨ 자동생성된 값:", generatedValue); + + if (generatedValue) { + setAutoGeneratedValue(generatedValue); + + // 폼 데이터에 자동생성된 값 설정 (인터랙티브 모드에서만) + if (isInteractive && onFormDataChange && component.columnName) { + console.log("📝 폼 데이터에 자동생성 값 설정:", { + columnName: component.columnName, + value: generatedValue, + }); + onFormDataChange(component.columnName, generatedValue); + } + } + } else if (!autoGeneratedValue && testAutoGeneration.type !== "none") { + // 디자인 모드에서도 미리보기용 자동생성 값 표시 + const previewValue = AutoGenerationUtils.generatePreviewValue(testAutoGeneration); + console.log("🎨 디자인 모드 미리보기 값:", previewValue); + setAutoGeneratedValue(previewValue); + } else { + console.log("⏭️ 이미 값이 있어서 자동생성 건너뜀:", { + hasAutoGenerated: !!autoGeneratedValue, + hasFormValue: !!currentFormValue, + hasComponentValue: !!currentComponentValue, + }); + } + } + }, [testAutoGeneration, isInteractive, component.columnName, component.value, formData, onFormDataChange]); + // 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외) const componentStyle: React.CSSProperties = { width: "100%", height: "100%", ...component.style, ...style, + // 숨김 기능: 디자인 모드에서는 연하게, 실제 화면에서는 완전히 숨김 + ...(isHidden && { + opacity: isDesignMode ? 0.4 : 0, + backgroundColor: isDesignMode ? "#f3f4f6" : "transparent", + pointerEvents: isDesignMode ? "auto" : "none", + display: isDesignMode ? "block" : "none", + }), }; // 디자인 모드 스타일 @@ -105,15 +201,37 @@ export const TextInputComponent: React.FC = ({ { + let displayValue = ""; + + if (isInteractive && formData && component.columnName) { + // 인터랙티브 모드: formData 우선, 없으면 자동생성 값 + displayValue = formData[component.columnName] || autoGeneratedValue || ""; + } else { + // 디자인 모드: component.value 우선, 없으면 자동생성 값 + displayValue = component.value || autoGeneratedValue || ""; + } + + console.log("📄 Input 값 계산:", { + isInteractive, + hasFormData: !!formData, + columnName: component.columnName, + formDataValue: formData?.[component.columnName], + componentValue: component.value, + autoGeneratedValue, + finalDisplayValue: displayValue, + }); + + return displayValue; + })()} + placeholder={ + testAutoGeneration.enabled && testAutoGeneration.type !== "none" + ? `자동생성: ${AutoGenerationUtils.getTypeDescription(testAutoGeneration.type)}` + : componentConfig.placeholder || "" } - placeholder={componentConfig.placeholder || ""} disabled={componentConfig.disabled || false} required={componentConfig.required || false} - readOnly={componentConfig.readonly || false} + readOnly={componentConfig.readonly || (testAutoGeneration.enabled && testAutoGeneration.type !== "none")} style={{ width: "100%", height: "100%", diff --git a/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx b/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx index 910e2b91..0e14e8c6 100644 --- a/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx +++ b/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx @@ -6,6 +6,8 @@ import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { TextInputConfig } from "./types"; +import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen"; +import { AutoGenerationUtils } from "@/lib/utils/autoGeneration"; export interface TextInputConfigPanelProps { config: TextInputConfig; @@ -16,21 +18,16 @@ export interface TextInputConfigPanelProps { * TextInput 설정 패널 * 컴포넌트의 설정값들을 편집할 수 있는 UI 제공 */ -export const TextInputConfigPanel: React.FC = ({ - config, - onChange, -}) => { +export const TextInputConfigPanel: React.FC = ({ config, onChange }) => { const handleChange = (key: keyof TextInputConfig, value: any) => { onChange({ [key]: value }); }; return (
-
- text-input 설정 -
+
text-input 설정
- {/* 텍스트 관련 설정 */} + {/* 텍스트 관련 설정 */}
= ({ onCheckedChange={(checked) => handleChange("readonly", checked)} />
+ + {/* 구분선 */} +
+
고급 기능
+ + {/* 숨김 기능 */} +
+ + handleChange("hidden", checked)} + /> +
+ + {/* 자동생성 기능 */} +
+ + { + const currentConfig = config.autoGeneration || { type: "none", enabled: false }; + handleChange("autoGeneration", { + ...currentConfig, + enabled: checked as boolean, + }); + }} + /> +
+ + {/* 자동생성 타입 선택 */} + {config.autoGeneration?.enabled && ( +
+ + + + {/* 선택된 타입 설명 */} + {config.autoGeneration?.type && config.autoGeneration.type !== "none" && ( +
+ {AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)} +
+ )} +
+ )} + + {/* 자동생성 옵션 */} + {config.autoGeneration?.enabled && + config.autoGeneration?.type && + ["random_string", "random_number", "sequence"].includes(config.autoGeneration.type) && ( +
+ + + {/* 길이 설정 (랜덤 문자열/숫자용) */} + {["random_string", "random_number"].includes(config.autoGeneration.type) && ( +
+ + { + const currentConfig = config.autoGeneration!; + handleChange("autoGeneration", { + ...currentConfig, + options: { + ...currentConfig.options, + length: parseInt(e.target.value) || 8, + }, + }); + }} + /> +
+ )} + + {/* 접두사 */} +
+ + { + const currentConfig = config.autoGeneration!; + handleChange("autoGeneration", { + ...currentConfig, + options: { + ...currentConfig.options, + prefix: e.target.value, + }, + }); + }} + /> +
+ + {/* 접미사 */} +
+ + { + const currentConfig = config.autoGeneration!; + handleChange("autoGeneration", { + ...currentConfig, + options: { + ...currentConfig.options, + suffix: e.target.value, + }, + }); + }} + /> +
+ + {/* 미리보기 */} +
+ +
+ {AutoGenerationUtils.generatePreviewValue(config.autoGeneration)} +
+
+
+ )} +
); }; diff --git a/frontend/lib/registry/components/text-input/types.ts b/frontend/lib/registry/components/text-input/types.ts index 20dde3eb..1d9499bc 100644 --- a/frontend/lib/registry/components/text-input/types.ts +++ b/frontend/lib/registry/components/text-input/types.ts @@ -1,32 +1,37 @@ "use client"; import { ComponentConfig } from "@/types/component"; +import { AutoGenerationConfig } from "@/types/screen"; /** * TextInput 컴포넌트 설정 타입 */ export interface TextInputConfig extends ComponentConfig { - // 텍스트 관련 설정 + // 텍스트 관련 설정 placeholder?: string; maxLength?: number; minLength?: number; - + // 공통 설정 disabled?: boolean; required?: boolean; readonly?: boolean; placeholder?: string; helperText?: string; - + // 스타일 관련 variant?: "default" | "outlined" | "filled"; size?: "sm" | "md" | "lg"; - + // 이벤트 관련 onChange?: (value: any) => void; onFocus?: () => void; onBlur?: () => void; onClick?: () => void; + + // 새로운 기능들 + hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김) + autoGeneration?: AutoGenerationConfig; // 자동생성 설정 } /** @@ -39,7 +44,7 @@ export interface TextInputProps { config?: TextInputConfig; className?: string; style?: React.CSSProperties; - + // 이벤트 핸들러 onChange?: (value: any) => void; onFocus?: () => void; diff --git a/frontend/lib/services/optimizedButtonDataflowService.ts b/frontend/lib/services/optimizedButtonDataflowService.ts index a2a0e2c9..950ab060 100644 --- a/frontend/lib/services/optimizedButtonDataflowService.ts +++ b/frontend/lib/services/optimizedButtonDataflowService.ts @@ -27,6 +27,33 @@ export interface QuickValidationResult { success: boolean; message?: string; canExecuteImmediately: boolean; + actions?: any[]; // 조건 만족 시 실행할 액션들 +} + +/** + * 제어 데이터 소스 타입 + */ +export type ControlDataSource = "form" | "table-selection" | "both"; + +/** + * 확장된 제어 컨텍스트 + */ +export interface ExtendedControlContext { + // 기존 폼 데이터 + formData: Record; + + // 테이블 선택 데이터 + selectedRows?: any[]; + selectedRowsData?: any[]; + + // 제어 데이터 소스 타입 + controlDataSource: ControlDataSource; + + // 기타 컨텍스트 + buttonId: string; + componentData?: any; + timestamp: string; + clickCount?: number; } /** @@ -289,7 +316,7 @@ export class OptimizedButtonDataflowService { } /** - * 🔥 빠른 검증 (메모리에서 즉시 처리) + * 🔥 빠른 검증 (메모리에서 즉시 처리) - 확장된 버전 */ private static async executeQuickValidation( config: ButtonDataflowConfig, @@ -326,6 +353,221 @@ export class OptimizedButtonDataflowService { }; } + /** + * 🔥 확장된 조건 검증 (폼 + 테이블 선택 데이터) + */ + static async executeExtendedValidation( + config: ButtonDataflowConfig, + context: ExtendedControlContext, + ): Promise { + console.log("🔍 executeExtendedValidation 시작:", { + controlMode: config.controlMode, + controlDataSource: context.controlDataSource, + selectedRowsData: context.selectedRowsData, + formData: context.formData, + directControlConditions: config.directControl?.conditions, + selectedDiagramId: config.selectedDiagramId, + selectedRelationshipId: config.selectedRelationshipId, + fullConfig: config, + }); + + // 🔥 simple 모드에서도 조건 검증을 수행해야 함 + // if (config.controlMode === "simple") { + // return { + // success: true, + // canExecuteImmediately: true, + // }; + // } + + let conditions = config.directControl?.conditions || []; + + // 🔥 관계도 방식인 경우 실제 API에서 조건 가져오기 + if (conditions.length === 0 && config.selectedDiagramId && config.selectedRelationshipId) { + console.log("🔍 관계도 방식에서 실제 조건 로딩:", { + selectedDiagramId: config.selectedDiagramId, + selectedRelationshipId: config.selectedRelationshipId, + }); + + try { + // 관계도에서 실제 조건을 가져오는 API 호출 + const response = await apiClient.get( + `/test-button-dataflow/diagrams/${config.selectedDiagramId}/relationships/${config.selectedRelationshipId}/preview`, + ); + + if (response.data.success && response.data.data) { + const previewData = response.data.data; + + // control.conditions에서 실제 조건 추출 + if (previewData.control && previewData.control.conditions) { + conditions = previewData.control.conditions.filter((cond: any) => cond.type === "condition"); + console.log("✅ 관계도에서 control 조건 로딩 성공:", { + totalConditions: previewData.control.conditions.length, + filteredConditions: conditions.length, + conditions, + }); + } else { + console.warn("⚠️ 관계도 control에 조건이 설정되지 않았습니다:", previewData); + } + + // 🔧 control.conditions가 비어있으면 plan.actions[].conditions에서 조건 추출 + if (conditions.length === 0 && previewData.plan && previewData.plan.actions) { + console.log("🔍 control 조건이 없어서 액션별 조건 확인:", previewData.plan.actions); + + previewData.plan.actions.forEach((action: any, index: number) => { + if (action.conditions && Array.isArray(action.conditions) && action.conditions.length > 0) { + conditions.push(...action.conditions); + console.log( + `✅ 액션 ${index + 1}(${action.name})에서 조건 ${action.conditions.length}개 로딩:`, + action.conditions, + ); + } + }); + + if (conditions.length > 0) { + console.log("✅ 총 액션별 조건 로딩 성공:", { + totalConditions: conditions.length, + conditions: conditions, + }); + } + } + + // plan.actions에서 실제 액션도 저장 (조건 만족 시 실행용) + if (previewData.plan && previewData.plan.actions) { + // config에 액션 정보 임시 저장 + (config as any)._relationshipActions = previewData.plan.actions; + console.log("✅ 관계도에서 실제 액션 로딩 성공:", { + totalActions: previewData.plan.actions.length, + actions: previewData.plan.actions, + }); + } else { + console.warn("⚠️ 관계도에 액션이 설정되지 않았습니다:", { + hasPlan: !!previewData.plan, + planActions: previewData.plan?.actions, + fullPreviewData: previewData, + }); + } + } else { + console.warn("⚠️ 관계도 API 응답이 올바르지 않습니다:", response.data); + } + } catch (error) { + console.error("❌ 관계도에서 조건 로딩 실패:", error); + + // API 실패 시 사용자에게 명확한 안내 + return { + success: false, + message: "관계도에서 조건을 불러올 수 없습니다. 관계도 설정을 확인해주세요.", + canExecuteImmediately: true, + }; + } + } + + // 🔥 여전히 조건이 없으면 경고 + if (conditions.length === 0) { + console.warn("⚠️ 제어 조건이 설정되지 않았습니다."); + return { + success: false, + message: "제어 조건이 설정되지 않았습니다. 관계도에서 관계를 선택하거나 직접 조건을 추가해주세요.", + canExecuteImmediately: true, + }; + } + + console.log("🔍 조건 검증 시작:", conditions); + + for (const condition of conditions) { + if (condition.type === "condition") { + let dataToCheck: Record = {}; + + // 제어 데이터 소스에 따라 검증할 데이터 결정 + console.log("🔍 제어 데이터 소스 확인:", { + controlDataSource: context.controlDataSource, + hasFormData: Object.keys(context.formData).length > 0, + hasSelectedRowsData: context.selectedRowsData && context.selectedRowsData.length > 0, + selectedRowsData: context.selectedRowsData, + }); + + switch (context.controlDataSource) { + case "form": + dataToCheck = context.formData; + console.log("📝 폼 데이터 사용:", dataToCheck); + break; + + case "table-selection": + // 선택된 첫 번째 행의 데이터 사용 (다중 선택 시) + if (context.selectedRowsData && context.selectedRowsData.length > 0) { + dataToCheck = context.selectedRowsData[0]; + console.log("📋 테이블 선택 데이터 사용:", dataToCheck); + } else { + console.warn("⚠️ 테이블에서 항목이 선택되지 않음"); + return { + success: false, + message: "테이블에서 항목을 선택해주세요.", + canExecuteImmediately: true, + }; + } + break; + + case "both": + // 폼 데이터와 선택된 데이터 모두 병합 + dataToCheck = { + ...context.formData, + ...(context.selectedRowsData?.[0] || {}), + }; + console.log("🔄 폼 + 테이블 데이터 병합 사용:", dataToCheck); + break; + + default: + // 기본값이 없는 경우 자동 판단 + if (context.selectedRowsData && context.selectedRowsData.length > 0) { + dataToCheck = context.selectedRowsData[0]; + console.log("🔄 자동 판단: 테이블 선택 데이터 사용:", dataToCheck); + } else { + dataToCheck = context.formData; + console.log("🔄 자동 판단: 폼 데이터 사용:", dataToCheck); + } + break; + } + + const fieldValue = dataToCheck[condition.field!]; + const isValid = this.evaluateSimpleCondition(fieldValue, condition.operator!, condition.value); + + console.log("🔍 조건 검증 결과:", { + field: condition.field, + operator: condition.operator, + expectedValue: condition.value, + actualValue: fieldValue, + isValid, + dataToCheck, + }); + + if (!isValid) { + const sourceLabel = + context.controlDataSource === "form" + ? "폼" + : context.controlDataSource === "table-selection" + ? "선택된 항목" + : "데이터"; + + const actualValueMsg = fieldValue !== undefined ? ` (실제값: ${fieldValue})` : " (값 없음)"; + + return { + success: false, + message: `${sourceLabel} 조건 불만족: ${condition.field} ${condition.operator} ${condition.value}${actualValueMsg}`, + canExecuteImmediately: true, + }; + } + } + } + + // 🔥 모든 조건을 만족했으므로 액션 정보도 함께 반환 + const relationshipActions = (config as any)._relationshipActions || []; + + return { + success: true, + canExecuteImmediately: true, + actions: relationshipActions, + }; + } + /** * 🔥 단순 조건 평가 (메모리에서 즉시) */ diff --git a/frontend/lib/utils/autoGeneration.ts b/frontend/lib/utils/autoGeneration.ts new file mode 100644 index 00000000..d7f67f3b --- /dev/null +++ b/frontend/lib/utils/autoGeneration.ts @@ -0,0 +1,292 @@ +"use client"; + +import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen"; + +/** + * 자동생성 값 생성 유틸리티 + */ +export class AutoGenerationUtils { + /** + * UUID 생성 + */ + static generateUUID(): string { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } + + /** + * 랜덤 문자열 생성 + */ + static generateRandomString(length: number = 8, prefix?: string, suffix?: string): string { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return `${prefix || ""}${result}${suffix || ""}`; + } + + /** + * 랜덤 숫자 생성 + */ + static generateRandomNumber(length: number = 6, prefix?: string, suffix?: string): string { + const min = Math.pow(10, length - 1); + const max = Math.pow(10, length) - 1; + const randomNum = Math.floor(Math.random() * (max - min + 1)) + min; + + return `${prefix || ""}${randomNum}${suffix || ""}`; + } + + /** + * 현재 시간 생성 + */ + static generateCurrentTime(format?: string): string { + console.log("🕒 generateCurrentTime 호출됨:", { format }); + const now = new Date(); + console.log("🕒 현재 시간 객체:", now); + + let result: string; + switch (format) { + case "date": + result = now.toISOString().split("T")[0]; // YYYY-MM-DD + break; + case "time": + result = now.toTimeString().split(" ")[0]; // HH:mm:ss + break; + case "datetime": + result = now.toISOString().replace("T", " ").split(".")[0]; // YYYY-MM-DD HH:mm:ss + break; + case "timestamp": + result = now.getTime().toString(); + break; + default: + result = now.toISOString(); // ISO 8601 format + break; + } + + console.log("🕒 generateCurrentTime 결과:", { format, result }); + return result; + } + + /** + * 시퀀스 번호 생성 (메모리 기반, 실제로는 DB 시퀀스 사용 권장) + */ + private static sequenceCounters: Map = new Map(); + + static generateSequence(key: string = "default", startValue: number = 1, prefix?: string, suffix?: string): string { + if (!this.sequenceCounters.has(key)) { + this.sequenceCounters.set(key, startValue); + } + + const current = this.sequenceCounters.get(key)!; + this.sequenceCounters.set(key, current + 1); + + return `${prefix || ""}${current}${suffix || ""}`; + } + + /** + * 현재 사용자 ID 가져오기 (실제로는 인증 컨텍스트에서 가져와야 함) + */ + static getCurrentUserId(): string { + // TODO: 실제 인증 시스템과 연동 + if (typeof window !== "undefined") { + const userInfo = localStorage.getItem("userInfo"); + if (userInfo) { + try { + const parsed = JSON.parse(userInfo); + return parsed.userId || parsed.id || "unknown"; + } catch { + return "unknown"; + } + } + } + return "system"; + } + + /** + * 회사 코드 가져오기 + */ + static getCompanyCode(): string { + // TODO: 실제 회사 정보와 연동 + if (typeof window !== "undefined") { + const companyInfo = localStorage.getItem("companyInfo"); + if (companyInfo) { + try { + const parsed = JSON.parse(companyInfo); + return parsed.companyCode || parsed.code || "COMPANY"; + } catch { + return "COMPANY"; + } + } + } + return "COMPANY"; + } + + /** + * 부서 코드 가져오기 + */ + static getDepartmentCode(): string { + // TODO: 실제 부서 정보와 연동 + if (typeof window !== "undefined") { + const userInfo = localStorage.getItem("userInfo"); + if (userInfo) { + try { + const parsed = JSON.parse(userInfo); + return parsed.departmentCode || parsed.deptCode || "DEPT"; + } catch { + return "DEPT"; + } + } + } + return "DEPT"; + } + + /** + * 자동생성 값 생성 메인 함수 + */ + static generateValue(config: AutoGenerationConfig, columnName?: string): string | null { + console.log("🔧 AutoGenerationUtils.generateValue 호출:", { + config, + columnName, + enabled: config.enabled, + type: config.type, + }); + + if (!config.enabled || config.type === "none") { + console.log("⚠️ AutoGenerationUtils.generateValue 스킵:", { + enabled: config.enabled, + type: config.type, + }); + return null; + } + + const options = config.options || {}; + + switch (config.type) { + case "uuid": + return this.generateUUID(); + + case "current_user": + return this.getCurrentUserId(); + + case "current_time": + console.log("🕒 AutoGenerationUtils.generateCurrentTime 호출:", { + format: options.format, + options, + }); + const timeValue = this.generateCurrentTime(options.format); + console.log("🕒 AutoGenerationUtils.generateCurrentTime 결과:", timeValue); + return timeValue; + + case "sequence": + return this.generateSequence(columnName || "default", options.startValue || 1, options.prefix, options.suffix); + + case "random_string": + return this.generateRandomString(options.length || 8, options.prefix, options.suffix); + + case "random_number": + return this.generateRandomNumber(options.length || 6, options.prefix, options.suffix); + + case "company_code": + return this.getCompanyCode(); + + case "department": + return this.getDepartmentCode(); + + default: + console.warn(`Unknown auto generation type: ${config.type}`); + return null; + } + } + + /** + * 자동생성 타입별 설명 가져오기 + */ + static getTypeDescription(type: AutoGenerationType): string { + const descriptions: Record = { + uuid: "고유 식별자 (UUID) 생성", + current_user: "현재 로그인한 사용자 ID", + current_time: "현재 날짜/시간", + sequence: "순차적 번호 생성", + random_string: "랜덤 문자열 생성", + random_number: "랜덤 숫자 생성", + company_code: "현재 회사 코드", + department: "현재 부서 코드", + none: "자동생성 없음", + }; + + return descriptions[type] || "알 수 없는 타입"; + } + + /** + * 자동생성 미리보기 값 생성 + */ + static generatePreviewValue(config: AutoGenerationConfig): string { + if (!config.enabled || config.type === "none") { + return ""; + } + + // 미리보기용으로 실제 값 대신 예시 값 반환 + const options = config.options || {}; + + switch (config.type) { + case "uuid": + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"; + + case "current_user": + return "user123"; + + case "current_time": + return this.generateCurrentTime(options.format); + + case "sequence": + return `${options.prefix || ""}1${options.suffix || ""}`; + + case "random_string": + return `${options.prefix || ""}ABC123${options.suffix || ""}`; + + case "random_number": + return `${options.prefix || ""}123456${options.suffix || ""}`; + + case "company_code": + return "COMPANY"; + + case "department": + return "DEPT"; + + default: + return ""; + } + } +} + +// 개발 모드에서 전역 함수로 등록 (테스트용) +if (typeof window !== "undefined") { + (window as any).__AUTO_GENERATION_TEST__ = { + generateCurrentTime: AutoGenerationUtils.generateCurrentTime, + generateValue: AutoGenerationUtils.generateValue, + test: () => { + console.log("🧪 자동생성 테스트 시작"); + + // 현재 시간 테스트 + const dateResult = AutoGenerationUtils.generateCurrentTime("date"); + console.log("📅 날짜 생성 결과:", dateResult); + + // 자동생성 설정 테스트 + const config = { + enabled: true, + type: "current_time" as any, + options: { format: "date" }, + }; + + const autoResult = AutoGenerationUtils.generateValue(config); + console.log("🎯 자동생성 결과:", autoResult); + + return { dateResult, autoResult }; + }, + }; +} diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index e57b454e..0ca559a7 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -3,6 +3,7 @@ import { toast } from "sonner"; import { screenApi } from "@/lib/api/screen"; import { DynamicFormApi } from "@/lib/api/dynamicForm"; +import { OptimizedButtonDataflowService, ExtendedControlContext } from "@/lib/services/optimizedButtonDataflowService"; /** * 버튼 액션 타입 정의 @@ -46,6 +47,10 @@ export interface ButtonActionConfig { confirmMessage?: string; successMessage?: string; errorMessage?: string; + + // 제어관리 관련 + enableDataflowControl?: boolean; + dataflowConfig?: any; // ButtonDataflowConfig 타입 (순환 참조 방지를 위해 any 사용) } /** @@ -116,6 +121,9 @@ export class ButtonActionExecutor { case "close": return this.handleClose(config, context); + case "control": + return this.handleControl(config, context); + default: console.warn(`지원되지 않는 액션 타입: ${config.type}`); return false; @@ -712,6 +720,497 @@ export class ButtonActionExecutor { return true; } + /** + * 제어 전용 액션 처리 (조건 체크만 수행) + */ + private static async handleControl(config: ButtonActionConfig, context: ButtonActionContext): Promise { + console.log("🎯 ButtonActionExecutor.handleControl 실행:", { + formData: context.formData, + selectedRows: context.selectedRows, + selectedRowsData: context.selectedRowsData, + config, + }); + + // 🔥 제어 조건이 설정되어 있는지 확인 + console.log("🔍 제어관리 활성화 상태 확인:", { + enableDataflowControl: config.enableDataflowControl, + hasDataflowConfig: !!config.dataflowConfig, + dataflowConfig: config.dataflowConfig, + fullConfig: config, + }); + + if (!config.dataflowConfig || !config.enableDataflowControl) { + console.warn("⚠️ 제어관리가 비활성화되어 있습니다:", { + enableDataflowControl: config.enableDataflowControl, + hasDataflowConfig: !!config.dataflowConfig, + }); + toast.warning( + "제어관리가 활성화되지 않았습니다. 버튼 설정에서 '제어관리 활성화'를 체크하고 조건을 설정해주세요.", + ); + return false; + } + + try { + // 🔥 확장된 제어 컨텍스트 생성 + // 자동으로 적절한 controlDataSource 결정 + let controlDataSource = config.dataflowConfig.controlDataSource; + + if (!controlDataSource) { + // 설정이 없으면 자동 판단 + if (context.selectedRowsData && context.selectedRowsData.length > 0) { + controlDataSource = "table-selection"; + console.log("🔄 자동 판단: table-selection 모드 사용"); + } else if (context.formData && Object.keys(context.formData).length > 0) { + controlDataSource = "form"; + console.log("🔄 자동 판단: form 모드 사용"); + } else { + controlDataSource = "form"; // 기본값 + console.log("🔄 기본값: form 모드 사용"); + } + } + + const extendedContext: ExtendedControlContext = { + formData: context.formData || {}, + selectedRows: context.selectedRows || [], + selectedRowsData: context.selectedRowsData || [], + controlDataSource, + }; + + console.log("🔍 제어 조건 검증 시작:", { + dataflowConfig: config.dataflowConfig, + extendedContext, + }); + + // 🔥 실제 제어 조건 검증 수행 + const validationResult = await OptimizedButtonDataflowService.executeExtendedValidation( + config.dataflowConfig, + extendedContext, + ); + + if (validationResult.success) { + console.log("✅ 제어 조건 만족 - 액션 실행 시작:", { + actions: validationResult.actions, + context, + }); + + // 🔥 조건을 만족했으므로 실제 액션 실행 + if (validationResult.actions && validationResult.actions.length > 0) { + console.log("🚀 액션 실행 시작:", validationResult.actions); + await this.executeRelationshipActions(validationResult.actions, context); + } else { + console.warn("⚠️ 실행할 액션이 없습니다:", { + hasActions: !!validationResult.actions, + actionsLength: validationResult.actions?.length, + validationResult, + }); + toast.success(config.successMessage || "제어 조건을 만족합니다. (실행할 액션 없음)"); + } + + // 새로고침이 필요한 경우 + if (context.onRefresh) { + context.onRefresh(); + } + + return true; + } else { + toast.error(validationResult.message || "제어 조건을 만족하지 않습니다."); + return false; + } + } catch (error) { + console.error("제어 조건 검증 중 오류:", error); + toast.error("제어 조건 검증 중 오류가 발생했습니다."); + return false; + } + } + + /** + * 관계도에서 가져온 액션들을 실행 + */ + private static async executeRelationshipActions(actions: any[], context: ButtonActionContext): Promise { + console.log("🚀 관계도 액션 실행 시작:", actions); + + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + + try { + console.log(`🔄 액션 ${i + 1}/${actions.length} 실행:`, action); + + const actionType = action.actionType || action.type; // actionType 우선, type 폴백 + + switch (actionType) { + case "save": + await this.executeActionSave(action, context); + break; + case "update": + await this.executeActionUpdate(action, context); + break; + case "delete": + await this.executeActionDelete(action, context); + break; + case "insert": + await this.executeActionInsert(action, context); + break; + default: + console.warn(`❌ 지원되지 않는 액션 타입 (${i + 1}/${actions.length}):`, { + actionType, + actionName: action.name, + fullAction: action, + }); + // 지원되지 않는 액션은 오류로 처리하여 중단 + throw new Error(`지원되지 않는 액션 타입: ${actionType}`); + } + + console.log(`✅ 액션 ${i + 1}/${actions.length} 완료:`, action.name); + + // 성공 토스트 (개별 액션별) + toast.success(`${action.name || `액션 ${i + 1}`} 완료`); + } catch (error) { + const actionType = action.actionType || action.type; + console.error(`❌ 액션 ${i + 1}/${actions.length} 실행 실패:`, action.name, error); + + // 실패 토스트 + toast.error( + `${action.name || `액션 ${i + 1}`} 실행 실패: ${error instanceof Error ? error.message : "알 수 없는 오류"}`, + ); + + // 🚨 순차 실행 중단: 하나라도 실패하면 전체 중단 + throw new Error( + `액션 ${i + 1}(${action.name})에서 실패하여 제어 프로세스를 중단합니다: ${error instanceof Error ? error.message : error}`, + ); + } + } + + console.log("🎉 모든 액션 실행 완료!"); + toast.success(`총 ${actions.length}개 액션이 모두 성공적으로 완료되었습니다.`); + } + + /** + * 저장 액션 실행 + */ + private static async executeActionSave(action: any, context: ButtonActionContext): Promise { + console.log("💾 저장 액션 실행:", action); + console.log("🔍 액션 상세 정보:", JSON.stringify(action, null, 2)); + + // 🎯 필드 매핑 정보 사용하여 저장 데이터 구성 + let saveData: Record = {}; + + // 액션에 필드 매핑 정보가 있는지 확인 + if (action.fieldMappings && Array.isArray(action.fieldMappings)) { + console.log("📋 필드 매핑 정보 발견:", action.fieldMappings); + + // 필드 매핑에 따라 데이터 구성 + action.fieldMappings.forEach((mapping: any) => { + const { sourceField, targetField, defaultValue, valueType } = mapping; + + let value: any; + + // 값 소스에 따라 데이터 가져오기 + if (valueType === "form" && context.formData && sourceField) { + value = context.formData[sourceField]; + } else if (valueType === "selected" && context.selectedRowsData?.[0] && sourceField) { + value = context.selectedRowsData[0][sourceField]; + } else if (valueType === "default" || !sourceField) { + value = defaultValue; + } + + // 타겟 필드에 값 설정 + if (targetField && value !== undefined) { + saveData[targetField] = value; + console.log(`📝 필드 매핑: ${sourceField || "default"} -> ${targetField} = ${value}`); + } + }); + } else { + console.log("⚠️ 필드 매핑 정보가 없음, 기본 데이터 사용"); + // 폴백: 기존 방식 + saveData = { + ...context.formData, + ...context.selectedRowsData?.[0], // 선택된 데이터도 포함 + }; + } + + console.log("📊 최종 저장할 데이터:", saveData); + + try { + // 🔥 실제 저장 API 호출 + if (!context.tableName) { + throw new Error("테이블명이 설정되지 않았습니다."); + } + + const result = await DynamicFormApi.saveFormData({ + screenId: 0, // 임시값 + tableName: context.tableName, + data: saveData, + }); + + if (result.success) { + console.log("✅ 저장 성공:", result); + toast.success("데이터가 저장되었습니다."); + } else { + throw new Error(result.message || "저장 실패"); + } + } catch (error) { + console.error("❌ 저장 실패:", error); + toast.error(`저장 실패: ${error.message}`); + throw error; + } + } + + /** + * 업데이트 액션 실행 + */ + private static async executeActionUpdate(action: any, context: ButtonActionContext): Promise { + console.log("🔄 업데이트 액션 실행:", action); + console.log("🔍 액션 상세 정보:", JSON.stringify(action, null, 2)); + + // 🎯 필드 매핑 정보 사용하여 업데이트 데이터 구성 + let updateData: Record = {}; + + // 액션에 필드 매핑 정보가 있는지 확인 + if (action.fieldMappings && Array.isArray(action.fieldMappings)) { + console.log("📋 필드 매핑 정보 발견:", action.fieldMappings); + + // 🔑 먼저 선택된 데이터의 모든 필드를 기본으로 포함 (기본키 보존) + if (context.selectedRowsData?.[0]) { + updateData = { ...context.selectedRowsData[0] }; + console.log("🔑 선택된 데이터를 기본으로 설정 (기본키 보존):", updateData); + } + + // 필드 매핑에 따라 데이터 구성 (덮어쓰기) + action.fieldMappings.forEach((mapping: any) => { + const { sourceField, targetField, defaultValue, valueType } = mapping; + + let value: any; + + // 값 소스에 따라 데이터 가져오기 + if (valueType === "form" && context.formData && sourceField) { + value = context.formData[sourceField]; + } else if (valueType === "selected" && context.selectedRowsData?.[0] && sourceField) { + value = context.selectedRowsData[0][sourceField]; + } else if (valueType === "default" || !sourceField) { + value = defaultValue; + } + + // 타겟 필드에 값 설정 (덮어쓰기) + if (targetField && value !== undefined) { + updateData[targetField] = value; + console.log(`📝 필드 매핑: ${sourceField || "default"} -> ${targetField} = ${value}`); + } + }); + } else { + console.log("⚠️ 필드 매핑 정보가 없음, 기본 데이터 사용"); + // 폴백: 기존 방식 + updateData = { + ...context.formData, + ...context.selectedRowsData?.[0], + }; + } + + console.log("📊 최종 업데이트할 데이터:", updateData); + + try { + // 🔥 실제 업데이트 API 호출 + if (!context.tableName) { + throw new Error("테이블명이 설정되지 않았습니다."); + } + + // 먼저 ID 찾기 + const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(context.tableName); + let updateId: string | undefined; + + if (primaryKeysResult.success && primaryKeysResult.data && primaryKeysResult.data.length > 0) { + updateId = updateData[primaryKeysResult.data[0]]; + } + + if (!updateId) { + // 폴백: 일반적인 ID 필드들 확인 + const commonIdFields = ["id", "objid", "pk", "sales_no", "contract_no"]; + for (const field of commonIdFields) { + if (updateData[field]) { + updateId = updateData[field]; + break; + } + } + } + + if (!updateId) { + throw new Error("업데이트할 항목의 ID를 찾을 수 없습니다."); + } + + const result = await DynamicFormApi.updateFormData(updateId, { + tableName: context.tableName, + data: updateData, + }); + + if (result.success) { + console.log("✅ 업데이트 성공:", result); + toast.success("데이터가 업데이트되었습니다."); + } else { + throw new Error(result.message || "업데이트 실패"); + } + } catch (error) { + console.error("❌ 업데이트 실패:", error); + toast.error(`업데이트 실패: ${error.message}`); + throw error; + } + } + + /** + * 삭제 액션 실행 + */ + private static async executeActionDelete(action: any, context: ButtonActionContext): Promise { + console.log("🗑️ 삭제 액션 실행:", action); + + // 실제 삭제 로직 (기존 handleDelete와 유사) + if (!context.selectedRowsData || context.selectedRowsData.length === 0) { + throw new Error("삭제할 항목을 선택해주세요."); + } + + const deleteData = context.selectedRowsData[0]; + console.log("삭제할 데이터:", deleteData); + + try { + // 🔥 실제 삭제 API 호출 + if (!context.tableName) { + throw new Error("테이블명이 설정되지 않았습니다."); + } + + // 기존 handleDelete와 동일한 로직으로 ID 찾기 + const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(context.tableName); + let deleteId: string | undefined; + + if (primaryKeysResult.success && primaryKeysResult.data && primaryKeysResult.data.length > 0) { + deleteId = deleteData[primaryKeysResult.data[0]]; + } + + if (!deleteId) { + // 폴백: 일반적인 ID 필드들 확인 + const commonIdFields = ["id", "objid", "pk", "sales_no", "contract_no"]; + for (const field of commonIdFields) { + if (deleteData[field]) { + deleteId = deleteData[field]; + break; + } + } + } + + if (!deleteId) { + throw new Error("삭제할 항목의 ID를 찾을 수 없습니다."); + } + + const result = await DynamicFormApi.deleteFormDataFromTable(deleteId, context.tableName); + + if (result.success) { + console.log("✅ 삭제 성공:", result); + toast.success("데이터가 삭제되었습니다."); + } else { + throw new Error(result.message || "삭제 실패"); + } + } catch (error) { + console.error("❌ 삭제 실패:", error); + toast.error(`삭제 실패: ${error.message}`); + throw error; + } + } + + /** + * 삽입 액션 실행 (체크박스 선택된 데이터를 필드매핑에 따라 새 테이블에 삽입) + */ + private static async executeActionInsert(action: any, context: ButtonActionContext): Promise { + console.log("➕ 삽입 액션 실행:", action); + + let insertData: Record = {}; + + // 액션에 필드 매핑 정보가 있는지 확인 + if (action.fieldMappings && Array.isArray(action.fieldMappings)) { + console.log("📋 삽입 액션 - 필드 매핑 정보:", action.fieldMappings); + + // 🎯 체크박스로 선택된 데이터가 있는지 확인 + if (!context.selectedRowsData || context.selectedRowsData.length === 0) { + throw new Error("삽입할 소스 데이터를 선택해주세요. (테이블에서 체크박스 선택 필요)"); + } + + const sourceData = context.selectedRowsData[0]; // 첫 번째 선택된 데이터 사용 + console.log("🎯 삽입 소스 데이터 (체크박스 선택):", sourceData); + console.log("🔍 소스 데이터 사용 가능한 키들:", Object.keys(sourceData)); + + // 필드 매핑에 따라 데이터 구성 + action.fieldMappings.forEach((mapping: any) => { + const { sourceField, targetField, defaultValue } = mapping; + // valueType이 없으면 기본값을 "selection"으로 설정 + const valueType = mapping.valueType || "selection"; + + let value: any; + + console.log(`🔍 매핑 처리 중: ${sourceField} → ${targetField} (valueType: ${valueType})`); + + // 값 소스에 따라 데이터 가져오기 + if (valueType === "form" && context.formData && sourceField) { + // 폼 데이터에서 가져오기 + value = context.formData[sourceField]; + console.log(`📝 폼에서 매핑: ${sourceField} → ${targetField} = ${value}`); + } else if (valueType === "selection" && sourceField) { + // 선택된 테이블 데이터에서 가져오기 (다양한 필드명 시도) + value = + sourceData[sourceField] || + sourceData[sourceField + "_name"] || // 조인된 필드 (_name 접미사) + sourceData[sourceField + "Name"]; // 카멜케이스 + console.log(`📊 테이블에서 매핑: ${sourceField} → ${targetField} = ${value} (소스필드: ${sourceField})`); + } else if (valueType === "default" || (defaultValue !== undefined && defaultValue !== "")) { + // 기본값 사용 (valueType이 "default"이거나 defaultValue가 있을 때) + value = defaultValue; + console.log(`🔧 기본값 매핑: ${targetField} = ${value}`); + } else { + console.warn(`⚠️ 매핑 실패: ${sourceField} → ${targetField} (값을 찾을 수 없음)`); + console.warn(` - valueType: ${valueType}, defaultValue: ${defaultValue}`); + console.warn(` - 소스 데이터 키들:`, Object.keys(sourceData)); + console.warn(` - sourceData[${sourceField}] =`, sourceData[sourceField]); + return; // 값이 없으면 해당 필드는 스킵 + } + + // 대상 필드에 값 설정 + if (targetField && value !== undefined && value !== null) { + insertData[targetField] = value; + } + }); + + console.log("🎯 최종 삽입 데이터 (필드매핑 적용):", insertData); + } else { + // 필드 매핑이 없으면 폼 데이터를 기본으로 사용 + insertData = { ...context.formData }; + console.log("📝 기본 삽입 데이터 (폼 기반):", insertData); + } + + try { + // 🔥 실제 삽입 API 호출 - 필수 매개변수 포함 + // 필드 매핑에서 첫 번째 targetTable을 찾거나 기본값 사용 + const targetTable = action.fieldMappings?.[0]?.targetTable || action.targetTable || "test_project_info"; + + const formDataPayload = { + screenId: 0, // 제어 관리에서는 screenId가 없으므로 0 사용 + tableName: targetTable, // 필드 매핑에서 대상 테이블명 가져오기 + data: insertData, + }; + + console.log("🎯 대상 테이블:", targetTable); + console.log("📋 삽입할 데이터:", insertData); + + console.log("💾 폼 데이터 저장 요청:", formDataPayload); + + const result = await DynamicFormApi.saveFormData(formDataPayload); + + if (result.success) { + console.log("✅ 삽입 성공:", result); + toast.success(`데이터가 타겟 테이블에 성공적으로 삽입되었습니다.`); + } else { + throw new Error(result.message || "삽입 실패"); + } + } catch (error) { + console.error("❌ 삽입 실패:", error); + toast.error(`삽입 실패: ${error instanceof Error ? error.message : "알 수 없는 오류"}`); + throw error; + } + } + /** * 폼 데이터 유효성 검사 */ diff --git a/frontend/types/component.ts b/frontend/types/component.ts index 4a291df6..34a8cd92 100644 --- a/frontend/types/component.ts +++ b/frontend/types/component.ts @@ -44,6 +44,9 @@ export interface ComponentSize { height: number; } +// screen.ts에서 자동생성 관련 타입들을 import +export type { AutoGenerationType, AutoGenerationConfig } from "./screen"; + /** * 컴포넌트 렌더러 Props * 모든 컴포넌트 렌더러가 받는 공통 Props @@ -61,6 +64,11 @@ export interface ComponentRendererProps { style?: React.CSSProperties; formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; + + // 새로운 기능들 + autoGeneration?: AutoGenerationConfig; // 자동생성 설정 + hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김) + [key: string]: any; } diff --git a/frontend/types/screen.ts b/frontend/types/screen.ts index 2173b667..c27fce44 100644 --- a/frontend/types/screen.ts +++ b/frontend/types/screen.ts @@ -46,7 +46,8 @@ export type ButtonActionType = | "popup" // 팝업 열기 | "modal" // 모달 열기 | "newWindow" // 새 창 열기 - | "navigate"; // 페이지 이동 + | "navigate" // 페이지 이동 + | "control"; // 제어 전용 (조건 체크만) // 위치 정보 export interface Position { @@ -155,6 +156,31 @@ export interface ComponentStyle { } // BaseComponent에 스타일 속성 추가 +// 자동생성 타입 정의 +export type AutoGenerationType = + | "uuid" // UUID 생성 + | "current_user" // 현재 사용자 ID + | "current_time" // 현재 시간 + | "sequence" // 시퀀스 번호 + | "random_string" // 랜덤 문자열 + | "random_number" // 랜덤 숫자 + | "company_code" // 회사 코드 + | "department" // 부서 코드 + | "none"; // 자동생성 없음 + +// 자동생성 설정 +export interface AutoGenerationConfig { + type: AutoGenerationType; + enabled: boolean; + options?: { + length?: number; // 랜덤 문자열/숫자 길이 + prefix?: string; // 접두사 + suffix?: string; // 접미사 + format?: string; // 시간 형식 (current_time용) + startValue?: number; // 시퀀스 시작값 + }; +} + export interface BaseComponent { id: string; type: ComponentType; @@ -174,7 +200,11 @@ export interface BaseComponent { | "current_user" | "uuid" | "sequence" - | "user_defined"; // 자동 값 타입 + | "user_defined"; // 자동 값 타입 (레거시) + + // 새로운 기능들 + hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김) + autoGeneration?: AutoGenerationConfig; // 자동생성 설정 } // 컨테이너 컴포넌트