feat(bom): COMPANY_7 BOM에 대체 품목 관리 기능 추가

- bom_detail_substitute 테이블 신규 (varchar PK, FK 미설정 / 코드 조인)
- 백엔드: 단일 행 substitute CRUD + BOM 단위 갯수 맵 API 5종 추가
- 프론트(COMPANY_7): 트리 행에 '대체 N' 뱃지 + 대체 품목 모달
  · 드래그앤드롭으로 우선순위 변경 + 자동 채번/재할당
  · 250ms debounce 실시간 검색, 결과 클릭 시 자동 행 추가
  · inline blur 저장, zebra 행 구분, sticky 헤더 단색 처리
- 트리뷰 액션 버튼 통합: 가상 루트 선택 시 1레벨, 일반 행 선택 시 하위로 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DDD1542
2026-05-06 17:32:10 +09:00
parent 45d3ccb004
commit 17173be350
4 changed files with 669 additions and 9 deletions

View File

@@ -224,3 +224,93 @@ export async function deleteBomVersion(req: Request, res: Response) {
res.status(500).json({ success: false, message: error.message });
}
}
// ─── 대체품 (Substitute) ─────────────────────────────
export async function getBomDetailSubstitutes(req: Request, res: Response) {
try {
const { detailId } = req.params;
const companyCode = (req as any).user?.companyCode || "*";
const data = await bomService.getBomDetailSubstitutes(detailId, companyCode);
res.json({ success: true, data });
} catch (error: any) {
logger.error("대체품 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function getBomSubstituteCounts(req: Request, res: Response) {
try {
const { bomId } = req.params;
const companyCode = (req as any).user?.companyCode || "*";
const versionId = (req.query.versionId as string) || undefined;
const rows = await bomService.getBomSubstituteCounts(bomId, companyCode, versionId);
const map: Record<string, number> = {};
for (const r of rows as any[]) {
map[r.bom_detail_id] = Number(r.count);
}
res.json({ success: true, data: map });
} catch (error: any) {
logger.error("대체품 갯수 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function createBomDetailSubstitute(req: Request, res: Response) {
try {
const { detailId } = req.params;
const companyCode = (req as any).user?.companyCode || "*";
const writer = (req as any).user?.userName || (req as any).user?.userId || "";
const { substitute_item_id, priority, ratio, remark, status } = req.body;
if (!substitute_item_id) {
res.status(400).json({ success: false, message: "substitute_item_id는 필수입니다" });
return;
}
const data = await bomService.createBomDetailSubstitute({
bom_detail_id: detailId,
substitute_item_id,
priority,
ratio,
remark,
status,
company_code: companyCode,
writer,
});
res.json({ success: true, data });
} catch (error: any) {
logger.error("대체품 등록 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function updateBomDetailSubstitute(req: Request, res: Response) {
try {
const { id } = req.params;
const data = await bomService.updateBomDetailSubstitute(id, req.body);
if (!data) {
res.status(404).json({ success: false, message: "대체품을 찾을 수 없습니다" });
return;
}
res.json({ success: true, data });
} catch (error: any) {
logger.error("대체품 수정 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function deleteBomDetailSubstitute(req: Request, res: Response) {
try {
const { id } = req.params;
const result = await bomService.deleteBomDetailSubstitute(id);
if (!result) {
res.status(404).json({ success: false, message: "대체품을 찾을 수 없습니다" });
return;
}
res.json({ success: true });
} catch (error: any) {
logger.error("대체품 삭제 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}