settings 저장

This commit is contained in:
dohyeons 2025-10-16 10:27:43 +09:00
parent 337cc448d0
commit 9e1a7c53e1
3 changed files with 283 additions and 197 deletions

View File

@ -1,89 +1,98 @@
import { v4 as uuidv4 } from 'uuid';
import { PostgreSQLService } from '../database/PostgreSQLService';
import {
Dashboard,
DashboardElement,
CreateDashboardRequest,
import { v4 as uuidv4 } from "uuid";
import { PostgreSQLService } from "../database/PostgreSQLService";
import {
Dashboard,
DashboardElement,
CreateDashboardRequest,
UpdateDashboardRequest,
DashboardListQuery
} from '../types/dashboard';
DashboardListQuery,
} from "../types/dashboard";
/**
* - Raw Query
* PostgreSQL CRUD
*/
export class DashboardService {
/**
*
*/
static async createDashboard(data: CreateDashboardRequest, userId: string): Promise<Dashboard> {
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(`
await client.query(
`
INSERT INTO dashboards (
id, title, description, is_public, created_by,
created_at, updated_at, tags, category, view_count
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
`, [
dashboardId,
data.title,
data.description || null,
data.isPublic || false,
userId,
now,
now,
JSON.stringify(data.tags || []),
data.category || null,
0
]);
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(`
await client.query(
`
INSERT INTO dashboard_elements (
id, dashboard_id, element_type, element_subtype,
position_x, position_y, width, height,
title, content, data_source_config, chart_config,
display_order, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
`, [
elementId,
dashboardId,
element.type,
element.subtype,
element.position.x,
element.position.y,
element.size.width,
element.size.height,
element.title,
element.content || null,
JSON.stringify(element.dataSource || {}),
JSON.stringify(element.chartConfig || {}),
i,
now,
now
]);
`,
[
elementId,
dashboardId,
element.type,
element.subtype,
element.position.x,
element.position.y,
element.size.width,
element.size.height,
element.title,
element.content || null,
JSON.stringify(element.dataSource || {}),
JSON.stringify(element.chartConfig || {}),
i,
now,
now,
]
);
}
}
return dashboardId;
});
// 생성된 대시보드 반환
try {
const dashboard = await this.getDashboardById(dashboardId, userId);
if (!dashboard) {
console.error('대시보드 생성은 성공했으나 조회에 실패:', dashboardId);
console.error("대시보드 생성은 성공했으나 조회에 실패:", dashboardId);
// 생성은 성공했으므로 기본 정보만이라도 반환
return {
id: dashboardId,
@ -97,13 +106,13 @@ export class DashboardService {
tags: data.tags || [],
category: data.category,
viewCount: 0,
elements: data.elements || []
elements: data.elements || [],
};
}
return dashboard;
} catch (fetchError) {
console.error('생성된 대시보드 조회 중 오류:', fetchError);
console.error("생성된 대시보드 조회 중 오류:", fetchError);
// 생성은 성공했으므로 기본 정보 반환
return {
id: dashboardId,
@ -117,76 +126,79 @@ export class DashboardService {
tags: data.tags || [],
category: data.category,
viewCount: 0,
elements: data.elements || []
elements: data.elements || [],
};
}
} catch (error) {
console.error('Dashboard creation error:', 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
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 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)`);
whereConditions.push(
`(d.created_by = $${paramIndex} OR d.is_public = true)`
);
params.push(userId);
paramIndex++;
} else {
whereConditions.push('d.is_public = true');
whereConditions.push("d.is_public = true");
}
// 검색 조건
if (search) {
whereConditions.push(`(d.title ILIKE $${paramIndex} OR d.description ILIKE $${paramIndex + 1})`);
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') {
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 ');
const whereClause = whereConditions.join(" AND ");
// 대시보드 목록 조회 (users 테이블 조인 제거)
const dashboardQuery = `
SELECT
@ -211,22 +223,23 @@ export class DashboardService {
ORDER BY d.updated_at DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`;
const dashboardResult = await PostgreSQLService.query(
dashboardQuery,
[...params, limit, offset]
);
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');
const total = parseInt(countResult.rows[0]?.total || "0");
return {
dashboards: dashboardResult.rows.map((row: any) => ({
id: row.id,
@ -237,33 +250,36 @@ export class DashboardService {
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at,
tags: JSON.parse(row.tags || '[]'),
tags: JSON.parse(row.tags || "[]"),
category: row.category,
viewCount: parseInt(row.view_count || '0'),
elementsCount: parseInt(row.elements_count || '0')
viewCount: parseInt(row.view_count || "0"),
elementsCount: parseInt(row.elements_count || "0"),
})),
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
totalPages: Math.ceil(total / limit),
},
};
} catch (error) {
console.error('Dashboard list error:', error);
console.error("Dashboard list error:", error);
throw error;
}
}
/**
*
*/
static async getDashboardById(dashboardId: string, userId?: string): Promise<Dashboard | null> {
static async getDashboardById(
dashboardId: string,
userId?: string
): Promise<Dashboard | null> {
try {
// 1. 대시보드 기본 정보 조회 (권한 체크 포함)
let dashboardQuery: string;
let dashboardParams: any[];
if (userId) {
dashboardQuery = `
SELECT d.*
@ -281,43 +297,50 @@ export class DashboardService {
`;
dashboardParams = [dashboardId];
}
const dashboardResult = await PostgreSQLService.query(dashboardQuery, dashboardParams);
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]);
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,
content: row.content,
dataSource: JSON.parse(row.data_source_config || '{}'),
chartConfig: JSON.parse(row.chart_config || '{}')
}));
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,
content: row.content,
dataSource: JSON.parse(row.data_source_config || "{}"),
chartConfig: JSON.parse(row.chart_config || "{}"),
})
);
return {
id: dashboard.id,
title: dashboard.title,
@ -327,44 +350,47 @@ export class DashboardService {
createdBy: dashboard.created_by,
createdAt: dashboard.created_at,
updatedAt: dashboard.updated_at,
tags: JSON.parse(dashboard.tags || '[]'),
tags: JSON.parse(dashboard.tags || "[]"),
category: dashboard.category,
viewCount: parseInt(dashboard.view_count || '0'),
elements
viewCount: parseInt(dashboard.view_count || "0"),
elements,
};
} catch (error) {
console.error('Dashboard get error:', error);
console.error("Dashboard get error:", error);
throw error;
}
}
/**
*
*/
static async updateDashboard(
dashboardId: string,
data: UpdateDashboardRequest,
dashboardId: string,
data: UpdateDashboardRequest,
userId: string
): Promise<Dashboard | null> {
try {
const result = await PostgreSQLService.transaction(async (client) => {
// 권한 체크
const authCheckResult = await client.query(`
const authCheckResult = await client.query(
`
SELECT id FROM dashboards
WHERE id = $1 AND created_by = $2 AND deleted_at IS NULL
`, [dashboardId, userId]);
`,
[dashboardId, userId]
);
if (authCheckResult.rows.length === 0) {
throw new Error('대시보드를 찾을 수 없거나 수정 권한이 없습니다.');
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);
@ -390,120 +416,141 @@ export class DashboardService {
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 외에 다른 필드가 있는 경우
if (updateFields.length > 1) {
// updated_at 외에 다른 필드가 있는 경우
const updateQuery = `
UPDATE dashboards
SET ${updateFields.join(', ')}
SET ${updateFields.join(", ")}
WHERE id = $${paramIndex}
`;
await client.query(updateQuery, updateParams);
}
// 2. 요소 업데이트 (있는 경우)
if (data.elements) {
// 기존 요소들 삭제
await client.query(`
await client.query(
`
DELETE FROM dashboard_elements WHERE dashboard_id = $1
`, [dashboardId]);
`,
[dashboardId]
);
// 새 요소들 추가
for (let i = 0; i < data.elements.length; i++) {
const element = data.elements[i];
const elementId = uuidv4();
await client.query(`
await client.query(
`
INSERT INTO dashboard_elements (
id, dashboard_id, element_type, element_subtype,
position_x, position_y, width, height,
title, content, data_source_config, chart_config,
display_order, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
`, [
elementId,
dashboardId,
element.type,
element.subtype,
element.position.x,
element.position.y,
element.size.width,
element.size.height,
element.title,
element.content || null,
JSON.stringify(element.dataSource || {}),
JSON.stringify(element.chartConfig || {}),
i,
now,
now
]);
`,
[
elementId,
dashboardId,
element.type,
element.subtype,
element.position.x,
element.position.y,
element.size.width,
element.size.height,
element.title,
element.content || null,
JSON.stringify(element.dataSource || {}),
JSON.stringify(element.chartConfig || {}),
i,
now,
now,
]
);
}
}
return dashboardId;
});
// 업데이트된 대시보드 반환
return await this.getDashboardById(dashboardId, userId);
} catch (error) {
console.error('Dashboard update error:', error);
console.error("Dashboard update error:", error);
throw error;
}
}
/**
* ( )
*/
static async deleteDashboard(dashboardId: string, userId: string): Promise<boolean> {
static async deleteDashboard(
dashboardId: string,
userId: string
): Promise<boolean> {
try {
const now = new Date();
const result = await PostgreSQLService.query(`
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]);
`,
[now, now, dashboardId, userId]
);
return (result.rowCount || 0) > 0;
} catch (error) {
console.error('Dashboard delete error:', error);
console.error("Dashboard delete error:", error);
throw error;
}
}
/**
*
*/
static async incrementViewCount(dashboardId: string): Promise<void> {
try {
await PostgreSQLService.query(`
await PostgreSQLService.query(
`
UPDATE dashboards
SET view_count = view_count + 1
WHERE id = $1 AND deleted_at IS NULL
`, [dashboardId]);
`,
[dashboardId]
);
} catch (error) {
console.error('View count increment error:', error);
console.error("View count increment error:", error);
// 조회수 증가 실패는 치명적이지 않으므로 에러를 던지지 않음
}
}
/**
*
*/
static async checkUserPermission(
dashboardId: string,
userId: string,
requiredPermission: 'view' | 'edit' | 'admin' = 'view'
dashboardId: string,
userId: string,
requiredPermission: "view" | "edit" | "admin" = "view"
): Promise<boolean> {
try {
const result = await PostgreSQLService.query(`
const result = await PostgreSQLService.query(
`
SELECT
CASE
WHEN d.created_by = $2 THEN 'admin'
@ -512,23 +559,26 @@ export class DashboardService {
END as permission
FROM dashboards d
WHERE d.id = $1 AND d.deleted_at IS NULL
`, [dashboardId, userId]);
`,
[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 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);
console.error("Permission check error:", error);
return false;
}
}
}
}

View File

@ -33,7 +33,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
const [canvasBackgroundColor, setCanvasBackgroundColor] = useState<string>("#f9fafb");
const canvasRef = useRef<HTMLDivElement>(null);
// 화면 해상도 자동 감지 및 기본 해상도 설정
// 화면 해상도 자동 감지
const [screenResolution] = useState<Resolution>(() => detectScreenResolution());
const [resolution, setResolution] = useState<Resolution>(screenResolution);
@ -62,6 +62,22 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
setDashboardId(dashboard.id);
setDashboardTitle(dashboard.title);
// 저장된 설정 복원
console.log("🔍 로드된 대시보드:", dashboard);
console.log("📦 저장된 settings:", (dashboard as any).settings);
if ((dashboard as any).settings?.resolution) {
const savedResolution = (dashboard as any).settings.resolution as Resolution;
console.log("✅ 저장된 해상도 복원:", savedResolution);
setResolution(savedResolution);
} else {
console.log("⚠️ 저장된 해상도 없음");
}
if ((dashboard as any).settings?.backgroundColor) {
setCanvasBackgroundColor((dashboard as any).settings.backgroundColor);
}
// 요소들 설정
if (dashboard.elements && dashboard.elements.length > 0) {
setElements(dashboard.elements);
@ -241,9 +257,17 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
if (dashboardId) {
// 기존 대시보드 업데이트
savedDashboard = await dashboardApi.updateDashboard(dashboardId, {
const updateData = {
elements: elementsData,
});
settings: {
resolution,
backgroundColor: canvasBackgroundColor,
},
};
console.log("💾 저장할 데이터:", updateData);
console.log("💾 저장할 해상도:", resolution);
savedDashboard = await dashboardApi.updateDashboard(dashboardId, updateData);
alert(`대시보드 "${savedDashboard.title}"이 업데이트되었습니다!`);
@ -261,6 +285,10 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
description: description || undefined,
isPublic: false,
elements: elementsData,
settings: {
resolution,
backgroundColor: canvasBackgroundColor,
},
};
savedDashboard = await dashboardApi.createDashboard(dashboardData);

View File

@ -78,6 +78,10 @@ export interface Dashboard {
elementsCount?: number;
creatorName?: string;
elements?: DashboardElement[];
settings?: {
resolution?: string;
backgroundColor?: string;
};
}
export interface CreateDashboardRequest {
@ -87,6 +91,10 @@ export interface CreateDashboardRequest {
elements: DashboardElement[];
tags?: string[];
category?: string;
settings?: {
resolution?: string;
backgroundColor?: string;
};
}
export interface DashboardListQuery {