summaryrefslogtreecommitdiff
path: root/components/common/material/material-group-selector.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/common/material/material-group-selector.tsx')
-rw-r--r--components/common/material/material-group-selector.tsx80
1 files changed, 54 insertions, 26 deletions
diff --git a/components/common/material/material-group-selector.tsx b/components/common/material/material-group-selector.tsx
index 69a929f2..f0276810 100644
--- a/components/common/material/material-group-selector.tsx
+++ b/components/common/material/material-group-selector.tsx
@@ -28,11 +28,11 @@ interface MaterialGroupSelectorProps {
placeholder?: string;
noValuePlaceHolder?: string;
disabled?: boolean;
- maxSelections?: number;
className?: string;
closeOnSelect?: boolean;
excludeMaterialCodes?: Set<string>; // 제외할 자재그룹코드들
showInitialData?: boolean; // 초기 클릭시 자재그룹들을 로드할지 여부
+ maxSelections?: number; // 최대 선택 가능한 자재 개수 (1이면 단일 선택, undefined면 제한 없음)
}
export function MaterialGroupSelector({
@@ -42,11 +42,11 @@ export function MaterialGroupSelector({
placeholder = "자재를 검색하세요...",
noValuePlaceHolder = "자재를 검색해주세요",
disabled = false,
- maxSelections,
className,
closeOnSelect = true,
excludeMaterialCodes,
- showInitialData = true
+ showInitialData = true,
+ maxSelections
}: MaterialGroupSelectorProps) {
const [open, setOpen] = useState(false);
@@ -153,22 +153,27 @@ export function MaterialGroupSelector({
let newSelectedMaterials: MaterialSearchItem[];
- if (singleSelect) {
+ // maxSelections가 1이면 단일 선택 모드로 동작
+ const isSingleSelectMode = singleSelect || maxSelections === 1;
+
+ if (isSingleSelectMode) {
newSelectedMaterials = [material];
} else {
const isAlreadySelected = selectedMaterials.some(
(selected) => selected.materialGroupCode === material.materialGroupCode &&
- selected.materialGroupDesc === material.materialGroupDesc
+ selected.materialGroupDescription === material.materialGroupDescription
);
if (isAlreadySelected) {
newSelectedMaterials = selectedMaterials.filter(
(selected) => !(selected.materialGroupCode === material.materialGroupCode &&
- selected.materialGroupDesc === material.materialGroupDesc)
+ selected.materialGroupDescription === material.materialGroupDescription)
);
} else {
+ // 최대 선택 개수 확인
if (maxSelections && selectedMaterials.length >= maxSelections) {
- return; // 최대 선택 수 초과 시 추가하지 않음
+ // 최대 개수에 도달한 경우 선택하지 않음
+ return;
}
newSelectedMaterials = [...selectedMaterials, material];
}
@@ -176,10 +181,10 @@ export function MaterialGroupSelector({
onMaterialsChange?.(newSelectedMaterials);
- if (closeOnSelect && singleSelect) {
+ if (closeOnSelect && isSingleSelectMode) {
setOpen(false);
}
- }, [disabled, singleSelect, selectedMaterials, maxSelections, onMaterialsChange, closeOnSelect]);
+ }, [disabled, singleSelect, maxSelections, selectedMaterials, onMaterialsChange, closeOnSelect]);
// 개별 자재 제거
const handleRemoveMaterial = useCallback((materialToRemove: MaterialSearchItem) => {
@@ -187,7 +192,7 @@ export function MaterialGroupSelector({
const newSelectedMaterials = selectedMaterials.filter(
(material) => !(material.materialGroupCode === materialToRemove.materialGroupCode &&
- material.materialGroupDesc === materialToRemove.materialGroupDesc)
+ material.materialGroupDescription === materialToRemove.materialGroupDescription)
);
onMaterialsChange?.(newSelectedMaterials);
}, [disabled, selectedMaterials, onMaterialsChange]);
@@ -196,7 +201,7 @@ export function MaterialGroupSelector({
const isMaterialSelected = useCallback((material: MaterialSearchItem) => {
return selectedMaterials.some(
(selected) => selected.materialGroupCode === material.materialGroupCode &&
- selected.materialGroupDesc === material.materialGroupDesc
+ selected.materialGroupDescription === material.materialGroupDescription
);
}, [selectedMaterials]);
@@ -217,11 +222,11 @@ export function MaterialGroupSelector({
) : (
selectedMaterials.map((material) => (
<Badge
- key={`${material.materialGroupCode}-${material.materialGroupDesc}`}
+ key={`${material.materialGroupCode}-${material.materialGroupDescription}`}
variant="secondary"
className="gap-1 pr-1"
>
- <span className="max-w-[200px] truncate">
+ <span className="">
{material.displayText}
</span>
{!disabled && (
@@ -257,8 +262,17 @@ export function MaterialGroupSelector({
/>
</div>
- <CommandList>
- <div className="h-64 overflow-y-auto">
+ {/* 스크롤 컨테이너 + 고정 페이지네이션 */}
+ <div className="max-h-[50vh] flex flex-col">
+ <CommandList
+ className="flex-1 overflow-y-auto overflow-x-hidden min-h-0"
+ // shadcn CommandList 버그 처리 - 스크롤 이벤트 전파 차단
+ onWheel={(e) => {
+ e.stopPropagation(); // 이벤트 전파 차단
+ const target = e.currentTarget;
+ target.scrollTop += e.deltaY; // 직접 스크롤 처리
+ }}
+ >
{!searchQuery.trim() && !showInitialData ? (
<div className="p-4 text-center text-sm text-muted-foreground">
자재를 검색하려면 검색어를 입력해주세요.
@@ -278,27 +292,31 @@ export function MaterialGroupSelector({
) : searchResults.length === 0 ? (
<CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
) : (
- <CommandGroup>
+ <CommandGroup className="overflow-visible">
{searchResults.map((material) => {
const isExcluded = excludeMaterialCodes?.has(material.materialGroupCode);
const isSelected = isMaterialSelected(material);
-
+ const isMaxReached = maxSelections && selectedMaterials.length >= maxSelections && !isSelected;
+ const isDisabled = isExcluded || isMaxReached;
+
return (
<CommandItem
- key={`${material.materialGroupCode}-${material.materialGroupDesc}`}
+ key={`${material.materialGroupCode}-${material.materialGroupDescription}`}
onSelect={() => {
- if (!isExcluded) {
+ if (!isDisabled) {
handleMaterialSelect(material);
}
}}
className={cn(
"cursor-pointer",
- isExcluded && "opacity-50 cursor-not-allowed bg-muted"
+ isDisabled && "opacity-50 cursor-not-allowed bg-muted"
)}
>
<div className="mr-2 h-4 w-4 flex items-center justify-center">
{isExcluded ? (
<span className="text-xs text-muted-foreground">✓</span>
+ ) : isMaxReached ? (
+ <span className="text-xs text-muted-foreground">-</span>
) : (
<Check
className={cn(
@@ -311,17 +329,27 @@ export function MaterialGroupSelector({
<div className="flex-1">
<div className={cn(
"font-medium",
- isExcluded && "text-muted-foreground"
+ isDisabled && "text-muted-foreground"
)}>
- {material.materialGroupDesc}
+ {material.materialGroupDescription}
{isExcluded && (
<span className="ml-2 text-xs bg-red-100 text-red-600 px-2 py-1 rounded">
이미 등록됨
</span>
)}
+ {isMaxReached && (
+ <span className="ml-2 text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded">
+ 선택 제한 ({maxSelections}개)
+ </span>
+ )}
</div>
<div className="text-xs text-muted-foreground">
자재그룹코드: {material.materialGroupCode}
+ {material.materialGroupUom && (
+ <span className="ml-2">
+ | UOM: {material.materialGroupUom}
+ </span>
+ )}
</div>
</div>
</CommandItem>
@@ -329,11 +357,11 @@ export function MaterialGroupSelector({
})}
</CommandGroup>
)}
- </div>
+ </CommandList>
- {/* 페이지네이션 */}
+ {/* 고정 페이지네이션 - 항상 밑에 표시 */}
{searchResults.length > 0 && pagination.pageCount > 1 && (
- <div className="flex items-center justify-between border-t px-3 py-2">
+ <div className="flex items-center justify-between border-t px-3 py-2 flex-shrink-0">
<div className="text-xs text-muted-foreground">
총 {pagination.total}개 중 {((pagination.page - 1) * pagination.perPage) + 1}-
{Math.min(pagination.page * pagination.perPage, pagination.total)}개 표시
@@ -363,7 +391,7 @@ export function MaterialGroupSelector({
</div>
</div>
)}
- </CommandList>
+ </div>
</Command>
</PopoverContent>
</Popover>