스타일 수정중
This commit is contained in:
421
frontend/components/examples/ExampleFormDialog.tsx
Normal file
421
frontend/components/examples/ExampleFormDialog.tsx
Normal file
@@ -0,0 +1,421 @@
|
||||
"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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user