summaryrefslogtreecommitdiff
path: root/db
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-12-08 14:19:37 +0900
committerjoonhoekim <26rote@gmail.com>2025-12-08 14:19:37 +0900
commit2ac7deb8494cf4123f0cff3321860585a44f157c (patch)
tree789b6980c8f863a0f675fad38c4a17d91ba28bf3 /db
parent71c0ba1f01b98770ec2c60cdb935ffb36c1830a9 (diff)
parente37cce51ccfa3dcb91904b2492df3a29970fadf7 (diff)
Merge remote-tracking branch 'origin/sec-patch' into table-v2
Diffstat (limited to 'db')
-rw-r--r--db/schema/basicContractDocumnet.ts15
-rw-r--r--db/schema/bidding.ts15
-rw-r--r--db/schema/generalContract.ts2
-rw-r--r--db/schema/index.ts4
-rw-r--r--db/schema/menu-v2.ts88
-rw-r--r--db/seeds/menu-v2-seed.js231
-rw-r--r--db/seeds/menu-v2-seed.ts145
7 files changed, 496 insertions, 4 deletions
diff --git a/db/schema/basicContractDocumnet.ts b/db/schema/basicContractDocumnet.ts
index 944c4b2c..e571c7e0 100644
--- a/db/schema/basicContractDocumnet.ts
+++ b/db/schema/basicContractDocumnet.ts
@@ -67,6 +67,12 @@ export const basicContract = pgTable('basic_contract', {
legalReviewRegNo: varchar('legal_review_reg_no', { length: 100 }), // 법무 시스템 REG_NO
legalReviewProgressStatus: varchar('legal_review_progress_status', { length: 255 }), // PRGS_STAT_DSC 값
+ // 준법문의 관련 필드
+ complianceReviewRequestedAt: timestamp('compliance_review_requested_at'), // 준법문의 요청일
+ complianceReviewCompletedAt: timestamp('compliance_review_completed_at'), // 준법문의 완료일
+ complianceReviewRegNo: varchar('compliance_review_reg_no', { length: 100 }), // 준법문의 시스템 REG_NO
+ complianceReviewProgressStatus: varchar('compliance_review_progress_status', { length: 255 }), // 준법문의 PRGS_STAT_DSC 값
+
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
completedAt: timestamp('completed_at'), // 계약 체결 완료 날짜
@@ -99,6 +105,12 @@ export const basicContractView = pgView('basic_contract_view').as((qb) => {
legalReviewRegNo: sql<string | null>`${basicContract.legalReviewRegNo}`.as('legal_review_reg_no'),
legalReviewProgressStatus: sql<string | null>`${basicContract.legalReviewProgressStatus}`.as('legal_review_progress_status'),
+ // 준법문의 관련 필드
+ complianceReviewRequestedAt: sql<Date | null>`${basicContract.complianceReviewRequestedAt}`.as('compliance_review_requested_at'),
+ complianceReviewCompletedAt: sql<Date | null>`${basicContract.complianceReviewCompletedAt}`.as('compliance_review_completed_at'),
+ complianceReviewRegNo: sql<string | null>`${basicContract.complianceReviewRegNo}`.as('compliance_review_reg_no'),
+ complianceReviewProgressStatus: sql<string | null>`${basicContract.complianceReviewProgressStatus}`.as('compliance_review_progress_status'),
+
createdAt: sql<Date>`${basicContract.createdAt}`.as('created_at'),
updatedAt: sql<Date>`${basicContract.updatedAt}`.as('updated_at'),
completedAt: sql<Date | null>`${basicContract.completedAt}`.as('completed_at'),
@@ -121,6 +133,9 @@ export const basicContractView = pgView('basic_contract_view').as((qb) => {
// 법무검토 상태 (PRGS_STAT_DSC 동기화 값)
legalReviewStatus: sql<string | null>`${basicContract.legalReviewProgressStatus}`.as('legal_review_status'),
+
+ // 준법문의 상태 (PRGS_STAT_DSC 동기화 값)
+ complianceReviewStatus: sql<string | null>`${basicContract.complianceReviewProgressStatus}`.as('compliance_review_status'),
// 템플릿 파일 정보
templateFilePath: sql<string | null>`${basicContractTemplates.filePath}`.as('template_file_path'),
diff --git a/db/schema/bidding.ts b/db/schema/bidding.ts
index c08ea921..8e5fe823 100644
--- a/db/schema/bidding.ts
+++ b/db/schema/bidding.ts
@@ -176,8 +176,10 @@ export const biddings = pgTable('biddings', {
// 일정 관리
preQuoteDate: date('pre_quote_date'), // 사전견적일
biddingRegistrationDate: date('bidding_registration_date'), // 입찰등록일
- submissionStartDate: timestamp('submission_start_date'), // 입찰서제출기간 시작
- submissionEndDate: timestamp('submission_end_date'), // 입찰서제출기간 끝
+ submissionStartDate: timestamp('submission_start_date'), // 입찰서제출기간 시작 (시간만 저장, 결재완료 후 실제 날짜로 계산)
+ submissionEndDate: timestamp('submission_end_date'), // 입찰서제출기간 끝 (시간만 저장, 결재완료 후 실제 날짜로 계산)
+ submissionStartOffset: integer('submission_start_offset'), // 시작일 오프셋 (결재완료일 + n일)
+ submissionDurationDays: integer('submission_duration_days'), // 입찰 기간 (시작일 + n일)
evaluationDate: timestamp('evaluation_date'),
// 사양설명회
@@ -188,11 +190,13 @@ export const biddings = pgTable('biddings', {
budget: decimal('budget', { precision: 15, scale: 2 }), // 예산
targetPrice: decimal('target_price', { precision: 15, scale: 2 }), // 내정가
targetPriceCalculationCriteria: text('target_price_calculation_criteria'), // 내정가 산정 기준
+ actualPrice: decimal('actual_price', { precision: 15, scale: 2 }), // 실적가
finalBidPrice: decimal('final_bid_price', { precision: 15, scale: 2 }), // 최종입찰가
// PR 정보
prNumber: varchar('pr_number', { length: 50 }), // PR No.
hasPrDocument: boolean('has_pr_document').default(false), // PR 문서 여부
+ plant: varchar('plant', { length: 10 }), // 플랜트 코드(WERKS), ECC 연동 시 설정
// 상태 및 설정
status: biddingStatusEnum('status').default('bidding_generated').notNull(),
@@ -297,7 +301,7 @@ export const prItemsForBidding = pgTable('pr_items_for_bidding', {
// 수량 및 중량
quantity: decimal('quantity', { precision: 10, scale: 3 }), // 수량
- quantityUnit: varchar('quantity_unit', { length: 50 }), // 수량단위 (구매단위)
+ quantityUnit: varchar('quantity_unit', { length: 50 }), // 수량단위
totalWeight: decimal('total_weight', { precision: 10, scale: 3 }), // 총 중량
weightUnit: varchar('weight_unit', { length: 50 }), // 중량단위 (자재순중량)
@@ -403,6 +407,11 @@ export const biddingCompanies = pgTable('bidding_companies', {
//연동제 적용요건 문의 여부
isPriceAdjustmentApplicableQuestion: boolean('is_price_adjustment_applicable_question').default(false), // 연동제 적용요건 문의 여부
+ // SHI 연동제 적용여부 및 관련 정보
+ shiPriceAdjustmentApplied: boolean('shi_price_adjustment_applied'), // SHI 연동제 적용여부 (null: 미정, true: 적용, false: 미적용)
+ priceAdjustmentNote: text('price_adjustment_note'), // 연동제 Note (textarea)
+ hasChemicalSubstance: boolean('has_chemical_substance'), // 화학물질여부
+
// 기타
notes: text('notes'), // 특이사항
contactPerson: varchar('contact_person', { length: 100 }), // 업체 담당자
diff --git a/db/schema/generalContract.ts b/db/schema/generalContract.ts
index 6f48581f..7cc6cd6e 100644
--- a/db/schema/generalContract.ts
+++ b/db/schema/generalContract.ts
@@ -37,7 +37,7 @@ export const generalContracts = pgTable('general_contracts', {
// ═══════════════════════════════════════════════════════════════
// 계약 분류 및 상태
// ═══════════════════════════════════════════════════════════════
- status: varchar('status', { length: 50 }).notNull(), // 계약 상태 (Draft, Complete the Contract, Contract Delete 등)
+ status: varchar('status', { length: 50 }).notNull(), // 계약 상태 (Draft, Complete the Contract, Contract Delete, approval request 등)
category: varchar('category', { length: 50 }).notNull(), // 계약구분 (단가계약, 일반계약, 매각계약)
type: varchar('type', { length: 50 }), // 계약종류 (UP, LE, IL, AL 등)
executionMethod: varchar('execution_method', { length: 50 }), // 체결방식 (오프라인, 온라인 등)
diff --git a/db/schema/index.ts b/db/schema/index.ts
index 459cc9e4..da17b069 100644
--- a/db/schema/index.ts
+++ b/db/schema/index.ts
@@ -29,7 +29,11 @@ export * from './evaluation';
export * from './evaluationTarget';
export * from './evaluationCriteria';
export * from './projectGtc';
+// 기존 menu 스키마 (deprecated - menu-v2로 대체됨)
export * from './menu';
+
+// 새로운 메뉴 트리 스키마 (v2)
+export * from './menu-v2';
export * from './information';
export * from './qna';
export * from './notice';
diff --git a/db/schema/menu-v2.ts b/db/schema/menu-v2.ts
new file mode 100644
index 00000000..2d0282fa
--- /dev/null
+++ b/db/schema/menu-v2.ts
@@ -0,0 +1,88 @@
+// db/schema/menu-v2.ts
+import { pgTable, pgEnum, integer, varchar, text, timestamp, boolean, index, uniqueIndex } from "drizzle-orm/pg-core";
+import { relations } from "drizzle-orm";
+import { users } from "./users";
+
+export const menuTreeNodeTypeEnum = pgEnum('menu_tree_node_type', [
+ 'menu_group', // 메뉴그룹 (1단계) - 헤더에 표시되는 드롭다운 트리거
+ 'group', // 그룹 (2단계) - 드롭다운 내 구분 영역
+ 'menu', // 메뉴 (3단계) - 드롭다운 내 링크
+ 'additional' // 추가 메뉴 - 최상위 단일 링크 (Dashboard, QNA, FAQ 등)
+]);
+
+export const menuDomainEnum = pgEnum('menu_domain', [
+ 'evcp', // 내부 사용자용
+ 'partners' // 협력업체용
+]);
+
+export const menuTreeNodes = pgTable("menu_tree_nodes", {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+
+ // 도메인 구분
+ domain: menuDomainEnum("domain").notNull(),
+
+ // 트리 구조
+ parentId: integer("parent_id").references((): any => menuTreeNodes.id, { onDelete: "cascade" }),
+ nodeType: menuTreeNodeTypeEnum("node_type").notNull(),
+ sortOrder: integer("sort_order").notNull().default(0),
+
+ // 다국어 텍스트 (DB 직접 관리)
+ titleKo: varchar("title_ko", { length: 255 }).notNull(),
+ titleEn: varchar("title_en", { length: 255 }),
+ descriptionKo: text("description_ko"),
+ descriptionEn: text("description_en"),
+
+ // 메뉴 전용 필드 (nodeType === 'menu' 또는 'additional'일 때)
+ menuPath: varchar("menu_path", { length: 255 }), // href 값 (예: /evcp/projects)
+ icon: varchar("icon", { length: 100 }),
+
+ // 권한 연동
+ // evcp: Oracle DB SCR_ID 참조
+ // partners: 자체 권한 시스템 (TODO)
+ scrId: varchar("scr_id", { length: 100 }),
+
+ // 상태
+ isActive: boolean("is_active").default(true).notNull(),
+
+ // 담당자 (evcp 전용)
+ manager1Id: integer("manager1_id").references(() => users.id, { onDelete: "set null" }),
+ manager2Id: integer("manager2_id").references(() => users.id, { onDelete: "set null" }),
+
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
+}, (table) => ({
+ domainIdx: index("menu_tree_domain_idx").on(table.domain),
+ parentIdx: index("menu_tree_parent_idx").on(table.parentId),
+ sortOrderIdx: index("menu_tree_sort_order_idx").on(table.sortOrder),
+ menuPathUnique: uniqueIndex("menu_tree_path_unique_idx").on(table.menuPath),
+ scrIdIdx: index("menu_tree_scr_id_idx").on(table.scrId),
+}));
+
+// Relations 정의
+export const menuTreeNodesRelations = relations(menuTreeNodes, ({ one, many }) => ({
+ parent: one(menuTreeNodes, {
+ fields: [menuTreeNodes.parentId],
+ references: [menuTreeNodes.id],
+ relationName: "parentChild",
+ }),
+ children: many(menuTreeNodes, {
+ relationName: "parentChild",
+ }),
+ manager1: one(users, {
+ fields: [menuTreeNodes.manager1Id],
+ references: [users.id],
+ relationName: "menuManager1",
+ }),
+ manager2: one(users, {
+ fields: [menuTreeNodes.manager2Id],
+ references: [users.id],
+ relationName: "menuManager2",
+ }),
+}));
+
+// Type exports
+export type MenuTreeNode = typeof menuTreeNodes.$inferSelect;
+export type NewMenuTreeNode = typeof menuTreeNodes.$inferInsert;
+export type NodeType = (typeof menuTreeNodeTypeEnum.enumValues)[number];
+export type MenuDomain = (typeof menuDomainEnum.enumValues)[number];
+
diff --git a/db/seeds/menu-v2-seed.js b/db/seeds/menu-v2-seed.js
new file mode 100644
index 00000000..e332f044
--- /dev/null
+++ b/db/seeds/menu-v2-seed.js
@@ -0,0 +1,231 @@
+"use strict";
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+};
+var __generator = (this && this.__generator) || function (thisArg, body) {
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
+ function verb(n) { return function (v) { return step([n, v]); }; }
+ function step(op) {
+ if (f) throw new TypeError("Generator is already executing.");
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
+ if (y = 0, t) op = [op[0] & 2, t.value];
+ switch (op[0]) {
+ case 0: case 1: t = op; break;
+ case 4: _.label++; return { value: op[1], done: false };
+ case 5: _.label++; y = op[1]; op = [0]; continue;
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
+ default:
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
+ if (t[2]) _.ops.pop();
+ _.trys.pop(); continue;
+ }
+ op = body.call(thisArg, _);
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
+ }
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.seedMenuTree = seedMenuTree;
+// db/seeds/menu-v2-seed.ts
+var menuConfig_1 = require("@/config/menuConfig");
+var menu_json_1 = require("@/i18n/locales/ko/menu.json");
+var menu_json_2 = require("@/i18n/locales/en/menu.json");
+var db_1 = require("@/db/db");
+var menu_v2_1 = require("@/db/schema/menu-v2");
+// 중첩 키로 번역 값 가져오기
+function getTranslation(key, locale) {
+ var translations = locale === 'ko' ? menu_json_1.default : menu_json_2.default;
+ var keys = key.split('.');
+ var value = translations;
+ for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) {
+ var k = keys_1[_i];
+ if (typeof value === 'object' && value !== null) {
+ value = value[k];
+ }
+ else {
+ return key;
+ }
+ if (value === undefined)
+ return key;
+ }
+ return typeof value === 'string' ? value : key;
+}
+function seedMenuTree() {
+ return __awaiter(this, void 0, void 0, function () {
+ return __generator(this, function (_a) {
+ switch (_a.label) {
+ case 0:
+ console.log('🌱 Starting menu tree seeding...');
+ // 기존 데이터 삭제
+ return [4 /*yield*/, db_1.default.delete(menu_v2_1.menuTreeNodes)];
+ case 1:
+ // 기존 데이터 삭제
+ _a.sent();
+ console.log('✅ Cleared existing menu tree data');
+ // evcp 도메인 seed
+ return [4 /*yield*/, seedDomainMenus('evcp', menuConfig_1.mainNav, menuConfig_1.additionalNav)];
+ case 2:
+ // evcp 도메인 seed
+ _a.sent();
+ console.log('✅ Seeded evcp menu tree');
+ // partners 도메인 seed
+ return [4 /*yield*/, seedDomainMenus('partners', menuConfig_1.mainNavVendor, menuConfig_1.additionalNavVendor)];
+ case 3:
+ // partners 도메인 seed
+ _a.sent();
+ console.log('✅ Seeded partners menu tree');
+ console.log('🎉 Menu tree seeding completed!');
+ return [2 /*return*/];
+ }
+ });
+ });
+}
+function seedDomainMenus(domain, navConfig, additionalConfig) {
+ return __awaiter(this, void 0, void 0, function () {
+ var globalSortOrder, _loop_1, _i, navConfig_1, section, additionalSortOrder, _a, additionalConfig_1, item;
+ return __generator(this, function (_b) {
+ switch (_b.label) {
+ case 0:
+ globalSortOrder = 0;
+ _loop_1 = function (section) {
+ var menuGroup, groupedItems, groupSortOrder, _c, groupedItems_1, _d, groupKey, items, parentId, group, menuSortOrder, _e, items_1, item;
+ return __generator(this, function (_f) {
+ switch (_f.label) {
+ case 0: return [4 /*yield*/, db_1.default.insert(menu_v2_1.menuTreeNodes).values({
+ domain: domain,
+ parentId: null,
+ nodeType: 'menu_group',
+ titleKo: getTranslation(section.titleKey, 'ko'),
+ titleEn: getTranslation(section.titleKey, 'en'),
+ sortOrder: globalSortOrder++,
+ isActive: true,
+ }).returning()];
+ case 1:
+ menuGroup = (_f.sent())[0];
+ groupedItems = new Map();
+ section.items.forEach(function (item) {
+ var groupKey = item.groupKey || '__default__';
+ if (!groupedItems.has(groupKey)) {
+ groupedItems.set(groupKey, []);
+ }
+ groupedItems.get(groupKey).push(item);
+ });
+ groupSortOrder = 0;
+ _c = 0, groupedItems_1 = groupedItems;
+ _f.label = 2;
+ case 2:
+ if (!(_c < groupedItems_1.length)) return [3 /*break*/, 9];
+ _d = groupedItems_1[_c], groupKey = _d[0], items = _d[1];
+ parentId = menuGroup.id;
+ if (!(groupKey !== '__default__')) return [3 /*break*/, 4];
+ return [4 /*yield*/, db_1.default.insert(menu_v2_1.menuTreeNodes).values({
+ domain: domain,
+ parentId: menuGroup.id,
+ nodeType: 'group',
+ titleKo: getTranslation(groupKey, 'ko'),
+ titleEn: getTranslation(groupKey, 'en'),
+ sortOrder: groupSortOrder++,
+ isActive: true,
+ }).returning()];
+ case 3:
+ group = (_f.sent())[0];
+ parentId = group.id;
+ _f.label = 4;
+ case 4:
+ menuSortOrder = 0;
+ _e = 0, items_1 = items;
+ _f.label = 5;
+ case 5:
+ if (!(_e < items_1.length)) return [3 /*break*/, 8];
+ item = items_1[_e];
+ return [4 /*yield*/, db_1.default.insert(menu_v2_1.menuTreeNodes).values({
+ domain: domain,
+ parentId: parentId,
+ nodeType: 'menu',
+ titleKo: getTranslation(item.titleKey, 'ko'),
+ titleEn: getTranslation(item.titleKey, 'en'),
+ descriptionKo: item.descriptionKey ? getTranslation(item.descriptionKey, 'ko') : null,
+ descriptionEn: item.descriptionKey ? getTranslation(item.descriptionKey, 'en') : null,
+ menuPath: item.href,
+ icon: item.icon || null,
+ sortOrder: menuSortOrder++,
+ isActive: true,
+ })];
+ case 6:
+ _f.sent();
+ _f.label = 7;
+ case 7:
+ _e++;
+ return [3 /*break*/, 5];
+ case 8:
+ _c++;
+ return [3 /*break*/, 2];
+ case 9: return [2 /*return*/];
+ }
+ });
+ };
+ _i = 0, navConfig_1 = navConfig;
+ _b.label = 1;
+ case 1:
+ if (!(_i < navConfig_1.length)) return [3 /*break*/, 4];
+ section = navConfig_1[_i];
+ return [5 /*yield**/, _loop_1(section)];
+ case 2:
+ _b.sent();
+ _b.label = 3;
+ case 3:
+ _i++;
+ return [3 /*break*/, 1];
+ case 4:
+ additionalSortOrder = 0;
+ _a = 0, additionalConfig_1 = additionalConfig;
+ _b.label = 5;
+ case 5:
+ if (!(_a < additionalConfig_1.length)) return [3 /*break*/, 8];
+ item = additionalConfig_1[_a];
+ return [4 /*yield*/, db_1.default.insert(menu_v2_1.menuTreeNodes).values({
+ domain: domain,
+ parentId: null,
+ nodeType: 'additional',
+ titleKo: getTranslation(item.titleKey, 'ko'),
+ titleEn: getTranslation(item.titleKey, 'en'),
+ descriptionKo: item.descriptionKey ? getTranslation(item.descriptionKey, 'ko') : null,
+ descriptionEn: item.descriptionKey ? getTranslation(item.descriptionKey, 'en') : null,
+ menuPath: item.href,
+ sortOrder: additionalSortOrder++,
+ isActive: true,
+ })];
+ case 6:
+ _b.sent();
+ _b.label = 7;
+ case 7:
+ _a++;
+ return [3 /*break*/, 5];
+ case 8: return [2 /*return*/];
+ }
+ });
+ });
+}
+// CLI에서 직접 실행 가능하도록
+if (require.main === module) {
+ seedMenuTree()
+ .then(function () {
+ console.log('Seed completed successfully');
+ process.exit(0);
+ })
+ .catch(function (error) {
+ console.error('Seed failed:', error);
+ process.exit(1);
+ });
+}
diff --git a/db/seeds/menu-v2-seed.ts b/db/seeds/menu-v2-seed.ts
new file mode 100644
index 00000000..0c6b310d
--- /dev/null
+++ b/db/seeds/menu-v2-seed.ts
@@ -0,0 +1,145 @@
+// db/seeds/menu-v2-seed.ts
+import { mainNav, additionalNav, mainNavVendor, additionalNavVendor, MenuSection, MenuItem } from "@/config/menuConfig";
+import koMenu from '@/i18n/locales/ko/menu.json';
+import enMenu from '@/i18n/locales/en/menu.json';
+import db from "@/db/db";
+import { menuTreeNodes } from "@/db/schema/menu-v2";
+import type { MenuDomain } from "@/lib/menu-v2/types";
+
+type TranslationObject = { [key: string]: string | TranslationObject };
+
+// 중첩 키로 번역 값 가져오기
+function getTranslation(key: string, locale: 'ko' | 'en'): string {
+ const translations: TranslationObject = locale === 'ko' ? koMenu : enMenu;
+ const keys = key.split('.');
+ let value: string | TranslationObject | undefined = translations;
+
+ for (const k of keys) {
+ if (typeof value === 'object' && value !== null) {
+ value = value[k];
+ } else {
+ return key;
+ }
+ if (value === undefined) return key;
+ }
+
+ return typeof value === 'string' ? value : key;
+}
+
+export async function seedMenuTree() {
+ console.log('🌱 Starting menu tree seeding...');
+
+ // 기존 데이터 삭제
+ await db.delete(menuTreeNodes);
+ console.log('✅ Cleared existing menu tree data');
+
+ // evcp 도메인 seed
+ await seedDomainMenus('evcp', mainNav, additionalNav);
+ console.log('✅ Seeded evcp menu tree');
+
+ // partners 도메인 seed
+ await seedDomainMenus('partners', mainNavVendor, additionalNavVendor);
+ console.log('✅ Seeded partners menu tree');
+
+ console.log('🎉 Menu tree seeding completed!');
+}
+
+async function seedDomainMenus(
+ domain: MenuDomain,
+ navConfig: MenuSection[],
+ additionalConfig: MenuItem[]
+) {
+ // 최상위 sortOrder (메뉴그룹과 최상위 메뉴 모두 같은 레벨에서 정렬)
+ let topLevelSortOrder = 0;
+
+ // 메인 네비게이션 (메뉴그룹 → 그룹 → 메뉴)
+ for (const section of navConfig) {
+ // 1단계: 메뉴그룹 생성
+ const [menuGroup] = await db.insert(menuTreeNodes).values({
+ domain,
+ parentId: null,
+ nodeType: 'menu_group',
+ titleKo: getTranslation(section.titleKey, 'ko'),
+ titleEn: getTranslation(section.titleKey, 'en'),
+ sortOrder: topLevelSortOrder++,
+ isActive: true,
+ }).returning();
+
+ // groupKey별로 그룹화
+ const groupedItems = new Map<string, MenuItem[]>();
+ section.items.forEach(item => {
+ const groupKey = item.groupKey || '__default__';
+ if (!groupedItems.has(groupKey)) {
+ groupedItems.set(groupKey, []);
+ }
+ groupedItems.get(groupKey)!.push(item);
+ });
+
+ let groupSortOrder = 0;
+ for (const [groupKey, items] of groupedItems) {
+ let parentId = menuGroup.id;
+
+ // groupKey가 있으면 2단계 그룹 생성
+ if (groupKey !== '__default__') {
+ const [group] = await db.insert(menuTreeNodes).values({
+ domain,
+ parentId: menuGroup.id,
+ nodeType: 'group',
+ titleKo: getTranslation(groupKey, 'ko'),
+ titleEn: getTranslation(groupKey, 'en'),
+ sortOrder: groupSortOrder++,
+ isActive: true,
+ }).returning();
+ parentId = group.id;
+ }
+
+ // 3단계: 메뉴 생성
+ let menuSortOrder = 0;
+ for (const item of items) {
+ await db.insert(menuTreeNodes).values({
+ domain,
+ parentId,
+ nodeType: 'menu',
+ titleKo: getTranslation(item.titleKey, 'ko'),
+ titleEn: getTranslation(item.titleKey, 'en'),
+ descriptionKo: item.descriptionKey ? getTranslation(item.descriptionKey, 'ko') : null,
+ descriptionEn: item.descriptionKey ? getTranslation(item.descriptionKey, 'en') : null,
+ menuPath: item.href,
+ icon: item.icon || null,
+ sortOrder: menuSortOrder++,
+ isActive: true,
+ });
+ }
+ }
+ }
+
+ // 최상위 단일 링크 메뉴 (기존 additional)
+ // nodeType을 'menu'로 설정하고 parentId를 null로 유지
+ for (const item of additionalConfig) {
+ await db.insert(menuTreeNodes).values({
+ domain,
+ parentId: null,
+ nodeType: 'menu', // 'additional' 대신 'menu' 사용
+ titleKo: getTranslation(item.titleKey, 'ko'),
+ titleEn: getTranslation(item.titleKey, 'en'),
+ descriptionKo: item.descriptionKey ? getTranslation(item.descriptionKey, 'ko') : null,
+ descriptionEn: item.descriptionKey ? getTranslation(item.descriptionKey, 'en') : null,
+ menuPath: item.href,
+ sortOrder: topLevelSortOrder++, // 메뉴그룹 다음 순서
+ isActive: true,
+ });
+ }
+}
+
+// CLI에서 직접 실행 가능하도록
+if (require.main === module) {
+ seedMenuTree()
+ .then(() => {
+ console.log('Seed completed successfully');
+ process.exit(0);
+ })
+ .catch((error) => {
+ console.error('Seed failed:', error);
+ process.exit(1);
+ });
+}