Files
hjjeong f31a7f852f feat(batch): Phase 2 — 프런트 ConditionalEditor + 조건 변환 매핑 UI
- batch.ts: ConditionalRule / ConditionalConfig 타입 추가,
  BatchMapping 에 mapping_type ('direct'|'fixed'|'conditional') + mapping_config 필드
- ConditionalEditor.tsx: 평가 필드 선택 + when/then 룰 add/remove + default 입력 컴포넌트.
  emptyConditionalConfig / normalizeConditionalConfig 헬퍼 동봉. vexplor_rps 1:1 포팅
- batchmngList/edit/[id]/page.tsx:
  · MappingItem.sourceType 에 'conditional' 추가 + conditionalConfig 필드
  · 소스타입 Select 에 "조건 변환" 옵션
  · Load: mapping_type=conditional 인 매핑은 mapping_config JSON 파싱 후 복원
  · Save: sourceType=conditional 매핑은 mapping_config 객체와 함께 전송

저장된 룰: {"rules":[{"when":"1","then":"Y"}],"default":"?"} 형태.
Phase 1 의 BatchService 직렬화 경로로 JSONB 에 저장된다.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 10:25:08 +09:00

164 lines
5.8 KiB
TypeScript

"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<ConditionalRule>) => {
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 (
<div className="space-y-1.5 rounded border bg-muted/30 p-2">
<div className="space-y-0.5">
<div className="flex items-center gap-1.5">
<span className="shrink-0 text-[10px] font-medium text-muted-foreground">
<span className="text-destructive">*</span>
</span>
<Select
value={evaluateField || "none"}
onValueChange={(v) => onEvaluateFieldChange(v === "none" ? "" : v)}
>
<SelectTrigger
className={cn(
"h-7 text-xs",
isEvaluateFieldMissing && "border-destructive ring-1 ring-destructive/40",
)}
>
<SelectValue placeholder="조건을 평가할 필드 선택 (필수)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
{/* 저장된 evaluateField 가 fieldOptions 에 없을 때 동적 추가 (응답 미리보기 안 한 편집 모드 대응) */}
{evaluateField && !fieldOptions.includes(evaluateField) && (
<SelectItem value={evaluateField}>{evaluateField} ()</SelectItem>
)}
{fieldOptions.map((f) => (
<SelectItem key={f} value={f}>
{f}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<p className="pl-[60px] text-[10px] text-muted-foreground">
(: status {" "}
<span className="font-mono">enrlFg</span> J01active ={" "}
<span className="font-mono">enrlFg</span>)
</p>
</div>
<div className="space-y-1">
{cfg.rules.map((rule, idx) => (
<div key={idx} className="flex items-center gap-1">
<span className="shrink-0 text-[10px] text-muted-foreground"></span>
<Input
value={rule.when}
onChange={(e) => updateRule(idx, { when: e.target.value })}
placeholder="예: J01"
className="h-7 flex-1 text-xs"
/>
<span className="shrink-0 text-[10px] text-muted-foreground"></span>
<Input
value={rule.then}
onChange={(e) => updateRule(idx, { then: e.target.value })}
placeholder="저장값"
className="h-7 flex-1 text-xs"
/>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => removeRule(idx)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
onClick={addRule}
className="h-6 gap-1 px-2 text-[10px]"
>
<Plus className="h-3 w-3" />
</Button>
</div>
<div className="flex items-center gap-1 border-t pt-1">
<span className="shrink-0 text-[10px] text-muted-foreground"> ()</span>
<Input
value={cfg.default}
onChange={(e) => onConfigChange({ ...cfg, default: e.target.value })}
placeholder="예: 0 또는 빈값"
className="h-7 flex-1 text-xs"
/>
</div>
</div>
);
}
// 빈 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 : "",
};
}