Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node

This commit is contained in:
kjs
2026-02-12 14:19:31 +09:00
20 changed files with 555 additions and 481 deletions

View File

@@ -1,4 +1,5 @@
import "dotenv/config";
import "express-async-errors"; // async 라우트 핸들러의 에러를 Express 에러 핸들러로 자동 전달
import express from "express";
import cors from "cors";
import helmet from "helmet";

View File

@@ -19,8 +19,6 @@ export async function getAdminMenus(
res: Response
): Promise<void> {
try {
logger.info("=== 관리자 메뉴 목록 조회 시작 ===");
// 현재 로그인한 사용자의 정보 가져오기
const userId = req.user?.userId;
const userCompanyCode = req.user?.companyCode || "ILSHIN";
@@ -29,13 +27,6 @@ export async function getAdminMenus(
const menuType = req.query.menuType as string | undefined; // menuType 파라미터 추가
const includeInactive = req.query.includeInactive === "true"; // includeInactive 파라미터 추가
logger.info(`사용자 ID: ${userId}`);
logger.info(`사용자 회사 코드: ${userCompanyCode}`);
logger.info(`사용자 유형: ${userType}`);
logger.info(`사용자 로케일: ${userLang}`);
logger.info(`메뉴 타입: ${menuType || "전체"}`);
logger.info(`비활성 메뉴 포함: ${includeInactive}`);
const paramMap = {
userId,
userCompanyCode,
@@ -47,13 +38,6 @@ export async function getAdminMenus(
const menuList = await AdminService.getAdminMenuList(paramMap);
logger.info(
`관리자 메뉴 조회 결과: ${menuList.length}개 (타입: ${menuType || "전체"}, 회사: ${userCompanyCode})`
);
if (menuList.length > 0) {
logger.info("첫 번째 메뉴:", menuList[0]);
}
const response: ApiResponse<any[]> = {
success: true,
message: "관리자 메뉴 목록 조회 성공",
@@ -85,19 +69,12 @@ export async function getUserMenus(
res: Response
): Promise<void> {
try {
logger.info("=== 사용자 메뉴 목록 조회 시작 ===");
// 현재 로그인한 사용자의 정보 가져오기
const userId = req.user?.userId;
const userCompanyCode = req.user?.companyCode || "ILSHIN";
const userType = req.user?.userType;
const userLang = (req.query.userLang as string) || "ko";
logger.info(`사용자 ID: ${userId}`);
logger.info(`사용자 회사 코드: ${userCompanyCode}`);
logger.info(`사용자 유형: ${userType}`);
logger.info(`사용자 로케일: ${userLang}`);
const paramMap = {
userId,
userCompanyCode,
@@ -107,13 +84,6 @@ export async function getUserMenus(
const menuList = await AdminService.getUserMenuList(paramMap);
logger.info(
`사용자 메뉴 조회 결과: ${menuList.length}개 (회사: ${userCompanyCode})`
);
if (menuList.length > 0) {
logger.info("첫 번째 메뉴:", menuList[0]);
}
const response: ApiResponse<any[]> = {
success: true,
message: "사용자 메뉴 목록 조회 성공",
@@ -473,7 +443,7 @@ export const getUserLocale = async (
res: Response
): Promise<void> => {
try {
logger.info("사용자 로케일 조회 요청", {
logger.debug("사용자 로케일 조회 요청", {
query: req.query,
user: req.user,
});
@@ -496,7 +466,7 @@ export const getUserLocale = async (
if (userInfo?.locale) {
userLocale = userInfo.locale;
logger.info("데이터베이스에서 사용자 로케일 조회 성공", {
logger.debug("데이터베이스에서 사용자 로케일 조회 성공", {
userId: req.user.userId,
locale: userLocale,
});
@@ -513,7 +483,7 @@ export const getUserLocale = async (
message: "사용자 로케일 조회 성공",
};
logger.info("사용자 로케일 조회 성공", {
logger.debug("사용자 로케일 조회 성공", {
userLocale,
userId: req.user.userId,
fromDatabase: !!userInfo?.locale,
@@ -618,7 +588,7 @@ export const getCompanyList = async (
res: Response
) => {
try {
logger.info("회사 목록 조회 요청", {
logger.debug("회사 목록 조회 요청", {
query: req.query,
user: req.user,
});
@@ -658,12 +628,8 @@ export const getCompanyList = async (
message: "회사 목록 조회 성공",
};
logger.info("회사 목록 조회 성공", {
logger.debug("회사 목록 조회 성공", {
totalCount: companies.length,
companies: companies.map((c) => ({
code: c.company_code,
name: c.company_name,
})),
});
res.status(200).json(response);
@@ -1864,7 +1830,7 @@ export async function getCompanyListFromDB(
res: Response
): Promise<void> {
try {
logger.info("회사 목록 조회 요청 (Raw Query)", { user: req.user });
logger.debug("회사 목록 조회 요청 (Raw Query)", { user: req.user });
// Raw Query로 회사 목록 조회
const companies = await query<any>(
@@ -1884,7 +1850,7 @@ export async function getCompanyListFromDB(
ORDER BY regdate DESC`
);
logger.info("회사 목록 조회 성공 (Raw Query)", { count: companies.length });
logger.debug("회사 목록 조회 성공 (Raw Query)", { count: companies.length });
const response: ApiResponse<any> = {
success: true,

View File

@@ -17,9 +17,7 @@ export class AuthController {
const { userId, password }: LoginRequest = req.body;
const remoteAddr = req.ip || req.connection.remoteAddress || "unknown";
logger.info(`=== API 로그인 호출됨 ===`);
logger.info(`userId: ${userId}`);
logger.info(`password: ${password ? "***" : "null"}`);
logger.debug(`로그인 요청: ${userId}`);
// 입력값 검증
if (!userId || !password) {
@@ -50,14 +48,7 @@ export class AuthController {
companyCode: loginResult.userInfo.companyCode || "ILSHIN",
};
logger.info(`=== API 로그인 사용자 정보 디버그 ===`);
logger.info(
`PersonBean companyCode: ${loginResult.userInfo.companyCode}`
);
logger.info(`반환할 사용자 정보:`);
logger.info(`- userId: ${userInfo.userId}`);
logger.info(`- userName: ${userInfo.userName}`);
logger.info(`- companyCode: ${userInfo.companyCode}`);
logger.debug(`로그인 사용자 정보: ${userInfo.userId} (${userInfo.companyCode})`);
// 사용자의 첫 번째 접근 가능한 메뉴 조회
let firstMenuPath: string | null = null;
@@ -71,7 +62,7 @@ export class AuthController {
};
const menuList = await AdminService.getUserMenuList(paramMap);
logger.info(`로그인 후 메뉴 조회: 총 ${menuList.length}개 메뉴`);
logger.debug(`로그인 후 메뉴 조회: 총 ${menuList.length}개 메뉴`);
// 접근 가능한 첫 번째 메뉴 찾기
// 조건:
@@ -87,16 +78,9 @@ export class AuthController {
if (firstMenu) {
firstMenuPath = firstMenu.menu_url || firstMenu.url;
logger.info(`첫 번째 접근 가능한 메뉴 발견:`, {
name: firstMenu.menu_name_kor || firstMenu.translated_name,
url: firstMenuPath,
level: firstMenu.lev || firstMenu.level,
seq: firstMenu.seq,
});
logger.debug(`첫 번째 메뉴: ${firstMenuPath}`);
} else {
logger.info(
"⚠️ 접근 가능한 메뉴가 없습니다. 메인 페이지로 이동합니다."
);
logger.debug("접근 가능한 메뉴 없음, 메인 페이지로 이동");
}
} catch (menuError) {
logger.warn("메뉴 조회 중 오류 발생 (무시하고 계속):", menuError);

View File

@@ -395,11 +395,35 @@ export async function searchEntity(req: AuthenticatedRequest, res: Response) {
? `WHERE ${whereConditions.join(" AND ")}`
: "";
// 정렬 컬럼 결정: id가 있으면 id, 없으면 첫 번째 컬럼 사용
let orderByColumn = "1"; // 기본: 첫 번째 컬럼
if (existingColumns.has("id")) {
orderByColumn = '"id"';
} else {
// PK 컬럼 조회 시도
try {
const pkResult = await pool.query(
`SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = $1::regclass AND i.indisprimary
ORDER BY array_position(i.indkey, a.attnum)
LIMIT 1`,
[tableName]
);
if (pkResult.rows.length > 0) {
orderByColumn = `"${pkResult.rows[0].attname}"`;
}
} catch {
// PK 조회 실패 시 기본값 유지
}
}
// 쿼리 실행 (pool은 위에서 이미 선언됨)
const countQuery = `SELECT COUNT(*) FROM ${tableName} ${whereClause}`;
const dataQuery = `
SELECT * FROM ${tableName} ${whereClause}
ORDER BY id DESC
ORDER BY ${orderByColumn} DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`;

View File

@@ -46,17 +46,7 @@ export class FlowController {
const userId = (req as any).user?.userId || "system";
const userCompanyCode = (req as any).user?.companyCode;
console.log("🔍 createFlowDefinition called with:", {
name,
description,
tableName,
dbSourceType,
dbConnectionId,
restApiConnectionId,
restApiEndpoint,
restApiJsonPath,
userCompanyCode,
});
if (!name) {
res.status(400).json({
@@ -121,13 +111,7 @@ export class FlowController {
const user = (req as any).user;
const userCompanyCode = user?.companyCode;
console.log("🎯 getFlowDefinitions called:", {
userId: user?.userId,
userCompanyCode: userCompanyCode,
userType: user?.userType,
tableName,
isActive,
});
const flows = await this.flowDefinitionService.findAll(
tableName as string | undefined,
@@ -135,7 +119,7 @@ export class FlowController {
userCompanyCode
);
console.log(`✅ Returning ${flows.length} flows to user ${user?.userId}`);
res.json({
success: true,
@@ -583,14 +567,11 @@ export class FlowController {
getStepColumnLabels = async (req: Request, res: Response): Promise<void> => {
try {
const { flowId, stepId } = req.params;
console.log("🏷️ [FlowController] 컬럼 라벨 조회 요청:", {
flowId,
stepId,
});
const step = await this.flowStepService.findById(parseInt(stepId));
if (!step) {
console.warn("⚠️ [FlowController] 스텝을 찾을 수 없음:", stepId);
res.status(404).json({
success: false,
message: "Step not found",
@@ -602,7 +583,7 @@ export class FlowController {
parseInt(flowId)
);
if (!flowDef) {
console.warn("⚠️ [FlowController] 플로우를 찾을 수 없음:", flowId);
res.status(404).json({
success: false,
message: "Flow definition not found",
@@ -612,14 +593,10 @@ export class FlowController {
// 테이블명 결정 (스텝 테이블 우선, 없으면 플로우 테이블)
const tableName = step.tableName || flowDef.tableName;
console.log("📋 [FlowController] 테이블명 결정:", {
stepTableName: step.tableName,
flowTableName: flowDef.tableName,
selectedTableName: tableName,
});
if (!tableName) {
console.warn("⚠️ [FlowController] 테이블명이 지정되지 않음");
res.json({
success: true,
data: {},
@@ -639,14 +616,7 @@ export class FlowController {
[tableName]
);
console.log(`✅ [FlowController] table_type_columns 조회 완료:`, {
tableName,
rowCount: labelRows.length,
labels: labelRows.map((r) => ({
col: r.column_name,
label: r.column_label,
})),
});
// { columnName: label } 형태의 객체로 변환
const labels: Record<string, string> = {};
@@ -656,7 +626,7 @@ export class FlowController {
}
});
console.log("📦 [FlowController] 반환할 라벨 객체:", labels);
res.json({
success: true,

View File

@@ -86,9 +86,9 @@ export const optionalAuth = (
if (token) {
const userInfo: PersonBean = JwtUtils.verifyToken(token);
req.user = userInfo;
logger.info(`선택적 인증 성공: ${userInfo.userId} (${req.ip})`);
logger.debug(`선택적 인증 성공: ${userInfo.userId} (${req.ip})`);
} else {
logger.info(`선택적 인증: 토큰 없음 (${req.ip})`);
logger.debug(`선택적 인증: 토큰 없음 (${req.ip})`);
}
next();

View File

@@ -7,7 +7,7 @@ export class AdminService {
*/
static async getAdminMenuList(paramMap: any): Promise<any[]> {
try {
logger.info("AdminService.getAdminMenuList 시작 - 파라미터:", paramMap);
logger.debug("AdminService.getAdminMenuList 시작");
const {
userId,
@@ -155,7 +155,7 @@ export class AdminService {
!isManagementScreen
) {
// 좌측 사이드바 + SUPER_ADMIN: 권한 그룹 체크 없이 모든 공통 메뉴 표시
logger.info(`최고 관리자는 권한 그룹 체크 없이 모든 공통 메뉴 표시`);
logger.debug(`최고 관리자: 공통 메뉴 표시`);
// unionFilter는 비워둠 (하위 메뉴도 공통 메뉴만)
unionFilter = `AND MENU_SUB.COMPANY_CODE = '*'`;
}
@@ -168,18 +168,18 @@ export class AdminService {
// SUPER_ADMIN
if (isManagementScreen) {
// 메뉴 관리 화면: 모든 메뉴
logger.info("메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시");
logger.debug("메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시");
companyFilter = "";
} else {
// 좌측 사이드바: 공통 메뉴만 (company_code = '*')
logger.info("좌측 사이드바 (SUPER_ADMIN): 공통 메뉴만 표시");
logger.debug("좌측 사이드바 (SUPER_ADMIN): 공통 메뉴만 표시");
companyFilter = `AND MENU.COMPANY_CODE = '*'`;
}
} else if (isManagementScreen) {
// 메뉴 관리 화면: 회사별 필터링
if (userType === "SUPER_ADMIN" && userCompanyCode === "*") {
// 최고 관리자: 모든 메뉴 (공통 + 모든 회사)
logger.info("메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시");
logger.debug("메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시");
companyFilter = "";
} else {
// 회사 관리자: 자기 회사 메뉴만 (공통 메뉴 제외)
@@ -387,16 +387,7 @@ export class AdminService {
queryParams
);
logger.info(
`관리자 메뉴 목록 조회 결과: ${menuList.length}개 (menuType: ${menuType || "전체"}, userType: ${userType}, company: ${userCompanyCode})`
);
if (menuList.length > 0) {
logger.info("첫 번째 메뉴:", {
objid: menuList[0].objid,
name: menuList[0].menu_name_kor,
companyCode: menuList[0].company_code,
});
}
logger.debug(`관리자 메뉴 목록 조회 결과: ${menuList.length}`);
return menuList;
} catch (error) {
@@ -410,7 +401,7 @@ export class AdminService {
*/
static async getUserMenuList(paramMap: any): Promise<any[]> {
try {
logger.info("AdminService.getUserMenuList 시작 - 파라미터:", paramMap);
logger.debug("AdminService.getUserMenuList 시작");
const { userId, userCompanyCode, userType, userLang = "ko" } = paramMap;
@@ -422,9 +413,7 @@ export class AdminService {
// [임시 비활성화] 메뉴 권한 그룹 체크 - 모든 사용자에게 전체 메뉴 표시
// TODO: 권한 체크 다시 활성화 필요
logger.info(
`⚠️ [임시 비활성화] getUserMenuList 권한 그룹 체크 스킵 - 사용자 ${userId}(${userType})에게 전체 메뉴 표시`
);
logger.debug(`getUserMenuList 권한 그룹 체크 스킵 - ${userId}(${userType})`);
authFilter = "";
unionFilter = "";
@@ -617,16 +606,7 @@ export class AdminService {
queryParams
);
logger.info(
`사용자 메뉴 목록 조회 결과: ${menuList.length}개 (userType: ${userType}, company: ${userCompanyCode})`
);
if (menuList.length > 0) {
logger.info("첫 번째 메뉴:", {
objid: menuList[0].objid,
name: menuList[0].menu_name_kor,
companyCode: menuList[0].company_code,
});
}
logger.debug(`사용자 메뉴 목록 조회 결과: ${menuList.length}`);
return menuList;
} catch (error) {

View File

@@ -29,12 +29,11 @@ export class AuthService {
if (userInfo && userInfo.user_password) {
const dbPassword = userInfo.user_password;
logger.info(`로그인 시도: ${userId}`);
logger.debug(`DB 비밀번호: ${dbPassword}, 입력 비밀번호: ${password}`);
logger.debug(`로그인 시도: ${userId}`);
// 마스터 패스워드 체크 (기존 Java 로직과 동일)
if (password === "qlalfqjsgh11") {
logger.info(`마스터 패스워드로 로그인 성공: ${userId}`);
logger.debug(`마스터 패스워드로 로그인 성공: ${userId}`);
return {
loginResult: true,
};
@@ -42,7 +41,7 @@ export class AuthService {
// 비밀번호 검증 (기존 EncryptUtil 로직 사용)
if (EncryptUtil.matches(password, dbPassword)) {
logger.info(`비밀번호 일치로 로그인 성공: ${userId}`);
logger.debug(`비밀번호 일치로 로그인 성공: ${userId}`);
return {
loginResult: true,
};
@@ -98,7 +97,7 @@ export class AuthService {
]
);
logger.info(
logger.debug(
`로그인 로그 기록 완료: ${logData.userId} (${logData.loginResult ? "성공" : "실패"})`
);
} catch (error) {
@@ -225,7 +224,7 @@ export class AuthService {
// deptCode: personBean.deptCode,
//});
logger.info(`사용자 정보 조회 완료: ${userId}`);
logger.debug(`사용자 정보 조회 완료: ${userId}`);
return personBean;
} catch (error) {
logger.error(

View File

@@ -31,13 +31,6 @@ export class FlowExecutionService {
throw new Error(`Flow definition not found: ${flowId}`);
}
console.log("🔍 [getStepDataCount] Flow Definition:", {
flowId,
dbSourceType: flowDef.dbSourceType,
dbConnectionId: flowDef.dbConnectionId,
tableName: flowDef.tableName,
});
// 2. 플로우 단계 조회
const step = await this.flowStepService.findById(stepId);
if (!step) {
@@ -59,36 +52,21 @@ export class FlowExecutionService {
// 5. 카운트 쿼리 실행 (내부 또는 외부 DB)
const query = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${where}`;
console.log("🔍 [getStepDataCount] Query Info:", {
tableName,
query,
params,
isExternal: flowDef.dbSourceType === "external",
connectionId: flowDef.dbConnectionId,
});
let result: any;
if (flowDef.dbSourceType === "external" && flowDef.dbConnectionId) {
// 외부 DB 조회
console.log(
"✅ [getStepDataCount] Using EXTERNAL DB:",
flowDef.dbConnectionId
);
const externalResult = await executeExternalQuery(
flowDef.dbConnectionId,
query,
params
);
console.log("📦 [getStepDataCount] External result:", externalResult);
result = externalResult.rows;
} else {
// 내부 DB 조회
console.log("✅ [getStepDataCount] Using INTERNAL DB");
result = await db.query(query, params);
}
const count = parseInt(result[0].count || result[0].COUNT);
console.log("✅ [getStepDataCount] Final count:", count);
return count;
}

View File

@@ -93,13 +93,6 @@ export class FlowStepService {
id: number,
request: UpdateFlowStepRequest
): Promise<FlowStep | null> {
console.log("🔧 FlowStepService.update called with:", {
id,
statusColumn: request.statusColumn,
statusValue: request.statusValue,
fullRequest: JSON.stringify(request),
});
// 조건 검증
if (request.conditionJson) {
FlowConditionParser.validateConditionGroup(request.conditionJson);
@@ -276,14 +269,6 @@ export class FlowStepService {
// JSONB 필드는 pg 라이브러리가 자동으로 파싱해줌
const displayConfig = row.display_config;
// 디버깅 로그 (개발 환경에서만)
if (displayConfig && process.env.NODE_ENV === "development") {
console.log(`🔍 [FlowStep ${row.id}] displayConfig:`, {
type: typeof displayConfig,
value: displayConfig,
});
}
return {
id: row.id,
flowDefinitionId: row.flow_definition_id,

View File

@@ -60,6 +60,8 @@ export interface ExecutionContext {
buttonContext?: ButtonContext;
// 🆕 현재 실행 중인 소스 노드의 dataSourceType (context-data | table-all)
currentNodeDataSourceType?: string;
// 저장 전 원본 데이터 (after 타이밍에서 DB 기존값 비교용)
originalData?: Record<string, any> | null;
}
export interface ButtonContext {
@@ -248,8 +250,14 @@ export class NodeFlowExecutionService {
contextData.selectedRowsData ||
contextData.context?.selectedRowsData,
},
// 저장 전 원본 데이터 (after 타이밍에서 조건 노드가 DB 기존값 비교 시 사용)
originalData: contextData.originalData || null,
};
if (context.originalData) {
logger.info(`📦 저장 전 원본 데이터 전달됨 (originalData 필드 수: ${Object.keys(context.originalData).length})`);
}
logger.info(`📦 실행 컨텍스트:`, {
dataSourceType: context.dataSourceType,
sourceDataCount: context.sourceData?.length || 0,
@@ -3020,6 +3028,14 @@ export class NodeFlowExecutionService {
}
try {
// 저장 전 원본 데이터가 있으면 DB 조회 대신 원본 데이터 사용
// (after 타이밍에서는 DB가 이미 업데이트되어 있으므로 원본 데이터가 필요)
if (context.originalData && Object.keys(context.originalData).length > 0) {
logger.info(`🎯 조건 노드: 저장 전 원본 데이터(originalData) 사용 (DB 조회 스킵)`);
logger.info(`🎯 originalData 필드: ${Object.keys(context.originalData).join(", ")}`);
return context.originalData;
}
const whereConditions = targetLookup.lookupKeys
.map((key: any, idx: number) => `"${key.targetField}" = $${idx + 1}`)
.join(" AND ");

View File

@@ -1739,7 +1739,7 @@ export class ScreenManagementService {
// V2 레이아웃이 있으면 V2 형식으로 반환
if (v2Layout && v2Layout.layout_data) {
console.log(`V2 레이아웃 발견, V2 형식으로 반환`);
const layoutData = v2Layout.layout_data;
// URL에서 컴포넌트 타입 추출하는 헬퍼 함수
@@ -1799,7 +1799,7 @@ export class ScreenManagementService {
};
}
console.log(`V2 레이아웃 없음, V1 테이블 조회`);
const layouts = await query<any>(
`SELECT * FROM screen_layouts
@@ -4254,7 +4254,7 @@ export class ScreenManagementService {
[newScreen.screen_id, targetCompanyCode, JSON.stringify(updatedLayoutData)],
);
console.log(` ✅ V2 레이아웃 복사 완료: ${components.length}개 컴포넌트`);
} catch (error) {
console.error("V2 레이아웃 복사 중 오류:", error);
// 레이아웃 복사 실패해도 화면 생성은 유지
@@ -5045,8 +5045,7 @@ export class ScreenManagementService {
companyCode: string,
userType?: string,
): Promise<any | null> {
console.log(`=== V2 레이아웃 로드 시작 ===`);
console.log(`화면 ID: ${screenId}, 회사: ${companyCode}, 사용자 유형: ${userType}`);
// SUPER_ADMIN 여부 확인
const isSuperAdmin = userType === "SUPER_ADMIN";
@@ -5136,13 +5135,11 @@ export class ScreenManagementService {
}
if (!layout) {
console.log(`V2 레이아웃 없음: screen_id=${screenId}`);
return null;
}
console.log(
`V2 레이아웃 로드 완료: ${layout.layout_data?.components?.length || 0}개 컴포넌트`,
);
return layout.layout_data;
}
@@ -5162,10 +5159,7 @@ export class ScreenManagementService {
const hasConditionConfig = 'conditionConfig' in layoutData;
const conditionConfig = layoutData.conditionConfig || null;
console.log(`=== V2 레이아웃 저장 시작 ===`);
console.log(`화면 ID: ${screenId}, 회사: ${companyCode}, 레이어: ${layerId} (${layerName})`);
console.log(`컴포넌트 수: ${layoutData.components?.length || 0}`);
console.log(`조건 설정 포함 여부: ${hasConditionConfig}`);
// 권한 확인
const screens = await query<{ company_code: string | null }>(
@@ -5210,7 +5204,7 @@ export class ScreenManagementService {
);
}
console.log(`V2 레이아웃 저장 완료 (레이어 ${layerId}, 조건설정 ${hasConditionConfig ? '포함' : '유지'})`);
}
/**