summaryrefslogtreecommitdiff
path: root/db/schema
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-06 04:23:40 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-06 04:23:40 +0000
commitde2ac5a2860bc25180971e7a11f852d9d44675b7 (patch)
treeb931c363f2cb19e177a0a7b17190d5de2a82d709 /db/schema
parent6c549b0f264e9be4d60af38f9efc05b189d6849f (diff)
(대표님) 정기평가, 법적검토, 정책, 가입관련 처리 및 관련 컴포넌트 추가, 메뉴 변경
Diffstat (limited to 'db/schema')
-rw-r--r--db/schema/consent.ts138
-rw-r--r--db/schema/gtc.ts4
-rw-r--r--db/schema/index.ts2
-rw-r--r--db/schema/legal.ts276
-rw-r--r--db/schema/users.ts10
5 files changed, 427 insertions, 3 deletions
diff --git a/db/schema/consent.ts b/db/schema/consent.ts
new file mode 100644
index 00000000..a02d5575
--- /dev/null
+++ b/db/schema/consent.ts
@@ -0,0 +1,138 @@
+// schema/consent.ts
+import {
+ pgTable,
+ integer,
+ varchar,
+ boolean,
+ timestamp,
+ text,
+ json,
+ pgEnum,
+ uniqueIndex,
+ index,
+ } from "drizzle-orm/pg-core";
+import { users } from "./users";
+
+ // 동의 타입 enum
+ export const consentTypeEnum = pgEnum("consent_type", [
+ "privacy_policy",
+ "terms_of_service",
+ "marketing",
+ "optional"
+ ]);
+
+ // 동의 액션 enum
+ export const consentActionEnum = pgEnum("consent_action", [
+ "consent",
+ "revoke",
+ "update"
+ ]);
+
+ // 정책 타입 enum
+ export const policyTypeEnum = pgEnum("policy_type", [
+ "privacy_policy",
+ "terms_of_service"
+ ]);
+
+ // 1. 정책 버전 관리 테이블
+ export const policyVersions = pgTable("policy_versions", {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ policyType: policyTypeEnum("policy_type").notNull(),
+ version: varchar("version", { length: 20 }).notNull(),
+ content: text("content").notNull(),
+ effectiveDate: timestamp("effective_date", { withTimezone: true }).notNull(),
+ isCurrent: boolean("is_current").default(false).notNull(),
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ }, (table) => {
+ return {
+ // 정책 타입과 버전의 유니크 조합
+ policyVersionUniqueIdx: uniqueIndex("policy_versions_type_version_idx")
+ .on(table.policyType, table.version),
+ // 현재 버전 조회용 인덱스
+ currentPolicyIdx: index("policy_versions_current_idx")
+ .on(table.isCurrent),
+ // 효력 발생일 인덱스
+ effectiveDateIdx: index("policy_versions_effective_date_idx")
+ .on(table.effectiveDate),
+ };
+ });
+
+ // 2. 사용자 동의 내역 테이블
+ export const userConsents = pgTable("user_consents", {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ userId: integer("user_id")
+ .references(() => users.id, { onDelete: "cascade" })
+ .notNull(),
+ consentType: consentTypeEnum("consent_type").notNull(),
+ consentStatus: boolean("consent_status").default(false).notNull(),
+ policyVersion: varchar("policy_version", { length: 20 }).notNull(),
+ consentedAt: timestamp("consented_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ ipAddress: varchar("ip_address", { length: 45 }), // IPv6 지원
+ userAgent: text("user_agent"),
+ revokedAt: timestamp("revoked_at", { withTimezone: true }),
+ revokeReason: text("revoke_reason"),
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ }, (table) => {
+ return {
+ // 사용자별 동의 타입 조회용 인덱스
+ userConsentTypeIdx: index("user_consents_user_type_idx")
+ .on(table.userId, table.consentType),
+ // 동의 시점 인덱스
+ consentedAtIdx: index("user_consents_consented_at_idx")
+ .on(table.consentedAt),
+ // 정책 버전별 인덱스
+ policyVersionIdx: index("user_consents_policy_version_idx")
+ .on(table.policyVersion),
+ };
+ });
+
+ // 3. 동의 로그 테이블 (모든 동의/철회 이력 보관)
+ export const consentLogs = pgTable("consent_logs", {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ userId: integer("user_id")
+ .references(() => users.id, { onDelete: "cascade" })
+ .notNull(),
+ consentType: consentTypeEnum("consent_type").notNull(),
+ action: consentActionEnum("action").notNull(),
+ oldStatus: boolean("old_status"),
+ newStatus: boolean("new_status").notNull(),
+ policyVersion: varchar("policy_version", { length: 20 }).notNull(),
+ ipAddress: varchar("ip_address", { length: 45 }),
+ userAgent: text("user_agent"),
+ actionTimestamp: timestamp("action_timestamp", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ additionalData: json("additional_data"), // 추가 메타데이터
+ }, (table) => {
+ return {
+ // 사용자별 액션 시간 순 조회용 인덱스
+ userActionTimestampIdx: index("consent_logs_user_action_timestamp_idx")
+ .on(table.userId, table.actionTimestamp),
+ // 동의 타입별 인덱스
+ consentTypeIdx: index("consent_logs_consent_type_idx")
+ .on(table.consentType),
+ // 액션 타입별 인덱스
+ actionIdx: index("consent_logs_action_idx")
+ .on(table.action),
+ };
+ });
+
+
+ export type PolicyVersion = typeof policyVersions.$inferSelect;
+ export type NewPolicyVersion = typeof policyVersions.$inferInsert;
+
+ export type UserConsent = typeof userConsents.$inferSelect;
+ export type NewUserConsent = typeof userConsents.$inferInsert;
+
+ export type ConsentLog = typeof consentLogs.$inferSelect;
+ export type NewConsentLog = typeof consentLogs.$inferInsert;
+ \ No newline at end of file
diff --git a/db/schema/gtc.ts b/db/schema/gtc.ts
index 610804a3..e91ed44f 100644
--- a/db/schema/gtc.ts
+++ b/db/schema/gtc.ts
@@ -569,7 +569,7 @@ export const gtcNegotiationHistory = pgTable("gtc_negotiation_history", {
})
// 관계 정의
-export const vendorsRelations = relations(vendors, ({ many, one }) => ({
+export const vendorsRelationsForGTC = relations(vendors, ({ many, one }) => ({
vendorDocuments: many(gtcVendorDocuments)
}))
@@ -669,8 +669,6 @@ export const gtcNegotiationHistoryRelations = relations(gtcNegotiationHistory, (
}))
// 타입 정의
-export type Vendor = typeof vendors.$inferSelect
-export type NewVendor = typeof vendors.$inferInsert
export type GtcTemplate = typeof gtcTemplates.$inferSelect
export type NewGtcTemplate = typeof gtcTemplates.$inferInsert
diff --git a/db/schema/index.ts b/db/schema/index.ts
index 87ea224a..8af7d74a 100644
--- a/db/schema/index.ts
+++ b/db/schema/index.ts
@@ -31,6 +31,8 @@ export * from './notification';
export * from './templates';
export * from './gtc';
export * from './docu-list-rule';
+export * from './legal';
+export * from './consent';
// 부서별 도메인 할당 관리
export * from './departmentDomainAssignments';
diff --git a/db/schema/legal.ts b/db/schema/legal.ts
new file mode 100644
index 00000000..e4880517
--- /dev/null
+++ b/db/schema/legal.ts
@@ -0,0 +1,276 @@
+import { pgView,pgTable, serial, varchar, boolean, date, text, timestamp, integer, decimal } from 'drizzle-orm/pg-core';
+import { vendors } from './vendors';
+import { eq , sql, relations} from "drizzle-orm";
+
+// 법무 업무 테이블
+export const legalWorks = pgTable('legal_works', {
+ id: serial('id').primaryKey(),
+
+ // 구분 (CP, GTC, 기타 등)
+ category: varchar('category', { length: 50 }).notNull(),
+
+ // 상태 (답변완료, 담당자배정, 검토요청 등)
+ status: varchar('status', { length: 100 }).notNull(),
+
+ vendorId: integer("company_id")
+ .references(() => vendors.id, { onDelete: "set null" }),
+
+ // 벤더 정보
+ vendorCode: varchar('vendor_code', { length: 50 }),
+ vendorName: varchar('vendor_name', { length: 200 }).notNull(),
+
+ // 긴급여부
+ isUrgent: boolean('is_urgent').default(false).notNull(),
+
+ // 날짜 필드들
+ requestDate: date('request_date'), // 답변요청일
+ consultationDate: date('consultation_date'), // 의뢰일
+ expectedAnswerDate: date('expected_answer_date'), // 답변예정일
+ legalCompletionDate: date('legal_completion_date'), // 법무완료일
+
+ // 담당자 정보
+ reviewer: varchar('reviewer', { length: 100 }), // 검토요청자
+ legalResponder: varchar('legal_responder', { length: 100 }), // 법무답변자
+
+ // 첨부파일 여부
+ hasAttachment: boolean('has_attachment').default(false).notNull(),
+
+ // 메타데이터
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+
+// 타입 정의
+export type LegalWork = typeof legalWorks.$inferSelect;
+export type NewLegalWork = typeof legalWorks.$inferInsert;
+
+export type LegalWorkCategory = typeof LEGAL_WORK_CATEGORIES[number];
+export type LegalWorkStatus = typeof LEGAL_WORK_STATUSES[number];
+
+
+// 법무 검토 요청 테이블
+export const legalWorkRequests = pgTable('legal_work_requests', {
+ id: serial('id').primaryKey(),
+ legalWorkId: integer('legal_work_id').references(() => legalWorks.id, { onDelete: 'cascade' }).notNull(),
+
+ // 검토부문 (준법문의 or 법무검토)
+ reviewDepartment: varchar('review_department', { length: 50 }).notNull(), // '준법문의' | '법무검토'
+
+ // 문의종류 (법무검토 선택시만)
+ inquiryType: varchar('inquiry_type', { length: 50 }), // '국내계약' | '국내자문' | '해외계약' | '해외자문'
+
+ // 제목
+ title: varchar('title', { length: 500 }).notNull(),
+
+ // 요청내용
+ requestContent: text('request_content').notNull(),
+
+ // ===== 준법문의 관련 필드들 =====
+
+ // 공개여부 (준법문의 선택시)
+ isPublic: boolean('is_public').default(false), // 기본값: 비공개
+
+ // ===== 법무검토 관련 공통 필드들 =====
+
+ // 계약명/프로젝트명 (국내계약/해외계약/해외자문 선택시)
+ contractProjectName: varchar('contract_project_name', { length: 300 }),
+
+ // 계약서 종류 (국내계약/해외계약/해외자문 선택시)
+ contractType: varchar('contract_type', { length: 100 }),
+
+ // 계약금액 (국내계약/해외계약/해외자문 선택시)
+ contractAmount: decimal('contract_amount', { precision: 15, scale: 2 }),
+
+ // ===== 국내계약 전용 필드들 =====
+
+ // 계약상대방 (국내계약 선택시)
+ contractCounterparty: varchar('contract_counterparty', { length: 200 }),
+
+ // 계약상대방 구분 (법인/개인)
+ counterpartyType: varchar('counterparty_type', { length: 20 }), // '법인' | '개인'
+
+ // 계약기간 (국내계약 선택시)
+ contractPeriod: varchar('contract_period', { length: 200 }),
+
+ // ===== 자문 관련 필드들 =====
+
+ // 사실관계 (국내자문/해외자문 선택시)
+ factualRelation: text('factual_relation'),
+
+ // ===== 해외 관련 필드들 =====
+
+ // 프로젝트번호 (해외계약/해외자문 선택시)
+ projectNumber: varchar('project_number', { length: 100 }),
+
+ // 선주/발주처 (해외계약/해외자문 선택시)
+ shipownerOrderer: varchar('shipowner_orderer', { length: 200 }),
+
+ // 준거법 (해외계약/해외자문 선택시)
+ governingLaw: varchar('governing_law', { length: 100 }),
+
+ // 프로젝트종류 (해외계약/해외자문 선택시)
+ projectType: varchar('project_type', { length: 100 }),
+
+ // 메타데이터
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+
+// 법무 회신 테이블
+export const legalWorkResponses = pgTable('legal_work_responses', {
+ id: serial('id').primaryKey(),
+ legalWorkId: integer('legal_work_id').references(() => legalWorks.id, { onDelete: 'cascade' }).notNull(),
+
+ // 답변내용
+ responseContent: text('response_content').notNull(),
+
+ // 답변 관련 담당자들
+ responseReviewer: varchar('response_reviewer', { length: 100 }), // 답변검토자
+ responseConfirmer: varchar('response_confirmer', { length: 100 }), // 답변확인자
+ responseApprover: varchar('response_approver', { length: 100 }), // 승인자
+
+ // 처리 시간들
+ reviewedAt: timestamp('reviewed_at'), // 검토시간
+ confirmedAt: timestamp('confirmed_at'), // 확인시간
+ approvedAt: timestamp('approved_at'), // 승인시간
+
+ // 공개여부
+ isPublic: boolean('is_public').default(false).notNull(),
+
+ // 재검토 관련
+ isReRevision: boolean('is_re_revision').default(false).notNull(), // 재검토 여부
+ parentResponseId: integer('parent_response_id'), // 이전 답변 참조
+
+ // 메타데이터
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+
+// 첨부파일 테이블
+export const legalWorkAttachments = pgTable('legal_work_attachments', {
+ id: serial('id').primaryKey(),
+ legalWorkId: integer('legal_work_id').references(() => legalWorks.id, { onDelete: 'cascade' }).notNull(),
+
+ fileName: varchar('file_name', { length: 255 }).notNull(),
+ originalFileName: varchar('original_file_name', { length: 255 }).notNull(),
+ filePath: varchar('file_path', { length: 500 }).notNull(),
+ fileSize: integer('file_size').notNull(),
+ mimeType: varchar('mime_type', { length: 100 }).notNull(),
+
+ // 자동 생성 파일 여부 (Case1에서 PDF 자동 생성)
+ isAutoGenerated: boolean('is_auto_generated').default(false).notNull(),
+
+ // 첨부파일 타입 (요청시 첨부 vs 답변시 첨부)
+ attachmentType: varchar('attachment_type', { length: 50 }).default('request'), // 'request' | 'response'
+
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+});
+
+
+export type LegalWorkRequest = typeof legalWorkRequests.$inferSelect;
+export type LegalWorkResponse = typeof legalWorkResponses.$inferSelect;
+export type LegalWorkAttachment = typeof legalWorkAttachments.$inferSelect;
+
+// 상수 정의
+export const LEGAL_WORK_CATEGORIES = [
+ 'CP',
+ 'GTC',
+ '기타'
+] as const;
+
+export const LEGAL_WORK_STATUSES = [
+ '신규등록',
+ '검토요청',
+ '담당자배정',
+ '검토중',
+ '답변완료',
+ '재검토요청',
+ '보류',
+ '취소'
+] as const;
+
+export const REVIEW_DEPARTMENTS = [
+ '준법문의',
+ '법무검토'
+] as const;
+
+export const INQUIRY_TYPES = [
+ '국내계약',
+ '국내자문',
+ '해외계약',
+ '해외자문'
+] as const;
+
+export const COUNTERPARTY_TYPES = [
+ '법인',
+ '개인'
+] as const;
+
+export const SOURCE_TYPES = [
+ 'interface', // 22번화면에서 이관
+ 'manual' // 신규생성
+] as const;
+
+export const ATTACHMENT_TYPES = [
+ 'request', // 요청시 첨부
+ 'response' // 답변시 첨부
+] as const;
+
+
+
+export const legalWorksDetailView = pgView("legal_works_detail_view").as((qb) => {
+ return qb
+ .select({
+ // legal_works 기본 필드들
+ id: legalWorks.id,
+ category: legalWorks.category,
+ status: legalWorks.status,
+ vendorId: legalWorks.vendorId,
+ vendorCode: legalWorks.vendorCode,
+ vendorName: legalWorks.vendorName,
+ isUrgent: legalWorks.isUrgent,
+ requestDate: legalWorks.requestDate,
+ consultationDate: legalWorks.consultationDate,
+ expectedAnswerDate: legalWorks.expectedAnswerDate,
+ legalCompletionDate: legalWorks.legalCompletionDate,
+ reviewer: legalWorks.reviewer,
+ legalResponder: legalWorks.legalResponder,
+ hasAttachment: legalWorks.hasAttachment,
+ createdAt: legalWorks.createdAt,
+ updatedAt: legalWorks.updatedAt,
+
+ // legal_work_requests 필드들
+ reviewDepartment: legalWorkRequests.reviewDepartment,
+ inquiryType: legalWorkRequests.inquiryType,
+ title: legalWorkRequests.title,
+ requestContent: legalWorkRequests.requestContent,
+ isPublicRequest: legalWorkRequests.isPublic,
+ contractProjectName: legalWorkRequests.contractProjectName,
+ contractType: legalWorkRequests.contractType,
+ contractAmount: legalWorkRequests.contractAmount,
+
+
+ // 최신 답변 정보 (서브쿼리)
+ responseContent: sql<string | null>`(
+ SELECT response_content
+ FROM legal_work_responses lwr_latest
+ WHERE lwr_latest.legal_work_id = ${legalWorks.id}
+ ORDER BY lwr_latest.created_at DESC
+ LIMIT 1
+ )`.as('response_content'),
+
+ // 첨부파일 개수
+ attachmentCount: sql<number>`(
+ SELECT COUNT(*)::integer
+ FROM legal_work_attachments lwa
+ WHERE lwa.legal_work_id = ${legalWorks.id}
+ )`.as('attachment_count'),
+ })
+ .from(legalWorks)
+ .leftJoin(legalWorkRequests, sql`${legalWorks.id} = ${legalWorkRequests.legalWorkId}`)
+ .leftJoin(vendors, sql`${legalWorks.vendorId} = ${vendors.id}`);
+});
+
+// 타입 추출
+export type LegalWorksDetailView = typeof legalWorksDetailView.$inferSelect;
+
diff --git a/db/schema/users.ts b/db/schema/users.ts
index 5ea399b4..0d727bb4 100644
--- a/db/schema/users.ts
+++ b/db/schema/users.ts
@@ -49,6 +49,16 @@ export const users = pgTable("users", {
deactivatedAt: timestamp("deactivated_at", { withTimezone: true }),
deactivationReason: varchar("deactivation_reason", { length: 50 }), // 'INACTIVE', 'ADMIN', 'GDPR' 등
+ // ✨ 새로 추가: 동의 관련 필드들
+ lastConsentUpdate: timestamp("last_consent_update", { withTimezone: true }),
+ consentVersion: varchar("consent_version", { length: 20 }), // 마지막 동의한 정책 버전
+ requiresConsentUpdate: boolean("requires_consent_update").default(false).notNull(),
+
+ // ✨ 새로 추가: 회원가입 관련
+ // emailVerified: boolean("email_verified").default(false).notNull(),
+ // emailVerifiedAt: timestamp("email_verified_at", { withTimezone: true }),
+ // registrationCompleted: boolean("registration_completed").default(false).notNull(),
+
}, (table) => {
return {
emailIdx: uniqueIndex("users_email_idx").on(table.email),