summaryrefslogtreecommitdiff
path: root/components/bidding
diff options
context:
space:
mode:
Diffstat (limited to 'components/bidding')
-rw-r--r--components/bidding/ProjectSelectorBid.tsx27
-rw-r--r--components/bidding/create/bidding-create-dialog.tsx8
-rw-r--r--components/bidding/manage/bidding-basic-info-editor.tsx22
-rw-r--r--components/bidding/manage/bidding-companies-editor.tsx27
-rw-r--r--components/bidding/manage/bidding-detail-vendor-create-dialog.tsx30
-rw-r--r--components/bidding/manage/bidding-items-editor.tsx271
-rw-r--r--components/bidding/manage/create-pre-quote-rfq-dialog.tsx62
-rw-r--r--components/bidding/price-adjustment-dialog.tsx23
8 files changed, 372 insertions, 98 deletions
diff --git a/components/bidding/ProjectSelectorBid.tsx b/components/bidding/ProjectSelectorBid.tsx
index de9e435e..0fc567b3 100644
--- a/components/bidding/ProjectSelectorBid.tsx
+++ b/components/bidding/ProjectSelectorBid.tsx
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
-import { Check, ChevronsUpDown, X } from "lucide-react"
+import { Check, ChevronsUpDown } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command"
@@ -10,7 +10,7 @@ import { getProjects, type Project } from "@/lib/rfqs/service"
interface ProjectSelectorProps {
selectedProjectId?: number | null;
- onProjectSelect: (project: Project) => void;
+ onProjectSelect: (project: Project | null) => void;
placeholder?: string;
filterType?: string; // 옵션으로 필터 타입 지정 가능
}
@@ -76,7 +76,7 @@ export function ProjectSelector({
// 이미 선택된 프로젝트를 다시 선택하면 선택 해제
if (selectedProject?.id === project.id) {
setSelectedProject(null);
- onProjectSelect(null as any); // 선택 해제를 위해 null 전달
+ onProjectSelect(null); // 선택 해제를 위해 null 전달
setOpen(false);
return;
}
@@ -86,12 +86,6 @@ export function ProjectSelector({
setOpen(false);
};
- // 프로젝트 선택 해제
- const handleClearSelection = (e: React.MouseEvent) => {
- e.stopPropagation(); // Popover가 열리지 않도록 방지
- setSelectedProject(null);
- onProjectSelect(null as any); // 선택 해제를 위해 null 전달
- };
return (
<Popover open={open} onOpenChange={setOpen}>
@@ -106,20 +100,7 @@ export function ProjectSelector({
{isLoading ? (
"프로젝트 로딩 중..."
) : selectedProject ? (
- <div className="flex items-center justify-between w-full">
- <span>{selectedProject.projectCode}</span>
- <div className="flex items-center gap-1">
- <Button
- variant="ghost"
- size="sm"
- className="h-4 w-4 p-0 hover:bg-destructive hover:text-destructive-foreground"
- onClick={handleClearSelection}
- >
- <X className="h-3 w-3" />
- </Button>
- <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
- </div>
- </div>
+ <span>{selectedProject.projectCode}</span>
) : (
<div className="flex items-center justify-between w-full">
<span>{placeholder}</span>
diff --git a/components/bidding/create/bidding-create-dialog.tsx b/components/bidding/create/bidding-create-dialog.tsx
index 9b0a6f66..498d8d1c 100644
--- a/components/bidding/create/bidding-create-dialog.tsx
+++ b/components/bidding/create/bidding-create-dialog.tsx
@@ -618,7 +618,7 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp
{/* 4행: 하도급법적용여부, SHI 지급조건 */}
<div className="grid grid-cols-2 gap-4">
- {/* <FormField
+ <FormField
control={form.control}
name="biddingConditions.isPriceAdjustmentApplicable"
render={({ field }) => (
@@ -637,13 +637,13 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp
}}
/>
<FormLabel htmlFor="price-adjustment" className="text-sm">
- 연동제 적용 요건
+ 하도급법 적용여부
</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
- /> */}
+ />
<FormField
control={form.control}
@@ -957,7 +957,7 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp
<FormItem>
<FormLabel className="flex items-center gap-1">
<Building className="h-3 w-3" />
- 구매조직
+ 구매조직 <span className="text-red-500">*</span>
</FormLabel>
<Select onValueChange={field.onChange} value={field.value || ''}>
<FormControl>
diff --git a/components/bidding/manage/bidding-basic-info-editor.tsx b/components/bidding/manage/bidding-basic-info-editor.tsx
index c2c668a4..3c450065 100644
--- a/components/bidding/manage/bidding-basic-info-editor.tsx
+++ b/components/bidding/manage/bidding-basic-info-editor.tsx
@@ -721,7 +721,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB
<FormItem>
<FormLabel className="flex items-center gap-1">
<Building className="h-3 w-3" />
- 구매조직
+ 구매조직 <span className="text-red-500">*</span>
</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
@@ -1019,7 +1019,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB
</div>
</div>
- {/* 4행: 계약 납품일, 연동제 적용 가능 */}
+ {/* 4행: 계약 납품일, 하도급법 적용여부 */}
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<FormLabel>계약 납품일</FormLabel>
@@ -1034,6 +1034,24 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB
}}
/>
</div>
+ <div>
+ <FormLabel>하도급법 적용여부</FormLabel>
+ <div className="flex items-center space-x-2">
+ <Switch
+ checked={biddingConditions.isPriceAdjustmentApplicable}
+ onCheckedChange={(checked) => {
+ setBiddingConditions(prev => ({
+ ...prev,
+ isPriceAdjustmentApplicable: checked
+ }))
+ }}
+ id="price-adjustment"
+ />
+ <FormLabel htmlFor="price-adjustment" className="text-sm">
+ {biddingConditions.isPriceAdjustmentApplicable ? "적용" : "미적용"}
+ </FormLabel>
+ </div>
+ </div>
</div>
{/* 5행: 스페어파트 옵션 */}
diff --git a/components/bidding/manage/bidding-companies-editor.tsx b/components/bidding/manage/bidding-companies-editor.tsx
index a5ce1349..da566c82 100644
--- a/components/bidding/manage/bidding-companies-editor.tsx
+++ b/components/bidding/manage/bidding-companies-editor.tsx
@@ -49,6 +49,7 @@ interface QuotationVendor {
currency: string
invitationStatus: string
isPriceAdjustmentApplicableQuestion?: boolean
+ businessSize?: string | null
}
interface BiddingCompaniesEditorProps {
@@ -106,17 +107,18 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC
try {
const result = await getBiddingVendors(biddingId)
if (result.success && result.data) {
- const vendorsList = result.data.map(v => ({
- ...v,
- companyId: v.companyId || undefined,
- vendorName: v.vendorName || '',
- vendorCode: v.vendorCode || '',
- contactPerson: v.contactPerson ?? undefined,
- contactEmail: v.contactEmail ?? undefined,
- contactPhone: v.contactPhone ?? undefined,
- quotationAmount: v.quotationAmount ? parseFloat(v.quotationAmount) : undefined,
- isPriceAdjustmentApplicableQuestion: v.isPriceAdjustmentApplicableQuestion ?? false,
- }))
+ const vendorsList = result.data.map(v => ({
+ ...v,
+ companyId: v.companyId || undefined,
+ vendorName: v.vendorName || '',
+ vendorCode: v.vendorCode || '',
+ contactPerson: v.contactPerson ?? undefined,
+ contactEmail: v.contactEmail ?? undefined,
+ contactPhone: v.contactPhone ?? undefined,
+ quotationAmount: v.quotationAmount ? parseFloat(v.quotationAmount) : undefined,
+ isPriceAdjustmentApplicableQuestion: v.isPriceAdjustmentApplicableQuestion ?? false,
+ businessSize: v.businessSize ?? undefined,
+ }))
setVendors(vendorsList)
// 각 업체별 첫 번째 담당자 정보 로드
@@ -161,6 +163,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC
currency: v.currency || 'KRW',
invitationStatus: v.invitationStatus,
isPriceAdjustmentApplicableQuestion: v.isPriceAdjustmentApplicableQuestion ?? false,
+ businessSize: v.businessSize ?? undefined,
}))
setVendors(vendorsList)
@@ -503,6 +506,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC
<TableHead className="w-[50px]">선택</TableHead>
<TableHead>업체명</TableHead>
<TableHead>업체코드</TableHead>
+ <TableHead>기업규모</TableHead>
<TableHead>담당자 이름</TableHead>
<TableHead>담당자 이메일</TableHead>
<TableHead>담당자 연락처</TableHead>
@@ -526,6 +530,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC
</TableCell>
<TableCell className="font-medium">{vendor.vendorName}</TableCell>
<TableCell>{vendor.vendorCode}</TableCell>
+ <TableCell>{vendor.businessSize || '-'}</TableCell>
<TableCell>
{vendor.companyId && vendorFirstContacts.has(vendor.companyId)
? vendorFirstContacts.get(vendor.companyId)!.contactName
diff --git a/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx
index de813121..0dd9f0eb 100644
--- a/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx
+++ b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx
@@ -28,7 +28,7 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { ChevronsUpDown, Loader2, X, Plus } from 'lucide-react'
import { createBiddingDetailVendor } from '@/lib/bidding/detail/service'
-import { searchVendorsForBidding } from '@/lib/bidding/service'
+import { searchVendorsForBidding, getVendorsBusinessSize } from '@/lib/bidding/service'
import { useToast } from '@/hooks/use-toast'
import { useTransition } from 'react'
import { Badge } from '@/components/ui/badge'
@@ -68,6 +68,9 @@ export function BiddingDetailVendorCreateDialog({
const [selectedVendorsWithQuestion, setSelectedVendorsWithQuestion] = React.useState<SelectedVendorWithQuestion[]>([])
const [vendorOpen, setVendorOpen] = React.useState(false)
+ // Business size 정보 캐싱
+ const [businessSizeMap, setBusinessSizeMap] = React.useState<Record<number, string | null>>({})
+
// 벤더 로드
const loadVendors = React.useCallback(async () => {
try {
@@ -204,6 +207,28 @@ export function BiddingDetailVendorCreateDialog({
const selectedVendors = selectedVendorsWithQuestion.map(item => item.vendor)
+ // 선택된 vendor들의 businessSize 정보를 useMemo로 캐싱
+ const loadBusinessSize = React.useMemo(() => {
+ const selectedVendorIds = selectedVendors.map(v => v.id)
+ if (selectedVendorIds.length === 0) return
+
+ // 이미 로드된 vendor들은 제외
+ const newVendorIds = selectedVendorIds.filter(id => !(id in businessSizeMap))
+
+ if (newVendorIds.length > 0) {
+ getVendorsBusinessSize(newVendorIds).then(result => {
+ setBusinessSizeMap(prev => ({ ...prev, ...result }))
+ }).catch(error => {
+ console.error('Failed to load business size:', error)
+ })
+ }
+ }, [selectedVendors, businessSizeMap])
+
+ // selectedVendors가 변경될 때마다 businessSize 로드
+ React.useEffect(() => {
+ loadBusinessSize
+ }, [loadBusinessSize])
+
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] p-0 flex flex-col">
@@ -382,6 +407,9 @@ export function BiddingDetailVendorCreateDialog({
>
연동제 적용요건 문의
</Label>
+ <span className="text-xs text-muted-foreground">
+ 기업규모: {businessSizeMap[item.vendor.id] || '미정'}
+ </span>
</div>
</div>
))}
diff --git a/components/bidding/manage/bidding-items-editor.tsx b/components/bidding/manage/bidding-items-editor.tsx
index f0287ae4..208cf040 100644
--- a/components/bidding/manage/bidding-items-editor.tsx
+++ b/components/bidding/manage/bidding-items-editor.tsx
@@ -441,8 +441,8 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
materialGroupInfo: null,
materialNumber: null,
materialInfo: null,
- priceUnit: null,
- purchaseUnit: '1',
+ priceUnit: 1,
+ purchaseUnit: 'EA',
materialWeight: null,
wbsCode: null,
wbsName: null,
@@ -495,8 +495,8 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
prev.map((item) => {
if (item.id === id) {
const updatedItem = { ...item, ...updates }
- // 내정단가, 수량, 중량, 구매단위가 변경되면 내정금액 재계산
- if (updates.targetUnitPrice || updates.quantity || updates.totalWeight || updates.purchaseUnit) {
+ // 내정단가, 수량, 중량, 가격단위가 변경되면 내정금액 재계산
+ if (updates.targetUnitPrice || updates.quantity || updates.totalWeight || updates.priceUnit) {
updatedItem.targetAmount = calculateTargetAmount(updatedItem)
}
return updatedItem
@@ -533,20 +533,66 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
const calculateTargetAmount = (item: PRItemInfo): string => {
const unitPrice = parseFloat(item.targetUnitPrice?.replace(/,/g, '') || '0') || 0
- const purchaseUnit = parseFloat(item.purchaseUnit || '1') || 1
+ const priceUnit = parseFloat(item.priceUnit || '1') || 1
let amount = 0
if (quantityWeightMode === 'quantity') {
const quantity = parseFloat(item.quantity || '0') || 0
- amount = (quantity / purchaseUnit) * unitPrice
+ amount = (quantity / priceUnit) * unitPrice
} else {
const weight = parseFloat(item.totalWeight || '0') || 0
- amount = (weight / purchaseUnit) * unitPrice
+ amount = (weight / priceUnit) * unitPrice
}
return Math.floor(amount).toString()
}
+ // 합계 계산 함수들
+ const calculateTotals = () => {
+ let quantityTotal = 0
+ let weightTotal = 0
+ let targetAmountTotal = 0
+ let budgetAmountTotal = 0
+ let actualAmountTotal = 0
+
+ items.forEach((item) => {
+ // 수량 합계
+ if (item.quantity) {
+ quantityTotal += parseFloat(item.quantity) || 0
+ }
+
+ // 중량 합계
+ if (item.totalWeight) {
+ weightTotal += parseFloat(item.totalWeight) || 0
+ }
+
+ // 내정금액 합계
+ if (item.targetAmount) {
+ targetAmountTotal += parseFloat(item.targetAmount.replace(/,/g, '')) || 0
+ }
+
+ // 예산금액 합계
+ if (item.budgetAmount) {
+ budgetAmountTotal += parseFloat(item.budgetAmount.replace(/,/g, '')) || 0
+ }
+
+ // 실적금액 합계
+ if (item.actualAmount) {
+ actualAmountTotal += parseFloat(item.actualAmount.replace(/,/g, '')) || 0
+ }
+ })
+
+ return {
+ quantityTotal,
+ weightTotal,
+ targetAmountTotal,
+ budgetAmountTotal,
+ actualAmountTotal,
+ }
+ }
+
+ const totals = calculateTotals()
+
if (isLoading) {
return (
<div className="flex items-center justify-center p-8">
@@ -572,14 +618,16 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">프로젝트코드</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">프로젝트명</th>
- <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">PR 번호</th>
+
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">자재그룹코드 <span className="text-red-500">*</span></th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">자재그룹명 <span className="text-red-500">*</span></th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">자재코드</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">자재명</th>
- <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">수량</th>
- <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">단위</th>
+ <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">수량(중량) <span className="text-red-500">*</span></th>
+ <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">단위 <span className="text-red-500">*</span></th>
+ <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">가격단위</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">구매단위</th>
+ <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[100px]">자재순중량</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">내정단가</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">내정금액</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">내정통화</th>
@@ -587,19 +635,120 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">예산통화</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">실적금액</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">실적통화</th>
+ <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">납품요청일 <span className="text-red-500">*</span></th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">WBS코드</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">WBS명</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">코스트센터코드</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">코스트센터명</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">GL계정코드</th>
<th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">GL계정명</th>
- <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">납품요청일 <span className="text-red-500">*</span></th>
+ <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">PR 번호</th>
<th className="sticky right-0 z-10 bg-muted/50 border-l px-3 py-3 text-center text-xs font-medium min-w-[100px]">
액션
</th>
</tr>
</thead>
<tbody>
+ {/* 합계 행 */}
+ <tr className="bg-blue-50 border-y-2 border-blue-200 font-semibold">
+ <td className="sticky left-0 z-10 bg-blue-50 border-r px-2 py-3 text-center">
+ <span className="text-xs">합계</span>
+ </td>
+ <td className="sticky left-[50px] z-10 bg-blue-50 border-r px-3 py-3 text-center">
+ <span className="text-xs">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs">
+ {quantityWeightMode === 'quantity'
+ ? `${formatNumberWithCommas(totals.quantityTotal.toString())}`
+ : `${formatNumberWithCommas(totals.weightTotal.toString())}`
+ }
+ </span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs">
+ {quantityWeightMode === 'quantity'
+ ? `${items[0]?.quantityUnit || 'EA'}`
+ : `${items[0]?.weightUnit || 'KG'}`
+ }
+ </span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs">{formatNumberWithCommas(totals.targetAmountTotal.toString())}</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs">{formatNumberWithCommas(totals.budgetAmountTotal.toString())}</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs">{formatNumberWithCommas(totals.actualAmountTotal.toString())}</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="border-r px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ <td className="sticky right-0 z-10 bg-blue-50 border-l px-3 py-3 text-center">
+ <span className="text-xs text-muted-foreground">-</span>
+ </td>
+ </tr>
{items.map((item, index) => (
<tr key={item.id} className="border-t hover:bg-muted/30">
<td className="sticky left-0 z-10 bg-background border-r px-2 py-2 text-center">
@@ -617,10 +766,17 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
<ProjectSelector
selectedProjectId={item.projectId || null}
onProjectSelect={(project) => {
- updatePRItem(item.id, {
- projectId: project.id,
- projectInfo: project.projectName
- })
+ if (project) {
+ updatePRItem(item.id, {
+ projectId: project.id,
+ projectInfo: project.projectName
+ })
+ } else {
+ updatePRItem(item.id, {
+ projectId: null,
+ projectInfo: null
+ })
+ }
}}
placeholder="프로젝트 선택"
/>
@@ -633,14 +789,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
className="h-8 text-xs bg-muted/50"
/>
</td>
- <td className="border-r px-3 py-2">
- <Input
- placeholder="PR 번호"
- value={item.prNumber || ''}
- readOnly
- className="h-8 text-xs bg-muted/50"
- />
- </td>
+
<td className="border-r px-3 py-2">
{biddingType !== 'equipment' ? (
<ProcurementItemSelectorDialogSingle
@@ -745,6 +894,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
value={item.quantity || ''}
onChange={(e) => updatePRItem(item.id, { quantity: e.target.value })}
className="h-8 text-xs"
+ required
/>
) : (
<Input
@@ -754,6 +904,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
value={item.totalWeight || ''}
onChange={(e) => updatePRItem(item.id, { totalWeight: e.target.value })}
className="h-8 text-xs"
+ required
/>
)}
</td>
@@ -762,6 +913,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
<Select
value={item.quantityUnit || 'EA'}
onValueChange={(value) => updatePRItem(item.id, { quantityUnit: value })}
+ required
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
@@ -779,6 +931,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
<Select
value={item.weightUnit || 'KG'}
onValueChange={(value) => updatePRItem(item.id, { weightUnit: value })}
+ required
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
@@ -797,9 +950,42 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
type="number"
min="1"
step="1"
- placeholder="구매단위"
- value={item.purchaseUnit || ''}
- onChange={(e) => updatePRItem(item.id, { purchaseUnit: e.target.value })}
+ placeholder="가격단위"
+ value={item.priceUnit || ''}
+ onChange={(e) => updatePRItem(item.id, { priceUnit: e.target.value })}
+ className="h-8 text-xs"
+ />
+ </td>
+ <td className="border-r px-3 py-2">
+ <Select
+ value={item.purchaseUnit || 'EA'}
+ onValueChange={(value) => updatePRItem(item.id, { purchaseUnit: value })}
+ >
+ <SelectTrigger className="h-8 text-xs">
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="EA">EA</SelectItem>
+ <SelectItem value="SET">SET</SelectItem>
+ <SelectItem value="LOT">LOT</SelectItem>
+ <SelectItem value="M">M</SelectItem>
+ <SelectItem value="M2">M²</SelectItem>
+ <SelectItem value="M3">M³</SelectItem>
+ <SelectItem value="KG">KG</SelectItem>
+ <SelectItem value="TON">TON</SelectItem>
+ <SelectItem value="G">G</SelectItem>
+ <SelectItem value="LB">LB</SelectItem>
+ </SelectContent>
+ </Select>
+ </td>
+ <td className="border-r px-3 py-2">
+ <Input
+ type="number"
+ min="0"
+ step="0.01"
+ placeholder="자재순중량"
+ value={item.materialWeight || ''}
+ onChange={(e) => updatePRItem(item.id, { materialWeight: e.target.value })}
className="h-8 text-xs"
/>
</td>
@@ -888,9 +1074,23 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
</Select>
</td>
<td className="border-r px-3 py-2">
+ <Input
+ type="date"
+ value={item.requestedDeliveryDate || ''}
+ onChange={(e) => updatePRItem(item.id, { requestedDeliveryDate: e.target.value })}
+ className="h-8 text-xs"
+ required
+ />
+ </td>
+ <td className="border-r px-3 py-2">
<Button
variant="outline"
onClick={() => {
+ // 재클릭 시 기존 데이터 클리어
+ updatePRItem(item.id, {
+ wbsCode: null,
+ wbsName: null
+ })
setSelectedItemForWbs(item.id)
setWbsCodeDialogOpen(true)
}}
@@ -941,6 +1141,11 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
<Button
variant="outline"
onClick={() => {
+ // 재클릭 시 기존 데이터 클리어
+ updatePRItem(item.id, {
+ costCenterCode: null,
+ costCenterName: null
+ })
setSelectedItemForCostCenter(item.id)
setCostCenterDialogOpen(true)
}}
@@ -992,6 +1197,11 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
<Button
variant="outline"
onClick={() => {
+ // 재클릭 시 기존 데이터 클리어
+ updatePRItem(item.id, {
+ glAccountCode: null,
+ glAccountName: null
+ })
setSelectedItemForGlAccount(item.id)
setGlAccountDialogOpen(true)
}}
@@ -1039,11 +1249,10 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
</td>
<td className="border-r px-3 py-2">
<Input
- type="date"
- value={item.requestedDeliveryDate || ''}
- onChange={(e) => updatePRItem(item.id, { requestedDeliveryDate: e.target.value })}
- className="h-8 text-xs"
- required
+ placeholder="PR 번호"
+ value={item.prNumber || ''}
+ readOnly
+ className="h-8 text-xs bg-muted/50"
/>
</td>
<td className="sticky right-0 z-10 bg-background border-l px-3 py-2">
diff --git a/components/bidding/manage/create-pre-quote-rfq-dialog.tsx b/components/bidding/manage/create-pre-quote-rfq-dialog.tsx
index c49f6232..cdcf1ef1 100644
--- a/components/bidding/manage/create-pre-quote-rfq-dialog.tsx
+++ b/components/bidding/manage/create-pre-quote-rfq-dialog.tsx
@@ -51,8 +51,8 @@ import { MaterialGroupSelectorDialogSingle } from "@/components/common/material/
import { MaterialSearchItem } from "@/lib/material/material-group-service"
import { MaterialSelectorDialogSingle } from "@/components/common/selectors/material/material-selector-dialog-single"
import { MaterialSearchItem as SAPMaterialSearchItem } from "@/components/common/selectors/material/material-service"
-import { ProcurementManagerSelector } from "@/components/common/selectors/procurement-manager"
-import type { ProcurementManagerWithUser } from "@/components/common/selectors/procurement-manager/procurement-manager-service"
+import { PurchaseGroupCodeSelector } from "@/components/common/selectors/purchase-group-code/purchase-group-code-selector"
+import { getBiddingById } from "@/lib/bidding/service"
// 아이템 스키마
const itemSchema = z.object({
@@ -122,7 +122,7 @@ export function CreatePreQuoteRfqDialog({
const [isLoading, setIsLoading] = React.useState(false)
const [previewCode, setPreviewCode] = React.useState("")
const [isLoadingPreview, setIsLoadingPreview] = React.useState(false)
- const [selectedManager, setSelectedManager] = React.useState<ProcurementManagerWithUser | undefined>(undefined)
+ const [selectedBidPic, setSelectedBidPic] = React.useState<any | undefined>(undefined)
const { data: session } = useSession()
const userId = React.useMemo(() => {
@@ -170,6 +170,29 @@ export function CreatePreQuoteRfqDialog({
name: "items",
})
+ // 입찰담당자 정보 로드
+ React.useEffect(() => {
+ const loadBiddingInfo = async () => {
+ if (!biddingId || !open) return
+
+ try {
+ const bidding = await getBiddingById(biddingId)
+ if (bidding && bidding.bidPicId) {
+ // 입찰담당자 정보를 로드하는 로직 추가 필요
+ // 현재는 임시로 bidPicId를 사용
+ setSelectedBidPic({
+ USER_ID: bidding.bidPicId,
+ DISPLAY_NAME: bidding.bidPicName || '입찰담당자'
+ })
+ }
+ } catch (error) {
+ console.error('Failed to load bidding info:', error)
+ }
+ }
+
+ loadBiddingInfo()
+ }, [biddingId, open])
+
// 다이얼로그가 열릴 때 폼 초기화
React.useEffect(() => {
if (open) {
@@ -177,7 +200,7 @@ export function CreatePreQuoteRfqDialog({
rfqType: "",
rfqTitle: "",
dueDate: undefined,
- picUserId: undefined,
+ picUserId: selectedBidPic?.USER_ID,
projectId: undefined,
remark: "",
items: initialItems.length > 0 ? initialItems : [
@@ -192,14 +215,13 @@ export function CreatePreQuoteRfqDialog({
},
],
})
- setSelectedManager(undefined)
setPreviewCode("")
}
- }, [open, initialItems, form])
+ }, [open, initialItems, form, selectedBidPic])
// 견적담당자 선택 시 RFQ 코드 미리보기 생성
React.useEffect(() => {
- if (!selectedManager?.user?.id) {
+ if (!selectedBidPic?.USER_ID) {
setPreviewCode("")
return
}
@@ -208,7 +230,7 @@ export function CreatePreQuoteRfqDialog({
(async () => {
setIsLoadingPreview(true)
try {
- const code = await previewGeneralRfqCode(selectedManager.user!.id!)
+ const code = await previewGeneralRfqCode(selectedBidPic.USER_ID)
setPreviewCode(code)
} catch (error) {
console.error("코드 미리보기 오류:", error)
@@ -217,7 +239,7 @@ export function CreatePreQuoteRfqDialog({
setIsLoadingPreview(false)
}
})()
- }, [selectedManager])
+ }, [selectedBidPic])
// 견적 종류 변경
const handleRfqTypeChange = (value: string) => {
@@ -244,8 +266,8 @@ export function CreatePreQuoteRfqDialog({
},
],
})
- setSelectedManager(undefined)
- setPreviewCode("")
+ setSelectedBidPic(undefined)
+ setPreviewCode("")
onOpenChange(false)
}
@@ -255,12 +277,12 @@ export function CreatePreQuoteRfqDialog({
return
}
- if (!selectedManager?.user?.id) {
- toast.error("견적담당자를 선택해주세요")
+ if (!selectedBidPic?.USER_ID) {
+ toast.error("입찰담당자를 선택해주세요")
return
}
- const picUserId = selectedManager.user.id
+ const picUserId = selectedBidPic.USER_ID
setIsLoading(true)
@@ -476,13 +498,13 @@ export function CreatePreQuoteRfqDialog({
견적담당자 <span className="text-red-500">*</span>
</FormLabel>
<FormControl>
- <ProcurementManagerSelector
- selectedManager={selectedManager}
- onManagerSelect={(manager) => {
- setSelectedManager(manager)
- field.onChange(manager.user?.id)
+ <PurchaseGroupCodeSelector
+ selectedCode={selectedBidPic}
+ onCodeSelect={(code) => {
+ setSelectedBidPic(code)
+ field.onChange(code.USER_ID)
}}
- placeholder="견적담당자를 선택하세요"
+ placeholder="입찰담당자 선택"
/>
</FormControl>
<FormDescription>
diff --git a/components/bidding/price-adjustment-dialog.tsx b/components/bidding/price-adjustment-dialog.tsx
index 149b8e9a..6a520eac 100644
--- a/components/bidding/price-adjustment-dialog.tsx
+++ b/components/bidding/price-adjustment-dialog.tsx
@@ -67,7 +67,7 @@ export function PriceAdjustmentDialog({
<h3 className="text-sm font-medium text-gray-900 mb-3">기본 정보</h3>
<div className="grid grid-cols-2 gap-4">
<div>
- <label className="text-xs text-gray-500">품목등의 명칭</label>
+ <label className="text-xs text-gray-500">물품등의 명칭</label>
<p className="text-sm font-medium">{data.itemName || '-'}</p>
</div>
<div>
@@ -126,16 +126,16 @@ export function PriceAdjustmentDialog({
</div>
<div className="grid grid-cols-2 gap-4">
<div>
- <label className="text-xs text-gray-500">기준시점</label>
+ <label className="text-xs text-gray-500">원재료 기준 가격의 변동률 산정을 위한 기준시점</label>
<p className="text-sm font-medium">{data.referenceDate ? formatDate(data.referenceDate, "kr") : '-'}</p>
</div>
<div>
- <label className="text-xs text-gray-500">비교시점</label>
+ <label className="text-xs text-gray-500">원재료 기준 가격의 변동률 산정을 위한 비교시점</label>
<p className="text-sm font-medium">{data.comparisonDate ? formatDate(data.comparisonDate, "kr") : '-'}</p>
</div>
</div>
<div>
- <label className="text-xs text-gray-500">연동 비율</label>
+ <label className="text-xs text-gray-500">반영비율</label>
<p className="text-sm font-medium">
{data.adjustmentRatio ? `${data.adjustmentRatio}%` : '-'}
</p>
@@ -166,11 +166,11 @@ export function PriceAdjustmentDialog({
</div>
</div>
<div>
- <label className="text-xs text-gray-500">수탁기업(협력사) 작성자</label>
+ <label className="text-xs text-gray-500">수탁기업(협력사)작성자</label>
<p className="text-sm font-medium">{data.contractorWriter || '-'}</p>
</div>
<div>
- <label className="text-xs text-gray-500">기타 사항</label>
+ <label className="text-xs text-gray-500">기타사항</label>
<p className="text-sm font-medium whitespace-pre-wrap">
{data.notes || '-'}
</p>
@@ -185,6 +185,17 @@ export function PriceAdjustmentDialog({
<p>작성일: {formatDate(data.createdAt, "kr")}</p>
<p>수정일: {formatDate(data.updatedAt, "kr")}</p>
</div>
+
+ <Separator />
+
+ {/* 참고 경고문 */}
+ <div className="text-xs text-red-600 space-y-2 bg-red-50 p-3 rounded-md border border-red-200">
+ <p className="font-medium">※ 참고사항</p>
+ <div className="space-y-1">
+ <p>• 납품대금의 10% 이상을 차지하는 주요 원재료가 있는 경우 모든 주요 원재료에 대해서 적용 또는 미적용에 대한 연동표를 작성해야 한다.</p>
+ <p>• 납품대급연동표를 허위로 작성하거나 근거자료를 허위로 제출할 경우 본 계약이 체결되지 않을 수 있으며, 본 계약이 체결되었더라도 계약의 전부 또는 일부를 해제 또는 해지할 수 있다.</p>
+ </div>
+ </div>
</div>
</DialogContent>
</Dialog>