fix: 웹사이트 동아리 import 품질 검증 및 통계 캐시 추가#667
Conversation
📝 WalkthroughWalkthroughWebsiteClubStatsReader를 도입해 대학·쿼리별 클럽 수를 Caffeine으로 캐시하고, WebsiteService 호출을 해당 리더로 전환했습니다. AdminWebsiteClubSheetImportService에 의심 텍스트 검증을 추가해 preview에서 비활성화하고 confirm에서 경고가 있는 행은 저장을 건너뛰며, 저장이 이뤄진 경우 커밋 이후 대학 통계 캐시를 무효화합니다. 변경 사항Website Club Statistics Caching
Club Sheet Import Content Validation
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
🧪 JaCoCo Coverage Report (Changed Files)Summary
Coverage by File
|
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/java/gg/agit/konect/admin/website/service/AdminWebsiteClubSheetImportService.java`:
- Around line 265-279: The isSuspiciousName method currently combines many
business rules (HEADER_NAME, contains "한 줄 소개"/"상세소개"/"동아리입니다",
endsWith("입니다.")/endsWith("합니다."), URL_OR_SNS_PATTERN, PHONE_NUMBER_PATTERN,
isPlaceholder) which is hard to read; add short inline comments above or beside
each predicate in isSuspiciousName explaining the intent of that specific check
(e.g., that HEADER_NAME detects header rows, "한 줄 소개"/"상세소개" detect literal
intro labels, "동아리입니다"/endsWith("입니다.")/endsWith("합니다.") catch generic
descriptive sentences, URL_OR_SNS_PATTERN detects links/handles,
PHONE_NUMBER_PATTERN detects phone numbers, isPlaceholder catches filler values)
and keep references to the existing symbols (HEADER_NAME, URL_OR_SNS_PATTERN,
PHONE_NUMBER_PATTERN, isPlaceholder) so future readers understand why each
condition flags a name.
- Around line 162-164: The cache invalidation using
websiteClubStatsReader.invalidateUniversity(universityId) inside the
`@Transactional` method in AdminWebsiteClubSheetImportService must be moved to run
after commit to avoid stale state on rollback; emit a domain event (e.g.,
ClubImportedEvent with universityId) after saveAll and remove the direct
invalidateUniversity call, then add a listener component
(ClubImportEventListener) with a `@TransactionalEventListener`(phase =
TransactionPhase.AFTER_COMMIT) method that calls
websiteClubStatsReader.invalidateUniversity(event.universityId()) so cache
invalidation only occurs after the transaction successfully commits.
In
`@src/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.java`:
- Around line 44-49: Add a concise comment above the normalizeQuery method
explaining the normalization rules and rationale: state that null or blank
inputs intentionally return null to indicate absence (used when building cache
keys), that trim() removes surrounding whitespace, and that
toLowerCase(Locale.ROOT) is used to ensure locale-independent, stable
lower-casing for cache key consistency; mention any business rule combining the
two conditions (null/blank -> null) so future maintainers understand why both
checks exist.
- Around line 18-19: The two unbounded maps universityClubCountCache and
categoryCountCache in WebsiteClubStatsReader risk memory leaks; replace them
with bounded Caffeine caches (e.g., com.github.benmanes.caffeine.cache.Cache)
such as Cache<Integer, Long> universityClubCountCache and
Cache<CategoryCountCacheKey, Map<ClubCategory, Long>> categoryCountCache
configured with maxSize and expireAfterWrite (TTL), and update all usages
(reads/writes) of universityClubCountCache and categoryCountCache to use
cache.getIfPresent/get or cache.get(key, mappingFunction) and
cache.put/putIfAbsent equivalents so eviction/TTL is enforced. Ensure the new
caches are constructed where the current fields are initialized (in
WebsiteClubStatsReader) and keep the same key types (Integer and
CategoryCountCacheKey) and value types to minimize call-site changes.
- Around line 39-42: The invalidateUniversity call is executed before
transaction commit which can let other threads rebuild stale caches; modify
AdminWebsiteClubSheetImportService.confirmImport to invoke
WebsiteClubStatsReader.invalidateUniversity only after the transaction commits
by registering a callback via
TransactionSynchronizationManager.registerSynchronization (afterCommit) or by
publishing an event and handling it with `@TransactionalEventListener`(phase =
AFTER_COMMIT) in WebsiteClubStatsReader; update WebsiteClubStatsReader to expose
a method that performs the same cache removals when invoked from the
after-commit callback, and add an integration test that runs concurrent read
requests during import to verify cache is invalidated only after commit.
In
`@src/test/java/gg/agit/konect/unit/admin/website/service/AdminWebsiteClubSheetImportServiceTest.java`:
- Around line 57-68: Add verification that the statistics cache invalidation is
triggered when import is confirmed: in AdminWebsiteClubSheetImportServiceTest,
update the existing test confirmImportSavesEnabledAndNonDuplicateClubsOnly (or
add a new test exercising service.confirmImport(...)) to assert that
websiteClubStatsReader.invalidateUniversity(UNIVERSITY_ID) is invoked by calling
Mockito.verify(websiteClubStatsReader).invalidateUniversity(UNIVERSITY_ID) after
the confirmImport call; ensure the test arranges any required stubs for
googleSheetsService/webUniversityRepository/webClubRepository so the
confirmImport path executes.
In
`@src/test/java/gg/agit/konect/unit/domain/website/service/WebsiteClubStatsReaderTest.java`:
- Around line 58-68: Add a concurrency test that verifies
WebsiteClubStatsReader.invalidateUniversity (which uses removeIf internally) is
safe under concurrent reads: create a test method (e.g.,
invalidateUniversityIsSafeUnderConcurrentAccess) that stubs
websiteQueryRepository.countClubsByUniversityId and countClubCategories, starts
an ExecutorService with multiple threads where half repeatedly call
getCategoryCounts (or getUniversityClubCount) and the other half repeatedly call
invalidateUniversity(UNIVERSITY_ID), collect Futures and assert that
future.get(...) does not throw any exception, then shutdown the executor; this
ensures concurrent calls to getCategoryCounts/getUniversityClubCount and
invalidateUniversity do not raise race conditions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d78496a4-2aa3-43a6-a4a6-d6e927491c37
📒 Files selected for processing (5)
src/main/java/gg/agit/konect/admin/website/service/AdminWebsiteClubSheetImportService.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteService.javasrc/test/java/gg/agit/konect/unit/admin/website/service/AdminWebsiteClubSheetImportServiceTest.javasrc/test/java/gg/agit/konect/unit/domain/website/service/WebsiteClubStatsReaderTest.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Analyze (java-kotlin)
- GitHub Check: coverage
🧰 Additional context used
📓 Path-based instructions (4)
**/*.java
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.java: Java 코드에서 import로 해결할 수 있는 경우 FQCN(Full Qualified Class Name)을 사용하지 않도록 지적한다
JPA/QueryDSL 조회 변경 시 N+1, 잘못된 fetch join, count 쿼리 왜곡, pagination 깨짐, distinct 누락을 확인한다
권한 로직은 관리자 우회, 요청자와 대상자 관계, 클럽/채팅방/공지/일정의 소속 검증이 빠지지 않았는지 확인한다
soft delete, 탈퇴 사용자, 차단/제외 조건, 중복 제거가 필요한 조회에서는 응답에 노출되면 안 되는 데이터가 포함되는지 확인한다
DTO 응답 변경은 기존 클라이언트가 기대하는 필드명, nullability, enum/string 값, 정렬 순서를 깨지 않는지 확인한다
조건이 2개 이상 결합된 비즈니스 규칙, 권한 조건, soft delete 제외, 중복 제거, fallback 우선순위, 대표값 선택, DTO 변환, count 쿼리 분리, fetch join 선택 이유처럼 코드만으로 의도가 숨겨지는 지점에는 주석을 권장한다
단순 생성자 호출, 필드 매핑, 컬렉션 반환, 이름만으로 명확한 분기에는 주석을 요구하지 않는다
Files:
src/test/java/gg/agit/konect/unit/domain/website/service/WebsiteClubStatsReaderTest.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteService.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.javasrc/test/java/gg/agit/konect/unit/admin/website/service/AdminWebsiteClubSheetImportServiceTest.javasrc/main/java/gg/agit/konect/admin/website/service/AdminWebsiteClubSheetImportService.java
**/*.{sql,java}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
데이터베이스 변경에서는 마이그레이션 순서, 기존 데이터 호환성, nullable/default 처리, 롤백 난이도, 인덱스 필요성을 확인한다
Files:
src/test/java/gg/agit/konect/unit/domain/website/service/WebsiteClubStatsReaderTest.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteService.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.javasrc/test/java/gg/agit/konect/unit/admin/website/service/AdminWebsiteClubSheetImportServiceTest.javasrc/main/java/gg/agit/konect/admin/website/service/AdminWebsiteClubSheetImportService.java
**/*
⚙️ CodeRabbit configuration file
**/*: 공통 리뷰 톤 가이드:
- 모든 코멘트는 첫 줄에
[LEVEL: ...]태그를 포함한다.- 과장된 표현 없이 사실 기반으로 작성한다.
- 한 코멘트에는 하나의 이슈만 다룬다.
- 코드 예시가 필요하면 최소 수정 예시를 제시한다.
- 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.
Files:
src/test/java/gg/agit/konect/unit/domain/website/service/WebsiteClubStatsReaderTest.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteService.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.javasrc/test/java/gg/agit/konect/unit/admin/website/service/AdminWebsiteClubSheetImportServiceTest.javasrc/main/java/gg/agit/konect/admin/website/service/AdminWebsiteClubSheetImportService.java
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.
- 코멘트는 반드시 한국어로 작성한다.
- 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
- 각 코멘트 첫 줄에 심각도를
[LEVEL: high|medium|low]형식으로 반드시 표기한다.- 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
- 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
- 가능하면 재현 조건 및 실패 시나리오도 포함한다.
- 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
- 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
- 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
- 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
- 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.
Files:
src/main/java/gg/agit/konect/domain/website/service/WebsiteService.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.javasrc/main/java/gg/agit/konect/admin/website/service/AdminWebsiteClubSheetImportService.java
🔇 Additional comments (1)
src/main/java/gg/agit/konect/domain/website/service/WebsiteService.java (1)
37-37: LGTM!Also applies to: 75-76, 83-83
|
리뷰 반영했습니다.
검증: 커밋: |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.java (1)
41-49:⚠️ Potential issue | 🟠 Major | ⚡ Quick win[LEVEL: medium]
캐시 키 정규화와 실제 조회 정규화 기준이 다릅니다.
문제: Line 42와 Line 62는
Locale.ROOT로 캐시 키를 만들지만, 실제 조회 조건은src/main/java/gg/agit/konect/domain/website/repository/WebsiteQueryRepository.javaLine 216-224에서 기본 로케일toLowerCase()를 사용합니다. 영향: JVM 기본 로케일이 Turkish 같은 환경이면 같은 검색어가 서로 다른 정규화 결과로 처리되어 카테고리 수 캐시가 잘못 재사용될 수 있습니다. 제안: 캐시 키와 DB 조회가 동일한 정규화 함수를 공유하도록 한 곳으로 모으거나, 저장소 쪽도Locale.ROOT기준으로 맞추세요.Also applies to: 57-62
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.java` around lines 41 - 49, The cache key normalization and the DB query normalization diverge (cache uses normalizeQuery/CategoryCountCacheKey with Locale.ROOT while WebsiteQueryRepository.countClubCategories uses String.toLowerCase() with default locale), causing cache misses or wrong reuse under locales like Turkish; fix by centralizing the normalization so both sides use the same Locale.ROOT-based normalization: either update WebsiteQueryRepository.countClubCategories (and any other repository query methods referenced by getCategoryCounts and the similar block at lines 57-62) to call the same normalizeQuery(Locale.ROOT) helper, or move normalization into a shared utility used by both CategoryCountCacheKey/normalizeQuery and WebsiteQueryRepository.countClubCategories so all code paths use the identical Locale.ROOT lowercasing.src/test/java/gg/agit/konect/unit/domain/website/service/WebsiteClubStatsReaderTest.java (1)
67-76:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win[LEVEL: low]
카테고리 캐시 무효화 회귀 테스트가 빠져 있습니다.
문제: Line 68-76은
invalidateUniversity이후 대학 총계 캐시만 다시 조회하고, 실제로 같이 변경된categoryCountCache무효화 경로는 검증하지 않습니다. 영향: 총계 테스트가 통과해도 카테고리 수가 stale 하게 남는 회귀를 이번 테스트 세트가 잡지 못합니다. 제안:getCategoryCounts를 한 번 채운 뒤invalidateUniversity후 다른 repository 응답이 다시 반영되는지 별도 테스트를 추가하세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/java/gg/agit/konect/unit/domain/website/service/WebsiteClubStatsReaderTest.java` around lines 67 - 76, Add a regression test that ensures invalidateUniversity invalidates the category cache as well: using websiteClubStatsReader and mocked websiteQueryRepository, first stub countClubsByUniversityId and countClubsByUniversityIdByCategory (or the method used by getCategoryCounts) to return initial values and call getCategoryCounts to populate categoryCountCache, then change the mock to return updated category counts, call websiteClubStatsReader.invalidateUniversity(UNIVERSITY_ID), call getCategoryCounts again and assert it returns the updated values, and verify the repository category-count method was invoked again (times(2)) to confirm the cache was cleared.
🤖 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.
Outside diff comments:
In
`@src/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.java`:
- Around line 41-49: The cache key normalization and the DB query normalization
diverge (cache uses normalizeQuery/CategoryCountCacheKey with Locale.ROOT while
WebsiteQueryRepository.countClubCategories uses String.toLowerCase() with
default locale), causing cache misses or wrong reuse under locales like Turkish;
fix by centralizing the normalization so both sides use the same
Locale.ROOT-based normalization: either update
WebsiteQueryRepository.countClubCategories (and any other repository query
methods referenced by getCategoryCounts and the similar block at lines 57-62) to
call the same normalizeQuery(Locale.ROOT) helper, or move normalization into a
shared utility used by both CategoryCountCacheKey/normalizeQuery and
WebsiteQueryRepository.countClubCategories so all code paths use the identical
Locale.ROOT lowercasing.
In
`@src/test/java/gg/agit/konect/unit/domain/website/service/WebsiteClubStatsReaderTest.java`:
- Around line 67-76: Add a regression test that ensures invalidateUniversity
invalidates the category cache as well: using websiteClubStatsReader and mocked
websiteQueryRepository, first stub countClubsByUniversityId and
countClubsByUniversityIdByCategory (or the method used by getCategoryCounts) to
return initial values and call getCategoryCounts to populate categoryCountCache,
then change the mock to return updated category counts, call
websiteClubStatsReader.invalidateUniversity(UNIVERSITY_ID), call
getCategoryCounts again and assert it returns the updated values, and verify the
repository category-count method was invoked again (times(2)) to confirm the
cache was cleared.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2d00d3a8-1921-4c52-80a0-10c2f9131526
📒 Files selected for processing (5)
build.gradlesrc/main/java/gg/agit/konect/admin/website/service/AdminWebsiteClubSheetImportService.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.javasrc/test/java/gg/agit/konect/unit/admin/website/service/AdminWebsiteClubSheetImportServiceTest.javasrc/test/java/gg/agit/konect/unit/domain/website/service/WebsiteClubStatsReaderTest.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: coverage
- GitHub Check: Analyze (java-kotlin)
🧰 Additional context used
📓 Path-based instructions (4)
**/*
⚙️ CodeRabbit configuration file
**/*: 공통 리뷰 톤 가이드:
- 모든 코멘트는 첫 줄에
[LEVEL: ...]태그를 포함한다.- 과장된 표현 없이 사실 기반으로 작성한다.
- 한 코멘트에는 하나의 이슈만 다룬다.
- 코드 예시가 필요하면 최소 수정 예시를 제시한다.
- 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.
Files:
build.gradlesrc/test/java/gg/agit/konect/unit/domain/website/service/WebsiteClubStatsReaderTest.javasrc/test/java/gg/agit/konect/unit/admin/website/service/AdminWebsiteClubSheetImportServiceTest.javasrc/main/java/gg/agit/konect/admin/website/service/AdminWebsiteClubSheetImportService.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.java
**/*.java
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.java: Java 코드에서 import로 해결할 수 있는 경우 FQCN(Full Qualified Class Name)을 사용하지 않도록 지적한다
JPA/QueryDSL 조회 변경 시 N+1, 잘못된 fetch join, count 쿼리 왜곡, pagination 깨짐, distinct 누락을 확인한다
권한 로직은 관리자 우회, 요청자와 대상자 관계, 클럽/채팅방/공지/일정의 소속 검증이 빠지지 않았는지 확인한다
soft delete, 탈퇴 사용자, 차단/제외 조건, 중복 제거가 필요한 조회에서는 응답에 노출되면 안 되는 데이터가 포함되는지 확인한다
DTO 응답 변경은 기존 클라이언트가 기대하는 필드명, nullability, enum/string 값, 정렬 순서를 깨지 않는지 확인한다
조건이 2개 이상 결합된 비즈니스 규칙, 권한 조건, soft delete 제외, 중복 제거, fallback 우선순위, 대표값 선택, DTO 변환, count 쿼리 분리, fetch join 선택 이유처럼 코드만으로 의도가 숨겨지는 지점에는 주석을 권장한다
단순 생성자 호출, 필드 매핑, 컬렉션 반환, 이름만으로 명확한 분기에는 주석을 요구하지 않는다
Files:
src/test/java/gg/agit/konect/unit/domain/website/service/WebsiteClubStatsReaderTest.javasrc/test/java/gg/agit/konect/unit/admin/website/service/AdminWebsiteClubSheetImportServiceTest.javasrc/main/java/gg/agit/konect/admin/website/service/AdminWebsiteClubSheetImportService.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.java
**/*.{sql,java}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
데이터베이스 변경에서는 마이그레이션 순서, 기존 데이터 호환성, nullable/default 처리, 롤백 난이도, 인덱스 필요성을 확인한다
Files:
src/test/java/gg/agit/konect/unit/domain/website/service/WebsiteClubStatsReaderTest.javasrc/test/java/gg/agit/konect/unit/admin/website/service/AdminWebsiteClubSheetImportServiceTest.javasrc/main/java/gg/agit/konect/admin/website/service/AdminWebsiteClubSheetImportService.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.java
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.
- 코멘트는 반드시 한국어로 작성한다.
- 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
- 각 코멘트 첫 줄에 심각도를
[LEVEL: high|medium|low]형식으로 반드시 표기한다.- 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
- 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
- 가능하면 재현 조건 및 실패 시나리오도 포함한다.
- 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
- 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
- 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
- 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
- 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.
Files:
src/main/java/gg/agit/konect/admin/website/service/AdminWebsiteClubSheetImportService.javasrc/main/java/gg/agit/konect/domain/website/service/WebsiteClubStatsReader.java
🔍 개요
🚀 주요 변경 내용
WebsiteClubStatsReader를 추가해 대학 전체 동아리 수와 검색어별 카테고리 카운트를 읽기 전용 캐시로 조회합니다.WebsiteClubStatsReader를 사용하도록 변경했습니다.💬 참고 사항
✅ Checklist (완료 조건)