feat: 화면 편집기에서 메뉴 기반 데이터 스코프 적용

- 백엔드: screenManagementService에 getMenuByScreen 함수 추가
- 백엔드: GET /api/screen-management/screens/:id/menu 엔드포인트 추가
- 프론트엔드: screenApi.getScreenMenu() 함수 추가
- ScreenDesigner: 화면 로드 시 menu_objid 자동 조회
- ScreenDesigner: menuObjid를 RealtimePreview와 UnifiedPropertiesPanel에 전달
- UnifiedPropertiesPanel: menuObjid를 DynamicComponentConfigPanel에 전달

이로써 화면 편집기에서 코드/카테고리/채번규칙이 해당 화면이 할당된 메뉴 기준으로 필터링됨
This commit is contained in:
kjs
2025-11-11 16:28:17 +09:00
parent 32d4575fb5
commit 6534d03ecd
14 changed files with 226 additions and 125 deletions

View File

@@ -112,6 +112,17 @@ router.post("/", authenticateToken, async (req: AuthenticatedRequest, res: Respo
const userId = req.user!.userId;
const ruleConfig = req.body;
logger.info("🔍 [POST /numbering-rules] 채번 규칙 생성 요청:", {
companyCode,
userId,
ruleId: ruleConfig.ruleId,
ruleName: ruleConfig.ruleName,
scopeType: ruleConfig.scopeType,
menuObjid: ruleConfig.menuObjid,
tableName: ruleConfig.tableName,
partsCount: ruleConfig.parts?.length,
});
try {
if (!ruleConfig.ruleId || !ruleConfig.ruleName) {
return res.status(400).json({ success: false, error: "규칙 ID와 규칙명은 필수입니다" });
@@ -122,12 +133,22 @@ router.post("/", authenticateToken, async (req: AuthenticatedRequest, res: Respo
}
const newRule = await numberingRuleService.createRule(ruleConfig, companyCode, userId);
logger.info("✅ [POST /numbering-rules] 채번 규칙 생성 성공:", {
ruleId: newRule.ruleId,
menuObjid: newRule.menuObjid,
});
return res.status(201).json({ success: true, data: newRule });
} catch (error: any) {
if (error.code === "23505") {
return res.status(409).json({ success: false, error: "이미 존재하는 규칙 ID입니다" });
}
logger.error("규칙 생성 실패", { error: error.message });
logger.error("❌ [POST /numbering-rules] 규칙 생성 실패:", {
error: error.message,
stack: error.stack,
code: error.code,
});
return res.status(500).json({ success: false, error: error.message });
}
});

View File

@@ -60,6 +60,29 @@ export const getScreen = async (
}
};
// 화면에 할당된 메뉴 조회
export const getScreenMenu = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { id } = req.params;
const { companyCode } = req.user as any;
const menuInfo = await screenManagementService.getMenuByScreen(
parseInt(id),
companyCode
);
res.json({ success: true, data: menuInfo });
} catch (error) {
console.error("화면 메뉴 조회 실패:", error);
res
.status(500)
.json({ success: false, message: "화면 메뉴 조회에 실패했습니다." });
}
};
// 화면 생성
export const createScreen = async (
req: AuthenticatedRequest,

View File

@@ -3,6 +3,7 @@ import { authenticateToken } from "../middleware/authMiddleware";
import {
getScreens,
getScreen,
getScreenMenu,
createScreen,
updateScreen,
updateScreenInfo,
@@ -33,6 +34,7 @@ router.use(authenticateToken);
// 화면 관리
router.get("/screens", getScreens);
router.get("/screens/:id", getScreen);
router.get("/screens/:id/menu", getScreenMenu); // 화면에 할당된 메뉴 조회
router.post("/screens", createScreen);
router.put("/screens/:id", updateScreen);
router.put("/screens/:id/info", updateScreenInfo); // 화면 정보만 수정

View File

@@ -158,6 +158,17 @@ export class CommonCodeService {
try {
const { search, isActive, page = 1, size = 20 } = params;
logger.info(`🔍 [getCodes] 코드 조회 시작:`, {
categoryCode,
menuObjid,
hasMenuObjid: !!menuObjid,
userCompanyCode,
search,
isActive,
page,
size,
});
const whereConditions: string[] = ["code_category = $1"];
const values: any[] = [categoryCode];
let paramIndex = 2;
@@ -169,7 +180,13 @@ export class CommonCodeService {
whereConditions.push(`menu_objid = ANY($${paramIndex})`);
values.push(siblingMenuObjids);
paramIndex++;
logger.info(`메뉴별 코드 필터링: ${menuObjid}, 형제 메뉴: ${siblingMenuObjids.join(', ')}`);
logger.info(`📋 [getCodes] 메뉴별 코드 필터링:`, {
menuObjid,
siblingMenuObjids,
siblingCount: siblingMenuObjids.length,
});
} else {
logger.warn(`⚠️ [getCodes] menuObjid 없음 - 전역 코드 조회`);
}
// 회사별 필터링 (최고 관리자가 아닌 경우)
@@ -199,6 +216,13 @@ export class CommonCodeService {
const offset = (page - 1) * size;
logger.info(`📝 [getCodes] 실행할 쿼리:`, {
whereClause,
values,
whereConditions,
paramIndex,
});
// 코드 조회
const codes = await query<CodeInfo>(
`SELECT * FROM code_info
@@ -217,9 +241,20 @@ export class CommonCodeService {
const total = parseInt(countResult?.count || "0");
logger.info(
`코드 조회 완료: ${categoryCode} - ${codes.length}개, 전체: ${total}개 (회사: ${userCompanyCode || "전체"})`
`✅ [getCodes] 코드 조회 완료: ${categoryCode} - ${codes.length}개, 전체: ${total}개 (회사: ${userCompanyCode || "전체"}, menuObjid: ${menuObjid || "없음"})`
);
logger.info(`📊 [getCodes] 조회된 코드 상세:`, {
categoryCode,
menuObjid,
codes: codes.map((c) => ({
code_value: c.code_value,
code_name: c.code_name,
menu_objid: c.menu_objid,
company_code: c.company_code,
})),
});
return { data: codes, total };
} catch (error) {
logger.error(`코드 조회 중 오류 (${categoryCode}):`, error);

View File

@@ -301,16 +301,18 @@ class NumberingRuleService {
scope_type = 'global'
OR scope_type = 'table'
OR (scope_type = 'menu' AND menu_objid = ANY($1))
OR (scope_type = 'table' AND menu_objid = ANY($1)) -- ⚠️ 임시: table 스코프도 menu_objid로 필터링
OR (scope_type = 'table' AND menu_objid IS NULL) -- ⚠️ 임시: 기존 규칙(menu_objid NULL) 포함
ORDER BY
CASE scope_type
WHEN 'menu' THEN 1
WHEN 'table' THEN 2
WHEN 'global' THEN 3
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
`;
params = [siblingObjids];
logger.info("최고 관리자: 형제 메뉴 포함 채번 규칙 조회", { siblingObjids });
logger.info("최고 관리자: 형제 메뉴 포함 채번 규칙 조회 (기존 규칙 포함)", { siblingObjids });
} else {
// 일반 회사: 자신의 규칙만 조회 (형제 메뉴 포함)
query = `
@@ -335,17 +337,19 @@ class NumberingRuleService {
scope_type = 'global'
OR scope_type = 'table'
OR (scope_type = 'menu' AND menu_objid = ANY($2))
OR (scope_type = 'table' AND menu_objid = ANY($2)) -- ⚠️ 임시: table 스코프도 menu_objid로 필터링
OR (scope_type = 'table' AND menu_objid IS NULL) -- ⚠️ 임시: 기존 규칙(menu_objid NULL) 포함
)
ORDER BY
CASE scope_type
WHEN 'menu' THEN 1
WHEN 'table' THEN 2
WHEN 'global' THEN 3
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
`;
params = [companyCode, siblingObjids];
logger.info("회사별: 형제 메뉴 포함 채번 규칙 조회", { companyCode, siblingObjids });
logger.info("회사별: 형제 메뉴 포함 채번 규칙 조회 (기존 규칙 포함)", { companyCode, siblingObjids });
}
logger.info("🔍 채번 규칙 쿼리 실행", {

View File

@@ -1547,6 +1547,39 @@ export class ScreenManagementService {
return screens.map((screen) => this.mapToScreenDefinition(screen));
}
/**
* 화면에 할당된 메뉴 조회 (첫 번째 할당만 반환)
* 화면 편집기에서 menuObjid를 가져오기 위해 사용
*/
async getMenuByScreen(
screenId: number,
companyCode: string
): Promise<{ menuObjid: number; menuName?: string } | null> {
const result = await queryOne<{
menu_objid: string;
menu_name_kor?: string;
}>(
`SELECT sma.menu_objid, mi.menu_name_kor
FROM screen_menu_assignments sma
LEFT JOIN menu_info mi ON sma.menu_objid = mi.objid
WHERE sma.screen_id = $1
AND sma.company_code = $2
AND sma.is_active = 'Y'
ORDER BY sma.created_at ASC
LIMIT 1`,
[screenId, companyCode]
);
if (!result) {
return null;
}
return {
menuObjid: parseInt(result.menu_objid),
menuName: result.menu_name_kor,
};
}
/**
* 화면-메뉴 할당 해제 (✅ Raw Query 전환 완료)
*/