Skip to content

[SRLT-155] PDF AI 리포트 생성 및 완료 메일 PDF 첨부 발송#99

Open
2ghrms wants to merge 19 commits into
developfrom
SRLT-155-ai-리포트-email에-리포트-pdf-업로드

Hidden character warning

The head ref may contain hidden characters: "SRLT-155-ai-\ub9ac\ud3ec\ud2b8-email\uc5d0-\ub9ac\ud3ec\ud2b8-pdf-\uc5c5\ub85c\ub4dc"
Open

[SRLT-155] PDF AI 리포트 생성 및 완료 메일 PDF 첨부 발송#99
2ghrms wants to merge 19 commits into
developfrom
SRLT-155-ai-리포트-email에-리포트-pdf-업로드

Conversation

@2ghrms

@2ghrms 2ghrms commented May 20, 2026

Copy link
Copy Markdown
Member

🚀 Why - 해결하려는 문제가 무엇인가요?

  • PDF 사업계획서 AI 채점이 완료된 뒤, 사용자에게 웹 리포트 URL만 안내하고 있어 실제 리포트 내용을 바로 확인·공유하기 어려웠습니다.
  • 채점 완료 알림 메일에 AI 리포트 PDF를 첨부해, 사용자가 메일만으로 결과를 확인할 수 있도록 개선이 필요했습니다.
  • PDF 생성(OpenHTMLtoPDF 렌더링)은 CPU·I/O 비용이 크고 시간이 오래 걸리므로, DB 트랜잭션 안에서 실행되면 커넥션 점유·타임아웃 위험이 있었습니다.
  • 기존 이벤트 리스너에 orchestration 로직이 섞여 있어, expert 패턴과 맞지 않는 헥사고날 아키텍처 경계를 정리할 필요가 있었습니다.

✅ What - 무엇이 변경됐나요?

구현한 기능 요약

PDF 사업계획서 AI 채점 완료 시, 서버에서 HTML → PDF를 생성하고 완료 안내 메일에 PDF를 첨부해 발송합니다.

주요 변경사항

영역 변경 내용
PDF 렌더링 PdfRenderer + OpenHTMLtoPDF로 pdf-ai-report.html → PDF 변환
헥사고날 경계 AiReportPdfView / AiReportPdfViewMapper를 adapter 계층으로 이동, AiReportPdfRenderPort 포트 정의
이벤트 흐름 PdfReportRequestedInput → 채점 → AiReportReadyMailInput → 메일 발송
Service orchestration handlePdfReportRequested()에서 채점 → 메일 이벤트 발행 담당, 리스너는 얇게 유지
트랜잭션 분리 PDF 렌더링·메일 준비를 write TX 밖으로 분리, DB 조회만 readOnly TX
메일 발송 SmtpMailClient.sendPdfAiReportReadyMail() — HTML 본문 + PDF 첨부
회원 조회 MemberLookupPort 추가, MemberJpa가 구현
예외 처리 AiReportErrorType.EMAIL_SEND_ERROR 추가
폰트/배포 NotoSansKR-Regular.ttf classpath 번들, Dockerfile에 Alpine 한글 폰트 패키지 추가
의존성 openhtmltopdf-pdfbox, openhtmltopdf-svg-support 추가
테스트 PDF 파이프라인 통합 테스트·유닛 테스트 보강

🛠️ How - 어떻게 해결했나요?

전체 흐름

requestCreateAndGradePdfBusinessPlan
  └─ publishEvent(PdfReportRequestedInput)
       └─ AiReportPdfEvaluationEventListener  [async, AFTER_COMMIT]
            └─ AiReportService.handlePdfReportRequested()
                 ├─ completePdfGrading()          ← write TX (OCR → LLM → DB 저장)
                 └─ publishAiReportReadyMailEvent()
                      ├─ loadMailPrepareDataInReadOnlyTransaction()  ← readOnly TX
                      ├─ aiReportPdfRenderPort.render()              ← TX 밖
                      └─ publishEvent(AiReportReadyMailInput)
                           └─ AiReportReadyMailEventListener  [async, AFTER_COMMIT, @Retryable]
                                └─ SmtpMailClient.sendPdfAiReportReadyMail()

핵심 로직

1. Listener 얇게 — orchestration을 Service로 이동

AiReportPdfEvaluationEventListener는 Service 한 줄만 호출합니다.

aiReportService.handlePdfReportRequested(event.businessPlanId(), event.pdfUrl(), event.memberId());

채점 실패·메일 이벤트 발행 실패는 Service에서 각각 catch 후 로깅하고, 채점 실패 시 메일은 발송하지 않습니다.

2. PDF 생성 트랜잭션 분리

메서드 트랜잭션
handlePdfReportRequested NOT_SUPPORTED
completePdfGrading 별도 write TX (ObjectProvider self-invocation)
publishAiReportReadyMailEvent NOT_SUPPORTED
loadMailPrepareDataInReadOnlyTransaction TransactionTemplate(readOnly=true)
aiReportPdfRenderPort.render() TX 없음

3. PDF 렌더링 (PdfRenderer)

  • Thymeleaf로 pdf-ai-report.html 렌더 → OpenHTMLtoPDF 변환
  • BatikSVGDrawer로 레이더 차트 SVG 렌더
  • 한글 폰트: classpath NotoSansKR-Regular.ttf 우선, 없으면 OS 폰트 fallback
  • CFF OTF 폰트는 PDFBox 로드 실패 → 사전 검증 후 skip

4. PDF 레이아웃 (pdf-ai-report.html)

  • 1~2장 압축 레이아웃
  • 1페이지: 대시보드(총점 + 영역별 점수 + 레이더 차트)
  • 2페이지: 영역별 상세 2×2 그리드 + 하단 총평(강점/약점)

5. 메일 발송 (SmtpMailClient)

  • 제목: [STARLIGHT] "{파일명}"에 대한 AI 리포트가 도착했습니다
  • 본문: pdf-ai-report-ready Thymeleaf 템플릿
  • 첨부: 생성된 PDF (AiReportReadyMailInput.pdfBytes)
  • 실패 시 AiReportException(EMAIL_SEND_ERROR) throw

6. 헥사고날 경계

application/required/AiReportPdfRenderPort  ← 포트
adapter/pdf/PdfRenderer                     ← 어댑터 구현
adapter/pdf/view/AiReportPdfView            ← PDF 전용 view (adapter 내부)
adapter/pdf/mapper/AiReportPdfViewMapper    ← AiReportResult → View 변환

application 계층은 AiReportResult만 알고, PDF view/mapper는 adapter에 격리했습니다.

예외 사항 · 고민 포인트

  • Self-invocation: @Transactional이 동작하도록 ObjectProvider<AiReportService>로 프록시 호출
  • 폰트 호환성: CFF OTF는 OpenHTMLtoPDF/PDFBox에서 NPE 발생 → TrueType(.ttf)만 사용, isPdfBoxLoadableTrueType()으로 사전 검증
  • 메일 리스너 재시도: @Retryable(maxAttempts=3, backoff=2s×2) + @Recover로 최종 실패 로깅
  • Docker Alpine: JRE 이미지에 font-noto, font-noto-cjk 설치해 OS 폰트 fallback 보장
  • OCR/LLM은 여전히 write TX 안: 이번 PR 범위에서는 PDF 렌더·메일 준비만 TX 분리 (의도적 최소 변경)

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • AI 리포트 PDF 생성 및 렌더링 기능 추가
    • AI 리포트 완료 시 이메일 알림 발송 기능 구현
  • 개선사항

    • 이메일 전송 오류 처리 강화로 신뢰성 개선
    • SMTP 설정 동적 구성으로 유연성 향상
    • PDF 렌더링을 위한 다국어 글꼴 라이브러리 추가로 다양한 문자 지원

@2ghrms 2ghrms self-assigned this May 20, 2026
@2ghrms 2ghrms added 🧵 REFACTOR 코드 리팩토링 ✨ FEAT 새로운 기능 구현 labels May 20, 2026
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@2ghrms, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 22 minutes and 22 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bacf228a-228f-4a79-a1d4-f275008d4d18

📥 Commits

Reviewing files that changed from the base of the PR and between 992aa88 and d1c77bb.

📒 Files selected for processing (5)
  • src/main/java/starlight/adapter/shared/infrastructure/mail/SmtpMailClient.java
  • src/main/java/starlight/adapter/shared/infrastructure/pdf/PdfRenderer.java
  • src/main/java/starlight/application/aireport/AiReportService.java
  • src/test/java/starlight/application/aireport/AiReportServiceIntegrationTest.java
  • src/test/java/starlight/application/aireport/AiReportServiceUnitTest.java

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key: "tools"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

전체 요약

이 PR은 사업계획서 AI 리포트를 OpenHTMLtoPDF 기반으로 PDF로 렌더링하고, 트랜잭션 커밋 기반 이벤트 아키텍처를 통해 완료 메일을 재시도 로직(@Retryable)과 함께 비동기로 전송하는 엔드-투-엔드 기능을 구현합니다. 인프라 글꼴 설정, 폰트 로드 검증, Thymeleaf PDF 템플릿, 이벤트 리스너 체인, 메일 예외 처리 및 테스트 리팩토링이 통합적으로 개발되었습니다.


변경사항

AI Report PDF 생성 및 메일 전송 파이프라인

Layer / File(s) 설명
인프라 및 런타임 환경 설정
deploy/Dockerfile, gradle/spring.gradle, src/main/java/starlight/bootstrap/MailConfig.java
Docker 런타임에 PDF 렌더링용 한글/CJK 글꼴(noto, ttf-dejavu)을 설치하고, OpenHTMLtoPDF 라이브러리(openhtmltopdf-pdfbox, openhtmltopdf-svg-support) 의존성을 추가합니다. MailConfig에서 StringUtils로 SMTP 인증 플래그를 동적 계산하고, 포트별로 SSL(465) 또는 STARTTLS(587) 설정을 분기합니다.
PDF 렌더링 계약 및 데이터 모델
src/main/java/starlight/application/aireport/required/AiReportPdfRenderPort.java, src/main/java/starlight/application/aireport/required/MemberLookupPort.java, src/main/java/starlight/adapter/shared/infrastructure/pdf/view/AiReportPdfView.java
AiReportPdfRenderPort 인터페이스(render 메서드)와 MemberLookupPort 조회 포트를 정의합니다. AiReportPdfView 레코드는 총점, 섹션(제목/점수/체크리스트), 강점/약점, 레이더 차트 SVG를 포함하는 중첩 레코드 구조를 제공합니다.
PDF 렌더링 구현 및 폰트 처리
src/main/java/starlight/adapter/shared/infrastructure/pdf/PdfRenderer.java, src/main/java/starlight/adapter/shared/infrastructure/pdf/mapper/AiReportPdfViewMapper.java
PdfRendererAiReportResult를 뷰로 매핑 후 Thymeleaf 템플릿을 HTML로 변환하고 OpenHTMLtoPDF로 PDF 바이트 배열을 생성합니다. 클래스패스/OS 글꼴을 순차 시도하며, ConcurrentHashMap 캐시와 synchronized 구간으로 동시성을 제어하고, 임시 파일 삭제를 안전하게 관리합니다. AiReportPdfViewMapper는 섹션 점수/체크리스트 매핑, JSON 파싱, 레이더 차트 SVG 생성을 담당합니다.
PDF 렌더링 HTML 템플릿
src/main/resources/templates/pdf-ai-report.html
Thymeleaf 템플릿으로 총점 대시보드, 섹션별 점수 카드(짝/홀 배치), 레이더 차트 SVG, 최대 4개 섹션의 영역별 상세 평가(체크리스트 조건부 렌더링), 강점/약점 목록을 PDF로 구성합니다.
이벤트 입력 타입 및 AiReportService 확장
src/main/java/starlight/application/aireport/event/PdfReportRequestedInput.java, src/main/java/starlight/application/aireport/event/AiReportReadyMailInput.java, src/main/java/starlight/application/aireport/AiReportService.java
PdfReportRequestedInput(이벤트 타입 이름 변경), AiReportReadyMailInput(패키지 이동: event), 두 퍼블릭 메서드 추가: handlePdfReportRequested(PDF 채점 완료 처리, self-invocation completePdfGrading), publishAiReportReadyMailEvent(NOT_SUPPORTED 전파로 트랜잭션 밖 PDF 렌더링, 읽기 전용 템플릿 로드, 메일 이벤트 발행).
메일 이벤트 리스너 및 재시도 로직
src/main/java/starlight/application/aireport/event/AiReportReadyMailEventListener.java, src/main/java/starlight/adapter/shared/infrastructure/mail/SmtpMailClient.java, src/main/java/starlight/domain/aireport/exception/AiReportErrorType.java
AiReportReadyMailEventListener 추가: AFTER_COMMIT 후 AiReportReadyMailInput 비동기 처리, @Retryable(3회, 2000ms, 지수 2), @Recover 최종 실패 로깅. SmtpMailClient@RequiredArgsConstructor 적용, 명시 생성자 제거, sendPdfAiReportReadyMail에서 예외를 AiReportException(EMAIL_SEND_ERROR)로 재전파. AiReportErrorTypeEMAIL_SEND_ERROR(500) 상수 추가.
어댑터 확장 및 리스너 단순화
src/main/java/starlight/adapter/member/persistence/MemberJpa.java, src/main/java/starlight/application/aireport/event/AiReportPdfEvaluationEventListener.java
MemberJpaaireport.required.MemberLookupPort 추가 구현. AiReportPdfEvaluationEventListener는 기존 30줄 이상의 조회/다운로드/메일 로직을 제거하고 aiReportService.handlePdfReportRequested(...) 단일 위임 호출로 단순화.
통합 및 단위 테스트 리팩토링
src/test/java/starlight/application/aireport/AiReportServiceIntegrationTest.java, src/test/java/starlight/application/aireport/AiReportServiceUnitTest.java
AiReportServiceIntegrationTest: 이벤트 리스너→AiReportReadyMailEventListener, @RecordApplicationEvents 추가, 동기 Executor(emailTaskExecutor, aiReportPdfExecutor), TransactionTemplate(REQUIRES_NEW) 커밋 경계 명시, dispatchRecordedAiReportReadyMailEvents 메서드로 이벤트 수동 디스패치. AiReportServiceUnitTest: 공통 createSut() 팩토리 메서드, 추가 의존성(AiReportPdfRenderPort, MemberLookupPort, PlatformTransactionManager) 모킹.

🎯 3 (Moderate) | ⏱️ ~25 분


연관 PR

  • StartUpLight/STARLIGHT_BE#94: 동일 AI 리포트 비동기 메일/PDF 렌더링 파이프라인 변경(AiReportService 이벤트 메서드, PdfReportRequestedInput 이벤트, 이벤트 입력/포트 타입, SmtpMailClient 예외 처리)이 겹침.
  • StartUpLight/STARLIGHT_BE#50: MemberJpa의 포트 인터페이스 구현 조정(MemberLookupPort 추가/변경) 관련.
  • StartUpLight/STARLIGHT_BE#27: @Async, @Retryable, 비동기 메일 전송 재시도 인프라 관련.

제안 레이블

✨ FEAT, ☁️ DEPLOY

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.20% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 'PDF AI 리포트 생성 및 완료 메일 PDF 첨부 발송'으로, 전체 변경사항의 핵심을 정확하고 간결하게 요약하고 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch SRLT-155-ai-리포트-email에-리포트-pdf-업로드

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@2ghrms 2ghrms changed the title [SRLT-155] ai 리포트 email에 리포트 pdf 업로드 [SRLT-155] PDF AI 리포트 생성 및 완료 메일 PDF 첨부 발송 May 20, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gradle/spring.gradle (1)

15-25: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

spring-boot-starter-mail 의존성 중복 제거

Line 15와 Line 24에서 동일한 메일 라이브러리가 중복 선언되고 있습니다. 한 쪽만 남겨 주세요. Gradle 의존성 관리의 복잡도를 줄이고 향후 버전 업데이트 시 혼선을 방지하기 위해 필요한 정리입니다. // Emailclient 주석 아래 Line 24의 선언 의도를 고려하여 Line 15의 중복을 제거하거나, 필요시 첫 번째 블록에 통합해 주시면 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gradle/spring.gradle` around lines 15 - 25, Remove the duplicate dependency
declaration for 'org.springframework.boot:spring-boot-starter-mail' so it
appears only once; keep the single declaration under the "// Emailclient" block
(or conversely remove the second one if you prefer the first block) and delete
the other occurrence to avoid duplicate Gradle entries.
🧹 Nitpick comments (2)
src/main/java/starlight/application/aireport/event/AiReportReadyMailEventListener.java (1)

28-37: 💤 Low value

재시도마다 중복 로깅 발생

try-catch 블록에서 예외 발생 시 로그를 남기고 다시 throw하는 구조로, 재시도될 때마다 동일한 에러 로그가 출력됩니다. 최종 실패 시에만 로깅하려면 @Recover 메서드에서만 로깅하거나, 여기서는 DEBUG 레벨로 변경하는 것을 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/starlight/application/aireport/event/AiReportReadyMailEventListener.java`
around lines 28 - 37, The handleAiReportReadyMailEvent method in
AiReportReadyMailEventListener currently logs an error and rethrows, causing
duplicate logs on each retry; change the catch block so it does not log an ERROR
on each retry—either downgrade the log call to log.debug(...) (including the
same context args) and rethrow, or remove the log here and add the error logging
into your `@Recover` method so only the final failure is logged; ensure you update
the catch in handleAiReportReadyMailEvent and the `@Recover` method consistently
to avoid losing context (use event.toEmail() and event.filename() in the final
log).
src/main/resources/templates/pdf-ai-report.html (1)

302-321: ⚡ Quick win

섹션 점수 카드 렌더링 로직을 인덱스 기반 접근으로 리팩토링하세요.

현재 th:each + th:if="${stat.index % 2 == 0/1}" 방식은 다음 문제가 있습니다:

  • 레이아웃 안정성: 섹션 개수가 홀수이거나 5개 이상일 경우, 한 행에 3개 이상의 <td>가 생성되어 각각 width: 50%를 적용받으면 총 너비가 100%를 초과하여 레이아웃이 깨질 수 있습니다.
  • 가독성 저하: 짝수/홀수 필터링 로직을 이해해야 하므로 코드 파악이 어렵습니다.
  • 일관성 부족: 동일 파일의 "영역별 상세 평가" 부분(lines 330-423)에서는 직접 인덱스 접근(sections[0], sections[1]...)을 사용하여 스타일이 혼재됩니다.
  • 유지보수성: 섹션 개수 변경 시 잠재적 버그 위험이 있습니다.

lines 330-423의 방식처럼 sections.size() 조건과 직접 인덱스 접근을 사용하면 각 행에 정확히 2개의 셀만 생성되어 안정적이고 명확합니다.

♻️ 제안하는 리팩토링
               <table class="cat-table">
                 <tr>
-                  <td class="cat-card" th:each="section, stat : ${sections}" th:if="${stat.index % 2 == 0}">
-                    <div class="cat-card__name" th:text="${section.title}">문제 정의</div>
+                  <td class="cat-card" th:if="${sections.size() > 0}" th:with="section=${sections[0]}">
+                    <div class="cat-card__name" th:text="${section.title}">섹션 1</div>
+                    <div class="cat-card__score">
+                      <span th:text="${section.score}">0</span>점
+                      <span class="cat-card__score-sub" th:text="|/ ${section.total}점|">/ 0점</span>
+                    </div>
+                  </td>
+                  <td class="cat-card" th:if="${sections.size() > 1}" th:with="section=${sections[1]}">
+                    <div class="cat-card__name" th:text="${section.title}">섹션 2</div>
                     <div class="cat-card__score">
                       <span th:text="${section.score}">0</span>점
                       <span class="cat-card__score-sub" th:text="|/ ${section.total}점|">/ 0점</span>
                     </div>
                   </td>
                 </tr>
                 <tr>
-                  <td class="cat-card" th:each="section, stat : ${sections}" th:if="${stat.index % 2 == 1}">
-                    <div class="cat-card__name" th:text="${section.title}">실현 가능성</div>
+                  <td class="cat-card" th:if="${sections.size() > 2}" th:with="section=${sections[2]}">
+                    <div class="cat-card__name" th:text="${section.title}">섹션 3</div>
+                    <div class="cat-card__score">
+                      <span th:text="${section.score}">0</span>점
+                      <span class="cat-card__score-sub" th:text="|/ ${section.total}점|">/ 0점</span>
+                    </div>
+                  </td>
+                  <td class="cat-card" th:if="${sections.size() > 3}" th:with="section=${sections[3]}">
+                    <div class="cat-card__name" th:text="${section.title}">섹션 4</div>
                     <div class="cat-card__score">
                       <span th:text="${section.score}">0</span>점
                       <span class="cat-card__score-sub" th:text="|/ ${section.total}점|">/ 0점</span>
                     </div>
                   </td>
                 </tr>
               </table>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/resources/templates/pdf-ai-report.html` around lines 302 - 321, The
table currently uses th:each with th:if="${stat.index % 2 == 0/1}" which can
produce uneven <td> counts and break layout; replace this with explicit
index-based rendering like the "영역별 상세 평가" pattern: check sections.size() and
render each row with exactly two cells using direct index access (e.g.,
sections[0], sections[1], sections[2], sections[3] etc.), guarding each access
with a sections.size() check so you only render a cell when that index exists;
update the two table rows that currently use th:each (the cat-table block
containing the th:each/ th:if pairs) to use these explicit sections[index]
conditions to guarantee precisely two cells per row and stable layout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/starlight/adapter/shared/infrastructure/mail/SmtpMailClient.java`:
- Around line 115-121: The catch blocks in SmtpMailClient currently throw
AiReportException without passing the original exception, losing the cause;
update both catch handlers (the MessagingException and generic Exception
catches) to include the original exception as the cause when constructing the
AiReportException (e.g., throw new
AiReportException(AiReportErrorType.EMAIL_SEND_ERROR, e) or use the
AiReportException constructor that accepts a Throwable), so the original stack
trace is preserved for debugging.

In `@src/main/java/starlight/adapter/shared/infrastructure/pdf/PdfRenderer.java`:
- Around line 96-103: The temp font files are being created on every render and
only rely on deleteOnExit(), causing disk leak; change PdfRenderer to cache and
reuse a single prepared temp File per classpath font resource instead of calling
copyFontToTemp on every render (make a static ConcurrentHashMap<String,File> or
similar keyed by classpathLocation and populate it once), still validate with
isPdfBoxLoadableTrueType before inserting into the cache, and call
builder.useFont with the cached File; apply the same caching fix to the other
occurrence around the builder.useFont call at the 144-153 region so fonts are
prepared once and not recreated per request.

In `@src/main/java/starlight/bootstrap/MailConfig.java`:
- Line 21: sender.setPort(mailProperties.getPort()) can NPE when
mailProperties.getPort() is null; change it to use the same defaulting logic as
buildJavaMailProps (use port 587 when null) before calling
JavaMailSenderImpl#setPort so an int is always passed. Locate the sender.setPort
call and mailProperties.getPort usage and replace with a null-safe conversion
(e.g., determine int port = (mailProperties.getPort() != null ?
mailProperties.getPort() : 587)) or equivalent, keeping buildJavaMailProps
behavior consistent.

---

Outside diff comments:
In `@gradle/spring.gradle`:
- Around line 15-25: Remove the duplicate dependency declaration for
'org.springframework.boot:spring-boot-starter-mail' so it appears only once;
keep the single declaration under the "// Emailclient" block (or conversely
remove the second one if you prefer the first block) and delete the other
occurrence to avoid duplicate Gradle entries.

---

Nitpick comments:
In
`@src/main/java/starlight/application/aireport/event/AiReportReadyMailEventListener.java`:
- Around line 28-37: The handleAiReportReadyMailEvent method in
AiReportReadyMailEventListener currently logs an error and rethrows, causing
duplicate logs on each retry; change the catch block so it does not log an ERROR
on each retry—either downgrade the log call to log.debug(...) (including the
same context args) and rethrow, or remove the log here and add the error logging
into your `@Recover` method so only the final failure is logged; ensure you update
the catch in handleAiReportReadyMailEvent and the `@Recover` method consistently
to avoid losing context (use event.toEmail() and event.filename() in the final
log).

In `@src/main/resources/templates/pdf-ai-report.html`:
- Around line 302-321: The table currently uses th:each with th:if="${stat.index
% 2 == 0/1}" which can produce uneven <td> counts and break layout; replace this
with explicit index-based rendering like the "영역별 상세 평가" pattern: check
sections.size() and render each row with exactly two cells using direct index
access (e.g., sections[0], sections[1], sections[2], sections[3] etc.), guarding
each access with a sections.size() check so you only render a cell when that
index exists; update the two table rows that currently use th:each (the
cat-table block containing the th:each/ th:if pairs) to use these explicit
sections[index] conditions to guarantee precisely two cells per row and stable
layout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 247c6807-5246-4521-b8bb-dc352283f4a5

📥 Commits

Reviewing files that changed from the base of the PR and between 2652b17 and 2da4fdf.

⛔ Files ignored due to path filters (1)
  • src/main/resources/fonts/NotoSansKR-Regular.ttf is excluded by !**/*.ttf
📒 Files selected for processing (20)
  • deploy/Dockerfile
  • gradle/spring.gradle
  • src/main/java/starlight/adapter/member/persistence/MemberJpa.java
  • src/main/java/starlight/adapter/shared/infrastructure/mail/SmtpMailClient.java
  • src/main/java/starlight/adapter/shared/infrastructure/pdf/PdfRenderer.java
  • src/main/java/starlight/adapter/shared/infrastructure/pdf/mapper/AiReportPdfViewMapper.java
  • src/main/java/starlight/adapter/shared/infrastructure/pdf/view/AiReportPdfView.java
  • src/main/java/starlight/application/aireport/AiReportService.java
  • src/main/java/starlight/application/aireport/event/AiReportPdfEvaluationEventListener.java
  • src/main/java/starlight/application/aireport/event/AiReportReadyMailEventListener.java
  • src/main/java/starlight/application/aireport/event/AiReportReadyMailInput.java
  • src/main/java/starlight/application/aireport/event/PdfReportRequestedInput.java
  • src/main/java/starlight/application/aireport/required/AiReportMailPort.java
  • src/main/java/starlight/application/aireport/required/AiReportPdfRenderPort.java
  • src/main/java/starlight/application/aireport/required/MemberLookupPort.java
  • src/main/java/starlight/bootstrap/MailConfig.java
  • src/main/java/starlight/domain/aireport/exception/AiReportErrorType.java
  • src/main/resources/templates/pdf-ai-report.html
  • src/test/java/starlight/application/aireport/AiReportServiceIntegrationTest.java
  • src/test/java/starlight/application/aireport/AiReportServiceUnitTest.java

Comment thread src/main/java/starlight/adapter/shared/infrastructure/pdf/PdfRenderer.java Outdated
Comment thread src/main/java/starlight/bootstrap/MailConfig.java Outdated
2ghrms added 4 commits June 18, 2026 19:56
- AiReportException 에러 트레이스 반영
- 폰트파일 재사용 로직 추가
- JavaMailSenderImpl의 setPort 메소드에 null 값 방지
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

Test Results

268 tests  ±0   268 ✅ ±0   11s ⏱️ +4s
 52 suites ±0     0 💤 ±0 
 52 files   ±0     0 ❌ ±0 

Results for commit d1c77bb. ± Comparison against base commit f8fc97f.

♻️ This comment has been updated with latest results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ FEAT 새로운 기능 구현 🧵 REFACTOR 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant