summaryrefslogtreecommitdiff
path: root/lib/bidding/list
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-17 10:40:12 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-17 10:40:12 +0000
commit10cb50753ccf318024c4394282f9e8d968dcd1a5 (patch)
treecf4edb96aa172c3b90d88532aff1f536944a2283 /lib/bidding/list
parentf7117370b9cc0c7b96bd1eb23a1b9f5b16cc8ceb (diff)
(최겸) 구매 입찰 오류 수정 및 선적지,하역지 연동,TO Cont, TO PO 개발
Diffstat (limited to 'lib/bidding/list')
-rw-r--r--lib/bidding/list/biddings-page-header.tsx4
-rw-r--r--lib/bidding/list/biddings-table-columns.tsx21
-rw-r--r--lib/bidding/list/biddings-table-toolbar-actions.tsx6
-rw-r--r--lib/bidding/list/biddings-table.tsx17
-rw-r--r--lib/bidding/list/biddings-transmission-dialog.tsx14
-rw-r--r--lib/bidding/list/create-bidding-dialog.tsx249
-rw-r--r--lib/bidding/list/edit-bidding-sheet.tsx37
7 files changed, 270 insertions, 78 deletions
diff --git a/lib/bidding/list/biddings-page-header.tsx b/lib/bidding/list/biddings-page-header.tsx
index 7fa9a39c..64e588c3 100644
--- a/lib/bidding/list/biddings-page-header.tsx
+++ b/lib/bidding/list/biddings-page-header.tsx
@@ -19,13 +19,13 @@ export function BiddingsPageHeader() {
{/* 우측: 액션 버튼들 */}
<div className="flex items-center gap-2">
- <Button
+ {/* <Button
variant="outline"
onClick={() => router.push('/evcp/biddings/analytics')}
>
<TrendingUp className="mr-2 h-4 w-4" />
분석 보기
- </Button>
+ </Button> */}
<Button
variant="outline"
diff --git a/lib/bidding/list/biddings-table-columns.tsx b/lib/bidding/list/biddings-table-columns.tsx
index 5240b134..7f0b8e40 100644
--- a/lib/bidding/list/biddings-table-columns.tsx
+++ b/lib/bidding/list/biddings-table-columns.tsx
@@ -293,12 +293,23 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef
},
{
- accessorKey: "contractPeriod",
+ id: "contractPeriod",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약기간" />,
- cell: ({ row }) => (
- <span className="truncate max-w-[100px]">{row.original.contractPeriod || '-'}</span>
- ),
- size: 100,
+ cell: ({ row }) => {
+ const startDate = row.original.contractStartDate
+ const endDate = row.original.contractEndDate
+
+ if (!startDate || !endDate) {
+ return <span className="text-muted-foreground">-</span>
+ }
+
+ return (
+ <div className="text-xs max-w-[120px] truncate" title={`${formatDate(startDate, "KR")} ~ ${formatDate(endDate, "KR")}`}>
+ {formatDate(startDate, "KR")} ~ {formatDate(endDate, "KR")}
+ </div>
+ )
+ },
+ size: 120,
meta: { excelHeader: "계약기간" },
},
]
diff --git a/lib/bidding/list/biddings-table-toolbar-actions.tsx b/lib/bidding/list/biddings-table-toolbar-actions.tsx
index ed5538c6..702396ae 100644
--- a/lib/bidding/list/biddings-table-toolbar-actions.tsx
+++ b/lib/bidding/list/biddings-table-toolbar-actions.tsx
@@ -23,11 +23,9 @@ import { TransmissionDialog } from "./biddings-transmission-dialog"
interface BiddingsTableToolbarActionsProps {
table: Table<BiddingListItem>
- paymentTermsOptions: Array<{code: string, description: string}>
- incotermsOptions: Array<{code: string, description: string}>
}
-export function BiddingsTableToolbarActions({ table, paymentTermsOptions, incotermsOptions }: BiddingsTableToolbarActionsProps) {
+export function BiddingsTableToolbarActions({ table }: BiddingsTableToolbarActionsProps) {
const router = useRouter()
const { data: session } = useSession()
const [isExporting, setIsExporting] = React.useState(false)
@@ -66,8 +64,6 @@ export function BiddingsTableToolbarActions({ table, paymentTermsOptions, incote
<div className="flex items-center gap-2">
{/* 신규 생성 */}
<CreateBiddingDialog
- paymentTermsOptions={paymentTermsOptions}
- incotermsOptions={incotermsOptions}
/>
{/* 전송하기 (업체선정 완료된 입찰만) */}
diff --git a/lib/bidding/list/biddings-table.tsx b/lib/bidding/list/biddings-table.tsx
index 2a8f98c3..2ecfaa73 100644
--- a/lib/bidding/list/biddings-table.tsx
+++ b/lib/bidding/list/biddings-table.tsx
@@ -12,7 +12,7 @@ import { useDataTable } from "@/hooks/use-data-table"
import { DataTable } from "@/components/data-table/data-table"
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
import { getBiddingsColumns } from "./biddings-table-columns"
-import { getBiddings, getBiddingStatusCounts, getActivePaymentTerms, getActiveIncoterms, getBiddingTypeCounts, getBiddingManagerCounts, getBiddingMonthlyStats } from "@/lib/bidding/service"
+import { getBiddings, getBiddingStatusCounts } from "@/lib/bidding/service"
import { BiddingListItem } from "@/db/schema"
import { BiddingsTableToolbarActions } from "./biddings-table-toolbar-actions"
import {
@@ -28,26 +28,17 @@ interface BiddingsTableProps {
promises: Promise<
[
Awaited<ReturnType<typeof getBiddings>>,
- Awaited<ReturnType<typeof getBiddingStatusCounts>>,
- Awaited<ReturnType<typeof getBiddingTypeCounts>>, // 추가
- Awaited<ReturnType<typeof getBiddingManagerCounts>>, // 추가
- Awaited<ReturnType<typeof getBiddingMonthlyStats>>, // 추가
- Awaited<ReturnType<typeof getActivePaymentTerms>>,
- Awaited<ReturnType<typeof getActiveIncoterms>>
+ Awaited<ReturnType<typeof getBiddingStatusCounts>>
]
>
}
export function BiddingsTable({ promises }: BiddingsTableProps) {
- const [biddingsResult, statusCounts, typeCounts, managerCounts, monthlyStats, paymentTermsResult, incotermsResult] = React.use(promises)
+ const [biddingsResult, statusCounts] = React.use(promises)
// biddingsResult에서 data와 pageCount 추출
const { data, pageCount } = biddingsResult
- const paymentTermsOptions = paymentTermsResult.success && 'data' in paymentTermsResult ? paymentTermsResult.data || [] : []
- const incotermsOptions = incotermsResult.success && 'data' in incotermsResult ? incotermsResult.data || [] : []
- console.log(paymentTermsOptions,"paymentTermsOptions")
- console.log(incotermsOptions,"incotermsOptions")
const [isCompact, setIsCompact] = React.useState<boolean>(false)
const [specMeetingDialogOpen, setSpecMeetingDialogOpen] = React.useState(false)
const [prDocumentsDialogOpen, setPrDocumentsDialogOpen] = React.useState(false)
@@ -179,8 +170,6 @@ export function BiddingsTable({ promises }: BiddingsTableProps) {
>
<BiddingsTableToolbarActions
table={table}
- paymentTermsOptions={paymentTermsOptions}
- incotermsOptions={incotermsOptions}
/>
</DataTableAdvancedToolbar>
</DataTable>
diff --git a/lib/bidding/list/biddings-transmission-dialog.tsx b/lib/bidding/list/biddings-transmission-dialog.tsx
index d307ec9d..035ab583 100644
--- a/lib/bidding/list/biddings-transmission-dialog.tsx
+++ b/lib/bidding/list/biddings-transmission-dialog.tsx
@@ -19,10 +19,6 @@ import { Label } from "@/components/ui/label"
import { BiddingListItem } from "@/db/schema"
import { transmitToContract, transmitToPO } from "@/lib/bidding/actions"
-console.log('=== Module loaded ===')
-console.log('transmitToContract imported:', typeof transmitToContract)
-console.log('transmitToPO imported:', typeof transmitToPO)
-
interface TransmissionDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
@@ -38,23 +34,15 @@ export function TransmissionDialog({ open, onOpenChange, bidding, userId }: Tran
const handleToContract = async () => {
try {
setIsLoading(true)
- console.log('=== START handleToContract ===')
console.log('bidding.id', bidding.id)
console.log('userId', userId)
- console.log('transmitToContract function:', typeof transmitToContract)
-
- console.log('About to call transmitToContract...')
- const result = await transmitToContract(bidding.id, userId)
- console.log('transmitToContract result:', result)
-
+ await transmitToContract(bidding.id, userId)
toast.success('계약서 생성이 완료되었습니다.')
onOpenChange(false)
} catch (error) {
- console.error('handleToContract error:', error)
toast.error(`계약서 생성에 실패했습니다: ${error}`)
} finally {
setIsLoading(false)
- console.log('=== END handleToContract ===')
}
}
diff --git a/lib/bidding/list/create-bidding-dialog.tsx b/lib/bidding/list/create-bidding-dialog.tsx
index 4fc4fd7b..a25dd363 100644
--- a/lib/bidding/list/create-bidding-dialog.tsx
+++ b/lib/bidding/list/create-bidding-dialog.tsx
@@ -67,7 +67,8 @@ import {
} from "@/components/ui/file-list"
import { Checkbox } from "@/components/ui/checkbox"
-import { createBidding, type CreateBiddingInput, getActivePaymentTerms, getActiveIncoterms } from "@/lib/bidding/service"
+import { createBidding, type CreateBiddingInput } from "@/lib/bidding/service"
+import { getIncotermsForSelection, getPaymentTermsForSelection, getPlaceOfShippingForSelection, getPlaceOfDestinationForSelection } from "@/lib/procurement-select/service"
import {
createBiddingSchema,
type CreateBiddingSchema
@@ -127,12 +128,7 @@ interface PRItemInfo {
const TAB_ORDER = ["basic", "contract", "schedule", "conditions", "details", "manager"] as const
type TabType = typeof TAB_ORDER[number]
-interface CreateBiddingDialogProps {
- paymentTermsOptions?: Array<{code: string, description: string}>
- incotermsOptions?: Array<{code: string, description: string}>
-}
-
-export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions = [] }: CreateBiddingDialogProps) {
+export function CreateBiddingDialog() {
const router = useRouter()
const [isSubmitting, setIsSubmitting] = React.useState(false)
const { data: session } = useSession()
@@ -141,6 +137,13 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
const [showSuccessDialog, setShowSuccessDialog] = React.useState(false) // 추가
const [createdBiddingId, setCreatedBiddingId] = React.useState<number | null>(null) // 추가
+ // 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 [procurementLoading, setProcurementLoading] = React.useState(false)
+
// 사양설명회 정보 상태
const [specMeetingInfo, setSpecMeetingInfo] = React.useState<SpecificationMeetingInfo>({
meetingDate: "",
@@ -157,8 +160,24 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
meetingFiles: [], // 사양설명회 첨부파일
})
- // PR 아이템들 상태
- const [prItems, setPrItems] = React.useState<PRItemInfo[]>([])
+ // PR 아이템들 상태 - 기본적으로 하나의 빈 아이템 생성
+ const [prItems, setPrItems] = React.useState<PRItemInfo[]>([
+ {
+ id: `pr-default`,
+ prNumber: "",
+ itemCode: "",
+ itemInfo: "",
+ quantity: "",
+ quantityUnit: "EA",
+ totalWeight: "",
+ weightUnit: "KG",
+ materialDescription: "",
+ hasSpecDocument: false,
+ requestedDeliveryDate: "",
+ specFiles: [],
+ isRepresentative: true, // 첫 번째 아이템은 대표 아이템
+ }
+ ])
// 파일 첨부를 위해 선택된 아이템 ID
const [selectedItemForFile, setSelectedItemForFile] = React.useState<string | null>(null)
@@ -175,6 +194,69 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
sparePartOptions: "",
})
+ // Procurement 데이터 로드 함수들
+ const loadPaymentTerms = React.useCallback(async () => {
+ setProcurementLoading(true);
+ try {
+ const data = await getPaymentTermsForSelection();
+ setPaymentTermsOptions(data);
+ } catch (error) {
+ console.error("Failed to load payment terms:", error);
+ toast.error("결제조건 목록을 불러오는데 실패했습니다.");
+ } finally {
+ setProcurementLoading(false);
+ }
+ }, []);
+
+ const loadIncoterms = React.useCallback(async () => {
+ setProcurementLoading(true);
+ try {
+ const data = await getIncotermsForSelection();
+ setIncotermsOptions(data);
+ } catch (error) {
+ console.error("Failed to load incoterms:", error);
+ toast.error("운송조건 목록을 불러오는데 실패했습니다.");
+ } finally {
+ setProcurementLoading(false);
+ }
+ }, []);
+
+ const loadShippingPlaces = React.useCallback(async () => {
+ setProcurementLoading(true);
+ try {
+ const data = await getPlaceOfShippingForSelection();
+ setShippingPlaces(data);
+ } catch (error) {
+ console.error("Failed to load shipping places:", error);
+ toast.error("선적지 목록을 불러오는데 실패했습니다.");
+ } finally {
+ setProcurementLoading(false);
+ }
+ }, []);
+
+ const loadDestinationPlaces = React.useCallback(async () => {
+ setProcurementLoading(true);
+ try {
+ const data = await getPlaceOfDestinationForSelection();
+ setDestinationPlaces(data);
+ } catch (error) {
+ console.error("Failed to load destination places:", error);
+ toast.error("하역지 목록을 불러오는데 실패했습니다.");
+ } finally {
+ setProcurementLoading(false);
+ }
+ }, []);
+
+ // 다이얼로그 열릴 때 procurement 데이터 로드
+ React.useEffect(() => {
+ if (open) {
+ loadPaymentTerms();
+ loadIncoterms();
+ loadShippingPlaces();
+ loadDestinationPlaces();
+ }
+ }, [open, loadPaymentTerms, loadIncoterms, loadShippingPlaces, loadDestinationPlaces])
+
// 사양설명회 파일 추가
const addMeetingFiles = (files: File[]) => {
@@ -211,7 +293,8 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
contractType: "general",
biddingType: "equipment",
awardCount: "single",
- contractPeriod: "",
+ contractStartDate: "",
+ contractEndDate: "",
submissionStartDate: "",
submissionEndDate: "",
@@ -268,9 +351,10 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
isValid: formValues.contractType &&
formValues.biddingType &&
formValues.awardCount &&
- formValues.contractPeriod.trim() !== "" &&
+ formValues.contractStartDate &&
+ formValues.contractEndDate &&
formValues.currency,
- hasErrors: !!(formErrors.contractType || formErrors.biddingType || formErrors.awardCount || formErrors.contractPeriod || formErrors.currency)
+ hasErrors: !!(formErrors.contractType || formErrors.biddingType || formErrors.awardCount || formErrors.contractStartDate || formErrors.contractEndDate || formErrors.currency)
},
schedule: {
isValid: formValues.submissionStartDate &&
@@ -289,7 +373,7 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
hasErrors: false
},
details: {
- isValid: true, // 세부내역은 선택사항
+ isValid: prItems.length > 0,
hasErrors: false
},
manager: {
@@ -369,6 +453,12 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
// PR 아이템 제거
const removePRItem = (id: string) => {
+ // 최소 하나의 아이템은 유지해야 함
+ if (prItems.length <= 1) {
+ toast.error("최소 하나의 품목이 필요합니다.")
+ return
+ }
+
setPrItems(prev => {
const filteredItems = prev.filter(item => item.id !== id)
// 만약 대표 아이템을 삭제했다면, 첫 번째 아이템을 대표로 설정
@@ -443,7 +533,9 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
toast.error("제출 시작일시와 마감일시를 입력해주세요")
}
} else if (activeTab === "conditions") {
- toast.error("입찰 조건을 모두 입력해주세요 (지급조건, 세금조건, 운송조건, 계약납품일, 선적지, 도착지)")
+ toast.error("입찰 조건을 모두 입력해주세요 (지급조건, 세금조건, 운송조건, 계약납품일, 선적지, 하역지)")
+ } else if (activeTab === "details") {
+ toast.error("품목정보, 수량/단위 또는 중량/중량단위를 입력해주세요")
}
return
}
@@ -524,7 +616,8 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
contractType: "general",
biddingType: "equipment",
awardCount: "single",
- contractPeriod: "",
+ contractStartDate: "",
+ contractEndDate: "",
submissionStartDate: "",
submissionEndDate: "",
hasSpecificationMeeting: false,
@@ -556,7 +649,23 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
isRequired: false,
meetingFiles: [],
})
- setPrItems([])
+ setPrItems([
+ {
+ id: `pr-default`,
+ prNumber: "",
+ itemCode: "",
+ itemInfo: "",
+ quantity: "",
+ quantityUnit: "EA",
+ totalWeight: "",
+ weightUnit: "KG",
+ materialDescription: "",
+ hasSpecDocument: false,
+ requestedDeliveryDate: "",
+ specFiles: [],
+ isRepresentative: true, // 첫 번째 아이템은 대표 아이템
+ }
+ ])
setSelectedItemForFile(null)
setBiddingConditions({
paymentTerms: "",
@@ -705,6 +814,9 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
}`}
>
세부내역
+ {!tabValidation.details.isValid && (
+ <span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span>
+ )}
</button>
<button
type="button"
@@ -927,18 +1039,34 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
)}
/>
- {/* 계약기간 */}
+ {/* 계약 시작일 */}
<FormField
control={form.control}
- name="contractPeriod"
+ name="contractStartDate"
render={({ field }) => (
<FormItem>
- <FormLabel>
- 계약기간 <span className="text-red-500">*</span>
- </FormLabel>
+ <FormLabel>계약 시작일 <span className="text-red-500">*</span></FormLabel>
+ <FormControl>
+ <Input
+ type="date"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 계약 종료일 */}
+ <FormField
+ control={form.control}
+ name="contractEndDate"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>계약 종료일 <span className="text-red-500">*</span></FormLabel>
<FormControl>
<Input
- placeholder="예: 계약일로부터 60일"
+ type="date"
{...field}
/>
</FormControl>
@@ -1403,26 +1531,58 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
<div className="space-y-2">
<label className="text-sm font-medium">선적지 <span className="text-red-500">*</span></label>
- <Input
- placeholder="예: 부산항, 인천항"
+ <Select
value={biddingConditions.shippingPort}
- onChange={(e) => setBiddingConditions(prev => ({
+ onValueChange={(value) => setBiddingConditions(prev => ({
...prev,
- shippingPort: e.target.value
+ 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 className="space-y-2">
- <label className="text-sm font-medium">도착지 <span className="text-red-500">*</span></label>
- <Input
- placeholder="예: 현장 직납, 창고 납품"
+ <label className="text-sm font-medium">하역지 <span className="text-red-500">*</span></label>
+ <Select
value={biddingConditions.destinationPort}
- onChange={(e) => setBiddingConditions(prev => ({
+ onValueChange={(value) => setBiddingConditions(prev => ({
...prev,
- destinationPort: e.target.value
+ 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>
@@ -1463,7 +1623,10 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
<div>
<CardTitle>세부내역 관리</CardTitle>
<p className="text-sm text-muted-foreground mt-1">
- PR 아이템 또는 수기 아이템을 추가하여 입찰 세부내역을 관리하세요
+ 최소 하나의 품목을 입력해야 합니다
+ </p>
+ <p className="text-xs text-amber-600 mt-1">
+ 수량/단위 또는 중량/중량단위를 선택해서 입력하세요
</p>
</div>
<Button
@@ -1487,7 +1650,7 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
<TableHead className="w-[60px]">대표</TableHead>
<TableHead className="w-[120px]">PR 번호</TableHead>
<TableHead className="w-[120px]">품목코드</TableHead>
- <TableHead>품목정보</TableHead>
+ <TableHead>품목정보 *</TableHead>
<TableHead className="w-[80px]">수량</TableHead>
<TableHead className="w-[80px]">단위</TableHead>
<TableHead className="w-[80px]">중량</TableHead>
@@ -1526,7 +1689,7 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
</TableCell>
<TableCell>
<Input
- placeholder="품목정보"
+ placeholder="품목정보 *"
value={item.itemInfo}
onChange={(e) => updatePRItem(item.id, { itemInfo: e.target.value })}
className="h-8"
@@ -1535,6 +1698,7 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
<TableCell>
<Input
type="number"
+ min="0"
placeholder="수량"
value={item.quantity}
onChange={(e) => updatePRItem(item.id, { quantity: e.target.value })}
@@ -1562,6 +1726,7 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
<TableCell>
<Input
type="number"
+ min="0"
placeholder="중량"
value={item.totalWeight}
onChange={(e) => updatePRItem(item.id, { totalWeight: e.target.value })}
@@ -1590,6 +1755,7 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
value={item.requestedDeliveryDate}
onChange={(e) => updatePRItem(item.id, { requestedDeliveryDate: e.target.value })}
className="h-8"
+ placeholder="납품요청일"
/>
</TableCell>
<TableCell>
@@ -1612,7 +1778,9 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
variant="outline"
size="sm"
onClick={() => removePRItem(item.id)}
+ disabled={prItems.length <= 1}
className="h-8 w-8 p-0"
+ title={prItems.length <= 1 ? "최소 하나의 품목이 필요합니다" : "품목 삭제"}
>
<Trash2 className="h-4 w-4" />
</Button>
@@ -1978,7 +2146,14 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
)}
</span>
)}
- {activeTab === "details" && "세부내역 아이템을 관리하세요 (선택사항)"}
+ {activeTab === "details" && (
+ <span>
+ 최소 하나의 품목을 입력하세요
+ {!tabValidation.details.isValid && (
+ <span className="text-red-500 ml-2">• 필수 항목이 누락되었습니다</span>
+ )}
+ </span>
+ )}
{activeTab === "manager" && "담당자 정보를 확인하고 입찰을 생성하세요"}
</div>
diff --git a/lib/bidding/list/edit-bidding-sheet.tsx b/lib/bidding/list/edit-bidding-sheet.tsx
index c76ec2a2..dc24d0cf 100644
--- a/lib/bidding/list/edit-bidding-sheet.tsx
+++ b/lib/bidding/list/edit-bidding-sheet.tsx
@@ -82,7 +82,8 @@ export function EditBiddingSheet({
contractType: "general",
biddingType: "equipment",
awardCount: "single",
- contractPeriod: "",
+ contractStartDate: "",
+ contractEndDate: "",
preQuoteDate: "",
biddingRegistrationDate: "",
@@ -126,7 +127,8 @@ export function EditBiddingSheet({
contractType: bidding.contractType || "general",
biddingType: bidding.biddingType || "equipment",
awardCount: bidding.awardCount || "single",
- contractPeriod: bidding.contractPeriod || "",
+ contractStartDate: formatDate(bidding.contractStartDate, "kr"),
+ contractEndDate: formatDate(bidding.contractEndDate, "kr"),
preQuoteDate: formatDate(bidding.preQuoteDate, "kr"),
biddingRegistrationDate: formatDate(bidding.biddingRegistrationDate, "kr"),
@@ -356,6 +358,37 @@ export function EditBiddingSheet({
/>
</div>
+ {/* 계약 기간 */}
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="contractStartDate"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>계약 시작일</FormLabel>
+ <FormControl>
+ <Input type="date" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="contractEndDate"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>계약 종료일</FormLabel>
+ <FormControl>
+ <Input type="date" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
<FormField
control={form.control}
name="status"