Merge branch 'main' of https://github.com/dg04-hi/hi-backend
# Conflicts: # analytics/src/main/java/com/ktds/hi/analytics/infra/config/SwaggerConfig.java # member/src/main/java/com/ktds/hi/member/config/SwaggerConfig.java # recommend/src/main/java/com/ktds/hi/recommend/infra/config/SwaggerConfig.java
This commit is contained in:
commit
55c5845772
@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
|
||||||
private final CorsConfigurationSource corsConfigurationSource;
|
private final CorsConfigurationSource corsConfigurationSource;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@ -20,10 +20,10 @@ import java.util.List;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class CorsConfig implements WebMvcConfigurer {
|
public class CorsConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Value("${app.cors.allowed-origins:http://localhost:3000,http://localhost:8080,http://localhost:3001}")
|
@Value("${app.cors.allowed-origins:http://20.214.126.84,http://localhost:3000}")
|
||||||
private String allowedOrigins;
|
private String allowedOrigins;
|
||||||
|
|
||||||
@Value("${app.cors.allowed-methods:GET,POST,PUT,DELETE,PATCH,OPTIONS}")
|
@Value("${app.cors.allowed-methods:GET,POST,PUT,DELETE,OPTIONS}")
|
||||||
private String allowedMethods;
|
private String allowedMethods;
|
||||||
|
|
||||||
@Value("${app.cors.allowed-headers:*}")
|
@Value("${app.cors.allowed-headers:*}")
|
||||||
|
|||||||
@ -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", "인증 처리 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 액세스 토큰 생성
|
* 액세스 토큰 생성
|
||||||
|
|||||||
@ -53,7 +53,7 @@ app:
|
|||||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000} # 7일
|
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000} # 7일
|
||||||
# CORS 설정
|
# CORS 설정
|
||||||
cors:
|
cors:
|
||||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://20.214.126.84:80,http://localhost:8080}
|
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://20.214.126.84,http://localhost:8080}
|
||||||
allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS}
|
allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS}
|
||||||
allowed-headers: ${CORS_ALLOWED_HEADERS:*}
|
allowed-headers: ${CORS_ALLOWED_HEADERS:*}
|
||||||
exposed-headers: ${CORS_EXPOSED_HEADERS:Authorization, X-Total-Count}
|
exposed-headers: ${CORS_EXPOSED_HEADERS:Authorization, X-Total-Count}
|
||||||
|
|||||||
@ -44,6 +44,8 @@ spring:
|
|||||||
order_updates: true
|
order_updates: true
|
||||||
open-in-view: false
|
open-in-view: false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Redis 설정 (올바른 구조)
|
# Redis 설정 (올바른 구조)
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
|
|||||||
@ -0,0 +1,177 @@
|
|||||||
|
// store/src/main/java/com/ktds/hi/store/biz/service/StoreService.java
|
||||||
|
package com.ktds.hi.store.biz.service;
|
||||||
|
|
||||||
|
import com.ktds.hi.store.biz.usecase.in.StoreUseCase;
|
||||||
|
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 lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 서비스 구현체 (간단 버전)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class StoreService implements StoreUseCase {
|
||||||
|
|
||||||
|
private final StoreJpaRepository storeJpaRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public StoreCreateResponse createStore(Long ownerId, StoreCreateRequest request) {
|
||||||
|
log.info("매장 등록: ownerId={}, storeName={}", ownerId, request.getStoreName());
|
||||||
|
|
||||||
|
// 기본 검증
|
||||||
|
if (request.getStoreName() == null || request.getStoreName().trim().isEmpty()) {
|
||||||
|
throw new BusinessException("INVALID_STORE_NAME", "매장명은 필수입니다.");
|
||||||
|
}
|
||||||
|
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
|
||||||
|
public List<MyStoreListResponse> getMyStores(Long ownerId) {
|
||||||
|
log.info("내 매장 목록 조회: ownerId={}", ownerId);
|
||||||
|
|
||||||
|
List<StoreEntity> stores = storeJpaRepository.findByOwnerId(ownerId);
|
||||||
|
|
||||||
|
return stores.stream()
|
||||||
|
.map(store -> MyStoreListResponse.builder()
|
||||||
|
.storeId(store.getId())
|
||||||
|
.storeName(store.getStoreName())
|
||||||
|
.address(store.getAddress())
|
||||||
|
.category(store.getCategory())
|
||||||
|
.rating(store.getRating())
|
||||||
|
.reviewCount(store.getReviewCount())
|
||||||
|
.status("운영중")
|
||||||
|
.operatingHours(store.getOperatingHours())
|
||||||
|
.build())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StoreDetailResponse getStoreDetail(Long storeId) {
|
||||||
|
log.info("매장 상세 조회: storeId={}", storeId);
|
||||||
|
|
||||||
|
StoreEntity store = storeJpaRepository.findById(storeId)
|
||||||
|
.orElseThrow(() -> new BusinessException("STORE_NOT_FOUND", "매장을 찾을 수 없습니다."));
|
||||||
|
|
||||||
|
return StoreDetailResponse.builder()
|
||||||
|
.storeId(store.getId())
|
||||||
|
.storeName(store.getStoreName())
|
||||||
|
.address(store.getAddress())
|
||||||
|
.latitude(store.getLatitude())
|
||||||
|
.longitude(store.getLongitude())
|
||||||
|
.description(store.getDescription())
|
||||||
|
.phone(store.getPhone())
|
||||||
|
.operatingHours(store.getOperatingHours())
|
||||||
|
.category(store.getCategory())
|
||||||
|
.rating(store.getRating())
|
||||||
|
.reviewCount(store.getReviewCount())
|
||||||
|
.status(store.getStatus())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public StoreUpdateResponse updateStore(Long storeId, Long ownerId, StoreUpdateRequest request) {
|
||||||
|
log.info("매장 수정: storeId={}, ownerId={}", storeId, ownerId);
|
||||||
|
|
||||||
|
StoreEntity store = storeJpaRepository.findByIdAndOwnerId(storeId, ownerId)
|
||||||
|
.orElseThrow(() -> new BusinessException("STORE_ACCESS_DENIED", "매장에 대한 권한이 없습니다."));
|
||||||
|
|
||||||
|
store.updateInfo(request.getStoreName(), request.getAddress(), request.getDescription(),
|
||||||
|
request.getPhone(), request.getOperatingHours());
|
||||||
|
|
||||||
|
storeJpaRepository.save(store);
|
||||||
|
|
||||||
|
return StoreUpdateResponse.builder()
|
||||||
|
.storeId(storeId)
|
||||||
|
.message("매장 정보가 수정되었습니다.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public StoreDeleteResponse deleteStore(Long storeId, Long ownerId) {
|
||||||
|
log.info("매장 삭제: storeId={}, ownerId={}", storeId, ownerId);
|
||||||
|
|
||||||
|
StoreEntity store = storeJpaRepository.findByIdAndOwnerId(storeId, ownerId)
|
||||||
|
.orElseThrow(() -> new BusinessException("STORE_ACCESS_DENIED", "매장에 대한 권한이 없습니다."));
|
||||||
|
|
||||||
|
store.updateStatus("DELETED");
|
||||||
|
storeJpaRepository.save(store);
|
||||||
|
|
||||||
|
return StoreDeleteResponse.builder()
|
||||||
|
.storeId(storeId)
|
||||||
|
.message("매장이 삭제되었습니다.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StoreSearchResponse> searchStores(String keyword, String category, String tags,
|
||||||
|
Double latitude, Double longitude, Integer radius,
|
||||||
|
Integer page, Integer size) {
|
||||||
|
log.info("매장 검색: keyword={}, category={}", keyword, category);
|
||||||
|
|
||||||
|
List<StoreEntity> stores;
|
||||||
|
if (keyword != null && !keyword.trim().isEmpty()) {
|
||||||
|
stores = storeJpaRepository.findByStoreNameContainingOrAddressContaining(keyword, keyword);
|
||||||
|
} else if (category != null && !category.trim().isEmpty()) {
|
||||||
|
stores = storeJpaRepository.findByCategory(category);
|
||||||
|
} else {
|
||||||
|
stores = storeJpaRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return stores.stream()
|
||||||
|
.map(store -> StoreSearchResponse.builder()
|
||||||
|
.storeId(store.getId())
|
||||||
|
.storeName(store.getStoreName())
|
||||||
|
.address(store.getAddress())
|
||||||
|
.category(store.getCategory())
|
||||||
|
.rating(store.getRating())
|
||||||
|
.reviewCount(store.getReviewCount())
|
||||||
|
.distance(1.5) // 더미 거리
|
||||||
|
.build())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,26 +1,24 @@
|
|||||||
package com.ktds.hi.store.biz.usecase.out;
|
package com.ktds.hi.store.biz.usecase.out;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캐시 포트 인터페이스
|
* 캐시 포트 인터페이스
|
||||||
* 캐시 기능을 정의
|
|
||||||
*/
|
*/
|
||||||
public interface CachePort {
|
public interface CachePort {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캐시에서 매장 데이터 조회
|
* 캐시에서 매장 정보 조회
|
||||||
*/
|
*/
|
||||||
Optional<Object> getStoreCache(String key);
|
<T> T getStoreCache(String key);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캐시에 매장 데이터 저장
|
* 캐시에 매장 정보 저장
|
||||||
*/
|
*/
|
||||||
void putStoreCache(String key, Object value, Duration ttl);
|
void putStoreCache(String key, Object value, long ttlSeconds);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캐시 무효화
|
* 캐시 무효화
|
||||||
*/
|
*/
|
||||||
void invalidateStoreCache(Long storeId);
|
void invalidateStoreCache(Object key);
|
||||||
}
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package com.ktds.hi.store.biz.usecase.out;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 좌표 정보 값 객체
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Coordinates {
|
||||||
|
private Double latitude;
|
||||||
|
private Double longitude;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.ktds.hi.store.biz.usecase.out;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지오코딩 포트 인터페이스
|
||||||
|
*/
|
||||||
|
public interface GeocodingPort {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 주소를 좌표로 변환
|
||||||
|
*/
|
||||||
|
Coordinates getCoordinates(String address);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 두 좌표 간 거리 계산 (km)
|
||||||
|
*/
|
||||||
|
Double calculateDistance(Coordinates coord1, Coordinates coord2);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
package com.ktds.hi.store.biz.usecase.out;
|
package com.ktds.hi.store.biz.usecase.out;
|
||||||
|
|
||||||
import com.ktds.hi.store.biz.domain.Menu;
|
import com.ktds.hi.store.domain.Menu;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.ktds.hi.store.biz.usecase.out;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 태그 리포지토리 포트 인터페이스
|
||||||
|
*/
|
||||||
|
public interface StoreTagRepositoryPort {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 ID로 태그 목록 조회
|
||||||
|
*/
|
||||||
|
List<String> findTagsByStoreId(Long storeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 태그 저장
|
||||||
|
*/
|
||||||
|
void saveStoreTags(Long storeId, List<String> tags);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 태그 삭제
|
||||||
|
*/
|
||||||
|
void deleteTagsByStoreId(Long storeId);
|
||||||
|
}
|
||||||
@ -1,23 +1,14 @@
|
|||||||
package com.ktds.hi.store.biz.domain;
|
package com.ktds.hi.store.domain;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 도메인 클래스
|
* 메뉴 도메인 엔티티
|
||||||
* 메뉴 정보를 담는 도메인 객체
|
|
||||||
*
|
|
||||||
* @author 하이오더 개발팀
|
|
||||||
* @version 1.0.0
|
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class Menu {
|
public class Menu {
|
||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
@ -27,164 +18,33 @@ public class Menu {
|
|||||||
private Integer price;
|
private Integer price;
|
||||||
private String category;
|
private String category;
|
||||||
private String imageUrl;
|
private String imageUrl;
|
||||||
private Boolean isAvailable;
|
private Boolean available;
|
||||||
private Integer orderCount;
|
private LocalDateTime createdAt; // 추가
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime updatedAt; // 추가
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 기본 정보 업데이트
|
* 메뉴 정보 업데이트
|
||||||
*/
|
*/
|
||||||
public Menu updateInfo(String menuName, String description, Integer price) {
|
public void updateMenuInfo(String menuName, String description, Integer price,
|
||||||
return Menu.builder()
|
String category, String imageUrl) {
|
||||||
.id(this.id)
|
this.menuName = menuName;
|
||||||
.storeId(this.storeId)
|
this.description = description;
|
||||||
.menuName(menuName)
|
this.price = price;
|
||||||
.description(description)
|
this.category = category;
|
||||||
.price(price)
|
this.imageUrl = imageUrl;
|
||||||
.category(this.category)
|
|
||||||
.imageUrl(this.imageUrl)
|
|
||||||
.isAvailable(this.isAvailable)
|
|
||||||
.orderCount(this.orderCount)
|
|
||||||
.createdAt(this.createdAt)
|
|
||||||
.updatedAt(LocalDateTime.now())
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 이미지 업데이트
|
* 메뉴 판매 상태 변경
|
||||||
*/
|
*/
|
||||||
public Menu updateImage(String imageUrl) {
|
public void setAvailable(Boolean available) {
|
||||||
return Menu.builder()
|
this.available = available;
|
||||||
.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) {
|
public boolean isAvailable() {
|
||||||
return Menu.builder()
|
return this.available != null && this.available;
|
||||||
.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,24 +1,20 @@
|
|||||||
|
// store/src/main/java/com/ktds/hi/store/biz/domain/Store.java
|
||||||
package com.ktds.hi.store.domain;
|
package com.ktds.hi.store.domain;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 도메인 클래스
|
* 매장 도메인 엔티티
|
||||||
* 매장 정보를 담는 도메인 객체
|
* Clean Architecture의 Domain Layer
|
||||||
*
|
*
|
||||||
* @author 하이오더 개발팀
|
* @author 하이오더 개발팀
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class Store {
|
public class Store {
|
||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
@ -27,160 +23,89 @@ public class Store {
|
|||||||
private String address;
|
private String address;
|
||||||
private Double latitude;
|
private Double latitude;
|
||||||
private Double longitude;
|
private Double longitude;
|
||||||
private String category;
|
|
||||||
private String description;
|
private String description;
|
||||||
private String phone;
|
private String phone;
|
||||||
private String operatingHours;
|
private String operatingHours;
|
||||||
private List<String> tags;
|
private String category;
|
||||||
private StoreStatus status;
|
|
||||||
private Double rating;
|
private Double rating;
|
||||||
private Integer reviewCount;
|
private Integer reviewCount;
|
||||||
private String imageUrl;
|
private StoreStatus status;
|
||||||
|
private List<String> tags; // 추가
|
||||||
|
private String imageUrl; // 추가
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 기본 정보 업데이트
|
* 매장 기본 정보 업데이트
|
||||||
*/
|
*/
|
||||||
public Store updateBasicInfo(String storeName, String address, String description,
|
public void updateBasicInfo(String storeName, String address, String description,
|
||||||
String phone, String operatingHours) {
|
String phone, String operatingHours) {
|
||||||
return Store.builder()
|
this.storeName = storeName;
|
||||||
.id(this.id)
|
this.address = address;
|
||||||
.ownerId(this.ownerId)
|
this.description = description;
|
||||||
.storeName(storeName)
|
this.phone = phone;
|
||||||
.address(address)
|
this.operatingHours = operatingHours;
|
||||||
.latitude(this.latitude)
|
this.updatedAt = LocalDateTime.now();
|
||||||
.longitude(this.longitude)
|
|
||||||
.category(this.category)
|
|
||||||
.description(description)
|
|
||||||
.phone(phone)
|
|
||||||
.operatingHours(operatingHours)
|
|
||||||
.tags(this.tags)
|
|
||||||
.status(this.status)
|
|
||||||
.rating(this.rating)
|
|
||||||
.reviewCount(this.reviewCount)
|
|
||||||
.imageUrl(this.imageUrl)
|
|
||||||
.createdAt(this.createdAt)
|
|
||||||
.updatedAt(LocalDateTime.now())
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 위치 정보 업데이트
|
* 매장 위치 업데이트
|
||||||
*/
|
*/
|
||||||
public Store updateLocation(Double latitude, Double longitude) {
|
public void updateLocation(Coordinates coordinates) {
|
||||||
return Store.builder()
|
this.latitude = coordinates.getLatitude();
|
||||||
.id(this.id)
|
this.longitude = coordinates.getLongitude();
|
||||||
.ownerId(this.ownerId)
|
this.updatedAt = LocalDateTime.now();
|
||||||
.storeName(this.storeName)
|
|
||||||
.address(this.address)
|
|
||||||
.latitude(latitude)
|
|
||||||
.longitude(longitude)
|
|
||||||
.category(this.category)
|
|
||||||
.description(this.description)
|
|
||||||
.phone(this.phone)
|
|
||||||
.operatingHours(this.operatingHours)
|
|
||||||
.tags(this.tags)
|
|
||||||
.status(this.status)
|
|
||||||
.rating(this.rating)
|
|
||||||
.reviewCount(this.reviewCount)
|
|
||||||
.imageUrl(this.imageUrl)
|
|
||||||
.createdAt(this.createdAt)
|
|
||||||
.updatedAt(LocalDateTime.now())
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 평점 및 리뷰 수 업데이트
|
* 매장 평점 업데이트
|
||||||
*/
|
*/
|
||||||
public Store updateRating(Double rating, Integer reviewCount) {
|
public void updateRating(Double rating, Integer reviewCount) {
|
||||||
return Store.builder()
|
this.rating = rating;
|
||||||
.id(this.id)
|
this.reviewCount = reviewCount;
|
||||||
.ownerId(this.ownerId)
|
this.updatedAt = LocalDateTime.now();
|
||||||
.storeName(this.storeName)
|
|
||||||
.address(this.address)
|
|
||||||
.latitude(this.latitude)
|
|
||||||
.longitude(this.longitude)
|
|
||||||
.category(this.category)
|
|
||||||
.description(this.description)
|
|
||||||
.phone(this.phone)
|
|
||||||
.operatingHours(this.operatingHours)
|
|
||||||
.tags(this.tags)
|
|
||||||
.status(this.status)
|
|
||||||
.rating(rating)
|
|
||||||
.reviewCount(reviewCount)
|
|
||||||
.imageUrl(this.imageUrl)
|
|
||||||
.createdAt(this.createdAt)
|
|
||||||
.updatedAt(LocalDateTime.now())
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 활성화
|
* 매장 활성화
|
||||||
*/
|
*/
|
||||||
public Store activate() {
|
public void activate() {
|
||||||
return Store.builder()
|
this.status = StoreStatus.ACTIVE;
|
||||||
.id(this.id)
|
this.updatedAt = LocalDateTime.now();
|
||||||
.ownerId(this.ownerId)
|
|
||||||
.storeName(this.storeName)
|
|
||||||
.address(this.address)
|
|
||||||
.latitude(this.latitude)
|
|
||||||
.longitude(this.longitude)
|
|
||||||
.category(this.category)
|
|
||||||
.description(this.description)
|
|
||||||
.phone(this.phone)
|
|
||||||
.operatingHours(this.operatingHours)
|
|
||||||
.tags(this.tags)
|
|
||||||
.status(StoreStatus.ACTIVE)
|
|
||||||
.rating(this.rating)
|
|
||||||
.reviewCount(this.reviewCount)
|
|
||||||
.imageUrl(this.imageUrl)
|
|
||||||
.createdAt(this.createdAt)
|
|
||||||
.updatedAt(LocalDateTime.now())
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 비활성화
|
* 매장 비활성화
|
||||||
*/
|
*/
|
||||||
public Store deactivate() {
|
public void deactivate() {
|
||||||
return Store.builder()
|
this.status = StoreStatus.INACTIVE;
|
||||||
.id(this.id)
|
this.updatedAt = LocalDateTime.now();
|
||||||
.ownerId(this.ownerId)
|
|
||||||
.storeName(this.storeName)
|
|
||||||
.address(this.address)
|
|
||||||
.latitude(this.latitude)
|
|
||||||
.longitude(this.longitude)
|
|
||||||
.category(this.category)
|
|
||||||
.description(this.description)
|
|
||||||
.phone(this.phone)
|
|
||||||
.operatingHours(this.operatingHours)
|
|
||||||
.tags(this.tags)
|
|
||||||
.status(StoreStatus.INACTIVE)
|
|
||||||
.rating(this.rating)
|
|
||||||
.reviewCount(this.reviewCount)
|
|
||||||
.imageUrl(this.imageUrl)
|
|
||||||
.createdAt(this.createdAt)
|
|
||||||
.updatedAt(LocalDateTime.now())
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 활성 상태 확인
|
* 매장 삭제 (소프트 삭제)
|
||||||
|
*/
|
||||||
|
public void delete() {
|
||||||
|
this.status = StoreStatus.DELETED;
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 활성 상태 확인
|
||||||
*/
|
*/
|
||||||
public boolean isActive() {
|
public boolean isActive() {
|
||||||
return StoreStatus.ACTIVE.equals(this.status);
|
return this.status == StoreStatus.ACTIVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 소유권 확인
|
* 점주 소유 확인
|
||||||
*/
|
*/
|
||||||
public boolean isOwnedBy(Long ownerId) {
|
public boolean isOwnedBy(Long ownerId) {
|
||||||
return this.ownerId != null && this.ownerId.equals(ownerId);
|
return this.ownerId.equals(ownerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 두 좌표 간의 거리 계산 (킬로미터)
|
* 거리 계산
|
||||||
*/
|
*/
|
||||||
public Double calculateDistance(Double targetLatitude, Double targetLongitude) {
|
public Double calculateDistance(Double targetLatitude, Double targetLongitude) {
|
||||||
if (this.latitude == null || this.longitude == null ||
|
if (this.latitude == null || this.longitude == null ||
|
||||||
@ -188,17 +113,18 @@ public class Store {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int EARTH_RADIUS = 6371; // 지구 반지름 (킬로미터)
|
// Haversine 공식을 사용한 거리 계산
|
||||||
|
double earthRadius = 6371; // 지구 반지름 (km)
|
||||||
|
|
||||||
double latDistance = Math.toRadians(targetLatitude - this.latitude);
|
double dLat = Math.toRadians(targetLatitude - this.latitude);
|
||||||
double lonDistance = Math.toRadians(targetLongitude - this.longitude);
|
double dLon = Math.toRadians(targetLongitude - this.longitude);
|
||||||
|
|
||||||
double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
|
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
+ Math.cos(Math.toRadians(this.latitude)) * Math.cos(Math.toRadians(targetLatitude))
|
Math.cos(Math.toRadians(this.latitude)) * Math.cos(Math.toRadians(targetLatitude)) *
|
||||||
* Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
|
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||||
|
|
||||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
|
||||||
return EARTH_RADIUS * c;
|
return earthRadius * c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,32 +2,12 @@ package com.ktds.hi.store.domain;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 상태 열거형
|
* 매장 상태 열거형
|
||||||
* 매장의 운영 상태를 정의
|
|
||||||
*
|
|
||||||
* @author 하이오더 개발팀
|
|
||||||
* @version 1.0.0
|
|
||||||
*/
|
*/
|
||||||
public enum StoreStatus {
|
public enum StoreStatus {
|
||||||
|
|
||||||
/**
|
|
||||||
* 활성 상태 - 정상 운영 중
|
|
||||||
*/
|
|
||||||
ACTIVE("활성"),
|
ACTIVE("활성"),
|
||||||
|
|
||||||
/**
|
|
||||||
* 비활성 상태 - 임시 휴업
|
|
||||||
*/
|
|
||||||
INACTIVE("비활성"),
|
INACTIVE("비활성"),
|
||||||
|
DELETED("삭제됨"),
|
||||||
/**
|
PENDING("승인대기");
|
||||||
* 일시 정지 상태 - 관리자에 의한 일시 정지
|
|
||||||
*/
|
|
||||||
SUSPENDED("일시정지"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 삭제 상태 - 영구 삭제 (소프트 삭제)
|
|
||||||
*/
|
|
||||||
DELETED("삭제");
|
|
||||||
|
|
||||||
private final String description;
|
private final String description;
|
||||||
|
|
||||||
@ -44,27 +24,13 @@ public enum StoreStatus {
|
|||||||
*/
|
*/
|
||||||
public static StoreStatus fromString(String status) {
|
public static StoreStatus fromString(String status) {
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
return INACTIVE;
|
return ACTIVE; // 기본값
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return StoreStatus.valueOf(status.toUpperCase());
|
return StoreStatus.valueOf(status.toUpperCase());
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
return INACTIVE;
|
return ACTIVE; // 기본값
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 매장이 서비스 가능한 상태인지 확인
|
|
||||||
*/
|
|
||||||
public boolean isServiceable() {
|
|
||||||
return this == ACTIVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 매장이 삭제된 상태인지 확인
|
|
||||||
*/
|
|
||||||
public boolean isDeleted() {
|
|
||||||
return this == DELETED;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -0,0 +1,120 @@
|
|||||||
|
// store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java
|
||||||
|
package com.ktds.hi.store.infra.controller;
|
||||||
|
|
||||||
|
import com.ktds.hi.store.biz.usecase.in.StoreUseCase;
|
||||||
|
import com.ktds.hi.store.infra.dto.*;
|
||||||
|
import com.ktds.hi.common.dto.ApiResponse;
|
||||||
|
import com.ktds.hi.common.security.JwtTokenProvider;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 관리 컨트롤러
|
||||||
|
* 매장 등록, 수정, 삭제, 조회 기능 제공
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Tag(name = "매장 관리", description = "매장 등록, 수정, 삭제, 조회 API")
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/stores")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Validated
|
||||||
|
public class StoreController {
|
||||||
|
|
||||||
|
private final StoreUseCase storeUseCase;
|
||||||
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
|
@Operation(summary = "매장 등록", description = "새로운 매장을 등록합니다.")
|
||||||
|
@PostMapping
|
||||||
|
@PreAuthorize("hasRole('OWNER')")
|
||||||
|
public ResponseEntity<ApiResponse<StoreCreateResponse>> createStore(
|
||||||
|
@Valid @RequestBody StoreCreateRequest request,
|
||||||
|
HttpServletRequest httpRequest) {
|
||||||
|
|
||||||
|
Long ownerId = jwtTokenProvider.extractOwnerInfo(httpRequest);
|
||||||
|
StoreCreateResponse response = storeUseCase.createStore(ownerId, request);
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED)
|
||||||
|
.body(ApiResponse.success(response, "매장이 성공적으로 등록되었습니다."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "내 매장 목록 조회", description = "점주가 등록한 매장 목록을 조회합니다.")
|
||||||
|
@GetMapping("/my")
|
||||||
|
@PreAuthorize("hasRole('OWNER')")
|
||||||
|
public ResponseEntity<ApiResponse<List<MyStoreListResponse>>> getMyStores(
|
||||||
|
HttpServletRequest httpRequest) {
|
||||||
|
|
||||||
|
Long ownerId = jwtTokenProvider.extractOwnerInfo(httpRequest);
|
||||||
|
List<MyStoreListResponse> responses = storeUseCase.getMyStores(ownerId);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(responses, "내 매장 목록 조회 완료"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "매장 상세 조회", description = "매장의 상세 정보를 조회합니다.")
|
||||||
|
@GetMapping("/{storeId}")
|
||||||
|
public ResponseEntity<ApiResponse<StoreDetailResponse>> getStoreDetail(
|
||||||
|
@Parameter(description = "매장 ID") @PathVariable Long storeId) {
|
||||||
|
|
||||||
|
StoreDetailResponse response = storeUseCase.getStoreDetail(storeId);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response, "매장 상세 정보 조회 완료"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "매장 정보 수정", description = "매장 정보를 수정합니다.")
|
||||||
|
@PutMapping("/{storeId}")
|
||||||
|
@PreAuthorize("hasRole('OWNER')")
|
||||||
|
public ResponseEntity<ApiResponse<StoreUpdateResponse>> updateStore(
|
||||||
|
@Parameter(description = "매장 ID") @PathVariable Long storeId,
|
||||||
|
@Valid @RequestBody StoreUpdateRequest request,
|
||||||
|
HttpServletRequest httpRequest) {
|
||||||
|
|
||||||
|
Long ownerId = jwtTokenProvider.extractOwnerInfo(httpRequest);
|
||||||
|
StoreUpdateResponse response = storeUseCase.updateStore(storeId, ownerId, request);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response, "매장 정보 수정 완료"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "매장 삭제", description = "매장을 삭제합니다.")
|
||||||
|
@DeleteMapping("/{storeId}")
|
||||||
|
@PreAuthorize("hasRole('OWNER')")
|
||||||
|
public ResponseEntity<ApiResponse<StoreDeleteResponse>> deleteStore(
|
||||||
|
@Parameter(description = "매장 ID") @PathVariable Long storeId,
|
||||||
|
HttpServletRequest httpRequest) {
|
||||||
|
|
||||||
|
Long ownerId = jwtTokenProvider.extractOwnerInfo(httpRequest);
|
||||||
|
StoreDeleteResponse response = storeUseCase.deleteStore(storeId, ownerId);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response, "매장 삭제 완료"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "매장 검색", description = "조건에 따라 매장을 검색합니다.")
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ResponseEntity<ApiResponse<List<StoreSearchResponse>>> searchStores(
|
||||||
|
@Parameter(description = "검색 키워드") @RequestParam(required = false) String keyword,
|
||||||
|
@Parameter(description = "카테고리") @RequestParam(required = false) String category,
|
||||||
|
@Parameter(description = "태그") @RequestParam(required = false) String tags,
|
||||||
|
@Parameter(description = "위도") @RequestParam(required = false) Double latitude,
|
||||||
|
@Parameter(description = "경도") @RequestParam(required = false) Double longitude,
|
||||||
|
@Parameter(description = "검색 반경(km)") @RequestParam(defaultValue = "5") Integer radius,
|
||||||
|
@Parameter(description = "페이지 번호") @RequestParam(defaultValue = "0") Integer page,
|
||||||
|
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "20") Integer size) {
|
||||||
|
|
||||||
|
List<StoreSearchResponse> responses = storeUseCase.searchStores(
|
||||||
|
keyword, category, tags, latitude, longitude, radius, page, size);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(responses, "매장 검색 완료"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto;
|
||||||
|
|
||||||
|
import com.ktds.hi.store.domain.Menu;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 등록 요청 DTO
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "메뉴 등록 요청")
|
||||||
|
public class MenuCreateRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "메뉴명은 필수입니다.")
|
||||||
|
@Schema(description = "메뉴명", example = "김치찌개")
|
||||||
|
private String menuName;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 설명", example = "얼큰한 김치찌개")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Min(value = 0, message = "가격은 0원 이상이어야 합니다.")
|
||||||
|
@Schema(description = "가격", example = "8000")
|
||||||
|
private Integer price;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 카테고리", example = "메인")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "이미지 URL", example = "https://example.com/kimchi.jpg")
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
@Schema(description = "이용 가능 여부", example = "true")
|
||||||
|
private Boolean available = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도메인 객체로 변환
|
||||||
|
*/
|
||||||
|
public Menu toDomain(Long storeId) {
|
||||||
|
return Menu.builder()
|
||||||
|
.storeId(storeId)
|
||||||
|
.menuName(this.menuName)
|
||||||
|
.description(this.description)
|
||||||
|
.price(this.price)
|
||||||
|
.category(this.category)
|
||||||
|
.imageUrl(this.imageUrl)
|
||||||
|
.available(this.available != null ? this.available : true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto;
|
||||||
|
|
||||||
|
import com.ktds.hi.store.domain.Menu;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 응답 DTO
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "메뉴 응답")
|
||||||
|
public class MenuResponse {
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 ID", example = "1")
|
||||||
|
private Long menuId;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴명", example = "김치찌개")
|
||||||
|
private String menuName;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 설명", example = "얼큰한 김치찌개")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "가격", example = "8000")
|
||||||
|
private Integer price;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 카테고리", example = "메인")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "이미지 URL")
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
@Schema(description = "이용 가능 여부", example = "true")
|
||||||
|
private Boolean available;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도메인 객체로부터 생성
|
||||||
|
*/
|
||||||
|
public static MenuResponse from(Menu menu) {
|
||||||
|
return MenuResponse.builder()
|
||||||
|
.menuId(menu.getId())
|
||||||
|
.menuName(menu.getMenuName())
|
||||||
|
.description(menu.getDescription())
|
||||||
|
.price(menu.getPrice())
|
||||||
|
.category(menu.getCategory())
|
||||||
|
.imageUrl(menu.getImageUrl())
|
||||||
|
.available(menu.getAvailable())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 내 매장 목록 응답 DTO
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "내 매장 목록 응답")
|
||||||
|
public class MyStoreListResponse {
|
||||||
|
|
||||||
|
@Schema(description = "매장 ID", example = "1")
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
|
@Schema(description = "매장명", example = "맛집 한번 가볼래?")
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@Schema(description = "주소", example = "서울시 강남구 테헤란로 123")
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@Schema(description = "카테고리", example = "한식")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "평점", example = "4.5")
|
||||||
|
private Double rating;
|
||||||
|
|
||||||
|
@Schema(description = "리뷰 수", example = "127")
|
||||||
|
private Integer reviewCount;
|
||||||
|
|
||||||
|
@Schema(description = "운영 상태", example = "운영중")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "운영시간", example = "월-금 09:00-21:00")
|
||||||
|
private String operatingHours;
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 등록 요청 DTO
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "매장 등록 요청")
|
||||||
|
public class StoreCreateRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "매장명은 필수입니다.")
|
||||||
|
@Size(max = 100, message = "매장명은 100자를 초과할 수 없습니다.")
|
||||||
|
@Schema(description = "매장명", example = "맛집 한번 가볼래?")
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@NotBlank(message = "주소는 필수입니다.")
|
||||||
|
@Schema(description = "매장 주소", example = "서울시 강남구 테헤란로 123")
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@Schema(description = "매장 설명", example = "맛있는 한식당입니다.")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "전화번호", example = "02-1234-5678")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Schema(description = "운영시간", example = "월-금 09:00-21:00, 토-일 10:00-20:00")
|
||||||
|
private String operatingHours;
|
||||||
|
|
||||||
|
@NotBlank(message = "카테고리는 필수입니다.")
|
||||||
|
@Schema(description = "카테고리", example = "한식")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "매장 태그 목록", example = "[\"맛집\", \"혼밥\", \"가성비\"]")
|
||||||
|
private List<String> tags;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 목록")
|
||||||
|
private List<MenuCreateRequest> menus;
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 등록 응답 DTO
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "매장 등록 응답")
|
||||||
|
public class StoreCreateResponse {
|
||||||
|
|
||||||
|
@Schema(description = "생성된 매장 ID", example = "1")
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
|
@Schema(description = "매장명", example = "맛집 한번 가볼래?")
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@Schema(description = "응답 메시지", example = "매장이 성공적으로 등록되었습니다.")
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package com.ktds.hi.store.infra.dto;
|
package com.ktds.hi.store.infra.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -12,8 +13,12 @@ import lombok.NoArgsConstructor;
|
|||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "매장 삭제 응답")
|
||||||
public class StoreDeleteResponse {
|
public class StoreDeleteResponse {
|
||||||
|
|
||||||
private Boolean success;
|
@Schema(description = "삭제된 매장 ID", example = "1")
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
|
@Schema(description = "응답 메시지", example = "매장이 성공적으로 삭제되었습니다.")
|
||||||
private String message;
|
private String message;
|
||||||
}
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 상세 조회 응답 DTO
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "매장 상세 조회 응답")
|
||||||
|
public class StoreDetailResponse {
|
||||||
|
|
||||||
|
@Schema(description = "매장 ID", example = "1")
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
|
@Schema(description = "매장명", example = "맛집 한번 가볼래?")
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@Schema(description = "주소", example = "서울시 강남구 테헤란로 123")
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@Schema(description = "위도", example = "37.5665")
|
||||||
|
private Double latitude;
|
||||||
|
|
||||||
|
@Schema(description = "경도", example = "126.9780")
|
||||||
|
private Double longitude;
|
||||||
|
|
||||||
|
@Schema(description = "매장 설명", example = "맛있는 한식당입니다.")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "전화번호", example = "02-1234-5678")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Schema(description = "운영시간", example = "월-금 09:00-21:00")
|
||||||
|
private String operatingHours;
|
||||||
|
|
||||||
|
@Schema(description = "카테고리", example = "한식")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "평점", example = "4.5")
|
||||||
|
private Double rating;
|
||||||
|
|
||||||
|
@Schema(description = "리뷰 수", example = "127")
|
||||||
|
private Integer reviewCount;
|
||||||
|
|
||||||
|
@Schema(description = "매장 상태", example = "ACTIVE")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "매장 태그 목록")
|
||||||
|
private List<String> tags;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 목록")
|
||||||
|
private List<MenuResponse> menus;
|
||||||
|
|
||||||
|
@Schema(description = "AI 요약 정보")
|
||||||
|
private String aiSummary;
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 검색 응답 DTO
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "매장 검색 응답")
|
||||||
|
public class StoreSearchResponse {
|
||||||
|
|
||||||
|
@Schema(description = "매장 ID", example = "1")
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
|
@Schema(description = "매장명", example = "맛집 한번 가볼래?")
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@Schema(description = "주소", example = "서울시 강남구 테헤란로 123")
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@Schema(description = "카테고리", example = "한식")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "평점", example = "4.5")
|
||||||
|
private Double rating;
|
||||||
|
|
||||||
|
@Schema(description = "리뷰 수", example = "127")
|
||||||
|
private Integer reviewCount;
|
||||||
|
|
||||||
|
@Schema(description = "거리(km)", example = "1.2")
|
||||||
|
private Double distance;
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 수정 요청 DTO
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "매장 수정 요청")
|
||||||
|
public class StoreUpdateRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "매장명은 필수입니다.")
|
||||||
|
@Size(max = 100, message = "매장명은 100자를 초과할 수 없습니다.")
|
||||||
|
@Schema(description = "매장명", example = "맛집 한번 가볼래?")
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@NotBlank(message = "주소는 필수입니다.")
|
||||||
|
@Schema(description = "매장 주소", example = "서울시 강남구 테헤란로 123")
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@Schema(description = "매장 설명", example = "맛있는 한식당입니다.")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "전화번호", example = "02-1234-5678")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Schema(description = "운영시간", example = "월-금 09:00-21:00, 토-일 10:00-20:00")
|
||||||
|
private String operatingHours;
|
||||||
|
|
||||||
|
@Schema(description = "매장 태그 목록", example = "[\"맛집\", \"혼밥\", \"가성비\"]")
|
||||||
|
private List<String> tags;
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package com.ktds.hi.store.infra.dto;
|
package com.ktds.hi.store.infra.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -12,8 +13,12 @@ import lombok.NoArgsConstructor;
|
|||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "매장 수정 응답")
|
||||||
public class StoreUpdateResponse {
|
public class StoreUpdateResponse {
|
||||||
|
|
||||||
private Boolean success;
|
@Schema(description = "매장 ID", example = "1")
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
|
@Schema(description = "응답 메시지", example = "매장 정보가 성공적으로 수정되었습니다.")
|
||||||
private String message;
|
private String message;
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캐시 어댑터 클래스
|
* 캐시 어댑터 클래스
|
||||||
@ -34,27 +36,43 @@ public class CacheAdapter implements CachePort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
||||||
|
log.debug("매장 캐시 무효화 완료: storeId={}", storeId);
|
||||||
|
|
||||||
redisTemplate.delete(storeDetailKey);
|
} 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);
|
||||||
|
}
|
||||||
|
|
||||||
log.debug("매장 캐시 무효화 완료: storeId={}", storeId);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("매장 캐시 무효화 실패: storeId={}, error={}", storeId, e.getMessage());
|
log.error("캐시 무효화 실패: key={}, error={}", key, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.ktds.hi.store.infra.gateway;
|
package com.ktds.hi.store.infra.gateway;
|
||||||
|
|
||||||
import com.ktds.hi.store.biz.domain.Menu;
|
import com.ktds.hi.store.domain.Menu;
|
||||||
import com.ktds.hi.store.biz.usecase.out.MenuRepositoryPort;
|
import com.ktds.hi.store.biz.usecase.out.MenuRepositoryPort;
|
||||||
import com.ktds.hi.store.infra.gateway.entity.MenuEntity;
|
import com.ktds.hi.store.infra.gateway.entity.MenuEntity;
|
||||||
import com.ktds.hi.store.infra.gateway.repository.MenuJpaRepository;
|
import com.ktds.hi.store.infra.gateway.repository.MenuJpaRepository;
|
||||||
@ -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();
|
||||||
@ -114,7 +114,7 @@ public class MenuRepositoryAdapter implements MenuRepositoryPort {
|
|||||||
.price(domain.getPrice())
|
.price(domain.getPrice())
|
||||||
.category(domain.getCategory())
|
.category(domain.getCategory())
|
||||||
.imageUrl(domain.getImageUrl())
|
.imageUrl(domain.getImageUrl())
|
||||||
.isAvailable(domain.getIsAvailable())
|
.isAvailable(domain.isAvailable())
|
||||||
.createdAt(domain.getCreatedAt())
|
.createdAt(domain.getCreatedAt())
|
||||||
.updatedAt(domain.getUpdatedAt())
|
.updatedAt(domain.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