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] - 좋아요 기능 구현 #336

Merged
merged 15 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
17 changes: 13 additions & 4 deletions backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public class JwtAuthFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if (token == null || token.isBlank()) {
if (isTokenBlank(token)) {
sendUnauthorizedResponse(response, "로그인을 해주세요.");
return;
}
Expand Down Expand Up @@ -79,12 +79,21 @@ private void sendUnauthorizedResponse(HttpServletResponse response, String messa

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
AntPathMatcher antPathMatcher = new AntPathMatcher();

String url = request.getRequestURI();
String method = request.getMethod();
String requestURI = request.getRequestURI();
String token = request.getHeader(HttpHeaders.AUTHORIZATION);

return isInWhiteList(method, requestURI) && isTokenBlank(token);
Copy link
Member Author

@nak-honest nak-honest Aug 17, 2024

Choose a reason for hiding this comment

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

화이트 리스트에 포함된 요청이더라도, 액세스 토큰이 존재한다면 JwtAuthFilter를 거치도록 구현하였습니다.
여행기 상세 조회 시에만 따로 헤더를 체크하는 것 보다, 전체적인 통일성을 주는 것이 낫겠다고 생각했습니다.

하지만 이 방식의 문제점은 유효하지 않은 토큰을 보낼 때 문제가 됩니다.
이전에 클로버가 구현한 방식대로 58번 라인의 try-catch에서 화이트 리스트에 포함되는지 확인하는 것이 좋을까요?
아니면 유효하지 않은 토큰을 보낸 것 자체를 잘못된 요청으로 봐야 할까요?

전체적으로 의견 주시면 감사하겠습니다.

Copy link

Choose a reason for hiding this comment

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

여행기 상세 조회 시에는 헤더를 체크하지 않는 것으로 알고 있는데 여행 계획을 말씀하신 거겠죠?

그리고 이전의 구현에서 58번 라인의 try-catch에서 화이트 리스트에 포함되어 있는지 확인하는 코드가 없는 것으로 확인했는데 어떤 것을 말씀하시는 건지 잘 모르겠습니다.

58번 라인

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (token == null || token.isBlank()) {
            sendUnauthorizedResponse(response, "로그인을 해주세요.");
            return;
        }

        token = token.split("Bearer|bearer")[1];
        try {
            String memberId = tokenProvider.decodeAccessToken(token);
            request.setAttribute(MEMBER_ID_ATTRIBUTE, memberId);
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            sendUnauthorizedResponse(response, e.getMessage());
        }
    }

화이트리스트 체크는 shouldNotFilter에서만 진행하고 있었고 이러한 구현에 위화감이 없다는 생각이었는데 shouldNotFilter의 조건을 화이트리스트 체크와 isTokenBlank(token)으로 바꾼 이유 좀 더 자세히 들을 수 있을까요?

Choose a reason for hiding this comment

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

Authorization 헤더가 있다면 WhiteList에 있더라도 검증을 시도하는 것으로 이해했는데 맞나용?
좋아요 여부 조회 때문에 요렇게 구현하신 것 같은데 일단 최선의 방법 같습니다.
근데 이것도 저번에 말했던 비로그인해도 문제 없어야 되는 동작에서 토큰 만료 오류가 발생한다는 문제점이 해결될 것 같진 않은데 요 부분은 어떻게 해결하실 생각이신가용? 의견이 궁금합니다

Copy link
Member Author

@nak-honest nak-honest Aug 17, 2024

Choose a reason for hiding this comment

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

여행기 상세 조회 시에는 헤더를 체크하지 않는 것으로 알고 있는데 여행 계획을 말씀하신 거겠죠?

여행기 상세 조회 시, 사용자가 게시글에 좋아요 여부를 같이 줘야 합니다! 그래야 빨간색 하트로 채울지 말지 결정할 수 있습니닷!!
따라서 로그인 한 경우에는 헤더를 체크해서 사용자 정보를 가져와야 하는데, 그러면서도 로그인 하지 않은 사용자도 정상 접속 할 수 있어야 합니다.
따라서 위와 같은 방식으로 구현하게 되었습니다..!

그리고 이전의 구현에서 58번 라인의 try-catch에서 화이트 리스트에 포함되어 있는지 확인하는 코드가 없는 것으로 확인했는데 어떤 것을 말씀하시는 건지 잘 모르겠습니다.

요건 제가 설명을 잘 못한것 같네요 ㅠㅠ 현재 그렇게 구현되어 있다는 것이 아니고, 그렇게 구현하면 어떨지 여쭈어 보는 것이었습니닷!

Copy link
Member Author

@nak-honest nak-honest Aug 17, 2024

Choose a reason for hiding this comment

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

이 부분만 해결 되면 approve 주세욥 @Libienz !!

@eunjungL

비로그인해도 문제 없어야 되는 동작에서 토큰 만료 오류가 발생한다는 문제점이 해결될 것 같진 않은데 요 부분은 어떻게 해결하실 생각이신가용? 의견이 궁금합니다

조금 고민해 보았는데, 인증과 인가에 대한 역할을 분리하는 것은 어떻게 생각하시나요?
즉 2개의 필터를 운용하는 것이지요!

제가 생각하는 각 필터의 역할은 다음과 같습니다.

인증에 대한 필터는 모든 요청에 대해 액세스 토큰으로부터 멤버를 추출하는 역할만 합니다.
만약 토큰이 잘못되면 에러를 응답시키는 것이 아니라, request에 attribute를 넣지 않고 다음으로 넘어갑니다.
즉, 사용자의 신원을 확인하는 인증에 대한 책임만 가집니다.

그러면 그 다음에 인가에 대한 필터가 적용 되어서 request에 attribute가 있는지 확인합니다.
그리고 이에 따라 접근을 허용할지 말지 결정합니다. 즉 인가 필터에서 화이트 리스트를 다룹니다.

이렇게 하면 나중에 admin과 같은 추가 role이 들어와도 인가에 대한 필터에서 처리하면 됩니다.

저희가 저번에 대화를 나누면서도 모호했던 것이 화이트 리스트 자체가 인증이 아닌가? 였습니다.
따라서 이를 분리하면 어떨까 생각되었습니다.

객체지향 관점에서도 인증/인가에 대한 역할을 분리하는 것이 좋다고 생각되는데 의견 주시면 감사하겠습니다.

}

private boolean isInWhiteList(String method, String url) {
AntPathMatcher antPathMatcher = new AntPathMatcher();

return WHITE_LIST.stream()
.anyMatch(white -> white.method().matches(method) && antPathMatcher.match(white.urlPattern(), url));
}

private boolean isTokenBlank(String token) {
return token == null || token.isBlank();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import kr.touroot.global.exception.dto.ExceptionResponse;
import kr.touroot.travelogue.dto.request.TravelogueRequest;
import kr.touroot.travelogue.dto.request.TravelogueSearchRequest;
import kr.touroot.travelogue.dto.response.TravelogueLikeResponse;
import kr.touroot.travelogue.dto.response.TravelogueResponse;
import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse;
import kr.touroot.travelogue.service.TravelogueFacadeService;
Expand All @@ -23,6 +24,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
Expand Down Expand Up @@ -65,6 +67,29 @@ public ResponseEntity<TravelogueResponse> createTravelogue(
.body(response);
}

@Operation(summary = "여행기 좋아요")
@ApiResponses(value = {
@ApiResponse(
responseCode = "201",
description = "요청이 정상적으로 처리되었을 때"
),
@ApiResponse(
responseCode = "400",
description = "존재하지 않는 여행기 ID로 요청했을 때",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
),
@ApiResponse(
responseCode = "401",
description = "로그인하지 않은 사용자가 좋아요를 할 때",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
),
})
@PostMapping("/{id}/like")
public ResponseEntity<TravelogueLikeResponse> likeTravelogue(@PathVariable Long id, @Valid MemberAuth member) {
return ResponseEntity.ok()
.body(travelogueFacadeService.likeTravelogue(id, member));
}

@Operation(summary = "여행기 상세 조회")
@ApiResponses(value = {
@ApiResponse(
Expand All @@ -82,6 +107,23 @@ public ResponseEntity<TravelogueResponse> findTravelogue(@PathVariable Long id)
return ResponseEntity.ok(travelogueFacadeService.findTravelogueById(id));
}

@Operation(summary = "여행기 상세 조회")
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "요청이 정상적으로 처리되었을 때"
),
@ApiResponse(
responseCode = "400",
description = "존재하지 않는 여행기 ID로 요청했을 때",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
),
})
@GetMapping(value = "/{id}", headers = {HttpHeaders.AUTHORIZATION})
public ResponseEntity<TravelogueResponse> findTravelogue(@PathVariable Long id, MemberAuth member) {
return ResponseEntity.ok(travelogueFacadeService.findTravelogueById(id, member));
}

@Operation(summary = "여행기 메인 페이지 조회")
@ApiResponses(value = {
@ApiResponse(
Expand Down Expand Up @@ -200,4 +242,27 @@ public ResponseEntity<Void> deleteTravelogue(@PathVariable Long id, MemberAuth m
return ResponseEntity.noContent()
.build();
}

@Operation(summary = "여행기 좋아요 취소")
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "요청이 정상적으로 처리되었을 때"
),
@ApiResponse(
responseCode = "400",
description = "존재하지 않는 여행기 ID로 요청했을 때",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
),
@ApiResponse(
responseCode = "401",
description = "로그인하지 않은 사용자가 좋아요를 취소 할 때",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
),
})
@DeleteMapping("/{id}/like")
public ResponseEntity<TravelogueLikeResponse> unlikeTravelogue(@PathVariable Long id, @Valid MemberAuth member) {
return ResponseEntity.ok()
.body(travelogueFacadeService.unlikeTravelogue(id, member));

Choose a reason for hiding this comment

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

그냥 의견) 별거 아니긴한데 dislike, unlike중에 통일되면 좋을 것 같네요!

Copy link
Member Author

Choose a reason for hiding this comment

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

와우 dislike로 하다가 unlike가 더 나은거 같아 중간에 바꿨는데 미처 바꾸지 못했던 부분이 있었군요!!
바로 수정하겠습니닷

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package kr.touroot.travelogue.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import kr.touroot.member.domain.Member;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"TRAVELOGUE_ID", "LIKER_ID"})})

Choose a reason for hiding this comment

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

질문) @JoinColumn에도 unique=true 줄 수 있던데 여기다 따로 빼두신 이유가 있나요? 둘이 다른 역할을 하는건지 잘 몰라서 여쭤봅니당
만약 동일한 역할이라면 갠적으로 nullable=false 설정하는 것처럼 해당 컬럼 관련 설정은 컬럼에 붙어있는게 좋을 것 같습니다. 그래야 한 눈에 확인하기 쉬울 것 같아서요!

Copy link
Member Author

Choose a reason for hiding this comment

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

@JoinColumn에 주면 해당 컬럼 하나에 대해서만 unique가 걸립니다!

하지만 여행기 좋아요는 (travelogue_id, liker_id) 이 조합이 unique 해야 해서 위와 같이 구현했습니다!
즉, 복합 컬럼에 대해 unique를 걸고 싶어서 따로 뺐습니다!

@Entity
public class TravelogueLike {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@JoinColumn(name = "TRAVELOGUE_ID", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private Travelogue travelogue;

@JoinColumn(name = "LIKER_ID", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private Member liker;

public TravelogueLike(Travelogue travelogue, Member liker) {
this(null, travelogue, liker);
}
}
Comment on lines +23 to +40
Copy link

Choose a reason for hiding this comment

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

다대다 확인!

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kr.touroot.travelogue.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

public record TravelogueLikeResponse(
@Schema(description = "사용자의 좋아요 여부", example = "true")
Boolean isLiked,
@Schema(description = "여행기의 좋아요 수", example = "10")
Long likeCount
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,18 @@ public record TravelogueResponse(
@Schema(description = "여행기 태그")
List<TagResponse> tags,
@Schema(description = "여행기 일자 목록")
List<TravelogueDayResponse> days
List<TravelogueDayResponse> days,
@Schema(description = "여행기 좋아요 숫자", example = "10")
Long likeCount,
@Schema(description = "사용자의 여행기 좋아요 여부", example = "true")
Boolean isLiked
) {

public static TravelogueResponse of(Travelogue travelogue, List<TravelogueDayResponse> days, List<TagResponse> tags) {
public static TravelogueResponse of(
Travelogue travelogue,
List<TravelogueDayResponse> days,
List<TagResponse> tags,
TravelogueLikeResponse like) {
return TravelogueResponse.builder()
.id(travelogue.getId())
.createdAt(travelogue.getCreatedAt().toLocalDate())
Expand All @@ -40,6 +48,8 @@ public static TravelogueResponse of(Travelogue travelogue, List<TravelogueDayRes
.thumbnail(travelogue.getThumbnail())
.days(days)
.tags(tags)
.likeCount(like.likeCount())
.isLiked(like.isLiked())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,23 @@ public record TravelogueSimpleResponse(
@Schema(description = "작성자 프로필 사진 URL", example = "https://dev.touroot.kr/images/profile.png")
String authorProfileUrl,
@Schema(description = "여행기 태그 목록")
List<TagResponse> tags
List<TagResponse> tags,
@Schema(description = "작성자 프로필 사진 URL", example = "10")
Long likeCount
) {

public static TravelogueSimpleResponse of(Travelogue travelogue, List<TagResponse> tags) {
public static TravelogueSimpleResponse of(
Travelogue travelogue,
List<TagResponse> tags,
TravelogueLikeResponse like) {
return TravelogueSimpleResponse.builder()
.id(travelogue.getId())
.title(travelogue.getTitle())
.thumbnail(travelogue.getThumbnail())
.authorNickname(travelogue.getAuthorNickname())
.authorProfileUrl(travelogue.getAuthorProfileImageUrl())
.tags(tags)
.likeCount(like.likeCount())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.touroot.travelogue.repository;

import kr.touroot.member.domain.Member;
import kr.touroot.travelogue.domain.Travelogue;
import kr.touroot.travelogue.domain.TravelogueLike;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TravelogueLikeRepository extends JpaRepository<TravelogueLike, Long> {

Long countByTravelogue(Travelogue travelogue);

Choose a reason for hiding this comment

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

아직 성능 논의할 단계가 아니긴한데 Like가 많아질수록 count가 자주 일어날 것 같기도합니다. 저희는 작성보다 조회가 훨씬 많이 일어나는 서비스라고 생각해서 TraveloguelikeCount를 직접 주는 방식으로 관리하면 어떨까 싶은데 어떻게 생각하시나요?
요 부분은 다 같이 한 번 얘기해보시죠!

Choose a reason for hiding this comment

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

코드 보니 좋아요와 관련된 모든 로직에서 countBy가 일어나고 있네요! 한 번 고민해보시져

Copy link
Member Author

Choose a reason for hiding this comment

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

확실히 고민해 볼만한 부분은 맞는 것 같습니다!
실제로 페이스북에서는 게시글 테이블 자체에 좋아요 숫자가 따로 존재한다고 하네요.
조회가 훨씬 많은 부분이고, 좋아요 숫자는 정확성이 엄청 높아야 하는 부분은 아니니까요!!

하지만 클로버가 말씀하신대로 아직 성능을 논의할 단계가 아니라는 점이 계속 고민을 하게 만드는 것 같습니다 ㅠㅠ
동시성도 아예 고민이 안되는 것은 아닌데, 이것도 또 고민하기에는 이른 거 같고 ㅎㅎ 어렵네요!

월요일에 다같이 한번 논의해 보시죠!
까먹지 않게 노션 백엔드 탭에 투두로 적어놓았습니닷


boolean existsByTravelogueAndLiker(Travelogue travelogue, Member liker);

void deleteByTravelogueAndLiker(Travelogue travelogue, Member liker);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import kr.touroot.travelogue.dto.request.TravelogueRequest;
import kr.touroot.travelogue.dto.request.TravelogueSearchRequest;
import kr.touroot.travelogue.dto.response.TravelogueDayResponse;
import kr.touroot.travelogue.dto.response.TravelogueLikeResponse;
import kr.touroot.travelogue.dto.response.TraveloguePlaceResponse;
import kr.touroot.travelogue.dto.response.TravelogueResponse;
import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse;
Expand All @@ -35,14 +36,16 @@ public class TravelogueFacadeService {
private final TraveloguePlaceService traveloguePlaceService;
private final TraveloguePhotoService traveloguePhotoService;
private final TravelogueTagService travelogueTagService;
private final TravelogueLikeService travelogueLikeService;
private final MemberService memberService;

@Transactional
public TravelogueResponse createTravelogue(MemberAuth member, TravelogueRequest request) {
Member author = memberService.getById(member.memberId());
Travelogue travelogue = travelogueService.createTravelogue(author, request);
List<TagResponse> tags = travelogueTagService.createTravelogueTags(travelogue, request.tags());
return TravelogueResponse.of(travelogue, createDays(request.days(), travelogue), tags);
TravelogueLikeResponse like = travelogueLikeService.findLikeByTravelogueAndLiker(travelogue, author);
Copy link
Member Author

@nak-honest nak-honest Aug 17, 2024

Choose a reason for hiding this comment

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

여행기를 생성할 때에는 좋아요 수가 0이고, 좋아요 여부가 false 입니다.
현재는 DB를 찔러서 확인하고 있지만, 다음과 같이 상수로 빼는 것이 더 나을까요?

private static final TravelogueLikeResponse INITIAL_LIKE = new TravelogueLikeResponse(false, 0L);

Copy link

Choose a reason for hiding this comment

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

개인적으로는 DB 찌르는 현재 구현이 신뢰성있다고 생각합니다!
후에 데이터가 꼬일 가능성이 없을 것 같긴 하지만 이 정도 쿼리 아낀다고 성능향상이 많이 있을 것 같지도 않아서요..!
개인적인 의견 드려봅니다 🙇🏻‍♂️

Choose a reason for hiding this comment

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

생성 시에는 어차피 좋아요 수 0, 여부 false가 고정값이라 상수로 빼두는 것도 괜찮은 구현 같습니다
DB 찌르는 것도 리비 말처럼 신뢰성 있을 것 같네요!

return TravelogueResponse.of(travelogue, createDays(request.days(), travelogue), tags, like);
}

private List<TravelogueDayResponse> createDays(List<TravelogueDayRequest> requests, Travelogue travelogue) {
Expand Down Expand Up @@ -71,15 +74,38 @@ private List<String> createPhotos(List<TraveloguePhotoRequest> requests, Travelo
.toList();
}

@Transactional
public TravelogueLikeResponse likeTravelogue(Long travelogueId, MemberAuth member) {
Travelogue travelogue = travelogueService.getTravelogueById(travelogueId);
Member liker = memberService.getById(member.memberId());

return travelogueLikeService.likeTravelogue(travelogue, liker);
}

@Transactional(readOnly = true)
public TravelogueResponse findTravelogueById(Long id) {
Travelogue travelogue = travelogueService.getTravelogueById(id);
return getTravelogueResponse(travelogue);
}

@Transactional(readOnly = true)
public TravelogueResponse findTravelogueById(Long id, MemberAuth member) {
Travelogue travelogue = travelogueService.getTravelogueById(id);
return getTravelogueResponse(travelogue, member);
}

private TravelogueResponse getTravelogueResponse(Travelogue travelogue) {
List<TagResponse> tagResponses = travelogueTagService.readTagByTravelogue(travelogue);
return TravelogueResponse.of(travelogue, findDaysOfTravelogue(travelogue), tagResponses);
TravelogueLikeResponse likeResponse = travelogueLikeService.findLikeByTravelogue(travelogue);
return TravelogueResponse.of(travelogue, findDaysOfTravelogue(travelogue), tagResponses, likeResponse);
}

private TravelogueResponse getTravelogueResponse(Travelogue travelogue, MemberAuth member) {
Member liker = memberService.getById(member.memberId());

List<TagResponse> tagResponses = travelogueTagService.readTagByTravelogue(travelogue);
TravelogueLikeResponse likeResponse = travelogueLikeService.findLikeByTravelogueAndLiker(travelogue, liker);
return TravelogueResponse.of(travelogue, findDaysOfTravelogue(travelogue), tagResponses, likeResponse);
}

private List<TravelogueDayResponse> findDaysOfTravelogue(Travelogue travelogue) {
Expand Down Expand Up @@ -125,7 +151,8 @@ public Page<TravelogueSimpleResponse> findSimpleTravelogues(Pageable pageable, T

private TravelogueSimpleResponse getTravelogueSimpleResponse(Travelogue travelogue) {
List<TagResponse> tagResponses = travelogueTagService.readTagByTravelogue(travelogue);
return TravelogueSimpleResponse.of(travelogue, tagResponses);
TravelogueLikeResponse likeResponse = travelogueLikeService.findLikeByTravelogue(travelogue);
return TravelogueSimpleResponse.of(travelogue, tagResponses, likeResponse);
}

@Transactional
Expand All @@ -135,10 +162,11 @@ public TravelogueResponse updateTravelogue(Long id, MemberAuth member, Travelogu

Travelogue updatedTravelogue = travelogueService.update(id, author, request);
List<TagResponse> tags = travelogueTagService.updateTravelogueTags(travelogue, request.tags());
TravelogueLikeResponse like = travelogueLikeService.findLikeByTravelogueAndLiker(travelogue, author);

clearTravelogueContents(travelogue);

return TravelogueResponse.of(updatedTravelogue, createDays(request.days(), updatedTravelogue), tags);
return TravelogueResponse.of(updatedTravelogue, createDays(request.days(), updatedTravelogue), tags, like);
}

private void clearTravelogueContents(Travelogue travelogue) {
Expand All @@ -156,4 +184,12 @@ public void deleteTravelogueById(Long id, MemberAuth member) {
clearTravelogueContents(travelogue);
travelogueService.delete(travelogue, author);
}

@Transactional
public TravelogueLikeResponse unlikeTravelogue(Long travelogueId, MemberAuth member) {
Travelogue travelogue = travelogueService.getTravelogueById(travelogueId);
Member liker = memberService.getById(member.memberId());

return travelogueLikeService.unlikeTravelogue(travelogue, liker);
}
}
Loading
Loading