summaryrefslogtreecommitdiff
path: root/db/schema
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-04-28 02:13:30 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-04-28 02:13:30 +0000
commitef4c533ebacc2cdc97e518f30e9a9350004fcdfb (patch)
tree345251a3ed0f4429716fa5edaa31024d8f4cb560 /db/schema
parent9ceed79cf32c896f8a998399bf1b296506b2cd4a (diff)
~20250428 작업사항
Diffstat (limited to 'db/schema')
-rw-r--r--db/schema/basicContractDocumnet.ts75
-rw-r--r--db/schema/contract.ts5
-rw-r--r--db/schema/index.ts2
-rw-r--r--db/schema/logs.ts61
-rw-r--r--db/schema/pq.ts46
-rw-r--r--db/schema/projects.ts54
-rw-r--r--db/schema/rfq.ts253
-rw-r--r--db/schema/vendorData.ts52
-rw-r--r--db/schema/vendors.ts181
9 files changed, 630 insertions, 99 deletions
diff --git a/db/schema/basicContractDocumnet.ts b/db/schema/basicContractDocumnet.ts
new file mode 100644
index 00000000..ce22fdc9
--- /dev/null
+++ b/db/schema/basicContractDocumnet.ts
@@ -0,0 +1,75 @@
+import { pgTable, pgView, text, timestamp, integer, varchar } from 'drizzle-orm/pg-core';
+import { vendors } from './vendors';
+import { eq, sql } from "drizzle-orm";
+import { users } from './users';
+
+export const basicContractTemplates = pgTable('basic_contract_templates', {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ templateName: text('template_name').notNull(),
+ status: text('status').notNull().default('ACTIVE'), // ACTIVE, DISPOSED
+ fileName: varchar("file_name", { length: 255 }).notNull(),
+ filePath: varchar("file_path", { length: 1024 }).notNull(),
+ createdAt: timestamp('created_at').defaultNow(),
+ updatedAt: timestamp('updated_at').defaultNow(),
+ validityPeriod: integer('validity_period'), // 계약 유효기간(개월) - 새로 추가할 필드
+
+});
+
+export const basicContract = pgTable('basic_contract', {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ templateId: integer('template_id').references(() => basicContractTemplates.id),
+ vendorId: integer('vendor_id').references(() => vendors.id),
+ requestedBy: integer('requested_by').references(() => users.id),
+ status: text('status').notNull().default('PENDING'), // PENDING, COMPLETED, REJECTED
+ fileName: varchar("file_name", { length: 255 }).notNull(),
+ filePath: varchar("file_path", { length: 1024 }).notNull(),
+ createdAt: timestamp('created_at').defaultNow(),
+ updatedAt: timestamp('updated_at').defaultNow(),
+ completedAt: timestamp('completed_at'), // 계약 체결 완료 날짜 - 새로 추가할 필드
+
+});
+
+
+// 기본 계약 요청 뷰
+export const basicContractView = pgView('basic_contract_view').as((qb) => {
+ return qb
+ .select({
+ // 기본 계약 정보
+ id: sql<number>`${basicContract.id}`.as('id'),
+ templateId: sql<number | null>`${basicContract.templateId}`.as('template_id'),
+ vendorId: sql<number | null>`${basicContract.vendorId}`.as('vendor_id'),
+ requestedBy: sql<number | null>`${basicContract.requestedBy}`.as('requested_by'),
+ status: sql<string>`${basicContract.status}`.as('basic_contract_status'),
+ createdAt: sql<Date>`${basicContract.createdAt}`.as('created_at'),
+ updatedAt: sql<Date>`${basicContract.updatedAt}`.as('updated_at'),
+ completedAt: sql<Date>`${basicContract.updatedAt}`.as('completed_at'),
+
+ // 벤더 정보
+ vendorCode: sql<string | null>`${vendors.vendorCode}`.as('vendor_code'),
+ vendorEmail: sql<string | null>`${vendors.email}`.as('vendor_email'),
+ vendorName: sql<string | null>`${vendors.vendorName}`.as('vendor_name'),
+
+ // 사용자 정보
+ userName: sql<string | null>`${users.name}`.as('user_name'),
+
+ // 템플릿 정보
+ templateName: sql<string | null>`${basicContractTemplates.templateName}`.as('template_name'),
+ validityPeriod: sql<number | null>`${basicContractTemplates.validityPeriod}`.as('validityPeriod'),
+
+ filePath: sql<string | null>`${basicContractTemplates.filePath}`.as('file_path'),
+ fileName: sql<string | null>`${basicContractTemplates.fileName}`.as('file_name'),
+
+ signedFilePath: sql<string | null>`${basicContract.filePath}`.as('signed_file_path'),
+
+ })
+ .from(basicContract)
+ .leftJoin(vendors, eq(basicContract.vendorId, vendors.id))
+ .leftJoin(users, eq(basicContract.requestedBy, users.id))
+ .leftJoin(basicContractTemplates, eq(basicContract.templateId, basicContractTemplates.id));
+});
+
+// 타입 정의
+export type BasicContractTemplate = typeof basicContractTemplates.$inferSelect;
+export type BasicContract = typeof basicContract.$inferSelect;
+export type BasicContractView = typeof basicContractView.$inferSelect;
+
diff --git a/db/schema/contract.ts b/db/schema/contract.ts
index 1d628442..226fecfa 100644
--- a/db/schema/contract.ts
+++ b/db/schema/contract.ts
@@ -14,13 +14,14 @@ import {
import { projects } from "./projects"
import { vendorContacts, vendors } from "./vendors"
import { eq, sql } from "drizzle-orm";
+import { items } from "./items";
// ============ contracts (계약/PO 정보) ============
export const contracts = pgTable("contracts", {
// 주 키
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
- // 프로젝트와 벤더 참조
+ // 프로젝트와 협력업체 참조
projectId: integer("project_id")
.notNull()
.references(() => projects.id, { onDelete: "cascade" }),
@@ -81,7 +82,7 @@ export const contractItems = pgTable("contract_items", {
itemId: integer("item_id")
.notNull()
- // .references(() => items.id, { onDelete: "cascade" })
+ .references(() => items.id, { onDelete: "cascade" })
,
// --- 품목(아이템) 단위 정보 ---
diff --git a/db/schema/index.ts b/db/schema/index.ts
index 714f058b..189519e8 100644
--- a/db/schema/index.ts
+++ b/db/schema/index.ts
@@ -10,3 +10,5 @@ export * from './vendorData';
export * from './vendorDocu';
export * from './vendors';
export * from './tasks';
+export * from './logs';
+export * from './basicContractDocumnet';
diff --git a/db/schema/logs.ts b/db/schema/logs.ts
new file mode 100644
index 00000000..1f402e1f
--- /dev/null
+++ b/db/schema/logs.ts
@@ -0,0 +1,61 @@
+
+import { pgTable, serial, varchar, text, timestamp, boolean, integer ,pgView} from "drizzle-orm/pg-core";
+import { items } from "./items";
+import { sql, eq, relations} from "drizzle-orm";
+import { users } from "./users";
+import { vendorCandidates, vendors } from "./vendors";
+
+
+export const vendorCandidateLogs = pgTable("vendor_candidate_logs", {
+ id: serial("id").primaryKey(),
+
+ // 어떤 vendor candidate에 대한 로그인지
+ vendorCandidateId: integer("vendor_candidate_id")
+ .references(() => vendorCandidates.id, { onDelete: "cascade" })
+ .notNull(),
+
+ // 어떤 user가 한 것인지
+ userId: integer("user_id")
+ .references(() => users.id, { onDelete: "cascade" })
+ .notNull(),
+
+ // 액션(행동) 종류 (예: "status_change", "invite_sent" 등)
+ action: varchar("action", { length: 100 }).notNull(),
+
+ // 상태가 바뀌는 로그라면, 이전/이후 상태를 기록
+ oldStatus: varchar("old_status", { length: 30 }),
+ newStatus: varchar("new_status", { length: 30 }),
+
+ // 기타 필요하면 메시지/설명/메타데이터 등에 대한 컬럼 추가
+ comment: text("comment"), // 변경 사유나 코멘트 등
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ });
+
+
+ export const vendorsLogs = pgTable("vendors_logs", {
+ id: serial("id").primaryKey(),
+
+ // 어떤 vendor candidate에 대한 로그인지
+ vendorId: integer("vendor_id")
+ .references(() => vendors.id, { onDelete: "cascade" })
+ .notNull(),
+
+ // 어떤 user가 한 것인지
+ userId: integer("user_id")
+ .references(() => users.id, { onDelete: "cascade" })
+ .notNull(),
+
+ // 액션(행동) 종류 (예: "status_change", "invite_sent" 등)
+ action: varchar("action", { length: 100 }).notNull(),
+
+ // 상태가 바뀌는 로그라면, 이전/이후 상태를 기록
+ oldStatus: varchar("old_status", { length: 30 }),
+ newStatus: varchar("new_status", { length: 30 }),
+
+ // 기타 필요하면 메시지/설명/메타데이터 등에 대한 컬럼 추가
+ comment: text("comment"), // 변경 사유나 코멘트 등
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ });
+ \ No newline at end of file
diff --git a/db/schema/pq.ts b/db/schema/pq.ts
index 76d15d5d..9aac7b89 100644
--- a/db/schema/pq.ts
+++ b/db/schema/pq.ts
@@ -1,9 +1,10 @@
import {
pgTable, serial, integer, varchar, text,
- timestamp, uniqueIndex
+ timestamp, uniqueIndex, pgView
} from "drizzle-orm/pg-core";
-import { vendors } from "./vendors";
+import { vendorTypes, vendors } from "./vendors";
import { projects } from "./projects";
+import { sql } from "drizzle-orm";
export const pqCriterias = pgTable("pq_criterias", {
id: serial("id").primaryKey(),
@@ -81,7 +82,7 @@ export const vendorPqReviewLogs = pgTable("vendor_pq_review_logs", {
export type PqCriterias = typeof pqCriterias.$inferSelect
-// 벤더와 프로젝트 PQ 요청 연결 테이블
+// 협력업체와 프로젝트 PQ 요청 연결 테이블
export const vendorProjectPQs = pgTable("vendor_project_pqs", {
id: serial("id").primaryKey(),
@@ -144,3 +145,42 @@ export const vendorPqCriteriaAnswers = pgTable("vendor_pq_criteria_answers", {
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
+
+
+export const projectApprovedVendors = pgView("project_approved_vendors").as((qb) => {
+ return qb
+ .select({
+ vendor_id: vendors.id,
+ vendor_name: vendors.vendorName,
+ vendor_code: vendors.vendorCode,
+ tax_id: vendors.taxId,
+ vendor_email: vendors.email,
+ vendor_phone: vendors.phone,
+ vendor_status: vendors.status,
+ // vendor_type_code: vendorTypes.code,
+ vendor_type_name_ko: vendorTypes.nameKo,
+ vendor_type_name_en: vendorTypes.nameEn,
+ project_code: projects.code,
+ project_name: projects.name,
+ project_type: projects.type,
+ // pq_status: vendorProjectPQs.status,
+ submitted_at: vendorProjectPQs.submittedAt,
+ approved_at: vendorProjectPQs.approvedAt
+ })
+ .from(vendors)
+ .innerJoin(
+ vendorProjectPQs,
+ sql`${vendorProjectPQs.vendorId} = ${vendors.id}`
+ )
+ .innerJoin(
+ projects,
+ sql`${vendorProjectPQs.projectId} = ${projects.id}`
+ )
+ .leftJoin(
+ vendorTypes,
+ sql`${vendors.vendorTypeId} = ${vendorTypes.id}`
+ )
+ .where(sql`${vendorProjectPQs.status} = 'APPROVED'`);
+});
+
+export type ProjectApprovedVendors = typeof projectApprovedVendors.$inferSelect
diff --git a/db/schema/projects.ts b/db/schema/projects.ts
index 9220b249..1b989d23 100644
--- a/db/schema/projects.ts
+++ b/db/schema/projects.ts
@@ -1,4 +1,4 @@
-import { pgTable, varchar, text, timestamp, serial } from "drizzle-orm/pg-core"
+import { pgTable, varchar, text, timestamp,char, decimal, serial,uniqueIndex } from "drizzle-orm/pg-core"
export const projects = pgTable("projects", {
id: serial("id").primaryKey(),
@@ -10,4 +10,54 @@ export const projects = pgTable("projects", {
updatedAt: timestamp("updated_at").defaultNow().notNull(),
})
-export type Project = typeof projects.$inferSelect \ No newline at end of file
+export type Project = typeof projects.$inferSelect
+
+
+export const biddingProjects = pgTable("bidding_projects", {
+ id: serial("id").primaryKey(),
+ pspid: char('pspid', { length: 24 }).notNull().unique(), // 견적프로젝트번호
+ projNm: varchar('proj_nm', { length: 90 }), // 견적프로젝트명
+ sector: char('sector', { length: 1 }), // 부문(S / M)
+ projMsrm: decimal('proj_msrm', { precision: 3, scale: 0 }), // 척수
+ kunnr: char('kunnr', { length: 10 }), // 선주코드
+ kunnrNm: varchar('kunnr_nm', { length: 30 }), // 선주명
+ cls1: char('cls_1', { length: 10 }), // 선급코드
+ cls1Nm: varchar('cls1_nm', { length: 30 }), // 선급명
+ ptype: char('ptype', { length: 3 }), // 선종코드
+ ptypeNm: varchar('ptype_nm', { length: 40 }), // 선종명
+ pmodelCd: char('pmodel_cd', { length: 10 }), // 선형코드
+ pmodelNm: varchar('pmodel_nm', { length: 40 }), // 선형명
+ pmodelSz: varchar('pmodel_sz', { length: 20 }), // 선형크기
+ pmodelUom: char('pmodel_uom', { length: 5 }), // 선형단위
+ txt04: char('txt04', { length: 4 }), // 견적상태코드
+ txt30: varchar('txt30', { length: 30 }), // 견적상태명
+ estmPm: varchar('estm_pm', { length: 30 }), // 견적대표PM 성명
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+});
+
+export const projectSeries = pgTable('project_series', {
+ pspid: char('pspid', { length: 24 }).notNull().references(() => biddingProjects.pspid), // 견적프로젝트번호
+ sersNo: char('sers_no', { length: 3 }).notNull(), // 시리즈번호
+ // 받은 인터페이스 정의서에 따라 수정
+ // klQtr: char('kl_qtr', { length: 10 }), // K/L 연도분기(YYYY_nQ)
+ scDt: char('sc_dt', {length: 8}), // Steel Cutting Date
+ klDt: char('kl_dt', {length: 8}), // Keel Laying Date
+ lcDt: char('lc_dt', {length: 8}), // Launching Date
+ dlDt: char('dl_dt', {length: 8}), // Delivery Date
+ dockNo: char('dock_no', { length: 3 }), // 도크코드
+ dockNm: varchar('dock_nm', { length: 40 }), // 도크명
+ projNo: char('proj_no', { length: 24 }), // SN공사번호(계약후)
+ post1: varchar('post1', { length: 40 }), // SN공사명(계약후)
+}, (table) => {
+ return {
+ uniqueIdx: uniqueIndex("project_sersNo_unique").on(
+ table.pspid,
+ table.sersNo
+ )
+ }
+});
+
+export type BiddingProjects = typeof biddingProjects.$inferSelect
+export type ProjectSeries = typeof projectSeries.$inferSelect \ No newline at end of file
diff --git a/db/schema/rfq.ts b/db/schema/rfq.ts
index 98c4245c..4e35dd10 100644
--- a/db/schema/rfq.ts
+++ b/db/schema/rfq.ts
@@ -3,7 +3,7 @@ import { vendors } from "./vendors";
import { users } from "./users";
import { items } from "./items";
import { eq, sql, and } from "drizzle-orm";
-import { projects } from "./projects";
+import { biddingProjects, projects } from "./projects";
export const rfqs = pgTable(
"rfqs",
@@ -17,6 +17,9 @@ export const rfqs = pgTable(
projectId: integer("project_id")
.references(() => projects.id, { onDelete: "set null" }),
+ bidProjectId: integer("bid_project_id")
+ .references(() => biddingProjects.id, { onDelete: "set null" }),
+
description: varchar("description", { length: 255 }),
dueDate: date("due_date", { mode: "date" })
@@ -71,7 +74,6 @@ export const rfqItems = pgTable("rfq_items", {
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
-
export const rfqAttachments = pgTable("rfq_attachments", {
id: serial("id").primaryKey(),
rfqId: integer("rfq_id").references(() => rfqs.id),
@@ -103,7 +105,7 @@ export const rfqComments = pgTable("rfq_comments", {
.references(() => rfqEvaluations.id)
.$type<number | null>(),
cbeId: integer("cbe_id")
- .references(() => cbeEvaluations.id)
+ .references(() => vendorResponses.id)
.$type<number | null>(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
@@ -175,6 +177,12 @@ export const vendorTechnicalResponses = pgTable(
.notNull()
.references(() => vendorResponses.id, { onDelete: "cascade" }),
+ responseStatus: varchar("response_status", { length: 30 })
+ .$type<"PENDING" | "IN_PROGRESS" | "SUBMITTED" | "REJECTED" | "ACCEPTED">()
+ .default("PENDING")
+ .notNull(),
+
+
// 간소화된 기술 응답 필드
// 기술 내용은 주로 첨부 파일로 받으므로 최소한의 필드만 유지
summary: text("summary"), // 간단한 기술 응답 요약
@@ -197,6 +205,11 @@ export const vendorCommercialResponses = pgTable(
.notNull()
.references(() => vendorResponses.id, { onDelete: "cascade" }),
+ responseStatus: varchar("response_status", { length: 30 })
+ .$type<"PENDING" | "IN_PROGRESS" | "SUBMITTED" | "REJECTED" | "ACCEPTED">()
+ .default("PENDING")
+ .notNull(),
+
// Commercial response fields
totalPrice: numeric("total_price", { precision: 18, scale: 2 }).$type<number>(),
currency: varchar("currency", { length: 10 }).default("USD"),
@@ -260,7 +273,7 @@ export const cbeEvaluations = pgTable("cbe_evaluations", {
.notNull()
.references(() => rfqs.id, { onDelete: "cascade" }),
- // 어떤 벤더의 상업평가(CBE)인지
+ // 어떤 협력업체의 상업평가(CBE)인지
vendorId: integer("vendor_id")
.notNull()
.references(() => vendors.id, { onDelete: "cascade" }),
@@ -380,7 +393,7 @@ export const rfqsView = pgView("rfqs_view").as((qb) => {
.leftJoin(users, eq(rfqs.createdBy, users.id))
})
-// vendorRfqView 업데이트 벤더 기준
+// vendorRfqView 업데이트 협력업체 기준
export const vendorRfqView = pgView("vendor_rfq_view").as((qb) => {
return qb
.select({
@@ -437,9 +450,17 @@ export const vendorTbeView = pgView("vendor_tbe_view").as((qb) => {
rfqVendorStatus: sql<string | null>`${vendorResponses.responseStatus}`.as("rfq_vendor_status"),
rfqVendorUpdated: sql<Date | null>`${vendorResponses.updatedAt}`.as("rfq_vendor_updated"),
+ // TBE 응답 상태 추가
+ technicalResponseId: sql<number | null>`${vendorTechnicalResponses.id}`.as("technical_response_id"),
+ technicalResponseStatus: sql<string | null>`${vendorTechnicalResponses.responseStatus}`.as("technical_response_status"),
+ technicalSummary: sql<string | null>`${vendorTechnicalResponses.summary}`.as("technical_summary"),
+ technicalNotes: sql<string | null>`${vendorTechnicalResponses.notes}`.as("technical_notes"),
+ technicalUpdated: sql<Date | null>`${vendorTechnicalResponses.updatedAt}`.as("technical_updated"),
+
// rfqs
rfqCode: sql<string | null>`${rfqs.rfqCode}`.as("rfq_code"),
rfqType: sql<string | null>`${rfqs.rfqType}`.as("rfq_type"),
+ rfqStatus: sql<string | null>`${rfqs.status}`.as("rfq_status"),
description: sql<string | null>`${rfqs.description}`.as("description"),
dueDate: sql<Date | null>`${rfqs.dueDate}`.as("due_date"),
@@ -468,6 +489,11 @@ export const vendorTbeView = pgView("vendor_tbe_view").as((qb) => {
.leftJoin(
projects,
eq(rfqs.projectId, projects.id)
+
+ )
+ .leftJoin(
+ vendorTechnicalResponses,
+ eq(vendorTechnicalResponses.responseId, vendorResponses.id)
)
// 3) rfqEvaluations
.leftJoin(
@@ -482,68 +508,68 @@ export const vendorTbeView = pgView("vendor_tbe_view").as((qb) => {
export const vendorCbeView = pgView("vendor_cbe_view").as((qb) => {
return qb
- .select({
- // [1] Vendor 기본정보
- vendorId: sql<number>`${vendors.id}`.as("vendor_id"),
- vendorName: sql<string>`${vendors.vendorName}`.as("vendor_name"),
- vendorCode: sql<string | null>`${vendors.vendorCode}`.as("vendor_code"),
- address: sql<string | null>`${vendors.address}`.as("address"),
- country: sql<string | null>`${vendors.country}`.as("country"),
- email: sql<string | null>`${vendors.email}`.as("email"),
- website: sql<string | null>`${vendors.website}`.as("website"),
- vendorStatus: sql<string>`${vendors.status}`.as("vendor_status"),
-
- // [2] rfq_vendors (vendorResponses)
- vendorResponseId: sql<number | null>`${vendorResponses.id}`.as("vendor_response_id"),
- rfqId: sql<number | null>`${vendorResponses.rfqId}`.as("rfq_id"),
- rfqVendorStatus: sql<string | null>`${vendorResponses.responseStatus}`.as("rfq_vendor_status"),
- rfqVendorUpdated: sql<Date | null>`${vendorResponses.updatedAt}`.as("rfq_vendor_updated"),
-
- // [3] rfqs
- rfqCode: sql<string | null>`${rfqs.rfqCode}`.as("rfq_code"),
- rfqType: sql<string | null>`${rfqs.rfqType}`.as("rfq_type"),
- description: sql<string | null>`${rfqs.description}`.as("description"),
- dueDate: sql<Date | null>`${rfqs.dueDate}`.as("due_date"),
-
- // [4] 프로젝트 정보
- projectId: sql<number | null>`${projects.id}`.as("project_id"),
- projectCode: sql<string | null>`${projects.code}`.as("project_code"),
- projectName: sql<string | null>`${projects.name}`.as("project_name"),
-
- // [5] CBE 평가(cbeEvaluations)
- cbeId: sql<number | null>`${cbeEvaluations.id}`.as("cbe_id"),
- cbeResult: sql<string | null>`${cbeEvaluations.result}`.as("cbe_result"),
- cbeNote: sql<string | null>`${cbeEvaluations.notes}`.as("cbe_note"),
- cbeUpdated: sql<Date | null>`${cbeEvaluations.updatedAt}`.as("cbe_updated"),
-
- // 상업평가용 추가 필드들
- totalCost: sql<number | null>`${cbeEvaluations.totalCost}`.as("total_cost"),
- currency: sql<string | null>`${cbeEvaluations.currency}`.as("currency"),
- paymentTerms: sql<string | null>`${cbeEvaluations.paymentTerms}`.as("payment_terms"),
- incoterms: sql<string | null>`${cbeEvaluations.incoterms}`.as("incoterms"),
- deliverySchedule: sql<string | null>`${cbeEvaluations.deliverySchedule}`.as("delivery_schedule"),
- })
- .from(vendors)
- .leftJoin(
- vendorResponses,
- eq(vendorResponses.vendorId, vendors.id)
- )
- .leftJoin(
- rfqs,
- eq(vendorResponses.rfqId, rfqs.id)
- )
- .leftJoin(
- projects,
- eq(rfqs.projectId, projects.id)
- )
- .leftJoin(
- cbeEvaluations,
- and(
- eq(cbeEvaluations.vendorId, vendors.id),
- eq(cbeEvaluations.rfqId, vendorResponses.rfqId)
+ .select({
+ // [1] Vendor 기본정보
+ vendorId: sql<number>`${vendors.id}`.as("vendor_id"),
+ vendorName: sql<string>`${vendors.vendorName}`.as("vendor_name"),
+ vendorCode: sql<string | null>`${vendors.vendorCode}`.as("vendor_code"),
+ address: sql<string | null>`${vendors.address}`.as("address"),
+ country: sql<string | null>`${vendors.country}`.as("country"),
+ email: sql<string | null>`${vendors.email}`.as("email"),
+ website: sql<string | null>`${vendors.website}`.as("website"),
+ vendorStatus: sql<string>`${vendors.status}`.as("vendor_status"),
+
+ // [2] rfq_vendors (vendorResponses)
+ vendorResponseId: sql<number | null>`${vendorResponses.id}`.as("vendor_response_id"),
+ rfqId: sql<number | null>`${vendorResponses.rfqId}`.as("rfq_id"),
+ rfqVendorStatus: sql<string | null>`${vendorResponses.responseStatus}`.as("rfq_vendor_status"),
+ rfqVendorUpdated: sql<Date | null>`${vendorResponses.updatedAt}`.as("rfq_vendor_updated"),
+
+ // [3] rfqs
+ rfqCode: sql<string | null>`${rfqs.rfqCode}`.as("rfq_code"),
+ rfqType: sql<string | null>`${rfqs.rfqType}`.as("rfq_type"),
+ description: sql<string | null>`${rfqs.description}`.as("description"),
+ dueDate: sql<Date | null>`${rfqs.dueDate}`.as("due_date"),
+
+ // [4] 프로젝트 정보
+ projectId: sql<number | null>`${projects.id}`.as("project_id"),
+ projectCode: sql<string | null>`${projects.code}`.as("project_code"),
+ projectName: sql<string | null>`${projects.name}`.as("project_name"),
+
+ // [5] CBE 평가(cbeEvaluations)
+ cbeId: sql<number | null>`${cbeEvaluations.id}`.as("cbe_id"),
+ cbeResult: sql<string | null>`${cbeEvaluations.result}`.as("cbe_result"),
+ cbeNote: sql<string | null>`${cbeEvaluations.notes}`.as("cbe_note"),
+ cbeUpdated: sql<Date | null>`${cbeEvaluations.updatedAt}`.as("cbe_updated"),
+
+ // 상업평가용 추가 필드들
+ totalCost: sql<number | null>`${cbeEvaluations.totalCost}`.as("total_cost"),
+ currency: sql<string | null>`${cbeEvaluations.currency}`.as("currency"),
+ paymentTerms: sql<string | null>`${cbeEvaluations.paymentTerms}`.as("payment_terms"),
+ incoterms: sql<string | null>`${cbeEvaluations.incoterms}`.as("incoterms"),
+ deliverySchedule: sql<string | null>`${cbeEvaluations.deliverySchedule}`.as("delivery_schedule"),
+ })
+ .from(vendors)
+ .leftJoin(
+ vendorResponses,
+ eq(vendorResponses.vendorId, vendors.id)
+ )
+ .leftJoin(
+ rfqs,
+ eq(vendorResponses.rfqId, rfqs.id)
)
- );
- });
+ .leftJoin(
+ projects,
+ eq(rfqs.projectId, projects.id)
+ )
+ .leftJoin(
+ cbeEvaluations,
+ and(
+ eq(cbeEvaluations.vendorId, vendors.id),
+ eq(cbeEvaluations.rfqId, vendorResponses.rfqId)
+ )
+ );
+});
// Types for TypeScript
export type VendorResponse = typeof vendorResponses.$inferSelect;
@@ -582,7 +608,7 @@ export const vendorResponsesView = pgView("vendor_responses_view").as((qb) => {
// Response status
responseStatus: sql<"INVITED" | "ACCEPTED" | "DECLINED" | "REVIEWING" | "RESPONDED">`${vendorResponses.responseStatus}`
- .as("response_status"),
+ .as("response_status"),
respondedAt: sql<Date>`${vendorResponses.respondedAt}`.as("responded_at"),
// Technical response indicators
@@ -713,6 +739,10 @@ export interface RfqWithItems extends Rfq {
lines: RfqItem[];
}
+export interface RfqViewWithItems extends RfqsView {
+ lines: RfqItem[];
+}
+
export interface RfqWithAll extends VendorResponsesView {
// 아래처럼 배열/객체로
@@ -741,3 +771,92 @@ export interface RfqWithAll extends VendorResponsesView {
}
export type RfqsView = typeof rfqsView.$inferSelect
+
+
+export const vendorResponseCBEView = pgView("vendor_response_cbe_view").as((qb) => {
+ return qb
+ .select({
+ // 기본 응답 식별 정보
+ responseId: sql<number>`${vendorResponses.id}`.as("response_id"),
+ rfqId: sql<number>`${vendorResponses.rfqId}`.as("rfq_id"),
+ vendorId: sql<number>`${vendorResponses.vendorId}`.as("vendor_id"),
+
+ // vendorResponses 상세 정보
+ responseStatus: sql<string>`${vendorResponses.responseStatus}`.as("response_status"),
+ notes: sql<string | null>`${vendorResponses.notes}`.as("response_notes"),
+ respondedBy: sql<string | null>`${vendorResponses.respondedBy}`.as("responded_by"),
+ respondedAt: sql<Date>`${vendorResponses.respondedAt}`.as("responded_at"),
+ responseUpdatedAt: sql<Date>`${vendorResponses.updatedAt}`.as("response_updated_at"),
+
+ // RFQ 정보
+ rfqCode: sql<string>`${rfqs.rfqCode}`.as("rfq_code"),
+ rfqDescription: sql<string | null>`${rfqs.description}`.as("rfq_description"),
+ rfqDueDate: sql<Date | null>`${rfqs.dueDate}`.as("rfq_due_date"),
+ rfqStatus: sql<string>`${rfqs.status}`.as("rfq_status"),
+ rfqType: sql<string | null>`${rfqs.rfqType}`.as("rfq_type"),
+
+ // 협력업체 정보
+ vendorName: sql<string>`${vendors.vendorName}`.as("vendor_name"),
+ vendorCode: sql<string | null>`${vendors.vendorCode}`.as("vendor_code"),
+ vendorStatus: sql<string | null>`${vendors.status}`.as("vendor_status"),
+
+ // 프로젝트 정보
+ projectId: sql<number | null>`${projects.id}`.as("project_id"),
+ projectCode: sql<string | null>`${projects.code}`.as("project_code"),
+ projectName: sql<string | null>`${projects.name}`.as("project_name"),
+
+ // 상업 응답 상세 정보
+ commercialResponseId: sql<number | null>`${vendorCommercialResponses.id}`.as("commercial_response_id"),
+ commercialResponseStatus: sql<string | null>`${vendorCommercialResponses.responseStatus}`.as("commercial_response_status"),
+ totalPrice: sql<number | null>`${vendorCommercialResponses.totalPrice}`.as("total_price"),
+ currency: sql<string | null>`${vendorCommercialResponses.currency}`.as("currency"),
+ paymentTerms: sql<string | null>`${vendorCommercialResponses.paymentTerms}`.as("payment_terms"),
+ incoterms: sql<string | null>`${vendorCommercialResponses.incoterms}`.as("incoterms"),
+ deliveryPeriod: sql<string | null>`${vendorCommercialResponses.deliveryPeriod}`.as("delivery_period"),
+ warrantyPeriod: sql<string | null>`${vendorCommercialResponses.warrantyPeriod}`.as("warranty_period"),
+ validityPeriod: sql<string | null>`${vendorCommercialResponses.validityPeriod}`.as("validity_period"),
+ priceBreakdown: sql<string | null>`${vendorCommercialResponses.priceBreakdown}`.as("price_breakdown"),
+ commercialNotes: sql<string | null>`${vendorCommercialResponses.commercialNotes}`.as("commercial_notes"),
+ commercialCreatedAt: sql<Date | null>`${vendorCommercialResponses.createdAt}`.as("commercial_created_at"),
+ commercialUpdatedAt: sql<Date | null>`${vendorCommercialResponses.updatedAt}`.as("commercial_updated_at"),
+
+ // 첨부파일 개수 및 상세 정보
+ attachmentCount: sql<number>`(
+ SELECT COUNT(*)
+ FROM "vendor_response_attachments"
+ WHERE "vendor_response_attachments"."response_id" = ${vendorResponses.id}
+ )`.as("attachment_count"),
+
+ commercialAttachmentCount: sql<number>`(
+ SELECT COUNT(*)
+ FROM "vendor_response_attachments"
+ WHERE "vendor_response_attachments"."commercial_response_id" = ${vendorCommercialResponses.id}
+ )`.as("commercial_attachment_count"),
+
+ // 첨부파일 유형별 개수 (선택적)
+ technicalAttachmentCount: sql<number>`(
+ SELECT COUNT(*)
+ FROM "vendor_response_attachments"
+ WHERE "vendor_response_attachments"."response_id" = ${vendorResponses.id}
+ AND "vendor_response_attachments"."attachment_type" = 'TECHNICAL_SPEC'
+ )`.as("technical_attachment_count"),
+
+ // 최신 첨부파일 정보 (선택적)
+ latestAttachmentDate: sql<Date | null>`(
+ SELECT MAX("uploaded_at")
+ FROM "vendor_response_attachments"
+ WHERE "vendor_response_attachments"."response_id" = ${vendorResponses.id}
+ )`.as("latest_attachment_date"),
+ })
+ .from(vendorResponses)
+ .innerJoin(rfqs, eq(vendorResponses.rfqId, rfqs.id))
+ .innerJoin(vendors, eq(vendorResponses.vendorId, vendors.id))
+ .leftJoin(projects, eq(rfqs.projectId, projects.id))
+ .leftJoin(
+ vendorCommercialResponses,
+ eq(vendorCommercialResponses.responseId, vendorResponses.id)
+ );
+});
+
+// TypeScript 타입 정의
+export type VendorResponseCBEView = typeof vendorResponseCBEView.$inferSelect; \ No newline at end of file
diff --git a/db/schema/vendorData.ts b/db/schema/vendorData.ts
index 36810e50..ea6180ea 100644
--- a/db/schema/vendorData.ts
+++ b/db/schema/vendorData.ts
@@ -10,9 +10,10 @@ import {
uniqueIndex,
primaryKey,
foreignKey,
- pgView
+ pgView,
+ boolean
} from "drizzle-orm/pg-core"
-import { and, eq} from "drizzle-orm";
+import { relations, and, eq, sql} from "drizzle-orm";
import { contractItems } from "./contract"
import { projects } from "./projects" // projects 테이블 임포트 가정
@@ -25,6 +26,10 @@ export const forms = pgTable("forms", {
.references(() => contractItems.id, { onDelete: "cascade" }),
formCode: varchar("form_code", { length: 100 }).notNull(),
formName: varchar("form_name", { length: 255 }).notNull(),
+ // source: varchar("source", { length: 255 }),
+ // 새로 추가된 칼럼: eng와 im
+ eng: boolean("eng").default(false).notNull(),
+ im: boolean("im").default(false).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
}, (table) => {
@@ -71,18 +76,22 @@ export const formEntries = pgTable("form_entries", {
export const tags = pgTable("tags", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
contractItemId: integer("contract_item_id")
- .notNull()
- .references(() => contractItems.id, { onDelete: "cascade" }),
+ .notNull()
+ .references(() => contractItems.id, { onDelete: "cascade" }),
formId: integer("form_id")
- .references(() => forms.id, { onDelete: "set null" }),
+ .references(() => forms.id, { onDelete: "set null" }),
tagNo: varchar("tag_no", { length: 100 }).notNull(),
tagType: varchar("tag_type", { length: 50 }).notNull(),
class: varchar("class", { length: 100 }).notNull(),
description: text("description"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
-})
-
+ }, (table) => {
+ return {
+ contractItemTagNoUnique: unique("contract_item_tag_no_unique").on(table.contractItemId, table.tagNo),
+ };
+ });
+
// tagTypes에 projectId 추가 및 복합 기본키 생성
export const tagTypes = pgTable("tag_types", {
code: varchar("code", { length: 50 }).notNull(),
@@ -122,10 +131,10 @@ export const tagSubfields = pgTable("tag_subfields", {
table.attributesId
),
// tagTypes 참조를 위한 복합 FK (tagTypeCode, projectId)
- tagTypeRef: foreignKey({
- columns: [table.tagTypeCode, table.projectId],
- foreignColumns: [tagTypes.code, tagTypes.projectId]
- }).onDelete("cascade")
+ // tagTypeRef: foreignKey({
+ // columns: [table.tagTypeCode, table.projectId],
+ // foreignColumns: [tagTypes.code, tagTypes.projectId]
+ // }).onDelete("cascade")
};
});
@@ -187,18 +196,17 @@ export const tagClasses = pgTable("tag_classes", {
// tagTypeClassFormMappings에 projectId 추가
export const tagTypeClassFormMappings = pgTable("tag_type_class_form_mappings", {
id: serial("id").primaryKey(),
- projectId: integer("project_id")
- .notNull()
- .references(() => projects.id, { onDelete: "cascade" }),
+ projectId: integer("project_id").notNull(), // Remove the reference here
tagTypeLabel: varchar("tag_type_label", { length: 255 }).notNull(),
classLabel: varchar("class_label", { length: 255 }).notNull(),
formCode: varchar("form_code", { length: 50 }).notNull(),
formName: varchar("form_name", { length: 255 }).notNull(),
+ ep: varchar("ep", { length: 255 }),
+ remark: varchar("remark", { length: 255 }),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
}, (table) => {
return {
- // 매핑은 프로젝트 내에서 유니크해야 함
uniqMappingInProject: unique("uniq_mapping_in_project").on(
table.projectId,
table.tagTypeLabel,
@@ -206,13 +214,21 @@ export const tagTypeClassFormMappings = pgTable("tag_type_class_form_mappings",
table.formCode
)
};
-})
+});
+
+export const tagTypeClassFormMappingsRelations = relations(tagTypeClassFormMappings, ({ one }) => ({
+ project: one(projects, {
+ fields: [tagTypeClassFormMappings.projectId],
+ references: [projects.id],
+ }),
+}));
// view_tag_subfields에도 projectId 추가
export const viewTagSubfields = pgView("view_tag_subfields").as((qb) => {
return qb
.select({
- // id: tagSubfields.id,
+
+ id: sql<number>`${tagSubfields.id}`.as("id"),
// projectId: tagSubfields.projectId,
tagTypeCode: tagSubfields.tagTypeCode,
tagTypeDescription: tagTypes.description,
@@ -224,7 +240,7 @@ export const viewTagSubfields = pgView("view_tag_subfields").as((qb) => {
createdAt: tagSubfields.createdAt,
updatedAt: tagSubfields.updatedAt,
// 프로젝트 관련 정보 추가
- projectId: projects.id,
+ projectId: sql<number>`${projects.id}`.as("project_id"), // Explicitly alias projects.id
projectCode: projects.code,
projectName: projects.name
})
diff --git a/db/schema/vendors.ts b/db/schema/vendors.ts
index acdec3d2..f56a1288 100644
--- a/db/schema/vendors.ts
+++ b/db/schema/vendors.ts
@@ -1,7 +1,20 @@
// db/schema/vendors.ts
-import { pgTable, serial, varchar, text, timestamp, boolean, integer ,pgView} from "drizzle-orm/pg-core";
+import { pgTable, serial, varchar, text, timestamp, boolean, integer ,pgView} from "drizzle-orm/pg-core";
import { items } from "./items";
-import { sql, eq} from "drizzle-orm";
+import { sql, eq, relations} from "drizzle-orm";
+import { users } from "./users";
+
+
+// vendorTypes 테이블 생성
+export const vendorTypes = pgTable("vendor_types", {
+ id: serial("id").primaryKey(),
+ code: varchar("code", { length: 50 }).notNull().unique(),
+ nameKo: varchar("name_ko", { length: 255 }).notNull(),
+ nameEn: varchar("name_en", { length: 255 }).notNull(),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+});
+
export const vendors = pgTable("vendors", {
id: serial("id").primaryKey(),
@@ -32,7 +45,7 @@ export const vendors = pgTable("vendors", {
})
.notNull()
.default("PENDING_REVIEW"),
-
+ vendorTypeId: integer("vendor_type_id").references(() => vendorTypes.id),
representativeName: varchar("representative_name", { length: 255 }),
representativeBirth: varchar("representative_birth", { length: 20 }),
representativeEmail: varchar("representative_email", { length: 255 }),
@@ -40,6 +53,7 @@ export const vendors = pgTable("vendors", {
corporateRegistrationNumber: varchar("corporate_registration_number", {
length: 100,
}),
+ items: text("items"),
creditAgency: varchar("credit_agency", { length: 50 }),
creditRating: varchar("credit_rating", { length: 50 }),
@@ -255,8 +269,12 @@ export const vendorInvestigationsView = pgView(
export const vendorCandidates = pgTable("vendor_candidates", {
id: serial("id").primaryKey(),
companyName: varchar("company_name", { length: 255 }).notNull(),
- contactEmail: varchar("contact_email", { length: 255 }).notNull(),
+ contactEmail: varchar("contact_email", { length: 255 }),
contactPhone: varchar("contact_phone", { length: 50 }),
+
+ taxId: varchar("tax_id", { length: 100 }).notNull(),
+ address: text("address"),
+
country: varchar("country", { length: 100 }),
// 웹 크롤링 등으로 얻은 상태나 분류
source: varchar("source", { length: 100 }), // 수집 출처
@@ -270,7 +288,10 @@ export const vendorCandidates = pgTable("vendor_candidates", {
})
.notNull()
.default("COLLECTED"),
-
+ remark: text("remark"),
+ items: text("items").notNull(),
+ vendorId: integer("vendor_id")
+ .references(() => vendors.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
@@ -281,7 +302,7 @@ export type VendorCandidates = typeof vendorCandidates.$inferSelect;
export const vendorDetailView = pgView("vendor_detail_view").as((qb) => {
return qb
.select({
- // 벤더 기본 정보
+ // 협력업체 기본 정보
id: vendors.id,
vendorName: vendors.vendorName,
vendorCode: vendors.vendorCode,
@@ -359,4 +380,150 @@ export const vendorDetailView = pgView("vendor_detail_view").as((qb) => {
});
// 타입 정의
-export type VendorDetailView = typeof vendorDetailView.$inferSelect; \ No newline at end of file
+export type VendorDetailView = typeof vendorDetailView.$inferSelect;
+
+
+// 관계 설정 (DrizzleORM을 사용한다고 가정)
+export const vendorRelations = relations(vendors, ({ one }) => ({
+ vendorType: one(vendorTypes, {
+ fields: [vendors.vendorTypeId],
+ references: [vendorTypes.id],
+ }),
+}));
+
+export const vendorTypeRelations = relations(vendorTypes, ({ many }) => ({
+ vendors: many(vendors),
+}));
+
+export type VendorTypes = typeof vendorTypes.$inferSelect;
+
+
+
+
+export const vendorCandidatesWithVendorInfo = pgView("vendor_candidates_with_vendor_info").as((qb) => {
+ return qb
+ .select({
+ // ----------------------------------------
+ // 1) vendorCandidates 기본 필드
+ id: vendorCandidates.id,
+ companyName: vendorCandidates.companyName,
+ contactEmail: vendorCandidates.contactEmail,
+ contactPhone: vendorCandidates.contactPhone,
+ taxId: vendorCandidates.taxId,
+ address: vendorCandidates.address,
+ country: vendorCandidates.country,
+ source: vendorCandidates.source,
+ status: vendorCandidates.status,
+ items: vendorCandidates.items,
+ remark: vendorCandidates.remark,
+ // remark, items, vendorId 등 필요한 필드도 추가
+ createdAt: vendorCandidates.createdAt,
+ updatedAt: vendorCandidates.updatedAt,
+
+ // ----------------------------------------
+ // 2) vendors 조인해서 가져올 필드
+ vendorName: vendors.vendorName,
+ vendorCode: vendors.vendorCode,
+ vendorCreatedAt: sql<Date>`${vendors.createdAt}`.as("vendor_created_at"),
+
+ // ----------------------------------------
+ // 3) 마지막 상태 변경 시각, 변경자 (action = 'status_change')
+ lastStatusChangeAt: sql<Date>`(
+ SELECT l2."created_at"
+ FROM "vendor_candidate_logs" l2
+ WHERE l2."vendor_candidate_id" = "vendor_candidates"."id"
+ AND l2."action" = 'status_change'
+ ORDER BY l2."created_at" DESC
+ LIMIT 1
+ )`.as("last_status_change_at"),
+
+ lastStatusChangeBy: sql<string>`(
+ SELECT u."name"
+ FROM "users" u
+ JOIN "vendor_candidate_logs" l3
+ ON l3."user_id" = u."id"
+ WHERE l3."vendor_candidate_id" = "vendor_candidates"."id"
+ AND l3."action" = 'status_change'
+ ORDER BY l3."created_at" DESC
+ LIMIT 1
+ )`.as("last_status_change_by"),
+
+ // ----------------------------------------
+ // 4) 마지막 초청(Invite) 시각, 초청자 (action = 'invite_sent')
+ lastInvitationAt: sql<Date>`(
+ SELECT l4."created_at"
+ FROM "vendor_candidate_logs" l4
+ WHERE l4."vendor_candidate_id" = "vendor_candidates"."id"
+ AND l4."action" = 'invite_sent'
+ ORDER BY l4."created_at" DESC
+ LIMIT 1
+ )`.as("last_invitation_at"),
+
+ lastInvitationBy: sql<string>`(
+ SELECT u2."name"
+ FROM "users" u2
+ JOIN "vendor_candidate_logs" l5
+ ON l5."user_id" = u2."id"
+ WHERE l5."vendor_candidate_id" = "vendor_candidates"."id"
+ AND l5."action" = 'invite_sent'
+ ORDER BY l5."created_at" DESC
+ LIMIT 1
+ )`.as("last_invitation_by"),
+ })
+ .from(vendorCandidates)
+ .leftJoin(vendors, eq(vendorCandidates.vendorId, vendors.id));
+});
+
+export type VendorCandidatesWithVendorInfo = typeof vendorCandidatesWithVendorInfo.$inferSelect;
+
+
+export const vendorsWithTypesView = pgView("vendors_with_types").as((qb) => {
+ return qb
+ .select({
+ // Vendor fields
+ id: vendors.id,
+ vendorName: vendors.vendorName,
+ vendorCode: vendors.vendorCode,
+ taxId: vendors.taxId,
+ address: vendors.address,
+ country: vendors.country,
+ phone: vendors.phone,
+ email: vendors.email,
+ website: vendors.website,
+ status: vendors.status,
+ vendorTypeId: vendors.vendorTypeId,
+ representativeName: vendors.representativeName,
+ representativeBirth: vendors.representativeBirth,
+ representativeEmail: vendors.representativeEmail,
+ representativePhone: vendors.representativePhone,
+ corporateRegistrationNumber: vendors.corporateRegistrationNumber,
+ items: vendors.items,
+ creditAgency: vendors.creditAgency,
+ creditRating: vendors.creditRating,
+ cashFlowRating: vendors.cashFlowRating,
+ createdAt: vendors.createdAt,
+ updatedAt: vendors.updatedAt,
+
+ // Vendor type fields
+ vendorTypeName: vendorTypes.nameKo,
+ vendorTypeNameEn: vendorTypes.nameEn,
+ vendorTypeCode: vendorTypes.code,
+
+ // Computed vendor category field
+ vendorCategory: sql`
+ CASE
+ WHEN ${vendors.status} = 'ACTIVE' THEN '정규업체'
+ WHEN ${vendors.status} IN ('INACTIVE', 'BLACKLISTED', 'REJECTED') THEN ''
+ ELSE '잠재업체'
+ END
+ `.as("vendor_category")
+ })
+ .from(vendors)
+ .leftJoin(
+ vendorTypes,
+ sql`${vendors.vendorTypeId} = ${vendorTypes.id}`
+ );
+});
+
+// You can also create interfaces for the view
+export type VendorWithType = typeof vendorsWithTypesView.$inferSelect; \ No newline at end of file