diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-29 11:33:37 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-29 11:33:37 +0000 |
| commit | 8438c05efc7a141e349c5d6416ad08156b4c0775 (patch) | |
| tree | d90080c294140db8082d0861c649845ec36c4cea /lib/rfq-last/vendor | |
| parent | c17b495c700dcfa040abc93a210727cbe72785f1 (diff) | |
(최겸) 구매 견적 이메일 추가, 미리보기, 첨부삭제, 기타 수정 등
Diffstat (limited to 'lib/rfq-last/vendor')
| -rw-r--r-- | lib/rfq-last/vendor/batch-update-conditions-dialog.tsx | 2 | ||||
| -rw-r--r-- | lib/rfq-last/vendor/rfq-vendor-table.tsx | 2 | ||||
| -rw-r--r-- | lib/rfq-last/vendor/send-rfq-dialog.tsx | 384 | ||||
| -rw-r--r-- | lib/rfq-last/vendor/vendor-detail-dialog.tsx | 2 |
4 files changed, 346 insertions, 44 deletions
diff --git a/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx b/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx index ff3e27cc..7eae48db 100644 --- a/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx +++ b/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx @@ -436,7 +436,7 @@ export function BatchUpdateConditionsDialog({ className="w-full justify-between" disabled={!fieldsToUpdate.currency} > - <span className="text-muted-foreground"> + <span className="truncate"> {field.value || "통화 선택"} </span> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> diff --git a/lib/rfq-last/vendor/rfq-vendor-table.tsx b/lib/rfq-last/vendor/rfq-vendor-table.tsx index ef906ed6..89a42602 100644 --- a/lib/rfq-last/vendor/rfq-vendor-table.tsx +++ b/lib/rfq-last/vendor/rfq-vendor-table.tsx @@ -451,6 +451,7 @@ export function RfqVendorTable({ buffer: number[]; fileName: string; }>; + hasToSendEmail?: boolean; }) => { try { // 서버 액션 호출 @@ -461,6 +462,7 @@ export function RfqVendorTable({ attachmentIds: data.attachments, message: data.message, generatedPdfs: data.generatedPdfs, + hasToSendEmail: data.hasToSendEmail, }); // 성공 후 처리 diff --git a/lib/rfq-last/vendor/send-rfq-dialog.tsx b/lib/rfq-last/vendor/send-rfq-dialog.tsx index ed43d87f..e63086ad 100644 --- a/lib/rfq-last/vendor/send-rfq-dialog.tsx +++ b/lib/rfq-last/vendor/send-rfq-dialog.tsx @@ -86,7 +86,14 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; import { Progress } from "@/components/ui/progress"; +import { getRfqEmailTemplate } from "../service"; interface ContractToGenerate { vendorId: number; @@ -164,6 +171,46 @@ interface RfqInfo { quotationType?: string; evaluationApply?: boolean; contractType?: string; + + // 추가 필드들 (HTML 템플릿에서 사용되는 변수들) + customerName?: string; + customerCode?: string; + shipType?: string; + shipClass?: string; + shipCount?: number; + projectFlag?: string; + flag?: string; + contractStartDate?: string; + contractEndDate?: string; + scDate?: string; + dlDate?: string; + itemCode?: string; + itemName?: string; + itemCount?: number; + prNumber?: string; + prIssueDate?: string; + warrantyDescription?: string; + repairDescription?: string; + totalWarrantyDescription?: string; + requiredDocuments?: string[]; + contractRequirements?: { + hasNda: boolean; + ndaDescription: string; + hasGeneralGtc: boolean; + generalGtcDescription: string; + hasProjectGtc: boolean; + projectGtcDescription: string; + hasAgreement: boolean; + agreementDescription: string; + }; + vendorCountry?: string; + formattedDueDate?: string; + systemName?: string; + hasAttachments?: boolean; + attachmentsCount?: number; + language?: string; + companyName?: string; + now?: Date; } interface VendorWithRecipients extends Vendor { @@ -202,7 +249,14 @@ interface SendRfqDialogProps { attachments: number[]; message?: string; generatedPdfs?: Array<{ key: string; buffer: number[]; fileName: string }>; - }) => Promise<void>; + hasToSendEmail?: boolean; + }) => Promise<{ + success: boolean; + message: string; + sentCount?: number; + failedCount?: number; + error?: string; + }>; } // 이메일 유효성 검사 함수 @@ -252,6 +306,13 @@ export function SendRfqDialog({ // 재전송 시 기본계약 스킵 옵션 - 업체별 관리 const [skipContractsForVendor, setSkipContractsForVendor] = React.useState<Record<number, boolean>>({}); + // 이메일 템플릿 관련 상태 + const [activeTab, setActiveTab] = React.useState<"recipients" | "template">("recipients"); + const [selectedTemplateSlug, setSelectedTemplateSlug] = React.useState<string>(""); + const [templatePreview, setTemplatePreview] = React.useState<{ subject: string; content: string } | null>(null); + const [isGeneratingPreview, setIsGeneratingPreview] = React.useState(false); + const [hasToSendEmail, setHasToSendEmail] = React.useState(true); // 이메일 발송 여부 + const generateContractPdf = async ( vendor: VendorWithRecipients, contractType: string, @@ -354,6 +415,130 @@ export function SendRfqDialog({ } }; + // 템플릿 미리보기 생성 + const generateTemplatePreview = React.useCallback(async (templateSlug: string) => { + + try { + setIsGeneratingPreview(true); + const template = await getRfqEmailTemplate(); + templateSlug = template?.slug || ""; + + const response = await fetch('/api/email-template/preview', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + templateSlug, + sampleData: { + // 기본 RFQ 정보 (실제 데이터 사용) + rfqCode: rfqInfo?.rfqCode || '', + rfqTitle: rfqInfo?.rfqTitle || '', + projectCode: rfqInfo?.projectCode, + projectName: rfqInfo?.projectName, + vendorName: "업체명 예시", // 실제로는 선택된 벤더 이름 사용 + picName: rfqInfo?.picName, + picCode: rfqInfo?.picCode, + picTeam: rfqInfo?.picTeam, + dueDate: rfqInfo?.dueDate, + + // 프로젝트 관련 정보 + customerName: rfqInfo?.customerName || (rfqInfo?.projectCode ? `${rfqInfo.projectCode} 고객사` : undefined), + customerCode: rfqInfo?.customerCode || rfqInfo?.projectCode, + shipType: rfqInfo?.shipType || "선종 정보", + shipClass: rfqInfo?.shipClass || "선급 정보", + shipCount: rfqInfo?.shipCount || 1, + projectFlag: rfqInfo?.projectFlag || "KR", + flag: rfqInfo?.flag || "한국", + contractStartDate: rfqInfo?.contractStartDate || new Date().toISOString().split('T')[0], + contractEndDate: rfqInfo?.contractEndDate || new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], + scDate: rfqInfo?.scDate || new Date().toISOString().split('T')[0], + dlDate: rfqInfo?.dlDate || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], + + // 패키지/자재 정보 + packageNo: rfqInfo?.packageNo, + packageName: rfqInfo?.packageName, + materialGroup: rfqInfo?.materialGroup, + materialGroupDesc: rfqInfo?.materialGroupDesc, + + // 품목 정보 + itemCode: rfqInfo?.itemCode || "품목코드", + itemName: rfqInfo?.itemName || "품목명", + itemCount: rfqInfo?.itemCount || 1, + prNumber: rfqInfo?.prNumber || "PR-001", + prIssueDate: rfqInfo?.prIssueDate || new Date().toISOString().split('T')[0], + + // 보증 정보 + warrantyDescription: rfqInfo?.warrantyDescription || "제조사의 표준 보증 조건 적용", + repairDescription: rfqInfo?.repairDescription || "하자 발생 시 무상 수리", + totalWarrantyDescription: rfqInfo?.totalWarrantyDescription || "전체 품목에 대한 보증 적용", + + // 필요 문서 + requiredDocuments: rfqInfo?.requiredDocuments || [ + "상세 견적서", + "납기 계획서", + "품질 보증서", + "기술 사양서" + ], + + // 계약 요구사항 + contractRequirements: rfqInfo?.contractRequirements || { + hasNda: true, + ndaDescription: "NDA (비밀유지계약)", + hasGeneralGtc: true, + generalGtcDescription: "General GTC", + hasProjectGtc: !!rfqInfo?.projectCode, + projectGtcDescription: `Project GTC (${rfqInfo?.projectCode || ''})`, + hasAgreement: false, + agreementDescription: "기술 자료 제공 동의서" + }, + + // 벤더 정보 + vendorCountry: rfqInfo?.vendorCountry || "한국", + + // 시스템 정보 + formattedDueDate: rfqInfo?.formattedDueDate || (rfqInfo?.dueDate ? new Date(rfqInfo.dueDate).toLocaleDateString('ko-KR') : ''), + systemName: rfqInfo?.systemName || "SHI EVCP", + hasAttachments: rfqInfo?.hasAttachments || false, + attachmentsCount: rfqInfo?.attachmentsCount || 0, + + // 언어 설정 + language: rfqInfo?.language || "ko", + + // 회사 정보 (t helper 대체용) + companyName: "삼성중공업", + email: "삼성중공업", + + // 현재 시간 + now: new Date(), + + // 기타 정보 + designPicName: rfqInfo?.designPicName, + designTeam: rfqInfo?.designTeam, + quotationType: rfqInfo?.quotationType, + evaluationApply: rfqInfo?.evaluationApply, + contractType: rfqInfo?.contractType + } + }) + }); + + const data = await response.json(); + + if (data.success) { + setTemplatePreview({ + subject: data.subject || '', + content: data.html || '' + }); + } else { + console.error('미리보기 생성 실패:', data.error); + setTemplatePreview(null); + } + } catch (error) { + console.error('미리보기 생성 실패:', error); + setTemplatePreview(null); + } finally { + setIsGeneratingPreview(false); + } + }, [rfqInfo]); + // 초기화 React.useEffect(() => { if (open && selectedVendors.length > 0) { @@ -595,7 +780,7 @@ export function SendRfqDialog({ setIsGeneratingPdfs(false); setIsSending(true); - await onSend({ + const sendResult = await onSend({ vendors: vendorsWithRecipients.map(v => ({ vendorId: v.vendorId, vendorName: v.vendorName, @@ -623,12 +808,15 @@ export function SendRfqDialog({ key, ...data })), + // 이메일 발송 처리 (사용자 선택에 따라) + hasToSendEmail: hasToSendEmail, }); - toast.success( - `${vendorsWithRecipients.length}개 업체에 RFQ를 발송했습니다.` + - (contractsToGenerate.length > 0 ? ` ${contractsToGenerate.length}개의 기본계약서가 포함되었습니다.` : '') - ); + if (!sendResult.success) { + throw new Error(sendResult.message); + } + + toast.success(sendResult.message); onOpenChange(false); } catch (error) { @@ -641,7 +829,7 @@ export function SendRfqDialog({ setCurrentGeneratingContract(""); setSkipContractsForVendor({}); // 초기화 } - }, [vendorsWithRecipients, selectedAttachments, additionalMessage, onSend, onOpenChange, rfqInfo, skipContractsForVendor]); + }, [vendorsWithRecipients, selectedAttachments, additionalMessage, onSend, onOpenChange, rfqInfo, skipContractsForVendor, hasToSendEmail]); // 전송 처리 const handleSend = async () => { @@ -695,9 +883,15 @@ export function SendRfqDialog({ </DialogDescription> </DialogHeader> - {/* ScrollArea 대신 div 사용 */} - <div className="flex-1 overflow-y-auto px-1" style={{ maxHeight: 'calc(90vh - 200px)' }}> - <div className="space-y-6 pr-4"> + {/* 탭 구조 */} + <Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as "recipients" | "template")} className="flex-1 flex flex-col"> + <TabsList className="grid w-full grid-cols-2"> + <TabsTrigger value="recipients">수신자 설정</TabsTrigger> + <TabsTrigger value="template">이메일 템플릿</TabsTrigger> + </TabsList> + + <div className="flex-1 overflow-y-auto px-1 mt-4" style={{ maxHeight: 'calc(90vh - 240px)' }}> + <TabsContent value="recipients" className="mt-0 space-y-6 pr-4"> {/* 재발송 경고 메시지 - 재발송 업체가 있을 때만 표시 */} {vendorsWithRecipients.some(v => v.sendVersion && v.sendVersion > 0) && ( <Alert className="border-yellow-500 bg-yellow-50"> @@ -1290,44 +1484,152 @@ export function SendRfqDialog({ <Separator /> - {/* 추가 메시지 */} - <div className="space-y-2"> - <Label htmlFor="message" className="text-sm font-medium"> - 추가 메시지 (선택사항) - </Label> - <textarea - id="message" - className="w-full min-h-[80px] p-3 text-sm border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary" - placeholder="업체에 전달할 추가 메시지를 입력하세요..." - value={additionalMessage} - onChange={(e) => setAdditionalMessage(e.target.value)} - /> - </div> + {/* 추가 메시지 */} + <div className="space-y-2"> + <Label htmlFor="message" className="text-sm font-medium"> + 추가 메시지 (선택사항) + </Label> + <textarea + id="message" + className="w-full min-h-[80px] p-3 text-sm border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary" + placeholder="업체에 전달할 추가 메시지를 입력하세요..." + value={additionalMessage} + onChange={(e) => setAdditionalMessage(e.target.value)} + /> + </div> - {/* PDF 생성 진행 상황 표시 */} - {isGeneratingPdfs && ( - <Alert className="border-blue-500 bg-blue-50"> - <div className="space-y-3"> + {/* PDF 생성 진행 상황 표시 */} + {isGeneratingPdfs && ( + <Alert className="border-blue-500 bg-blue-50"> + <div className="space-y-3"> + <div className="flex items-center gap-2"> + <RefreshCw className="h-4 w-4 animate-spin text-blue-600" /> + <AlertTitle className="text-blue-800">기본계약서 생성 중</AlertTitle> + </div> + <AlertDescription> + <div className="space-y-2"> + <p className="text-sm text-blue-700">{currentGeneratingContract}</p> + <Progress value={pdfGenerationProgress} className="h-2" /> + <p className="text-xs text-blue-600"> + {Math.round(pdfGenerationProgress)}% 완료 + </p> + </div> + </AlertDescription> + </div> + </Alert> + )} + </TabsContent> + + <TabsContent value="template" className="mt-0 space-y-6 pr-4"> + {/* 이메일 발송 설정 및 미리보기 섹션 */} + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2 text-sm font-medium"> + <Mail className="h-4 w-4" /> + 이메일 발송 설정 + </div> <div className="flex items-center gap-2"> - <RefreshCw className="h-4 w-4 animate-spin text-blue-600" /> - <AlertTitle className="text-blue-800">기본계약서 생성 중</AlertTitle> + <Checkbox + id="hasToSendEmail" + checked={hasToSendEmail} + onCheckedChange={setHasToSendEmail} + /> + <Label htmlFor="hasToSendEmail" className="text-sm"> + 이메일 발송 + </Label> </div> - <AlertDescription> - <div className="space-y-2"> - <p className="text-sm text-blue-700">{currentGeneratingContract}</p> - <Progress value={pdfGenerationProgress} className="h-2" /> - <p className="text-xs text-blue-600"> - {Math.round(pdfGenerationProgress)}% 완료 - </p> - </div> - </AlertDescription> </div> - </Alert> - )} + {/* 이메일 발송 여부에 따른 설명 */} + <Alert className={cn( + "border-2", + hasToSendEmail ? "border-blue-200 bg-blue-50" : "border-gray-200 bg-gray-50" + )}> + <Mail className={cn("h-4 w-4", hasToSendEmail ? "text-blue-600" : "text-gray-600")} /> + <AlertTitle className={cn(hasToSendEmail ? "text-blue-800" : "text-gray-800")}> + {hasToSendEmail ? "이메일 발송 모드" : "RFQ만 발송 모드"} + </AlertTitle> + <AlertDescription className={cn("text-sm", hasToSendEmail ? "text-blue-700" : "text-gray-700")}> + {hasToSendEmail + ? "선택된 이메일 템플릿으로 RFQ와 함께 이메일을 발송합니다." + : "EVCP 시스템에서 RFQ만 발송하고 이메일은 발송하지 않습니다." + } + </AlertDescription> + </Alert> + {/* 이메일 발송 시에만 미리보기 표시 */} + {hasToSendEmail && ( + <div className="space-y-4"> + <div className="space-y-4"> + {/* 미리보기 새로고침 버튼 */} + <div className="flex justify-end"> + <Button + variant="outline" + size="sm" + onClick={() => generateTemplatePreview(selectedTemplateSlug)} + disabled={isGeneratingPreview} + > + {isGeneratingPreview ? ( + <RefreshCw className="h-4 w-4 animate-spin mr-2" /> + ) : ( + '미리보기 새로고침' + )} + </Button> + </div> + + {/* 미리보기 */} + <div className="space-y-2"> + <Label className="text-sm font-medium">이메일 미리보기</Label> + {isGeneratingPreview ? ( + <div className="h-96 border rounded-lg flex items-center justify-center"> + <div className="text-center"> + <RefreshCw className="h-8 w-8 animate-spin mx-auto mb-2 text-blue-500" /> + <p className="text-sm text-muted-foreground">미리보기 생성 중...</p> + </div> + </div> + ) : templatePreview ? ( + <div className="space-y-4"> + {/* 제목 미리보기 */} + <div className="p-3 bg-blue-50 rounded-lg"> + <Label className="text-xs font-medium text-blue-900">제목:</Label> + <p className="font-semibold text-blue-900 break-words">{templatePreview.subject}</p> + </div> + + {/* 본문 미리보기 */} + <div className="border rounded-lg bg-white"> + <iframe + srcDoc={templatePreview.content} + sandbox="allow-same-origin" + className="w-full h-96 border-0 rounded-lg" + title="Template Preview" + /> + </div> + </div> + ) : ( + <div className="h-96 border rounded-lg flex items-center justify-center"> + <div className="text-center"> + <Mail className="h-12 w-12 text-gray-400 mx-auto mb-4" /> + <p className="text-gray-500 mb-2">미리보기를 생성하면 이메일 내용이 표시됩니다</p> + <Button + variant="outline" + size="sm" + onClick={() => generateTemplatePreview(selectedTemplateSlug)} + > + 미리보기 생성 + </Button> + </div> + </div> + )} + </div> + </div> + + </div> + )} + + </div> + </TabsContent> </div> - </div> + </Tabs> <DialogFooter className="flex-shrink-0"> <Alert className="max-w-md"> diff --git a/lib/rfq-last/vendor/vendor-detail-dialog.tsx b/lib/rfq-last/vendor/vendor-detail-dialog.tsx index 17eed54c..074924eb 100644 --- a/lib/rfq-last/vendor/vendor-detail-dialog.tsx +++ b/lib/rfq-last/vendor/vendor-detail-dialog.tsx @@ -586,7 +586,6 @@ export function VendorResponseDetailDialog({ <TableHead className="text-right">단가</TableHead> <TableHead className="text-right">금액</TableHead> <TableHead>납기일</TableHead> - <TableHead>제조사</TableHead> </TableRow> </TableHeader> <TableBody> @@ -608,7 +607,6 @@ export function VendorResponseDetailDialog({ ? format(new Date(item.vendorDeliveryDate), "MM-dd") : "-"} </TableCell> - <TableCell>{item.manufacturer || "-"}</TableCell> </TableRow> ))} </TableBody> |
