Skip to content

Comments

[Release] BE v1.0.8 배포#766

Merged
Zepelown merged 29 commits intomainfrom
develop/be
Sep 26, 2025
Merged

[Release] BE v1.0.8 배포#766
Zepelown merged 29 commits intomainfrom
develop/be

Conversation

@Zepelown
Copy link
Member

@Zepelown Zepelown commented Sep 26, 2025

📝작업 내용

중점적으로 리뷰받고 싶은 부분(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • New Features
    • 클럽 검색 결과 정렬 개선: 모집상태 우선 + 카테고리 무작위 우선순위 + 이름 순으로 정렬되어 더 다양한 결과를 제공합니다.
  • Refactor
    • 클럽 지표 관련 API가 제거되었습니다(일/주/월 활성 사용자, 랭킹, DAU 등). 해당 엔드포인트를 사용 중이었다면 호출을 중단해 주세요.
  • Chores
    • Google Drive 및 관련 스토리지 의존성/구성이 정리되었습니다.
    • 전반적인 코드/임포트 정리 및 테스트 유틸 주석/메타데이터 업데이트.

alsdddk and others added 29 commits September 13, 2025 23:23
- semesterYear : semester에 사용될 연도
- semesterTerm : 1학기, 여름계절학기, 2학기, 겨울계절학기
현재 날짜 기준으로 default로 3개 조회
ex) 2025-SECOND, 2025-WINTER, 2026-FIRST
- semesterOption을 semesterOptionResponse로
1. 생성 : service에서 getClubQuestion 사용을 제거하여 한 클럽 내 다중 지원서 생성을 허용하고, semester 값 반영
2. 수정  : clubQuestionId를 경로변수로 받아서 특정 지원서만 수정되도록 변경
기존 코드에 있던
클럽 지원 시, questionId 빌드를 임시로 clcubId로 해둔 거 같은데, 이를 수정함
학기 별로 분류, 최신순 정렬
- aggregation 사용 x
- 성능비교를 위해 둘 다 구현
- 파라미터 값 agg/sever 로 구분
[refactor] MOA-228 백엔드 비즈니스 로직 리팩토링
- 지원서 양식(괸리자) : ClubApplicationForm
- 지원자 또는 지원서(사용자)  : ClubApplicant
지원서 양식 생성에서 허용 범위가 아닌 학기 요청 검증
GET /semesters 로그인 인증 구현 및 isExist 활성화
…-selection-MOA-231

[feature] 동아리 지원서 폼 제작 시에 학기를 선택할 수 있고 생성된 모든 지원서 폼을 학기별로 분류하여 조회할 수 있다
…on-semester-selection-MOA-231

Revert "[feature] 동아리 지원서 폼 제작 시에 학기를 선택할 수 있고 생성된 모든 지원서 폼을 학기별로 분류하여 조회할 수 있다"
[feature] 동아리 검색 결과에서 분과 순서가 고정된 것에서 분과 순서를 랜덤으로 바꾼다
@Zepelown Zepelown requested a review from lepitaaar September 26, 2025 14:26
@vercel
Copy link

vercel bot commented Sep 26, 2025

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

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Sep 26, 2025 2:26pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 26, 2025

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.
  • 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 Drive/GCS 기반 이미지 서비스와 설정, Gradle 의존성이 제거되었습니다. ClubSearchService에 카테고리 무작위 우선순위를 포함한 정렬 로직이 추가되었습니다. 다수 파일의 임포트/포맷 정리가 이루어졌습니다.

Changes

Cohort / File(s) Summary
Club metrics 제거 및 참조 정리
backend/src/main/java/moadong/club/controller/ClubMetricController.java, backend/src/main/java/moadong/club/service/ClubMetricService.java, backend/src/main/java/moadong/club/entity/ClubMetric.java, backend/src/main/java/moadong/club/repository/ClubMetricRepository.java, backend/src/main/java/moadong/club/controller/ClubProfileController.java, backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java, backend/src/test/java/moadong/club/fixture/MetricFixture.java
메트릭스 컨트롤러/서비스/엔티티/리포지토리 및 테스트/픽스처 삭제. ClubProfileController에서 ClubMetricService 연동 제거 및 메서드 시그니처 단순화.
미디어 스택(GDrive/GCS) 제거 및 빌드/보안 설정 정리
backend/src/main/java/moadong/media/service/GcsClubImageService.java, .../media/service/GoogleDriveClubImageService.java, .../media/util/GoogleDriveConfig.java, backend/build.gradle, backend/src/main/java/moadong/global/config/SecurityConfig.java, backend/src/test/java/moadong/media/service/CloudflareClubImageServiceLogoTest.java
GCS/Google Drive 이미지 서비스 구현 및 설정 제거. Gradle에서 Google Drive 관련 의존성 삭제. SecurityConfig 불필요 임포트 정리. 관련 테스트 임포트 정리.
검색 로직 변경
backend/src/main/java/moadong/club/service/ClubSearchService.java, backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java
모집상태 우선 정렬에 더해 카테고리 무작위 우선순위(셔플)와 이름을 보조 키로 사용하는 3단계 정렬 도입. 일부 테스트 비활성화(주석 처리).
컨트롤러/서비스 임포트 정리
backend/src/main/java/moadong/club/controller/ClubApplyController.java, .../club/controller/ClubSearchController.java, backend/src/main/java/moadong/user/controller/UserController.java, backend/src/main/java/moadong/club/service/ClubApplyService.java
와일드카드 임포트를 명시 임포트로 교체 및 불필요 임포트 제거. 런타임 동작 변화 없음.
엔티티/DTO/리포지토리 정리
backend/src/main/java/moadong/club/entity/*(Club, ClubApplication, ClubApplicationQuestion, ClubQuestion), backend/src/main/java/moadong/club/payload/dto/*(ClubApplicantsResult, ClubDetailedResult, ClubSearchResult), backend/src/main/java/moadong/club/payload/request/*(ClubApplicantDeleteRequest, ClubApplicationCreateRequest, ClubApplicationEditRequest, ClubApplyQuestion, ClubApplyRequest, ClubCreateRequest) , backend/src/main/java/moadong/club/repository/*(ClubApplicationRepository, ClubQuestionRepository, ClubRepository, ClubSearchRepository)
임포트 정리 및 중복 제거. ClubCreateRequest 레코드 삭제. 리포지토리 임포트 정리 및 와일드카드 해제.
글로벌 어노테이션/밸리데이터 정리
backend/src/main/java/moadong/global/annotation/*(Korean, Password, PhoneNumber, UserId), backend/src/main/java/moadong/global/validator/*(KoreanValidator, PasswordValidator, PhoneNumberValidator, UserIdValidator), backend/src/main/java/moadong/global/exception/ErrorCode.java, backend/src/main/java/moadong/global/util/AESCipher.java
임포트 재배치/정리. 기능 변화 없음.
유저 영역 포맷/임포트 정리
backend/src/main/java/moadong/user/entity/*(RefreshToken, UserInformation), backend/src/main/java/moadong/user/payload/request/*(UserRegisterRequest, UserUpdateRequest), backend/src/main/java/moadong/user/payload/response/*(FindUserClubResponse, LoginResponse, RefreshResponse, TempPasswordResponse)
불필요 임포트 제거 및 레코드 선언 포맷 단일 행으로 정리. 동작 변화 없음.
테스트 인프라 및 기타 테스트 변경
backend/src/test/java/moadong/util/annotations/*(IntegrationTest, UnitTest), backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java, backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java, backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java, backend/src/test/java/moadong/unit/user/*(UserLoginTest, UserRegisterTest), backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java
테스트 어노테이션에 @SpringBootTest/@tag, @ExtendWith(MockitoExtension) 추가. 일부 테스트 임포트/포맷 정리, 사용 시나리오 단순화 및 주석 정리.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant ClubProfileController as Controller
  participant ClubProfileService as Service

  rect rgb(245,248,255)
  note over Client,Service: 기존 흐름 (삭제됨)
  Client->>Controller: GET /api/club/{clubId}
  Controller->>ClubMetricService: patch(clubId, requesterIP)
  ClubMetricService-->>Controller: void
  Controller->>Service: getClubDetail(clubId)
  Service-->>Controller: ClubDetailedResult
  Controller-->>Client: 200 OK (Response.ok)
  end

  rect rgb(242,255,245)
  note over Client,Service: 변경 후
  Client->>Controller: GET /api/club/{clubId}
  Controller->>Service: getClubDetail(clubId)
  Service-->>Controller: ClubDetailedResult
  Controller-->>Client: 200 OK (Response.ok)
  end
Loading
sequenceDiagram
  autonumber
  actor Client
  participant ClubSearchController as Controller
  participant ClubSearchService as Service
  participant Repository as ClubRepository

  Client->>Controller: GET /api/club/search?...
  Controller->>Service: search(request)
  Service->>Repository: findByCriteria(...)
  Repository-->>Service: List<Club>
  note over Service: 1) 모집상태 우선순위<br/>2) 무작위 카테고리 우선순위 생성/적용<br/>3) 이름 사전순
  Service-->>Controller: ClubSearchResponse
  Controller-->>Client: 200 OK (Response.ok)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

📈 release, 💾 BE, 📬 API, 🔨 Refactor

Suggested reviewers

  • lepitaaar
  • PororoAndFriends
  • alsdddk

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning 현재 제목 “[Release] BE v1.0.8 배포”는 배포 목적만을 나타내고 실제 변경된 주요 기능인 검색 결과 모집 분과의 랜덤 정렬이나 메트릭 및 Google Drive 관련 기능 제거 등 코드베이스 상의 핵심 작업을 반영하지 못해 제목과 변경 내용이 불일치합니다. 제목을 “검색 결과 모집 분과 순서를 랜덤으로 표시하도록 구현 및 메트릭/Google Drive 기능 제거”와 같이 주요 기능과 리팩토링 범위를 포함하도록 구체적으로 수정하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop/be

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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
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: 3

Caution

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

⚠️ Outside diff range comments (2)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)

77-79: CyclicBarrier 무한 대기 가능성 — 타임아웃 추가 필요

한 스레드라도 barrier.await() 이전에 예외로 빠지면 나머지 스레드가 영원히 대기하여 테스트가 hang될 수 있습니다. 타임아웃을 주고 TimeoutException을 처리하세요.

적용 제안:

-                    barrier.await();
+                    barrier.await(10, TimeUnit.SECONDS);

필요한 import (아래 import 블록에도 추가 제안 포함):

+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
backend/src/main/java/moadong/club/entity/ClubQuestion.java (1)

56-59: NPE 가능성: newQuestions가 null이면 addAll에서 NPE

호출자가 null을 넘기면 addAll(null)로 NPE가 발생합니다. 방어 코드 추가와 함께 editedAt 갱신까지 묶어주세요.

-    public void updateQuestions(List<ClubApplicationQuestion> newQuestions) {
-        this.questions.clear();
-        this.questions.addAll(newQuestions);
-    }
+    public void updateQuestions(List<ClubApplicationQuestion> newQuestions) {
+        this.questions.clear();
+        if (newQuestions == null || newQuestions.isEmpty()) {
+            updateEditedAt();
+            return;
+        }
+        this.questions.addAll(new ArrayList<>(newQuestions));
+        updateEditedAt();
+    }
🧹 Nitpick comments (9)
backend/src/main/java/moadong/user/payload/response/LoginResponse.java (1)

3-3: 콤마 뒤 공백 추가 제안

콤마 뒤 공백이 없어 가독성과 코드 스타일 일관성이 떨어집니다. 공백을 하나 추가하는 편이 좋겠습니다.

-public record LoginResponse(String accessToken,String clubId) {
+public record LoginResponse(String accessToken, String clubId) {
backend/src/test/java/moadong/unit/user/UserLoginTest.java (1)

3-9: JUnit의 assertTrue로 통일해주세요

이 테스트 클래스는 이미 JUnit5 Assertions를 사용하고 있는데, assertTruecom.mongodb.assertions.Assertions에 의존하고 있습니다. MongoDB 드라이버 내부 유틸을 테스트 검증에 끌어다 쓰면 향후 의존성 정리 시 불필요한 결합도가 생기고, 의도도 읽기 어렵습니다. JUnit의 assertTrue로 맞춰 주세요.

-import static com.mongodb.assertions.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (5)

87-95: 충돌 판정 정밀도 개선 및 테스트 실패 처리

현재는 모든 DataAccessException을 충돌로 집계합니다. 낙관적 락 실패만 충돌로 집계하고, 그 외 예외는 테스트 실패로 처리해야 오탐을 줄일 수 있습니다. 또한 printStackTrace() 대신 fail() 사용을 권장합니다.

적용 제안:

-                } catch (DataAccessException e) {
-                    // DataAccessException은 WriteConflict 등을 포함
-                    conflictCount.incrementAndGet();
-                } catch (InterruptedException | BrokenBarrierException e) {
-                    Thread.currentThread().interrupt();
-                    e.printStackTrace();
-                } catch (Exception e) {
-                    e.printStackTrace();
+                } catch (OptimisticLockingFailureException e) {
+                    conflictCount.incrementAndGet();
+                } catch (DataAccessException e) {
+                    fail("예상치 못한 DataAccessException: " + e.getClass().getSimpleName());
+                } catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
+                    Thread.currentThread().interrupt();
+                    fail("동기화 대기 중 예외 발생: " + e.getClass().getSimpleName());
+                } catch (Exception e) {
+                    fail("예상치 못한 예외: " + e);
                 } finally {
                     latch.countDown();
                 }

추가 import:

+import static org.junit.jupiter.api.Assertions.fail;
+import org.springframework.dao.OptimisticLockingFailureException;

101-102: 스레드 종료 보장

shutdown()awaitTermination()로 스레드 종료를 확실히 보장하세요. 타임아웃은 테스트 환경에 맞게 조정하십시오.

적용 제안:

         latch.await();
         executorService.shutdown();
+        executorService.awaitTermination(5, TimeUnit.SECONDS);

103-106: 합계 불변식 검증 추가

예상치 못한 경로(예: 예외 처리 누락)로 인해 카운트 누락을 조기 탐지하기 위해 합계가 스레드 수와 일치하는지 검증을 추가하세요.

적용 제안:

         assertEquals(1, successCount.get(), "성공한 요청은 1개여야 합니다.");
         assertEquals(numberOfThreads - 1, conflictCount.get(), "실패(충돌)한 요청은 " + (numberOfThreads - 1) + "개여야 합니다.");
+        assertEquals(numberOfThreads, successCount.get() + conflictCount.get(), "성공+실패 합은 스레드 수와 같아야 합니다.");

49-53: 테스트 데이터 전제조건 확인 필요

여기서 문서가 없으면 한 스레드가 barrier.await()에 도달하지 않아 다른 스레드가 무기한 대기할 수 있습니다. 픽스처에서 해당 ClubQuestion 생성을 보장하거나, 없을 경우 생성/skip하도록 처리해 주세요.

원하시면 픽스처/셋업 코드 보완 패치를 제안하겠습니다.


31-38: 서비스 메서드 경로로 테스트하거나, 미사용 필드 정리

현재 clubApplyService를 주입만 하고 사용하지 않습니다. 실제 운영 경로 보증을 위해 서비스 메서드를 통해 갱신을 수행하거나, 이 테스트가 저장소 레벨 검증만 의도라면 미사용 필드를 정리하세요.

backend/src/main/java/moadong/club/entity/ClubQuestion.java (2)

48-54: Setter 유효성 및 수정 시각 동기화

@NotNull 제약과 일치하도록 null 입력을 방지하고, 필드 변경 시 editedAt을 함께 갱신하는 편이 안전합니다.

     public void updateFormTitle(String title) {
-        this.title = title;
+        if (title == null) throw new IllegalArgumentException("title must not be null");
+        this.title = title;
+        updateEditedAt();
     }

     public void updateFormDescription(String description) {
-        this.description = description;
+        if (description == null) throw new IllegalArgumentException("description must not be null");
+        this.description = description;
+        updateEditedAt();
     }

3-3: MongoDB 문서 모델에는 JPA Id 대신 Spring Data Id를 사용하세요

@document 엔티티에서는 org.springframework.data.annotation.Id 사용이 권장됩니다. jakarta.persistence.Id는 혼선을 유발하고, 도구/메타데이터 처리에서도 일관성이 떨어집니다.

- import jakarta.persistence.Id;
+ import org.springframework.data.annotation.Id;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7430bd9 and 1f9f275.

📒 Files selected for processing (66)
  • backend/build.gradle (1 hunks)
  • backend/src/main/java/moadong/club/controller/ClubApplyController.java (1 hunks)
  • backend/src/main/java/moadong/club/controller/ClubMetricController.java (0 hunks)
  • backend/src/main/java/moadong/club/controller/ClubProfileController.java (1 hunks)
  • backend/src/main/java/moadong/club/controller/ClubSearchController.java (0 hunks)
  • backend/src/main/java/moadong/club/entity/Club.java (1 hunks)
  • backend/src/main/java/moadong/club/entity/ClubApplication.java (1 hunks)
  • backend/src/main/java/moadong/club/entity/ClubApplicationQuestion.java (1 hunks)
  • backend/src/main/java/moadong/club/entity/ClubMetric.java (0 hunks)
  • backend/src/main/java/moadong/club/entity/ClubQuestion.java (1 hunks)
  • backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java (1 hunks)
  • backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java (0 hunks)
  • backend/src/main/java/moadong/club/payload/dto/ClubSearchResult.java (1 hunks)
  • backend/src/main/java/moadong/club/payload/request/ClubApplicantDeleteRequest.java (0 hunks)
  • backend/src/main/java/moadong/club/payload/request/ClubApplicationCreateRequest.java (0 hunks)
  • backend/src/main/java/moadong/club/payload/request/ClubApplicationEditRequest.java (0 hunks)
  • backend/src/main/java/moadong/club/payload/request/ClubApplyQuestion.java (1 hunks)
  • backend/src/main/java/moadong/club/payload/request/ClubApplyRequest.java (0 hunks)
  • backend/src/main/java/moadong/club/payload/request/ClubCreateRequest.java (0 hunks)
  • backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java (1 hunks)
  • backend/src/main/java/moadong/club/repository/ClubMetricRepository.java (0 hunks)
  • backend/src/main/java/moadong/club/repository/ClubQuestionRepository.java (1 hunks)
  • backend/src/main/java/moadong/club/repository/ClubRepository.java (0 hunks)
  • backend/src/main/java/moadong/club/repository/ClubSearchRepository.java (1 hunks)
  • backend/src/main/java/moadong/club/service/ClubApplyService.java (1 hunks)
  • backend/src/main/java/moadong/club/service/ClubMetricService.java (0 hunks)
  • backend/src/main/java/moadong/club/service/ClubProfileService.java (1 hunks)
  • backend/src/main/java/moadong/club/service/ClubSearchService.java (3 hunks)
  • backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java (1 hunks)
  • backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java (1 hunks)
  • backend/src/main/java/moadong/global/annotation/Korean.java (1 hunks)
  • backend/src/main/java/moadong/global/annotation/Password.java (1 hunks)
  • backend/src/main/java/moadong/global/annotation/PhoneNumber.java (1 hunks)
  • backend/src/main/java/moadong/global/annotation/UserId.java (1 hunks)
  • backend/src/main/java/moadong/global/config/SecurityConfig.java (0 hunks)
  • backend/src/main/java/moadong/global/exception/ErrorCode.java (0 hunks)
  • backend/src/main/java/moadong/global/util/AESCipher.java (1 hunks)
  • backend/src/main/java/moadong/global/validator/KoreanValidator.java (1 hunks)
  • backend/src/main/java/moadong/global/validator/PasswordValidator.java (1 hunks)
  • backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java (1 hunks)
  • backend/src/main/java/moadong/global/validator/UserIdValidator.java (1 hunks)
  • backend/src/main/java/moadong/media/service/GcsClubImageService.java (0 hunks)
  • backend/src/main/java/moadong/media/service/GoogleDriveClubImageService.java (0 hunks)
  • backend/src/main/java/moadong/media/util/GoogleDriveConfig.java (0 hunks)
  • backend/src/main/java/moadong/user/controller/UserController.java (1 hunks)
  • backend/src/main/java/moadong/user/entity/RefreshToken.java (1 hunks)
  • backend/src/main/java/moadong/user/entity/UserInformation.java (0 hunks)
  • backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java (0 hunks)
  • backend/src/main/java/moadong/user/payload/request/UserUpdateRequest.java (0 hunks)
  • backend/src/main/java/moadong/user/payload/response/FindUserClubResponse.java (1 hunks)
  • backend/src/main/java/moadong/user/payload/response/LoginResponse.java (1 hunks)
  • backend/src/main/java/moadong/user/payload/response/RefreshResponse.java (1 hunks)
  • backend/src/main/java/moadong/user/payload/response/TempPasswordResponse.java (0 hunks)
  • backend/src/main/java/moadong/user/repository/UserRepository.java (1 hunks)
  • backend/src/test/java/moadong/club/fixture/MetricFixture.java (0 hunks)
  • backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1 hunks)
  • backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java (0 hunks)
  • backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java (0 hunks)
  • backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java (1 hunks)
  • backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1 hunks)
  • backend/src/test/java/moadong/media/service/CloudflareClubImageServiceLogoTest.java (0 hunks)
  • backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java (0 hunks)
  • backend/src/test/java/moadong/unit/user/UserLoginTest.java (1 hunks)
  • backend/src/test/java/moadong/unit/user/UserRegisterTest.java (1 hunks)
  • backend/src/test/java/moadong/util/annotations/IntegrationTest.java (1 hunks)
  • backend/src/test/java/moadong/util/annotations/UnitTest.java (1 hunks)
💤 Files with no reviewable changes (26)
  • backend/src/main/java/moadong/club/controller/ClubSearchController.java
  • backend/src/main/java/moadong/global/exception/ErrorCode.java
  • backend/src/main/java/moadong/club/payload/request/ClubApplicationCreateRequest.java
  • backend/src/main/java/moadong/global/config/SecurityConfig.java
  • backend/src/main/java/moadong/club/payload/request/ClubCreateRequest.java
  • backend/src/main/java/moadong/user/entity/UserInformation.java
  • backend/src/main/java/moadong/club/entity/ClubMetric.java
  • backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java
  • backend/src/main/java/moadong/club/payload/request/ClubApplicationEditRequest.java
  • backend/src/main/java/moadong/media/service/GoogleDriveClubImageService.java
  • backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java
  • backend/src/test/java/moadong/media/service/CloudflareClubImageServiceLogoTest.java
  • backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java
  • backend/src/main/java/moadong/club/service/ClubMetricService.java
  • backend/src/main/java/moadong/club/repository/ClubMetricRepository.java
  • backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java
  • backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java
  • backend/src/main/java/moadong/media/util/GoogleDriveConfig.java
  • backend/src/main/java/moadong/club/payload/request/ClubApplyRequest.java
  • backend/src/main/java/moadong/club/repository/ClubRepository.java
  • backend/src/main/java/moadong/user/payload/response/TempPasswordResponse.java
  • backend/src/main/java/moadong/media/service/GcsClubImageService.java
  • backend/src/main/java/moadong/user/payload/request/UserUpdateRequest.java
  • backend/src/main/java/moadong/club/controller/ClubMetricController.java
  • backend/src/test/java/moadong/club/fixture/MetricFixture.java
  • backend/src/main/java/moadong/club/payload/request/ClubApplicantDeleteRequest.java
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
PR: Moadong/moadong#703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.

Applied to files:

  • backend/src/main/java/moadong/club/payload/request/ClubApplyQuestion.java
  • backend/src/main/java/moadong/club/service/ClubApplyService.java
  • backend/src/main/java/moadong/club/entity/ClubQuestion.java
  • backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
  • backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java
  • backend/src/main/java/moadong/club/entity/ClubApplicationQuestion.java
  • backend/src/main/java/moadong/club/repository/ClubQuestionRepository.java
  • backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyService.java
  • backend/src/main/java/moadong/club/controller/ClubApplyController.java
  • backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java
📚 Learning: 2025-05-01T08:32:09.398Z
Learnt from: Due-IT
PR: Moadong/moadong#329
File: backend/src/main/java/moadong/club/controller/ClubSearchController.java:0-0
Timestamp: 2025-05-01T08:32:09.398Z
Learning: ClubSearchController의 searchClubsByKeyword 메서드와 ClubSearchService의 searchClubsByKeyword 메서드 사이에 파라미터 순서 일치가 중요하다. 현재 컨트롤러의 매개변수 순서는 'keyword, recruitmentStatus, category, division'인 반면, 서비스 메서드의 순서는 'keyword, recruitmentStatus, division, category'이다. 컨트롤러에서 서비스 호출 시 'keyword, recruitmentStatus, division, category' 순서로 전달하면 category와 division 값이 바뀌어 검색 결과가 잘못될 수 있다.

Applied to files:

  • backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java
📚 Learning: 2025-05-15T12:03:57.356Z
Learnt from: Zepelown
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/entity/ClubApplicationQuestion.java:32-33
Timestamp: 2025-05-15T12:03:57.356Z
Learning: 엔티티 클래스는 요청/응답 객체(DTO)에 의존해서는 안 됩니다. 계층 간 의존성 문제를 방지하기 위해 엔티티와 DTO는 분리되어야 합니다. 예를 들어, `ClubApplicationQuestion` 엔티티가 `ClubApplicationRequest.Options`와 같은 요청 객체를 직접 참조하는 대신, 엔티티 패키지 내에 `QuestionOptions`와 같은 별도의 클래스를 정의하고 사용해야 합니다. 이렇게 하면 요청 객체 변경이 데이터베이스 스키마나 엔티티 계층에 영향을 미치지 않습니다.

Applied to files:

  • backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java
🧬 Code graph analysis (9)
backend/src/main/java/moadong/global/annotation/Korean.java (1)
backend/src/main/java/moadong/global/validator/KoreanValidator.java (1)
  • KoreanValidator (8-20)
backend/src/main/java/moadong/global/annotation/PhoneNumber.java (1)
backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java (1)
  • PhoneNumberValidator (8-24)
backend/src/main/java/moadong/club/service/ClubApplyService.java (2)
backend/src/main/java/moadong/club/entity/ClubApplicationQuestion.java (1)
  • AllArgsConstructor (12-36)
backend/src/main/java/moadong/club/entity/ClubQuestionAnswer.java (1)
  • AllArgsConstructor (7-16)
backend/src/main/java/moadong/global/annotation/UserId.java (1)
backend/src/main/java/moadong/global/validator/UserIdValidator.java (1)
  • UserIdValidator (8-18)
backend/src/test/java/moadong/util/annotations/UnitTest.java (1)
backend/src/test/java/moadong/media/service/CloudflareClubImageServiceLogoTest.java (1)
  • ExtendWith (30-127)
backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java (1)
frontend/src/types/club.ts (1)
  • Club (3-12)
backend/src/main/java/moadong/club/controller/ClubProfileController.java (1)
frontend/src/apis/getClubDetail.ts (1)
  • getClubDetail (4-17)
backend/src/main/java/moadong/global/annotation/Password.java (1)
backend/src/main/java/moadong/global/validator/PasswordValidator.java (1)
  • PasswordValidator (8-20)
backend/src/test/java/moadong/util/annotations/IntegrationTest.java (1)
backend/src/test/java/moadong/MoadongApplicationTests.java (1)
  • SpringBootTest (6-13)
🔇 Additional comments (37)
backend/src/main/java/moadong/club/payload/request/ClubApplyQuestion.java (1)

9-9: 임포트 순서 정리 확인 완료

임포트 재정렬로 인한 동작 변화는 없으며 코드 스타일만 개선된 것으로 보입니다.

backend/src/main/java/moadong/global/util/AESCipher.java (1)

3-9: LGTM – 임포트 정리가 표준 순서를 잘 따릅니다.
기존 동작과 충돌 없이 가독성이 개선되었습니다.

backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java (1)

3-4: List/Optional 명시 import 적절

이미 사용 중인 ListOptional 타입을 명시적으로 import하여 가독성과 컴파일 안정성을 높였네요. 별도 이슈 없습니다.

backend/src/main/java/moadong/user/entity/RefreshToken.java (1)

3-3: 불필요한 중복 import 정리 감사합니다.

Date import를 한 번만 유지해 가독성과 유지보수성이 좋아졌습니다.

backend/src/main/java/moadong/user/repository/UserRepository.java (1)

3-3: 불필요한 중복 import 정리 감사합니다

Optional 중복 선언을 정리해 import 섹션이 깔끔해졌습니다.

backend/src/main/java/moadong/club/repository/ClubQuestionRepository.java (1)

3-3: Optional 임포트 정리 확인 완료

중복 없이 한 번만 Optional을 임포트하도록 정리된 것 확인했습니다.

backend/src/main/java/moadong/club/entity/ClubApplicationQuestion.java (1)

6-6: 불필요한 중복 임포트 정리 깔끔합니다.

java.util.List 임포트가 한 번만 유지돼 가독성과 유지보수성이 좋아졌네요.

backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java (1)

3-4: 중복 import 정리로 가독성 개선 👍

불필요한 재정의를 제거하고 import 블록을 상단으로 통일해 유지보수가 더 수월해졌습니다.

backend/src/main/java/moadong/club/repository/ClubSearchRepository.java (1)

3-7: 명시적 import 정리 완료 👍

실제로 사용하는 컬렉션 타입만 명시해 와일드카드 import를 제거한 변경이 보여요. 유지보수성이 좋아져서 환영합니다.

backend/src/main/java/moadong/global/validator/UserIdValidator.java (1)

6-6: 임포트 정렬 변경 이상 없음

임포트 순서 정리만 이루어졌고 동작에 영향이 없습니다.

backend/src/main/java/moadong/global/annotation/UserId.java (1)

9-9: 임포트 위치 조정 문제 없음

임포트 재배치는 스타일 변경일 뿐이며 애노테이션 동작에 영향이 없습니다.

backend/src/main/java/moadong/user/controller/UserController.java (1)

26-32: 명시적 import 전환이 가독성과 유지보수성에 유리합니다.

필요한 애너테이션만 선별적으로 가져오면서 와일드카드 import를 배제해 불필요한 클래스 충돌 가능성을 줄이고 코드 의도를 분명히 했습니다.

backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java (1)

3-5: 필요한 import 정리 확인했습니다. LocalDateTime, List, ArrayList를 모두 사용하는 구조라 명시적인 import 추가가 적절합니다.

backend/src/main/java/moadong/club/entity/Club.java (1)

3-4: 명시적 컬렉션 임포트 추가 확인 완료

ListMap 사용에 필요한 명시적 임포트가 정리되어 가독성과 유지보수성이 좋아졌습니다. 문제 없습니다.

backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java (1)

6-7: 임포트 순서 정리 좋습니다

내부 도메인 임포트를 표준 라이브러리 임포트 뒤로 정리해 일관성과 가독성을 확보했습니다. 이 변경은 기능에 영향을 주지 않으며 적절합니다.

backend/src/test/java/moadong/unit/user/UserRegisterTest.java (2)

4-11: 정리된 static import 구성 좋습니다

테스트에서 사용하는 AssertJ/JUnit/Mockito 호출이 모두 상단으로 모여 가독성이 확실히 좋아졌습니다.


16-16: 필요한 Set import 반영 확인

ConstraintViolation 검증 로직에서 사용하는 Set이 정상적으로 선언되어 더 이상 중복 선언 우려가 없네요.

backend/src/main/java/moadong/global/validator/KoreanValidator.java (1)

6-6: import 정리 확인 완료

Korean import 재배치만 수행되어 논리 변화 없고 기존 구동에 영향 없습니다.

backend/src/main/java/moadong/global/annotation/Korean.java (1)

9-9: import 정리 확인 완료

KoreanValidator import 순서 조정만 존재하며 기능 변화는 없습니다.

backend/src/main/java/moadong/global/annotation/PhoneNumber.java (1)

9-9: import 정리 확인 완료

PhoneNumberValidator import 재배치만 이루어져 동작에는 영향이 없습니다.

backend/src/main/java/moadong/global/validator/PasswordValidator.java (1)

6-6: import 정리 확인 완료

Password import 위치만 조정되었고 검증 로직은 그대로입니다.

backend/src/main/java/moadong/global/annotation/Password.java (1)

9-9: import 정리 확인 완료

PasswordValidator import 순서 변경 외 추가 수정 없이 기존 동작을 유지합니다.

backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java (1)

6-6: 정렬 변경 문제 없음

임포트 순서 조정이 기능이나 동작에 영향을 주지 않습니다.

backend/src/main/java/moadong/club/entity/ClubApplication.java (1)

11-16: 임포트 정리 확인

로직 변경 없이 사용 중인 Lombok·상태 enum 및 Mongo 문서 애너테이션을 명시적으로 정리해 일관성이 좋아졌습니다.

backend/src/main/java/moadong/club/service/ClubApplyService.java (1)

4-28: 불필요 와일드카드 제거 환영

기존에 사용되던 타입만 개별 임포트로 명시해 가독성과 IDE 리졸브가 한층 명확해졌습니다. 기능상 영향도 없고 좋습니다.

backend/src/main/java/moadong/club/controller/ClubApplyController.java (1)

8-30: 컨트롤러 임포트 정리 좋습니다

컨트롤러에서 활용하는 요청 DTO와 스프링 MVC 애너테이션만 명시적으로 남겨 유지보수가 쉬워졌습니다.

backend/src/main/java/moadong/club/service/ClubProfileService.java (1)

3-3: 불필요한 중복 임포트 정리 👍
필요한 List 임포트만 남겨 가독성이 좋아졌습니다.

backend/src/main/java/moadong/club/controller/ClubProfileController.java (1)

34-36: 의존성 정리 및 위임 흐름 확인 완료
컨트롤러가 서비스에만 위임하도록 단순화되어 호출 흐름이 명확해졌습니다.

backend/src/main/java/moadong/user/payload/response/FindUserClubResponse.java (1)

3-4: 형식 변경만 적용된 것으로 보입니다. 기존 API와 행위에 영향이 없어 추가 조치 필요 없습니다.

backend/src/main/java/moadong/user/payload/response/RefreshResponse.java (1)

3-4: 레코드 선언 정리가 적절합니다. 포맷 외 변경이 없어 문제없이 진행해도 되겠습니다.

backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1)

4-4: 정리된 import 👍

Fixture가 불필요한 타입 의존성을 끊고 필요한 요청 객체만 가져오기 때문에 유지보수성이 좋아졌습니다.

backend/build.gradle (1)

35-58: Google Drive 의존성 정리 👍

GoogleDrive 관련 서비스와 설정이 빠지면서 Gradle에서도 관련 의존성이 모두 제거된 것 확인했습니다. 빌드 관점에서 남는 잔여 의존성도 없어 보여 정리 잘 되었습니다.

backend/src/test/java/moadong/util/annotations/IntegrationTest.java (1)

7-13: 통합 테스트 공통 구성이 잘 정리됐습니다.

@IntegrationTest@SpringBootTest@Tag를 메타로 부여해 통합 테스트의 기본 환경과 필터링 기준을 일관되게 적용할 수 있게 된 점이 좋습니다.

backend/src/test/java/moadong/util/annotations/UnitTest.java (1)

7-15: Mockito 확장을 메타로 포함한 선택이 적절합니다.

@UnitTest@ExtendWith(MockitoExtension.class)@Tag를 묶어 반복 선언을 줄이고 필터링 체계를 통합한 구성이 깔끔합니다.

backend/src/main/java/moadong/club/payload/dto/ClubSearchResult.java (1)

3-4: 변경 내용 이상 없습니다.

임포트 정리만 이뤄졌고 동작에는 영향이 없습니다.

backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (2)

50-51: clubId로 ClubQuestion 조회 방식은 컨벤션과 일치

findByClubId로 조회해 ID를 보관하는 접근은 코드베이스 컨벤션과 부합합니다.

Based on learnings


68-84: @Version 필드 확인됨 — 테스트 유효성 확보
ClubQuestion 엔티티에 @Version 필드를 확인했습니다. 동시 업데이트 재현 전략은 낙관적 락 검증에 적합합니다.

Comment on lines +5 to +9
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

중요: KST(now) → LocalDateTime 변환 시 타임존 소실로 저장 시각이 왜곡될 수 있습니다

ZonedDateTime.now(Asia/Seoul).toLocalDateTime()로 타임존 정보를 제거한 뒤, Spring Data Mongo가 LocalDateTime→Date로 변환할 때 시스템 기본 타임존을 사용합니다. 서버 타임존이 KST가 아니면 저장되는 Instant가 몇 시간씩 어긋납니다. 배포 환경이 UTC인 경우 특히 치명적입니다.

권장 수정 1 (선호): Instant로 저장하고 표현 계층에서 타임존을 적용하세요.

- import java.time.LocalDateTime;
- import java.time.ZoneId;
- import java.time.ZonedDateTime;
+ import java.time.Instant;
-    private LocalDateTime createdAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime();
+    private Instant createdAt = Instant.now();
-    private LocalDateTime editedAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime();
+    private Instant editedAt = Instant.now();
-    public void updateEditedAt() {
-       this.editedAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime();
-    }
+    public void updateEditedAt() {
+        this.editedAt = Instant.now();
+    }

단기 핫픽스(타입 변경이 어려운 경우): 최소한 이중 타임존 변환을 멈추세요. LocalDateTime.now()만 사용하면 계산과 저장 모두 시스템 타임존에 의존하므로 즉각적인 시차 왜곡은 막을 수 있습니다(다만 KST 고정 요구사항은 충족하지 못함).

-    private LocalDateTime createdAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime();
+    private LocalDateTime createdAt = LocalDateTime.now();
-    private LocalDateTime editedAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime();
+    private LocalDateTime editedAt = LocalDateTime.now();
-    public void updateEditedAt() {
-       this.editedAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime();
-    }
+    public void updateEditedAt() {
+        this.editedAt = LocalDateTime.now();
+    }

추가 권장: Spring Data Auditing(@CreatedDate, @LastModifiedDate) + Instant 사용으로 생성/수정 시각을 일관되게 관리하는 방안을 고려해 주세요.

Also applies to: 39-44, 61-63

🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/entity/ClubQuestion.java around lines 5-9
(also applies to 39-44 and 61-63): the code converts
ZonedDateTime.now(Asia/Seoul).toLocalDateTime(), which strips zone info and
causes wrong instants when the server TZ ≠ KST; change the timestamp handling to
use Instant for storage (compute and store Instant.now(ZoneOffset.UTC) or obtain
Instant from ZonedDateTime) and apply KST conversion only in the presentation
layer, or if a quick hotfix is required, stop using
ZonedDateTime.toLocalDateTime() and use LocalDateTime.now() consistently;
additionally consider switching auditing to Spring Data
@CreatedDate/@LastModifiedDate with Instant to ensure consistent
created/modified timestamps.

Comment on lines +34 to 55
List<ClubCategory> categories = new ArrayList<>(asList(ClubCategory.values()));
Collections.shuffle(categories);

// 2. 섞인 순서를 기반으로 Category에 대한 랜덤 우선순위 Map을 생성합니다.
Map<String, Integer> randomCategoryPriorities = new HashMap<>();
for (int i = 0; i < categories.size(); i++) {
randomCategoryPriorities.put(categories.get(i).name(), i);
}

result = result.stream()
.sorted(
//
Comparator
// 1차: recruitmentStatus는 기존 enum의 우선순위로 정렬
.comparingInt((ClubSearchResult club) -> ClubRecruitmentStatus.getPriorityFromString(club.recruitmentStatus()))
.thenComparingInt((ClubSearchResult club) -> ClubCategory.getPriorityFromString(club.category()))
// 2차: category는 랜덤하게 생성된 우선순위로 정렬
.thenComparingInt((ClubSearchResult club) ->
randomCategoryPriorities.getOrDefault(
club.category() != null ? club.category().toUpperCase() : null,
Integer.MAX_VALUE))
// 3차: 이름순으로 정렬
.thenComparing(ClubSearchResult::name)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

랜덤 카테고리 우선순위가 실제로 적용되지 않습니다

randomCategoryPriorities 맵은 ClubCategory.values()name()(예: VOLUNTEER)을 키로 사용하지만, 실제 검색 결과에서 내려오는 club.category() 값은 테스트에서도 보이듯 "봉사"처럼 한글 표시 문자열입니다. 이 불일치 때문에 getOrDefault(...)가 항상 Integer.MAX_VALUE를 반환하여 카테고리 우선순위 비교가 무력화되고, 결국 이름순 정렬로만 동작합니다. 요구한 “랜덤 분과 순서”가 전혀 반영되지 않는 심각한 로직 버그입니다. 결과 리스트에서 실제로 등장한 카테고리 문자열을 기준으로 우선순위 맵을 만들어 주십시오.

아래와 같은 방향으로 수정하면 문제를 해결할 수 있습니다:

-        List<ClubCategory> categories = new ArrayList<>(asList(ClubCategory.values()));
-        Collections.shuffle(categories);
-
-        Map<String, Integer> randomCategoryPriorities = new HashMap<>();
-        for (int i = 0; i < categories.size(); i++) {
-            randomCategoryPriorities.put(categories.get(i).name(), i);
-        }
+        List<String> categories = result.stream()
+                .map(ClubSearchResult::category)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toCollection(ArrayList::new));
+        Collections.shuffle(categories);
+
+        Map<String, Integer> randomCategoryPriorities = new HashMap<>();
+        for (int i = 0; i < categories.size(); i++) {
+            randomCategoryPriorities.put(categories.get(i), i);
+        }
 ...
-                                .thenComparingInt((ClubSearchResult club) ->
-                                        randomCategoryPriorities.getOrDefault(
-                                                club.category() != null ? club.category().toUpperCase() : null,
-                                                Integer.MAX_VALUE))
+                                .thenComparingInt((ClubSearchResult club) ->
+                                        randomCategoryPriorities.getOrDefault(
+                                                club.category(), Integer.MAX_VALUE))
📝 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
List<ClubCategory> categories = new ArrayList<>(asList(ClubCategory.values()));
Collections.shuffle(categories);
// 2. 섞인 순서를 기반으로 Category에 대한 랜덤 우선순위 Map을 생성합니다.
Map<String, Integer> randomCategoryPriorities = new HashMap<>();
for (int i = 0; i < categories.size(); i++) {
randomCategoryPriorities.put(categories.get(i).name(), i);
}
result = result.stream()
.sorted(
//
Comparator
// 1차: recruitmentStatus는 기존 enum의 우선순위로 정렬
.comparingInt((ClubSearchResult club) -> ClubRecruitmentStatus.getPriorityFromString(club.recruitmentStatus()))
.thenComparingInt((ClubSearchResult club) -> ClubCategory.getPriorityFromString(club.category()))
// 2차: category는 랜덤하게 생성된 우선순위로 정렬
.thenComparingInt((ClubSearchResult club) ->
randomCategoryPriorities.getOrDefault(
club.category() != null ? club.category().toUpperCase() : null,
Integer.MAX_VALUE))
// 3차: 이름순으로 정렬
.thenComparing(ClubSearchResult::name)
)
// 1. 실제 결과에 등장하는 카테고리 문자열을 기반으로 리스트를 생성하고 섞습니다.
List<String> categories = result.stream()
.map(ClubSearchResult::category)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toCollection(ArrayList::new));
Collections.shuffle(categories);
// 2. 섞인 순서를 기반으로 Category에 대한 랜덤 우선순위 Map을 생성합니다.
Map<String, Integer> randomCategoryPriorities = new HashMap<>();
for (int i = 0; i < categories.size(); i++) {
randomCategoryPriorities.put(categories.get(i), i);
}
result = result.stream()
.sorted(
Comparator
// 1차: recruitmentStatus는 기존 enum의 우선순위로 정렬
.comparingInt((ClubSearchResult club) ->
ClubRecruitmentStatus.getPriorityFromString(club.recruitmentStatus()))
// 2차: 실제 문자열 카테고리의 랜덤 우선순위로 정렬
.thenComparingInt((ClubSearchResult club) ->
randomCategoryPriorities.getOrDefault(
club.category(), Integer.MAX_VALUE))
// 3차: 이름순으로 정렬
.thenComparing(ClubSearchResult::name)
)
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/service/ClubSearchService.java around
lines 34 to 55, the randomCategoryPriorities map is built using
ClubCategory.name() (e.g. "VOLUNTEER") while club.category() returns display
strings like "봉사", causing lookups to miss and always return Integer.MAX_VALUE;
rebuild the map using the actual display/localized category string keys that
appear in results (for example by calling the enum method that returns the
display name or by mapping each ClubCategory to its localized label), ensure you
handle nulls (skip or map null to a default) and use that map in the comparator
so random priorities are applied correctly (fall back to Integer.MAX_VALUE only
when a category truly has no mapping).

Comment on lines +57 to +103
// @Test
// void 모집상태에_해당하는_동아리가_없으면_빈_리스트를_반환한다() {
// // given
// String keyword = "없는키워드";
// String recruitmentStatus = "OPEN";
// String division = "중동";
// String category = "봉사";
//
// when(clubSearchRepository.searchClubsByKeyword(keyword, recruitmentStatus, division, category))
// .thenReturn(List.of()); // 빈 리스트 반환
//
// // when
// ClubSearchResponse response = clubSearchService.searchClubsByKeyword(keyword, recruitmentStatus, division, category);
//
// // then
// assertTrue(response.clubs().isEmpty());
// }
//
// @Test
// void 모집상태가_같다면_카테고리순으로_정렬하고_카테고리도_같다면_이름순으로_반환한다() {
// // given
// String keyword = "동아리";
// String recruitmentStatus = "all";
// String division = "all";
// String category = "all"; // 전체 카테고리
//
// ClubSearchResult club1 = ClubSearchResult.builder().name("club1").recruitmentStatus("OPEN")
// .division("중동").category("봉사").build();
// ClubSearchResult club2 = ClubSearchResult.builder().name("club2").recruitmentStatus("OPEN")
// .division("중동").category("종교").build();
// ClubSearchResult club3 = ClubSearchResult.builder().name("club3").recruitmentStatus("OPEN")
// .division("중동").category("종교").build();
//
// List<ClubSearchResult> unsorted = List.of(club3, club2, club1);
//
// when(clubSearchRepository.searchClubsByKeyword(keyword, recruitmentStatus, division, category))
// .thenReturn(unsorted);
//
// // when
// ClubSearchResponse response = clubSearchService.searchClubsByKeyword(keyword, recruitmentStatus, division, category);
//
// // then
// List<ClubSearchResult> sorted = response.clubs();
// assertEquals("club1", sorted.get(0).name());
// assertEquals("club2", sorted.get(1).name());
// assertEquals("club3", sorted.get(2).name());
// }
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

주석 처리로 테스트 커버리지를 없애면 안 됩니다

두 개의 핵심 시나리오 테스트를 통째로 주석 처리해 버리면 검색 서비스의 기본 동작(빈 결과 처리, 동일 모집상태 내 정렬 등)에 대한 회귀 검증이 사라집니다. 랜덤 정렬 요구사항에 맞춰 단언을 조정하거나, 서비스 쪽에서 랜덤 시드를 주입 가능하게 만드는 등 테스트 가능한 형태로 수정해 다시 활성화해 주십시오.

🤖 Prompt for AI Agents
In backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java around
lines 57-103, two tests were commented out which removes coverage for
empty-result handling and sorting within same recruitment status; re-enable
these tests and make them deterministic: either (A) update the tests to not
assume a strict deterministic order by asserting the response contains the
expected elements and that grouping/sorting rules hold (e.g., verify list size,
verify all expected names present, verify category grouping and name-order
within each category), or (B) modify the service to accept an injectable
Random/seed used for tie-breaking and in tests inject a fixed seed so the
original strict assertions hold; pick one approach and implement the test
changes (or service DI) so the tests can be un-commented and pass reliably.

Copy link
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

릴리즈바로갑니다~

Copy link
Collaborator

@PororoAndFriends PororoAndFriends left a comment

Choose a reason for hiding this comment

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

이번 버전은 리팩터링 위주의 패치가 진행됐네요! 고생하셨습니다~

@Zepelown Zepelown merged commit 4b30989 into main Sep 26, 2025
5 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 25, 2026
12 tasks
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.

4 participants