diff options
Diffstat (limited to 'db/schema')
| -rw-r--r-- | db/schema/basicContractDocumnet.ts | 3 | ||||
| -rw-r--r-- | db/schema/gtc.ts | 659 |
2 files changed, 398 insertions, 264 deletions
diff --git a/db/schema/basicContractDocumnet.ts b/db/schema/basicContractDocumnet.ts index 7a9c972d..3a0b84a9 100644 --- a/db/schema/basicContractDocumnet.ts +++ b/db/schema/basicContractDocumnet.ts @@ -58,8 +58,6 @@ export const basicContract = pgTable('basic_contract', { createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow(), completedAt: timestamp('completed_at'), // 계약 체결 완료 날짜 - - }); // 기본 계약 요청 뷰 @@ -120,6 +118,7 @@ export const basicContractView = pgView('basic_contract_view').as((qb) => { .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; diff --git a/db/schema/gtc.ts b/db/schema/gtc.ts index e91ed44f..3642de36 100644 --- a/db/schema/gtc.ts +++ b/db/schema/gtc.ts @@ -1,152 +1,152 @@ -import { - pgTable, - serial, - varchar, - text, - integer, - timestamp, - pgEnum, - boolean, - index, - uniqueIndex, pgView, jsonb ,decimal, json - } from "drizzle-orm/pg-core" - import { relations ,sql, eq} from "drizzle-orm" +import { + pgTable, + serial, + varchar, + text, + integer, + timestamp, + pgEnum, + boolean, + index, + uniqueIndex, pgView, jsonb, decimal, json +} from "drizzle-orm/pg-core" +import { relations, sql, eq } from "drizzle-orm" import { projects } from "./projects" import { users } from "./users" import { vendors } from "./vendors" - - // GTC 구분 enum - export const gtcTypeEnum = pgEnum("gtc_type", ["standard", "project"]) - - // GTC 문서 테이블 - export const gtcDocuments = pgTable("gtc_documents", { - id: serial("id").primaryKey(), - - // 구분 (표준/프로젝트) - type: gtcTypeEnum("type").notNull(), - - // 프로젝트 참조 (프로젝트 타입인 경우만) - projectId: integer("project_id").references(() => projects.id, { - onDelete: "cascade" - }), - - title:varchar("title", { length: 255 }), - - // 리비전 번호 - revision: integer("revision").notNull().default(0), - - // 파일 정보 - fileName: varchar("file_name", { length: 255 }), - filePath: varchar("file_path", { length: 500 }), - fileSize: integer("file_size"), // bytes - - // 최초 등록 정보 - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), - createdById: integer("created_by_id") - .references(() => users.id, { onDelete: "set null" }) - .notNull(), - - // 최종 수정 정보 - updatedAt: timestamp("updated_at", { withTimezone: true }) - .defaultNow() - .notNull(), - updatedById: integer("updated_by_id") - .references(() => users.id, { onDelete: "set null" }), - - // 편집 사유 - editReason: text("edit_reason"), - - // 활성 상태 - isActive: boolean("is_active").default(true).notNull(), - - }, (table) => { - return { - // 프로젝트별 리비전 유니크 (표준의 경우 projectId가 null) - projectRevisionIdx: uniqueIndex("gtc_project_revision_idx") - .on(table.projectId, table.revision, table.type), - - // 조회 성능을 위한 인덱스들 - typeIdx: index("gtc_type_idx").on(table.type), - projectIdx: index("gtc_project_idx").on(table.projectId), - createdAtIdx: index("gtc_created_at_idx").on(table.createdAt), - updatedAtIdx: index("gtc_updated_at_idx").on(table.updatedAt), - } - }) - - // 관계 정의 (필요한 경우) - export const gtcDocumentsRelations = relations(gtcDocuments, ({ one }) => ({ - project: one(projects, { - fields: [gtcDocuments.projectId], - references: [projects.id], - }), - createdBy: one(users, { - fields: [gtcDocuments.createdById], - references: [users.id], - }), - updatedBy: one(users, { - fields: [gtcDocuments.updatedById], - references: [users.id], - }), - })) - - // 타입 정의 - export type GtcDocument = typeof gtcDocuments.$inferSelect - export type NewGtcDocument = typeof gtcDocuments.$inferInsert - - // 조인된 결과를 위한 타입 - export type GtcDocumentWithRelations = GtcDocument & { - project?: { - id: number - projectCode: string - projectName: string - } - createdBy?: { - id: number - name: string - } - updatedBy?: { - id: number - name: string - } + +// GTC 구분 enum +export const gtcTypeEnum = pgEnum("gtc_type", ["standard", "project"]) + +// GTC 문서 테이블 +export const gtcDocuments = pgTable("gtc_documents", { + id: serial("id").primaryKey(), + + // 구분 (표준/프로젝트) + type: gtcTypeEnum("type").notNull(), + + // 프로젝트 참조 (프로젝트 타입인 경우만) + projectId: integer("project_id").references(() => projects.id, { + onDelete: "cascade" + }), + + title: varchar("title", { length: 255 }), + + // 리비전 번호 + revision: integer("revision").notNull().default(0), + + // 파일 정보 + fileName: varchar("file_name", { length: 255 }), + filePath: varchar("file_path", { length: 500 }), + fileSize: integer("file_size"), // bytes + + // 최초 등록 정보 + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + createdById: integer("created_by_id") + .references(() => users.id, { onDelete: "set null" }) + .notNull(), + + // 최종 수정 정보 + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedById: integer("updated_by_id") + .references(() => users.id, { onDelete: "set null" }), + + // 편집 사유 + editReason: text("edit_reason"), + + // 활성 상태 + isActive: boolean("is_active").default(true).notNull(), + +}, (table) => { + return { + // 프로젝트별 리비전 유니크 (표준의 경우 projectId가 null) + projectRevisionIdx: uniqueIndex("gtc_project_revision_idx") + .on(table.projectId, table.revision, table.type), + + // 조회 성능을 위한 인덱스들 + typeIdx: index("gtc_type_idx").on(table.type), + projectIdx: index("gtc_project_idx").on(table.projectId), + createdAtIdx: index("gtc_created_at_idx").on(table.createdAt), + updatedAtIdx: index("gtc_updated_at_idx").on(table.updatedAt), } +}) +// 관계 정의 (필요한 경우) +export const gtcDocumentsRelations = relations(gtcDocuments, ({ one }) => ({ + project: one(projects, { + fields: [gtcDocuments.projectId], + references: [projects.id], + }), + createdBy: one(users, { + fields: [gtcDocuments.createdById], + references: [users.id], + }), + updatedBy: one(users, { + fields: [gtcDocuments.updatedById], + references: [users.id], + }), +})) - export const gtcDocumentsView = pgView('gtc_documents_view').as((qb) => - qb - .select({ - // GTC 문서 기본 필드 - id: gtcDocuments.id, - type: gtcDocuments.type, - projectId: gtcDocuments.projectId, - revision: gtcDocuments.revision, - title: gtcDocuments.title, - fileName: gtcDocuments.fileName, - filePath: gtcDocuments.filePath, - fileSize: gtcDocuments.fileSize, - createdAt: gtcDocuments.createdAt, - createdById: gtcDocuments.createdById, - updatedAt: gtcDocuments.updatedAt, - updatedById: gtcDocuments.updatedById, - editReason: gtcDocuments.editReason, - isActive: gtcDocuments.isActive, - - // 프로젝트 정보 - projectCode: projects.code, - projectName: projects.name, - - // 생성자 정보 - createdByName: sql<string>`created_by_user.name`.as('created_by_name'), - createdByEmail: sql<string>`created_by_user.email`.as('created_by_email'), - - // 수정자 정보 - updatedByName: sql<string>`updated_by_user.name`.as('updated_by_name'), - updatedByEmail: sql<string>`updated_by_user.email`.as('updated_by_email'), - - // 집계 필드들 - // 같은 타입/프로젝트의 총 문서 수 - totalDocumentsInGroup: sql<number>` +// 타입 정의 +export type GtcDocument = typeof gtcDocuments.$inferSelect +export type NewGtcDocument = typeof gtcDocuments.$inferInsert + +// 조인된 결과를 위한 타입 +export type GtcDocumentWithRelations = GtcDocument & { + project?: { + id: number + projectCode: string + projectName: string + } + createdBy?: { + id: number + name: string + } + updatedBy?: { + id: number + name: string + } +} + + +export const gtcDocumentsView = pgView('gtc_documents_view').as((qb) => + qb + .select({ + // GTC 문서 기본 필드 + id: gtcDocuments.id, + type: gtcDocuments.type, + projectId: gtcDocuments.projectId, + revision: gtcDocuments.revision, + title: gtcDocuments.title, + fileName: gtcDocuments.fileName, + filePath: gtcDocuments.filePath, + fileSize: gtcDocuments.fileSize, + createdAt: gtcDocuments.createdAt, + createdById: gtcDocuments.createdById, + updatedAt: gtcDocuments.updatedAt, + updatedById: gtcDocuments.updatedById, + editReason: gtcDocuments.editReason, + isActive: gtcDocuments.isActive, + + // 프로젝트 정보 + projectCode: projects.code, + projectName: projects.name, + + // 생성자 정보 + createdByName: sql<string>`created_by_user.name`.as('created_by_name'), + createdByEmail: sql<string>`created_by_user.email`.as('created_by_email'), + + // 수정자 정보 + updatedByName: sql<string>`updated_by_user.name`.as('updated_by_name'), + updatedByEmail: sql<string>`updated_by_user.email`.as('updated_by_email'), + + // 집계 필드들 + // 같은 타입/프로젝트의 총 문서 수 + totalDocumentsInGroup: sql<number>` ( SELECT count(*) FROM gtc_documents gd2 @@ -158,9 +158,9 @@ import { vendors } from "./vendors" ) ) `.as('total_documents_in_group'), - - // 최신 리비전 번호 - latestRevision: sql<number>` + + // 최신 리비전 번호 + latestRevision: sql<number>` ( SELECT max(revision) FROM gtc_documents gd3 @@ -172,9 +172,9 @@ import { vendors } from "./vendors" ) ) `.as('latest_revision'), - - // 현재 문서가 최신인지 여부 - isLatestRevision: sql<boolean>` + + // 현재 문서가 최신인지 여부 + isLatestRevision: sql<boolean>` ${gtcDocuments.revision} = ( SELECT max(revision) FROM gtc_documents gd4 @@ -186,9 +186,9 @@ import { vendors } from "./vendors" ) ) `.as('is_latest_revision'), - - // 이전 리비전 ID (있다면) - previousRevisionId: sql<number>` + + // 이전 리비전 ID (있다면) + previousRevisionId: sql<number>` ( SELECT id FROM gtc_documents gd5 @@ -203,9 +203,9 @@ import { vendors } from "./vendors" LIMIT 1 ) `.as('previous_revision_id'), - - // 다음 리비전 ID (있다면) - nextRevisionId: sql<number>` + + // 다음 리비전 ID (있다면) + nextRevisionId: sql<number>` ( SELECT id FROM gtc_documents gd6 @@ -220,9 +220,9 @@ import { vendors } from "./vendors" LIMIT 1 ) `.as('next_revision_id'), - - // 파일 크기 (사람이 읽기 쉬운 형태) - fileSizeFormatted: sql<string>` + + // 파일 크기 (사람이 읽기 쉬운 형태) + fileSizeFormatted: sql<string>` CASE WHEN ${gtcDocuments.fileSize} IS NULL THEN NULL WHEN ${gtcDocuments.fileSize} < 1024 THEN ${gtcDocuments.fileSize} || ' B' @@ -231,9 +231,9 @@ import { vendors } from "./vendors" ELSE round(${gtcDocuments.fileSize} / (1024.0 * 1024 * 1024), 1) || ' GB' END `.as('file_size_formatted'), - - // 프로젝트가 있는 경우, 해당 프로젝트의 총 GTC 문서 수 - projectTotalDocuments: sql<number>` + + // 프로젝트가 있는 경우, 해당 프로젝트의 총 GTC 문서 수 + projectTotalDocuments: sql<number>` CASE WHEN ${gtcDocuments.projectId} IS NOT NULL THEN ( SELECT count(*) @@ -244,9 +244,9 @@ import { vendors } from "./vendors" ELSE NULL END `.as('project_total_documents'), - - // 리비전 히스토리 (배열로 반환) - revisionHistory: sql<number[]>` + + // 리비전 히스토리 (배열로 반환) + revisionHistory: sql<number[]>` ( SELECT array_agg(revision ORDER BY revision) FROM gtc_documents gd8 @@ -258,28 +258,28 @@ import { vendors } from "./vendors" ) ) `.as('revision_history'), - - // 수정 이력이 있는지 여부 - hasEditHistory: sql<boolean>` + + // 수정 이력이 있는지 여부 + hasEditHistory: sql<boolean>` ${gtcDocuments.createdById} != ${gtcDocuments.updatedById} OR ${gtcDocuments.createdAt} != ${gtcDocuments.updatedAt} `.as('has_edit_history'), - }) - .from(gtcDocuments) - .leftJoin(projects, sql`${gtcDocuments.projectId} = ${projects.id}`) - .leftJoin( - sql`users created_by_user`, - sql`${gtcDocuments.createdById} = created_by_user.id` - ) - .leftJoin( - sql`users updated_by_user`, - sql`${gtcDocuments.updatedById} = updated_by_user.id` - ) - ); - - export type GtcDocumentView = typeof gtcDocumentsView.$inferSelect; - - // 협의 상태 enum + }) + .from(gtcDocuments) + .leftJoin(projects, sql`${gtcDocuments.projectId} = ${projects.id}`) + .leftJoin( + sql`users created_by_user`, + sql`${gtcDocuments.createdById} = created_by_user.id` + ) + .leftJoin( + sql`users updated_by_user`, + sql`${gtcDocuments.updatedById} = updated_by_user.id` + ) +); + +export type GtcDocumentView = typeof gtcDocumentsView.$inferSelect; + +// 협의 상태 enum export const reviewStatusEnum = pgEnum("review_status", [ "draft", // 초안 "pending", // 협의 대기 @@ -289,43 +289,43 @@ export const reviewStatusEnum = pgEnum("review_status", [ "revised" // 수정됨 ]) - - // 템플릿 파일 관리 테이블 + +// 템플릿 파일 관리 테이블 export const gtcTemplates = pgTable("gtc_templates", { id: serial("id").primaryKey(), documentId: integer("document_id") - .references(() => gtcDocuments.id, { onDelete: "cascade" }) - .notNull(), - + .references(() => gtcDocuments.id, { onDelete: "cascade" }) + .notNull(), + name: varchar("name", { length: 255 }).notNull(), description: text("description"), version: varchar("version", { length: 50 }).default("1.0").notNull(), - + // 파일 정보 originalFileName: varchar("original_file_name", { length: 255 }).notNull(), filePath: varchar("file_path", { length: 500 }).notNull(), fileSize: integer("file_size"), - + // PDFTron 변수 메타데이터 variableMetadata: jsonb("variable_metadata"), - + isActive: boolean("is_active").default(true).notNull(), isDefault: boolean("is_default").default(false).notNull(), - + createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), createdById: integer("created_by_id") .references(() => users.id, { onDelete: "set null" }) .notNull(), - + updatedAt: timestamp("updated_at", { withTimezone: true }) .defaultNow() .notNull(), updatedById: integer("updated_by_id") .references(() => users.id, { onDelete: "set null" }), - + }, (table) => { return { nameIdx: index("gtc_templates_name_idx").on(table.name), @@ -336,58 +336,58 @@ export const gtcTemplates = pgTable("gtc_templates", { // 표준(마스터) GTC 조항 테이블 export const gtcClauses = pgTable("gtc_clauses", { id: serial("id").primaryKey(), - + // GTC 문서 참조 documentId: integer("document_id") .references(() => gtcDocuments.id, { onDelete: "cascade" }) .notNull(), - + // 계층구조 parentId: integer("parent_id"), - + // 채번 itemNumber: varchar("item_number", { length: 50 }).notNull(), - + // 분류 (수기입력) category: varchar("category", { length: 100 }), - + // 소제목 subtitle: varchar("subtitle", { length: 500 }).notNull(), - + // 상세항목 content: text("content"), - + // 순서 sortOrder: decimal("sort_order", { precision: 10, scale: 2 }) .default('0') .notNull(), - + // 계층 정보 depth: integer("depth").default(0).notNull(), fullPath: varchar("full_path", { length: 200 }), - + // PDFTron 변수명들 images: json("images"), // ✅ 이미지 JSON 배열 추가 - + isActive: boolean("is_active").default(true).notNull(), - + createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), createdById: integer("created_by_id") .references(() => users.id, { onDelete: "set null" }) .notNull(), - + updatedAt: timestamp("updated_at", { withTimezone: true }) .defaultNow() .notNull(), updatedById: integer("updated_by_id") .references(() => users.id, { onDelete: "set null" }), - + editReason: text("edit_reason"), - + }, (table) => { return { documentItemNumberIdx: uniqueIndex("gtc_clauses_document_item_number_idx") @@ -401,50 +401,50 @@ export const gtcClauses = pgTable("gtc_clauses", { // 벤더별 GTC 문서 테이블 export const gtcVendorDocuments = pgTable("gtc_vendor_documents", { id: serial("id").primaryKey(), - + // 표준 GTC 문서 참조 baseDocumentId: integer("base_document_id") .references(() => gtcDocuments.id, { onDelete: "cascade" }) .notNull(), - + // 벤더 참조 vendorId: integer("vendor_id") .references(() => vendors.id, { onDelete: "cascade" }) .notNull(), - + // 벤더별 문서 정보 name: varchar("name", { length: 255 }).notNull(), description: text("description"), version: varchar("version", { length: 50 }).default("1.0").notNull(), - + // 협의 상태 reviewStatus: reviewStatusEnum("review_status").default("draft").notNull(), - + // 협의 일정 negotiationStartDate: timestamp("negotiation_start_date", { withTimezone: true }), negotiationEndDate: timestamp("negotiation_end_date", { withTimezone: true }), approvalDate: timestamp("approval_date", { withTimezone: true }), - + // 최종 파일 (협의 완료 후 생성된 파일) finalFileName: varchar("final_file_name", { length: 255 }), finalFilePath: varchar("final_file_path", { length: 500 }), finalFileSize: integer("final_file_size"), - + isActive: boolean("is_active").default(true).notNull(), - + createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), createdById: integer("created_by_id") .references(() => users.id, { onDelete: "set null" }) .notNull(), - + updatedAt: timestamp("updated_at", { withTimezone: true }) .defaultNow() .notNull(), updatedById: integer("updated_by_id") .references(() => users.id, { onDelete: "set null" }), - + }, (table) => { return { // 벤더별 기본문서 유니크 (벤더당 하나의 기본문서에 대해 하나의 협의문서) @@ -459,64 +459,64 @@ export const gtcVendorDocuments = pgTable("gtc_vendor_documents", { // 벤더별 GTC 조항 테이블 (표준 조항을 참조하되 내용 수정 가능) export const gtcVendorClauses = pgTable("gtc_vendor_clauses", { id: serial("id").primaryKey(), - + // 벤더 문서 참조 vendorDocumentId: integer("vendor_document_id") .references(() => gtcVendorDocuments.id, { onDelete: "cascade" }) .notNull(), - + // 표준 조항 참조 (기준이 되는 마스터 조항) baseClauseId: integer("base_clause_id") .references(() => gtcClauses.id, { onDelete: "cascade" }) .notNull(), - + // 계층구조 (벤더별 문서 내에서) parentId: integer("parent_id"), - + // 벤더별로 수정된 내용 (null이면 표준 내용 사용) modifiedItemNumber: varchar("modified_item_number", { length: 50 }), modifiedCategory: varchar("modified_category", { length: 100 }), modifiedSubtitle: varchar("modified_subtitle", { length: 500 }), modifiedContent: text("modified_content"), - + // 순서 (벤더별 문서 내에서) sortOrder: decimal("sort_order", { precision: 10, scale: 2 }) .default('0') .notNull(), - + // 계층 정보 depth: integer("depth").default(0).notNull(), fullPath: varchar("full_path", { length: 200 }), - + // 수정 여부 플래그들 isNumberModified: boolean("is_number_modified").default(false).notNull(), isCategoryModified: boolean("is_category_modified").default(false).notNull(), isSubtitleModified: boolean("is_subtitle_modified").default(false).notNull(), isContentModified: boolean("is_content_modified").default(false).notNull(), - + // 협의 관련 reviewStatus: reviewStatusEnum("review_status").default("draft").notNull(), negotiationNote: text("negotiation_note"), // 협의 과정 메모 - + // 활성/비활성 (특정 조항을 벤더와의 협의에서 제외할 수 있음) isActive: boolean("is_active").default(true).notNull(), isExcluded: boolean("is_excluded").default(false).notNull(), // 벤더 협의에서 제외 - + createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), createdById: integer("created_by_id") .references(() => users.id, { onDelete: "set null" }) .notNull(), - + updatedAt: timestamp("updated_at", { withTimezone: true }) .defaultNow() .notNull(), updatedById: integer("updated_by_id") .references(() => users.id, { onDelete: "set null" }), - + editReason: text("edit_reason"), - + }, (table) => { return { // 벤더 문서별 기본 조항 유니크 @@ -532,34 +532,34 @@ export const gtcVendorClauses = pgTable("gtc_vendor_clauses", { // 협의 이력 테이블 export const gtcNegotiationHistory = pgTable("gtc_negotiation_history", { id: serial("id").primaryKey(), - + // 벤더 조항 참조 vendorClauseId: integer("vendor_clause_id") .references(() => gtcVendorClauses.id, { onDelete: "cascade" }) .notNull(), - + // 협의 정보 action: varchar("action", { length: 50 }).notNull(), // "created", "modified", "approved", "rejected", "commented" previousStatus: reviewStatusEnum("previous_status"), newStatus: reviewStatusEnum("new_status"), - + // 변경 내용 changedFields: jsonb("changed_fields"), // 어떤 필드가 변경되었는지 comment: text("comment"), - + // 첨부파일 attachments: jsonb("attachments"), - + // 협의 당사자 actorType: varchar("actor_type", { length: 20 }).notNull(), // "internal", "vendor" actorId: integer("actor_id").references(() => users.id, { onDelete: "set null" }), actorName: varchar("actor_name", { length: 100 }), // 외부 벤더인 경우 actorEmail: varchar("actor_email", { length: 255 }), - + createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), - + }, (table) => { return { vendorClauseIdx: index("gtc_negotiation_history_vendor_clause_idx").on(table.vendorClauseId), @@ -708,23 +708,23 @@ export const gtcClausesTreeView = pgView('gtc_clauses_tree_view').as((qb) => updatedAt: gtcClauses.updatedAt, updatedById: gtcClauses.updatedById, editReason: gtcClauses.editReason, - + // 문서 정보 documentType: gtcDocuments.type, documentFileName: gtcDocuments.fileName, documentRevision: gtcDocuments.revision, projectId: gtcDocuments.projectId, - + // 작성자/수정자 정보 createdByName: sql<string>`created_by_user.name`.as('created_by_name'), createdByEmail: sql<string>`created_by_user.email`.as('created_by_email'), updatedByName: sql<string>`updated_by_user.name`.as('updated_by_name'), updatedByEmail: sql<string>`updated_by_user.email`.as('updated_by_email'), - + // 부모 정보 parentItemNumber: sql<string>`parent_clause.item_number`.as('parent_item_number'), parentSubtitle: sql<string>`parent_clause.subtitle`.as('parent_subtitle'), - + // 자식 수 childrenCount: sql<number>` ( @@ -734,7 +734,7 @@ export const gtcClausesTreeView = pgView('gtc_clauses_tree_view').as((qb) => AND children.is_active = true ) `.as('children_count'), - + // 같은 부모 아래 형제 수 siblingsCount: sql<number>` ( @@ -744,7 +744,7 @@ export const gtcClausesTreeView = pgView('gtc_clauses_tree_view').as((qb) => AND siblings.is_active = true ) `.as('siblings_count'), - + // 수정 이력이 있는지 여부 hasEditHistory: sql<boolean>` ${gtcClauses.createdById} != ${gtcClauses.updatedById} OR @@ -754,15 +754,15 @@ export const gtcClausesTreeView = pgView('gtc_clauses_tree_view').as((qb) => .from(gtcClauses) .leftJoin(gtcDocuments, sql`${gtcClauses.documentId} = ${gtcDocuments.id}`) .leftJoin( - sql`users created_by_user`, + sql`users created_by_user`, sql`${gtcClauses.createdById} = created_by_user.id` ) .leftJoin( - sql`users updated_by_user`, + sql`users updated_by_user`, sql`${gtcClauses.updatedById} = updated_by_user.id` ) .leftJoin( - sql`gtc_clauses parent_clause`, + sql`gtc_clauses parent_clause`, sql`${gtcClauses.parentId} = parent_clause.id` ) ) @@ -778,56 +778,56 @@ export const gtcVendorClausesView = pgView('gtc_vendor_clauses_view').as((qb) => vendorDocumentId: gtcVendorClauses.vendorDocumentId, baseClauseId: gtcVendorClauses.baseClauseId, parentId: gtcVendorClauses.parentId, - + // 실제 사용될 값들 (수정된 값이 있으면 수정값, 없으면 표준값) effectiveItemNumber: sql<string>` COALESCE(${gtcVendorClauses.modifiedItemNumber}, ${gtcClauses.itemNumber}) `.as('effective_item_number'), - + effectiveCategory: sql<string>` COALESCE(${gtcVendorClauses.modifiedCategory}, ${gtcClauses.category}) `.as('effective_category'), - + effectiveSubtitle: sql<string>` COALESCE(${gtcVendorClauses.modifiedSubtitle}, ${gtcClauses.subtitle}) `.as('effective_subtitle'), - + effectiveContent: sql<string>` COALESCE(${gtcVendorClauses.modifiedContent}, ${gtcClauses.content}) `.as('effective_content'), - + // 수정 정보 isNumberModified: gtcVendorClauses.isNumberModified, isCategoryModified: gtcVendorClauses.isCategoryModified, isSubtitleModified: gtcVendorClauses.isSubtitleModified, isContentModified: gtcVendorClauses.isContentModified, - + // 표준 조항 정보 baseItemNumber: gtcClauses.itemNumber, baseCategory: gtcClauses.category, baseSubtitle: gtcClauses.subtitle, baseContent: gtcClauses.content, - + // 벤더 정보 vendorId: gtcVendorDocuments.vendorId, vendorCode: vendors.vendorCode, vendorName: vendors.vendorName, - + // 문서 정보 baseDocumentId: gtcVendorDocuments.baseDocumentId, documentType: gtcDocuments.type, documentFileName: gtcDocuments.fileName, - + // 협의 상태 reviewStatus: gtcVendorClauses.reviewStatus, negotiationNote: gtcVendorClauses.negotiationNote, isExcluded: gtcVendorClauses.isExcluded, - + // 계층 정보 sortOrder: gtcVendorClauses.sortOrder, depth: gtcVendorClauses.depth, fullPath: gtcVendorClauses.fullPath, - + // 수정 여부 (하나라도 수정되었는지) hasModifications: sql<boolean>` ${gtcVendorClauses.isNumberModified} OR @@ -835,8 +835,8 @@ export const gtcVendorClausesView = pgView('gtc_vendor_clauses_view').as((qb) => ${gtcVendorClauses.isSubtitleModified} OR ${gtcVendorClauses.isContentModified} `.as('has_modifications'), - - + + createdAt: gtcVendorClauses.createdAt, updatedAt: gtcVendorClauses.updatedAt, }) @@ -865,6 +865,141 @@ export type GtcVendorClauseForPreview = { variables: { number: string subtitle: string - content: string| null + content: string | null } -}
\ No newline at end of file +} + + +// 새로운 뷰: 모든 기본 조항 + 벤더 수정사항 (선택적) +export const gtcClausesWithVendorView = pgView('gtc_clauses_with_vendor_view').as((qb) => + qb + .select({ + // 기본 조항 정보 (항상 존재) - 모든 컬럼을 명시적 SQL로 참조 + baseClauseId: sql<number>`base_clauses.id`.as('base_clause_id'), + documentId: sql<number>`base_clauses.document_id`.as('document_id'), + parentId: sql<number>`base_clauses.parent_id`.as('parent_id'), + + // 기본 조항 내용 + baseItemNumber: sql<string>`base_clauses.item_number`.as('base_item_number'), + baseCategory: sql<string>`base_clauses.category`.as('base_category'), + baseSubtitle: sql<string>`base_clauses.subtitle`.as('base_subtitle'), + baseContent: sql<string>`base_clauses.content`.as('base_content'), + baseSortOrder: sql<string>`base_clauses.sort_order`.as('base_sort_order'), + baseDepth: sql<number>`base_clauses.depth`.as('base_depth'), + baseFullPath: sql<string>`base_clauses.full_path`.as('base_full_path'), + baseIsActive: sql<boolean>`base_clauses.is_active`.as('base_is_active'), + + // 벤더 조항 정보 (있을 수도 없을 수도) + vendorClauseId: sql<number>`vendor_clauses.id`.as('vendor_clause_id'), + vendorDocumentId: sql<number>`vendor_clauses.vendor_document_id`.as('vendor_document_id'), + + // 벤더 정보 + vendorId: sql<number>`vendor_docs.vendor_id`.as('vendor_id'), + vendorCode: sql<string>`v.vendor_code`.as('vendor_code'), + vendorName: sql<string>`v.vendor_name`.as('vendor_name'), + + // 문서 정보 + documentType: sql<string>`base_doc.type`.as('document_type'), + documentTitle: sql<string>`base_doc.title`.as('document_title'), + documentRevision: sql<number>`base_doc.revision`.as('document_revision'), + + // 실제 표시될 값 (벤더 수정값이 있으면 수정값, 없으면 기본값) + effectiveItemNumber: sql<string>` + COALESCE(vendor_clauses.modified_item_number, base_clauses.item_number) + `.as('effective_item_number'), + + effectiveCategory: sql<string>` + COALESCE(vendor_clauses.modified_category, base_clauses.category) + `.as('effective_category'), + + effectiveSubtitle: sql<string>` + COALESCE(vendor_clauses.modified_subtitle, base_clauses.subtitle) + `.as('effective_subtitle'), + + effectiveContent: sql<string>` + COALESCE(vendor_clauses.modified_content, base_clauses.content) + `.as('effective_content'), + + effectiveSortOrder: sql<string>` + COALESCE(vendor_clauses.sort_order, base_clauses.sort_order) + `.as('effective_sort_order'), + + effectiveDepth: sql<number>` + COALESCE(vendor_clauses.depth, base_clauses.depth) + `.as('effective_depth'), + + effectiveFullPath: sql<string>` + COALESCE(vendor_clauses.full_path, base_clauses.full_path) + `.as('effective_full_path'), + + // 수정 상태 정보 + isNumberModified: sql<boolean>` + COALESCE(vendor_clauses.is_number_modified, false) + `.as('is_number_modified'), + + isCategoryModified: sql<boolean>` + COALESCE(vendor_clauses.is_category_modified, false) + `.as('is_category_modified'), + + isSubtitleModified: sql<boolean>` + COALESCE(vendor_clauses.is_subtitle_modified, false) + `.as('is_subtitle_modified'), + + isContentModified: sql<boolean>` + COALESCE(vendor_clauses.is_content_modified, false) + `.as('is_content_modified'), + + // 벤더가 이 조항을 건드렸는지 여부 + hasVendorClause: sql<boolean>` + vendor_clauses.id IS NOT NULL + `.as('has_vendor_clause'), + + // 하나라도 수정되었는지 + hasModifications: sql<boolean>` + COALESCE(vendor_clauses.is_number_modified, false) OR + COALESCE(vendor_clauses.is_category_modified, false) OR + COALESCE(vendor_clauses.is_subtitle_modified, false) OR + COALESCE(vendor_clauses.is_content_modified, false) + `.as('has_modifications'), + + // 협의 상태 (벤더 조항이 있을 때만) + reviewStatus: sql<string>`vendor_clauses.review_status`.as('review_status'), + negotiationNote: sql<string>`vendor_clauses.negotiation_note`.as('negotiation_note'), + isExcluded: sql<boolean>` + COALESCE(vendor_clauses.is_excluded, false) + `.as('is_excluded'), + + // 협의 이력이 있는지 (subquery로 확인) + hasNegotiationHistory: sql<boolean>` + EXISTS( + SELECT 1 + FROM gtc_negotiation_history gnh + WHERE gnh.vendor_clause_id = vendor_clauses.id + AND gnh.comment IS NOT NULL + AND gnh.comment != '' + ) + `.as('has_negotiation_history'), + + // 날짜 정보 + baseCreatedAt: sql<Date>`base_clauses.created_at`.as('base_created_at'), + baseUpdatedAt: sql<Date>`base_clauses.updated_at`.as('base_updated_at'), + vendorCreatedAt: sql<Date>`vendor_clauses.created_at`.as('vendor_created_at'), + vendorUpdatedAt: sql<Date>`vendor_clauses.updated_at`.as('vendor_updated_at'), + }) + .from(sql`gtc_clauses base_clauses`) + // 모든 JOIN에서 명시적 별칭 사용 + .leftJoin(sql`gtc_vendor_clauses vendor_clauses`, sql` + base_clauses.id = vendor_clauses.base_clause_id + `) + .leftJoin(sql`gtc_vendor_documents vendor_docs`, sql` + vendor_clauses.vendor_document_id = vendor_docs.id + `) + .leftJoin(sql`vendors v`, sql` + vendor_docs.vendor_id = v.id + `) + .leftJoin(sql`gtc_documents base_doc`, sql` + base_clauses.document_id = base_doc.id + `) +) + +export type GtcClauseWithVendorView = typeof gtcClausesWithVendorView.$inferSelect
\ No newline at end of file |
