summaryrefslogtreecommitdiff
path: root/components/bidding/manage/bidding-basic-info-editor.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/bidding/manage/bidding-basic-info-editor.tsx')
-rw-r--r--components/bidding/manage/bidding-basic-info-editor.tsx1407
1 files changed, 1407 insertions, 0 deletions
diff --git a/components/bidding/manage/bidding-basic-info-editor.tsx b/components/bidding/manage/bidding-basic-info-editor.tsx
new file mode 100644
index 00000000..d60c5d88
--- /dev/null
+++ b/components/bidding/manage/bidding-basic-info-editor.tsx
@@ -0,0 +1,1407 @@
+'use client'
+
+import * as React from 'react'
+import { useForm } from 'react-hook-form'
+import { ChevronRight, Upload, FileText, Eye, User, Building, Calendar, DollarSign } from 'lucide-react'
+import { toast } from 'sonner'
+
+import { Button } from '@/components/ui/button'
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import { Input } from '@/components/ui/input'
+import { Textarea } from '@/components/ui/textarea'
+import { Switch } from '@/components/ui/switch'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+// CreateBiddingInput 타입 정의가 없으므로 CreateBiddingSchema를 확장하여 사용합니다.
+import { getBiddingById, updateBiddingBasicInfo, getBiddingConditions, getBiddingNotice, updateBiddingConditions } from '@/lib/bidding/service'
+import { getBiddingNoticeTemplate } from '@/lib/bidding/service'
+import {
+ getIncotermsForSelection,
+ getPaymentTermsForSelection,
+ getPlaceOfShippingForSelection,
+ getPlaceOfDestinationForSelection,
+} from '@/lib/procurement-select/service'
+import { TAX_CONDITIONS } from '@/lib/tax-conditions/types'
+import { contractTypeLabels, biddingTypeLabels, awardCountLabels, biddingNoticeTypeLabels } from '@/db/schema'
+import TiptapEditor from '@/components/qna/tiptap-editor'
+import { PurchaseGroupCodeSelector } from '@/components/common/selectors/purchase-group-code'
+import { ProcurementManagerSelector } from '@/components/common/selectors/procurement-manager'
+import type { PurchaseGroupCodeWithUser } from '@/components/common/selectors/purchase-group-code/purchase-group-code-service'
+import type { ProcurementManagerWithUser } from '@/components/common/selectors/procurement-manager/procurement-manager-service'
+import { getBiddingDocuments, uploadBiddingDocument, deleteBiddingDocument } from '@/lib/bidding/detail/service'
+import { downloadFile } from '@/lib/file-download'
+
+// 입찰 기본 정보 에디터 컴포넌트
+interface BiddingBasicInfo {
+ title?: string
+ description?: string
+ content?: string
+ noticeType?: string
+ contractType?: string
+ biddingType?: string
+ biddingTypeCustom?: string
+ awardCount?: string
+ budget?: string
+ finalBidPrice?: string
+ targetPrice?: string
+ prNumber?: string
+ contractStartDate?: string
+ contractEndDate?: string
+ submissionStartDate?: string
+ submissionEndDate?: string
+ evaluationDate?: string
+ hasSpecificationMeeting?: boolean
+ hasPrDocument?: boolean
+ currency?: string
+ purchasingOrganization?: string
+ bidPicName?: string
+ bidPicCode?: string
+ supplyPicName?: string
+ supplyPicCode?: string
+ requesterName?: string
+ remarks?: string
+}
+
+interface BiddingBasicInfoEditorProps {
+ biddingId: number
+}
+
+interface UploadedDocument {
+ id: number
+ biddingId: number
+ companyId: number | null
+ documentType: string
+ fileName: string
+ originalFileName: string
+ fileSize: number | null
+ filePath: string
+ title: string | null
+ description: string | null
+ uploadedAt: string
+ uploadedBy: string
+}
+
+export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProps) {
+ const [isLoading, setIsLoading] = React.useState(true)
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
+ const [isLoadingTemplate, setIsLoadingTemplate] = React.useState(false)
+ const [noticeTemplate, setNoticeTemplate] = React.useState('')
+
+ // 첨부파일 관련 상태
+ const [shiAttachmentFiles, setShiAttachmentFiles] = React.useState<File[]>([])
+ const [vendorAttachmentFiles, setVendorAttachmentFiles] = React.useState<File[]>([])
+ const [existingDocuments, setExistingDocuments] = React.useState<UploadedDocument[]>([])
+ const [isLoadingDocuments, setIsLoadingDocuments] = React.useState(false)
+
+ // 담당자 selector 상태
+ const [selectedBidPic, setSelectedBidPic] = React.useState<PurchaseGroupCodeWithUser | undefined>(undefined)
+ const [selectedSupplyPic, setSelectedSupplyPic] = React.useState<ProcurementManagerWithUser | undefined>(undefined)
+
+ // 입찰 조건 관련 상태
+ const [biddingConditions, setBiddingConditions] = React.useState({
+ paymentTerms: '',
+ taxConditions: 'V1',
+ incoterms: 'DAP',
+ incotermsOption: '',
+ contractDeliveryDate: '',
+ shippingPort: '',
+ destinationPort: '',
+ isPriceAdjustmentApplicable: false,
+ sparePartOptions: '',
+ })
+
+ // Procurement 데이터 상태들
+ const [paymentTermsOptions, setPaymentTermsOptions] = React.useState<Array<{ code: string; description: string }>>([])
+ const [incotermsOptions, setIncotermsOptions] = React.useState<Array<{ code: string; description: string }>>([])
+ const [shippingPlaces, setShippingPlaces] = React.useState<Array<{ code: string; description: string }>>([])
+ const [destinationPlaces, setDestinationPlaces] = React.useState<Array<{ code: string; description: string }>>([])
+
+ const form = useForm<BiddingBasicInfo>({
+ defaultValues: {}
+ })
+
+
+ // 공고 템플릿 로드 - 현재 저장된 템플릿 우선
+ const loadNoticeTemplate = React.useCallback(async (noticeType?: string) => {
+ setIsLoadingTemplate(true)
+ try {
+ // 먼저 현재 입찰에 저장된 템플릿이 있는지 확인
+ const savedNotice = await getBiddingNotice(biddingId)
+ if (savedNotice && savedNotice.content) {
+ setNoticeTemplate(savedNotice.content)
+ const currentContent = form.getValues('content')
+ if (!currentContent || currentContent.trim() === '') {
+ form.setValue('content', savedNotice.content)
+ }
+ setIsLoadingTemplate(false)
+ return
+ }
+
+ // 저장된 템플릿이 없으면 타입별 템플릿 로드
+ if (noticeType) {
+ const template = await getBiddingNoticeTemplate(noticeType)
+ if (template) {
+ setNoticeTemplate(template.content || '')
+ const currentContent = form.getValues('content')
+ if (!currentContent || currentContent.trim() === '') {
+ form.setValue('content', template.content || '')
+ }
+ } else {
+ // 템플릿이 없으면 표준 템플릿 사용
+ const defaultTemplate = await getBiddingNoticeTemplate('standard')
+ if (defaultTemplate) {
+ setNoticeTemplate(defaultTemplate.content)
+ const currentContent = form.getValues('content')
+ if (!currentContent || currentContent.trim() === '') {
+ form.setValue('content', defaultTemplate.content)
+ }
+ }
+ }
+ }
+ } catch (error) {
+ console.warn('Failed to load notice template:', error)
+ } finally {
+ setIsLoadingTemplate(false)
+ }
+ }, [biddingId, form])
+
+ // 데이터 로딩
+ React.useEffect(() => {
+ const loadBiddingData = async () => {
+ setIsLoading(true)
+ try {
+ const bidding = await getBiddingById(biddingId)
+ if (bidding) {
+ // 타입 확장된 bidding 객체
+ const biddingExtended = bidding as typeof bidding & {
+ content?: string | null
+ noticeType?: string | null
+ biddingTypeCustom?: string | null
+ awardCount?: string | null
+ requesterName?: string | null
+ }
+
+ // 날짜를 문자열로 변환하는 헬퍼
+ const formatDate = (date: unknown): string => {
+ if (!date) return ''
+ if (typeof date === 'string') return date.split('T')[0]
+ if (date instanceof Date) return date.toISOString().split('T')[0]
+ return ''
+ }
+
+ const formatDateTime = (date: unknown): string => {
+ if (!date) return ''
+ if (typeof date === 'string') return date.slice(0, 16)
+ if (date instanceof Date) return date.toISOString().slice(0, 16)
+ return ''
+ }
+
+ // 폼 데이터 설정
+ form.reset({
+ title: bidding.title || '',
+ description: bidding.description || '',
+ content: biddingExtended.content || '',
+ noticeType: biddingExtended.noticeType || '',
+ contractType: bidding.contractType || '',
+ biddingType: bidding.biddingType || '',
+ biddingTypeCustom: biddingExtended.biddingTypeCustom || '',
+ awardCount: biddingExtended.awardCount || (bidding.awardCount ? String(bidding.awardCount) : ''),
+ budget: bidding.budget ? bidding.budget.toString() : '',
+ finalBidPrice: bidding.finalBidPrice ? bidding.finalBidPrice.toString() : '',
+ targetPrice: bidding.targetPrice ? bidding.targetPrice.toString() : '',
+ prNumber: bidding.prNumber || '',
+ contractStartDate: formatDate(bidding.contractStartDate),
+ contractEndDate: formatDate(bidding.contractEndDate),
+ submissionStartDate: formatDateTime(bidding.submissionStartDate),
+ submissionEndDate: formatDateTime(bidding.submissionEndDate),
+ evaluationDate: formatDateTime(bidding.evaluationDate),
+ hasSpecificationMeeting: bidding.hasSpecificationMeeting || false,
+ hasPrDocument: bidding.hasPrDocument || false,
+ currency: bidding.currency || 'KRW',
+ purchasingOrganization: bidding.purchasingOrganization || '',
+ bidPicName: bidding.bidPicName || '',
+ bidPicCode: bidding.bidPicCode || '',
+ supplyPicName: bidding.supplyPicName || '',
+ supplyPicCode: bidding.supplyPicCode || '',
+ requesterName: biddingExtended.requesterName || '',
+ remarks: bidding.remarks || '',
+ })
+
+ // 입찰 조건 로드
+ const conditions = await getBiddingConditions(biddingId)
+ if (conditions) {
+ setBiddingConditions({
+ paymentTerms: conditions.paymentTerms || '',
+ taxConditions: conditions.taxConditions || 'V1',
+ incoterms: conditions.incoterms || 'DAP',
+ incotermsOption: conditions.incotermsOption || '',
+ contractDeliveryDate: conditions.contractDeliveryDate
+ ? new Date(conditions.contractDeliveryDate).toISOString().split('T')[0]
+ : '',
+ shippingPort: conditions.shippingPort || '',
+ destinationPort: conditions.destinationPort || '',
+ isPriceAdjustmentApplicable: conditions.isPriceAdjustmentApplicable || false,
+ sparePartOptions: conditions.sparePartOptions || '',
+ })
+ }
+
+ // Procurement 데이터 로드
+ const [paymentTermsData, incotermsData, shippingData, destinationData] = await Promise.all([
+ getPaymentTermsForSelection().catch(() => []),
+ getIncotermsForSelection().catch(() => []),
+ getPlaceOfShippingForSelection().catch(() => []),
+ getPlaceOfDestinationForSelection().catch(() => []),
+ ])
+ setPaymentTermsOptions(paymentTermsData)
+ setIncotermsOptions(incotermsData)
+ setShippingPlaces(shippingData)
+ setDestinationPlaces(destinationData)
+
+ // 공고 템플릿 로드
+ await loadNoticeTemplate(biddingExtended.noticeType || undefined)
+ } else {
+ toast.error('입찰 정보를 찾을 수 없습니다.')
+ }
+ } catch (error) {
+ console.error('Error loading bidding data:', error)
+ toast.error('입찰 정보를 불러오는데 실패했습니다.')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ loadBiddingData()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [biddingId, loadNoticeTemplate])
+
+ // 구매유형 변경 시 템플릿 자동 로드
+ const noticeTypeValue = form.watch('noticeType')
+ React.useEffect(() => {
+ if (noticeTypeValue) {
+ loadNoticeTemplate(noticeTypeValue)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [noticeTypeValue])
+
+ // 기존 첨부파일 로드
+ const loadExistingDocuments = async () => {
+ setIsLoadingDocuments(true)
+ try {
+ const docs = await getBiddingDocuments(biddingId)
+ const mappedDocs = docs.map((doc) => ({
+ ...doc,
+ uploadedAt: doc.uploadedAt?.toString() || '',
+ uploadedBy: doc.uploadedBy || ''
+ }))
+ setExistingDocuments(mappedDocs)
+ } catch (error) {
+ console.error('Failed to load documents:', error)
+ toast.error('첨부파일 목록을 불러오는데 실패했습니다.')
+ } finally {
+ setIsLoadingDocuments(false)
+ }
+ }
+
+ // 초기 로드 시 첨부파일도 함께 로드
+ React.useEffect(() => {
+ if (biddingId) {
+ loadExistingDocuments()
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [biddingId])
+
+ // SHI용 파일 첨부 핸들러
+ const handleShiFileUpload = async (files: File[]) => {
+ try {
+ // 파일을 업로드하고 기존 문서 목록 갱신
+ for (const file of files) {
+ const result = await uploadBiddingDocument(
+ biddingId,
+ file,
+ 'bid_attachment',
+ file.name,
+ 'SHI용 첨부파일',
+ '1' // TODO: 실제 사용자 ID 가져오기
+ )
+ if (result.success) {
+ toast.success(`${file.name} 업로드 완료`)
+ }
+ }
+ await loadExistingDocuments()
+ setShiAttachmentFiles([])
+ } catch (error) {
+ console.error('Failed to upload SHI files:', error)
+ toast.error('파일 업로드에 실패했습니다.')
+ }
+ }
+
+ const removeShiFile = (index: number) => {
+ setShiAttachmentFiles(prev => prev.filter((_, i) => i !== index))
+ }
+
+ // 협력업체용 파일 첨부 핸들러
+ const handleVendorFileUpload = async (files: File[]) => {
+ try {
+ // 파일을 업로드하고 기존 문서 목록 갱신
+ for (const file of files) {
+ const result = await uploadBiddingDocument(
+ biddingId,
+ file,
+ 'bid_attachment',
+ file.name,
+ '협력업체용 첨부파일',
+ '1' // TODO: 실제 사용자 ID 가져오기
+ )
+ if (result.success) {
+ toast.success(`${file.name} 업로드 완료`)
+ }
+ }
+ await loadExistingDocuments()
+ setVendorAttachmentFiles([])
+ } catch (error) {
+ console.error('Failed to upload vendor files:', error)
+ toast.error('파일 업로드에 실패했습니다.')
+ }
+ }
+
+ const removeVendorFile = (index: number) => {
+ setVendorAttachmentFiles(prev => prev.filter((_, i) => i !== index))
+ }
+
+ // 파일 삭제
+ const handleDeleteDocument = async (documentId: number) => {
+ if (!confirm('이 파일을 삭제하시겠습니까?')) {
+ return
+ }
+
+ try {
+ const result = await deleteBiddingDocument(documentId, biddingId, '1') // TODO: 실제 사용자 ID 가져오기
+ if (result.success) {
+ toast.success('파일이 삭제되었습니다.')
+ await loadExistingDocuments()
+ } else {
+ toast.error(result.error || '파일 삭제에 실패했습니다.')
+ }
+ } catch (error) {
+ console.error('Failed to delete document:', error)
+ toast.error('파일 삭제에 실패했습니다.')
+ }
+ }
+
+ // 파일 다운로드
+ const handleDownloadDocument = async (document: UploadedDocument) => {
+ try {
+ await downloadFile(document.filePath, document.originalFileName, {
+ showToast: true
+ })
+ } catch (error) {
+ console.error('Failed to download document:', error)
+ toast.error('파일 다운로드에 실패했습니다.')
+ }
+ }
+
+ // 입찰담당자 선택 핸들러
+ const handleBidPicSelect = (code: PurchaseGroupCodeWithUser) => {
+ setSelectedBidPic(code)
+ form.setValue('bidPicName', code.DISPLAY_NAME || '')
+ form.setValue('bidPicCode', code.PURCHASE_GROUP_CODE || '')
+ }
+
+ // 조달담당자 선택 핸들러
+ const handleSupplyPicSelect = (manager: ProcurementManagerWithUser) => {
+ setSelectedSupplyPic(manager)
+ form.setValue('supplyPicName', manager.DISPLAY_NAME || '')
+ form.setValue('supplyPicCode', manager.PROCUREMENT_MANAGER_CODE || '')
+ }
+
+ // 파일 크기 포맷팅
+ 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]
+ }
+
+ // 저장 처리
+ const handleSave = async (data: BiddingBasicInfo) => {
+ setIsSubmitting(true)
+ try {
+ // 기본 정보 저장
+ const result = await updateBiddingBasicInfo(biddingId, data, '1') // TODO: 실제 사용자 ID 가져오기
+
+ if (result.success) {
+ // 입찰 조건 저장
+ const conditionsResult = await updateBiddingConditions(biddingId, {
+ paymentTerms: biddingConditions.paymentTerms,
+ taxConditions: biddingConditions.taxConditions,
+ incoterms: biddingConditions.incoterms,
+ incotermsOption: biddingConditions.incotermsOption,
+ contractDeliveryDate: biddingConditions.contractDeliveryDate || undefined,
+ shippingPort: biddingConditions.shippingPort,
+ destinationPort: biddingConditions.destinationPort,
+ isPriceAdjustmentApplicable: biddingConditions.isPriceAdjustmentApplicable,
+ sparePartOptions: biddingConditions.sparePartOptions,
+ })
+
+ if (conditionsResult.success) {
+ toast.success('입찰 기본 정보와 조건이 성공적으로 저장되었습니다.')
+ } else {
+ const errorMessage = 'error' in conditionsResult ? conditionsResult.error : '입찰 조건 저장에 실패했습니다.'
+ toast.error(errorMessage)
+ }
+ } else {
+ const errorMessage = 'error' in result ? result.error : '입찰 기본 정보 저장에 실패했습니다.'
+ toast.error(errorMessage)
+ }
+ } catch (error) {
+ console.error('Failed to save bidding basic info:', error)
+ toast.error('입찰 기본 정보 저장에 실패했습니다.')
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ if (isLoading) {
+ return (
+ <div className="flex items-center justify-center p-8">
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
+ <span className="ml-2">입찰 정보를 불러오는 중...</span>
+ </div>
+ )
+ }
+
+ return (
+ <div className="space-y-6">
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="h-5 w-5" />
+ 입찰 기본 정보
+ </CardTitle>
+ <p className="text-sm text-muted-foreground">
+ 입찰의 기본 정보를 수정할 수 있습니다.
+ </p>
+ </CardHeader>
+ <CardContent>
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(handleSave)} className="space-y-4">
+ {/* 1행: 입찰명, PR번호, 입찰유형, 계약구분 */}
+ <div className="grid grid-cols-4 gap-4">
+ <FormField control={form.control} name="title" render={({ field }) => (
+ <FormItem>
+ <FormLabel>입찰명 <span className="text-red-500">*</span></FormLabel>
+ <FormControl>
+ <Input placeholder="입찰명을 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ <FormField control={form.control} name="prNumber" render={({ field }) => (
+ <FormItem>
+ <FormLabel>PR 번호</FormLabel>
+ <FormControl>
+ <Input placeholder="PR 번호를 입력하세요" {...field} readOnly className="bg-muted" />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ <FormField control={form.control} name="biddingType" render={({ field }) => (
+ <FormItem>
+ <FormLabel>입찰유형</FormLabel>
+ <Select onValueChange={field.onChange} value={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="입찰유형 선택" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {Object.entries(biddingTypeLabels).map(([value, label]) => (
+ <SelectItem key={value} value={value}>
+ {label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ <FormField control={form.control} name="contractType" render={({ field }) => (
+ <FormItem>
+ <FormLabel>계약구분</FormLabel>
+ <Select onValueChange={field.onChange} value={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="계약구분 선택" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {Object.entries(contractTypeLabels).map(([value, label]) => (
+ <SelectItem key={value} value={value}>
+ {label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )} />
+ </div>
+
+ {/* 기타 입찰유형 선택 시 직접입력 필드 */}
+ {form.watch('biddingType') === 'other' && (
+ <div className="grid grid-cols-4 gap-4">
+ <div></div>
+ <div></div>
+ <FormField control={form.control} name="biddingTypeCustom" render={({ field }) => (
+ <FormItem>
+ <FormLabel>기타 입찰유형 <span className="text-red-500">*</span></FormLabel>
+ <FormControl>
+ <Input placeholder="직접 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+ <div></div>
+ </div>
+ )}
+
+ {/* 2행: 예산, 실적가, 내정가, 낙찰수 */}
+ <div className="grid grid-cols-4 gap-4">
+ <FormField control={form.control} name="budget" render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ <DollarSign className="h-3 w-3" />
+ 예산
+ </FormLabel>
+ <FormControl>
+ <Input placeholder="예산 입력" {...field} readOnly className="bg-muted" />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ <FormField control={form.control} name="finalBidPrice" render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ <DollarSign className="h-3 w-3" />
+ 실적가
+ </FormLabel>
+ <FormControl>
+ <Input placeholder="실적가" {...field} readOnly className="bg-muted" />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ <FormField control={form.control} name="targetPrice" render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ <Eye className="h-3 w-3" />
+ 내정가
+ </FormLabel>
+ <FormControl>
+ <Input placeholder="내정가" {...field} readOnly className="bg-muted" />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ <FormField control={form.control} name="awardCount" render={({ field }) => (
+ <FormItem>
+ <FormLabel>낙찰수</FormLabel>
+ <Select onValueChange={field.onChange} value={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="낙찰수 선택" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {Object.entries(awardCountLabels).map(([value, label]) => (
+ <SelectItem key={value} value={value}>
+ {label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )} />
+ </div>
+
+ {/* 3행: 입찰담당자, 조달담당자, 구매조직, 통화 */}
+ <div className="grid grid-cols-4 gap-4">
+ <FormField control={form.control} name="bidPicName" render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ <User className="h-3 w-3" />
+ 입찰담당자
+ </FormLabel>
+ <FormControl>
+ <PurchaseGroupCodeSelector
+ selectedCode={selectedBidPic}
+ onCodeSelect={(code) => {
+ handleBidPicSelect(code)
+ field.onChange(code.DISPLAY_NAME || '')
+ }}
+ placeholder="입찰담당자 선택"
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ <FormField control={form.control} name="supplyPicName" render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ <User className="h-3 w-3" />
+ 조달담당자
+ </FormLabel>
+ <FormControl>
+ <ProcurementManagerSelector
+ selectedManager={selectedSupplyPic}
+ onManagerSelect={(manager) => {
+ handleSupplyPicSelect(manager)
+ field.onChange(manager.DISPLAY_NAME || '')
+ }}
+ placeholder="조달담당자 선택"
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ <FormField control={form.control} name="purchasingOrganization" render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ <Building className="h-3 w-3" />
+ 구매조직
+ </FormLabel>
+ <Select onValueChange={field.onChange} value={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="구매조직 선택" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="조선">조선</SelectItem>
+ <SelectItem value="해양">해양</SelectItem>
+ <SelectItem value="기타">기타</SelectItem>
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ <FormField control={form.control} name="currency" render={({ field }) => (
+ <FormItem>
+ <FormLabel>통화</FormLabel>
+ <Select onValueChange={field.onChange} value={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="통화 선택" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="KRW">KRW (원)</SelectItem>
+ <SelectItem value="USD">USD (달러)</SelectItem>
+ <SelectItem value="EUR">EUR (유로)</SelectItem>
+ <SelectItem value="JPY">JPY (엔)</SelectItem>
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )} />
+ </div>
+
+ {/* 구매유형 필드 추가 */}
+ <div className="grid grid-cols-4 gap-4">
+ <FormField control={form.control} name="noticeType" render={({ field }) => (
+ <FormItem>
+ <FormLabel>구매유형 <span className="text-red-500">*</span></FormLabel>
+ <Select onValueChange={field.onChange} value={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="구매유형 선택" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {Object.entries(biddingNoticeTypeLabels).map(([value, label]) => (
+ <SelectItem key={value} value={value}>
+ {label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )} />
+ <div></div>
+ <div></div>
+ <div></div>
+ </div>
+
+ {/* 4행: 계약기간 시작/종료, 입찰서 제출 시작/마감 */}
+ <div className="grid grid-cols-4 gap-4">
+ <FormField control={form.control} name="contractStartDate" render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ <Calendar className="h-3 w-3" />
+ 계약기간 시작
+ </FormLabel>
+ <FormControl>
+ <Input type="date" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ <FormField control={form.control} name="contractEndDate" render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ <Calendar className="h-3 w-3" />
+ 계약기간 종료
+ </FormLabel>
+ <FormControl>
+ <Input type="date" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ {/* <FormField control={form.control} name="submissionStartDate" render={({ field }) => (
+ <FormItem>
+ <FormLabel>입찰서 제출 시작</FormLabel>
+ <FormControl>
+ <Input type="datetime-local" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ <FormField control={form.control} name="submissionEndDate" render={({ field }) => (
+ <FormItem>
+ <FormLabel>입찰서 제출 마감</FormLabel>
+ <FormControl>
+ <Input type="datetime-local" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} /> */}
+ </div>
+
+ {/* 5행: 개찰 일시, 사양설명회, PR문서 */}
+ {/* <div className="grid grid-cols-3 gap-4">
+ <FormField control={form.control} name="evaluationDate" render={({ field }) => (
+ <FormItem>
+ <FormLabel>개찰 일시</FormLabel>
+ <FormControl>
+ <Input type="datetime-local" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} /> */}
+
+ {/* <FormField control={form.control} name="hasSpecificationMeeting" render={({ field }) => (
+ <FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
+ <div className="space-y-0.5">
+ <FormLabel className="text-base">사양설명회</FormLabel>
+ <div className="text-sm text-muted-foreground">
+ 사양설명회가 필요한 경우 체크
+ </div>
+ </div>
+ <FormControl>
+ <Switch checked={field.value} onCheckedChange={field.onChange} />
+ </FormControl>
+ </FormItem>
+ )} /> */}
+
+ {/* <FormField control={form.control} name="hasPrDocument" render={({ field }) => (
+ <FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
+ <div className="space-y-0.5">
+ <FormLabel className="text-base">PR 문서</FormLabel>
+ <div className="text-sm text-muted-foreground">
+ PR 문서가 있는 경우 체크
+ </div>
+ </div>
+ <FormControl>
+ <Switch checked={field.value} onCheckedChange={field.onChange} />
+ </FormControl>
+ </FormItem>
+ )} /> */}
+ {/* </div> */}
+
+ {/* 입찰개요 */}
+ <div className="pt-2">
+ <FormField control={form.control} name="description" render={({ field }) => (
+ <FormItem>
+ <FormLabel>입찰개요</FormLabel>
+ <FormControl>
+ <Textarea placeholder="입찰에 대한 설명을 입력하세요" rows={2} {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+ </div>
+
+ {/* 비고 */}
+ <div className="pt-2">
+ <FormField control={form.control} name="remarks" render={({ field }) => (
+ <FormItem>
+ <FormLabel>비고</FormLabel>
+ <FormControl>
+ <Textarea placeholder="추가 사항이나 참고사항을 입력하세요" rows={3} {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+ </div>
+
+ {/* 입찰 조건 */}
+ <div className="pt-4 border-t">
+ <CardTitle className="text-lg mb-4">입찰 조건</CardTitle>
+
+ {/* 1행: SHI 지급조건, SHI 매입부가가치세 */}
+ <div className="grid grid-cols-2 gap-4 mb-4">
+ <div>
+ <FormLabel>SHI 지급조건 <span className="text-red-500">*</span></FormLabel>
+ <Select
+ value={biddingConditions.paymentTerms}
+ onValueChange={(value) => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ paymentTerms: value
+ }))
+ }}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="지급조건 선택" />
+ </SelectTrigger>
+ <SelectContent>
+ {paymentTermsOptions.length > 0 ? (
+ paymentTermsOptions.map((option) => (
+ <SelectItem key={option.code} value={option.code}>
+ {option.code} {option.description && `(${option.description})`}
+ </SelectItem>
+ ))
+ ) : (
+ <SelectItem value="loading" disabled>
+ 데이터를 불러오는 중...
+ </SelectItem>
+ )}
+ </SelectContent>
+ </Select>
+ </div>
+
+ <div>
+ <FormLabel>SHI 매입부가가치세 <span className="text-red-500">*</span></FormLabel>
+ <Select
+ value={biddingConditions.taxConditions}
+ onValueChange={(value) => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ taxConditions: value
+ }))
+ }}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="세금조건 선택" />
+ </SelectTrigger>
+ <SelectContent>
+ {TAX_CONDITIONS.map((condition) => (
+ <SelectItem key={condition.code} value={condition.code}>
+ {condition.name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+
+ {/* 2행: SHI 인도조건, SHI 인도조건2 */}
+ <div className="grid grid-cols-2 gap-4 mb-4">
+ <div>
+ <FormLabel>SHI 인도조건 <span className="text-red-500">*</span></FormLabel>
+ <Select
+ value={biddingConditions.incoterms}
+ onValueChange={(value) => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ incoterms: value
+ }))
+ }}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="인코텀즈 선택" />
+ </SelectTrigger>
+ <SelectContent>
+ {incotermsOptions.length > 0 ? (
+ incotermsOptions.map((option) => (
+ <SelectItem key={option.code} value={option.code}>
+ {option.code} {option.description && `(${option.description})`}
+ </SelectItem>
+ ))
+ ) : (
+ <SelectItem value="loading" disabled>
+ 데이터를 불러오는 중...
+ </SelectItem>
+ )}
+ </SelectContent>
+ </Select>
+ </div>
+
+ <div>
+ <FormLabel>SHI 인도조건2</FormLabel>
+ <Input
+ placeholder="인도조건 추가 정보"
+ value={biddingConditions.incotermsOption}
+ onChange={(e) => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ incotermsOption: e.target.value
+ }))
+ }}
+ />
+ </div>
+ </div>
+
+ {/* 3행: SHI 선적지, SHI 하역지 */}
+ <div className="grid grid-cols-2 gap-4 mb-4">
+ <div>
+ <FormLabel>SHI 선적지</FormLabel>
+ <Select
+ value={biddingConditions.shippingPort}
+ onValueChange={(value) => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ shippingPort: value
+ }))
+ }}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="선적지 선택" />
+ </SelectTrigger>
+ <SelectContent>
+ {shippingPlaces.length > 0 ? (
+ shippingPlaces.map((place) => (
+ <SelectItem key={place.code} value={place.code}>
+ {place.code} {place.description && `(${place.description})`}
+ </SelectItem>
+ ))
+ ) : (
+ <SelectItem value="loading" disabled>
+ 데이터를 불러오는 중...
+ </SelectItem>
+ )}
+ </SelectContent>
+ </Select>
+ </div>
+
+ <div>
+ <FormLabel>SHI 하역지</FormLabel>
+ <Select
+ value={biddingConditions.destinationPort}
+ onValueChange={(value) => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ destinationPort: value
+ }))
+ }}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="하역지 선택" />
+ </SelectTrigger>
+ <SelectContent>
+ {destinationPlaces.length > 0 ? (
+ destinationPlaces.map((place) => (
+ <SelectItem key={place.code} value={place.code}>
+ {place.code} {place.description && `(${place.description})`}
+ </SelectItem>
+ ))
+ ) : (
+ <SelectItem value="loading" disabled>
+ 데이터를 불러오는 중...
+ </SelectItem>
+ )}
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+
+ {/* 4행: 계약 납품일, 연동제 적용 가능 */}
+ <div className="grid grid-cols-2 gap-4 mb-4">
+ <div>
+ <FormLabel>계약 납품일</FormLabel>
+ <Input
+ type="date"
+ value={biddingConditions.contractDeliveryDate}
+ onChange={(e) => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ contractDeliveryDate: e.target.value
+ }))
+ }}
+ />
+ </div>
+
+ <div className="flex flex-row items-center justify-between rounded-lg border p-3">
+ <div className="space-y-0.5">
+ <FormLabel className="text-base">연동제 적용 가능</FormLabel>
+ <div className="text-sm text-muted-foreground">
+ 연동제 적용 요건 여부
+ </div>
+ </div>
+ <Switch
+ checked={biddingConditions.isPriceAdjustmentApplicable}
+ onCheckedChange={(checked) => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ isPriceAdjustmentApplicable: checked
+ }))
+ }}
+ />
+ </div>
+ </div>
+
+ {/* 5행: 스페어파트 옵션 */}
+ <div className="mb-4">
+ <div>
+ <FormLabel>스페어파트 옵션</FormLabel>
+ <Textarea
+ placeholder="스페어파트 관련 옵션을 입력하세요"
+ value={biddingConditions.sparePartOptions}
+ onChange={(e) => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ sparePartOptions: e.target.value
+ }))
+ }}
+ rows={3}
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* 입찰공고 내용 */}
+ <div className="pt-4 border-t">
+ <CardTitle className="text-lg mb-4">입찰공고 내용</CardTitle>
+ <FormField control={form.control} name="content" render={({ field }) => (
+ <FormItem>
+ <FormControl>
+ <div className="border rounded-lg">
+ <TiptapEditor
+ content={field.value || noticeTemplate}
+ setContent={field.onChange}
+ disabled={isLoadingTemplate}
+ height="300px"
+ />
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )} />
+
+ {isLoadingTemplate && (
+ <div className="flex items-center justify-center p-4 text-sm text-muted-foreground">
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2"></div>
+ 입찰공고 템플릿을 불러오는 중...
+ </div>
+ )}
+ </div>
+
+ {/* 액션 버튼 */}
+ <div className="flex justify-end gap-4 pt-4">
+ <Button type="submit" disabled={isSubmitting} className="flex items-center gap-2">
+ {isSubmitting ? '저장 중...' : '저장'}
+ <ChevronRight className="h-4 w-4" />
+ </Button>
+ </div>
+ </form>
+ </Form>
+ </CardContent>
+ </Card>
+
+ {/* SHI용 첨부파일 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Upload className="h-5 w-5" />
+ SHI용 첨부파일
+ </CardTitle>
+ <p className="text-sm text-muted-foreground">
+ SHI에서 제공하는 문서나 파일을 업로드하세요
+ </p>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="border-2 border-dashed border-gray-300 rounded-lg p-6">
+ <div className="text-center">
+ <Upload className="h-12 w-12 text-gray-400 mx-auto mb-4" />
+ <div className="space-y-2">
+ <p className="text-sm text-gray-600">
+ 파일을 드래그 앤 드롭하거나{' '}
+ <label className="text-blue-600 hover:text-blue-500 cursor-pointer">
+ <input
+ type="file"
+ multiple
+ className="hidden"
+ onChange={(e) => {
+ const files = Array.from(e.target.files || [])
+ setShiAttachmentFiles(prev => [...prev, ...files])
+ }}
+ accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg"
+ />
+ 찾아보세요
+ </label>
+ </p>
+ <p className="text-xs text-gray-500">
+ PDF, DOC, DOCX, XLS, XLSX, PNG, JPG, JPEG 파일을 업로드할 수 있습니다
+ </p>
+ </div>
+ </div>
+ </div>
+
+ {shiAttachmentFiles.length > 0 && (
+ <div className="space-y-2">
+ <h4 className="text-sm font-medium">업로드 예정 파일</h4>
+ <div className="space-y-2">
+ {shiAttachmentFiles.map((file, index) => (
+ <div
+ key={index}
+ className="flex items-center justify-between p-3 bg-muted rounded-lg"
+ >
+ <div className="flex items-center gap-3">
+ <FileText className="h-4 w-4 text-muted-foreground" />
+ <div>
+ <p className="text-sm font-medium">{file.name}</p>
+ <p className="text-xs text-muted-foreground">
+ {(file.size / 1024 / 1024).toFixed(2)} MB
+ </p>
+ </div>
+ </div>
+ <div className="flex gap-2">
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => {
+ handleShiFileUpload([file])
+ }}
+ >
+ 업로드
+ </Button>
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => removeShiFile(index)}
+ >
+ 제거
+ </Button>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {/* 기존 문서 목록 */}
+ {isLoadingDocuments ? (
+ <div className="flex items-center justify-center p-4 text-sm text-muted-foreground">
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2"></div>
+ 문서 목록을 불러오는 중...
+ </div>
+ ) : existingDocuments.length > 0 && (
+ <div className="space-y-2">
+ <h4 className="text-sm font-medium">업로드된 문서</h4>
+ <div className="space-y-2">
+ {existingDocuments
+ .filter(doc => doc.description?.includes('SHI용') || doc.title?.includes('SHI'))
+ .map((doc) => (
+ <div
+ key={doc.id}
+ className="flex items-center justify-between p-3 bg-muted rounded-lg"
+ >
+ <div className="flex items-center gap-3">
+ <FileText className="h-4 w-4 text-muted-foreground" />
+ <div>
+ <p className="text-sm font-medium">{doc.originalFileName}</p>
+ <p className="text-xs text-muted-foreground">
+ {formatFileSize(doc.fileSize)} • {new Date(doc.uploadedAt).toLocaleDateString('ko-KR')}
+ </p>
+ </div>
+ </div>
+ <div className="flex gap-2">
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => handleDownloadDocument(doc)}
+ >
+ 다운로드
+ </Button>
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => handleDeleteDocument(doc.id)}
+ >
+ 삭제
+ </Button>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 협력업체용 첨부파일 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Upload className="h-5 w-5" />
+ 협력업체용 첨부파일
+ </CardTitle>
+ <p className="text-sm text-muted-foreground">
+ 협력업체에서 제공하는 문서나 파일을 업로드하세요
+ </p>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="border-2 border-dashed border-gray-300 rounded-lg p-6">
+ <div className="text-center">
+ <Upload className="h-12 w-12 text-gray-400 mx-auto mb-4" />
+ <div className="space-y-2">
+ <p className="text-sm text-gray-600">
+ 파일을 드래그 앤 드롭하거나{' '}
+ <label className="text-blue-600 hover:text-blue-500 cursor-pointer">
+ <input
+ type="file"
+ multiple
+ className="hidden"
+ onChange={(e) => {
+ const files = Array.from(e.target.files || [])
+ setVendorAttachmentFiles(prev => [...prev, ...files])
+ }}
+ accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg"
+ />
+ 찾아보세요
+ </label>
+ </p>
+ <p className="text-xs text-gray-500">
+ PDF, DOC, DOCX, XLS, XLSX, PNG, JPG, JPEG 파일을 업로드할 수 있습니다
+ </p>
+ </div>
+ </div>
+ </div>
+
+ {vendorAttachmentFiles.length > 0 && (
+ <div className="space-y-2">
+ <h4 className="text-sm font-medium">업로드 예정 파일</h4>
+ <div className="space-y-2">
+ {vendorAttachmentFiles.map((file, index) => (
+ <div
+ key={index}
+ className="flex items-center justify-between p-3 bg-muted rounded-lg"
+ >
+ <div className="flex items-center gap-3">
+ <FileText className="h-4 w-4 text-muted-foreground" />
+ <div>
+ <p className="text-sm font-medium">{file.name}</p>
+ <p className="text-xs text-muted-foreground">
+ {(file.size / 1024 / 1024).toFixed(2)} MB
+ </p>
+ </div>
+ </div>
+ <div className="flex gap-2">
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => {
+ handleVendorFileUpload([file])
+ }}
+ >
+ 업로드
+ </Button>
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => removeVendorFile(index)}
+ >
+ 제거
+ </Button>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {/* 기존 문서 목록 */}
+ {existingDocuments.length > 0 && (
+ <div className="space-y-2">
+ <h4 className="text-sm font-medium">업로드된 문서</h4>
+ <div className="space-y-2">
+ {existingDocuments
+ .filter(doc => doc.description?.includes('협력업체용') || !doc.description?.includes('SHI용'))
+ .map((doc) => (
+ <div
+ key={doc.id}
+ className="flex items-center justify-between p-3 bg-muted rounded-lg"
+ >
+ <div className="flex items-center gap-3">
+ <FileText className="h-4 w-4 text-muted-foreground" />
+ <div>
+ <p className="text-sm font-medium">{doc.originalFileName}</p>
+ <p className="text-xs text-muted-foreground">
+ {formatFileSize(doc.fileSize)} • {new Date(doc.uploadedAt).toLocaleDateString('ko-KR')}
+ </p>
+ </div>
+ </div>
+ <div className="flex gap-2">
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => handleDownloadDocument(doc)}
+ >
+ 다운로드
+ </Button>
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => handleDeleteDocument(doc.id)}
+ >
+ 삭제
+ </Button>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ </div>
+ )
+} \ No newline at end of file