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) {
- {/* 새로고침 버튼 */}
+ {/* 새로고침 버튼 *\/}
- {/* 전체화면 버튼 */}
+ {/* 전체화면 버튼 *\/}
{
if (document.fullscreenElement) {
@@ -138,7 +138,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
⛶
- {/* 편집 버튼 */}
+ {/* 편집 버튼 *\/}
{
router.push(`/admin/dashboard?load=${resolvedParams.dashboardId}`);
@@ -150,22 +150,20 @@ 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") && (
onConfigure(element)}
diff --git a/frontend/components/admin/dashboard/ChartConfigPanel.tsx b/frontend/components/admin/dashboard/ChartConfigPanel.tsx
index 2257848d..59f0822a 100644
--- a/frontend/components/admin/dashboard/ChartConfigPanel.tsx
+++ b/frontend/components/admin/dashboard/ChartConfigPanel.tsx
@@ -98,7 +98,7 @@ export function ChartConfigPanel({
setDateColumns(schema.dateColumns);
})
.catch((error) => {
- console.error("❌ 테이블 스키마 조회 실패:", error);
+ // console.error("❌ 테이블 스키마 조회 실패:", error);
// 실패 시 빈 배열 (날짜 필터 비활성화)
setDateColumns([]);
});
diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx
index 7f4dc32f..27115ee1 100644
--- a/frontend/components/admin/dashboard/DashboardDesigner.tsx
+++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx
@@ -6,7 +6,6 @@ import { DashboardCanvas } from "./DashboardCanvas";
import { DashboardTopMenu } from "./DashboardTopMenu";
import { ElementConfigModal } from "./ElementConfigModal";
import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal";
-import { TodoWidgetConfigModal } from "./widgets/TodoWidgetConfigModal";
import { DashboardSaveModal } from "./DashboardSaveModal";
import { DashboardElement, ElementType, ElementSubtype } from "./types";
import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize } from "./gridUtils";
@@ -185,7 +184,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
(type: ElementType, subtype: ElementSubtype, x: number, y: number) => {
// 좌표 유효성 검사
if (isNaN(x) || isNaN(y)) {
- console.error("Invalid coordinates:", { x, y });
+ // console.error("Invalid coordinates:", { x, y });
return;
}
@@ -207,14 +206,14 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
// 크기 유효성 검사
if (isNaN(defaultWidth) || isNaN(defaultHeight) || defaultWidth <= 0 || defaultHeight <= 0) {
- console.error("Invalid size calculated:", {
- canvasConfig,
- cellSize,
- cellWithGap,
- defaultCells,
- defaultWidth,
- defaultHeight,
- });
+ // console.error("Invalid size calculated:", {
+ // canvasConfig,
+ // cellSize,
+ // cellWithGap,
+ // defaultCells,
+ // defaultWidth,
+ // defaultHeight,
+ // });
return;
}
@@ -244,7 +243,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
// 좌표 유효성 확인
if (isNaN(centerX) || isNaN(centerY)) {
- console.error("Invalid canvas config:", canvasConfig);
+ // console.error("Invalid canvas config:", canvasConfig);
return;
}
@@ -310,15 +309,6 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
[configModalElement, updateElement],
);
- const saveTodoWidgetConfig = useCallback(
- (updates: Partial) => {
- if (configModalElement) {
- updateElement(configModalElement.id, updates);
- }
- },
- [configModalElement, updateElement],
- );
-
// 레이아웃 저장
const saveLayout = useCallback(() => {
if (elements.length === 0) {
@@ -470,7 +460,8 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
/>
{/* 캔버스 영역 - 해상도에 따른 크기, 중앙 정렬 */}
-
+ {/* overflow-auto 제거 - 외부 페이지 스크롤 사용 */}
+
- ) : configModalElement.type === "widget" && configModalElement.subtype === "todo" ? (
-
) : (
-
+ /> */}
{/* 정비 일정 관리 위젯 제거 - 커스텀 목록 카드로 대체 가능 */}
setCustomTitle(e.target.value)}
- placeholder={"예: 정비 일정 목록, 창고 위치 현황 등 (비워두면 자동 생성)"}
+ onKeyDown={(e) => {
+ // 모든 키보드 이벤트를 input 필드 내부에서만 처리
+ e.stopPropagation();
+ }}
+ placeholder="예: 정비 일정 목록, 창고 위치 현황 등 (비워두면 자동 생성)"
className="focus:border-primary focus:ring-primary w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-1 focus:outline-none"
/>
-
- 💡 비워두면 테이블명으로 자동 생성됩니다 (예: "maintenance_schedules 목록")
-
+ 비워두면 테이블명으로 자동 생성됩니다 (예: "maintenance_schedules 목록")
- {/* 헤더 표시 여부 */}
-
+ {/* 헤더 표시 옵션 */}
+
setShowHeader(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
/>
-
- 위젯 헤더 표시
+
+ 위젯 헤더 표시 (제목 + 새로고침 버튼)
- {/* 진행 상황 표시 - 간단한 위젯은 표시 안 함 */}
- {!isSimpleWidget && (
+ {/* 진행 상황 표시 - 간단한 위젯과 헤더 전용 위젯은 표시 안 함 */}
+ {!isSimpleWidget && !isHeaderOnlyWidget && (
@@ -247,12 +249,13 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
)}
{/* 단계별 내용 */}
-
- {currentStep === 1 && (
-
- )}
+ {!isHeaderOnlyWidget && (
+
+ {currentStep === 1 && (
+
+ )}
- {currentStep === 2 && (
+ {currentStep === 2 && (
{/* 왼쪽: 데이터 설정 */}
@@ -308,15 +311,16 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
)}
- )}
-
+ )}
+
+ )}
{/* 모달 푸터 */}
{queryResult && {queryResult.rows.length}개 데이터 로드됨 }
- {!isSimpleWidget && currentStep > 1 && (
+ {!isSimpleWidget && !isHeaderOnlyWidget && currentStep > 1 && (
이전
@@ -325,14 +329,20 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
취소
- {currentStep === 1 ? (
- // 1단계: 다음 버튼 (모든 타입 공통)
+ {isHeaderOnlyWidget ? (
+ // 헤더 전용 위젯: 바로 저장
+
+
+ 저장
+
+ ) : currentStep === 1 ? (
+ // 1단계: 다음 버튼
다음
) : (
- // 2단계: 저장 버튼 (모든 타입 공통)
+ // 2단계: 저장 버튼
저장
diff --git a/frontend/components/admin/dashboard/MenuAssignmentModal.tsx b/frontend/components/admin/dashboard/MenuAssignmentModal.tsx
index 9220a0c8..cd4ac614 100644
--- a/frontend/components/admin/dashboard/MenuAssignmentModal.tsx
+++ b/frontend/components/admin/dashboard/MenuAssignmentModal.tsx
@@ -61,7 +61,7 @@ export const MenuAssignmentModal: React.FC = ({
setUserMenus(userResponse.data || []);
}
} catch (error) {
- console.error("메뉴 목록 로드 실패:", error);
+ // console.error("메뉴 목록 로드 실패:", error);
toast.error("메뉴 목록을 불러오는데 실패했습니다.");
} finally {
setLoading(false);
diff --git a/frontend/components/admin/dashboard/QueryEditor.tsx b/frontend/components/admin/dashboard/QueryEditor.tsx
index 181d80fa..81bee6ea 100644
--- a/frontend/components/admin/dashboard/QueryEditor.tsx
+++ b/frontend/components/admin/dashboard/QueryEditor.tsx
@@ -35,9 +35,9 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que
// 쿼리 실행
const executeQuery = useCallback(async () => {
- console.log("🚀 executeQuery 호출됨!");
- console.log("📝 현재 쿼리:", query);
- console.log("✅ query.trim():", query.trim());
+ // console.log("🚀 executeQuery 호출됨!");
+ // console.log("📝 현재 쿼리:", query);
+ // console.log("✅ query.trim():", query.trim());
if (!query.trim()) {
setError("쿼리를 입력해주세요.");
@@ -47,13 +47,13 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que
// 외부 DB인 경우 커넥션 ID 확인
if (dataSource?.connectionType === "external" && !dataSource?.externalConnectionId) {
setError("외부 DB 커넥션을 선택해주세요.");
- console.log("❌ 쿼리가 비어있음!");
+ // console.log("❌ 쿼리가 비어있음!");
return;
}
setIsExecuting(true);
setError(null);
- console.log("🔄 쿼리 실행 시작...");
+ // console.log("🔄 쿼리 실행 시작...");
try {
let apiResult: { columns: string[]; rows: any[]; rowCount: number };
@@ -247,7 +247,7 @@ ORDER BY Q4 DESC;`,
-
+
수동
10초
30초
diff --git a/frontend/components/admin/dashboard/widgets/ListWidget.tsx b/frontend/components/admin/dashboard/widgets/ListWidget.tsx
index ec432299..ab721c4b 100644
--- a/frontend/components/admin/dashboard/widgets/ListWidget.tsx
+++ b/frontend/components/admin/dashboard/widgets/ListWidget.tsx
@@ -219,8 +219,9 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
return (
+ {/* 제목 - 항상 표시 */}
-
{element.title}
+ {element.customTitle || element.title}
{/* 테이블 뷰 */}
diff --git a/frontend/components/admin/dashboard/widgets/ListWidgetConfigModal.tsx b/frontend/components/admin/dashboard/widgets/ListWidgetConfigModal.tsx
index eb2a0e8a..e182f433 100644
--- a/frontend/components/admin/dashboard/widgets/ListWidgetConfigModal.tsx
+++ b/frontend/components/admin/dashboard/widgets/ListWidgetConfigModal.tsx
@@ -131,7 +131,7 @@ export function ListWidgetConfigModal({ isOpen, element, onClose, onSave }: List
// 저장
const handleSave = () => {
onSave({
- title,
+ customTitle: title,
dataSource,
listConfig,
});
@@ -166,10 +166,19 @@ export function ListWidgetConfigModal({ isOpen, element, onClose, onSave }: List
id="list-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
+ onKeyDown={(e) => {
+ // 모든 키보드 이벤트를 input 필드 내부에서만 처리
+ e.stopPropagation();
+ }}
placeholder="예: 사용자 목록"
className="mt-1"
/>
+
+ {/* 참고: 리스트 위젯은 제목이 항상 표시됩니다 */}
+
+ 💡 리스트 위젯은 제목이 항상 표시됩니다
+
{/* 진행 상태 표시 */}
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
{/* 헤더 */}
-
⚠️ 고객 클레임/이슈
+
고객 클레임/이슈
{/* 헤더 */}
-
📅 오늘 처리 현황
+
오늘 처리 현황
-
📂 {element?.customTitle || "문서 관리"}
+
{element?.customTitle || "문서 관리"}
+ 업로드
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()} 할일
-
- )}
-
-
setShowAddForm(!showAddForm)}
- className="flex items-center gap-1 rounded-lg bg-primary px-3 py-1.5 text-sm text-white transition-colors hover:bg-primary/90"
- >
-
- 추가
-
-
-
- {/* 통계 */}
- {stats && (
-
-
-
-
{stats.inProgress}
-
진행중
-
-
-
+ {/* 제목 - 항상 표시 */}
+
+
{element?.customTitle || "To-Do / 긴급 지시"}
+ {selectedDate && (
+
+
+ {formatSelectedDate()} 할일
)}
-
- {/* 필터 */}
-
- {(["all", "pending", "in_progress", "completed"] as const).map((f) => (
- setFilter(f)}
- className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
- filter === f
- ? "bg-primary text-white"
- : "bg-gray-100 text-gray-600 hover:bg-gray-200"
- }`}
- >
- {f === "all" ? "전체" : f === "pending" ? "대기" : f === "in_progress" ? "진행중" : "완료"}
-
- ))}
-
+ {/* 헤더 (추가 버튼, 통계, 필터) - showHeader가 false일 때만 숨김 */}
+ {element?.showHeader !== false && (
+
+
+
setShowAddForm(!showAddForm)}
+ className="flex items-center gap-1 rounded-lg bg-primary px-3 py-1.5 text-sm text-white transition-colors hover:bg-primary/90"
+ >
+
+ 추가
+
+
+
+ {/* 통계 */}
+ {stats && (
+
+
+
+
{stats.inProgress}
+
진행중
+
+
+
+
+ )}
+
+ {/* 필터 */}
+
+ {(["all", "pending", "in_progress", "completed"] as const).map((f) => (
+ setFilter(f)}
+ className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
+ filter === f
+ ? "bg-primary text-white"
+ : "bg-gray-100 text-gray-600 hover:bg-gray-200"
+ }`}
+ >
+ {f === "all" ? "전체" : f === "pending" ? "대기" : f === "in_progress" ? "진행중" : "완료"}
+
+ ))}
+
+
+ )}
+
{/* 추가 폼 */}
{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")}
diff --git a/frontend/components/dashboard/widgets/WeatherWidget.tsx b/frontend/components/dashboard/widgets/WeatherWidget.tsx
index 833f0f2a..57683a57 100644
--- a/frontend/components/dashboard/widgets/WeatherWidget.tsx
+++ b/frontend/components/dashboard/widgets/WeatherWidget.tsx
@@ -280,9 +280,12 @@ export default function WeatherWidget({
if (loading && !weather) {
return (
-
+
-
날씨 정보 불러오는 중...
+
+
실제 기상청 API 연결 중...
+
실시간 관측 데이터를 가져오고 있습니다
+
);
@@ -290,10 +293,27 @@ export default function WeatherWidget({
// 에러 상태
if (error || !weather) {
+ const isTestMode = error?.includes('API 키가 설정되지 않았습니다');
return (
-
+
-
{error || '날씨 정보를 불러올 수 없습니다.'}
+
+
+ {isTestMode ? '⚠️ 테스트 모드' : '❌ 연결 실패'}
+
+
+ {error || '날씨 정보를 불러올 수 없습니다.'}
+
+ {isTestMode && (
+
+ 임시 데이터가 표시됩니다
+
+ )}
+
{/* 가운데 컨텐츠 영역 - overflow 문제 해결 */}
- {children}
+ {children}
{/* 프로필 수정 모달 */}