diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 825ce956..8445f5e1 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -89,7 +89,7 @@ const CascadingDropdownWrapper: React.FC = ({ relationCode, parentValue, }); - + // 실제 사용할 설정 (직접 설정 또는 API에서 가져온 설정) const effectiveConfig = config || relationConfig; @@ -110,11 +110,7 @@ const CascadingDropdownWrapper: React.FC = ({ const isDisabled = disabled || !parentValue || loading; return ( - onChange?.(newValue)} disabled={isDisabled}> {loading ? (
@@ -191,7 +187,7 @@ export const InteractiveScreenViewer: React.FC = ( const { userLang } = useMultiLang(); // 다국어 훅 const [localFormData, setLocalFormData] = useState>({}); const [dateValues, setDateValues] = useState>({}); - + // 다국어 번역 상태 (langKeyId가 있는 컴포넌트들의 번역 텍스트) const [translations, setTranslations] = useState>({}); @@ -200,7 +196,7 @@ export const InteractiveScreenViewer: React.FC = ( const loadTranslations = async () => { // 모든 컴포넌트에서 langKey 수집 const langKeysToFetch: string[] = []; - + const collectLangKeys = (comps: ComponentData[]) => { comps.forEach((comp) => { // 컴포넌트 라벨의 langKey @@ -217,22 +213,26 @@ export const InteractiveScreenViewer: React.FC = ( } }); }; - + collectLangKeys(allComponents); - + // langKey가 있으면 배치 조회 if (langKeysToFetch.length > 0 && userLang) { try { const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.post("/multilang/batch", { - langKeys: [...new Set(langKeysToFetch)], // 중복 제거 - }, { - params: { - userLang, - companyCode: user?.companyCode || "*", + const response = await apiClient.post( + "/multilang/batch", + { + langKeys: [...new Set(langKeysToFetch)], // 중복 제거 }, - }); - + { + params: { + userLang, + companyCode: user?.companyCode || "*", + }, + }, + ); + if (response.data?.success && response.data?.data) { setTranslations(response.data.data); } @@ -241,23 +241,23 @@ export const InteractiveScreenViewer: React.FC = ( } } }; - + loadTranslations(); }, [allComponents, userLang, user?.companyCode]); - + // 팝업 화면 상태 const [popupScreen, setPopupScreen] = useState<{ screenId: number; title: string; size: string; } | null>(null); - + // 팝업 화면 레이아웃 상태 const [popupLayout, setPopupLayout] = useState([]); const [popupLoading, setPopupLoading] = useState(false); const [popupScreenResolution, setPopupScreenResolution] = useState<{ width: number; height: number } | null>(null); const [popupScreenInfo, setPopupScreenInfo] = useState<{ id: number; tableName?: string } | null>(null); - + // 팝업 전용 formData 상태 const [popupFormData, setPopupFormData] = useState>({}); @@ -265,64 +265,68 @@ export const InteractiveScreenViewer: React.FC = ( const finalFormData = { ...localFormData, ...externalFormData }; // 개선된 검증 시스템 (선택적 활성화) - const enhancedValidation = enableEnhancedValidation && screenInfo && tableColumns.length > 0 - ? useFormValidation( - finalFormData, - allComponents.filter(c => c.type === 'widget') as WidgetComponent[], - tableColumns, - { - id: screenInfo.id, - screenName: screenInfo.tableName || "unknown", - tableName: screenInfo.tableName, - screenResolution: { width: 800, height: 600 }, - gridSettings: { size: 20, color: "#e0e0e0", opacity: 0.5 }, - description: "동적 화면" - }, - { - enableRealTimeValidation: true, - validationDelay: 300, - enableAutoSave: false, - showToastMessages: true, - ...validationOptions, - } - ) - : null; + const enhancedValidation = + enableEnhancedValidation && screenInfo && tableColumns.length > 0 + ? useFormValidation( + finalFormData, + allComponents.filter((c) => c.type === "widget") as WidgetComponent[], + tableColumns, + { + id: screenInfo.id, + screenName: screenInfo.tableName || "unknown", + tableName: screenInfo.tableName, + screenResolution: { width: 800, height: 600 }, + gridSettings: { size: 20, color: "#e0e0e0", opacity: 0.5 }, + description: "동적 화면", + }, + { + enableRealTimeValidation: true, + validationDelay: 300, + enableAutoSave: false, + showToastMessages: true, + ...validationOptions, + }, + ) + : null; // 자동값 생성 함수 - const generateAutoValue = useCallback(async (autoValueType: string, ruleId?: string): Promise => { - const now = new Date(); - switch (autoValueType) { - case "current_datetime": - return now.toISOString().slice(0, 19).replace("T", " "); // YYYY-MM-DD HH:mm:ss - case "current_date": - return now.toISOString().slice(0, 10); // YYYY-MM-DD - case "current_time": - return now.toTimeString().slice(0, 8); // HH:mm:ss - case "current_user": - // 실제 접속중인 사용자명 사용 - return userName || "사용자"; // 사용자명이 없으면 기본값 - case "uuid": - return crypto.randomUUID(); - case "sequence": - return `SEQ_${Date.now()}`; - case "numbering_rule": - // 채번 규칙 사용 - if (ruleId) { - try { - const { generateNumberingCode } = await import("@/lib/api/numberingRule"); - const response = await generateNumberingCode(ruleId); - if (response.success && response.data) { - return response.data.generatedCode; + const generateAutoValue = useCallback( + async (autoValueType: string, ruleId?: string): Promise => { + const now = new Date(); + switch (autoValueType) { + case "current_datetime": + return now.toISOString().slice(0, 19).replace("T", " "); // YYYY-MM-DD HH:mm:ss + case "current_date": + return now.toISOString().slice(0, 10); // YYYY-MM-DD + case "current_time": + return now.toTimeString().slice(0, 8); // HH:mm:ss + case "current_user": + // 실제 접속중인 사용자명 사용 + return userName || "사용자"; // 사용자명이 없으면 기본값 + case "uuid": + return crypto.randomUUID(); + case "sequence": + return `SEQ_${Date.now()}`; + case "numbering_rule": + // 채번 규칙 사용 + if (ruleId) { + try { + const { generateNumberingCode } = await import("@/lib/api/numberingRule"); + const response = await generateNumberingCode(ruleId); + if (response.success && response.data) { + return response.data.generatedCode; + } + } catch (error) { + console.error("채번 규칙 코드 생성 실패:", error); } - } catch (error) { - console.error("채번 규칙 코드 생성 실패:", error); } - } - return ""; - default: - return ""; - } - }, [userName]); // userName 의존성 추가 + return ""; + default: + return ""; + } + }, + [userName], + ); // userName 의존성 추가 // 팝업 화면 레이아웃 로드 React.useEffect(() => { @@ -331,29 +335,29 @@ export const InteractiveScreenViewer: React.FC = ( try { setPopupLoading(true); // console.log("🔍 팝업 화면 로드 시작:", popupScreen); - + // 화면 레이아웃과 화면 정보를 병렬로 가져오기 const [layout, screen] = await Promise.all([ screenApi.getLayout(popupScreen.screenId), - screenApi.getScreen(popupScreen.screenId) + screenApi.getScreen(popupScreen.screenId), ]); - + console.log("📊 팝업 화면 로드 완료:", { componentsCount: layout.components?.length || 0, screenInfo: { screenId: screen.screenId, - tableName: screen.tableName + tableName: screen.tableName, }, - popupFormData: {} + popupFormData: {}, }); - + setPopupLayout(layout.components || []); setPopupScreenResolution(layout.screenResolution || null); setPopupScreenInfo({ id: popupScreen.screenId, - tableName: screen.tableName + tableName: screen.tableName, }); - + // 팝업 formData 초기화 setPopupFormData({}); } catch (error) { @@ -364,7 +368,7 @@ export const InteractiveScreenViewer: React.FC = ( setPopupLoading(false); } }; - + loadPopupLayout(); } }, [popupScreen]); @@ -375,7 +379,7 @@ export const InteractiveScreenViewer: React.FC = ( external: externalFormData, local: localFormData, merged: formData, - hasExternalCallback: !!onFormDataChange + hasExternalCallback: !!onFormDataChange, }); // 폼 데이터 업데이트 @@ -384,16 +388,16 @@ export const InteractiveScreenViewer: React.FC = ( if (isPreviewMode) { return; } - + // console.log(`🔄 updateFormData: ${fieldName} = "${value}" (외부콜백: ${!!onFormDataChange})`); - + // 항상 로컬 상태도 업데이트 setLocalFormData((prev) => ({ ...prev, [fieldName]: value, })); // console.log(`💾 로컬 상태 업데이트: ${fieldName} = "${value}"`); - + // 외부 콜백이 있는 경우에도 전달 (개별 필드 단위로) if (onFormDataChange) { onFormDataChange(fieldName, value); @@ -408,29 +412,24 @@ export const InteractiveScreenViewer: React.FC = ( // console.log("🔧 initAutoInputFields 실행 시작"); for (const comp of allComponents) { // 🆕 type: "component" 또는 type: "widget" 모두 처리 - if (comp.type === 'widget' || comp.type === 'component') { + if (comp.type === "widget" || comp.type === "component") { const widget = comp as WidgetComponent; const fieldName = widget.columnName || widget.id; - + // 🆕 autoFill 처리 (테이블 조회 기반 자동 입력) if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) { const autoFillConfig = widget.autoFill || (comp as any).autoFill; const currentValue = formData[fieldName]; - if (currentValue === undefined || currentValue === '') { + if (currentValue === undefined || currentValue === "") { const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig; - + // 사용자 정보에서 필터 값 가져오기 const userValue = user?.[userField]; - + if (userValue && sourceTable && filterColumn && displayColumn) { try { - const result = await tableTypeApi.getTableRecord( - sourceTable, - filterColumn, - userValue, - displayColumn - ); - + const result = await tableTypeApi.getTableRecord(sourceTable, filterColumn, userValue, displayColumn); + updateFormData(fieldName, result.value); } catch (error) { console.error(`autoFill 조회 실패: ${fieldName}`, error); @@ -439,37 +438,40 @@ export const InteractiveScreenViewer: React.FC = ( } continue; // autoFill이 활성화되면 일반 자동입력은 건너뜀 } - + // 기존 widget 타입 전용 로직은 widget인 경우만 - if (comp.type !== 'widget') continue; - + if (comp.type !== "widget") continue; + // 텍스트 타입 위젯의 자동입력 처리 (기존 로직) - if ((widget.widgetType === 'text' || widget.widgetType === 'email' || widget.widgetType === 'tel') && - widget.webTypeConfig) { + if ( + (widget.widgetType === "text" || widget.widgetType === "email" || widget.widgetType === "tel") && + widget.webTypeConfig + ) { const config = widget.webTypeConfig as TextTypeConfig; const isAutoInput = config?.autoInput || false; - + if (isAutoInput && config?.autoValueType) { // 이미 값이 있으면 덮어쓰지 않음 const currentValue = formData[fieldName]; console.log(`🔍 자동입력 필드 체크: ${fieldName}`, { currentValue, - isEmpty: currentValue === undefined || currentValue === '', + isEmpty: currentValue === undefined || currentValue === "", isAutoInput, - autoValueType: config.autoValueType + autoValueType: config.autoValueType, }); - - if (currentValue === undefined || currentValue === '') { - const autoValue = config.autoValueType === "custom" - ? config.customValue || "" - : generateAutoValue(config.autoValueType); - + + if (currentValue === undefined || currentValue === "") { + const autoValue = + config.autoValueType === "custom" + ? config.customValue || "" + : generateAutoValue(config.autoValueType); + console.log("🔄 자동입력 필드 초기화:", { fieldName, autoValueType: config.autoValueType, - autoValue + autoValue, }); - + updateFormData(fieldName, autoValue); } else { // console.log(`⏭️ 자동입력 건너뜀 (값 있음): ${fieldName} = "${currentValue}"`); @@ -526,7 +528,7 @@ export const InteractiveScreenViewer: React.FC = ( const FlowWidget = require("@/components/screen/widgets/FlowWidget").FlowWidget; // componentConfig에서 flowId 추출 const flowConfig = (comp as any).componentConfig || {}; - + console.log("🔍 InteractiveScreenViewer 플로우 위젯 변환:", { compType: comp.type, hasComponentConfig: !!(comp as any).componentConfig, @@ -534,7 +536,7 @@ export const InteractiveScreenViewer: React.FC = ( flowConfigFlowId: flowConfig.flowId, finalFlowId: flowConfig.flowId, }); - + const flowComponent = { ...comp, type: "flow" as const, @@ -544,9 +546,9 @@ export const InteractiveScreenViewer: React.FC = ( allowDataMove: flowConfig.allowDataMove || false, displayMode: flowConfig.displayMode || "horizontal", }; - + console.log("🔍 InteractiveScreenViewer 최종 flowComponent:", flowComponent); - + return (
@@ -558,7 +560,7 @@ export const InteractiveScreenViewer: React.FC = ( const componentType = (comp as any).componentType || (comp as any).componentId; if (comp.type === "tabs" || (comp.type === "component" && componentType === "tabs-widget")) { const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget; - + // componentConfig에서 탭 정보 추출 const tabsConfig = comp.componentConfig || {}; const tabsComponent = { @@ -571,7 +573,7 @@ export const InteractiveScreenViewer: React.FC = ( allowCloseable: tabsConfig.allowCloseable || false, persistSelection: tabsConfig.persistSelection || false, }; - + console.log("🔍 탭 컴포넌트 렌더링:", { originalType: comp.type, componentType, @@ -579,11 +581,11 @@ export const InteractiveScreenViewer: React.FC = ( tabs: tabsComponent.tabs, tabsConfig, }); - + return (
-
@@ -596,7 +598,7 @@ export const InteractiveScreenViewer: React.FC = ( const componentConfig = (comp as any).componentConfig || {}; // config가 중첩되어 있을 수 있음: componentConfig.config 또는 componentConfig 직접 const rackConfig = componentConfig.config || componentConfig; - + console.log("🏗️ 렉 구조 컴포넌트 렌더링:", { componentType, componentConfig, @@ -604,7 +606,7 @@ export const InteractiveScreenViewer: React.FC = ( fieldMapping: rackConfig.fieldMapping, formData, }); - + return (
= ( const { widgetType, label: originalLabel, placeholder, required, readonly, columnName } = comp; const fieldName = columnName || comp.id; const currentValue = formData[fieldName] || ""; - + // 다국어 라벨 적용 (langKey가 있으면 번역 텍스트 사용) const compLangKey = (comp as any).langKey; const label = compLangKey && translations[compLangKey] ? translations[compLangKey] : originalLabel; @@ -642,7 +644,7 @@ export const InteractiveScreenViewer: React.FC = ( return React.cloneElement(element, { style: { ...element.props.style, // 기존 스타일 유지 - ...styleWithoutSize, // width/height 제외한 스타일만 적용 + ...styleWithoutSize, // width/height 제외한 스타일만 적용 boxSizing: "border-box", }, }); @@ -657,12 +659,13 @@ export const InteractiveScreenViewer: React.FC = ( // 자동입력 관련 처리 const isAutoInput = config?.autoInput || false; - const autoValue = isAutoInput && config?.autoValueType - ? config.autoValueType === "custom" - ? config.customValue || "" - : generateAutoValue(config.autoValueType) - : ""; - + const autoValue = + isAutoInput && config?.autoValueType + ? config.autoValueType === "custom" + ? config.customValue || "" + : generateAutoValue(config.autoValueType) + : ""; + // 기본값 또는 자동값 설정 const displayValue = isAutoInput ? autoValue : currentValue || config?.defaultValue || ""; @@ -855,17 +858,17 @@ export const InteractiveScreenViewer: React.FC = ( }); const finalPlaceholder = config?.placeholder || placeholder || "선택하세요..."; - + // 🆕 연쇄 드롭다운 처리 (방법 1: 관계 코드 방식 - 권장) if (config?.cascadingRelationCode && config?.cascadingParentField) { const parentFieldValue = formData[config.cascadingParentField]; - + console.log("🔗 연쇄 드롭다운 (관계코드 방식):", { relationCode: config.cascadingRelationCode, parentField: config.cascadingParentField, parentValue: parentFieldValue, }); - + return applyStyles( = ( />, ); } - + // 🔄 연쇄 드롭다운 처리 (방법 2: 직접 설정 방식 - 레거시) if (config?.cascading?.enabled) { const cascadingConfig = config.cascading; const parentValue = formData[cascadingConfig.parentField]; - + return applyStyles( = ( />, ); } - + // 일반 Select const options = config?.options || [ { label: "옵션 1", value: "option1" }, @@ -1066,7 +1069,7 @@ export const InteractiveScreenViewer: React.FC = ( min={config?.minDate} max={config?.maxDate} className="w-full" - style={{ height: "100%" }} + style={{ height: "100%" }} />, ); } else { @@ -1134,19 +1137,20 @@ export const InteractiveScreenViewer: React.FC = ( case "file": { const widget = comp as WidgetComponent; const config = widget.webTypeConfig as FileTypeConfig | undefined; - + // 현재 파일 값 가져오기 const getCurrentValue = () => { const fieldName = widget.columnName || widget.id; return (externalFormData?.[fieldName] || localFormData[fieldName]) as any; }; - + const currentValue = getCurrentValue(); // 화면 ID 추출 (URL에서) - const screenId = typeof window !== 'undefined' && window.location.pathname.includes('/screens/') - ? parseInt(window.location.pathname.split('/screens/')[1]) - : null; + const screenId = + typeof window !== "undefined" && window.location.pathname.includes("/screens/") + ? parseInt(window.location.pathname.split("/screens/")[1]) + : null; console.log("📁 InteractiveScreenViewer - File 위젯:", { componentId: widget.id, @@ -1168,14 +1172,14 @@ export const InteractiveScreenViewer: React.FC = ( e.target.value = ""; // 파일 선택 취소 return; } - + const files = e.target.files; const fieldName = widget.columnName || widget.id; - + // 파일 선택을 취소한 경우 (files가 null이거나 길이가 0) if (!files || files.length === 0) { // console.log("📁 파일 선택 취소됨 - 기존 파일 유지"); - + // 현재 저장된 파일이 있는지 확인 const currentStoredValue = externalFormData?.[fieldName] || localFormData[fieldName]; if (currentStoredValue) { @@ -1204,19 +1208,19 @@ export const InteractiveScreenViewer: React.FC = ( // 실제 서버로 파일 업로드 try { toast.loading(`${files.length}개 파일 업로드 중...`); - + const uploadResult = await uploadFilesAndCreateData(files); - + if (uploadResult.success) { // console.log("📁 업로드 완료된 파일 데이터:", uploadResult.data); - - setLocalFormData(prev => ({ ...prev, [fieldName]: uploadResult.data })); - + + setLocalFormData((prev) => ({ ...prev, [fieldName]: uploadResult.data })); + // 외부 폼 데이터 변경 콜백 호출 if (onFormDataChange) { onFormDataChange(fieldName, uploadResult.data); } - + toast.success(uploadResult.message); } else { throw new Error("파일 업로드에 실패했습니다."); @@ -1224,7 +1228,7 @@ export const InteractiveScreenViewer: React.FC = ( } catch (error) { // console.error("파일 업로드 오류:", error); toast.error("파일 업로드에 실패했습니다."); - + // 파일 입력 초기화 e.target.value = ""; return; @@ -1233,13 +1237,13 @@ export const InteractiveScreenViewer: React.FC = ( const clearFile = () => { const fieldName = widget.columnName || widget.id; - setLocalFormData(prev => ({ ...prev, [fieldName]: null })); - + setLocalFormData((prev) => ({ ...prev, [fieldName]: null })); + // 외부 폼 데이터 변경 콜백 호출 if (onFormDataChange) { onFormDataChange(fieldName, null); } - + // 파일 input 초기화 const fileInput = document.querySelector(`input[type="file"][data-field="${fieldName}"]`) as HTMLInputElement; if (fileInput) { @@ -1253,39 +1257,31 @@ export const InteractiveScreenViewer: React.FC = ( // 새로운 JSON 구조에서 파일 정보 추출 const fileData = currentValue.files || []; if (fileData.length === 0) return null; - + return (
-
- 업로드된 파일 ({fileData.length}개) -
+
업로드된 파일 ({fileData.length}개)
{fileData.map((fileInfo: any, index: number) => { - const isImage = fileInfo.type?.startsWith('image/'); - + const isImage = fileInfo.type?.startsWith("image/"); + return ( -
-
+
+
{isImage ? (
IMG
) : ( - + )}
-
-

{fileInfo.name}

-

- {(fileInfo.size / 1024 / 1024).toFixed(2)} MB +

+

{fileInfo.name}

+

{(fileInfo.size / 1024 / 1024).toFixed(2)} MB

+

{fileInfo.type || "알 수 없는 형식"}

+

+ 업로드: {new Date(fileInfo.uploadedAt).toLocaleString("ko-KR")}

-

{fileInfo.type || '알 수 없는 형식'}

-

업로드: {new Date(fileInfo.uploadedAt).toLocaleString('ko-KR')}

-
@@ -1296,7 +1292,7 @@ export const InteractiveScreenViewer: React.FC = ( }; const fieldName = widget.columnName || widget.id; - + return applyStyles(
{/* 파일 선택 영역 */} @@ -1309,45 +1305,45 @@ export const InteractiveScreenViewer: React.FC = ( required={required} multiple={config?.multiple} accept={config?.accept} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed" + className="absolute inset-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed" style={{ zIndex: 1 }} /> -
0 - ? 'border-success/30 bg-success/10' - : 'border-input bg-muted hover:border-input/80 hover:bg-muted/80', - readonly && 'cursor-not-allowed opacity-50', - !readonly && 'cursor-pointer' - )}> +
0 + ? "border-success/30 bg-success/10" + : "border-input bg-muted hover:border-input/80 hover:bg-muted/80", + readonly && "cursor-not-allowed opacity-50", + !readonly && "cursor-pointer", + )} + >
{currentValue && currentValue.files && currentValue.files.length > 0 ? ( <>
-
- +
+
-

- {currentValue.totalCount === 1 - ? '파일 선택됨' - : `${currentValue.totalCount}개 파일 선택됨`} +

+ {currentValue.totalCount === 1 ? "파일 선택됨" : `${currentValue.totalCount}개 파일 선택됨`}

-

+

총 {(currentValue.totalSize / 1024 / 1024).toFixed(2)}MB

-

클릭하여 다른 파일 선택

+

클릭하여 다른 파일 선택

) : ( <> - -

- {config?.dragDrop ? '파일을 드래그하여 놓거나 클릭하여 선택' : '클릭하여 파일 선택'} + +

+ {config?.dragDrop ? "파일을 드래그하여 놓거나 클릭하여 선택" : "클릭하여 파일 선택"}

{(config?.accept || config?.maxSize) && ( -
+
{config.accept &&
허용 형식: {config.accept}
} {config.maxSize &&
최대 크기: {config.maxSize}MB
} {config.multiple &&
다중 선택 가능
} @@ -1358,10 +1354,10 @@ export const InteractiveScreenViewer: React.FC = (
- + {/* 파일 미리보기 */} {renderFilePreview()} -
+
, ); } @@ -1369,7 +1365,7 @@ export const InteractiveScreenViewer: React.FC = ( const widget = comp as WidgetComponent; const config = widget.webTypeConfig as CodeTypeConfig | undefined; - console.log(`🔍 [InteractiveScreenViewer] Code 위젯 렌더링:`, { + console.log("🔍 [InteractiveScreenViewer] Code 위젯 렌더링:", { componentId: widget.id, columnName: widget.columnName, codeCategory: config?.codeCategory, @@ -1403,11 +1399,11 @@ export const InteractiveScreenViewer: React.FC = ( onEvent={(event: string, data: any) => { // console.log(`Code widget event: ${event}`, data); }} - /> + />, ); } catch (error) { // console.error("DynamicWebTypeRenderer 로딩 실패, 기본 Select 사용:", error); - + // 폴백: 기본 Select 컴포넌트 사용 return applyStyles( + , ); } } @@ -1459,9 +1455,9 @@ export const InteractiveScreenViewer: React.FC = ( if (isPreviewMode) { return; } - + const actionType = config?.actionType || "save"; - + try { switch (actionType) { case "save": @@ -1498,7 +1494,7 @@ export const InteractiveScreenViewer: React.FC = ( await handleCustomAction(); break; default: - // console.log(`알 수 없는 액션 타입: ${actionType}`); + // console.log(`알 수 없는 액션 타입: ${actionType}`); } } catch (error) { // console.error(`버튼 액션 실행 오류 (${actionType}):`, error); @@ -1529,24 +1525,24 @@ export const InteractiveScreenViewer: React.FC = ( // 기존 방식 (레거시 지원) const currentFormData = { ...localFormData, ...externalFormData }; // console.log("💾 기존 방식으로 저장 - currentFormData:", currentFormData); - + // formData 유효성 체크를 완화 (빈 객체라도 위젯이 있으면 저장 진행) - const hasWidgets = allComponents.some(comp => comp.type === 'widget'); + const hasWidgets = allComponents.some((comp) => comp.type === "widget"); if (!hasWidgets) { alert("저장할 입력 컴포넌트가 없습니다."); return; } // 필수 항목 검증 - const requiredFields = allComponents.filter(c => c.required && (c.columnName || c.id)); - const missingFields = requiredFields.filter(field => { + const requiredFields = allComponents.filter((c) => c.required && (c.columnName || c.id)); + const missingFields = requiredFields.filter((field) => { const fieldName = field.columnName || field.id; const value = currentFormData[fieldName]; return !value || value.toString().trim() === ""; }); if (missingFields.length > 0) { - const fieldNames = missingFields.map(f => f.label || f.columnName || f.id).join(", "); + const fieldNames = missingFields.map((f) => f.label || f.columnName || f.id).join(", "); alert(`다음 필수 항목을 입력해주세요: ${fieldNames}`); return; } @@ -1559,56 +1555,59 @@ export const InteractiveScreenViewer: React.FC = ( try { // 컬럼명 기반으로 데이터 매핑 const mappedData: Record = {}; - + // 입력 가능한 컴포넌트에서 데이터 수집 - allComponents.forEach(comp => { + allComponents.forEach((comp) => { // 위젯 컴포넌트이고 입력 가능한 타입인 경우 - if (comp.type === 'widget') { + if (comp.type === "widget") { const widget = comp as WidgetComponent; const fieldName = widget.columnName || widget.id; let value = currentFormData[fieldName]; - + console.log(`🔍 컴포넌트 처리: ${fieldName}`, { widgetType: widget.widgetType, formDataValue: value, hasWebTypeConfig: !!widget.webTypeConfig, - config: widget.webTypeConfig + config: widget.webTypeConfig, }); - + // 자동입력 필드인 경우에만 값이 없을 때 생성 - if ((widget.widgetType === 'text' || widget.widgetType === 'email' || widget.widgetType === 'tel') && - widget.webTypeConfig) { + if ( + (widget.widgetType === "text" || widget.widgetType === "email" || widget.widgetType === "tel") && + widget.webTypeConfig + ) { const config = widget.webTypeConfig as TextTypeConfig; const isAutoInput = config?.autoInput || false; - + console.log(`📋 ${fieldName} 자동입력 체크:`, { isAutoInput, autoValueType: config?.autoValueType, hasValue: !!value, - value + value, }); - - if (isAutoInput && config?.autoValueType && (!value || value === '')) { + + if (isAutoInput && config?.autoValueType && (!value || value === "")) { // 자동입력이고 값이 없을 때만 생성 - value = config.autoValueType === "custom" - ? config.customValue || "" - : generateAutoValue(config.autoValueType); - + value = + config.autoValueType === "custom" + ? config.customValue || "" + : generateAutoValue(config.autoValueType); + console.log("💾 자동입력 값 저장 (값이 없어서 생성):", { fieldName, autoValueType: config.autoValueType, - generatedValue: value + generatedValue: value, }); } else if (isAutoInput && value) { console.log("💾 자동입력 필드지만 기존 값 유지:", { fieldName, - existingValue: value + existingValue: value, }); } else if (!isAutoInput) { // console.log(`📝 일반 입력 필드: ${fieldName} = "${value}"`); } } - + // 값이 있는 경우만 매핑 (빈 문자열도 포함하되, undefined는 제외) if (value !== undefined && value !== null && value !== "undefined") { // columnName이 있으면 columnName을 키로, 없으면 컴포넌트 ID를 키로 사용 @@ -1627,35 +1626,34 @@ export const InteractiveScreenViewer: React.FC = ( 매핑된데이터: mappedData, 화면정보: screenInfo, 전체컴포넌트수: allComponents.length, - 위젯컴포넌트수: allComponents.filter(c => c.type === 'widget').length, + 위젯컴포넌트수: allComponents.filter((c) => c.type === "widget").length, }); // 각 컴포넌트의 상세 정보 로그 // console.log("🔍 컴포넌트별 데이터 수집 상세:"); - allComponents.forEach(comp => { - if (comp.type === 'widget') { + allComponents.forEach((comp) => { + if (comp.type === "widget") { const widget = comp as WidgetComponent; const fieldName = widget.columnName || widget.id; const value = currentFormData[fieldName]; - const hasValue = value !== undefined && value !== null && value !== ''; + const hasValue = value !== undefined && value !== null && value !== ""; // console.log(` - ${fieldName} (${widget.widgetType}): "${value}" (값있음: ${hasValue}, 컬럼명: ${widget.columnName})`); } }); - + // 매핑된 데이터가 비어있으면 경고 if (Object.keys(mappedData).length === 0) { // console.warn("⚠️ 매핑된 데이터가 없습니다. 빈 데이터로 저장됩니다."); } // 테이블명 결정 (화면 정보에서 가져오거나 첫 번째 컴포넌트의 테이블명 사용) - const tableName = screenInfo.tableName || - allComponents.find(c => c.columnName)?.tableName || - "dynamic_form_data"; // 기본값 + const tableName = + screenInfo.tableName || allComponents.find((c) => c.columnName)?.tableName || "dynamic_form_data"; // 기본값 // 🆕 자동으로 작성자 정보 추가 (user.userId가 확실히 있음) const writerValue = user.userId; const companyCodeValue = user.companyCode || ""; - + console.log("👤 현재 사용자 정보:", { userId: user.userId, userName: userName, @@ -1687,11 +1685,11 @@ export const InteractiveScreenViewer: React.FC = ( if (result.success) { alert("저장되었습니다."); // console.log("✅ 저장 성공:", result.data); - + // 저장 후 데이터 초기화 (선택사항) if (onFormDataChange) { const resetData: Record = {}; - Object.keys(formData).forEach(key => { + Object.keys(formData).forEach((key) => { resetData[key] = ""; }); onFormDataChange(resetData); @@ -1705,27 +1703,25 @@ export const InteractiveScreenViewer: React.FC = ( } }; - // 삭제 액션 const handleDeleteAction = async () => { const confirmMessage = config?.confirmMessage || "정말로 삭제하시겠습니까?"; - + if (!confirm(confirmMessage)) { return; } // 삭제할 레코드 ID가 필요 (폼 데이터에서 id 필드 찾기) const recordId = formData["id"] || formData["ID"] || formData["objid"]; - + if (!recordId) { alert("삭제할 데이터를 찾을 수 없습니다. (ID가 없음)"); return; } // 테이블명 결정 - const tableName = screenInfo?.tableName || - allComponents.find(c => c.columnName)?.tableName || - "unknown_table"; + const tableName = + screenInfo?.tableName || allComponents.find((c) => c.columnName)?.tableName || "unknown_table"; if (!tableName || tableName === "unknown_table") { alert("테이블 정보가 없어 삭제할 수 없습니다."); @@ -1741,11 +1737,11 @@ export const InteractiveScreenViewer: React.FC = ( if (result.success) { alert("삭제되었습니다."); // console.log("✅ 삭제 성공"); - + // 삭제 후 폼 초기화 if (onFormDataChange) { const resetData: Record = {}; - Object.keys(formData).forEach(key => { + Object.keys(formData).forEach((key) => { resetData[key] = ""; }); onFormDataChange(resetData); @@ -1762,13 +1758,13 @@ export const InteractiveScreenViewer: React.FC = ( // 편집 액션 const handleEditAction = () => { console.log("✏️ 수정 액션 실행"); - + // 버튼 컴포넌트의 수정 모달 설정 가져오기 const editModalTitle = config?.editModalTitle || ""; const editModalDescription = config?.editModalDescription || ""; - + console.log("📝 버튼 수정 모달 설정:", { editModalTitle, editModalDescription }); - + // EditModal 열기 이벤트 발생 const event = new CustomEvent("openEditModal", { detail: { @@ -1797,7 +1793,7 @@ export const InteractiveScreenViewer: React.FC = ( const handleSearchAction = () => { // console.log("🔍 검색 실행:", formData); // 검색 로직 - const searchTerms = Object.values(formData).filter(v => v && v.toString().trim()); + const searchTerms = Object.values(formData).filter((v) => v && v.toString().trim()); if (searchTerms.length === 0) { alert("검색할 내용을 입력해주세요."); } else { @@ -1810,7 +1806,7 @@ export const InteractiveScreenViewer: React.FC = ( if (confirm("모든 입력을 초기화하시겠습니까?")) { if (onFormDataChange) { const resetData: Record = {}; - Object.keys(formData).forEach(key => { + Object.keys(formData).forEach((key) => { resetData[key] = ""; }); onFormDataChange(resetData); @@ -1830,22 +1826,24 @@ export const InteractiveScreenViewer: React.FC = ( // 닫기 액션 const handleCloseAction = () => { // console.log("❌ 닫기 액션 실행"); - + // 모달 내부에서 실행되는지 확인 const isInModal = document.querySelector('[role="dialog"]') !== null; const isInPopup = window.opener !== null; - + if (isInModal) { // 모달 내부인 경우: 모달의 닫기 버튼 클릭하거나 모달 닫기 이벤트 발생 // console.log("🔄 모달 내부에서 닫기 - 모달 닫기 시도"); - + // 모달의 닫기 버튼을 찾아서 클릭 - const modalCloseButton = document.querySelector('[role="dialog"] button[aria-label*="Close"], [role="dialog"] button[data-dismiss="modal"], [role="dialog"] .dialog-close'); + const modalCloseButton = document.querySelector( + '[role="dialog"] button[aria-label*="Close"], [role="dialog"] button[data-dismiss="modal"], [role="dialog"] .dialog-close', + ); if (modalCloseButton) { (modalCloseButton as HTMLElement).click(); } else { // ESC 키 이벤트 발생시키기 - const escEvent = new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, which: 27 }); + const escEvent = new KeyboardEvent("keydown", { key: "Escape", keyCode: 27, which: 27 }); document.dispatchEvent(escEvent); } } else if (isInPopup) { @@ -1862,7 +1860,7 @@ export const InteractiveScreenViewer: React.FC = ( // 팝업 액션 const handlePopupAction = () => { // console.log("🎯 팝업 액션 실행:", { popupScreenId: config?.popupScreenId }); - + if (config?.popupScreenId) { // 화면 모달 열기 setPopupScreen({ @@ -1881,17 +1879,17 @@ export const InteractiveScreenViewer: React.FC = ( // 네비게이션 액션 const handleNavigateAction = () => { const navigateType = config?.navigateType || "url"; - + if (navigateType === "screen" && config?.navigateScreenId) { // 화면으로 이동 const screenPath = `/screens/${config.navigateScreenId}`; - + console.log("🎯 화면으로 이동:", { screenId: config.navigateScreenId, target: config.navigateTarget || "_self", - path: screenPath + path: screenPath, }); - + if (config.navigateTarget === "_blank") { window.open(screenPath, "_blank"); } else { @@ -1901,9 +1899,9 @@ export const InteractiveScreenViewer: React.FC = ( // URL로 이동 console.log("🔗 URL로 이동:", { url: config.navigateUrl, - target: config.navigateTarget || "_self" + target: config.navigateTarget || "_self", }); - + if (config.navigateTarget === "_blank") { window.open(config.navigateUrl, "_blank"); } else { @@ -1913,7 +1911,7 @@ export const InteractiveScreenViewer: React.FC = ( console.log("🔗 네비게이션 정보가 설정되지 않았습니다:", { navigateType, hasUrl: !!config?.navigateUrl, - hasScreenId: !!config?.navigateScreenId + hasScreenId: !!config?.navigateScreenId, }); } }; @@ -1938,21 +1936,22 @@ export const InteractiveScreenViewer: React.FC = ( // 버튼 텍스트 다국어 적용 (componentConfig.langKey 확인) const buttonLangKey = (widget as any).componentConfig?.langKey; - const buttonText = buttonLangKey && translations[buttonLangKey] - ? translations[buttonLangKey] - : (widget as any).componentConfig?.text || label || "버튼"; + const buttonText = + buttonLangKey && translations[buttonLangKey] + ? translations[buttonLangKey] + : (widget as any).componentConfig?.text || label || "버튼"; // 커스텀 색상이 있으면 Tailwind 클래스 대신 직접 스타일 적용 const hasCustomColors = config?.backgroundColor || config?.textColor; - + return applyStyles( + , ); } @@ -1985,71 +1984,73 @@ export const InteractiveScreenViewer: React.FC = ( // 파일 첨부 컴포넌트 처리 if (isFileComponent(component)) { const fileComponent = component as FileComponent; - + console.log("🎯 File 컴포넌트 렌더링:", { componentId: fileComponent.id, currentUploadedFiles: fileComponent.uploadedFiles?.length || 0, hasOnFormDataChange: !!onFormDataChange, - userInfo: user ? { userId: user.userId, companyCode: user.companyCode } : "no user" + userInfo: user ? { userId: user.userId, companyCode: user.companyCode } : "no user", }); - - const handleFileUpdate = useCallback(async (updates: Partial) => { - // 실제 화면에서는 파일 업데이트를 처리 - console.log("📎 InteractiveScreenViewer - 파일 컴포넌트 업데이트:", { - updates, - hasUploadedFiles: !!updates.uploadedFiles, - uploadedFilesCount: updates.uploadedFiles?.length || 0, - hasOnFormDataChange: !!onFormDataChange - }); - - if (updates.uploadedFiles && onFormDataChange) { - const fieldName = fileComponent.columnName || fileComponent.id; - - // attach_file_info 테이블 구조에 맞는 데이터 생성 - const fileInfoForDB = updates.uploadedFiles.map(file => ({ - objid: file.objid.replace('temp_', ''), // temp_ 제거 - target_objid: "", - saved_file_name: file.savedFileName, - real_file_name: file.realFileName, - doc_type: file.docType, - doc_type_name: file.docTypeName, - file_size: file.fileSize, - file_ext: file.fileExt, - file_path: file.filePath, - writer: file.writer, - regdate: file.regdate, - status: file.status, - parent_target_objid: "", - company_code: file.companyCode - })); - // console.log("💾 attach_file_info 형태로 변환된 데이터:", fileInfoForDB); - - // FormData에는 파일 연결 정보만 저장 (간단한 형태) - const formDataValue = { - fileCount: updates.uploadedFiles.length, - docType: fileComponent.fileConfig.docType, - files: updates.uploadedFiles.map(file => ({ - objid: file.objid, - realFileName: file.realFileName, - fileSize: file.fileSize, - status: file.status - })) - }; - - // console.log("📝 FormData 저장값:", { fieldName, formDataValue }); - onFormDataChange(fieldName, formDataValue); - - // TODO: 실제 API 연동 시 attach_file_info 테이블에 저장 - // await saveFilesToDatabase(fileInfoForDB); - - } else { - console.warn("⚠️ 파일 업데이트 실패:", { + const handleFileUpdate = useCallback( + async (updates: Partial) => { + // 실제 화면에서는 파일 업데이트를 처리 + console.log("📎 InteractiveScreenViewer - 파일 컴포넌트 업데이트:", { + updates, hasUploadedFiles: !!updates.uploadedFiles, - hasOnFormDataChange: !!onFormDataChange + uploadedFilesCount: updates.uploadedFiles?.length || 0, + hasOnFormDataChange: !!onFormDataChange, }); - } - }, [fileComponent, onFormDataChange]); + + if (updates.uploadedFiles && onFormDataChange) { + const fieldName = fileComponent.columnName || fileComponent.id; + + // attach_file_info 테이블 구조에 맞는 데이터 생성 + const fileInfoForDB = updates.uploadedFiles.map((file) => ({ + objid: file.objid.replace("temp_", ""), // temp_ 제거 + target_objid: "", + saved_file_name: file.savedFileName, + real_file_name: file.realFileName, + doc_type: file.docType, + doc_type_name: file.docTypeName, + file_size: file.fileSize, + file_ext: file.fileExt, + file_path: file.filePath, + writer: file.writer, + regdate: file.regdate, + status: file.status, + parent_target_objid: "", + company_code: file.companyCode, + })); + + // console.log("💾 attach_file_info 형태로 변환된 데이터:", fileInfoForDB); + + // FormData에는 파일 연결 정보만 저장 (간단한 형태) + const formDataValue = { + fileCount: updates.uploadedFiles.length, + docType: fileComponent.fileConfig.docType, + files: updates.uploadedFiles.map((file) => ({ + objid: file.objid, + realFileName: file.realFileName, + fileSize: file.fileSize, + status: file.status, + })), + }; + + // console.log("📝 FormData 저장값:", { fieldName, formDataValue }); + onFormDataChange(fieldName, formDataValue); + + // TODO: 실제 API 연동 시 attach_file_info 테이블에 저장 + // await saveFilesToDatabase(fileInfoForDB); + } else { + console.warn("⚠️ 파일 업데이트 실패:", { + hasUploadedFiles: !!updates.uploadedFiles, + hasOnFormDataChange: !!onFormDataChange, + }); + } + }, + [fileComponent, onFormDataChange], + ); return (
@@ -2132,7 +2133,6 @@ export const InteractiveScreenViewer: React.FC = ( marginBottom: component.style?.labelMarginBottom || "4px", }; - // 상위에서 라벨을 표시한 경우, 컴포넌트 내부에서는 라벨을 숨김 const componentForRendering = shouldShowLabel ? { @@ -2148,112 +2148,122 @@ export const InteractiveScreenViewer: React.FC = ( -
- {/* 테이블 옵션 툴바 */} - - - {/* 메인 컨텐츠 */} -
- {/* 라벨이 있는 경우 표시 (데이터 테이블 제외) */} - {shouldShowLabel && ( - - )} +
+ {/* 테이블 옵션 툴바 */} + - {/* 실제 위젯 - 상위에서 라벨을 렌더링했으므로 자식은 라벨 숨김 */} -
{renderInteractiveWidget(componentForRendering)}
-
-
- - {/* 개선된 검증 패널 (선택적 표시) */} - {showValidationPanel && enhancedValidation && ( -
- { - const success = await enhancedValidation.saveForm(); - if (success) { - toast.success("데이터가 성공적으로 저장되었습니다!"); - } - }} - canSave={enhancedValidation.canSave} - compact={true} - showDetails={false} - /> -
- )} - - {/* 모달 화면 */} - { - setPopupScreen(null); - setPopupFormData({}); // 팝업 닫을 때 formData도 초기화 - }}> - - - {popupScreen?.title || "상세 정보"} - - -
- {popupLoading ? ( -
-
화면을 불러오는 중...
-
- ) : popupLayout.length > 0 ? ( -
- {/* 팝업에서도 실제 위치와 크기로 렌더링 */} - {popupLayout.map((popupComponent) => ( -
- {/* 🎯 핵심 수정: 팝업 전용 formData 사용 */} - { - console.log("💾 팝업 formData 업데이트:", { - fieldName, - value, - valueType: typeof value, - prevFormData: popupFormData - }); - - setPopupFormData(prev => ({ - ...prev, - [fieldName]: value - })); - }} - /> -
- ))} -
- ) : ( -
-
화면 데이터가 없습니다.
-
+ {/* 메인 컨텐츠 */} +
+ {/* 라벨이 있는 경우 표시 (데이터 테이블 제외) */} + {shouldShowLabel && ( + )} + + {/* 실제 위젯 - 상위에서 라벨을 렌더링했으므로 자식은 라벨 숨김 */} +
+ {renderInteractiveWidget(componentForRendering)} +
- -
+
+ + {/* 개선된 검증 패널 (선택적 표시) */} + {showValidationPanel && enhancedValidation && ( +
+ { + const success = await enhancedValidation.saveForm(); + if (success) { + toast.success("데이터가 성공적으로 저장되었습니다!"); + } + }} + canSave={enhancedValidation.canSave} + compact={true} + showDetails={false} + /> +
+ )} + + {/* 모달 화면 */} + { + setPopupScreen(null); + setPopupFormData({}); // 팝업 닫을 때 formData도 초기화 + }} + > + + + {popupScreen?.title || "상세 정보"} + + +
+ {popupLoading ? ( +
+
화면을 불러오는 중...
+
+ ) : popupLayout.length > 0 ? ( +
+ {/* 팝업에서도 실제 위치와 크기로 렌더링 */} + {popupLayout.map((popupComponent) => ( +
+ {/* 🎯 핵심 수정: 팝업 전용 formData 사용 */} + { + console.log("💾 팝업 formData 업데이트:", { + fieldName, + value, + valueType: typeof value, + prevFormData: popupFormData, + }); + + setPopupFormData((prev) => ({ + ...prev, + [fieldName]: value, + })); + }} + /> +
+ ))} +
+ ) : ( +
+
화면 데이터가 없습니다.
+
+ )} +
+
+