diff options
| -rw-r--r-- | components/bidding/manage/bidding-items-editor.tsx | 28 | ||||
| -rw-r--r-- | components/bidding/manage/bidding-schedule-editor.tsx | 15 | ||||
| -rw-r--r-- | components/common/selectors/procurement-item/procurement-item-selector.tsx | 12 | ||||
| -rw-r--r-- | db/schema/bidding.ts | 2 | ||||
| -rw-r--r-- | lib/bidding/actions.ts | 3 | ||||
| -rw-r--r-- | lib/bidding/detail/service.ts | 7 | ||||
| -rw-r--r-- | lib/bidding/list/biddings-table-columns.tsx | 11 | ||||
| -rw-r--r-- | lib/bidding/pre-quote/service.ts | 87 | ||||
| -rw-r--r-- | lib/bidding/receive/biddings-receive-columns.tsx | 12 | ||||
| -rw-r--r-- | lib/bidding/receive/biddings-receive-table.tsx | 13 | ||||
| -rw-r--r-- | lib/bidding/selection/biddings-selection-columns.tsx | 9 | ||||
| -rw-r--r-- | lib/bidding/service.ts | 87 | ||||
| -rw-r--r-- | lib/bidding/vendor/components/pr-items-pricing-table.tsx | 140 | ||||
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-detail.tsx | 205 | ||||
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-list-columns.tsx | 52 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/bidding-and-pr-mapper.ts | 50 |
16 files changed, 470 insertions, 263 deletions
diff --git a/components/bidding/manage/bidding-items-editor.tsx b/components/bidding/manage/bidding-items-editor.tsx index f61b3960..ef0aa568 100644 --- a/components/bidding/manage/bidding-items-editor.tsx +++ b/components/bidding/manage/bidding-items-editor.tsx @@ -676,6 +676,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <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]">수량(중량) <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-[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]">가격단위</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> @@ -686,7 +687,6 @@ 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> @@ -755,6 +755,9 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <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"> @@ -784,9 +787,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <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> @@ -998,6 +999,15 @@ 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 + /> + </td> + <td className="border-r px-3 py-2"> + <Input type="number" min="1" step="1" @@ -1124,15 +1134,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems </SelectContent> </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" diff --git a/components/bidding/manage/bidding-schedule-editor.tsx b/components/bidding/manage/bidding-schedule-editor.tsx index ca4643ff..4ddaee08 100644 --- a/components/bidding/manage/bidding-schedule-editor.tsx +++ b/components/bidding/manage/bidding-schedule-editor.tsx @@ -140,6 +140,15 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc return round > 1 } + // UTC 날짜를 한국 시간(KST) 기준 datetime-local 입력값으로 변환 + const toKstInputValue = (date: string | Date | undefined | null) => { + if (!date) return '' + const d = new Date(date) + // UTC 시간에 9시간을 더함 + const kstTime = d.getTime() + (9 * 60 * 60 * 1000) + return new Date(kstTime).toISOString().slice(0, 16) + } + // 데이터 로딩 React.useEffect(() => { const loadSchedule = async () => { @@ -156,8 +165,8 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc }) setSchedule({ - submissionStartDate: bidding.submissionStartDate ? new Date(bidding.submissionStartDate).toISOString().slice(0, 16) : '', - submissionEndDate: bidding.submissionEndDate ? new Date(bidding.submissionEndDate).toISOString().slice(0, 16) : '', + submissionStartDate: toKstInputValue(bidding.submissionStartDate), + submissionEndDate: toKstInputValue(bidding.submissionEndDate), remarks: bidding.remarks || '', isUrgent: bidding.isUrgent || false, hasSpecificationMeeting: bidding.hasSpecificationMeeting || false, @@ -170,7 +179,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc if (meetingDetails.success && meetingDetails.data) { const meeting = meetingDetails.data setSpecMeetingInfo({ - meetingDate: meeting.meetingDate ? new Date(meeting.meetingDate).toISOString().slice(0, 16) : '', + meetingDate: toKstInputValue(meeting.meetingDate), meetingTime: meeting.meetingTime || '', location: meeting.location || '', address: meeting.address || '', diff --git a/components/common/selectors/procurement-item/procurement-item-selector.tsx b/components/common/selectors/procurement-item/procurement-item-selector.tsx index 5650959c..f09fea3d 100644 --- a/components/common/selectors/procurement-item/procurement-item-selector.tsx +++ b/components/common/selectors/procurement-item/procurement-item-selector.tsx @@ -44,13 +44,9 @@ export function ProcurementItemSelector({ // 검색 쿼리가 변경될 때마다 검색 실행 useEffect(() => { const performSearch = async () => { - if (debouncedSearchQuery.length < 1) { - setSearchResults([]); - return; - } - setIsLoading(true); try { + // 검색어가 없어도 기본 목록 로드 (기본 50개 등 백엔드 설정에 따름) const results = await searchProcurementItemsForSelector(debouncedSearchQuery); setSearchResults(results); } catch (error) { @@ -119,11 +115,7 @@ export function ProcurementItemSelector({ <CommandEmpty> {isLoading ? ( <div className="py-6 text-center text-sm text-muted-foreground"> - 검색 중... - </div> - ) : searchQuery.length < 1 ? ( - <div className="py-6 text-center text-sm text-muted-foreground"> - 품목코드 또는 품목명을 입력하세요 + 로딩 중... </div> ) : ( <div className="py-6 text-center text-sm text-muted-foreground"> diff --git a/db/schema/bidding.ts b/db/schema/bidding.ts index 99a14475..d87f9fa8 100644 --- a/db/schema/bidding.ts +++ b/db/schema/bidding.ts @@ -284,7 +284,7 @@ export const prItemsForBidding = pgTable('pr_items_for_bidding', { materialGroupInfo: varchar('material_group_info', { length: 300 }), // 자재그룹정보 // 자재 정보 (새로 추가) materialNumber: varchar('material_number', { length: 100 }), // 자재번호 - materialInfo: varchar('material_info', { length: 500 }), // 자재정보 + materialInfo: varchar('material_info', { length: 500 }), // 자재명 // 납품 일정 requestedDeliveryDate: date('requested_delivery_date'), // 납품요청일 diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts index 0bf2af57..02501b27 100644 --- a/lib/bidding/actions.ts +++ b/lib/bidding/actions.ts @@ -683,7 +683,8 @@ export async function openBiddingAction(biddingId: number) { } const now = new Date() - const isDeadlinePassed = bidding.submissionEndDate && now > bidding.submissionEndDate + const submissionEndDate = bidding.submissionEndDate ? new Date(bidding.submissionEndDate) : null + const isDeadlinePassed = submissionEndDate && now > submissionEndDate // 2. 개찰 가능 여부 확인 if (!isDeadlinePassed) { diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index c9aaa66c..0b68eaa7 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -36,7 +36,7 @@ export interface BiddingDetailData { } // getBiddingById 함수 임포트 (기존 함수 재사용) -import { getBiddingById } from '@/lib/bidding/service' +import { getBiddingById, updateBiddingProjectInfo } from '@/lib/bidding/service' // Promise.all을 사용하여 모든 데이터를 병렬로 조회 (캐시 적용) export async function getBiddingDetailData(biddingId: number): Promise<BiddingDetailData> { @@ -1375,6 +1375,9 @@ export async function updatePrItem(prItemId: number, input: Partial<typeof prIte // PR 아이템 금액 합산하여 bidding 업데이트 await updateBiddingAmounts(biddingId) + // 프로젝트 정보 업데이트 + await updateBiddingProjectInfo(biddingId) + // 캐시 무효화 revalidateTag(`bidding-${biddingId}`) revalidateTag('pr-items') @@ -1759,6 +1762,7 @@ export async function getBiddingDetailsForPartners(biddingId: number, companyId: status: biddings.status, isUrgent: biddings.isUrgent, bidPicName: biddings.bidPicName, + bidPicPhone: users.phone, supplyPicName: biddings.supplyPicName, // 협력업체 특정 정보 @@ -1788,6 +1792,7 @@ export async function getBiddingDetailsForPartners(biddingId: number, companyId: responseSubmittedAt: companyConditionResponses.submittedAt, }) .from(biddings) + .leftJoin(users, eq(biddings.bidPicId, users.id)) .innerJoin(biddingCompanies, eq(biddings.id, biddingCompanies.biddingId)) .leftJoin(companyConditionResponses, eq(biddingCompanies.id, companyConditionResponses.biddingCompanyId)) .where(and( diff --git a/lib/bidding/list/biddings-table-columns.tsx b/lib/bidding/list/biddings-table-columns.tsx index 36abd03c..907115b1 100644 --- a/lib/bidding/list/biddings-table-columns.tsx +++ b/lib/bidding/list/biddings-table-columns.tsx @@ -256,10 +256,15 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef if (!startDate || !endDate) return <span className="text-muted-foreground">-</span> - const now = new Date() - const isActive = now >= new Date(startDate) && now <= new Date(endDate) - const isPast = now > new Date(endDate) + const now = new Date().toString() + console.log(now, "now") + const startIso = new Date(startDate).toISOString() + const endIso = new Date(endDate).toISOString() + const isActive = new Date(now) >= new Date(startIso) && new Date(now) <= new Date(endIso) + console.log(isActive, "isActive") + const isPast = new Date(now) > new Date(endIso) + console.log(isPast, "isPast") return ( <div className="text-xs"> <div className={`${isActive ? 'text-green-600 font-medium' : isPast ? 'text-red-600' : 'text-gray-600'}`}> diff --git a/lib/bidding/pre-quote/service.ts b/lib/bidding/pre-quote/service.ts index 1dd06b3c..0f938b24 100644 --- a/lib/bidding/pre-quote/service.ts +++ b/lib/bidding/pre-quote/service.ts @@ -840,37 +840,66 @@ export async function setPreQuoteParticipation( }
// PR 아이템 조회 (입찰에 포함된 품목들)
-export async function getPrItemsForBidding(biddingId: number) {
+export async function getPrItemsForBidding(biddingId: number, companyId?: number) {
try {
- const prItems = await db
- .select({
- id: prItemsForBidding.id,
- biddingId: prItemsForBidding.biddingId,
- itemNumber: prItemsForBidding.itemNumber,
- projectId: prItemsForBidding.projectId,
- projectInfo: prItemsForBidding.projectInfo,
- itemInfo: prItemsForBidding.itemInfo,
- shi: prItemsForBidding.shi,
- materialGroupNumber: prItemsForBidding.materialGroupNumber,
- materialGroupInfo: prItemsForBidding.materialGroupInfo,
- materialNumber: prItemsForBidding.materialNumber,
- materialInfo: prItemsForBidding.materialInfo,
- requestedDeliveryDate: prItemsForBidding.requestedDeliveryDate,
- annualUnitPrice: prItemsForBidding.annualUnitPrice,
- currency: prItemsForBidding.currency,
- quantity: prItemsForBidding.quantity,
- quantityUnit: prItemsForBidding.quantityUnit,
- totalWeight: prItemsForBidding.totalWeight,
- weightUnit: prItemsForBidding.weightUnit,
- priceUnit: prItemsForBidding.priceUnit,
- purchaseUnit: prItemsForBidding.purchaseUnit,
- materialWeight: prItemsForBidding.materialWeight,
- prNumber: prItemsForBidding.prNumber,
- hasSpecDocument: prItemsForBidding.hasSpecDocument,
- })
- .from(prItemsForBidding)
- .where(eq(prItemsForBidding.biddingId, biddingId))
+ const selectFields: any = {
+ id: prItemsForBidding.id,
+ biddingId: prItemsForBidding.biddingId,
+ itemNumber: prItemsForBidding.itemNumber,
+ projectId: prItemsForBidding.projectId,
+ projectInfo: prItemsForBidding.projectInfo,
+ itemInfo: prItemsForBidding.itemInfo,
+ shi: prItemsForBidding.shi,
+ materialGroupNumber: prItemsForBidding.materialGroupNumber,
+ materialGroupInfo: prItemsForBidding.materialGroupInfo,
+ materialNumber: prItemsForBidding.materialNumber,
+ materialInfo: prItemsForBidding.materialInfo,
+ requestedDeliveryDate: prItemsForBidding.requestedDeliveryDate,
+ annualUnitPrice: prItemsForBidding.annualUnitPrice,
+ currency: prItemsForBidding.currency,
+ quantity: prItemsForBidding.quantity,
+ quantityUnit: prItemsForBidding.quantityUnit,
+ totalWeight: prItemsForBidding.totalWeight,
+ weightUnit: prItemsForBidding.weightUnit,
+ priceUnit: prItemsForBidding.priceUnit,
+ purchaseUnit: prItemsForBidding.purchaseUnit,
+ materialWeight: prItemsForBidding.materialWeight,
+ targetUnitPrice: prItemsForBidding.targetUnitPrice,
+ targetAmount: prItemsForBidding.targetAmount,
+ targetCurrency: prItemsForBidding.targetCurrency,
+ budgetAmount: prItemsForBidding.budgetAmount,
+ budgetCurrency: prItemsForBidding.budgetCurrency,
+ actualAmount: prItemsForBidding.actualAmount,
+ actualCurrency: prItemsForBidding.actualCurrency,
+ prNumber: prItemsForBidding.prNumber,
+ hasSpecDocument: prItemsForBidding.hasSpecDocument,
+ specification: prItemsForBidding.specification,
+ }
+
+ if (companyId) {
+ selectFields.bidUnitPrice = companyPrItemBids.bidUnitPrice
+ selectFields.bidAmount = companyPrItemBids.bidAmount
+ selectFields.proposedDeliveryDate = companyPrItemBids.proposedDeliveryDate
+ selectFields.technicalSpecification = companyPrItemBids.technicalSpecification
+ }
+
+ let query = db.select(selectFields).from(prItemsForBidding)
+
+ if (companyId) {
+ query = query
+ .leftJoin(biddingCompanies, and(
+ eq(biddingCompanies.biddingId, biddingId),
+ eq(biddingCompanies.companyId, companyId)
+ ))
+ .leftJoin(companyPrItemBids, and(
+ eq(companyPrItemBids.prItemId, prItemsForBidding.id),
+ eq(companyPrItemBids.biddingCompanyId, biddingCompanies.id)
+ )) as any
+ }
+
+ query = query.where(eq(prItemsForBidding.biddingId, biddingId)).orderBy(prItemsForBidding.id) as any
+ const prItems = await query
return prItems
} catch (error) {
console.error('Failed to get PR items for bidding:', error)
diff --git a/lib/bidding/receive/biddings-receive-columns.tsx b/lib/bidding/receive/biddings-receive-columns.tsx index ab2c0d02..4bde849c 100644 --- a/lib/bidding/receive/biddings-receive-columns.tsx +++ b/lib/bidding/receive/biddings-receive-columns.tsx @@ -196,13 +196,19 @@ export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): Co if (!startDate || !endDate) return <span className="text-muted-foreground">-</span>
const now = new Date()
- const isActive = now >= new Date(startDate) && now <= new Date(endDate)
- const isPast = now > new Date(endDate)
+ const startObj = new Date(startDate)
+ const endObj = new Date(endDate)
+
+ const isActive = now >= startObj && now <= endObj
+ const isPast = now > endObj
+
+ // UI 표시용 KST 변환
+ const formatKst = (d: Date) => new Date(d.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
return (
<div className="text-xs">
<div className={`${isActive ? 'text-green-600 font-medium' : isPast ? 'text-red-600' : 'text-gray-600'}`}>
- {new Date(startDate).toISOString().slice(0, 16).replace('T', ' ')} ~ {new Date(endDate).toISOString().slice(0, 16).replace('T', ' ')}
+ {formatKst(startObj)} ~ {formatKst(endObj)}
</div>
{isActive && (
<Badge variant="default" className="text-xs mt-1">진행중</Badge>
diff --git a/lib/bidding/receive/biddings-receive-table.tsx b/lib/bidding/receive/biddings-receive-table.tsx index 97d627ea..5bda921e 100644 --- a/lib/bidding/receive/biddings-receive-table.tsx +++ b/lib/bidding/receive/biddings-receive-table.tsx @@ -164,17 +164,6 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { setIsCompact(compact)
}, [])
- // const handleSpecMeetingDialogClose = React.useCallback(() => {
- // setSpecMeetingDialogOpen(false)
- // setRowAction(null)
- // setSelectedBidding(null)
- // }, [])
-
- // const handlePrDocumentsDialogClose = React.useCallback(() => {
- // setPrDocumentsDialogOpen(false)
- // setRowAction(null)
- // setSelectedBidding(null)
- // }, [])
// 선택된 행 가져오기
const selectedRows = table.getFilteredSelectedRowModel().rows
@@ -185,7 +174,7 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { if (!selectedBiddingForAction) return false
const now = new Date()
- const submissionEndDate = selectedBiddingForAction.submissionEndDate
+ const submissionEndDate = selectedBiddingForAction.submissionEndDate ? new Date(selectedBiddingForAction.submissionEndDate) : null
// 1. 입찰 마감일이 지났으면 무조건 가능
if (submissionEndDate && now > submissionEndDate) return true
diff --git a/lib/bidding/selection/biddings-selection-columns.tsx b/lib/bidding/selection/biddings-selection-columns.tsx index 9efa849b..355d5aaa 100644 --- a/lib/bidding/selection/biddings-selection-columns.tsx +++ b/lib/bidding/selection/biddings-selection-columns.tsx @@ -176,13 +176,18 @@ export function getBiddingsSelectionColumns({ setRowAction }: GetColumnsProps): if (!startDate || !endDate) return <span className="text-muted-foreground">-</span>
const now = new Date()
- const isPast = now > new Date(endDate)
+ const startObj = new Date(startDate)
+ const endObj = new Date(endDate)
+ const isPast = now > endObj
const isClosed = isPast
+ // UI 표시용 KST 변환
+ const formatKst = (d: Date) => new Date(d.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
+
return (
<div className="text-xs">
<div className={`${isClosed ? 'text-red-600' : 'text-gray-600'}`}>
- {formatDate(startDate, "KR")} ~ {formatDate(endDate, "KR")}
+ {formatKst(startObj)} ~ {formatKst(endObj)}
</div>
{isClosed && (
<Badge variant="destructive" className="text-xs mt-1">마감</Badge>
diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index 489268c6..8fd1d368 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -797,6 +797,22 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { return null } } + + // 담당자 정보 준비 + let bidPicId = input.bidPicId ? parseInt(input.bidPicId.toString()) : null + let bidPicName = input.bidPicName || null + + if (!bidPicId && input.bidPicCode) { + try { + const userInfo = await findUserInfoByEKGRP(input.bidPicCode) + if (userInfo) { + bidPicId = userInfo.userId + bidPicName = userInfo.userName + } + } catch (e) { + console.error('Failed to find user info by EKGRP:', e) + } + } // 1. 입찰 생성 const [newBidding] = await tx @@ -849,8 +865,8 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { purchasingOrganization: input.purchasingOrganization, // 담당자 정보 (user FK) - bidPicId: input.bidPicId ? parseInt(input.bidPicId.toString()) : null, - bidPicName: input.bidPicName || null, + bidPicId, + bidPicName, bidPicCode: input.bidPicCode || null, supplyPicId: input.supplyPicId ? parseInt(input.supplyPicId.toString()) : null, supplyPicName: input.supplyPicName || null, @@ -1234,6 +1250,24 @@ export async function updateBidding(input: UpdateBiddingInput, userId: string) { // 담당자 정보 (user FK) if (input.bidPicId !== undefined) updateData.bidPicId = input.bidPicId if (input.bidPicName !== undefined) updateData.bidPicName = input.bidPicName + + // bidPicCode가 있으면 담당자 정보 자동 조회 및 업데이트 + if (input.bidPicCode !== undefined) { + updateData.bidPicCode = input.bidPicCode + // bidPicId가 명시적으로 제공되지 않았고 코드가 있는 경우 자동 조회 + if (!input.bidPicId && input.bidPicCode) { + try { + const userInfo = await findUserInfoByEKGRP(input.bidPicCode) + if (userInfo) { + updateData.bidPicId = userInfo.userId + updateData.bidPicName = userInfo.userName + } + } catch (e) { + console.error('Failed to find user info by EKGRP:', e) + } + } + } + if (input.supplyPicId !== undefined) updateData.supplyPicId = input.supplyPicId if (input.supplyPicName !== undefined) updateData.supplyPicName = input.supplyPicName @@ -1927,22 +1961,12 @@ export async function updateBiddingSchedule( try { const userName = await getUserNameById(userId) - // 날짜 문자열을 Date 객체로 수동 변환 + // 날짜 문자열을 Date 객체로 변환 (KST 기준) const parseDate = (dateStr?: string) => { if (!dateStr) return undefined - // 'YYYY-MM-DDTHH:mm' 또는 'YYYY-MM-DD HH:mm' 등을 허용 - // 잘못된 포맷이면 undefined 반환 - const m = dateStr.match( - /^(\d{4})-(\d{2})-(\d{2})[T ]?(\d{2}):(\d{2})(?::(\d{2}))?$/ - ) - if (!m) return undefined - const year = parseInt(m[1], 10) - const month = parseInt(m[2], 10) - 1 // JS month는 0부터 - const day = parseInt(m[3], 10) - const hour = parseInt(m[4], 10) - const min = parseInt(m[5], 10) - const sec = m[6] ? parseInt(m[6], 10) : 0 - return new Date(Date.UTC(year, month, day, hour, min, sec)) + // 'YYYY-MM-DDTHH:mm' 형식을 가정하고 KST(+09:00) 오프셋을 붙여서 파싱 + // 초(:00)를 추가하여 ISO 8601 호환성 확보 + return new Date(`${dateStr}:00+09:00`) } return await db.transaction(async (tx) => { @@ -2178,6 +2202,34 @@ export async function removeBiddingItem(itemId: number) { } } +// 입찰의 첫 번째 PR 아이템 프로젝트 정보로 bidding 업데이트 +export async function updateBiddingProjectInfo(biddingId: number) { + try { + const firstItem = await db + .select({ + projectInfo: prItemsForBidding.projectInfo + }) + .from(prItemsForBidding) + .where(eq(prItemsForBidding.biddingId, biddingId)) + .orderBy(prItemsForBidding.id) + .limit(1) + + if (firstItem.length > 0 && firstItem[0].projectInfo) { + await db + .update(biddings) + .set({ + projectName: firstItem[0].projectInfo, + updatedAt: new Date() + }) + .where(eq(biddings.id, biddingId)) + + console.log(`Bidding ${biddingId} project info updated to: ${firstItem[0].projectInfo}`) + } + } catch (error) { + console.error('Failed to update bidding project info:', error) + } +} + // 입찰의 PR 아이템 금액 합산하여 bidding 업데이트 async function updateBiddingAmounts(biddingId: number) { try { @@ -2289,6 +2341,9 @@ export async function addPRItemForBidding( // PR 아이템 금액 합산하여 bidding 업데이트 await updateBiddingAmounts(biddingId) + // 프로젝트 정보 업데이트 + await updateBiddingProjectInfo(biddingId) + revalidatePath(`/evcp/bid/${biddingId}/info`) revalidatePath(`/evcp/bid/${biddingId}`) diff --git a/lib/bidding/vendor/components/pr-items-pricing-table.tsx b/lib/bidding/vendor/components/pr-items-pricing-table.tsx index a0230478..7dd8384e 100644 --- a/lib/bidding/vendor/components/pr-items-pricing-table.tsx +++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx @@ -42,7 +42,7 @@ interface PrItem { materialGroupInfo: string | null materialNumber: string | null materialInfo: string | null - requestedDeliveryDate: Date | null + requestedDeliveryDate: Date | string | null annualUnitPrice: string | null currency: string | null quantity: string | null @@ -54,6 +54,11 @@ interface PrItem { materialWeight: string | null prNumber: string | null hasSpecDocument: boolean | null + specification: string | null + bidUnitPrice?: string | number | null + bidAmount?: string | number | null + proposedDeliveryDate?: string | Date | null + technicalSpecification?: string | null } interface PrItemQuotation { @@ -189,6 +194,18 @@ export function PrItemsPricingTable({ if (existing) { return existing } + + // prItems 자체에 견적 정보가 있는 경우 활용 + if (item.bidUnitPrice !== undefined || item.bidAmount !== undefined) { + return { + prItemId: item.id, + bidUnitPrice: item.bidUnitPrice ? Number(item.bidUnitPrice) : 0, + bidAmount: item.bidAmount ? Number(item.bidAmount) : 0, + proposedDeliveryDate: item.proposedDeliveryDate ? (item.proposedDeliveryDate instanceof Date ? item.proposedDeliveryDate.toISOString().split('T')[0] : String(item.proposedDeliveryDate)) : '', + technicalSpecification: item.technicalSpecification || '' + } + } + return { prItemId: item.id, bidUnitPrice: 0, @@ -288,22 +305,22 @@ export function PrItemsPricingTable({ <Table> <TableHeader> <TableRow> - <TableHead>아이템번호</TableHead> - <TableHead>PR번호</TableHead> - <TableHead>품목정보</TableHead> - <TableHead>자재내역</TableHead> + <TableHead>자재번호</TableHead> + <TableHead>자재명</TableHead> + <TableHead>SHI 납품예정일</TableHead> + <TableHead>업체 납품예정일</TableHead> <TableHead>수량</TableHead> - <TableHead>단위</TableHead> + <TableHead>구매단위</TableHead> <TableHead>가격단위</TableHead> - <TableHead>중량</TableHead> - <TableHead>중량단위</TableHead> <TableHead>구매단위</TableHead> - <TableHead>SHI 납품요청일</TableHead> + <TableHead>총중량</TableHead> + <TableHead>중량단위</TableHead> <TableHead>입찰단가</TableHead> <TableHead>입찰금액</TableHead> - <TableHead>납품예정일</TableHead> - {/* <TableHead>기술사양</TableHead> */} - <TableHead>SPEC</TableHead> + <TableHead>업체 통화</TableHead> + <TableHead>자재내역상세</TableHead> + <TableHead>스팩</TableHead> + <TableHead>P/R번호</TableHead> </TableRow> </TableHeader> <TableBody> @@ -318,35 +335,46 @@ export function PrItemsPricingTable({ return ( <TableRow key={item.id}> - <TableCell className="font-medium"> - {item.itemNumber || '-'} - </TableCell> - <TableCell>{item.prNumber || '-'}</TableCell> <TableCell> - <div className="max-w-32 truncate" title={item.itemInfo || ''}> - {item.itemInfo || '-'} - </div> + {item.materialNumber || '-'} </TableCell> <TableCell> <div className="max-w-32 truncate" title={item.materialInfo || ''}> {item.materialInfo || '-'} </div> </TableCell> + <TableCell> + {item.requestedDeliveryDate ? + formatDate(new Date(item.requestedDeliveryDate), 'KR') : '-' + } + </TableCell> + <TableCell> + {readOnly ? ( + quotation.proposedDeliveryDate ? + formatDate(quotation.proposedDeliveryDate, 'KR') : '-' + ) : ( + <Input + type="date" + value={quotation.proposedDeliveryDate} + onChange={(e) => updateQuotation( + item.id, + 'proposedDeliveryDate', + e.target.value + )} + className="w-40" + /> + )} + </TableCell> <TableCell className="text-right"> {item.quantity ? parseFloat(item.quantity).toLocaleString() : '-'} </TableCell> <TableCell>{item.quantityUnit || '-'}</TableCell> <TableCell>{item.priceUnit || '-'}</TableCell> + <TableCell>{item.purchaseUnit || '-'}</TableCell> <TableCell className="text-right"> {item.totalWeight ? parseFloat(item.totalWeight).toLocaleString() : '-'} </TableCell> <TableCell>{item.weightUnit || '-'}</TableCell> - <TableCell>{item.purchaseUnit || '-'}</TableCell> - <TableCell> - {item.requestedDeliveryDate ? - formatDate(item.requestedDeliveryDate, 'KR') : '-' - } - </TableCell> <TableCell> {readOnly ? ( <span className="font-medium"> @@ -355,12 +383,23 @@ export function PrItemsPricingTable({ ) : ( <Input type="number" - value={quotation.bidUnitPrice} - onChange={(e) => updateQuotation( - item.id, - 'bidUnitPrice', - parseFloat(e.target.value) || 0 - )} + inputMode="decimal" + min={0} + pattern="^(0|[1-9][0-9]*)(\.[0-9]+)?$" + value={quotation.bidUnitPrice === 0 ? '' : quotation.bidUnitPrice} + onChange={(e) => { + let value = e.target.value + if (/^0[0-9]+/.test(value)) { + value = value.replace(/^0+/, '') + if (value === '') value = '0' + } + const numericValue = parseFloat(value) + updateQuotation( + item.id, + 'bidUnitPrice', + isNaN(numericValue) ? 0 : numericValue + ) + }} className="w-32 text-right" placeholder="단가" /> @@ -371,42 +410,12 @@ export function PrItemsPricingTable({ {formatCurrency(quotation.bidAmount)} </div> </TableCell> + <TableCell>{currency}</TableCell> <TableCell> - {readOnly ? ( - quotation.proposedDeliveryDate ? - formatDate(quotation.proposedDeliveryDate, 'KR') : '-' - ) : ( - <Input - type="date" - value={quotation.proposedDeliveryDate} - onChange={(e) => updateQuotation( - item.id, - 'proposedDeliveryDate', - e.target.value - )} - className="w-40" - /> - )} + <div className="max-w-48 truncate" title={item.specification || ''}> + {item.specification || '-'} + </div> </TableCell> - {/* <TableCell> - {readOnly ? ( - <div className="max-w-32 truncate" title={quotation.technicalSpecification || ''}> - {quotation.technicalSpecification || '-'} - </div> - ) : ( - <Textarea - value={quotation.technicalSpecification} - onChange={(e) => updateQuotation( - item.id, - 'technicalSpecification', - e.target.value - )} - placeholder="기술사양 입력" - className="w-48 min-h-[60px]" - rows={2} - /> - )} - </TableCell> */} <TableCell> {item.hasSpecDocument ? ( <div className="space-y-1"> @@ -435,6 +444,7 @@ export function PrItemsPricingTable({ <Badge variant="outline">SPEC 없음</Badge> )} </TableCell> + <TableCell>{item.prNumber || '-'}</TableCell> </TableRow> ) })} diff --git a/lib/bidding/vendor/partners-bidding-detail.tsx b/lib/bidding/vendor/partners-bidding-detail.tsx index 03429cca..bf76de62 100644 --- a/lib/bidding/vendor/partners-bidding-detail.tsx +++ b/lib/bidding/vendor/partners-bidding-detail.tsx @@ -81,6 +81,7 @@ interface BiddingDetail { targetPrice: number | null status: string bidPicName: string | null // 입찰담당자 + bidPicPhone?: string | null // 입찰담당자 전화번호 supplyPicName: string | null // 조달담당자 biddingCompanyId: number biddingId: number @@ -122,6 +123,11 @@ interface PrItem { materialWeight: string | null prNumber: string | null hasSpecDocument: boolean | null + specification: string | null + bidUnitPrice?: string | number | null + bidAmount?: string | number | null + proposedDeliveryDate?: string | Date | null + technicalSpecification?: string | null } interface BiddingPrItemQuotation { @@ -239,7 +245,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD console.error('Failed to get bidding details:', error) return null }), - getPrItemsForBidding(biddingId).catch(error => { + getPrItemsForBidding(biddingId, companyId).catch(error => { console.error('Failed to get PR items:', error) return [] }), @@ -302,50 +308,33 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD // PR 아이템 설정 setPrItems(prItemsResult) + // PR 아이템 결과로부터 견적 정보 추출 및 설정 + if (Array.isArray(prItemsResult) && prItemsResult.length > 0) { + const initialQuotations = prItemsResult.map((item: any) => ({ + prItemId: item.id, + bidUnitPrice: item.bidUnitPrice ? Number(item.bidUnitPrice) : 0, + bidAmount: item.bidAmount ? Number(item.bidAmount) : 0, + proposedDeliveryDate: item.proposedDeliveryDate ? (item.proposedDeliveryDate instanceof Date ? item.proposedDeliveryDate.toISOString().split('T')[0] : item.proposedDeliveryDate) : undefined, + technicalSpecification: item.technicalSpecification || undefined + })); + setPrItemQuotations(initialQuotations); + + // 총 금액 계산 + const total = initialQuotations.reduce((sum: number, q: any) => sum + q.bidAmount, 0); + setTotalQuotationAmount(total); + + // 응찰 확정 시 총 금액 설정 + if (total > 0 && result?.isBiddingParticipated === true) { + setResponseData(prev => ({ + ...prev, + finalQuoteAmount: total.toString() + })); + } + } + // 입찰 데이터를 본입찰용으로 로드 (응찰 확정 시 또는 입찰이 있는 경우) if (result?.biddingCompanyId) { try { - // 입찰 데이터를 가져와서 본입찰용으로 변환 - const preQuoteData = await getPartnerBiddingItemQuotations(result.biddingCompanyId) - - if (preQuoteData && Array.isArray(preQuoteData) && preQuoteData.length > 0) { - console.log('입찰 데이터:', preQuoteData) - - // 입찰 데이터를 본입찰 포맷으로 변환 - const convertedQuotations = preQuoteData - .filter(item => item && typeof item === 'object' && item.prItemId) - .map(item => ({ - prItemId: item.prItemId, - bidUnitPrice: item.bidUnitPrice, - bidAmount: item.bidAmount, - proposedDeliveryDate: item.proposedDeliveryDate || undefined, - technicalSpecification: item.technicalSpecification || undefined - })) - - console.log('변환된 입찰 데이터:', convertedQuotations) - - if (Array.isArray(convertedQuotations) && convertedQuotations.length > 0) { - setPrItemQuotations(convertedQuotations) - - // 총 금액 계산 - const total = convertedQuotations.reduce((sum, q) => { - const amount = Number(q.bidAmount) || 0 - return sum + amount - }, 0) - setTotalQuotationAmount(total) - console.log('계산된 총 금액:', total) - - // 응찰 확정 시에만 입찰 금액을 finalQuoteAmount로 설정 - if (total > 0 && result?.isBiddingParticipated === true) { - console.log('응찰 확정됨, 입찰 금액 설정:', total) - console.log('입찰 금액을 finalQuoteAmount로 설정:', total) - setResponseData(prev => ({ - ...prev, - finalQuoteAmount: total.toString() - })) - } - } - } // 연동제 데이터 로드 (입찰에서 답변했으면 로드, 아니면 입찰 조건 확인) if (result.priceAdjustmentResponse !== null) { @@ -833,9 +822,16 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD </div> <div> <Label className="text-sm font-medium text-muted-foreground">입찰담당자</Label> - <div className="flex items-center gap-2 mt-1"> - <User className="w-4 h-4" /> - <span>{biddingDetail.bidPicName || '미설정'}</span> + <div className="flex flex-col mt-1"> + <div className="flex items-center gap-2"> + <User className="w-4 h-4" /> + <span>{biddingDetail.bidPicName || '미설정'}</span> + </div> + {biddingDetail.bidPicPhone && ( + <div className="text-xs text-muted-foreground ml-6"> + {biddingDetail.bidPicPhone} + </div> + )} </div> </div> <div> @@ -868,11 +864,12 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD <Label className="text-sm font-medium text-muted-foreground mb-2 block">제출 마감 정보</Label> {(() => { const now = new Date() - const deadline = new Date(biddingDetail.submissionEndDate.toISOString().slice(0, 16).replace('T', ' ')) + const deadline = new Date(biddingDetail.submissionEndDate) // isExpired 상태 사용 const timeLeft = deadline.getTime() - now.getTime() const daysLeft = Math.floor(timeLeft / (1000 * 60 * 60 * 24)) const hoursLeft = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) + const kstDeadline = new Date(deadline.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ') return ( <div className={`p-3 rounded-lg border-2 ${ @@ -887,7 +884,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD <Calendar className="w-5 h-5" /> <span className="font-medium">제출 마감일:</span> <span className="text-lg font-semibold"> - {biddingDetail.submissionEndDate.toISOString().slice(0, 16).replace('T', ' ')} + {kstDeadline} </span> </div> {isExpired ? ( @@ -921,7 +918,13 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD <div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm"> {biddingDetail.submissionStartDate && biddingDetail.submissionEndDate && ( <div> - <span className="font-medium">입찰서 제출기간:</span> {new Date(biddingDetail.submissionStartDate).toISOString().slice(0, 16).replace('T', ' ')} ~ {new Date(biddingDetail.submissionEndDate).toISOString().slice(0, 16).replace('T', ' ')} + <span className="font-medium">입찰서 제출기간:</span> {(() => { + const start = new Date(biddingDetail.submissionStartDate!) + const end = new Date(biddingDetail.submissionEndDate!) + const kstStart = new Date(start.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ') + const kstEnd = new Date(end.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ') + return `${kstStart} ~ ${kstEnd}` + })()} </div> )} {biddingDetail.evaluationDate && ( @@ -1080,45 +1083,75 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD </CardContent> </Card> ) : biddingDetail.isBiddingParticipated === null ? ( - /* 참여 의사 확인 섹션 */ - <Card> - <CardHeader> - <CardTitle className="flex items-center gap-2"> - <Users className="w-5 h-5" /> - 입찰 참여 의사 확인 - </CardTitle> - </CardHeader> - <CardContent> - <div className="text-center py-8"> - <div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4"> - <Users className="w-8 h-8 text-primary" /> - </div> - <h3 className="text-lg font-semibold mb-2">이 입찰에 참여하시겠습니까?</h3> - <p className="text-muted-foreground mb-6"> - 참여를 선택하시면 입찰 작성 및 제출이 가능합니다. - </p> - <div className="flex justify-center gap-4"> - <Button - onClick={() => handleParticipationDecision(true)} - disabled={isUpdatingParticipation || isExpired} - className="min-w-[120px]" - > - <CheckCircle className="w-4 h-4 mr-2" /> - {isExpired ? '마감됨' : '참여하기'} - </Button> - <Button - onClick={() => handleParticipationDecision(false)} - disabled={isUpdatingParticipation || isExpired} - variant="destructive" - className="min-w-[120px]" - > - <XCircle className="w-4 h-4 mr-2" /> - 미참여 - </Button> + <> + {/* 품목 정보 확인 (Read Only) */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Package className="w-5 h-5" /> + 입찰 품목 정보 + </CardTitle> + </CardHeader> + <CardContent> + {prItems.length > 0 ? ( + <PrItemsPricingTable + prItems={prItems} + initialQuotations={prItemQuotations} + currency={biddingDetail?.currency || 'KRW'} + onQuotationsChange={() => {}} + onTotalAmountChange={() => {}} + readOnly={true} + /> + ) : ( + <div className="border rounded-lg p-4 bg-muted/20"> + <p className="text-sm text-muted-foreground text-center py-4"> + 등록된 품목이 없습니다. + </p> + </div> + )} + </CardContent> + </Card> + + {/* 참여 의사 확인 섹션 */} + <Card className="mt-6"> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Users className="w-5 h-5" /> + 입찰 참여 의사 확인 + </CardTitle> + </CardHeader> + <CardContent> + <div className="text-center py-8"> + <div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4"> + <Users className="w-8 h-8 text-primary" /> + </div> + <h3 className="text-lg font-semibold mb-2">이 입찰에 참여하시겠습니까?</h3> + <p className="text-muted-foreground mb-6"> + 참여를 선택하시면 입찰 작성 및 제출이 가능합니다. + </p> + <div className="flex justify-center gap-4"> + <Button + onClick={() => handleParticipationDecision(true)} + disabled={isUpdatingParticipation || isExpired} + className="min-w-[120px]" + > + <CheckCircle className="w-4 h-4 mr-2" /> + {isExpired ? '마감됨' : '참여하기'} + </Button> + <Button + onClick={() => handleParticipationDecision(false)} + disabled={isUpdatingParticipation || isExpired} + variant="destructive" + className="min-w-[120px]" + > + <XCircle className="w-4 h-4 mr-2" /> + 미참여 + </Button> + </div> </div> - </div> - </CardContent> - </Card> + </CardContent> + </Card> + </> ) : biddingDetail.isBiddingParticipated === true ? ( /* 응찰 폼 섹션 */ <Card> diff --git a/lib/bidding/vendor/partners-bidding-list-columns.tsx b/lib/bidding/vendor/partners-bidding-list-columns.tsx index a090c3fe..64b4bebf 100644 --- a/lib/bidding/vendor/partners-bidding-list-columns.tsx +++ b/lib/bidding/vendor/partners-bidding-list-columns.tsx @@ -230,11 +230,53 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL // 입찰명 columnHelper.accessor('title', { header: '입찰명', - cell: ({ row }) => ( - <div className="max-w-48 truncate" title={row.original.title}> - {row.original.title} - </div> - ), + cell: ({ row }) => { + const handleTitleClick = (e: React.MouseEvent) => { + e.stopPropagation() + + // 사양설명회 참석여부 체크 + const hasSpecMeeting = row.original.hasSpecificationMeeting + const isAttending = row.original.isAttendingMeeting + + // 사양설명회가 있고, 참석여부가 아직 설정되지 않은 경우 + if (hasSpecMeeting && isAttending === null) { + toast.warning('사양설명회 참석여부 필요', { + description: '사전견적 또는 입찰을 진행하기 전에 사양설명회 참석여부를 먼저 설정해주세요.', + duration: 5000, + }) + return + } + + // 입찰기간 체크 (현 시간 기준으로 입찰기간 시작 전이면 접근 불가) + const now = new Date() + const startDate = row.original.submissionStartDate ? new Date(row.original.submissionStartDate) : null + + if (startDate && now < startDate) { + toast.warning('입찰기간 전 접근 제한', { + description: `입찰기간이 아직 시작되지 않았습니다`, + duration: 5000, + }) + return + } + + if (setRowAction) { + setRowAction({ + type: 'view', + row: { original: row.original } + }) + } + } + + return ( + <div + className="max-w-48 truncate cursor-pointer underline font-bold hover:text-blue-600" + title={row.original.title} + onClick={handleTitleClick} + > + {row.original.title} + </div> + ) + }, }), // 사양설명회 diff --git a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts index 1a5089eb..9bdd238d 100644 --- a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts +++ b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts @@ -215,6 +215,17 @@ export async function mapECCBiddingHeaderToBidding( // 첫번째 PR Item 가져오기 (관련 아이템들 중 첫번째) const firstItem = eccItems.find(item => item.ANFNR === eccHeader.ANFNR); + + // 대표 PR Item 찾기 (ZCON_NO_PO와 BANFN이 같은 아이템 - RFQ 매퍼 로직 반영) + const representativeItem = eccItems.find(item => + item.ANFNR === eccHeader.ANFNR && + item.ZCON_NO_PO && + item.ZCON_NO_PO.trim() && + item.BANFN === item.ZCON_NO_PO.trim() + ); + + // 타겟 아이템 설정 (대표 아이템 우선, 없으면 첫번째 아이템) + const targetItem = representativeItem || firstItem; // 날짜 파싱 (실패시 현재 Date 들어감) const createdAt = parseSAPDateTime(eccHeader.ZRFQ_TRS_DT || null, eccHeader.ZRFQ_TRS_TM || null); @@ -222,23 +233,27 @@ export async function mapECCBiddingHeaderToBidding( // 담당자 찾기 const inChargeUserInfo = await findUserInfoByEKGRP(eccHeader.EKGRP || null); - // 첫번째 PR Item 기반으로 projectName, itemName 설정 + // 타겟 PR Item 기반으로 projectName, itemName 설정 let projectName: string | null = null; let itemName: string | null = null; let prNumber: string | null = null; - if (firstItem) { - // projectName: 첫번째 PR Item의 PSPID와 projects.code 매칭 - const projectInfo = await findProjectInfoByPSPID(firstItem.PSPID || null); + if (targetItem) { + // projectName: 타겟 PR Item의 PSPID와 projects.code 매칭 + const projectInfo = await findProjectInfoByPSPID(targetItem.PSPID || null); if (projectInfo) { projectName = projectInfo.name; } - // itemName: 첫번째 PR Item의 MATNR로 MDG에서 ZZNAME 조회 - itemName = await findMaterialNameByMATNR(firstItem.MATNR || null); + // itemName: 타겟 PR Item의 TXZ01(내역) 우선 사용, 없으면 MATNR로 조회 + if (targetItem.TXZ01) { + itemName = targetItem.TXZ01; + } else { + itemName = await findMaterialNameByMATNR(targetItem.MATNR || null); + } - // prNumber: 첫번째 PR의 ZREQ_FN 값 - prNumber = firstItem.ZREQ_FN || null; + // prNumber: 대표 PR의 BANFN 또는 타겟 PR의 ZREQ_FN 값 + prNumber = representativeItem?.BANFN || targetItem.ZREQ_FN || null; } // 매핑 @@ -246,9 +261,9 @@ export async function mapECCBiddingHeaderToBidding( biddingNumber, // 생성된 Bidding 코드 originalBiddingNumber: eccHeader.ANFNR || null, // 원입찰번호 revision: 0, // 기본 리비전 0 (I/F 해서 가져온 건 보낸 적 없으므로 0 고정) - projectName, // 첫번째 PR Item의 PSPID로 찾은 프로젝트 이름 - itemName, // 첫번째 PR Item의 MATNR로 조회한 자재명 - title: `${firstItem?.PSPID || ''}${itemName || ''}입찰`, // PSPID+자재그룹명+계약구분+입찰, 계약구분은 없으니까 제외했음 + projectName, // 타겟 PR Item의 PSPID로 찾은 프로젝트 이름 + itemName, // 타겟 PR Item 정보로 조회한 자재명/내역 + title: `${targetItem?.PSPID || ''}${itemName || ''}입찰`, // PSPID+자재명+입찰 description: null, // 계약 정보 - ECC에서 제공되지 않으므로 기본값 설정 @@ -269,7 +284,7 @@ export async function mapECCBiddingHeaderToBidding( hasSpecificationMeeting: false, // 기본값 처리하고, 입찰관리상세에서 사용자가 관리 // 예산 및 가격 정보 - currency: firstItem?.WAERS1, + currency: targetItem?.WAERS1, budget: null, targetPrice: null, targetPriceCalculationCriteria: null, @@ -286,6 +301,7 @@ export async function mapECCBiddingHeaderToBidding( // 담당자 정보 - EKGRP 기반으로 설정 (입찰담당자로 매핑) bidPicId: inChargeUserInfo?.userId || null, bidPicName: inChargeUserInfo?.userName || null, + bidPicCode: eccHeader.EKGRP || null, // 메타 정보 remarks: `ECC ANFNR: ${eccHeader.ANFNR}`, @@ -409,7 +425,15 @@ export async function mapAndSaveECCBiddingData( // 2) 헤더별 관련 아이템 그룹핑 + 헤더 매핑을 병렬로 수행 - 유효한 헤더만 사용 const biddingGroups = await Promise.all( validHeaders.map(async (eccHeader) => { - const relatedItems = eccItems.filter((item) => item.ANFNR === eccHeader.ANFNR); + // 아이템 필터링 및 정렬 (ANFPS 오름차순) + const relatedItems = eccItems + .filter((item) => item.ANFNR === eccHeader.ANFNR) + .sort((a, b) => { + const anfpsA = parseInt(a.ANFPS || '0', 10); + const anfpsB = parseInt(b.ANFPS || '0', 10); + return anfpsA - anfpsB; + }); + const biddingNumber = biddingCodeMap.get(eccHeader.ANFNR) || `BID${eccHeader.EKGRP || 'UNKNOWN'}00001`; // 헤더 매핑 시 아이템 정보와 생성된 Bidding 코드 전달 const biddingData = await mapECCBiddingHeaderToBidding(eccHeader, relatedItems, biddingNumber); |
