From a8674e6b91fb4d356c311fad0251878de154da53 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 24 Nov 2025 11:16:32 +0000 Subject: (최겸) 구매 입찰 수정(폐찰, 낙찰 결재 기능 추가 등) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/detail-table/rfq-detail-table.tsx | 10 +- .../tech-sales-rfq-attachments-sheet-copy-1118.tsx | 710 --------------------- 2 files changed, 8 insertions(+), 712 deletions(-) delete mode 100644 lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet-copy-1118.tsx (limited to 'lib/techsales-rfq') diff --git a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx index 28b281f4..72f03dc3 100644 --- a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx +++ b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx @@ -287,7 +287,13 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps const result = await sendTechSalesRfqToVendors({ rfqId: selectedRfqId, vendorIds: vendorIds as number[], - selectedContacts: selectedContacts + selectedContacts: selectedContacts, + currentUser: { + id: Number(session.data.user.id), + epId: session.data.user.epId || null, + name: session.data.user.name || undefined, + email: session.data.user.email || undefined, + }, }); if (result.success) { @@ -308,7 +314,7 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps } finally { setIsSendingRfq(false); } - }, [selectedRfqId, selectedRows, handleRefreshData]); + }, [selectedRfqId, selectedRows, handleRefreshData, session.data?.user]); // 벤더 선택 핸들러 추가 const [isAcceptingVendors, setIsAcceptingVendors] = useState(false); diff --git a/lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet-copy-1118.tsx b/lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet-copy-1118.tsx deleted file mode 100644 index 82f83b7c..00000000 --- a/lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet-copy-1118.tsx +++ /dev/null @@ -1,710 +0,0 @@ -"use client" - -import * as React from "react" -import { z } from "zod" -import { useForm, useFieldArray } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetDescription, - SheetFooter, - SheetClose, -} from "@/components/ui/sheet" -import { Button } from "@/components/ui/button" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, - FormDescription -} from "@/components/ui/form" -import { Loader, Download, X, Eye, AlertCircle } from "lucide-react" -import { toast } from "sonner" -import { Badge } from "@/components/ui/badge" - -import { - Dropzone, - DropzoneDescription, - DropzoneInput, - DropzoneTitle, - DropzoneUploadIcon, - DropzoneZone, -} from "@/components/ui/dropzone" -import { - FileList, - FileListAction, - FileListDescription, - FileListHeader, - FileListIcon, - FileListInfo, - FileListItem, - FileListName, -} from "@/components/ui/file-list" - -import prettyBytes from "pretty-bytes" -import { formatDate } from "@/lib/utils" -import { processTechSalesRfqAttachments } from "@/lib/techsales-rfq/service" -import { useSession } from "next-auth/react" -import { ApprovalPreviewDialog } from "@/lib/approval/client" -import { ApplicationReasonDialog } from "@/lib/rfq-last/vendor/application-reason-dialog" -import { requestRfqResendWithDrmApproval } from "@/lib/techsales-rfq/approval-actions" -import { mapTechSalesRfqSendToTemplateVariables } from "@/lib/techsales-rfq/approval-handlers" - -const MAX_FILE_SIZE = 6e8 // 600MB - -/** 기존 첨부 파일 정보 (techSalesAttachments 테이블 구조) */ -export interface ExistingTechSalesAttachment { - id: number - techSalesRfqId: number - fileName: string - originalFileName: string - filePath: string - fileSize?: number - fileType?: string - attachmentType: "RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT" - description?: string - createdBy: number - createdAt: Date -} - -/** 새로 업로드할 파일 */ -const newUploadSchema = z.object({ - fileObj: z.any().optional(), // 실제 File - attachmentType: z.enum(["RFQ_COMMON", "TBE_RESULT", "CBE_RESULT"]).default("RFQ_COMMON"), - description: z.string().optional(), -}) - -/** 기존 첨부 (react-hook-form에서 관리) */ -const existingAttachSchema = z.object({ - id: z.number(), - techSalesRfqId: z.number(), - fileName: z.string(), - originalFileName: z.string(), - filePath: z.string(), - fileSize: z.number().optional(), - fileType: z.string().optional(), - attachmentType: z.enum(["RFQ_COMMON", "TBE_RESULT", "CBE_RESULT"]), - description: z.string().optional(), - createdBy: z.number(), - createdAt: z.custom(), -}) - -/** RHF 폼 전체 스키마 */ -const attachmentsFormSchema = z.object({ - techSalesRfqId: z.number().int(), - existing: z.array(existingAttachSchema), - newUploads: z.array(newUploadSchema), -}) - -type AttachmentsFormValues = z.infer - -// TechSalesRfq 타입 (간단 버전) -interface TechSalesRfq { - id: number - rfqCode: string | null - status: string - // 필요한 다른 필드들... -} - -interface TechSalesRfqAttachmentsSheetProps - extends React.ComponentPropsWithRef { - defaultAttachments?: ExistingTechSalesAttachment[] - rfq: TechSalesRfq | null - /** 첨부파일 타입 */ - attachmentType?: "RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT" - /** 읽기 전용 모드 (벤더용) */ - readOnly?: boolean - /** 업로드/삭제 후 상위 테이블에 attachmentCount 등을 업데이트하기 위한 콜백 */ - // onAttachmentsUpdated?: (rfqId: number, newAttachmentCount: number) => void - -} - -export function TechSalesRfqAttachmentsSheet({ - defaultAttachments = [], - // onAttachmentsUpdated, - rfq, - attachmentType = "RFQ_COMMON", - readOnly = false, - ...props -}: TechSalesRfqAttachmentsSheetProps) { - const [isPending, setIsPending] = React.useState(false) - const session = useSession() - - // 재발송 결재 관련 상태 - const [showResendApprovalDialog, setShowResendApprovalDialog] = React.useState(false) - const [showApplicationReasonDialog, setShowApplicationReasonDialog] = React.useState(false) - const [resendApprovalData, setResendApprovalData] = React.useState<{ - rfqId: number - drmFiles: Array<{ - file: File - attachmentType: string - description?: string - }> - } | null>(null) - const [approvalPreviewData, setApprovalPreviewData] = React.useState<{ - templateVariables: Record - applicationReason: string - } | null>(null) - - // 파일 다운로드 핸들러 - const handleDownloadClick = React.useCallback(async (filePath: string, fileName: string) => { - try { - const { downloadFile } = await import('@/lib/file-download') - await downloadFile(filePath, fileName, { - showToast: true, - onError: (error) => { - console.error('다운로드 오류:', error) - toast.error(error) - }, - onSuccess: (fileName, fileSize) => { - console.log(`다운로드 성공: ${fileName} (${fileSize} bytes)`) - } - }) - } catch (error) { - console.error('다운로드 오류:', error) - toast.error('파일 다운로드 중 오류가 발생했습니다.') - } - }, []) - // 첨부파일 타입별 제목과 설명 설정 - const attachmentConfig = React.useMemo(() => { - switch (attachmentType) { - case "TBE_RESULT": - return { - title: "TBE 결과 첨부파일", - description: "기술 평가(TBE) 결과 파일을 관리합니다.", - fileTypeLabel: "TBE 결과", - canEdit: !readOnly - } - case "CBE_RESULT": - return { - title: "CBE 결과 첨부파일", - description: "상업성 평가(CBE) 결과 파일을 관리합니다.", - fileTypeLabel: "CBE 결과", - canEdit: !readOnly - } - default: // RFQ_COMMON - return { - title: "RFQ 첨부파일", - description: readOnly ? "RFQ 공통 첨부파일을 조회합니다." : "RFQ 공통 첨부파일을 관리합니다.", - fileTypeLabel: "공통", - canEdit: !readOnly - } - } - }, [attachmentType, readOnly]) - - // // RFQ 상태에 따른 편집 가능 여부 결정 (readOnly prop이 true면 항상 false) - // const isEditable = React.useMemo(() => { - // if (!rfq) return false - // return attachmentConfig.canEdit - // }, [rfq, attachmentConfig.canEdit]) - - const form = useForm({ - resolver: zodResolver(attachmentsFormSchema), - defaultValues: { - techSalesRfqId: rfq?.id || 0, - existing: defaultAttachments.map(att => ({ - id: att.id, - techSalesRfqId: att.techSalesRfqId, - fileName: att.fileName, - originalFileName: att.originalFileName, - filePath: att.filePath, - fileSize: att.fileSize || undefined, - fileType: att.fileType || undefined, - attachmentType: att.attachmentType, - description: att.description || undefined, - createdBy: att.createdBy, - createdAt: att.createdAt, - })), - newUploads: [], - }, - }) - - // useFieldArray for existing and new uploads - const { - fields: existingFields, - remove: removeExisting, - } = useFieldArray({ - control: form.control, - name: "existing", - }) - - const { - fields: newUploadFields, - append: appendNewUpload, - remove: removeNewUpload, - } = useFieldArray({ - control: form.control, - name: "newUploads", - }) - - // Reset form when defaultAttachments changes - React.useEffect(() => { - if (defaultAttachments) { - form.reset({ - techSalesRfqId: rfq?.id || 0, - existing: defaultAttachments.map(att => ({ - id: att.id, - techSalesRfqId: att.techSalesRfqId, - fileName: att.fileName, - originalFileName: att.originalFileName, - filePath: att.filePath, - fileSize: att.fileSize || undefined, - fileType: att.fileType || undefined, - attachmentType: att.attachmentType, - description: att.description || undefined, - createdBy: att.createdBy, - createdAt: att.createdAt, - })), - newUploads: [], - }) - } - }, [defaultAttachments, rfq?.id, form]) - - // Handle dropzone accept - const handleDropAccepted = React.useCallback((acceptedFiles: File[]) => { - acceptedFiles.forEach((file) => { - appendNewUpload({ - fileObj: file, - attachmentType: "RFQ_COMMON", - description: "", - }) - }) - }, [appendNewUpload]) - - // Handle dropzone reject - const handleDropRejected = React.useCallback(() => { - toast.error("파일 크기가 너무 크거나 지원하지 않는 파일 형식입니다.") - }, []) - - // Handle remove existing attachment - const handleRemoveExisting = React.useCallback((index: number) => { - removeExisting(index) - }, [removeExisting]) - - // Handle form submission - const onSubmit = async (data: AttachmentsFormValues) => { - if (!rfq) { - toast.error("RFQ 정보를 찾을 수 없습니다.") - return - } - - setIsPending(true) - try { - // 삭제할 첨부파일 ID 수집 - const deleteAttachmentIds = defaultAttachments - .filter((original) => !data.existing.find(existing => existing.id === original.id)) - .map(attachment => attachment.id) - - // 새 파일 정보 수집 - const newFiles = data.newUploads - .filter(upload => upload.fileObj) - .map(upload => ({ - file: upload.fileObj as File, - attachmentType: attachmentType, - description: upload.description, - })) - - // 실제 API 호출 - const result = await processTechSalesRfqAttachments({ - techSalesRfqId: rfq.id, - newFiles, - deleteAttachmentIds, - createdBy: parseInt(session.data?.user.id || "0"), - }) - - if (result.error) { - // DRM 파일 추가로 인한 재발송 결재 필요 - if (result.error === "DRM_FILE_ADDED_TO_SENT_RFQ") { - // DRM 파일만 필터링 - const drmFiles = newFiles.filter((_, index) => { - // DRM 파일 검출은 서버에서 이미 완료되었으므로, 업로드된 파일 중 DRM 파일만 추출 - // 실제로는 서버에서 반환된 정보를 사용해야 하지만, 여기서는 업로드된 파일을 그대로 사용 - return true // 임시로 모든 새 파일을 DRM 파일로 간주 (실제로는 서버에서 필터링 필요) - }) - - setResendApprovalData({ - rfqId: rfq.id, - drmFiles: newFiles, // 모든 새 파일을 DRM 파일로 간주 - }) - setShowApplicationReasonDialog(true) - setIsPending(false) - return - } else { - toast.error(result.error) - return - } - } - - // 성공 메시지 표시 (업로드된 파일 수 포함) - const uploadedCount = newFiles.length - const deletedCount = deleteAttachmentIds.length - - let successMessage = "첨부파일이 저장되었습니다." - if (uploadedCount > 0 && deletedCount > 0) { - successMessage = `${uploadedCount}개 파일 업로드, ${deletedCount}개 파일 삭제 완료` - } else if (uploadedCount > 0) { - successMessage = `${uploadedCount}개 파일이 업로드되었습니다.` - } else if (deletedCount > 0) { - successMessage = `${deletedCount}개 파일이 삭제되었습니다.` - } - - toast.success(successMessage) - - // 다이얼로그 자동 닫기 - props.onOpenChange?.(false) - - // // 즉시 첨부파일 목록 새로고침 - // const refreshResult = await getTechSalesRfqAttachments(rfq.id) - // if (refreshResult.error) { - // console.error("첨부파일 목록 새로고침 실패:", refreshResult.error) - // toast.warning("첨부파일 목록 새로고침에 실패했습니다. 시트를 다시 열어주세요.") - // } else { - // // 새로운 첨부파일 목록으로 폼 업데이트 - // const refreshedAttachments = refreshResult.data.map(att => ({ - // id: att.id, - // techSalesRfqId: att.techSalesRfqId || rfq.id, - // fileName: att.fileName, - // originalFileName: att.originalFileName, - // filePath: att.filePath, - // fileSize: att.fileSize, - // fileType: att.fileType, - // attachmentType: att.attachmentType as "RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT", - // description: att.description, - // createdBy: att.createdBy, - // createdAt: att.createdAt, - // })) - - // // 폼을 새로운 데이터로 리셋 (새 업로드 목록은 비움) - // form.reset({ - // techSalesRfqId: rfq.id, - // existing: refreshedAttachments.map(att => ({ - // ...att, - // fileSize: att.fileSize || undefined, - // fileType: att.fileType || undefined, - // description: att.description || undefined, - // })), - // newUploads: [], - // }) - - // // 즉시 UI 업데이트를 위한 추가 피드백 - // if (uploadedCount > 0) { - // toast.success("첨부파일 목록이 업데이트되었습니다.", { duration: 2000 }) - // } - // } - - // // 콜백으로 상위 컴포넌트에 변경사항 알림 - // const newAttachmentCount = refreshResult.error ? - // (data.existing.length + newFiles.length - deleteAttachmentIds.length) : - // refreshResult.data.length - // onAttachmentsUpdated?.(rfq.id, newAttachmentCount) - - } catch (error) { - console.error("첨부파일 저장 오류:", error) - toast.error("첨부파일 저장 중 오류가 발생했습니다.") - } finally { - setIsPending(false) - } - } - - // 신청사유 입력 완료 핸들러 - const handleApplicationReasonConfirm = React.useCallback(async (reason: string) => { - if (!resendApprovalData) { - toast.error("결재 데이터가 없습니다.") - return - } - - try { - // 템플릿 변수 생성 (신청사유 포함) - const templateVariables = await mapTechSalesRfqSendToTemplateVariables({ - attachments: resendApprovalData.drmFiles.map(f => ({ - fileName: f.file.name, - fileSize: f.file.size, - })), - vendorNames: [], // 기존 벤더 목록은 후처리에서 조회 - applicationReason: reason, - }) - - // 결재 미리보기 데이터 업데이트 - setApprovalPreviewData({ - templateVariables, - applicationReason: reason, - }) - - // 신청사유 다이얼로그 닫고 결재 미리보기 열기 - setShowApplicationReasonDialog(false) - setShowResendApprovalDialog(true) - } catch (error) { - console.error("템플릿 변수 생성 실패:", error) - toast.error("결재 문서 생성에 실패했습니다.") - } - }, [resendApprovalData]) - - // 결재 미리보기 확인 핸들러 - const handleApprovalConfirm = React.useCallback(async (approvalData: { - approvers: string[] - title: string - description?: string - }) => { - if (!resendApprovalData || !approvalPreviewData || !session?.data?.user) { - toast.error("결재 데이터가 없습니다.") - return - } - - try { - const result = await requestRfqResendWithDrmApproval({ - rfqId: resendApprovalData.rfqId, - rfqCode: rfq?.rfqCode || undefined, - drmFiles: resendApprovalData.drmFiles, - applicationReason: approvalPreviewData.applicationReason, - currentUser: { - id: Number(session.data.user.id), - epId: session.data.user.epId || null, - name: session.data.user.name || undefined, - email: session.data.user.email || undefined, - }, - approvers: approvalData.approvers, - }) - - if (result.success) { - toast.success(result.message) - setShowResendApprovalDialog(false) - setResendApprovalData(null) - setApprovalPreviewData(null) - props.onOpenChange?.(false) - } - } catch (error) { - console.error("재발송 결재 상신 실패:", error) - toast.error(error instanceof Error ? error.message : "재발송 결재 상신에 실패했습니다.") - } - }, [resendApprovalData, approvalPreviewData, session, rfq, props]) - - return ( - - - - {attachmentConfig.title} - -
RFQ: {rfq?.rfqCode || "N/A"}
-
{attachmentConfig.description}
- {!attachmentConfig.canEdit && ( -
- - 현재 상태에서는 편집할 수 없습니다 -
- )} -
-
- -
- - {/* 1) Existing attachments */} -
-
- 기존 첨부파일 ({existingFields.length}개) -
- {existingFields.map((field, index) => { - const typeLabel = attachmentConfig.fileTypeLabel - const sizeText = field.fileSize ? prettyBytes(field.fileSize) : "알 수 없음" - const dateText = field.createdAt ? formatDate(field.createdAt, "KR") : "" - - return ( -
-
-
-

- {field.originalFileName || field.fileName} -

- - {typeLabel} - -
-

- {sizeText} • {dateText} -

- {field.description && ( -

- {field.description} -

- )} -
- -
- {/* Download button */} - {field.filePath && ( - - )} - {/* Remove button - 편집 가능할 때만 표시 */} - {attachmentConfig.canEdit && ( - - )} -
-
- ) - })} -
- - {/* 2) Dropzone for new uploads - 편집 가능할 때만 표시 */} - {attachmentConfig.canEdit ? ( - <> - - {({ maxSize }) => ( - ( - - 새 파일 업로드 - - - - -
- -
- 파일을 드래그하거나 클릭하세요 - - 최대 크기: {maxSize ? prettyBytes(maxSize) : "600MB"} - -
-
-
- 파일을 여러 개 선택할 수 있습니다. - -
- )} - /> - )} -
- - {/* newUpload fields -> FileList */} - {newUploadFields.length > 0 && ( -
-
- 새 파일 ({newUploadFields.length}개) -
- - {newUploadFields.map((field, idx) => { - const fileObj = form.getValues(`newUploads.${idx}.fileObj`) - if (!fileObj) return null - - const fileName = fileObj.name - const fileSize = fileObj.size - return ( - - - - - {fileName} - - {prettyBytes(fileSize)} - - - removeNewUpload(idx)}> - - 제거 - - - - - ) - })} - -
- )} - - ) : ( -
-
- -

보기 모드에서는 파일 첨부를 할 수 없습니다.

-
-
- )} - - - - - - {attachmentConfig.canEdit && ( - - )} - -
- -
- - {/* 신청사유 입력 다이얼로그 */} - {resendApprovalData && ( - - )} - - {/* 결재 미리보기 다이얼로그 */} - {resendApprovalData && session?.data?.user?.epId && approvalPreviewData && ( - - )} -
- ) -} \ No newline at end of file -- cgit v1.2.3