diff --git a/backend-node/src/controllers/batchController.ts b/backend-node/src/controllers/batchController.ts index 0858cc37..8a29e5bf 100644 --- a/backend-node/src/controllers/batchController.ts +++ b/backend-node/src/controllers/batchController.ts @@ -4,7 +4,11 @@ import { Request, Response } from "express"; import { BatchService } from "../services/batchService"; import { BatchSchedulerService } from "../services/batchSchedulerService"; -import { BatchConfigFilter, CreateBatchConfigRequest, UpdateBatchConfigRequest } from "../types/batchTypes"; +import { + BatchConfigFilter, + CreateBatchConfigRequest, + UpdateBatchConfigRequest, +} from "../types/batchTypes"; export interface AuthenticatedRequest extends Request { user?: { @@ -16,32 +20,36 @@ export interface AuthenticatedRequest extends Request { export class BatchController { /** - * 배치 설정 목록 조회 + * 배치 설정 목록 조회 (회사별) * GET /api/batch-configs */ static async getBatchConfigs(req: AuthenticatedRequest, res: Response) { try { const { page = 1, limit = 10, search, isActive } = req.query; - + const userCompanyCode = req.user?.companyCode; + const filter: BatchConfigFilter = { page: Number(page), limit: Number(limit), search: search as string, - is_active: isActive as string + is_active: isActive as string, }; - const result = await BatchService.getBatchConfigs(filter); - + const result = await BatchService.getBatchConfigs( + filter, + userCompanyCode + ); + res.json({ success: true, data: result.data, - pagination: result.pagination + pagination: result.pagination, }); } catch (error) { console.error("배치 설정 목록 조회 오류:", error); res.status(500).json({ success: false, - message: "배치 설정 목록 조회에 실패했습니다." + message: "배치 설정 목록 조회에 실패했습니다.", }); } } @@ -50,10 +58,13 @@ export class BatchController { * 사용 가능한 커넥션 목록 조회 * GET /api/batch-configs/connections */ - static async getAvailableConnections(req: AuthenticatedRequest, res: Response) { + static async getAvailableConnections( + req: AuthenticatedRequest, + res: Response + ) { try { const result = await BatchService.getAvailableConnections(); - + if (result.success) { res.json(result); } else { @@ -63,7 +74,7 @@ export class BatchController { console.error("커넥션 목록 조회 오류:", error); res.status(500).json({ success: false, - message: "커넥션 목록 조회에 실패했습니다." + message: "커넥션 목록 조회에 실패했습니다.", }); } } @@ -73,20 +84,26 @@ export class BatchController { * GET /api/batch-configs/connections/:type/tables * GET /api/batch-configs/connections/:type/:id/tables */ - static async getTablesFromConnection(req: AuthenticatedRequest, res: Response) { + static async getTablesFromConnection( + req: AuthenticatedRequest, + res: Response + ) { try { const { type, id } = req.params; - - if (!type || (type !== 'internal' && type !== 'external')) { + + if (!type || (type !== "internal" && type !== "external")) { return res.status(400).json({ success: false, - message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)" + message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)", }); } - const connectionId = type === 'external' ? Number(id) : undefined; - const result = await BatchService.getTablesFromConnection(type, connectionId); - + const connectionId = type === "external" ? Number(id) : undefined; + const result = await BatchService.getTablesFromConnection( + type, + connectionId + ); + if (result.success) { return res.json(result); } else { @@ -96,7 +113,7 @@ export class BatchController { console.error("테이블 목록 조회 오류:", error); return res.status(500).json({ success: false, - message: "테이블 목록 조회에 실패했습니다." + message: "테이블 목록 조회에 실패했습니다.", }); } } @@ -109,24 +126,28 @@ export class BatchController { static async getTableColumns(req: AuthenticatedRequest, res: Response) { try { const { type, id, tableName } = req.params; - + if (!type || !tableName) { return res.status(400).json({ success: false, - message: "연결 타입과 테이블명을 모두 지정해주세요." + message: "연결 타입과 테이블명을 모두 지정해주세요.", }); } - if (type !== 'internal' && type !== 'external') { + if (type !== "internal" && type !== "external") { return res.status(400).json({ success: false, - message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)" + message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)", }); } - const connectionId = type === 'external' ? Number(id) : undefined; - const result = await BatchService.getTableColumns(type, connectionId, tableName); - + const connectionId = type === "external" ? Number(id) : undefined; + const result = await BatchService.getTableColumns( + type, + connectionId, + tableName + ); + if (result.success) { return res.json(result); } else { @@ -136,36 +157,40 @@ export class BatchController { console.error("컬럼 정보 조회 오류:", error); return res.status(500).json({ success: false, - message: "컬럼 정보 조회에 실패했습니다." + message: "컬럼 정보 조회에 실패했습니다.", }); } } /** - * 특정 배치 설정 조회 + * 특정 배치 설정 조회 (회사별) * GET /api/batch-configs/:id */ static async getBatchConfigById(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; - const batchConfig = await BatchService.getBatchConfigById(Number(id)); - + const userCompanyCode = req.user?.companyCode; + const batchConfig = await BatchService.getBatchConfigById( + Number(id), + userCompanyCode + ); + if (!batchConfig) { return res.status(404).json({ success: false, - message: "배치 설정을 찾을 수 없습니다." + message: "배치 설정을 찾을 수 없습니다.", }); } - + return res.json({ success: true, - data: batchConfig + data: batchConfig, }); } catch (error) { console.error("배치 설정 조회 오류:", error); return res.status(500).json({ success: false, - message: "배치 설정 조회에 실패했습니다." + message: "배치 설정 조회에 실패했습니다.", }); } } @@ -177,11 +202,17 @@ export class BatchController { static async createBatchConfig(req: AuthenticatedRequest, res: Response) { try { const { batchName, description, cronSchedule, mappings } = req.body; - - if (!batchName || !cronSchedule || !mappings || !Array.isArray(mappings)) { + + if ( + !batchName || + !cronSchedule || + !mappings || + !Array.isArray(mappings) + ) { return res.status(400).json({ success: false, - message: "필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)" + message: + "필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)", }); } @@ -189,102 +220,123 @@ export class BatchController { batchName, description, cronSchedule, - mappings + mappings, } as CreateBatchConfigRequest); // 생성된 배치가 활성화 상태라면 스케줄러에 등록 (즉시 실행 비활성화) - if (batchConfig.data && batchConfig.data.is_active === 'Y' && batchConfig.data.id) { - await BatchSchedulerService.updateBatchSchedule(batchConfig.data.id, false); + if ( + batchConfig.data && + batchConfig.data.is_active === "Y" && + batchConfig.data.id + ) { + await BatchSchedulerService.updateBatchSchedule( + batchConfig.data.id, + false + ); } - + return res.status(201).json({ success: true, data: batchConfig, - message: "배치 설정이 성공적으로 생성되었습니다." + message: "배치 설정이 성공적으로 생성되었습니다.", }); } catch (error) { console.error("배치 설정 생성 오류:", error); return res.status(500).json({ success: false, - message: "배치 설정 생성에 실패했습니다." + message: "배치 설정 생성에 실패했습니다.", }); } } /** - * 배치 설정 수정 + * 배치 설정 수정 (회사별) * PUT /api/batch-configs/:id */ static async updateBatchConfig(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; - const { batchName, description, cronSchedule, mappings, isActive } = req.body; - + const { batchName, description, cronSchedule, mappings, isActive } = + req.body; + const userId = req.user?.userId; + const userCompanyCode = req.user?.companyCode; + if (!batchName || !cronSchedule) { return res.status(400).json({ success: false, - message: "필수 필드가 누락되었습니다. (batchName, cronSchedule)" + message: "필수 필드가 누락되었습니다. (batchName, cronSchedule)", }); } - const batchConfig = await BatchService.updateBatchConfig(Number(id), { - batchName, - description, - cronSchedule, - mappings, - isActive - } as UpdateBatchConfigRequest); - + const batchConfig = await BatchService.updateBatchConfig( + Number(id), + { + batchName, + description, + cronSchedule, + mappings, + isActive, + } as UpdateBatchConfigRequest, + userId, + userCompanyCode + ); + if (!batchConfig) { return res.status(404).json({ success: false, - message: "배치 설정을 찾을 수 없습니다." + message: "배치 설정을 찾을 수 없습니다.", }); } // 스케줄러에서 배치 스케줄 업데이트 (즉시 실행 비활성화) await BatchSchedulerService.updateBatchSchedule(Number(id), false); - + return res.json({ success: true, data: batchConfig, - message: "배치 설정이 성공적으로 수정되었습니다." + message: "배치 설정이 성공적으로 수정되었습니다.", }); } catch (error) { console.error("배치 설정 수정 오류:", error); return res.status(500).json({ success: false, - message: "배치 설정 수정에 실패했습니다." + message: "배치 설정 수정에 실패했습니다.", }); } } /** - * 배치 설정 삭제 (논리 삭제) + * 배치 설정 삭제 (논리 삭제, 회사별) * DELETE /api/batch-configs/:id */ static async deleteBatchConfig(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; - const result = await BatchService.deleteBatchConfig(Number(id)); - + const userId = req.user?.userId; + const userCompanyCode = req.user?.companyCode; + const result = await BatchService.deleteBatchConfig( + Number(id), + userId, + userCompanyCode + ); + if (!result) { return res.status(404).json({ success: false, - message: "배치 설정을 찾을 수 없습니다." + message: "배치 설정을 찾을 수 없습니다.", }); } - + return res.json({ success: true, - message: "배치 설정이 성공적으로 삭제되었습니다." + message: "배치 설정이 성공적으로 삭제되었습니다.", }); } catch (error) { console.error("배치 설정 삭제 오류:", error); return res.status(500).json({ success: false, - message: "배치 설정 삭제에 실패했습니다." + message: "배치 설정 삭제에 실패했습니다.", }); } } -} \ No newline at end of file +} diff --git a/backend-node/src/controllers/batchExecutionLogController.ts b/backend-node/src/controllers/batchExecutionLogController.ts index 68d8d880..84608731 100644 --- a/backend-node/src/controllers/batchExecutionLogController.ts +++ b/backend-node/src/controllers/batchExecutionLogController.ts @@ -4,7 +4,11 @@ import { Request, Response } from "express"; import { AuthenticatedRequest } from "../types/auth"; import { BatchExecutionLogService } from "../services/batchExecutionLogService"; -import { BatchExecutionLogFilter, CreateBatchExecutionLogRequest, UpdateBatchExecutionLogRequest } from "../types/batchExecutionLogTypes"; +import { + BatchExecutionLogFilter, + CreateBatchExecutionLogRequest, + UpdateBatchExecutionLogRequest, +} from "../types/batchExecutionLogTypes"; export class BatchExecutionLogController { /** @@ -18,7 +22,7 @@ export class BatchExecutionLogController { start_date, end_date, page, - limit + limit, } = req.query; const filter: BatchExecutionLogFilter = { @@ -27,11 +31,15 @@ export class BatchExecutionLogController { start_date: start_date ? new Date(start_date as string) : undefined, end_date: end_date ? new Date(end_date as string) : undefined, page: page ? Number(page) : undefined, - limit: limit ? Number(limit) : undefined + limit: limit ? Number(limit) : undefined, }; - const result = await BatchExecutionLogService.getExecutionLogs(filter); - + const userCompanyCode = req.user?.companyCode; + const result = await BatchExecutionLogService.getExecutionLogs( + filter, + userCompanyCode + ); + if (result.success) { res.json(result); } else { @@ -42,7 +50,7 @@ export class BatchExecutionLogController { res.status(500).json({ success: false, message: "배치 실행 로그 조회 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } @@ -53,9 +61,9 @@ export class BatchExecutionLogController { static async createExecutionLog(req: AuthenticatedRequest, res: Response) { try { const data: CreateBatchExecutionLogRequest = req.body; - + const result = await BatchExecutionLogService.createExecutionLog(data); - + if (result.success) { res.status(201).json(result); } else { @@ -66,7 +74,7 @@ export class BatchExecutionLogController { res.status(500).json({ success: false, message: "배치 실행 로그 생성 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } @@ -78,9 +86,12 @@ export class BatchExecutionLogController { try { const { id } = req.params; const data: UpdateBatchExecutionLogRequest = req.body; - - const result = await BatchExecutionLogService.updateExecutionLog(Number(id), data); - + + const result = await BatchExecutionLogService.updateExecutionLog( + Number(id), + data + ); + if (result.success) { res.json(result); } else { @@ -91,7 +102,7 @@ export class BatchExecutionLogController { res.status(500).json({ success: false, message: "배치 실행 로그 업데이트 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } @@ -102,9 +113,11 @@ export class BatchExecutionLogController { static async deleteExecutionLog(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; - - const result = await BatchExecutionLogService.deleteExecutionLog(Number(id)); - + + const result = await BatchExecutionLogService.deleteExecutionLog( + Number(id) + ); + if (result.success) { res.json(result); } else { @@ -115,7 +128,7 @@ export class BatchExecutionLogController { res.status(500).json({ success: false, message: "배치 실행 로그 삭제 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } @@ -126,9 +139,11 @@ export class BatchExecutionLogController { static async getLatestExecutionLog(req: AuthenticatedRequest, res: Response) { try { const { batchConfigId } = req.params; - - const result = await BatchExecutionLogService.getLatestExecutionLog(Number(batchConfigId)); - + + const result = await BatchExecutionLogService.getLatestExecutionLog( + Number(batchConfigId) + ); + if (result.success) { res.json(result); } else { @@ -139,7 +154,7 @@ export class BatchExecutionLogController { res.status(500).json({ success: false, message: "최신 배치 실행 로그 조회 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } @@ -149,18 +164,14 @@ export class BatchExecutionLogController { */ static async getExecutionStats(req: AuthenticatedRequest, res: Response) { try { - const { - batch_config_id, - start_date, - end_date - } = req.query; + const { batch_config_id, start_date, end_date } = req.query; const result = await BatchExecutionLogService.getExecutionStats( batch_config_id ? Number(batch_config_id) : undefined, start_date ? new Date(start_date as string) : undefined, end_date ? new Date(end_date as string) : undefined ); - + if (result.success) { res.json(result); } else { @@ -171,9 +182,8 @@ export class BatchExecutionLogController { res.status(500).json({ success: false, message: "배치 실행 통계 조회 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } } - diff --git a/backend-node/src/controllers/batchManagementController.ts b/backend-node/src/controllers/batchManagementController.ts index 71640577..d1be2311 100644 --- a/backend-node/src/controllers/batchManagementController.ts +++ b/backend-node/src/controllers/batchManagementController.ts @@ -3,7 +3,12 @@ import { Response } from "express"; import { AuthenticatedRequest } from "../types/auth"; -import { BatchManagementService, BatchConnectionInfo, BatchTableInfo, BatchColumnInfo } from "../services/batchManagementService"; +import { + BatchManagementService, + BatchConnectionInfo, + BatchTableInfo, + BatchColumnInfo, +} from "../services/batchManagementService"; import { BatchService } from "../services/batchService"; import { BatchSchedulerService } from "../services/batchSchedulerService"; import { BatchExternalDbService } from "../services/batchExternalDbService"; @@ -11,11 +16,16 @@ import { CreateBatchConfigRequest, BatchConfig } from "../types/batchTypes"; export class BatchManagementController { /** - * 사용 가능한 커넥션 목록 조회 + * 사용 가능한 커넥션 목록 조회 (회사별) */ - static async getAvailableConnections(req: AuthenticatedRequest, res: Response) { + static async getAvailableConnections( + req: AuthenticatedRequest, + res: Response + ) { try { - const result = await BatchManagementService.getAvailableConnections(); + const userCompanyCode = req.user?.companyCode; + const result = + await BatchManagementService.getAvailableConnections(userCompanyCode); if (result.success) { res.json(result); } else { @@ -26,28 +36,36 @@ export class BatchManagementController { res.status(500).json({ success: false, message: "커넥션 목록 조회 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } /** - * 특정 커넥션의 테이블 목록 조회 + * 특정 커넥션의 테이블 목록 조회 (회사별) */ - static async getTablesFromConnection(req: AuthenticatedRequest, res: Response) { + static async getTablesFromConnection( + req: AuthenticatedRequest, + res: Response + ) { try { const { type, id } = req.params; - - if (type !== 'internal' && type !== 'external') { + const userCompanyCode = req.user?.companyCode; + + if (type !== "internal" && type !== "external") { return res.status(400).json({ success: false, - message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)" + message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)", }); } - const connectionId = type === 'external' ? Number(id) : undefined; - const result = await BatchManagementService.getTablesFromConnection(type, connectionId); - + const connectionId = type === "external" ? Number(id) : undefined; + const result = await BatchManagementService.getTablesFromConnection( + type, + connectionId, + userCompanyCode + ); + if (result.success) { return res.json(result); } else { @@ -58,28 +76,34 @@ export class BatchManagementController { return res.status(500).json({ success: false, message: "테이블 목록 조회 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } /** - * 특정 테이블의 컬럼 정보 조회 + * 특정 테이블의 컬럼 정보 조회 (회사별) */ static async getTableColumns(req: AuthenticatedRequest, res: Response) { try { const { type, id, tableName } = req.params; - - if (type !== 'internal' && type !== 'external') { + const userCompanyCode = req.user?.companyCode; + + if (type !== "internal" && type !== "external") { return res.status(400).json({ success: false, - message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)" + message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)", }); } - const connectionId = type === 'external' ? Number(id) : undefined; - const result = await BatchManagementService.getTableColumns(type, connectionId, tableName); - + const connectionId = type === "external" ? Number(id) : undefined; + const result = await BatchManagementService.getTableColumns( + type, + connectionId, + tableName, + userCompanyCode + ); + if (result.success) { return res.json(result); } else { @@ -90,7 +114,7 @@ export class BatchManagementController { return res.status(500).json({ success: false, message: "컬럼 정보 조회 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } @@ -101,12 +125,19 @@ export class BatchManagementController { */ static async createBatchConfig(req: AuthenticatedRequest, res: Response) { try { - const { batchName, description, cronSchedule, mappings, isActive } = req.body; - - if (!batchName || !cronSchedule || !mappings || !Array.isArray(mappings)) { + const { batchName, description, cronSchedule, mappings, isActive } = + req.body; + + if ( + !batchName || + !cronSchedule || + !mappings || + !Array.isArray(mappings) + ) { return res.status(400).json({ success: false, - message: "필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)" + message: + "필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)", }); } @@ -115,20 +146,20 @@ export class BatchManagementController { description, cronSchedule, mappings, - isActive: isActive !== undefined ? isActive : true + isActive: isActive !== undefined ? isActive : true, } as CreateBatchConfigRequest); - + return res.status(201).json({ success: true, data: batchConfig, - message: "배치 설정이 성공적으로 생성되었습니다." + message: "배치 설정이 성공적으로 생성되었습니다.", }); } catch (error) { console.error("배치 설정 생성 오류:", error); return res.status(500).json({ success: false, message: "배치 설정 생성에 실패했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } @@ -141,28 +172,28 @@ export class BatchManagementController { try { const { id } = req.params; console.log("🔍 배치 설정 조회 요청:", id); - + const result = await BatchService.getBatchConfigById(Number(id)); - + if (!result.success) { return res.status(404).json({ success: false, - message: result.message || "배치 설정을 찾을 수 없습니다." + message: result.message || "배치 설정을 찾을 수 없습니다.", }); } - + console.log("📋 조회된 배치 설정:", result.data); - + return res.json({ success: true, - data: result.data + data: result.data, }); } catch (error) { console.error("❌ 배치 설정 조회 오류:", error); return res.status(500).json({ success: false, message: "배치 설정 조회에 실패했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } @@ -174,27 +205,27 @@ export class BatchManagementController { static async getBatchConfigs(req: AuthenticatedRequest, res: Response) { try { const { page = 1, limit = 10, search, isActive } = req.query; - + const filter = { page: Number(page), limit: Number(limit), search: search as string, - is_active: isActive as string + is_active: isActive as string, }; const result = await BatchService.getBatchConfigs(filter); - + res.json({ success: true, data: result.data, - pagination: result.pagination + pagination: result.pagination, }); } catch (error) { console.error("배치 설정 목록 조회 오류:", error); res.status(500).json({ success: false, message: "배치 설정 목록 조회에 실패했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } @@ -206,20 +237,22 @@ export class BatchManagementController { static async executeBatchConfig(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; - + if (!id || isNaN(Number(id))) { return res.status(400).json({ success: false, - message: "올바른 배치 설정 ID를 제공해주세요." + message: "올바른 배치 설정 ID를 제공해주세요.", }); } // 배치 설정 조회 - const batchConfigResult = await BatchService.getBatchConfigById(Number(id)); + const batchConfigResult = await BatchService.getBatchConfigById( + Number(id) + ); if (!batchConfigResult.success || !batchConfigResult.data) { return res.status(404).json({ success: false, - message: "배치 설정을 찾을 수 없습니다." + message: "배치 설정을 찾을 수 없습니다.", }); } @@ -229,25 +262,28 @@ export class BatchManagementController { console.log(`배치 수동 실행 시작: ${batchConfig.batch_name} (ID: ${id})`); let executionLog: any = null; - + try { // 실행 로그 생성 executionLog = await BatchService.createExecutionLog({ batch_config_id: Number(id), - execution_status: 'RUNNING', + execution_status: "RUNNING", start_time: startTime, total_records: 0, success_records: 0, - failed_records: 0 + failed_records: 0, }); // BatchSchedulerService의 executeBatchConfig 메서드 사용 (중복 로직 제거) - const { BatchSchedulerService } = await import('../services/batchSchedulerService'); - const result = await BatchSchedulerService.executeBatchConfig(batchConfig); + const { BatchSchedulerService } = await import( + "../services/batchSchedulerService" + ); + const result = + await BatchSchedulerService.executeBatchConfig(batchConfig); // result가 undefined인 경우 처리 if (!result) { - throw new Error('배치 실행 결과를 받을 수 없습니다.'); + throw new Error("배치 실행 결과를 받을 수 없습니다."); } const endTime = new Date(); @@ -255,12 +291,12 @@ export class BatchManagementController { // 실행 로그 업데이트 (성공) await BatchService.updateExecutionLog(executionLog.id, { - execution_status: 'SUCCESS', + execution_status: "SUCCESS", end_time: endTime, duration_ms: duration, total_records: result.totalRecords, success_records: result.successRecords, - failed_records: result.failedRecords + failed_records: result.failedRecords, }); return res.json({ @@ -270,45 +306,49 @@ export class BatchManagementController { totalRecords: result.totalRecords, successRecords: result.successRecords, failedRecords: result.failedRecords, - executionTime: duration + executionTime: duration, }, - message: "배치가 성공적으로 실행되었습니다." + message: "배치가 성공적으로 실행되었습니다.", }); - } catch (batchError) { console.error(`배치 실행 실패: ${batchConfig.batch_name}`, batchError); - + // 실행 로그 업데이트 (실패) - executionLog가 생성되었을 경우에만 try { const endTime = new Date(); const duration = endTime.getTime() - startTime.getTime(); - + // executionLog가 정의되어 있는지 확인 - if (typeof executionLog !== 'undefined') { + if (typeof executionLog !== "undefined") { await BatchService.updateExecutionLog(executionLog.id, { - execution_status: 'FAILED', + execution_status: "FAILED", end_time: endTime, duration_ms: duration, - error_message: batchError instanceof Error ? batchError.message : "알 수 없는 오류" + error_message: + batchError instanceof Error + ? batchError.message + : "알 수 없는 오류", }); } } catch (logError) { - console.error('실행 로그 업데이트 실패:', logError); + console.error("실행 로그 업데이트 실패:", logError); } return res.status(500).json({ success: false, message: "배치 실행에 실패했습니다.", - error: batchError instanceof Error ? batchError.message : "알 수 없는 오류" + error: + batchError instanceof Error + ? batchError.message + : "알 수 없는 오류", }); } - } catch (error) { console.error(`배치 실행 오류 (ID: ${req.params.id}):`, error); return res.status(500).json({ success: false, message: "배치 실행 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "Unknown error" + error: error instanceof Error ? error.message : "Unknown error", }); } } @@ -325,26 +365,29 @@ export class BatchManagementController { if (!id || isNaN(Number(id))) { return res.status(400).json({ success: false, - message: "올바른 배치 설정 ID를 제공해주세요." + message: "올바른 배치 설정 ID를 제공해주세요.", }); } - const batchConfig = await BatchService.updateBatchConfig(Number(id), updateData); - + const batchConfig = await BatchService.updateBatchConfig( + Number(id), + updateData + ); + // 스케줄러에서 배치 스케줄 업데이트 (즉시 실행 비활성화) await BatchSchedulerService.updateBatchSchedule(Number(id), false); - + return res.json({ success: true, data: batchConfig, - message: "배치 설정이 성공적으로 업데이트되었습니다." + message: "배치 설정이 성공적으로 업데이트되었습니다.", }); } catch (error) { console.error("배치 설정 업데이트 오류:", error); return res.status(500).json({ success: false, message: "배치 설정 업데이트에 실패했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } @@ -354,21 +397,21 @@ export class BatchManagementController { */ static async previewRestApiData(req: AuthenticatedRequest, res: Response) { try { - const { - apiUrl, - apiKey, - endpoint, - method = 'GET', + const { + apiUrl, + apiKey, + endpoint, + method = "GET", paramType, paramName, paramValue, - paramSource + paramSource, } = req.body; if (!apiUrl || !apiKey || !endpoint) { return res.status(400).json({ success: false, - message: "API URL, API Key, 엔드포인트는 필수입니다." + message: "API URL, API Key, 엔드포인트는 필수입니다.", }); } @@ -378,16 +421,16 @@ export class BatchManagementController { paramType, paramName, paramValue, - paramSource + paramSource, }); // RestApiConnector 사용하여 데이터 조회 - const { RestApiConnector } = await import('../database/RestApiConnector'); - + const { RestApiConnector } = await import("../database/RestApiConnector"); + const connector = new RestApiConnector({ baseUrl: apiUrl, apiKey: apiKey, - timeout: 30000 + timeout: 30000, }); // 연결 테스트 @@ -396,7 +439,7 @@ export class BatchManagementController { // 파라미터가 있는 경우 엔드포인트 수정 let finalEndpoint = endpoint; if (paramType && paramName && paramValue) { - if (paramType === 'url') { + if (paramType === "url") { // URL 파라미터: /api/users/{userId} → /api/users/123 if (endpoint.includes(`{${paramName}}`)) { finalEndpoint = endpoint.replace(`{${paramName}}`, paramValue); @@ -404,9 +447,9 @@ export class BatchManagementController { // 엔드포인트에 {paramName}이 없으면 뒤에 추가 finalEndpoint = `${endpoint}/${paramValue}`; } - } else if (paramType === 'query') { + } else if (paramType === "query") { // 쿼리 파라미터: /api/users?userId=123 - const separator = endpoint.includes('?') ? '&' : '?'; + const separator = endpoint.includes("?") ? "&" : "?"; finalEndpoint = `${endpoint}${separator}${paramName}=${paramValue}`; } } @@ -417,10 +460,11 @@ export class BatchManagementController { const result = await connector.executeQuery(finalEndpoint, method); console.log(`[previewRestApiData] executeQuery 결과:`, { rowCount: result.rowCount, - rowsLength: result.rows ? result.rows.length : 'undefined', - firstRow: result.rows && result.rows.length > 0 ? result.rows[0] : 'no data' + rowsLength: result.rows ? result.rows.length : "undefined", + firstRow: + result.rows && result.rows.length > 0 ? result.rows[0] : "no data", }); - + const data = result.rows.slice(0, 5); // 최대 5개 샘플만 console.log(`[previewRestApiData] 슬라이스된 데이터:`, data); @@ -428,15 +472,15 @@ export class BatchManagementController { // 첫 번째 객체에서 필드명 추출 const fields = Object.keys(data[0]); console.log(`[previewRestApiData] 추출된 필드:`, fields); - + return res.json({ success: true, data: { fields: fields, samples: data, - totalCount: result.rowCount || data.length + totalCount: result.rowCount || data.length, }, - message: `${fields.length}개 필드, ${result.rowCount || data.length}개 레코드를 조회했습니다.` + message: `${fields.length}개 필드, ${result.rowCount || data.length}개 레코드를 조회했습니다.`, }); } else { return res.json({ @@ -444,9 +488,9 @@ export class BatchManagementController { data: { fields: [], samples: [], - totalCount: 0 + totalCount: 0, }, - message: "API에서 데이터를 가져올 수 없습니다." + message: "API에서 데이터를 가져올 수 없습니다.", }); } } catch (error) { @@ -454,7 +498,7 @@ export class BatchManagementController { return res.status(500).json({ success: false, message: "REST API 데이터 미리보기 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류" + error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } @@ -464,18 +508,19 @@ export class BatchManagementController { */ static async saveRestApiBatch(req: AuthenticatedRequest, res: Response) { try { - const { - batchName, - batchType, - cronSchedule, - description, - apiMappings - } = req.body; + const { batchName, batchType, cronSchedule, description, apiMappings } = + req.body; - if (!batchName || !batchType || !cronSchedule || !apiMappings || apiMappings.length === 0) { + if ( + !batchName || + !batchType || + !cronSchedule || + !apiMappings || + apiMappings.length === 0 + ) { return res.status(400).json({ success: false, - message: "필수 필드가 누락되었습니다." + message: "필수 필드가 누락되었습니다.", }); } @@ -484,15 +529,15 @@ export class BatchManagementController { batchType, cronSchedule, description, - apiMappings + apiMappings, }); // BatchService를 사용하여 배치 설정 저장 const batchConfig: CreateBatchConfigRequest = { batchName: batchName, - description: description || '', + description: description || "", cronSchedule: cronSchedule, - mappings: apiMappings + mappings: apiMappings, }; const result = await BatchService.createBatchConfig(batchConfig); @@ -501,7 +546,9 @@ export class BatchManagementController { // 스케줄러에 자동 등록 ✅ try { await BatchSchedulerService.scheduleBatchConfig(result.data); - console.log(`✅ 새로운 배치가 스케줄러에 등록되었습니다: ${batchName} (ID: ${result.data.id})`); + console.log( + `✅ 새로운 배치가 스케줄러에 등록되었습니다: ${batchName} (ID: ${result.data.id})` + ); } catch (schedulerError) { console.error(`❌ 스케줄러 등록 실패: ${batchName}`, schedulerError); // 스케줄러 등록 실패해도 배치 저장은 성공으로 처리 @@ -510,19 +557,19 @@ export class BatchManagementController { return res.json({ success: true, message: "REST API 배치가 성공적으로 저장되었습니다.", - data: result.data + data: result.data, }); } else { return res.status(500).json({ success: false, - message: result.message || "배치 저장에 실패했습니다." + message: result.message || "배치 저장에 실패했습니다.", }); } } catch (error) { console.error("REST API 배치 저장 오류:", error); return res.status(500).json({ success: false, - message: "배치 저장 중 오류가 발생했습니다." + message: "배치 저장 중 오류가 발생했습니다.", }); } } 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/controllers/flowController.ts b/backend-node/src/controllers/flowController.ts index 16668b24..85ad2259 100644 --- a/backend-node/src/controllers/flowController.ts +++ b/backend-node/src/controllers/flowController.ts @@ -557,9 +557,14 @@ export class FlowController { getStepColumnLabels = async (req: Request, res: Response): Promise => { try { const { flowId, stepId } = req.params; + console.log("🏷️ [FlowController] 컬럼 라벨 조회 요청:", { + flowId, + stepId, + }); - const step = await this.flowStepService.getById(parseInt(stepId)); + const step = await this.flowStepService.findById(parseInt(stepId)); if (!step) { + console.warn("⚠️ [FlowController] 스텝을 찾을 수 없음:", stepId); res.status(404).json({ success: false, message: "Step not found", @@ -567,10 +572,11 @@ export class FlowController { return; } - const flowDef = await this.flowDefinitionService.getById( + const flowDef = await this.flowDefinitionService.findById( parseInt(flowId) ); if (!flowDef) { + console.warn("⚠️ [FlowController] 플로우를 찾을 수 없음:", flowId); res.status(404).json({ success: false, message: "Flow definition not found", @@ -580,7 +586,14 @@ export class FlowController { // 테이블명 결정 (스텝 테이블 우선, 없으면 플로우 테이블) const tableName = step.tableName || flowDef.tableName; + console.log("📋 [FlowController] 테이블명 결정:", { + stepTableName: step.tableName, + flowTableName: flowDef.tableName, + selectedTableName: tableName, + }); + if (!tableName) { + console.warn("⚠️ [FlowController] 테이블명이 지정되지 않음"); res.json({ success: true, data: {}, @@ -589,7 +602,7 @@ export class FlowController { } // column_labels 테이블에서 라벨 정보 조회 - const { query } = await import("../config/database"); + const { query } = await import("../database/db"); const labelRows = await query<{ column_name: string; column_label: string | null; @@ -600,6 +613,15 @@ export class FlowController { [tableName] ); + console.log(`✅ [FlowController] column_labels 조회 완료:`, { + tableName, + rowCount: labelRows.length, + labels: labelRows.map((r) => ({ + col: r.column_name, + label: r.column_label, + })), + }); + // { columnName: label } 형태의 객체로 변환 const labels: Record = {}; labelRows.forEach((row) => { @@ -608,12 +630,14 @@ export class FlowController { } }); + console.log("📦 [FlowController] 반환할 라벨 객체:", labels); + res.json({ success: true, data: labels, }); } catch (error: any) { - console.error("Error getting step column labels:", error); + console.error("❌ [FlowController] 컬럼 라벨 조회 오류:", error); res.status(500).json({ success: false, message: error.message || "Failed to get step column labels", 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/RoleService_backup.ts b/backend-node/src/services/RoleService_backup.ts deleted file mode 100644 index 2932a2cc..00000000 --- a/backend-node/src/services/RoleService_backup.ts +++ /dev/null @@ -1,554 +0,0 @@ -import { query } from "../database/db"; -import { logger } from "../utils/logger"; - -/** - * 권한 그룹 인터페이스 - */ -export interface RoleGroup { - objid: number; - authName: string; - authCode: string; - companyCode: string; - status: string; - writer: string; - regdate: Date; - memberCount?: number; - menuCount?: number; - memberNames?: string; -} - -/** - * 권한 그룹 멤버 인터페이스 - */ -export interface RoleMember { - objid: number; - masterObjid: number; - userId: string; - userName?: string; - deptName?: string; - positionName?: string; - writer: string; - regdate: Date; -} - -/** - * 메뉴 권한 인터페이스 - */ -export interface MenuPermission { - objid: number; - menuObjid: number; - authObjid: number; - menuName?: string; - createYn: string; - readYn: string; - updateYn: string; - deleteYn: string; - writer: string; - regdate: Date; -} - -/** - * 권한 그룹 서비스 - */ -export class RoleService { - /** - * 회사별 권한 그룹 목록 조회 - * @param companyCode - 회사 코드 (undefined 시 전체 조회) - * @param search - 검색어 - */ - static async getRoleGroups( - companyCode?: string, - search?: string - ): Promise { - try { - let sql = ` - SELECT - objid, - auth_name AS "authName", - auth_code AS "authCode", - company_code AS "companyCode", - status, - writer, - regdate, - (SELECT COUNT(*) FROM authority_sub_user asu WHERE asu.master_objid = am.objid) AS "memberCount", - (SELECT COUNT(*) FROM rel_menu_auth rma WHERE rma.auth_objid = am.objid) AS "menuCount", - (SELECT STRING_AGG(ui.user_name, ', ' ORDER BY ui.user_name) - FROM authority_sub_user asu - JOIN user_info ui ON asu.user_id = ui.user_id - WHERE asu.master_objid = am.objid) AS "memberNames" - FROM authority_master am - WHERE 1=1 - `; - - const params: any[] = []; - let paramIndex = 1; - - // 회사 코드 필터 (companyCode가 undefined면 전체 조회) - if (companyCode) { - sql += ` AND company_code = $${paramIndex}`; - params.push(companyCode); - paramIndex++; - } - - // 검색어 필터 - if (search && search.trim()) { - sql += ` AND (auth_name ILIKE $${paramIndex} OR auth_code ILIKE $${paramIndex})`; - params.push(`%${search.trim()}%`); - paramIndex++; - } - - sql += ` ORDER BY regdate DESC`; - - logger.info("권한 그룹 조회 SQL", { sql, params }); - const result = await query(sql, params); - logger.info("권한 그룹 조회 결과", { count: result.length }); - return result; - } catch (error) { - logger.error("권한 그룹 목록 조회 실패", { error, companyCode, search }); - throw error; - } - } - - /** - * 권한 그룹 상세 조회 - */ - static async getRoleGroupById(objid: number): Promise { - try { - const sql = ` - SELECT - objid, - auth_name AS "authName", - auth_code AS "authCode", - company_code AS "companyCode", - status, - writer, - regdate - FROM authority_master - WHERE objid = $1 - `; - - const result = await query(sql, [objid]); - return result.length > 0 ? result[0] : null; - } catch (error) { - logger.error("권한 그룹 상세 조회 실패", { error, objid }); - throw error; - } - } - - /** - * 권한 그룹 생성 - */ - static async createRoleGroup(data: { - authName: string; - authCode: string; - companyCode: string; - writer: string; - }): Promise { - try { - const sql = ` - INSERT INTO authority_master (objid, auth_name, auth_code, company_code, status, writer, regdate) - VALUES (nextval('seq_authority_master'), $1, $2, $3, 'active', $4, NOW()) - RETURNING objid, auth_name AS "authName", auth_code AS "authCode", - company_code AS "companyCode", status, writer, regdate - `; - - const result = await query(sql, [ - data.authName, - data.authCode, - data.companyCode, - data.writer, - ]); - - logger.info("권한 그룹 생성 성공", { - objid: result[0].objid, - authName: data.authName, - }); - return result[0]; - } catch (error) { - logger.error("권한 그룹 생성 실패", { error, data }); - throw error; - } - } - - /** - * 권한 그룹 수정 - */ - static async updateRoleGroup( - objid: number, - data: { - authName?: string; - authCode?: string; - status?: string; - } - ): Promise { - try { - const updates: string[] = []; - const params: any[] = []; - let paramIndex = 1; - - if (data.authName !== undefined) { - updates.push(`auth_name = $${paramIndex}`); - params.push(data.authName); - paramIndex++; - } - - if (data.authCode !== undefined) { - updates.push(`auth_code = $${paramIndex}`); - params.push(data.authCode); - paramIndex++; - } - - if (data.status !== undefined) { - updates.push(`status = $${paramIndex}`); - params.push(data.status); - paramIndex++; - } - - if (updates.length === 0) { - throw new Error("수정할 데이터가 없습니다"); - } - - params.push(objid); - - const sql = ` - UPDATE authority_master - SET ${updates.join(", ")} - WHERE objid = $${paramIndex} - RETURNING objid, auth_name AS "authName", auth_code AS "authCode", - company_code AS "companyCode", status, writer, regdate - `; - - const result = await query(sql, params); - - if (result.length === 0) { - throw new Error("권한 그룹을 찾을 수 없습니다"); - } - - logger.info("권한 그룹 수정 성공", { objid, updates }); - return result[0]; - } catch (error) { - logger.error("권한 그룹 수정 실패", { error, objid, data }); - throw error; - } - } - - /** - * 권한 그룹 삭제 - */ - static async deleteRoleGroup(objid: number): Promise { - try { - // CASCADE로 연결된 데이터도 함께 삭제됨 (authority_sub_user, rel_menu_auth) - await query("DELETE FROM authority_master WHERE objid = $1", [objid]); - logger.info("권한 그룹 삭제 성공", { objid }); - } catch (error) { - logger.error("권한 그룹 삭제 실패", { error, objid }); - throw error; - } - } - - /** - * 권한 그룹 멤버 목록 조회 - */ - static async getRoleMembers(masterObjid: number): Promise { - try { - const sql = ` - SELECT - asu.objid, - asu.master_objid AS "masterObjid", - asu.user_id AS "userId", - ui.user_name AS "userName", - ui.dept_name AS "deptName", - ui.position_name AS "positionName", - asu.writer, - asu.regdate - FROM authority_sub_user asu - JOIN user_info ui ON asu.user_id = ui.user_id - WHERE asu.master_objid = $1 - ORDER BY ui.user_name - `; - - const result = await query(sql, [masterObjid]); - return result; - } catch (error) { - logger.error("권한 그룹 멤버 조회 실패", { error, masterObjid }); - throw error; - } - } - - /** - * 권한 그룹 멤버 추가 (여러 명) - */ - static async addRoleMembers( - masterObjid: number, - userIds: string[], - writer: string - ): Promise { - try { - // 이미 존재하는 멤버 제외 - const existingSql = ` - SELECT user_id - FROM authority_sub_user - WHERE master_objid = $1 AND user_id = ANY($2) - `; - const existing = await query<{ user_id: string }>(existingSql, [ - masterObjid, - userIds, - ]); - const existingIds = new Set( - existing.map((row: { user_id: string }) => row.user_id) - ); - - const newUserIds = userIds.filter((userId) => !existingIds.has(userId)); - - if (newUserIds.length === 0) { - logger.info("추가할 신규 멤버가 없습니다", { masterObjid, userIds }); - return; - } - - // 배치 삽입 - const values = newUserIds - .map( - (_, index) => - `(nextval('seq_authority_sub_user'), $1, $${index + 2}, $${newUserIds.length + 2}, NOW())` - ) - .join(", "); - - const sql = ` - INSERT INTO authority_sub_user (objid, master_objid, user_id, writer, regdate) - VALUES ${values} - `; - - await query(sql, [masterObjid, ...newUserIds, writer]); - - // 히스토리 기록 - for (const userId of newUserIds) { - await this.insertAuthorityHistory(masterObjid, userId, "ADD", writer); - } - - logger.info("권한 그룹 멤버 추가 성공", { - masterObjid, - count: newUserIds.length, - }); - } catch (error) { - logger.error("권한 그룹 멤버 추가 실패", { error, masterObjid, userIds }); - throw error; - } - } - - /** - * 권한 그룹 멤버 제거 (여러 명) - */ - static async removeRoleMembers( - masterObjid: number, - userIds: string[], - writer: string - ): Promise { - try { - await query( - "DELETE FROM authority_sub_user WHERE master_objid = $1 AND user_id = ANY($2)", - [masterObjid, userIds] - ); - - // 히스토리 기록 - for (const userId of userIds) { - await this.insertAuthorityHistory( - masterObjid, - userId, - "REMOVE", - writer - ); - } - - logger.info("권한 그룹 멤버 제거 성공", { - masterObjid, - count: userIds.length, - }); - } catch (error) { - logger.error("권한 그룹 멤버 제거 실패", { error, masterObjid, userIds }); - throw error; - } - } - - /** - * 권한 히스토리 기록 - */ - private static async insertAuthorityHistory( - masterObjid: number, - userId: string, - historyType: "ADD" | "REMOVE", - writer: string - ): Promise { - try { - const sql = ` - INSERT INTO authority_master_history - (objid, parent_objid, parent_name, parent_code, user_id, active, history_type, writer, reg_date) - SELECT - nextval('seq_authority_master'), - $1, - am.auth_name, - am.auth_code, - $2, - am.status, - $3, - $4, - NOW() - FROM authority_master am - WHERE am.objid = $1 - `; - - await query(sql, [masterObjid, userId, historyType, writer]); - } catch (error) { - logger.error("권한 히스토리 기록 실패", { - error, - masterObjid, - userId, - historyType, - }); - // 히스토리 기록 실패는 메인 작업을 중단하지 않음 - } - } - - /** - * 메뉴 권한 목록 조회 - */ - static async getMenuPermissions( - authObjid: number - ): Promise { - try { - const sql = ` - SELECT - rma.objid, - rma.menu_objid AS "menuObjid", - rma.auth_objid AS "authObjid", - mi.menu_name_kor AS "menuName", - mi.menu_code AS "menuCode", - mi.menu_url AS "menuUrl", - rma.create_yn AS "createYn", - rma.read_yn AS "readYn", - rma.update_yn AS "updateYn", - rma.delete_yn AS "deleteYn", - rma.execute_yn AS "executeYn", - rma.export_yn AS "exportYn", - rma.writer, - rma.regdate - FROM rel_menu_auth rma - LEFT JOIN menu_info mi ON rma.menu_objid = mi.objid - WHERE rma.auth_objid = $1 - ORDER BY mi.menu_name_kor - `; - - const result = await query(sql, [authObjid]); - return result; - } catch (error) { - logger.error("메뉴 권한 조회 실패", { error, authObjid }); - throw error; - } - } - - /** - * 메뉴 권한 설정 (여러 메뉴) - */ - static async setMenuPermissions( - authObjid: number, - permissions: Array<{ - menuObjid: number; - createYn: string; - readYn: string; - updateYn: string; - deleteYn: string; - }>, - writer: string - ): Promise { - try { - // 기존 권한 삭제 - await query("DELETE FROM rel_menu_auth WHERE auth_objid = $1", [ - authObjid, - ]); - - // 새로운 권한 삽입 - if (permissions.length > 0) { - const values = permissions - .map( - (_, index) => - `(nextval('seq_rel_menu_auth'), $${index * 5 + 2}, $1, $${index * 5 + 3}, $${index * 5 + 4}, $${index * 5 + 5}, $${index * 5 + 6}, $${permissions.length * 5 + 2}, NOW())` - ) - .join(", "); - - const params = permissions.flatMap((p) => [ - p.menuObjid, - p.createYn, - p.readYn, - p.updateYn, - p.deleteYn, - ]); - - const sql = ` - INSERT INTO rel_menu_auth (objid, menu_objid, auth_objid, create_yn, read_yn, update_yn, delete_yn, writer, regdate) - VALUES ${values} - `; - - await query(sql, [authObjid, ...params, writer]); - } - - logger.info("메뉴 권한 설정 성공", { - authObjid, - count: permissions.length, - }); - } catch (error) { - logger.error("메뉴 권한 설정 실패", { error, authObjid, permissions }); - throw error; - } - } - - /** - * 사용자가 속한 권한 그룹 목록 조회 - */ - static async getUserRoleGroups( - userId: string, - companyCode: string - ): Promise { - try { - const sql = ` - SELECT - am.objid, - am.auth_name AS "authName", - am.auth_code AS "authCode", - am.company_code AS "companyCode", - am.status, - am.writer, - am.regdate - FROM authority_master am - JOIN authority_sub_user asu ON am.objid = asu.master_objid - WHERE asu.user_id = $1 - AND am.company_code = $2 - AND am.status = 'active' - ORDER BY am.auth_name - `; - - const result = await query(sql, [userId, companyCode]); - return result; - } catch (error) { - logger.error("사용자 권한 그룹 조회 실패", { - error, - userId, - companyCode, - }); - throw error; - } - } - - /** - * 전체 메뉴 목록 조회 (권한 설정용) - */ - /** - * 전체 메뉴 목록 조회 (권한 설정용) - */ - static async getAllMenus(companyCode?: string): Promise { - try { - logger.info("🔍 전체 메뉴 목록 조회 시작", { companyCode }); - - let whereConditions: string[] = ["status = 'active'"]; - const params: any[] = []; - let paramIndex = 1; - - // 회사 코드 필터 (선택적) diff --git a/backend-node/src/services/RoleService_getAllMenus_fixed.ts b/backend-node/src/services/RoleService_getAllMenus_fixed.ts deleted file mode 100644 index 9dd1689d..00000000 --- a/backend-node/src/services/RoleService_getAllMenus_fixed.ts +++ /dev/null @@ -1,66 +0,0 @@ - /** - * 전체 메뉴 목록 조회 (권한 설정용) - */ - static async getAllMenus(companyCode?: string): Promise { - try { - logger.info("🔍 전체 메뉴 목록 조회 시작", { companyCode }); - - let whereConditions: string[] = ["status = 'active'"]; - const params: any[] = []; - let paramIndex = 1; - - // 회사 코드 필터 (선택적) - // 공통 메뉴(*)와 특정 회사 메뉴를 모두 조회 - if (companyCode) { - whereConditions.push(`(company_code = \$${paramIndex} OR company_code = '*')`); - params.push(companyCode); - paramIndex++; - logger.info("📋 회사 코드 필터 적용", { companyCode }); - } else { - logger.info("📋 회사 코드 필터 없음 (전체 조회)"); - } - - const whereClause = whereConditions.join(" AND "); - - const sql = ` - SELECT - objid, - menu_name_kor AS "menuName", - menu_name_eng AS "menuNameEng", - menu_code AS "menuCode", - menu_url AS "menuUrl", - menu_type AS "menuType", - parent_obj_id AS "parentObjid", - seq AS "sortOrder", - company_code AS "companyCode" - FROM menu_info - WHERE ${whereClause} - ORDER BY seq, menu_name_kor - `; - - logger.info("🔍 SQL 쿼리 실행", { - whereClause, - params, - sql: sql.substring(0, 200) + "...", - }); - - const result = await query(sql, params); - - logger.info("✅ 메뉴 목록 조회 성공", { - count: result.length, - companyCode, - menus: result.map((m) => ({ - objid: m.objid, - name: m.menuName, - code: m.menuCode, - companyCode: m.companyCode, - })), - }); - - return result; - } catch (error) { - logger.error("❌ 메뉴 목록 조회 실패", { error, companyCode }); - throw error; - } - } -} diff --git a/backend-node/src/services/batchExecutionLogService.ts b/backend-node/src/services/batchExecutionLogService.ts index c134b0db..f2fc583c 100644 --- a/backend-node/src/services/batchExecutionLogService.ts +++ b/backend-node/src/services/batchExecutionLogService.ts @@ -13,10 +13,11 @@ import { ApiResponse } from "../types/batchTypes"; export class BatchExecutionLogService { /** - * 배치 실행 로그 목록 조회 + * 배치 실행 로그 목록 조회 (회사별) */ static async getExecutionLogs( - filter: BatchExecutionLogFilter = {} + filter: BatchExecutionLogFilter = {}, + userCompanyCode?: string ): Promise> { try { const { @@ -36,6 +37,12 @@ export class BatchExecutionLogService { const params: any[] = []; let paramIndex = 1; + // 회사별 필터링 (최고 관리자가 아닌 경우) + if (userCompanyCode && userCompanyCode !== "*") { + whereConditions.push(`bc.company_code = $${paramIndex++}`); + params.push(userCompanyCode); + } + if (batch_config_id) { whereConditions.push(`bel.batch_config_id = $${paramIndex++}`); params.push(batch_config_id); diff --git a/backend-node/src/services/batchManagementService.ts b/backend-node/src/services/batchManagementService.ts index 6bb452da..72ff52c4 100644 --- a/backend-node/src/services/batchManagementService.ts +++ b/backend-node/src/services/batchManagementService.ts @@ -35,11 +35,11 @@ export interface BatchApiResponse { export class BatchManagementService { /** - * 배치관리용 연결 목록 조회 + * 배치관리용 연결 목록 조회 (회사별) */ - static async getAvailableConnections(): Promise< - BatchApiResponse - > { + static async getAvailableConnections( + userCompanyCode?: string + ): Promise> { try { const connections: BatchConnectionInfo[] = []; @@ -50,19 +50,27 @@ export class BatchManagementService { db_type: "postgresql", }); - // 활성화된 외부 DB 연결 조회 + // 활성화된 외부 DB 연결 조회 (회사별 필터링) + let query_sql = `SELECT id, connection_name, db_type, description + FROM external_db_connections + WHERE is_active = 'Y'`; + + const params: any[] = []; + + // 회사별 필터링 (최고 관리자가 아닌 경우) + if (userCompanyCode && userCompanyCode !== "*") { + query_sql += ` AND company_code = $1`; + params.push(userCompanyCode); + } + + query_sql += ` ORDER BY connection_name ASC`; + const externalConnections = await query<{ id: number; connection_name: string; db_type: string; description: string; - }>( - `SELECT id, connection_name, db_type, description - FROM external_db_connections - WHERE is_active = 'Y' - ORDER BY connection_name ASC`, - [] - ); + }>(query_sql, params); // 외부 DB 연결 추가 externalConnections.forEach((conn) => { @@ -90,11 +98,12 @@ export class BatchManagementService { } /** - * 배치관리용 테이블 목록 조회 + * 배치관리용 테이블 목록 조회 (회사별) */ static async getTablesFromConnection( connectionType: "internal" | "external", - connectionId?: number + connectionId?: number, + userCompanyCode?: string ): Promise> { try { let tables: BatchTableInfo[] = []; @@ -115,8 +124,11 @@ export class BatchManagementService { columns: [], })); } else if (connectionType === "external" && connectionId) { - // 외부 DB 테이블 조회 - const tablesResult = await this.getExternalTables(connectionId); + // 외부 DB 테이블 조회 (회사별 필터링) + const tablesResult = await this.getExternalTables( + connectionId, + userCompanyCode + ); if (tablesResult.success && tablesResult.data) { tables = tablesResult.data; } @@ -138,12 +150,13 @@ export class BatchManagementService { } /** - * 배치관리용 테이블 컬럼 정보 조회 + * 배치관리용 테이블 컬럼 정보 조회 (회사별) */ static async getTableColumns( connectionType: "internal" | "external", connectionId: number | undefined, - tableName: string + tableName: string, + userCompanyCode?: string ): Promise> { try { console.log(`[BatchManagementService] getTableColumns 호출:`, { @@ -189,14 +202,15 @@ export class BatchManagementService { column_default: row.column_default, })); } else if (connectionType === "external" && connectionId) { - // 외부 DB 컬럼 조회 + // 외부 DB 컬럼 조회 (회사별 필터링) console.log( `[BatchManagementService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}` ); const columnsResult = await this.getExternalTableColumns( connectionId, - tableName + tableName, + userCompanyCode ); console.log( @@ -226,22 +240,29 @@ export class BatchManagementService { } /** - * 외부 DB 테이블 목록 조회 (내부 구현) + * 외부 DB 테이블 목록 조회 (내부 구현, 회사별) */ private static async getExternalTables( - connectionId: number + connectionId: number, + userCompanyCode?: string ): Promise> { try { - // 연결 정보 조회 - const connection = await queryOne( - `SELECT * FROM external_db_connections WHERE id = $1`, - [connectionId] - ); + // 연결 정보 조회 (회사별 필터링) + let query_sql = `SELECT * FROM external_db_connections WHERE id = $1`; + const params: any[] = [connectionId]; + + // 회사별 필터링 (최고 관리자가 아닌 경우) + if (userCompanyCode && userCompanyCode !== "*") { + query_sql += ` AND company_code = $2`; + params.push(userCompanyCode); + } + + const connection = await queryOne(query_sql, params); if (!connection) { return { success: false, - message: "연결 정보를 찾을 수 없습니다.", + message: "연결 정보를 찾을 수 없거나 권한이 없습니다.", }; } @@ -299,26 +320,33 @@ export class BatchManagementService { } /** - * 외부 DB 테이블 컬럼 정보 조회 (내부 구현) + * 외부 DB 테이블 컬럼 정보 조회 (내부 구현, 회사별) */ private static async getExternalTableColumns( connectionId: number, - tableName: string + tableName: string, + userCompanyCode?: string ): Promise> { try { console.log( `[BatchManagementService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}` ); - // 연결 정보 조회 - const connection = await queryOne( - `SELECT * FROM external_db_connections WHERE id = $1`, - [connectionId] - ); + // 연결 정보 조회 (회사별 필터링) + let query_sql = `SELECT * FROM external_db_connections WHERE id = $1`; + const params: any[] = [connectionId]; + + // 회사별 필터링 (최고 관리자가 아닌 경우) + if (userCompanyCode && userCompanyCode !== "*") { + query_sql += ` AND company_code = $2`; + params.push(userCompanyCode); + } + + const connection = await queryOne(query_sql, params); if (!connection) { console.log( - `[BatchManagementService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}` + `[BatchManagementService] 연결 정보를 찾을 수 없거나 권한이 없음: connectionId=${connectionId}` ); return { success: false, diff --git a/backend-node/src/services/batchService.ts b/backend-node/src/services/batchService.ts index 31368be9..07cdd61c 100644 --- a/backend-node/src/services/batchService.ts +++ b/backend-node/src/services/batchService.ts @@ -20,27 +20,33 @@ import { DbConnectionManager } from "./dbConnectionManager"; export class BatchService { /** - * 배치 설정 목록 조회 + * 배치 설정 목록 조회 (회사별) */ static async getBatchConfigs( - filter: BatchConfigFilter + filter: BatchConfigFilter, + userCompanyCode?: string ): Promise> { try { const whereConditions: string[] = []; const values: any[] = []; let paramIndex = 1; + // 회사별 필터링 (최고 관리자가 아닌 경우 필수) + if (userCompanyCode && userCompanyCode !== "*") { + whereConditions.push(`bc.company_code = $${paramIndex++}`); + values.push(userCompanyCode); + } else if (userCompanyCode === "*" && filter.company_code) { + // 최고 관리자: 필터가 있으면 적용 + whereConditions.push(`bc.company_code = $${paramIndex++}`); + values.push(filter.company_code); + } + // 필터 조건 적용 if (filter.is_active) { whereConditions.push(`bc.is_active = $${paramIndex++}`); values.push(filter.is_active); } - if (filter.company_code) { - whereConditions.push(`bc.company_code = $${paramIndex++}`); - values.push(filter.company_code); - } - // 검색 조건 적용 (OR) if (filter.search && filter.search.trim()) { whereConditions.push( @@ -122,14 +128,14 @@ export class BatchService { } /** - * 특정 배치 설정 조회 + * 특정 배치 설정 조회 (회사별) */ static async getBatchConfigById( - id: number + id: number, + userCompanyCode?: string ): Promise> { try { - const batchConfig = await queryOne( - `SELECT bc.id, bc.batch_name, bc.description, bc.cron_schedule, + let query = `SELECT bc.id, bc.batch_name, bc.description, bc.cron_schedule, bc.is_active, bc.company_code, bc.created_date, bc.created_by, bc.updated_date, bc.updated_by, COALESCE( @@ -155,15 +161,25 @@ export class BatchService { ) as batch_mappings FROM batch_configs bc LEFT JOIN batch_mappings bm ON bc.id = bm.batch_config_id - WHERE bc.id = $1 - GROUP BY bc.id`, - [id] - ); + WHERE bc.id = $1`; + + const params: any[] = [id]; + let paramIndex = 2; + + // 회사별 필터링 (최고 관리자가 아닌 경우) + if (userCompanyCode && userCompanyCode !== "*") { + query += ` AND bc.company_code = $${paramIndex}`; + params.push(userCompanyCode); + } + + query += ` GROUP BY bc.id`; + + const batchConfig = await queryOne(query, params); if (!batchConfig) { return { success: false, - message: "배치 설정을 찾을 수 없습니다.", + message: "배치 설정을 찾을 수 없거나 권한이 없습니다.", }; } @@ -267,15 +283,21 @@ export class BatchService { } /** - * 배치 설정 수정 + * 배치 설정 수정 (회사별) */ static async updateBatchConfig( id: number, data: UpdateBatchConfigRequest, - userId?: string + userId?: string, + userCompanyCode?: string ): Promise> { try { - // 기존 배치 설정 확인 + // 기존 배치 설정 확인 (회사 권한 체크 포함) + const existing = await this.getBatchConfigById(id, userCompanyCode); + if (!existing.success) { + return existing; + } + const existingConfig = await queryOne( `SELECT bc.*, COALESCE( @@ -416,13 +438,20 @@ export class BatchService { } /** - * 배치 설정 삭제 (논리 삭제) + * 배치 설정 삭제 (논리 삭제, 회사별) */ static async deleteBatchConfig( id: number, - userId?: string + userId?: string, + userCompanyCode?: string ): Promise> { try { + // 기존 배치 설정 확인 (회사 권한 체크 포함) + const existing = await this.getBatchConfigById(id, userCompanyCode); + if (!existing.success) { + return existing as ApiResponse; + } + const existingConfig = await queryOne( `SELECT * FROM batch_configs WHERE id = $1`, [id] 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 63472e6b..28eac869 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, endpoint_path, 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: "연결을 찾을 수 없거나 권한이 없습니다.", }; } @@ -226,11 +252,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; } @@ -360,24 +387,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, diff --git a/frontend/components/admin/dashboard/VehicleMapConfigPanel.tsx b/frontend/components/admin/dashboard/VehicleMapConfigPanel.tsx index 55df37d0..9b9e9567 100644 --- a/frontend/components/admin/dashboard/VehicleMapConfigPanel.tsx +++ b/frontend/components/admin/dashboard/VehicleMapConfigPanel.tsx @@ -1,7 +1,9 @@ -'use client'; +"use client"; -import React, { useState, useCallback } from 'react'; -import { ChartConfig, QueryResult } from './types'; +import React, { useState, useCallback } from "react"; +import { ChartConfig, QueryResult, MarkerColorRule } from "./types"; +import { Plus, Trash2 } from "lucide-react"; +import { v4 as uuidv4 } from "uuid"; interface VehicleMapConfigPanelProps { config?: ChartConfig; @@ -18,24 +20,80 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V const [currentConfig, setCurrentConfig] = useState(config || {}); // 설정 업데이트 - const updateConfig = useCallback((updates: Partial) => { - const newConfig = { ...currentConfig, ...updates }; - setCurrentConfig(newConfig); - onConfigChange(newConfig); - }, [currentConfig, onConfigChange]); + const updateConfig = useCallback( + (updates: Partial) => { + const newConfig = { ...currentConfig, ...updates }; + setCurrentConfig(newConfig); + onConfigChange(newConfig); + }, + [currentConfig, onConfigChange], + ); // 사용 가능한 컬럼 목록 const availableColumns = queryResult?.columns || []; const sampleData = queryResult?.rows?.[0] || {}; + // 마커 색상 모드 변경 + const handleMarkerColorModeChange = useCallback( + (mode: "single" | "conditional") => { + if (mode === "single") { + updateConfig({ + markerColorMode: "single", + markerColorColumn: undefined, + markerColorRules: undefined, + markerDefaultColor: "#3b82f6", // 파란색 + }); + } else { + updateConfig({ + markerColorMode: "conditional", + markerColorRules: [], + markerDefaultColor: "#6b7280", // 회색 + }); + } + }, + [updateConfig], + ); + + // 마커 색상 규칙 추가 + const addColorRule = useCallback(() => { + const newRule: MarkerColorRule = { + id: uuidv4(), + value: "", + color: "#3b82f6", + label: "", + }; + const currentRules = currentConfig.markerColorRules || []; + updateConfig({ markerColorRules: [...currentRules, newRule] }); + }, [currentConfig.markerColorRules, updateConfig]); + + // 마커 색상 규칙 삭제 + const deleteColorRule = useCallback( + (id: string) => { + const currentRules = currentConfig.markerColorRules || []; + updateConfig({ markerColorRules: currentRules.filter((rule) => rule.id !== id) }); + }, + [currentConfig.markerColorRules, updateConfig], + ); + + // 마커 색상 규칙 업데이트 + const updateColorRule = useCallback( + (id: string, updates: Partial) => { + const currentRules = currentConfig.markerColorRules || []; + updateConfig({ + markerColorRules: currentRules.map((rule) => (rule.id === id ? { ...rule, ...updates } : rule)), + }); + }, + [currentConfig.markerColorRules, updateConfig], + ); + return (

🗺️ 지도 설정

{/* 쿼리 결과가 없을 때 */} {!queryResult && ( -
-
+
+
💡 먼저 SQL 쿼리를 실행하여 데이터를 가져온 후 지도를 설정할 수 있습니다.
@@ -49,10 +107,10 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V updateConfig({ title: e.target.value })} placeholder="차량 위치 지도" - className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs" + className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs" />
@@ -60,12 +118,12 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
updateConfig({ longitudeColumn: e.target.value })} - className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs" + className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs" > {availableColumns.map((col) => ( @@ -98,13 +156,11 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V {/* 라벨 컬럼 (선택사항) */}
- +
- {/* 상태 컬럼 (선택사항) */} -
- - + {/* 마커 색상 설정 */} +
+
🎨 마커 색상 설정
+ + {/* 색상 모드 선택 */} +
+ +
+ + +
+
+ + {/* 단일 색상 모드 */} + {(currentConfig.markerColorMode || "single") === "single" && ( +
+ +
+ updateConfig({ markerDefaultColor: e.target.value })} + className="h-8 w-12 cursor-pointer rounded border border-gray-300" + /> + updateConfig({ markerDefaultColor: e.target.value })} + placeholder="#3b82f6" + className="flex-1 rounded-lg border border-gray-300 px-2 py-1.5 text-xs" + /> +
+

모든 마커가 동일한 색상으로 표시됩니다

+
+ )} + + {/* 조건부 색상 모드 */} + {currentConfig.markerColorMode === "conditional" && ( +
+ {/* 색상 조건 컬럼 선택 */} +
+ + +

이 컬럼의 값에 따라 마커 색상이 결정됩니다

+
+ + {/* 기본 색상 */} +
+ +
+ updateConfig({ markerDefaultColor: e.target.value })} + className="h-8 w-12 cursor-pointer rounded border border-gray-300" + /> + updateConfig({ markerDefaultColor: e.target.value })} + placeholder="#6b7280" + className="flex-1 rounded-lg border border-gray-300 px-2 py-1.5 text-xs" + /> +
+

규칙에 매칭되지 않는 경우 사용할 색상

+
+ + {/* 색상 규칙 목록 */} +
+
+ + +
+ + {/* 규칙 리스트 */} + {(currentConfig.markerColorRules || []).length === 0 ? ( +
+

추가 버튼을 눌러 색상 규칙을 만드세요

+
+ ) : ( +
+ {(currentConfig.markerColorRules || []).map((rule) => ( +
+ {/* 규칙 헤더 */} +
+ 규칙 + +
+ + {/* 조건 값 */} +
+ + updateColorRule(rule.id, { value: e.target.value })} + placeholder="예: active, inactive" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + /> +
+ + {/* 색상 */} +
+ +
+ updateColorRule(rule.id, { color: e.target.value })} + className="h-8 w-12 cursor-pointer rounded border border-gray-300" + /> + updateColorRule(rule.id, { color: e.target.value })} + placeholder="#3b82f6" + className="flex-1 rounded border border-gray-300 px-2 py-1 text-xs" + /> +
+
+ + {/* 라벨 (선택사항) */} +
+ + updateColorRule(rule.id, { label: e.target.value })} + placeholder="예: 활성, 비활성" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + /> +
+
+ ))} +
+ )} +
+
+ )}
{/* 날씨 정보 표시 옵션 */} -
- -

- 마커 팝업에 해당 위치의 날씨 정보를 함께 표시합니다 -

-
+
+ +

마커 팝업에 해당 위치의 날씨 정보를 함께 표시합니다

+
-
- -

- 현재 발효 중인 기상특보(주의보/경보)를 지도에 색상 영역으로 표시합니다 -

-
+
+ +

+ 현재 발효 중인 기상특보(주의보/경보)를 지도에 색상 영역으로 표시합니다 +

+
{/* 설정 미리보기 */} -
-
📋 설정 미리보기
-
-
위도: {currentConfig.latitudeColumn || '미설정'}
-
경도: {currentConfig.longitudeColumn || '미설정'}
-
라벨: {currentConfig.labelColumn || '없음'}
-
상태: {currentConfig.statusColumn || '없음'}
-
날씨 표시: {currentConfig.showWeather ? '활성화' : '비활성화'}
-
기상특보 표시: {currentConfig.showWeatherAlerts ? '활성화' : '비활성화'}
-
데이터 개수: {queryResult.rows.length}개
+
+
📋 설정 미리보기
+
+
+ 위도: {currentConfig.latitudeColumn || "미설정"} +
+
+ 경도: {currentConfig.longitudeColumn || "미설정"} +
+
+ 라벨: {currentConfig.labelColumn || "없음"} +
+
+ 색상 모드: {currentConfig.markerColorMode === "conditional" ? "조건부" : "단일"} +
+ {currentConfig.markerColorMode === "conditional" && ( + <> +
+ 색상 조건 컬럼: {currentConfig.markerColorColumn || "미설정"} +
+
+ 색상 규칙 개수: {(currentConfig.markerColorRules || []).length}개 +
+ + )} +
+ 날씨 표시: {currentConfig.showWeather ? "활성화" : "비활성화"} +
+
+ 기상특보 표시: {currentConfig.showWeatherAlerts ? "활성화" : "비활성화"} +
+
+ 데이터 개수: {queryResult.rows.length}개 +
{/* 필수 필드 확인 */} {(!currentConfig.latitudeColumn || !currentConfig.longitudeColumn) && ( -
-
+
+
⚠️ 위도와 경도 컬럼을 반드시 선택해야 지도에 표시할 수 있습니다.
@@ -192,4 +439,3 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
); } - diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index 15236d42..d90db2b2 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -223,6 +223,20 @@ export interface ChartConfig { statusColumn?: string; // 상태 컬럼 showWeather?: boolean; // 날씨 정보 표시 여부 showWeatherAlerts?: boolean; // 기상특보 영역 표시 여부 + + // 마커 색상 설정 + markerColorMode?: "single" | "conditional"; // 마커 색상 모드 (단일/조건부) + markerColorColumn?: string; // 색상 조건 컬럼 + markerColorRules?: MarkerColorRule[]; // 색상 규칙 배열 + markerDefaultColor?: string; // 기본 마커 색상 +} + +// 마커 색상 규칙 +export interface MarkerColorRule { + id: string; // 고유 ID + value: string; // 컬럼 값 (예: "active", "inactive") + color: string; // 마커 색상 (hex) + label?: string; // 라벨 (선택사항) } export interface QueryResult { diff --git a/frontend/components/dashboard/widgets/MapSummaryWidget.tsx b/frontend/components/dashboard/widgets/MapSummaryWidget.tsx index 58c49814..ae911260 100644 --- a/frontend/components/dashboard/widgets/MapSummaryWidget.tsx +++ b/frontend/components/dashboard/widgets/MapSummaryWidget.tsx @@ -42,6 +42,7 @@ interface MarkerData { name: string; info: any; weather?: WeatherData | null; + markerColor?: string; // 마커 색상 } // 테이블명 한글 번역 @@ -472,6 +473,33 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) { const latCol = element.chartConfig?.latitudeColumn || "latitude"; const lngCol = element.chartConfig?.longitudeColumn || "longitude"; + // 마커 색상 결정 함수 + const getMarkerColor = (row: any): string => { + const colorMode = element.chartConfig?.markerColorMode || "single"; + + if (colorMode === "single") { + // 단일 색상 모드 + return element.chartConfig?.markerDefaultColor || "#3b82f6"; + } else { + // 조건부 색상 모드 + const colorColumn = element.chartConfig?.markerColorColumn; + const colorRules = element.chartConfig?.markerColorRules || []; + const defaultColor = element.chartConfig?.markerDefaultColor || "#6b7280"; + + if (!colorColumn || colorRules.length === 0) { + return defaultColor; + } + + // 컬럼 값 가져오기 + const columnValue = String(row[colorColumn] || ""); + + // 색상 규칙 매칭 + const matchedRule = colorRules.find((rule) => String(rule.value) === columnValue); + + return matchedRule ? matchedRule.color : defaultColor; + } + }; + // 유효한 좌표 필터링 및 마커 데이터 생성 const markerData = rows .filter((row: any) => row[latCol] && row[lngCol]) @@ -481,6 +509,7 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) { name: row.name || row.vehicle_number || row.warehouse_name || row.customer_name || "알 수 없음", info: row, weather: null, + markerColor: getMarkerColor(row), // 마커 색상 추가 })); setMarkers(markerData); @@ -693,54 +722,81 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) { })} {/* 마커 표시 */} - {markers.map((marker, idx) => ( - - -
- {/* 마커 정보 */} -
-
{marker.name}
- {Object.entries(marker.info) - .filter(([key]) => !["latitude", "longitude", "lat", "lng"].includes(key.toLowerCase())) - .map(([key, value]) => ( -
- {key}: {String(value)} -
- ))} -
+ {markers.map((marker, idx) => { + // Leaflet 커스텀 아이콘 생성 (클라이언트 사이드에서만) + let customIcon; + if (typeof window !== "undefined") { + const L = require("leaflet"); + customIcon = L.divIcon({ + className: "custom-marker", + html: ` +
+ `, + iconSize: [30, 30], + iconAnchor: [15, 15], + }); + } - {/* 날씨 정보 */} - {marker.weather && ( -
-
- {getWeatherIcon(marker.weather.weatherMain)} - 현재 날씨 -
-
{marker.weather.weatherDescription}
-
-
- 온도 - {marker.weather.temperature}°C -
-
- 체감온도 - {marker.weather.feelsLike}°C -
-
- 습도 - {marker.weather.humidity}% -
-
- 풍속 - {marker.weather.windSpeed} m/s -
-
+ return ( + + +
+ {/* 마커 정보 */} +
+
{marker.name}
+ {Object.entries(marker.info) + .filter(([key]) => !["latitude", "longitude", "lat", "lng"].includes(key.toLowerCase())) + .map(([key, value]) => ( +
+ {key}: {String(value)} +
+ ))}
- )} -
-
-
- ))} + + {/* 날씨 정보 */} + {marker.weather && ( +
+
+ {getWeatherIcon(marker.weather.weatherMain)} + 현재 날씨 +
+
{marker.weather.weatherDescription}
+
+
+ 온도 + {marker.weather.temperature}°C +
+
+ 체감온도 + {marker.weather.feelsLike}°C +
+
+ 습도 + {marker.weather.humidity}% +
+
+ 풍속 + {marker.weather.windSpeed} m/s +
+
+
+ )} +
+ + + ); + })} {/* 범례 (특보가 있을 때만 표시) */} diff --git a/frontend/components/screen/widgets/FlowWidget.tsx b/frontend/components/screen/widgets/FlowWidget.tsx index 69900000..7b10e071 100644 --- a/frontend/components/screen/widgets/FlowWidget.tsx +++ b/frontend/components/screen/widgets/FlowWidget.tsx @@ -127,6 +127,12 @@ export function FlowWidget({ // 컬럼 라벨 조회 const labelsResponse = await getStepColumnLabels(flowId, selectedStepId); + console.log("🔄 새로고침 시 컬럼 라벨 조회:", { + stepId: selectedStepId, + success: labelsResponse.success, + labelsCount: labelsResponse.data ? Object.keys(labelsResponse.data).length : 0, + labels: labelsResponse.data, + }); if (labelsResponse.success && labelsResponse.data) { setColumnLabels(labelsResponse.data); } @@ -220,6 +226,12 @@ export function FlowWidget({ try { // 컬럼 라벨 조회 const labelsResponse = await getStepColumnLabels(flowId!, firstStep.id); + console.log("🏷️ 첫 번째 스텝 컬럼 라벨 조회:", { + stepId: firstStep.id, + success: labelsResponse.success, + labelsCount: labelsResponse.data ? Object.keys(labelsResponse.data).length : 0, + labels: labelsResponse.data, + }); if (labelsResponse.success && labelsResponse.data) { setColumnLabels(labelsResponse.data); } @@ -297,9 +309,16 @@ export function FlowWidget({ try { // 컬럼 라벨 조회 const labelsResponse = await getStepColumnLabels(flowId!, stepId); + console.log("🏷️ 컬럼 라벨 조회 결과:", { + stepId, + success: labelsResponse.success, + labelsCount: labelsResponse.data ? Object.keys(labelsResponse.data).length : 0, + labels: labelsResponse.data, + }); if (labelsResponse.success && labelsResponse.data) { setColumnLabels(labelsResponse.data); } else { + console.warn("⚠️ 컬럼 라벨 조회 실패 또는 데이터 없음:", labelsResponse); setColumnLabels({}); }