Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into feature/v2-unified-renewal
This commit is contained in:
commit
dd1ddd6418
|
|
@ -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<any>) => {
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -1461,11 +1461,8 @@ async function cleanupMenuRelatedData(menuObjid: number): Promise<void> {
|
|||
[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(
|
||||
|
|
|
|||
|
|
@ -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 삭제 (해당 그룹만 - 하위 그룹은 프론트엔드에서 순차 삭제)
|
||||
|
|
|
|||
|
|
@ -557,7 +557,16 @@ export async function updateColumnInputType(
|
|||
): Promise<void> {
|
||||
try {
|
||||
const { tableName, columnName } = req.params;
|
||||
const { inputType, detailSettings } = req.body;
|
||||
let { inputType, detailSettings } = req.body;
|
||||
|
||||
// 🔥 "direct" 또는 "auto"는 프론트엔드의 입력 방식 구분값이므로
|
||||
// DB의 input_type(웹타입)으로 저장하면 안 됨 - "text"로 변환
|
||||
if (inputType === "direct" || inputType === "auto") {
|
||||
logger.warn(
|
||||
`잘못된 inputType 값 감지: ${inputType} → 'text'로 변환 (${tableName}.${columnName})`
|
||||
);
|
||||
inputType = "text";
|
||||
}
|
||||
|
||||
// 🔥 회사 코드 추출 (JWT에서 또는 DB에서 조회)
|
||||
let companyCode = req.user?.companyCode;
|
||||
|
|
@ -1360,8 +1369,17 @@ export async function updateColumnWebType(
|
|||
`레거시 API 사용: updateColumnWebType → updateColumnInputType 사용 권장`
|
||||
);
|
||||
|
||||
// webType을 inputType으로 변환
|
||||
const convertedInputType = inputType || webType || "text";
|
||||
// 🔥 inputType이 "direct" 또는 "auto"이면 무시하고 webType 사용
|
||||
// "direct"/"auto"는 프론트엔드의 입력 방식(직접입력/자동입력) 구분값이지
|
||||
// DB에 저장할 웹 타입(text, number, date 등)이 아님
|
||||
let convertedInputType = webType || "text";
|
||||
if (inputType && inputType !== "direct" && inputType !== "auto") {
|
||||
convertedInputType = inputType;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`웹타입 변환: webType=${webType}, inputType=${inputType} → ${convertedInputType}`
|
||||
);
|
||||
|
||||
// 새로운 메서드 호출
|
||||
req.body = { inputType: convertedInputType, detailSettings };
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 = '*'
|
||||
|
|
|
|||
|
|
@ -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<string, string> }> {
|
||||
let copiedCount = 0;
|
||||
const ruleIdMap = new Map<string, string>();
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 카테고리 매핑 + 값 복사 (최적화: 배치 조회)
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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())`,
|
||||
|
|
|
|||
|
|
@ -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<string, string>(
|
||||
|
|
@ -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],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -473,6 +473,15 @@ export class TableManagementService {
|
|||
`컬럼 설정 업데이트 시작: ${tableName}.${columnName}, company: ${companyCode}`
|
||||
);
|
||||
|
||||
// 🔥 "direct" 또는 "auto"는 프론트엔드의 입력 방식 구분값이므로
|
||||
// DB의 input_type(웹타입)으로 저장하면 안 됨 - "text"로 변환
|
||||
if (settings.inputType === "direct" || settings.inputType === "auto") {
|
||||
logger.warn(
|
||||
`잘못된 inputType 값 감지: ${settings.inputType} → 'text'로 변환 (${tableName}.${columnName})`
|
||||
);
|
||||
settings.inputType = "text";
|
||||
}
|
||||
|
||||
// 테이블이 table_labels에 없으면 자동 추가
|
||||
await this.insertTableIfNotExists(tableName);
|
||||
|
||||
|
|
@ -725,12 +734,22 @@ export class TableManagementService {
|
|||
inputType?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 🔥 'direct'나 'auto'는 프론트엔드의 입력 방식 구분값이므로
|
||||
// DB의 input_type(웹타입)으로 저장하면 안 됨 - 'text'로 변환
|
||||
let finalWebType = webType;
|
||||
if (webType === "direct" || webType === "auto") {
|
||||
logger.warn(
|
||||
`잘못된 webType 값 감지: ${webType} → 'text'로 변환 (${tableName}.${columnName})`
|
||||
);
|
||||
finalWebType = "text";
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${webType}`
|
||||
`컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${finalWebType}`
|
||||
);
|
||||
|
||||
// 웹 타입별 기본 상세 설정 생성
|
||||
const defaultDetailSettings = this.generateDefaultDetailSettings(webType);
|
||||
const defaultDetailSettings = this.generateDefaultDetailSettings(finalWebType);
|
||||
|
||||
// 사용자 정의 설정과 기본 설정 병합
|
||||
const finalDetailSettings = {
|
||||
|
|
@ -749,10 +768,10 @@ export class TableManagementService {
|
|||
input_type = EXCLUDED.input_type,
|
||||
detail_settings = EXCLUDED.detail_settings,
|
||||
updated_date = NOW()`,
|
||||
[tableName, columnName, webType, JSON.stringify(finalDetailSettings)]
|
||||
[tableName, columnName, finalWebType, JSON.stringify(finalDetailSettings)]
|
||||
);
|
||||
logger.info(
|
||||
`컬럼 입력 타입 설정 완료: ${tableName}.${columnName} = ${webType}`
|
||||
`컬럼 입력 타입 설정 완료: ${tableName}.${columnName} = ${finalWebType}`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
|
|
@ -777,13 +796,23 @@ export class TableManagementService {
|
|||
detailSettings?: Record<string, any>
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 🔥 'direct'나 'auto'는 프론트엔드의 입력 방식 구분값이므로
|
||||
// DB의 input_type(웹타입)으로 저장하면 안 됨 - 'text'로 변환
|
||||
let finalInputType = inputType;
|
||||
if (inputType === "direct" || inputType === "auto") {
|
||||
logger.warn(
|
||||
`잘못된 input_type 값 감지: ${inputType} → 'text'로 변환 (${tableName}.${columnName})`
|
||||
);
|
||||
finalInputType = "text";
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${inputType}, company: ${companyCode}`
|
||||
`컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${finalInputType}, company: ${companyCode}`
|
||||
);
|
||||
|
||||
// 입력 타입별 기본 상세 설정 생성
|
||||
const defaultDetailSettings =
|
||||
this.generateDefaultInputTypeSettings(inputType);
|
||||
this.generateDefaultInputTypeSettings(finalInputType);
|
||||
|
||||
// 사용자 정의 설정과 기본 설정 병합
|
||||
const finalDetailSettings = {
|
||||
|
|
@ -805,7 +834,7 @@ export class TableManagementService {
|
|||
[
|
||||
tableName,
|
||||
columnName,
|
||||
inputType,
|
||||
finalInputType,
|
||||
JSON.stringify(finalDetailSettings),
|
||||
companyCode,
|
||||
]
|
||||
|
|
@ -815,7 +844,7 @@ export class TableManagementService {
|
|||
await this.syncScreenLayoutsInputType(
|
||||
tableName,
|
||||
columnName,
|
||||
inputType,
|
||||
finalInputType,
|
||||
companyCode
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
|||
// 전체 카테고리 옵션 로드 (모든 테이블의 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<NumberingRuleDesignerProps> = ({
|
|||
ruleToSave,
|
||||
});
|
||||
|
||||
// 테스트 테이블에 저장 (numbering_rules_test)
|
||||
// 테스트 테이블에 저장 (numbering_rules)
|
||||
const response = await saveNumberingRuleToTest(ruleToSave);
|
||||
|
||||
if (response.success && response.data) {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
||||
{/* 로딩 오버레이 */}
|
||||
{isCopying && (
|
||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center rounded-lg bg-background/90 backdrop-blur-sm">
|
||||
<Loader2 className="h-10 w-10 animate-spin text-primary" />
|
||||
<p className="mt-4 text-sm font-medium">{copyProgress.message}</p>
|
||||
<>
|
||||
{/* 로딩 오버레이 - Dialog 바깥에서 화면 전체 고정 */}
|
||||
{isCopying && (
|
||||
<div className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background/95 backdrop-blur-md">
|
||||
<div className="rounded-lg bg-card p-8 shadow-lg border flex flex-col items-center">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
<p className="mt-4 text-base font-medium">{copyProgress.message}</p>
|
||||
{copyProgress.total > 0 && (
|
||||
<>
|
||||
<div className="mt-3 h-2 w-48 overflow-hidden rounded-full bg-secondary">
|
||||
<div className="mt-4 h-3 w-64 overflow-hidden rounded-full bg-secondary">
|
||||
<div
|
||||
className="h-full bg-primary transition-all duration-300"
|
||||
style={{ width: `${Math.round((copyProgress.current / copyProgress.total) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{copyProgress.current} / {copyProgress.total} 화면
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
{copyProgress.current} / {copyProgress.total} 화면 복제 중...
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<p className="mt-4 text-xs text-muted-foreground">
|
||||
복제가 완료될 때까지 잠시 기다려주세요
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-base sm:text-lg">
|
||||
</div>
|
||||
)}
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-base sm:text-lg">
|
||||
<FolderTree className="h-5 w-5" />
|
||||
그룹 복제
|
||||
</DialogTitle>
|
||||
|
|
@ -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) => (
|
||||
<option key={company.companyCode} value={company.companyCode}>
|
||||
{company.companyName} ({company.companyCode})
|
||||
</option>
|
||||
))}
|
||||
{companies
|
||||
.filter((company) => company.companyCode !== sourceGroup?.company_code)
|
||||
.map((company) => (
|
||||
<option key={company.companyCode} value={company.companyCode}>
|
||||
{company.companyName} ({company.companyCode})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-[10px] text-muted-foreground sm:text-xs">
|
||||
복제된 그룹과 화면이 이 회사에 생성됩니다
|
||||
</p>
|
||||
{sourceGroup && (
|
||||
<p className="mt-1 text-[10px] text-amber-600 sm:text-xs">
|
||||
* 원본 회사({sourceGroup.company_code})로는 복제할 수 없습니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -1590,14 +1620,25 @@ export default function CopyScreenModal({
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 화면 복제 모드 렌더링
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<>
|
||||
{/* 로딩 오버레이 - Dialog 바깥에서 화면 전체 고정 */}
|
||||
{isCopying && (
|
||||
<div className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background/95 backdrop-blur-md">
|
||||
<div className="rounded-lg bg-card p-8 shadow-lg border flex flex-col items-center">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
<p className="mt-4 text-base font-medium">복제가 완료될 때까지 잠시 기다려주세요</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">화면 복제</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
"{sourceScreen?.screenName}" 화면을 복제합니다.
|
||||
|
|
@ -1694,13 +1735,20 @@ export default function CopyScreenModal({
|
|||
<SelectValue placeholder="회사 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{companies.map((company) => (
|
||||
<SelectItem key={company.companyCode} value={company.companyCode}>
|
||||
{company.companyName}
|
||||
</SelectItem>
|
||||
))}
|
||||
{companies
|
||||
.filter((company) => company.companyCode !== sourceScreen?.companyCode)
|
||||
.map((company) => (
|
||||
<SelectItem key={company.companyCode} value={company.companyCode}>
|
||||
{company.companyName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{sourceScreen && (
|
||||
<p className="mt-1 text-[10px] text-amber-600">
|
||||
* 원본 회사({sourceScreen.companyCode})로는 복제할 수 없습니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -1840,6 +1888,7 @@ export default function CopyScreenModal({
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
|
||||
{/* 그룹 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||
<AlertDialogContent className="max-w-[95vw] sm:max-w-[500px] border-destructive/50">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-base sm:text-lg">그룹 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-xs sm:text-sm">
|
||||
"{deletingGroup?.group_name}" 그룹을 삭제하시겠습니까?
|
||||
<br />
|
||||
{deleteScreensWithGroup
|
||||
? <span className="text-destructive font-medium">그룹에 속한 화면들도 함께 삭제됩니다.</span>
|
||||
: "그룹에 속한 화면들은 미분류로 이동됩니다."
|
||||
}
|
||||
<AlertDialogTitle className="text-base sm:text-lg flex items-center gap-2 text-destructive">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
그룹 삭제 경고
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="text-xs sm:text-sm">
|
||||
<div className="mt-2 rounded-md bg-destructive/10 border border-destructive/30 p-3">
|
||||
<p className="font-semibold text-destructive">
|
||||
"{deletingGroup?.group_name}" 그룹을 정말 삭제하시겠습니까?
|
||||
</p>
|
||||
<p className="mt-2 text-destructive/80">
|
||||
{deleteScreensWithGroup
|
||||
? "⚠️ 그룹에 속한 모든 화면, 플로우, 관련 데이터가 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다."
|
||||
: "그룹에 속한 화면들은 미분류로 이동됩니다."
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
|
|
@ -1570,11 +1581,21 @@ export function ScreenGroupTreeView({
|
|||
)}
|
||||
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-base sm:text-lg">화면 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-xs sm:text-sm">
|
||||
"{deletingScreen?.screenName}" 화면을 삭제하시겠습니까?
|
||||
<br />
|
||||
삭제된 화면은 휴지통으로 이동됩니다.
|
||||
<AlertDialogTitle className="text-base sm:text-lg flex items-center gap-2 text-destructive">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
화면 삭제 경고
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="text-xs sm:text-sm">
|
||||
<div className="mt-2 rounded-md bg-destructive/10 border border-destructive/30 p-3">
|
||||
<p className="font-semibold text-destructive">
|
||||
"{deletingScreen?.screenName}" 화면을 정말 삭제하시겠습니까?
|
||||
</p>
|
||||
<p className="mt-2 text-destructive/80">
|
||||
⚠️ 화면과 연결된 플로우, 레이아웃 데이터가 모두 삭제됩니다. 삭제된 화면은 휴지통으로 이동됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="gap-2 sm:gap-0">
|
||||
|
|
|
|||
|
|
@ -174,30 +174,10 @@ export default function TableTypeSelector({
|
|||
}
|
||||
};
|
||||
|
||||
// 입력 타입 변경
|
||||
const handleInputTypeChange = async (columnName: string, inputType: "direct" | "auto") => {
|
||||
try {
|
||||
// 현재 컬럼 정보 가져오기
|
||||
const currentColumn = columns.find((col) => col.columnName === columnName);
|
||||
if (!currentColumn) return;
|
||||
|
||||
// 웹 타입과 함께 입력 타입 업데이트
|
||||
await tableTypeApi.setColumnWebType(
|
||||
selectedTable,
|
||||
columnName,
|
||||
currentColumn.webType || "text",
|
||||
undefined, // detailSettings
|
||||
inputType,
|
||||
);
|
||||
|
||||
// 로컬 상태 업데이트
|
||||
setColumns((prev) => prev.map((col) => (col.columnName === columnName ? { ...col, inputType } : col)));
|
||||
|
||||
// console.log(`컬럼 ${columnName}의 입력 타입을 ${inputType}로 변경했습니다.`);
|
||||
} catch (error) {
|
||||
// console.error("입력 타입 변경 실패:", error);
|
||||
alert("입력 타입 설정에 실패했습니다. 다시 시도해주세요.");
|
||||
}
|
||||
// 입력 타입 변경 (로컬 상태만 - DB에 저장하지 않음)
|
||||
const handleInputTypeChange = (columnName: string, inputType: "direct" | "auto") => {
|
||||
// 로컬 상태만 업데이트 (DB에는 저장하지 않음 - inputType은 화면 렌더링용)
|
||||
setColumns((prev) => prev.map((col) => (col.columnName === columnName ? { ...col, inputType } : col)));
|
||||
};
|
||||
|
||||
const filteredTables = tables.filter((table) => table.displayName.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>((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<HTMLDivElement, UnifiedSelectProps>((pro
|
|||
fetchedOptions = data;
|
||||
}
|
||||
} else if (source === "category") {
|
||||
// 카테고리에서 로드 (category_values_test 테이블)
|
||||
// 카테고리에서 로드 (category_values 테이블)
|
||||
// tableName, columnName은 props에서 가져옴
|
||||
const catTable = categoryTable || tableName;
|
||||
const catColumn = categoryColumn || columnName;
|
||||
|
|
|
|||
|
|
@ -470,7 +470,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
|||
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<HTMLDivElement, V2SelectProps>(
|
|||
fetchedOptions = data;
|
||||
}
|
||||
} else if (source === "category") {
|
||||
// 카테고리에서 로드 (category_values_test 테이블)
|
||||
// 카테고리에서 로드 (category_values 테이블)
|
||||
// tableName, columnName은 props에서 가져옴
|
||||
const catTable = categoryTable || tableName;
|
||||
const catColumn = categoryColumn || columnName;
|
||||
|
|
|
|||
|
|
@ -173,11 +173,11 @@ export async function resetSequence(ruleId: string): Promise<ApiResponse<void>>
|
|||
}
|
||||
}
|
||||
|
||||
// ====== 테스트용 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
|
||||
|
|
|
|||
|
|
@ -347,12 +347,10 @@ export const tableTypeApi = {
|
|||
columnName: string,
|
||||
webType: string,
|
||||
detailSettings?: Record<string, any>,
|
||||
inputType?: "direct" | "auto",
|
||||
): Promise<void> => {
|
||||
await apiClient.put(`/table-management/tables/${tableName}/columns/${columnName}/web-type`, {
|
||||
webType,
|
||||
detailSettings,
|
||||
inputType,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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; // 카테고리 값 라벨 (조회 시 조인)
|
||||
|
||||
// 메타 정보
|
||||
|
|
|
|||
Loading…
Reference in New Issue