지도 위젯 REST API Request Body 전달 오류 수정

This commit is contained in:
dohyeons 2025-12-05 18:29:32 +09:00 committed by kjs
parent fab292f465
commit 133b50dcaa
2 changed files with 85 additions and 69 deletions

View File

@ -709,9 +709,9 @@ export class DashboardController {
} }
// 기상청 API 등 EUC-KR 인코딩을 사용하는 경우 arraybuffer로 받아서 디코딩 // 기상청 API 등 EUC-KR 인코딩을 사용하는 경우 arraybuffer로 받아서 디코딩
const isKmaApi = urlObj.hostname.includes('kma.go.kr'); const isKmaApi = urlObj.hostname.includes("kma.go.kr");
if (isKmaApi) { if (isKmaApi) {
requestConfig.responseType = 'arraybuffer'; requestConfig.responseType = "arraybuffer";
} }
const response = await axios(requestConfig); const response = await axios(requestConfig);
@ -727,18 +727,22 @@ export class DashboardController {
// 기상청 API 인코딩 처리 (UTF-8 우선, 실패 시 EUC-KR) // 기상청 API 인코딩 처리 (UTF-8 우선, 실패 시 EUC-KR)
if (isKmaApi && Buffer.isBuffer(data)) { if (isKmaApi && Buffer.isBuffer(data)) {
const iconv = require('iconv-lite'); const iconv = require("iconv-lite");
const buffer = Buffer.from(data); const buffer = Buffer.from(data);
const utf8Text = buffer.toString('utf-8'); const utf8Text = buffer.toString("utf-8");
// UTF-8로 정상 디코딩되었는지 확인 // UTF-8로 정상 디코딩되었는지 확인
if (utf8Text.includes('특보') || utf8Text.includes('경보') || utf8Text.includes('주의보') || if (
(utf8Text.includes('#START7777') && !utf8Text.includes('<27>'))) { utf8Text.includes("특보") ||
data = { text: utf8Text, contentType, encoding: 'utf-8' }; utf8Text.includes("경보") ||
utf8Text.includes("주의보") ||
(utf8Text.includes("#START7777") && !utf8Text.includes("<22>"))
) {
data = { text: utf8Text, contentType, encoding: "utf-8" };
} else { } else {
// EUC-KR로 디코딩 // EUC-KR로 디코딩
const eucKrText = iconv.decode(buffer, 'EUC-KR'); const eucKrText = iconv.decode(buffer, "EUC-KR");
data = { text: eucKrText, contentType, encoding: 'euc-kr' }; data = { text: eucKrText, contentType, encoding: "euc-kr" };
} }
} }
// 텍스트 응답인 경우 포맷팅 // 텍스트 응답인 경우 포맷팅

View File

@ -94,12 +94,12 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [geoJsonData, setGeoJsonData] = useState<any>(null); const [geoJsonData, setGeoJsonData] = useState<any>(null);
const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null); const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null);
// 이동경로 상태 // 이동경로 상태
const [routePoints, setRoutePoints] = useState<RoutePoint[]>([]); const [routePoints, setRoutePoints] = useState<RoutePoint[]>([]);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null); const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [routeLoading, setRouteLoading] = useState(false); const [routeLoading, setRouteLoading] = useState(false);
const [routeDate, setRouteDate] = useState<string>(new Date().toISOString().split('T')[0]); // YYYY-MM-DD 형식 const [routeDate, setRouteDate] = useState<string>(new Date().toISOString().split("T")[0]); // YYYY-MM-DD 형식
// dataSources를 useMemo로 추출 (circular reference 방지) // dataSources를 useMemo로 추출 (circular reference 방지)
const dataSources = useMemo(() => { const dataSources = useMemo(() => {
@ -122,62 +122,59 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
}, []); }, []);
// 이동경로 로드 함수 // 이동경로 로드 함수
const loadRoute = useCallback(async (userId: string, date?: string) => { const loadRoute = useCallback(
if (!userId) { async (userId: string, date?: string) => {
console.log("🛣️ 이동경로 조회 불가: userId 없음"); if (!userId) {
return; return;
} }
setRouteLoading(true); setRouteLoading(true);
setSelectedUserId(userId); setSelectedUserId(userId);
try { try {
// 선택한 날짜 기준으로 이동경로 조회 // 선택한 날짜 기준으로 이동경로 조회
const targetDate = date || routeDate; const targetDate = date || routeDate;
const startOfDay = `${targetDate}T00:00:00.000Z`; const startOfDay = `${targetDate}T00:00:00.000Z`;
const endOfDay = `${targetDate}T23:59:59.999Z`; const endOfDay = `${targetDate}T23:59:59.999Z`;
const query = `SELECT latitude, longitude, recorded_at const query = `SELECT latitude, longitude, recorded_at
FROM vehicle_location_history FROM vehicle_location_history
WHERE user_id = '${userId}' WHERE user_id = '${userId}'
AND recorded_at >= '${startOfDay}' AND recorded_at >= '${startOfDay}'
AND recorded_at <= '${endOfDay}' AND recorded_at <= '${endOfDay}'
ORDER BY recorded_at ASC`; 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"), { if (response.ok) {
method: "POST", const result = await response.json();
headers: { if (result.success && result.data.rows.length > 0) {
"Content-Type": "application/json", const points: RoutePoint[] = result.data.rows.map((row: any) => ({
Authorization: `Bearer ${typeof window !== "undefined" ? localStorage.getItem("authToken") || "" : ""}`, lat: parseFloat(row.latitude),
}, lng: parseFloat(row.longitude),
body: JSON.stringify({ query }), recordedAt: row.recorded_at,
}); }));
if (response.ok) { setRoutePoints(points);
const result = await response.json(); } else {
if (result.success && result.data.rows.length > 0) { setRoutePoints([]);
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([]);
} }
} catch {
setRoutePoints([]);
} }
} catch (error) {
console.error("이동경로 로드 실패:", error);
setRoutePoints([]);
}
setRouteLoading(false); setRouteLoading(false);
}, [routeDate]); },
[routeDate],
);
// 이동경로 숨기기 // 이동경로 숨기기
const clearRoute = useCallback(() => { 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 호출 // 백엔드 프록시를 통해 API 호출
const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), {
method: "POST", method: "POST",
@ -309,6 +317,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
method: source.method || "GET", method: source.method || "GET",
headers, headers,
queryParams, 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 rows = Array.isArray(data) ? data : [data];
// 컬럼 매핑 적용 // 컬럼 매핑 적용
const mappedRows = applyColumnMapping(rows, source.columnMapping); const mappedRows = applyColumnMapping(rows, source.columnMapping);
// 마커와 폴리곤으로 변환 (mapDisplayType + dataSource 전달) // 마커와 폴리곤으로 변환 (mapDisplayType + dataSource 전달)
const finalResult = convertToMapData(mappedRows, source.name || source.id || "API", source.mapDisplayType, source); return convertToMapData(mappedRows, source.name || source.id || "API", source.mapDisplayType, source);
return finalResult;
}; };
// Database 데이터 로딩 // Database 데이터 로딩
@ -485,6 +499,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
const polygons: PolygonData[] = []; const polygons: PolygonData[] = [];
rows.forEach((row, index) => { rows.forEach((row, index) => {
// null/undefined 체크
if (!row) {
return;
}
// 텍스트 데이터 체크 (기상청 API 등) // 텍스트 데이터 체크 (기상청 API 등)
if (row && typeof row === "object" && row.text && typeof row.text === "string") { if (row && typeof row === "object" && row.text && typeof row.text === "string") {
const parsedData = parseTextData(row.text); 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" className="h-6 rounded border-none bg-transparent px-1 text-xs text-blue-600 focus:outline-none"
/> />
<span className="text-xs text-blue-600"> <span className="text-xs text-blue-600">({routePoints.length})</span>
({routePoints.length}) <button onClick={clearRoute} className="ml-1 text-xs text-blue-400 hover:text-blue-600">
</span>
<button
onClick={clearRoute}
className="ml-1 text-xs text-blue-400 hover:text-blue-600"
>
</button> </button>
</div> </div>
@ -1409,12 +1423,12 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 트럭 마커 // 트럭 마커
// 트럭 아이콘이 오른쪽(90도)을 보고 있으므로, 북쪽(0도)으로 가려면 -90도 회전 필요 // 트럭 아이콘이 오른쪽(90도)을 보고 있으므로, 북쪽(0도)으로 가려면 -90도 회전 필요
const rotation = heading - 90; const rotation = heading - 90;
// 회전 각도가 90~270도 범위면 차량이 뒤집어짐 (바퀴가 위로) // 회전 각도가 90~270도 범위면 차량이 뒤집어짐 (바퀴가 위로)
// 이 경우 scaleY(-1)로 상하 반전하여 바퀴가 아래로 오도록 함 // 이 경우 scaleY(-1)로 상하 반전하여 바퀴가 아래로 오도록 함
const normalizedRotation = ((rotation % 360) + 360) % 360; const normalizedRotation = ((rotation % 360) + 360) % 360;
const isFlipped = normalizedRotation > 90 && normalizedRotation < 270; 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) scaleY(-1)`
: `translate(-50%, -50%) rotate(${rotation}deg)`; : `translate(-50%, -50%) rotate(${rotation}deg)`;
@ -1654,9 +1668,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
disabled={routeLoading} disabled={routeLoading}
className="w-full rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600 disabled:opacity-50" 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 ? "로딩 중..." : "🛣️ 이동경로 보기"}
? "로딩 중..."
: "🛣️ 이동경로 보기"}
</button> </button>
</div> </div>
); );