저번에 말했던대로 오늘은 매장 음식메뉴를 구현해보도록 하겠습니다
간단하게 생각하면 DB에서 음식의 메뉴를 가져와 화면에 뿌려주기만 하면 되지만
몇가지 생각해야할 부분이 있습니다
예를들어 짜장면이라는 메뉴를 클릭시에는 곱빼기로 변경하거나 사이드메뉴를
추가할수 있는 추가옵션의 기능이 필요합니다
음식 메뉴의 경우 사용자가 특정 매장의 상세페이지에 접근했을때 매장의 정보와
함께 한번에 보여줘야 하는 필수 데이터이지만 추가옵션의 경우는 해당 메뉴를
클릭하기전까지는 필요하지 않습니다 따라서 추가옵션의 경우는 restApi와 AJAX통신을
사용하여 동적으로 추가해줘야 합니다 일단 이를 구현하기전에 테이블을 생성해줍시다
CREATE table DL_FOOD (
id number primary key,
store_id number NOT NULL,
food_name varchar2(100) NOT NULL,
food_price number NOT NULL,
food_dec varchar2(200),
food_img varchar2(200),
food_thumb varchar2(200)
);
CREATE SEQUENCE FOOD_ID_SEQ
INCREMENT BY 1
START WITH 1
MINVALUE 1
MAXVALUE 99999999999
NOCYCLE
NOCACHE
NOORDER;
ALTER TABLE DL_FOOD
ADD CONSTRAINT FOOD
FOREIGN KEY (STORE_ID)
REFERENCES DL_STORE(ID)
on delete cascade;
-- 음식 추가옵션
CREATE TABLE DL_FOOD_OPTION (
id number PRIMARY KEY,
food_id number not null,
option_name varchar2(100) not null,
option_price number not null
);
CREATE SEQUENCE OPTION_ID_SEQ
INCREMENT BY 1
START WITH 1
MINVALUE 1
MAXVALUE 99999999999
NOCYCLE
NOCACHE
NOORDER;
ALTER TABLE DL_FOOD_OPTION
ADD CONSTRAINT FOOD_OPTION
FOREIGN KEY (FOOD_ID)
REFERENCES DL_FOOD(ID)
on delete cascade;
하나의 매장은 여러개의 메뉴를 가지므로 1:N의 관계입니다
하나의 메뉴는 여러개의 추가옵션을 가지므로 1:N의 관계입니다
따라서 매장 메뉴를 가져올때 해당 매장에 모든 메뉴를 테이블에서 조회해야하므로
음식테이블은 매장ID를 외래키로 가지고 있어야 합니다
하나의 메뉴는 사용자가 해당 메뉴를 클릭했을시 모든옵션을 테이블에서 조회해야하므로
옵션테이블은 메뉴ID를 외럐키로 가지고 있어야 합니다
우리는 기존에 storeDetailDto를 model에 심어 화면에 데이터를 뿌려주고 있기 때문에
Controller를 수정할 필요가 없습니다. storeDeatilDto는 하나의 매장에 관련된
모든 정보를 전달할 Dto이기때문에 FoodDto를 만들고 필드값으로 넣어줘야합니다
@Data
public class FoodDto {
private long id;
private long storeId;
private String foodName;
private String foodPrice;
private String foodDec;
private String foodImg;
private String foodThumb;
}
//메뉴 목록
private List<FoodDto> foodList;
//기존
@Transactional
public StoreDetailDto storeDetail(long storeId) {
StoreDto storeDto = storeMapper.storeDetail(storeId);
return new StoreDetailDto(storeDto);
}
//수정
@Transactional
public StoreDetailDto storeDetail(long storeId) {
StoreDto storeDto = storeMapper.storeDetail(storeId);
List<FoodDto> foodList = storeMapper.foodList(storeId);
return new StoreDetailDto(storeDto, foodList);
}
현재 service를 보면 아시겠지만 매장의 상세정보를 가져오기 위해 Store테이블을 1번
메뉴를 가져오기 위해 Food테이블을 1번 총 2번의 Select쿼리문이 날아가게 됩니다
여기에 Food의 추가옵션까지 넣으면 총 3번의 쿼리문이 날아가게 되는데 이는 좋은방법이
아닙니다. StoreDeatilDto안에는 StoreDto, FoodDto가 모두 포함되어 있고
mybatis는 join을 통해 여러 테이블을 합친 결과를 각각의 Dto에 맞게 한번에 넣어주는
기능을 제공하고 있습니다. 하지만 아직 매장 상세화면에 대한 모든 기능을 구현하지
못했으므로 추후에 수정하도록 하겠습니다
public List<FoodDto> foodList(long storeId);
<select id="foodList" resultType="com.han.delivery.dto.FoodDto">
select * from dl_food where store_id = #{storeId}
</select>
다음으로 model에 심어져 넘어온 메뉴의 목록을 화면에 뿌려줘야 합니다
기존 storeDetail.jsp에 메뉴의 목록을 뿌려줄 공간을 추가해주도록 합시다
<!-- 메뉴 탭 -->
<ul class="menu">
//추가
<c:forEach items="${store.foodList }" var="foodList" >
<li>
<c:if test="${adminPage && SPRING_SECURITY_CONTEXT.authentication.principal.user.role == 'ROLE_ADMIN' }">
<label class="menu_delete_label">
<i class="fas fa-check-square" ></i>
<input type="checkbox" class="menu_delete_checkbox" name="deleteNumber" value="${foodList.id }">
</label>
</c:if>
<div class="menu_box">
<div>
<h2>${foodList.foodName } </h2>
<fm:formatNumber value="${foodList.foodPrice }" pattern="###,###" />원
<input type="hidden" value="${foodList.storeId }" name="storeId" >
<input type="hidden" value="${foodList.id }" name="foodId" class="food_id" >
<input type="hidden" value="${foodList.foodName }" name="foodName" class="food_name" >
<input type="hidden" value="${foodList.foodPrice }" name="foodPrice" class="food_price" >
<input type="hidden" value="${foodList.foodDec }" name="foodDec" class="food_dec" >
<input type="hidden" value="${foodList.foodImg }" name="foodImg" class="food_img" >
<input type="hidden" value="${foodList.foodThumb }" name="foodThumb" class="food_thumb" >
</div>
<div><img src="${foodList.foodImg }" alt="이미지"></div>
</div>
</li>
</c:forEach>
메뉴화면 구현은 여기까지가 끝이지만 우리가 기존에 카카오지도Api를 사용하여
매장의 위치를 지도에 나타내도록 했었는데 메뉴탭 -> 정보탭 으로 넘어갈시
지도가 깨지는 현상이 발생합니다 따라서 스크립트 파일에 다음의 코드를 추가해줍시다
$(document).ready(function() {
// css로 display none시 카카오 맵 깨짐
$("main ul.info").hide();
// 탭 눌렀을때 색변경 콘텐츠 변경
$("ul.tab > li").click(function() {
const index = $(this).index() + 1;
$("ul.tab > li").removeClass("select");
$(this).addClass("select");
$("main ul").eq(1).hide();
$("main ul").eq(2).hide();
$("main ul").eq(3).hide();
$("main ul").eq(index).show();
const offset = $(".offset").offset();
const scrollPosition = $(document).scrollTop();
if (offset["top"] < scrollPosition) {
$("html").animate({ scrollTop: offset.top }, 100);
}
});
});
이제 메뉴에 대한 추가옵션을 추가해줘야 합니다
포스팅 제일 처음에 말했듯이 추가옵션은 사용자가 메뉴를 클릭했을때
테이블을 조회하여 동적으로 추가해줘야 합니다
우리는 RestApi와 AJAX통신을 사용할것이므로 최상위 패키지안에 api패키지를
생성하여 그안에 StoreApiController 클래스를 추가해줍시다
@RestController
public class StoreApiController {
@Autowired
StoreService storeService;
// 메뉴 클릭시 음식 추가옵션 가져요기
@GetMapping("/api/food/{foodId}/option")
public List<FoodOptionDto> menuDetail(@PathVariable long foodId) {
List<FoodOptionDto> foodOption = storeService.foodOption(foodId);
return foodOption;
}
}
@Data
public class FoodOptionDto {
private long id;
private long foodId;
private String optionName;
private long optionPrice;
}
@Transactional
public List<FoodOptionDto> foodOption(long foodId) {
return storeMapper.foodOption(foodId);
}
public List<FoodOptionDto> foodOption(long foodId);
<select id="foodOption" resultType="com.han.delivery.dto.FoodOptionDto">
select * from dl_food_option where food_id = #{foodId}
</select>
이제 서버에서 넘겨받은 데이터를 가지고 화면에서 데이터를 동적으로 추가해줘야합니다
사용자가 메뉴를 클릭했을시 modal창이 뜨며 해당 메뉴에 추가옵션이 있을시에는
추가옵션을 나타내고 추가옵션이 없을경우에는 추가옵션을 나타내는 화면을 숨김처리합니다
서버로부터 넘겨받은 Option List의 length가 0이면 추가옵션이 없으므로 숨김처리
length가 0이 아니면 추가옵션이 존재하므로 동적으로 태그를 추가해줍니다
//메뉴 클릭시 modal 열기
function openModal(modal) {
const size = window.innerWidth;
if (size > 767) {
modal.css("transition", "0s").css("top", "0%");
console.log("pc");
} else {
modal.css("transition", "0.2s").css("top", "0%");
console.log("mobile");
}
$("#modal_bg").show();
$("body").css("overflow", "hidden");
$("body").css("overflow-y", "hidden");
$(".closeA").click(function() {
closeModal();
});
$("#modal_bg").click(function() {
closeModal();
});
$(".closeB").click(function() {
closeModal();
});
}
// modal 닫기
function closeModal() {
$("#modal_bg").hide();
$(".modal").css("top", "100%");
$(".modal_box").scrollTop(0);
$("body").css("overflow", "visible");
$(".modal input[type='checkBox']").prop("checked", false);
$("#amount").val(1);
};
// 메뉴 클릭시 모달창
$(".menu > li .menu_box").click(function() {
const foodId = $(this).find(".food_id").val();
$.ajax({
url: `/api/food/${foodId}/option`,
type: "get",
})
.done(function(result){
console.log(result);
foodModalHtml(result);
if(result.length == 0) {
$("#option").hide();
} else {
$("#option").show();
}
})
.fail(function(){
swal("에러가 발생했습니다");
food.hide();
}) // ajax
const addCartModal = $(".food_modal");
const foodImg = $(this).find(".food_img").val();
const foodName = $(this).find(".food_name").val();
let foodPrice = Number($(this).find(".food_price").val());
const foodDec = $(this).find(".food_dec").val();
const amount = $("#amount").val();
const totalPrice = amount * foodPrice;
$(".menu_img").attr("src", foodImg);
$(".menu_name").text(foodName);
$(".menu_dec").text(foodDec);
$(".price").text(Number(foodPrice).toLocaleString() + '원');
$(".total_price").text(Number(totalPrice).toLocaleString() + '원');
$(".add_cart_food_name").val(foodName);
$(".add_cart_food_price").val(foodPrice);
$(".add_cart_food_id").val(foodId);
openModal(addCartModal);
// 수량 증가 감소
$(".minus").off().click(function() {
if (1 < Number($("#amount").val())) {
$("#amount").val(Number($("#amount").val()) - 1);
}
const amount = Number($("#amount").val());
const totalPrice = amount * foodPrice;
$(".total_price").text(Number(totalPrice).toLocaleString() + '원');
})
$(".plus").off().click(function() {
$("#amount").val(Number($("#amount").val()) + 1);
const amount = $("#amount").val();
const totalPrice = amount * foodPrice;
$(".total_price").text(Number(totalPrice).toLocaleString() + '원');
})
// 옵션 체크박스 변경
$(document).off().on("click" , ".option_box i", function(){
const optionPrice = Number($(this).siblings(".option_price").val());
if($(this).siblings(".menu_option").is(":checked")) {
$(this).siblings(".menu_option").prop("checked" ,false);
$(this).css("color" , "#ddd");
foodPrice -= optionPrice;
} else {
$(this).siblings(".menu_option").prop("checked" , true);
$(this).css("color" , "#30DAD9");
foodPrice += optionPrice;
}
const amount = Number($("#amount").val());
const totalPrice = amount * foodPrice;
$(".total_price").text(Number(totalPrice).toLocaleString() + '원');
})
}) // 메뉴 클릭
//추가옵션 동적추가
function foodModalHtml(result) {
let html = "";
for(var i=0;i<result.length;i++) {
html += `<li>
<div class="option_box">
<span>
<i class="fas fa-check-square"></i>
<input type="checkbox" class="menu_option" name="option" value="${result[i]["optionName"] }"> ${result[i]["optionName"] }
<input type="hidden" class="option_price" value="${result[i]["optionPrice"] }">
<input type="hidden" class="option_id" value="${result[i]["id"] }">
</span>
<span>${result[i]["optionPrice"] } 원</span>
</div>
</li>`;
}
$("#option ul").html(html);
}
사용자가 옵션을 선택하거나 수량을 변경하면 그에 따라 총 결제 금액도 달라져야합니다
데이터 변조의 위험이 있기에 결제금액과 관련된 부분은 서버에서 처리해야 하지만
사용자가 체크박스를 선택/해제하거나 수량을 증가/감소 시킬때마다 서버와 통신하는건
좋은방법이 아니므로 최종결제시에만 서버에서 확인하도록 할겁니다
아직 장바구니 기능은 구현이 되지 않아 장바구니에 담기 버튼을 클릭해도 아무 이벤트도
일어나지 않으므로 다음시간에 장바구니 기능을 구현하면서 수정해보도록 하겠습니다
감사합니다. 잘 배우고 갑니다.