회사별 메뉴 분리 및 권한 관리
This commit is contained in:
@@ -21,14 +21,22 @@ export class CommonCodeController {
|
||||
async getCategories(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { search, isActive, page = "1", size = "20" } = req.query;
|
||||
const userCompanyCode = req.user?.companyCode;
|
||||
|
||||
const categories = await this.commonCodeService.getCategories({
|
||||
search: search as string,
|
||||
isActive:
|
||||
isActive === "true" ? true : isActive === "false" ? false : undefined,
|
||||
page: parseInt(page as string),
|
||||
size: parseInt(size as string),
|
||||
});
|
||||
const categories = await this.commonCodeService.getCategories(
|
||||
{
|
||||
search: search as string,
|
||||
isActive:
|
||||
isActive === "true"
|
||||
? true
|
||||
: isActive === "false"
|
||||
? false
|
||||
: undefined,
|
||||
page: parseInt(page as string),
|
||||
size: parseInt(size as string),
|
||||
},
|
||||
userCompanyCode
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
@@ -54,14 +62,23 @@ export class CommonCodeController {
|
||||
try {
|
||||
const { categoryCode } = req.params;
|
||||
const { search, isActive, page, size } = req.query;
|
||||
const userCompanyCode = req.user?.companyCode;
|
||||
|
||||
const result = await this.commonCodeService.getCodes(categoryCode, {
|
||||
search: search as string,
|
||||
isActive:
|
||||
isActive === "true" ? true : isActive === "false" ? false : undefined,
|
||||
page: page ? parseInt(page as string) : undefined,
|
||||
size: size ? parseInt(size as string) : undefined,
|
||||
});
|
||||
const result = await this.commonCodeService.getCodes(
|
||||
categoryCode,
|
||||
{
|
||||
search: search as string,
|
||||
isActive:
|
||||
isActive === "true"
|
||||
? true
|
||||
: isActive === "false"
|
||||
? false
|
||||
: undefined,
|
||||
page: page ? parseInt(page as string) : undefined,
|
||||
size: size ? parseInt(size as string) : undefined,
|
||||
},
|
||||
userCompanyCode
|
||||
);
|
||||
|
||||
// 프론트엔드가 기대하는 형식으로 데이터 변환
|
||||
const transformedData = result.data.map((code: any) => ({
|
||||
@@ -73,7 +90,8 @@ export class CommonCodeController {
|
||||
sortOrder: code.sort_order,
|
||||
isActive: code.is_active,
|
||||
useYn: code.is_active,
|
||||
|
||||
companyCode: code.company_code, // 추가
|
||||
|
||||
// 기존 필드명도 유지 (하위 호환성)
|
||||
code_category: code.code_category,
|
||||
code_value: code.code_value,
|
||||
@@ -81,6 +99,7 @@ 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, // 추가
|
||||
created_date: code.created_date,
|
||||
created_by: code.created_by,
|
||||
updated_date: code.updated_date,
|
||||
@@ -110,7 +129,8 @@ export class CommonCodeController {
|
||||
async createCategory(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const categoryData: CreateCategoryData = req.body;
|
||||
const userId = req.user?.userId || "SYSTEM"; // 인증 미들웨어에서 설정된 사용자 ID
|
||||
const userId = req.user?.userId || "SYSTEM";
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
// 입력값 검증
|
||||
if (!categoryData.categoryCode || !categoryData.categoryName) {
|
||||
@@ -122,7 +142,8 @@ export class CommonCodeController {
|
||||
|
||||
const category = await this.commonCodeService.createCategory(
|
||||
categoryData,
|
||||
userId
|
||||
userId,
|
||||
companyCode
|
||||
);
|
||||
|
||||
return res.status(201).json({
|
||||
@@ -135,7 +156,7 @@ export class CommonCodeController {
|
||||
|
||||
// PostgreSQL 에러 처리
|
||||
if (
|
||||
((error as any)?.code === "23505") || // PostgreSQL unique_violation
|
||||
(error as any)?.code === "23505" || // PostgreSQL unique_violation
|
||||
(error instanceof Error && error.message.includes("Unique constraint"))
|
||||
) {
|
||||
return res.status(409).json({
|
||||
@@ -161,11 +182,13 @@ export class CommonCodeController {
|
||||
const { categoryCode } = req.params;
|
||||
const categoryData: Partial<CreateCategoryData> = req.body;
|
||||
const userId = req.user?.userId || "SYSTEM";
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
const category = await this.commonCodeService.updateCategory(
|
||||
categoryCode,
|
||||
categoryData,
|
||||
userId
|
||||
userId,
|
||||
companyCode
|
||||
);
|
||||
|
||||
return res.json({
|
||||
@@ -201,8 +224,9 @@ export class CommonCodeController {
|
||||
async deleteCategory(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { categoryCode } = req.params;
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
await this.commonCodeService.deleteCategory(categoryCode);
|
||||
await this.commonCodeService.deleteCategory(categoryCode, companyCode);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
@@ -238,6 +262,7 @@ export class CommonCodeController {
|
||||
const { categoryCode } = req.params;
|
||||
const codeData: CreateCodeData = req.body;
|
||||
const userId = req.user?.userId || "SYSTEM";
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
// 입력값 검증
|
||||
if (!codeData.codeValue || !codeData.codeName) {
|
||||
@@ -250,7 +275,8 @@ export class CommonCodeController {
|
||||
const code = await this.commonCodeService.createCode(
|
||||
categoryCode,
|
||||
codeData,
|
||||
userId
|
||||
userId,
|
||||
companyCode
|
||||
);
|
||||
|
||||
return res.status(201).json({
|
||||
@@ -288,12 +314,14 @@ export class CommonCodeController {
|
||||
const { categoryCode, codeValue } = req.params;
|
||||
const codeData: Partial<CreateCodeData> = req.body;
|
||||
const userId = req.user?.userId || "SYSTEM";
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
const code = await this.commonCodeService.updateCode(
|
||||
categoryCode,
|
||||
codeValue,
|
||||
codeData,
|
||||
userId
|
||||
userId,
|
||||
companyCode
|
||||
);
|
||||
|
||||
return res.json({
|
||||
@@ -332,8 +360,13 @@ export class CommonCodeController {
|
||||
async deleteCode(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { categoryCode, codeValue } = req.params;
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
await this.commonCodeService.deleteCode(categoryCode, codeValue);
|
||||
await this.commonCodeService.deleteCode(
|
||||
categoryCode,
|
||||
codeValue,
|
||||
companyCode
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
@@ -370,8 +403,12 @@ export class CommonCodeController {
|
||||
async getCodeOptions(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { categoryCode } = req.params;
|
||||
const userCompanyCode = req.user?.companyCode;
|
||||
|
||||
const options = await this.commonCodeService.getCodeOptions(categoryCode);
|
||||
const options = await this.commonCodeService.getCodeOptions(
|
||||
categoryCode,
|
||||
userCompanyCode
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
|
||||
@@ -383,6 +383,79 @@ export class DDLController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/ddl/tables/:tableName - 테이블 삭제 (최고 관리자 전용)
|
||||
*/
|
||||
static async dropTable(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const userId = req.user!.userId;
|
||||
const userCompanyCode = req.user!.companyCode;
|
||||
|
||||
// 입력값 기본 검증
|
||||
if (!tableName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: "INVALID_INPUT",
|
||||
details: "테이블명이 필요합니다.",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("테이블 삭제 요청", {
|
||||
tableName,
|
||||
userId,
|
||||
userCompanyCode,
|
||||
ip: req.ip,
|
||||
});
|
||||
|
||||
// DDL 실행 서비스 호출
|
||||
const ddlService = new DDLExecutionService();
|
||||
const result = await ddlService.dropTable(
|
||||
tableName,
|
||||
userCompanyCode,
|
||||
userId
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: result.message,
|
||||
data: {
|
||||
tableName,
|
||||
executedQuery: result.executedQuery,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: result.message,
|
||||
error: result.error,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("테이블 삭제 컨트롤러 오류:", {
|
||||
error: (error as Error).message,
|
||||
stack: (error as Error).stack,
|
||||
userId: req.user?.userId,
|
||||
tableName: req.params.tableName,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
details: "테이블 삭제 중 서버 오류가 발생했습니다.",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/ddl/logs/cleanup - 오래된 DDL 로그 정리
|
||||
*/
|
||||
|
||||
@@ -551,6 +551,76 @@ export class FlowController {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 플로우 스텝의 컬럼 라벨 조회
|
||||
*/
|
||||
getStepColumnLabels = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { flowId, stepId } = req.params;
|
||||
|
||||
const step = await this.flowStepService.getById(parseInt(stepId));
|
||||
if (!step) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "Step not found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const flowDef = await this.flowDefinitionService.getById(
|
||||
parseInt(flowId)
|
||||
);
|
||||
if (!flowDef) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "Flow definition not found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 테이블명 결정 (스텝 테이블 우선, 없으면 플로우 테이블)
|
||||
const tableName = step.tableName || flowDef.tableName;
|
||||
if (!tableName) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// column_labels 테이블에서 라벨 정보 조회
|
||||
const { query } = await import("../config/database");
|
||||
const labelRows = await query<{
|
||||
column_name: string;
|
||||
column_label: string | null;
|
||||
}>(
|
||||
`SELECT column_name, column_label
|
||||
FROM column_labels
|
||||
WHERE table_name = $1 AND column_label IS NOT NULL`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
// { columnName: label } 형태의 객체로 변환
|
||||
const labels: Record<string, string> = {};
|
||||
labelRows.forEach((row) => {
|
||||
if (row.column_label) {
|
||||
labels[row.column_name] = row.column_label;
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: labels,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error getting step column labels:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "Failed to get step column labels",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 플로우의 모든 단계별 카운트 조회
|
||||
*/
|
||||
|
||||
@@ -42,6 +42,18 @@ router.post(
|
||||
DDLController.addColumn
|
||||
);
|
||||
|
||||
/**
|
||||
* 테이블 삭제
|
||||
* DELETE /api/ddl/tables/:tableName
|
||||
*/
|
||||
router.delete(
|
||||
"/tables/:tableName",
|
||||
authenticateToken,
|
||||
requireSuperAdmin,
|
||||
validateDDLPermission,
|
||||
DDLController.dropTable
|
||||
);
|
||||
|
||||
/**
|
||||
* 테이블 생성 사전 검증 (실제 생성하지 않고 검증만)
|
||||
* POST /api/ddl/validate/table
|
||||
@@ -135,6 +147,7 @@ router.get("/info", authenticateToken, requireSuperAdmin, (req, res) => {
|
||||
tables: {
|
||||
create: "POST /api/ddl/tables",
|
||||
addColumn: "POST /api/ddl/tables/:tableName/columns",
|
||||
drop: "DELETE /api/ddl/tables/:tableName",
|
||||
getInfo: "GET /api/ddl/tables/:tableName/info",
|
||||
getHistory: "GET /api/ddl/tables/:tableName/history",
|
||||
},
|
||||
|
||||
@@ -33,6 +33,10 @@ router.delete("/connections/:connectionId", flowController.deleteConnection);
|
||||
// ==================== 플로우 실행 ====================
|
||||
router.get("/:flowId/step/:stepId/count", flowController.getStepDataCount);
|
||||
router.get("/:flowId/step/:stepId/list", flowController.getStepDataList);
|
||||
router.get(
|
||||
"/:flowId/step/:stepId/column-labels",
|
||||
flowController.getStepColumnLabels
|
||||
);
|
||||
router.get("/:flowId/steps/counts", flowController.getAllStepCounts);
|
||||
|
||||
// ==================== 데이터 이동 ====================
|
||||
|
||||
@@ -23,6 +23,7 @@ export class AdminService {
|
||||
|
||||
// 1. 권한 그룹 기반 필터링 (좌측 사이드바인 경우만)
|
||||
let authFilter = "";
|
||||
let unionFilter = ""; // UNION ALL의 하위 메뉴 필터
|
||||
let queryParams: any[] = [userLang];
|
||||
let paramIndex = 2;
|
||||
|
||||
@@ -51,17 +52,36 @@ export class AdminService {
|
||||
if (userRoleGroups.length > 0) {
|
||||
const roleObjids = userRoleGroups.map((rg: any) => rg.role_objid);
|
||||
// 루트 메뉴: 회사 코드만 체크 (권한 체크 X)
|
||||
// 하위 메뉴: 회사 메뉴는 모두, 공통 메뉴는 권한 체크
|
||||
authFilter = `AND MENU.COMPANY_CODE IN ($${paramIndex}, '*')`;
|
||||
queryParams.push(userCompanyCode);
|
||||
const companyParamIndex = paramIndex;
|
||||
paramIndex++;
|
||||
|
||||
// 하위 메뉴: 회사 메뉴는 모두, 공통 메뉴는 권한 체크
|
||||
unionFilter = `
|
||||
AND (
|
||||
MENU_SUB.COMPANY_CODE = $${companyParamIndex}
|
||||
OR (
|
||||
MENU_SUB.COMPANY_CODE = '*'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM rel_menu_auth rma
|
||||
WHERE rma.menu_objid = MENU_SUB.OBJID
|
||||
AND rma.auth_objid = ANY($${paramIndex})
|
||||
AND rma.read_yn = 'Y'
|
||||
)
|
||||
)
|
||||
)
|
||||
`;
|
||||
queryParams.push(roleObjids);
|
||||
paramIndex += 2;
|
||||
paramIndex++;
|
||||
logger.info(
|
||||
`✅ 회사 관리자: 회사 ${userCompanyCode} 메뉴 전체 + 권한 있는 공통 메뉴`
|
||||
);
|
||||
} else {
|
||||
// 권한 그룹이 없는 회사 관리자: 자기 회사 메뉴만
|
||||
authFilter = `AND MENU.COMPANY_CODE = $${paramIndex}`;
|
||||
unionFilter = `AND MENU_SUB.COMPANY_CODE = $${paramIndex}`;
|
||||
queryParams.push(userCompanyCode);
|
||||
paramIndex++;
|
||||
logger.info(
|
||||
@@ -81,6 +101,15 @@ export class AdminService {
|
||||
AND rma.read_yn = 'Y'
|
||||
)
|
||||
`;
|
||||
unionFilter = `
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM rel_menu_auth rma
|
||||
WHERE rma.menu_objid = MENU_SUB.OBJID
|
||||
AND rma.auth_objid = ANY($${paramIndex})
|
||||
AND rma.read_yn = 'Y'
|
||||
)
|
||||
`;
|
||||
queryParams.push(roleObjids);
|
||||
paramIndex++;
|
||||
logger.info(
|
||||
@@ -97,6 +126,8 @@ export class AdminService {
|
||||
} else if (menuType !== undefined && userType === "SUPER_ADMIN") {
|
||||
// 좌측 사이드바 + SUPER_ADMIN: 권한 그룹 체크 없이 모든 공통 메뉴 표시
|
||||
logger.info(`✅ 최고 관리자는 권한 그룹 체크 없이 모든 공통 메뉴 표시`);
|
||||
// unionFilter는 비워둠 (하위 메뉴도 공통 메뉴만)
|
||||
unionFilter = `AND MENU_SUB.COMPANY_CODE = '*'`;
|
||||
}
|
||||
|
||||
// 2. 회사별 필터링 조건 생성
|
||||
@@ -274,19 +305,7 @@ export class AdminService {
|
||||
JOIN V_MENU ON MENU_SUB.PARENT_OBJ_ID = V_MENU.OBJID
|
||||
WHERE MENU_SUB.OBJID != ANY(V_MENU.PATH)
|
||||
AND MENU_SUB.STATUS = 'active'
|
||||
AND (
|
||||
MENU_SUB.COMPANY_CODE = $2
|
||||
OR (
|
||||
MENU_SUB.COMPANY_CODE = '*'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM rel_menu_auth rma
|
||||
WHERE rma.menu_objid = MENU_SUB.OBJID
|
||||
AND rma.auth_objid = ANY($3)
|
||||
AND rma.read_yn = 'Y'
|
||||
)
|
||||
)
|
||||
)
|
||||
${unionFilter}
|
||||
)
|
||||
SELECT
|
||||
LEVEL AS LEV,
|
||||
@@ -347,66 +366,82 @@ export class AdminService {
|
||||
|
||||
const { userId, userCompanyCode, userType, userLang = "ko" } = paramMap;
|
||||
|
||||
// 1. 사용자가 속한 권한 그룹 조회
|
||||
const userRoleGroups = await query<any>(
|
||||
`
|
||||
SELECT DISTINCT am.objid AS role_objid, am.auth_name
|
||||
FROM authority_master am
|
||||
JOIN authority_sub_user asu ON am.objid = asu.master_objid
|
||||
WHERE asu.user_id = $1
|
||||
AND am.status = 'active'
|
||||
`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`✅ 사용자 ${userId}가 속한 권한 그룹: ${userRoleGroups.length}개`,
|
||||
{
|
||||
roleGroups: userRoleGroups.map((rg: any) => rg.auth_name),
|
||||
}
|
||||
);
|
||||
|
||||
// 2. 권한 그룹 기반 메뉴 필터 조건 생성
|
||||
// 1. 권한 그룹 기반 필터링 (SUPER_ADMIN은 제외)
|
||||
let authFilter = "";
|
||||
let unionFilter = "";
|
||||
let queryParams: any[] = [userLang];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (userRoleGroups.length > 0) {
|
||||
// 권한 그룹이 있는 경우: read_yn = 'Y'인 메뉴만 필터링
|
||||
const roleObjids = userRoleGroups.map((rg: any) => rg.role_objid);
|
||||
authFilter = `
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM rel_menu_auth rma
|
||||
WHERE rma.menu_objid = MENU.OBJID
|
||||
AND rma.auth_objid = ANY($${paramIndex})
|
||||
AND rma.read_yn = 'Y'
|
||||
)
|
||||
`;
|
||||
queryParams.push(roleObjids);
|
||||
paramIndex++;
|
||||
logger.info(
|
||||
`✅ 권한 그룹 기반 메뉴 필터링 적용: ${roleObjids.length}개 그룹`
|
||||
);
|
||||
if (userType === "SUPER_ADMIN" && userCompanyCode === "*") {
|
||||
// SUPER_ADMIN: 권한 그룹 체크 없이 공통 메뉴만 표시
|
||||
logger.info("✅ 좌측 사이드바 (SUPER_ADMIN): 공통 메뉴만 표시");
|
||||
authFilter = "";
|
||||
unionFilter = "";
|
||||
} else {
|
||||
// 권한 그룹이 없는 경우: 메뉴 없음
|
||||
logger.warn(
|
||||
`⚠️ 사용자 ${userId}는 권한 그룹이 없어 메뉴가 표시되지 않습니다.`
|
||||
// 일반 사용자 / 회사 관리자: 권한 그룹 조회 필요
|
||||
const userRoleGroups = await query<any>(
|
||||
`
|
||||
SELECT DISTINCT am.objid AS role_objid, am.auth_name
|
||||
FROM authority_master am
|
||||
JOIN authority_sub_user asu ON am.objid = asu.master_objid
|
||||
WHERE asu.user_id = $1
|
||||
AND am.status = 'active'
|
||||
`,
|
||||
[userId]
|
||||
);
|
||||
return [];
|
||||
|
||||
logger.info(
|
||||
`✅ 사용자 ${userId}가 속한 권한 그룹: ${userRoleGroups.length}개`,
|
||||
{
|
||||
roleGroups: userRoleGroups.map((rg: any) => rg.auth_name),
|
||||
}
|
||||
);
|
||||
|
||||
if (userRoleGroups.length > 0) {
|
||||
// 권한 그룹이 있는 경우: read_yn = 'Y'인 메뉴만 필터링
|
||||
const roleObjids = userRoleGroups.map((rg: any) => rg.role_objid);
|
||||
authFilter = `
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM rel_menu_auth rma
|
||||
WHERE rma.menu_objid = MENU.OBJID
|
||||
AND rma.auth_objid = ANY($${paramIndex})
|
||||
AND rma.read_yn = 'Y'
|
||||
)
|
||||
`;
|
||||
unionFilter = `
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM rel_menu_auth rma
|
||||
WHERE rma.menu_objid = MENU_SUB.OBJID
|
||||
AND rma.auth_objid = ANY($${paramIndex})
|
||||
AND rma.read_yn = 'Y'
|
||||
)
|
||||
`;
|
||||
queryParams.push(roleObjids);
|
||||
paramIndex++;
|
||||
logger.info(
|
||||
`✅ 권한 그룹 기반 메뉴 필터링 적용: ${roleObjids.length}개 그룹`
|
||||
);
|
||||
} else {
|
||||
// 권한 그룹이 없는 경우: 메뉴 없음
|
||||
logger.warn(
|
||||
`⚠️ 사용자 ${userId}는 권한 그룹이 없어 메뉴가 표시되지 않습니다.`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 회사별 필터링 조건 생성
|
||||
// 2. 회사별 필터링 조건 생성
|
||||
let companyFilter = "";
|
||||
|
||||
if (userType === "SUPER_ADMIN" && userCompanyCode === "*") {
|
||||
// SUPER_ADMIN: 공통 메뉴만 (company_code = '*')
|
||||
logger.info("✅ 좌측 사이드바 (SUPER_ADMIN): 공통 메뉴만 표시");
|
||||
companyFilter = `AND MENU.COMPANY_CODE = '*'`;
|
||||
} else {
|
||||
// COMPANY_ADMIN/USER: 자기 회사 메뉴만
|
||||
logger.info(
|
||||
`✅ 좌측 사이드바 (COMPANY_ADMIN): 회사 ${userCompanyCode} 메뉴만 표시`
|
||||
`✅ 좌측 사이드바 (COMPANY_ADMIN/USER): 회사 ${userCompanyCode} 메뉴만 표시`
|
||||
);
|
||||
companyFilter = `AND MENU.COMPANY_CODE = $${paramIndex}`;
|
||||
queryParams.push(userCompanyCode);
|
||||
@@ -480,7 +515,7 @@ export class AdminService {
|
||||
FROM MENU_INFO MENU_SUB
|
||||
JOIN V_MENU ON MENU_SUB.PARENT_OBJ_ID = V_MENU.OBJID
|
||||
WHERE MENU_SUB.STATUS = 'active'
|
||||
${authFilter.replace(/MENU\.OBJID/g, "MENU_SUB.OBJID")}
|
||||
${unionFilter}
|
||||
)
|
||||
SELECT
|
||||
LEVEL AS LEV,
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface CodeCategory {
|
||||
description?: string | null;
|
||||
sort_order: number;
|
||||
is_active: string;
|
||||
company_code: string; // 추가
|
||||
created_date?: Date | null;
|
||||
created_by?: string | null;
|
||||
updated_date?: Date | null;
|
||||
@@ -22,6 +23,7 @@ export interface CodeInfo {
|
||||
description?: string | null;
|
||||
sort_order: number;
|
||||
is_active: string;
|
||||
company_code: string; // 추가
|
||||
created_date?: Date | null;
|
||||
created_by?: string | null;
|
||||
updated_date?: Date | null;
|
||||
@@ -64,7 +66,7 @@ export class CommonCodeService {
|
||||
/**
|
||||
* 카테고리 목록 조회
|
||||
*/
|
||||
async getCategories(params: GetCategoriesParams) {
|
||||
async getCategories(params: GetCategoriesParams, userCompanyCode?: string) {
|
||||
try {
|
||||
const { search, isActive, page = 1, size = 20 } = params;
|
||||
|
||||
@@ -72,6 +74,17 @@ export class CommonCodeService {
|
||||
const values: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 회사별 필터링 (최고 관리자가 아닌 경우)
|
||||
if (userCompanyCode && userCompanyCode !== "*") {
|
||||
whereConditions.push(`company_code = $${paramIndex}`);
|
||||
values.push(userCompanyCode);
|
||||
paramIndex++;
|
||||
logger.info(`회사별 코드 카테고리 필터링: ${userCompanyCode}`);
|
||||
} else if (userCompanyCode === "*") {
|
||||
// 최고 관리자는 모든 데이터 조회 가능
|
||||
logger.info(`최고 관리자: 모든 코드 카테고리 조회`);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
whereConditions.push(
|
||||
`(category_name ILIKE $${paramIndex} OR category_code ILIKE $${paramIndex})`
|
||||
@@ -110,7 +123,7 @@ export class CommonCodeService {
|
||||
const total = parseInt(countResult?.count || "0");
|
||||
|
||||
logger.info(
|
||||
`카테고리 조회 완료: ${categories.length}개, 전체: ${total}개`
|
||||
`카테고리 조회 완료: ${categories.length}개, 전체: ${total}개 (회사: ${userCompanyCode || "전체"})`
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -126,7 +139,11 @@ export class CommonCodeService {
|
||||
/**
|
||||
* 카테고리별 코드 목록 조회
|
||||
*/
|
||||
async getCodes(categoryCode: string, params: GetCodesParams) {
|
||||
async getCodes(
|
||||
categoryCode: string,
|
||||
params: GetCodesParams,
|
||||
userCompanyCode?: string
|
||||
) {
|
||||
try {
|
||||
const { search, isActive, page = 1, size = 20 } = params;
|
||||
|
||||
@@ -134,6 +151,16 @@ export class CommonCodeService {
|
||||
const values: any[] = [categoryCode];
|
||||
let paramIndex = 2;
|
||||
|
||||
// 회사별 필터링 (최고 관리자가 아닌 경우)
|
||||
if (userCompanyCode && userCompanyCode !== "*") {
|
||||
whereConditions.push(`company_code = $${paramIndex}`);
|
||||
values.push(userCompanyCode);
|
||||
paramIndex++;
|
||||
logger.info(`회사별 코드 필터링: ${userCompanyCode}`);
|
||||
} else if (userCompanyCode === "*") {
|
||||
logger.info(`최고 관리자: 모든 코드 조회`);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
whereConditions.push(
|
||||
`(code_name ILIKE $${paramIndex} OR code_value ILIKE $${paramIndex})`
|
||||
@@ -169,7 +196,7 @@ export class CommonCodeService {
|
||||
const total = parseInt(countResult?.count || "0");
|
||||
|
||||
logger.info(
|
||||
`코드 조회 완료: ${categoryCode} - ${codes.length}개, 전체: ${total}개`
|
||||
`코드 조회 완료: ${categoryCode} - ${codes.length}개, 전체: ${total}개 (회사: ${userCompanyCode || "전체"})`
|
||||
);
|
||||
|
||||
return { data: codes, total };
|
||||
@@ -182,13 +209,17 @@ export class CommonCodeService {
|
||||
/**
|
||||
* 카테고리 생성
|
||||
*/
|
||||
async createCategory(data: CreateCategoryData, createdBy: string) {
|
||||
async createCategory(
|
||||
data: CreateCategoryData,
|
||||
createdBy: string,
|
||||
companyCode: string
|
||||
) {
|
||||
try {
|
||||
const category = await queryOne<CodeCategory>(
|
||||
`INSERT INTO code_category
|
||||
(category_code, category_name, category_name_eng, description, sort_order,
|
||||
is_active, created_by, updated_by, created_date, updated_date)
|
||||
VALUES ($1, $2, $3, $4, $5, 'Y', $6, $7, NOW(), NOW())
|
||||
is_active, company_code, created_by, updated_by, created_date, updated_date)
|
||||
VALUES ($1, $2, $3, $4, $5, 'Y', $6, $7, $8, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
[
|
||||
data.categoryCode,
|
||||
@@ -196,12 +227,15 @@ export class CommonCodeService {
|
||||
data.categoryNameEng || null,
|
||||
data.description || null,
|
||||
data.sortOrder || 0,
|
||||
companyCode,
|
||||
createdBy,
|
||||
createdBy,
|
||||
]
|
||||
);
|
||||
|
||||
logger.info(`카테고리 생성 완료: ${data.categoryCode}`);
|
||||
logger.info(
|
||||
`카테고리 생성 완료: ${data.categoryCode} (회사: ${companyCode})`
|
||||
);
|
||||
return category;
|
||||
} catch (error) {
|
||||
logger.error("카테고리 생성 중 오류:", error);
|
||||
@@ -215,11 +249,12 @@ export class CommonCodeService {
|
||||
async updateCategory(
|
||||
categoryCode: string,
|
||||
data: Partial<CreateCategoryData>,
|
||||
updatedBy: string
|
||||
updatedBy: string,
|
||||
companyCode?: string
|
||||
) {
|
||||
try {
|
||||
// 디버깅: 받은 데이터 로그
|
||||
logger.info(`카테고리 수정 데이터:`, { categoryCode, data });
|
||||
logger.info(`카테고리 수정 데이터:`, { categoryCode, data, companyCode });
|
||||
|
||||
// 동적 UPDATE 쿼리 생성
|
||||
const updateFields: string[] = [
|
||||
@@ -256,15 +291,28 @@ export class CommonCodeService {
|
||||
values.push(activeValue);
|
||||
}
|
||||
|
||||
// WHERE 절 구성
|
||||
let whereClause = `WHERE category_code = $${paramIndex}`;
|
||||
values.push(categoryCode);
|
||||
|
||||
// 회사 필터링 (최고 관리자가 아닌 경우)
|
||||
if (companyCode && companyCode !== "*") {
|
||||
paramIndex++;
|
||||
whereClause += ` AND company_code = $${paramIndex}`;
|
||||
values.push(companyCode);
|
||||
}
|
||||
|
||||
const category = await queryOne<CodeCategory>(
|
||||
`UPDATE code_category
|
||||
SET ${updateFields.join(", ")}
|
||||
WHERE category_code = $${paramIndex}
|
||||
${whereClause}
|
||||
RETURNING *`,
|
||||
[...values, categoryCode]
|
||||
values
|
||||
);
|
||||
|
||||
logger.info(`카테고리 수정 완료: ${categoryCode}`);
|
||||
logger.info(
|
||||
`카테고리 수정 완료: ${categoryCode} (회사: ${companyCode || "전체"})`
|
||||
);
|
||||
return category;
|
||||
} catch (error) {
|
||||
logger.error(`카테고리 수정 중 오류 (${categoryCode}):`, error);
|
||||
@@ -275,13 +323,22 @@ export class CommonCodeService {
|
||||
/**
|
||||
* 카테고리 삭제
|
||||
*/
|
||||
async deleteCategory(categoryCode: string) {
|
||||
async deleteCategory(categoryCode: string, companyCode?: string) {
|
||||
try {
|
||||
await query(`DELETE FROM code_category WHERE category_code = $1`, [
|
||||
categoryCode,
|
||||
]);
|
||||
let sql = `DELETE FROM code_category WHERE category_code = $1`;
|
||||
const values: any[] = [categoryCode];
|
||||
|
||||
logger.info(`카테고리 삭제 완료: ${categoryCode}`);
|
||||
// 회사 필터링 (최고 관리자가 아닌 경우)
|
||||
if (companyCode && companyCode !== "*") {
|
||||
sql += ` AND company_code = $2`;
|
||||
values.push(companyCode);
|
||||
}
|
||||
|
||||
await query(sql, values);
|
||||
|
||||
logger.info(
|
||||
`카테고리 삭제 완료: ${categoryCode} (회사: ${companyCode || "전체"})`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`카테고리 삭제 중 오류 (${categoryCode}):`, error);
|
||||
throw error;
|
||||
@@ -294,14 +351,15 @@ export class CommonCodeService {
|
||||
async createCode(
|
||||
categoryCode: string,
|
||||
data: CreateCodeData,
|
||||
createdBy: string
|
||||
createdBy: string,
|
||||
companyCode: string
|
||||
) {
|
||||
try {
|
||||
const code = await queryOne<CodeInfo>(
|
||||
`INSERT INTO code_info
|
||||
(code_category, code_value, code_name, code_name_eng, description, sort_order,
|
||||
is_active, created_by, updated_by, created_date, updated_date)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, NOW(), NOW())
|
||||
is_active, company_code, created_by, updated_by, created_date, updated_date)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, $9, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
[
|
||||
categoryCode,
|
||||
@@ -310,12 +368,15 @@ export class CommonCodeService {
|
||||
data.codeNameEng || null,
|
||||
data.description || null,
|
||||
data.sortOrder || 0,
|
||||
companyCode,
|
||||
createdBy,
|
||||
createdBy,
|
||||
]
|
||||
);
|
||||
|
||||
logger.info(`코드 생성 완료: ${categoryCode}.${data.codeValue}`);
|
||||
logger.info(
|
||||
`코드 생성 완료: ${categoryCode}.${data.codeValue} (회사: ${companyCode})`
|
||||
);
|
||||
return code;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
@@ -333,11 +394,17 @@ export class CommonCodeService {
|
||||
categoryCode: string,
|
||||
codeValue: string,
|
||||
data: Partial<CreateCodeData>,
|
||||
updatedBy: string
|
||||
updatedBy: string,
|
||||
companyCode?: string
|
||||
) {
|
||||
try {
|
||||
// 디버깅: 받은 데이터 로그
|
||||
logger.info(`코드 수정 데이터:`, { categoryCode, codeValue, data });
|
||||
logger.info(`코드 수정 데이터:`, {
|
||||
categoryCode,
|
||||
codeValue,
|
||||
data,
|
||||
companyCode,
|
||||
});
|
||||
|
||||
// 동적 UPDATE 쿼리 생성
|
||||
const updateFields: string[] = [
|
||||
@@ -374,15 +441,28 @@ export class CommonCodeService {
|
||||
values.push(activeValue);
|
||||
}
|
||||
|
||||
// WHERE 절 구성
|
||||
let whereClause = `WHERE code_category = $${paramIndex++} AND code_value = $${paramIndex}`;
|
||||
values.push(categoryCode, codeValue);
|
||||
|
||||
// 회사 필터링 (최고 관리자가 아닌 경우)
|
||||
if (companyCode && companyCode !== "*") {
|
||||
paramIndex++;
|
||||
whereClause += ` AND company_code = $${paramIndex}`;
|
||||
values.push(companyCode);
|
||||
}
|
||||
|
||||
const code = await queryOne<CodeInfo>(
|
||||
`UPDATE code_info
|
||||
SET ${updateFields.join(", ")}
|
||||
WHERE code_category = $${paramIndex++} AND code_value = $${paramIndex}
|
||||
${whereClause}
|
||||
RETURNING *`,
|
||||
[...values, categoryCode, codeValue]
|
||||
values
|
||||
);
|
||||
|
||||
logger.info(`코드 수정 완료: ${categoryCode}.${codeValue}`);
|
||||
logger.info(
|
||||
`코드 수정 완료: ${categoryCode}.${codeValue} (회사: ${companyCode || "전체"})`
|
||||
);
|
||||
return code;
|
||||
} catch (error) {
|
||||
logger.error(`코드 수정 중 오류 (${categoryCode}.${codeValue}):`, error);
|
||||
@@ -393,14 +473,26 @@ export class CommonCodeService {
|
||||
/**
|
||||
* 코드 삭제
|
||||
*/
|
||||
async deleteCode(categoryCode: string, codeValue: string) {
|
||||
async deleteCode(
|
||||
categoryCode: string,
|
||||
codeValue: string,
|
||||
companyCode?: string
|
||||
) {
|
||||
try {
|
||||
await query(
|
||||
`DELETE FROM code_info WHERE code_category = $1 AND code_value = $2`,
|
||||
[categoryCode, codeValue]
|
||||
);
|
||||
let sql = `DELETE FROM code_info WHERE code_category = $1 AND code_value = $2`;
|
||||
const values: any[] = [categoryCode, codeValue];
|
||||
|
||||
logger.info(`코드 삭제 완료: ${categoryCode}.${codeValue}`);
|
||||
// 회사 필터링 (최고 관리자가 아닌 경우)
|
||||
if (companyCode && companyCode !== "*") {
|
||||
sql += ` AND company_code = $3`;
|
||||
values.push(companyCode);
|
||||
}
|
||||
|
||||
await query(sql, values);
|
||||
|
||||
logger.info(
|
||||
`코드 삭제 완료: ${categoryCode}.${codeValue} (회사: ${companyCode || "전체"})`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`코드 삭제 중 오류 (${categoryCode}.${codeValue}):`, error);
|
||||
throw error;
|
||||
@@ -410,20 +502,30 @@ export class CommonCodeService {
|
||||
/**
|
||||
* 카테고리별 옵션 조회 (화면관리용)
|
||||
*/
|
||||
async getCodeOptions(categoryCode: string) {
|
||||
async getCodeOptions(categoryCode: string, userCompanyCode?: string) {
|
||||
try {
|
||||
let sql = `SELECT code_value, code_name, code_name_eng, sort_order
|
||||
FROM code_info
|
||||
WHERE code_category = $1 AND is_active = 'Y'`;
|
||||
const values: any[] = [categoryCode];
|
||||
|
||||
// 회사별 필터링 (최고 관리자가 아닌 경우)
|
||||
if (userCompanyCode && userCompanyCode !== "*") {
|
||||
sql += ` AND company_code = $2`;
|
||||
values.push(userCompanyCode);
|
||||
logger.info(`회사별 코드 옵션 필터링: ${userCompanyCode}`);
|
||||
} else if (userCompanyCode === "*") {
|
||||
logger.info(`최고 관리자: 모든 코드 옵션 조회`);
|
||||
}
|
||||
|
||||
sql += ` ORDER BY sort_order ASC, code_value ASC`;
|
||||
|
||||
const codes = await query<{
|
||||
code_value: string;
|
||||
code_name: string;
|
||||
code_name_eng: string | null;
|
||||
sort_order: number;
|
||||
}>(
|
||||
`SELECT code_value, code_name, code_name_eng, sort_order
|
||||
FROM code_info
|
||||
WHERE code_category = $1 AND is_active = 'Y'
|
||||
ORDER BY sort_order ASC, code_value ASC`,
|
||||
[categoryCode]
|
||||
);
|
||||
}>(sql, values);
|
||||
|
||||
const options = codes.map((code) => ({
|
||||
value: code.code_value,
|
||||
@@ -431,7 +533,9 @@ export class CommonCodeService {
|
||||
labelEng: code.code_name_eng,
|
||||
}));
|
||||
|
||||
logger.info(`코드 옵션 조회 완료: ${categoryCode} - ${options.length}개`);
|
||||
logger.info(
|
||||
`코드 옵션 조회 완료: ${categoryCode} - ${options.length}개 (회사: ${userCompanyCode || "전체"})`
|
||||
);
|
||||
return options;
|
||||
} catch (error) {
|
||||
logger.error(`코드 옵션 조회 중 오류 (${categoryCode}):`, error);
|
||||
|
||||
@@ -759,6 +759,124 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 삭제 (DROP TABLE)
|
||||
*/
|
||||
async dropTable(
|
||||
tableName: string,
|
||||
userCompanyCode: string,
|
||||
userId: string
|
||||
): Promise<DDLExecutionResult> {
|
||||
// DDL 실행 시작 로그
|
||||
await DDLAuditLogger.logDDLStart(
|
||||
userId,
|
||||
userCompanyCode,
|
||||
"DROP_TABLE",
|
||||
tableName,
|
||||
{}
|
||||
);
|
||||
|
||||
try {
|
||||
// 1. 권한 검증 (최고 관리자만 가능)
|
||||
this.validateSuperAdminPermission(userCompanyCode);
|
||||
|
||||
// 2. 테이블 존재 여부 확인
|
||||
const tableExists = await this.checkTableExists(tableName);
|
||||
if (!tableExists) {
|
||||
const errorMessage = `테이블 '${tableName}'이 존재하지 않습니다.`;
|
||||
|
||||
await DDLAuditLogger.logDDLExecution(
|
||||
userId,
|
||||
userCompanyCode,
|
||||
"DROP_TABLE",
|
||||
tableName,
|
||||
"TABLE_NOT_FOUND",
|
||||
false,
|
||||
errorMessage
|
||||
);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
error: {
|
||||
code: "TABLE_NOT_FOUND",
|
||||
details: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 3. DDL 쿼리 생성
|
||||
const ddlQuery = `DROP TABLE IF EXISTS "${tableName}" CASCADE`;
|
||||
|
||||
// 4. 트랜잭션으로 안전하게 실행
|
||||
await transaction(async (client) => {
|
||||
// 4-1. 테이블 삭제
|
||||
await client.query(ddlQuery);
|
||||
|
||||
// 4-2. 관련 메타데이터 삭제
|
||||
await client.query(`DELETE FROM column_labels WHERE table_name = $1`, [
|
||||
tableName,
|
||||
]);
|
||||
await client.query(`DELETE FROM table_labels WHERE table_name = $1`, [
|
||||
tableName,
|
||||
]);
|
||||
});
|
||||
|
||||
// 5. 성공 로그 기록
|
||||
await DDLAuditLogger.logDDLExecution(
|
||||
userId,
|
||||
userCompanyCode,
|
||||
"DROP_TABLE",
|
||||
tableName,
|
||||
ddlQuery,
|
||||
true
|
||||
);
|
||||
|
||||
logger.info("테이블 삭제 성공", {
|
||||
tableName,
|
||||
userId,
|
||||
});
|
||||
|
||||
// 테이블 삭제 후 관련 캐시 무효화
|
||||
this.invalidateTableCache(tableName);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `테이블 '${tableName}'이 성공적으로 삭제되었습니다.`,
|
||||
executedQuery: ddlQuery,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = `테이블 삭제 실패: ${(error as Error).message}`;
|
||||
|
||||
// 실패 로그 기록
|
||||
await DDLAuditLogger.logDDLExecution(
|
||||
userId,
|
||||
userCompanyCode,
|
||||
"DROP_TABLE",
|
||||
tableName,
|
||||
`FAILED: ${(error as Error).message}`,
|
||||
false,
|
||||
errorMessage
|
||||
);
|
||||
|
||||
logger.error("테이블 삭제 실패:", {
|
||||
tableName,
|
||||
userId,
|
||||
error: (error as Error).message,
|
||||
stack: (error as Error).stack,
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
error: {
|
||||
code: "EXECUTION_FAILED",
|
||||
details: (error as Error).message,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 관련 캐시 무효화
|
||||
* DDL 작업 후 호출하여 캐시된 데이터를 클리어
|
||||
|
||||
Reference in New Issue
Block a user