422 lines
15 KiB
TypeScript
422 lines
15 KiB
TypeScript
"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>
|
|
</>
|
|
);
|
|
}
|