위젯 헤더 표시/숨김 및 커스텀 제목 기능 복구 - f183b4a 커밋에서 선택적으로 가져옴
This commit is contained in:
parent
d819a7c77e
commit
6de288eba5
|
|
@ -1,98 +1,91 @@
|
|||
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, 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 || {}),
|
||||
]
|
||||
);
|
||||
|
||||
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
|
||||
]);
|
||||
|
||||
// 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,
|
||||
title, custom_title, show_header, 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,
|
||||
]
|
||||
);
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||
`, [
|
||||
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,
|
||||
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,
|
||||
|
|
@ -106,13 +99,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,
|
||||
|
|
@ -126,79 +119,76 @@ 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
|
||||
|
|
@ -223,23 +213,22 @@ 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,
|
||||
|
|
@ -250,36 +239,33 @@ 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.*
|
||||
|
|
@ -297,50 +283,53 @@ 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) => ({
|
||||
console.log('📊 대시보드 요소 개수:', elementsResult.rows.length);
|
||||
|
||||
const elements: DashboardElement[] = elementsResult.rows.map((row: any, index: number) => {
|
||||
const element = {
|
||||
id: row.id,
|
||||
type: row.element_type,
|
||||
subtype: row.element_subtype,
|
||||
position: {
|
||||
x: row.position_x,
|
||||
y: row.position_y,
|
||||
y: row.position_y
|
||||
},
|
||||
size: {
|
||||
width: row.width,
|
||||
height: row.height,
|
||||
height: row.height
|
||||
},
|
||||
title: row.title,
|
||||
customTitle: row.custom_title,
|
||||
showHeader: row.show_header !== false,
|
||||
content: row.content,
|
||||
dataSource: JSON.parse(row.data_source_config || "{}"),
|
||||
chartConfig: JSON.parse(row.chart_config || "{}"),
|
||||
})
|
||||
);
|
||||
|
||||
dataSource: JSON.parse(row.data_source_config || '{}'),
|
||||
chartConfig: JSON.parse(row.chart_config || '{}')
|
||||
};
|
||||
|
||||
console.log(`📊 위젯 #${index + 1}: type="${element.type}", subtype="${element.subtype}", title="${element.title}"`);
|
||||
|
||||
return element;
|
||||
});
|
||||
|
||||
return {
|
||||
id: dashboard.id,
|
||||
title: dashboard.title,
|
||||
|
|
@ -350,48 +339,44 @@ 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"),
|
||||
settings: dashboard.settings || undefined,
|
||||
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);
|
||||
|
|
@ -417,141 +402,122 @@ 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,
|
||||
title, custom_title, show_header, 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,
|
||||
]
|
||||
);
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||
`, [
|
||||
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,
|
||||
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'
|
||||
|
|
@ -560,26 +526,23 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
export interface DashboardElement {
|
||||
id: string;
|
||||
type: "chart" | "widget";
|
||||
subtype: "bar" | "pie" | "line" | "exchange" | "weather";
|
||||
type: 'chart' | 'widget';
|
||||
subtype: 'bar' | 'pie' | 'line' | 'exchange' | 'weather';
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
|
|
@ -19,7 +19,7 @@ export interface DashboardElement {
|
|||
showHeader?: boolean; // 헤더 표시 여부 (기본값: true)
|
||||
content?: string;
|
||||
dataSource?: {
|
||||
type: "api" | "database" | "static";
|
||||
type: 'api' | 'database' | 'static';
|
||||
endpoint?: string;
|
||||
query?: string;
|
||||
refreshInterval?: number;
|
||||
|
|
@ -30,7 +30,7 @@ export interface DashboardElement {
|
|||
xAxis?: string;
|
||||
yAxis?: string;
|
||||
groupBy?: string;
|
||||
aggregation?: "sum" | "avg" | "count" | "max" | "min";
|
||||
aggregation?: 'sum' | 'avg' | 'count' | 'max' | 'min';
|
||||
colors?: string[];
|
||||
title?: string;
|
||||
showLegend?: boolean;
|
||||
|
|
@ -50,10 +50,6 @@ export interface Dashboard {
|
|||
tags?: string[];
|
||||
category?: string;
|
||||
viewCount: number;
|
||||
settings?: {
|
||||
resolution?: string;
|
||||
backgroundColor?: string;
|
||||
};
|
||||
elements: DashboardElement[];
|
||||
}
|
||||
|
||||
|
|
@ -64,10 +60,6 @@ export interface CreateDashboardRequest {
|
|||
elements: DashboardElement[];
|
||||
tags?: string[];
|
||||
category?: string;
|
||||
settings?: {
|
||||
resolution?: string;
|
||||
backgroundColor?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateDashboardRequest {
|
||||
|
|
@ -77,10 +69,6 @@ export interface UpdateDashboardRequest {
|
|||
elements?: DashboardElement[];
|
||||
tags?: string[];
|
||||
category?: string;
|
||||
settings?: {
|
||||
resolution?: string;
|
||||
backgroundColor?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DashboardListQuery {
|
||||
|
|
@ -97,7 +85,7 @@ export interface DashboardShare {
|
|||
dashboardId: string;
|
||||
sharedWithUser?: string;
|
||||
sharedWithRole?: string;
|
||||
permissionLevel: "view" | "edit" | "admin";
|
||||
permissionLevel: 'view' | 'edit' | 'admin';
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
expiresAt?: string;
|
||||
|
|
|
|||
|
|
@ -110,7 +110,6 @@ interface CanvasElementProps {
|
|||
element: DashboardElement;
|
||||
isSelected: boolean;
|
||||
cellSize: number;
|
||||
canvasWidth?: number;
|
||||
onUpdate: (id: string, updates: Partial<DashboardElement>) => void;
|
||||
onRemove: (id: string) => void;
|
||||
onSelect: (id: string | null) => void;
|
||||
|
|
@ -127,7 +126,6 @@ export function CanvasElement({
|
|||
element,
|
||||
isSelected,
|
||||
cellSize,
|
||||
canvasWidth = 1560,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
onSelect,
|
||||
|
|
@ -166,11 +164,7 @@ export function CanvasElement({
|
|||
return;
|
||||
}
|
||||
|
||||
// 선택되지 않은 경우에만 선택 처리
|
||||
if (!isSelected) {
|
||||
onSelect(element.id);
|
||||
}
|
||||
|
||||
onSelect(element.id);
|
||||
setIsDragging(true);
|
||||
setDragStart({
|
||||
x: e.clientX,
|
||||
|
|
@ -180,7 +174,7 @@ export function CanvasElement({
|
|||
});
|
||||
e.preventDefault();
|
||||
},
|
||||
[element.id, element.position.x, element.position.y, onSelect, isSelected],
|
||||
[element.id, element.position.x, element.position.y, onSelect],
|
||||
);
|
||||
|
||||
// 리사이즈 핸들 마우스다운
|
||||
|
|
@ -213,7 +207,7 @@ export function CanvasElement({
|
|||
const rawY = Math.max(0, dragStart.elementY + deltaY);
|
||||
|
||||
// X 좌표가 캔버스 너비를 벗어나지 않도록 제한
|
||||
const maxX = canvasWidth - element.size.width;
|
||||
const maxX = GRID_CONFIG.CANVAS_WIDTH - element.size.width;
|
||||
rawX = Math.min(rawX, maxX);
|
||||
|
||||
setTempPosition({ x: rawX, y: rawY });
|
||||
|
|
@ -229,8 +223,8 @@ export function CanvasElement({
|
|||
// 최소 크기 설정: 달력은 2x3, 나머지는 2x2
|
||||
const minWidthCells = 2;
|
||||
const minHeightCells = element.type === "widget" && element.subtype === "calendar" ? 3 : 2;
|
||||
const minWidth = cellSize * minWidthCells;
|
||||
const minHeight = cellSize * minHeightCells;
|
||||
const minWidth = GRID_CONFIG.CELL_SIZE * minWidthCells;
|
||||
const minHeight = GRID_CONFIG.CELL_SIZE * minHeightCells;
|
||||
|
||||
switch (resizeStart.handle) {
|
||||
case "se": // 오른쪽 아래
|
||||
|
|
@ -256,7 +250,7 @@ export function CanvasElement({
|
|||
}
|
||||
|
||||
// 가로 너비가 캔버스를 벗어나지 않도록 제한
|
||||
const maxWidth = canvasWidth - newX;
|
||||
const maxWidth = GRID_CONFIG.CANVAS_WIDTH - newX;
|
||||
newWidth = Math.min(newWidth, maxWidth);
|
||||
|
||||
// 임시 크기/위치 저장 (스냅 안 됨)
|
||||
|
|
@ -264,7 +258,7 @@ export function CanvasElement({
|
|||
setTempSize({ width: newWidth, height: newHeight });
|
||||
}
|
||||
},
|
||||
[isDragging, isResizing, dragStart, resizeStart, element.size.width, element.type, element.subtype, canvasWidth],
|
||||
[isDragging, isResizing, dragStart, resizeStart, element.size.width, element.type, element.subtype],
|
||||
);
|
||||
|
||||
// 마우스 업 처리 (그리드 스냅 적용)
|
||||
|
|
@ -275,7 +269,7 @@ export function CanvasElement({
|
|||
const snappedY = snapToGrid(tempPosition.y, cellSize);
|
||||
|
||||
// X 좌표가 캔버스 너비를 벗어나지 않도록 최종 제한
|
||||
const maxX = canvasWidth - element.size.width;
|
||||
const maxX = GRID_CONFIG.CANVAS_WIDTH - element.size.width;
|
||||
snappedX = Math.min(snappedX, maxX);
|
||||
|
||||
onUpdate(element.id, {
|
||||
|
|
@ -293,7 +287,7 @@ export function CanvasElement({
|
|||
const snappedHeight = snapSizeToGrid(tempSize.height, 2, cellSize);
|
||||
|
||||
// 가로 너비가 캔버스를 벗어나지 않도록 최종 제한
|
||||
const maxWidth = canvasWidth - snappedX;
|
||||
const maxWidth = GRID_CONFIG.CANVAS_WIDTH - snappedX;
|
||||
snappedWidth = Math.min(snappedWidth, maxWidth);
|
||||
|
||||
onUpdate(element.id, {
|
||||
|
|
@ -307,7 +301,7 @@ export function CanvasElement({
|
|||
|
||||
setIsDragging(false);
|
||||
setIsResizing(false);
|
||||
}, [isDragging, isResizing, tempPosition, tempSize, element.id, element.size.width, onUpdate, cellSize, canvasWidth]);
|
||||
}, [isDragging, isResizing, tempPosition, tempSize, element.id, element.size.width, onUpdate, cellSize]);
|
||||
|
||||
// 전역 마우스 이벤트 등록
|
||||
React.useEffect(() => {
|
||||
|
|
@ -551,7 +545,12 @@ export function CanvasElement({
|
|||
) : element.type === "widget" && element.subtype === "status-summary" ? (
|
||||
// 커스텀 상태 카드 - 범용 위젯
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<StatusSummaryWidget element={element} title="상태 요약" icon="📊" bgGradient="from-slate-50 to-blue-50" />
|
||||
<StatusSummaryWidget
|
||||
element={element}
|
||||
title="상태 요약"
|
||||
icon="📊"
|
||||
bgGradient="from-slate-50 to-blue-50"
|
||||
/>
|
||||
</div>
|
||||
) : /* element.type === "widget" && element.subtype === "list-summary" ? (
|
||||
// 커스텀 목록 카드 - 범용 위젯 (다른 분 작업 중 - 임시 주석)
|
||||
|
|
@ -561,7 +560,7 @@ export function CanvasElement({
|
|||
) : */ element.type === "widget" && element.subtype === "delivery-status" ? (
|
||||
// 배송/화물 현황 위젯 - 범용 위젯 사용 (구버전 호환)
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<StatusSummaryWidget
|
||||
<StatusSummaryWidget
|
||||
element={element}
|
||||
title="배송/화물 현황"
|
||||
icon="📦"
|
||||
|
|
@ -571,23 +570,23 @@ export function CanvasElement({
|
|||
) : element.type === "widget" && element.subtype === "delivery-status-summary" ? (
|
||||
// 배송 상태 요약 - 범용 위젯 사용
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<StatusSummaryWidget
|
||||
<StatusSummaryWidget
|
||||
element={element}
|
||||
title="배송 상태 요약"
|
||||
icon="📊"
|
||||
bgGradient="from-slate-50 to-blue-50"
|
||||
statusConfig={{
|
||||
배송중: { label: "배송중", color: "blue" },
|
||||
완료: { label: "완료", color: "green" },
|
||||
지연: { label: "지연", color: "red" },
|
||||
"픽업 대기": { label: "픽업 대기", color: "yellow" },
|
||||
"배송중": { label: "배송중", color: "blue" },
|
||||
"완료": { label: "완료", color: "green" },
|
||||
"지연": { label: "지연", color: "red" },
|
||||
"픽업 대기": { label: "픽업 대기", color: "yellow" }
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "delivery-today-stats" ? (
|
||||
// 오늘 처리 현황 - 범용 위젯 사용
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<StatusSummaryWidget
|
||||
<StatusSummaryWidget
|
||||
element={element}
|
||||
title="오늘 처리 현황"
|
||||
icon="📈"
|
||||
|
|
@ -597,7 +596,7 @@ export function CanvasElement({
|
|||
) : element.type === "widget" && element.subtype === "cargo-list" ? (
|
||||
// 화물 목록 - 범용 위젯 사용
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<StatusSummaryWidget
|
||||
<StatusSummaryWidget
|
||||
element={element}
|
||||
title="화물 목록"
|
||||
icon="📦"
|
||||
|
|
@ -607,7 +606,7 @@ export function CanvasElement({
|
|||
) : element.type === "widget" && element.subtype === "customer-issues" ? (
|
||||
// 고객 클레임/이슈 - 범용 위젯 사용
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<StatusSummaryWidget
|
||||
<StatusSummaryWidget
|
||||
element={element}
|
||||
title="고객 클레임/이슈"
|
||||
icon="⚠️"
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
import React, { useState, useRef, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { DashboardCanvas } from "./DashboardCanvas";
|
||||
import { DashboardTopMenu } from "./DashboardTopMenu";
|
||||
import { DashboardSidebar } from "./DashboardSidebar";
|
||||
import { DashboardToolbar } from "./DashboardToolbar";
|
||||
import { ElementConfigModal } from "./ElementConfigModal";
|
||||
import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal";
|
||||
import { DashboardElement, ElementType, ElementSubtype } from "./types";
|
||||
import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize } from "./gridUtils";
|
||||
import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSelector";
|
||||
import { GRID_CONFIG } from "./gridUtils";
|
||||
|
||||
interface DashboardDesignerProps {
|
||||
dashboardId?: string;
|
||||
|
|
@ -33,82 +33,6 @@ 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);
|
||||
|
||||
// resolution 변경 감지 및 요소 자동 조정
|
||||
const handleResolutionChange = useCallback(
|
||||
(newResolution: Resolution) => {
|
||||
console.log("🎯 해상도 변경 요청:", newResolution);
|
||||
setResolution((prev) => {
|
||||
console.log("🎯 이전 해상도:", prev);
|
||||
|
||||
// 이전 해상도와 새 해상도의 캔버스 너비 비율 계산
|
||||
const oldConfig = RESOLUTIONS[prev];
|
||||
const newConfig = RESOLUTIONS[newResolution];
|
||||
const widthRatio = newConfig.width / oldConfig.width;
|
||||
|
||||
console.log("📐 너비 비율:", widthRatio, `(${oldConfig.width}px → ${newConfig.width}px)`);
|
||||
|
||||
// 요소들의 위치와 크기를 비율에 맞춰 조정
|
||||
if (widthRatio !== 1 && elements.length > 0) {
|
||||
// 새 해상도의 셀 크기 계산
|
||||
const newCellSize = calculateCellSize(newConfig.width);
|
||||
|
||||
const adjustedElements = elements.map((el) => {
|
||||
// 비율에 맞춰 조정 (X와 너비만)
|
||||
const scaledX = el.position.x * widthRatio;
|
||||
const scaledWidth = el.size.width * widthRatio;
|
||||
|
||||
// 그리드에 스냅 (X, Y, 너비, 높이 모두)
|
||||
const snappedX = snapToGrid(scaledX, newCellSize);
|
||||
const snappedY = snapToGrid(el.position.y, newCellSize);
|
||||
const snappedWidth = snapSizeToGrid(scaledWidth, 2, newCellSize);
|
||||
const snappedHeight = snapSizeToGrid(el.size.height, 2, newCellSize);
|
||||
|
||||
return {
|
||||
...el,
|
||||
position: {
|
||||
x: snappedX,
|
||||
y: snappedY,
|
||||
},
|
||||
size: {
|
||||
width: snappedWidth,
|
||||
height: snappedHeight,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
console.log("✨ 요소 위치/크기 자동 조정 (그리드 스냅 적용):", adjustedElements.length, "개");
|
||||
setElements(adjustedElements);
|
||||
}
|
||||
|
||||
return newResolution;
|
||||
});
|
||||
},
|
||||
[elements],
|
||||
);
|
||||
|
||||
// 현재 해상도 설정 (안전하게 기본값 제공)
|
||||
const canvasConfig = RESOLUTIONS[resolution] || RESOLUTIONS.fhd;
|
||||
|
||||
// 캔버스 높이 동적 계산 (요소들의 최하단 위치 기준)
|
||||
const calculateCanvasHeight = useCallback(() => {
|
||||
if (elements.length === 0) {
|
||||
return canvasConfig.height; // 기본 높이
|
||||
}
|
||||
|
||||
// 모든 요소의 최하단 y 좌표 계산
|
||||
const maxBottomY = Math.max(...elements.map((el) => el.position.y + el.size.height));
|
||||
|
||||
// 최소 높이는 기본 높이, 요소가 아래로 내려가면 자동으로 늘어남
|
||||
// 패딩 추가 (100px 여유)
|
||||
return Math.max(canvasConfig.height, maxBottomY + 100);
|
||||
}, [elements, canvasConfig.height]);
|
||||
|
||||
const dynamicCanvasHeight = calculateCanvasHeight();
|
||||
|
||||
// 대시보드 ID가 props로 전달되면 로드
|
||||
React.useEffect(() => {
|
||||
if (initialDashboardId) {
|
||||
|
|
@ -131,25 +55,6 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
setDashboardId(dashboard.id);
|
||||
setDashboardTitle(dashboard.title);
|
||||
|
||||
// 저장된 설정 복원
|
||||
console.log("🔍 로드된 대시보드:", dashboard);
|
||||
console.log("📦 저장된 settings:", (dashboard as any).settings);
|
||||
console.log("🎯 settings 타입:", typeof (dashboard as any).settings);
|
||||
console.log("🔍 resolution 값:", (dashboard as any).settings?.resolution);
|
||||
|
||||
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) {
|
||||
console.log("✅ 저장된 배경색 복원:", (dashboard as any).settings.backgroundColor);
|
||||
setCanvasBackgroundColor((dashboard as any).settings.backgroundColor);
|
||||
}
|
||||
|
||||
// 요소들 설정
|
||||
if (dashboard.elements && dashboard.elements.length > 0) {
|
||||
setElements(dashboard.elements);
|
||||
|
|
@ -176,15 +81,9 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
}
|
||||
};
|
||||
|
||||
// 새로운 요소 생성 (동적 그리드 기반 기본 크기)
|
||||
// 새로운 요소 생성 (고정 그리드 기반 기본 크기)
|
||||
const createElement = useCallback(
|
||||
(type: ElementType, subtype: ElementSubtype, x: number, y: number) => {
|
||||
// 좌표 유효성 검사
|
||||
if (isNaN(x) || isNaN(y)) {
|
||||
console.error("Invalid coordinates:", { x, y });
|
||||
return;
|
||||
}
|
||||
|
||||
// 기본 크기 설정
|
||||
let defaultCells = { width: 2, height: 2 }; // 기본 위젯 크기
|
||||
|
||||
|
|
@ -194,26 +93,11 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
defaultCells = { width: 2, height: 3 }; // 달력 최소 크기
|
||||
}
|
||||
|
||||
// 현재 해상도에 맞는 셀 크기 계산
|
||||
const cellSize = Math.floor((canvasConfig.width + GRID_CONFIG.GAP) / GRID_CONFIG.COLUMNS) - GRID_CONFIG.GAP;
|
||||
const cellWithGap = cellSize + GRID_CONFIG.GAP;
|
||||
const cellWithGap = GRID_CONFIG.CELL_SIZE + GRID_CONFIG.GAP;
|
||||
|
||||
const defaultWidth = defaultCells.width * cellWithGap - GRID_CONFIG.GAP;
|
||||
const defaultHeight = defaultCells.height * cellWithGap - GRID_CONFIG.GAP;
|
||||
|
||||
// 크기 유효성 검사
|
||||
if (isNaN(defaultWidth) || isNaN(defaultHeight) || defaultWidth <= 0 || defaultHeight <= 0) {
|
||||
console.error("Invalid size calculated:", {
|
||||
canvasConfig,
|
||||
cellSize,
|
||||
cellWithGap,
|
||||
defaultCells,
|
||||
defaultWidth,
|
||||
defaultHeight,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newElement: DashboardElement = {
|
||||
id: `element-${elementCounter + 1}`,
|
||||
type,
|
||||
|
|
@ -228,25 +112,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
setElementCounter((prev) => prev + 1);
|
||||
setSelectedElement(newElement.id);
|
||||
},
|
||||
[elementCounter, canvasConfig.width],
|
||||
);
|
||||
|
||||
// 메뉴에서 요소 추가 시 (캔버스 중앙에 배치)
|
||||
const addElementFromMenu = useCallback(
|
||||
(type: ElementType, subtype: ElementSubtype) => {
|
||||
// 캔버스 중앙 좌표 계산
|
||||
const centerX = Math.floor(canvasConfig.width / 2);
|
||||
const centerY = Math.floor(canvasConfig.height / 2);
|
||||
|
||||
// 좌표 유효성 확인
|
||||
if (isNaN(centerX) || isNaN(centerY)) {
|
||||
console.error("Invalid canvas config:", canvasConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
createElement(type, subtype, centerX, centerY);
|
||||
},
|
||||
[canvasConfig.width, canvasConfig.height, createElement],
|
||||
[elementCounter],
|
||||
);
|
||||
|
||||
// 요소 업데이트
|
||||
|
|
@ -334,24 +200,9 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
|
||||
if (dashboardId) {
|
||||
// 기존 대시보드 업데이트
|
||||
console.log("💾 저장 시작 - 현재 resolution 상태:", resolution);
|
||||
console.log("💾 저장 시작 - 현재 배경색 상태:", canvasBackgroundColor);
|
||||
|
||||
const updateData = {
|
||||
savedDashboard = await dashboardApi.updateDashboard(dashboardId, {
|
||||
elements: elementsData,
|
||||
settings: {
|
||||
resolution,
|
||||
backgroundColor: canvasBackgroundColor,
|
||||
},
|
||||
};
|
||||
|
||||
console.log("💾 저장할 데이터:", updateData);
|
||||
console.log("💾 저장할 settings:", updateData.settings);
|
||||
|
||||
savedDashboard = await dashboardApi.updateDashboard(dashboardId, updateData);
|
||||
|
||||
console.log("✅ 저장된 대시보드:", savedDashboard);
|
||||
console.log("✅ 저장된 settings:", (savedDashboard as any).settings);
|
||||
});
|
||||
|
||||
alert(`대시보드 "${savedDashboard.title}"이 업데이트되었습니다!`);
|
||||
|
||||
|
|
@ -369,10 +220,6 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
description: description || undefined,
|
||||
isPublic: false,
|
||||
elements: elementsData,
|
||||
settings: {
|
||||
resolution,
|
||||
backgroundColor: canvasBackgroundColor,
|
||||
},
|
||||
};
|
||||
|
||||
savedDashboard = await dashboardApi.createDashboard(dashboardData);
|
||||
|
|
@ -387,7 +234,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류";
|
||||
alert(`대시보드 저장 중 오류가 발생했습니다.\n\n오류: ${errorMessage}\n\n관리자에게 문의하세요.`);
|
||||
}
|
||||
}, [elements, dashboardId, router, resolution, canvasBackgroundColor]);
|
||||
}, [elements, dashboardId, router]);
|
||||
|
||||
// 로딩 중이면 로딩 화면 표시
|
||||
if (isLoading) {
|
||||
|
|
@ -403,30 +250,25 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-gray-50">
|
||||
{/* 상단 메뉴바 */}
|
||||
<DashboardTopMenu
|
||||
onSaveLayout={saveLayout}
|
||||
onClearCanvas={clearCanvas}
|
||||
onViewDashboard={dashboardId ? () => router.push(`/dashboard/${dashboardId}`) : undefined}
|
||||
dashboardTitle={dashboardTitle}
|
||||
onAddElement={addElementFromMenu}
|
||||
resolution={resolution}
|
||||
onResolutionChange={handleResolutionChange}
|
||||
currentScreenResolution={screenResolution}
|
||||
backgroundColor={canvasBackgroundColor}
|
||||
onBackgroundColorChange={setCanvasBackgroundColor}
|
||||
/>
|
||||
<div className="flex h-full bg-gray-50">
|
||||
{/* 캔버스 영역 */}
|
||||
<div className="relative flex-1 overflow-auto border-r-2 border-gray-300 bg-gray-100">
|
||||
{/* 편집 중인 대시보드 표시 */}
|
||||
{dashboardTitle && (
|
||||
<div className="bg-accent0 absolute top-6 left-6 z-10 rounded-lg px-3 py-1 text-sm font-medium text-white shadow-lg">
|
||||
📝 편집 중: {dashboardTitle}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 캔버스 영역 - 해상도에 따른 크기, 중앙 정렬 */}
|
||||
<div className="flex flex-1 items-start justify-center overflow-auto bg-gray-100 p-8">
|
||||
<div
|
||||
className="relative shadow-2xl"
|
||||
style={{
|
||||
width: `${canvasConfig.width}px`,
|
||||
minHeight: `${canvasConfig.height}px`,
|
||||
}}
|
||||
>
|
||||
<DashboardToolbar
|
||||
onClearCanvas={clearCanvas}
|
||||
onSaveLayout={saveLayout}
|
||||
canvasBackgroundColor={canvasBackgroundColor}
|
||||
onCanvasBackgroundColorChange={setCanvasBackgroundColor}
|
||||
/>
|
||||
|
||||
{/* 캔버스 중앙 정렬 컨테이너 */}
|
||||
<div className="flex justify-center p-4">
|
||||
<DashboardCanvas
|
||||
ref={canvasRef}
|
||||
elements={elements}
|
||||
|
|
@ -437,12 +279,13 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
onSelectElement={setSelectedElement}
|
||||
onConfigureElement={openConfigModal}
|
||||
backgroundColor={canvasBackgroundColor}
|
||||
canvasWidth={canvasConfig.width}
|
||||
canvasHeight={dynamicCanvasHeight}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 사이드바 */}
|
||||
<DashboardSidebar />
|
||||
|
||||
{/* 요소 설정 모달 */}
|
||||
{configModalElement && (
|
||||
<>
|
||||
|
|
|
|||
Loading…
Reference in New Issue