TIL - 17주차 코드

 

1. 타임리프 사용법 : 230320


  • th:href@{url 경로}로 설정하여 이용하는데 “이동 경로”를 의미한다.
  • th:href=”@{detail(id=${menu.id})}”
  • “소괄호”가 쿼리스트링인 “?”를 의미한다.


  • 추가 내용 :
    • th:text는 문자열 생성
    • th:each는 반복문
    • th:if는 if 조건문


  • 실습 코드 :
    • MenuController.java
	// @RequestMapping에서 detail을 "/detail" 이나 "detail"를 사용하면 된다! 상관없더라
	@RequestMapping("detail")
	public String detail(long id, Model model) {
		// @requestParam은 인자의 변수명이 달라지면 사용하고 이름이 같다면, int로 인자를 바로 받는다. 
		
		
		// 집계가 필요하면, MenuView가 필요하다. 하지만, entity를 따로 만들 수도 있다.
		Menu menu = service.getById(id);
		
		// service 계층에서 받아와서 view 단으로 던져줘서 view 단에서 출력 한다.
		model.addAttribute("menu", menu);
		
		return "menu/detail";
	}
  • MenuService.java
package kr.co.rland.web.service;

import java.util.List;

import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.entity.MenuView;

public interface MenuService {
	
	// 서비스 계층에서는 사용자 요청을 이름 그대로 그대로 만들어라!!
	void pointUp();
	
	List<Menu> getList();	
	List<Menu> getList(int page);
	List<Menu> getList(int page, String query);
	List<Menu> getList(int page, int categoryId);
	List<Menu> getList(int page, int categoryId, String query);
	
	List<MenuView> getViewList();
	List<MenuView> getViewList(int page);
	List<MenuView> getViewList(int page, String query);
	List<MenuView> getViewList(int page, int categoryId);
	List<MenuView> getViewList(int page, int categoryId, String query);

	Menu getById(long id);
	
}

  • DefaultMenuService.java

@Override
public Menu getById(long id) {
	Menu menu = repository.findById(id);
	
	return menu;
}
	


  • 테스트 코드
    • DefaultMenuServiceTest.java
package kr.co.rland.web.service;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.mybatis.spring.boot.test.autoconfigure.AutoConfigureMybatis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.entity.MenuView;

@SpringBootTest
@AutoConfigureMybatis
class DefaultMenuServiceTest {

	@Autowired
	private MenuService service;
	
	// @Test
	void test() {
		service.pointUp();
		List<MenuView> list = service.getViewList(1, 1);
		System.out.println(list);
		
		
		System.out.println("작업 완료");
	}
	
	
	@Test
	void testgetbyId() {
		Menu menu = service.getById(737L);
		System.out.println(menu);
		
	}
	

}



2) 타임리프에 문자열 템플릿


  • 실습코드 :
    • datail.html
<article>
	<header>
		<h1 class="text-title2" th:text="${menu.name}">딸기청</h1>
		<span class="text-normal" th:text="${menu.price}">4,500원</span>
	</header>
	<p class="text-normal" th:text="${menu.description} + ' 원'">신선한 과일로 만들고 알랜드만의 비법으로 직접 만들어서
		다른 곳에서는 느낄 수 없는 상큼함과 새콤함을 자랑합니다.
	</p>
</article>


  • Menu.java
    • description 변수 추가
package kr.co.rland.web.entity;

import java.util.Date;

public class Menu {
	private Long id;
	private String name;
	private Integer price;
	private String img;
	private Date regDate;
	private Integer categoryId;
	private Long regMemberId;
	private String description;

	public Menu() {
		// TODO Auto-generated constructor stub
	}

	public Menu(Long id, String name, Integer price, String img, Date regDate, Integer categoryId, Long regMemberId) {
		super();
		this.id = id;
		this.name = name;
		this.price = price;
		this.img = img;
		this.regDate = regDate;
		this.categoryId = categoryId;
		this.regMemberId = regMemberId;
	}

	public Menu(String name, Integer price, String img, Integer categoryId, Long regMemberId) {
		super();
		this.name = name;
		this.price = price;
		this.img = img;
		this.categoryId = categoryId;
		this.regMemberId = regMemberId;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getPrice() {
		return price;
	}

	public void setPrice(Integer price) {
		this.price = price;
	}

	public String getImg() {
		return img;
	}

	public void setImg(String img) {
		this.img = img;
	}

	public Date getRegDate() {
		return regDate;
	}

	public void setRegDate(Date regDate) {
		this.regDate = regDate;
	}

	public Integer getCategoryId() {
		return categoryId;
	}

	public void setCategoryId(Integer categoryId) {
		this.categoryId = categoryId;
	}

	public Long getRegMemberId() {
		return regMemberId;
	}

	public void setRegMemberId(Long regMemberId) {
		this.regMemberId = regMemberId;
	}

	
	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	@Override
	public String toString() {
		return "Menu [id=" + id + ", name=" + name + ", price=" + price + ", img=" + img + ", regDate=" + regDate
				+ ", categoryId=" + categoryId + ", regMemberId=" + regMemberId + "]";
	}

}


  • 문제점 1 :
    • 위의 실습 코드를 이용하면, style 태그가 적용되지 않고 태그가 그대로 노출된다.
    • 그 이유는 “html 이스케이프”로 인코딩되었기 때문에 이를 인코딩 되지 않고 나오게 해야한다.(<&lt로 인코딩)


  • 해결 방법 1 :
    • 태그의 속성에 th:utext만 써주면 된다!(utext의 u는 unencoding의 의미를 가지고 있다.)

<p class="text-normal" th:utext="${menu.description} + ' 원'">
	신선한 과일로 만들고 알랜드만의 비법으로 직접 만들어서 다른 곳에서는 느낄 수 없는 상큼함과 새콤함을 자랑합니다.
</p>




3) 타임리프로 이미지 추가


  • th:src 속성 이용


  • 실습 코드 1
<header>
	<h1 class="d-none">수제청</h1>
	<div class="img-div">
		<img alt="" th:src="'/image/menu/' + ${menu.img}" src="https://images.unsplash.com/photo-1515442261605-65987783cb6a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80">
	</div>
</header>


  • 실습 코드 2
    • url에 맞게 데이터 바꿔주기
<header>
	<h1 class="d-none">수제청</h1>
	<div class="img-div">
		<!-- <img alt="" th:src="'/image/menu/' + ${menu.img}" src="https://images.unsplash.com/photo-1515442261605-65987783cb6a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80"> -->
		<img alt="" th:src="@{/image/menu/{img}(img=${menu.img})}" src="https://images.unsplash.com/photo-1515442261605-65987783cb6a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80">
	
	</div>
</header>
<div class="menu-img-box">
    <a href="detail" th:href="@{detail(id=${menu.id})}"><img class="menu-img" th:src="@{/image/menu/{img}(img=${menu.img})}" src="/image/menu/12.png"></a>
</div>    



4) url 중간에 id 넣는 방법

  • <a th:href="@{/menus/{id}/edit(id=${menu.id})}">test</a> 이용!!
<header>
	<h1 class="d-none">수제청</h1>
	<div class="img-div">
		<!-- <img alt="" th:src="'/image/menu/' + ${menu.img}" src="https://images.unsplash.com/photo-1515442261605-65987783cb6a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80"> -->
		<img alt="" th:src="@{/image/menu/{img}(img=${menu.img})}" src="https://images.unsplash.com/photo-1515442261605-65987783cb6a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80">
	
		<!-- url의 중간에 id를 넣는 방법  -->
		<a th:href="@{/menus/{id}/edit(id=${menu.id})}">test</a>
	</div>
</header>



5) lombok(롬북)

  • @Data는 여러가지를 포함하는 어노테이션이다.(@Getter, @Setter, @NoArgsConstructor, @AllArgsConsructor, @ToString를 포함한다.)
  • @Builder는 중요하다.
package kr.co.rland.web.entity;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class Category {
	private int id;
	private String name;
	
	
}




6) Category 테이블 추가

  • lombok 이용!


  • 에러 주의!! :
    • mybatis 사용시, 속성에서 namespace, resultType, parameterType 주의!!
    • namespace를 제대로 설정 안 하면, sqlSession에 빈이 없다고 에러가 발생한다!!


  • Category.java
package kr.co.rland.web.entity;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class Category {
	private int id;
	private String name;
}


  • CategoryRepository.java
package kr.co.rland.web.repository;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import kr.co.rland.web.entity.Category;
import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.entity.MenuView;


@Mapper
public interface CategoryRepository {
	

	List<Category> findAll();
	
	Category findById(int id);
	
	// 몇 개 업데이트 되었는지 확인을 위해
	// int형으로 반환한다.
	int insert(Category category);
	
	int update(Category category);
	
	int delete(int id);
	
}


  • CategoryRepositoryMapper.java
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.co.rland.web.repository.CategoryRepository">
	
	<!-- mybatis 사용시, namespace, resultType, parameterType 주의  -->
	<!-- namespace를 제대로 설정안하면, sqlSession에 빈이 없다고 에러가 발생한다. -->
	<select id="findAll" resultType="Category">
		select * 
		from category
	</select>
	
	<insert id="insert" parameterType="Category">
		insert into category(name)
		values(${name})
	</insert>
	
	<update id="update" parameterType="Category">
		update category
		<trim prefix="SET" suffixOverrides=",">
			<if test="name != null">name=#{name}</if>
		</trim>
		where id=#{id}
	</update>
	 
 	<delete id="delete" parameterType="Category">
 		delete from category where id=${id}
	</delete> 
</mapper>


  • 테이블을 하나 추가해서 test 코드 :
package kr.co.rland.web.repository;

import static org.junit.jupiter.api.Assertions.fail;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;

import kr.co.rland.web.entity.Category;
import kr.co.rland.web.entity.Menu;

//@AutoConfigureMybatis
//@SpringBootTest
@AutoConfigureTestDatabase(replace=Replace.NONE)
@MybatisTest
class CategoryRepositoryTest {
	
	// @Autowired만 쓰면 안 된다. IOC에 담아줘야 테스트가 돌아간다.. 그래서 @MybatisTest 이용
	@Autowired
	private CategoryRepository repository;
	
	
	// Entity에 ToString가 없으면 list를 주소값으로 반환한다.
	// 우리는 DB의 데이터값이 필요하다.
	@Test
	void testFindAll() {
		List<Category> list = repository.findAll();
		
		System.out.println(list);
	}


}


  • 정리 :
    • Entity에 ToString가 없으면 list를 주소값으로 반환한다.
    • 우리는 DB의 데이터값이 필요하다. 그래서 Entity에 생성자와 toString 만들어 줄 것



2. Category 서비스, 쿼리스트링, 검색기능 : 230321

1) Category 서비스 완성

a-1. 서비스 설계

  • MenuController.java
package kr.co.rland.web.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import kr.co.rland.web.entity.Category;
import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.entity.MenuView;
import kr.co.rland.web.service.CategoryService;
import kr.co.rland.web.service.MenuService;

// FrontController(POJO 클래스)를 만드는 방법! 
// 클래스는 보통 폴더명이 된다.(기능별로 넣자!)
@Controller
@RequestMapping("/menu")
public class MenuController {
	
	@Autowired
	private MenuService service;
	
	@Autowired
	private CategoryService categoryservice;
	
	// 함수 이름은 보통 url이 된다.
	@RequestMapping("list")
	public String list(Model model) {
		
		List<Category> categoryList = categoryservice.getList();
		
		model.addAttribute("categoryList", categoryList);
		
		List<MenuView> list = service.getViewList();
		
		model.addAttribute("list", list);
		
		// model.addAttribute("data","hello");
//		service.getList();			// 1, query:"",
//		service.getList(1);
//		service.getList(1, "아");
//		service.getList(1, 1 /* category */ );		/* category 있는 애들만!!*/
//		service.getList(1, 1 /* category */, "아");
		
		
		
		// 상대경로이면 타임리프가 알아서 찾아준다. 
		return "menu/list";
	}
	
	// @RequestMapping에서 detail을 "/detail" 이나 "detail"를 사용하면 된다! 상관없더라
	@RequestMapping("detail")
	public String detail(long id, Model model) {
		// @requestParam은 인자의 변수명이 달라지면 사용하고 이름이 같다면, int로 인자를 바로 받는다. 
		
		
		// 집계가 필요하면, MenuView가 필요하다. 하지만, entity를 따로 만들 수도 있다.
		Menu menu = service.getById(id);
		
		// service 계층에서 받아와서 view 단으로 던져줘서 view 단에서 출력 한다.
		model.addAttribute("menu", menu);
		
		return "menu/detail";
	}

}


  • MenuService.java
package kr.co.rland.web.service;

import java.util.List;

import kr.co.rland.web.entity.Category;

public interface CategoryService {

	List<Category> getList();

}


  • DefaultMenuService.java

package kr.co.rland.web.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import kr.co.rland.web.entity.Category;
import kr.co.rland.web.repository.CategoryRepository;

@Service
public class DefaultCategoryService implements CategoryService {

	@Autowired
	private CategoryRepository categoryrepository;
	
	@Override
	public List<Category> getList() {
		
		// List<Category> categoryList = categoryrepository.findAll(); 
		// return categoryList;
		
		return categoryrepository.findAll();
	}

}



a-2. DefaultCategoryService의 Test 코드


package kr.co.rland.web.service;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.mybatis.spring.boot.test.autoconfigure.AutoConfigureMybatis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import kr.co.rland.web.entity.Category;


@SpringBootTest
@AutoConfigureMybatis
class DefaultCategoryServiceTest {
	
	@Autowired
	private CategoryService categoryService;
	
	// 여태까지 이렇게 하는 것이 단일 테스트이다. 
	@Test
	void testGetList() {
		List<Category> list = categoryService.getList();
		System.out.println(list);
	}

}



b. view단 설계 :

  • 카테고리 별 name 변경, url 변경


a) list.html 첫 번째 방법 :

<nav class="menu-category">
	<div>
		<h1 class="text-normal-bold">메뉴분류</h1>
	</div>
	<ul>
		<li class="menu-selected" >
			<a href="/menu/list">전체</a>
		</li>
		
		<!-- each문에 url 변경해주기! 중요!! -->
		<li th:each="categoryList: ${categoryList}" >
			<a href="" th:href="@{/member/list/c(id=${categoryList.id})}" th:text="${categoryList.name}">커피음료</a>
		</li>
	</ul>
</nav>


b) list.html 두 번째 방법 :
<li th:each="categoryList: ${categoryList}" >
	<!-- <a href="" th:href="@{/member/list/c(id=${categoryList.id})}" th:text="${categoryList.name}">커피음료</a> -->
	<a href="" th:href="@{list/c(id=${categoryList.id})}" th:text="${categoryList.name}">커피음료</a>
</li>


c) list.html 세 번째 방법(이게 정답)** :
  • list?c=1 이런 식으로 url이 보여야 한다. 소괄호가 ?를 의미하고 url에서 ?가 출력되기 위해서는 소괄호 앞에는 무슨 값이 필요하다.(여기서는 list가 있었다.)
  • 이렇게 되어야지 같은 페이지에서 쿼리스트링만 바뀌고 페이지는 이동하지 않는다.
<!-- each문에 url 변경해주기! 중요!! -->
<li th:each="c: ${categoryList}" >
	<!-- <a href="" th:href="@{/member/list/c(id=${categoryList.id})}" th:text="${categoryList.name}">커피음료</a> -->
	<a href="?c=1" th:href="@{list(c=${c.id})}" th:text="${c.name}">커피음료</a>
</li>


d) 네 번째 방법(추가 내용)** :
  • URL에 쿼리스트링 2개 쓰는 방법 : “,”로 구분해서 사용해주기!!

<!-- each문에 url 변경해주기! 중요!! -->
<li th:each="c: ${categoryList}" >
	<a href="?c=1" th:href="@{list(c=${c.id},p=${c.id})}" th:text="${c.name}">커피음료</a>
</li>




2) url에서 쿼리스트링 비교

a. 클래스 다루는 속성으로 비교하는 방법 :

  • 클래스 내부에서 비교하기 위해서는 다음과 같이 사용한다.
    • th:class=”${}? ‘ ‘”


b. 쿼리스트링으로 비교하는 방법(편의 객체)** :

  • ${param.c}를 이용하자!!(이거 말고도 session, @ 등이 있다.)


a) 편의 객체 정리**
  • HTTP 요청 파라미터 접근 : param
    • ${param.data}


  • HTTP 세션 접근 : session
    • ${session.data}


  • 스프링 빈 접근 : @
    • 빈 객체의 메서드에 접근할 수 있다.(보통 빈 객체의 mapping된 곳으로 이동한다.)
    • ${@helloBean.hello('Bean')}

<nav class="menu-category">
	<div>
		<h1 class="text-normal-bold">메뉴분류</h1>
	</div>
	<ul>
		<li class="" >
			<a href="/menu/list">전체</a>
		</li>
		
		<!-- each문에 url 변경해주기! 중요!! -->
		<li class="" th:class="${#strings.equals(param.c, c.id)}?'menu-selected'" 
			th:each="c: ${categoryList}" >
			
			<a href="?c=1" th:href="@{list(c=${c.id})}" th:text="${c.name}">커피음료</a>
		</li>
	</ul>
</nav>




3) Category와 엮어서 목록을 검색하는 방법 :

  • view단에서 컨트롤러로 값이 다 넘어와야하므로 인자에 @RequestParam을 이용한다.
    • 칼럼명을 바꿔야할때도 @RequestParam을 사용한다.


  • MenuController.java
    • categoryId가 Integer가 되는 이유는 null이 될 수도 있기 때문이다.
package kr.co.rland.web.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import kr.co.rland.web.entity.Category;
import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.entity.MenuView;
import kr.co.rland.web.service.CategoryService;
import kr.co.rland.web.service.MenuService;

// FrontController(POJO 클래스)를 만드는 방법! 
// 클래스는 보통 폴더명이 된다.(기능별로 넣자!)
@Controller
@RequestMapping("/menu")
public class MenuController {
	
	@Autowired
	private MenuService service;
	
	@Autowired
	private CategoryService categoryService;
	
	// 함수 이름은 보통 url이 된다.
	@RequestMapping("list")
	public String list(
			@RequestParam(name="p", defaultValue="1") int page,
			@RequestParam(name="c") Integer categoryId,
			@RequestParam(name="q") String query,
			Model model) {
		
		List<Category> categoryList = categoryService.getList();
		
		model.addAttribute("categoryList", categoryList);
		
		List<MenuView> list = service.getViewList();
		
		model.addAttribute("list", list);
		
		// model.addAttribute("data","hello");
//		service.getList();			// 1, query:"",
//		service.getList(1);
//		service.getList(1, "아");
//		service.getList(1, 1 /* category */ );		/* category 있는 애들만!!*/
//		service.getList(1, 1 /* category */, "아");
		
		
		
		// 상대경로이면 타임리프가 알아서 찾아준다. 
		return "menu/list";
	}
	
	// @RequestMapping에서 detail을 "/detail" 이나 "detail"를 사용하면 된다! 상관없더라
	@RequestMapping("detail")
	public String detail(long id, Model model) {
		// @requestParam은 인자의 변수명이 달라지면 사용하고 이름이 같다면, int로 인자를 바로 받는다. 
		
		
		// 집계가 필요하면, MenuView가 필요하다. 하지만, entity를 따로 만들 수도 있다.
		Menu menu = service.getById(id);
		
		// service 계층에서 받아와서 view 단으로 던져줘서 view 단에서 출력 한다.
		model.addAttribute("menu", menu);
		
		return "menu/detail";
	}

}


  • list.html
    • th:value="${param.c}, th:value="${param.q}"를 해줘야지 검색 실행 후 검색창에 값이 남아 있는다. id값은 숨기고 검색하려고 한 query 부분만 남긴다.
    • 하지만, 검색 후 카테고리 버튼을 누르면 검색창이 초기화되는 것은 해결하지 못했다. 이것은 문제까지는 아니고 업무에 따라 달라진다.

<header class="search-header">
	<h1 class="text-title1-h1" th:text="${data}">알랜드 메뉴</h1>
	<form action="list" method="get">
		<!-- th:value="${param.c}, th:value="${param.q}"를 해줘야지 검색 실행 후 
		검색창에 값이 남아 있는다. id값은 숨기고 검색하려고 한 query 부분만 남긴다.  -->
		<input type="hidden" name="c" th:value="${param.c}">
		<input type="text" name="q" th:value="${param.q}">
		<input type="submit" class="icon icon-find">
	</form>
</header>


  • DefaultMenuService.java
    • page, offset 관계 주의(등차 수열)
    • 1->0, 2->10, 3->20 : page가 1이면 offset은 0부터 시작, page가 2이면 offset은 10부터 시작을 만들어줘야 한다.
    • 0이 가장 처음을 의미한다.
package kr.co.rland.web.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.entity.MenuView;
import kr.co.rland.web.repository.MenuRepository;

@Service
public class DefaultMenuService implements MenuService {
	
	@Autowired
	private MenuRepository repository;

	public void setRepository(MenuRepository repository) {
		this.repository = repository;
	}

	@Override
	public List<Menu> getList() {
		// TODO Auto-generated method stub
		// return repository.findAll(0,10,"",1,3000,"regDate","desc");
		return repository.findAll(0,10,"",null, null, null, null);
	}


	@Override
	public List<Menu> getList(int page) {
		// TODO Auto-generated method stub
		int size=10;
		List<Menu> list = repository.findAll(page, size, null, null, null, null, null);
		
		return list;
	}

	@Override
	public List<Menu> getList(int page, String query) {
		// TODO Auto-generated method stub
		int size=10;
		List<Menu> list = repository.findAll(page, size, query, null, null, null, null);
		
		return list;
	}

	@Override
	public List<Menu> getList(int page, Integer categoryId) {
		// TODO Auto-generated method stub
		int size=10;
		List<Menu> list = repository.findAll(page, size, null, categoryId, null, null, null);
		
		return list;
	}

	@Override
	public List<Menu> getList(int page, Integer categoryId, String query) {
		// TODO Auto-generated method stub
		int size=10;
		List<Menu> list = repository.findAll(page, size, query, categoryId, null, null, null);
		
		return list;
	}

	@Override
	public List<MenuView> getViewList() {
		
		int size=10;
		
		List<MenuView> list = repository.findViewAll(0, size, null, null, null, null, null);

		return list;
	}

	@Override
	public List<MenuView> getViewList(int page) {
		
		int size=10;
		int offset = (page - 1)*10;
		
		List<MenuView> list = repository.findViewAll(offset, size, null, null, null, null, null);
		
		return list;
	}

	@Override
	public List<MenuView> getViewList(int page, String query) {
		
		int size=10;
		int offset = (page - 1)*10;
		
		List<MenuView> list = repository.findViewAll(offset, size, query, null, null, null, null);
		
		return list;
	}

	@Override
	public List<MenuView> getViewList(int page, Integer categoryId) {
		
		int size=10;
		int offset = (page - 1)*10;
		List<MenuView> list = repository.findViewAll(offset, size, null, categoryId, null, null, null);

		return list;
	}

	@Override
	public List<MenuView> getViewList(int page, Integer categoryId, String query) {
		
		int size=10;
		int offset = (page - 1)*10;
		List<MenuView> list = repository.findViewAll(offset, size, query, categoryId, null, null, null);
		
		return list;
	}
	
	@Transactional
	@Override
	public void pointUp() {
		Menu menu = new Menu();
		menu.setId(917L);
		menu.setPrice(7777);
		repository.update(menu);
		
		menu.setId(917L);
		menu.setPrice(999999);
		repository.update(menu);
	}

	@Override
	public Menu getById(long id) {
		Menu menu = repository.findById(id);
		
		return menu;
	}
	
}


  • MenuController.java
    • 여기서 동작해서 인자를 넣어줘야 한다. 실제 동작하는 부분!!(컨트롤러)
package kr.co.rland.web.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import kr.co.rland.web.entity.Category;
import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.entity.MenuView;
import kr.co.rland.web.service.CategoryService;
import kr.co.rland.web.service.MenuService;

// FrontController(POJO 클래스)를 만드는 방법! 
// 클래스는 보통 폴더명이 된다.(기능별로 넣자!)
@Controller
@RequestMapping("/menu")
public class MenuController {
	
	@Autowired
	private MenuService service;
	
	@Autowired
	private CategoryService categoryService;
	
	// 함수 이름은 보통 url이 된다.
	@RequestMapping("list")
	public String list(
			@RequestParam(name="p", defaultValue="1") int page,
			@RequestParam(name="c", required=false) Integer categoryId,
			@RequestParam(name="q", required=false) String query,
			Model model) {
		
		List<Category> categoryList = categoryService.getList();
		
		model.addAttribute("categoryList", categoryList);
		
		// 여기서 동작해서 인자를 넣어줘야 한다. 실제 동작하는 부분!!(컨트롤러) 
		List<MenuView> list = service.getViewList(page, categoryId, query);
		
		model.addAttribute("list", list);
		
		// model.addAttribute("data","hello");
//		service.getList();			// 1, query:"",
//		service.getList(1);
//		service.getList(1, "아");
//		service.getList(1, 1 /* category */ );		/* category 있는 애들만!!*/
//		service.getList(1, 1 /* category */, "아");
		
		
		
		// 상대경로이면 타임리프가 알아서 찾아준다. 
		return "menu/list";
	}
	
	// @RequestMapping에서 detail을 "/detail" 이나 "detail"를 사용하면 된다! 상관없더라
	@RequestMapping("detail")
	public String detail(long id, Model model) {
		// @requestParam은 인자의 변수명이 달라지면 사용하고 이름이 같다면, int로 인자를 바로 받는다. 
		
		
		// 집계가 필요하면, MenuView가 필요하다. 하지만, entity를 따로 만들 수도 있다.
		Menu menu = service.getById(id);
		
		// service 계층에서 받아와서 view 단으로 던져줘서 view 단에서 출력 한다.
		model.addAttribute("menu", menu);
		
		return "menu/detail";
	}

}




4) 검색 시, 카테고리를 눌러 페이지 이동하면 검색어 유지

  • 업무에서 정하기 : 카테고리를 대상으로 검색하는 경우도 있고 전체를 대상으로 하는 검색도 있다.


  • 추가적으로 구현할 것: 목록 페이지의 더보기 버튼!



3. 메뉴 추천, detail 페이지 : 230322


1) 추천 기능


a. 다대다 관계의 DB 모델링의 key의 포함 관계


a) 다대다 관계에서 포함 관계 예시** :
  • 예를 들어, 시험 문제를 내는 사람이 문제도 같이 등록하면 문제를 등록하는 DB 테이블에 그 사람을 포함시키지 않는다!
    • 첫 번째 예시 정리 : 시험 문제를 ‘등록’하는 Key와 ‘시험 문제’ 테이블의 Key만 포함 = 원래는 ‘사람’과 ‘시험문제’(를 내다) 사이에 등록이라는 액션을 두고 다대다 관계였다.


  • 또한, 예약을 하고 결제하는 사람이 같은 사람이라는 가정을 잡는다. 그렇다면 결제 시, 예약하는 DB 테이블에 결제 Key를 Key로 포함시키지 않는다.
    • 두 번째 예시 정리 : ‘예약’ 테이블 Key와 그 ‘상품’ Key만 포함 = 원래는 ‘사람’과 ‘상품’(결제) 사이에 ‘예약’이라는 액션을 두고 다대다 관계였다.


b) 위의 예시 정리** :
  • 정리 : 사전에 하는 일이 4형식 문형인데 그 사람이 또 다른 같은 일을 한다. 그렇다변 같은 일을 하는 그 사람은 PK에 포함시키지 않는다.


  • 결론** : 같은 키를 2개의 PK로 받아서(원래는 FK의 역할과 같지만) 이름만 의미에 맞게만 바꿔서 사용한다.**
    • 그래서 우리가 구현하려는 추천 메뉴 기능을 보면 간단히 메뉴가 같은 메뉴를 가리켜서 추천한다. 마치 게시글의 댓글과 같다고 생각할 수 있겠지만 사실은 다르다.
    • 왜냐하면 다대다 관계라서 게시글과 댓글의 관계(이것은 1대다 관계)가 아니다. 메뉴가 자신인 메뉴를 추천해서 자기가 자신을 가리키는 다대다 관계이다.


  • 그래서, 추천과 구성을 의미하는 ‘rcmd_menu_id’로서 id를 정한다. 원래 메뉴를 가리키는 키는 ‘menu_id’이다.


c) 추가 의문점 :
  • 여기서 ‘대리키’인 ‘추천메뉴’ 테이블에서 원래 생기는 PK가 식별키로서 사용되야 하는가? 내 생각은 안 그래도 될 것 같다.


  • 식별이 가능하면 대리키인 id가 필요가 없다. 따라서, 대리키인 id가 필요가 없다. ‘menu_id’와 rcmd_menu_id(recommend 메뉴 테이블에서 포함되는 id)만 필요하다!
    • 이 2개가 PK가 되며 이 두가지 Key가 식별키가 된다!
    • 추가로 ‘추천’ 테이블에 다른 칼럼으로 추천의 ‘궁합도’나 ‘설명’이라는 칼럼을 추가해줄 수 있다.


b. VIEW 테이블을 쓰는 이유

  • 목록이 나오는 것을 반복적으로 가져올 때, 다시 select하는 경우는 없어서 이것을 붙이는 VIEW 테이블이 필요하다. select는 1가지 목적에 대하여 1번만 사용한다.


c. VIEW 테이블을 위한 SQL문 설계**

a) Outer Join vs Inner Join :
  • outer join은 null이 들어가는 outer가 생긴다. 그것이 왼쪽에 생기느냐 오른쪽에 생기냐이다.(left join, right join)
  • inner join은 null이 들어간 레코드가 안 생긴다. id를 가지고 있는 레코드만 생긴다.


b) 테이블의 칼럼명 수정 :
  • 위의 내용대로 설계하면 rcmd_menu_id인데 이것은 추천 메뉴 테이블의 id를 의미하므로 그냥 id라고 사용해도 된다!!
  • 그래서, SQL문 조건은 식별되어야 하기 때문에 rm.id로 비교해야 한다. rm.menu_id가 아니라 바뀐 칼럼명인 rm.id로 비교해야 한다.

select rm.*, m.img img, m.name name
from rcmd_menu rm 
join menu m
on rm.id = m.id;



d. VIEW 테이블을 위한 Entity 설정


  • RcmdMenu.java
package kr.co.rland.web.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RcmdMenu {
	private int id;
	private int menuId;
}


  • RcmdMenuView.java
package kr.co.rland.web.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;


// @Builder 	// 상속 받을 때는 @Builder는 사용할 수 없다!!

// 상속 받은 Entity인 RcmdMenuView는 

@ToString
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class RcmdMenuView extends RcmdMenu {
	private String img;
	private String name;
}



e. Mapper.xml 타입 설정**

a) parameterType 정리
  • Object인 RcmdMenu로 넘겨주면 parameterType를 적어줘야 한다. parameterType을 안 적어주려면 필요한 파라미터 인자들을 아키텍처 계층화 중 Service 계층에서도 일일이 다 적어줘야 한다.(id, name 등등)


b) #{id}getter 조건 정리
  • 또한, #{id} 매핑은 getter가 없으면 같은 필드명에서 getter 찾는다. 그래서 getter는 없어도 동작하는데 그 이유는 Mybatis에서 임의로 만들어주는 것 같다. 정리하면, Mybatis를 이용하기 위한 Mapper.xml에서 #{id}의 getter가 없다면 필드명을 우선 찾는다.**


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.co.rland.web.repository.RcmdMenuRepository">

	<resultMap id="rcmdMenuViewResultMap" type="RcmdMenuView">
	  <result property="menuId" column="menu_id"/>
	</resultMap>
	
	<!-- 1) id는 무조건 필수 이다!! if문이 필요가 없다. -->
	<!-- 2) 추천메뉴라서 업데이트 기능은 필요가 없다!! -->
	<!-- 3) id는 사용자 입력이라서 #으로 받는다! -->
	<!-- 4) menu_id가 menuId로 자동으로 매핑이 되나? 안 된다!!-->
	<select id="findViewAllByMenuId" resultMap="RcmdMenuView">
		select * 
		from rcmd_menu_view
		where menu_id = #{menuId}
	</select>
	
	<!-- #{id} 매핑은 getter가 없으면 같은 필드명에서 getter 찾는다. -->
	<insert id="insert" parameterType="RcmdMenu">
		insert into rcmd_menu_view(menu_id, id)
		values(#{menuId}, #{id})
	</insert>
	
	
 	<delete id="delete" parameterType="RcmdMenu">
 		delete from rcmd_menu_view 
 		where menu_id = #{menuId}
 		and id = #{id}
	</delete> 
	
</mapper>




2) detail 페이지 설계


  • 각 부분이 부분적으로 다 메서드로 받아와야 한다.(굉장히 붙일게 많다!)
    • getCountCart() 등등


a. Repository(Dao) 구현

  • RcmdMenuRepository.java
package kr.co.rland.web.repository;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import kr.co.rland.web.entity.RcmdMenu;
import kr.co.rland.web.entity.RcmdMenuView;


@Mapper
public interface RcmdMenuRepository {
	
	// RcmdMenuView는 목록 조회에서만 쓰인다.
	List<RcmdMenuView> findViewAllByMenuId(int menuId);
	
	int insert(RcmdMenu rcmdMenu);
	
	int delete(RcmdMenu rcmdMenu);
	// udpate
}


  • RcmdMenuRepositoryMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.co.rland.web.repository.RcmdMenuRepository">

	<resultMap id="rcmdMenuViewResultMap" type="RcmdMenuView">
	  <result property="menuId" column="menu_id"/>
	</resultMap>
	
	<!-- 1) id는 무조건 필수 이다!! if문이 필요가 없다. -->
	<!-- 2) 추천메뉴라서 업데이트 기능은 필요가 없다!! -->
	<!-- 3) id는 사용자 입력이라서 #으로 받는다! -->
	<!-- 4) menu_id가 menuId로 자동으로 매핑이 되나? 안 된다!!-->
	<select id="findViewAllByMenuId" resultMap="rcmdMenuViewResultMap">
		select * 
		from rcmd_menu_view
		where menu_id = #{menuId}
	</select>
	
	
	<!-- #{id} 매핑은 getter가 없으면 같은 필드명에서 getter 찾는다. -->
	<insert id="insert" parameterType="RcmdMenu">
		insert into rcmd_menu_view(menu_id, id)
		values(#{menuId}, #{id})
	</insert>
	
	
 	<delete id="delete" parameterType="RcmdMenu">
 		delete from rcmd_menu_view 
 		where menu_id = #{menuId}
 		and id = #{id}
	</delete> 
	
</mapper>


  • RcmdMenuRepository의 test 코드
    • RcmdMenuRepositoryTest
    • 하지만, lombok은 ToString 상속을 못 받아서 id가 안나온다. 추가로 설정을 해줘야 한다.
package kr.co.rland.web.repository;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;

import kr.co.rland.web.entity.RcmdMenuView;


@AutoConfigureTestDatabase(replace=Replace.NONE)
@MybatisTest
class RcmdMenuRepositoryTest {
	
	@Autowired
	private RcmdMenuRepository repository;
	
	@Test
	void testfindViewAllByMenuId() {
		List<RcmdMenuView> list= repository.findViewAllByMenuId(617);
		System.out.println(list);
	}

}



b. 서비스단 설계

  • RcmdMenuService.java

package kr.co.rland.web.service;

import java.util.List;

import kr.co.rland.web.entity.RcmdMenuView;

public interface RcmdMenuService {
	List<RcmdMenuView> getViewListByMenuId(int menuId);
}


  • DefaultRcmdMenuService.java


package kr.co.rland.web.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import kr.co.rland.web.entity.RcmdMenuView;
import kr.co.rland.web.repository.RcmdMenuRepository;

@Service
public class DefaultRcmdMenuService implements RcmdMenuService {

	@Autowired
	private RcmdMenuRepository rcmdMenuRepository;
	
	
	@Override
	public List<RcmdMenuView> getViewListByMenuId(int menuId) {
		
		return rcmdMenuRepository.findViewAllByMenuId(menuId);
	}

}



c. 컨트롤러 설계

  • menuController.java
package kr.co.rland.web.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import kr.co.rland.web.entity.Category;
import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.entity.MenuView;
import kr.co.rland.web.entity.RcmdMenuView;
import kr.co.rland.web.service.CategoryService;
import kr.co.rland.web.service.MenuService;
import kr.co.rland.web.service.RcmdMenuService;

// FrontController(POJO 클래스)를 만드는 방법! 
// 클래스는 보통 폴더명이 된다.(기능별로 넣자!)
@Controller
@RequestMapping("/menu")
public class MenuController {
	
	@Autowired
	private MenuService service;
	
	@Autowired
	private CategoryService categoryService;
	
	@Autowired
	private RcmdMenuService rcmdMenuService;
	
	
	// @RequestMapping에서 detail을 "/detail" 이나 "detail"를 사용하면 된다! 상관없더라
	@RequestMapping("detail")
	public String detail(long id, Model model) {
		// @requestParam은 인자의 변수명이 달라지면 사용하고 이름이 같다면, int로 인자를 바로 받는다. 
		
		
		// 집계가 필요하면, MenuView가 필요하다. 하지만, entity를 따로 만들 수도 있다.
		Menu menu = service.getById(id);
		
		// service 계층에서 받아와서 view 단으로 던져줘서 view 단에서 출력 한다.
		model.addAttribute("menu", menu);
		
		
		List<RcmdMenuView> rcmdMenuList = rcmdMenuService.getViewListByMenuId(617);
		
		model.addAttribute("rcmdMenuList", rcmdMenuList);
		
		return "menu/detail";
	}

}




d. View단 설계

  • detail.html

<section class="menu-rcmd">
	<header>
		<h1 class="d-none">추천 메뉴</h1>
		<span>함께 시키면 맛나요! ^^</span>
	</header>

	<div>
		<a href="" th:each="r:${rcmdMenuList}">
			<img th:src="@{/image/menu/{img}(img=${r.img})}" class="image-view-round" src="https://images.unsplash.com/photo-1515442261605-65987783cb6a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80" alt="">
			<span th:text="${r.name}" style="color:#000">아몬드 쿠키</span>
		</a>
	</div>
</section>




4. Datail 페이지 구현 : 230323

1) Datail 페이지 옵션 기능

a. Detail 페이지 구성할 때, 어떤 데이터 가져오는지

  • VIEW 테이블을 이용하는 객체는 여러 가지로 만들 때만 이용한다. VIEW 테이블은 여러가지 데이터와 섞이기 때문에 detail 페이지의 카테고리를 출력해주는 부분은 그 카테고리 목록만 필요하다.

  • 따라서, 하나의 view 단에서 모든 것들을 VIEW 테이블을 이용하면 안되고 ‘목록’을 위해서만 사용해야 한다.

  • 단일 값을 가져오는 페이지는 각자 다른 Entity에서 가져와야 한다. 왜냐하면, 나중에 다른 것들을 추가할 때, 그때도 연결된 것이 없는 추가만 해주면 된다.**

  • 중요! : 목록은 데이터가 결합이 필요하다면 VIEW 테이블이 필요하다!**


b. Repository명 규칙**

  • 데이터를 제공하는 입장에서는 레코드 단위로 기본적이기 때문에 find라고 Repository의 메서드 명을 적어줘야 한다.

  • 그 업무는 레코드 1줄 전체 중 일부분만 쓸 수 있지만 데이터를 제공해주는 입장에서는 데이터 전체를 제공해주어야 한다.


c. CategoryService에서 카테고리Id 출력 방법

  • 집계가 필요하면, MenuView가 필요하다. 하지만, entity를 따로 만들 수도 있다.

int cartCount = 10;
Menu menu = service.getById(id);



  • 수정 후 코드 : 카테고리 Id를 가지고 올 때는 CategoryService에 붙어잇는 카테고리 ID를 가지고 와야 한다.
  • 카테고리 ID만 가지고 오면 붙어있는 Menu 테이블의 칼럼 중 데이터를 가져 올 수 있다. 제일 중요!!

String categoryName = categoryService.getNameById(menu.getCategoryId());

// 메서드가 2번 걸치기 때문에 getNameById에 넣어 줄 때,
// Menu 엔티티에서 Java에서는 칼럼명이 categoryId로 쓰이는데 **
// DB에서는 category_id 이렇게 쓰여서 Mapper에서 ResultMap 설정 해주기!!**

model.addAttribute("rcmdMenuList", rcmdMenuList);


d. 서비스 계층 설계 방법

  • 서비스 계층은 Repository 계층을 여러개 가질 수 있다.(1대다 관계)
  • 서비스 계층은 같은 계층인 서비스 계층을 포함할 수 없다.


e. Detail 페이지 옵션 구현

a) param 객체

  • HTTP의 parameter를 사용할 수 있다.


b) 옵션 출력의 코드 구현(d-none 이용)

  • detail.html
<!-- param.id 중요!! HTTP parameter를 가져온다. -->
<div class="tab-div">
	<a href="" th:href="@{detail(id=${param.id})}" class="btn btn-fill border-bottom-0 btn-size-1">Default 크기</a>
	<a href="" th:href="@{detail(id=${param.id}, op=lg)}" class="btn btn-line border-bottom-0 border-left-0 btn-size-1">Large 크기</a>
</div>	

<!-- 세부 옵션 추가하거나 말거나 하기  -->
<!-- 옵션이 여러가지가 있을 때, 이렇게 비교한다! -->
<section class="nutrition-info"
th:classappend="${param.op}!=null? 'd-none'">
	<header>
		<div class="common-p">
			<h1 class="text-normal">영양정보</h1>
			<div class="">총량 300ml</div>
		</div>							
	</header>
	<ul class="common-p">
		<li>
			<span>포화지방</span>
			<span>2 g</span>
		</li>
		<li>
			<span>단백질</span>
			<span>2 g</span>
		</li>
		<li>
			<span>나트륨</span>
			<span>2 mg</span>
		</li>
		<li>
			<span></span>
			<span>2 g</span>
		</li>
		<li>
			<span>카페인</span>
			<span>2 mg</span>
		</li>
	</ul>
</section>

<!-- 세부 옵션 추가하거나 말거나 하기  -->
<!-- 옵션이 여러가지가 있을 때, 이렇게 비교한다! -->
<section class="nutrition-info"
th:classappend="${not #strings.equals(param.op, 'lg')}?'d-none'">
	<header>
		<div class="common-p">
			<h1 class="text-normal">영양정보</h1>
			<div class="">총량 500ml</div>
		</div>							
	</header>
	<ul class="common-p">
		<li>
			<span>포화지방</span>
			<span>4 g</span>
		</li>
		<li>
			<span>단백질</span>
			<span>4 g</span>
		</li>
		<li>
			<span>나트륨</span>
			<span>8 mg</span>
		</li>
		<li>
			<span></span>
			<span>8 g</span>
		</li>
		<li>
			<span>카페인</span>
			<span>28mg</span>
		</li>
	</ul>
</section>


b) 옵션 출력의 코드 구현(d-none 이용)

  • 아래의 코드는 서버에게 재요청해서 css를 다시 만들어야 해서 불필요한 데이터를 끌어와야 한다..
  • 그리고, 성능이 느리다. 코드량이 불필요하게 많다.


  • detail.html

<div class="tab-div">
	<a href="" th:href="@{detail(id=${param.id})}" 
	class="btn btn-fill border-bottom-0 btn-size-1" 
	th:classappend="${param.op}!=null? 'btn-line':'btn-fill'"
	>Default 크기</a>
	
	<a href="" th:href="@{detail(id=${param.id}, op=lg)}" 
	class="btn btn-line border-bottom-0 border-left-0 btn-size-1"
	th:classappend="${not #strings.equals(param.op, 'lg')}?'btn-fill':'btn-line'"
	>Large 크기</a>
</div>	




2) REST API, AJAX 시작

a. REST API 이용하기

  • REST API에서는 반환값이 문서를 전달 받는 것이 아니라 사용자가 받는 데이터 자체를 반환해준다. 그래서, 이제는 반환값을 List<MenuView>로도 받을 수 있다.

  • HTML이 제공하는 method는 2가지밖에 없었는데 웹(HTTP)이 제공해주는 method는 PUT,DELETE,DELETE,GET,POST가 있다.


a) 기존 방식(HTML 제공)

  • @Controller -> 문서명
  • 그래서 반환 값이 /menu/list이며 menu/detail이다.
  • /menu/detail?id=3
  • /menu/edit : GET
  • /menu/edit : POST


b) 새로운 방식(HTTP가 제공 = REST API)


  • /menus/ method:GET
  • /menus/3 method:GET get 기능
  • /menus/3 method:DELETE delete 기능
  • /menus/3 method:PUT edit 기능
  • /menus/3 method:PUT insert 기능


c) 정리

  • 따라서, URL이 같고 메서드에 따라서 기능이 달라진다!!
  • 이전까지는 HTML이 제공해주는 Method만 이용했었다.
  • 이제 HTTP가 제공해주는 API를 쓸 수 있다.
  • 하지만, 이러한 것을 테스트하기 위해서는 스크립트가 필요한데 이를 대신해주는 도구가 POST MAN이 있다.


b. POST MAN 이용

  • 설치 방법 : home brew postman



5. REST API : 230324

0) REST 개념** :

  • REST의 R은 ‘Representation’을 의미한다. 즉, ‘표현’이라는 의미이다.


  • 왜 REST에서는 표현이라고 하는지?
    • 예를 들어, 우리는 회원이라는 정보를 HTML로 ‘표현’을 한 것을 보게 된다. 우리는 회원이라는 리소스를 DB의 실제 데이터가 HTTP로 전송이 될 때, HTML으로 정보가 ‘표현’이 되거나 JSON으로도 정보가 ‘표현’될 수도 있어서 실제 전달하는 것을 표현이라고 정의를 내렸다.
    • HTTP BODY에서 ‘표현 데이터’를 BODY에 담아서 전달해준다. 표현 헤더는 표현 데이터를 해석할 수 있는 정보를 제공해준다.


  • REST API에서 @GetMapping은 자바스크립트가 요청한다. 그래서, 우리는 자바스크립트 코드가 필요하다.


1) PostMan 사용법

  • 1) 새로운 Collection에서 add Request를 눌러서 설정!

  • 2) add Request의 이름도 설정하고 Save 버튼을 눌러서 저장하기.

  • 3) Method와 반환 받고 싶은 url을 정하여 입력한다.

  • 4) Send 버튼의 결과를 반환하기 위해서 스프링 부트에서 @GetMapping 등 관련 코드를 입력하고 스프링 부트의 서버를 킨다.

  • 5) POSTMAN에서 해당 MethodURL에 맞게 설정하고 Send 버튼을 누른다.

  • 6) Send 버튼을 누르면, 해당 요청에 대한 응답이 나온다.

  • 정리** : 이런 것들을 하기 위해서는 원래 “자바스크립트 코드”가 필요한데 아직 준비된 코드가 없으면 POSTMAN이 대신 HTTP 요청에 대해서 반환해준다.


  • 실습 코드 :
    • 경로의 중간이 {}로 감싸면 경로를 변수화할 수 있다.
    • 요청이 들어오면 해당 id를 찾다가 없으면 쿼리스트링에서 찾는다. 그러다가 없으면 에러 발생
    • @PathVariable : Path에서 찾아서 이 변수에 담아달라고 매핑해준다.
    • return 값에 데이터 형태로 전달한다.
package kr.co.rland.web.controller.api;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;

// API 이용하기 = AJAX


@RestController("apiMenuController")
public class MenuController {
	
	// REST API에서는 반환값이 문서를 전달하는 것이 아니라 사용자가 받는 데이터이다. 
	// List<MenuView>
//	public List<MenuView> getList(){
//		
//	}
	
	
	// 이전까지는 HTML이 제공해주는 Method만 이용했었다.
	// 이제 HTTP가 제공해주는 API를 쓸 수 있다.
	@GetMapping("/menus")
	public String getList() {
		return "menu List";
	}
	
	// 경로를 중간이 {}로 감싸면 경로를 변수화할 수 있다. 
	// 이 id를 찾다가 없으면 쿼리스트링을 찾다가 
	// @PathVariable : Path에서 찾아서 이 변수에 담아달라고 매핑해준다.  
	// return 값에 이렇게 전달된다.
	@GetMapping("/menus/{id}")
	public String get(
			@PathVariable("id")  int id) {
		return "menu " + id;
	}

	@PutMapping("/menus/{id}")
	public String edit(
			@PathVariable("id") int id) {
		return "menu edit: " + id;
	}
	
	@DeleteMapping("/menus/{id}")
	public String delete(
			@PathVariable("id") int id) {
		return "menu del : " + id;
	}
	
	@PostMapping("/menus")
	public String insert() {
		return "menu insert";
	}

	
	
}




2) REST 요청에 대한 반환을 List 자료형으로 보내기

  • REST API에서 반환값은 문서를 전달하는 것이 아니라 사용자가 받는 데이터이다.

package kr.co.rland.web.controller.api;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;

import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.service.MenuService;

// API 이용하기 = AJAX


@RestController("apiMenuController")
public class MenuController {
	
	// REST API에서는 반환값이 문서를 전달하는 것이 아니라 사용자가 받는 데이터이다. 
	// List<MenuView>
//	public List<MenuView> getList(){
//		
//	}
	
	@Autowired
	private MenuService service;

	
	// 이전까지는 HTML이 제공해주는 Method만 이용했었다.
	// 이제 HTTP가 제공해주는 API를 쓸 수 있다.
	@GetMapping("/menus")
	public List<Menu> getList() {
		List<Menu> list = service.getList();
		
		return list;
	}
}
	


3) REST API 경로 수정

  • 가장 위에 있는 @RequestMapping과 URL 매핑 경로가 같다면 아래에서 매핑된 @GetMappig에서 경로는 안 써도 된다.
  • @PutMapping은 업데이트해주기 위해서 통째로 데이터가 바뀌기 때문에 Mapping 경로에서 {id}를 없애도 된다.
package kr.co.rland.web.controller.api;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.service.MenuService;

// API 이용하기 = AJAX


@RestController("apiMenuController")
@RequestMapping("menus")	// 예전에는 절대경로로 썼지만 지금은 "/"를 안써도 된다. 
public class MenuController {
	
	// REST API에서는 반환값이 문서를 전달하는 것이 아니라 사용자가 받는 데이터이다. 
	// List<MenuView>
//	public List<MenuView> getList(){
//		
//	}
	
	@Autowired
	private MenuService service;

	
	// 이전까지는 HTML이 제공해주는 Method만 이용했었다.
	// 이제 HTTP가 제공해주는 API를 쓸 수 있다.
	@GetMapping	// @RequestMapping과 경로가 같다면 여기는 안써도 된다.
	public List<Menu> getList() {
		List<Menu> list = service.getList();
		
		return list;
	}
	
	// 경로를 중간이 {}로 감싸면 경로를 변수화할 수 있다. 
	// 이 id를 찾다가 없으면 쿼리스트링을 찾다가 
	// @PathVariable : Path에서 찾아서 이 변수에 담아달라고 매핑해준다.  
	// return 값에 이렇게 전달된다.
	@GetMapping("{id}")
	public Menu get(
			@PathVariable("id")  int id) {
		
		// Menu menu = service.getById(id);
		Menu menu = service.getById(id);
		
		return menu;	// 객체를 반환할수 없어서 데이터를 반환해줘야 한다.
	}

	@PutMapping			// put도 통째로 바뀌어서 Mapping 경로를 {id}도 없애도 된다.?? 
	public String edit(
			@PathVariable("id") int id) {
		return "menu edit: " + id;
	}
	
	@DeleteMapping("{id}")
	public String delete(
			@PathVariable("id") int id) {
		return "menu del : " + id;
	}
	
	@PostMapping
	public String insert() {
		return "menu insert";
	}
	
}




4) View단에서 인자 받기

  • 아래 코드 테스트 방법 : POSTMAN에서 Request 창에서 아래 코드의 getList 메서드 인자에 넣어준@RequestParam에 알맞은 QueryParam 값 넣어주기
package kr.co.rland.web.controller.api;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.entity.MenuView;
import kr.co.rland.web.service.MenuService;

// API 이용하기 = AJAX


@RestController("apiMenuController")
@RequestMapping("menus")	// 예전에는 절대경로로 썼지만 지금은 "/"를 안써도 된다. 
public class MenuController {
	
	// REST API에서는 반환값이 문서를 전달하는 것이 아니라 사용자가 받는 데이터이다. 
	// List<MenuView>
//	public List<MenuView> getList(){
//		
//	}
	
	@Autowired
	private MenuService service;

	
	// 이전까지는 HTML이 제공해주는 Method만 이용했었다.
	// 이제 HTTP가 제공해주는 API를 쓸 수 있다.
	@GetMapping	// @RequestMapping과 경로가 같다면 여기는 안써도 된다.
	public List<MenuView> getList(
			@RequestParam(name = "p", defaultValue="1") int page, 	
			@RequestParam(name = "c", required=false) Integer categoryId, 	// categoryId는 무조건 사용하는 것이 아니라서 
			@RequestParam(name = "q", required=false) String query) {		// @RequestParam 이용하자!!
		// null을 담기 위해서 required false 설정을 한다. 
		List<MenuView> list = service.getViewList(page, categoryId, query);
		
		return list;
	}
}

5) View단에서 인자 받기(JS)

a. JS를 이용하기 위해서 기존 코드 수정하는 과정

  • 1) 기존 list.html과 detail.html의 파일을 복사해서 list2.html과 detail2.html로 파일명 바꾸기
    • AJAX를 테스트 하기 위해서 list2.html과 detail2.html를 이용한다.


  • 2) 그래서 기존 MenuController에서 매핑된 list, detail 메서드의 반환값을 list2, detail2로 바꾸자. 아직, 기존 컨트롤러는 화면을 보여주기 위해서 사용하고 API용 컨트롤러는 데이터를 전송해주는 API라서 두 개의 컨트롤러가 다 필요하다!!


  • 3) list2.html에서 <script scr="/js/menu/list.js" defer="defer"></script>를 main 태그 바로 아랫 줄에 추가해주기


  • 4) 3)의 스크립트 태그의 defer 옵션?
    • 원래 상단에 스크립트 태그가 존재하면, 이 .js 파일이 읽히는 이 순간 바로 스크립트 코드가 실행된다. 하지만, 이것은 모든 코드가 로드되기도 전에 실행된다.
    • 이렇게 되면 비효율적으로 실행되어서 로드가 될 때까지 기다렸다가 실행되도록 설정해주는 defer 옵션이 필요하다.
    • defer 옵션은 html 코드 순서상 위에 올려놓아도 가장 아래 순서로 깔려서 다 로드되고나서 스크립트코드가 실행되도록 해준다.


  • 5) 기존에 있던 static/js/menu/list.js의 자바스크립트 코드 지우기(지웠던 코드는 아래 코드와 같다.)


b. list.js에서 지웠던 코드

window.addEventListener("load", function(){
	const ul = document.querySelector(".menu-category ul");
	const menubox = document.querySelector(".menu-list");
	let currentLi = document.querySelector(".menu-category ul li.menu-selected");
		
	let controller = new AbortController();
	
	
	ul.onclick = function(e){		
		e.preventDefault();
		const el = e.target;
		
		if(menubox.classList.contains("ajax-loader")){
			if(confirm("기달려바바좀...\n\r요청을 취소하려면 확인버튼을 눌러주세요")){			
				controller.abort();
				controller = new AbortController();
			}
				
			return;
		}
				
		if(el.tagName != "LI" && el.tagName != "A")
			return;
		let li = el;
		if(el.tagName == "A")
			li = el.parentElement;
					
		li.classList.add("menu-selected");
		currentLi.classList.remove("menu-selected");
		
		currentLi = li;
		
		// 데이터를 요청  ?c=2 -> param:2
		// /api/menus/cate/2
		let queryString = `?c=${currentLi.dataset.id}`;
		if(currentLi.dataset.id == 0)
			queryString = "";		
		
		// 요청전 -> 아이콘 띄우고
		menubox.classList.add("ajax-loader");
				
		const signal = controller.signal;
		fetch(`/api/menus${queryString}`,{signal})
		.then((response)=>response.json())
		.then((list)=>{
			menubox.innerHTML = "";
		
			for(let m of list){
				let template = `
					<section class="menu hidden">
					    <form class="" action="list" method="post">
					        <h1>${m.name}</h1> 
					        <div class="menu-img-box">
					            <a href="detail.html"><img class="menu-img" src="/image/product/12.png"></a>
					        </div>
					        <div class="menu-price">${m.price} 원</div>
					        <div class="menu-option-list">
					            <span class="menu-option">
					                <input class="menu-option-input" type="checkbox" name="ice" value="true">
					                <label>ICED</label>
					            </span>            
					            <span class="menu-option ml-2">
					                <input class="menu-option-input" type="checkbox" name="large" value="true">
					                <label>Large</label>
					            </span>
					        </div>
					        <div class="menu-button-list">
					        	<input type="hidden" name="menu-id">
					            <input class="btn btn-cancel btn-cancel-lg btn-size-1 btn-size-1-lg" type="submit" name="cmd" value="담기">
					            <input class="btn btn-default btn-default-lg btn-size-1 btn-size-1-lg ml-1" type="submit" name="cmd" value="주문하기">
					        </div>
					    </form>
					</section>
				`;
				
				//menubox.innerHTML += 
				//menubox.insertAdjacentHTML("beforeend", template);
				
				//let el = new DOMParser()
				//		.parseFromString(template, "text/html")
				//		.body
				//		.firstElementChild;
				let div = document.createElement("div");
				div.innerHTML = template;
				let el = div.firstChild;				
						
				menubox.append(el);
				
					
			}
			
			setTimeout(()=>{
				let length = menubox.children.length;
				for(let i=0; i<length; i++)
					menubox.children[i].classList.remove("hidden");				
			},10);
			
			// appendChild() / append() / insertAdjacentElement() / insertAdjacentHTML()
			// 아이콘 지우기
			menubox.classList.remove("ajax-loader");
		})
		.catch(err=>{
			console.log(`fetch error:${err.message}`);
			menubox.classList.remove("ajax-loader");
		});
		
		// 화면을 갱신
	};
});


6) AJAX 개념

  • 기존 페이지 요청 방식인 SSR은 페이지를 매번 새로 만들기 때문에 문제가 많았었다.
  • 따라서, AJAX가 필요하다. 이것은 서버를 요청하지 않고 데이터를 처리해준다.


a. AJAX 의미 정리

  • A : Async이며 비동기 처리 방식
  • JA : 자바스크립트를 의미한다.
  • X : ‘XML’을 의미하는데 XML은 데이터를 상징적인 의미이다.(JSON도 될 수도 있다.)


b. 실습 코드 수정하기

  • html에서 script파일은 문서 안으로 포함되어야해서 main 태그 안에 넣어주기!!
    • 뭔가 이상하긴 하다..
<!DOCTYPE html>
<html
	 xmlns=th="http://www.thymeleaf.org"
	 xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
	 layout:decorate="inc/layout"
>	

	<!-- 타임리프는 속성으로 쓰기 때문에 속성으로 쓰기 위해서 div 태그에 사용한다. -->
	<!-- <div th:replace=""></div> -->
	
	<!-- replace는 대체, insert는 div 태그 모양을 유지한다. -->
	
	<main layout:fragment="main">
	
	<!-- script 코드 위치 중요!! -->
	<script src="/js/menu/list.js" defer="defer"></script>
		<section>
			<header class="search-header">
				<h1 class="text-title1-h1" th:text="${data}">알랜드 메뉴</h1>
				<form action="list" method="get">
					<!-- th:value="${param.c}, th:value="${param.q}"를 해줘야지 검색 실행 후 
					검색창에 값이 남아 있는다. id값은 숨기고 검색하려고 한 query 부분만 남긴다.  -->
					<input type="hidden" name="c" th:value="${param.c}">
					<input type="text" name="q" th:value="${param.q}">
					<input type="submit" class="icon icon-find">
				</form>
			</header>
			<aside class="aside-bar">
			
				<h1>aside</h1>
				<section class="aside-bar-content">
					<h1>메인메뉴</h1>
					<ul class="mt-3">
						<li><a href="">카페메뉴</a></li>
						<li><a href="">공지사항</a></li>
						<li><a href="/user/login.html">로그인</a></li>
					</ul>
				</section>
			</aside>
			<nav class="menu-category">
				<div>
					<h1 class="text-normal-bold">메뉴분류</h1>
				</div>
				<ul>
					<li class="" th:class="${param.c} ==null ? 'menu-selected'">
						<a href="list">전체</a>
					</li>
					
					<!-- each문에 url 변경해주기! 중요!! -->
					<li th:attr="data-cid=${c.id}" class="" th:class="${#strings.equals(param.c, c.id)}?'menu-selected'" 
						th:each="c: ${categoryList}" >
						
						<!-- <a href="" th:href="@{/member/list/c(id=${categoryList.id})}" th:text="${categoryList.name}">커피음료</a> -->
						<!-- <a href="?c=1" th:href="@{list(c=${c.id})}" th:text="${c.name}">커피음료</a> -->
						<a href="?c=1" th:href="@{list(c=${c.id})}" th:text="${c.name}">커피음료</a>
					</li>
				</ul>
			</nav>

			<section class="cart-section">
				<h1 class="d-none">장바구니</h1>
				<span class="text-title3">커피음료</span>
				<div class="icon icon-basket icon-text" >1</div>
			</section>

			<section class="menu-section">
		        <h1 class="d-none">메뉴목록</h1>
		        <div class="menu-list">
		        
		            <section class="menu" th:each="menu: ${list}">
		                <form class="">
		                    <h1><span th:text=${menu.name}>알랜드 커피/</span><span style="font-size:11px;" th:text="${menu.categoryName}">(커피음료)</span></h1>
		                    <div class="menu-img-box">
		                        <a href="detail" th:href="@{detail(id=${menu.id})}"><img class="menu-img" th:src="@{/image/menu/{img}(img=${menu.img})}" src="/image/menu/12.png"></a>
		                    </div>    
		                    <div class="menu-price">4500 원</div>
		                    <div class="menu-option-list">
		                        <span class="menu-option">
		                            <input class="menu-option-input" type="checkbox">
		                            <label>ICED</label>
		                        </span>            
		                        <span class="menu-option ml-2">
		                            <input class="menu-option-input" type="checkbox">
		                            <label>Large</label>
		                        </span>
		                    </div>
		                    <div class="menu-button-list">
		                        <input class="btn btn-fill btn-size-1 btn-size-1-lg" type="submit" value="담기">
		                        <input class="btn btn-line btn-size-1 btn-size-1-lg ml-1" type="submit" value="주문하기">
		                    </div>
		                </form>
		            </section>
		        </div>
		    </section>

			<div class="d-flex justify-content-center py-3">
				<a href="" class="btn btn-round w-100 w-50-md py-2" style="color:#000;">더보기(13+)</a>
			</div>

			<section class="new-menu menu-section-p">
				<h1 class="d-none">신메뉴 목록</h1>
				<!-- <ul>
                    <li>
                    </li>
                </ul>  -->
				<div class="list">
					<span>신규로 출시된 메뉴가 없습니다.</span>
				</div>
			</section>

		</section>
	</main> 

</html>



7) 비동기 처리 방법(XHR vs Fetch)

  • Fetch API는 JS의 Promise 개념을 이용한다.
  • XHR은 콜백함수를 이용한다.
  • 하지만, XHR는 코드량이 많다. 그래서 Fetch 이용하자!


  • 원래, 스크립트의 동작하는 구간을 브라우저로 한정되어 있어서 벗어낫으면 안 되었다. 이러한 스크립트 코드의 유효박스인 안전한 박스인 ‘샌드박스’(모래놀이터)에서만 사용가능 해야 한다. 벗어나게 해주는 것이 XmlHttpRequest가 있는데 지금은 모든 브라우저에 포함되는 기능이다.


  • MS에서 만든 com component? ‘common’을 의미. 공통으로 사용하게 해준다. 이게 나중에 Active X라고 사용되는데 이것은 모든 코드에서 사용할 수 있었는데 나중에 이를 자바스크립트(원래 안전한 박스인 샌드박스에서만 사용했다.)로 사용가능하게 했다.
    • 이를(Active X) 통해 자바스크립트에서 데이터를 가져오게 했다.
    • 원래 윈도우즈에서만 Active X를 사용했었다. 그래서, 다른 브라우저에서 파이어폭스나 사파리에서 Active X를 사용하게 되었는데 윈도우즈에서 IE에서 포함하다가 IE가 망해버림.


  • MS에서 동접자 문제?(더 찾아보기)
    • MS에서는 다른 브라우저에서 접속하는 경우를 막았어야 했다. 개인정보 때문에 Active X로 막았다. 그래서 우리나라에서는 아직도 관공서에서 아직도 사용한다.
    • 우리나라에서는 개인정보 관련 교육이 더 필요?(아이디를 공유해서)


a. XHR을 이용하는 실습 코드 1

  • 아직은 동기 처리하는 코드이다. 문제점이 많다.


  • AJAX를 이용하기 위해서 JS 규칙 :
    • 1) AJAX에서 addEventListener 쓰지마 : 여러 함수를 쓸 때 사용한다.(함수 누적 시, 사용한다.)**
    • 2) AJAX에서 람다도 쓰지마 : 람다는 지역화를 쓸 수가 없어서!!**
    • 3) 위의 2개는 css의 연장선에서 사용하는 JS에서는 사용가능하다.(버블링 등등)
window.addEventListener("load", function() {
	let ul = document.querySelector(".menu-category>ul");
	
	// 클릭 시 이벤트 요청 :
	// 1) AJAX에서 addEventListener 쓰지마 : 여러 함수를 쓸 때 사용한다.(함수 누적 시, 사용한다.)**
	// 2) AJAX에서 람다도 쓰지마 : 람다는 지역화를 쓸 수가 없어서!!**
	// 3) 위의 2개는 css의 연장선에서 사용가능
	ul.onclick = function(e){
		
		// a태그는 기본 행위가 있어서 그것을 없애준다.
		e.preventDefault(); 
		
		// 이벤트 객체의 요소로서 tagName은 대문자이다.
		let tagName = e.target.tagName;
		
		// if(!(tagName == 'LI' || tagName == 'A'))
		if(tagName != 'LI' && tagName != 'A'){	// li가 아니면 return;(종료)
			return;				// tagName은 반환값이 대문자인 경우가 많다. 
			
		}
	
		// console.log("clicked");
		
		const request = new XMLHttpRequest();
		// open은 브라우저에서 url을 입력하는 것과 같다.
		request.open("GET", "http://localhost:8080/menus?p=1&c=4&q=버", false); 	// 동기 처리를 하면 문제가 많다.
		request.send();
		console.log(request.responseText);	// 여기서는 콜백함수가 필요가 없다.(동기형이라서) 
	}
});



8) 콜백함수 이용**

  • 원래, 메인 스레드는 잠궈버리면 안된다! 아래의 자바 예시 코드에서는 Thread에 sleep을 걸어주었지만 메인 스레드를 잠궈 버리면 안되기 때문에 자바 스크립트에서는 콜백함수를 쓸 수 밖에 없다.
  • 그래서, 자바스크립트에서 비동기 처리하기 위하여 XHR 객체를 이용할 때는 아래 자바 예시 코드의 Thread.sleep을 제거하고 JS 코드에서는 콜백 함수를 이용하여 비동기 처리를 하자!**


a. 콜백 함수 개념

  • 콜백함수는 이벤트를 위임하는 것이다. 미리 설정해놓고 나중에 호출한다.
  • 콜백함수는 비동기 작업이 완료되면 호출한다.**
const request = new XMLHttpRequest();

request.onload = function() {  
	console.log(request.responseText);
}


b. ‘Promise’ 등장 배경(JS ES6)

  • ES5의 자바스크립트는 비동기 처리하는 것이 없었어서 다른 플랫폼에게 이벤트를 위임했었다. 하지만, 자바스크립트는 비동기 처리를 언제 처리하는지 알 수 있는 방도가 없었다. 그래서, JS ES6에서 비동기 처리 방법이 새로 등장했다.
  • 그것이 JS에서 ‘Promise’라는 개념이고 자바스크립트에서 비동기 처리를 해주기 위해서는 ‘Promise’(비동기 처리 방법)가 필요하다.
  • 이것은 비동기 처리하는 방법 중 Fetch API에서 쓰인다.**


c. 콜백함수 실습 코드

  • list.js
window.addEventListener("load", function() {
	let ul = document.querySelector(".menu-category>ul");
	
	// 클릭 시 이벤트 요청 :
	// 1) AJAX에서 addEventListener 쓰지마 : 여러 함수를 쓸 때 사용한다.(함수 누적 시, 사용한다.)**
	// 2) AJAX에서 람다도 쓰지마 : 람다는 지역화를 쓸 수가 없어서!!**
	// 3) 위의 2개는 css의 연장선에서 사용가능
	ul.onclick = function(e){
		
		// a태그는 기본 행위가 있어서 그것을 없애준다.
		e.preventDefault(); 
		
		// 이벤트 객체의 요소로서 tagName은 대문자이다.
		let tagName = e.target.tagName;
		
		// if(!(tagName == 'LI' || tagName == 'A'))
		if(tagName != 'LI' && tagName != 'A'){	// li가 아니면 return;(종료)
			return;				// tagName은 반환값이 대문자인 경우가 많다. 
			
		}
	
		// console.log("clicked");
		
		const request = new XMLHttpRequest();
		// open은 브라우저에서 url을 입력하는 것과 같다.
		
		// 메인 스레드는 잠궈버리면 안된다! 그래서 콜백함수를 사용한다. 
		// 콜백함수는 이벤트를 위임하는 것이다. 미리 설정해놓고 나중에 호출한다. 
		// 비동기 작업이 완료되면 호출한다.**
		// 비동기를 처리해서 따로 빠져버린다!!
		request.onload = function() {  
			console.log(request.responseText);
		}
		
		request.open("GET", "http://localhost:8080/menus?p=1&c=4&q=버", false); 	// 동기 처리를 하면 문제가 많다.
		request.send();
		console.log(request.responseText);	// 여기서는 콜백함수가 필요가 없다.(동기형이라서) 
	


  • MenuController.java

package kr.co.rland.web.controller.api;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.entity.MenuView;
import kr.co.rland.web.service.MenuService;

// API 이용하기 = AJAX


@RestController("apiMenuController")
@RequestMapping("menus")	// 예전에는 절대경로로 썼지만 지금은 "/"를 안써도 된다. 
public class MenuController {
	
	// REST API에서는 반환값이 문서를 전달하는 것이 아니라 사용자가 받는 데이터이다. 
	// List<MenuView>
//	public List<MenuView> getList(){
//		
//	}
	
	@Autowired
	private MenuService service;

	
	// 이전까지는 HTML이 제공해주는 Method만 이용했었다.
	// 이제 HTTP가 제공해주는 API를 쓸 수 있다.
	@GetMapping	// @RequestMapping과 경로가 같다면 여기는 안써도 된다.
	public List<MenuView> getList(
			@RequestParam(name = "p", defaultValue="1") int page, 	
			@RequestParam(name = "c", required=false) Integer categoryId, 	// categoryId는 무조건 사용하는 것이 아니라서 
			@RequestParam(name = "q", required=false) String query) {		// @RequestParam 이용하자!!
		// null을 담기 위해서 required false 설정을 한다. 
		List<MenuView> list = service.getViewList(page, categoryId, query);
		
		// 메인 스레드는 잠궈버리면 안된다! 그래서 콜백함수를 사용한다. 
		// 콜백함수는 이벤트를 위임하는 것이다. 미리 설정해놓고 나중에 호출한다. 
		// 콜백함수는 비동기 작업이 완료되면 호출한다.**
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return list;
	}
	


9) ES6 Promise 준비 과정

a. JSON 처리 방법 :

  • ES6 Promise 개념을 이해하기 위해서 JSON라는 데이터 타입을 다룰줄 알아야 하고 파서하는 방법에 관해 이해가 필요하다.
  • 다음은 JSON.parse()를 이용하는 예시이다.
window.addEventListener("load", function() {
	let ul = document.querySelector(".menu-category>ul");
	
	// 클릭 시 이벤트 요청 :
	// 1) AJAX에서 addEventListener 쓰지마 : 여러 함수를 쓸 때 사용한다.(함수 누적 시, 사용한다.)**
	// 2) AJAX에서 람다도 쓰지마 : 람다는 지역화를 쓸 수가 없어서!!**
	// 3) 위의 2개는 css의 연장선에서 사용가능
	ul.onclick = function(e){
		
		// a태그는 기본 행위가 있어서 그것을 없애준다.
		e.preventDefault(); 
		
		// 이벤트 객체의 요소로서 tagName은 대문자이다.
		let tagName = e.target.tagName;
		
		// if(!(tagName == 'LI' || tagName == 'A'))
		if(tagName != 'LI' && tagName != 'A'){	// LI가 아니면 return;(종료)
			return;				// tagName은 반환값이 대문자인 경우가 많다. 
			
		}
	
		// 데이터 수집하기!!
		let el = tagName === 'LI'?e.target: e.target.parentNode;
		
		// PreviousSibling 개념 (노드 순회)
		
		// 우리는 url에서 데이터를 심어줘야 한다. 이것은 html에서 'dataset'으로 심어준다! 
		// 구글 'mdn dataset' 검색하고 'thymeleaf dataset' 검색 -> th:attr로 심는다.
		let categoryId =1;
		
		
		// 자바스크립트 코드를 외부에서 이용하기 위해서 XMLHttpRequest 객체 이용!!
		const request = new XMLHttpRequest();
		// open은 브라우저에서 url을 입력하는 것과 같다.
		
		// 메인 스레드는 잠궈버리면 안된다! 그래서 콜백함수를 사용한다. 
		// 콜백함수는 이벤트를 위임하는 것이다. 미리 설정해놓고 나중에 호출한다. 
		// 비동기 작업이 완료되면 호출한다.**
		// 비동기를 처리해서 따로 빠져버린다!!
		request.onload = function() {  
			
			// 이렇게 쓰면 JSON 객체에서 출력된다.
			let menus = JSON.parse(request.responseText);
			console.log(menus[0]);	// 일단 같은 메뉴 1개만 출력
			
		}
		
		// 백틱은 템플릿이 가능한 문자이다. 데이터를 여기에 꽂을 것이라서.. 그래서 백틱이 필요하다!!
		request.open("GET", `http://localhost:8080/menus?c=${categoryId}`, true); 	// 동기 처리를 하면 문제가 많다. 그래서 비동기 처리를 하자!
		request.send();
		console.log(request.responseText);	// 아직은 콜백함수가 필요가 없다.(동기형이라서) 하지만, 비동기 처리를 하려면 콜백함수가 필요하다! 
	
	


b. View단에서 심어놓은 데이터를 JS에서 사용하기 :

  • JS에서 자식 노드에서 부모 노드는 1개라서 부모 노드 찾기는 쉽다. 반대로 자식 노드는 여러 개이므로 한 번에 찾기가 힘들다.(여러 조건이 필요)


  • View단에서 심어놓은 데이터를 심기 위해서 html dataset을 이용한다. 하지만, 우리는 thymeleaf를 사용하기 때문에 타임리프에서 사용하는 dataset을 이용하게 된다.


  • 위의 코드에서 let categoryId =1;로 데이터를 상수로 지정했지만, 우리가 원하는 형태는 사용자 입력에 의해서 데이터를 심어 놓고 JS에서는 그 심어놓은 데이터를 사용해야 한다.
    • html에서 data-cid=${c.id}-로 구분지어 dataset에 의해 데이터를 심었었다.
    • 이것을 JS에서는 el라는 변수에 담아서 사용하는데 el.dataset.cid 로 꺼내서 쓰며 html에서 심은 -로 구분된 dataset의 뒷부분 값으로 식별하여 사용한다.


a) HTML의 dataset 개념
  • 아래 예시 코드처럼 html에서 -라는 구분자를 이용하여 dataset을 심고 JS에서 .라는 구분자를 이용하여 dataset을 꺼낸다.
<div id="user" data-id="1234567890" data-user="carinaanand" data-date-of-birth>
  Carina Anand
</div>
const el = document.querySelector("#user");

// el.id === 'user'
// el.dataset.id === '1234567890'
// el.dataset.user === 'carinaanand'
// el.dataset.dateOfBirth === ''


b) 타임리프의 dataset 사용법
  • 타임리프에서는 th:attr 속성을 이용해서 th:attr="data-cid=${c.id}" 이렇게 dataset을 심는다.
  • dataset 심는 방법 : <li th:attr="data-cid=${c.id}" class="" th:class="${#strings.equals(param.c, c.id)}?'menu-selected'" th:each="c: ${categoryList}" >


c) dataset 실습 코드 :
<nav class="menu-category">
	<div>
		<h1 class="text-normal-bold">메뉴분류</h1>
	</div>
	<ul>
		<li class="" th:class="${param.c} ==null ? 'menu-selected'">
			<a href="list">전체</a>
		</li>
		
		<li th:attr="data-cid=${c.id}" class="" th:class="${#strings.equals(param.c, c.id)}?'menu-selected'" 
			th:each="c: ${categoryList}" >
			<a href="?c=1" th:href="@{list(c=${c.id})}" th:text="${c.name}">커피음료</a>
		</li>
	</ul>
</nav>



c. PreviousSibling 개념 (노드 순회)**


  • JSON 데이터 사용하기 위해 우리는 데이터 수집을 해야 한다. 그래서 tagName에서 해당 이벤트 타겟이 없으면 부모노드로 올라가서 전체 객체에서 알맞은 이벤트 타겟을 찾는다!!
    • let el = tagName === 'LI'? e.target: e.target.parentNode;


  • e.target.parentNode를 이해하기 위해서는 PreviousSibling(노드 순회) 개념 필요!!
    • Element(태그들이 객체화 모든 것들), Attr, Document 등등의 집중화된 것이 ‘Node’ 객체이다.
    • PreviousSibling은 같은 Node 형제 계층에서 앞으로 이동하거나 뒤로 이동하여 접근할 수 있는 순회 방법이다.**


d. 전체 실습 코드 + 추가 개념(dataset)


list.js
window.addEventListener("load", function() {
	let ul = document.querySelector(".menu-category>ul");
	
	// 클릭 시 이벤트 요청 :
	// 1) AJAX에서 addEventListener 하지마 : 여러 함수를 쓸 때 사용한다.(함수 누적 시, 사용한다.)**
	// 2) AJAX에서 람다도 쓰지 : 람다는 지역화를 쓸 수가 없어서!!**
	// 3) 위의 2개는 css의 연장선에서 사용가능
	ul.onclick = function(e){
		
		// a태그는 기본 행위가 있어서 그것을 없애준다.
		e.preventDefault(); 
		
		// 이벤트 객체의 요소로서 tagName은 대문자이다.
		let tagName = e.target.tagName;
		
		// if(!(tagName == 'LI' || tagName == 'A'))
		if(tagName != 'LI' && tagName != 'A'){	// li가 아니면 return;(종료)
			return;				// tagName은 반환값이 대문자인 경우가 많다. 
			
		}
	
		// JSON 데이터 사용하기 위해 우리는 데이터 수집을 해야 한다. 타겟이 없으면 부모노드로 올라가서 전체에서 찾는다!!
		let el = tagName === 'LI'? e.target: e.target.parentNode;
		
		// e.target.parentNode를 이해하기 위해서는 PreviousSibling(노드 순회) 개념 필요!!
		// Element(태그들이 객체화 모든 것들), Attr, Document 등등의 집중화된 것이 'Node' 객체이다.
		
		
		
		// 우리는 url에서 데이터를 심어줘야 한다. 이것은 html에서 'dataset'으로 심어준다! 
		// 구글 'mdn dataset' 검색하고 'thymeleaf dataset' 검색 -> th:attr로 심는다.
		let categoryId =1;
		
		
		// 자바스크립트 코드를 외부에서 이용하기 위해서 XMLHttpRequest 객체 이용!!
		const request = new XMLHttpRequest();
		// open은 브라우저에서 url을 입력하는 것과 같다.
		
		// 메인 스레드는 잠궈버리면 안된다! 그래서 콜백함수를 사용한다. 
		// 콜백함수는 이벤트를 위임하는 것이다. 미리 설정해놓고 나중에 호출한다. 
		// 비동기 작업이 완료되면 호출한다.**
		// 비동기를 처리해서 따로 빠져버린다!!
		request.onload = function() {  
			
			// 이렇게 쓰면 JSON 객체에서 출력된다.
			let menus = JSON.parse(request.responseText);
			console.log(menus[0]);			// 같은 메뉴 1개만 출력한다. 
			// menus.forEach(e => console.log(e));	// 이렇게 하면, 클릭 시, 그 카테고리 Id를 가진 모든 리스트 출력 
			
		}
		
		// 백틱은 템플릿이 가능한 문자이다. 데이터를 여기에 꽂을 것이라서.. 그래서 백틱이 필요하다!!
		// request.open("GET", `http://localhost:8080/menus?c=${el.dataset.cid}`, true); 	// 이렇게 하면 JSON으로 심어진 데이터를 확인할 수 있다.
		request.open("GET", `http://localhost:8080/menus?c=${categoryId}`, true); 	// 동기 처리를 하면 문제가 많다. 그래서 비동기 처리를 하자!
		request.send();
		// console.log(request.responseText);	// 아직은 콜백함수가 필요가 없다.(동기형이라서) 하지만, 비동기 처리를 하려면 콜백함수가 필요하다! 
	
	}
});


list.html
<!DOCTYPE html>
<html
	 xmlns=th="http://www.thymeleaf.org"
	 xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
	 layout:decorate="inc/layout"
>	

	<!-- 타임리프는 속성으로 쓰기 때문에 속성으로 쓰기 위해서 div 태그에 사용한다. -->
	<!-- <div th:replace=""></div> -->
	
	<!-- replace는 대체, insert는 div 태그 모양을 유지한다. -->
	
	<main layout:fragment="main">
	
	<!-- script 코드 위치 중요!! -->
	<script src="/js/menu/list.js" defer="defer"></script>
		<section>
			<header class="search-header">
				<h1 class="text-title1-h1" th:text="${data}">알랜드 메뉴</h1>
				<form action="list" method="get">
					<!-- th:value="${param.c}, th:value="${param.q}"를 해줘야지 검색 실행 후 
					검색창에 값이 남아 있는다. id값은 숨기고 검색하려고 한 query 부분만 남긴다.  -->
					<input type="hidden" name="c" th:value="${param.c}">
					<input type="text" name="q" th:value="${param.q}">
					<input type="submit" class="icon icon-find">
				</form>
			</header>
			<aside class="aside-bar">
			
				<h1>aside</h1>
				<section class="aside-bar-content">
					<h1>메인메뉴</h1>
					<ul class="mt-3">
						<li><a href="">카페메뉴</a></li>
						<li><a href="">공지사항</a></li>
						<li><a href="/user/login.html">로그인</a></li>
					</ul>
				</section>
			</aside>
			<nav class="menu-category">
				<div>
					<h1 class="text-normal-bold">메뉴분류</h1>
				</div>
				<ul>
					<li class="" th:class="${param.c} ==null ? 'menu-selected'">
						<a href="list">전체</a>
					</li>
					
					<!-- each문에 url 변경해주기! 중요!! -->
					<li th:attr="data-cid=${c.id}" class="" th:class="${#strings.equals(param.c, c.id)}?'menu-selected'" 
						th:each="c: ${categoryList}" >
						
						<!-- <a href="" th:href="@{/member/list/c(id=${categoryList.id})}" th:text="${categoryList.name}">커피음료</a> -->
						<!-- <a href="?c=1" th:href="@{list(c=${c.id})}" th:text="${c.name}">커피음료</a> -->
						<a href="?c=1" th:href="@{list(c=${c.id})}" th:text="${c.name}">커피음료</a>
					</li>
				</ul>
			</nav>

			<section class="cart-section">
				<h1 class="d-none">장바구니</h1>
				<span class="text-title3">커피음료</span>
				<div class="icon icon-basket icon-text" >1</div>
			</section>

			<section class="menu-section">
		        <h1 class="d-none">메뉴목록</h1>
		        <div class="menu-list">
		        
		            <section class="menu" th:each="menu: ${list}">
		                <form class="">
		                    <h1><span th:text=${menu.name}>알랜드 커피/</span><span style="font-size:11px;" th:text="${menu.categoryName}">(커피음료)</span></h1>
		                    <div class="menu-img-box">
		                        <a href="detail" th:href="@{detail(id=${menu.id})}"><img class="menu-img" th:src="@{/image/menu/{img}(img=${menu.img})}" src="/image/menu/12.png"></a>
		                    </div>    
		                    <div class="menu-price">4500 원</div>
		                    <div class="menu-option-list">
		                        <span class="menu-option">
		                            <input class="menu-option-input" type="checkbox">
		                            <label>ICED</label>
		                        </span>            
		                        <span class="menu-option ml-2">
		                            <input class="menu-option-input" type="checkbox">
		                            <label>Large</label>
		                        </span>
		                    </div>
		                    <div class="menu-button-list">
		                        <input class="btn btn-fill btn-size-1 btn-size-1-lg" type="submit" value="담기">
		                        <input class="btn btn-line btn-size-1 btn-size-1-lg ml-1" type="submit" value="주문하기">
		                    </div>
		                </form>
		            </section>
		        </div>
		    </section>

			<div class="d-flex justify-content-center py-3">
				<a href="" class="btn btn-round w-100 w-50-md py-2" style="color:#000;">더보기(13+)</a>
			</div>

			<section class="new-menu menu-section-p">
				<h1 class="d-none">신메뉴 목록</h1>
				<!-- <ul>
                    <li>
                    </li>
                </ul>  -->
				<div class="list">
					<span>신규로 출시된 메뉴가 없습니다.</span>
				</div>
			</section>

		</section>
	</main> 

</html>


MenuController.java(API용)
package kr.co.rland.web.controller.api;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.entity.MenuView;
import kr.co.rland.web.service.MenuService;

// API 이용하기 = AJAX


@RestController("apiMenuController")
@RequestMapping("menus")	// 예전에는 절대경로로 썼지만 지금은 "/"를 안써도 된다. 
public class MenuController {
	
	// REST API에서는 반환값이 문서를 전달하는 것이 아니라 사용자가 받는 데이터이다. 
	// List<MenuView>
//	public List<MenuView> getList(){
//		
//	}
	
	@Autowired
	private MenuService service;

	
	// 이전까지는 HTML이 제공해주는 Method만 이용했었다.
	// 이제 HTTP가 제공해주는 API를 쓸 수 있다.
	@GetMapping	// @RequestMapping과 경로가 같다면 여기는 안써도 된다.
	public List<MenuView> getList(
			@RequestParam(name = "p", defaultValue="1") int page, 	
			@RequestParam(name = "c", required=false) Integer categoryId, 	// categoryId는 무조건 사용하는 것이 아니라서 
			@RequestParam(name = "q", required=false) String query) {		// @RequestParam 이용하자!!
		// null을 담기 위해서 required false 설정을 한다. 
		List<MenuView> list = service.getViewList(page, categoryId, query);
		
		// 메인 스레드는 잠궈버리면 안된다! 그래서 콜백함수를 사용한다. 
		// 콜백함수는 이벤트를 위임하는 것이다. 미리 설정해놓고 나중에 호출한다. 
		// 콜백함수는 비동기 작업이 완료되면 호출한다.**
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return list;
	}
	
	// 경로를 중간이 {}로 감싸면 경로를 변수화할 수 있다. 
	// 이 id를 찾다가 없으면 쿼리스트링을 찾다가 
	// @PathVariable : Path에서 찾아서 이 변수에 담아달라고 매핑해준다.  
	// return 값에 이렇게 전달된다.
	@GetMapping("{id}")
	public Menu get(
			@PathVariable("id")  int id) {
		
		// Menu menu = service.getById(id);
		Menu menu = service.getById(id);
		
		return menu;	// 객체를 반환할수 없어서 데이터를 반환해줘야 한다.
	}

	@PutMapping			// put도 통째로 바뀌어서 Mapping 경로를 {id}도 없애도 된다.?? 
	public String edit(
			@PathVariable("id") int id) {
		return "menu edit: " + id;
	}
	
	@DeleteMapping("{id}")
	public String delete(
			@PathVariable("id") int id) {
		return "menu del : " + id;
	}
	
	// insert는 아직 넣어줄 데이터가 없어서 문제가 많다.
	@PostMapping
	public String insert() {
		return "menu insert";
	}
	
	
}


MenuController.java(View단 용)
package kr.co.rland.web.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import kr.co.rland.web.entity.Category;
import kr.co.rland.web.entity.Menu;
import kr.co.rland.web.entity.MenuView;
import kr.co.rland.web.entity.RcmdMenuView;
import kr.co.rland.web.service.CategoryService;
import kr.co.rland.web.service.MenuService;
import kr.co.rland.web.service.RcmdMenuService;

// FrontController(POJO 클래스)를 만드는 방법! 
// 클래스는 보통 폴더명이 된다.(기능별로 넣자!)
@Controller
@RequestMapping("/menu")
public class MenuController {
	
	@Autowired
	private MenuService service;
	
	@Autowired
	private CategoryService categoryService;
	
	@Autowired
	private RcmdMenuService rcmdMenuService;
	
	// 함수 이름은 보통 url이 된다.
	@RequestMapping("list")
	public String list(
			@RequestParam(name="p", defaultValue="1") int page,
			@RequestParam(name="c", required=false) Integer categoryId,
			@RequestParam(name="q", required=false) String query,
			Model model) {
		
		List<Category> categoryList = categoryService.getList();
		
		model.addAttribute("categoryList", categoryList);
		
		// 여기서 동작해서 인자를 넣어줘야 한다. 실제 동작하는 부분!!(컨트롤러) 
		List<MenuView> list = service.getViewList(page, categoryId, query);
		
		model.addAttribute("list", list);
		
		// 상대경로이면 타임리프가 알아서 찾아준다. 
		return "menu/list2";
	}
	
	// @RequestMapping에서 detail을 "/detail" 이나 "detail"를 사용하면 된다! 상관없더라
	@RequestMapping("detail")
	public String detail(int id, Model model) {
		// @requestParam은 인자의 변수명이 달라지면 사용하고 이름이 같다면, int로 인자를 바로 받는다. 
		
		
		// 수정 전 코드 : String categoryName = categoryService.getNameByMenuId(id);
		
		
		// 집계가 필요하면, MenuView가 필요하다. 하지만, entity를 따로 만들 수도 있다.
		int cartCount = 10;
		Menu menu = service.getById(id);
		
		// 수정 후 코드 : 카테고리 Id를 가지고 올 때는 CategoryService에 붙어잇는 카테고리 ID를 가지고 와야 한다. 
		// 카테고리 ID만 가지고 오면 붙어있는 Menu 테이블의 칼럼 중 데이터를 가져 올 수 있다. 제일 중요!!
		String categoryName	= categoryService.getNameById(menu.getCategoryId());
		// 메서드가 2번 걸치기 때문에 getNameById에 넣어 줄 때,
		// Menu 엔티티에서 Java에서는 칼럼명이 categoryId로 쓰이는데 **
		// DB에서는 category_id 이렇게 쓰여서 Mapper에서 ResultMap 설정 해주기!!**
		

		// service 계층에서 받아와서 view 단으로 던져줘서 view 단에서 출력 한다.
	
		List<RcmdMenuView> rcmdMenuList = rcmdMenuService.getViewListByMenuId(id);
		
		model.addAttribute("menu", menu);
		model.addAttribute("rcmdMenuList", rcmdMenuList);
		model.addAttribute("categoryName",categoryName);
		
		return "menu/detail2";
	}

}