diff --git a/src/components/process-management/RuleModal.tsx b/src/components/process-management/RuleModal.tsx index d8fd37f0..8ce0f161 100644 --- a/src/components/process-management/RuleModal.tsx +++ b/src/components/process-management/RuleModal.tsx @@ -74,7 +74,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp // 개별 품목용 상태 const [searchKeyword, setSearchKeyword] = useState(''); const [selectedItemType, setSelectedItemType] = useState('all'); - const [selectedItemCodes, setSelectedItemCodes] = useState>(new Set()); + const [selectedItemIds, setSelectedItemIds] = useState>(new Set()); // 품목 목록 API 상태 const [itemList, setItemList] = useState([]); @@ -86,16 +86,35 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp const items = await getItemList({ q: q || undefined, itemType: itemType === 'all' ? undefined : itemType, - size: 100, + size: 1000, // 전체 품목 조회 }); setItemList(items); setIsItemsLoading(false); }, []); + // 검색어 유효성 검사 함수 + const isValidSearchKeyword = (keyword: string): boolean => { + if (!keyword || keyword.trim() === '') return false; + + const trimmed = keyword.trim(); + // 한글이 포함되어 있으면 1자 이상 + const hasKorean = /[가-힣]/.test(trimmed); + if (hasKorean) return trimmed.length >= 1; + + // 영어/숫자만 있으면 2자 이상 + return trimmed.length >= 2; + }; + // 검색어/품목유형 변경 시 API 호출 (debounce) useEffect(() => { if (registrationType !== 'individual') return; + // 검색어 유효성 검사 - 유효하지 않으면 빈 목록 + if (!isValidSearchKeyword(searchKeyword)) { + setItemList([]); + return; + } + const timer = setTimeout(() => { loadItems(searchKeyword, selectedItemType); }, 300); @@ -103,21 +122,30 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp return () => clearTimeout(timer); }, [searchKeyword, selectedItemType, registrationType, loadItems]); - // 모달 열릴 때 품목 목록 초기 로드 + // 품목유형 변경 시 검색어가 유효하면 재검색 + useEffect(() => { + if (registrationType !== 'individual') return; + if (!isValidSearchKeyword(searchKeyword)) return; + + loadItems(searchKeyword, selectedItemType); + }, [selectedItemType]); + + // 모달 열릴 때 품목 목록 초기화 (초기 로드 안함) useEffect(() => { if (open && registrationType === 'individual') { - loadItems('', 'all'); + setItemList([]); + setSearchKeyword(''); } - }, [open, registrationType, loadItems]); + }, [open, registrationType]); // 체크박스 토글 - const handleToggleItem = (code: string) => { - setSelectedItemCodes((prev) => { + const handleToggleItem = (id: string) => { + setSelectedItemIds((prev) => { const newSet = new Set(prev); - if (newSet.has(code)) { - newSet.delete(code); + if (newSet.has(id)) { + newSet.delete(id); } else { - newSet.add(code); + newSet.add(id); } return newSet; }); @@ -125,13 +153,13 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp // 전체 선택 const handleSelectAll = () => { - const allCodes = itemList.map((item) => item.code); - setSelectedItemCodes(new Set(allCodes)); + const allIds = itemList.map((item) => item.id); + setSelectedItemIds(new Set(allIds)); }; // 초기화 const handleResetSelection = () => { - setSelectedItemCodes(new Set()); + setSelectedItemIds(new Set()); }; // 모달 열릴 때 초기화 또는 수정 데이터 로드 @@ -149,12 +177,12 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp setSearchKeyword(''); setSelectedItemType('all'); - // 개별 품목인 경우 선택된 품목 코드 설정 + // 개별 품목인 경우 선택된 품목 ID 설정 if (editRule.registrationType === 'individual') { - const codes = editRule.conditionValue.split(',').filter(Boolean); - setSelectedItemCodes(new Set(codes)); + const ids = editRule.conditionValue.split(',').filter(Boolean); + setSelectedItemIds(new Set(ids)); } else { - setSelectedItemCodes(new Set()); + setSelectedItemIds(new Set()); } } else { // 추가 모드: 초기화 @@ -167,7 +195,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp setIsActive(true); setSearchKeyword(''); setSelectedItemType('all'); - setSelectedItemCodes(new Set()); + setSelectedItemIds(new Set()); } } }, [open, editRule]); @@ -179,7 +207,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp return; } } else { - if (selectedItemCodes.size === 0) { + if (selectedItemIds.size === 0) { alert('품목을 최소 1개 이상 선택해주세요.'); return; } @@ -188,7 +216,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp // 개별 품목의 경우 conditionValue에 품목코드들을 저장 const finalConditionValue = registrationType === 'individual' - ? Array.from(selectedItemCodes).join(',') + ? Array.from(selectedItemIds).join(',') : conditionValue.trim(); onAdd({ @@ -211,7 +239,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp setIsActive(true); setSearchKeyword(''); setSelectedItemType('all'); - setSelectedItemCodes(new Set()); + setSelectedItemIds(new Set()); onOpenChange(false); }; @@ -389,7 +417,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp {isItemsLoading ? ( '로딩 중...' ) : ( - <>품목 목록 ({itemList.length}개) | 선택됨 ({selectedItemCodes.size}개) + <>품목 목록 ({itemList.length}개) | 선택됨 ({selectedItemIds.size}개) )} @@ -411,7 +439,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp size="sm" onClick={handleResetSelection} className="text-xs h-7" - disabled={selectedItemCodes.size === 0} + disabled={selectedItemIds.size === 0} > 초기화 @@ -439,7 +467,9 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp ) : itemList.length === 0 ? ( - 검색 결과가 없습니다 + {searchKeyword.trim() === '' + ? '품목을 검색해주세요 (한글 1자 이상, 영문 2자 이상)' + : '검색 결과가 없습니다'} ) : ( @@ -447,12 +477,12 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp handleToggleItem(item.code)} + onClick={() => handleToggleItem(item.id)} > handleToggleItem(item.code)} + checked={selectedItemIds.has(item.id)} + onCheckedChange={() => handleToggleItem(item.id)} onClick={(e) => e.stopPropagation()} />