Merge branch 'main' of https://github.com/dg04-hi/hi-backend
This commit is contained in:
commit
2345a4a41a
@ -0,0 +1,138 @@
|
|||||||
|
package com.ktds.hi.store.biz.usecase.out;
|
||||||
|
|
||||||
|
import com.ktds.hi.store.domain.Store;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 리포지토리 포트 인터페이스
|
||||||
|
* 매장 데이터 영속성 기능을 정의
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
public interface StoreRepositoryPort {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 점주 ID로 매장 목록 조회
|
||||||
|
*
|
||||||
|
* @param ownerId 점주 ID
|
||||||
|
* @return 매장 목록
|
||||||
|
*/
|
||||||
|
List<Store> findStoresByOwnerId(Long ownerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 ID로 매장 조회
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @return 매장 정보 (Optional)
|
||||||
|
*/
|
||||||
|
Optional<Store> findStoreById(Long storeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 ID와 점주 ID로 매장 조회
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @param ownerId 점주 ID
|
||||||
|
* @return 매장 정보 (Optional)
|
||||||
|
*/
|
||||||
|
Optional<Store> findStoreByIdAndOwnerId(Long storeId, Long ownerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 저장
|
||||||
|
*
|
||||||
|
* @param store 저장할 매장 정보
|
||||||
|
* @return 저장된 매장 정보
|
||||||
|
*/
|
||||||
|
Store saveStore(Store store);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 삭제
|
||||||
|
*
|
||||||
|
* @param storeId 삭제할 매장 ID
|
||||||
|
*/
|
||||||
|
void deleteStore(Long storeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 검색
|
||||||
|
*
|
||||||
|
* @param searchCriteria 검색 조건
|
||||||
|
* @return 검색된 매장 목록
|
||||||
|
*/
|
||||||
|
List<Store> searchStores(StoreSearchCriteria searchCriteria);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카테고리별 매장 목록 조회
|
||||||
|
*
|
||||||
|
* @param category 카테고리
|
||||||
|
* @return 카테고리별 매장 목록
|
||||||
|
*/
|
||||||
|
List<Store> findStoresByCategory(String category);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 위치 기반 매장 검색 (반경 내)
|
||||||
|
*
|
||||||
|
* @param latitude 위도
|
||||||
|
* @param longitude 경도
|
||||||
|
* @param radiusKm 반경 (킬로미터)
|
||||||
|
* @return 반경 내 매장 목록
|
||||||
|
*/
|
||||||
|
List<Store> findStoresWithinRadius(Double latitude, Double longitude, Double radiusKm);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장명 또는 주소로 검색
|
||||||
|
*
|
||||||
|
* @param keyword 검색 키워드
|
||||||
|
* @return 검색된 매장 목록
|
||||||
|
*/
|
||||||
|
List<Store> findStoresByKeyword(String keyword);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 활성 상태의 매장 목록 조회
|
||||||
|
*
|
||||||
|
* @return 활성 매장 목록
|
||||||
|
*/
|
||||||
|
List<Store> findActiveStores();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 평점 기준 상위 매장 조회
|
||||||
|
*
|
||||||
|
* @param limit 조회할 매장 수
|
||||||
|
* @return 상위 평점 매장 목록
|
||||||
|
*/
|
||||||
|
List<Store> findTopRatedStores(Integer limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 존재 여부 확인
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @return 존재 여부
|
||||||
|
*/
|
||||||
|
boolean existsById(Long storeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 점주의 매장 수 조회
|
||||||
|
*
|
||||||
|
* @param ownerId 점주 ID
|
||||||
|
* @return 매장 수
|
||||||
|
*/
|
||||||
|
Long countStoresByOwnerId(Long ownerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 상태 업데이트
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @param status 새로운 상태
|
||||||
|
* @return 업데이트된 매장 정보
|
||||||
|
*/
|
||||||
|
Store updateStoreStatus(Long storeId, String status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 여러 매장 일괄 저장
|
||||||
|
*
|
||||||
|
* @param stores 저장할 매장 목록
|
||||||
|
* @return 저장된 매장 목록
|
||||||
|
*/
|
||||||
|
List<Store> saveStores(List<Store> stores);
|
||||||
|
}
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
package com.ktds.hi.store.biz.usecase.out;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 검색 조건 클래스
|
||||||
|
* 매장 검색 시 사용되는 필터 조건들을 정의
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class StoreSearchCriteria {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검색 키워드 (매장명, 주소)
|
||||||
|
*/
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카테고리 필터
|
||||||
|
*/
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 태그 필터 목록
|
||||||
|
*/
|
||||||
|
private List<String> tags;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검색 중심 위도
|
||||||
|
*/
|
||||||
|
private Double latitude;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검색 중심 경도
|
||||||
|
*/
|
||||||
|
private Double longitude;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검색 반경 (킬로미터)
|
||||||
|
*/
|
||||||
|
private Double radiusKm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 최소 평점
|
||||||
|
*/
|
||||||
|
private Double minRating;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 최대 평점
|
||||||
|
*/
|
||||||
|
private Double maxRating;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 상태 필터
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 정렬 기준 (rating, distance, reviewCount 등)
|
||||||
|
*/
|
||||||
|
private String sortBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 정렬 방향 (ASC, DESC)
|
||||||
|
*/
|
||||||
|
private String sortDirection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 페이지 번호 (0부터 시작)
|
||||||
|
*/
|
||||||
|
private Integer page;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 페이지 크기
|
||||||
|
*/
|
||||||
|
private Integer size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검색 조건 유효성 검증
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
// 위치 기반 검색인 경우 위도, 경도, 반경이 모두 있어야 함
|
||||||
|
if (latitude != null || longitude != null || radiusKm != null) {
|
||||||
|
return latitude != null && longitude != null && radiusKm != null &&
|
||||||
|
latitude >= -90 && latitude <= 90 &&
|
||||||
|
longitude >= -180 && longitude <= 180 &&
|
||||||
|
radiusKm > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 키워드나 카테고리 중 하나는 있어야 함
|
||||||
|
return (keyword != null && !keyword.trim().isEmpty()) ||
|
||||||
|
(category != null && !category.trim().isEmpty()) ||
|
||||||
|
(tags != null && !tags.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 위치 기반 검색 여부 확인
|
||||||
|
*/
|
||||||
|
public boolean hasLocationFilter() {
|
||||||
|
return latitude != null && longitude != null && radiusKm != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 키워드 검색 여부 확인
|
||||||
|
*/
|
||||||
|
public boolean hasKeywordFilter() {
|
||||||
|
return keyword != null && !keyword.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카테고리 필터 여부 확인
|
||||||
|
*/
|
||||||
|
public boolean hasCategoryFilter() {
|
||||||
|
return category != null && !category.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 태그 필터 여부 확인
|
||||||
|
*/
|
||||||
|
public boolean hasTagFilter() {
|
||||||
|
return tags != null && !tags.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 평점 필터 여부 확인
|
||||||
|
*/
|
||||||
|
public boolean hasRatingFilter() {
|
||||||
|
return minRating != null || maxRating != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 정렬 조건 여부 확인
|
||||||
|
*/
|
||||||
|
public boolean hasSortFilter() {
|
||||||
|
return sortBy != null && !sortBy.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본 페이징 설정 적용
|
||||||
|
*/
|
||||||
|
public StoreSearchCriteria withDefaultPaging() {
|
||||||
|
return StoreSearchCriteria.builder()
|
||||||
|
.keyword(this.keyword)
|
||||||
|
.category(this.category)
|
||||||
|
.tags(this.tags)
|
||||||
|
.latitude(this.latitude)
|
||||||
|
.longitude(this.longitude)
|
||||||
|
.radiusKm(this.radiusKm)
|
||||||
|
.minRating(this.minRating)
|
||||||
|
.maxRating(this.maxRating)
|
||||||
|
.status(this.status)
|
||||||
|
.sortBy(this.sortBy != null ? this.sortBy : "rating")
|
||||||
|
.sortDirection(this.sortDirection != null ? this.sortDirection : "DESC")
|
||||||
|
.page(this.page != null ? this.page : 0)
|
||||||
|
.size(this.size != null ? this.size : 20)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,10 +5,14 @@ import lombok.Builder;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 도메인 엔티티
|
* 메뉴 도메인 클래스
|
||||||
|
* 메뉴 정보를 담는 도메인 객체
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@Builder
|
@Builder
|
||||||
@ -18,10 +22,169 @@ public class Menu {
|
|||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
private Long storeId;
|
private Long storeId;
|
||||||
private String name;
|
private String menuName;
|
||||||
private String description;
|
private String description;
|
||||||
private BigDecimal price;
|
private Integer price;
|
||||||
private String category;
|
private String category;
|
||||||
private String imageUrl;
|
private String imageUrl;
|
||||||
private Boolean isAvailable;
|
private Boolean isAvailable;
|
||||||
|
private Integer orderCount;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 기본 정보 업데이트
|
||||||
|
*/
|
||||||
|
public Menu updateInfo(String menuName, String description, Integer price) {
|
||||||
|
return Menu.builder()
|
||||||
|
.id(this.id)
|
||||||
|
.storeId(this.storeId)
|
||||||
|
.menuName(menuName)
|
||||||
|
.description(description)
|
||||||
|
.price(price)
|
||||||
|
.category(this.category)
|
||||||
|
.imageUrl(this.imageUrl)
|
||||||
|
.isAvailable(this.isAvailable)
|
||||||
|
.orderCount(this.orderCount)
|
||||||
|
.createdAt(this.createdAt)
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 이미지 업데이트
|
||||||
|
*/
|
||||||
|
public Menu updateImage(String imageUrl) {
|
||||||
|
return Menu.builder()
|
||||||
|
.id(this.id)
|
||||||
|
.storeId(this.storeId)
|
||||||
|
.menuName(this.menuName)
|
||||||
|
.description(this.description)
|
||||||
|
.price(this.price)
|
||||||
|
.category(this.category)
|
||||||
|
.imageUrl(imageUrl)
|
||||||
|
.isAvailable(this.isAvailable)
|
||||||
|
.orderCount(this.orderCount)
|
||||||
|
.createdAt(this.createdAt)
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 판매 가능 상태 설정
|
||||||
|
*/
|
||||||
|
public Menu setAvailable(Boolean available) {
|
||||||
|
return Menu.builder()
|
||||||
|
.id(this.id)
|
||||||
|
.storeId(this.storeId)
|
||||||
|
.menuName(this.menuName)
|
||||||
|
.description(this.description)
|
||||||
|
.price(this.price)
|
||||||
|
.category(this.category)
|
||||||
|
.imageUrl(this.imageUrl)
|
||||||
|
.isAvailable(available)
|
||||||
|
.orderCount(this.orderCount)
|
||||||
|
.createdAt(this.createdAt)
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 주문 수 증가
|
||||||
|
*/
|
||||||
|
public Menu incrementOrderCount() {
|
||||||
|
return Menu.builder()
|
||||||
|
.id(this.id)
|
||||||
|
.storeId(this.storeId)
|
||||||
|
.menuName(this.menuName)
|
||||||
|
.description(this.description)
|
||||||
|
.price(this.price)
|
||||||
|
.category(this.category)
|
||||||
|
.imageUrl(this.imageUrl)
|
||||||
|
.isAvailable(this.isAvailable)
|
||||||
|
.orderCount(this.orderCount != null ? this.orderCount + 1 : 1)
|
||||||
|
.createdAt(this.createdAt)
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 판매 가능 여부 확인
|
||||||
|
*/
|
||||||
|
public Boolean isAvailable() {
|
||||||
|
return this.isAvailable != null && this.isAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 매장에 속하는지 확인
|
||||||
|
*/
|
||||||
|
public boolean belongsToStore(Long storeId) {
|
||||||
|
return this.storeId != null && this.storeId.equals(storeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴가 유효한지 확인
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
return this.menuName != null && !this.menuName.trim().isEmpty() &&
|
||||||
|
this.price != null && this.price > 0 &&
|
||||||
|
this.storeId != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 카테고리 업데이트
|
||||||
|
*/
|
||||||
|
public Menu updateCategory(String category) {
|
||||||
|
return Menu.builder()
|
||||||
|
.id(this.id)
|
||||||
|
.storeId(this.storeId)
|
||||||
|
.menuName(this.menuName)
|
||||||
|
.description(this.description)
|
||||||
|
.price(this.price)
|
||||||
|
.category(category)
|
||||||
|
.imageUrl(this.imageUrl)
|
||||||
|
.isAvailable(this.isAvailable)
|
||||||
|
.orderCount(this.orderCount)
|
||||||
|
.createdAt(this.createdAt)
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 가격 할인 적용
|
||||||
|
*/
|
||||||
|
public Menu applyDiscount(double discountRate) {
|
||||||
|
if (discountRate < 0 || discountRate > 1) {
|
||||||
|
throw new IllegalArgumentException("할인율은 0~1 사이의 값이어야 합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int discountedPrice = (int) (this.price * (1 - discountRate));
|
||||||
|
|
||||||
|
return Menu.builder()
|
||||||
|
.id(this.id)
|
||||||
|
.storeId(this.storeId)
|
||||||
|
.menuName(this.menuName)
|
||||||
|
.description(this.description)
|
||||||
|
.price(discountedPrice)
|
||||||
|
.category(this.category)
|
||||||
|
.imageUrl(this.imageUrl)
|
||||||
|
.isAvailable(this.isAvailable)
|
||||||
|
.orderCount(this.orderCount)
|
||||||
|
.createdAt(this.createdAt)
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 정보가 변경되었는지 확인
|
||||||
|
*/
|
||||||
|
public boolean hasChanges(Menu other) {
|
||||||
|
if (other == null) return true;
|
||||||
|
|
||||||
|
return !this.menuName.equals(other.menuName) ||
|
||||||
|
!this.description.equals(other.description) ||
|
||||||
|
!this.price.equals(other.price) ||
|
||||||
|
!this.category.equals(other.category) ||
|
||||||
|
!this.isAvailable.equals(other.isAvailable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.ktds.hi.store.biz.domain;
|
package com.ktds.hi.store.domain;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
|||||||
@ -1,10 +1,15 @@
|
|||||||
package com.ktds.hi.store.infra.gateway;
|
package com.ktds.hi.store.infra.gateway;
|
||||||
|
|
||||||
import com.ktds.hi.store.biz.domain.Store;
|
import com.ktds.hi.store.domain.Store;
|
||||||
|
import com.ktds.hi.store.domain.StoreStatus;
|
||||||
import com.ktds.hi.store.biz.usecase.out.StoreRepositoryPort;
|
import com.ktds.hi.store.biz.usecase.out.StoreRepositoryPort;
|
||||||
|
import com.ktds.hi.store.biz.usecase.out.StoreSearchCriteria;
|
||||||
import com.ktds.hi.store.infra.gateway.entity.StoreEntity;
|
import com.ktds.hi.store.infra.gateway.entity.StoreEntity;
|
||||||
import com.ktds.hi.store.infra.gateway.repository.StoreJpaRepository;
|
import com.ktds.hi.store.infra.gateway.repository.StoreJpaRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -15,6 +20,9 @@ import java.util.stream.Collectors;
|
|||||||
/**
|
/**
|
||||||
* 매장 리포지토리 어댑터 클래스
|
* 매장 리포지토리 어댑터 클래스
|
||||||
* Store Repository Port를 구현하여 데이터 영속성 기능을 제공
|
* Store Repository Port를 구현하여 데이터 영속성 기능을 제공
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -54,6 +62,110 @@ public class StoreRepositoryAdapter implements StoreRepositoryPort {
|
|||||||
storeJpaRepository.deleteById(storeId);
|
storeJpaRepository.deleteById(storeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Store> searchStores(StoreSearchCriteria searchCriteria) {
|
||||||
|
// 복잡한 검색 로직을 단순화 - 실제로는 QueryDSL이나 Criteria API 사용 권장
|
||||||
|
List<StoreEntity> entities;
|
||||||
|
|
||||||
|
if (searchCriteria.hasKeywordFilter()) {
|
||||||
|
entities = storeJpaRepository.findByStoreNameContainingOrAddressContaining(
|
||||||
|
searchCriteria.getKeyword(), searchCriteria.getKeyword());
|
||||||
|
} else if (searchCriteria.hasCategoryFilter()) {
|
||||||
|
entities = storeJpaRepository.findByCategory(searchCriteria.getCategory());
|
||||||
|
} else {
|
||||||
|
entities = storeJpaRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities.stream()
|
||||||
|
.map(this::toDomain)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Store> findStoresByCategory(String category) {
|
||||||
|
return storeJpaRepository.findByCategory(category)
|
||||||
|
.stream()
|
||||||
|
.map(this::toDomain)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Store> findStoresWithinRadius(Double latitude, Double longitude, Double radiusKm) {
|
||||||
|
// 실제로는 공간 데이터베이스 쿼리 사용 (PostGIS 등)
|
||||||
|
// 여기서는 단순한 구현으로 대체
|
||||||
|
return storeJpaRepository.findAll()
|
||||||
|
.stream()
|
||||||
|
.map(this::toDomain)
|
||||||
|
.filter(store -> {
|
||||||
|
if (store.getLatitude() == null || store.getLongitude() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Double distance = store.calculateDistance(latitude, longitude);
|
||||||
|
return distance != null && distance <= radiusKm;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Store> findStoresByKeyword(String keyword) {
|
||||||
|
return storeJpaRepository.findByStoreNameContainingOrAddressContaining(keyword, keyword)
|
||||||
|
.stream()
|
||||||
|
.map(this::toDomain)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Store> findActiveStores() {
|
||||||
|
return storeJpaRepository.findByStatus(StoreStatus.ACTIVE.name())
|
||||||
|
.stream()
|
||||||
|
.map(this::toDomain)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Store> findTopRatedStores(Integer limit) {
|
||||||
|
Pageable pageable = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "rating"));
|
||||||
|
return storeJpaRepository.findAllByOrderByRatingDesc(pageable)
|
||||||
|
.stream()
|
||||||
|
.map(this::toDomain)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean existsById(Long storeId) {
|
||||||
|
return storeJpaRepository.existsById(storeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long countStoresByOwnerId(Long ownerId) {
|
||||||
|
return storeJpaRepository.countByOwnerId(ownerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Store updateStoreStatus(Long storeId, String status) {
|
||||||
|
Optional<StoreEntity> entityOpt = storeJpaRepository.findById(storeId);
|
||||||
|
if (entityOpt.isPresent()) {
|
||||||
|
StoreEntity entity = entityOpt.get();
|
||||||
|
entity.updateStatus(status);
|
||||||
|
StoreEntity saved = storeJpaRepository.save(entity);
|
||||||
|
return toDomain(saved);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Store> saveStores(List<Store> stores) {
|
||||||
|
List<StoreEntity> entities = stores.stream()
|
||||||
|
.map(this::toEntity)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<StoreEntity> savedEntities = storeJpaRepository.saveAll(entities);
|
||||||
|
|
||||||
|
return savedEntities.stream()
|
||||||
|
.map(this::toDomain)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity를 Domain으로 변환
|
* Entity를 Domain으로 변환
|
||||||
*/
|
*/
|
||||||
@ -70,7 +182,7 @@ public class StoreRepositoryAdapter implements StoreRepositoryPort {
|
|||||||
.phone(entity.getPhone())
|
.phone(entity.getPhone())
|
||||||
.operatingHours(entity.getOperatingHours())
|
.operatingHours(entity.getOperatingHours())
|
||||||
.tags(entity.getTagsJson() != null ? parseTagsJson(entity.getTagsJson()) : List.of())
|
.tags(entity.getTagsJson() != null ? parseTagsJson(entity.getTagsJson()) : List.of())
|
||||||
.status(entity.getStatus())
|
.status(StoreStatus.fromString(entity.getStatus()))
|
||||||
.rating(entity.getRating())
|
.rating(entity.getRating())
|
||||||
.reviewCount(entity.getReviewCount())
|
.reviewCount(entity.getReviewCount())
|
||||||
.imageUrl(entity.getImageUrl())
|
.imageUrl(entity.getImageUrl())
|
||||||
@ -95,7 +207,7 @@ public class StoreRepositoryAdapter implements StoreRepositoryPort {
|
|||||||
.phone(domain.getPhone())
|
.phone(domain.getPhone())
|
||||||
.operatingHours(domain.getOperatingHours())
|
.operatingHours(domain.getOperatingHours())
|
||||||
.tagsJson(domain.getTags() != null ? String.join(",", domain.getTags()) : "")
|
.tagsJson(domain.getTags() != null ? String.join(",", domain.getTags()) : "")
|
||||||
.status(domain.getStatus())
|
.status(domain.getStatus() != null ? domain.getStatus().name() : StoreStatus.INACTIVE.name())
|
||||||
.rating(domain.getRating())
|
.rating(domain.getRating())
|
||||||
.reviewCount(domain.getReviewCount())
|
.reviewCount(domain.getReviewCount())
|
||||||
.imageUrl(domain.getImageUrl())
|
.imageUrl(domain.getImageUrl())
|
||||||
|
|||||||
@ -0,0 +1,177 @@
|
|||||||
|
package com.ktds.hi.store.infra.gateway.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 엔티티 클래스
|
||||||
|
* 데이터베이스 stores 테이블과 매핑되는 JPA 엔티티
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "stores")
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
public class StoreEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "owner_id", nullable = false)
|
||||||
|
private Long ownerId;
|
||||||
|
|
||||||
|
@Column(name = "store_name", nullable = false, length = 100)
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 300)
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@Column(precision = 10, scale = 8)
|
||||||
|
private Double latitude;
|
||||||
|
|
||||||
|
@Column(precision = 11, scale = 8)
|
||||||
|
private Double longitude;
|
||||||
|
|
||||||
|
@Column(length = 500)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(length = 20)
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Column(name = "operating_hours", length = 200)
|
||||||
|
private String operatingHours;
|
||||||
|
|
||||||
|
@Column(length = 50)
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Column(name = "tags_json", columnDefinition = "TEXT")
|
||||||
|
private String tagsJson;
|
||||||
|
|
||||||
|
@Column(length = 20)
|
||||||
|
@Builder.Default
|
||||||
|
private String status = "INACTIVE";
|
||||||
|
|
||||||
|
@Column(precision = 3, scale = 2)
|
||||||
|
@Builder.Default
|
||||||
|
private Double rating = 0.0;
|
||||||
|
|
||||||
|
@Column(name = "review_count")
|
||||||
|
@Builder.Default
|
||||||
|
private Integer reviewCount = 0;
|
||||||
|
|
||||||
|
@Column(name = "image_url", length = 500)
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "created_at", updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@LastModifiedDate
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 상태 업데이트
|
||||||
|
*/
|
||||||
|
public void updateStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 기본 정보 업데이트
|
||||||
|
*/
|
||||||
|
public void updateBasicInfo(String storeName, String address, String description,
|
||||||
|
String phone, String operatingHours) {
|
||||||
|
this.storeName = storeName;
|
||||||
|
this.address = address;
|
||||||
|
this.description = description;
|
||||||
|
this.phone = phone;
|
||||||
|
this.operatingHours = operatingHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 위치 정보 업데이트
|
||||||
|
*/
|
||||||
|
public void updateLocation(Double latitude, Double longitude) {
|
||||||
|
this.latitude = latitude;
|
||||||
|
this.longitude = longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 평점 및 리뷰 수 업데이트
|
||||||
|
*/
|
||||||
|
public void updateRating(Double rating, Integer reviewCount) {
|
||||||
|
this.rating = rating;
|
||||||
|
this.reviewCount = reviewCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 태그 업데이트
|
||||||
|
*/
|
||||||
|
public void updateTags(String tagsJson) {
|
||||||
|
this.tagsJson = tagsJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 카테고리 업데이트
|
||||||
|
*/
|
||||||
|
public void updateCategory(String category) {
|
||||||
|
this.category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 이미지 URL 업데이트
|
||||||
|
*/
|
||||||
|
public void updateImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장이 활성 상태인지 확인
|
||||||
|
*/
|
||||||
|
public boolean isActive() {
|
||||||
|
return "ACTIVE".equals(this.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 점주의 매장인지 확인
|
||||||
|
*/
|
||||||
|
public boolean isOwnedBy(Long ownerId) {
|
||||||
|
return this.ownerId != null && this.ownerId.equals(ownerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 위치 정보가 있는지 확인
|
||||||
|
*/
|
||||||
|
public boolean hasLocationInfo() {
|
||||||
|
return this.latitude != null && this.longitude != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 평점이 설정되어 있는지 확인
|
||||||
|
*/
|
||||||
|
public boolean hasRating() {
|
||||||
|
return this.rating != null && this.rating > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장에 리뷰가 있는지 확인
|
||||||
|
*/
|
||||||
|
public boolean hasReviews() {
|
||||||
|
return this.reviewCount != null && this.reviewCount > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
package com.ktds.hi.store.infra.gateway.repository;
|
||||||
|
|
||||||
|
import com.ktds.hi.store.infra.gateway.entity.StoreEntity;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 JPA 리포지토리 인터페이스
|
||||||
|
* 매장 데이터의 CRUD 작업을 담당
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface StoreJpaRepository extends JpaRepository<StoreEntity, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 점주 ID로 매장 목록 조회
|
||||||
|
*/
|
||||||
|
List<StoreEntity> findByOwnerId(Long ownerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 ID와 점주 ID로 매장 조회
|
||||||
|
*/
|
||||||
|
Optional<StoreEntity> findByIdAndOwnerId(Long id, Long ownerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장명 또는 주소로 검색
|
||||||
|
*/
|
||||||
|
List<StoreEntity> findByStoreNameContainingOrAddressContaining(String storeName, String address);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카테고리로 매장 조회
|
||||||
|
*/
|
||||||
|
List<StoreEntity> findByCategory(String category);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상태로 매장 조회
|
||||||
|
*/
|
||||||
|
List<StoreEntity> findByStatus(String status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 평점 기준 내림차순으로 매장 조회
|
||||||
|
*/
|
||||||
|
@Query("SELECT s FROM StoreEntity s ORDER BY s.rating DESC")
|
||||||
|
Page<StoreEntity> findAllByOrderByRatingDesc(Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 점주별 매장 수 조회
|
||||||
|
*/
|
||||||
|
Long countByOwnerId(Long ownerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 활성 상태 매장 조회
|
||||||
|
*/
|
||||||
|
List<StoreEntity> findByStatusAndRatingGreaterThanEqual(String status, Double minRating);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카테고리와 상태로 매장 조회 (평점 내림차순)
|
||||||
|
*/
|
||||||
|
List<StoreEntity> findByCategoryAndStatusOrderByRatingDesc(String category, String status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 위치 기반 매장 검색 (네이티브 쿼리 - PostGIS 사용 시)
|
||||||
|
*/
|
||||||
|
@Query(value = "SELECT * FROM stores s WHERE " +
|
||||||
|
"ST_DWithin(ST_Point(s.longitude, s.latitude)::geography, " +
|
||||||
|
"ST_Point(:longitude, :latitude)::geography, :radiusMeters) " +
|
||||||
|
"AND s.status = 'ACTIVE'", nativeQuery = true)
|
||||||
|
List<StoreEntity> findStoresWithinRadius(@Param("latitude") Double latitude,
|
||||||
|
@Param("longitude") Double longitude,
|
||||||
|
@Param("radiusMeters") Double radiusMeters);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 키워드로 매장 검색 (매장명, 주소, 설명 포함)
|
||||||
|
*/
|
||||||
|
@Query("SELECT s FROM StoreEntity s WHERE " +
|
||||||
|
"s.storeName LIKE %:keyword% OR " +
|
||||||
|
"s.address LIKE %:keyword% OR " +
|
||||||
|
"s.description LIKE %:keyword%")
|
||||||
|
List<StoreEntity> findByKeyword(@Param("keyword") String keyword);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 평점 범위로 매장 조회
|
||||||
|
*/
|
||||||
|
List<StoreEntity> findByRatingBetweenAndStatus(Double minRating, Double maxRating, String status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 리뷰 수 기준으로 인기 매장 조회
|
||||||
|
*/
|
||||||
|
@Query("SELECT s FROM StoreEntity s WHERE s.status = 'ACTIVE' ORDER BY s.reviewCount DESC")
|
||||||
|
Page<StoreEntity> findPopularStores(Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 최근 생성된 매장 조회
|
||||||
|
*/
|
||||||
|
@Query("SELECT s FROM StoreEntity s WHERE s.status = 'ACTIVE' ORDER BY s.createdAt DESC")
|
||||||
|
Page<StoreEntity> findRecentStores(Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 지역(주소 포함) 매장 조회
|
||||||
|
*/
|
||||||
|
List<StoreEntity> findByAddressContainingAndStatus(String addressKeyword, String status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 점주별 활성 매장 조회
|
||||||
|
*/
|
||||||
|
List<StoreEntity> findByOwnerIdAndStatus(Long ownerId, String status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 존재 여부 확인 (점주 ID와 매장명으로)
|
||||||
|
*/
|
||||||
|
boolean existsByOwnerIdAndStoreName(Long ownerId, String storeName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 복합 검색 쿼리
|
||||||
|
*/
|
||||||
|
@Query("SELECT s FROM StoreEntity s WHERE " +
|
||||||
|
"(:category IS NULL OR s.category = :category) AND " +
|
||||||
|
"(:status IS NULL OR s.status = :status) AND " +
|
||||||
|
"(:minRating IS NULL OR s.rating >= :minRating) AND " +
|
||||||
|
"(:keyword IS NULL OR s.storeName LIKE %:keyword% OR s.address LIKE %:keyword%)")
|
||||||
|
Page<StoreEntity> findStoresWithFilters(@Param("category") String category,
|
||||||
|
@Param("status") String status,
|
||||||
|
@Param("minRating") Double minRating,
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
Pageable pageable);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user