1. 타임리프 사용법 : 230320
1) 타임리프에서 link 태그를 사용해서 url 생성
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 이스케이프”로 인코딩되었기 때문에 이를 인코딩 되지 않고 나오게 해야한다.(
<
가<
로 인코딩)
- 해결 방법 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에 담아서 전달해준다. 표현 헤더는 표현 데이터를 해석할 수 있는 정보를 제공해준다.
- 예를 들어, 우리는 회원이라는 정보를 HTML로 ‘표현’을 한 것을 보게 된다. 우리는 회원이라는 리소스를 DB의 실제 데이터가 HTTP로 전송이 될 때, HTML으로 정보가 ‘표현’이 되거나 JSON으로도 정보가 ‘표현’될 수도 있어서 실제 전달하는 것을
- REST API에서
@GetMapping
은 자바스크립트가 요청한다. 그래서, 우리는 자바스크립트 코드가 필요하다.
1) PostMan 사용법
-
1) 새로운
Collection
에서add Request
를 눌러서 설정! -
2)
add Request
의 이름도 설정하고Save 버튼
을 눌러서 저장하기. -
3)
Method
와 반환 받고 싶은url
을 정하여 입력한다. -
4)
Send 버튼
의 결과를 반환하기 위해서 스프링 부트에서@GetMapping
등 관련 코드를 입력하고 스프링 부트의 서버를 킨다. -
5)
POSTMAN
에서 해당Method
와URL
에 맞게 설정하고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의 뒷부분 값으로 식별하여 사용한다.
- html에서
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";
}
}