[Spring 4.1] Velocity 2.x환경에서 iText 5.x를 통한 PDF View 만들기

올해 3월부터 작업중인 그룹웨어의 PDF View부분때문에 상당히 애먹었었는데, 어케저케 해결을 하긴 했다.

사실 PDF View가 웹에서 HTML이 있는데 무슨 필요가 있나 싶었는데, 사무화를 웹으로 옮기기 위해서 어떠한 문서의 표준 규격을 마련하기에는 A4만한 것이 없다고 생각한다. 목록 같은 부분이야 엑셀로 옮길 수 있고 데이터 보기정도야 당연히 HTML-Table로 보게 하면 되겠지만, 그룹웨어에서 보통 들어가는 뭐 계약서나 견적서.. 같은 공적인 문서에는 확실히 표준이 필요한 것 같다.

단순히 HTML로 하다 보니깐 일단 N-Screen상에서 팝업의 사이즈 문제도 있었고, 일단 반응형 웹으로 추후 모바일 이식에도 손쉽게 하기 위해 팝업 없이 Modal로만 처리하다 보니, 정말 그놈의 사이즈 문제가 크더라. 그래서 대부분의 View는 PDF로 처리하기로 결심했다.

문제는 현재 환경이 Velocity 2.x View를 사용하고 있는데, 뭐 최근의 Spring은 dispatcher설정에서 contentNegotiationManager 를 통한 확장자 관리를 지원한다 하는데, velocity는 이게 먹히지 않는다. 게다가 뭐 pdf 확장자 혹은 application/pdf타입만 특정 클래스로 빼기 보다는, 기존의 MVC구조에서 Controller의 RequestMapping에서 자연스래 맵핑된 URI가 PDF파일로 나오길 바랬다.

여차저차 해서 아래와 같은 Y2K님의 문서를 발견.

http://netframework.tistory.com/entry/24-View의-표현법-Application

좀 웃기지만 하도 나는 상위버전을 좋아하기 때문에 iText 5.x를 사용하기로 결정하고(뭐가 좋은지도 사실 모른다..) 위 방법대로 따라해봤다. 잘되더라. 하지만 위 Y2K님의 글에는 Controller설정 방법이 없어 다시금 서칭.

http://www.codejava.net/frameworks/spring/spring-web-mvc-with-pdf-view-example-using-itext-5x

그렇게 위와 같은 글을 발견했다. 얼씨구나~

<bean id="viewResolver1" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <property name="order" value="1"/>
    <property name="basename" value="views"/>
</bean>

<bean id="viewResolver2"
      class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
    <property name="exposeRequestAttributes" value="true" />
    <property name="exposeSessionAttributes" value="true" />
    <property name="exposeSpringMacroHelpers" value="true" />
    <property name="requestContextAttribute" value="rc" />
    <property name="cache" value="false" />
    <property name="suffix" value=".vm" />
    <property name="order" value="2" />
    <property name="contentType" value="text/html; charset=UTF-8" />
    <property name="viewClass"
              value="org.springframework.web.servlet.view.velocity.VelocityView" />
    <property name="prefix" value="" />
    <property name="toolboxConfigLocation" value="WEB-INF/classes/config/velocity/velocity-toolbox.xml"/>
</bean>

그렇게 위와 같이 Velocity와 함께 설정을 했다. 보니깐 views.properties는 최종 컴파일된 폴더의 /WEB-INF/classes/views.properties로 직접 넣어줘야했다.(IDEA가 자동으로 옮기지 못하더라..)

EstimatePdfView.(class)=com.izect.fiamm.estimate.view.EstimateViewPdf

위와 같이 views.properties를 처리했다.

<<AbstractITextPdfView.java>>

package com.izect.fiamm.common.util.pdf;

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.view.AbstractView;

import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.pdf.PdfWriter;

/**
 * Created by changmatthew on 15. 6. 30..
 */
public abstract class AbstractITextPdfView extends AbstractView {

    public AbstractITextPdfView() {
        setContentType("application/pdf");
    }

    @Override
    protected boolean generatesDownloadContent() {
        return false;
    }

    @Override
    protected void renderMergedOutputModel(Map<String, Object> model,
                                           HttpServletRequest request, HttpServletResponse response) throws Exception {
        // IE workaround: write into byte array first.
        ByteArrayOutputStream baos = createTemporaryOutputStream();

        // Apply preferences and build metadata.
        Document document = newDocument();
        PdfWriter writer = newWriter(document, baos);
        prepareWriter(model, writer, request);
        buildPdfMetadata(model, document, request);

        // Build PDF document.
        document.open();
        buildPdfDocument(model, document, writer, request, response);
        document.close();

        // Flush to HTTP response.
        writeToResponse(response, baos);
    }

    protected Document newDocument() {
        return new Document(PageSize.A4);
    }

    protected PdfWriter newWriter(Document document, OutputStream os) throws DocumentException {
        return PdfWriter.getInstance(document, os);
    }

    protected void prepareWriter(Map<String, Object> model, PdfWriter writer, HttpServletRequest request)
            throws DocumentException {

        writer.setViewerPreferences(getViewerPreferences());
    }

    protected int getViewerPreferences() {
        return PdfWriter.ALLOW_PRINTING | PdfWriter.PageLayoutSinglePage;
    }

    protected void buildPdfMetadata(Map<String, Object> model, Document document, HttpServletRequest request) {
    }

    protected abstract void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer,
                                             HttpServletRequest request, HttpServletResponse response) throws Exception;
}

 

이건 뭐 강의에 나온 것과 같고..

<<EstimateViewPdf.java>>

public class EstimateViewPdf extends AbstractITextPdfView {
    @Override
    protected void buildPdfDocument(Map<String, Object> model,
                                    Document document, PdfWriter writer, HttpServletRequest request,
                                    HttpServletResponse response) throws Exception {

        Estimate em = (Estimate)model.get("em");

        String fileName = createFileName(EstimateUtil.getEstNo(em));

        BaseFont cfont = BaseFont.createFont("HYGoThic-Medium", "UniKS-UCS2-H", BaseFont.NOT_EMBEDDED);
        Font objFont = new Font(cfont, 12);

        Chapter chapter = new Chapter(new Paragraph("this is english"), 1);
        chapter.add(new Paragraph("이건 메세지입니다.", objFont));
        document.add(chapter);


        PdfPTable table = new PdfPTable(3);
        table.getDefaultCell().setHorizontalAlignment(Element.ALIGN_CENTER);
        table.getDefaultCell().setVerticalAlignment(Element.ALIGN_MIDDLE);
        table.getDefaultCell().setBackgroundColor(BaseColor.WHITE);

        table.addCell(em.getTitle());
        table.addCell("Flavor");
        table.addCell("Toppings");

        document.add(table);

    }

    private String createFileName(String estNo) {
        SimpleDateFormat fileFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
        return new StringBuilder("견적서").append("_").append(estNo).append("_").append(fileFormat.format(new Date())).append(".pdf").toString();
    }
}

 

대충 강의 따라다가 현재 만들고 있는 그룹웨어의 견적서 View의 Pdf처리를 만들어 봤다.

여기서 중요한건, Spring MVC구조에서 Service Interface가 이 View에서는 단순히 먹히지 않는다. 고로, Controller에서 EstimateViewPdf객체 생성 후, model을 함께 요청시에 넘겨줘야 한다. 말 그대로, EstimateviewPdf는 View만 처리할 뿐, 여기서 사용될 데이터는 모두 컨트롤러에서 함께 모델로 넘겨줘야 한다는 것..

나의 경우는, Getter/Setter로 구성된 Estimate 라는 모델이 있는데,

Estimate em = (Estimate)model.get("em");

위에서처럼 EstimateViewPdf내에서 model에 넣어준 객체로 받아와서, 데이터만 고대로 처리해야 한다는 것이다.

<<EstimateController.java 중 일부>>

// View
@RequestMapping(value = "/viewPdf/{seq}", method = {RequestMethod.GET, RequestMethod.POST})
public ModelAndView estimatePdfView(@PathVariable("seq") int seq,
                                 HttpServletRequest request,
                                 HttpSession sess) throws Exception {

    ModelAndView mv;
    EstimateViewPdf estimateViewPdf = new EstimateViewPdf();
    Entity param = new Entity(request);

    Entity param2 = new Entity();
    param2.put("seq",seq);
    Estimate em = estimateService.selectOne(param2);

    mv = new ModelAndView(estimateViewPdf);

    mv.addObject("em",em);
 
    return mv;
}

이 Controller에서 보면, em이라는 객체에 estimateService라는 @Autowire된 서비스 객체에서 ServiceInterface->ServiceImpl->Mapper->DAO이런식으로(본인은 MyBatis를 사용함..) 가져오게 되는데, 저 구조를 단순히 EstimateViewPdf 내에서는 사용할 수 없다는 것이다.

고로 mv.addObject("em",em); 이와 같이 모델에 넣어주고 ModelAndView를 리턴해야 한다.

좌우간 iText의 기능은 아직 안써서 모르겠지만, View에서 충돌나는 것은 뭔가 구조를 명확히 잡아서 위에서 알게 된 View Negotiation으로 처리해야 한다는 것은 알겠는데, 아직 그 구조를 잡을 자신이 없다.. ㅠㅠ 어쨌든 몇주간 고민하던 것 해결하게 되서 기분은 좋다.. 어여 빨리 개발해야지 🙂