diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 8d363aa16..52b8953a3 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -51,7 +51,7 @@ public enum ErrorCode implements ResponseCode { BOOK_KEYWORD_REQUIRED(HttpStatus.BAD_REQUEST, 80007, "검색어는 필수 입력값입니다."), BOOK_PAGE_NUMBER_INVALID(HttpStatus.BAD_REQUEST, 80008, "페이지 번호는 1 이상의 값이어야 합니다."), BOOK_NAVER_API_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80009, "네이버 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."), - BOOK_NOT_FOUND(HttpStatus.BAD_REQUEST, 80010, "존재하지 않는 BOOK 입니다."), + BOOK_NOT_FOUND(HttpStatus.NOT_FOUND, 80010, "존재하지 않는 BOOK 입니다."), BOOK_ALREADY_SAVED(HttpStatus.BAD_REQUEST, 80011, "사용자가 이미 저장한 책입니다."), DUPLICATED_BOOKS_IN_COLLECTION(HttpStatus.INTERNAL_SERVER_ERROR, 80012, "중복된 책이 존재합니다."), BOOK_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80013, "사용자가 저장하지 않은 책은 저장삭제 할 수 없습니다."), diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java index 4d68835cb..6ffa7db3b 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java @@ -4,8 +4,10 @@ import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.room.adapter.in.web.response.RoomRecruitingDetailViewResponse; import konkuk.thip.room.adapter.in.web.response.RoomGetHomeJoinedListResponse; +import konkuk.thip.room.adapter.in.web.response.RoomGetMemberListResponse; import konkuk.thip.room.adapter.in.web.response.RoomSearchResponse; import konkuk.thip.room.application.port.in.RoomGetHomeJoinedListUseCase; +import konkuk.thip.room.application.port.in.RoomGetMemberListUseCase; import konkuk.thip.room.application.port.in.RoomSearchUseCase; import jakarta.validation.Valid; import konkuk.thip.room.adapter.in.web.request.RoomVerifyPasswordRequest; @@ -23,9 +25,10 @@ public class RoomQueryController { private final RoomSearchUseCase roomSearchUseCase; + private final RoomGetHomeJoinedListUseCase roomGetHomeJoinedListUseCase; private final RoomVerifyPasswordUseCase roomVerifyPasswordUseCase; private final RoomShowRecruitingDetailViewUseCase roomShowRecruitingDetailViewUseCase; - private final RoomGetHomeJoinedListUseCase roomGetHomeJoinedListUseCase; + private final RoomGetMemberListUseCase roomGetMemberListUseCase; @GetMapping("/rooms/search") public BaseResponse searchRooms( @@ -62,4 +65,10 @@ public BaseResponse getHomeJoinedRooms(@UserId fi .page(page).build())); } + // 독서메이트 조회 + @GetMapping("/rooms/{roomId}/users") + public BaseResponse getRoomMemberList(@PathVariable("roomId") final Long roomId){ + return BaseResponse.ok(roomGetMemberListUseCase.getRoomMemberList(roomId)); + } + } diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetMemberListResponse.java b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetMemberListResponse.java new file mode 100644 index 000000000..712a40dd6 --- /dev/null +++ b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetMemberListResponse.java @@ -0,0 +1,20 @@ +package konkuk.thip.room.adapter.in.web.response; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record RoomGetMemberListResponse( + + List userList +){ + @Builder + public record MemberSearchResult( + Long userId, + String nickname, + String imageUrl, + String alias, + int subscriberCount + ) {} +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomGetMemberListUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomGetMemberListUseCase.java new file mode 100644 index 000000000..8abda1f1e --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/in/RoomGetMemberListUseCase.java @@ -0,0 +1,7 @@ +package konkuk.thip.room.application.port.in; + +import konkuk.thip.room.adapter.in.web.response.RoomGetMemberListResponse; + +public interface RoomGetMemberListUseCase { + RoomGetMemberListResponse getRoomMemberList(Long roomId); +} diff --git a/src/main/java/konkuk/thip/room/application/service/RoomGetMemberListService.java b/src/main/java/konkuk/thip/room/application/service/RoomGetMemberListService.java new file mode 100644 index 000000000..6227ea0e5 --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/service/RoomGetMemberListService.java @@ -0,0 +1,72 @@ +package konkuk.thip.room.application.service; + +import konkuk.thip.room.adapter.in.web.response.RoomGetMemberListResponse; +import konkuk.thip.room.application.port.in.RoomGetMemberListUseCase; +import konkuk.thip.room.application.port.out.RoomCommandPort; +import konkuk.thip.room.domain.Room; +import konkuk.thip.user.application.port.out.FollowingQueryPort; +import konkuk.thip.user.application.port.out.UserCommandPort; +import konkuk.thip.user.application.port.out.UserRoomCommandPort; +import konkuk.thip.user.domain.User; +import konkuk.thip.user.domain.UserRoom; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class RoomGetMemberListService implements RoomGetMemberListUseCase { + + private final RoomCommandPort roomCommandPort; + private final UserRoomCommandPort userRoomCommandPort; + private final UserCommandPort userCommandPort; + private final FollowingQueryPort followingQueryPort; + + @Override + @Transactional(readOnly = true) + public RoomGetMemberListResponse getRoomMemberList(Long roomId) { + + // 1. 방 검증 및 방 조회 + Room room = roomCommandPort.findById(roomId); + + // 2. 방 참여자(UserRoom) 전체 조회 + List userRooms = userRoomCommandPort.findAllByRoomId(room.getId()); + + + // 3. 참여자 userId 목록 추출 + List userIds = userRooms.stream() + .map(UserRoom::getUserId) + .toList(); + + // 4. 배치 쿼리로 유저 정보, 팔로워 수 조회 + Map userMap = userCommandPort.findByIds(userIds); + Map subscriberCountMap = followingQueryPort.countByFollowingUserIds(userIds); + + // 5. 각 userRoom에 대해 DTO 조립 + List userList = userRooms.stream() + .map(userRoom -> { + Long userId = userRoom.getUserId(); + User user = userMap.get(userId); + int subscriberCount = subscriberCountMap.getOrDefault(userId, 0); + + return RoomGetMemberListResponse.MemberSearchResult.builder() + .userId(userId) + .nickname(user.getNickname()) + .imageUrl(user.getAlias().getImageUrl()) + .alias(user.getAlias().getValue()) + .subscriberCount(subscriberCount) + .build(); + }) + .toList(); + + // 6. DTO 반환 + return RoomGetMemberListResponse.builder() + .userList(userList) + .build(); + } + + +} diff --git a/src/main/java/konkuk/thip/test/TestExceptionController.java b/src/main/java/konkuk/thip/test/TestExceptionController.java deleted file mode 100644 index a89d1f1a9..000000000 --- a/src/main/java/konkuk/thip/test/TestExceptionController.java +++ /dev/null @@ -1,50 +0,0 @@ -package konkuk.thip.test; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import konkuk.thip.common.exception.EntityNotFoundException; -import konkuk.thip.common.exception.code.ErrorCode; -import org.springframework.web.HttpRequestMethodNotSupportedException; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/test") -public class TestExceptionController { - - @GetMapping("/method-not-allowed") - public void methodNotAllowed() throws HttpRequestMethodNotSupportedException { - throw new HttpRequestMethodNotSupportedException("POST", List.of("GET")); - } - - @GetMapping("/invalid-param") - public void invalidParam(@Valid @RequestBody DummyRequest request) { - // 유효성 검사 실패 유도 - } - - @GetMapping("/type-mismatch") - public void typeMismatch(@RequestParam Integer id) { - // id에 문자열 전달 시 예외 발생 - } - - @GetMapping("/missing-param") - public void missingParam(@RequestParam String requiredParam) { - // 파라미터 누락 시 예외 발생 - } - - @GetMapping("/business") - public void business() { - throw new EntityNotFoundException(ErrorCode.API_BAD_REQUEST); - } - - @GetMapping("/runtime") - public void runtime() { - throw new IllegalStateException("서버 내부 오류"); - } - - public static class DummyRequest { - @NotBlank - public String name; - } -} diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingJpaRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingJpaRepository.java new file mode 100644 index 000000000..f0ded99ef --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingJpaRepository.java @@ -0,0 +1,7 @@ +package konkuk.thip.user.adapter.out.persistence; + +import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FollowingJpaRepository extends JpaRepository,FollowingQueryRepository { +} diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java new file mode 100644 index 000000000..b27b6eca6 --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java @@ -0,0 +1,22 @@ +package konkuk.thip.user.adapter.out.persistence; + +import konkuk.thip.user.adapter.out.mapper.FollowingMapper; +import konkuk.thip.user.application.port.out.FollowingQueryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Map; + +@Repository +@RequiredArgsConstructor +public class FollowingQueryPersistenceAdapter implements FollowingQueryPort { + + private final FollowingJpaRepository followingJpaRepository; + private final FollowingMapper followingMapper; + + @Override + public Map countByFollowingUserIds(List userIds) { + return followingJpaRepository.countByFollowingUserIds(userIds); + } +} diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepository.java new file mode 100644 index 000000000..118cc8a53 --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepository.java @@ -0,0 +1,8 @@ +package konkuk.thip.user.adapter.out.persistence; + +import java.util.List; +import java.util.Map; + +public interface FollowingQueryRepository { + Map countByFollowingUserIds(List userIds); +} diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java new file mode 100644 index 000000000..cec44bc76 --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java @@ -0,0 +1,38 @@ +package konkuk.thip.user.adapter.out.persistence; + +import com.querydsl.core.Tuple; +import com.querydsl.jpa.impl.JPAQueryFactory; +import konkuk.thip.user.adapter.out.jpa.QFollowingJpaEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Repository +@RequiredArgsConstructor +public class FollowingQueryRepositoryImpl implements FollowingQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + // 주어진 userId 리스트에 대해 각 userId의 팔로워(구독자) 수를 집계하여 Map으로 반환 + public Map countByFollowingUserIds(List userIds) { + + QFollowingJpaEntity following = QFollowingJpaEntity.followingJpaEntity; + + List results = jpaQueryFactory + .select(following.followingUserJpaEntity.userId, following.count()) + .from(following) + .where(following.followingUserJpaEntity.userId.in(userIds)) + .groupBy(following.followingUserJpaEntity.userId) + .fetch(); + + // 결과를 Map로 변환 + return results.stream() + .collect(Collectors.toMap( + tuple -> tuple.get(following.followingUserJpaEntity.userId), + tuple -> tuple.get(following.count()).intValue() + )); + } +} diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java index 6909bf929..6f819812d 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java @@ -9,6 +9,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + import static konkuk.thip.common.exception.code.ErrorCode.ALIAS_NOT_FOUND; import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND; @@ -37,4 +42,12 @@ public User findById(Long userId) { return userMapper.toDomainEntity(userJpaEntity); } + + @Override + public Map findByIds(List userIds) { + List entities = userJpaRepository.findAllById(userIds); + return entities.stream() + .map(userMapper::toDomainEntity) + .collect(Collectors.toMap(User::getId, Function.identity())); + } } diff --git a/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java b/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java new file mode 100644 index 000000000..ea40bd1ac --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java @@ -0,0 +1,9 @@ +package konkuk.thip.user.application.port.out; + +import java.util.List; +import java.util.Map; + +public interface FollowingQueryPort { + Map countByFollowingUserIds(List userIds); +} + diff --git a/src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.java b/src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.java index c196f63b6..cac05e1b6 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.java +++ b/src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.java @@ -2,8 +2,12 @@ import konkuk.thip.user.domain.User; +import java.util.List; +import java.util.Map; + public interface UserCommandPort { Long save(User user); User findById(Long userId); + Map findByIds(List userIds); } diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java index 3ebebee79..e6625c9d0 100644 --- a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -135,4 +135,11 @@ public static CommentJpaEntity createComment(PostJpaEntity post, UserJpaEntity u .userJpaEntity(user) .build(); } + + public static FollowingJpaEntity createFollowing(UserJpaEntity user,UserJpaEntity followingUser) { + return FollowingJpaEntity.builder() + .userJpaEntity(user) + .followingUserJpaEntity(followingUser) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java new file mode 100644 index 000000000..40d645f0f --- /dev/null +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java @@ -0,0 +1,189 @@ +package konkuk.thip.room.adapter.in.web; + +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.persistence.CategoryJpaRepository; +import konkuk.thip.room.adapter.out.persistence.RoomJpaRepository; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserRoomRole; +import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; +import konkuk.thip.user.adapter.out.persistence.FollowingJpaRepository; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; +import konkuk.thip.user.adapter.out.persistence.UserRoomJpaRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 독서 메이트(방 멤버) 조회 api 통합 테스트") +class RoomGetMemberListApiTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private AliasJpaRepository aliasJpaRepository; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private CategoryJpaRepository categoryJpaRepository; + + @Autowired + private BookJpaRepository bookJpaRepository; + + @Autowired + private RoomJpaRepository roomJpaRepository; + + @Autowired + private UserRoomJpaRepository userRoomJpaRepository; + + @Autowired + private FollowingJpaRepository followingJpaRepository; + + private RoomJpaEntity room1; + private UserJpaEntity user1; + private UserJpaEntity user2; + private UserJpaEntity user3; + private BookJpaEntity book; + private CategoryJpaEntity category; + + @BeforeEach + void setUp() { + AliasJpaEntity alias = TestEntityFactory.createLiteratureAlias(); + aliasJpaRepository.save(alias); + + user1 = userJpaRepository.save(TestEntityFactory.createUser(alias)); + user2 = userJpaRepository.save(TestEntityFactory.createUser(alias)); + user3 = userJpaRepository.save(TestEntityFactory.createUser(alias)); + + book = bookJpaRepository.save(TestEntityFactory.createBook()); + category = categoryJpaRepository.save(TestEntityFactory.createLiteratureCategory(alias)); + + room1 = roomJpaRepository.save(TestEntityFactory.createRoom(book, category)); + + // 유저1(호스트), 유저2(멤버), 유저3(멤버)로 참여 + userRoomJpaRepository.save(TestEntityFactory.createUserRoom(room1, user1, UserRoomRole.HOST, 80.0)); + userRoomJpaRepository.save(TestEntityFactory.createUserRoom(room1, user2, UserRoomRole.MEMBER, 60.0)); + userRoomJpaRepository.save(TestEntityFactory.createUserRoom(room1, user3, UserRoomRole.MEMBER, 50.0)); + + + // 팔로잉 관계 설정 + // user1이 user2, user3을 팔로우 + followingJpaRepository.save(TestEntityFactory.createFollowing(user1, user2)); + followingJpaRepository.save(TestEntityFactory.createFollowing(user1, user3)); + // user2가 user3을 팔로우 + followingJpaRepository.save(TestEntityFactory.createFollowing(user2, user3)); + // user3이 user1을 팔로우 + followingJpaRepository.save(TestEntityFactory.createFollowing(user3, user1)); + } + + @AfterEach + void tearDown() { + followingJpaRepository.deleteAll(); + userRoomJpaRepository.deleteAll(); + roomJpaRepository.deleteAll(); + bookJpaRepository.deleteAll(); + userJpaRepository.deleteAll(); + categoryJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + + @Test + @DisplayName("방 멤버 리스트(독서메이트)가 userId, nickname, imageUrl, alias, subscriberCount로 조회된다.") + void getRoomMemberList_success() throws Exception { + //given + //room1에 user1,user2,user3가 참여 + Long roomId = room1.getRoomId(); + + //when + ResultActions result = mockMvc.perform(get("/rooms/{roomId}/users", roomId)); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.userList", hasSize(3))) + .andExpect(jsonPath("$.data.userList[0].userId").value(user1.getUserId().intValue())) + .andExpect(jsonPath("$.data.userList[0].nickname").exists()) + .andExpect(jsonPath("$.data.userList[0].imageUrl").exists()) + .andExpect(jsonPath("$.data.userList[0].alias").exists()) + .andExpect(jsonPath("$.data.userList[0].subscriberCount").isNumber()) + .andExpect(jsonPath("$.data.userList[1].userId").value(user2.getUserId().intValue())) + .andExpect(jsonPath("$.data.userList[1].nickname").exists()) + .andExpect(jsonPath("$.data.userList[1].imageUrl").exists()) + .andExpect(jsonPath("$.data.userList[1].alias").exists()) + .andExpect(jsonPath("$.data.userList[1].subscriberCount").isNumber()) + .andExpect(jsonPath("$.data.userList[2].userId").value(user3.getUserId().intValue())) + .andExpect(jsonPath("$.data.userList[2].nickname").exists()) + .andExpect(jsonPath("$.data.userList[2].imageUrl").exists()) + .andExpect(jsonPath("$.data.userList[2].alias").exists()) + .andExpect(jsonPath("$.data.userList[2].subscriberCount").isNumber()); + } + + @Test + @DisplayName("방에 멤버가 없으면 빈 리스트를 반환한다.") + void getRoomMemberList_empty() throws Exception { + //given + RoomJpaEntity emptyRoom = roomJpaRepository.save(TestEntityFactory.createRoom(book, category)); + + //when + ResultActions result = mockMvc.perform(get("/rooms/{roomId}/users", emptyRoom.getRoomId())); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.userList", hasSize(0))); + } + + @Test + @DisplayName("팔로워(구독자) 수가 올바르게 집계된다.") + void getRoomMemberList_subscriberCount() throws Exception { + //given + Long roomId = room1.getRoomId(); + + //when + ResultActions result = mockMvc.perform(get("/rooms/{roomId}/users", roomId)); + + //then + // user1: user3이 팔로우(1명) + // user2: user1이 팔로우(1명) + // user3: user1, user2가 팔로우(2명) + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.userList[?(@.userId==" + user1.getUserId() + ")].subscriberCount").value(contains(1))) + .andExpect(jsonPath("$.data.userList[?(@.userId==" + user2.getUserId() + ")].subscriberCount").value(contains(1))) + .andExpect(jsonPath("$.data.userList[?(@.userId==" + user3.getUserId() + ")].subscriberCount").value(contains(2))); + } + + @Test + @DisplayName("팔로워가 한 명도 없는 사용자는 subscriberCount가 0으로 조회된다.") + void getRoomMemberList_noSubscriber() throws Exception { + //given + UserJpaEntity userNoFollower = userJpaRepository.save(TestEntityFactory.createUser(aliasJpaRepository.findAll().get(0))); + userRoomJpaRepository.save(TestEntityFactory.createUserRoom(room1, userNoFollower, UserRoomRole.MEMBER, 10.0)); + Long roomId = room1.getRoomId(); + + //when + ResultActions result = mockMvc.perform(get("/rooms/{roomId}/users", roomId)); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.userList[?(@.userId==" + userNoFollower.getUserId() + ")].subscriberCount").value(contains(0))); + } +} \ No newline at end of file diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListControllerTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListControllerTest.java new file mode 100644 index 000000000..ea4eeb2a5 --- /dev/null +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListControllerTest.java @@ -0,0 +1,73 @@ +package konkuk.thip.room.adapter.in.web; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.HashMap; +import java.util.Map; + +import static konkuk.thip.common.exception.code.ErrorCode.*; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[단위] 독서메이트(방 멤버) 조회 api controller 테스트") +class RoomGetMemberListControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + private Map buildValidRequest() { + Map request = new HashMap<>(); + request.put("roomId", 1L); + return request; + } + + private void assertRoomNotFound(Map req, String msg) throws Exception { + mockMvc.perform(get("/rooms/{roomId}/users", req.get("roomId")) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(ROOM_NOT_FOUND.getCode())) + .andExpect(jsonPath("$.message", containsString(msg))); + } + + @Nested + @DisplayName("roomId 검증") + class RoomIdValidation { + + @Test + @DisplayName("roomId가 없을 때 400 error") + void missing_roomId() throws Exception { + Map req = buildValidRequest(); + req.remove("roomId"); + mockMvc.perform(get("/rooms//users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("DB에 존재하지 않는 roomId가 들어오면 404 error") + void not_found_roomId() throws Exception { + Map req = buildValidRequest(); + req.remove("roomId"); + req.put("roomId", 99999L); + assertRoomNotFound(req, "존재하지 않는 ROOM 입니다."); + } + } +}