summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx')
-rw-r--r--lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx1043
1 files changed, 522 insertions, 521 deletions
diff --git a/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx b/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx
index 0425ccc9..0a56b702 100644
--- a/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx
+++ b/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx
@@ -1,522 +1,523 @@
-"use client"
-
-import * as React from "react"
-import { useState, useEffect } from "react"
-import { useRouter } from "next/navigation"
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { Textarea } from "@/components/ui/textarea"
-import { Badge } from "@/components/ui/badge"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { CalendarIcon, Send, AlertCircle, Upload, X, FileText, Download } from "lucide-react"
-import { Calendar } from "@/components/ui/calendar"
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
-import { Alert, AlertDescription } from "@/components/ui/alert"
-import { formatDate, cn } from "@/lib/utils"
-import { toast } from "sonner"
-import { useSession } from "next-auth/react"
-
-interface QuotationResponseTabProps {
- quotation: {
- id: number
- status: string
- totalPrice: string | null
- currency: string | null
- validUntil: Date | null
- remark: string | null
- quotationAttachments?: Array<{
- id: number
- fileName: string
- fileSize: number
- filePath: string
- description?: string | null
- }>
- rfq: {
- id: number
- rfqCode: string | null
- materialCode: string | null
- dueDate: Date | null
- status: string | null
- item?: {
- itemName: string | null
- } | null
- } | null
- vendor: {
- vendorName: string
- } | null
- }
-}
-
-const CURRENCIES = [
- { value: "KRW", label: "KRW (원)" },
- { value: "USD", label: "USD (달러)" },
- { value: "EUR", label: "EUR (유로)" },
- { value: "JPY", label: "JPY (엔)" },
- { value: "CNY", label: "CNY (위안)" },
-]
-
-export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) {
- const [totalPrice, setTotalPrice] = useState(quotation.totalPrice?.toString() || "")
- const [currency, setCurrency] = useState(quotation.currency || "KRW")
- const [validUntil, setValidUntil] = useState<Date | undefined>(
- quotation.validUntil ? new Date(quotation.validUntil) : undefined
- )
- const [remark, setRemark] = useState(quotation.remark || "")
- const [isLoading, setIsLoading] = useState(false)
- const [attachments, setAttachments] = useState<Array<{
- id?: number
- fileName: string
- fileSize: number
- filePath: string
- isNew?: boolean
- file?: File
- }>>([])
- const [isUploadingFiles, setIsUploadingFiles] = useState(false)
- const router = useRouter()
- const session = useSession()
-
- // // 초기 첨부파일 데이터 로드
- // useEffect(() => {
- // if (quotation.quotationAttachments) {
- // setAttachments(quotation.quotationAttachments.map(att => ({
- // id: att.id,
- // fileName: att.fileName,
- // fileSize: att.fileSize,
- // filePath: att.filePath,
- // isNew: false
- // })))
- // }
- // }, [quotation.quotationAttachments])
-
- const rfq = quotation.rfq
- const isDueDatePassed = rfq?.dueDate ? new Date(rfq.dueDate) < new Date() : false
- const canSubmit = !["Accepted", "Rejected"].includes(quotation.status) && !isDueDatePassed
- const canEdit = !["Accepted", "Rejected"].includes(quotation.status) && !isDueDatePassed
-
- // 파일 업로드 핸들러
- const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
- const files = event.target.files
- if (!files) return
-
- Array.from(files).forEach(file => {
- setAttachments(prev => [
- ...prev,
- {
- fileName: file.name,
- fileSize: file.size,
- filePath: '',
- isNew: true,
- file
- }
- ])
- })
- }
-
- // 첨부파일 제거
- const removeAttachment = (index: number) => {
- setAttachments(prev => prev.filter((_, i) => i !== index))
- }
-
- // 파일 업로드 함수
- const uploadFiles = async () => {
- const newFiles = attachments.filter(att => att.isNew && att.file)
- if (newFiles.length === 0) return []
-
- setIsUploadingFiles(true)
- const uploadedFiles = []
-
- try {
- for (const attachment of newFiles) {
- const formData = new FormData()
- formData.append('file', attachment.file!)
-
- const response = await fetch('/api/upload', {
- method: 'POST',
- body: formData
- })
-
- if (!response.ok) throw new Error('파일 업로드 실패')
-
- const result = await response.json()
- uploadedFiles.push({
- fileName: result.fileName,
- filePath: result.url,
- fileSize: attachment.fileSize
- })
- }
- return uploadedFiles
- } catch (error) {
- console.error('파일 업로드 오류:', error)
- toast.error('파일 업로드 중 오류가 발생했습니다.')
- return []
- } finally {
- setIsUploadingFiles(false)
- }
- }
-
- const handleSubmit = async () => {
- if (!totalPrice || !currency || !validUntil) {
- toast.error("모든 필수 항목을 입력해주세요.")
- return
- }
-
- setIsLoading(true)
- try {
- // 파일 업로드 먼저 처리
- const uploadedFiles = await uploadFiles()
-
- const { submitTechSalesVendorQuotation } = await import("@/lib/techsales-rfq/service")
-
- const result = await submitTechSalesVendorQuotation({
- id: quotation.id,
- currency,
- totalPrice,
- validUntil: validUntil!,
- remark,
- attachments: uploadedFiles,
- updatedBy: parseInt(session.data?.user.id || "0")
- })
-
- if (result.error) {
- toast.error(result.error)
- } else {
- toast.success("견적서가 제출되었습니다.")
- // // 페이지 새로고침 대신 router.refresh() 사용
- // router.refresh()
- // 페이지 새로고침
- window.location.reload()
- }
- } catch {
- toast.error("제출 중 오류가 발생했습니다.")
- } finally {
- setIsLoading(false)
- }
- }
-
- const getStatusBadgeVariant = (status: string) => {
- switch (status) {
- case "Draft":
- return "secondary"
- case "Submitted":
- return "default"
- case "Revised":
- return "outline"
- case "Rejected":
- return "destructive"
- case "Accepted":
- return "success"
- default:
- return "secondary"
- }
- }
-
- const getStatusLabel = (status: string) => {
- switch (status) {
- case "Draft":
- return "초안"
- case "Submitted":
- return "제출됨"
- case "Revised":
- return "수정됨"
- case "Rejected":
- return "반려됨"
- case "Accepted":
- return "승인됨"
- default:
- return status
- }
- }
-
- return (
- <ScrollArea className="h-full">
- <div className="space-y-6 p-1">
- {/* 견적서 상태 정보 */}
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- 견적서 상태
- <Badge variant={getStatusBadgeVariant(quotation.status)}>
- {getStatusLabel(quotation.status)}
- </Badge>
- </CardTitle>
- <CardDescription>
- 현재 견적서 상태 및 마감일 정보
- </CardDescription>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
- <div className="space-y-2">
- <div className="text-sm font-medium text-muted-foreground">견적서 상태</div>
- <div className="text-sm">{getStatusLabel(quotation.status)}</div>
- </div>
- <div className="space-y-2">
- <div className="text-sm font-medium text-muted-foreground">RFQ 마감일</div>
- <div className="text-sm">
- {rfq?.dueDate ? formatDate(rfq.dueDate) : "N/A"}
- </div>
- </div>
- <div className="space-y-2">
- <div className="text-sm font-medium text-muted-foreground">남은 시간</div>
- <div className="text-sm">
- {isDueDatePassed ? (
- <span className="text-destructive">마감됨</span>
- ) : rfq?.dueDate ? (
- <span className="text-green-600">
- {Math.ceil((new Date(rfq.dueDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24))}일
- </span>
- ) : (
- "N/A"
- )}
- </div>
- </div>
- </div>
-
- {isDueDatePassed && (
- <Alert>
- <AlertCircle className="h-4 w-4" />
- <AlertDescription>
- RFQ 마감일이 지났습니다. 견적서를 수정하거나 제출할 수 없습니다.
- </AlertDescription>
- </Alert>
- )}
-
- {!canEdit && !isDueDatePassed && (
- <Alert>
- <AlertCircle className="h-4 w-4" />
- <AlertDescription>
- 현재 상태에서는 견적서를 수정할 수 없습니다.
- </AlertDescription>
- </Alert>
- )}
- </CardContent>
- </Card>
-
- {/* 견적 응답 폼 */}
- <Card>
- <CardHeader>
- <CardTitle>견적 응답</CardTitle>
- <CardDescription>
- 총 가격, 통화, 유효기간을 입력해주세요.
- </CardDescription>
- </CardHeader>
- <CardContent className="space-y-6">
- {/* 총 가격 */}
- <div className="space-y-2">
- <Label htmlFor="totalPrice">
- 총 가격 <span className="text-destructive">*</span>
- </Label>
- <Input
- id="totalPrice"
- type="number"
- placeholder="총 가격을 입력하세요"
- value={totalPrice}
- onChange={(e) => setTotalPrice(e.target.value)}
- disabled={!canEdit}
- className="text-right"
- />
- </div>
-
- {/* 통화 */}
- <div className="space-y-2">
- <Label htmlFor="currency">
- 통화 <span className="text-destructive">*</span>
- </Label>
- <Select value={currency} onValueChange={setCurrency} disabled={!canEdit}>
- <SelectTrigger>
- <SelectValue placeholder="통화를 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- {CURRENCIES.map((curr) => (
- <SelectItem key={curr.value} value={curr.value}>
- {curr.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
-
- {/* 유효기간 */}
- <div className="space-y-2">
- <Label>
- 견적 유효기간 <span className="text-destructive">*</span>
- </Label>
- <Popover>
- <PopoverTrigger asChild>
- <Button
- variant="outline"
- className={cn(
- "w-full justify-start text-left font-normal",
- !validUntil && "text-muted-foreground"
- )}
- disabled={!canEdit}
- >
- <CalendarIcon className="mr-2 h-4 w-4" />
- {validUntil ? formatDate(validUntil) : "날짜를 선택하세요"}
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-auto p-0" align="start">
- <Calendar
- mode="single"
- selected={validUntil}
- onSelect={setValidUntil}
- disabled={(date) => date < new Date()}
- initialFocus
- />
- </PopoverContent>
- </Popover>
- </div>
-
- {/* 비고 */}
- <div className="space-y-2">
- <Label htmlFor="remark">비고</Label>
- <Textarea
- id="remark"
- placeholder="추가 설명이나 조건을 입력하세요"
- value={remark}
- onChange={(e) => setRemark(e.target.value)}
- disabled={!canEdit}
- rows={4}
- />
- </div>
-
- {/* 첨부파일 */}
- <div className="space-y-4">
- <Label>첨부파일</Label>
-
- {/* 파일 업로드 버튼 */}
- {canEdit && (
- <div className="flex items-center gap-2">
- <Button
- type="button"
- variant="outline"
- size="sm"
- disabled={isUploadingFiles}
- onClick={() => document.getElementById('file-input')?.click()}
- >
- <Upload className="h-4 w-4 mr-2" />
- 파일 선택
- </Button>
- <input
- id="file-input"
- type="file"
- multiple
- onChange={handleFileSelect}
- className="hidden"
- accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.jpg,.jpeg,.png,.zip"
- />
- <span className="text-sm text-muted-foreground">
- PDF, 문서파일, 이미지파일, 압축파일 등
- </span>
- </div>
- )}
-
- {/* 첨부파일 목록 */}
- {attachments.length > 0 && (
- <div className="space-y-2">
- {attachments.map((attachment, index) => (
- <div
- key={index}
- className="flex items-center justify-between p-3 border rounded-lg bg-muted/50"
- >
- <div className="flex items-center gap-2">
- <FileText className="h-4 w-4 text-muted-foreground" />
- <div>
- <div className="text-sm font-medium">{attachment.fileName}</div>
- <div className="text-xs text-muted-foreground">
- {(attachment.fileSize / 1024 / 1024).toFixed(2)} MB
- {attachment.isNew && (
- <Badge variant="secondary" className="ml-2">
- 새 파일
- </Badge>
- )}
- </div>
- </div>
- </div>
- <div className="flex items-center gap-2">
- {!attachment.isNew && (
- <Button
- type="button"
- variant="ghost"
- size="sm"
- onClick={() => window.open(attachment.filePath, '_blank')}
- >
- <Download className="h-4 w-4" />
- </Button>
- )}
- {canEdit && (
- <Button
- type="button"
- variant="ghost"
- size="sm"
- onClick={() => removeAttachment(index)}
- >
- <X className="h-4 w-4" />
- </Button>
- )}
- </div>
- </div>
- ))}
- </div>
- )}
- </div>
-
- {/* 액션 버튼 */}
- {canEdit && canSubmit && (
- <div className="flex justify-center pt-4">
- <Button
- onClick={handleSubmit}
- disabled={isLoading || !totalPrice || !currency || !validUntil}
- className="w-full "
- >
- <Send className="mr-2 h-4 w-4" />
- 견적서 제출
- </Button>
- </div>
- )}
- </CardContent>
- </Card>
-
- {/* 현재 견적 정보 (읽기 전용) */}
- {quotation.totalPrice && (
- <Card>
- <CardHeader>
- <CardTitle>현재 견적 정보</CardTitle>
- <CardDescription>
- 저장된 견적 정보
- </CardDescription>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
- <div className="space-y-2">
- <div className="text-sm font-medium text-muted-foreground">총 가격</div>
- <div className="text-lg font-semibold">
- {parseFloat(quotation.totalPrice).toLocaleString()} {quotation.currency}
- </div>
- </div>
- <div className="space-y-2">
- <div className="text-sm font-medium text-muted-foreground">통화</div>
- <div className="text-sm">{quotation.currency}</div>
- </div>
- <div className="space-y-2">
- <div className="text-sm font-medium text-muted-foreground">유효기간</div>
- <div className="text-sm">
- {quotation.validUntil ? formatDate(quotation.validUntil) : "N/A"}
- </div>
- </div>
- </div>
- {quotation.remark && (
- <div className="space-y-2">
- <div className="text-sm font-medium text-muted-foreground">비고</div>
- <div className="text-sm p-3 bg-muted rounded-md">{quotation.remark}</div>
- </div>
- )}
- </CardContent>
- </Card>
- )}
- </div>
- </ScrollArea>
- )
+"use client"
+
+import * as React from "react"
+import { useState } from "react"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Textarea } from "@/components/ui/textarea"
+import { Badge } from "@/components/ui/badge"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { CalendarIcon, Send, AlertCircle, X, FileText, Download, History, FileIcon } from "lucide-react"
+import { Calendar } from "@/components/ui/calendar"
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { Alert, AlertDescription } from "@/components/ui/alert"
+import { formatDate, cn } from "@/lib/utils"
+import { toast } from "sonner"
+import { useSession } from "next-auth/react"
+import { QuotationHistoryDialog } from "@/lib/techsales-rfq/table/detail-table/quotation-history-dialog"
+
+interface QuotationResponseTabProps {
+ quotation: {
+ id: number
+ status: string
+ totalPrice: string | null
+ currency: string | null
+ validUntil: Date | null
+ remark: string | null
+ quotationAttachments?: Array<{
+ id: number
+ fileName: string
+ fileSize: number
+ filePath: string
+ description?: string | null
+ }>
+ rfq: {
+ id: number
+ rfqCode: string | null
+ materialCode: string | null
+ dueDate: Date | null
+ status: string | null
+ item?: {
+ itemName: string | null
+ } | null
+ } | null
+ vendor: {
+ vendorName: string
+ } | null
+ }
+}
+
+const CURRENCIES = [
+ { value: "KRW", label: "KRW (원)" },
+ { value: "USD", label: "USD (달러)" },
+ { value: "EUR", label: "EUR (유로)" },
+ { value: "JPY", label: "JPY (엔)" },
+ { value: "CNY", label: "CNY (위안)" },
+]
+
+export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) {
+ const [totalPrice, setTotalPrice] = useState(quotation.totalPrice?.toString() || "")
+ const [currency, setCurrency] = useState(quotation.currency || "KRW")
+ const [validUntil, setValidUntil] = useState<Date | undefined>(
+ quotation.validUntil ? new Date(quotation.validUntil) : undefined
+ )
+ const [remark, setRemark] = useState(quotation.remark || "")
+ const [isLoading, setIsLoading] = useState(false)
+ const [attachments, setAttachments] = useState<Array<{
+ id?: number
+ fileName: string
+ fileSize: number
+ filePath: string
+ isNew?: boolean
+ file?: File
+ }>>([])
+ const [isUploadingFiles, setIsUploadingFiles] = useState(false)
+
+ // 견적 히스토리 다이얼로그 상태 관리
+ const [historyDialogOpen, setHistoryDialogOpen] = useState(false)
+
+ const session = useSession()
+
+ // // 초기 첨부파일 데이터 로드
+ // useEffect(() => {
+ // if (quotation.quotationAttachments) {
+ // setAttachments(quotation.quotationAttachments.map(att => ({
+ // id: att.id,
+ // fileName: att.fileName,
+ // fileSize: att.fileSize,
+ // filePath: att.filePath,
+ // isNew: false
+ // })))
+ // }
+ // }, [quotation.quotationAttachments])
+
+ const rfq = quotation.rfq
+ const isDueDatePassed = rfq?.dueDate ? new Date(rfq.dueDate) < new Date() : false
+ const canSubmit = !["Accepted", "Rejected"].includes(quotation.status) && !isDueDatePassed
+ const canEdit = !["Accepted", "Rejected"].includes(quotation.status) && !isDueDatePassed
+
+ // 파일 업로드 핸들러
+ const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const files = event.target.files
+ if (!files) return
+
+ Array.from(files).forEach(file => {
+ setAttachments(prev => [
+ ...prev,
+ {
+ fileName: file.name,
+ fileSize: file.size,
+ filePath: '',
+ isNew: true,
+ file
+ }
+ ])
+ })
+ }
+
+ // 첨부파일 제거
+ const removeAttachment = (index: number) => {
+ setAttachments(prev => prev.filter((_, i) => i !== index))
+ }
+
+ // 파일 업로드 함수
+ const uploadFiles = async () => {
+ const newFiles = attachments.filter(att => att.isNew && att.file)
+ if (newFiles.length === 0) return []
+
+ setIsUploadingFiles(true)
+
+ try {
+ // 서비스 함수를 사용하여 파일 업로드
+ const { uploadQuotationAttachments } = await import('@/lib/techsales-rfq/service')
+
+ const files = newFiles.map(att => att.file!).filter(Boolean)
+ const userId = parseInt(session.data?.user.id || "0")
+
+ const result = await uploadQuotationAttachments(quotation.id, files, userId)
+
+ if (result.success && result.attachments) {
+ return result.attachments
+ } else {
+ throw new Error(result.error || '파일 저장에 실패했습니다.')
+ }
+ } catch (error) {
+ console.error('파일 업로드 오류:', error)
+ toast.error('파일 업로드 중 오류가 발생했습니다.')
+ return []
+ } finally {
+ setIsUploadingFiles(false)
+ }
+ }
+
+ const handleSubmit = async () => {
+ if (!totalPrice || !currency || !validUntil) {
+ toast.error("모든 필수 항목을 입력해주세요.")
+ return
+ }
+
+ setIsLoading(true)
+ try {
+ // 파일 업로드 먼저 처리
+ const uploadedFiles = await uploadFiles()
+
+ const { submitTechSalesVendorQuotation } = await import("@/lib/techsales-rfq/service")
+
+ const result = await submitTechSalesVendorQuotation({
+ id: quotation.id,
+ currency,
+ totalPrice,
+ validUntil: validUntil!,
+ remark,
+ attachments: uploadedFiles,
+ updatedBy: parseInt(session.data?.user.id || "0")
+ })
+
+ if (result.error) {
+ toast.error(result.error)
+ } else {
+ toast.success("견적서가 제출되었습니다.")
+ // // 페이지 새로고침 대신 router.refresh() 사용
+ // router.refresh()
+ // 페이지 새로고침
+ window.location.reload()
+ }
+ } catch {
+ toast.error("제출 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const getStatusBadgeVariant = (status: string) => {
+ switch (status) {
+ case "Draft":
+ return "secondary"
+ case "Submitted":
+ return "default"
+ case "Revised":
+ return "outline"
+ case "Rejected":
+ return "destructive"
+ case "Accepted":
+ return "success"
+ default:
+ return "secondary"
+ }
+ }
+
+ const getStatusLabel = (status: string) => {
+ switch (status) {
+ case "Draft":
+ return "초안"
+ case "Submitted":
+ return "제출됨"
+ case "Revised":
+ return "수정됨"
+ case "Rejected":
+ return "반려됨"
+ case "Accepted":
+ return "승인됨"
+ default:
+ return status
+ }
+ }
+
+ return (
+ <ScrollArea className="h-full">
+ <div className="space-y-6 p-1">
+ {/* 견적서 상태 정보 */}
+ <Card>
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div>
+ <CardTitle className="flex items-center gap-2">
+ 견적서 상태
+ <Badge variant={getStatusBadgeVariant(quotation.status)}>
+ {getStatusLabel(quotation.status)}
+ </Badge>
+ </CardTitle>
+ <CardDescription>
+ 현재 견적서 상태 및 마감일 정보
+ </CardDescription>
+ </div>
+
+ {/* 견적 히스토리 보기 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setHistoryDialogOpen(true)}
+ className="flex items-center gap-2"
+ >
+ <History className="h-4 w-4" />
+ 이전 견적 히스토리 보기
+ </Button>
+ </div>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">견적서 상태</div>
+ <div className="text-sm">{getStatusLabel(quotation.status)}</div>
+ </div>
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">RFQ 마감일</div>
+ <div className="text-sm">
+ {rfq?.dueDate ? formatDate(rfq.dueDate) : "N/A"}
+ </div>
+ </div>
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">남은 시간</div>
+ <div className="text-sm">
+ {isDueDatePassed ? (
+ <span className="text-destructive">마감됨</span>
+ ) : rfq?.dueDate ? (
+ <span className="text-green-600">
+ {Math.ceil((new Date(rfq.dueDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24))}일
+ </span>
+ ) : (
+ "N/A"
+ )}
+ </div>
+ </div>
+ </div>
+
+ {isDueDatePassed && (
+ <Alert>
+ <AlertCircle className="h-4 w-4" />
+ <AlertDescription>
+ RFQ 마감일이 지났습니다. 견적서를 수정하거나 제출할 수 없습니다.
+ </AlertDescription>
+ </Alert>
+ )}
+
+ {!canEdit && !isDueDatePassed && (
+ <Alert>
+ <AlertCircle className="h-4 w-4" />
+ <AlertDescription>
+ 현재 상태에서는 견적서를 수정할 수 없습니다.
+ </AlertDescription>
+ </Alert>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 견적 응답 폼 */}
+ <Card>
+ <CardHeader>
+ <CardTitle>견적 응답</CardTitle>
+ <CardDescription>
+ 견적 정보를 입력하고 제출하세요
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-2">
+ <Label htmlFor="totalPrice">견적 금액 *</Label>
+ <Input
+ id="totalPrice"
+ type="number"
+ value={totalPrice}
+ onChange={(e) => setTotalPrice(e.target.value)}
+ placeholder="견적 금액을 입력하세요"
+ disabled={!canEdit}
+ />
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor="currency">통화 *</Label>
+ <Select value={currency} onValueChange={setCurrency} disabled={!canEdit}>
+ <SelectTrigger>
+ <SelectValue placeholder="통화를 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ {CURRENCIES.map((curr) => (
+ <SelectItem key={curr.value} value={curr.value}>
+ {curr.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <Label>견적 유효기한 *</Label>
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button
+ variant="outline"
+ className={cn(
+ "w-full justify-start text-left font-normal",
+ !validUntil && "text-muted-foreground"
+ )}
+ disabled={!canEdit}
+ >
+ <CalendarIcon className="mr-2 h-4 w-4" />
+ {validUntil ? formatDate(validUntil) : "유효기한을 선택하세요"}
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-auto p-0">
+ <Calendar
+ mode="single"
+ selected={validUntil}
+ onSelect={setValidUntil}
+ initialFocus
+ />
+ </PopoverContent>
+ </Popover>
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="remark">비고</Label>
+ <Textarea
+ id="remark"
+ value={remark}
+ onChange={(e) => setRemark(e.target.value)}
+ placeholder="추가 설명이나 특이사항을 입력하세요"
+ rows={3}
+ disabled={!canEdit}
+ />
+ </div>
+
+ {/* 첨부파일 섹션 */}
+ <div className="space-y-2">
+ <Label>첨부파일</Label>
+ <div className="border-2 border-dashed border-gray-300 rounded-lg p-4">
+ <input
+ type="file"
+ id="file-upload"
+ multiple
+ onChange={handleFileSelect}
+ className="hidden"
+ disabled={!canEdit}
+ />
+ <div className="text-center">
+ <FileText className="mx-auto h-12 w-12 text-gray-400" />
+ <div className="mt-2">
+ <Label htmlFor="file-upload" className="cursor-pointer">
+ <span className="text-sm font-medium text-blue-600 hover:text-blue-500">
+ 파일을 선택하세요
+ </span>
+ </Label>
+ </div>
+ <p className="text-xs text-gray-500 mt-1">
+ PDF, DOC, DOCX, XLS, XLSX, PNG, JPG 등
+ </p>
+ </div>
+ </div>
+
+ {/* 첨부파일 목록 */}
+ {attachments.length > 0 && (
+ <div className="space-y-2">
+ <Label>첨부된 파일</Label>
+ <div className="space-y-2">
+ {attachments.map((attachment, index) => (
+ <div key={index} className="flex items-center justify-between p-2 bg-gray-50 rounded">
+ <div className="flex items-center gap-2">
+ <FileIcon className="h-4 w-4 text-gray-500" />
+ <span className="text-sm">{attachment.fileName}</span>
+ <span className="text-xs text-gray-500">
+ ({(attachment.fileSize / 1024).toFixed(1)} KB)
+ </span>
+ </div>
+ <div className="flex items-center gap-2">
+ {!attachment.isNew && (
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => window.open(attachment.filePath, '_blank')}
+ >
+ <Download className="h-4 w-4" />
+ </Button>
+ )}
+ {canEdit && (
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => removeAttachment(index)}
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+ </div>
+
+ {/* 제출 버튼 */}
+ <div className="flex justify-end">
+ <Button
+ onClick={handleSubmit}
+ disabled={!canSubmit || isLoading || isUploadingFiles}
+ className="flex items-center gap-2"
+ >
+ {isLoading ? (
+ <>
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
+ 제출 중...
+ </>
+ ) : (
+ <>
+ <Send className="h-4 w-4" />
+ 견적서 제출
+ </>
+ )}
+ </Button>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 현재 견적 정보 (읽기 전용) */}
+ {quotation.totalPrice && (
+ <Card>
+ <CardHeader>
+ <CardTitle>현재 견적 정보</CardTitle>
+ <CardDescription>
+ 저장된 견적 정보
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">총 가격</div>
+ <div className="text-lg font-semibold">
+ {parseFloat(quotation.totalPrice).toLocaleString()} {quotation.currency}
+ </div>
+ </div>
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">통화</div>
+ <div className="text-sm">{quotation.currency}</div>
+ </div>
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">유효기간</div>
+ <div className="text-sm">
+ {quotation.validUntil ? formatDate(quotation.validUntil) : "N/A"}
+ </div>
+ </div>
+ </div>
+ {quotation.remark && (
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">비고</div>
+ <div className="text-sm p-3 bg-muted rounded-md">{quotation.remark}</div>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ )}
+ </div>
+
+ {/* 견적 히스토리 다이얼로그 */}
+ <QuotationHistoryDialog
+ open={historyDialogOpen}
+ onOpenChange={setHistoryDialogOpen}
+ quotationId={quotation.id}
+ />
+ </ScrollArea>
+ )
} \ No newline at end of file