diff options
17 files changed, 138 insertions, 46 deletions
diff --git a/app/[lng]/evcp/(evcp)/(procurement)/rfq-last/[id]/vendor/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/rfq-last/[id]/vendor/page.tsx index d390eab7..18e17704 100644 --- a/app/[lng]/evcp/(evcp)/(procurement)/rfq-last/[id]/vendor/page.tsx +++ b/app/[lng]/evcp/(evcp)/(procurement)/rfq-last/[id]/vendor/page.tsx @@ -81,7 +81,7 @@ export default async function VendorPage(props: VendorPageProps) { {/* 응답 상태 요약 카드 */} - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5"> + {/* <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5"> <VendorResponseStatusCard title="전체 벤더" count={statusSummary.total} @@ -112,7 +112,7 @@ export default async function VendorPage(props: VendorPageProps) { icon={FileText} variant="primary" /> - </div> + </div> */} {/* 벤더 목록 테이블 */} <Card> diff --git a/app/[lng]/partners/(partners)/settings/page.tsx b/app/[lng]/partners/(partners)/settings/page.tsx index 727be46b..80f17c7c 100644 --- a/app/[lng]/partners/(partners)/settings/page.tsx +++ b/app/[lng]/partners/(partners)/settings/page.tsx @@ -135,6 +135,9 @@ export default function SettingsAccountPage() { <SimpleReAuthModal isOpen={showReAuthModal} onSuccess={handleReAuthSuccess} + onClose={() => { + router.refresh() + }} userEmail={userEmail} /> </div> diff --git a/components/auth/simple-reauth-modal.tsx b/components/auth/simple-reauth-modal.tsx index f00674e3..80f03ad9 100644 --- a/components/auth/simple-reauth-modal.tsx +++ b/components/auth/simple-reauth-modal.tsx @@ -36,13 +36,15 @@ type ReAuthFormValues = z.infer<typeof reAuthSchema> interface SimpleReAuthModalProps { isOpen: boolean onSuccess: () => void + onClose?: () => void userEmail: string } -export function SimpleReAuthModal({ - isOpen, - onSuccess, - userEmail +export function SimpleReAuthModal({ + isOpen, + onSuccess, + onClose, + userEmail }: SimpleReAuthModalProps) { const [isLoading, setIsLoading] = React.useState(false) const [attemptCount, setAttemptCount] = React.useState(0) @@ -114,11 +116,18 @@ export function SimpleReAuthModal({ if (!isOpen) { form.reset() setAttemptCount(0) + if (onClose) { + // 모달이 닫힐 때 정리 작업 + } } - }, [isOpen, form]) + }, [isOpen, form, onClose]) return ( - <Dialog open={isOpen} onOpenChange={() => {}}> + <Dialog open={isOpen} onOpenChange={(open) => { + if (!open && onClose) { + onClose() + } + }}> <DialogContent className="sm:max-w-[400px]"> <DialogHeader> <DialogTitle className="flex items-center gap-2"> diff --git a/components/bidding/create/bidding-create-dialog.tsx b/components/bidding/create/bidding-create-dialog.tsx index 30550ca4..bdb00a01 100644 --- a/components/bidding/create/bidding-create-dialog.tsx +++ b/components/bidding/create/bidding-create-dialog.tsx @@ -609,7 +609,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 }) => (
@@ -634,7 +634,7 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp <FormMessage />
</FormItem>
)}
- />
+ /> */}
<FormField
control={form.control}
diff --git a/components/bidding/manage/bidding-basic-info-editor.tsx b/components/bidding/manage/bidding-basic-info-editor.tsx index 2f55d563..e92c39a5 100644 --- a/components/bidding/manage/bidding-basic-info-editor.tsx +++ b/components/bidding/manage/bidding-basic-info-editor.tsx @@ -1090,7 +1090,7 @@ export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProp /> </div> - <div className="flex flex-row items-center justify-between rounded-lg border p-3"> + {/* <div className="flex flex-row items-center justify-between rounded-lg border p-3"> <div className="space-y-0.5"> <FormLabel className="text-base">연동제 적용 가능</FormLabel> <div className="text-sm text-muted-foreground"> @@ -1106,7 +1106,7 @@ export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProp })) }} /> - </div> + </div> */} </div> {/* 5행: 스페어파트 옵션 */} diff --git a/components/bidding/manage/bidding-companies-editor.tsx b/components/bidding/manage/bidding-companies-editor.tsx index 1ce8b014..4992c2ab 100644 --- a/components/bidding/manage/bidding-companies-editor.tsx +++ b/components/bidding/manage/bidding-companies-editor.tsx @@ -5,13 +5,14 @@ import { Building, User, Plus, Trash2 } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' -import { - getBiddingVendors, +import { + getBiddingVendors, getBiddingCompanyContacts, createBiddingCompanyContact, deleteBiddingCompanyContact, getVendorContactsByVendorId, - updateBiddingCompanyPriceAdjustmentQuestion + updateBiddingCompanyPriceAdjustmentQuestion, + getBiddingById } from '@/lib/bidding/service' import { deleteBiddingCompany } from '@/lib/bidding/pre-quote/service' import { BiddingDetailVendorCreateDialog } from './bidding-detail-vendor-create-dialog' @@ -87,7 +88,11 @@ export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProp const [isLoadingContacts, setIsLoadingContacts] = React.useState(false) // 각 업체별 첫 번째 담당자 정보 저장 (vendorId -> 첫 번째 담당자) const [vendorFirstContacts, setVendorFirstContacts] = React.useState<Map<number, BiddingCompanyContact>>(new Map()) - + + // 입찰 정보 (단수/복수 낙찰 확인용) + const [biddingInfo, setBiddingInfo] = React.useState<any>(null) + const [isLoadingBiddingInfo, setIsLoadingBiddingInfo] = React.useState(false) + // 담당자 추가 다이얼로그 const [addContactDialogOpen, setAddContactDialogOpen] = React.useState(false) const [newContact, setNewContact] = React.useState({ @@ -141,6 +146,29 @@ export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProp } }, [biddingId]) + // 입찰 정보 로딩 + const loadBiddingInfo = React.useCallback(async () => { + setIsLoadingBiddingInfo(true) + try { + const result = await getBiddingById(biddingId) + if (result) { + setBiddingInfo(result) + } else { + console.error('Failed to load bidding info') + setBiddingInfo(null) + } + } catch (error) { + console.error('Failed to load bidding info:', error) + setBiddingInfo(null) + } finally { + setIsLoadingBiddingInfo(false) + } + }, [biddingId]) + + // 단수 입찰 여부 확인 및 업체 추가 제한 + const isSingleAwardBidding = biddingInfo?.awardCount === 'single' + const canAddVendor = !isSingleAwardBidding || vendors.length === 0 + // 데이터 로딩 React.useEffect(() => { const loadVendors = async () => { @@ -177,7 +205,7 @@ export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProp console.error(`Failed to load contact for vendor ${vendor.companyId}:`, error) } }) - + await Promise.all(contactPromises) setVendorFirstContacts(firstContactsMap) } else { @@ -194,7 +222,8 @@ export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProp } loadVendors() - }, [biddingId]) + loadBiddingInfo() + }, [biddingId, loadBiddingInfo]) // 업체 선택 핸들러 (단일 선택) const handleVendorSelect = async (vendor: QuotationVendor) => { @@ -482,6 +511,7 @@ export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProp <p className="text-sm text-muted-foreground mt-1"> 입찰에 참여하는 업체들을 관리합니다. 업체를 선택하면 하단에 담당자 목록이 표시됩니다. </p> + <p className="text-sm text-muted-foreground mt-1"> 단수 입찰의 경우 1개 업체만 등록 가능합니다. </p> </div> <Button onClick={() => setAddVendorDialogOpen(true)} className="flex items-center gap-2"> <Plus className="h-4 w-4" /> @@ -657,6 +687,8 @@ export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProp open={addVendorDialogOpen} onOpenChange={setAddVendorDialogOpen} onSuccess={reloadVendors} + isSingleAwardBidding={isSingleAwardBidding} + currentVendorCount={vendors.length} /> {/* 담당자 추가 다이얼로그 (직접 입력) */} diff --git a/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx index ed3e2be6..205224b9 100644 --- a/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx +++ b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx @@ -39,6 +39,8 @@ interface BiddingDetailVendorCreateDialogProps { open: boolean onOpenChange: (open: boolean) => void onSuccess: () => void + isSingleAwardBidding?: boolean + currentVendorCount?: number } interface Vendor { @@ -57,7 +59,9 @@ export function BiddingDetailVendorCreateDialog({ biddingId, open, onOpenChange, - onSuccess + onSuccess, + isSingleAwardBidding = false, + currentVendorCount = 0 }: BiddingDetailVendorCreateDialogProps) { const { toast } = useToast() const [isPending, startTransition] = useTransition() @@ -100,6 +104,16 @@ export function BiddingDetailVendorCreateDialog({ // 벤더 추가 const handleAddVendor = (vendor: Vendor) => { + // 단수 입찰이고 이미 업체가 선택되었거나 기존 업체가 있는 경우 제한 + if (isSingleAwardBidding && (selectedVendorsWithQuestion.length > 0 || currentVendorCount > 0)) { + toast({ + title: '제한 사항', + description: '단수 입찰의 경우 1개 업체만 등록 가능합니다.', + variant: 'destructive', + }) + return + } + if (!selectedVendorsWithQuestion.find(v => v.vendor.id === vendor.id)) { setSelectedVendorsWithQuestion([ ...selectedVendorsWithQuestion, diff --git a/db/schema/bidding.ts b/db/schema/bidding.ts index c8382ea6..2f0dd07f 100644 --- a/db/schema/bidding.ts +++ b/db/schema/bidding.ts @@ -163,6 +163,7 @@ export const biddings = pgTable('biddings', { // 계약 정보 contractType: contractTypeEnum('contract_type').notNull(), // 계약구분 + noticeType: varchar('notice_type', { length: 50 }).default('standard'), // 입찰공고 타입 biddingType: biddingTypeEnum('bidding_type').notNull(), // 입찰유형 awardCount: awardCountEnum('award_count').default('single'), // 낙찰수 // contractPeriod: varchar('contract_period', { length: 100 }), // 계약기간 @@ -592,6 +593,7 @@ export type Bidding = typeof biddings.$inferSelect & { bidPicName?: string | null supplyPicId?: number | null supplyPicName?: string | null + noticeType?: string | null } export type NewBidding = typeof biddings.$inferInsert diff --git a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx index af7d70e1..6f35405d 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx @@ -79,7 +79,7 @@ export function getBiddingDetailVendorColumns({ <div className="text-right font-mono"> {hasAmount ? ( <button - onClick={() => onViewItemDetails?.(row.original)} + onClick={() => onViewQuotationHistory?.(row.original)} className="text-primary hover:text-primary/80 hover:underline cursor-pointer" title="품목별 견적 상세 보기" > diff --git a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx index 315c2aac..cfdab9c6 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx @@ -133,11 +133,12 @@ export function BiddingDetailVendorTableContent({ try { const { getQuotationHistory } = await import('@/lib/bidding/selection/actions') const result = await getQuotationHistory(biddingId, vendor.vendorId) + console.log(result) if (result.success) { setQuotationHistoryData({ vendorName: vendor.vendorName, - history: result.data.history, + history: result.data?.history || [], biddingCurrency: bidding.currency || 'KRW', targetPrice: bidding.targetPrice ? parseFloat(bidding.targetPrice.toString()) : undefined }) diff --git a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx index 491f29f7..c1677ae7 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx @@ -164,7 +164,7 @@ export function BiddingDetailVendorToolbarActions({ title: "성공", description: result.message, }) - router.refresh() + router.push(`/evcp/bid`) onSuccess() } else { toast({ @@ -180,8 +180,8 @@ export function BiddingDetailVendorToolbarActions({ <> <div className="flex items-center gap-2"> {/* 상태별 액션 버튼 */} - {/* 차수증가: 입찰평가중 또는 입찰 진행중 상태 */} - {(bidding.status === 'evaluation_of_bidding ' || bidding.status === 'bidding_opened') && ( + {/* 차수증가: 입찰공고 또는 입찰평가중 상태에서만 */} + {(bidding.status === 'evaluation_of_bidding' || bidding.status === 'bidding_opened') && ( <Button variant="outline" size="sm" @@ -193,8 +193,8 @@ export function BiddingDetailVendorToolbarActions({ </Button> )} - {/* 유찰/낙찰: 입찰 진행중 상태에서만 */} - {bidding.status === 'bidding_opened' && ( + {/* 유찰/낙찰: 입찰공고 또는 입찰평가중 상태에서만 */} + {(bidding.status === 'bidding_opened' || bidding.status === 'evaluation_of_bidding') && ( <> <Button variant="destructive" diff --git a/lib/bidding/list/biddings-table-columns.tsx b/lib/bidding/list/biddings-table-columns.tsx index f5e77d03..92b2fe42 100644 --- a/lib/bidding/list/biddings-table-columns.tsx +++ b/lib/bidding/list/biddings-table-columns.tsx @@ -29,6 +29,7 @@ import { biddingStatusLabels, contractTypeLabels, biddingTypeLabels, + biddingNoticeTypeLabels, } from "@/db/schema" import { formatDate } from "@/lib/utils" diff --git a/lib/bidding/receive/biddings-receive-table.tsx b/lib/bidding/receive/biddings-receive-table.tsx index 873f3fa4..1c5dd16b 100644 --- a/lib/bidding/receive/biddings-receive-table.tsx +++ b/lib/bidding/receive/biddings-receive-table.tsx @@ -283,7 +283,7 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { variant="outline"
size="sm"
onClick={handleOpenBidding}
- disabled={!selectedBiddingForAction || !canOpen || isOpeningBidding}
+ disabled={!selectedBiddingForAction || isOpeningBidding}
>
{isOpeningBidding && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
개찰
diff --git a/lib/bidding/selection/actions.ts b/lib/bidding/selection/actions.ts index e17e9292..16b2c083 100644 --- a/lib/bidding/selection/actions.ts +++ b/lib/bidding/selection/actions.ts @@ -127,7 +127,8 @@ export async function getQuotationHistory(biddingId: number, vendorId: number) { )) .limit(1) - if (!companyData.length || !companyData[0].quotationSnapshots) { + // 데이터 존재 여부 및 유효성 체크 + if (!companyData.length || !companyData[0]?.quotationSnapshots) { return { success: true, data: { @@ -136,7 +137,33 @@ export async function getQuotationHistory(biddingId: number, vendorId: number) { } } - const snapshots = companyData[0].quotationSnapshots as any[] + let snapshots = companyData[0].quotationSnapshots + + // quotationSnapshots가 JSONB 타입이므로 파싱이 필요할 수 있음 + if (typeof snapshots === 'string') { + try { + snapshots = JSON.parse(snapshots) + } catch (parseError) { + console.error('Failed to parse quotationSnapshots:', parseError) + return { + success: true, + data: { + history: [] + } + } + } + } + + // snapshots가 배열인지 확인 + if (!Array.isArray(snapshots)) { + console.error('quotationSnapshots is not an array:', typeof snapshots) + return { + success: true, + data: { + history: [] + } + } + } // PR 항목 정보 조회 (스냅샷의 prItemId로 매핑하기 위해) const prItemIds = snapshots.flatMap(snapshot => @@ -146,12 +173,11 @@ export async function getQuotationHistory(biddingId: number, vendorId: number) { const prItems = prItemIds.length > 0 ? await db .select({ id: prItemsForBidding.id, - itemCode: prItemsForBidding.itemCode, - itemName: prItemsForBidding.itemName, - specification: prItemsForBidding.specification, + itemNumber: prItemsForBidding.itemNumber, + itemInfo: prItemsForBidding.itemInfo, quantity: prItemsForBidding.quantity, - unit: prItemsForBidding.unit, - deliveryDate: prItemsForBidding.deliveryDate + quantityUnit: prItemsForBidding.quantityUnit, + requestedDeliveryDate: prItemsForBidding.requestedDeliveryDate }) .from(prItemsForBidding) .where(sql`${prItemsForBidding.id} IN ${prItemIds}`) : [] @@ -181,14 +207,13 @@ export async function getQuotationHistory(biddingId: number, vendorId: number) { const items = snapshot.items?.map((item: any) => { const prItem = prItemMap.get(item.prItemId) return { - itemCode: prItem?.itemCode || `ITEM${item.prItemId}`, - itemName: prItem?.itemName || '품목 정보 없음', - specification: prItem?.specification || item.technicalSpecification || '-', + itemCode: prItem?.itemNumber || `ITEM${item.prItemId}`, + itemName: prItem?.itemInfo || '품목 정보 없음', quantity: prItem?.quantity || 0, - unit: prItem?.unit || 'EA', + unit: prItem?.quantityUnit || 'EA', unitPrice: item.bidUnitPrice, totalPrice: item.bidAmount, - deliveryDate: item.proposedDeliveryDate ? new Date(item.proposedDeliveryDate) : prItem?.deliveryDate ? new Date(prItem.deliveryDate) : new Date() + deliveryDate: item.proposedDeliveryDate ? new Date(item.proposedDeliveryDate) : prItem?.requestedDeliveryDate ? new Date(prItem.requestedDeliveryDate) : new Date() } }) || [] diff --git a/lib/bidding/selection/biddings-selection-columns.tsx b/lib/bidding/selection/biddings-selection-columns.tsx index 0d1a8c9d..8351a0dd 100644 --- a/lib/bidding/selection/biddings-selection-columns.tsx +++ b/lib/bidding/selection/biddings-selection-columns.tsx @@ -5,7 +5,7 @@ import { type ColumnDef } from "@tanstack/react-table" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
- Eye, Calendar, FileText, DollarSign, TrendingUp, TrendingDown
+ Eye, Calendar, FileText, DollarSign, TrendingUp, TrendingDown, MoreHorizontal
} from "lucide-react"
import {
Tooltip,
@@ -242,13 +242,13 @@ export function getBiddingsSelectionColumns({ setRowAction }: GetColumnsProps): // ═══════════════════════════════════════════════════════════════
{
id: "actions",
- header: "액션",
+ header: "작업",
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">메뉴 열기</span>
- <FileText className="h-4 w-4" />
+ <MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
@@ -256,7 +256,7 @@ export function getBiddingsSelectionColumns({ setRowAction }: GetColumnsProps): <Eye className="mr-2 h-4 w-4" />
상세보기
</DropdownMenuItem>
- {row.original.status === 'bidding_opened' && (
+ {/* {row.original.status === 'bidding_opened' && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setRowAction({ row, type: "close_bidding" })}>
@@ -264,7 +264,7 @@ export function getBiddingsSelectionColumns({ setRowAction }: GetColumnsProps): 입찰마감
</DropdownMenuItem>
</>
- )}
+ )} */}
{row.original.status === 'bidding_closed' && (
<>
<DropdownMenuSeparator />
diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index b60fc73d..cbeeb24a 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -35,7 +35,7 @@ import { } from 'drizzle-orm' import { revalidatePath } from 'next/cache' import { filterColumns } from '@/lib/filter-columns' -import { GetBiddingsSchema } from './validation' +import { GetBiddingsSchema, CreateBiddingSchema } from './validation' @@ -865,6 +865,9 @@ export interface CreateBiddingInput extends CreateBiddingSchema { meetingFiles: File[] } | null + // noticeType 필드 명시적 추가 (CreateBiddingSchema에 포함되어 있지만 타입 추론 문제 해결) + noticeType?: 'standard' | 'facility' | 'unit_price' + // PR 아이템들 (선택사항) prItems?: Array<{ id: string @@ -1174,6 +1177,7 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { description: input.description, contractType: input.contractType, + noticeType: input.noticeType || 'standard', biddingType: input.biddingType, awardCount: input.awardCount, contractStartDate: input.contractStartDate ? parseDate(input.contractStartDate) : new Date(), @@ -1494,6 +1498,7 @@ export async function updateBidding(input: UpdateBiddingInput, userId: string) { if (input.content !== undefined) updateData.content = input.content if (input.contractType !== undefined) updateData.contractType = input.contractType + if (input.noticeType !== undefined) updateData.noticeType = input.noticeType if (input.biddingType !== undefined) updateData.biddingType = input.biddingType if (input.awardCount !== undefined) updateData.awardCount = input.awardCount if (input.contractStartDate !== undefined) updateData.contractStartDate = parseDate(input.contractStartDate) diff --git a/lib/bidding/vendor/partners-bidding-list-columns.tsx b/lib/bidding/vendor/partners-bidding-list-columns.tsx index 5870067a..6e8591c2 100644 --- a/lib/bidding/vendor/partners-bidding-list-columns.tsx +++ b/lib/bidding/vendor/partners-bidding-list-columns.tsx @@ -67,7 +67,7 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL return ( <div className="font-mono text-sm"> <div>{biddingNumber}</div> - <div className="text-muted-foreground text-xs">Rev. {revision ?? 0}</div> + {/* <div className="text-muted-foreground text-xs">Rev. {revision ?? 0}</div> */} {originalBiddingNumber && ( <div className="text-xs text-muted-foreground">원: {originalBiddingNumber}</div> )} |
