summaryrefslogtreecommitdiff
path: root/lib/bidding/detail
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/detail')
-rw-r--r--lib/bidding/detail/service.ts65
-rw-r--r--lib/bidding/detail/table/bidding-detail-header.tsx101
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx41
3 files changed, 63 insertions, 144 deletions
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts
index d9bcb255..e22331bb 100644
--- a/lib/bidding/detail/service.ts
+++ b/lib/bidding/detail/service.ts
@@ -1,7 +1,7 @@
'use server'
import db from '@/db/db'
-import { biddings, prItemsForBidding, biddingDocuments, biddingCompanies, vendors, companyPrItemBids, companyConditionResponses, vendorSelectionResults, BiddingListItem, biddingConditions, priceAdjustmentForms } from '@/db/schema'
+import { biddings, prItemsForBidding, biddingDocuments, biddingCompanies, vendors, companyPrItemBids, companyConditionResponses, vendorSelectionResults, BiddingListItem, biddingConditions, priceAdjustmentForms, users } from '@/db/schema'
import { specificationMeetings } from '@/db/schema/bidding'
import { eq, and, sql, desc, ne } from 'drizzle-orm'
import { revalidatePath, revalidateTag } from 'next/cache'
@@ -9,6 +9,22 @@ import { unstable_cache } from "@/lib/unstable-cache";
import { sendEmail } from '@/lib/mail/sendEmail'
import { saveFile } from '@/lib/file-stroage'
+// userId를 user.name으로 변환하는 유틸리티 함수
+async function getUserNameById(userId: string): Promise<string> {
+ try {
+ const user = await db
+ .select({ name: users.name })
+ .from(users)
+ .where(eq(users.id, parseInt(userId)))
+ .limit(1)
+
+ return user[0]?.name || userId // user.name이 없으면 userId를 그대로 반환
+ } catch (error) {
+ console.error('Failed to get user name:', error)
+ return userId // 에러 시 userId를 그대로 반환
+ }
+}
+
// 데이터 조회 함수들
export interface BiddingDetailData {
bidding: Awaited<ReturnType<typeof getBiddingById>>
@@ -266,6 +282,7 @@ export async function getQuotationVendors(biddingId: number): Promise<QuotationV
awardRatio: vendor.awardRatio ? Number(vendor.awardRatio) : null,
isBiddingParticipated: vendor.isBiddingParticipated,
status: vendor.status as 'pending' | 'submitted' | 'selected' | 'rejected',
+ documents: [], // 빈 배열로 초기화
}))
} catch (error) {
console.error('Failed to get quotation vendors:', error)
@@ -664,22 +681,20 @@ export async function createBiddingDetailVendor(
// 협력업체 정보 저장 - biddingCompanies와 companyConditionResponses 테이블에 레코드 생성
export async function createQuotationVendor(input: any, userId: string) {
try {
+ const userName = await getUserNameById(userId)
const result = await db.transaction(async (tx) => {
// 1. biddingCompanies에 레코드 생성
const biddingCompanyResult = await tx.insert(biddingCompanies).values({
biddingId: input.biddingId,
companyId: input.vendorId,
- quotationAmount: input.quotationAmount,
- currency: input.currency,
- status: input.status,
- awardRatio: input.awardRatio,
+ finalQuoteAmount: input.quotationAmount?.toString(),
+ awardRatio: input.awardRatio?.toString(),
isWinner: false,
contactPerson: input.contactPerson,
contactEmail: input.contactEmail,
contactPhone: input.contactPhone,
- submissionDate: new Date(),
- createdBy: userId,
- updatedBy: userId,
+ finalQuoteSubmittedAt: new Date(),
+ // 스키마에 createdBy, updatedBy 필드가 없으므로 제거
}).returning({ id: biddingCompanies.id })
if (biddingCompanyResult.length === 0) {
@@ -728,6 +743,7 @@ export async function createQuotationVendor(input: any, userId: string) {
// 협력업체 정보 업데이트
export async function updateQuotationVendor(id: number, input: any, userId: string) {
try {
+ const userName = await getUserNameById(userId)
const result = await db.transaction(async (tx) => {
// 1. biddingCompanies 테이블 업데이트
const updateData: any = {}
@@ -735,9 +751,9 @@ export async function updateQuotationVendor(id: number, input: any, userId: stri
if (input.contactPerson !== undefined) updateData.contactPerson = input.contactPerson
if (input.contactEmail !== undefined) updateData.contactEmail = input.contactEmail
if (input.contactPhone !== undefined) updateData.contactPhone = input.contactPhone
- if (input.awardRatio !== undefined) updateData.awardRatio = input.awardRatio
- if (input.status !== undefined) updateData.status = input.status
- updateData.updatedBy = userId
+ if (input.awardRatio !== undefined) updateData.awardRatio = input.awardRatio?.toString()
+ // status 필드가 스키마에 없으므로 제거
+ // updatedBy 필드가 스키마에 없으므로 제거
updateData.updatedAt = new Date()
if (Object.keys(updateData).length > 0) {
@@ -1031,8 +1047,10 @@ export async function registerBidding(biddingId: number, userId: string) {
// 캐시 무효화
revalidateTag(`bidding-${biddingId}`)
+ revalidateTag('bidding-detail')
revalidateTag('quotation-vendors')
revalidateTag('quotation-details')
+ revalidateTag('pr-items')
revalidatePath(`/evcp/bid/${biddingId}`)
return {
@@ -1157,6 +1175,7 @@ export async function createRebidding(biddingId: number, userId: string) {
// 업체 선정 사유 업데이트
export async function updateVendorSelectionReason(biddingId: number, selectedCompanyId: number, selectionReason: string, userId: string) {
try {
+ const userName = await getUserNameById(userId)
// vendorSelectionResults 테이블에 삽입 또는 업데이트
await db
.insert(vendorSelectionResults)
@@ -1164,7 +1183,7 @@ export async function updateVendorSelectionReason(biddingId: number, selectedCom
biddingId,
selectedCompanyId,
selectionReason,
- selectedBy: userId,
+ selectedBy: userName,
selectedAt: new Date(),
createdAt: new Date(),
updatedAt: new Date()
@@ -1174,7 +1193,7 @@ export async function updateVendorSelectionReason(biddingId: number, selectedCom
set: {
selectedCompanyId,
selectionReason,
- selectedBy: userId,
+ selectedBy: userName,
selectedAt: new Date(),
updatedAt: new Date()
}
@@ -1194,6 +1213,7 @@ export async function updateVendorSelectionReason(biddingId: number, selectedCom
// 낙찰용 문서 업로드
export async function uploadAwardDocument(biddingId: number, file: File, userId: string) {
try {
+ const userName = await getUserNameById(userId)
const saveResult = await saveFile({
file,
directory: `biddings/${biddingId}/award`,
@@ -1211,10 +1231,9 @@ export async function uploadAwardDocument(biddingId: number, file: File, userId:
documentType: 'other',
title: '낙찰 관련 문서',
description: '낙찰 관련 첨부파일',
- uploadedBy: userId,
+ uploadedBy: userName,
uploadedAt: new Date(),
- createdAt: new Date(),
- updatedAt: new Date()
+ // createdAt, updatedAt 필드가 스키마에 없으므로 제거
}).returning()
return {
@@ -1292,6 +1311,7 @@ export async function getAwardDocumentForDownload(documentId: number, biddingId:
// 낙찰용 문서 삭제
export async function deleteAwardDocument(documentId: number, biddingId: number, userId: string) {
try {
+ const userName = await getUserNameById(userId)
// 문서 정보 조회
const documents = await db
.select()
@@ -1300,7 +1320,7 @@ export async function deleteAwardDocument(documentId: number, biddingId: number,
eq(biddingDocuments.id, documentId),
eq(biddingDocuments.biddingId, biddingId),
eq(biddingDocuments.documentType, 'other'),
- eq(biddingDocuments.uploadedBy, userId)
+ eq(biddingDocuments.uploadedBy, userName)
))
.limit(1)
@@ -1335,6 +1355,7 @@ export async function deleteAwardDocument(documentId: number, biddingId: number,
// 낙찰 처리 (발주비율과 함께)
export async function awardBidding(biddingId: number, selectionReason: string, userId: string) {
try {
+ const userName = await getUserNameById(userId)
// 낙찰된 업체들 조회 (isWinner가 true인 업체들)
const awardedCompanies = await db
.select({
@@ -1388,7 +1409,7 @@ export async function awardBidding(biddingId: number, selectionReason: string, u
.set({
selectedCompanyId: firstAwardedCompany.companyId,
selectionReason,
- selectedBy: userId,
+ selectedBy: userName,
selectedAt: new Date(),
updatedAt: new Date()
})
@@ -1401,7 +1422,7 @@ export async function awardBidding(biddingId: number, selectionReason: string, u
biddingId,
selectedCompanyId: firstAwardedCompany.companyId,
selectionReason,
- selectedBy: userId,
+ selectedBy: userName,
selectedAt: new Date(),
createdAt: new Date(),
updatedAt: new Date()
@@ -1532,6 +1553,7 @@ export async function saveBiddingDraft(
userId: string
) {
try {
+ const userName = await getUserNameById(userId)
let totalAmount = 0
await db.transaction(async (tx) => {
@@ -1668,7 +1690,7 @@ export interface PartnersBiddingListItem {
// biddings 정보
biddingId: number
biddingNumber: string
- revision: number
+ revision: number | null
projectName: string
itemName: string
title: string
@@ -1883,6 +1905,7 @@ export async function submitPartnerResponse(
userId: string
) {
try {
+ const userName = await getUserNameById(userId)
const result = await db.transaction(async (tx) => {
// 0. 품목별 견적 정보 최종 저장 (본입찰 제출) - Upsert 방식
if (response.prItemQuotations && response.prItemQuotations.length > 0) {
@@ -1981,7 +2004,7 @@ export async function submitPartnerResponse(
const companyUpdateData: any = {
respondedAt: new Date(),
updatedAt: new Date(),
- // updatedBy: userId, // 이 필드가 존재하지 않음
+ // updatedBy: userName, // 이 필드가 존재하지 않음
}
if (response.finalQuoteAmount !== undefined) {
diff --git a/lib/bidding/detail/table/bidding-detail-header.tsx b/lib/bidding/detail/table/bidding-detail-header.tsx
index 2798478c..8d18472f 100644
--- a/lib/bidding/detail/table/bidding-detail-header.tsx
+++ b/lib/bidding/detail/table/bidding-detail-header.tsx
@@ -40,107 +40,6 @@ export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) {
router.push('/evcp/bid')
}
- const handleRegister = () => {
- // 상태 검증
- if (bidding.status !== 'bidding_generated') {
- toast({
- title: '실행 불가',
- description: '입찰 등록은 입찰 생성 상태에서만 가능합니다.',
- variant: 'destructive',
- })
- return
- }
-
- if (!confirm('입찰을 등록하시겠습니까?')) return
-
- startTransition(async () => {
- const result = await registerBidding(bidding.id, 'current-user') // TODO: 실제 사용자 ID
-
- if (result.success) {
- toast({
- title: '성공',
- description: result.message,
- })
- router.refresh()
- } else {
- toast({
- title: '오류',
- description: result.error,
- variant: 'destructive',
- })
- }
- })
- }
-
- const handleMarkAsDisposal = () => {
- // 상태 검증
- if (bidding.status !== 'bidding_closed') {
- toast({
- title: '실행 불가',
- description: '유찰 처리는 입찰 마감 상태에서만 가능합니다.',
- variant: 'destructive',
- })
- return
- }
-
- if (!confirm('입찰을 유찰 처리하시겠습니까?')) return
-
- startTransition(async () => {
- const result = await markAsDisposal(bidding.id, 'current-user') // TODO: 실제 사용자 ID
-
- if (result.success) {
- toast({
- title: '성공',
- description: result.message,
- })
- router.refresh()
- } else {
- toast({
- title: '오류',
- description: result.error,
- variant: 'destructive',
- })
- }
- })
- }
-
- const handleCreateRebidding = () => {
- // 상태 검증
- if (bidding.status !== 'bidding_disposal') {
- toast({
- title: '실행 불가',
- description: '재입찰은 유찰 상태에서만 가능합니다.',
- variant: 'destructive',
- })
- return
- }
-
- if (!confirm('재입찰을 생성하시겠습니까?')) return
-
- startTransition(async () => {
- const result = await createRebidding(bidding.id, 'current-user') // TODO: 실제 사용자 ID
-
- if (result.success) {
- toast({
- title: '성공',
- description: result.message,
- })
- // 새로 생성된 입찰로 이동
- if (result.data) {
- router.push(`/evcp/bid/${result.data.id}`)
- } else {
- router.refresh()
- }
- } else {
- toast({
- title: '오류',
- description: result.error,
- variant: 'destructive',
- })
- }
- })
- }
-
return (
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="px-6 py-4">
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 64c31633..654d9941 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
@@ -106,7 +106,6 @@ export function BiddingDetailVendorToolbarActions({
<>
<div className="flex items-center gap-2">
{/* 상태별 액션 버튼 */}
- {bidding.status === 'set_target_price' && (
<Button
variant="default"
size="sm"
@@ -116,27 +115,25 @@ export function BiddingDetailVendorToolbarActions({
<Send className="mr-2 h-4 w-4" />
입찰 등록
</Button>
- )}
- <>
- <Button
- variant="destructive"
- size="sm"
- onClick={handleMarkAsDisposal}
- disabled={isPending}
- >
- <XCircle className="mr-2 h-4 w-4" />
- 유찰
- </Button>
- <Button
- variant="default"
- size="sm"
- onClick={onOpenAwardDialog}
- disabled={isPending}
- >
- <Trophy className="mr-2 h-4 w-4" />
- 낙찰
- </Button>
- </>
+ <Button
+ variant="destructive"
+ size="sm"
+ onClick={handleMarkAsDisposal}
+ disabled={isPending}
+ >
+ <XCircle className="mr-2 h-4 w-4" />
+ 유찰
+ </Button>
+ <Button
+ variant="default"
+ size="sm"
+ onClick={onOpenAwardDialog}
+ disabled={isPending}
+ >
+ <Trophy className="mr-2 h-4 w-4" />
+ 낙찰
+ </Button>
+
{bidding.status === 'bidding_disposal' && (
<Button