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 @@ -103,8 +103,25 @@ public CursorBasedList<RoomQueryDto> findPlayingRoomsUserParticipated(Long userI

@Override
public CursorBasedList<RoomQueryDto> findPlayingAndRecruitingRoomsUserParticipated(Long userId, Cursor cursor) {
return findRoomsByDeadlineCursor(cursor, (lastLocalDate, lastId, pageSize) ->
roomJpaRepository.findPlayingAndRecruitingRoomsUserParticipated(userId, lastLocalDate, lastId, pageSize));
Integer lastPriority = cursor.isFirstRequest() ? null : cursor.getInteger(0);
LocalDate lastLocalDate = cursor.isFirstRequest() ? null : cursor.getLocalDate(1);
Long lastId = cursor.isFirstRequest() ? null : cursor.getLong(2);
int pageSize = cursor.getPageSize();

List<RoomQueryDto> dtos = roomJpaRepository.findPlayingAndRecruitingRoomsUserParticipated(
userId, lastPriority, lastLocalDate, lastId, pageSize
);

return CursorBasedList.of(dtos, pageSize, dto -> {
int priority = dto.startDate().isAfter(LocalDate.now()) ? 1 : 0; // 0 : 진행중인 방, 1 : 모집중인 방 // TODO : dto에 RoomStatus 도입되면 수정해야함

Cursor nextCursor = new Cursor(List.of(
String.valueOf(priority),
dto.endDate().toString(),
dto.roomId().toString()
));
return nextCursor.toEncodedString();
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public interface RoomQueryRepository {

List<RoomQueryDto> findPlayingRoomsUserParticipated(Long userId, LocalDate dateCursor, Long roomIdCursor, int pageSize);

List<RoomQueryDto> findPlayingAndRecruitingRoomsUserParticipated(Long userId, LocalDate dateCursor, Long roomIdCursor, int pageSize);
List<RoomQueryDto> findPlayingAndRecruitingRoomsUserParticipated(Long userId, Integer priorityCursor, LocalDate dateCursor, Long roomIdCursor, int pageSize);

List<RoomQueryDto> findExpiredRoomsUserParticipated(Long userId, LocalDate dateCursor, Long roomIdCursor, int pageSize);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ public List<RoomQueryDto> findPlayingRoomsUserParticipated(
// 3) 진행+모집 통합
@Override
public List<RoomQueryDto> findPlayingAndRecruitingRoomsUserParticipated(
Long userId, LocalDate dateCursor, Long roomIdCursor, int pageSize
Long userId, Integer priorityCursor, LocalDate dateCursor, Long roomIdCursor, int pageSize
) {
LocalDate today = LocalDate.now();
BooleanExpression playing = room.startDate.loe(today).and(room.endDate.goe(today));
Expand All @@ -299,13 +299,7 @@ public List<RoomQueryDto> findPlayingAndRecruitingRoomsUserParticipated(
.when(playing).then(0)
.otherwise(1);

OrderSpecifier<?>[] orders = new OrderSpecifier<?>[]{
priority.asc(),
cursorExpr.asc(),
room.roomId.asc()
};

return fetchMyRooms(base, cursorExpr, orders, true, dateCursor, roomIdCursor, pageSize);
return fetchMyRoomsWithPriority(base, priority, cursorExpr, priorityCursor, dateCursor, roomIdCursor, pageSize);
}

// 4) 만료된 방
Expand Down Expand Up @@ -412,9 +406,9 @@ private BooleanExpression userJoinedRoom(Long userId) {
.exists();
}

/**
* 공통 커서 + 2단계 조회 (IDs → entities) 처리
*/
// ======================================================
// 공통 fetch (키셋: (date, id)) - 단일 축(모집/진행/만료) 전용
// ======================================================
private List<RoomQueryDto> fetchMyRooms(
BooleanExpression baseCondition,
DateExpression<LocalDate> cursorExpr,
Expand All @@ -425,7 +419,7 @@ private List<RoomQueryDto> fetchMyRooms(
int pageSize
) {
BooleanBuilder where = new BooleanBuilder(baseCondition);
if (dateCursor != null && roomIdCursor != null) { // 첫 페이지가 아닌 경우
if (dateCursor != null && roomIdCursor != null) { // 2중 복합 커서
if (ascending) {
where.and(cursorExpr.gt(dateCursor)
.or(cursorExpr.eq(dateCursor)
Expand All @@ -437,7 +431,6 @@ private List<RoomQueryDto> fetchMyRooms(
}
}

// 2) DTO 프로젝션: 필요한 필드만 바로 조회
return queryFactory
.select(new QRoomQueryDto(
room.roomId,
Expand All @@ -446,7 +439,7 @@ private List<RoomQueryDto> fetchMyRooms(
room.recruitCount,
room.memberCount,
room.startDate,
room.endDate,
cursorExpr, // endDate 자리에 상황별 deadline 컬럼 전달
room.isPublic
))
.from(participant)
Expand All @@ -457,4 +450,53 @@ private List<RoomQueryDto> fetchMyRooms(
.limit(pageSize + 1)
.fetch();
}

// ======================================================
// 공통 fetch (키셋: (priority, date, id)) - 혼합(진행+모집) 전용
// ======================================================
private List<RoomQueryDto> fetchMyRoomsWithPriority(
BooleanExpression baseCondition,
NumberExpression<Integer> priorityExpr,
DateExpression<LocalDate> cursorExpr,
Integer priorityCursor,
LocalDate dateCursor,
Long roomIdCursor,
int pageSize
) {
BooleanBuilder where = new BooleanBuilder(baseCondition);

if (priorityCursor != null && dateCursor != null && roomIdCursor != null) { // 3중 복합 커서
where.and(
priorityExpr.gt(priorityCursor)
.or(priorityExpr.eq(priorityCursor)
.and(cursorExpr.gt(dateCursor)
.or(cursorExpr.eq(dateCursor)
.and(room.roomId.gt(roomIdCursor))
)
)
)
);
}

return queryFactory
.select(new QRoomQueryDto(
room.roomId,
book.imageUrl,
room.title,
room.recruitCount,
room.memberCount,
room.startDate,
cursorExpr, // endDate 자리에 상황별 deadline 컬럼 전달
room.isPublic
))
.from(participant)
.join(participant.roomJpaEntity, room)
.join(room.bookJpaEntity, book)
.where(where)
.orderBy(
new OrderSpecifier<?>[]{priorityExpr.asc(), cursorExpr.asc(), room.roomId.asc()}
)
.limit(pageSize + 1)
.fetch();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package konkuk.thip.room.application.port.out.dto;

import com.querydsl.core.annotations.QueryProjection;
import jakarta.annotation.Nullable;
import lombok.Builder;
import org.springframework.util.Assert;

import java.time.LocalDate;

// TODO : RoomStatus 도입 + RoomQueryRepositoryImpl 코드 수정
@Builder
public record RoomQueryDto(
Long roomId,
Expand All @@ -18,7 +18,7 @@ public record RoomQueryDto(
LocalDate endDate, // 방 진행 마감일 or 방 모집 마감일
Boolean isPublic // 공개방 여부
) {
// 내가 참여한 모임방(모집중, 진행중, 모집+진행중z, 완료된) 조회 시 활용
// 내가 참여한 모임방(모집중, 진행중, 모집+진행중, 완료된) 조회 시 활용
@QueryProjection
public RoomQueryDto {
Assert.notNull(roomId, "roomId must not be null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,4 +492,91 @@ void get_my_rooms_about_exit_rooms() throws Exception {
.andExpect(jsonPath("$.data.roomList[1].roomName", is("과학-방-1일뒤-활동시작")))
.andExpect(jsonPath("$.data.roomList[1].memberCount", is(6))); // 기존 5명 + user
}

@Test
@DisplayName("혼합(진행+모집) 무한스크롤: (priority, date, id) 키셋으로 중복/누락 없이 페이징된다.")
void get_my_playing_and_recruiting_rooms_pagination() throws Exception {
// given
// 진행중인 방 6개 (endDate 임박 순서: +1d ~ +6d)
RoomJpaEntity playing1 = saveScienceRoom("진행중인방-책-P1", "pisbn1", "과학-방-1일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(1), 10);
changeRoomMemberCount(playing1, 3);
RoomJpaEntity playing2 = saveScienceRoom("진행중인방-책-P2", "pisbn2", "과학-방-2일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(2), 10);
changeRoomMemberCount(playing2, 4);
RoomJpaEntity playing3 = saveScienceRoom("진행중인방-책-P3", "pisbn3", "과학-방-3일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(3), 10);
changeRoomMemberCount(playing3, 5);
RoomJpaEntity playing4 = saveScienceRoom("진행중인방-책-P4", "pisbn4", "과학-방-4일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(4), 10);
changeRoomMemberCount(playing4, 6);
RoomJpaEntity playing5 = saveScienceRoom("진행중인방-책-P5", "pisbn5", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(5), 10);
changeRoomMemberCount(playing5, 7);
RoomJpaEntity playing6 = saveScienceRoom("진행중인방-책-P6", "pisbn6", "과학-방-6일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(6), 10);
changeRoomMemberCount(playing6, 8);

// 모집중인 방 6개 (startDate 임박 순서: +1d ~ +6d)
RoomJpaEntity recruiting1 = saveScienceRoom("모집중인방-책-R1", "risbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruiting1, 3);
RoomJpaEntity recruiting2 = saveScienceRoom("모집중인방-책-R2", "risbn2", "과학-방-2일뒤-활동시작", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruiting2, 4);
RoomJpaEntity recruiting3 = saveScienceRoom("모집중인방-책-R3", "risbn3", "과학-방-3일뒤-활동시작", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruiting3, 5);
RoomJpaEntity recruiting4 = saveScienceRoom("모집중인방-책-R4", "risbn4", "과학-방-4일뒤-활동시작", LocalDate.now().plusDays(4), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruiting4, 6);
RoomJpaEntity recruiting5 = saveScienceRoom("모집중인방-책-R5", "risbn5", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruiting5, 7);
RoomJpaEntity recruiting6 = saveScienceRoom("모집중인방-책-R6", "risbn6", "과학-방-6일뒤-활동시작", LocalDate.now().plusDays(6), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruiting6, 8);

Alias alias = TestEntityFactory.createScienceAlias();
UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(alias));

// 유저 참여
saveSingleUserToRoom(playing1, user);
saveSingleUserToRoom(playing2, user);
saveSingleUserToRoom(playing3, user);
saveSingleUserToRoom(playing4, user);
saveSingleUserToRoom(playing5, user);
saveSingleUserToRoom(playing6, user);
saveSingleUserToRoom(recruiting1, user);
saveSingleUserToRoom(recruiting2, user);
saveSingleUserToRoom(recruiting3, user);
saveSingleUserToRoom(recruiting4, user);
saveSingleUserToRoom(recruiting5, user);
saveSingleUserToRoom(recruiting6, user);

// when: 첫 페이지 (type 파라미터 없음 -> 혼합 조회)
ResultActions page1 = mockMvc.perform(get("/rooms/my")
.requestAttr("userId", user.getUserId()));

// then: 첫 페이지는 10개, 진행중(6) 먼저, 이후 모집중(4)
page1.andExpect(status().isOk())
.andExpect(jsonPath("$.data.isLast", is(false)))
.andExpect(jsonPath("$.data.roomList", hasSize(10)))
// 진행중 6개 (endDate 임박순)
.andExpect(jsonPath("$.data.roomList[0].roomName", is("과학-방-1일뒤-활동마감")))
.andExpect(jsonPath("$.data.roomList[1].roomName", is("과학-방-2일뒤-활동마감")))
.andExpect(jsonPath("$.data.roomList[2].roomName", is("과학-방-3일뒤-활동마감")))
.andExpect(jsonPath("$.data.roomList[3].roomName", is("과학-방-4일뒤-활동마감")))
.andExpect(jsonPath("$.data.roomList[4].roomName", is("과학-방-5일뒤-활동마감")))
.andExpect(jsonPath("$.data.roomList[5].roomName", is("과학-방-6일뒤-활동마감")))
// 이어서 모집중 4개 (startDate 임박순)
.andExpect(jsonPath("$.data.roomList[6].roomName", is("과학-방-1일뒤-활동시작")))
.andExpect(jsonPath("$.data.roomList[7].roomName", is("과학-방-2일뒤-활동시작")))
.andExpect(jsonPath("$.data.roomList[8].roomName", is("과학-방-3일뒤-활동시작")))
.andExpect(jsonPath("$.data.roomList[9].roomName", is("과학-방-4일뒤-활동시작")));

// 다음 페이지 커서: 첫 페이지의 마지막 레코드 = recruiting4
// 혼합 커서 형식 = priority|deadlineDate|roomId (priority: 진행=0, 모집=1; deadlineDate: 진행=endDate, 모집=startDate)
String nextCursor = "1|" + recruiting4.getStartDate() + "|" + recruiting4.getRoomId();

// when: 두 번째 페이지
ResultActions page2 = mockMvc.perform(get("/rooms/my")
.requestAttr("userId", user.getUserId())
.param("cursor", nextCursor));

// then: 남은 모집중 2개, isLast=true, 중복/누락 없음
page2.andExpect(status().isOk())
.andExpect(jsonPath("$.data.isLast", is(true)))
.andExpect(jsonPath("$.data.roomList", hasSize(2)))
.andExpect(jsonPath("$.data.roomList[0].roomName", is("과학-방-5일뒤-활동시작")))
.andExpect(jsonPath("$.data.roomList[1].roomName", is("과학-방-6일뒤-활동시작")));
}
}