store update

This commit is contained in:
youbeen 2025-06-12 14:56:33 +09:00
parent 51d69c27e2
commit d538e89d5b
7 changed files with 912 additions and 19 deletions

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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,13 +20,16 @@ import java.util.stream.Collectors;
/** /**
* 매장 리포지토리 어댑터 클래스 * 매장 리포지토리 어댑터 클래스
* Store Repository Port를 구현하여 데이터 영속성 기능을 제공 * Store Repository Port를 구현하여 데이터 영속성 기능을 제공
*
* @author 하이오더 개발팀
* @version 1.0.0
*/ */
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class StoreRepositoryAdapter implements StoreRepositoryPort { public class StoreRepositoryAdapter implements StoreRepositoryPort {
private final StoreJpaRepository storeJpaRepository; private final StoreJpaRepository storeJpaRepository;
@Override @Override
public List<Store> findStoresByOwnerId(Long ownerId) { public List<Store> findStoresByOwnerId(Long ownerId) {
return storeJpaRepository.findByOwnerId(ownerId) return storeJpaRepository.findByOwnerId(ownerId)
@ -29,31 +37,135 @@ public class StoreRepositoryAdapter implements StoreRepositoryPort {
.map(this::toDomain) .map(this::toDomain)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override @Override
public Optional<Store> findStoreById(Long storeId) { public Optional<Store> findStoreById(Long storeId) {
return storeJpaRepository.findById(storeId) return storeJpaRepository.findById(storeId)
.map(this::toDomain); .map(this::toDomain);
} }
@Override @Override
public Optional<Store> findStoreByIdAndOwnerId(Long storeId, Long ownerId) { public Optional<Store> findStoreByIdAndOwnerId(Long storeId, Long ownerId) {
return storeJpaRepository.findByIdAndOwnerId(storeId, ownerId) return storeJpaRepository.findByIdAndOwnerId(storeId, ownerId)
.map(this::toDomain); .map(this::toDomain);
} }
@Override @Override
public Store saveStore(Store store) { public Store saveStore(Store store) {
StoreEntity entity = toEntity(store); StoreEntity entity = toEntity(store);
StoreEntity saved = storeJpaRepository.save(entity); StoreEntity saved = storeJpaRepository.save(entity);
return toDomain(saved); return toDomain(saved);
} }
@Override @Override
public void deleteStore(Long storeId) { public void deleteStore(Long storeId) {
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())
@ -78,7 +190,7 @@ public class StoreRepositoryAdapter implements StoreRepositoryPort {
.updatedAt(entity.getUpdatedAt()) .updatedAt(entity.getUpdatedAt())
.build(); .build();
} }
/** /**
* Domain을 Entity로 변환 * Domain을 Entity로 변환
*/ */
@ -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())
@ -103,7 +215,7 @@ public class StoreRepositoryAdapter implements StoreRepositoryPort {
.updatedAt(domain.getUpdatedAt()) .updatedAt(domain.getUpdatedAt())
.build(); .build();
} }
/** /**
* JSON 태그를 List로 파싱 * JSON 태그를 List로 파싱
*/ */
@ -113,4 +225,4 @@ public class StoreRepositoryAdapter implements StoreRepositoryPort {
} }
return Arrays.asList(tagsJson.split(",")); return Arrays.asList(tagsJson.split(","));
} }
} }

View File

@ -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;
}
}

View File

@ -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);
}