feat: [process-management] 공정 품목 배정 정보 표시 + 중복 배정 품목 disabled 처리
- 품목 목록에 배정 공정 컬럼 추가 (현재 공정/다른 공정 구분 표시) - 다른 공정 배정 품목은 disabled 처리 (선택 불가) - ItemOption 타입에 assignedProcesses 추가
This commit is contained in:
@@ -100,13 +100,9 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule, processId, proc
|
||||
size: 1000,
|
||||
excludeProcessId: processId,
|
||||
});
|
||||
// 이미 등록된 품목 필터링
|
||||
const filtered = registeredItemIds && registeredItemIds.size > 0
|
||||
? items.filter((item) => !registeredItemIds.has(item.id))
|
||||
: items;
|
||||
setItemList(filtered);
|
||||
setItemList(items);
|
||||
setIsItemsLoading(false);
|
||||
}, [processId, registeredItemIds]);
|
||||
}, [processId]);
|
||||
|
||||
// 검색어 유효성 검사 함수
|
||||
const isValidSearchKeyword = (keyword: string): boolean => {
|
||||
@@ -164,8 +160,38 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule, processId, proc
|
||||
setCategoryFilter('all');
|
||||
}, [processFilter]);
|
||||
|
||||
// 품목이 다른 공정에 배정되었는지 확인 (현재 공정 제외)
|
||||
const isAssignedToOtherProcess = useCallback((item: ItemOption): boolean => {
|
||||
if (!item.assignedProcesses?.length) return false;
|
||||
if (!processId) return item.assignedProcesses.length > 0;
|
||||
return item.assignedProcesses.some((ap) => String(ap.processId) !== processId);
|
||||
}, [processId]);
|
||||
|
||||
// 이미 등록된 품목인지 확인
|
||||
const isAlreadyRegistered = useCallback((item: ItemOption): boolean => {
|
||||
return registeredItemIds?.has(item.id) ?? false;
|
||||
}, [registeredItemIds]);
|
||||
|
||||
// 품목 선택 불가 여부
|
||||
const isItemDisabled = useCallback((item: ItemOption): boolean => {
|
||||
return isAssignedToOtherProcess(item) || isAlreadyRegistered(item);
|
||||
}, [isAssignedToOtherProcess, isAlreadyRegistered]);
|
||||
|
||||
// 배정된 공정명 (현재 공정 제외)
|
||||
const getAssignedProcessName = useCallback((item: ItemOption): string | null => {
|
||||
if (!item.assignedProcesses?.length) return null;
|
||||
const otherProcesses = processId
|
||||
? item.assignedProcesses.filter((ap) => String(ap.processId) !== processId)
|
||||
: item.assignedProcesses;
|
||||
if (otherProcesses.length === 0) return null;
|
||||
return otherProcesses.map((ap) => ap.processName).join(', ');
|
||||
}, [processId]);
|
||||
|
||||
// 체크박스 토글
|
||||
const handleToggleItem = (id: string) => {
|
||||
const item = itemList.find((i) => i.id === id);
|
||||
if (item && isItemDisabled(item)) return;
|
||||
|
||||
setSelectedItemIds((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(id)) {
|
||||
@@ -285,50 +311,58 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule, processId, proc
|
||||
<TableHead className="w-[80px]">품목유형</TableHead>
|
||||
<TableHead>품목코드</TableHead>
|
||||
<TableHead>품목명</TableHead>
|
||||
{/* TODO: 백엔드 API에서 process_name, process_category 응답 지원 후 공정/구분 컬럼 활성화
|
||||
<TableHead className="w-[80px]">공정</TableHead>
|
||||
<TableHead className="w-[80px]">구분</TableHead>
|
||||
*/}
|
||||
<TableHead className="w-[100px]">배정 공정</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isItemsLoading ? (
|
||||
<TableRow key="loading">
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground py-8">
|
||||
<TableCell colSpan={5} className="text-center text-muted-foreground py-8">
|
||||
품목 목록을 불러오는 중...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : itemList.length === 0 ? (
|
||||
<TableRow key="empty">
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground py-8">
|
||||
<TableCell colSpan={5} className="text-center text-muted-foreground py-8">
|
||||
{searchKeyword.trim() === ''
|
||||
? '품목을 검색해주세요 (한글 1자 이상, 영문 2자 이상)'
|
||||
: '검색 결과가 없습니다'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
itemList.map((item) => (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => handleToggleItem(item.id)}
|
||||
>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedItemIds.has(item.id)}
|
||||
onCheckedChange={() => handleToggleItem(item.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{item.type}</TableCell>
|
||||
<TableCell className="font-medium">{item.code}</TableCell>
|
||||
<TableCell>{item.fullName}</TableCell>
|
||||
{/* TODO: 백엔드 API 지원 후 item.processName / item.processCategory 표시
|
||||
<TableCell className="text-muted-foreground">{item.processName || '-'}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{item.processCategory || '-'}</TableCell>
|
||||
*/}
|
||||
</TableRow>
|
||||
))
|
||||
itemList.map((item) => {
|
||||
const disabled = isItemDisabled(item);
|
||||
const assignedName = getAssignedProcessName(item);
|
||||
const isRegistered = isAlreadyRegistered(item);
|
||||
return (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className={disabled ? 'opacity-50' : 'cursor-pointer hover:bg-muted/50'}
|
||||
onClick={() => !disabled && handleToggleItem(item.id)}
|
||||
>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedItemIds.has(item.id)}
|
||||
onCheckedChange={() => handleToggleItem(item.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{item.type}</TableCell>
|
||||
<TableCell className="font-medium">{item.code}</TableCell>
|
||||
<TableCell>{item.fullName}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{isRegistered ? (
|
||||
<span className="text-blue-500">현재 공정</span>
|
||||
) : assignedName ? (
|
||||
<span className="text-orange-500">{assignedName}</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -483,29 +483,25 @@ export interface ItemOption {
|
||||
id: string;
|
||||
fullName: string;
|
||||
type: string;
|
||||
// TODO: API 응답에 process_name, process_category 필드 추가 후 활성화
|
||||
processName?: string;
|
||||
processCategory?: string;
|
||||
/** 배정된 공정 목록 */
|
||||
assignedProcesses?: Array<{ processId: number; processName: string }>;
|
||||
}
|
||||
|
||||
interface GetItemListParams {
|
||||
q?: string;
|
||||
itemType?: string;
|
||||
size?: number;
|
||||
/** 해당 공정 외 다른 공정에 이미 배정된 품목 제외 (공정 ID) */
|
||||
/** 공정 배정 정보 포함을 위한 현재 공정 ID */
|
||||
excludeProcessId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 목록 조회 (분류 규칙용)
|
||||
* - excludeProcessId: 다른 공정에 이미 배정된 품목 제외 (중복 방지)
|
||||
*
|
||||
* TODO: 백엔드 API 수정 요청
|
||||
* - 응답에 process_name, process_category 필드 추가 필요 (공정 품목 선택 팝업에서 공정/구분 컬럼 표시용)
|
||||
* - 파라미터에 processName, processCategory 필터 추가 필요 (공정/구분 필터링용)
|
||||
* - excludeProcessId: 공정 배정 정보를 포함하여 응답 (다른 공정 배정 품목은 disabled 처리용)
|
||||
*/
|
||||
export async function getItemList(params?: GetItemListParams): Promise<ItemOption[]> {
|
||||
interface ItemListResponse { data: Array<{ id: number; name: string; item_code?: string; item_type?: string; item_type_name?: string }> }
|
||||
interface AssignedProcess { process_id: number; process_name: string }
|
||||
interface ItemListResponse { data: Array<{ id: number; name: string; item_code?: string; item_type?: string; item_type_name?: string; assigned_processes?: AssignedProcess[] }> }
|
||||
const result = await executeServerAction<ItemListResponse>({
|
||||
url: buildApiUrl('/api/v1/items', {
|
||||
size: params?.size || 1000,
|
||||
@@ -523,6 +519,10 @@ export async function getItemList(params?: GetItemListParams): Promise<ItemOptio
|
||||
id: String(item.id),
|
||||
fullName: item.name,
|
||||
type: item.item_type_name || item.item_type || '',
|
||||
assignedProcesses: item.assigned_processes?.map((ap) => ({
|
||||
processId: ap.process_id,
|
||||
processName: ap.process_name,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user