feat: Allow duplicate items at the same level in BOM management

- Updated the logic in the BomManagementPage to permit the registration of duplicate items at the same level, enabling separate rows for items with different requirements or processes.
- Removed the previous check for duplicate items at the same level, enhancing flexibility in item management within the BOM structure.
This commit is contained in:
kjs
2026-04-22 18:09:57 +09:00
parent bf58ce3c07
commit ad1180daa5
8 changed files with 35 additions and 63 deletions

View File

@@ -834,14 +834,7 @@ export default function BomManagementPage() {
return;
}
// 같은 레벨(같은 부모) 중복 품목 체크
const siblings = addTargetParentId
? (findNodeById(editingTree, addTargetParentId)?.children || [])
: editingTree;
if (siblings.some((n) => n.child_item_id === item.id)) {
toast.error("같은 레벨에 이미 동일 품목이 존재합니다");
return;
}
// 같은 레벨 중복 허용 — 소요량/공정 등이 다른 동일 품목을 별도 row로 등록할 수 있음
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const parentNode = addTargetParentId ? findNodeById(editingTree, addTargetParentId) : null;

View File

@@ -834,14 +834,7 @@ export default function BomManagementPage() {
return;
}
// 같은 레벨(같은 부모) 중복 품목 체크
const siblings = addTargetParentId
? (findNodeById(editingTree, addTargetParentId)?.children || [])
: editingTree;
if (siblings.some((n) => n.child_item_id === item.id)) {
toast.error("같은 레벨에 이미 동일 품목이 존재합니다");
return;
}
// 같은 레벨 중복 허용 — 소요량/공정 등이 다른 동일 품목을 별도 row로 등록할 수 있음
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const parentNode = addTargetParentId ? findNodeById(editingTree, addTargetParentId) : null;

View File

@@ -834,14 +834,7 @@ export default function BomManagementPage() {
return;
}
// 같은 레벨(같은 부모) 중복 품목 체크
const siblings = addTargetParentId
? (findNodeById(editingTree, addTargetParentId)?.children || [])
: editingTree;
if (siblings.some((n) => n.child_item_id === item.id)) {
toast.error("같은 레벨에 이미 동일 품목이 존재합니다");
return;
}
// 같은 레벨 중복 허용 — 소요량/공정 등이 다른 동일 품목을 별도 row로 등록할 수 있음
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const parentNode = addTargetParentId ? findNodeById(editingTree, addTargetParentId) : null;

View File

@@ -841,14 +841,7 @@ export default function BomManagementPage() {
return;
}
// 같은 레벨(같은 부모) 중복 품목 체크
const siblings = addTargetParentId
? (findNodeById(editingTree, addTargetParentId)?.children || [])
: editingTree;
if (siblings.some((n) => n.child_item_id === item.id)) {
toast.error("같은 레벨에 이미 동일 품목이 존재합니다");
return;
}
// 같은 레벨 중복 허용 — 소요량/공정 등이 다른 동일 품목을 별도 row로 등록할 수 있음
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const parentNode = addTargetParentId ? findNodeById(editingTree, addTargetParentId) : null;

View File

@@ -834,14 +834,7 @@ export default function BomManagementPage() {
return;
}
// 같은 레벨(같은 부모) 중복 품목 체크
const siblings = addTargetParentId
? (findNodeById(editingTree, addTargetParentId)?.children || [])
: editingTree;
if (siblings.some((n) => n.child_item_id === item.id)) {
toast.error("같은 레벨에 이미 동일 품목이 존재합니다");
return;
}
// 같은 레벨 중복 허용 — 소요량/공정 등이 다른 동일 품목을 별도 row로 등록할 수 있음
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const parentNode = addTargetParentId ? findNodeById(editingTree, addTargetParentId) : null;

View File

@@ -834,14 +834,7 @@ export default function BomManagementPage() {
return;
}
// 같은 레벨(같은 부모) 중복 품목 체크
const siblings = addTargetParentId
? (findNodeById(editingTree, addTargetParentId)?.children || [])
: editingTree;
if (siblings.some((n) => n.child_item_id === item.id)) {
toast.error("같은 레벨에 이미 동일 품목이 존재합니다");
return;
}
// 같은 레벨 중복 허용 — 소요량/공정 등이 다른 동일 품목을 별도 row로 등록할 수 있음
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const parentNode = addTargetParentId ? findNodeById(editingTree, addTargetParentId) : null;

View File

@@ -841,14 +841,7 @@ export default function BomManagementPage() {
return;
}
// 같은 레벨(같은 부모) 중복 품목 체크
const siblings = addTargetParentId
? (findNodeById(editingTree, addTargetParentId)?.children || [])
: editingTree;
if (siblings.some((n) => n.child_item_id === item.id)) {
toast.error("같은 레벨에 이미 동일 품목이 존재합니다");
return;
}
// 같은 레벨 중복 허용 — 소요량/공정 등이 다른 동일 품목을 별도 row로 등록할 수 있음
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const parentNode = addTargetParentId ? findNodeById(editingTree, addTargetParentId) : null;

View File

@@ -131,13 +131,33 @@ export function DetailFormModal({
}, [selectedItemCode]);
// BOM 자재 로드 완료 후 체크 상태 초기화
// bomChecked 키는 BOM detail 고유 id(mat.id) 기준 — 동일 child_item_id가 여러 row로
// 있어도 독립적으로 체크되도록 함. legacy 저장값은 child_item_id로 저장되어 있을 수
// 있으므로 폴백 매핑으로 복원.
useEffect(() => {
if (!open || bomMaterials.length === 0) return;
if (mode === "edit" && editData?.selected_bom_items) {
const savedBom = editData.selected_bom_items;
const parsedBom = typeof savedBom === "string" ? JSON.parse(savedBom) : savedBom;
if (Array.isArray(parsedBom)) {
setBomChecked(new Set(parsedBom));
const matIds = new Set(bomMaterials.map((m) => m.id));
const restored = new Set<string>();
const seenChildForLegacy = new Set<string>();
for (const saved of parsedBom) {
if (matIds.has(saved)) {
restored.add(saved); // 신규 포맷: bom_detail id 저장값
} else {
// legacy 폴백: child_item_id로 저장된 값 → 해당 품목의 첫 번째 행 1건 자동 체크
const legacyKey = String(saved);
if (seenChildForLegacy.has(legacyKey)) continue;
const firstMat = bomMaterials.find((m) => m.child_item_id === legacyKey);
if (firstMat) {
restored.add(firstMat.id);
seenChildForLegacy.add(legacyKey);
}
}
}
setBomChecked(restored);
return;
}
}
@@ -310,8 +330,9 @@ export function DetailFormModal({
submitData.content = submitData.content || "작업수량 / 불량수량 / 양품수량";
}
if (type === "material_input") {
// 선택된 BOM 자재를 각각 개별 상세 항목으로 등록
const checkedMats = bomMaterials.filter(m => bomChecked.has(m.child_item_id));
// 선택된 BOM 자재를 각각 개별 상세 항목으로 등록 — 동일 품목 중복 row를 독립
// 처리하기 위해 bom_detail 고유 id(m.id) 기준으로 매칭
const checkedMats = bomMaterials.filter(m => bomChecked.has(m.id));
if (checkedMats.length > 0) {
const resolveType = (code: string) => itemTypeCatMap[code] || code || "";
for (const mat of checkedMats) {
@@ -955,7 +976,7 @@ export function DetailFormModal({
}
onCheckedChange={(checked) => {
if (checked) {
setBomChecked(new Set(bomMaterials.map((m) => m.child_item_id)));
setBomChecked(new Set(bomMaterials.map((m) => m.id)));
} else {
setBomChecked(new Set());
}
@@ -994,12 +1015,12 @@ export function DetailFormModal({
className="flex items-center gap-2.5 border-b px-3 py-2.5 last:border-b-0 hover:bg-sky-50/50"
>
<Checkbox
checked={bomChecked.has(mat.child_item_id)}
checked={bomChecked.has(mat.id)}
onCheckedChange={(checked) => {
setBomChecked((prev) => {
const next = new Set(prev);
if (checked) next.add(mat.child_item_id);
else next.delete(mat.child_item_id);
if (checked) next.add(mat.id);
else next.delete(mat.id);
return next;
});
}}