diff --git a/build.gradle b/build.gradle index 60b4f2b5..75db4f34 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,12 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' implementation 'mysql:mysql-connector-java:8.0.33' + + //QueryDSL 의존성 추가 + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" } tasks.named('test') { diff --git a/src/main/java/gaji/service/config/QueryDSLConfig.java b/src/main/java/gaji/service/config/QueryDSLConfig.java new file mode 100644 index 00000000..889731b5 --- /dev/null +++ b/src/main/java/gaji/service/config/QueryDSLConfig.java @@ -0,0 +1,19 @@ +package gaji.service.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QueryDSLConfig{ + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/gaji/service/config/SwaggerConfig.java b/src/main/java/gaji/service/config/SwaggerConfig.java index faa1e254..c398f614 100644 --- a/src/main/java/gaji/service/config/SwaggerConfig.java +++ b/src/main/java/gaji/service/config/SwaggerConfig.java @@ -36,5 +36,4 @@ private Info apiInfo() { .description("GAJI API 명세서입니다.") .version("1.0.0"); } - } \ No newline at end of file diff --git a/src/main/java/gaji/service/domain/common/annotation/CheckPage.java b/src/main/java/gaji/service/domain/common/annotation/CheckPage.java new file mode 100644 index 00000000..da951170 --- /dev/null +++ b/src/main/java/gaji/service/domain/common/annotation/CheckPage.java @@ -0,0 +1,17 @@ +package gaji.service.domain.common.annotation; + +import gaji.service.domain.common.validation.PageNumberValidator; +import jakarta.validation.Payload; +import jakarta.validation.Constraint; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = PageNumberValidator.class) +@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckPage { + String message() default "유효하지 않은 페이지 숫자 입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/gaji/service/domain/common/validation/PageNumberValidator.java b/src/main/java/gaji/service/domain/common/validation/PageNumberValidator.java new file mode 100644 index 00000000..24cdc980 --- /dev/null +++ b/src/main/java/gaji/service/domain/common/validation/PageNumberValidator.java @@ -0,0 +1,29 @@ +package gaji.service.domain.common.validation; + +import gaji.service.domain.common.annotation.CheckPage; +import gaji.service.global.exception.code.status.GlobalErrorStatus; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.springframework.stereotype.Component; + +@Component +public class PageNumberValidator implements ConstraintValidator { + + @Override + public void initialize(CheckPage constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + boolean isValid = value>=0; + + if (!isValid){ + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(GlobalErrorStatus._INTERNAL_PAGE_ERROR.toString()) + .addConstraintViolation(); + } + + return isValid; + } +} \ No newline at end of file diff --git a/src/main/java/gaji/service/domain/enums/RoomTypeEnum.java b/src/main/java/gaji/service/domain/enums/RoomTypeEnum.java new file mode 100644 index 00000000..33c4ab39 --- /dev/null +++ b/src/main/java/gaji/service/domain/enums/RoomTypeEnum.java @@ -0,0 +1,5 @@ +package gaji.service.domain.enums; + +public enum RoomTypeEnum { + ONGOING, ENDED +} diff --git a/src/main/java/gaji/service/domain/room/repository/RoomCustomRepository.java b/src/main/java/gaji/service/domain/room/repository/RoomCustomRepository.java new file mode 100644 index 00000000..aecc6e6a --- /dev/null +++ b/src/main/java/gaji/service/domain/room/repository/RoomCustomRepository.java @@ -0,0 +1,13 @@ +package gaji.service.domain.room.repository; + +import com.querydsl.core.Tuple; +import gaji.service.domain.user.entity.User; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import java.time.LocalDate; + +public interface RoomCustomRepository { + public Slice findAllOngoingRoomsByUser(User user, LocalDate cursorDate, Long cursorId, Pageable pageable); + public Slice findAllEndedRoomsByUser(User user, LocalDate cursorDate, Long cursorId, Pageable pageable); +} diff --git a/src/main/java/gaji/service/domain/room/repository/RoomCustomRepositoryImpl.java b/src/main/java/gaji/service/domain/room/repository/RoomCustomRepositoryImpl.java new file mode 100644 index 00000000..37767e24 --- /dev/null +++ b/src/main/java/gaji/service/domain/room/repository/RoomCustomRepositoryImpl.java @@ -0,0 +1,91 @@ +package gaji.service.domain.room.repository; + +import com.querydsl.core.Tuple; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import gaji.service.domain.room.entity.QRoom; +import gaji.service.domain.studyMate.QStudyMate; +import gaji.service.domain.user.entity.User; +import lombok.AllArgsConstructor; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; + +@Repository +@AllArgsConstructor +public class RoomCustomRepositoryImpl implements RoomCustomRepository { + private final JPAQueryFactory jpaQueryFactory; + + private final QRoom room = QRoom.room; + private final QStudyMate studyMate = QStudyMate.studyMate; + + public Slice findAllOngoingRoomsByUser(User user, LocalDate cursorDate, Long cursorId, Pageable pageable) { + List userRoomIds = jpaQueryFactory + .select(studyMate.room.id) + .from(studyMate) + .where(studyMate.user.eq(user)) + .fetch(); + + List ongoingRooms = jpaQueryFactory.select(room.id, room.name, room.description, room.thumbnailUrl, room.studyStartDay) + .from(room) + .where(room.id.in(userRoomIds) + .and(room.studyEndDay.after(getCurrentDay())) + .and(getCursorCondition(cursorDate, cursorId))) + .orderBy(room.studyStartDay.desc(), room.id.asc()) + .limit(pageable.getPageSize()+1) // size보다 1개 더 가져와서 다음 페이지 여부 확인 + .fetch(); + + return checkLastPage(pageable, ongoingRooms); + } + + + public Slice findAllEndedRoomsByUser(User user, LocalDate cursorDate, Long cursorId, Pageable pageable) { + LocalDate now = LocalDate.now(); + + BooleanExpression cursorCondition = (room.studyStartDay.eq(cursorDate).and(room.id.gt(cursorId))) + .or(room.studyStartDay.lt(cursorDate)); + + List userRoomIds = jpaQueryFactory + .select(studyMate.room.id) + .from(studyMate) + .where(studyMate.user.eq(user)) + .fetch(); + + List ongoingRooms = jpaQueryFactory.select(room.id, room.name, room.description, room.thumbnailUrl, room.studyStartDay) + .from(room) + .where(room.id.in(userRoomIds) + .and(room.studyEndDay.before(getCurrentDay())) + .and(getCursorCondition(cursorDate, cursorId))) + .orderBy(room.studyStartDay.desc(), room.id.asc()) + .limit(pageable.getPageSize()+1) // size보다 1개 더 가져와서 다음 페이지 여부 확인 + .fetch(); + + return checkLastPage(pageable, ongoingRooms); + } + + + private Slice checkLastPage(Pageable pageable, List roomList) { + boolean hasNext = false; + + if (roomList.size() > pageable.getPageSize()) { + hasNext = true; + roomList.remove(pageable.getPageSize()); // 더 가져왔을 시, 삭제 + } + return new SliceImpl(roomList, pageable, hasNext); + } + + private BooleanExpression getCursorCondition(LocalDate cursorDate, Long cursorId) { + return (room.studyStartDay.eq(cursorDate).and(room.id.gt(cursorId))) + .or(room.studyStartDay.lt(cursorDate)); + } + + private LocalDate getCurrentDay() { + return LocalDate.now(); + } + +} diff --git a/src/main/java/gaji/service/domain/user/converter/UserConverter.java b/src/main/java/gaji/service/domain/user/converter/UserConverter.java index 297a1f39..54b8936e 100644 --- a/src/main/java/gaji/service/domain/user/converter/UserConverter.java +++ b/src/main/java/gaji/service/domain/user/converter/UserConverter.java @@ -1,8 +1,13 @@ package gaji.service.domain.user.converter; -import gaji.service.domain.enums.UserActive; +import com.querydsl.core.Tuple; +import gaji.service.domain.room.entity.QRoom; import gaji.service.domain.user.entity.User; import gaji.service.domain.user.web.dto.UserResponseDTO; +import org.springframework.data.domain.Slice; + +import java.util.List; +import java.util.stream.Collectors; public class UserConverter { public static UserResponseDTO.CancleResultDTO toCancleResultDTO(User user) { @@ -19,4 +24,24 @@ public static UserResponseDTO.UpdateNicknameResultDTO toUpdateNicknameResultDTO( .build(); } + public static UserResponseDTO.GetRoomDTO toGetRoomDTO(Tuple tuple) { + return UserResponseDTO.GetRoomDTO.builder() + .roomId(tuple.get(QRoom.room.id)) + .name(tuple.get(QRoom.room.name)) + .description(tuple.get(QRoom.room.description)) + .thumbnail_url(tuple.get(QRoom.room.thumbnailUrl)) + .studyStartDay(tuple.get(QRoom.room.studyStartDay)) + .build(); + } + + public static UserResponseDTO.GetRoomListDTO toGetRoomListDTO(Slice roomList) { + List getRoomDTOList = roomList.stream() + .map(UserConverter::toGetRoomDTO) + .collect(Collectors.toList()); + + return UserResponseDTO.GetRoomListDTO.builder() + .roomList(getRoomDTOList) + .hasNext(roomList.hasNext()) + .build(); + } } diff --git a/src/main/java/gaji/service/domain/user/service/UserQueryService.java b/src/main/java/gaji/service/domain/user/service/UserQueryService.java index e0a19691..14dea5eb 100644 --- a/src/main/java/gaji/service/domain/user/service/UserQueryService.java +++ b/src/main/java/gaji/service/domain/user/service/UserQueryService.java @@ -1,9 +1,17 @@ package gaji.service.domain.user.service; + +import com.querydsl.core.Tuple; +import gaji.service.domain.enums.RoomTypeEnum; import gaji.service.domain.user.entity.User; +import org.springframework.data.domain.Slice; + +import java.time.LocalDate; public interface UserQueryService { boolean existUserById(Long userId); User findUserById(Long userId); + Slice getUserRoomList(Long userId, LocalDate cursorDate, Long cursorId, RoomTypeEnum type, int size); + } diff --git a/src/main/java/gaji/service/domain/user/service/UserQueryServiceImpl.java b/src/main/java/gaji/service/domain/user/service/UserQueryServiceImpl.java index 42af0378..5dabc569 100644 --- a/src/main/java/gaji/service/domain/user/service/UserQueryServiceImpl.java +++ b/src/main/java/gaji/service/domain/user/service/UserQueryServiceImpl.java @@ -1,13 +1,19 @@ package gaji.service.domain.user.service; +import com.querydsl.core.Tuple; +import gaji.service.domain.enums.RoomTypeEnum; +import gaji.service.domain.room.repository.RoomCustomRepository; import gaji.service.domain.user.code.UserErrorStatus; import gaji.service.domain.user.entity.User; import gaji.service.domain.user.repository.UserRepository; import gaji.service.global.exception.RestApiException; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; @Service @RequiredArgsConstructor @@ -15,6 +21,7 @@ public class UserQueryServiceImpl implements UserQueryService { private final UserRepository userRepository; + private final RoomCustomRepository roomCustomRepository; @Override public boolean existUserById(Long userId) { @@ -26,4 +33,26 @@ public User findUserById(Long userId) { return userRepository.findById(userId) .orElseThrow(() -> new RestApiException(UserErrorStatus._USER_NOT_FOUND)); } + + @Override + public Slice getUserRoomList(Long userId, LocalDate cursorDate, Long cursorId, RoomTypeEnum type, int size) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new RestApiException(UserErrorStatus._USER_NOT_FOUND)); + + cursorDate = cursorDate == null ? LocalDate.now() : cursorDate; + cursorId = cursorId == null ? 0 : cursorId; + + PageRequest pageRequest = PageRequest.of(0, size); + + Slice roomList; + + if(type == RoomTypeEnum.ONGOING) { + roomList = roomCustomRepository.findAllOngoingRoomsByUser(user, cursorDate, cursorId, pageRequest); + } + else{ + roomList = roomCustomRepository.findAllEndedRoomsByUser(user, cursorDate, cursorId, pageRequest); + } + + return roomList; + } } diff --git a/src/main/java/gaji/service/domain/user/web/controller/UserRestController.java b/src/main/java/gaji/service/domain/user/web/controller/UserRestController.java index 2425c82b..80a9b2f1 100644 --- a/src/main/java/gaji/service/domain/user/web/controller/UserRestController.java +++ b/src/main/java/gaji/service/domain/user/web/controller/UserRestController.java @@ -1,8 +1,7 @@ package gaji.service.domain.user.web.controller; -import gaji.service.domain.enums.PostTypeEnum; -import gaji.service.domain.post.entity.Post; -import gaji.service.domain.user.code.UserErrorStatus; +import com.querydsl.core.Tuple; +import gaji.service.domain.enums.RoomTypeEnum; import gaji.service.domain.user.converter.UserConverter; import gaji.service.domain.user.entity.User; import gaji.service.domain.user.service.UserCommandService; @@ -10,13 +9,13 @@ import gaji.service.domain.user.web.dto.UserRequestDTO; import gaji.service.domain.user.web.dto.UserResponseDTO; import gaji.service.global.base.BaseResponse; -import gaji.service.global.exception.RestApiException; import gaji.service.jwt.service.TokenProviderService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; import org.springframework.web.bind.annotation.*; -import java.util.List; +import java.time.LocalDate; @RestController @RequestMapping("/api/users") @@ -45,5 +44,27 @@ public BaseResponse updateNickname(@Req User user = userCommandService.updateUserNickname(userIdFromToken, userIdFromPathVariable, request); return BaseResponse.onSuccess(UserConverter.toUpdateNicknameResultDTO(user)); } + + @GetMapping("/rooms") + public BaseResponse getMyRoomList(@RequestHeader("Authorization") String authorizationHeader, + @RequestParam(value = "cursorDate",required = false) LocalDate cursorDate, + @RequestParam(value = "cursorId",required = false) Long cursorId, + @RequestParam("type") RoomTypeEnum type, + @RequestParam(defaultValue = "10") int size) { + Long userId = tokenProviderService.getUserIdFromToken(authorizationHeader); + Slice userRoomList = userQueryService.getUserRoomList(userId, cursorDate, cursorId, type, size); + return BaseResponse.onSuccess(UserConverter.toGetRoomListDTO(userRoomList)); + } + + @GetMapping("/rooms/{userId}") + public BaseResponse getUserRoomList(@PathVariable Long userId, + @RequestParam(value = "cursorDate",required = false) LocalDate cursorDate, + @RequestParam(value = "cursorId",required = false) Long cursorId, + @RequestParam("type") RoomTypeEnum type, + @RequestParam(defaultValue = "10") int size) { + Slice userRoomList = userQueryService.getUserRoomList(userId, cursorDate, cursorId, type, size); + return BaseResponse.onSuccess(UserConverter.toGetRoomListDTO(userRoomList)); + } + } diff --git a/src/main/java/gaji/service/domain/user/web/dto/UserResponseDTO.java b/src/main/java/gaji/service/domain/user/web/dto/UserResponseDTO.java index fdf9a76e..51bed26d 100644 --- a/src/main/java/gaji/service/domain/user/web/dto/UserResponseDTO.java +++ b/src/main/java/gaji/service/domain/user/web/dto/UserResponseDTO.java @@ -7,6 +7,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDate; import java.util.List; public class UserResponseDTO { @@ -37,6 +38,27 @@ public static class UpdateNicknameResultDTO { Long userId; String nickname; } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class GetRoomDTO { + Long roomId; + String name; + String description; + String thumbnail_url; + LocalDate studyStartDay; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class GetRoomListDTO { + List roomList; + boolean hasNext; + } }