fix: [production] 자재투입 모달 — bomGroupKey 그룹핑, 카테고리 순서 정렬, 번호 표시

- bomGroupKey 기반 그룹핑 (같은 item_id라도 category+partType별 분리)
- 카테고리 순서 정렬 (가이드레일→하단마감재→셔터박스→연기차단재)
- 카테고리 내 원형번호(①②③) 표시
- partType 배지 추가
- MaterialForItemInput에 bomGroupKey 필드 추가
This commit is contained in:
2026-03-03 20:58:10 +09:00
parent 5ff5093d7b
commit 8bcabafd08
2 changed files with 30 additions and 4 deletions

View File

@@ -99,16 +99,22 @@ export function MaterialInputModal({
const getLotKey = (material: MaterialForInput) =>
String(material.stockLotId ?? `item-${material.itemId}`);
// 품목별 그룹핑
// 품목별 그룹핑 (BOM 엔트리별 고유키 사용 — 같은 item_id라도 category+partType 다르면 별도 그룹)
const materialGroups: MaterialGroup[] = useMemo(() => {
// dynamic_bom 항목은 (itemId, workOrderItemId) 쌍으로 그룹핑
const groups = new Map<string, MaterialForInput[]>();
for (const m of materials) {
const groupKey = m.workOrderItemId ? `${m.itemId}_${m.workOrderItemId}` : String(m.itemId);
const itemInput = m as unknown as MaterialForItemInput;
const groupKey = itemInput.bomGroupKey
?? (m.workOrderItemId ? `${m.itemId}_${m.workOrderItemId}` : String(m.itemId));
const existing = groups.get(groupKey) || [];
existing.push(m);
groups.set(groupKey, existing);
}
// 작업일지와 동일한 카테고리 순서
const categoryOrder: Record<string, number> = {
guideRail: 0, bottomBar: 1, shutterBox: 2, smokeBarrier: 3,
};
return Array.from(groups.entries()).map(([groupKey, lots]) => {
const first = lots[0];
const itemInput = first as unknown as MaterialForItemInput;
@@ -129,6 +135,10 @@ export function MaterialInputModal({
partType: first.partType,
category: first.category,
};
}).sort((a, b) => {
const catA = categoryOrder[a.category ?? ''] ?? 99;
const catB = categoryOrder[b.category ?? ''] ?? 99;
return catA - catB;
});
}, [materials]);
@@ -344,7 +354,15 @@ export function MaterialInputModal({
</div>
) : (
<div className="space-y-4 flex-1 overflow-y-auto min-h-0">
{materialGroups.map((group) => {
{materialGroups.map((group, groupIdx) => {
// 같은 카테고리 내 순번 계산 (①②③...)
const categoryIndex = group.category
? materialGroups.slice(0, groupIdx).filter(g => g.category === group.category).length
: -1;
const circledNumbers = ['①','②','③','④','⑤','⑥','⑦','⑧','⑨','⑩'];
const circledNum = categoryIndex >= 0 && categoryIndex < circledNumbers.length
? circledNumbers[categoryIndex] : '';
const groupAllocated = group.lots.reduce(
(sum, lot) => sum + (allocations.get(getLotKey(lot)) || 0),
0
@@ -372,6 +390,11 @@ export function MaterialInputModal({
group.category}
</span>
)}
{group.partType && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-gray-200 text-gray-600 font-medium">
{circledNum}{group.partType}
</span>
)}
<span className="text-sm font-semibold text-gray-900">
{group.materialName}
</span>

View File

@@ -316,6 +316,7 @@ export async function registerMaterialInput(
export interface MaterialForItemInput extends MaterialForInput {
alreadyInputted: number; // 이미 투입된 수량
remainingRequiredQty: number; // 남은 필요 수량
bomGroupKey?: string; // BOM 엔트리별 고유 그룹키 (category+partType 기반)
}
export async function getMaterialsForItem(
@@ -336,6 +337,7 @@ export async function getMaterialsForItem(
receipt_date: string | null; supplier: string | null;
// dynamic_bom 추가 필드
work_order_item_id?: number; lot_prefix?: string; part_type?: string; category?: string;
bom_group_key?: string;
}
const result = await executeServerAction<MaterialItemApiItem[]>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/materials`,
@@ -354,6 +356,7 @@ export async function getMaterialsForItem(
remainingRequiredQty: item.remaining_required_qty,
workOrderItemId: item.work_order_item_id, lotPrefix: item.lot_prefix,
partType: item.part_type, category: item.category,
bomGroupKey: item.bom_group_key,
})),
};
}