summaryrefslogtreecommitdiff
path: root/db/schema
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-04-02 09:54:08 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-04-02 09:54:08 +0000
commitdfdfae3018f8499240f48d28ce634f4a5c56e006 (patch)
tree4493b172c061fa5bf4e94c083788110eb1507f6d /db/schema
parent21a72eeddc74cf775e2a76e2c569de970bd62a7f (diff)
벤더 코멘트 처리
Diffstat (limited to 'db/schema')
-rw-r--r--db/schema/index.ts12
-rw-r--r--db/schema/pq.ts103
-rw-r--r--db/schema/vendorData.ts2
-rw-r--r--db/schema/vendorDocu.ts161
-rw-r--r--db/schema/vendors.ts250
5 files changed, 420 insertions, 108 deletions
diff --git a/db/schema/index.ts b/db/schema/index.ts
new file mode 100644
index 00000000..714f058b
--- /dev/null
+++ b/db/schema/index.ts
@@ -0,0 +1,12 @@
+export * from './companies';
+export * from './contract';
+export * from './items';
+export * from './pq';
+export * from './projects';
+export * from './rfq';
+export * from './users';
+
+export * from './vendorData';
+export * from './vendorDocu';
+export * from './vendors';
+export * from './tasks';
diff --git a/db/schema/pq.ts b/db/schema/pq.ts
index 59ec8f07..76d15d5d 100644
--- a/db/schema/pq.ts
+++ b/db/schema/pq.ts
@@ -3,6 +3,7 @@ import {
timestamp, uniqueIndex
} from "drizzle-orm/pg-core";
import { vendors } from "./vendors";
+import { projects } from "./projects";
export const pqCriterias = pgTable("pq_criterias", {
id: serial("id").primaryKey(),
@@ -17,21 +18,34 @@ export const pqCriterias = pgTable("pq_criterias", {
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
-export const vendorPqCriteriaAnswers = pgTable("vendor_pq_criteria_answers", {
+export const pqCriteriasExtension = pgTable("pq_criterias_extension", {
id: serial("id").primaryKey(),
- vendorId: integer("vendor_id")
- .notNull()
- .references(() => vendors.id, { onDelete: "cascade" }),
-
- criteriaId: integer("criteria_id")
- .notNull()
- .references(() => pqCriterias.id, { onDelete: "cascade" }),
-
- answer: text("answer"),
-
+
+ // pq_criterias와 연결 (1:1 or 1:N 중 필요에 맞춰 사용)
+ pqCriteriaId: integer("pq_criteria_id")
+ .notNull()
+ .references(() => pqCriterias.id, {
+ onDelete: "cascade",
+ onUpdate: "cascade",
+ }),
+
+ // projects 테이블에 대한 FK
+ projectId: integer("project_id")
+ .notNull()
+ .references(() => projects.id, {
+ onDelete: "cascade",
+ onUpdate: "cascade",
+ }),
+
+ // 프로젝트별 PQ 시 필요한 추가 정보
+ contractInfo: text("contract_info"),
+ additionalRequirement: text("additional_requirement"),
+
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
-});
+ });
+
+
export const vendorCriteriaAttachments = pgTable("vendor_criteria_attachments", {
id: serial("id").primaryKey(),
@@ -65,3 +79,68 @@ export const vendorPqReviewLogs = pgTable("vendor_pq_review_logs", {
createdAt: timestamp("created_at").defaultNow().notNull(),
})
export type PqCriterias = typeof pqCriterias.$inferSelect
+
+
+// 벤더와 프로젝트 PQ 요청 연결 테이블
+export const vendorProjectPQs = pgTable("vendor_project_pqs", {
+ id: serial("id").primaryKey(),
+
+ // vendors 테이블 FK
+ vendorId: integer("vendor_id")
+ .notNull()
+ .references(() => vendors.id, {
+ onDelete: "cascade",
+ onUpdate: "cascade",
+ }),
+
+ // projects 테이블 FK
+ projectId: integer("project_id")
+ .notNull()
+ .references(() => projects.id, {
+ onDelete: "cascade",
+ onUpdate: "cascade",
+ }),
+
+ // 상태: REQUESTED(요청됨), IN_PROGRESS(진행중), SUBMITTED(제출됨), APPROVED(승인됨), REJECTED(거부됨)
+ status: varchar("status", { length: 20 }).notNull().default("REQUESTED"),
+
+ // 메타데이터
+ submittedAt: timestamp("submitted_at"),
+ approvedAt: timestamp("approved_at"),
+ rejectedAt: timestamp("rejected_at"),
+ rejectReason: text("reject_reason"),
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+});
+
+// 기존 vendorPqCriteriaAnswers 테이블에 projectId 필드 추가
+export const vendorPqCriteriaAnswers = pgTable("vendor_pq_criteria_answers", {
+ id: serial("id").primaryKey(),
+
+ vendorId: integer("vendor_id")
+ .notNull()
+ .references(() => vendors.id, {
+ onDelete: "cascade",
+ onUpdate: "cascade",
+ }),
+
+ criteriaId: integer("criteria_id")
+ .notNull()
+ .references(() => pqCriterias.id, {
+ onDelete: "cascade",
+ onUpdate: "cascade",
+ }),
+
+ // 추가: 프로젝트 ID (null은 일반 PQ를 의미)
+ projectId: integer("project_id")
+ .references(() => projects.id, {
+ onDelete: "cascade",
+ onUpdate: "cascade",
+ }),
+
+ answer: text("answer"),
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+});
diff --git a/db/schema/vendorData.ts b/db/schema/vendorData.ts
index 92a92c8e..03248fb8 100644
--- a/db/schema/vendorData.ts
+++ b/db/schema/vendorData.ts
@@ -28,7 +28,7 @@ export const forms = pgTable("forms", {
}
})
-export const rfqAttachments = pgTable("form_templates", {
+export const formTemplates = pgTable("form_templates", {
id: serial("id").primaryKey(),
formId: integer("form_id").references(() => forms.id),
fileName: varchar("file_name", { length: 255 }).notNull(),
diff --git a/db/schema/vendorDocu.ts b/db/schema/vendorDocu.ts
index 2e6ba5a1..ce498b94 100644
--- a/db/schema/vendorDocu.ts
+++ b/db/schema/vendorDocu.ts
@@ -1,5 +1,5 @@
-import { pgTable, integer, varchar, timestamp, date ,pgView,uniqueIndex ,jsonb} from "drizzle-orm/pg-core"
-import { eq , sql} from "drizzle-orm";
+import { pgTable, integer, varchar, timestamp, date, pgView, uniqueIndex, jsonb } from "drizzle-orm/pg-core"
+import { eq, sql } from "drizzle-orm";
import { projects } from "./projects";
import { vendors } from "./vendors";
import { contracts } from "./contract";
@@ -76,94 +76,75 @@ export const issueStages = pgTable(
}
);
- export const revisions = pgTable(
- "revisions",
- {
- id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
- issueStageId: integer("issue_stage_id").notNull(),
- revision: varchar("revision", { length: 50 }).notNull(),
- // 새로운 필드: 업로더 타입 (업체 또는 고객사)
- uploaderType: varchar("uploader_type", { length: 20 }).notNull().default("vendor"),
- // 선택적: 업로더 ID 또는 이름
- uploaderId: integer("uploader_id"),
- uploaderName: varchar("uploader_name", { length: 100 }),
- // 선택적: 추가 메타데이터
- comment: varchar("comment", { length: 500 }),
- status: varchar("status", { length: 50 }),
- approvedDate: date("approved_date"),
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
- },
- (table) => {
- return {
- // "issue_stage_id + revision" 조합을 유니크로 묶음 (유지)
- uniqueStageRev: uniqueIndex("unique_stage_rev").on(
- table.issueStageId,
- table.revision
- ),
- }
- }
- )
- export const documentAttachments = pgTable(
- "document_attachments",
- {
- id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
- revisionId: integer("revision_id")
- .notNull()
- .references(() => revisions.id, { onDelete: "cascade" }),
- fileName: varchar("file_name", { length: 255 }).notNull(),
- filePath: varchar("file_path", { length: 1024 }).notNull(),
- fileType: varchar("file_type", { length: 50 }),
- fileSize: integer("file_size"),
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
+export const revisions = pgTable(
+ "revisions",
+ {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ issueStageId: integer("issue_stage_id").notNull(),
+ revision: varchar("revision", { length: 50 }).notNull(),
+ // 새로운 필드: 업로더 타입 (업체 또는 고객사)
+ uploaderType: varchar("uploader_type", { length: 20 }).notNull().default("vendor"),
+ // 선택적: 업로더 ID 또는 이름
+ uploaderId: integer("uploader_id"),
+ uploaderName: varchar("uploader_name", { length: 100 }),
+ // 선택적: 추가 메타데이터
+ comment: varchar("comment", { length: 500 }),
+ status: varchar("status", { length: 50 }),
+ approvedDate: date("approved_date"),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+ },
+ (table) => {
+ return {
+ // "issue_stage_id + revision" 조합을 유니크로 묶음 (유지)
+ uniqueStageRev: uniqueIndex("unique_stage_rev").on(
+ table.issueStageId,
+ table.revision
+ ),
}
- )
-
+ }
+)
+export const documentAttachments = pgTable(
+ "document_attachments",
+ {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ revisionId: integer("revision_id")
+ .notNull()
+ .references(() => revisions.id, { onDelete: "cascade" }),
+ fileName: varchar("file_name", { length: 255 }).notNull(),
+ filePath: varchar("file_path", { length: 1024 }).notNull(),
+ fileType: varchar("file_type", { length: 50 }),
+ fileSize: integer("file_size"),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+ }
+)
- // export const vendorDocumentsView = pgTable("vendor_documents", {
- // vendorId: integer("vendor_id").notNull(),
- // id: integer("id").notNull(),
- // docNumber: varchar("doc_number", { length: 100 }).notNull(),
- // title: varchar("title", { length: 255 }).notNull(),
-
- // // 새로 추가된 컬럼들 (contractId, contractNo, contractName, status)
- // contractId: integer("contract_id").notNull(),
- // contractNo: varchar("contract_no", { length: 100 }).notNull(),
- // contractName: varchar("contract_name", { length: 255 }).notNull(),
- // status: varchar("status", { length: 50 }).notNull(),
-
- // createdAt: timestamp("created_at", { withTimezone: true }),
- // updatedAt: timestamp("updated_at", { withTimezone: true }),
-
- // })
+export const vendorDocumentsView = pgView("vendor_documents_view", {
+ // Match the columns in your SELECT statement
+ id: integer("id").notNull(),
+ docNumber: varchar("doc_number", { length: 100 }).notNull(),
+ title: varchar("title", { length: 255 }).notNull(),
+ status: varchar("status", { length: 50 }).notNull(),
+ issuedDate: date("issued_date"),
+ contractId: integer("contract_id").notNull(),
- export const vendorDocumentsView = pgView("vendor_documents_view", {
- // Match the columns in your SELECT statement
- id: integer("id").notNull(),
- docNumber: varchar("doc_number", { length: 100 }).notNull(),
- title: varchar("title", { length: 255 }).notNull(),
- status: varchar("status", { length: 50 }).notNull(),
- issuedDate: date("issued_date"),
-
- contractId: integer("contract_id").notNull(),
-
- latestStageId: integer("latest_stage_id"), // possibly can be null
- latestStageName: varchar("latest_stage_name", { length: 100 }),
- latestStagePlanDate: date("latest_stage_plan_date"),
- latestStageActualDate: date("latest_stage_actual_date"),
-
- latestRevisionId: integer("latest_revision_id"),
- latestRevision: varchar("latest_revision", { length: 50 }),
- latestRevisionUploaderType: varchar("latest_revision_uploader_type", { length: 20 }),
- latestRevisionUploaderName: varchar("latest_revision_uploader_name", { length: 100 }),
-
- attachmentCount: integer("attachment_count"),
-
- createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
- }).as(sql`
+ latestStageId: integer("latest_stage_id"), // possibly can be null
+ latestStageName: varchar("latest_stage_name", { length: 100 }),
+ latestStagePlanDate: date("latest_stage_plan_date"),
+ latestStageActualDate: date("latest_stage_actual_date"),
+
+ latestRevisionId: integer("latest_revision_id"),
+ latestRevision: varchar("latest_revision", { length: 50 }),
+ latestRevisionUploaderType: varchar("latest_revision_uploader_type", { length: 20 }),
+ latestRevisionUploaderName: varchar("latest_revision_uploader_name", { length: 100 }),
+
+ attachmentCount: integer("attachment_count"),
+
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
+}).as(sql`
SELECT
d.id,
d.doc_number,
@@ -232,8 +213,6 @@ export const issueStages = pgTable(
JOIN contracts c ON d.contract_id = c.id
`);
-
-
// 문서 + 스테이지 리스트 뷰
export const documentStagesView = pgView("document_stages_view", {
documentId: integer("document_id").notNull(),
@@ -279,7 +258,5 @@ export const documentStagesView = pgView("document_stages_view", {
FROM documents d
`);
-
- export type VendorDocumentsView = typeof vendorDocumentsView.$inferSelect
- export type DocumentStagesView = typeof documentStagesView.$inferSelect
- \ No newline at end of file
+export type VendorDocumentsView = typeof vendorDocumentsView.$inferSelect
+export type DocumentStagesView = typeof documentStagesView.$inferSelect
diff --git a/db/schema/vendors.ts b/db/schema/vendors.ts
index b2005537..acdec3d2 100644
--- a/db/schema/vendors.ts
+++ b/db/schema/vendors.ts
@@ -1,7 +1,7 @@
// db/schema/vendors.ts
import { pgTable, serial, varchar, text, timestamp, boolean, integer ,pgView} from "drizzle-orm/pg-core";
import { items } from "./items";
-import { eq} from "drizzle-orm";
+import { sql, eq} from "drizzle-orm";
export const vendors = pgTable("vendors", {
id: serial("id").primaryKey(),
@@ -20,9 +20,11 @@ export const vendors = pgTable("vendors", {
"IN_REVIEW", // 심사 중
"REJECTED", // 심사 거부됨
"IN_PQ", // PQ 진행 중
- "PQ_SUBMITTED", // PQ 제출
+ "PQ_SUBMITTED", // PQ 제출
"PQ_FAILED", // PQ 실패
+ "PQ_APPROVED", // PQ 통과, 승인됨
"APPROVED", // PQ 통과, 승인됨
+ "READY_TO_SEND", // PQ 통과, 승인됨
"ACTIVE", // 활성 상태 (실제 거래 중)
"INACTIVE", // 비활성 상태 (일시적)
"BLACKLISTED", // 거래 금지 상태
@@ -115,4 +117,246 @@ export type VendorWithAttachments = Vendor & {
attachmentsList?: VendorAttach[];
}
-export type VendorItemsView = typeof vendorItemsView.$inferSelect \ No newline at end of file
+export type VendorItemsView = typeof vendorItemsView.$inferSelect
+
+export const vendorInvestigations = pgTable("vendor_investigations", {
+ id: serial("id").primaryKey(),
+
+ // 어떤 벤더에 대한 실사인지 참조
+ vendorId: integer("vendor_id").notNull().references(() => vendors.id),
+
+ // 실사 상태
+ // 예: "PLANNED" | "IN_PROGRESS" | "COMPLETED" | "CANCELED"
+ investigationStatus: varchar("investigation_status", {
+ length: 50,
+ enum: [
+ "PLANNED",
+ "IN_PROGRESS",
+ "COMPLETED",
+ "CANCELED",
+ ],
+ })
+ .notNull()
+ .default("PLANNED"),
+
+ // 실사 일정 시작일 / 종료일
+ scheduledStartAt: timestamp("scheduled_start_at"),
+ scheduledEndAt: timestamp("scheduled_end_at"),
+
+ // 실제 실사 완료일
+ completedAt: timestamp("completed_at"),
+
+ // 실사 내용이나 특이사항
+ investigationNotes: text("investigation_notes"),
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+});
+
+// 타입 정의
+export type VendorInvestigation = typeof vendorInvestigations.$inferSelect;
+
+export const vendorInvestigationAttachments = pgTable(
+ "vendor_investigation_attachments",
+ {
+ id: serial("id").primaryKey(),
+ // 어떤 실사 (investigation)에 대한 첨부파일인지
+ investigationId: integer("investigation_id")
+ .notNull()
+ .references(() => vendorInvestigations.id, { onDelete: "cascade" }),
+
+ fileName: varchar("file_name", { length: 255 }).notNull(),
+ filePath: varchar("file_path", { length: 1024 }).notNull(),
+
+ // 첨부파일 종류 (예: 보고서, 사진, 기타 등 구분)
+ attachmentType: varchar("attachment_type", { length: 50 }).default("REPORT"),
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+ }
+);
+
+// 첨부파일 테이블 타입
+export type VendorInvestigationAttachment =
+ typeof vendorInvestigationAttachments.$inferSelect;
+
+/**
+ * A view that joins vendor_investigations + vendors,
+ * and also embeds contacts & possibleItems as JSON arrays.
+ */
+export const vendorInvestigationsView = pgView(
+ "vendor_investigations_view"
+).as((qb) => {
+ return qb
+ .select({
+ // Investigation fields
+ investigationId: vendorInvestigations.id,
+ investigationStatus: vendorInvestigations.investigationStatus,
+ scheduledStartAt: vendorInvestigations.scheduledStartAt,
+ scheduledEndAt: vendorInvestigations.scheduledEndAt,
+ completedAt: vendorInvestigations.completedAt,
+ investigationNotes: vendorInvestigations.investigationNotes,
+ investigationCreatedAt: vendorInvestigations.createdAt,
+ investigationUpdatedAt: vendorInvestigations.updatedAt,
+
+ // Vendor fields
+ vendorId: vendorInvestigations.vendorId,
+ vendorName: vendors.vendorName,
+ vendorCode: vendors.vendorCode,
+ vendorTaxId: vendors.taxId,
+ vendorStatus: vendors.status,
+ vendorCountry: vendors.country,
+ vendorEmail: vendors.email,
+ vendorPhone: vendors.phone,
+ vendorWebsite: vendors.website,
+
+ // JSON-aggregated contacts
+ contacts: sql<string>`(
+ SELECT COALESCE(
+ json_agg(json_build_object(
+ 'contactName', c.contact_name,
+ 'contactEmail', c.contact_email,
+ 'contactPhone', c.contact_phone,
+ 'contactPosition', c.contact_position,
+ 'isPrimary', c.is_primary,
+ 'contactCreatedAt', c.created_at,
+ 'contactUpdatedAt', c.updated_at
+ )),
+ '[]'::json
+ )
+ FROM vendor_contacts c
+ WHERE c.vendor_id = ${vendors.id}
+ )`.as("contacts"),
+
+ // JSON-aggregated possible items with itemName from items table
+ possibleItems: sql<string>`(
+ SELECT COALESCE(
+ json_agg(json_build_object(
+ 'itemCode', pi.item_code,
+ 'itemName', i.item_name,
+ 'itemCreatedAt', pi.created_at,
+ 'itemUpdatedAt', pi.updated_at
+ )),
+ '[]'::json
+ )
+ FROM vendor_possible_items pi
+ LEFT JOIN items i ON pi.item_code = i.item_code
+ WHERE pi.vendor_id = ${vendors.id}
+ )`.as("possibleItems"),
+ })
+ .from(vendorInvestigations)
+ .leftJoin(
+ vendors,
+ eq(vendorInvestigations.vendorId, vendors.id)
+ )
+})
+
+
+export const vendorCandidates = pgTable("vendor_candidates", {
+ id: serial("id").primaryKey(),
+ companyName: varchar("company_name", { length: 255 }).notNull(),
+ contactEmail: varchar("contact_email", { length: 255 }).notNull(),
+ contactPhone: varchar("contact_phone", { length: 50 }),
+ country: varchar("country", { length: 100 }),
+ // 웹 크롤링 등으로 얻은 상태나 분류
+ source: varchar("source", { length: 100 }), // 수집 출처
+ status: varchar("status", {
+ length: 30,
+ enum: [
+ "COLLECTED", // 단순 데이터 수집 완료
+ "INVITED", // 초청 메일 발송
+ "DISCARDED", // 불필요, 검토 후 배제됨
+ ],
+ })
+ .notNull()
+ .default("COLLECTED"),
+
+ 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,
+ 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<string>`
+ (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<string>`
+ (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<number>`
+ (SELECT COUNT(*)
+ FROM vendor_attachments a
+ WHERE a.vendor_id = vendors.id)
+ `.as("attachment_count"),
+
+ // 연락처 수 (수정된 버전)
+ contactCount: sql<number>`
+ (SELECT COUNT(*)
+ FROM vendor_contacts c
+ WHERE c.vendor_id = vendors.id)
+ `.as("contact_count")
+ })
+ .from(vendors)
+});
+
+// 타입 정의
+export type VendorDetailView = typeof vendorDetailView.$inferSelect; \ No newline at end of file