안녕하세요

오랜만에 글을 작성하네요

구글 Firebase Admin sdk 를 사용한 FCM 발송에 대해 간단하게 다루려고합니다.

 

개발 환경 : java 11 , Spring Boot 2.7.x

 

소스 내부에 주석으로 참고 라고 남겨둔 부분은 참고하시면 아마 도움이 되실 것 같습니다

소스 파일 우선 첨부합니다

 

package fcm.firebase;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

import static org.springframework.util.StringUtils.hasText;

/*
    @Author : 전남혁 ( all_step@naver.com )
    @Description : FCM 푸시 알림
    @Created : 2023.05.24
    @Version : 1.0.0
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
public class Fcm {

    /* ========================================= GOOGLE FCM =================================================
        사용 라이브러리 : implementation 'com.google.firebase:firebase-admin:9.1.1' // Firebase Admin SDK
        * 1. firebase console 접속
        * 2. 프로젝트 생성
        * 3. 프로젝트 설정 -> 서비스 계정 -> 새 비공개 키 생성 -> json 파일 다운로드 ( fcm.key.location 에 경로 저장 )
        * 4. 프로젝트 설정 -> 클라우드 메시징 -> 서버 키 복사 ( 현재 미사용 )


        * 참고 : MulticastMessage 는 한번에 최대 500명에게 전송 가능


        * 참고 : 다중 전송의 BatchResponse 는 아래와 같이 사용
        int getFailureCount()               : 실패한 메세지 수
        List<SendResponse> getResponses()   : 전송 결과
        int getSuccessCount()               : 성공한 메세지 수


        * 참고 : 링크 관련
        앱은 링크를 별첨 할수 없음 (https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?hl=ko#Message.FIELDS.data)
        네이티브 앱에선 deep link 관련 컨트롤이 가능.. 하이브리드 앱에선 불가
        ( 네이티브 앱 추가 참고 : https://firebase.google.com/docs/dynamic-links?hl=ko )
        Chrome 에서 발송되는 web push 알림은 링크를 별첨 할수 있음
        Message.builder()
                    .setNotification(Notification.builder()
                            .setTitle(title)
                            .setBody(body)
                            .setImage(imageUrl)
                            .build())
                    .setToken(targetToken)
                    .setWebpushConfig(WebpushConfig.builder().putData("link","https://www.naver.com").build())
            .build()


        * 참고 : 이미지 관련
        장치에 다운로드되어 알림에 표시될 이미지의 URL을 포함합니다.
        JPEG, PNG, BMP는 모든 플랫폼에서 완벽하게 지원됩니다.
        애니메이션 GIF 및 비디오는 iOS에서만 작동합니다.
        WebP 및 HEIF는 플랫폼 및 플랫폼 버전에 따라 다양한 수준의 지원을 제공합니다.
        Android는 1MB 이미지 크기 제한이 있습니다.
        Firebase 저장소에서 이미지 호스팅에 대한 할당량 사용 및 영향/비용: https://firebase.google.com/pricing
     */
    @Value("${fcm.key.location}")
    private String fcmKeyLocation;
    private final String fireBaseScope = "https://www.googleapis.com/auth/cloud-platform";





    // ============================================ FIREBASE INIT =============================================
    /* FCM 초기화 ( 필수 )
        application.properties 에서 fcm.key.location 의 경로에 있는 json 파일을 읽어서 초기화
        json 파일이 없으면 초기화 하지 않음
        이미 초기화 되었을 경우 하지 않음
    */
    public void init() {
        Path path = Paths.get(fcmKeyLocation);
        if (!Files.exists(path)) {
            log.info("NOT FOUND FCM KEY FILE");
            return;
        }
        Resource resource;
        InputStream stringInputStream;
        try {
            resource = new InputStreamResource(Files.newInputStream(path));
            stringInputStream = new ByteArrayInputStream(resource.getInputStream().readAllBytes());
        } catch (IOException e) {
            throw new RuntimeException("FCM KEY FILE IOException ",e);
        }

        if(resource == null || stringInputStream == null) {
            log.info("NOT FOUND FCM KEY FILE");
            throw new RuntimeException("FCM KEY FILE NOT FOUND");
        }

        try {
            FirebaseOptions options = FirebaseOptions.builder()
                    .setCredentials(GoogleCredentials.fromStream(stringInputStream)
                            .createScoped(List.of(fireBaseScope))
                    )
                    .build();
            log.info("Firebase initializeApp start : " + FirebaseApp.getApps().isEmpty());
            if (FirebaseApp.getApps().isEmpty()) {
                FirebaseApp.initializeApp(options);
                log.info("Firebase application has been initialized");
            }
        } catch (Exception e) {
            throw new RuntimeException("Firebase initializeApp error", e);
        }
    }
    // ================================================================================================





    // ================================== getAccessToken (현재 사용 X) ==================================
    public String getAccessToken() {
        GoogleCredentials googleCredentials;
        try {
            googleCredentials = GoogleCredentials
                    .fromStream(new ClassPathResource(fcmKeyLocation).getInputStream())
                    .createScoped(List.of(fireBaseScope));
        } catch (IOException e) {
            throw new RuntimeException("FCM KEY FILE IOException ",e);
        }

        if (googleCredentials == null) {
            throw new RuntimeException("FCM KEY FILE NOT FOUND");
        }

        return googleCredentials.getAccessToken().getTokenValue();
    }
    // =================================================================================================





    // =================================== FCM 단일건 메세지 객체 생성 ========================================
    /* 단일건 메세지 객체 생성
        @param targetToken : FCM 토큰
        @param title : 제목
        @param body : 내용
        @param imageUrl : 이미지 URL
        @return : Firebase Cloud Messaging 메세지 객체
    */
    public Message setSingleMessage(String title, String body, String imageUrl, String targetToken) {
        Message message;

        if (hasText(imageUrl)) {
            message = Message.builder()
                    .setNotification(Notification.builder()
                            .setTitle(title)
                            .setBody(body)
                            .setImage(imageUrl)
                            .build())
                    .setToken(targetToken)
                    .build();
        } else {
            message = Message.builder()
                    .setNotification(Notification.builder()
                            .setTitle(title)
                            .setBody(body)
                            .build())
                    .setToken(targetToken)
                    .build();
        }

        return message;
    }
    // ===================================================================================================





    // ================================= FCM 단일건 다중 전송 메세지 객체 생성 =====================================
    /* 한번에 여러명에게 FCM 메세지 전송을 위한 객체 생성
        @param targetToken : FCM 토큰 리스트
        @param title : 제목
        @param body : 내용
        @param imageUrl : 이미지 URL
        @return : 다중 FCM 메세지 객체
    */
    public MulticastMessage setMulticastMessage(String title, String body, String imageUrl, List<String> registrationTokens) {
        MulticastMessage multicastMessage;

        if (hasText(imageUrl)) {
            multicastMessage =  MulticastMessage.builder()
                    .setNotification(Notification.builder()
                            .setTitle(title)
                            .setBody(body)
                            .setImage(imageUrl)
                            .build())
                    .addAllTokens(registrationTokens)
                    .build();
        } else {
            multicastMessage =  MulticastMessage.builder()
                    .setNotification(Notification.builder()
                            .setTitle(title)
                            .setBody(body)
                            .build())
                    .addAllTokens(registrationTokens)
                    .build();
        }

        return multicastMessage;
    }
    // ===================================================================================================





    // ====================================== FCM 단일건 메세지 전송 ==========================================
    /* FCM 토큰으로 메시지 보내기
        @param Message : FCM 메세지 객체
        @return : FCM 응답
    */
    public String sendSingleMessage(Message message) {
        String response;
        try {
            response = FirebaseMessaging.getInstance().send(message);
        } catch (Exception e) {
            throw new RuntimeException("Firebase sendSingleMessage error", e);
        }
        return response;
    }
    // ===================================================================================================




    // ====================================== FCM 여러종류의 메세지 전송 ==========================================
    /* 메세지 여러번 보내기
        @param List<Message> : FCM 메세지 객체
        @return : FCM 응답 ( 객체 타입 )
    */
    public BatchResponse sendListMessage(List<Message> messageList) {
        BatchResponse response;
        try {
            response = FirebaseMessaging.getInstance().sendAll(messageList);
        } catch (Exception e) {
            throw new RuntimeException("Firebase sendListMessage error", e);
        }
        return response;
    }
    // ===================================================================================================




    // ====================================== FCM 단일 메세지 다중 전송 ==========================================
    /* 여러명에게 한번에 메세지 보내기
        @param MulticastMessage : FCM 다중 메세지 객체
        @return : FCM 응답 ( 객체 타입 )
    */
    public BatchResponse sendMultiMessage(MulticastMessage multicastMessage) {
        BatchResponse response;
        try {
            response = FirebaseMessaging.getInstance().sendMulticast(multicastMessage);
        } catch (Exception e) {
            throw new RuntimeException("Firebase sendMultiMessage error", e);
        }
        return response;
    }
    // ===================================================================================================
}

 

 

한명에게만 전송할땐 Message 타입을, 여러명에게 전송할떈 MulticastMessage 타입을 사용하여

메세지객체를 List로 담아 발송하면 여러번의 Http 전송이 일어나지만

MulticastMessage로 여러명에 발송하면, 한번의 Http 전송으로 대량 발송이 가능한점이 좋아 같이 쓰게 되었습니다

Fcm 전송시 List<Message> 타입으로 전송할땐 다른메세지를 한번에 만들어 전송할때 사용하기 좋아 보였습니다

 

예시 컨트롤러도 아래 첨부합니다

 

package fcm.push.controller;

import fcm.common.JSONResponse;
import fcm.member.dto.MemberCondition;
import fcm.member.dto.MemberListDto;
import fcm.member.service.MemberService;
import fcm.push.dto.AdminPushDetailDto;
import fcm.push.dto.AdminPushListDto;
import fcm.push.dto.PushCondition;
import fcm.push.dto.PushWriteDto;
import fcm.push.service.PushService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

import static org.springframework.util.StringUtils.hasText;

@Slf4j
@Controller
@RequiredArgsConstructor
public class PushController {
    private final PushService pushService;
    private final MemberService memberService;

    @GetMapping("/admin/push/list")
    public String pushList(@ModelAttribute("condition") PushCondition condition,
                           Model model) {
        if(condition.getPage() < 1)
            condition.setPage(1);

        if(condition.getRowPerPage() == null)
            condition.setRowPerPage(20);

        Pageable pageable = PageRequest.of(condition.getPage() - 1, condition.getRowPerPage());
        Page<AdminPushListDto> pushList = pushService.getAdminPushPage(condition, pageable);
        model.addAttribute("pushList", pushList);
        return "admin/member/push_list";
    }

    @GetMapping("/admin/push/write")
    public String pushWrite(Model model) {
        return "admin/member/push_write";
    }


    @GetMapping("/admin/push/member/list")
    public String pushMemberList(@ModelAttribute("condition") MemberCondition condition,
                                 Model model) {

        List<MemberListDto> memberList = new ArrayList<>();
        if(hasText(condition.getName())) {
           memberList = memberService.getMemberPopupList(condition);
        }

        model.addAttribute("memberList", memberList);
        return "admin/popup/member_search";
    }

    @PostMapping("/admin/push/save")
    @ResponseBody
    public JSONResponse<?> pushSave(@RequestBody PushWriteDto pushWriteDto) {

        try {
            pushService.savePush(pushWriteDto);
        } catch (Exception e) {
            log.error("푸시 발송에 실패하였습니다.", e);
            return new JSONResponse<>(500, "푸시 발송에 실패하였습니다.", null);
        }

        return new JSONResponse<>(200, "푸시 발송에 성공하였습니다.", null);
    }

    @GetMapping("/admin/push/detail/{id}")
    public String pushDetail(@PathVariable("id") Long id,
                             Model model) {
        AdminPushDetailDto push = pushService.getAdminPushDetail(id);
        model.addAttribute("dto", push);
        return "admin/member/push_view";
    }

    @PostMapping("/admin/push/update")
    @ResponseBody
    public JSONResponse<?> pushUpdate(@RequestBody PushWriteDto pushWriteDto) {

        try {
            pushService.updatePush(pushWriteDto);
        } catch (Exception e) {
            log.error("푸시 발송에 실패하였습니다.", e);
            return new JSONResponse<>(500, "푸시 발송에 실패하였습니다.", null);
        }

        return new JSONResponse<>(200, "푸시 발송에 성공하였습니다.", null);
    }
}

 

 

사용시 참고하실 부분은 application.properties에 json 파일 경로 ( Fcm.java 참고 주석 ) 를 명확히 써주셔야 작동합니다.

 

 

질문이나 피드백 댓글로 남겨주시면 확인하고 답변 남기겠습니다.

 

 

 

 

https://github.com/wjsskagur/fcm_only

 

GitHub - wjsskagur/fcm_only: Google Firebase Admin SDK를 사용한 Spring Boot 백엔드에서 앱 푸쉬 알림 발송

Google Firebase Admin SDK를 사용한 Spring Boot 백엔드에서 앱 푸쉬 알림 발송 - GitHub - wjsskagur/fcm_only: Google Firebase Admin SDK를 사용한 Spring Boot 백엔드에서 앱 푸쉬 알림 발송

github.com

 

 

이번엔 두번째 글로 자바에서 여러개의 파일을 하나의 zip으로 압축하여 다운로드하는 과정을 진행해 보려고 합니다.

IDE : intelliJ

자바 : Java 17

스프링 부트 : Spring Boot 3.0.2

템플릿 엔진 : Thymeleaf

빌드 도구 : Gradle

그외 Lombok , spring boot web

 

작성자의 OS는 Mac입니다. ( Window의 경우 파일 디렉토리 확인 바랍니다. )

 

 

 

 

 

 

 

 

 

src 하위 패키지 구조 입니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

우선 DTO 입니다.

압축 될 파일의 이름과 파일을 담아둘 List 변수를 같이 선언해둡니다.

@Getter
@Setter
public class DownloadDto {
    private String zipFileName; // 압축될 파일 이름 (xxxx.zip)
    private List<String> sourceFiles; // 압축될 파일 리스트
}

 

바로 실제 압축 동작이 실행되는 소스 입니다.

@Component
@RequiredArgsConstructor
public class DownloadUtil {

    public void downloadZip(DownloadDto downloadDto, HttpServletResponse response) {
        
        // 압축될 파일명이 존재하지 않을 경우
        if(downloadDto.getZipFileName() == null || "".equals(downloadDto.getZipFileName()))
            throw new IllegalArgumentException("파일명이 존재하지 않습니다.");
        
        // 파일이 존재하지 않을 경우
        if(downloadDto.getSourceFiles() == null || downloadDto.getSourceFiles().size() == 0)
            throw new IllegalArgumentException("파일이 존재하지 않습니다.");
        
        // ======================== 파일 다운로드 위한 response 세팅 ========================
        response.setContentType("application/zip");
        response.setHeader("Content-Disposition", "attachment; filename=" + new String(downloadDto.getZipFileName().getBytes(StandardCharsets.UTF_8)) + ".zip;");
        response.setStatus(HttpServletResponse.SC_OK);
        // =============================================================================
        
        // 본격적인 zip 파일을 만들어 내기 위한 로직
        try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())){
            
            // List<String> 변수에 담아두었던 파일명을 검색한다
            for (String sourceFile : downloadDto.getSourceFiles()) {
                Path path = Path.of(sourceFile);
                try (FileInputStream fis = new FileInputStream(path.toFile())) {
                    // 압축될 파일명을 ZipEntry에 담아준다
                    ZipEntry zipEntry = new ZipEntry(path.getFileName().toString());
                    // 압축될 파일명을 ZipOutputStream 에 담아준다
                    zos.putNextEntry(zipEntry);
                    byte[] buffer = new byte[1024];
                    int length;
                    while ((length = fis.read(buffer)) >= 0) {
                        zos.write(buffer, 0, length);
                    }
                } catch (FileNotFoundException e) {
                    response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                    throw new IllegalArgumentException("파일 변환 작업중, [ " + sourceFile + " ] 파일을 찾을 수 없습니다.");
                } catch (IOException e) {
                    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                    throw new IllegalArgumentException("파일 변환 작업중, [ " + sourceFile + " ] 파일을 다운로드 할 수 없습니다.");
                } finally {
                    // ZipOutputStream 에 담아둔 압축될 파일명을 flush 시켜준다
                    zos.flush();
                    // ZipOutputStream 에 담아둔 압축될 파일명을 close 시켜준다
                    zos.closeEntry();
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                // response 에 담아둔 파일을 flush 시켜준다
                response.flushBuffer();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 

파일의 경로를 찾은 후 ZipEntry에 담아 압축파일을 생성하는 과정입니다

.

햇갈리실 수 있는 List<String> 에 변수를 담은 컨트롤러 부분입니다.

 

@Controller
@RequiredArgsConstructor
public class DownloadController {
    private final DownloadUtil downloadUtil;

    @GetMapping("/download/zip")
    public void downloadZip(HttpServletResponse response) {
        // 파일 작업처리를 위한 DTO 선언
        DownloadDto downloadDto = new DownloadDto();
        // 압축할 파일 일므 지정
        downloadDto.setZipFileName("imageZip");
        // 파일 경로를 담아둘 List 선언
        List<String> downloadFileList = new ArrayList<>();

        // ===================================== 여기에 파일 경로를 넣어주세요 ==============================================================
        // 작성자는 Mac 기준으로 작성했습니다.
        downloadFileList.add("/Users/jeon/Documents/JavaProject/Blog/zip/src/main/resources/static/images/beach.jpg");
        downloadFileList.add("/Users/jeon/Documents/JavaProject/Blog/zip/src/main/resources/static/images/bird.jpg");
        downloadFileList.add("/Users/jeon/Documents/JavaProject/Blog/zip/src/main/resources/static/images/corgi.jpg");
        // Window 예시
        // downloadFileList.add("C:\\Users\\jeon\\Documents\\JavaProject\\Blog\\zip\\src\\main\\resources\\static\\images\\beach.jpg");
        // downloadFileList.add("C:\\Users\\jeon\\Documents\\JavaProject\\Blog\\zip\\src\\main\\resources\\static\\images\\bird.jpg");
        // downloadFileList.add("C:\\Users\\jeon\\Documents\\JavaProject\\Blog\\zip\\src\\main\\resources\\static\\images\\bird.jpg");
        downloadDto.setSourceFiles(downloadFileList);
        // ============================================================================================================================


        // 데이터를 담은 DTO 를 압축 파일 생성 및 다운로드를 위한 메소드에 전달
        try {
            downloadUtil.downloadZip(downloadDto, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

실행 화면 입니다

 

해당 소스 GitHub : https://github.com/wjsskagur/zip

 

첫 글로 Java PDF 생성시 많이 사용하고 있는 itext를 이용하여 HTML페이지를 CSS와 폰트도 함께 적용하여,

PDF를 만들고 또한 라이브 서버 jar 배포시 대응 하기 위한 내용도 같이 포함하여 진행해 보겠습니다.

 

우선 시작하기에 앞서 환경은 아래와 같습니다.

 

IDE : intelliJ

자바 : Java 11

스프링 부트 : Spring Boot 2.7.7

템플릿 엔진 : Thymeleaf

빌드 도구 : Gradle

그외 Lombok , spring boot web

 

작성자의 OS는 Mac입니다. ( Window의 경우 파일 디렉토리 확인 바랍니다. )

 

 

사용한 itext 라이브러리 입니다.

// ====================================================================================================
// HTML TO PDF LIBRARY
implementation 'com.itextpdf:itextpdf:5.5.13.3'
implementation 'com.itextpdf.tool:xmlworker:5.5.13.3'
implementation 'com.itextpdf:pdfa:7.2.3'
// ====================================================================================================

 

 

 

대략적인 프로젝트 구조입니다.

itext 패키지에 컨트롤러와 DTO ,

실제 기능이 구현될 Util을 만들어 두었습니다.

그리고 호출될 CSS와 호출해볼 이미지를 미리 준비해 놓았습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

templates 하위 index.html의 소스입니다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Hello itext pdf</title>
</head>
<body>
    <h1>Hello itext pdf</h1>
    <p>Click <a href="/attachment/pdf">here</a> to download the pdf</p>
</body>
</html>

itextPdf.css에서 적용할 간단한 스타일 입니다.

body{font-family: AppleSDGothicNeo, sans-serif; font-size:12px; color:#222; background:#fff; height: 100%;}
p{background-color: #e73a3a;}
img{border:0; vertical-align:middle;}

 

사용할 DTO 입니다

@Getter
@Setter
public class ItextPdfDto {
    private String pdfCode; // pdf 종류가 여러개일 경우 html 을 구분하기 위한 코드
    private String pdfFilePath; // pdf 파일이 저장될 경로
    private String pdfFileName; // pdf 파일명
}

 

 

이어서

ItextPdfUtil 파일에 하나씩 기술 하겠습니다.

 

임의로 정의된 html을 담아둘 메소드입니다

* 현재 글에선 hyeok만 사용 할 예정입니다*

// 사용할 html 코드를 가져오는 메소드
public String getHtml(String code) {

    String return_html = "";

    switch (code) {
        case "jeon" :
            return_html = "<html>" +
                    "<body>" +
                    "<h1>jeon</h1>" +
                    "</body>" +
                    "</html>";
            break;
        case "nam" :
            return_html = "<html>" +
                    "<body>" +
                    "<h1>nam</h1>" +
                    "<p>CSS 테스트 입니다.</p>" +
                    "</body>" +
                    "</html>";
            break;
        case "hyeok" :
            return_html = "<html>" +
                    "<body>" +
                    "<h1>hyeok</h1>" +
                    "<p>이미지 테스트 합니다.</p>" +
                    "<img src='http://localhost:8080/images/test.png' />" +
                    "</body>" +
                    "</html>";
            break;
    }

    return return_html;
}

 

 

 

실제 itext가 사용되는 메소드 입니다 ( ItextPdfUtil 안에 있습니다 )

/*
     * iText 라이브러리를 사용한 PDF 파일 생성
     * CSS , Font 설정 기능 포함
     * */
    public void createPDF(ItextPdfDto itextPdfDto) {

        // 최초 문서 사이즈 설정
        Document document = new Document(PageSize.B4, 30, 30, 30, 30);

        try {
            // PDF 파일 생성
            PdfWriter pdfWriter = PdfWriter.getInstance(document, new FileOutputStream(itextPdfDto.getPdfFilePath()+itextPdfDto.getPdfFileName()));
            // PDF 파일에 사용할 폰트 크기 설정
            pdfWriter.setInitialLeading(12.5f);
            // PDF 파일 열기
            document.open();

            // XMLWorkerHelper xmlWorkerHelper = XMLWorkerHelper.getInstance();

            // CSS 설정 변수 세팅
            CSSResolver cssResolver = new StyleAttrCSSResolver();
            CssFile cssFile = null;

            try {
                /*
                 * CSS 파일 설정
                 * 기존 방식은 FileInputStream을 사용했으나, jar 파일로 빌드 시 파일을 찾을 수 없는 문제가 발생
                 * 따라서, ClassLoader를 사용하여 파일을 읽어오는 방식으로 변경
                 */
                InputStream cssStream = getClass().getClassLoader().getResourceAsStream("static/css/ItextPdf.css");

                // CSS 파일 담기
                cssFile = XMLWorkerHelper.getCSS(cssStream);
//                cssFile = XMLWorkerHelper.getCSS(new FileInputStream("src/main/resources/static/css/test.css"));
            } catch (Exception e) {
                throw new IllegalArgumentException("PDF CSS 파일을 찾을 수 없습니다.");
            }

            if(cssFile == null) {
                throw new IllegalArgumentException("PDF CSS 파일을 찾을 수 없습니다.");
            }

            // CSS 파일 적용
            cssResolver.addCss(cssFile);

            // PDF 파일에 HTML 내용 삽입
            XMLWorkerFontProvider fontProvider = new XMLWorkerFontProvider(XMLWorkerFontProvider.DONTLOOKFORFONTS);

            /*
             * 폰트 설정
             * CSS 와 다르게, fontProvider.register() 메소드를 사용하여 폰트를 등록해야 함
             * 해당 메소드 내부에서 경로처리를 하여 개발, 배포 시 폰트 파일을 찾을 수 있도록 함
             * */
            try {
                fontProvider.register("static/font/AppleSDGothicNeoR.ttf", "AppleSDGothicNeo");
            } catch (Exception e) {
                throw new IllegalArgumentException("PDF 폰트 파일을 찾을 수 없습니다.");
            }

            if(fontProvider.getRegisteredFonts() == null) {
                throw new IllegalArgumentException("PDF 폰트 파일을 찾을 수 없습니다.");
            }

            // 사용할 폰트를 담아두었던 내용을
            // CSSAppliersImpl에 담아 적용
            CssAppliers cssAppliers = new CssAppliersImpl(fontProvider);

            // HTML Pipeline 생성
            HtmlPipelineContext htmlPipelineContext = new HtmlPipelineContext(cssAppliers);
            htmlPipelineContext.setTagFactory(Tags.getHtmlTagProcessorFactory());

            // ========================================================================================
            // Pipelines
            PdfWriterPipeline pdfWriterPipeline = new PdfWriterPipeline(document, pdfWriter);
            HtmlPipeline htmlPipeline = new HtmlPipeline(htmlPipelineContext, pdfWriterPipeline);
            CssResolverPipeline cssResolverPipeline = new CssResolverPipeline(cssResolver, htmlPipeline);
            // ========================================================================================


            // ========================================================================================
            // XMLWorker
            XMLWorker xmlWorker = new XMLWorker(cssResolverPipeline, true);
            XMLParser xmlParser = new XMLParser(true, xmlWorker, StandardCharsets.UTF_8);
            // ========================================================================================


            /* HTML 내용을 담은 String 변수
            주의점
            1. HTML 태그는 반드시 닫아야 함
            2. xml 기준 html 태그 확인( ex : <p> </p> , <img/> , <col/> )
            위 조건을 지키지 않을 경우 DocumentException 발생
            */
            String htmlStr = getHtml(itextPdfDto.getPdfCode());

            // HTML 내용을 PDF 파일에 삽입
            StringReader stringReader = new StringReader(htmlStr);
            // XML 파싱
            xmlParser.parse(stringReader);
            // PDF 문서 닫기
            document.close();
            // PDF Writer 닫기
            pdfWriter.close();

        } catch (DocumentException e1) {
            throw new IllegalArgumentException("PDF 라이브러리 설정 에러");
        } catch (FileNotFoundException e2) {
            e2.printStackTrace();
            throw new IllegalArgumentException("PDF 파일 생성중 에러");
        } catch (IOException e3) {
            e3.printStackTrace();
            throw new IllegalArgumentException("PDF 파일 생성중 에러2");
        } catch (Exception e4) {
            e4.printStackTrace();
            throw new IllegalArgumentException("PDF 파일 생성중 에러3");
        }
        finally {
            try {
                document.close();
            } catch (Exception e) {
                System.out.println("PDF 파일 닫기 에러");
                e.printStackTrace();
            }
        }

    }

 

 

추가적인 메소드로 PDF 파일이 매번 생성되지 않게 하기 위한 메소드 입니다.

/*
 * PDF 유무를 체크한 후
 * PDF 파일이 없을 경우 PDF 파일 생성 메소드 실행
 */
public File checkPDF (ItextPdfDto pdfDto) {
    File file = new File(pdfDto.getPdfFilePath(),pdfDto.getPdfFileName());
    int fileSize = (int) file.length();
    if (fileSize == 0) {
        createPDF(pdfDto);
        file = new File(pdfDto.getPdfFilePath(),pdfDto.getPdfFileName());
    }
    return file;
}

 

 

생성된 파일을 다운로드 하기위한 컨트롤러 입니다.

@Controller
@RequiredArgsConstructor
public class ItextPdfController {
    @Autowired
    private final ItextPdfUtil itextPdfUtil;

    @RequestMapping("/attachment/pdf")
    public void pdfDownload(HttpServletResponse response) {

        // 미리 준비한 DTO 선언
        ItextPdfDto itextPdfDto = new ItextPdfDto();
        // pdf 파일이 저장될 경로 ( Mac 기준 )
        itextPdfDto.setPdfFilePath("/Users/jeon/Documents/JavaProject/Blog/pdf/");

        // pdf 파일이 저장될 경로 ( Windows 기준 )
        // itextPdfDto.setPdfFilePath("C:\\Users\\hyeok\\Desktop\\pdf");

        // pdf 파일명 ( 테스트를 위해 랜덤으로 생성 )
        itextPdfDto.setPdfFileName(new Random().nextInt() + ".pdf");
        // itextPdfDto.setPdfFileName("test.pdf");

        // getHtml 에서 호출될 코드명
        itextPdfDto.setPdfCode("hyeok");

        // ======================= PDF 존재 유무 체크 =======================
        // 없다면 PDF 파일 만들기
        File file = itextPdfUtil.checkPDF(itextPdfDto);
        int fileSize = (int) file.length();
        // ===============================================================


        // ===============================================================
        // 파일 다운로드를 위한 header 설정
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment; filename="+itextPdfDto.getPdfFileName()+";");
        response.setContentLengthLong(fileSize);
        response.setStatus(HttpServletResponse.SC_OK);
        // ===============================================================

        // 파일 다운로드
        BufferedInputStream in = null;
        BufferedOutputStream out = null;

        // PDF 파일을 버퍼에 담은 후 다운로드
        try{
            in = new BufferedInputStream(new FileInputStream(file));
            out = new BufferedOutputStream(response.getOutputStream());
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            byte[] buffer = new byte[4096];
            int read = 0;
            while ((read = in.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                in.close();
                Objects.requireNonNull(out).flush();
                out.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

 

 

이상으로 Java itext를 사용한 PDF만들기 ( HTML을 PDF로 만들기 ) 입니다.

아래 링크에서 원본 소스 확인 가능합니다.

 

GitHub : https://github.com/wjsskagur/itextPdf

 

 

감사합니다.

 

+ Recent posts