테스트테이블 생성 및 오류 수정
This commit is contained in:
parent
ddcecfd5e2
commit
f7d884568b
|
|
@ -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 *
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 : {}),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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로 설정
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
Loading…
Reference in New Issue