fix(WEB): 공정관리 개별 품목 저장 안되는 버그 수정
- selectedItemCodes → selectedItemIds로 변경 - item.code 대신 item.id 사용하여 API에 올바른 ID 전달 - 검색어 유효성 검사 추가 (한글 1자, 영문 2자 이상) - 품목 조회 size 100 → 1000으로 변경
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user