카테고리 설정 구현

This commit is contained in:
kjs
2025-12-18 14:12:48 +09:00
parent 75e5326b3e
commit f03b247db2
11 changed files with 2927 additions and 138 deletions

View File

@@ -156,22 +156,48 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
// 🆕 연쇄 드롭다운 설정 확인
const cascadingRelationCode = config?.cascadingRelationCode || componentConfig?.cascadingRelationCode;
// 🆕 카테고리 값 연쇄관계 설정
const categoryRelationCode = config?.categoryRelationCode || componentConfig?.categoryRelationCode;
const cascadingRole = config?.cascadingRole || componentConfig?.cascadingRole || "child";
const cascadingParentField = config?.cascadingParentField || componentConfig?.cascadingParentField;
// 자식 역할일 때만 부모 값 필요
const parentValue = cascadingRole === "child" && cascadingParentField && formData
// 🆕 자식 역할일 때 부모 값 추출 (단일 또는 다중)
const rawParentValue = cascadingRole === "child" && cascadingParentField && formData
? formData[cascadingParentField]
: undefined;
// 🆕 연쇄 드롭다운 훅 사용 (역할에 따라 다른 옵션 로드)
// 🆕 부모값이 콤마로 구분된 문자열이면 배열로 변환 (다중 선택 지원)
const parentValues: string[] | undefined = useMemo(() => {
if (!rawParentValue) return undefined;
// 이미 배열인 경우
if (Array.isArray(rawParentValue)) {
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);
}
// 단일 값
return [strValue];
}, [rawParentValue]);
// 🆕 연쇄 드롭다운 훅 사용 (역할에 따라 다른 옵션 로드) - 다중 부모값 지원
const {
options: cascadingOptions,
loading: isLoadingCascading,
} = useCascadingDropdown({
relationCode: cascadingRelationCode,
categoryRelationCode: categoryRelationCode, // 🆕 카테고리 값 연쇄관계 지원
role: cascadingRole, // 부모/자식 역할 전달
parentValue: parentValue,
parentValues: parentValues, // 다중 부모값
});
// 🆕 카테고리 값 연쇄관계가 활성화되었는지 확인
const hasCategoryRelation = !!categoryRelationCode;
useEffect(() => {
if (webType === "category" && component.tableName && component.columnName) {
@@ -324,6 +350,10 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
// 선택된 값에 따른 라벨 업데이트
useEffect(() => {
const getAllOptionsForLabel = () => {
// 🆕 카테고리 값 연쇄관계가 설정된 경우 연쇄 옵션 사용
if (categoryRelationCode) {
return cascadingOptions;
}
// 🆕 연쇄 드롭다운이 설정된 경우 연쇄 옵션만 사용
if (cascadingRelationCode) {
return cascadingOptions;
@@ -353,7 +383,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
if (newLabel !== selectedLabel) {
setSelectedLabel(newLabel);
}
}, [selectedValue, codeOptions, config.options, cascadingOptions, cascadingRelationCode]);
}, [selectedValue, codeOptions, config.options, cascadingOptions, cascadingRelationCode, categoryRelationCode]);
// 클릭 이벤트 핸들러 (React Query로 간소화)
const handleToggle = () => {
@@ -404,6 +434,10 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
// 모든 옵션 가져오기
const getAllOptions = () => {
// 🆕 카테고리 값 연쇄관계가 설정된 경우 연쇄 옵션 사용
if (categoryRelationCode) {
return cascadingOptions;
}
// 🆕 연쇄 드롭다운이 설정된 경우 연쇄 옵션만 사용
if (cascadingRelationCode) {
return cascadingOptions;
@@ -776,50 +810,121 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
{(isLoadingCodes || isLoadingCategories) ? (
<div className="bg-white px-3 py-2 text-gray-900"> ...</div>
) : allOptions.length > 0 ? (
allOptions.map((option, index) => {
const isOptionSelected = 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",
isOptionSelected && "bg-blue-50 font-medium"
)}
onClick={() => {
const newVals = isOptionSelected
? 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={isOptionSelected}
value={option.value}
onChange={(e) => {
// 체크박스 직접 클릭 시에도 올바른 값으로 처리
e.stopPropagation();
const newVals = isOptionSelected
? selectedValues.filter((v) => v !== option.value)
: [...selectedValues, option.value];
setSelectedValues(newVals);
const newValue = newVals.join(",");
if (isInteractive && onFormDataChange && component.columnName) {
onFormDataChange(component.columnName, newValue);
}
}}
className="h-4 w-4 pointer-events-auto"
/>
<span>{option.label || option.value}</span>
(() => {
// 부모별 그룹핑 (카테고리 연쇄관계인 경우)
const hasParentInfo = allOptions.some((opt: any) => opt.parent_label);
if (hasParentInfo) {
// 부모별로 그룹핑
const groupedOptions: Record<string, { parentLabel: string; options: typeof allOptions }> = {};
allOptions.forEach((opt: any) => {
const parentKey = opt.parent_value || "기타";
const parentLabel = opt.parent_label || "기타";
if (!groupedOptions[parentKey]) {
groupedOptions[parentKey] = { parentLabel, options: [] };
}
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">
{group.parentLabel}
</div>
{/* 그룹 옵션들 */}
{group.options.map((option, index) => {
const isOptionSelected = 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",
isOptionSelected && "bg-blue-50 font-medium"
)}
onClick={() => {
const newVals = isOptionSelected
? 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={isOptionSelected}
value={option.value}
onChange={(e) => {
e.stopPropagation();
const newVals = isOptionSelected
? selectedValues.filter((v) => v !== option.value)
: [...selectedValues, option.value];
setSelectedValues(newVals);
const newValue = newVals.join(",");
if (isInteractive && onFormDataChange && component.columnName) {
onFormDataChange(component.columnName, newValue);
}
}}
className="h-4 w-4 pointer-events-auto"
/>
<span>{option.label || option.value}</span>
</div>
</div>
);
})}
</div>
</div>
);
})
));
}
// 부모 정보가 없으면 기존 방식
return allOptions.map((option, index) => {
const isOptionSelected = 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",
isOptionSelected && "bg-blue-50 font-medium"
)}
onClick={() => {
const newVals = isOptionSelected
? 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={isOptionSelected}
value={option.value}
onChange={(e) => {
e.stopPropagation();
const newVals = isOptionSelected
? selectedValues.filter((v) => v !== option.value)
: [...selectedValues, option.value];
setSelectedValues(newVals);
const newValue = newVals.join(",");
if (isInteractive && onFormDataChange && component.columnName) {
onFormDataChange(component.columnName, newValue);
}
}}
className="h-4 w-4 pointer-events-auto"
/>
<span>{option.label || option.value}</span>
</div>
</div>
);
});
})()
) : (
<div className="bg-white px-3 py-2 text-gray-900"> </div>
)}