ERP-node/frontend/components/examples/ExampleFormDialog.tsx

422 lines
15 KiB
TypeScript
Raw Permalink Normal View History

2025-10-30 12:03:50 +09:00
"use client";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { CustomCalendar } from "@/components/ui/custom-calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { AlertCircle, CheckCircle, Calendar as CalendarIcon } from "lucide-react";
import { cn } from "@/lib/utils";
interface FormData {
name: string;
email: string;
phone: string;
category: string;
priority: string;
startDate?: Date;
endDate?: Date;
description: string;
isActive: boolean;
}
interface FormErrors {
name?: string;
email?: string;
phone?: string;
category?: string;
startDate?: string;
}
export function ExampleFormDialog() {
const [isOpen, setIsOpen] = useState(false);
const [formData, setFormData] = useState<FormData>({
name: "",
email: "",
phone: "",
category: "",
priority: "medium",
description: "",
isActive: true,
});
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
// 폼 유효성 검사
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
// 이름 검증
if (!formData.name.trim()) {
newErrors.name = "이름을 입력해주세요";
} else if (formData.name.length < 2) {
newErrors.name = "이름은 2자 이상이어야 합니다";
}
// 이메일 검증
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email.trim()) {
newErrors.email = "이메일을 입력해주세요";
} else if (!emailRegex.test(formData.email)) {
newErrors.email = "올바른 이메일 형식이 아닙니다";
}
// 전화번호 검증
const phoneRegex = /^[0-9-]+$/;
if (formData.phone && !phoneRegex.test(formData.phone)) {
newErrors.phone = "올바른 전화번호 형식이 아닙니다";
}
// 카테고리 검증
if (!formData.category) {
newErrors.category = "카테고리를 선택해주세요";
}
// 날짜 검증
if (formData.startDate && formData.endDate) {
if (formData.startDate > formData.endDate) {
newErrors.startDate = "시작일은 종료일보다 이전이어야 합니다";
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 폼 제출
const handleSubmit = async () => {
if (!validateForm()) {
return;
}
setIsSubmitting(true);
// API 호출 시뮬레이션 (실제로는 API 호출)
await new Promise((resolve) => setTimeout(resolve, 1500));
console.log("폼 데이터:", formData);
setIsSubmitting(false);
setIsOpen(false);
// 폼 초기화
setFormData({
name: "",
email: "",
phone: "",
category: "",
priority: "medium",
description: "",
isActive: true,
});
setErrors({});
};
// 폼 필드 변경
const handleChange = (field: keyof FormData, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }));
// 해당 필드의 에러 제거
if (errors[field as keyof FormErrors]) {
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[field as keyof FormErrors];
return newErrors;
});
}
};
// 취소
const handleCancel = () => {
setIsOpen(false);
setFormData({
name: "",
email: "",
phone: "",
category: "",
priority: "medium",
description: "",
isActive: true,
});
setErrors({});
};
return (
<>
{/* 트리거 버튼 */}
<Button onClick={() => setIsOpen(true)} className="gap-2">
</Button>
{/* 폼 Dialog */}
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg"> </DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
. * .
</DialogDescription>
</DialogHeader>
{/* 폼 컨텐츠 */}
<div className="space-y-4">
{/* 이름 (필수) */}
<div className="space-y-2">
<Label htmlFor="name" className="text-xs sm:text-sm">
<span className="text-destructive">*</span>
</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
placeholder="홍길동"
className={cn("h-8 text-xs sm:h-10 sm:text-sm", errors.name && "border-destructive")}
/>
{errors.name && (
<p className="text-destructive flex items-center gap-1 text-xs">
<AlertCircle className="h-3 w-3" />
{errors.name}
</p>
)}
</div>
{/* 이메일 (필수) */}
<div className="space-y-2">
<Label htmlFor="email" className="text-xs sm:text-sm">
<span className="text-destructive">*</span>
</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
placeholder="example@email.com"
className={cn("h-8 text-xs sm:h-10 sm:text-sm", errors.email && "border-destructive")}
/>
{errors.email && (
<p className="text-destructive flex items-center gap-1 text-xs">
<AlertCircle className="h-3 w-3" />
{errors.email}
</p>
)}
{!errors.email && formData.email && formData.email.includes("@") && (
<p className="text-muted-foreground flex items-center gap-1 text-xs">
<CheckCircle className="h-3 w-3 text-green-600" />
</p>
)}
</div>
{/* 전화번호 (선택) */}
<div className="space-y-2">
<Label htmlFor="phone" className="text-xs sm:text-sm">
</Label>
<Input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) => handleChange("phone", e.target.value)}
placeholder="010-1234-5678"
className={cn("h-8 text-xs sm:h-10 sm:text-sm", errors.phone && "border-destructive")}
/>
{errors.phone && (
<p className="text-destructive flex items-center gap-1 text-xs">
<AlertCircle className="h-3 w-3" />
{errors.phone}
</p>
)}
</div>
{/* 카테고리 & 우선순위 (같은 줄) */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{/* 카테고리 (필수) */}
<div className="space-y-2">
<Label htmlFor="category" className="text-xs sm:text-sm">
<span className="text-destructive">*</span>
</Label>
<Select value={formData.category} onValueChange={(value) => handleChange("category", value)}>
<SelectTrigger
id="category"
className={cn("h-8 text-xs sm:h-10 sm:text-sm", errors.category && "border-destructive")}
>
<SelectValue placeholder="선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="customer"></SelectItem>
<SelectItem value="partner"></SelectItem>
<SelectItem value="supplier"></SelectItem>
<SelectItem value="employee"></SelectItem>
</SelectContent>
</Select>
{errors.category && (
<p className="text-destructive flex items-center gap-1 text-xs">
<AlertCircle className="h-3 w-3" />
{errors.category}
</p>
)}
</div>
{/* 우선순위 */}
<div className="space-y-2">
<Label htmlFor="priority" className="text-xs sm:text-sm">
</Label>
<Select value={formData.priority} onValueChange={(value) => handleChange("priority", value)}>
<SelectTrigger id="priority" className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low"></SelectItem>
<SelectItem value="medium"></SelectItem>
<SelectItem value="high"></SelectItem>
<SelectItem value="urgent"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* 시작일 & 종료일 (같은 줄) */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{/* 시작일 */}
<div className="space-y-2">
<Label className="text-xs sm:text-sm"></Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"h-8 w-full justify-start text-left text-xs font-normal sm:h-10 sm:text-sm",
!formData.startDate && "text-muted-foreground",
errors.startDate && "border-destructive",
)}
>
{formData.startDate ? (
formData.startDate.toLocaleDateString("ko-KR", {
year: "numeric",
month: "long",
day: "numeric",
})
) : (
<span> </span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<CustomCalendar
mode="single"
selected={formData.startDate}
onSelect={(date) => handleChange("startDate", date)}
/>
</PopoverContent>
</Popover>
{errors.startDate && (
<p className="text-destructive flex items-center gap-1 text-xs">
<AlertCircle className="h-3 w-3" />
{errors.startDate}
</p>
)}
</div>
{/* 종료일 */}
<div className="space-y-2">
<Label className="text-xs sm:text-sm"></Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"h-8 w-full justify-start text-left text-xs font-normal sm:h-10 sm:text-sm",
!formData.endDate && "text-muted-foreground",
)}
>
{formData.endDate ? (
formData.endDate.toLocaleDateString("ko-KR", {
year: "numeric",
month: "long",
day: "numeric",
})
) : (
<span> </span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<CustomCalendar
mode="single"
selected={formData.endDate}
onSelect={(date) => handleChange("endDate", date)}
/>
</PopoverContent>
</Popover>
</div>
</div>
{/* 설명 */}
<div className="space-y-2">
<Label htmlFor="description" className="text-xs sm:text-sm">
</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => handleChange("description", e.target.value)}
placeholder="추가 정보를 입력하세요..."
className="min-h-[80px] text-xs sm:text-sm"
/>
<p className="text-muted-foreground text-xs">{formData.description.length} / 500</p>
</div>
{/* 활성화 상태 */}
<div className="flex items-center justify-between rounded-lg border p-3 sm:p-4">
<div className="space-y-0.5">
<Label htmlFor="isActive" className="text-xs font-medium sm:text-sm">
</Label>
<p className="text-muted-foreground text-xs"> </p>
</div>
<Switch
id="isActive"
checked={formData.isActive}
onCheckedChange={(checked) => handleChange("isActive", checked)}
/>
</div>
</div>
{/* 푸터 */}
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={handleCancel}
disabled={isSubmitting}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button
onClick={handleSubmit}
disabled={isSubmitting}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{isSubmitting ? "처리 중..." : "등록"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}