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:
hjjeong
2026-05-13 16:32:41 +09:00
parent b752de23a1
commit 54a8f97f78
3 changed files with 56 additions and 39 deletions
@@ -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>
))}
+10 -2
View File
@@ -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;