fix(batch): 미리보기 → 매핑 카드 표시 흐름 정상화 + 매핑 카드 컴팩트화
배치 생성 흐름 검증 중 발견된 4가지 이슈 일괄 정정.
1) BatchManagementService.previewRestApiData — camelCase 키 명시 remap
직전 커밋(b752de23)에서 convertCamelToSnake() 호출 추가했지만 그 함수의 실제 구현이
batch_configs 전용 snake→snake remap 이라 사실상 no-op. 프론트의 apiUrl 등 camelCase
가 변환되지 않아 isBlank(api_url)=true → 400.
→ previewRestApiData 진입부에 직접 remap (apiUrl/apiKey/requestBody/dataArrayPath/
paramType/paramName/paramValue/paramSource/authServiceName 9개 키).
2) batchManagement.ts.previewRestApiData — 응답 totalCount 정규화
백엔드는 total_count (snake_case) 로 응답하는데 프론트는 result.totalCount 로 읽음.
토스트가 "2개 필드, undefined개 레코드" 로 표시됨.
→ 응답 normalize: total_count ?? totalCount ?? 0.
3) batch-management-new/page.tsx — root h-full overflow-y-auto
페이지 root 가 overflow 처리가 없어 FROM/TO 카드 아래의 매핑 카드가 탭 컨테이너
밖으로 잘려 사용자가 못 봄.
→ root div 에 h-full overflow-y-auto 추가.
4) RestApiToDbMappingCard — v5 컨벤션에 맞춘 컴팩트화
다른 메뉴들과 톤 통일. CardHeader 패딩 축소, 폰트 size 일괄 다운,
행 padding p-3 → p-2, Select/Input h-9 → h-7 text-xs, 순서 원형 h-6 → h-5,
카드 내부 height 360 → 300px, 매핑 추가 버튼/삭제 버튼 컴팩트.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -200,9 +200,18 @@ public class BatchManagementService extends BaseService {
|
||||
// ── REST API Preview / Save ───────────────────────────────────────────────
|
||||
|
||||
public Map<String, Object> previewRestApiData(Map<String, Object> body) {
|
||||
// 프론트는 camelCase 로 보내고 백엔드는 snake_case 로 읽음 — 변환 필요
|
||||
// (updateBatchConfig / executeBatchConfig 와 동일 패턴. 누락되어 있던 것을 보강)
|
||||
convertCamelToSnake(body);
|
||||
// 프론트(batchManagement.ts)는 camelCase 로 키를 보내고 백엔드는 snake_case 로 읽음.
|
||||
// 기존 convertCamelToSnake() 는 batch_configs 전용 remap 이라 여기엔 효과 없음.
|
||||
// → previewRestApiData 전용으로 사용하는 키만 직접 remap.
|
||||
remap(body, "apiUrl", "api_url");
|
||||
remap(body, "apiKey", "api_key");
|
||||
remap(body, "requestBody", "request_body");
|
||||
remap(body, "dataArrayPath", "data_array_path");
|
||||
remap(body, "paramType", "param_type");
|
||||
remap(body, "paramName", "param_name");
|
||||
remap(body, "paramValue", "param_value");
|
||||
remap(body, "paramSource", "param_source");
|
||||
remap(body, "authServiceName", "auth_service_name");
|
||||
|
||||
String apiUrl = str(body.get("api_url"));
|
||||
String endpoint = str(body.get("endpoint"));
|
||||
|
||||
@@ -721,7 +721,7 @@ export default function BatchManagementNewPage() {
|
||||
const goBack = () => openTab({ type: "admin", title: "배치 관리", admin_url: "/admin/automaticMng/batchmngList" });
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-6 p-4 sm:p-6">
|
||||
<div className="h-full w-full space-y-6 overflow-y-auto p-4 sm:p-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -1630,30 +1630,30 @@ const RestApiToDbMappingCard = memo(function RestApiToDbMappingCard({
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>컬럼 매핑 설정</CardTitle>
|
||||
<CardDescription>DB 컬럼에 API 필드 또는 고정값을 매핑합니다.</CardDescription>
|
||||
<CardHeader className="px-4 pb-2 pt-3">
|
||||
<CardTitle className="text-sm">컬럼 매핑 설정</CardTitle>
|
||||
<CardDescription className="text-[11px]">DB 컬럼에 API 필드 또는 고정값을 매핑합니다.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<CardContent className="px-4 pb-3 pt-0">
|
||||
<div className="grid grid-cols-1 gap-3 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 className="mb-2 flex h-7 items-center">
|
||||
<h4 className="text-xs 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">
|
||||
<div className="bg-muted/30 h-[300px] overflow-y-auto rounded-lg border p-2">
|
||||
<div className="space-y-1.5">
|
||||
{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 key={index} className="bg-background rounded border p-1.5">
|
||||
<pre className="font-mono text-[11px] whitespace-pre-wrap">{json}</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[360px] items-center justify-center rounded-lg border border-dashed">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<div className="flex h-[300px] items-center justify-center rounded-lg border border-dashed">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
API 데이터 미리보기를 실행하면 샘플 데이터가 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1662,39 +1662,39 @@ const RestApiToDbMappingCard = memo(function RestApiToDbMappingCard({
|
||||
|
||||
{/* 오른쪽: 매핑 영역 (스크롤) */}
|
||||
<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" />
|
||||
<div className="mb-2 flex h-7 items-center justify-between">
|
||||
<h4 className="text-xs font-semibold">매핑 설정</h4>
|
||||
<Button variant="outline" size="sm" onClick={addMapping} className="h-7 gap-1 px-2 text-[11px]">
|
||||
<Plus className="h-3 w-3" />
|
||||
매핑 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{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">
|
||||
<div className="flex h-[300px] flex-col items-center justify-center rounded-lg border border-dashed text-center">
|
||||
<p className="text-muted-foreground text-xs">매핑이 없습니다.</p>
|
||||
<Button variant="link" size="sm" onClick={addMapping} className="mt-1 h-auto text-xs">
|
||||
매핑 추가하기
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-muted/30 h-[360px] space-y-3 overflow-y-auto rounded-lg border p-3">
|
||||
<div className="bg-muted/30 h-[300px] space-y-2 overflow-y-auto rounded-lg border p-2">
|
||||
{mappingList.map((mapping, index) => (
|
||||
<div key={mapping.id} className="bg-background flex items-center gap-2 rounded-lg border p-3">
|
||||
<div key={mapping.id} className="bg-background flex items-center gap-1.5 rounded-lg border p-2">
|
||||
{/* 순서 표시 */}
|
||||
<div className="bg-primary/10 text-primary flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-medium">
|
||||
<div className="bg-primary/10 text-primary flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10px] font-medium">
|
||||
{index + 1}
|
||||
</div>
|
||||
|
||||
{/* DB 컬럼 선택 (좌측 - TO) */}
|
||||
<div className="w-36 shrink-0">
|
||||
<div className="w-32 shrink-0">
|
||||
<Select
|
||||
value={mapping.dbColumn || "none"}
|
||||
onValueChange={(value) =>
|
||||
updateMapping(mapping.id, { dbColumn: value === "none" ? "" : value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="DB 컬럼" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -1716,10 +1716,10 @@ const RestApiToDbMappingCard = memo(function RestApiToDbMappingCard({
|
||||
</div>
|
||||
|
||||
{/* 화살표 */}
|
||||
<ArrowLeft className="text-muted-foreground h-4 w-4 shrink-0" />
|
||||
<ArrowLeft className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
|
||||
|
||||
{/* 소스 타입 선택 */}
|
||||
<div className="w-28 shrink-0">
|
||||
<div className="w-24 shrink-0">
|
||||
<Select
|
||||
value={mapping.sourceType}
|
||||
onValueChange={(value: "api" | "fixed" | "conditional") =>
|
||||
@@ -1738,7 +1738,7 @@ const RestApiToDbMappingCard = memo(function RestApiToDbMappingCard({
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -1758,7 +1758,7 @@ const RestApiToDbMappingCard = memo(function RestApiToDbMappingCard({
|
||||
updateMapping(mapping.id, { apiField: value === "none" ? "" : value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="API 필드" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -1768,7 +1768,7 @@ const RestApiToDbMappingCard = memo(function RestApiToDbMappingCard({
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{field}</span>
|
||||
{firstSample && firstSample[field] !== undefined && (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
(예: {String(firstSample[field]).substring(0, 15)}
|
||||
{String(firstSample[field]).length > 15 ? "..." : ""})
|
||||
</span>
|
||||
@@ -1784,7 +1784,7 @@ const RestApiToDbMappingCard = memo(function RestApiToDbMappingCard({
|
||||
value={mapping.fixedValue}
|
||||
onChange={(e) => updateMapping(mapping.id, { fixedValue: e.target.value })}
|
||||
placeholder="고정값 입력"
|
||||
className="h-9"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
)}
|
||||
{mapping.sourceType === "conditional" && (
|
||||
@@ -1807,9 +1807,9 @@ const RestApiToDbMappingCard = memo(function RestApiToDbMappingCard({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeMapping(mapping.id)}
|
||||
className="text-muted-foreground hover:text-destructive h-8 w-8 shrink-0"
|
||||
className="text-muted-foreground hover:text-destructive h-6 w-6 shrink-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -164,7 +164,9 @@ class BatchManagementAPIClass {
|
||||
BatchApiResponse<{
|
||||
fields: string[];
|
||||
samples: any[];
|
||||
totalCount: number;
|
||||
// 백엔드는 snake_case (total_count) 로 응답하므로 두 키 모두 옵션으로 받음
|
||||
total_count?: number;
|
||||
totalCount?: number;
|
||||
}>
|
||||
>(`${this.BASE_PATH}/rest-api/preview`, requestData);
|
||||
|
||||
@@ -172,7 +174,13 @@ class BatchManagementAPIClass {
|
||||
throw new Error(response.data.message || "REST API 미리보기에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data || { fields: [], samples: [], totalCount: 0 };
|
||||
const raw = response.data.data;
|
||||
return {
|
||||
fields: raw?.fields ?? [],
|
||||
samples: raw?.samples ?? [],
|
||||
// 백엔드는 total_count 로 응답 → camelCase totalCount 로 normalize
|
||||
totalCount: raw?.total_count ?? raw?.totalCount ?? 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("REST API 미리보기 오류:", error);
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user