summaryrefslogtreecommitdiff
path: root/lib/bidding/vendor
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/vendor')
-rw-r--r--lib/bidding/vendor/components/pre-quote-file-upload.tsx367
-rw-r--r--lib/bidding/vendor/components/simple-file-upload.tsx315
-rw-r--r--lib/bidding/vendor/partners-bidding-list-columns.tsx2
-rw-r--r--lib/bidding/vendor/partners-bidding-list.tsx38
-rw-r--r--lib/bidding/vendor/partners-bidding-pre-quote.tsx12
-rw-r--r--lib/bidding/vendor/partners-bidding-toolbar-actions.tsx4
-rw-r--r--lib/bidding/vendor/vendor-prequote-participation-dialog.tsx268
7 files changed, 331 insertions, 675 deletions
diff --git a/lib/bidding/vendor/components/pre-quote-file-upload.tsx b/lib/bidding/vendor/components/pre-quote-file-upload.tsx
deleted file mode 100644
index b6d8990b..00000000
--- a/lib/bidding/vendor/components/pre-quote-file-upload.tsx
+++ /dev/null
@@ -1,367 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import { Card, CardContent, 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 { Badge } from '@/components/ui/badge'
-import { Progress } from '@/components/ui/progress'
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@/components/ui/table'
-import {
- Upload,
- FileText,
- Download,
- Trash2,
- AlertCircle
-} from 'lucide-react'
-import { useToast } from '@/hooks/use-toast'
-import { saveFile } from '@/lib/file-stroage'
-import { downloadFile } from '@/lib/file-download'
-import {
- uploadPreQuoteDocument,
- getPreQuoteDocuments
-} from '../../pre-quote/service'
-
-interface UploadedDocument {
- id: number
- fileName: string
- originalFileName: string
- fileSize: number | null
- filePath: string
- title: string | null
- description: string | null
- uploadedAt: string
-}
-
-interface PreQuoteFileUploadProps {
- biddingId: number
- companyId: number
- onUploadComplete?: (documentId: number) => void
- readOnly?: boolean
-}
-
-export function PreQuoteFileUpload({
- biddingId,
- companyId,
- onUploadComplete,
- readOnly = false
-}: PreQuoteFileUploadProps) {
- const { toast } = useToast()
- const [documents, setDocuments] = React.useState<UploadedDocument[]>([])
- const [isUploading, setIsUploading] = React.useState(false)
- const [uploadProgress, setUploadProgress] = React.useState(0)
- const [dragActive, setDragActive] = React.useState(false)
-
- // 업로드된 문서 목록 로드
- const loadDocuments = React.useCallback(async () => {
- try {
- const docs = await getPreQuoteDocuments(biddingId, companyId)
- // Date를 string으로 변환
- const mappedDocs = docs.map(doc => ({
- ...doc,
- uploadedAt: doc.uploadedAt.toString()
- }))
- setDocuments(mappedDocs)
- } catch (error) {
- console.error('Failed to load documents:', error)
- toast({
- title: '오류',
- description: '업로드된 문서 목록을 불러오는데 실패했습니다.',
- variant: 'destructive',
- })
- }
- }, [biddingId, companyId, toast])
-
- React.useEffect(() => {
- loadDocuments()
- }, [loadDocuments])
-
- // 파일 업로드 처리
- const handleFileUpload = async (files: FileList | File[]) => {
- if (readOnly) return
-
- const fileArray = Array.from(files)
- if (fileArray.length === 0) return
-
- setIsUploading(true)
- setUploadProgress(0)
-
- try {
- for (let i = 0; i < fileArray.length; i++) {
- const file = fileArray[i]
-
- // 파일 크기 체크 (50MB 제한)
- if (file.size > 50 * 1024 * 1024) {
- toast({
- title: '파일 크기 초과',
- description: `${file.name}의 크기가 50MB를 초과합니다.`,
- variant: 'destructive',
- })
- continue
- }
-
- // 파일 타입 체크
- const allowedTypes = [
- 'application/pdf',
- 'application/msword',
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- 'application/vnd.ms-excel',
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
- 'image/jpeg',
- 'image/png',
- 'application/zip'
- ]
-
- if (!allowedTypes.includes(file.type)) {
- toast({
- title: '지원하지 않는 파일 형식',
- description: `${file.name}: PDF, Word, Excel, 이미지, ZIP 파일만 업로드 가능합니다.`,
- variant: 'destructive',
- })
- continue
- }
-
- // 파일 저장
- const saveResult = await saveFile({
- file,
- directory: `bidding/${biddingId}/quotations`,
- originalName: file.name,
- userId: 'current-user' // TODO: 실제 사용자 ID
- })
-
- if (!saveResult.success) {
- toast({
- title: '업로드 실패',
- description: `${file.name}: ${saveResult.error}`,
- variant: 'destructive',
- })
- continue
- }
-
- // 데이터베이스에 문서 정보 저장
- const uploadResult = await uploadPreQuoteDocument(
- biddingId,
- companyId,
- {
- fileName: saveResult.fileName!,
- originalFileName: file.name,
- fileSize: file.size,
- mimeType: file.type,
- filePath: saveResult.path!
- },
- 'current-user' // TODO: 실제 사용자 ID
- )
-
- if (uploadResult.success) {
- toast({
- title: '업로드 완료',
- description: `${file.name}이 성공적으로 업로드되었습니다.`,
- })
-
- if (onUploadComplete && uploadResult.documentId) {
- onUploadComplete(uploadResult.documentId)
- }
- } else {
- toast({
- title: '업로드 실패',
- description: uploadResult.error,
- variant: 'destructive',
- })
- }
-
- // 진행률 업데이트
- setUploadProgress(((i + 1) / fileArray.length) * 100)
- }
-
- // 문서 목록 새로고침
- await loadDocuments()
-
- } catch (error) {
- console.error('Upload error:', error)
- toast({
- title: '업로드 오류',
- description: '파일 업로드 중 오류가 발생했습니다.',
- variant: 'destructive',
- })
- } finally {
- setIsUploading(false)
- setUploadProgress(0)
- }
- }
-
- // 드래그 앤 드롭 처리
- const handleDrag = (e: React.DragEvent) => {
- e.preventDefault()
- e.stopPropagation()
- if (e.type === 'dragenter' || e.type === 'dragover') {
- setDragActive(true)
- } else if (e.type === 'dragleave') {
- setDragActive(false)
- }
- }
-
- const handleDrop = (e: React.DragEvent) => {
- e.preventDefault()
- e.stopPropagation()
- setDragActive(false)
-
- if (readOnly) return
-
- if (e.dataTransfer.files && e.dataTransfer.files[0]) {
- handleFileUpload(e.dataTransfer.files)
- }
- }
-
- // 파일 다운로드
- const handleDownload = async (document: UploadedDocument) => {
- try {
- await downloadFile(document.filePath, document.originalFileName, {
- showToast: true
- })
- } catch (error) {
- console.error('Failed to download document:', error)
- toast({
- title: '다운로드 실패',
- description: '파일 다운로드에 실패했습니다.',
- variant: 'destructive',
- })
- }
- }
-
- // 파일 크기 포맷팅
- const formatFileSize = (bytes: number | null) => {
- if (!bytes) return '-'
- if (bytes === 0) return '0 Bytes'
- const k = 1024
- const sizes = ['Bytes', 'KB', 'MB', 'GB']
- const i = Math.floor(Math.log(bytes) / Math.log(k))
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
- }
-
- return (
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <FileText className="w-5 h-5" />
- 견적 문서 업로드
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- {!readOnly && (
- <div
- className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
- dragActive
- ? 'border-primary bg-primary/5'
- : 'border-gray-300 hover:border-gray-400'
- }`}
- onDragEnter={handleDrag}
- onDragLeave={handleDrag}
- onDragOver={handleDrag}
- onDrop={handleDrop}
- >
- <Upload className="w-8 h-8 mx-auto text-gray-400 mb-2" />
- <div className="space-y-2">
- <p className="text-sm text-gray-600">
- 파일을 드래그하여 업로드하거나 클릭하여 선택하세요
- </p>
- <Input
- type="file"
- multiple
- accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.zip"
- onChange={(e) => e.target.files && handleFileUpload(e.target.files)}
- className="hidden"
- id="file-upload"
- />
- <Label htmlFor="file-upload">
- <Button variant="outline" className="cursor-pointer" asChild>
- <span>파일 선택</span>
- </Button>
- </Label>
- </div>
- <p className="text-xs text-gray-500 mt-2">
- 지원 형식: PDF, Word, Excel, 이미지, ZIP (최대 50MB)
- </p>
- </div>
- )}
-
- {isUploading && (
- <div className="space-y-2">
- <div className="flex items-center gap-2">
- <Upload className="w-4 h-4 animate-pulse" />
- <span className="text-sm">업로드 중...</span>
- </div>
- <Progress value={uploadProgress} className="h-2" />
- </div>
- )}
-
- {/* 업로드된 문서 목록 */}
- {documents.length > 0 ? (
- <div className="space-y-2">
- <Label className="text-sm font-medium">업로드된 문서</Label>
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>파일명</TableHead>
- <TableHead>크기</TableHead>
- <TableHead>업로드일</TableHead>
- <TableHead className="w-24">작업</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {documents.map((doc) => (
- <TableRow key={doc.id}>
- <TableCell>
- <div className="flex items-center gap-2">
- <FileText className="w-4 h-4 text-gray-500" />
- <span className="truncate max-w-48" title={doc.originalFileName}>
- {doc.originalFileName}
- </span>
- </div>
- </TableCell>
- <TableCell className="text-sm text-gray-500">
- {formatFileSize(doc.fileSize)}
- </TableCell>
- <TableCell className="text-sm text-gray-500">
- {new Date(doc.uploadedAt).toLocaleDateString('ko-KR')}
- </TableCell>
- <TableCell>
- <Button
- variant="outline"
- size="sm"
- onClick={() => handleDownload(doc)}
- >
- <Download className="w-3 h-3" />
- </Button>
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </div>
- ) : (
- <div className="text-center py-4 text-gray-500">
- <FileText className="w-8 h-8 mx-auto mb-2 opacity-50" />
- <p className="text-sm">업로드된 문서가 없습니다</p>
- </div>
- )}
-
- {readOnly && documents.length === 0 && (
- <div className="flex items-center gap-2 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
- <AlertCircle className="w-4 h-4 text-yellow-600" />
- <span className="text-sm text-yellow-800">
- 견적 문서가 업로드되지 않았습니다.
- </span>
- </div>
- )}
- </CardContent>
- </Card>
- )
-}
diff --git a/lib/bidding/vendor/components/simple-file-upload.tsx b/lib/bidding/vendor/components/simple-file-upload.tsx
new file mode 100644
index 00000000..b1eb8b8f
--- /dev/null
+++ b/lib/bidding/vendor/components/simple-file-upload.tsx
@@ -0,0 +1,315 @@
+'use client'
+
+import * as React from 'react'
+import { Card, CardContent, 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 { Badge } from '@/components/ui/badge'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table'
+import {
+ Upload,
+ FileText,
+ Download,
+ Trash2
+} from 'lucide-react'
+import { useToast } from '@/hooks/use-toast'
+import { useTransition } from 'react'
+import {
+ uploadPreQuoteDocument,
+ getPreQuoteDocuments,
+ getPreQuoteDocumentForDownload,
+ deletePreQuoteDocument
+} from '../../pre-quote/service'
+import { downloadFile } from '@/lib/file-download'
+
+interface UploadedDocument {
+ id: number
+ fileName: string
+ originalFileName: string
+ fileSize: number | null
+ filePath: string
+ title: string | null
+ description: string | null
+ uploadedAt: string
+ uploadedBy: string
+}
+
+interface SimpleFileUploadProps {
+ biddingId: number
+ companyId: number
+ userId: string
+ readOnly?: boolean
+}
+
+export function SimpleFileUpload({
+ biddingId,
+ companyId,
+ userId,
+ readOnly = false
+}: SimpleFileUploadProps) {
+ const { toast } = useToast()
+ const [isPending, startTransition] = useTransition()
+ const [documents, setDocuments] = React.useState<UploadedDocument[]>([])
+ const [isLoading, setIsLoading] = React.useState(true)
+
+ // 업로드된 문서 목록 로드
+ const loadDocuments = React.useCallback(async () => {
+ try {
+ setIsLoading(true)
+ const docs = await getPreQuoteDocuments(biddingId, companyId)
+ // Date를 string으로 변환
+ const mappedDocs = docs.map(doc => ({
+ ...doc,
+ uploadedAt: doc.uploadedAt.toString(),
+ uploadedBy: doc.uploadedBy || ''
+ }))
+ setDocuments(mappedDocs)
+ } catch (error) {
+ console.error('Failed to load documents:', error)
+ toast({
+ title: '오류',
+ description: '업로드된 문서 목록을 불러오는데 실패했습니다.',
+ variant: 'destructive',
+ })
+ } finally {
+ setIsLoading(false)
+ }
+ }, [biddingId, companyId, toast])
+
+ React.useEffect(() => {
+ loadDocuments()
+ }, [loadDocuments])
+
+ // 파일 업로드 처리
+ const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const files = event.target.files
+ if (!files || files.length === 0) return
+
+ const file = files[0]
+
+ // 파일 크기 체크 (50MB 제한)
+ if (file.size > 50 * 1024 * 1024) {
+ toast({
+ title: '파일 크기 초과',
+ description: '파일 크기가 50MB를 초과합니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ // 파일 타입 체크
+ const allowedTypes = [
+ 'application/pdf',
+ 'application/msword',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/vnd.ms-excel',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'image/jpeg',
+ 'image/png',
+ 'application/zip'
+ ]
+
+ if (!allowedTypes.includes(file.type)) {
+ toast({
+ title: '지원하지 않는 파일 형식',
+ description: 'PDF, Word, Excel, 이미지, ZIP 파일만 업로드 가능합니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ startTransition(async () => {
+ const result = await uploadPreQuoteDocument(biddingId, companyId, file, userId)
+
+ if (result.success) {
+ toast({
+ title: '업로드 완료',
+ description: result.message,
+ })
+ await loadDocuments() // 문서 목록 새로고침
+ } else {
+ toast({
+ title: '업로드 실패',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ })
+
+ // input 초기화
+ event.target.value = ''
+ }
+
+ // 파일 다운로드
+ const handleDownload = (document: UploadedDocument) => {
+ startTransition(async () => {
+ const result = await getPreQuoteDocumentForDownload(document.id, biddingId, companyId)
+
+ if (result.success) {
+ try {
+ await downloadFile(result.document?.filePath, result.document?.originalFileName, {
+ showToast: true
+ })
+ } catch (error) {
+ toast({
+ title: '다운로드 실패',
+ description: '파일 다운로드에 실패했습니다.',
+ variant: 'destructive',
+ })
+ }
+ } else {
+ toast({
+ title: '다운로드 실패',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
+ // 파일 삭제
+ const handleDelete = (document: UploadedDocument) => {
+ if (!confirm(`"${document.originalFileName}" 파일을 삭제하시겠습니까?`)) {
+ return
+ }
+
+ startTransition(async () => {
+ const result = await deletePreQuoteDocument(document.id, biddingId, companyId, userId)
+
+ if (result.success) {
+ toast({
+ title: '삭제 완료',
+ description: result.message,
+ })
+ await loadDocuments() // 문서 목록 새로고침
+ } else {
+ toast({
+ title: '삭제 실패',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
+ // 파일 크기 포맷팅
+ const formatFileSize = (bytes: number | null) => {
+ if (!bytes) return '-'
+ if (bytes === 0) return '0 Bytes'
+ const k = 1024
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+ }
+
+ return (
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="w-5 h-5" />
+ 견적 문서 업로드
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {!readOnly && (
+ <div className="space-y-2">
+ <Label htmlFor="file-upload">견적서 파일</Label>
+ <Input
+ id="file-upload"
+ type="file"
+ accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.zip"
+ onChange={handleFileUpload}
+ disabled={isPending}
+ />
+ <p className="text-xs text-muted-foreground">
+ 지원 형식: PDF, Word, Excel, 이미지, ZIP (최대 50MB)
+ </p>
+ </div>
+ )}
+
+ {/* 업로드된 문서 목록 */}
+ {isLoading ? (
+ <div className="text-center py-4">
+ <p className="text-muted-foreground">문서 목록을 불러오는 중...</p>
+ </div>
+ ) : documents.length > 0 ? (
+ <div className="space-y-2">
+ <Label className="text-sm font-medium">업로드된 문서</Label>
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>파일명</TableHead>
+ <TableHead>크기</TableHead>
+ <TableHead>업로드일</TableHead>
+ <TableHead>작성자</TableHead>
+ <TableHead className="w-24">작업</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {documents.map((doc) => (
+ <TableRow key={doc.id}>
+ <TableCell>
+ <div className="flex items-center gap-2">
+ <FileText className="w-4 h-4 text-gray-500" />
+ <span className="truncate max-w-48" title={doc.originalFileName}>
+ {doc.originalFileName}
+ </span>
+ </div>
+ </TableCell>
+ <TableCell className="text-sm text-gray-500">
+ {formatFileSize(doc.fileSize)}
+ </TableCell>
+ <TableCell className="text-sm text-gray-500">
+ {new Date(doc.uploadedAt).toLocaleDateString('ko-KR')}
+ </TableCell>
+ <TableCell className="text-sm text-gray-500">
+ {doc.uploadedBy}
+ </TableCell>
+ <TableCell>
+ <div className="flex items-center gap-1">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => handleDownload(doc)}
+ disabled={isPending}
+ title="다운로드"
+ >
+ <Download className="w-3 h-3" />
+ </Button>
+ {!readOnly && doc.uploadedBy === userId && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => handleDelete(doc)}
+ disabled={isPending}
+ title="삭제"
+ className="text-red-600 hover:text-red-700"
+ >
+ <Trash2 className="w-3 h-3" />
+ </Button>
+ )}
+ </div>
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ </div>
+ ) : (
+ <div className="text-center py-4 text-gray-500">
+ <FileText className="w-8 h-8 mx-auto mb-2 opacity-50" />
+ <p className="text-sm">업로드된 문서가 없습니다</p>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ )
+}
diff --git a/lib/bidding/vendor/partners-bidding-list-columns.tsx b/lib/bidding/vendor/partners-bidding-list-columns.tsx
index 04575550..0d1e3123 100644
--- a/lib/bidding/vendor/partners-bidding-list-columns.tsx
+++ b/lib/bidding/vendor/partners-bidding-list-columns.tsx
@@ -127,7 +127,7 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
row.original.invitationStatus === 'accepted'
// 사전견적이 가능한 조건: 초대 발송(sent) 상태인 경우
- const canDoPreQuote = row.original.invitationStatus === 'sent'
+ const canDoPreQuote = row.original.invitationStatus === 'sent' || row.original.invitationStatus === 'pending' || row.original.invitationStatus === 'submitted';
return (
<DropdownMenu>
diff --git a/lib/bidding/vendor/partners-bidding-list.tsx b/lib/bidding/vendor/partners-bidding-list.tsx
index a13334ef..9f182911 100644
--- a/lib/bidding/vendor/partners-bidding-list.tsx
+++ b/lib/bidding/vendor/partners-bidding-list.tsx
@@ -16,7 +16,6 @@ import { getBiddingListForPartners, PartnersBiddingListItem } from '../detail/se
import { PartnersBiddingToolbarActions } from './partners-bidding-toolbar-actions'
import { PartnersBiddingAttendanceDialog } from './partners-bidding-attendance-dialog'
import { PartnersBiddingParticipationDialog } from './partners-bidding-participation-dialog'
-import { VendorPreQuoteParticipationDialog } from './vendor-prequote-participation-dialog'
import { setPreQuoteParticipation, getBiddingCompaniesForPartners } from '../pre-quote/service'
interface PartnersBiddingListProps {
@@ -30,7 +29,6 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
const [rowAction, setRowAction] = React.useState<{ type: string; row: { original: PartnersBiddingListItem } } | null>(null)
const [isParticipationDialogOpen, setIsParticipationDialogOpen] = React.useState(false)
const [selectedBiddingForParticipation, setSelectedBiddingForParticipation] = React.useState<PartnersBiddingListItem | null>(null)
- const [isPreQuoteParticipationDialogOpen, setIsPreQuoteParticipationDialogOpen] = React.useState(false)
const [selectedBiddingForPreQuoteParticipation, setSelectedBiddingForPreQuoteParticipation] = React.useState<any | null>(null)
const router = useRouter()
@@ -48,20 +46,7 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
}
}, [companyId])
- // 사전견적 참여의사 결정을 위한 상세 데이터 로드
- const loadBiddingDetailForParticipation = React.useCallback(async (bidding: PartnersBiddingListItem) => {
- try {
- const biddingDetail = await getBiddingCompaniesForPartners(bidding.biddingId, companyId)
- if (biddingDetail) {
- setSelectedBiddingForPreQuoteParticipation(biddingDetail)
- setIsPreQuoteParticipationDialogOpen(true)
- }
- } catch (error) {
- console.error('Failed to load bidding detail for participation:', error)
- }
- }, [companyId])
-
- // 사전견적 참여의사 결정 핸들러
+ // 입찰 참여의사 결정 핸들러
const handlePreQuoteParticipationDecision = React.useCallback(async (participate: boolean) => {
if (!selectedBiddingForPreQuoteParticipation?.biddingCompanyId) {
throw new Error('업체 정보를 찾을 수 없습니다.')
@@ -112,15 +97,15 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
router.push(`/partners/bid/${rowAction.row.original.biddingId}/pre-quote`)
break
case 'participation':
- // 사전견적 참여 의사 결정 다이얼로그 열기 - 상세 데이터 로드 필요
- loadBiddingDetailForParticipation(rowAction.row.original)
+ // 입찰 참여 의사 결정 다이얼로그 열기 - 상세 데이터 로드 필요
+ handlePreQuoteParticipationDecision(true)
setRowAction(null) // rowAction 초기화
break
default:
break
}
}
- }, [rowAction, router, loadBiddingDetailForParticipation])
+ }, [rowAction, router, handlePreQuoteParticipationDecision])
const columns = React.useMemo(
() => getPartnersBiddingListColumns({ setRowAction }),
@@ -234,7 +219,7 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
isAttending={rowAction?.row.original?.isAttendingMeeting || null}
onSuccess={refreshData}
/>
-
+{/*
<PartnersBiddingParticipationDialog
open={isParticipationDialogOpen}
onOpenChange={setIsParticipationDialogOpen}
@@ -244,19 +229,8 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
refreshData()
setSelectedBiddingForParticipation(null)
}}
- />
+ /> */}
- <VendorPreQuoteParticipationDialog
- open={isPreQuoteParticipationDialogOpen}
- onOpenChange={(open) => {
- setIsPreQuoteParticipationDialogOpen(open)
- if (!open) {
- setSelectedBiddingForPreQuoteParticipation(null)
- }
- }}
- biddingDetail={selectedBiddingForPreQuoteParticipation}
- onParticipationDecision={handlePreQuoteParticipationDecision}
- />
</>
)
}
diff --git a/lib/bidding/vendor/partners-bidding-pre-quote.tsx b/lib/bidding/vendor/partners-bidding-pre-quote.tsx
index 7b29b1a6..94b76f58 100644
--- a/lib/bidding/vendor/partners-bidding-pre-quote.tsx
+++ b/lib/bidding/vendor/partners-bidding-pre-quote.tsx
@@ -35,7 +35,7 @@ import {
import { getBiddingConditions } from '../service'
import { getPriceAdjustmentFormByBiddingCompanyId } from '../detail/service'
import { PrItemsPricingTable } from './components/pr-items-pricing-table'
-import { PreQuoteFileUpload } from './components/pre-quote-file-upload'
+import { SimpleFileUpload } from './components/simple-file-upload'
import {
biddingStatusLabels,
contractTypeLabels,
@@ -43,6 +43,7 @@ import {
} from '@/db/schema'
import { useToast } from '@/hooks/use-toast'
import { useTransition } from 'react'
+import { useSession } from 'next-auth/react'
interface PartnersBiddingPreQuoteProps {
biddingId: number
@@ -98,6 +99,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
const router = useRouter()
const { toast } = useToast()
const [isPending, startTransition] = useTransition()
+ const session = useSession()
const [biddingDetail, setBiddingDetail] = React.useState<BiddingDetail | null>(null)
const [isLoading, setIsLoading] = React.useState(true)
const [biddingConditions, setBiddingConditions] = React.useState<any | null>(null)
@@ -142,6 +144,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
adjustmentDate: '',
nonApplicableReason: '',
})
+ const userId = session.data?.user?.id || ''
// 데이터 로드
React.useEffect(() => {
@@ -627,11 +630,12 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
)}
{/* 견적 문서 업로드 섹션 */}
- {/* <PreQuoteFileUpload
+ <SimpleFileUpload
biddingId={biddingId}
companyId={companyId}
- readOnly={biddingDetail?.invitationStatus === 'submitted'}
- /> */}
+ userId={userId}
+ readOnly={false}
+ />
{/* 사전견적 폼 섹션 */}
<Card>
diff --git a/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx b/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx
index 324e21d1..c2fb6487 100644
--- a/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx
+++ b/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx
@@ -32,9 +32,7 @@ export function PartnersBiddingToolbarActions({
)
// 참여 의사 결정 버튼 활성화 조건 (sent 상태이고 아직 참여의사를 결정하지 않은 경우)
- const canDecideParticipation = selectedBidding &&
- selectedBidding.invitationStatus === 'sent' &&
- selectedBidding.isPreQuoteSelected === null
+ const canDecideParticipation = selectedBidding
const handleAttendanceClick = () => {
if (selectedBidding && setRowAction) {
diff --git a/lib/bidding/vendor/vendor-prequote-participation-dialog.tsx b/lib/bidding/vendor/vendor-prequote-participation-dialog.tsx
deleted file mode 100644
index c8098c3d..00000000
--- a/lib/bidding/vendor/vendor-prequote-participation-dialog.tsx
+++ /dev/null
@@ -1,268 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import { Button } from '@/components/ui/button'
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog'
-import { Badge } from '@/components/ui/badge'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { CheckCircle, XCircle, AlertCircle, Calendar, Package, Building2, User } from 'lucide-react'
-import { useToast } from '@/hooks/use-toast'
-import { useTransition } from 'react'
-import { formatDate } from '@/lib/utils'
-
-interface VendorPreQuoteParticipationDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- biddingDetail: any // BiddingDetail 타입
- onParticipationDecision: (participate: boolean) => Promise<void>
-}
-
-export function VendorPreQuoteParticipationDialog({
- open,
- onOpenChange,
- biddingDetail,
- onParticipationDecision
-}: VendorPreQuoteParticipationDialogProps) {
- const { toast } = useToast()
- const [isPending, startTransition] = useTransition()
- const [selectedDecision, setSelectedDecision] = React.useState<boolean | null>(null)
-
- const handleSubmit = () => {
- if (selectedDecision === null) {
- toast({
- title: '선택 필요',
- description: '사전견적 참여 여부를 선택해주세요.',
- variant: 'destructive',
- })
- return
- }
-
- startTransition(async () => {
- try {
- await onParticipationDecision(selectedDecision)
-
- toast({
- title: '완료',
- description: selectedDecision
- ? '사전견적 참여를 결정했습니다. 이제 견적서를 작성하실 수 있습니다.'
- : '사전견적 참여를 거절했습니다.',
- })
-
- setSelectedDecision(null)
- onOpenChange(false)
- } catch (error) {
- toast({
- title: '오류',
- description: '처리 중 오류가 발생했습니다.',
- variant: 'destructive',
- })
- }
- })
- }
-
- const handleOpenChange = (open: boolean) => {
- onOpenChange(open)
- if (!open) {
- setSelectedDecision(null)
- }
- }
-
- if (!biddingDetail) return null
-
- return (
- <Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogContent className="sm:max-w-[700px]">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <AlertCircle className="w-5 h-5 text-blue-600" />
- 사전견적 참여 의사 결정
- </DialogTitle>
- <DialogDescription>
- 다음 입찰건에 대한 사전견적 참여 여부를 결정해주세요.
- </DialogDescription>
- </DialogHeader>
-
- <div className="py-4 space-y-6">
- {/* 입찰 정보 카드 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-lg flex items-center gap-2">
- <Package className="w-5 h-5" />
- 입찰 상세 정보
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div>
- <strong className="text-gray-700">입찰번호:</strong>
- <div className="flex items-center gap-2 mt-1">
- <span className="font-mono">{biddingDetail.biddingNumber}</span>
- {biddingDetail.revision && biddingDetail.revision > 0 && (
- <Badge variant="outline">Rev.{biddingDetail.revision}</Badge>
- )}
- </div>
- </div>
-
- <div>
- <strong className="text-gray-700">프로젝트:</strong>
- <div className="flex items-center gap-2 mt-1">
- <Building2 className="w-4 h-4" />
- <span>{biddingDetail.projectName}</span>
- </div>
- </div>
-
- <div className="md:col-span-2">
- <strong className="text-gray-700">입찰명:</strong>
- <div className="mt-1">
- <span className="text-lg">{biddingDetail.title}</span>
- </div>
- </div>
-
- <div>
- <strong className="text-gray-700">품목명:</strong>
- <div className="mt-1">{biddingDetail.itemName}</div>
- </div>
-
- <div>
- <strong className="text-gray-700">담당자:</strong>
- <div className="flex items-center gap-2 mt-1">
- <User className="w-4 h-4" />
- <span>{biddingDetail.managerName}</span>
- </div>
- </div>
-
- {biddingDetail.preQuoteDate && (
- <div className="md:col-span-2">
- <strong className="text-gray-700">사전견적 마감일:</strong>
- <div className="flex items-center gap-2 mt-1">
- <Calendar className="w-4 h-4 text-red-500" />
- <span className="text-red-600 font-semibold">
- {formatDate(biddingDetail.preQuoteDate, 'KR')}
- </span>
- </div>
- </div>
- )}
-
- {biddingDetail.budget && (
- <div>
- <strong className="text-gray-700">예산:</strong>
- <div className="mt-1 font-mono">
- {biddingDetail.budget?.toLocaleString()} {biddingDetail.currency || 'KRW'}
- </div>
- </div>
- )}
- </div>
- </CardContent>
- </Card>
-
- {/* 참여 의사 선택 */}
- <div className="space-y-4">
- <h3 className="text-lg font-semibold text-gray-900">
- 사전견적에 참여하시겠습니까?
- </h3>
-
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- {/* 참여 */}
- <Card
- className={`cursor-pointer transition-all border-2 ${
- selectedDecision === true
- ? 'border-green-500 bg-green-50 shadow-md'
- : 'border-gray-200 hover:border-green-300 hover:shadow-sm'
- }`}
- onClick={() => setSelectedDecision(true)}
- >
- <CardContent className="p-6 text-center">
- <CheckCircle className="w-16 h-16 text-green-600 mx-auto mb-4" />
- <h4 className="text-xl font-semibold text-green-700 mb-2">
- 참여하겠습니다
- </h4>
- <p className="text-sm text-gray-600 leading-relaxed">
- 사전견적서를 작성하여 제출하겠습니다.<br/>
- 마감일까지 견적을 완료해주세요.
- </p>
- </CardContent>
- </Card>
-
- {/* 참여 안함 */}
- <Card
- className={`cursor-pointer transition-all border-2 ${
- selectedDecision === false
- ? 'border-red-500 bg-red-50 shadow-md'
- : 'border-gray-200 hover:border-red-300 hover:shadow-sm'
- }`}
- onClick={() => setSelectedDecision(false)}
- >
- <CardContent className="p-6 text-center">
- <XCircle className="w-16 h-16 text-red-600 mx-auto mb-4" />
- <h4 className="text-xl font-semibold text-red-700 mb-2">
- 참여하지 않겠습니다
- </h4>
- <p className="text-sm text-gray-600 leading-relaxed">
- 이번 사전견적에는 참여하지 않겠습니다.<br/>
- 다음 기회에 참여하겠습니다.
- </p>
- </CardContent>
- </Card>
- </div>
-
- {selectedDecision !== null && (
- <div className={`mt-4 p-4 rounded-lg border ${
- selectedDecision
- ? 'bg-green-50 border-green-200'
- : 'bg-red-50 border-red-200'
- }`}>
- <div className="flex items-center gap-2">
- {selectedDecision ? (
- <CheckCircle className="w-5 h-5 text-green-600" />
- ) : (
- <XCircle className="w-5 h-5 text-red-600" />
- )}
- <span className={`font-medium ${
- selectedDecision ? 'text-green-800' : 'text-red-800'
- }`}>
- {selectedDecision
- ? '사전견적 참여를 선택하셨습니다.'
- : '사전견적 참여를 거절하셨습니다.'
- }
- </span>
- </div>
- <p className={`text-sm mt-1 ${
- selectedDecision ? 'text-green-600' : 'text-red-600'
- }`}>
- {selectedDecision
- ? '확인을 누르시면 견적서 작성 화면으로 이동합니다.'
- : '확인을 누르시면 이 입찰건의 참여가 종료됩니다.'
- }
- </p>
- </div>
- )}
- </div>
- </div>
-
- <DialogFooter>
- <Button variant="outline" onClick={() => handleOpenChange(false)}>
- 취소
- </Button>
- <Button
- onClick={handleSubmit}
- disabled={isPending || selectedDecision === null}
- className={selectedDecision === true ? 'bg-green-600 hover:bg-green-700' :
- selectedDecision === false ? 'bg-red-600 hover:bg-red-700' : ''}
- >
- {selectedDecision === true && <CheckCircle className="w-4 h-4 mr-2" />}
- {selectedDecision === false && <XCircle className="w-4 h-4 mr-2" />}
- {selectedDecision === true ? '참여 확정' :
- selectedDecision === false ? '참여 거절' : '선택하세요'}
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
-}