Project/BackOffice

Java itext-pdf적용해보기

hankyungtory 2025. 5. 11. 23:18

BackOffice 프로젝트를 진행하면서 사원들의 급여가 지급되고 급여명세서를 어떤식으로 발급해주면 좋을까 생각을 하다가

 

itext라는 라이브러리를 찾았다. 이후에 이 라이브러리를 적용시켜서 SMTP와 함께 사용해서 메일로 급여명세서를 pdf로 발급해주는

 

형식으로 진행하기로 했다.

 

build.gradle에 관련 라이브러리르 주입해준다.

 

 

PayService

//급여 정산
    public void salary() {
        // 직원 리스트 가져오기
        List<EmployeesDto> employeeIds = employeesMapper.findAllEmployees();

        // 직원별 급여 계산
        for (EmployeesDto employee : employeeIds) {
            try {
                // 직급에 맞는 기본급 가져오기
                GradesDto grade = gradesMapper.findSalary(employee.getId());

                EmployeesDto employeesDto = employeesMapper.findHireANdDepartmentName(employee.getId());

                if (grade == null) {
                    throw new IllegalStateException("직급 정보가 없습니다. 직원 ID: " + employee.getId());
                }


                String departmentName = employeesDto.getDepartmentName();
                String gradeName = grade.getName();

                // 기본급을 가져온 후 보너스, 공제 등을 반영하여 최종 급여 계산
                BigDecimal basePay = grade.getBasePay();
                
                Timestamp hireDate = Timestamp.valueOf(employeesDto.getHireDate().toLocalDateTime());

                int currentMonth = java.time.LocalDate.now().getMonthValue();
                BigDecimal bonus = BigDecimal.ZERO;

                if (currentMonth == 12) {
                    bonus = BigDecimal.valueOf(200000.00);
                }
                BigDecimal deductionPercentage = BigDecimal.valueOf(0.10);  // 10%
                BigDecimal deductions = basePay.multiply(deductionPercentage);

                // 최종 급여 계산
                BigDecimal finalPay = basePay.add(bonus).subtract(deductions);

                LocalDateTime payDate = LocalDateTime.now();

                // 급여 계산 후 PaySalaryRequest 객체 생성
                PaySalaryRequest paySalaryRequest = new PaySalaryRequest();
                paySalaryRequest.setEmployeeId(employee.getId());
                paySalaryRequest.setBonus(bonus);
                paySalaryRequest.setDeductions(deductions);
                paySalaryRequest.setFinalPay(finalPay);
                paySalaryRequest.setPayDate(Timestamp.valueOf(payDate));

                // 급여 데이터 DB에 저장
                paysMapper.salary(paySalaryRequest);

                List<String> mailList = paysMapper.getMail(employee.getId());
                String mail = null;
                if (!mailList.isEmpty()) {
                     mail = mailList.get(0);
                }
                List<SalaryDetailRequest> salaryDetails = new ArrayList<>();
                salaryDetails.add(new SalaryDetailRequest(bonus, deductions, finalPay, Timestamp.valueOf(payDate),gradeName, departmentName, basePay,hireDate));


                // 급여 명세서를 PDF로 생성
                byte[] pdfData = pdfGenerationService.generateSalarySlipPdf(employee.getName(), salaryDetails);

                // 메일 발송
                try {
                    if (mail != null && !mail.isEmpty()) {
                        emailService.sendSalaryTransferEmailWithPdf(mail, pdfData);
                    } else {
                        System.err.println("메일 주소가 없습니다. 직원 ID: " + employee.getId());
                    }
                } catch (MessagingException e) {
                    System.err.println("메일 발송 중 오류 발생: " + e.getMessage());
                    e.printStackTrace();  // 메일 발송 오류를 로깅
                }

                // 급여 계산 결과 반환 (선택 사항)
                PaySalaryResponse response = new PaySalaryResponse(employee.getId(), bonus, deductions, finalPay, Timestamp.valueOf(payDate));
                // 필요한 경우 response를 로깅하거나 다른 처리
            } catch (Exception e) {
                System.err.println("급여 계산 중 오류 발생: " + e.getMessage());
                e.printStackTrace();
            }
        }
    }

급여를 정산해주는 PayService이다.

 

직원의 리스트를 가져와서 직원별 급여를 계산한다. 급여 계산 이후 PaySalaryRequestDto 객체를 생성해서 DB에 저장하고

 

pdfGenerationService에 보내줄 salaryDetails 객체를 만들어 준다. pdf파일에 들어가야할 값들이 담긴 객체이다.

 

이 객체를 만들어 준 후 pdfGenerationService의 인자값으로 넘겨준 후

 

메일 발송이 완료가 되면서 급여 정산 로직이 끝이난다.

 

 

EmailService

    // 급여 이체 알림 메일
    public void sendSalaryTransferEmailWithPdf(String to, byte[] pdfData) throws MessagingException {
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

        helper.setTo(to);
        helper.setSubject("급여 이체 완료 안내");
        helper.setText(
                "<h1>급여 이체 완료</h1>" +
                        "<p>사원님의 급여 이체가 완료되었습니다.</p>",
                true
        );
        helper.setFrom(fromEmail);

        // PDF 첨부
        helper.addAttachment("급여명세서.pdf", new ByteArrayResource(pdfData));

        mailSender.send(message);
    }

메일 서비스를 담당하는 로직이다.

해당 사원들의 이메일을 PayService에서 받아와서 각 메일에 급여 명세서 메일을 뿌린다.

 

// PDF 첨부
helper.addAttachment("급여명세서.pdf", new ByteArrayResource(pdfData));

pdf를 첨부하는 로직

 

 

PdfGenerationService

@Service
public class PdfGenerationService {

    private static final String KOREAN_FONT_PATH = "classpath:/fonts/NanumGothic-Regular.ttf";
    private final ResourceLoader resourceLoader;

    @Autowired
    public PdfGenerationService(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    public byte[] generateSalarySlipPdf(String employeeName, List<SalaryDetailRequest> salaryDetails) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        PdfWriter writer = new PdfWriter(byteArrayOutputStream);
        PdfDocument pdfDoc = new PdfDocument(writer);
        Document document = new Document(pdfDoc);

        // 폰트 설정
        Resource resource = resourceLoader.getResource(KOREAN_FONT_PATH);
        File tempFontFile = Files.createTempFile("nanumfont", ".ttf").toFile();
        try (InputStream inputStream = resource.getInputStream();
             FileOutputStream outputStream = new FileOutputStream(tempFontFile)) {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
            }
        }
        PdfFont koreanFont = PdfFontFactory.createFont(
                tempFontFile.getAbsolutePath(),
                PdfEncodings.IDENTITY_H,
                PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED
        );
        document.setFont(koreanFont);

        String payDateStr = new SimpleDateFormat("yyyy-MM-dd").format(salaryDetails.get(0).getPayDate());
        String payMonth = new SimpleDateFormat("MM월").format(salaryDetails.get(0).getPayDate());

        // 제목
        document.add(new Paragraph(payMonth + " 급여명세서")
                .setFontSize(22)
                .setBold()
                .setTextAlignment(TextAlignment.CENTER)
                .setMarginBottom(15));

        // 기본 정보 테이블
        String department = salaryDetails.get(0).getDepartmentName();
        String position = salaryDetails.get(0).getGradeName();
        Timestamp joinDate = salaryDetails.get(0).getHireDate();
        // 날짜 포맷팅
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // 원하는 형식으로 설정
        String formattedJoinDate = sdf.format(joinDate);

        Table infoTable = new Table(new float[]{100, 200, 100, 200});
        infoTable.setWidth(UnitValue.createPercentValue(100));
        infoTable.addCell(createHeaderCell("이름"));
        infoTable.addCell(createInfoCell(employeeName));
        infoTable.addCell(createHeaderCell("직급"));
        infoTable.addCell(createInfoCell(position));
        System.out.println("직급 : " + position);
        infoTable.addCell(createHeaderCell("부서"));
        infoTable.addCell(createInfoCell(department));
        System.out.println("부서 : " + department);
        infoTable.addCell(createHeaderCell("입사일"));
        infoTable.addCell(createInfoCell(formattedJoinDate));
        infoTable.setMarginBottom(20);
        document.add(infoTable);

        // 세부 급여 테이블 (좌측: 지급, 우측: 공제)
        Table detailTable = new Table(new float[]{100, 100, 100, 100});
        detailTable.setWidth(UnitValue.createPercentValue(100));

// 헤더: 지급 항목(2칸 병합), 공제 항목(2칸 병합)
        detailTable.addCell(createSummaryHeaderCell("지급 항목", 2));
        detailTable.addCell(createSummaryHeaderCell("공제 항목", 2));

// 더미 데이터 추가
        BigDecimal totalPay = BigDecimal.ZERO;
        BigDecimal totalDeduction = BigDecimal.ZERO;

        BigDecimal bonus1 = new BigDecimal("1000000");  // 보너스 예시
        BigDecimal bonus2 = new BigDecimal(String.valueOf(salaryDetails.get(0).getBonus()));   // 보너스 예시
        BigDecimal deductions1 = new BigDecimal("150000");  // 공제 예시 (세금)
        BigDecimal deductions2 = new BigDecimal("200000");  // 공제 예시 (기존 보험료)

// 총합 계산
        totalPay = totalPay.add(salaryDetails.get(0).getBasePay()).add(salaryDetails.get(0).getBonus());
//        totalDeduction = totalDeduction.add(deductions1).add(deductions2);

// 기본급, 세금, 보너스 항목 추가
        detailTable.addCell(createCell("기본급"));
        detailTable.addCell(createCell(formatMoney(salaryDetails.get(0).getBasePay())));
        detailTable.addCell(createCell("세금"));
        detailTable.addCell(createCell(formatMoney(salaryDetails.get(0).getDeductions())));

// 보너스 추가
        detailTable.addCell(createCell("보너스"));
        detailTable.addCell(createCell(formatMoney(bonus2)));

// 삭제된 보험료 항목을 제외한 공제 항목 추가
        detailTable.addCell(createCell("공제 총액"));
        detailTable.addCell(createCell(formatMoney(salaryDetails.get(0).getDeductions())));

// 마지막 줄: 총계
        detailTable.addCell(createHeaderCell("지급 합계"));
        detailTable.addCell(createHeaderCell(formatMoney(totalPay)));
        detailTable.addCell(createHeaderCell("공제 합계"));
        detailTable.addCell(createHeaderCell(formatMoney(salaryDetails.get(0).getDeductions())));

        document.add(detailTable.setMarginBottom(20));

        BigDecimal finalPay = totalPay.subtract(totalDeduction);

// 요약 테이블
        Table summaryTable = new Table(new float[]{300, 200});
        summaryTable.setWidth(UnitValue.createPercentValue(100));
        summaryTable.addHeaderCell(createSummaryHeaderCell("요약 항목"));
        summaryTable.addHeaderCell(createSummaryHeaderCell("합계 (원)"));

        summaryTable.addCell(createSummaryCell("지급 총액"));
        summaryTable.addCell(createSummaryCell(formatMoney(totalPay)));
        summaryTable.addCell(createSummaryCell("공제 총액"));
        summaryTable.addCell(createSummaryCell(formatMoney(salaryDetails.get(0).getDeductions())));
        summaryTable.addCell(createSummaryCell("실 지급액"));
        summaryTable.addCell(createSummaryCell(formatMoney(salaryDetails.get(0).getFinalPay())));

        document.add(summaryTable.setMarginBottom(30));


        // 하단 문구
        document.add(new Paragraph("지급일: " + payDateStr)
                .setFontSize(9)
                .setFontColor(ColorConstants.GRAY)
                .setTextAlignment(TextAlignment.RIGHT));

        document.close();
        return byteArrayOutputStream.toByteArray();
    }

    private Cell createCell(String text) {
        return new Cell().add(new Paragraph(text))
                .setPadding(5)
                .setBorder(new SolidBorder(0.5f));
    }

    private Cell createHeaderCell(String text) {
        return new Cell().add(new Paragraph(text).setBold())
                .setBackgroundColor(ColorConstants.LIGHT_GRAY)
                .setPadding(5)
                .setBorder(new SolidBorder(1));
    }

    private Cell createInfoCell(String text) {
        return new Cell().add(new Paragraph(text))
                .setPadding(5)
                .setBorder(new SolidBorder(0.5f));
    }

    private Cell createSectionHeaderCell(String text) {
        return new Cell().add(new Paragraph(text).setBold())
                .setBackgroundColor(ColorConstants.LIGHT_GRAY)
                .setPadding(7)
                .setTextAlignment(TextAlignment.CENTER)
                .setBorder(new SolidBorder(1.0f));
    }

    private Cell createDetailCell(String text) {
        return new Cell().add(new Paragraph(text))
                .setPadding(5)
                .setBorder(new SolidBorder(0.5f));
    }

    private Cell createSummaryHeaderCell(String text) {
        return new Cell().add(new Paragraph(text).setBold())
                .setBackgroundColor(ColorConstants.GRAY)
                .setFontColor(ColorConstants.WHITE)
                .setTextAlignment(TextAlignment.CENTER)
                .setPadding(6)
                .setBorder(new SolidBorder(1));
    }

    private Cell createSummaryHeaderCell(String text, int colspan) {
        return new Cell(1, colspan).add(new Paragraph(text).setBold())
                .setBackgroundColor(ColorConstants.GRAY)
                .setFontColor(ColorConstants.WHITE)
                .setTextAlignment(TextAlignment.CENTER)
                .setPadding(6)
                .setBorder(new SolidBorder(1));
    }

    private Cell createSummaryCell(String text) {
        return new Cell().add(new Paragraph(text).setBold())
                .setPadding(6)
                .setBorder(new SolidBorder(1));
    }

    private String formatMoney(BigDecimal amount) {
        return String.format("%,.0f", amount);
    }
}

itext는 한글지원이 안되는 부분이 있다. 처음에는 모르고 사용을하다가 다른건 다 출력이 되는데 한글만 자꾸 출력이 안되는 부분이 있어서

 

찾아보니까 한글 폰트를 다운받아서 직접 적용을 해줘야 한다고 한다. 

 

그래서 src - resources - fonts - 여기에 폰트 파일을 넣어주고 classpath로 불러들어와서 한글폰트를 적용시켜줬다.

 

itex를 사용하면 백엔드에서도 테이블을 만들 수 있다.

 

각 테이블에 급여명세서에 필요한 값들을 넣어주고 테이블 색상등을 설정 할 수 있다. 

 

급여 로직은 스케줄러를 사용해서 매달 15일날 작동이 가능하게끔 만들었지만 테스트를위해서 수동 컨트롤러를 만들었다.

 

수동 컨트롤러를 작동 시키면

 

 

급여명세서가 메일로 pdf 파일로 전송이 된다. 이후 파일을 열어보면 테이블 형식의 급여명세서를 확인할 수 있다.