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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user