summaryrefslogtreecommitdiff
path: root/lib/bidding
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding')
-rw-r--r--lib/bidding/actions.ts6
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx4
-rw-r--r--lib/bidding/pre-quote/service.ts17
-rw-r--r--lib/bidding/service.ts137
4 files changed, 103 insertions, 61 deletions
diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts
index d0c7a0cd..5909cd62 100644
--- a/lib/bidding/actions.ts
+++ b/lib/bidding/actions.ts
@@ -613,11 +613,11 @@ export async function cancelDisposalAction(
}
}
- // 3. 입찰 상태를 입찰 진행중으로 변경
+ // 3. 입찰 상태를 입찰생성으로 변경
await tx
.update(biddings)
.set({
- status: 'evaluation_of_bidding',
+ status: 'bidding_generated',
updatedAt: new Date(),
updatedBy: userName,
})
@@ -734,7 +734,7 @@ export async function earlyOpenBiddingAction(biddingId: number) {
// 5. 참여협력사 중 최종응찰 버튼을 클릭한 업체들만 있는지 검증
// bidding_submitted 상태인 업체들이 있는지 확인 (이미 위에서 검증됨)
- // 6. 조기개찰 상태로 변경
+ // 6. 입찰평가중 상태로 변경
await tx
.update(biddings)
.set({
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 e3db8861..491f29f7 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx
@@ -180,8 +180,8 @@ export function BiddingDetailVendorToolbarActions({
<>
<div className="flex items-center gap-2">
{/* 상태별 액션 버튼 */}
- {/* 차수증가: 입찰공고 또는 입찰 진행중 상태 */}
- {(bidding.status === 'bidding_generated' || bidding.status === 'bidding_opened') && (
+ {/* 차수증가: 입찰평가중 또는 입찰 진행중 상태 */}
+ {(bidding.status === 'evaluation_of_bidding ' || bidding.status === 'bidding_opened') && (
<Button
variant="outline"
size="sm"
diff --git a/lib/bidding/pre-quote/service.ts b/lib/bidding/pre-quote/service.ts
index ea92f294..19b418ae 100644
--- a/lib/bidding/pre-quote/service.ts
+++ b/lib/bidding/pre-quote/service.ts
@@ -1492,18 +1492,15 @@ export async function sendBiddingBasicContracts(
try {
await sendEmail({
to: vendor.selectedMainEmail,
- template: 'basic-contract-notification',
+ subject: `[eVCP] 기본계약서 서명 요청`,
+ template: "contract-sign-request",
context: {
vendorName: vendor.vendorName,
- biddingId: biddingId,
- contractCount: contractTypes.length,
- deadline: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000).toLocaleDateString('ko-KR'),
- loginUrl: `${process.env.NEXT_PUBLIC_APP_URL}/partners/bid/${biddingId}`,
- message: message || '',
- currentYear: new Date().getFullYear(),
- language: 'ko'
- }
- })
+ templateName: contractTypes.map(ct => ct.templateName).join(', '),
+ loginUrl: `${process.env.NEXT_PUBLIC_APP_URL}/partners/basic-contract`,
+ language:'ko'
+ },
+ });
} catch (emailError) {
console.error(`이메일 발송 실패 (${vendor.selectedMainEmail}):`, emailError)
// 이메일 발송 실패해도 계약 생성은 유지
diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts
index a7cd8286..b60fc73d 100644
--- a/lib/bidding/service.ts
+++ b/lib/bidding/service.ts
@@ -33,6 +33,11 @@ import {
like,
notInArray
} from 'drizzle-orm'
+import { revalidatePath } from 'next/cache'
+import { filterColumns } from '@/lib/filter-columns'
+import { GetBiddingsSchema } from './validation'
+
+
// 사용자 이메일로 사용자 코드 조회
export async function getUserCodeByEmail(email: string): Promise<string | null> {
@@ -49,9 +54,6 @@ export async function getUserCodeByEmail(email: string): Promise<string | null>
return null
}
}
-import { revalidatePath } from 'next/cache'
-import { filterColumns } from '@/lib/filter-columns'
-import { CreateBiddingSchema, GetBiddingsSchema, UpdateBiddingSchema } from './validation'
import { saveFile } from '../file-stroage'
// userId를 user.name으로 변환하는 유틸리티 함수
@@ -973,6 +975,74 @@ function generateNextSequence(currentMax: string | null): string {
return result.padStart(4, '0');
}
+// 입찰 참여 현황 카운트 계산 함수
+export async function getParticipantCountsForBidding(biddingId: number) {
+ try {
+ // 전체 참여자 수 (예상 참여자)
+ const expectedResult = await db
+ .select({ count: count() })
+ .from(biddingCompanies)
+ .where(eq(biddingCompanies.biddingId, biddingId))
+
+ const expected = expectedResult[0]?.count || 0
+
+ // 참여 완료 수
+ const participatedResult = await db
+ .select({ count: count() })
+ .from(biddingCompanies)
+ .where(and(
+ eq(biddingCompanies.biddingId, biddingId),
+ eq(biddingCompanies.invitationStatus, 'bidding_submitted')
+ ))
+
+ const participated = participatedResult[0]?.count || 0
+
+ // 거부/취소 수
+ const declinedResult = await db
+ .select({ count: count() })
+ .from(biddingCompanies)
+ .where(and(
+ eq(biddingCompanies.biddingId, biddingId),
+ or(
+ eq(biddingCompanies.invitationStatus, 'bidding_declined'),
+ eq(biddingCompanies.invitationStatus, 'bidding_cancelled')
+ )
+ ))
+
+ const declined = declinedResult[0]?.count || 0
+
+ // 대기중 수
+ const pendingResult = await db
+ .select({ count: count() })
+ .from(biddingCompanies)
+ .where(and(
+ eq(biddingCompanies.biddingId, biddingId),
+ or(
+ eq(biddingCompanies.invitationStatus, 'pending'),
+ eq(biddingCompanies.invitationStatus, 'bidding_sent'),
+ eq(biddingCompanies.invitationStatus, 'bidding_accepted')
+ )
+ ))
+
+ const pending = pendingResult[0]?.count || 0
+
+ return {
+ participantExpected: expected,
+ participantParticipated: participated,
+ participantDeclined: declined,
+ participantPending: pending
+ }
+ } catch (error) {
+ console.error('Error in getParticipantCountsForBidding:', error)
+ return {
+ participantExpected: 0,
+ participantParticipated: 0,
+ participantDeclined: 0,
+ participantPending: 0
+ }
+ }
+}
+
// 자동 입찰번호 생성
export async function generateBiddingNumber(
contractType: string,
@@ -982,9 +1052,9 @@ export async function generateBiddingNumber(
): Promise<string> {
// 계약 타입별 접두사 설정
const typePrefix = {
- 'general': 'E',
- 'unit_price': 'F',
- 'sale': 'G'
+ 'general': 'E', // 일반계약
+ 'unit_price': 'F', // 단가계약
+ 'sale': 'G', // 매각계약
};
const prefix = typePrefix[contractType as keyof typeof typePrefix] || 'E';
@@ -3163,8 +3233,8 @@ export async function increaseRoundOrRebid(biddingId: number, userId: string | u
prNumber: existingBidding.prNumber,
hasPrDocument: existingBidding.hasPrDocument,
- // 상태는 내정가 산정으로 초기화
- status: 'set_target_price',
+ // 상태는 입찰생성으로 초기화
+ status: 'bidding_generated',
isPublic: existingBidding.isPublic,
isUrgent: existingBidding.isUrgent,
@@ -3530,8 +3600,8 @@ export async function getBiddingsForReceive(input: GetBiddingsSchema) {
orderByColumns.push(desc(biddings.createdAt))
}
- // bid-receive 페이지용 데이터 조회 (필요한 컬럼만 선택)
- const data = await db
+ // bid-receive 페이지용 데이터 조회 (참여 현황 제외하고 기본 정보만)
+ const biddingData = await db
.select({
// 기본 입찰 정보
id: biddings.id,
@@ -3549,42 +3619,6 @@ export async function getBiddingsForReceive(input: GetBiddingsSchema) {
createdAt: biddings.createdAt,
updatedAt: biddings.updatedAt,
- // 참여 현황 집계
- participantExpected: sql<number>`
- COALESCE((
- SELECT count(*)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- ), 0)
- `.as('participant_expected'),
-
- participantParticipated: sql<number>`
- COALESCE((
- SELECT count(*)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND invitation_status = 'bidding_submitted'
- ), 0)
- `.as('participant_participated'),
-
- participantDeclined: sql<number>`
- COALESCE((
- SELECT count(*)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND invitation_status IN ('bidding_declined', 'bidding_cancelled')
- ), 0)
- `.as('participant_declined'),
-
- participantPending: sql<number>`
- COALESCE((
- SELECT count(*)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND invitation_status IN ('pending', 'bidding_sent', 'bidding_accepted')
- ), 0)
- `.as('participant_pending'),
-
// 개찰 정보
openedAt: biddings.openedAt,
openedBy: biddings.openedBy,
@@ -3595,6 +3629,17 @@ export async function getBiddingsForReceive(input: GetBiddingsSchema) {
.limit(input.perPage)
.offset(offset)
+ // 각 입찰에 대한 참여 현황 카운트 계산
+ const data = await Promise.all(
+ biddingData.map(async (bidding) => {
+ const participantCounts = await getParticipantCountsForBidding(bidding.id)
+ return {
+ ...bidding,
+ ...participantCounts
+ }
+ })
+ )
+
const pageCount = Math.ceil(total / input.perPage)
return { data, pageCount, total }