feat: 인증 미들웨어 적용 및 화면 그룹 삭제 로직 개선

- 모든 라우트에 인증 미들웨어를 적용하여 보안을 강화하였습니다.
- 화면 그룹 삭제 시 회사 코드 확인 및 권한 체크 로직을 추가하여, 다른 회사의 그룹 삭제를 방지하였습니다.
- 채번 규칙, 카테고리 값, 테이블 타입 컬럼 복제 시 같은 회사로 복제하는 경우 경고 메시지를 추가하였습니다.
- 메뉴 URL 업데이트 기능을 추가하여 복제된 화면 ID에 맞게 URL을 재매핑하도록 하였습니다.
This commit is contained in:
DDD1542
2026-02-02 09:22:34 +09:00
parent 4daa77f9a1
commit 51492a8911
9 changed files with 784 additions and 508 deletions

View File

@@ -961,6 +961,16 @@ export class MenuCopyService {
const menus = await this.collectMenuTree(sourceMenuObjid, client);
const sourceCompanyCode = menus[0].company_code!;
// 같은 회사로 복제하는 경우 경고 (자기 자신의 데이터 손상 위험)
if (sourceCompanyCode === targetCompanyCode) {
logger.warn(
`⚠️ 같은 회사로 메뉴 복제 시도: ${sourceCompanyCode}${targetCompanyCode}`
);
warnings.push(
"같은 회사로 복제하면 추가 데이터(카테고리, 채번 등)가 복제되지 않습니다."
);
}
const screenIds = await this.collectScreens(
menus.map((m) => m.objid),
sourceCompanyCode,
@@ -1116,6 +1126,10 @@ export class MenuCopyService {
client
);
// === 6.5단계: 메뉴 URL 업데이트 (화면 ID 재매핑) ===
logger.info("\n🔄 [6.5단계] 메뉴 URL 화면 ID 재매핑");
await this.updateMenuUrls(menuIdMap, screenIdMap, client);
// === 7단계: 테이블 타입 설정 복사 ===
if (additionalCopyOptions?.copyTableTypeColumns) {
logger.info("\n📦 [7단계] 테이블 타입 설정 복사");
@@ -2268,6 +2282,68 @@ export class MenuCopyService {
}
}
/**
* 메뉴 URL 업데이트 (화면 ID 재매핑)
* menu_url에 포함된 /screens/{screenId} 형식의 화면 ID를 복제된 화면 ID로 교체
*/
private async updateMenuUrls(
menuIdMap: Map<number, number>,
screenIdMap: Map<number, number>,
client: PoolClient
): Promise<void> {
if (menuIdMap.size === 0 || screenIdMap.size === 0) {
logger.info("📭 메뉴 URL 업데이트 대상 없음");
return;
}
const newMenuObjids = Array.from(menuIdMap.values());
// 복제된 메뉴 중 menu_url이 있는 것 조회
const menusWithUrl = await client.query<{
objid: number;
menu_url: string;
}>(
`SELECT objid, menu_url FROM menu_info
WHERE objid = ANY($1) AND menu_url IS NOT NULL AND menu_url != ''`,
[newMenuObjids]
);
if (menusWithUrl.rows.length === 0) {
logger.info("📭 menu_url 업데이트 대상 없음");
return;
}
let updatedCount = 0;
const screenIdPattern = /\/screens\/(\d+)/;
for (const menu of menusWithUrl.rows) {
const match = menu.menu_url.match(screenIdPattern);
if (!match) continue;
const originalScreenId = parseInt(match[1], 10);
const newScreenId = screenIdMap.get(originalScreenId);
if (newScreenId && newScreenId !== originalScreenId) {
const newMenuUrl = menu.menu_url.replace(
`/screens/${originalScreenId}`,
`/screens/${newScreenId}`
);
await client.query(
`UPDATE menu_info SET menu_url = $1 WHERE objid = $2`,
[newMenuUrl, menu.objid]
);
logger.info(
` 🔗 메뉴 URL 업데이트: ${menu.menu_url}${newMenuUrl}`
);
updatedCount++;
}
}
logger.info(`✅ 메뉴 URL 업데이트 완료: ${updatedCount}`);
}
/**
* 코드 카테고리 + 코드 복사 (최적화: 배치 조회/삽입)
*/

View File

@@ -1782,8 +1782,8 @@ class NumberingRuleService {
}
/**
* 회사별 채번규칙 복제 (메뉴 동기화 완료 후 호출)
* 메뉴 이름을 기준으로 채번규칙을 대상 회사의 메뉴에 연결
* 회사별 채번규칙 복제 (테이블 기반)
* numbering_rules_test, numbering_rule_parts_test 테이블 사용
* 복제 후 화면 레이아웃의 numberingRuleId 참조도 업데이트
*/
async copyRulesForCompany(
@@ -1798,12 +1798,9 @@ class NumberingRuleService {
try {
await client.query("BEGIN");
// 1. 원본 회사의 채번규칙 조회 (menu + table 스코프 모두)
// 1. 원본 회사의 채번규칙 조회 - numbering_rules_test 사용
const sourceRulesResult = await client.query(
`SELECT nr.*, mi.menu_name_kor as source_menu_name
FROM numbering_rules nr
LEFT JOIN menu_info mi ON nr.menu_objid = mi.objid
WHERE nr.company_code = $1 AND nr.scope_type IN ('menu', 'table')`,
`SELECT * FROM numbering_rules_test WHERE company_code = $1`,
[sourceCompanyCode]
);
@@ -1817,9 +1814,9 @@ class NumberingRuleService {
// 새 rule_id 생성
const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// 이미 존재하는지 확인 (이름 기반)
// 이미 존재하는지 확인 (이름 기반) - numbering_rules_test 사용
const existsCheck = await client.query(
`SELECT rule_id FROM numbering_rules
`SELECT rule_id FROM numbering_rules_test
WHERE company_code = $1 AND rule_name = $2`,
[targetCompanyCode, rule.rule_name]
);
@@ -1832,32 +1829,12 @@ class NumberingRuleService {
continue;
}
let targetMenuObjid = null;
// menu 스코프인 경우 대상 메뉴 찾기
if (rule.scope_type === 'menu' && rule.source_menu_name) {
const targetMenuResult = await client.query(
`SELECT objid FROM menu_info
WHERE company_code = $1 AND menu_name_kor = $2
LIMIT 1`,
[targetCompanyCode, rule.source_menu_name]
);
if (targetMenuResult.rows.length === 0) {
result.skippedCount++;
result.details.push(`건너뜀 (메뉴 없음): ${rule.rule_name} - 메뉴: ${rule.source_menu_name}`);
continue;
}
targetMenuObjid = targetMenuResult.rows[0].objid;
}
// 채번규칙 복제
// 채번규칙 복제 - numbering_rules_test 사용
await client.query(
`INSERT INTO numbering_rules (
`INSERT INTO numbering_rules_test (
rule_id, rule_name, description, separator, reset_period,
current_sequence, table_name, column_name, company_code,
created_at, updated_at, created_by, scope_type, menu_objid
created_at, updated_at, created_by, category_column, category_value_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW(), $10, $11, $12)`,
[
newRuleId,
@@ -1870,20 +1847,20 @@ class NumberingRuleService {
rule.column_name,
targetCompanyCode,
rule.created_by,
rule.scope_type,
targetMenuObjid,
rule.category_column,
rule.category_value_id,
]
);
// 채번규칙 파트 복제
// 채번규칙 파트 복제 - numbering_rule_parts_test 사용
const partsResult = await client.query(
`SELECT * FROM numbering_rule_parts WHERE rule_id = $1 ORDER BY part_order`,
`SELECT * FROM numbering_rule_parts_test 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 (
`INSERT INTO numbering_rule_parts_test (
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())`,
@@ -1902,12 +1879,11 @@ class NumberingRuleService {
// 매핑 추가
result.ruleIdMap[rule.rule_id] = newRuleId;
result.copiedCount++;
result.details.push(`복제 완료: ${rule.rule_name} (${rule.scope_type})`);
result.details.push(`복제 완료: ${rule.rule_name}`);
logger.info("채번규칙 복제 완료", {
ruleName: rule.rule_name,
oldRuleId: rule.rule_id,
newRuleId,
targetMenuObjid
newRuleId
});
}

View File

@@ -4595,6 +4595,15 @@ export class ScreenManagementService {
details: [] as string[],
};
// 같은 회사로 복제하는 경우 스킵 (자기 자신의 데이터 삭제 방지)
if (sourceCompanyCode === targetCompanyCode) {
logger.warn(
`⚠️ 같은 회사로 코드 카테고리/코드 복제 시도 - 스킵: ${sourceCompanyCode}`,
);
result.details.push("같은 회사로는 복제할 수 없습니다.");
return result;
}
return transaction(async (client) => {
logger.info(
`📦 코드 카테고리/코드 복제: ${sourceCompanyCode}${targetCompanyCode}`,
@@ -4716,12 +4725,21 @@ export class ScreenManagementService {
details: [] as string[],
};
// 같은 회사로 복제하는 경우 스킵 (자기 자신의 데이터 삭제 방지)
if (sourceCompanyCode === targetCompanyCode) {
logger.warn(
`⚠️ 같은 회사로 카테고리 값 복제 시도 - 스킵: ${sourceCompanyCode}`,
);
result.details.push("같은 회사로는 복제할 수 없습니다.");
return result;
}
return transaction(async (client) => {
logger.info(
`📦 카테고리 값 복제: ${sourceCompanyCode}${targetCompanyCode}`,
);
// 1. 기존 대상 회사 데이터 삭제
// 1. 기존 대상 회사 데이터 삭제 (다른 회사로 복제 시에만)
await client.query(
`DELETE FROM category_values_test WHERE company_code = $1`,
[targetCompanyCode],
@@ -4798,6 +4816,15 @@ export class ScreenManagementService {
details: [] as string[],
};
// 같은 회사로 복제하는 경우 스킵 (자기 자신의 데이터 삭제 방지)
if (sourceCompanyCode === targetCompanyCode) {
logger.warn(
`⚠️ 같은 회사로 테이블 타입 컬럼 복제 시도 - 스킵: ${sourceCompanyCode}`,
);
result.details.push("같은 회사로는 복제할 수 없습니다.");
return result;
}
return transaction(async (client) => {
logger.info(
`📦 테이블 타입 컬럼 복제: ${sourceCompanyCode}${targetCompanyCode}`,
@@ -4861,6 +4888,15 @@ export class ScreenManagementService {
details: [] as string[],
};
// 같은 회사로 복제하는 경우 스킵 (자기 자신의 데이터 삭제 방지)
if (sourceCompanyCode === targetCompanyCode) {
logger.warn(
`⚠️ 같은 회사로 연쇄관계 설정 복제 시도 - 스킵: ${sourceCompanyCode}`,
);
result.details.push("같은 회사로는 복제할 수 없습니다.");
return result;
}
return transaction(async (client) => {
logger.info(
`📦 연쇄관계 설정 복제: ${sourceCompanyCode}${targetCompanyCode}`,