feat: select-basic 컴포넌트에 다중선택 기능 추가
기능: - 설정 패널에 '다중 선택' 체크박스 추가 - multiple 옵션 활성화 시 다중선택 UI 렌더링 - 선택된 항목을 태그 형식으로 표시 - 각 태그에 X 버튼으로 개별 제거 가능 - 드롭다운에 체크박스 표시 - 콤마(,) 구분자로 값 저장/파싱 수정사항: - SelectBasicConfigPanel: 다중 선택 체크박스 추가 - SelectBasicConfigPanel: config 병합 방식으로 변경 (다른 속성 보호) - SelectBasicComponent: 초기값 콤마 구분자로 파싱 - SelectBasicComponent: 외부 value 변경 시 다중선택 배열 동기화 - SelectBasicComponent: 다중선택 UI 렌더링 로직 추가 사용법: 1. 설정 패널에서 '다중 선택' 체크 2. 드롭다운에서 여러 항목 선택 3. 선택된 항목이 태그로 표시되며 X로 제거 가능 4. 저장 시 '값1,값2,값3' 형식으로 저장
This commit is contained in:
parent
3219015a39
commit
c57e0218fe
|
|
@ -62,8 +62,14 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
const [selectedValue, setSelectedValue] = useState(externalValue || config?.value || "");
|
const [selectedValue, setSelectedValue] = useState(externalValue || config?.value || "");
|
||||||
const [selectedLabel, setSelectedLabel] = useState("");
|
const [selectedLabel, setSelectedLabel] = useState("");
|
||||||
|
|
||||||
// multiselect의 경우 배열로 관리
|
// multiselect의 경우 배열로 관리 (콤마 구분자로 파싱)
|
||||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
const [selectedValues, setSelectedValues] = useState<string[]>(() => {
|
||||||
|
const initialValue = externalValue || config?.value || "";
|
||||||
|
if (config?.multiple && typeof initialValue === "string" && initialValue) {
|
||||||
|
return initialValue.split(",").map(v => v.trim()).filter(v => v);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
// autocomplete의 경우 검색어 관리
|
// autocomplete의 경우 검색어 관리
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
@ -116,8 +122,14 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
// 값이 실제로 다른 경우에만 업데이트 (빈 문자열도 유효한 값으로 처리)
|
// 값이 실제로 다른 경우에만 업데이트 (빈 문자열도 유효한 값으로 처리)
|
||||||
if (newValue !== selectedValue) {
|
if (newValue !== selectedValue) {
|
||||||
setSelectedValue(newValue);
|
setSelectedValue(newValue);
|
||||||
|
|
||||||
|
// 다중선택 모드인 경우 selectedValues도 업데이트
|
||||||
|
if (config?.multiple && typeof newValue === "string" && newValue) {
|
||||||
|
const values = newValue.split(",").map(v => v.trim()).filter(v => v);
|
||||||
|
setSelectedValues(values);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [externalValue, config?.value]);
|
}, [externalValue, config?.value, config?.multiple]);
|
||||||
|
|
||||||
// ✅ React Query가 자동으로 처리하므로 복잡한 전역 상태 관리 제거
|
// ✅ React Query가 자동으로 처리하므로 복잡한 전역 상태 관리 제거
|
||||||
// - 캐싱: React Query가 자동 관리 (10분 staleTime, 30분 gcTime)
|
// - 캐싱: React Query가 자동 관리 (10분 staleTime, 30분 gcTime)
|
||||||
|
|
@ -500,6 +512,93 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// select (기본 선택박스)
|
// select (기본 선택박스)
|
||||||
|
// 다중선택 모드인 경우
|
||||||
|
if (config?.multiple) {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"box-border flex h-full min-h-[40px] w-full flex-wrap gap-2 rounded-lg border border-gray-300 bg-white px-3 py-2",
|
||||||
|
!isDesignMode && "hover:border-orange-400",
|
||||||
|
isSelected && "ring-2 ring-orange-500",
|
||||||
|
)}
|
||||||
|
onClick={() => !isDesignMode && setIsOpen(true)}
|
||||||
|
style={{ pointerEvents: isDesignMode ? "none" : "auto" }}
|
||||||
|
>
|
||||||
|
{selectedValues.map((val, idx) => {
|
||||||
|
const opt = allOptions.find((o) => o.value === val);
|
||||||
|
return (
|
||||||
|
<span key={idx} className="flex items-center gap-1 rounded bg-blue-100 px-2 py-1 text-sm text-blue-800">
|
||||||
|
{opt?.label || val}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const newVals = selectedValues.filter((v) => v !== val);
|
||||||
|
setSelectedValues(newVals);
|
||||||
|
const newValue = newVals.join(",");
|
||||||
|
if (isInteractive && onFormDataChange && component.columnName) {
|
||||||
|
onFormDataChange(component.columnName, newValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="ml-1 text-blue-600 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{selectedValues.length === 0 && (
|
||||||
|
<span className="text-gray-500">{placeholder}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isOpen && !isDesignMode && (
|
||||||
|
<div className="absolute z-[99999] mt-1 max-h-60 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg">
|
||||||
|
{isLoadingCodes ? (
|
||||||
|
<div className="bg-white px-3 py-2 text-gray-900">로딩 중...</div>
|
||||||
|
) : allOptions.length > 0 ? (
|
||||||
|
allOptions.map((option, index) => {
|
||||||
|
const isSelected = selectedValues.includes(option.value);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${option.value}-${index}`}
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer px-3 py-2 text-gray-900 hover:bg-gray-100",
|
||||||
|
isSelected && "bg-blue-50 font-medium"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
const newVals = isSelected
|
||||||
|
? selectedValues.filter((v) => v !== option.value)
|
||||||
|
: [...selectedValues, option.value];
|
||||||
|
setSelectedValues(newVals);
|
||||||
|
const newValue = newVals.join(",");
|
||||||
|
if (isInteractive && onFormDataChange && component.columnName) {
|
||||||
|
onFormDataChange(component.columnName, newValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => {}}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<span>{option.label || option.value}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="bg-white px-3 py-2 text-gray-900">옵션이 없습니다</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 단일선택 모드
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const handleChange = (key: keyof SelectBasicConfig, value: any) => {
|
const handleChange = (key: keyof SelectBasicConfig, value: any) => {
|
||||||
onChange({ [key]: value });
|
// 기존 config와 병합하여 전체 객체 전달 (다른 속성 보호)
|
||||||
|
const newConfig = { ...config, [key]: value };
|
||||||
|
onChange(newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -67,6 +69,15 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
|
||||||
onCheckedChange={(checked) => handleChange("readonly", checked)}
|
onCheckedChange={(checked) => handleChange("readonly", checked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="multiple">다중 선택</Label>
|
||||||
|
<Checkbox
|
||||||
|
id="multiple"
|
||||||
|
checked={config.multiple || false}
|
||||||
|
onCheckedChange={(checked) => handleChange("multiple", checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue