diff --git a/backend-node/src/services/DashboardService.ts b/backend-node/src/services/DashboardService.ts index fa0ce775..9829c49d 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,43 +297,50 @@ export class DashboardService { `; dashboardParams = [dashboardId]; } - - const dashboardResult = await PostgreSQLService.query(dashboardQuery, dashboardParams); - + + const dashboardResult = await PostgreSQLService.query( + dashboardQuery, + dashboardParams + ); + if (dashboardResult.rows.length === 0) { return null; } - + const dashboard = dashboardResult.rows[0]; - + // 2. 대시보드 요소들 조회 const elementsQuery = ` SELECT * FROM dashboard_elements WHERE dashboard_id = $1 ORDER BY display_order ASC `; - - const elementsResult = await PostgreSQLService.query(elementsQuery, [dashboardId]); - + + const elementsResult = await PostgreSQLService.query(elementsQuery, [ + dashboardId, + ]); + // 3. 요소 데이터 변환 - const elements: DashboardElement[] = elementsResult.rows.map((row: any) => ({ - id: row.id, - type: row.element_type, - subtype: row.element_subtype, - position: { - x: row.position_x, - y: row.position_y - }, - size: { - width: row.width, - height: row.height - }, - title: row.title, - content: row.content, - dataSource: JSON.parse(row.data_source_config || '{}'), - chartConfig: JSON.parse(row.chart_config || '{}') - })); - + const elements: DashboardElement[] = elementsResult.rows.map( + (row: any) => ({ + id: row.id, + type: row.element_type, + subtype: row.element_subtype, + position: { + x: row.position_x, + y: row.position_y, + }, + size: { + width: row.width, + height: row.height, + }, + title: row.title, + content: row.content, + dataSource: JSON.parse(row.data_source_config || "{}"), + chartConfig: JSON.parse(row.chart_config || "{}"), + }) + ); + return { id: dashboard.id, title: dashboard.title, @@ -327,44 +350,47 @@ export class DashboardService { createdBy: dashboard.created_by, createdAt: dashboard.created_at, updatedAt: dashboard.updated_at, - tags: JSON.parse(dashboard.tags || '[]'), + tags: JSON.parse(dashboard.tags || "[]"), category: dashboard.category, - viewCount: parseInt(dashboard.view_count || '0'), - elements + viewCount: parseInt(dashboard.view_count || "0"), + elements, }; } catch (error) { - console.error('Dashboard get error:', error); + console.error("Dashboard get error:", error); throw error; } } - + /** * 대시보드 업데이트 */ static async updateDashboard( - dashboardId: string, - data: UpdateDashboardRequest, + dashboardId: string, + data: UpdateDashboardRequest, userId: string ): Promise { try { const result = await PostgreSQLService.transaction(async (client) => { // 권한 체크 - const authCheckResult = await client.query(` + const authCheckResult = await client.query( + ` SELECT id FROM dashboards WHERE id = $1 AND created_by = $2 AND deleted_at IS NULL - `, [dashboardId, userId]); - + `, + [dashboardId, userId] + ); + if (authCheckResult.rows.length === 0) { - throw new Error('대시보드를 찾을 수 없거나 수정 권한이 없습니다.'); + throw new Error("대시보드를 찾을 수 없거나 수정 권한이 없습니다."); } - + const now = new Date(); - + // 1. 대시보드 메인 정보 업데이트 const updateFields: string[] = []; const updateParams: any[] = []; let paramIndex = 1; - + if (data.title !== undefined) { updateFields.push(`title = $${paramIndex}`); updateParams.push(data.title); @@ -390,120 +416,141 @@ export class DashboardService { updateParams.push(data.category); paramIndex++; } - + if (data.settings !== undefined) { + updateFields.push(`settings = $${paramIndex}`); + updateParams.push(JSON.stringify(data.settings)); + paramIndex++; + } + updateFields.push(`updated_at = $${paramIndex}`); updateParams.push(now); paramIndex++; - + updateParams.push(dashboardId); - - if (updateFields.length > 1) { // updated_at 외에 다른 필드가 있는 경우 + + if (updateFields.length > 1) { + // updated_at 외에 다른 필드가 있는 경우 const updateQuery = ` UPDATE dashboards - SET ${updateFields.join(', ')} + SET ${updateFields.join(", ")} WHERE id = $${paramIndex} `; - + await client.query(updateQuery, updateParams); } - + // 2. 요소 업데이트 (있는 경우) if (data.elements) { // 기존 요소들 삭제 - await client.query(` + await client.query( + ` DELETE FROM dashboard_elements WHERE dashboard_id = $1 - `, [dashboardId]); - + `, + [dashboardId] + ); + // 새 요소들 추가 for (let i = 0; i < data.elements.length; i++) { const element = data.elements[i]; const elementId = uuidv4(); - - await client.query(` + + await client.query( + ` INSERT INTO dashboard_elements ( id, dashboard_id, element_type, element_subtype, position_x, position_y, width, height, title, content, data_source_config, chart_config, display_order, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) - `, [ - elementId, - dashboardId, - element.type, - element.subtype, - element.position.x, - element.position.y, - element.size.width, - element.size.height, - element.title, - element.content || null, - JSON.stringify(element.dataSource || {}), - JSON.stringify(element.chartConfig || {}), - i, - now, - now - ]); + `, + [ + elementId, + dashboardId, + element.type, + element.subtype, + element.position.x, + element.position.y, + element.size.width, + element.size.height, + element.title, + element.content || null, + JSON.stringify(element.dataSource || {}), + JSON.stringify(element.chartConfig || {}), + i, + now, + now, + ] + ); } } - + return dashboardId; }); - + // 업데이트된 대시보드 반환 return await this.getDashboardById(dashboardId, userId); - } catch (error) { - console.error('Dashboard update error:', error); + console.error("Dashboard update error:", error); throw error; } } - + /** * 대시보드 삭제 (소프트 삭제) */ - static async deleteDashboard(dashboardId: string, userId: string): Promise { + 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' @@ -512,23 +559,26 @@ export class DashboardService { END as permission FROM dashboards d WHERE d.id = $1 AND d.deleted_at IS NULL - `, [dashboardId, userId]); - + `, + [dashboardId, userId] + ); + if (result.rows.length === 0) { return false; } - + const userPermission = result.rows[0].permission; - + // 권한 레벨 체크 - const permissionLevels = { 'view': 1, 'edit': 2, 'admin': 3 }; - const userLevel = permissionLevels[userPermission as keyof typeof permissionLevels] || 0; + const permissionLevels = { view: 1, edit: 2, admin: 3 }; + const userLevel = + permissionLevels[userPermission as keyof typeof permissionLevels] || 0; const requiredLevel = permissionLevels[requiredPermission]; - + return userLevel >= requiredLevel; } catch (error) { - console.error('Permission check error:', error); + console.error("Permission check error:", error); return false; } } -} \ No newline at end of file +} diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index 4683ee65..63451284 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -33,7 +33,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D const [canvasBackgroundColor, setCanvasBackgroundColor] = useState("#f9fafb"); const canvasRef = useRef(null); - // 화면 해상도 자동 감지 및 기본 해상도 설정 + // 화면 해상도 자동 감지 const [screenResolution] = useState(() => detectScreenResolution()); const [resolution, setResolution] = useState(screenResolution); @@ -62,6 +62,22 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D setDashboardId(dashboard.id); setDashboardTitle(dashboard.title); + // 저장된 설정 복원 + console.log("🔍 로드된 대시보드:", dashboard); + console.log("📦 저장된 settings:", (dashboard as any).settings); + + if ((dashboard as any).settings?.resolution) { + const savedResolution = (dashboard as any).settings.resolution as Resolution; + console.log("✅ 저장된 해상도 복원:", savedResolution); + setResolution(savedResolution); + } else { + console.log("⚠️ 저장된 해상도 없음"); + } + + if ((dashboard as any).settings?.backgroundColor) { + setCanvasBackgroundColor((dashboard as any).settings.backgroundColor); + } + // 요소들 설정 if (dashboard.elements && dashboard.elements.length > 0) { setElements(dashboard.elements); @@ -241,9 +257,17 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D if (dashboardId) { // 기존 대시보드 업데이트 - savedDashboard = await dashboardApi.updateDashboard(dashboardId, { + const updateData = { elements: elementsData, - }); + settings: { + resolution, + backgroundColor: canvasBackgroundColor, + }, + }; + console.log("💾 저장할 데이터:", updateData); + console.log("💾 저장할 해상도:", resolution); + + savedDashboard = await dashboardApi.updateDashboard(dashboardId, updateData); alert(`대시보드 "${savedDashboard.title}"이 업데이트되었습니다!`); @@ -261,6 +285,10 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D description: description || undefined, isPublic: false, elements: elementsData, + settings: { + resolution, + backgroundColor: canvasBackgroundColor, + }, }; savedDashboard = await dashboardApi.createDashboard(dashboardData); diff --git a/frontend/lib/api/dashboard.ts b/frontend/lib/api/dashboard.ts index f6365854..7767d89b 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 {