diff options
Diffstat (limited to 'db')
| -rw-r--r-- | db/schema/index.ts | 4 | ||||
| -rw-r--r-- | db/schema/menu-v2.ts | 88 | ||||
| -rw-r--r-- | db/seeds/menu-v2-seed.js | 231 | ||||
| -rw-r--r-- | db/seeds/menu-v2-seed.ts | 145 |
4 files changed, 468 insertions, 0 deletions
diff --git a/db/schema/index.ts b/db/schema/index.ts index 6463e0ec..022431cc 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); + }); +} |
