diff --git a/build.gradle b/build.gradle index 3dfc1442..9ed3a6e6 100644 --- a/build.gradle +++ b/build.gradle @@ -43,8 +43,18 @@ dependencies { // AWS implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.4' implementation 'io.awspring.cloud:spring-cloud-starter-aws-secrets-manager-config:2.4.4' + + //Querydsl 추가 + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" } tasks.named('test') { useJUnitPlatform() } + +clean { + delete file('src/main/generated') +} diff --git a/src/main/java/com/listywave/common/config/EnumMappingConfig.java b/src/main/java/com/listywave/common/config/EnumMappingConfig.java new file mode 100644 index 00000000..653040f3 --- /dev/null +++ b/src/main/java/com/listywave/common/config/EnumMappingConfig.java @@ -0,0 +1,15 @@ +package com.listywave.common.config; + +import org.springframework.boot.convert.ApplicationConversionService; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class EnumMappingConfig implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + ApplicationConversionService.configure(registry); + } +} diff --git a/src/main/java/com/listywave/common/config/QuerydslConfig.java b/src/main/java/com/listywave/common/config/QuerydslConfig.java new file mode 100644 index 00000000..1967c990 --- /dev/null +++ b/src/main/java/com/listywave/common/config/QuerydslConfig.java @@ -0,0 +1,15 @@ +package com.listywave.common.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + + @Bean + JPAQueryFactory jpaQueryFactory(EntityManager em){ + return new JPAQueryFactory(em); + } +} diff --git a/src/main/java/com/listywave/list/application/domain/CategoryType.java b/src/main/java/com/listywave/list/application/domain/CategoryType.java index c0d9a411..b12260f1 100644 --- a/src/main/java/com/listywave/list/application/domain/CategoryType.java +++ b/src/main/java/com/listywave/list/application/domain/CategoryType.java @@ -1,5 +1,8 @@ package com.listywave.list.application.domain; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.listywave.common.exception.CustomException; +import com.listywave.common.exception.ErrorCode; import java.util.Arrays; import lombok.AllArgsConstructor; import lombok.Getter; @@ -8,24 +11,32 @@ @AllArgsConstructor public enum CategoryType { - ENTIRE("1","ENTIRE","전체"), - CULTURE("2","CULTURE","문화"), - LIFE("3","LIFE","일상생활"), - PLACE("4","PLACE","장소"), - MUSIC("5","MUSIC","음악"), - MOVIE_DRAMA("6","MOVIE_DRAMA","영화/드라마"), - BOOK("7","BOOK","도서"), - ANIMAL_PLANT("8","ANIMAL_PLANT","동식물"), - ETC("9","ETC","기타"), + ENTIRE("0", "전체"), + CULTURE("1", "문화"), + LIFE("2", "일상생활"), + PLACE("3", "장소"), + MUSIC("4", "음악"), + MOVIE_DRAMA("5", "영화/드라마"), + BOOK("6", "도서"), + ANIMAL_PLANT("7", "동식물"), + ETC("8", "기타"), ; private final String codeValue; - private final String nameValue; private final String korNameValue; public static CategoryType enumOf(String codeValue) { return Arrays.stream(CategoryType.values()) - .filter( t -> t.getCodeValue().equals(codeValue)) - .findAny().orElse(null); + .filter(t -> t.getCodeValue().equals(codeValue)) + .findAny() + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND, "해당 카테고리코드는 존재하지 않습니다.")); + } + + @JsonCreator + public static CategoryType fromString(String key) { + return Arrays.stream(CategoryType.values()) + .filter(categoryType -> categoryType.name().equalsIgnoreCase(key)) + .findFirst() + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND, "해당 카테고리는 존재하지 않습니다.")); } } diff --git a/src/main/java/com/listywave/list/application/service/ListService.java b/src/main/java/com/listywave/list/application/service/ListService.java index 9f7b31bb..3b039d71 100644 --- a/src/main/java/com/listywave/list/application/service/ListService.java +++ b/src/main/java/com/listywave/list/application/service/ListService.java @@ -40,7 +40,7 @@ public ListCreateResponse listCreate( Boolean isLabels = isLabelCountValid(labels); validateItemsCount(items); - Boolean hasCollaboratorId = hascollaboratorExistence(collaboratorIds); + Boolean hasCollaboratorId = isExistCollaborator(collaboratorIds); Lists list = Lists.createList( user, @@ -79,7 +79,10 @@ private List findExistingCollaborators(List collaboratorIds) { return existingCollaborators; } - private Boolean hascollaboratorExistence(List collaboratorIds) { + private Boolean isExistCollaborator(List collaboratorIds) { + if (collaboratorIds != null && collaboratorIds.size() > 20) { + throw new CustomException(ErrorCode.INVALID_COUNT, "콜라보레이터는 최대 20명까지 가능합니다."); + } return collaboratorIds != null && !collaboratorIds.isEmpty(); } diff --git a/src/main/java/com/listywave/list/presentation/dto/response/CategoryTypeResponse.java b/src/main/java/com/listywave/list/presentation/dto/response/CategoryTypeResponse.java index 6aa0b27a..72151a49 100644 --- a/src/main/java/com/listywave/list/presentation/dto/response/CategoryTypeResponse.java +++ b/src/main/java/com/listywave/list/presentation/dto/response/CategoryTypeResponse.java @@ -11,7 +11,7 @@ public record CategoryTypeResponse( public static CategoryTypeResponse fromEnum(CategoryType categoryType) { return new CategoryTypeResponse( categoryType.getCodeValue(), - categoryType.getNameValue(), + categoryType.name().toLowerCase(), categoryType.getKorNameValue() ); } diff --git a/src/main/java/com/listywave/user/application/dto/FeedListsResponse.java b/src/main/java/com/listywave/user/application/dto/FeedListsResponse.java new file mode 100644 index 00000000..c87b5ee3 --- /dev/null +++ b/src/main/java/com/listywave/user/application/dto/FeedListsResponse.java @@ -0,0 +1,47 @@ +package com.listywave.user.application.dto; + +import com.listywave.list.application.domain.Item; +import com.listywave.list.application.domain.Lists; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Builder; + +@Builder +public record FeedListsResponse( + Long id, + String title, + Boolean isPublic, + List listItems +) { + public static FeedListsResponse of(Lists lists) { + return FeedListsResponse.builder() + .id(lists.getId()) + .title(lists.getTitle()) + .isPublic(lists.isPublic()) + .listItems(ListItemsResponse.toList(lists)) + .build(); + } +} + +@Builder +record ListItemsResponse( + Long id, + int ranking, + String title, + String imageUrl +) { + public static ListItemsResponse of(Item item) { + return ListItemsResponse.builder() + .id(item.getId()) + .ranking(item.getRanking()) + .title(item.getTitle()) + .imageUrl(item.getImageUrl()) + .build(); + } + + public static List toList(Lists lists) { + return lists.getItems().stream() + .map(ListItemsResponse::of) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/listywave/user/application/service/UserService.java b/src/main/java/com/listywave/user/application/service/UserService.java index ce8ce3f9..b08e0318 100644 --- a/src/main/java/com/listywave/user/application/service/UserService.java +++ b/src/main/java/com/listywave/user/application/service/UserService.java @@ -4,9 +4,13 @@ import com.listywave.auth.application.domain.JwtManager; import com.listywave.common.exception.CustomException; +import com.listywave.common.util.UserUtil; +import com.listywave.list.application.domain.CategoryType; +import com.listywave.list.application.domain.Lists; import com.listywave.user.application.domain.User; import com.listywave.user.application.dto.AllUserResponse; import com.listywave.user.application.dto.UserInfoResponse; +import com.listywave.user.presentation.dto.response.AllUserListsResponse; import com.listywave.user.repository.UserRepository; import java.util.List; import java.util.Optional; @@ -19,6 +23,7 @@ public class UserService { private final JwtManager jwtManager; + private final UserUtil userUtil; private final UserRepository userRepository; @Transactional(readOnly = true) @@ -37,10 +42,34 @@ public UserInfoResponse getUserInfo(Long userId, String accessToken) { return UserInfoResponse.of(foundUser, false, false); } - private static boolean isSignedIn(String accessToken) { + + private boolean isSignedIn(String accessToken) { return accessToken.isBlank(); } + @Transactional(readOnly = true) + public AllUserListsResponse getAllListOfUser( + Long userId, + String type, + CategoryType category, + Long cursorId, + int size + ) { + userUtil.getUserByUserid(userId); + + List feedList = userRepository.findFeedLists(userId, type, category, cursorId, size); + + boolean hasNext = false; + cursorId = null; + + if (feedList.size() == size + 1) { + feedList.remove(size); + hasNext = true; + cursorId = feedList.get(feedList.size() - 1).getId(); + } + return AllUserListsResponse.of(hasNext, cursorId, feedList); + } + @Transactional(readOnly = true) public AllUserResponse getAllUser() { List allUser = userRepository.findAll(); diff --git a/src/main/java/com/listywave/user/presentation/UserController.java b/src/main/java/com/listywave/user/presentation/UserController.java index 2f2bbe12..6321d67e 100644 --- a/src/main/java/com/listywave/user/presentation/UserController.java +++ b/src/main/java/com/listywave/user/presentation/UserController.java @@ -1,13 +1,16 @@ package com.listywave.user.presentation; +import com.listywave.list.application.domain.CategoryType; import com.listywave.user.application.dto.AllUserResponse; import com.listywave.user.application.dto.UserInfoResponse; import com.listywave.user.application.service.UserService; +import com.listywave.user.presentation.dto.response.AllUserListsResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -30,4 +33,16 @@ ResponseEntity getAllUser() { AllUserResponse allUserResponse = userService.getAllUser(); return ResponseEntity.ok(allUserResponse); } + + @GetMapping("/users/{userId}/lists") + ResponseEntity getAllUserLists( + @PathVariable(name = "userId") Long userId, + @RequestParam(name = "type", defaultValue = "my") String type, + @RequestParam(name = "category", defaultValue = "entire") CategoryType category, + @RequestParam(name = "cursorId", required = false) Long cursorId, + @RequestParam(name = "size") int size + ) { + AllUserListsResponse allUserListsResponse = userService.getAllListOfUser(userId, type, category, cursorId, size); + return ResponseEntity.ok(allUserListsResponse); + } } diff --git a/src/main/java/com/listywave/user/presentation/dto/response/AllUserListsResponse.java b/src/main/java/com/listywave/user/presentation/dto/response/AllUserListsResponse.java new file mode 100644 index 00000000..33323c21 --- /dev/null +++ b/src/main/java/com/listywave/user/presentation/dto/response/AllUserListsResponse.java @@ -0,0 +1,25 @@ +package com.listywave.user.presentation.dto.response; + +import com.listywave.list.application.domain.Lists; +import com.listywave.user.application.dto.FeedListsResponse; +import java.util.List; + +public record AllUserListsResponse( + Long cursorId, + Boolean hasNext, + List feedLists +) { + public static AllUserListsResponse of(boolean hasNext, Long cursorId, List feedLists) { + return new AllUserListsResponse( + cursorId, + hasNext, + toList(feedLists) + ); + } + + public static List toList(List feedLists) { + return feedLists.stream() + .map(FeedListsResponse::of) + .toList(); + } +} diff --git a/src/main/java/com/listywave/user/repository/UserRepository.java b/src/main/java/com/listywave/user/repository/UserRepository.java index aded1908..9240d215 100644 --- a/src/main/java/com/listywave/user/repository/UserRepository.java +++ b/src/main/java/com/listywave/user/repository/UserRepository.java @@ -1,10 +1,11 @@ package com.listywave.user.repository; import com.listywave.user.application.domain.User; +import com.listywave.user.repository.custom.UserRepositoryCustom; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -public interface UserRepository extends JpaRepository { +public interface UserRepository extends JpaRepository, UserRepositoryCustom { Optional findByOauthId(Long oauthId); } diff --git a/src/main/java/com/listywave/user/repository/custom/UserRepositoryCustom.java b/src/main/java/com/listywave/user/repository/custom/UserRepositoryCustom.java new file mode 100644 index 00000000..c27d43e8 --- /dev/null +++ b/src/main/java/com/listywave/user/repository/custom/UserRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.listywave.user.repository.custom; + +import com.listywave.list.application.domain.CategoryType; +import com.listywave.list.application.domain.Lists; +import java.util.List; + +public interface UserRepositoryCustom { + + List findFeedLists(Long userId, String type, CategoryType category, Long cursorId, int size); +} diff --git a/src/main/java/com/listywave/user/repository/custom/impl/UserRepositoryImpl.java b/src/main/java/com/listywave/user/repository/custom/impl/UserRepositoryImpl.java new file mode 100644 index 00000000..048d52b3 --- /dev/null +++ b/src/main/java/com/listywave/user/repository/custom/impl/UserRepositoryImpl.java @@ -0,0 +1,59 @@ +package com.listywave.user.repository.custom.impl; + +import static com.listywave.list.application.domain.QItem.item; +import static com.listywave.list.application.domain.QLists.lists; + +import com.listywave.list.application.domain.CategoryType; +import com.listywave.list.application.domain.Lists; +import com.listywave.user.repository.custom.UserRepositoryCustom; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class UserRepositoryImpl implements UserRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public List findFeedLists(Long userId, String type, CategoryType category, Long cursorId, int size) { + List fetch = queryFactory + .select(lists) + .from(lists) + .leftJoin(lists.items, item) + .where( + userIdEq(userId), + typeEq(type), + categoryEq(category), + listIdGt(cursorId) + ) + .distinct() + .limit(size + 1) + .orderBy(lists.updatedDate.desc()) + .fetch(); + return fetch; + } + + private BooleanExpression listIdGt(Long cursorId) { + return cursorId == null ? null : lists.id.lt(cursorId); + } + + private BooleanExpression categoryEq(CategoryType categoryCode) { + if ("0".equals(categoryCode.getCodeValue())) { + return null; + } + return lists.category.eq(categoryCode); + } + + private BooleanExpression typeEq(String type) { + if (type.equals("my")) { + return lists.hasCollaboration.eq(false); + } + return lists.hasCollaboration.eq(true); + } + + private BooleanExpression userIdEq(Long userId) { + return userId == null ? null : lists.user.id.eq(userId); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 18c04f30..b03856bf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: datasource: - url: jdbc:h2:mem:testdb;MODE=MySQL + url: jdbc:h2:tcp://localhost/~/listywave;MODE=MySQL username: sa driver-class-name: org.h2.Driver jpa: @@ -11,6 +11,7 @@ spring: format_sql: true use_sql_comments: true highlight_sql: true + default_batch_fetch_size: 100 show-sql: true defer-datasource-initialization: true h2: @@ -28,6 +29,8 @@ logging: org.springframework.orm.jpa: DEBUG org.springframework.orm.transaction: DEBUG org.apache.coyote.http11: debug + org.hibernate.SQL: debug + org.hibernate.orm.jdbc.bind: trace # 쿼리 파라미터 로그 남기기 cloud: aws: