112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import type { FieldConfig, FieldOption } from '@/types/invyone-component';
|
|
|
|
interface SelectFieldProps {
|
|
field: FieldConfig;
|
|
value: any;
|
|
onChange: (value: any) => void;
|
|
mode: 'form' | 'search';
|
|
disabled?: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
/** FieldOption → { value, label } 정규화. string이면 value=label */
|
|
function normalizeOption(opt: FieldOption): { value: string; label: string } {
|
|
if (typeof opt === 'string') return { value: opt, label: opt };
|
|
return { value: opt.value, label: opt.label };
|
|
}
|
|
|
|
/** value로 label 찾기 */
|
|
function findLabel(options: FieldOption[], val: string): string {
|
|
for (const opt of options) {
|
|
const norm = normalizeOption(opt);
|
|
if (norm.value === val) return norm.label;
|
|
}
|
|
return val;
|
|
}
|
|
|
|
export function SelectField({ field, value, onChange, mode, disabled, error }: SelectFieldProps) {
|
|
const rawOptions = field.options ?? [];
|
|
const options = rawOptions.map(normalizeOption);
|
|
|
|
if (mode === 'search') {
|
|
// 검색: MultiSelect (다중, 체크박스) — ★ value를 저장/전송
|
|
const selected: string[] = Array.isArray(value) ? value : [];
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const toggle = (optValue: string) => {
|
|
const next = selected.includes(optValue)
|
|
? selected.filter((v) => v !== optValue)
|
|
: [...selected, optValue];
|
|
onChange(next.length > 0 ? next : undefined);
|
|
};
|
|
|
|
return (
|
|
<div className="relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(!open)}
|
|
disabled={disabled}
|
|
className={`flex h-7 w-full items-center justify-between rounded-md border bg-transparent px-2 text-xs
|
|
${error ? 'border-destructive' : 'border-input'}
|
|
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
|
>
|
|
<span className="truncate text-left">
|
|
{selected.length > 0
|
|
? selected.map((v) => findLabel(rawOptions, v)).join(', ')
|
|
: field.label}
|
|
</span>
|
|
<svg className="h-3 w-3 opacity-50 shrink-0" viewBox="0 0 12 12" fill="none">
|
|
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
</svg>
|
|
</button>
|
|
{open && (
|
|
<div className="absolute z-50 mt-1 max-h-48 w-full overflow-auto rounded-md border bg-popover p-1 shadow-md">
|
|
{options.map((opt) => (
|
|
<label
|
|
key={opt.value}
|
|
className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 text-xs hover:bg-accent"
|
|
>
|
|
<Checkbox
|
|
checked={selected.includes(opt.value)}
|
|
onCheckedChange={() => toggle(opt.value)}
|
|
/>
|
|
<span>{opt.label}</span>
|
|
</label>
|
|
))}
|
|
{options.length === 0 && (
|
|
<div className="px-2 py-1 text-xs text-muted-foreground">옵션 없음</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 폼: 단일 선택 — ★ value를 저장, label을 표시
|
|
return (
|
|
<Select
|
|
value={value ?? ''}
|
|
onValueChange={(v) => onChange(v || null)}
|
|
disabled={disabled}
|
|
>
|
|
<SelectTrigger className={`h-7 text-xs ${error ? 'border-destructive' : ''}`}>
|
|
<SelectValue placeholder={field.placeholder ?? field.label} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{options.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
);
|
|
}
|