diff --git a/server/api/Dockerfile b/server/api/Dockerfile index fd5298c..eb47127 100644 --- a/server/api/Dockerfile +++ b/server/api/Dockerfile @@ -10,8 +10,11 @@ RUN gradle --no-daemon bootJar FROM eclipse-temurin:21-jre WORKDIR /app +RUN apt-get update \ + && apt-get install -y --no-install-recommends fonts-nanum \ + && rm -rf /var/lib/apt/lists/* + COPY --from=builder /workspace/build/libs/*.jar /app/app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/app.jar"] - diff --git a/server/api/src/main/java/com/hanwha/nexacrodemo/report/ReportService.java b/server/api/src/main/java/com/hanwha/nexacrodemo/report/ReportService.java index 9bbb444..ca3f2f1 100644 --- a/server/api/src/main/java/com/hanwha/nexacrodemo/report/ReportService.java +++ b/server/api/src/main/java/com/hanwha/nexacrodemo/report/ReportService.java @@ -8,13 +8,14 @@ import com.hanwha.nexacrodemo.consolidation.ConsolidationPayload; import com.hanwha.nexacrodemo.minio.ObjectStorageService; import com.hanwha.nexacrodemo.minio.StoredObject; import com.lowagie.text.Document; +import com.lowagie.text.DocumentException; import com.lowagie.text.Element; import com.lowagie.text.Font; -import com.lowagie.text.FontFactory; import com.lowagie.text.PageSize; import com.lowagie.text.Paragraph; import com.lowagie.text.Phrase; import com.lowagie.text.Rectangle; +import com.lowagie.text.pdf.BaseFont; import com.lowagie.text.pdf.PdfPCell; import com.lowagie.text.pdf.PdfPTable; import com.lowagie.text.pdf.PdfWriter; @@ -22,10 +23,13 @@ import java.awt.Color; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Path; import java.text.DecimalFormat; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -43,6 +47,7 @@ import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service @@ -55,15 +60,19 @@ public class ReportService { private final ReportMapper reportMapper; private final ConsolidationMapper consolidationMapper; private final ObjectStorageService objectStorageService; + private final String pdfFontPath; + private volatile BaseFont pdfBaseFont; public ReportService( ReportMapper reportMapper, ConsolidationMapper consolidationMapper, - ObjectStorageService objectStorageService + ObjectStorageService objectStorageService, + @Value("${app.report.pdf-font-path:}") String pdfFontPath ) { this.reportMapper = reportMapper; this.consolidationMapper = consolidationMapper; this.objectStorageService = objectStorageService; + this.pdfFontPath = pdfFontPath; } public TxResponse loadRunOverview() { @@ -358,7 +367,7 @@ public class ReportService { } private void addPdfTitle(Document document, String text, int size, Color color) { - Font font = FontFactory.getFont(FontFactory.HELVETICA_BOLD, size, color); + Font font = pdfFont(size, Font.BOLD, color); Paragraph paragraph = new Paragraph(text, font); paragraph.setAlignment(Element.ALIGN_LEFT); paragraph.setSpacingAfter(4f); @@ -380,14 +389,14 @@ public class ReportService { } private void addDetailTable(Document document, String title, List> rows, int maxRows) { - Font sectionFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 12, new Color(24, 54, 93)); + Font sectionFont = pdfFont(12, Font.BOLD, new Color(24, 54, 93)); Paragraph section = new Paragraph(title, sectionFont); section.setSpacingBefore(8f); section.setSpacingAfter(6f); document.add(section); if (rows.isEmpty()) { - document.add(new Paragraph("데이터가 없습니다.", FontFactory.getFont(FontFactory.HELVETICA_OBLIQUE, 10, new Color(120, 120, 120)))); + document.add(new Paragraph("데이터가 없습니다.", pdfFont(10, Font.ITALIC, new Color(120, 120, 120)))); return; } @@ -412,13 +421,13 @@ public class ReportService { if (rows.size() > maxRows) { document.add(new Paragraph( "전체 " + rows.size() + "건 중 상위 " + maxRows + "건만 표시합니다.", - FontFactory.getFont(FontFactory.HELVETICA_OBLIQUE, 9, new Color(120, 120, 120)) + pdfFont(9, Font.ITALIC, new Color(120, 120, 120)) )); } } private PdfPCell pdfHeaderCell(String text) { - PdfPCell cell = new PdfPCell(new Phrase(text, FontFactory.getFont(FontFactory.HELVETICA_BOLD, 9, Color.WHITE))); + PdfPCell cell = new PdfPCell(new Phrase(text, pdfFont(9, Font.BOLD, Color.WHITE))); cell.setHorizontalAlignment(Element.ALIGN_CENTER); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setBackgroundColor(new Color(243, 127, 32)); @@ -428,7 +437,7 @@ public class ReportService { } private PdfPCell pdfBodyCell(String text, int alignment) { - PdfPCell cell = new PdfPCell(new Phrase(text, FontFactory.getFont(FontFactory.HELVETICA, 9, Color.DARK_GRAY))); + PdfPCell cell = new PdfPCell(new Phrase(text, pdfFont(9, Font.NORMAL, Color.DARK_GRAY))); cell.setHorizontalAlignment(alignment); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setBorderColor(new Color(226, 229, 233)); @@ -436,6 +445,53 @@ public class ReportService { return cell; } + private Font pdfFont(float size, int style, Color color) { + BaseFont baseFont = resolvePdfBaseFont(); + if (baseFont == null) { + return new Font(Font.HELVETICA, size, style, color); + } + return new Font(baseFont, size, style, color); + } + + private BaseFont resolvePdfBaseFont() { + if (pdfBaseFont != null) { + return pdfBaseFont; + } + synchronized (this) { + if (pdfBaseFont != null) { + return pdfBaseFont; + } + for (String candidate : pdfFontCandidates()) { + try { + pdfBaseFont = BaseFont.createFont(candidate, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); + return pdfBaseFont; + } catch (DocumentException | IOException ignored) { + // Fall through to the next candidate. The Docker image installs NanumGothic, + // but local test environments may not have a Korean font available. + } + } + return null; + } + } + + private List pdfFontCandidates() { + List candidates = new ArrayList<>(); + if (pdfFontPath != null && !pdfFontPath.isBlank()) { + candidates.add(pdfFontPath.trim()); + } + addIfExists(candidates, "/usr/share/fonts/truetype/nanum/NanumGothic.ttf"); + addIfExists(candidates, "/usr/share/fonts/truetype/nanum/NanumBarunGothic.ttf"); + addIfExists(candidates, "/workspace/server/api/src/main/resources/fonts/NanumGothic.ttf"); + addIfExists(candidates, "/app/resources/fonts/NanumGothic.ttf"); + return candidates; + } + + private void addIfExists(List candidates, String path) { + if (Files.exists(Path.of(path))) { + candidates.add(path); + } + } + private boolean isNumericValue(Object value) { return value instanceof Number || value instanceof BigDecimal; } diff --git a/server/api/src/main/resources/application.yml b/server/api/src/main/resources/application.yml index 9ecae6e..10525a5 100644 --- a/server/api/src/main/resources/application.yml +++ b/server/api/src/main/resources/application.yml @@ -43,6 +43,8 @@ app: enabled: false poll-interval-ms: ${BATCH_POLL_INTERVAL_MS:5000} worker-name: ${BATCH_WORKER_NAME:batch-1} + report: + pdf-font-path: ${PDF_FONT_PATH:} storage: provider: ${STORAGE_PROVIDER:minio} bucket: ${MINIO_BUCKET:reports} @@ -57,4 +59,3 @@ logging: level: root: INFO com.hanwha.nexacrodemo: INFO -