Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into reportMng

This commit is contained in:
dohyeons
2025-12-23 17:37:37 +09:00
36 changed files with 3936 additions and 506 deletions

View File

@@ -94,7 +94,9 @@ export class CommonCodeController {
sortOrder: code.sort_order,
isActive: code.is_active,
useYn: code.is_active,
companyCode: code.company_code, // 추가
companyCode: code.company_code,
parentCodeValue: code.parent_code_value, // 계층구조: 부모 코드값
depth: code.depth, // 계층구조: 깊이
// 기존 필드명도 유지 (하위 호환성)
code_category: code.code_category,
@@ -103,7 +105,9 @@ export class CommonCodeController {
code_name_eng: code.code_name_eng,
sort_order: code.sort_order,
is_active: code.is_active,
company_code: code.company_code, // 추가
company_code: code.company_code,
parent_code_value: code.parent_code_value, // 계층구조: 부모 코드값
// depth는 위에서 이미 정의됨 (snake_case와 camelCase 동일)
created_date: code.created_date,
created_by: code.created_by,
updated_date: code.updated_date,
@@ -286,19 +290,17 @@ export class CommonCodeController {
});
}
if (!menuObjid) {
return res.status(400).json({
success: false,
message: "메뉴 OBJID는 필수입니다.",
});
}
// menuObjid가 없으면 공통코드관리 메뉴의 기본 OBJID 사용 (전역 코드)
// 공통코드관리 메뉴 OBJID: 1757401858940
const DEFAULT_CODE_MANAGEMENT_MENU_OBJID = 1757401858940;
const effectiveMenuObjid = menuObjid ? Number(menuObjid) : DEFAULT_CODE_MANAGEMENT_MENU_OBJID;
const code = await this.commonCodeService.createCode(
categoryCode,
codeData,
userId,
companyCode,
Number(menuObjid)
effectiveMenuObjid
);
return res.status(201).json({
@@ -588,4 +590,129 @@ export class CommonCodeController {
});
}
}
/**
* 계층구조 코드 조회
* GET /api/common-codes/categories/:categoryCode/hierarchy
* Query: parentCodeValue (optional), depth (optional), menuObjid (optional)
*/
async getHierarchicalCodes(req: AuthenticatedRequest, res: Response) {
try {
const { categoryCode } = req.params;
const { parentCodeValue, depth, menuObjid } = req.query;
const userCompanyCode = req.user?.companyCode;
const menuObjidNum = menuObjid ? Number(menuObjid) : undefined;
// parentCodeValue가 빈 문자열이면 최상위 코드 조회
const parentValue = parentCodeValue === '' || parentCodeValue === undefined
? null
: parentCodeValue as string;
const codes = await this.commonCodeService.getHierarchicalCodes(
categoryCode,
parentValue,
depth ? parseInt(depth as string) : undefined,
userCompanyCode,
menuObjidNum
);
// 프론트엔드 형식으로 변환
const transformedData = codes.map((code: any) => ({
codeValue: code.code_value,
codeName: code.code_name,
codeNameEng: code.code_name_eng,
description: code.description,
sortOrder: code.sort_order,
isActive: code.is_active,
parentCodeValue: code.parent_code_value,
depth: code.depth,
// 기존 필드도 유지
code_category: code.code_category,
code_value: code.code_value,
code_name: code.code_name,
code_name_eng: code.code_name_eng,
sort_order: code.sort_order,
is_active: code.is_active,
parent_code_value: code.parent_code_value,
}));
return res.json({
success: true,
data: transformedData,
message: `계층구조 코드 조회 성공 (${categoryCode})`,
});
} catch (error) {
logger.error(`계층구조 코드 조회 실패 (${req.params.categoryCode}):`, error);
return res.status(500).json({
success: false,
message: "계층구조 코드 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* 코드 트리 조회
* GET /api/common-codes/categories/:categoryCode/tree
*/
async getCodeTree(req: AuthenticatedRequest, res: Response) {
try {
const { categoryCode } = req.params;
const { menuObjid } = req.query;
const userCompanyCode = req.user?.companyCode;
const menuObjidNum = menuObjid ? Number(menuObjid) : undefined;
const result = await this.commonCodeService.getCodeTree(
categoryCode,
userCompanyCode,
menuObjidNum
);
return res.json({
success: true,
data: result,
message: `코드 트리 조회 성공 (${categoryCode})`,
});
} catch (error) {
logger.error(`코드 트리 조회 실패 (${req.params.categoryCode}):`, error);
return res.status(500).json({
success: false,
message: "코드 트리 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* 자식 코드 존재 여부 확인
* GET /api/common-codes/categories/:categoryCode/codes/:codeValue/has-children
*/
async hasChildren(req: AuthenticatedRequest, res: Response) {
try {
const { categoryCode, codeValue } = req.params;
const companyCode = req.user?.companyCode;
const hasChildren = await this.commonCodeService.hasChildren(
categoryCode,
codeValue,
companyCode
);
return res.json({
success: true,
data: { hasChildren },
message: "자식 코드 확인 완료",
});
} catch (error) {
logger.error(
`자식 코드 확인 실패 (${req.params.categoryCode}.${req.params.codeValue}):`,
error
);
return res.status(500).json({
success: false,
message: "자식 코드 확인 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
}

View File

@@ -54,3 +54,4 @@ export default router;

View File

@@ -50,3 +50,4 @@ export default router;

View File

@@ -66,3 +66,4 @@ export default router;

View File

@@ -54,3 +54,4 @@ export default router;

View File

@@ -46,6 +46,21 @@ router.put("/categories/:categoryCode/codes/reorder", (req, res) =>
commonCodeController.reorderCodes(req, res)
);
// 계층구조 코드 조회 (구체적인 경로를 먼저 배치)
router.get("/categories/:categoryCode/hierarchy", (req, res) =>
commonCodeController.getHierarchicalCodes(req, res)
);
// 코드 트리 조회
router.get("/categories/:categoryCode/tree", (req, res) =>
commonCodeController.getCodeTree(req, res)
);
// 자식 코드 존재 여부 확인
router.get("/categories/:categoryCode/codes/:codeValue/has-children", (req, res) =>
commonCodeController.hasChildren(req, res)
);
router.put("/categories/:categoryCode/codes/:codeValue", (req, res) =>
commonCodeController.updateCode(req, res)
);

View File

@@ -25,6 +25,8 @@ export interface CodeInfo {
is_active: string;
company_code: string;
menu_objid?: number | null; // 메뉴 기반 코드 관리용
parent_code_value?: string | null; // 계층구조: 부모 코드값
depth?: number; // 계층구조: 깊이 (1, 2, 3단계)
created_date?: Date | null;
created_by?: string | null;
updated_date?: Date | null;
@@ -61,6 +63,8 @@ export interface CreateCodeData {
description?: string;
sortOrder?: number;
isActive?: string;
parentCodeValue?: string; // 계층구조: 부모 코드값
depth?: number; // 계층구조: 깊이 (1, 2, 3단계)
}
export class CommonCodeService {
@@ -405,11 +409,22 @@ export class CommonCodeService {
menuObjid: number
) {
try {
// 계층구조: depth 계산 (부모가 있으면 부모의 depth + 1, 없으면 1)
let depth = 1;
if (data.parentCodeValue) {
const parentCode = await queryOne<CodeInfo>(
`SELECT depth FROM code_info
WHERE code_category = $1 AND code_value = $2 AND company_code = $3`,
[categoryCode, data.parentCodeValue, companyCode]
);
depth = parentCode ? (parentCode.depth || 1) + 1 : 1;
}
const code = await queryOne<CodeInfo>(
`INSERT INTO code_info
(code_category, code_value, code_name, code_name_eng, description, sort_order,
is_active, menu_objid, company_code, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, $9, $10, NOW(), NOW())
is_active, menu_objid, company_code, parent_code_value, depth, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, $9, $10, $11, $12, NOW(), NOW())
RETURNING *`,
[
categoryCode,
@@ -420,13 +435,15 @@ export class CommonCodeService {
data.sortOrder || 0,
menuObjid,
companyCode,
data.parentCodeValue || null,
depth,
createdBy,
createdBy,
]
);
logger.info(
`코드 생성 완료: ${categoryCode}.${data.codeValue} (메뉴: ${menuObjid}, 회사: ${companyCode})`
`코드 생성 완료: ${categoryCode}.${data.codeValue} (메뉴: ${menuObjid}, 회사: ${companyCode}, 부모: ${data.parentCodeValue || '없음'}, 깊이: ${depth})`
);
return code;
} catch (error) {
@@ -491,6 +508,24 @@ export class CommonCodeService {
updateFields.push(`is_active = $${paramIndex++}`);
values.push(activeValue);
}
// 계층구조: 부모 코드값 수정
if (data.parentCodeValue !== undefined) {
updateFields.push(`parent_code_value = $${paramIndex++}`);
values.push(data.parentCodeValue || null);
// depth도 함께 업데이트
let newDepth = 1;
if (data.parentCodeValue) {
const parentCode = await queryOne<CodeInfo>(
`SELECT depth FROM code_info
WHERE code_category = $1 AND code_value = $2`,
[categoryCode, data.parentCodeValue]
);
newDepth = parentCode ? (parentCode.depth || 1) + 1 : 1;
}
updateFields.push(`depth = $${paramIndex++}`);
values.push(newDepth);
}
// WHERE 절 구성
let whereClause = `WHERE code_category = $${paramIndex++} AND code_value = $${paramIndex}`;
@@ -847,4 +882,170 @@ export class CommonCodeService {
throw error;
}
}
/**
* 계층구조 코드 조회 (특정 depth 또는 부모코드 기준)
* @param categoryCode 카테고리 코드
* @param parentCodeValue 부모 코드값 (없으면 최상위 코드만 조회)
* @param depth 특정 깊이만 조회 (선택)
*/
async getHierarchicalCodes(
categoryCode: string,
parentCodeValue?: string | null,
depth?: number,
userCompanyCode?: string,
menuObjid?: number
) {
try {
const whereConditions: string[] = ["code_category = $1", "is_active = 'Y'"];
const values: any[] = [categoryCode];
let paramIndex = 2;
// 부모 코드값 필터링
if (parentCodeValue === null || parentCodeValue === undefined) {
// 최상위 코드 (부모가 없는 코드)
whereConditions.push("(parent_code_value IS NULL OR parent_code_value = '')");
} else if (parentCodeValue !== '') {
whereConditions.push(`parent_code_value = $${paramIndex}`);
values.push(parentCodeValue);
paramIndex++;
}
// 특정 깊이 필터링
if (depth !== undefined) {
whereConditions.push(`depth = $${paramIndex}`);
values.push(depth);
paramIndex++;
}
// 메뉴별 필터링 (형제 메뉴 포함)
if (menuObjid) {
const { getSiblingMenuObjids } = await import('./menuService');
const siblingMenuObjids = await getSiblingMenuObjids(menuObjid);
whereConditions.push(`menu_objid = ANY($${paramIndex})`);
values.push(siblingMenuObjids);
paramIndex++;
}
// 회사별 필터링
if (userCompanyCode && userCompanyCode !== "*") {
whereConditions.push(`company_code = $${paramIndex}`);
values.push(userCompanyCode);
paramIndex++;
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const codes = await query<CodeInfo>(
`SELECT * FROM code_info
${whereClause}
ORDER BY sort_order ASC, code_value ASC`,
values
);
logger.info(
`계층구조 코드 조회: ${categoryCode}, 부모: ${parentCodeValue || '최상위'}, 깊이: ${depth || '전체'} - ${codes.length}`
);
return codes;
} catch (error) {
logger.error(`계층구조 코드 조회 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 계층구조 코드 트리 전체 조회 (카테고리 기준)
*/
async getCodeTree(
categoryCode: string,
userCompanyCode?: string,
menuObjid?: number
) {
try {
const whereConditions: string[] = ["code_category = $1", "is_active = 'Y'"];
const values: any[] = [categoryCode];
let paramIndex = 2;
// 메뉴별 필터링 (형제 메뉴 포함)
if (menuObjid) {
const { getSiblingMenuObjids } = await import('./menuService');
const siblingMenuObjids = await getSiblingMenuObjids(menuObjid);
whereConditions.push(`menu_objid = ANY($${paramIndex})`);
values.push(siblingMenuObjids);
paramIndex++;
}
// 회사별 필터링
if (userCompanyCode && userCompanyCode !== "*") {
whereConditions.push(`company_code = $${paramIndex}`);
values.push(userCompanyCode);
paramIndex++;
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const allCodes = await query<CodeInfo>(
`SELECT * FROM code_info
${whereClause}
ORDER BY depth ASC, sort_order ASC, code_value ASC`,
values
);
// 트리 구조로 변환
const buildTree = (codes: CodeInfo[], parentValue: string | null = null): any[] => {
return codes
.filter(code => {
const codeParent = code.parent_code_value || null;
return codeParent === parentValue;
})
.map(code => ({
...code,
children: buildTree(codes, code.code_value)
}));
};
const tree = buildTree(allCodes);
logger.info(
`코드 트리 조회 완료: ${categoryCode} - 전체 ${allCodes.length}`
);
return {
flat: allCodes,
tree
};
} catch (error) {
logger.error(`코드 트리 조회 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 자식 코드가 있는지 확인
*/
async hasChildren(
categoryCode: string,
codeValue: string,
companyCode?: string
): Promise<boolean> {
try {
let sql = `SELECT COUNT(*) as count FROM code_info
WHERE code_category = $1 AND parent_code_value = $2`;
const values: any[] = [categoryCode, codeValue];
if (companyCode && companyCode !== "*") {
sql += ` AND company_code = $3`;
values.push(companyCode);
}
const result = await queryOne<{ count: string }>(sql, values);
const count = parseInt(result?.count || "0");
return count > 0;
} catch (error) {
logger.error(`자식 코드 확인 중 오류 (${categoryCode}.${codeValue}):`, error);
throw error;
}
}
}

View File

@@ -279,11 +279,90 @@ export class MenuCopyService {
logger.debug(` 📐 분할 패널 우측 화면 참조 발견: ${numId}`);
}
}
// 5) 모달 화면 ID (addModalScreenId, editModalScreenId, modalScreenId)
if (props?.componentConfig?.addModalScreenId) {
const addModalScreenId = props.componentConfig.addModalScreenId;
const numId =
typeof addModalScreenId === "number"
? addModalScreenId
: parseInt(addModalScreenId);
if (!isNaN(numId) && numId > 0) {
referenced.push(numId);
logger.debug(` 📋 추가 모달 화면 참조 발견: ${numId}`);
}
}
if (props?.componentConfig?.editModalScreenId) {
const editModalScreenId = props.componentConfig.editModalScreenId;
const numId =
typeof editModalScreenId === "number"
? editModalScreenId
: parseInt(editModalScreenId);
if (!isNaN(numId) && numId > 0) {
referenced.push(numId);
logger.debug(` 📝 수정 모달 화면 참조 발견: ${numId}`);
}
}
if (props?.componentConfig?.modalScreenId) {
const modalScreenId = props.componentConfig.modalScreenId;
const numId =
typeof modalScreenId === "number"
? modalScreenId
: parseInt(modalScreenId);
if (!isNaN(numId) && numId > 0) {
referenced.push(numId);
logger.debug(` 🔲 모달 화면 참조 발견: ${numId}`);
}
}
// 6) 재귀적으로 모든 properties에서 화면 ID 추출 (깊은 탐색)
this.extractScreenIdsFromObject(props, referenced);
}
return referenced;
}
/**
* 객체 내부에서 화면 ID를 재귀적으로 추출
*/
private extractScreenIdsFromObject(obj: any, referenced: number[]): void {
if (!obj || typeof obj !== "object") return;
if (Array.isArray(obj)) {
for (const item of obj) {
this.extractScreenIdsFromObject(item, referenced);
}
return;
}
for (const key of Object.keys(obj)) {
const value = obj[key];
// 화면 ID 키 패턴 확인
if (
key === "screenId" ||
key === "targetScreenId" ||
key === "leftScreenId" ||
key === "rightScreenId" ||
key === "addModalScreenId" ||
key === "editModalScreenId" ||
key === "modalScreenId"
) {
const numId = typeof value === "number" ? value : parseInt(value);
if (!isNaN(numId) && numId > 0 && !referenced.includes(numId)) {
referenced.push(numId);
}
}
// 재귀 탐색
if (typeof value === "object" && value !== null) {
this.extractScreenIdsFromObject(value, referenced);
}
}
}
/**
* 화면 수집 (중복 제거, 재귀적 참조 추적)
*/
@@ -483,7 +562,8 @@ export class MenuCopyService {
properties: any,
screenIdMap: Map<number, number>,
flowIdMap: Map<number, number>,
numberingRuleIdMap?: Map<string, string>
numberingRuleIdMap?: Map<string, string>,
menuIdMap?: Map<number, number>
): any {
if (!properties) return properties;
@@ -496,7 +576,8 @@ export class MenuCopyService {
screenIdMap,
flowIdMap,
"",
numberingRuleIdMap
numberingRuleIdMap,
menuIdMap
);
return updated;
@@ -510,7 +591,8 @@ export class MenuCopyService {
screenIdMap: Map<number, number>,
flowIdMap: Map<number, number>,
path: string = "",
numberingRuleIdMap?: Map<string, string>
numberingRuleIdMap?: Map<string, string>,
menuIdMap?: Map<number, number>
): void {
if (!obj || typeof obj !== "object") return;
@@ -522,7 +604,8 @@ export class MenuCopyService {
screenIdMap,
flowIdMap,
`${path}[${index}]`,
numberingRuleIdMap
numberingRuleIdMap,
menuIdMap
);
});
return;
@@ -533,13 +616,16 @@ export class MenuCopyService {
const value = obj[key];
const currentPath = path ? `${path}.${key}` : key;
// screen_id, screenId, targetScreenId, leftScreenId, rightScreenId 매핑 (숫자 또는 숫자 문자열)
// screen_id, screenId, targetScreenId, leftScreenId, rightScreenId, addModalScreenId, editModalScreenId, modalScreenId 매핑 (숫자 또는 숫자 문자열)
if (
key === "screen_id" ||
key === "screenId" ||
key === "targetScreenId" ||
key === "leftScreenId" ||
key === "rightScreenId"
key === "rightScreenId" ||
key === "addModalScreenId" ||
key === "editModalScreenId" ||
key === "modalScreenId"
) {
const numValue = typeof value === "number" ? value : parseInt(value);
if (!isNaN(numValue) && numValue > 0) {
@@ -549,6 +635,11 @@ export class MenuCopyService {
logger.info(
` 🔗 화면 참조 업데이트 (${currentPath}): ${value}${newId}`
);
} else {
// 매핑이 없으면 경고 로그 (복사되지 않은 화면 참조)
logger.warn(
` ⚠️ 화면 매핑 없음 (${currentPath}): ${value} - 원본 화면이 복사되지 않았을 수 있음`
);
}
}
}
@@ -573,9 +664,9 @@ export class MenuCopyService {
}
}
// numberingRuleId 매핑 (문자열)
// numberingRuleId, ruleId 매핑 (문자열) - 채번규칙 참조
if (
key === "numberingRuleId" &&
(key === "numberingRuleId" || key === "ruleId") &&
numberingRuleIdMap &&
typeof value === "string" &&
value
@@ -595,6 +686,25 @@ export class MenuCopyService {
}
}
// selectedMenuObjid 매핑 (메뉴 objid 참조)
if (key === "selectedMenuObjid" && menuIdMap) {
const numValue = typeof value === "number" ? value : parseInt(value);
if (!isNaN(numValue) && numValue > 0) {
const newId = menuIdMap.get(numValue);
if (newId) {
obj[key] = typeof value === "number" ? newId : String(newId);
logger.info(
` 🔗 메뉴 참조 업데이트 (${currentPath}): ${value}${newId}`
);
} else {
// 매핑이 없으면 경고 로그 (복사되지 않은 메뉴 참조)
logger.warn(
` ⚠️ 메뉴 매핑 없음 (${currentPath}): ${value} - 원본 메뉴가 복사되지 않았을 수 있음`
);
}
}
}
// 재귀 호출
if (typeof value === "object" && value !== null) {
this.recursiveUpdateReferences(
@@ -602,7 +712,8 @@ export class MenuCopyService {
screenIdMap,
flowIdMap,
currentPath,
numberingRuleIdMap
numberingRuleIdMap,
menuIdMap
);
}
}
@@ -938,7 +1049,9 @@ export class MenuCopyService {
copiedCategoryMappings = await this.copyCategoryMappingsAndValues(
menuObjids,
menuIdMap,
sourceCompanyCode,
targetCompanyCode,
Array.from(screenIds),
userId,
client
);
@@ -979,7 +1092,8 @@ export class MenuCopyService {
userId,
client,
screenNameConfig,
numberingRuleIdMap
numberingRuleIdMap,
menuIdMap
);
// === 6단계: 화면-메뉴 할당 ===
@@ -1313,7 +1427,8 @@ export class MenuCopyService {
removeText?: string;
addPrefix?: string;
},
numberingRuleIdMap?: Map<string, string>
numberingRuleIdMap?: Map<string, string>,
menuIdMap?: Map<number, number>
): Promise<Map<number, number>> {
const screenIdMap = new Map<number, number>();
@@ -1599,7 +1714,8 @@ export class MenuCopyService {
layout.properties,
screenIdMap,
flowIdMap,
numberingRuleIdMap
numberingRuleIdMap,
menuIdMap
);
layoutValues.push(
@@ -2569,11 +2685,16 @@ export class MenuCopyService {
/**
* 카테고리 매핑 + 값 복사 (최적화: 배치 조회)
*
* 화면에서 사용하는 table_name + column_name 조합을 기준으로 카테고리 값 복사
* menu_objid 기준이 아닌 화면 컴포넌트 기준으로 복사하여 누락 방지
*/
private async copyCategoryMappingsAndValues(
menuObjids: number[],
menuIdMap: Map<number, number>,
sourceCompanyCode: string,
targetCompanyCode: string,
screenIds: number[],
userId: string,
client: PoolClient
): Promise<number> {
@@ -2697,12 +2818,70 @@ export class MenuCopyService {
);
}
// 4. 모든 원본 카테고리 값 한 번에 조회
// 4. 화면에서 사용하는 카테고리 컬럼 조합 수집
// 복사된 화면의 레이아웃에서 webType='category'인 컴포넌트의 tableName, columnName 추출
const categoryColumnsResult = await client.query(
`SELECT DISTINCT
sl.properties->>'tableName' as table_name,
sl.properties->>'columnName' as column_name
FROM screen_layouts sl
JOIN screen_definitions sd ON sl.screen_id = sd.screen_id
WHERE sd.screen_id = ANY($1)
AND sl.properties->'componentConfig'->>'webType' = 'category'
AND sl.properties->>'tableName' IS NOT NULL
AND sl.properties->>'columnName' IS NOT NULL`,
[screenIds]
);
// 카테고리 매핑에서 사용하는 table_name, column_name도 추가
const mappingColumnsResult = await client.query(
`SELECT DISTINCT table_name, logical_column_name as column_name
FROM category_column_mapping
WHERE menu_objid = ANY($1)`,
[menuObjids]
);
// 두 결과 합치기
const categoryColumns = new Set<string>();
for (const row of categoryColumnsResult.rows) {
if (row.table_name && row.column_name) {
categoryColumns.add(`${row.table_name}|${row.column_name}`);
}
}
for (const row of mappingColumnsResult.rows) {
if (row.table_name && row.column_name) {
categoryColumns.add(`${row.table_name}|${row.column_name}`);
}
}
logger.info(
` 📋 화면에서 사용하는 카테고리 컬럼: ${categoryColumns.size}`
);
if (categoryColumns.size === 0) {
logger.info(`✅ 카테고리 매핑 + 값 복사 완료: ${copiedCount}`);
return copiedCount;
}
// 5. 원본 회사의 카테고리 값 조회 (table_name + column_name 기준)
// menu_objid 조건 대신 table_name + column_name + 원본 회사 코드로 조회
const columnConditions = Array.from(categoryColumns).map((col, i) => {
const [tableName, columnName] = col.split("|");
return `(table_name = $${i * 2 + 2} AND column_name = $${i * 2 + 3})`;
});
const columnParams: string[] = [];
for (const col of categoryColumns) {
const [tableName, columnName] = col.split("|");
columnParams.push(tableName, columnName);
}
const allValuesResult = await client.query(
`SELECT * FROM table_column_category_values
WHERE menu_objid = ANY($1)
WHERE company_code = $1
AND (${columnConditions.join(" OR ")})
ORDER BY depth NULLS FIRST, parent_value_id NULLS FIRST, value_order`,
[menuObjids]
[sourceCompanyCode, ...columnParams]
);
if (allValuesResult.rows.length === 0) {
@@ -2710,6 +2889,8 @@ export class MenuCopyService {
return copiedCount;
}
logger.info(` 📋 원본 카테고리 값: ${allValuesResult.rows.length}개 발견`);
// 5. 대상 회사에 이미 존재하는 값 한 번에 조회
const existingValuesResult = await client.query(
`SELECT value_id, table_name, column_name, value_code
@@ -2763,8 +2944,12 @@ export class MenuCopyService {
)
.join(", ");
// 기본 menu_objid: 매핑이 없을 경우 첫 번째 복사된 메뉴 사용
const defaultMenuObjid = menuIdMap.values().next().value || 0;
const valueParams = values.flatMap((v) => {
const newMenuObjid = menuIdMap.get(v.menu_objid);
// 원본 menu_objid가 매핑에 있으면 사용, 없으면 기본값 사용
const newMenuObjid = menuIdMap.get(v.menu_objid) ?? defaultMenuObjid;
const newParentId = v.parent_value_id
? valueIdMap.get(v.parent_value_id) || null
: null;