지도 위젯 REST API Request Body 전달 오류 수정
This commit is contained in:
parent
fab292f465
commit
133b50dcaa
|
|
@ -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" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 텍스트 응답인 경우 포맷팅
|
// 텍스트 응답인 경우 포맷팅
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
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>
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue