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

feat: 즐겨찾기 기능 구현 #116

Merged
merged 33 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bff9b88
#113 feat: 즐겨찾기 관련 에러코드 추가
JeongUijeong Dec 12, 2023
f6bb04a
#113 feat: 이미 즐겨찾기 등록한 숙소에 대한 등록 예외 생성
JeongUijeong Dec 12, 2023
9ad0dbd
#113 feat: 즐겨찾기 응답 DTO 생성
JeongUijeong Dec 12, 2023
ef25a2b
#113 feat: 회원, 숙소로 즐겨찾기 내역 조회 메서드 추가
JeongUijeong Dec 12, 2023
4c8e882
#113 feat: 즐겨찾기 등록 기능 구현
JeongUijeong Dec 12, 2023
4b6de65
#113 feat: 즐겨찾기 등록 API 구현
JeongUijeong Dec 12, 2023
b9c5d1a
#113 feat: 회원으로 즐겨찾기 목록 조회 메서드 추가
JeongUijeong Dec 12, 2023
aa30b7b
#113 feat: 즐겨찾기 조회 기능 구현
JeongUijeong Dec 12, 2023
c62dfb0
#113 feat: 즐겨찾기 목록 조회 API 구현
JeongUijeong Dec 12, 2023
724fcd7
#113 refactor: 즐겨찾기 예외 코드 수정
JeongUijeong Dec 12, 2023
e1e1ed9
#113 feat: 즐겨찾기 조회 실패 익셉션 생성
JeongUijeong Dec 12, 2023
ad63211
#113 feat: 즐겨찾기 삭제 기능 구현
JeongUijeong Dec 12, 2023
37a2142
#113 refactor: 즐겨찾기 삭제 메서드명 수정
JeongUijeong Dec 12, 2023
d0fef4f
#113 feat: 즐겨찾기 취소 API 구현
JeongUijeong Dec 12, 2023
61b7cda
#113 fix: 조건문 에러 수정
JeongUijeong Dec 12, 2023
a1bf2fe
#113 test: 즐겨찾기 서비스 테스트 코드 작성
JeongUijeong Dec 12, 2023
0f59c07
#113 refactor: 회원을 통한 즐겨찾기 목록 조회 메서드명 수정
JeongUijeong Dec 12, 2023
e2caefe
#113 refactor: 회원을 통한 즐겨찾기 목록 조회 메서드명 수정 적용
JeongUijeong Dec 12, 2023
3c77845
#113 test: 회원을 통한 즐겨찾기 목록 조회 메서드명 수정 적용
JeongUijeong Dec 12, 2023
11b7aa5
#113 test: 즐겨찾기 레포지토리 테스트 코드 작성
JeongUijeong Dec 12, 2023
3391f34
#113 feat: 즐겨찾기 커스텀 인터페이스 인터페이스 생성
JeongUijeong Dec 12, 2023
09e8733
#113 feat: 즐겨찾기 커스텀 레포지토리 구현체 구현
JeongUijeong Dec 12, 2023
0cea8f4
#113 feat: 페이징 기능 추가된 즐겨찾기 목록 조회 응답 DTO 생성
JeongUijeong Dec 12, 2023
b759dde
#113 refactor: 즐겨찾기 커스텀 레포지토리를 상속 받도록 수정
JeongUijeong Dec 12, 2023
c629aef
#113 feat: 즐겨찾기 목록 조회 서비스에 페이징 기능 추가
JeongUijeong Dec 12, 2023
dcfb89b
#113 feat: 즐겨찾기 목록 조회 API에 페이징 기능 추가
JeongUijeong Dec 12, 2023
b0b739c
#113 test: 즐겨찾기 목록 조회 페이징 기능 추가 적용 수정
JeongUijeong Dec 12, 2023
e1f3c7e
#113 test: 즐겨찾기 목록 조회 페이징 기능 추가 적용 수정
JeongUijeong Dec 12, 2023
5cb315e
#113 test: 즐겨찾기 컨트롤러 테스트 작성
JeongUijeong Dec 12, 2023
4f2b467
#113 test: 즐겨찾기 API 문서화를 위한 테스트 코드 작성
JeongUijeong Dec 12, 2023
61d9283
#113 docs: 즐겨찾기 API adoc 생성
JeongUijeong Dec 12, 2023
b278711
Merge branch 'develop' of https://github.com/Shimpyo-House/Shimpyo_BE…
JeongUijeong Dec 12, 2023
8dbcea1
#113 chore: 오타 수정
JeongUijeong Dec 12, 2023
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
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::[]

= Member REST API Docs
JeongUijeong marked this conversation as resolved.
Show resolved Hide resolved
: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