ERP-node/frontend/components/screen/EnhancedInteractiveScreenVi...

442 lines
14 KiB
TypeScript

/**
* 개선된 대화형 화면 뷰어
* 실시간 검증과 개선된 저장 시스템이 통합된 컴포넌트
*/
"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<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
hideLabel?: boolean;
validationOptions?: UseFormValidationOptions;
showValidationPanel?: boolean;
compactValidation?: boolean;
}
export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreenViewerProps> = ({
component,
allComponents,
screenInfo,
tableColumns,
formData: externalFormData = {},
onFormDataChange,
hideLabel = false,
validationOptions = {},
showValidationPanel = true,
compactValidation = false,
}) => {
const { userName, user } = useAuth();
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
// 최종 폼 데이터 (외부 + 로컬)
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(
(autoValueType: string): string => {
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()}`;
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 autoValueUpdates: Record<string, any> = {};
for (const widget of widgetComponents) {
const fieldName = widget.columnName || widget.id;
const currentValue = finalFormData[fieldName];
// 자동값이 설정되어 있고 현재 값이 없는 경우
if (widget.inputType === "auto" && widget.autoValueType && !currentValue) {
const autoValue = generateAutoValue(widget.autoValueType);
if (autoValue) {
autoValueUpdates[fieldName] = autoValue;
}
}
}
if (Object.keys(autoValueUpdates).length > 0) {
setLocalFormData((prev) => ({ ...prev, ...autoValueUpdates }));
}
}, [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 (
<div key={comp.id} className="w-full">
<InteractiveDataTable
component={dataTable}
formData={finalFormData}
onFormDataChange={handleFormDataChange}
/>
</div>
);
}
// 위젯 컴포넌트가 아닌 경우 일반 컨테이너 렌더링
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 = (
<label
className={`mb-1 block text-sm font-medium ${hasError ? "text-destructive" : ""}`}
style={{
fontSize: labelStyle.labelFontSize || "14px",
color: hasError ? "#ef4444" : labelStyle.labelColor || "#374151",
fontWeight: labelStyle.labelFontWeight || "500",
fontFamily: labelStyle.labelFontFamily,
textAlign: labelStyle.labelTextAlign || "left",
backgroundColor: labelStyle.labelBackgroundColor,
padding: labelStyle.labelPadding,
borderRadius: labelStyle.labelBorderRadius,
marginBottom: labelStyle.labelMarginBottom || "4px",
}}
>
{widget.label}
{widget.required && <span className="ml-1 text-orange-500">*</span>}
</label>
);
return labelElement;
};
// 필드 검증 표시기
const renderFieldValidation = () => {
if (!fieldError && !fieldWarning) return null;
return (
<FieldValidationIndicator
fieldName={fieldName}
error={fieldError}
warning={fieldWarning}
status={validationState.fieldStates[fieldName]?.status}
className="mt-1"
/>
);
};
// 웹타입별 렌더링
const renderByWebType = () => {
const widgetType = widget.widgetType;
const placeholder = widget.placeholder || `${widget.label}을(를) 입력하세요`;
const required = widget.required;
const readonly = widget.readonly;
// DynamicWebTypeRenderer 사용
try {
const dynamicElement = (
<DynamicWebTypeRenderer
webType={widgetType || "text"}
config={widget.webTypeConfig}
props={{
component: widget,
value: currentValue,
onChange: (value: any) => 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 = (
<Input
type="text"
value={currentValue}
onChange={(e) => handleFormDataChange(fieldName, e.target.value)}
placeholder={placeholder}
disabled={readonly}
required={required}
className="h-full w-full"
/>
);
return applyStyles(fallbackElement);
}
};
return (
<div key={comp.id} className="space-y-1">
{renderLabel()}
{renderByWebType()}
{renderFieldValidation()}
</div>
);
};
// 컨테이너 렌더링
const renderContainer = (comp: ComponentData) => {
const children = allComponents.filter((c) => c.parentId === comp.id);
return (
<div key={comp.id} className="space-y-4">
{comp.type === "container" && (comp as any).title && (
<h3 className="text-lg font-semibold">{(comp as any).title}</h3>
)}
{children.map((child) => renderInteractiveWidget(child))}
</div>
);
};
// 버튼 렌더링
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 (
<Button
key={comp.id}
onClick={handleButtonClick}
disabled={actionType === "save" && !canSave}
variant={buttonConfig?.variant || "default"}
className="flex items-center gap-2"
>
{saveState.status === "saving" && actionType === "save" && <Clock className="h-4 w-4 animate-spin" />}
{validationState.status === "validating" && actionType === "validate" && (
<Clock className="h-4 w-4 animate-spin" />
)}
{comp.label || "버튼"}
</Button>
);
};
// 메인 렌더링
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 (
<div className="space-y-4">
{/* 검증 상태 패널 */}
{showValidationPanel && (
<FormValidationIndicator
validationState={validationState}
saveState={saveState}
onValidate={validateForm}
onSave={handleEnhancedSave}
canSave={canSave}
compact={compactValidation}
showDetails={!compactValidation}
showPerformance={!compactValidation}
/>
)}
{/* 메인 컴포넌트 */}
<div className="space-y-4">{renderComponent()}</div>
{/* 개발 정보 (개발 환경에서만 표시) */}
{process.env.NODE_ENV === "development" && (
<>
<Separator />
<Card>
<CardHeader>
<CardTitle className="text-sm"> </CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center gap-2">
<Badge variant="outline"></Badge>
<span className="text-sm">{screenInfo.tableName}</span>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline"></Badge>
<span className="text-sm">{Object.keys(finalFormData).length}</span>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline"></Badge>
<span className="text-sm">{validationState.validationCount}</span>
</div>
{saveState.result?.performance && (
<div className="flex items-center gap-2">
<Badge variant="outline"></Badge>
<span className="text-sm">{saveState.result.performance.totalTime.toFixed(2)}ms</span>
</div>
)}
</CardContent>
</Card>
</>
)}
</div>
);
};