diff options
| -rw-r--r-- | components/basic-contract/send-contract-dialog.tsx | 709 |
1 files changed, 709 insertions, 0 deletions
diff --git a/components/basic-contract/send-contract-dialog.tsx b/components/basic-contract/send-contract-dialog.tsx new file mode 100644 index 00000000..511011b0 --- /dev/null +++ b/components/basic-contract/send-contract-dialog.tsx @@ -0,0 +1,709 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, FileText, X, Plus } from "lucide-react" +import { toast } from "sonner" +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" +import { Checkbox } from "@/components/ui/checkbox" +import { Label } from "@/components/ui/label" +import { Badge } from "@/components/ui/badge" +import { Progress } from "@/components/ui/progress" +import { Vendor } from "@/db/schema/vendors" +import { requestBasicContractInfo } from "@/lib/vendors/service" +import { getALLBasicContractTemplates } from "@/lib/basic-contract/service" +import type { BasicContractTemplate } from "@/db/schema" +import { saveNdaAttachments } from "@/lib/vendors/service" +import { useSession } from "next-auth/react" +import { useRouter } from "next/navigation" +import { createGtcVendorDocuments, createProjectGtcVendorDocuments, getStandardGtcDocumentId } from "@/lib/gtc-contract/service" + +export interface SendContractDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> { + vendors: Row<Vendor>["original"][] + showTrigger?: boolean + onSuccess?: () => void + title?: string + description?: string + triggerLabel?: string +} + +export function SendContractDialog({ + vendors, + showTrigger = true, + onSuccess, + title = "기본계약서 발송", + description = "선택된 협력업체들에게 기본계약서를 발송합니다.", + triggerLabel = "기본계약서 발송", + ...props +}: SendContractDialogProps) { + const [isProcessing, startProcessingTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + const { data: session } = useSession() + const router = useRouter() + + // 상태 관리 + const [basicContractTemplates, setBasicContractTemplates] = React.useState<BasicContractTemplate[]>([]) + const [selectedTemplateIds, setSelectedTemplateIds] = React.useState<number[]>([]) + const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false) + + // 비밀유지 계약서 첨부파일 관련 상태 + const [ndaAttachments, setNdaAttachments] = React.useState<File[]>([]) + const [isUploadingNdaFiles, setIsUploadingNdaFiles] = React.useState(false) + + // 프로그레스 관련 상태 + const [progressValue, setProgressValue] = React.useState(0) + const [currentStep, setCurrentStep] = React.useState("") + const [showProgress, setShowProgress] = React.useState(false) + + // 기본계약서 템플릿 로딩 및 자동 선택 + React.useEffect(() => { + setIsLoadingTemplates(true) + getALLBasicContractTemplates() + .then((templates) => { + setBasicContractTemplates(templates) + + // 벤더 국가별 자동 선택 로직 + if (vendors.length > 0) { + const isAllForeign = vendors.every(vendor => vendor.country !== 'KR') + const isAllDomestic = vendors.every(vendor => vendor.country === 'KR') + + if (isAllForeign) { + // 외자: 준법서약 (영문), GTC 선택 (GTC는 1개만 선택하도록) + const foreignTemplates = templates.filter(template => + template.templateName?.includes('준법서약') && template.templateName?.includes('영문') || + template.templateName?.includes('gtc') + ) + // GTC 템플릿 중 최신 리비전의 것만 선택 + const gtcTemplates = foreignTemplates.filter(t => t.templateName?.includes('gtc')) + const nonGtcTemplates = foreignTemplates.filter(t => !t.templateName?.includes('gtc')) + + if (gtcTemplates.length > 0) { + // GTC 템플릿 중 이름이 가장 긴 것 (프로젝트 GTC 우선) 선택 + const selectedGtcTemplate = gtcTemplates.reduce((prev, current) => + (prev.templateName?.length || 0) > (current.templateName?.length || 0) ? prev : current + ) + setSelectedTemplateIds([...nonGtcTemplates.map(t => t.id), selectedGtcTemplate.id]) + } else { + setSelectedTemplateIds(nonGtcTemplates.map(t => t.id)) + } + } else if (isAllDomestic) { + // 내자: 준법서약 (영문), GTC 제외한 모든 템플릿 선택 + const domesticTemplates = templates.filter(template => { + const name = template.templateName?.toLowerCase() || '' + return !(name.includes('준법서약') && name.includes('영문')) && !name.includes('gtc') + }) + setSelectedTemplateIds(domesticTemplates.map(t => t.id)) + } + } + }) + .catch(() => toast.error("기본계약서 템플릿 로딩 실패")) + .finally(() => setIsLoadingTemplates(false)) + }, [vendors]) + + React.useEffect(() => { + if (!props.open) { + setSelectedTemplateIds([]) + setNdaAttachments([]) + setIsUploadingNdaFiles(false) + setProgressValue(0) + setCurrentStep("") + setShowProgress(false) + } + }, [props.open]) + + // 비밀유지 계약서 첨부파일 추가 함수 + const handleAddNdaAttachment = (event: React.ChangeEvent<HTMLInputElement>) => { + const files = event.target.files + if (files) { + const newFiles = Array.from(files) + setNdaAttachments(prev => [...prev, ...newFiles]) + } + // input 초기화 + event.target.value = '' + } + + // 비밀유지 계약서 첨부파일 제거 함수 + const handleRemoveNdaAttachment = (fileIndex: number) => { + setNdaAttachments(prev => prev.filter((_, index) => index !== fileIndex)) + } + + // 비밀유지 계약서가 선택되었는지 확인하는 함수 + const isNdaTemplateSelected = () => { + return basicContractTemplates.some(template => + selectedTemplateIds.includes(template.id) && + template.templateName?.includes("비밀유지") + ) + } + + const onSendContracts = () => { + if (selectedTemplateIds.length === 0) { + return toast.error("최소 하나의 기본계약서 템플릿을 선택하세요.") + } + if (!session?.user?.id) return toast.error("인증 실패") + + // GTC 템플릿 선택 검증 + const selectedGtcTemplates = basicContractTemplates.filter(template => + selectedTemplateIds.includes(template.id) && + template.templateName?.toLowerCase().includes('gtc') + ) + + if (selectedGtcTemplates.length > 1) { + return toast.error("GTC 템플릿은 하나만 선택할 수 있습니다.") + } + + // 프로그레스 바를 즉시 표시 + setShowProgress(true) + setProgressValue(0) + setCurrentStep("시작 중...") + + startProcessingTransition(async () => { + try { + // 전체 단계 수 계산 + const gtcTemplates = basicContractTemplates.filter(template => + selectedTemplateIds.includes(template.id) && + template.templateName?.toLowerCase().includes('gtc') + ) + + const totalSteps = + (selectedTemplateIds.length > 0 ? 1 : 0) + + (isNdaTemplateSelected() && ndaAttachments.length > 0 ? 1 : 0) + + (gtcTemplates.length > 0 ? 1 : 0) + let completedSteps = 0 + + // 1단계: 기본계약서 템플릿이 선택된 경우 백그라운드에서 처리 + if (selectedTemplateIds.length > 0) { + const templates = basicContractTemplates.filter(t => + selectedTemplateIds.includes(t.id) + ) + + setCurrentStep(`기본계약서 생성 중... (${templates.length}개 템플릿)`) + console.log("📋 기본계약서 백그라운드 처리 시작", templates.length, "개 템플릿") + await processBasicContractsInBackground(templates, vendors) + + completedSteps++ + setProgressValue((completedSteps / totalSteps) * 100) + } + + // 2단계: 비밀유지 계약서 첨부파일이 있는 경우 저장 + if (isNdaTemplateSelected() && ndaAttachments.length > 0) { + setCurrentStep(`비밀유지 계약서 첨부파일 저장 중... (${ndaAttachments.length}개 파일)`) + console.log("📎 비밀유지 계약서 첨부파일 처리 시작", ndaAttachments.length, "개 파일") + + const ndaResult = await saveNdaAttachments({ + vendorIds: vendors.map((v) => v.id), + files: ndaAttachments, + userId: session.user.id.toString() + }) + + if (ndaResult.success) { + toast.success(`비밀유지 계약서 첨부파일이 모두 저장되었습니다 (${ndaResult.summary?.success}/${ndaResult.summary?.total})`) + } else { + toast.error(`첨부파일 처리 중 일부 오류가 발생했습니다: ${ndaResult.error}`) + } + + completedSteps++ + setProgressValue((completedSteps / totalSteps) * 100) + } + + // 3단계: GTC 템플릿 처리 + if (selectedGtcTemplates.length > 0) { + setCurrentStep(`GTC 문서 생성 중... (${selectedGtcTemplates.length}개 템플릿)`) + console.log("📋 GTC 문서 생성 시작", selectedGtcTemplates.length, "개 템플릿") + + try { + await processGtcTemplates(selectedGtcTemplates, vendors) + completedSteps++ + setProgressValue((completedSteps / totalSteps) * 100) + } catch (error) { + console.error("GTC 템플릿 처리 중 오류:", error) + toast.error(`GTC 템플릿 처리 중 오류가 발생했습니다`) + // GTC 처리 실패해도 전체 프로세스는 성공으로 간주 + completedSteps++ + setProgressValue((completedSteps / totalSteps) * 100) + } + } + + setCurrentStep("완료!") + setProgressValue(100) + toast.success("기본계약서가 성공적으로 발송되었습니다") + + // 잠시 완료 상태를 보여준 후 다이얼로그 닫기 + setTimeout(() => { + setShowProgress(false) + props.onOpenChange?.(false) + onSuccess?.() + }, 1000) + + } catch (error) { + console.error('기본계약서 발송 오류:', error) + setShowProgress(false) + toast.error(`처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) + } + }) + } + + // 백그라운드에서 기본계약서 처리 + const processBasicContractsInBackground = async (templates: BasicContractTemplate[], vendors: any[]) => { + if (!session?.user?.id) { + toast.error("인증 정보가 없습니다") + return + } + + try { + const totalContracts = templates.length * vendors.length + let processedCount = 0 + + // 각 벤더별로, 각 템플릿을 처리 + for (let vendorIndex = 0; vendorIndex < vendors.length; vendorIndex++) { + const vendor = vendors[vendorIndex] + + // 벤더별 템플릿 데이터 생성 (한글 변수명 사용) + const templateData = { + company_name: vendor.vendorName || '협력업체명', + company_address: vendor.address || '주소', + representative_name: vendor.representativeName || '대표자명', + signature_date: new Date().toLocaleDateString('ko-KR'), + tax_id: vendor.taxId || '사업자번호', + phone_number: vendor.phone || '전화번호', + } + + console.log(`🔄 벤더 ${vendorIndex + 1}/${vendors.length} 템플릿 데이터:`, templateData) + + // 해당 벤더에 대해 각 템플릿을 순차적으로 처리 + for (let templateIndex = 0; templateIndex < templates.length; templateIndex++) { + const template = templates[templateIndex] + processedCount++ + + // 진행률 업데이트 + const contractProgress = (processedCount / totalContracts) * 100 + setProgressValue(contractProgress) + setCurrentStep(`기본계약서 생성 중... (${processedCount}/${totalContracts})`) + + console.log(`📄 처리 중: ${vendor.vendorName} - ${template.templateName} (${processedCount}/${totalContracts})`) + + // 개별 벤더에 대한 기본계약 생성 + await processTemplate(template, templateData, [vendor]) + + console.log(`✅ 완료: ${vendor.vendorName} - ${template.templateName}`) + } + } + + toast.success(`총 ${totalContracts}개 기본계약이 모두 생성되었습니다`) + router.refresh(); + + } catch (error) { + console.error('기본계약 처리 중 오류:', error) + toast.error(`기본계약 처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) + } + } + + const processTemplate = async (template: BasicContractTemplate, templateData: any, vendors: any[]) => { + try { + // 1. 템플릿 파일 가져오기 + const templateResponse = await fetch('/api/basic-contract/get-template', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ templateId: template.id }) + }) + + if (!templateResponse.ok) { + throw new Error(`템플릿 파일을 가져올 수 없습니다: ${template.templateName}`) + } + + const templateBlob = await templateResponse.blob() + + // 2. PDFTron을 사용해서 변수 치환 및 PDF 변환 + // @ts-ignore + const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer) + + // 임시 WebViewer 인스턴스 생성 (DOM에 추가하지 않음) + const tempDiv = document.createElement('div') + tempDiv.style.display = 'none' + document.body.appendChild(tempDiv) + + const instance = await WebViewer( + { + path: "/pdftronWeb", + licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, + fullAPI: true, + }, + tempDiv + ) + + try { + const { Core } = instance + const { createDocument } = Core + + // 3. 템플릿 문서 생성 및 변수 치환 + const templateDoc = await createDocument(templateBlob, { + filename: template.fileName || 'template.docx', + extension: 'docx', + }) + + console.log("🔄 변수 치환 시작:", templateData) + await templateDoc.applyTemplateValues(templateData) + console.log("✅ 변수 치환 완료") + + // 4. PDF 변환 + const fileData = await templateDoc.getFileData() + const pdfBuffer = await Core.officeToPDFBuffer(fileData, { extension: 'docx' }) + + console.log(`✅ PDF 변환 완료: ${template.templateName}`, `크기: ${pdfBuffer.byteLength} bytes`) + + // 5. 기본계약 생성 요청 + const { error: contractError } = await requestBasicContractInfo({ + vendorIds: vendors.map((v) => v.id), + requestedBy: Number(session!.user.id), + templateId: template.id, + pdfBuffer: new Uint8Array(pdfBuffer), + }) + + if (contractError) { + throw new Error(contractError) + } + + console.log(`✅ 기본계약 생성 완료: ${template.templateName}`) + + } finally { + // 임시 WebViewer 정리 + instance.UI.dispose() + document.body.removeChild(tempDiv) + } + + } catch (error) { + console.error(`❌ 템플릿 처리 실패: ${template.templateName}`, error) + throw error + } + } + + // GTC 템플릿 처리 함수 + const processGtcTemplates = async (gtcTemplates: BasicContractTemplate[], vendors: any[]) => { + if (!session?.user?.id) { + toast.error("인증 정보가 없습니다") + return + } + + try { + const vendorIds = vendors.map(v => v.id) + const userId = Number(session.user.id) + + for (const template of gtcTemplates) { + const templateName = template.templateName?.toLowerCase() || '' + + if (templateName.includes('general gtc') || (templateName.includes('gtc') && !templateName.includes(' '))) { + // General GTC 처리 + console.log(`📄 General GTC 템플릿 처리: ${template.templateName}`) + + const gtcDocument = await getStandardGtcDocumentId() + if (!gtcDocument) { + toast.error(`표준 GTC 문서를 찾을 수 없습니다.`) + continue + } + + const result = await createGtcVendorDocuments({ + baseDocumentId: gtcDocument.id, + vendorIds, + createdById: userId, + documentTitle: gtcDocument.title + }) + + if (result.success) { + console.log(`✅ General GTC 문서 생성 완료: ${result.count}개`) + } else { + toast.error(`General GTC 문서 생성 실패: ${result.error}`) + } + + } else if (templateName.includes('gtc') && templateName.includes(' ')) { + // 프로젝트 GTC 처리 (프로젝트 코드 추출) + const projectCodeMatch = template.templateName?.match(/^([A-Z0-9]+)\s+GTC/) + console.log("🔄 프로젝트 GTC 템플릿 처리: ", template.templateName, projectCodeMatch) + console.log(` - 템플릿 이름 분석: "${template.templateName}"`) + console.log(` - 소문자 변환: "${templateName}"`) + if (projectCodeMatch) { + const projectCode = projectCodeMatch[1] + console.log(`📄 프로젝트 GTC 템플릿 처리: ${template.templateName} (프로젝트: ${projectCode})`) + + const result = await createProjectGtcVendorDocuments({ + projectCode, + vendorIds, + createdById: userId, + documentTitle: template.templateName + }) + + if (result.success) { + console.log(`✅ 프로젝트 GTC 문서 생성 완료: ${result.count}개`) + } else { + toast.error(`프로젝트 GTC 문서 생성 실패: ${result.error}`) + } + } else { + toast.error(`프로젝트 GTC 템플릿 이름 형식이 올바르지 않습니다: ${template.templateName}`) + } + } else { + console.log(`⚠️ GTC 템플릿이지만 처리할 수 없는 형식: ${template.templateName}`) + } + } + + toast.success(`GTC 문서가 ${gtcTemplates.length}개 템플릿에 대해 생성되었습니다`) + + } catch (error) { + console.error('GTC 템플릿 처리 중 오류:', error) + toast.error(`GTC 템플릿 처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) + } + } + + const dialogContent = ( + <div className="space-y-4 py-2"> + {/* 선택된 협력업체 정보 */} + <div className="space-y-2"> + <Label>선택된 협력업체 ({vendors.length}개)</Label> + <div className="max-h-40 overflow-y-auto border rounded-md p-3 space-y-2"> + {vendors.map((vendor) => ( + <div key={vendor.id} className="flex items-center justify-between text-sm"> + <div className="flex-1"> + <div className="font-medium">{vendor.vendorName}</div> + <div className="text-muted-foreground"> + {vendor.vendorCode} • {vendor.email || "이메일 없음"} + </div> + </div> + </div> + ))} + </div> + </div> + + {/* 기본계약서 템플릿 선택 (다중 선택) */} + <div className="space-y-2"> + <Label>기본계약서 템플릿 (복수 선택 가능)</Label> + {isLoadingTemplates ? ( + <div className="text-sm text-muted-foreground">템플릿 로딩 중...</div> + ) : ( + <div className="space-y-2 max-h-40 overflow-y-auto border rounded-md p-3"> + {basicContractTemplates.map((template) => ( + <div key={template.id} className="flex items-center gap-2"> + <Checkbox + id={`template-${template.id}`} + checked={selectedTemplateIds.includes(template.id)} + onCheckedChange={(checked) => { + if (checked) { + setSelectedTemplateIds(prev => [...prev, template.id]) + } else { + setSelectedTemplateIds(prev => prev.filter(id => id !== template.id)) + } + }} + /> + <Label htmlFor={`template-${template.id}`} className="text-sm"> + {template.templateName} + </Label> + </div> + ))} + {basicContractTemplates.length === 0 && ( + <div className="text-sm text-muted-foreground">사용 가능한 템플릿이 없습니다.</div> + )} + </div> + )} + {selectedTemplateIds.length > 0 && ( + <div className="text-xs text-muted-foreground"> + {selectedTemplateIds.length}개 템플릿이 선택되었습니다. + {vendors.length > 0 && vendors.every(v => v.country !== 'KR') && + " (외자 벤더 - 자동 선택됨)"} + {vendors.length > 0 && vendors.every(v => v.country === 'KR') && + " (내자 벤더 - 자동 선택됨)"} + </div> + )} + </div> + + {/* 비밀유지 계약서 첨부파일 */} + {isNdaTemplateSelected() && ( + <div className="space-y-2"> + <Label>비밀유지 계약서 첨부파일</Label> + + {/* 선택된 파일들 표시 */} + {ndaAttachments.length > 0 && ( + <div className="space-y-2"> + <div className="text-sm text-muted-foreground"> + 선택된 파일 ({ndaAttachments.length}개) + </div> + <div className="space-y-1 max-h-32 overflow-y-auto border rounded-md p-2"> + {ndaAttachments.map((file, index) => ( + <div key={index} className="flex items-center justify-between text-sm bg-muted/50 rounded px-2 py-1"> + <div className="flex-1 truncate"> + <span className="font-medium">{file.name}</span> + <span className="text-muted-foreground ml-2"> + ({(file.size / 1024 / 1024).toFixed(2)} MB) + </span> + </div> + <Button + type="button" + variant="ghost" + size="sm" + className="h-6 w-6 p-0 hover:bg-destructive hover:text-destructive-foreground" + onClick={() => handleRemoveNdaAttachment(index)} + > + <X className="h-3 w-3" /> + </Button> + </div> + ))} + </div> + </div> + )} + + {/* 파일 선택 버튼 */} + <div className="flex items-center gap-2"> + <input + type="file" + multiple + accept=".pdf,.doc,.docx,.xlsx,.xls,.png,.jpg,.jpeg" + onChange={handleAddNdaAttachment} + className="hidden" + id="nda-file-input" + /> + <Button + type="button" + variant="outline" + size="sm" + className="gap-2" + onClick={() => document.getElementById('nda-file-input')?.click()} + disabled={isUploadingNdaFiles} + > + <Plus className="h-4 w-4" /> + 파일 추가 + </Button> + {isUploadingNdaFiles && ( + <div className="text-sm text-muted-foreground"> + 파일 업로드 중... + </div> + )} + </div> + + <div className="text-xs text-muted-foreground"> + 비밀유지 계약서와 관련된 첨부파일을 업로드하세요. + 각 벤더별로 동일한 파일이 저장됩니다. + </div> + </div> + )} + </div> + ) + + if (isDesktop) { + return ( + <Dialog {...props}> + {showTrigger && ( + <DialogTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <FileText className="size-4" /> {triggerLabel} ({vendors.length}) + </Button> + </DialogTrigger> + )} + <DialogContent className="max-h-[80vh] flex flex-col"> + <DialogHeader> + <DialogTitle>{title}</DialogTitle> + <DialogDescription> + <span className="font-medium">{vendors.length}</span> + {vendors.length === 1 ? "개 협력업체" : "개 협력업체들"}에게 {description} + </DialogDescription> + </DialogHeader> + <div className="flex-1 overflow-y-auto"> + {dialogContent} + </div> + <DialogFooter className="flex-col gap-4"> + {/* 프로그레스 바 */} + {(showProgress || isProcessing) && ( + <div className="w-full space-y-2"> + <div className="flex items-center justify-between text-sm"> + <span className="text-muted-foreground">{currentStep || "처리 중..."}</span> + <span className="font-medium">{Math.round(progressValue)}%</span> + </div> + <Progress value={progressValue} className="w-full" /> + </div> + )} + + {/* 버튼들 */} + <div className="flex justify-end gap-2"> + <DialogClose asChild> + <Button variant="outline" disabled={isProcessing}>취소</Button> + </DialogClose> + <Button + onClick={onSendContracts} + disabled={isProcessing || selectedTemplateIds.length === 0} + > + {isProcessing && <Loader className="mr-2 size-4 animate-spin" />} + 발송하기 + </Button> + </div> + </DialogFooter> + </DialogContent> + </Dialog> + ) + } + + return ( + <Drawer {...props}> + {showTrigger && ( + <DrawerTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <FileText className="size-4" /> {triggerLabel} ({vendors.length}) + </Button> + </DrawerTrigger> + )} + <DrawerContent className="max-h-[80vh] flex flex-col"> + <DrawerHeader> + <DrawerTitle>{title}</DrawerTitle> + <DrawerDescription> + <span className="font-medium">{vendors.length}</span> + {vendors.length === 1 ? "개 협력업체" : "개 협력업체들"}에게 {description} + </DrawerDescription> + </DrawerHeader> + <div className="flex-1 overflow-y-auto px-4"> + {dialogContent} + </div> + <DrawerFooter className="gap-4"> + {/* 프로그레스 바 */} + {(showProgress || isProcessing) && ( + <div className="w-full space-y-2"> + <div className="flex items-center justify-between text-sm"> + <span className="text-muted-foreground">{currentStep || "처리 중..."}</span> + <span className="font-medium">{Math.round(progressValue)}%</span> + </div> + <Progress value={progressValue} className="w-full" /> + </div> + )} + + {/* 버튼들 */} + <div className="flex gap-2"> + <DrawerClose asChild> + <Button variant="outline" disabled={isProcessing} className="flex-1">취소</Button> + </DrawerClose> + <Button + onClick={onSendContracts} + disabled={isProcessing || selectedTemplateIds.length === 0} + className="flex-1" + > + {isProcessing && <Loader className="mr-2 size-4 animate-spin" />} + 발송하기 + </Button> + </div> + </DrawerFooter> + </DrawerContent> + </Drawer> + ) +} |
