diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 9483726a..6660bc13 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -233,6 +233,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 a9164370..c980b7e6 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -338,6 +338,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 ef2f1e1c..08431996 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; @@ -217,6 +219,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 90668874..73a4bf89 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -56,6 +56,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 d5790779..2f587db4 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -300,29 +300,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" ? ( ) : (