summaryrefslogtreecommitdiff
path: root/db/schema
diff options
context:
space:
mode:
Diffstat (limited to 'db/schema')
-rw-r--r--db/schema/bidding.ts4
-rw-r--r--db/schema/index.ts1
-rw-r--r--db/schema/pq.ts11
-rw-r--r--db/schema/vendorDocu.ts290
-rw-r--r--db/schema/vendorRegistrations.ts88
5 files changed, 390 insertions, 4 deletions
diff --git a/db/schema/bidding.ts b/db/schema/bidding.ts
index 96c189c6..02e869b4 100644
--- a/db/schema/bidding.ts
+++ b/db/schema/bidding.ts
@@ -237,9 +237,9 @@ export const prItemsForBidding = pgTable('pr_items_for_bidding', {
// 수량 및 중량
quantity: decimal('quantity', { precision: 10, scale: 2 }), // 수량
- quantityUnit: quantityUnitEnum('quantity_unit'), // 수량단위
+ quantityUnit: varchar('quantity_unit', { length: 50 }), // 수량단위
totalWeight: decimal('total_weight', { precision: 10, scale: 2 }), // 총 중량
- weightUnit: weightUnitEnum('weight_unit'), // 중량단위
+ weightUnit: varchar('weight_unit', { length: 50 }), // 중량단위
// 상세 정보
materialDescription: text('material_description'), // 자재내역상세
diff --git a/db/schema/index.ts b/db/schema/index.ts
index d19e501e..6f0f11d6 100644
--- a/db/schema/index.ts
+++ b/db/schema/index.ts
@@ -34,6 +34,7 @@ export * from './docu-list-rule';
export * from './legal';
export * from './consent';
export * from './bidding';
+export * from './vendorRegistrations';
// 부서별 도메인 할당 관리
export * from './departmentDomainAssignments';
diff --git a/db/schema/pq.ts b/db/schema/pq.ts
index 7adcbf8b..0bc720f6 100644
--- a/db/schema/pq.ts
+++ b/db/schema/pq.ts
@@ -133,8 +133,8 @@ export const vendorPQSubmissions = pgTable("vendor_pq_submissions", {
dueDate: timestamp("due_date"),
agreements: jsonb("agreements").notNull().default({}), // ✅ 체크 항목들을 JSON으로 저장
- // PQ 대상품목
- pqItems: varchar("pq_items", { length: 1000 }),
+ // PQ 대상품목 - [{ itemCode: string, itemName: string }, ...]
+ pqItems: jsonb("pq_items"),
submittedAt: timestamp("submitted_at"),
approvedAt: timestamp("approved_at"),
@@ -405,6 +405,9 @@ export const vendorInvestigationsView = pgView(
vendorName: vendors.vendorName,
vendorCode: vendors.vendorCode,
+ // PQ 정보
+ pqItems: vendorPQSubmissions.pqItems,
+
// User names and emails instead of just IDs
requesterName: sql<string>`requester.name`.as("requesterName"),
requesterEmail: sql<string>`requester.email`.as("requesterEmail"),
@@ -436,6 +439,10 @@ export const vendorInvestigationsView = pgView(
sql`users AS qm_manager`,
eq(vendorInvestigations.qmManagerId, sql`qm_manager.id`)
)
+ .leftJoin(
+ vendorPQSubmissions,
+ eq(vendorInvestigations.pqSubmissionId, vendorPQSubmissions.id)
+ )
})
// 방문실사 요청 테이블
diff --git a/db/schema/vendorDocu.ts b/db/schema/vendorDocu.ts
index 1fda2285..58c0ad29 100644
--- a/db/schema/vendorDocu.ts
+++ b/db/schema/vendorDocu.ts
@@ -21,6 +21,10 @@ export const documents = pgTable(
// .notNull()
.references(() => contracts.id, { onDelete: "cascade" }),
+ vendorId: integer("vendor_id")
+ // .notNull()
+ .references(() => vendors.id, { onDelete: "cascade" }),
+
// 기본 문서 정보
docNumber: varchar("doc_number", { length: 100 }).notNull(),
vendorDocNumber: varchar("vendor_doc_number", { length: 100 }),
@@ -810,6 +814,7 @@ export const simplifiedDocumentsView = pgView("simplified_documents_view", {
status: varchar("status", { length: 50 }).notNull(),
issuedDate: date("issued_date"),
contractId: integer("contract_id"),
+ vendorId: integer("vendor_id"),
// 외부 시스템 연동 정보
externalDocumentId: varchar("external_document_id", { length: 100 }),
@@ -1054,6 +1059,7 @@ export const simplifiedDocumentsView = pgView("simplified_documents_view", {
SELECT
d.id as document_id,
d.project_id,
+ d.vendor_id,
d.doc_number,
d.drawing_kind,
d.drawing_move_gbn,
@@ -1323,3 +1329,287 @@ export const documentStagesOnlyView = pgView("document_stages_only_view", {
// 타입 추출
export type DocumentStagesOnlyView = typeof documentStagesOnlyView.$inferSelect
+
+
+export const stageDocuments = pgTable(
+ "stage_documents",
+ {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+
+ // 필수 문서 정보
+ docNumber: varchar("doc_number", { length: 100 }).notNull(),
+ title: varchar("title", { length: 255 }).notNull(),
+ status: varchar("status", { length: 50 }).notNull().default("ACTIVE"),
+
+ // 선택적 문서 정보
+ vendorDocNumber: varchar("vendor_doc_number", { length: 100 }),
+ issuedDate: date("issued_date"),
+
+ // 직접 참조로 JOIN 최소화
+ projectId: integer("project_id")
+ .notNull()
+ .references(() => projects.id, { onDelete: "cascade" }),
+ vendorId: integer("vendor_id").notNull(), // vendors 테이블의 vendor_id 직접 저장
+ contractId: integer("contract_id").notNull(),
+ // 메타 정보
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+ },
+ (table) => {
+ return {
+ // 프로젝트 내에서 문서 번호 유니크
+ uniqueProjectDoc: uniqueIndex("unique_project_doc").on(
+ table.projectId,
+ table.docNumber,
+ table.status
+ ),
+
+ // 벤더 문서 번호 유니크 (있는 경우)
+ uniqueProjectVendorDoc: uniqueIndex("unique_project_vendor_doc").on(
+ table.projectId,
+ table.vendorDocNumber
+ ).where(sql`${table.vendorDocNumber} IS NOT NULL`),
+
+ // 검색용 인덱스
+ vendorIdIndex: index("stage_doc_vendor_id_idx").on(table.vendorId),
+ statusIndex: index("stage_doc_status_idx").on(table.status),
+ }
+ }
+)
+
+// 🎯 간소화된 스테이지 전용 뷰
+export const stageDocumentsView = pgView("stage_documents_view", {
+ // 기본 문서 정보
+ documentId: integer("document_id").notNull(),
+ docNumber: varchar("doc_number", { length: 100 }).notNull(),
+ vendorDocNumber: varchar("vendor_doc_number", { length: 100 }),
+ title: varchar("title", { length: 255 }).notNull(),
+ status: varchar("status", { length: 50 }).notNull(),
+ issuedDate: date("issued_date"),
+
+ // 프로젝트 및 벤더 정보 (직접 참조로 간소화)
+ projectId: integer("project_id").notNull(),
+ contractId: integer("contract_id").notNull(),
+ projectCode: varchar("project_code", { length: 50 }),
+ vendorId: integer("vendor_id").notNull(),
+ vendorName: varchar("vendor_name", { length: 255 }),
+ vendorCode: varchar("vendor_code", { length: 50 }),
+
+ // 현재 스테이지 정보 (가장 우선순위가 높은 미완료 스테이지)
+ currentStageId: integer("current_stage_id"),
+ currentStageName: varchar("current_stage_name", { length: 100 }),
+ currentStageStatus: varchar("current_stage_status", { length: 50 }),
+ currentStageOrder: integer("current_stage_order"),
+ currentStagePlanDate: date("current_stage_plan_date"),
+ currentStageActualDate: date("current_stage_actual_date"),
+ currentStageAssigneeName: varchar("current_stage_assignee_name", { length: 100 }),
+ currentStagePriority: varchar("current_stage_priority", { length: 20 }),
+
+ // 계산 필드
+ daysUntilDue: integer("days_until_due"),
+ isOverdue: boolean("is_overdue"),
+ daysDifference: integer("days_difference"),
+
+ // 전체 진행률
+ totalStages: integer("total_stages"),
+ completedStages: integer("completed_stages"),
+ progressPercentage: integer("progress_percentage"),
+
+ // 전체 스테이지 목록 (리비전 및 첨부파일 제외)
+ allStages: jsonb("all_stages").$type<Array<{
+ id: number;
+ stageName: string;
+ stageStatus: string;
+ stageOrder: number;
+ planDate: string | null;
+ actualDate: string | null;
+ assigneeName: string | null;
+ priority: string;
+ description: string | null;
+ notes: string | null;
+ reminderDays: number | null;
+ }>>(),
+
+ // 메타 정보
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
+}).as(sql`
+ WITH document_stats AS (
+ SELECT
+ sd.id as document_id,
+ COUNT(ist.id) as total_stages,
+ COUNT(CASE WHEN ist.stage_status IN ('COMPLETED', 'APPROVED') THEN 1 END) as completed_stages,
+ CASE
+ WHEN COUNT(ist.id) > 0
+ THEN ROUND((COUNT(CASE WHEN ist.stage_status IN ('COMPLETED', 'APPROVED') THEN 1 END) * 100.0) / COUNT(ist.id))
+ ELSE 0
+ END as progress_percentage
+ FROM stage_documents sd
+ LEFT JOIN stage_issue_stages ist ON sd.id = ist.document_id
+ GROUP BY sd.id
+ ),
+ current_stage_info AS (
+ SELECT DISTINCT ON (document_id)
+ document_id,
+ id as current_stage_id,
+ stage_name as current_stage_name,
+ stage_status as current_stage_status,
+ stage_order as current_stage_order,
+ plan_date as current_stage_plan_date,
+ actual_date as current_stage_actual_date,
+ assignee_name as current_stage_assignee_name,
+ priority as current_stage_priority,
+ CASE
+ WHEN actual_date IS NULL AND plan_date IS NOT NULL
+ THEN plan_date - CURRENT_DATE
+ ELSE NULL
+ END as days_until_due,
+ CASE
+ WHEN actual_date IS NULL AND plan_date < CURRENT_DATE
+ THEN true
+ WHEN actual_date IS NOT NULL AND actual_date > plan_date
+ THEN true
+ ELSE false
+ END as is_overdue,
+ CASE
+ WHEN actual_date IS NOT NULL AND plan_date IS NOT NULL
+ THEN actual_date - plan_date
+ ELSE NULL
+ END as days_difference
+ FROM stage_issue_stages
+ WHERE stage_status NOT IN ('COMPLETED', 'APPROVED')
+ ORDER BY document_id, stage_order ASC, priority DESC
+ ),
+ -- 문서별 스테이지 집계 (리비전 제외)
+ stage_aggregation AS (
+ SELECT
+ ist.document_id,
+ json_agg(
+ json_build_object(
+ 'id', ist.id,
+ 'stageName', ist.stage_name,
+ 'stageStatus', ist.stage_status,
+ 'stageOrder', ist.stage_order,
+ 'planDate', ist.plan_date,
+ 'actualDate', ist.actual_date,
+ 'assigneeName', ist.assignee_name,
+ 'priority', ist.priority,
+ 'description', ist.description,
+ 'notes', ist.notes,
+ 'reminderDays', ist.reminder_days
+ ) ORDER BY ist.stage_order
+ ) as all_stages
+ FROM stage_issue_stages ist
+ GROUP BY ist.document_id
+ )
+
+ SELECT
+ sd.id as document_id,
+ sd.doc_number,
+ sd.vendor_doc_number,
+ sd.title,
+ sd.status,
+ sd.issued_date,
+
+ -- 프로젝트 및 벤더 정보 (직접 참조로 간소화)
+ sd.project_id,
+ sd.contract_id,
+ p.code as project_code,
+ sd.vendor_id,
+ v.vendor_name,
+ v.vendor_code,
+
+ -- 현재 스테이지 정보
+ csi.current_stage_id,
+ csi.current_stage_name,
+ csi.current_stage_status,
+ csi.current_stage_order,
+ csi.current_stage_plan_date,
+ csi.current_stage_actual_date,
+ csi.current_stage_assignee_name,
+ csi.current_stage_priority,
+
+ -- 계산 필드
+ csi.days_until_due,
+ csi.is_overdue,
+ csi.days_difference,
+
+ -- 진행률 정보
+ ds.total_stages,
+ ds.completed_stages,
+ ds.progress_percentage,
+
+ -- 전체 스테이지 (리비전 제외)
+ COALESCE(sa.all_stages, '[]'::json) as all_stages,
+
+ -- 메타 정보
+ sd.created_at,
+ sd.updated_at
+
+ FROM stage_documents sd
+ -- 간소화된 JOIN (vendors는 vendor_id로 직접 조인)
+ LEFT JOIN projects p ON sd.project_id = p.id
+ LEFT JOIN vendors v ON sd.vendor_id = v.id
+
+ -- 스테이지 관련 정보 JOIN
+ LEFT JOIN document_stats ds ON sd.id = ds.document_id
+ LEFT JOIN current_stage_info csi ON sd.id = csi.document_id
+ LEFT JOIN stage_aggregation sa ON sd.id = sa.document_id
+
+ ORDER BY sd.created_at DESC
+`);
+
+
+// 🎯 issue_stages 테이블도 stage_documents를 참조하도록 수정
+export const stageIssueStages = pgTable(
+ "stage_issue_stages", // 또는 기존 issue_stages 테이블을 수정
+ {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ documentId: integer("document_id")
+ .notNull()
+ .references(() => stageDocuments.id, { onDelete: "cascade" }),
+ stageName: varchar("stage_name", { length: 100 }).notNull(),
+
+ // 기존 필드들
+ planDate: date("plan_date"),
+ actualDate: date("actual_date"),
+
+ // 스케줄 관리 필드들
+ stageStatus: varchar("stage_status", { length: 50 })
+ .notNull()
+ .default("PLANNED"), // PLANNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED, COMPLETED
+ stageOrder: integer("stage_order").default(0),
+ priority: varchar("priority", { length: 20 }).default("MEDIUM"), // HIGH, MEDIUM, LOW
+
+ // 담당자 정보
+ assigneeId: integer("assignee_id"),
+ assigneeName: varchar("assignee_name", { length: 100 }),
+
+ // 알림 및 추가 정보
+ reminderDays: integer("reminder_days").default(3),
+ description: varchar("description", { length: 500 }),
+ notes: varchar("notes", { length: 1000 }),
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+ },
+ (table) => {
+ return {
+ uniqueDocumentStage: uniqueIndex("unique_stage_document_stage").on(
+ table.documentId,
+ table.stageName
+ ),
+ documentStageOrder: uniqueIndex("stage_document_stage_order").on(
+ table.documentId,
+ table.stageOrder
+ ),
+ }
+ }
+);
+
+// 타입 추출
+export type StageDocument = typeof stageDocuments.$inferSelect
+export type StageDocumentInsert = typeof stageDocuments.$inferInsert
+export type StageDocumentsView = typeof stageDocumentsView.$inferSelect
+export type StageIssueStage = typeof stageIssueStages.$inferSelect
+export type StageIssueStageInsert = typeof stageIssueStages.$inferInsert
diff --git a/db/schema/vendorRegistrations.ts b/db/schema/vendorRegistrations.ts
new file mode 100644
index 00000000..308d664f
--- /dev/null
+++ b/db/schema/vendorRegistrations.ts
@@ -0,0 +1,88 @@
+import { relations } from "drizzle-orm";
+import {
+ date,
+ integer,
+ pgTable,
+ serial,
+ text,
+ timestamp,
+ varchar,
+ decimal,
+} from "drizzle-orm/pg-core";
+import { vendors } from "./vendors";
+
+// 정규업체 등록 관리 테이블
+export const vendorRegularRegistrations = pgTable("vendor_regular_registrations", {
+ id: serial("id").primaryKey(),
+ vendorId: integer("vendor_id").notNull().references(() => vendors.id),
+ status: varchar("status", { length: 50 }).notNull().default("audit_pass"), // audit_pass, cp_submitted, cp_review, cp_finished, approval_ready, in_review, pending_approval
+ potentialCode: varchar("potential_code", { length: 20 }), // 잠재코드
+ majorItems: text("major_items"), // 주요품목 (JSON 형태로 저장)
+ registrationRequestDate: date("registration_request_date"), // 등록요청일
+ assignedDepartment: varchar("assigned_department", { length: 100 }), // 담당부서
+ assignedDepartmentCode: varchar("assigned_department_code", { length: 20 }), // 담당부서코드
+ assignedUser: varchar("assigned_user", { length: 100 }), // 담당자
+ assignedUserCode: varchar("assigned_user_code", { length: 20 }), // 담당자코드
+ remarks: text("remarks"), // 비고
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+});
+
+// 업무담당자 정보 테이블
+export const vendorBusinessContacts = pgTable("vendor_business_contacts", {
+ id: serial("id").primaryKey(),
+ vendorId: integer("vendor_id").notNull().references(() => vendors.id),
+ contactType: varchar("contact_type", { length: 20 }).notNull(), // sales, design, delivery, quality, tax_invoice
+ contactName: varchar("contact_name", { length: 100 }).notNull(), // 담당자명
+ position: varchar("position", { length: 50 }).notNull(), // 직급
+ department: varchar("department", { length: 100 }).notNull(), // 부서
+ responsibility: varchar("responsibility", { length: 200 }).notNull(), // 담당업무
+ email: varchar("email", { length: 255 }).notNull(), // Email
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+});
+
+// 추가정보 테이블
+export const vendorAdditionalInfo = pgTable("vendor_additional_info", {
+ id: serial("id").primaryKey(),
+ vendorId: integer("vendor_id").notNull().references(() => vendors.id),
+ businessType: varchar("business_type", { length: 50 }), // 사업유형
+ industryType: varchar("industry_type", { length: 50 }), // 산업유형
+ companySize: varchar("company_size", { length: 20 }), // 기업규모
+ revenue: decimal("revenue", { precision: 15, scale: 2 }), // 매출액
+ factoryEstablishedDate: date("factory_established_date"), // 공장설립일
+ preferredContractTerms: text("preferred_contract_terms"), // 선호계약조건
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+});
+
+// Relations
+export const vendorRegularRegistrationsRelations = relations(
+ vendorRegularRegistrations,
+ ({ one }) => ({
+ vendor: one(vendors, {
+ fields: [vendorRegularRegistrations.vendorId],
+ references: [vendors.id],
+ }),
+ })
+);
+
+export const vendorBusinessContactsRelations = relations(
+ vendorBusinessContacts,
+ ({ one }) => ({
+ vendor: one(vendors, {
+ fields: [vendorBusinessContacts.vendorId],
+ references: [vendors.id],
+ }),
+ })
+);
+
+export const vendorAdditionalInfoRelations = relations(
+ vendorAdditionalInfo,
+ ({ one }) => ({
+ vendor: one(vendors, {
+ fields: [vendorAdditionalInfo.vendorId],
+ references: [vendors.id],
+ }),
+ })
+);