# 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:
UNGGU0704 2025-06-13 17:37:28 +09:00
commit 55c5845772
41 changed files with 2821 additions and 2148 deletions

View File

@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class SecurityConfig {
private final CorsConfigurationSource corsConfigurationSource;
@Bean

View File

@ -20,10 +20,10 @@ import java.util.List;
@Configuration
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;
@Value("${app.cors.allowed-methods:GET,POST,PUT,DELETE,PATCH,OPTIONS}")
@Value("${app.cors.allowed-methods:GET,POST,PUT,DELETE,OPTIONS}")
private String allowedMethods;
@Value("${app.cors.allowed-headers:*}")

View File

@ -1,5 +1,7 @@
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 io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
@ -55,6 +57,82 @@ public class JwtTokenProvider {
.verifyWith(secretKey)
.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", "인증 처리 중 오류가 발생했습니다.");
}
}
/**
* 액세스 토큰 생성

View File

@ -53,7 +53,7 @@ app:
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000} # 7일
# 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-headers: ${CORS_ALLOWED_HEADERS:*}
exposed-headers: ${CORS_EXPOSED_HEADERS:Authorization, X-Total-Count}

View File

@ -44,6 +44,8 @@ spring:
order_updates: true
open-in-view: false
# Redis 설정 (올바른 구조)
data:
redis:

View File

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

View File

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

View File

@ -1,26 +1,24 @@
package com.ktds.hi.store.biz.usecase.out;
import java.time.Duration;
import java.util.Optional;
import java.util.List;
/**
* 캐시 포트 인터페이스
* 캐시 기능을 정의
*/
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);
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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.Optional;

View File

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

View File

@ -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.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 메뉴 도메인 클래스
* 메뉴 정보를 담는 도메인 객체
*
* @author 하이오더 개발팀
* @version 1.0.0
* 메뉴 도메인 엔티티
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Menu {
private Long id;
@ -27,164 +18,33 @@ public class Menu {
private Integer price;
private String category;
private String imageUrl;
private Boolean isAvailable;
private Integer orderCount;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private Boolean available;
private LocalDateTime createdAt; // 추가
private LocalDateTime updatedAt; // 추가
/**
* 메뉴 기본 정보 업데이트
* 메뉴 정보 업데이트
*/
public Menu updateInfo(String menuName, String description, Integer price) {
return Menu.builder()
.id(this.id)
.storeId(this.storeId)
.menuName(menuName)
.description(description)
.price(price)
.category(this.category)
.imageUrl(this.imageUrl)
.isAvailable(this.isAvailable)
.orderCount(this.orderCount)
.createdAt(this.createdAt)
.updatedAt(LocalDateTime.now())
.build();
public void updateMenuInfo(String menuName, String description, Integer price,
String category, String imageUrl) {
this.menuName = menuName;
this.description = description;
this.price = price;
this.category = category;
this.imageUrl = imageUrl;
}
/**
* 메뉴 이미지 업데이트
* 메뉴 판매 상태 변경
*/
public Menu updateImage(String imageUrl) {
return Menu.builder()
.id(this.id)
.storeId(this.storeId)
.menuName(this.menuName)
.description(this.description)
.price(this.price)
.category(this.category)
.imageUrl(imageUrl)
.isAvailable(this.isAvailable)
.orderCount(this.orderCount)
.createdAt(this.createdAt)
.updatedAt(LocalDateTime.now())
.build();
public void setAvailable(Boolean available) {
this.available = available;
}
/**
* 메뉴 판매 가능 상태 설정
* 메뉴 이용 가능 여부 확인
*/
public Menu setAvailable(Boolean available) {
return Menu.builder()
.id(this.id)
.storeId(this.storeId)
.menuName(this.menuName)
.description(this.description)
.price(this.price)
.category(this.category)
.imageUrl(this.imageUrl)
.isAvailable(available)
.orderCount(this.orderCount)
.createdAt(this.createdAt)
.updatedAt(LocalDateTime.now())
.build();
}
/**
* 주문 증가
*/
public Menu incrementOrderCount() {
return Menu.builder()
.id(this.id)
.storeId(this.storeId)
.menuName(this.menuName)
.description(this.description)
.price(this.price)
.category(this.category)
.imageUrl(this.imageUrl)
.isAvailable(this.isAvailable)
.orderCount(this.orderCount != null ? this.orderCount + 1 : 1)
.createdAt(this.createdAt)
.updatedAt(LocalDateTime.now())
.build();
}
/**
* 메뉴 판매 가능 여부 확인
*/
public Boolean isAvailable() {
return this.isAvailable != null && this.isAvailable;
}
/**
* 특정 매장에 속하는지 확인
*/
public boolean belongsToStore(Long storeId) {
return this.storeId != null && this.storeId.equals(storeId);
}
/**
* 메뉴가 유효한지 확인
*/
public boolean isValid() {
return this.menuName != null && !this.menuName.trim().isEmpty() &&
this.price != null && this.price > 0 &&
this.storeId != null;
}
/**
* 메뉴 카테고리 업데이트
*/
public Menu updateCategory(String category) {
return Menu.builder()
.id(this.id)
.storeId(this.storeId)
.menuName(this.menuName)
.description(this.description)
.price(this.price)
.category(category)
.imageUrl(this.imageUrl)
.isAvailable(this.isAvailable)
.orderCount(this.orderCount)
.createdAt(this.createdAt)
.updatedAt(LocalDateTime.now())
.build();
}
/**
* 메뉴 가격 할인 적용
*/
public Menu applyDiscount(double discountRate) {
if (discountRate < 0 || discountRate > 1) {
throw new IllegalArgumentException("할인율은 0~1 사이의 값이어야 합니다.");
}
int discountedPrice = (int) (this.price * (1 - discountRate));
return Menu.builder()
.id(this.id)
.storeId(this.storeId)
.menuName(this.menuName)
.description(this.description)
.price(discountedPrice)
.category(this.category)
.imageUrl(this.imageUrl)
.isAvailable(this.isAvailable)
.orderCount(this.orderCount)
.createdAt(this.createdAt)
.updatedAt(LocalDateTime.now())
.build();
}
/**
* 메뉴 정보가 변경되었는지 확인
*/
public boolean hasChanges(Menu other) {
if (other == null) return true;
return !this.menuName.equals(other.menuName) ||
!this.description.equals(other.description) ||
!this.price.equals(other.price) ||
!this.category.equals(other.category) ||
!this.isAvailable.equals(other.isAvailable);
public boolean isAvailable() {
return this.available != null && this.available;
}
}

View File

@ -1,24 +1,20 @@
// store/src/main/java/com/ktds/hi/store/biz/domain/Store.java
package com.ktds.hi.store.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* 매장 도메인 클래스
* 매장 정보를 담는 도메인 객체
* 매장 도메인 엔티티
* Clean Architecture의 Domain Layer
*
* @author 하이오더 개발팀
* @version 1.0.0
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Store {
private Long id;
@ -27,160 +23,89 @@ public class Store {
private String address;
private Double latitude;
private Double longitude;
private String category;
private String description;
private String phone;
private String operatingHours;
private List<String> tags;
private StoreStatus status;
private String category;
private Double rating;
private Integer reviewCount;
private String imageUrl;
private StoreStatus status;
private List<String> tags; // 추가
private String imageUrl; // 추가
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
/**
* 매장 기본 정보 업데이트
*/
public Store updateBasicInfo(String storeName, String address, String description,
String phone, String operatingHours) {
return Store.builder()
.id(this.id)
.ownerId(this.ownerId)
.storeName(storeName)
.address(address)
.latitude(this.latitude)
.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 void updateBasicInfo(String storeName, String address, String description,
String phone, String operatingHours) {
this.storeName = storeName;
this.address = address;
this.description = description;
this.phone = phone;
this.operatingHours = operatingHours;
this.updatedAt = LocalDateTime.now();
}
/**
* 매장 위치 정보 업데이트
* 매장 위치 업데이트
*/
public Store updateLocation(Double latitude, Double longitude) {
return Store.builder()
.id(this.id)
.ownerId(this.ownerId)
.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 void updateLocation(Coordinates coordinates) {
this.latitude = coordinates.getLatitude();
this.longitude = coordinates.getLongitude();
this.updatedAt = LocalDateTime.now();
}
/**
* 매장 평점 리뷰 업데이트
* 매장 평점 업데이트
*/
public Store updateRating(Double rating, Integer reviewCount) {
return Store.builder()
.id(this.id)
.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(this.status)
.rating(rating)
.reviewCount(reviewCount)
.imageUrl(this.imageUrl)
.createdAt(this.createdAt)
.updatedAt(LocalDateTime.now())
.build();
public void updateRating(Double rating, Integer reviewCount) {
this.rating = rating;
this.reviewCount = reviewCount;
this.updatedAt = LocalDateTime.now();
}
/**
* 매장 활성화
*/
public Store activate() {
return Store.builder()
.id(this.id)
.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 void activate() {
this.status = StoreStatus.ACTIVE;
this.updatedAt = LocalDateTime.now();
}
/**
* 매장 비활성화
*/
public Store deactivate() {
return Store.builder()
.id(this.id)
.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 deactivate() {
this.status = StoreStatus.INACTIVE;
this.updatedAt = LocalDateTime.now();
}
/**
* 매장 활성 상태 확인
* 매장 삭제 (소프트 삭제)
*/
public void delete() {
this.status = StoreStatus.DELETED;
this.updatedAt = LocalDateTime.now();
}
/**
* 활성 상태 확인
*/
public boolean isActive() {
return StoreStatus.ACTIVE.equals(this.status);
return this.status == StoreStatus.ACTIVE;
}
/**
* 매장 소유권 확인
* 점주 소유 확인
*/
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) {
if (this.latitude == null || this.longitude == null ||
@ -188,17 +113,18 @@ public class Store {
return null;
}
final int EARTH_RADIUS = 6371; // 지구 반지름 (킬로미터)
// Haversine 공식을 사용한 거리 계산
double earthRadius = 6371; // 지구 반지름 (km)
double latDistance = Math.toRadians(targetLatitude - this.latitude);
double lonDistance = Math.toRadians(targetLongitude - this.longitude);
double dLat = Math.toRadians(targetLatitude - this.latitude);
double dLon = Math.toRadians(targetLongitude - this.longitude);
double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
+ Math.cos(Math.toRadians(this.latitude)) * Math.cos(Math.toRadians(targetLatitude))
* Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(this.latitude)) * Math.cos(Math.toRadians(targetLatitude)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS * c;
return earthRadius * c;
}
}

View File

@ -2,32 +2,12 @@ package com.ktds.hi.store.domain;
/**
* 매장 상태 열거형
* 매장의 운영 상태를 정의
*
* @author 하이오더 개발팀
* @version 1.0.0
*/
public enum StoreStatus {
/**
* 활성 상태 - 정상 운영
*/
ACTIVE("활성"),
/**
* 비활성 상태 - 임시 휴업
*/
INACTIVE("비활성"),
/**
* 일시 정지 상태 - 관리자에 의한 일시 정지
*/
SUSPENDED("일시정지"),
/**
* 삭제 상태 - 영구 삭제 (소프트 삭제)
*/
DELETED("삭제");
DELETED("삭제됨"),
PENDING("승인대기");
private final String description;
@ -44,27 +24,13 @@ public enum StoreStatus {
*/
public static StoreStatus fromString(String status) {
if (status == null) {
return INACTIVE;
return ACTIVE; // 기본값
}
try {
return StoreStatus.valueOf(status.toUpperCase());
} catch (IllegalArgumentException e) {
return INACTIVE;
return ACTIVE; // 기본값
}
}
/**
* 매장이 서비스 가능한 상태인지 확인
*/
public boolean isServiceable() {
return this == ACTIVE;
}
/**
* 매장이 삭제된 상태인지 확인
*/
public boolean isDeleted() {
return this == DELETED;
}
}

View File

@ -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, "매장 검색 완료"));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package com.ktds.hi.store.infra.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@ -12,8 +13,12 @@ import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "매장 삭제 응답")
public class StoreDeleteResponse {
private Boolean success;
@Schema(description = "삭제된 매장 ID", example = "1")
private Long storeId;
@Schema(description = "응답 메시지", example = "매장이 성공적으로 삭제되었습니다.")
private String message;
}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package com.ktds.hi.store.infra.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@ -12,8 +13,12 @@ import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "매장 수정 응답")
public class StoreUpdateResponse {
private Boolean success;
@Schema(description = "매장 ID", example = "1")
private Long storeId;
@Schema(description = "응답 메시지", example = "매장 정보가 성공적으로 수정되었습니다.")
private String message;
}

View File

@ -9,6 +9,8 @@ import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 캐시 어댑터 클래스
@ -34,27 +36,43 @@ public class CacheAdapter implements CachePort {
}
@Override
public void putStoreCache(String key, Object value, Duration ttl) {
public void putStoreCache(String key, Object value, long ttlSeconds) {
try {
redisTemplate.opsForValue().set(key, value, ttl);
log.debug("매장 캐시 저장 완료: key={}, ttl={}분", key, ttl.toMinutes());
redisTemplate.opsForValue().set(key, value, ttlSeconds, TimeUnit.SECONDS);
log.debug("캐시 저장: key={}, ttl={}초", key, ttlSeconds);
} catch (Exception e) {
log.error("매장 캐시 저장 실패: key={}, error={}", key, e.getMessage());
log.warn("캐시 저장 실패: key={}, error={}", key, e.getMessage());
}
}
@Override
public void invalidateStoreCache(Long storeId) {
public void invalidateStoreCache(Object key) {
try {
// 매장 관련 모든 캐시 패턴 삭제
String storeDetailKey = "store_detail:" + storeId;
String myStoresKey = "my_stores:*";
if (key instanceof Long) {
// 매장 ID로 특정 매장 캐시 삭제
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) {
log.error("매장 캐시 무효화 실패: storeId={}, error={}", storeId, e.getMessage());
log.error("캐시 무효화 실패: key={}, error={}", key, e.getMessage());
}
}
}

View File

@ -1,6 +1,6 @@
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.infra.gateway.entity.MenuEntity;
import com.ktds.hi.store.infra.gateway.repository.MenuJpaRepository;
@ -96,7 +96,7 @@ public class MenuRepositoryAdapter implements MenuRepositoryPort {
.price(entity.getPrice())
.category(entity.getCategory())
.imageUrl(entity.getImageUrl())
.isAvailable(entity.getIsAvailable())
.available(entity.getIsAvailable())
.createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt())
.build();
@ -114,7 +114,7 @@ public class MenuRepositoryAdapter implements MenuRepositoryPort {
.price(domain.getPrice())
.category(domain.getCategory())
.imageUrl(domain.getImageUrl())
.isAvailable(domain.getIsAvailable())
.isAvailable(domain.isAvailable())
.createdAt(domain.getCreatedAt())
.updatedAt(domain.getUpdatedAt())
.build();

View File

@ -119,6 +119,19 @@ public class StoreEntity {
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();
}
/**
* 매장 태그 업데이트
*/