fix(WEB): 공정관리 개별 품목 저장 안되는 버그 수정

- selectedItemCodes → selectedItemIds로 변경
- item.code 대신 item.id 사용하여 API에 올바른 ID 전달
- 검색어 유효성 검사 추가 (한글 1자, 영문 2자 이상)
- 품목 조회 size 100 → 1000으로 변경
This commit is contained in:
2026-01-08 20:20:08 +09:00
parent 3d2dea6118
commit d797868c17

View File

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