summaryrefslogtreecommitdiff
path: root/lib/rfq-last/vendor-response
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-response
parentc17b495c700dcfa040abc93a210727cbe72785f1 (diff)
(최겸) 구매 견적 이메일 추가, 미리보기, 첨부삭제, 기타 수정 등
Diffstat (limited to 'lib/rfq-last/vendor-response')
-rw-r--r--lib/rfq-last/vendor-response/editor/attachments-upload.tsx134
-rw-r--r--lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx50
-rw-r--r--lib/rfq-last/vendor-response/rfq-attachments-dialog.tsx2
-rw-r--r--lib/rfq-last/vendor-response/rfq-items-dialog.tsx46
4 files changed, 196 insertions, 36 deletions
diff --git a/lib/rfq-last/vendor-response/editor/attachments-upload.tsx b/lib/rfq-last/vendor-response/editor/attachments-upload.tsx
index a2967767..ea7bb9c9 100644
--- a/lib/rfq-last/vendor-response/editor/attachments-upload.tsx
+++ b/lib/rfq-last/vendor-response/editor/attachments-upload.tsx
@@ -1,11 +1,20 @@
"use client"
import { useState, useRef } from "react"
+import { useSession } from "next-auth/react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Alert, AlertDescription } from "@/components/ui/alert"
import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import {
Table,
TableBody,
TableCell,
@@ -23,10 +32,13 @@ import {
Paperclip,
FileCheck,
Calculator,
- Wrench
+ Wrench,
+ X
} from "lucide-react"
import { formatBytes } from "@/lib/utils"
import { cn } from "@/lib/utils"
+import { toast } from "sonner"
+import { deleteVendorResponseAttachment } from "../../service"
interface FileWithType extends File {
attachmentType?: "구매" | "설계"
@@ -37,6 +49,9 @@ interface AttachmentsUploadProps {
attachments: FileWithType[]
onAttachmentsChange: (files: FileWithType[]) => void
existingAttachments?: any[]
+ onExistingAttachmentsChange?: (files: any[]) => void
+ responseId?: number
+ userId?: number
}
const acceptedFileTypes = {
@@ -49,13 +64,18 @@ const acceptedFileTypes = {
export default function AttachmentsUpload({
attachments,
onAttachmentsChange,
- existingAttachments = []
+ existingAttachments = [],
+ onExistingAttachmentsChange,
+ responseId,
+ userId
}: AttachmentsUploadProps) {
const purchaseInputRef = useRef<HTMLInputElement>(null)
const designInputRef = useRef<HTMLInputElement>(null)
const [purchaseDragActive, setPurchaseDragActive] = useState(false)
const [designDragActive, setDesignDragActive] = useState(false)
const [uploadErrors, setUploadErrors] = useState<string[]>([])
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
+ const [fileToDelete, setFileToDelete] = useState<{file: any, isExisting: boolean, index: number} | null>(null)
// 파일 유효성 검사
const validateFile = (file: File): string | null => {
@@ -158,6 +178,57 @@ export default function AttachmentsUpload({
newFiles[index].attachmentType = newType
onAttachmentsChange(newFiles)
}
+
+ // 파일 삭제 확인
+ const handleDeleteClick = (file: any, isExisting: boolean, index: number) => {
+ setFileToDelete({ file, isExisting, index })
+ setDeleteDialogOpen(true)
+ }
+
+ // 파일 삭제 실행
+ const handleDeleteConfirm = async () => {
+ if (!fileToDelete) return
+
+ const { isExisting, index } = fileToDelete
+
+ if (isExisting) {
+ // 기존 첨부파일 삭제 - 서버액션 호출
+ if (responseId && userId && fileToDelete.file.id) {
+ try {
+ const result = await deleteVendorResponseAttachment({
+ attachmentId: fileToDelete.file.id,
+ responseId,
+ userId
+ })
+ if (result.success) {
+ // 클라이언트 상태 업데이트
+ const newExistingAttachments = existingAttachments.filter((_, i) => i !== index)
+ onExistingAttachmentsChange?.(newExistingAttachments)
+ } else {
+ toast.error(`삭제 실패: ${result.error}`)
+ return
+ }
+ } catch (error) {
+ console.error('삭제 API 호출 실패:', error)
+ toast.error('삭제 중 오류가 발생했습니다.')
+ return
+ }
+ }
+ } else {
+ // 새 첨부파일 삭제 (클라이언트에서만)
+ const newFiles = attachments.filter((_, i) => i !== index)
+ onAttachmentsChange(newFiles)
+ }
+
+ setDeleteDialogOpen(false)
+ setFileToDelete(null)
+ }
+
+ // 파일 삭제 취소
+ const handleDeleteCancel = () => {
+ setDeleteDialogOpen(false)
+ setFileToDelete(null)
+ }
// 파일 아이콘 가져오기
const getFileIcon = (fileName: string) => {
@@ -388,14 +459,24 @@ export default function AttachmentsUpload({
</Badge>
</TableCell>
<TableCell>
- <Button
- type="button"
- variant="ghost"
- size="sm"
- onClick={() => window.open(file.filePath, '_blank')}
- >
- <Download className="h-4 w-4" />
- </Button>
+ <div className="flex items-center gap-1">
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => window.open(file.filePath, '_blank')}
+ >
+ <Download className="h-4 w-4" />
+ </Button>
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => handleDeleteClick(file, true, index)}
+ >
+ <Trash2 className="h-4 w-4 text-red-500" />
+ </Button>
+ </div>
</TableCell>
</TableRow>
))}
@@ -449,7 +530,7 @@ export default function AttachmentsUpload({
type="button"
variant="ghost"
size="sm"
- onClick={() => handleFileRemove(index)}
+ onClick={() => handleDeleteClick(file, false, index)}
>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
@@ -461,6 +542,37 @@ export default function AttachmentsUpload({
</CardContent>
</Card>
)}
+
+ {/* 파일 삭제 확인 다이얼로그 */}
+ <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>파일 삭제</DialogTitle>
+ <DialogDescription>
+ {fileToDelete?.isExisting ? '기존 첨부파일' : '새로 업로드한 파일'} "{fileToDelete?.file.originalFileName || fileToDelete?.file.name}"을(를) 삭제하시겠습니까?
+ <br />
+ <strong>삭제된 파일은 복구할 수 없습니다.</strong>
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={handleDeleteCancel}
+ >
+ 취소
+ </Button>
+ <Button
+ type="button"
+ variant="destructive"
+ onClick={handleDeleteConfirm}
+ >
+ <Trash2 className="h-4 w-4 mr-2" />
+ 삭제
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
</div>
)
} \ No newline at end of file
diff --git a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx
index 569546dd..fec9a2b9 100644
--- a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx
+++ b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx
@@ -14,6 +14,11 @@ import RfqInfoHeader from "./rfq-info-header"
import CommercialTermsForm from "./commercial-terms-form"
import QuotationItemsTable from "./quotation-items-table"
import AttachmentsUpload from "./attachments-upload"
+
+interface FileWithType extends File {
+ attachmentType?: "구매" | "설계"
+ description?: string
+}
import { formatDate, formatCurrency } from "@/lib/utils"
import { Shield, FileText, CheckCircle, XCircle, Clock, Download, Eye, Save, Send, AlertCircle, Upload, } from "lucide-react"
import { Progress } from "@/components/ui/progress"
@@ -103,11 +108,34 @@ export default function VendorResponseEditor({
const router = useRouter()
const [loading, setLoading] = useState(false)
const [activeTab, setActiveTab] = useState("info")
- const [attachments, setAttachments] = useState<File[]>([])
+ const [attachments, setAttachments] = useState<FileWithType[]>([])
+ const [existingAttachments, setExistingAttachments] = useState<any[]>([])
+ const [deletedAttachments, setDeletedAttachments] = useState<any[]>([])
const [uploadProgress, setUploadProgress] = useState(0) // 추가
console.log(existingResponse,"existingResponse")
+ // existingResponse가 변경될 때 existingAttachments 초기화
+ useEffect(() => {
+ if (existingResponse?.attachments) {
+ setExistingAttachments([...existingResponse.attachments])
+ setDeletedAttachments([]) // 삭제 목록 초기화
+ } else {
+ setExistingAttachments([])
+ setDeletedAttachments([])
+ }
+ }, [existingResponse?.attachments])
+
+ // 기존 첨부파일 삭제 처리
+ const handleExistingAttachmentsChange = (files: any[]) => {
+ const currentAttachments = existingResponse?.attachments || []
+ const deleted = currentAttachments.filter(
+ curr => !files.some(f => f.id === curr.id)
+ )
+ setExistingAttachments(files)
+ setDeletedAttachments(prev => [...prev, ...deleted])
+ }
+
// Form 초기값 설정
const defaultValues: VendorResponseFormData = {
@@ -229,10 +257,20 @@ export default function VendorResponseEditor({
try {
const formData = new FormData()
- const fileMetadata = attachments.map((file: any) => ({
+ const fileMetadata = attachments.map((file: FileWithType) => ({
attachmentType: file.attachmentType || "기타",
description: file.description || ""
}))
+
+ // 삭제된 첨부파일 ID 목록
+ const deletedAttachmentIds = deletedAttachments.map(file => file.id)
+
+ // 디버그: 첨부파일 attachmentType 확인
+ console.log('Attachments with types:', attachments.map(f => ({
+ name: f.name,
+ attachmentType: f.attachmentType,
+ size: f.size
+ })))
// 기본 데이터 추가
@@ -246,7 +284,8 @@ export default function VendorResponseEditor({
submittedBy: isSubmit ? userId : null,
totalAmount: data.quotationItems.reduce((sum, item) => sum + item.totalPrice, 0),
updatedBy: userId,
- fileMetadata
+ fileMetadata,
+ deletedAttachmentIds
}
console.log('Submitting data:', submitData) // 디버깅용
@@ -468,7 +507,10 @@ export default function VendorResponseEditor({
<AttachmentsUpload
attachments={attachments}
onAttachmentsChange={setAttachments}
- existingAttachments={existingResponse?.attachments}
+ existingAttachments={existingAttachments}
+ onExistingAttachmentsChange={handleExistingAttachmentsChange}
+ responseId={existingResponse?.id}
+ userId={userId}
/>
</TabsContent>
</Tabs>
diff --git a/lib/rfq-last/vendor-response/rfq-attachments-dialog.tsx b/lib/rfq-last/vendor-response/rfq-attachments-dialog.tsx
index 2b3138d6..3ca01191 100644
--- a/lib/rfq-last/vendor-response/rfq-attachments-dialog.tsx
+++ b/lib/rfq-last/vendor-response/rfq-attachments-dialog.tsx
@@ -67,7 +67,7 @@ export function RfqAttachmentsDialog({ isOpen, onClose, rfqData }: RfqAttachment
const attachments = await getRfqAttachmentsAction(rfqId);
if (!attachments.success || attachments.data.length === 0) {
- toast.error(result.error || "다운로드할 파일이 없습니다");
+ toast.error(attachments.error || "다운로드할 파일이 없습니다");
}
diff --git a/lib/rfq-last/vendor-response/rfq-items-dialog.tsx b/lib/rfq-last/vendor-response/rfq-items-dialog.tsx
index daa692e9..9790a1bd 100644
--- a/lib/rfq-last/vendor-response/rfq-items-dialog.tsx
+++ b/lib/rfq-last/vendor-response/rfq-items-dialog.tsx
@@ -94,7 +94,7 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
if (result.success) {
setItems(result.data)
- setStatistics(result.statistics)
+ setStatistics(result.statistics ?? null)
} else {
toast.error(result.error || "품목을 불러오는데 실패했습니다")
setItems([])
@@ -118,17 +118,6 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
window.open(specUrl, '_blank', 'noopener,noreferrer')
}
- // 수량 포맷팅
- const formatQuantity = (quantity: number | null, uom: string | null) => {
- if (!quantity) return "-"
- return `${quantity.toLocaleString()}${uom ? ` ${uom}` : ""}`
- }
-
- // 중량 포맷팅
- const formatWeight = (weight: number | null, uom: string | null) => {
- if (!weight) return "-"
- return `${weight.toLocaleString()} ${uom || "KG"}`
- }
return (
<Dialog open={isOpen} onOpenChange={onClose}>
@@ -177,8 +166,10 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
<TableHead className="w-[60px]">구분</TableHead>
<TableHead className="w-[120px]">자재코드</TableHead>
<TableHead>자재명</TableHead>
- <TableHead className="w-[100px]">수량</TableHead>
- <TableHead className="w-[100px]">중량</TableHead>
+ <TableHead className="w-[80px]">수량</TableHead>
+ <TableHead className="w-[60px]">수량단위</TableHead>
+ <TableHead className="w-[80px]">중량</TableHead>
+ <TableHead className="w-[60px]">중량단위</TableHead>
<TableHead className="w-[100px]">납기일</TableHead>
<TableHead className="w-[100px]">PR번호</TableHead>
<TableHead className="w-[80px]">사양</TableHead>
@@ -197,6 +188,9 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
<TableCell><Skeleton className="h-8 w-full" /></TableCell>
<TableCell><Skeleton className="h-8 w-full" /></TableCell>
<TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
+ <TableCell><Skeleton className="h-8 w-full" /></TableCell>
</TableRow>
))}
</TableBody>
@@ -213,8 +207,10 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
<TableHead className="w-[60px]">구분</TableHead>
<TableHead className="w-[120px]">자재코드</TableHead>
<TableHead>자재명</TableHead>
- <TableHead className="w-[100px]">수량</TableHead>
- <TableHead className="w-[100px]">중량</TableHead>
+ <TableHead className="w-[80px]">수량</TableHead>
+ <TableHead className="w-[60px]">수량단위</TableHead>
+ <TableHead className="w-[80px]">중량</TableHead>
+ <TableHead className="w-[60px]">중량단위</TableHead>
<TableHead className="w-[100px]">납기일</TableHead>
<TableHead className="w-[100px]">PR번호</TableHead>
<TableHead className="w-[100px]">사양</TableHead>
@@ -264,12 +260,22 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
</TableCell>
<TableCell>
<span className="text-sm font-medium">
- {formatQuantity(item.quantity, item.uom)}
+ {item.quantity ? item.quantity.toLocaleString() : "-"}
</span>
</TableCell>
<TableCell>
- <span className="text-sm">
- {formatWeight(item.grossWeight, item.gwUom)}
+ <span className="text-sm text-muted-foreground">
+ {item.uom || "-"}
+ </span>
+ </TableCell>
+ <TableCell>
+ <span className="text-sm font-medium">
+ {item.grossWeight ? item.grossWeight.toLocaleString() : "-"}
+ </span>
+ </TableCell>
+ <TableCell>
+ <span className="text-sm text-muted-foreground">
+ {item.gwUom || "-"}
</span>
</TableCell>
<TableCell>
@@ -313,7 +319,7 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
<TableCell>
<div className="text-xs">
{[
- item.projectDef && `DEF: ${item.projectDef}`,
+ item.projectDef && `${item.projectDef}`,
item.projectSc && `SC: ${item.projectSc}`,
item.projectKl && `KL: ${item.projectKl}`,
item.projectLc && `LC: ${item.projectLc}`,