배치 생성 페이지에 memo 사용
This commit is contained in:
parent
707328e765
commit
25c2ab3413
|
|
@ -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<string, string>;
|
||||
setApiFieldMappings: React.Dispatch<
|
||||
React.SetStateAction<Record<string, string>>
|
||||
>;
|
||||
apiFieldPathOverrides: Record<string, string>;
|
||||
setApiFieldPathOverrides: React.Dispatch<
|
||||
React.SetStateAction<Record<string, string>>
|
||||
>;
|
||||
}
|
||||
|
||||
interface DbToRestApiMappingCardProps {
|
||||
fromColumns: BatchColumnInfo[];
|
||||
selectedColumns: string[];
|
||||
toApiFields: string[];
|
||||
dbToApiFieldMapping: Record<string, string>;
|
||||
setDbToApiFieldMapping: React.Dispatch<
|
||||
React.SetStateAction<Record<string, string>>
|
||||
>;
|
||||
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 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API 필드 → DB 컬럼 매핑</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto border rounded-lg p-4">
|
||||
{fromApiFields.map((apiField) => (
|
||||
<div key={apiField} className="flex items-center space-x-4 p-3 bg-gray-50 rounded-lg">
|
||||
{/* API 필드 정보 */}
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{apiField}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{fromApiData.length > 0 && fromApiData[0][apiField] !== undefined
|
||||
? `예: ${String(fromApiData[0][apiField]).substring(0, 30)}${
|
||||
String(fromApiData[0][apiField]).length > 30 ? "..." : ""
|
||||
}`
|
||||
: "API 필드"}
|
||||
</div>
|
||||
{/* JSON 경로 오버라이드 입력 */}
|
||||
<div className="mt-1.5">
|
||||
<Input
|
||||
value={apiFieldPathOverrides[apiField] || ""}
|
||||
onChange={(e) =>
|
||||
setApiFieldPathOverrides((prev) => ({
|
||||
...prev,
|
||||
[apiField]: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="JSON 경로 (예: response.access_token)"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
<p className="text-[11px] text-gray-500 mt-0.5">
|
||||
비워두면 "{apiField}" 필드 전체를 사용하고, 입력하면 해당 경로의 값을 사용합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 화살표 */}
|
||||
<div className="text-gray-400">
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
|
||||
{/* DB 컬럼 선택 */}
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={apiFieldMappings[apiField] || "none"}
|
||||
onValueChange={(value) => {
|
||||
setApiFieldMappings(prev => ({
|
||||
...prev,
|
||||
[apiField]: value === "none" ? "" : value
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="DB 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">선택 안함</SelectItem>
|
||||
{toColumns.map((column) => (
|
||||
<SelectItem key={column.column_name} value={column.column_name}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{column.column_name.toUpperCase()}</span>
|
||||
<span className="text-xs text-gray-500">{column.data_type}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{fromApiData.length > 0 && (
|
||||
<div className="mt-3 p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-2">샘플 데이터 (최대 3개)</div>
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||||
{fromApiData.slice(0, 3).map((item, index) => (
|
||||
<div key={index} className="text-xs bg-white p-2 rounded border">
|
||||
<pre className="whitespace-pre-wrap">{JSON.stringify(item, null, 2)}</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{batchType === "restapi-to-db" &&
|
||||
fromApiFields.length > 0 &&
|
||||
toColumns.length > 0 && (
|
||||
<RestApiToDbMappingCard
|
||||
fromApiFields={fromApiFields}
|
||||
toColumns={toColumns}
|
||||
fromApiData={fromApiData}
|
||||
apiFieldMappings={apiFieldMappings}
|
||||
setApiFieldMappings={setApiFieldMappings}
|
||||
apiFieldPathOverrides={apiFieldPathOverrides}
|
||||
setApiFieldPathOverrides={setApiFieldPathOverrides}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* DB → REST API 매핑 */}
|
||||
{batchType === 'db-to-restapi' && selectedColumns.length > 0 && toApiFields.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>DB 컬럼 → API 필드 매핑</CardTitle>
|
||||
<CardDescription>
|
||||
DB 컬럼 값을 REST API Request Body에 매핑하세요. Request Body 템플릿에서 {`{{컬럼명}}`} 형태로 사용됩니다.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto border rounded-lg p-4">
|
||||
{fromColumns.filter(column => selectedColumns.includes(column.column_name)).map((column) => (
|
||||
<div key={column.column_name} className="flex items-center space-x-4 p-3 bg-gray-50 rounded-lg">
|
||||
{/* DB 컬럼 정보 */}
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{column.column_name}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
타입: {column.data_type} | NULL: {column.is_nullable ? 'Y' : 'N'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 화살표 */}
|
||||
<div className="text-gray-400">→</div>
|
||||
|
||||
{/* 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 필드명을 직접 입력하세요"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 mt-2"
|
||||
onChange={(e) => {
|
||||
setDbToApiFieldMapping(prev => ({
|
||||
...prev,
|
||||
[column.column_name]: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{dbToApiFieldMapping[column.column_name]
|
||||
? `매핑: ${column.column_name} → ${dbToApiFieldMapping[column.column_name]}`
|
||||
: `기본값: ${column.column_name} (DB 컬럼명 사용)`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 템플릿 미리보기 */}
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-mono bg-white p-2 rounded border">
|
||||
{`{{${column.column_name}}}`}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
실제 DB 값으로 치환됩니다
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="text-sm font-medium text-blue-800">매핑 사용 예시</div>
|
||||
<div className="text-xs text-blue-600 mt-1 font-mono">
|
||||
{`{"id": "{{id}}", "name": "{{user_name}}", "email": "{{email}}"}`}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{batchType === "db-to-restapi" &&
|
||||
selectedColumns.length > 0 &&
|
||||
toApiFields.length > 0 && (
|
||||
<DbToRestApiMappingCard
|
||||
fromColumns={fromColumns}
|
||||
selectedColumns={selectedColumns}
|
||||
toApiFields={toApiFields}
|
||||
dbToApiFieldMapping={dbToApiFieldMapping}
|
||||
setDbToApiFieldMapping={setDbToApiFieldMapping}
|
||||
setToApiBody={setToApiBody}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TO 설정 */}
|
||||
<Card>
|
||||
|
|
@ -1435,4 +1255,278 @@ export default function BatchManagementNewPage() {
|
|||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API 필드 → DB 컬럼 매핑</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto border rounded-lg p-4">
|
||||
{fromApiFields.map((apiField) => (
|
||||
<div
|
||||
key={apiField}
|
||||
className="flex items-center space-x-4 p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
{/* API 필드 정보 */}
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{apiField}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{firstSample && firstSample[apiField] !== undefined
|
||||
? `예: ${String(firstSample[apiField]).substring(0, 30)}${
|
||||
String(firstSample[apiField]).length > 30 ? "..." : ""
|
||||
}`
|
||||
: "API 필드"}
|
||||
</div>
|
||||
{/* JSON 경로 오버라이드 입력 */}
|
||||
<div className="mt-1.5">
|
||||
<Input
|
||||
value={apiFieldPathOverrides[apiField] || ""}
|
||||
onChange={(e) =>
|
||||
setApiFieldPathOverrides((prev) => ({
|
||||
...prev,
|
||||
[apiField]: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="JSON 경로 (예: response.access_token)"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
<p className="text-[11px] text-gray-500 mt-0.5">
|
||||
비워두면 "{apiField}" 필드 전체를 사용하고, 입력하면 해당
|
||||
경로의 값을 사용합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 화살표 */}
|
||||
<div className="text-gray-400">
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
|
||||
{/* DB 컬럼 선택 */}
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={apiFieldMappings[apiField] || "none"}
|
||||
onValueChange={(value) => {
|
||||
setApiFieldMappings((prev) => ({
|
||||
...prev,
|
||||
[apiField]: value === "none" ? "" : value,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="DB 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">선택 안함</SelectItem>
|
||||
{toColumns.map((column) => (
|
||||
<SelectItem
|
||||
key={column.column_name}
|
||||
value={column.column_name}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">
|
||||
{column.column_name.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{column.data_type}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{sampleJsonList.length > 0 && (
|
||||
<div className="mt-3 p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-2">
|
||||
샘플 데이터 (최대 3개)
|
||||
</div>
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||||
{sampleJsonList.map((json, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="text-xs bg-white p-2 rounded border"
|
||||
>
|
||||
<pre className="whitespace-pre-wrap">{json}</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
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<string, string>);
|
||||
return JSON.stringify(obj, null, 2);
|
||||
}, [selectedColumns, dbToApiFieldMapping]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>DB 컬럼 → API 필드 매핑</CardTitle>
|
||||
<CardDescription>
|
||||
DB 컬럼 값을 REST API Request Body에 매핑하세요. Request Body
|
||||
템플릿에서 {`{{컬럼명}}`} 형태로 사용됩니다.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto border rounded-lg p-4">
|
||||
{selectedColumnObjects.map((column) => (
|
||||
<div
|
||||
key={column.column_name}
|
||||
className="flex items-center space-x-4 p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
{/* DB 컬럼 정보 */}
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{column.column_name}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
타입: {column.data_type} | NULL: {column.is_nullable ? "Y" : "N"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 화살표 */}
|
||||
<div className="text-gray-400">→</div>
|
||||
|
||||
{/* 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 필드명을 직접 입력하세요"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 mt-2"
|
||||
onChange={(e) => {
|
||||
setDbToApiFieldMapping((prev) => ({
|
||||
...prev,
|
||||
[column.column_name]: e.target.value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{dbToApiFieldMapping[column.column_name]
|
||||
? `매핑: ${column.column_name} → ${dbToApiFieldMapping[column.column_name]}`
|
||||
: `기본값: ${column.column_name} (DB 컬럼명 사용)`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 템플릿 미리보기 */}
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-mono bg-white p-2 rounded border">
|
||||
{`{{${column.column_name}}}`}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
실제 DB 값으로 치환됩니다
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedColumns.length > 0 && (
|
||||
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="text-sm font-medium text-blue-800">
|
||||
자동 생성된 JSON 구조
|
||||
</div>
|
||||
<pre className="mt-1 text-xs text-blue-600 font-mono overflow-x-auto">
|
||||
{autoJsonPreview}
|
||||
</pre>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setToApiBody(autoJsonPreview);
|
||||
toast.success(
|
||||
"Request Body에 자동 생성된 JSON이 적용되었습니다."
|
||||
);
|
||||
}}
|
||||
className="mt-2 px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
|
||||
>
|
||||
Request Body에 적용
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="text-sm font-medium text-blue-800">매핑 사용 예시</div>
|
||||
<div className="text-xs text-blue-600 mt-1 font-mono">
|
||||
{`{"id": "{{id}}", "name": "{{user_name}}", "email": "{{email}}"}`}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
Loading…
Reference in New Issue