// db/schema/vendors.ts import { pgTable, serial, varchar, text, timestamp, boolean, integer ,pgView} from "drizzle-orm/pg-core"; import { items, materials } from "./items"; 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(), vendorName: varchar("vendor_name", { length: 255 }).notNull(), // 벤더 코드 유니크 아니어도 괜찮은지? vendorCode: varchar("vendor_code", { length: 100 }), taxId: varchar("tax_id", { length: 100 }).notNull(), address: text("address"), country: varchar("country", { length: 100 }), phone: varchar("phone", { length: 50 }), email: varchar("email", { length: 255 }), website: varchar("website", { length: 255 }), status: varchar("status", { length: 30, enum: [ "PENDING_REVIEW", // 가입 신청 중 (초기 신청) "IN_REVIEW", // 심사 중 "REJECTED", // 심사 거부됨 "IN_PQ", // PQ 진행 중 "PQ_SUBMITTED", // PQ 제출 "PQ_FAILED", // PQ 실패 "PQ_APPROVED", // PQ 통과, 승인됨 "APPROVED", // PQ 통과, 승인됨 "READY_TO_SEND", // PQ 통과, 승인됨 "ACTIVE", // 활성 상태 (실제 거래 중) "INACTIVE", // 비활성 상태 (일시적) "BLACKLISTED", // 거래 금지 상태 ] }) .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 }), representativePhone: varchar("representative_phone", { length: 50 }), corporateRegistrationNumber: varchar("corporate_registration_number", { length: 100, }), items: text("items"), creditAgency: varchar("credit_agency", { length: 50 }), creditRating: varchar("credit_rating", { length: 50 }), cashFlowRating: varchar("cash_flow_rating", { length: 50 }), businessSize: varchar("business_size", { length: 255 }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), // VENDOR_GENERAL 테이블은 별도 테이블로 분리함 }); // eVCP 내 연락처로 오라클 측과는 무관... export const vendorContacts = pgTable("vendor_contacts", { id: serial("id").primaryKey(), vendorId: integer("vendor_id").notNull().references(() => vendors.id), contactName: varchar("contact_name", { length: 255 }).notNull(), contactPosition: varchar("contact_position", { length: 100 }), contactEmail: varchar("contact_email", { length: 255 }).notNull(), contactPhone: varchar("contact_phone", { length: 50 }), isPrimary: boolean("is_primary").default(false).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); export const vendorPossibleItems = pgTable("vendor_possible_items", { id: serial("id").primaryKey(), vendorId: integer("vendor_id").notNull().references(() => vendors.id), // itemId: integer("item_id"), // 별도 item 테이블 연동시 itemCode: varchar("item_code", { length: 100 }) .notNull() .references(() => items.itemCode, { onDelete: "cascade" }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); export const vendorPossibleMateirals = pgTable("vendor_possible_materials", { id: serial("id").primaryKey(), vendorId: integer("vendor_id").notNull().references(() => vendors.id), // itemId: integer("item_id"), // 별도 item 테이블 연동시 itemCode: varchar("item_code", { length: 100 }) .notNull() .references(() => materials.itemCode, { onDelete: "cascade" }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); export const vendorItemsView = pgView("vendor_items_view").as((qb) => { return qb .select({ // vendorPossibleItems의 "id" -> "vendorItemId" vendorItemId: vendorPossibleItems.id, vendorId: vendorPossibleItems.vendorId, // items의 "id" -> "itemId" // itemId: items.id, itemName: items.itemName, itemCode: items.itemCode, description: items.description, createdAt: vendorPossibleItems.createdAt, updatedAt: vendorPossibleItems.updatedAt, }) .from(vendorPossibleItems) .leftJoin(items, eq(vendorPossibleItems.itemCode, items.itemCode)) }) export const vendorMaterialsView = pgView("vendor_materials_view").as((qb) => { return qb .select({ // vendorPossibleItems의 "id" -> "vendorItemId" vendorItemId: vendorPossibleMateirals.id, vendorId: vendorPossibleMateirals.vendorId, // items의 "id" -> "itemId" // itemId: items.id, itemName: materials.itemName, itemCode: materials.itemCode, description: materials.description, unitOfMeasure: materials.unitOfMeasure, steelType: materials.steelType, gradeMaterial: materials.gradeMaterial, createdAt: vendorPossibleMateirals.createdAt, updatedAt: vendorPossibleMateirals.updatedAt, }) .from(vendorPossibleMateirals) .leftJoin(materials, eq(vendorPossibleMateirals.itemCode, materials.itemCode)) }) export const vendorAttachments = pgTable("vendor_attachments", { id: serial("id").primaryKey(), vendorId: integer("vendor_id").references(() => vendors.id), fileName: varchar("file_name", { length: 255 }).notNull(), filePath: varchar("file_path", { length: 1024 }).notNull(), attachmentType: varchar("attachment_type", { length: 50, }).default("GENERAL"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); export type Vendor = typeof vendors.$inferSelect export type VendorContact = typeof vendorContacts.$inferSelect export type VendorItem = typeof vendorPossibleItems.$inferSelect export type VendorAttach = typeof vendorAttachments.$inferSelect export type VendorWithAttachments = Vendor & { hasAttachments?: boolean; attachmentsList?: VendorAttach[]; } export type VendorItemsView = typeof vendorItemsView.$inferSelect export type VendorMaterialsView = typeof vendorMaterialsView.$inferSelect export const vendorCandidates = pgTable("vendor_candidates", { id: serial("id").primaryKey(), companyName: varchar("company_name", { 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 }), // 수집 출처 status: varchar("status", { length: 30, enum: [ "COLLECTED", // 단순 데이터 수집 완료 "INVITED", // 초청 메일 발송 "DISCARDED", // 불필요, 검토 후 배제됨 ], }) .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(), }); 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, taxId: vendors.taxId, address: vendors.address, businessSize:vendors.businessSize, country: vendors.country, phone: vendors.phone, email: vendors.email, website: vendors.website, status: vendors.status, representativeName: vendors.representativeName, representativeBirth: vendors.representativeBirth, representativeEmail: vendors.representativeEmail, representativePhone: vendors.representativePhone, corporateRegistrationNumber: vendors.corporateRegistrationNumber, creditAgency: vendors.creditAgency, creditRating: vendors.creditRating, cashFlowRating: vendors.cashFlowRating, createdAt: vendors.createdAt, updatedAt: vendors.updatedAt, // 연락처 정보 (수정된 버전) contacts: sql` (SELECT COALESCE( json_agg( json_build_object( 'id', c.id, 'contactName', c.contact_name, 'contactPosition', c.contact_position, 'contactEmail', c.contact_email, 'contactPhone', c.contact_phone, 'isPrimary', c.is_primary ) ), '[]'::json ) FROM vendor_contacts c WHERE c.vendor_id = vendors.id) `.as("contacts"), // 첨부파일 정보 (수정된 버전) attachments: sql` (SELECT COALESCE( json_agg( json_build_object( 'id', a.id, 'fileName', a.file_name, 'filePath', a.file_path, 'attachmentType', a.attachment_type, 'createdAt', a.created_at ) ORDER BY a.attachment_type, a.created_at DESC ), '[]'::json ) FROM vendor_attachments a WHERE a.vendor_id = vendors.id) `.as("attachments"), // 첨부파일 수 (수정된 버전) attachmentCount: sql` (SELECT COUNT(*) FROM vendor_attachments a WHERE a.vendor_id = vendors.id) `.as("attachment_count"), // 연락처 수 (수정된 버전) contactCount: sql` (SELECT COUNT(*) FROM vendor_contacts c WHERE c.vendor_id = vendors.id) `.as("contact_count") }) .from(vendors) }); // 타입 정의 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`${vendors.createdAt}`.as("vendor_created_at"), // ---------------------------------------- // 3) 마지막 상태 변경 시각, 변경자 (action = 'status_change') lastStatusChangeAt: sql`( 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`( 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`( 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`( 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: sql`${vendors.id}`.as("id"), vendorName: sql`${vendors.vendorName}`.as("vendor_name"), vendorCode: sql`${vendors.vendorCode}`.as("vendor_code"), taxId: sql`${vendors.taxId}`.as("tax_id"), address: sql`${vendors.address}`.as("address"), country: sql`${vendors.country}`.as("country"), phone: sql`${vendors.phone}`.as("phone"), email: sql`${vendors.email}`.as("email"), businessSize: sql`${vendors.businessSize}`.as("business_size"), website: sql`${vendors.website}`.as("website"), status: sql`${vendors.status}`.as("status"), vendorTypeId: sql`${vendors.vendorTypeId}`.as("vendor_type_id"), representativeName: sql`${vendors.representativeName}`.as("representative_name"), representativeBirth: sql`${vendors.representativeBirth}`.as("representative_birth"), representativeEmail: sql`${vendors.representativeEmail}`.as("representative_email"), representativePhone: sql`${vendors.representativePhone}`.as("representative_phone"), corporateRegistrationNumber: sql`${vendors.corporateRegistrationNumber}`.as("corporate_registration_number"), items: sql`${vendors.items}`.as("items"), creditAgency: sql`${vendors.creditAgency}`.as("credit_agency"), creditRating: sql`${vendors.creditRating}`.as("credit_rating"), cashFlowRating: sql`${vendors.cashFlowRating}`.as("cash_flow_rating"), createdAt: sql`${vendors.createdAt}`.as("created_at"), updatedAt: sql`${vendors.updatedAt}`.as("updated_at"), // Vendor type fields vendorTypeName: sql`${vendorTypes.nameKo}`.as("vendor_type_name"), vendorTypeNameEn: sql`${vendorTypes.nameEn}`.as("vendor_type_name_en"), vendorTypeCode: sql`${vendorTypes.code}`.as("vendor_type_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;