608 lines
18 KiB
TypeScript
608 lines
18 KiB
TypeScript
import { v4 as uuidv4 } from "uuid";
|
|
import { PostgreSQLService } from "../database/PostgreSQLService";
|
|
import {
|
|
Dashboard,
|
|
DashboardElement,
|
|
CreateDashboardRequest,
|
|
UpdateDashboardRequest,
|
|
DashboardListQuery,
|
|
} from "../types/dashboard";
|
|
|
|
/**
|
|
* 대시보드 서비스 - Raw Query 방식
|
|
* PostgreSQL 직접 연결을 통한 CRUD 작업
|
|
*/
|
|
export class DashboardService {
|
|
/**
|
|
* 대시보드 생성
|
|
*/
|
|
static async createDashboard(
|
|
data: CreateDashboardRequest,
|
|
userId: string
|
|
): Promise<Dashboard> {
|
|
const dashboardId = uuidv4();
|
|
const now = new Date();
|
|
|
|
try {
|
|
// 트랜잭션으로 대시보드와 요소들을 함께 생성
|
|
const result = await PostgreSQLService.transaction(async (client) => {
|
|
// 1. 대시보드 메인 정보 저장
|
|
await client.query(
|
|
`
|
|
INSERT INTO dashboards (
|
|
id, title, description, is_public, created_by,
|
|
created_at, updated_at, tags, category, view_count, settings
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
`,
|
|
[
|
|
dashboardId,
|
|
data.title,
|
|
data.description || null,
|
|
data.isPublic || false,
|
|
userId,
|
|
now,
|
|
now,
|
|
JSON.stringify(data.tags || []),
|
|
data.category || null,
|
|
0,
|
|
JSON.stringify(data.settings || {}),
|
|
]
|
|
);
|
|
|
|
// 2. 대시보드 요소들 저장
|
|
if (data.elements && data.elements.length > 0) {
|
|
for (let i = 0; i < data.elements.length; i++) {
|
|
const element = data.elements[i];
|
|
const elementId = uuidv4(); // 항상 새로운 UUID 생성
|
|
|
|
await client.query(
|
|
`
|
|
INSERT INTO dashboard_elements (
|
|
id, dashboard_id, element_type, element_subtype,
|
|
position_x, position_y, width, height,
|
|
title, custom_title, show_header, content, data_source_config, chart_config,
|
|
list_config, yard_config,
|
|
display_order, created_at, updated_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
|
`,
|
|
[
|
|
elementId,
|
|
dashboardId,
|
|
element.type,
|
|
element.subtype,
|
|
element.position.x,
|
|
element.position.y,
|
|
element.size.width,
|
|
element.size.height,
|
|
element.title,
|
|
element.customTitle || null,
|
|
element.showHeader !== false, // 기본값 true
|
|
element.content || null,
|
|
JSON.stringify(element.dataSource || {}),
|
|
JSON.stringify(element.chartConfig || {}),
|
|
JSON.stringify(element.listConfig || null),
|
|
JSON.stringify(element.yardConfig || null),
|
|
i,
|
|
now,
|
|
now,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
return dashboardId;
|
|
});
|
|
|
|
// 생성된 대시보드 반환
|
|
try {
|
|
const dashboard = await this.getDashboardById(dashboardId, userId);
|
|
if (!dashboard) {
|
|
console.error("대시보드 생성은 성공했으나 조회에 실패:", dashboardId);
|
|
// 생성은 성공했으므로 기본 정보만이라도 반환
|
|
return {
|
|
id: dashboardId,
|
|
title: data.title,
|
|
description: data.description,
|
|
thumbnailUrl: undefined,
|
|
isPublic: data.isPublic || false,
|
|
createdBy: userId,
|
|
createdAt: now.toISOString(),
|
|
updatedAt: now.toISOString(),
|
|
tags: data.tags || [],
|
|
category: data.category,
|
|
viewCount: 0,
|
|
elements: data.elements || [],
|
|
};
|
|
}
|
|
|
|
return dashboard;
|
|
} catch (fetchError) {
|
|
console.error("생성된 대시보드 조회 중 오류:", fetchError);
|
|
// 생성은 성공했으므로 기본 정보 반환
|
|
return {
|
|
id: dashboardId,
|
|
title: data.title,
|
|
description: data.description,
|
|
thumbnailUrl: undefined,
|
|
isPublic: data.isPublic || false,
|
|
createdBy: userId,
|
|
createdAt: now.toISOString(),
|
|
updatedAt: now.toISOString(),
|
|
tags: data.tags || [],
|
|
category: data.category,
|
|
viewCount: 0,
|
|
elements: data.elements || [],
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.error("Dashboard creation error:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 대시보드 목록 조회
|
|
*/
|
|
static async getDashboards(query: DashboardListQuery, userId?: string) {
|
|
const {
|
|
page = 1,
|
|
limit = 20,
|
|
search,
|
|
category,
|
|
isPublic,
|
|
createdBy,
|
|
} = query;
|
|
|
|
const offset = (page - 1) * limit;
|
|
|
|
try {
|
|
// 기본 WHERE 조건
|
|
let whereConditions = ["d.deleted_at IS NULL"];
|
|
let params: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
// 권한 필터링
|
|
if (userId) {
|
|
whereConditions.push(
|
|
`(d.created_by = $${paramIndex} OR d.is_public = true)`
|
|
);
|
|
params.push(userId);
|
|
paramIndex++;
|
|
} else {
|
|
whereConditions.push("d.is_public = true");
|
|
}
|
|
|
|
// 검색 조건
|
|
if (search) {
|
|
whereConditions.push(
|
|
`(d.title ILIKE $${paramIndex} OR d.description ILIKE $${paramIndex + 1})`
|
|
);
|
|
params.push(`%${search}%`, `%${search}%`);
|
|
paramIndex += 2;
|
|
}
|
|
|
|
// 카테고리 필터
|
|
if (category) {
|
|
whereConditions.push(`d.category = $${paramIndex}`);
|
|
params.push(category);
|
|
paramIndex++;
|
|
}
|
|
|
|
// 공개/비공개 필터
|
|
if (typeof isPublic === "boolean") {
|
|
whereConditions.push(`d.is_public = $${paramIndex}`);
|
|
params.push(isPublic);
|
|
paramIndex++;
|
|
}
|
|
|
|
// 작성자 필터
|
|
if (createdBy) {
|
|
whereConditions.push(`d.created_by = $${paramIndex}`);
|
|
params.push(createdBy);
|
|
paramIndex++;
|
|
}
|
|
|
|
const whereClause = whereConditions.join(" AND ");
|
|
|
|
// 대시보드 목록 조회 (users 테이블 조인 제거)
|
|
const dashboardQuery = `
|
|
SELECT
|
|
d.id,
|
|
d.title,
|
|
d.description,
|
|
d.thumbnail_url,
|
|
d.is_public,
|
|
d.created_by,
|
|
d.created_at,
|
|
d.updated_at,
|
|
d.tags,
|
|
d.category,
|
|
d.view_count,
|
|
COUNT(de.id) as elements_count
|
|
FROM dashboards d
|
|
LEFT JOIN dashboard_elements de ON d.id = de.dashboard_id
|
|
WHERE ${whereClause}
|
|
GROUP BY d.id, d.title, d.description, d.thumbnail_url, d.is_public,
|
|
d.created_by, d.created_at, d.updated_at, d.tags, d.category,
|
|
d.view_count
|
|
ORDER BY d.updated_at DESC
|
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
|
`;
|
|
|
|
const dashboardResult = await PostgreSQLService.query(dashboardQuery, [
|
|
...params,
|
|
limit,
|
|
offset,
|
|
]);
|
|
|
|
// 전체 개수 조회
|
|
const countQuery = `
|
|
SELECT COUNT(DISTINCT d.id) as total
|
|
FROM dashboards d
|
|
WHERE ${whereClause}
|
|
`;
|
|
|
|
const countResult = await PostgreSQLService.query(countQuery, params);
|
|
const total = parseInt(countResult.rows[0]?.total || "0");
|
|
|
|
return {
|
|
dashboards: dashboardResult.rows.map((row: any) => ({
|
|
id: row.id,
|
|
title: row.title,
|
|
description: row.description,
|
|
thumbnailUrl: row.thumbnail_url,
|
|
isPublic: row.is_public,
|
|
createdBy: row.created_by,
|
|
createdAt: row.created_at,
|
|
updatedAt: row.updated_at,
|
|
tags: JSON.parse(row.tags || "[]"),
|
|
category: row.category,
|
|
viewCount: parseInt(row.view_count || "0"),
|
|
elementsCount: parseInt(row.elements_count || "0"),
|
|
})),
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages: Math.ceil(total / limit),
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error("Dashboard list error:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 대시보드 상세 조회
|
|
*/
|
|
static async getDashboardById(
|
|
dashboardId: string,
|
|
userId?: string
|
|
): Promise<Dashboard | null> {
|
|
try {
|
|
// 1. 대시보드 기본 정보 조회 (권한 체크 포함)
|
|
let dashboardQuery: string;
|
|
let dashboardParams: any[];
|
|
|
|
if (userId) {
|
|
dashboardQuery = `
|
|
SELECT d.*
|
|
FROM dashboards d
|
|
WHERE d.id = $1 AND d.deleted_at IS NULL
|
|
AND (d.created_by = $2 OR d.is_public = true)
|
|
`;
|
|
dashboardParams = [dashboardId, userId];
|
|
} else {
|
|
dashboardQuery = `
|
|
SELECT d.*
|
|
FROM dashboards d
|
|
WHERE d.id = $1 AND d.deleted_at IS NULL
|
|
AND d.is_public = true
|
|
`;
|
|
dashboardParams = [dashboardId];
|
|
}
|
|
|
|
const dashboardResult = await PostgreSQLService.query(
|
|
dashboardQuery,
|
|
dashboardParams
|
|
);
|
|
|
|
if (dashboardResult.rows.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const dashboard = dashboardResult.rows[0];
|
|
|
|
// 2. 대시보드 요소들 조회
|
|
const elementsQuery = `
|
|
SELECT * FROM dashboard_elements
|
|
WHERE dashboard_id = $1
|
|
ORDER BY display_order ASC
|
|
`;
|
|
|
|
const elementsResult = await PostgreSQLService.query(elementsQuery, [
|
|
dashboardId,
|
|
]);
|
|
|
|
// 3. 요소 데이터 변환
|
|
const elements: DashboardElement[] = elementsResult.rows.map(
|
|
(row: any) => ({
|
|
id: row.id,
|
|
type: row.element_type,
|
|
subtype: row.element_subtype,
|
|
position: {
|
|
x: row.position_x,
|
|
y: row.position_y,
|
|
},
|
|
size: {
|
|
width: row.width,
|
|
height: row.height,
|
|
},
|
|
title: row.title,
|
|
customTitle: row.custom_title || undefined,
|
|
showHeader: row.show_header !== false, // 기본값 true
|
|
content: row.content,
|
|
dataSource: JSON.parse(row.data_source_config || "{}"),
|
|
chartConfig: JSON.parse(row.chart_config || "{}"),
|
|
listConfig: row.list_config
|
|
? typeof row.list_config === "string"
|
|
? JSON.parse(row.list_config)
|
|
: row.list_config
|
|
: undefined,
|
|
yardConfig: row.yard_config
|
|
? typeof row.yard_config === "string"
|
|
? JSON.parse(row.yard_config)
|
|
: row.yard_config
|
|
: undefined,
|
|
})
|
|
);
|
|
|
|
return {
|
|
id: dashboard.id,
|
|
title: dashboard.title,
|
|
description: dashboard.description,
|
|
thumbnailUrl: dashboard.thumbnail_url,
|
|
isPublic: dashboard.is_public,
|
|
createdBy: dashboard.created_by,
|
|
createdAt: dashboard.created_at,
|
|
updatedAt: dashboard.updated_at,
|
|
tags: JSON.parse(dashboard.tags || "[]"),
|
|
category: dashboard.category,
|
|
viewCount: parseInt(dashboard.view_count || "0"),
|
|
settings: dashboard.settings || undefined,
|
|
elements,
|
|
};
|
|
} catch (error) {
|
|
console.error("Dashboard get error:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 대시보드 업데이트
|
|
*/
|
|
static async updateDashboard(
|
|
dashboardId: string,
|
|
data: UpdateDashboardRequest,
|
|
userId: string
|
|
): Promise<Dashboard | null> {
|
|
try {
|
|
const result = await PostgreSQLService.transaction(async (client) => {
|
|
// 권한 체크
|
|
const authCheckResult = await client.query(
|
|
`
|
|
SELECT id FROM dashboards
|
|
WHERE id = $1 AND created_by = $2 AND deleted_at IS NULL
|
|
`,
|
|
[dashboardId, userId]
|
|
);
|
|
|
|
if (authCheckResult.rows.length === 0) {
|
|
throw new Error("대시보드를 찾을 수 없거나 수정 권한이 없습니다.");
|
|
}
|
|
|
|
const now = new Date();
|
|
|
|
// 1. 대시보드 메인 정보 업데이트
|
|
const updateFields: string[] = [];
|
|
const updateParams: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (data.title !== undefined) {
|
|
updateFields.push(`title = $${paramIndex}`);
|
|
updateParams.push(data.title);
|
|
paramIndex++;
|
|
}
|
|
if (data.description !== undefined) {
|
|
updateFields.push(`description = $${paramIndex}`);
|
|
updateParams.push(data.description);
|
|
paramIndex++;
|
|
}
|
|
if (data.isPublic !== undefined) {
|
|
updateFields.push(`is_public = $${paramIndex}`);
|
|
updateParams.push(data.isPublic);
|
|
paramIndex++;
|
|
}
|
|
if (data.tags !== undefined) {
|
|
updateFields.push(`tags = $${paramIndex}`);
|
|
updateParams.push(JSON.stringify(data.tags));
|
|
paramIndex++;
|
|
}
|
|
if (data.category !== undefined) {
|
|
updateFields.push(`category = $${paramIndex}`);
|
|
updateParams.push(data.category);
|
|
paramIndex++;
|
|
}
|
|
if (data.settings !== undefined) {
|
|
updateFields.push(`settings = $${paramIndex}`);
|
|
updateParams.push(JSON.stringify(data.settings));
|
|
paramIndex++;
|
|
}
|
|
|
|
updateFields.push(`updated_at = $${paramIndex}`);
|
|
updateParams.push(now);
|
|
paramIndex++;
|
|
|
|
updateParams.push(dashboardId);
|
|
|
|
if (updateFields.length > 1) {
|
|
// updated_at 외에 다른 필드가 있는 경우
|
|
const updateQuery = `
|
|
UPDATE dashboards
|
|
SET ${updateFields.join(", ")}
|
|
WHERE id = $${paramIndex}
|
|
`;
|
|
|
|
await client.query(updateQuery, updateParams);
|
|
}
|
|
|
|
// 2. 요소 업데이트 (있는 경우)
|
|
if (data.elements) {
|
|
// 기존 요소들 삭제
|
|
await client.query(
|
|
`
|
|
DELETE FROM dashboard_elements WHERE dashboard_id = $1
|
|
`,
|
|
[dashboardId]
|
|
);
|
|
|
|
// 새 요소들 추가
|
|
for (let i = 0; i < data.elements.length; i++) {
|
|
const element = data.elements[i];
|
|
const elementId = uuidv4();
|
|
|
|
await client.query(
|
|
`
|
|
INSERT INTO dashboard_elements (
|
|
id, dashboard_id, element_type, element_subtype,
|
|
position_x, position_y, width, height,
|
|
title, custom_title, show_header, content, data_source_config, chart_config,
|
|
list_config, yard_config,
|
|
display_order, created_at, updated_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
|
`,
|
|
[
|
|
elementId,
|
|
dashboardId,
|
|
element.type,
|
|
element.subtype,
|
|
element.position.x,
|
|
element.position.y,
|
|
element.size.width,
|
|
element.size.height,
|
|
element.title,
|
|
element.customTitle || null,
|
|
element.showHeader !== false, // 기본값 true
|
|
element.content || null,
|
|
JSON.stringify(element.dataSource || {}),
|
|
JSON.stringify(element.chartConfig || {}),
|
|
JSON.stringify(element.listConfig || null),
|
|
JSON.stringify(element.yardConfig || null),
|
|
i,
|
|
now,
|
|
now,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
return dashboardId;
|
|
});
|
|
|
|
// 업데이트된 대시보드 반환
|
|
return await this.getDashboardById(dashboardId, userId);
|
|
} catch (error) {
|
|
console.error("Dashboard update error:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 대시보드 삭제 (소프트 삭제)
|
|
*/
|
|
static async deleteDashboard(
|
|
dashboardId: string,
|
|
userId: string
|
|
): Promise<boolean> {
|
|
try {
|
|
const now = new Date();
|
|
|
|
const result = await PostgreSQLService.query(
|
|
`
|
|
UPDATE dashboards
|
|
SET deleted_at = $1, updated_at = $2
|
|
WHERE id = $3 AND created_by = $4 AND deleted_at IS NULL
|
|
`,
|
|
[now, now, dashboardId, userId]
|
|
);
|
|
|
|
return (result.rowCount || 0) > 0;
|
|
} catch (error) {
|
|
console.error("Dashboard delete error:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 조회수 증가
|
|
*/
|
|
static async incrementViewCount(dashboardId: string): Promise<void> {
|
|
try {
|
|
await PostgreSQLService.query(
|
|
`
|
|
UPDATE dashboards
|
|
SET view_count = view_count + 1
|
|
WHERE id = $1 AND deleted_at IS NULL
|
|
`,
|
|
[dashboardId]
|
|
);
|
|
} catch (error) {
|
|
console.error("View count increment error:", error);
|
|
// 조회수 증가 실패는 치명적이지 않으므로 에러를 던지지 않음
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 사용자 권한 체크
|
|
*/
|
|
static async checkUserPermission(
|
|
dashboardId: string,
|
|
userId: string,
|
|
requiredPermission: "view" | "edit" | "admin" = "view"
|
|
): Promise<boolean> {
|
|
try {
|
|
const result = await PostgreSQLService.query(
|
|
`
|
|
SELECT
|
|
CASE
|
|
WHEN d.created_by = $2 THEN 'admin'
|
|
WHEN d.is_public = true THEN 'view'
|
|
ELSE 'none'
|
|
END as permission
|
|
FROM dashboards d
|
|
WHERE d.id = $1 AND d.deleted_at IS NULL
|
|
`,
|
|
[dashboardId, userId]
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
const userPermission = result.rows[0].permission;
|
|
|
|
// 권한 레벨 체크
|
|
const permissionLevels = { view: 1, edit: 2, admin: 3 };
|
|
const userLevel =
|
|
permissionLevels[userPermission as keyof typeof permissionLevels] || 0;
|
|
const requiredLevel = permissionLevels[requiredPermission];
|
|
|
|
return userLevel >= requiredLevel;
|
|
} catch (error) {
|
|
console.error("Permission check error:", error);
|
|
return false;
|
|
}
|
|
}
|
|
}
|