summaryrefslogtreecommitdiff
path: root/lib/bidding
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding')
-rw-r--r--lib/bidding/list/bidding-detail-dialogs.tsx16
-rw-r--r--lib/bidding/list/biddings-table-toolbar-actions.tsx52
-rw-r--r--lib/bidding/list/create-bidding-dialog.tsx24
-rw-r--r--lib/bidding/list/edit-bidding-sheet.tsx33
-rw-r--r--lib/bidding/pre-quote/service.ts27
-rw-r--r--lib/bidding/service.ts22
-rw-r--r--lib/bidding/validation.ts12
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) => {
// 제출 기간 검증: 시작일이 마감일보다 이전이어야 함