Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/dashboard
This commit is contained in:
@@ -54,6 +54,7 @@ import deliveryRoutes from "./routes/deliveryRoutes"; // 배송/화물 관리
|
||||
import riskAlertRoutes from "./routes/riskAlertRoutes"; // 리스크/알림 관리
|
||||
import todoRoutes from "./routes/todoRoutes"; // To-Do 관리
|
||||
import bookingRoutes from "./routes/bookingRoutes"; // 예약 요청 관리
|
||||
import mapDataRoutes from "./routes/mapDataRoutes"; // 지도 데이터 관리
|
||||
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
||||
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
||||
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
|
||||
@@ -202,6 +203,7 @@ app.use("/api/delivery", deliveryRoutes); // 배송/화물 관리
|
||||
app.use("/api/risk-alerts", riskAlertRoutes); // 리스크/알림 관리
|
||||
app.use("/api/todos", todoRoutes); // To-Do 관리
|
||||
app.use("/api/bookings", bookingRoutes); // 예약 요청 관리
|
||||
app.use("/api/map-data", mapDataRoutes); // 지도 데이터 조회
|
||||
// app.use("/api/collections", collectionRoutes); // 임시 주석
|
||||
// app.use("/api/batch", batchRoutes); // 임시 주석
|
||||
// app.use('/api/users', userRoutes);
|
||||
|
||||
137
backend-node/src/controllers/mapDataController.ts
Normal file
137
backend-node/src/controllers/mapDataController.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Request, Response } from "express";
|
||||
import { MapDataService } from "../services/mapDataService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* 지도 데이터 조회 컨트롤러
|
||||
* 외부 DB 연결에서 위도/경도 데이터를 가져와 지도에 표시할 수 있도록 변환
|
||||
*/
|
||||
export class MapDataController {
|
||||
private mapDataService: MapDataService;
|
||||
|
||||
constructor() {
|
||||
this.mapDataService = new MapDataService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 DB에서 지도 데이터 조회
|
||||
*/
|
||||
getMapData = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { connectionId } = req.params;
|
||||
const {
|
||||
tableName,
|
||||
latColumn,
|
||||
lngColumn,
|
||||
labelColumn,
|
||||
statusColumn,
|
||||
additionalColumns,
|
||||
whereClause,
|
||||
} = req.query;
|
||||
|
||||
logger.info("🗺️ 지도 데이터 조회 요청:", {
|
||||
connectionId,
|
||||
tableName,
|
||||
latColumn,
|
||||
lngColumn,
|
||||
});
|
||||
|
||||
// 필수 파라미터 검증
|
||||
if (!tableName || !latColumn || !lngColumn) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "tableName, latColumn, lngColumn은 필수입니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const markers = await this.mapDataService.getMapData({
|
||||
connectionId: parseInt(connectionId as string),
|
||||
tableName: tableName as string,
|
||||
latColumn: latColumn as string,
|
||||
lngColumn: lngColumn as string,
|
||||
labelColumn: labelColumn as string,
|
||||
statusColumn: statusColumn as string,
|
||||
additionalColumns: additionalColumns
|
||||
? (additionalColumns as string).split(",")
|
||||
: [],
|
||||
whereClause: whereClause as string,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
markers,
|
||||
count: markers.length,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("❌ 지도 데이터 조회 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "지도 데이터 조회 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 내부 DB에서 지도 데이터 조회
|
||||
*/
|
||||
getInternalMapData = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const {
|
||||
tableName,
|
||||
latColumn,
|
||||
lngColumn,
|
||||
labelColumn,
|
||||
statusColumn,
|
||||
additionalColumns,
|
||||
whereClause,
|
||||
} = req.query;
|
||||
|
||||
logger.info("🗺️ 내부 DB 지도 데이터 조회 요청:", {
|
||||
tableName,
|
||||
latColumn,
|
||||
lngColumn,
|
||||
});
|
||||
|
||||
// 필수 파라미터 검증
|
||||
if (!tableName || !latColumn || !lngColumn) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "tableName, latColumn, lngColumn은 필수입니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const markers = await this.mapDataService.getInternalMapData({
|
||||
tableName: tableName as string,
|
||||
latColumn: latColumn as string,
|
||||
lngColumn: lngColumn as string,
|
||||
labelColumn: labelColumn as string,
|
||||
statusColumn: statusColumn as string,
|
||||
additionalColumns: additionalColumns
|
||||
? (additionalColumns as string).split(",")
|
||||
: [],
|
||||
whereClause: whereClause as string,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
markers,
|
||||
count: markers.length,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("❌ 내부 DB 지도 데이터 조회 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "지도 데이터 조회 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
18
backend-node/src/routes/mapDataRoutes.ts
Normal file
18
backend-node/src/routes/mapDataRoutes.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Router } from "express";
|
||||
import { MapDataController } from "../controllers/mapDataController";
|
||||
|
||||
const router = Router();
|
||||
const mapDataController = new MapDataController();
|
||||
|
||||
/**
|
||||
* 지도 데이터 라우트
|
||||
*/
|
||||
|
||||
// 외부 DB 지도 데이터 조회
|
||||
router.get("/external/:connectionId", mapDataController.getMapData);
|
||||
|
||||
// 내부 DB 지도 데이터 조회
|
||||
router.get("/internal", mapDataController.getInternalMapData);
|
||||
|
||||
export default router;
|
||||
|
||||
229
backend-node/src/services/mapDataService.ts
Normal file
229
backend-node/src/services/mapDataService.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { logger } from "../utils/logger";
|
||||
import { query } from "../database/db";
|
||||
import { ExternalDbConnectionService } from "./externalDbConnectionService";
|
||||
|
||||
interface MapDataQuery {
|
||||
connectionId?: number;
|
||||
tableName: string;
|
||||
latColumn: string;
|
||||
lngColumn: string;
|
||||
labelColumn?: string;
|
||||
statusColumn?: string;
|
||||
additionalColumns?: string[];
|
||||
whereClause?: string;
|
||||
}
|
||||
|
||||
export interface MapMarker {
|
||||
id: string | number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
label?: string;
|
||||
status?: string;
|
||||
additionalInfo?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 지도 데이터 서비스
|
||||
* 외부/내부 DB에서 위도/경도 데이터를 조회하여 지도 마커로 변환
|
||||
*/
|
||||
export class MapDataService {
|
||||
constructor() {
|
||||
// ExternalDbConnectionService는 static 메서드를 사용
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 DB에서 지도 데이터 조회
|
||||
*/
|
||||
async getMapData(params: MapDataQuery): Promise<MapMarker[]> {
|
||||
try {
|
||||
logger.info("🗺️ 외부 DB 지도 데이터 조회 시작:", params);
|
||||
|
||||
// SELECT할 컬럼 목록 구성
|
||||
const selectColumns = [
|
||||
params.latColumn,
|
||||
params.lngColumn,
|
||||
params.labelColumn,
|
||||
params.statusColumn,
|
||||
...(params.additionalColumns || []),
|
||||
].filter(Boolean);
|
||||
|
||||
// 중복 제거
|
||||
const uniqueColumns = Array.from(new Set(selectColumns));
|
||||
|
||||
// SQL 쿼리 구성
|
||||
let sql = `SELECT ${uniqueColumns.map((col) => `"${col}"`).join(", ")} FROM "${params.tableName}"`;
|
||||
|
||||
if (params.whereClause) {
|
||||
sql += ` WHERE ${params.whereClause}`;
|
||||
}
|
||||
|
||||
logger.info("📝 실행할 SQL:", sql);
|
||||
|
||||
// 외부 DB 쿼리 실행 (static 메서드 사용)
|
||||
const result = await ExternalDbConnectionService.executeQuery(
|
||||
params.connectionId!,
|
||||
sql
|
||||
);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
throw new Error("외부 DB 쿼리 실패");
|
||||
}
|
||||
|
||||
// 데이터를 MapMarker 형식으로 변환
|
||||
const markers = this.convertToMarkers(
|
||||
result.data,
|
||||
params.latColumn,
|
||||
params.lngColumn,
|
||||
params.labelColumn,
|
||||
params.statusColumn,
|
||||
params.additionalColumns
|
||||
);
|
||||
|
||||
logger.info(`✅ ${markers.length}개의 마커 데이터 변환 완료`);
|
||||
|
||||
return markers;
|
||||
} catch (error) {
|
||||
logger.error("❌ 외부 DB 지도 데이터 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 내부 DB에서 지도 데이터 조회
|
||||
*/
|
||||
async getInternalMapData(
|
||||
params: Omit<MapDataQuery, "connectionId">
|
||||
): Promise<MapMarker[]> {
|
||||
try {
|
||||
logger.info("🗺️ 내부 DB 지도 데이터 조회 시작:", params);
|
||||
|
||||
// SELECT할 컬럼 목록 구성
|
||||
const selectColumns = [
|
||||
params.latColumn,
|
||||
params.lngColumn,
|
||||
params.labelColumn,
|
||||
params.statusColumn,
|
||||
...(params.additionalColumns || []),
|
||||
].filter(Boolean);
|
||||
|
||||
// 중복 제거
|
||||
const uniqueColumns = Array.from(new Set(selectColumns));
|
||||
|
||||
// SQL 쿼리 구성
|
||||
let sql = `SELECT ${uniqueColumns.map((col) => `"${col}"`).join(", ")} FROM "${params.tableName}"`;
|
||||
|
||||
if (params.whereClause) {
|
||||
sql += ` WHERE ${params.whereClause}`;
|
||||
}
|
||||
|
||||
logger.info("📝 실행할 SQL:", sql);
|
||||
|
||||
// 내부 DB 쿼리 실행
|
||||
const rows = await query(sql);
|
||||
|
||||
// 데이터를 MapMarker 형식으로 변환
|
||||
const markers = this.convertToMarkers(
|
||||
rows,
|
||||
params.latColumn,
|
||||
params.lngColumn,
|
||||
params.labelColumn,
|
||||
params.statusColumn,
|
||||
params.additionalColumns
|
||||
);
|
||||
|
||||
logger.info(`✅ ${markers.length}개의 마커 데이터 변환 완료`);
|
||||
|
||||
return markers;
|
||||
} catch (error) {
|
||||
logger.error("❌ 내부 DB 지도 데이터 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DB 결과를 MapMarker 배열로 변환
|
||||
*/
|
||||
private convertToMarkers(
|
||||
data: any[],
|
||||
latColumn: string,
|
||||
lngColumn: string,
|
||||
labelColumn?: string,
|
||||
statusColumn?: string,
|
||||
additionalColumns?: string[]
|
||||
): MapMarker[] {
|
||||
const markers: MapMarker[] = [];
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const row = data[i];
|
||||
|
||||
// 위도/경도 추출 (다양한 컬럼명 지원)
|
||||
const lat = this.extractCoordinate(row, latColumn);
|
||||
const lng = this.extractCoordinate(row, lngColumn);
|
||||
|
||||
// 유효한 좌표인지 확인
|
||||
if (lat === null || lng === null || isNaN(lat) || isNaN(lng)) {
|
||||
logger.warn(`⚠️ 유효하지 않은 좌표 스킵: row ${i}`, { lat, lng });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 위도 범위 체크 (-90 ~ 90)
|
||||
if (lat < -90 || lat > 90) {
|
||||
logger.warn(`⚠️ 위도 범위 초과: ${lat}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 경도 범위 체크 (-180 ~ 180)
|
||||
if (lng < -180 || lng > 180) {
|
||||
logger.warn(`⚠️ 경도 범위 초과: ${lng}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 추가 정보 수집
|
||||
const additionalInfo: Record<string, any> = {};
|
||||
if (additionalColumns) {
|
||||
for (const col of additionalColumns) {
|
||||
if (col && row[col] !== undefined) {
|
||||
additionalInfo[col] = row[col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 마커 생성
|
||||
markers.push({
|
||||
id: row.id || row.ID || `marker-${i}`,
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
label: labelColumn ? row[labelColumn] : undefined,
|
||||
status: statusColumn ? row[statusColumn] : undefined,
|
||||
additionalInfo: Object.keys(additionalInfo).length > 0 ? additionalInfo : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 다양한 형식의 좌표 추출
|
||||
*/
|
||||
private extractCoordinate(row: any, columnName: string): number | null {
|
||||
const value = row[columnName];
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 이미 숫자인 경우
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 문자열인 경우 파싱
|
||||
if (typeof value === "string") {
|
||||
const parsed = parseFloat(value);
|
||||
return isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user