feat(pop): POP 화면 복사 기능 구현 (단일 화면 + 카테고리 일괄 복사)

최고관리자의 POP 화면을 다른 회사로 복사하는 기능 추가.
화면 단위 복사와 카테고리(그룹) 단위 일괄 복사를 모두 지원하며,
화면 간 참조(cartScreenId, sourceScreenId 등)를 자동 치환하고
카테고리 구조까지 대상 회사에 재생성한다.
[백엔드]
- analyzePopScreenLinks: POP 레이아웃 내 다른 화면 참조 스캔
- deployPopScreens: screen_definitions + screen_layouts_pop 복사,
  screenId 참조 치환, numberingRuleId 초기화, 그룹 구조 복사
- POP 그룹 조회 쿼리 개선 (screen_layouts_pop JOIN으로 실제 POP 화면만 카운트)
- ensurePopRootGroup 최고관리자 전용으로 변경
[프론트엔드]
- PopDeployModal: 단일 화면/카테고리 일괄 복사 모달 (대상 회사 선택,
  연결 화면 감지, 카테고리 트리 미리보기)
- PopCategoryTree: 그룹 컨텍스트 메뉴에 '카테고리 복사' 추가,
  하위 그룹 화면까지 재귀 수집
- PopScreenSettingModal: UI 간소화 및 화면명 저장 기능 보완
- screenApi: analyzePopScreenLinks, deployPopScreens 클라이언트 함수 추가
This commit is contained in:
SeongHyun Kim
2026-03-04 11:41:31 +09:00
parent 6c9e35e8b2
commit ce5c2426b5
10 changed files with 1539 additions and 125 deletions
@@ -2574,11 +2574,11 @@ export const getPopScreenGroups = async (req: AuthenticatedRequest, res: Respons
const companyCode = req.user?.companyCode || "*";
const { searchTerm } = req.query;
let whereClause = "WHERE hierarchy_path LIKE 'POP/%' OR hierarchy_path = 'POP'";
let whereClause = "WHERE (hierarchy_path LIKE 'POP/%' OR hierarchy_path = 'POP')";
const params: any[] = [];
let paramIndex = 1;
// 회사 코드 필터링 (멀티테넌시)
// 회사 코드 필터링 (멀티테넌시) - 일반 회사는 자기 회사 데이터만
if (companyCode !== "*") {
whereClause += ` AND company_code = $${paramIndex}`;
params.push(companyCode);
@@ -2592,11 +2592,13 @@ export const getPopScreenGroups = async (req: AuthenticatedRequest, res: Respons
paramIndex++;
}
// POP 그룹 조회 (계층 구조를 위해 전체 조회)
// POP 그룹 조회 (POP 레이아웃이 있는 화면만 카운트/포함)
const dataQuery = `
SELECT
sg.*,
(SELECT COUNT(*) FROM screen_group_screens sgs WHERE sgs.group_id = sg.id) as screen_count,
(SELECT COUNT(*) FROM screen_group_screens sgs
INNER JOIN screen_layouts_pop slp ON sgs.screen_id = slp.screen_id AND sgs.company_code = slp.company_code
WHERE sgs.group_id = sg.id AND sgs.company_code = sg.company_code) as screen_count,
(SELECT json_agg(
json_build_object(
'id', sgs.id,
@@ -2609,7 +2611,8 @@ export const getPopScreenGroups = async (req: AuthenticatedRequest, res: Respons
) ORDER BY sgs.display_order
) FROM screen_group_screens sgs
LEFT JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id
WHERE sgs.group_id = sg.id
INNER JOIN screen_layouts_pop slp ON sgs.screen_id = slp.screen_id AND sgs.company_code = slp.company_code
WHERE sgs.group_id = sg.id AND sgs.company_code = sg.company_code
) as screens
FROM screen_groups sg
${whereClause}
@@ -2768,6 +2771,14 @@ export const deletePopScreenGroup = async (req: AuthenticatedRequest, res: Respo
const existing = await pool.query(checkQuery, checkParams);
if (existing.rows.length === 0) {
// 그룹이 다른 회사 소속인지 확인하여 구체적 메시지 제공
const anyGroup = await pool.query(`SELECT company_code FROM screen_groups WHERE id = $1`, [id]);
if (anyGroup.rows.length > 0) {
return res.status(403).json({
success: false,
message: `이 그룹은 ${anyGroup.rows[0].company_code === "*" ? "최고관리자" : anyGroup.rows[0].company_code} 소속이라 삭제할 수 없습니다.`
});
}
return res.status(404).json({ success: false, message: "그룹을 찾을 수 없습니다." });
}
@@ -2782,7 +2793,10 @@ export const deletePopScreenGroup = async (req: AuthenticatedRequest, res: Respo
[id]
);
if (parseInt(childCheck.rows[0].count) > 0) {
return res.status(400).json({ success: false, message: "하위 그룹이 있어 삭제할 수 없습니다." });
return res.status(400).json({
success: false,
message: `하위 그룹이 ${childCheck.rows[0].count}개 있어 삭제할 수 없습니다. 하위 그룹을 먼저 삭제해주세요.`
});
}
// 연결된 화면 확인
@@ -2791,7 +2805,10 @@ export const deletePopScreenGroup = async (req: AuthenticatedRequest, res: Respo
[id]
);
if (parseInt(screenCheck.rows[0].count) > 0) {
return res.status(400).json({ success: false, message: "그룹에 연결된 화면이 있어 삭제할 수 없습니다." });
return res.status(400).json({
success: false,
message: `그룹에 연결된 화면이 ${screenCheck.rows[0].count}개 있어 삭제할 수 없습니다. 화면을 먼저 제거해주세요.`
});
}
// 삭제
@@ -2806,33 +2823,44 @@ export const deletePopScreenGroup = async (req: AuthenticatedRequest, res: Respo
}
};
// POP 루트 그룹 확보 (없으면 자동 생성)
// POP 루트 그룹 확보 (최고관리자 전용 - 일반 회사는 복사 기능으로 배포)
export const ensurePopRootGroup = async (req: AuthenticatedRequest, res: Response) => {
try {
const companyCode = req.user?.companyCode || "*";
// POP 루트 그룹 확인
const checkQuery = `
SELECT * FROM screen_groups
WHERE hierarchy_path = 'POP' AND company_code = $1
`;
const existing = await pool.query(checkQuery, [companyCode]);
if (existing.rows.length > 0) {
return res.json({ success: true, data: existing.rows[0], message: "POP 루트 그룹이 이미 존재합니다." });
// 최고관리자만 자동 생성
if (companyCode !== "*") {
const existing = await pool.query(
`SELECT * FROM screen_groups WHERE hierarchy_path = 'POP' AND company_code = $1`,
[companyCode]
);
if (existing.rows.length > 0) {
return res.json({ success: true, data: existing.rows[0] });
}
return res.json({ success: true, data: null, message: "POP 루트 그룹이 없습니다." });
}
// 최고관리자(*): 루트 그룹 확인 후 없으면 생성
const checkQuery = `
SELECT * FROM screen_groups
WHERE hierarchy_path = 'POP' AND company_code = '*'
`;
const existing = await pool.query(checkQuery, []);
if (existing.rows.length > 0) {
return res.json({ success: true, data: existing.rows[0] });
}
// 없으면 생성 (writer 컬럼 사용, is_active는 'Y' - 기존 스키마에 맞춤)
const insertQuery = `
INSERT INTO screen_groups (
group_name, group_code, hierarchy_path, company_code,
description, display_order, is_active, writer
) VALUES ('POP 화면', 'POP', 'POP', $1, 'POP 화면 관리 루트', 0, 'Y', $2)
) VALUES ('POP 화면', 'POP', 'POP', '*', 'POP 화면 관리 루트', 0, 'Y', $1)
RETURNING *
`;
const result = await pool.query(insertQuery, [companyCode, req.user?.userId || ""]);
const result = await pool.query(insertQuery, [req.user?.userId || ""]);
logger.info("POP 루트 그룹 생성", { groupId: result.rows[0].id, companyCode });
logger.info("POP 루트 그룹 생성", { groupId: result.rows[0].id });
res.json({ success: true, data: result.rows[0], message: "POP 루트 그룹이 생성되었습니다." });
} catch (error: any) {
@@ -1237,3 +1237,82 @@ export const copyCascadingRelation = async (
});
}
};
// POP 화면 연결 분석
export const analyzePopScreenLinks = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { screenId } = req.params;
const { companyCode } = req.user as any;
const result = await screenManagementService.analyzePopScreenLinks(
parseInt(screenId),
companyCode,
);
res.json({ success: true, data: result });
} catch (error: any) {
console.error("POP 화면 연결 분석 실패:", error);
res.status(500).json({
success: false,
message: error.message || "POP 화면 연결 분석에 실패했습니다.",
});
}
};
// POP 화면 배포 (다른 회사로 복사)
export const deployPopScreens = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { screens, targetCompanyCode, groupStructure } = req.body;
const { companyCode, userId } = req.user as any;
if (!screens || !Array.isArray(screens) || screens.length === 0) {
res.status(400).json({
success: false,
message: "배포할 화면 목록이 필요합니다.",
});
return;
}
if (!targetCompanyCode) {
res.status(400).json({
success: false,
message: "대상 회사 코드가 필요합니다.",
});
return;
}
if (companyCode !== "*") {
res.status(403).json({
success: false,
message: "최고 관리자만 POP 화면을 배포할 수 있습니다.",
});
return;
}
const result = await screenManagementService.deployPopScreens({
screens,
groupStructure: groupStructure || undefined,
targetCompanyCode,
companyCode,
userId,
});
res.json({
success: true,
data: result,
message: `POP 화면 ${result.deployedScreens.length}개가 ${targetCompanyCode}에 배포되었습니다.`,
});
} catch (error: any) {
console.error("POP 화면 배포 실패:", error);
res.status(500).json({
success: false,
message: error.message || "POP 화면 배포에 실패했습니다.",
});
}
};
@@ -42,6 +42,8 @@ import {
copyCategoryMapping,
copyTableTypeColumns,
copyCascadingRelation,
analyzePopScreenLinks,
deployPopScreens,
} from "../controllers/screenManagementController";
const router = express.Router();
@@ -123,4 +125,8 @@ router.post("/copy-table-type-columns", copyTableTypeColumns);
// 연쇄관계 설정 복제
router.post("/copy-cascading-relation", copyCascadingRelation);
// POP 화면 배포 (다른 회사로 복사)
router.get("/screens/:screenId/pop-links", analyzePopScreenLinks);
router.post("/deploy-pop-screens", deployPopScreens);
export default router;
@@ -5425,28 +5425,24 @@ export class ScreenManagementService {
async getScreenIdsWithPopLayout(
companyCode: string,
): Promise<number[]> {
console.log(`=== POP 레이아웃 존재 화면 ID 조회 ===`);
console.log(`회사 코드: ${companyCode}`);
let result: { screen_id: number }[];
if (companyCode === "*") {
// 최고 관리자: 모든 POP 레이아웃 조회
result = await query<{ screen_id: number }>(
`SELECT DISTINCT screen_id FROM screen_layouts_pop`,
[],
);
} else {
// 일반 회사: 해당 회사 또는 공통(*) 레이아웃 조회
// 일반 회사: 해당 회사 레이아웃 조회 (company_code='*'는 최고관리자 전용)
result = await query<{ screen_id: number }>(
`SELECT DISTINCT screen_id FROM screen_layouts_pop
WHERE company_code = $1 OR company_code = '*'`,
WHERE company_code = $1`,
[companyCode],
);
}
const screenIds = result.map((r) => r.screen_id);
console.log(`POP 레이아웃 존재 화면 수: ${screenIds.length}`);
logger.info("POP 레이아웃 존재 화면 ID 조회", { companyCode, count: screenIds.length });
return screenIds;
}
@@ -5484,6 +5480,512 @@ export class ScreenManagementService {
console.log(`POP 레이아웃 삭제 완료`);
return true;
}
// ============================================================
// POP 화면 배포 (다른 회사로 복사)
// ============================================================
/**
* POP layout_data 내 다른 화면 참조를 스캔하여 연결 관계 분석
*/
async analyzePopScreenLinks(
screenId: number,
companyCode: string,
): Promise<{
linkedScreenIds: number[];
references: Array<{
componentId: string;
referenceType: string;
targetScreenId: number;
}>;
}> {
const layoutResult = await queryOne<{ layout_data: any }>(
`SELECT layout_data FROM screen_layouts_pop
WHERE screen_id = $1 AND company_code = $2`,
[screenId, companyCode],
);
if (!layoutResult?.layout_data) {
return { linkedScreenIds: [], references: [] };
}
const layoutData = layoutResult.layout_data;
const references: Array<{
componentId: string;
referenceType: string;
targetScreenId: number;
}> = [];
const scanComponents = (components: Record<string, any>) => {
for (const [compId, comp] of Object.entries(components)) {
const config = (comp as any).config || {};
if (config.cart?.cartScreenId) {
const sid = parseInt(config.cart.cartScreenId);
if (!isNaN(sid) && sid !== screenId) {
references.push({
componentId: compId,
referenceType: "cartScreenId",
targetScreenId: sid,
});
}
}
if (config.cartListMode?.sourceScreenId) {
const sid =
typeof config.cartListMode.sourceScreenId === "number"
? config.cartListMode.sourceScreenId
: parseInt(config.cartListMode.sourceScreenId);
if (!isNaN(sid) && sid !== screenId) {
references.push({
componentId: compId,
referenceType: "sourceScreenId",
targetScreenId: sid,
});
}
}
if (Array.isArray(config.followUpActions)) {
for (const action of config.followUpActions) {
if (action.targetScreenId) {
const sid = parseInt(action.targetScreenId);
if (!isNaN(sid) && sid !== screenId) {
references.push({
componentId: compId,
referenceType: "targetScreenId",
targetScreenId: sid,
});
}
}
}
}
if (config.action?.modalScreenId) {
const sid = parseInt(config.action.modalScreenId);
if (!isNaN(sid) && sid !== screenId) {
references.push({
componentId: compId,
referenceType: "modalScreenId",
targetScreenId: sid,
});
}
}
}
};
if (layoutData.components) {
scanComponents(layoutData.components);
}
if (Array.isArray(layoutData.modals)) {
for (const modal of layoutData.modals) {
if (modal.components) {
scanComponents(modal.components);
}
}
}
const linkedScreenIds = [
...new Set(references.map((r) => r.targetScreenId)),
];
return { linkedScreenIds, references };
}
/**
* POP 화면 배포 (최고관리자 화면을 특정 회사로 복사)
* - screen_definitions + screen_layouts_pop 복사
* - 화면 간 참조(cartScreenId, sourceScreenId 등) 자동 치환
* - numberingRuleId 초기화
*/
async deployPopScreens(data: {
screens: Array<{
sourceScreenId: number;
screenName: string;
screenCode: string;
}>;
groupStructure?: {
sourceGroupId: number;
groupName: string;
groupCode: string;
children?: Array<{
sourceGroupId: number;
groupName: string;
groupCode: string;
screenIds: number[];
}>;
screenIds: number[];
};
targetCompanyCode: string;
companyCode: string;
userId: string;
}): Promise<{
deployedScreens: Array<{
sourceScreenId: number;
newScreenId: number;
screenName: string;
screenCode: string;
}>;
createdGroups?: number;
}> {
if (data.companyCode !== "*") {
throw new Error("최고 관리자만 POP 화면을 배포할 수 있습니다.");
}
return await transaction(async (client) => {
const screenIdMap = new Map<number, number>();
const deployedScreens: Array<{
sourceScreenId: number;
newScreenId: number;
screenName: string;
screenCode: string;
}> = [];
// 1단계: screen_definitions 복사
for (const screen of data.screens) {
const sourceResult = await client.query<any>(
`SELECT * FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
[screen.sourceScreenId],
);
if (sourceResult.rows.length === 0) {
throw new Error(
`원본 화면(ID: ${screen.sourceScreenId})을 찾을 수 없습니다.`,
);
}
const sourceScreen = sourceResult.rows[0];
const existingResult = await client.query<any>(
`SELECT screen_id FROM screen_definitions
WHERE screen_code = $1 AND company_code = $2 AND deleted_date IS NULL
LIMIT 1`,
[screen.screenCode, data.targetCompanyCode],
);
if (existingResult.rows.length > 0) {
throw new Error(
`화면 코드 "${screen.screenCode}"가 대상 회사에 이미 존재합니다.`,
);
}
const newScreenResult = await client.query<any>(
`INSERT INTO screen_definitions (
screen_code, screen_name, description, company_code, table_name,
is_active, created_by, created_date, updated_by, updated_date
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $7, NOW())
RETURNING *`,
[
screen.screenCode,
screen.screenName,
sourceScreen.description,
data.targetCompanyCode,
sourceScreen.table_name,
"Y",
data.userId,
],
);
const newScreen = newScreenResult.rows[0];
screenIdMap.set(screen.sourceScreenId, newScreen.screen_id);
deployedScreens.push({
sourceScreenId: screen.sourceScreenId,
newScreenId: newScreen.screen_id,
screenName: screen.screenName,
screenCode: screen.screenCode,
});
logger.info("POP 화면 배포 - screen_definitions 생성", {
sourceScreenId: screen.sourceScreenId,
newScreenId: newScreen.screen_id,
targetCompanyCode: data.targetCompanyCode,
});
}
// 2단계: screen_layouts_pop 복사 + 참조 치환
for (const screen of data.screens) {
const newScreenId = screenIdMap.get(screen.sourceScreenId);
if (!newScreenId) continue;
// 원본 POP 레이아웃 조회 (company_code = '*' 우선, fallback)
let layoutResult = await client.query<{ layout_data: any }>(
`SELECT layout_data FROM screen_layouts_pop
WHERE screen_id = $1 AND company_code = '*'`,
[screen.sourceScreenId],
);
let layoutData = layoutResult.rows[0]?.layout_data;
if (!layoutData) {
const fallbackResult = await client.query<{ layout_data: any }>(
`SELECT layout_data FROM screen_layouts_pop
WHERE screen_id = $1 LIMIT 1`,
[screen.sourceScreenId],
);
layoutData = fallbackResult.rows[0]?.layout_data;
}
if (!layoutData) {
logger.warn("POP 레이아웃 없음, 건너뜀", {
sourceScreenId: screen.sourceScreenId,
});
continue;
}
const updatedLayoutData = this.updatePopLayoutScreenReferences(
JSON.parse(JSON.stringify(layoutData)),
screenIdMap,
);
await client.query(
`INSERT INTO screen_layouts_pop (screen_id, company_code, layout_data, created_at, updated_at, created_by, updated_by)
VALUES ($1, $2, $3, NOW(), NOW(), $4, $4)
ON CONFLICT (screen_id, company_code)
DO UPDATE SET layout_data = $3, updated_at = NOW(), updated_by = $4`,
[
newScreenId,
data.targetCompanyCode,
JSON.stringify(updatedLayoutData),
data.userId,
],
);
logger.info("POP 레이아웃 복사 완료", {
sourceScreenId: screen.sourceScreenId,
newScreenId,
componentCount: Object.keys(updatedLayoutData.components || {})
.length,
});
}
// 3단계: 그룹 구조 복사 (groupStructure가 있는 경우)
let createdGroups = 0;
if (data.groupStructure) {
const gs = data.groupStructure;
// 대상 회사의 POP 루트 그룹 찾기/생성
let popRootResult = await client.query<any>(
`SELECT id FROM screen_groups
WHERE hierarchy_path = 'POP' AND company_code = $1 LIMIT 1`,
[data.targetCompanyCode],
);
let popRootId: number;
if (popRootResult.rows.length > 0) {
popRootId = popRootResult.rows[0].id;
} else {
const createRootResult = await client.query<any>(
`INSERT INTO screen_groups (group_name, group_code, hierarchy_path, company_code, writer, is_active, display_order)
VALUES ('POP 화면', 'POP_ROOT', 'POP', $1, $2, 'Y', 0) RETURNING id`,
[data.targetCompanyCode, data.userId],
);
popRootId = createRootResult.rows[0].id;
}
// 메인 그룹 생성 (중복 코드 방지: _COPY 접미사 추가)
const mainGroupCode = gs.groupCode + "_COPY";
const dupCheck = await client.query<any>(
`SELECT id FROM screen_groups WHERE group_code = $1 AND company_code = $2`,
[mainGroupCode, data.targetCompanyCode],
);
let mainGroupId: number;
if (dupCheck.rows.length > 0) {
mainGroupId = dupCheck.rows[0].id;
} else {
const mainGroupResult = await client.query<any>(
`INSERT INTO screen_groups (group_name, group_code, hierarchy_path, company_code, parent_group_id, writer, is_active, display_order)
VALUES ($1, $2, $3, $4, $5, $6, 'Y', 0) RETURNING id`,
[
gs.groupName,
mainGroupCode,
`POP/${mainGroupCode}`,
data.targetCompanyCode,
popRootId,
data.userId,
],
);
mainGroupId = mainGroupResult.rows[0].id;
createdGroups++;
}
// 메인 그룹에 화면 연결
for (const oldScreenId of gs.screenIds) {
const newScreenId = screenIdMap.get(oldScreenId);
if (!newScreenId) continue;
await client.query(
`INSERT INTO screen_group_screens (group_id, screen_id, screen_role, display_order, is_default, company_code)
VALUES ($1, $2, 'main', 0, 'N', $3)
ON CONFLICT DO NOTHING`,
[mainGroupId, newScreenId, data.targetCompanyCode],
);
}
// 하위 그룹 생성 + 화면 연결
if (gs.children) {
for (let i = 0; i < gs.children.length; i++) {
const child = gs.children[i];
const childGroupCode = child.groupCode + "_COPY";
const childDupCheck = await client.query<any>(
`SELECT id FROM screen_groups WHERE group_code = $1 AND company_code = $2`,
[childGroupCode, data.targetCompanyCode],
);
let childGroupId: number;
if (childDupCheck.rows.length > 0) {
childGroupId = childDupCheck.rows[0].id;
} else {
const childResult = await client.query<any>(
`INSERT INTO screen_groups (group_name, group_code, hierarchy_path, company_code, parent_group_id, writer, is_active, display_order)
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7) RETURNING id`,
[
child.groupName,
childGroupCode,
`POP/${mainGroupCode}/${childGroupCode}`,
data.targetCompanyCode,
mainGroupId,
data.userId,
i,
],
);
childGroupId = childResult.rows[0].id;
createdGroups++;
}
for (const oldScreenId of child.screenIds) {
const newScreenId = screenIdMap.get(oldScreenId);
if (!newScreenId) continue;
await client.query(
`INSERT INTO screen_group_screens (group_id, screen_id, screen_role, display_order, is_default, company_code)
VALUES ($1, $2, 'main', 0, 'N', $3)
ON CONFLICT DO NOTHING`,
[childGroupId, newScreenId, data.targetCompanyCode],
);
}
}
}
logger.info("POP 그룹 구조 복사 완료", {
targetCompanyCode: data.targetCompanyCode,
createdGroups,
mainGroupName: gs.groupName,
});
}
return { deployedScreens, createdGroups };
});
}
/**
* POP layout_data 내 screen_id 참조 치환
* componentId, connectionId는 레이아웃 내부 식별자이므로 변경 불필요
*/
private updatePopLayoutScreenReferences(
layoutData: any,
screenIdMap: Map<number, number>,
): any {
if (!layoutData?.components) return layoutData;
const updateComponents = (
components: Record<string, any>,
): Record<string, any> => {
const updated: Record<string, any> = {};
for (const [compId, comp] of Object.entries(components)) {
const updatedComp = JSON.parse(JSON.stringify(comp));
const config = updatedComp.config || {};
// cart.cartScreenId (string)
if (config.cart?.cartScreenId) {
const oldId = parseInt(config.cart.cartScreenId);
const newId = screenIdMap.get(oldId);
if (newId) {
config.cart.cartScreenId = String(newId);
logger.info(`POP 참조 치환: cartScreenId ${oldId} -> ${newId}`);
}
}
// cartListMode.sourceScreenId (number)
if (config.cartListMode?.sourceScreenId) {
const oldId =
typeof config.cartListMode.sourceScreenId === "number"
? config.cartListMode.sourceScreenId
: parseInt(config.cartListMode.sourceScreenId);
const newId = screenIdMap.get(oldId);
if (newId) {
config.cartListMode.sourceScreenId = newId;
logger.info(
`POP 참조 치환: sourceScreenId ${oldId} -> ${newId}`,
);
}
}
// followUpActions[].targetScreenId (string)
if (Array.isArray(config.followUpActions)) {
for (const action of config.followUpActions) {
if (action.targetScreenId) {
const oldId = parseInt(action.targetScreenId);
const newId = screenIdMap.get(oldId);
if (newId) {
action.targetScreenId = String(newId);
logger.info(
`POP 참조 치환: targetScreenId ${oldId} -> ${newId}`,
);
}
}
}
}
// action.modalScreenId (숫자형이면 화면 참조로 간주)
if (config.action?.modalScreenId) {
const oldId = parseInt(config.action.modalScreenId);
if (!isNaN(oldId)) {
const newId = screenIdMap.get(oldId);
if (newId) {
config.action.modalScreenId = String(newId);
logger.info(
`POP 참조 치환: modalScreenId ${oldId} -> ${newId}`,
);
}
}
}
// numberingRuleId 초기화 (배포 후 대상 회사에서 재설정 필요)
if (config.numberingRuleId) {
logger.info(`POP 채번규칙 초기화: ${config.numberingRuleId}`);
config.numberingRuleId = "";
}
if (config.autoGenMappings) {
for (const mapping of Object.values(config.autoGenMappings) as any[]) {
if (mapping?.numberingRuleId) {
logger.info(
`POP 채번규칙 초기화: ${mapping.numberingRuleId}`,
);
mapping.numberingRuleId = "";
}
}
}
updatedComp.config = config;
updated[compId] = updatedComp;
}
return updated;
};
layoutData.components = updateComponents(layoutData.components);
if (Array.isArray(layoutData.modals)) {
for (const modal of layoutData.modals) {
if (modal.components) {
modal.components = updateComponents(modal.components);
}
}
}
return layoutData;
}
}
// 서비스 인스턴스 export