diff options
Diffstat (limited to 'lib/bidding')
| -rw-r--r-- | lib/bidding/list/bidding-detail-dialogs.tsx | 16 | ||||
| -rw-r--r-- | lib/bidding/list/biddings-table-toolbar-actions.tsx | 52 | ||||
| -rw-r--r-- | lib/bidding/list/create-bidding-dialog.tsx | 24 | ||||
| -rw-r--r-- | lib/bidding/list/edit-bidding-sheet.tsx | 33 | ||||
| -rw-r--r-- | lib/bidding/pre-quote/service.ts | 27 | ||||
| -rw-r--r-- | lib/bidding/service.ts | 22 | ||||
| -rw-r--r-- | lib/bidding/validation.ts | 12 |
7 files changed, 54 insertions, 132 deletions
diff --git a/lib/bidding/list/bidding-detail-dialogs.tsx b/lib/bidding/list/bidding-detail-dialogs.tsx index 2e58d676..4fbca616 100644 --- a/lib/bidding/list/bidding-detail-dialogs.tsx +++ b/lib/bidding/list/bidding-detail-dialogs.tsx @@ -45,6 +45,7 @@ import { toast } from "sonner" import { BiddingListItem } from "@/db/schema" import { downloadFile, formatFileSize, getFileInfo } from "@/lib/file-download" import { getPRDetailsAction, getSpecificationMeetingDetailsAction } from "../service" +import { formatDate } from "@/lib/utils" // 타입 정의 interface SpecificationMeetingDetails { @@ -301,19 +302,6 @@ export function SpecificationMeetingDialog({ } }; - const formatDate = (dateString: string) => { - try { - return new Date(dateString).toLocaleDateString('ko-KR', { - year: 'numeric', - month: 'long', - day: 'numeric', - weekday: 'long' - }); - } catch { - return dateString; - } - }; - return ( <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent className="max-w-4xl max-h-[90vh]"> @@ -355,7 +343,7 @@ export function SpecificationMeetingDialog({ <div className="text-sm space-y-1"> <div> <CalendarIcon className="inline h-3 w-3 text-muted-foreground mr-2" /> - <span className="font-medium">날짜:</span> {formatDate(data.meetingDate)} + <span className="font-medium">날짜:</span> {formatDate(data.meetingDate, "kr")} {data.meetingTime && <span className="ml-4"><ClockIcon className="inline h-3 w-3 text-muted-foreground mr-1" />{data.meetingTime}</span>} </div> diff --git a/lib/bidding/list/biddings-table-toolbar-actions.tsx b/lib/bidding/list/biddings-table-toolbar-actions.tsx index 81982a43..70b48a36 100644 --- a/lib/bidding/list/biddings-table-toolbar-actions.tsx +++ b/lib/bidding/list/biddings-table-toolbar-actions.tsx @@ -8,7 +8,6 @@ import { } from "lucide-react" import { toast } from "sonner" import { useRouter } from "next/navigation" - import { exportTableToExcel } from "@/lib/export" import { Button } from "@/components/ui/button" import { @@ -37,41 +36,6 @@ export function BiddingsTableToolbarActions({ table }: BiddingsTableToolbarActio .map(row => row.original) }, [table.getFilteredSelectedRowModel().rows]) - // 사전견적 요청 가능한 입찰들 (입찰생성 상태) - const preQuoteEligibleBiddings = React.useMemo(() => { - return selectedBiddings.filter(bidding => - bidding.status === 'bidding_generated' - ) - }, [selectedBiddings]) - - // 개찰 가능한 입찰들 (내정가 산정 완료) - const openEligibleBiddings = React.useMemo(() => { - return selectedBiddings.filter(bidding => - bidding.status === 'set_target_price' - ) - }, [selectedBiddings]) - - - const handlePreQuoteRequest = () => { - if (preQuoteEligibleBiddings.length === 0) { - toast.warning("사전견적 요청 가능한 입찰을 선택해주세요.") - return - } - - toast.success(`${preQuoteEligibleBiddings.length}개 입찰의 사전견적을 요청했습니다.`) - // TODO: 실제 사전견적 요청 로직 구현 - } - - const handleBiddingOpen = () => { - if (openEligibleBiddings.length === 0) { - toast.warning("개찰 가능한 입찰을 선택해주세요.") - return - } - - toast.success(`${openEligibleBiddings.length}개 입찰을 개찰했습니다.`) - // TODO: 실제 개찰 로직 구현 - } - const handleExport = async () => { try { setIsExporting(true) @@ -92,20 +56,8 @@ export function BiddingsTableToolbarActions({ table }: BiddingsTableToolbarActio {/* 신규 생성 */} <CreateBiddingDialog/> - {/* 사전견적 요청 */} - {preQuoteEligibleBiddings.length > 0 && ( - <Button - variant="outline" - size="sm" - onClick={handlePreQuoteRequest} - > - <Send className="mr-2 h-4 w-4" /> - 사전견적 요청 ({preQuoteEligibleBiddings.length}) - </Button> - )} - {/* 개찰 (입찰 오픈) */} - {openEligibleBiddings.length > 0 && ( + {/* {openEligibleBiddings.length > 0 && ( <Button variant="outline" size="sm" @@ -114,7 +66,7 @@ export function BiddingsTableToolbarActions({ table }: BiddingsTableToolbarActio <Gavel className="mr-2 h-4 w-4" /> 개찰 ({openEligibleBiddings.length}) </Button> - )} + )} */} {/* Export */} <DropdownMenu> diff --git a/lib/bidding/list/create-bidding-dialog.tsx b/lib/bidding/list/create-bidding-dialog.tsx index 88697903..f21782ff 100644 --- a/lib/bidding/list/create-bidding-dialog.tsx +++ b/lib/bidding/list/create-bidding-dialog.tsx @@ -274,7 +274,10 @@ export function CreateBiddingDialog() { conditions: { isValid: biddingConditions.paymentTerms.trim() !== "" && biddingConditions.taxConditions.trim() !== "" && - biddingConditions.incoterms.trim() !== "", + biddingConditions.incoterms.trim() !== "" && + biddingConditions.contractDeliveryDate.trim() !== "" && + biddingConditions.shippingPort.trim() !== "" && + biddingConditions.destinationPort.trim() !== "", hasErrors: false }, details: { @@ -286,7 +289,7 @@ export function CreateBiddingDialog() { hasErrors: !!(formErrors.managerName || formErrors.managerEmail || formErrors.managerPhone) } } - }, [form, specMeetingInfo.meetingDate, specMeetingInfo.location, specMeetingInfo.contactPerson]) + }, [form, specMeetingInfo.meetingDate, specMeetingInfo.location, specMeetingInfo.contactPerson, biddingConditions]) const tabValidation = getTabValidationState() @@ -428,7 +431,7 @@ export function CreateBiddingDialog() { toast.error("제출 시작일시와 마감일시를 입력해주세요") } } else if (activeTab === "conditions") { - toast.error("입찰 조건을 모두 입력해주세요 (지급조건, 세금조건, 운송조건)") + toast.error("입찰 조건을 모두 입력해주세요 (지급조건, 세금조건, 운송조건, 계약납품일, 선적지, 도착지)") } return } @@ -474,17 +477,18 @@ export function CreateBiddingDialog() { const result = await createBidding(extendedData, userId) if (result.success) { - toast.success(result.message) + toast.success(result.message || "입찰이 성공적으로 생성되었습니다.") setOpen(false) router.refresh() // 생성된 입찰 상세페이지로 이동할지 묻기 - if (result.data?.id) { + if (result.success && 'data' in result && result.data?.id) { setCreatedBiddingId(result.data.id) setShowSuccessDialog(true) } } else { - toast.error(result.error || "입찰 생성에 실패했습니다.") + const errorMessage = result.success === false && 'error' in result ? result.error : "입찰 생성에 실패했습니다." + toast.error(errorMessage) } } catch (error) { console.error("Error creating bidding:", error) @@ -1316,7 +1320,7 @@ export function CreateBiddingDialog() { 세금조건 <span className="text-red-500">*</span> </label> <Input - + placeholder="예: 부가세 별도" value={biddingConditions.taxConditions} onChange={(e) => setBiddingConditions(prev => ({ ...prev, @@ -1341,7 +1345,7 @@ export function CreateBiddingDialog() { <div className="space-y-2"> <label className="text-sm font-medium"> - 계약 납품일 + 계약 납품일 <span className="text-red-500">*</span> </label> <Input type="date" @@ -1354,7 +1358,7 @@ export function CreateBiddingDialog() { </div> <div className="space-y-2"> - <label className="text-sm font-medium">선적지</label> + <label className="text-sm font-medium">선적지 <span className="text-red-500">*</span></label> <Input placeholder="예: 부산항, 인천항" value={biddingConditions.shippingPort} @@ -1366,7 +1370,7 @@ export function CreateBiddingDialog() { </div> <div className="space-y-2"> - <label className="text-sm font-medium">도착지</label> + <label className="text-sm font-medium">도착지 <span className="text-red-500">*</span></label> <Input placeholder="예: 현장 직납, 창고 납품" value={biddingConditions.destinationPort} diff --git a/lib/bidding/list/edit-bidding-sheet.tsx b/lib/bidding/list/edit-bidding-sheet.tsx index f3bc1805..71eeed2b 100644 --- a/lib/bidding/list/edit-bidding-sheet.tsx +++ b/lib/bidding/list/edit-bidding-sheet.tsx @@ -49,6 +49,7 @@ import { biddingTypeLabels, awardCountLabels } from "@/db/schema" +import { formatDate } from "@/lib/utils" interface EditBiddingSheetProps { open: boolean @@ -111,28 +112,6 @@ export function EditBiddingSheet({ // 시트가 열릴 때 기존 데이터로 폼 초기화 React.useEffect(() => { if (open && bidding) { - const formatDateForInput = (date: Date | string | null): string => { - if (!date) return "" - try { - const d = new Date(date) - if (isNaN(d.getTime())) return "" - return d.toISOString().slice(0, 16) // YYYY-MM-DDTHH:mm - } catch { - return "" - } - } - - const formatDateOnlyForInput = (date: Date | string | null): string => { - if (!date) return "" - try { - const d = new Date(date) - if (isNaN(d.getTime())) return "" - return d.toISOString().slice(0, 10) // YYYY-MM-DD - } catch { - return "" - } - } - form.reset({ biddingNumber: bidding.biddingNumber || "", revision: bidding.revision || 0, @@ -147,11 +126,11 @@ export function EditBiddingSheet({ awardCount: bidding.awardCount || "single", contractPeriod: bidding.contractPeriod || "", - preQuoteDate: formatDateOnlyForInput(bidding.preQuoteDate), - biddingRegistrationDate: formatDateOnlyForInput(bidding.biddingRegistrationDate), - submissionStartDate: formatDateForInput(bidding.submissionStartDate), - submissionEndDate: formatDateForInput(bidding.submissionEndDate), - evaluationDate: formatDateForInput(bidding.evaluationDate), + preQuoteDate: formatDate(bidding.preQuoteDate, "kr"), + biddingRegistrationDate: formatDate(bidding.biddingRegistrationDate, "kr"), + submissionStartDate: formatDate(bidding.submissionStartDate, "kr"), + submissionEndDate: formatDate(bidding.submissionEndDate, "kr"), + evaluationDate: formatDate(bidding.evaluationDate, "kr"), hasSpecificationMeeting: bidding.hasSpecificationMeeting || false, hasPrDocument: bidding.hasPrDocument || false, diff --git a/lib/bidding/pre-quote/service.ts b/lib/bidding/pre-quote/service.ts index b5b06769..35bc8941 100644 --- a/lib/bidding/pre-quote/service.ts +++ b/lib/bidding/pre-quote/service.ts @@ -330,17 +330,20 @@ export async function sendPreQuoteInvitations(companyIds: number[]) { } } // 3. 입찰 상태를 사전견적 요청으로 변경 (bidding_generated 상태에서만) - await tx - .update(biddings) - .set({ - status: 'request_for_quotation', - updatedAt: new Date() - }) - .where(and( - eq(biddings.id, biddingId), - eq(biddings.status, 'bidding_generated') - )) - + for (const company of companiesInfo) { + await db.transaction(async (tx) => { + await tx + .update(biddings) + .set({ + status: 'request_for_quotation', + updatedAt: new Date() + }) + .where(and( + eq(biddings.id, company.biddingId), + eq(biddings.status, 'bidding_generated') + )) + }) + } return { success: true, message: `${companyIds.length}개 업체에 사전견적 초대를 발송했습니다.` @@ -608,7 +611,7 @@ export async function submitPreQuoteResponse( .update(biddings) .set({ status: 'received_quotation', - preQuoteReceivedAt: new Date(), // 사전견적 접수일 업데이트 + preQuoteDate: new Date().toISOString().split('T')[0], // 사전견적 접수일 업데이트 updatedAt: new Date() }) .where(and( diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index 8c99bfed..c4904219 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -378,13 +378,11 @@ export interface CreateBiddingInput extends CreateBiddingSchema { paymentTerms: string taxConditions: string incoterms: string - proposedDeliveryDate: string - proposedShippingPort: string - proposedDestinationPort: string - priceAdjustmentApplicable: boolean - specialConditions: string - sparePartRequirement: string - additionalNotes: string + contractDeliveryDate: string + shippingPort: string + destinationPort: string + isPriceAdjustmentApplicable: boolean + sparePartOptions: string } } @@ -612,11 +610,11 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { paymentTerms: input.biddingConditions.paymentTerms, taxConditions: input.biddingConditions.taxConditions, incoterms: input.biddingConditions.incoterms, - contractDeliveryDate: input.biddingConditions.proposedDeliveryDate ? new Date(input.biddingConditions.proposedDeliveryDate) : null, - shippingPort: input.biddingConditions.proposedShippingPort, - destinationPort: input.biddingConditions.proposedDestinationPort, - isPriceAdjustmentApplicable: input.biddingConditions.priceAdjustmentApplicable, - sparePartOptions: input.biddingConditions.sparePartRequirement, + contractDeliveryDate: input.biddingConditions.contractDeliveryDate ? new Date(input.biddingConditions.contractDeliveryDate) : null, + shippingPort: input.biddingConditions.shippingPort, + destinationPort: input.biddingConditions.destinationPort, + isPriceAdjustmentApplicable: input.biddingConditions.isPriceAdjustmentApplicable, + sparePartOptions: input.biddingConditions.sparePartOptions, }) } catch (error) { console.error('Error saving bidding conditions:', error) diff --git a/lib/bidding/validation.ts b/lib/bidding/validation.ts index 95cbb02c..a7f78f72 100644 --- a/lib/bidding/validation.ts +++ b/lib/bidding/validation.ts @@ -107,13 +107,11 @@ export const createBiddingSchema = z.object({ paymentTerms: z.string().min(1, "지급조건은 필수입니다"), taxConditions: z.string().min(1, "세금조건은 필수입니다"), incoterms: z.string().min(1, "운송조건은 필수입니다"), - proposedDeliveryDate: z.string().optional(), - proposedShippingPort: z.string().optional(), - proposedDestinationPort: z.string().optional(), - priceAdjustmentApplicable: z.boolean().default(false), - specialConditions: z.string().optional(), - sparePartRequirement: z.string().optional(), - additionalNotes: z.string().optional(), + contractDeliveryDate: z.string().min(1, "계약납품일은 필수입니다"), + shippingPort: z.string().min(1, "선적지는 필수입니다"), + destinationPort: z.string().min(1, "도착지는 필수입니다"), + isPriceAdjustmentApplicable: z.boolean().default(false), + sparePartOptions: z.string().optional(), }).optional(), }).refine((data) => { // 제출 기간 검증: 시작일이 마감일보다 이전이어야 함 |
