ERP-node/backend-node/src/controllers/DashboardController.ts

784 lines
21 KiB
TypeScript
Raw Normal View History

2025-10-15 10:02:32 +09:00
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
*/
2025-10-15 10:02:32 +09:00
async createDashboard(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const userId = req.user?.userId;
const companyCode = req.user?.companyCode;
if (!userId) {
res.status(401).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "인증이 필요합니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
const {
title,
description,
elements,
isPublic = false,
tags,
category,
2025-11-21 10:29:47 +09:00
settings,
2025-10-15 10:02:32 +09:00
}: CreateDashboardRequest = req.body;
// 유효성 검증
if (!title || title.trim().length === 0) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "대시보드 제목이 필요합니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
if (!elements || !Array.isArray(elements)) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "대시보드 요소 데이터가 필요합니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
// 제목 길이 체크
if (title.length > 200) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "제목은 200자를 초과할 수 없습니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
// 설명 길이 체크
if (description && description.length > 1000) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "설명은 1000자를 초과할 수 없습니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
const dashboardData: CreateDashboardRequest = {
title: title.trim(),
description: description?.trim(),
isPublic,
2025-10-15 10:02:32 +09:00
elements,
tags,
category,
2025-11-21 10:29:47 +09:00
settings,
2025-10-15 10:02:32 +09:00
};
// console.log('대시보드 생성 시작:', { title: dashboardData.title, userId, elementsCount: elements.length });
const savedDashboard = await DashboardService.createDashboard(
dashboardData,
userId,
companyCode
2025-10-15 10:02:32 +09:00
);
// console.log('대시보드 생성 성공:', { id: savedDashboard.id, title: savedDashboard.title });
res.status(201).json({
success: true,
data: savedDashboard,
2025-10-15 10:02:32 +09:00
message: "대시보드가 성공적으로 생성되었습니다.",
});
} catch (error: any) {
// console.error('Dashboard creation error:', {
// message: error?.message,
// stack: error?.stack,
// error
// });
res.status(500).json({
success: false,
2025-10-15 10:02:32 +09:00
message: error?.message || "대시보드 생성 중 오류가 발생했습니다.",
error:
process.env.NODE_ENV === "development" ? error?.message : undefined,
});
}
}
2025-10-15 10:02:32 +09:00
/**
*
* GET /api/dashboards
*/
async getDashboards(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const userId = req.user?.userId;
const companyCode = req.user?.companyCode;
2025-10-15 10:02:32 +09:00
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,
2025-10-15 10:02:32 +09:00
isPublic:
req.query.isPublic === "true"
? true
: req.query.isPublic === "false"
? false
: undefined,
createdBy: req.query.createdBy as string,
};
2025-10-15 10:02:32 +09:00
// 페이지 번호 유효성 검증
if (query.page! < 1) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "페이지 번호는 1 이상이어야 합니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
const result = await DashboardService.getDashboards(
query,
userId,
companyCode
);
2025-10-15 10:02:32 +09:00
res.json({
success: true,
data: result.dashboards,
2025-10-15 10:02:32 +09:00
pagination: result.pagination,
});
} catch (error) {
// console.error('Dashboard list error:', error);
res.status(500).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "대시보드 목록 조회 중 오류가 발생했습니다.",
error:
process.env.NODE_ENV === "development"
? (error as Error).message
: undefined,
});
}
}
2025-10-15 10:02:32 +09:00
/**
*
* GET /api/dashboards/:id
*/
async getDashboard(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { id } = req.params;
const userId = req.user?.userId;
const companyCode = req.user?.companyCode;
2025-10-15 10:02:32 +09:00
if (!id) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "대시보드 ID가 필요합니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
const dashboard = await DashboardService.getDashboardById(
id,
userId,
companyCode
);
2025-10-15 10:02:32 +09:00
if (!dashboard) {
res.status(404).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "대시보드를 찾을 수 없거나 접근 권한이 없습니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
// 조회수 증가 (본인이 만든 대시보드가 아닌 경우에만)
if (userId && dashboard.createdBy !== userId) {
await DashboardService.incrementViewCount(id);
}
2025-10-15 10:02:32 +09:00
res.json({
success: true,
2025-10-15 10:02:32 +09:00
data: dashboard,
});
} catch (error) {
// console.error('Dashboard get error:', error);
res.status(500).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "대시보드 조회 중 오류가 발생했습니다.",
error:
process.env.NODE_ENV === "development"
? (error as Error).message
: undefined,
});
}
}
2025-10-15 10:02:32 +09:00
/**
*
* PUT /api/dashboards/:id
*/
2025-10-15 10:02:32 +09:00
async updateDashboard(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { id } = req.params;
const userId = req.user?.userId;
2025-10-15 10:02:32 +09:00
if (!userId) {
res.status(401).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "인증이 필요합니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
if (!id) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "대시보드 ID가 필요합니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
const updateData: UpdateDashboardRequest = req.body;
2025-10-15 10:02:32 +09:00
// 유효성 검증
if (updateData.title !== undefined) {
2025-10-15 10:02:32 +09:00
if (
typeof updateData.title !== "string" ||
updateData.title.trim().length === 0
) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "올바른 제목을 입력해주세요.",
});
return;
}
if (updateData.title.length > 200) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "제목은 200자를 초과할 수 없습니다.",
});
return;
}
updateData.title = updateData.title.trim();
}
2025-10-15 10:02:32 +09:00
if (
updateData.description !== undefined &&
updateData.description &&
updateData.description.length > 1000
) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "설명은 1000자를 초과할 수 없습니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
const updatedDashboard = await DashboardService.updateDashboard(
id,
updateData,
userId
);
if (!updatedDashboard) {
res.status(404).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "대시보드를 찾을 수 없거나 수정 권한이 없습니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
res.json({
success: true,
data: updatedDashboard,
2025-10-15 10:02:32 +09:00
message: "대시보드가 성공적으로 수정되었습니다.",
});
} catch (error) {
// console.error('Dashboard update error:', error);
2025-10-15 10:02:32 +09:00
if ((error as Error).message.includes("권한이 없습니다")) {
res.status(403).json({
success: false,
2025-10-15 10:02:32 +09:00
message: (error as Error).message,
});
return;
}
2025-10-15 10:02:32 +09:00
res.status(500).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "대시보드 수정 중 오류가 발생했습니다.",
error:
process.env.NODE_ENV === "development"
? (error as Error).message
: undefined,
});
}
}
2025-10-15 10:02:32 +09:00
/**
*
* DELETE /api/dashboards/:id
*/
2025-10-15 10:02:32 +09:00
async deleteDashboard(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { id } = req.params;
const userId = req.user?.userId;
2025-10-15 10:02:32 +09:00
if (!userId) {
res.status(401).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "인증이 필요합니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
if (!id) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "대시보드 ID가 필요합니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
const deleted = await DashboardService.deleteDashboard(id, userId);
2025-10-15 10:02:32 +09:00
if (!deleted) {
res.status(404).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "대시보드를 찾을 수 없거나 삭제 권한이 없습니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
res.json({
success: true,
2025-10-15 10:02:32 +09:00
message: "대시보드가 성공적으로 삭제되었습니다.",
});
} catch (error) {
// console.error('Dashboard delete error:', error);
res.status(500).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "대시보드 삭제 중 오류가 발생했습니다.",
error:
process.env.NODE_ENV === "development"
? (error as Error).message
: undefined,
});
}
}
2025-10-15 10:02:32 +09:00
/**
*
* GET /api/dashboards/my
*/
2025-10-15 10:02:32 +09:00
async getMyDashboards(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const userId = req.user?.userId;
2025-10-15 10:02:32 +09:00
if (!userId) {
res.status(401).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "인증이 필요합니다.",
});
return;
}
2025-10-15 10:02:32 +09:00
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,
2025-10-15 10:02:32 +09:00
createdBy: userId, // 본인이 만든 대시보드만
};
2025-10-15 10:02:32 +09:00
const result = await DashboardService.getDashboards(
query,
userId,
companyCode
);
2025-10-15 10:02:32 +09:00
res.json({
success: true,
data: result.dashboards,
2025-10-15 10:02:32 +09:00
pagination: result.pagination,
});
} catch (error) {
// console.error('My dashboards error:', error);
res.status(500).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "내 대시보드 목록 조회 중 오류가 발생했습니다.",
error:
process.env.NODE_ENV === "development"
? (error as Error).message
: undefined,
});
}
}
/**
* (SELECT만)
* POST /api/dashboards/execute-query
*/
async executeQuery(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
// 개발용으로 인증 체크 제거
// const userId = req.user?.userId;
// if (!userId) {
// res.status(401).json({
// success: false,
// message: '인증이 필요합니다.'
// });
// return;
// }
const { query } = req.body;
2025-10-15 10:02:32 +09:00
// 유효성 검증
2025-10-15 10:02:32 +09:00
if (!query || typeof query !== "string" || query.trim().length === 0) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "쿼리가 필요합니다.",
});
return;
}
// SQL 인젝션 방지를 위한 기본적인 검증
const trimmedQuery = query.trim().toLowerCase();
2025-10-15 10:02:32 +09:00
if (!trimmedQuery.startsWith("select")) {
res.status(400).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "SELECT 쿼리만 허용됩니다.",
});
return;
}
// 쿼리 실행
const result = await PostgreSQLService.query(query.trim());
2025-10-15 10:02:32 +09:00
// 결과 변환
2025-10-15 10:02:32 +09:00
const columns = result.fields?.map((field) => field.name) || [];
const rows = result.rows || [];
res.status(200).json({
success: true,
data: {
columns,
rows,
2025-10-15 10:02:32 +09:00
rowCount: rows.length,
},
2025-10-15 10:02:32 +09:00
message: "쿼리가 성공적으로 실행되었습니다.",
});
} catch (error) {
// console.error('Query execution error:', error);
res.status(500).json({
success: false,
2025-10-15 10:02:32 +09:00
message: "쿼리 실행 중 오류가 발생했습니다.",
error:
process.env.NODE_ENV === "development"
? (error as Error).message
: "쿼리 실행 오류",
});
}
}
/**
* DML (INSERT, UPDATE, DELETE)
* POST /api/dashboards/execute-dml
*/
async executeDML(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
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();
const allowedCommands = ["insert", "update", "delete"];
const isAllowed = allowedCommands.some((cmd) =>
trimmedQuery.startsWith(cmd)
);
if (!isAllowed) {
res.status(400).json({
success: false,
message: "INSERT, UPDATE, DELETE 쿼리만 허용됩니다.",
});
return;
}
// 위험한 명령어 차단
const dangerousPatterns = [
/drop\s+table/i,
/drop\s+database/i,
/truncate/i,
/alter\s+table/i,
/create\s+table/i,
];
if (dangerousPatterns.some((pattern) => pattern.test(query))) {
res.status(403).json({
success: false,
message: "허용되지 않는 쿼리입니다.",
});
return;
}
// 쿼리 실행
const result = await PostgreSQLService.query(query.trim());
res.status(200).json({
success: true,
data: {
rowCount: result.rowCount || 0,
command: result.command,
},
message: "쿼리가 성공적으로 실행되었습니다.",
});
} catch (error) {
console.error("DML execution error:", error);
res.status(500).json({
success: false,
message: "쿼리 실행 중 오류가 발생했습니다.",
error:
process.env.NODE_ENV === "development"
? (error as Error).message
: "쿼리 실행 오류",
});
}
}
2025-10-15 10:02:32 +09:00
/**
* API (CORS )
* POST /api/dashboards/fetch-external-api
*/
async fetchExternalApi(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
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 호출 (타임아웃 30초)
// @ts-ignore - node-fetch dynamic import
2025-10-15 10:02:32 +09:00
const fetch = (await import("node-fetch")).default;
// 타임아웃 설정 (Node.js 글로벌 AbortController 사용)
const controller = new (global as any).AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60초 (기상청 API는 느림)
let response;
try {
response = await fetch(urlObj.toString(), {
method: method.toUpperCase(),
headers: {
"Content-Type": "application/json",
...headers,
},
signal: controller.signal,
});
clearTimeout(timeoutId);
} catch (err: any) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
throw new Error('외부 API 요청 타임아웃 (30초 초과)');
}
throw err;
}
2025-10-15 10:02:32 +09:00
if (!response.ok) {
throw new Error(
`외부 API 오류: ${response.status} ${response.statusText}`
);
}
// Content-Type에 따라 응답 파싱
const contentType = response.headers.get("content-type");
let data: any;
// 한글 인코딩 처리 (EUC-KR → UTF-8)
const isKoreanApi = urlObj.hostname.includes('kma.go.kr') ||
urlObj.hostname.includes('data.go.kr');
if (isKoreanApi) {
// 한국 정부 API는 EUC-KR 인코딩 사용
const buffer = await response.arrayBuffer();
const decoder = new TextDecoder('euc-kr');
const text = decoder.decode(buffer);
try {
data = JSON.parse(text);
} catch {
data = { text, contentType };
}
} else if (contentType && contentType.includes("application/json")) {
data = await response.json();
} else if (contentType && contentType.includes("text/")) {
// 텍스트 응답 (CSV, 일반 텍스트 등)
const text = await response.text();
data = { text, contentType };
} else {
// 기타 응답 (JSON으로 시도)
try {
data = await response.json();
} catch {
const text = await response.text();
data = { text, contentType };
}
}
2025-10-15 10:02:32 +09:00
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 호출 오류",
});
}
}
2025-10-15 15:05:20 +09:00
/**
* ( )
* POST /api/dashboards/table-schema
*/
async getTableSchema(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
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
: "스키마 조회 오류",
});
}
}
2025-10-15 10:02:32 +09:00
}