diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 3b424dac..b048d498 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -129,9 +129,57 @@ export class DynamicFormService { dataToInsert.updated_by = updated_by; } if (company_code && tableColumns.includes("company_code")) { - dataToInsert.company_code = company_code; + // company_code가 UUID 형태(36자)라면 하이픈 제거하여 32자로 만듦 + let processedCompanyCode = company_code; + if ( + typeof company_code === "string" && + company_code.length === 36 && + company_code.includes("-") + ) { + processedCompanyCode = company_code.replace(/-/g, ""); + console.log( + `🔧 company_code 길이 조정: "${company_code}" -> "${processedCompanyCode}" (${processedCompanyCode.length}자)` + ); + } + // 여전히 32자를 초과하면 앞의 32자만 사용 + if ( + typeof processedCompanyCode === "string" && + processedCompanyCode.length > 32 + ) { + processedCompanyCode = processedCompanyCode.substring(0, 32); + console.log( + `⚠️ company_code 길이 제한: 앞의 32자로 자름 -> "${processedCompanyCode}"` + ); + } + dataToInsert.company_code = processedCompanyCode; } + // 날짜/시간 문자열을 적절한 형태로 변환 + Object.keys(dataToInsert).forEach((key) => { + const value = dataToInsert[key]; + + // 날짜/시간 관련 컬럼명 패턴 체크 (regdate, created_at, updated_at 등) + if ( + typeof value === "string" && + (key.toLowerCase().includes("date") || + key.toLowerCase().includes("time") || + key.toLowerCase().includes("created") || + key.toLowerCase().includes("updated") || + key.toLowerCase().includes("reg")) + ) { + // YYYY-MM-DD HH:mm:ss 형태의 문자열을 Date 객체로 변환 + if (value.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) { + console.log(`📅 날짜 변환: ${key} = "${value}" -> Date 객체`); + dataToInsert[key] = new Date(value); + } + // YYYY-MM-DD 형태의 문자열을 Date 객체로 변환 + else if (value.match(/^\d{4}-\d{2}-\d{2}$/)) { + console.log(`📅 날짜 변환: ${key} = "${value}" -> Date 객체`); + dataToInsert[key] = new Date(value + "T00:00:00"); + } + } + }); + // 존재하지 않는 컬럼 제거 Object.keys(dataToInsert).forEach((key) => { if (!tableColumns.includes(key)) { diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index cd5d58cb..de841376 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; @@ -12,6 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u import { CalendarIcon } from "lucide-react"; import { format } from "date-fns"; import { ko } from "date-fns/locale"; +import { useAuth } from "@/hooks/useAuth"; import { ComponentData, WidgetComponent, @@ -53,6 +54,7 @@ export const InteractiveScreenViewer: React.FC = ( hideLabel = false, screenInfo, }) => { + const { userName } = useAuth(); // 현재 로그인한 사용자명 가져오기 const [localFormData, setLocalFormData] = useState>({}); const [dateValues, setDateValues] = useState>({}); @@ -67,6 +69,32 @@ export const InteractiveScreenViewer: React.FC = ( 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>({}); + + // 자동값 생성 함수 + const generateAutoValue = useCallback((autoValueType: string): string => { + 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()}`; + default: + return ""; + } + }, [userName]); // userName 의존성 추가 // 팝업 화면 레이아웃 로드 React.useEffect(() => { @@ -74,29 +102,36 @@ export const InteractiveScreenViewer: React.FC = ( const loadPopupLayout = async () => { try { setPopupLoading(true); - console.log("🔍 팝업 화면 로드 시작:", { - screenId: popupScreen.screenId, - title: popupScreen.title, - size: popupScreen.size - }); + console.log("🔍 팝업 화면 로드 시작:", popupScreen); - const layout = await screenApi.getLayout(popupScreen.screenId); - console.log("📊 팝업 화면 레이아웃 로드 완료:", { + // 화면 레이아웃과 화면 정보를 병렬로 가져오기 + const [layout, screen] = await Promise.all([ + screenApi.getLayout(popupScreen.screenId), + screenApi.getScreen(popupScreen.screenId) + ]); + + console.log("📊 팝업 화면 로드 완료:", { componentsCount: layout.components?.length || 0, - gridSettings: layout.gridSettings, - screenResolution: layout.screenResolution, - components: layout.components?.map(c => ({ - id: c.id, - type: c.type, - title: (c as any).title - })) + screenInfo: { + screenId: screen.screenId, + tableName: screen.tableName + }, + popupFormData: {} }); setPopupLayout(layout.components || []); setPopupScreenResolution(layout.screenResolution || null); + setPopupScreenInfo({ + id: popupScreen.screenId, + tableName: screen.tableName + }); + + // 팝업 formData 초기화 + setPopupFormData({}); } catch (error) { - console.error("❌ 팝업 화면 레이아웃 로드 실패:", error); + console.error("❌ 팝업 화면 로드 실패:", error); setPopupLayout([]); + setPopupScreenInfo(null); } finally { setPopupLoading(false); } @@ -106,23 +141,86 @@ export const InteractiveScreenViewer: React.FC = ( } }, [popupScreen]); - // 실제 사용할 폼 데이터 (외부에서 제공된 경우 우선 사용) - const formData = externalFormData || localFormData; + // 실제 사용할 폼 데이터 (외부와 로컬 데이터 병합) + const formData = { ...localFormData, ...externalFormData }; + console.log("🔄 formData 구성:", { + external: externalFormData, + local: localFormData, + merged: formData, + hasExternalCallback: !!onFormDataChange + }); // 폼 데이터 업데이트 const updateFormData = (fieldName: string, value: any) => { + console.log(`🔄 updateFormData: ${fieldName} = "${value}" (외부콜백: ${!!onFormDataChange})`); + + // 항상 로컬 상태도 업데이트 + setLocalFormData((prev) => ({ + ...prev, + [fieldName]: value, + })); + console.log(`💾 로컬 상태 업데이트: ${fieldName} = "${value}"`); + + // 외부 콜백이 있는 경우에도 전달 if (onFormDataChange) { - // 외부 콜백이 있는 경우 사용 - onFormDataChange(fieldName, value); - } else { - // 로컬 상태 업데이트 - setLocalFormData((prev) => ({ - ...prev, - [fieldName]: value, - })); + // 개별 필드를 객체로 변환해서 전달 + const dataToSend = { [fieldName]: value }; + onFormDataChange(dataToSend); + console.log(`📤 외부 콜백으로 전달: ${fieldName} = "${value}" (객체: ${JSON.stringify(dataToSend)})`); } }; + // 자동입력 필드들의 값을 formData에 초기 설정 + React.useEffect(() => { + console.log("🚀 자동입력 초기화 useEffect 실행 - allComponents 개수:", allComponents.length); + const initAutoInputFields = () => { + console.log("🔧 initAutoInputFields 실행 시작"); + allComponents.forEach(comp => { + if (comp.type === 'widget') { + const widget = comp as WidgetComponent; + const fieldName = widget.columnName || widget.id; + + // 텍스트 타입 위젯의 자동입력 처리 + 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 === '', + isAutoInput, + autoValueType: config.autoValueType + }); + + if (currentValue === undefined || currentValue === '') { + const autoValue = config.autoValueType === "custom" + ? config.customValue || "" + : generateAutoValue(config.autoValueType); + + console.log("🔄 자동입력 필드 초기화:", { + fieldName, + autoValueType: config.autoValueType, + autoValue + }); + + updateFormData(fieldName, autoValue); + } else { + console.log(`⏭️ 자동입력 건너뜀 (값 있음): ${fieldName} = "${currentValue}"`); + } + } + } + } + }); + }; + + // 초기 로드 시 자동입력 필드들 설정 + initAutoInputFields(); + }, [allComponents, generateAutoValue]); // formData는 의존성에서 제외 (무한 루프 방지) + // 날짜 값 업데이트 const updateDateValue = (fieldName: string, date: Date | undefined) => { setDateValues((prev) => ({ @@ -177,6 +275,17 @@ export const InteractiveScreenViewer: React.FC = ( const widget = comp as WidgetComponent; const config = widget.webTypeConfig as TextTypeConfig | undefined; + // 자동입력 관련 처리 + const isAutoInput = config?.autoInput || false; + const autoValue = isAutoInput && config?.autoValueType + ? config.autoValueType === "custom" + ? config.customValue || "" + : generateAutoValue(config.autoValueType) + : ""; + + // 기본값 또는 자동값 설정 + const displayValue = isAutoInput ? autoValue : currentValue || config?.defaultValue || ""; + console.log("📝 InteractiveScreenViewer - Text 위젯:", { componentId: widget.id, widgetType: widget.widgetType, @@ -187,6 +296,11 @@ export const InteractiveScreenViewer: React.FC = ( maxLength: config?.maxLength, pattern: config?.pattern, placeholder: config?.placeholder, + defaultValue: config?.defaultValue, + autoInput: isAutoInput, + autoValueType: config?.autoValueType, + autoValue, + displayValue, }, }); @@ -215,6 +329,7 @@ export const InteractiveScreenViewer: React.FC = ( // 입력 검증 함수 const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; + console.log(`📝 입력 변경: ${fieldName} = "${value}"`); // 형식별 실시간 검증 if (config?.format && config.format !== "none") { @@ -222,6 +337,7 @@ export const InteractiveScreenViewer: React.FC = ( if (pattern) { const regex = new RegExp(`^${pattern}$`); if (value && !regex.test(value)) { + console.log(`❌ 형식 검증 실패: ${fieldName} = "${value}"`); return; // 유효하지 않은 입력 차단 } } @@ -229,9 +345,11 @@ export const InteractiveScreenViewer: React.FC = ( // 길이 제한 검증 if (config?.maxLength && value.length > config.maxLength) { + console.log(`❌ 길이 제한 초과: ${fieldName} = "${value}" (최대: ${config.maxLength})`); return; // 최대 길이 초과 차단 } + console.log(`✅ updateFormData 호출: ${fieldName} = "${value}"`); updateFormData(fieldName, value); }; @@ -241,15 +359,16 @@ export const InteractiveScreenViewer: React.FC = ( return applyStyles( = ( case "save": await handleSaveAction(); break; - case "cancel": - handleCancelAction(); - break; case "delete": await handleDeleteAction(); break; @@ -794,8 +910,23 @@ export const InteractiveScreenViewer: React.FC = ( // 저장 액션 const handleSaveAction = async () => { - if (!formData || Object.keys(formData).length === 0) { - alert("저장할 데이터가 없습니다."); + // 저장 시점에서 최신 formData 구성 + const currentFormData = { ...localFormData, ...externalFormData }; + console.log("💾 저장 시작 - currentFormData:", currentFormData); + console.log("💾 저장 시점 formData 상세:", { + local: localFormData, + external: externalFormData, + merged: currentFormData + }); + console.log("💾 currentFormData 키-값 상세:"); + Object.entries(currentFormData).forEach(([key, value]) => { + console.log(` ${key}: "${value}" (타입: ${typeof value})`); + }); + + // formData 유효성 체크를 완화 (빈 객체라도 위젯이 있으면 저장 진행) + const hasWidgets = allComponents.some(comp => comp.type === 'widget'); + if (!hasWidgets) { + alert("저장할 입력 컴포넌트가 없습니다."); return; } @@ -803,7 +934,7 @@ export const InteractiveScreenViewer: React.FC = ( const requiredFields = allComponents.filter(c => c.required && (c.columnName || c.id)); const missingFields = requiredFields.filter(field => { const fieldName = field.columnName || field.id; - const value = formData[fieldName]; + const value = currentFormData[fieldName]; return !value || value.toString().trim() === ""; }); @@ -822,27 +953,93 @@ export const InteractiveScreenViewer: React.FC = ( // 컬럼명 기반으로 데이터 매핑 const mappedData: Record = {}; - // 컴포넌트에서 컬럼명이 있는 것들만 매핑 + // 입력 가능한 컴포넌트에서 데이터 수집 allComponents.forEach(comp => { - if (comp.columnName) { - const fieldName = comp.columnName; - const componentId = comp.id; + // 위젯 컴포넌트이고 입력 가능한 타입인 경우 + if (comp.type === 'widget') { + const widget = comp as WidgetComponent; + const fieldName = widget.columnName || widget.id; + let value = currentFormData[fieldName]; - // formData에서 해당 값 찾기 (컬럼명 우선, 없으면 컴포넌트 ID) - const value = formData[fieldName] || formData[componentId]; + console.log(`🔍 컴포넌트 처리: ${fieldName}`, { + widgetType: widget.widgetType, + formDataValue: value, + hasWebTypeConfig: !!widget.webTypeConfig, + config: widget.webTypeConfig + }); - if (value !== undefined && value !== "") { - mappedData[fieldName] = value; + // 자동입력 필드인 경우에만 값이 없을 때 생성 + 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 + }); + + if (isAutoInput && config?.autoValueType && (!value || value === '')) { + // 자동입력이고 값이 없을 때만 생성 + value = config.autoValueType === "custom" + ? config.customValue || "" + : generateAutoValue(config.autoValueType); + + console.log("💾 자동입력 값 저장 (값이 없어서 생성):", { + fieldName, + autoValueType: config.autoValueType, + generatedValue: value + }); + } else if (isAutoInput && value) { + console.log("💾 자동입력 필드지만 기존 값 유지:", { + fieldName, + existingValue: value + }); + } else if (!isAutoInput) { + console.log(`📝 일반 입력 필드: ${fieldName} = "${value}"`); + } + } + + // 값이 있는 경우만 매핑 (빈 문자열도 포함하되, undefined는 제외) + if (value !== undefined && value !== null && value !== "undefined") { + // columnName이 있으면 columnName을 키로, 없으면 컴포넌트 ID를 키로 사용 + const saveKey = widget.columnName || `comp_${widget.id}`; + mappedData[saveKey] = value; + } else if (widget.columnName) { + // 값이 없지만 columnName이 있는 경우, 빈 문자열로 저장 + console.log(`⚠️ ${widget.columnName} 필드에 값이 없어 빈 문자열로 저장`); + mappedData[widget.columnName] = ""; } } }); console.log("💾 저장할 데이터 매핑:", { - 원본데이터: formData, + 원본데이터: currentFormData, 매핑된데이터: mappedData, 화면정보: screenInfo, + 전체컴포넌트수: allComponents.length, + 위젯컴포넌트수: allComponents.filter(c => c.type === 'widget').length, }); + // 각 컴포넌트의 상세 정보 로그 + console.log("🔍 컴포넌트별 데이터 수집 상세:"); + 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 !== ''; + 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 || @@ -864,9 +1061,11 @@ export const InteractiveScreenViewer: React.FC = ( // 저장 후 데이터 초기화 (선택사항) if (onFormDataChange) { + const resetData: Record = {}; Object.keys(formData).forEach(key => { - onFormDataChange(key, ""); + resetData[key] = ""; }); + onFormDataChange(resetData); } } else { throw new Error(result.message || "저장에 실패했습니다."); @@ -877,19 +1076,6 @@ export const InteractiveScreenViewer: React.FC = ( } }; - // 취소 액션 - const handleCancelAction = () => { - if (confirm("변경사항을 취소하시겠습니까?")) { - // 폼 초기화 또는 이전 페이지로 이동 - if (onFormDataChange) { - // 모든 폼 데이터 초기화 - Object.keys(formData).forEach(key => { - onFormDataChange(key, ""); - }); - } - console.log("❌ 작업이 취소되었습니다."); - } - }; // 삭제 액션 const handleDeleteAction = async () => { @@ -928,9 +1114,11 @@ export const InteractiveScreenViewer: React.FC = ( // 삭제 후 폼 초기화 if (onFormDataChange) { + const resetData: Record = {}; Object.keys(formData).forEach(key => { - onFormDataChange(key, ""); + resetData[key] = ""; }); + onFormDataChange(resetData); } } else { throw new Error(result.message || "삭제에 실패했습니다."); @@ -971,9 +1159,11 @@ export const InteractiveScreenViewer: React.FC = ( const handleResetAction = () => { if (confirm("모든 입력을 초기화하시겠습니까?")) { if (onFormDataChange) { + const resetData: Record = {}; Object.keys(formData).forEach(key => { - onFormDataChange(key, ""); + resetData[key] = ""; }); + onFormDataChange(resetData); } console.log("🔄 폼 초기화 완료"); alert("입력이 초기화되었습니다."); @@ -989,42 +1179,92 @@ export const InteractiveScreenViewer: React.FC = ( // 닫기 액션 const handleCloseAction = () => { - console.log("❌ 창 닫기"); - // 창 닫기 또는 모달 닫기 - if (window.opener) { + 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'); + if (modalCloseButton) { + (modalCloseButton as HTMLElement).click(); + } else { + // ESC 키 이벤트 발생시키기 + const escEvent = new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, which: 27 }); + document.dispatchEvent(escEvent); + } + } else if (isInPopup) { + // 팝업 창인 경우 + console.log("🔄 팝업 창 닫기"); window.close(); } else { - history.back(); + // 일반 페이지인 경우 - 이전 페이지로 이동하지 않고 아무것도 하지 않음 + console.log("🔄 일반 페이지에서 닫기 - 아무 동작 하지 않음"); + alert("닫기 버튼이 클릭되었습니다."); } }; // 팝업 액션 const handlePopupAction = () => { + console.log("🎯 팝업 액션 실행:", { popupScreenId: config?.popupScreenId }); + if (config?.popupScreenId) { - // 화면 팝업 열기 + // 화면 모달 열기 setPopupScreen({ screenId: config.popupScreenId, title: config.popupTitle || "상세 정보", - size: config.popupSize || "md", + size: "lg", }); } else if (config?.popupTitle && config?.popupContent) { - // 텍스트 팝업 표시 + // 텍스트 모달 표시 alert(`${config.popupTitle}\n\n${config.popupContent}`); } else { - alert("팝업을 표시합니다."); + alert("모달을 표시합니다."); } }; // 네비게이션 액션 const handleNavigateAction = () => { - if (config?.navigateUrl) { + 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 + }); + + if (config.navigateTarget === "_blank") { + window.open(screenPath, "_blank"); + } else { + window.location.href = screenPath; + } + } else if (navigateType === "url" && config?.navigateUrl) { + // URL로 이동 + console.log("🔗 URL로 이동:", { + url: config.navigateUrl, + target: config.navigateTarget || "_self" + }); + if (config.navigateTarget === "_blank") { window.open(config.navigateUrl, "_blank"); } else { window.location.href = config.navigateUrl; } } else { - console.log("🔗 네비게이션 URL이 설정되지 않았습니다."); + console.log("🔗 네비게이션 정보가 설정되지 않았습니다:", { + navigateType, + hasUrl: !!config?.navigateUrl, + hasScreenId: !!config?.navigateScreenId + }); } }; @@ -1050,7 +1290,7 @@ export const InteractiveScreenViewer: React.FC = (