diff options
Diffstat (limited to 'components/vendor-regular-registrations')
3 files changed, 865 insertions, 0 deletions
diff --git a/components/vendor-regular-registrations/additional-info-dialog.tsx b/components/vendor-regular-registrations/additional-info-dialog.tsx new file mode 100644 index 00000000..fbd60515 --- /dev/null +++ b/components/vendor-regular-registrations/additional-info-dialog.tsx @@ -0,0 +1,502 @@ +"use client";
+
+import * as React from "react";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Plus, Trash2 } from "lucide-react";
+import { toast } from "sonner";
+import {
+ saveVendorBusinessContacts,
+ saveVendorAdditionalInfo,
+ fetchVendorRegistrationStatus
+} from "@/lib/vendor-regular-registrations/service";
+
+// 업무담당자 정보 스키마
+const businessContactSchema = z.object({
+ contactType: z.enum(["sales", "design", "delivery", "quality", "tax_invoice"]),
+ contactName: z.string().min(1, "담당자명은 필수입니다"),
+ position: z.string().min(1, "직급은 필수입니다"),
+ department: z.string().min(1, "부서는 필수입니다"),
+ responsibility: z.string().min(1, "담당업무는 필수입니다"),
+ email: z.string().email("올바른 이메일 형식이 아닙니다"),
+});
+
+// 추가정보 스키마
+const additionalInfoSchema = z.object({
+ businessType: z.string().optional(),
+ industryType: z.string().optional(),
+ companySize: z.string().optional(),
+ revenue: z.string().optional(),
+ factoryEstablishedDate: z.string().optional(),
+ preferredContractTerms: z.string().optional(),
+});
+
+// 전체 폼 스키마
+const formSchema = z.object({
+ businessContacts: z.array(businessContactSchema).min(5, "모든 업무담당자 정보를 입력해주세요"),
+ additionalInfo: additionalInfoSchema,
+});
+
+type FormData = z.infer<typeof formSchema>;
+
+interface AdditionalInfoDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ vendorId: number;
+ onSave?: () => void;
+}
+
+const contactTypes = [
+ { value: "sales", label: "영업", required: true },
+ { value: "design", label: "설계", required: true },
+ { value: "delivery", label: "납기", required: true },
+ { value: "quality", label: "품질", required: true },
+ { value: "tax_invoice", label: "세금계산서", required: true },
+];
+
+const businessTypes = [
+ { value: "manufacturing", label: "제조업" },
+ { value: "trading", label: "무역업" },
+ { value: "service", label: "서비스업" },
+ { value: "construction", label: "건설업" },
+ { value: "other", label: "기타" },
+];
+
+const industryTypes = [
+ { value: "shipbuilding", label: "조선업" },
+ { value: "marine", label: "해양플랜트" },
+ { value: "energy", label: "에너지" },
+ { value: "automotive", label: "자동차" },
+ { value: "other", label: "기타" },
+];
+
+const companySizes = [
+ { value: "large", label: "대기업" },
+ { value: "medium", label: "중견기업" },
+ { value: "small", label: "중소기업" },
+ { value: "startup", label: "스타트업" },
+];
+
+export function AdditionalInfoDialog({
+ open,
+ onOpenChange,
+ vendorId,
+ onSave,
+}: AdditionalInfoDialogProps) {
+ const [saving, setSaving] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const form = useForm<FormData>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ businessContacts: contactTypes.map(type => ({
+ contactType: type.value as any,
+ contactName: "",
+ position: "",
+ department: "",
+ responsibility: "",
+ email: "",
+ })),
+ additionalInfo: {
+ businessType: "",
+ industryType: "",
+ companySize: "",
+ revenue: "",
+ factoryEstablishedDate: "",
+ preferredContractTerms: "",
+ },
+ },
+ });
+
+ // 기존 데이터 로드
+ const loadExistingData = async () => {
+ if (!vendorId || !open) return;
+
+ setLoading(true);
+ try {
+ const result = await fetchVendorRegistrationStatus(vendorId);
+ if (result.success && result.data) {
+ const { businessContacts, additionalInfo } = result.data;
+
+ // 업무담당자 데이터 설정
+ const contactsData = contactTypes.map(type => {
+ const existingContact = businessContacts.find(c => c.contactType === type.value);
+ return existingContact ? {
+ contactType: existingContact.contactType,
+ contactName: existingContact.contactName,
+ position: existingContact.position,
+ department: existingContact.department,
+ responsibility: existingContact.responsibility,
+ email: existingContact.email,
+ } : {
+ contactType: type.value as any,
+ contactName: "",
+ position: "",
+ department: "",
+ responsibility: "",
+ email: "",
+ };
+ });
+
+ // 추가정보 데이터 설정
+ const additionalData = {
+ businessType: additionalInfo?.businessType || "",
+ industryType: additionalInfo?.industryType || "",
+ companySize: additionalInfo?.companySize || "",
+ revenue: additionalInfo?.revenue || "",
+ factoryEstablishedDate: additionalInfo?.factoryEstablishedDate
+ ? new Date(additionalInfo.factoryEstablishedDate).toISOString().split('T')[0]
+ : "",
+ preferredContractTerms: additionalInfo?.preferredContractTerms || "",
+ };
+
+ // 폼 데이터 업데이트
+ form.reset({
+ businessContacts: contactsData,
+ additionalInfo: additionalData,
+ });
+ }
+ } catch (error) {
+ console.error("Error loading existing data:", error);
+ toast.error("기존 데이터를 불러오는 중 오류가 발생했습니다.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 다이얼로그가 열릴 때 데이터 로드
+ React.useEffect(() => {
+ loadExistingData();
+ }, [vendorId, open]);
+
+ const handleSave = async (data: FormData) => {
+ setSaving(true);
+ try {
+ // 업무담당자 정보 저장
+ const contactsResult = await saveVendorBusinessContacts(vendorId, data.businessContacts);
+ if (!contactsResult.success) {
+ throw new Error(contactsResult.error);
+ }
+
+ // 추가정보 저장
+ const additionalResult = await saveVendorAdditionalInfo(vendorId, data.additionalInfo);
+ if (!additionalResult.success) {
+ throw new Error(additionalResult.error);
+ }
+
+ toast.success("추가정보가 저장되었습니다.");
+ onSave?.();
+ onOpenChange(false);
+ } catch (error) {
+ console.error("Error saving additional info:", error);
+ toast.error(error instanceof Error ? error.message : "추가정보 저장 중 오류가 발생했습니다.");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>추가정보 입력</DialogTitle>
+ <p className="text-sm text-muted-foreground">
+ 정규업체 등록을 위한 추가정보를 입력해주세요. <span className="text-red-500">*</span> 표시는 필수 입력 항목입니다.
+ </p>
+ </DialogHeader>
+
+ {loading ? (
+ <div className="p-8 text-center">
+ <div className="text-sm text-muted-foreground">데이터를 불러오는 중...</div>
+ </div>
+ ) : (
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(handleSave)}>
+ <Tabs defaultValue="contacts" className="w-full">
+ <TabsList className="grid w-full grid-cols-2">
+ <TabsTrigger value="contacts">업무담당자 정보</TabsTrigger>
+ <TabsTrigger value="additional">추가정보</TabsTrigger>
+ </TabsList>
+
+ <TabsContent value="contacts" className="space-y-4">
+ <div className="space-y-4">
+ {contactTypes.map((contactType, index) => (
+ <Card key={contactType.value}>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-lg flex items-center gap-2">
+ {contactType.label} 담당자
+ <Badge variant="destructive" className="text-xs">필수</Badge>
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name={`businessContacts.${index}.contactName`}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>담당자명 *</FormLabel>
+ <FormControl>
+ <Input placeholder="담당자명 입력" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name={`businessContacts.${index}.position`}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>직급 *</FormLabel>
+ <FormControl>
+ <Input placeholder="직급 입력" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name={`businessContacts.${index}.department`}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>부서 *</FormLabel>
+ <FormControl>
+ <Input placeholder="부서명 입력" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name={`businessContacts.${index}.email`}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Email *</FormLabel>
+ <FormControl>
+ <Input placeholder="이메일 입력" type="email" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ <FormField
+ control={form.control}
+ name={`businessContacts.${index}.responsibility`}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>담당업무 *</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="담당업무 상세 입력"
+ className="h-20"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </CardContent>
+ </Card>
+ ))}
+ </div>
+ </TabsContent>
+
+ <TabsContent value="additional" className="space-y-4">
+ <Card>
+ <CardHeader>
+ <CardTitle>회사 추가정보</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="additionalInfo.businessType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>사업유형</FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="사업유형 선택" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {businessTypes.map((type) => (
+ <SelectItem key={type.value} value={type.value}>
+ {type.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="additionalInfo.industryType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>산업유형</FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="산업유형 선택" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {industryTypes.map((type) => (
+ <SelectItem key={type.value} value={type.value}>
+ {type.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="additionalInfo.companySize"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>기업규모</FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="기업규모 선택" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {companySizes.map((size) => (
+ <SelectItem key={size.value} value={size.value}>
+ {size.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="additionalInfo.revenue"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>매출액 (억원)</FormLabel>
+ <FormControl>
+ <Input
+ placeholder="매출액 입력"
+ type="number"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="additionalInfo.factoryEstablishedDate"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>공장설립일</FormLabel>
+ <FormControl>
+ <Input
+ placeholder="YYYY-MM-DD"
+ type="date"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ <FormField
+ control={form.control}
+ name="additionalInfo.preferredContractTerms"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>선호계약조건</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="선호하는 계약조건을 상세히 입력해주세요"
+ className="h-32"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </CardContent>
+ </Card>
+ </TabsContent>
+ </Tabs>
+
+ <DialogFooter className="mt-6">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={saving}
+ >
+ 취소
+ </Button>
+ <Button type="submit" disabled={saving}>
+ {saving ? "저장 중..." : "저장"}
+ </Button>
+ </DialogFooter>
+ </form>
+ </Form>
+ )}
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/components/vendor-regular-registrations/document-status-dialog.tsx b/components/vendor-regular-registrations/document-status-dialog.tsx new file mode 100644 index 00000000..f8e2e1cd --- /dev/null +++ b/components/vendor-regular-registrations/document-status-dialog.tsx @@ -0,0 +1,265 @@ +"use client";
+
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { FileText, Download, CheckCircle, XCircle, Clock } from "lucide-react";
+import { toast } from "sonner";
+
+import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig";
+import {
+ documentStatusColumns,
+ contractAgreementColumns,
+} from "@/config/vendorRegularRegistrationsColumnsConfig";
+import { downloadFile } from "@/lib/file-download";
+
+interface DocumentStatusDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ registration: VendorRegularRegistration | null;
+}
+
+const StatusIcon = ({ status }: { status: string | boolean }) => {
+ if (typeof status === "boolean") {
+ return status ? (
+ <CheckCircle className="w-4 h-4 text-green-600" />
+ ) : (
+ <XCircle className="w-4 h-4 text-red-500" />
+ );
+ }
+
+ switch (status) {
+ case "completed":
+ return <CheckCircle className="w-4 h-4 text-green-600" />;
+ case "reviewing":
+ return <Clock className="w-4 h-4 text-yellow-600" />;
+ case "not_submitted":
+ default:
+ return <XCircle className="w-4 h-4 text-red-500" />;
+ }
+};
+
+const StatusBadge = ({ status }: { status: string | boolean }) => {
+ if (typeof status === "boolean") {
+ return (
+ <Badge variant={status ? "default" : "destructive"}>
+ {status ? "제출완료" : "미제출"}
+ </Badge>
+ );
+ }
+
+ const statusConfig = {
+ completed: { label: "완료", variant: "default" as const },
+ reviewing: { label: "검토중", variant: "secondary" as const },
+ not_submitted: { label: "미제출", variant: "destructive" as const },
+ };
+
+ const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.not_submitted;
+
+ return <Badge variant={config.variant}>{config.label}</Badge>;
+};
+
+export function DocumentStatusDialog({
+ open,
+ onOpenChange,
+ registration,
+}: DocumentStatusDialogProps) {
+ if (!registration) return null;
+
+ // 파일 다운로드 핸들러
+ const handleFileDownload = async (docKey: string, fileIndex: number = 0) => {
+ try {
+ const files = registration.documentFiles[docKey as keyof typeof registration.documentFiles];
+ if (!files || files.length === 0) {
+ toast.error("다운로드할 파일이 없습니다.");
+ return;
+ }
+
+ const file = files[fileIndex];
+ if (!file) {
+ toast.error("파일을 찾을 수 없습니다.");
+ return;
+ }
+
+ // filePath와 fileName 추출
+ const filePath = file.filePath || file.path;
+ const fileName = file.originalFileName || file.fileName || file.name;
+
+ if (!filePath || !fileName) {
+ toast.error("파일 정보가 올바르지 않습니다.");
+ return;
+ }
+
+ console.log(`📥 파일 다운로드 시작:`, { filePath, fileName, docKey });
+
+ // downloadFile 함수를 사용하여 파일 다운로드
+ const result = await downloadFile(filePath, fileName, {
+ showToast: true,
+ onError: (error) => {
+ console.error("파일 다운로드 오류:", error);
+ toast.error(`파일 다운로드 실패: ${error}`);
+ },
+ onSuccess: (fileName, fileSize) => {
+ console.log(`✅ 파일 다운로드 성공:`, { fileName, fileSize });
+ }
+ });
+
+ if (!result.success) {
+ console.error("파일 다운로드 실패:", result.error);
+ }
+ } catch (error) {
+ console.error("파일 다운로드 중 오류 발생:", error);
+ toast.error("파일 다운로드 중 오류가 발생했습니다.");
+ }
+ };
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <FileText className="w-5 h-5" />
+ 문서/자료 접수 현황 - {registration.companyName}
+ </DialogTitle>
+ </DialogHeader>
+
+ <div className="space-y-6">
+ {/* 기본 정보 */}
+ <div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg">
+ <div>
+ <span className="text-sm font-medium text-gray-600">업체명:</span>
+ <span className="ml-2">{registration.companyName}</span>
+ </div>
+ <div>
+ <span className="text-sm font-medium text-gray-600">사업자번호:</span>
+ <span className="ml-2">{registration.businessNumber}</span>
+ </div>
+ <div>
+ <span className="text-sm font-medium text-gray-600">대표자:</span>
+ <span className="ml-2">{registration.representative || "-"}</span>
+ </div>
+ <div>
+ <span className="text-sm font-medium text-gray-600">현재상태:</span>
+ <Badge className="ml-2">{registration.status}</Badge>
+ </div>
+ </div>
+
+ {/* 문서 제출 현황 */}
+ <div>
+ <div className="flex items-center justify-between mb-4">
+ <h3 className="text-lg font-semibold">문서 제출 현황</h3>
+ </div>
+ <div className="border rounded-lg">
+ <div className="grid grid-cols-4 gap-4 p-4 bg-gray-50 font-medium text-sm">
+ <div>문서유형</div>
+ <div>상태</div>
+ <div>제출일자</div>
+ <div>액션</div>
+ </div>
+ {documentStatusColumns.map((doc) => {
+ const isSubmitted = registration.documentSubmissions[
+ doc.key as keyof typeof registration.documentSubmissions
+ ] as boolean;
+
+ return (
+ <div
+ key={doc.key}
+ className="grid grid-cols-4 gap-4 p-4 border-t items-center"
+ >
+ <div className="flex items-center gap-2">
+ <StatusIcon status={isSubmitted} />
+ {doc.label}
+ </div>
+ <div>
+ <StatusBadge status={isSubmitted} />
+ </div>
+ <div className="text-sm text-gray-600">
+ {isSubmitted ? "2024.01.01" : "-"}
+ </div>
+ <div>
+ {isSubmitted && (
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => handleFileDownload(doc.key)}
+ >
+ <Download className="w-4 h-4 mr-1" />
+ 다운로드
+ </Button>
+ )}
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </div>
+
+ {/* 계약 동의 현황 */}
+ <div>
+ <div className="flex items-center justify-between mb-4">
+ <h3 className="text-lg font-semibold">계약 동의 현황</h3>
+ </div>
+ <div className="border rounded-lg">
+ <div className="grid grid-cols-4 gap-4 p-4 bg-gray-50 font-medium text-sm">
+ <div>계약유형</div>
+ <div>상태</div>
+ <div>서약일자</div>
+ <div>액션</div>
+ </div>
+ {contractAgreementColumns.map((agreement) => {
+ const status = registration.contractAgreements[
+ agreement.key as keyof typeof registration.contractAgreements
+ ] as string;
+
+ return (
+ <div
+ key={agreement.key}
+ className="grid grid-cols-4 gap-4 p-4 border-t items-center"
+ >
+ <div className="flex items-center gap-2">
+ <StatusIcon status={status} />
+ {agreement.label}
+ </div>
+ <div>
+ <StatusBadge status={status} />
+ </div>
+ <div className="text-sm text-gray-600">
+ {status === "completed" ? "2024.01.01" : "-"}
+ </div>
+ <div>
+ {status === "completed" && (
+ <Button size="sm" variant="outline">
+ <Download className="w-4 h-4 mr-1" />
+ 다운로드
+ </Button>
+ )}
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </div>
+
+ {/* 추가 정보 */}
+ <div>
+ <h3 className="text-lg font-semibold mb-4">추가 정보</h3>
+ <div className="p-4 border rounded-lg">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <StatusIcon status={registration.additionalInfo} />
+ <span>추가 정보 등록</span>
+ </div>
+ <StatusBadge status={registration.additionalInfo} />
+ </div>
+ </div>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/components/vendor-regular-registrations/skip-reason-dialog.tsx b/components/vendor-regular-registrations/skip-reason-dialog.tsx new file mode 100644 index 00000000..f47d8929 --- /dev/null +++ b/components/vendor-regular-registrations/skip-reason-dialog.tsx @@ -0,0 +1,98 @@ +"use client";
+
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+import { useState } from "react";
+import { toast } from "sonner";
+
+interface SkipReasonDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ title: string;
+ description: string;
+ onConfirm: (reason: string) => Promise<void>;
+ loading: boolean;
+}
+
+export function SkipReasonDialog({
+ open,
+ onOpenChange,
+ title,
+ description,
+ onConfirm,
+ loading,
+}: SkipReasonDialogProps) {
+ const [reason, setReason] = useState("");
+
+ const handleConfirm = async () => {
+ if (!reason.trim()) {
+ toast.error("Skip 사유를 입력해주세요.");
+ return;
+ }
+
+ try {
+ await onConfirm(reason.trim());
+ setReason(""); // 성공 시 초기화
+ onOpenChange(false);
+ } catch (error) {
+ // 에러는 상위 컴포넌트에서 처리
+ }
+ };
+
+ const handleCancel = () => {
+ setReason("");
+ onOpenChange(false);
+ };
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="sm:max-w-md">
+ <DialogHeader>
+ <DialogTitle>{title}</DialogTitle>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <p className="text-sm text-muted-foreground">
+ {description}
+ </p>
+
+ <div className="space-y-2">
+ <Label htmlFor="reason">Skip 사유</Label>
+ <Textarea
+ id="reason"
+ placeholder="Skip 사유를 입력해주세요..."
+ value={reason}
+ onChange={(e) => setReason(e.target.value)}
+ rows={4}
+ disabled={loading}
+ />
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={handleCancel}
+ disabled={loading}
+ >
+ 취소
+ </Button>
+ <Button
+ onClick={handleConfirm}
+ disabled={loading || !reason.trim()}
+ >
+ {loading ? "처리 중..." : "확인"}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
|
