공통코드 계층구조 구현
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { useCodeOptions, useTableCodeCategory } from "@/hooks/queries/useCodes";
|
||||
import { useCodeOptions, useTableCodeCategory, useTableColumnHierarchy } from "@/hooks/queries/useCodes";
|
||||
import { cn } from "@/lib/registry/components/common/inputStyles";
|
||||
import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
import type { DataProvidable } from "@/types/data-transfer";
|
||||
import { useCascadingDropdown } from "@/hooks/useCascadingDropdown";
|
||||
import { HierarchicalCodeSelect } from "@/components/common/HierarchicalCodeSelect";
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
@@ -58,7 +59,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
// 🆕 읽기전용/비활성화 상태 확인
|
||||
const isReadonly = (component as any).readonly || (props as any).readonly || componentConfig?.readonly || false;
|
||||
const isDisabled = (component as any).disabled || (props as any).disabled || componentConfig?.disabled || false;
|
||||
const isFieldDisabled = isDesignMode || isReadonly || isDisabled;
|
||||
const isFieldDisabledBase = isDesignMode || isReadonly || isDisabled;
|
||||
// 화면 컨텍스트 (데이터 제공자로 등록)
|
||||
const screenContext = useScreenContextOptional();
|
||||
|
||||
@@ -94,7 +95,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
console.log(" componentConfig 전체:", componentConfig);
|
||||
console.log(" component.componentConfig 전체:", component.componentConfig);
|
||||
console.log(" =======================================");
|
||||
|
||||
|
||||
// 다중선택이 활성화되었는지 알림
|
||||
if (isMultiple) {
|
||||
console.log("✅ 다중선택 모드 활성화됨!");
|
||||
@@ -114,7 +115,10 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>(() => {
|
||||
const initialValue = externalValue || config?.value || "";
|
||||
if (isMultiple && typeof initialValue === "string" && initialValue) {
|
||||
return initialValue.split(",").map(v => v.trim()).filter(v => v);
|
||||
return initialValue
|
||||
.split(",")
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
@@ -122,7 +126,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
// autocomplete의 경우 검색어 관리
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
|
||||
const selectRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 안정적인 쿼리 키를 위한 메모이제이션
|
||||
@@ -133,6 +136,9 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
// 🚀 React Query: 테이블 코드 카테고리 조회
|
||||
const { data: dynamicCodeCategory } = useTableCodeCategory(stableTableName, stableColumnName);
|
||||
|
||||
// 🆕 React Query: 테이블 컬럼의 계층구조 설정 조회
|
||||
const { data: columnHierarchy } = useTableColumnHierarchy(stableTableName, stableColumnName);
|
||||
|
||||
// 코드 카테고리 결정: 동적 카테고리 > 설정 카테고리 (메모이제이션)
|
||||
const codeCategory = useMemo(() => {
|
||||
const category = dynamicCodeCategory || staticCodeCategory;
|
||||
@@ -150,6 +156,11 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
isFetching,
|
||||
} = useCodeOptions(codeCategory, isCodeCategoryValid, menuObjid);
|
||||
|
||||
// 🆕 계층구조 코드 자동 감지: 비활성화 (테이블 타입관리에서 hierarchyRole 설정 방식 사용)
|
||||
// 기존: depth > 1인 코드가 있으면 자동으로 HierarchicalCodeSelect 사용
|
||||
// 변경: 항상 false 반환하여 자동 감지 비활성화
|
||||
const hasHierarchicalCodes = false;
|
||||
|
||||
// 🆕 카테고리 타입 (category webType)을 위한 옵션 로딩
|
||||
const [categoryOptions, setCategoryOptions] = useState<Option[]>([]);
|
||||
const [isLoadingCategories, setIsLoadingCategories] = useState(false);
|
||||
@@ -160,42 +171,125 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
const categoryRelationCode = config?.categoryRelationCode || componentConfig?.categoryRelationCode;
|
||||
const cascadingRole = config?.cascadingRole || componentConfig?.cascadingRole || "child";
|
||||
const cascadingParentField = config?.cascadingParentField || componentConfig?.cascadingParentField;
|
||||
|
||||
|
||||
// 🆕 계층구조 역할 설정 (대분류/중분류/소분류)
|
||||
// 1순위: 동적으로 조회된 값 (테이블 타입관리에서 설정)
|
||||
// 2순위: config에서 전달된 값
|
||||
const hierarchyRole = columnHierarchy?.hierarchyRole || config?.hierarchyRole || componentConfig?.hierarchyRole;
|
||||
const hierarchyParentField = columnHierarchy?.hierarchyParentField || config?.hierarchyParentField || componentConfig?.hierarchyParentField;
|
||||
|
||||
// 디버깅 로그
|
||||
console.log("🔍 [SelectBasic] 계층구조 설정:", {
|
||||
columnName: component.columnName,
|
||||
tableName: component.tableName,
|
||||
columnHierarchy,
|
||||
hierarchyRole,
|
||||
hierarchyParentField,
|
||||
codeCategory,
|
||||
});
|
||||
|
||||
// 🆕 자식 역할일 때 부모 값 추출 (단일 또는 다중)
|
||||
const rawParentValue = cascadingRole === "child" && cascadingParentField && formData
|
||||
? formData[cascadingParentField]
|
||||
: undefined;
|
||||
|
||||
const rawParentValue =
|
||||
cascadingRole === "child" && cascadingParentField && formData ? formData[cascadingParentField] : undefined;
|
||||
|
||||
// 🆕 계층구조 역할에 따른 부모 값 추출
|
||||
const hierarchyParentValue = useMemo(() => {
|
||||
if (!hierarchyRole || hierarchyRole === "large" || !hierarchyParentField || !formData) {
|
||||
return undefined;
|
||||
}
|
||||
return formData[hierarchyParentField] as string | undefined;
|
||||
}, [hierarchyRole, hierarchyParentField, formData]);
|
||||
|
||||
// 🆕 계층구조에서 상위 항목 미선택 시 비활성화
|
||||
const isHierarchyDisabled = (hierarchyRole === "medium" || hierarchyRole === "small") && !hierarchyParentValue;
|
||||
|
||||
// 최종 비활성화 상태
|
||||
const isFieldDisabled = isFieldDisabledBase || isHierarchyDisabled;
|
||||
|
||||
console.log("🔍 [SelectBasic] 비활성화 상태:", {
|
||||
columnName: component.columnName,
|
||||
hierarchyRole,
|
||||
hierarchyParentValue,
|
||||
isHierarchyDisabled,
|
||||
isFieldDisabled,
|
||||
});
|
||||
|
||||
// 🆕 계층구조 역할에 따라 옵션 필터링
|
||||
const filteredCodeOptions = useMemo(() => {
|
||||
console.log("🔍 [SelectBasic] 옵션 필터링:", {
|
||||
columnName: component.columnName,
|
||||
hierarchyRole,
|
||||
hierarchyParentField,
|
||||
hierarchyParentValue,
|
||||
codeOptionsCount: codeOptions?.length || 0,
|
||||
sampleOptions: codeOptions?.slice(0, 3),
|
||||
});
|
||||
|
||||
if (!hierarchyRole || !codeOptions || codeOptions.length === 0) {
|
||||
console.log("🔍 [SelectBasic] 필터링 스킵 - hierarchyRole 없음 또는 옵션 없음");
|
||||
return codeOptions;
|
||||
}
|
||||
|
||||
// 대분류: depth = 1 (최상위)
|
||||
if (hierarchyRole === "large") {
|
||||
const filtered = codeOptions.filter((opt: any) => {
|
||||
const depth = opt.depth || 1;
|
||||
const parentCodeValue = opt.parentCodeValue || opt.parent_code_value;
|
||||
return depth === 1 || !parentCodeValue;
|
||||
});
|
||||
console.log("🔍 [SelectBasic] 대분류 필터링 결과:", filtered.length, "개");
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// 중분류/소분류: 부모 값이 있어야 함
|
||||
if ((hierarchyRole === "medium" || hierarchyRole === "small") && hierarchyParentValue) {
|
||||
const filtered = codeOptions.filter((opt: any) => {
|
||||
const parentCodeValue = opt.parentCodeValue || opt.parent_code_value;
|
||||
return parentCodeValue === hierarchyParentValue;
|
||||
});
|
||||
console.log("🔍 [SelectBasic] 중/소분류 필터링 결과:", filtered.length, "개");
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// 부모 값이 없으면 빈 배열 반환 (선택 불가 상태)
|
||||
if (hierarchyRole === "medium" || hierarchyRole === "small") {
|
||||
console.log("🔍 [SelectBasic] 중/소분류 - 부모값 없음, 빈 배열 반환");
|
||||
return [];
|
||||
}
|
||||
|
||||
return codeOptions;
|
||||
}, [codeOptions, hierarchyRole, hierarchyParentValue, hierarchyParentField, component.columnName]);
|
||||
|
||||
// 🆕 부모값이 콤마로 구분된 문자열이면 배열로 변환 (다중 선택 지원)
|
||||
const parentValues: string[] | undefined = useMemo(() => {
|
||||
if (!rawParentValue) return undefined;
|
||||
|
||||
|
||||
// 이미 배열인 경우
|
||||
if (Array.isArray(rawParentValue)) {
|
||||
return rawParentValue.map(v => String(v)).filter(v => v);
|
||||
return rawParentValue.map((v) => String(v)).filter((v) => v);
|
||||
}
|
||||
|
||||
|
||||
// 콤마로 구분된 문자열인 경우
|
||||
const strValue = String(rawParentValue);
|
||||
if (strValue.includes(',')) {
|
||||
return strValue.split(',').map(v => v.trim()).filter(v => v);
|
||||
if (strValue.includes(",")) {
|
||||
return strValue
|
||||
.split(",")
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v);
|
||||
}
|
||||
|
||||
|
||||
// 단일 값
|
||||
return [strValue];
|
||||
}, [rawParentValue]);
|
||||
|
||||
|
||||
// 🆕 연쇄 드롭다운 훅 사용 (역할에 따라 다른 옵션 로드) - 다중 부모값 지원
|
||||
const {
|
||||
options: cascadingOptions,
|
||||
loading: isLoadingCascading,
|
||||
} = useCascadingDropdown({
|
||||
const { options: cascadingOptions, loading: isLoadingCascading } = useCascadingDropdown({
|
||||
relationCode: cascadingRelationCode,
|
||||
categoryRelationCode: categoryRelationCode, // 🆕 카테고리 값 연쇄관계 지원
|
||||
role: cascadingRole, // 부모/자식 역할 전달
|
||||
parentValues: parentValues, // 다중 부모값
|
||||
});
|
||||
|
||||
|
||||
// 🆕 카테고리 값 연쇄관계가 활성화되었는지 확인
|
||||
const hasCategoryRelation = !!categoryRelationCode;
|
||||
|
||||
@@ -206,32 +300,32 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
columnName: component.columnName,
|
||||
webType,
|
||||
});
|
||||
|
||||
|
||||
setIsLoadingCategories(true);
|
||||
|
||||
|
||||
import("@/lib/api/tableCategoryValue").then(({ getCategoryValues }) => {
|
||||
getCategoryValues(component.tableName!, component.columnName!)
|
||||
.then((response) => {
|
||||
console.log("🔍 [SelectBasic] 카테고리 API 응답:", response);
|
||||
|
||||
|
||||
if (response.success && response.data) {
|
||||
console.log("🔍 [SelectBasic] 원본 데이터 샘플:", {
|
||||
firstItem: response.data[0],
|
||||
keys: response.data[0] ? Object.keys(response.data[0]) : [],
|
||||
});
|
||||
|
||||
|
||||
const activeValues = response.data.filter((v) => v.isActive !== false);
|
||||
const options = activeValues.map((v) => ({
|
||||
value: v.valueCode,
|
||||
label: v.valueLabel || v.valueCode,
|
||||
}));
|
||||
|
||||
|
||||
console.log("✅ [SelectBasic] 카테고리 옵션 설정:", {
|
||||
activeValuesCount: activeValues.length,
|
||||
options,
|
||||
sampleOption: options[0],
|
||||
});
|
||||
|
||||
|
||||
setCategoryOptions(options);
|
||||
} else {
|
||||
console.error("❌ [SelectBasic] 카테고리 응답 실패:", response);
|
||||
@@ -264,7 +358,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
// 외부 value prop 변경 시 selectedValue 업데이트
|
||||
useEffect(() => {
|
||||
const newValue = externalValue || config?.value || "";
|
||||
|
||||
|
||||
console.log("🔍 [SelectBasic] 외부 값 변경 감지:", {
|
||||
componentId: component.id,
|
||||
columnName: (component as any).columnName,
|
||||
@@ -275,13 +369,16 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
externalValue,
|
||||
"config.value": config?.value,
|
||||
});
|
||||
|
||||
|
||||
// 다중선택 모드인 경우
|
||||
if (isMultiple) {
|
||||
if (typeof newValue === "string" && newValue) {
|
||||
const values = newValue.split(",").map(v => v.trim()).filter(v => v);
|
||||
const values = newValue
|
||||
.split(",")
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v);
|
||||
const currentValuesStr = selectedValues.join(",");
|
||||
|
||||
|
||||
if (newValue !== currentValuesStr) {
|
||||
console.log("✅ [SelectBasic] 다중선택 값 업데이트:", {
|
||||
from: selectedValues,
|
||||
@@ -310,23 +407,25 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
const dataProvider: DataProvidable = {
|
||||
componentId: component.id,
|
||||
componentType: "select",
|
||||
|
||||
|
||||
getSelectedData: () => {
|
||||
// 현재 선택된 값을 배열로 반환
|
||||
const fieldName = component.columnName || "selectedValue";
|
||||
return [{
|
||||
[fieldName]: selectedValue,
|
||||
value: selectedValue,
|
||||
label: selectedLabel,
|
||||
}];
|
||||
return [
|
||||
{
|
||||
[fieldName]: selectedValue,
|
||||
value: selectedValue,
|
||||
label: selectedLabel,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
|
||||
getAllData: () => {
|
||||
// 모든 옵션 반환
|
||||
const configOptions = config.options || [];
|
||||
return [...codeOptions, ...categoryOptions, ...configOptions];
|
||||
},
|
||||
|
||||
|
||||
clearSelection: () => {
|
||||
setSelectedValue("");
|
||||
setSelectedLabel("");
|
||||
@@ -340,7 +439,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
useEffect(() => {
|
||||
if (screenContext && component.id) {
|
||||
screenContext.registerDataProvider(component.id, dataProvider);
|
||||
|
||||
|
||||
return () => {
|
||||
screenContext.unregisterDataProvider(component.id);
|
||||
};
|
||||
@@ -442,9 +541,18 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
if (cascadingRelationCode) {
|
||||
return cascadingOptions;
|
||||
}
|
||||
|
||||
|
||||
const configOptions = config.options || [];
|
||||
return [...codeOptions, ...categoryOptions, ...configOptions];
|
||||
// 🆕 계층구조 역할이 설정된 경우 필터링된 옵션 사용
|
||||
console.log("🔍 [SelectBasic] getAllOptions 호출:", {
|
||||
columnName: component.columnName,
|
||||
hierarchyRole,
|
||||
codeOptionsCount: codeOptions?.length || 0,
|
||||
filteredCodeOptionsCount: filteredCodeOptions?.length || 0,
|
||||
categoryOptionsCount: categoryOptions?.length || 0,
|
||||
configOptionsCount: configOptions?.length || 0,
|
||||
});
|
||||
return [...filteredCodeOptions, ...categoryOptions, ...configOptions];
|
||||
};
|
||||
|
||||
const allOptions = getAllOptions();
|
||||
@@ -482,6 +590,45 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
|
||||
// 세부 타입별 렌더링
|
||||
const renderSelectByWebType = () => {
|
||||
// 🆕 계층구조 코드: 자동 감지 또는 수동 설정 시 1,2,3단계 셀렉트박스로 렌더링
|
||||
// 단, hierarchyRole이 설정된 경우(개별 컬럼별 계층구조)는 일반 셀렉트 사용
|
||||
const shouldUseHierarchical = !hierarchyRole && (config?.useHierarchicalCode || hasHierarchicalCodes);
|
||||
|
||||
if (shouldUseHierarchical && codeCategory) {
|
||||
const maxDepth = config?.hierarchicalMaxDepth || 3;
|
||||
const labels = config?.hierarchicalLabels || ["대분류", "중분류", "소분류"];
|
||||
const placeholders = config?.hierarchicalPlaceholders || ["선택하세요", "선택하세요", "선택하세요"];
|
||||
const isInline = config?.hierarchicalInline || false;
|
||||
|
||||
return (
|
||||
<HierarchicalCodeSelect
|
||||
categoryCode={codeCategory}
|
||||
menuObjid={menuObjid}
|
||||
maxDepth={maxDepth}
|
||||
value={selectedValue}
|
||||
onChange={(codeValue: string) => {
|
||||
setSelectedValue(codeValue);
|
||||
// 라벨 업데이트 - 선택된 값을 라벨로도 설정 (계층구조에서는 값=라벨인 경우가 많음)
|
||||
setSelectedLabel(codeValue);
|
||||
|
||||
// 디자인 모드에서의 컴포넌트 속성 업데이트
|
||||
if (onUpdate) {
|
||||
onUpdate("value", codeValue);
|
||||
}
|
||||
|
||||
// 인터랙티브 모드에서 폼 데이터 업데이트
|
||||
if (isInteractive && onFormDataChange && component.columnName) {
|
||||
onFormDataChange(component.columnName, codeValue);
|
||||
}
|
||||
}}
|
||||
labels={labels as [string, string?, string?]}
|
||||
placeholders={placeholders as [string, string?, string?]}
|
||||
className={isInline ? "flex-row gap-2" : "flex-col gap-2"}
|
||||
disabled={isFieldDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// code-radio: 라디오 버튼으로 코드 선택
|
||||
if (webType === "code-radio") {
|
||||
return (
|
||||
@@ -527,7 +674,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
"h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 transition-all outline-none",
|
||||
!isFieldDisabled && "hover:border-orange-400 focus:border-orange-500 focus:ring-2 focus:ring-orange-200",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
isFieldDisabled && "bg-gray-100 cursor-not-allowed",
|
||||
isFieldDisabled && "cursor-not-allowed !bg-gray-100 opacity-60",
|
||||
)}
|
||||
readOnly={isFieldDisabled}
|
||||
disabled={isFieldDisabled}
|
||||
@@ -565,7 +712,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
!isFieldDisabled && "cursor-pointer hover:border-orange-400",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
isOpen && "border-orange-500",
|
||||
isFieldDisabled && "bg-gray-100 cursor-not-allowed",
|
||||
isFieldDisabled && "cursor-not-allowed !bg-gray-100 opacity-60",
|
||||
)}
|
||||
onClick={handleToggle}
|
||||
style={{ pointerEvents: isFieldDisabled ? "none" : "auto" }}
|
||||
@@ -612,7 +759,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
"box-border flex h-full w-full flex-wrap gap-2 rounded-lg border border-gray-300 bg-white px-3 py-2",
|
||||
!isFieldDisabled && "hover:border-orange-400",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
isFieldDisabled && "bg-gray-100 cursor-not-allowed",
|
||||
isFieldDisabled && "cursor-not-allowed !bg-gray-100 opacity-60",
|
||||
)}
|
||||
>
|
||||
{selectedValues.map((val, idx) => {
|
||||
@@ -673,7 +820,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
"h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 transition-all outline-none",
|
||||
!isFieldDisabled && "hover:border-orange-400 focus:border-orange-500 focus:ring-2 focus:ring-orange-200",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
isFieldDisabled && "bg-gray-100 cursor-not-allowed",
|
||||
isFieldDisabled && "cursor-not-allowed !bg-gray-100 opacity-60",
|
||||
)}
|
||||
readOnly={isFieldDisabled}
|
||||
disabled={isFieldDisabled}
|
||||
@@ -713,7 +860,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
!isFieldDisabled && "cursor-pointer hover:border-orange-400",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
isOpen && "border-orange-500",
|
||||
isFieldDisabled && "bg-gray-100 cursor-not-allowed",
|
||||
isFieldDisabled && "cursor-not-allowed !bg-gray-100 opacity-60",
|
||||
)}
|
||||
onClick={handleToggle}
|
||||
style={{ pointerEvents: isFieldDisabled ? "none" : "auto" }}
|
||||
@@ -770,12 +917,12 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
"box-border flex w-full flex-wrap items-center gap-2 rounded-lg border border-gray-300 bg-white px-3 py-2",
|
||||
!isFieldDisabled && "hover:border-orange-400",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
isFieldDisabled && "bg-gray-100 cursor-not-allowed",
|
||||
isFieldDisabled && "cursor-not-allowed !bg-gray-100 opacity-60",
|
||||
)}
|
||||
onClick={() => !isFieldDisabled && setIsOpen(true)}
|
||||
style={{
|
||||
style={{
|
||||
pointerEvents: isFieldDisabled ? "none" : "auto",
|
||||
height: "100%"
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{selectedValues.map((val, idx) => {
|
||||
@@ -801,19 +948,17 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{selectedValues.length === 0 && (
|
||||
<span className="text-gray-500">{placeholder}</span>
|
||||
)}
|
||||
{selectedValues.length === 0 && <span className="text-gray-500">{placeholder}</span>}
|
||||
</div>
|
||||
{isOpen && !isFieldDisabled && (
|
||||
<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 || isLoadingCategories) ? (
|
||||
{isLoadingCodes || isLoadingCategories ? (
|
||||
<div className="bg-white px-3 py-2 text-gray-900">로딩 중...</div>
|
||||
) : allOptions.length > 0 ? (
|
||||
(() => {
|
||||
// 부모별 그룹핑 (카테고리 연쇄관계인 경우)
|
||||
const hasParentInfo = allOptions.some((opt: any) => opt.parent_label);
|
||||
|
||||
|
||||
if (hasParentInfo) {
|
||||
// 부모별로 그룹핑
|
||||
const groupedOptions: Record<string, { parentLabel: string; options: typeof allOptions }> = {};
|
||||
@@ -825,11 +970,11 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
}
|
||||
groupedOptions[parentKey].options.push(opt);
|
||||
});
|
||||
|
||||
|
||||
return Object.entries(groupedOptions).map(([parentKey, group]) => (
|
||||
<div key={parentKey}>
|
||||
{/* 그룹 헤더 */}
|
||||
<div className="sticky top-0 bg-gray-100 px-3 py-1.5 text-xs font-semibold text-gray-600 border-b">
|
||||
<div className="sticky top-0 border-b bg-gray-100 px-3 py-1.5 text-xs font-semibold text-gray-600">
|
||||
{group.parentLabel}
|
||||
</div>
|
||||
{/* 그룹 옵션들 */}
|
||||
@@ -840,7 +985,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
key={`${option.value}-${index}`}
|
||||
className={cn(
|
||||
"cursor-pointer px-3 py-2 text-gray-900 hover:bg-gray-100",
|
||||
isOptionSelected && "bg-blue-50 font-medium"
|
||||
isOptionSelected && "bg-blue-50 font-medium",
|
||||
)}
|
||||
onClick={() => {
|
||||
const newVals = isOptionSelected
|
||||
@@ -869,7 +1014,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
onFormDataChange(component.columnName, newValue);
|
||||
}
|
||||
}}
|
||||
className="h-4 w-4 pointer-events-auto"
|
||||
className="pointer-events-auto h-4 w-4"
|
||||
/>
|
||||
<span>{option.label || option.value}</span>
|
||||
</div>
|
||||
@@ -879,7 +1024,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// 부모 정보가 없으면 기존 방식
|
||||
return allOptions.map((option, index) => {
|
||||
const isOptionSelected = selectedValues.includes(option.value);
|
||||
@@ -888,7 +1033,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
key={`${option.value}-${index}`}
|
||||
className={cn(
|
||||
"cursor-pointer px-3 py-2 text-gray-900 hover:bg-gray-100",
|
||||
isOptionSelected && "bg-blue-50 font-medium"
|
||||
isOptionSelected && "bg-blue-50 font-medium",
|
||||
)}
|
||||
onClick={() => {
|
||||
const newVals = isOptionSelected
|
||||
@@ -917,7 +1062,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
onFormDataChange(component.columnName, newValue);
|
||||
}
|
||||
}}
|
||||
className="h-4 w-4 pointer-events-auto"
|
||||
className="pointer-events-auto h-4 w-4"
|
||||
/>
|
||||
<span>{option.label || option.value}</span>
|
||||
</div>
|
||||
@@ -933,7 +1078,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 단일선택 모드
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -943,7 +1088,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
!isFieldDisabled && "cursor-pointer hover:border-orange-400",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
isOpen && "border-orange-500",
|
||||
isFieldDisabled && "bg-gray-100 cursor-not-allowed",
|
||||
isFieldDisabled && "cursor-not-allowed !bg-gray-100 opacity-60",
|
||||
)}
|
||||
onClick={handleToggle}
|
||||
style={{ pointerEvents: isFieldDisabled ? "none" : "auto" }}
|
||||
|
||||
Reference in New Issue
Block a user