null로 저장되게 성공시킴

This commit is contained in:
leeheejin 2025-11-28 14:45:04 +09:00
parent 652617fe37
commit 552beabdc0
5 changed files with 317 additions and 17 deletions

View File

@ -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;

View File

@ -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 : "알 수 없는 오류",
},
};
}
}
/**
*
*/

View File

@ -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>

View File

@ -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(() => {

View File

@ -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!;
}
/**
*
*/