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";
|
2025-10-01 12:06:24 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 대시보드 컨트롤러
|
|
|
|
|
* - REST API 엔드포인트 처리
|
|
|
|
|
* - 요청 검증 및 응답 포맷팅
|
|
|
|
|
*/
|
|
|
|
|
export class DashboardController {
|
|
|
|
|
/**
|
|
|
|
|
* 대시보드 생성
|
|
|
|
|
* POST /api/dashboards
|
|
|
|
|
*/
|
2025-10-15 10:02:32 +09:00
|
|
|
async createDashboard(
|
|
|
|
|
req: AuthenticatedRequest,
|
|
|
|
|
res: Response
|
|
|
|
|
): Promise<void> {
|
2025-10-01 12:06:24 +09:00
|
|
|
try {
|
|
|
|
|
const userId = req.user?.userId;
|
2025-10-21 15:53:17 +09:00
|
|
|
const companyCode = req.user?.companyCode;
|
|
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
if (!userId) {
|
|
|
|
|
res.status(401).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "인증이 필요합니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
elements,
|
|
|
|
|
isPublic = false,
|
|
|
|
|
tags,
|
|
|
|
|
category,
|
|
|
|
|
}: CreateDashboardRequest = req.body;
|
|
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
// 유효성 검증
|
|
|
|
|
if (!title || title.trim().length === 0) {
|
|
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "대시보드 제목이 필요합니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
if (!elements || !Array.isArray(elements)) {
|
|
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "대시보드 요소 데이터가 필요합니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
// 제목 길이 체크
|
|
|
|
|
if (title.length > 200) {
|
|
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "제목은 200자를 초과할 수 없습니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
// 설명 길이 체크
|
|
|
|
|
if (description && description.length > 1000) {
|
|
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "설명은 1000자를 초과할 수 없습니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
const dashboardData: CreateDashboardRequest = {
|
|
|
|
|
title: title.trim(),
|
|
|
|
|
description: description?.trim(),
|
|
|
|
|
isPublic,
|
2025-10-15 10:02:32 +09:00
|
|
|
elements,
|
|
|
|
|
tags,
|
|
|
|
|
category,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// console.log('대시보드 생성 시작:', { title: dashboardData.title, userId, elementsCount: elements.length });
|
|
|
|
|
|
|
|
|
|
const savedDashboard = await DashboardService.createDashboard(
|
|
|
|
|
dashboardData,
|
2025-10-21 15:53:17 +09:00
|
|
|
userId,
|
|
|
|
|
companyCode
|
2025-10-15 10:02:32 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// console.log('대시보드 생성 성공:', { id: savedDashboard.id, title: savedDashboard.title });
|
|
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
res.status(201).json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: savedDashboard,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "대시보드가 성공적으로 생성되었습니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
} 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-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
/**
|
|
|
|
|
* 대시보드 목록 조회
|
|
|
|
|
* GET /api/dashboards
|
|
|
|
|
*/
|
|
|
|
|
async getDashboards(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
const userId = req.user?.userId;
|
2025-10-21 15:53:17 +09:00
|
|
|
const companyCode = req.user?.companyCode;
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +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-01 12:06:24 +09:00
|
|
|
};
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
// 페이지 번호 유효성 검증
|
|
|
|
|
if (query.page! < 1) {
|
|
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "페이지 번호는 1 이상이어야 합니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-21 15:53:17 +09:00
|
|
|
const result = await DashboardService.getDashboards(
|
|
|
|
|
query,
|
|
|
|
|
userId,
|
|
|
|
|
companyCode
|
|
|
|
|
);
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: result.dashboards,
|
2025-10-15 10:02:32 +09:00
|
|
|
pagination: result.pagination,
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
} 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-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
/**
|
|
|
|
|
* 대시보드 상세 조회
|
|
|
|
|
* GET /api/dashboards/:id
|
|
|
|
|
*/
|
|
|
|
|
async getDashboard(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const userId = req.user?.userId;
|
2025-10-21 15:53:17 +09:00
|
|
|
const companyCode = req.user?.companyCode;
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
if (!id) {
|
|
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "대시보드 ID가 필요합니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-21 15:53:17 +09:00
|
|
|
const dashboard = await DashboardService.getDashboardById(
|
|
|
|
|
id,
|
|
|
|
|
userId,
|
|
|
|
|
companyCode
|
|
|
|
|
);
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
if (!dashboard) {
|
|
|
|
|
res.status(404).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "대시보드를 찾을 수 없거나 접근 권한이 없습니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
// 조회수 증가 (본인이 만든 대시보드가 아닌 경우에만)
|
|
|
|
|
if (userId && dashboard.createdBy !== userId) {
|
|
|
|
|
await DashboardService.incrementViewCount(id);
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
res.json({
|
|
|
|
|
success: true,
|
2025-10-15 10:02:32 +09:00
|
|
|
data: dashboard,
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
} 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-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
/**
|
|
|
|
|
* 대시보드 수정
|
|
|
|
|
* PUT /api/dashboards/:id
|
|
|
|
|
*/
|
2025-10-15 10:02:32 +09:00
|
|
|
async updateDashboard(
|
|
|
|
|
req: AuthenticatedRequest,
|
|
|
|
|
res: Response
|
|
|
|
|
): Promise<void> {
|
2025-10-01 12:06:24 +09:00
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const userId = req.user?.userId;
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
if (!userId) {
|
|
|
|
|
res.status(401).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "인증이 필요합니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
if (!id) {
|
|
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "대시보드 ID가 필요합니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
const updateData: UpdateDashboardRequest = req.body;
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
// 유효성 검증
|
|
|
|
|
if (updateData.title !== undefined) {
|
2025-10-15 10:02:32 +09:00
|
|
|
if (
|
|
|
|
|
typeof updateData.title !== "string" ||
|
|
|
|
|
updateData.title.trim().length === 0
|
|
|
|
|
) {
|
2025-10-01 12:06:24 +09:00
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "올바른 제목을 입력해주세요.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (updateData.title.length > 200) {
|
|
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "제목은 200자를 초과할 수 없습니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
updateData.title = updateData.title.trim();
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
updateData.description !== undefined &&
|
|
|
|
|
updateData.description &&
|
|
|
|
|
updateData.description.length > 1000
|
|
|
|
|
) {
|
2025-10-01 12:06:24 +09:00
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "설명은 1000자를 초과할 수 없습니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
|
|
|
|
const updatedDashboard = await DashboardService.updateDashboard(
|
|
|
|
|
id,
|
|
|
|
|
updateData,
|
|
|
|
|
userId
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
if (!updatedDashboard) {
|
|
|
|
|
res.status(404).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "대시보드를 찾을 수 없거나 수정 권한이 없습니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: updatedDashboard,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "대시보드가 성공적으로 수정되었습니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// console.error('Dashboard update error:', error);
|
2025-10-15 10:02:32 +09:00
|
|
|
|
|
|
|
|
if ((error as Error).message.includes("권한이 없습니다")) {
|
2025-10-01 12:06:24 +09:00
|
|
|
res.status(403).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: (error as Error).message,
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +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-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
/**
|
|
|
|
|
* 대시보드 삭제
|
|
|
|
|
* DELETE /api/dashboards/:id
|
|
|
|
|
*/
|
2025-10-15 10:02:32 +09:00
|
|
|
async deleteDashboard(
|
|
|
|
|
req: AuthenticatedRequest,
|
|
|
|
|
res: Response
|
|
|
|
|
): Promise<void> {
|
2025-10-01 12:06:24 +09:00
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const userId = req.user?.userId;
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
if (!userId) {
|
|
|
|
|
res.status(401).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "인증이 필요합니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
if (!id) {
|
|
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "대시보드 ID가 필요합니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
const deleted = await DashboardService.deleteDashboard(id, userId);
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
if (!deleted) {
|
|
|
|
|
res.status(404).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "대시보드를 찾을 수 없거나 삭제 권한이 없습니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
res.json({
|
|
|
|
|
success: true,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "대시보드가 성공적으로 삭제되었습니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
} 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-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
/**
|
|
|
|
|
* 내 대시보드 목록 조회
|
|
|
|
|
* GET /api/dashboards/my
|
|
|
|
|
*/
|
2025-10-15 10:02:32 +09:00
|
|
|
async getMyDashboards(
|
|
|
|
|
req: AuthenticatedRequest,
|
|
|
|
|
res: Response
|
|
|
|
|
): Promise<void> {
|
2025-10-01 12:06:24 +09:00
|
|
|
try {
|
|
|
|
|
const userId = req.user?.userId;
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
if (!userId) {
|
|
|
|
|
res.status(401).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "인증이 필요합니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-21 15:53:17 +09:00
|
|
|
const companyCode = req.user?.companyCode;
|
|
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
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-01 12:06:24 +09:00
|
|
|
};
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-21 15:53:17 +09:00
|
|
|
const result = await DashboardService.getDashboards(
|
|
|
|
|
query,
|
|
|
|
|
userId,
|
|
|
|
|
companyCode
|
|
|
|
|
);
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: result.dashboards,
|
2025-10-15 10:02:32 +09:00
|
|
|
pagination: result.pagination,
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
} 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,
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-23 15:11:10 +09:00
|
|
|
* 쿼리 실행 (SELECT만)
|
2025-10-01 12:06:24 +09:00
|
|
|
* 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-01 12:06:24 +09:00
|
|
|
// 유효성 검증
|
2025-10-15 10:02:32 +09:00
|
|
|
if (!query || typeof query !== "string" || query.trim().length === 0) {
|
2025-10-01 12:06:24 +09:00
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "쿼리가 필요합니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SQL 인젝션 방지를 위한 기본적인 검증
|
|
|
|
|
const trimmedQuery = query.trim().toLowerCase();
|
2025-10-15 10:02:32 +09:00
|
|
|
if (!trimmedQuery.startsWith("select")) {
|
2025-10-01 12:06:24 +09:00
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "SELECT 쿼리만 허용됩니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 쿼리 실행
|
|
|
|
|
const result = await PostgreSQLService.query(query.trim());
|
2025-10-15 10:02:32 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
// 결과 변환
|
2025-10-15 10:02:32 +09:00
|
|
|
const columns = result.fields?.map((field) => field.name) || [];
|
2025-10-01 12:06:24 +09:00
|
|
|
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-01 12:06:24 +09:00
|
|
|
},
|
2025-10-15 10:02:32 +09:00
|
|
|
message: "쿼리가 성공적으로 실행되었습니다.",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
} 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
|
|
|
|
|
: "쿼리 실행 오류",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-23 15:11:10 +09:00
|
|
|
/**
|
|
|
|
|
* 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 호출
|
2025-10-17 15:31:23 +09:00
|
|
|
// @ts-ignore - node-fetch dynamic import
|
2025-10-15 10:02:32 +09:00
|
|
|
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 호출 오류",
|
2025-10-01 12:06:24 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
}
|