From 46ef858c1d7386797c541acda9fa3bc90c45cad3 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Fri, 5 Dec 2025 18:29:32 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=9C=84=EC=A0=AF=20REST?= =?UTF-8?q?=20API=20Request=20Body=20=EC=A0=84=EB=8B=AC=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/DashboardController.ts | 24 ++-- .../dashboard/widgets/MapTestWidgetV2.tsx | 130 ++++++++++-------- 2 files changed, 85 insertions(+), 69 deletions(-) diff --git a/backend-node/src/controllers/DashboardController.ts b/backend-node/src/controllers/DashboardController.ts index e324c332..d0b22db4 100644 --- a/backend-node/src/controllers/DashboardController.ts +++ b/backend-node/src/controllers/DashboardController.ts @@ -709,9 +709,9 @@ export class DashboardController { } // 기상청 API 등 EUC-KR 인코딩을 사용하는 경우 arraybuffer로 받아서 디코딩 - const isKmaApi = urlObj.hostname.includes('kma.go.kr'); + const isKmaApi = urlObj.hostname.includes("kma.go.kr"); if (isKmaApi) { - requestConfig.responseType = 'arraybuffer'; + requestConfig.responseType = "arraybuffer"; } const response = await axios(requestConfig); @@ -727,18 +727,22 @@ export class DashboardController { // 기상청 API 인코딩 처리 (UTF-8 우선, 실패 시 EUC-KR) if (isKmaApi && Buffer.isBuffer(data)) { - const iconv = require('iconv-lite'); + const iconv = require("iconv-lite"); const buffer = Buffer.from(data); - const utf8Text = buffer.toString('utf-8'); - + const utf8Text = buffer.toString("utf-8"); + // UTF-8로 정상 디코딩되었는지 확인 - if (utf8Text.includes('특보') || utf8Text.includes('경보') || utf8Text.includes('주의보') || - (utf8Text.includes('#START7777') && !utf8Text.includes('�'))) { - data = { text: utf8Text, contentType, encoding: 'utf-8' }; + if ( + utf8Text.includes("특보") || + utf8Text.includes("경보") || + utf8Text.includes("주의보") || + (utf8Text.includes("#START7777") && !utf8Text.includes("�")) + ) { + data = { text: utf8Text, contentType, encoding: "utf-8" }; } else { // EUC-KR로 디코딩 - const eucKrText = iconv.decode(buffer, 'EUC-KR'); - data = { text: eucKrText, contentType, encoding: 'euc-kr' }; + const eucKrText = iconv.decode(buffer, "EUC-KR"); + data = { text: eucKrText, contentType, encoding: "euc-kr" }; } } // 텍스트 응답인 경우 포맷팅 diff --git a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx index 02cafe2b..94c3a217 100644 --- a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx +++ b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx @@ -94,12 +94,12 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const [error, setError] = useState(null); const [geoJsonData, setGeoJsonData] = useState(null); const [lastRefreshTime, setLastRefreshTime] = useState(null); - + // 이동경로 상태 const [routePoints, setRoutePoints] = useState([]); const [selectedUserId, setSelectedUserId] = useState(null); const [routeLoading, setRouteLoading] = useState(false); - const [routeDate, setRouteDate] = useState(new Date().toISOString().split('T')[0]); // YYYY-MM-DD 형식 + const [routeDate, setRouteDate] = useState(new Date().toISOString().split("T")[0]); // YYYY-MM-DD 형식 // dataSources를 useMemo로 추출 (circular reference 방지) const dataSources = useMemo(() => { @@ -122,62 +122,59 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { }, []); // 이동경로 로드 함수 - const loadRoute = useCallback(async (userId: string, date?: string) => { - if (!userId) { - console.log("🛣️ 이동경로 조회 불가: userId 없음"); - return; - } + const loadRoute = useCallback( + async (userId: string, date?: string) => { + if (!userId) { + return; + } - setRouteLoading(true); - setSelectedUserId(userId); + setRouteLoading(true); + setSelectedUserId(userId); - try { - // 선택한 날짜 기준으로 이동경로 조회 - const targetDate = date || routeDate; - const startOfDay = `${targetDate}T00:00:00.000Z`; - const endOfDay = `${targetDate}T23:59:59.999Z`; - - const query = `SELECT latitude, longitude, recorded_at + try { + // 선택한 날짜 기준으로 이동경로 조회 + const targetDate = date || routeDate; + const startOfDay = `${targetDate}T00:00:00.000Z`; + const endOfDay = `${targetDate}T23:59:59.999Z`; + + const query = `SELECT latitude, longitude, recorded_at FROM vehicle_location_history WHERE user_id = '${userId}' AND recorded_at >= '${startOfDay}' AND recorded_at <= '${endOfDay}' ORDER BY recorded_at ASC`; - console.log("🛣️ 이동경로 쿼리:", query); + const response = await fetch(getApiUrl("/api/dashboards/execute-query"), { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${typeof window !== "undefined" ? localStorage.getItem("authToken") || "" : ""}`, + }, + body: JSON.stringify({ query }), + }); - const response = await fetch(getApiUrl("/api/dashboards/execute-query"), { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${typeof window !== "undefined" ? localStorage.getItem("authToken") || "" : ""}`, - }, - body: JSON.stringify({ query }), - }); + if (response.ok) { + const result = await response.json(); + if (result.success && result.data.rows.length > 0) { + const points: RoutePoint[] = result.data.rows.map((row: any) => ({ + lat: parseFloat(row.latitude), + lng: parseFloat(row.longitude), + recordedAt: row.recorded_at, + })); - if (response.ok) { - const result = await response.json(); - if (result.success && result.data.rows.length > 0) { - const points: RoutePoint[] = result.data.rows.map((row: any) => ({ - lat: parseFloat(row.latitude), - lng: parseFloat(row.longitude), - recordedAt: row.recorded_at, - })); - - console.log(`🛣️ 이동경로 ${points.length}개 포인트 로드 완료`); - setRoutePoints(points); - } else { - console.log("🛣️ 이동경로 데이터 없음"); - setRoutePoints([]); + setRoutePoints(points); + } else { + setRoutePoints([]); + } } + } catch { + setRoutePoints([]); } - } catch (error) { - console.error("이동경로 로드 실패:", error); - setRoutePoints([]); - } - setRouteLoading(false); - }, [routeDate]); + setRouteLoading(false); + }, + [routeDate], + ); // 이동경로 숨기기 const clearRoute = useCallback(() => { @@ -297,6 +294,17 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { }); } + // Request Body 파싱 + let requestBody: any = undefined; + if (source.body) { + try { + requestBody = JSON.parse(source.body); + } catch { + // JSON 파싱 실패시 문자열 그대로 사용 + requestBody = source.body; + } + } + // 백엔드 프록시를 통해 API 호출 const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", @@ -309,6 +317,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { method: source.method || "GET", headers, queryParams, + body: requestBody, + externalConnectionId: source.externalConnectionId, }), }); @@ -344,14 +354,18 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { } } + // 데이터가 null/undefined면 빈 결과 반환 + if (data === null || data === undefined) { + return { markers: [], polygons: [] }; + } + const rows = Array.isArray(data) ? data : [data]; // 컬럼 매핑 적용 const mappedRows = applyColumnMapping(rows, source.columnMapping); // 마커와 폴리곤으로 변환 (mapDisplayType + dataSource 전달) - const finalResult = convertToMapData(mappedRows, source.name || source.id || "API", source.mapDisplayType, source); - return finalResult; + return convertToMapData(mappedRows, source.name || source.id || "API", source.mapDisplayType, source); }; // Database 데이터 로딩 @@ -485,6 +499,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const polygons: PolygonData[] = []; rows.forEach((row, index) => { + // null/undefined 체크 + if (!row) { + return; + } + // 텍스트 데이터 체크 (기상청 API 등) if (row && typeof row === "object" && row.text && typeof row.text === "string") { const parsedData = parseTextData(row.text); @@ -1098,13 +1117,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { }} className="h-6 rounded border-none bg-transparent px-1 text-xs text-blue-600 focus:outline-none" /> - - ({routePoints.length}개) - - @@ -1409,12 +1423,12 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 트럭 마커 // 트럭 아이콘이 오른쪽(90도)을 보고 있으므로, 북쪽(0도)으로 가려면 -90도 회전 필요 const rotation = heading - 90; - + // 회전 각도가 90~270도 범위면 차량이 뒤집어짐 (바퀴가 위로) // 이 경우 scaleY(-1)로 상하 반전하여 바퀴가 아래로 오도록 함 const normalizedRotation = ((rotation % 360) + 360) % 360; const isFlipped = normalizedRotation > 90 && normalizedRotation < 270; - const transformStyle = isFlipped + const transformStyle = isFlipped ? `translate(-50%, -50%) rotate(${rotation}deg) scaleY(-1)` : `translate(-50%, -50%) rotate(${rotation}deg)`; @@ -1654,9 +1668,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { disabled={routeLoading} className="w-full rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600 disabled:opacity-50" > - {routeLoading && selectedUserId === userId - ? "로딩 중..." - : "🛣️ 이동경로 보기"} + {routeLoading && selectedUserId === userId ? "로딩 중..." : "🛣️ 이동경로 보기"} );