From 6d51aced2c16994331f676b3eade38bd1600bd37 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Thu, 16 Oct 2025 14:59:07 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9C=84=EC=A0=AF=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=EC=A0=9C=EB=AA=A9=20=EB=B0=8F=20=ED=97=A4=EB=8D=94?= =?UTF-8?q?=20=ED=91=9C=EC=8B=9C/=EC=88=A8=EA=B9=80=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 위젯 설정에서 제목 변경 가능 - 위젯 헤더 표시/숨김 토글 추가 - DB 마이그레이션 자동 실행 (custom_title, show_header 컬럼) - 편집 모드/보기 모드 모두 지원 - DashboardTopMenu 레이아웃 유지 --- backend-node/src/app.ts | 8 ++++ backend-node/src/database/runMigration.ts | 42 +++++++++++++++++++ backend-node/src/services/DashboardService.ts | 14 +++++-- backend-node/src/types/dashboard.ts | 2 + .../admin/dashboard/CanvasElement.tsx | 2 +- .../admin/dashboard/DashboardDesigner.tsx | 2 + .../admin/dashboard/ElementConfigModal.tsx | 18 +++++++- frontend/components/admin/dashboard/types.ts | 1 + .../components/dashboard/DashboardViewer.tsx | 42 ++++++++++--------- 9 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 backend-node/src/database/runMigration.ts diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index ae10a6fe..0d7e0e15 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -231,6 +231,14 @@ app.listen(PORT, HOST, async () => { logger.info(`🔗 Health check: http://${HOST}:${PORT}/health`); logger.info(`🌐 External access: http://39.117.244.52:${PORT}/health`); + // 대시보드 마이그레이션 실행 + try { + const { runDashboardMigration } = await import('./database/runMigration'); + await runDashboardMigration(); + } catch (error) { + logger.error(`❌ 대시보드 마이그레이션 실패:`, error); + } + // 배치 스케줄러 초기화 try { await BatchSchedulerService.initialize(); diff --git a/backend-node/src/database/runMigration.ts b/backend-node/src/database/runMigration.ts new file mode 100644 index 00000000..61b98241 --- /dev/null +++ b/backend-node/src/database/runMigration.ts @@ -0,0 +1,42 @@ +import { PostgreSQLService } from './PostgreSQLService'; + +/** + * 데이터베이스 마이그레이션 실행 + * dashboard_elements 테이블에 custom_title, show_header 컬럼 추가 + */ +export async function runDashboardMigration() { + try { + console.log('🔄 대시보드 마이그레이션 시작...'); + + // custom_title 컬럼 추가 + await PostgreSQLService.query(` + ALTER TABLE dashboard_elements + ADD COLUMN IF NOT EXISTS custom_title VARCHAR(255) + `); + console.log('✅ custom_title 컬럼 추가 완료'); + + // show_header 컬럼 추가 + await PostgreSQLService.query(` + ALTER TABLE dashboard_elements + ADD COLUMN IF NOT EXISTS show_header BOOLEAN DEFAULT true + `); + console.log('✅ show_header 컬럼 추가 완료'); + + // 기존 데이터 업데이트 + await PostgreSQLService.query(` + UPDATE dashboard_elements + SET show_header = true + WHERE show_header IS NULL + `); + console.log('✅ 기존 데이터 업데이트 완료'); + + console.log('✅ 대시보드 마이그레이션 완료!'); + } catch (error) { + console.error('❌ 대시보드 마이그레이션 실패:', error); + // 이미 컬럼이 있는 경우는 무시 + if (error instanceof Error && error.message.includes('already exists')) { + console.log('ℹ️ 컬럼이 이미 존재합니다.'); + } + } +} + diff --git a/backend-node/src/services/DashboardService.ts b/backend-node/src/services/DashboardService.ts index d7245ce0..f8816555 100644 --- a/backend-node/src/services/DashboardService.ts +++ b/backend-node/src/services/DashboardService.ts @@ -60,9 +60,9 @@ export class DashboardService { 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) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) `, [ elementId, @@ -74,6 +74,8 @@ export class DashboardService { element.size.width, element.size.height, element.title, + element.customTitle || null, + element.showHeader !== false, // 기본값 true element.content || null, JSON.stringify(element.dataSource || {}), JSON.stringify(element.chartConfig || {}), @@ -335,6 +337,8 @@ export class DashboardService { height: row.height, }, title: row.title, + customTitle: row.custom_title || undefined, + showHeader: row.show_header !== false, // 기본값 true content: row.content, dataSource: JSON.parse(row.data_source_config || "{}"), chartConfig: JSON.parse(row.chart_config || "{}"), @@ -460,9 +464,9 @@ export class DashboardService { 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) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) `, [ elementId, @@ -474,6 +478,8 @@ export class DashboardService { element.size.width, element.size.height, element.title, + element.customTitle || null, + element.showHeader !== false, // 기본값 true element.content || null, JSON.stringify(element.dataSource || {}), JSON.stringify(element.chartConfig || {}), diff --git a/backend-node/src/types/dashboard.ts b/backend-node/src/types/dashboard.ts index f40ee768..789adda3 100644 --- a/backend-node/src/types/dashboard.ts +++ b/backend-node/src/types/dashboard.ts @@ -15,6 +15,8 @@ export interface DashboardElement { height: number; }; title: string; + customTitle?: string; // 사용자 정의 제목 (옵션) + showHeader?: boolean; // 헤더 표시 여부 (기본값: true) content?: string; dataSource?: { type: "api" | "database" | "static"; diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index c1a682d3..7050b1f2 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -455,7 +455,7 @@ export function CanvasElement({ > {/* 헤더 */}
- {element.title} + {element.customTitle || element.title}
{/* 설정 버튼 (시계, 달력, 기사관리 위젯은 자체 설정 UI 사용) */} {onConfigure && diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index 048e5172..9cbff1b8 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -320,6 +320,8 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D position: el.position, size: el.size, title: el.title, + customTitle: el.customTitle, + showHeader: el.showHeader, content: el.content, dataSource: el.dataSource, chartConfig: el.chartConfig, diff --git a/frontend/components/admin/dashboard/ElementConfigModal.tsx b/frontend/components/admin/dashboard/ElementConfigModal.tsx index aa074317..5e2df2df 100644 --- a/frontend/components/admin/dashboard/ElementConfigModal.tsx +++ b/frontend/components/admin/dashboard/ElementConfigModal.tsx @@ -32,6 +32,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element const [queryResult, setQueryResult] = useState(null); const [currentStep, setCurrentStep] = useState<1 | 2>(1); const [customTitle, setCustomTitle] = useState(element.customTitle || ""); + const [showHeader, setShowHeader] = useState(element.showHeader !== false); // 차트 설정이 필요 없는 위젯 (쿼리/API만 필요) const isSimpleWidget = @@ -122,13 +123,14 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element dataSource, chartConfig, customTitle: customTitle.trim() || undefined, // 빈 문자열이면 undefined + showHeader, // 헤더 표시 여부 }; console.log(" 저장할 element:", updatedElement); onSave(updatedElement); onClose(); - }, [element, dataSource, chartConfig, customTitle, onSave, onClose]); + }, [element, dataSource, chartConfig, customTitle, showHeader, onSave, onClose]); // 모달이 열려있지 않으면 렌더링하지 않음 if (!isOpen) return null; @@ -218,6 +220,20 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element 💡 비워두면 테이블명으로 자동 생성됩니다 (예: "maintenance_schedules 목록")

+ + {/* 헤더 표시 여부 */} +
+ setShowHeader(e.target.checked)} + className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary" + /> + +
{/* 진행 상황 표시 - 간단한 위젯은 표시 안 함 */} diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index cdf70550..1cc4a649 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -55,6 +55,7 @@ export interface DashboardElement { size: Size; title: string; customTitle?: string; // 사용자 정의 제목 (옵션) + showHeader?: boolean; // 헤더 표시 여부 (기본값: true) content: string; dataSource?: ChartDataSource; // 데이터 소스 설정 chartConfig?: ChartConfig; // 차트 설정 diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx index f4f8caea..a363de36 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -280,29 +280,31 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > - {/* 헤더 */} -
-

{element.title}

+ {/* 헤더 (showHeader가 false가 아닐 때만 표시) */} + {element.showHeader !== false && ( +
+

{element.customTitle || element.title}

- {/* 새로고침 버튼 (호버 시에만 표시) */} - {isHovered && ( - - )} -
+ {/* 새로고침 버튼 (호버 시에만 표시) */} + {isHovered && ( + + )} +
+ )} {/* 내용 */} -
+
{element.type === "chart" ? ( ) : (