feat(batch): 등록 REST API 연결 자동 호출 + 응답 필드 추출
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<any[]>([]);
|
||||
const [fromApiFields, setFromApiFields] = useState<string[]>([]);
|
||||
|
||||
// 등록된 REST API 연결 (외부 커넥션 관리에서 등록한 연결 선택)
|
||||
// - 선택 시 폼(URL/엔드포인트/메서드/Body/인증) 자동 채움
|
||||
// - 자동으로 API 호출하여 응답 필드 추출 → 매핑 드롭다운 즉시 활성화
|
||||
const [registeredRestApis, setRegisteredRestApis] = useState<ExternalRestApiConnection[]>([]);
|
||||
const [selectedRestApiId, setSelectedRestApiId] = useState<string>("manual"); // "manual" = 직접 입력
|
||||
const [rawResponse, setRawResponse] = useState<unknown>(null);
|
||||
const [rawResponseLoading, setRawResponseLoading] = useState(false);
|
||||
const [rawResponseError, setRawResponseError] = useState<string>("");
|
||||
|
||||
// 통합 매핑 리스트
|
||||
const [mappingList, setMappingList] = useState<MappingItem[]>([]);
|
||||
|
||||
@@ -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<string, unknown>);
|
||||
setFromApiFields(fields);
|
||||
setFromApiData(arr as Record<string, unknown>[]);
|
||||
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<string, unknown>);
|
||||
setFromApiFields(fields);
|
||||
setFromApiData([result.response_data as Record<string, unknown>]);
|
||||
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" && (
|
||||
<div className="space-y-4">
|
||||
{/* 등록된 연결 선택 — 외부 커넥션 관리에 등록한 REST API 연결을 골라 자동 호출 */}
|
||||
<div>
|
||||
<Label htmlFor="registeredRestApi" className="flex items-center gap-1.5">
|
||||
<span>🔗 등록된 연결</span>
|
||||
{rawResponseLoading && (
|
||||
<RefreshCw className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedRestApiId}
|
||||
onValueChange={applyRegisteredRestApi}
|
||||
>
|
||||
<SelectTrigger id="registeredRestApi">
|
||||
<SelectValue placeholder="직접 입력 (등록된 연결 사용 안 함)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="manual">직접 입력 (등록된 연결 사용 안 함)</SelectItem>
|
||||
{registeredRestApis.map((c) => (
|
||||
<SelectItem key={c.id} value={String(c.id)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{c.connection_name}</span>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
[{c.auth_type || "none"}]
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{rawResponseError && (
|
||||
<p className="mt-1 text-[11px] text-destructive">{rawResponseError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* API 서버 URL */}
|
||||
<div>
|
||||
<Label htmlFor="fromApiUrl">API 서버 URL *</Label>
|
||||
|
||||
Reference in New Issue
Block a user