null로 저장되게 성공시킴
This commit is contained in:
parent
652617fe37
commit
552beabdc0
|
|
@ -267,4 +267,46 @@ router.post(
|
|||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /api/external-rest-api-connections/:id/fetch
|
||||
* REST API 데이터 조회 (화면관리용 프록시)
|
||||
*/
|
||||
router.post(
|
||||
"/:id/fetch",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 ID입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const { endpoint, jsonPath } = req.body;
|
||||
const userCompanyCode = req.user?.companyCode;
|
||||
|
||||
logger.info(`REST API 데이터 조회 요청: 연결 ID=${id}, endpoint=${endpoint}, jsonPath=${jsonPath}`);
|
||||
|
||||
const result = await ExternalRestApiConnectionService.fetchData(
|
||||
id,
|
||||
endpoint,
|
||||
jsonPath,
|
||||
userCompanyCode
|
||||
);
|
||||
|
||||
return res.status(result.success ? 200 : 400).json(result);
|
||||
} catch (error) {
|
||||
logger.error("REST API 데이터 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -166,6 +166,9 @@ export class ExternalRestApiConnectionService {
|
|||
? this.decryptSensitiveData(connection.auth_config)
|
||||
: null;
|
||||
|
||||
// 디버깅: 조회된 연결 정보 로깅
|
||||
logger.info(`REST API 연결 조회 결과 (ID: ${id}): connection_name=${connection.connection_name}, default_method=${connection.default_method}, endpoint_path=${connection.endpoint_path}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: connection,
|
||||
|
|
@ -227,6 +230,15 @@ export class ExternalRestApiConnectionService {
|
|||
data.created_by || "system",
|
||||
];
|
||||
|
||||
// 디버깅: 저장하려는 데이터 로깅
|
||||
logger.info(`REST API 연결 생성 요청 데이터:`, {
|
||||
connection_name: data.connection_name,
|
||||
default_method: data.default_method,
|
||||
endpoint_path: data.endpoint_path,
|
||||
base_url: data.base_url,
|
||||
default_body: data.default_body ? "있음" : "없음",
|
||||
});
|
||||
|
||||
const result: QueryResult<any> = await pool.query(query, params);
|
||||
|
||||
logger.info(`REST API 연결 생성 성공: ${data.connection_name}`);
|
||||
|
|
@ -316,12 +328,14 @@ export class ExternalRestApiConnectionService {
|
|||
updateFields.push(`default_method = $${paramIndex}`);
|
||||
params.push(data.default_method);
|
||||
paramIndex++;
|
||||
logger.info(`수정 요청 - default_method: ${data.default_method}`);
|
||||
}
|
||||
|
||||
if (data.default_body !== undefined) {
|
||||
updateFields.push(`default_request_body = $${paramIndex}`);
|
||||
params.push(data.default_body);
|
||||
params.push(data.default_body); // null이면 DB에서 NULL로 저장됨
|
||||
paramIndex++;
|
||||
logger.info(`수정 요청 - default_body: ${data.default_body ? "있음" : "삭제(null)"}`);
|
||||
}
|
||||
|
||||
if (data.auth_type !== undefined) {
|
||||
|
|
@ -870,6 +884,166 @@ export class ExternalRestApiConnectionService {
|
|||
return decrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API 데이터 조회 (화면관리용 프록시)
|
||||
* 저장된 연결 정보를 사용하여 외부 REST API를 호출하고 데이터를 반환
|
||||
*/
|
||||
static async fetchData(
|
||||
connectionId: number,
|
||||
endpoint?: string,
|
||||
jsonPath?: string,
|
||||
userCompanyCode?: string
|
||||
): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
// 연결 정보 조회
|
||||
const connectionResult = await this.getConnectionById(connectionId, userCompanyCode);
|
||||
|
||||
if (!connectionResult.success || !connectionResult.data) {
|
||||
return {
|
||||
success: false,
|
||||
message: "REST API 연결을 찾을 수 없습니다.",
|
||||
error: {
|
||||
code: "CONNECTION_NOT_FOUND",
|
||||
details: `연결 ID ${connectionId}를 찾을 수 없습니다.`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const connection = connectionResult.data;
|
||||
|
||||
// 비활성화된 연결인지 확인
|
||||
if (connection.is_active !== "Y") {
|
||||
return {
|
||||
success: false,
|
||||
message: "비활성화된 REST API 연결입니다.",
|
||||
error: {
|
||||
code: "CONNECTION_INACTIVE",
|
||||
details: "연결이 비활성화 상태입니다.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 엔드포인트 결정 (파라미터 > 저장된 값)
|
||||
const effectiveEndpoint = endpoint || connection.endpoint_path || "";
|
||||
|
||||
// API 호출을 위한 테스트 요청 생성
|
||||
const testRequest: RestApiTestRequest = {
|
||||
id: connection.id,
|
||||
base_url: connection.base_url,
|
||||
endpoint: effectiveEndpoint,
|
||||
method: (connection.default_method as any) || "GET",
|
||||
headers: connection.default_headers,
|
||||
body: connection.default_body,
|
||||
auth_type: connection.auth_type,
|
||||
auth_config: connection.auth_config,
|
||||
timeout: connection.timeout,
|
||||
};
|
||||
|
||||
// API 호출
|
||||
const result = await this.testConnection(testRequest, connection.company_code);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: result.message || "REST API 호출에 실패했습니다.",
|
||||
error: {
|
||||
code: "API_CALL_FAILED",
|
||||
details: result.error_details,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 응답 데이터에서 jsonPath로 데이터 추출
|
||||
let extractedData = result.response_data;
|
||||
|
||||
logger.info(`REST API 원본 응답 데이터 타입: ${typeof result.response_data}`);
|
||||
logger.info(`REST API 원본 응답 데이터 (일부): ${JSON.stringify(result.response_data)?.substring(0, 500)}`);
|
||||
|
||||
if (jsonPath && result.response_data) {
|
||||
try {
|
||||
// jsonPath로 데이터 추출 (예: "data", "data.items", "result.list")
|
||||
const pathParts = jsonPath.split(".");
|
||||
logger.info(`JSON Path 파싱: ${jsonPath} -> [${pathParts.join(", ")}]`);
|
||||
|
||||
for (const part of pathParts) {
|
||||
if (extractedData && typeof extractedData === "object") {
|
||||
extractedData = (extractedData as any)[part];
|
||||
logger.info(`JSON Path '${part}' 추출 결과 타입: ${typeof extractedData}, 배열?: ${Array.isArray(extractedData)}`);
|
||||
} else {
|
||||
logger.warn(`JSON Path '${part}' 추출 실패: extractedData가 객체가 아님`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (pathError) {
|
||||
logger.warn(`JSON Path 추출 실패: ${jsonPath}`, pathError);
|
||||
// 추출 실패 시 원본 데이터 반환
|
||||
extractedData = result.response_data;
|
||||
}
|
||||
}
|
||||
|
||||
// 데이터가 배열이 아닌 경우 배열로 변환
|
||||
// null이나 undefined인 경우 빈 배열로 처리
|
||||
let dataArray: any[] = [];
|
||||
if (extractedData === null || extractedData === undefined) {
|
||||
logger.warn("추출된 데이터가 null/undefined입니다. 원본 응답 데이터를 사용합니다.");
|
||||
// jsonPath 추출 실패 시 원본 데이터에서 직접 컬럼 추출 시도
|
||||
if (result.response_data && typeof result.response_data === "object") {
|
||||
dataArray = Array.isArray(result.response_data) ? result.response_data : [result.response_data];
|
||||
}
|
||||
} else {
|
||||
dataArray = Array.isArray(extractedData) ? extractedData : [extractedData];
|
||||
}
|
||||
|
||||
logger.info(`최종 데이터 배열 길이: ${dataArray.length}`);
|
||||
if (dataArray.length > 0) {
|
||||
logger.info(`첫 번째 데이터 항목: ${JSON.stringify(dataArray[0])?.substring(0, 300)}`);
|
||||
}
|
||||
|
||||
// 컬럼 정보 추출 (첫 번째 유효한 데이터 기준)
|
||||
let columns: Array<{ columnName: string; columnLabel: string; dataType: string }> = [];
|
||||
|
||||
// 첫 번째 유효한 객체 찾기
|
||||
const firstValidItem = dataArray.find(item => item && typeof item === "object" && !Array.isArray(item));
|
||||
|
||||
if (firstValidItem) {
|
||||
columns = Object.keys(firstValidItem).map((key) => ({
|
||||
columnName: key,
|
||||
columnLabel: key,
|
||||
dataType: typeof firstValidItem[key],
|
||||
}));
|
||||
logger.info(`추출된 컬럼 수: ${columns.length}, 컬럼명: [${columns.map(c => c.columnName).join(", ")}]`);
|
||||
} else {
|
||||
logger.warn("유효한 데이터 항목을 찾을 수 없어 컬럼을 추출할 수 없습니다.");
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
rows: dataArray,
|
||||
columns,
|
||||
total: dataArray.length,
|
||||
connectionInfo: {
|
||||
connectionId: connection.id,
|
||||
connectionName: connection.connection_name,
|
||||
baseUrl: connection.base_url,
|
||||
endpoint: effectiveEndpoint,
|
||||
},
|
||||
},
|
||||
message: `${dataArray.length}개의 데이터를 조회했습니다.`,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("REST API 데이터 조회 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "REST API 데이터 조회에 실패했습니다.",
|
||||
error: {
|
||||
code: "FETCH_ERROR",
|
||||
details: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 연결 데이터 유효성 검증
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
|||
endpoint_path: endpointPath || undefined,
|
||||
default_headers: defaultHeaders,
|
||||
default_method: defaultMethod,
|
||||
default_body: defaultBody || undefined,
|
||||
default_body: defaultBody.trim() || null, // 빈 문자열이면 null로 전송하여 DB 업데이트
|
||||
auth_type: authType,
|
||||
auth_config: authType === "none" ? undefined : authConfig,
|
||||
timeout,
|
||||
|
|
@ -236,6 +236,13 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
|||
is_active: isActive ? "Y" : "N",
|
||||
};
|
||||
|
||||
console.log("저장하려는 데이터:", {
|
||||
connection_name: connectionName,
|
||||
default_method: defaultMethod,
|
||||
endpoint_path: endpointPath,
|
||||
base_url: baseUrl,
|
||||
});
|
||||
|
||||
if (connection?.id) {
|
||||
await ExternalRestApiConnectionAPI.updateConnection(connection.id, data);
|
||||
toast({
|
||||
|
|
@ -303,7 +310,13 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
|||
기본 URL <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Select value={defaultMethod} onValueChange={setDefaultMethod}>
|
||||
<Select
|
||||
value={defaultMethod}
|
||||
onValueChange={(val) => {
|
||||
setDefaultMethod(val);
|
||||
setTestMethod(val); // 테스트 Method도 동기화
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectValue placeholder="Method" />
|
||||
</SelectTrigger>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ const calculateGridInfo = (width: number, height: number, settings: any) => {
|
|||
import { GroupingToolbar } from "./GroupingToolbar";
|
||||
import { screenApi, tableTypeApi } from "@/lib/api/screen";
|
||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||
import { ExternalRestApiConnectionAPI } from "@/lib/api/externalRestApiConnection";
|
||||
import { toast } from "sonner";
|
||||
import { MenuAssignmentModal } from "./MenuAssignmentModal";
|
||||
import { FileAttachmentDetailModal } from "./FileAttachmentDetailModal";
|
||||
|
|
@ -835,9 +836,52 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
}
|
||||
}, []);
|
||||
|
||||
// 화면의 기본 테이블 정보 로드 (원래대로 복원)
|
||||
// 화면의 기본 테이블/REST API 정보 로드
|
||||
useEffect(() => {
|
||||
const loadScreenTable = async () => {
|
||||
const loadScreenDataSource = async () => {
|
||||
// REST API 데이터 소스인 경우
|
||||
if (selectedScreen?.dataSourceType === "restapi" && selectedScreen?.restApiConnectionId) {
|
||||
try {
|
||||
const restApiData = await ExternalRestApiConnectionAPI.fetchData(
|
||||
selectedScreen.restApiConnectionId,
|
||||
selectedScreen.restApiEndpoint,
|
||||
selectedScreen.restApiJsonPath || "data",
|
||||
);
|
||||
|
||||
// REST API 응답에서 컬럼 정보 생성
|
||||
const columns: ColumnInfo[] = restApiData.columns.map((col) => ({
|
||||
tableName: `restapi_${selectedScreen.restApiConnectionId}`,
|
||||
columnName: col.columnName,
|
||||
columnLabel: col.columnLabel,
|
||||
dataType: col.dataType === "string" ? "varchar" : col.dataType === "number" ? "numeric" : col.dataType,
|
||||
webType: col.dataType === "number" ? "number" : "text",
|
||||
input_type: "text",
|
||||
widgetType: col.dataType === "number" ? "number" : "text",
|
||||
isNullable: "YES",
|
||||
required: false,
|
||||
}));
|
||||
|
||||
const tableInfo: TableInfo = {
|
||||
tableName: `restapi_${selectedScreen.restApiConnectionId}`,
|
||||
tableLabel: restApiData.connectionInfo.connectionName || "REST API 데이터",
|
||||
columns,
|
||||
};
|
||||
|
||||
setTables([tableInfo]);
|
||||
console.log("REST API 데이터 소스 로드 완료:", {
|
||||
connectionName: restApiData.connectionInfo.connectionName,
|
||||
columnsCount: columns.length,
|
||||
rowsCount: restApiData.total,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("REST API 데이터 소스 로드 실패:", error);
|
||||
toast.error("REST API 데이터를 불러오는데 실패했습니다.");
|
||||
setTables([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 데이터베이스 데이터 소스인 경우 (기존 로직)
|
||||
const tableName = selectedScreen?.tableName;
|
||||
if (!tableName) {
|
||||
setTables([]);
|
||||
|
|
@ -859,16 +903,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => {
|
||||
const widgetType = col.widgetType || col.widget_type || col.webType || col.web_type;
|
||||
|
||||
// 🔍 이미지 타입 디버깅
|
||||
// if (widgetType === "image" || col.webType === "image" || col.web_type === "image") {
|
||||
// console.log("🖼️ 이미지 컬럼 발견:", {
|
||||
// columnName: col.columnName || col.column_name,
|
||||
// widgetType,
|
||||
// webType: col.webType || col.web_type,
|
||||
// rawData: col,
|
||||
// });
|
||||
// }
|
||||
|
||||
return {
|
||||
tableName: col.tableName || tableName,
|
||||
columnName: col.columnName || col.column_name,
|
||||
|
|
@ -899,8 +933,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
}
|
||||
};
|
||||
|
||||
loadScreenTable();
|
||||
}, [selectedScreen?.tableName, selectedScreen?.screenName]);
|
||||
loadScreenDataSource();
|
||||
}, [selectedScreen?.tableName, selectedScreen?.screenName, selectedScreen?.dataSourceType, selectedScreen?.restApiConnectionId, selectedScreen?.restApiEndpoint, selectedScreen?.restApiJsonPath]);
|
||||
|
||||
// 화면 레이아웃 로드
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -192,6 +192,43 @@ export class ExternalRestApiConnectionAPI {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API 데이터 조회 (화면관리용 프록시)
|
||||
*/
|
||||
static async fetchData(
|
||||
connectionId: number,
|
||||
endpoint?: string,
|
||||
jsonPath?: string,
|
||||
): Promise<{
|
||||
rows: any[];
|
||||
columns: Array<{ columnName: string; columnLabel: string; dataType: string }>;
|
||||
total: number;
|
||||
connectionInfo: {
|
||||
connectionId: number;
|
||||
connectionName: string;
|
||||
baseUrl: string;
|
||||
endpoint: string;
|
||||
};
|
||||
}> {
|
||||
const response = await apiClient.post<ApiResponse<{
|
||||
rows: any[];
|
||||
columns: Array<{ columnName: string; columnLabel: string; dataType: string }>;
|
||||
total: number;
|
||||
connectionInfo: {
|
||||
connectionId: number;
|
||||
connectionName: string;
|
||||
baseUrl: string;
|
||||
endpoint: string;
|
||||
};
|
||||
}>>(`${this.BASE_PATH}/${connectionId}/fetch`, { endpoint, jsonPath });
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "REST API 데이터 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 지원되는 인증 타입 목록
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue