Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0f283a3
[feat] FollowingJpaRepository.countByFollowingUserJpaEntity_UserId (#70)
hd0rable Jul 13, 2025
6de3b78
[feat] FollowingQueryPersistenceAdapter.countByFollowingUserId (#70)
hd0rable Jul 13, 2025
13e89ee
[feat] FollowingQueryPort.countByFollowingUserId (#70)
hd0rable Jul 13, 2025
be4eb77
[feat] RoomGetMemberListResponse dto 작성 (#70)
hd0rable Jul 13, 2025
8f28789
[feat] RoomGetMemberListUseCase 구현체RoomGetMemberListService.getRoomMe…
hd0rable Jul 13, 2025
9041b22
[feat] RoomGetMemberListUseCase 작성 (#70)
hd0rable Jul 13, 2025
f05874a
[feat] 독서메이트 조회 컨트롤러 작성 (#70)
hd0rable Jul 13, 2025
5f5fa87
Merge remote-tracking branch 'origin/feat/#56-get-home-user-room' int…
hd0rable Jul 13, 2025
802fc6b
[refactor] 머지 충돌 해결 (#70)
hd0rable Jul 13, 2025
7702387
[refactor] 에러코드 수정 (#70)
hd0rable Jul 13, 2025
5f8ac0c
[test] 통합 테스트 코드 작성 (#70)
hd0rable Jul 13, 2025
e1d1d06
[test] 단위 컨트롤러 테스트 코드 작성 (#70)
hd0rable Jul 13, 2025
7f1eab3
[test] 더미 팔로잉 생성 코드 추가 (#70)
hd0rable Jul 13, 2025
ab1478b
Merge remote-tracking branch 'origin/develop' into feat/#70-get-room-…
hd0rable Jul 13, 2025
09a4324
[fix] 잘못된 로직 수정 (#70)
hd0rable Jul 13, 2025
24051bd
[refactor] 트랜잭션 어노테이션 추가 (#70)
hd0rable Jul 13, 2025
1077995
[test] 테스트 코드 수정 (#70)
hd0rable Jul 13, 2025
4572dea
[refactor] 배치쿼리로 수정 (#70)
hd0rable Jul 14, 2025
04be116
Merge remote-tracking branch 'origin/develop' into feat/#70-get-room-…
hd0rable Jul 14, 2025
59991ff
[refactor] 충돌 해결 (#70)
hd0rable Jul 14, 2025
7598586
[remove] 안쓰는 파일 삭제 (#70)
hd0rable Jul 14, 2025
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 @@ -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 입니다."),
Comment thread
hd0rable marked this conversation as resolved.
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, "사용자가 저장하지 않은 책은 저장삭제 할 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<RoomSearchResponse> searchRooms(
Expand Down Expand Up @@ -62,4 +65,10 @@ public BaseResponse<RoomGetHomeJoinedListResponse> getHomeJoinedRooms(@UserId fi
.page(page).build()));
}

// 독서메이트 조회
@GetMapping("/rooms/{roomId}/users")
public BaseResponse<RoomGetMemberListResponse> getRoomMemberList(@PathVariable("roomId") final Long roomId){
return BaseResponse.ok(roomGetMemberListUseCase.getRoomMemberList(roomId));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package konkuk.thip.room.adapter.in.web.response;

import lombok.Builder;

import java.util.List;

@Builder
public record RoomGetMemberListResponse(

List<MemberSearchResult> userList
){
@Builder
public record MemberSearchResult(
Long userId,
String nickname,
String imageUrl,
String alias,
int subscriberCount
) {}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<UserRoom> userRooms = userRoomCommandPort.findAllByRoomId(room.getId());


// 3. 참여자 userId 목록 추출
List<Long> userIds = userRooms.stream()
.map(UserRoom::getUserId)
.toList();

// 4. 배치 쿼리로 유저 정보, 팔로워 수 조회
Map<Long, User> userMap = userCommandPort.findByIds(userIds);
Map<Long, Integer> subscriberCountMap = followingQueryPort.countByFollowingUserIds(userIds);

// 5. 각 userRoom에 대해 DTO 조립
List<RoomGetMemberListResponse.MemberSearchResult> userList = userRooms.stream()
.map(userRoom -> {
Long userId = userRoom.getUserId();
User user = userMap.get(userId);
int subscriberCount = subscriberCountMap.getOrDefault(userId, 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM


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();
}


}
50 changes: 0 additions & 50 deletions src/main/java/konkuk/thip/test/TestExceptionController.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<FollowingJpaEntity, Long>,FollowingQueryRepository {
}
Original file line number Diff line number Diff line change
@@ -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<Long, Integer> countByFollowingUserIds(List<Long> userIds) {
return followingJpaRepository.countByFollowingUserIds(userIds);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package konkuk.thip.user.adapter.out.persistence;

import java.util.List;
import java.util.Map;

public interface FollowingQueryRepository {
Map<Long, Integer> countByFollowingUserIds(List<Long> userIds);
}
Original file line number Diff line number Diff line change
@@ -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<Long, Integer> countByFollowingUserIds(List<Long> userIds) {

QFollowingJpaEntity following = QFollowingJpaEntity.followingJpaEntity;

List<Tuple> results = jpaQueryFactory
.select(following.followingUserJpaEntity.userId, following.count())
.from(following)
.where(following.followingUserJpaEntity.userId.in(userIds))
.groupBy(following.followingUserJpaEntity.userId)
.fetch();

// 결과를 Map<Long, Integer>로 변환
return results.stream()
.collect(Collectors.toMap(
tuple -> tuple.get(following.followingUserJpaEntity.userId),
tuple -> tuple.get(following.count()).intValue()
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -37,4 +42,12 @@ public User findById(Long userId) {

return userMapper.toDomainEntity(userJpaEntity);
}

@Override
public Map<Long, User> findByIds(List<Long> userIds) {
List<UserJpaEntity> entities = userJpaRepository.findAllById(userIds);
return entities.stream()
.map(userMapper::toDomainEntity)
.collect(Collectors.toMap(User::getId, Function.identity()));
}
Comment on lines +46 to +52

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

구현 로직은 올바르나 예외 처리와 입력 검증을 강화해야 합니다.

현재 구현은 기본적인 기능은 수행하지만 몇 가지 중요한 개선사항이 필요합니다:

  1. 존재하지 않는 사용자 ID에 대한 처리가 없습니다
  2. null 입력에 대한 검증이 부족합니다
  3. 빈 리스트 처리에 대한 고려가 필요합니다

다음과 같은 개선된 구현을 제안합니다:

 @Override
 public Map<Long, User> findByIds(List<Long> userIds) {
+    if (userIds == null || userIds.isEmpty()) {
+        return Map.of();
+    }
+    
     List<UserJpaEntity> entities = userJpaRepository.findAllById(userIds);
+    
+    // 존재하지 않는 사용자 ID 검증 (선택사항)
+    if (entities.size() != userIds.size()) {
+        Set<Long> foundIds = entities.stream()
+                .map(UserJpaEntity::getUserId)
+                .collect(Collectors.toSet());
+        List<Long> missingIds = userIds.stream()
+                .filter(id -> !foundIds.contains(id))
+                .collect(Collectors.toList());
+        // 로깅 또는 예외 처리 고려
+    }
+    
     return entities.stream()
             .map(userMapper::toDomainEntity)
             .collect(Collectors.toMap(User::getId, Function.identity()));
 }

또는 더 간단한 방식으로 null 체크만 추가할 수도 있습니다:

 @Override
 public Map<Long, User> findByIds(List<Long> userIds) {
+    if (userIds == null || userIds.isEmpty()) {
+        return Map.of();
+    }
+    
     List<UserJpaEntity> entities = userJpaRepository.findAllById(userIds);
     return entities.stream()
             .map(userMapper::toDomainEntity)
             .collect(Collectors.toMap(User::getId, Function.identity()));
 }
📝 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
@Override
public Map<Long, User> findByIds(List<Long> userIds) {
List<UserJpaEntity> entities = userJpaRepository.findAllById(userIds);
return entities.stream()
.map(userMapper::toDomainEntity)
.collect(Collectors.toMap(User::getId, Function.identity()));
}
@Override
public Map<Long, User> findByIds(List<Long> userIds) {
if (userIds == null || userIds.isEmpty()) {
return Map.of();
}
List<UserJpaEntity> entities = userJpaRepository.findAllById(userIds);
// 존재하지 않는 사용자 ID 검증 (선택사항)
if (entities.size() != userIds.size()) {
Set<Long> foundIds = entities.stream()
.map(UserJpaEntity::getUserId)
.collect(Collectors.toSet());
List<Long> missingIds = userIds.stream()
.filter(id -> !foundIds.contains(id))
.collect(Collectors.toList());
// TODO: 로깅 또는 예외 처리 고려
}
return entities.stream()
.map(userMapper::toDomainEntity)
.collect(Collectors.toMap(User::getId, Function.identity()));
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java
around lines 46 to 52, the method findByIds lacks input validation and handling
for missing user IDs. Add a null check for the input list userIds to prevent
NullPointerException. Also, handle the case when the input list is empty by
returning an empty map immediately. Additionally, consider how to handle user
IDs that do not exist in the database, such as by returning only found users
without errors or by logging missing IDs. Implement these checks before
processing the list and ensure the method returns a consistent and safe result.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package konkuk.thip.user.application.port.out;

import java.util.List;
import java.util.Map;

public interface FollowingQueryPort {
Map<Long, Integer> countByFollowingUserIds(List<Long> userIds);
}

Original file line number Diff line number Diff line change
Expand Up @@ -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<Long, User> findByIds(List<Long> userIds);
}
7 changes: 7 additions & 0 deletions src/test/java/konkuk/thip/common/util/TestEntityFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Loading