summaryrefslogtreecommitdiff
path: root/db/schema/gtc.ts
diff options
context:
space:
mode:
Diffstat (limited to 'db/schema/gtc.ts')
-rw-r--r--db/schema/gtc.ts659
1 files changed, 397 insertions, 262 deletions
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