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

@@ -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?: {

View File

@@ -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) {