f31a7f852f
- 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>
164 lines
5.8 KiB
TypeScript
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> 의 값을 보고 J01→active 변환 시 평가 필드 ={" "}
|
|
<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 : "",
|
|
};
|
|
}
|