diff --git a/backend-node/.env.example b/backend-node/.env.example index fdba2895..807ae916 100644 --- a/backend-node/.env.example +++ b/backend-node/.env.example @@ -10,3 +10,8 @@ BOOKING_DATA_SOURCE=file MAINTENANCE_DATA_SOURCE=memory DOCUMENT_DATA_SOURCE=memory + +# OpenWeatherMap API 키 추가 (실시간 날씨) +# https://openweathermap.org/api 에서 무료 가입 후 발급 +OPENWEATHER_API_KEY=your_openweathermap_api_key_here + diff --git a/backend-node/src/controllers/openApiProxyController.ts b/backend-node/src/controllers/openApiProxyController.ts index f737a833..b84dc218 100644 --- a/backend-node/src/controllers/openApiProxyController.ts +++ b/backend-node/src/controllers/openApiProxyController.ts @@ -17,19 +17,54 @@ export class OpenApiProxyController { console.log(`🌤️ 날씨 조회 요청: ${city}`); - // 기상청 API Hub 키 확인 + // 1순위: OpenWeatherMap API (실시간에 가까움, 10분마다 업데이트) + const openWeatherKey = process.env.OPENWEATHER_API_KEY; + if (openWeatherKey) { + try { + console.log(`🌍 OpenWeatherMap API 호출: ${city}`); + const response = await axios.get('https://api.openweathermap.org/data/2.5/weather', { + params: { + q: `${city},KR`, + appid: openWeatherKey, + units: 'metric', + lang: 'kr', + }, + timeout: 10000, + }); + + const data = response.data; + const weatherData = { + city: data.name, + country: data.sys.country, + 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(), + }; + + console.log(`✅ OpenWeatherMap 날씨 조회 성공: ${weatherData.city} ${weatherData.temperature}°C`); + res.json({ success: true, data: weatherData }); + return; + } catch (error) { + console.warn('⚠️ OpenWeatherMap API 실패, 기상청 API로 폴백:', error instanceof Error ? error.message : error); + } + } + + // 2순위: 기상청 API Hub (매시간 정시 데이터) const apiKey = process.env.KMA_API_KEY; - // API 키가 없으면 테스트 모드로 실시간 날씨 제공 + // API 키가 없으면 오류 반환 if (!apiKey) { - console.log('⚠️ 기상청 API 키가 없습니다. 테스트 데이터를 반환합니다.'); - - const regionCode = getKMARegionCode(city as string); - const weatherData = generateRealisticWeatherData(regionCode?.name || (city as string)); - - res.json({ - success: true, - data: weatherData, + console.log('⚠️ 기상청 API 키가 설정되지 않았습니다.'); + res.status(503).json({ + success: false, + message: '기상청 API 키가 설정되지 않았습니다. 관리자에게 문의하세요.', }); return; } @@ -48,32 +83,39 @@ export class OpenApiProxyController { // 기상청 API Hub 사용 (apihub.kma.go.kr) const now = new Date(); - // 기상청 데이터는 매시간 정시(XX:00)에 발표되고 약 10분 후 조회 가능 - // 현재 시각이 XX:10 이전이면 이전 시간 데이터 조회 - const minute = now.getMinutes(); - let targetTime = new Date(now); + // 한국 시간(KST = UTC+9)으로 변환 + const kstOffset = 9 * 60 * 60 * 1000; // 9시간을 밀리초로 + const kstNow = new Date(now.getTime() + kstOffset); - if (minute < 10) { - // 아직 이번 시간 데이터가 업데이트되지 않음 → 이전 시간으로 - targetTime = new Date(now.getTime() - 60 * 60 * 1000); - } + // 기상청 지상관측 데이터는 매시간 정시(XX:00)에 발표 + // 가장 최근의 정시 데이터를 가져오기 위해 현재 시간의 정시로 설정 + const targetTime = new Date(kstNow); // tm 파라미터: YYYYMMDDHH00 형식 (정시만 조회) - 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 year = targetTime.getUTCFullYear(); + const month = String(targetTime.getUTCMonth() + 1).padStart(2, '0'); + const day = String(targetTime.getUTCDate()).padStart(2, '0'); + const hour = String(targetTime.getUTCHours()).padStart(2, '0'); const tm = `${year}${month}${day}${hour}00`; + + console.log(`🕐 현재 시각(KST): ${kstNow.toISOString().slice(0, 16).replace('T', ' ')}, 조회 시각: ${tm}`); - // 기상청 API Hub - 지상관측시간자료 - const url = 'https://apihub.kma.go.kr/api/typ01/url/kma_sfctm2.php'; + // 기상청 API Hub - 지상관측시간자료 (시간 범위 조회로 최신 데이터 확보) + // sfctm3: 시간 범위 조회 가능 (tm1~tm2) + const url = 'https://apihub.kma.go.kr/api/typ01/url/kma_sfctm3.php'; + + // 최근 1시간 범위 조회 (현재 시간 - 1시간 ~ 현재 시간) - KST 기준 + const tm1Time = new Date(kstNow.getTime() - 60 * 60 * 1000); // 1시간 전 + const tm1 = `${tm1Time.getUTCFullYear()}${String(tm1Time.getUTCMonth() + 1).padStart(2, '0')}${String(tm1Time.getUTCDate()).padStart(2, '0')}${String(tm1Time.getUTCHours()).padStart(2, '0')}00`; + const tm2 = tm; // 현재 시간 - console.log(`📡 기상청 API Hub 호출: ${regionCode.name} (관측소: ${regionCode.stnId}, 시간: ${tm})`); + console.log(`📡 기상청 API Hub 호출: ${regionCode.name} (관측소: ${regionCode.stnId}, 기간: ${tm1}~${tm2})`); const response = await axios.get(url, { params: { - tm: tm, - stn: 0, // 0 = 전체 관측소 데이터 조회 + tm1: tm1, + tm2: tm2, + stn: regionCode.stnId, // 특정 관측소만 조회 authKey: apiKey, help: 0, disp: 1, @@ -95,30 +137,36 @@ export class OpenApiProxyController { } catch (error: unknown) { console.error('❌ 날씨 조회 실패:', error); - // API 호출 실패 시 자동으로 테스트 모드로 전환 + // 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, - }); + if (status === 401 || status === 403) { + res.status(401).json({ + success: false, + message: '기상청 API 인증에 실패했습니다. API 키를 확인하세요.', + }); + } else if (status === 404) { + res.status(404).json({ + success: false, + message: '기상청 API에서 데이터를 찾을 수 없습니다.', + }); + } else if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') { + res.status(504).json({ + success: false, + message: '기상청 API 연결 시간이 초과되었습니다. 잠시 후 다시 시도하세요.', + }); + } else { + res.status(500).json({ + success: false, + message: '기상청 API 호출 중 오류가 발생했습니다.', + error: error.message, + }); + } } 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, + res.status(500).json({ + success: false, + message: '날씨 정보를 가져오는 중 예상치 못한 오류가 발생했습니다.', }); } } @@ -169,15 +217,19 @@ export class OpenApiProxyController { } catch (error: unknown) { console.error('❌ 환율 조회 실패:', error); - // API 호출 실패 시 실제 근사값 반환 - console.log('⚠️ API 오류 발생. 근사값을 반환합니다.'); - const { base = 'KRW', target = 'USD' } = req.query; - const approximateRate = generateRealisticExchangeRate(base as string, target as string); - - res.json({ - success: true, - data: approximateRate, - }); + // API 호출 실패 시 명확한 오류 메시지 반환 + if (axios.isAxiosError(error)) { + res.status(500).json({ + success: false, + message: '환율 정보를 가져오는 중 오류가 발생했습니다.', + error: error.message, + }); + } else { + res.status(500).json({ + success: false, + message: '환율 정보를 가져오는 중 예상치 못한 오류가 발생했습니다.', + }); + } } } @@ -605,19 +657,26 @@ function parseKMAHubWeatherData(data: any, regionCode: { name: string; stnId: st throw new Error('날씨 데이터를 파싱할 수 없습니다.'); } - // 요청한 관측소(stnId)의 데이터 찾기 - const targetLine = lines.find((line: string) => { + // 요청한 관측소(stnId)의 모든 데이터 찾기 (시간 범위 조회 시 여러 줄 반환됨) + const targetLines = lines.filter((line: string) => { const cols = line.trim().split(/\s+/); return cols[1] === regionCode.stnId; // STN 컬럼 (인덱스 1) }); - if (!targetLine) { + if (targetLines.length === 0) { throw new Error(`${regionCode.name} 관측소 데이터를 찾을 수 없습니다.`); } + + // 가장 최근 데이터 선택 (마지막 줄) + const targetLine = targetLines[targetLines.length - 1]; // 데이터 라인 파싱 (공백으로 구분) const values = targetLine.trim().split(/\s+/); + // 관측 시각 로깅 + const obsTime = values[0]; // YYMMDDHHMI + console.log(`🕐 관측 시각: ${obsTime} (${regionCode.name})`); + // 기상청 API Hub 데이터 형식 (실제 응답 기준): // [0]YYMMDDHHMI [1]STN [2]WD [3]WS [4]GST_WD [5]GST_WS [6]GST_TM [7]PA [8]PS [9]PT [10]PR [11]TA [12]TD [13]HM [14]PV [15]RN ... const temperature = parseFloat(values[11]) || 0; // TA: 기온 (인덱스 11) diff --git a/backend-node/src/routes/vehicleRoutes.ts b/backend-node/src/routes/vehicleRoutes.ts new file mode 100644 index 00000000..b8cfa8ac --- /dev/null +++ b/backend-node/src/routes/vehicleRoutes.ts @@ -0,0 +1,52 @@ +import express from "express"; +import { query } from "../database/db"; + +const router = express.Router(); + +/** + * 차량 위치 자동 업데이트 API + * - 모든 active/warning 상태 차량의 위치를 랜덤하게 조금씩 이동 + */ +router.post("/move", async (req, res) => { + try { + // move_vehicles() 함수 실행 + await query("SELECT move_vehicles()"); + + res.json({ + success: true, + message: "차량 위치가 업데이트되었습니다" + }); + } catch (error) { + console.error("차량 위치 업데이트 오류:", error); + res.status(500).json({ + success: false, + error: "차량 위치 업데이트 실패" + }); + } +}); + +/** + * 차량 위치 목록 조회 + */ +router.get("/locations", async (req, res) => { + try { + const result = await query(` + SELECT * FROM vehicle_locations + ORDER BY last_update DESC + `); + + res.json({ + success: true, + data: result.rows + }); + } catch (error) { + console.error("차량 위치 조회 오류:", error); + res.status(500).json({ + success: false, + error: "차량 위치 조회 실패" + }); + } +}); + +export default router; + diff --git a/frontend/app/(main)/dashboard/[dashboardId]/page.tsx b/frontend/app/(main)/dashboard/[dashboardId]/page.tsx index 6f5af7ba..f8af0d0f 100644 --- a/frontend/app/(main)/dashboard/[dashboardId]/page.tsx +++ b/frontend/app/(main)/dashboard/[dashboardId]/page.tsx @@ -105,8 +105,8 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) { return (
- {/* 대시보드 헤더 */} -
+ {/* 대시보드 헤더 - 보기 모드에서는 숨김 */} + {/*

{dashboard.title}

@@ -114,7 +114,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
- {/* 새로고침 버튼 */} + {/* 새로고침 버튼 *\/} - {/* 편집 버튼 */} + {/* 편집 버튼 *\/}
- {/* 메타 정보 */} + {/* 메타 정보 *\/}
생성: {new Date(dashboard.createdAt).toLocaleString()} 수정: {new Date(dashboard.updatedAt).toLocaleString()} 요소: {dashboard.elements.length}개
-
+
*/} {/* 대시보드 뷰어 */} -
- -
+
); } diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index eb1e679d..e294a797 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -432,7 +432,7 @@ export function CanvasElement({ }); } } catch (error) { - console.error("Chart data loading error:", error); + // console.error("Chart data loading error:", error); setChartData(null); } finally { setIsLoadingData(false); @@ -521,12 +521,9 @@ export function CanvasElement({
{element.customTitle || element.title}
- {/* 설정 버튼 (시계, 달력, 기사관리 위젯은 자체 설정 UI 사용) */} + {/* 설정 버튼 (기사관리 위젯만 자체 설정 UI 사용) */} {onConfigure && - !( - element.type === "widget" && - (element.subtype === "clock" || element.subtype === "calendar" || element.subtype === "driver-management") - ) && ( + !(element.type === "widget" && element.subtype === "driver-management") && ( - {currentStep === 1 ? ( - // 1단계: 다음 버튼 (모든 타입 공통) + {isHeaderOnlyWidget ? ( + // 헤더 전용 위젯: 바로 저장 + + ) : currentStep === 1 ? ( + // 1단계: 다음 버튼 ) : ( - // 2단계: 저장 버튼 (모든 타입 공통) + // 2단계: 저장 버튼
{/* 진행 상태 표시 */} diff --git a/frontend/components/admin/dashboard/widgets/TodoWidgetConfigModal.tsx b/frontend/components/admin/dashboard/widgets/TodoWidgetConfigModal.tsx index 3c238873..398c4d17 100644 --- a/frontend/components/admin/dashboard/widgets/TodoWidgetConfigModal.tsx +++ b/frontend/components/admin/dashboard/widgets/TodoWidgetConfigModal.tsx @@ -67,17 +67,17 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo // 쿼리 실행 결과 처리 const handleQueryTest = useCallback( (result: QueryResult) => { - console.log("🎯 TodoWidget - handleQueryTest 호출됨!"); - console.log("📊 쿼리 결과:", result); - console.log("📝 rows 개수:", result.rows?.length); - console.log("❌ error:", result.error); + // console.log("🎯 TodoWidget - handleQueryTest 호출됨!"); + // console.log("📊 쿼리 결과:", result); + // console.log("📝 rows 개수:", result.rows?.length); + // console.log("❌ error:", result.error); setQueryResult(result); - console.log("✅ setQueryResult 호출 완료!"); + // console.log("✅ setQueryResult 호출 완료!"); // 강제 리렌더링 확인 - setTimeout(() => { - console.log("🔄 1초 후 queryResult 상태:", result); - }, 1000); + // setTimeout(() => { + // console.log("🔄 1초 후 queryResult 상태:", result); + // }, 1000); }, [], ); @@ -318,8 +318,8 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo onClick={handleSave} disabled={(() => { const isDisabled = !queryResult || queryResult.error || !queryResult.rows || queryResult.rows.length === 0; - console.log("💾 저장 버튼 disabled:", isDisabled); - console.log("💾 queryResult:", queryResult); + // console.log("💾 저장 버튼 disabled:", isDisabled); + // console.log("💾 queryResult:", queryResult); return isDisabled; })()} > diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx index 6319242e..9b6e83f8 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -256,7 +256,8 @@ export function DashboardViewer({ return ( -
+ {/* overflow-auto 제거 - 외부 페이지 스크롤 사용 */} +
{/* 고정 크기 캔버스 (편집 화면과 동일한 레이아웃) */}
-

🔔 {element?.customTitle || "예약 요청 알림"}

+

{element?.customTitle || "예약 요청 알림"}

{newCount > 0 && ( {newCount} diff --git a/frontend/components/dashboard/widgets/CalculatorWidget.tsx b/frontend/components/dashboard/widgets/CalculatorWidget.tsx index b8816bbc..d86c44e3 100644 --- a/frontend/components/dashboard/widgets/CalculatorWidget.tsx +++ b/frontend/components/dashboard/widgets/CalculatorWidget.tsx @@ -7,7 +7,7 @@ * - 대시보드 위젯으로 사용 가능 */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { DashboardElement } from '@/components/admin/dashboard/types'; @@ -117,11 +117,62 @@ export default function CalculatorWidget({ element, className = '' }: Calculator setDisplay(String(value / 100)); }; + // 키보드 입력 처리 + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + const key = event.key; + + // 숫자 키 (0-9) + if (/^[0-9]$/.test(key)) { + event.preventDefault(); + handleNumber(key); + } + // 연산자 키 + else if (key === '+' || key === '-' || key === '*' || key === '/') { + event.preventDefault(); + handleOperation(key); + } + // 소수점 + else if (key === '.') { + event.preventDefault(); + handleDecimal(); + } + // Enter 또는 = (계산) + else if (key === 'Enter' || key === '=') { + event.preventDefault(); + handleEquals(); + } + // Escape 또는 c (초기화) + else if (key === 'Escape' || key.toLowerCase() === 'c') { + event.preventDefault(); + handleClear(); + } + // Backspace (지우기) + else if (key === 'Backspace') { + event.preventDefault(); + handleBackspace(); + } + // % (퍼센트) + else if (key === '%') { + event.preventDefault(); + handlePercent(); + } + }; + + // 이벤트 리스너 등록 + window.addEventListener('keydown', handleKeyDown); + + // 클린업 + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [display, previousValue, operation, waitingForOperand]); + return (
{/* 제목 */} -

🧮 {element?.customTitle || "계산기"}

+

{element?.customTitle || "계산기"}

{/* 디스플레이 */}
diff --git a/frontend/components/dashboard/widgets/CustomerIssuesWidget.tsx b/frontend/components/dashboard/widgets/CustomerIssuesWidget.tsx index f7f50a43..26d6d27d 100644 --- a/frontend/components/dashboard/widgets/CustomerIssuesWidget.tsx +++ b/frontend/components/dashboard/widgets/CustomerIssuesWidget.tsx @@ -150,7 +150,7 @@ export default function CustomerIssuesWidget({ element }: CustomerIssuesWidgetPr
{/* 헤더 */}
-

⚠️ 고객 클레임/이슈

+

고객 클레임/이슈

diff --git a/frontend/components/dashboard/widgets/ExchangeWidget.tsx b/frontend/components/dashboard/widgets/ExchangeWidget.tsx index d7fb2128..20e0cea7 100644 --- a/frontend/components/dashboard/widgets/ExchangeWidget.tsx +++ b/frontend/components/dashboard/widgets/ExchangeWidget.tsx @@ -139,7 +139,7 @@ export default function ExchangeWidget({ {/* 헤더 */}
-

💱 {element?.customTitle || "환율"}

+

{element?.customTitle || "환율"}

{lastUpdated ? `업데이트: ${lastUpdated.toLocaleTimeString('ko-KR', { diff --git a/frontend/components/dashboard/widgets/MapSummaryWidget.tsx b/frontend/components/dashboard/widgets/MapSummaryWidget.tsx index e91746a9..3ad09305 100644 --- a/frontend/components/dashboard/widgets/MapSummaryWidget.tsx +++ b/frontend/components/dashboard/widgets/MapSummaryWidget.tsx @@ -158,7 +158,7 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) { {/* 헤더 */}

-

📍 {displayTitle}

+

{displayTitle}

{element?.dataSource?.query ? (

총 {markers.length.toLocaleString()}개 마커

) : ( diff --git a/frontend/components/dashboard/widgets/TodoWidget.tsx b/frontend/components/dashboard/widgets/TodoWidget.tsx index 977a9b6c..dd4652d5 100644 --- a/frontend/components/dashboard/widgets/TodoWidget.tsx +++ b/frontend/components/dashboard/widgets/TodoWidget.tsx @@ -64,10 +64,10 @@ export default function TodoWidget({ element }: TodoWidgetProps) { // 외부 DB 조회 (dataSource가 설정된 경우) if (element?.dataSource?.query) { - console.log("🔍 TodoWidget - 외부 DB 조회 시작"); - console.log("📝 Query:", element.dataSource.query); - console.log("🔗 ConnectionId:", element.dataSource.externalConnectionId); - console.log("🔗 ConnectionType:", element.dataSource.connectionType); + // console.log("🔍 TodoWidget - 외부 DB 조회 시작"); + // console.log("📝 Query:", element.dataSource.query); + // console.log("🔗 ConnectionId:", element.dataSource.externalConnectionId); + // console.log("🔗 ConnectionType:", element.dataSource.connectionType); // 현재 DB vs 외부 DB 분기 const apiUrl = element.dataSource.connectionType === "external" && element.dataSource.externalConnectionId @@ -83,8 +83,8 @@ export default function TodoWidget({ element }: TodoWidgetProps) { query: element.dataSource.query, }; - console.log("🌐 API URL:", apiUrl); - console.log("📦 Request Body:", requestBody); + // console.log("🌐 API URL:", apiUrl); + // console.log("📦 Request Body:", requestBody); const response = await fetch(apiUrl, { method: "POST", @@ -95,29 +95,29 @@ export default function TodoWidget({ element }: TodoWidgetProps) { body: JSON.stringify(requestBody), }); - console.log("📡 Response status:", response.status); + // console.log("📡 Response status:", response.status); if (response.ok) { const result = await response.json(); - console.log("✅ API 응답:", result); - console.log("📦 result.data:", result.data); - console.log("📦 result.data.rows:", result.data?.rows); + // console.log("✅ API 응답:", result); + // console.log("📦 result.data:", result.data); + // console.log("📦 result.data.rows:", result.data?.rows); // API 응답 형식에 따라 데이터 추출 const rows = result.data?.rows || result.data || []; - console.log("📊 추출된 rows:", rows); + // console.log("📊 추출된 rows:", rows); const externalTodos = mapExternalDataToTodos(rows); - console.log("📋 변환된 Todos:", externalTodos); - console.log("📋 변환된 Todos 개수:", externalTodos.length); + // console.log("📋 변환된 Todos:", externalTodos); + // console.log("📋 변환된 Todos 개수:", externalTodos.length); setTodos(externalTodos); setStats(calculateStatsFromTodos(externalTodos)); - console.log("✅ setTodos, setStats 호출 완료!"); + // console.log("✅ setTodos, setStats 호출 완료!"); } else { const errorText = await response.text(); - console.error("❌ API 오류:", errorText); + // console.error("❌ API 오류:", errorText); } } // 내장 API 조회 (기본) @@ -323,67 +323,71 @@ export default function TodoWidget({ element }: TodoWidgetProps) { return (
- {/* 헤더 */} -
-
-
-

✅ {element?.customTitle || "To-Do / 긴급 지시"}

- {selectedDate && ( -
- - {formatSelectedDate()} 할일 -
- )} -
- -
- - {/* 통계 */} - {stats && ( -
-
-
{stats.pending}
-
대기
-
-
-
{stats.inProgress}
-
진행중
-
-
-
{stats.urgent}
-
긴급
-
-
-
{stats.overdue}
-
지연
-
+ {/* 제목 - 항상 표시 */} +
+

{element?.customTitle || "To-Do / 긴급 지시"}

+ {selectedDate && ( +
+ + {formatSelectedDate()} 할일
)} - - {/* 필터 */} -
- {(["all", "pending", "in_progress", "completed"] as const).map((f) => ( - - ))} -
+ {/* 헤더 (추가 버튼, 통계, 필터) - showHeader가 false일 때만 숨김 */} + {element?.showHeader !== false && ( +
+
+ +
+ + {/* 통계 */} + {stats && ( +
+
+
{stats.pending}
+
대기
+
+
+
{stats.inProgress}
+
진행중
+
+
+
{stats.urgent}
+
긴급
+
+
+
{stats.overdue}
+
지연
+
+
+ )} + + {/* 필터 */} +
+ {(["all", "pending", "in_progress", "completed"] as const).map((f) => ( + + ))} +
+
+ )} + {/* 추가 폼 */} {showAddForm && (
diff --git a/frontend/components/dashboard/widgets/VehicleListWidget.tsx b/frontend/components/dashboard/widgets/VehicleListWidget.tsx index 1ea927a8..d15d8ffa 100644 --- a/frontend/components/dashboard/widgets/VehicleListWidget.tsx +++ b/frontend/components/dashboard/widgets/VehicleListWidget.tsx @@ -97,7 +97,7 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }: {/* 헤더 */}
-

📋 차량 목록

+

차량 목록

마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}

{/* 프로필 수정 모달 */}