/** * 개선된 대화형 화면 뷰어 * 실시간 검증과 개선된 저장 시스템이 통합된 컴포넌트 */ "use client"; import React, { useState, useCallback, useEffect } 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 { CalendarIcon, AlertCircle, CheckCircle, Clock } from "lucide-react"; import { format } from "date-fns"; import { ko } from "date-fns/locale"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { ComponentData, WidgetComponent, DataTableComponent, ScreenDefinition, ColumnInfo } from "@/types/screen"; import { InteractiveDataTable } from "./InteractiveDataTable"; import { DynamicWebTypeRenderer } from "@/lib/registry/DynamicWebTypeRenderer"; import { useFormValidation, UseFormValidationOptions } from "@/hooks/useFormValidation"; import { FormValidationIndicator, FieldValidationIndicator } from "@/components/common/FormValidationIndicator"; import { enhancedFormService } from "@/lib/services/enhancedFormService"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; interface EnhancedInteractiveScreenViewerProps { component: ComponentData; allComponents: ComponentData[]; screenInfo: ScreenDefinition; tableColumns: ColumnInfo[]; formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; hideLabel?: boolean; validationOptions?: UseFormValidationOptions; showValidationPanel?: boolean; compactValidation?: boolean; } export const EnhancedInteractiveScreenViewer: React.FC = ({ component, allComponents, screenInfo, tableColumns, formData: externalFormData = {}, onFormDataChange, hideLabel = false, validationOptions = {}, showValidationPanel = true, compactValidation = false, }) => { const { userName, user } = useAuth(); const [localFormData, setLocalFormData] = useState>({}); const [dateValues, setDateValues] = useState>({}); // 최종 폼 데이터 (외부 + 로컬) const finalFormData = { ...localFormData, ...externalFormData }; // 폼 검증 훅 사용 const { validationState, saveState, validateForm, validateField, saveForm, clearValidation, getFieldError, getFieldWarning, hasFieldError, isFieldValid, canSave, } = useFormValidation(finalFormData, allComponents, tableColumns, screenInfo, { enableRealTimeValidation: true, validationDelay: 300, enableAutoSave: false, showToastMessages: true, validateOnMount: false, ...validationOptions, }); // 자동값 생성 함수 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", " "); case "current_date": return now.toISOString().slice(0, 10); case "current_time": return now.toTimeString().slice(0, 8); 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); } } return ""; default: return ""; } }, [userName], ); // 폼 데이터 변경 핸들러 (검증 포함) const handleFormDataChange = useCallback( async (fieldName: string, value: any) => { // 로컬 상태 업데이트 setLocalFormData((prev) => ({ ...prev, [fieldName]: value, })); // 외부 핸들러 호출 onFormDataChange?.(fieldName, value); // 개별 필드 검증 (debounced) setTimeout(() => { validateField(fieldName, value); }, 100); }, [onFormDataChange, validateField], ); // 자동값 설정 useEffect(() => { const widgetComponents = allComponents.filter((c) => c.type === "widget") as WidgetComponent[]; const loadAutoValues = async () => { const autoValueUpdates: Record = {}; for (const widget of widgetComponents) { const fieldName = widget.columnName || widget.id; const currentValue = finalFormData[fieldName]; // 자동값이 설정되어 있고 현재 값이 없는 경우 if (widget.inputType === "auto" && widget.autoValueType && !currentValue) { const autoValue = await generateAutoValue( widget.autoValueType, (widget as any).numberingRuleId // 채번 규칙 ID ); if (autoValue) { autoValueUpdates[fieldName] = autoValue; } } } if (Object.keys(autoValueUpdates).length > 0) { setLocalFormData((prev) => ({ ...prev, ...autoValueUpdates })); } }; loadAutoValues(); }, [allComponents, finalFormData, generateAutoValue]); // 향상된 저장 핸들러 const handleEnhancedSave = useCallback(async () => { const success = await saveForm(); if (success) { toast.success("데이터가 성공적으로 저장되었습니다.", { description: `성능: ${saveState.result?.performance?.totalTime.toFixed(2)}ms`, }); } }, [saveForm, saveState.result]); // 대화형 위젯 렌더링 const renderInteractiveWidget = (comp: ComponentData) => { // 데이터 테이블 컴포넌트 처리 if (comp.type === "datatable") { const dataTable = comp as DataTableComponent; return (
); } // 위젯 컴포넌트가 아닌 경우 일반 컨테이너 렌더링 if (comp.type !== "widget") { return renderContainer(comp); } const widget = comp as WidgetComponent; const fieldName = widget.columnName || widget.id; const currentValue = finalFormData[fieldName] || ""; // 필드 검증 상태 const fieldError = getFieldError(fieldName); const fieldWarning = getFieldWarning(fieldName); const hasError = hasFieldError(fieldName); const isValid = isFieldValid(fieldName); // 스타일 적용 const applyStyles = (element: React.ReactElement) => { const style = widget.style || {}; const inlineStyle: React.CSSProperties = { width: style.width || "100%", height: style.height || "auto", fontSize: style.fontSize, color: style.color, backgroundColor: style.backgroundColor, border: style.border, borderRadius: style.borderRadius, padding: style.padding, margin: style.margin, ...style, }; // 검증 상태에 따른 스타일 조정 if (hasError) { inlineStyle.borderColor = "#ef4444"; inlineStyle.boxShadow = "0 0 0 1px #ef4444"; } else if (isValid && finalFormData[fieldName]) { inlineStyle.borderColor = "#22c55e"; } return React.cloneElement(element, { style: inlineStyle, className: `${element.props.className || ""} ${hasError ? "border-destructive" : ""} ${isValid && finalFormData[fieldName] ? "border-green-500" : ""}`.trim(), }); }; // 라벨 렌더링 const renderLabel = () => { if (hideLabel) return null; const labelStyle = widget.style || {}; const labelElement = ( ); return labelElement; }; // 필드 검증 표시기 const renderFieldValidation = () => { if (!fieldError && !fieldWarning) return null; return ( ); }; // 웹타입별 렌더링 const renderByWebType = () => { const widgetType = widget.widgetType; const placeholder = widget.placeholder || `${widget.label}을(를) 입력하세요`; const required = widget.required; const readonly = widget.readonly; // DynamicWebTypeRenderer 사용 try { const dynamicElement = ( handleFormDataChange(fieldName, value), placeholder, disabled: readonly, required, className: "h-full w-full", }} /> ); return applyStyles(dynamicElement); } catch (error) { // console.warn(`DynamicWebTypeRenderer 오류 (${widgetType}):`, error); // 폴백: 기본 input const fallbackElement = ( handleFormDataChange(fieldName, e.target.value)} placeholder={placeholder} disabled={readonly} required={required} className="h-full w-full" /> ); return applyStyles(fallbackElement); } }; return (
{renderLabel()} {renderByWebType()} {renderFieldValidation()}
); }; // 컨테이너 렌더링 const renderContainer = (comp: ComponentData) => { const children = allComponents.filter((c) => c.parentId === comp.id); return (
{comp.type === "container" && (comp as any).title && (

{(comp as any).title}

)} {children.map((child) => renderInteractiveWidget(child))}
); }; // 버튼 렌더링 const renderButton = (comp: ComponentData) => { const buttonConfig = (comp as any).webTypeConfig; const actionType = buttonConfig?.actionType || "save"; const handleButtonClick = async () => { switch (actionType) { case "save": await handleEnhancedSave(); break; case "reset": setLocalFormData({}); clearValidation(); toast.info("폼이 초기화되었습니다."); break; case "validate": await validateForm(); break; default: toast.info(`${actionType} 액션이 실행되었습니다.`); } }; return ( ); }; // 메인 렌더링 const renderComponent = () => { if (component.type === "widget") { const widget = component as WidgetComponent; if (widget.widgetType === "button") { return renderButton(component); } return renderInteractiveWidget(component); } return renderContainer(component); }; return (
{/* 검증 상태 패널 */} {showValidationPanel && ( )} {/* 메인 컴포넌트 */}
{renderComponent()}
{/* 개발 정보 (개발 환경에서만 표시) */} {process.env.NODE_ENV === "development" && ( <> 개발 정보
테이블 {screenInfo.tableName}
필드 {Object.keys(finalFormData).length}개
검증 {validationState.validationCount}회
{saveState.result?.performance && (
성능 {saveState.result.performance.totalTime.toFixed(2)}ms
)}
)}
); };