summaryrefslogtreecommitdiff
path: root/lib/rfq-last/vendor
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-29 11:33:37 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-29 11:33:37 +0000
commit8438c05efc7a141e349c5d6416ad08156b4c0775 (patch)
treed90080c294140db8082d0861c649845ec36c4cea /lib/rfq-last/vendor
parentc17b495c700dcfa040abc93a210727cbe72785f1 (diff)
(최겸) 구매 견적 이메일 추가, 미리보기, 첨부삭제, 기타 수정 등
Diffstat (limited to 'lib/rfq-last/vendor')
-rw-r--r--lib/rfq-last/vendor/batch-update-conditions-dialog.tsx2
-rw-r--r--lib/rfq-last/vendor/rfq-vendor-table.tsx2
-rw-r--r--lib/rfq-last/vendor/send-rfq-dialog.tsx384
-rw-r--r--lib/rfq-last/vendor/vendor-detail-dialog.tsx2
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>