테스트테이블 생성 및 오류 수정

This commit is contained in:
kjs 2025-09-19 02:15:21 +09:00
parent ddcecfd5e2
commit f7d884568b
20 changed files with 1024 additions and 180 deletions

View File

@ -87,6 +87,48 @@ export class DynamicFormService {
return Boolean(value);
}
// 날짜/시간 타입 처리
if (
lowerDataType.includes("date") ||
lowerDataType.includes("timestamp") ||
lowerDataType.includes("time")
) {
if (typeof value === "string") {
// 빈 문자열이면 null 반환
if (value.trim() === "") {
return null;
}
try {
// YYYY-MM-DD 형식인 경우 시간 추가해서 Date 객체 생성
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`);
return new Date(value + "T00:00:00");
}
// 다른 날짜 형식도 Date 객체로 변환
else {
console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`);
return new Date(value);
}
} catch (error) {
console.error(`❌ 날짜 변환 실패: ${value}`, error);
return null;
}
}
// 이미 Date 객체인 경우 그대로 반환
if (value instanceof Date) {
return value;
}
// 숫자인 경우 timestamp로 처리
if (typeof value === "number") {
return new Date(value);
}
return null;
}
// 기본적으로 문자열로 반환
return value;
}
@ -479,7 +521,7 @@ export class DynamicFormService {
const updateQuery = `
UPDATE ${tableName}
SET ${setClause}
WHERE ${primaryKeyColumn} = $${values.length}
WHERE ${primaryKeyColumn} = $${values.length}::text
RETURNING *
`;
@ -552,6 +594,31 @@ export class DynamicFormService {
}
});
// 컬럼 타입에 맞는 데이터 변환 (UPDATE용)
const columnInfo = await this.getTableColumnInfo(tableName);
console.log(`📊 테이블 ${tableName}의 컬럼 타입 정보:`, columnInfo);
// 각 컬럼의 타입에 맞게 데이터 변환
Object.keys(dataToUpdate).forEach((columnName) => {
const column = columnInfo.find((col) => col.column_name === columnName);
if (column) {
const originalValue = dataToUpdate[columnName];
const convertedValue = this.convertValueForPostgreSQL(
originalValue,
column.data_type
);
if (originalValue !== convertedValue) {
console.log(
`🔄 UPDATE 타입 변환: ${columnName} (${column.data_type}) = "${originalValue}" -> ${convertedValue}`
);
dataToUpdate[columnName] = convertedValue;
}
}
});
console.log("✅ UPDATE 타입 변환 완료된 데이터:", dataToUpdate);
console.log("🎯 실제 테이블에서 업데이트할 데이터:", {
tableName,
id,
@ -650,12 +717,15 @@ export class DynamicFormService {
tableName,
});
// 1. 먼저 테이블의 기본키 컬럼명을 동적으로 조회
// 1. 먼저 테이블의 기본키 컬럼명과 데이터 타입을 동적으로 조회
const primaryKeyQuery = `
SELECT kcu.column_name
SELECT kcu.column_name, c.data_type
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.columns c
ON kcu.column_name = c.column_name
AND kcu.table_name = c.table_name
WHERE tc.table_name = $1
AND tc.constraint_type = 'PRIMARY KEY'
LIMIT 1
@ -677,13 +747,37 @@ export class DynamicFormService {
throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다.`);
}
const primaryKeyColumn = (primaryKeyResult[0] as any).column_name;
console.log("🔑 발견된 기본키 컬럼:", primaryKeyColumn);
const primaryKeyInfo = primaryKeyResult[0] as any;
const primaryKeyColumn = primaryKeyInfo.column_name;
const primaryKeyDataType = primaryKeyInfo.data_type;
console.log("🔑 발견된 기본키:", {
column: primaryKeyColumn,
dataType: primaryKeyDataType,
});
// 2. 동적으로 발견된 기본키를 사용한 DELETE SQL 생성
// 2. 데이터 타입에 맞는 타입 캐스팅 적용
let typeCastSuffix = "";
if (
primaryKeyDataType.includes("character") ||
primaryKeyDataType.includes("text")
) {
typeCastSuffix = "::text";
} else if (
primaryKeyDataType.includes("integer") ||
primaryKeyDataType.includes("bigint")
) {
typeCastSuffix = "::bigint";
} else if (
primaryKeyDataType.includes("numeric") ||
primaryKeyDataType.includes("decimal")
) {
typeCastSuffix = "::numeric";
}
// 3. 동적으로 발견된 기본키와 타입 캐스팅을 사용한 DELETE SQL 생성
const deleteQuery = `
DELETE FROM ${tableName}
WHERE ${primaryKeyColumn} = $1
WHERE ${primaryKeyColumn} = $1${typeCastSuffix}
RETURNING *
`;

View File

@ -1,7 +1,7 @@
"use client";
import React, { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
import { screenApi } from "@/lib/api/screen";
import { ComponentData } from "@/types/screen";
@ -45,24 +45,60 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
let maxWidth = 800; // 최소 너비
let maxHeight = 600; // 최소 높이
components.forEach((component) => {
const x = parseFloat(component.style?.positionX || "0");
const y = parseFloat(component.style?.positionY || "0");
const width = parseFloat(component.style?.width || "100");
const height = parseFloat(component.style?.height || "40");
console.log("🔍 화면 크기 계산 시작:", { componentsCount: components.length });
components.forEach((component, index) => {
// position과 size는 BaseComponent에서 별도 속성으로 관리
const x = parseFloat(component.position?.x?.toString() || "0");
const y = parseFloat(component.position?.y?.toString() || "0");
const width = parseFloat(component.size?.width?.toString() || "100");
const height = parseFloat(component.size?.height?.toString() || "40");
// 컴포넌트의 오른쪽 끝과 아래쪽 끝 계산
const rightEdge = x + width;
const bottomEdge = y + height;
maxWidth = Math.max(maxWidth, rightEdge + 50); // 여백 추가
maxHeight = Math.max(maxHeight, bottomEdge + 50); // 여백 추가
console.log(
`📏 컴포넌트 ${index + 1} (${component.id}): x=${x}, y=${y}, w=${width}, h=${height}, rightEdge=${rightEdge}, bottomEdge=${bottomEdge}`,
);
const newMaxWidth = Math.max(maxWidth, rightEdge + 100); // 여백 증가
const newMaxHeight = Math.max(maxHeight, bottomEdge + 100); // 여백 증가
if (newMaxWidth > maxWidth || newMaxHeight > maxHeight) {
console.log(`🔄 크기 업데이트: ${maxWidth}×${maxHeight}${newMaxWidth}×${newMaxHeight}`);
maxWidth = newMaxWidth;
maxHeight = newMaxHeight;
}
});
return {
width: Math.min(maxWidth, window.innerWidth * 0.9), // 화면의 90%를 넘지 않도록
height: Math.min(maxHeight, window.innerHeight * 0.8), // 화면의 80%를 넘지 않도록
console.log("📊 컴포넌트 기반 계산 결과:", { maxWidth, maxHeight });
// 브라우저 크기 제한 확인 (더욱 관대하게 설정)
const maxAllowedWidth = window.innerWidth * 0.98; // 95% -> 98%
const maxAllowedHeight = window.innerHeight * 0.95; // 90% -> 95%
console.log("📐 크기 제한 정보:", {
: { maxWidth, maxHeight },
: { maxAllowedWidth, maxAllowedHeight },
: { width: window.innerWidth, height: window.innerHeight },
});
// 컴포넌트 기반 크기를 우선 적용하되, 브라우저 제한을 고려
const finalDimensions = {
width: Math.min(maxWidth, maxAllowedWidth),
height: Math.min(maxHeight, maxAllowedHeight),
};
console.log("✅ 최종 화면 크기:", finalDimensions);
console.log("🔧 크기 적용 분석:", {
width적용: maxWidth <= maxAllowedWidth ? "컴포넌트기준" : "브라우저제한",
height적용: maxHeight <= maxAllowedHeight ? "컴포넌트기준" : "브라우저제한",
: { maxWidth, maxHeight },
최종크기: finalDimensions,
});
return finalDimensions;
};
// 전역 모달 이벤트 리스너
@ -154,17 +190,17 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
};
}
// 헤더 높이와 패딩을 고려한 전체 높이 계산
const headerHeight = 60; // DialogHeader + 패딩
// 헤더 높이와 패딩을 고려한 전체 높이 계산 (실제 측정값 기반)
const headerHeight = 80; // DialogHeader + 패딩 (더 정확한 값)
const totalHeight = screenDimensions.height + headerHeight;
return {
className: "overflow-hidden p-0",
style: {
width: `${screenDimensions.width + 48}px`, // 헤더 패딩과 여백 고려
height: `${Math.min(totalHeight, window.innerHeight * 0.8)}px`,
maxWidth: "90vw",
maxHeight: "80vh",
width: `${Math.min(screenDimensions.width + 48, window.innerWidth * 0.98)}px`, // 브라우저 제한 적용
height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`, // 브라우저 제한 적용
maxWidth: "98vw", // 안전장치
maxHeight: "95vh", // 안전장치
},
};
};
@ -176,9 +212,10 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
<DialogContent className={`${modalStyle.className} ${className || ""}`} style={modalStyle.style}>
<DialogHeader className="border-b px-6 py-4">
<DialogTitle>{modalState.title}</DialogTitle>
<DialogDescription>{loading ? "화면을 불러오는 중입니다..." : "화면 내용을 표시합니다."}</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-hidden p-4">
<div className="flex-1 p-4">
{loading ? (
<div className="flex h-full items-center justify-center">
<div className="text-center">
@ -188,7 +225,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
</div>
) : screenData ? (
<div
className="relative overflow-hidden bg-white"
className="relative bg-white"
style={{
width: screenDimensions?.width || 800,
height: screenDimensions?.height || 600,
@ -202,13 +239,13 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
formData={formData}
onFormDataChange={(fieldName, value) => {
console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`);
console.log(`📋 현재 formData:`, formData);
console.log("📋 현재 formData:", formData);
setFormData((prev) => {
const newFormData = {
...prev,
[fieldName]: value,
};
console.log(`📝 ScreenModal 업데이트된 formData:`, newFormData);
console.log("📝 ScreenModal 업데이트된 formData:", newFormData);
return newFormData;
});
}}

View File

@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
import { X, Save, RotateCcw } from "lucide-react";
import { toast } from "sonner";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
import { InteractiveScreenViewer } from "./InteractiveScreenViewer";
import { screenApi } from "@/lib/api/screen";
import { ComponentData } from "@/lib/types/screen";
@ -145,7 +146,19 @@ export const EditModal: React.FC<EditModalProps> = ({
layoutData.components.forEach((comp) => {
if (comp.columnName) {
const formValue = formData[comp.columnName];
console.log(` - ${comp.columnName}: "${formValue}" (컴포넌트 ID: ${comp.id})`);
console.log(
` - ${comp.columnName}: "${formValue}" (타입: ${comp.type}, 웹타입: ${(comp as any).widgetType})`,
);
// 코드 타입인 경우 특별히 로깅
if ((comp as any).widgetType === "code") {
console.log(` 🔍 코드 타입 세부정보:`, {
columnName: comp.columnName,
componentId: comp.id,
formValue,
webTypeConfig: (comp as any).webTypeConfig,
});
}
}
});
} else {
@ -270,8 +283,38 @@ export const EditModal: React.FC<EditModalProps> = ({
zIndex: 1,
}}
>
<DynamicComponentRenderer
{/* 위젯 컴포넌트는 InteractiveScreenViewer 사용 (라벨 표시를 위해) */}
{component.type === "widget" ? (
<InteractiveScreenViewer
component={component}
allComponents={components}
hideLabel={false} // 라벨 표시 활성화
formData={formData}
onFormDataChange={(fieldName, value) => {
console.log("📝 폼 데이터 변경:", fieldName, value);
const newFormData = { ...formData, [fieldName]: value };
setFormData(newFormData);
// 변경된 데이터를 즉시 부모로 전달
if (onDataChange) {
console.log("📤 EditModal -> 부모로 데이터 전달:", newFormData);
onDataChange(newFormData);
}
}}
screenInfo={{
id: screenId || 0,
tableName: screenData.tableName,
}}
/>
) : (
<DynamicComponentRenderer
component={{
...component,
style: {
...component.style,
labelDisplay: true, // 수정 모달에서는 라벨 강제 표시
},
}}
screenId={screenId}
tableName={screenData.tableName}
formData={formData}
@ -294,6 +337,7 @@ export const EditModal: React.FC<EditModalProps> = ({
// 인터랙티브 모드 활성화 (formData 사용을 위해 필수)
isInteractive={true}
/>
)}
</div>
))}
</div>

View File

@ -37,6 +37,7 @@ import { FileUpload } from "./widgets/FileUpload";
import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
import { useParams } from "next/navigation";
import { screenApi } from "@/lib/api/screen";
import { DynamicWebTypeRenderer } from "@/lib/registry/DynamicWebTypeRenderer";
interface InteractiveScreenViewerProps {
component: ComponentData;
@ -936,42 +937,65 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
const widget = comp as WidgetComponent;
const config = widget.webTypeConfig as CodeTypeConfig | undefined;
console.log("💻 InteractiveScreenViewer - Code 위젯:", {
console.log("🔍 InteractiveScreenViewer - Code 위젯 (공통코드 선택):", {
componentId: widget.id,
widgetType: widget.widgetType,
columnName: widget.columnName,
fieldName,
currentValue,
formData,
config,
appliedSettings: {
language: config?.language,
theme: config?.theme,
fontSize: config?.fontSize,
defaultValue: config?.defaultValue,
wordWrap: config?.wordWrap,
tabSize: config?.tabSize,
},
codeCategory: config?.codeCategory,
});
const finalPlaceholder = config?.placeholder || "코드를 입력하세요...";
const rows = config?.rows || 4;
// code 타입은 공통코드 선택박스로 처리
// DynamicWebTypeRenderer를 사용하여 SelectBasicComponent 렌더링
try {
return applyStyles(
<Textarea
placeholder={finalPlaceholder}
value={currentValue || config?.defaultValue || ""}
onChange={(e) => updateFormData(fieldName, e.target.value)}
<DynamicWebTypeRenderer
webType="select"
props={{
component: widget,
value: currentValue,
onChange: (value: any) => updateFormData(fieldName, value),
onFormDataChange: updateFormData,
isInteractive: true,
readonly: readonly,
required: required,
placeholder: config?.placeholder || "코드를 선택하세요...",
className: "w-full h-full",
}}
config={{
...config,
codeCategory: config?.codeCategory,
isCodeType: true, // 코드 타입임을 명시
}}
onEvent={(event: string, data: any) => {
console.log(`Code widget event: ${event}`, data);
}}
/>
);
} catch (error) {
console.error("DynamicWebTypeRenderer 로딩 실패, 기본 Select 사용:", error);
// 폴백: 기본 Select 컴포넌트 사용
return applyStyles(
<Select
value={currentValue || ""}
onValueChange={(value) => updateFormData(fieldName, value)}
disabled={readonly}
required={required}
rows={rows}
className="h-full w-full resize-none font-mono text-sm"
style={{
fontSize: `${config?.fontSize || 14}px`,
backgroundColor: config?.theme === "dark" ? "#1e1e1e" : "#ffffff",
color: config?.theme === "dark" ? "#ffffff" : "#000000",
whiteSpace: config?.wordWrap === false ? "nowrap" : "normal",
tabSize: config?.tabSize || 2,
}}
/>,
>
<SelectTrigger className="h-full w-full">
<SelectValue placeholder={config?.placeholder || "코드를 선택하세요..."} />
</SelectTrigger>
<SelectContent>
<SelectItem value="loading"> ...</SelectItem>
</SelectContent>
</Select>
);
}
}
case "entity": {
const widget = comp as WidgetComponent;
@ -1623,6 +1647,16 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
const labelText = component.style?.labelText || component.label || "";
// 라벨 표시 여부 로그 (디버깅용)
if (component.type === "widget") {
console.log("🏷️ 라벨 표시 체크:", {
componentId: component.id,
hideLabel,
shouldShowLabel,
labelText,
});
}
// 라벨 스타일 적용
const labelStyle = {
fontSize: component.style?.labelFontSize || "14px",

View File

@ -11,6 +11,7 @@ import { ComponentData, WidgetComponent, DataTableComponent, FileComponent, Butt
import { InteractiveDataTable } from "./InteractiveDataTable";
import { DynamicWebTypeRenderer } from "@/lib/registry";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
// 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록
import "@/lib/registry/components/ButtonRenderer";
@ -191,11 +192,6 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
// 화면 닫기 로직 (필요시 구현)
console.log("화면 닫기 요청");
}}
style={{
width: "100%",
height: "100%",
...comp.style,
}}
/>
);
}

View File

@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[9999] bg-black/80",
className,
)}
{...props}
@ -36,7 +36,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-[9999] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
className,
)}
{...props}
@ -104,5 +104,3 @@ export {
AlertDialogAction,
AlertDialogCancel,
};

View File

@ -175,11 +175,37 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
const startTime = Date.now();
totalRequests.current += 1;
// 🎯 디버깅: 캐시 상태 로깅
console.log(`🔍 optimizedConvertCode 호출: categoryCode="${categoryCode}", codeValue="${codeValue}"`);
// 캐시에서 동기적으로 조회 시도
const syncResult = codeCache.getCodeSync(categoryCode);
console.log(`🔍 getCodeSync("${categoryCode}") 결과:`, syncResult);
// 🎯 캐시 내용 상세 로깅 (키값들 확인)
if (syncResult) {
console.log(`🔍 캐시 키값들:`, Object.keys(syncResult));
console.log(`🔍 캐시 전체 데이터:`, JSON.stringify(syncResult, null, 2));
}
if (syncResult && Array.isArray(syncResult)) {
cacheHits.current += 1;
const result = syncResult[codeValue?.toUpperCase()] || codeValue;
console.log(`🔍 배열에서 코드 검색: codeValue="${codeValue}"`);
console.log(
`🔍 캐시 배열 내용:`,
syncResult.map((item) => ({
code_value: item.code_value,
code_name: item.code_name,
})),
);
// 배열에서 해당 code_value를 가진 항목 찾기
const foundCode = syncResult.find(
(item) => String(item.code_value).toUpperCase() === String(codeValue).toUpperCase(),
);
const result = foundCode ? foundCode.code_name : codeValue;
console.log(`🔍 최종 결과: "${codeValue}" → "${result}"`, { foundCode });
// 응답 시간 추적 (캐시 히트)
requestTimes.current.push(Date.now() - startTime);
@ -190,10 +216,13 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
return result;
}
console.log(`⚠️ 캐시 미스: categoryCode="${categoryCode}" - 비동기 로딩 트리거`);
// 캐시 미스인 경우 비동기 로딩 트리거 (백그라운드)
codeCache
.getCode(categoryCode)
.getCodeAsync(categoryCode)
.then(() => {
console.log(`✅ 비동기 로딩 완료: categoryCode="${categoryCode}"`);
updateMetrics();
})
.catch((err) => {

View File

@ -4,6 +4,7 @@ import React from "react";
import { ComponentData } from "@/types/screen";
import { DynamicLayoutRenderer } from "./DynamicLayoutRenderer";
import { ComponentRegistry } from "./ComponentRegistry";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
// 컴포넌트 렌더러 인터페이스
export interface ComponentRenderer {
@ -86,6 +87,12 @@ export interface DynamicComponentRendererProps {
tableName?: string;
onRefresh?: () => void;
onClose?: () => void;
// 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[];
selectedRowsData?: any[];
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
// 테이블 새로고침 키
refreshKey?: number;
// 편집 모드
mode?: "view" | "edit";
// 모달 내에서 렌더링 여부
@ -107,6 +114,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
// 레이아웃 컴포넌트 처리
if (componentType === "layout") {
// DOM 안전한 props만 전달
const safeLayoutProps = filterDOMProps(props);
return (
<DynamicLayoutRenderer
layout={component as any}
@ -118,7 +128,17 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
onUpdateLayout={props.onUpdateLayout}
// onComponentDrop 제거 - 일반 캔버스 드롭만 사용
onZoneClick={props.onZoneClick}
{...props}
isInteractive={props.isInteractive}
formData={props.formData}
onFormDataChange={props.onFormDataChange}
screenId={props.screenId}
tableName={props.tableName}
onRefresh={props.onRefresh}
onClose={props.onClose}
mode={props.mode}
isInModal={props.isInModal}
originalData={props.originalData}
{...safeLayoutProps}
/>
);
}
@ -131,18 +151,47 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
try {
const NewComponentRenderer = newComponent.component;
if (NewComponentRenderer) {
// React 전용 props 필터링
const { isInteractive, formData, onFormDataChange, ...safeProps } = props;
// React 전용 props들을 명시적으로 분리하고 DOM 안전한 props만 전달
const {
isInteractive,
formData,
onFormDataChange,
tableName,
onRefresh,
onClose,
screenId,
mode,
isInModal,
originalData,
allComponents,
onUpdateLayout,
onZoneClick,
selectedRows,
selectedRowsData,
onSelectedRowsChange,
refreshKey,
...safeProps
} = props;
// 컴포넌트의 columnName에 해당하는 formData 값 추출
const fieldName = (component as any).columnName || component.id;
const currentValue = formData?.[fieldName] || "";
console.log("🔍 DynamicComponentRenderer - 새 컴포넌트 시스템:", {
componentType,
componentId: component.id,
columnName: (component as any).columnName,
fieldName,
currentValue,
hasFormData: !!formData,
formDataKeys: formData ? Object.keys(formData) : [],
});
return (
<NewComponentRenderer
{...safeProps}
component={component}
isSelected={isSelected}
onClick={onClick}
isInteractive={isInteractive}
formData={formData}
onFormDataChange={onFormDataChange}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
size={component.size || newComponent.defaultSize}
@ -150,10 +199,26 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
style={component.style}
config={component.componentConfig}
componentConfig={component.componentConfig}
screenId={props.screenId}
tableName={props.tableName}
onRefresh={props.onRefresh}
onClose={props.onClose}
value={currentValue} // formData에서 추출한 현재 값 전달
// React 전용 props들은 직접 전달 (DOM에 전달되지 않음)
isInteractive={isInteractive}
formData={formData}
onFormDataChange={onFormDataChange}
tableName={tableName}
onRefresh={onRefresh}
onClose={onClose}
screenId={screenId}
mode={mode}
isInModal={isInModal}
originalData={originalData}
allComponents={allComponents}
onUpdateLayout={onUpdateLayout}
onZoneClick={onZoneClick}
// 테이블 선택된 행 정보 전달
selectedRows={selectedRows}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={onSelectedRowsChange}
refreshKey={refreshKey}
/>
);
}
@ -187,6 +252,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
// 동적 렌더링 실행
try {
// 레거시 시스템에서도 DOM 안전한 props만 전달
const safeLegacyProps = filterDOMProps(props);
return renderer({
component,
isSelected,
@ -194,7 +262,28 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
onDragStart,
onDragEnd,
children,
...props,
// React 전용 props들은 명시적으로 전달 (레거시 컴포넌트가 필요한 경우)
isInteractive: props.isInteractive,
formData: props.formData,
onFormDataChange: props.onFormDataChange,
screenId: props.screenId,
tableName: props.tableName,
onRefresh: props.onRefresh,
onClose: props.onClose,
mode: props.mode,
isInModal: props.isInModal,
originalData: props.originalData,
onUpdateLayout: props.onUpdateLayout,
onZoneClick: props.onZoneClick,
onZoneComponentDrop: props.onZoneComponentDrop,
allComponents: props.allComponents,
// 테이블 선택된 행 정보 전달
selectedRows: props.selectedRows,
selectedRowsData: props.selectedRowsData,
onSelectedRowsChange: props.onSelectedRowsChange,
refreshKey: props.refreshKey,
// DOM 안전한 props들
...safeLegacyProps,
});
} catch (error) {
console.error(`❌ 컴포넌트 렌더링 실패 (${componentType}):`, error);

View File

@ -3,6 +3,7 @@
import React from "react";
import { LayoutComponent, ComponentData } from "@/types/screen";
import { LayoutRegistry } from "./LayoutRegistry";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
export interface DynamicLayoutRendererProps {
layout: LayoutComponent;
@ -70,6 +71,9 @@ export const DynamicLayoutRenderer: React.FC<DynamicLayoutRendererProps> = ({
// 레이아웃 렌더링 실행
try {
// DOM 안전한 props만 필터링
const safeProps = filterDOMProps(restProps);
return (
<LayoutComponent
layout={layout}
@ -84,7 +88,7 @@ export const DynamicLayoutRenderer: React.FC<DynamicLayoutRendererProps> = ({
onUpdateLayout={onUpdateLayout}
className={className}
style={style}
{...restProps}
{...safeProps}
/>
);
} catch (error) {

View File

@ -20,6 +20,7 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { toast } from "sonner";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
config?: ButtonPrimaryConfig;
@ -313,15 +314,20 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
}
};
// DOM 안전한 props만 필터링
const safeDomProps = filterDOMProps(domProps);
return (
<>
<div style={componentStyle} className={className} {...domProps}>
<div style={componentStyle} className={className} {...safeDomProps}>
<button
type={componentConfig.actionType || "button"}
disabled={componentConfig.disabled || false}
style={{
width: "100%",
height: "100%",
minHeight: "100%", // 최소 높이 강제 적용
maxHeight: "100%", // 최대 높이 제한
border: "1px solid #3b82f6",
borderRadius: "4px",
backgroundColor: "#3b82f6",
@ -330,6 +336,16 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
fontWeight: "500",
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
outline: "none",
boxSizing: "border-box", // 패딩/보더 포함 크기 계산
display: "flex", // flex로 변경
alignItems: "center", // 세로 중앙 정렬
justifyContent: "center", // 가로 중앙 정렬
padding: "0", // 패딩 제거
margin: "0", // 마진 제거
lineHeight: "1", // 라인 높이 고정
// 강제 높이 적용
minHeight: "36px",
height: "36px",
// isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}),
}}

View File

@ -3,9 +3,11 @@
import React from "react";
import { ComponentRendererProps } from "@/types/component";
import { DateInputConfig } from "./types";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
export interface DateInputComponentProps extends ComponentRendererProps {
config?: DateInputConfig;
value?: any; // 외부에서 전달받는 값
}
/**
@ -25,6 +27,7 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
style,
formData,
onFormDataChange,
value: externalValue, // 외부에서 전달받은 값
...props
}) => {
// 컴포넌트 설정
@ -33,6 +36,92 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
...component.config,
} as DateInputConfig;
// 날짜 값 계산 및 디버깅
const fieldName = component.columnName || component.id;
const rawValue =
externalValue !== undefined
? externalValue
: isInteractive && formData && component.columnName
? formData[component.columnName]
: component.value;
console.log("🔍 DateInputComponent 값 디버깅:", {
componentId: component.id,
fieldName,
externalValue,
formDataValue: formData?.[component.columnName || ""],
componentValue: component.value,
rawValue,
isInteractive,
hasFormData: !!formData,
});
// 날짜 형식 변환 함수 (HTML input[type="date"]는 YYYY-MM-DD 형식만 허용)
const formatDateForInput = (dateValue: any): string => {
if (!dateValue) return "";
const dateStr = String(dateValue);
// 이미 YYYY-MM-DD 형식인 경우
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
return dateStr;
}
// YYYY-MM-DD HH:mm:ss 형식에서 날짜 부분만 추출
if (/^\d{4}-\d{2}-\d{2}\s/.test(dateStr)) {
return dateStr.split(" ")[0];
}
// YYYY/MM/DD 형식
if (/^\d{4}\/\d{2}\/\d{2}$/.test(dateStr)) {
return dateStr.replace(/\//g, "-");
}
// MM/DD/YYYY 형식
if (/^\d{2}\/\d{2}\/\d{4}$/.test(dateStr)) {
const [month, day, year] = dateStr.split("/");
return `${year}-${month}-${day}`;
}
// DD-MM-YYYY 형식
if (/^\d{2}-\d{2}-\d{4}$/.test(dateStr)) {
const [day, month, year] = dateStr.split("-");
return `${year}-${month}-${day}`;
}
// ISO 8601 날짜 (2023-12-31T00:00:00.000Z 등)
if (/^\d{4}-\d{2}-\d{2}T/.test(dateStr)) {
return dateStr.split("T")[0];
}
// 다른 형식의 날짜 문자열이나 Date 객체 처리
try {
const date = new Date(dateValue);
if (isNaN(date.getTime())) {
console.warn("🚨 DateInputComponent - 유효하지 않은 날짜:", dateValue);
return "";
}
// YYYY-MM-DD 형식으로 변환 (로컬 시간대 사용)
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const formattedDate = `${year}-${month}-${day}`;
console.log("📅 날짜 형식 변환:", {
원본: dateValue,
변환후: formattedDate,
});
return formattedDate;
} catch (error) {
console.error("🚨 DateInputComponent - 날짜 변환 오류:", error, "원본:", dateValue);
return "";
}
};
const formattedValue = formatDateForInput(rawValue);
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
const componentStyle: React.CSSProperties = {
width: "100%",
@ -74,10 +163,13 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
...domProps
} = props;
// DOM 안전한 props만 필터링
const safeDomProps = filterDOMProps(domProps);
return (
<div style={componentStyle} className={className} {...domProps}>
<div style={componentStyle} className={className} {...safeDomProps}>
{/* 라벨 렌더링 */}
{component.label && (
{component.label && component.style?.labelDisplay !== false && (
<label
style={{
position: "absolute",
@ -86,17 +178,15 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151",
fontWeight: "500",
// isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}),
}}
>
{component.label}
{component.required && (
<span style={{
<span
style={{
color: "#ef4444",
// isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}),
}}>
}}
>
*
</span>
)}
@ -105,12 +195,13 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
<input
type="date"
value={component.value || ""}
value={formattedValue}
placeholder={componentConfig.placeholder || ""}
disabled={componentConfig.disabled || false}
required={componentConfig.required || false}
readOnly={componentConfig.readonly || false}
style={{width: "100%",
style={{
width: "100%",
height: "100%",
border: "1px solid #d1d5db",
borderRadius: "4px",
@ -118,13 +209,36 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
fontSize: "14px",
outline: "none",
// isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}),}}
...(isInteractive && component.style ? component.style : {}),
}}
onClick={handleClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onChange={(e) => {
if (component.onChange) {
component.onChange(e.target.value);
const newValue = e.target.value;
console.log("🎯 DateInputComponent onChange 호출:", {
componentId: component.id,
columnName: component.columnName,
newValue,
isInteractive,
hasOnFormDataChange: !!onFormDataChange,
hasOnChange: !!props.onChange,
});
// isInteractive 모드에서는 formData 업데이트
if (isInteractive && onFormDataChange && component.columnName) {
console.log(`📤 DateInputComponent -> onFormDataChange 호출: ${component.columnName} = "${newValue}"`);
onFormDataChange(component.columnName, newValue);
}
// 디자인 모드에서는 component.onChange 호출
else if (component.onChange) {
console.log(`📤 DateInputComponent -> component.onChange 호출: ${newValue}`);
component.onChange(newValue);
}
// props.onChange가 있으면 호출 (호환성)
else if (props.onChange) {
console.log(`📤 DateInputComponent -> props.onChange 호출: ${newValue}`);
props.onChange(newValue);
}
}}
/>

View File

@ -3,9 +3,11 @@
import React from "react";
import { ComponentRendererProps } from "@/types/component";
import { NumberInputConfig } from "./types";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
export interface NumberInputComponentProps extends ComponentRendererProps {
config?: NumberInputConfig;
value?: any; // 외부에서 전달받는 값
}
/**
@ -25,6 +27,7 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
style,
formData,
onFormDataChange,
value: externalValue, // 외부에서 전달받은 값
...props
}) => {
// 컴포넌트 설정
@ -74,10 +77,13 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
...domProps
} = props;
// DOM 안전한 props만 필터링
const safeDomProps = filterDOMProps(domProps);
return (
<div style={componentStyle} className={className} {...domProps}>
<div style={componentStyle} className={className} {...safeDomProps}>
{/* 라벨 렌더링 */}
{component.label && (
{component.label && component.style?.labelDisplay !== false && (
<label
style={{
position: "absolute",
@ -86,17 +92,15 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151",
fontWeight: "500",
// isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}),
}}
>
{component.label}
{component.required && (
<span style={{
<span
style={{
color: "#ef4444",
// isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}),
}}>
}}
>
*
</span>
)}
@ -105,7 +109,16 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
<input
type="number"
value={component.value || ""}
value={
// 1순위: 외부에서 전달받은 value (DynamicComponentRenderer에서 전달)
externalValue !== undefined
? externalValue
: // 2순위: 인터랙티브 모드에서 formData
isInteractive && formData && component.columnName
? formData[component.columnName] || ""
: // 3순위: 컴포넌트 자체 값
component.value || ""
}
placeholder={componentConfig.placeholder || ""}
disabled={componentConfig.disabled || false}
required={componentConfig.required || false}
@ -113,7 +126,8 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
min={componentConfig.min}
max={componentConfig.max}
step={componentConfig.step || 1}
style={{width: "100%",
style={{
width: "100%",
height: "100%",
border: "1px solid #d1d5db",
borderRadius: "4px",
@ -121,13 +135,36 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
fontSize: "14px",
outline: "none",
// isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}),}}
...(isInteractive && component.style ? component.style : {}),
}}
onClick={handleClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onChange={(e) => {
if (component.onChange) {
component.onChange(e.target.value);
const newValue = e.target.value;
console.log("🎯 NumberInputComponent onChange 호출:", {
componentId: component.id,
columnName: component.columnName,
newValue,
isInteractive,
hasOnFormDataChange: !!onFormDataChange,
hasOnChange: !!props.onChange,
});
// isInteractive 모드에서는 formData 업데이트
if (isInteractive && onFormDataChange && component.columnName) {
console.log(`📤 NumberInputComponent -> onFormDataChange 호출: ${component.columnName} = "${newValue}"`);
onFormDataChange(component.columnName, newValue);
}
// 디자인 모드에서는 component.onChange 호출
else if (component.onChange) {
console.log(`📤 NumberInputComponent -> component.onChange 호출: ${newValue}`);
component.onChange(newValue);
}
// props.onChange가 있으면 호출 (호환성)
else if (props.onChange) {
console.log(`📤 NumberInputComponent -> props.onChange 호출: ${newValue}`);
props.onChange(newValue);
}
}}
/>

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from "react";
import { commonCodeApi } from "../../../api/commonCode";
import { tableTypeApi } from "../../../api/screen";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
interface Option {
value: string;
@ -14,11 +15,14 @@ export interface SelectBasicComponentProps {
onUpdate?: (field: string, value: any) => void;
isSelected?: boolean;
isDesignMode?: boolean;
isInteractive?: boolean;
onFormDataChange?: (fieldName: string, value: any) => void;
className?: string;
style?: React.CSSProperties;
onClick?: () => void;
onDragStart?: () => void;
onDragEnd?: () => void;
value?: any; // 외부에서 전달받는 값
[key: string]: any;
}
@ -164,6 +168,7 @@ const loadGlobalCodeOptions = async (codeCategory: string): Promise<Option[]> =>
const actualValue = code.code || code.CODE || code.value || code.code_value || `code_${index}`;
const actualLabel =
code.codeName ||
code.code_name || // 스네이크 케이스 추가!
code.name ||
code.CODE_NAME ||
code.NAME ||
@ -233,16 +238,34 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
onUpdate,
isSelected = false,
isDesignMode = false,
isInteractive = false,
onFormDataChange,
className,
style,
onClick,
onDragStart,
onDragEnd,
value: externalValue, // 명시적으로 value prop 받기
...props
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState(componentConfig?.value || "");
// webTypeConfig 또는 componentConfig 사용 (DynamicWebTypeRenderer 호환성)
const config = (props as any).webTypeConfig || componentConfig || {};
// 외부에서 전달받은 value가 있으면 우선 사용, 없으면 config.value 사용
const [selectedValue, setSelectedValue] = useState(externalValue || config?.value || "");
const [selectedLabel, setSelectedLabel] = useState("");
console.log("🔍 SelectBasicComponent 초기화:", {
componentId: component.id,
externalValue,
componentConfigValue: componentConfig?.value,
webTypeConfigValue: (props as any).webTypeConfig?.value,
configValue: config?.value,
finalSelectedValue: externalValue || config?.value || "",
props: Object.keys(props),
});
const [codeOptions, setCodeOptions] = useState<Option[]>([]);
const [isLoadingCodes, setIsLoadingCodes] = useState(false);
const [dynamicCodeCategory, setDynamicCodeCategory] = useState<string | null>(null);
@ -250,7 +273,25 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
const selectRef = useRef<HTMLDivElement>(null);
// 코드 카테고리 결정: 동적 카테고리 > 설정 카테고리
const codeCategory = dynamicCodeCategory || componentConfig?.codeCategory;
const codeCategory = dynamicCodeCategory || config?.codeCategory;
// 외부 value prop 변경 시 selectedValue 업데이트
useEffect(() => {
const newValue = externalValue || config?.value || "";
// 값이 실제로 다른 경우에만 업데이트 (빈 문자열도 유효한 값으로 처리)
if (newValue !== selectedValue) {
console.log(`🔄 SelectBasicComponent value 업데이트: "${selectedValue}" → "${newValue}"`);
console.log(`🔍 업데이트 조건 분석:`, {
externalValue,
componentConfigValue: componentConfig?.value,
configValue: config?.value,
newValue,
selectedValue,
shouldUpdate: newValue !== selectedValue,
});
setSelectedValue(newValue);
}
}, [externalValue, config?.value]);
// 🚀 전역 상태 구독 및 동기화
useEffect(() => {
@ -359,7 +400,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
// 선택된 값에 따른 라벨 업데이트
useEffect(() => {
const getAllOptions = () => {
const configOptions = componentConfig.options || [];
const configOptions = config.options || [];
return [...codeOptions, ...configOptions];
};
@ -370,7 +411,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
if (newLabel !== selectedLabel) {
setSelectedLabel(newLabel);
}
}, [selectedValue, codeOptions, componentConfig.options]);
}, [selectedValue, codeOptions, config.options]);
// 클릭 이벤트 핸들러 (전역 상태 새로고침)
const handleToggle = () => {
@ -416,10 +457,23 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
setSelectedLabel(label);
setIsOpen(false);
// 디자인 모드에서의 컴포넌트 속성 업데이트
if (onUpdate) {
onUpdate("value", value);
}
// 인터랙티브 모드에서 폼 데이터 업데이트 (TextInputComponent와 동일한 로직)
if (isInteractive && onFormDataChange && component.columnName) {
console.log(`📤 SelectBasicComponent -> onFormDataChange 호출: ${component.columnName} = "${value}"`);
onFormDataChange(component.columnName, value);
} else {
console.log("❌ SelectBasicComponent onFormDataChange 조건 미충족:", {
isInteractive,
hasOnFormDataChange: !!onFormDataChange,
hasColumnName: !!component.columnName,
});
}
console.log(`✅ [${component.id}] 옵션 선택:`, { value, label });
};
@ -473,7 +527,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
// 모든 옵션 가져오기
const getAllOptions = () => {
const configOptions = componentConfig.options || [];
const configOptions = config.options || [];
console.log(`🔧 [${component.id}] 옵션 병합:`, {
codeOptionsLength: codeOptions.length,
codeOptions: codeOptions.map((o) => ({ value: o.value, label: o.label })),
@ -486,6 +540,24 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
const allOptions = getAllOptions();
const placeholder = componentConfig.placeholder || "선택하세요";
// DOM props에서 React 전용 props 필터링
const {
component: _component,
componentConfig: _componentConfig,
screenId: _screenId,
onUpdate: _onUpdate,
isSelected: _isSelected,
isDesignMode: _isDesignMode,
className: _className,
style: _style,
onClick: _onClick,
onDragStart: _onDragStart,
onDragEnd: _onDragEnd,
...otherProps
} = props;
const safeDomProps = filterDOMProps(otherProps);
return (
<div
ref={selectRef}
@ -494,8 +566,27 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
onClick={onClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
{...props}
{...safeDomProps}
>
{/* 라벨 렌더링 */}
{component.label && component.style?.labelDisplay !== false && (
<label
style={{
position: "absolute",
top: "-25px",
left: "0px",
fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151",
fontWeight: "500",
// isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}),
}}
>
{component.label}
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
</label>
)}
{/* 커스텀 셀렉트 박스 */}
<div
className={`flex w-full cursor-pointer items-center justify-between rounded-md border border-gray-300 bg-white px-3 py-2 ${isDesignMode ? "pointer-events-none" : "hover:border-gray-400"} ${isSelected ? "ring-2 ring-blue-500" : ""} ${isOpen ? "border-blue-500" : ""} `}
@ -520,11 +611,11 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
{/* 드롭다운 옵션 */}
{isOpen && !isDesignMode && (
<div
className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg"
className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg"
style={{
backgroundColor: "white",
color: "black",
zIndex: 9999,
zIndex: 99999, // 더 높은 z-index로 설정
}}
>
{(() => {

View File

@ -704,13 +704,29 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
return (value: any, format?: string, columnName?: string) => {
if (value === null || value === undefined) return "";
// 디버깅: 모든 값 변환 시도를 로깅
if (
columnName &&
(columnName === "contract_type" || columnName === "domestic_foreign" || columnName === "status")
) {
console.log(`🔍 값 변환 시도: ${columnName}="${value}"`, {
columnMeta: columnMeta[columnName],
hasColumnMeta: !!columnMeta[columnName],
webType: columnMeta[columnName]?.webType,
codeCategory: columnMeta[columnName]?.codeCategory,
globalColumnMeta: Object.keys(columnMeta),
});
}
// 🎯 코드 컬럼인 경우 최적화된 코드명 변환 사용
if (columnName && columnMeta[columnName]?.webType === "code" && columnMeta[columnName]?.codeCategory) {
const categoryCode = columnMeta[columnName].codeCategory!;
const convertedValue = optimizedConvertCode(categoryCode, String(value));
if (convertedValue !== String(value)) {
console.log(`🔄 코드 변환: ${columnName}[${categoryCode}] ${value}${convertedValue}`);
console.log(`🔄 코드 변환 성공: ${columnName}[${categoryCode}] ${value}${convertedValue}`);
} else {
console.log(`⚠️ 코드 변환 실패: ${columnName}[${categoryCode}] ${value}${convertedValue} (값 동일)`);
}
value = convertedValue;

View File

@ -3,6 +3,7 @@
import React from "react";
import { ComponentRendererProps } from "@/types/component";
import { TextInputConfig } from "./types";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
export interface TextInputComponentProps extends ComponentRendererProps {
config?: TextInputConfig;
@ -80,10 +81,13 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
...domProps
} = props;
// DOM 안전한 props만 필터링
const safeDomProps = filterDOMProps(domProps);
return (
<div style={componentStyle} className={className} {...domProps}>
<div style={componentStyle} className={className} {...safeDomProps}>
{/* 라벨 렌더링 */}
{component.label && (
{component.label && component.style?.labelDisplay !== false && (
<label
style={{
position: "absolute",
@ -126,7 +130,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
onDragEnd={onDragEnd}
onChange={(e) => {
const newValue = e.target.value;
console.log(`🎯 TextInputComponent onChange 호출:`, {
console.log("🎯 TextInputComponent onChange 호출:", {
componentId: component.id,
columnName: component.columnName,
newValue,
@ -138,13 +142,13 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
// isInteractive 모드에서는 formData 업데이트
if (isInteractive && onFormDataChange && component.columnName) {
console.log(`📤 TextInputComponent -> onFormDataChange 호출: ${component.columnName} = "${newValue}"`);
console.log(`🔍 onFormDataChange 함수 정보:`, {
console.log("🔍 onFormDataChange 함수 정보:", {
functionName: onFormDataChange.name,
functionString: onFormDataChange.toString().substring(0, 200),
});
onFormDataChange(component.columnName, newValue);
} else {
console.log(`❌ TextInputComponent onFormDataChange 조건 미충족:`, {
console.log("❌ TextInputComponent onFormDataChange 조건 미충족:", {
isInteractive,
hasOnFormDataChange: !!onFormDataChange,
hasColumnName: !!component.columnName,

View File

@ -19,6 +19,18 @@ export interface LayoutRendererProps {
children?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
// 추가된 props들 (레이아웃에서 사용되지 않지만 필터링 시 필요)
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
isInteractive?: boolean;
screenId?: number;
tableName?: string;
onRefresh?: () => void;
onClose?: () => void;
mode?: "view" | "edit";
isInModal?: boolean;
originalData?: Record<string, any>;
[key: string]: any; // 기타 props 허용
}
export abstract class BaseLayoutRenderer extends React.Component<LayoutRendererProps> {

View File

@ -2,6 +2,7 @@
import React from "react";
import { LayoutRendererProps } from "../BaseLayoutRenderer";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
/**
* Flexbox
@ -93,19 +94,8 @@ export const FlexboxLayout: React.FC<FlexboxLayoutProps> = ({
flexStyle.padding = "8px";
}
// DOM props만 추출 (React DOM에서 인식하는 props만)
const {
children: propsChildren,
onUpdateLayout,
onSelectComponent,
isDesignMode: _isDesignMode,
allComponents,
onComponentDrop,
onDragStart,
onDragEnd,
selectedScreen, // DOM에 전달하지 않도록 제외
...domProps
} = props;
// DOM 안전한 props만 필터링
const domProps = filterDOMProps(props);
return (
<>

View File

@ -2,6 +2,7 @@
import React from "react";
import { LayoutRendererProps } from "../BaseLayoutRenderer";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
/**
*
@ -61,19 +62,8 @@ export const GridLayout: React.FC<GridLayoutProps> = ({
gridStyle.borderRadius = "8px";
}
// DOM props만 추출 (React DOM에서 인식하는 props만)
const {
children: propsChildren,
onUpdateLayout,
onSelectComponent,
isDesignMode: _isDesignMode,
allComponents,
onComponentDrop,
onDragStart,
onDragEnd,
selectedScreen, // DOM에 전달하지 않도록 제외
...domProps
} = props;
// DOM 안전한 props만 필터링
const domProps = filterDOMProps(props);
return (
<>

View File

@ -166,7 +166,10 @@ export class ButtonActionExecutor {
const primaryKeys = primaryKeyResult.data || [];
const primaryKeyValue = this.extractPrimaryKeyValueFromDB(formData, primaryKeys);
const isUpdate = primaryKeyValue !== null && primaryKeyValue !== undefined && primaryKeyValue !== "";
// 단순히 기본키 값 존재 여부로 판단 (임시)
// TODO: 실제 테이블에서 기본키로 레코드 존재 여부 확인하는 API 필요
const isUpdate = false; // 현재는 항상 INSERT로 처리
console.log("💾 저장 모드 판단 (DB 기반):", {
tableName,
@ -316,12 +319,67 @@ export class ButtonActionExecutor {
if (selectedRowsData && selectedRowsData.length > 0) {
console.log(`다중 삭제 액션 실행: ${selectedRowsData.length}개 항목`, selectedRowsData);
// 테이블의 기본키 조회
let primaryKeys: string[] = [];
if (tableName) {
try {
const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(tableName);
if (primaryKeysResult.success && primaryKeysResult.data) {
primaryKeys = primaryKeysResult.data;
console.log(`🔑 테이블 ${tableName}의 기본키:`, primaryKeys);
}
} catch (error) {
console.warn("기본키 조회 실패, 폴백 방법 사용:", error);
}
}
// 각 선택된 항목을 삭제
for (const rowData of selectedRowsData) {
// 더 포괄적인 ID 찾기 (테이블 구조에 따라 다양한 필드명 시도)
const deleteId = rowData.id || rowData.objid || rowData.pk || rowData.ID || rowData.OBJID || rowData.PK;
let deleteId: any = null;
// 1순위: 데이터베이스에서 조회한 기본키 사용
if (primaryKeys.length > 0) {
const primaryKey = primaryKeys[0]; // 첫 번째 기본키 사용
deleteId = rowData[primaryKey];
console.log(`📊 기본키 ${primaryKey}로 ID 추출:`, deleteId);
}
// 2순위: 폴백 - 일반적인 ID 필드명들 시도
if (!deleteId) {
deleteId =
rowData.id ||
rowData.objid ||
rowData.pk ||
rowData.ID ||
rowData.OBJID ||
rowData.PK ||
// 테이블별 기본키 패턴들
rowData.sales_no ||
rowData.contract_no ||
rowData.order_no ||
rowData.seq_no ||
rowData.code ||
rowData.code_id ||
rowData.user_id ||
rowData.menu_id;
// _no로 끝나는 필드들 찾기
if (!deleteId) {
const noField = Object.keys(rowData).find((key) => key.endsWith("_no") && rowData[key]);
if (noField) deleteId = rowData[noField];
}
// _id로 끝나는 필드들 찾기
if (!deleteId) {
const idField = Object.keys(rowData).find((key) => key.endsWith("_id") && rowData[key]);
if (idField) deleteId = rowData[idField];
}
console.log(`🔍 폴백 방법으로 ID 추출:`, deleteId);
}
console.log("선택된 행 데이터:", rowData);
console.log("추출된 deleteId:", deleteId);
console.log("최종 추출된 deleteId:", deleteId);
if (deleteId) {
console.log("다중 데이터 삭제:", { tableName, screenId, id: deleteId });
@ -332,7 +390,9 @@ export class ButtonActionExecutor {
}
} else {
console.error("삭제 ID를 찾을 수 없습니다. 행 데이터:", rowData);
throw new Error(`삭제 ID를 찾을 수 없습니다. 사용 가능한 필드: ${Object.keys(rowData).join(", ")}`);
throw new Error(
`삭제 ID를 찾을 수 없습니다. 기본키: ${primaryKeys.join(", ")}, 사용 가능한 필드: ${Object.keys(rowData).join(", ")}`,
);
}
}

View File

@ -0,0 +1,189 @@
/**
* DOM props
* React props들을 DOM .
*/
// DOM에 전달하면 안 되는 React 전용 props 목록
const REACT_ONLY_PROPS = new Set([
// 컴포넌트 관련
"component",
"componentConfig",
"config",
"isSelected",
"isDesignMode",
"isInteractive",
"size",
"position",
// 이벤트 핸들러 (React 이벤트 외)
"onFormDataChange",
"onRefresh",
"onClose",
"onZoneComponentDrop",
"onZoneClick",
"onSelectedRowsChange",
"onUpdateLayout",
// 데이터 관련
"formData",
"originalData",
"selectedScreen",
"allComponents",
"refreshKey",
// 화면/테이블 관련
"screenId",
"tableName",
// 상태 관련
"mode",
"isInModal",
// 테이블 관련
"selectedRows",
"selectedRowsData",
// 추가된 React 전용 props
"allComponents",
]);
// DOM에 안전하게 전달할 수 있는 표준 HTML 속성들
const SAFE_DOM_PROPS = new Set([
// 표준 HTML 속성
"id",
"className",
"style",
"title",
"lang",
"dir",
"role",
"tabIndex",
"accessKey",
"contentEditable",
"draggable",
"hidden",
"spellCheck",
"translate",
// ARIA 속성 (aria-로 시작)
// data 속성 (data-로 시작)
// 표준 이벤트 핸들러
"onClick",
"onDoubleClick",
"onMouseDown",
"onMouseUp",
"onMouseOver",
"onMouseOut",
"onMouseEnter",
"onMouseLeave",
"onMouseMove",
"onKeyDown",
"onKeyUp",
"onKeyPress",
"onFocus",
"onBlur",
"onChange",
"onInput",
"onSubmit",
"onReset",
"onDragStart",
"onDragEnd",
"onDragOver",
"onDragEnter",
"onDragLeave",
"onDrop",
"onScroll",
"onWheel",
"onLoad",
"onError",
"onResize",
]);
/**
* props에서 React DOM props만
*/
export function filterDOMProps<T extends Record<string, any>>(props: T): Partial<T> {
const filtered: Partial<T> = {};
for (const [key, value] of Object.entries(props)) {
// React 전용 props는 제외
if (REACT_ONLY_PROPS.has(key)) {
continue;
}
// aria- 또는 data- 속성은 안전하게 포함
if (key.startsWith("aria-") || key.startsWith("data-")) {
filtered[key as keyof T] = value;
continue;
}
// 안전한 DOM props만 포함
if (SAFE_DOM_PROPS.has(key)) {
filtered[key as keyof T] = value;
}
}
return filtered;
}
/**
* props를 React DOM
*/
export function separateProps<T extends Record<string, any>>(
props: T,
): {
reactProps: Partial<T>;
domProps: Partial<T>;
} {
const reactProps: Partial<T> = {};
const domProps: Partial<T> = {};
for (const [key, value] of Object.entries(props)) {
if (REACT_ONLY_PROPS.has(key)) {
reactProps[key as keyof T] = value;
} else if (key.startsWith("aria-") || key.startsWith("data-") || SAFE_DOM_PROPS.has(key)) {
domProps[key as keyof T] = value;
}
// 둘 다 해당하지 않는 경우 무시 (안전을 위해)
}
return { reactProps, domProps };
}
/**
* React props
*/
export function isReactOnlyProp(propName: string): boolean {
return REACT_ONLY_PROPS.has(propName);
}
/**
* DOM props
*/
export function isDOMSafeProp(propName: string): boolean {
return SAFE_DOM_PROPS.has(propName) || propName.startsWith("aria-") || propName.startsWith("data-");
}
/**
* 디버깅용: 필터링된 props
*/
export function logFilteredProps<T extends Record<string, any>>(
originalProps: T,
componentName: string = "Component",
): void {
const { reactProps, domProps } = separateProps(originalProps);
console.group(`🔍 ${componentName} Props 필터링`);
console.log("📥 원본 props:", Object.keys(originalProps));
console.log("⚛️ React 전용 props:", Object.keys(reactProps));
console.log("🌐 DOM 안전 props:", Object.keys(domProps));
// React 전용 props가 DOM에 전달될 뻔한 경우 경고
const reactPropsKeys = Object.keys(reactProps);
if (reactPropsKeys.length > 0) {
console.warn("⚠️ 다음 React 전용 props가 필터링되었습니다:", reactPropsKeys);
}
console.groupEnd();
}