feat: 채번규칙 메뉴 스코프 전환 완료
✅ 주요 변경사항: - 백엔드: menuService.ts 추가 (형제 메뉴 조회 유틸리티) - 백엔드: numberingRuleService.getAvailableRulesForMenu() 메뉴 스코프 적용 - 백엔드: tableCategoryValueService 메뉴 스코프 준비 (menuObjid 파라미터 추가) - 프론트엔드: TextInputConfigPanel에 부모 메뉴 선택 UI 추가 - 프론트엔드: 메뉴별 채번규칙 필터링 (형제 메뉴 공유) 🔧 기술 세부사항: - getSiblingMenuObjids(): 같은 부모를 가진 형제 메뉴 OBJID 조회 - 채번규칙 우선순위: menu (형제) > table > global - 사용자 메뉴(menu_type='1') 레벨 2만 부모 메뉴로 선택 가능 📝 다음 단계: - 카테고리 컴포넌트도 메뉴 스코프로 전환 예정
This commit is contained in:
@@ -27,12 +27,24 @@ router.get("/available/:menuObjid?", authenticateToken, async (req: Authenticate
|
||||
const companyCode = req.user!.companyCode;
|
||||
const menuObjid = req.params.menuObjid ? parseInt(req.params.menuObjid) : undefined;
|
||||
|
||||
logger.info("📥 메뉴별 채번 규칙 조회 요청", { companyCode, menuObjid });
|
||||
|
||||
try {
|
||||
const rules = await numberingRuleService.getAvailableRulesForMenu(companyCode, menuObjid);
|
||||
|
||||
logger.info("✅ 메뉴별 채번 규칙 조회 성공 (컨트롤러)", {
|
||||
companyCode,
|
||||
menuObjid,
|
||||
rulesCount: rules.length
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: rules });
|
||||
} catch (error: any) {
|
||||
logger.error("메뉴별 사용 가능한 규칙 조회 실패", {
|
||||
logger.error("❌ 메뉴별 사용 가능한 규칙 조회 실패 (컨트롤러)", {
|
||||
error: error.message,
|
||||
errorCode: error.code,
|
||||
errorStack: error.stack,
|
||||
companyCode,
|
||||
menuObjid,
|
||||
});
|
||||
return res.status(500).json({ success: false, error: error.message });
|
||||
|
||||
@@ -32,18 +32,31 @@ export const getCategoryColumns = async (req: AuthenticatedRequest, res: Respons
|
||||
|
||||
/**
|
||||
* 카테고리 값 목록 조회 (메뉴 스코프 적용)
|
||||
*
|
||||
* Query Parameters:
|
||||
* - menuObjid: 메뉴 OBJID (선택사항, 제공 시 형제 메뉴의 카테고리 값 포함)
|
||||
* - includeInactive: 비활성 값 포함 여부
|
||||
*/
|
||||
export const getCategoryValues = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { tableName, columnName } = req.params;
|
||||
const includeInactive = req.query.includeInactive === "true";
|
||||
const menuObjid = req.query.menuObjid ? Number(req.query.menuObjid) : undefined;
|
||||
|
||||
logger.info("카테고리 값 조회 요청", {
|
||||
tableName,
|
||||
columnName,
|
||||
menuObjid,
|
||||
companyCode,
|
||||
});
|
||||
|
||||
const values = await tableCategoryValueService.getCategoryValues(
|
||||
tableName,
|
||||
columnName,
|
||||
companyCode,
|
||||
includeInactive
|
||||
includeInactive,
|
||||
menuObjid // ← menuObjid 전달
|
||||
);
|
||||
|
||||
return res.json({
|
||||
@@ -61,18 +74,37 @@ export const getCategoryValues = async (req: AuthenticatedRequest, res: Response
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 값 추가
|
||||
* 카테고리 값 추가 (메뉴 스코프)
|
||||
*
|
||||
* Body:
|
||||
* - menuObjid: 메뉴 OBJID (필수)
|
||||
* - 나머지 카테고리 값 정보
|
||||
*/
|
||||
export const addCategoryValue = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const value = req.body;
|
||||
const { menuObjid, ...value } = req.body;
|
||||
|
||||
if (!menuObjid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "menuObjid는 필수입니다",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("카테고리 값 추가 요청", {
|
||||
tableName: value.tableName,
|
||||
columnName: value.columnName,
|
||||
menuObjid,
|
||||
companyCode,
|
||||
});
|
||||
|
||||
const newValue = await tableCategoryValueService.addCategoryValue(
|
||||
value,
|
||||
companyCode,
|
||||
userId
|
||||
userId,
|
||||
Number(menuObjid) // ← menuObjid 전달
|
||||
);
|
||||
|
||||
return res.status(201).json({
|
||||
|
||||
159
backend-node/src/services/menuService.ts
Normal file
159
backend-node/src/services/menuService.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* 메뉴 관련 유틸리티 서비스
|
||||
*
|
||||
* 메뉴 스코프 기반 데이터 공유를 위한 형제 메뉴 조회 기능 제공
|
||||
*/
|
||||
|
||||
/**
|
||||
* 메뉴의 형제 메뉴 OBJID 목록 조회
|
||||
* (같은 부모를 가진 메뉴들)
|
||||
*
|
||||
* 메뉴 스코프 규칙:
|
||||
* - 같은 부모를 가진 형제 메뉴들은 카테고리/채번규칙을 공유
|
||||
* - 최상위 메뉴(parent_obj_id = 0)는 자기 자신만 반환
|
||||
* - 메뉴를 찾을 수 없으면 안전하게 자기 자신만 반환
|
||||
*
|
||||
* @param menuObjid 현재 메뉴의 OBJID
|
||||
* @returns 형제 메뉴 OBJID 배열 (자기 자신 포함, 정렬됨)
|
||||
*
|
||||
* @example
|
||||
* // 영업관리 (200)
|
||||
* // ├── 고객관리 (201)
|
||||
* // ├── 계약관리 (202)
|
||||
* // └── 주문관리 (203)
|
||||
*
|
||||
* await getSiblingMenuObjids(201);
|
||||
* // 결과: [201, 202, 203] - 모두 같은 부모(200)를 가진 형제
|
||||
*/
|
||||
export async function getSiblingMenuObjids(menuObjid: number): Promise<number[]> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
logger.info("형제 메뉴 조회 시작", { menuObjid });
|
||||
|
||||
// 1. 현재 메뉴의 부모 찾기
|
||||
const parentQuery = `
|
||||
SELECT parent_obj_id FROM menu_info WHERE objid = $1
|
||||
`;
|
||||
const parentResult = await pool.query(parentQuery, [menuObjid]);
|
||||
|
||||
if (parentResult.rows.length === 0) {
|
||||
logger.warn("메뉴를 찾을 수 없음, 자기 자신만 반환", { menuObjid });
|
||||
return [menuObjid]; // 메뉴가 없으면 안전하게 자기 자신만 반환
|
||||
}
|
||||
|
||||
const parentObjId = parentResult.rows[0].parent_obj_id;
|
||||
|
||||
if (!parentObjId || parentObjId === 0) {
|
||||
// 최상위 메뉴인 경우 자기 자신만 반환
|
||||
logger.info("최상위 메뉴 (형제 없음)", { menuObjid, parentObjId });
|
||||
return [menuObjid];
|
||||
}
|
||||
|
||||
// 2. 같은 부모를 가진 형제 메뉴들 조회
|
||||
const siblingsQuery = `
|
||||
SELECT objid FROM menu_info
|
||||
WHERE parent_obj_id = $1
|
||||
ORDER BY objid
|
||||
`;
|
||||
const siblingsResult = await pool.query(siblingsQuery, [parentObjId]);
|
||||
|
||||
const siblingObjids = siblingsResult.rows.map((row) => Number(row.objid));
|
||||
|
||||
logger.info("형제 메뉴 조회 완료", {
|
||||
menuObjid,
|
||||
parentObjId,
|
||||
siblingCount: siblingObjids.length,
|
||||
siblings: siblingObjids,
|
||||
});
|
||||
|
||||
return siblingObjids;
|
||||
} catch (error: any) {
|
||||
logger.error("형제 메뉴 조회 실패", {
|
||||
menuObjid,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
// 에러 발생 시 안전하게 자기 자신만 반환
|
||||
return [menuObjid];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 메뉴의 형제 메뉴 OBJID 합집합 조회
|
||||
*
|
||||
* 여러 메뉴에 속한 모든 형제 메뉴를 중복 제거하여 반환
|
||||
*
|
||||
* @param menuObjids 메뉴 OBJID 배열
|
||||
* @returns 모든 형제 메뉴 OBJID 배열 (중복 제거, 정렬됨)
|
||||
*
|
||||
* @example
|
||||
* // 서로 다른 부모를 가진 메뉴들의 형제를 모두 조회
|
||||
* await getAllSiblingMenuObjids([201, 301]);
|
||||
* // 201의 형제: [201, 202, 203]
|
||||
* // 301의 형제: [301, 302]
|
||||
* // 결과: [201, 202, 203, 301, 302]
|
||||
*/
|
||||
export async function getAllSiblingMenuObjids(
|
||||
menuObjids: number[]
|
||||
): Promise<number[]> {
|
||||
if (!menuObjids || menuObjids.length === 0) {
|
||||
logger.warn("getAllSiblingMenuObjids: 빈 배열 입력");
|
||||
return [];
|
||||
}
|
||||
|
||||
const allSiblings = new Set<number>();
|
||||
|
||||
for (const objid of menuObjids) {
|
||||
const siblings = await getSiblingMenuObjids(objid);
|
||||
siblings.forEach((s) => allSiblings.add(s));
|
||||
}
|
||||
|
||||
const result = Array.from(allSiblings).sort((a, b) => a - b);
|
||||
|
||||
logger.info("여러 메뉴의 형제 조회 완료", {
|
||||
inputMenus: menuObjids,
|
||||
resultCount: result.length,
|
||||
result,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 정보 조회
|
||||
*
|
||||
* @param menuObjid 메뉴 OBJID
|
||||
* @returns 메뉴 정보 (없으면 null)
|
||||
*/
|
||||
export async function getMenuInfo(menuObjid: number): Promise<any | null> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
objid,
|
||||
parent_obj_id AS "parentObjId",
|
||||
menu_name_kor AS "menuNameKor",
|
||||
menu_name_eng AS "menuNameEng",
|
||||
menu_url AS "menuUrl",
|
||||
company_code AS "companyCode"
|
||||
FROM menu_info
|
||||
WHERE objid = $1
|
||||
`;
|
||||
const result = await pool.query(query, [menuObjid]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.rows[0];
|
||||
} catch (error: any) {
|
||||
logger.error("메뉴 정보 조회 실패", { menuObjid, error: error.message });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
import { getSiblingMenuObjids } from "./menuService";
|
||||
|
||||
interface NumberingRulePart {
|
||||
id?: number;
|
||||
@@ -150,22 +151,33 @@ class NumberingRuleService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 메뉴에서 사용 가능한 규칙 목록 조회
|
||||
* 현재 메뉴에서 사용 가능한 규칙 목록 조회 (메뉴 스코프)
|
||||
*
|
||||
* 메뉴 스코프 규칙:
|
||||
* - menuObjid가 제공되면 형제 메뉴의 채번 규칙 포함
|
||||
* - 우선순위: menu (형제 메뉴) > table > global
|
||||
*/
|
||||
async getAvailableRulesForMenu(
|
||||
companyCode: string,
|
||||
menuObjid?: number
|
||||
): Promise<NumberingRuleConfig[]> {
|
||||
try {
|
||||
logger.info("메뉴별 사용 가능한 채번 규칙 조회 시작", {
|
||||
logger.info("메뉴별 사용 가능한 채번 규칙 조회 시작 (메뉴 스코프)", {
|
||||
companyCode,
|
||||
menuObjid,
|
||||
});
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
// 1. 형제 메뉴 OBJID 조회
|
||||
let siblingObjids: number[] = [];
|
||||
if (menuObjid) {
|
||||
siblingObjids = await getSiblingMenuObjids(menuObjid);
|
||||
logger.info("형제 메뉴 OBJID 목록", { menuObjid, siblingObjids });
|
||||
}
|
||||
|
||||
// menuObjid가 없으면 global 규칙만 반환
|
||||
if (!menuObjid) {
|
||||
if (!menuObjid || siblingObjids.length === 0) {
|
||||
let query: string;
|
||||
let params: any[];
|
||||
|
||||
@@ -261,35 +273,13 @@ class NumberingRuleService {
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
// 현재 메뉴의 상위 계층 조회 (2레벨 메뉴 찾기)
|
||||
const menuHierarchyQuery = `
|
||||
WITH RECURSIVE menu_path AS (
|
||||
SELECT objid, objid_parent, menu_level
|
||||
FROM menu_info
|
||||
WHERE objid = $1
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT mi.objid, mi.objid_parent, mi.menu_level
|
||||
FROM menu_info mi
|
||||
INNER JOIN menu_path mp ON mi.objid = mp.objid_parent
|
||||
)
|
||||
SELECT objid, menu_level
|
||||
FROM menu_path
|
||||
WHERE menu_level = 2
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const hierarchyResult = await pool.query(menuHierarchyQuery, [menuObjid]);
|
||||
const level2MenuObjid =
|
||||
hierarchyResult.rowCount > 0 ? hierarchyResult.rows[0].objid : null;
|
||||
|
||||
// 사용 가능한 규칙 조회 (멀티테넌시 적용)
|
||||
// 2. 메뉴 스코프: 형제 메뉴의 채번 규칙 조회
|
||||
// 우선순위: menu (형제 메뉴) > table > global
|
||||
let query: string;
|
||||
let params: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 규칙 조회
|
||||
// 최고 관리자: 모든 규칙 조회 (형제 메뉴 포함)
|
||||
query = `
|
||||
SELECT
|
||||
rule_id AS "ruleId",
|
||||
@@ -309,12 +299,20 @@ class NumberingRuleService {
|
||||
FROM numbering_rules
|
||||
WHERE
|
||||
scope_type = 'global'
|
||||
OR (scope_type = 'menu' AND menu_objid = $1)
|
||||
ORDER BY scope_type DESC, created_at DESC
|
||||
OR scope_type = 'table'
|
||||
OR (scope_type = 'menu' AND menu_objid = ANY($1))
|
||||
ORDER BY
|
||||
CASE scope_type
|
||||
WHEN 'menu' THEN 1
|
||||
WHEN 'table' THEN 2
|
||||
WHEN 'global' THEN 3
|
||||
END,
|
||||
created_at DESC
|
||||
`;
|
||||
params = [level2MenuObjid];
|
||||
params = [siblingObjids];
|
||||
logger.info("최고 관리자: 형제 메뉴 포함 채번 규칙 조회", { siblingObjids });
|
||||
} else {
|
||||
// 일반 회사: 자신의 규칙만 조회
|
||||
// 일반 회사: 자신의 규칙만 조회 (형제 메뉴 포함)
|
||||
query = `
|
||||
SELECT
|
||||
rule_id AS "ruleId",
|
||||
@@ -335,58 +333,91 @@ class NumberingRuleService {
|
||||
WHERE company_code = $1
|
||||
AND (
|
||||
scope_type = 'global'
|
||||
OR (scope_type = 'menu' AND menu_objid = $2)
|
||||
OR scope_type = 'table'
|
||||
OR (scope_type = 'menu' AND menu_objid = ANY($2))
|
||||
)
|
||||
ORDER BY scope_type DESC, created_at DESC
|
||||
ORDER BY
|
||||
CASE scope_type
|
||||
WHEN 'menu' THEN 1
|
||||
WHEN 'table' THEN 2
|
||||
WHEN 'global' THEN 3
|
||||
END,
|
||||
created_at DESC
|
||||
`;
|
||||
params = [companyCode, level2MenuObjid];
|
||||
params = [companyCode, siblingObjids];
|
||||
logger.info("회사별: 형제 메뉴 포함 채번 규칙 조회", { companyCode, siblingObjids });
|
||||
}
|
||||
|
||||
logger.info("🔍 채번 규칙 쿼리 실행", {
|
||||
queryPreview: query.substring(0, 200),
|
||||
paramsTypes: params.map(p => typeof p),
|
||||
paramsValues: params,
|
||||
});
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
logger.info("✅ 채번 규칙 쿼리 성공", { rowCount: result.rows.length });
|
||||
|
||||
// 파트 정보 추가
|
||||
for (const rule of result.rows) {
|
||||
let partsQuery: string;
|
||||
let partsParams: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
partsQuery = `
|
||||
SELECT
|
||||
id,
|
||||
part_order AS "order",
|
||||
part_type AS "partType",
|
||||
generation_method AS "generationMethod",
|
||||
auto_config AS "autoConfig",
|
||||
manual_config AS "manualConfig"
|
||||
FROM numbering_rule_parts
|
||||
WHERE rule_id = $1
|
||||
ORDER BY part_order
|
||||
`;
|
||||
partsParams = [rule.ruleId];
|
||||
} else {
|
||||
partsQuery = `
|
||||
SELECT
|
||||
id,
|
||||
part_order AS "order",
|
||||
part_type AS "partType",
|
||||
generation_method AS "generationMethod",
|
||||
auto_config AS "autoConfig",
|
||||
manual_config AS "manualConfig"
|
||||
FROM numbering_rule_parts
|
||||
WHERE rule_id = $1 AND company_code = $2
|
||||
ORDER BY part_order
|
||||
`;
|
||||
partsParams = [rule.ruleId, companyCode];
|
||||
}
|
||||
try {
|
||||
let partsQuery: string;
|
||||
let partsParams: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
partsQuery = `
|
||||
SELECT
|
||||
id,
|
||||
part_order AS "order",
|
||||
part_type AS "partType",
|
||||
generation_method AS "generationMethod",
|
||||
auto_config AS "autoConfig",
|
||||
manual_config AS "manualConfig"
|
||||
FROM numbering_rule_parts
|
||||
WHERE rule_id = $1
|
||||
ORDER BY part_order
|
||||
`;
|
||||
partsParams = [rule.ruleId];
|
||||
} else {
|
||||
partsQuery = `
|
||||
SELECT
|
||||
id,
|
||||
part_order AS "order",
|
||||
part_type AS "partType",
|
||||
generation_method AS "generationMethod",
|
||||
auto_config AS "autoConfig",
|
||||
manual_config AS "manualConfig"
|
||||
FROM numbering_rule_parts
|
||||
WHERE rule_id = $1 AND company_code = $2
|
||||
ORDER BY part_order
|
||||
`;
|
||||
partsParams = [rule.ruleId, companyCode];
|
||||
}
|
||||
|
||||
const partsResult = await pool.query(partsQuery, partsParams);
|
||||
rule.parts = partsResult.rows;
|
||||
const partsResult = await pool.query(partsQuery, partsParams);
|
||||
rule.parts = partsResult.rows;
|
||||
|
||||
logger.info("✅ 규칙 파트 조회 성공", {
|
||||
ruleId: rule.ruleId,
|
||||
ruleName: rule.ruleName,
|
||||
partsCount: partsResult.rows.length,
|
||||
});
|
||||
} catch (partError: any) {
|
||||
logger.error("❌ 규칙 파트 조회 실패", {
|
||||
ruleId: rule.ruleId,
|
||||
ruleName: rule.ruleName,
|
||||
error: partError.message,
|
||||
errorCode: partError.code,
|
||||
errorStack: partError.stack,
|
||||
});
|
||||
throw partError;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("메뉴별 사용 가능한 채번 규칙 조회 완료", {
|
||||
companyCode,
|
||||
menuObjid,
|
||||
level2MenuObjid,
|
||||
siblingCount: siblingObjids.length,
|
||||
count: result.rowCount,
|
||||
});
|
||||
|
||||
@@ -394,8 +425,11 @@ class NumberingRuleService {
|
||||
} catch (error: any) {
|
||||
logger.error("메뉴별 채번 규칙 조회 실패", {
|
||||
error: error.message,
|
||||
errorCode: error.code,
|
||||
errorStack: error.stack,
|
||||
companyCode,
|
||||
menuObjid,
|
||||
siblingObjids: siblingObjids || [],
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
import { getSiblingMenuObjids } from "./menuService";
|
||||
import {
|
||||
TableCategoryValue,
|
||||
CategoryColumn,
|
||||
@@ -79,84 +80,164 @@ class TableCategoryValueService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 컬럼의 카테고리 값 목록 조회 (테이블 스코프)
|
||||
* 특정 컬럼의 카테고리 값 목록 조회 (메뉴 스코프)
|
||||
*
|
||||
* 메뉴 스코프 규칙:
|
||||
* - menuObjid가 제공되면 해당 메뉴와 형제 메뉴의 카테고리 값을 조회
|
||||
* - menuObjid가 없으면 테이블 스코프로 동작 (하위 호환성)
|
||||
*/
|
||||
async getCategoryValues(
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
companyCode: string,
|
||||
includeInactive: boolean = false
|
||||
includeInactive: boolean = false,
|
||||
menuObjid?: number
|
||||
): Promise<TableCategoryValue[]> {
|
||||
try {
|
||||
logger.info("카테고리 값 목록 조회", {
|
||||
logger.info("카테고리 값 목록 조회 (메뉴 스코프)", {
|
||||
tableName,
|
||||
columnName,
|
||||
companyCode,
|
||||
includeInactive,
|
||||
menuObjid,
|
||||
});
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
// 멀티테넌시: 최고 관리자만 company_code="*" 데이터를 볼 수 있음
|
||||
// 1. 메뉴 스코프: 형제 메뉴 OBJID 조회
|
||||
let siblingObjids: number[] = [];
|
||||
if (menuObjid) {
|
||||
siblingObjids = await getSiblingMenuObjids(menuObjid);
|
||||
logger.info("형제 메뉴 OBJID 목록", { menuObjid, siblingObjids });
|
||||
}
|
||||
|
||||
// 2. 카테고리 값 조회 (형제 메뉴 포함)
|
||||
let query: string;
|
||||
let params: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 카테고리 값 조회
|
||||
query = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy",
|
||||
updated_by AS "updatedBy"
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
`;
|
||||
params = [tableName, columnName];
|
||||
if (menuObjid && siblingObjids.length > 0) {
|
||||
// 메뉴 스코프 적용
|
||||
query = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
menu_objid AS "menuObjid",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy",
|
||||
updated_by AS "updatedBy"
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND menu_objid = ANY($3)
|
||||
`;
|
||||
params = [tableName, columnName, siblingObjids];
|
||||
} else {
|
||||
// 테이블 스코프 (하위 호환성)
|
||||
query = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
menu_objid AS "menuObjid",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy",
|
||||
updated_by AS "updatedBy"
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
`;
|
||||
params = [tableName, columnName];
|
||||
}
|
||||
logger.info("최고 관리자 카테고리 값 조회");
|
||||
} else {
|
||||
// 일반 회사: 자신의 카테고리 값만 조회
|
||||
query = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy",
|
||||
updated_by AS "updatedBy"
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND company_code = $3
|
||||
`;
|
||||
params = [tableName, columnName, companyCode];
|
||||
if (menuObjid && siblingObjids.length > 0) {
|
||||
// 메뉴 스코프 적용
|
||||
query = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
menu_objid AS "menuObjid",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy",
|
||||
updated_by AS "updatedBy"
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND menu_objid = ANY($3)
|
||||
AND company_code = $4
|
||||
`;
|
||||
params = [tableName, columnName, siblingObjids, companyCode];
|
||||
} else {
|
||||
// 테이블 스코프 (하위 호환성)
|
||||
query = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
menu_objid AS "menuObjid",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy",
|
||||
updated_by AS "updatedBy"
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND company_code = $3
|
||||
`;
|
||||
params = [tableName, columnName, companyCode];
|
||||
}
|
||||
logger.info("회사별 카테고리 값 조회", { companyCode });
|
||||
}
|
||||
|
||||
@@ -175,6 +256,8 @@ class TableCategoryValueService {
|
||||
tableName,
|
||||
columnName,
|
||||
companyCode,
|
||||
menuObjid,
|
||||
scopeType: menuObjid ? "menu" : "table",
|
||||
});
|
||||
|
||||
return values;
|
||||
@@ -185,17 +268,31 @@ class TableCategoryValueService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 값 추가
|
||||
* 카테고리 값 추가 (메뉴 스코프)
|
||||
*
|
||||
* @param value 카테고리 값 정보
|
||||
* @param companyCode 회사 코드
|
||||
* @param userId 생성자 ID
|
||||
* @param menuObjid 메뉴 OBJID (필수)
|
||||
*/
|
||||
async addCategoryValue(
|
||||
value: TableCategoryValue,
|
||||
companyCode: string,
|
||||
userId: string
|
||||
userId: string,
|
||||
menuObjid: number
|
||||
): Promise<TableCategoryValue> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
// 중복 코드 체크 (멀티테넌시 적용)
|
||||
logger.info("카테고리 값 추가 (메뉴 스코프)", {
|
||||
tableName: value.tableName,
|
||||
columnName: value.columnName,
|
||||
valueCode: value.valueCode,
|
||||
menuObjid,
|
||||
companyCode,
|
||||
});
|
||||
|
||||
// 중복 코드 체크 (멀티테넌시 + 메뉴 스코프)
|
||||
let duplicateQuery: string;
|
||||
let duplicateParams: any[];
|
||||
|
||||
@@ -207,8 +304,9 @@ class TableCategoryValueService {
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND value_code = $3
|
||||
AND menu_objid = $4
|
||||
`;
|
||||
duplicateParams = [value.tableName, value.columnName, value.valueCode];
|
||||
duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid];
|
||||
} else {
|
||||
// 일반 회사: 자신의 회사에서만 중복 체크
|
||||
duplicateQuery = `
|
||||
@@ -217,9 +315,10 @@ class TableCategoryValueService {
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND value_code = $3
|
||||
AND company_code = $4
|
||||
AND menu_objid = $4
|
||||
AND company_code = $5
|
||||
`;
|
||||
duplicateParams = [value.tableName, value.columnName, value.valueCode, companyCode];
|
||||
duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid, companyCode];
|
||||
}
|
||||
|
||||
const duplicateResult = await pool.query(duplicateQuery, duplicateParams);
|
||||
@@ -232,8 +331,8 @@ class TableCategoryValueService {
|
||||
INSERT INTO table_column_category_values (
|
||||
table_name, column_name, value_code, value_label, value_order,
|
||||
parent_value_id, depth, description, color, icon,
|
||||
is_active, is_default, company_code, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
is_active, is_default, company_code, menu_objid, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
RETURNING
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
@@ -249,6 +348,7 @@ class TableCategoryValueService {
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
menu_objid AS "menuObjid",
|
||||
created_at AS "createdAt",
|
||||
created_by AS "createdBy"
|
||||
`;
|
||||
@@ -267,6 +367,7 @@ class TableCategoryValueService {
|
||||
value.isActive !== false,
|
||||
value.isDefault || false,
|
||||
companyCode,
|
||||
menuObjid, // ← 메뉴 OBJID 저장
|
||||
userId,
|
||||
]);
|
||||
|
||||
@@ -274,6 +375,7 @@ class TableCategoryValueService {
|
||||
valueId: result.rows[0].valueId,
|
||||
tableName: value.tableName,
|
||||
columnName: value.columnName,
|
||||
menuObjid,
|
||||
});
|
||||
|
||||
return result.rows[0];
|
||||
|
||||
Reference in New Issue
Block a user