Skip to content

[release] BE v1.4.0#1385

Merged
seongwon030 merged 76 commits intomainfrom
develop/be
Apr 5, 2026
Merged

[release] BE v1.4.0#1385
seongwon030 merged 76 commits intomainfrom
develop/be

Conversation

@seongwon030
Copy link
Copy Markdown
Member

@seongwon030 seongwon030 commented Apr 5, 2026

🚀 릴리즈 PR

📦 버전 정보

항목 내용
서비스 💾 BE / 💻 FE
Bump 타입 🚨 MAJOR / ➕ MINOR / 🔧 PATCH
예상 버전 vX.Y.Z

⚠️ 반드시 라벨을 지정해주세요: 서비스 라벨(💾 BE, 💻 FE)과 버전 라벨(🚨 MAJOR, ➕ MINOR, 🔧 PATCH)이 없으면 태그가 생성되지 않습니다.

📖 버전 라벨 선택 가이드 (Semantic Versioning)
라벨 버전 변화 선택 기준 예시
🚨 MAJOR v1.0.0v2.0.0 기존 API/기능이 호환되지 않는 변경 API 엔드포인트 삭제/변경, 요청/응답 스펙 변경, DB 스키마 대규모 변경
➕ MINOR v1.0.0v1.1.0 기존 기능은 유지하면서 새 기능 추가 새 API 엔드포인트 추가, 새 기능 도입, 기존 API에 선택적 필드 추가
🔧 PATCH v1.0.0v1.0.1 기능 변경 없이 버그 수정/내부 개선 버그 수정, 성능 개선, 리팩토링, 문서 수정

📋 포함된 변경사항

이번 릴리즈에 포함된 주요 변경사항을 요약합니다.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 클럽 캘린더에 Google Calendar 통합 추가 (Notion Calendar와 함께 사용 가능)
    • 여러 소스의 캘린더 이벤트를 하나로 통합하여 조회 가능
    • 홍보 게시글 관리 시스템 추가
    • 게시글 이미지 업로드 기능 구현
  • 개선사항

    • 클럽 프로필에 통합 캘린더 이벤트 조회 기능 추가
    • 이미지 저장소 서비스 개선

lepitaaar and others added 30 commits March 19, 2026 11:48
…ript-MOA-756

[feat] 프로퍼티 온보딩 스크립트 추가
…elop-page-MOA-757

[feature] 홍보게시판 관리 개발자 페이지 기능 추가
Notion 연결 키를 clubId 기준으로 전환하고, 클럽 상세 및 공개 calendar-events API에서 저장된 Notion 데이터베이스 일정을 조회하도록 확장합니다.

Made-with: Cursor
saveNotionConnection과 saveDatabaseId에서 findById → save 패턴을
MongoTemplate의 upsert/$set 및 updateFirst/$set으로 교체하여
동시 요청 시 필드 덮어쓰기 문제를 제거

Made-with: Cursor
AES-GCM에서 고정 IV를 반복 사용 시 기밀성/무결성이 손상되는 문제 수정.
encrypt는 SecureRandom으로 랜덤 IV를 생성해 v2: 포맷으로 저장하고,
decrypt는 v2: 접두사 유무로 레거시 경로 폴백을 지원해 기존 데이터 호환 유지.
지원서 개인정보(ClubApply) 및 Notion 토큰 암호화 경로 모두 해당.

Made-with: Cursor
Notion API 호출 성공 이후에 saveDatabaseId를 수행하도록 순서 변경.
잘못된 databaseId가 저장되어 이후 캘린더 조회가 오염되는 문제 방지.

Made-with: Cursor
요청 횟수 50회 초과로 루프 강제 종료 시 has_more=false로 덮어쓰던 문제 수정.
실제 has_more/next_cursor를 유지하고 partial=true 플래그를 추가해
클라이언트가 데이터가 절사되었음을 인지할 수 있도록 개선.
getDatabases, getDatabasePages 두 메서드 모두 적용.

Made-with: Cursor
…vent-api-MOA-779

[feature] Google 캘린더 이벤트 조회 API 추가
@seongwon030 seongwon030 added 💾 BE Backend 📈 release 릴리즈 배포 ➕ MINOR Minor 릴리즈 labels Apr 5, 2026
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
moadong Ready Ready Preview, Comment Apr 5, 2026 1:26pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 5, 2026

Warning

Rate limit exceeded

@seongwon030 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 54 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 22 minutes and 54 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4b60687e-1f00-4ed4-a79f-3130ceef41c9

📥 Commits

Reviewing files that changed from the base of the PR and between 40f05ca and 1f74f5d.

📒 Files selected for processing (11)
  • backend/docs/banner-image-upload-extension-fix-2026-04-05.md
  • backend/src/main/java/moadong/calendar/google/entity/GoogleConnection.java
  • backend/src/main/java/moadong/calendar/google/payload/request/GoogleCalendarSelectRequest.java
  • backend/src/main/java/moadong/calendar/google/service/GoogleOAuthService.java
  • backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java
  • backend/src/main/java/moadong/club/service/ClubProfileService.java
  • backend/src/main/java/moadong/global/exception/ErrorCode.java
  • backend/src/main/java/moadong/media/service/BannerImageUploadService.java
  • backend/src/main/java/moadong/media/service/PromotionImageUploadService.java
  • backend/src/main/java/moadong/media/service/R2ImageUploadService.java
  • backend/src/test/java/moadong/media/service/R2ImageUploadServiceTest.java

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ 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

Google Calendar 및 Notion OAuth 통합을 추가하여 클럽 캘린더 이벤트를 단일 끝점에서 제공합니다. 프로모션 게시글 생성 기능, 미디어 업로드 서비스 리팩토링(R2 위임), 그리고 개발자 포털 UI 확장이 포함됩니다. Infisical 기반 설정 관리와 AES 암호화 개선(동적 IV)도 추가됩니다.

Changes

Cohort / File(s) Summary
Infisical 및 빌드 설정
backend/.gitignore, backend/.infisical.json, backend/build.gradle, backend/gradle/infisical.gradle
Infisical 환경 기반 설정 관리 통합: 개발/스테이징/프로덕션 태스크 추가, Firebase 설정 및 application.yml 동적 생성. Gradle 태스크 등록 방식 현대화, Firebase Admin 의존성 업그레이드(9.7.0 → 9.8.0).
문서 및 개발 가이드
backend/CLAUDE.md, backend/docs/frontend-google-calendar-integration.md, backend/docs/notion-integration-decisions-2026-03-22.md, backend/docs/superpowers/specs/2026-03-30-google-calendar-integration-design.md, docs/draft/promotion-admin-create-plan-2026-03-29.md
개발자 저장소 가이드, Google/Notion 캘린더 통합 문서, 의사결정 기록, 프로모션 관리 구현 계획 추가.
Google Calendar 통합
backend/src/main/java/moadong/calendar/google/config/GoogleCalendarProperties.java, backend/src/main/java/moadong/calendar/google/controller/GoogleOAuthController.java, backend/src/main/java/moadong/calendar/google/entity/GoogleConnection.java, backend/src/main/java/moadong/calendar/google/payload/.../*.java, backend/src/main/java/moadong/calendar/google/repository/GoogleConnectionRepository.java, backend/src/main/java/moadong/calendar/google/service/GoogleOAuthService.java
Google OAuth 인증 및 캘린더 API 통합: 설정 속성, REST 컨트롤러(인증 URL 생성, 토큰 교환, 캘린더 목록/선택/조회), MongoDB 엔티티, 토큰 관리 및 갱신 로직, 암호화된 토큰 저장.
Notion OAuth 통합
backend/src/main/java/moadong/calendar/notion/config/NotionProperties.java, backend/src/main/java/moadong/calendar/notion/controller/NotionOAuthController.java, backend/src/main/java/moadong/calendar/notion/entity/NotionConnection.java, backend/src/main/java/moadong/calendar/notion/payload/.../*.java, backend/src/main/java/moadong/calendar/notion/repository/NotionConnectionRepository.java, backend/src/main/java/moadong/calendar/notion/service/NotionOAuthService.java
Notion OAuth 인증 및 데이터 조회: 설정 속성, REST 컨트롤러, MongoDB 엔티티, 페이지/데이터베이스 조회 로직, 암호화된 토큰 저장.
캘린더 집계 서비스
backend/src/main/java/moadong/calendar/service/CalendarAggregationService.java
Notion 및 Google 캘린더 이벤트 단일 끝점 제공: 소스별 독립적 실패 격리, 정렬 및 병합.
클럽 프로필 통합
backend/src/main/java/moadong/club/controller/ClubProfileController.java, backend/src/main/java/moadong/club/payload/dto/ClubCalendarEventResult.java, backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java, backend/src/main/java/moadong/club/payload/response/ClubCalendarEventsResponse.java, backend/src/main/java/moadong/club/service/ClubProfileService.java
클럽 프로필 응답에 캘린더 이벤트 필드 추가: source 필드(NOTION/GOOGLE 구분), 집계된 이벤트 제공, 캘린더 연결 여부 플래그.
프로모션 관리 기능
backend/src/main/java/moadong/club/controller/PromotionArticleController.java, backend/src/main/java/moadong/club/payload/dto/PromotionArticleCreateResultDto.java, backend/src/main/java/moadong/club/payload/request/PromotionArticleCreateRequest.java, backend/src/main/java/moadong/club/service/PromotionArticleService.java
프로모션 생성 시 articleId 반환: 요청 검증 완화(images: @NotNull), 서비스 반환값 추가.
미디어 업로드 서비스 리팩토링
backend/src/main/java/moadong/media/controller/PromotionImageController.java, backend/src/main/java/moadong/media/dto/PromotionImageUploadResponse.java, backend/src/main/java/moadong/media/service/BannerImageUploadService.java, backend/src/main/java/moadong/media/service/PromotionImageUploadService.java, backend/src/main/java/moadong/media/service/R2ImageUploadService.java, backend/src/main/java/moadong/media/util/ClubImageUtil.java
S3 직접 업로드에서 R2ImageUploadService 위임으로 전환: 새로운 PromotionImageController/Service 추가, 파일 검증 및 키 생성 로직 중앙화. 로케일 안전성 개선(toLowerCase → toLowerCase(Locale.ROOT)).
암호화 및 보안
backend/src/main/java/moadong/global/exception/ErrorCode.java, backend/src/main/java/moadong/global/util/AESCipher.java
AES/GCM 암호화 개선: 고정 IV 취약점 수정(동적 랜덤 IV, v2: 프리픽스), 하위 호환성 유지. Notion/Google 통합 오류 코드 추가(950xx, 960xx).
테스트 추가
backend/src/test/java/moadong/calendar/service/CalendarAggregationServiceTest.java, backend/src/test/java/moadong/club/service/PromotionArticleServiceTest.java, backend/src/test/java/moadong/fixture/GoogleConnectionFixture.java, backend/src/test/java/moadong/media/service/BannerImageUploadServiceTest.java, backend/src/test/java/moadong/media/service/PromotionImageUploadServiceTest.java, backend/src/test/java/moadong/media/service/R2ImageUploadServiceTest.java
캘린더 집계, 프로모션 생성, 미디어 업로드 테스트 추가. R2 업로드로 S3 의존성 제거, 로케일 처리 검증 추가. GoogleConnection 픽스처 추가.
개발자 포털 UI
backend/src/main/resources/static/dev/index.html
프로모션 게시판 관리 섹션 추가: 목록/선택/편집 폼, 이미지 미리보기, CRUD 상호작용, 이미지 업로드 워크플로우. 더티 상태 감지, 유효성 검증, 네비게이션 경고.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant FE as 프론트엔드
    participant Controller as GoogleOAuthController
    participant Service as GoogleOAuthService
    participant DB as MongoDB
    participant GoogleAPI as Google API

    User->>FE: OAuth 인증 시작 (state 제공)
    FE->>Controller: GET /oauth/authorize?state=...
    Controller->>Service: getAuthorizeUrl(state)
    Service->>FE: Google 인증 URL 반환
    FE->>GoogleAPI: 리다이렉트 & 사용자 승인
    GoogleAPI-->>FE: code 포함 콜백
    FE->>Controller: POST /oauth/token { code }
    Controller->>Service: exchangeCode(user, request)
    Service->>GoogleAPI: code → access/refresh token
    GoogleAPI-->>Service: tokens + email
    Service->>Service: AES 암호화
    Service->>DB: GoogleConnection 저장 (암호화 토큰)
    DB-->>Service: 완료
    Service-->>Controller: email 반환
    Controller-->>FE: { email }
    FE->>Controller: GET /calendars
    Controller->>Service: getCalendars(user)
    Service->>DB: 저장된 토큰 조회
    Service->>Service: 복호화
    Service->>GoogleAPI: 캘린더 목록 요청
    GoogleAPI-->>Service: 캘린더 목록
    Service-->>Controller: { calendars: [...] }
    Controller-->>FE: 캘린더 표시
Loading
sequenceDiagram
    participant User as 사용자
    participant FE as 프론트엔드
    participant CalendarAgg as CalendarAggregationService
    participant NotionService as NotionOAuthService
    participant GoogleService as GoogleOAuthService
    participant DB as MongoDB

    User->>FE: 클럽 캘린더 이벤트 조회
    FE->>CalendarAgg: getAggregatedEvents(clubId)
    par 병렬 요청
        CalendarAgg->>NotionService: getClubCalendarEvents(clubId)
        NotionService->>DB: 저장된 Notion 토큰 조회
        NotionService-->>CalendarAgg: Notion 이벤트 리스트 (source: "NOTION")
    and
        CalendarAgg->>GoogleService: getClubCalendarEvents(clubId)
        GoogleService->>DB: 저장된 Google 토큰 조회
        GoogleService-->>CalendarAgg: Google 이벤트 리스트 (source: "GOOGLE")
    end
    CalendarAgg->>CalendarAgg: 병합 & start 기준 정렬
    CalendarAgg-->>FE: 통합 이벤트 리스트
    FE->>User: 캘린더 표시 (Notion/Google 이벤트 혼합)
Loading
sequenceDiagram
    participant User as 사용자
    participant FE as 프론트엔드
    participant PromotionCtrl as PromotionImageController
    participant PromotionService as PromotionImageUploadService
    participant R2Service as R2ImageUploadService
    participant DB as MongoDB
    participant R2 as Cloudflare R2

    User->>FE: 프로모션 이미지 업로드
    FE->>PromotionCtrl: POST /{articleId}/upload { file }
    PromotionCtrl->>PromotionService: upload(articleId, file)
    PromotionService->>DB: PromotionArticle 존재 확인
    alt 미존재
        DB-->>PromotionService: 없음
        PromotionService-->>PromotionCtrl: 예외: PROMOTION_ARTICLE_NOT_FOUND
    else 존재
        DB-->>PromotionService: 존재
        PromotionService->>PromotionService: S3 키 생성 (promotion/articles/{id}/...)
        PromotionService->>R2Service: upload(file, bucket, endpoint, key)
        R2Service->>R2Service: 파일 검증 (크기, 확장자, MIME)
        R2Service->>R2: putObject(key, file)
        R2-->>R2Service: 성공
        R2Service-->>PromotionService: CDN URL
        PromotionService-->>PromotionCtrl: PromotionImageUploadResponse
    end
    PromotionCtrl-->>FE: { imageUrl }
    FE->>User: 이미지 미리보기 표시
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • lepitaaar
  • Zepelown
  • oesnuj
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목 'Develop/be'는 개발 브랜치 이름일 뿐, 변경사항의 실제 내용을 설명하지 못합니다. 제목을 변경하여 주요 변경사항을 명확하게 표현하세요. 예: 'Notion/Google Calendar 통합 및 프로모션 이미지 업로드 기능 추가'
Docstring Coverage ⚠️ Warning Docstring coverage is 1.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 develop/be

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
Contributor

@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: 11

🧹 Nitpick comments (10)
backend/src/main/java/moadong/global/util/AESCipher.java (1)

28-28: null 또는 빈 문자열 입력 시 방어 로직 검토

encrypt()decrypt() 메서드에서 null 또는 빈 문자열 입력에 대한 검증이 없습니다. 상위 호출부에서 검증이 보장된다면 문제없지만, 방어적으로 유틸리티 레벨에서 처리하는 것을 고려해볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/moadong/global/util/AESCipher.java` at line 28, In
AESCipher, add defensive input validation to both encrypt(String text) and
decrypt(String text): if the argument is null return null immediately, if it's
an empty string return an empty string, otherwise proceed with
encryption/decryption; ensure the checks are the first lines in the encrypt and
decrypt methods so behavior is consistent and avoids NPEs or unnecessary
processing.
backend/src/main/java/moadong/club/payload/request/PromotionArticleCreateRequest.java (1)

16-16: images에 원소 단위 검증을 추가하는 것을 권장합니다.

Line 16은 리스트 자체만 검증하므로 "" 같은 무의미한 값이 통과할 수 있습니다. 비즈니스 정책상 빈 리스트를 허용하더라도 원소는 @NotBlank로 제한하는 편이 안전합니다.

검증 보강 예시
-public record PromotionArticleCreateRequest(
+public record PromotionArticleCreateRequest(
     `@NotBlank` String clubId,
     `@NotBlank` String title,
     `@NotBlank` String location,
     `@NotNull` Instant eventStartDate,
     `@NotNull` Instant eventEndDate,
     `@NotBlank` String description,
-    `@NotNull` List<String> images
+    `@NotNull` List<@NotBlank String> images
 ) {
 }

As per coding guidelines "Spring Boot + Gradle 기반 백엔드에서 예외 처리와 검증은 기존 프로젝트 방식과 일관되게 맞춘다".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/moadong/club/payload/request/PromotionArticleCreateRequest.java`
at line 16, The images field in PromotionArticleCreateRequest is only validated
as a non-null list, so empty or blank strings can pass; update the field to
validate elements by using an element-level constraint (e.g., change
List<String> images to List<@NotBlank String> images) and add the corresponding
import for `@NotBlank` (or javax.validation equivalent) so each entry must be
non-blank while keeping the list-level constraint (`@NotNull` or `@NotEmpty`) as
required by your business rules; adjust any DTO construction/tests that create
requests to satisfy the new element validation.
backend/src/test/java/moadong/calendar/service/CalendarAggregationServiceTest.java (1)

93-124: hasAnyCalendarConnection 테스트 케이스가 충분합니다.

Notion만, Google만, 둘 다 미연결 케이스가 잘 검증되었습니다. 완전성을 위해 둘 다 연결된 경우의 테스트도 추가하면 좋겠습니다.

✅ 둘 다 연결된 케이스 테스트 추가
`@Test`
`@DisplayName`("둘 다 연결된 경우 true를 반환한다")
void hasAnyCalendarConnection_bothConnected_returnsTrue() {
    when(notionOAuthService.hasCalendarConnection(CLUB_ID)).thenReturn(true);
    when(googleOAuthService.hasCalendarConnection(CLUB_ID)).thenReturn(true);

    boolean result = calendarAggregationService.hasAnyCalendarConnection(CLUB_ID);

    assertThat(result).isTrue();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/test/java/moadong/calendar/service/CalendarAggregationServiceTest.java`
around lines 93 - 124, Add a test to CalendarAggregationServiceTest verifying
the case when both providers are connected: mock
notionOAuthService.hasCalendarConnection(CLUB_ID) and
googleOAuthService.hasCalendarConnection(CLUB_ID) to return true, call
calendarAggregationService.hasAnyCalendarConnection(CLUB_ID), and assert the
result is true (e.g., name the test
hasAnyCalendarConnection_bothConnected_returnsTrue and include a `@DisplayName` "둘
다 연결된 경우 true를 반환한다").
backend/CLAUDE.md (1)

50-61: Markdown 코드 블록에 언어 지정이 누락되었습니다.

정적 분석 도구에서 경고한 대로, fenced code block에 언어를 지정하면 일관성과 가독성이 향상됩니다.

📝 수정 제안
-```
+```text
 src/main/java/moadong/
 ├── club/           # 동아리 CRUD, 모집, 지원서
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/CLAUDE.md` around lines 50 - 61, The fenced code block in
backend/CLAUDE.md is missing a language identifier; update the block that
contains the tree starting with "src/main/java/moadong/" to use an explicit
language tag (e.g., ```text or ```bash) so the markdown renderer and linters
correctly recognize the content and remove the static-analysis warning.
backend/src/main/java/moadong/calendar/service/CalendarAggregationService.java (2)

25-39: 예외 처리 범위를 좁히고 로그에 예외 객체를 포함해 주세요.

현재는 Exception을 광범위하게 삼키고 메시지만 남겨 장애 원인 추적이 어렵습니다. 통합 실패 격리는 유지하되, 예상 가능한 예외 타입으로 좁히고 log.warn(..., e) 형태로 스택 정보를 함께 남기는 쪽이 운영 안정성에 유리합니다.

As per coding guidelines "Spring Boot + Gradle 기반 백엔드에서 예외 처리와 검증은 기존 프로젝트 방식과 일관되게 맞춘다".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/moadong/calendar/service/CalendarAggregationService.java`
around lines 25 - 39, Narrow the catch blocks around
notionOAuthService.getClubCalendarEvents(clubId) and
googleOAuthService.getClubCalendarEvents(clubId) to handle specific expected
exceptions instead of catching Exception, and change the log.warn calls to pass
the exception object so the stacktrace is recorded (e.g., log.warn("...
clubId={}, message={}", clubId, e.getMessage(), e)); keep the per-service
isolation but replace broad catches with the concrete exception types thrown by
those methods (or a small set of relevant runtime exceptions) and include the
exception variable in the log.warn to preserve stack traces.

41-45: 문자열 정렬 대신 시간 타입 기준 정렬을 권장합니다.

start를 문자열로 비교하면 포맷이 섞일 때(날짜-only vs datetime/offset) 시간순 정렬이 깨질 수 있습니다. 파싱 가능한 시간 타입(LocalDate, OffsetDateTime, Instant)으로 정규화 후 정렬하는 쪽이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/moadong/calendar/service/CalendarAggregationService.java`
around lines 41 - 45, The current sort in CalendarAggregationService uses
Comparator.comparing(ClubCalendarEventResult::start,
Comparator.nullsLast(String::compareTo)), which compares start as raw strings
and breaks when formats vary; replace it by normalizing start into a temporal
type (e.g., Instant or OffsetDateTime) and sorting by that Instant while still
handling nulls. Implement a small parser method (e.g.,
parseStartToInstant(String start) in CalendarAggregationService or a utility)
that tries OffsetDateTime.parse, then falls back to
LocalDate.atStartOfDay(ZoneOffset.UTC) or a configurable zone, and return
Instant; then change the comparator to Comparator.comparing(e ->
parseStartToInstant(e.start()),
Comparator.nullsLast(Comparator.naturalOrder())). Ensure parsing exceptions are
caught and treated (e.g., return null) so nullsLast still applies.
backend/src/main/java/moadong/calendar/notion/service/NotionOAuthService.java (3)

142-230: 페이지네이션 로직 중복을 리팩토링하는 것을 권장합니다.

getRecentPages, getDatabases, getDatabasePages 메서드에서 페이지네이션 처리 로직이 거의 동일합니다. 공통 헬퍼 메서드로 추출하면 유지보수성이 향상됩니다.

♻️ 제안된 리팩토링 예시
private Map<String, Object> paginateNotionApi(
        String clubId,
        java.util.function.BiFunction<String, String, Map<String, Object>> apiCaller,
        String accessToken) {
    String nextCursor = null;
    List<Object> allResults = new ArrayList<>();
    int requestCount = 0;
    boolean hasMore = false;
    boolean partial = false;

    while (true) {
        Map<String, Object> responseBody = apiCaller.apply(accessToken, nextCursor);
        requestCount++;

        Object resultsObj = responseBody.get("results");
        if (resultsObj instanceof List<?> resultList) {
            allResults.addAll(resultList);
        }

        hasMore = Boolean.TRUE.equals(responseBody.get("has_more"));
        Object cursorObj = responseBody.get("next_cursor");
        nextCursor = cursorObj instanceof String cursor && StringUtils.hasText(cursor) ? cursor : null;

        if (!hasMore || !StringUtils.hasText(nextCursor)) {
            break;
        }

        if (requestCount >= 50) {
            partial = true;
            log.warn("Notion 페이지네이션 요청 상한 도달. clubId={}, collected={}", clubId, allResults.size());
            break;
        }
    }

    Map<String, Object> aggregated = new LinkedHashMap<>();
    aggregated.put("object", "list");
    aggregated.put("results", allResults);
    aggregated.put("has_more", partial && hasMore);
    aggregated.put("next_cursor", partial ? nextCursor : null);
    aggregated.put("total_results", allResults.size());
    if (partial) {
        aggregated.put("partial", true);
    }
    return aggregated;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/moadong/calendar/notion/service/NotionOAuthService.java`
around lines 142 - 230, The pagination logic in getRecentPages and getDatabases
(and getDatabasePages) is duplicated; extract it into a single helper (e.g.,
paginateNotionApi) that accepts clubId, the accessToken and a BiFunction<String
accessToken, String nextCursor, Map<String,Object>> apiCaller, then replace the
loops in getRecentPages/getDatabases/getDatabasePages to call paginateNotionApi
and return its aggregated map; ensure the helper reuses the same variables
(nextCursor, allResults, requestCount, hasMore, partial), logs the same warning
message, and uses searchNotionPages/searchNotionDatabases (or the appropriate
API method) as the apiCaller.

532-548: 레거시 연결 마이그레이션 후 원본 데이터 정리를 고려해보세요.

마이그레이션 시 새로운 연결을 저장하지만, 이전 레거시 연결(userId 기반)은 삭제되지 않습니다. 의도된 동작이라면 무시해도 되지만, 중복 데이터가 남을 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/moadong/calendar/notion/service/NotionOAuthService.java`
around lines 532 - 548, The migration in findLegacyNotionConnectionAndMigrate
creates a new NotionConnection from the legacy record but leaves the legacy
(userId-keyed) row behind; update this method to remove or mark the legacy entry
after successful save (e.g., call notionConnectionRepository.delete(legacy) or
deleteById(legacy.getId())) and perform the save+delete inside a transaction to
avoid data loss; ensure you reference the legacy variable returned from
notionConnectionRepository.findById and only delete it after
notionConnectionRepository.save(migrated) completes successfully, and
handle/propagate exceptions appropriately.

353-421: searchNotionPagessearchNotionDatabases 메서드 통합을 고려해보세요.

두 메서드는 filter 값만 다르고 나머지 로직이 동일합니다. 파라미터로 filter 타입을 받는 공통 메서드로 통합할 수 있습니다.

♻️ 제안된 리팩토링
-private Map<String, Object> searchNotionPages(String notionAccessToken, String startCursor) {
+private Map<String, Object> searchNotion(String notionAccessToken, String startCursor, String filterValue) {
     HttpHeaders headers = new HttpHeaders();
     headers.setBearerAuth(notionAccessToken);
     headers.setContentType(MediaType.APPLICATION_JSON);
     headers.set("Notion-Version", notionProperties.version());

     Map<String, Object> body = new LinkedHashMap<>();
-    body.put("filter", Map.of("property", "object", "value", "page"));
+    body.put("filter", Map.of("property", "object", "value", filterValue));
     body.put("sort", Map.of("direction", "descending", "timestamp", "last_edited_time"));
     body.put("page_size", 100);
     // ... rest of the method
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/moadong/calendar/notion/service/NotionOAuthService.java`
around lines 353 - 421, Both searchNotionPages and searchNotionDatabases
duplicate the same HTTP request logic; extract a single helper (e.g.,
searchNotionByObjectFilter) that takes the notionAccessToken and a filterValue
string ("page" or "database") plus optional startCursor, builds headers and the
request body with body.put("filter", Map.of("property","object","value",
filterValue)), performs the restTemplate.exchange call, handles null
responseBody and HttpStatusCodeException the same way, and then have
searchNotionPages and searchNotionDatabases delegate to that helper (preserving
existing log messages and thrown RestApiException/ErrorCode.NOTION_SEARCH_FAILED
behavior and the pagination page_size/start_cursor handling).
backend/src/main/java/moadong/calendar/google/service/GoogleOAuthService.java (1)

504-548: NotionOAuthService와 중복되는 헬퍼 메서드들이 있습니다.

requireAuthenticatedClubIdasString 메서드가 두 서비스에 동일하게 존재합니다. 공통 유틸리티 클래스나 추상 베이스 클래스로 추출하는 것을 고려해보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/moadong/calendar/google/service/GoogleOAuthService.java`
around lines 504 - 548, The requireAuthenticatedClubId and asString methods are
duplicated in GoogleOAuthService and NotionOAuthService; extract them into a
shared utility or base class and update both services to call the shared
implementations. Create either a static helper like
OAuthHelpers.requireAuthenticatedClubId(ClubRepository, CustomUserDetails) and
OAuthHelpers.asString(Object) preserving current behavior and exceptions, or
introduce an AbstractOAuthService that holds a protected ClubRepository and
implements protected String requireAuthenticatedClubId(CustomUserDetails) and
protected String asString(Object); then remove the duplicate methods from
GoogleOAuthService and NotionOAuthService and ensure dependency injection
(clubRepository) is wired if you choose the abstract-base approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/docs/frontend-google-calendar-integration.md`:
- Line 199: The markdown file frontend-google-calendar-integration.md contains
triple-backtick fence code blocks with ASCII UI mockups that lack a language
identifier (MD040); update each unlabeled block to use a language identifier by
changing the opening fence from ``` to ```text for all UI mockup blocks (the
three ASCII calendar/mockup blocks), so they become fenced as "text" and the
markdownlint MD040 warning is resolved.

In `@backend/src/main/java/moadong/calendar/google/entity/GoogleConnection.java`:
- Around line 35-42: The updateTokens method in GoogleConnection currently
unconditionally overwrites encryptedRefreshToken which can erase a valid stored
refresh token when Google omits refresh_token on non-reconsent flows; change
updateTokens to only set this.encryptedRefreshToken when the
encryptedRefreshToken parameter is non-null and non-empty (e.g.,
StringUtils.hasText/enforce a null/empty check), while still updating
encryptedAccessToken, email, tokenExpiresAt and updatedAt as before; also review
GoogleOAuthService.saveGoogleConnection() usage to ensure it passes null when no
new refresh token is available so the guarded update in GoogleConnection
preserves the existing refresh token.

In
`@backend/src/main/java/moadong/calendar/google/payload/request/GoogleCalendarSelectRequest.java`:
- Around line 6-7: GoogleCalendarSelectRequest currently declares an unused
`@NotBlank` String calendarId while selectCalendar() uses the path variable
calendarId; either remove calendarId from the DTO (keep only String
calendarName) to avoid unnecessary validation, or add explicit validation in the
controller/service selectCalendar(...) to compare the path variable calendarId
with request.getCalendarId() and throw a BadRequestException if they differ;
update tests and DTO usages accordingly and ensure `@NotBlank` is removed if you
choose to delete the field.

In
`@backend/src/main/java/moadong/calendar/google/service/GoogleOAuthService.java`:
- Around line 316-319: The warning is logged too early while the loop may still
continue; update the condition in the pagination loop (references: page,
MAX_GOOGLE_PAGE_REQUESTS, nextPageToken, pageToken) to only warn when you've
reached the page limit and there actually is another page to fetch — e.g. change
the check to require nextPageToken != null (if page == MAX_GOOGLE_PAGE_REQUESTS
- 1 && nextPageToken != null) before calling log.warn(...), so the warning
reflects that further pages exist but won't be fetched.
- Around line 167-170: The warning about hitting the page limit is logged too
early; in GoogleOAuthService (look for MAX_GOOGLE_PAGE_REQUESTS, page,
pageToken, nextPageToken) change the log placement so it is emitted only when
you actually stop paging (i.e., right before you break/return because the max
pages was reached or when you detect that nextPageToken is non-null but page ==
MAX_GOOGLE_PAGE_REQUESTS - 1 and you will not request another page). Concretely,
remove the current log from the `if (page == MAX_GOOGLE_PAGE_REQUESTS - 1)` spot
and add it immediately where you decide to stop paging (the break/return branch
that follows detection of the page limit), keeping the same message and
including clubId and MAX_GOOGLE_PAGE_REQUESTS.

In `@backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java`:
- Around line 41-43: The current ClubDetailedResult.of(Club,
List<ClubCalendarEventResult>) calls !calendarEvents.isEmpty() and can NPE if
calendarEvents is null; update this method to normalize calendarEvents before
checking/forwarding (e.g., replace the direct call with a null-safe check or
assign calendarEvents =
Optional.ofNullable(calendarEvents).orElse(Collections.emptyList())) and then
call the overloaded of(Club, List<ClubCalendarEventResult>, boolean) with the
normalized list and its isEmpty() result so the null case is handled safely.

In `@backend/src/main/java/moadong/club/service/ClubProfileService.java`:
- Around line 93-94: The code is using
calendarAggregationService.hasAnyCalendarConnection(club.getId()) to populate
hasCalendarEvents, but that method only checks for connections not actual event
existence; update ClubProfileService to pass the correct boolean into
ClubDetailedResult.of by either (A) changing the field name to reflect
connection status (e.g., hasCalendarConnection) where
ClubDetailedResult.of(club, List.of(), hasCalendarConnection) is used, or (B)
compute a real event-existence flag by calling the appropriate
calendarAggregationService method that checks for events (e.g.,
hasAnyCalendarEvents or a count method) and pass that boolean as
hasCalendarEvents; adjust both locations (the lines around hasCalendarEvents and
the other occurrence at lines ~102-103) and ensure the ClubDetailedResult
factory/field name matches the chosen semantics.

In `@backend/src/main/java/moadong/global/exception/ErrorCode.java`:
- Around line 86-87: The two ErrorCode enum entries NOTION_SEARCH_FAILED and
NOTION_DATABASE_QUERY_FAILED are mapped to HttpStatus.BAD_REQUEST but represent
upstream Notion failures and should be 5xx; update their HttpStatus from
BAD_REQUEST to HttpStatus.BAD_GATEWAY to match GOOGLE_API_FAILED and consistent
retry/alert semantics (also check and adjust any related entry 95-95 similarly
if present).

In `@backend/src/main/java/moadong/media/service/BannerImageUploadService.java`:
- Around line 23-30: The upload method in BannerImageUploadService currently
falls back to "image" when file.getOriginalFilename() is null, producing keys
without extensions (e.g., "web/image"); update upload(MultipartFile file,
PlatformType type) to preserve/append a valid file extension: first try to
extract the extension from the original filename, and if absent, map
file.getContentType() to a reasonable extension (use a small mapping or MimeType
utilities) and append it to the sanitized base name; ensure sanitizeFilename is
applied after you build the filename-with-extension and then use that result
when creating key passed to r2ImageUploadService.upload (keep bannerBucketName
and bannerViewEndpoint usage unchanged).

In
`@backend/src/main/java/moadong/media/service/PromotionImageUploadService.java`:
- Around line 43-46: buildPromotionImageKey currently calls
file.getOriginalFilename() directly which can throw NPE before
R2ImageUploadService's validation runs; change the method to null-safe read of
the filename (e.g., treat file == null or file.getOriginalFilename() == null as
an empty string) before cleaning and sanitizing so the code consistently follows
the common validation path in R2ImageUploadService; update logic around
StringUtils.cleanPath and sanitizeFilename to operate on the null-safe filename
and keep method name buildPromotionImageKey and helper sanitizeFilename
unchanged.

In `@backend/src/main/resources/static/dev/index.html`:
- Around line 1690-1709: The create/edit flow is not atomic:
createPromotionArticleRequest is called separately from the image upload (from
normalizePromotionPayload), leaving orphaned articles or files on partial
failure; fix by making the server handle article+uploads in one endpoint or
implement client-side compensation: in isPromotionCreateMode() after calling
createPromotionArticleRequest include uploaded file references in the same
request or, if you must upload afterwards, call a rollback API (e.g.,
deletePromotionArticleRequest) to remove the newly created article when the
subsequent upload fails; similarly in edit mode ensure the PUT
(updatePromotionArticleRequest) and file uploads are performed atomically or, on
upload failure, call deleteUploadedFilesRequest for any newly uploaded files and
revert the article update; update error handling around
normalizePromotionPayload, createPromotionArticleRequest and the edit-path PUT
to trigger these compensating calls and surface errors via setPromotionBanner.

---

Nitpick comments:
In `@backend/CLAUDE.md`:
- Around line 50-61: The fenced code block in backend/CLAUDE.md is missing a
language identifier; update the block that contains the tree starting with
"src/main/java/moadong/" to use an explicit language tag (e.g., ```text or
```bash) so the markdown renderer and linters correctly recognize the content
and remove the static-analysis warning.

In
`@backend/src/main/java/moadong/calendar/google/service/GoogleOAuthService.java`:
- Around line 504-548: The requireAuthenticatedClubId and asString methods are
duplicated in GoogleOAuthService and NotionOAuthService; extract them into a
shared utility or base class and update both services to call the shared
implementations. Create either a static helper like
OAuthHelpers.requireAuthenticatedClubId(ClubRepository, CustomUserDetails) and
OAuthHelpers.asString(Object) preserving current behavior and exceptions, or
introduce an AbstractOAuthService that holds a protected ClubRepository and
implements protected String requireAuthenticatedClubId(CustomUserDetails) and
protected String asString(Object); then remove the duplicate methods from
GoogleOAuthService and NotionOAuthService and ensure dependency injection
(clubRepository) is wired if you choose the abstract-base approach.

In
`@backend/src/main/java/moadong/calendar/notion/service/NotionOAuthService.java`:
- Around line 142-230: The pagination logic in getRecentPages and getDatabases
(and getDatabasePages) is duplicated; extract it into a single helper (e.g.,
paginateNotionApi) that accepts clubId, the accessToken and a BiFunction<String
accessToken, String nextCursor, Map<String,Object>> apiCaller, then replace the
loops in getRecentPages/getDatabases/getDatabasePages to call paginateNotionApi
and return its aggregated map; ensure the helper reuses the same variables
(nextCursor, allResults, requestCount, hasMore, partial), logs the same warning
message, and uses searchNotionPages/searchNotionDatabases (or the appropriate
API method) as the apiCaller.
- Around line 532-548: The migration in findLegacyNotionConnectionAndMigrate
creates a new NotionConnection from the legacy record but leaves the legacy
(userId-keyed) row behind; update this method to remove or mark the legacy entry
after successful save (e.g., call notionConnectionRepository.delete(legacy) or
deleteById(legacy.getId())) and perform the save+delete inside a transaction to
avoid data loss; ensure you reference the legacy variable returned from
notionConnectionRepository.findById and only delete it after
notionConnectionRepository.save(migrated) completes successfully, and
handle/propagate exceptions appropriately.
- Around line 353-421: Both searchNotionPages and searchNotionDatabases
duplicate the same HTTP request logic; extract a single helper (e.g.,
searchNotionByObjectFilter) that takes the notionAccessToken and a filterValue
string ("page" or "database") plus optional startCursor, builds headers and the
request body with body.put("filter", Map.of("property","object","value",
filterValue)), performs the restTemplate.exchange call, handles null
responseBody and HttpStatusCodeException the same way, and then have
searchNotionPages and searchNotionDatabases delegate to that helper (preserving
existing log messages and thrown RestApiException/ErrorCode.NOTION_SEARCH_FAILED
behavior and the pagination page_size/start_cursor handling).

In
`@backend/src/main/java/moadong/calendar/service/CalendarAggregationService.java`:
- Around line 25-39: Narrow the catch blocks around
notionOAuthService.getClubCalendarEvents(clubId) and
googleOAuthService.getClubCalendarEvents(clubId) to handle specific expected
exceptions instead of catching Exception, and change the log.warn calls to pass
the exception object so the stacktrace is recorded (e.g., log.warn("...
clubId={}, message={}", clubId, e.getMessage(), e)); keep the per-service
isolation but replace broad catches with the concrete exception types thrown by
those methods (or a small set of relevant runtime exceptions) and include the
exception variable in the log.warn to preserve stack traces.
- Around line 41-45: The current sort in CalendarAggregationService uses
Comparator.comparing(ClubCalendarEventResult::start,
Comparator.nullsLast(String::compareTo)), which compares start as raw strings
and breaks when formats vary; replace it by normalizing start into a temporal
type (e.g., Instant or OffsetDateTime) and sorting by that Instant while still
handling nulls. Implement a small parser method (e.g.,
parseStartToInstant(String start) in CalendarAggregationService or a utility)
that tries OffsetDateTime.parse, then falls back to
LocalDate.atStartOfDay(ZoneOffset.UTC) or a configurable zone, and return
Instant; then change the comparator to Comparator.comparing(e ->
parseStartToInstant(e.start()),
Comparator.nullsLast(Comparator.naturalOrder())). Ensure parsing exceptions are
caught and treated (e.g., return null) so nullsLast still applies.

In
`@backend/src/main/java/moadong/club/payload/request/PromotionArticleCreateRequest.java`:
- Line 16: The images field in PromotionArticleCreateRequest is only validated
as a non-null list, so empty or blank strings can pass; update the field to
validate elements by using an element-level constraint (e.g., change
List<String> images to List<@NotBlank String> images) and add the corresponding
import for `@NotBlank` (or javax.validation equivalent) so each entry must be
non-blank while keeping the list-level constraint (`@NotNull` or `@NotEmpty`) as
required by your business rules; adjust any DTO construction/tests that create
requests to satisfy the new element validation.

In `@backend/src/main/java/moadong/global/util/AESCipher.java`:
- Line 28: In AESCipher, add defensive input validation to both encrypt(String
text) and decrypt(String text): if the argument is null return null immediately,
if it's an empty string return an empty string, otherwise proceed with
encryption/decryption; ensure the checks are the first lines in the encrypt and
decrypt methods so behavior is consistent and avoids NPEs or unnecessary
processing.

In
`@backend/src/test/java/moadong/calendar/service/CalendarAggregationServiceTest.java`:
- Around line 93-124: Add a test to CalendarAggregationServiceTest verifying the
case when both providers are connected: mock
notionOAuthService.hasCalendarConnection(CLUB_ID) and
googleOAuthService.hasCalendarConnection(CLUB_ID) to return true, call
calendarAggregationService.hasAnyCalendarConnection(CLUB_ID), and assert the
result is true (e.g., name the test
hasAnyCalendarConnection_bothConnected_returnsTrue and include a `@DisplayName` "둘
다 연결된 경우 true를 반환한다").
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0db5f813-8799-46f2-bf59-061405d186c3

📥 Commits

Reviewing files that changed from the base of the PR and between 41c14a3 and 40f05ca.

📒 Files selected for processing (51)
  • backend/.gitignore
  • backend/.infisical.json
  • backend/CLAUDE.md
  • backend/build.gradle
  • backend/docs/frontend-google-calendar-integration.md
  • backend/docs/notion-integration-decisions-2026-03-22.md
  • backend/docs/superpowers/specs/2026-03-30-google-calendar-integration-design.md
  • backend/gradle/infisical.gradle
  • backend/src/main/java/moadong/calendar/google/config/GoogleCalendarProperties.java
  • backend/src/main/java/moadong/calendar/google/controller/GoogleOAuthController.java
  • backend/src/main/java/moadong/calendar/google/entity/GoogleConnection.java
  • backend/src/main/java/moadong/calendar/google/payload/dto/GoogleTokenApiResponse.java
  • backend/src/main/java/moadong/calendar/google/payload/request/GoogleCalendarSelectRequest.java
  • backend/src/main/java/moadong/calendar/google/payload/request/GoogleTokenExchangeRequest.java
  • backend/src/main/java/moadong/calendar/google/payload/response/GoogleTokenExchangeResponse.java
  • backend/src/main/java/moadong/calendar/google/repository/GoogleConnectionRepository.java
  • backend/src/main/java/moadong/calendar/google/service/GoogleOAuthService.java
  • backend/src/main/java/moadong/calendar/notion/config/NotionProperties.java
  • backend/src/main/java/moadong/calendar/notion/controller/NotionOAuthController.java
  • backend/src/main/java/moadong/calendar/notion/entity/NotionConnection.java
  • backend/src/main/java/moadong/calendar/notion/payload/dto/NotionTokenApiResponse.java
  • backend/src/main/java/moadong/calendar/notion/payload/request/NotionTokenExchangeRequest.java
  • backend/src/main/java/moadong/calendar/notion/payload/response/NotionTokenExchangeResponse.java
  • backend/src/main/java/moadong/calendar/notion/repository/NotionConnectionRepository.java
  • backend/src/main/java/moadong/calendar/notion/service/NotionOAuthService.java
  • backend/src/main/java/moadong/calendar/service/CalendarAggregationService.java
  • backend/src/main/java/moadong/club/controller/ClubProfileController.java
  • backend/src/main/java/moadong/club/controller/PromotionArticleController.java
  • backend/src/main/java/moadong/club/payload/dto/ClubCalendarEventResult.java
  • backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java
  • backend/src/main/java/moadong/club/payload/dto/PromotionArticleCreateResultDto.java
  • backend/src/main/java/moadong/club/payload/request/PromotionArticleCreateRequest.java
  • backend/src/main/java/moadong/club/payload/response/ClubCalendarEventsResponse.java
  • backend/src/main/java/moadong/club/service/ClubProfileService.java
  • backend/src/main/java/moadong/club/service/PromotionArticleService.java
  • backend/src/main/java/moadong/global/exception/ErrorCode.java
  • backend/src/main/java/moadong/global/util/AESCipher.java
  • backend/src/main/java/moadong/media/controller/PromotionImageController.java
  • backend/src/main/java/moadong/media/dto/PromotionImageUploadResponse.java
  • backend/src/main/java/moadong/media/service/BannerImageUploadService.java
  • backend/src/main/java/moadong/media/service/PromotionImageUploadService.java
  • backend/src/main/java/moadong/media/service/R2ImageUploadService.java
  • backend/src/main/java/moadong/media/util/ClubImageUtil.java
  • backend/src/main/resources/static/dev/index.html
  • backend/src/test/java/moadong/calendar/service/CalendarAggregationServiceTest.java
  • backend/src/test/java/moadong/club/service/PromotionArticleServiceTest.java
  • backend/src/test/java/moadong/fixture/GoogleConnectionFixture.java
  • backend/src/test/java/moadong/media/service/BannerImageUploadServiceTest.java
  • backend/src/test/java/moadong/media/service/PromotionImageUploadServiceTest.java
  • backend/src/test/java/moadong/media/service/R2ImageUploadServiceTest.java
  • docs/draft/promotion-admin-create-plan-2026-03-29.md

@seongwon030 seongwon030 mentioned this pull request Apr 5, 2026
Copy link
Copy Markdown
Collaborator

@suhyun113 suhyun113 left a comment

Choose a reason for hiding this comment

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

릴리즈 가시져

@seongwon030 seongwon030 merged commit 3fc4c7a into main Apr 5, 2026
5 checks passed
@seongwon030 seongwon030 changed the title Develop/be [release] BE v1.4.0 Apr 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💾 BE Backend ➕ MINOR Minor 릴리즈 📈 release 릴리즈 배포

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants