diff --git a/frontend/app/(main)/admin/batch-management-new/page.tsx b/frontend/app/(main)/admin/batch-management-new/page.tsx index e9d34340..2046ed3e 100644 --- a/frontend/app/(main)/admin/batch-management-new/page.tsx +++ b/frontend/app/(main)/admin/batch-management-new/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo, memo } from "react"; import { useRouter } from "next/navigation"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -33,6 +33,31 @@ interface BatchColumnInfo { is_nullable: string; } +interface RestApiToDbMappingCardProps { + fromApiFields: string[]; + toColumns: BatchColumnInfo[]; + fromApiData: any[]; + apiFieldMappings: Record; + setApiFieldMappings: React.Dispatch< + React.SetStateAction> + >; + apiFieldPathOverrides: Record; + setApiFieldPathOverrides: React.Dispatch< + React.SetStateAction> + >; +} + +interface DbToRestApiMappingCardProps { + fromColumns: BatchColumnInfo[]; + selectedColumns: string[]; + toApiFields: string[]; + dbToApiFieldMapping: Record; + setDbToApiFieldMapping: React.Dispatch< + React.SetStateAction> + >; + setToApiBody: (body: string) => void; +} + export default function BatchManagementNewPage() { const router = useRouter(); @@ -185,24 +210,17 @@ export default function BatchManagementNewPage() { // TO 테이블 변경 핸들러 const handleToTableChange = async (tableName: string) => { - console.log("🔍 테이블 변경:", { tableName, toConnection }); setToTable(tableName); setToColumns([]); if (toConnection && tableName) { try { const connectionType = toConnection.type === 'internal' ? 'internal' : 'external'; - console.log("🔍 컬럼 조회 시작:", { connectionType, connectionId: toConnection.id, tableName }); - const result = await BatchManagementAPI.getTableColumns(connectionType, tableName, toConnection.id); - console.log("🔍 컬럼 조회 결과:", result); - if (result && result.length > 0) { setToColumns(result); - console.log("✅ 컬럼 설정 완료:", result.length, "개"); } else { setToColumns([]); - console.log("⚠️ 컬럼이 없음"); } } catch (error) { console.error("❌ 컬럼 목록 로드 오류:", error); @@ -242,7 +260,6 @@ export default function BatchManagementNewPage() { // FROM 테이블 변경 핸들러 (DB → REST API용) const handleFromTableChange = async (tableName: string) => { - console.log("🔍 FROM 테이블 변경:", { tableName, fromConnection }); setFromTable(tableName); setFromColumns([]); setSelectedColumns([]); // 선택된 컬럼도 초기화 @@ -251,17 +268,11 @@ export default function BatchManagementNewPage() { if (fromConnection && tableName) { try { const connectionType = fromConnection.type === 'internal' ? 'internal' : 'external'; - console.log("🔍 FROM 컬럼 조회 시작:", { connectionType, connectionId: fromConnection.id, tableName }); - const result = await BatchManagementAPI.getTableColumns(connectionType, tableName, fromConnection.id); - console.log("🔍 FROM 컬럼 조회 결과:", result); - if (result && result.length > 0) { setFromColumns(result); - console.log("✅ FROM 컬럼 설정 완료:", result.length, "개"); } else { setFromColumns([]); - console.log("⚠️ FROM 컬럼이 없음"); } } catch (error) { console.error("❌ FROM 컬럼 목록 로드 오류:", error); @@ -279,8 +290,6 @@ export default function BatchManagementNewPage() { } try { - console.log("🔍 TO API 미리보기 시작:", { toApiUrl, toApiKey, toEndpoint, toApiMethod }); - const result = await BatchManagementAPI.previewRestApiData( toApiUrl, toApiKey, @@ -288,8 +297,6 @@ export default function BatchManagementNewPage() { 'GET' // 미리보기는 항상 GET으로 ); - console.log("🔍 TO API 미리보기 결과:", result); - if (result.fields && result.fields.length > 0) { setToApiFields(result.fields); toast.success(`TO API 필드 ${result.fields.length}개를 조회했습니다.`); @@ -319,8 +326,6 @@ export default function BatchManagementNewPage() { } try { - console.log("REST API 데이터 미리보기 시작..."); - const result = await BatchManagementAPI.previewRestApiData( fromApiUrl, fromApiKey || "", @@ -337,30 +342,18 @@ export default function BatchManagementNewPage() { (fromApiMethod === 'POST' || fromApiMethod === 'PUT' || fromApiMethod === 'DELETE') ? fromApiBody : undefined ); - console.log("API 미리보기 결과:", result); - console.log("result.fields:", result.fields); - console.log("result.samples:", result.samples); - console.log("result.totalCount:", result.totalCount); - if (result.fields && result.fields.length > 0) { - console.log("✅ 백엔드에서 fields 제공됨:", result.fields); setFromApiFields(result.fields); setFromApiData(result.samples); - - console.log("추출된 필드:", result.fields); toast.success(`API 데이터 미리보기 완료! ${result.fields.length}개 필드, ${result.totalCount}개 레코드`); } else if (result.samples && result.samples.length > 0) { // 백엔드에서 fields를 제대로 보내지 않은 경우, 프론트엔드에서 직접 추출 - console.log("⚠️ 백엔드에서 fields가 없어서 프론트엔드에서 추출"); const extractedFields = Object.keys(result.samples[0]); - console.log("프론트엔드에서 추출한 필드:", extractedFields); - setFromApiFields(extractedFields); setFromApiData(result.samples); toast.success(`API 데이터 미리보기 완료! ${extractedFields.length}개 필드, ${result.samples.length}개 레코드`); } else { - console.log("❌ 데이터가 없음"); setFromApiFields([]); setFromApiData([]); toast.warning("API에서 데이터를 가져올 수 없습니다."); @@ -431,14 +424,6 @@ export default function BatchManagementNewPage() { }; }); - console.log("REST API 배치 설정 저장:", { - batchName, - batchType, - cronSchedule, - description, - apiMappings - }); - // 실제 API 호출 try { const result = await BatchManagementAPI.saveRestApiBatch({ @@ -527,14 +512,6 @@ export default function BatchManagementNewPage() { } } - console.log("DB → REST API 배치 설정 저장:", { - batchName, - batchType, - cronSchedule, - description, - dbMappings - }); - // 실제 API 호출 (기존 saveRestApiBatch 재사용) try { const result = await BatchManagementAPI.saveRestApiBatch({ @@ -1049,190 +1026,33 @@ export default function BatchManagementNewPage() { {/* 매핑 UI - 배치 타입별 동적 렌더링 */} {/* REST API → DB 매핑 */} - {batchType === 'restapi-to-db' && fromApiFields.length > 0 && toColumns.length > 0 && ( - - - API 필드 → DB 컬럼 매핑 - - -
- {fromApiFields.map((apiField) => ( -
- {/* API 필드 정보 */} -
-
{apiField}
-
- {fromApiData.length > 0 && fromApiData[0][apiField] !== undefined - ? `예: ${String(fromApiData[0][apiField]).substring(0, 30)}${ - String(fromApiData[0][apiField]).length > 30 ? "..." : "" - }` - : "API 필드"} -
- {/* JSON 경로 오버라이드 입력 */} -
- - setApiFieldPathOverrides((prev) => ({ - ...prev, - [apiField]: e.target.value, - })) - } - placeholder="JSON 경로 (예: response.access_token)" - className="h-7 text-xs" - /> -

- 비워두면 "{apiField}" 필드 전체를 사용하고, 입력하면 해당 경로의 값을 사용합니다. -

-
-
- - {/* 화살표 */} -
- -
- - {/* DB 컬럼 선택 */} -
- -
-
- ))} -
- - {fromApiData.length > 0 && ( -
-
샘플 데이터 (최대 3개)
-
- {fromApiData.slice(0, 3).map((item, index) => ( -
-
{JSON.stringify(item, null, 2)}
-
- ))} -
-
- )} -
-
- )} + {batchType === "restapi-to-db" && + fromApiFields.length > 0 && + toColumns.length > 0 && ( + + )} {/* DB → REST API 매핑 */} - {batchType === 'db-to-restapi' && selectedColumns.length > 0 && toApiFields.length > 0 && ( - - - DB 컬럼 → API 필드 매핑 - - DB 컬럼 값을 REST API Request Body에 매핑하세요. Request Body 템플릿에서 {`{{컬럼명}}`} 형태로 사용됩니다. - - - -
- {fromColumns.filter(column => selectedColumns.includes(column.column_name)).map((column) => ( -
- {/* DB 컬럼 정보 */} -
-
{column.column_name}
-
- 타입: {column.data_type} | NULL: {column.is_nullable ? 'Y' : 'N'} -
-
- - {/* 화살표 */} -
- - {/* API 필드 선택 드롭다운 */} -
- - - {/* 직접 입력 모드 */} - {dbToApiFieldMapping[column.column_name] === 'custom' && ( - { - setDbToApiFieldMapping(prev => ({ - ...prev, - [column.column_name]: e.target.value - })); - }} - /> - )} - -
- {dbToApiFieldMapping[column.column_name] - ? `매핑: ${column.column_name} → ${dbToApiFieldMapping[column.column_name]}` - : `기본값: ${column.column_name} (DB 컬럼명 사용)` - } -
-
- - {/* 템플릿 미리보기 */} -
-
- {`{{${column.column_name}}}`} -
-
- 실제 DB 값으로 치환됩니다 -
-
-
- ))} -
- -
-
매핑 사용 예시
-
- {`{"id": "{{id}}", "name": "{{user_name}}", "email": "{{email}}"}`} -
-
-
-
- )} + {batchType === "db-to-restapi" && + selectedColumns.length > 0 && + toApiFields.length > 0 && ( + + )} {/* TO 설정 */} @@ -1435,4 +1255,278 @@ export default function BatchManagementNewPage() { ); -} \ No newline at end of file +} + +const RestApiToDbMappingCard = memo(function RestApiToDbMappingCard({ + fromApiFields, + toColumns, + fromApiData, + apiFieldMappings, + setApiFieldMappings, + apiFieldPathOverrides, + setApiFieldPathOverrides, +}: RestApiToDbMappingCardProps) { + // 샘플 JSON 문자열은 의존 데이터가 바뀔 때만 계산 + const sampleJsonList = useMemo( + () => + fromApiData.slice(0, 3).map((item) => JSON.stringify(item, null, 2)), + [fromApiData] + ); + + const firstSample = fromApiData[0] || null; + + return ( + + + API 필드 → DB 컬럼 매핑 + + +
+ {fromApiFields.map((apiField) => ( +
+ {/* API 필드 정보 */} +
+
{apiField}
+
+ {firstSample && firstSample[apiField] !== undefined + ? `예: ${String(firstSample[apiField]).substring(0, 30)}${ + String(firstSample[apiField]).length > 30 ? "..." : "" + }` + : "API 필드"} +
+ {/* JSON 경로 오버라이드 입력 */} +
+ + setApiFieldPathOverrides((prev) => ({ + ...prev, + [apiField]: e.target.value, + })) + } + placeholder="JSON 경로 (예: response.access_token)" + className="h-7 text-xs" + /> +

+ 비워두면 "{apiField}" 필드 전체를 사용하고, 입력하면 해당 + 경로의 값을 사용합니다. +

+
+
+ + {/* 화살표 */} +
+ +
+ + {/* DB 컬럼 선택 */} +
+ +
+
+ ))} +
+ + {sampleJsonList.length > 0 && ( +
+
+ 샘플 데이터 (최대 3개) +
+
+ {sampleJsonList.map((json, index) => ( +
+
{json}
+
+ ))} +
+
+ )} +
+
+ ); +}); + +const DbToRestApiMappingCard = memo(function DbToRestApiMappingCard({ + fromColumns, + selectedColumns, + toApiFields, + dbToApiFieldMapping, + setDbToApiFieldMapping, + setToApiBody, +}: DbToRestApiMappingCardProps) { + const selectedColumnObjects = useMemo( + () => + fromColumns.filter((column) => + selectedColumns.includes(column.column_name) + ), + [fromColumns, selectedColumns] + ); + + const autoJsonPreview = useMemo(() => { + if (selectedColumns.length === 0) { + return ""; + } + const obj = selectedColumns.reduce((acc, col) => { + const apiField = dbToApiFieldMapping[col] || col; + acc[apiField] = `{{${col}}}`; + return acc; + }, {} as Record); + return JSON.stringify(obj, null, 2); + }, [selectedColumns, dbToApiFieldMapping]); + + return ( + + + DB 컬럼 → API 필드 매핑 + + DB 컬럼 값을 REST API Request Body에 매핑하세요. Request Body + 템플릿에서 {`{{컬럼명}}`} 형태로 사용됩니다. + + + +
+ {selectedColumnObjects.map((column) => ( +
+ {/* DB 컬럼 정보 */} +
+
{column.column_name}
+
+ 타입: {column.data_type} | NULL: {column.is_nullable ? "Y" : "N"} +
+
+ + {/* 화살표 */} +
+ + {/* API 필드 선택 드롭다운 */} +
+ + + {/* 직접 입력 모드 */} + {dbToApiFieldMapping[column.column_name] === "custom" && ( + { + setDbToApiFieldMapping((prev) => ({ + ...prev, + [column.column_name]: e.target.value, + })); + }} + /> + )} + +
+ {dbToApiFieldMapping[column.column_name] + ? `매핑: ${column.column_name} → ${dbToApiFieldMapping[column.column_name]}` + : `기본값: ${column.column_name} (DB 컬럼명 사용)`} +
+
+ + {/* 템플릿 미리보기 */} +
+
+ {`{{${column.column_name}}}`} +
+
+ 실제 DB 값으로 치환됩니다 +
+
+
+ ))} +
+ + {selectedColumns.length > 0 && ( +
+
+ 자동 생성된 JSON 구조 +
+
+              {autoJsonPreview}
+            
+ +
+ )} + +
+
매핑 사용 예시
+
+ {`{"id": "{{id}}", "name": "{{user_name}}", "email": "{{email}}"}`} +
+
+
+
+ ); +}); \ No newline at end of file