일상

2023 07/01~2023 07/07 MES Project

윤태영(Coding) 2023. 7. 7. 18:20

일주일간의 휴식을 가지면서, 하고싶은 프로젝트가 있어서 만들어 보았다.

< 프로젝트 목표 및 개념 >

프로젝트를 계획하면서는 제조 공정에서 발생하는 기업의 다양한 요구사항을 반영하려 노력했다. 스스로 다른 기업에서 생성 관리를 도와주는 앱을 쓴다면 어떤 기능들이 필요할까 고민했고 정보가 없었으나 편의점 아르바이트를 하면서 물류를 받고 정리할 때를 생각하며 구상을 하니,  우선 생산 계획 플랜이 있어야 할 것이며, 지시가 있어야 만들어 질 것이고, 제품 정보가 있어야 할 것이며, 생산 결과가 있어야 하며,이후 그것을 추적할 수 있어야 한다.

그렇게 정리를 했고 제조업체의 생산관리를 도와주는 웹 애플리케이션을 만들고 싶었고, ASP.NET Core(NET6.0)를 사용해 개발하였다.



우선 모델은 다음과 같다.

ProductionPlan: 생산 계획을 나타내는 모델이다. 특정 제품에 대한 계획된 생산 일정과 수량 등을 관리한다.

namespace SimpleMES.Models
{
    public class ProductionPlan
    {
        public int ProductionPlanId { get; set; }
        public DateTime PlanDate { get; set; }
        public int Quantity { get; set; }
        public int ProductId { get; set; }
        public Product Product { get; set; }
    }
}

 

Product: 제품에 대한 정보를 나타내는 모델이다. 제품의 ID, 이름, 코드, 설명, 단위 가격 등이 포함되어 있다.

namespace SimpleMES.Models
{
    public class Product
    {
        public int ProductId { get; set; }
        public string ProductName { get; set; }
        public string ProductCode { get; set; }
        public string Description { get; set; }
        public decimal UnitPrice { get; set; }
        public ICollection<ProductionPlan> ProductionPlans { get; set; }
        public ICollection<WorkOrder> WorkOrders { get; set; }
        public ICollection<ProductionResult> ProductionResults { get; set; }
    }
}

 

WorkOrder: 작업 지시를 나타내는 모델이다. 특정 제품에 대한 작업 지시 내용을 관리한다.

namespace SimpleMES.Models
{
    public class WorkOrder
    {
        public int WorkOrderId { get; set; }
        public DateTime OrderDate { get; set; }
        public int OrderQuantity { get; set; }
        public int ProductId { get; set; }
        public Product Product { get; set; }
    }
}

ProductionResult: 생산 결과를 나타내는 모델이다. 특정 제품에 대한 실제 생산 결과를 관리한다.

namespace SimpleMES.Models
{
    public class ProductionResult
    {
        public int ProductionResultId { get; set; }
        public DateTime ResultDate { get; set; }
        public int ProducedQuantity { get; set; }
        public int ProductId { get; set; }
        public Product Product { get; set; }
    }
}

 

LotTracking: 제품 묶음을 추적하는 모델이다. 특정 제품의 묶음을 추적할 수 있게 한다.

namespace SimpleMES.Models
{
    public class LotTracking
    {
        public int LotTrackingId { get; set; }
        public string LotCode { get; set; }
        public DateTime LotDate { get; set; }
        public int ProductId { get; set; }
        public Product Product { get; set; }
        public ICollection<ProductionResult> ProductionResults { get; set; }
    }
}


그리고 이 모델들은 ProductionDbContext 클래스에서 관리된다. 이 클래스는 DbContext를 상속받아  데이터베이스 연결 및 쿼리를 처리한다.

using Microsoft.EntityFrameworkCore;

namespace SimpleMES.Models
{
    public class ProductionDbContext : DbContext
    {
        public ProductionDbContext(DbContextOptions<ProductionDbContext> options)
            : base(options)
        {
        }

        public DbSet<Product> Products { get; set; }
        public DbSet<ProductionPlan> ProductionPlans { get; set; }
        public DbSet<WorkOrder> WorkOrders { get; set; }
        public DbSet<ProductionResult> ProductionResults { get; set; }
        public DbSet<LotTracking> LotTrackings { get; set; }
    }
}

 

< 유효성 검사를 위한 Helper 클래스 >

ValidationHelper라는 정적 클래스를 생성했다. 이 클래스에서는 위의 모델의 속성들에 대한 유효성 검사 메서드를 제공한다.

 

 

using SimpleMES.Models;

public static class ValidationHelper
{
    // 제품 유효성 검사 함수
    public static bool ValidateProduct(Product product)
    {
        // 제품 이름이 없는 경우 false 반환
        if (string.IsNullOrEmpty(product.ProductName))
        {
            return false;
        }

        // 제품 코드가 없는 경우 false 반환
        if (string.IsNullOrEmpty(product.ProductCode))
        {
            return false;
        }

        // 제품 단위 가격이 0 이하인 경우 false 반환
        if (product.UnitPrice <= 0)
        {
            return false;
        }

        // 모든 검사를 통과한 경우 true 반환
        return true;
    }

    // 로트 트래킹 유효성 검사 함수
    public static bool ValidateLotTracking(LotTracking lotTracking)
    {
        // 로트 코드가 없는 경우 false 반환
        if (string.IsNullOrEmpty(lotTracking.LotCode))
        {
            return false;
        }

        // 로트 날짜가 없거나 미래의 날짜인 경우 false 반환
        if (lotTracking.LotDate == null || lotTracking.LotDate > DateTime.Now)
        {
            return false;
        }

        // 모든 검사를 통과한 경우 true 반환
        return true;
    }

    // 생산 계획 유효성 검사 함수
    public static bool ValidateProductionPlan(ProductionPlan productionPlan)
    {
        // 계획 날짜가 없거나 미래의 날짜인 경우 false 반환
        if (productionPlan.PlanDate == null || productionPlan.PlanDate > DateTime.Now)
        {
            return false;
        }

        // 계획된 수량이 0 이하인 경우 false 반환
        if (productionPlan.Quantity <= 0)
        {
            return false;
        }

        // 모든 검사를 통과한 경우 true 반환
        return true;
    }

    // 생산 결과 유효성 검사 함수
    public static bool ValidateProductionResult(ProductionResult productionResult)
    {
        // 결과 날짜가 없거나 미래의 날짜인 경우 false 반환
        if (productionResult.ResultDate == null || productionResult.ResultDate > DateTime.Now)
        {
            return false;
        }

        // 생산된 수량이 0 이하인 경우 false 반환
        if (productionResult.ProducedQuantity <= 0)
        {
            return false;
        }

        // 모든 검사를 통과한 경우 true 반환
        return true;
    }

    // 작업 지시 유효성 검사 함수
    public static bool ValidateWorkOrder(WorkOrder workOrder)
    {
        // 주문 날짜가 없거나 미래의 날짜인 경우 false 반환
        if (workOrder.OrderDate == null || workOrder.OrderDate > DateTime.Now)
        {
            return false;
        }

        // 주문 수량이 0 이하인 경우 false 반환
        if (workOrder.OrderQuantity <= 0)
        {
            return false;
        }

        // 모든 검사를 통과한 경우 true 반환
        return true;
    }
}


ValidateProduct: 제품 이름과 제품 코드가 null이 아니며, 단위 가격이 0 이상인지 확인한더.

ValidateLotTracking: Lot 코드가 null이 아니며, Lot 날짜가 현재 시간 이전인지 확인한다.

ValidateProductionPlan: 계획 날짜가 현재 시간 이전이며, 수량이 0 이상인지 확인한다.

ValidateProductionResult: 결과 날짜가 현재 시간 이전이며, 생산 수량이 0 이상인지 확인한다.

ValidateWorkOrder: 주문 날짜가 현재 시간 이전이며, 주문 수량이 0 이상인지 확인한다.

이렇게 각각의 유효성 검사 메서드를 사용하면, 사용자로부터 받은 데이터가 요구사항을 충족하는지 확인할 수 있다.


< 데이터베이스 연결 설정 >

데이터베이스 연결 설정은 appsettings.json 파일에 저장되어 있다. "DefaultConnection" 문자열을 통해 데이터베이스 서버의 위치, 데이터베이스 이름, 그리고 연결 옵션들을 설정하였다. 이 설정은 ProductionDbContext에서 사용되며, 애플리케이션과 데이터베이스 사이의 연결을 관리한다.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=LAPTOP-G9PM4O7F\\SQLEXPRESS;Database=ProductionDb;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

< Repository >

먼저, Repository 클래스는 데이터베이스와의 상호작용을 추상화하는 역할을 한다. 

5개의 Repository 클래스는 각각의 Repository 클래스는 모델에 대한 CRUD(Create, Read, Update, Delete) 연산을 제공한다. 

[LotTrackingRepository]
LotTrackingRepository는 LotTracking 모델에 대한 데이터 액세스를 제공한다. GetAll, GetById, Add, Update, Delete 이렇게 5개의 메서드를 포함하고 있다.

using SimpleMES.Models;

public class LotTrackingRepository
{
    private readonly ProductionDbContext _context;

    public LotTrackingRepository(ProductionDbContext context)
    {
        _context = context;
    }

    public IEnumerable<LotTracking> GetAll()
    {
        return _context.LotTrackings.ToList();
    }

    public LotTracking GetById(int id)
    {
        return _context.LotTrackings.Find(id);
    }

    public void Add(LotTracking lotTracking)
    {
        _context.LotTrackings.Add(lotTracking);
        _context.SaveChanges();
    }

    public void Update(LotTracking lotTracking)
    {
        _context.LotTrackings.Update(lotTracking);
        _context.SaveChanges();
    }

    public void Delete(int id)
    {
        var lotTracking = _context.LotTrackings.Find(id);
        if (lotTracking != null)
        {
            _context.LotTrackings.Remove(lotTracking);
            _context.SaveChanges();
        }
    }
}

[ProductionPlanRepository]
ProductionPlanRepository는 ProductionPlan 모델에 대한 데이터 액세스를 제공한다. LotTrackingRepository와 마찬가지로 5개의 메서드를 포함하고 있다.

using SimpleMES.Models;

public class ProductionPlanRepository
{
    private readonly ProductionDbContext _context;

    public ProductionPlanRepository(ProductionDbContext context)
    {
        _context = context;
    }

    public IEnumerable<ProductionPlan> GetAll()
    {
        return _context.ProductionPlans.ToList();
    }

    public ProductionPlan GetById(int id)
    {
        return _context.ProductionPlans.Find(id);
    }

    public void Add(ProductionPlan productionPlan)
    {
        _context.ProductionPlans.Add(productionPlan);
        _context.SaveChanges();
    }

    public void Update(ProductionPlan productionPlan)
    {
        _context.ProductionPlans.Update(productionPlan);
        _context.SaveChanges();
    }

    public void Delete(int id)
    {
        var productionPlan = _context.ProductionPlans.Find(id);
        if (productionPlan != null)
        {
            _context.ProductionPlans.Remove(productionPlan);
            _context.SaveChanges();
        }
    }
}

[ProductionResultRepository]
ProductionResultRepository는 ProductionResult 모델에 대한 데이터 액세스를 제공한다.

using SimpleMES.Models;

public class ProductionResultRepository
{
    private readonly ProductionDbContext _context;

    public ProductionResultRepository(ProductionDbContext context)
    {
        _context = context;
    }

    public IEnumerable<ProductionResult> GetAll()
    {
        return _context.ProductionResults.ToList();
    }

    public ProductionResult GetById(int id)
    {
        return _context.ProductionResults.Find(id);
    }

    public void Add(ProductionResult productionResult)
    {
        _context.ProductionResults.Add(productionResult);
        _context.SaveChanges();
    }

    public void Update(ProductionResult productionResult)
    {
        _context.ProductionResults.Update(productionResult);
        _context.SaveChanges();
    }

    public void Delete(int id)
    {
        var productionResult = _context.ProductionResults.Find(id);
        if (productionResult != null)
        {
            _context.ProductionResults.Remove(productionResult);
            _context.SaveChanges();
        }
    }
}

[ProductRepository]

using SimpleMES.Models;

public class ProductRepository
{
    private readonly ProductionDbContext _context;

    public ProductRepository(ProductionDbContext context)
    {
        _context = context;
    }

    public IEnumerable<Product> GetAll()
    {
        return _context.Products.ToList();
    }

    public Product GetById(int id)
    {
        return _context.Products.Find(id);
    }

    public void Add(Product product)
    {
        _context.Products.Add(product);
        _context.SaveChanges();
    }

    public void Update(Product product)
    {
        _context.Products.Update(product);
        _context.SaveChanges();
    }

    public void Delete(int id)
    {
        var product = _context.Products.Find(id);
        if (product != null)
        {
            _context.Products.Remove(product);
            _context.SaveChanges();
        }
    }
}

[WorkOrderRepository]

using SimpleMES.Models;

public class WorkOrderRepository
{
    private readonly ProductionDbContext _context;

    public WorkOrderRepository(ProductionDbContext context)
    {
        _context = context;
    }

    public IEnumerable<WorkOrder> GetAll()
    {
        return _context.WorkOrders.ToList();
    }

    public WorkOrder GetById(int id)
    {
        return _context.WorkOrders.Find(id);
    }

    public void Add(WorkOrder workOrder)
    {
        _context.WorkOrders.Add(workOrder);
        _context.SaveChanges();
    }

    public void Update(WorkOrder workOrder)
    {
        _context.WorkOrders.Update(workOrder);
        _context.SaveChanges();
    }

    public void Delete(int id)
    {
        var workOrder = _context.WorkOrders.Find(id);
        if (workOrder != null)
        {
            _context.WorkOrders.Remove(workOrder);
            _context.SaveChanges();
        }
    }
}

< Service >

Service 클래스는 애플리케이션의 비즈니스 로직을 포함하고 있다. 이 클래스는 Repository 클래스를 통해 데이터에 액세스하고, 비즈니스 규칙을 적용하여 애플리케이션의 핵심 기능을 제공한다.
5개 모델에 대한 Service 클래스를 구현하였다. 각각의 Service 클래스는 모델에 대한 비즈니스 로직을 제공한다.

 

[LotTrackingService]

public class LotTrackingService
{
    //데이터베이스와의 통신을 담당
    private readonly LotTrackingRepository _repository;

    public LotTrackingService(LotTrackingRepository repository)
    {
        _repository = repository;
    }

    // GetAll : 모든 LotTracking 객체를 조회하여 반환
    public IEnumerable<LotTracking> GetAll()
    {
        return _repository.GetAll();
    }

    // GetById : 메서드는 주어진 id에 해당하는 LotTracking 객체를 조회하여 반환
    public LotTracking GetById(int id)
    {
        return _repository.GetById(id);
    }

    // 유효성 검사를 먼저, 이를 통과한 경우에만 _repository의 Add를 호출하여 객체를 추가
    public void Add(LotTracking lotTracking)
    {
        if (!ValidationHelper.ValidateLotTracking(lotTracking))
        {
            throw new ArgumentException("Invalid lot tracking data");
        }

        _repository.Add(lotTracking);
    }

    // 유효성 검사를 먼저, 이를 통과한 경우에만 _repository의 Update를 호출하여 객체를 업데이트
    public void Update(LotTracking lotTracking)
    {
        if (!ValidationHelper.ValidateLotTracking(lotTracking))
        {
            throw new ArgumentException("Invalid lot tracking data");
        }

        _repository.Update(lotTracking);
    }

    //주어진 id에 해당하는 LotTracking 객체삭제
    public void Delete(int id)
    {
        _repository.Delete(id);
    }
}

[ProductionPlanService]

using SimpleMES.Models;

public class ProductionPlanService
{
    private readonly ProductionPlanRepository _repository;

    public ProductionPlanService(ProductionPlanRepository repository)
    {
        _repository = repository;
    }

    public IEnumerable<ProductionPlan> GetAll()
    {
        return _repository.GetAll();
    }

    public ProductionPlan GetById(int id)
    {
        return _repository.GetById(id);
    }

    public void Add(ProductionPlan productionPlan)
    {
        if (!ValidationHelper.ValidateProductionPlan(productionPlan))
        {
            throw new ArgumentException("Invalid production plan data");
        }

        _repository.Add(productionPlan);
    }

    public void Update(ProductionPlan productionPlan)
    {
        if (!ValidationHelper.ValidateProductionPlan(productionPlan))
        {
            throw new ArgumentException("Invalid production plan data");
        }

        _repository.Update(productionPlan);
    }

    public void Delete(int id)
    {
        _repository.Delete(id);
    }
}

[ProductionResultService]

using SimpleMES.Models;

public class ProductionResultService
{
    private readonly ProductionResultRepository _repository;

    public ProductionResultService(ProductionResultRepository repository)
    {
        _repository = repository;
    }

    public IEnumerable<ProductionResult> GetAll()
    {
        return _repository.GetAll();
    }

    public ProductionResult GetById(int id)
    {
        return _repository.GetById(id);
    }

    public void Add(ProductionResult productionResult)
    {
        if (!ValidationHelper.ValidateProductionResult(productionResult))
        {
            throw new ArgumentException("Invalid production result data");
        }

        _repository.Add(productionResult);
    }

    public void Update(ProductionResult productionResult)
    {
        if (!ValidationHelper.ValidateProductionResult(productionResult))
        {
            throw new ArgumentException("Invalid production result data");
        }

        _repository.Update(productionResult);
    }

    public void Delete(int id)
    {
        _repository.Delete(id);
    }
}

[ProductService]

using SimpleMES.Models;

public class ProductService
{
    private readonly ProductRepository _repository;

    public ProductService(ProductRepository repository)
    {
        _repository = repository;
    }

    public IEnumerable<Product> GetAll()
    {
        return _repository.GetAll();
    }

    public Product GetById(int id)
    {
        return _repository.GetById(id);
    }

    public void Add(Product product)
    {
        if (!ValidationHelper.ValidateProduct(product))
        {
            throw new ArgumentException("Invalid product data");
        }

        _repository.Add(product);
    }

    public void Update(Product product)
    {
        if (!ValidationHelper.ValidateProduct(product))
        {
            throw new ArgumentException("Invalid product data");
        }

        _repository.Update(product);
    }

    public void Delete(int id)
    {
        _repository.Delete(id);
    }
}

[WorkOrderService]

using SimpleMES.Models;

public class WorkOrderService
{
    private readonly WorkOrderRepository _repository;

    public WorkOrderService(WorkOrderRepository repository)
    {
        _repository = repository;
    }

    public IEnumerable<WorkOrder> GetAll()
    {
        return _repository.GetAll();
    }

    public WorkOrder GetById(int id)
    {
        return _repository.GetById(id);
    }

    public void Add(WorkOrder workOrder)
    {
        if (!ValidationHelper.ValidateWorkOrder(workOrder))
        {
            throw new ArgumentException("Invalid work order data");
        }

        _repository.Add(workOrder);
    }

    public void Update(WorkOrder workOrder)
    {
        if (!ValidationHelper.ValidateWorkOrder(workOrder))
        {
            throw new ArgumentException("Invalid work order data");
        }

        _repository.Update(workOrder);
    }

    public void Delete(int id)
    {
        _repository.Delete(id);
    }
}

< Controller >

// ProductsController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SimpleMES.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SimpleMES.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        private readonly ProductionDbContext _context;

        public ProductsController(ProductionDbContext context)
        {
            _context = context;
        }
  // [HttpGet] 어트리뷰트를 사용하여 이 메서드를 GET 요청에 바인딩
    // 모든 Product 객체를 비동기로 조회하여 반환
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
        {
            try
            {
                return await _context.Products.ToListAsync();
            }
            catch (Exception ex)
            {
                return StatusCode(500, $"Error fetching products: {ex.Message}");
            }
        }


        [HttpGet("{id}")]
        public async Task<ActionResult<Product>> GetProduct(int id)
        {
            var product = await _context.Products.FindAsync(id);

            if (product == null)
            {
                return NotFound();
            }

            return product;
        }
 // [Authorize] : 이 엔드포인트가 인증된 사용자만에게 허용.
        [Authorize]
        [HttpPut("{id}")]
        public async Task<IActionResult> UpdateProduct(int id, [FromBody] Product product)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (id != product.ProductId)
            {
                return BadRequest();
            }

            _context.Entry(product).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!ProductExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        [Authorize]
        [HttpPost]
        public async Task<ActionResult<Product>> AddProduct([FromBody] Product product)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            try
            {
                _context.Products.Add(product);
                await _context.SaveChangesAsync();

                return CreatedAtAction(nameof(GetProduct), new { id = product.ProductId }, product);
            }
            catch (Exception ex)
            {
                return StatusCode(500, $"Error adding product: {ex.Message}");
            }
        }

        [Authorize]
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteProduct(int id)
        {
            var product = await _context.Products.FindAsync(id);
            if (product == null)
            {
                return NotFound();
            }

            _context.Products.Remove(product);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private bool ProductExists(int id)
        {
            return _context.Products.Any(e => e.ProductId == id);
        }
    }
}

< Startup.cs >

// Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SimpleMES.Models;

namespace SimpleMES
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ProductionDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
            );

            services.AddCors(options =>
            {
                options.AddDefaultPolicy(builder =>
                {
                    builder.AllowAnyOrigin()
                           .AllowAnyHeader()
                           .AllowAnyMethod();
                });
            });

            services.AddControllers();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();
            app.UseRouting();

            app.UseCors();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

 

 

이제 여기서 React 코드를 만들어서 상호작용하는 코드를 완성했다. Axios를 사용해서 하고 있는데 잘 해결이 안되고 있다. 코드는 잘 짰는데 localhost 3000 port와 7223port cros 해결이 잘 안되는 것 같고.. 뭔가 Version 호환성이 안맞는 것 같다. 계속 찾아보고 있는데 문제 해결이 된다면 좋을 것 같다.