get요청 db 저장기능

This commit is contained in:
kjs 2025-09-29 13:32:59 +09:00
parent c9afdec09f
commit 0fca8cd90b
5 changed files with 287 additions and 76 deletions

View File

@ -92,7 +92,7 @@ app.use(
// Rate Limiting (개발 환경에서는 완화) // Rate Limiting (개발 환경에서는 완화)
const limiter = rateLimit({ const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1분 windowMs: 1 * 60 * 1000, // 1분
max: config.nodeEnv === "development" ? 10000 : 1000, // 개발환경에서는 10000으로 증가, 운영환경에서는 100 max: config.nodeEnv === "development" ? 10000 : 100, // 개발환경에서는 10000으로 증가, 운영환경에서는 100
message: { message: {
error: "너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.", error: "너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.",
}, },

View File

@ -30,6 +30,7 @@ interface DataMappingSettingsProps {
httpMethod: string; httpMethod: string;
availableTables?: TableInfo[]; availableTables?: TableInfo[];
readonly?: boolean; readonly?: boolean;
tablesLoading?: boolean;
} }
export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
@ -38,6 +39,7 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
httpMethod, httpMethod,
availableTables = [], availableTables = [],
readonly = false, readonly = false,
tablesLoading = false,
}) => { }) => {
const [localConfig, setLocalConfig] = useState<DataMappingConfig>(config); const [localConfig, setLocalConfig] = useState<DataMappingConfig>(config);
@ -228,17 +230,27 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
<Select <Select
value={localConfig.inboundMapping?.targetTable || ""} value={localConfig.inboundMapping?.targetTable || ""}
onValueChange={(value) => handleInboundMappingChange({ targetTable: value })} onValueChange={(value) => handleInboundMappingChange({ targetTable: value })}
disabled={readonly} disabled={readonly || tablesLoading}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="저장할 테이블을 선택하세요" /> <SelectValue placeholder={tablesLoading ? "테이블 목록 로딩 중..." : "저장할 테이블을 선택하세요"} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{availableTables.map((table) => ( {tablesLoading ? (
<SelectItem value="" disabled>
...
</SelectItem>
) : availableTables.length === 0 ? (
<SelectItem value="" disabled>
</SelectItem>
) : (
availableTables.map((table) => (
<SelectItem key={table.name} value={table.name}> <SelectItem key={table.name} value={table.name}>
{table.displayName || table.name} {table.displayName || table.name}
</SelectItem> </SelectItem>
))} ))
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>

View File

@ -17,6 +17,10 @@ import {
} from "@/types/external-call/ExternalCallTypes"; } from "@/types/external-call/ExternalCallTypes";
import { DataMappingConfig, TableInfo } from "@/types/external-call/DataMappingTypes"; import { DataMappingConfig, TableInfo } from "@/types/external-call/DataMappingTypes";
// API import
import { DataFlowAPI } from "@/lib/api/dataflow";
import { toast } from "sonner";
// 하위 컴포넌트 import // 하위 컴포넌트 import
import RestApiSettings from "./RestApiSettings"; import RestApiSettings from "./RestApiSettings";
import ExternalCallTestPanel from "./ExternalCallTestPanel"; import ExternalCallTestPanel from "./ExternalCallTestPanel";
@ -41,8 +45,14 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
}); });
// 상태 관리 // 상태 관리
const [config, setConfig] = useState<ExternalCallConfig>( const [config, setConfig] = useState<ExternalCallConfig>(
() => () => {
initialSettings || { if (initialSettings) {
console.log("🔄 [ExternalCallPanel] 기존 설정 로드:", initialSettings);
return initialSettings;
}
console.log("🔄 [ExternalCallPanel] 기본 설정 사용");
return {
callType: "rest-api", callType: "rest-api",
restApiSettings: { restApiSettings: {
apiUrl: "", apiUrl: "",
@ -63,6 +73,7 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
timeout: 30000, // 30초 timeout: 30000, // 30초
retryCount: 3, retryCount: 3,
}, },
};
}, },
); );
@ -71,48 +82,69 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
const [isConfigValid, setIsConfigValid] = useState<boolean>(false); const [isConfigValid, setIsConfigValid] = useState<boolean>(false);
// 데이터 매핑 상태 // 데이터 매핑 상태
const [dataMappingConfig, setDataMappingConfig] = useState<DataMappingConfig>(() => ({ const [dataMappingConfig, setDataMappingConfig] = useState<DataMappingConfig>(() => {
direction: "none", // initialSettings에서 데이터 매핑 정보 불러오기
})); if (initialSettings?.dataMappingConfig) {
console.log("🔄 [ExternalCallPanel] 기존 데이터 매핑 설정 로드:", initialSettings.dataMappingConfig);
return initialSettings.dataMappingConfig;
}
// 사용 가능한 테이블 목록 (임시 데이터) console.log("🔄 [ExternalCallPanel] 기본 데이터 매핑 설정 사용");
const [availableTables] = useState<TableInfo[]>([ return {
{ direction: "none",
name: "customers", };
displayName: "고객", });
fields: [
{ name: "id", dataType: "number", nullable: false, isPrimaryKey: true }, // 사용 가능한 테이블 목록 (실제 API에서 로드)
{ name: "name", dataType: "string", nullable: false }, const [availableTables, setAvailableTables] = useState<TableInfo[]>([]);
{ name: "email", dataType: "string", nullable: true }, const [tablesLoading, setTablesLoading] = useState(false);
{ name: "phone", dataType: "string", nullable: true },
{ name: "created_at", dataType: "date", nullable: false }, // 테이블 목록 로드
], useEffect(() => {
}, const loadTables = async () => {
{ try {
name: "orders", setTablesLoading(true);
displayName: "주문", const tables = await DataFlowAPI.getTables();
fields: [
{ name: "id", dataType: "number", nullable: false, isPrimaryKey: true }, // 테이블 정보를 TableInfo 형식으로 변환
{ name: "customer_id", dataType: "number", nullable: false }, const tableInfos: TableInfo[] = await Promise.all(
{ name: "product_name", dataType: "string", nullable: false }, tables.map(async (table) => {
{ name: "quantity", dataType: "number", nullable: false }, try {
{ name: "price", dataType: "number", nullable: false }, const columns = await DataFlowAPI.getTableColumns(table.tableName);
{ name: "status", dataType: "string", nullable: false }, return {
{ name: "order_date", dataType: "date", nullable: false }, name: table.tableName,
], displayName: table.displayName || table.tableName,
}, fields: columns.map((col) => ({
{ name: col.columnName,
name: "products", dataType: col.dataType,
displayName: "제품", nullable: col.nullable,
fields: [ isPrimaryKey: col.isPrimaryKey || false,
{ name: "id", dataType: "number", nullable: false, isPrimaryKey: true }, })),
{ name: "name", dataType: "string", nullable: false }, };
{ name: "price", dataType: "number", nullable: false }, } catch (error) {
{ name: "stock", dataType: "number", nullable: false }, console.warn(`테이블 ${table.tableName} 컬럼 정보 로드 실패:`, error);
{ name: "category", dataType: "string", nullable: true }, return {
], name: table.tableName,
}, displayName: table.displayName || table.tableName,
]); fields: [],
};
}
})
);
setAvailableTables(tableInfos);
} catch (error) {
console.error("테이블 목록 로드 실패:", error);
toast.error("테이블 목록을 불러오는데 실패했습니다.");
// 실패 시 빈 배열로 설정
setAvailableTables([]);
} finally {
setTablesLoading(false);
}
};
loadTables();
}, []);
// 설정 변경 핸들러 // 설정 변경 핸들러
const handleRestApiSettingsChange = useCallback( const handleRestApiSettingsChange = useCallback(
@ -136,6 +168,22 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
[config, onSettingsChange, dataMappingConfig], [config, onSettingsChange, dataMappingConfig],
); );
// 데이터 매핑 설정 변경 핸들러
const handleDataMappingConfigChange = useCallback(
(newMappingConfig: DataMappingConfig) => {
console.log("🔄 [ExternalCallPanel] 데이터 매핑 설정 변경:", newMappingConfig);
setDataMappingConfig(newMappingConfig);
// 전체 설정에 데이터 매핑 정보 포함하여 상위로 전달
onSettingsChange({
...config,
dataMappingConfig: newMappingConfig,
});
},
[config, onSettingsChange],
);
// 테스트 결과 핸들러 // 테스트 결과 핸들러
const handleTestResult = useCallback((result: ApiTestResult) => { const handleTestResult = useCallback((result: ApiTestResult) => {
setLastTestResult(result); setLastTestResult(result);
@ -225,10 +273,11 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
<TabsContent value="mapping" className="flex-1 space-y-2 overflow-y-auto"> <TabsContent value="mapping" className="flex-1 space-y-2 overflow-y-auto">
<DataMappingSettings <DataMappingSettings
config={dataMappingConfig} config={dataMappingConfig}
onConfigChange={setDataMappingConfig} onConfigChange={handleDataMappingConfigChange}
httpMethod={config.restApiSettings?.httpMethod || "GET"} httpMethod={config.restApiSettings?.httpMethod || "GET"}
availableTables={availableTables} availableTables={availableTables}
readonly={readonly} readonly={readonly}
tablesLoading={tablesLoading}
/> />
</TabsContent> </TabsContent>

View File

@ -272,6 +272,14 @@ export class ImprovedButtonActionExecutor {
const relationships = relationshipData.relationships; const relationships = relationshipData.relationships;
const connectionType = relationships.connectionType; const connectionType = relationships.connectionType;
console.log(`🔍 관계 상세 정보:`, {
connectionType,
hasExternalCallConfig: !!relationships.externalCallConfig,
externalCallConfig: relationships.externalCallConfig,
hasDataSaveConfig: !!relationships.dataSaveConfig,
dataSaveConfig: relationships.dataSaveConfig,
});
let result: ExecutionResult; let result: ExecutionResult;
if (connectionType === "external_call") { if (connectionType === "external_call") {
@ -339,8 +347,13 @@ export class ImprovedButtonActionExecutor {
context: ButtonExecutionContext context: ButtonExecutionContext
): Promise<ExecutionResult> { ): Promise<ExecutionResult> {
try { try {
console.log(`🔍 외부 호출 실행 시작 - relationships 구조:`, relationships);
const externalCallConfig = relationships.externalCallConfig; const externalCallConfig = relationships.externalCallConfig;
console.log(`🔍 externalCallConfig:`, externalCallConfig);
if (!externalCallConfig) { if (!externalCallConfig) {
console.error('❌ 외부 호출 설정이 없습니다. relationships 구조:', relationships);
throw new Error('외부 호출 설정이 없습니다'); throw new Error('외부 호출 설정이 없습니다');
} }
@ -394,7 +407,7 @@ export class ImprovedButtonActionExecutor {
timeout: restApiSettings.timeout || 30000, timeout: restApiSettings.timeout || 30000,
retryCount: restApiSettings.retryCount || 3, retryCount: restApiSettings.retryCount || 3,
}, },
templateData: restApiSettings.httpMethod !== 'GET' ? JSON.parse(requestBody) : {}, templateData: restApiSettings.httpMethod !== 'GET' && requestBody ? JSON.parse(requestBody) : formData,
}; };
console.log(`📤 백엔드로 전송할 데이터:`, requestPayload); console.log(`📤 백엔드로 전송할 데이터:`, requestPayload);
@ -412,11 +425,17 @@ export class ImprovedButtonActionExecutor {
// 데이터 매핑 처리 (inbound mapping) // 데이터 매핑 처리 (inbound mapping)
if (externalCallConfig.dataMappingConfig?.inboundMapping) { if (externalCallConfig.dataMappingConfig?.inboundMapping) {
console.log(`📥 데이터 매핑 설정 발견 - HTTP 메서드: ${restApiSettings.httpMethod}`);
console.log(`📥 매핑 설정:`, externalCallConfig.dataMappingConfig.inboundMapping);
console.log(`📥 응답 데이터:`, responseData);
await this.processInboundMapping( await this.processInboundMapping(
externalCallConfig.dataMappingConfig.inboundMapping, externalCallConfig.dataMappingConfig.inboundMapping,
responseData, responseData,
context context
); );
} else {
console.log(` 데이터 매핑 설정이 없습니다 - HTTP 메서드: ${restApiSettings.httpMethod}`);
} }
return { return {
@ -596,28 +615,24 @@ export class ImprovedButtonActionExecutor {
connection?: any connection?: any
): Promise<any> { ): Promise<any> {
try { try {
// 데이터 저장 API 호출 console.log(`💾 테이블 데이터 저장 시작: ${tableName}`, {
const response = await fetch('/api/dataflow/execute-data-action', { actionType,
method: 'POST', data,
headers: { connection
'Content-Type': 'application/json', });
'Authorization': `Bearer ${localStorage.getItem('token')}`,
}, // 데이터 저장 API 호출 (apiClient 사용)
body: JSON.stringify({ const response = await apiClient.post('/dataflow/execute-data-action', {
tableName, tableName,
data, data,
actionType, actionType,
connection, connection,
}),
}); });
if (!response.ok) { console.log(`✅ 테이블 데이터 저장 성공: ${tableName}`, response.data);
throw new Error(`데이터 저장 API 호출 실패: ${response.status}`); return response.data;
}
return await response.json();
} catch (error) { } catch (error) {
console.error('데이터 저장 오류:', error); console.error('테이블 데이터 저장 오류:', error);
throw error; throw error;
} }
} }
@ -670,6 +685,111 @@ export class ImprovedButtonActionExecutor {
return true; return true;
} }
/**
* API
*/
private static extractActualData(responseData: any): any {
console.log(`🔍 데이터 추출 시작 - 원본 타입: ${typeof responseData}`);
// null이나 undefined인 경우
if (!responseData) {
console.log(`⚠️ 응답 데이터가 null 또는 undefined`);
return [];
}
// 이미 배열인 경우 (직접 배열 응답)
if (Array.isArray(responseData)) {
console.log(`✅ 직접 배열 응답 감지`);
return responseData;
}
// 문자열인 경우 JSON 파싱 시도
if (typeof responseData === 'string') {
console.log(`🔄 JSON 문자열 파싱 시도`);
try {
const parsed = JSON.parse(responseData);
console.log(`✅ JSON 파싱 성공, 재귀 호출`);
return this.extractActualData(parsed);
} catch (error) {
console.log(`⚠️ JSON 파싱 실패, 원본 문자열 반환:`, error);
return [responseData];
}
}
// 객체가 아닌 경우 (숫자 등)
if (typeof responseData !== 'object') {
console.log(`⚠️ 객체가 아닌 응답: ${typeof responseData}`);
return [responseData];
}
// 일반적인 데이터 필드명들을 우선순위대로 확인
const commonDataFields = [
'data', // { data: [...] }
'result', // { result: [...] }
'results', // { results: [...] }
'items', // { items: [...] }
'list', // { list: [...] }
'records', // { records: [...] }
'rows', // { rows: [...] }
'content', // { content: [...] }
'payload', // { payload: [...] }
'response', // { response: [...] }
];
for (const field of commonDataFields) {
if (responseData[field] !== undefined) {
console.log(`✅ '${field}' 필드에서 데이터 추출`);
const extractedData = responseData[field];
// 추출된 데이터가 문자열인 경우 JSON 파싱 시도
if (typeof extractedData === 'string') {
console.log(`🔄 추출된 데이터가 JSON 문자열, 파싱 시도`);
try {
const parsed = JSON.parse(extractedData);
console.log(`✅ JSON 파싱 성공, 재귀 호출`);
return this.extractActualData(parsed);
} catch (error) {
console.log(`⚠️ JSON 파싱 실패, 원본 문자열 반환:`, error);
return [extractedData];
}
}
// 추출된 데이터가 객체이고 또 다른 중첩 구조일 수 있으므로 재귀 호출
if (typeof extractedData === 'object' && !Array.isArray(extractedData)) {
console.log(`🔄 중첩된 객체 감지, 재귀 추출 시도`);
return this.extractActualData(extractedData);
}
return extractedData;
}
}
// 특별한 필드가 없는 경우, 객체의 값들 중에서 배열을 찾기
const objectValues = Object.values(responseData);
const arrayValue = objectValues.find(value => Array.isArray(value));
if (arrayValue) {
console.log(`✅ 객체 값 중 배열 발견`);
return arrayValue;
}
// 객체의 값들 중에서 객체를 찾아서 재귀 탐색
for (const value of objectValues) {
if (value && typeof value === 'object' && !Array.isArray(value)) {
console.log(`🔄 객체 값에서 재귀 탐색`);
const nestedResult = this.extractActualData(value);
if (Array.isArray(nestedResult) && nestedResult.length > 0) {
return nestedResult;
}
}
}
// 모든 시도가 실패한 경우, 원본 객체를 단일 항목 배열로 반환
console.log(`📦 원본 객체를 단일 항목으로 처리`);
return [responseData];
}
/** /**
* *
*/ */
@ -680,29 +800,56 @@ export class ImprovedButtonActionExecutor {
): Promise<void> { ): Promise<void> {
try { try {
console.log(`📥 인바운드 데이터 매핑 처리 시작`); console.log(`📥 인바운드 데이터 매핑 처리 시작`);
console.log(`📥 원본 응답 데이터:`, responseData);
const targetTable = inboundMapping.targetTable; const targetTable = inboundMapping.targetTable;
const fieldMappings = inboundMapping.fieldMappings || []; const fieldMappings = inboundMapping.fieldMappings || [];
const insertMode = inboundMapping.insertMode || 'insert'; const insertMode = inboundMapping.insertMode || 'insert';
// 응답 데이터가 배열인 경우 각 항목 처리 console.log(`📥 매핑 설정:`, {
const dataArray = Array.isArray(responseData) ? responseData : [responseData]; targetTable,
fieldMappings,
insertMode
});
// 응답 데이터에서 실제 데이터 추출 (다양한 구조 지원)
let actualData = this.extractActualData(responseData);
console.log(`📥 추출된 실제 데이터:`, actualData);
// 배열이 아닌 경우 배열로 변환
const dataArray = Array.isArray(actualData) ? actualData : [actualData];
console.log(`📥 처리할 데이터 배열:`, dataArray);
if (dataArray.length === 0) {
console.log(`⚠️ 처리할 데이터가 없습니다`);
return;
}
for (const item of dataArray) { for (const item of dataArray) {
const mappedData: Record<string, any> = {}; const mappedData: Record<string, any> = {};
console.log(`📥 개별 아이템 처리:`, item);
// 필드 매핑 적용 // 필드 매핑 적용
for (const mapping of fieldMappings) { for (const mapping of fieldMappings) {
const sourceValue = item[mapping.sourceField]; const sourceValue = item[mapping.sourceField];
if (sourceValue !== undefined) { console.log(`📥 필드 매핑: ${mapping.sourceField} -> ${mapping.targetField} = ${sourceValue}`);
if (sourceValue !== undefined && sourceValue !== null) {
mappedData[mapping.targetField] = sourceValue; mappedData[mapping.targetField] = sourceValue;
} }
} }
console.log(`📋 매핑된 데이터:`, mappedData); console.log(`📋 매핑된 데이터:`, mappedData);
// 데이터 저장 // 매핑된 데이터가 비어있지 않은 경우에만 저장
if (Object.keys(mappedData).length > 0) {
await this.saveDataToTable(targetTable, mappedData, insertMode); await this.saveDataToTable(targetTable, mappedData, insertMode);
} else {
console.log(`⚠️ 매핑된 데이터가 비어있어 저장을 건너뜁니다`);
}
} }
console.log(`✅ 인바운드 데이터 매핑 완료`); console.log(`✅ 인바운드 데이터 매핑 완료`);

View File

@ -3,10 +3,13 @@
* *
*/ */
import { DataMappingConfig } from "./DataMappingTypes";
// 외부호출 메인 설정 타입 // 외부호출 메인 설정 타입
export interface ExternalCallConfig { export interface ExternalCallConfig {
callType: "rest-api"; // 향후 "webhook", "email", "ftp" 등 확장 가능 callType: "rest-api"; // 향후 "webhook", "email", "ftp" 등 확장 가능
restApiSettings: RestApiSettings; restApiSettings: RestApiSettings;
dataMappingConfig?: DataMappingConfig; // 데이터 매핑 설정 추가
metadata?: { metadata?: {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;