diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-28 02:13:30 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-28 02:13:30 +0000 |
| commit | ef4c533ebacc2cdc97e518f30e9a9350004fcdfb (patch) | |
| tree | 345251a3ed0f4429716fa5edaa31024d8f4cb560 /db/schema | |
| parent | 9ceed79cf32c896f8a998399bf1b296506b2cd4a (diff) | |
~20250428 작업사항
Diffstat (limited to 'db/schema')
| -rw-r--r-- | db/schema/basicContractDocumnet.ts | 75 | ||||
| -rw-r--r-- | db/schema/contract.ts | 5 | ||||
| -rw-r--r-- | db/schema/index.ts | 2 | ||||
| -rw-r--r-- | db/schema/logs.ts | 61 | ||||
| -rw-r--r-- | db/schema/pq.ts | 46 | ||||
| -rw-r--r-- | db/schema/projects.ts | 54 | ||||
| -rw-r--r-- | db/schema/rfq.ts | 253 | ||||
| -rw-r--r-- | db/schema/vendorData.ts | 52 | ||||
| -rw-r--r-- | db/schema/vendors.ts | 181 |
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 |
