feat: [process-management] 공정 품목 배정 정보 표시 + 중복 배정 품목 disabled 처리

- 품목 목록에 배정 공정 컬럼 추가 (현재 공정/다른 공정 구분 표시)
- 다른 공정 배정 품목은 disabled 처리 (선택 불가)
- ItemOption 타입에 assignedProcesses 추가
This commit is contained in:
2026-03-18 23:18:54 +09:00
parent c9e1238e7d
commit 0b89c99111
2 changed files with 78 additions and 44 deletions

View File

@@ -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>

View File

@@ -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,
})),
}));
}