store Menu update
This commit is contained in:
parent
17a68d3cdb
commit
a6548db953
@ -1,5 +1,7 @@
|
|||||||
package com.ktds.hi.common.security;
|
package com.ktds.hi.common.security;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import com.ktds.hi.common.exception.BusinessException;
|
||||||
import com.ktds.hi.common.constants.SecurityConstants;
|
import com.ktds.hi.common.constants.SecurityConstants;
|
||||||
import io.jsonwebtoken.*;
|
import io.jsonwebtoken.*;
|
||||||
import io.jsonwebtoken.security.Keys;
|
import io.jsonwebtoken.security.Keys;
|
||||||
@ -55,6 +57,82 @@ public class JwtTokenProvider {
|
|||||||
.verifyWith(secretKey)
|
.verifyWith(secretKey)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* HTTP 요청에서 점주 정보 추출
|
||||||
|
*/
|
||||||
|
public Long extractOwnerInfo(HttpServletRequest request) {
|
||||||
|
try {
|
||||||
|
// Authorization 헤더에서 토큰 추출
|
||||||
|
String authHeader = request.getHeader("Authorization");
|
||||||
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
|
throw new BusinessException("UNAUTHORIZED", "인증 토큰이 필요합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = authHeader.substring(7); // "Bearer " 제거
|
||||||
|
|
||||||
|
// 토큰 유효성 검증
|
||||||
|
if (!validateToken(token)) {
|
||||||
|
throw new BusinessException("UNAUTHORIZED", "유효하지 않은 토큰입니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 토큰에서 사용자 ID 추출
|
||||||
|
String userId = getUserIdFromToken(token);
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException("UNAUTHORIZED", "토큰에서 사용자 정보를 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 토큰에서 권한 정보 추출
|
||||||
|
String roles = getRolesFromToken(token);
|
||||||
|
if (roles == null || !roles.contains("OWNER")) {
|
||||||
|
throw new BusinessException("FORBIDDEN", "점주 권한이 필요합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("점주 정보 추출 완료: ownerId={}", userId);
|
||||||
|
return Long.parseLong(userId);
|
||||||
|
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.error("사용자 ID 형변환 실패: {}", e.getMessage());
|
||||||
|
throw new BusinessException("UNAUTHORIZED", "잘못된 사용자 ID 형식입니다.");
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e; // 비즈니스 예외는 그대로 전파
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("점주 정보 추출 중 오류 발생: {}", e.getMessage(), e);
|
||||||
|
throw new BusinessException("UNAUTHORIZED", "인증 처리 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP 요청에서 사용자 정보 추출 (일반 사용자용)
|
||||||
|
*/
|
||||||
|
public Long extractUserInfo(HttpServletRequest request) {
|
||||||
|
try {
|
||||||
|
String authHeader = request.getHeader("Authorization");
|
||||||
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
|
throw new BusinessException("UNAUTHORIZED", "인증 토큰이 필요합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = authHeader.substring(7);
|
||||||
|
|
||||||
|
if (!validateToken(token)) {
|
||||||
|
throw new BusinessException("UNAUTHORIZED", "유효하지 않은 토큰입니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String userId = getUserIdFromToken(token);
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException("UNAUTHORIZED", "토큰에서 사용자 정보를 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Long.parseLong(userId);
|
||||||
|
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new BusinessException("UNAUTHORIZED", "잘못된 사용자 ID 형식입니다.");
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("사용자 정보 추출 중 오류 발생: {}", e.getMessage(), e);
|
||||||
|
throw new BusinessException("UNAUTHORIZED", "인증 처리 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 액세스 토큰 생성
|
* 액세스 토큰 생성
|
||||||
|
|||||||
@ -2,10 +2,9 @@
|
|||||||
package com.ktds.hi.store.biz.service;
|
package com.ktds.hi.store.biz.service;
|
||||||
|
|
||||||
import com.ktds.hi.store.biz.usecase.in.StoreUseCase;
|
import com.ktds.hi.store.biz.usecase.in.StoreUseCase;
|
||||||
import com.ktds.hi.store.biz.usecase.out.*;
|
|
||||||
import com.ktds.hi.store.biz.domain.Store;
|
|
||||||
import com.ktds.hi.store.biz.domain.StoreStatus;
|
|
||||||
import com.ktds.hi.store.infra.dto.*;
|
import com.ktds.hi.store.infra.dto.*;
|
||||||
|
import com.ktds.hi.store.infra.gateway.entity.StoreEntity;
|
||||||
|
import com.ktds.hi.store.infra.gateway.repository.StoreJpaRepository;
|
||||||
import com.ktds.hi.common.exception.BusinessException;
|
import com.ktds.hi.common.exception.BusinessException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -17,11 +16,7 @@ import java.util.List;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 서비스 구현체
|
* 매장 서비스 구현체 (간단 버전)
|
||||||
* Clean Architecture의 Application Service Layer
|
|
||||||
*
|
|
||||||
* @author 하이오더 개발팀
|
|
||||||
* @version 1.0.0
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -29,141 +24,77 @@ import java.util.stream.Collectors;
|
|||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public class StoreService implements StoreUseCase {
|
public class StoreService implements StoreUseCase {
|
||||||
|
|
||||||
private final StoreRepositoryPort storeRepositoryPort;
|
private final StoreJpaRepository storeJpaRepository;
|
||||||
private final MenuRepositoryPort menuRepositoryPort;
|
|
||||||
private final StoreTagRepositoryPort storeTagRepositoryPort;
|
|
||||||
private final GeocodingPort geocodingPort;
|
|
||||||
private final CachePort cachePort;
|
|
||||||
private final EventPort eventPort;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public StoreCreateResponse createStore(Long ownerId, StoreCreateRequest request) {
|
public StoreCreateResponse createStore(Long ownerId, StoreCreateRequest request) {
|
||||||
log.info("매장 등록 시작: ownerId={}, storeName={}", ownerId, request.getStoreName());
|
log.info("매장 등록: ownerId={}, storeName={}", ownerId, request.getStoreName());
|
||||||
|
|
||||||
try {
|
// 기본 검증
|
||||||
// 1. 입력값 검증
|
if (request.getStoreName() == null || request.getStoreName().trim().isEmpty()) {
|
||||||
validateStoreCreateRequest(request);
|
throw new BusinessException("INVALID_STORE_NAME", "매장명은 필수입니다.");
|
||||||
|
|
||||||
// 2. 점주 매장 개수 제한 확인 (예: 최대 10개)
|
|
||||||
validateOwnerStoreLimit(ownerId);
|
|
||||||
|
|
||||||
// 3. 주소 지오코딩 (좌표 변환)
|
|
||||||
Coordinates coordinates = geocodingPort.getCoordinates(request.getAddress());
|
|
||||||
|
|
||||||
// 4. Store 도메인 객체 생성
|
|
||||||
Store store = Store.builder()
|
|
||||||
.ownerId(ownerId)
|
|
||||||
.storeName(request.getStoreName())
|
|
||||||
.address(request.getAddress())
|
|
||||||
.latitude(coordinates.getLatitude())
|
|
||||||
.longitude(coordinates.getLongitude())
|
|
||||||
.description(request.getDescription())
|
|
||||||
.phone(request.getPhone())
|
|
||||||
.operatingHours(request.getOperatingHours())
|
|
||||||
.category(request.getCategory())
|
|
||||||
.status(StoreStatus.ACTIVE)
|
|
||||||
.rating(0.0)
|
|
||||||
.reviewCount(0)
|
|
||||||
.createdAt(LocalDateTime.now())
|
|
||||||
.updatedAt(LocalDateTime.now())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 5. 매장 저장
|
|
||||||
Store savedStore = storeRepositoryPort.saveStore(store);
|
|
||||||
|
|
||||||
// 6. 매장 태그 저장
|
|
||||||
if (request.getTags() != null && !request.getTags().isEmpty()) {
|
|
||||||
storeTagRepositoryPort.saveStoreTags(savedStore.getId(), request.getTags());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. 메뉴 정보 저장
|
|
||||||
if (request.getMenus() != null && !request.getMenus().isEmpty()) {
|
|
||||||
menuRepositoryPort.saveMenus(savedStore.getId(),
|
|
||||||
request.getMenus().stream()
|
|
||||||
.map(menuReq -> menuReq.toDomain(savedStore.getId()))
|
|
||||||
.collect(Collectors.toList()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. 매장 생성 이벤트 발행
|
|
||||||
eventPort.publishStoreCreatedEvent(savedStore);
|
|
||||||
|
|
||||||
// 9. 캐시 무효화
|
|
||||||
cachePort.invalidateStoreCache(ownerId);
|
|
||||||
|
|
||||||
log.info("매장 등록 완료: storeId={}", savedStore.getId());
|
|
||||||
|
|
||||||
return StoreCreateResponse.builder()
|
|
||||||
.storeId(savedStore.getId())
|
|
||||||
.storeName(savedStore.getStoreName())
|
|
||||||
.message("매장이 성공적으로 등록되었습니다.")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("매장 등록 실패: ownerId={}, error={}", ownerId, e.getMessage(), e);
|
|
||||||
throw new BusinessException("STORE_CREATE_FAILED", "매장 등록에 실패했습니다: " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
if (request.getAddress() == null || request.getAddress().trim().isEmpty()) {
|
||||||
|
throw new BusinessException("INVALID_ADDRESS", "주소는 필수입니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 매장 엔티티 생성
|
||||||
|
StoreEntity store = StoreEntity.builder()
|
||||||
|
.ownerId(ownerId)
|
||||||
|
.storeName(request.getStoreName())
|
||||||
|
.address(request.getAddress())
|
||||||
|
.latitude(37.5665) // 기본 좌표 (서울시청)
|
||||||
|
.longitude(126.9780)
|
||||||
|
.description(request.getDescription())
|
||||||
|
.phone(request.getPhone())
|
||||||
|
.operatingHours(request.getOperatingHours())
|
||||||
|
.category(request.getCategory())
|
||||||
|
.status("ACTIVE")
|
||||||
|
.rating(0.0)
|
||||||
|
.reviewCount(0)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StoreEntity savedStore = storeJpaRepository.save(store);
|
||||||
|
|
||||||
|
log.info("매장 등록 완료: storeId={}", savedStore.getId());
|
||||||
|
|
||||||
|
return StoreCreateResponse.builder()
|
||||||
|
.storeId(savedStore.getId())
|
||||||
|
.storeName(savedStore.getStoreName())
|
||||||
|
.message("매장이 성공적으로 등록되었습니다.")
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<MyStoreListResponse> getMyStores(Long ownerId) {
|
public List<MyStoreListResponse> getMyStores(Long ownerId) {
|
||||||
log.info("내 매장 목록 조회: ownerId={}", ownerId);
|
log.info("내 매장 목록 조회: ownerId={}", ownerId);
|
||||||
|
|
||||||
// 1. 캐시 확인
|
List<StoreEntity> stores = storeJpaRepository.findByOwnerId(ownerId);
|
||||||
String cacheKey = "stores:owner:" + ownerId;
|
|
||||||
List<MyStoreListResponse> cachedStores = cachePort.getStoreCache(cacheKey);
|
|
||||||
if (cachedStores != null) {
|
|
||||||
log.info("캐시에서 매장 목록 반환: ownerId={}, count={}", ownerId, cachedStores.size());
|
|
||||||
return cachedStores;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. DB에서 매장 목록 조회
|
return stores.stream()
|
||||||
List<Store> stores = storeRepositoryPort.findStoresByOwnerId(ownerId);
|
.map(store -> MyStoreListResponse.builder()
|
||||||
|
.storeId(store.getId())
|
||||||
// 3. 응답 DTO 변환
|
.storeName(store.getStoreName())
|
||||||
List<MyStoreListResponse> responses = stores.stream()
|
.address(store.getAddress())
|
||||||
.map(store -> {
|
.category(store.getCategory())
|
||||||
String status = calculateStoreStatus(store);
|
.rating(store.getRating())
|
||||||
return MyStoreListResponse.builder()
|
.reviewCount(store.getReviewCount())
|
||||||
.storeId(store.getId())
|
.status("운영중")
|
||||||
.storeName(store.getStoreName())
|
.operatingHours(store.getOperatingHours())
|
||||||
.address(store.getAddress())
|
.build())
|
||||||
.category(store.getCategory())
|
|
||||||
.rating(store.getRating())
|
|
||||||
.reviewCount(store.getReviewCount())
|
|
||||||
.status(status)
|
|
||||||
.operatingHours(store.getOperatingHours())
|
|
||||||
.build();
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
// 4. 캐시 저장 (1시간)
|
|
||||||
cachePort.putStoreCache(cacheKey, responses, 3600);
|
|
||||||
|
|
||||||
log.info("내 매장 목록 조회 완료: ownerId={}, count={}", ownerId, responses.size());
|
|
||||||
return responses;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StoreDetailResponse getStoreDetail(Long storeId) {
|
public StoreDetailResponse getStoreDetail(Long storeId) {
|
||||||
log.info("매장 상세 조회: storeId={}", storeId);
|
log.info("매장 상세 조회: storeId={}", storeId);
|
||||||
|
|
||||||
// 1. 매장 기본 정보 조회
|
StoreEntity store = storeJpaRepository.findById(storeId)
|
||||||
Store store = storeRepositoryPort.findStoreById(storeId)
|
|
||||||
.orElseThrow(() -> new BusinessException("STORE_NOT_FOUND", "매장을 찾을 수 없습니다."));
|
.orElseThrow(() -> new BusinessException("STORE_NOT_FOUND", "매장을 찾을 수 없습니다."));
|
||||||
|
|
||||||
// 2. 매장 태그 조회
|
|
||||||
List<String> tags = storeTagRepositoryPort.findTagsByStoreId(storeId);
|
|
||||||
|
|
||||||
// 3. 메뉴 정보 조회
|
|
||||||
List<MenuResponse> menus = menuRepositoryPort.findMenusByStoreId(storeId)
|
|
||||||
.stream()
|
|
||||||
.map(MenuResponse::from)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 4. AI 요약 정보 조회 (외부 서비스)
|
|
||||||
String aiSummary = getAISummary(storeId);
|
|
||||||
|
|
||||||
return StoreDetailResponse.builder()
|
return StoreDetailResponse.builder()
|
||||||
.storeId(store.getId())
|
.storeId(store.getId())
|
||||||
.storeName(store.getStoreName())
|
.storeName(store.getStoreName())
|
||||||
@ -176,58 +107,26 @@ public class StoreService implements StoreUseCase {
|
|||||||
.category(store.getCategory())
|
.category(store.getCategory())
|
||||||
.rating(store.getRating())
|
.rating(store.getRating())
|
||||||
.reviewCount(store.getReviewCount())
|
.reviewCount(store.getReviewCount())
|
||||||
.status(store.getStatus().name())
|
.status(store.getStatus())
|
||||||
.tags(tags)
|
|
||||||
.menus(menus)
|
|
||||||
.aiSummary(aiSummary)
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public StoreUpdateResponse updateStore(Long storeId, Long ownerId, StoreUpdateRequest request) {
|
public StoreUpdateResponse updateStore(Long storeId, Long ownerId, StoreUpdateRequest request) {
|
||||||
log.info("매장 정보 수정: storeId={}, ownerId={}", storeId, ownerId);
|
log.info("매장 수정: storeId={}, ownerId={}", storeId, ownerId);
|
||||||
|
|
||||||
// 1. 매장 소유권 확인
|
StoreEntity store = storeJpaRepository.findByIdAndOwnerId(storeId, ownerId)
|
||||||
Store store = validateStoreOwnership(storeId, ownerId);
|
.orElseThrow(() -> new BusinessException("STORE_ACCESS_DENIED", "매장에 대한 권한이 없습니다."));
|
||||||
|
|
||||||
// 2. 주소 변경 시 지오코딩
|
store.updateInfo(request.getStoreName(), request.getAddress(), request.getDescription(),
|
||||||
Coordinates coordinates = null;
|
request.getPhone(), request.getOperatingHours());
|
||||||
if (!store.getAddress().equals(request.getAddress())) {
|
|
||||||
coordinates = geocodingPort.getCoordinates(request.getAddress());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 매장 정보 업데이트
|
storeJpaRepository.save(store);
|
||||||
store.updateBasicInfo(
|
|
||||||
request.getStoreName(),
|
|
||||||
request.getAddress(),
|
|
||||||
request.getDescription(),
|
|
||||||
request.getPhone(),
|
|
||||||
request.getOperatingHours()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (coordinates != null) {
|
|
||||||
store.updateLocation(coordinates);
|
|
||||||
}
|
|
||||||
|
|
||||||
Store updatedStore = storeRepositoryPort.saveStore(store);
|
|
||||||
|
|
||||||
// 4. 태그 업데이트
|
|
||||||
if (request.getTags() != null) {
|
|
||||||
storeTagRepositoryPort.deleteTagsByStoreId(storeId);
|
|
||||||
storeTagRepositoryPort.saveStoreTags(storeId, request.getTags());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 매장 수정 이벤트 발행
|
|
||||||
eventPort.publishStoreUpdatedEvent(updatedStore);
|
|
||||||
|
|
||||||
// 6. 캐시 무효화
|
|
||||||
cachePort.invalidateStoreCache(storeId);
|
|
||||||
cachePort.invalidateStoreCache(ownerId);
|
|
||||||
|
|
||||||
return StoreUpdateResponse.builder()
|
return StoreUpdateResponse.builder()
|
||||||
.storeId(storeId)
|
.storeId(storeId)
|
||||||
.message("매장 정보가 성공적으로 수정되었습니다.")
|
.message("매장 정보가 수정되었습니다.")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,23 +135,15 @@ public class StoreService implements StoreUseCase {
|
|||||||
public StoreDeleteResponse deleteStore(Long storeId, Long ownerId) {
|
public StoreDeleteResponse deleteStore(Long storeId, Long ownerId) {
|
||||||
log.info("매장 삭제: storeId={}, ownerId={}", storeId, ownerId);
|
log.info("매장 삭제: storeId={}, ownerId={}", storeId, ownerId);
|
||||||
|
|
||||||
// 1. 매장 소유권 확인
|
StoreEntity store = storeJpaRepository.findByIdAndOwnerId(storeId, ownerId)
|
||||||
Store store = validateStoreOwnership(storeId, ownerId);
|
.orElseThrow(() -> new BusinessException("STORE_ACCESS_DENIED", "매장에 대한 권한이 없습니다."));
|
||||||
|
|
||||||
// 2. 소프트 삭제 (상태 변경)
|
store.updateStatus("DELETED");
|
||||||
store.delete();
|
storeJpaRepository.save(store);
|
||||||
storeRepositoryPort.saveStore(store);
|
|
||||||
|
|
||||||
// 3. 매장 삭제 이벤트 발행
|
|
||||||
eventPort.publishStoreDeletedEvent(storeId);
|
|
||||||
|
|
||||||
// 4. 캐시 무효화
|
|
||||||
cachePort.invalidateStoreCache(storeId);
|
|
||||||
cachePort.invalidateStoreCache(ownerId);
|
|
||||||
|
|
||||||
return StoreDeleteResponse.builder()
|
return StoreDeleteResponse.builder()
|
||||||
.storeId(storeId)
|
.storeId(storeId)
|
||||||
.message("매장이 성공적으로 삭제되었습니다.")
|
.message("매장이 삭제되었습니다.")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,20 +151,16 @@ public class StoreService implements StoreUseCase {
|
|||||||
public List<StoreSearchResponse> searchStores(String keyword, String category, String tags,
|
public List<StoreSearchResponse> searchStores(String keyword, String category, String tags,
|
||||||
Double latitude, Double longitude, Integer radius,
|
Double latitude, Double longitude, Integer radius,
|
||||||
Integer page, Integer size) {
|
Integer page, Integer size) {
|
||||||
log.info("매장 검색: keyword={}, category={}, location=({}, {})", keyword, category, latitude, longitude);
|
log.info("매장 검색: keyword={}, category={}", keyword, category);
|
||||||
|
|
||||||
StoreSearchCriteria criteria = StoreSearchCriteria.builder()
|
List<StoreEntity> stores;
|
||||||
.keyword(keyword)
|
if (keyword != null && !keyword.trim().isEmpty()) {
|
||||||
.category(category)
|
stores = storeJpaRepository.findByStoreNameContainingOrAddressContaining(keyword, keyword);
|
||||||
.tags(tags)
|
} else if (category != null && !category.trim().isEmpty()) {
|
||||||
.latitude(latitude)
|
stores = storeJpaRepository.findByCategory(category);
|
||||||
.longitude(longitude)
|
} else {
|
||||||
.radius(radius)
|
stores = storeJpaRepository.findAll();
|
||||||
.page(page)
|
}
|
||||||
.size(size)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<Store> stores = storeRepositoryPort.searchStores(criteria);
|
|
||||||
|
|
||||||
return stores.stream()
|
return stores.stream()
|
||||||
.map(store -> StoreSearchResponse.builder()
|
.map(store -> StoreSearchResponse.builder()
|
||||||
@ -283,60 +170,8 @@ public class StoreService implements StoreUseCase {
|
|||||||
.category(store.getCategory())
|
.category(store.getCategory())
|
||||||
.rating(store.getRating())
|
.rating(store.getRating())
|
||||||
.reviewCount(store.getReviewCount())
|
.reviewCount(store.getReviewCount())
|
||||||
.distance(calculateDistance(latitude, longitude, store.getLatitude(), store.getLongitude()))
|
.distance(1.5) // 더미 거리
|
||||||
.build())
|
.build())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Private Helper Methods ===
|
|
||||||
|
|
||||||
private void validateStoreCreateRequest(StoreCreateRequest request) {
|
|
||||||
if (request.getStoreName() == null || request.getStoreName().trim().isEmpty()) {
|
|
||||||
throw new BusinessException("INVALID_STORE_NAME", "매장명은 필수입니다.");
|
|
||||||
}
|
|
||||||
if (request.getStoreName().length() > 100) {
|
|
||||||
throw new BusinessException("INVALID_STORE_NAME", "매장명은 100자를 초과할 수 없습니다.");
|
|
||||||
}
|
|
||||||
if (request.getAddress() == null || request.getAddress().trim().isEmpty()) {
|
|
||||||
throw new BusinessException("INVALID_ADDRESS", "주소는 필수입니다.");
|
|
||||||
}
|
|
||||||
if (request.getPhone() != null && !request.getPhone().matches("^\\d{2,3}-\\d{3,4}-\\d{4}$")) {
|
|
||||||
throw new BusinessException("INVALID_PHONE", "전화번호 형식이 올바르지 않습니다.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateOwnerStoreLimit(Long ownerId) {
|
|
||||||
Long storeCount = storeRepositoryPort.countStoresByOwnerId(ownerId);
|
|
||||||
if (storeCount >= 10) {
|
|
||||||
throw new BusinessException("STORE_LIMIT_EXCEEDED", "매장은 최대 10개까지 등록할 수 있습니다.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Store validateStoreOwnership(Long storeId, Long ownerId) {
|
|
||||||
return storeRepositoryPort.findStoreByIdAndOwnerId(storeId, ownerId)
|
|
||||||
.orElseThrow(() -> new BusinessException("STORE_ACCESS_DENIED", "매장에 대한 권한이 없습니다."));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String calculateStoreStatus(Store store) {
|
|
||||||
if (!store.isActive()) {
|
|
||||||
return "비활성";
|
|
||||||
}
|
|
||||||
// 운영시간 기반 현재 상태 계산 로직
|
|
||||||
return "운영중";
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getAISummary(Long storeId) {
|
|
||||||
// TODO: AI 분석 서비스 연동 구현
|
|
||||||
return "AI 요약 정보가 준비 중입니다.";
|
|
||||||
}
|
|
||||||
|
|
||||||
private Double calculateDistance(Double lat1, Double lon1, Double lat2, Double lon2) {
|
|
||||||
if (lat1 == null || lon1 == null || lat2 == null || lon2 == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return geocodingPort.calculateDistance(
|
|
||||||
new Coordinates(lat1, lon1),
|
|
||||||
new Coordinates(lat2, lon2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,76 @@
|
|||||||
|
package com.ktds.hi.store.biz.usecase.in;
|
||||||
|
|
||||||
|
import com.ktds.hi.store.infra.dto.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 관리 유스케이스 인터페이스
|
||||||
|
* Clean Architecture의 Input Port
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
public interface StoreUseCase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 등록
|
||||||
|
*
|
||||||
|
* @param ownerId 점주 ID
|
||||||
|
* @param request 매장 등록 요청 정보
|
||||||
|
* @return 매장 등록 응답
|
||||||
|
*/
|
||||||
|
StoreCreateResponse createStore(Long ownerId, StoreCreateRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 내 매장 목록 조회
|
||||||
|
*
|
||||||
|
* @param ownerId 점주 ID
|
||||||
|
* @return 내 매장 목록
|
||||||
|
*/
|
||||||
|
List<MyStoreListResponse> getMyStores(Long ownerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 상세 조회
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @return 매장 상세 정보
|
||||||
|
*/
|
||||||
|
StoreDetailResponse getStoreDetail(Long storeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 정보 수정
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @param ownerId 점주 ID
|
||||||
|
* @param request 매장 수정 요청 정보
|
||||||
|
* @return 매장 수정 응답
|
||||||
|
*/
|
||||||
|
StoreUpdateResponse updateStore(Long storeId, Long ownerId, StoreUpdateRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 삭제
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @param ownerId 점주 ID
|
||||||
|
* @return 매장 삭제 응답
|
||||||
|
*/
|
||||||
|
StoreDeleteResponse deleteStore(Long storeId, Long ownerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 검색
|
||||||
|
*
|
||||||
|
* @param keyword 검색 키워드
|
||||||
|
* @param category 카테고리
|
||||||
|
* @param tags 태그
|
||||||
|
* @param latitude 위도
|
||||||
|
* @param longitude 경도
|
||||||
|
* @param radius 검색 반경(km)
|
||||||
|
* @param page 페이지 번호
|
||||||
|
* @param size 페이지 크기
|
||||||
|
* @return 검색된 매장 목록
|
||||||
|
*/
|
||||||
|
List<StoreSearchResponse> searchStores(String keyword, String category, String tags,
|
||||||
|
Double latitude, Double longitude, Integer radius,
|
||||||
|
Integer page, Integer size);
|
||||||
|
}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
package com.ktds.hi.store.biz.domain;
|
package com.ktds.hi.store.domain;
|
||||||
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 도메인 엔티티
|
* 메뉴 도메인 엔티티
|
||||||
@ -18,6 +19,8 @@ public class Menu {
|
|||||||
private String category;
|
private String category;
|
||||||
private String imageUrl;
|
private String imageUrl;
|
||||||
private Boolean available;
|
private Boolean available;
|
||||||
|
private LocalDateTime createdAt; // 추가
|
||||||
|
private LocalDateTime updatedAt; // 추가
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 정보 업데이트
|
* 메뉴 정보 업데이트
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.ktds.hi.store.infra.dto;
|
package com.ktds.hi.store.infra.dto;
|
||||||
|
|
||||||
import com.ktds.hi.store.biz.domain.Menu;
|
import com.ktds.hi.store.domain.Menu;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캐시 어댑터 클래스
|
* 캐시 어댑터 클래스
|
||||||
@ -32,29 +34,45 @@ public class CacheAdapter implements CachePort {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void putStoreCache(String key, Object value, Duration ttl) {
|
public void putStoreCache(String key, Object value, long ttlSeconds) {
|
||||||
try {
|
try {
|
||||||
redisTemplate.opsForValue().set(key, value, ttl);
|
redisTemplate.opsForValue().set(key, value, ttlSeconds, TimeUnit.SECONDS);
|
||||||
log.debug("매장 캐시 저장 완료: key={}, ttl={}분", key, ttl.toMinutes());
|
log.debug("캐시 저장: key={}, ttl={}초", key, ttlSeconds);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("매장 캐시 저장 실패: key={}, error={}", key, e.getMessage());
|
log.warn("캐시 저장 실패: key={}, error={}", key, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invalidateStoreCache(Long storeId) {
|
public void invalidateStoreCache(Object key) {
|
||||||
try {
|
try {
|
||||||
// 매장 관련 모든 캐시 키 패턴 삭제
|
if (key instanceof Long) {
|
||||||
String storeDetailKey = "store_detail:" + storeId;
|
// 매장 ID로 특정 매장 캐시 삭제
|
||||||
String myStoresKey = "my_stores:*";
|
Long storeId = (Long) key;
|
||||||
|
String storeDetailKey = "store_detail:" + storeId;
|
||||||
redisTemplate.delete(storeDetailKey);
|
redisTemplate.delete(storeDetailKey);
|
||||||
|
log.debug("매장 캐시 무효화 완료: storeId={}", storeId);
|
||||||
log.debug("매장 캐시 무효화 완료: storeId={}", storeId);
|
|
||||||
|
} else if (key instanceof String) {
|
||||||
|
// 패턴으로 캐시 삭제
|
||||||
|
String pattern = key.toString();
|
||||||
|
Set<String> keys = redisTemplate.keys(pattern);
|
||||||
|
if (keys != null && !keys.isEmpty()) {
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
}
|
||||||
|
log.debug("패턴 캐시 무효화 완료: pattern={}", pattern);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 기본적으로 toString()으로 키 생성
|
||||||
|
String cacheKey = "stores:" + key.toString();
|
||||||
|
redisTemplate.delete(cacheKey);
|
||||||
|
log.debug("캐시 무효화 완료: key={}", key);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("매장 캐시 무효화 실패: storeId={}, error={}", storeId, e.getMessage());
|
log.error("캐시 무효화 실패: key={}, error={}", key, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,7 +96,7 @@ public class MenuRepositoryAdapter implements MenuRepositoryPort {
|
|||||||
.price(entity.getPrice())
|
.price(entity.getPrice())
|
||||||
.category(entity.getCategory())
|
.category(entity.getCategory())
|
||||||
.imageUrl(entity.getImageUrl())
|
.imageUrl(entity.getImageUrl())
|
||||||
.isAvailable(entity.getIsAvailable())
|
.available(entity.getIsAvailable())
|
||||||
.createdAt(entity.getCreatedAt())
|
.createdAt(entity.getCreatedAt())
|
||||||
.updatedAt(entity.getUpdatedAt())
|
.updatedAt(entity.getUpdatedAt())
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@ -119,6 +119,19 @@ public class StoreEntity {
|
|||||||
this.reviewCount = reviewCount;
|
this.reviewCount = reviewCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 기본 정보 업데이트
|
||||||
|
*/
|
||||||
|
public void updateInfo(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;
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 태그 업데이트
|
* 매장 태그 업데이트
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user