ERP-node/frontend/components/common/FormValidationIndicator.tsx

379 lines
12 KiB
TypeScript

/**
* 폼 검증 상태 표시 컴포넌트
* 실시간 검증 피드백과 사용자 가이드를 제공
*/
import React from "react";
import { AlertCircle, CheckCircle, Clock, AlertTriangle, Info } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Progress } from "@/components/ui/progress";
import { FormValidationState, SaveState, ValidationError, ValidationWarning } from "@/hooks/useFormValidation";
// Props 타입
export interface FormValidationIndicatorProps {
validationState: FormValidationState;
saveState: SaveState;
onValidate?: () => void;
onSave?: () => void;
canSave?: boolean;
compact?: boolean;
showDetails?: boolean;
showPerformance?: boolean;
}
/**
* 메인 검증 상태 표시 컴포넌트
*/
export const FormValidationIndicator: React.FC<FormValidationIndicatorProps> = ({
validationState,
saveState,
onValidate,
onSave,
canSave = false,
compact = false,
showDetails = true,
showPerformance = false,
}) => {
if (compact) {
return (
<CompactValidationIndicator
validationState={validationState}
saveState={saveState}
onSave={onSave}
canSave={canSave}
/>
);
}
return (
<Card className="w-full">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium"> </CardTitle>
<div className="flex items-center gap-2">
<ValidationStatusBadge status={validationState.status} />
{validationState.lastValidated && (
<span className="text-muted-foreground text-xs">
{validationState.lastValidated.toLocaleTimeString()}
</span>
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* 검증 요약 */}
<ValidationSummary validationState={validationState} />
{/* 액션 버튼들 */}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={onValidate}
disabled={validationState.status === "validating"}
className="flex items-center gap-2"
>
{validationState.status === "validating" ? (
<Clock className="h-4 w-4 animate-spin" />
) : (
<CheckCircle className="h-4 w-4" />
)}
</Button>
<Button
size="sm"
onClick={onSave}
disabled={!canSave || saveState.status === "saving"}
className="flex items-center gap-2"
>
{saveState.status === "saving" ? (
<Clock className="h-4 w-4 animate-spin" />
) : (
<CheckCircle className="h-4 w-4" />
)}
</Button>
</div>
{/* 상세 정보 */}
{showDetails && (
<>
<Separator />
<ValidationDetails validationState={validationState} />
</>
)}
{/* 성능 정보 */}
{showPerformance && saveState.result?.performance && (
<>
<Separator />
<PerformanceInfo performance={saveState.result.performance} />
</>
)}
</CardContent>
</Card>
);
};
/**
* 간단한 검증 상태 표시
*/
const CompactValidationIndicator: React.FC<{
validationState: FormValidationState;
saveState: SaveState;
onSave?: () => void;
canSave: boolean;
}> = ({ validationState, saveState, onSave, canSave }) => {
return (
<div className="bg-muted/50 flex items-center gap-3 rounded-md p-2">
<ValidationStatusBadge status={validationState.status} />
<div className="flex-1 text-sm">
{validationState.errors.length > 0 && (
<span className="text-destructive">{validationState.errors.length} </span>
)}
{validationState.warnings.length > 0 && (
<span className="ml-2 text-orange-600">{validationState.warnings.length} </span>
)}
{validationState.isValid && <span className="text-green-600"> </span>}
</div>
<Button size="sm" onClick={onSave} disabled={!canSave || saveState.status === "saving"} className="h-8">
{saveState.status === "saving" ? "저장중..." : "저장"}
</Button>
</div>
);
};
/**
* 검증 상태 배지
*/
const ValidationStatusBadge: React.FC<{ status: FormValidationState["status"] }> = ({ status }) => {
const getStatusConfig = () => {
switch (status) {
case "idle":
return {
variant: "secondary" as const,
icon: Info,
text: "대기중",
};
case "validating":
return {
variant: "secondary" as const,
icon: Clock,
text: "검증중",
animate: true,
};
case "valid":
return {
variant: "default" as const,
icon: CheckCircle,
text: "유효함",
className: "bg-green-500 hover:bg-green-600",
};
case "invalid":
return {
variant: "destructive" as const,
icon: AlertCircle,
text: "오류",
};
default:
return {
variant: "secondary" as const,
icon: Info,
text: "알 수 없음",
};
}
};
const config = getStatusConfig();
const IconComponent = config.icon;
return (
<Badge variant={config.variant} className={config.className}>
<IconComponent className={`mr-1 h-3 w-3 ${config.animate ? "animate-spin" : ""}`} />
{config.text}
</Badge>
);
};
/**
* 검증 요약 정보
*/
const ValidationSummary: React.FC<{ validationState: FormValidationState }> = ({ validationState }) => {
const totalFields = Object.keys(validationState.fieldStates).length;
const validFields = Object.values(validationState.fieldStates).filter((field) => field.status === "valid").length;
const progress = totalFields > 0 ? (validFields / totalFields) * 100 : 0;
return (
<div className="space-y-3">
{/* 진행률 */}
{totalFields > 0 && (
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span> </span>
<span>
{validFields}/{totalFields}
</span>
</div>
<Progress value={progress} className="h-2" />
</div>
)}
{/* 오류/경고 카운트 */}
<div className="flex items-center gap-4 text-sm">
{validationState.errors.length > 0 && (
<div className="text-destructive flex items-center gap-1">
<AlertCircle className="h-4 w-4" />
<span>{validationState.errors.length} </span>
</div>
)}
{validationState.warnings.length > 0 && (
<div className="flex items-center gap-1 text-orange-600">
<AlertTriangle className="h-4 w-4" />
<span>{validationState.warnings.length} </span>
</div>
)}
{validationState.isValid && validationState.errors.length === 0 && validationState.warnings.length === 0 && (
<div className="flex items-center gap-1 text-green-600">
<CheckCircle className="h-4 w-4" />
<span> </span>
</div>
)}
</div>
</div>
);
};
/**
* 검증 상세 정보
*/
const ValidationDetails: React.FC<{ validationState: FormValidationState }> = ({ validationState }) => {
if (validationState.errors.length === 0 && validationState.warnings.length === 0) {
return (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription> .</AlertDescription>
</Alert>
);
}
return (
<ScrollArea className="h-32 w-full">
<div className="space-y-2">
{/* 오류 목록 */}
{validationState.errors.map((error, index) => (
<ValidationErrorItem key={`error-${index}`} error={error} />
))}
{/* 경고 목록 */}
{validationState.warnings.map((warning, index) => (
<ValidationWarningItem key={`warning-${index}`} warning={warning} />
))}
</div>
</ScrollArea>
);
};
/**
* 개별 오류 아이템
*/
const ValidationErrorItem: React.FC<{ error: ValidationError }> = ({ error }) => {
return (
<Alert variant="destructive" className="py-2">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-sm">
<span className="font-medium">{error.field}:</span> {error.message}
{error.value !== undefined && (
<span className="mt-1 block text-xs opacity-75">: "{String(error.value)}"</span>
)}
</AlertDescription>
</Alert>
);
};
/**
* 개별 경고 아이템
*/
const ValidationWarningItem: React.FC<{ warning: ValidationWarning }> = ({ warning }) => {
return (
<Alert className="border-orange-200 bg-orange-50 py-2">
<AlertTriangle className="h-4 w-4 text-orange-600" />
<AlertDescription className="text-sm">
<span className="font-medium">{warning.field}:</span> {warning.message}
{warning.suggestion && <span className="mt-1 block text-xs text-orange-700">💡 {warning.suggestion}</span>}
</AlertDescription>
</Alert>
);
};
/**
* 성능 정보 표시
*/
const PerformanceInfo: React.FC<{
performance: { validationTime: number; saveTime: number; totalTime: number };
}> = ({ performance }) => {
return (
<div className="bg-muted/50 rounded-md p-3">
<h4 className="mb-2 text-sm font-medium"> </h4>
<div className="grid grid-cols-3 gap-4 text-xs">
<div>
<span className="text-muted-foreground"> </span>
<div className="font-mono">{performance.validationTime.toFixed(2)}ms</div>
</div>
<div>
<span className="text-muted-foreground"> </span>
<div className="font-mono">{performance.saveTime.toFixed(2)}ms</div>
</div>
<div>
<span className="text-muted-foreground"> </span>
<div className="font-mono">{performance.totalTime.toFixed(2)}ms</div>
</div>
</div>
</div>
);
};
/**
* 필드별 검증 상태 표시 컴포넌트
*/
export const FieldValidationIndicator: React.FC<{
fieldName: string;
error?: ValidationError;
warning?: ValidationWarning;
status?: "idle" | "validating" | "valid" | "invalid";
showIcon?: boolean;
className?: string;
}> = ({ fieldName, error, warning, status = "idle", showIcon = true, className }) => {
if (status === "idle" && !error && !warning) {
return null;
}
return (
<div className={`flex items-center gap-1 text-xs ${className}`}>
{showIcon && (
<>
{status === "validating" && <Clock className="text-muted-foreground h-3 w-3 animate-spin" />}
{status === "valid" && !error && <CheckCircle className="h-3 w-3 text-green-600" />}
{error && <AlertCircle className="text-destructive h-3 w-3" />}
{warning && !error && <AlertTriangle className="h-3 w-3 text-orange-600" />}
</>
)}
{error && <span className="text-destructive">{error.message}</span>}
{warning && !error && <span className="text-orange-600">{warning.message}</span>}
</div>
);
};