From c333a9fd9d8dd90bf92105ad7c570b1441e34c6f Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 28 Oct 2025 11:54:44 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=B5=ED=86=B5=EC=BD=94=EB=93=9C,REST=20API?= =?UTF-8?q?=20=ED=9A=8C=EC=82=AC=EB=B3=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/commonCodeController.ts | 12 ++- .../src/routes/externalDbConnectionRoutes.ts | 17 +++- .../routes/externalRestApiConnectionRoutes.ts | 29 +++++-- .../src/services/commonCodeService.ts | 30 ++++++- .../services/externalDbConnectionService.ts | 86 +++++++++++++++---- .../externalRestApiConnectionService.ts | 75 ++++++++++++---- 6 files changed, 195 insertions(+), 54 deletions(-) diff --git a/backend-node/src/controllers/commonCodeController.ts b/backend-node/src/controllers/commonCodeController.ts index f31e55e1..616e0c6c 100644 --- a/backend-node/src/controllers/commonCodeController.ts +++ b/backend-node/src/controllers/commonCodeController.ts @@ -461,12 +461,13 @@ export class CommonCodeController { } /** - * 카테고리 중복 검사 + * 카테고리 중복 검사 (회사별) * GET /api/common-codes/categories/check-duplicate?field=categoryCode&value=USER_STATUS&excludeCode=OLD_CODE */ async checkCategoryDuplicate(req: AuthenticatedRequest, res: Response) { try { const { field, value, excludeCode } = req.query; + const userCompanyCode = req.user?.companyCode; // 입력값 검증 if (!field || !value) { @@ -488,7 +489,8 @@ export class CommonCodeController { const result = await this.commonCodeService.checkCategoryDuplicate( field as "categoryCode" | "categoryName" | "categoryNameEng", value as string, - excludeCode as string + excludeCode as string, + userCompanyCode ); return res.json({ @@ -511,13 +513,14 @@ export class CommonCodeController { } /** - * 코드 중복 검사 + * 코드 중복 검사 (회사별) * GET /api/common-codes/categories/:categoryCode/codes/check-duplicate?field=codeValue&value=ACTIVE&excludeCode=OLD_CODE */ async checkCodeDuplicate(req: AuthenticatedRequest, res: Response) { try { const { categoryCode } = req.params; const { field, value, excludeCode } = req.query; + const userCompanyCode = req.user?.companyCode; // 입력값 검증 if (!field || !value) { @@ -540,7 +543,8 @@ export class CommonCodeController { categoryCode, field as "codeValue" | "codeName" | "codeNameEng", value as string, - excludeCode as string + excludeCode as string, + userCompanyCode ); return res.json({ diff --git a/backend-node/src/routes/externalDbConnectionRoutes.ts b/backend-node/src/routes/externalDbConnectionRoutes.ts index 5ad87dab..c116a74d 100644 --- a/backend-node/src/routes/externalDbConnectionRoutes.ts +++ b/backend-node/src/routes/externalDbConnectionRoutes.ts @@ -87,7 +87,10 @@ router.get( filter, }); - const result = await ExternalDbConnectionService.getConnections(filter); + const result = await ExternalDbConnectionService.getConnections( + filter, + userCompanyCode + ); if (result.success) { return res.status(200).json(result); @@ -319,7 +322,12 @@ router.delete( }); } - const result = await ExternalDbConnectionService.deleteConnection(id); + const userCompanyCode = req.user?.companyCode; + + const result = await ExternalDbConnectionService.deleteConnection( + id, + userCompanyCode + ); if (result.success) { return res.status(200).json(result); @@ -517,7 +525,10 @@ router.get( }); const externalConnections = - await ExternalDbConnectionService.getConnections(filter); + await ExternalDbConnectionService.getConnections( + filter, + userCompanyCode + ); if (!externalConnections.success) { return res.status(400).json(externalConnections); diff --git a/backend-node/src/routes/externalRestApiConnectionRoutes.ts b/backend-node/src/routes/externalRestApiConnectionRoutes.ts index 0e2de684..9f577e52 100644 --- a/backend-node/src/routes/externalRestApiConnectionRoutes.ts +++ b/backend-node/src/routes/externalRestApiConnectionRoutes.ts @@ -29,8 +29,12 @@ router.get( company_code: req.query.company_code as string, }; - const result = - await ExternalRestApiConnectionService.getConnections(filter); + const userCompanyCode = req.user?.companyCode; + + const result = await ExternalRestApiConnectionService.getConnections( + filter, + userCompanyCode + ); return res.status(result.success ? 200 : 400).json(result); } catch (error) { @@ -62,8 +66,12 @@ router.get( }); } - const result = - await ExternalRestApiConnectionService.getConnectionById(id); + const userCompanyCode = req.user?.companyCode; + + const result = await ExternalRestApiConnectionService.getConnectionById( + id, + userCompanyCode + ); return res.status(result.success ? 200 : 404).json(result); } catch (error) { @@ -129,9 +137,12 @@ router.put( updated_by: req.user?.userId || "system", }; + const userCompanyCode = req.user?.companyCode; + const result = await ExternalRestApiConnectionService.updateConnection( id, - data + data, + userCompanyCode ); return res.status(result.success ? 200 : 400).json(result); @@ -164,8 +175,12 @@ router.delete( }); } - const result = - await ExternalRestApiConnectionService.deleteConnection(id); + const userCompanyCode = req.user?.companyCode; + + const result = await ExternalRestApiConnectionService.deleteConnection( + id, + userCompanyCode + ); return res.status(result.success ? 200 : 404).json(result); } catch (error) { diff --git a/backend-node/src/services/commonCodeService.ts b/backend-node/src/services/commonCodeService.ts index a823532d..8c02a60d 100644 --- a/backend-node/src/services/commonCodeService.ts +++ b/backend-node/src/services/commonCodeService.ts @@ -604,12 +604,13 @@ export class CommonCodeService { } /** - * 카테고리 중복 검사 + * 카테고리 중복 검사 (회사별) */ async checkCategoryDuplicate( field: "categoryCode" | "categoryName" | "categoryNameEng", value: string, - excludeCategoryCode?: string + excludeCategoryCode?: string, + userCompanyCode?: string ): Promise<{ isDuplicate: boolean; message: string }> { try { if (!value || !value.trim()) { @@ -655,6 +656,12 @@ export class CommonCodeService { break; } + // 회사별 필터링 (최고 관리자가 아닌 경우) + if (userCompanyCode && userCompanyCode !== "*") { + sql += ` AND company_code = $${paramIndex++}`; + values.push(userCompanyCode); + } + // 수정 시 자기 자신 제외 if (excludeCategoryCode) { sql += ` AND category_code != $${paramIndex++}`; @@ -675,6 +682,10 @@ export class CommonCodeService { categoryNameEng: "카테고리 영문명", }; + logger.info( + `카테고리 중복 검사: ${field}=${value}, 회사=${userCompanyCode}, 중복=${isDuplicate}` + ); + return { isDuplicate, message: isDuplicate @@ -688,13 +699,14 @@ export class CommonCodeService { } /** - * 코드 중복 검사 + * 코드 중복 검사 (회사별) */ async checkCodeDuplicate( categoryCode: string, field: "codeValue" | "codeName" | "codeNameEng", value: string, - excludeCodeValue?: string + excludeCodeValue?: string, + userCompanyCode?: string ): Promise<{ isDuplicate: boolean; message: string }> { try { if (!value || !value.trim()) { @@ -743,6 +755,12 @@ export class CommonCodeService { break; } + // 회사별 필터링 (최고 관리자가 아닌 경우) + if (userCompanyCode && userCompanyCode !== "*") { + sql += ` AND company_code = $${paramIndex++}`; + values.push(userCompanyCode); + } + // 수정 시 자기 자신 제외 if (excludeCodeValue) { sql += ` AND code_value != $${paramIndex++}`; @@ -760,6 +778,10 @@ export class CommonCodeService { codeNameEng: "코드 영문명", }; + logger.info( + `코드 중복 검사: ${categoryCode}.${field}=${value}, 회사=${userCompanyCode}, 중복=${isDuplicate}` + ); + return { isDuplicate, message: isDuplicate diff --git a/backend-node/src/services/externalDbConnectionService.ts b/backend-node/src/services/externalDbConnectionService.ts index d25aa64b..99164ae1 100644 --- a/backend-node/src/services/externalDbConnectionService.ts +++ b/backend-node/src/services/externalDbConnectionService.ts @@ -17,7 +17,8 @@ export class ExternalDbConnectionService { * 외부 DB 연결 목록 조회 */ static async getConnections( - filter: ExternalDbConnectionFilter + filter: ExternalDbConnectionFilter, + userCompanyCode?: string ): Promise> { try { // WHERE 조건 동적 생성 @@ -25,6 +26,26 @@ export class ExternalDbConnectionService { const params: any[] = []; let paramIndex = 1; + // 회사별 필터링 (최고 관리자가 아닌 경우 필수) + if (userCompanyCode && userCompanyCode !== "*") { + whereConditions.push(`company_code = $${paramIndex++}`); + params.push(userCompanyCode); + logger.info(`회사별 외부 DB 연결 필터링: ${userCompanyCode}`); + } else if (userCompanyCode === "*") { + logger.info(`최고 관리자: 모든 외부 DB 연결 조회`); + // 필터가 있으면 적용 + if (filter.company_code) { + whereConditions.push(`company_code = $${paramIndex++}`); + params.push(filter.company_code); + } + } else { + // userCompanyCode가 없는 경우 (하위 호환성) + if (filter.company_code) { + whereConditions.push(`company_code = $${paramIndex++}`); + params.push(filter.company_code); + } + } + // 필터 조건 적용 if (filter.db_type) { whereConditions.push(`db_type = $${paramIndex++}`); @@ -36,11 +57,6 @@ export class ExternalDbConnectionService { params.push(filter.is_active); } - if (filter.company_code) { - whereConditions.push(`company_code = $${paramIndex++}`); - params.push(filter.company_code); - } - // 검색 조건 적용 (연결명 또는 설명에서 검색) if (filter.search && filter.search.trim()) { whereConditions.push( @@ -496,23 +512,36 @@ export class ExternalDbConnectionService { /** * 외부 DB 연결 삭제 (물리 삭제) */ - static async deleteConnection(id: number): Promise> { + static async deleteConnection( + id: number, + userCompanyCode?: string + ): Promise> { try { - const existingConnection = await queryOne( - `SELECT id FROM external_db_connections WHERE id = $1`, - [id] - ); + let selectQuery = `SELECT id FROM external_db_connections WHERE id = $1`; + const selectParams: any[] = [id]; + + // 회사별 필터링 (최고 관리자가 아닌 경우) + if (userCompanyCode && userCompanyCode !== "*") { + selectQuery += ` AND company_code = $2`; + selectParams.push(userCompanyCode); + } + + const existingConnection = await queryOne(selectQuery, selectParams); if (!existingConnection) { return { success: false, - message: "해당 연결 설정을 찾을 수 없습니다.", + message: "해당 연결 설정을 찾을 수 없거나 권한이 없습니다.", }; } // 물리 삭제 (실제 데이터 삭제) await query(`DELETE FROM external_db_connections WHERE id = $1`, [id]); + logger.info( + `외부 DB 연결 삭제: ID ${id} (회사: ${userCompanyCode || "전체"})` + ); + return { success: true, message: "연결 설정이 삭제되었습니다.", @@ -747,8 +776,11 @@ export class ExternalDbConnectionService { try { // 보안 검증: SELECT 쿼리만 허용 const trimmedQuery = query.trim().toUpperCase(); - if (!trimmedQuery.startsWith('SELECT')) { - console.log("보안 오류: SELECT가 아닌 쿼리 시도:", { id, query: query.substring(0, 100) }); + if (!trimmedQuery.startsWith("SELECT")) { + console.log("보안 오류: SELECT가 아닌 쿼리 시도:", { + id, + query: query.substring(0, 100), + }); return { success: false, message: "외부 데이터베이스에서는 SELECT 쿼리만 실행할 수 있습니다.", @@ -756,16 +788,32 @@ export class ExternalDbConnectionService { } // 위험한 키워드 검사 - const dangerousKeywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER', 'TRUNCATE', 'EXEC', 'EXECUTE', 'CALL', 'MERGE']; - const hasDangerousKeyword = dangerousKeywords.some(keyword => + const dangerousKeywords = [ + "INSERT", + "UPDATE", + "DELETE", + "DROP", + "CREATE", + "ALTER", + "TRUNCATE", + "EXEC", + "EXECUTE", + "CALL", + "MERGE", + ]; + const hasDangerousKeyword = dangerousKeywords.some((keyword) => trimmedQuery.includes(keyword) ); - + if (hasDangerousKeyword) { - console.log("보안 오류: 위험한 키워드 포함 쿼리 시도:", { id, query: query.substring(0, 100) }); + console.log("보안 오류: 위험한 키워드 포함 쿼리 시도:", { + id, + query: query.substring(0, 100), + }); return { success: false, - message: "데이터를 변경하거나 삭제하는 쿼리는 허용되지 않습니다. SELECT 쿼리만 사용해주세요.", + message: + "데이터를 변경하거나 삭제하는 쿼리는 허용되지 않습니다. SELECT 쿼리만 사용해주세요.", }; } diff --git a/backend-node/src/services/externalRestApiConnectionService.ts b/backend-node/src/services/externalRestApiConnectionService.ts index 4d0539b4..e5189530 100644 --- a/backend-node/src/services/externalRestApiConnectionService.ts +++ b/backend-node/src/services/externalRestApiConnectionService.ts @@ -23,7 +23,8 @@ export class ExternalRestApiConnectionService { * REST API 연결 목록 조회 */ static async getConnections( - filter: ExternalRestApiConnectionFilter = {} + filter: ExternalRestApiConnectionFilter = {}, + userCompanyCode?: string ): Promise> { try { let query = ` @@ -39,11 +40,27 @@ export class ExternalRestApiConnectionService { const params: any[] = []; let paramIndex = 1; - // 회사 코드 필터 - if (filter.company_code) { + // 회사별 필터링 (최고 관리자가 아닌 경우 필수) + if (userCompanyCode && userCompanyCode !== "*") { query += ` AND company_code = $${paramIndex}`; - params.push(filter.company_code); + params.push(userCompanyCode); paramIndex++; + logger.info(`회사별 REST API 연결 필터링: ${userCompanyCode}`); + } else if (userCompanyCode === "*") { + logger.info(`최고 관리자: 모든 REST API 연결 조회`); + // 필터가 있으면 적용 + if (filter.company_code) { + query += ` AND company_code = $${paramIndex}`; + params.push(filter.company_code); + paramIndex++; + } + } else { + // userCompanyCode가 없는 경우 (하위 호환성) + if (filter.company_code) { + query += ` AND company_code = $${paramIndex}`; + params.push(filter.company_code); + paramIndex++; + } } // 활성 상태 필터 @@ -105,10 +122,11 @@ export class ExternalRestApiConnectionService { * REST API 연결 상세 조회 */ static async getConnectionById( - id: number + id: number, + userCompanyCode?: string ): Promise> { try { - const query = ` + let query = ` SELECT id, connection_name, description, base_url, default_headers, auth_type, auth_config, timeout, retry_count, retry_delay, @@ -118,12 +136,20 @@ export class ExternalRestApiConnectionService { WHERE id = $1 `; - const result: QueryResult = await pool.query(query, [id]); + const params: any[] = [id]; + + // 회사별 필터링 (최고 관리자가 아닌 경우) + if (userCompanyCode && userCompanyCode !== "*") { + query += ` AND company_code = $2`; + params.push(userCompanyCode); + } + + const result: QueryResult = await pool.query(query, params); if (result.rows.length === 0) { return { success: false, - message: "연결을 찾을 수 없습니다.", + message: "연결을 찾을 수 없거나 권한이 없습니다.", }; } @@ -225,11 +251,12 @@ export class ExternalRestApiConnectionService { */ static async updateConnection( id: number, - data: Partial + data: Partial, + userCompanyCode?: string ): Promise> { try { - // 기존 연결 확인 - const existing = await this.getConnectionById(id); + // 기존 연결 확인 (회사 코드로 권한 체크) + const existing = await this.getConnectionById(id, userCompanyCode); if (!existing.success) { return existing; } @@ -353,24 +380,38 @@ export class ExternalRestApiConnectionService { /** * REST API 연결 삭제 */ - static async deleteConnection(id: number): Promise> { + static async deleteConnection( + id: number, + userCompanyCode?: string + ): Promise> { try { - const query = ` + let query = ` DELETE FROM external_rest_api_connections WHERE id = $1 - RETURNING connection_name `; - const result: QueryResult = await pool.query(query, [id]); + const params: any[] = [id]; + + // 회사별 필터링 (최고 관리자가 아닌 경우) + if (userCompanyCode && userCompanyCode !== "*") { + query += ` AND company_code = $2`; + params.push(userCompanyCode); + } + + query += ` RETURNING connection_name`; + + const result: QueryResult = await pool.query(query, params); if (result.rows.length === 0) { return { success: false, - message: "연결을 찾을 수 없습니다.", + message: "연결을 찾을 수 없거나 권한이 없습니다.", }; } - logger.info(`REST API 연결 삭제 성공: ${result.rows[0].connection_name}`); + logger.info( + `REST API 연결 삭제 성공: ${result.rows[0].connection_name} (회사: ${userCompanyCode || "전체"})` + ); return { success: true,