본문 바로가기
기초 및 언어/▶ Spring

07. Spring_마이바티스(MyBatis) + 페이지 설정

by 류딩이 2025. 9. 24.

🌈목차

 


1. 기본준비 코드

 

🔽 application.properties에 작성 -- db연결

spring.datasource.url=jdbc:oracle:thin:@localhost:1521:orcl
spring.datasource.username=sqlid
spring.datasource.password=sqlpw
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

mybatis.mapper-locations=classpath:mapper/*.xml

 

🔽 유효성검사 <dependency>

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
</dependency>


🔽 <properties> 아래에 추가  (오라클)

<repositories>
        <repository>
            <id>oracle</id>
            <url>https://repo.maven.apache.org/maven2</url>
        </repository>
</repositories>

 

🔽 <dependency> 아래에 추가

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.5</version> 
</dependency>

 

🔽 마이바티스 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="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

⬆ 목차로


🌈2. 전체코드 Ex08-Travel

 

📁com.example.Ex02

 

📁 controller

더보기
package com.example.Ex02.controller;

import com.example.Ex02.dto.TravelDto;
import com.example.Ex02.mapper.TravelMapper;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Controller
public class TravelController {
    @Autowired
    private TravelMapper travelMapper;

    // 삽입폼으로 이동
    @GetMapping(value="/insert")
    public String form(@ModelAttribute("tDto")TravelDto tDto) {
        
        return "insert";
    }

    // 삽입 처리
    @PostMapping(value = "/insertProc")
    public String insert(@ModelAttribute("tDto") @Valid TravelDto tDto, BindingResult br){

        String resultPage;
        if (br.hasErrors()) {
            resultPage = "insert";  // 유효성 검사 실패 시 다시 입력 폼으로
        } else {
            travelMapper.insertTravel(tDto);
            resultPage = "redirect:/list";
        }
        return resultPage;
    }

	// 전체조회 list + 검색 입력(whatColumn, keyword) +  페이징
    @GetMapping(value = "/list")
    public String list(Model model,
                       // required = false : 작성하지 않으면 반드시 값을 넘겨야함
                       // 값을 넘길때가 있고 안넘길때가 있을때 작성
                       @RequestParam(value = "whatColumn", required = false) String whatColumn,
                       @RequestParam(value = "keyword", required = false) String keyword,
                       @RequestParam(value = "page",defaultValue = "1") int page) /*페이지가 안넘어오면 디폴트= 1*/
    {



        // 기본 페이징 설정
        int limit=3; // 한 페이지에 보여줄 레코드 수
        int offset= (page-1) * limit;
        /*page=1, offset=0
        * page=2, offset=3
        * page=3, offset=6
        * */
        

        // MyBatis 에 전달할 파라미터 맵
        Map<String, Object> params = new HashMap<String, Object>(); //key, value
        params.put("whatColumn", whatColumn); 
        params.put("keyword", keyword);
        params.put("offset", offset);  
        params.put("limit", limit); 

        // (1) 페이징된 리스트 조회
        List<TravelDto>lists = travelMapper.selectAll(params);

        // (2) 전체 검색된 레코드 개수 조회
        int totalCount = travelMapper.getCount(params);

        // (3) 전체 페이지 수 계산
        int totalPage = (int) Math.ceil((double)totalCount / limit);

        // 뷰로 전달할 데이터 세팅
        model.addAttribute("lists", lists);          
        model.addAttribute("page", page);          
        model.addAttribute("totalCount", totalCount); 
        model.addAttribute("totalPage", totalPage); 
        model.addAttribute("keyword", keyword);    
        model.addAttribute("whatColumn", whatColumn); 
		// 주의: 뷰 쪽 링크들에 whatColumn/keyword 같이 전달 안 하면 검색 유지 안 됨

        return "list";
    }

    // 상세조회 이동
    @GetMapping(value = "/content_view")
    public String content_view(@RequestParam("num") int num, Model model){
        TravelDto tDtd = travelMapper.findByNum(num);
        model.addAttribute("num", num);
        model.addAttribute("tDtd", tDtd);
        return "content_view";
    }

    // 수정폼 이동
    @GetMapping(value = "/update")
    public String update(@RequestParam("num") int num,
                         @RequestParam(value = "whatColumn", required = false) String whatColumn,
                         @RequestParam(value = "keyword", required = false) String keyword,
                         @RequestParam(value = "page",defaultValue = "1") int page, /*페이지가 안넘어오면 디폴트= 1*/
                         Model model){

        model.addAttribute("page",page);
        model.addAttribute("whatColumn",whatColumn);
        model.addAttribute("keyword",keyword);

        TravelDto tDto = travelMapper.findByNum(num);   
        model.addAttribute("num", num);
        model.addAttribute("tDto", tDto);
        
        // 뷰에서 사용할 선택지 배열 전달
        model.addAttribute("areaArr", List.of("유럽", "동남아", "일본", "중국"));
        model.addAttribute("typeArr", List.of("패키지", "크루즈", "자유여행", "골프여행"));
        model.addAttribute("priceArr", List.of("100~200", "200~300", "300~400"));

        return "update";
    }

    // 수정처리
    @PostMapping(value = "/updateProc")
    public String updateproc(@ModelAttribute("tDto") @Valid TravelDto tDto, BindingResult br, Model model,
                             @RequestParam(value = "whatColumn", required = false) String whatColumn,
                             @RequestParam(value = "keyword", required = false) String keyword,
                             @RequestParam(value = "page",defaultValue = "1") int page){

        System.out.println("updateProc whatColumn : " + whatColumn);


        /* 입력에 실패했을 때 update폼에 다시 렌더링 되기 위함 */
        model.addAttribute("areaArr", List.of("유럽", "동남아", "일본", "중국"));
        model.addAttribute("typeArr", List.of("패키지", "크루즈", "자유여행", "골프여행"));
        model.addAttribute("priceArr", List.of("100~200", "200~300", "300~400"));
        model.addAttribute("page",page);
        model.addAttribute("whatColumn",whatColumn);
        model.addAttribute("keyword",keyword);

        String resultPage="";
        if (br.hasErrors()) {
            resultPage = "update";
        } else {
            travelMapper.updateTravel(tDto); // 검색 키워드가 있을 경우 URL 인코딩
            String encodeKeyword = keyword != null ? URLEncoder.encode(keyword, StandardCharsets.UTF_8) : "";
            resultPage = "redirect:/list?page="+page+"&whatColumn="+whatColumn+"&keyword="+encodeKeyword;
        }
        return resultPage;
    }

	// 삭제 처리
    @GetMapping(value = "/delete")
    public String delete(@RequestParam("num") int num, Model model,
                         @RequestParam(value = "whatColumn", required = false) String whatColumn,
                         @RequestParam(value = "keyword", required = false) String keyword,
                         @RequestParam(value = "page",defaultValue = "1") int page){


        model.addAttribute("page",page);
        model.addAttribute("whatColumn",whatColumn);
        model.addAttribute("keyword",keyword);


        // 삭제 실행 sql문
        travelMapper.deleteTravel(num);


		// 삭제 후에도 페이징 유지 + 페이지 보정
        int limit=3; // 한 페이지 크기 (list 메서드랑 동일해야 함)
        int offset= (page-1) * limit;
        Map<String, Object> params = new HashMap<String, Object>();
        params.put("whatColumn", whatColumn);
        params.put("keyword", keyword); 
        params.put("offset", offset);  
        params.put("limit", limit); 

 
 		// 삭제했을때 그 페이지에 레코드가 0이면 앞페이지로 이동
 		int totalCount = travelMapper.getCount(params);
        if(totalCount % limit == 0) {
            page = page - 1;
        }

        String resultPage="";
        String encodeKeyword = keyword != null ? URLEncoder.encode(keyword, StandardCharsets.UTF_8) : "";
        resultPage = "redirect:/list?page="+page+"&whatColumn="+whatColumn+"&keyword="+encodeKeyword;
        return resultPage;
    }

}

 📁 TravelDto (Dto)

더보기
package com.example.Ex02.dto;

import jakarta.validation.constraints.*;

import java.util.Arrays;
import java.util.List;

public class TravelDto {

    private int num;

    @NotBlank(message = "이름 누락")
    private String name;

    @Min(value = 1, message = "나이는 최소 1살 이상이어야 합니다")
    @Max(value = 120, message = "나이는 최대 120살 이하여야 합니다")
    private int age;

    @NotEmpty(message = "관심지역은 하나이상 선택해야 합니다.")
    private List<String> area;

    @NotEmpty(message = "원하는 여행 타입을 선택해주세요")
    private String style;

    @NotEmpty(message = "원하는 가격을 선택하세요")
    private String price;

    private String areaAsString;

    public String getAreaAsString() {
        return (area !=null)? String.join(",", area) : null;
    }

    public void setAreaAsString(String areaAsString) {
        this.area = Arrays.asList(areaAsString.split(","));
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public List<String> getArea() {
        return area;
    }

    public void setArea(List<String> area) {
        this.area = area;
    }

    public String getStyle() {
        return style;
    }

    public void setStyle(String style) {
        this.style = style;
    }

    public String getPrice() {
        return price;
    }

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

 

 📁 mapper (인터페이스)

더보기
package com.example.Ex02.mapper;

import com.example.Ex02.dto.TravelDto;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;
import java.util.Map;

@Mapper
public interface TravelMapper {
    void insertTravel(TravelDto tDto);
    List<TravelDto> selectAll(Map<String, Object> map);
    /*List<TravelDto> selectAll();*/
    TravelDto findByNum(int num);
    void updateTravel(TravelDto tDto);
    void deleteTravel(int num);

    int getCount(Map<String, Object> map);
}

 

🗂️Resources

 

📁 mapper.xml

 

 <!-- #{} = 값 바인딩 자리-->
<!-- ${} = SQL 구문에 그대로 삽입 (컬럼명/테이블명 동적 처리)-->

더보기
<?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="com.example.Ex02.mapper.TravelMapper">

  <resultMap id="resultMapTravel" type="com.example.Ex02.dto.TravelDto">
    <id property="num" column="num"/>
    <result property="name" column="name"/>
    <result property="age" column="age"/>
    <result property="areaAsString" column="area"/>
    <result property="style" column="style"/>
    <result property="price" column="price"/>
  </resultMap>

  <!-- 삽입 -->
  <insert id="insertTravel" parameterType="com.example.Ex02.dto.TravelDto">
    INSERT INTO travel
    VALUES(travel_seq.nextval, #{name}, #{age}, #{areaAsString}, #{style}, #{price})
  </insert>

  <!-- 검색 + 페이징 조회 -->
  <select id="selectAll" resultMap="resultMapTravel">
    SELECT * FROM travel
    <where>
      <choose>
        <!-- 전체 검색 (모든 열에서 검색) -->
        <when test="whatColumn == 'all' and keyword != null and keyword != ''">
          style LIKE '%' || #{keyword} || '%'
          OR area LIKE '%' || #{keyword} || '%'
        </when>

        <!-- 특정 컬럼 검색 -->
        <when test="whatColumn != null and whatColumn != 'all' and keyword != null and keyword != ''">
          ${whatColumn} LIKE '%' || #{keyword} || '%'
        </when>
      </choose>
    </where>

    ORDER BY num DESC
    OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY
  </select>

  <!-- 검색된 레코드 개수 (count) -->
  <select id="getCount" resultType="int">
    SELECT COUNT(*)
    FROM travel
    <where>
      <choose>
        <when test="whatColumn == 'all' and keyword != null and keyword != ''">
          style LIKE '%' || #{keyword} || '%'
          OR area LIKE '%' || #{keyword} || '%'
        </when>
        <when test="whatColumn != null and whatColumn != 'all' and keyword != null and keyword != ''">
          ${whatColumn} LIKE '%' || #{keyword} || '%'
        </when>
      </choose>
    </where>
  </select>

  <!-- 상세 조회 -->
  <select id="findByNum" resultMap="resultMapTravel">
    SELECT * FROM travel WHERE num = #{num}
  </select>

  <!-- 수정 -->
  <update id="updateTravel" parameterType="com.example.Ex02.dto.TravelDto">
    UPDATE travel
    SET name = #{name},
        age = #{age},
        area = #{areaAsString},
        style = #{style},
        price = #{price}
    WHERE num = #{num}
  </update>

  <!-- 삭제 -->
  <delete id="deleteTravel">
    DELETE FROM travel WHERE num = #{num}
  </delete>

</mapper>

📁templates

 📁 insert.html

더보기
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>여행 정보 입력</title>
    <style>
        .err { color: red; }
        h2 {
           margin: auto;
           text-align: center;
       }

       table {
           width: 800px;
           border-collapse: collapse;
           margin: auto;
       }
        th{
             text-align: center;
        }
    </style>
</head>
<body>
<h2>여행 정보 입력</h2>
<form th:action="@{/insertProc}" th:object="${tDto}" method="post">
    <table border="1" width="500">
        <tr>
            <th>이름</th>
            <td>
                <input type="text" th:field="*{name}"><br>
                <div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="err"></div>
            </td>
        </tr>
        <tr>
            <th>나이</th>
            <td>
                <input type="number" th:field="*{age}"><br>
                <div th:if="${#fields.hasErrors('age')}" th:errors="*{age}" class="err"></div>
            </td>
        </tr>
        <tr>
            <th>관심지역</th>
            <td>
                <input type="checkbox" th:field="*{area}" value="유럽">유럽
                <input type="checkbox" th:field="*{area}" value="동남아">동남아
                <input type="checkbox" th:field="*{area}" value="일본">일본
                <input type="checkbox" th:field="*{area}" value="중국">중국
                <div th:if="${#fields.hasErrors('area')}" th:errors="*{area}" class="err"></div>
            </td>
        </tr>
        <tr>
            <th>여행타입</th>
            <td>
                <input type="radio" th:field="*{style}" value="패키지">패키지
                <input type="radio" th:field="*{style}" value="크루즈">크루즈
                <input type="radio" th:field="*{style}" value="자유여행">자유여행
                <input type="radio" th:field="*{style}" value="골프여행">골프여행
                <div th:if="${#fields.hasErrors('style')}" th:errors="*{style}" class="err"></div>
            </td>
        </tr>
        <tr>
            <th>가격</th>
            <td>
                <select th:field="*{price}">
                    <option value="">-- 선택하세요 --</option>
                    <option value="0~100">0~100</option>
                    <option value="200~300">200~300</option>
                    <option value="300~400">300~400</option>
                </select>
                <div th:if="${#fields.hasErrors('price')}" th:errors="*{price}" class="err"></div>
            </td>
        </tr>
        <tr>
            <td colspan="2" style="text-align:center;">
                <input type="submit" value="추가하기">
                <a href="list">목록보기</a>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

 

 📁 list.html

더보기
<!DOCTYPE html>

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>

    <style>
        .center {
            width: 600px;
            margin: auto; /* div 자체를 중앙 배치 */
        }

        /* 페이지네이션 영역 */
        .pagination {
            text-align: center;   /* 가운데 정렬 */
            margin-top: 20px;     /* 위쪽 여백 */
        }

        /* 페이지 번호 간격 */
        .pagination span,
        .pagination a {
            margin: 0 6px;
            text-decoration: none;   /* 밑줄 제거 */
            color: black;            /* 링크 색상 통일 */
        }

        /* 현재 페이지 */
        .pagination .current {
            font-weight: bold;
            color: red;
        }

        /* 링크 hover 효과 */
        .pagination a:hover {
            color: blue;
        }
    </style>

    <meta charset="UTF-8">
    <title>list</title>
</head>
<body>
list.html<br>
<div class="center">
    <h2>여행 리스트 화면 ( <span th:text="${lists.size()}"></span>/[[${totalCount}]] ) </h2><br>
    <!--검색창-->
    <form th:action="@{list}" method="get">
        <select name="whatColumn">
            <option value="all">전체검색</option>
            <option value="area">지역</option>
            <option value="style">여행타입</option>
        </select>
        <input type="text" name="keyword">
        <input type="submit" value="검색">
        <!--whatColumn = area,style-->
        <!--keyword = 내가 입력한 단어-->

    </form>

<br>
    <table border="1">
        <tr>
            <th>번호</th>
            <th>이름</th>
            <th>나이</th>
            <th>관심지역</th>
            <th>여행타입</th>
            <th>예상비용</th>
            <th>삭제</th>
            <th>수정</th>
        </tr>


            <th:block th:each="list: ${lists}">
                <tr>
                <td th:text="${list.num}"></td>
                <td>
                    <a th:href="@{content_view(num=${list.num})}" th:text="${list.name}"></a>
                </td>
                <td th:text="${list.age}"></td>
                <td th:text="${list.areaAsString}"></td>
                <td th:text="${list.style}"></td>
                <td th:text="${list.price}"></td>
                <td>
                    <a th:href="@{/delete(num=${list.num}, page=${page}, whatColumn=${whatColumn}, keyword=${keyword})}">삭제</a>
                </td>
                <td>
                    <a th:href="@{/update(num=${list.num}, page=${page}, whatColumn=${whatColumn}, keyword=${keyword})}">수정</a>
                </td>
                </tr>
            </th:block>
        <td colspan="8" style="text-align:center">
            <a th:href="insert">삽입</a>
        </td>

    </table>
</div>

<!--페이지 설정 -->
<!--이전 1 2 3 다음-->
<div class="pagination">
    <!--이전 버튼-->
    <span th:if="${page > 1}"> <!--내가 선택한 페이지가 1보다 크면 '이전'페이지 생김-->
        <a th:href="@{list(page=${page-1}, whatColumn=${whatColumn}, keyword=${keyword})}">이전</a> <!--페이지에서 하나 빼기-->
    </span>

    <!--현재 보고있는 페이지-->
    <span th:each="i : ${#numbers.sequence(1, totalPage)}">

            <!-- 현재 페이지가 아닌 경우: 번호에 링크를 걸어서 이동 가능 -->
            <a th:if="${i != page}"
               th:href="@{/list(page=${i}, whatColumn=${whatColumn}, keyword=${keyword})}"
               th:text="${i}"></a>

            <!-- 현재 페이지인 경우: 굵은 글씨 + 빨간색 표시 -->
            <span th:if="${i == page}"
                  th:text="${i}"
                  style="font-weight:bold;color:red"></span>
    </span>

    <!--다음 버튼-->
    <span th:if="${page < totalPage}">
    <a th:href="@{list(page=${page+1}, whatColumn=${whatColumn}, keyword=${keyword})}">다음</a>
</span>
</div>
</body>
</html>

 📁 update.html

더보기
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>update</title>

    <style>
        .err { color: red; }
        h2 {
           margin: auto;
           text-align: center;
       }

       table {
           width: 800px;
           border-collapse: collapse;
           margin: auto;
       }
        th{
             text-align: center;
        }
    </style>
</head>
<body>
<h2>수정폼</h2>
<!--list.html 수정 클릭시 (whatColumn, keyword, page) updateproc에 요청처리-->
<br>
<form th:action="@{/updateProc}" th:object="${tDto}" method="post">
    <!-- 수정 불가지만 값은 넘겨야 함 -->
    <input type="hidden" th:field="*{num}">
    <input type="hidden" th:field="*{name}">

    <!--선택한 페이지에서 수정후 그 페이지에 유지를 위해 값 보냄-->
    <!--Dto에 없는 필드명이기 떄문에 value로 값을 보내기-->
    <input type="hidden"  name="whatColumn" th:value="${whatColumn}">
    <input type="hidden" name="keyword" th:value="${keyword}">
    <input type="hidden" name="page" th:value="${page}">

    <table border="1" width="500">
        <tr>
            <th>번호</th>
            <td th:text="*{num}"></td>
        </tr>

        <tr>
            <th>이름</th>
            <td th:text="*{name}"></td>
        </tr>

        <tr>
            <th>나이</th>
            <td>
                <input type="text" th:field="*{age}">
                <div th:if="${#fields.hasErrors('age')}" th:errors="*{age}" class="err"></div>
            </td>
        </tr>

        <tr>
            <th>관심지역</th>
            <td>
                <span th:each="a : ${areaArr}">
                    <input type="checkbox" th:field="*{area}" th:value="${a}"> [[${a}]]
                </span>
                <div th:if="${#fields.hasErrors('area')}" th:errors="*{area}" class="err"></div>
            </td>
        </tr>

        <tr>
            <th>여행타입</th>
            <td>
                <span th:each="t : ${typeArr}">
                    <input type="radio" th:field="*{style}" th:value="${t}"> [[${t}]]
                </span>
                <div th:if="${#fields.hasErrors('style')}" th:errors="*{style}" class="err"></div>
            </td>
        </tr>

        <tr>
            <th>가격</th>
            <td>
                <select th:field="*{price}">
                    <option th:each="p:${priceArr}" th:value="${p}" th:text="${p}"></option>
                </select>
                <div th:if="${#fields.hasErrors('price')}" th:errors="*{price}" class="err"></div>
            </td>
        </tr>

        <tr>
            <td colspan="2" style="text-align:center;">
                <input type="submit" value="수정하기"> <!--수정하기 눌렀을때 보던 페이지로 이동-->
                <a href="list">목록보기</a>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

 📁 content_view.html

더보기
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>content_view</title>
    <style>
     .err { color: red; }
     h2 {
        margin: auto;
        text-align: center;
    }

    table {
        width: 800px;
        border-collapse: collapse;
        text-align: center;
        margin: auto;
    }
    </style>
</head>
<body>
content_view<br>
<h2>상세보기 : <span th:text="${num}"></span></h2><br>

    <table border="1">
        <tr>
            <td>번호</td><td><span th:text="${tDtd.num}"></span></td>
        </tr>
        <tr>
            <td>이름</td><td><span th:text="${tDtd.name}"></span></td>
        </tr>
        <tr>
            <td>나이</td><td><span th:text="${tDtd.age}"></span></td>
        </tr>
        <tr>
            <td>관심지역</td><td><span th:text="${tDtd.areaAsString}"></span></td>
        </tr>
        <tr>
            <td>여행타입</td><td><span th:text="${tDtd.style}"></span></td>
        </tr>
        <tr>
            <td>예상비용</td><td><span th:text="${tDtd.price}"></span></td>
        </tr>
        <tr>
            <td colspan="2">
                <a th:href="@{/update(num=${tDtd.num})}">수정</a>|
                <a th:href="@{/delete(num=${tDtd.num})}">삭제</a>|
                <a th:href="@{list}">목록가기</a>
            </td>
        </tr>
    </table>
</form>
</body>

</html>

 

⬆ 목차로


🌈3. 코드 설명

📌 공통적으로 쓰이는 변수

  • page : 현재 페이지 번호 (기본값 1)
  • limit : 한 페이지에 보여줄 레코드 수 (여기서는 3)
  • offset : SQL 조회 시 시작 위치 = (page - 1) * limit
    • page=1 → offset=0 → 0,1,2 레코
    • page=2 → offset=3 → 3,4,5 레코드
    • page=3 → offset=6 → 6,7,8 레코드
  • whatColumn : 검색할 컬럼명 (예: area, style 등)
  • keyword : 검색 키워드 문자열
  • params (Map<String,Object>) : MyBatis에 전달할 파라미터들을 담은 맵
    • key = "whatColumn", "keyword", "offset", "limit"
    • value = 위 요청 값들
Map<String, Object> params = new HashMap<>();

params.put("whatColumn", "area");   // key = "whatColumn", value = "area"
params.put("keyword", "한국");        // key = "keyword", value = "한국"
params.put("offset", 0);           // key = "offset", value = 0
params.put("limit", 3);            // key = "limit", value = 3
  • totalCount : 조건에 맞는 전체 레코드 개수
  • totalPage : 총 페이지 수 = ceil(totalCount / limit)
  • encodeKeyword : 검색 키워드가 있을 경우 URL에 붙일 때 한글깨짐 방지 위해 인코딩된 값

메서드별 주요 변수

1) 삽입 

public String insert(@ModelAttribute("tDto") @Valid TravelDto tDto, BindingResult br)

 

  • tDto : 입력 폼에서 전달받은 여행 데이터 DTO
  • br : 유효성 검증 결과 (에러가 있으면 true)

 

2) 목록 조회 

// 전체조회 list + 검색 입력(whatColumn, keyword) +  페이징
    @GetMapping(value = "/list")
    public String list(Model model,
                       // required = false : 작성하지 않으면 반드시 값을 넘겨야함
                       // 값을 넘길때가 있고 안넘길때가 있을때 작성
                       @RequestParam(value = "whatColumn", required = false) String whatColumn,
                       @RequestParam(value = "keyword", required = false) String keyword,
                       @RequestParam(value = "page",defaultValue = "1") int page) /*페이지가 안넘어오면 디폴트= 1*/
    {

        // 기본 페이징 설정
        int limit=3; // 한 페이지에 보여줄 레코드 수
        int offset= (page-1) * limit;

        // MyBatis 에 전달할 파라미터 맵
        Map<String, Object> params = new HashMap<String, Object>(); //key, value
        params.put("whatColumn", whatColumn); 
        params.put("keyword", keyword);
        params.put("offset", offset);  
        params.put("limit", limit); 

        // (1) 페이징된 리스트 조회
        List<TravelDto>lists = travelMapper.selectAll(params);

        // (2) 전체 검색된 레코드 개수 조회
        int totalCount = travelMapper.getCount(params);

        // (3) 전체 페이지 수 계산
        int totalPage = (int) Math.ceil((double) totalCount / limit);

        // 뷰로 전달할 데이터 세팅
        model.addAttribute("lists", lists);          
        model.addAttribute("page", page);          
        model.addAttribute("totalCount", totalCount); 
        model.addAttribute("totalPage", totalPage); 
        model.addAttribute("keyword", keyword);    
        model.addAttribute("whatColumn", whatColumn); 

        return "list";
    }

 

  • lists : travelMapper.selectAll(params) 로 가져온 현재 페이지 데이터 목록
  • model : 뷰로 데이터 전달용
    • lists, page, totalCount, totalPage, whatColumn, keyword 저장

 

3) 상세조회

 

    @GetMapping(value = "/content_view")
    public String content_view(@RequestParam("num") int num, Model model){
        TravelDto tDtd = travelMapper.findByNum(num);
        model.addAttribute("num", num);
        model.addAttribute("tDtd", tDtd);
        return "content_view";
    }
  • num : 여행 데이터의 PK 번호 ==> 리스트에서 선택한 항목의 num을 Request받음
  • tDtd : 해당 num에 해당하는 레코드 (TravelDto)
  • 상세보기창에서 num에 해당하는 레코드(Dto)에 담긴 정보를 출력하기 위해 

 

4) 수정

4-1) 수정폼 이동

// 수정폼 이동
@GetMapping(value = "/update")
public String update(@RequestParam("num") int num,
                     @RequestParam(value = "whatColumn", required = false) String whatColumn,
                     @RequestParam(value = "keyword", required = false) String keyword,
                     @RequestParam(value = "page",defaultValue = "1") int page, /*페이지가 안넘어오면 디폴트= 1*/
                     Model model){

    model.addAttribute("page",page);
    model.addAttribute("whatColumn",whatColumn);
    model.addAttribute("keyword",keyword);

    TravelDto tDto = travelMapper.findByNum(num);   
    model.addAttribute("num", num);
    model.addAttribute("tDto", tDto);

    // 뷰에서 사용할 선택지 배열 전달
    model.addAttribute("areaArr", List.of("유럽", "동남아", "일본", "중국"));
    model.addAttribute("typeArr", List.of("패키지", "크루즈", "자유여행", "골프여행"));
    model.addAttribute("priceArr", List.of("100~200", "200~300", "300~400"));

    return "update";
}

 

// 수정처리
@PostMapping(value = "/updateProc")
public String updateproc(@ModelAttribute("tDto") @Valid TravelDto tDto, BindingResult br, Model model,
                         @RequestParam(value = "whatColumn", required = false) String whatColumn,
                         @RequestParam(value = "keyword", required = false) String keyword,
                         @RequestParam(value = "page",defaultValue = "1") int page){

    System.out.println("updateProc whatColumn : " + whatColumn);


    /* 입력에 실패했을 때 update폼에 다시 렌더링 되기 위함 */
    model.addAttribute("areaArr", List.of("유럽", "동남아", "일본", "중국"));
    model.addAttribute("typeArr", List.of("패키지", "크루즈", "자유여행", "골프여행"));
    model.addAttribute("priceArr", List.of("100~200", "200~300", "300~400"));
    model.addAttribute("page",page);
    model.addAttribute("whatColumn",whatColumn);
    model.addAttribute("keyword",keyword);

    String resultPage="";
    if (br.hasErrors()) {
        resultPage = "update";
    } else {
        travelMapper.updateTravel(tDto); // 검색 키워드가 있을 경우 URL 인코딩
        String encodeKeyword = keyword != null ? URLEncoder.encode(keyword, StandardCharsets.UTF_8) : "";
        resultPage = "redirect:/list?page="+page+"&whatColumn="+whatColumn+"&keyword="+encodeKeyword;
    }
    return resultPage;
}

 

 

 

5) 삭제

// 삭제 처리
@GetMapping(value = "/delete")
public String delete(@RequestParam("num") int num, Model model,
                     @RequestParam(value = "whatColumn", required = false) String whatColumn,
                     @RequestParam(value = "keyword", required = false) String keyword,
                     @RequestParam(value = "page",defaultValue = "1") int page){


    model.addAttribute("page",page);
    model.addAttribute("whatColumn",whatColumn);
    model.addAttribute("keyword",keyword);


    // 삭제 실행 sql문
    travelMapper.deleteTravel(num);


    // 삭제 후에도 페이징 유지 + 페이지 보정
    int limit=3; // 한 페이지 크기 (list 메서드랑 동일해야 함)
    int offset= (page-1) * limit;
    Map<String, Object> params = new HashMap<String, Object>();
    params.put("whatColumn", whatColumn);
    params.put("keyword", keyword); 
    params.put("offset", offset);  
    params.put("limit", limit); 


    // 삭제했을때 그 페이지에 레코드가 0이면 앞페이지로 이동
    int totalCount = travelMapper.getCount(params);
    if(totalCount % limit == 0) {
        page = page - 1;
    }

    String resultPage="";
    String encodeKeyword = keyword != null ? URLEncoder.encode(keyword, StandardCharsets.UTF_8) : "";
    resultPage = "redirect:/list?page="+page+"&whatColumn="+whatColumn+"&keyword="+encodeKeyword;
    return resultPage;
}

 

  • num : 삭제할 레코드 번호
  • totalCount : 삭제 후 남은 레코드 개수
  • page 보정 : 삭제 후 현재 페이지가 비어있으면 page-1 처리

 


 

📌 list() 메서드의 Map

👉 단순히 검색 + 페이징된 목록을 가져오기 위한 조건을 전달하기 위해 Map을 사용
 
 
 

 

📌 delete () 메서드의 Map

 

  • 어떤 데이터를 삭제하고 나면
  • 현재 페이지에 레코드가 남아 있는지 없는지  확인
  • 확인을 위해 getCount(params)를 다시 호출하는데, (getCount() : 조건에 맞는 전체 레코드(행)의 개수를 구하는 메서드) 
    이때도 검색조건(whatColumn, keyword) + 페이징(offset, limit)을 전달해야 하니까 Map을 새로 만듬

 

 

getCount()  사용되는곳

list() 메서드 :  총 페이지 수 계산 가능 → totalPage 구할 때 사용

int totalCount = travelMapper.getCount(params);
int totalPage = (int) Math.ceil((double) totalCount / limit);

 

delete() 메서드

  • 삭제 후에 현재 페이지에 레코드가 남아 있는지 확인하려고 사용
  • 만약 totalCount % limit == 0이면 → 마지막 페이지가 비었으니까 page-1로 이동
int totalCount = travelMapper.getCount(params);
if(totalCount % limit == 0) {
    page = page - 1;
}

 


list.html 에서 페이징

==> 현재보고있는 페이지에서 반복문을 돌릴때 #numbers

 

<!--현재 보고있는 페이지-->
<span th:each="i : ${#numbers.sequence(1, totalPage)}">

        <!-- 현재 페이지가 아닌 경우: 번호에 링크를 걸어서 이동 가능 -->
        <a th:if="${i != page}"
           th:href="@{/list(page=${i}, whatColumn=${whatColumn}, keyword=${keyword})}"
           th:text="${i}"></a>

        <!-- 현재 페이지인 경우: 굵은 글씨 + 빨간색 표시 -->
        <span th:if="${i == page}"
              th:text="${i}"
              style="font-weight:bold;color:red"></span>
</span>

 

 

🔹 Thymeleaf의 Utility Objects

Thymeleaf는 기본적으로 여러 가지 유틸리티 객체를 제공하고, 이 객체들은 #을 붙여 접근합니다.
예시:

  • #numbers → 숫자 관련 유틸리티 (sequence, formatDecimal …)
  • #dates → 날짜/시간 관련 유틸리티 (format, createNow …)
  • #strings → 문자열 관련 유틸리티 (isEmpty, toUpperCase …)
  • #lists → 리스트 관련 유틸리티 (size, contains …)
  • #maps → 맵 관련 유틸리티
  • #aggregates → 합계, 평균 같은 집계 함수

🌈4. 페이징 작성순서

Controller → MyBatis → Controller → View

 

1. Controller (변수 정의)

  • page (현재 페이지)
  • limit (한 페이지당 레코드 수)
  • offset = (page - 1) * limit

2. MyBatis SQL 실행

  • selectAll: offset #{offset} rows fetch next #{limit} rows only
  • getCount: 전체 레코드 수 조회

3. Controller (계산)

  • totalCount (검색된 전체 개수)
  • totalPage = ceil(totalCount / limit)

4. View (Thymeleaf)

  • 이전, 1 2 3 ..., 다음 버튼 생성
  • page, whatColumn, keyword 유지해서 주소 전달

 

 

⬆ 목차로