diff --git a/backend-node/src/services/DashboardService.ts b/backend-node/src/services/DashboardService.ts index c25efe4f..d7245ce0 100644 --- a/backend-node/src/services/DashboardService.ts +++ b/backend-node/src/services/DashboardService.ts @@ -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 { + static async createDashboard( + data: CreateDashboardRequest, + userId: string + ): Promise { 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 { + static async getDashboardById( + dashboardId: string, + userId?: string + ): Promise { try { // 1. 대시보드 기본 정보 조회 (권한 체크 포함) let dashboardQuery: string; let dashboardParams: any[]; - + if (userId) { dashboardQuery = ` SELECT d.* @@ -281,51 +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. 요소 데이터 변환 - console.log('📊 대시보드 요소 개수:', elementsResult.rows.length); - - const elements: DashboardElement[] = elementsResult.rows.map((row: any, index: number) => { - const element = { + 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 + y: row.position_y, }, size: { width: row.width, - height: row.height + height: row.height, }, title: row.title, content: row.content, - 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; - }); - + dataSource: JSON.parse(row.data_source_config || "{}"), + chartConfig: JSON.parse(row.chart_config || "{}"), + }) + ); + return { id: dashboard.id, title: dashboard.title, @@ -335,44 +350,48 @@ 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"), + settings: dashboard.settings || undefined, + 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 { 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); @@ -398,120 +417,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 { + static async deleteDashboard( + dashboardId: string, + userId: string + ): Promise { 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 { 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 { try { - const result = await PostgreSQLService.query(` + const result = await PostgreSQLService.query( + ` SELECT CASE WHEN d.created_by = $2 THEN 'admin' @@ -520,23 +560,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; } } -} \ No newline at end of file +} diff --git a/backend-node/src/types/dashboard.ts b/backend-node/src/types/dashboard.ts index c37beae8..f40ee768 100644 --- a/backend-node/src/types/dashboard.ts +++ b/backend-node/src/types/dashboard.ts @@ -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; @@ -17,7 +17,7 @@ export interface DashboardElement { title: string; content?: string; dataSource?: { - type: 'api' | 'database' | 'static'; + type: "api" | "database" | "static"; endpoint?: string; query?: string; refreshInterval?: number; @@ -28,7 +28,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; @@ -48,6 +48,10 @@ export interface Dashboard { tags?: string[]; category?: string; viewCount: number; + settings?: { + resolution?: string; + backgroundColor?: string; + }; elements: DashboardElement[]; } @@ -58,6 +62,10 @@ export interface CreateDashboardRequest { elements: DashboardElement[]; tags?: string[]; category?: string; + settings?: { + resolution?: string; + backgroundColor?: string; + }; } export interface UpdateDashboardRequest { @@ -67,6 +75,10 @@ export interface UpdateDashboardRequest { elements?: DashboardElement[]; tags?: string[]; category?: string; + settings?: { + resolution?: string; + backgroundColor?: string; + }; } export interface DashboardListQuery { @@ -83,7 +95,7 @@ export interface DashboardShare { dashboardId: string; sharedWithUser?: string; sharedWithRole?: string; - permissionLevel: 'view' | 'edit' | 'admin'; + permissionLevel: "view" | "edit" | "admin"; createdBy: string; createdAt: string; expiresAt?: string; diff --git a/frontend/app/(main)/admin/dashboard/page.tsx b/frontend/app/(main)/admin/dashboard/page.tsx index dcf81963..cde559ee 100644 --- a/frontend/app/(main)/admin/dashboard/page.tsx +++ b/frontend/app/(main)/admin/dashboard/page.tsx @@ -156,8 +156,6 @@ export default function DashboardListPage() { 제목 설명 - 요소 수 - 상태 생성일 수정일 작업 @@ -166,29 +164,10 @@ export default function DashboardListPage() { {dashboards.map((dashboard) => ( - -
- {dashboard.title} - {dashboard.isPublic && ( - - 공개 - - )} -
-
+ {dashboard.title} {dashboard.description || "-"} - - {dashboard.elementsCount || 0}개 - - - {dashboard.isPublic ? ( - 공개 - ) : ( - 비공개 - )} - {formatDate(dashboard.createdAt)} {formatDate(dashboard.updatedAt)} diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 5c75acb7..c1a682d3 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -110,6 +110,7 @@ interface CanvasElementProps { element: DashboardElement; isSelected: boolean; cellSize: number; + canvasWidth?: number; onUpdate: (id: string, updates: Partial) => void; onRemove: (id: string) => void; onSelect: (id: string | null) => void; @@ -126,6 +127,7 @@ export function CanvasElement({ element, isSelected, cellSize, + canvasWidth = 1560, onUpdate, onRemove, onSelect, @@ -164,7 +166,11 @@ export function CanvasElement({ return; } - onSelect(element.id); + // 선택되지 않은 경우에만 선택 처리 + if (!isSelected) { + onSelect(element.id); + } + setIsDragging(true); setDragStart({ x: e.clientX, @@ -174,7 +180,7 @@ export function CanvasElement({ }); e.preventDefault(); }, - [element.id, element.position.x, element.position.y, onSelect], + [element.id, element.position.x, element.position.y, onSelect, isSelected], ); // 리사이즈 핸들 마우스다운 @@ -207,7 +213,7 @@ export function CanvasElement({ const rawY = Math.max(0, dragStart.elementY + deltaY); // X 좌표가 캔버스 너비를 벗어나지 않도록 제한 - const maxX = GRID_CONFIG.CANVAS_WIDTH - element.size.width; + const maxX = canvasWidth - element.size.width; rawX = Math.min(rawX, maxX); setTempPosition({ x: rawX, y: rawY }); @@ -223,8 +229,8 @@ export function CanvasElement({ // 최소 크기 설정: 달력은 2x3, 나머지는 2x2 const minWidthCells = 2; const minHeightCells = element.type === "widget" && element.subtype === "calendar" ? 3 : 2; - const minWidth = GRID_CONFIG.CELL_SIZE * minWidthCells; - const minHeight = GRID_CONFIG.CELL_SIZE * minHeightCells; + const minWidth = cellSize * minWidthCells; + const minHeight = cellSize * minHeightCells; switch (resizeStart.handle) { case "se": // 오른쪽 아래 @@ -250,7 +256,7 @@ export function CanvasElement({ } // 가로 너비가 캔버스를 벗어나지 않도록 제한 - const maxWidth = GRID_CONFIG.CANVAS_WIDTH - newX; + const maxWidth = canvasWidth - newX; newWidth = Math.min(newWidth, maxWidth); // 임시 크기/위치 저장 (스냅 안 됨) @@ -258,7 +264,7 @@ export function CanvasElement({ setTempSize({ width: newWidth, height: newHeight }); } }, - [isDragging, isResizing, dragStart, resizeStart, element.size.width, element.type, element.subtype], + [isDragging, isResizing, dragStart, resizeStart, element.size.width, element.type, element.subtype, canvasWidth], ); // 마우스 업 처리 (그리드 스냅 적용) @@ -269,7 +275,7 @@ export function CanvasElement({ const snappedY = snapToGrid(tempPosition.y, cellSize); // X 좌표가 캔버스 너비를 벗어나지 않도록 최종 제한 - const maxX = GRID_CONFIG.CANVAS_WIDTH - element.size.width; + const maxX = canvasWidth - element.size.width; snappedX = Math.min(snappedX, maxX); onUpdate(element.id, { @@ -287,7 +293,7 @@ export function CanvasElement({ const snappedHeight = snapSizeToGrid(tempSize.height, 2, cellSize); // 가로 너비가 캔버스를 벗어나지 않도록 최종 제한 - const maxWidth = GRID_CONFIG.CANVAS_WIDTH - snappedX; + const maxWidth = canvasWidth - snappedX; snappedWidth = Math.min(snappedWidth, maxWidth); onUpdate(element.id, { @@ -301,7 +307,7 @@ export function CanvasElement({ setIsDragging(false); setIsResizing(false); - }, [isDragging, isResizing, tempPosition, tempSize, element.id, element.size.width, onUpdate, cellSize]); + }, [isDragging, isResizing, tempPosition, tempSize, element.id, element.size.width, onUpdate, cellSize, canvasWidth]); // 전역 마우스 이벤트 등록 React.useEffect(() => { @@ -545,12 +551,7 @@ export function CanvasElement({ ) : element.type === "widget" && element.subtype === "status-summary" ? ( // 커스텀 상태 카드 - 범용 위젯
- +
) : /* element.type === "widget" && element.subtype === "list-summary" ? ( // 커스텀 목록 카드 - 범용 위젯 (다른 분 작업 중 - 임시 주석) @@ -560,7 +561,7 @@ export function CanvasElement({ ) : */ element.type === "widget" && element.subtype === "delivery-status" ? ( // 배송/화물 현황 위젯 - 범용 위젯 사용 (구버전 호환)
- -
) : element.type === "widget" && element.subtype === "delivery-today-stats" ? ( // 오늘 처리 현황 - 범용 위젯 사용
- - - void; onConfigureElement?: (element: DashboardElement) => void; backgroundColor?: string; + canvasWidth?: number; + canvasHeight?: number; } /** @@ -34,11 +36,17 @@ export const DashboardCanvas = forwardRef( onSelectElement, onConfigureElement, backgroundColor = "#f9fafb", + canvasWidth = 1560, + canvasHeight = 768, }, ref, ) => { const [isDragOver, setIsDragOver] = useState(false); + // 현재 캔버스 크기에 맞는 그리드 설정 계산 + const gridConfig = useMemo(() => calculateGridConfig(canvasWidth), [canvasWidth]); + const cellSize = gridConfig.CELL_SIZE; + // 드래그 오버 처리 const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); @@ -71,20 +79,20 @@ export const DashboardCanvas = forwardRef( const rawX = e.clientX - rect.left + (ref.current?.scrollLeft || 0); const rawY = e.clientY - rect.top + (ref.current?.scrollTop || 0); - // 그리드에 스냅 (고정 셀 크기 사용) - let snappedX = snapToGrid(rawX, GRID_CONFIG.CELL_SIZE); - const snappedY = snapToGrid(rawY, GRID_CONFIG.CELL_SIZE); + // 그리드에 스냅 (동적 셀 크기 사용) + let snappedX = snapToGrid(rawX, cellSize); + const snappedY = snapToGrid(rawY, cellSize); // X 좌표가 캔버스 너비를 벗어나지 않도록 제한 - const maxX = GRID_CONFIG.CANVAS_WIDTH - GRID_CONFIG.CELL_SIZE * 2; // 최소 2칸 너비 보장 + const maxX = canvasWidth - cellSize * 2; // 최소 2칸 너비 보장 snappedX = Math.max(0, Math.min(snappedX, maxX)); onCreateElement(dragData.type, dragData.subtype, snappedX, snappedY); - } catch (error) { - // console.error('드롭 데이터 파싱 오류:', error); + } catch { + // 드롭 데이터 파싱 오류 무시 } }, - [ref, onCreateElement], + [ref, onCreateElement, canvasWidth, cellSize], ); // 캔버스 클릭 시 선택 해제 @@ -97,28 +105,25 @@ export const DashboardCanvas = forwardRef( [onSelectElement], ); - // 고정 그리드 크기 - const cellWithGap = GRID_CONFIG.CELL_SIZE + GRID_CONFIG.GAP; + // 동적 그리드 크기 계산 + const cellWithGap = cellSize + GRID_CONFIG.GAP; const gridSize = `${cellWithGap}px ${cellWithGap}px`; - // 캔버스 높이를 요소들의 최대 y + height 기준으로 계산 (최소 화면 높이 보장) - const minCanvasHeight = Math.max( - typeof window !== "undefined" ? window.innerHeight : 800, - ...elements.map((el) => el.position.y + el.size.height + 100), // 하단 여백 100px - ); + // 12개 컬럼 구분선 위치 계산 + const columnLines = Array.from({ length: GRID_CONFIG.COLUMNS + 1 }, (_, i) => i * cellWithGap); return (
( onDrop={handleDrop} onClick={handleCanvasClick} > + {/* 12개 컬럼 메인 구분선 */} + {columnLines.map((x, i) => ( +
+ ))} {/* 배치된 요소들 렌더링 */} + {elements.length === 0 && ( +
+
+
상단 메뉴에서 차트나 위젯을 선택하세요
+
+
+ )} {elements.map((element) => ( ("#f9fafb"); const canvasRef = useRef(null); + // 화면 해상도 자동 감지 + const [screenResolution] = useState(() => detectScreenResolution()); + const [resolution, setResolution] = useState(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) { @@ -55,6 +131,25 @@ 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); @@ -81,9 +176,15 @@ 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 }; // 기본 위젯 크기 @@ -93,11 +194,26 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D defaultCells = { width: 2, height: 3 }; // 달력 최소 크기 } - const cellWithGap = GRID_CONFIG.CELL_SIZE + GRID_CONFIG.GAP; + // 현재 해상도에 맞는 셀 크기 계산 + const cellSize = Math.floor((canvasConfig.width + GRID_CONFIG.GAP) / GRID_CONFIG.COLUMNS) - GRID_CONFIG.GAP; + const cellWithGap = cellSize + 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, @@ -112,7 +228,25 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D setElementCounter((prev) => prev + 1); setSelectedElement(newElement.id); }, - [elementCounter], + [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], ); // 요소 업데이트 @@ -195,9 +329,24 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D if (dashboardId) { // 기존 대시보드 업데이트 - savedDashboard = await dashboardApi.updateDashboard(dashboardId, { + console.log("💾 저장 시작 - 현재 resolution 상태:", resolution); + console.log("💾 저장 시작 - 현재 배경색 상태:", canvasBackgroundColor); + + const updateData = { 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}"이 업데이트되었습니다!`); @@ -215,6 +364,10 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D description: description || undefined, isPublic: false, elements: elementsData, + settings: { + resolution, + backgroundColor: canvasBackgroundColor, + }, }; savedDashboard = await dashboardApi.createDashboard(dashboardData); @@ -229,7 +382,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류"; alert(`대시보드 저장 중 오류가 발생했습니다.\n\n오류: ${errorMessage}\n\n관리자에게 문의하세요.`); } - }, [elements, dashboardId, router]); + }, [elements, dashboardId, router, resolution, canvasBackgroundColor]); // 로딩 중이면 로딩 화면 표시 if (isLoading) { @@ -245,25 +398,30 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D } return ( -
- {/* 캔버스 영역 */} -
- {/* 편집 중인 대시보드 표시 */} - {dashboardTitle && ( -
- 📝 편집 중: {dashboardTitle} -
- )} +
+ {/* 상단 메뉴바 */} + router.push(`/dashboard/${dashboardId}`) : undefined} + dashboardTitle={dashboardTitle} + onAddElement={addElementFromMenu} + resolution={resolution} + onResolutionChange={handleResolutionChange} + currentScreenResolution={screenResolution} + backgroundColor={canvasBackgroundColor} + onBackgroundColorChange={setCanvasBackgroundColor} + /> - - - {/* 캔버스 중앙 정렬 컨테이너 */} -
+ {/* 캔버스 영역 - 해상도에 따른 크기, 중앙 정렬 */} +
+
- {/* 사이드바 */} - - {/* 요소 설정 모달 */} {configModalElement && ( <> diff --git a/frontend/components/admin/dashboard/DashboardTopMenu.tsx b/frontend/components/admin/dashboard/DashboardTopMenu.tsx new file mode 100644 index 00000000..1d58988f --- /dev/null +++ b/frontend/components/admin/dashboard/DashboardTopMenu.tsx @@ -0,0 +1,230 @@ +"use client"; + +import React from "react"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Save, Trash2, Eye, Palette } from "lucide-react"; +import { ElementType, ElementSubtype } from "./types"; +import { ResolutionSelector, Resolution } from "./ResolutionSelector"; +import { Input } from "@/components/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; + +interface DashboardTopMenuProps { + onSaveLayout: () => void; + onClearCanvas: () => void; + onViewDashboard?: () => void; + dashboardTitle?: string; + onAddElement?: (type: ElementType, subtype: ElementSubtype) => void; + resolution?: Resolution; + onResolutionChange?: (resolution: Resolution) => void; + currentScreenResolution?: Resolution; + backgroundColor?: string; + onBackgroundColorChange?: (color: string) => void; +} + +/** + * 대시보드 편집 화면 상단 메뉴바 + * - 차트/위젯 선택 (셀렉트박스) + * - 저장/초기화/보기 버튼 + */ +export function DashboardTopMenu({ + onSaveLayout, + onClearCanvas, + onViewDashboard, + dashboardTitle, + onAddElement, + resolution = "fhd", + onResolutionChange, + currentScreenResolution, + backgroundColor = "#f9fafb", + onBackgroundColorChange, +}: DashboardTopMenuProps) { + const [chartValue, setChartValue] = React.useState(""); + const [widgetValue, setWidgetValue] = React.useState(""); + + // 차트 선택 시 캔버스 중앙에 추가 + const handleChartSelect = (value: string) => { + if (onAddElement) { + onAddElement("chart", value as ElementSubtype); + // 선택 후 즉시 리셋하여 같은 항목을 연속으로 선택 가능하게 + setTimeout(() => setChartValue(""), 0); + } + }; + + // 위젯 선택 시 캔버스 중앙에 추가 + const handleWidgetSelect = (value: string) => { + if (onAddElement) { + onAddElement("widget", value as ElementSubtype); + // 선택 후 즉시 리셋하여 같은 항목을 연속으로 선택 가능하게 + setTimeout(() => setWidgetValue(""), 0); + } + }; + + return ( +
+ {/* 좌측: 대시보드 제목 */} +
+ {dashboardTitle && ( +
+ {dashboardTitle} + 편집 중 +
+ )} +
+ + {/* 중앙: 해상도 선택 & 요소 추가 */} +
+ {/* 해상도 선택 */} + {onResolutionChange && ( + + )} + +
+ + {/* 배경색 선택 */} + {onBackgroundColorChange && ( + + + + + +
+
+ +
+
+ onBackgroundColorChange(e.target.value)} + className="h-10 w-20 cursor-pointer" + /> + onBackgroundColorChange(e.target.value)} + placeholder="#f9fafb" + className="flex-1" + /> +
+
+ {[ + "#ffffff", + "#f9fafb", + "#f3f4f6", + "#e5e7eb", + "#1f2937", + "#111827", + "#fef3c7", + "#fde68a", + "#dbeafe", + "#bfdbfe", + "#fecaca", + "#fca5a5", + ].map((color) => ( +
+
+
+
+ )} + +
+ {/* 차트 선택 */} + + + {/* 위젯 선택 */} + +
+ + {/* 우측: 액션 버튼 */} +
+ {onViewDashboard && ( + + )} + + +
+
+ ); +} diff --git a/frontend/components/admin/dashboard/ResolutionSelector.tsx b/frontend/components/admin/dashboard/ResolutionSelector.tsx new file mode 100644 index 00000000..5f5bda53 --- /dev/null +++ b/frontend/components/admin/dashboard/ResolutionSelector.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Monitor } from "lucide-react"; + +export type Resolution = "hd" | "fhd" | "qhd" | "uhd"; + +export interface ResolutionConfig { + width: number; + height: number; + label: string; +} + +export const RESOLUTIONS: Record = { + hd: { + width: 1280 - 360, + height: 720 - 312, + label: "HD (1280x720)", + }, + fhd: { + width: 1920 - 360, + height: 1080 - 312, + label: "Full HD (1920x1080)", + }, + qhd: { + width: 2560 - 360, + height: 1440 - 312, + label: "QHD (2560x1440)", + }, + uhd: { + width: 3840 - 360, + height: 2160 - 312, + label: "4K UHD (3840x2160)", + }, +}; + +interface ResolutionSelectorProps { + value: Resolution; + onChange: (resolution: Resolution) => void; + currentScreenResolution?: Resolution; +} + +/** + * 현재 화면 해상도 감지 + */ +export function detectScreenResolution(): Resolution { + if (typeof window === "undefined") return "fhd"; + + const width = window.screen.width; + const height = window.screen.height; + + // 화면 해상도에 따라 적절한 캔버스 해상도 반환 + if (width >= 3840 || height >= 2160) return "uhd"; + if (width >= 2560 || height >= 1440) return "qhd"; + if (width >= 1920 || height >= 1080) return "fhd"; + return "hd"; +} + +/** + * 해상도 선택 컴포넌트 + * - HD, Full HD, QHD, 4K UHD 지원 + * - 12칸 그리드 유지, 셀 크기만 변경 + * - 현재 화면 해상도 감지 및 경고 표시 + */ +export function ResolutionSelector({ value, onChange, currentScreenResolution }: ResolutionSelectorProps) { + const currentConfig = RESOLUTIONS[value]; + const screenConfig = currentScreenResolution ? RESOLUTIONS[currentScreenResolution] : null; + + // 현재 선택된 해상도가 화면보다 큰지 확인 + const isTooLarge = + screenConfig && + (currentConfig.width > screenConfig.width + 360 || currentConfig.height > screenConfig.height + 312); + + return ( +
+ + + {isTooLarge && ⚠️ 현재 화면보다 큽니다} +
+ ); +} diff --git a/frontend/components/admin/dashboard/gridUtils.ts b/frontend/components/admin/dashboard/gridUtils.ts index f5ec9d7c..54149222 100644 --- a/frontend/components/admin/dashboard/gridUtils.ts +++ b/frontend/components/admin/dashboard/gridUtils.ts @@ -5,18 +5,36 @@ * - 스냅 기능 */ -// 그리드 설정 (고정 크기) +// 기본 그리드 설정 (FHD 기준) export const GRID_CONFIG = { - COLUMNS: 12, - CELL_SIZE: 132, // 고정 셀 크기 - GAP: 8, // 셀 간격 + COLUMNS: 12, // 모든 해상도에서 12칸 고정 + GAP: 8, // 셀 간격 고정 SNAP_THRESHOLD: 15, // 스냅 임계값 (px) ELEMENT_PADDING: 4, // 요소 주위 여백 (px) - CANVAS_WIDTH: 1682, // 고정 캔버스 너비 (실제 측정값) - // 계산식: (132 + 8) × 12 - 8 = 1672px (그리드) - // 추가 여백 10px 포함 = 1682px + // CELL_SIZE와 CANVAS_WIDTH는 해상도에 따라 동적 계산 } as const; +/** + * 캔버스 너비에 맞춰 셀 크기 계산 + * 공식: (CELL_SIZE + GAP) * 12 - GAP = canvasWidth + * CELL_SIZE = (canvasWidth + GAP) / 12 - GAP + */ +export function calculateCellSize(canvasWidth: number): number { + return Math.floor((canvasWidth + GRID_CONFIG.GAP) / GRID_CONFIG.COLUMNS) - GRID_CONFIG.GAP; +} + +/** + * 해상도별 그리드 설정 계산 + */ +export function calculateGridConfig(canvasWidth: number) { + const cellSize = calculateCellSize(canvasWidth); + return { + ...GRID_CONFIG, + CELL_SIZE: cellSize, + CANVAS_WIDTH: canvasWidth, + }; +} + /** * 실제 그리드 셀 크기 계산 (gap 포함) */ diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx index c6e941e3..f4f8caea 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -25,7 +25,18 @@ const DocumentWidget = dynamic(() => import("./widgets/DocumentWidget"), { ssr: const BookingAlertWidget = dynamic(() => import("./widgets/BookingAlertWidget"), { ssr: false }); const MaintenanceWidget = dynamic(() => import("./widgets/MaintenanceWidget"), { ssr: false }); const CalculatorWidget = dynamic(() => import("./widgets/CalculatorWidget"), { ssr: false }); -const CalendarWidget = dynamic(() => import("@/components/admin/dashboard/widgets/CalendarWidget").then(mod => ({ default: mod.CalendarWidget })), { ssr: false }); +const CalendarWidget = dynamic( + () => import("@/components/admin/dashboard/widgets/CalendarWidget").then((mod) => ({ default: mod.CalendarWidget })), + { ssr: false }, +); +const ClockWidget = dynamic( + () => import("@/components/admin/dashboard/widgets/ClockWidget").then((mod) => ({ default: mod.ClockWidget })), + { ssr: false }, +); +const ListWidget = dynamic( + () => import("@/components/admin/dashboard/widgets/ListWidget").then((mod) => ({ default: mod.ListWidget })), + { ssr: false }, +); /** * 위젯 렌더링 함수 - DashboardSidebar의 모든 subtype 처리 @@ -34,7 +45,7 @@ const CalendarWidget = dynamic(() => import("@/components/admin/dashboard/widget function renderWidget(element: DashboardElement) { switch (element.subtype) { // 차트는 ChartRenderer에서 처리됨 (이 함수 호출 안됨) - + // === 위젯 종류 === case "exchange": return ; @@ -43,14 +54,7 @@ function renderWidget(element: DashboardElement) { case "calculator": return ; case "clock": - return ( -
-
-
-
시계 위젯 (개발 예정)
-
-
- ); + return ; case "map-summary": return ; case "list-summary": @@ -61,7 +65,7 @@ function renderWidget(element: DashboardElement) { return ; case "status-summary": return ; - + // === 운영/작업 지원 === case "todo": return ; @@ -72,8 +76,8 @@ function renderWidget(element: DashboardElement) { case "document": return ; case "list": - return ; - + return ; + // === 차량 관련 (추가 위젯) === case "vehicle-status": return ; @@ -81,7 +85,7 @@ function renderWidget(element: DashboardElement) { return ; case "vehicle-map": return ; - + // === 배송 관련 (추가 위젯) === case "delivery-status": return ; @@ -93,7 +97,7 @@ function renderWidget(element: DashboardElement) { return ; case "customer-issues": return ; - + // === 기본 fallback === default: return ( @@ -301,7 +305,9 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
{element.type === "chart" ? ( - ) : renderWidget(element)} + ) : ( + renderWidget(element) + )}
{/* 로딩 오버레이 */} diff --git a/frontend/lib/api/dashboard.ts b/frontend/lib/api/dashboard.ts index d5a53136..80663e1c 100644 --- a/frontend/lib/api/dashboard.ts +++ b/frontend/lib/api/dashboard.ts @@ -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 {