PDF 파일 다운로드 API
1단계
데이터베이스에서 PDF 파일에 넣을 데이터 조회하기
2단계
조회한 데이터를 Thymeleaf 템플릿 엔진을 통해 HTML에 랜더링 해주기
3단계
생성된 HTML를 “flying-saucer-pdf” 라이브러리를 통해 PDF 파일로 변환하기
(위 라이브러리 말고도 “itextpdf”가 있음)
build.gradle
// pdf
implementation("org.xhtmlrenderer:flying-saucer-pdf:9.1.20")
https://mvnrepository.com/artifact/org.xhtmlrenderer/flying-saucer-pdf
PdfController.class
@Operation(summary = "모집 결과 PDF 파일로 다운하기", description = "특정 모집의 결과를 조회하여 PDF 파일을 다운합니다.")
@Parameter(name = "recruitId", description = "모집 ID")
@PostMapping("/recruits/{recruitId}/export")
public ResponseEntity<ByteArrayResource> exportLottery(
@PathVariable("recruitId") Long recruitId) {
//** 1단계 처리 **//
List<RecruitResult> recruitResult = lotteryQueryService.getRecruitResult(recruitId);
RecruitInfo recruitInfo = recruitQueryService.getRecruitInfo(recruitId);
byte[] pdfBytes = pdfUtil.resultPdf(recruitResult, recruitInfo);
ByteArrayResource resource = new ByteArrayResource(pdfBytes);
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=result.pdf");
headers.add(HttpHeaders.CONTENT_TYPE, "application/pdf");
return new ResponseEntity<>(resource, headers, HttpStatus.OK);
}
URL을 /recruits/{recruitId}/export로 설계하였습니다.
→ 내가 구현한 API는 한 모집의 추첨 결과를 파일 형식으로 다운로드하게 하는 것이다.
export를 url에 넣은 이유는 다른 API와 좀 더 명확하게 구분이 되도록 컨트롤 자원 개념으로 추가하였습니다.
PdfUtil.class
@Service
@RequiredArgsConstructor
public class PdfUtil {
private final TemplateEngine templateEngine;
public byte[] resultPdf(List<RecruitResult> results, RecruitInfo recruitInfo) {
try {
//** 2단계 처리 **//
Context context = new Context();
context.setVariable("results", results);
context.setVariable("info", recruitInfo);
String exportHtml = templateEngine.process("exportResult", context);
//** 3단계 처리 **//
ByteArrayOutputStream pdfStream = new ByteArrayOutputStream();
ITextRenderer renderer = new ITextRenderer();
renderer.getFontResolver()
.addFont(
new ClassPathResource("NanumGothic.ttf")
.getURL()
.toString(),
BaseFont.IDENTITY_H,
BaseFont.EMBEDDED);
renderer.setDocumentFromString(exportHtml);
renderer.layout();
renderer.createPDF(pdfStream);
pdfStream.close();
return pdfStream.toByteArray();
} catch (Exception e) {
throw new ApiException(ErrorStatus._RECRUIT_EXPORT_ERROR);
}
}
}
- Context에 랜더링할 변수를 저장합니다.
- TemplateEngine을 통해 Html과 Context를 처리합니다.
- 생성된 exportHtml 값을 ITextRenderer를 통해 PDF로 변환합니다.
한글 깨짐 문제~!!
영어는 그대로 잘 변환이 되지만, 한글은 깨지는 문제가 있기 때문에 한글 폰트를 설정해주어야 합니다.
(아래 구글 폰트 경로에서 다운하기)
https://fonts.google.com/specimen/Nanum+Gothic?subset=korean&script=Kore
(우리는 무료로 구글 폰트에서 나눔 고딕을 다운하겠습니다)
다운한 *. ttf 파일을 resources 파일 안에 추가합니다.
renderer.getFontResolver()
.addFont(new ClassPathResource("NanumGothic.ttf")
.getURL()
.toString(),
BaseFont.IDENTITY_H,
BaseFont.EMBEDDED);
ClassPathResource()를 통해 폰트 파일의 위치를 불러옵니다.
- Spring에서 기본으로 resources/static 까지 리소스 경로를 설정해 준다고 해서 /static/font/. ttf로 두고 불러오는데, 파일을 찾을 수 없다고 해서 resources/. ttf 위치에 두었습니다.
다시 Controller : 전송 부분
//** Controller **//
byte[] pdfBytes = pdfUtil.resultPdf(recruitResult, recruitInfo);
ByteArrayResource resource = new ByteArrayResource(pdfBytes);
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=result.pdf");
headers.add(HttpHeaders.CONTENT_TYPE, "application/pdf");
return new ResponseEntity<>(resource, headers, HttpStatus.OK);
- ByteArrayResource는 Byte 배열을 HTTP 응답의 콘텐츠로 변환하는 데 사용합니다.
- new ByteArrayResource(pdfBytes)를 통해 PDF 파일의 바이트 배열을 감싸 전송할 수 있는 형태로 만듭니다.
- HttpHeaders는 HTTP 요청이나 응답에서 사용할 헤더 정보를 설정하는 데 사용합니다.
- headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=result.pdf")
- HttpHeaders.CONTENT_DISPOSITION 응답의 콘텐츠가 파일 다운로드로 처리됨을 명시합니다.
- "attachment; filename=result.pdf"는 브라우저가 이 응답을 파일로 처리하고, 다운로드할 파일의 이름을 지정합니다.
- headers.add(HttpHeaders.CONTENT_TYPE, "application/pdf") 응답 콘텐츠가 PDF 파일임을 명시합니다.
참고자료
https://devnm.tistory.com/32#2.%20%ED%95%9C%EA%B8%80%20%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-1
https://mvnrepository.com/artifact/org.xhtmlrenderer/flying-saucer-pdf
https://fonts.google.com/specimen/Nanum+Gothic?subset=korean&script=Kore
'Spring Framework > Spring' 카테고리의 다른 글
[Spring] Spring Batch Tasklet 작업 단위 이해하기 - StepContribution, ChunkContext (0) | 2024.10.07 |
---|---|
[Spring] MessageSource, messages_en.properties 파일에서 작은 따옴표 및 특수 문자 처리 오류 해결 방법 (2) | 2024.10.03 |
[Spring] 스프링 reactive-stream, 비동기 통신 이해하기 (0) | 2024.06.23 |
[Spring Cloud] Spring Config Server를 이용해 설정 파일 관리하기 (0) | 2024.04.01 |
[Spring Cloud] Eureka Server, Discovery Service 이해하기 (0) | 2024.03.31 |