diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 771ab80d..e2626414 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -10,6 +10,43 @@ import { logger } from "./utils/logger"; import { errorHandler } from "./middleware/errorHandler"; import { refreshTokenIfNeeded } from "./middleware/authMiddleware"; +// ============================================ +// 프로세스 레벨 예외 처리 (서버 크래시 방지) +// ============================================ + +// 처리되지 않은 Promise 거부 핸들러 +process.on("unhandledRejection", (reason: Error | any, promise: Promise) => { + logger.error("⚠️ Unhandled Promise Rejection:", { + reason: reason?.message || reason, + stack: reason?.stack, + }); + // 프로세스를 종료하지 않고 로깅만 수행 + // 심각한 에러의 경우 graceful shutdown 고려 +}); + +// 처리되지 않은 예외 핸들러 +process.on("uncaughtException", (error: Error) => { + logger.error("🔥 Uncaught Exception:", { + message: error.message, + stack: error.stack, + }); + // 예외 발생 후에도 서버를 유지하되, 상태가 불안정할 수 있으므로 주의 + // 심각한 에러의 경우 graceful shutdown 후 재시작 권장 +}); + +// SIGTERM 시그널 처리 (Docker/Kubernetes 환경) +process.on("SIGTERM", () => { + logger.info("📴 SIGTERM 시그널 수신, graceful shutdown 시작..."); + // 여기서 연결 풀 정리 등 cleanup 로직 추가 가능 + process.exit(0); +}); + +// SIGINT 시그널 처리 (Ctrl+C) +process.on("SIGINT", () => { + logger.info("📴 SIGINT 시그널 수신, graceful shutdown 시작..."); + process.exit(0); +}); + // 라우터 임포트 import authRoutes from "./routes/authRoutes"; import adminRoutes from "./routes/adminRoutes"; diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index a530cf15..a89e50d1 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -1461,11 +1461,8 @@ async function cleanupMenuRelatedData(menuObjid: number): Promise { [menuObjid] ); - // 4. numbering_rules에서 menu_objid를 NULL로 설정 - await query( - `UPDATE numbering_rules SET menu_objid = NULL WHERE menu_objid = $1`, - [menuObjid] - ); + // 4. numbering_rules: 새 스키마에서는 메뉴와 연결되지 않음 (스킵) + // 새 스키마: table_name + column_name + company_code 기반 // 5. rel_menu_auth에서 관련 권한 삭제 await query( diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index ba690aa5..df0c4f4d 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -344,13 +344,65 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response childGroupIds: groupIdsToDelete }); - // 2. menu_info에서 삭제될 screen_group 참조를 NULL로 정리 + // 2. 삭제될 그룹에 연결된 메뉴 정리 if (groupIdsToDelete.length > 0) { - await client.query(` - UPDATE menu_info - SET screen_group_id = NULL + // 2-1. 삭제할 메뉴 objid 수집 + const menusToDelete = await client.query(` + SELECT objid FROM menu_info WHERE screen_group_id = ANY($1::int[]) - `, [groupIdsToDelete]); + AND company_code = $2 + `, [groupIdsToDelete, targetCompanyCode]); + const menuObjids = menusToDelete.rows.map((r: any) => r.objid); + + if (menuObjids.length > 0) { + // 2-2. screen_menu_assignments에서 해당 메뉴 관련 데이터 삭제 + await client.query(` + DELETE FROM screen_menu_assignments + WHERE menu_objid = ANY($1::bigint[]) + AND company_code = $2 + `, [menuObjids, targetCompanyCode]); + + // 2-3. menu_info에서 해당 메뉴 삭제 + await client.query(` + DELETE FROM menu_info + WHERE screen_group_id = ANY($1::int[]) + AND company_code = $2 + `, [groupIdsToDelete, targetCompanyCode]); + + logger.info("그룹 삭제 시 연결된 메뉴 삭제", { + groupIds: groupIdsToDelete, + deletedMenuCount: menuObjids.length, + companyCode: targetCompanyCode + }); + } + + // 2-4. 해당 회사의 채번 규칙 삭제 (최상위 그룹 삭제 시) + // 삭제되는 그룹이 최상위인지 확인 + const isRootGroup = await client.query( + `SELECT 1 FROM screen_groups WHERE id = $1 AND parent_group_id IS NULL`, + [id] + ); + + if (isRootGroup.rows.length > 0) { + // 최상위 그룹 삭제 시 해당 회사의 채번 규칙도 삭제 + // 먼저 파트 삭제 + await client.query( + `DELETE FROM numbering_rule_parts + WHERE rule_id IN (SELECT rule_id FROM numbering_rules WHERE company_code = $1)`, + [targetCompanyCode] + ); + // 규칙 삭제 + const deletedRules = await client.query( + `DELETE FROM numbering_rules WHERE company_code = $1 RETURNING rule_id`, + [targetCompanyCode] + ); + if (deletedRules.rowCount && deletedRules.rowCount > 0) { + logger.info("그룹 삭제 시 채번 규칙 삭제", { + companyCode: targetCompanyCode, + deletedCount: deletedRules.rowCount + }); + } + } } // 3. screen_groups 삭제 (해당 그룹만 - 하위 그룹은 프론트엔드에서 순차 삭제) diff --git a/backend-node/src/database/db.ts b/backend-node/src/database/db.ts index ae775525..4c249ac3 100644 --- a/backend-node/src/database/db.ts +++ b/backend-node/src/database/db.ts @@ -81,8 +81,26 @@ export const initializePool = (): Pool => { pool.on("error", (err, client) => { console.error("❌ PostgreSQL 연결 풀 에러:", err); + // 연결 풀 에러 발생 시 자동 재연결 시도 + // Pool은 자동으로 연결을 재생성하므로 별도 처리 불필요 + // 다만, 연속 에러 발생 시 알림이 필요할 수 있음 }); + // 연결 풀 상태 체크 (5분마다) + setInterval(() => { + if (pool) { + const status = { + totalCount: pool.totalCount, + idleCount: pool.idleCount, + waitingCount: pool.waitingCount, + }; + // 대기 중인 연결이 많으면 경고 + if (status.waitingCount > 5) { + console.warn("⚠️ PostgreSQL 연결 풀 대기열 증가:", status); + } + } + }, 5 * 60 * 1000); + console.log( `🚀 PostgreSQL 연결 풀 초기화 완료: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}` ); diff --git a/backend-node/src/services/categoryTreeService.ts b/backend-node/src/services/categoryTreeService.ts index 985e671f..9296eed9 100644 --- a/backend-node/src/services/categoryTreeService.ts +++ b/backend-node/src/services/categoryTreeService.ts @@ -89,7 +89,7 @@ class CategoryTreeService { updated_at AS "updatedAt", created_by AS "createdBy", updated_by AS "updatedBy" - FROM category_values_test + FROM category_values WHERE (company_code = $1 OR company_code = '*') AND table_name = $2 AND column_name = $3 @@ -142,7 +142,7 @@ class CategoryTreeService { company_code AS "companyCode", created_at AS "createdAt", updated_at AS "updatedAt" - FROM category_values_test + FROM category_values WHERE (company_code = $1 OR company_code = '*') AND table_name = $2 AND column_name = $3 @@ -184,7 +184,7 @@ class CategoryTreeService { company_code AS "companyCode", created_at AS "createdAt", updated_at AS "updatedAt" - FROM category_values_test + FROM category_values WHERE (company_code = $1 OR company_code = '*') AND value_id = $2 `; @@ -221,7 +221,7 @@ class CategoryTreeService { } const query = ` - INSERT INTO category_values_test ( + INSERT INTO category_values ( table_name, column_name, value_code, value_label, value_order, parent_value_id, depth, path, description, color, icon, is_active, is_default, company_code, created_by, updated_by @@ -334,7 +334,7 @@ class CategoryTreeService { } const query = ` - UPDATE category_values_test + UPDATE category_values SET value_code = COALESCE($3, value_code), value_label = COALESCE($4, value_label), @@ -415,11 +415,11 @@ class CategoryTreeService { // 재귀 CTE를 사용하여 모든 하위 카테고리 수집 const query = ` WITH RECURSIVE category_tree AS ( - SELECT value_id FROM category_values_test + SELECT value_id FROM category_values WHERE parent_value_id = $1 AND (company_code = $2 OR company_code = '*') UNION ALL SELECT cv.value_id - FROM category_values_test cv + FROM category_values cv INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id WHERE cv.company_code = $2 OR cv.company_code = '*' ) @@ -452,7 +452,7 @@ class CategoryTreeService { for (const id of reversedIds) { await pool.query( - `DELETE FROM category_values_test WHERE (company_code = $1 OR company_code = '*') AND value_id = $2`, + `DELETE FROM category_values WHERE (company_code = $1 OR company_code = '*') AND value_id = $2`, [companyCode, id] ); } @@ -479,7 +479,7 @@ class CategoryTreeService { const query = ` SELECT value_id, value_label - FROM category_values_test + FROM category_values WHERE (company_code = $1 OR company_code = '*') AND parent_value_id = $2 `; @@ -488,7 +488,7 @@ class CategoryTreeService { for (const child of result.rows) { const newPath = `${parentPath}/${child.value_label}`; - await pool.query(`UPDATE category_values_test SET path = $1, updated_at = NOW() WHERE value_id = $2`, [ + await pool.query(`UPDATE category_values SET path = $1, updated_at = NOW() WHERE value_id = $2`, [ newPath, child.value_id, ]); @@ -550,7 +550,7 @@ class CategoryTreeService { /** * 전체 카테고리 키 목록 조회 (모든 테이블.컬럼 조합) - * category_values_test 테이블에서 고유한 table_name, column_name 조합을 조회 + * category_values 테이블에서 고유한 table_name, column_name 조합을 조회 * 라벨 정보도 함께 반환 */ async getAllCategoryKeys(companyCode: string): Promise<{ tableName: string; columnName: string; tableLabel: string; columnLabel: string }[]> { @@ -564,7 +564,7 @@ class CategoryTreeService { cv.column_name AS "columnName", COALESCE(tl.table_label, cv.table_name) AS "tableLabel", COALESCE(ttc.column_label, cv.column_name) AS "columnLabel" - FROM category_values_test cv + FROM category_values cv LEFT JOIN table_labels tl ON tl.table_name = cv.table_name LEFT JOIN table_type_columns ttc ON ttc.table_name = cv.table_name AND ttc.column_name = cv.column_name AND ttc.company_code = '*' WHERE cv.company_code = $1 OR cv.company_code = '*' diff --git a/backend-node/src/services/menuCopyService.ts b/backend-node/src/services/menuCopyService.ts index ac049799..e91124af 100644 --- a/backend-node/src/services/menuCopyService.ts +++ b/backend-node/src/services/menuCopyService.ts @@ -851,47 +851,10 @@ export class MenuCopyService { ]); logger.info(` ✅ 메뉴 권한 삭제 완료`); - // 5-4. 채번 규칙 처리 (체크 제약조건 고려) - // scope_type = 'menu'인 채번 규칙: 메뉴 전용이므로 삭제 (파트 포함) - // check_menu_scope_requires_menu_objid 제약: scope_type='menu'이면 menu_objid NOT NULL 필수 - const menuScopedRulesResult = await client.query( - `SELECT rule_id FROM numbering_rules - WHERE menu_objid = ANY($1) AND company_code = $2 AND scope_type = 'menu'`, - [existingMenuIds, targetCompanyCode] - ); - if (menuScopedRulesResult.rows.length > 0) { - const menuScopedRuleIds = menuScopedRulesResult.rows.map( - (r) => r.rule_id - ); - // 채번 규칙 파트 먼저 삭제 - await client.query( - `DELETE FROM numbering_rule_parts WHERE rule_id = ANY($1)`, - [menuScopedRuleIds] - ); - // 채번 규칙 삭제 - await client.query( - `DELETE FROM numbering_rules WHERE rule_id = ANY($1)`, - [menuScopedRuleIds] - ); - logger.info( - ` ✅ 메뉴 전용 채번 규칙 삭제: ${menuScopedRuleIds.length}개` - ); - } - - // scope_type != 'menu'인 채번 규칙: menu_objid만 NULL로 설정 (규칙 보존) - const updatedNumberingRules = await client.query( - `UPDATE numbering_rules - SET menu_objid = NULL - WHERE menu_objid = ANY($1) AND company_code = $2 - AND (scope_type IS NULL OR scope_type != 'menu') - RETURNING rule_id`, - [existingMenuIds, targetCompanyCode] - ); - if (updatedNumberingRules.rowCount && updatedNumberingRules.rowCount > 0) { - logger.info( - ` ✅ 테이블 스코프 채번 규칙 연결 해제: ${updatedNumberingRules.rowCount}개 (데이터 보존됨)` - ); - } + // 5-4. 채번 규칙 처리 (새 스키마에서는 menu_objid 없음 - 스킵) + // 새 numbering_rules 스키마: table_name + column_name + company_code 기반 + // 메뉴와 직접 연결되지 않으므로 메뉴 삭제 시 처리 불필요 + logger.info(` ⏭️ 채번 규칙: 새 스키마에서는 메뉴와 연결되지 않음 (스킵)`); // 5-5. 카테고리 매핑 삭제 (menu_objid가 NOT NULL이므로 NULL 설정 불가) // 카테고리 매핑은 메뉴와 강하게 연결되어 있으므로 함께 삭제 @@ -2590,8 +2553,9 @@ export class MenuCopyService { } /** - * 채번 규칙 복사 (최적화: 배치 조회/삽입) - * 화면 복사 전에 호출되어 numberingRuleId 참조 업데이트에 사용됨 + * 채번 규칙 복사 (새 스키마: table_name + column_name 기반) + * 프론트엔드에서 /numbering-rules/copy-for-company API를 별도 호출하므로 + * 이 함수는 ruleIdMap 생성만 담당 (실제 복제는 numberingRuleService에서 처리) */ private async copyNumberingRulesWithMap( menuObjids: number[], @@ -2600,222 +2564,47 @@ export class MenuCopyService { userId: string, client: PoolClient ): Promise<{ copiedCount: number; ruleIdMap: Map }> { - let copiedCount = 0; const ruleIdMap = new Map(); - if (menuObjids.length === 0) { - return { copiedCount, ruleIdMap }; - } - - // === 최적화: 배치 조회 === - // 1. 모든 원본 채번 규칙 한 번에 조회 - const allRulesResult = await client.query( - `SELECT * FROM numbering_rules WHERE menu_objid = ANY($1)`, - [menuObjids] + // 새 스키마에서는 채번규칙이 메뉴와 직접 연결되지 않음 + // 프론트엔드에서 /numbering-rules/copy-for-company API를 별도 호출 + // 여기서는 기존 규칙 ID를 그대로 매핑 (화면 레이아웃의 numberingRuleId 참조용) + + // 원본 회사의 채번규칙 조회 (company_code 기반) + const sourceRulesResult = await client.query( + `SELECT rule_id, rule_name FROM numbering_rules WHERE company_code = $1`, + [menuObjids.length > 0 ? (await client.query( + `SELECT company_code FROM menu_info WHERE objid = $1`, + [menuObjids[0]] + )).rows[0]?.company_code : null] ); - if (allRulesResult.rows.length === 0) { - logger.info(` 📭 복사할 채번 규칙 없음`); - return { copiedCount, ruleIdMap }; - } - - // 2. 대상 회사에 이미 존재하는 모든 채번 규칙 조회 (원본 ID + 새로 생성될 ID 모두 체크 필요) - const existingRulesResult = await client.query( - `SELECT rule_id FROM numbering_rules WHERE company_code = $1`, + // 대상 회사의 채번규칙 조회 (이름 기준 매핑) + const targetRulesResult = await client.query( + `SELECT rule_id, rule_name FROM numbering_rules WHERE company_code = $1`, [targetCompanyCode] ); - const existingRuleIds = new Set( - existingRulesResult.rows.map((r) => r.rule_id) + + const targetRulesByName = new Map( + targetRulesResult.rows.map((r: any) => [r.rule_name, r.rule_id]) ); - // 3. 복사할 규칙과 스킵할 규칙 분류 - const rulesToCopy: any[] = []; - const originalToNewRuleMap: Array<{ original: string; new: string }> = []; - - // 기존 규칙 중 menu_objid 업데이트가 필요한 규칙들 - const rulesToUpdate: Array<{ ruleId: string; newMenuObjid: number }> = []; - - for (const rule of allRulesResult.rows) { - // 새 rule_id 계산: 회사코드 접두사 제거 후 대상 회사코드 추가 - // 예: COMPANY_10_rule-123 -> rule-123 -> COMPANY_16_rule-123 - // 예: rule-123 -> rule-123 -> COMPANY_16_rule-123 - // 예: WACE_품목코드 -> 품목코드 -> COMPANY_16_품목코드 - let baseName = rule.rule_id; - - // 회사코드 접두사 패턴들을 순서대로 제거 시도 - // 1. COMPANY_숫자_ 패턴 (예: COMPANY_10_) - // 2. 일반 접두사_ 패턴 (예: WACE_) - if (baseName.match(/^COMPANY_\d+_/)) { - baseName = baseName.replace(/^COMPANY_\d+_/, ""); - } else if (baseName.includes("_")) { - baseName = baseName.replace(/^[^_]+_/, ""); - } - - const newRuleId = `${targetCompanyCode}_${baseName}`; - - if (existingRuleIds.has(rule.rule_id)) { - // 원본 ID가 이미 존재 (동일한 ID로 매핑) - ruleIdMap.set(rule.rule_id, rule.rule_id); - - const newMenuObjid = menuIdMap.get(rule.menu_objid); - if (newMenuObjid) { - rulesToUpdate.push({ ruleId: rule.rule_id, newMenuObjid }); - } - logger.info(` ♻️ 채번규칙 이미 존재 (원본 ID): ${rule.rule_id}`); - } else if (existingRuleIds.has(newRuleId)) { - // 새로 생성될 ID가 이미 존재 (기존 규칙으로 매핑) - ruleIdMap.set(rule.rule_id, newRuleId); - - const newMenuObjid = menuIdMap.get(rule.menu_objid); - if (newMenuObjid) { - rulesToUpdate.push({ ruleId: newRuleId, newMenuObjid }); - } - logger.info( - ` ♻️ 채번규칙 이미 존재 (대상 ID): ${rule.rule_id} -> ${newRuleId}` - ); - } else { - // 새로 복사 필요 - ruleIdMap.set(rule.rule_id, newRuleId); - originalToNewRuleMap.push({ original: rule.rule_id, new: newRuleId }); - rulesToCopy.push({ ...rule, newRuleId }); - logger.info(` 📋 채번규칙 복사 예정: ${rule.rule_id} -> ${newRuleId}`); + // 이름 기준으로 매핑 생성 + for (const sourceRule of sourceRulesResult.rows) { + const targetRuleId = targetRulesByName.get(sourceRule.rule_name); + if (targetRuleId) { + ruleIdMap.set(sourceRule.rule_id, targetRuleId); + logger.info(` 🔗 채번규칙 매핑: ${sourceRule.rule_id} -> ${targetRuleId}`); } } - // 4. 배치 INSERT로 채번 규칙 복사 - // menu 스코프인데 menu_objid 매핑이 없는 규칙은 제외 (연결 없이 복제하지 않음) - const validRulesToCopy = rulesToCopy.filter((r) => { - if (r.scope_type === "menu") { - const newMenuObjid = menuIdMap.get(r.menu_objid); - if (newMenuObjid === undefined) { - logger.info(` ⏭️ 채번규칙 "${r.rule_name}" 건너뜀: 메뉴 연결 없음 (원본 menu_objid: ${r.menu_objid})`); - // ruleIdMap에서도 제거 - ruleIdMap.delete(r.rule_id); - return false; // 복제 대상에서 제외 - } - } - return true; - }); - - if (validRulesToCopy.length > 0) { - const ruleValues = validRulesToCopy - .map( - (_, i) => - `($${i * 13 + 1}, $${i * 13 + 2}, $${i * 13 + 3}, $${i * 13 + 4}, $${i * 13 + 5}, $${i * 13 + 6}, $${i * 13 + 7}, $${i * 13 + 8}, $${i * 13 + 9}, NOW(), $${i * 13 + 10}, $${i * 13 + 11}, $${i * 13 + 12}, $${i * 13 + 13})` - ) - .join(", "); - - const ruleParams = validRulesToCopy.flatMap((r) => { - const newMenuObjid = menuIdMap.get(r.menu_objid); - // menu 스코프인 경우 반드시 menu_objid가 있음 (위에서 필터링됨) - const finalMenuObjid = newMenuObjid !== undefined ? newMenuObjid : null; - // scope_type은 원본 유지 (menu 스코프는 반드시 menu_objid가 있으므로) - const finalScopeType = r.scope_type; - - return [ - r.newRuleId, - r.rule_name, - r.description, - r.separator, - r.reset_period, - 0, - r.table_name, - r.column_name, - targetCompanyCode, - userId, - finalMenuObjid, - finalScopeType, - null, - ]; - }); - - await client.query( - `INSERT INTO numbering_rules ( - rule_id, rule_name, description, separator, reset_period, - current_sequence, table_name, column_name, company_code, - created_at, created_by, menu_objid, scope_type, last_generated_date - ) VALUES ${ruleValues}`, - ruleParams - ); - - copiedCount = validRulesToCopy.length; - logger.info(` ✅ 채번 규칙 ${copiedCount}개 복사 (${rulesToCopy.length - validRulesToCopy.length}개 건너뜀)`); - } - - // 4-1. 기존 채번 규칙의 menu_objid 업데이트 (새 메뉴와 연결) - 배치 처리 - if (rulesToUpdate.length > 0) { - // CASE WHEN을 사용한 배치 업데이트 - // menu_objid는 numeric 타입이므로 ::numeric 캐스팅 필요 - const caseWhen = rulesToUpdate - .map( - (_, i) => `WHEN rule_id = $${i * 2 + 1} THEN $${i * 2 + 2}::numeric` - ) - .join(" "); - const ruleIdsForUpdate = rulesToUpdate.map((r) => r.ruleId); - const params = rulesToUpdate.flatMap((r) => [r.ruleId, r.newMenuObjid]); - - await client.query( - `UPDATE numbering_rules - SET menu_objid = CASE ${caseWhen} END, updated_at = NOW() - WHERE rule_id = ANY($${params.length + 1}) AND company_code = $${params.length + 2}`, - [...params, ruleIdsForUpdate, targetCompanyCode] - ); - logger.info( - ` ✅ 기존 채번 규칙 ${rulesToUpdate.length}개 메뉴 연결 갱신` - ); - } - - // 5. 모든 원본 파트 한 번에 조회 (새로 복사한 규칙만 대상) - if (rulesToCopy.length > 0) { - const originalRuleIds = rulesToCopy.map((r) => r.rule_id); - const allPartsResult = await client.query( - `SELECT * FROM numbering_rule_parts - WHERE rule_id = ANY($1) ORDER BY rule_id, part_order`, - [originalRuleIds] - ); - - // 6. 배치 INSERT로 채번 규칙 파트 복사 - if (allPartsResult.rows.length > 0) { - // 원본 rule_id -> 새 rule_id 매핑 - const ruleMapping = new Map( - originalToNewRuleMap.map((m) => [m.original, m.new]) - ); - - const partValues = allPartsResult.rows - .map( - (_, i) => - `($${i * 7 + 1}, $${i * 7 + 2}, $${i * 7 + 3}, $${i * 7 + 4}, $${i * 7 + 5}, $${i * 7 + 6}, $${i * 7 + 7}, NOW())` - ) - .join(", "); - - const partParams = allPartsResult.rows.flatMap((p) => [ - ruleMapping.get(p.rule_id), - p.part_order, - p.part_type, - p.generation_method, - p.auto_config, - p.manual_config, - targetCompanyCode, - ]); - - await client.query( - `INSERT INTO numbering_rule_parts ( - rule_id, part_order, part_type, generation_method, - auto_config, manual_config, company_code, created_at - ) VALUES ${partValues}`, - partParams - ); - - logger.info(` ✅ 채번 규칙 파트 ${allPartsResult.rows.length}개 복사`); - } - } - - logger.info( - `✅ 채번 규칙 복사 완료: ${copiedCount}개, 매핑: ${ruleIdMap.size}개` - ); - return { copiedCount, ruleIdMap }; + logger.info(` 📋 채번규칙 매핑 완료: ${ruleIdMap.size}개`); + + // 실제 복제는 프론트엔드에서 별도 API 호출로 처리됨 + return { copiedCount: 0, ruleIdMap }; } + /** * 카테고리 매핑 + 값 복사 (최적화: 배치 조회) * diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index b5d8fb62..abdfd739 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -65,8 +65,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -88,8 +88,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -199,13 +199,13 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" FROM numbering_rules - WHERE scope_type = 'global' + WHERE 1=1 ORDER BY created_at DESC `; params = []; @@ -222,14 +222,13 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" FROM numbering_rules - WHERE company_code = $1 AND scope_type = 'global' - ORDER BY created_at DESC + WHERE company_code = $1 ORDER BY created_at DESC `; params = [companyCode]; } @@ -284,7 +283,7 @@ class NumberingRuleService { let params: any[]; if (companyCode === "*") { - // 최고 관리자: 모든 규칙 조회 (선택한 메뉴 + 하위 메뉴) + // 최고 관리자: 모든 규칙 조회 query = ` SELECT rule_id AS "ruleId", @@ -296,28 +295,18 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" FROM numbering_rules - WHERE - scope_type = 'global' - OR (scope_type = 'menu' AND menu_objid = ANY($1)) - OR (scope_type = 'table' AND menu_objid = ANY($1)) - ORDER BY - CASE - WHEN scope_type = 'menu' OR (scope_type = 'table' AND menu_objid = ANY($1)) THEN 1 - WHEN scope_type = 'table' THEN 2 - WHEN scope_type = 'global' THEN 3 - END, - created_at DESC + ORDER BY created_at DESC `; - params = [menuAndChildObjids]; - logger.info("최고 관리자: 메뉴 및 하위 메뉴 기반 채번 규칙 조회", { menuAndChildObjids }); + params = []; + logger.info("최고 관리자: 전체 채번 규칙 조회"); } else { - // 일반 회사: 자신의 규칙만 조회 (선택한 메뉴 + 하위 메뉴) + // 일반 회사: 자신의 규칙만 조회 query = ` SELECT rule_id AS "ruleId", @@ -329,28 +318,17 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" FROM numbering_rules WHERE company_code = $1 - AND ( - scope_type = 'global' - OR (scope_type = 'menu' AND menu_objid = ANY($2)) - OR (scope_type = 'table' AND menu_objid = ANY($2)) - ) - ORDER BY - CASE - WHEN scope_type = 'menu' OR (scope_type = 'table' AND menu_objid = ANY($2)) THEN 1 - WHEN scope_type = 'table' THEN 2 - WHEN scope_type = 'global' THEN 3 - END, - created_at DESC + ORDER BY created_at DESC `; - params = [companyCode, menuAndChildObjids]; - logger.info("회사별: 메뉴 및 하위 메뉴 기반 채번 규칙 조회", { companyCode, menuAndChildObjids }); + params = [companyCode]; + logger.info("회사별 채번 규칙 조회", { companyCode }); } logger.info("🔍 채번 규칙 쿼리 실행", { @@ -475,8 +453,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -500,8 +478,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -577,8 +555,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -599,8 +577,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -676,7 +654,7 @@ class NumberingRuleService { INSERT INTO numbering_rules ( rule_id, rule_name, description, separator, reset_period, current_sequence, table_name, column_name, company_code, - menu_objid, scope_type, created_by + category_column, category_value_id, created_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING rule_id AS "ruleId", @@ -688,8 +666,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -705,8 +683,8 @@ class NumberingRuleService { config.tableName || null, config.columnName || null, companyCode, - config.menuObjid || null, - config.scopeType || "global", + config.categoryColumn || null, + config.categoryValueId || null, userId, ]); @@ -778,8 +756,8 @@ class NumberingRuleService { reset_period = COALESCE($4, reset_period), table_name = COALESCE($5, table_name), column_name = COALESCE($6, column_name), - menu_objid = COALESCE($7, menu_objid), - scope_type = COALESCE($8, scope_type), + category_column = COALESCE($7, category_column), + category_value_id = COALESCE($8, category_value_id), updated_at = NOW() WHERE rule_id = $9 AND company_code = $10 RETURNING @@ -792,8 +770,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -806,8 +784,8 @@ class NumberingRuleService { updates.resetPeriod, updates.tableName, updates.columnName, - updates.menuObjid, - updates.scopeType, + updates.categoryColumn, + updates.categoryValueId, ruleId, companyCode, ]); @@ -1198,7 +1176,7 @@ class NumberingRuleService { /** * [테스트] 테스트 테이블에서 채번 규칙 목록 조회 - * numbering_rules_test 테이블 사용 + * numbering_rules 테이블 사용 */ async getRulesFromTest( companyCode: string, @@ -1231,7 +1209,7 @@ class NumberingRuleService { created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" - FROM numbering_rules_test + FROM numbering_rules ORDER BY created_at DESC `; params = []; @@ -1253,7 +1231,7 @@ class NumberingRuleService { created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" - FROM numbering_rules_test + FROM numbering_rules WHERE company_code = $1 ORDER BY created_at DESC `; @@ -1272,7 +1250,7 @@ class NumberingRuleService { generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" - FROM numbering_rule_parts_test + FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2 ORDER BY part_order `; @@ -1300,8 +1278,8 @@ class NumberingRuleService { } /** - * [테스트] 테이블명 + 컬럼명 기반으로 채번규칙 조회 (menu_objid 없이) - * numbering_rules_test 테이블 사용 + * 테이블명 + 컬럼명 기반으로 채번규칙 조회 + * numbering_rules 테이블 사용 */ async getNumberingRuleByColumn( companyCode: string, @@ -1333,8 +1311,8 @@ class NumberingRuleService { r.created_at AS "createdAt", r.updated_at AS "updatedAt", r.created_by AS "createdBy" - FROM numbering_rules_test r - LEFT JOIN category_values_test cv ON r.category_value_id = cv.value_id + FROM numbering_rules r + LEFT JOIN category_values cv ON r.category_value_id = cv.value_id WHERE r.company_code = $1 AND r.table_name = $2 AND r.column_name = $3 @@ -1365,7 +1343,7 @@ class NumberingRuleService { generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" - FROM numbering_rule_parts_test + FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2 ORDER BY part_order `; @@ -1391,7 +1369,7 @@ class NumberingRuleService { /** * [테스트] 테스트 테이블에 채번규칙 저장 - * numbering_rules_test 테이블 사용 + * numbering_rules 테이블 사용 */ async saveRuleToTest( config: NumberingRuleConfig, @@ -1414,7 +1392,7 @@ class NumberingRuleService { // 기존 규칙 확인 const existingQuery = ` - SELECT rule_id FROM numbering_rules_test + SELECT rule_id FROM numbering_rules WHERE rule_id = $1 AND company_code = $2 `; const existingResult = await client.query(existingQuery, [config.ruleId, companyCode]); @@ -1422,7 +1400,7 @@ class NumberingRuleService { if (existingResult.rows.length > 0) { // 업데이트 const updateQuery = ` - UPDATE numbering_rules_test SET + UPDATE numbering_rules SET rule_name = $1, description = $2, separator = $3, @@ -1449,13 +1427,13 @@ class NumberingRuleService { // 기존 파트 삭제 await client.query( - "DELETE FROM numbering_rule_parts_test WHERE rule_id = $1 AND company_code = $2", + "DELETE FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2", [config.ruleId, companyCode] ); } else { // 신규 등록 const insertQuery = ` - INSERT INTO numbering_rules_test ( + INSERT INTO numbering_rules ( rule_id, rule_name, description, separator, reset_period, current_sequence, table_name, column_name, company_code, category_column, category_value_id, @@ -1482,7 +1460,7 @@ class NumberingRuleService { if (config.parts && config.parts.length > 0) { for (const part of config.parts) { const partInsertQuery = ` - INSERT INTO numbering_rule_parts_test ( + INSERT INTO numbering_rule_parts ( rule_id, part_order, part_type, generation_method, auto_config, manual_config, company_code, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) @@ -1523,7 +1501,7 @@ class NumberingRuleService { /** * [테스트] 테스트 테이블에서 채번규칙 삭제 - * numbering_rules_test 테이블 사용 + * numbering_rules 테이블 사용 */ async deleteRuleFromTest(ruleId: string, companyCode: string): Promise { const pool = getPool(); @@ -1536,13 +1514,13 @@ class NumberingRuleService { // 파트 먼저 삭제 await client.query( - "DELETE FROM numbering_rule_parts_test WHERE rule_id = $1 AND company_code = $2", + "DELETE FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2", [ruleId, companyCode] ); // 규칙 삭제 const result = await client.query( - "DELETE FROM numbering_rules_test WHERE rule_id = $1 AND company_code = $2", + "DELETE FROM numbering_rules WHERE rule_id = $1 AND company_code = $2", [ruleId, companyCode] ); @@ -1608,8 +1586,8 @@ class NumberingRuleService { r.created_at AS "createdAt", r.updated_at AS "updatedAt", r.created_by AS "createdBy" - FROM numbering_rules_test r - LEFT JOIN category_values_test cv ON r.category_value_id = cv.value_id + FROM numbering_rules r + LEFT JOIN category_values cv ON r.category_value_id = cv.value_id WHERE r.company_code = $1 AND r.table_name = $2 AND r.column_name = $3 @@ -1636,7 +1614,7 @@ class NumberingRuleService { generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" - FROM numbering_rule_parts_test + FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2 ORDER BY part_order `; @@ -1668,7 +1646,7 @@ class NumberingRuleService { r.created_at AS "createdAt", r.updated_at AS "updatedAt", r.created_by AS "createdBy" - FROM numbering_rules_test r + FROM numbering_rules r WHERE r.company_code = $1 AND r.table_name = $2 AND r.column_name = $3 @@ -1688,7 +1666,7 @@ class NumberingRuleService { generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" - FROM numbering_rule_parts_test + FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2 ORDER BY part_order `; @@ -1745,8 +1723,8 @@ class NumberingRuleService { r.created_at AS "createdAt", r.updated_at AS "updatedAt", r.created_by AS "createdBy" - FROM numbering_rules_test r - LEFT JOIN category_values_test cv ON r.category_value_id = cv.value_id + FROM numbering_rules r + LEFT JOIN category_values cv ON r.category_value_id = cv.value_id WHERE r.company_code = $1 AND r.table_name = $2 AND r.column_name = $3 @@ -1764,7 +1742,7 @@ class NumberingRuleService { generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" - FROM numbering_rule_parts_test + FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2 ORDER BY part_order `; @@ -1783,7 +1761,7 @@ class NumberingRuleService { /** * 회사별 채번규칙 복제 (테이블 기반) - * numbering_rules_test, numbering_rule_parts_test 테이블 사용 + * numbering_rules, numbering_rule_parts 테이블 사용 * 복제 후 화면 레이아웃의 numberingRuleId 참조도 업데이트 */ async copyRulesForCompany( @@ -1798,9 +1776,28 @@ class NumberingRuleService { try { await client.query("BEGIN"); - // 1. 원본 회사의 채번규칙 조회 - numbering_rules_test 사용 + // 0. 대상 회사의 기존 채번규칙 삭제 (깨끗하게 복제하기 위해) + // 먼저 파트 삭제 + await client.query( + `DELETE FROM numbering_rule_parts + WHERE rule_id IN (SELECT rule_id FROM numbering_rules WHERE company_code = $1)`, + [targetCompanyCode] + ); + // 규칙 삭제 + const deleteResult = await client.query( + `DELETE FROM numbering_rules WHERE company_code = $1 RETURNING rule_id`, + [targetCompanyCode] + ); + if (deleteResult.rowCount && deleteResult.rowCount > 0) { + logger.info("기존 채번규칙 삭제", { + targetCompanyCode, + deletedCount: deleteResult.rowCount + }); + } + + // 1. 원본 회사의 채번규칙 조회 - numbering_rules 사용 const sourceRulesResult = await client.query( - `SELECT * FROM numbering_rules_test WHERE company_code = $1`, + `SELECT * FROM numbering_rules WHERE company_code = $1`, [sourceCompanyCode] ); @@ -1814,9 +1811,9 @@ class NumberingRuleService { // 새 rule_id 생성 const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - // 이미 존재하는지 확인 (이름 기반) - numbering_rules_test 사용 + // 이미 존재하는지 확인 (이름 기반) - numbering_rules 사용 const existsCheck = await client.query( - `SELECT rule_id FROM numbering_rules_test + `SELECT rule_id FROM numbering_rules WHERE company_code = $1 AND rule_name = $2`, [targetCompanyCode, rule.rule_name] ); @@ -1829,9 +1826,9 @@ class NumberingRuleService { continue; } - // 채번규칙 복제 - numbering_rules_test 사용 + // 채번규칙 복제 - numbering_rules 사용 await client.query( - `INSERT INTO numbering_rules_test ( + `INSERT INTO numbering_rules ( rule_id, rule_name, description, separator, reset_period, current_sequence, table_name, column_name, company_code, created_at, updated_at, created_by, category_column, category_value_id @@ -1852,15 +1849,15 @@ class NumberingRuleService { ] ); - // 채번규칙 파트 복제 - numbering_rule_parts_test 사용 + // 채번규칙 파트 복제 - numbering_rule_parts 사용 const partsResult = await client.query( - `SELECT * FROM numbering_rule_parts_test WHERE rule_id = $1 ORDER BY part_order`, + `SELECT * FROM numbering_rule_parts WHERE rule_id = $1 ORDER BY part_order`, [rule.rule_id] ); for (const part of partsResult.rows) { await client.query( - `INSERT INTO numbering_rule_parts_test ( + `INSERT INTO numbering_rule_parts ( rule_id, part_order, part_type, generation_method, auto_config, manual_config, company_code, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())`, diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 8cd6d4e0..b201f567 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -635,7 +635,76 @@ export class ScreenManagementService { // 트랜잭션으로 화면 삭제와 메뉴 할당 정리를 함께 처리 (Raw Query) await transaction(async (client) => { - // 소프트 삭제 (휴지통으로 이동) + // 1. 화면에서 사용하는 flowId 수집 (V2 레이아웃) + const layoutResult = await client.query<{ layout_data: any }>( + `SELECT layout_data FROM screen_layouts_v2 + WHERE screen_id = $1 AND (company_code = $2 OR company_code = '*') + ORDER BY CASE WHEN company_code = $2 THEN 0 ELSE 1 END + LIMIT 1`, + [screenId, userCompanyCode], + ); + + const layoutData = layoutResult.rows[0]?.layout_data; + const flowIds = this.collectFlowIdsFromLayoutData(layoutData); + + // 2. 각 flowId가 다른 화면에서도 사용되는지 체크 후 삭제 + if (flowIds.size > 0) { + for (const flowId of flowIds) { + // 다른 화면에서 사용 중인지 확인 (같은 회사 내, 삭제되지 않은 화면 기준) + const companyFilterForCheck = userCompanyCode === "*" ? "" : " AND sd.company_code = $3"; + const checkParams = userCompanyCode === "*" + ? [screenId, flowId] + : [screenId, flowId, userCompanyCode]; + + const otherUsageResult = await client.query<{ count: string }>( + `SELECT COUNT(*) as count FROM screen_layouts_v2 slv + JOIN screen_definitions sd ON slv.screen_id = sd.screen_id + WHERE slv.screen_id != $1 + AND sd.is_active != 'D' + ${companyFilterForCheck} + AND ( + slv.layout_data::text LIKE '%"flowId":' || $2 || '%' + OR slv.layout_data::text LIKE '%"flowId":"' || $2 || '"%' + )`, + checkParams, + ); + + const otherUsageCount = parseInt(otherUsageResult.rows[0]?.count || "0"); + + // 다른 화면에서 사용하지 않는 경우에만 플로우 삭제 + if (otherUsageCount === 0) { + // 해당 회사의 플로우만 삭제 (멀티테넌시) + const companyFilter = userCompanyCode === "*" ? "" : " AND company_code = $2"; + const flowParams = userCompanyCode === "*" ? [flowId] : [flowId, userCompanyCode]; + + // 1. flow_definition 관련 데이터 먼저 삭제 (외래키 순서) + await client.query( + `DELETE FROM flow_step_connection WHERE flow_definition_id = $1`, + [flowId], + ); + await client.query( + `DELETE FROM flow_step WHERE flow_definition_id = $1`, + [flowId], + ); + await client.query( + `DELETE FROM flow_definition WHERE id = $1${companyFilter}`, + flowParams, + ); + + // 2. node_flows 테이블에서도 삭제 (제어플로우) + await client.query( + `DELETE FROM node_flows WHERE flow_id = $1${companyFilter}`, + flowParams, + ); + + logger.info("화면 삭제 시 플로우 삭제 (flow_definition + node_flows)", { screenId, flowId, companyCode: userCompanyCode }); + } else { + logger.debug("플로우가 다른 화면에서 사용 중 - 삭제 스킵", { screenId, flowId, otherUsageCount }); + } + } + } + + // 3. 소프트 삭제 (휴지통으로 이동) await client.query( `UPDATE screen_definitions SET is_active = 'D', @@ -655,7 +724,7 @@ export class ScreenManagementService { ], ); - // 메뉴 할당도 비활성화 + // 4. 메뉴 할당도 비활성화 await client.query( `UPDATE screen_menu_assignments SET is_active = 'N' @@ -2946,7 +3015,7 @@ export class ScreenManagementService { * - current_sequence는 0으로 초기화 */ /** - * 채번 규칙 복제 (numbering_rules_test 테이블 사용) + * 채번 규칙 복제 (numbering_rules 테이블 사용) * - menu_objid 의존성 제거됨 * - table_name + column_name + company_code 기반 */ @@ -2964,10 +3033,10 @@ export class ScreenManagementService { console.log(`🔄 채번 규칙 복사 시작: ${ruleIds.size}개 규칙`); - // 1. 원본 채번 규칙 조회 (numbering_rules_test 테이블) + // 1. 원본 채번 규칙 조회 (numbering_rules 테이블) const ruleIdArray = Array.from(ruleIds); const sourceRulesResult = await client.query( - `SELECT * FROM numbering_rules_test WHERE rule_id = ANY($1)`, + `SELECT * FROM numbering_rules WHERE rule_id = ANY($1)`, [ruleIdArray], ); @@ -2980,7 +3049,7 @@ export class ScreenManagementService { // 2. 대상 회사의 기존 채번 규칙 조회 (이름 기준) const existingRulesResult = await client.query( - `SELECT rule_id, rule_name FROM numbering_rules_test WHERE company_code = $1`, + `SELECT rule_id, rule_name FROM numbering_rules WHERE company_code = $1`, [targetCompanyCode], ); const existingRulesByName = new Map( @@ -3001,9 +3070,9 @@ export class ScreenManagementService { // 새로 복사 - 새 rule_id 생성 const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - // numbering_rules_test 복사 (current_sequence = 0으로 초기화) + // numbering_rules 복사 (current_sequence = 0으로 초기화) await client.query( - `INSERT INTO numbering_rules_test ( + `INSERT INTO numbering_rules ( rule_id, rule_name, description, separator, reset_period, current_sequence, table_name, column_name, company_code, created_at, updated_at, created_by, last_generated_date, @@ -3028,15 +3097,15 @@ export class ScreenManagementService { ], ); - // numbering_rule_parts_test 복사 + // numbering_rule_parts 복사 const partsResult = await client.query( - `SELECT * FROM numbering_rule_parts_test WHERE rule_id = $1 ORDER BY part_order`, + `SELECT * FROM numbering_rule_parts WHERE rule_id = $1 ORDER BY part_order`, [rule.rule_id], ); for (const part of partsResult.rows) { await client.query( - `INSERT INTO numbering_rule_parts_test ( + `INSERT INTO numbering_rule_parts ( rule_id, part_order, part_type, generation_method, auto_config, manual_config, company_code, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, @@ -4542,7 +4611,8 @@ export class ScreenManagementService { ); if (menuInfo.rows.length > 0) { - const isAdminMenu = menuInfo.rows[0].menu_type === "1"; + // menu_type: "0" = 관리자 메뉴, "1" = 사용자 메뉴 + const isAdminMenu = menuInfo.rows[0].menu_type === "0"; const newMenuUrl = isAdminMenu ? `/screens/${newScreenId}?mode=admin` : `/screens/${newScreenId}`; @@ -4707,7 +4777,7 @@ export class ScreenManagementService { } /** - * 카테고리 값 복제 (category_values_test 테이블 사용) + * 카테고리 값 복제 (category_values 테이블 사용) * - menu_objid 의존성 제거됨 * - table_name + column_name + company_code 기반 */ @@ -4741,13 +4811,13 @@ export class ScreenManagementService { // 1. 기존 대상 회사 데이터 삭제 (다른 회사로 복제 시에만) await client.query( - `DELETE FROM category_values_test WHERE company_code = $1`, + `DELETE FROM category_values WHERE company_code = $1`, [targetCompanyCode], ); - // 2. category_values_test 복제 + // 2. category_values 복제 const values = await client.query( - `SELECT * FROM category_values_test WHERE company_code = $1`, + `SELECT * FROM category_values WHERE company_code = $1`, [sourceCompanyCode], ); @@ -4756,7 +4826,7 @@ export class ScreenManagementService { for (const v of values.rows) { const insertResult = await client.query( - `INSERT INTO category_values_test + `INSERT INTO category_values (table_name, column_name, value_code, value_label, value_order, parent_value_id, depth, path, description, color, icon, is_active, is_default, company_code, created_by) @@ -4791,7 +4861,7 @@ export class ScreenManagementService { const newValueId = valueIdMap.get(v.value_id); if (newParentId && newValueId) { await client.query( - `UPDATE category_values_test SET parent_value_id = $1 WHERE value_id = $2`, + `UPDATE category_values SET parent_value_id = $1 WHERE value_id = $2`, [newParentId, newValueId], ); } diff --git a/backend-node/src/services/tableCategoryValueService.ts b/backend-node/src/services/tableCategoryValueService.ts index c4149147..2eb35f64 100644 --- a/backend-node/src/services/tableCategoryValueService.ts +++ b/backend-node/src/services/tableCategoryValueService.ts @@ -212,22 +212,22 @@ class TableCategoryValueService { updated_at AS "updatedAt", created_by AS "createdBy", updated_by AS "updatedBy" - FROM category_values_test + FROM category_values WHERE table_name = $1 AND column_name = $2 `; - // category_values_test 테이블 사용 (menu_objid 없음) + // category_values 테이블 사용 (menu_objid 없음) if (companyCode === "*") { // 최고 관리자: 모든 값 조회 query = baseSelect; params = [tableName, columnName]; - logger.info("최고 관리자 전체 카테고리 값 조회 (category_values_test)"); + logger.info("최고 관리자 전체 카테고리 값 조회 (category_values)"); } else { // 일반 회사: 자신의 회사 또는 공통(*) 카테고리 조회 query = baseSelect + ` AND (company_code = $3 OR company_code = '*')`; params = [tableName, columnName, companyCode]; - logger.info("회사별 카테고리 값 조회 (category_values_test)", { companyCode }); + logger.info("회사별 카테고리 값 조회 (category_values)", { companyCode }); } if (!includeInactive) { diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx index 57e4896b..2ab42e75 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -108,7 +108,7 @@ export const NumberingRuleDesigner: React.FC = ({ // 전체 카테고리 옵션 로드 (모든 테이블의 category 타입 컬럼) const loadAllCategoryOptions = async () => { try { - // category_values_test 테이블에서 고유한 테이블.컬럼 조합 조회 + // category_values 테이블에서 고유한 테이블.컬럼 조합 조회 const response = await getAllCategoryKeys(); if (response.success && response.data) { const options: CategoryOption[] = response.data.map((item) => ({ @@ -341,7 +341,7 @@ export const NumberingRuleDesigner: React.FC = ({ ruleToSave, }); - // 테스트 테이블에 저장 (numbering_rules_test) + // 테스트 테이블에 저장 (numbering_rules) const response = await saveNumberingRuleToTest(ruleToSave); if (response.success && response.data) { diff --git a/frontend/components/screen/CopyScreenModal.tsx b/frontend/components/screen/CopyScreenModal.tsx index 24f8231e..070a0ce6 100644 --- a/frontend/components/screen/CopyScreenModal.tsx +++ b/frontend/components/screen/CopyScreenModal.tsx @@ -253,6 +253,24 @@ export default function CopyScreenModal({ } }, [useBulkRename, removeText, addPrefix]); + // 원본 회사가 선택된 경우 다른 회사로 자동 변경 + useEffect(() => { + if (!companies.length || !isOpen) return; + + const sourceCompanyCode = mode === "group" + ? sourceGroup?.company_code + : sourceScreen?.companyCode; + + // 원본 회사와 같은 회사가 선택되어 있으면 다른 회사로 변경 + if (sourceCompanyCode && targetCompanyCode === sourceCompanyCode) { + const otherCompany = companies.find(c => c.companyCode !== sourceCompanyCode); + if (otherCompany) { + console.log("🔄 원본 회사 선택됨 → 다른 회사로 자동 변경:", otherCompany.companyCode); + setTargetCompanyCode(otherCompany.companyCode); + } + } + }, [companies, isOpen, mode, sourceGroup, sourceScreen, targetCompanyCode]); + // 대상 회사 변경 시 기존 코드 초기화 useEffect(() => { if (targetCompanyCode) { @@ -1182,31 +1200,36 @@ export default function CopyScreenModal({ // 그룹 복제 모드 렌더링 if (mode === "group") { return ( - - - {/* 로딩 오버레이 */} - {isCopying && ( -
- -

{copyProgress.message}

+ <> + {/* 로딩 오버레이 - Dialog 바깥에서 화면 전체 고정 */} + {isCopying && ( +
+
+ +

{copyProgress.message}

{copyProgress.total > 0 && ( <> -
+
-

- {copyProgress.current} / {copyProgress.total} 화면 +

+ {copyProgress.current} / {copyProgress.total} 화면 복제 중...

)} +

+ 복제가 완료될 때까지 잠시 기다려주세요 +

- )} - - - +
+ )} + + + + 그룹 복제 @@ -1486,15 +1509,22 @@ export default function CopyScreenModal({ onChange={(e) => setTargetCompanyCode(e.target.value)} className="mt-1 flex h-8 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:text-sm" > - {companies.map((company) => ( - - ))} + {companies + .filter((company) => company.companyCode !== sourceGroup?.company_code) + .map((company) => ( + + ))}

복제된 그룹과 화면이 이 회사에 생성됩니다

+ {sourceGroup && ( +

+ * 원본 회사({sourceGroup.company_code})로는 복제할 수 없습니다 +

+ )}
)} @@ -1590,14 +1620,25 @@ export default function CopyScreenModal({
+ ); } // 화면 복제 모드 렌더링 return ( - - - + <> + {/* 로딩 오버레이 - Dialog 바깥에서 화면 전체 고정 */} + {isCopying && ( +
+
+ +

복제가 완료될 때까지 잠시 기다려주세요

+
+
+ )} + + + 화면 복제 "{sourceScreen?.screenName}" 화면을 복제합니다. @@ -1694,13 +1735,20 @@ export default function CopyScreenModal({ - {companies.map((company) => ( - - {company.companyName} - - ))} + {companies + .filter((company) => company.companyCode !== sourceScreen?.companyCode) + .map((company) => ( + + {company.companyName} + + ))} + {sourceScreen && ( +

+ * 원본 회사({sourceScreen.companyCode})로는 복제할 수 없습니다 +

+ )} )} @@ -1840,6 +1888,7 @@ export default function CopyScreenModal({
+ ); } diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index e89225a3..1aa47f0d 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -18,6 +18,7 @@ import { Loader2, RefreshCw, Building2, + AlertTriangle, } from "lucide-react"; import { ScreenDefinition } from "@/types/screen"; import { @@ -1463,16 +1464,26 @@ export function ScreenGroupTreeView({ {/* 그룹 삭제 확인 다이얼로그 */} - + - 그룹 삭제 - - "{deletingGroup?.group_name}" 그룹을 삭제하시겠습니까? -
- {deleteScreensWithGroup - ? 그룹에 속한 화면들도 함께 삭제됩니다. - : "그룹에 속한 화면들은 미분류로 이동됩니다." - } + + + 그룹 삭제 경고 + + +
+
+

+ "{deletingGroup?.group_name}" 그룹을 정말 삭제하시겠습니까? +

+

+ {deleteScreensWithGroup + ? "⚠️ 그룹에 속한 모든 화면, 플로우, 관련 데이터가 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다." + : "그룹에 속한 화면들은 미분류로 이동됩니다." + } +

+
+
@@ -1570,11 +1581,21 @@ export function ScreenGroupTreeView({ )} - 화면 삭제 - - "{deletingScreen?.screenName}" 화면을 삭제하시겠습니까? -
- 삭제된 화면은 휴지통으로 이동됩니다. + + + 화면 삭제 경고 + + +
+
+

+ "{deletingScreen?.screenName}" 화면을 정말 삭제하시겠습니까? +

+

+ ⚠️ 화면과 연결된 플로우, 레이아웃 데이터가 모두 삭제됩니다. 삭제된 화면은 휴지통으로 이동됩니다. +

+
+
diff --git a/frontend/components/unified/UnifiedSelect.tsx b/frontend/components/unified/UnifiedSelect.tsx index 99a82e17..9560fa38 100644 --- a/frontend/components/unified/UnifiedSelect.tsx +++ b/frontend/components/unified/UnifiedSelect.tsx @@ -492,7 +492,7 @@ export const UnifiedSelect = forwardRef((pro const categoryTable = (config as any).categoryTable; const categoryColumn = (config as any).categoryColumn; - // category 소스 유지 (category_values_test 테이블에서 로드) + // category 소스 유지 (category_values 테이블에서 로드) const source = rawSource; const codeGroup = config.codeGroup; @@ -612,7 +612,7 @@ export const UnifiedSelect = forwardRef((pro fetchedOptions = data; } } else if (source === "category") { - // 카테고리에서 로드 (category_values_test 테이블) + // 카테고리에서 로드 (category_values 테이블) // tableName, columnName은 props에서 가져옴 const catTable = categoryTable || tableName; const catColumn = categoryColumn || columnName; diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx index 31c5ba2a..872945f9 100644 --- a/frontend/components/v2/V2Select.tsx +++ b/frontend/components/v2/V2Select.tsx @@ -470,7 +470,7 @@ export const V2Select = forwardRef( const categoryTable = (config as any).categoryTable; const categoryColumn = (config as any).categoryColumn; - // category 소스 유지 (category_values_test 테이블에서 로드) + // category 소스 유지 (category_values 테이블에서 로드) const source = rawSource; const codeGroup = config.codeGroup; @@ -590,7 +590,7 @@ export const V2Select = forwardRef( fetchedOptions = data; } } else if (source === "category") { - // 카테고리에서 로드 (category_values_test 테이블) + // 카테고리에서 로드 (category_values 테이블) // tableName, columnName은 props에서 가져옴 const catTable = categoryTable || tableName; const catColumn = categoryColumn || columnName; diff --git a/frontend/lib/api/numberingRule.ts b/frontend/lib/api/numberingRule.ts index 8b7f47bd..3a9b7930 100644 --- a/frontend/lib/api/numberingRule.ts +++ b/frontend/lib/api/numberingRule.ts @@ -173,11 +173,11 @@ export async function resetSequence(ruleId: string): Promise> } } -// ====== 테스트용 API (numbering_rules_test 테이블 사용) ====== +// ====== 테스트용 API (numbering_rules 테이블 사용) ====== /** * [테스트] 테스트 테이블에서 채번규칙 목록 조회 - * numbering_rules_test 테이블 사용 + * numbering_rules 테이블 사용 * @param menuObjid 메뉴 OBJID (선택) - 필터링용 */ export async function getNumberingRulesFromTest( @@ -199,7 +199,7 @@ export async function getNumberingRulesFromTest( /** * [테스트] 테이블+컬럼 기반 채번규칙 조회 - * numbering_rules_test 테이블 사용 + * numbering_rules 테이블 사용 */ export async function getNumberingRuleByColumn( tableName: string, @@ -220,7 +220,7 @@ export async function getNumberingRuleByColumn( /** * [테스트] 테스트 테이블에 채번규칙 저장 - * numbering_rules_test 테이블 사용 + * numbering_rules 테이블 사용 */ export async function saveNumberingRuleToTest( config: NumberingRuleConfig @@ -238,7 +238,7 @@ export async function saveNumberingRuleToTest( /** * [테스트] 테스트 테이블에서 채번규칙 삭제 - * numbering_rules_test 테이블 사용 + * numbering_rules 테이블 사용 */ export async function deleteNumberingRuleFromTest( ruleId: string diff --git a/frontend/types/numbering-rule.ts b/frontend/types/numbering-rule.ts index b788814c..7f21fa44 100644 --- a/frontend/types/numbering-rule.ts +++ b/frontend/types/numbering-rule.ts @@ -109,7 +109,7 @@ export interface NumberingRuleConfig { // 카테고리 조건 (특정 카테고리 값일 때만 이 규칙 적용) categoryColumn?: string; // 카테고리 조건 컬럼명 (예: 'type', 'material') - categoryValueId?: number; // 카테고리 값 ID (category_values_test.value_id) + categoryValueId?: number; // 카테고리 값 ID (category_values.value_id) categoryValueLabel?: string; // 카테고리 값 라벨 (조회 시 조인) // 메타 정보