상품조회
상품조회(검색)
➡️ 상품 목록 화면을 만든다.
➡️ 1) 상품을 검색할 수 있는 폼을 만든다. (get방식)
2) th:fragment으로 레이아웃 조각을 만든 후 상품 목록 화면에 붙여준다.
🗣 조각을 붙이는 방법
👉🏻 insert vs replace
- insert : 정의된 조각을 삽입하는 방법
ex) <div><div th:insert=”~{페이지 :: 조각이름}”></div></div> 렌더링 시 → <div><div>조각</div></div>
- replace : 선언한 태그와 관련없이 조각으로 교체한다.
ex) <div><div th:replace=”~{페이지 :: 조각이름}”></div></div> 렌더링 시 → <div>조각</div>
[코드]search.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <div th:fragment="searchFragment2"> <form th:action="@{/goods/goodsList}" method="get"> <select name="searchKey"> <option value="goodsCode">상품코드</option> <option value="goodsName">상품명</option> <option value="sellerId">판매자아이디</option> <option value="sellerName">판매자이름</option> <option value="sellerEmail">판매자이메일</option> </select> <input type="text" name="searchValue" placeholder="검색어를 입력해주세요."> <button type="submit">검색</button> </form> </div> </html>
[코드]goodsList.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/default}"> <head> <link rel="stylesheet" type="text/css" th:href="@{/css/table.css}"> </head> <body> <th:block layout:fragment="customContents"> // 컨텐츠 레이아웃 조각내에 searchFragment2 조각 삽입 <div th:insert="~{fragments/search :: searchFragment2}"></div> <br><br> <table> <thead> <tr> <th>상품코드</th> <th>상품명</th> <th>상품가격</th> <th>판매자아이디</th> <th>판매자이름</th> <th>판매자이메일</th> <th>상품등록날짜</th> <th>수정</th> <th>삭제</th> </tr> </thead> <tbody> /* 타임리프 문법으로 그려질 화면 */ </tbody> </th:block> </body> </html>
➡️ 상품목록 화면을 보여주기 위해 주소요청을 한다. (goodsList.html 생성)
➡️ 화면목록조회 메소드의 매개변수에 데이터를 전달 받는다.
[코드]Controller.java
package ex.mybatis.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; @Controller @Slf4j public class GoodsController { @GetMapping("/goodsList") public String goodsList(Model model, @RequestParam(value = "searchKey", required = false, defaultValue = "") String searchKey, @RequestParam(value = "searchValue", required = false) String searchValue) { log.info("searchKey : {}", searchKey); log.info("searchValue : {}", searchValue); model.addAttribute("title", "상품목록조회"); return "goods/goodsList"; } }
➡️ 상품정보를 setter, getter하기 위한 Goods dto를 생성해준다.
👉🏻 상품 dto에 member 정보를 가져오고 싶을때
- 상품에서 바라 봤을 때 회원과의 관계는 1:1 관계이다.
→ private Member sellerInfo (멤버변수 추가)
[코드]Goods.java
package ex.mybatis.dto; import lombok.Data; @Data public class Goods { private String goodsCode; private String goodsName; private int goodsPrice; private String goodsSellerId; private String goodsRegDate; // 판매자 정보 (1대1 관계) private Member sellerInfo; }
➡️ 검색 조건에 따른 상품코드, 이름, 가격, 판매자 아이디, 판매자 이름, 판매자 이메일, 등록일자를 조회하는 쿼리를 작성한다. (JOIN 사용)
➡️ 조인을 했을 때 조회하는 컬럼이 상품과 회원이 둘다 필요할 때, 상품 resultMap에 member 객체를 association 으로 1:1 관계 맵핑을 해준다.
👉🏻 Mybatis JOIN 관계 맵핑
- association : “has-one” 타입의 관계 (1:1)
- collection : “has-many” 타입의 관 (1:N)
👉🏻 Mybatis JOIN 관계 맵핑시 속성 종류
- fetchSize : 조회 시 한번에 읽을 수 있는 레코드 수 조정
- property : 결과 컬럼에 매핑하기 위한 DTO의 필드나 프로퍼티
- column : 데이터베이스의 컬럼명이나 별칭된 컬럼 라벨
- javaType : 패키지 경로를 포함한 클래스명
- jdbcType : 지원되는 타입 목록에서
[코드]Mapper.java
package ex.mybatis.mapper; import ex.mybatis.dto.Goods; import org.apache.ibatis.annotations.Mapper; import java.util.List; import java.util.Map; @Mapper public interface GoodsMapper { // 상품목록 조회 public List<Goods> getGoodsList(Map<String, Object> paramMap); }
[코드]Mapper.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="ex.mybatis.mapper.GoodsMapper"> <resultMap id="goodsResultMap" type="Goods"> <id column="g_code" property="goodsCode"/> <result column="g_name" property="goodsName"/> <result column="g_price" property="goodsPrice"/> <result column="g_seller_id" property="goodsSellerId"/> <result column="g_reg_date" property="goodsRegDate"/> <!-- 관계맵핑 1:1 association --> <association property="sellerInfo" javaType="Member"> <!-- id태그는 조회시 테이블의 PK(기본키) JOIN 키(외래키) = 키(기본키) --> <id column="m_id" property="memberId"/> <!-- result태그는 조회시 테이블의 일반컬럼 혹은 외래키 --> <result column="m_pw" property="memberPw"/> <result column="m_name" property="memberName"/> <result column="m_level" property="memberLevel"/> <result column="m_email" property="memberEmail"/> <result column="m_addr" property="memberAddr"/> <result column="m_reg_date" property="memberRegDate"/> </association> </resultMap> <select id="getGoodsList" parameterType="map" resultMap="goodsResultMap"> /* 상품 목록 조회 */ SELECT g.g_code, g.g_name, g.g_price, g.g_seller_id, m.m_name, m.m_email, g.g_reg_date FROM tb_goods AS g INNER JOIN tb_member AS m ON g.g_seller_id = m.m_id <where> <if test="searchValue != null and searchValue != ''"> ${searchKey} LIKE CONCAT('%', #{searchValue}, '%'); </if> </where> </select> </mapper>
➡️ 1) Controller에서 넘겨받은 데이터를 매개변수로 받는 메소드를 생성한다.
2) 데이터를 담을 map 구조를 생성한다.
3) searchValue가 null이 아닐 때, searchKey의 값을 DB 컬럼명과 일치시키도록 로직을 작성한다.
4) map에 searchKey, searchValue 값을 담아서 Mapper에 선언한 메소드 매개변수에 넣어주고, 호출한다.
5) mapper에서 받은 값을 리턴해준다.
[코드]Service.java
package ex.mybatis.service; import ex.mybatis.dto.Goods; import ex.mybatis.mapper.GoodsMapper; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.List; import java.util.Map; @Service @AllArgsConstructor @Slf4j public class GoodsService { private final GoodsMapper goodsMapper; public List<Goods> getGoodsList(String searchKey, String searchValue) { Map<String, Object> paramMap = null; if(searchValue != null) { switch (searchKey) { case "goodsCode" -> { searchKey = "g.g_code"; } case "goodsName" -> { searchKey = "g.g_name"; } case "sellerId" -> { searchKey = "g.g_seller_id"; } case "sellerName" -> { searchKey = "m.m_name"; } case "sellerEmail" -> { searchKey = "m.m_email"; } } paramMap = new HashMap<String, Object>(); paramMap.put("searchKey", searchKey); paramMap.put("searchValue", searchValue); } List<Goods> goodsList = goodsMapper.getGoodsList(paramMap); log.info("goodsList : {}", goodsList); return goodsList; } }
➡️ 1) 기존 코드에서 service에서 선언한 메소드를 호출한다. (매개변수 searchKey, searchValue)
2) 리턴값을 model에 담아서 화면쪽으로 넘겨준다.
[코드]Controller.java
package ex.mybatis.controller; import ex.mybatis.dto.Goods; import ex.mybatis.service.GoodsService; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; @Controller @Slf4j @AllArgsConstructor @RequestMapping("/goods") public class GoodsController { private final GoodsService goodsService; @GetMapping("/goodsList") public String goodsList(Model model, @RequestParam(value = "searchKey", required = false, defaultValue = "") String searchKey, @RequestParam(value = "searchValue", required = false) String searchValue) { log.info("searchKey : {}", searchKey); log.info("searchValue : {}", searchValue); List<Goods> goodsList = goodsService.getGoodsList(searchKey, searchValue); model.addAttribute("title", "상품목록조회"); model.addAttribute("goodsList", goodsList); return "goods/goodsList"; } }
➡️ 전달받은 값을 타임리프 문법을 사용해서 화면에 출력한다.
[코드]goodsList.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/default}"> <head> <link rel="stylesheet" type="text/css" th:href="@{/css/table.css}"> </head> <body> <th:block layout:fragment="customContents"> <!--/* 검색(searchFragment2 select:상품코드, 상품명, 판매자 아이디, 판매자 이름, 판매자 이메일 */--> <!--/* 검색 주소: /goods/goodsList, HTTP method: get */--> <th:block th:insert="~{search/search :: searchFragment2}"> </th:block> <br><br> <table> <thead> <tr> <th>상품코드</th> <th>상품명</th> <th>상품가격</th> <th>판매자아이디</th> <th>판매자이름</th> <th>판매자이메일</th> <th>상품등록날짜</th> <th>수정</th> <th>삭제</th> </tr> </thead> <tbody> <tr th:unless="${#lists.isEmpty(goodsList)}" th:each="l : ${goodsList}"> <td th:text="${l.goodsCode}"></td> <td th:text="${l.goodsName}"></td> <td th:text="${l.goodsPrice}"></td> <td th:text="${l.goodsSellerId}"></td> <th:block th:object="${l.sellerInfo}"> <td th:text="${l.sellerInfo.memberName}"></td> <td th:text="*{memberEmail}"></td> </th:block> <td th:text="${l.goodsRegDate}"></td> <td> <a th:href="@{#}">수정</a> </td> <td> <a th:href="@{#}">삭제</a> </td> </tr> <tr th:if="${#lists.isEmpty(goodsList)}"> <td colspan="9">등록된 상품이 없습니다.</td> </tr> </tbody> </table> </th:block> </body> </html>
상품등록
상품등록화면
➡️ 상품등록 화면으로 주소 요청하는 메소드를 생성한다.
[코드]Controller.java
/** * 상품등록화면 * @param model * @return */ @GetMapping("/addGoods") public String addGoods(Model model) { model.addAttribute("title", "상품등록화면"); return "goods/addGoods"; }
회원 등급 별 회원 목록을 조회하는 쿼리를 작성한다.
➡️ 1) 회원의 정보를 가져와야 하니까 memberMapper에서 쿼리를 작성한다. (goodsMapper에서 해도 되는데 불필요한 데이터가 쌓여서 좋지 않다.)
2) 판매자의 권한 번호는 2로 고정되어 있다.
[코드]MemberMapper.java
// 등급별 회원목록 조회 public List<Member> getMemberListByLevel(int memberLevel);
[코드]MemberMapper.xml
<select id="getMemberListByLevel" parameterType="int" resultMap="memberResultMap"> /* 등급별 회원목록 조회 */ SELECT m_id, m_name, m_level, m_email, m_addr, m_reg_date FROM tb_member WHERE m_level = #{memberLevel} </select>
➡️ 1) MemberMapper에서 선언한 메소드를 GoodsService에서 호출한다.
2) 호출 시에 매개변수에 int타입의 숫자 2(판매자)를 넣는다.
[코드]GoodsService.java
/** * 판매자 회원정보 조회 * @return */ public List<Member> getSellerInfoList() { List<Member> sellerInfoList = memberMapper.getMemberListByLevel(2); return sellerInfoList; }
➡️ 판매자 정보가 담긴 데이터를 model에 담아서 화면쪾으로 전달한다. (판매자 아이디와 이름을 화면에 보이도록 하기 위해서)
[코드]Controller.java
/** * 상품등록화면 * @param model * @return */ @GetMapping("/addGoods") public String addGoods(Model model) { List<Member> sellerInfoList = goodsService.getSellerInfoList(); log.info("sellerInfoList : {}", sellerInfoList); model.addAttribute("title", "상품등록화면"); model.addAttribute("sellerInfoList", sellerInfoList); return "goods/addGoods"; }
➡️ 전달받은 값을 타임리프 문법을 통해서 화면에 출력한다.
[코드]addGoods.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/default}"> <head> <link rel="stylesheet" type="text/css" th:href="@{/css/table.css}"> <style> input { width: 98%; height: 23px; } select { width: 98%; text-align: center; height: 23px; } #addGoodsBtn, #resetBtn { width: 49%; height: 25px; } </style> </head> <th:block layout:fragment="customJs"> <script th:src="@{/js/jquery-3.7.0.js}"></script> <script> </script> </th:block> <th:block layout:fragment="customContents"> <form id="addGoodsForm" th:action="@{/goods/addGoods}" method="post"> <table> <tbody> <tr> <td> <label for="sellerName">판매자</label> </td> <td> <select name="goodsSellerId" id="sellerName"> <option value="">===판매자를 선택해주세요.===</option> <option th:unless="${#lists.isEmpty(sellerInfoList)}" th:each="l : ${sellerInfoList}" th:value="${l.memberId}" th:text="|${l.memberId} :::: ${l.memberName}|"> </option> </select> </td> </tr> <tr> <td> <label for="goodsName">상품명</label> </td> <td> <input type="text" id="goodsName" name="goodsName" placeholder="상품명을 입력해주세요."> </td> </tr> <tr> <td> <label for="goodsPrice">상품가격</label> </td> <td> <input type="number" id="goodsPrice" name="goodsPrice" placeholder="상품가격을 입력해주세요."> </td> </tr> <tr> <td colspan="2"> <button type="button" id="addGoodsBtn">상품등록</button> <button type="reset" id="resetBtn">입력취소</button> </td> </tr> </tbody> </table> </form> </th:block> </html>
상품등록처리
➡️ 상품화면에서 상품등록 버튼을 눌렀을 때 유효성 검사와 검사가 끝났을 경우 폼을 전송하는 자바스크립트 코드를 작성한다.
[코드]addGoods.html
<th:block layout:fragment="customJs"> <script th:src="@{/js/jquery-3.7.0.js}"></script> <script> // 유효성 검사 함수 선언 const validCheck = function (ele) { let eleId = $(ele).attr('id'); let eleValue = $(ele).val(); if(typeof eleValue == 'undefined' || eleValue == null || eleValue == ''){ let msg = $(`label[for="${eleId}"]`).text(); alert(`${msg} 항목을 입력해주세요.`); $(ele).focus(); return false; } return true; } // 버튼 클릭 시 이벤트 $('#addGoodsBtn').click(function () { const validEles = $('#addGoodsForm select,input'); let isSubmit = false; console.log(validEles); validEles.each((idx, item) => { isSubmit = validCheck(item); return isSubmit; }); if (isSubmit) $('#addGoodsForm').submit(); }); </script> </th:block>
➡️ 1) 상품등록 화면에서 폼을 전송할 때 → <form id="addGoodsForm" th:action="@{/goods/addGoods}" method="post">
2) 폼 action 경로가 /goods/addGoods 이므로 PostMapping 시 해당 경로로 주소요청을 한다.
3) Post로 화면처리를 하는 메소드를 생성하고, 리턴으로 상품목록화면으로 리다이렉트 해준다.
[코드]Controller.java
@PostMapping("/addGoods") public String addGoods() { return "redirect:/goods/goodsList"; }
➡️ 상품을 등록할 때 상품코드를 자동으로 생성되도록 쿼리를 작성한다.
- CONCAT('g', LPAD(MAX(CAST(SUBSTRING_INDEX(g_code, 'g', -1) AS UNSIGNED))+1, 3, '0'))
1) SUBSTRING_INDEX(g_code, ‘g’ -1) → 뒤에서부터 g를 찾아서 자른다.
2) CAST((…) AS UNSIGNED) → UNSIGNED(부호를 없애고, int값으로 변환)
3) MAX(…) + 1 → 최댓값에서 +1을 해준다.
4) LPAD( (…), 3, ‘0’) → 왼쪽에 3자리를 유지하는데 빈값에 0을 채운다.
5) CONCAT(’g’, … ) → 문자열을 합한다.
👉🏻 CASE ~ END 구문
CASE문은 여러 조건을 비교할 수 있다.
- (CASE
WHEN (조건) THEN ‘결과’
…
ELSE ‘결과’
END) AS 별칭
👉🏻 selectKey
쿼리문에서 Insert 시 자동 생성키를 별로의 SQL 수행 없이 바로 사용하고 싶을 때 사용한다.
- resultType : 결과 타입
- keyColumn : 리턴되는 결과의 컬럼명
- keyProperty : 구문 결과가 셋팅될 대상의 프로퍼티 명
- order : BEFORE(insert 쿼리 실행 전 selectKey 실행), AFTER(insert 쿼리 실행 후 selectKey 실행)
👉🏻 <![CDATA[ … ]]>
CDATA 안에 쿼리를 사용하면 쿼리 내용의 괄호나 특수문자를 XML parser로 인식하지 않고. "문자열"로 인식한다.
[코드]GoodsMapper.xml
<insert id="addGoods" parameterType="Goods"> <selectKey resultType="String" keyColumn="newGoodsCode" keyProperty="goodsCode" order="BEFORE"> <![CDATA[ /* 상품자동증가코드 */ SELECT CASE WHEN COUNT(*) = 0 THEN 'g001' WHEN CAST(SUBSTRING_INDEX(g_code, 'g', -1) AS UNSIGNED) < 999 THEN CONCAT('g', LPAD(MAX(CAST(SUBSTRING_INDEX(g_code, 'g', -1) AS UNSIGNED))+1, 3, '0')) ELSE CONCAT('g', MAX(CAST(SUBSTRING_INDEX(g_code, 'g', -1) AS UNSIGNED))+1) END AS newGoodsCode FROM tb_goods; ]]> </selectKey> /* 상품등록 */ INSERT INTO tb_goods (g_code, g_name, g_price, g_seller_id, g_reg_date) VALUES (#{goodsCode}, #{goodsName}, #{goodsPrice}, #{goodsSellerId}, CURDATE()) </insert>
[코드]GoodsMapper.java
// 상품등록 public int addGoods(Goods goods);
➡️ 상품 등록을 위한 로직을 처리하는 메소드를 생성 후 Mapper를 호출한다. (Mapper 호출 전/후를 비교한다.)
- 결과값이 없어서 리턴이 없는 메소드로 생성
[코드]GoodsService.java
public void addGoods(Goods goods) { // goods 객체에 현재 상품코드가 없다 log.info("insert 전 goods : {}", goods); goodsMapper.addGoods(goods); // goods 객체에 상품코드가 있다. log.info("insert 후 goods : {}", goods); }
➡️ 기존 post로 처리하는 메소드안에 service를 호출한다.
[코드]GoodsController.java
@PostMapping("/addGoods") public String addGoods(Goods goods) { log.info("goods : {}", goods); goodsService.addGoods(goods); return "redirect:/goods/goodsList"; }
tag : #Spring #Mybatis #Controller #Service #Mapper #selectKey #CDATA #CASE
Uploaded by N2T