feat: [품질관리] 수주선택 모달 발주처별 비활성화 제약 추가
- SearchableSelectionModal에 isItemDisabled 콜백 prop 추가 (공통) - renderItem에 isDisabled 3번째 파라미터 전달 (하위호환) - disabled 아이템 클릭 차단 + opacity/cursor 스타일 적용 - 전체선택 시 disabled 아이템 제외 - OrderSelectModal: 선택된 발주처와 다른 발주처의 수주 비활성화 - 이미 선택된 아이템은 해제 가능 (disabled 예외)
This commit is contained in:
@@ -38,6 +38,7 @@ export function SearchableSelectionModal<T>(props: SearchableSelectionModalProps
|
||||
listWrapper,
|
||||
infoText,
|
||||
mode,
|
||||
isItemDisabled,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
@@ -88,15 +89,20 @@ export function SearchableSelectionModal<T>(props: SearchableSelectionModalProps
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 전체선택 토글
|
||||
// 전체선택 토글 (비활성 아이템 제외)
|
||||
const handleToggleAll = useCallback(() => {
|
||||
const targetItems = isItemDisabled
|
||||
? items.filter((item) => !isItemDisabled(item, items.filter((i) => selectedIds.has(keyExtractor(i)))))
|
||||
: items;
|
||||
setSelectedIds((prev) => {
|
||||
if (prev.size === items.length) {
|
||||
const targetIds = targetItems.map((item) => keyExtractor(item));
|
||||
const allSelected = targetIds.every((id) => prev.has(id));
|
||||
if (allSelected) {
|
||||
return new Set();
|
||||
}
|
||||
return new Set(items.map((item) => keyExtractor(item)));
|
||||
return new Set(targetIds);
|
||||
});
|
||||
}, [items, keyExtractor]);
|
||||
}, [items, keyExtractor, isItemDisabled, selectedIds]);
|
||||
|
||||
// 다중선택 확인
|
||||
const handleConfirm = useCallback(() => {
|
||||
@@ -107,16 +113,34 @@ export function SearchableSelectionModal<T>(props: SearchableSelectionModalProps
|
||||
}
|
||||
}, [mode, items, selectedIds, keyExtractor, props, onOpenChange]);
|
||||
|
||||
// 선택된 아이템 목록 (isItemDisabled 콜백용)
|
||||
const selectedItems = useCallback(() => {
|
||||
return items.filter((item) => selectedIds.has(keyExtractor(item)));
|
||||
}, [items, selectedIds, keyExtractor]);
|
||||
|
||||
// 비활성 판정
|
||||
const checkDisabled = useCallback((item: T) => {
|
||||
if (!isItemDisabled) return false;
|
||||
// 이미 선택된 아이템은 disabled가 아님 (해제 가능해야 함)
|
||||
if (selectedIds.has(keyExtractor(item))) return false;
|
||||
return isItemDisabled(item, selectedItems());
|
||||
}, [isItemDisabled, selectedIds, keyExtractor, selectedItems]);
|
||||
|
||||
// 클릭 핸들러: 모드에 따라 분기
|
||||
const handleItemClick = useCallback((item: T) => {
|
||||
if (checkDisabled(item)) return;
|
||||
if (mode === 'single') {
|
||||
handleSingleSelect(item);
|
||||
} else {
|
||||
handleToggle(keyExtractor(item));
|
||||
}
|
||||
}, [mode, handleSingleSelect, handleToggle, keyExtractor]);
|
||||
}, [mode, handleSingleSelect, handleToggle, keyExtractor, checkDisabled]);
|
||||
|
||||
const isAllSelected = items.length > 0 && selectedIds.size === items.length;
|
||||
// 전체선택 (비활성 아이템 제외)
|
||||
const enabledItems = isItemDisabled
|
||||
? items.filter((item) => !checkDisabled(item))
|
||||
: items;
|
||||
const isAllSelected = enabledItems.length > 0 && enabledItems.every((item) => selectedIds.has(keyExtractor(item)));
|
||||
const isSelected = (item: T) => selectedIds.has(keyExtractor(item));
|
||||
|
||||
// 빈 상태 메시지 결정
|
||||
@@ -158,7 +182,8 @@ export function SearchableSelectionModal<T>(props: SearchableSelectionModalProps
|
||||
|
||||
const itemElements = items.map((item) => {
|
||||
const key = keyExtractor(item);
|
||||
const rendered = renderItem(item, isSelected(item));
|
||||
const disabled = checkDisabled(item);
|
||||
const rendered = renderItem(item, isSelected(item), disabled);
|
||||
|
||||
// renderItem이 유효한 React 엘리먼트를 반환하면 key와 onClick을 직접 주입 (div 래핑 없이)
|
||||
// 이렇게 하면 <TableRow> 등 테이블 요소를 <div>로 감싸는 HTML 유효성 에러를 방지
|
||||
@@ -166,20 +191,27 @@ export function SearchableSelectionModal<T>(props: SearchableSelectionModalProps
|
||||
return cloneElement(rendered as React.ReactElement<Record<string, unknown>>, {
|
||||
key,
|
||||
onClick: (e: React.MouseEvent) => {
|
||||
// 기존 onClick이 있으면 먼저 호출
|
||||
if (disabled) return;
|
||||
const existingOnClick = (rendered.props as Record<string, unknown>)?.onClick;
|
||||
if (typeof existingOnClick === 'function') {
|
||||
(existingOnClick as (e: React.MouseEvent) => void)(e);
|
||||
}
|
||||
handleItemClick(item);
|
||||
},
|
||||
className: `cursor-pointer ${(rendered.props as Record<string, unknown>)?.className || ''}`.trim(),
|
||||
className: [
|
||||
disabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer',
|
||||
(rendered.props as Record<string, unknown>)?.className || '',
|
||||
].filter(Boolean).join(' '),
|
||||
});
|
||||
}
|
||||
|
||||
// 일반 텍스트/fragment인 경우 기존 div 래핑 유지
|
||||
return (
|
||||
<div key={key} onClick={() => handleItemClick(item)} className="cursor-pointer">
|
||||
<div
|
||||
key={key}
|
||||
onClick={disabled ? undefined : () => handleItemClick(item)}
|
||||
className={disabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'}
|
||||
>
|
||||
{rendered}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,8 +17,10 @@ interface BaseProps<T> {
|
||||
fetchData: (query: string) => Promise<T[]>;
|
||||
/** 고유 키 추출 */
|
||||
keyExtractor: (item: T) => string;
|
||||
/** 아이템 렌더링 */
|
||||
renderItem: (item: T, isSelected: boolean) => ReactNode;
|
||||
/** 아이템 렌더링 (isDisabled: 비활성 상태) */
|
||||
renderItem: (item: T, isSelected: boolean, isDisabled?: boolean) => ReactNode;
|
||||
/** 아이템 비활성 조건 (선택된 아이템 목록 기반) */
|
||||
isItemDisabled?: (item: T, selectedItems: T[]) => boolean;
|
||||
|
||||
// 검색 설정
|
||||
/** 검색 모드: debounce(자동) vs enter(수동) */
|
||||
|
||||
@@ -70,6 +70,11 @@ export function OrderSelectModal({
|
||||
onSelect={onSelect}
|
||||
confirmLabel="선택"
|
||||
allowSelectAll
|
||||
isItemDisabled={(item, selectedItems) => {
|
||||
if (selectedItems.length === 0) return false;
|
||||
const selectedClient = selectedItems[0].clientName;
|
||||
return item.clientName !== selectedClient;
|
||||
}}
|
||||
listWrapper={(children, selectState) => (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -95,10 +100,10 @@ export function OrderSelectModal({
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
renderItem={(item, isSelected) => (
|
||||
<TableRow className="cursor-pointer hover:bg-muted/50">
|
||||
renderItem={(item, isSelected, isDisabled) => (
|
||||
<TableRow className={isDisabled ? '' : 'hover:bg-muted/50'}>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox checked={isSelected} />
|
||||
<Checkbox checked={isSelected} disabled={isDisabled} />
|
||||
</TableCell>
|
||||
<TableCell>{item.orderNumber}</TableCell>
|
||||
<TableCell>{item.siteName}</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user