feat: 인증 미들웨어 적용 및 화면 그룹 삭제 로직 개선
- 모든 라우트에 인증 미들웨어를 적용하여 보안을 강화하였습니다. - 화면 그룹 삭제 시 회사 코드 확인 및 권한 체크 로직을 추가하여, 다른 회사의 그룹 삭제를 방지하였습니다. - 채번 규칙, 카테고리 값, 테이블 타입 컬럼 복제 시 같은 회사로 복제하는 경우 경고 메시지를 추가하였습니다. - 메뉴 URL 업데이트 기능을 추가하여 복제된 화면 ID에 맞게 URL을 재매핑하도록 하였습니다.
This commit is contained in:
parent
4daa77f9a1
commit
51492a8911
|
|
@ -5,9 +5,13 @@
|
|||
import { Router, Request, Response } from "express";
|
||||
import { categoryTreeService, CreateCategoryValueInput, UpdateCategoryValueInput } from "../services/categoryTreeService";
|
||||
import { logger } from "../utils/logger";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(authenticateToken);
|
||||
|
||||
// 인증된 사용자 타입
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
|
|
|
|||
|
|
@ -308,18 +308,42 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response
|
|||
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 1. 삭제할 그룹과 하위 그룹 ID 수집 (CASCADE 삭제 대상)
|
||||
// 0. 삭제할 그룹의 company_code 확인
|
||||
const targetGroupResult = await client.query(
|
||||
`SELECT company_code FROM screen_groups WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
if (targetGroupResult.rows.length === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(404).json({ success: false, message: "화면 그룹을 찾을 수 없습니다." });
|
||||
}
|
||||
const targetCompanyCode = targetGroupResult.rows[0].company_code;
|
||||
|
||||
// 권한 체크: 최고관리자가 아닌 경우 자신의 회사 그룹만 삭제 가능
|
||||
if (companyCode !== "*" && targetCompanyCode !== companyCode) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(403).json({ success: false, message: "권한이 없습니다." });
|
||||
}
|
||||
|
||||
// 1. 삭제할 그룹과 하위 그룹 ID 수집 (같은 회사만 - CASCADE 삭제 대상)
|
||||
const childGroupsResult = await client.query(`
|
||||
WITH RECURSIVE child_groups AS (
|
||||
SELECT id FROM screen_groups WHERE id = $1
|
||||
SELECT id, company_code FROM screen_groups WHERE id = $1 AND company_code = $2
|
||||
UNION ALL
|
||||
SELECT sg.id FROM screen_groups sg
|
||||
JOIN child_groups cg ON sg.parent_group_id = cg.id
|
||||
SELECT sg.id, sg.company_code FROM screen_groups sg
|
||||
JOIN child_groups cg ON sg.parent_group_id = cg.id AND sg.company_code = cg.company_code
|
||||
)
|
||||
SELECT id FROM child_groups
|
||||
`, [id]);
|
||||
`, [id, targetCompanyCode]);
|
||||
const groupIdsToDelete = childGroupsResult.rows.map((r: any) => r.id);
|
||||
|
||||
logger.info("화면 그룹 삭제 대상", {
|
||||
companyCode,
|
||||
targetCompanyCode,
|
||||
groupId: id,
|
||||
childGroupIds: groupIdsToDelete
|
||||
});
|
||||
|
||||
// 2. menu_info에서 삭제될 screen_group 참조를 NULL로 정리
|
||||
if (groupIdsToDelete.length > 0) {
|
||||
await client.query(`
|
||||
|
|
@ -329,18 +353,11 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response
|
|||
`, [groupIdsToDelete]);
|
||||
}
|
||||
|
||||
// 3. screen_groups 삭제
|
||||
let query = `DELETE FROM screen_groups WHERE id = $1`;
|
||||
const params: any[] = [id];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
query += ` AND company_code = $2`;
|
||||
params.push(companyCode);
|
||||
}
|
||||
|
||||
query += " RETURNING id";
|
||||
|
||||
const result = await client.query(query, params);
|
||||
// 3. screen_groups 삭제 (해당 그룹만 - 하위 그룹은 프론트엔드에서 순차 삭제)
|
||||
const result = await client.query(
|
||||
`DELETE FROM screen_groups WHERE id = $1 AND company_code = $2 RETURNING id`,
|
||||
[id, targetCompanyCode]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
|
|
@ -349,7 +366,7 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response
|
|||
|
||||
await client.query('COMMIT');
|
||||
|
||||
logger.info("화면 그룹 삭제", { companyCode, groupId: id, cleanedRefs: groupIdsToDelete.length });
|
||||
logger.info("화면 그룹 삭제 완료", { companyCode, targetCompanyCode, groupId: id, cleanedRefs: groupIdsToDelete.length });
|
||||
|
||||
res.json({ success: true, message: "화면 그룹이 삭제되었습니다." });
|
||||
} catch (error: any) {
|
||||
|
|
|
|||
|
|
@ -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}개`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 카테고리 + 코드 복사 (최적화: 배치 조회/삽입)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -597,7 +597,7 @@ export default function CopyScreenModal({
|
|||
screen_id: result.mainScreen.screenId,
|
||||
screen_role: "MAIN",
|
||||
display_order: 1,
|
||||
target_company_code: finalCompanyCode, // 대상 회사 코드 전달
|
||||
target_company_code: targetCompanyCode || sourceScreen.companyCode, // 대상 회사 코드 전달
|
||||
});
|
||||
console.log(`✅ 복제된 화면을 그룹(${selectedTargetGroupId})에 추가 완료`);
|
||||
} catch (groupError) {
|
||||
|
|
@ -606,8 +606,68 @@ export default function CopyScreenModal({
|
|||
}
|
||||
}
|
||||
|
||||
// 추가 복사 옵션 처리 (단일 화면 복제용)
|
||||
const sourceCompanyCode = sourceScreen.companyCode;
|
||||
const copyTargetCompanyCode = targetCompanyCode || sourceCompanyCode;
|
||||
let additionalCopyMessages: string[] = [];
|
||||
|
||||
// 채번규칙 복제
|
||||
if (copyNumberingRules && sourceCompanyCode !== copyTargetCompanyCode) {
|
||||
try {
|
||||
console.log("📋 단일 화면: 채번규칙 복제 시작...");
|
||||
const numberingResult = await apiClient.post("/api/screen-management/copy-numbering-rules", {
|
||||
sourceCompanyCode,
|
||||
targetCompanyCode: copyTargetCompanyCode
|
||||
});
|
||||
if (numberingResult.data.success) {
|
||||
additionalCopyMessages.push(`채번규칙 ${numberingResult.data.copiedCount || 0}개`);
|
||||
console.log("✅ 채번규칙 복제 완료:", numberingResult.data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("채번규칙 복제 실패:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 카테고리 값 복제
|
||||
if (copyCategoryValues && sourceCompanyCode !== copyTargetCompanyCode) {
|
||||
try {
|
||||
console.log("📋 단일 화면: 카테고리 값 복제 시작...");
|
||||
const categoryResult = await apiClient.post("/api/screen-management/copy-category-mapping", {
|
||||
sourceCompanyCode,
|
||||
targetCompanyCode: copyTargetCompanyCode
|
||||
});
|
||||
if (categoryResult.data.success) {
|
||||
additionalCopyMessages.push(`카테고리 값 ${categoryResult.data.copiedValues || 0}개`);
|
||||
console.log("✅ 카테고리 값 복제 완료:", categoryResult.data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("카테고리 값 복제 실패:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블 타입 컬럼 복제
|
||||
if (copyTableTypeColumns && sourceCompanyCode !== copyTargetCompanyCode) {
|
||||
try {
|
||||
console.log("📋 단일 화면: 테이블 타입 컬럼 복제 시작...");
|
||||
const tableTypeResult = await apiClient.post("/api/screen-management/copy-table-type-columns", {
|
||||
sourceCompanyCode,
|
||||
targetCompanyCode: copyTargetCompanyCode
|
||||
});
|
||||
if (tableTypeResult.data.success) {
|
||||
additionalCopyMessages.push(`테이블 타입 컬럼 ${tableTypeResult.data.copiedCount || 0}개`);
|
||||
console.log("✅ 테이블 타입 컬럼 복제 완료:", tableTypeResult.data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("테이블 타입 컬럼 복제 실패:", err);
|
||||
}
|
||||
}
|
||||
|
||||
const additionalInfo = additionalCopyMessages.length > 0
|
||||
? ` + 추가: ${additionalCopyMessages.join(", ")}`
|
||||
: "";
|
||||
|
||||
toast.success(
|
||||
`화면 복사가 완료되었습니다! (메인 1개 + 모달 ${result.modalScreens.length}개)`
|
||||
`화면 복사가 완료되었습니다! (메인 1개 + 모달 ${result.modalScreens.length}개${additionalInfo})`
|
||||
);
|
||||
|
||||
// 새로고침 완료 후 모달 닫기
|
||||
|
|
@ -1678,6 +1738,50 @@ export default function CopyScreenModal({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 추가 복사 옵션 (단일 화면 복제용) */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs sm:text-sm font-medium">추가 복사 옵션 (선택사항):</Label>
|
||||
|
||||
{/* 채번규칙 복제 */}
|
||||
<div className="flex items-center space-x-2 p-2 bg-muted/30 rounded-md ml-2">
|
||||
<Checkbox
|
||||
id="copyNumberingRulesScreen"
|
||||
checked={copyNumberingRules}
|
||||
onCheckedChange={(checked) => setCopyNumberingRules(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="copyNumberingRulesScreen" className="text-xs sm:text-sm cursor-pointer flex items-center gap-2">
|
||||
<Hash className="h-4 w-4 text-muted-foreground" />
|
||||
채번 규칙 복사
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{/* 카테고리 값 복사 */}
|
||||
<div className="flex items-center space-x-2 p-2 bg-muted/30 rounded-md ml-2">
|
||||
<Checkbox
|
||||
id="copyCategoryValuesScreen"
|
||||
checked={copyCategoryValues}
|
||||
onCheckedChange={(checked) => setCopyCategoryValues(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="copyCategoryValuesScreen" className="text-xs sm:text-sm cursor-pointer flex items-center gap-2">
|
||||
<Table className="h-4 w-4 text-muted-foreground" />
|
||||
카테고리 값 복사
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{/* 테이블 타입관리 입력타입 설정 복사 */}
|
||||
<div className="flex items-center space-x-2 p-2 bg-muted/30 rounded-md ml-2">
|
||||
<Checkbox
|
||||
id="copyTableTypeColumnsScreen"
|
||||
checked={copyTableTypeColumns}
|
||||
onCheckedChange={(checked) => setCopyTableTypeColumns(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="copyTableTypeColumnsScreen" className="text-xs sm:text-sm cursor-pointer flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||
테이블 타입관리 입력타입 설정 복사
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 화면명 일괄 수정 (접히는 옵션) */}
|
||||
<details className="text-sm">
|
||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export function ScreenGroupTreeView({
|
|||
const [syncProgress, setSyncProgress] = useState<{ message: string; detail?: string } | null>(null);
|
||||
|
||||
// 회사 선택 (최고 관리자용)
|
||||
const { user, switchCompany } = useAuth();
|
||||
const { user } = useAuth();
|
||||
const [companies, setCompanies] = useState<Company[]>([]);
|
||||
const [selectedCompanyCode, setSelectedCompanyCode] = useState<string>("");
|
||||
const [isSyncCompanySelectOpen, setIsSyncCompanySelectOpen] = useState(false);
|
||||
|
|
@ -301,23 +301,18 @@ export function ScreenGroupTreeView({
|
|||
}
|
||||
};
|
||||
|
||||
// 회사 선택 시 회사 전환 + 상태 조회
|
||||
// 회사 선택 시 상태만 변경 (페이지 새로고침 없이)
|
||||
const handleCompanySelect = async (companyCode: string) => {
|
||||
setSelectedCompanyCode(companyCode);
|
||||
setIsSyncCompanySelectOpen(false);
|
||||
setSyncStatus(null);
|
||||
|
||||
if (companyCode) {
|
||||
// 🔧 회사 전환 (JWT 토큰 변경) - 모든 API가 선택한 회사로 동작하도록
|
||||
const switchResult = await switchCompany(companyCode);
|
||||
if (!switchResult.success) {
|
||||
toast.error(switchResult.message || "회사 전환 실패");
|
||||
return;
|
||||
// 동기화 상태 조회 (선택한 회사 코드로)
|
||||
const response = await getMenuScreenSyncStatus(companyCode);
|
||||
if (response.success && response.data) {
|
||||
setSyncStatus(response.data);
|
||||
}
|
||||
toast.success(`${companyCode} 회사로 전환되었습니다. 페이지를 새로고침합니다.`);
|
||||
|
||||
// 🔧 페이지 새로고침으로 새 JWT 확실하게 적용
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -447,17 +442,24 @@ export function ScreenGroupTreeView({
|
|||
};
|
||||
|
||||
// 그룹과 모든 하위 그룹의 화면을 재귀적으로 수집
|
||||
const getAllScreensInGroupRecursively = (groupId: number): ScreenDefinition[] => {
|
||||
// 같은 회사의 그룹만 필터링하여 다른 회사 화면이 잘못 수집되는 것을 방지
|
||||
const getAllScreensInGroupRecursively = (groupId: number, targetCompanyCode?: string): ScreenDefinition[] => {
|
||||
const result: ScreenDefinition[] = [];
|
||||
// 부모 그룹의 company_code 확인
|
||||
const parentGroup = groups.find(g => g.id === groupId);
|
||||
const companyCode = targetCompanyCode || parentGroup?.company_code;
|
||||
|
||||
// 현재 그룹의 화면들
|
||||
const currentGroupScreens = getScreensInGroup(groupId);
|
||||
result.push(...currentGroupScreens);
|
||||
|
||||
// 하위 그룹들 찾기
|
||||
const childGroups = groups.filter((g) => (g as any).parent_group_id === groupId);
|
||||
// 같은 회사 + 같은 부모를 가진 하위 그룹들 찾기
|
||||
const childGroups = groups.filter((g) =>
|
||||
(g as any).parent_group_id === groupId &&
|
||||
(!companyCode || g.company_code === companyCode)
|
||||
);
|
||||
for (const childGroup of childGroups) {
|
||||
const childScreens = getAllScreensInGroupRecursively(childGroup.id);
|
||||
const childScreens = getAllScreensInGroupRecursively(childGroup.id, companyCode);
|
||||
result.push(...childScreens);
|
||||
}
|
||||
|
||||
|
|
@ -465,13 +467,22 @@ export function ScreenGroupTreeView({
|
|||
};
|
||||
|
||||
// 모든 하위 그룹 ID를 재귀적으로 수집 (삭제 순서: 자식 → 부모)
|
||||
const getAllChildGroupIds = (groupId: number): number[] => {
|
||||
// 같은 회사의 그룹만 필터링하여 다른 회사 그룹이 잘못 삭제되는 것을 방지
|
||||
const getAllChildGroupIds = (groupId: number, targetCompanyCode?: string): number[] => {
|
||||
const result: number[] = [];
|
||||
const childGroups = groups.filter((g) => (g as any).parent_group_id === groupId);
|
||||
// 부모 그룹의 company_code 확인
|
||||
const parentGroup = groups.find(g => g.id === groupId);
|
||||
const companyCode = targetCompanyCode || parentGroup?.company_code;
|
||||
|
||||
// 같은 회사 + 같은 부모를 가진 그룹만 필터링
|
||||
const childGroups = groups.filter((g) =>
|
||||
(g as any).parent_group_id === groupId &&
|
||||
(!companyCode || g.company_code === companyCode)
|
||||
);
|
||||
|
||||
for (const childGroup of childGroups) {
|
||||
// 자식의 자식들을 먼저 수집 (깊은 곳부터)
|
||||
const grandChildIds = getAllChildGroupIds(childGroup.id);
|
||||
const grandChildIds = getAllChildGroupIds(childGroup.id, companyCode);
|
||||
result.push(...grandChildIds);
|
||||
result.push(childGroup.id);
|
||||
}
|
||||
|
|
@ -483,10 +494,35 @@ export function ScreenGroupTreeView({
|
|||
const confirmDeleteGroup = async () => {
|
||||
if (!deletingGroup) return;
|
||||
|
||||
// 🔍 디버깅: 삭제 대상 그룹 정보
|
||||
console.log("========== 그룹 삭제 디버깅 ==========");
|
||||
console.log("삭제 대상 그룹:", {
|
||||
id: deletingGroup.id,
|
||||
name: deletingGroup.group_name,
|
||||
company_code: deletingGroup.company_code,
|
||||
parent_group_id: (deletingGroup as any).parent_group_id
|
||||
});
|
||||
|
||||
// 🔍 디버깅: 전체 groups 배열에서 같은 회사 그룹 출력
|
||||
const sameCompanyGroups = groups.filter(g => g.company_code === deletingGroup.company_code);
|
||||
console.log("같은 회사 그룹들:", sameCompanyGroups.map(g => ({
|
||||
id: g.id,
|
||||
name: g.group_name,
|
||||
parent_group_id: (g as any).parent_group_id
|
||||
})));
|
||||
|
||||
// 삭제 전 통계 수집 (화면 수는 삭제 전에 계산)
|
||||
const totalScreensToDelete = getAllScreensInGroupRecursively(deletingGroup.id).length;
|
||||
const childGroupIds = getAllChildGroupIds(deletingGroup.id);
|
||||
|
||||
// 🔍 디버깅: 수집된 하위 그룹 ID들
|
||||
console.log("수집된 하위 그룹 ID들:", childGroupIds);
|
||||
console.log("하위 그룹 상세:", childGroupIds.map(id => {
|
||||
const g = groups.find(grp => grp.id === id);
|
||||
return g ? { id: g.id, name: g.group_name, parent_group_id: (g as any).parent_group_id } : { id, name: "NOT_FOUND" };
|
||||
}));
|
||||
console.log("==========================================");
|
||||
|
||||
// 총 작업 수 계산 (화면 + 하위 그룹 + 현재 그룹)
|
||||
const totalSteps = totalScreensToDelete + childGroupIds.length + 1;
|
||||
let currentStep = 0;
|
||||
|
|
@ -511,7 +547,7 @@ export function ScreenGroupTreeView({
|
|||
total: totalSteps,
|
||||
message: `화면 삭제 중: ${screen.screenName}`
|
||||
});
|
||||
await screenApi.deleteScreen(screen.screenId, "그룹 삭제와 함께 삭제");
|
||||
await screenApi.deleteScreen(screen.screenId, "그룹 삭제와 함께 삭제", true); // force: true로 의존성 무시
|
||||
}
|
||||
console.log(`✅ 그룹 및 하위 그룹 내 화면 ${allScreens.length}개 삭제 완료`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export interface CreateCategoryValueInput {
|
|||
icon?: string;
|
||||
isActive?: boolean;
|
||||
isDefault?: boolean;
|
||||
targetCompanyCode?: string; // 저장할 회사 코드 (최고 관리자가 회사 선택 시)
|
||||
}
|
||||
|
||||
// 카테고리 값 수정 입력
|
||||
|
|
|
|||
Loading…
Reference in New Issue