From 6de288eba583c55c4a58dd3900ee0df1cc6e080a Mon Sep 17 00:00:00 2001 From: leeheejin Date: Thu, 16 Oct 2025 14:06:40 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9C=84=EC=A0=AF=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C/=EC=88=A8=EA=B9=80=20=EB=B0=8F=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=EC=A0=9C=EB=AA=A9=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=B3=B5=EA=B5=AC=20-=20f183b4a=20=EC=BB=A4=EB=B0=8B=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=84=A0=ED=83=9D=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/services/DashboardService.ts | 439 ++++++++---------- backend-node/src/types/dashboard.ts | 22 +- .../admin/dashboard/CanvasElement.tsx | 51 +- .../admin/dashboard/DashboardDesigner.tsx | 217 ++------- 4 files changed, 261 insertions(+), 468 deletions(-) diff --git a/backend-node/src/services/DashboardService.ts b/backend-node/src/services/DashboardService.ts index d7245ce0..69b9aba3 100644 --- a/backend-node/src/services/DashboardService.ts +++ b/backend-node/src/services/DashboardService.ts @@ -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 { + 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, 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 { + static async getDashboardById(dashboardId: string, userId?: string): Promise { 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 { 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 { + 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' @@ -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; } } -} +} \ No newline at end of file diff --git a/backend-node/src/types/dashboard.ts b/backend-node/src/types/dashboard.ts index f1d558d6..3428ec8e 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; @@ -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; diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 4b4cd18b..a70bfd61 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -110,7 +110,6 @@ 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; @@ -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" ? ( // 커스텀 상태 카드 - 범용 위젯
- +
) : /* element.type === "widget" && element.subtype === "list-summary" ? ( // 커스텀 목록 카드 - 범용 위젯 (다른 분 작업 중 - 임시 주석) @@ -561,7 +560,7 @@ export function CanvasElement({ ) : */ element.type === "widget" && element.subtype === "delivery-status" ? ( // 배송/화물 현황 위젯 - 범용 위젯 사용 (구버전 호환)
- -
) : element.type === "widget" && element.subtype === "delivery-today-stats" ? ( // 오늘 처리 현황 - 범용 위젯 사용
- - - ("#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) { @@ -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 ( -
- {/* 상단 메뉴바 */} - router.push(`/dashboard/${dashboardId}`) : undefined} - dashboardTitle={dashboardTitle} - onAddElement={addElementFromMenu} - resolution={resolution} - onResolutionChange={handleResolutionChange} - currentScreenResolution={screenResolution} - backgroundColor={canvasBackgroundColor} - onBackgroundColorChange={setCanvasBackgroundColor} - /> +
+ {/* 캔버스 영역 */} +
+ {/* 편집 중인 대시보드 표시 */} + {dashboardTitle && ( +
+ 📝 편집 중: {dashboardTitle} +
+ )} - {/* 캔버스 영역 - 해상도에 따른 크기, 중앙 정렬 */} -
-
+ + + {/* 캔버스 중앙 정렬 컨테이너 */} +
+ {/* 사이드바 */} + + {/* 요소 설정 모달 */} {configModalElement && ( <>