summaryrefslogtreecommitdiff
path: root/db/schema
diff options
context:
space:
mode:
Diffstat (limited to 'db/schema')
-rw-r--r--db/schema/index.ts2
-rw-r--r--db/schema/rfqLast.ts764
-rw-r--r--db/schema/rfqVendor.ts382
3 files changed, 1148 insertions, 0 deletions
diff --git a/db/schema/index.ts b/db/schema/index.ts
index 7637d247..bcabac1b 100644
--- a/db/schema/index.ts
+++ b/db/schema/index.ts
@@ -36,6 +36,8 @@ export * from './consent';
export * from './bidding';
export * from './vendorRegistrations';
export * from './compliance';
+export * from './rfqLast';
+export * from './rfqVendor';
// 부서별 도메인 할당 관리
export * from './departmentDomainAssignments';
diff --git a/db/schema/rfqLast.ts b/db/schema/rfqLast.ts
new file mode 100644
index 00000000..34cb7bf3
--- /dev/null
+++ b/db/schema/rfqLast.ts
@@ -0,0 +1,764 @@
+import { foreignKey, pgTable, pgView, serial, varchar, text, timestamp, boolean, integer, numeric, date, alias, check, uniqueIndex } from "drizzle-orm/pg-core";
+import { eq, sql, relations } from "drizzle-orm";
+import { projects } from "./projects";
+import { users } from "./users";
+import { vendors } from "./vendors";
+import { incoterms, paymentTerms } from "./procurementRFQ";
+
+
+export type RfqStatus =
+ | "RFQ 생성"
+ | "구매담당지정"
+ | "견적요청문서 확정"
+ | "Short List 확정"
+ | "TBE 완료"
+ | "RFQ 발송"
+ | "견적접수"
+ | "최종업체선정";
+
+
+export const rfqsLast = pgTable(
+ "rfqs_last",
+ {
+ id: serial("id").primaryKey(),
+
+ rfqCode: varchar("rfq_code", { length: 50 }).unique(), // ex) "RFQ-2025-001"
+
+ projectId: integer("project_id")
+ .references(() => projects.id, { onDelete: "set null" }),
+
+ // 자재코드, 자재명: ECC RFQ는 자재코드, 자재명을 가지지 않음
+ // WHY?: 여러 프로젝트 혹은 여러 시리즈의 동일 품목을 PR로 묶어 올리기 때문
+ // 아래 컬럼은 대표 자재코드, 대표 자재명으로 사용
+ itemCode: varchar("item_code", { length: 100 }),
+ itemName: varchar("item_name", { length: 255 }),
+
+ dueDate: date("due_date", { mode: "date" })
+ .$type<Date>(), // 인터페이스한 값은 dueDate가 없으므로 notNull 제약조건 제거
+
+ rfqSendDate: date("rfq_send_date", { mode: "date" })
+ .$type<Date | null>(), // notNull() 제약조건 제거, null 허용 (ECC에서 수신 후 보내지 않은 RFQ)
+
+ status: varchar("status", { length: 30 })
+ .$type<RfqStatus>()
+ .default("RFQ 생성")
+ .notNull(),
+
+ rfqSealedYn: boolean("rfq_sealed_yn").default(false),
+
+ EngPicName: varchar("eng_pic_name", { length: 50 }),
+
+ packageNo: varchar("package_no", { length: 50 }),
+ packageName: varchar("package_name", { length: 255 }),
+
+ remark: text("remark"),
+
+ pic: integer("pic")
+ .references(() => users.id, { onDelete: "set null" }),
+ picCode: varchar("pic_code", { length: 50 }), // 구매그룹에 대응시킴 (담당자 코드로 3자리)
+ picName: varchar("pic_name", { length: 50 }),
+
+ sentBy: integer("sent_by")
+ .references(() => users.id, { onDelete: "set null" }),
+
+ createdBy: integer("created_by")
+ .notNull()
+ .references(() => users.id, { onDelete: "set null" }),
+
+ updatedBy: integer("updated_by")
+ .notNull()
+ .references(() => users.id, { onDelete: "set null" }),
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+
+ //일반견적 추가 필드
+ rfqType: varchar("rfq_type", { length: 255 }),
+ rfqTitle: varchar("rfq_title", { length: 255 }),
+
+ //ITB 추가 필드
+ projectCompany: varchar("project_company", { length: 255 }),
+ projectFlag: varchar("project_flag", { length: 255 }),
+ projectSite: varchar("project_site", { length: 255 }),
+ smCode: varchar("sm_code", { length: 255 }),
+
+ //RFQ 추가 필드
+ prNumber: varchar("pr_number", { length: 255 }),
+ prIssueDate: date("pr_issue_date", { mode: "date" })
+ .$type<Date | null>(),
+ // SS, II, null 값을 가질 수 있음.
+ // SS = 시리즈 통합, II = 품목 통합, 공란 = 통합 없음
+ series: varchar("series", { length: 50 }),
+
+ },
+
+);
+
+
+
+export const rfqLastDetails = pgTable(
+ "rfq_last_details",
+ {
+ id: serial("id").primaryKey(),
+ rfqsLastId: integer("rfqs_last_id")
+ .references(() => rfqsLast.id, { onDelete: "set null" }),
+ vendorsId: integer("vendors_id")
+ .references(() => vendors.id, { onDelete: "set null" }),
+ currency: varchar("currency", { length: 10 }).default("USD"),
+
+ // 정규화된 paymentTerms 참조
+ paymentTermsCode: varchar("payment_terms_code", { length: 50 })
+ .references(() => paymentTerms.code, { onDelete: "set null" }),
+ // paymentTerms 필드는 제거 (코드로 조회)
+
+ // 정규화된 incoterms 참조
+ incotermsCode: varchar("incoterms_code", { length: 20 })
+ .references(() => incoterms.code, { onDelete: "set null" }),
+ incotermsDetail: varchar("incoterms_detail", { length: 255 }),
+
+ deliveryDate: date("delivery_date", { mode: "date" })
+ .$type<Date>()
+ .notNull(),
+
+ contractDuration: varchar("contract_duration", { length: 255 }),
+
+
+ taxCode: varchar("tax_code", { length: 255 }).default("VV"),
+ placeOfShipping: varchar("place_of_shipping", { length: 255 }),
+ placeOfDestination: varchar("place_of_destination", { length: 255 }),
+ remark: text("remark"),
+ cancelReason: text("cancel_reason"),
+ updatedBy: integer("updated_by")
+ .notNull()
+ .references(() => users.id, { onDelete: "set null" }),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+
+ shortList: boolean('short_list').notNull().default(false),
+ returnYn: boolean('return_yn').notNull().default(false),
+ returnedAt: timestamp("returned_at").defaultNow(),
+
+
+ prjectGtcYn: boolean('prject_gtc_yn').notNull().default(false),
+ generalGtcYn: boolean('general_gtc_yn').notNull().default(false),
+ ndaYn: boolean('nda_yn').notNull().default(false),
+ agreementYn: boolean('agreement_yn').notNull().default(false),
+
+ materialPriceRelatedYn: boolean("material_price_related_yn").default(false),
+ sparepartYn: boolean("sparepart_yn").default(false),
+ firstYn: boolean("first_yn").default(false),
+
+ firstDescription: text("first_description"),
+ sparepartDescription: text("sparepart_escription"),
+
+
+ }
+);
+
+export const rfqPrItems = pgTable(
+ "rfq_pr_items",
+ {
+ id: serial("id").primaryKey(),
+ rfqsLastId: integer("rfqs_last_id")
+ .references(() => rfqsLast.id, { onDelete: "set null" }),
+
+ rfqItem: varchar("rfq_item", { length: 50 }), // 단위
+ prItem: varchar("pr_item", { length: 50 }), // 단위
+ prNo: varchar("pr_no", { length: 50 }), // 단위
+
+ // itemId: integer("item_id")
+ // .references(() => items.id, { onDelete: "set null" }),
+
+ materialCode: varchar("material_code", { length: 255 }),
+ materialCategory: varchar("material_category", { length: 255 }),
+ acc: varchar("acc", { length: 255 }),
+ materialDescription: varchar("material_description", { length: 255 }),
+ size: varchar("size", { length: 255 }),
+ deliveryDate: date("delivery_date", { mode: "date" })
+ .$type<Date>(),
+ quantity: numeric("quantity", { precision: 12, scale: 2 })
+ .$type<number>()
+ .default(1),
+ uom: varchar("uom", { length: 50 }), // 단위
+ grossWeight: numeric("gross_weight", { precision: 12, scale: 2 })
+ .$type<number>()
+ .default(1),
+ gwUom: varchar("gw_uom", { length: 50 }), // 단위
+
+ specNo: varchar("spec_no", { length: 255 }),
+ specUrl: varchar("spec_url", { length: 255 }),
+ trackingNo: varchar("tracking_no", { length: 255 }),
+
+ majorYn: boolean("major_yn").default(false),
+
+ projectDef: varchar("project_def", { length: 255 }),
+ projectSc: varchar("project_sc", { length: 255 }),
+ projectKl: varchar("project_kl", { length: 255 }),
+ projectLc: varchar("project_lc", { length: 255 }),
+ projectDl: varchar("project_dl", { length: 255 }),
+
+ remark: text("remark"),
+
+ },
+);
+
+
+//view
+export const rfqsLastView = pgView("rfqs_last_view").as((qb) => {
+ const createdByUser = alias(users, "created_by_user");
+ const updatedByUser = alias(users, "updated_by_user");
+ const sentByUser = alias(users, "sent_by_user");
+ const picUser = alias(users, "pic_user");
+
+ return qb
+ .select({
+ // Basic RFQ identification
+ id: sql<number>`${rfqsLast.id}`.as("id"),
+ rfqCode: sql<string>`${rfqsLast.rfqCode}`.as("rfq_code"),
+ series: sql<string | null>`${rfqsLast.series}`.as("series"),
+ rfqSealedYn: sql<boolean | null>`${rfqsLast.rfqSealedYn}`.as("rfq_sealed_yn"),
+
+ // RFQ Type and Title (일반견적 필드)
+ rfqType: sql<string | null>`${rfqsLast.rfqType}`.as("rfq_type"),
+ rfqTitle: sql<string | null>`${rfqsLast.rfqTitle}`.as("rfq_title"),
+
+ // ITB 관련 필드
+ projectCompany: sql<string | null>`${rfqsLast.projectCompany}`.as("project_company"),
+ projectFlag: sql<string | null>`${rfqsLast.projectFlag}`.as("project_flag"),
+ projectSite: sql<string | null>`${rfqsLast.projectSite}`.as("project_site"),
+ smCode: sql<string | null>`${rfqsLast.smCode}`.as("sm_code"),
+
+ // RFQ 추가 필드
+ prNumber: sql<string | null>`${rfqsLast.prNumber}`.as("pr_number"),
+ prIssueDate: sql<Date | null>`${rfqsLast.prIssueDate}`.as("pr_issue_date"),
+
+ // Project information
+ projectId: sql<number | null>`${rfqsLast.projectId}`.as("project_id"),
+ projectCode: sql<string | null>`${projects.code}`.as("project_code"),
+ projectName: sql<string | null>`${projects.name}`.as("project_name"),
+
+ // Item information
+ itemCode: sql<string | null>`${rfqsLast.itemCode}`.as("item_code"),
+ itemName: sql<string | null>`${rfqsLast.itemName}`.as("item_name"),
+
+ // Package information
+ packageNo: sql<string | null>`${rfqsLast.packageNo}`.as("package_no"),
+ packageName: sql<string | null>`${rfqsLast.packageName}`.as("package_name"),
+
+ // Engineering PIC
+ engPicName: sql<string | null>`${rfqsLast.EngPicName}`.as("eng_pic_name"),
+
+ // Status and dates
+ status: sql<string>`${rfqsLast.status}`.as("status"),
+ rfqSendDate: sql<Date | null>`${rfqsLast.rfqSendDate}`.as("rfq_send_date"),
+ dueDate: sql<Date | null>`${rfqsLast.dueDate}`.as("due_date"),
+
+ // PIC information
+ picId: sql<number | null>`${rfqsLast.pic}`.as("pic_id"),
+ picCode: sql<string | null>`${rfqsLast.picCode}`.as("pic_code"),
+ picName: sql<string | null>`${rfqsLast.picName}`.as("pic_name"),
+ picUserName: sql<string | null>`${picUser.name}`.as("pic_user_name"),
+
+ // 가장 빠른 견적서 제출 날짜
+ earliestQuotationSubmittedAt: sql<Date | null>`(
+ SELECT MIN(submitted_at)
+ FROM rfq_last_vendor_responses
+ WHERE rfqs_last_id = ${rfqsLast.id}
+ AND submitted_at IS NOT NULL
+ )`.as("earliest_quotation_submitted_at"),
+
+ // Vendor details count
+ vendorCount: sql<number>`(
+ SELECT COUNT(*)
+ FROM rfq_last_details
+ WHERE rfqs_last_id = ${rfqsLast.id}
+ )`.as("vendor_count"),
+
+ // Short listed vendor count
+ shortListedVendorCount: sql<number>`(
+ SELECT COUNT(*)
+ FROM rfq_last_details
+ WHERE rfqs_last_id = ${rfqsLast.id}
+ AND short_list = true
+ )`.as("short_listed_vendor_count"),
+
+ // Quotation received count
+ quotationReceivedCount: sql<number>`(
+ SELECT COUNT(DISTINCT vendor_id)
+ FROM rfq_last_vendor_responses
+ WHERE rfqs_last_id = ${rfqsLast.id}
+ AND submitted_at IS NOT NULL
+ )`.as("quotation_received_count"),
+
+ // Audit information
+ createdBy: sql<number>`${rfqsLast.createdBy}`.as("created_by"),
+ createdByUserName: sql<string | null>`${createdByUser.name}`.as("created_by_user_name"),
+ createdAt: sql<Date>`${rfqsLast.createdAt}`.as("created_at"),
+
+ sentBy: sql<number | null>`${rfqsLast.sentBy}`.as("sent_by"),
+ sentByUserName: sql<string | null>`${sentByUser.name}`.as("sent_by_user_name"),
+
+ updatedBy: sql<number>`${rfqsLast.updatedBy}`.as("updated_by"),
+ updatedByUserName: sql<string | null>`${updatedByUser.name}`.as("updated_by_user_name"),
+ updatedAt: sql<Date>`${rfqsLast.updatedAt}`.as("updated_at"),
+
+ remark: sql<string | null>`${rfqsLast.remark}`.as("remark"),
+
+ // PR Items related information
+ majorItemMaterialCode: sql<string | null>`(
+ SELECT material_code
+ FROM rfq_pr_items
+ WHERE rfqs_last_id = ${rfqsLast.id}
+ AND major_yn = true
+ LIMIT 1
+ )`.as("major_item_material_code"),
+
+ majorItemPrNo: sql<string | null>`(
+ SELECT pr_no
+ FROM rfq_pr_items
+ WHERE rfqs_last_id = ${rfqsLast.id}
+ AND major_yn = true
+ LIMIT 1
+ )`.as("major_item_pr_no"),
+
+ prItemsCount: sql<number>`(
+ SELECT COUNT(*)
+ FROM rfq_pr_items
+ WHERE rfqs_last_id = ${rfqsLast.id}
+ )`.as("pr_items_count"),
+
+ majorItemsCount: sql<number>`(
+ SELECT COUNT(*)
+ FROM rfq_pr_items
+ WHERE rfqs_last_id = ${rfqsLast.id}
+ AND major_yn = true
+ )`.as("major_items_count")
+ })
+ .from(rfqsLast)
+ .leftJoin(projects, eq(rfqsLast.projectId, projects.id))
+ .leftJoin(createdByUser, eq(rfqsLast.createdBy, createdByUser.id))
+ .leftJoin(updatedByUser, eq(rfqsLast.updatedBy, updatedByUser.id))
+ .leftJoin(sentByUser, eq(rfqsLast.sentBy, sentByUser.id))
+ .leftJoin(picUser, eq(rfqsLast.pic, picUser.id));
+});
+
+// Updated rfqLastDetailsView with new fields
+export const rfqLastDetailsView = pgView("rfq_last_details_view").as((qb) => {
+ const rfqDetailsTable = alias(rfqLastDetails, "rfq_details");
+ const rfqsTable = alias(rfqsLast, "rfqs");
+ const projectsTable = alias(projects, "projects");
+ const vendorsTable = alias(vendors, "vendors");
+ const paymentTermsTable = alias(paymentTerms, "payment_terms");
+ const incotermsTable = alias(incoterms, "incoterms");
+ const updatedByUser = alias(users, "updated_by_user");
+
+ return qb
+ .select({
+ // rfqLastDetails 기본 필드
+ detailId: sql<number>`${rfqDetailsTable.id}`.as("detail_id"),
+ rfqId: sql<number>`${rfqsTable.id}`.as("rfq_id"),
+ rfqCode: sql<string>`${rfqsTable.rfqCode}`.as("rfq_code"),
+
+ // RFQ 타입 정보
+ rfqType: sql<string | null>`${rfqsTable.rfqType}`.as("rfq_type"),
+ rfqTitle: sql<string | null>`${rfqsTable.rfqTitle}`.as("rfq_title"),
+
+ // ITB 관련 정보
+ projectCompany: sql<string | null>`${rfqsTable.projectCompany}`.as("project_company"),
+ projectFlag: sql<string | null>`${rfqsTable.projectFlag}`.as("project_flag"),
+ projectSite: sql<string | null>`${rfqsTable.projectSite}`.as("project_site"),
+ smCode: sql<string | null>`${rfqsTable.smCode}`.as("sm_code"),
+
+ // PR 정보
+ prNumber: sql<string | null>`${rfqsTable.prNumber}`.as("pr_number"),
+ prIssueDate: sql<Date | null>`${rfqsTable.prIssueDate}`.as("pr_issue_date"),
+ series: sql<string | null>`${rfqsTable.series}`.as("series"),
+
+ // 프로젝트 관련 필드
+ projectCode: sql<string | null>`${projectsTable.code}`.as("project_code"),
+ projectName: sql<string | null>`${projectsTable.name}`.as("project_name"),
+
+ // 아이템 관련 필드
+ itemCode: sql<string | null>`${rfqsTable.itemCode}`.as("item_code"),
+ itemName: sql<string | null>`${rfqsTable.itemName}`.as("item_name"),
+
+ // 벤더 관련 필드
+ vendorId: sql<number | null>`${vendorsTable.id}`.as("vendor_id"),
+ vendorName: sql<string | null>`${vendorsTable.vendorName}`.as("vendor_name"),
+ vendorCode: sql<string | null>`${vendorsTable.vendorCode}`.as("vendor_code"),
+ vendorCountry: sql<string | null>`${vendorsTable.country}`.as("vendor_country"),
+
+ // RFQ 상세 정보 필드
+ currency: sql<string | null>`${rfqDetailsTable.currency}`.as("currency"),
+ paymentTermsCode: sql<string | null>`${paymentTermsTable.code}`.as("payment_terms_code"),
+ paymentTermsDescription: sql<string | null>`${paymentTermsTable.description}`.as("payment_terms_description"),
+ incotermsCode: sql<string | null>`${incotermsTable.code}`.as("incoterms_code"),
+ incotermsDescription: sql<string | null>`${incotermsTable.description}`.as("incoterms_description"),
+ incotermsDetail: sql<string | null>`${rfqDetailsTable.incotermsDetail}`.as("incoterms_detail"),
+ deliveryDate: sql<Date | null>`${rfqDetailsTable.deliveryDate}`.as("delivery_date"),
+ contractDuration: sql<string | null>`${rfqDetailsTable.contractDuration}`.as("contract_duration"),
+ taxCode: sql<string | null>`${rfqDetailsTable.taxCode}`.as("tax_code"),
+ placeOfShipping: sql<string | null>`${rfqDetailsTable.placeOfShipping}`.as("place_of_shipping"),
+ placeOfDestination: sql<string | null>`${rfqDetailsTable.placeOfDestination}`.as("place_of_destination"),
+
+ // Boolean 필드들
+ shortList: sql<boolean>`${rfqDetailsTable.shortList}`.as("short_list"),
+ returnYn: sql<boolean>`${rfqDetailsTable.returnYn}`.as("return_yn"),
+ returnedAt: sql<Date | null>`${rfqDetailsTable.returnedAt}`.as("returned_at"),
+
+ // GTC/NDA/Agreement 필드
+ prjectGtcYn: sql<boolean>`${rfqDetailsTable.prjectGtcYn}`.as("prject_gtc_yn"),
+ generalGtcYn: sql<boolean>`${rfqDetailsTable.generalGtcYn}`.as("general_gtc_yn"),
+ ndaYn: sql<boolean>`${rfqDetailsTable.ndaYn}`.as("nda_yn"),
+ agreementYn: sql<boolean>`${rfqDetailsTable.agreementYn}`.as("agreement_yn"),
+
+ // 추가 YN 필드들
+ materialPriceRelatedYn: sql<boolean | null>`${rfqDetailsTable.materialPriceRelatedYn}`.as("material_price_related_yn"),
+ sparepartYn: sql<boolean | null>`${rfqDetailsTable.sparepartYn}`.as("sparepart_yn"),
+ firstYn: sql<boolean | null>`${rfqDetailsTable.firstYn}`.as("first_yn"),
+
+ // Remarks
+ remark: sql<string | null>`${rfqDetailsTable.remark}`.as("remark"),
+ cancelReason: sql<string | null>`${rfqDetailsTable.cancelReason}`.as("cancel_reason"),
+
+ firstDescription: sql<string | null>`${rfqDetailsTable.firstDescription}`.as("first_description"),
+ sparepartDescription: sql<string | null>`${rfqDetailsTable.sparepartDescription}`.as("sparepart_description"),
+
+
+ // 업데이트 관련 필드
+ updatedBy: sql<number>`${rfqDetailsTable.updatedBy}`.as("updated_by"),
+ updatedByUserName: sql<string | null>`${updatedByUser.name}`.as("updated_by_user_name"),
+ updatedAt: sql<Date | null>`${rfqDetailsTable.updatedAt}`.as("updated_at"),
+
+ // PR items 관련 카운트
+ prItemsCount: sql<number>`(
+ SELECT COUNT(*)
+ FROM rfq_pr_items
+ WHERE rfqs_last_id = ${rfqsTable.id}
+ )`.as("pr_items_count"),
+
+ majorItemsCount: sql<number>`(
+ SELECT COUNT(*)
+ FROM rfq_pr_items
+ WHERE rfqs_last_id = ${rfqsTable.id}
+ AND major_yn = true
+ )`.as("major_items_count"),
+
+
+ // 견적서 관련 필드
+ hasQuotation: sql<boolean>`(
+ SELECT COUNT(*) > 0
+ FROM rfq_last_vendor_responses
+ WHERE rfqs_last_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId}
+ )`.as("has_quotation"),
+
+ quotationStatus: sql<string | null>`(
+ SELECT status
+ FROM rfq_last_vendor_responses
+ WHERE rfqs_last_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId}
+ ORDER BY created_at DESC LIMIT 1
+ )`.as("quotation_status"),
+
+ quotationTotalPrice: sql<number | null>`(
+ SELECT total_amount
+ FROM rfq_last_vendor_responses
+ WHERE rfqs_last_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId}
+ ORDER BY created_at DESC LIMIT 1
+ )`.as("quotation_total_price"),
+
+ quotationVersion: sql<number | null>`(
+ SELECT response_version
+ FROM rfq_last_vendor_responses
+ WHERE rfqs_last_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId}
+ ORDER BY response_version DESC LIMIT 1
+ )`.as("quotation_version"),
+
+ quotationVersionCount: sql<number>`(
+ SELECT COUNT(DISTINCT response_version)
+ FROM rfq_last_vendor_responses
+ WHERE rfqs_last_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId}
+ )`.as("quotation_version_count"),
+
+ lastQuotationDate: sql<Date | null>`(
+ SELECT created_at
+ FROM rfq_last_vendor_responses
+ WHERE rfqs_last_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId}
+ ORDER BY response_version DESC LIMIT 1
+ )`.as("last_quotation_date"),
+
+ quotationSubmittedAt: sql<Date | null>`(
+ SELECT submitted_at
+ FROM rfq_last_vendor_responses
+ WHERE rfqs_last_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId}
+ ORDER BY response_version DESC LIMIT 1
+ )`.as("quotation_submitted_at")
+ })
+ .from(rfqDetailsTable)
+ .leftJoin(rfqsTable, eq(rfqDetailsTable.rfqsLastId, rfqsTable.id))
+ .leftJoin(projectsTable, eq(rfqsTable.projectId, projectsTable.id))
+ .leftJoin(vendorsTable, eq(rfqDetailsTable.vendorsId, vendorsTable.id))
+ .leftJoin(paymentTermsTable, eq(rfqDetailsTable.paymentTermsCode, paymentTermsTable.code))
+ .leftJoin(incotermsTable, eq(rfqDetailsTable.incotermsCode, incotermsTable.code))
+ .leftJoin(updatedByUser, eq(rfqDetailsTable.updatedBy, updatedByUser.id));
+});
+
+// Updated prItemsLastView to match new table structure
+export const prItemsLastView = pgView("pr_items_last_view").as((qb) => {
+ return qb
+ .select({
+ // PR Item 기본 필드
+ id: rfqPrItems.id,
+ rfqsLastId: rfqPrItems.rfqsLastId,
+ rfqItem: rfqPrItems.rfqItem,
+ prItem: rfqPrItems.prItem,
+ prNo: rfqPrItems.prNo,
+
+ // Material 정보
+ materialCode: rfqPrItems.materialCode,
+ materialCategory: rfqPrItems.materialCategory,
+ acc: rfqPrItems.acc,
+ materialDescription: rfqPrItems.materialDescription,
+ size: rfqPrItems.size,
+
+ // Delivery & Quantity
+ deliveryDate: rfqPrItems.deliveryDate,
+ quantity: rfqPrItems.quantity,
+ uom: rfqPrItems.uom,
+ grossWeight: rfqPrItems.grossWeight,
+ gwUom: rfqPrItems.gwUom,
+
+ // Specification
+ specNo: rfqPrItems.specNo,
+ specUrl: rfqPrItems.specUrl,
+ trackingNo: rfqPrItems.trackingNo,
+
+ // Major flag
+ majorYn: rfqPrItems.majorYn,
+
+ // Project 정보
+ projectDef: rfqPrItems.projectDef,
+ projectSc: rfqPrItems.projectSc,
+ projectKl: rfqPrItems.projectKl,
+ projectLc: rfqPrItems.projectLc,
+ projectDl: rfqPrItems.projectDl,
+
+ // Remark
+ remark: rfqPrItems.remark,
+
+ // RFQ 관련 정보 (from rfqsLast)
+ rfqCode: rfqsLast.rfqCode,
+ rfqType: rfqsLast.rfqType,
+ rfqTitle: rfqsLast.rfqTitle,
+ itemCode: rfqsLast.itemCode,
+ itemName: rfqsLast.itemName,
+ prNumber: rfqsLast.prNumber,
+ prIssueDate: rfqsLast.prIssueDate,
+ series: rfqsLast.series,
+ status: rfqsLast.status,
+
+ // ITB 관련 정보
+ projectCompany: rfqsLast.projectCompany,
+ projectFlag: rfqsLast.projectFlag,
+ projectSite: rfqsLast.projectSite,
+ smCode: rfqsLast.smCode,
+
+ // Package 정보
+ packageNo: rfqsLast.packageNo,
+ packageName: rfqsLast.packageName,
+
+ // Project 정보
+ projectCode: projects.code,
+ projectName: projects.name
+ })
+ .from(rfqPrItems)
+ .leftJoin(rfqsLast, eq(rfqPrItems.rfqsLastId, rfqsLast.id))
+ .leftJoin(projects, eq(rfqsLast.projectId, projects.id));
+});
+
+
+
+export const rfqLastAttachments = pgTable(
+ "rfq_last_attachments",
+ {
+ id: serial("id").primaryKey(),
+ attachmentType: varchar("attachment_type", { length: 50 }).notNull(),
+ serialNo: varchar("serial_no", { length: 50 }).notNull(),
+ rfqId: integer("rfq_id")
+ .notNull()
+ .references(() => rfqsLast.id),
+
+ // 현재 리비전 정보 (빠른 접근용)
+ currentRevision: varchar("current_revision", { length: 10 }).notNull().default("Rev.0"),
+ latestRevisionId: integer("latest_revision_id"), // self-reference to bRfqAttachmentRevisions
+
+ // 메타 정보
+ description: varchar("description", { length: 500 }),
+ createdBy: integer("created_by")
+ .references(() => users.id, { onDelete: "set null" })
+ .notNull(),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+ }
+)
+
+// 리비전 테이블 (모든 파일 버전 관리)
+export const rfqLastAttachmentRevisions = pgTable(
+ "rfq_last_attachment_revisions",
+ {
+ id: serial("id").primaryKey(),
+ attachmentId: integer("attachment_id")
+ .notNull()
+ .references(() => rfqLastAttachments.id, { onDelete: "cascade" }),
+
+ // 리비전 정보
+ revisionNo: varchar("revision_no", { length: 10 }).notNull(), // "Rev.0", "Rev.1", "Rev.2"
+ revisionComment: text("revision_comment"),
+ isLatest: boolean("is_latest").notNull().default(true),
+
+ // 파일 정보
+ fileName: varchar("file_name", { length: 255 }).notNull(),
+ originalFileName: varchar("original_file_name", { length: 255 }).notNull(),
+ filePath: varchar("file_path", { length: 512 }).notNull(),
+ fileSize: integer("file_size"),
+ fileType: varchar("file_type", { length: 100 }),
+
+ // 리비전 생성 정보
+ createdBy: integer("created_by")
+ .references(() => users.id, { onDelete: "set null" })
+ .notNull(),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+
+
+ },
+ (t) => ({
+ // 첨부파일당 하나의 최신 리비전만 허용
+ rfqLastLatestRevisionIdx: uniqueIndex('rfq_last_latest_revision_idx')
+ .on(t.attachmentId, t.isLatest)
+ .where(eq(t.isLatest, true)),
+
+ // 첨부파일 + 리비전 번호 유니크
+ rfqLastAttachmentRevisionIdx: uniqueIndex('rfq_last_attachment_revision_idx')
+ .on(t.attachmentId, t.revisionNo),
+ })
+)
+
+// 첨부파일 + 최신 리비전 뷰
+export const rfpAttachmentsWithLatestRevisionView = pgView("rfq_attachments_with_latest_revision", {
+ // 메인 첨부파일 정보
+ attachmentId: integer("attachment_id"),
+ attachmentType: varchar("attachment_type", { length: 50 }),
+ serialNo: varchar("serial_no", { length: 50 }),
+ rfqId: integer("rfq_id"),
+ description: varchar("description", { length: 500 }),
+ currentRevision: varchar("current_revision", { length: 10 }),
+
+ // 최신 리비전 파일 정보
+ revisionId: integer("revision_id"),
+ fileName: varchar("file_name", { length: 255 }),
+ originalFileName: varchar("original_file_name", { length: 255 }),
+ filePath: varchar("file_path", { length: 512 }),
+ fileSize: integer("file_size"),
+ fileType: varchar("file_type", { length: 100 }),
+ revisionComment: text("revision_comment"),
+
+ // 생성/수정 정보
+ createdBy: integer("created_by"),
+ createdByName: varchar("created_by_name", { length: 255 }),
+ createdAt: timestamp("created_at"),
+ updatedAt: timestamp("updated_at"),
+}).as(sql`
+ SELECT
+ a.id as attachment_id,
+ a.attachment_type,
+ a.serial_no,
+ a.rfq_id,
+ a.description,
+ a.current_revision,
+
+ r.id as revision_id,
+ r.file_name,
+ r.original_file_name,
+ r.file_path,
+ r.file_size,
+ r.file_type,
+ r.revision_comment,
+
+ a.created_by,
+ u.name as created_by_name,
+ a.created_at,
+ a.updated_at
+ FROM rfq_last_attachments a
+ LEFT JOIN rfq_last_attachment_revisions r ON a.latest_revision_id = r.id
+ LEFT JOIN users u ON a.created_by = u.id
+ `)
+
+
+export type RfqsLastView = typeof rfqsLastView.$inferSelect;
+export type PrItemsLastView = typeof prItemsLastView.$inferSelect;
+export type RfqLastDetailsView = typeof rfqLastDetailsView.$inferSelect;
+
+
+export const rfqLastRelations = relations(
+ rfqsLast,
+ ({ one, many }) => ({
+ project: one(projects, {
+ fields: [rfqsLast.projectId],
+ references: [projects.id],
+ }),
+ // item: one(items, {
+ // fields: [rfqsLast.itemId],
+ // references: [items.id],
+ // }),
+ createdByUser: one(users, {
+ fields: [rfqsLast.createdBy],
+ references: [users.id],
+ relationName: "rfqCreator",
+ }),
+ updatedByUser: one(users, {
+ fields: [rfqsLast.updatedBy],
+ references: [users.id],
+ relationName: "rfqUpdater",
+ }),
+ rfqDetails: many(rfqLastDetails),
+ rfqPrItems: many(rfqPrItems, { relationName: "rfqPrItems" }),
+
+ })
+);
+
+export const prItemsLastRelations = relations(
+ rfqPrItems,
+ ({ one }) => ({
+ rfq: one(rfqsLast, {
+ fields: [rfqPrItems.rfqsLastId],
+ references: [rfqsLast.id],
+ relationName: "rfqPrItems"
+ }),
+ })
+);
+
+// rfqLastDetails 테이블의 관계 정의
+export const rfqDetailsLastRelations = relations(
+ rfqLastDetails,
+ ({ one }) => ({
+ rfq: one(rfqsLast, {
+ fields: [rfqLastDetails.rfqsLastId],
+ references: [rfqsLast.id],
+ }),
+ vendor: one(vendors, {
+ fields: [rfqLastDetails.vendorsId],
+ references: [vendors.id],
+ }),
+ paymentTerms: one(paymentTerms, {
+ fields: [rfqLastDetails.paymentTermsCode],
+ references: [paymentTerms.code],
+ }),
+ incoterms: one(incoterms, {
+ fields: [rfqLastDetails.incotermsCode],
+ references: [incoterms.code],
+ }),
+ updatedByUser: one(users, {
+ fields: [rfqLastDetails.updatedBy],
+ references: [users.id],
+ }),
+ })
+);
+
diff --git a/db/schema/rfqVendor.ts b/db/schema/rfqVendor.ts
new file mode 100644
index 00000000..e92b1e10
--- /dev/null
+++ b/db/schema/rfqVendor.ts
@@ -0,0 +1,382 @@
+import { pgTable, pgView, serial, varchar, text, timestamp, boolean, integer, numeric, date, alias, jsonb } from "drizzle-orm/pg-core";
+import { eq, sql, relations } from "drizzle-orm";
+import { rfqsLast, rfqLastDetails, rfqPrItems } from "./rfqLast";
+import { users } from "./users";
+import { vendors } from "./vendors";
+import { incoterms, paymentTerms } from "./procurementRFQ";
+
+// ==========================================
+// 1. 벤더 응답 메인 테이블 (견적서 헤더)
+// ==========================================
+export const rfqLastVendorResponses = pgTable(
+ "rfq_last_vendor_responses",
+ {
+ id: serial("id").primaryKey(),
+
+ // RFQ 및 벤더 참조
+ rfqsLastId: integer("rfqs_last_id")
+ .notNull()
+ .references(() => rfqsLast.id, { onDelete: "cascade" }),
+ rfqLastDetailsId: integer("rfq_last_details_id")
+ .notNull()
+ .references(() => rfqLastDetails.id, { onDelete: "cascade" }),
+ vendorId: integer("vendor_id")
+ .notNull()
+ .references(() => vendors.id, { onDelete: "cascade" }),
+
+ // 응답 버전 관리
+ responseVersion: integer("response_version").notNull().default(1),
+ isLatest: boolean("is_latest").notNull().default(true),
+
+ // 응답 상태
+ status: varchar("status", { length: 30 })
+ .$type<"작성중" | "제출완료" | "수정요청" | "최종확정" | "취소">()
+ .notNull()
+ .default("작성중"),
+
+ // 제출 정보
+ submittedAt: timestamp("submitted_at"),
+ submittedBy: integer("submitted_by")
+ .references(() => users.id, { onDelete: "set null" }),
+
+ // 총액 정보
+ totalAmount: numeric("total_amount", { precision: 15, scale: 2 }).$type<number>(),
+ currency: varchar("currency", { length: 10 }).default("USD"),
+
+ // 벤더 제안 조건 (구매자 제시와 다른 경우)
+ vendorCurrency: varchar("vendor_currency", { length: 10 }),
+ vendorPaymentTermsCode: varchar("vendor_payment_terms_code", { length: 50 })
+ .references(() => paymentTerms.code, { onDelete: "set null" }),
+ vendorIncotermsCode: varchar("vendor_incoterms_code", { length: 20 })
+ .references(() => incoterms.code, { onDelete: "set null" }),
+ vendorIncotermsDetail: varchar("vendor_incoterms_detail", { length: 255 }),
+ vendorDeliveryDate: date("vendor_delivery_date", { mode: "date" }).$type<Date | null>(),
+ vendorContractDuration: varchar("vendor_contract_duration", { length: 255 }),
+ vendorTaxCode: varchar("vendor_tax_code", { length: 255 }),
+ vendorPlaceOfShipping: varchar("vendor_place_of_shipping", { length: 255 }),
+ vendorPlaceOfDestination: varchar("vendor_place_of_destination", { length: 255 }),
+
+ // 초도품관리 응답
+ vendorFirstYn: boolean("vendor_first_yn"),
+ vendorFirstDescription: text("vendor_first_description"),
+ vendorFirstAcceptance: varchar("vendor_first_acceptance", { length: 50 })
+ .$type<"수용" | "부분수용" | "거부" | null>(),
+
+ // Spare part 응답
+ vendorSparepartYn: boolean("vendor_sparepart_yn"),
+ vendorSparepartDescription: text("vendor_sparepart_description"),
+ vendorSparepartAcceptance: varchar("vendor_sparepart_acceptance", { length: 50 })
+ .$type<"수용" | "부분수용" | "거부" | null>(),
+
+ // 연동제 적용 응답
+ vendorMaterialPriceRelatedYn: boolean("vendor_material_price_related_yn"),
+ vendorMaterialPriceRelatedReason: text("vendor_material_price_related_reason"),
+
+ // 각 조건별 변경 사유 (구매자 제시와 다른 경우)
+ currencyReason: text("currency_reason"),
+ paymentTermsReason: text("payment_terms_reason"),
+ deliveryDateReason: text("delivery_date_reason"),
+ incotermsReason: text("incoterms_reason"),
+ taxReason: text("tax_reason"),
+ shippingReason: text("shipping_reason"),
+
+ // 전체 비고 및 특이사항
+ generalRemark: text("general_remark"),
+ technicalProposal: text("technical_proposal"),
+
+ // 감사 필드
+ createdBy: integer("created_by")
+ .notNull()
+ .references(() => users.id, { onDelete: "set null" }),
+ updatedBy: integer("updated_by")
+ .notNull()
+ .references(() => users.id, { onDelete: "set null" }),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+ }
+);
+
+// ==========================================
+// 2. 벤더 견적 아이템 테이블 (PR 아이템별 가격)
+// ==========================================
+export const rfqLastVendorQuotationItems = pgTable(
+ "rfq_last_vendor_quotation_items",
+ {
+ id: serial("id").primaryKey(),
+ vendorResponseId: integer("vendor_response_id")
+ .notNull()
+ .references(() => rfqLastVendorResponses.id, { onDelete: "cascade" }),
+ rfqPrItemId: integer("rfq_pr_item_id")
+ .notNull()
+ .references(() => rfqPrItems.id, { onDelete: "cascade" }),
+
+ // PR 아이템 정보 (캐시)
+ prNo: varchar("pr_no", { length: 50 }),
+ materialCode: varchar("material_code", { length: 255 }),
+ materialDescription: varchar("material_description", { length: 255 }),
+
+ // 견적 정보
+ quantity: numeric("quantity", { precision: 12, scale: 2 })
+ .$type<number>()
+ .notNull(),
+ uom: varchar("uom", { length: 50 }),
+
+ // 가격 정보
+ unitPrice: numeric("unit_price", { precision: 15, scale: 4 })
+ .$type<number>()
+ .notNull(),
+ totalPrice: numeric("total_price", { precision: 15, scale: 2 })
+ .$type<number>()
+ .notNull(),
+ currency: varchar("currency", { length: 10 }).default("USD"),
+
+ // 납기 정보
+ vendorDeliveryDate: date("vendor_delivery_date", { mode: "date" }).$type<Date | null>(),
+ leadTime: integer("lead_time"), // days
+
+ // 제조사 정보
+ manufacturer: varchar("manufacturer", { length: 255 }),
+ manufacturerCountry: varchar("manufacturer_country", { length: 100 }),
+ modelNo: varchar("model_no", { length: 255 }),
+
+ // 기술 사양
+ technicalCompliance: boolean("technical_compliance").default(true),
+ alternativeProposal: text("alternative_proposal"),
+
+ // 할인 정보
+ discountRate: numeric("discount_rate", { precision: 5, scale: 2 }).$type<number>(),
+ discountAmount: numeric("discount_amount", { precision: 15, scale: 2 }).$type<number>(),
+
+ // 비고
+ itemRemark: text("item_remark"),
+ deviationReason: text("deviation_reason"), // 요구사항과 다른 경우 사유
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+ }
+);
+
+// ==========================================
+// 4. 벤더 첨부파일 테이블
+// ==========================================
+export const rfqLastVendorAttachments = pgTable(
+ "rfq_last_vendor_attachments",
+ {
+ id: serial("id").primaryKey(),
+ vendorResponseId: integer("vendor_response_id")
+ .notNull()
+ .references(() => rfqLastVendorResponses.id, { onDelete: "cascade" }),
+
+ // 첨부파일 구분
+ attachmentType: varchar("attachment_type", { length: 50 }).notNull(),
+ // "견적서", "기술제안서", "인증서", "카탈로그", "도면", "테스트성적서", "기타"
+
+ documentNo: varchar("document_no", { length: 100 }), // 문서번호
+
+ // 파일 정보
+ fileName: varchar("file_name", { length: 255 }).notNull(),
+ originalFileName: varchar("original_file_name", { length: 255 }).notNull(),
+ filePath: varchar("file_path", { length: 512 }).notNull(),
+ fileSize: integer("file_size"),
+ fileType: varchar("file_type", { length: 100 }),
+
+ // 파일 설명
+ description: text("description"),
+
+ // 유효기간 (인증서 등)
+ validFrom: date("valid_from", { mode: "date" }).$type<Date | null>(),
+ validTo: date("valid_to", { mode: "date" }).$type<Date | null>(),
+
+ // 감사 필드
+ uploadedBy: integer("uploaded_by")
+ .notNull()
+ .references(() => users.id, { onDelete: "set null" }),
+ uploadedAt: timestamp("uploaded_at").defaultNow().notNull(),
+ }
+);
+
+// ==========================================
+// 4. 벤더 응답 이력 테이블 (변경 이력 관리)
+// ==========================================
+export const rfqLastVendorResponseHistory = pgTable(
+ "rfq_last_vendor_response_history",
+ {
+ id: serial("id").primaryKey(),
+ vendorResponseId: integer("vendor_response_id")
+ .notNull()
+ .references(() => rfqLastVendorResponses.id, { onDelete: "cascade" }),
+
+ action: varchar("action", { length: 50 }).notNull(),
+ // "생성", "수정", "제출", "철회", "재제출", "승인", "반려"
+
+ previousStatus: varchar("previous_status", { length: 30 }),
+ newStatus: varchar("new_status", { length: 30 }),
+
+ changeDetails: jsonb("change_details"), // 변경된 필드들
+ changeReason: text("change_reason"),
+
+ performedBy: integer("performed_by")
+ .notNull()
+ .references(() => users.id, { onDelete: "set null" }),
+ performedAt: timestamp("performed_at").defaultNow().notNull(),
+ }
+);
+
+// ==========================================
+// Views
+// ==========================================
+
+// 벤더 응답 종합 뷰
+export const vendorResponseSummaryView = pgView("vendor_response_summary_view").as((qb) => {
+ const responseTable = alias(rfqLastVendorResponses, "response");
+ const rfqTable = alias(rfqsLast, "rfq");
+ const detailTable = alias(rfqLastDetails, "detail");
+ const vendorTable = alias(vendors, "vendor");
+ const submittedByUser = alias(users, "submitted_by_user");
+
+ return qb
+ .select({
+ // 응답 기본 정보
+ responseId: sql<number>`${responseTable.id}`.as("response_id"),
+ rfqId: sql<number>`${rfqTable.id}`.as("rfq_id"),
+ rfqCode: sql<string>`${rfqTable.rfqCode}`.as("rfq_code"),
+
+ // 벤더 정보
+ vendorId: sql<number>`${vendorTable.id}`.as("vendor_id"),
+ vendorName: sql<string>`${vendorTable.vendorName}`.as("vendor_name"),
+ vendorCode: sql<string>`${vendorTable.vendorCode}`.as("vendor_code"),
+
+ // 응답 상태
+ responseVersion: sql<number>`${responseTable.responseVersion}`.as("response_version"),
+ status: sql<string>`${responseTable.status}`.as("status"),
+ isLatest: sql<boolean>`${responseTable.isLatest}`.as("is_latest"),
+
+ // 제출 정보
+ submittedAt: sql<Date | null>`${responseTable.submittedAt}`.as("submitted_at"),
+ submittedByName: sql<string | null>`${submittedByUser.name}`.as("submitted_by_name"),
+
+ // 금액 정보
+ totalAmount: sql<number | null>`${responseTable.totalAmount}`.as("total_amount"),
+ currency: sql<string>`${responseTable.currency}`.as("currency"),
+
+ // 주요 조건 차이
+ deliveryDateMatch: sql<boolean>`
+ CASE
+ WHEN ${detailTable.deliveryDate} = ${responseTable.vendorDeliveryDate} THEN true
+ ELSE false
+ END
+ `.as("delivery_date_match"),
+
+ currencyMatch: sql<boolean>`
+ CASE
+ WHEN ${detailTable.currency} = ${responseTable.vendorCurrency} THEN true
+ ELSE false
+ END
+ `.as("currency_match"),
+
+ // 아이템 수
+ quotedItemCount: sql<number>`(
+ SELECT COUNT(*)
+ FROM rfq_last_vendor_quotation_items
+ WHERE vendor_response_id = ${responseTable.id}
+ )`.as("quoted_item_count"),
+
+ // 첨부파일 수
+ attachmentCount: sql<number>`(
+ SELECT COUNT(*)
+ FROM rfq_last_vendor_attachments
+ WHERE vendor_response_id = ${responseTable.id}
+ )`.as("attachment_count"),
+
+ // 타임스탬프
+ createdAt: sql<Date>`${responseTable.createdAt}`.as("created_at"),
+ updatedAt: sql<Date>`${responseTable.updatedAt}`.as("updated_at"),
+ })
+ .from(responseTable)
+ .leftJoin(rfqTable, eq(responseTable.rfqsLastId, rfqTable.id))
+ .leftJoin(detailTable, eq(responseTable.rfqLastDetailsId, detailTable.id))
+ .leftJoin(vendorTable, eq(responseTable.vendorId, vendorTable.id))
+ .leftJoin(submittedByUser, eq(responseTable.submittedBy, submittedByUser.id));
+});
+
+// 벤더 견적 아이템 상세 뷰
+export const vendorQuotationItemsDetailView = pgView("vendor_quotation_items_detail_view").as((qb) => {
+ const quotationTable = alias(rfqLastVendorQuotationItems, "quotation");
+ const prItemTable = alias(rfqPrItems, "pr_item");
+ const responseTable = alias(rfqLastVendorResponses, "response");
+ const rfqTable = alias(rfqsLast, "rfq");
+
+ return qb
+ .select({
+ // 견적 아이템 정보
+ quotationItemId: sql<number>`${quotationTable.id}`.as("quotation_item_id"),
+ vendorResponseId: sql<number>`${quotationTable.vendorResponseId}`.as("vendor_response_id"),
+
+ // RFQ 정보
+ rfqCode: sql<string>`${rfqTable.rfqCode}`.as("rfq_code"),
+
+ // PR 아이템 정보
+ prNo: sql<string | null>`${prItemTable.prNo}`.as("pr_no"),
+ prItem: sql<string | null>`${prItemTable.prItem}`.as("pr_item"),
+ materialCode: sql<string | null>`${prItemTable.materialCode}`.as("material_code"),
+ materialDescription: sql<string | null>`${prItemTable.materialDescription}`.as("material_description"),
+ originalQuantity: sql<number | null>`${prItemTable.quantity}`.as("original_quantity"),
+ originalDeliveryDate: sql<Date | null>`${prItemTable.deliveryDate}`.as("original_delivery_date"),
+
+ // 벤더 견적 정보
+ quotedQuantity: sql<number>`${quotationTable.quantity}`.as("quoted_quantity"),
+ unitPrice: sql<number>`${quotationTable.unitPrice}`.as("unit_price"),
+ totalPrice: sql<number>`${quotationTable.totalPrice}`.as("total_price"),
+ currency: sql<string>`${quotationTable.currency}`.as("currency"),
+ vendorDeliveryDate: sql<Date | null>`${quotationTable.vendorDeliveryDate}`.as("vendor_delivery_date"),
+ leadTime: sql<number | null>`${quotationTable.leadTime}`.as("lead_time"),
+
+ // 제조사 정보
+ manufacturer: sql<string | null>`${quotationTable.manufacturer}`.as("manufacturer"),
+ modelNo: sql<string | null>`${quotationTable.modelNo}`.as("model_no"),
+
+ // 할인 정보
+ discountRate: sql<number | null>`${quotationTable.discountRate}`.as("discount_rate"),
+ discountAmount: sql<number | null>`${quotationTable.discountAmount}`.as("discount_amount"),
+
+ // 기술 준수
+ technicalCompliance: sql<boolean>`${quotationTable.technicalCompliance}`.as("technical_compliance"),
+ alternativeProposal: sql<string | null>`${quotationTable.alternativeProposal}`.as("alternative_proposal"),
+
+ // 비고
+ itemRemark: sql<string | null>`${quotationTable.itemRemark}`.as("item_remark"),
+ deviationReason: sql<string | null>`${quotationTable.deviationReason}`.as("deviation_reason"),
+ })
+ .from(quotationTable)
+ .leftJoin(prItemTable, eq(quotationTable.rfqPrItemId, prItemTable.id))
+ .leftJoin(responseTable, eq(quotationTable.vendorResponseId, responseTable.id))
+ .leftJoin(rfqTable, eq(responseTable.rfqsLastId, rfqTable.id));
+});
+
+// Relations
+export const vendorResponseRelations = relations(
+ rfqLastVendorResponses,
+ ({ one, many }) => ({
+ rfq: one(rfqsLast, {
+ fields: [rfqLastVendorResponses.rfqsLastId],
+ references: [rfqsLast.id],
+ }),
+ rfqDetail: one(rfqLastDetails, {
+ fields: [rfqLastVendorResponses.rfqLastDetailsId],
+ references: [rfqLastDetails.id],
+ }),
+ vendor: one(vendors, {
+ fields: [rfqLastVendorResponses.vendorId],
+ references: [vendors.id],
+ }),
+ quotationItems: many(rfqLastVendorQuotationItems),
+ attachments: many(rfqLastVendorAttachments),
+ history: many(rfqLastVendorResponseHistory),
+ })
+);
+
+// Type exports
+export type VendorResponse = typeof rfqLastVendorResponses.$inferSelect;
+export type VendorQuotationItem = typeof rfqLastVendorQuotationItems.$inferSelect;
+export type VendorAttachment = typeof rfqLastVendorAttachments.$inferSelect;
+export type VendorResponseHistory = typeof rfqLastVendorResponseHistory.$inferSelect; \ No newline at end of file