SOLIDWORKS Manage에서 BOM 데이터를 편집하는 것은 실제 업무에서 매우 빈번하게 발생하는 요구사항입니다. 본 문서에서는 C#을 활용하여 BOM 항목 수량 변경, 텍스트 행 추가, 자재 추가/삭제, 버전 저장 등 기업 수준의 BOM 편집 기능을 구현하는 방법을 상세히 설명합니다.
핵심 편집 기능 목록
- 특정 BOM 항목의 수량 조정
- BOM에 텍스트 행(비고/설명) 추가
- BOM 계층 구조에 새 자재 추가
- BOM 항목(자재/텍스트 행) 삭제
- BOM 변경 사항 저장 및 새 버전 생성
사전 조건 및 핵심 규칙
- BOM 편집 권한: 로그인 계정에 'BOM 편집' 권한이 있어야 합니다(Manage 백엔드에서 설정).
- BOM 상태: 편집 전에 BOM이 다른 사용자에게 체크아웃되지 않았는지 확인합니다(
/api/CheckIn/CheckOutStatusAPI 호출). - 작업 흐름: 모든 BOM 수정은 '편집 후 저장' 순서로 진행되며, 저장 후에만 변경 사항이 영구적으로 반영됩니다.
- 버전 규칙: 저장 시 메이저 버전(V1.0→V2.0) 또는 마이너 버전(V1.0→V1.1)을 지정할 수 있습니다.
C# BOM 편집 구현 (전체 코드)
1. BOM 편집 모델 정의
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace ManageApiDev
{
public class ApiResult
{
public bool Success { get; set; }
public string Message { get; set; }
public object Payload { get; set; }
}
public class QtyUpdatePayload
{
public string ConfigId { get; set; }
public string BomItemId { get; set; }
public decimal Quantity { get; set; }
}
public class TextLineAddPayload
{
public string ConfigId { get; set; }
public string ParentId { get; set; }
public string Description { get; set; }
public decimal Quantity { get; set; } = 1;
public string Unit { get; set; } = "건";
}
public class MaterialAddPayload
{
public string ConfigId { get; set; }
public string ParentId { get; set; }
public string PartNumber { get; set; }
public decimal Quantity { get; set; }
public string Unit { get; set; }
public string Description { get; set; } = "";
}
public class BomSavePayload
{
public string ConfigId { get; set; }
public string VersionDescription { get; set; }
public bool IsMajorVersion { get; set; }
}
}
2. BOM 편집 유틸리티 클래스 확장
namespace ManageApiDev
{
public class BomManager
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl = "https://your-manage-instance/api/";
public BomManager(string authToken)
{
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {authToken}");
}
public async Task ModifyItemQtyAsync(string configId, string itemId, decimal newQty)
{
try
{
var url = $"{_baseUrl}Bom/UpdateQuantity";
var body = new QtyUpdatePayload
{
ConfigId = configId,
BomItemId = itemId,
Quantity = newQty
};
var response = await _httpClient.PostAsJsonAsync(url, body);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ApiResult>(json);
if (!result.Success)
throw new Exception($"수량 변경 실패: {result.Message}");
Console.WriteLine($"항목 {itemId} 수량이 {newQty}(으)로 변경되었습니다.");
}
catch (Exception ex)
{
Console.WriteLine($"수량 변경 중 오류 발생: {ex.Message}");
throw;
}
}
public async Task InsertTextLineAsync(string configId, string parentId, string description)
{
try
{
var url = $"{_baseUrl}Bom/AddTextLine";
var body = new TextLineAddPayload
{
ConfigId = configId,
ParentId = parentId,
Description = description
};
var response = await _httpClient.PostAsJsonAsync(url, body);
response.EnsureSuccessStatusCode();
var result = JsonConvert.DeserializeObject<ApiResult>(await response.Content.ReadAsStringAsync());
if (!result.Success)
throw new Exception($"텍스트 행 추가 실패: {result.Message}");
Console.WriteLine("BOM 텍스트 행이 추가되었습니다.");
}
catch (Exception ex)
{
Console.WriteLine($"텍스트 행 추가 중 오류 발생: {ex.Message}");
throw;
}
}
public async Task AddMaterialAsync(string configId, string parentId, string partNumber, decimal qty, string unit, string desc = "")
{
try
{
var url = $"{_baseUrl}Bom/AddItem";
var body = new MaterialAddPayload
{
ConfigId = configId,
ParentId = parentId,
PartNumber = partNumber,
Quantity = qty,
Unit = unit,
Description = desc
};
var response = await _httpClient.PostAsJsonAsync(url, body);
response.EnsureSuccessStatusCode();
var result = JsonConvert.DeserializeObject<ApiResult>(await response.Content.ReadAsStringAsync());
if (!result.Success)
throw new Exception($"자재 추가 실패: {result.Message}");
Console.WriteLine($"자재 {partNumber}이(가) BOM에 추가되었습니다.");
}
catch (Exception ex)
{
Console.WriteLine($"자재 추가 중 오류 발생: {ex.Message}");
throw;
}
}
public async Task RemoveBomItemAsync(string configId, string itemId)
{
try
{
var url = $"{_baseUrl}Bom/DeleteItem?configId={configId}&bomItemId={itemId}";
var response = await _httpClient.PostAsync(url, null);
response.EnsureSuccessStatusCode();
var result = JsonConvert.DeserializeObject<ApiResult>(await response.Content.ReadAsStringAsync());
if (!result.Success)
throw new Exception($"BOM 항목 삭제 실패: {result.Message}");
Console.WriteLine($"BOM 항목 {itemId}이(가) 삭제되었습니다.");
}
catch (Exception ex)
{
Console.WriteLine($"BOM 항목 삭제 중 오류 발생: {ex.Message}");
throw;
}
}
public async Task SaveBomRevisionAsync(string configId, string versionDesc, bool isMajor = false)
{
try
{
var url = $"{_baseUrl}Bom/SaveChanges";
var body = new BomSavePayload
{
ConfigId = configId,
VersionDescription = versionDesc,
IsMajorVersion = isMajor
};
var response = await _httpClient.PostAsJsonAsync(url, body);
response.EnsureSuccessStatusCode();
var result = JsonConvert.DeserializeObject<ApiResult>(await response.Content.ReadAsStringAsync());
if (!result.Success)
throw new Exception($"변경 사항 저장 실패: {result.Message}");
Console.WriteLine($"BOM 변경 사항이 저장되었습니다. ({(isMajor ? "메이저" : "마이너")} 버전)");
}
catch (Exception ex)
{
Console.WriteLine($"변경 사항 저장 중 오류 발생: {ex.Message}");
throw;
}
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
}
3. 전체 호출 예제
namespace ManageApiDev
{
class Program
{
static async Task Main(string[] args)
{
var auth = new AuthHelper();
string token = await auth.GetTokenAsync();
var bomMgr = new BomManager(token);
try
{
string objId = "101";
string recId = "123456";
string versionNote = "자재 수량 조정 및 비고 추가";
string configId = await bomMgr.GetBomConfigIdAsync(objId, recId);
var bomData = await bomMgr.GetBomStructureAsync(configId);
var firstChild = bomData.Items.FirstOrDefault(i => i.ParentId == bomData.RootId);
if (firstChild != null)
await bomMgr.ModifyItemQtyAsync(configId, firstChild.Id, 10);
await bomMgr.InsertTextLineAsync(configId, bomData.RootId, "비고: 양산 버전 전용 BOM");
await bomMgr.AddMaterialAsync(configId, bomData.RootId, "PART-007", 2, "개", "신규 체결 부품");
await bomMgr.SaveBomRevisionAsync(configId, versionNote, false);
var updatedBom = await bomMgr.GetBomStructureAsync(configId);
Console.WriteLine("\n===== 수정된 BOM 구조 =====");
bomMgr.PrintBomStructure(updatedBom.Items, updatedBom.RootId);
}
catch (Exception ex)
{
Console.WriteLine($"BOM 편집 중 오류 발생: {ex.Message}");
}
finally
{
bomMgr.Dispose();
auth.Dispose();
}
Console.ReadKey();
}
}
}
자주 발생하는 문제와 해결 방법
| 문제 현상 | 원인 | 해결 방법 |
|---|---|---|
| 수량 변경 시 'BOM이 체크아웃됨' | 다른 사용자가 BOM을 잠금 | /api/CheckIn/CheckInBom API를 호출하여 체크인 |
| 자재 추가 시 '자재 없음' | PartNumber 오류 또는 Manage에 미등록 | 자재 코드 확인 후 시스템에 등록 |
| 저장 시 '버전 설명 누락' | VersionDescription 필드 누락 | 반드시 버전 설명 입력 (예: "202405-수량조정") |
| 수정 후 조회 시 변화 없음 | SaveBomRevisionAsync 미호출 | 모든 수정은 메모리 상태이므로 저장 필수 |
| 403 Forbidden | 계정에 BOM 편집 권한 없음 | 백엔드에서 'BOM 편집' 및 '버전 관리' 권한 할당 |
성능 및 보안 권장 사항
- 일괄 처리 최적화: 여러 BOM 항목 수정 시 '일괄 수정 후 한 번 저장'하여 SaveChanges 호출 최소화
- 트랜잭션 보장: BOM 수정에 내장 트랜잭션이 없으므로, 수정 전 BOM 구조를 백업하고 실패 시 복구 로직 구현
- 권한 검증: 코드 내에서
/api/User/GetCurrentUserAPI를 호출하여 계정 권한 확인 - 로깅: 모든 BOM 수정 작업(누가, 언제, 무엇을 수정했는지)을 기록하여 추적 가능성 확보
핵심 요약
- BOM 쓰기 작업은 '수정 → 저장' 프로세스를 따르며, SaveChanges API만이 변경 사항을 영구적으로 반영합니다.
- 모든 쓰기 API는 POST 방식이며 JSON 요청 본문과 인증 토큰이 필요합니다.
- 수정 전 BOM 상태(체크아웃 여부), 자재 존재 여부, 계정 권한을 확인하여 오류를 방지해야 합니다.
- 저장 시 버전 설명과 버전 유형(메이저/마이너)은 비즈니스 규칙에 따라 설정해야 합니다.