import express from "express"; import { dataService } from "../services/dataService"; import { authenticateToken } from "../middleware/authMiddleware"; import { AuthenticatedRequest } from "../types/auth"; const router = express.Router(); /** * 조인 데이터 조회 API (다른 라우트보다 먼저 정의) * GET /api/data/join?leftTable=...&rightTable=...&leftColumn=...&rightColumn=...&leftValue=... */ router.get( "/join", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { leftTable, rightTable, leftColumn, rightColumn, leftValue } = req.query; // 입력값 검증 if (!leftTable || !rightTable || !leftColumn || !rightColumn) { return res.status(400).json({ success: false, message: "필수 파라미터가 누락되었습니다 (leftTable, rightTable, leftColumn, rightColumn).", error: "MISSING_PARAMETERS", }); } // SQL 인젝션 방지를 위한 검증 const tables = [leftTable as string, rightTable as string]; const columns = [leftColumn as string, rightColumn as string]; for (const table of tables) { if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) { return res.status(400).json({ success: false, message: `유효하지 않은 테이블명입니다: ${table}`, error: "INVALID_TABLE_NAME", }); } } for (const column of columns) { if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(column)) { return res.status(400).json({ success: false, message: `유효하지 않은 컬럼명입니다: ${column}`, error: "INVALID_COLUMN_NAME", }); } } // 회사 코드 추출 (멀티테넌시 필터링) const userCompany = req.user?.companyCode; console.log(`🔗 조인 데이터 조회:`, { leftTable, rightTable, leftColumn, rightColumn, leftValue, userCompany, }); // 조인 데이터 조회 (회사 코드 전달) const result = await dataService.getJoinedData( leftTable as string, rightTable as string, leftColumn as string, rightColumn as string, leftValue as string, userCompany ); if (!result.success) { return res.status(400).json(result); } console.log( `✅ 조인 데이터 조회 성공: ${result.data?.length || 0}개 항목` ); return res.json({ success: true, data: result.data, }); } catch (error) { console.error("조인 데이터 조회 오류:", error); return res.status(500).json({ success: false, message: "조인 데이터 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 동적 테이블 데이터 조회 API * GET /api/data/{tableName} */ router.get( "/:tableName", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName } = req.params; const { limit, offset, page, size, orderBy, searchTerm, sortBy, sortOrder, userLang, ...filters } = req.query; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } // SQL 인젝션 방지를 위한 테이블명 검증 if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } // page/size 또는 limit/offset 방식 지원 let finalLimit = 100; let finalOffset = 0; if (page && size) { // page/size 방식 const pageNum = parseInt(page as string) || 1; const sizeNum = parseInt(size as string) || 100; finalLimit = sizeNum; finalOffset = (pageNum - 1) * sizeNum; } else if (limit || offset) { // limit/offset 방식 finalLimit = parseInt(limit as string) || 10; finalOffset = parseInt(offset as string) || 0; } console.log(`📊 데이터 조회 요청: ${tableName}`, { limit: finalLimit, offset: finalOffset, orderBy: orderBy || sortBy, searchTerm, filters, user: req.user?.userId, }); // filters에서 searchTerm과 sortOrder 제거 (이미 별도로 처리됨) const cleanFilters = { ...filters }; delete cleanFilters.searchTerm; delete cleanFilters.sortOrder; // 데이터 조회 const result = await dataService.getTableData({ tableName, limit: finalLimit, offset: finalOffset, orderBy: (orderBy || sortBy) as string, filters: cleanFilters as Record, userCompany: req.user?.companyCode, }); if (!result.success) { return res.status(400).json(result); } console.log( `✅ 데이터 조회 성공: ${tableName}, ${result.data?.length || 0}개 항목` ); // 페이징 정보 포함하여 반환 const total = result.data?.length || 0; const responsePage = finalLimit > 0 ? Math.floor(finalOffset / finalLimit) + 1 : 1; const responseSize = finalLimit; const totalPages = responseSize > 0 ? Math.ceil(total / responseSize) : 1; return res.json({ success: true, data: result.data, total, page: responsePage, size: responseSize, totalPages, }); } catch (error) { console.error("데이터 조회 오류:", error); return res.status(500).json({ success: false, message: "데이터 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 테이블 컬럼 정보 조회 API * GET /api/data/{tableName}/columns */ router.get( "/:tableName/columns", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName } = req.params; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } // SQL 인젝션 방지를 위한 테이블명 검증 if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } console.log(`📋 컬럼 정보 조회: ${tableName}`); // 컬럼 정보 조회 const result = await dataService.getTableColumns(tableName); if (!result.success) { return res.status(400).json(result); } console.log( `✅ 컬럼 정보 조회 성공: ${tableName}, ${result.data?.length || 0}개 컬럼` ); return res.json(result); } catch (error) { console.error("컬럼 정보 조회 오류:", error); return res.status(500).json({ success: false, message: "컬럼 정보 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 레코드 상세 조회 API * GET /api/data/{tableName}/{id} */ router.get( "/:tableName/:id", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName, id } = req.params; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } console.log(`🔍 레코드 상세 조회: ${tableName}/${id}`); // 레코드 상세 조회 const result = await dataService.getRecordDetail(tableName, id); if (!result.success) { return res.status(400).json(result); } if (!result.data) { return res.status(404).json({ success: false, message: "레코드를 찾을 수 없습니다.", }); } console.log(`✅ 레코드 조회 성공: ${tableName}/${id}`); return res.json({ success: true, data: result.data, }); } catch (error) { console.error("레코드 상세 조회 오류:", error); return res.status(500).json({ success: false, message: "레코드 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 레코드 생성 API * POST /api/data/{tableName} */ router.post( "/:tableName", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName } = req.params; const data = req.body; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } console.log(`➕ 레코드 생성: ${tableName}`, data); // company_code와 company_name 자동 추가 (멀티테넌시) const enrichedData = { ...data }; // 테이블에 company_code 컬럼이 있는지 확인하고 자동으로 추가 const hasCompanyCode = await dataService.checkColumnExists(tableName, "company_code"); if (hasCompanyCode && req.user?.companyCode) { enrichedData.company_code = req.user.companyCode; console.log(`🏢 company_code 자동 추가: ${req.user.companyCode}`); } // 테이블에 company_name 컬럼이 있는지 확인하고 자동으로 추가 const hasCompanyName = await dataService.checkColumnExists(tableName, "company_name"); if (hasCompanyName && req.user?.companyName) { enrichedData.company_name = req.user.companyName; console.log(`🏢 company_name 자동 추가: ${req.user.companyName}`); } // 레코드 생성 const result = await dataService.createRecord(tableName, enrichedData); if (!result.success) { return res.status(400).json(result); } console.log(`✅ 레코드 생성 성공: ${tableName}`); return res.status(201).json({ success: true, data: result.data, message: "레코드가 생성되었습니다.", }); } catch (error) { console.error("레코드 생성 오류:", error); return res.status(500).json({ success: false, message: "레코드 생성 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 레코드 수정 API * PUT /api/data/{tableName}/{id} */ router.put( "/:tableName/:id", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName, id } = req.params; const data = req.body; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } console.log(`✏️ 레코드 수정: ${tableName}/${id}`, data); // 레코드 수정 const result = await dataService.updateRecord(tableName, id, data); if (!result.success) { return res.status(400).json(result); } console.log(`✅ 레코드 수정 성공: ${tableName}/${id}`); return res.json({ success: true, data: result.data, message: "레코드가 수정되었습니다.", }); } catch (error) { console.error("레코드 수정 오류:", error); return res.status(500).json({ success: false, message: "레코드 수정 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 레코드 삭제 API * DELETE /api/data/{tableName}/{id} */ router.delete( "/:tableName/:id", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName, id } = req.params; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } console.log(`🗑️ 레코드 삭제: ${tableName}/${id}`); // 레코드 삭제 const result = await dataService.deleteRecord(tableName, id); if (!result.success) { return res.status(400).json(result); } console.log(`✅ 레코드 삭제 성공: ${tableName}/${id}`); return res.json({ success: true, message: "레코드가 삭제되었습니다.", }); } catch (error) { console.error("레코드 삭제 오류:", error); return res.status(500).json({ success: false, message: "레코드 삭제 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); export default router;