From fe930b5831f47c6ecc7e864e2e32980f09fec099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Thu, 5 Mar 2026 23:15:17 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=ED=92=88=EC=A7=88=EA=B4=80=EB=A6=AC]?= =?UTF-8?q?=20=EC=88=98=EC=A3=BC=EC=84=A0=ED=83=9D=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EB=B0=9C=EC=A3=BC=EC=B2=98=EB=B3=84=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94=20=EC=A0=9C=EC=95=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SearchableSelectionModal에 isItemDisabled 콜백 prop 추가 (공통) - renderItem에 isDisabled 3번째 파라미터 전달 (하위호환) - disabled 아이템 클릭 차단 + opacity/cursor 스타일 적용 - 전체선택 시 disabled 아이템 제외 - OrderSelectModal: 선택된 발주처와 다른 발주처의 수주 비활성화 - 이미 선택된 아이템은 해제 가능 (disabled 예외) --- .../SearchableSelectionModal.tsx | 52 +++++++++++++++---- .../SearchableSelectionModal/types.ts | 6 ++- .../InspectionManagement/OrderSelectModal.tsx | 11 ++-- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/components/organisms/SearchableSelectionModal/SearchableSelectionModal.tsx b/src/components/organisms/SearchableSelectionModal/SearchableSelectionModal.tsx index 84ebddbb..ece26258 100644 --- a/src/components/organisms/SearchableSelectionModal/SearchableSelectionModal.tsx +++ b/src/components/organisms/SearchableSelectionModal/SearchableSelectionModal.tsx @@ -38,6 +38,7 @@ export function SearchableSelectionModal(props: SearchableSelectionModalProps listWrapper, infoText, mode, + isItemDisabled, } = props; const { @@ -88,15 +89,20 @@ export function SearchableSelectionModal(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(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(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 래핑 없이) // 이렇게 하면 등 테이블 요소를
로 감싸는 HTML 유효성 에러를 방지 @@ -166,20 +191,27 @@ export function SearchableSelectionModal(props: SearchableSelectionModalProps return cloneElement(rendered as React.ReactElement>, { key, onClick: (e: React.MouseEvent) => { - // 기존 onClick이 있으면 먼저 호출 + if (disabled) return; const existingOnClick = (rendered.props as Record)?.onClick; if (typeof existingOnClick === 'function') { (existingOnClick as (e: React.MouseEvent) => void)(e); } handleItemClick(item); }, - className: `cursor-pointer ${(rendered.props as Record)?.className || ''}`.trim(), + className: [ + disabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer', + (rendered.props as Record)?.className || '', + ].filter(Boolean).join(' '), }); } // 일반 텍스트/fragment인 경우 기존 div 래핑 유지 return ( -
handleItemClick(item)} className="cursor-pointer"> +
handleItemClick(item)} + className={disabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'} + > {rendered}
); diff --git a/src/components/organisms/SearchableSelectionModal/types.ts b/src/components/organisms/SearchableSelectionModal/types.ts index 8e42df8c..e5bfcb8c 100644 --- a/src/components/organisms/SearchableSelectionModal/types.ts +++ b/src/components/organisms/SearchableSelectionModal/types.ts @@ -17,8 +17,10 @@ interface BaseProps { fetchData: (query: string) => Promise; /** 고유 키 추출 */ 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(수동) */ diff --git a/src/components/quality/InspectionManagement/OrderSelectModal.tsx b/src/components/quality/InspectionManagement/OrderSelectModal.tsx index db56d4a7..8047386c 100644 --- a/src/components/quality/InspectionManagement/OrderSelectModal.tsx +++ b/src/components/quality/InspectionManagement/OrderSelectModal.tsx @@ -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) => ( @@ -95,10 +100,10 @@ export function OrderSelectModal({
)} - renderItem={(item, isSelected) => ( - + renderItem={(item, isSelected, isDisabled) => ( + e.stopPropagation()}> - + {item.orderNumber} {item.siteName}