Merge branch 'ksh-v2-work' into main
POP 화면 관리 기능 일괄 병합: - POP 컴포넌트 연결/상태변경 규칙/후속 액션 - POP 장바구니(CartList) 모드 + 멀티필드 입력 - POP 화면 복사 기능 (단일 + 카테고리 일괄) - POP 화면관리 UX 개선 (스크롤/접기) - PC/POP 화면 데이터 분리 (excludePop 필터) - .gitignore 미사용 항목 정리 충돌 1건 해결 (screenManagementRoutes.ts import 양쪽 통합)
This commit is contained in:
@@ -114,6 +114,7 @@ import numberingRuleRoutes from "./routes/numberingRuleRoutes"; // 채번 규칙
|
||||
import entitySearchRoutes, { entityOptionsRouter } from "./routes/entitySearchRoutes"; // 엔티티 검색 및 옵션
|
||||
import screenEmbeddingRoutes from "./routes/screenEmbeddingRoutes"; // 화면 임베딩 및 데이터 전달
|
||||
import screenGroupRoutes from "./routes/screenGroupRoutes"; // 화면 그룹 관리
|
||||
import popActionRoutes from "./routes/popActionRoutes"; // POP 액션 실행
|
||||
import vehicleTripRoutes from "./routes/vehicleTripRoutes"; // 차량 운행 이력 관리
|
||||
import driverRoutes from "./routes/driverRoutes"; // 공차중계 운전자 관리
|
||||
import taxInvoiceRoutes from "./routes/taxInvoiceRoutes"; // 세금계산서 관리
|
||||
@@ -241,6 +242,7 @@ app.use("/api/table-management", tableManagementRoutes);
|
||||
app.use("/api/table-management", entityJoinRoutes); // 🎯 Entity 조인 기능
|
||||
app.use("/api/screen-management", screenManagementRoutes);
|
||||
app.use("/api/screen-groups", screenGroupRoutes); // 화면 그룹 관리
|
||||
app.use("/api/pop", popActionRoutes); // POP 액션 실행
|
||||
app.use("/api/common-codes", commonCodeRoutes);
|
||||
app.use("/api/dynamic-form", dynamicFormRoutes);
|
||||
app.use("/api/files", fileRoutes);
|
||||
|
||||
@@ -20,7 +20,7 @@ const pool = getPool();
|
||||
export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { page = 1, size = 20, searchTerm } = req.query;
|
||||
const { page = 1, size = 20, searchTerm, excludePop } = req.query;
|
||||
const offset = (parseInt(page as string) - 1) * parseInt(size as string);
|
||||
|
||||
let whereClause = "WHERE 1=1";
|
||||
@@ -34,6 +34,11 @@ export const getScreenGroups = async (req: AuthenticatedRequest, res: Response)
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// POP 그룹 제외 (PC 화면관리용)
|
||||
if (excludePop === "true") {
|
||||
whereClause += ` AND (hierarchy_path IS NULL OR (hierarchy_path NOT LIKE 'POP/%' AND hierarchy_path != 'POP'))`;
|
||||
}
|
||||
|
||||
// 검색어 필터링
|
||||
if (searchTerm) {
|
||||
whereClause += ` AND (group_name ILIKE $${paramIndex} OR group_code ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`;
|
||||
@@ -2574,11 +2579,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 +2597,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 +2616,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 +2776,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 +2798,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 +2810,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 +2828,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) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AuthenticatedRequest } from "../types/auth";
|
||||
export const getScreens = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = (req.user as any).companyCode;
|
||||
const { page = 1, size = 20, searchTerm, companyCode } = req.query;
|
||||
const { page = 1, size = 20, searchTerm, companyCode, excludePop } = req.query;
|
||||
|
||||
// 쿼리 파라미터로 companyCode가 전달되면 해당 회사의 화면 조회 (최고 관리자 전용)
|
||||
// 아니면 현재 사용자의 companyCode 사용
|
||||
@@ -24,7 +24,8 @@ export const getScreens = async (req: AuthenticatedRequest, res: Response) => {
|
||||
targetCompanyCode,
|
||||
parseInt(page as string),
|
||||
parseInt(size as string),
|
||||
searchTerm as string // 검색어 전달
|
||||
searchTerm as string,
|
||||
{ excludePop: excludePop === "true" },
|
||||
);
|
||||
|
||||
res.json({
|
||||
@@ -1364,3 +1365,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 화면 배포에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
280
backend-node/src/routes/popActionRoutes.ts
Normal file
280
backend-node/src/routes/popActionRoutes.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { getPool } from "../database/db";
|
||||
import logger from "../utils/logger";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// SQL 인젝션 방지: 테이블명/컬럼명 패턴 검증
|
||||
const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||
|
||||
function isSafeIdentifier(name: string): boolean {
|
||||
return SAFE_IDENTIFIER.test(name);
|
||||
}
|
||||
|
||||
interface MappingInfo {
|
||||
targetTable: string;
|
||||
columnMapping: Record<string, string>;
|
||||
}
|
||||
|
||||
interface StatusConditionRule {
|
||||
whenColumn: string;
|
||||
operator: string;
|
||||
whenValue: string;
|
||||
thenValue: string;
|
||||
}
|
||||
|
||||
interface ConditionalValueRule {
|
||||
conditions: StatusConditionRule[];
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
interface StatusChangeRuleBody {
|
||||
targetTable: string;
|
||||
targetColumn: string;
|
||||
lookupMode?: "auto" | "manual";
|
||||
manualItemField?: string;
|
||||
manualPkColumn?: string;
|
||||
valueType: "fixed" | "conditional";
|
||||
fixedValue?: string;
|
||||
conditionalValue?: ConditionalValueRule;
|
||||
// 하위호환: 기존 형식
|
||||
value?: string;
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
interface ExecuteActionBody {
|
||||
action: string;
|
||||
data: {
|
||||
items?: Record<string, unknown>[];
|
||||
fieldValues?: Record<string, unknown>;
|
||||
};
|
||||
mappings?: {
|
||||
cardList?: MappingInfo | null;
|
||||
field?: MappingInfo | null;
|
||||
};
|
||||
statusChanges?: StatusChangeRuleBody[];
|
||||
}
|
||||
|
||||
function resolveStatusValue(
|
||||
valueType: string,
|
||||
fixedValue: string,
|
||||
conditionalValue: ConditionalValueRule | undefined,
|
||||
item: Record<string, unknown>
|
||||
): string {
|
||||
if (valueType !== "conditional" || !conditionalValue) return fixedValue;
|
||||
|
||||
for (const cond of conditionalValue.conditions) {
|
||||
const actual = String(item[cond.whenColumn] ?? "");
|
||||
const expected = cond.whenValue;
|
||||
let match = false;
|
||||
|
||||
switch (cond.operator) {
|
||||
case "=": match = actual === expected; break;
|
||||
case "!=": match = actual !== expected; break;
|
||||
case ">": match = parseFloat(actual) > parseFloat(expected); break;
|
||||
case "<": match = parseFloat(actual) < parseFloat(expected); break;
|
||||
case ">=": match = parseFloat(actual) >= parseFloat(expected); break;
|
||||
case "<=": match = parseFloat(actual) <= parseFloat(expected); break;
|
||||
default: match = actual === expected;
|
||||
}
|
||||
|
||||
if (match) return cond.thenValue;
|
||||
}
|
||||
|
||||
return conditionalValue.defaultValue ?? fixedValue;
|
||||
}
|
||||
|
||||
router.post("/execute-action", authenticateToken, async (req: Request, res: Response) => {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const companyCode = (req as any).user?.companyCode;
|
||||
const userId = (req as any).user?.userId;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { action, data, mappings, statusChanges } = req.body as ExecuteActionBody;
|
||||
const items = data?.items ?? [];
|
||||
const fieldValues = data?.fieldValues ?? {};
|
||||
|
||||
logger.info("[pop/execute-action] 요청", {
|
||||
action,
|
||||
companyCode,
|
||||
userId,
|
||||
itemCount: items.length,
|
||||
hasFieldValues: Object.keys(fieldValues).length > 0,
|
||||
hasMappings: !!mappings,
|
||||
statusChangeCount: statusChanges?.length ?? 0,
|
||||
});
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
let processedCount = 0;
|
||||
let insertedCount = 0;
|
||||
|
||||
if (action === "inbound-confirm") {
|
||||
// 1. 매핑 기반 INSERT (장바구니 데이터 -> 대상 테이블)
|
||||
const cardMapping = mappings?.cardList;
|
||||
const fieldMapping = mappings?.field;
|
||||
|
||||
if (cardMapping?.targetTable && Object.keys(cardMapping.columnMapping).length > 0) {
|
||||
if (!isSafeIdentifier(cardMapping.targetTable)) {
|
||||
throw new Error(`유효하지 않은 테이블명: ${cardMapping.targetTable}`);
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const columns: string[] = ["company_code"];
|
||||
const values: unknown[] = [companyCode];
|
||||
|
||||
for (const [sourceField, targetColumn] of Object.entries(cardMapping.columnMapping)) {
|
||||
if (!isSafeIdentifier(targetColumn)) continue;
|
||||
columns.push(`"${targetColumn}"`);
|
||||
values.push(item[sourceField] ?? null);
|
||||
}
|
||||
|
||||
if (fieldMapping?.targetTable === cardMapping.targetTable) {
|
||||
for (const [sourceField, targetColumn] of Object.entries(fieldMapping.columnMapping)) {
|
||||
if (!isSafeIdentifier(targetColumn)) continue;
|
||||
if (columns.includes(`"${targetColumn}"`)) continue;
|
||||
columns.push(`"${targetColumn}"`);
|
||||
values.push(fieldValues[sourceField] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
if (columns.length > 1) {
|
||||
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
||||
const sql = `INSERT INTO "${cardMapping.targetTable}" (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||
|
||||
logger.info("[pop/execute-action] INSERT 실행", {
|
||||
table: cardMapping.targetTable,
|
||||
columnCount: columns.length,
|
||||
});
|
||||
|
||||
await client.query(sql, values);
|
||||
insertedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
fieldMapping?.targetTable &&
|
||||
Object.keys(fieldMapping.columnMapping).length > 0 &&
|
||||
fieldMapping.targetTable !== cardMapping?.targetTable
|
||||
) {
|
||||
if (!isSafeIdentifier(fieldMapping.targetTable)) {
|
||||
throw new Error(`유효하지 않은 테이블명: ${fieldMapping.targetTable}`);
|
||||
}
|
||||
|
||||
const columns: string[] = ["company_code"];
|
||||
const values: unknown[] = [companyCode];
|
||||
|
||||
for (const [sourceField, targetColumn] of Object.entries(fieldMapping.columnMapping)) {
|
||||
if (!isSafeIdentifier(targetColumn)) continue;
|
||||
columns.push(`"${targetColumn}"`);
|
||||
values.push(fieldValues[sourceField] ?? null);
|
||||
}
|
||||
|
||||
if (columns.length > 1) {
|
||||
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
||||
const sql = `INSERT INTO "${fieldMapping.targetTable}" (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||
await client.query(sql, values);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 상태 변경 규칙 실행 (설정 기반)
|
||||
if (statusChanges && statusChanges.length > 0) {
|
||||
for (const rule of statusChanges) {
|
||||
if (!rule.targetTable || !rule.targetColumn) continue;
|
||||
if (!isSafeIdentifier(rule.targetTable) || !isSafeIdentifier(rule.targetColumn)) {
|
||||
logger.warn("[pop/execute-action] 유효하지 않은 식별자, 건너뜀", { table: rule.targetTable, column: rule.targetColumn });
|
||||
continue;
|
||||
}
|
||||
|
||||
const valueType = rule.valueType ?? "fixed";
|
||||
const fixedValue = rule.fixedValue ?? rule.value ?? "";
|
||||
const lookupMode = rule.lookupMode ?? "auto";
|
||||
|
||||
// 조회 키 결정: 아이템 필드(itemField) -> 대상 테이블 PK 컬럼(pkColumn)
|
||||
let itemField: string;
|
||||
let pkColumn: string;
|
||||
|
||||
if (lookupMode === "manual" && rule.manualItemField && rule.manualPkColumn) {
|
||||
if (!isSafeIdentifier(rule.manualPkColumn)) {
|
||||
logger.warn("[pop/execute-action] 수동 PK 컬럼 유효하지 않음", { manualPkColumn: rule.manualPkColumn });
|
||||
continue;
|
||||
}
|
||||
itemField = rule.manualItemField;
|
||||
pkColumn = rule.manualPkColumn;
|
||||
logger.info("[pop/execute-action] 수동 조회 키", { itemField, pkColumn, table: rule.targetTable });
|
||||
} else if (rule.targetTable === "cart_items") {
|
||||
itemField = "__cart_id";
|
||||
pkColumn = "id";
|
||||
} else {
|
||||
itemField = "__cart_row_key";
|
||||
const pkResult = await client.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`,
|
||||
[rule.targetTable]
|
||||
);
|
||||
pkColumn = pkResult.rows[0]?.attname || "id";
|
||||
}
|
||||
|
||||
const lookupValues = items.map((item) => item[itemField] ?? item[itemField.replace(/^__cart_/, "")]).filter(Boolean);
|
||||
if (lookupValues.length === 0) {
|
||||
logger.warn("[pop/execute-action] 조회 키 값 없음, 건너뜀", { table: rule.targetTable, itemField });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (valueType === "fixed") {
|
||||
const placeholders = lookupValues.map((_, i) => `$${i + 3}`).join(", ");
|
||||
const sql = `UPDATE "${rule.targetTable}" SET "${rule.targetColumn}" = $1 WHERE company_code = $2 AND "${pkColumn}" IN (${placeholders})`;
|
||||
await client.query(sql, [fixedValue, companyCode, ...lookupValues]);
|
||||
processedCount += lookupValues.length;
|
||||
} else {
|
||||
for (let i = 0; i < lookupValues.length; i++) {
|
||||
const item = items[i] ?? {};
|
||||
const resolvedValue = resolveStatusValue(valueType, fixedValue, rule.conditionalValue, item);
|
||||
await client.query(
|
||||
`UPDATE "${rule.targetTable}" SET "${rule.targetColumn}" = $1 WHERE company_code = $2 AND "${pkColumn}" = $3`,
|
||||
[resolvedValue, companyCode, lookupValues[i]]
|
||||
);
|
||||
processedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("[pop/execute-action] 상태 변경 실행", {
|
||||
table: rule.targetTable, column: rule.targetColumn, lookupMode, itemField, pkColumn, count: lookupValues.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("[pop/execute-action] 완료", {
|
||||
action,
|
||||
companyCode,
|
||||
processedCount,
|
||||
insertedCount,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `${processedCount}건 처리 완료${insertedCount > 0 ? `, ${insertedCount}건 생성` : ""}`,
|
||||
data: { processedCount, insertedCount },
|
||||
});
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("[pop/execute-action] 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "처리 중 오류가 발생했습니다.",
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -51,6 +51,8 @@ import {
|
||||
updateZone,
|
||||
deleteZone,
|
||||
addLayerToZone,
|
||||
analyzePopScreenLinks,
|
||||
deployPopScreens,
|
||||
} from "../controllers/screenManagementController";
|
||||
|
||||
const router = express.Router();
|
||||
@@ -145,4 +147,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;
|
||||
|
||||
@@ -108,42 +108,49 @@ export class ScreenManagementService {
|
||||
companyCode: string,
|
||||
page: number = 1,
|
||||
size: number = 20,
|
||||
searchTerm?: string, // 검색어 추가
|
||||
searchTerm?: string,
|
||||
options?: { excludePop?: boolean },
|
||||
): Promise<PaginatedResponse<ScreenDefinition>> {
|
||||
const offset = (page - 1) * size;
|
||||
|
||||
// WHERE 절 동적 생성
|
||||
const whereConditions: string[] = ["is_active != 'D'"];
|
||||
const whereConditions: string[] = ["sd.is_active != 'D'"];
|
||||
const params: any[] = [];
|
||||
|
||||
if (companyCode !== "*") {
|
||||
whereConditions.push(`company_code = $${params.length + 1}`);
|
||||
whereConditions.push(`sd.company_code = $${params.length + 1}`);
|
||||
params.push(companyCode);
|
||||
}
|
||||
|
||||
// 검색어 필터링 추가 (화면명, 화면 코드, 테이블명 검색)
|
||||
if (searchTerm && searchTerm.trim() !== "") {
|
||||
whereConditions.push(`(
|
||||
screen_name ILIKE $${params.length + 1} OR
|
||||
screen_code ILIKE $${params.length + 1} OR
|
||||
table_name ILIKE $${params.length + 1}
|
||||
sd.screen_name ILIKE $${params.length + 1} OR
|
||||
sd.screen_code ILIKE $${params.length + 1} OR
|
||||
sd.table_name ILIKE $${params.length + 1}
|
||||
)`);
|
||||
params.push(`%${searchTerm.trim()}%`);
|
||||
}
|
||||
|
||||
// POP 화면 제외 필터: screen_layouts_pop에 레이아웃이 있는 화면 제외
|
||||
if (options?.excludePop) {
|
||||
whereConditions.push(
|
||||
`NOT EXISTS (SELECT 1 FROM screen_layouts_pop slp WHERE slp.screen_id = sd.screen_id)`
|
||||
);
|
||||
}
|
||||
|
||||
const whereSQL = whereConditions.join(" AND ");
|
||||
|
||||
// 페이징 쿼리 (Raw Query)
|
||||
const [screens, totalResult] = await Promise.all([
|
||||
query<any>(
|
||||
`SELECT * FROM screen_definitions
|
||||
`SELECT sd.* FROM screen_definitions sd
|
||||
WHERE ${whereSQL}
|
||||
ORDER BY created_date DESC
|
||||
ORDER BY sd.created_date DESC
|
||||
LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
|
||||
[...params, size, offset],
|
||||
),
|
||||
query<{ count: string }>(
|
||||
`SELECT COUNT(*)::text as count FROM screen_definitions
|
||||
`SELECT COUNT(*)::text as count FROM screen_definitions sd
|
||||
WHERE ${whereSQL}`,
|
||||
params,
|
||||
),
|
||||
@@ -5814,28 +5821,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;
|
||||
}
|
||||
|
||||
@@ -5873,6 +5876,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