Skip to content

Commit

Permalink
Merge pull request #116 from Shimpyo-House/feature/favorite
Browse files Browse the repository at this point in the history
feat: 즐겨찾기 기능 구현
  • Loading branch information
JeongUijeong authored Dec 12, 2023
2 parents 5c99a44 + 8dbcea1 commit 4e16e10
Show file tree
Hide file tree
Showing 15 changed files with 1,149 additions and 3 deletions.
52 changes: 52 additions & 0 deletions src/docs/asciidoc/favorite/favorite-api.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
ifndef::snippets[]
:snippets: build/generated-snippets
endif::[]

= Favorite REST API Docs
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2

[[Register]]
== 즐겨찾기 등록

즐겨찾기 등록 API 입니다.

=== HttpRequest

include::{snippets}/favorite-rest-controller-docs-test/register/http-request.adoc[]

=== HttpResponse

include::{snippets}/favorite-rest-controller-docs-test/register/http-response.adoc[]
include::{snippets}/favorite-rest-controller-docs-test/register/response-fields.adoc[]

[[Get-Favorites]]
== 즐겨찾기 목록 조회

즐겨찾기 목록 조회 API 입니다.

=== HttpRequest

include::{snippets}/favorite-rest-controller-docs-test/get-favorites/http-request.adoc[]

=== HttpResponse

include::{snippets}/favorite-rest-controller-docs-test/get-favorites/http-response.adoc[]
include::{snippets}/favorite-rest-controller-docs-test/get-favorites/response-fields.adoc[]

[[Cancel]]
== 즐겨찾기 취소

즐겨찾기 취소 API 입니다.

=== HttpRequest

include::{snippets}/favorite-rest-controller-docs-test/cancel/http-request.adoc[]

=== HttpResponse

include::{snippets}/favorite-rest-controller-docs-test/cancel/http-response.adoc[]
include::{snippets}/favorite-rest-controller-docs-test/cancel/response-fields.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.fc.shimpyo_be.domain.favorite.controller;

import com.fc.shimpyo_be.domain.favorite.dto.FavoriteResponseDto;
import com.fc.shimpyo_be.domain.favorite.dto.FavoritesResponseDto;
import com.fc.shimpyo_be.domain.favorite.entity.Favorite;
import com.fc.shimpyo_be.domain.favorite.service.FavoriteService;
import com.fc.shimpyo_be.domain.product.util.model.PageableConstraint;
import com.fc.shimpyo_be.global.common.ResponseDto;
import com.fc.shimpyo_be.global.util.SecurityUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/favorites")
public class FavoriteRestController {

private final FavoriteService favoriteService;
private final SecurityUtil securityUtil;

@PostMapping("/{productId}")
public ResponseEntity<ResponseDto<FavoriteResponseDto>> register(@PathVariable long productId) {
log.debug("memberId: {}, productId: {}", securityUtil.getCurrentMemberId(), productId);
return ResponseEntity.status(HttpStatus.CREATED).body(ResponseDto.res(HttpStatus.CREATED,
favoriteService.register(securityUtil.getCurrentMemberId(), productId),
"성공적으로 즐겨찾기를 등록했습니다."));
}

@GetMapping
public ResponseEntity<ResponseDto<FavoritesResponseDto>> getFavorites(
@PageableConstraint(Favorite.class) @PageableDefault(size = 10, page = 0) Pageable pageable) {
log.debug("memberId: {}", securityUtil.getCurrentMemberId());
return ResponseEntity.status(HttpStatus.OK)
.body(ResponseDto.res(HttpStatus.OK, favoriteService.getFavorites(
securityUtil.getCurrentMemberId(), pageable), "성공적으로 즐겨찾기 목록을 조회했습니다."));
}

@DeleteMapping("/{productId}")
public ResponseEntity<ResponseDto<FavoriteResponseDto>> cancel(@PathVariable long productId) {
log.debug("memberId: {}, productId: {}", securityUtil.getCurrentMemberId(), productId);
return ResponseEntity.status(HttpStatus.OK).body(ResponseDto.res(HttpStatus.OK,
favoriteService.delete(securityUtil.getCurrentMemberId(), productId),
"성공적으로 즐겨찾기를 취소했습니다."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.fc.shimpyo_be.domain.favorite.dto;

import com.fc.shimpyo_be.domain.favorite.entity.Favorite;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class FavoriteResponseDto {

private Long favoriteId;
private Long memberId;
private Long productId;

@Builder
public FavoriteResponseDto(Long favoriteId, Long memberId, Long productId) {
this.favoriteId = favoriteId;
this.memberId = memberId;
this.productId = productId;
}

public static FavoriteResponseDto of(Favorite favorite) {
return FavoriteResponseDto.builder()
.favoriteId(favorite.getId())
.memberId(favorite.getMember().getId())
.productId(favorite.getProduct().getId())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.fc.shimpyo_be.domain.favorite.dto;

import com.fc.shimpyo_be.domain.product.dto.response.ProductResponse;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class FavoritesResponseDto {

private int pageCount;
private Boolean isLast;
private List<ProductResponse> products;

@Builder
public FavoritesResponseDto(int pageCount, Boolean isLast, List<ProductResponse> products) {
this.pageCount = pageCount;
this.isLast = isLast;
this.products = products;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.fc.shimpyo_be.domain.favorite.exception;

import com.fc.shimpyo_be.global.exception.ApplicationException;
import com.fc.shimpyo_be.global.exception.ErrorCode;

public class FavoriteAlreadyRegisterException extends ApplicationException {

public FavoriteAlreadyRegisterException() {
super(ErrorCode.FAVORITE_ALREADY_REGISTER);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.fc.shimpyo_be.domain.favorite.exception;

import com.fc.shimpyo_be.global.exception.ApplicationException;
import com.fc.shimpyo_be.global.exception.ErrorCode;

public class FavoriteNotFoundException extends ApplicationException {

public FavoriteNotFoundException() {
super(ErrorCode.FAVORITE_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.fc.shimpyo_be.domain.favorite.repository;

import com.fc.shimpyo_be.domain.favorite.entity.Favorite;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface FavoriteCustomRepository {

Page<Favorite> findAllByMemberId(long memberId, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.fc.shimpyo_be.domain.favorite.repository;

import static com.fc.shimpyo_be.domain.favorite.entity.QFavorite.favorite;
import static com.fc.shimpyo_be.domain.member.entity.QMember.member;
import static com.fc.shimpyo_be.domain.product.entity.QProduct.product;

import com.fc.shimpyo_be.domain.favorite.entity.Favorite;
import com.fc.shimpyo_be.global.util.QueryDslUtil;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.LinkedList;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.stereotype.Repository;

@Repository
public class FavoriteCustomRepositoryImpl implements FavoriteCustomRepository {

private final JPAQueryFactory queryFactory;

FavoriteCustomRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
this.queryFactory = jpaQueryFactory;
}

@Override
public Page<Favorite> findAllByMemberId(long memberId, Pageable pageable) {
JPAQuery<Favorite> query = queryFactory
.selectDistinct(favorite)
.from(favorite)
.leftJoin(favorite.member, member)
.leftJoin(favorite.product, product)
.where(member.id.eq(memberId))
.offset(pageable.getOffset())
.orderBy(getAllOrderSpecifiers(pageable).toArray(OrderSpecifier[]::new))
.limit(pageable.getPageSize());
JPAQuery<Favorite> countQuery = queryFactory
.selectDistinct(favorite)
.from(favorite)
.leftJoin(favorite.member, member)
.leftJoin(favorite.product, product)
.where(member.id.eq(memberId));
List<Favorite> content = query.fetch();
return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetch().size());
}

private List<OrderSpecifier<?>> getAllOrderSpecifiers(Pageable pageable) {
List<OrderSpecifier<?>> ORDERS = new LinkedList<>();
ORDERS.add(QueryDslUtil.getSortedColumn(Order.DESC, favorite, "id"));
return ORDERS;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package com.fc.shimpyo_be.domain.favorite.repository;

import com.fc.shimpyo_be.domain.favorite.entity.Favorite;
import com.fc.shimpyo_be.domain.member.entity.Member;
import com.fc.shimpyo_be.domain.product.entity.Product;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FavoriteRepository extends JpaRepository<Favorite, Long> {
public interface FavoriteRepository extends JpaRepository<Favorite, Long>,
FavoriteCustomRepository {

Optional<Favorite> findByMemberAndProduct(Member member, Product product);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.fc.shimpyo_be.domain.favorite.service;

import com.fc.shimpyo_be.domain.favorite.dto.FavoriteResponseDto;
import com.fc.shimpyo_be.domain.favorite.dto.FavoritesResponseDto;
import com.fc.shimpyo_be.domain.favorite.entity.Favorite;
import com.fc.shimpyo_be.domain.favorite.exception.FavoriteAlreadyRegisterException;
import com.fc.shimpyo_be.domain.favorite.exception.FavoriteNotFoundException;
import com.fc.shimpyo_be.domain.favorite.repository.FavoriteRepository;
import com.fc.shimpyo_be.domain.member.entity.Member;
import com.fc.shimpyo_be.domain.member.service.MemberService;
import com.fc.shimpyo_be.domain.product.dto.response.ProductResponse;
import com.fc.shimpyo_be.domain.product.entity.Product;
import com.fc.shimpyo_be.domain.product.exception.ProductNotFoundException;
import com.fc.shimpyo_be.domain.product.repository.ProductRepository;
import com.fc.shimpyo_be.domain.product.util.ProductMapper;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class FavoriteService {

private final FavoriteRepository favoriteRepository;
private final MemberService memberService;
private final ProductRepository productRepository;

public FavoriteResponseDto register(long memberId, long productId) {
Member member = memberService.getMemberById(memberId);
Product product = productRepository.findById(productId).orElseThrow(
ProductNotFoundException::new);
Optional<Favorite> favorite = favoriteRepository.findByMemberAndProduct(member, product);
if (favorite.isPresent()) {
throw new FavoriteAlreadyRegisterException();
}
return FavoriteResponseDto.of(favoriteRepository.save(Favorite.builder()
.member(member)
.product(product)
.build()));
}

public FavoritesResponseDto getFavorites(long memberId, Pageable pageable) {
List<ProductResponse> productResponses = new ArrayList<>();
Member member = memberService.getMemberById(memberId);
Page<Favorite> favorites = favoriteRepository.findAllByMemberId(member.getId(), pageable);
for (Favorite favorite : favorites) {
productResponses.add(ProductMapper.toProductResponse(favorite.getProduct()));
}
return FavoritesResponseDto.builder()
.pageCount(favorites.getTotalPages())
.isLast(favorites.isLast())
.products(productResponses)
.build();
}

public FavoriteResponseDto delete(long memberId, long productId) {
Member member = memberService.getMemberById(memberId);
Product product = productRepository.findById(productId).orElseThrow(
ProductNotFoundException::new);
Favorite favorite = favoriteRepository.findByMemberAndProduct(member, product)
.orElseThrow(FavoriteNotFoundException::new);
FavoriteResponseDto favoriteResponseDto = FavoriteResponseDto.of(favorite);
favoriteRepository.delete(favorite);
return favoriteResponseDto;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@ public enum ErrorCode {
HTTP_CLIENT_CONNECTION_ERROR(HttpStatus.UNAUTHORIZED, "외부 API 연결에 실패했습니다."),
OPEN_API_ERROR(HttpStatus.NOT_FOUND, "오픈 API에서 데이터를 불러오는데 실패했습니다."),

//Common
INVALID_DATE(HttpStatus.BAD_REQUEST,"잘못된 날짜 데이터입니다.");
// Common
INVALID_DATE(HttpStatus.BAD_REQUEST,"잘못된 날짜 데이터입니다."),

// 즐겨찾기
FAVORITE_ALREADY_REGISTER(HttpStatus.BAD_REQUEST, "이미 즐겨찾기에 등록한 숙소입니다."),
FAVORITE_NOT_FOUND(HttpStatus.NOT_FOUND, "즐겨찾기 정보를 찾을 수 없습니다.");

private final HttpStatus httpStatus;
private final String simpleMessage;
Expand Down
Loading

0 comments on commit 4e16e10

Please sign in to comment.