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 [selectedItemType, setSelectedItemType] = useState('all');
const [selectedItemCodes, setSelectedItemCodes] = useState<Set<string>>(new Set());
const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(new Set());
// 품목 목록 API 상태
const [itemList, setItemList] = useState<ItemOption[]>([]);
@@ -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})</>
)}
</span>
</div>
@@ -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}
>
</Button>
@@ -439,7 +467,9 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp
) : itemList.length === 0 ? (
<TableRow key="empty">
<TableCell colSpan={4} className="text-center text-muted-foreground py-8">
{searchKeyword.trim() === ''
? '품목을 검색해주세요 (한글 1자 이상, 영문 2자 이상)'
: '검색 결과가 없습니다'}
</TableCell>
</TableRow>
) : (
@@ -447,12 +477,12 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp
<TableRow
key={item.id}
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleToggleItem(item.code)}
onClick={() => handleToggleItem(item.id)}
>
<TableCell>
<Checkbox
checked={selectedItemCodes.has(item.code)}
onCheckedChange={() => handleToggleItem(item.code)}
checked={selectedItemIds.has(item.id)}
onCheckedChange={() => handleToggleItem(item.id)}
onClick={(e) => e.stopPropagation()}
/>
</TableCell>