• JSP를 이용한 웹프로그램을 개발중이라면 일반 웹호스팅이 아닌 jsp호스팅을 이용해야한다.
  • 따로 도메인을 구매하지 않으면, cafe24 사이트에 가입할 때 사용한 아이디가 웹페이지 주소가 된다.
    예를 들면 아이디를 abc123으로 만들었으면 abc123.cafe24.com 이런식으로.

 

 

cafe24에 가입한 후,

호스팅센터로 들어가보면 수많은 호스팅 종류가 있다.

그중에 Tomcat JSP호스팅을 선택한다.

cafe24 호스팅 선택 화면
cafe24에서 제공하는 Tomcat JSP호스팅 종류

실제로 사람들이 많이 찾아오는 웹사이트를 운영하는 것이 아니라 포트폴리오용으로 가지고 있을 사이트이기때문에 절약형으로 선택했다.

DB는 MariaDB가 기본인것 같지만 MySQL도 지원한다.

Tomcat 8.0.x 로 되어있었으나 실제 Tomcat 9.0.x을 사용한 프로젝트도 잘 구동됐다.

호스팅 신청을 하고 결제까지 하고나면 따로 기다릴 필요 없이 바로 세팅이 된다.

 

로컬에만 담겨있던 내 프로젝트가 로컬을 탈출하기 위해서 필요한 프로그램이 세개 있다.

그리고 배포할 프로젝트의 WAR파일도 필요하다.

 

[이클립스 기준]

프로젝트명 우클릭 - Export - WAR file

Destination : war파일을 저장할 위치를 선택한다.

보통 도메인주소만 입력 후 엔터를 쳤을 때

바로 웹페이지가 나오게 하려면(abc123.cafe24.com) 파일명을 ROOT.war로 저장하면 되고,

웹페이지주소/프로젝트명(abc123.cafe24.com/bakingdom) 이런식으로 여러가지 프로젝트를 배포하려면 프로젝트명.war로 저장하면 된다.

나는 한 두가지 프로젝트를 나누어 담고 싶었으므로 bakingdom.war로 저장했다.

 

 

FileZilla

[파일] - [사이트 관리자]

[New site]

- 프로토콜 : SFTP

- 호스트 : 아이디.cafe24.com

- 포트 : 22

- 로그온 유형 : 일반 (또는 보통)

- 사용자 : 아이디

- 비밀번호 : FTP비밀번호 (cafe24 회원 비밀번호가 아닌, 호스팅 신청시 새로 설정한 FTP 비밀번호이다.)

[연결]

 

로컬사이트(내 PC)에서 아까 저장한 war 파일을 찾아

리모트사이트(cafe24)의 tomcat/webapps 폴더안으로 로 넘겨준다. 

이걸 쓰고 있는 시점에서는 이미 설정을 모두 완료했기때문에 bakingdom.war 그리고 bakingdom이라는 폴더가 리모트사이트쪽에 이미 생성되어 있는데, 처음엔 이런게 없고 기본적으로 manager, host-manager, ROOT 폴더만 달랑 있을 것이다. 아무튼 war파일을 옮겨주고 나도 별로 달라지는 일은 없다. 

※ 참고 : cafe24에서 안내하는 FTP사용 방법

 

 

PuTTY

일단 그 다음은 PuTTY로 서버를 실행시킨다.

PuTTY를 실행시키면 처음 나오는 창에서 설정해줄 내용은

Host Name (아이디.cafe24.com)

Port (22)

이 두개뿐이다.

설정을 하고나면 login as: 라는 문구와 함께 까만 창이 뜨는데,

cafe24 아이디를 입력해주고 enter

그리고 password를 입력해준다. 역시 cafe24회원 비밀번호가 아닌 FTP에서 사용했던 비밀번호를 입력한다. 입력을 할때 ***같은 입력표시가 나지 않고 아무것도 써지지 않는데, 어쨌든 그냥 키보드로 다 입력하고 엔터를 누르면 알아서 서버가 가동된다.

./tomcat/bin/startup.sh 를 입력하면 톰캣이 실행된다.


 

톰캣을 실행시키고 나서

다시 FileZilla로 돌아와 새로고침을 눌러보면,

아까 단순히 war파일이 전송되어 있기만 했던 리모트 폴더에

bakingdom 이라는 폴더가 생성되고

자동으로 그 안에 war파일 안에 있던 내용들이 담겨져있다.

만약 ROOT.war로 저장했었다면 따로 폴더가 더 생기진 않고

기존에 있던 ROOT폴더 안에 war의 내용들이 들어가 있을 것이다.

 

 

이렇게 war파일이 적용된 것을 확인하고 

도메인주소로 접속해보면! (배포파일명을 ROOT.war로 설정한 경우엔 아이디.cafe24.com 만 입력하여 접속한다.)

잘 접속이 된다. 

 

 

그런데 DB와 연결되어 있는 게시판을 들어가면 에러가 발생한다.

root-context.xml에서 설정해준 jdbc url이 아직도 로컬로 적용되어 있기때문이다. 

	<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
	    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
	    <property name="url" value="jdbc:mysql://아이디.cafe24.com:3306/아이디?useSSL=false&amp;allowPublicKeyRetrieval=true&amp;serverTimezone=Asia/Seoul" />
	    <property name="username" value="아이디"/>
	    <property name="password" value="비밀번호"/>
  	</bean>

url의 value부분을 위와 같이 작성하고, 아이디와 비밀번호도 올바르게 써준다.

바뀐 내용을 저장하고 root-context.xml파일을 FTP를 통해 리모트사이트로 넘겨 덮어쓰기 해준다.

 

그리고 워크벤치로 로컬에서 관리하던 sql파일들을 HeidiSQL프로그램으로 옮겨준다.

 

HeidiSQL

 

HeidiSQL에서 세션관리자 창을 열고

세션이 아무것도 없으면 신규 버튼을 눌러 새로운 세션을 만들어준다.

 

네트워크 유형 : MariaDB or MySQL (TCP/IP)

호스트명 / IP : 아이디.cafe24.com

사용자 : 아이디

암호 : 비밀번호

포트 : 3306

 

기존에 워크벤치에서 사용하던 sql파일들을 export해두었기때문에, 

SQL 파일 실행... 메뉴를 통해 한꺼번에 테이블을 설정해주었다.

 

기존 sql파일을 정상적으로 불러온 모습

 

 

 

※ MySQL 8.0의 sql파일을 불러올 때 Unknown collation: 'utf8mb4_0900_ai_ci' 에러 해결 방법

에러가나는 SQL파일을 불러오기 한 후, 해당 쿼리문 중에 COLLATE=utf8mb4_0900_ai_ci 부분을

utf8mb4_general_ci 로 변경해주고 저장한 후 쿼리를 실행하면 테이블이 정상적으로 생성된다.

 

 

※ Host '아이피주소' is not allowed to connect to this MariaDB server 에러발생시

아래 MySQL 외부 IP 접근 설정 방법을 통해 에러가 나는 아이피주소를 카페24호스팅센터에서 추가로 등록해준다.

이 아이피는 네이버에서 '내 IP'를 검색하면 나오는 IP! 내 로컬PC의 IP이다!

설정하기 버튼을 통해 IP등록을 마친 후의 모습

 

 

 

 

카페24 호스팅센터의 나의서비스관리 페이지를 가보면,

메뉴 중 [호스팅관리] - [기본관리] - [서비스 사용현황] 페이지에서

 

상단의 서버아이피를 확인한 후,

하단의 MySQL 외부 IP 접근설정 부분에서 설정하기 버튼을 눌러 서버아이피를 등록해준다.

 

 

 

 

 

 

 

이제 바뀐 설정들이 많으니 톰캣 서버를 종료했다가 다시 재실행을 하면 된다.

PuTTY프로그램을 다시 열어 놓고

./tomcat/bin/shutdown.sh 를 입력하면 톰캣 서버가 종료된다.

톰캣 서버를 종료했다가 다시 재시작한 모습이다.

 

 

 

DB를 이용한 페이지도 에러없이 잘 구동되는 모습.

 

 

 

 

 

 

 

처음 해보는 웹사이트 배포여서 상당히 애먹었다. 5시간 정도를 삽질 ㅠㅠ 한 느낌이지만 어쨌든 해냈으니까 기록으로 남겨둔다!!!

'SPRING' 카테고리의 다른 글

요청(HttpServletRequest)과 응답(HttpServletResponse)  (0) 2021.07.22

 

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 전송 안됨)

 

 

root-context.xml 
  • 로그인 시도 할 때 로그인 정보가 틀리면 리다이렉트 되야하는데,
    어느날 갑자기 그렇게 되지 않고 public key retrieval is not allowed 에러 페이지가 나옴.
  • 해결하려면 url의 value에 아래 코드를 추가한다.

useSSL=false&amp;allowPublicKeyRetrieval=true

<property name="url"
    value="jdbc:mysql://localhost:3306/bakingdom?useSSL=false&amp;allowPublicKeyRetrieval=true&amp;serverTimezone=Asia/Seoul">
</property>

 

목표

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 "";
  }

 

실행 결과

 

 

에러내용 : 이메일주소를 넣고, 임시비밀번호를 메일로 발급받는 기능을 구현하던 중에 발생한 에러.
org.springframework.mail.MailSendException: Mail server connection failed; nested exception is javax.mail.MessagingException: Could not convert socket to TLS; nested exception is: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate). Failed messages: javax.mail.MessagingException: Could not convert socket to TLS; nested exception is: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate); message exceptions (1) are: Failed message 1: javax.mail.MessagingException: Could not convert socket to TLS; nested exception is: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

 

해결방법 : root-context.xml에 다음 prop을 추가한다.
<prop key="mail.smtp.ssl.trust">smtp.gmail.com</prop> 
<prop key="mail.smtp.ssl.protocols">TLSv1.2</prop>
  • 나같은 경우, <prop key="mail.smtp.ssl.trust">smtp.gmail.com</prop> 는 생략해도 정상적으로 구동되었다.

 

적용한 코드

  <bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
    <property name="host" value="smtp.gmail.com" />
    <property name="port" value="587" />
    <property name="username" value="발송메일주소" />
    <property name="password" value="비밀번호" />
    <property name="javaMailProperties">
        <props>
            <prop key="mail.transport.protocol">smtp</prop>
            <prop key="mail.smtp.auth">true</prop>
            <prop key="mail.smtp.starttls.enable">true</prop>
            <prop key="mail.debug">true</prop>
            <!-- <prop key="mail.smtp.ssl.trust">smtp.gmail.com</prop> -->
            <prop key="mail.smtp.ssl.protocols">TLSv1.2</prop>
        </props>
    </property>
  </bean>

세션과 쿠키

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 경로에 있는 받는 변수값 뒤에 :.+ 를 붙여주는 것. 

 

 

 

아래는 바꾼 코드

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