Skip to content

[feature] 동아리 관리자 통계 API 구현#1814

Open
Zepelown wants to merge 1 commit into
develop/befrom
feature/#1813-statistic-MOA-1029
Open

[feature] 동아리 관리자 통계 API 구현#1814
Zepelown wants to merge 1 commit into
develop/befrom
feature/#1813-statistic-MOA-1029

Conversation

@Zepelown

@Zepelown Zepelown commented Jul 8, 2026

Copy link
Copy Markdown
Member

#️⃣연관된 이슈

#1813

📝작업 내용

동아리 관리자 페이지에서 사용할 통계 백엔드 API를 구현했습니다. Mixpanel은 초기 과거 데이터 backfill 용도로만 사용하고, 이후 상세 조회수와 검색어는 백엔드 API 흐름에서 직접 집계합니다.

변경 사항

항목 내용
관리자 통계 조회 내 동아리 overview/trend/search-keywords API 추가
실시간 집계 동아리 상세 조회와 검색 API 흐름에서 일별 통계 직접 기록
지원자 추이 지원서 formId와 지원자 createdAt 기준 일자별 지원자 수 집계
Mixpanel backfill 개발자 권한 전용 수동 backfill API 및 dev 페이지 버튼 추가
저장 구조 club_analytics_daily, club_search_keyword_daily, mixpanel_backfilled_events 컬렉션 추가
안정성 backfill 기간 제한, UTC to_date 검증, dedup 및 실패 시 rollback 처리
문서 구현 계획과 운영 주의사항 문서 추가

✅ 테스트

  • .\gradlew.bat test

🫡 참고사항

  • 카드 노출수/카드 클릭수는 1차 범위에서 제외했습니다.
  • Mixpanel backfill은 서버 자체 집계가 배포되기 전 과거 기간에만 실행해야 중복 집계를 피할 수 있습니다.
  • 운영 환경에서는 mixpanel.enabled, project-id, service-account 설정이 필요합니다.

Summary by CodeRabbit

  • New Features

    • 관리자용 클럽 통계 조회 화면과 API가 추가되어, 개요·추이·검색 키워드 통계를 확인할 수 있습니다.
    • Mixpanel 데이터 백필 기능이 추가되어, 지정 기간의 이벤트를 일괄 반영할 수 있습니다.
    • 클럽 상세 조회와 검색 시 통계가 자동으로 누적됩니다.
  • Bug Fixes

    • 날짜 범위 검증과 기본값 처리가 보강되어 잘못된 입력을 더 안전하게 처리합니다.
    • 중복 백필 이벤트를 방지해 재실행 시 동일 데이터가 반복 반영되지 않습니다.

@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
moadong Ready Ready Preview, Comment Jul 8, 2026 11:01pm

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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. 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

Mixpanel 이벤트 데이터를 활용한 동아리 통계 기능이 추가되었다. Mixpanel Export API 연동, 이벤트 백필 서비스, 실시간 상세뷰/검색어 기록, 신규 MongoDB 집계 컬렉션/레포지토리, 관리자용 통계 조회 및 백필 API, 관리자 대시보드 UI가 도입되었으며 관련 설계 문서와 테스트가 포함되었다.

Changes

동아리 관리자 통계 및 Mixpanel Backfill 기능

Layer / File(s) Summary
설정 및 공통 유틸리티
MixpanelProperties.java, ErrorCode.java, AnalyticsTime.java, AnalyticsDateRangeValidator.java
Mixpanel 서버/서비스계정/백필 기본값 계산, KST 날짜 유틸리티, 통계 관련 신규 ErrorCode 3종, 조회/백필 날짜 범위 검증 로직을 추가.
데이터 모델 및 레포지토리
entity/ClubAnalyticsDaily.java, entity/ClubSearchKeywordDaily.java, entity/MixpanelBackfilledEvent.java, repository/*Repository.java, payload/dto/MixpanelRawEvent.java, club/entity/ClubApplicant.java, club/repository/ClubApplicantsRepository.java
일별 조회/검색어 집계 및 백필 중복방지 컬렉션과 각 MongoRepository, Mixpanel 원본 이벤트 DTO를 정의하고 지원자 조회용 인덱스/쿼리 메서드를 추가.
실시간 통계 기록 서비스
service/ClubAnalyticsRecordService.java, club/service/ClubProfileService.java, club/service/ClubSearchService.java, 관련 테스트
상세뷰/검색어 이벤트를 upsert로 누적 기록하며, 클럽 상세 조회 및 검색 흐름에 기록 호출을 연동하고 검증 테스트를 추가.
Mixpanel Export 및 Backfill 서비스
service/MixpanelExportClient.java, service/MixpanelBackfillService.java, controller/MixpanelBackfillAdminController.java, payload/response/MixpanelBackfillResponse.java, 테스트
Export API 호출/파싱, 이벤트 중복제거 및 이벤트별 집계 처리, 클럽명 매핑, 개발자 권한 백필 엔드포인트를 구현.
관리자 통계 조회 API
service/ClubApplicantStatisticsService.java, service/ClubStatisticsAdminService.java, payload/response/ClubStatistics*Response.java, controller/ClubStatisticsAdminController.java
일별 지원자 수 집계, overview/trend/search-keywords 조회 로직 및 응답 DTO, 인증된 GET 엔드포인트 3종을 추가.
관리자 Backfill UI
resources/static/dev/index.html
사이드바 및 백필 실행 섹션, 버튼 클릭 시 백필 API 호출과 결과 표시 스크립트를 추가.
설계 문서
docs/draft/club-admin-mixpanel-statistics-plan-2026-07-09.md
전체 설계, API 계약, 컬렉션 스키마, 정책, 테스트 계획을 정리한 신규 문서를 추가.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: ✨ Feature, 📬 API, 💾 BE, 📃 Docs, 💻 FE

Suggested reviewers: lepitaaar

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 동아리 관리자 통계 API와 관련 백엔드/백필 기능 추가를 잘 요약한 제목입니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 feature/#1813-statistic-MOA-1029

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a comprehensive club statistics and analytics module, including real-time tracking of club detail views and search keywords, MongoDB aggregation, and a Mixpanel backfill feature for historical data. Key feedback points out critical timezone handling issues in applicant statistics and date range validation, a redundant database check in the real-time tracking service, and a potential memory leak in the Mixpanel JSONL parser when processing large payloads.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Test Results

167 tests   167 ✅  24s ⏱️
 41 suites    0 💤
 41 files      0 ❌

Results for commit fd86f53.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (7)
backend/src/test/java/moadong/analytics/service/MixpanelBackfillServiceTest.java (1)

38-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

테스트 커버리지 확장 권장

성공 경로(이벤트 처리 완료), 중복 이벤트 처리, 날짜 불일치 skip, clubName 중복 skip, 각 이벤트 타입별 처리 검증이 부족합니다. 백필 파이프라인의 신뢰성을 위해 추가 테스트를 권장합니다.

🤖 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
`@backend/src/test/java/moadong/analytics/service/MixpanelBackfillServiceTest.java`
around lines 38 - 68, The current MixpanelBackfillServiceTest only covers the
disabled-flow and rollback-on-failure cases, so expand coverage around
MixpanelBackfillService.backfill and related event handling. Add tests for the
successful processing path, duplicate event deduplication, date-mismatch
skipping, duplicate clubName skipping, and each supported event type to verify
the pipeline behaves correctly across all branches.
backend/src/main/java/moadong/analytics/service/MixpanelBackfillService.java (1)

79-84: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

catch 블록의 deleteById가 원본 예외를 가릴 수 있음

deleteById 자체가 예외를 던지면 원본 처리 실패 예외(e)가 손실됩니다. 롤백 실패를 로깅하더라도 원본 예외를 보존하도록 감싸는 것을 권장합니다.

♻️ 제안: 원본 예외 보존
 } catch (Exception e) {
-    mixpanelBackfilledEventRepository.deleteById(backfillKey);
+    try {
+        mixpanelBackfilledEventRepository.deleteById(backfillKey);
+    } catch (Exception deleteEx) {
+        log.warn("Mixpanel backfill 롤백 실패. backfillKey={}", backfillKey, deleteEx);
+    }
     log.error("Mixpanel backfill 이벤트 처리 실패. eventName={}, eventDate={}",
             event.event(), eventDate, e);
     throw e;
 }
🤖 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 `@backend/src/main/java/moadong/analytics/service/MixpanelBackfillService.java`
around lines 79 - 84, The catch block in
MixpanelBackfillService.process/backfill flow currently calls
mixpanelBackfilledEventRepository.deleteById(backfillKey) directly, which can
throw and hide the original exception e. Wrap the rollback delete in its own
try/catch so any cleanup failure is only logged, then rethrow the original
exception e unchanged; use the existing log.error and the backfillKey/event
metadata to identify the failure path.
backend/src/main/java/moadong/analytics/service/ClubStatisticsAdminService.java (1)

100-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

컬렉션 이름 하드코딩 대신 엔티티에서 유도 권장

"club_search_keyword_daily" 컬렉션 이름이 문자열 리터럴로 하드코딩되어 있습니다. ClubSearchKeywordDaily 엔티티의 @Document 어노테이션 값과 불일치가 발생할 수 있습니다. 엔티티 변경 시 이곳도 함께 수정해야 하므로 유지보수 리스크가 있습니다.

♻️ 제안: 엔티티 메타데이터에서 컬렉션 이름 유도
-                mongoTemplate.aggregate(
-                        aggregation,
-                        "club_search_keyword_daily",
-                        SearchKeywordStatisticsResponse.SearchKeywordRankItem.class
-                );
+                mongoTemplate.aggregate(
+                        aggregation,
+                        moadong.analytics.entity.ClubSearchKeywordDaily.class,
+                        SearchKeywordStatisticsResponse.SearchKeywordRankItem.class
+                );
🤖 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
`@backend/src/main/java/moadong/analytics/service/ClubStatisticsAdminService.java`
around lines 100 - 103, The collection name used in ClubStatisticsAdminService
is hardcoded as a string literal, which can drift from the
ClubSearchKeywordDaily entity’s `@Document` value. Update the aggregation call to
derive the collection name from ClubSearchKeywordDaily metadata instead of
embedding "club_search_keyword_daily" directly, so the service stays aligned if
the entity mapping changes.
backend/src/main/java/moadong/analytics/entity/ClubSearchKeywordDaily.java (1)

15-27: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

@Indexed on date 중복 인덱스 제거 권장

복합 인덱스 {'date': 1, 'normalizedKeyword': 1}의 최좌측 필드가 date이므로, date 단일 필드 쿼리는 이미 복합 인덱스로 처리됩니다. @Indexed on date는 불필요한 중복 인덱스입니다. 주요 집계 쿼리가 date 범위만으로 필터링하므로 복합 인덱스 필드 순서는 적절합니다.

♻️ 제안: 중복 인덱스 제거
 `@Document`("club_search_keyword_daily")
 `@CompoundIndex`(name = "date_keyword_unique", def = "{'date': 1, 'normalizedKeyword': 1}", unique = true)
 `@Getter`
 `@NoArgsConstructor`
 `@AllArgsConstructor`
 `@Builder`
 public class ClubSearchKeywordDaily {

     `@Id`
     private String id;

-    `@Indexed`
     private LocalDate date;
🤖 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 `@backend/src/main/java/moadong/analytics/entity/ClubSearchKeywordDaily.java`
around lines 15 - 27, Remove the redundant single-field index on the date
property in ClubSearchKeywordDaily, since the existing date_keyword_unique
CompoundIndex already covers date as the leftmost field. Keep the compound index
on date and normalizedKeyword unchanged, and update the entity annotations
around ClubSearchKeywordDaily so only the necessary indexing remains.
backend/src/main/java/moadong/analytics/entity/ClubAnalyticsDaily.java (1)

15-30: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

중복 인덱스 및 복합 인덱스 필드 순서 최적화 필요

두 가지 인덱스 성능 이슈가 있습니다:

  1. @Indexed on date 중복: 복합 인덱스 {'date': 1, 'clubId': 1}의 최좌측 필드가 date이므로, date 단일 필드 쿼리는 이미 복합 인덱스로 처리됩니다. @Indexed on date는 불필요한 중복 인덱스로 저장 공간과 쓰기 성능을 낭비합니다.

  2. 복합 인덱스 필드 순서: 주요 쿼리인 findByClubIdAndDateBetweenOrderByDateAsc(clubId, from, to)clubId 등가 매칭 + date 범위 스캔 + date 정렬입니다. 현재 {date: 1, clubId: 1} 순서는 date 범위 스캔 후 clubId 필터링이 필요하지만, {clubId: 1, date: 1} 순서면 clubId 등가 매칭 후 date 범위 스캔으로 더 효율적이며 정렬도 인덱스로 처리 가능합니다. upsert 고유성 제약도 동일하게 만족합니다.

♻️ 제안: 중복 인덱스 제거 및 복합 인덱스 필드 순서 변경
 `@Document`("club_analytics_daily")
-@CompoundIndex(name = "date_club_unique", def = "{'date': 1, 'clubId': 1}", unique = true)
+@CompoundIndex(name = "club_date_unique", def = "{'clubId': 1, 'date': 1}", unique = true)
 `@Getter`
 `@NoArgsConstructor`
 `@AllArgsConstructor`
 `@Builder`
 public class ClubAnalyticsDaily {

     `@Id`
     private String id;

-    `@Indexed`
     private LocalDate date;

-    `@Indexed`
     private String clubId;
🤖 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 `@backend/src/main/java/moadong/analytics/entity/ClubAnalyticsDaily.java`
around lines 15 - 30, Remove the redundant `@Indexed` annotation from
ClubAnalyticsDaily.date because the existing compound index already covers date
as the leftmost field. Update the `@CompoundIndex` definition on
ClubAnalyticsDaily so the field order matches the main query pattern, placing
clubId before date. Keep the unique constraint intact while adjusting the index
declaration to support findByClubIdAndDateBetweenOrderByDateAsc more
efficiently.
backend/src/main/java/moadong/analytics/service/ClubAnalyticsRecordService.java (1)

24-33: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

recordDetailView에서 중복 existsById 호출 제거 검토

recordDetailViewincrementClubDaily 호출 시 clubRepository.existsById(clubId)가 실행됩니다. 하지만 ClubProfileService의 모든 호출 지점(getClubDetail, getClubDetailByClubName)에서 이미 클럽을 조회했으므로 이 검증은 중복되어 매 상세 조회마다 추가 MongoDB 왕복 비용이 발생합니다. incrementClubDailyWithoutExistenceCheck를 직접 호출하거나, recordDetailView가 별도의 존재 검증 없이 upsert만 수행하도록 변경을 검토하세요.

♻️ 제안: recordDetailView에서 existsById 생략
 public void recordDetailView(String clubId, String clubName) {
     try {
         if (clubId == null || clubId.isBlank()) {
             return;
         }
-        incrementClubDaily(clubId, clubName, AnalyticsTime.todayKst(), 1, 0, 0);
+        incrementClubDailyWithoutExistenceCheck(
+                clubId, clubName, AnalyticsTime.todayKst(), 1, 0, 0);
     } catch (Exception e) {
         log.warn("동아리 상세 조회 통계 기록 실패. clubId={}", clubId, e);
     }
 }

Also applies to: 47-70

🤖 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
`@backend/src/main/java/moadong/analytics/service/ClubAnalyticsRecordService.java`
around lines 24 - 33, `recordDetailView` is doing a redundant existence check
through `incrementClubDaily`, causing an extra database round trip on every
detail view. Update `ClubAnalyticsRecordService.recordDetailView` to skip the
`clubRepository.existsById` path by calling
`incrementClubDailyWithoutExistenceCheck` directly, or make the detail-view
recording path perform only the upsert logic. Keep the existing null/blank
`clubId` guard and preserve the current exception handling/logging.
backend/src/main/java/moadong/club/service/ClubSearchService.java (1)

29-29: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

검색어 통계 기록을 비동기 처리하는 것을 권장합니다.

recordSearchKeyword는 내부적으로 모든 예외를 catch하여 검색 흐름이 깨지지는 않지만, MongoDB upsert를 동기적으로 수행하므로 검색 요청 스레드에 I/O 지연이 추가됩니다. 검색 API 응답 시간에 민감한 경우 @Async 적용 또는 이벤트 기반 비동기 처리를 고려하세요.

Also applies to: 46-46

🤖 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 `@backend/src/main/java/moadong/club/service/ClubSearchService.java` at line
29, Make the search keyword statistics recording asynchronous in
ClubSearchService by moving the recordSearchKeyword invocation off the request
thread. Update the ClubSearchService flow around clubAnalyticsRecordService and
recordSearchKeyword so it uses `@Async` or an event-driven async handoff, and
ensure the existing exception-swallowing behavior is preserved so search results
are not affected if analytics persistence fails.
🤖 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
`@backend/src/main/java/moadong/analytics/service/ClubStatisticsAdminService.java`:
- Around line 107-109: `validateClubManager` currently only delegates to
`findClub(clubId)`, so the method name implies manager authorization that it
does not perform. Update `ClubStatisticsAdminService.validateClubManager` to
either rename it to something like `validateClubExists` so it matches the actual
behavior, or add real manager-permission checks there if that is the intended
contract. Make sure the call site in `ClubStatisticsAdminController` still uses
the correctly named/behaving validation method.

In
`@backend/src/main/java/moadong/analytics/support/AnalyticsDateRangeValidator.java`:
- Around line 22-30: The upper bound check in validateBackfillRange is using
UTC, which can reject a backfill date that is still valid in KST. Update
AnalyticsDateRangeValidator.validateBackfillRange to compare against
AnalyticsTime.todayKst() instead of LocalDate.now(ZoneOffset.UTC), keeping the
date-range validation consistent with the rest of the KST-based logic.

In `@backend/src/main/java/moadong/club/entity/ClubApplicant.java`:
- Around line 16-19: Mongo 인덱스 자동 생성 설정이 빠져 있어 ClubApplicant의 `@CompoundIndex와`
MixpanelBackfilledEvent의 `@Indexed가` 생성되지 않을 수 있습니다. Spring Mongo 설정에서
auto-index-creation을 활성화하거나, 애플리케이션 시작 시 인덱스 생성이 보장되도록 구성하세요. 관련 위치는
ClubApplicant와 MixpanelBackfilledEvent의 인덱스 선언 및 Mongo configuration 설정입니다.

In
`@backend/src/test/java/moadong/analytics/service/MixpanelBackfillServiceTest.java`:
- Around line 23-24: The unit test class MixpanelBackfillServiceTest is missing
the required `@UnitTest` annotation per the test guidelines. Update the class
declaration alongside `@ExtendWith`(MockitoExtension.class) to include `@UnitTest`
so this test is correctly marked as a unit test, using the existing
MixpanelBackfillServiceTest symbol to locate it.

In `@docs/draft/club-admin-mixpanel-statistics-plan-2026-07-09.md`:
- Around line 567-570: 백필 허용 구간이 문서 경고로만 남아 있어 중복 집계를 막지 못하므로, backfill 입력을 처리하는
검증 로직에 cutoff를 강제하세요. Mixpanel backfill을 수행하는 함수나 설정 지점에서 서버 실시간 수집 시작 시점 이후 날짜는
거부하도록 검사하고, `MixpanelBackfilledEvent` dedup과 별개로 `$_inc` 누적 전에 차단되게 만드세요. 운영자가
실수로 라이브 집계가 이미 시작된 구간을 다시 넣지 못하도록, 허용 시작일을 명확히 계산해 validate 단계에서 실패시키세요.
- Line 27: The backfill plan relies on `clubName -> clubs.name`, which is
unstable and can misattribute or skip historical `ClubDetailPage Visited`
events. Update the plan to use a stable backfill key, such as historical
`clubId` or a dedicated name-history/alias mapping, and make the `clubs`
matching logic explicit so existing events are linked through a durable
identifier rather than the current `clubName` value. Refer to the backfill
section and the `ClubDetailPage Visited` event mapping to revise the migration
approach.
- Around line 505-507: The current plan only warns when a day’s Mixpanel Export
API response reaches requestLimit, but this can silently truncate events; update
the export handling so the logic around the daily request flow treats a
requestLimit-sized response as a failure condition. In the Mixpanel export/retry
flow, change the behavior to either split the date range and retry or explicitly
fail the job when the response is capped, and make sure the relevant
requestLimit check no longer logs warn only.

---

Nitpick comments:
In `@backend/src/main/java/moadong/analytics/entity/ClubAnalyticsDaily.java`:
- Around line 15-30: Remove the redundant `@Indexed` annotation from
ClubAnalyticsDaily.date because the existing compound index already covers date
as the leftmost field. Update the `@CompoundIndex` definition on
ClubAnalyticsDaily so the field order matches the main query pattern, placing
clubId before date. Keep the unique constraint intact while adjusting the index
declaration to support findByClubIdAndDateBetweenOrderByDateAsc more
efficiently.

In `@backend/src/main/java/moadong/analytics/entity/ClubSearchKeywordDaily.java`:
- Around line 15-27: Remove the redundant single-field index on the date
property in ClubSearchKeywordDaily, since the existing date_keyword_unique
CompoundIndex already covers date as the leftmost field. Keep the compound index
on date and normalizedKeyword unchanged, and update the entity annotations
around ClubSearchKeywordDaily so only the necessary indexing remains.

In
`@backend/src/main/java/moadong/analytics/service/ClubAnalyticsRecordService.java`:
- Around line 24-33: `recordDetailView` is doing a redundant existence check
through `incrementClubDaily`, causing an extra database round trip on every
detail view. Update `ClubAnalyticsRecordService.recordDetailView` to skip the
`clubRepository.existsById` path by calling
`incrementClubDailyWithoutExistenceCheck` directly, or make the detail-view
recording path perform only the upsert logic. Keep the existing null/blank
`clubId` guard and preserve the current exception handling/logging.

In
`@backend/src/main/java/moadong/analytics/service/ClubStatisticsAdminService.java`:
- Around line 100-103: The collection name used in ClubStatisticsAdminService is
hardcoded as a string literal, which can drift from the ClubSearchKeywordDaily
entity’s `@Document` value. Update the aggregation call to derive the collection
name from ClubSearchKeywordDaily metadata instead of embedding
"club_search_keyword_daily" directly, so the service stays aligned if the entity
mapping changes.

In
`@backend/src/main/java/moadong/analytics/service/MixpanelBackfillService.java`:
- Around line 79-84: The catch block in MixpanelBackfillService.process/backfill
flow currently calls mixpanelBackfilledEventRepository.deleteById(backfillKey)
directly, which can throw and hide the original exception e. Wrap the rollback
delete in its own try/catch so any cleanup failure is only logged, then rethrow
the original exception e unchanged; use the existing log.error and the
backfillKey/event metadata to identify the failure path.

In `@backend/src/main/java/moadong/club/service/ClubSearchService.java`:
- Line 29: Make the search keyword statistics recording asynchronous in
ClubSearchService by moving the recordSearchKeyword invocation off the request
thread. Update the ClubSearchService flow around clubAnalyticsRecordService and
recordSearchKeyword so it uses `@Async` or an event-driven async handoff, and
ensure the existing exception-swallowing behavior is preserved so search results
are not affected if analytics persistence fails.

In
`@backend/src/test/java/moadong/analytics/service/MixpanelBackfillServiceTest.java`:
- Around line 38-68: The current MixpanelBackfillServiceTest only covers the
disabled-flow and rollback-on-failure cases, so expand coverage around
MixpanelBackfillService.backfill and related event handling. Add tests for the
successful processing path, duplicate event deduplication, date-mismatch
skipping, duplicate clubName skipping, and each supported event type to verify
the pipeline behaves correctly across all branches.
🪄 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: 95146ee2-18b4-42b4-8b54-42a1638e1c5b

📥 Commits

Reviewing files that changed from the base of the PR and between cd376fb and fd86f53.

📒 Files selected for processing (30)
  • backend/src/main/java/moadong/analytics/config/MixpanelProperties.java
  • backend/src/main/java/moadong/analytics/controller/ClubStatisticsAdminController.java
  • backend/src/main/java/moadong/analytics/controller/MixpanelBackfillAdminController.java
  • backend/src/main/java/moadong/analytics/entity/ClubAnalyticsDaily.java
  • backend/src/main/java/moadong/analytics/entity/ClubSearchKeywordDaily.java
  • backend/src/main/java/moadong/analytics/entity/MixpanelBackfilledEvent.java
  • backend/src/main/java/moadong/analytics/payload/dto/MixpanelRawEvent.java
  • backend/src/main/java/moadong/analytics/payload/response/ClubStatisticsOverviewResponse.java
  • backend/src/main/java/moadong/analytics/payload/response/ClubStatisticsTrendResponse.java
  • backend/src/main/java/moadong/analytics/payload/response/MixpanelBackfillResponse.java
  • backend/src/main/java/moadong/analytics/payload/response/SearchKeywordStatisticsResponse.java
  • backend/src/main/java/moadong/analytics/repository/ClubAnalyticsDailyRepository.java
  • backend/src/main/java/moadong/analytics/repository/ClubSearchKeywordDailyRepository.java
  • backend/src/main/java/moadong/analytics/repository/MixpanelBackfilledEventRepository.java
  • backend/src/main/java/moadong/analytics/service/ClubAnalyticsRecordService.java
  • backend/src/main/java/moadong/analytics/service/ClubApplicantStatisticsService.java
  • backend/src/main/java/moadong/analytics/service/ClubStatisticsAdminService.java
  • backend/src/main/java/moadong/analytics/service/MixpanelBackfillService.java
  • backend/src/main/java/moadong/analytics/service/MixpanelExportClient.java
  • backend/src/main/java/moadong/analytics/support/AnalyticsDateRangeValidator.java
  • backend/src/main/java/moadong/analytics/support/AnalyticsTime.java
  • backend/src/main/java/moadong/club/entity/ClubApplicant.java
  • backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.java
  • backend/src/main/java/moadong/club/service/ClubProfileService.java
  • backend/src/main/java/moadong/club/service/ClubSearchService.java
  • backend/src/main/java/moadong/global/exception/ErrorCode.java
  • backend/src/main/resources/static/dev/index.html
  • backend/src/test/java/moadong/analytics/service/MixpanelBackfillServiceTest.java
  • backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java
  • docs/draft/club-admin-mixpanel-statistics-plan-2026-07-09.md

Comment on lines +107 to +109
public void validateClubManager(String clubId) {
findClub(clubId);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

validateClubManager 메서드명이 실제 동작과 불일치

메서드 이름은 "클럽 매니저 검증"을 의미하지만, 실제로는 findClub(clubId)만 호출하여 클럽 존재 여부만 확인합니다. 매니저 권한 검증은 수행하지 않습니다. ClubStatisticsAdminController Line 63에서 이 메서드를 호출하지만, 실제 인가는 @PreAuthorize("isAuthenticated()")user.getClubId()에 의존합니다.

메서드명을 validateClubExists로 변경하거나, 실제 매니저 권한 검증 로직을 추가하는 것이 좋습니다.

♻️ 제안: 메서드명을 실제 동작에 맞게 수정
-    public void validateClubManager(String clubId) {
+    public void validateClubExists(String clubId) {
         findClub(clubId);
     }
🤖 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
`@backend/src/main/java/moadong/analytics/service/ClubStatisticsAdminService.java`
around lines 107 - 109, `validateClubManager` currently only delegates to
`findClub(clubId)`, so the method name implies manager authorization that it
does not perform. Update `ClubStatisticsAdminService.validateClubManager` to
either rename it to something like `validateClubExists` so it matches the actual
behavior, or add real manager-permission checks there if that is the intended
contract. Make sure the call site in `ClubStatisticsAdminController` still uses
the correctly named/behaving validation method.

Comment on lines +22 to +30
public static void validateBackfillRange(LocalDate from, LocalDate to, int maxRangeDays) {
validateRequiredAndOrder(from, to);
if (from.plusDays(maxRangeDays - 1L).isBefore(to)) {
throw new RestApiException(ErrorCode.STATISTICS_BACKFILL_RANGE_TOO_LONG);
}
if (to.isAfter(LocalDate.now(java.time.ZoneOffset.UTC))) {
throw new RestApiException(ErrorCode.STATISTICS_DATE_RANGE_INVALID);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify if AnalyticsTime.KST is used consistently for date boundary checks across the codebase.
rg -n "LocalDate.now" backend/src/main/java/moadong/analytics/ --type java

Repository: Moadong/moadong

Length of output: 409


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n[AnalyticsTime.java]\n'
sed -n '1,120p' backend/src/main/java/moadong/analytics/support/AnalyticsTime.java

printf '\n[AnalyticsDateRangeValidator.java]\n'
sed -n '1,120p' backend/src/main/java/moadong/analytics/support/AnalyticsDateRangeValidator.java

printf '\n[MixpanelBackfillService.java]\n'
sed -n '1,220p' backend/src/main/java/moadong/analytics/service/MixpanelBackfillService.java

Repository: Moadong/moadong

Length of output: 10920


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "AnalyticsTime|eventDate|ZoneOffset.UTC|ZoneId.of\\(\"Asia/Seoul\"\\)|KST" backend/src/main/java/moadong/analytics/ backend/src/main/java/moadong -g '*.java'

Repository: Moadong/moadong

Length of output: 13412


validateBackfillRange의 상한 기준을 KST로 통일하세요 LocalDate.now(ZoneOffset.UTC) 때문에 KST 기준 오늘 날짜의 백필이 UTC 전날 구간에서 거부됩니다. AnalyticsTime.todayKst()로 맞추는 편이 일관됩니다.

🤖 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
`@backend/src/main/java/moadong/analytics/support/AnalyticsDateRangeValidator.java`
around lines 22 - 30, The upper bound check in validateBackfillRange is using
UTC, which can reject a backfill date that is still valid in KST. Update
AnalyticsDateRangeValidator.validateBackfillRange to compare against
AnalyticsTime.todayKst() instead of LocalDate.now(ZoneOffset.UTC), keeping the
date-range validation consistent with the rest of the KST-based logic.

Comment on lines +16 to +19
import org.springframework.data.mongodb.core.index.CompoundIndex;

@Document("club_applicants")
@CompoundIndex(name = "form_created_at_idx", def = "{'formId': 1, 'createdAt': 1}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# auto-index-creation 설정 확인
rg -n 'auto-index-creation' -g '*.yml' -g '*.yaml' -g '*.properties' backend/

# 프로그래밍 방식의 인덱스 생성 확인
rg -n 'autoIndexCreation|IndexResolver|IndexOperations|ensureIndex' --type java backend/src/main/

Repository: Moadong/moadong

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Mongo 관련 설정과 인덱스 생성 경로를 넓게 확인
git ls-files 'backend/**/*' | rg '(\.yml$|\.yaml$|\.properties$|\.java$|\.kt$)' 

printf '\n== auto-index related config ==\n'
rg -n --hidden --glob '!**/build/**' --glob '!**/target/**' \
  'auto-index-creation|autoIndexCreation|spring\.data\.mongodb|MongoCustomConversions|MongoTemplate|IndexResolver|IndexOperations|ensureIndex|mongoIndex' backend/

printf '\n== ClubApplicant / MixpanelBackfilledEvent ==\n'
rg -n --hidden --glob '!**/build/**' --glob '!**/target/**' \
  'ClubApplicant|MixpanelBackfilledEvent|findByFormIdInAndCreatedAtBetween|`@CompoundIndex`|`@Indexed`' backend/src/main/

Repository: Moadong/moadong

Length of output: 38777


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== MongoConfig ==\n'
if [ -f backend/src/main/java/moadong/global/config/MongoConfig.java ]; then
  wc -l backend/src/main/java/moadong/global/config/MongoConfig.java
  cat -n backend/src/main/java/moadong/global/config/MongoConfig.java
fi

printf '\n== Mongo-related application config files ==\n'
git ls-files 'backend/src/main/resources/*' | rg '(\.yml$|\.yaml$|\.properties$)'

printf '\n== auto-index references across backend ==\n'
rg -n --hidden --glob '!**/build/**' --glob '!**/target/**' \
  'auto-index-creation|autoIndexCreation|setAutoIndexCreation|MongoMappingContext|IndexResolver|IndexOperations|ensureIndex' backend/

printf '\n== ClubApplicant and MixpanelBackfilledEvent declarations ==\n'
cat -n backend/src/main/java/moadong/club/entity/ClubApplicant.java | sed -n '1,120p'
printf '\n---\n'
cat -n backend/src/main/java/moadong/analytics/entity/MixpanelBackfilledEvent.java | sed -n '1,120p'

Repository: Moadong/moadong

Length of output: 1402


Mongo 인덱스 자동 생성 설정 추가 필요
현재 코드베이스에 spring.data.mongodb.auto-index-creation 설정이 보이지 않아 ClubApplicant@CompoundIndexMixpanelBackfilledEvent@Indexed가 생성되지 않을 수 있습니다. 이 경우 findByFormIdInAndCreatedAtBetween 같은 조회가 컬렉션 스캔으로 커질 수 있으니, 자동 생성 설정을 켜거나 시작 시 인덱스를 보장하세요.

🤖 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 `@backend/src/main/java/moadong/club/entity/ClubApplicant.java` around lines 16
- 19, Mongo 인덱스 자동 생성 설정이 빠져 있어 ClubApplicant의 `@CompoundIndex와`
MixpanelBackfilledEvent의 `@Indexed가` 생성되지 않을 수 있습니다. Spring Mongo 설정에서
auto-index-creation을 활성화하거나, 애플리케이션 시작 시 인덱스 생성이 보장되도록 구성하세요. 관련 위치는
ClubApplicant와 MixpanelBackfilledEvent의 인덱스 선언 및 Mongo configuration 설정입니다.

Source: Coding guidelines

Comment on lines +23 to +24
@ExtendWith(MockitoExtension.class)
class MixpanelBackfillServiceTest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

@UnitTest 애노테이션 누락

코딩 가이드라인에 따르면 backend/**/src/test/java/**/*Test.java 경로의 단위 테스트에는 @UnitTest 애노테이션을 추가해야 합니다. @ExtendWith(MockitoExtension.class)만 있고 @UnitTest가 없습니다.

As per coding guidelines: backend/**/src/test/java/**/*Test.java: Annotate unit tests with @UnitTest and integration tests with @IntegrationTest

💚 제안: `@UnitTest` 추가
+import moadong.global.annotation.UnitTest;
+
+@UnitTest
 `@ExtendWith`(MockitoExtension.class)
 class MixpanelBackfillServiceTest {
📝 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
@ExtendWith(MockitoExtension.class)
class MixpanelBackfillServiceTest {
import moadong.global.annotation.UnitTest;
`@UnitTest`
`@ExtendWith`(MockitoExtension.class)
class MixpanelBackfillServiceTest {
🤖 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
`@backend/src/test/java/moadong/analytics/service/MixpanelBackfillServiceTest.java`
around lines 23 - 24, The unit test class MixpanelBackfillServiceTest is missing
the required `@UnitTest` annotation per the test guidelines. Update the class
declaration alongside `@ExtendWith`(MockitoExtension.class) to include `@UnitTest`
so this test is correctly marked as a unit test, using the existing
MixpanelBackfillServiceTest symbol to locate it.

Source: Coding guidelines

카드 노출수는 1차 구현 범위에서 제외한다. 노출 이벤트는 발생 빈도가 높고 프론트/네트워크 부담이 크며, 이번 관리자 통계의 필수 지표가 아니라고 판단한다.
카드 클릭수도 1차 구현 범위에서 제외한다. 인증 없는 행동 수집 API를 만들지 않기로 했으므로, 신규 카드 클릭을 서버가 정확하게 관측할 수 없다. 상세 페이지 조회수로 실제 관심도를 판단한다.

`ClubDetailPage Visited` Mixpanel 과거 이벤트에는 현재 `club_id`가 없으므로, backfill 시에는 `clubName`으로 기존 `clubs.name`에 매핑한다. backfill 이후 신규 상세 조회는 기존 백엔드 조회 흐름에서 확인된 `clubId`로 기록한다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files first.
for f in \
  docs/draft/club-admin-mixpanel-statistics-plan-2026-07-09.md \
  backend/src/main/java/moadong/analytics/controller/ClubStatisticsAdminController.java \
  backend/src/main/java/moadong/analytics/controller/MixpanelBackfillAdminController.java \
  backend/src/main/java/moadong/analytics/service/MixpanelBackfillService.java
do
  echo "### $f"
  wc -l "$f"
done

echo
echo "### Outline"
ast-grep outline docs/draft/club-admin-mixpanel-statistics-plan-2026-07-09.md --view expanded || true

echo
echo "### Relevant snippets"
sed -n '1,220p' docs/draft/club-admin-mixpanel-statistics-plan-2026-07-09.md

echo
sed -n '1,220p' backend/src/main/java/moadong/analytics/controller/ClubStatisticsAdminController.java

echo
sed -n '1,220p' backend/src/main/java/moadong/analytics/controller/MixpanelBackfillAdminController.java

echo
sed -n '1,260p' backend/src/main/java/moadong/analytics/service/MixpanelBackfillService.java

Repository: Moadong/moadong

Length of output: 19435


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate all references to clubName/clubId in the analytics backfill flow.
rg -n --hidden --glob '!**/build/**' --glob '!**/dist/**' \
  'clubName|clubId|MixpanelBackfill|ClubDetailPage Visited|backfill|line limit|export' \
  docs backend/src/main/java/moadong/analytics

Repository: Moadong/moadong

Length of output: 25979


클럽명 기반 백필 키는 불안정합니다. clubName -> clubs.name 매핑은 이름 변경/재사용 시 과거 이벤트를 다른 동아리로 잘못 귀속시키거나 아예 건너뛰게 만듭니다. 백필용 안정 키(과거 clubId 포함, 이름 이력/alias 테이블 등)를 두지 않으면 관리자 통계 정합성이 깨집니다.

🤖 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 `@docs/draft/club-admin-mixpanel-statistics-plan-2026-07-09.md` at line 27, The
backfill plan relies on `clubName -> clubs.name`, which is unstable and can
misattribute or skip historical `ClubDetailPage Visited` events. Update the plan
to use a stable backfill key, such as historical `clubId` or a dedicated
name-history/alias mapping, and make the `clubs` matching logic explicit so
existing events are linked through a durable identifier rather than the current
`clubName` value. Refer to the backfill section and the `ClubDetailPage Visited`
event mapping to revise the migration approach.

Comment on lines +505 to +507
- 날짜별 1 request로 호출한다.
- 응답 line 수가 `requestLimit`과 같으면 warn 로그를 남긴다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find the referenced document and inspect the relevant lines
git ls-files 'docs/draft/club-admin-mixpanel-statistics-plan-2026-07-09.md'
wc -l docs/draft/club-admin-mixpanel-statistics-plan-2026-07-09.md
sed -n '490,520p' docs/draft/club-admin-mixpanel-statistics-plan-2026-07-09.md

# 2) Search for requestLimit and related handling in the repo
rg -n --hidden --no-ignore-vcs 'requestLimit|request limit|100000|100k|warn log|warn.*requestLimit|split retry|backfill.*fail|failure' .

# 3) Map likely implementation files if found
fd -i 'mixpanel|club-admin|backfill|statistics' .

Repository: Moadong/moadong

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Mixpanel export client
ast-grep outline backend/src/main/java/moadong/analytics/service/MixpanelExportClient.java --view expanded
sed -n '1,220p' backend/src/main/java/moadong/analytics/service/MixpanelExportClient.java

# Inspect the backfill service
ast-grep outline backend/src/main/java/moadong/analytics/service/MixpanelBackfillService.java --view expanded
sed -n '1,260p' backend/src/main/java/moadong/analytics/service/MixpanelBackfillService.java

# Inspect the properties/config used for requestLimit
ast-grep outline backend/src/main/java/moadong/analytics/config/MixpanelProperties.java --view expanded
sed -n '1,160p' backend/src/main/java/moadong/analytics/config/MixpanelProperties.java

# Inspect tests that cover the limit behavior
sed -n '1,220p' backend/src/test/java/moadong/analytics/service/MixpanelBackfillServiceTest.java
sed -n '1,220p' backend/src/test/java/moadong/analytics/service/MixpanelExportClientTest.java

Repository: Moadong/moadong

Length of output: 19861


🌐 Web query:

Mixpanel export API limit parameter returns maximum number of events and truncation behavior

💡 Result:

In the Mixpanel Raw Data Export API, the limit parameter is used to cap the maximum number of events returned in a single API request [1][2]. The maximum allowable value for this parameter is 100,000 events per call [2][3]. Regarding truncation behavior, the API processes data in chronological order, starting from the specified from_date [4]. If the number of events matching your criteria within your requested time range (or even a single day) exceeds the limit you have set, or exceeds the hard cap of 100,000 events, the API will stop returning data as soon as that limit is reached [4][5]. This truncation can cause the response to end before reaching the specified to_date, which may appear as if the date range was ignored [4]. To export larger datasets, it is necessary to batch your requests by using narrower date ranges or hourly time slices to ensure you remain within the 100,000-event limit per call [4].

Citations:


100k 도달 시 잘림을 실패로 처리하세요.

requestLimit에 닿으면 Mixpanel Export API가 응답을 더 이상 채우지 못하고 잘릴 수 있습니다. warn만 남기면 해당 일자의 일부 이벤트가 조용히 누락되므로, 이 경우는 분할 재시도하거나 작업을 실패로 전환해야 합니다.

🤖 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 `@docs/draft/club-admin-mixpanel-statistics-plan-2026-07-09.md` around lines
505 - 507, The current plan only warns when a day’s Mixpanel Export API response
reaches requestLimit, but this can silently truncate events; update the export
handling so the logic around the daily request flow treats a requestLimit-sized
response as a failure condition. In the Mixpanel export/retry flow, change the
behavior to either split the date range and retry or explicitly fail the job
when the response is capped, and make sure the relevant requestLimit check no
longer logs warn only.

Comment on lines +567 to +570
실시간 집계와 backfill 모두 `$inc`를 사용한다. Backfill은 `MixpanelBackfilledEvent` dedup으로 같은 Mixpanel 이벤트를 반복 실행해도 중복 누적을 막는다.

주의: backfill 기간은 서버 자체 통계 수집을 배포하기 전 과거 기간으로 제한해야 한다. 배포 이후 실시간 수집이 이미 동작한 날짜를 Mixpanel로 다시 backfill하면, Mixpanel 이벤트와 서버 조회 이벤트가 서로 다른 원천으로 더해져 같은 행동이 중복 집계될 수 있다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

백필 허용 구간을 문서 경고가 아니라 강제 검증으로 고정해 주세요.

현재는 “배포 이후 기간을 다시 백필하면 중복 집계될 수 있다”는 주의만 있고, 이를 막는 cutoff 검증이 없습니다. 운영자가 실수로 라이브 집계가 이미 시작된 날짜를 다시 넣으면 $_inc가 그대로 누적돼 통계가 틀어집니다.

🤖 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 `@docs/draft/club-admin-mixpanel-statistics-plan-2026-07-09.md` around lines
567 - 570, 백필 허용 구간이 문서 경고로만 남아 있어 중복 집계를 막지 못하므로, backfill 입력을 처리하는 검증 로직에
cutoff를 강제하세요. Mixpanel backfill을 수행하는 함수나 설정 지점에서 서버 실시간 수집 시작 시점 이후 날짜는 거부하도록
검사하고, `MixpanelBackfilledEvent` dedup과 별개로 `$_inc` 누적 전에 차단되게 만드세요. 운영자가 실수로 라이브
집계가 이미 시작된 구간을 다시 넣지 못하도록, 허용 시작일을 명확히 계산해 validate 단계에서 실패시키세요.

@Zepelown
Zepelown requested a review from seongwon030 July 11, 2026 09:19
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