Java itext-pdf적용해보기
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 파일로 전송이 된다. 이후 파일을 열어보면 테이블 형식의 급여명세서를 확인할 수 있다.