배치 생성 페이지에 memo 사용

This commit is contained in:
dohyeons 2025-11-27 11:48:03 +09:00
parent 707328e765
commit 25c2ab3413
1 changed files with 326 additions and 232 deletions

View File

@ -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>
);
});