🌈목차
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
📌 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 유지해서 주소 전달
'기초 및 언어 > ▶ Spring' 카테고리의 다른 글
| 09. Spring Boot 📂파일 업로드 + DB 저장 정리 (1) | 2025.09.26 |
|---|---|
| 08. Spring&Java 어노테이션 정리 (1) | 2025.09.25 |
| 06. Spring_마이바티스(MyBatis) (2) | 2025.09.23 |
| 05. Spring_유효성검사 (0) | 2025.09.21 |
| 04. Spring MVC에서 데이터 전달하기 (1) | 2025.09.19 |