Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 57 additions & 54 deletions backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,80 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import lombok.Builder;
import moadong.club.entity.Club;
import moadong.club.entity.ClubRecruitmentInformation;
import moadong.club.entity.Faq;

@Builder
public record ClubDetailedResult(
String id,
String name,
String logo,
String cover,
List<String> tags,
String state,
List<String> feeds,
String introduction,
String description,
String presidentName,
String presidentPhoneNumber,
String recruitmentPeriod,
String recruitmentTarget,
String recruitmentStatus,
String externalApplicationUrl,
Map<String, String> socialLinks,
String category,
String division,
List<Faq> faqs
String id,
String name,
String logo,
String cover,
List<String> tags,
String state,
List<String> feeds,
String introduction,
String description,
String presidentName,
String presidentPhoneNumber,
String recruitmentPeriod,
String recruitmentTarget,
String recruitmentStatus,
String externalApplicationUrl,
Map<String, String> socialLinks,
String category,
String division,
List<Faq> faqs,
List<ClubSearchResult> recommendClubs
) {

public static ClubDetailedResult of(Club club) {
public static ClubDetailedResult of(Club club, List<ClubSearchResult> recommendClubs) {
String period = "미정";
ClubRecruitmentInformation clubRecruitmentInformation = club.getClubRecruitmentInformation();
if (clubRecruitmentInformation.hasRecruitmentPeriod()) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm");
period = clubRecruitmentInformation.getRecruitmentStart().format(formatter) + " ~ "
+ clubRecruitmentInformation.getRecruitmentEnd().format(formatter);
+ clubRecruitmentInformation.getRecruitmentEnd().format(formatter);
}
return ClubDetailedResult.builder()
.id(club.getId() == null ? "" : club.getId())
.name(club.getName() == null ? "" : club.getName())
.logo(clubRecruitmentInformation.getLogo() == null ? ""
: clubRecruitmentInformation.getLogo())
.cover(clubRecruitmentInformation.getCover() == null ? ""
: clubRecruitmentInformation.getCover())
.tags(clubRecruitmentInformation.getTags() == null ? List.of()
: clubRecruitmentInformation.getTags())
.state(club.getState() == null ? "" : club.getState().getDesc())
.feeds(clubRecruitmentInformation.getFeedImages() == null ? List.of()
: clubRecruitmentInformation.getFeedImages())
.category(club.getCategory() == null ? "" : club.getCategory())
.division(club.getDivision() == null ? "" : club.getDivision())
.introduction(clubRecruitmentInformation.getIntroduction() == null ? ""
: clubRecruitmentInformation.getIntroduction())
.description(clubRecruitmentInformation.getDescription() == null ? ""
: clubRecruitmentInformation.getDescription())
.presidentName(clubRecruitmentInformation.getPresidentName() == null ? ""
: clubRecruitmentInformation.getPresidentName())
.presidentPhoneNumber(
clubRecruitmentInformation.getPresidentTelephoneNumber() == null ? ""
: clubRecruitmentInformation.getPresidentTelephoneNumber())
.recruitmentPeriod(period)
.recruitmentTarget(clubRecruitmentInformation.getRecruitmentTarget() == null ? ""
: clubRecruitmentInformation.getRecruitmentTarget())
.recruitmentStatus(clubRecruitmentInformation.getClubRecruitmentStatus() == null
? "" : clubRecruitmentInformation.getClubRecruitmentStatus().getDescription())
.externalApplicationUrl(club.getClubRecruitmentInformation().getExternalApplicationUrl()== null ? "" :
.id(club.getId() == null ? "" : club.getId())
.name(club.getName() == null ? "" : club.getName())
.logo(clubRecruitmentInformation.getLogo() == null ? ""
: clubRecruitmentInformation.getLogo())
.cover(clubRecruitmentInformation.getCover() == null ? ""
: clubRecruitmentInformation.getCover())
.tags(clubRecruitmentInformation.getTags() == null ? List.of()
: clubRecruitmentInformation.getTags())
.state(club.getState() == null ? "" : club.getState().getDesc())
.feeds(clubRecruitmentInformation.getFeedImages() == null ? List.of()
: clubRecruitmentInformation.getFeedImages())
.category(club.getCategory() == null ? "" : club.getCategory())
.division(club.getDivision() == null ? "" : club.getDivision())
.introduction(clubRecruitmentInformation.getIntroduction() == null ? ""
: clubRecruitmentInformation.getIntroduction())
.description(clubRecruitmentInformation.getDescription() == null ? ""
: clubRecruitmentInformation.getDescription())
.presidentName(clubRecruitmentInformation.getPresidentName() == null ? ""
: clubRecruitmentInformation.getPresidentName())
.presidentPhoneNumber(
clubRecruitmentInformation.getPresidentTelephoneNumber() == null ? ""
: clubRecruitmentInformation.getPresidentTelephoneNumber())
.recruitmentPeriod(period)
.recruitmentTarget(clubRecruitmentInformation.getRecruitmentTarget() == null ? ""
: clubRecruitmentInformation.getRecruitmentTarget())
.recruitmentStatus(clubRecruitmentInformation.getClubRecruitmentStatus() == null
? "" : clubRecruitmentInformation.getClubRecruitmentStatus().getDescription())
.externalApplicationUrl(club.getClubRecruitmentInformation().getExternalApplicationUrl() == null ? "" :
club.getClubRecruitmentInformation().getExternalApplicationUrl())
.socialLinks(club.getSocialLinks() == null ? Map.of()
: club.getSocialLinks())
.faqs(club.getClubRecruitmentInformation().getFaqs() == null ? List.of()
: club.getClubRecruitmentInformation().getFaqs())
.build();
.socialLinks(club.getSocialLinks() == null ? Map.of()
: club.getSocialLinks())
.faqs(club.getClubRecruitmentInformation().getFaqs() == null ? List.of()
: club.getClubRecruitmentInformation().getFaqs())
.recommendClubs(recommendClubs)
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,40 @@ public List<ClubSearchResult> searchClubsByKeyword(String keyword, String recrui
return results.getMappedResults();
}

public List<ClubSearchResult> searchRecommendClubs(String category, String excludeClubId) {

List<AggregationOperation> operations = new ArrayList<>();
// 모집 상태 & 같은 category & 제외할 club _id 필터
operations.add(Aggregation.match(
new Criteria()
.and("state").is(ClubState.AVAILABLE.getName())
.and("category").is(category)
.and("_id").ne(excludeClubId)
.and("recruitmentInformation.clubRecruitmentStatus")
.in(
ClubRecruitmentStatus.ALWAYS.toString(),
ClubRecruitmentStatus.OPEN.toString(),
ClubRecruitmentStatus.UPCOMING.toString()
)
));
Comment on lines +68 to +83
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

자기 자신 제외 필터가 작동하지 않을 수 있음(String vs ObjectId 비교)

and("_id").ne(excludeClubId)_id:ObjectIdString의 타입 불일치로 매치되지 않을 수 있습니다. ObjectId로 변환해 비교해야 합니다.

다음 패치를 제안합니다:

@@
-    public List<ClubSearchResult> searchRecommendClubs(String category, String excludeClubId) {
+    public List<ClubSearchResult> searchRecommendClubs(String category, String excludeClubId) {
@@
-        operations.add(Aggregation.match(
-                new Criteria()
-                        .and("state").is(ClubState.AVAILABLE.getName())
-                        .and("category").is(category)
-                        .and("_id").ne(excludeClubId)
-                        .and("recruitmentInformation.clubRecruitmentStatus")
-                        .in(
-                                ClubRecruitmentStatus.ALWAYS.toString(),
-                                ClubRecruitmentStatus.OPEN.toString(),
-                                ClubRecruitmentStatus.UPCOMING.toString()
-                        )
-        ));
+        operations.add(Aggregation.match(
+                new Criteria()
+                        .and("state").is(ClubState.AVAILABLE.getName())
+                        .and("category").is(category)
+                        .and("_id").ne(new org.bson.types.ObjectId(excludeClubId))
+                        .and("recruitmentInformation.clubRecruitmentStatus")
+                        .in(
+                                ClubRecruitmentStatus.ALWAYS.toString(),
+                                ClubRecruitmentStatus.OPEN.toString(),
+                                ClubRecruitmentStatus.UPCOMING.toString()
+                        )
+        ));

또는 메서드 시그니처를 ObjectId excludeClubId로 변경하고 서비스에서 objectId를 직접 전달하는 것도 좋습니다.

📝 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
public List<ClubSearchResult> searchRecommendClubs(String category, String excludeClubId) {
List<AggregationOperation> operations = new ArrayList<>();
// 모집 상태 & 같은 category & 제외할 club _id 필터
operations.add(Aggregation.match(
new Criteria()
.and("state").is(ClubState.AVAILABLE.getName())
.and("category").is(category)
.and("_id").ne(excludeClubId)
.and("recruitmentInformation.clubRecruitmentStatus")
.in(
ClubRecruitmentStatus.ALWAYS.toString(),
ClubRecruitmentStatus.OPEN.toString(),
ClubRecruitmentStatus.UPCOMING.toString()
)
));
public List<ClubSearchResult> searchRecommendClubs(String category, String excludeClubId) {
List<AggregationOperation> operations = new ArrayList<>();
// 모집 상태 & 같은 category & 제외할 club _id 필터
operations.add(Aggregation.match(
new Criteria()
.and("state").is(ClubState.AVAILABLE.getName())
.and("category").is(category)
.and("_id").ne(new org.bson.types.ObjectId(excludeClubId))
.and("recruitmentInformation.clubRecruitmentStatus")
.in(
ClubRecruitmentStatus.ALWAYS.toString(),
ClubRecruitmentStatus.OPEN.toString(),
ClubRecruitmentStatus.UPCOMING.toString()
)
));
// …rest of the method…
}
🤖 Prompt for AI Agents
backend/src/main/java/moadong/club/repository/ClubSearchRepository.java around
lines 68 to 83: the exclusion filter uses and("_id").ne(excludeClubId) which
compares a String to an ObjectId and can fail; convert the excludeClubId to an
ObjectId before comparing (e.g., import org.bson.types.ObjectId and use new
ObjectId(excludeClubId) in the Criteria) or change the method signature to
accept ObjectId excludeClubId and use that directly, and add null/invalid-id
handling to skip the ne(...) clause when the exclude id is blank/invalid.

// 랜덤 추출 (5개)
operations.add(Aggregation.sample(5L));
// 필요한 필드만 매핑
operations.add(
Aggregation.project("name", "state", "category", "division")
.and("recruitmentInformation.introduction").as("introduction")
.and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus")
.and(ConditionalOperators.ifNull("$recruitmentInformation.logo").then("")).as("logo")
.and(ConditionalOperators.ifNull("$recruitmentInformation.tags").then("")).as("tags")
);
Comment on lines +87 to +93
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

_id(id) 필드 노출 필요 여부 확인 및 추가 제안

추천 목록을 클릭해 상세로 이동해야 한다면 id가 필요합니다. 현재 project_id가 포함되지 않아 DTO에 매핑되지 않을 수 있습니다.

다음과 같이 id를 포함시키는 것을 제안합니다:

-                Aggregation.project("name", "state", "category", "division")
+                Aggregation.project("name", "state", "category", "division")
+                        .and("_id").as("id")

ClubSearchResult에 id 필드가 없다면 DTO에도 반영이 필요합니다.
다음 스크립트로 ClubSearchResult 정의를 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# ClubSearchResult의 필드 정의 확인
rg -n -C2 --type=java 'record\s+ClubSearchResult|class\s+ClubSearchResult' backend/src/main/java

Length of output: 536


Aggregation 프로젝션에 _id 필드 매핑 누락
프로젝트 단계에서 MongoDB의 _id 값을 매핑하지 않아 ClubSearchResult.id 필드에 값이 들어오지 않습니다.
다음처럼 .and("_id").as("id")를 추가해주세요.

파일: backend/src/main/java/moadong/club/repository/ClubSearchRepository.java
위치: 87–93행

         operations.add(
-                Aggregation.project("name", "state", "category", "division")
+                Aggregation.project("name", "state", "category", "division")
+                        .and("_id").as("id")
                         .and("recruitmentInformation.introduction").as("introduction")
                         .and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus")
                         .and(ConditionalOperators.ifNull("$recruitmentInformation.logo").then("")).as("logo")
  • ClubSearchResult 레코드에 String id 필드가 정의되어 있어(확인됨), DTO 매핑을 위해 반드시 프로젝션에 포함시켜야 합니다.
  • 반영 후, 클럽 상세 조회 링크 등에 정상적으로 id가 전달되는지 확인해주세요.
📝 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
operations.add(
Aggregation.project("name", "state", "category", "division")
.and("recruitmentInformation.introduction").as("introduction")
.and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus")
.and(ConditionalOperators.ifNull("$recruitmentInformation.logo").then("")).as("logo")
.and(ConditionalOperators.ifNull("$recruitmentInformation.tags").then("")).as("tags")
);
operations.add(
Aggregation.project("name", "state", "category", "division")
.and("_id").as("id")
.and("recruitmentInformation.introduction").as("introduction")
.and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus")
.and(ConditionalOperators.ifNull("$recruitmentInformation.logo").then("")).as("logo")
.and(ConditionalOperators.ifNull("$recruitmentInformation.tags").then("")).as("tags")
);
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/repository/ClubSearchRepository.java
around lines 87–93, the aggregation projection omits MongoDB's _id so
ClubSearchResult.id remains empty; update the projection to include the _id
mapping by adding .and("_id").as("id") to the Aggregation.project chain, then
re-run tests or a sample query to confirm the id is correctly populated and
propagated to club detail links.


Aggregation aggregation = Aggregation.newAggregation(operations);
AggregationResults<ClubSearchResult> results = mongoTemplate.aggregate(aggregation, "clubs",
ClubSearchResult.class);

return results.getMappedResults();
}

private Criteria getMatchedCriteria(String recruitmentStatus, String division,
String category) {
List<Criteria> criteriaList = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import lombok.AllArgsConstructor;
import moadong.club.entity.Club;
import moadong.club.payload.dto.ClubDetailedResult;
import moadong.club.payload.dto.ClubSearchResult;
import moadong.club.payload.request.ClubInfoRequest;
import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest;
import moadong.club.payload.response.ClubDetailedResponse;
import moadong.club.repository.ClubRepository;
import moadong.club.repository.ClubSearchRepository;
import moadong.club.util.RecruitmentStateCalculator;
import moadong.global.exception.ErrorCode;
import moadong.global.exception.RestApiException;
Expand All @@ -15,11 +17,14 @@
import org.bson.types.ObjectId;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@AllArgsConstructor
public class ClubProfileService {

private final ClubRepository clubRepository;
private final ClubSearchRepository clubSearchRepository;

public void updateClubInfo(ClubInfoRequest request, CustomUserDetails user) {
Club club = clubRepository.findClubByUserId(user.getId())
Expand Down Expand Up @@ -47,8 +52,10 @@ public ClubDetailedResponse getClubDetail(String clubId) {
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));

List<ClubSearchResult> clubSearchResults = clubSearchRepository.searchRecommendClubs(club.getCategory(), clubId);

ClubDetailedResult clubDetailedResult = ClubDetailedResult.of(
club
club,clubSearchResults
);
Comment on lines +55 to 59
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

자기 자신 제외 필터가 동작하지 않을 가능성(ObjectId 타입 불일치)

searchRecommendClubs(club.getCategory(), clubId)로 문자열 clubId를 전달하고, 리포지토리에서는 _id 필드를 비교합니다. MongoDB의 _id가 ObjectId 타입이라면 String 비교는 매치되지 않아 자기 자신이 제외되지 않을 수 있습니다. 리포지토리에서 new ObjectId(excludeClubId)로 변환해 비교하도록 수정하는 것이 안전합니다. (아래 리포지토리 코멘트에 패치 제안 포함)

변경 후, 동일 클럽이 추천 결과에 포함되지 않는지 꼭 수동/자동 테스트로 검증해 주세요.


🏁 Script executed:

#!/bin/bash
set -e
# searchRecommendClubs 메서드 선언 및 구현 위치 확인
rg -nC3 'searchRecommendClubs' -g '*.java'

Length of output: 1708


🏁 Script executed:

#!/bin/bash
# searchRecommendClubs 구현부 전체 확인
sed -n '1,150p' backend/src/main/java/moadong/club/repository/ClubSearchRepository.java

Length of output: 6099


🏁 Script executed:

#!/bin/bash
# ClubProfileService 근처 코드 확인
sed -n '1,100p' backend/src/main/java/moadong/club/service/ClubProfileService.java

Length of output: 2535


ObjectId 타입 불일치로 자기 자신 제외 필터 미동작 가능성 확인 및 수정 필요

searchRecommendClubs(String category, String excludeClubId)에서 _id(ObjectId)와 excludeClubId(String)를 직접 비교하고 있어 타입 불일치로 필터가 적용되지 않을 수 있습니다. String을 ObjectId로 변환해 비교하도록 변경해야 합니다.

  • 위치

    • backend/src/main/java/moadong/club/repository/ClubSearchRepository.java
      searchRecommendClubs 메서드
  • 제안 수정사항

     // 기존: String 타입 excludeClubId를 _id와 비교
     .and("_id").ne(excludeClubId)
    +// 수정: String → ObjectId 변환 후 비교
    +.and("_id").ne(new ObjectId(excludeClubId))
  • import 추가

    import org.bson.types.ObjectId;
  • 검증
    변경 후 동일한 클럽이 추천 결과에서 정상적으로 제외되는지 자동/수동 테스트로 확인 부탁드립니다.

🤖 Prompt for AI Agents
backend/src/main/java/moadong/club/service/ClubProfileService.java around lines
55-59: the recommend-club query currently may fail to exclude the current club
because ClubSearchRepository.searchRecommendClubs compares MongoDB _id
(ObjectId) with a String excludeClubId; update searchRecommendClubs in
backend/src/main/java/moadong/club/repository/ClubSearchRepository.java to
convert the String excludeClubId to an org.bson.types.ObjectId (add import
org.bson.types.ObjectId) and use that ObjectId in the query/filter (handle
null/invalid id formats defensively), then run automated/manual tests to confirm
the same club is properly excluded from recommendations.

return new ClubDetailedResponse(clubDetailedResult);
}
Expand Down
Loading