Files
invyone/frontend/components/fc/fields/SelectField.tsx
T
2026-04-10 13:33:37 +09:00

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>
);
}