store add
This commit is contained in:
parent
bc51e15662
commit
861f181052
@ -1,6 +1,8 @@
|
|||||||
// common/src/main/java/com/ktds/hi/common/exception/BusinessException.java
|
// common/src/main/java/com/ktds/hi/common/exception/BusinessException.java
|
||||||
package com.ktds.hi.common.exception;
|
package com.ktds.hi.common.exception;
|
||||||
|
|
||||||
|
import com.ktds.hi.common.dto.ResponseCode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 비즈니스 로직 예외의 기본 클래스
|
* 비즈니스 로직 예외의 기본 클래스
|
||||||
* 모든 커스텀 예외의 부모 클래스
|
* 모든 커스텀 예외의 부모 클래스
|
||||||
@ -10,6 +12,22 @@ public class BusinessException extends RuntimeException {
|
|||||||
private String errorCode;
|
private String errorCode;
|
||||||
private Object[] args;
|
private Object[] args;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResponseCode와 메시지로 예외 생성
|
||||||
|
*/
|
||||||
|
public BusinessException(ResponseCode responseCode, String message) {
|
||||||
|
super(message);
|
||||||
|
this.errorCode = responseCode.getCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResponseCode로 예외 생성 (기본 메시지 사용)
|
||||||
|
*/
|
||||||
|
public BusinessException(ResponseCode responseCode) {
|
||||||
|
super(responseCode.getMessage());
|
||||||
|
this.errorCode = responseCode.getCode();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메시지만으로 예외 생성
|
* 메시지만으로 예외 생성
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -3,6 +3,9 @@ package com.ktds.hi.common.security;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import com.ktds.hi.common.exception.BusinessException;
|
import com.ktds.hi.common.exception.BusinessException;
|
||||||
import com.ktds.hi.common.constants.SecurityConstants;
|
import com.ktds.hi.common.constants.SecurityConstants;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import com.ktds.hi.common.exception.BusinessException;
|
||||||
|
import com.ktds.hi.common.dto.ResponseCode;
|
||||||
import io.jsonwebtoken.*;
|
import io.jsonwebtoken.*;
|
||||||
import io.jsonwebtoken.security.Keys;
|
import io.jsonwebtoken.security.Keys;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -134,6 +137,52 @@ public class JwtTokenProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HttpServletRequest에서 점주 ID 추출
|
||||||
|
*
|
||||||
|
* @param request HTTP 요청 객체
|
||||||
|
* @return 점주 ID
|
||||||
|
*/
|
||||||
|
public Long extractOwnerIdFromRequest(HttpServletRequest request) {
|
||||||
|
try {
|
||||||
|
String token = getJwtFromRequest(request);
|
||||||
|
if (token == null) {
|
||||||
|
throw new BusinessException(ResponseCode.UNAUTHORIZED, "토큰이 필요합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateToken(token)) {
|
||||||
|
throw new BusinessException(ResponseCode.INVALID_TOKEN, "유효하지 않은 토큰입니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String userId = getUserIdFromToken(token);
|
||||||
|
if (userId == null || userId.trim().isEmpty()) {
|
||||||
|
throw new BusinessException(ResponseCode.INVALID_TOKEN, "토큰에서 사용자 정보를 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Long.parseLong(userId);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new BusinessException(ResponseCode.INVALID_TOKEN, "유효하지 않은 사용자 ID 형식입니다.");
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("토큰에서 점주 ID 추출 실패", e);
|
||||||
|
throw new BusinessException(ResponseCode.UNAUTHORIZED, "인증에 실패했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 요청에서 JWT 토큰 추출
|
||||||
|
*/
|
||||||
|
private String getJwtFromRequest(HttpServletRequest request) {
|
||||||
|
String bearerToken = request.getHeader("Authorization");
|
||||||
|
|
||||||
|
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
|
||||||
|
return bearerToken.substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 액세스 토큰 생성
|
* 액세스 토큰 생성
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,247 @@
|
|||||||
|
// store/src/main/java/com/ktds/hi/store/biz/service/MenuService.java
|
||||||
|
package com.ktds.hi.store.biz.service;
|
||||||
|
|
||||||
|
import com.ktds.hi.common.dto.ResponseCode;
|
||||||
|
import com.ktds.hi.common.exception.BusinessException;
|
||||||
|
import com.ktds.hi.store.domain.Menu;
|
||||||
|
import com.ktds.hi.store.domain.Store;
|
||||||
|
import com.ktds.hi.store.biz.usecase.in.MenuUseCase;
|
||||||
|
import com.ktds.hi.store.biz.usecase.out.MenuRepositoryPort;
|
||||||
|
import com.ktds.hi.store.biz.usecase.out.StoreRepositoryPort;
|
||||||
|
import com.ktds.hi.store.infra.dto.request.MenuCreateRequest;
|
||||||
|
import com.ktds.hi.store.infra.dto.request.MenuUpdateRequest;
|
||||||
|
import com.ktds.hi.store.infra.dto.response.MenuCreateResponse;
|
||||||
|
import com.ktds.hi.store.infra.dto.response.MenuDetailResponse;
|
||||||
|
import com.ktds.hi.store.infra.dto.response.MenuUpdateResponse;
|
||||||
|
import com.ktds.hi.store.infra.dto.response.StoreMenuListResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 서비스 구현체
|
||||||
|
* 메뉴 관련 비즈니스 로직을 처리
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
//@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class MenuService implements MenuUseCase {
|
||||||
|
|
||||||
|
private final MenuRepositoryPort menuRepositoryPort;
|
||||||
|
private final StoreRepositoryPort storeRepositoryPort;
|
||||||
|
|
||||||
|
public MenuService(@Qualifier("menuJpaAdapter") MenuRepositoryPort menuRepositoryPort,
|
||||||
|
StoreRepositoryPort storeRepositoryPort) {
|
||||||
|
this.menuRepositoryPort = menuRepositoryPort;
|
||||||
|
this.storeRepositoryPort = storeRepositoryPort;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public List<StoreMenuListResponse> getStoreMenus(Long storeId) {
|
||||||
|
log.info("매장 메뉴 목록 조회 시작 - storeId: {}", storeId);
|
||||||
|
|
||||||
|
// 매장 존재 여부 확인
|
||||||
|
validateStoreExists(storeId);
|
||||||
|
|
||||||
|
List<Menu> menus = menuRepositoryPort.findMenusByStoreId(storeId);
|
||||||
|
|
||||||
|
return menus.stream()
|
||||||
|
.map(this::mapToStoreMenuListResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MenuDetailResponse getMenuDetail(Long menuId) {
|
||||||
|
log.info("메뉴 상세 조회 시작 - menuId: {}", menuId);
|
||||||
|
|
||||||
|
Menu menu = menuRepositoryPort.findMenuById(menuId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, "메뉴를 찾을 수 없습니다."));
|
||||||
|
|
||||||
|
return mapToMenuDetailResponse(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public MenuCreateResponse createMenu(Long ownerId, Long storeId, MenuCreateRequest request) {
|
||||||
|
log.info("메뉴 등록 시작 - ownerId: {}, storeId: {}, menuName: {}", ownerId, storeId, request.getMenuName());
|
||||||
|
|
||||||
|
// 매장 소유권 확인
|
||||||
|
validateStoreOwnership(ownerId, storeId);
|
||||||
|
|
||||||
|
// 메뉴 생성
|
||||||
|
Menu menu = Menu.builder()
|
||||||
|
.storeId(storeId)
|
||||||
|
.menuName(request.getMenuName())
|
||||||
|
.description(request.getDescription())
|
||||||
|
.price(request.getPrice())
|
||||||
|
.category(request.getCategory())
|
||||||
|
.imageUrl(request.getImageUrl())
|
||||||
|
.available(request.getIsAvailable() != null ? request.getIsAvailable() : true)
|
||||||
|
.orderCount(0)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 메뉴 유효성 검증
|
||||||
|
if (!menu.isValid()) {
|
||||||
|
throw new BusinessException(ResponseCode.INVALID_INPUT, "메뉴 정보가 올바르지 않습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu savedMenu = menuRepositoryPort.saveMenu(menu);
|
||||||
|
|
||||||
|
log.info("메뉴 등록 완료 - menuId: {}", savedMenu.getId());
|
||||||
|
|
||||||
|
return MenuCreateResponse.builder()
|
||||||
|
.menuId(savedMenu.getId())
|
||||||
|
.message("메뉴가 성공적으로 등록되었습니다.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public MenuUpdateResponse updateMenu(Long ownerId, Long menuId, MenuUpdateRequest request) {
|
||||||
|
log.info("메뉴 수정 시작 - ownerId: {}, menuId: {}", ownerId, menuId);
|
||||||
|
|
||||||
|
// 메뉴 조회 및 소유권 확인
|
||||||
|
Menu existingMenu = menuRepositoryPort.findMenuById(menuId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, "메뉴를 찾을 수 없습니다."));
|
||||||
|
|
||||||
|
validateStoreOwnership(ownerId, existingMenu.getStoreId());
|
||||||
|
|
||||||
|
// 메뉴 정보 업데이트
|
||||||
|
Menu updatedMenu = existingMenu.updateInfo(
|
||||||
|
request.getMenuName() != null ? request.getMenuName() : existingMenu.getMenuName(),
|
||||||
|
request.getDescription() != null ? request.getDescription() : existingMenu.getDescription(),
|
||||||
|
request.getPrice() != null ? request.getPrice() : existingMenu.getPrice()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (request.getCategory() != null) {
|
||||||
|
updatedMenu = updatedMenu.updateCategory(request.getCategory());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getImageUrl() != null) {
|
||||||
|
updatedMenu = updatedMenu.updateImage(request.getImageUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getIsAvailable() != null) {
|
||||||
|
updatedMenu = updatedMenu.setAvailable(request.getIsAvailable());
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu savedMenu = menuRepositoryPort.saveMenu(updatedMenu);
|
||||||
|
|
||||||
|
log.info("메뉴 수정 완료 - menuId: {}", savedMenu.getId());
|
||||||
|
|
||||||
|
return MenuUpdateResponse.builder()
|
||||||
|
.menuId(savedMenu.getId())
|
||||||
|
.message("메뉴가 성공적으로 수정되었습니다.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void deleteMenu(Long ownerId, Long menuId) {
|
||||||
|
log.info("메뉴 삭제 시작 - ownerId: {}, menuId: {}", ownerId, menuId);
|
||||||
|
|
||||||
|
// 메뉴 조회 및 소유권 확인
|
||||||
|
Menu existingMenu = menuRepositoryPort.findMenuById(menuId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, "메뉴를 찾을 수 없습니다."));
|
||||||
|
|
||||||
|
validateStoreOwnership(ownerId, existingMenu.getStoreId());
|
||||||
|
|
||||||
|
menuRepositoryPort.deleteMenu(menuId);
|
||||||
|
|
||||||
|
log.info("메뉴 삭제 완료 - menuId: {}", menuId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void updateMenuAvailability(Long ownerId, Long menuId, Boolean isAvailable) {
|
||||||
|
log.info("메뉴 가용성 변경 시작 - ownerId: {}, menuId: {}, isAvailable: {}", ownerId, menuId, isAvailable);
|
||||||
|
|
||||||
|
// 메뉴 조회 및 소유권 확인
|
||||||
|
Menu existingMenu = menuRepositoryPort.findMenuById(menuId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, "메뉴를 찾을 수 없습니다."));
|
||||||
|
|
||||||
|
validateStoreOwnership(ownerId, existingMenu.getStoreId());
|
||||||
|
|
||||||
|
Menu updatedMenu = existingMenu.setAvailable(isAvailable);
|
||||||
|
menuRepositoryPort.saveMenu(updatedMenu);
|
||||||
|
|
||||||
|
log.info("메뉴 가용성 변경 완료 - menuId: {}, isAvailable: {}", menuId, isAvailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StoreMenuListResponse> getMenusByCategory(Long storeId, String category) {
|
||||||
|
log.info("카테고리별 메뉴 조회 시작 - storeId: {}, category: {}", storeId, category);
|
||||||
|
|
||||||
|
validateStoreExists(storeId);
|
||||||
|
|
||||||
|
List<Menu> menus = menuRepositoryPort.findMenusByStoreIdAndCategory(storeId, category);
|
||||||
|
|
||||||
|
return menus.stream()
|
||||||
|
.map(this::mapToStoreMenuListResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 존재 여부 확인
|
||||||
|
*/
|
||||||
|
private void validateStoreExists(Long storeId) {
|
||||||
|
if (!storeRepositoryPort.findStoreById(storeId).isPresent()) {
|
||||||
|
throw new BusinessException(ResponseCode.STORE_NOT_FOUND, "매장을 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 소유권 확인
|
||||||
|
*/
|
||||||
|
private void validateStoreOwnership(Long ownerId, Long storeId) {
|
||||||
|
Store store = storeRepositoryPort.findStoreByIdAndOwnerId(storeId, ownerId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ResponseCode.ACCESS_DENIED, "해당 매장에 대한 권한이 없습니다."));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu를 StoreMenuListResponse로 변환
|
||||||
|
*/
|
||||||
|
private StoreMenuListResponse mapToStoreMenuListResponse(Menu menu) {
|
||||||
|
return StoreMenuListResponse.builder()
|
||||||
|
.menuId(menu.getId())
|
||||||
|
.menuName(menu.getMenuName())
|
||||||
|
.description(menu.getDescription())
|
||||||
|
.price(menu.getPrice())
|
||||||
|
.category(menu.getCategory())
|
||||||
|
.imageUrl(menu.getImageUrl())
|
||||||
|
.isAvailable(menu.getAvailable())
|
||||||
|
.orderCount(menu.getOrderCount())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu를 MenuDetailResponse로 변환
|
||||||
|
*/
|
||||||
|
private MenuDetailResponse mapToMenuDetailResponse(Menu menu) {
|
||||||
|
return MenuDetailResponse.builder()
|
||||||
|
.menuId(menu.getId())
|
||||||
|
.storeId(menu.getStoreId())
|
||||||
|
.menuName(menu.getMenuName())
|
||||||
|
.description(menu.getDescription())
|
||||||
|
.price(menu.getPrice())
|
||||||
|
.category(menu.getCategory())
|
||||||
|
.imageUrl(menu.getImageUrl())
|
||||||
|
.isAvailable(menu.getAvailable())
|
||||||
|
.orderCount(menu.getOrderCount())
|
||||||
|
.createdAt(menu.getCreatedAt())
|
||||||
|
.updatedAt(menu.getUpdatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package com.ktds.hi.store.biz.usecase.in;
|
||||||
|
|
||||||
|
import com.ktds.hi.store.infra.dto.response.StoreMenuListResponse;
|
||||||
|
import java.util.List;
|
||||||
|
import com.ktds.hi.store.infra.dto.request.MenuCreateRequest;
|
||||||
|
import com.ktds.hi.store.infra.dto.request.MenuUpdateRequest;
|
||||||
|
import com.ktds.hi.store.infra.dto.response.MenuCreateResponse;
|
||||||
|
import com.ktds.hi.store.infra.dto.response.MenuDetailResponse;
|
||||||
|
import com.ktds.hi.store.infra.dto.response.MenuUpdateResponse;
|
||||||
|
|
||||||
|
public interface MenuUseCase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 메뉴 목록 조회
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @return 메뉴 목록 응답
|
||||||
|
*/
|
||||||
|
List<StoreMenuListResponse> getStoreMenus(Long storeId);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 상세 조회
|
||||||
|
*
|
||||||
|
* @param menuId 메뉴 ID
|
||||||
|
* @return 메뉴 상세 정보
|
||||||
|
*/
|
||||||
|
MenuDetailResponse getMenuDetail(Long menuId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 등록
|
||||||
|
*
|
||||||
|
* @param ownerId 점주 ID
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @param request 메뉴 등록 요청
|
||||||
|
* @return 메뉴 등록 응답
|
||||||
|
*/
|
||||||
|
MenuCreateResponse createMenu(Long ownerId, Long storeId, MenuCreateRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 수정
|
||||||
|
*
|
||||||
|
* @param ownerId 점주 ID
|
||||||
|
* @param menuId 메뉴 ID
|
||||||
|
* @param request 메뉴 수정 요청
|
||||||
|
* @return 메뉴 수정 응답
|
||||||
|
*/
|
||||||
|
MenuUpdateResponse updateMenu(Long ownerId, Long menuId, MenuUpdateRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 삭제
|
||||||
|
*
|
||||||
|
* @param ownerId 점주 ID
|
||||||
|
* @param menuId 메뉴 ID
|
||||||
|
*/
|
||||||
|
void deleteMenu(Long ownerId, Long menuId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 가용성 변경
|
||||||
|
*
|
||||||
|
* @param ownerId 점주 ID
|
||||||
|
* @param menuId 메뉴 ID
|
||||||
|
* @param isAvailable 가용성 여부
|
||||||
|
*/
|
||||||
|
void updateMenuAvailability(Long ownerId, Long menuId, Boolean isAvailable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 카테고리별 조회
|
||||||
|
*
|
||||||
|
* @param storeId 매장 ID
|
||||||
|
* @param category 카테고리
|
||||||
|
* @return 카테고리별 메뉴 목록
|
||||||
|
*/
|
||||||
|
List<StoreMenuListResponse> getMenusByCategory(Long storeId, String category);
|
||||||
|
}
|
||||||
@ -21,6 +21,8 @@ public class Menu {
|
|||||||
private Boolean available;
|
private Boolean available;
|
||||||
private LocalDateTime createdAt; // 추가
|
private LocalDateTime createdAt; // 추가
|
||||||
private LocalDateTime updatedAt; // 추가
|
private LocalDateTime updatedAt; // 추가
|
||||||
|
private Integer orderCount;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 정보 업데이트
|
* 메뉴 정보 업데이트
|
||||||
@ -35,16 +37,93 @@ public class Menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 판매 상태 변경
|
* 메뉴 카테고리 업데이트
|
||||||
*/
|
*/
|
||||||
public void setAvailable(Boolean available) {
|
public Menu updateCategory(String category) {
|
||||||
this.available = available;
|
return Menu.builder()
|
||||||
|
.id(this.id)
|
||||||
|
.storeId(this.storeId)
|
||||||
|
.menuName(this.menuName)
|
||||||
|
.description(this.description)
|
||||||
|
.price(this.price)
|
||||||
|
.category(category)
|
||||||
|
.imageUrl(this.imageUrl)
|
||||||
|
.available(this.available)
|
||||||
|
.orderCount(this.orderCount)
|
||||||
|
.createdAt(this.createdAt)
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 이미지 업데이트
|
||||||
|
*/
|
||||||
|
public Menu updateImage(String imageUrl) {
|
||||||
|
return Menu.builder()
|
||||||
|
.id(this.id)
|
||||||
|
.storeId(this.storeId)
|
||||||
|
.menuName(this.menuName)
|
||||||
|
.description(this.description)
|
||||||
|
.price(this.price)
|
||||||
|
.category(this.category)
|
||||||
|
.imageUrl(imageUrl)
|
||||||
|
.available(this.available)
|
||||||
|
.orderCount(this.orderCount)
|
||||||
|
.createdAt(this.createdAt)
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 정보 업데이트 (불변 객체 반환)
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
.available(this.available)
|
||||||
|
.orderCount(this.orderCount)
|
||||||
|
.createdAt(this.createdAt)
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 판매 상태 변경 (불변 객체 반환)
|
||||||
|
*/
|
||||||
|
public Menu setAvailable(Boolean available) {
|
||||||
|
return Menu.builder()
|
||||||
|
.id(this.id)
|
||||||
|
.storeId(this.storeId)
|
||||||
|
.menuName(this.menuName)
|
||||||
|
.description(this.description)
|
||||||
|
.price(this.price)
|
||||||
|
.category(this.category)
|
||||||
|
.imageUrl(this.imageUrl)
|
||||||
|
.available(available)
|
||||||
|
.orderCount(this.orderCount)
|
||||||
|
.createdAt(this.createdAt)
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 메뉴 이용 가능 여부 확인
|
* 메뉴 이용 가능 여부 확인
|
||||||
*/
|
*/
|
||||||
public boolean isAvailable() {
|
public boolean isAvailable() {
|
||||||
return this.available != null && this.available;
|
return this.available != null && this.available;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴가 유효한지 확인
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
return this.menuName != null && !this.menuName.trim().isEmpty() &&
|
||||||
|
this.price != null && this.price > 0 &&
|
||||||
|
this.storeId != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
package com.ktds.hi.store.infra.controller;
|
||||||
|
|
||||||
|
import com.ktds.hi.common.dto.ApiResponse;
|
||||||
|
import com.ktds.hi.common.security.JwtTokenProvider;
|
||||||
|
import com.ktds.hi.store.biz.usecase.in.MenuUseCase;
|
||||||
|
import com.ktds.hi.store.infra.dto.request.MenuCreateRequest;
|
||||||
|
import com.ktds.hi.store.infra.dto.request.MenuUpdateRequest;
|
||||||
|
import com.ktds.hi.store.infra.dto.request.MenuAvailabilityRequest;
|
||||||
|
import com.ktds.hi.store.infra.dto.response.MenuCreateResponse;
|
||||||
|
import com.ktds.hi.store.infra.dto.response.MenuDetailResponse;
|
||||||
|
import com.ktds.hi.store.infra.dto.response.MenuUpdateResponse;
|
||||||
|
import com.ktds.hi.store.infra.dto.response.StoreMenuListResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 관리 컨트롤러
|
||||||
|
* 메뉴 관련 REST API 엔드포인트를 제공
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/menus")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Tag(name = "메뉴 관리", description = "메뉴 등록, 조회, 수정, 삭제 API")
|
||||||
|
public class MenuController {
|
||||||
|
|
||||||
|
private final MenuUseCase menuUseCase;
|
||||||
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
|
@GetMapping("/stores/{storeId}")
|
||||||
|
@Operation(summary = "매장 메뉴 목록 조회", description = "특정 매장의 모든 메뉴를 조회합니다.")
|
||||||
|
public ResponseEntity<ApiResponse<List<StoreMenuListResponse>>> getStoreMenus(
|
||||||
|
@Parameter(description = "매장 ID") @PathVariable Long storeId) {
|
||||||
|
|
||||||
|
log.info("매장 메뉴 목록 조회 요청 - storeId: {}", storeId);
|
||||||
|
|
||||||
|
List<StoreMenuListResponse> response = menuUseCase.getStoreMenus(storeId);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{menuId}")
|
||||||
|
@Operation(summary = "메뉴 상세 조회", description = "특정 메뉴의 상세 정보를 조회합니다.")
|
||||||
|
public ResponseEntity<ApiResponse<MenuDetailResponse>> getMenuDetail(
|
||||||
|
@Parameter(description = "메뉴 ID") @PathVariable Long menuId) {
|
||||||
|
|
||||||
|
log.info("메뉴 상세 조회 요청 - menuId: {}", menuId);
|
||||||
|
|
||||||
|
MenuDetailResponse response = menuUseCase.getMenuDetail(menuId);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/stores/{storeId}")
|
||||||
|
@Operation(summary = "메뉴 등록", description = "새로운 메뉴를 등록합니다.")
|
||||||
|
public ResponseEntity<ApiResponse<MenuCreateResponse>> createMenu(
|
||||||
|
@Parameter(description = "매장 ID") @PathVariable Long storeId,
|
||||||
|
@Parameter(description = "메뉴 등록 정보") @Valid @RequestBody MenuCreateRequest request,
|
||||||
|
HttpServletRequest httpRequest) {
|
||||||
|
|
||||||
|
Long ownerId = jwtTokenProvider.extractOwnerIdFromRequest(httpRequest);
|
||||||
|
log.info("메뉴 등록 요청 - ownerId: {}, storeId: {}, menuName: {}", ownerId, storeId, request.getMenuName());
|
||||||
|
|
||||||
|
MenuCreateResponse response = menuUseCase.createMenu(ownerId, storeId, request);
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{menuId}")
|
||||||
|
@Operation(summary = "메뉴 수정", description = "기존 메뉴 정보를 수정합니다.")
|
||||||
|
public ResponseEntity<ApiResponse<MenuUpdateResponse>> updateMenu(
|
||||||
|
@Parameter(description = "메뉴 ID") @PathVariable Long menuId,
|
||||||
|
@Parameter(description = "메뉴 수정 정보") @Valid @RequestBody MenuUpdateRequest request,
|
||||||
|
HttpServletRequest httpRequest) {
|
||||||
|
|
||||||
|
Long ownerId = jwtTokenProvider.extractOwnerIdFromRequest(httpRequest);
|
||||||
|
log.info("메뉴 수정 요청 - ownerId: {}, menuId: {}", ownerId, menuId);
|
||||||
|
|
||||||
|
MenuUpdateResponse response = menuUseCase.updateMenu(ownerId, menuId, request);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{menuId}")
|
||||||
|
@Operation(summary = "메뉴 삭제", description = "메뉴를 삭제합니다.")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> deleteMenu(
|
||||||
|
@Parameter(description = "메뉴 ID") @PathVariable Long menuId,
|
||||||
|
HttpServletRequest httpRequest) {
|
||||||
|
|
||||||
|
Long ownerId = jwtTokenProvider.extractOwnerIdFromRequest(httpRequest);
|
||||||
|
log.info("메뉴 삭제 요청 - ownerId: {}, menuId: {}", ownerId, menuId);
|
||||||
|
|
||||||
|
menuUseCase.deleteMenu(ownerId, menuId);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PatchMapping("/{menuId}/availability")
|
||||||
|
@Operation(summary = "메뉴 가용성 변경", description = "메뉴의 판매 가능 여부를 변경합니다.")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> updateMenuAvailability(
|
||||||
|
@Parameter(description = "메뉴 ID") @PathVariable Long menuId,
|
||||||
|
@Parameter(description = "가용성 변경 정보") @Valid @RequestBody MenuAvailabilityRequest request,
|
||||||
|
HttpServletRequest httpRequest) {
|
||||||
|
|
||||||
|
Long ownerId = jwtTokenProvider.extractOwnerIdFromRequest(httpRequest);
|
||||||
|
log.info("메뉴 가용성 변경 요청 - ownerId: {}, menuId: {}, isAvailable: {}", ownerId, menuId, request.getIsAvailable());
|
||||||
|
|
||||||
|
menuUseCase.updateMenuAvailability(ownerId, menuId, request.getIsAvailable());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/stores/{storeId}/categories/{category}")
|
||||||
|
@Operation(summary = "카테고리별 메뉴 조회", description = "특정 매장의 카테고리별 메뉴를 조회합니다.")
|
||||||
|
public ResponseEntity<ApiResponse<List<StoreMenuListResponse>>> getMenusByCategory(
|
||||||
|
@Parameter(description = "매장 ID") @PathVariable Long storeId,
|
||||||
|
@Parameter(description = "메뉴 카테고리") @PathVariable String category) {
|
||||||
|
|
||||||
|
log.info("카테고리별 메뉴 조회 요청 - storeId: {}, category: {}", storeId, category);
|
||||||
|
|
||||||
|
List<StoreMenuListResponse> response = menuUseCase.getMenusByCategory(storeId, category);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,12 @@
|
|||||||
// store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java
|
// store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java
|
||||||
package com.ktds.hi.store.infra.controller;
|
package com.ktds.hi.store.infra.controller;
|
||||||
|
|
||||||
|
import com.ktds.hi.store.biz.usecase.in.MenuUseCase;
|
||||||
import com.ktds.hi.store.biz.usecase.in.StoreUseCase;
|
import com.ktds.hi.store.biz.usecase.in.StoreUseCase;
|
||||||
import com.ktds.hi.store.infra.dto.*;
|
import com.ktds.hi.store.infra.dto.*;
|
||||||
import com.ktds.hi.common.dto.ApiResponse;
|
import com.ktds.hi.common.dto.ApiResponse;
|
||||||
import com.ktds.hi.common.security.JwtTokenProvider;
|
import com.ktds.hi.common.security.JwtTokenProvider;
|
||||||
|
import com.ktds.hi.store.infra.dto.response.StoreMenuListResponse;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@ -37,6 +39,8 @@ public class StoreController {
|
|||||||
|
|
||||||
private final StoreUseCase storeUseCase;
|
private final StoreUseCase storeUseCase;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
private final MenuUseCase menuUseCase;
|
||||||
|
|
||||||
|
|
||||||
@Operation(summary = "매장 등록", description = "새로운 매장을 등록합니다.")
|
@Operation(summary = "매장 등록", description = "새로운 매장을 등록합니다.")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@ -117,4 +121,26 @@ public class StoreController {
|
|||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(responses, "매장 검색 완료"));
|
return ResponseEntity.ok(ApiResponse.success(responses, "매장 검색 완료"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{storeId}/menus")
|
||||||
|
@Operation(summary = "매장 메뉴 목록 조회 (매장 상세에서)", description = "매장 상세 정보 조회 시 메뉴 목록을 함께 제공")
|
||||||
|
public ResponseEntity<ApiResponse<List<StoreMenuListResponse>>> getStoreMenusFromStore(
|
||||||
|
@Parameter(description = "매장 ID") @PathVariable Long storeId) {
|
||||||
|
|
||||||
|
List<StoreMenuListResponse> response = menuUseCase.getStoreMenus(storeId);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@GetMapping("/{storeId}/menus/popular")
|
||||||
|
@Operation(summary = "매장 인기 메뉴 조회", description = "매장의 인기 메뉴(주문 횟수 기준)를 조회합니다.")
|
||||||
|
public ResponseEntity<ApiResponse<List<StoreMenuListResponse>>> getPopularMenus(
|
||||||
|
@Parameter(description = "매장 ID") @PathVariable Long storeId,
|
||||||
|
@Parameter(description = "조회할 메뉴 개수") @RequestParam(defaultValue = "5") int limit) {
|
||||||
|
|
||||||
|
List<StoreMenuListResponse> response = menuUseCase.getStoreMenus(storeId);
|
||||||
|
// 인기 메뉴 조회 로직 추가 필요
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto.request;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "메뉴 가용성 변경 요청")
|
||||||
|
public class MenuAvailabilityRequest {
|
||||||
|
|
||||||
|
@NotNull(message = "가용성 여부는 필수입니다.")
|
||||||
|
@Schema(description = "판매 가능 여부", example = "true", required = true)
|
||||||
|
private Boolean isAvailable;
|
||||||
|
|
||||||
|
@Schema(description = "변경 사유", example = "재료 소진으로 인한 일시 판매 중단")
|
||||||
|
private String reason;
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto.request;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 등록 요청 DTO
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "메뉴 등록 요청")
|
||||||
|
public class MenuCreateRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "메뉴명은 필수입니다.")
|
||||||
|
@Schema(description = "메뉴명", example = "치킨버거")
|
||||||
|
private String menuName;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 설명", example = "바삭한 치킨과 신선한 야채가 들어간 버거")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@NotNull(message = "가격은 필수입니다.")
|
||||||
|
@Min(value = 0, message = "가격은 0원 이상이어야 합니다.")
|
||||||
|
@Schema(description = "가격", example = "8500")
|
||||||
|
private Integer price;
|
||||||
|
|
||||||
|
@Schema(description = "카테고리", example = "메인메뉴")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "이미지 URL", example = "/images/chicken-burger.jpg")
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
@Schema(description = "판매 가능 여부", example = "true")
|
||||||
|
private Boolean isAvailable;
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto.request;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 수정 요청 DTO
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "메뉴 수정 요청")
|
||||||
|
public class MenuUpdateRequest {
|
||||||
|
|
||||||
|
@Schema(description = "메뉴명", example = "치킨버거")
|
||||||
|
private String menuName;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 설명", example = "바삭한 치킨과 신선한 야채가 들어간 버거")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Min(value = 0, message = "가격은 0원 이상이어야 합니다.")
|
||||||
|
@Schema(description = "가격", example = "8500")
|
||||||
|
private Integer price;
|
||||||
|
|
||||||
|
@Schema(description = "카테고리", example = "메인메뉴")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "이미지 URL", example = "/images/chicken-burger.jpg")
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
@Schema(description = "판매 가능 여부", example = "true")
|
||||||
|
private Boolean isAvailable;
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto.response;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 등록 요청 DTO
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "메뉴 등록 응답")
|
||||||
|
public class MenuCreateResponse {
|
||||||
|
|
||||||
|
@Schema(description = "생성된 메뉴 ID", example = "1")
|
||||||
|
private Long menuId;
|
||||||
|
|
||||||
|
@Schema(description = "응답 메시지", example = "메뉴가 성공적으로 등록되었습니다.")
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto.response;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 상세 응답 DTO
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "메뉴 상세 정보")
|
||||||
|
public class MenuDetailResponse {
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 ID", example = "1")
|
||||||
|
private Long menuId;
|
||||||
|
|
||||||
|
@Schema(description = "매장 ID", example = "1")
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴명", example = "치킨버거")
|
||||||
|
private String menuName;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 설명", example = "바삭한 치킨과 신선한 야채가 들어간 버거")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "가격", example = "8500")
|
||||||
|
private Integer price;
|
||||||
|
|
||||||
|
@Schema(description = "카테고리", example = "메인메뉴")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "이미지 URL", example = "/images/chicken-burger.jpg")
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
@Schema(description = "판매 가능 여부", example = "true")
|
||||||
|
private Boolean isAvailable;
|
||||||
|
|
||||||
|
@Schema(description = "주문 횟수", example = "25")
|
||||||
|
private Integer orderCount;
|
||||||
|
|
||||||
|
@Schema(description = "생성일시", example = "2024-01-15T10:30:00")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "수정일시", example = "2024-01-15T10:30:00")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.ktds.hi.store.infra.dto.response;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 수정 응답 DTO
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "메뉴 수정 응답")
|
||||||
|
public class MenuUpdateResponse {
|
||||||
|
|
||||||
|
@Schema(description = "수정된 메뉴 ID", example = "1")
|
||||||
|
private Long menuId;
|
||||||
|
|
||||||
|
@Schema(description = "응답 메시지", example = "메뉴가 성공적으로 수정되었습니다.")
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
// store/src/main/java/com/ktds/hi/store/infra/dto/response/StoreMenuListResponse.java
|
||||||
|
package com.ktds.hi.store.infra.dto.response;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장 메뉴 목록 응답 DTO
|
||||||
|
* 매장의 메뉴 목록을 조회할 때 사용
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "매장 메뉴 목록 응답")
|
||||||
|
public class StoreMenuListResponse {
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 ID", example = "1")
|
||||||
|
private Long menuId;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴명", example = "치킨버거")
|
||||||
|
private String menuName;
|
||||||
|
|
||||||
|
@Schema(description = "메뉴 설명", example = "바삭한 치킨과 신선한 야채가 들어간 버거")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "가격", example = "8500")
|
||||||
|
private Integer price;
|
||||||
|
|
||||||
|
@Schema(description = "카테고리", example = "메인메뉴")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "이미지 URL", example = "/images/chicken-burger.jpg")
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
@Schema(description = "판매 가능 여부", example = "true")
|
||||||
|
private Boolean isAvailable;
|
||||||
|
|
||||||
|
@Schema(description = "주문 횟수", example = "25")
|
||||||
|
private Integer orderCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴가 판매 가능한지 확인
|
||||||
|
*/
|
||||||
|
public boolean isMenuAvailable() {
|
||||||
|
return isAvailable != null && isAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 가격을 포맷된 문자열로 반환
|
||||||
|
*/
|
||||||
|
public String getFormattedPrice() {
|
||||||
|
if (price == null) {
|
||||||
|
return "0원";
|
||||||
|
}
|
||||||
|
return String.format("%,d원", price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인기 메뉴 여부 확인 (주문 횟수 기준)
|
||||||
|
*/
|
||||||
|
public boolean isPopularMenu() {
|
||||||
|
return orderCount != null && orderCount >= 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 상태 텍스트 반환
|
||||||
|
*/
|
||||||
|
public String getStatusText() {
|
||||||
|
if (isMenuAvailable()) {
|
||||||
|
return "판매중";
|
||||||
|
} else {
|
||||||
|
return "품절";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 빌더 패턴을 위한 정적 메서드
|
||||||
|
*/
|
||||||
|
public static StoreMenuListResponse of(Long menuId, String menuName, String description,
|
||||||
|
Integer price, String category, String imageUrl,
|
||||||
|
Boolean isAvailable, Integer orderCount) {
|
||||||
|
return StoreMenuListResponse.builder()
|
||||||
|
.menuId(menuId)
|
||||||
|
.menuName(menuName)
|
||||||
|
.description(description)
|
||||||
|
.price(price)
|
||||||
|
.category(category)
|
||||||
|
.imageUrl(imageUrl)
|
||||||
|
.isAvailable(isAvailable)
|
||||||
|
.orderCount(orderCount)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
package com.ktds.hi.store.infra.gateway;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 JPA 어댑터
|
||||||
|
* 메뉴 리포지토리 포트의 JPA 구현체
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MenuJpaAdapter implements MenuRepositoryPort {
|
||||||
|
|
||||||
|
private final MenuJpaRepository menuJpaRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Menu> findMenusByStoreId(Long storeId) {
|
||||||
|
List<MenuEntity> entities = menuJpaRepository.findByStoreId(storeId);
|
||||||
|
return entities.stream()
|
||||||
|
.map(MenuEntity::toDomain)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Menu> findMenuById(Long menuId) {
|
||||||
|
return menuJpaRepository.findById(menuId)
|
||||||
|
.map(MenuEntity::toDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Menu saveMenu(Menu menu) {
|
||||||
|
MenuEntity entity = MenuEntity.fromDomain(menu);
|
||||||
|
MenuEntity savedEntity = menuJpaRepository.save(entity);
|
||||||
|
return savedEntity.toDomain();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteMenu(Long menuId) {
|
||||||
|
menuJpaRepository.deleteById(menuId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Menu> findAvailableMenusByStoreId(Long storeId) {
|
||||||
|
List<MenuEntity> entities = menuJpaRepository.findByStoreIdAndIsAvailableTrue(storeId);
|
||||||
|
return entities.stream()
|
||||||
|
.map(MenuEntity::toDomain)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Menu> findMenusByStoreIdAndCategory(Long storeId, String category) {
|
||||||
|
List<MenuEntity> entities = menuJpaRepository.findByStoreIdAndCategoryAndIsAvailableTrue(storeId, category);
|
||||||
|
return entities.stream()
|
||||||
|
.map(MenuEntity::toDomain)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Menu> saveMenus(List<Menu> menus) {
|
||||||
|
List<MenuEntity> entities = menus.stream()
|
||||||
|
.map(MenuEntity::fromDomain)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<MenuEntity> savedEntities = menuJpaRepository.saveAll(entities);
|
||||||
|
|
||||||
|
return savedEntities.stream()
|
||||||
|
.map(MenuEntity::toDomain)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteMenusByStoreId(Long storeId) {
|
||||||
|
menuJpaRepository.deleteByStoreId(storeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,61 +1,106 @@
|
|||||||
package com.ktds.hi.store.infra.gateway.entity;
|
package com.ktds.hi.store.infra.gateway.entity;
|
||||||
|
|
||||||
|
import com.ktds.hi.store.domain.Menu;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import org.springframework.data.annotation.CreatedDate;
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
import org.springframework.data.annotation.LastModifiedDate;
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 엔티티 클래스
|
* 메뉴 엔티티
|
||||||
* 데이터베이스 menus 테이블과 매핑되는 JPA 엔티티
|
* 메뉴 정보를 데이터베이스에 저장하기 위한 JPA 엔티티
|
||||||
|
*
|
||||||
|
* @author 하이오더 개발팀
|
||||||
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "menus")
|
@Table(name = "menus")
|
||||||
@Getter
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@EntityListeners(AuditingEntityListener.class)
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
public class MenuEntity {
|
public class MenuEntity {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "menu_id")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Column(name = "store_id", nullable = false)
|
@Column(name = "store_id", nullable = false)
|
||||||
private Long storeId;
|
private Long storeId;
|
||||||
|
|
||||||
@Column(name = "menu_name", nullable = false, length = 100)
|
@Column(name = "menu_name", nullable = false, length = 100)
|
||||||
private String menuName;
|
private String menuName;
|
||||||
|
|
||||||
@Column(length = 500)
|
@Column(name = "description", columnDefinition = "TEXT")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(name = "price", nullable = false)
|
||||||
private Integer price;
|
private Integer price;
|
||||||
|
|
||||||
@Column(length = 50)
|
@Column(name = "category", length = 50)
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
@Column(name = "image_url", length = 500)
|
@Column(name = "image_url", length = 500)
|
||||||
private String imageUrl;
|
private String imageUrl;
|
||||||
|
|
||||||
@Column(name = "is_available")
|
@Column(name = "is_available", nullable = false)
|
||||||
|
private Boolean isAvailable;
|
||||||
|
|
||||||
|
@Column(name = "order_count", nullable = false)
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private Boolean isAvailable = true;
|
private Integer orderCount = 0;
|
||||||
|
|
||||||
@CreatedDate
|
@CreatedDate
|
||||||
@Column(name = "created_at", updatable = false)
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
@LastModifiedDate
|
@LastModifiedDate
|
||||||
@Column(name = "updated_at")
|
@Column(name = "updated_at", nullable = false)
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 엔티티를 도메인 객체로 변환
|
||||||
|
*/
|
||||||
|
public Menu toDomain() {
|
||||||
|
return Menu.builder()
|
||||||
|
.id(this.id)
|
||||||
|
.storeId(this.storeId)
|
||||||
|
.menuName(this.menuName)
|
||||||
|
.description(this.description)
|
||||||
|
.price(this.price)
|
||||||
|
.category(this.category)
|
||||||
|
.imageUrl(this.imageUrl)
|
||||||
|
.available(this.isAvailable)
|
||||||
|
.orderCount(this.orderCount)
|
||||||
|
.createdAt(this.createdAt)
|
||||||
|
.updatedAt(this.updatedAt)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도메인 객체를 엔티티로 변환
|
||||||
|
*/
|
||||||
|
public static MenuEntity fromDomain(Menu menu) {
|
||||||
|
return MenuEntity.builder()
|
||||||
|
.id(menu.getId())
|
||||||
|
.storeId(menu.getStoreId())
|
||||||
|
.menuName(menu.getMenuName())
|
||||||
|
.description(menu.getDescription())
|
||||||
|
.price(menu.getPrice())
|
||||||
|
.category(menu.getCategory())
|
||||||
|
.imageUrl(menu.getImageUrl())
|
||||||
|
.isAvailable(menu.getAvailable())
|
||||||
|
.orderCount(menu.getOrderCount())
|
||||||
|
.createdAt(menu.getCreatedAt())
|
||||||
|
.updatedAt(menu.getUpdatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@ package com.ktds.hi.store.infra.gateway.repository;
|
|||||||
|
|
||||||
import com.ktds.hi.store.infra.gateway.entity.MenuEntity;
|
import com.ktds.hi.store.infra.gateway.entity.MenuEntity;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -12,24 +14,47 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Repository
|
@Repository
|
||||||
public interface MenuJpaRepository extends JpaRepository<MenuEntity, Long> {
|
public interface MenuJpaRepository extends JpaRepository<MenuEntity, Long> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 ID로 이용 가능한 메뉴 목록 조회
|
* 매장 ID로 이용 가능한 메뉴 목록 조회
|
||||||
*/
|
*/
|
||||||
List<MenuEntity> findByStoreIdAndIsAvailableTrue(Long storeId);
|
List<MenuEntity> findByStoreIdAndIsAvailableTrue(Long storeId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 ID로 모든 메뉴 목록 조회
|
* 매장 ID로 모든 메뉴 목록 조회
|
||||||
*/
|
*/
|
||||||
List<MenuEntity> findByStoreId(Long storeId);
|
List<MenuEntity> findByStoreId(Long storeId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 ID로 메뉴 삭제
|
* 매장 ID로 메뉴 삭제
|
||||||
*/
|
*/
|
||||||
void deleteByStoreId(Long storeId);
|
void deleteByStoreId(Long storeId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 카테고리별 메뉴 조회
|
* 카테고리별 이용 가능한 메뉴 조회
|
||||||
*/
|
*/
|
||||||
List<MenuEntity> findByStoreIdAndCategoryAndIsAvailableTrue(Long storeId, String category);
|
List<MenuEntity> findByStoreIdAndCategoryAndIsAvailableTrue(Long storeId, String category);
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 매장 ID와 카테고리로 모든 메뉴 조회 (가용성 무관)
|
||||||
|
*/
|
||||||
|
List<MenuEntity> findByStoreIdAndCategory(Long storeId, String category);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장별 메뉴 개수 조회
|
||||||
|
*/
|
||||||
|
@Query("SELECT COUNT(m) FROM MenuEntity m WHERE m.storeId = :storeId")
|
||||||
|
Long countByStoreId(@Param("storeId") Long storeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매장별 이용 가능한 메뉴 개수 조회
|
||||||
|
*/
|
||||||
|
@Query("SELECT COUNT(m) FROM MenuEntity m WHERE m.storeId = :storeId AND m.isAvailable = true")
|
||||||
|
Long countByStoreIdAndIsAvailableTrue(@Param("storeId") Long storeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인기 메뉴 조회 (주문 횟수 기준 상위 N개)
|
||||||
|
*/
|
||||||
|
@Query("SELECT m FROM MenuEntity m WHERE m.storeId = :storeId AND m.isAvailable = true ORDER BY m.orderCount DESC")
|
||||||
|
List<MenuEntity> findPopularMenusByStoreId(@Param("storeId") Long storeId);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user