페이지네이션과 페이징을 매칭시키는 과정
- StudyBoardController -
@GetMapping("/list")
public String studyListGET(PageVO page, Model model) {
model.addAttribute("studyboard", sBoardService.getListPaging(page));
int total = sBoardService.getTotal();
PageMakerDTO pmk = new PageMakerDTO(total, page);
model.addAttribute("pmk", pmk);
model.addAttribute("page", page);
return "studyBoard/studyList";
}
- studyList.html -
<style>
.pageNav {
list-style-type: none;
}
.pageNav li {
float: left;
margin-right: 10px;
}
.active {
font: bold;
color: tomato;
}
</style>
<li th:if="${pmk.prev}">
<a th:href="@{/studyboard/list} + '?pageNum=__${pmk.startPage - 1}__'" aria-label="이전">
<span>이전</span>
</a>
</li>
<li class="page-item" th:each="number : ${#numbers.sequence(pmk.startPage, pmk.endPage)}">
<a class="page-link" th:href="@{/studyboard/list} + '?pageNum=__${number}__'" th:classappend="${page.pageNum == number} ? 'active' : '' " th:text="${number}"></a>
</li>
<li th:if="${pmk.next}">
<a th:href="@{/studyboard/list} + '?pageNum=__${pmk.endPage + 1}__'" aria-label="다음">
<span>다음</span>
</a>
</li>
페이지네이션바의 페이지 수를 10개로 고정해두었으므로 총 페이지 수가 10페이지가 넘어가야 이전, 다음버튼을 사용할 수 있음.(이 부분 착각하기 쉬우니 주의.)
컨트롤러에서 model.addAttribute("pmk", pmk);
만 전달하고 view에서
<li class="page-item" th:each="number : ${#numbers.sequence(pmk.startPage, pmk.endPage)}">
<a class="page-link" th:href="@{/studyboard/list} + '?pageNum=__${number}__'" th:classappend="${page.pageNum == number} ? 'active' : '' " th:text="${number}"></a>
</li>
처럼 th:classappend="${pmk.page.pageNum == number} ? 'active' : '' "
로 active클래스를 추가하면 에러가 발생함.
pmk.page.pageNum == number
에서 발생하는 에러인데, pmk
객체에 page
객체가 담겨서 view로 전달되는게 아니라 pageNum값을 찾을 수 없는 에러같음.
=> 컨트롤러에서 model.addAttribute("page", page);
를 추가하여 page
객체를 따로 넘겨주는 방법으로 해결.
view에서의 호출도 pmk
객체를 통하지 않고 바로 page.pageNum
으로 호출.
가입하기 게시판에도 페이징 적용.
- GreetBoardMapper.interface -
public int getTotal(); // 게시글 총 갯수
- GreetBoardMapper.xml -
<select id="getTotal" resultType="int">
SELECT count(*) FROM greetboard
</select>
- GreetBoardMapperTest -
@Test // 총 게시글 구하기
public void testGetTotal() {
int result = gBoardMapper.getTotal();
log.info("총 게시글 수: " + result);
}
- GreetBoardService -
public int getTotal(); // 게시글 총 갯수
- GreetBoardServiceImpl -
@Override
public int getTotal() {
// 전체 게시글 수
return sBoardMapper.getTotal();
}
- GreetBoardMapper.interface -
public List<GreetBoardVO> getListPaging(PageVO page); // 페이징을 적용한 전체 게시글 가져오기.
- GreetBoardMapper.xml -
<!-- 페이징 적용 게시판 목록 -->
<select id="getListPaging" resultType="GreetBoardVO">
SELECT * FROM (
SELECT gno, content, writer, regdate
FROM greetboard ORDER BY gno DESC) as T1
LIMIT #{skip}, #{amount}
</select>
- GreetBoardMapperTest -
@Test
public void testGetListPaging() {
PageVO page = new PageVO(1, 5);
List<GreetBoardVO> list = gBoardMapper.getListPaging(page);
list.forEach(board -> log.info("" + board));
}
- GreetBoardService -
public List<GreetBoardVO> getListPaging(PageVO page); // 페이징을 적용한 전체 게시글 가져오기.
- GreetBoardServiceImpl -
@Override
public List<GreetBoardVO> getListPaging(PageVO page) {
// 페이징 적용 가입인사 전체 가져오기
// page.setAmount(5);
return gBoardMapper.getListPaging(page);
}
- GreetBoardController -
@GetMapping("/list")
public String greetListGET(PageVO page, Model model) {
// 페이징 적용 모든 가입인사 가져오기
page.setAmount(5);
model.addAttribute("greetboard", gBoardService.getListPaging(page));
model.addAttribute("board", new GreetBoardVO());
return "greetBoard/greetList";
}
한 페이지에 보여줄 게시글 갯수인 amount
는 serviceImpl에서 구현할때 set메서드로 지정해도 되고, 컨트롤러에서 지정해도 됨.
여기서는 직관적으로 볼 수 있도록 컨트롤러에서 지정.
http://localhost:8080/greetboard/list
http://localhost:8080/greetboard/list?pageNum=3
pageNum의 값을 변경해주면 각 페이지에 맞는 가입인사가 출력됨.
단, 생성자 기본값(pageNum, amount)을 이용했던 공부게시판과는 달리 컨트롤러에서 amount
의 값을 지정해주었으므로 주소창을 통한 값변경은 불가능함.
- GreetBoardController -
@GetMapping("/list")
public String greetListGET(PageVO page, Model model) {
// 페이징 적용 모든 가입인사 가져오기
model.addAttribute("greetboard", gBoardService.getListPaging(page));
int total = gBoardService.getTotal();
PageMakerDTO pmk = new PageMakerDTO(total, page);
page.setAmount(5);
model.addAttribute("pmk", pmk);
model.addAttribute("page", page);
model.addAttribute("board", new GreetBoardVO());
return "greetBoard/greetList";
}
- greetList.html -
<style>
.pageNav {
list-style-type: none;
}
.pageNav li {
float: left;
margin-right: 10px;
}
.active {
font: bold;
color: tomato;
}
</style>
<nav th:if="${pmk.endPage > 0}">
<ul class="pageNav">
<li th:if="${pmk.prev}">
<a th:href="@{/greetboard/list} + '?pageNum=__${pmk.startPage - 1}__'" aria-label="이전">
<span>이전</span>
</a>
</li>
<li class="page-item" th:each="number : ${#numbers.sequence(pmk.startPage, pmk.endPage)}">
<a class="page-link" th:href="@{/greetboard/list} + '?pageNum=__${number}__'" th:classappend="${page.pageNum == number} ? 'active' : '' " th:text="${number}"></a>
</li>
<li th:if="${pmk.next}">
<a th:href="@{/greetboard/list} + '?pageNum=__${pmk.endPage + 1}__'" aria-label="다음">
<span>다음</span>
</a>
</li>
</ul>
</nav>
http://localhost:8080/greetboard/list
페이지네이션바의 각 숫자를 눌렀을 때 해당 페이지로 이동함을 확인.
시큐리티를 이용한 인증과 허가 적용.
보안유지를 윙해 비밀번호의 암호화를 적용하려할때 시큐리티를 사용하면 미리 만들어진 기능을 상속받아 설정할 기능을 오버라이딩(overrriding)하여 사용한다.
시큐리티를 적용하지 않고도 암호화와 복호화는 가능함. 시큐리티를 이용했을때와는 달리 개발자가 기능을 일일히 작성하여야 하는 대신 기능을 커스텀하기 편리하다는 장점이 있음.
수업시간에 진행했던 bbs프로젝트의 경우 시큐리티를 사용하지 않아 인터셉터로 인증(로그인)을 확인하는 과정까지만 진행함.
참고로 인터셉터에서 인증을, 시큐리티에서 허가를 기능하는것도 가능은 하나, 시큐리티에 인증과 허가 두 기능 모두가 포함되어있으므로 간단하게 쓰려면 시큐리티를 사용하여 오버라이딩하는편이 편리함.
이 부분은 수업시간의 shoppingmall프로젝트의 시큐리티 파트를 참고함.
시큐리티 적용 순서:
0. pom.xml에 디펜던시 추가
1. 상속 WebSecurityConfigurerAdapter
2. 어노테이션 @EnableWebSecurity 적용
pom.xml에 시큐리티 디펜던시 추가
정상적으로 추가되면 실행 시 로그인 화면으로 이동됨.
이때 로그인 비밀번호는 콘솔창에 security password: 뒤의 임시번호를 입력하면 됨.
비밀번호가 길어 로그인이 번거로우므로 여기서는 application.properties
에서 id와 password를 설정해줌.
# security
spring.security.user.name=user
spring.security.user.password=pass
설정을 마치면 id=user, password=pass 입력 시 로그인 가능.
시큐리티로 허가기능을 추가하기 위한 패키지 생성.
- SecurityConfig -
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 시큐리티는 1.인증(로그인), 2.허가(role에 따른 허용가능범위 설정) 이 가능.
@Override
protected void configure(HttpSecurity http) throws Exception {
// 허가(role에 따른 허용범위)
http.authorizeHttpRequests()
.antMatchers("/").permitAll(); // 누구나 허용
}
}
.antMatchers("/").permitAll();
을 추가하여 인증이 되지 않은 사용자까지 모두 접근을 허용함으로써 앞서 security를 추가했을때 페이지실행 시 로그인 화면부터 뜨던 것과 달리 바로 접근이 가능함.
# 유저 테이블
CREATE TABLE IF NOT EXISTS users (
uno int not null auto_increment,
uid VARCHAR(45) not null,
password VARCHAR(255) not null,
email VARCHAR(45) not null,
username VARCHAR(45) not null,
phone_number VARCHAR(45) not null,
profile_img VARCHAR(45),
profile_description VARCHAR(1000),
PRIMARY KEY (uno)
);
순서대로 회원번호, 유저id, 비밀번호, 이메일주소, 회원성명, 휴대폰번호, 프로필이미지, 프로필설명 레코드.
jpa를 사용하지 않기때문에 @Entity, @Table
을 이용한 매핑은 사용불가.
앞서 게시판을 만들었을때처럼 mapper인터페이스와 mapper.xml, service인터페이스와 serviceImpl클래스로 구현하는 방법을 사용.
UserDetails
인터페이스를 구현하여 로그인에 필요한 기능을 오버라이딩하여 사용한다.
- UserVO -
@Data
public class UserVO implements UserDetails {
private static final long serialVersionUID = 1L;
private int uno; // 회원번호
private String uid; // 유저id
private String password; // password
private String email; // 이메일
private String username; // 유저이름
private String phone_number; // 폰번호
private String profile_img; // 프로필사진
private String profile_description; // 프로필소개
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 권한 목록을 리턴
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public boolean isAccountNonExpired() {
// 계정이 만료되었는가?
return false; // 만료안됨
}
@Override
public boolean isAccountNonLocked() {
// 계정이 잠겨있는가?
return false; // 잠기지 않음
}
@Override
public boolean isCredentialsNonExpired() {
// 비밀번호가 만료되었는가?
return false; // 만료안됨
}
@Override
public boolean isEnabled() {
// 사용가능한 계저인가?
return false; // 사용가능
}
}
유효성검사는 회원가입기능 먼저 테스트해본 후 추가.
Spring Boot spring security 로 로그인을 구현하자!
순서와 방법은 앞선 공부게시판, 가입인사 게시판과 동일함.
- mapper인터페이스 생성
- mapper.xml에서 쿼리문 작성
- service인터페이스 생성
- ServiceImpl클래스에서 구현
- UserService.interface -
public interface UserService {
public void save(UserVO user); // 새 유저 저장
}
- UserMapper.interface -
@Mapper
public interface UserMapper {
public void save(UserVO user); // 새 유저 저장
}
- UserMapper.xml -
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.myapp.maybeCafe.dao.UserMapper">
<select id="save" resultType="UserVO">
INSERT INTO users(uid, password, email, username, phone_number)
VALUES (#{uid}, #{password}, #{email}, #{username}, #{phone_number})
</select>
</mapper>
- UserServiceImpl -
@Service
public class UserServiceImpl implements UserService{
private UserMapper uMapper;
// 생성자 주입
public UserServiceImpl(UserMapper userMapper) {
this.uMapper = userMapper;
}
@Override
public void save(UserVO user) {
// 새 유저 저장(가입하기)
uMapper.save(user);
}
}
- RegistratinController -
@Controller
@RequestMapping("/register")
public class RegistratinController {
private UserService uService;
public RegistratinController(UserService userService) {
this.uService = userService;
}
@GetMapping
public String registerGET(Model model) {
model.addAttribute("user", new UserVO());
return "register";
}
@PostMapping
public String registerPOST(UserVO user, RedirectAttributes attr) {
uService.save(user); // DB에 유저객체 저장
return "redirect:/login"; // 회원가입 완료시 로그인 페이지로
}
}
- register.html -
<h2>회원가입</h2>
<form th:action="@{/register}" method="post" th:object="${user}">
<div>
<label for="title">아이디</label>
<input type="text" th:field="*{uid}" required />
</div>
<div>
<label for="password">비밀번호</label>
<input type="text" th:field="*{password}" required />
</div>
<div>
<label for="password">비밀번호 확인</label>
<input type="text" th:field="*{password}" required />
</div>
<div>
<label for="username">성명</label>
<input type="text" th:field="*{username}" required />
</div>
<div>
<label for="writer">이메일</label>
<input type="text" th:field="*{email}" required />
</div>
<div>
<label for="writer">휴대폰 번호</label>
<input type="text" th:field="*{phone_number}" required />
</div>
<button type="submit">가입하기</button>
</form>
회원가입을 마친 후 /login
경로로 이동함을 확인.
현재는 유효성검사가 갖춰지지 않았으므로 DB에 정상적으로 저장되는지를 위주로 확인함.