Files
sam-react-prod/src/components/molecules/GenericCRUDDialog.tsx

172 lines
4.9 KiB
TypeScript
Raw Normal View History

'use client';
import { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Loader2 } from 'lucide-react';
/**
*
*/
export interface CRUDFieldDefinition {
key: string;
label: string;
type: 'text' | 'select';
placeholder?: string;
options?: { value: string; label: string }[];
defaultValue?: string;
}
export interface GenericCRUDDialogProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
mode: 'add' | 'edit';
entityName: string;
fields: CRUDFieldDefinition[];
initialData?: Record<string, string>;
onSubmit: (data: Record<string, string>) => void;
isLoading?: boolean;
addLabel?: string;
editLabel?: string;
}
/**
* CRUD
*
* + Select .
* RankDialog, TitleDialog .
*/
export function GenericCRUDDialog({
isOpen,
onOpenChange,
mode,
entityName,
fields,
initialData,
onSubmit,
isLoading = false,
addLabel = '등록',
editLabel = '수정',
}: GenericCRUDDialogProps) {
const [formData, setFormData] = useState<Record<string, string>>({});
useEffect(() => {
if (isOpen) {
if (mode === 'edit' && initialData) {
setFormData({ ...initialData });
} else {
const defaults: Record<string, string> = {};
fields.forEach((f) => {
defaults[f.key] = f.defaultValue ?? '';
});
setFormData(defaults);
}
}
}, [isOpen, mode, initialData, fields]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const firstTextField = fields.find((f) => f.type === 'text');
if (firstTextField && !formData[firstTextField.key]?.trim()) return;
const trimmed: Record<string, string> = {};
Object.entries(formData).forEach(([k, v]) => {
trimmed[k] = v.trim();
});
onSubmit(trimmed);
const defaults: Record<string, string> = {};
fields.forEach((f) => {
defaults[f.key] = f.defaultValue ?? '';
});
setFormData(defaults);
};
const title = mode === 'add' ? `${entityName} 추가` : `${entityName} 수정`;
const submitText = mode === 'add' ? addLabel : editLabel;
const firstTextField = fields.find((f) => f.type === 'text');
const isSubmitDisabled =
isLoading || (firstTextField ? !formData[firstTextField.key]?.trim() : false);
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="space-y-4 py-4">
{fields.map((field, idx) => (
<div key={field.key} className="space-y-2">
<Label htmlFor={`crud-${field.key}`}>{field.label}</Label>
{field.type === 'text' ? (
<Input
id={`crud-${field.key}`}
value={formData[field.key] ?? ''}
onChange={(e) =>
setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))
}
placeholder={field.placeholder}
autoFocus={idx === 0}
disabled={isLoading}
/>
) : (
<Select
value={formData[field.key] ?? ''}
onValueChange={(value) =>
setFormData((prev) => ({ ...prev, [field.key]: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{field.options?.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
))}
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isLoading}
>
</Button>
<Button type="submit" disabled={isSubmitDisabled}>
{isLoading ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : null}
{submitText}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}