ERP-node/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx

488 lines
14 KiB
TypeScript
Raw Normal View History

"use client";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ChevronLeft, ChevronRight, Plus, Trash2 } from "lucide-react";
import type {
PopSearchConfig,
SearchInputType,
DatePresetOption,
} from "./types";
import { SEARCH_INPUT_TYPE_LABELS, DATE_PRESET_LABELS } from "./types";
// ========================================
// 기본값
// ========================================
const DEFAULT_CONFIG: PopSearchConfig = {
inputType: "text",
fieldName: "",
placeholder: "검색어 입력",
debounceMs: 500,
triggerOnEnter: true,
labelPosition: "top",
labelText: "",
labelVisible: true,
};
// ========================================
// 설정 패널 메인
// ========================================
interface ConfigPanelProps {
config: PopSearchConfig | undefined;
onUpdate: (config: PopSearchConfig) => void;
}
export function PopSearchConfigPanel({ config, onUpdate }: ConfigPanelProps) {
const [step, setStep] = useState(0);
const cfg = { ...DEFAULT_CONFIG, ...(config || {}) };
const update = (partial: Partial<PopSearchConfig>) => {
onUpdate({ ...cfg, ...partial });
};
const STEPS = ["기본 설정", "상세 설정"];
return (
<div className="space-y-3 overflow-y-auto pr-1 pb-32">
{/* Stepper 헤더 */}
<div className="flex items-center gap-2 rounded-lg bg-muted/50 px-3 py-2">
{STEPS.map((s, i) => (
<button
key={s}
type="button"
onClick={() => setStep(i)}
className={cn(
"flex items-center gap-1 rounded px-2 py-1 text-[10px] font-medium transition-colors",
step === i
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:text-foreground"
)}
>
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-background text-[8px] font-bold text-foreground">
{i + 1}
</span>
{s}
</button>
))}
</div>
{/* STEP 1: 기본 설정 */}
{step === 0 && (
<StepBasicSettings cfg={cfg} update={update} />
)}
{/* STEP 2: 타입별 상세 설정 */}
{step === 1 && (
<StepDetailSettings cfg={cfg} update={update} />
)}
{/* 이전/다음 버튼 */}
<div className="flex justify-between pt-2">
<Button
variant="outline"
size="sm"
className="h-7 text-[10px]"
disabled={step === 0}
onClick={() => setStep(step - 1)}
>
<ChevronLeft className="mr-1 h-3 w-3" />
</Button>
<Button
variant="outline"
size="sm"
className="h-7 text-[10px]"
disabled={step === STEPS.length - 1}
onClick={() => setStep(step + 1)}
>
<ChevronRight className="ml-1 h-3 w-3" />
</Button>
</div>
</div>
);
}
// ========================================
// STEP 1: 기본 설정
// ========================================
interface StepProps {
cfg: PopSearchConfig;
update: (partial: Partial<PopSearchConfig>) => void;
}
function StepBasicSettings({ cfg, update }: StepProps) {
return (
<div className="space-y-3">
{/* 입력 타입 */}
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
<Select
value={cfg.inputType}
onValueChange={(v) => update({ inputType: v as SearchInputType })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(SEARCH_INPUT_TYPE_LABELS).map(([key, label]) => (
<SelectItem key={key} value={key} className="text-xs">
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 필드명 */}
<div className="space-y-1">
<Label className="text-[10px]">
<span className="text-destructive">*</span>
</Label>
<Input
value={cfg.fieldName}
onChange={(e) => update({ fieldName: e.target.value })}
placeholder="예: supplier_code"
className="h-8 text-xs"
/>
<p className="text-[9px] text-muted-foreground">
filter_changed
</p>
</div>
{/* 플레이스홀더 */}
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Input
value={cfg.placeholder || ""}
onChange={(e) => update({ placeholder: e.target.value })}
placeholder="입력 힌트 텍스트"
className="h-8 text-xs"
/>
</div>
{/* 라벨 표시 */}
<div className="flex items-center gap-2">
<Checkbox
id="labelVisible"
checked={cfg.labelVisible !== false}
onCheckedChange={(checked) =>
update({ labelVisible: Boolean(checked) })
}
/>
<Label htmlFor="labelVisible" className="text-[10px]">
</Label>
</div>
{/* 라벨 텍스트 + 위치 (라벨 표시 ON일 때만) */}
{cfg.labelVisible !== false && (
<>
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
<Input
value={cfg.labelText || ""}
onChange={(e) => update({ labelText: e.target.value })}
placeholder="예: 거래처명"
className="h-8 text-xs"
/>
</div>
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
<Select
value={cfg.labelPosition || "top"}
onValueChange={(v) =>
update({ labelPosition: v as "top" | "left" })
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="top" className="text-xs"> ()</SelectItem>
<SelectItem value="left" className="text-xs"></SelectItem>
</SelectContent>
</Select>
</div>
</>
)}
</div>
);
}
// ========================================
// STEP 2: 타입별 상세 설정
// ========================================
function StepDetailSettings({ cfg, update }: StepProps) {
switch (cfg.inputType) {
case "text":
case "number":
return <TextDetailSettings cfg={cfg} update={update} />;
case "select":
return <SelectDetailSettings cfg={cfg} update={update} />;
case "date-preset":
return <DatePresetDetailSettings cfg={cfg} update={update} />;
case "modal-table":
case "modal-card":
case "modal-icon-grid":
return <ModalDetailSettings cfg={cfg} update={update} />;
case "toggle":
return (
<div className="rounded-lg bg-muted/50 p-3">
<p className="text-[10px] text-muted-foreground">
. ON/OFF .
</p>
</div>
);
default:
return (
<div className="rounded-lg bg-muted/50 p-3">
<p className="text-[10px] text-muted-foreground">
{cfg.inputType} .
</p>
</div>
);
}
}
// ========================================
// text/number 상세 설정
// ========================================
function TextDetailSettings({ cfg, update }: StepProps) {
return (
<div className="space-y-3">
{/* 디바운스 */}
<div className="space-y-1">
<Label className="text-[10px]"> (ms)</Label>
<Input
type="number"
value={cfg.debounceMs ?? 500}
onChange={(e) =>
update({ debounceMs: Math.max(0, Number(e.target.value)) })
}
min={0}
max={5000}
step={100}
className="h-8 text-xs"
/>
<p className="text-[9px] text-muted-foreground">
. 0 (권장: 300~500)
</p>
</div>
{/* Enter 발행 */}
<div className="flex items-center gap-2">
<Checkbox
id="triggerOnEnter"
checked={cfg.triggerOnEnter !== false}
onCheckedChange={(checked) =>
update({ triggerOnEnter: Boolean(checked) })
}
/>
<Label htmlFor="triggerOnEnter" className="text-[10px]">
Enter
</Label>
</div>
</div>
);
}
// ========================================
// select 상세 설정: 정적 옵션 편집
// ========================================
function SelectDetailSettings({ cfg, update }: StepProps) {
const options = cfg.options || [];
const addOption = () => {
update({
options: [
...options,
{ value: `opt_${options.length + 1}`, label: `옵션 ${options.length + 1}` },
],
});
};
const removeOption = (index: number) => {
update({ options: options.filter((_, i) => i !== index) });
};
const updateOption = (index: number, field: "value" | "label", val: string) => {
const next = options.map((opt, i) =>
i === index ? { ...opt, [field]: val } : opt
);
update({ options: next });
};
return (
<div className="space-y-3">
<Label className="text-[10px]"> </Label>
{options.length === 0 && (
<p className="text-[9px] text-muted-foreground">
. .
</p>
)}
{options.map((opt, i) => (
<div key={i} className="flex items-center gap-1">
<Input
value={opt.value}
onChange={(e) => updateOption(i, "value", e.target.value)}
placeholder="값"
className="h-7 flex-1 text-[10px]"
/>
<Input
value={opt.label}
onChange={(e) => updateOption(i, "label", e.target.value)}
placeholder="라벨"
className="h-7 flex-1 text-[10px]"
/>
<button
type="button"
onClick={() => removeOption(i)}
className="flex h-7 w-7 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
))}
<Button
variant="outline"
size="sm"
className="h-7 w-full text-[10px]"
onClick={addOption}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
);
}
// ========================================
// date-preset 상세 설정: 프리셋 선택
// ========================================
function DatePresetDetailSettings({ cfg, update }: StepProps) {
const ALL_PRESETS: DatePresetOption[] = [
"today",
"this-week",
"this-month",
"custom",
];
const activePresets = cfg.datePresets || ["today", "this-week", "this-month"];
const togglePreset = (preset: DatePresetOption) => {
const next = activePresets.includes(preset)
? activePresets.filter((p) => p !== preset)
: [...activePresets, preset];
update({ datePresets: next.length > 0 ? next : ["today"] });
};
return (
<div className="space-y-3">
<Label className="text-[10px]"> </Label>
{ALL_PRESETS.map((preset) => (
<div key={preset} className="flex items-center gap-2">
<Checkbox
id={`preset_${preset}`}
checked={activePresets.includes(preset)}
onCheckedChange={() => togglePreset(preset)}
/>
<Label htmlFor={`preset_${preset}`} className="text-[10px]">
{DATE_PRESET_LABELS[preset]}
</Label>
</div>
))}
{activePresets.includes("custom") && (
<p className="text-[9px] text-muted-foreground">
&quot;&quot; UI가 ( )
</p>
)}
</div>
);
}
// ========================================
// modal-* 상세 설정
// ========================================
function ModalDetailSettings({ cfg, update }: StepProps) {
const mc = cfg.modalConfig || { modalCanvasId: "", displayField: "", valueField: "" };
const updateModal = (partial: Partial<typeof mc>) => {
update({ modalConfig: { ...mc, ...partial } });
};
return (
<div className="space-y-3">
<div className="rounded-lg border border-dashed border-muted-foreground/30 p-3">
<p className="text-[10px] text-muted-foreground">
.
.
</p>
</div>
{/* 모달 캔버스 ID */}
<div className="space-y-1">
<Label className="text-[10px]"> ID</Label>
<Input
value={mc.modalCanvasId}
onChange={(e) => updateModal({ modalCanvasId: e.target.value })}
placeholder="예: modal-supplier"
className="h-8 text-xs"
/>
</div>
{/* 표시 필드 */}
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
<Input
value={mc.displayField}
onChange={(e) => updateModal({ displayField: e.target.value })}
placeholder="예: supplier_name"
className="h-8 text-xs"
/>
<p className="text-[9px] text-muted-foreground">
</p>
</div>
{/* 값 필드 */}
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
<Input
value={mc.valueField}
onChange={(e) => updateModal({ valueField: e.target.value })}
placeholder="예: supplier_code"
className="h-8 text-xs"
/>
<p className="text-[9px] text-muted-foreground">
</p>
</div>
</div>
);
}