summaryrefslogtreecommitdiff
path: root/db/schema/bidding.ts
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2025-11-10 11:25:19 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2025-11-10 11:25:19 +0900
commita5501ad1d1cb836d2b2f84e9b0f06049e22c901e (patch)
tree667ed8c5d6ec35b109190e9f976d66ae54def4ce /db/schema/bidding.ts
parentb0fe980376fcf1a19ff4b90851ca8b01f378fdc0 (diff)
parentf8a38907911d940cb2e8e6c9aa49488d05b2b578 (diff)
Merge remote-tracking branch 'origin/dujinkim' into master_homemaster
Diffstat (limited to 'db/schema/bidding.ts')
-rw-r--r--db/schema/bidding.ts958
1 files changed, 543 insertions, 415 deletions
diff --git a/db/schema/bidding.ts b/db/schema/bidding.ts
index 74603df0..18699633 100644
--- a/db/schema/bidding.ts
+++ b/db/schema/bidding.ts
@@ -10,19 +10,21 @@ import {
decimal,
boolean,
pgEnum,
- uuid,
- date, pgView
+ date,
} from 'drizzle-orm/pg-core'
import { Vendor, vendors } from './vendors'
-import { relations , sql} from 'drizzle-orm';
import { projects } from './projects';
+import { users } from './users';
+
// 입찰공고문 템플릿 (기존)
export const biddingNoticeTemplate = pgTable('bidding_notice_template', {
id: serial('id').primaryKey(),
- type: varchar('type', { length: 50 }).notNull().default('standard'), // 'standard' 고정
- title: varchar('title', { length: 200 }).notNull().default('표준 입찰공고문'),
+ biddingId: integer('bidding_id').references(() => biddings.id), // 연결된 입찰 ID (null이면 템플릿)
+ type: varchar('type', { length: 50 }).notNull().default('standard'), // 입찰공고 타입
+ title: varchar('title', { length: 200 }).notNull().default('입찰공고문'),
content: text('content').notNull().default(''),
+ isTemplate: boolean('is_template').default(false).notNull(), // 템플릿 여부
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
@@ -40,7 +42,13 @@ export const biddingStatusEnum = pgEnum('bidding_status', [
'bidding_closed', // 입찰마감
'evaluation_of_bidding', // 입찰평가중
'bidding_disposal', // 유찰
- 'vendor_selected' // 업체선정
+ 'vendor_selected', // 업체선정
+ 'bid_opening', // 개찰
+ 'early_bid_opening', // 조기개찰
+ 'rebidding', // 재입찰
+ 'disposal_cancelled', // 유찰취소
+ 'bid_closure', // 폐찰
+ 'round_increase' // 차수증가
])
// 2. 계약구분 enum
@@ -61,7 +69,6 @@ export const biddingTypeEnum = pgEnum('bidding_type', [
'transport', // 운송
'waste', // 폐기물
'sale', // 매각
- 'steel', // 강재
'other' // 기타(직접입력)
])
@@ -73,15 +80,32 @@ export const awardCountEnum = pgEnum('award_count', [
// 5. 업체 초대/응답 상태 enum
export const invitationStatusEnum = pgEnum('invitation_status', [
- 'pending', // 초대 대기
- 'sent', // 초대 발송
- 'bidding_invited', // 입찰 초대
- 'accepted', // 참여 수락
- 'declined', // 참여 거절
- 'submitted', // 견적 제출 완료
- 'bidding_submitted' // 입찰 제출 완료
+ 'pending', // 초대 대기
+ 'pre_quote_sent', // 사전견적 초대 발송
+ 'pre_quote_accepted', // 사전견적 참여
+ 'pre_quote_declined', // 사전견적 미참여
+ 'pre_quote_submitted', // 사전견적제출완료
+ 'bidding_sent', // 입찰 초대 발송
+ 'bidding_accepted', // 입찰 참여
+ 'bidding_declined', // 입찰 미참여
+ 'bidding_cancelled', // 응찰 취소
+ 'bidding_submitted' // 응찰 완료
])
+// invitationStatus 라벨 맵핑
+export const invitationStatusLabels: Record<string, string> = {
+ pending: '초대 대기',
+ pre_quote_sent: '사전견적 초대 발송',
+ pre_quote_accepted: '사전견적 참여',
+ pre_quote_declined: '사전견적 미참여',
+ pre_quote_submitted: '사전견적제출완료',
+ bidding_sent: '입찰 초대 발송',
+ bidding_accepted: '입찰 참여',
+ bidding_declined: '입찰 미참여',
+ bidding_cancelled: '응찰 취소',
+ bidding_submitted: '응찰 완료'
+}
+
// 6. 문서 타입 enum
export const documentTypeEnum = pgEnum('document_type', [
'notice', // 입찰공고서
@@ -124,8 +148,8 @@ export const weightUnitEnum = pgEnum('weight_unit', [
export const biddings = pgTable('biddings', {
id: serial('id').primaryKey(),
biddingNumber: varchar('bidding_number', { length: 50 }).unique().notNull(), // 입찰 No.
+ originalBiddingNumber: varchar('original_bidding_number', { length: 50 }), // 원입찰번호
revision: integer('revision').default(0), // Rev.
- projectId: integer('project_id').references(() => projects.id),
//견적에서 넘어온 레코드인지, 자체생산인지, 디폴트는 자체생산, notnull
biddingSourceType: varchar('bidding_source_type', { length: 20 }).notNull().default('manual'),
// 기본 정보
@@ -133,16 +157,16 @@ export const biddings = pgTable('biddings', {
itemName: varchar('item_name', { length: 300 }), // 품목명
title: varchar('title', { length: 300 }).notNull(), // 입찰명
description: text('description'),
- content: text('content'), // 입찰공고 내용 (HTML) - 표준 템플릿에서 수정된 내용
+ // 입찰공고 내용은 별도 biddingNotices 테이블에서 관리됨
// 계약 정보
contractType: contractTypeEnum('contract_type').notNull(), // 계약구분
biddingType: biddingTypeEnum('bidding_type').notNull(), // 입찰유형
awardCount: awardCountEnum('award_count').default('single'), // 낙찰수
// contractPeriod: varchar('contract_period', { length: 100 }), // 계약기간
- //시작일
- contractStartDate: date('contract_start_date'),
- //종료일
+ //시작일 (기본값: 현재 날짜)
+ contractStartDate: date('contract_start_date').defaultNow(),
+ //종료일 (기본값: 시작일로부터 1년 후)
contractEndDate: date('contract_end_date'),
// 일정 관리
@@ -170,11 +194,19 @@ export const biddings = pgTable('biddings', {
status: biddingStatusEnum('status').default('bidding_generated').notNull(),
isPublic: boolean('is_public').default(false), // 공개 입찰 여부
isUrgent: boolean('is_urgent').default(false), // 긴급여부
-
- // 담당자 정보
- managerName: varchar('manager_name', { length: 100 }), // 입찰담당자
- managerEmail: varchar('manager_email', { length: 100 }),
- managerPhone: varchar('manager_phone', { length: 20 }),
+
+ // 구매조직 정보
+ purchasingOrganization: varchar('purchasing_organization', { length: 100 }), // 구매조직
+
+ // 담당자 정보 (개선된 구조)
+ bidPicId: integer('bid_pic_id').references(() => users.id), // 입찰담당자 ID
+ bidPicName: varchar('bid_pic_name', { length: 100 }), // 입찰담당자 이름
+ bidPicCode: varchar('bid_pic_code', { length: 50 }), // 입찰담당자 코드
+ supplyPicId: integer('supply_pic_id').references(() => users.id), // 조달담당자 ID
+ supplyPicName: varchar('supply_pic_name', { length: 100 }), // 조달담당자 이름
+ supplyPicCode: varchar('supply_pic_code', { length: 50 }), // 조달담당자 코드
+
+ // 기존 담당자 정보 제거됨 (user FK로 대체)
// 메타 정보
remarks: text('remarks'), // 비고
@@ -236,9 +268,16 @@ export const prItemsForBidding = pgTable('pr_items_for_bidding', {
// 기본 정보
itemNumber: varchar('item_number', { length: 50 }), // 아이템 번호
- projectInfo: varchar('project_info', { length: 300 }), // 프로젝트 정보
+ projectId: integer('project_id').references(() => projects.id), // 프로젝트 ID (새로 추가)
+ projectInfo: varchar('project_info', { length: 300 }), // 프로젝트 정보 (기존, 추후 제거 가능)
itemInfo: varchar('item_info', { length: 300 }), // 품목정보
shi: varchar('shi', { length: 100 }), // SHI
+ // 자재 그룹 정보 (새로 추가)
+ materialGroupNumber: varchar('material_group_number', { length: 100 }), // 자재그룹번호
+ materialGroupInfo: varchar('material_group_info', { length: 300 }), // 자재그룹정보
+ // 자재 정보 (새로 추가)
+ materialNumber: varchar('material_number', { length: 100 }), // 자재번호
+ materialInfo: varchar('material_info', { length: 500 }), // 자재정보
// 납품 일정
requestedDeliveryDate: date('requested_delivery_date'), // 납품요청일
@@ -249,12 +288,41 @@ export const prItemsForBidding = pgTable('pr_items_for_bidding', {
// 수량 및 중량
quantity: decimal('quantity', { precision: 10, scale: 2 }), // 수량
- quantityUnit: varchar('quantity_unit', { length: 50 }), // 수량단위
+ quantityUnit: varchar('quantity_unit', { length: 50 }), // 수량단위 (구매단위)
totalWeight: decimal('total_weight', { precision: 10, scale: 2 }), // 총 중량
- weightUnit: varchar('weight_unit', { length: 50 }), // 중량단위
+ weightUnit: varchar('weight_unit', { length: 50 }), // 중량단위 (자재순중량)
+
+ // 가격 단위 추가
+ priceUnit: varchar('price_unit', { length: 50 }), // 가격단위
+ purchaseUnit: varchar('purchase_unit', { length: 50 }), // 구매단위
+ materialWeight: decimal('material_weight', { precision: 10, scale: 2 }), // 자재순중량
+
+ // WBS 정보
+ wbsCode: varchar('wbs_code', { length: 100 }), // WBS 코드
+ wbsName: varchar('wbs_name', { length: 300 }), // WBS 명칭
+
+ // Cost Center 정보
+ costCenterCode: varchar('cost_center_code', { length: 100 }), // 코스트센터 코드
+ costCenterName: varchar('cost_center_name', { length: 300 }), // 코스트센터 명칭
+
+ // GL Account 정보
+ glAccountCode: varchar('gl_account_code', { length: 100 }), // GL 계정 코드
+ glAccountName: varchar('gl_account_name', { length: 300 }), // GL 계정 명칭
+
+ // 내정 정보 (새로 추가)
+ targetUnitPrice: decimal('target_unit_price', { precision: 15, scale: 2 }), // 내정단가
+ targetAmount: decimal('target_amount', { precision: 15, scale: 2 }), // 내정금액
+ targetCurrency: varchar('target_currency', { length: 3 }).default('KRW'), // 내정통화
+
+ // 예산 정보 (새로 추가)
+ budgetAmount: decimal('budget_amount', { precision: 15, scale: 2 }), // 예산금액
+ budgetCurrency: varchar('budget_currency', { length: 3 }).default('KRW'), // 예산통화
+
+ // 실적 정보 (새로 추가)
+ actualAmount: decimal('actual_amount', { precision: 15, scale: 2 }), // 실적금액
+ actualCurrency: varchar('actual_currency', { length: 3 }).default('KRW'), // 실적통화
- // 상세 정보
- materialDescription: text('material_description'), // 자재내역상세
+
prNumber: varchar('pr_number', { length: 50 }), // PR번호
// SPEC 파일 정보
@@ -280,7 +348,8 @@ export const biddingConditions = pgTable('bidding_conditions', {
isPriceAdjustmentApplicable: boolean('is_price_adjustment_applicable'), // 연동제적용 여부
// 무역조건
- incoterms: text('incoterms'), // Incoterms 옵션들
+ incoterms: text('incoterms'), // Incoterms 옵션들
+ incotermsOption: text('incoterms_option'), // Incoterms 옵션 (추가)
shippingPort: varchar('shipping_port', { length: 200 }), // 선적지
destinationPort: varchar('destination_port', { length: 200 }), // 하역지
@@ -298,7 +367,7 @@ export const biddingCompanies = pgTable('bidding_companies', {
companyId: integer('company_id').references(() => vendors.id).notNull(),
// 초대 및 응답 상태
- invitationStatus: invitationStatusEnum('invitation_status').default('pending').notNull(),
+ invitationStatus: invitationStatusEnum('invitation_status').notNull(),
invitedAt: timestamp('invited_at'),
respondedAt: timestamp('responded_at'),
@@ -314,9 +383,12 @@ export const biddingCompanies = pgTable('bidding_companies', {
isBiddingParticipated: boolean('is_bidding_participated'),//본입찰 참여 여부
finalQuoteAmount: decimal('final_quote_amount', { precision: 15, scale: 2 }),
finalQuoteSubmittedAt: timestamp('final_quote_submitted_at'),
+ isFinalSubmission: boolean('is_final_submission').default(false), // 최종제출 여부
isWinner: boolean('is_winner'), // 낙찰 여부
isAttendingMeeting: boolean('is_attending_meeting'), // 사양설명회 참석 여부
awardRatio: decimal('award_ratio', { precision: 5, scale: 2 }), // 발주비율
+ //연동제 적용요건 문의 여부
+ isPriceAdjustmentApplicableQuestion: boolean('is_price_adjustment_applicable_question').default(false), // 연동제 적용요건 문의 여부
// 기타
notes: text('notes'), // 특이사항
@@ -328,6 +400,18 @@ export const biddingCompanies = pgTable('bidding_companies', {
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
+// 13-1. 입찰 참여 업체 담당자 테이블
+export const biddingCompaniesContacts = pgTable('bidding_companies_contacts', {
+ id: serial('id').primaryKey(),
+ biddingId: integer('bidding_id').references(() => biddings.id).notNull(),
+ vendorId: integer('vendor_id').references(() => vendors.id).notNull(),
+ contactName: varchar('contact_name', { length: 255 }).notNull(),
+ contactEmail: varchar('contact_email', { length: 255 }).notNull(),
+ contactNumber: varchar('contact_number', { length: 50 }),
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+})
+
// 14. 업체별 PR 아이템 응찰 정보
export const companyPrItemBids = pgTable('company_pr_item_bids', {
id: serial('id').primaryKey(),
@@ -491,7 +575,15 @@ export const priceAdjustmentForms = pgTable('price_adjustment_forms', {
});
// 타입 정의
-export type Bidding = typeof biddings.$inferSelect
+export type Bidding = typeof biddings.$inferSelect & {
+ // 추가 필드들 (쿼리 결과에 포함되므로 타입에도 추가)
+ purchasingOrganization?: string | null
+ bidPicId?: number | null
+ bidPicName?: string | null
+ supplyPicId?: number | null
+ supplyPicName?: string | null
+}
+
export type NewBidding = typeof biddings.$inferInsert
export type SpecificationMeeting = typeof specificationMeetings.$inferSelect
@@ -509,6 +601,9 @@ export type NewBiddingConditions = typeof biddingConditions.$inferInsert
export type BiddingCompany = typeof biddingCompanies.$inferSelect
export type NewBiddingCompany = typeof biddingCompanies.$inferInsert
+export type BiddingCompanyContact = typeof biddingCompaniesContacts.$inferSelect
+export type NewBiddingCompanyContact = typeof biddingCompaniesContacts.$inferInsert
+
export type CompanyPrItemBid = typeof companyPrItemBids.$inferSelect
export type NewCompanyPrItemBid = typeof companyPrItemBids.$inferInsert
@@ -543,6 +638,29 @@ export type BiddingWithDetails = Bidding & {
}
export type BiddingListItem = Bidding & {
+ // 전체 참여 현황
+ participantExpected: number // 초대업체 수
+ participationRate: number // 참여율 (입찰 기준)
+
+ // 사전견적 참여 현황
+ preQuotePending: number // 사전견적 초대 대기/발송
+ preQuoteAccepted: number // 사전견적 참여
+ preQuoteDeclined: number // 사전견적 미참여
+ preQuoteSubmitted: number // 사전견적 제출완료
+
+ // 입찰 참여 현황
+ biddingPending: number // 입찰 초대 발송
+ biddingAccepted: number // 입찰 참여
+ biddingDeclined: number // 입찰 미참여
+ biddingCancelled: number // 응찰 취소
+ biddingSubmitted: number // 응찰 완료
+
+ // 호환성을 위한 기존 필드 (deprecated)
+ participantParticipated: number // 참여
+ participantDeclined: number // 포기
+ participantPending: number // 대기
+ participantAccepted: number // 수락
+
participantStats: {
expected: number // 참여예정
participated: number // 참여
@@ -562,7 +680,13 @@ export const biddingStatusLabels = {
bidding_closed: '입찰마감',
evaluation_of_bidding: '입찰평가중',
bidding_disposal: '유찰',
- vendor_selected: '업체선정'
+ vendor_selected: '업체선정',
+ bid_opening: '개찰',
+ early_bid_opening: '조기개찰',
+ rebidding: '재입찰',
+ disposal_cancelled: '유찰취소',
+ bid_closure: '폐찰',
+ round_increase: '차수증가'
} as const
export const contractTypeLabels = {
@@ -571,6 +695,12 @@ export const contractTypeLabels = {
sale: '매각계약'
} as const
+export const biddingNoticeTypeLabels = {
+ standard: '표준',
+ facility: '시설재',
+ unit_price: '단가계약'
+} as const
+
export const biddingTypeLabels = {
equipment: '기자재',
construction: '공사',
@@ -581,7 +711,6 @@ export const biddingTypeLabels = {
transport: '운송',
waste: '폐기물',
sale: '매각',
- steel: '강재',
other: '기타(직접입력)'
} as const
@@ -610,384 +739,383 @@ export const weightUnitLabels = {
} as const
-export const biddingListView = pgView('bidding_list_view').as((qb) =>
- qb
- .select({
- // ═══════════════════════════════════════════════════════════════
- // 기본 입찰 정보
- // ═══════════════════════════════════════════════════════════════
- id: biddings.id,
- biddingNumber: biddings.biddingNumber,
- revision: biddings.revision,
- projectName: biddings.projectName,
- itemName: biddings.itemName,
- title: biddings.title,
- description: biddings.description,
- content: biddings.content,
- biddingSourceType: biddings.biddingSourceType,
- isUrgent: biddings.isUrgent,
-
- // ═══════════════════════════════════════════════════════════════
- // 계약 정보
- // ═══════════════════════════════════════════════════════════════
- contractType: biddings.contractType,
- biddingType: biddings.biddingType,
- awardCount: biddings.awardCount,
- contractStartDate: biddings.contractStartDate,
- contractEndDate: biddings.contractEndDate,
-
- // ═══════════════════════════════════════════════════════════════
- // 일정 관리
- // ═══════════════════════════════════════════════════════════════
- preQuoteDate: biddings.preQuoteDate,
- biddingRegistrationDate: biddings.biddingRegistrationDate,
- submissionStartDate: biddings.submissionStartDate,
- submissionEndDate: biddings.submissionEndDate,
- evaluationDate: biddings.evaluationDate,
-
- // ═══════════════════════════════════════════════════════════════
- // 회의 및 문서
- // ═══════════════════════════════════════════════════════════════
- hasSpecificationMeeting: biddings.hasSpecificationMeeting,
- hasPrDocument: biddings.hasPrDocument,
- prNumber: biddings.prNumber,
-
- // ═══════════════════════════════════════════════════════════════
- // 가격 정보
- // ═══════════════════════════════════════════════════════════════
- currency: biddings.currency,
- budget: biddings.budget,
- targetPrice: biddings.targetPrice,
- finalBidPrice: biddings.finalBidPrice,
-
- // ═══════════════════════════════════════════════════════════════
- // 상태 및 담당자
- // ═══════════════════════════════════════════════════════════════
- status: biddings.status,
- isPublic: biddings.isPublic,
- managerName: biddings.managerName,
- managerEmail: biddings.managerEmail,
- managerPhone: biddings.managerPhone,
-
- // ═══════════════════════════════════════════════════════════════
- // 메타 정보
- // ═══════════════════════════════════════════════════════════════
- remarks: biddings.remarks,
- createdBy: biddings.createdBy,
- createdAt: biddings.createdAt,
- updatedAt: biddings.updatedAt,
- updatedBy: biddings.updatedBy,
-
- // ═══════════════════════════════════════════════════════════════
- // 사양설명회 상세 정보
- // ═══════════════════════════════════════════════════════════════
- hasSpecificationMeetingDetails: sql<boolean>`${specificationMeetings.id} IS NOT NULL`.as('has_specification_meeting_details'),
- meetingDate: specificationMeetings.meetingDate,
- meetingLocation: specificationMeetings.location,
- meetingContactPerson: specificationMeetings.contactPerson,
- meetingIsRequired: specificationMeetings.isRequired,
-
- // ═══════════════════════════════════════════════════════════════
- // PR 문서 집계
- // ═══════════════════════════════════════════════════════════════
- prDocumentCount: sql<number>`
- COALESCE((
- SELECT count(*)
- FROM pr_documents
- WHERE bidding_id = ${biddings.id}
- ), 0)
- `.as('pr_document_count'),
-
- // PR 문서 목록 (최대 5개 문서명)
- prDocumentNames: sql<string[]>`
- (
- SELECT array_agg(document_name ORDER BY registered_at DESC)
- FROM pr_documents
- WHERE bidding_id = ${biddings.id}
- LIMIT 5
- )
- `.as('pr_document_names'),
-
- // ═══════════════════════════════════════════════════════════════
- // 참여 현황 집계 (핵심)
- // ═══════════════════════════════════════════════════════════════
- 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 = 'submitted'
- ), 0)
- `.as('participant_participated'),
-
- participantDeclined: sql<number>`
- COALESCE((
- SELECT count(*)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND invitation_status = 'declined'
- ), 0)
- `.as('participant_declined'),
-
- participantPending: sql<number>`
- COALESCE((
- SELECT count(*)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND invitation_status IN ('pending', 'sent')
- ), 0)
- `.as('participant_pending'),
-
- participantAccepted: sql<number>`
- COALESCE((
- SELECT count(*)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND invitation_status = 'accepted'
- ), 0)
- `.as('participant_accepted'),
-
- // ═══════════════════════════════════════════════════════════════
- // 참여율 계산
- // ═══════════════════════════════════════════════════════════════
- participationRate: sql<number>`
- CASE
- WHEN (
- SELECT count(*)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- ) > 0
- THEN ROUND(
- (
- SELECT count(*)::decimal
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND invitation_status = 'submitted'
- ) / (
- SELECT count(*)::decimal
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- ) * 100, 1
- )
- ELSE 0
- END
- `.as('participation_rate'),
-
- // ═══════════════════════════════════════════════════════════════
- // 견적 금액 통계
- // ═══════════════════════════════════════════════════════════════
- avgPreQuoteAmount: sql<number>`
- (
- SELECT AVG(pre_quote_amount)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND pre_quote_amount IS NOT NULL
- )
- `.as('avg_pre_quote_amount'),
-
- minPreQuoteAmount: sql<number>`
- (
- SELECT MIN(pre_quote_amount)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND pre_quote_amount IS NOT NULL
- )
- `.as('min_pre_quote_amount'),
-
- maxPreQuoteAmount: sql<number>`
- (
- SELECT MAX(pre_quote_amount)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND pre_quote_amount IS NOT NULL
- )
- `.as('max_pre_quote_amount'),
-
- avgFinalQuoteAmount: sql<number>`
- (
- SELECT AVG(final_quote_amount)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND final_quote_amount IS NOT NULL
- )
- `.as('avg_final_quote_amount'),
-
- minFinalQuoteAmount: sql<number>`
- (
- SELECT MIN(final_quote_amount)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND final_quote_amount IS NOT NULL
- )
- `.as('min_final_quote_amount'),
-
- maxFinalQuoteAmount: sql<number>`
- (
- SELECT MAX(final_quote_amount)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND final_quote_amount IS NOT NULL
- )
- `.as('max_final_quote_amount'),
-
- // ═══════════════════════════════════════════════════════════════
- // 선정 및 낙찰 정보
- // ═══════════════════════════════════════════════════════════════
- selectedForFinalBidCount: sql<number>`
- COALESCE((
- SELECT count(*)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND is_pre_quote_selected = true
- ), 0)
- `.as('selected_for_final_bid_count'),
-
- winnerCount: sql<number>`
- COALESCE((
- SELECT count(*)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND is_winner = true
- ), 0)
- `.as('winner_count'),
-
- // 낙찰 업체명 목록
- winnerCompanyNames: sql<string[]>`
- (
- SELECT array_agg(v.vendor_name ORDER BY v.vendor_name)
- FROM bidding_companies bc
- JOIN vendors v ON bc.company_id = v.id
- WHERE bc.bidding_id = ${biddings.id}
- AND bc.is_winner = true
- )
- `.as('winner_company_names'),
-
- // ═══════════════════════════════════════════════════════════════
- // 일정 상태 계산
- // ═══════════════════════════════════════════════════════════════
- submissionStatus: sql<string>`
- CASE
- WHEN ${biddings.submissionStartDate} IS NULL OR ${biddings.submissionEndDate} IS NULL
- THEN 'not_scheduled'
- WHEN NOW() < ${biddings.submissionStartDate}
- THEN 'scheduled'
- WHEN NOW() BETWEEN ${biddings.submissionStartDate} AND ${biddings.submissionEndDate}
- THEN 'active'
- WHEN NOW() > ${biddings.submissionEndDate}
- THEN 'closed'
- ELSE 'unknown'
- END
- `.as('submission_status'),
-
- // 마감까지 남은 일수
- daysUntilDeadline: sql<number>`
- CASE
- WHEN ${biddings.submissionEndDate} IS NOT NULL
- AND NOW() < ${biddings.submissionEndDate}
- THEN EXTRACT(DAYS FROM (${biddings.submissionEndDate} - NOW()))::integer
- ELSE NULL
- END
- `.as('days_until_deadline'),
-
- // 시작까지 남은 일수
- daysUntilStart: sql<number>`
- CASE
- WHEN ${biddings.submissionStartDate} IS NOT NULL
- AND NOW() < ${biddings.submissionStartDate}
- THEN EXTRACT(DAYS FROM (${biddings.submissionStartDate} - NOW()))::integer
- ELSE NULL
- END
- `.as('days_until_start'),
-
- // ═══════════════════════════════════════════════════════════════
- // 추가 유용한 계산 필드들
- // ═══════════════════════════════════════════════════════════════
-
- // 예산 대비 최저 견적 비율
- budgetEfficiencyRate: sql<number>`
- CASE
- WHEN ${biddings.budget} IS NOT NULL AND ${biddings.budget} > 0
- AND (
- SELECT MIN(final_quote_amount)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND final_quote_amount IS NOT NULL
- ) IS NOT NULL
- THEN ROUND(
- (
- SELECT MIN(final_quote_amount)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND final_quote_amount IS NOT NULL
- ) / ${biddings.budget} * 100, 1
- )
- ELSE NULL
- END
- `.as('budget_efficiency_rate'),
-
- // 내정가 대비 최저 견적 비율
- targetPriceEfficiencyRate: sql<number>`
- CASE
- WHEN ${biddings.targetPrice} IS NOT NULL AND ${biddings.targetPrice} > 0
- AND (
- SELECT MIN(final_quote_amount)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND final_quote_amount IS NOT NULL
- ) IS NOT NULL
- THEN ROUND(
- (
- SELECT MIN(final_quote_amount)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- AND final_quote_amount IS NOT NULL
- ) / ${biddings.targetPrice} * 100, 1
- )
- ELSE NULL
- END
- `.as('target_price_efficiency_rate'),
-
- // 입찰 진행 단계 점수 (0-100)
- progressScore: sql<number>`
- CASE ${biddings.status}
- WHEN 'bidding_generated' THEN 10
- WHEN 'request_for_quotation' THEN 20
- WHEN 'received_quotation' THEN 40
- WHEN 'set_target_price' THEN 60
- WHEN 'bidding_opened' THEN 70
- WHEN 'bidding_closed' THEN 80
- WHEN 'evaluation_of_bidding' THEN 90
- WHEN 'vendor_selected' THEN 100
- WHEN 'bidding_disposal' THEN 0
- ELSE 0
- END
- `.as('progress_score'),
-
- // 마지막 활동일 (가장 최근 업체 응답일)
- lastActivityDate: sql<Date>`
- GREATEST(
- ${biddings.updatedAt},
- COALESCE((
- SELECT MAX(updated_at)
- FROM bidding_companies
- WHERE bidding_id = ${biddings.id}
- ), ${biddings.updatedAt})
- )
- `.as('last_activity_date'),
- })
- .from(biddings)
- .leftJoin(
- specificationMeetings,
- sql`${biddings.id} = ${specificationMeetings.biddingId}`
- )
-)
-
-export type BiddingListView = typeof biddingListView.$inferSelect
+// export const biddingListView = pgView('bidding_list_view').as((qb) =>
+// qb
+// .select({
+// // ═══════════════════════════════════════════════════════════════
+// // 기본 입찰 정보
+// // ═══════════════════════════════════════════════════════════════
+// id: biddings.id,
+// biddingNumber: biddings.biddingNumber,
+// revision: biddings.revision,
+// projectName: biddings.projectName,
+// itemName: biddings.itemName,
+// title: biddings.title,
+// description: biddings.description,
+// biddingSourceType: biddings.biddingSourceType,
+// isUrgent: biddings.isUrgent,
+
+// // ═══════════════════════════════════════════════════════════════
+// // 계약 정보
+// // ═══════════════════════════════════════════════════════════════
+// contractType: biddings.contractType,
+// biddingType: biddings.biddingType,
+// awardCount: biddings.awardCount,
+// contractStartDate: biddings.contractStartDate,
+// contractEndDate: biddings.contractEndDate,
+
+// // ═══════════════════════════════════════════════════════════════
+// // 일정 관리
+// // ═══════════════════════════════════════════════════════════════
+// preQuoteDate: biddings.preQuoteDate,
+// biddingRegistrationDate: biddings.biddingRegistrationDate,
+// submissionStartDate: biddings.submissionStartDate,
+// submissionEndDate: biddings.submissionEndDate,
+// evaluationDate: biddings.evaluationDate,
+
+// // ═══════════════════════════════════════════════════════════════
+// // 회의 및 문서
+// // ═══════════════════════════════════════════════════════════════
+// hasSpecificationMeeting: biddings.hasSpecificationMeeting,
+// hasPrDocument: biddings.hasPrDocument,
+// prNumber: biddings.prNumber,
+
+// // ═══════════════════════════════════════════════════════════════
+// // 가격 정보
+// // ═══════════════════════════════════════════════════════════════
+// currency: biddings.currency,
+// budget: biddings.budget,
+// targetPrice: biddings.targetPrice,
+// finalBidPrice: biddings.finalBidPrice,
+
+// // ═══════════════════════════════════════════════════════════════
+// // 상태 및 담당자
+// // ═══════════════════════════════════════════════════════════════
+// status: biddings.status,
+// isPublic: biddings.isPublic,
+// managerName: biddings.managerName,
+// managerEmail: biddings.managerEmail,
+// managerPhone: biddings.managerPhone,
+
+// // ═══════════════════════════════════════════════════════════════
+// // 메타 정보
+// // ═══════════════════════════════════════════════════════════════
+// remarks: biddings.remarks,
+// createdBy: biddings.createdBy,
+// createdAt: biddings.createdAt,
+// updatedAt: biddings.updatedAt,
+// updatedBy: biddings.updatedBy,
+
+// // ═══════════════════════════════════════════════════════════════
+// // 사양설명회 상세 정보
+// // ═══════════════════════════════════════════════════════════════
+// hasSpecificationMeetingDetails: sql<boolean>`${specificationMeetings.id} IS NOT NULL`.as('has_specification_meeting_details'),
+// meetingDate: specificationMeetings.meetingDate,
+// meetingLocation: specificationMeetings.location,
+// meetingContactPerson: specificationMeetings.contactPerson,
+// meetingIsRequired: specificationMeetings.isRequired,
+
+// // ═══════════════════════════════════════════════════════════════
+// // PR 문서 집계
+// // ═══════════════════════════════════════════════════════════════
+// prDocumentCount: sql<number>`
+// COALESCE((
+// SELECT count(*)
+// FROM pr_documents
+// WHERE bidding_id = ${biddings.id}
+// ), 0)
+// `.as('pr_document_count'),
+
+// // PR 문서 목록 (최대 5개 문서명)
+// prDocumentNames: sql<string[]>`
+// (
+// SELECT array_agg(document_name ORDER BY registered_at DESC)
+// FROM pr_documents
+// WHERE bidding_id = ${biddings.id}
+// LIMIT 5
+// )
+// `.as('pr_document_names'),
+
+// // ═══════════════════════════════════════════════════════════════
+// // 참여 현황 집계 (핵심)
+// // ═══════════════════════════════════════════════════════════════
+// 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 = 'submitted'
+// ), 0)
+// `.as('participant_participated'),
+
+// participantDeclined: sql<number>`
+// COALESCE((
+// SELECT count(*)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND invitation_status = 'declined'
+// ), 0)
+// `.as('participant_declined'),
+
+// participantPending: sql<number>`
+// COALESCE((
+// SELECT count(*)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND invitation_status IN ('pending', 'sent')
+// ), 0)
+// `.as('participant_pending'),
+
+// participantAccepted: sql<number>`
+// COALESCE((
+// SELECT count(*)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND invitation_status = 'accepted'
+// ), 0)
+// `.as('participant_accepted'),
+
+// // ═══════════════════════════════════════════════════════════════
+// // 참여율 계산
+// // ═══════════════════════════════════════════════════════════════
+// participationRate: sql<number>`
+// CASE
+// WHEN (
+// SELECT count(*)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// ) > 0
+// THEN ROUND(
+// (
+// SELECT count(*)::decimal
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND invitation_status = 'submitted'
+// ) / (
+// SELECT count(*)::decimal
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// ) * 100, 1
+// )
+// ELSE 0
+// END
+// `.as('participation_rate'),
+
+// // ═══════════════════════════════════════════════════════════════
+// // 견적 금액 통계
+// // ═══════════════════════════════════════════════════════════════
+// avgPreQuoteAmount: sql<number>`
+// (
+// SELECT AVG(pre_quote_amount)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND pre_quote_amount IS NOT NULL
+// )
+// `.as('avg_pre_quote_amount'),
+
+// minPreQuoteAmount: sql<number>`
+// (
+// SELECT MIN(pre_quote_amount)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND pre_quote_amount IS NOT NULL
+// )
+// `.as('min_pre_quote_amount'),
+
+// maxPreQuoteAmount: sql<number>`
+// (
+// SELECT MAX(pre_quote_amount)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND pre_quote_amount IS NOT NULL
+// )
+// `.as('max_pre_quote_amount'),
+
+// avgFinalQuoteAmount: sql<number>`
+// (
+// SELECT AVG(final_quote_amount)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND final_quote_amount IS NOT NULL
+// )
+// `.as('avg_final_quote_amount'),
+
+// minFinalQuoteAmount: sql<number>`
+// (
+// SELECT MIN(final_quote_amount)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND final_quote_amount IS NOT NULL
+// )
+// `.as('min_final_quote_amount'),
+
+// maxFinalQuoteAmount: sql<number>`
+// (
+// SELECT MAX(final_quote_amount)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND final_quote_amount IS NOT NULL
+// )
+// `.as('max_final_quote_amount'),
+
+// // ═══════════════════════════════════════════════════════════════
+// // 선정 및 낙찰 정보
+// // ═══════════════════════════════════════════════════════════════
+// selectedForFinalBidCount: sql<number>`
+// COALESCE((
+// SELECT count(*)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND is_pre_quote_selected = true
+// ), 0)
+// `.as('selected_for_final_bid_count'),
+
+// winnerCount: sql<number>`
+// COALESCE((
+// SELECT count(*)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND is_winner = true
+// ), 0)
+// `.as('winner_count'),
+
+// // 낙찰 업체명 목록
+// winnerCompanyNames: sql<string[]>`
+// (
+// SELECT array_agg(v.vendor_name ORDER BY v.vendor_name)
+// FROM bidding_companies bc
+// JOIN vendors v ON bc.company_id = v.id
+// WHERE bc.bidding_id = ${biddings.id}
+// AND bc.is_winner = true
+// )
+// `.as('winner_company_names'),
+
+// // ═══════════════════════════════════════════════════════════════
+// // 일정 상태 계산
+// // ═══════════════════════════════════════════════════════════════
+// submissionStatus: sql<string>`
+// CASE
+// WHEN ${biddings.submissionStartDate} IS NULL OR ${biddings.submissionEndDate} IS NULL
+// THEN 'not_scheduled'
+// WHEN NOW() < ${biddings.submissionStartDate}
+// THEN 'scheduled'
+// WHEN NOW() BETWEEN ${biddings.submissionStartDate} AND ${biddings.submissionEndDate}
+// THEN 'active'
+// WHEN NOW() > ${biddings.submissionEndDate}
+// THEN 'closed'
+// ELSE 'unknown'
+// END
+// `.as('submission_status'),
+
+// // 마감까지 남은 일수
+// daysUntilDeadline: sql<number>`
+// CASE
+// WHEN ${biddings.submissionEndDate} IS NOT NULL
+// AND NOW() < ${biddings.submissionEndDate}
+// THEN EXTRACT(DAYS FROM (${biddings.submissionEndDate} - NOW()))::integer
+// ELSE NULL
+// END
+// `.as('days_until_deadline'),
+
+// // 시작까지 남은 일수
+// daysUntilStart: sql<number>`
+// CASE
+// WHEN ${biddings.submissionStartDate} IS NOT NULL
+// AND NOW() < ${biddings.submissionStartDate}
+// THEN EXTRACT(DAYS FROM (${biddings.submissionStartDate} - NOW()))::integer
+// ELSE NULL
+// END
+// `.as('days_until_start'),
+
+// // ═══════════════════════════════════════════════════════════════
+// // 추가 유용한 계산 필드들
+// // ═══════════════════════════════════════════════════════════════
+
+// // 예산 대비 최저 견적 비율
+// budgetEfficiencyRate: sql<number>`
+// CASE
+// WHEN ${biddings.budget} IS NOT NULL AND ${biddings.budget} > 0
+// AND (
+// SELECT MIN(final_quote_amount)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND final_quote_amount IS NOT NULL
+// ) IS NOT NULL
+// THEN ROUND(
+// (
+// SELECT MIN(final_quote_amount)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND final_quote_amount IS NOT NULL
+// ) / ${biddings.budget} * 100, 1
+// )
+// ELSE NULL
+// END
+// `.as('budget_efficiency_rate'),
+
+// // 내정가 대비 최저 견적 비율
+// targetPriceEfficiencyRate: sql<number>`
+// CASE
+// WHEN ${biddings.targetPrice} IS NOT NULL AND ${biddings.targetPrice} > 0
+// AND (
+// SELECT MIN(final_quote_amount)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND final_quote_amount IS NOT NULL
+// ) IS NOT NULL
+// THEN ROUND(
+// (
+// SELECT MIN(final_quote_amount)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// AND final_quote_amount IS NOT NULL
+// ) / ${biddings.targetPrice} * 100, 1
+// )
+// ELSE NULL
+// END
+// `.as('target_price_efficiency_rate'),
+
+// // 입찰 진행 단계 점수 (0-100)
+// progressScore: sql<number>`
+// CASE ${biddings.status}
+// WHEN 'bidding_generated' THEN 10
+// WHEN 'request_for_quotation' THEN 20
+// WHEN 'received_quotation' THEN 40
+// WHEN 'set_target_price' THEN 60
+// WHEN 'bidding_opened' THEN 70
+// WHEN 'bidding_closed' THEN 80
+// WHEN 'evaluation_of_bidding' THEN 90
+// WHEN 'vendor_selected' THEN 100
+// WHEN 'bidding_disposal' THEN 0
+// ELSE 0
+// END
+// `.as('progress_score'),
+
+// // 마지막 활동일 (가장 최근 업체 응답일)
+// lastActivityDate: sql<Date>`
+// GREATEST(
+// ${biddings.updatedAt},
+// COALESCE((
+// SELECT MAX(updated_at)
+// FROM bidding_companies
+// WHERE bidding_id = ${biddings.id}
+// ), ${biddings.updatedAt})
+// )
+// `.as('last_activity_date'),
+// })
+// .from(biddings)
+// .leftJoin(
+// specificationMeetings,
+// sql`${biddings.id} = ${specificationMeetings.biddingId}`
+// )
+// )
+
+// export type BiddingListView = typeof biddingListView.$inferSelect