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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership;
import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority;
Expand Down Expand Up @@ -45,4 +46,17 @@ public interface MembershipRepository extends JpaRepository<Membership, Integer>
where m.member.id = :memberId and m.space.id = :spaceId
""")
Optional<Membership> findByMemberIdAndSpaceId(Integer memberId, Integer spaceId);

@Query("SELECT m FROM Membership m JOIN FETCH m.space WHERE m.member = :member ORDER BY m.id ASC")
Page<Membership> findAllByMemberWithSpace(@Param("member") Member member, Pageable pageable);

@Query("SELECT m FROM Membership m JOIN FETCH m.space WHERE m.member = :member AND m.authority = :authority ORDER BY m.id ASC")
Page<Membership> findAllByMemberAndAuthorityWithSpace(@Param("member") Member member, @Param("authority") Authority authority, Pageable pageable);

@Query("SELECT m FROM Membership m JOIN FETCH m.space WHERE m.member = :member AND m.authority <> :authority ORDER BY m.id ASC")
Page<Membership> findAllByMemberAndAuthorityIsNotWithSpace(@Param("member") Member member, @Param("authority") Authority authority, Pageable pageable);

// 여러 Space에 속한 Member 목록 한번에 조회 (JOIN FETCH로 Member 정보까지)
@Query("SELECT m FROM Membership m JOIN FETCH m.member WHERE m.space IN :spaces AND m.authority <> org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority.PENDING")
Copy link

Copilot AI Oct 13, 2025

Choose a reason for hiding this comment

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

The fully qualified enum reference org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority.PENDING is unnecessarily verbose. Since Authority is already imported, use Authority.PENDING for better readability and consistency with other queries in the same file.

Suggested change
@Query("SELECT m FROM Membership m JOIN FETCH m.member WHERE m.space IN :spaces AND m.authority <> org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority.PENDING")
@Query("SELECT m FROM Membership m JOIN FETCH m.member WHERE m.space IN :spaces AND m.authority <> Authority.PENDING")

Copilot uses AI. Check for mistakes.
List<Membership> findAllMembersInSpaces(@Param("spaces") List<Space> spaces);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.tuna.zoopzoop.backend.domain.SSE.service.EmitterService;
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
import org.tuna.zoopzoop.backend.domain.member.service.MemberService;
import org.tuna.zoopzoop.backend.domain.space.membership.dto.etc.SpaceMemberInfo;
import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership;
import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority;
import org.tuna.zoopzoop.backend.domain.space.membership.enums.JoinState;
Expand All @@ -21,8 +22,11 @@
import org.tuna.zoopzoop.backend.global.rsData.RsData;

import java.nio.file.AccessDeniedException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -64,11 +68,11 @@ public Membership findByMemberAndSpace(Member member, Space space) {
*/
public Page<Membership> findByMember(Member member, String state, Pageable pageable) {
if (state.equalsIgnoreCase("PENDING")) {
return membershipRepository.findAllByMemberAndAuthorityOrderById(member, Authority.PENDING, pageable);
return membershipRepository.findAllByMemberAndAuthorityWithSpace(member, Authority.PENDING, pageable);
} else if (state.equalsIgnoreCase("JOINED")) {
return membershipRepository.findAllByMemberAndAuthorityIsNotOrderById(member, Authority.PENDING, pageable);
return membershipRepository.findAllByMemberAndAuthorityIsNotWithSpace(member, Authority.PENDING, pageable);
} else {
return membershipRepository.findAllByMemberOrderById(member, pageable);
return membershipRepository.findAllByMemberWithSpace(member, pageable);
}
}

Expand Down Expand Up @@ -107,6 +111,36 @@ public List<Membership> findMembersBySpace(Space space) {
return membershipRepository.findAllBySpaceAndAuthorityIsNotOrderById(space, Authority.PENDING);
}

/**
* 여러 스페이스에 속한 멤버 목록을 한 번의 쿼리로 조회 (N+1 문제 해결용)
* @param spaces 조회할 스페이스 목록
* @return 스페이스 ID를 key로, 해당 스페이스의 멤버 정보 리스트를 value로 갖는 Map
*/
@Transactional(readOnly = true)
public Map<Integer, List<SpaceMemberInfo>> findMembersBySpaces(List<Space> spaces) {
if (spaces == null || spaces.isEmpty()) {
return Collections.emptyMap();
}

// 1. 한 번의 쿼리로 모든 스페이스의 멤버십 정보를 가져옴
List<Membership> allMemberships = membershipRepository.findAllMembersInSpaces(spaces);

// 2. Space ID 별로 그룹핑하여 Map으로 변환
return allMemberships.stream()
.collect(Collectors.groupingBy(
membership -> membership.getSpace().getId(), // Key: Space ID
Collectors.mapping( // Value: List<SpaceMemberInfo> DTO로 변환
membership -> new SpaceMemberInfo(
membership.getMember().getId(),
membership.getMember().getName(),
membership.getMember().getProfileImageUrl(),
membership.getAuthority()
),
Collectors.toList()
)
));
}

// ======================== 권한 조회 ======================== //
/**
* 멤버가 스페이스의 어드민 권한을 가지고 있는지 확인
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
import org.tuna.zoopzoop.backend.global.security.jwt.CustomUserDetails;

import java.nio.file.AccessDeniedException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@RestController
Expand Down Expand Up @@ -144,35 +146,39 @@ public RsData<ResBodyForSpaceListPage> getAllSpaces(
String stateStr = (state == null) ? "ALL" : state.name();
Page<Membership> membershipsPage = membershipService.findByMember(member, stateStr, pageable);

Map<Integer, List<SpaceMemberInfo>> membersBySpaceId = Collections.emptyMap();
if (includeMembers) {
// 현재 페이지에 포함된 Space 엔티티 목록 추출
List<Space> spacesOnPage = membershipsPage.getContent().stream()
.map(Membership::getSpace)
.distinct()
.collect(Collectors.toList());
// 멤버 정보를 한 번의 쿼리로 조회
membersBySpaceId = membershipService.findMembersBySpaces(spacesOnPage);
}

// Page<Membership>를 Page<SpaceMembershipInfo>로 변환
final Map<Integer, List<SpaceMemberInfo>> finalMembersMap = membersBySpaceId;

Page<SpaceInfo> spaceInfosPage = membershipsPage.map(membership -> {
Space space = membership.getSpace();
List<SpaceMemberInfo> memberInfos = null;

if (includeMembers) {
// 스페이스에 속한 멤버 목록 조회 (가입 상태만)
List<Membership> spaceMemberships = membershipService.findMembersBySpace(space);
// 멤버 목록을 DTO로 변환
memberInfos = spaceMemberships.stream()
.map(spaceMembership -> new SpaceMemberInfo(
spaceMembership.getMember().getId(),
spaceMembership.getMember().getName(),
spaceMembership.getMember().getProfileImageUrl(),
spaceMembership.getAuthority()
))
.collect(Collectors.toList());

// Map에서 spaceId로 멤버 목록을 O(1)에 조회 (기존 N+1 유발 코드 대체)
List<SpaceMemberInfo> memberInfos = finalMembersMap.get(space.getId());

if (includeMembers && memberInfos == null) {
memberInfos = Collections.emptyList();
}

return new SpaceInfo(
space.getId(),
space.getName(),
space.getThumbnailUrl(),
membership.getAuthority(),
memberInfos // 조회된 멤버 목록 (null일 수도 있음)
memberInfos
);
});

// 새로운 응답 DTO 생성
ResBodyForSpaceListPage resBody = new ResBodyForSpaceListPage(spaceInfosPage);

return new RsData<>(
Expand Down