ABP 프레임워크에서 TPL 템플릿을 활용한 PDF 문서 생성

ABP 프레임워크에서 TPL(Template Processing Language) 파일을 사용하여 HTML 템플릿을 PDF 문서로 변환하는 방법을 살펴봅니다. 이 강력한 방법이 문서 생성을 어떻게 단순화하고 개발자가 고품질 PDF를 효율적으로 생성할 수 있게 하는지 알아보겠습니다. TPL 파일을 활용하여 애플리케이션의 문서 생성 기능을 강화하는 단계와 모범 사례를 함께 따라가 보겠습니다.

시나리오

시스템에는 광고가 있습니다. 광고가 관리자의 승인을 받으면, 승인된 광고 보고서를 생성하여 이메일로 발송하는 기능이 필요합니다.

참고: 본 예제에서는 Puppeteer Sharp 라이브러리를 사용하여 PDF를 생성합니다.

승인된 광고 보고서 TPL 파일

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ L "AdvertApprovedReport" }}</title>
</head>
<body>
    <div class="container">
        <div class="report-header">
            <div class="report-to">
                <strong>{{ L "ReportTo" }}</strong><br>
                {{ model.warehouse_name }}<br>
                {{ model.warehouse_postal_code }}, {{ model.warehouse_place }}<br>
                {{ model.warehouse_country }}
            </div>
            <div class="report-info">
                <div><strong>{{ L "AdvertCode" }}:</strong> {{ model.advert_code }}</div>
                <div><strong>{{ L "ApproveDate" }}:</strong> {{ model.approve_date }}</div>
            </div>
        </div>

        <h4>{{ L "AdvertItems" }}</h4>
        | {{ L "ProductName" }} | {{ L "UnitPrice" }} | {{ L "Quantity" }} | {{ L "TotalCost" }} |
|---|---|---|---|
| {{ item.product\_name }} | €{{ item.unit\_price }} | {{ item.quantity }} | €{{ item.total\_cost }} |

        <h4>{{ L "ApprovedAdvertItems" }}</h4>
        | {{ L "ProductName" }} | {{ L "UnitPrice" }} | {{ L "Quantity" }} | {{ L "TotalCost" }} |
|---|---|---|---|
| {{ item.product\_name }} | €{{ item.unit\_price }} | {{ item.quantity }} | €{{ item.total\_cost }} |

        <div class="total">{{ L "Subtotal" }}: &euro;{{ model.subtotal }}</div>
        <div class="total">(-) {{ L "FreightPrice" }}:  &euro;{{ model.freight_price }}</div>
        <div class="total">(-) {{ L "ServiceFee" }}:  &euro;{{ model.service_fee }}</div>
        <div class="total"></div>
        <div class="expected-total">{{ L "ExpectedTotal" }}: &euro;{{ model.expected_total }}</div>
    </div>
</body>
</html>

이 TPL 파일은 TemplateDefinitionProvider를 상속받는 클래스에서 선언해야 합니다.

public class AdvertisementTemplateDefinitionProvider : TemplateDefinitionProvider  
{  
    public override void Define(ITemplateDefinitionContext context)  
    {  
        context.Add(  
            new TemplateDefinition(  
                AdvertisementEmailTemplates.ApprovedReport,  
                displayName: LocalizableString.Create<AdvertisementResource>(  
                    $"TextTemplate:{AdvertisementEmailTemplates.ApprovedReport}"  
                ),  
                layout: StandardEmailTemplates.DefaultLayout,  
                localizationResource: typeof(AdvertisementResource)  
            ).WithVirtualFilePath("/Advertisements/Templates/ApprovedReport.tpl", true)  
        );  
    }  
}  
  
public class AdvertisementEmailTemplates  
{  
    public const string ApprovedReport = "ApprovedReport";  
}

이메일 발송 로직

public async Task SendApprovedReportAsync(
     Advertisement advertisement,
     List<string> recipientEmails,
     List<string> adminEmails
 )
 {
     var reportData = new
     {
         warehouse_name = advertisement?.Warehouse?.Name ?? string.Empty,
         warehouse_postal_code = advertisement?.Warehouse?.PostalCode?.Code ?? string.Empty,
         warehouse_place = advertisement?.Warehouse?.Place?.Name ?? string.Empty,
         warehouse_country = advertisement?.Warehouse?.Country?.Name ?? string.Empty,
         advert_code = advertisement?.TransactionNumber ?? string.Empty,
         approve_date = DateTime.Now.ToString("dd.MM.yyyy HH:mm"),
         advert_items = advertisement?.Items?.Select(x => new
         {
             product_name = x?.Product?.Name ?? string.Empty,
             quantity = x?.Quantity ?? 0,
             total_cost = x?.TotalCost ?? 0m,
             unit_price = x?.UnitPrice ?? 0m,
         }) ?? Enumerable.Empty<object>(),
         approved_advert_items = advertisement?.ApprovedItems?.Select(x => new
         {
             product_name = x?.Product?.Name ?? string.Empty,
             quantity = x?.Quantity ?? 0,
             total_cost = x?.TotalCost ?? 0m,
             unit_price = x?.UnitPrice ?? 0m,
         }) ?? Enumerable.Empty<object>(),
         subtotal = advertisement?.ApprovedItems?.Sum(x => x.TotalCost) ?? 0m,
         freight_price = advertisement?.TransportationPrice?.Price ?? 0m,
         service_fee = advertisement?.ApprovedItems?.Sum(x =>
             x.Quantity * x.ServiceFees.Sum(fee => fee.ServicePrice.Price)
         ) ?? 0m,
         expected_total = (advertisement?.ApprovedItems?.Sum(x => x.TotalCost) ?? 0m)
             - (advertisement?.TransportationPrice?.Price ?? 0m)
             - (
                 advertisement?.ApprovedItems?.Sum(x =>
                     x.Quantity * x.ServiceFees.Sum(fee => fee.ServicePrice.Price)
                 ) ?? 0m
             )
     };

     var renderedHtml = await _templateService.RenderAsync(
         AdvertisementEmailTemplates.ApprovedReport,
         reportData
     );
     byte[] pdfData = await _pdfGenerator.GeneratePdfFromHtml(renderedHtml);

     var mailMessage = new System.Net.Mail.MailMessage
     {
         Subject = _stringLocalizer["AdvertApprovedReport"],
         IsBodyHtml = true,
         Body = emailBody, // 이메일 본문 내용
     };

     foreach (var recipient in recipientEmails)
     {
         mailMessage.To.Add(recipient);
     }

     foreach (var admin in adminEmails)
     {
         mailMessage.Bcc.Add(admin);
     }

     mailMessage.Attachments.Add(
         new Attachment(new MemoryStream(pdfData), "ApprovedReport.pdf", "application/pdf")
     );

     await _emailService.SendAsync(mailMessage);
 }

백그라운드 작업 로직

public class ProcessApprovedReportJob
    : AsyncBackgroundJob<ApprovedReportJobArgs>,
        ITransientDependency
{
    private readonly IAdvertisementRepository _advertisementRepository;
    private readonly IEmailService _emailService;

    public ProcessApprovedReportJob(
        IAdvertisementRepository advertisementRepository,
        IEmailService emailService
    )
    {
        _advertisementRepository = advertisementRepository;
        _emailService = emailService;
    }

    public override async Task ExecuteAsync(ApprovedReportJobArgs args)
    {
        var advertisement = await _advertisementRepository.GetAsync(args.AdvertisementId);

        var ownerEmail = advertisement
            .Company.Members.Where(x => x.IsOwner)
            .Select(x => x.User.Email)
            .FirstOrDefault();
        Check.NotNullOrEmpty(ownerEmail, nameof(ownerEmail));

        await _emailService.SendApprovedReportAsync(ownerEmail, args.TransactionNumber);
    }
}

PDF 변환 서비스

public class HtmlToPdfConverter : IHtmlToPdfConverter
  {
      public async Task<byte[]> GeneratePdfFromHtml(string htmlString)
      {
          await new BrowserFetcher().DownloadAsync();

          using (
              var browser = await PuppeteerSharp.LaunchAsync(
                  new LaunchOptions { Headless = true, Args = ["--no-sandbox"] }
              )
          )
          using (var page = await browser.NewPageAsync())
          {
              await page.SetContentAsync(htmlString);
              var pdfData = await page.PdfDataAsync();
              return pdfData;
          }
      }
    }

보고서 생성 작업 호출

[HttpPut("/api/app/advertisement-approval/{id}/{approvedItemId}")]
 public async Task<AdvertisementDto> ApproveAsync(
     Guid id,
     long approvedItemId,
     ApprovalInputDto input
 )
 {
     var advertisement = await _advertisementRepository.GetAsync(id);
     var item = advertisement.GetApprovedItem(approvedItemId);

     item.SetQuantity(input.Quantity);

     var advertisementManager = GetAdvertisementManager(advertisement.AppModule);
     await advertisementManager.CalculateTotalCost(advertisement);
     await _advertisementRepository.UpdateAsync(advertisement);

     await _backgroundJobManager.EnqueueAsync(new GenerateReportJobArgs(advertisement.Id));

     return ObjectMapper.MapToDto(advertisement);
 }

여기서 광고 상태를 업데이트할 때 이메일 발송 로직을 호출합니다.

태그: ABP 프레임워크 TPL HTML to PDF Puppeteer Sharp C#

7월 26일 05:54에 게시됨