validate 플러그인으로 form 정보의 유효성을 검사한 후, 양식이 submit 되기 전에 다른 실행을 시킬 때.

 

1
2
//form
<form id="test"></form>
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$(function(){
    $('#test').validate({   
        rules : { },
        messages : { },      
        submitHandler: function(){
            let confirm = confirm("제출하시겠습니까?");
            if(confirm){
                alert('제출완료')
                return true;
            } else {
                alert('제출취소')
                return false;
            }     
        }   
    })
})​
cs

 

submitHandler는 form정보들의 유효성 검사가 모두 통과된 후 submit 버튼을 누르게 되면 실행 된다.

  • return true : submit 실행. (form 정보 전송)
  • return false : submit 취소. (from 전송 안됨)

 

목표

1. 게시판 List에서 기본 페이지네이션 구현하기 [1]
2. 원래 있던 페이지 번호로 돌아가기 [2]
      - 게시글 상세에서 [목록]버튼 눌렀을 때
      - 게시글 수정 후 [목록]버튼 눌렀을 때
      - 게시글 삭제 후

 

 

게시글을 열람 하고 목록버튼을 눌러 목록으로 돌아 갈 때,

게시글을 수정한 후에 수정한 게시글을 확인하고 목록버튼으로 돌아갈 때,

게시글을 삭제 한 후 목록으로 돌아갈 때

 

기존 코드로는 몇페이지에 있던 게시글이던 간에 무조건 1페이지의 목록으로 돌아가게 된다.

이를 보완하기 위해, 각 화면의 URL마다 기존 페이지번호 파라미터를 실어보내서 원래 있던 페이지번호로 목록을 불러오려고한다. 

 

 

 


 

게시글 목록(List.jsp)에서 [제목]을 클릭했을 때

Controller -- ListGET
  @GetMapping("/noticeBoard/noticeList")
  public ModelAndView noticeBoardListGet(ModelAndView mv, Criteria cri) {
    PageMaker pm = new PageMaker();
    cri.setPostsPerPage(5);
    pm.setCriteria(cri);
    pm.setDisplayPageNum(5);   
    int totalCount = boardService.getTotalCount(cri);
    pm.setTotalCount(totalCount);
    pm.calcData();

    ArrayList<PostVO> list = boardService.getBoardList(cri);
    mv.addObject("list", list);  
    mv.addObject("pm", pm);
    mv.setViewName("/template/board/noticeBoard/noticeList");
    
    return mv;
  }

 

 

List.jsp -- 게시글 제목을 클릭하여 상세화면을 불러 올 때
<td class="data-title">                    
	<a href="<%=request.getContextPath()%>/noticeBoard/noticeDetail?post_num=${post.post_num}&page=${pm.criteria.page}">
		<c:out value="${post.post_title}" />
	</a>                    
</td>

<%=request.getContextPath()%>/noticeBoard/noticeDetail?post_num=${post.post_num}&page=${pm.criteria.page}

(pm.criteria.page는 컨트롤러((GET)BoardList)의 mv.addObject("pm", pm);를 이용하여 불러온 것이다.)

게시글 목록에서 열람할 게시글 제목을 클릭하면, ListGet으로 부터 받은

게시글의 정보(post_num, page)를 주소에 담아 DetailGET 컨트롤러로 전송한다. 

 

 

Controller -- DetailGET
  @GetMapping("/noticeBoard/noticeDetail")
  public ModelAndView noticePostDetailGet
  (ModelAndView mv, Integer post_num, HttpServletRequest request, HttpServletResponse response, Criteria cri) 
  	throws IOException {
    
	PostVO post = boardService.getPostDetail(post_num);   

	...
	//예외처리 및 상세코드 중략
	...

	mv.addObject("post", post);
	mv.addObject("cri", cri);
	mv.setViewName("/template/board/noticeBoard/noticeDetail");     
    
    return mv;
  }
  • 제목클릭(List.jsp)으로부터 받은 page정보를 처리할 수 있도록 DetailGET 컨트롤러의 매개변수에 Criteria cri 를 추가
  • 로직 처리 후, cri 정보를 다시 jsp로 보내줄 수 있도록 mv.addObject("cri", cri); 를 추가

받아온 정보로 DetailGET의 로직이 처리 된 후, 읽어온 상세화면의 url은 다음과 같이 적용된다.

http://localhost:8080/noticeBoard/noticeDetail?post_num=33&page=2

 

 

 

 

게시글 상세(Detail.jsp)에서 [목록]을 클릭했을 때

Detail.jsp -- 상세화면에서 목록 버튼을 클릭했을 때
<button class="btn btn-warning" type="button"
	onclick="location.href='<%=request.getContextPath()%>/noticeBoard/noticeList?page=${cri.page}'">목록</button>

<%=request.getContextPath()%>/noticeBoard/noticeList?page=${cri.page}

상세화면에서 다시 목록버튼을 클릭하면, DetailGET으로 부터 받은

현재 게시글의 페이지 정보(page)를 URL에 실어 ListGET 컨트롤러로 전송한다.

 

그러면 ListGET 컨트롤러에 있는 아래 코드가 해당 페이지의 목록을 불러올 것이다.

pm.setCriteria(cri);

ArrayList<PostVO> list = boardService.getBoardList(cri);

 

그리고 ListGET이 읽어온 URL은 다음과 같다.

http://localhost:8080/noticeBoard/noticeList?page=2

 

 

 

 

게시글 수정 후, 상세화면에서 [목록]버튼 눌렀을 때

게시글 수정버튼을 눌러 글을 수정하고, 수정한 글을 상세화면에서 확인 한 후, 목록버튼을 눌렀을 때

위에서 목록 버튼에 대한 페이지처리를 했으니 간단히 원래 페이지로 돌아올 것 같았지만..

 

1. 수정할 게시글의 페이지 정보 (post_num, page)

2. 수정완료한 게시글의 페이지 정보 (post_num, page)

를 화면마다 넘겨줘야한다.

 

Detail.jsp -- 1.  상세화면에서 수정버튼을 눌렀을 때
<button class="btn btn-warning" type="button"
onclick="location.href='<%=request.getContextPath()%>/noticeBoard/noticeModify?post_num=${post.post_num}&page=${cri.page}'">수정</button>

<%=request.getContextPath()%>/noticeBoard/noticeModify?post_num=${post.post_num}&page=${cri.page}

게시글 상세화면에서 수정 버튼을 클릭하면, DetailGET으로부터 받은

수정할 게시글의 정보를 가지고(page_num, page) 수정화면을 띄운다(ModifyGET).

 

ModifyGET이 읽어온 화면의 URL은 다음과 같다.

http://localhost:8080/noticeBoard/noticeModify?post_num=33&page=2

 

 

Controller -- ModifyGET
  @GetMapping("/noticeBoard/noticeModify")
  public ModelAndView noticePostModifyGet
  (ModelAndView mv, Integer post_num, HttpServletResponse response, Criteria cri)
    	throws IOException {
        
	PostVO post = boardService.getPostDetail(post_num);   
    
	...
	//예외처리 중략
	...
    
	mv.addObject("cri", cri);
	mv.addObject("post", post);
	mv.setViewName("/template/board/noticeBoard/noticeModify");
    return mv;
  }
  • 상세화면(Detail.jsp)으로부터 받은 page정보를 처리할 수 있도록 ModifyGET 컨트롤러의 매개변수에 Criteria cri 를 추가
  • 로직 처리 후, cri 정보를 다시 jsp로 보내줄 수 있도록 mv.addObject("cri", cri); 를 추가

 

 

Modify.jsp -- 2.  수정화면에서 수정완료버튼을 눌렀을 때
<form class="container notice-modify" method="post" id="notice-modify" name="notice-modify"
	action="<%=request.getContextPath()%>/noticeBoard/noticeModify?post_num=${post.post_num}&page=${cri.page}">
	
	<!--
	...
	중략
	...
	-->
	
    <input class="btn btn-warning notice-modify-btn" type="submit" value="수정">
	<input class="form-control" id="post_num" name="post_num" value="${post.post_num}" />
</form>

<%=request.getContextPath()%>/noticeBoard/noticeModify?post_num=${post.post_num}&page=${cri.page}

게시글 수정완료 버튼을 클릭하면, ModifyGET이 받아왔던 수정할 게시글의 정보를

ModifyPOST 컨트롤러로 전송하여 게시글 수정을 완료한다.

 

 

Controller -- ModifyPOST
  @PostMapping("/noticeBoard/noticeModify")
  public ModelAndView noticePostModifyPost(ModelAndView mv, PostVO post, HttpServletRequest request, Criteria cri) {
	
	...
	//상세코드 중략
	...
   
	mv.addObject("cri", cri);
	mv.setViewName("redirect:/noticeBoard/noticeDetail?post_num="+post.getPost_num()+"&page="+cri.getPage());
    
    return mv;
  }
  • 수정할 게시글의 정보를 처리하기 위해 ModifyPOST 매개변수에 Criteria cri를 추가한다.
  • 수정 로직을 수행한 후, 수정한 게시글의 상세화면으로 돌아가는데
    이때에도 게시글 정보를 DetailGET컨트롤러에 전달해줘야하므로 setViewName의 redirect:/경로에 직접 변수와 값을 추가해준다.

수정완료 후 돌아온 게시글 상세의 URL은 다음과 같다.

http://localhost:8080/noticeBoard/noticeDetail?post_num=33&page=2

 

 

 

 

게시글 상세(Detail.jsp)에서 [삭제]를 클릭했을 때

삭제된 게시글을 삭제할건지에 대한 확인alert를 띄운 후, 삭제된 게시물이 있던 페이지로 돌아가는 방법에 대해 작성해보자..!

 

Detail.jsp -- 상세화면에서 삭제버튼을 눌렀을 때
<button class="btn btn-danger" type="button" onclick="confirmDelete();">삭제</button>

<!--삭제확인 alert창 메서드-->
<script>
  function confirmDelete(){
    if(confirm("게시글을 삭제하시겠습니까?") == true) {
      location.href='<%=request.getContextPath()%>/noticeBoard/noticeDelete?post_num=${post.post_num}&page=${cri.page}';
    }else {
      return;
    }
  }
</script>
  • 버튼에 onclick으로 연결된 메서드 confirmDelete() :
    - 삭제버튼을 누르면, "게시글을 삭제하시겠습니까?" 라는 확인 alert이 발생한다.
    - [확인]을 누르면 DeleteGET 컨트롤러로 연결되고, 취소를 누르면 return 되어 아무일도 일어나지 않는다.
  • [확인]을 눌렀을 때 DeleteGET으로 해당 게시글의 정보(post_num, page)를 전달한다.

 

 

Controller -- DeleteGET
  @GetMapping("/noticeBoard/noticeDelete")
  public ModelAndView noticePostDeleteGet
  (ModelAndView mv, Integer post_num, HttpServletRequest request, HttpServletResponse response, Criteria cri)
    	throws IOException {   
 
	UserVO user = userService.getUser(request);
	int result = boardService.deletePost(post_num, user);
    
	...
	//예외처리 중략
	...
    
	mv.setViewName("redirect:/noticeBoard/noticeList?page="+cri.getPage());
  
	mv.addObject("cri", cri);
	return mv;
  }
  • 삭제 할 게시글의 정보를 처리하기 위해 DeleteGET 컨트롤러 매개변수에 Criteria cri를 추가한다.
  • 삭제 로직을 수행한 후, 삭제된 게시글이 있던 페이지 정보를 DeleteGET 컨트롤러에 전달하여
    그 페이지로 이동할 수 있도록 setViewName의 redirect:/경로에 직접 변수와 값을 추가해준다.

 

 

 


 

이렇게 하나하나 글로 늘어놓으면 참 장황하지만,

그림으로 표현하자면 아래처럼 간단히 그릴 수 있다..!

게시글 열람/수정/삭제 후 원래페이지로 돌아가기 기능

 

 

이 간단한 그림으로 정리하기 위해......

얼마나 많이 헷갈리고 머리가 아팠는지 모르겠다..ㅠㅠ

하루 빼곡히 걸림..

난 아직 많이 부족하니꽈..............

 

 

아무튼 정리 끘...!

 

 

외국어 직독직해하듯 코드 독해하기

 

목표

1. 게시판 List에서 기본 페이지네이션 구현하기 [1]
2. 원래 있던 페이지 번호로 돌아가기 [2]
      - 게시글 상세에서 [목록]버튼 눌렀을 때
      - 게시글 수정 후 [목록]버튼 눌렀을 때
      - 게시글 삭제 후

 

 

 

 

게시판 List에서 페이지네이션 활성화시키기

 

Criteria.java -- 페이지네이션 출력 기본 설정 관리
public class Criteria {

  private int page; // 현재 페이지 번호
  private int postsPerPage; // 한 페이지 당 게시글 수
  
  //Criteria 디폴트 생성자
  public Criteria() { 
    this.page = 1; // 현재 페이지를 1페이지로 설정
    this.postsPerPage = 10; // 한 페이지 당 게시글 수 10개로 설정
  }
  
  
  //getter and setter
  
  public int getPage() {
    return page;
  }
  
  public void setPage(int page) {
    if(page <= 0) { // 현재 페이지 번호가 음수라면,
      this.page = 1; // 페이지 번호 1로 설정
    }
    else
      this.page = page;
  }
  
  public int getPostsPerPage() {
    return postsPerPage;
  }
  
  public void setPostsPerPage(int postsPerPage) {   
    if(postsPerPage <=0 || postsPerPage > 100) { // 페이지 당 게시글 수가 0개 이하거나 100개를 초과할 때
      this.postsPerPage = 10;  // 페이지 당 게시글 수를 10개로 설정.
    }
    else
      this.postsPerPage = postsPerPage;
  }
  
  
  @Override
  //Criteria 정보 콘솔 출력 메서드
  public String toString() {
    return "Criteria [page=" + page + ", postsPerPage=" + postsPerPage + "]";
  }
  
  //쿼리문에서 limit에 사용되는 인덱스를 계산하는 getter
  public int getPageStart() {
    return (this.page -1) * postsPerPage;
  }
}

 마지막 getter -- getPageStart()는 아래 BoardMapper.xml에서 활용되니 거기서 설명.

 

PageMaker.java -- 페이지네이션 출력 상세 설정 관리
import lombok.Data;

@Data
public class PageMaker {
  private int totalCount; // 총 게시글 수
  private int startPage; // 시작 번호
  private int endPage; // 끝 번호
  private boolean prev; // 이전 버튼 유무
  private boolean next; // 다음 버튼 유무
  private int displayPageNum; // 페이지네이션 한 세트에 보여질 페이지번호 갯수
  private Criteria criteria; // 페이지네이션 기본 설정 정보
  
  //endPage, startPage, prev, next 값을 계산하는 메서드
  public void calcData() {
    
    endPage = (int) (Math.ceil(criteria.getPage()/(double) displayPageNum)*displayPageNum);
    //endPage = (int)(Math.ceil(3/(double)10) * 10)
    startPage = (endPage - displayPageNum)+1;
    
    //총 게시글 개수를 이용하여 제일 마지막 페이지 번호를 계산
    int theLastPage = (int)(Math.ceil(totalCount/(double)criteria.getPostsPerPage()));
    
    if(endPage > theLastPage) {
      endPage = theLastPage;
    }
    
    //현재 페이지가 1페이지면 이전(prev)버튼이 없어야 함
    prev = startPage == 1 ? false : true;
    //현재 페이지에 마지막 게시글이 포함되어 있으면 다음(next)버튼이 없어야 함
    next = endPage * criteria.getPostsPerPage() >= totalCount ? false:true;
  }

}
EX ) 현재 페이지가 13페이지이고, 페이지네이션 한 세트에 보여지는 페이지 번호 수가 10일 때 
          ==> startPage = 11 , endPage = 20 이어야 함.
  • endPage의 계산 : (int) (Math.ceil(criteria.getPage()/(double) displayPageNum)*displayPageNum);
    Math.ceil() : 소수점 올림. (현재 실수와 같거나 큰 정수를 반환)
    endPage = (13/10.0)*10 => 1.3*10 => 2*20 => 20
  • startPage의 계산 : (endPage - displayPageNum)+1;
    startPage = (20 - 10)+1 => 11
결과 : prev   11 12 13 14 15 16 17 18 19 20   next

 

 

EX) 총 게시글 수가 223개이고, 한 페이지 당 게시글 수가 10개일 때
  • 페이지네이션의 맨~~~마지막 페이지 계산 : (int)(Math.ceil(totalCount/(double)criteria.getPostsPerPage()));
    theLastPage = (223/10.0) => 6.2 => 23
결과 : prev   21 22 23

 

 

 

 

 

BoardController.java -- 게시글 리스트 출력 컨트롤러
  @GetMapping("/noticeBoard/noticeList")
  public ModelAndView noticeBoardListGet(ModelAndView mv, Criteria cri) { // 매개변수 Criteria cri 를 추가.
    
    cri.setPostsPerPage(5); // 한 페이지당 보여질 게시글 개수 설정
    PageMaker pm = new PageMaker(); // PageMaker 객체 생성
    pm.setCriteria(cri); // cri 정보를 pm의 Criteria에 담기.
    pm.setDisplayPageNum(2); // 페이지네이션 한 세트에 보여지는 페이지 번호의 개수를 설정
    
    int totalCount = boardService.getTotalCount(cri); // 게시글 총 개수 구하기
    pm.setTotalCount(totalCount); // pm의 TotalCount에 위에서 구한 게시글 총 개수 설정하기
    pm.calcData(); // endPage, startPage, prev/next 버튼 노출 여부 설정하는 메서드 실행

    ArrayList<PostVO> list = boardService.getBoardList(cri);
    mv.addObject("list", list);
  
    mv.addObject("pm", pm); // pm정보를 "pm"에 담아 jsp에 전달
    mv.setViewName("/template/board/noticeBoard/noticeList");
    return mv;
  }

 

  • noticeBoardListGet -- Controller의 매개변수에 Criteria cri 를 추가
  • 한 페이지당 게시글 5개씩, 페이지네이션 한 세트당 2개 페이지가 설정되도록 작성 됨.
  • addObject로 pm객체 정보를 전달.

 

 

BoardService.java

 

ArrayList<PostVO> getBoardList(Criteria cri); // 매개변수 Criteria cri 추가
int getTotalCount(Criteria cri);
  • getBoardList - Service에 Criteria cri 추가.
  • getTotalCount - Service 생성.

 

 

BoardServiceImp.java
  @Override
  public ArrayList<PostVO> getBoardList(Criteria cri) {  // 매개변수 Criteria cri 추가
    return boardDao.getBoardList(cri); // cri 추가
  }

  @Override
  public int getTotalCount(Criteria cri) {
    return boardDao.getTotalCount(cri);
  }
  • getBoardList - ServiceImp에 Criteria cri 추가.
  • getTotalCount - ServiceImp 생성.

 

 

BoardDAO.java
  ArrayList<PostVO> getBoardList(@Param("cri") Criteria cri); // 매개변수 Criteria cri 추가
  int getTotalCount(@Param("cri") Criteria cri);
  • getBoardList - DAO에 Criteria cri 추가.
  • getTotalCount - DAO 생성.

 

 

BoardMapper.xml -- 게시판의 게시글을 불러오는 쿼리문 수정
<select id="getBoardList" resultType="com.ysy.bakingdom.vo.PostVO">
  select * from post where post_state = '1' order by post_num desc
    <!-- 불러올 게시글 개수 설정  -->
    limit #{cri.pageStart}, #{cri.postsPerPage}
</select>
  • limit #{cri.pageStart}, #{cri.postsPerPage} : 전체 데이터 값에서 "pageStart+1부터 postsPerPage까지" 불러옴
    cri.pageStart : Critera.java 에 있는 인덱스 계산 getter의 리턴 값 => return (this.page -1) * postsPerPage;    
EX) 만약 한 페이지당 게시글 수가 3개 라고 가정하면,



BoardMapper.xml -- 전체 게시글 수를 구하는 쿼리문 작성
<select id="getTotalCount" resultType="int">
  select count(*) from post where post_state = '1'
</select>

 

 

 

<nav aria-label="Page navigation example">
  <ul class="pagination d-flex justify-content-center">
  
    <!-- 이전 버튼 눌렀을 때 -->
    <c:if test="${pm.prev}">
      <li class="page-item">
        <a class="page-link" href="<%=request.getContextPath()%>/noticeBoard/noticeList?page=${pm.startPage-1}" aria-label="Previous">
          <span aria-hidden="true">&laquo;</span> <!-- &laquo; = "<<" -->
        </a>
      </li>
    </c:if>
    
    <!-- 페이지 번호를 눌렀을 때 -->
    <c:forEach begin="${pm.startPage}" end="${pm.endPage}" var="index">
      <li class="page-item <c:if test="${pm.criteria.page == index}">active</c:if>">
        <a class="page-link" href="<%=request.getContextPath()%>/noticeBoard/noticeList?page=${index}">${index}</a></li>
    </c:forEach>

    <!-- 다음 버튼 눌렀을 때 -->
    <c:if test="${pm.next}">
      <li class="page-item">
        <a class="page-link" href="<%=request.getContextPath()%>/noticeBoard/noticeList?page=${pm.endPage+1}" aria-label="Next">
          <span aria-hidden="true">&raquo;</span> <!-- &raquo; = ">>" -->
        </a>
      </li>
    </c:if>
    
  </ul>
</nav>
  • <c:if test="${ [pm.prev | pm.next] }"> </c:if>
    - [pm.prev | pm.next] 가 ture이면 [ 이전 | 다음 ] 버튼 활성화.
  • <c:forEach begin="${pm.startPage}" end="${pm.endPage}" var="index">
    - begin(시작페이지) 부터 end(끝페이지)까지 (PageMaker.java 참고) 를 반복문으로 변수 "index"에 담고, 쭉 출력(나열)한다. 
  • 현재 페이지 번호와 index가 같은 페이지네이션 번호는 active(파란 색 표시)를 활성화 시킨다.

 

 

작업완료 결과
http://localhost:8080/noticeBoard/noticeList
페이지네이션 한 세트에 2개의 페이지가 설정되고, 한 페이지 당 게시글 5개씩 보여짐.

 

 

 

 

여기까지가 기본 페이지네이션 구현이다.

다음 포스트는..

 

2. 원래 있던 페이지 번호로 돌아가기
      - 게시글 상세에서 [목록]버튼 눌렀을 때
      - 게시글 수정 후 [목록]버튼 눌렀을 때
      - 게시글 삭제 후

 

를 작성해보겠다. (힘들당..)

 

 

 

 

servlet-context.xml

아래 코드는 내가 이해하기 쉽게 위치를 분할하고 수정해서 붙여 놓은 것임.

※ <interceptor></interceptor>들은

     <interceptors></interceptors> 안에 위치해야함. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
    <!-- 인터셉터 설정 / 동작 경로 -->
    
    <!-- 로그인처리 인터셉터 (로그인유지) -->
    <beans:bean id="loginInterceptor" 
    class="kr.xxx.yyyy.interceptor.LoginInterceptor"></beans:bean>
 
        <interceptor>
            <mapping path="/member/signin"/>            
            <beans:ref bean="loginInterceptor"/>
        </interceptor>
 
    
 
    <!-- 자동 로그인 처리 인터셉터 (자동로그인유지) -->
    <beans:bean id="autoLoginInterceptor" 
    class="kr.xxx.yyyy.interceptor.AutoLoginInterceptor"></beans:bean>
 
        <interceptor>
            <mapping path="/**/"/>
            <beans:ref bean="autoLoginInterceptor"/>
        </interceptor>
 
 
    
    <!-- 비회원만 접근할 수 있게 하는 인터셉터 -->
    <beans:bean id="guestInterceptor" 
    class="kr.xxx.yyyy.interceptor.GuestInterceptor"></beans:bean>
 
        <interceptor>
            <mapping path="/member/signup"/>
            <mapping path="/member/signin"/>        
            <beans:ref bean="guestInterceptor"/>
        </interceptor>
 
 
    
    <!-- 회원만 접근할 수 있게 하는 인터셉터 -->
    <beans:bean id="userInterceptor" 
    class="kr.xxx.yyyy.interceptor.UserInterceptor"></beans:bean>
 
        <interceptor>
            <mapping path="/abcd/*"/>
            <mapping path="/board/register"/>
            <mapping path="/board/modify"/>
            <mapping path="/board/detail"/>
            <mapping path="/board/delete"/>
            <beans:ref bean="userInterceptor"/> 
        </interceptor>
 
 
    
    <!-- 관리자만 접근할 수 있게 하는 인터셉터 -->
    <beans:bean id="adminInterceptor" 
    class="kr.xxx.yyyy.interceptor.AdminInterceptor"></beans:bean>
 
        <interceptor>
            <mapping path="/admin/**/"/>
            <beans:ref bean="adminInterceptor"/>
        </interceptor>
 
 
cs

 

 

 

 

 

jsp의 form 안에 있는 name 데이터들을 한번에 묶어 controller로 전송하는 방법.

 

var data = $("form[name=XXX]").serialize();

로 묶어주고,

 

ajax의 data는

data: data 로 보내주면 된다.

 

예제 : form안에 있는 이름과 연락처 정보를 한번에 묶어 보내기.
<form method="post" id="findEmail" name="findEmail" action="<%=request.getContextPath()%>/findEmail">
  <label>이름</label>
  <input type="text" class="form-control" placeholder="실명을 입력하세요." id="user_name" name="user_name">
  <label>연락처</label>
  <input id="user_phone" name="user_phone" value="">
</form>
<button type="button" class="btn" id="findEmailBtn">이메일 찾기</button>


<script type="text/javascript">
$(function(){
  $('#findEmailBtn').click(function(){
    var data = $("form[name=findEmail]").serialize();	
    $.ajax({
      type: 'post',
      url: '<%=request.getContextPath()%>/findEmail',
      data: data,
      success: function(result) {
      alert('정보 전송 성공')
      }     
    });
  })
})

</script>
  @ResponseBody
  @PostMapping("/findEmail")
  public String findEmailPost(String user_name, String user_phone) {
    System.out.println(user_name);
    System.out.println(user_phone);
    return "";
  }

 

실행 결과

 

세션과 쿠키

Session Cookie
사용자 데이터를 저장하는 역할
데이터를 서버에 저장
브라우저 단위로 생성되며,
브라우저가 종료되면 세션에 있는 데이터도 사라진다.
데이터를 사용자의 PC(로컬)에 저장
데이터의 유효기간 설정이 가능하여,
브라우저가 종료되더라도
유효기간 동안은 데이터를 계속 사용할 수 있다. 

 

UserController

1
2
3
4
5
6
7
8
9
10
11
12
13
@ResponseBody
@PostMapping("/signin")
public ModelAndView signinPost(ModelAndView mv, UserVO user) {   
  UserVO dbUser = userService.signin(user);      
  if(dbUser != null)
    mv.setViewName("redirect:/");
  else
    mv.setViewName("redirect:/signin");   
  
   mv.addObject("user", dbUser); //Interceptor(postHandler)에게 전송할 user 정보 
   return mv;
}
 
cs

 

 

LoginInterceptor

  • Servlet-context.xml에서 로그인 컨트롤러와 연결되어 있음
1
2
3
4
5
6
7
<beans:bean id="loginInterceptor" class="com.ysy.bakingdom.interceptor.LoginInterceptor" />
<interceptors>
  <interceptor>
    <mapping path="/signin"/>
    <beans:ref bean="loginInterceptor"/>
  </interceptor>
</interceptors>
cs


  • 로그인버튼을 클릭하고 컨트롤러에서 setViewName을 실행하기 직전에 작동하는 인터셉터(postHandle)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class LoginInterceptor extends HandlerInterceptorAdapter {
  @Autowired
  UserService userService;
  
  //로그인 완료 후 실행할 인터셉터
  @Override
  public void postHandle(
    HttpServletRequest request, HttpServletResponse response, Object handler, 
    ModelAndView modelAndView) throws Exception {
        
    //Controller의 mv에서 addObject로 "user"를 전달해 왔을 때 실행. 전달값이 없으면 null로 설정됨.   
    UserVO user = (UserVO)modelAndView.getModelMap().get("user");
    
    if(user != null) {
      //리퀘스트에 있는 세션 정보 가져오기
      HttpSession session = request.getSession();
      //세션에 user 정보 추가
      session.setAttribute("user", user);
            
      //자동로그인에 체크가 되어있으면       
      if(user.getUseCookie() != null) {
      // 여기서 getId는 SessionId를 의미한다.   
      Cookie autoLoginCookie = new Cookie("autoLoginCookie", session.getId());        
      // 쿠키를 찾을 경로    
      autoLoginCookie.setPath("/");         
      // 쿠키 유지 시간 ( sessionLimit)
      autoLoginCookie.setMaxAge(60*60*24*30); 
      response.addCookie(autoLoginCookie);
      Date user_sessionLimit = new Date(System.currentTimeMillis() + (amount * 1000L));
      userService.keepLogin(user.getUser_email(), session.getId(), user_sessionLimit);
      }            
    }
  }
}​​
 
cs
  • Controller에서 addObject로 담아보낸 user데이터를 받아옴.
  • user데이터가 있으면 (로그인이 가능하면), 세션에 user데이터를 담아 저장한다.

    === 자동로그인에 체크되어 있을 때 ==
  • useCookie 값이 null이 아니면 (로그인 할 때 자동로그인에 체크했으면)
    새로운 쿠키(autologincookie)를 생성하여 현재 세션에 저장되어 있는 세션아이디를 쿠키에 담는다.
  • 모든 URL범위에서 쿠키를 전송할 수 있도록 경로를 설정해준다. (setpath)
  • 저장된 쿠키의 유효기간을 설정해준다.
    - 30일동안 유효 : 60초 60분 24시간 * 30일 => 60*60*24*30
    - currentTimeMillis()는 long 타입으로 값을 반환한다. 여기에 60*60*24*30*1000을 연산하여 값을 보내주면 int값으로 계산되어 int타입을 벗어나 오버플로우가 발생한다. 따라서 1000L로 계산하여 연산 값을 long타입으로 만들어줘야한다. (int 범위 : –2,147,483,648 ~ 2,147,483,647 ,  60*60*24*30*1000 =2,592,000,000) 
  • response.addCookie에 쿠키데이터를 실어서 응답.
  • 아까 실려온 user데이터에서의 email과 세션데이터에 담겨져 있는 세션id, 그리고 오늘날짜로부터의 유효기간을 담아 Service에게 keepLogin을 시킨다.

 

  • UserService
1
void keepLogin(String user_email, String user_sessionId, Date user_sessionLimit);
cs

 

  • UserServiceImp
1
2
3
4
5
6
7
  @Override
  public void keepLogin(String user_email, String user_sessionId, Date user_sessionLimit) {
    if(user_email == null || user_sessionId == null || user_sessionLimit == null
      return;
    userDao.keepLogin(user_email, user_sessionId, user_sessionLimit);  
  }
 
cs

 

  • UserDAO
1
void keepLogin(@Param("user_email"String user_email, @Param("user_sessionId"String user_sessionId, @Param("user_sessionLimit") Date user_sessionLimit);
cs

 

  • UserMapper
1
2
3
4
5
6
  <update id="keepLogin">
    update user set
      user_sessionId = #{user_sessionId},
      user_sessionLimit = #{user_sessionLimit}
    where user_email = #{user_email}
  </update>
cs

 

 

AutoLoginInterceptor

  • servlet-context.xml에서 모든 url 경로와 연결되어 있다. = 모든 페이지변경마다 AutoLoginInterceptor가 작동된다. 
<interceptors>
    <interceptor>
        <mapping path="/signin"/>
        <beans:ref bean="loginInterceptor"/>
    </interceptor>
    <interceptor>
        <mapping path="/**/"/>
        <beans:ref bean="autoLoginInterceptor"/>
    </interceptor>
</interceptors>
cs

 

  • url 경로를 따라 컨트롤러로 진입하기 직전에 작동되는 인터셉터(preHandle)
public class AutoLoginInterceptor extends HandlerInterceptorAdapter {
  
  @Autowired
  UserService userService;
  // url이동시 컨트롤러에 진입하기 전에 실행되는 인터셉터
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
                            throws Exception {   
    // 현재 세션 데이터를 가져온다.
    HttpSession session = request.getSession();
    // 세션에 담겨진 user정보를 VO타입으로 담는다.
    UserVO user = (UserVO)session.getAttribute("user");   
    // 만약 세션에 담겨진 user정보가 없으면, 
    if(user == null) {
      Cookie autoLoginCookie = WebUtils.getCookie(request, "autoLoginCookie");  
      if(autoLoginCookie!=null) {
        user = userService.getUserByCookie(autoLoginCookie.getValue());
        if(user != null) {
          session.setAttribute("user", user);
        }
      }
    }    
    return true;
  }
}
cs
  • 세션에 "user"로 전달 받은 유저 데이터를 가져와 VO타입 user에 저장한다.
  • 만약 세션에 담겨진 "user" 정보가 없으면
    (= 로그인되지 않은 상태 or 세션유효기간이 만료되어 자동로그인이 종료된 상태 or 자동로그인이 아닌 상태)
    해당 user의 로그인 관련 쿠키 상태를 확인하기 위해, request에서 "autoLoginCookie"를 가져온다.
  • 가져올 autoLoginCookie 정보가 없으면 (autoLoginCookie==null) 인터셉터 작동 종료.
  • 가져올 autoLoginCookie가 있으면, 해당 Cookie 값(세션아이디)을 가지는 유저 데이터를 찾아 VO user에 저장한다. 유저 데이터가 정상적으로 존재하면 세션 정보를 user로 설정한다.
  • 위 내용을 매 페이지마다(경로 변경마다) 작동하며 로그인을 유지시킨다.

 

  • UserService
UserVO getUserByCookie(String user_sessionId);
cs

 

  • UserServiceImp
@Override
public UserVO getUserByCookie(String user_sessionId) {
  return userDao.getUserByCookie(user_sessionId);
}
cs

 

  • UserDAO
UserVO getUserByCookie(String user_sessionId);
cs

 

  • UserMapper
<select id="getUserByCookie" resultType="com.ysy.bakingdom.vo.UserVO">
  select * from user where user_sessionId = #{user_sessionId} and user_sessionLimit > now()
</select>
cs
  • user_sessionLimit이 현재 시간보다 클 경우 (유효기간이 남아있을 경우)에만 검색된다.

이메일 중복 확인 기능 구현 중에 jsp에서 보내온 이메일 주소가 .뒤로 잘려 나오는 것을 확인했다.

 

jsp에서 보낸 이메일주소는 love@love.com 이었는데,

controller에서 받아낸 이메일주소는 love@love 가 끝이다..

그러니 당연히 DB에 있는 정보 조회가 안되는 거였음!

해결 방법은 GetMapping 경로에 있는 받는 변수값 뒤에 :.+ 를 붙여주는 것. 

 

 

 

아래는 바꾼 코드

메일 주소를 정상적으로 받아오는 것을 확인 할 수 있다.

 

consolas 탈출!👯‍♀️

아래 폰트들은 모두 무료 이용 가능하다.

▼다운로드 링크

Jetbrain mono : https://github.com/JetBrains/JetBrainsMono

monaco : https://www.cufonfonts.com/font/monaco

d2coding : https://github.com/naver/d2codingfont

※ 개발연습생의 설정 방법이므로, 내용이 정확하지 않거나 자세하지 않을 수 있습니다! (또륵)

 

 

 

MySQL에 담겨있는 나의 DB들을 다른 PC나 장소에서도 똑같이 동기화 하고 공유할 수 있으면 정말 좋겠지만...

간단한 연결이나 동기화는 아마도 보안적인 부분에서 이슈가 생기기 쉬울테니까? 이렇게 하는 거겠지? 하는 연습생의 스치는 생각은 버려두고....,

아무튼 노트북과 데스크탑을 옮겨다니며 연습생활을 하고 있는데, 각 기기에 DB동기화가 안되니까 export-import를 통해 직접 파일을 넘겨주며 연결해줘야하는게 너무너무너무너무너무 불편했다. 

 

그래서 다른 PC에서도 실시간(?)으로 DB를 사용하고 작업할 수 있는 방법에 대해 구글링을 하다보니 원격접속이라는게 있었다! 몇가지 블로그들을 찾아보니 커맨드창에서 직접 쿼리문을 이용하여 원격접속 설정을 하는 것이 정석인 것 같다.

 

그런데 나는 커맨드창을 좋아하지 않는다. (아니, 커맨드창이 날 좋아하지 않는다.)

그래서 보기 좋고 설정하기 편한 MySQL 워크벤치를 이용해서 원격 접속을 설정하였다!

 

 

준비물
서버PC (DB를 공유해줄 PC)
      |
접속PC (DB를 공유받을 PC)
  • 서버PC의 IP주소만 제대로 알고 있으면, 인터넷 환경이 같지 않아도 된다.
    (나 같은 경우 서버PC는 유선 환경ㅡ즉 본체에 랜선 장착 중, 그리고 접속PC는 무선 환경ㅡ즉 Wifi로 연결중이었다!)
  • 각 PC마다 워크벤치가 설치되어 있어야한다.
  • 서버PC가 전원이 켜져있어야지만 접속PC로 DB를 다룰 수 있다.

 


1. 서버PC의 Workbench에서 root계정으로 접속 한 뒤, 새로운 계정을 생성한다.

  • 워크벤치 좌측의 Users and Privileges - Add Account 클릭
  • Login Name : 새로운 계정명. 아무거나 자유롭게
  • Limit to Hosts Matching : 접근 가능한 IP를 제한해주는 부분.
    - % : 모~~든 IP에서 접근 가능하다.
    - 특정 IP주소 : 기입한 IP주소로만 접근이 가능하다.
      (예를들어 내 DB에 접근 할 수 있는건 내 노트북 뿐이다 -> 내 노트북의 IP주소를 기입)
  • 비밀번호를 자유롭게 입력하고 우측 하단의 Apply 클릭하여 계정 생성을 완료한다.

2. 접근허용할 스키마를 선택한 후, 스키마에 대한 새계정의 권한을 설정한다.

  • 좌측에 생성된 계정을 클릭하고, 우측에서 Schema Privileges 탭을 선택 - Add Entry 클릭
  • 접근허용할 스키마(테이블)를 설정할 수 있다.

  • All Schema : 현재 root 계정에 있는 모든 DB에 접근할 수 있게 한다.
    - 지금 설정은 모두 프로그래밍 연습을 위한 접속이므로 일단 어느 컴에서나 모든 DB를 다 관리할 수 있게 All Schema를 선택했다. 특정 스키마만 필요한 부분이라면 Selected schema 에서 선택할 수 있다.


  • 추가된 스키마 목록을 클릭하고 , 하단에서 새로운 계정에서 사용가능한 DDL, DML 권한을 설정한다. 
    - 이 부분 또한 연습을 위한 목적이므로 모든 권한을 부여하도록 Select "ALL"을 클릭하여 적용해주었다.

3. 접속할 PC에서 워크벤치를 켜고, 새로운 연결을 등록해준다.

  • 워크벤치의 메인화면에서 캡쳐에 표시된 +를 누르고 Setup New Connection창을 열어준다.
    - Connection Name : 연결명. 아무거나 알아보기 쉽게 입력해준다.
    - Hostname : 서버 PC의 IP주소를 적어준다.

커맨드에서 ipconfig 를 입력면 나오는 정보 중, IPv4 주소에 적힌 192.xxx.xxx.xxx 형식의 IP주소를 적어주면 된다. 

  • Username : 아까 서버PC에서 만들어둔 새로운 계정이름을 입력한다. 
  • Store in Vault ... 를 클릭하고 새로운 계정의 비밀번호를 입력하고 OK - OK !
  • 여기서 연결이 성공했다면, 만들어둔 Connection으로 접속하면 서버PC에서 설정한대로 스키마들이 들어와있고, 관리할 수 있다. 

연결에 성공한 접속 PC에서 DB를 만지면, 만진 내용이 바로 서버PC에서도 동기화 되어 있는 것을 알 수 있다!