From 828a1293b4a8b8b741bae0135f0a440f42d7b433 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sun, 29 Oct 2023 18:49:50 +0900 Subject: [PATCH] =?UTF-8?q?#49=20=EC=B0=9C=EA=BD=81=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=84=A4=EA=B3=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=9E=AC=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존) 비식별관계 테이블 설계 - 현재) 식별관계 테이블 설계 --- .../myongsick/domain/mark/MarkRequest.java | 19 +++ .../domain/mark/StoreMarkController.java | 95 ++++++++++++++ .../myongsick/domain/mark/entity/MarkId.java | 16 +++ .../domain/mark/entity/StoreMark.java | 46 +++++++ .../mark/repository/StoreMarkRepository.java | 14 ++ .../repository/StoreMarkRepositoryCustom.java | 17 +++ .../repository/StoreMarkRepositoryImpl.java | 122 ++++++++++++++++++ .../domain/mark/service/StoreMarkService.java | 13 ++ .../mark/service/StoreMarkServiceImpl.java | 85 ++++++++++++ .../domain/scrap/dto/ScrapResponse.java | 24 ++++ .../myongsick/domain/scrap/entity/Store.java | 4 + .../myongsick/domain/user/entity/User.java | 6 + 12 files changed, 461 insertions(+) create mode 100644 src/main/java/com/example/myongsick/domain/mark/MarkRequest.java create mode 100644 src/main/java/com/example/myongsick/domain/mark/StoreMarkController.java create mode 100644 src/main/java/com/example/myongsick/domain/mark/entity/MarkId.java create mode 100644 src/main/java/com/example/myongsick/domain/mark/entity/StoreMark.java create mode 100644 src/main/java/com/example/myongsick/domain/mark/repository/StoreMarkRepository.java create mode 100644 src/main/java/com/example/myongsick/domain/mark/repository/StoreMarkRepositoryCustom.java create mode 100644 src/main/java/com/example/myongsick/domain/mark/repository/StoreMarkRepositoryImpl.java create mode 100644 src/main/java/com/example/myongsick/domain/mark/service/StoreMarkService.java create mode 100644 src/main/java/com/example/myongsick/domain/mark/service/StoreMarkServiceImpl.java diff --git a/src/main/java/com/example/myongsick/domain/mark/MarkRequest.java b/src/main/java/com/example/myongsick/domain/mark/MarkRequest.java new file mode 100644 index 0000000..0eff51e --- /dev/null +++ b/src/main/java/com/example/myongsick/domain/mark/MarkRequest.java @@ -0,0 +1,19 @@ +package com.example.myongsick.domain.mark; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +@ApiModel(description = "찜꽁 리스트에 가게 등록") +public class MarkRequest { + @NotNull + @NotBlank + @ApiModelProperty(required = true, dataType = "String") + private String phoneId; + @NotNull @NotBlank + @ApiModelProperty(required = true, dataType = "String") + private String code; +} diff --git a/src/main/java/com/example/myongsick/domain/mark/StoreMarkController.java b/src/main/java/com/example/myongsick/domain/mark/StoreMarkController.java new file mode 100644 index 0000000..d672147 --- /dev/null +++ b/src/main/java/com/example/myongsick/domain/mark/StoreMarkController.java @@ -0,0 +1,95 @@ +package com.example.myongsick.domain.mark; + +import com.example.myongsick.domain.mark.service.StoreMarkService; +import com.example.myongsick.domain.scrap.dto.ScrapCountResponse; +import com.example.myongsick.domain.scrap.dto.ScrapResponse; +import com.example.myongsick.global.object.ApplicationResponse; +import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.Param; +import org.springframework.data.web.PageableDefault; +import org.springframework.stereotype.Controller; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v3/marks") +public class StoreMarkController { + + private final StoreMarkService storeMarkService; + + @GetMapping + @ApiOperation(value = "가게별 담은 유저 수 조회 \n" + + "") + public ApplicationResponse> getStoreMarkCntList( + @Param("campus") String campus, + @RequestParam(value = "offset", defaultValue = "0") + @Schema(title = "페이지 번호", example = "0", description = "0부터 시작합니다") + Integer offset, + @RequestParam(value = "limit", defaultValue = "10") + @Schema( + title = "Data 갯수", + example = "10", + description = "한 페이지에 보여지는 데이터 수 입니다.") + Integer limit, + @RequestParam(value = "orderBy") + @Schema( + title = "정렬 기준", + description = "정렬 기준은 distance (거리) | scrapCount (찜꽁수)가 있습니다..", + allowableValues = { + "distance", + "scrapCount", + }) + String orderBy + ) { + return ApplicationResponse.ok(storeMarkService.getMarkCount(campus, PageRequest.of(offset, limit), orderBy)); + } + + @PostMapping + @ApiOperation(value = "찜꽁 리스트 추가") + public ApplicationResponse createMark( + @RequestBody @Valid MarkRequest request + ) { + storeMarkService.createMark(request.getPhoneId(), request.getCode()); + return ApplicationResponse.ok(); + } + + @DeleteMapping + @ApiOperation(value = "찜꽁 리스트 삭제") + public ApplicationResponse deleteMark( + @RequestBody @Valid MarkRequest request + ) { + storeMarkService.deleteMark(request.getPhoneId(), request.getCode()); + return ApplicationResponse.ok(); + } + + @GetMapping("/my") + @ApiOperation(value = "유저 찜꽁 리스트 조회") + public ApplicationResponse> getMyMarkList( + @RequestParam String phoneId, + @RequestParam(value = "offset", defaultValue = "0") + @Schema(title = "페이지 번호", example = "0", description = "0부터 시작합니다") + Integer offset, + @RequestParam(value = "limit", defaultValue = "10") + @Schema( + title = "Data 갯수", + example = "10", + description = "한 페이지에 보여지는 데이터 수 입니다.") + Integer limit + ) { + return ApplicationResponse.ok(storeMarkService.getMarkList(phoneId, PageRequest.of(offset, limit))); + } +} diff --git a/src/main/java/com/example/myongsick/domain/mark/entity/MarkId.java b/src/main/java/com/example/myongsick/domain/mark/entity/MarkId.java new file mode 100644 index 0000000..9a79cb2 --- /dev/null +++ b/src/main/java/com/example/myongsick/domain/mark/entity/MarkId.java @@ -0,0 +1,16 @@ +package com.example.myongsick.domain.mark.entity; + +import java.io.Serializable; + +public class MarkId implements Serializable { + + private Long user; + private Long store; + + public MarkId(){} + public MarkId(Long user, Long store) { + super(); + this.user = user; + this.store = store; + } +} diff --git a/src/main/java/com/example/myongsick/domain/mark/entity/StoreMark.java b/src/main/java/com/example/myongsick/domain/mark/entity/StoreMark.java new file mode 100644 index 0000000..56a6119 --- /dev/null +++ b/src/main/java/com/example/myongsick/domain/mark/entity/StoreMark.java @@ -0,0 +1,46 @@ +package com.example.myongsick.domain.mark.entity; + +import com.example.myongsick.domain.scrap.entity.Store; +import com.example.myongsick.domain.user.entity.User; +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.Serializable; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "mark") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@IdClass(MarkId.class) +public class StoreMark implements Serializable { + + @Id + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @Id + @ManyToOne + @JoinColumn(name = "store_id") + private Store store; + + public StoreMark StoreMark( + User user, + Store store + ) { + this.user = user; + this.store = store; + return this; + } +} diff --git a/src/main/java/com/example/myongsick/domain/mark/repository/StoreMarkRepository.java b/src/main/java/com/example/myongsick/domain/mark/repository/StoreMarkRepository.java new file mode 100644 index 0000000..0216301 --- /dev/null +++ b/src/main/java/com/example/myongsick/domain/mark/repository/StoreMarkRepository.java @@ -0,0 +1,14 @@ +package com.example.myongsick.domain.mark.repository; + +import com.example.myongsick.domain.mark.entity.MarkId; +import com.example.myongsick.domain.mark.entity.StoreMark; +import com.example.myongsick.domain.scrap.entity.Scrap; +import java.util.List; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StoreMarkRepository extends JpaRepository { + Page findAllByUserId(Long userId, Pageable pageable); +} diff --git a/src/main/java/com/example/myongsick/domain/mark/repository/StoreMarkRepositoryCustom.java b/src/main/java/com/example/myongsick/domain/mark/repository/StoreMarkRepositoryCustom.java new file mode 100644 index 0000000..c3d1e20 --- /dev/null +++ b/src/main/java/com/example/myongsick/domain/mark/repository/StoreMarkRepositoryCustom.java @@ -0,0 +1,17 @@ +package com.example.myongsick.domain.mark.repository; + +import com.example.myongsick.domain.mark.entity.StoreMark; +import com.example.myongsick.domain.scrap.dto.ScrapCountResponse; +import com.example.myongsick.domain.scrap.dto.ScrapResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface StoreMarkRepositoryCustom { + + StoreMark findByUserIdAndStoreId(Long userId, Long storeId); + + Page findByCampus(String campus, Pageable pageable, String orderBy); + + Page findByUserId(Long userId, Pageable pageable); + +} diff --git a/src/main/java/com/example/myongsick/domain/mark/repository/StoreMarkRepositoryImpl.java b/src/main/java/com/example/myongsick/domain/mark/repository/StoreMarkRepositoryImpl.java new file mode 100644 index 0000000..883835a --- /dev/null +++ b/src/main/java/com/example/myongsick/domain/mark/repository/StoreMarkRepositoryImpl.java @@ -0,0 +1,122 @@ +package com.example.myongsick.domain.mark.repository; + +import static com.example.myongsick.domain.mark.entity.QStoreMark.storeMark; +import static com.example.myongsick.domain.scrap.entity.QStore.store; +import static com.example.myongsick.domain.user.entity.QUser.user; + +import com.example.myongsick.domain.mark.entity.StoreMark; +import com.example.myongsick.domain.scrap.dto.QScrapCountResponse; +import com.example.myongsick.domain.scrap.dto.QScrapResponse; +import com.example.myongsick.domain.scrap.dto.ScrapCountResponse; +import com.example.myongsick.domain.scrap.dto.ScrapResponse; +import com.example.myongsick.domain.scrap.entity.CampusType; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class StoreMarkRepositoryImpl implements StoreMarkRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + /** + * ============================================================================================= + * 유저가 가게를 찜꽁했는지 조회 -- 1건 조회 + * ============================================================================================= + * */ + @Override + public StoreMark findByUserIdAndStoreId(Long userId, Long storeId) { + return jpaQueryFactory + .select(storeMark) + .from(storeMark) + .where( + storeMark.user.id.eq(userId), + storeMark.store.id.eq(storeId)) + .fetchOne(); + } + /** + * ============================================================================================= + * 캠퍼스별 가게 목록 조회 -- 스크랩 수 포함 + * ============================================================================================= + * */ + @Override + public Page findByCampus(String campus, Pageable pageable, String orderBy) { + var result = jpaQueryFactory + .select(new QScrapCountResponse( + store.id.as("storeId"), + store.code, + store.name, + store.category, + store.address, + store.contact, + store.urlAddress, + store.distance, + store.latitude, + store.longitude, + store.storeMarkList.size().intValue().as("scrapCount") + )) + .from(store) + .where(store.campus.eq(CampusType.valueOf(campus))) + .orderBy(getOrderSpecifier(orderBy)) + .fetch(); + var countQuery = jpaQueryFactory + .select(store.storeMarkList.size()) + .from(store) + .where(store.campus.eq(CampusType.valueOf(campus))) + .fetch().size(); + return new PageImpl<>(result, pageable, countQuery); + } + /** + * ============================================================================================= + * 유저의 찜꽁리스트 조회 + * ============================================================================================= + * */ + @Override + public Page findByUserId(Long userId, Pageable pageable) { + var result = jpaQueryFactory + .select(new QScrapResponse( + store.id, + store.code, + store.name, + store.category, + store.address, + store.urlAddress, + store.distance, + store.latitude, + store.longitude + )) + .from(storeMark) + .leftJoin(storeMark.store, store) + .leftJoin(storeMark.user, user) + .where(user.id.eq(userId)) + .groupBy(storeMark.store, storeMark.user) + .fetch(); + var countQuery = jpaQueryFactory + .select(storeMark.count()) + .from(storeMark) + .leftJoin(storeMark.store, store) + .leftJoin(storeMark.user, user) + .where(user.id.eq(userId)) + .groupBy(storeMark.store, storeMark.user) + .fetch().size(); + System.out.println(countQuery); + return new PageImpl<>(result, pageable, countQuery); + } + /** + * ============================================================================================= + * PRIVATE FUNCTION + * ============================================================================================= + * */ + private OrderSpecifier getOrderSpecifier(String orderBy) { + if (orderBy.equals("distance")) { + return new OrderSpecifier(Order.DESC, store.distance.castToNum(Long.class)); + } else { + return new OrderSpecifier(Order.DESC, store.storeMarkList.size()); + } + } +} diff --git a/src/main/java/com/example/myongsick/domain/mark/service/StoreMarkService.java b/src/main/java/com/example/myongsick/domain/mark/service/StoreMarkService.java new file mode 100644 index 0000000..d307b39 --- /dev/null +++ b/src/main/java/com/example/myongsick/domain/mark/service/StoreMarkService.java @@ -0,0 +1,13 @@ +package com.example.myongsick.domain.mark.service; + +import com.example.myongsick.domain.scrap.dto.ScrapCountResponse; +import com.example.myongsick.domain.scrap.dto.ScrapResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface StoreMarkService { + Page getMarkList(String phoneId, Pageable pageable); + void createMark(String phoneId, String storeCode); + void deleteMark(String phoneId, String storeCode); + Page getMarkCount(String campus, Pageable pageable, String orderBy); +} diff --git a/src/main/java/com/example/myongsick/domain/mark/service/StoreMarkServiceImpl.java b/src/main/java/com/example/myongsick/domain/mark/service/StoreMarkServiceImpl.java new file mode 100644 index 0000000..6c23a40 --- /dev/null +++ b/src/main/java/com/example/myongsick/domain/mark/service/StoreMarkServiceImpl.java @@ -0,0 +1,85 @@ +package com.example.myongsick.domain.mark.service; + +import com.example.myongsick.domain.mark.entity.StoreMark; +import com.example.myongsick.domain.mark.repository.StoreMarkRepository; +import com.example.myongsick.domain.mark.repository.StoreMarkRepositoryCustom; +import com.example.myongsick.domain.scrap.dto.ScrapCountResponse; +import com.example.myongsick.domain.scrap.dto.ScrapResponse; +import com.example.myongsick.domain.scrap.entity.Store; +import com.example.myongsick.domain.scrap.exception.NotFoundStoreException; +import com.example.myongsick.domain.scrap.repository.StoreRepository; +import com.example.myongsick.domain.user.entity.User; +import com.example.myongsick.domain.user.exception.NotFoundUserException; +import com.example.myongsick.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class StoreMarkServiceImpl implements StoreMarkService { + /** + * ============================================================================================= + * DI for Repository + * ============================================================================================= + * */ + private final UserRepository userRepository; + private final StoreRepository storeRepository; + private final StoreMarkRepository storeMarkRepository; + private final StoreMarkRepositoryCustom storeMarkRepositoryCustom; + + /** + * ============================================================================================= + * 유저의 찜꽁리스트 조회 + * ============================================================================================= + * */ + @Override + public Page getMarkList(String phoneId, Pageable pageable) { + User user = userRepository.findByPhoneId(phoneId).orElseThrow(NotFoundUserException::new); + return storeMarkRepositoryCustom.findByUserId(user.getId(), pageable); + } + /** + * ============================================================================================= + * 찜꽁리스트에 가게 추가 + * ============================================================================================= + * */ + @Override + @Transactional + public void createMark(String phoneId, String storeCode) { + User user = userRepository.findByPhoneId(phoneId).orElseThrow(NotFoundUserException::new); + Store store = storeRepository.findByCode(storeCode).orElseThrow(NotFoundStoreException::new); + var mark = storeMarkRepositoryCustom.findByUserIdAndStoreId(user.getId(), store.getId()); + if (mark == null) { + mark = StoreMark.builder().user(user).store(store).build(); + } + storeMarkRepository.save(mark); + } + /** + * ============================================================================================= + * 찜꽁 리스트에서 가게 제거 + * ============================================================================================= + * */ + @Override + @Transactional + public void deleteMark(String phoneId, String storeCode) { + User user = userRepository.findByPhoneId(phoneId).orElseThrow(NotFoundUserException::new); + Store store = storeRepository.findByCode(storeCode).orElseThrow(NotFoundStoreException::new); + var mark = storeMarkRepositoryCustom.findByUserIdAndStoreId(user.getId(), store.getId()); + if (mark == null) { + return; + } + storeMarkRepository.delete(mark); + } + /** + * ============================================================================================= + * 찜꽁 수 포함 가게 리스트 조회 -- 스크랩수(DESC), 거리(ASC) 적용 + * ============================================================================================= + * */ + @Override + public Page getMarkCount(String campus, Pageable pageable, String orderBy) { + return storeMarkRepositoryCustom.findByCampus(campus, pageable, orderBy); + } +} diff --git a/src/main/java/com/example/myongsick/domain/scrap/dto/ScrapResponse.java b/src/main/java/com/example/myongsick/domain/scrap/dto/ScrapResponse.java index fc50bb6..628cc37 100644 --- a/src/main/java/com/example/myongsick/domain/scrap/dto/ScrapResponse.java +++ b/src/main/java/com/example/myongsick/domain/scrap/dto/ScrapResponse.java @@ -1,6 +1,7 @@ package com.example.myongsick.domain.scrap.dto; import com.example.myongsick.domain.scrap.entity.Scrap; +import com.querydsl.core.annotations.QueryProjection; import io.swagger.annotations.ApiModel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -39,4 +40,27 @@ public static ScrapResponse toDto(Scrap scrap) { .longitude(scrap.getStore().getLongitude()) .build(); } + + @QueryProjection + public ScrapResponse( + Long id, + String code, + String name, + String category, + String address, + String contact, + String urlAddress, + String latitude, + String longitude + ) { + this.id = id; + this.code = code; + this.name = name; + this.category = category; + this.address = address; + this.contact = contact; + this.urlAddress = urlAddress; + this.latitude = latitude; + this.longitude = longitude; + } } diff --git a/src/main/java/com/example/myongsick/domain/scrap/entity/Store.java b/src/main/java/com/example/myongsick/domain/scrap/entity/Store.java index d369695..0fdde0e 100644 --- a/src/main/java/com/example/myongsick/domain/scrap/entity/Store.java +++ b/src/main/java/com/example/myongsick/domain/scrap/entity/Store.java @@ -1,5 +1,6 @@ package com.example.myongsick.domain.scrap.entity; +import com.example.myongsick.domain.mark.entity.StoreMark; import java.util.ArrayList; import java.util.List; import javax.persistence.CascadeType; @@ -39,6 +40,9 @@ public class Store { @OneToMany(mappedBy = "store", cascade = CascadeType.ALL) List scrapList = new ArrayList<>(); + @OneToMany(mappedBy = "store", cascade = CascadeType.ALL) + List storeMarkList = new ArrayList<>(); + @Builder public Store( String code, diff --git a/src/main/java/com/example/myongsick/domain/user/entity/User.java b/src/main/java/com/example/myongsick/domain/user/entity/User.java index 254ed98..5f43a2a 100644 --- a/src/main/java/com/example/myongsick/domain/user/entity/User.java +++ b/src/main/java/com/example/myongsick/domain/user/entity/User.java @@ -1,9 +1,12 @@ package com.example.myongsick.domain.user.entity; +import com.example.myongsick.domain.mark.entity.StoreMark; import com.example.myongsick.domain.review.entity.Review; import com.example.myongsick.domain.scrap.entity.Scrap; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; @@ -37,6 +40,9 @@ public class User { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) List reviewList = new ArrayList<>(); + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) + List storeMarkList = new ArrayList<>(); + @Builder public User(String phoneId) { this.phoneId = phoneId;