Merge remote-tracking branch 'origin/main' into ksh
This commit is contained in:
@@ -632,6 +632,9 @@ export class DashboardController {
|
||||
validateStatus: () => true, // 모든 상태 코드 허용 (에러도 응답으로 처리)
|
||||
};
|
||||
|
||||
// 연결 정보 (응답에 포함용)
|
||||
let connectionInfo: { saveToHistory?: boolean } | null = null;
|
||||
|
||||
// 외부 커넥션 ID가 있는 경우, 해당 커넥션의 인증 정보(DB 토큰 등)를 적용
|
||||
if (externalConnectionId) {
|
||||
try {
|
||||
@@ -652,6 +655,11 @@ export class DashboardController {
|
||||
if (connectionResult.success && connectionResult.data) {
|
||||
const connection = connectionResult.data;
|
||||
|
||||
// 연결 정보 저장 (응답에 포함)
|
||||
connectionInfo = {
|
||||
saveToHistory: connection.save_to_history === "Y",
|
||||
};
|
||||
|
||||
// 인증 헤더 생성 (DB 토큰 등)
|
||||
const authHeaders =
|
||||
await ExternalRestApiConnectionService.getAuthHeaders(
|
||||
@@ -709,9 +717,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 +735,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('<27>'))) {
|
||||
data = { text: utf8Text, contentType, encoding: 'utf-8' };
|
||||
if (
|
||||
utf8Text.includes("특보") ||
|
||||
utf8Text.includes("경보") ||
|
||||
utf8Text.includes("주의보") ||
|
||||
(utf8Text.includes("#START7777") && !utf8Text.includes("<22>"))
|
||||
) {
|
||||
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" };
|
||||
}
|
||||
}
|
||||
// 텍스트 응답인 경우 포맷팅
|
||||
@@ -749,6 +761,7 @@ export class DashboardController {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data,
|
||||
connectionInfo, // 외부 연결 정보 (saveToHistory 등)
|
||||
});
|
||||
} catch (error: any) {
|
||||
const status = error.response?.status || 500;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 배치관리 전용 컨트롤러 (기존 소스와 완전 분리)
|
||||
// 작성일: 2024-12-24
|
||||
|
||||
import { Response } from "express";
|
||||
import { Request, Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import {
|
||||
BatchManagementService,
|
||||
@@ -13,6 +13,7 @@ import { BatchService } from "../services/batchService";
|
||||
import { BatchSchedulerService } from "../services/batchSchedulerService";
|
||||
import { BatchExternalDbService } from "../services/batchExternalDbService";
|
||||
import { CreateBatchConfigRequest, BatchConfig } from "../types/batchTypes";
|
||||
import { query } from "../database/db";
|
||||
|
||||
export class BatchManagementController {
|
||||
/**
|
||||
@@ -422,6 +423,8 @@ export class BatchManagementController {
|
||||
paramValue,
|
||||
paramSource,
|
||||
requestBody,
|
||||
authServiceName, // DB에서 토큰 가져올 서비스명
|
||||
dataArrayPath, // 데이터 배열 경로 (예: response, data.items)
|
||||
} = req.body;
|
||||
|
||||
// apiUrl, endpoint는 항상 필수
|
||||
@@ -432,15 +435,47 @@ export class BatchManagementController {
|
||||
});
|
||||
}
|
||||
|
||||
// GET 요청일 때만 API Key 필수 (POST/PUT/DELETE는 선택)
|
||||
if ((!method || method === "GET") && !apiKey) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "GET 메서드에서는 API Key가 필요합니다.",
|
||||
});
|
||||
// 토큰 결정: authServiceName이 있으면 DB에서 조회, 없으면 apiKey 사용
|
||||
let finalApiKey = apiKey || "";
|
||||
if (authServiceName) {
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
// DB에서 토큰 조회 (멀티테넌시: company_code 필터링)
|
||||
let tokenQuery: string;
|
||||
let tokenParams: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 회사 토큰 조회 가능
|
||||
tokenQuery = `SELECT access_token FROM auth_tokens
|
||||
WHERE service_name = $1
|
||||
ORDER BY created_date DESC LIMIT 1`;
|
||||
tokenParams = [authServiceName];
|
||||
} else {
|
||||
// 일반 회사: 자신의 회사 토큰만 조회
|
||||
tokenQuery = `SELECT access_token FROM auth_tokens
|
||||
WHERE service_name = $1 AND company_code = $2
|
||||
ORDER BY created_date DESC LIMIT 1`;
|
||||
tokenParams = [authServiceName, companyCode];
|
||||
}
|
||||
|
||||
const tokenResult = await query<{ access_token: string }>(
|
||||
tokenQuery,
|
||||
tokenParams
|
||||
);
|
||||
if (tokenResult.length > 0 && tokenResult[0].access_token) {
|
||||
finalApiKey = tokenResult[0].access_token;
|
||||
console.log(`auth_tokens에서 토큰 조회 성공: ${authServiceName}`);
|
||||
} else {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `서비스 '${authServiceName}'의 토큰을 찾을 수 없습니다. 먼저 토큰 저장 배치를 실행하세요.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🔍 REST API 미리보기 요청:", {
|
||||
// 토큰이 없어도 공개 API 호출 가능 (토큰 검증 제거)
|
||||
|
||||
console.log("REST API 미리보기 요청:", {
|
||||
apiUrl,
|
||||
endpoint,
|
||||
method,
|
||||
@@ -449,6 +484,8 @@ export class BatchManagementController {
|
||||
paramValue,
|
||||
paramSource,
|
||||
requestBody: requestBody ? "Included" : "None",
|
||||
authServiceName: authServiceName || "직접 입력",
|
||||
dataArrayPath: dataArrayPath || "전체 응답",
|
||||
});
|
||||
|
||||
// RestApiConnector 사용하여 데이터 조회
|
||||
@@ -456,7 +493,7 @@ export class BatchManagementController {
|
||||
|
||||
const connector = new RestApiConnector({
|
||||
baseUrl: apiUrl,
|
||||
apiKey: apiKey || "",
|
||||
apiKey: finalApiKey,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
@@ -511,8 +548,50 @@ export class BatchManagementController {
|
||||
result.rows && result.rows.length > 0 ? result.rows[0] : "no data",
|
||||
});
|
||||
|
||||
const data = result.rows.slice(0, 5); // 최대 5개 샘플만
|
||||
console.log(`[previewRestApiData] 슬라이스된 데이터:`, data);
|
||||
// 데이터 배열 추출 헬퍼 함수
|
||||
const getValueByPath = (obj: any, path: string): any => {
|
||||
if (!path) return obj;
|
||||
const keys = path.split(".");
|
||||
let current = obj;
|
||||
for (const key of keys) {
|
||||
if (current === null || current === undefined) return undefined;
|
||||
current = current[key];
|
||||
}
|
||||
return current;
|
||||
};
|
||||
|
||||
// dataArrayPath가 있으면 해당 경로에서 배열 추출
|
||||
let extractedData: any[] = [];
|
||||
if (dataArrayPath) {
|
||||
// result.rows가 단일 객체일 수 있음 (API 응답 전체)
|
||||
const rawData = result.rows.length === 1 ? result.rows[0] : result.rows;
|
||||
const arrayData = getValueByPath(rawData, dataArrayPath);
|
||||
|
||||
if (Array.isArray(arrayData)) {
|
||||
extractedData = arrayData;
|
||||
console.log(
|
||||
`[previewRestApiData] '${dataArrayPath}' 경로에서 ${arrayData.length}개 항목 추출`
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`[previewRestApiData] '${dataArrayPath}' 경로가 배열이 아님:`,
|
||||
typeof arrayData
|
||||
);
|
||||
// 배열이 아니면 단일 객체로 처리
|
||||
if (arrayData) {
|
||||
extractedData = [arrayData];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// dataArrayPath가 없으면 기존 로직 사용
|
||||
extractedData = result.rows;
|
||||
}
|
||||
|
||||
const data = extractedData.slice(0, 5); // 최대 5개 샘플만
|
||||
console.log(
|
||||
`[previewRestApiData] 슬라이스된 데이터 (${extractedData.length}개 중 ${data.length}개):`,
|
||||
data
|
||||
);
|
||||
|
||||
if (data.length > 0) {
|
||||
// 첫 번째 객체에서 필드명 추출
|
||||
@@ -524,9 +603,9 @@ export class BatchManagementController {
|
||||
data: {
|
||||
fields: fields,
|
||||
samples: data,
|
||||
totalCount: result.rowCount || data.length,
|
||||
totalCount: extractedData.length,
|
||||
},
|
||||
message: `${fields.length}개 필드, ${result.rowCount || data.length}개 레코드를 조회했습니다.`,
|
||||
message: `${fields.length}개 필드, ${extractedData.length}개 레코드를 조회했습니다.`,
|
||||
});
|
||||
} else {
|
||||
return res.json({
|
||||
@@ -554,8 +633,17 @@ export class BatchManagementController {
|
||||
*/
|
||||
static async saveRestApiBatch(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { batchName, batchType, cronSchedule, description, apiMappings } =
|
||||
req.body;
|
||||
const {
|
||||
batchName,
|
||||
batchType,
|
||||
cronSchedule,
|
||||
description,
|
||||
apiMappings,
|
||||
authServiceName,
|
||||
dataArrayPath,
|
||||
saveMode,
|
||||
conflictKey,
|
||||
} = req.body;
|
||||
|
||||
if (
|
||||
!batchName ||
|
||||
@@ -576,6 +664,10 @@ export class BatchManagementController {
|
||||
cronSchedule,
|
||||
description,
|
||||
apiMappings,
|
||||
authServiceName,
|
||||
dataArrayPath,
|
||||
saveMode,
|
||||
conflictKey,
|
||||
});
|
||||
|
||||
// 🔐 멀티테넌시: 현재 사용자 회사 코드 사용 (프론트에서 받지 않음)
|
||||
@@ -589,6 +681,10 @@ export class BatchManagementController {
|
||||
cronSchedule: cronSchedule,
|
||||
isActive: "Y",
|
||||
companyCode,
|
||||
authServiceName: authServiceName || undefined,
|
||||
dataArrayPath: dataArrayPath || undefined,
|
||||
saveMode: saveMode || "INSERT",
|
||||
conflictKey: conflictKey || undefined,
|
||||
mappings: apiMappings,
|
||||
};
|
||||
|
||||
@@ -625,4 +721,51 @@ export class BatchManagementController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증 토큰 서비스명 목록 조회
|
||||
*/
|
||||
static async getAuthServiceNames(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
// 멀티테넌시: company_code 필터링
|
||||
let queryText: string;
|
||||
let queryParams: any[] = [];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 서비스 조회
|
||||
queryText = `SELECT DISTINCT service_name
|
||||
FROM auth_tokens
|
||||
WHERE service_name IS NOT NULL
|
||||
ORDER BY service_name`;
|
||||
} else {
|
||||
// 일반 회사: 자신의 회사 서비스만 조회
|
||||
queryText = `SELECT DISTINCT service_name
|
||||
FROM auth_tokens
|
||||
WHERE service_name IS NOT NULL
|
||||
AND company_code = $1
|
||||
ORDER BY service_name`;
|
||||
queryParams = [companyCode];
|
||||
}
|
||||
|
||||
const result = await query<{ service_name: string }>(
|
||||
queryText,
|
||||
queryParams
|
||||
);
|
||||
|
||||
const serviceNames = result.map((row) => row.service_name);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: serviceNames,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("인증 서비스 목록 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "인증 서비스 목록 조회 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,7 +492,7 @@ export const saveLocationHistory = async (
|
||||
res: Response
|
||||
): Promise<Response | void> => {
|
||||
try {
|
||||
const { companyCode, userId } = req.user as any;
|
||||
const { companyCode, userId: loginUserId } = req.user as any;
|
||||
const {
|
||||
latitude,
|
||||
longitude,
|
||||
@@ -508,10 +508,17 @@ export const saveLocationHistory = async (
|
||||
destinationName,
|
||||
recordedAt,
|
||||
vehicleId,
|
||||
userId: requestUserId, // 프론트엔드에서 보낸 userId (차량 번호판 등)
|
||||
} = req.body;
|
||||
|
||||
// 프론트엔드에서 보낸 userId가 있으면 그것을 사용 (차량 번호판 등)
|
||||
// 없으면 로그인한 사용자의 userId 사용
|
||||
const userId = requestUserId || loginUserId;
|
||||
|
||||
console.log("📍 [saveLocationHistory] 요청:", {
|
||||
userId,
|
||||
requestUserId,
|
||||
loginUserId,
|
||||
companyCode,
|
||||
latitude,
|
||||
longitude,
|
||||
|
||||
Reference in New Issue
Block a user