Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] - 여행기 정렬 및 날짜별 필터링 추가 #442

Merged
merged 17 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
aa0cfa1
feat: Travelogue Entity에 likeCount 필드 추가
eunjungL Sep 22, 2024
0921781
refactor: Travelogue Entity의 likeCount long 타입으로 변경
eunjungL Sep 22, 2024
baa3a37
feat: Travelogue 좋아요 생성, 삭제 시 likeCount 변경 추가
eunjungL Sep 22, 2024
7540226
refactor: 좋아요 수 조회 시 travelogue 필드 활용하게 변경
eunjungL Sep 22, 2024
f4cd65a
refactor: 필터링 조건 추가에 따른 TravelogueFilterCondition 추가
eunjungL Sep 22, 2024
2c6a6f1
test: 여행기 조회 시 좋아요 순 정렬 테스트 추가
eunjungL Sep 23, 2024
188a27d
feat: 여행기 기간 기반으로 필터링 기능 추가
eunjungL Sep 23, 2024
73768c2
refactor: 정렬 방향 클라이언트의 입력 값으로 결정하게 변경
eunjungL Sep 24, 2024
c3e79ac
refactor: 검색 최대 기간 매직 넘버 상수화
eunjungL Sep 24, 2024
8e23ff7
refactor: 필터에 따른 여행기 조회 분기 service로 이동 및 dto, domain 분리
eunjungL Sep 24, 2024
f812e0e
refactor: FilterCondition 관련 판단 로직 FilterCondition 내부로 응집
eunjungL Sep 24, 2024
44428f1
refactor: 태그 기반 필터링 쿼리 travelogue 기반으로 변경
eunjungL Sep 24, 2024
ed96678
refactor: likeCount Column default 제거
eunjungL Sep 24, 2024
9959a83
fix: develop/be 충돌 해결
eunjungL Sep 25, 2024
8936f13
feat: Travelogue likeCount 컬럼 추가 flyway V3 작성
eunjungL Sep 25, 2024
16d469d
Merge branch 'develop/be' into feature/be/#410
eunjungL Sep 25, 2024
531a7b7
Merge branch 'develop/be' into feature/be/#410
eunjungL Sep 25, 2024
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 @@ -9,9 +9,9 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.net.URI;
import java.util.List;
import kr.touroot.global.auth.dto.MemberAuth;
import kr.touroot.global.exception.dto.ExceptionResponse;
import kr.touroot.travelogue.dto.request.TravelogueFilterRequest;
import kr.touroot.travelogue.dto.request.TravelogueRequest;
import kr.touroot.travelogue.dto.request.TravelogueSearchRequest;
import kr.touroot.travelogue.dto.response.TravelogueLikeResponse;
Expand All @@ -33,7 +33,6 @@
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "여행기")
Expand Down Expand Up @@ -138,35 +137,13 @@ public ResponseEntity<TravelogueResponse> findTravelogue(@PathVariable Long id,
})
@PageableAsQueryParam
@GetMapping
public ResponseEntity<Page<TravelogueSimpleResponse>> findMainPageTravelogues(
@Parameter(hidden = true)
@PageableDefault(size = 5, sort = "id", direction = Direction.DESC)
Pageable pageable
) {
return ResponseEntity.ok(travelogueFacadeService.findSimpleTravelogues(pageable));
}

@Operation(summary = "여행기 메인 페이지 필터링")
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "요청이 정상적으로 처리되었을 때"
),
@ApiResponse(
responseCode = "400",
description = "올바르지 않은 페이지네이션 옵션으로 요청했을 때",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
),
})
@PageableAsQueryParam
@GetMapping(params = {"tag-filter"})
public ResponseEntity<Page<TravelogueSimpleResponse>> findMainPageTravelogues(
@Parameter(hidden = true)
@PageableDefault(size = 5, sort = "id", direction = Direction.DESC)
Pageable pageable,
@RequestParam(name = "tag-filter", required = false) List<Long> tagFilter
TravelogueFilterRequest filter
) {
return ResponseEntity.ok(travelogueFacadeService.findSimpleTravelogues(tagFilter, pageable));
return ResponseEntity.ok(travelogueFacadeService.findSimpleTravelogues(filter, pageable));
}

@Operation(summary = "여행기 검색")
Expand Down
18 changes: 16 additions & 2 deletions backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;

Expand All @@ -34,6 +35,7 @@ public class Travelogue extends BaseEntity {

private static final int MIN_TITLE_LENGTH = 1;
private static final int MAX_TITLE_LENGTH = 20;
private static final int LIKE_COUNT_WEIGHT = 1;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand All @@ -49,19 +51,23 @@ public class Travelogue extends BaseEntity {
@Column(nullable = false)
private String thumbnail;

@Column(nullable = false)
private Long likeCount;

@OneToMany(mappedBy = "travelogue", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TravelogueDay> travelogueDays = new ArrayList<>();

public Travelogue(Long id, Member author, String title, String thumbnail) {
private Travelogue(Long id, Member author, String title, String thumbnail, Long likeCount) {
validate(author, title, thumbnail);
this.id = id;
this.author = author;
this.title = title;
this.thumbnail = thumbnail;
this.likeCount = likeCount;
}

public Travelogue(Member author, String title, String thumbnail) {
this(null, author, title, thumbnail);
this(null, author, title, thumbnail, 0L);
}

public void update(String title, String thumbnail) {
Expand Down Expand Up @@ -101,6 +107,14 @@ private void validateThumbnailFormat(String thumbnailUrl) {
throw new BadRequestException("이미지 url 형식이 잘못되었습니다");
}
}

public void increaseLikeCount() {
likeCount += LIKE_COUNT_WEIGHT;
}

public void decreaseLikeCount() {
likeCount -= LIKE_COUNT_WEIGHT;
}
Comment on lines +111 to +117
Copy link
Member

Choose a reason for hiding this comment

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

변경 감지를 이용하게 되면서, 서로 다른 트랜잭션에서 race condition 이 발생할 수 있겠네요!
저희 모두 인지는 하고 있으나, 현재 데모데이 요구사항을 지키는 것이 더 우선순위가 높은 것 같아 다음 스프린트 때 해결해 보도록 하시죠!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

인지해두겠습니다 👍

Comment on lines +111 to +117
Copy link
Contributor

Choose a reason for hiding this comment

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

개인적으로는 +1이 좀 더 가독성 있다고 생각하는데 어떻게 생각하시나유?
지금 구현도 크게 위화감 있다고 생각하지 않아서요~ 반드시 변경하기보다 클로버가 마음가는 구현으로 남겨주세요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

1이 가독성 있긴한데 매직넘버라 생각해 분리해뒀습니다.하나로 관리하는게 증가, 삭제 일관성 지키기도 더 쉬울 것 같구요.
일단 유지하겠습니다!


public boolean isAuthor(Member author) {
return Objects.equals(author.getId(), this.author.getId());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package kr.touroot.travelogue.domain;

import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class TravelogueFilterCondition {

public static final int MAX_PERIOD_BOUNDARY = 8;

private final List<Long> tag;
private final Integer period;

public boolean isEmptyTagCondition() {
return tag == null;
}

public boolean isEmptyPeriodCondition() {
return period == null;
}

public boolean isEmptyCondition() {
return isEmptyTagCondition() && isEmptyPeriodCondition();
}

public boolean isMaxPeriod() {
return period == MAX_PERIOD_BOUNDARY;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kr.touroot.travelogue.dto.request;

import java.util.List;
import kr.touroot.travelogue.domain.TravelogueFilterCondition;

public record TravelogueFilterRequest(List<Long> tag, Integer period) {

public TravelogueFilterCondition toFilterCondition() {
return new TravelogueFilterCondition(tag, period);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

public interface TravelogueLikeRepository extends JpaRepository<TravelogueLike, Long> {

Long countByTravelogue(Travelogue travelogue);

boolean existsByTravelogueAndLiker(Travelogue travelogue, Member liker);

void deleteAllByTravelogue(Travelogue travelogue);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package kr.touroot.travelogue.repository.query;

import java.util.List;
import kr.touroot.travelogue.domain.Travelogue;
import kr.touroot.travelogue.domain.TravelogueFilterCondition;
import kr.touroot.travelogue.domain.search.SearchCondition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -10,5 +10,5 @@ public interface TravelogueQueryRepository {

Page<Travelogue> findByKeywordAndSearchType(SearchCondition condition, Pageable pageable);

Page<Travelogue> findAllByTag(List<Long> tagFilter, Pageable pageable);
Page<Travelogue> findAllByFilter(TravelogueFilterCondition filter, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@
import static kr.touroot.travelogue.domain.QTravelogue.travelogue;
import static kr.touroot.travelogue.domain.QTravelogueTag.travelogueTag;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.StringPath;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import kr.touroot.travelogue.domain.Travelogue;
import kr.touroot.travelogue.domain.TravelogueFilterCondition;
import com.querydsl.core.types.dsl.StringPath;
import kr.touroot.travelogue.domain.search.SearchCondition;
import kr.touroot.travelogue.domain.search.SearchType;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Repository;

@RequiredArgsConstructor
Expand Down Expand Up @@ -49,22 +53,64 @@ private StringPath getTargetField(SearchType searchType) {
}

@Override
public Page<Travelogue> findAllByTag(List<Long> tagFilter, Pageable pageable) {
List<Travelogue> results = jpaQueryFactory.select(travelogue)
.from(travelogueTag)
.where(travelogueTag.tag.id.in(tagFilter))
.groupBy(travelogueTag.travelogue)
.having(isSameCountWithFilter(tagFilter))
.orderBy(travelogueTag.travelogue.createdAt.desc())
public Page<Travelogue> findAllByFilter(TravelogueFilterCondition filter, Pageable pageable) {
JPAQuery<Travelogue> query = jpaQueryFactory.selectFrom(travelogue);

addTagFilter(query, filter);
addPeriodFilter(query, filter);

List<Travelogue> results = query.orderBy(findSortCondition(pageable.getSort()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

return new PageImpl<>(results, pageable, results.size());
}

private BooleanExpression isSameCountWithFilter(List<Long> tagFilter) {
return travelogueTag.travelogue.count()
.eq(Long.valueOf(tagFilter.size()));
public void addTagFilter(JPAQuery<Travelogue> query, TravelogueFilterCondition filter) {
if (filter.isEmptyTagCondition()) {
return;
}

List<Long> tags = filter.getTag();

query.join(travelogueTag).on(travelogueTag.travelogue.eq(travelogue))
.where(travelogueTag.tag.id.in(tags))
.groupBy(travelogue)
.having(travelogueTag.count().eq(Long.valueOf(tags.size())));
}

public void addPeriodFilter(JPAQuery<Travelogue> query, TravelogueFilterCondition filter) {
if (filter.isEmptyPeriodCondition()) {
return;
}

if (filter.isMaxPeriod()) {
query.where(travelogue.travelogueDays.size().goe(TravelogueFilterCondition.MAX_PERIOD_BOUNDARY));
return;
}

query.where(travelogue.travelogueDays.size().eq(filter.getPeriod()));
}

private OrderSpecifier<?> findSortCondition(Sort sort) {
Sort.Order order = sort.iterator()
.next();
String sortBy = order.getProperty();
Order direction = getDirection(order);

if (sortBy.equals("createdAt")) {
return new OrderSpecifier<>(direction, travelogue.createdAt);
}

return new OrderSpecifier<>(direction, travelogue.likeCount);
}

private Order getDirection(Sort.Order order) {
if (order.isAscending()) {
return Order.ASC;
}

return Order.DESC;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import kr.touroot.tag.dto.TagResponse;
import kr.touroot.travelogue.domain.Travelogue;
import kr.touroot.travelogue.domain.TravelogueDay;
import kr.touroot.travelogue.domain.TravelogueFilterCondition;
import kr.touroot.travelogue.domain.TraveloguePhoto;
import kr.touroot.travelogue.domain.TraveloguePlace;
import kr.touroot.travelogue.dto.request.TravelogueDayRequest;
import kr.touroot.travelogue.dto.request.TravelogueFilterRequest;
import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest;
import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest;
import kr.touroot.travelogue.dto.request.TravelogueRequest;
Expand Down Expand Up @@ -131,15 +133,21 @@ private List<String> findPhotoUrlsOfTraveloguePlace(TraveloguePlace place) {
}

@Transactional(readOnly = true)
public Page<TravelogueSimpleResponse> findSimpleTravelogues(Pageable pageable) {
Page<Travelogue> travelogues = travelogueService.findAll(pageable);
public Page<TravelogueSimpleResponse> findSimpleTravelogues(
TravelogueFilterRequest filterRequest,
Pageable pageable
) {
TravelogueFilterCondition filter = filterRequest.toFilterCondition();
Page<Travelogue> travelogues = findSimpleTraveloguesByFilter(filter, pageable);
return travelogues.map(this::getTravelogueSimpleResponse);
}

@Transactional(readOnly = true)
public Page<TravelogueSimpleResponse> findSimpleTravelogues(List<Long> tagFilter, Pageable pageable) {
Page<Travelogue> travelogues = travelogueService.findAllByFilter(tagFilter, pageable);
return travelogues.map(this::getTravelogueSimpleResponse);
private Page<Travelogue> findSimpleTraveloguesByFilter(TravelogueFilterCondition filter, Pageable pageable) {
if (filter.isEmptyCondition()) {
return travelogueService.findAll(pageable);
}

return travelogueService.findAllByFilter(filter, pageable);
Comment on lines +145 to +150
Copy link
Contributor

Choose a reason for hiding this comment

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

너무 클린 & 깔끔

}

@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ public class TravelogueLikeService {

@Transactional(readOnly = true)
public TravelogueLikeResponse findLikeByTravelogue(Travelogue travelogue) {
return new TravelogueLikeResponse(false, travelogueLikeRepository.countByTravelogue(travelogue));
return new TravelogueLikeResponse(false, travelogue.getLikeCount());
}

@Transactional(readOnly = true)
public TravelogueLikeResponse findLikeByTravelogueAndLiker(Travelogue travelogue, Member liker) {
boolean exists = travelogueLikeRepository.existsByTravelogueAndLiker(travelogue, liker);
return new TravelogueLikeResponse(exists, travelogueLikeRepository.countByTravelogue(travelogue));
return new TravelogueLikeResponse(exists, travelogue.getLikeCount());
}

@Transactional
Expand All @@ -32,19 +32,21 @@ public TravelogueLikeResponse likeTravelogue(Travelogue travelogue, Member liker
if (notExists) {
TravelogueLike travelogueLike = new TravelogueLike(travelogue, liker);
travelogueLikeRepository.save(travelogueLike);
travelogue.increaseLikeCount();
}

return new TravelogueLikeResponse(true, travelogueLikeRepository.countByTravelogue(travelogue));
return new TravelogueLikeResponse(true, travelogue.getLikeCount());
}

@Transactional
public TravelogueLikeResponse unlikeTravelogue(Travelogue travelogue, Member liker) {
boolean exists = travelogueLikeRepository.existsByTravelogueAndLiker(travelogue, liker);
if (exists) {
travelogueLikeRepository.deleteByTravelogueAndLiker(travelogue, liker);
travelogue.decreaseLikeCount();
}

return new TravelogueLikeResponse(false, travelogueLikeRepository.countByTravelogue(travelogue));
return new TravelogueLikeResponse(false, travelogue.getLikeCount());
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package kr.touroot.travelogue.service;

import java.util.List;
import kr.touroot.global.exception.BadRequestException;
import kr.touroot.global.exception.ForbiddenException;
import kr.touroot.image.infrastructure.AwsS3Provider;
import kr.touroot.member.domain.Member;
import kr.touroot.travelogue.domain.Travelogue;
import kr.touroot.travelogue.domain.TravelogueFilterCondition;
import kr.touroot.travelogue.dto.request.TravelogueRequest;
import kr.touroot.travelogue.domain.search.SearchCondition;
import kr.touroot.travelogue.domain.search.SearchType;
import kr.touroot.travelogue.dto.request.TravelogueRequest;
import kr.touroot.travelogue.dto.request.TravelogueSearchRequest;
import kr.touroot.travelogue.repository.TravelogueRepository;
import kr.touroot.travelogue.repository.query.TravelogueQueryRepository;
Expand Down Expand Up @@ -58,8 +58,8 @@ public Page<Travelogue> findByKeyword(TravelogueSearchRequest request, Pageable
}

@Transactional(readOnly = true)
public Page<Travelogue> findAllByFilter(List<Long> filter, Pageable pageable) {
return travelogueQueryRepository.findAllByTag(filter, pageable);
public Page<Travelogue> findAllByFilter(TravelogueFilterCondition filter, Pageable pageable) {
return travelogueQueryRepository.findAllByFilter(filter, pageable);
Comment on lines -61 to +62
Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏻 👍🏻 추상화 좋군요

}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ALTER TABLE travelogue ADD like_count BIGINT;

UPDATE travelogue AS t LEFT JOIN (SELECT travelogue_id, COUNT(*) AS like_count
FROM travelogue_like
GROUP BY travelogue_id) AS tl ON t.id = tl.travelogue_id
SET t.like_count = COALESCE(tl.like_count, 0);
Loading
Loading