From f183b4a7272d241157dede2292031dc692e96005 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Thu, 16 Oct 2025 13:48:48 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9C=84=EC=A0=AF=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/app.ts | 8 + .../src/controllers/openApiProxyController.ts | 104 ++++++--- backend-node/src/database/migrations.ts | 27 +++ backend-node/src/services/DashboardService.ts | 14 +- backend-node/src/services/riskAlertService.ts | 206 ++++++++++++++++++ backend-node/src/types/dashboard.ts | 2 + .../(main)/dashboard/[dashboardId]/page.tsx | 3 +- .../admin/dashboard/CanvasElement.tsx | 52 ++--- .../admin/dashboard/DashboardDesigner.tsx | 35 +-- .../admin/dashboard/ElementConfigModal.tsx | 53 ++++- frontend/components/admin/dashboard/types.ts | 1 + .../components/dashboard/DashboardViewer.tsx | 62 ++++-- .../dashboard/widgets/BookingAlertWidget.tsx | 6 +- .../dashboard/widgets/CalculatorWidget.tsx | 8 +- .../dashboard/widgets/DocumentWidget.tsx | 6 +- .../dashboard/widgets/ExchangeWidget.tsx | 34 +-- .../dashboard/widgets/RiskAlertWidget.tsx | 37 +--- .../dashboard/widgets/StatusSummaryWidget.tsx | 14 +- .../dashboard/widgets/TodoWidget.tsx | 6 +- .../dashboard/widgets/WeatherWidget.tsx | 32 +-- 20 files changed, 527 insertions(+), 183 deletions(-) create mode 100644 backend-node/src/database/migrations.ts diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index ae10a6fe..793d7d0a 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 { runMigrations } = await import('./database/migrations'); + await runMigrations(); + } catch (error) { + logger.error(`โŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ:`, error); + } + // ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ ์ดˆ๊ธฐํ™” try { await BatchSchedulerService.initialize(); diff --git a/backend-node/src/controllers/openApiProxyController.ts b/backend-node/src/controllers/openApiProxyController.ts index f737a833..25362866 100644 --- a/backend-node/src/controllers/openApiProxyController.ts +++ b/backend-node/src/controllers/openApiProxyController.ts @@ -17,7 +17,7 @@ export class OpenApiProxyController { console.log(`๐ŸŒค๏ธ ๋‚ ์”จ ์กฐํšŒ ์š”์ฒญ: ${city}`); - // ๊ธฐ์ƒ์ฒญ API Hub ํ‚ค ํ™•์ธ + // ๊ธฐ์ƒ์ฒญ API Hub ํ‚ค ํ™•์ธ (์šฐ์„  ์‚ฌ์šฉ) const apiKey = process.env.KMA_API_KEY; // API ํ‚ค๊ฐ€ ์—†์œผ๋ฉด ํ…Œ์ŠคํŠธ ๋ชจ๋“œ๋กœ ์‹ค์‹œ๊ฐ„ ๋‚ ์”จ ์ œ๊ณต @@ -93,34 +93,18 @@ export class OpenApiProxyController { data: weatherData, }); } catch (error: unknown) { - console.error('โŒ ๋‚ ์”จ ์กฐํšŒ ์‹คํŒจ:', error); + console.error('โŒ ๊ธฐ์ƒ์ฒญ ๋‚ ์”จ ์กฐํšŒ ์‹คํŒจ:', error); - // API ํ˜ธ์ถœ ์‹คํŒจ ์‹œ ์ž๋™์œผ๋กœ ํ…Œ์ŠคํŠธ ๋ชจ๋“œ๋กœ ์ „ํ™˜ - if (axios.isAxiosError(error)) { - const status = error.response?.status; - - // ๋ชจ๋“  ์˜ค๋ฅ˜ โ†’ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ - console.log('โš ๏ธ API ์˜ค๋ฅ˜ ๋ฐœ์ƒ. ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.'); - const { city = '์„œ์šธ' } = req.query; - const regionCode = getKMARegionCode(city as string); - const weatherData = generateRealisticWeatherData(regionCode?.name || (city as string)); - - res.json({ - success: true, - data: weatherData, - }); - } else { - // ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ โ†’ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ - console.log('โš ๏ธ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜. ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.'); - const { city = '์„œ์šธ' } = req.query; - const regionCode = getKMARegionCode(city as string); - const weatherData = generateRealisticWeatherData(regionCode?.name || (city as string)); - - res.json({ - success: true, - data: weatherData, - }); - } + // ๊ธฐ์ƒ์ฒญ ์‹คํŒจ ์‹œ ์‹ค์‹œ๊ฐ„ ๊ทผ์‚ฌ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + console.log('โš ๏ธ ๊ธฐ์ƒ์ฒญ API ์‹คํŒจ. ์‹ค์‹œ๊ฐ„ ๊ทผ์‚ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.'); + const { city = '์„œ์šธ' } = req.query; + const regionCode = getKMARegionCode(city as string); + const weatherData = generateRealisticWeatherData(regionCode?.name || (city as string)); + + res.json({ + success: true, + data: weatherData, + }); } } @@ -250,6 +234,60 @@ export class OpenApiProxyController { } } +/** + * OpenWeatherMap API ํ˜ธ์ถœ (์‹ค์‹œ๊ฐ„ ๋‚ ์”จ) + */ +async function fetchOpenWeatherMapData(city: string, apiKey: string): Promise { + // ํ•œ๊ธ€ ๋„์‹œ๋ช… โ†’ ์˜๋ฌธ ๋ณ€ํ™˜ + const cityMap: Record = { + '์„œ์šธ': 'Seoul', + '๋ถ€์‚ฐ': 'Busan', + '์ธ์ฒœ': 'Incheon', + '๋Œ€๊ตฌ': 'Daegu', + '๊ด‘์ฃผ': 'Gwangju', + '๋Œ€์ „': 'Daejeon', + '์šธ์‚ฐ': 'Ulsan', + '์„ธ์ข…': 'Sejong', + '์ œ์ฃผ': 'Jeju', + '์ˆ˜์›': 'Suwon', + '๊ณ ์–‘': 'Goyang', + '์šฉ์ธ': 'Yongin', + '์ฐฝ์›': 'Changwon', + }; + + const englishCity = cityMap[city] || city; + + console.log(`๐Ÿ“ก OpenWeatherMap API ํ˜ธ์ถœ: ${englishCity}`); + + const url = 'https://api.openweathermap.org/data/2.5/weather'; + const response = await axios.get(url, { + params: { + q: `${englishCity},KR`, + appid: apiKey, + units: 'metric', // ์„ญ์”จ + lang: 'kr', // ํ•œ๊ตญ์–ด + }, + timeout: 10000, + }); + + const data = response.data; + + return { + city: city, // ์›๋ž˜ ์š”์ฒญํ•œ ๋„์‹œ๋ช… ์œ ์ง€ + country: 'KR', + temperature: Math.round(data.main.temp), + feelsLike: Math.round(data.main.feels_like), + humidity: data.main.humidity, + pressure: data.main.pressure, + weatherMain: data.weather[0].main, + weatherDescription: data.weather[0].description, + weatherIcon: data.weather[0].icon, + windSpeed: Math.round(data.wind.speed * 10) / 10, + clouds: data.clouds.all, + timestamp: new Date().toISOString(), + }; +} + /** * ๊ธฐ์ƒ์ฒญ ์ง€์—ญ ์ฝ”๋“œ ๋งคํ•‘ (์ „๊ตญ ์‹œ/๊ตฐ/๊ตฌ ๋‹จ์œ„) */ @@ -606,13 +644,19 @@ function parseKMAHubWeatherData(data: any, regionCode: { name: string; stnId: st } // ์š”์ฒญํ•œ ๊ด€์ธก์†Œ(stnId)์˜ ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ - const targetLine = lines.find((line: string) => { + let targetLine = lines.find((line: string) => { const cols = line.trim().split(/\s+/); return cols[1] === regionCode.stnId; // STN ์ปฌ๋Ÿผ (์ธ๋ฑ์Šค 1) }); + // ์š”์ฒญํ•œ ๊ด€์ธก์†Œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ ๊ด€์ธก์†Œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ if (!targetLine) { - throw new Error(`${regionCode.name} ๊ด€์ธก์†Œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); + console.log(`โš ๏ธ ๊ด€์ธก์†Œ ${regionCode.stnId} (${regionCode.name}) ๋ฐ์ดํ„ฐ ์—†์Œ. ๋‹ค๋ฅธ ๊ด€์ธก์†Œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ`); + targetLine = lines[0]; // ์ฒซ ๋ฒˆ์งธ ๊ด€์ธก์†Œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ + + if (!targetLine) { + throw new Error(`๋‚ ์”จ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`); + } } // ๋ฐ์ดํ„ฐ ๋ผ์ธ ํŒŒ์‹ฑ (๊ณต๋ฐฑ์œผ๋กœ ๊ตฌ๋ถ„) diff --git a/backend-node/src/database/migrations.ts b/backend-node/src/database/migrations.ts new file mode 100644 index 00000000..5433d65d --- /dev/null +++ b/backend-node/src/database/migrations.ts @@ -0,0 +1,27 @@ +/** + * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + * ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹œ ์ž๋™์œผ๋กœ ์‹คํ–‰๋จ + */ + +import { PostgreSQLService } from './PostgreSQLService'; + +export async function runMigrations(): Promise { + console.log('๐Ÿ”„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ์ž‘...'); + + try { + // dashboard_elements์— custom_title, show_header ์ปฌ๋Ÿผ ์ถ”๊ฐ€ + await PostgreSQLService.query(` + ALTER TABLE dashboard_elements + ADD COLUMN IF NOT EXISTS custom_title VARCHAR(255), + ADD COLUMN IF NOT EXISTS show_header BOOLEAN DEFAULT TRUE + `); + + console.log('โœ… dashboard_elements ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ'); + } catch (error) { + console.error('โŒ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์˜ค๋ฅ˜:', error); + throw error; + } + + console.log('โœ… ๋ชจ๋“  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ'); +} + diff --git a/backend-node/src/services/DashboardService.ts b/backend-node/src/services/DashboardService.ts index c25efe4f..69b9aba3 100644 --- a/backend-node/src/services/DashboardService.ts +++ b/backend-node/src/services/DashboardService.ts @@ -53,9 +53,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, dashboardId, @@ -66,6 +66,8 @@ export class DashboardService { 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 || {}), @@ -316,6 +318,8 @@ export class DashboardService { 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 || '{}') @@ -431,9 +435,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, dashboardId, @@ -444,6 +448,8 @@ export class DashboardService { 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 || {}), diff --git a/backend-node/src/services/riskAlertService.ts b/backend-node/src/services/riskAlertService.ts index d911de94..2e10283c 100644 --- a/backend-node/src/services/riskAlertService.ts +++ b/backend-node/src/services/riskAlertService.ts @@ -16,6 +16,21 @@ export interface Alert { timestamp: string; } +export interface WeatherData { + city: string; + country: string; + temperature: number; + feelsLike: number; + humidity: number; + pressure: number; + weatherMain: string; + weatherDescription: string; + weatherIcon: string; + windSpeed: number; + clouds: number; + timestamp: string; +} + export class RiskAlertService { /** * ๊ธฐ์ƒ์ฒญ ํŠน๋ณด ์ •๋ณด ์กฐํšŒ (๊ธฐ์ƒ์ฒญ API ํ—ˆ๋ธŒ - ํ˜„์žฌ ๋ฐœํšจ ์ค‘์ธ ํŠน๋ณด API) @@ -361,6 +376,134 @@ export class RiskAlertService { return this.generateDummyRoadworkAlerts(); } + /** + * ๊ธฐ์ƒ์ฒญ ์‹ค์‹œ๊ฐ„ ๋‚ ์”จ ์ •๋ณด ์กฐํšŒ + */ + async getCurrentWeather(city: string = '์„œ์šธ'): Promise { + try { + const apiKey = process.env.KMA_API_KEY; + + if (!apiKey) { + console.log('โš ๏ธ ๊ธฐ์ƒ์ฒญ API ํ‚ค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ทผ์‚ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.'); + return this.generateRealisticWeatherData(city); + } + + // ๋„์‹œ๋ช… โ†’ ๊ด€์ธก์†Œ ์ฝ”๋“œ ๋งคํ•‘ + const regionCode = this.getKMARegionCode(city); + + if (!regionCode) { + console.log(`โš ๏ธ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ง€์—ญ: ${city}. ๊ทผ์‚ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.`); + return this.generateRealisticWeatherData(city); + } + + // ๊ธฐ์ƒ์ฒญ API Hub - ์ง€์ƒ๊ด€์ธก์‹œ๊ฐ„์ž๋ฃŒ + const now = new Date(); + const minute = now.getMinutes(); + let targetTime = new Date(now); + + // ๋ฐ์ดํ„ฐ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์•˜์œผ๋ฉด ์ด์ „ ์‹œ๊ฐ„์œผ๋กœ + if (minute < 10) { + targetTime = new Date(now.getTime() - 60 * 60 * 1000); + } + + const year = targetTime.getFullYear(); + const month = String(targetTime.getMonth() + 1).padStart(2, '0'); + const day = String(targetTime.getDate()).padStart(2, '0'); + const hour = String(targetTime.getHours()).padStart(2, '0'); + const tm = `${year}${month}${day}${hour}00`; + + const url = 'https://apihub.kma.go.kr/api/typ01/url/kma_sfctm2.php'; + + console.log(`๐Ÿ“ก ๊ธฐ์ƒ์ฒญ ์‹ค์‹œ๊ฐ„ ๋‚ ์”จ ์กฐํšŒ: ${regionCode.name} (${tm})`); + + const response = await axios.get(url, { + params: { + tm: tm, + stn: 0, + authKey: apiKey, + help: 0, + disp: 1, + }, + timeout: 10000, + responseType: 'arraybuffer', + }); + + // EUC-KR ๋””์ฝ”๋”ฉ + const iconv = require('iconv-lite'); + const responseText = iconv.decode(Buffer.from(response.data), 'EUC-KR'); + + // ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ + const lines = responseText.split('\n').filter((line: string) => line.trim() && !line.startsWith('#')); + + if (lines.length === 0) { + throw new Error('๋‚ ์”จ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'); + } + + // ์š”์ฒญํ•œ ๊ด€์ธก์†Œ ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ (์—†์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ ๊ด€์ธก์†Œ ์‚ฌ์šฉ) + let targetLine = lines.find((line: string) => { + const cols = line.trim().split(/\s+/); + return cols[1] === regionCode.stnId; + }); + + if (!targetLine) { + console.log(`โš ๏ธ ๊ด€์ธก์†Œ ${regionCode.stnId} ๋ฐ์ดํ„ฐ ์—†์Œ. ๋‹ค๋ฅธ ๊ด€์ธก์†Œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ`); + targetLine = lines[0]; + } + + const values = targetLine.trim().split(/\s+/); + + // ๋ฐ์ดํ„ฐ ์ถ”์ถœ + const temperature = parseFloat(values[11]) || 0; // TA: ๊ธฐ์˜จ + const humidity = parseFloat(values[13]) || 0; // HM: ์Šต๋„ + const pressure = parseFloat(values[7]) || 1013; // PA: ๊ธฐ์•• + const windSpeed = parseFloat(values[3]) || 0; // WS: ํ’์† + const rainfall = parseFloat(values[15]) || 0; // RN: ๊ฐ•์ˆ˜๋Ÿ‰ + + console.log(`โœ… ๊ธฐ์ƒ์ฒญ ์‹ค์‹œ๊ฐ„ ๋‚ ์”จ: ${regionCode.name} ${temperature}ยฐC, ์Šต๋„ ${humidity}%`); + + // ๋‚ ์”จ ์ƒํƒœ ์ถ”์ • + let weatherMain = 'Clear'; + let weatherDescription = '๋ง‘์Œ'; + let weatherIcon = '01d'; + let clouds = 10; + + if (rainfall > 0) { + weatherMain = 'Rain'; + weatherDescription = rainfall >= 10 ? '๋น„ (๊ฐ•์ˆ˜)' : '๋น„'; + weatherIcon = rainfall >= 10 ? '10d' : '09d'; + clouds = 100; + } else if (humidity > 80) { + weatherMain = 'Clouds'; + weatherDescription = 'ํ๋ฆผ'; + weatherIcon = '04d'; + clouds = 90; + } else if (humidity > 60) { + weatherMain = 'Clouds'; + weatherDescription = '๊ตฌ๋ฆ„ ๋งŽ์Œ'; + weatherIcon = '03d'; + clouds = 60; + } + + return { + city: regionCode.name, + country: 'KR', + temperature: Math.round(temperature), + feelsLike: Math.round(temperature - 2), + humidity: Math.round(humidity), + pressure: Math.round(pressure), + weatherMain, + weatherDescription, + weatherIcon, + windSpeed: Math.round(windSpeed * 10) / 10, + clouds, + timestamp: new Date().toISOString(), + }; + } catch (error: any) { + console.error('โŒ ๊ธฐ์ƒ์ฒญ ์‹ค์‹œ๊ฐ„ ๋‚ ์”จ ์กฐํšŒ ์‹คํŒจ:', error.message); + return this.generateRealisticWeatherData(city); + } + } + /** * ์ „์ฒด ์•Œ๋ฆผ ์กฐํšŒ (ํ†ตํ•ฉ) */ @@ -385,6 +528,69 @@ export class RiskAlertService { } } + /** + * ๋„์‹œ๋ช… โ†’ ๊ธฐ์ƒ์ฒญ ๊ด€์ธก์†Œ ์ฝ”๋“œ ๋งคํ•‘ + */ + private getKMARegionCode(city: string): { name: string; stnId: string } | null { + const regions: Record = { + 'Seoul': { name: '์„œ์šธ', stnId: '108' }, + 'Busan': { name: '๋ถ€์‚ฐ', stnId: '159' }, + 'Incheon': { name: '์ธ์ฒœ', stnId: '112' }, + '์„œ์šธ': { name: '์„œ์šธ', stnId: '108' }, + '๋ถ€์‚ฐ': { name: '๋ถ€์‚ฐ', stnId: '159' }, + '์ธ์ฒœ': { name: '์ธ์ฒœ', stnId: '112' }, + '๋Œ€๊ตฌ': { name: '๋Œ€๊ตฌ', stnId: '143' }, + '๊ด‘์ฃผ': { name: '๊ด‘์ฃผ', stnId: '156' }, + '๋Œ€์ „': { name: '๋Œ€์ „', stnId: '133' }, + '์šธ์‚ฐ': { name: '์šธ์‚ฐ', stnId: '152' }, + '์„ธ์ข…': { name: '์„ธ์ข…', stnId: '239' }, + '์ œ์ฃผ': { name: '์ œ์ฃผ', stnId: '184' }, + }; + return regions[city] || null; + } + + /** + * ์‹ค์‹œ๊ฐ„ ๊ทผ์‚ฌ ๋‚ ์”จ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ + */ + private generateRealisticWeatherData(cityName: string): WeatherData { + const now = new Date(); + const hour = now.getHours(); + const month = now.getMonth() + 1; + + // ์‹œ๊ฐ„๋Œ€๋ณ„ ๊ธฐ์˜จ ๋ณ€ํ™” + let baseTemp = 15; + if (hour >= 6 && hour < 9) baseTemp = 12; + else if (hour >= 9 && hour < 12) baseTemp = 18; + else if (hour >= 12 && hour < 15) baseTemp = 22; + else if (hour >= 15 && hour < 18) baseTemp = 20; + else if (hour >= 18 && hour < 21) baseTemp = 16; + else baseTemp = 13; + + // ๊ณ„์ ˆ๋ณ„ ๋ณด์ • + if (month >= 12 || month <= 2) baseTemp -= 8; + else if (month >= 3 && month <= 5) baseTemp += 2; + else if (month >= 6 && month <= 8) baseTemp += 8; + else baseTemp += 3; + + const tempVariation = Math.floor(Math.random() * 5) - 2; + const temperature = baseTemp + tempVariation; + + return { + city: cityName, + country: 'KR', + temperature: Math.round(temperature), + feelsLike: Math.round(temperature - 2), + humidity: Math.floor(Math.random() * 30) + 50, + pressure: Math.floor(Math.random() * 15) + 1008, + weatherMain: 'Clear', + weatherDescription: '๋ง‘์Œ', + weatherIcon: '01d', + windSpeed: Math.random() * 4 + 1, + clouds: 10, + timestamp: now.toISOString(), + }; + } + /** * ๊ธฐ์ƒ์ฒญ ์‹œ๊ฐ„ ํ˜•์‹ ํŒŒ์‹ฑ (YYYYMMDDHHmm -> ISO) */ diff --git a/backend-node/src/types/dashboard.ts b/backend-node/src/types/dashboard.ts index c37beae8..3428ec8e 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/app/(main)/dashboard/[dashboardId]/page.tsx b/frontend/app/(main)/dashboard/[dashboardId]/page.tsx index 0705d77b..adf37986 100644 --- a/frontend/app/(main)/dashboard/[dashboardId]/page.tsx +++ b/frontend/app/(main)/dashboard/[dashboardId]/page.tsx @@ -46,6 +46,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) { try { const dashboardData = await dashboardApi.getDashboard(resolvedParams.dashboardId); + console.log("๐Ÿ“Š ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ๋จ:", dashboardData); setDashboard(dashboardData); } catch (apiError) { console.warn("API ํ˜ธ์ถœ ์‹คํŒจ, ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ํ™•์ธ:", apiError); @@ -156,7 +157,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) { {/* ๋Œ€์‹œ๋ณด๋“œ ๋ทฐ์–ด */}
- +
); diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 5c75acb7..a70bfd61 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -447,34 +447,34 @@ export function CanvasElement({ }} onMouseDown={handleMouseDown} > - {/* ํ—ค๋” */} + {/* ํ—ค๋” (ํŽธ์ง‘ ๋ชจ๋“œ์—์„œ๋Š” ํ•ญ์ƒ ํ‘œ์‹œ) */}
- {element.title} -
- {/* ์„ค์ • ๋ฒ„ํŠผ (์‹œ๊ณ„, ๋‹ฌ๋ ฅ, ๊ธฐ์‚ฌ๊ด€๋ฆฌ ์œ„์ ฏ์€ ์ž์ฒด ์„ค์ • UI ์‚ฌ์šฉ) */} - {onConfigure && - !( - element.type === "widget" && - (element.subtype === "clock" || element.subtype === "calendar" || element.subtype === "driver-management") - ) && ( - - )} - {/* ์‚ญ์ œ ๋ฒ„ํŠผ */} - + {element.customTitle || element.title} +
+ {/* ์„ค์ • ๋ฒ„ํŠผ (์‹œ๊ณ„, ๋‹ฌ๋ ฅ, ๊ธฐ์‚ฌ๊ด€๋ฆฌ ์œ„์ ฏ์€ ์ž์ฒด ์„ค์ • UI ์‚ฌ์šฉ) */} + {onConfigure && + !( + element.type === "widget" && + (element.subtype === "clock" || element.subtype === "calendar" || element.subtype === "driver-management") + ) && ( + + )} + {/* ์‚ญ์ œ ๋ฒ„ํŠผ */} + +
-
{/* ๋‚ด์šฉ */}
diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index 65ba514c..4539bee5 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -117,6 +117,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D // ์š”์†Œ ์—…๋ฐ์ดํŠธ const updateElement = useCallback((id: string, updates: Partial) => { + console.log("๐Ÿ”ง updateElement ํ˜ธ์ถœ:", id, updates); setElements((prev) => prev.map((el) => (el.id === id ? { ...el, ...updates } : el))); }, []); @@ -153,9 +154,11 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D // ์š”์†Œ ์„ค์ • ์ €์žฅ const saveElementConfig = useCallback( (updatedElement: DashboardElement) => { + console.log("๐Ÿ’พ saveElementConfig ํ˜ธ์ถœ:", updatedElement.id, updatedElement.customTitle); updateElement(updatedElement.id, updatedElement); + closeConfigModal(); }, - [updateElement], + [updateElement, closeConfigModal], ); // ๋ฆฌ์ŠคํŠธ ์œ„์ ฏ ์„ค์ • ์ €์žฅ (Partial ์—…๋ฐ์ดํŠธ) @@ -186,6 +189,8 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D position: el.position, size: el.size, title: el.title, + customTitle: el.customTitle, // customTitle์„ ๋ณ„๋„๋กœ ์ €์žฅ + showHeader: el.showHeader, // ํ—ค๋” ํ‘œ์‹œ ์—ฌ๋ถ€๋„ ์ €์žฅ content: el.content, dataSource: el.dataSource, chartConfig: el.chartConfig, @@ -310,36 +315,36 @@ function getElementTitle(type: ElementType, subtype: ElementSubtype): string { if (type === "chart") { switch (subtype) { case "bar": - return "๐Ÿ“Š ๋ฐ” ์ฐจํŠธ"; + return "๋ฐ” ์ฐจํŠธ"; case "horizontal-bar": - return "๐Ÿ“Š ์ˆ˜ํ‰ ๋ฐ” ์ฐจํŠธ"; + return "์ˆ˜ํ‰ ๋ฐ” ์ฐจํŠธ"; case "pie": - return "๐Ÿฅง ์›ํ˜• ์ฐจํŠธ"; + return "์›ํ˜• ์ฐจํŠธ"; case "line": - return "๐Ÿ“ˆ ๊บพ์€์„  ์ฐจํŠธ"; + return "๊บพ์€์„  ์ฐจํŠธ"; default: - return "๐Ÿ“Š ์ฐจํŠธ"; + return "์ฐจํŠธ"; } } else if (type === "widget") { switch (subtype) { case "exchange": - return "๐Ÿ’ฑ ํ™˜์œจ ์œ„์ ฏ"; + return "ํ™˜์œจ ์œ„์ ฏ"; case "weather": - return "โ˜๏ธ ๋‚ ์”จ ์œ„์ ฏ"; + return "๋‚ ์”จ ์œ„์ ฏ"; case "clock": - return "โฐ ์‹œ๊ณ„ ์œ„์ ฏ"; + return "์‹œ๊ณ„ ์œ„์ ฏ"; case "calculator": - return "๐Ÿงฎ ๊ณ„์‚ฐ๊ธฐ ์œ„์ ฏ"; + return "๊ณ„์‚ฐ๊ธฐ ์œ„์ ฏ"; case "vehicle-map": - return "๐Ÿšš ์ฐจ๋Ÿ‰ ์œ„์น˜ ์ง€๋„"; + return "์ฐจ๋Ÿ‰ ์œ„์น˜ ์ง€๋„"; case "calendar": - return "๐Ÿ“… ๋‹ฌ๋ ฅ ์œ„์ ฏ"; + return "๋‹ฌ๋ ฅ ์œ„์ ฏ"; case "driver-management": - return "๐Ÿšš ๊ธฐ์‚ฌ ๊ด€๋ฆฌ ์œ„์ ฏ"; + return "๊ธฐ์‚ฌ ๊ด€๋ฆฌ ์œ„์ ฏ"; case "list": - return "๐Ÿ“‹ ๋ฆฌ์ŠคํŠธ ์œ„์ ฏ"; + return "๋ฆฌ์ŠคํŠธ ์œ„์ ฏ"; default: - return "๐Ÿ”ง ์œ„์ ฏ"; + return "์œ„์ ฏ"; } } return "์š”์†Œ"; diff --git a/frontend/components/admin/dashboard/ElementConfigModal.tsx b/frontend/components/admin/dashboard/ElementConfigModal.tsx index aa074317..2b35e97d 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 = @@ -58,6 +59,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element setQueryResult(null); setCurrentStep(1); setCustomTitle(element.customTitle || ""); + setShowHeader(element.showHeader !== false); } }, [isOpen, element]); @@ -122,13 +124,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; @@ -141,6 +144,13 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element return null; } + // ํ™˜์œจ, ๋‚ ์”จ, ๊ณ„์‚ฐ๊ธฐ, ๋ฆฌ์Šคํฌ/์•Œ๋ฆผ, To-Do, ๋ฌธ์„œ๊ด€๋ฆฌ, ์˜ˆ์•ฝ์š”์ฒญ์•Œ๋ฆผ ์œ„์ ฏ์€ ์ œ๋ชฉ๋งŒ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•œ ๊ฐ„๋‹จํ•œ ๋ชจ๋‹ฌ ํ‘œ์‹œ + const isSimpleConfigWidget = + element.type === "widget" && + (element.subtype === "exchange" || element.subtype === "weather" || element.subtype === "calculator" || + element.subtype === "risk-alert" || element.subtype === "todo" || element.subtype === "document" || + element.subtype === "booking-alert"); + // ์ €์žฅ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ const isPieChart = element.subtype === "pie" || element.subtype === "donut"; const isApiSource = dataSource.type === "api"; @@ -153,7 +163,9 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element // customTitle์ด ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ const isTitleChanged = customTitle.trim() !== (element.customTitle || ""); - const canSave = isTitleChanged || // ์ œ๋ชฉ๋งŒ ๋ณ€๊ฒฝํ•ด๋„ ์ €์žฅ ๊ฐ€๋Šฅ + const canSave = + isSimpleConfigWidget || // ๊ฐ„๋‹จํ•œ ์„ค์ • ์œ„์ ฏ์€ ํ•ญ์ƒ ์ €์žฅ ๊ฐ€๋Šฅ + isTitleChanged || // ์ œ๋ชฉ๋งŒ ๋ณ€๊ฒฝํ•ด๋„ ์ €์žฅ ๊ฐ€๋Šฅ (isSimpleWidget ? // ๊ฐ„๋‹จํ•œ ์œ„์ ฏ: 2๋‹จ๊ณ„์—์„œ ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ ํ›„ ์ €์žฅ ๊ฐ€๋Šฅ currentStep === 2 && queryResult && queryResult.rows.length > 0 @@ -190,11 +202,13 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element

{element.title} ์„ค์ •

- {isSimpleWidget - ? "๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์„ค์ •ํ•˜์„ธ์š”" - : currentStep === 1 - ? "๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์„ ํƒํ•˜์„ธ์š”" - : "์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์ฐจํŠธ๋ฅผ ์„ค์ •ํ•˜์„ธ์š”"} + {isSimpleConfigWidget + ? "์œ„์ ฏ ์ œ๋ชฉ๊ณผ ํ‘œ์‹œ ์˜ต์…˜์„ ์„ค์ •ํ•˜์„ธ์š”" + : isSimpleWidget + ? "๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์„ค์ •ํ•˜์„ธ์š”" + : currentStep === 1 + ? "๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์„ ํƒํ•˜์„ธ์š”" + : "์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์ฐจํŠธ๋ฅผ ์„ค์ •ํ•˜์„ธ์š”"}

+ + {/* ํ—ค๋” ํ‘œ์‹œ ์—ฌ๋ถ€ */} +
+ +

+ ์ฒดํฌ ํ•ด์ œ ์‹œ ํšŒ์ƒ‰ ํ—ค๋”๊ฐ€ ์ˆจ๊ฒจ์ง€๊ณ  ์œ„์ ฏ์ด ์ „์ฒด ์˜์—ญ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค +

+
{/* ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ - ๊ฐ„๋‹จํ•œ ์œ„์ ฏ์€ ํ‘œ์‹œ ์•ˆ ํ•จ */} - {!isSimpleWidget && ( + {!isSimpleWidget && !isSimpleConfigWidget && (
@@ -233,7 +263,12 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element {/* ๋‹จ๊ณ„๋ณ„ ๋‚ด์šฉ */}
- {currentStep === 1 && ( + {/* ๊ฐ„๋‹จํ•œ ์„ค์ • ์œ„์ ฏ (ํ™˜์œจ, ๋‚ ์”จ, ๊ณ„์‚ฐ๊ธฐ)๋Š” ์ œ๋ชฉ๋งŒ ํ‘œ์‹œ */} + {isSimpleConfigWidget ? ( +
+

์œ„ ์ œ๋ชฉ ์ž…๋ ฅ๋ž€๊ณผ ํ—ค๋” ํ‘œ์‹œ ์˜ต์…˜์„ ์„ค์ •ํ•œ ํ›„ ์ €์žฅํ•˜์„ธ์š”.

+
+ ) : currentStep === 1 && ( )} 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 c6e941e3..3da45b7b 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -123,6 +123,11 @@ export function DashboardViewer({ elements, dashboardId, refreshInterval }: Dash const [elementData, setElementData] = useState>({}); const [loadingElements, setLoadingElements] = useState>(new Set()); const [lastRefresh, setLastRefresh] = useState(new Date()); + + // elements ๋ณ€๊ฒฝ ๊ฐ์ง€ + React.useEffect(() => { + console.log("๐ŸŽฏ DashboardViewer elements ๋ณ€๊ฒฝ๋จ:", elements.map(el => ({ id: el.id, customTitle: el.customTitle, title: el.title }))); + }, [elements]); // ๊ฐœ๋ณ„ ์š”์†Œ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ const loadElementData = useCallback(async (element: DashboardElement) => { @@ -263,10 +268,12 @@ interface ViewerElementProps { */ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementProps) { const [isHovered, setIsHovered] = useState(false); + + console.log("๐Ÿ–ผ๏ธ ViewerElement ๋ Œ๋”๋ง:", element.id, "customTitle:", element.customTitle, "title:", element.title); return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > - {/* ํ—ค๋” */} -
-

{element.title}

+ {/* ํ—ค๋” (showHeader๊ฐ€ true์ผ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {element.showHeader !== false && ( +
+

{element.title}

- {/* ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ (ํ˜ธ๋ฒ„ ์‹œ์—๋งŒ ํ‘œ์‹œ) */} - {isHovered && ( - - )} -
+ {/* ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ (ํ˜ธ๋ฒ„ ์‹œ์—๋งŒ ํ‘œ์‹œ) */} + {isHovered && ( + + )} +
+ )} {/* ๋‚ด์šฉ */} -
+
{element.type === "chart" ? ( - - ) : renderWidget(element)} + + ) : element.subtype === "map-summary" ? ( + // ์ง€๋„๋Š” key ์—†์ด ๋ Œ๋”๋ง (Leaflet ์ดˆ๊ธฐํ™” ๋ฌธ์ œ ๋ฐฉ์ง€) + renderWidget(element) + ) : ( +
+ {renderWidget(element)} +
+ )}
{/* ๋กœ๋”ฉ ์˜ค๋ฒ„๋ ˆ์ด */} diff --git a/frontend/components/dashboard/widgets/BookingAlertWidget.tsx b/frontend/components/dashboard/widgets/BookingAlertWidget.tsx index b47f0fb4..a14edd46 100644 --- a/frontend/components/dashboard/widgets/BookingAlertWidget.tsx +++ b/frontend/components/dashboard/widgets/BookingAlertWidget.tsx @@ -149,7 +149,7 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps) } return ( -
+
{/* ์‹ ๊ทœ ์•Œ๋ฆผ ๋ฐฐ๋„ˆ */} {showNotification && newCount > 0 && (
@@ -158,10 +158,10 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps) )} {/* ํ—ค๋” */} -
+
-

๐Ÿ”” {element?.customTitle || "์˜ˆ์•ฝ ์š”์ฒญ ์•Œ๋ฆผ"}

+

{element?.customTitle || element?.title || "์˜ˆ์•ฝ ์š”์ฒญ ์•Œ๋ฆผ"}

{newCount > 0 && ( {newCount} diff --git a/frontend/components/dashboard/widgets/CalculatorWidget.tsx b/frontend/components/dashboard/widgets/CalculatorWidget.tsx index b8816bbc..4ab09440 100644 --- a/frontend/components/dashboard/widgets/CalculatorWidget.tsx +++ b/frontend/components/dashboard/widgets/CalculatorWidget.tsx @@ -118,10 +118,12 @@ export default function CalculatorWidget({ element, className = '' }: Calculator }; return ( -
+
- {/* ์ œ๋ชฉ */} -

๐Ÿงฎ {element?.customTitle || "๊ณ„์‚ฐ๊ธฐ"}

+ {/* ์ œ๋ชฉ (showHeader๊ฐ€ true์ผ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {element?.showHeader !== false && ( +

{element?.customTitle || "๊ณ„์‚ฐ๊ธฐ"}

+ )} {/* ๋””์Šคํ”Œ๋ ˆ์ด */}
diff --git a/frontend/components/dashboard/widgets/DocumentWidget.tsx b/frontend/components/dashboard/widgets/DocumentWidget.tsx index 6a15cce1..62afc540 100644 --- a/frontend/components/dashboard/widgets/DocumentWidget.tsx +++ b/frontend/components/dashboard/widgets/DocumentWidget.tsx @@ -128,11 +128,11 @@ export default function DocumentWidget({ element }: DocumentWidgetProps) { }; return ( -
+
{/* ํ—ค๋” */} -
+
-

๐Ÿ“‚ {element?.customTitle || "๋ฌธ์„œ ๊ด€๋ฆฌ"}

+

{element?.customTitle || element?.title || "๋ฌธ์„œ ๊ด€๋ฆฌ"}

diff --git a/frontend/components/dashboard/widgets/ExchangeWidget.tsx b/frontend/components/dashboard/widgets/ExchangeWidget.tsx index 86743326..d9db986e 100644 --- a/frontend/components/dashboard/widgets/ExchangeWidget.tsx +++ b/frontend/components/dashboard/widgets/ExchangeWidget.tsx @@ -135,12 +135,13 @@ export default function ExchangeWidget({ const hasError = error || !exchangeRate; return ( -
- {/* ํ—ค๋” */} -
-
-

๐Ÿ’ฑ {element?.customTitle || "ํ™˜์œจ"}

-

+

+ {/* ํ—ค๋” (showHeader๊ฐ€ true์ผ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {element?.showHeader !== false && ( +
+
+

{element?.customTitle || "ํ™˜์œจ"}

+

{lastUpdated ? `์—…๋ฐ์ดํŠธ: ${lastUpdated.toLocaleTimeString('ko-KR', { hour: '2-digit', @@ -149,16 +150,17 @@ export default function ExchangeWidget({ : ''}

- -
+ +
+ )} {/* ํ†ตํ™” ์„ ํƒ */}
diff --git a/frontend/components/dashboard/widgets/RiskAlertWidget.tsx b/frontend/components/dashboard/widgets/RiskAlertWidget.tsx index e4ace173..0340722e 100644 --- a/frontend/components/dashboard/widgets/RiskAlertWidget.tsx +++ b/frontend/components/dashboard/widgets/RiskAlertWidget.tsx @@ -163,32 +163,19 @@ export default function RiskAlertWidget({ element }: RiskAlertWidgetProps) { }; return ( -
- {/* ํ—ค๋” */} -
-
- -

{element?.customTitle || "๋ฆฌ์Šคํฌ / ์•Œ๋ฆผ"}

- {stats.high > 0 && ( - ๊ธด๊ธ‰ {stats.high}๊ฑด - )} +
+ {/* ํ—ค๋” (showHeader๊ฐ€ true์ผ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {element?.showHeader !== false && ( +
+
+ +

{element?.customTitle || "๋ฆฌ์Šคํฌ / ์•Œ๋ฆผ"}

+
+ + {stats.high}๊ฑด ๊ธด๊ธ‰ +
-
- {lastUpdated && newAlertIds.size > 0 && ( - - ์ƒˆ ์•Œ๋ฆผ {newAlertIds.size}๊ฑด - - )} - {lastUpdated && ( - - {lastUpdated.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })} - - )} - -
-
+ )} {/* ํ†ต๊ณ„ ์นด๋“œ */}
diff --git a/frontend/components/dashboard/widgets/StatusSummaryWidget.tsx b/frontend/components/dashboard/widgets/StatusSummaryWidget.tsx index e5478cdb..43ba681e 100644 --- a/frontend/components/dashboard/widgets/StatusSummaryWidget.tsx +++ b/frontend/components/dashboard/widgets/StatusSummaryWidget.tsx @@ -353,20 +353,20 @@ export default function StatusSummaryWidget({ const displayTitle = element.customTitle || (tableName ? `${translateTableName(tableName)} ํ˜„ํ™ฉ` : title); return ( -
+
{/* ํ—ค๋” */} -
+
-

{icon} {displayTitle}

+

{icon} {displayTitle}

{totalCount > 0 ? ( -

์ด {totalCount.toLocaleString()}๊ฑด

+

์ด {totalCount.toLocaleString()}๊ฑด

) : ( -

โš™๏ธ ๋ฐ์ดํ„ฐ ์—ฐ๊ฒฐ ํ•„์š”

+

โš™๏ธ ๋ฐ์ดํ„ฐ ์—ฐ๊ฒฐ ํ•„์š”

)}
-
+ +
+ )} {/* ๋ฐ˜์‘ํ˜• ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ - ์ž๋™ ์กฐ์ • */}