"use client"; import React, { useState, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Calendar } from "@/components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { CalendarIcon, File, Upload, X } from "lucide-react"; import { format } from "date-fns"; import { ko } from "date-fns/locale"; import { useAuth } from "@/hooks/useAuth"; import { uploadFilesAndCreateData } from "@/lib/api/file"; import { toast } from "sonner"; import { ComponentData, WidgetComponent, DataTableComponent, FileComponent, TextTypeConfig, NumberTypeConfig, DateTypeConfig, SelectTypeConfig, RadioTypeConfig, CheckboxTypeConfig, TextareaTypeConfig, FileTypeConfig, CodeTypeConfig, EntityTypeConfig, ButtonTypeConfig, } from "@/types"; import { InteractiveDataTable } from "./InteractiveDataTable"; 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"; import { enhancedFormService } from "@/lib/services/enhancedFormService"; import { FormValidationIndicator } from "@/components/common/FormValidationIndicator"; import { useFormValidation } from "@/hooks/useFormValidation"; import { UnifiedColumnInfo as ColumnInfo } from "@/types"; import { isFileComponent } from "@/lib/utils/componentTypeUtils"; import { buildGridClasses } from "@/lib/constants/columnSpans"; import { cn } from "@/lib/utils"; import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; interface InteractiveScreenViewerProps { component: ComponentData; allComponents: ComponentData[]; formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; hideLabel?: boolean; screenInfo?: { id: number; tableName?: string; }; // 새로운 검증 관련 옵션들 enableEnhancedValidation?: boolean; tableColumns?: ColumnInfo[]; showValidationPanel?: boolean; validationOptions?: { enableRealTimeValidation?: boolean; validationDelay?: number; enableAutoSave?: boolean; showToastMessages?: boolean; }; } export const InteractiveScreenViewer: React.FC = ({ component, allComponents, formData: externalFormData, onFormDataChange, hideLabel = false, screenInfo, enableEnhancedValidation = false, tableColumns = [], showValidationPanel = false, validationOptions = {}, }) => { // component가 없으면 빈 div 반환 if (!component) { console.warn("⚠️ InteractiveScreenViewer: component가 undefined입니다."); return
; } const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { userName, user } = useAuth(); // 현재 로그인한 사용자명과 사용자 정보 가져오기 const [localFormData, setLocalFormData] = useState>({}); const [dateValues, setDateValues] = useState>({}); // 팝업 화면 상태 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>({}); // 통합된 폼 데이터 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 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(() => { if (popupScreen) { const loadPopupLayout = async () => { try { setPopupLoading(true); // console.log("🔍 팝업 화면 로드 시작:", popupScreen); // 화면 레이아웃과 화면 정보를 병렬로 가져오기 const [layout, screen] = await Promise.all([ screenApi.getLayout(popupScreen.screenId), screenApi.getScreen(popupScreen.screenId) ]); console.log("📊 팝업 화면 로드 완료:", { componentsCount: layout.components?.length || 0, 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); setPopupLayout([]); setPopupScreenInfo(null); } finally { setPopupLoading(false); } }; loadPopupLayout(); } }, [popupScreen]); // 실제 사용할 폼 데이터 (외부와 로컬 데이터 병합) const formData = { ...localFormData, ...externalFormData }; console.log("🔄 formData 구성:", { external: externalFormData, local: localFormData, merged: formData, hasExternalCallback: !!onFormDataChange }); // 폼 데이터 업데이트 const updateFormData = (fieldName: string, value: any) => { // 프리뷰 모드에서는 데이터 업데이트 하지 않음 if (isPreviewMode) { return; } // console.log(`🔄 updateFormData: ${fieldName} = "${value}" (외부콜백: ${!!onFormDataChange})`); // 항상 로컬 상태도 업데이트 setLocalFormData((prev) => ({ ...prev, [fieldName]: value, })); // console.log(`💾 로컬 상태 업데이트: ${fieldName} = "${value}"`); // 외부 콜백이 있는 경우에도 전달 (개별 필드 단위로) if (onFormDataChange) { onFormDataChange(fieldName, value); // console.log(`📤 외부 콜백으로 전달: ${fieldName} = "${value}"`); } }; // 자동입력 필드들의 값을 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) => ({ ...prev, [fieldName]: date, })); updateFormData(fieldName, date ? format(date, "yyyy-MM-dd") : ""); }; // 실제 사용 가능한 위젯 렌더링 const renderInteractiveWidget = (comp: ComponentData) => { // 데이터 테이블 컴포넌트 처리 if (comp.type === "datatable") { return ( { // 테이블 내부에서 loadData 호출하므로 여기서는 빈 함수 console.log("🔄 InteractiveDataTable 새로고침 트리거됨"); }} /> ); } // 플로우 위젯 컴포넌트 처리 if (comp.type === "flow" || (comp.type === "component" && (comp as any).componentConfig?.type === "flow-widget")) { 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, flowConfig, flowConfigFlowId: flowConfig.flowId, finalFlowId: flowConfig.flowId, }); const flowComponent = { ...comp, type: "flow" as const, flowId: flowConfig.flowId, flowName: flowConfig.flowName, showStepCount: flowConfig.showStepCount !== false, allowDataMove: flowConfig.allowDataMove || false, displayMode: flowConfig.displayMode || "horizontal", }; console.log("🔍 InteractiveScreenViewer 최종 flowComponent:", flowComponent); return (
); } const { widgetType, label, placeholder, required, readonly, columnName } = comp; const fieldName = columnName || comp.id; const currentValue = formData[fieldName] || ""; // 스타일 적용 const applyStyles = (element: React.ReactElement) => { if (!comp.style) return element; return React.cloneElement(element, { style: { ...element.props.style, // 기존 스타일 유지 ...comp.style, // 크기는 부모 컨테이너에서 처리하므로 제거 (하지만 다른 스타일은 유지) width: "100%", height: "100%", minHeight: "100%", maxHeight: "100%", boxSizing: "border-box", }, }); }; switch (widgetType) { case "text": case "email": case "tel": { 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, config, appliedSettings: { format: config?.format, minLength: config?.minLength, maxLength: config?.maxLength, pattern: config?.pattern, placeholder: config?.placeholder, defaultValue: config?.defaultValue, autoInput: isAutoInput, autoValueType: config?.autoValueType, autoValue, displayValue, }, }); // 형식별 패턴 생성 const getPatternByFormat = (format: string) => { switch (format) { case "korean": return "[가-힣\\s]*"; case "english": return "[a-zA-Z\\s]*"; case "alphanumeric": return "[a-zA-Z0-9]*"; case "numeric": return "[0-9]*"; case "email": return "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"; case "phone": return "\\d{3}-\\d{4}-\\d{4}"; case "url": return "https?://[\\w\\-]+(\\.[\\w\\-]+)+([\\w\\-\\.,@?^=%&:/~\\+#]*[\\w\\-\\@?^=%&/~\\+#])?"; default: return config?.pattern || undefined; } }; // 입력 검증 함수 const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; // console.log(`📝 입력 변경: ${fieldName} = "${value}"`); // 형식별 실시간 검증 if (config?.format && config.format !== "none") { const pattern = getPatternByFormat(config.format); if (pattern) { const regex = new RegExp(`^${pattern}$`); if (value && !regex.test(value)) { // console.log(`❌ 형식 검증 실패: ${fieldName} = "${value}"`); return; // 유효하지 않은 입력 차단 } } } // 길이 제한 검증 if (config?.maxLength && value.length > config.maxLength) { // console.log(`❌ 길이 제한 초과: ${fieldName} = "${value}" (최대: ${config.maxLength})`); return; // 최대 길이 초과 차단 } // console.log(`✅ updateFormData 호출: ${fieldName} = "${value}"`); updateFormData(fieldName, value); }; const finalPlaceholder = config?.placeholder || placeholder || "입력하세요..."; const inputType = widgetType === "email" ? "email" : widgetType === "tel" ? "tel" : "text"; return applyStyles( , ); } case "number": case "decimal": { const widget = comp as WidgetComponent; const config = widget.webTypeConfig as NumberTypeConfig | undefined; console.log("🔢 InteractiveScreenViewer - Number 위젯:", { componentId: widget.id, widgetType: widget.widgetType, config, appliedSettings: { format: config?.format, min: config?.min, max: config?.max, step: config?.step, decimalPlaces: config?.decimalPlaces, thousandSeparator: config?.thousandSeparator, prefix: config?.prefix, suffix: config?.suffix, }, }); const step = config?.step || (widgetType === "decimal" ? 0.01 : 1); const finalPlaceholder = config?.placeholder || placeholder || "숫자를 입력하세요..."; return applyStyles( updateFormData(fieldName, e.target.valueAsNumber || 0)} disabled={readonly} required={required} min={config?.min} max={config?.max} step={step} className="w-full" style={{ height: "100%" }} />, ); } case "textarea": case "text_area": { const widget = comp as WidgetComponent; const config = widget.webTypeConfig as TextareaTypeConfig | undefined; console.log("📄 InteractiveScreenViewer - Textarea 위젯:", { componentId: widget.id, widgetType: widget.widgetType, config, appliedSettings: { rows: config?.rows, maxLength: config?.maxLength, minLength: config?.minLength, placeholder: config?.placeholder, defaultValue: config?.defaultValue, resizable: config?.resizable, wordWrap: config?.wordWrap, }, }); const finalPlaceholder = config?.placeholder || placeholder || "내용을 입력하세요..."; const rows = config?.rows || 3; return applyStyles(