pdf 렌더링별 sample 페이지

/common/pdf/*
dev
박성영 4 months ago
parent 3e96932399
commit 301fd7a617

1
.gitignore vendored

@ -30,3 +30,4 @@ replay_pid*
/.gradle/
/.idea/
/.fastRequest/
/.claude/settings.local.json

@ -139,6 +139,12 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// ===== PDF (HTML -> PDF) =====
// OpenHTMLtoPDF (PdfRendererBuilder ) - PDFBox
implementation 'com.openhtmltopdf:openhtmltopdf-core:1.0.10'
implementation 'com.openhtmltopdf:openhtmltopdf-pdfbox:1.0.10'
implementation 'com.openhtmltopdf:openhtmltopdf-slf4j:1.0.10'
// ===== =====
// Lombok - (Getter, Setter, Builder )
compileOnly 'org.projectlombok:lombok'

@ -0,0 +1,254 @@
package go.kr.project.common.controller;
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
/**
* JSP -> HTML -> PDF
* - OpenHTMLtoPDF(PdfRendererBuilder) + PDFBox
* - JSP include HTML PDF
* -
*/
@Controller
@RequestMapping("/common/pdf")
@Tag(name = "Common PDF", description = "JSP를 PDF로 변환하는 샘플 API")
public class CommonPdfController {
/**
* PDF ( inline )
* URL: /common/pdf/view
*/
@Operation(summary = "JSP를 PDF로 변환하여 브라우저에 표시", description = "JSP를 HTML로 렌더링한 후 즉시 PDF로 변환하여 inline으로 응답합니다.")
@GetMapping(value = "/view")
public void viewPdf(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// [보안] 세션 체크 예시 (프로젝트 정책에 맞춰 실제 인증 객체/세션 키로 대체 가능)
// Object user = request.getSession(false) != null ? request.getSession(false).getAttribute("loginUser") : null;
// if (user == null) {
// response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "로그인이 필요합니다.");
// return;
// }
// 1) JSP를 HTML 문자열로 렌더링
// - Tiles가 아닌 독립 JSP를 대상으로 함(샘플)
String jspPath = "/WEB-INF/views/common/pdf/sample.jsp"; // 내부 경로
String html = renderJspToHtml(request, response, jspPath);
// 2) 응답 헤더 설정: inline PDF
response.setContentType(MediaType.APPLICATION_PDF_VALUE);
response.setHeader("Content-Disposition", "inline; filename=sample.pdf");
// 3) HTML -> PDF 변환 수행
// - openhtmltopdf PdfRendererBuilder 사용
// - setBaseUri를 null로 두면 상대 리소스 경로 해석이 어려울 수 있으므로, 필요 시 컨텍스트 기반 URL 설정 가능
try (OutputStream os = response.getOutputStream()) {
PdfRendererBuilder builder = new PdfRendererBuilder();
builder.useFastMode();
// 한글 폰트가 누락될 경우 폰트 설정이 필요할 수 있음(서버 폰트 파일을 자동 탐색/등록)
registerKoreanFonts(builder);
builder.withHtmlContent(html, null);
builder.toStream(os);
builder.run();
} catch (Exception e) {
// 변환 실패 시 500 에러 응답
response.reset();
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "PDF 변환 중 오류가 발생했습니다.");
}
}
/**
* OpenHTMLtoPDF CJK
* - (Windows: Malgun Gothic, Linux: NanumGothic )
* -
* - :
*/
private void registerKoreanFonts(PdfRendererBuilder builder) {
// [중요 로직] 한글 폰트 경로 후보 (운영체제별)
String[] candidates = new String[] {
// Windows
"C:\\Windows\\Fonts\\malgun.ttf",
"C:\\Windows\\Fonts\\Malgun.ttf",
"C:\\Windows\\Fonts\\malgunbd.ttf",
// Linux (Nanum)
"/usr/share/fonts/truetype/nanum/NanumGothic.ttf",
"/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf",
// Linux (Noto CJK - 경로는 배포판에 따라 상이할 수 있음)
"/usr/share/fonts/opentype/noto/NotoSansCJKkr-Regular.otf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.otf",
// macOS
"/Library/Fonts/AppleSDGothicNeo.ttc"
};
for (String path : candidates) {
try {
File f = new File(path);
if (f.exists() && f.isFile()) {
// [중요 로직] CSS에서 사용하는 패밀리명으로 별칭 등록하여 매칭률 향상
String lower = path.toLowerCase();
if (lower.contains("malgun")) {
builder.useFont(f, "Malgun Gothic"); // CSS: 'Malgun Gothic'
builder.useFont(f, "malgun"); // 보조 별칭
} else if (lower.contains("nanumgothic")) {
builder.useFont(f, "NanumGothic"); // CSS: 'NanumGothic'
builder.useFont(f, "Nanum Gothic"); // 보조 별칭
} else if (lower.contains("notosanscjk")) {
builder.useFont(f, "Noto Sans CJK KR"); // CSS: 'Noto Sans CJK KR'
builder.useFont(f, "NotoSansCJKkr"); // 보조 별칭
} else if (lower.contains("applesdgothicneo")) {
builder.useFont(f, "Apple SD Gothic Neo"); // CSS: 'Apple SD Gothic Neo'
} else {
// 알 수 없는 경우 파일명 기반으로도 등록
builder.useFont(f, f.getName());
}
}
} catch (Exception ignore) {
// 폰트 등록 실패는 무시하고 다음 후보 시도
}
}
}
/**
* PDF.js
* URL: /common/pdf/viewer
*/
@Operation(summary = "PDF.js 뷰어 페이지", description = "PDF.js를 이용해 생성된 PDF를 페이지 내 캔버스에 렌더링합니다.")
@GetMapping(value = "/viewer")
public String viewerPage(HttpServletRequest request) {
// [보안] 세션 체크 예시
// Object user = request.getSession(false) != null ? request.getSession(false).getAttribute("loginUser") : null;
// if (user == null) { return "redirect:/login"; }
// Tiles 정의 없이도 JSP를 바로 호출할 수 있도록 forward 사용
return "forward:/WEB-INF/views/common/pdf/pdf-viewer.jsp";
}
/**
* <object> PDF
* URL: /common/pdf/object-view
* - /
*/
@Operation(summary = "<object> 기반 PDF 표시", description = "브라우저의 기본 PDF 뷰어를 사용해 <object>로 PDF를 표시하고, 상단에 다운로드/이동 버튼을 제공합니다.")
@GetMapping(value = "/object-view")
public String objectViewPage(HttpServletRequest request) {
// [보안] 세션 체크 예시
// Object user = request.getSession(false) != null ? request.getSession(false).getAttribute("loginUser") : null;
// if (user == null) { return "redirect:/login"; }
return "forward:/WEB-INF/views/common/pdf/pdf-object-view.jsp";
}
/**
* PDF
* URL: /common/pdf/responsive-view
* - : PDF.js ( )
* - : <object>
*/
@Operation(summary = "반응형 PDF 보기", description = "모바일에서는 PDF.js, 데스크톱에서는 <object> 기반으로 자동 전환합니다.")
@GetMapping(value = "/responsive-view")
public String responsiveView(HttpServletRequest request) {
// [보안] 세션 체크 예시
// Object user = request.getSession(false) != null ? request.getSession(false).getAttribute("loginUser") : null;
// if (user == null) { return "redirect:/login"; }
if (isMobile(request)) {
return "forward:/WEB-INF/views/common/pdf/pdf-viewer.jsp";
}
return "forward:/WEB-INF/views/common/pdf/pdf-object-view.jsp";
}
// [중요 로직] 단순 User-Agent 기반 모바일 환경 감지
private boolean isMobile(HttpServletRequest request) {
String ua = request.getHeader("User-Agent");
if (ua == null) return false;
String u = ua.toLowerCase();
return u.contains("mobile") || u.contains("android") || u.contains("iphone") || u.contains("ipad") || u.contains("ipod") || u.contains("windows phone");
}
/**
* PDF (/ )
* URL: /common/pdf/generate
* : download=true , inline
*/
@Operation(summary = "실시간 PDF 생성", description = "JSP(혹은 문자열) 기반 HTML을 PDF로 변환하여 스트리밍합니다. download 파라미터로 다운로드/미리보기 제어")
@GetMapping(value = "/generate")
public void generatePdf(HttpServletRequest request, HttpServletResponse response) throws IOException {
// [보안] 세션 체크 예시
// Object user = request.getSession(false) != null ? request.getSession(false).getAttribute("loginUser") : null;
// if (user == null) {
// response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "로그인이 필요합니다.");
// return;
// }
boolean download = "true".equalsIgnoreCase(request.getParameter("download"));
// 1) PDF로 변환할 HTML 콘텐츠 준비 (샘플)
// - 실제로는 JSP 렌더링 결과를 사용해도 됨. 여기서는 간단한 문자열 예시.
String html = "<html><head><meta charset='UTF-8'/><style>body{font-family:'Malgun Gothic','NanumGothic','Apple SD Gothic Neo',sans-serif;}</style></head><body>" +
"<h1>고지서</h1>" +
"<p>" + java.time.LocalDate.now() + " 기준 고지서입니다.</p>" +
"<table border='1' cellspacing='0' cellpadding='5'><tr><th>항목</th><th>금액</th></tr>" +
"<tr><td>서비스 이용료</td><td>10,000원</td></tr>" +
"<tr><td>부가세</td><td>1,000원</td></tr>" +
"<tr><td><b>합계</b></td><td><b>11,000원</b></td></tr></table>" +
"</body></html>";
// 2) HTML -> PDF 변환 (메모리 스트림)
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
PdfRendererBuilder builder = new PdfRendererBuilder();
builder.useFastMode();
// [중요] 한글 깨짐(# 표시) 방지: 폰트 등록 필수
registerKoreanFonts(builder);
builder.withHtmlContent(html, null);
builder.toStream(baos);
builder.run();
byte[] pdfData = baos.toByteArray();
// 3) 응답 헤더 구성: 다운로드 여부에 따라 분기
response.setContentType(MediaType.APPLICATION_PDF_VALUE);
response.setHeader("Content-Disposition", (download ? "attachment" : "inline") + "; filename=notice.pdf");
response.setContentLength(pdfData.length);
// 4) 스트리밍 전송
try (OutputStream out = response.getOutputStream()) {
out.write(pdfData);
out.flush();
}
} catch (Exception e) {
response.reset();
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "PDF 생성 중 오류가 발생했습니다.");
}
}
/**
* JSP include HTML
* - Response getWriter() StringWriter
*/
private String renderJspToHtml(HttpServletRequest request, HttpServletResponse originalResponse, String jspPath)
throws ServletException, IOException {
// JSP 출력물을 담을 버퍼
final StringWriter stringWriter = new StringWriter();
// HttpServletResponseWrapper 구현: JSP의 Writer 출력을 가로채서 stringWriter에 기록
HttpServletResponseWrapper capturingResponse = new HttpServletResponseWrapper(originalResponse) {
private final PrintWriter writer = new PrintWriter(stringWriter);
@Override
public PrintWriter getWriter() {
return writer;
}
};
// RequestDispatcher.include를 사용하여 JSP를 현재 요청/응답 컨텍스트로 렌더링
RequestDispatcher dispatcher = request.getRequestDispatcher(jspPath);
dispatcher.include(request, capturingResponse);
return stringWriter.toString();
}
}

@ -0,0 +1,51 @@
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>PDF 미리보기 (<object> 기반)</title>
<style>
/* [화면 스타일] - 프로젝트 전반 스타일 영향 최소화 위해 페이지 로컬 스타일만 사용 */
body { font-family: 'Malgun Gothic','NanumGothic','Apple SD Gothic Neo',sans-serif; margin: 16px; }
.toolbar { margin-bottom: 10px; }
.toolbar button { padding: 6px 10px; margin-right: 6px; }
.pdf-host { border: 1px solid #ddd; background: #fefefe; }
.pdf-object { width: 100%; height: calc(100vh - 120px); border: 0; }
.fallback { padding: 12px; color: #333; }
</style>
</head>
<body>
<h2>고지서 미리보기</h2>
<!-- [툴바] - 다운로드/이동 기능 제공 -->
<div class="toolbar">
<!-- 다운로드: 서버 엔드포인트에 download=true를 전달하여 파일로 저장 -->
<a class="btn" href="<c:url value='/common/pdf/generate?download=true'/>"><button type="button">다운로드</button></a>
<!-- 새 창으로 열기: 브라우저 기본 PDF 뷰어로 새 탭 표시 -->
<button type="button" onclick="openNewTab()">새 창으로 열기</button>
<!-- 뒤로가기: 사용자의 이전 페이지로 이동 -->
<button type="button" onclick="history.back()">이전 페이지</button>
</div>
<!-- [PDF 표시] - <object>를 사용하여 브라우저 기본 PDF 뷰어로 표시 -->
<div class="pdf-host">
<object id="pdfObject" class="pdf-object" data="<c:url value='/common/pdf/generate'/>" type="application/pdf">
<div class="fallback">
PDF를 표시할 수 없습니다.
<a href="<c:url value='/common/pdf/generate?download=true'/>">여기를 클릭</a>하여 파일을 다운로드하세요.
</div>
</object>
</div>
<script>
// [이동 버튼] 새 탭으로 PDF 열기
function openNewTab() {
const url = '<c:url value="/common/pdf/generate"/>';
window.open(url, '_blank');
}
// 주의: <object>로 임베딩된 PDF 내부 페이지를 JS로 직접 제어하기는 브라우저/뷰어 제약으로 불가합니다.
// 따라서 페이지 밖(상단 툴바)에 다운로드/이동 기능을 제공합니다.
</script>
</body>
</html>

@ -0,0 +1,79 @@
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<meta>
<meta charset="UTF-8"/>
<title>PDF.js 뷰어 - 고지서 미리보기</title>
<!-- 중요: 배포 시 로컬 pdfjs를 사용할 경우, 아래 CDN 스크립트를 /pdfjs/pdf.js 로 교체 가능 -->
<!-- <script src="<c:url value='/pdfjs/pdf.js'/>\"></script> -->
<script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js"></script>
<style>
/* 화면 스타일: 캔버스 배경, 레이아웃 등 */
body { font-family: Arial, sans-serif; margin: 20px; }
h2 { margin-bottom: 10px; }
#viewerWrapper { border: 1px solid #ddd; padding: 10px; display: inline-block; background: #fefefe; }
#pdf-canvas { background: #fff; border: 1px solid #ccc; }
.toolbar { margin: 10px 0; }
.toolbar button { padding: 6px 10px; }
</style>
</head>
<body>
<h2>고지서 미리보기</h2>
<div class="toolbar">
<button type="button" onclick="downloadPDF()">다운로드</button>
</div>
<div id="viewerWrapper">
<canvas id="pdf-canvas"></canvas>
</div>
<script>
// PDF.js 워커 설정 (CDN 사용)
// 로컬 사용 시: pdfjsLib.GlobalWorkerOptions.workerSrc = '<c:url value="/pdfjs/pdf.worker.js"/>';
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js';
// 실시간 PDF 생성 엔드포인트 (Spring Controller)
const pdfUrl = '<c:url value="/common/pdf/generate"/>';
// PDF 첫 페이지를 캔버스에 렌더링 (화면 너비에 맞춰 자동 스케일)
pdfjsLib.getDocument(pdfUrl).promise.then(function(pdf) {
return pdf.getPage(1).then(function(page) {
var canvas = document.getElementById('pdf-canvas');
var context = canvas.getContext('2d');
var wrapper = document.getElementById('viewerWrapper');
function render() {
var unscaled = page.getViewport({ scale: 1 });
// 가용 폭 계산(모바일 고려): wrapper 너비 또는 화면 폭 기준
var available = Math.max(320, (wrapper.clientWidth || (window.innerWidth - 40)) - 20);
var scale = available / unscaled.width;
// 과대/과소 확대 제한
if (scale > 2.0) scale = 2.0;
if (scale < 0.7) scale = 0.7;
var viewport = page.getViewport({ scale: scale });
canvas.height = viewport.height;
canvas.width = viewport.width;
var renderContext = { canvasContext: context, viewport: viewport };
page.render(renderContext);
}
render();
// 창 크기 변경 시 재렌더링(간단 디바운스)
var tid;
window.addEventListener('resize', function(){
clearTimeout(tid);
tid = setTimeout(render, 150);
});
});
}).catch(function(err){
console.error('PDF 로딩 오류:', err);
alert('PDF 로딩 중 오류가 발생했습니다.');
});
// 다운로드 버튼: 동일 엔드포인트에 download=true 파라미터 전달
function downloadPDF() {
window.location.href = pdfUrl + '?download=true';
}
</script>
</body>
</html>

@ -0,0 +1,25 @@
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>PDF Sample</title>
<style>
/* 간단한 스타일 - PDF 변환 시 반영됨 */
body { font-family: 'Malgun Gothic','NanumGothic','Apple SD Gothic Neo',sans-serif; }
h1 { color: navy; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid black; padding: 5px; }
</style>
</head>
<body>
<h1>PDF 변환 테스트</h1>
<p>이 페이지는 JSP에서 바로 PDF로 변환됩니다.</p>
<table>
<tr><th>번호</th><th>내용</th></tr>
<tr><td>1</td><td>샘플 데이터</td></tr>
<tr><td>2</td><td>테스트 데이터</td></tr>
</table>
</body>
</html>
Loading…
Cancel
Save