Skip to content

Commit

Permalink
Merge pull request #191 from side-peek/feat/#173-read-projects-with-s…
Browse files Browse the repository at this point in the history
…earch

프로젝트 전체 조회 시 기술 스택 및 제목과 멤버로 검색 기능
  • Loading branch information
yenzip authored Mar 18, 2024
2 parents 71a2747 + 0a3c054 commit b5cebf2
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import sixgaezzang.sidepeek.common.exception.ErrorResponse;
import sixgaezzang.sidepeek.projects.dto.request.CursorPaginationInfoRequest;
import sixgaezzang.sidepeek.projects.dto.request.FindProjectRequest;
import sixgaezzang.sidepeek.projects.dto.request.SaveProjectRequest;
import sixgaezzang.sidepeek.projects.dto.request.UpdateProjectRequest;
import sixgaezzang.sidepeek.projects.dto.response.CursorPaginationResponse;
Expand Down Expand Up @@ -47,7 +47,7 @@ ResponseEntity<ProjectResponse> save(@Parameter(hidden = true) Long loginId,
})
ResponseEntity<CursorPaginationResponse<ProjectListResponse>> getByCondition(
@Parameter(hidden = true) Long loginId,
@Valid @ModelAttribute CursorPaginationInfoRequest pageable);
@Valid @ModelAttribute FindProjectRequest pageable);

@Operation(summary = "지난 주 인기 프로젝트 조회", description = "지난 주 좋아요를 많이 받은 순으로 최대 5개 프로젝트 목록 조회, 로그인 선택")
@ApiResponses({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ProjectDescription {
// CursorPaginationInfoRequest

// FindProjectRequest
public static final String IS_RELEASED_DESCRIPTION = "출시 서비스만 보기(기본 - false)";
public static final String SORT_DESCRIPTION = "정렬 조건 [ createdAt(default), view, like ]";
public static final String PAGE_SIZE_DESCRIPTION = "한 페이지내 보여질 데이터의 개수";
public static final String LAST_ORDER_COUNT_DESCRIPTION = "더보기 이전 마지막으로 보여진 좋아요수/조회수(첫 페이지면 null)";
public static final String LAST_PROJECT_ID_DESCRIPTION = "더보기 이전 마지막으로 보여진 프로젝트 식별자(첫 페이지면 null)";
public static final String SKILL_DESCRIPTION = "조회할 기술 스택 목록(없으면 null)";
public static final String SEARCH_DESCRIPTION = "검색어 [ 프로젝트 제목, 멤버 ](없으면 null)";

// SaveProjectRequest, UpdateProjectRequest
public static final String NAME_DESCRIPTION = "제목, " + MAX_PROJECT_NAME_LENGTH + "자 이하";
Expand All @@ -43,6 +46,7 @@ public final class ProjectDescription {

// SaveMemberRequest
public static final String MEMBER_ROLE_DESCRIPTION = "멤버 역할, " + MAX_ROLE_LENGTH + "자 이하";
public static final String MEMBER_NICKNAME_DESCRIPTION = "멤버 닉네임, 회원도 설정 가능, " + MAX_NICKNAME_LENGTH + "자 이하";
public static final String MEMBER_NICKNAME_DESCRIPTION =
"멤버 닉네임, 회원도 설정 가능, " + MAX_NICKNAME_LENGTH + "자 이하";
public static final String MEMBER_USER_ID_DESCRIPTION = "회원 멤버 유저 식별자(비회원 멤버이면 null)";
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import sixgaezzang.sidepeek.common.annotation.Login;
import sixgaezzang.sidepeek.common.doc.ProjectControllerDoc;
import sixgaezzang.sidepeek.projects.dto.request.CursorPaginationInfoRequest;
import sixgaezzang.sidepeek.projects.dto.request.FindProjectRequest;
import sixgaezzang.sidepeek.projects.dto.request.SaveProjectRequest;
import sixgaezzang.sidepeek.projects.dto.request.UpdateProjectRequest;
import sixgaezzang.sidepeek.projects.dto.response.CursorPaginationResponse;
Expand Down Expand Up @@ -63,10 +63,10 @@ public ResponseEntity<ProjectResponse> getById(
@GetMapping
public ResponseEntity<CursorPaginationResponse<ProjectListResponse>> getByCondition(
@Login Long loginId,
@Valid @ModelAttribute CursorPaginationInfoRequest pageable
@Valid @ModelAttribute FindProjectRequest request
) {
CursorPaginationResponse<ProjectListResponse> responses = projectService.findByCondition(
loginId, pageable);
loginId, request);
return ResponseEntity.ok().body(responses);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@
import static sixgaezzang.sidepeek.common.doc.description.ProjectDescription.LAST_ORDER_COUNT_DESCRIPTION;
import static sixgaezzang.sidepeek.common.doc.description.ProjectDescription.LAST_PROJECT_ID_DESCRIPTION;
import static sixgaezzang.sidepeek.common.doc.description.ProjectDescription.PAGE_SIZE_DESCRIPTION;
import static sixgaezzang.sidepeek.common.doc.description.ProjectDescription.SEARCH_DESCRIPTION;
import static sixgaezzang.sidepeek.common.doc.description.ProjectDescription.SKILL_DESCRIPTION;
import static sixgaezzang.sidepeek.common.doc.description.ProjectDescription.SORT_DESCRIPTION;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.annotation.Nullable;
import java.util.List;
import lombok.Builder;

@Schema(description = "프로젝트 조회 시 필터 및 페이지네이션 정보")
@Builder
public record CursorPaginationInfoRequest(

public record FindProjectRequest(
// Cursor Based Pagination
@Schema(description = LAST_PROJECT_ID_DESCRIPTION)
@Nullable
Long lastProjectId,
Expand All @@ -26,22 +29,35 @@ public record CursorPaginationInfoRequest(
@Nullable
Integer pageSize,

// Sort
@Schema(description = SORT_DESCRIPTION)
@Nullable
SortType sort,

// Filter
@Schema(description = IS_RELEASED_DESCRIPTION)
@Nullable
Boolean isReleased
Boolean isReleased,

@Schema(description = SKILL_DESCRIPTION)
@Nullable
List<String> skill,

@Schema(description = SEARCH_DESCRIPTION)
@Nullable
String search
) {
public CursorPaginationInfoRequest(@Nullable Long lastProjectId, Long lastOrderCount,
Integer pageSize, SortType sort,
Boolean isReleased) {

public FindProjectRequest(Long lastProjectId, Long lastOrderCount,
Integer pageSize, SortType sort,
Boolean isReleased, List<String> skill, String search) {
this.lastProjectId = lastProjectId;
this.lastOrderCount = lastOrderCount;
this.pageSize = (pageSize != null) ? pageSize : 24;
this.sort = (sort != null) ? sort : SortType.createdAt;
this.isReleased = (isReleased != null) ? isReleased : false;
this.skill = skill;
this.search = search;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import sixgaezzang.sidepeek.projects.dto.request.CursorPaginationInfoRequest;
import sixgaezzang.sidepeek.projects.dto.request.FindProjectRequest;
import sixgaezzang.sidepeek.projects.dto.response.CursorPaginationResponse;
import sixgaezzang.sidepeek.projects.dto.response.ProjectBannerResponse;
import sixgaezzang.sidepeek.projects.dto.response.ProjectListResponse;
Expand All @@ -14,16 +14,17 @@ public interface ProjectRepositoryCustom {

CursorPaginationResponse<ProjectListResponse> findByCondition(
List<Long> likedProjectIds,
CursorPaginationInfoRequest pageable);
FindProjectRequest request);

List<ProjectBannerResponse> findAllPopularOfPeriod(LocalDate startDate, LocalDate endDate, int count);
List<ProjectBannerResponse> findAllPopularOfPeriod(LocalDate startDate, LocalDate endDate,
int count);

Page<ProjectListResponse> findAllByUserJoined(List<Long> likedProjectIds, User user,
Pageable pageable);
Pageable pageable);

Page<ProjectListResponse> findAllByUserLiked(List<Long> likedProjectIds, User user,
Pageable pageable);
Pageable pageable);

Page<ProjectListResponse> findAllByUserCommented(List<Long> likedProjectIds, User user,
Pageable pageable);
Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,27 @@
import static sixgaezzang.sidepeek.comments.domain.QComment.comment;
import static sixgaezzang.sidepeek.like.domain.QLike.like;
import static sixgaezzang.sidepeek.projects.domain.QProject.project;
import static sixgaezzang.sidepeek.projects.domain.QProjectSkill.projectSkill;
import static sixgaezzang.sidepeek.projects.domain.member.QMember.member;

import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.DateTemplate;
import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberTemplate;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import sixgaezzang.sidepeek.projects.domain.Project;
import sixgaezzang.sidepeek.projects.domain.QProject;
import sixgaezzang.sidepeek.projects.dto.request.CursorPaginationInfoRequest;
import sixgaezzang.sidepeek.projects.dto.request.FindProjectRequest;
import sixgaezzang.sidepeek.projects.dto.request.SortType;
import sixgaezzang.sidepeek.projects.dto.response.CursorPaginationResponse;
import sixgaezzang.sidepeek.projects.dto.response.ProjectBannerResponse;
Expand All @@ -40,58 +42,83 @@ public ProjectRepositoryCustomImpl(EntityManager em) {
@Override
public CursorPaginationResponse<ProjectListResponse> findByCondition(
List<Long> likedProjectIds,
CursorPaginationInfoRequest pageable) {
FindProjectRequest request) {
// where
BooleanExpression deployCondition =
pageable.isReleased() ? project.deployUrl.isNotNull() : null;
BooleanExpression cursorCondition = getCursorCondition(pageable.sort(),
pageable.lastProjectId(), pageable.lastOrderCount());
OrderSpecifier<?> orderSpecifier = getOrderSpecifier(pageable.sort());
long totalElements = getTotalElementsByCondition(deployCondition);

List<ProjectListResponse> results = queryFactory
.selectFrom(project)
request.isReleased() ? project.deployUrl.isNotNull() : null;
BooleanExpression cursorCondition = getCursorCondition(request.sort(),
request.lastProjectId(), request.lastOrderCount());
BooleanExpression searchCondition = getSearchCondition(request.search());
BooleanExpression skillCondition = getSkillCondition(request.skill());

// orderBy
OrderSpecifier<?> orderSpecifier = getOrderSpecifier(request.sort());

Long totalElements = 0L;

JPAQuery<Project> query = queryFactory
.selectFrom(project);

if (searchCondition != null) {
query
.join(member).on(project.id.eq(member.project.id))
.where(searchCondition);
totalElements = getTotalElementsByCondition(member, deployCondition, searchCondition,
member.project);
} else if (skillCondition != null) {
query
.where(skillCondition);
totalElements = getTotalElementsByCondition(projectSkill, deployCondition,
skillCondition,
projectSkill.project);
} else {
totalElements = getTotalElements(deployCondition);
}

List<ProjectListResponse> results = query
.where(
deployCondition,
cursorCondition
)
.orderBy(orderSpecifier, project.id.desc())
.limit(pageable.pageSize() + 1)
.limit(request.pageSize() + 1)
.fetch()
.stream()
.map(project -> {
boolean isLiked = likedProjectIds.contains(project.getId());
return ProjectListResponse.from(project, isLiked);
})
.toList();

return checkEndPage(results, pageable.pageSize(), totalElements);
return checkEndPage(results, request.pageSize(), totalElements);
}

@Override
public Page<ProjectListResponse> findAllByUserJoined(List<Long> likedProjectIds, User user,
Pageable pageable) {
Pageable pageable) {
BooleanExpression memberCondition = member.user.eq(user);
return findPageByCondition(member, member.project, memberCondition, pageable,
likedProjectIds);
}

@Override
public Page<ProjectListResponse> findAllByUserLiked(List<Long> likedProjectIds, User user,
Pageable pageable) {
Pageable pageable) {
BooleanExpression likeCondition = like.user.eq(user);
return findPageByCondition(like, like.project, likeCondition, pageable, likedProjectIds);
}

@Override
public Page<ProjectListResponse> findAllByUserCommented(List<Long> likedProjectIds, User user,
Pageable pageable) {
Pageable pageable) {
BooleanExpression commentCondition = comment.user.eq(user);
return findPageByCondition(comment, comment.project, commentCondition, pageable,
likedProjectIds);
}

private Page<ProjectListResponse> findPageByCondition(EntityPathBase<?> from,
QProject join, BooleanExpression condition, Pageable pageable,
List<Long> likedProjectIds) {
QProject join, BooleanExpression condition, Pageable pageable,
List<Long> likedProjectIds) {
List<Project> projects = queryFactory
.select(project)
.from(from)
Expand All @@ -111,23 +138,48 @@ private Page<ProjectListResponse> findPageByCondition(EntityPathBase<?> from,

private Long getCount(EntityPathBase<?> from, BooleanExpression condition, QProject join) {
return queryFactory
.select(project.count())
.select(project.countDistinct())
.from(from)
.join(join, project)
.where(condition)
.fetchOne();
}

private long getTotalElements(BooleanExpression deployCondition) {
return queryFactory
.select(project.countDistinct())
.from(project)
.where(deployCondition)
.fetchOne();
}

private Long getTotalElementsByCondition(EntityPathBase<?> from,
BooleanExpression deployCondition,
BooleanExpression filterCondition,
QProject join) {

return queryFactory
.select(project.countDistinct())
.from(from)
.join(join, project)
.where(
deployCondition,
filterCondition
)
.fetchOne();
}

private List<ProjectListResponse> toProjectListResponseList(List<Long> likedProjectIds,
List<Project> projects) {
List<Project> projects) {
return projects.stream()
.map(project -> ProjectListResponse.from(project,
likedProjectIds.contains(project.getId())))
.toList();
}

@Override
public List<ProjectBannerResponse> findAllPopularOfPeriod(LocalDate startDate, LocalDate endDate, int count) {
public List<ProjectBannerResponse> findAllPopularOfPeriod(LocalDate startDate,
LocalDate endDate, int count) {
DateTemplate<LocalDate> createdAt = Expressions.dateTemplate(
LocalDate.class, "DATE_FORMAT({0}, {1})", like.createdAt, "%Y-%m-%d");

Expand All @@ -146,17 +198,6 @@ public List<ProjectBannerResponse> findAllPopularOfPeriod(LocalDate startDate, L
.toList();
}

private long getTotalElementsByCondition(BooleanExpression deployCondition) {
NumberTemplate<Long> countTemplate = Expressions.numberTemplate(Long.class, "COUNT({0})",
project.id);

return queryFactory
.select(countTemplate)
.from(project)
.where(deployCondition)
.fetchOne();
}

private BooleanExpression getCursorCondition(SortType sort, Long lastProjectId,
Long orderCount) {
if (lastProjectId == null && orderCount == null) { // 첫 번째 페이지
Expand All @@ -177,6 +218,34 @@ private BooleanExpression getCursorCondition(SortType sort, Long lastProjectId,
}
}

private BooleanExpression getSearchCondition(String search) {
if (Objects.isNull(search) || search.isEmpty()) {
return null;
}

String keyword = "%" + search.trim() + "%";
return project.name.likeIgnoreCase(keyword)
.or(member.nickname.likeIgnoreCase(keyword));
}

public BooleanExpression getSkillCondition(List<String> skillNames) {
if (Objects.isNull(skillNames) || skillNames.isEmpty()) {
return null;
}

// 프로젝트 ID 서브쿼리를 생성하여 스킬을 모두 포함하는 프로젝트를 찾기
JPAQuery<Long> projectHasSkillsSubQuery = queryFactory
.select(projectSkill.project.id)
.from(projectSkill)
.join(projectSkill.skill)
.where(projectSkill.skill.name.in(skillNames))
.groupBy(projectSkill.project.id)
.having(projectSkill.project.id.count().eq(Expressions.constant(skillNames.size())));

// 프로젝트 ID 서브쿼리와 매칭되는 프로젝트를 찾는 조건을 반환합니다.
return project.id.in(projectHasSkillsSubQuery);
}

private OrderSpecifier<?> getOrderSpecifier(SortType sort) {
switch (sort) {
case like:
Expand Down
Loading

0 comments on commit b5cebf2

Please sign in to comment.