import { Response } from "express"; import { AuthenticatedRequest } from "../middleware/authMiddleware"; import { DashboardService } from "../services/DashboardService"; import { CreateDashboardRequest, UpdateDashboardRequest, DashboardListQuery, } from "../types/dashboard"; import { PostgreSQLService } from "../database/PostgreSQLService"; /** * 대시보드 컨트롤러 * - REST API 엔드포인트 처리 * - 요청 검증 및 응답 포맷팅 */ export class DashboardController { /** * 대시보드 생성 * POST /api/dashboards */ async createDashboard( req: AuthenticatedRequest, res: Response ): Promise { try { const userId = req.user?.userId; const companyCode = req.user?.companyCode; if (!userId) { res.status(401).json({ success: false, message: "인증이 필요합니다.", }); return; } const { title, description, elements, isPublic = false, tags, category, }: CreateDashboardRequest = req.body; // 유효성 검증 if (!title || title.trim().length === 0) { res.status(400).json({ success: false, message: "대시보드 제목이 필요합니다.", }); return; } if (!elements || !Array.isArray(elements)) { res.status(400).json({ success: false, message: "대시보드 요소 데이터가 필요합니다.", }); return; } // 제목 길이 체크 if (title.length > 200) { res.status(400).json({ success: false, message: "제목은 200자를 초과할 수 없습니다.", }); return; } // 설명 길이 체크 if (description && description.length > 1000) { res.status(400).json({ success: false, message: "설명은 1000자를 초과할 수 없습니다.", }); return; } const dashboardData: CreateDashboardRequest = { title: title.trim(), description: description?.trim(), isPublic, elements, tags, category, }; // console.log('대시보드 생성 시작:', { title: dashboardData.title, userId, elementsCount: elements.length }); const savedDashboard = await DashboardService.createDashboard( dashboardData, userId, companyCode ); // console.log('대시보드 생성 성공:', { id: savedDashboard.id, title: savedDashboard.title }); res.status(201).json({ success: true, data: savedDashboard, message: "대시보드가 성공적으로 생성되었습니다.", }); } catch (error: any) { // console.error('Dashboard creation error:', { // message: error?.message, // stack: error?.stack, // error // }); res.status(500).json({ success: false, message: error?.message || "대시보드 생성 중 오류가 발생했습니다.", error: process.env.NODE_ENV === "development" ? error?.message : undefined, }); } } /** * 대시보드 목록 조회 * GET /api/dashboards */ async getDashboards(req: AuthenticatedRequest, res: Response): Promise { try { const userId = req.user?.userId; const companyCode = req.user?.companyCode; const query: DashboardListQuery = { page: parseInt(req.query.page as string) || 1, limit: Math.min(parseInt(req.query.limit as string) || 20, 100), // 최대 100개 search: req.query.search as string, category: req.query.category as string, isPublic: req.query.isPublic === "true" ? true : req.query.isPublic === "false" ? false : undefined, createdBy: req.query.createdBy as string, }; // 페이지 번호 유효성 검증 if (query.page! < 1) { res.status(400).json({ success: false, message: "페이지 번호는 1 이상이어야 합니다.", }); return; } const result = await DashboardService.getDashboards( query, userId, companyCode ); res.json({ success: true, data: result.dashboards, pagination: result.pagination, }); } catch (error) { // console.error('Dashboard list error:', error); res.status(500).json({ success: false, message: "대시보드 목록 조회 중 오류가 발생했습니다.", error: process.env.NODE_ENV === "development" ? (error as Error).message : undefined, }); } } /** * 대시보드 상세 조회 * GET /api/dashboards/:id */ async getDashboard(req: AuthenticatedRequest, res: Response): Promise { try { const { id } = req.params; const userId = req.user?.userId; const companyCode = req.user?.companyCode; if (!id) { res.status(400).json({ success: false, message: "대시보드 ID가 필요합니다.", }); return; } const dashboard = await DashboardService.getDashboardById( id, userId, companyCode ); if (!dashboard) { res.status(404).json({ success: false, message: "대시보드를 찾을 수 없거나 접근 권한이 없습니다.", }); return; } // 조회수 증가 (본인이 만든 대시보드가 아닌 경우에만) if (userId && dashboard.createdBy !== userId) { await DashboardService.incrementViewCount(id); } res.json({ success: true, data: dashboard, }); } catch (error) { // console.error('Dashboard get error:', error); res.status(500).json({ success: false, message: "대시보드 조회 중 오류가 발생했습니다.", error: process.env.NODE_ENV === "development" ? (error as Error).message : undefined, }); } } /** * 대시보드 수정 * PUT /api/dashboards/:id */ async updateDashboard( req: AuthenticatedRequest, res: Response ): Promise { try { const { id } = req.params; const userId = req.user?.userId; if (!userId) { res.status(401).json({ success: false, message: "인증이 필요합니다.", }); return; } if (!id) { res.status(400).json({ success: false, message: "대시보드 ID가 필요합니다.", }); return; } const updateData: UpdateDashboardRequest = req.body; // 유효성 검증 if (updateData.title !== undefined) { if ( typeof updateData.title !== "string" || updateData.title.trim().length === 0 ) { res.status(400).json({ success: false, message: "올바른 제목을 입력해주세요.", }); return; } if (updateData.title.length > 200) { res.status(400).json({ success: false, message: "제목은 200자를 초과할 수 없습니다.", }); return; } updateData.title = updateData.title.trim(); } if ( updateData.description !== undefined && updateData.description && updateData.description.length > 1000 ) { res.status(400).json({ success: false, message: "설명은 1000자를 초과할 수 없습니다.", }); return; } const updatedDashboard = await DashboardService.updateDashboard( id, updateData, userId ); if (!updatedDashboard) { res.status(404).json({ success: false, message: "대시보드를 찾을 수 없거나 수정 권한이 없습니다.", }); return; } res.json({ success: true, data: updatedDashboard, message: "대시보드가 성공적으로 수정되었습니다.", }); } catch (error) { // console.error('Dashboard update error:', error); if ((error as Error).message.includes("권한이 없습니다")) { res.status(403).json({ success: false, message: (error as Error).message, }); return; } res.status(500).json({ success: false, message: "대시보드 수정 중 오류가 발생했습니다.", error: process.env.NODE_ENV === "development" ? (error as Error).message : undefined, }); } } /** * 대시보드 삭제 * DELETE /api/dashboards/:id */ async deleteDashboard( req: AuthenticatedRequest, res: Response ): Promise { try { const { id } = req.params; const userId = req.user?.userId; if (!userId) { res.status(401).json({ success: false, message: "인증이 필요합니다.", }); return; } if (!id) { res.status(400).json({ success: false, message: "대시보드 ID가 필요합니다.", }); return; } const deleted = await DashboardService.deleteDashboard(id, userId); if (!deleted) { res.status(404).json({ success: false, message: "대시보드를 찾을 수 없거나 삭제 권한이 없습니다.", }); return; } res.json({ success: true, message: "대시보드가 성공적으로 삭제되었습니다.", }); } catch (error) { // console.error('Dashboard delete error:', error); res.status(500).json({ success: false, message: "대시보드 삭제 중 오류가 발생했습니다.", error: process.env.NODE_ENV === "development" ? (error as Error).message : undefined, }); } } /** * 내 대시보드 목록 조회 * GET /api/dashboards/my */ async getMyDashboards( req: AuthenticatedRequest, res: Response ): Promise { try { const userId = req.user?.userId; if (!userId) { res.status(401).json({ success: false, message: "인증이 필요합니다.", }); return; } const companyCode = req.user?.companyCode; const query: DashboardListQuery = { page: parseInt(req.query.page as string) || 1, limit: Math.min(parseInt(req.query.limit as string) || 20, 100), search: req.query.search as string, category: req.query.category as string, createdBy: userId, // 본인이 만든 대시보드만 }; const result = await DashboardService.getDashboards( query, userId, companyCode ); res.json({ success: true, data: result.dashboards, pagination: result.pagination, }); } catch (error) { // console.error('My dashboards error:', error); res.status(500).json({ success: false, message: "내 대시보드 목록 조회 중 오류가 발생했습니다.", error: process.env.NODE_ENV === "development" ? (error as Error).message : undefined, }); } } /** * 쿼리 실행 * POST /api/dashboards/execute-query */ async executeQuery(req: AuthenticatedRequest, res: Response): Promise { try { // 개발용으로 인증 체크 제거 // const userId = req.user?.userId; // if (!userId) { // res.status(401).json({ // success: false, // message: '인증이 필요합니다.' // }); // return; // } const { query } = req.body; // 유효성 검증 if (!query || typeof query !== "string" || query.trim().length === 0) { res.status(400).json({ success: false, message: "쿼리가 필요합니다.", }); return; } // SQL 인젝션 방지를 위한 기본적인 검증 const trimmedQuery = query.trim().toLowerCase(); if (!trimmedQuery.startsWith("select")) { res.status(400).json({ success: false, message: "SELECT 쿼리만 허용됩니다.", }); return; } // 쿼리 실행 const result = await PostgreSQLService.query(query.trim()); // 결과 변환 const columns = result.fields?.map((field) => field.name) || []; const rows = result.rows || []; res.status(200).json({ success: true, data: { columns, rows, rowCount: rows.length, }, message: "쿼리가 성공적으로 실행되었습니다.", }); } catch (error) { // console.error('Query execution error:', error); res.status(500).json({ success: false, message: "쿼리 실행 중 오류가 발생했습니다.", error: process.env.NODE_ENV === "development" ? (error as Error).message : "쿼리 실행 오류", }); } } /** * 외부 API 프록시 (CORS 우회용) * POST /api/dashboards/fetch-external-api */ async fetchExternalApi( req: AuthenticatedRequest, res: Response ): Promise { try { const { url, method = "GET", headers = {}, queryParams = {} } = req.body; if (!url || typeof url !== "string") { res.status(400).json({ success: false, message: "URL이 필요합니다.", }); return; } // 쿼리 파라미터 추가 const urlObj = new URL(url); Object.entries(queryParams).forEach(([key, value]) => { if (key && value) { urlObj.searchParams.append(key, String(value)); } }); // 외부 API 호출 // @ts-ignore - node-fetch dynamic import const fetch = (await import("node-fetch")).default; const response = await fetch(urlObj.toString(), { method: method.toUpperCase(), headers: { "Content-Type": "application/json", ...headers, }, }); if (!response.ok) { throw new Error( `외부 API 오류: ${response.status} ${response.statusText}` ); } const data = await response.json(); res.status(200).json({ success: true, data, }); } catch (error) { res.status(500).json({ success: false, message: "외부 API 호출 중 오류가 발생했습니다.", error: process.env.NODE_ENV === "development" ? (error as Error).message : "외부 API 호출 오류", }); } } /** * 테이블 스키마 조회 (날짜 컬럼 감지용) * POST /api/dashboards/table-schema */ async getTableSchema( req: AuthenticatedRequest, res: Response ): Promise { try { const { tableName } = req.body; if (!tableName || typeof tableName !== "string") { res.status(400).json({ success: false, message: "테이블명이 필요합니다.", }); return; } // 테이블명 검증 (SQL 인젝션 방지) if (!/^[a-z_][a-z0-9_]*$/i.test(tableName)) { res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", }); return; } // PostgreSQL information_schema에서 컬럼 정보 조회 const query = ` SELECT column_name, data_type, udt_name FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position `; const result = await PostgreSQLService.query(query, [ tableName.toLowerCase(), ]); // 날짜/시간 타입 컬럼 필터링 const dateColumns = result.rows .filter((row: any) => { const dataType = row.data_type?.toLowerCase(); const udtName = row.udt_name?.toLowerCase(); return ( dataType === "timestamp" || dataType === "timestamp without time zone" || dataType === "timestamp with time zone" || dataType === "date" || dataType === "time" || dataType === "time without time zone" || dataType === "time with time zone" || udtName === "timestamp" || udtName === "timestamptz" || udtName === "date" || udtName === "time" || udtName === "timetz" ); }) .map((row: any) => row.column_name); res.status(200).json({ success: true, data: { tableName, columns: result.rows.map((row: any) => ({ name: row.column_name, type: row.data_type, udtName: row.udt_name, })), dateColumns, }, }); } catch (error) { res.status(500).json({ success: false, message: "테이블 스키마 조회 중 오류가 발생했습니다.", error: process.env.NODE_ENV === "development" ? (error as Error).message : "스키마 조회 오류", }); } } }