diff --git a/frontend/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page.tsx b/frontend/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page.tsx index d7182fdf..60e71eca 100644 --- a/frontend/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page.tsx +++ b/frontend/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page.tsx @@ -25,8 +25,14 @@ import { ConnectionInfo, type NodeFlowInfo, type BatchExecutionType, + type ConditionalConfig, } from "@/lib/api/batch"; import { BatchManagementAPI } from "@/lib/api/batchManagement"; +import { + ConditionalEditor, + emptyConditionalConfig, + normalizeConditionalConfig, +} from "@/components/admin/batch/ConditionalEditor"; const SCHEDULE_PRESETS = [ { label: "5분마다", cron: "*/5 * * * *", preview: "5분마다 실행돼요" }, @@ -165,12 +171,17 @@ export default function BatchEditPage() { const [apiParamSource, setApiParamSource] = useState<"static" | "dynamic">("static"); // 매핑 리스트 (새로운 UI용) + // sourceType: + // - "api" : apiField 의 값을 그대로 복사 (mapping_type=direct) + // - "fixed" : fixedValue 자체가 저장값 (mapping_type=fixed) + // - "conditional" : apiField 값을 conditionalConfig 룰로 변환 (mapping_type=conditional) interface MappingItem { id: string; dbColumn: string; - sourceType: "api" | "fixed"; + sourceType: "api" | "fixed" | "conditional"; apiField: string; fixedValue: string; + conditionalConfig?: ConditionalConfig; } const [mappingList, setMappingList] = useState([]); @@ -377,13 +388,27 @@ export default function BatchEditPage() { }); // 기존 매핑을 mappingList로 변환 - const convertedMappingList: MappingItem[] = config.batch_mappings.map((mapping, index) => ({ - id: `mapping-${index}-${Date.now()}`, - dbColumn: mapping.to_column_name || "", - sourceType: (mapping as any).mapping_type === "fixed" ? "fixed" as const : "api" as const, - apiField: (mapping as any).mapping_type === "fixed" ? "" : mapping.from_column_name || "", - fixedValue: (mapping as any).mapping_type === "fixed" ? mapping.from_column_name || "" : "", - })); + // mapping_type 분기: + // "fixed" → from_column_name 자체가 고정값 → fixedValue + // "conditional" → from_column_name 이 평가 필드명 → apiField + conditionalConfig + // 그 외(direct) → from_column_name 이 API 필드명 → apiField + const convertedMappingList: MappingItem[] = config.batch_mappings.map((mapping, index) => { + const mt = (mapping as any).mapping_type || "direct"; + const sourceType: MappingItem["sourceType"] = + mt === "fixed" ? "fixed" : mt === "conditional" ? "conditional" : "api"; + const conditionalConfig = + sourceType === "conditional" + ? normalizeConditionalConfig((mapping as any).mapping_config) + : undefined; + return { + id: `mapping-${index}-${Date.now()}`, + dbColumn: mapping.to_column_name || "", + sourceType, + apiField: sourceType === "fixed" ? "" : mapping.from_column_name || "", + fixedValue: sourceType === "fixed" ? mapping.from_column_name || "" : "", + conditionalConfig, + }; + }); setMappingList(convertedMappingList); console.log("🔄 변환된 mappingList:", convertedMappingList); } @@ -679,26 +704,46 @@ export default function BatchEditPage() { const first = batchConfig.batch_mappings[0] as any; finalMappings = mappingList .filter((m) => m.dbColumn) // DB 컬럼이 선택된 것만 - .map((m, index) => ({ - // FROM: REST API (기존 설정 복사) - from_connection_type: "restapi" as any, - from_connection_id: first.from_connection_id, - from_table_name: first.from_table_name, - from_column_name: m.sourceType === "fixed" ? m.fixedValue : m.apiField, - from_column_type: m.sourceType === "fixed" ? "text" : "text", - from_api_url: mappings[0]?.from_api_url || first.from_api_url, - from_api_key: authTokenMode === "direct" ? fromApiKey : first.from_api_key, - from_api_method: mappings[0]?.from_api_method || first.from_api_method, - from_api_body: mappings[0]?.from_api_body || first.from_api_body, - // TO: DB (기존 설정 복사) - to_connection_type: first.to_connection_type as any, - to_connection_id: first.to_connection_id, - to_table_name: toTable || first.to_table_name, - to_column_name: m.dbColumn, - to_column_type: toColumns.find((c) => c.column_name === m.dbColumn)?.data_type || "text", - mapping_type: m.sourceType === "fixed" ? "fixed" : "direct", - mapping_order: index + 1, - })) as BatchMapping[]; + .map((m, index) => { + // from_column_name 결정: + // fixed → fixedValue 자체가 저장됨 + // conditional → apiField (평가할 API 필드) + // direct(api) → apiField + const fromColumnName = + m.sourceType === "fixed" ? m.fixedValue : m.apiField; + const mappingType: "direct" | "fixed" | "conditional" = + m.sourceType === "fixed" + ? "fixed" + : m.sourceType === "conditional" + ? "conditional" + : "direct"; + return { + // FROM: REST API (기존 설정 복사) + from_connection_type: "restapi" as any, + from_connection_id: first.from_connection_id, + from_table_name: first.from_table_name, + from_column_name: fromColumnName, + from_column_type: "text", + from_api_url: mappings[0]?.from_api_url || first.from_api_url, + from_api_key: authTokenMode === "direct" ? fromApiKey : first.from_api_key, + from_api_method: mappings[0]?.from_api_method || first.from_api_method, + from_api_body: mappings[0]?.from_api_body || first.from_api_body, + // TO: DB (기존 설정 복사) + to_connection_type: first.to_connection_type as any, + to_connection_id: first.to_connection_id, + to_table_name: toTable || first.to_table_name, + to_column_name: m.dbColumn, + to_column_type: + toColumns.find((c) => c.column_name === m.dbColumn)?.data_type || "text", + mapping_type: mappingType, + // conditional 일 때만 룰 객체를 함께 전송. 백엔드가 JSONB 로 저장. + mapping_config: + m.sourceType === "conditional" && m.conditionalConfig + ? m.conditionalConfig + : null, + mapping_order: index + 1, + }; + }) as BatchMapping[]; } await BatchAPI.updateBatchConfig(batchId, { @@ -1617,14 +1662,22 @@ export default function BatchEditPage() { {/* 소스 타입 선택 */} -
+
- {/* API 필드 선택 또는 고정값 입력 (우측 - FROM) */} + {/* API 필드 선택 / 고정값 입력 / 조건 변환 (우측 - FROM) */}
- {mapping.sourceType === "api" ? ( + {mapping.sourceType === "api" && ( - ) : ( + )} + {mapping.sourceType === "fixed" && ( updateMappingListItem(mapping.id, { fixedValue: e.target.value })} @@ -1675,6 +1730,19 @@ export default function BatchEditPage() { className="h-9" /> )} + {mapping.sourceType === "conditional" && ( + + updateMappingListItem(mapping.id, { apiField: v }) + } + onConfigChange={(cfg) => + updateMappingListItem(mapping.id, { conditionalConfig: cfg }) + } + /> + )}
{/* 삭제 버튼 */} diff --git a/frontend/components/admin/batch/ConditionalEditor.tsx b/frontend/components/admin/batch/ConditionalEditor.tsx new file mode 100644 index 00000000..520cda6c --- /dev/null +++ b/frontend/components/admin/batch/ConditionalEditor.tsx @@ -0,0 +1,163 @@ +"use client"; + +import { Plus, Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import type { ConditionalConfig, ConditionalRule } from "@/lib/api/batch"; + +interface ConditionalEditorProps { + // 평가 대상 필드명 (REST API 응답 필드명 또는 DB 컬럼명) + evaluateField: string; + // 선택 가능한 필드 후보 — REST API 응답 필드 목록 등 + fieldOptions: string[]; + // 현재 conditional 규칙 + config: ConditionalConfig; + onEvaluateFieldChange: (field: string) => void; + onConfigChange: (cfg: ConditionalConfig) => void; +} + +// vexplor_rps 의 ConditionalEditor 1:1 포팅. +// 예: enrlFg 가 "J01" → "active", "J05" → "inactive", 나머지 → "" 식 lookup. +export function ConditionalEditor({ + evaluateField, + fieldOptions, + config, + onEvaluateFieldChange, + onConfigChange, +}: ConditionalEditorProps) { + const cfg: ConditionalConfig = config?.rules + ? config + : { rules: [{ when: "", then: "" }], default: "" }; + + const isEvaluateFieldMissing = !evaluateField; + + const updateRule = (idx: number, patch: Partial) => { + const rules = cfg.rules.map((r, i) => (i === idx ? { ...r, ...patch } : r)); + onConfigChange({ ...cfg, rules }); + }; + const addRule = () => + onConfigChange({ ...cfg, rules: [...cfg.rules, { when: "", then: "" }] }); + const removeRule = (idx: number) => + onConfigChange({ ...cfg, rules: cfg.rules.filter((_, i) => i !== idx) }); + + return ( +
+
+
+ + 평가 필드 * + + +
+

+ 이 필드 값에 따라 아래 규칙으로 변환됩니다 (예: status 컬럼에{" "} + enrlFg 의 값을 보고 J01→active 변환 시 평가 필드 ={" "} + enrlFg) +

+
+ +
+ {cfg.rules.map((rule, idx) => ( +
+ 값이 + updateRule(idx, { when: e.target.value })} + placeholder="예: J01" + className="h-7 flex-1 text-xs" + /> + + updateRule(idx, { then: e.target.value })} + placeholder="저장값" + className="h-7 flex-1 text-xs" + /> + +
+ ))} + +
+ +
+ 매칭 없음(기본) + onConfigChange({ ...cfg, default: e.target.value })} + placeholder="예: 0 또는 빈값" + className="h-7 flex-1 text-xs" + /> +
+
+ ); +} + +// 빈 conditionalConfig 기본값 — page 측에서 모드 전환 시 사용 +export const emptyConditionalConfig = (): ConditionalConfig => ({ + rules: [{ when: "", then: "" }], + default: "", +}); + +// 저장된 mapping_config (string|object|null) 를 안전하게 ConditionalConfig 로 normalize +export function normalizeConditionalConfig(raw: unknown): ConditionalConfig { + if (!raw) return emptyConditionalConfig(); + let parsed: any = raw; + if (typeof raw === "string") { + try { + parsed = JSON.parse(raw); + } catch { + return emptyConditionalConfig(); + } + } + return { + rules: Array.isArray(parsed?.rules) ? parsed.rules : [], + default: typeof parsed?.default === "string" ? parsed.default : "", + }; +} diff --git a/frontend/lib/api/batch.ts b/frontend/lib/api/batch.ts index fac428a1..3de17b44 100644 --- a/frontend/lib/api/batch.ts +++ b/frontend/lib/api/batch.ts @@ -64,24 +64,43 @@ export interface RecentLog { duration_ms: number | null; } +// 조건 변환 규칙 — mapping_type === 'conditional' 일 때 mapping_config 에 저장. +// 평가: row[from_column_name] 값을 cfg.rules 의 when 과 문자열 동등 비교, 매칭되는 then 반환. +// 매칭 없으면 cfg.default. +export interface ConditionalRule { + when: string; + then: string; +} + +export interface ConditionalConfig { + rules: ConditionalRule[]; + default: string; +} + export interface BatchMapping { id?: number; batch_config_id?: number; - + // FROM 정보 from_connection_type: 'internal' | 'external'; from_connection_id?: number; from_table_name: string; from_column_name: string; from_column_type?: string; - + // TO 정보 to_connection_type: 'internal' | 'external'; to_connection_id?: number; to_table_name: string; to_column_name: string; to_column_type?: string; - + + // 매핑 유형 — 'direct' (그대로 복사) / 'fixed' (from_column_name 자체가 고정값) / 'conditional' (when/then 룰) + mapping_type?: 'direct' | 'fixed' | 'conditional'; + // conditional 일 때 ConditionalConfig 의 JSON. 백엔드는 JSONB 로 저장. + // 요청 시 string(JSON) 또는 object 둘 다 허용 — 백엔드가 normalize. + mapping_config?: ConditionalConfig | string | null; + mapping_order?: number; created_date?: Date; created_by?: string;