Skip to content

fix: 배너 이미지 한글 깨짐 수정 및 재생성 API 추가#409

Merged
KoSeonJe merged 1 commit intodevelopfrom
fix/banner-korean-font-encoding
Apr 1, 2026
Merged

fix: 배너 이미지 한글 깨짐 수정 및 재생성 API 추가#409
KoSeonJe merged 1 commit intodevelopfrom
fix/banner-korean-font-encoding

Conversation

@KoSeonJe
Copy link
Copy Markdown
Collaborator

@KoSeonJe KoSeonJe commented Apr 1, 2026

🚀 작업 내용

  • Docker 환경에서 배너 이미지의 한글이 깨지는 문제를 수정했습니다
  • BannerImageGenerator에서 Pretendard 폰트를 Java GraphicsEnvironment에 명시적으로 등록하여 fontconfig 시스템 라이브러리 없이도 한글이 정상 렌더링되도록 변경했습니다
  • 폰트 로드 실패 시 깨진 이미지를 조용히 생성하는 대신 즉시 예외를 던지도록 fail-fast 처리했습니다
  • 기존에 S3에 올라간 깨진 배너 이미지를 복구하기 위한 임시 Admin 재생성 API를 추가했습니다

🤔 고민했던 내용

  • Dockerfile에 fontconfig 설치 vs 코드 레벨 해결: GraphicsEnvironment.registerFont()를 사용하면 인프라 변경 없이 해결 가능하여 코드 레벨 방식을 선택했습니다
  • 폰트 로드 실패 시 fallback vs fail-fast: 한글 미지원 SansSerif로 깨진 이미지를 만드는 것보다 명시적 실패가 낫다고 판단했습니다
  • 재생성 API 위치: Controller에서 Repository를 직접 의존하지 않도록 FacadeRankingBannerService에 재생성 로직을 배치했습니다

💬 리뷰 중점사항

  • 재생성 API는 배너 복구 후 다음 PR에서 제거 예정입니다
  • @PostConstruct에서 폰트 로드 실패 시 애플리케이션 기동이 실패하는데, 폰트 파일이 resources에 포함되어 있으므로 fail-fast가 적절하다고 판단했습니다

🤖 Generated with Claude Code

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 관리자가 순위 배너를 수동으로 재생성할 수 있는 새로운 기능이 추가되었습니다.
  • 버그 수정

    • 배너 생성 시 폰트 리소스 누락으로 인한 오류 처리가 개선되었습니다. 기본 폰트 대신 명확한 오류 메시지를 반환하도록 변경되었습니다.

GraphicsEnvironment에 폰트를 명시적으로 등록하여 Docker 환경에서
fontconfig 없이도 Pretendard 폰트가 정상 동작하도록 수정.
폰트 로드 실패 시 깨진 이미지 생성 대신 즉시 예외를 던지도록 변경.
기존 깨진 배너 복구를 위한 임시 Admin 재생성 API 추가.

Constraint: Docker 베이스 이미지(eclipse-temurin:17-jdk)에 fontconfig 미포함
Rejected: Dockerfile에 fontconfig 설치 | 인프라 변경 없이 코드 레벨에서 해결 가능
Rejected: SansSerif fallback 유지 | Linux 서버에서 한글 글리프 미지원
Confidence: high
Scope-risk: narrow
Directive: 재생성 API는 임시이므로 배너 복구 후 다음 PR에서 제거할 것

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

요약

Walkthrough

새로운 관리자 엔드포인트 POST /ranking/regenerate를 추가하여 최근 월별 순위 배너를 재생성하는 기능을 구현. 이전 달의 1위 랭킹을 조회한 후 기존 배너를 삭제하고 새 배너를 생성하는 워크플로우 추가.

Changes

Cohort / File(s) Summary
API 계층
src/main/java/.../banner/api/AdminBannerApi.java
POST /ranking/regenerate 엔드포인트 추가. Swagger 문서화 및 204 No Content 응답 상태 설정, AccessToken 보안 요구사항 적용.
컨트롤러 계층
src/main/java/.../banner/controller/AdminBannerController.java
FacadeRankingBannerService 의존성 주입 추가. regenerateRankingBanners() 메서드 구현하여 서비스에 위임.
서비스 계층
src/main/java/.../banner/service/FacadeRankingBannerService.java, src/main/java/.../banner/service/FacadeRankingBannerServiceImpl.java
인터페이스에 regenerateLatestRankingBanners() 메서드 추가. 구현부에서 FeedMonthlyRankingRepository 의존성 추가, 이전 달의 1위 랭킹을 조회하여 배너 재생성 로직 구현.
유틸리티
src/main/java/.../banner/service/BannerImageGenerator.java
loadFont() 메서드 개선: 폰트 로드 실패 시 예외 발생, 성공 시 GraphicsEnvironment에 폰트 등록, 기본 폰트 폴백 제거.

Sequence Diagram(s)

sequenceDiagram
    actor Admin as 관리자
    participant API as AdminBannerApi
    participant Controller as AdminBannerController
    participant Service as FacadeRankingBannerServiceImpl
    participant Repo as FeedMonthlyRankingRepository
    participant DB as Database

    Admin->>API: POST /ranking/regenerate
    API->>Controller: regenerateRankingBanners()
    Controller->>Service: regenerateLatestRankingBanners()
    Service->>Service: 이전 달 계산 (LocalDate.now().minusMonths(1))
    Service->>Repo: findAllByTargetYearAndTargetMonthAndRanking(year, month, 1)
    Repo->>DB: 1위 랭킹 조회
    DB-->>Repo: 랭킹 데이터 반환
    Repo-->>Service: 결과 목록
    alt 결과 존재
        Service->>Service: createRankingBanners(firstPlaceRankings)
        Service->>DB: 기존 배너 삭제 및 새 배너 생성
        DB-->>Service: 완료
    else 결과 없음
        Service->>Service: 로그 기록 후 종료
    end
    Service-->>Controller: void
    Controller-->>API: HTTP 204 No Content
    API-->>Admin: 응답
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

D-0

Suggested reviewers

  • wonjunYou
  • 5uhwann
  • Seooooo24
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항인 한글 깨짐 수정과 재생성 API 추가를 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 저장소의 템플릿 구조를 완벽하게 따르고 있으며, 작업 내용, 고민했던 내용, 리뷰 중점사항이 모두 명확하게 작성되어 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/banner-korean-font-encoding

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/main/java/ddingdong/ddingdongBE/domain/banner/service/FacadeRankingBannerServiceImpl.java (1)

58-59: 1등 랭크 값은 상수로 분리하는 편이 가독성이 좋습니다.

Line [59]의 1은 의미 있는 상수(FIRST_PLACE_RANK)로 분리하면 의도를 더 빠르게 파악할 수 있습니다.

제안 diff
 public class FacadeRankingBannerServiceImpl implements FacadeRankingBannerService {

     private static final String RANKING_BANNER_DIRECTORY = "ranking-banner";
     private static final String IMAGE_CONTENT_TYPE = "image/png";
+    private static final int FIRST_PLACE_RANK = 1;
@@
         List<FeedMonthlyRanking> firstPlaceRankings =
                 feedMonthlyRankingRepository.findAllByTargetYearAndTargetMonthAndRanking(
-                        lastMonth.getYear(), lastMonth.getMonthValue(), 1);
+                        lastMonth.getYear(), lastMonth.getMonthValue(), FIRST_PLACE_RANK);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/ddingdong/ddingdongBE/domain/banner/service/FacadeRankingBannerServiceImpl.java`
around lines 58 - 59, The literal '1' passed to
feedMonthlyRankingRepository.findAllByTargetYearAndTargetMonthAndRanking(...)
should be replaced with a named constant to improve readability; add a private
static final int FIRST_PLACE_RANK = 1 (or an equivalent constant) in
FacadeRankingBannerServiceImpl and use FIRST_PLACE_RANK in the repository call
to make the intent explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/ddingdong/ddingdongBE/domain/banner/api/AdminBannerApi.java`:
- Around line 55-57: The POST endpoint in AdminBannerApi currently declares a
204 status via `@ApiResponse`(responseCode = "204") and
`@ResponseStatus`(HttpStatus.NO_CONTENT); change both to return 201 by updating
`@ApiResponse`(responseCode = "201", description = "...") and
`@ResponseStatus`(HttpStatus.CREATED) so the POST follows the guideline (HTTP POST
-> 201 Created); locate these annotations above the method that performs the
ranking banner recreation in AdminBannerApi.java and adjust the values
accordingly.

In
`@src/main/java/ddingdong/ddingdongBE/domain/banner/service/BannerImageGenerator.java`:
- Around line 200-204: The catch blocks in BannerImageGenerator swallowing the
exception lose the stacktrace and the first catch for
BannerImageGenerationException is redundant; remove the separate catch
(BannerImageGenerationException e) block, and in the remaining catch (Exception
e) include the Throwable when logging (use log.error with the exception
parameter) and rethrow a new or the original BannerImageGenerationException as
appropriate so the root cause is preserved; update the log.error call that
references path to pass e as the last argument (or include e) and
construct/throw BannerImageGenerationException with the original exception as
its cause.

---

Nitpick comments:
In
`@src/main/java/ddingdong/ddingdongBE/domain/banner/service/FacadeRankingBannerServiceImpl.java`:
- Around line 58-59: The literal '1' passed to
feedMonthlyRankingRepository.findAllByTargetYearAndTargetMonthAndRanking(...)
should be replaced with a named constant to improve readability; add a private
static final int FIRST_PLACE_RANK = 1 (or an equivalent constant) in
FacadeRankingBannerServiceImpl and use FIRST_PLACE_RANK in the repository call
to make the intent explicit.
🪄 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: 6d3d537f-2436-4cc8-92c7-7a6047255e13

📥 Commits

Reviewing files that changed from the base of the PR and between 7d02486 and b69324b.

📒 Files selected for processing (5)
  • src/main/java/ddingdong/ddingdongBE/domain/banner/api/AdminBannerApi.java
  • src/main/java/ddingdong/ddingdongBE/domain/banner/controller/AdminBannerController.java
  • src/main/java/ddingdong/ddingdongBE/domain/banner/service/BannerImageGenerator.java
  • src/main/java/ddingdong/ddingdongBE/domain/banner/service/FacadeRankingBannerService.java
  • src/main/java/ddingdong/ddingdongBE/domain/banner/service/FacadeRankingBannerServiceImpl.java

Comment on lines +55 to +57
@ApiResponse(responseCode = "204", description = "랭킹 배너 재생성 성공")
@ResponseStatus(HttpStatus.NO_CONTENT)
@SecurityRequirement(name = "AccessToken")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

POST 재생성 엔드포인트의 상태 코드는 201로 맞춰주세요.

Line [56]이 204 NO_CONTENT로 선언되어 있어 저장소 규칙과 충돌합니다.

제안 diff
-    `@ApiResponse`(responseCode = "204", description = "랭킹 배너 재생성 성공")
-    `@ResponseStatus`(HttpStatus.NO_CONTENT)
+    `@ApiResponse`(responseCode = "201", description = "랭킹 배너 재생성 성공")
+    `@ResponseStatus`(HttpStatus.CREATED)

As per coding guidelines, HTTP status codes: POST returns 201 Created, GET returns 200 OK, PUT/PATCH/DELETE returns 204 No Content 규칙을 따라야 합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@ApiResponse(responseCode = "204", description = "랭킹 배너 재생성 성공")
@ResponseStatus(HttpStatus.NO_CONTENT)
@SecurityRequirement(name = "AccessToken")
`@ApiResponse`(responseCode = "201", description = "랭킹 배너 재생성 성공")
`@ResponseStatus`(HttpStatus.CREATED)
`@SecurityRequirement`(name = "AccessToken")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/ddingdong/ddingdongBE/domain/banner/api/AdminBannerApi.java`
around lines 55 - 57, The POST endpoint in AdminBannerApi currently declares a
204 status via `@ApiResponse`(responseCode = "204") and
`@ResponseStatus`(HttpStatus.NO_CONTENT); change both to return 201 by updating
`@ApiResponse`(responseCode = "201", description = "...") and
`@ResponseStatus`(HttpStatus.CREATED) so the POST follows the guideline (HTTP POST
-> 201 Created); locate these annotations above the method that performs the
ranking banner recreation in AdminBannerApi.java and adjust the values
accordingly.

Comment on lines +200 to +204
} catch (BannerImageGenerationException e) {
throw e;
} catch (Exception e) {
log.warn("커스텀 폰트 로드 실패 ({}), 기본 폰트 사용: {}", path, e.getMessage());
log.error("커스텀 폰트 로드 실패 ({}): {}", path, e.getMessage());
throw new BannerImageGenerationException();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

예외 로깅에서 스택트레이스가 유실됩니다.

현재는 메시지만 기록되어 폰트 로드 실패 원인 추적이 어렵습니다. 동시에 재던지기용 catch (BannerImageGenerationException e)는 제거해도 동작이 같습니다.

수정 예시
-        } catch (BannerImageGenerationException e) {
-            throw e;
-        } catch (Exception e) {
-            log.error("커스텀 폰트 로드 실패 ({}): {}", path, e.getMessage());
+        } catch (Exception e) {
+            if (e instanceof BannerImageGenerationException) {
+                throw (BannerImageGenerationException) e;
+            }
+            log.error("커스텀 폰트 로드 실패 ({})", path, e);
             throw new BannerImageGenerationException();
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/ddingdong/ddingdongBE/domain/banner/service/BannerImageGenerator.java`
around lines 200 - 204, The catch blocks in BannerImageGenerator swallowing the
exception lose the stacktrace and the first catch for
BannerImageGenerationException is redundant; remove the separate catch
(BannerImageGenerationException e) block, and in the remaining catch (Exception
e) include the Throwable when logging (use log.error with the exception
parameter) and rethrow a new or the original BannerImageGenerationException as
appropriate so the root cause is preserved; update the log.error call that
references path to pass e as the last argument (or include e) and
construct/throw BannerImageGenerationException with the original exception as
its cause.

@KoSeonJe KoSeonJe merged commit 087226a into develop Apr 1, 2026
2 checks passed
@KoSeonJe KoSeonJe deleted the fix/banner-korean-font-encoding branch April 1, 2026 12:00
KoSeonJe added a commit that referenced this pull request Apr 1, 2026
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@KoSeonJe KoSeonJe mentioned this pull request Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant