From 0c9e22a679d3fde30a1cb4eb6fc5e9ca5af85e81 Mon Sep 17 00:00:00 2001 From: hjjeong Date: Wed, 13 May 2026 13:54:11 +0900 Subject: [PATCH] =?UTF-8?q?feat(batch):=20=EB=93=B1=EB=A1=9D=20REST=20API?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=EC=9E=90=EB=8F=99=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=20+=20=EC=9D=91=EB=8B=B5=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vexplor_rps batch-management-new 의 applyRegisteredRestApi 핵심 흐름을 INVYONE 에 이식. 등록된 REST API 연결 선택 시: 1. 폼(URL/엔드포인트/메서드/Body/인증 토큰) 자동 채움 2. ExternalRestApiConnectionAPI.testConnectionById 로 자동 API 호출 3. 응답 안에서 배열 자동 탐색 (depth ≤ 4) 4. fromApiFields / fromApiData 채움 → 매핑 드롭다운에 필드 즉시 노출 UI: FROM 카드 최상단에 "🔗 등록된 연결" 셀렉터 추가. 로딩 중에는 셀렉터 라벨 옆에 스피너, 에러 시 destructive 톤 메시지. vexplor_rps 와 다르게 제외한 부분: - Amaranth/Wehago 회사 전용 프리셋 (AMARANTH_TARGET_PRESETS) — 6종 ERP 동기화 배치 전용 하드코딩이라 INVYONE 일반 사용자에겐 의미 없음. Phase 6 후속에서 검토할 별도 영역. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../admin/batch-management-new/page.tsx | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/frontend/app/(main)/admin/batch-management-new/page.tsx b/frontend/app/(main)/admin/batch-management-new/page.tsx index 02feea91..19e4be8b 100644 --- a/frontend/app/(main)/admin/batch-management-new/page.tsx +++ b/frontend/app/(main)/admin/batch-management-new/page.tsx @@ -18,6 +18,10 @@ import { ConditionalEditor, emptyConditionalConfig, } from "@/components/admin/batch/ConditionalEditor"; +import { + ExternalRestApiConnectionAPI, + type ExternalRestApiConnection, +} from "@/lib/api/externalRestApiConnection"; // 타입 정의 type BatchType = "db-to-restapi" | "restapi-to-db" | "restapi-to-restapi"; @@ -127,6 +131,15 @@ export default function BatchManagementNewPage() { const [fromApiData, setFromApiData] = useState([]); const [fromApiFields, setFromApiFields] = useState([]); + // 등록된 REST API 연결 (외부 커넥션 관리에서 등록한 연결 선택) + // - 선택 시 폼(URL/엔드포인트/메서드/Body/인증) 자동 채움 + // - 자동으로 API 호출하여 응답 필드 추출 → 매핑 드롭다운 즉시 활성화 + const [registeredRestApis, setRegisteredRestApis] = useState([]); + const [selectedRestApiId, setSelectedRestApiId] = useState("manual"); // "manual" = 직접 입력 + const [rawResponse, setRawResponse] = useState(null); + const [rawResponseLoading, setRawResponseLoading] = useState(false); + const [rawResponseError, setRawResponseError] = useState(""); + // 통합 매핑 리스트 const [mappingList, setMappingList] = useState([]); @@ -155,8 +168,100 @@ export default function BatchManagementNewPage() { useEffect(() => { loadConnections(); loadAuthServiceNames(); + loadRegisteredRestApis(); }, []); + // 등록된 REST API 연결 목록 로드 + const loadRegisteredRestApis = async () => { + try { + const list = await ExternalRestApiConnectionAPI.getConnections(); + setRegisteredRestApis(Array.isArray(list) ? list : []); + } catch (e) { + console.error("등록된 REST API 연결 목록 로드 실패:", e); + } + }; + + // 등록된 연결 선택 시 폼 자동 채우기 + API 호출 + 응답 필드 추출 (자동 매핑 준비). + // vexplor_rps 의 applyRegisteredRestApi 에서 회사 전용 프리셋(Amaranth) 분기는 의도적으로 제외. + const applyRegisteredRestApi = async (id: string) => { + setSelectedRestApiId(id); + if (id === "manual") return; + const conn = registeredRestApis.find((c) => String(c.id) === id); + if (!conn) return; + + // 폼 자동 채움 + setFromApiUrl(conn.base_url || ""); + setFromEndpoint(conn.endpoint_path || ""); + setFromApiMethod((conn.default_method as "GET" | "POST" | "PUT" | "DELETE") || "GET"); + setFromApiBody(conn.default_body || ""); + + // 인증 토큰 자동 채움 (직접 입력 모드) + setAuthTokenMode("direct"); + setAuthServiceName(""); + if (conn.auth_type === "bearer" && conn.auth_config?.token) { + setFromApiKey(`Bearer ${conn.auth_config.token}`); + } else if (conn.auth_type === "api-key" && conn.auth_config?.keyValue) { + setFromApiKey(conn.auth_config.keyValue); + } else { + // wehago 등 백엔드 자동 서명 타입은 토큰 입력 불필요 — 비워둠 + setFromApiKey(""); + } + + // 자동으로 API 호출 → 응답 본문 + 필드 추출하여 매핑 드롭다운 즉시 활성화 + setRawResponseError(""); + setRawResponseLoading(true); + setRawResponse(null); + try { + const result = await ExternalRestApiConnectionAPI.testConnectionById( + Number(id), + conn.endpoint_path || undefined, + ); + if (result.success) { + setRawResponse(result.response_data); + // 응답 안에서 배열을 자동 탐색 (dataArrayPath 가 아직 안 박혀도 동작) + const findArr = (o: unknown, depth = 0): unknown[] | null => { + if (Array.isArray(o)) return o; + if (depth >= 4 || typeof o !== "object" || o === null) return null; + for (const v of Object.values(o)) { + const a = findArr(v, depth + 1); + if (a) return a; + } + return null; + }; + const arr = findArr(result.response_data); + if (arr && arr.length > 0 && typeof arr[0] === "object" && arr[0] !== null) { + const fields = Object.keys(arr[0] as Record); + setFromApiFields(fields); + setFromApiData(arr as Record[]); + toast.success( + `'${conn.connection_name}' API 호출 완료 — 배열 ${arr.length}건 / 필드 ${fields.length}개 추출`, + ); + } else if ( + result.response_data && + typeof result.response_data === "object" && + !Array.isArray(result.response_data) + ) { + const fields = Object.keys(result.response_data as Record); + setFromApiFields(fields); + setFromApiData([result.response_data as Record]); + toast.success(`'${conn.connection_name}' API 호출 완료 — 필드 ${fields.length}개 추출`); + } else { + toast.success(`'${conn.connection_name}' API 호출 완료 — 응답을 받았어요`); + } + } else { + const msg = result.message || result.error_details || "API 호출 실패"; + setRawResponseError(msg); + toast.error(`'${conn.connection_name}' API 호출 실패: ${msg}`); + } + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + setRawResponseError(msg); + toast.error(`API 호출 중 오류: ${msg}`); + } finally { + setRawResponseLoading(false); + } + }; + // 인증 서비스명 목록 로드 const loadAuthServiceNames = async () => { try { @@ -699,6 +804,40 @@ export default function BatchManagementNewPage() { {/* REST API 설정 (REST API → DB) */} {batchType === "restapi-to-db" && (
+ {/* 등록된 연결 선택 — 외부 커넥션 관리에 등록한 REST API 연결을 골라 자동 호출 */} +
+ + + {rawResponseError && ( +

{rawResponseError}

+ )} +
+ {/* API 서버 URL */}