; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
This commit is contained in:
leeheejin
2025-10-28 10:08:40 +09:00
13 changed files with 1273 additions and 370 deletions

View File

@@ -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,

View File

@@ -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 로그 정리
*/

View File

@@ -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",
});
}
};
/**
* 플로우의 모든 단계별 카운트 조회
*/

View File

@@ -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",
},

View File

@@ -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);
// ==================== 데이터 이동 ====================

View File

@@ -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,

View File

@@ -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);

View File

@@ -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 작업 후 호출하여 캐시된 데이터를 클리어