2025-09-25 11:04:16 +09:00
"use client" ;
2025-11-27 11:48:03 +09:00
import React , { useState , useEffect , useMemo , memo } from "react" ;
2025-09-26 17:29:20 +09:00
import { useRouter } from "next/navigation" ;
2026-03-19 15:07:07 +09:00
import { useTabStore } from "@/stores/tabStore" ;
2025-09-26 17:29:20 +09:00
import { Card , CardContent , CardHeader , CardTitle , CardDescription } from "@/components/ui/card" ;
2025-09-25 11:04:16 +09:00
import { Button } from "@/components/ui/button" ;
import { Input } from "@/components/ui/input" ;
import { Label } from "@/components/ui/label" ;
import { Textarea } from "@/components/ui/textarea" ;
import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "@/components/ui/select" ;
2025-12-04 17:26:29 +09:00
import { Trash2 , Plus , ArrowLeft , Save , RefreshCw , Globe , Database , Eye } from "lucide-react" ;
2025-09-25 11:04:16 +09:00
import { toast } from "sonner" ;
2026-03-03 16:04:11 +09:00
import { showErrorToast } from "@/lib/utils/toastUtils" ;
2025-09-26 17:29:20 +09:00
import { BatchManagementAPI } from "@/lib/api/batchManagement" ;
// 타입 정의
2025-12-04 17:26:29 +09:00
type BatchType = "db-to-restapi" | "restapi-to-db" | "restapi-to-restapi" ;
2025-09-26 17:29:20 +09:00
interface BatchTypeOption {
value : BatchType ;
label : string ;
description : string ;
}
interface BatchConnectionInfo {
id : number ;
name : string ;
type : string ;
}
interface BatchColumnInfo {
column_name : string ;
data_type : string ;
is_nullable : string ;
2025-09-25 11:04:16 +09:00
}
2025-12-04 17:26:29 +09:00
// 통합 매핑 아이템 타입
interface MappingItem {
id : string ;
dbColumn : string ;
sourceType : "api" | "fixed" ;
apiField : string ;
fixedValue : string ;
}
2025-11-27 11:48:03 +09:00
interface RestApiToDbMappingCardProps {
fromApiFields : string [ ] ;
toColumns : BatchColumnInfo [ ] ;
fromApiData : any [ ] ;
2025-12-04 17:26:29 +09:00
mappingList : MappingItem [ ] ;
setMappingList : React.Dispatch < React.SetStateAction < MappingItem [ ] > > ;
2025-11-27 11:48:03 +09:00
}
interface DbToRestApiMappingCardProps {
fromColumns : BatchColumnInfo [ ] ;
selectedColumns : string [ ] ;
toApiFields : string [ ] ;
dbToApiFieldMapping : Record < string , string > ;
2025-12-04 17:26:29 +09:00
setDbToApiFieldMapping : React.Dispatch < React.SetStateAction < Record < string , string > >> ;
2025-11-27 11:48:03 +09:00
setToApiBody : ( body : string ) = > void ;
}
2025-09-25 11:04:16 +09:00
export default function BatchManagementNewPage() {
2025-09-26 17:29:20 +09:00
const router = useRouter ( ) ;
2026-03-19 15:07:07 +09:00
const { openTab } = useTabStore ( ) ;
2025-12-04 17:26:29 +09:00
2025-09-25 11:04:16 +09:00
// 기본 상태
const [ batchName , setBatchName ] = useState ( "" ) ;
const [ cronSchedule , setCronSchedule ] = useState ( "0 12 * * *" ) ;
const [ description , setDescription ] = useState ( "" ) ;
2025-09-26 17:29:20 +09:00
2025-12-04 17:26:29 +09:00
// 인증 토큰 설정
const [ authTokenMode , setAuthTokenMode ] = useState < "direct" | "db" > ( "direct" ) ; // 직접입력 / DB에서 선택
const [ authServiceName , setAuthServiceName ] = useState ( "" ) ;
const [ authServiceNames , setAuthServiceNames ] = useState < string [ ] > ( [ ] ) ;
2025-09-26 17:29:20 +09:00
// 연결 정보
2025-09-25 11:04:16 +09:00
const [ connections , setConnections ] = useState < BatchConnectionInfo [ ] > ( [ ] ) ;
2025-09-26 17:29:20 +09:00
const [ toConnection , setToConnection ] = useState < BatchConnectionInfo | null > ( null ) ;
2025-09-25 11:04:16 +09:00
const [ toTables , setToTables ] = useState < string [ ] > ( [ ] ) ;
2025-09-26 17:29:20 +09:00
const [ toTable , setToTable ] = useState ( "" ) ;
2025-09-25 11:04:16 +09:00
const [ toColumns , setToColumns ] = useState < BatchColumnInfo [ ] > ( [ ] ) ;
2025-09-26 17:29:20 +09:00
// REST API 설정 (REST API → DB용)
const [ fromApiUrl , setFromApiUrl ] = useState ( "" ) ;
const [ fromApiKey , setFromApiKey ] = useState ( "" ) ;
const [ fromEndpoint , setFromEndpoint ] = useState ( "" ) ;
2025-12-04 17:26:29 +09:00
const [ fromApiMethod , setFromApiMethod ] = useState < "GET" | "POST" | "PUT" | "DELETE" > ( "GET" ) ;
2025-11-27 11:32:19 +09:00
const [ fromApiBody , setFromApiBody ] = useState ( "" ) ; // Request Body (JSON)
2025-12-04 17:26:29 +09:00
const [ dataArrayPath , setDataArrayPath ] = useState ( "" ) ; // 데이터 배열 경로 (예: response, data.items)
2025-09-29 13:48:59 +09:00
// REST API 파라미터 설정
2025-12-04 17:26:29 +09:00
const [ apiParamType , setApiParamType ] = useState < "none" | "url" | "query" > ( "none" ) ;
2025-09-29 13:48:59 +09:00
const [ apiParamName , setApiParamName ] = useState ( "" ) ; // 파라미터명 (예: userId, id)
const [ apiParamValue , setApiParamValue ] = useState ( "" ) ; // 파라미터 값 또는 템플릿
2025-12-04 17:26:29 +09:00
const [ apiParamSource , setApiParamSource ] = useState < "static" | "dynamic" > ( "static" ) ; // 정적 값 또는 동적 값
2025-09-26 17:29:20 +09:00
// DB → REST API용 상태
2025-09-25 11:04:16 +09:00
const [ fromConnection , setFromConnection ] = useState < BatchConnectionInfo | null > ( null ) ;
2025-09-26 17:29:20 +09:00
const [ fromTables , setFromTables ] = useState < string [ ] > ( [ ] ) ;
2025-09-25 11:04:16 +09:00
const [ fromTable , setFromTable ] = useState ( "" ) ;
2025-09-26 17:29:20 +09:00
const [ fromColumns , setFromColumns ] = useState < BatchColumnInfo [ ] > ( [ ] ) ;
const [ selectedColumns , setSelectedColumns ] = useState < string [ ] > ( [ ] ) ; // 선택된 컬럼들
const [ dbToApiFieldMapping , setDbToApiFieldMapping ] = useState < Record < string , string > > ( { } ) ; // DB 컬럼 → API 필드 매핑
2025-12-04 17:26:29 +09:00
2025-09-26 17:29:20 +09:00
// REST API 대상 설정 (DB → REST API용)
const [ toApiUrl , setToApiUrl ] = useState ( "" ) ;
const [ toApiKey , setToApiKey ] = useState ( "" ) ;
const [ toEndpoint , setToEndpoint ] = useState ( "" ) ;
2025-12-04 17:26:29 +09:00
const [ toApiMethod , setToApiMethod ] = useState < "POST" | "PUT" | "DELETE" > ( "POST" ) ;
const [ toApiBody , setToApiBody ] = useState < string > ( "" ) ; // Request Body 템플릿
2025-09-26 17:29:20 +09:00
const [ toApiFields , setToApiFields ] = useState < string [ ] > ( [ ] ) ; // TO API 필드 목록
const [ urlPathColumn , setUrlPathColumn ] = useState ( "" ) ; // URL 경로에 사용할 컬럼 (PUT/DELETE용)
// API 데이터 미리보기
const [ fromApiData , setFromApiData ] = useState < any [ ] > ( [ ] ) ;
const [ fromApiFields , setFromApiFields ] = useState < string [ ] > ( [ ] ) ;
2025-12-04 17:26:29 +09:00
// 통합 매핑 리스트
const [ mappingList , setMappingList ] = useState < MappingItem [ ] > ( [ ] ) ;
// INSERT/UPSERT 설정
const [ saveMode , setSaveMode ] = useState < "INSERT" | "UPSERT" > ( "INSERT" ) ;
const [ conflictKey , setConflictKey ] = useState ( "" ) ;
2025-09-26 17:29:20 +09:00
// 배치 타입 상태
2025-12-04 17:26:29 +09:00
const [ batchType , setBatchType ] = useState < BatchType > ( "restapi-to-db" ) ;
2025-09-26 17:29:20 +09:00
// 배치 타입 옵션
const batchTypeOptions : BatchTypeOption [ ] = [
{
2025-12-04 17:26:29 +09:00
value : "restapi-to-db" ,
label : "REST API → DB" ,
description : "REST API에서 데이터베이스로 데이터 수집" ,
2025-09-26 17:29:20 +09:00
} ,
{
2025-12-04 17:26:29 +09:00
value : "db-to-restapi" ,
label : "DB → REST API" ,
description : "데이터베이스에서 REST API로 데이터 전송" ,
} ,
2025-09-26 17:29:20 +09:00
] ;
2025-09-25 11:04:16 +09:00
// 초기 데이터 로드
useEffect ( ( ) = > {
loadConnections ( ) ;
2025-12-04 17:26:29 +09:00
loadAuthServiceNames ( ) ;
2025-09-25 11:04:16 +09:00
} , [ ] ) ;
2025-12-04 17:26:29 +09:00
// 인증 서비스명 목록 로드
const loadAuthServiceNames = async ( ) = > {
try {
const serviceNames = await BatchManagementAPI . getAuthServiceNames ( ) ;
setAuthServiceNames ( serviceNames ) ;
} catch ( error ) {
console . error ( "인증 서비스 목록 로드 실패:" , error ) ;
}
} ;
2025-09-26 17:29:20 +09:00
// 배치 타입 변경 시 상태 초기화
useEffect ( ( ) = > {
// 공통 초기화
2025-12-04 17:26:29 +09:00
setMappingList ( [ ] ) ;
2025-09-26 17:29:20 +09:00
// REST API → DB 관련 초기화
setToConnection ( null ) ;
setToTables ( [ ] ) ;
setToTable ( "" ) ;
setToColumns ( [ ] ) ;
setFromApiUrl ( "" ) ;
setFromApiKey ( "" ) ;
setFromEndpoint ( "" ) ;
setFromApiData ( [ ] ) ;
setFromApiFields ( [ ] ) ;
2025-12-04 17:26:29 +09:00
2025-09-26 17:29:20 +09:00
// DB → REST API 관련 초기화
setFromConnection ( null ) ;
setFromTables ( [ ] ) ;
2025-09-25 11:04:16 +09:00
setFromTable ( "" ) ;
setFromColumns ( [ ] ) ;
2025-09-26 17:29:20 +09:00
setSelectedColumns ( [ ] ) ;
setDbToApiFieldMapping ( { } ) ;
setToApiUrl ( "" ) ;
setToApiKey ( "" ) ;
setToEndpoint ( "" ) ;
setToApiBody ( "" ) ;
setToApiFields ( [ ] ) ;
} , [ batchType ] ) ;
// 연결 목록 로드
const loadConnections = async ( ) = > {
2025-09-25 11:04:16 +09:00
try {
2025-09-26 17:29:20 +09:00
const result = await BatchManagementAPI . getAvailableConnections ( ) ;
setConnections ( result || [ ] ) ;
2025-09-25 11:04:16 +09:00
} catch ( error ) {
2025-09-26 17:29:20 +09:00
console . error ( "연결 목록 로드 오류:" , error ) ;
toast . error ( "연결 목록을 불러오는데 실패했습니다." ) ;
2025-09-25 11:04:16 +09:00
}
} ;
2025-09-26 17:29:20 +09:00
// TO 연결 변경 핸들러
const handleToConnectionChange = async ( connectionValue : string ) = > {
let connection : BatchConnectionInfo | null = null ;
2025-12-04 17:26:29 +09:00
if ( connectionValue === "internal" ) {
2025-09-26 17:29:20 +09:00
// 내부 데이터베이스 선택
2025-12-04 17:26:29 +09:00
connection = connections . find ( ( conn ) = > conn . type === "internal" ) || null ;
2025-09-26 17:29:20 +09:00
} else {
// 외부 데이터베이스 선택
const connectionId = parseInt ( connectionValue ) ;
2025-12-04 17:26:29 +09:00
connection = connections . find ( ( conn ) = > conn . id === connectionId ) || null ;
2025-09-26 17:29:20 +09:00
}
2025-12-04 17:26:29 +09:00
2025-09-25 11:04:16 +09:00
setToConnection ( connection ) ;
setToTable ( "" ) ;
setToColumns ( [ ] ) ;
2025-09-26 17:29:20 +09:00
if ( connection ) {
try {
2025-12-04 17:26:29 +09:00
const connectionType = connection . type === "internal" ? "internal" : "external" ;
2025-09-26 17:29:20 +09:00
const result = await BatchManagementAPI . getTablesFromConnection ( connectionType , connection . id ) ;
2025-12-04 17:26:29 +09:00
const tableNames = Array . isArray ( result )
? result . map ( ( table : any ) = > ( typeof table === "string" ? table : table.table_name || String ( table ) ) )
2025-09-26 17:29:20 +09:00
: [ ] ;
setToTables ( tableNames ) ;
} catch ( error ) {
console . error ( "테이블 목록 로드 오류:" , error ) ;
toast . error ( "테이블 목록을 불러오는데 실패했습니다." ) ;
}
2025-09-25 11:04:16 +09:00
}
} ;
2025-09-26 17:29:20 +09:00
// TO 테이블 변경 핸들러
const handleToTableChange = async ( tableName : string ) = > {
setToTable ( tableName ) ;
setToColumns ( [ ] ) ;
if ( toConnection && tableName ) {
try {
2025-12-04 17:26:29 +09:00
const connectionType = toConnection . type === "internal" ? "internal" : "external" ;
2025-09-26 17:29:20 +09:00
const result = await BatchManagementAPI . getTableColumns ( connectionType , tableName , toConnection . id ) ;
if ( result && result . length > 0 ) {
setToColumns ( result ) ;
} else {
setToColumns ( [ ] ) ;
}
} catch ( error ) {
console . error ( "❌ 컬럼 목록 로드 오류:" , error ) ;
toast . error ( "컬럼 목록을 불러오는데 실패했습니다." ) ;
setToColumns ( [ ] ) ;
}
}
} ;
// FROM 연결 변경 핸들러 (DB → REST API용)
const handleFromConnectionChange = async ( connectionValue : string ) = > {
let connection : BatchConnectionInfo | null = null ;
2025-12-04 17:26:29 +09:00
if ( connectionValue === "internal" ) {
connection = connections . find ( ( conn ) = > conn . type === "internal" ) || null ;
2025-09-26 17:29:20 +09:00
} else {
const connectionId = parseInt ( connectionValue ) ;
2025-12-04 17:26:29 +09:00
connection = connections . find ( ( conn ) = > conn . id === connectionId ) || null ;
2025-09-26 17:29:20 +09:00
}
setFromConnection ( connection ) ;
setFromTable ( "" ) ;
setFromColumns ( [ ] ) ;
if ( connection ) {
try {
2025-12-04 17:26:29 +09:00
const connectionType = connection . type === "internal" ? "internal" : "external" ;
2025-09-26 17:29:20 +09:00
const result = await BatchManagementAPI . getTablesFromConnection ( connectionType , connection . id ) ;
const tableNames = Array . isArray ( result )
2025-12-04 17:26:29 +09:00
? result . map ( ( table : any ) = > ( typeof table === "string" ? table : table.table_name || String ( table ) ) )
2025-09-26 17:29:20 +09:00
: [ ] ;
setFromTables ( tableNames ) ;
} catch ( error ) {
console . error ( "테이블 목록 로드 오류:" , error ) ;
toast . error ( "테이블 목록을 불러오는데 실패했습니다." ) ;
}
}
} ;
// FROM 테이블 변경 핸들러 (DB → REST API용)
2025-09-25 11:04:16 +09:00
const handleFromTableChange = async ( tableName : string ) = > {
setFromTable ( tableName ) ;
2025-09-26 17:29:20 +09:00
setFromColumns ( [ ] ) ;
setSelectedColumns ( [ ] ) ; // 선택된 컬럼도 초기화
setDbToApiFieldMapping ( { } ) ; // 매핑도 초기화
if ( fromConnection && tableName ) {
try {
2025-12-04 17:26:29 +09:00
const connectionType = fromConnection . type === "internal" ? "internal" : "external" ;
2025-09-26 17:29:20 +09:00
const result = await BatchManagementAPI . getTableColumns ( connectionType , tableName , fromConnection . id ) ;
if ( result && result . length > 0 ) {
setFromColumns ( result ) ;
} else {
setFromColumns ( [ ] ) ;
}
} catch ( error ) {
console . error ( "❌ FROM 컬럼 목록 로드 오류:" , error ) ;
toast . error ( "컬럼 목록을 불러오는데 실패했습니다." ) ;
setFromColumns ( [ ] ) ;
}
}
} ;
// TO API 미리보기 (DB → REST API용)
const previewToApiData = async ( ) = > {
if ( ! toApiUrl || ! toApiKey || ! toEndpoint ) {
toast . error ( "API URL, API Key, 엔드포인트를 모두 입력해주세요." ) ;
return ;
}
2025-09-25 11:04:16 +09:00
try {
2025-09-26 17:29:20 +09:00
const result = await BatchManagementAPI . previewRestApiData (
toApiUrl ,
toApiKey ,
toEndpoint ,
2025-12-04 17:26:29 +09:00
"GET" , // 미리보기는 항상 GET으로
2025-09-25 11:04:16 +09:00
) ;
2025-09-26 17:29:20 +09:00
if ( result . fields && result . fields . length > 0 ) {
setToApiFields ( result . fields ) ;
toast . success ( ` TO API 필드 ${ result . fields . length } 개를 조회했습니다. ` ) ;
} else {
setToApiFields ( [ ] ) ;
toast . warning ( "TO API에서 필드를 찾을 수 없습니다." ) ;
}
2025-09-25 11:04:16 +09:00
} catch ( error ) {
2025-09-26 17:29:20 +09:00
console . error ( "❌ TO API 미리보기 오류:" , error ) ;
toast . error ( "TO API 미리보기에 실패했습니다." ) ;
setToApiFields ( [ ] ) ;
2025-09-25 11:04:16 +09:00
}
} ;
2025-09-26 17:29:20 +09:00
// REST API 데이터 미리보기
const previewRestApiData = async ( ) = > {
2025-11-27 11:32:19 +09:00
// API URL, 엔드포인트는 항상 필수
if ( ! fromApiUrl || ! fromEndpoint ) {
toast . error ( "API URL과 엔드포인트를 모두 입력해주세요." ) ;
return ;
}
2025-12-04 17:26:29 +09:00
// 직접 입력 모드일 때만 토큰 검증
if ( authTokenMode === "direct" && ! fromApiKey ) {
toast . error ( "인증 토큰을 입력해주세요." ) ;
return ;
}
// DB 선택 모드일 때 서비스명 검증
if ( authTokenMode === "db" && ! authServiceName ) {
toast . error ( "인증 토큰 서비스를 선택해주세요." ) ;
2025-09-26 17:29:20 +09:00
return ;
}
2025-09-25 11:04:16 +09:00
try {
2025-09-26 17:29:20 +09:00
const result = await BatchManagementAPI . previewRestApiData (
fromApiUrl ,
2025-12-04 17:26:29 +09:00
authTokenMode === "direct" ? fromApiKey : "" , // 직접 입력일 때만 API 키 전달
2025-09-26 17:29:20 +09:00
fromEndpoint ,
2025-09-29 13:48:59 +09:00
fromApiMethod ,
// 파라미터 정보 추가
2025-12-04 17:26:29 +09:00
apiParamType !== "none"
? {
paramType : apiParamType ,
paramName : apiParamName ,
paramValue : apiParamValue ,
paramSource : apiParamSource ,
}
: undefined ,
2025-11-27 11:32:19 +09:00
// Request Body 추가 (POST/PUT/DELETE)
2025-12-04 17:26:29 +09:00
fromApiMethod === "POST" || fromApiMethod === "PUT" || fromApiMethod === "DELETE" ? fromApiBody : undefined ,
// DB 선택 모드일 때 서비스명 전달
authTokenMode === "db" ? authServiceName : undefined ,
// 데이터 배열 경로 전달
dataArrayPath || undefined ,
2025-09-25 11:04:16 +09:00
) ;
2025-09-26 17:29:20 +09:00
if ( result . fields && result . fields . length > 0 ) {
setFromApiFields ( result . fields ) ;
setFromApiData ( result . samples ) ;
toast . success ( ` API 데이터 미리보기 완료! ${ result . fields . length } 개 필드, ${ result . totalCount } 개 레코드 ` ) ;
} else if ( result . samples && result . samples . length > 0 ) {
// 백엔드에서 fields를 제대로 보내지 않은 경우, 프론트엔드에서 직접 추출
const extractedFields = Object . keys ( result . samples [ 0 ] ) ;
setFromApiFields ( extractedFields ) ;
setFromApiData ( result . samples ) ;
2025-12-04 17:26:29 +09:00
2025-09-26 17:29:20 +09:00
toast . success ( ` API 데이터 미리보기 완료! ${ extractedFields . length } 개 필드, ${ result . samples . length } 개 레코드 ` ) ;
} else {
setFromApiFields ( [ ] ) ;
setFromApiData ( [ ] ) ;
toast . warning ( "API에서 데이터를 가져올 수 없습니다." ) ;
}
2025-09-25 11:04:16 +09:00
} catch ( error ) {
2025-09-26 17:29:20 +09:00
console . error ( "REST API 미리보기 오류:" , error ) ;
toast . error ( "API 데이터 미리보기에 실패했습니다." ) ;
setFromApiFields ( [ ] ) ;
setFromApiData ( [ ] ) ;
2025-09-25 11:04:16 +09:00
}
} ;
2025-09-26 17:29:20 +09:00
// 배치 설정 저장
const handleSave = async ( ) = > {
if ( ! batchName . trim ( ) ) {
toast . error ( "배치명을 입력해주세요." ) ;
2025-09-25 11:04:16 +09:00
return ;
}
2025-09-26 17:29:20 +09:00
// 배치 타입별 검증 및 저장
2025-12-04 17:26:29 +09:00
if ( batchType === "restapi-to-db" ) {
// 유효한 매핑만 필터링 (DB 컬럼이 선택되고, API 필드 또는 고정값이 있는 것)
const validMappings = mappingList . filter (
( m ) = > m . dbColumn && ( m . sourceType === "api" ? m.apiField : m.fixedValue ) ,
2025-11-27 11:32:19 +09:00
) ;
2025-12-04 17:26:29 +09:00
if ( validMappings . length === 0 ) {
toast . error ( "최소 하나의 매핑을 설정해주세요." ) ;
2025-09-26 17:29:20 +09:00
return ;
}
2025-11-27 11:32:19 +09:00
2025-12-04 17:26:29 +09:00
// UPSERT 모드일 때 conflict key 검증
if ( saveMode === "UPSERT" && ! conflictKey ) {
toast . error ( "UPSERT 모드에서는 충돌 기준 컬럼을 선택해주세요." ) ;
return ;
}
// 통합 매핑 리스트를 배치 매핑 형태로 변환
// 고정값 매핑도 동일한 from_connection_type을 사용해야 같은 그룹으로 처리됨
const apiMappings = validMappings . map ( ( mapping ) = > ( {
from_connection_type : "restapi" as const , // 고정값도 동일한 소스 타입 사용
from_table_name : fromEndpoint ,
from_column_name : mapping.sourceType === "api" ? mapping.apiField : mapping.fixedValue ,
from_api_url : fromApiUrl ,
from_api_key : authTokenMode === "direct" ? fromApiKey : "" ,
from_api_method : fromApiMethod ,
from_api_body :
fromApiMethod === "POST" || fromApiMethod === "PUT" || fromApiMethod === "DELETE" ? fromApiBody : undefined ,
from_api_param_type : apiParamType !== "none" ? apiParamType : undefined ,
from_api_param_name : apiParamType !== "none" ? apiParamName : undefined ,
from_api_param_value : apiParamType !== "none" ? apiParamValue : undefined ,
from_api_param_source : apiParamType !== "none" ? apiParamSource : undefined ,
to_connection_type : toConnection?.type === "internal" ? "internal" : "external" ,
to_connection_id : toConnection?.type === "internal" ? undefined : toConnection ? . id ,
to_table_name : toTable ,
to_column_name : mapping.dbColumn ,
mapping_type : mapping.sourceType === "fixed" ? ( "fixed" as const ) : ( "direct" as const ) ,
fixed_value : mapping.sourceType === "fixed" ? mapping.fixedValue : undefined ,
} ) ) ;
2025-09-26 17:29:20 +09:00
// 실제 API 호출
try {
const result = await BatchManagementAPI . saveRestApiBatch ( {
batchName ,
batchType ,
cronSchedule ,
description ,
2025-12-04 17:26:29 +09:00
apiMappings ,
authServiceName : authTokenMode === "db" ? authServiceName : undefined ,
dataArrayPath : dataArrayPath || undefined ,
saveMode ,
conflictKey : saveMode === "UPSERT" ? conflictKey : undefined ,
2025-09-26 17:29:20 +09:00
} ) ;
if ( result . success ) {
toast . success ( result . message || "REST API 배치 설정이 저장되었습니다." ) ;
setTimeout ( ( ) = > {
2026-03-19 15:07:07 +09:00
openTab ( { type : "admin" , title : "배치 관리" , adminUrl : "/admin/automaticMng/batchmngList" } ) ;
2025-09-26 17:29:20 +09:00
} , 1000 ) ;
} else {
toast . error ( result . message || "배치 저장에 실패했습니다." ) ;
}
} catch ( error ) {
console . error ( "배치 저장 오류:" , error ) ;
2026-03-03 16:04:11 +09:00
showErrorToast ( "배치 설정 저장에 실패했습니다" , error , {
guidance : "입력 데이터를 확인하고 다시 시도해 주세요." ,
} ) ;
2025-09-26 17:29:20 +09:00
}
2025-09-25 11:04:16 +09:00
return ;
2025-12-04 17:26:29 +09:00
} else if ( batchType === "db-to-restapi" ) {
2025-09-26 17:29:20 +09:00
// DB → REST API 배치 검증
if ( ! fromConnection || ! fromTable || selectedColumns . length === 0 ) {
toast . error ( "소스 데이터베이스, 테이블, 컬럼을 선택해주세요." ) ;
return ;
}
2025-12-04 17:26:29 +09:00
2025-09-26 17:29:20 +09:00
if ( ! toApiUrl || ! toApiKey || ! toEndpoint ) {
toast . error ( "대상 API URL, API Key, 엔드포인트를 입력해주세요." ) ;
return ;
}
2025-09-25 11:04:16 +09:00
2025-12-04 17:26:29 +09:00
if ( ( toApiMethod === "POST" || toApiMethod === "PUT" ) && ! toApiBody ) {
2025-09-26 17:29:20 +09:00
toast . error ( "POST/PUT 메서드의 경우 Request Body 템플릿을 입력해주세요." ) ;
return ;
2025-09-25 11:04:16 +09:00
}
2025-09-26 17:29:20 +09:00
// DELETE의 경우 빈 Request Body라도 템플릿 로직을 위해 "{}" 설정
let finalToApiBody = toApiBody ;
2025-12-04 17:26:29 +09:00
if ( toApiMethod === "DELETE" && ! finalToApiBody . trim ( ) ) {
finalToApiBody = "{}" ;
2025-09-26 17:29:20 +09:00
}
2025-09-25 11:04:16 +09:00
2025-09-26 17:29:20 +09:00
// DB → REST API 매핑 생성 (선택된 컬럼만)
2025-12-04 17:26:29 +09:00
const selectedColumnObjects = fromColumns . filter ( ( column ) = > selectedColumns . includes ( column . column_name ) ) ;
2025-09-26 17:29:20 +09:00
const dbMappings = selectedColumnObjects . map ( ( column , index ) = > ( {
2025-12-04 17:26:29 +09:00
from_connection_type : fromConnection.type === "internal" ? "internal" : "external" ,
from_connection_id : fromConnection.type === "internal" ? undefined : fromConnection . id ,
2025-09-26 17:29:20 +09:00
from_table_name : fromTable ,
from_column_name : column.column_name ,
from_column_type : column.data_type ,
2025-12-04 17:26:29 +09:00
to_connection_type : "restapi" as const ,
2025-09-26 17:29:20 +09:00
to_table_name : toEndpoint , // API 엔드포인트
to_column_name : dbToApiFieldMapping [ column . column_name ] || column . column_name , // 매핑된 API 필드명
to_api_url : toApiUrl ,
to_api_key : toApiKey ,
to_api_method : toApiMethod ,
to_api_body : finalToApiBody , // Request Body 템플릿
2025-12-04 17:26:29 +09:00
mapping_type : "template" as const ,
mapping_order : index + 1 ,
2025-09-26 17:29:20 +09:00
} ) ) ;
2025-09-25 11:04:16 +09:00
2025-09-26 17:29:20 +09:00
// URL 경로 파라미터 매핑 추가 (PUT/DELETE용)
2025-12-04 17:26:29 +09:00
if ( ( toApiMethod === "PUT" || toApiMethod === "DELETE" ) && urlPathColumn ) {
const urlPathColumnObject = fromColumns . find ( ( col ) = > col . column_name === urlPathColumn ) ;
2025-09-26 17:29:20 +09:00
if ( urlPathColumnObject ) {
dbMappings . push ( {
2025-12-04 17:26:29 +09:00
from_connection_type : fromConnection.type === "internal" ? "internal" : "external" ,
from_connection_id : fromConnection.type === "internal" ? undefined : fromConnection . id ,
2025-09-26 17:29:20 +09:00
from_table_name : fromTable ,
from_column_name : urlPathColumn ,
from_column_type : urlPathColumnObject.data_type ,
2025-12-04 17:26:29 +09:00
to_connection_type : "restapi" as const ,
2025-09-26 17:29:20 +09:00
to_table_name : toEndpoint ,
2025-12-04 17:26:29 +09:00
to_column_name : "URL_PATH_PARAM" , // 특별한 식별자
2025-09-26 17:29:20 +09:00
to_api_url : toApiUrl ,
to_api_key : toApiKey ,
to_api_method : toApiMethod ,
to_api_body : finalToApiBody ,
2025-12-04 17:26:29 +09:00
mapping_type : "url_path" as const ,
mapping_order : 999 , // 마지막 순서
2025-09-26 17:29:20 +09:00
} ) ;
}
}
2025-09-25 11:04:16 +09:00
2025-09-26 17:29:20 +09:00
// 실제 API 호출 (기존 saveRestApiBatch 재사용)
try {
const result = await BatchManagementAPI . saveRestApiBatch ( {
batchName ,
batchType ,
cronSchedule ,
description ,
2025-12-04 17:26:29 +09:00
apiMappings : dbMappings ,
authServiceName : authServiceName || undefined ,
2025-09-26 17:29:20 +09:00
} ) ;
if ( result . success ) {
toast . success ( result . message || "DB → REST API 배치 설정이 저장되었습니다." ) ;
setTimeout ( ( ) = > {
2026-03-19 15:07:07 +09:00
openTab ( { type : "admin" , title : "배치 관리" , adminUrl : "/admin/automaticMng/batchmngList" } ) ;
2025-09-26 17:29:20 +09:00
} , 1000 ) ;
} else {
toast . error ( result . message || "배치 저장에 실패했습니다." ) ;
}
} catch ( error ) {
console . error ( "배치 저장 오류:" , error ) ;
2026-03-03 16:04:11 +09:00
showErrorToast ( "배치 설정 저장에 실패했습니다" , error , {
guidance : "입력 데이터를 확인하고 다시 시도해 주세요." ,
} ) ;
2025-09-26 17:29:20 +09:00
}
2025-09-25 11:04:16 +09:00
return ;
}
2025-09-26 17:29:20 +09:00
toast . error ( "지원하지 않는 배치 타입입니다." ) ;
2025-09-25 11:04:16 +09:00
} ;
2026-03-19 15:07:07 +09:00
const goBack = ( ) = > openTab ( { type : "admin" , title : "배치 관리" , adminUrl : "/admin/automaticMng/batchmngList" } ) ;
2025-09-25 11:04:16 +09:00
2026-03-19 15:07:07 +09:00
return (
< div className = "mx-auto max-w-5xl space-y-6 p-4 sm:p-6" >
{ /* 헤더 */ }
< div className = "flex items-center justify-between" >
< div className = "flex items-center gap-3" >
< button onClick = { goBack } className = "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" >
< ArrowLeft className = "h-4 w-4" / >
< / button >
2025-09-26 17:29:20 +09:00
< div >
2026-03-19 15:07:07 +09:00
< h1 className = "text-lg font-semibold sm:text-xl" > 고 급 배 치 생 성 < / h1 >
< p className = "text-xs text-muted-foreground" > REST API / DB 간 데 이 터 동 기 화 배 치 를 설 정 합 니 다 < / p >
2025-09-26 17:29:20 +09:00
< / div >
2026-03-19 15:07:07 +09:00
< / div >
< / div >
2025-09-26 17:29:20 +09:00
2026-03-19 15:07:07 +09:00
{ /* 배치 타입 선택 */ }
< div className = "grid grid-cols-2 gap-3" >
{ batchTypeOptions . map ( ( option ) = > (
< button
key = { option . value }
onClick = { ( ) = > setBatchType ( option . value ) }
className = { ` group relative flex items-center gap-3 rounded-lg border p-4 text-left transition-all ${
batchType === option . value
? "border-primary bg-primary/5 ring-1 ring-primary/30"
: "border-border hover:border-muted-foreground/30 hover:bg-muted/50"
} ` }
>
< div className = { ` flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${ batchType === option . value ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground" } ` } >
{ option . value === "restapi-to-db" ? < Globe className = "h-5 w-5" / > : < Database className = "h-5 w-5" / > }
2025-09-25 11:04:16 +09:00
< / div >
2026-03-19 15:07:07 +09:00
< div className = "min-w-0" >
< div className = "text-sm font-medium" > { option . label } < / div >
< div className = "text-[11px] text-muted-foreground" > { option . description } < / div >
2025-09-25 11:04:16 +09:00
< / div >
2026-03-19 15:07:07 +09:00
{ batchType === option . value && < div className = "absolute right-3 top-3 h-2 w-2 rounded-full bg-primary" / > }
< / button >
) ) }
< / div >
2025-09-26 17:29:20 +09:00
2026-03-19 15:07:07 +09:00
{ /* 기본 정보 */ }
< div className = "space-y-4 rounded-lg border p-4 sm:p-5" >
< div className = "flex items-center gap-2 text-sm font-medium" >
< Eye className = "h-4 w-4 text-muted-foreground" / >
기 본 정 보
< / div >
< div className = "grid grid-cols-1 gap-4 sm:grid-cols-2" >
< div className = "space-y-1.5" >
< Label htmlFor = "batchName" className = "text-xs" > 배 치 명 < span className = "text-destructive" > * < / span > < / Label >
< Input id = "batchName" value = { batchName } onChange = { e = > setBatchName ( e . target . value ) } placeholder = "배치명을 입력하세요" className = "h-9 text-sm" / >
< / div >
< div className = "space-y-1.5" >
< Label htmlFor = "cronSchedule" className = "text-xs" > 실 행 스 케 줄 < span className = "text-destructive" > * < / span > < / Label >
< Input id = "cronSchedule" value = { cronSchedule } onChange = { e = > setCronSchedule ( e . target . value ) } placeholder = "0 12 * * *" className = "h-9 font-mono text-sm" / >
2025-09-25 11:04:16 +09:00
< / div >
2026-03-19 15:07:07 +09:00
< / div >
< div className = "space-y-1.5" >
< Label htmlFor = "description" className = "text-xs" > 설 명 < / Label >
< Textarea id = "description" value = { description } onChange = { e = > setDescription ( e . target . value ) } placeholder = "배치에 대한 설명을 입력하세요" rows = { 2 } className = "resize-none text-sm" / >
< / div >
< / div >
2025-09-25 11:04:16 +09:00
2025-12-04 18:34:29 +09:00
{ /* FROM/TO 설정 - 가로 배치 */ }
< div className = "grid grid-cols-1 gap-6 lg:grid-cols-2" >
{ /* FROM 설정 */ }
< Card >
< CardHeader >
< CardTitle className = "flex items-center" >
{ batchType === "restapi-to-db" ? (
< >
< Globe className = "mr-2 h-5 w-5" / >
FROM : REST API ( 소 스 )
< / >
) : (
< >
< Database className = "mr-2 h-5 w-5" / >
FROM : 데이터베이스 ( 소 스 )
< / >
) }
< / CardTitle >
< / CardHeader >
< CardContent className = "space-y-4" >
{ /* REST API 설정 (REST API → DB) */ }
{ batchType === "restapi-to-db" && (
< div className = "space-y-4" >
{ /* API 서버 URL */ }
2025-09-26 17:29:20 +09:00
< div >
< Label htmlFor = "fromApiUrl" > API 서 버 URL * < / Label >
< Input
id = "fromApiUrl"
value = { fromApiUrl }
onChange = { ( e ) = > setFromApiUrl ( e . target . value ) }
placeholder = "https://api.example.com"
/ >
< / div >
2025-12-04 18:34:29 +09:00
{ /* 인증 토큰 설정 */ }
2025-09-26 17:29:20 +09:00
< div >
2025-12-04 17:26:29 +09:00
< Label > 인 증 토 큰 ( Authorization ) < / Label >
{ /* 토큰 설정 방식 선택 */ }
< div className = "mt-2 flex gap-4" >
< label className = "flex cursor-pointer items-center gap-1.5" >
< input
type = "radio"
name = "authTokenMode"
value = "direct"
checked = { authTokenMode === "direct" }
onChange = { ( ) = > {
setAuthTokenMode ( "direct" ) ;
setAuthServiceName ( "" ) ;
} }
className = "h-3.5 w-3.5"
/ >
< span className = "text-xs" > 직 접 입 력 < / span >
< / label >
< label className = "flex cursor-pointer items-center gap-1.5" >
< input
type = "radio"
name = "authTokenMode"
value = "db"
checked = { authTokenMode === "db" }
onChange = { ( ) = > setAuthTokenMode ( "db" ) }
className = "h-3.5 w-3.5"
/ >
< span className = "text-xs" > DB에서 선 택 < / span >
< / label >
< / div >
{ /* 직접 입력 모드 */ }
{ authTokenMode === "direct" && (
< Input
id = "fromApiKey"
value = { fromApiKey }
onChange = { ( e ) = > setFromApiKey ( e . target . value ) }
placeholder = "Bearer eyJhbGciOiJIUzI1NiIs..."
className = "mt-2"
/ >
) }
{ /* DB 선택 모드 */ }
{ authTokenMode === "db" && (
< Select
value = { authServiceName || "none" }
onValueChange = { ( value ) = > setAuthServiceName ( value === "none" ? "" : value ) }
>
< SelectTrigger className = "mt-2" >
< SelectValue placeholder = "서비스명 선택" / >
< / SelectTrigger >
< SelectContent >
< SelectItem value = "none" > 선 택 안 함 < / SelectItem >
{ authServiceNames . map ( ( name ) = > (
< SelectItem key = { name } value = { name } >
{ name }
< / SelectItem >
) ) }
< / SelectContent >
< / Select >
) }
2026-03-09 14:31:59 +09:00
< p className = "mt-1 text-xs text-muted-foreground" >
2025-12-04 17:26:29 +09:00
{ authTokenMode === "direct"
? "API 호출 시 Authorization 헤더에 사용할 토큰을 입력하세요."
: "auth_tokens 테이블에서 선택한 서비스의 최신 토큰을 사용합니다." }
2025-11-27 11:32:19 +09:00
< / p >
2025-09-26 17:29:20 +09:00
< / div >
2025-09-25 11:04:16 +09:00
2025-12-04 18:34:29 +09:00
{ /* 엔드포인트 */ }
2025-09-26 17:29:20 +09:00
< div >
< Label htmlFor = "fromEndpoint" > 엔 드 포 인 트 * < / Label >
< Input
id = "fromEndpoint"
value = { fromEndpoint }
onChange = { ( e ) = > setFromEndpoint ( e . target . value ) }
placeholder = "/api/users"
/ >
< / div >
2025-12-04 18:34:29 +09:00
{ /* HTTP 메서드 */ }
2025-09-26 17:29:20 +09:00
< div >
< Label > HTTP 메 서 드 < / Label >
< Select value = { fromApiMethod } onValueChange = { ( value : any ) = > setFromApiMethod ( value ) } >
< SelectTrigger >
< SelectValue / >
< / SelectTrigger >
< SelectContent >
< SelectItem value = "GET" > GET ( 데 이 터 조 회 ) < / SelectItem >
2025-11-27 11:32:19 +09:00
< SelectItem value = "POST" > POST ( 데 이 터 조 회 / 전 송 ) < / SelectItem >
< SelectItem value = "PUT" > PUT < / SelectItem >
< SelectItem value = "DELETE" > DELETE < / SelectItem >
2025-09-26 17:29:20 +09:00
< / SelectContent >
< / Select >
< / div >
2025-12-04 18:34:29 +09:00
{ /* 데이터 배열 경로 */ }
2025-11-27 11:32:19 +09:00
< div >
2025-12-04 18:34:29 +09:00
< Label htmlFor = "dataArrayPath" > 데 이 터 배 열 경 로 < / Label >
< Input
id = "dataArrayPath"
value = { dataArrayPath }
onChange = { ( e ) = > setDataArrayPath ( e . target . value ) }
placeholder = "response (예: data.items, results)"
2025-11-27 11:32:19 +09:00
/ >
2026-03-09 14:31:59 +09:00
< p className = "mt-1 text-xs text-muted-foreground" >
2025-12-04 18:34:29 +09:00
API 응 답 에 서 배 열 데 이 터 가 있 는 경 로 를 입 력 하 세 요 . 비 워 두 면 응 답 전 체 를 사 용 합 니 다 .
< br / >
예시 : response , data . items , result . list
< / p >
2025-11-27 11:32:19 +09:00
< / div >
2025-12-04 18:34:29 +09:00
{ /* Request Body (POST/PUT/DELETE용) */ }
{ ( fromApiMethod === "POST" || fromApiMethod === "PUT" || fromApiMethod === "DELETE" ) && (
< div >
< Label htmlFor = "fromApiBody" > Request Body ( JSON ) < / Label >
< Textarea
id = "fromApiBody"
value = { fromApiBody }
onChange = { ( e ) = > setFromApiBody ( e . target . value ) }
placeholder = '{"username": "myuser", "token": "abc"}'
className = "min-h-[100px]"
rows = { 5 }
/ >
2026-03-09 14:31:59 +09:00
< p className = "mt-1 text-xs text-muted-foreground" > API 호 출 시 함 께 전 송 할 JSON 데 이 터 를 입 력 하 세 요 . < / p >
2025-12-04 18:34:29 +09:00
< / div >
) }
2025-09-29 13:48:59 +09:00
2025-12-04 18:34:29 +09:00
{ /* API 파라미터 설정 */ }
< div className = "space-y-4" >
< div className = "border-t pt-4" >
< Label className = "text-base font-medium" > API 파 라 미 터 설 정 < / Label >
2026-03-09 14:31:59 +09:00
< p className = "mt-1 text-sm text-muted-foreground" > 특 정 사 용 자 나 조 건 으 로 데 이 터 를 조 회 할 때 사 용 합 니 다 . < / p >
2025-12-04 18:34:29 +09:00
< / div >
2025-09-29 13:48:59 +09:00
2025-12-04 18:34:29 +09:00
< div >
< Label > 파 라 미 터 타 입 < / Label >
< Select value = { apiParamType } onValueChange = { ( value : any ) = > setApiParamType ( value ) } >
< SelectTrigger >
< SelectValue / >
< / SelectTrigger >
< SelectContent >
< SelectItem value = "none" > 파 라 미 터 없 음 < / SelectItem >
< SelectItem value = "url" > URL 파 라 미 터 ( / a p i / u s e r s / { " { u s e r I d } " } ) < / S e l e c t I t e m >
< SelectItem value = "query" > 쿼 리 파 라 미 터 ( / a p i / u s e r s ? u s e r I d = 1 2 3 ) < / S e l e c t I t e m >
< / SelectContent >
< / Select >
< / div >
{ apiParamType !== "none" && (
< >
{ /* 파라미터명 */ }
2025-09-29 13:48:59 +09:00
< div >
< Label htmlFor = "apiParamName" > 파 라 미 터 명 * < / Label >
< Input
id = "apiParamName"
value = { apiParamName }
onChange = { ( e ) = > setApiParamName ( e . target . value ) }
placeholder = "userId, id, email 등"
/ >
< / div >
2025-12-04 18:34:29 +09:00
{ /* 파라미터 소스 */ }
2025-09-29 13:48:59 +09:00
< div >
< Label > 파 라 미 터 소 스 < / Label >
< Select value = { apiParamSource } onValueChange = { ( value : any ) = > setApiParamSource ( value ) } >
< SelectTrigger >
< SelectValue / >
< / SelectTrigger >
< SelectContent >
< SelectItem value = "static" > 고 정 값 < / SelectItem >
< SelectItem value = "dynamic" > 동 적 값 ( 실 행 시 결 정 ) < / SelectItem >
< / SelectContent >
< / Select >
< / div >
2025-12-04 18:34:29 +09:00
< div >
< Label htmlFor = "apiParamValue" >
{ apiParamSource === "static" ? "파라미터 값" : "파라미터 템플릿" } *
< / Label >
< Input
id = "apiParamValue"
value = { apiParamValue }
onChange = { ( e ) = > setApiParamValue ( e . target . value ) }
placeholder = {
apiParamSource === "static"
? "123, john@example.com 등"
: "{{user_id}}, {{email}} 등 (실행 시 치환됨)"
}
/ >
{ apiParamSource === "dynamic" && (
2026-03-09 14:31:59 +09:00
< p className = "mt-1 text-xs text-muted-foreground" >
2025-12-04 18:34:29 +09:00
동 적 값 은 배 치 실 행 시 설 정 된 값 으 로 치 환 됩 니 다 . 예 : { "{{user_id}}" } → 실 제 사 용 자 ID
< / p >
) }
< / div >
2025-09-29 13:48:59 +09:00
2025-12-04 18:34:29 +09:00
{ apiParamType === "url" && (
2026-03-09 14:31:59 +09:00
< div className = "rounded-lg bg-primary/10 p-3" >
< div className = "text-sm font-medium text-primary" > URL 파 라 미 터 예 시 < / div >
< div className = "mt-1 text-sm text-primary" >
2025-12-04 18:34:29 +09:00
엔 드 포 인 트 : / a p i / u s e r s / { ` { $ { a p i P a r a m N a m e | | " u s e r I d " } } ` }
< / div >
2026-03-09 14:31:59 +09:00
< div className = "text-sm text-primary" > 실 제 호 출 : / a p i / u s e r s / { a p i P a r a m V a l u e | | " 1 2 3 " } < / d i v >
2025-09-29 13:48:59 +09:00
< / div >
2025-12-04 18:34:29 +09:00
) }
2025-09-29 13:48:59 +09:00
2025-12-04 18:34:29 +09:00
{ apiParamType === "query" && (
2026-03-09 14:31:59 +09:00
< div className = "rounded-lg bg-emerald-50 p-3" >
< div className = "text-sm font-medium text-emerald-800" > 쿼 리 파 라 미 터 예 시 < / div >
< div className = "mt-1 text-sm text-emerald-700" >
2025-12-04 18:34:29 +09:00
실 제 호 출 : { fromEndpoint || "/api/users" } ? { apiParamName || "userId" } =
{ apiParamValue || "123" }
< / div >
2025-09-29 13:48:59 +09:00
< / div >
2025-12-04 18:34:29 +09:00
) }
< / >
) }
< / div >
2025-09-29 13:48:59 +09:00
2025-12-04 18:34:29 +09:00
{ /* API 호출 미리보기 정보 */ }
{ fromApiUrl && fromEndpoint && (
2026-03-09 14:31:59 +09:00
< div className = "rounded-lg bg-muted p-3" >
< div className = "text-sm font-medium text-foreground" > API 호 출 정 보 < / div >
< div className = "mt-1 text-sm text-muted-foreground" >
2025-12-04 18:34:29 +09:00
{ fromApiMethod } { fromApiUrl }
{ apiParamType === "url" && apiParamName && apiParamValue
? fromEndpoint . replace ( ` { ${ apiParamName } } ` , apiParamValue ) || fromEndpoint + ` / ${ apiParamValue } `
: fromEndpoint }
{ apiParamType === "query" && apiParamName && apiParamValue
? ` ? ${ apiParamName } = ${ apiParamValue } `
: "" }
< / div >
{ ( ( authTokenMode === "direct" && fromApiKey ) || ( authTokenMode === "db" && authServiceName ) ) && (
2026-03-09 14:31:59 +09:00
< div className = "mt-1 text-xs text-muted-foreground" >
2025-12-04 17:26:29 +09:00
{ authTokenMode === "direct"
? ` Authorization: Bearer ${ fromApiKey . substring ( 0 , 15 ) } ... `
: ` Authorization: DB 토큰 ( ${ authServiceName } ) ` }
2025-09-29 13:48:59 +09:00
< / div >
2025-12-04 18:34:29 +09:00
) }
{ apiParamType !== "none" && apiParamName && apiParamValue && (
2026-03-09 14:31:59 +09:00
< div className = "mt-1 text-xs text-primary" >
2025-12-04 18:34:29 +09:00
파 라 미 터 : { apiParamName } = { apiParamValue } ( { apiParamSource === "static" ? "고정값" : "동적값" } )
< / div >
) }
2025-09-26 17:29:20 +09:00
< / div >
2025-12-04 17:26:29 +09:00
) }
2025-12-04 18:34:29 +09:00
< / div >
) }
2025-09-25 11:04:16 +09:00
2025-12-04 18:34:29 +09:00
{ /* DB 설정 (DB → REST API) */ }
{ batchType === "db-to-restapi" && (
< div className = "space-y-4" >
< div className = "grid grid-cols-1 gap-4 md:grid-cols-2" >
< div >
< Label > 소 스 연 결 * < / Label >
< Select
value = { fromConnection ? . id ? . toString ( ) || fromConnection ? . type || "" }
onValueChange = { handleFromConnectionChange }
>
< SelectTrigger >
< SelectValue placeholder = "연결을 선택하세요" / >
< / SelectTrigger >
< SelectContent >
{ connections . map ( ( connection ) = > (
< SelectItem
key = { connection . id || "internal" }
value = { connection . id ? connection . id . toString ( ) : "internal" }
>
{ connection . name } ( { connection . type === "internal" ? "내부 DB" : connection . db_type } )
2025-12-04 17:26:29 +09:00
< / SelectItem >
) ) }
2025-12-04 18:34:29 +09:00
< / SelectContent >
< / Select >
< / div >
< div >
< Label > 소 스 테 이 블 * < / Label >
< Select value = { fromTable } onValueChange = { handleFromTableChange } >
< SelectTrigger >
< SelectValue placeholder = "테이블을 선택하세요" / >
< / SelectTrigger >
< SelectContent >
{ Array . isArray ( fromTables ) &&
fromTables . map ( ( table : string ) = > (
< SelectItem key = { table } value = { table } >
{ table . toUpperCase ( ) }
< / SelectItem >
) ) }
< / SelectContent >
< / Select >
< / div >
2025-09-26 17:29:20 +09:00
< / div >
2025-12-04 18:34:29 +09:00
{ /* 컬럼 선택 UI */ }
{ fromColumns . length > 0 && (
< div >
< Label > 컬 럼 선 택 ( 선 택 된 컬 럼 이 API로 전 송 됩 니 다 ) < / Label >
< div className = "mt-2 grid max-h-60 grid-cols-2 gap-2 overflow-y-auto rounded-lg border p-3 md:grid-cols-3 lg:grid-cols-4" >
{ fromColumns . map ( ( column ) = > (
< div key = { column . column_name } className = "flex items-center space-x-2" >
< input
type = "checkbox"
id = { ` col- ${ column . column_name } ` }
checked = { selectedColumns . includes ( column . column_name ) }
onChange = { ( e ) = > {
if ( e . target . checked ) {
setSelectedColumns ( [ . . . selectedColumns , column . column_name ] ) ;
} else {
setSelectedColumns ( selectedColumns . filter ( ( col ) = > col !== column . column_name ) ) ;
}
} }
2026-03-09 14:31:59 +09:00
className = "rounded border-input"
2025-12-04 18:34:29 +09:00
/ >
< label
htmlFor = { ` col- ${ column . column_name } ` }
className = "flex-1 cursor-pointer text-sm"
title = { ` 타입: ${ column . data_type } | NULL: ${ column . is_nullable ? "Y" : "N" } ` }
>
{ column . column_name }
< / label >
< / div >
) ) }
< / div >
2025-12-04 17:26:29 +09:00
2025-12-04 18:34:29 +09:00
{ /* 선택된 컬럼 개수 표시 */ }
2026-03-09 14:31:59 +09:00
< div className = "mt-2 text-xs text-muted-foreground" >
2025-12-04 18:34:29 +09:00
선 택 된 컬 럼 : { selectedColumns . length } 개 / 전 체 : { fromColumns . length } 개
< / div >
2025-09-26 17:29:20 +09:00
2025-12-04 18:34:29 +09:00
{ /* 빠른 매핑 버튼들 */ }
{ selectedColumns . length > 0 && toApiFields . length > 0 && (
2026-03-09 14:31:59 +09:00
< div className = "mt-3 rounded-lg border border-emerald-200 bg-emerald-50 p-3" >
< div className = "mb-2 text-sm font-medium text-emerald-800" > 빠 른 매 핑 < / div >
2025-12-04 18:34:29 +09:00
< div className = "flex flex-wrap gap-2" >
< button
type = "button"
onClick = { ( ) = > {
const mapping : Record < string , string > = { } ;
selectedColumns . forEach ( ( col ) = > {
// 스마트 매핑 로직
const matchingApiField = toApiFields . find ( ( apiField ) = > {
const colLower = col . toLowerCase ( ) ;
const apiLower = apiField . toLowerCase ( ) ;
// 정확한 매치
if ( colLower === apiLower ) return true ;
// 언더스코어 무시 매치
if ( colLower . replace ( /_/g , "" ) === apiLower . replace ( /_/g , "" ) ) return true ;
// 의미적 매핑
if (
( colLower . includes ( "created" ) || colLower . includes ( "reg" ) ) &&
( apiLower . includes ( "date" ) || apiLower . includes ( "time" ) )
)
return true ;
if (
( colLower . includes ( "updated" ) || colLower . includes ( "mod" ) ) &&
( apiLower . includes ( "date" ) || apiLower . includes ( "time" ) )
)
return true ;
if ( colLower . includes ( "id" ) && apiLower . includes ( "id" ) ) return true ;
if ( colLower . includes ( "name" ) && apiLower . includes ( "name" ) ) return true ;
if ( colLower . includes ( "code" ) && apiLower . includes ( "code" ) ) return true ;
return false ;
} ) ;
if ( matchingApiField ) {
mapping [ col ] = matchingApiField ;
}
2025-09-26 17:29:20 +09:00
} ) ;
2025-12-04 18:34:29 +09:00
setDbToApiFieldMapping ( mapping ) ;
toast . success ( ` ${ Object . keys ( mapping ) . length } 개 컬럼이 자동 매핑되었습니다. ` ) ;
} }
2026-03-09 14:31:59 +09:00
className = "rounded bg-primary px-3 py-1 text-xs text-white hover:bg-primary/90"
2025-12-04 18:34:29 +09:00
>
스 마 트 자 동 매 핑
< / button >
< button
type = "button"
onClick = { ( ) = > {
setDbToApiFieldMapping ( { } ) ;
toast . success ( "매핑이 초기화되었습니다." ) ;
} }
2026-03-09 14:31:59 +09:00
className = "rounded bg-foreground/80 px-3 py-1 text-xs text-white hover:bg-foreground/90"
2025-12-04 18:34:29 +09:00
>
매 핑 초 기 화
< / button >
< / div >
2025-09-26 17:29:20 +09:00
< / div >
2025-12-04 18:34:29 +09:00
) }
2025-09-26 17:29:20 +09:00
2025-12-04 18:34:29 +09:00
{ /* 자동 생성된 JSON 미리보기 */ }
{ selectedColumns . length > 0 && (
2026-03-09 14:31:59 +09:00
< div className = "mt-3 rounded-lg border border-primary/20 bg-primary/10 p-3" >
< div className = "mb-2 text-sm font-medium text-primary" > 자 동 생 성 된 JSON 구 조 < / div >
< pre className = "overflow-x-auto font-mono text-xs text-primary" >
2025-12-04 18:34:29 +09:00
{ JSON . stringify (
2025-12-04 17:26:29 +09:00
selectedColumns . reduce (
( obj , col ) = > {
const apiField = dbToApiFieldMapping [ col ] || col ; // 매핑된 API 필드명 또는 원본 컬럼명
obj [ apiField ] = ` {{ ${ col } }} ` ;
return obj ;
} ,
{ } as Record < string , string > ,
) ,
2025-09-26 17:29:20 +09:00
null ,
2025-12-04 17:26:29 +09:00
2 ,
2025-12-04 18:34:29 +09:00
) }
< / pre >
< button
type = "button"
onClick = { ( ) = > {
const autoJson = JSON . stringify (
selectedColumns . reduce (
( obj , col ) = > {
const apiField = dbToApiFieldMapping [ col ] || col ; // 매핑된 API 필드명 또는 원본 컬럼명
obj [ apiField ] = ` {{ ${ col } }} ` ;
return obj ;
} ,
{ } as Record < string , string > ,
) ,
null ,
2 ,
) ;
setToApiBody ( autoJson ) ;
toast . success ( "Request Body에 자동 생성된 JSON이 적용되었습니다." ) ;
} }
2026-03-09 14:31:59 +09:00
className = "mt-2 rounded bg-primary px-3 py-1 text-xs text-white hover:bg-primary/90"
2025-12-04 18:34:29 +09:00
>
Request Body에 적 용
< / button >
< / div >
) }
< / div >
) }
< / div >
) }
< / CardContent >
< / Card >
{ /* TO 설정 */ }
< Card >
< CardHeader >
< CardTitle className = "flex items-center" >
{ batchType === "restapi-to-db" ? (
< >
< Database className = "mr-2 h-5 w-5" / >
TO : 데이터베이스 ( 대 상 )
< / >
) : (
< >
< Globe className = "mr-2 h-5 w-5" / >
TO : REST API ( 대 상 )
< / >
2025-09-26 17:29:20 +09:00
) }
2025-12-04 18:34:29 +09:00
< / CardTitle >
< / CardHeader >
< CardContent className = "space-y-4" >
{ /* DB 설정 (REST API → DB) - 단계적 활성화 */ }
{ batchType === "restapi-to-db" && (
< div className = "space-y-4" >
{ /* 1. 커넥션 선택 - 항상 활성화 */ }
< div >
< Label > 데 이 터 베 이 스 커 넥 션 선 택 * < / Label >
< Select onValueChange = { handleToConnectionChange } >
< SelectTrigger >
< SelectValue placeholder = "커넥션을 선택하세요" / >
< / SelectTrigger >
< SelectContent >
{ connections . map ( ( connection , index ) = > (
< SelectItem
key = { connection . id || ` internal- ${ index } ` }
value = { connection . id ? connection . id . toString ( ) : "internal" }
>
{ connection . name }
< / SelectItem >
) ) }
< / SelectContent >
< / Select >
< / div >
2025-09-25 11:04:16 +09:00
2025-12-04 18:34:29 +09:00
{ /* 2. 테이블 선택 - 커넥션 선택 후 활성화 */ }
< div className = { toTables . length === 0 ? "pointer-events-none opacity-50" : "" } >
< Label > 테 이 블 선 택 * < / Label >
< Select onValueChange = { handleToTableChange } disabled = { toTables . length === 0 } >
< SelectTrigger >
< SelectValue
placeholder = { toTables . length === 0 ? "먼저 커넥션을 선택하세요" : "테이블을 선택하세요" }
/ >
< / SelectTrigger >
< SelectContent >
{ toTables . map ( ( table : string ) = > (
< SelectItem key = { table } value = { table } >
{ table . toUpperCase ( ) }
< / SelectItem >
) ) }
< / SelectContent >
< / Select >
< / div >
2025-09-26 17:29:20 +09:00
2025-12-04 18:34:29 +09:00
{ /* 3. 저장 모드 - 테이블 선택 후 활성화 */ }
< div className = { ! toTable ? "pointer-events-none opacity-50" : "" } >
< Label > 저 장 모 드 < / Label >
< div className = "mt-2 flex gap-4" >
< label className = "flex cursor-pointer items-center gap-2" >
< input
type = "radio"
name = "saveMode"
value = "INSERT"
checked = { saveMode === "INSERT" }
onChange = { ( ) = > {
setSaveMode ( "INSERT" ) ;
setConflictKey ( "" ) ;
} }
className = "h-4 w-4"
disabled = { ! toTable }
/ >
< span className = "text-sm" > INSERT ( 새 데 이 터 추 가 ) < / span >
< / label >
< label className = "flex cursor-pointer items-center gap-2" >
< input
type = "radio"
name = "saveMode"
value = "UPSERT"
checked = { saveMode === "UPSERT" }
onChange = { ( ) = > setSaveMode ( "UPSERT" ) }
className = "h-4 w-4"
disabled = { ! toTable }
/ >
< span className = "text-sm" > UPSERT ( 있 으 면 업 데 이 트 , 없 으 면 추 가 ) < / span >
< / label >
< / div >
< / div >
2025-09-26 17:29:20 +09:00
2025-12-04 18:34:29 +09:00
{ /* 4. 충돌 기준 컬럼 - UPSERT 선택 후 활성화 */ }
< div className = { saveMode !== "UPSERT" ? "pointer-events-none opacity-50" : "" } >
< Label htmlFor = "conflictKey" > 충 돌 기 준 컬 럼 ( Conflict Key ) * < / Label >
< Select value = { conflictKey } onValueChange = { setConflictKey } disabled = { saveMode !== "UPSERT" } >
< SelectTrigger className = "mt-2" >
< SelectValue
placeholder = {
saveMode !== "UPSERT" ? "UPSERT 모드를 선택하세요" : "UPSERT 기준 컬럼을 선택하세요"
}
/ >
< / SelectTrigger >
< SelectContent >
{ toColumns . map ( ( col ) = > (
< SelectItem key = { col . column_name } value = { col . column_name } >
{ col . column_name }
< / SelectItem >
) ) }
< / SelectContent >
< / Select >
2026-03-09 14:31:59 +09:00
< p className = "mt-1 text-xs text-muted-foreground" >
2025-12-04 18:34:29 +09:00
이 컬 럼 값 이 같 으 면 UPDATE , 없 으 면 INSERT 합 니 다 . ( 예 : device_serial_number )
< / p >
< / div >
2025-09-26 17:29:20 +09:00
< / div >
2025-12-04 18:34:29 +09:00
) }
2025-09-26 17:29:20 +09:00
2025-12-04 18:34:29 +09:00
{ /* REST API 설정 (DB → REST API) */ }
{ batchType === "db-to-restapi" && (
< div className = "space-y-4" >
{ /* API 서버 URL */ }
2025-09-26 17:29:20 +09:00
< div >
< Label htmlFor = "toApiUrl" > API 서 버 URL * < / Label >
< Input
id = "toApiUrl"
value = { toApiUrl }
onChange = { ( e ) = > setToApiUrl ( e . target . value ) }
placeholder = "https://api.example.com"
/ >
< / div >
2025-12-04 18:34:29 +09:00
{ /* API 키 */ }
2025-09-26 17:29:20 +09:00
< div >
< Label htmlFor = "toApiKey" > API 키 * < / Label >
< Input
id = "toApiKey"
value = { toApiKey }
onChange = { ( e ) = > setToApiKey ( e . target . value ) }
placeholder = "ak_your_api_key_here"
/ >
< / div >
2025-12-04 18:34:29 +09:00
{ /* 엔드포인트 */ }
2025-09-26 17:29:20 +09:00
< div >
< Label htmlFor = "toEndpoint" > 엔 드 포 인 트 * < / Label >
< Input
id = "toEndpoint"
value = { toEndpoint }
onChange = { ( e ) = > setToEndpoint ( e . target . value ) }
placeholder = "/api/users"
/ >
2025-12-04 17:26:29 +09:00
{ ( toApiMethod === "PUT" || toApiMethod === "DELETE" ) && (
2026-03-09 14:31:59 +09:00
< p className = "mt-1 text-xs text-muted-foreground" >
2025-12-04 18:34:29 +09:00
실 제 URL : { toEndpoint } / { urlPathColumn ? ` { ${ urlPathColumn } } ` : "{ID}" }
2025-09-26 17:29:20 +09:00
< / p >
) }
< / div >
2025-12-04 18:34:29 +09:00
{ /* HTTP 메서드 */ }
2025-09-26 17:29:20 +09:00
< div >
< Label > HTTP 메 서 드 < / Label >
< Select value = { toApiMethod } onValueChange = { ( value : any ) = > setToApiMethod ( value ) } >
< SelectTrigger >
< SelectValue / >
< / SelectTrigger >
< SelectContent >
< SelectItem value = "POST" > POST ( 데 이 터 생 성 ) < / SelectItem >
< SelectItem value = "PUT" > PUT ( 데 이 터 업 데 이 트 ) < / SelectItem >
< SelectItem value = "DELETE" > DELETE ( 데 이 터 삭 제 ) < / SelectItem >
< / SelectContent >
< / Select >
< / div >
2025-12-04 18:34:29 +09:00
{ /* URL 경로 파라미터 설정 (PUT/DELETE용) */ }
{ ( toApiMethod === "PUT" || toApiMethod === "DELETE" ) && (
< div >
< Label > URL 경 로 파 라 미 터 컬 럼 * < / Label >
< Select value = { urlPathColumn } onValueChange = { setUrlPathColumn } >
< SelectTrigger >
< SelectValue placeholder = "URL 경로에 사용할 컬럼을 선택하세요" / >
< / SelectTrigger >
< SelectContent >
{ selectedColumns . map ( ( column ) = > (
< SelectItem key = { column } value = { column } >
{ column } ( 예 : / a p i / u s e r s / { ` { $ { c o l u m n } } ` } )
< / SelectItem >
) ) }
< / SelectContent >
< / Select >
2026-03-09 14:31:59 +09:00
< p className = "mt-1 text-xs text-muted-foreground" >
2025-12-04 18:34:29 +09:00
PUT / DELETE 요 청 시 URL 경 로 에 포 함 될 컬 럼 을 선 택 하 세 요 . ( 예 : USER_ID → / api / users / user123 )
< / p >
< / div >
) }
2025-09-26 17:29:20 +09:00
2025-12-04 18:34:29 +09:00
{ /* TO API 미리보기 버튼 */ }
< div className = "flex justify-center" >
< button
type = "button"
onClick = { previewToApiData }
2026-03-09 14:31:59 +09:00
className = "flex items-center space-x-2 rounded-md bg-emerald-600 px-4 py-2 text-white hover:bg-green-700"
2025-12-04 18:34:29 +09:00
>
< Eye className = "h-4 w-4" / >
< span > API 필 드 미 리 보 기 < / span >
< / button >
< / div >
2025-09-26 17:29:20 +09:00
2025-12-04 18:34:29 +09:00
{ /* TO API 필드 표시 */ }
{ toApiFields . length > 0 && (
2026-03-09 14:31:59 +09:00
< div className = "rounded-lg border border-emerald-200 bg-emerald-50 p-3" >
< div className = "mb-2 text-sm font-medium text-emerald-800" >
2025-12-04 18:34:29 +09:00
API 필 드 목 록 ( { toApiFields . length } 개 )
< / div >
< div className = "flex flex-wrap gap-2" >
{ toApiFields . map ( ( field ) = > (
2026-03-09 14:31:59 +09:00
< span key = { field } className = "rounded bg-emerald-100 px-2 py-1 text-xs text-emerald-700" >
2025-12-04 18:34:29 +09:00
{ field }
< / span >
) ) }
< / div >
2025-09-26 17:29:20 +09:00
< / div >
2025-12-04 18:34:29 +09:00
) }
2025-09-26 17:29:20 +09:00
2025-12-04 18:34:29 +09:00
{ /* Request Body 템플릿 */ }
{ ( toApiMethod === "POST" || toApiMethod === "PUT" ) && (
< div >
< Label htmlFor = "toApiBody" > Request Body 템 플 릿 ( JSON ) < / Label >
< textarea
id = "toApiBody"
value = { toApiBody }
onChange = { ( e ) = > setToApiBody ( e . target . value ) }
placeholder = '{"id": "{{id}}", "name": "{{name}}", "email": "{{email}}"}'
className = "h-24 w-full rounded-md border p-2 font-mono text-sm"
/ >
2026-03-09 14:31:59 +09:00
< div className = "mt-1 text-xs text-muted-foreground" >
2025-12-04 18:34:29 +09:00
DB 컬 럼 값 을 { "{{컬럼명}}" } 형 태 로 매 핑 하 세 요 . 예 : { "{{user_id}}, {{user_name}}" }
< / div >
2025-09-26 17:29:20 +09:00
< / div >
2025-12-04 18:34:29 +09:00
) }
2025-09-25 11:04:16 +09:00
2025-12-04 18:34:29 +09:00
{ /* API 호출 정보 */ }
{ toApiUrl && toApiKey && toEndpoint && (
2026-03-09 14:31:59 +09:00
< div className = "rounded-lg bg-muted p-3" >
< div className = "text-sm font-medium text-foreground" > API 호 출 정 보 < / div >
< div className = "mt-1 text-sm text-muted-foreground" >
2025-12-04 18:34:29 +09:00
{ toApiMethod } { toApiUrl }
{ toEndpoint }
< / div >
2026-03-09 14:31:59 +09:00
< div className = "mt-1 text-xs text-muted-foreground" > Headers : X - API - Key : { toApiKey . substring ( 0 , 10 ) } . . . < / div >
2025-12-04 18:34:29 +09:00
{ toApiBody && (
2026-03-09 14:31:59 +09:00
< div className = "mt-1 text-xs text-primary" > Body : { toApiBody . substring ( 0 , 50 ) } . . . < / div >
2025-12-04 18:34:29 +09:00
) }
2025-09-26 17:29:20 +09:00
< / div >
2025-12-04 18:34:29 +09:00
) }
< / div >
) }
< / CardContent >
< / Card >
< / div >
2025-09-26 17:29:20 +09:00
2025-12-04 18:34:29 +09:00
{ /* API 데이터 미리보기 버튼 - FROM/TO 섹션 아래 */ }
{ batchType === "restapi-to-db" && (
< div className = "flex justify-center" >
< Button
onClick = { previewRestApiData }
variant = "outline"
disabled = { ! fromApiUrl || ! fromEndpoint || ! toTable }
className = "gap-2"
>
< RefreshCw className = "h-4 w-4" / >
데 이 터 불 러 오 고 매 핑 하 기
< / Button >
{ ( ! fromApiUrl || ! fromEndpoint || ! toTable ) && (
2026-03-09 14:31:59 +09:00
< p className = "ml-4 flex items-center text-xs text-muted-foreground" >
2025-12-04 18:34:29 +09:00
FROM 섹 션 과 TO 섹 션 의 필 수 값 을 모 두 입 력 해 야 합 니 다 .
< / p >
2025-09-25 11:04:16 +09:00
) }
2025-12-04 18:34:29 +09:00
< / div >
) }
2025-09-25 11:04:16 +09:00
2025-12-04 18:34:29 +09:00
{ /* 매핑 UI - 배치 타입별 동적 렌더링 */ }
{ /* REST API → DB 매핑 */ }
{ batchType === "restapi-to-db" && fromApiFields . length > 0 && toColumns . length > 0 && (
< RestApiToDbMappingCard
fromApiFields = { fromApiFields }
toColumns = { toColumns }
fromApiData = { fromApiData }
mappingList = { mappingList }
setMappingList = { setMappingList }
/ >
) }
{ /* DB → REST API 매핑 */ }
{ batchType === "db-to-restapi" && selectedColumns . length > 0 && toApiFields . length > 0 && (
< DbToRestApiMappingCard
fromColumns = { fromColumns }
selectedColumns = { selectedColumns }
toApiFields = { toApiFields }
dbToApiFieldMapping = { dbToApiFieldMapping }
setDbToApiFieldMapping = { setDbToApiFieldMapping }
setToApiBody = { setToApiBody }
/ >
) }
{ /* 하단 액션 버튼 */ }
2026-03-19 15:07:07 +09:00
< div className = "flex items-center justify-end gap-2 border-t pt-4" >
< Button onClick = { goBack } variant = "outline" size = "sm" className = "h-8 gap-1 text-xs" > 취 소 < / Button >
< Button onClick = { loadConnections } variant = "outline" size = "sm" className = "h-8 gap-1 text-xs" >
< RefreshCw className = "h-3.5 w-3.5" / >
2025-12-04 18:34:29 +09:00
새 로 고 침
< / Button >
2026-03-19 15:07:07 +09:00
< Button onClick = { handleSave } size = "sm" className = "h-8 gap-1 text-xs" >
< Save className = "h-3.5 w-3.5" / >
2025-12-04 18:34:29 +09:00
저 장
< / Button >
< / div >
2025-09-25 11:04:16 +09:00
< / div >
) ;
2025-11-27 11:48:03 +09:00
}
const RestApiToDbMappingCard = memo ( function RestApiToDbMappingCard ( {
fromApiFields ,
toColumns ,
fromApiData ,
2025-12-04 17:26:29 +09:00
mappingList ,
setMappingList ,
2025-11-27 11:48:03 +09:00
} : RestApiToDbMappingCardProps ) {
2025-12-04 17:26:29 +09:00
// 샘플 JSON 문자열
2025-11-27 11:48:03 +09:00
const sampleJsonList = useMemo (
2025-12-04 17:26:29 +09:00
( ) = > fromApiData . slice ( 0 , 3 ) . map ( ( item ) = > JSON . stringify ( item , null , 2 ) ) ,
[ fromApiData ] ,
2025-11-27 11:48:03 +09:00
) ;
const firstSample = fromApiData [ 0 ] || null ;
2025-12-04 17:26:29 +09:00
// 이미 매핑된 DB 컬럼들
const mappedDbColumns = useMemo ( ( ) = > mappingList . map ( ( m ) = > m . dbColumn ) . filter ( Boolean ) , [ mappingList ] ) ;
// 매핑 추가
const addMapping = ( ) = > {
const newId = ` mapping- ${ Date . now ( ) } ` ;
setMappingList ( ( prev ) = > [
. . . prev ,
{
id : newId ,
dbColumn : "" ,
sourceType : "api" ,
apiField : "" ,
fixedValue : "" ,
} ,
] ) ;
} ;
// 매핑 삭제
const removeMapping = ( id : string ) = > {
setMappingList ( ( prev ) = > prev . filter ( ( m ) = > m . id !== id ) ) ;
} ;
// 매핑 업데이트
const updateMapping = ( id : string , updates : Partial < MappingItem > ) = > {
setMappingList ( ( prev ) = > prev . map ( ( m ) = > ( m . id === id ? { . . . m , . . . updates } : m ) ) ) ;
} ;
2025-11-27 11:48:03 +09:00
return (
< Card >
< CardHeader >
2025-12-04 18:34:29 +09:00
< CardTitle > 컬 럼 매 핑 설 정 < / CardTitle >
2025-12-04 17:26:29 +09:00
< CardDescription > DB 컬 럼 에 API 필 드 또 는 고 정 값 을 매 핑 합 니 다 . < / CardDescription >
2025-11-27 11:48:03 +09:00
< / CardHeader >
< CardContent >
2025-12-04 18:34:29 +09:00
< div className = "grid grid-cols-1 gap-6 lg:grid-cols-2" >
{ /* 왼쪽: 샘플 데이터 */ }
< div className = "flex flex-col" >
< div className = "mb-3 flex h-8 items-center" >
< h4 className = "text-sm font-semibold" > 샘 플 데 이 터 ( 최 대 3 개 ) < / h4 >
< / div >
{ sampleJsonList . length > 0 ? (
< div className = "bg-muted/30 h-[360px] overflow-y-auto rounded-lg border p-3" >
< div className = "space-y-2" >
{ sampleJsonList . map ( ( json , index ) = > (
< div key = { index } className = "bg-background rounded border p-2" >
< pre className = "font-mono text-xs whitespace-pre-wrap" > { json } < / pre >
< / div >
) ) }
2025-11-27 11:48:03 +09:00
< / div >
2025-12-04 18:34:29 +09:00
< / div >
) : (
< div className = "flex h-[360px] items-center justify-center rounded-lg border border-dashed" >
< p className = "text-muted-foreground text-sm" >
API 데 이 터 미 리 보 기 를 실 행 하 면 샘 플 데 이 터 가 표 시 됩 니 다 .
< / p >
< / div >
) }
< / div >
2025-12-04 17:26:29 +09:00
2025-12-04 18:34:29 +09:00
{ /* 오른쪽: 매핑 영역 (스크롤) */ }
< div className = "flex flex-col" >
< div className = "mb-3 flex h-8 items-center justify-between" >
< h4 className = "text-sm font-semibold" > 매 핑 설 정 < / h4 >
< Button variant = "outline" size = "sm" onClick = { addMapping } className = "h-8 gap-1" >
< Plus className = "h-4 w-4" / >
매 핑 추 가
< / Button >
< / div >
2025-12-04 17:26:29 +09:00
2025-12-04 18:34:29 +09:00
{ mappingList . length === 0 ? (
< div className = "flex h-[360px] flex-col items-center justify-center rounded-lg border border-dashed text-center" >
< p className = "text-muted-foreground text-sm" > 매 핑 이 없 습 니 다 . < / p >
< Button variant = "link" onClick = { addMapping } className = "mt-2" >
매 핑 추 가 하 기
< / Button >
< / div >
) : (
< div className = "bg-muted/30 h-[360px] space-y-3 overflow-y-auto rounded-lg border p-3" >
{ mappingList . map ( ( mapping , index ) = > (
< div key = { mapping . id } className = "bg-background flex items-center gap-2 rounded-lg border p-3" >
{ /* 순서 표시 */ }
< div className = "bg-primary/10 text-primary flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-medium" >
{ index + 1 }
< / div >
2025-11-27 11:48:03 +09:00
2025-12-04 18:34:29 +09:00
{ /* DB 컬럼 선택 (좌측 - TO) */ }
< div className = "w-36 shrink-0" >
< Select
value = { mapping . dbColumn || "none" }
onValueChange = { ( value ) = >
updateMapping ( mapping . id , { dbColumn : value === "none" ? "" : value } )
}
>
< SelectTrigger className = "h-9" >
< SelectValue placeholder = "DB 컬럼" / >
< / SelectTrigger >
< SelectContent >
< SelectItem value = "none" > 선 택 안 함 < / SelectItem >
{ toColumns . map ( ( col ) = > {
const isUsed =
mappedDbColumns . includes ( col . column_name ) && mapping . dbColumn !== col . column_name ;
return (
< SelectItem key = { col . column_name } value = { col . column_name } disabled = { isUsed } >
< div className = "flex items-center gap-2" >
< span className = { isUsed ? "text-muted-foreground" : "" } > { col . column_name } < / span >
< span className = "text-muted-foreground text-xs" > ( { col . data_type } ) < / span >
< / div >
< / SelectItem >
) ;
} ) }
< / SelectContent >
< / Select >
< / div >
2025-11-27 11:48:03 +09:00
2025-12-04 18:34:29 +09:00
{ /* 화살표 */ }
< ArrowLeft className = "text-muted-foreground h-4 w-4 shrink-0" / >
{ /* 소스 타입 선택 */ }
< div className = "w-24 shrink-0" >
< Select
value = { mapping . sourceType }
onValueChange = { ( value : "api" | "fixed" ) = >
updateMapping ( mapping . id , {
sourceType : value ,
apiField : value === "fixed" ? "" : mapping . apiField ,
fixedValue : value === "api" ? "" : mapping . fixedValue ,
} )
}
>
< SelectTrigger className = "h-9" >
< SelectValue / >
< / SelectTrigger >
< SelectContent >
< SelectItem value = "api" > API 필 드 < / SelectItem >
< SelectItem value = "fixed" > 고 정 값 < / SelectItem >
< / SelectContent >
< / Select >
< / div >
{ /* API 필드 선택 또는 고정값 입력 (우측 - FROM) */ }
< div className = "min-w-0 flex-1" >
{ mapping . sourceType === "api" ? (
< Select
value = { mapping . apiField || "none" }
onValueChange = { ( value ) = >
updateMapping ( mapping . id , { apiField : value === "none" ? "" : value } )
}
>
< SelectTrigger className = "h-9" >
< SelectValue placeholder = "API 필드" / >
< / SelectTrigger >
< SelectContent >
< SelectItem value = "none" > 선 택 안 함 < / SelectItem >
{ fromApiFields . map ( ( field ) = > (
< SelectItem key = { field } value = { field } >
< div className = "flex items-center gap-2" >
< span > { field } < / span >
{ firstSample && firstSample [ field ] !== undefined && (
< span className = "text-muted-foreground text-xs" >
( 예 : { String ( firstSample [ field ] ) . substring ( 0 , 15 ) }
{ String ( firstSample [ field ] ) . length > 15 ? "..." : "" } )
< / span >
) }
< / div >
< / SelectItem >
) ) }
< / SelectContent >
< / Select >
) : (
< Input
value = { mapping . fixedValue }
onChange = { ( e ) = > updateMapping ( mapping . id , { fixedValue : e.target.value } ) }
placeholder = "고정값 입력"
className = "h-9"
/ >
) }
< / div >
{ /* 삭제 버튼 */ }
< Button
variant = "ghost"
size = "icon"
onClick = { ( ) = > removeMapping ( mapping . id ) }
className = "text-muted-foreground hover:text-destructive h-8 w-8 shrink-0"
>
< Trash2 className = "h-4 w-4" / >
< / Button >
< / div >
) ) }
2025-11-27 11:48:03 +09:00
< / div >
2025-12-04 18:34:29 +09:00
) }
2025-11-27 11:48:03 +09:00
< / div >
2025-12-04 18:34:29 +09:00
< / div >
2025-11-27 11:48:03 +09:00
< / CardContent >
< / Card >
) ;
} ) ;
const DbToRestApiMappingCard = memo ( function DbToRestApiMappingCard ( {
fromColumns ,
selectedColumns ,
toApiFields ,
dbToApiFieldMapping ,
setDbToApiFieldMapping ,
setToApiBody ,
} : DbToRestApiMappingCardProps ) {
const selectedColumnObjects = useMemo (
2025-12-04 17:26:29 +09:00
( ) = > fromColumns . filter ( ( column ) = > selectedColumns . includes ( column . column_name ) ) ,
[ fromColumns , selectedColumns ] ,
2025-11-27 11:48:03 +09:00
) ;
const autoJsonPreview = useMemo ( ( ) = > {
if ( selectedColumns . length === 0 ) {
return "" ;
}
2025-12-04 17:26:29 +09:00
const obj = selectedColumns . reduce (
( acc , col ) = > {
const apiField = dbToApiFieldMapping [ col ] || col ;
acc [ apiField ] = ` {{ ${ col } }} ` ;
return acc ;
} ,
{ } as Record < string , string > ,
) ;
2025-11-27 11:48:03 +09:00
return JSON . stringify ( obj , null , 2 ) ;
} , [ selectedColumns , dbToApiFieldMapping ] ) ;
return (
< Card >
< CardHeader >
< CardTitle > DB 컬 럼 → API 필 드 매 핑 < / CardTitle >
< CardDescription >
2025-12-04 17:26:29 +09:00
DB 컬 럼 값 을 REST API Request Body에 매 핑 하 세 요 . Request Body 템 플 릿 에 서 { "{{컬럼명}}" } 형 태 로 사 용 됩 니 다 .
2025-11-27 11:48:03 +09:00
< / CardDescription >
< / CardHeader >
< CardContent >
2025-12-04 17:26:29 +09:00
< div className = "max-h-96 space-y-3 overflow-y-auto rounded-lg border p-4" >
2025-11-27 11:48:03 +09:00
{ selectedColumnObjects . map ( ( column ) = > (
2026-03-09 14:31:59 +09:00
< div key = { column . column_name } className = "flex items-center space-x-4 rounded-lg bg-muted p-3" >
2025-11-27 11:48:03 +09:00
{ /* DB 컬럼 정보 */ }
< div className = "flex-1" >
2025-12-04 17:26:29 +09:00
< div className = "text-sm font-medium" > { column . column_name } < / div >
2026-03-09 14:31:59 +09:00
< div className = "text-xs text-muted-foreground" >
2025-11-27 11:48:03 +09:00
타 입 : { column . data_type } | NULL : { column . is_nullable ? "Y" : "N" }
< / div >
< / div >
{ /* 화살표 */ }
2026-03-09 14:31:59 +09:00
< div className = "text-muted-foreground/70" > → < / div >
2025-11-27 11:48:03 +09:00
{ /* API 필드 선택 드롭다운 */ }
< div className = "flex-1" >
< Select
value = { dbToApiFieldMapping [ column . column_name ] || "" }
onValueChange = { ( value ) = > {
setDbToApiFieldMapping ( ( prev ) = > ( {
. . . prev ,
[ column . column_name ] : value === "none" ? "" : value ,
} ) ) ;
} }
>
< SelectTrigger className = "w-full" >
< SelectValue placeholder = "API 필드 선택" / >
< / SelectTrigger >
< SelectContent >
< SelectItem value = "none" > 선 택 안 함 < / SelectItem >
{ toApiFields . map ( ( apiField ) = > (
< SelectItem key = { apiField } value = { apiField } >
{ apiField }
< / SelectItem >
) ) }
< SelectItem value = "custom" > 직 접 입 력 . . . < / SelectItem >
< / SelectContent >
< / Select >
{ /* 직접 입력 모드 */ }
{ dbToApiFieldMapping [ column . column_name ] === "custom" && (
< input
type = "text"
placeholder = "API 필드명을 직접 입력하세요"
2026-03-09 14:31:59 +09:00
className = "mt-2 w-full rounded-md border border-input px-3 py-2 text-sm focus:ring-2 focus:ring-ring focus:outline-none"
2025-11-27 11:48:03 +09:00
onChange = { ( e ) = > {
setDbToApiFieldMapping ( ( prev ) = > ( {
. . . prev ,
[ column . column_name ] : e . target . value ,
} ) ) ;
} }
/ >
) }
2026-03-09 14:31:59 +09:00
< div className = "mt-1 text-xs text-muted-foreground" >
2025-11-27 11:48:03 +09:00
{ dbToApiFieldMapping [ column . column_name ]
? ` 매핑: ${ column . column_name } → ${ dbToApiFieldMapping [ column . column_name ] } `
: ` 기본값: ${ column . column_name } (DB 컬럼명 사용) ` }
< / div >
< / div >
{ /* 템플릿 미리보기 */ }
< div className = "flex-1" >
2025-12-04 17:26:29 +09:00
< div className = "rounded border bg-white p-2 font-mono text-sm" > { ` {{ ${ column . column_name } }} ` } < / div >
2026-03-09 14:31:59 +09:00
< div className = "mt-1 text-xs text-muted-foreground" > 실 제 DB 값 으 로 치 환 됩 니 다 < / div >
2025-11-27 11:48:03 +09:00
< / div >
< / div >
) ) }
< / div >
{ selectedColumns . length > 0 && (
2026-03-09 14:31:59 +09:00
< div className = "mt-4 rounded-lg border border-primary/20 bg-primary/10 p-3" >
< div className = "text-sm font-medium text-primary" > 자 동 생 성 된 JSON 구 조 < / div >
< pre className = "mt-1 overflow-x-auto font-mono text-xs text-primary" > { autoJsonPreview } < / pre >
2025-11-27 11:48:03 +09:00
< button
type = "button"
onClick = { ( ) = > {
setToApiBody ( autoJsonPreview ) ;
2025-12-04 17:26:29 +09:00
toast . success ( "Request Body에 자동 생성된 JSON이 적용되었습니다." ) ;
2025-11-27 11:48:03 +09:00
} }
2026-03-09 14:31:59 +09:00
className = "mt-2 rounded bg-primary px-3 py-1 text-xs text-white hover:bg-primary/90"
2025-11-27 11:48:03 +09:00
>
Request Body에 적 용
< / button >
< / div >
) }
2026-03-09 14:31:59 +09:00
< div className = "mt-4 rounded-lg border border-primary/20 bg-primary/10 p-3" >
< div className = "text-sm font-medium text-primary" > 매 핑 사 용 예 시 < / div >
< div className = "mt-1 font-mono text-xs text-primary" >
2025-12-04 17:26:29 +09:00
{ '{"id": "{{id}}", "name": "{{user_name}}", "email": "{{email}}"}' }
2025-11-27 11:48:03 +09:00
< / div >
< / div >
< / CardContent >
< / Card >
) ;
2025-12-04 17:26:29 +09:00
} ) ;