데이터융합 JAVA응용 SW개발자 기업 채용연계 연수과정 66일차 강의 정리

misung·2022년 7월 8일
0

Spring

SpringWebMvcProject (이어서)

글 수정/삭제 버튼 관련 처리

현재 상태 기준으로는 자신이 쓴 글이 아님에도 글을 수정/삭제하는 버튼이 표시되어 있다. 따라서 남의 글은 수정/삭제할 수 없도록 해야 한다.

<c:if test="${login.name == article.writer}">
	          	<input id="mod-btn" class="btn" type="button" value="수정"
				style="background-color: orange; margin-top: 0; height: 40px; color: white; border: 0px solid #388E3C; opacity: 0.8">&nbsp;&nbsp;
	          
	         	<input class="btn" type="submit" value="삭제" onclick="return confirm('정말로 삭제하시겠습니까?')"
				style="background-color: red; margin-top: 0; height: 40px; color: white; border: 0px solid #388E3C; opacity: 0.8">&nbsp;&nbsp;
	          </c:if>

<c:if> 태그로 감싸고 조건의 경우에는 세션 파라미터 중 유저 로그인 정보인 login (UserVO) 과, 게시물 정보인 article 중 서로의 닉네임과 작성자를 비교했는데, 사실 이렇게 비교하는 것은 좋지 못하다.

유니크한 (PRIMARY KEY, UNIQUE) 회원 번호나, 아이디 등으로 비교하는게 옳은데, 닉네임은 중복될 수 있기 때문이다.

물론 우리는 회원번호에 대한 처리를 하지 않았으므로 닉네임 비교에서 그치도록 한다. 그리고 약간의 팁으로는, 요즘엔 id도 개인정보에 속하기 때문에 고유한 회원번호로 처리를 해주는 것이 좋다고 한다.

인터셉터 (Interceptor)

이제부턴 게시판을 회원제 게시판으로 전환하려고 한다. 그렇게 하려면, 회원만이 글을 쓸 수 있도록 처리를 해 줄 필요가 있다.

예전엔 <c:if> 로 로그인 세션이 있는지를 검사해서 글을 쓸 수 있는지 없는지 여부를 판단해줬었는데, 우리는 이것을 모든 jsp 페이지마다 해줄 수는 없다.

그래서 jsp 에선 필터를 사용해서 컨트롤러에 요청이 들어가기 전에 처리를 해 줬었는데, 우리는 필터보다 더 유용한 Spring 라이브러리의 Interceptor 를 사용하기로 한다.

그러면 필터를 안 쓰는 것이냐? 하겠지만 필터는 필터대로 사용하고, 인터셉터는 인터셉터대로 사용한다고 보면 된다.

우리는 web.xml 에서 한글 인코딩 필터를 이미 사용중이다.

web.xml

...
<!-- 한글 인코딩 필터 설정(톰캣 내부의 한글처리) -->
	<filter>
		<filter-name>encodingFilter</filter-name>
		<filter-class>
			org.springframework.web.filter.CharacterEncodingFilter
		</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<!-- 위에 지정한 encodingFilter이름을 모든 패턴에 적용 -->
	<filter-mapping>
		<filter-name>encodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
...

이렇게, 모든 페이지에 대해서 요청이 들어갈 때 인코딩 처리를 해 주고 있다.

인터셉터의 동작 방식에 대해 살펴보자면,

  1. 요청이 들어옴
  2. 디스패처 서블릿이 요청을 받고 요청을 처리함
  3. 컨트롤러로 요청이 넘어가기 전에 인터셉터가 요청을 가로챔

preHandle(), postHandle(), afterCompletion() 세 개의 메서드가 있는데, 원하는 메서드를 구현해서 사용하면 된다.

3-1) preHandle() : 컨트롤러 실행 전에 실행됨. false를 리턴하는 경우 컨트롤러로 요청을 넘기지 않아 작업 수행을 중지시킨다.

3-2) postHandle() : 컨트롤러 실행 후 실행됨

3-3) afterCompletion() : 리졸버뷰에 뷰를 전송한 후 실행됨

인터셉터를 사용하려면 HandlerInterceptorAdapter 클래스를 상속받아 사용한다.

인터셉터 구현 및 테스트해보기

com.spring.mvc.board.commons.interceptor 패키지 하위에 BoardInterceptor 클래스 생성.

extends 키워드를 사용하여 클래스를 구현하려고 하면 deprecated (곧 없어질 문법) 라고 뜨므로, implements로 구현한다. (HandlerInterceptorAdapter 가 아닌, HandlerInterceptor 를 구현하게 될 것)

package com.spring.mvc.board.commons.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class BoardInterceptor implements HandlerInterceptor {
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		System.out.println("게시판 인터셉터 활동 : prehandle");
		return true;
	}
	
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		
		System.out.println("게시판 인터셉터 활동 : posthandle");
	}
}

기존에 작성되어있던 메서드들의 내용을 저렇게 바꿨다.
각 메서드 구현 시에는 자동완성으로 구현하였다.

이제 빈 등록을 해야 한다.

인터셉터 빈 등록하고 확인하기

servlet-config.xml

<!-- 인터셉터 빈 등록 양식 -->
	<beans:bean id="boardInterceptor" class="com.spring.mvc.board.commons.interceptor.BoardInterceptor" />
	
	<interceptors>
		<interceptor>
			<!-- <mapping path="/board/**"/> /board 로 시작하는 모든 요청에 반응 -->
			<mapping path="/board/write"/>
			<mapping path="/board/content/**"/> <!-- 컨텐츠(글)은 보드번호가 있으므로 뒤에 주소가 더 올 수 있다고 표시해줘야 함. 안해주면 고장! -->
			<beans:ref bean="boardInterceptor" />
		</interceptor>
	</interceptors>

등록이 잘 되었으면 beans graph 탭에 잘 뜰 것이다.

이제 확인을 위해서 프로젝트를 기동하고, 게시판 탭에 들어가 글 작성 버튼을 눌러 글 쓰기 페이지로 이동하면,

preHandle -> postHandle 순으로 인터셉터가 활동하는 것을 확인할 수 있다.

인터셉터를 통해 로그인하지 않은 사용자가 글 쓰기 페이지에 접근을 하지 못 하게 하기

이것을 하기 위해서 BoardInterceptor 클래스로 가자.

자, postHandle 메서드를 사용해서 페이지를 표시하기 전에 미리 login 세션 파라미터가 존재하는지 확인하려고 한다.

따라서 매개 변수에 HttpSession 객체를 받아와서 체크하려고 하면? 오류가 난다.

왜냐하면 메서드 오버라이딩은 부모와 매개 변수의 개수와 순서 및 이름, 반환형이 같아야 하는데 마음대로 바꾸면 당연히 오류가 난다.

그러면 세션을 어떻게 끌어다 써야 할까?
매개 변수 말고 메서드 내에다가 HttpSession request 객체를 구현하고 request.getSession() 이렇게 가져오면 된다.

BoardInterceptor.java

...
@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		System.out.println("게시판 인터셉터 활동 : prehandle");
		
		HttpSession session = request.getSession();
		
		if (session.getAttribute("login") == null) {
			System.out.println("회원 인증 실패");
			response.sendRedirect("/board/list");
			return false;
		}
		System.out.println("회원 인증 성공");
		return true;
	}
...

이렇게 구현하고 나서, 프로젝트를 기동해서 글쓰기 버튼을 눌러보거나 글 확인을 위해 아무 게시물이나 눌러보면 들어가지지 않을 것이고,

회원 인증 실패가 뜨는 것을 확인할 수 있다.

그런데 지금 상태에선 너무 밋밋한 (아무런 경고가 안 뜸) 상태이므로 경고창을 띄울 수 있게 해 보도록 하자.

BoardInterceptor.java

@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		System.out.println("게시판 인터셉터 활동 : prehandle");
		
		HttpSession session = request.getSession();
		
		if (session.getAttribute("login") == null) {
			System.out.println("회원 인증 실패");
			
			response.setContentType("text/html; charset=UTF-8");
			PrintWriter out = response.getWriter();
			
			String htmlCode = "<script> \r\n"
		               + "alert('로그인이 필요한 페이지 입니다.'); \r\n"
		               + "location.href='/board/list'; \r\n"
		               + "</script>";
			
			out.print(htmlCode);
			out.flush();
			
			return false;
		}
		System.out.println("회원 인증 성공");
		return true;
	}

.setContentType() 메서드로 보낼 컨텐츠의 타입을 정해놓고, PrintWriter 객체를 구현해 html 코드를 보낼 수 있도록 한다.

<script> 태그가 포함된 코드를 작성하고 out.print()out.flush() 로 요청을 보낸다.

이제 프로젝트를 기동해서 로그인 하기 전에 게시판 컨텐츠에 접근하려고 하면 경고창과 함께 잘 리다이렉트될 것이고, 아무 아이디로나 로그인 한 후 접근하면 접근이 잘 될 것이다.

글 작성 시 게시글 작성자 집어넣기

지금은 글 작성자를 자신의 실제 id에 관계없이 아무렇게나 작성해 넣을 수 있게 되어 있다. 이제 이것을 사용자의 id를 얻어와 채우도록 바꿔 보자.

write.jsp

<div class="form-group">
            <label>작성자</label>
            <input type="text" class="form-control" name='writer' value="${login.name}" readonly>
</div>

value 속성을 주고 속성값으로 login.name 을 준다. UserVO 객체의 name 이 작성자 창 부분에 뜨게 될 것이고, readonly 로 수정을 불가한 읽기 전용으로 바꿔 준다.

잘 적용되었음을 단번에 확인할 수 있다!

그리고 글 쓰기 상태에서 취소를 누르면 오류가 터질 텐데,

http://localhost/board/%3Cc:url%20value='/board/list'%20/%3E 이런 식으로 jstl이 그대로 노출되어있다. 왜냐하면 jstl을 가져다 쓰지 않았기 때문에.. jsp 파일 상단에 <taglib> 으로 가져다 쓰도록 해 둔다.

그러면 이제 취소 버튼을 눌러서 되돌아가는 것도 잘 될 것이다.

postHandle 메서드로 데이터 확인하기

BoardInterceptor.java

...
@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		
		System.out.println("게시판 인터셉터 활동 : posthandle");
		
		System.out.println("모델 객체의 내부 : " + modelAndView.getModel());
		
		Object data = modelAndView.getModel().get("article");
		System.out.println("article 이라는 이름의 데이터 : " + data);
		System.out.println("뷰페이지 이름 : " + modelAndView.getViewName());

		/*
		 	컨트롤러에서 로직을 처리하고 나가는 흐름을 붙잡아서,
		 	모델 데이터가 제대로 전송이 되는 지 확인하고 추가할 내용이나 수정할 내용이 있다면
		 	모델 객체를 받아와서 추가, 수정도 가능하다.
		 	기타 특징을 이용하여 흐름을 제어할 수도 있다. (sendRedirect, viewName을 수정 등)
		 */
	}
...

매개변수로 modelAndView 가 있으므로 저 변수를 활용해서 View 안에 담길 파라미터등을 확인할 수 있다.

잠시 BoardController 클래스를 참고하자면,

BoardController.java

...
//게시글 상세보기 요청
	@GetMapping("/content/{boardNo}")
	//@PathVariable은 URL 경로에 변수를 포함시켜 주는 방식
	//null이나 공백이 들어갈 수 있는 파라미터라면 적용하지 않는 것을 추천.
	//파라미터 값에 .이 포함되어 있다면 .뒤의 값은 잘린다는 걸 알아두세요.
	//{}안에 변수명을 지어주시고, @PathVariable 괄호 안에 영역을 지목해서
	//값을 받아옵니다.
	public String content(@PathVariable int boardNo, Model model, 
							@ModelAttribute("p") SearchVO search) {
		System.out.println("/board/content: GET");
		System.out.println("요청된 글 번호: " + boardNo);
		model.addAttribute("article", service.getArticle(boardNo));
		return "board/content";
	}
...

컨트롤러 클래스의 content() 메서드는 "article" 이라는 이름으로 파라미터를 설정해서 뷰 페이지로 보내고 있다.

따라서 우리는 postHandle() 메서드에서 "article" 을 가져와 볼 것인데, getModel() 메서드로 모델을 가져온 후 get() 메서드에 파라미터 이름을 넘겨 파라미터를 건네받는다.

중요한 것은, 반환 타입이 Object 형으로 되어 있으므로 데이터를 받을 변수를 Object data 로 선언해주고 출력을 확인해본다.

자동 로그인 기능 구현하기

유저 로그인 창에서 자동 로그인 체크박스를 만들어주고, 체크를 하면 WAS 측에서 자동 로그인 처리를 해 주도록 만들 것.

사용자가 브라우저를 열 때마다 생성되는 고유하게 생성되는 세션 ID(브라우저를 식별 가능한) 컬럼을 추가해서 저장하고, 자동 로그인 만료 시간이 얼마나 되는지도 DB에 저장하여 구현할 것이다.

  • session_id, expired_date 컬럼 추가

[login_modal.jsp] 자동 로그인 체크박스 만들기

login_modal.jsp

...
<!-- 자동 로그인 체크박스 -->
						<tr>
							<td>
								<input type="checkbox" id="auto-login" name="autoLogin">자동 로그인
							</td>
						</tr>
...

유저 패스워드 입력창 아래에 적용했다.

[login_modal.jsp] 자동로그인 체크여부 확인을 위한 변수 추가 작업

...
// 자동 로그인 체크박스가 체크가 되었는지의 여부
				const autoLogin = $('#auto-login').is(':checked');
				
				const userInfo = {
					"account" : id,
					"password" : pw,
					"autoLogin" : autoLogin
				};
...

로그인 버튼 클릭 이벤트 처리 부분에서 autoLogin 부분들을 추가했다.

그리고 UserVO 에도 autoLogin 을 추가한다.

UserVO.java

...
// 자동 로그인 체크 여부
	private boolean autoLogin;
...

또, DB에도 반영을 해 주어야 한다.

mvc_user 테이블

-- 자동 로그인 관련 컬럼 추가
ALTER TABLE mvc_user
ADD session_id VARCHAR2(80)
DEFAULT 'none' NOT NULL;

ALTER TABLE mvc_user
ADD limit_time DATE;

세션 ID를 저장할 session_id 와, 세션의 유효 시간을 저장할 limit_time 컬럼을 추가했다.

세션 ID의 경우에는 기본값을 none 으로 해 두었는데, 자동로그인을 안 하는 것을 기본으로 하고 있으므로 그렇게 해 준 것이다.

limit_time 의 경우 자동로그인을 신청한 사람의 경우에만 유효하므로 NOT NULL 제약조건은 걸어주지 않아도 된다.

그리고 추가적으로, DB에 추가한 컬럼을 UserVO 에도 반영한다.

UserVO.java

...
private String sessionId;
private Timestamp limitTime;
...

[UserController] 단에서 자동 로그인 처리 로직 추가

...
//로그인 요청 처리
	@PostMapping("/loginCheck")
	public String loginCheck(@RequestBody UserVO vo, /*HttpServletRequest request*/
								HttpSession session,
								HttpServletResponse response) {
		System.out.println("/user/loginCheck: POST");
		System.out.println("param: " + vo);
		
		//서버에서 세션 객체를 얻는 방법
		//1. HttpServletRequest 객체 사용
		//HttpSession session = request.getSession();
		
		//2. 매개값으로 HttpSession 객체 받아서 사용.
		
		
		UserVO dbData = service.selectOne(vo.getAccount());
		BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
		
		if(dbData != null) {
			if(encoder.matches(vo.getPassword(), dbData.getPassword())) {
				//로그인 성공 회원을 대상으로 세션 정보를 생성
				session.setAttribute("login", dbData);
				
				long limitTime = 60 * 60 * 24 * 90;	// 90일짜리 유효기간 쿠키
				
				// 자동 로그인 체크 시 처리해야 할 내용.
				if (vo.isAutoLogin()) {
					// 자동 로그인을 희망하는 경우, 쿠키를 이용하여 저장.
					System.out.println("자동 로그인 쿠키 생성 중");
					// 세션 ID를 가지고 와서 쿠키에 저장 (고유한 값 필요)
					Cookie loginCookie = new Cookie("loginCookie", session.getId());
					loginCookie.setPath("/"); // 쿠키가 동작할 수 있는 유효한 url. (메인 페이지에서만 유효한 쿠키가 됨)
					loginCookie.setMaxAge((int) limitTime);
					response.addCookie(loginCookie);
					
					// 자동 로그인 유지 시간을 날짜 객체로 변환. (DB에 삽입하기 위해, 밀리초)
					long expiredDate = System.currentTimeMillis() + (limitTime * 1000);
					
					// Date 객체의 생성자에 매개값으로 밀리초의 정수를 전달하면 날짜 형태로 변경해 준다.
					Date limitDate = new Date(expiredDate);
					
					System.out.println("자동 로그인 만료 시간 : " + limitDate);
				}
				
				return "loginSuccess";
			} else {
				return "pwFail";
			}
		} else {
			return "idFail";
		}
	}
...

UserController 에서 로그인 요청 처리를 하는 메서드에 로직을 추가한다.

long 타입으로 된 90일짜리 유효 시간 (limitTime) 을 먼저 잡아 놓는다.

if (vo.isAutoLogin()) 으로 자동 로그인 bool 변수가 체크되었는지를 판별해서 Cookie 객체를 생성해주고, 생성된 쿠키가 동작하는 곳은 "/" 루트로 한정한다. (이렇게 하면 홈 외에는 다른 곳에 전달되지 않는다)

그리고 아까 설정해 둔 유효시간을 넣는데, 해당 메서드는 int 형만 받으므로 int 형으로 캐스팅해준다.

그리고 response 객체를 통해서 쿠키를 추가해줘야 하는데, 우리는 이 메서드에서 해당 객체를 사용하기 위해 HttpServletResponse 객체를 매개변수로 받도록 한다.

그리고 자동 로그인 유지 시간을 밀리초가 아닌 날짜 객체로 변환하기 위해 currentTimeMillis() 부터 Date(expiredDate) 까지의 처리를 해 준다.

DB에 자동로그인 쿠키 유효시간 등록하기

지금까지 처리한 것으로는 WAS 상에서만 돌아가게 되어 있고, DB에 직접 데이터를 입출력하는 과정은 빠져있다.

IUserMapper.java

...
// 자동 로그인 쿠키값 DB 저장 처리
	void keepLogin(Map<String, Object> Data);
...

IUserMapper 인터페이스에 해당 메서드를 추가해둔다.
sessionId와 limitDate 두 개를 전달해야 하는데, MyBatis에서 한번에 두 개의 데이터를 전달할 방법이 없으므로 아래와 같은 방법 중 하나를 사용해야 한다.

  1. @Param 사용
  2. Map으로 포장
  3. 객체를 디자인하여 객체로 전달

3번의 방법은 전에 사용한 경우가 있었지만 이 경우에는 겨우 2개의 데이터를 전달하기 위해서는 낭비이므로, Map으로 포장하는 방법을 선택하였다.

UserMapper.xml

...
<!-- 자동 로그인을 체크한 경우 쿠키값(세션 아이디)과 유효시간을 갱신 -->
	<update id="keepLogin">
		UPDATE mvc_user
		SET session_id=#{sessionId}, limit_time=#{limitDate}
		WHERE account=#{account}
	</update>
...

유저 자동로그인 쿠키는 처음에 이미 none 상태로 생성되어 있고 그것을 계속 갱신하는 것이므로, INSERT 가 아니라 UPDATE 로 만들어 두어야 한다.

IUserService.java

...
// 자동 로그인 쿠키값 DB 저장 처리
	void keepLogin(String session, Date limitTime, String account);
...

그리고 당연히 UserService 클래스에도 반영해야 한다.

UserService.java

...
// 자동 로그인 쿠키값 DB 저장 처리
	@Override
	public void keepLogin(String session, Date limitTime, String account) {
		Map<String, Object> data = new HashMap<>();
		data.put("sessionId", session);
		data.put("limitDate", limitTime);
		data.put("account", account);
		
		mapper.keepLogin(data);
	}
...

아까랑 다르게 Map 이 아닌 이유는, 컨트롤러쪽에서 포장을 해서 넘겨줄게 아니기 때문이다.

UserController.java

...
if(dbData != null) {
			if(encoder.matches(vo.getPassword(), dbData.getPassword())) {
				
                ...
                
				// 자동 로그인 체크 시 처리해야 할 내용.
				if (vo.isAutoLogin()) {
					
                    ...
					
					service.keepLogin(session.getId(), limitDate, vo.getAccount());
				}
				
				return "loginSuccess";
			} else {
				return "pwFail";
			}
		} else {
			return "idFail";
		}
...

해당 메서드에서 서비스의 keepLogin() 메서드를 호출하여 각각 세션ID, 유효시간, 유저 계정명을 보낸다.

인터셉터를 통해 자동 로그인 처리 하기

자동 로그인 처리를 해 주려면, 사용자가 페이지를 로드하기 전에 저장된 쿠키의 세션ID를 꺼내 와서, DB에 있는 세션 ID와 비교해서 그것이 존재하면 자동 로그인 처리를 해 주어야 한다.

그러기 위해서는 인터셉터를 사용해야 한다.

com.spring.mvc.user.commons.interceptor 패키지 하위에 AutoLoginInterceptor 클래스 생성

AutoLoginInterceptor.java

package com.spring.mvc.user.commons.interceptor;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.WebUtils;

import com.spring.mvc.user.model.UserVO;
import com.spring.mvc.user.repository.IUserMapper;

public class AutoLoginInterceptor implements HandlerInterceptor {
	
	@Autowired
	private IUserMapper mapper;
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		
		System.out.println("자동 로그인 인터셉터 발동!");
		
		// 1. 자동 로그인 쿠키가 있는지의 여부를 확인.
		// -> loginCookie의 존재 유무를 확인.
		
		// 원하는 쿠키의 값을 한방에 꺼내올 수 있다. (request 객체, 쿠키 이름 전달)
		Cookie loginCookie = WebUtils.getCookie(request, "loginCookie");
		
		// 자동 로그인을 신청한 사람이라면 로그인 유지를 위해 세션 데이터를 만들어 준다.
		HttpSession session = request.getSession();
		if (loginCookie != null) {	// 자동 로그인을 이전에 신청한 적이 있다는 뜻
			// 2. DB에서 쿠키값(세션ID)과 일치하는 세션ID를 가진 회원의 정보를 조회
			UserVO vo = mapper.getUserWithSessionId(loginCookie.getValue());
			System.out.println("DB에서 가지고 온 값 : " + vo);
			if (vo != null) {
				// 자동 로그인 신청한 사람의 로그인 데이터 (세션) 를 만들어 준다.
				session.setAttribute("login", vo);
				System.out.println("세션 제작 완료!");
			}
		}
		
		/*
		 	true면 컨트롤러로 요청이 들어가고, false면 요청을 막는다.
		 	자동 로그인 신청 여부와 상관없이 홈 화면은 무조건 봐야 하니 true를 작성한다.
		 */
		return true;
	}
}

UserService.java

...
@Autowired
private IUserMapper mapper;
    
// 자동 로그인 쿠키값 DB 저장 처리
	@Override
	public void keepLogin(String session, Date limitTime, String account) {
		Map<String, Object> data = new HashMap<>();
		data.put("sessionId", session);
		data.put("limitDate", limitTime);
		data.put("account", account);
		
		mapper.keepLogin(data);
	}
...

IUserMapper.java

...
// 세션 아이디를 통한 회원 정보 조회 기능
	UserVO getUserWithSessionId(String sessionId);
...

UserMapper.xml

...
<resultMap type="com.spring.mvc.user.model.UserVO" id="UserMap">
		<result property="regDate" column="reg_date" />
		<result property="sessionId" column="session_id" />
		<result property="limitTime" column="limit_time" />
	</resultMap>

<!-- 
		자동 로그인을 신청했던 회원이 다시 사이트에 방문 시
		로컬에 저장된 쿠키값(세션ID)과 일치하는 회원의 모든 정보를
		조회하는 SQL문
	 -->
	<select id="getUserWithSessionId" resultMap="UserMap">
		SELECT * FROM mvc_user
		WHERE session_id=#{session_id}
	</select>
...

빈 등록까지 마치고 나면 이제 자동 로그인을 체크해 둔 경우 브라우저를 완전히 종료했더라도 웹 페이지에 접근하면 알아서 자동 로그인이 되게 된다.

로그아웃 처리

컨트롤러의 logout() 메서드 부분에서 로그아웃 처리를 추가적으로 해 주어야 한다. 왜냐하면 로그아웃 처리 부분, 현 상태에선 로그아웃을 하더라도 자동로그인 쿠키때문에 메인페이지로 가면 자동으로 로그인이 되어버리기 때문이다.

이 부분을 고치기 위해서 자동 로그인 쿠키가 존재하는지 확인하고, 존재하는 경우 아래와 같이 바꿔주어야 한다.

  • 쿠키의 수명을 0으로
  • setPath를 동일하게 지정
  • DB의 내용 또한 바꿔줘야 함
  • 세션ID : none, 만료 시간 : 지금 이 시간

이 문제에 대해 해결할 때 어떻게 접근해야하나 막막했는데, 애초에 개념 자체를 명확하게 외워두지 않아서 문제에 접근조차 할 수 없었다.

HttpServletRequest 객체에 쿠키가 담겨있으므로, 매개 변수로 저것을 받아서 체크를 하면 됐던 것으로, 모르면 접근이 불가능했다.

UserController.java

...
//로그아웃 처리
	@GetMapping("/logout")
	public ModelAndView logout(HttpSession session, RedirectAttributes ra,
								HttpServletRequest request,
								HttpServletResponse response) {
		System.out.println("/user/logout: GET");
		
//		session.invalidate();
		UserVO user = (UserVO)session.getAttribute("login");
		
		session.removeAttribute("login");
		
		/*
		 	자동 로그인 쿠키가 있는지 확인 (없는 사람도 있는 경우가 있으니)
		 	쿠키가 존재하는 경우 -> 쿠키의 수명을 0
		 	쿠키를 지울 때도 setPath를 동일하게 지정 및 DB의 내용도 변경
		 	세션ID : 'none', 만료 시간 : 지금 이 시간
		 */
		
		Cookie loginCookie = WebUtils.getCookie(request, "loginCookie");
		if (loginCookie != null) {
			loginCookie.setMaxAge(0);
			loginCookie.setPath("/");
			response.addCookie(loginCookie);
			service.keepLogin("none", new Date(), user.getAccount());
		}
		
		ra.addFlashAttribute("msg", "logout");
		
//		ModelAndView mv = new ModelAndView();
//		mv.setViewName("/");
		
		return new ModelAndView("redirect:/");
	}
...
UserVO user = (UserVO)session.getAttribute("login");

우선, 유저 ID가 담겨있는 세션 파라미터를 삭제하기 전에 받아서 저장해 둔다. 이유는 밑에서 확인한다.

Cookie loginCookie = WebUtils.getCookie(request, "loginCookie");
		if (loginCookie != null) {
			loginCookie.setMaxAge(0);
			loginCookie.setPath("/");
			response.addCookie(loginCookie);
			service.keepLogin("none", new Date(), user.getAccount());
		}

쿠키는 리퀘스트 객체에서 찾을 수 있다. WebUtils 클래스의 getCookie() 메서드를 통해서 리퀘스트 객체와 쿠키 이름을 넘기면, 쿠키를 찾아서 받아올 수 있따.

그리고 모든 사용자에 대해서 자동로그인 쿠키가 생성되는게 아니므로, 로그인 쿠키가 존재하는지 확인 작업이 필요하다.

확인한 후, 쿠키의 수명을 0으로 만들어 주고, 동작할 Path를 기존 쿠키와 동일하게 다시 설정해 주어야 한다.

그리고 응답이 나가야 하므로 response 객체를 사용해야 하는데 당연히 매개 변수에 HttpServletRespone 객체를 추가해 두어야 한다. (마찬가지로 request 또한 HttpServletRequest 를 추가해 두자)

addCookie() 메서드를 사용하여 방금 만든 쿠키를 response 객체에 담고, DB에도 이 내용을 반영해야 하므로 우리가 만든 keepLogin() 메서드에 매개값을 차례로 넘긴다.

로그인 쿠키의 세션ID는 이제 없을 것이므로 "none" 으로, 현재 시간을 넘기는 방법은 매개 변수가 없는 Date 생성자를 new Date() 이런 식으로 넘기면 된다.

마지막으로 현재 유저의 계정명을 넘겨야 하는데, 아까 위에서 세션에 담긴 유저 정보를 그냥 날리면 유저 ID정보를 가져올 수 없으므로, 삭제하기 전에 미리 UserVO 로 담아둔 다음 여기서 활용해야 한다.

이제 로그아웃을 해 보면 저절로 자동 로그인이 되는 문제는 해결되었을 것이고, 이것으로 이번 프로젝트는 마무리되었다.

MyWeb 프로젝트

com.spring.myweb 기본 패키지로 잡고, 기본적인 프로젝트 생성 튜토리얼 적어둔 것을 참고해서 라이브러리 로드 및 업데이트를 해줄 것.

이번에는 패키지를 나누는 방식을 이전과는 조금 다르게 할 것.

DB 생성

 CREATE TABLE freeboard(
    bno NUMBER(10, 0),
    title VARCHAR2(300) NOT NULL,
    writer VARCHAR2(50) NOT NULL,
    content VARCHAR2(2000) NOT NULL,
    regdate DATE DEFAULT sysdate,
    updatedate DATE DEFAULT NULL
);

ALTER TABLE freeboard
ADD CONSTRAINT freeboard_pk PRIMARY KEY(bno);

CREATE SEQUENCE freeboard_seq
    START WITH 1
    INCREMENT BY 1
    MAXVALUE 5000
    NOCYCLE
    NOCACHE;

테이블을 만들고, 제약조건을 걸어준 후 시퀀스까지 생성해준다.

[FreeBoardVO] 보드 VO 생성

~.command 패키지 하위에 생성

package com.spring.myweb.command;

import java.sql.Timestamp;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/*
  CREATE TABLE freeboard(
    bno NUMBER(10, 0),
    title VARCHAR2(300) NOT NULL,
    writer VARCHAR2(50) NOT NULL,
    content VARCHAR2(2000) NOT NULL,
    regdate DATE DEFAULT sysdate,
    updatedate DATE DEFAULT NULL
);

ALTER TABLE freeboard
ADD CONSTRAINT freeboard_pk PRIMARY KEY(bno);

CREATE SEQUENCE freeboard_seq
    START WITH 1
    INCREMENT BY 1
    MAXVALUE 5000
    NOCYCLE
    NOCACHE;
 */

@Getter
@Setter
@ToString
public class FreeBoardVO {

	private int bno;
	private String title;
	private String writer;
	private String content;
	private Timestamp regDate;
	private Timestamp updateDate;
	private boolean newMark;
	
}

[IFreeBoardMapper] 인터페이스 클래스 생성

~.freeboard.mapper 패키지 하위에 생성

package com.spring.myweb.freeboard.mapper;

import java.util.List;

import com.spring.myweb.command.FreeBoardVO;
import com.spring.myweb.util.PageVO;

public interface IFreeBoardMapper {
	
	//글 등록
	void regist(FreeBoardVO vo);
	
	//글 목록
	List<FreeBoardVO> getList(PageVO vo);
	
	//총 게시물 수
	int getTotal(PageVO vo);
	
	//상세보기
	FreeBoardVO getContent(int bno);
	
	//수정
	void update(FreeBoardVO vo);
	
	//삭제
	void delete(int bno);

}

[PageVO] 페이지 VO클래스 생성

~.util 패키지 하위에 생성

package com.spring.myweb.util;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class PageVO {
	
	//사용자가 선택한 페이지 정보를 담을 변수.
	private int pageNum;
	private int cpp;
	
	//검색에 필요한 데이터를 변수로 선언.
	private String keyword;
	private String condition;
	
	public PageVO() {
		this.pageNum = 1;
		this.cpp = 20;
	}

}

[PageCreator] 클래스 생성

~.util 패키지 하위에 생성

package com.spring.myweb.util;

import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class PageCreator {

	private PageVO paging;
	private int articleTotalCount;
	private int endPage;
	private int beginPage;
	private boolean prev;
	private boolean next;
	
	private final int buttonNum = 5;
	
	//URI 파라미터를 쉽게 만들어 주는 유틸 메서드
	public String makeURI(int page) {
		UriComponents ucp = UriComponentsBuilder.newInstance().queryParam("page", page)
															  .queryParam("cpp", paging.getCpp())
															  .queryParam("keyword", paging.getKeyword())
															  .queryParam("condition", paging.getCondition())
															  .build();
		return ucp.toUriString();
	}

	
	private void calcDataOfPage() {
		
		endPage = (int) (Math.ceil(paging.getPageNum() / (double) buttonNum) * buttonNum);
		
		beginPage = (endPage - buttonNum) + 1;
		
		prev = (beginPage == 1) ? false : true;
		
		next = articleTotalCount <= (endPage * paging.getCpp()) ? false : true;
		
		if(!next) {
			endPage = (int) Math.ceil(articleTotalCount / (double) paging.getCpp()); 
		}
		
	}
	
	//컨트롤러가 총 게시물의 개수를 PageCreator에게 전달한 직후에 
	//바로 페이징 버튼 알고리즘이 돌아갈 수 있도록 setter를 커스텀.
	public void setArticleTotalCount(int articleTotalCount) {
		this.articleTotalCount = articleTotalCount;
		calcDataOfPage();
	}
	
}

디렉토리 이름 및 파일명 변경

/src/main/webapp/WEB-INFspring 이라는 디렉토리 이름을 config 로 변경하고, servlet-context.xmlservlet-config.xml 로, root-context.xmldb-config.xml 로 바꿨다.

[servlet-config.xml] 수정

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

	<!-- 자동으로 컨트롤러와 매핑 메서드 탐색 (핸들러 매핑, 핸들러 어댑터 빈 등록) -->
	<annotation-driven />

	<!-- css, img, js... 의 파일 경로가 복잡할 때 많이 사용합니다. -->
	<!-- jsp 파일 같은 데서 경로 긴거 일일히 쓰기 귀찮잖아요. 그래서 선언합니다. -->
	<!-- 내부 경로를 숨겨주는 역할도 해요. -->
	<resources mapping="/resources/**" location="/resources/" />
	<resources mapping="/img/**" location="/resources/img/" />
	<resources mapping="/css/**" location="/resources/css/" />
	<resources mapping="/fonts/**" location="/resources/fonts/" />
	<resources mapping="/js/**" location="/resources/js/" />

	<!-- 
		컨트롤러가 리턴한 문자열 앞, 뒤에 적절한 경로를 붙여서
		화면을 응답할 수 있도록 도와주는 뷰 리졸버~
	 -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>
	
	<!-- 
		아노테이션으로 등록된 클래스 객체들을 탐색해 주는 설정 태그.
		base-package에다가는 탐색할 패키지 경로를 쓰시면
		하위 패키지까지 몽땅 검색해 줍니다.
	 -->
	<context:component-scan base-package="com.spring.myweb" />
	
	
	
</beans:beans>

기존에 servlet-contextservlet-config 로 바꾸고 위와 같은 내용으로 변경.

[web.xml] 수정

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

	<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/db-config.xml</param-value>
	</context-param>
	
	<!-- Creates the Spring Container shared by all Servlets and Filters -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<!-- Processes application requests -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-config.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
		
	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
	
	<!-- 스프링 한글처리 -->
   <filter>
      <filter-name>encodingFilter</filter-name>
      <filter-class>
         org.springframework.web.filter.CharacterEncodingFilter
      </filter-class>
      <init-param>
         <param-name>encoding</param-name>
         <param-value>UTF-8</param-value>
      </init-param>
      <init-param>
         <param-name>forceEncoding</param-name>
         <param-value>true</param-value>
      </init-param>
   </filter>
   <!-- 위에 지정한 encodingFilter이름을 모든 패턴에 적용 -->
   <filter-mapping>
      <filter-name>encodingFilter</filter-name>
      <url-pattern>/*</url-pattern>
   </filter-mapping>

</web-app>

db-config, servlet-config 로 각각 파일이름을 바꿨었으므로, 여기 태그에도 반영을 해 둔다.

그리고 필터 태그를 추가해 스프링에서 한글 처리를 할 수 있도록 해 준다.

[db-config.xml] 에 빈 등록

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
	xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd
		http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
	
	<!-- 
		프로젝트를 구성하다 보면 자주 변경되지 않는 설정 파일들이나 공통 정보들에 대한
		내용이 존재하게 되고, 그 내용들은 한 번 지정되면 잘 바뀌지 않습니다.
		이런 경우에는 .properties라는 파일을 사용하여 텍스트 형식으로 간단히 지정하고
		필요할 때 불러와서 사용하는 방식을 많이 사용합니다.
	 -->
	 
	 <!-- 외부에 따로 설정한 설정파일을 참조하는 곳에 사용하는 클래스 -->
	 <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
	 	<property name="location" value="classpath:/db-config/Hikari.properties" />
	 </bean>
	
	
	<!-- 히카리 커넥션 풀 빈 등록 -->
	<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
		<property name="driverClassName" value="${ds.driverClassName}" />
		<property name="jdbcUrl" value="${ds.url}" />
		<property name="username" value="${ds.username}" />
		<property name="password" value="${ds.password}" />
	</bean>
	
	<!-- 히카리 데이터소스 빈 등록 -->
	<bean id="ds" class="com.zaxxer.hikari.HikariDataSource">
		<constructor-arg ref="hikariConfig" />
	</bean>
		
	<!-- 마이바티스 SQL 동작을 위한 핵심 객체 SqlSessionFactory 클래스 빈 등록 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="ds" />
		
		<!-- 
			마이바티스 같은 경우는 mapper 구현체를 xml로 대체하니까
			해당 구현체 xml 파일의 경로를 알려줘야 한다.
			와일드카드 매핑을 이용해서 규칙을 설정.
		 -->
		<property name="mapperLocations" value="classpath:/mappers/*.xml" />
 	</bean>
	
	<!-- 지정한 패키지를 스캔하여 존재하는 mapper 인터페이스를 빈 타입으로 등록. 
		나중에 sqlSessionFactory가 xml파일을 클래스로 변환하여 빈으로 등록하려는 시도를 할 때
		타입을 지정해 줘야 하기 때문. 
	-->
	<mybatis-spring:scan base-package="com.spring.myweb.freeboard.mapper"/>
	<mybatis-spring:scan base-package="com.spring.myweb.reply.mapper"/>
	

		
</beans>

기존 프로젝트의 내용을 복붙해도 괜찮다.
그리고 중요한건 붙여넣기만 해서 끝나는게 아니라, Namespaces 탭을 눌러서, jdbc, mybatis 두 개의 항목에 체크를 해 주어야 한다.

[Hikari.properties] 작성

src/main/resources/db-config 하위에 Hikari.properties 작성

## local oracle
ds.driverClassName = oracle.jdbc.driver.OracleDriver
ds.url = jdbc:oracle:thin:@localhost:1521:xe
ds.username = spring
ds.password = spring

## local mysql
mydb.driverClassName = com.mysql.cj.jdbc.driver
mydb.url = jdbc:mysql://localhost:3306/spring
mydb.username = myspring
mydb.password = myspring

오라클 계정명이나 URL 등은 변경될 여지가 크지 않으니까 .properties 파일에 작성해둔다.

매퍼 디렉토리 만들고 작성

[FreeBoardMapper.xml] 작성

src/main/resourcesmappers 디렉토리를 만들고 그 하위에 작성.

<?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.spring.myweb.freeboard.mapper.IFreeBoardMapper">
	
	<sql id="search">
		<if test="condition == 'title'">
			WHERE title LIKE '%'||#{keyword}||'%'
		</if>
		<if test="condition == 'content'">
			WHERE content LIKE '%'||#{keyword}||'%'
		</if>
		<if test="condition == 'writer'">
			WHERE writer LIKE '%'||#{keyword}||'%'
		</if>
		<if test="condition == 'titleContent'">
			WHERE title LIKE '%'||#{keyword}||'%'
			OR content LIKE '%'||#{keyword}||'%' 
		</if>
	</sql>
	
	
	<insert id="regist">
		INSERT INTO freeboard(bno, title, writer, content)
		VALUES(freeboard_seq.NEXTVAL, #{title}, #{writer}, #{content})
	</insert>
	
	<select id="getList" resultType="com.spring.myweb.command.FreeBoardVO">
		SELECT * FROM
			(
			SELECT ROWNUM AS rn, tbl.* FROM
				(
				SELECT * FROM freeboard
				<include refid="search" />
				ORDER BY bno DESC
				) tbl
			)
		<![CDATA[
		WHERE rn > (#{pageNum}-1) * #{cpp}
		AND rn <= #{pageNum} * #{cpp}
		]]>
	</select>
	
	<select id="getTotal" resultType="int">
		SELECT COUNT(*)
		FROM freeboard
		<include refid="search" />
	</select>
	
	<select id="getContent" resultType="com.spring.myweb.command.FreeBoardVO">
		SELECT * FROM freeboard
		WHERE bno=#{bno}
	</select>
	
	<update id="update">
		UPDATE freeboard
		SET title=#{title}, content=#{content}, updatedate = sysdate
		WHERE bno=#{bno}
	</update>
	
	<delete id="delete">
		DELETE FROM freeboard
		WHERE bno = #{bno}
	</delete>
	
</mapper>

지금은 완성된 프로젝트를 받아버렸기 때문에 한번에 다 작성되어 있긴 하지만, 일단 비어있는 mapper 파일을 작성해 두는 것을 전제로 한다.

[FreeBoardMapperTest] 간단한 테스트를 위한 클래스 작성

package com.spring.myweb.freeboard;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.spring.myweb.command.FreeBoardVO;
import com.spring.myweb.freeboard.mapper.IFreeBoardMapper;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/config/db-config.xml")
public class FreeBoardMapperTest {
	
	@Autowired
	private IFreeBoardMapper mapper;
	
	@Test
	public void registTest() {
		for(int i=1; i<=30; i++) {
			FreeBoardVO vo = new FreeBoardVO();
			vo.setTitle("마이페이지 테스트" + i);
			vo.setWriter("kim1234");
			vo.setContent("테스트 글쓰기 내용입니다." + i);
			mapper.regist(vo);
		}
	}

}

src/test/java 하위의 com.spring.myweb.freeboard 패키지 밑에 생성한다.

Hikari 설정과 Mapper 를 테스트 해보기 위해서 클래스를 작성하고 테스트해본다.

컨트롤러 및 서비스 작성

[FreeBoardController] 작성

~.controller 패키지 하위에 생성

package com.spring.myweb.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.spring.myweb.command.FreeBoardVO;
import com.spring.myweb.freeboard.service.IFreeBoardService;
import com.spring.myweb.util.PageCreator;
import com.spring.myweb.util.PageVO;

@Controller
@RequestMapping("/freeboard")
public class FreeBoardController {
	
	@Autowired
	private IFreeBoardService service;
	
	//목록 화면
	@GetMapping("/freeList")
	public void freeList(PageVO vo, Model model) {
		
		System.out.println(vo);
		
		PageCreator pc = new PageCreator();
		pc.setPaging(vo);
		pc.setArticleTotalCount(service.getTotal(vo));
		
		System.out.println(pc);
		
		model.addAttribute("boardList", service.getList(vo));
		model.addAttribute("pc", pc);		
		
	}
	
	//글쓰기 화면 처리
	@GetMapping("/freeRegist")
	public void freeRegist() {}
	
	//글 등록 처리
	@PostMapping("/registForm")
	public String registForm(FreeBoardVO vo, RedirectAttributes ra) {
		service.regist(vo);
		ra.addFlashAttribute("msg", "정상 등록 처리되었습니다.");
		return "redirect:/freeboard/freeList";
	}
	
	//글 상세보기 처리
	@GetMapping("/freeDetail/{bno}")
	public String getContent(@PathVariable int bno, 
							@ModelAttribute("p") PageVO vo,
							Model model) {
		model.addAttribute("article", service.getContent(bno));
		
		return "freeboard/freeDetail";
	}
	
	//글 수정 페이지 이동 처리
	@GetMapping("/freeModify")
	public void modify(int bno, Model model) {
		model.addAttribute("article", service.getContent(bno));
	}
	
	//글 수정 처리
	@PostMapping("/freeUpdate")
	public String freeUpdate(FreeBoardVO vo, RedirectAttributes ra) {
		service.update(vo);
		ra.addFlashAttribute("msg", "updateSuccess");
		return "redirect:/freeboard/freeDetail/" + vo.getBno();
	}
	
	//글 삭제 처리
	@PostMapping("/freeDelete")
	public String freeDelete(int bno, RedirectAttributes ra) {
		service.delete(bno);
		
		ra.addFlashAttribute("msg", "게시글이 정상적으로 삭제되었습니다.");
		return "redirect:/freeboard/freeList";
	}
	
	

}

[IFreeBoardService] 작성

~.freeboard.service 패키지 하위에 작성

package com.spring.myweb.freeboard.service;

import java.util.List;

import com.spring.myweb.command.FreeBoardVO;
import com.spring.myweb.util.PageVO;

public interface IFreeBoardService {
	
	//글 등록
	void regist(FreeBoardVO vo);
	
	//글 목록
	List<FreeBoardVO> getList(PageVO vo);
	
	//총 게시물 수
	int getTotal(PageVO vo);
	
	//상세보기
	FreeBoardVO getContent(int bno);
	
	//수정
	void update(FreeBoardVO vo);
	
	//삭제
	void delete(int bno);


}

[FreeBoardService] 작성

~.freeboard.service 패키지 하위에 작성

package com.spring.myweb.freeboard.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.spring.myweb.command.FreeBoardVO;
import com.spring.myweb.freeboard.mapper.IFreeBoardMapper;
import com.spring.myweb.util.PageVO;

@Service
public class FreeBoardService implements IFreeBoardService {

	@Autowired
	private IFreeBoardMapper mapper;
	
	@Override
	public void regist(FreeBoardVO vo) {
		mapper.regist(vo);
	}

	@Override
	public List<FreeBoardVO> getList(PageVO vo) {
		
		List<FreeBoardVO> list = mapper.getList(vo);
		
		for(FreeBoardVO article : list) {
			long now = System.currentTimeMillis();
			long regTime = article.getRegDate().getTime();
			
			if(now - regTime < 60 * 60 * 24 * 2 * 1000) {
				article.setNewMark(true);
			}
		}
		
		return list;
	}

	@Override
	public int getTotal(PageVO vo) {
		return mapper.getTotal(vo);
	}

	@Override
	public FreeBoardVO getContent(int bno) {
		return mapper.getContent(bno);
	}

	@Override
	public void update(FreeBoardVO vo) {
		mapper.update(vo);
	}

	@Override
	public void delete(int bno) {
		mapper.delete(bno);
	}

}

[freeList.jsp] 페이징 처리해주기

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>  
    
    <%@ include file="../include/header.jsp" %>
    
    
    <section>
        <div class="container-fluid">
            <div class="row">
                <!--lg에서 9그리드, xs에서 전체그리드-->   
                <div class="col-lg-9 col-xs-12 board-table">
                    <div class="titlebox">
                        <p>자유게시판</p>
                    </div>
                    <hr>
                    
                    <!--form select를 가져온다 -->
            <form action="<c:url value='/freeboard/freeList' />">
		    		<div class="search-wrap">
                       <button type="submit" class="btn btn-info search-btn">검색</button>
                       <input type="text" name="keyword" class="form-control search-input" value="${pc.paging.keyword}">
                       <select class="form-control search-select" name="condition">
                            <option value="title" ${pc.paging.condition == 'title' ? 'selected' : ''}>제목</option>
                            <option value="content" ${pc.paging.condition == 'content' ? 'selected' : ''}>내용</option>
                            <option value="writer" ${pc.paging.condition == 'writer' ? 'selected' : ''}>작성자</option>
                            <option value="titleContent" ${pc.paging.condition == 'titleContent' ? 'selected' : ''}>제목+내용</option>
                       </select>
                    </div>
		    </form>
                   
                    <table class="table table-bordered">
                        <thead>
                            <tr>
                                <th>번호</th>
                                <th class="board-title">제목</th>
                                <th>작성자</th>
                                <th>등록일</th>
                                <th>수정일</th>
                            </tr>
                        </thead>
                        <tbody>
                        	<c:forEach var="vo" items="${boardList}">
	                            <tr>
	                                <td>${vo.bno}</td>
	                                <td>
	                                	<a href="<c:url value='/freeboard/freeDetail/${vo.bno}${pc.makeURI(pc.paging.pageNum)}' />">${vo.title}</a>
	                                &nbsp;&nbsp;&nbsp;
	                                <c:if test="${vo.newMark}">
	                                	<img alt="newMark" src="<c:url value='/img/icon_new.gif' />">
	                                </c:if>
	                                </td>
	                                <td>${vo.writer}</td>
	                                <td><fmt:formatDate value="${vo.regDate}" pattern="yyyy-MM-dd HH:mm" /></td>
	                                <td><fmt:formatDate value="${vo.updateDate}" pattern="yyyy-MM-dd HH:mm"/></td>
	                            </tr>
                            </c:forEach>
                        </tbody>
                        
                    </table>


                    <!--페이지 네이션을 가져옴-->
		    <form action="<c:url value='/freeboard/freeList' />" name="pageForm">
                    <div class="text-center">
                    <hr>
                    <ul id="pagination" class="pagination pagination-sm">
                    	<c:if test="${pc.prev}">
                        	<li><a href="#" data-pagenum="${pc.beginPage-1}">이전</a></li>
                        </c:if>
                        
                        <c:forEach var="num" begin="${pc.beginPage}" end="${pc.endPage}">
                        	<li class="${pc.paging.pageNum == num ? 'active' : ''}"><a href="#" data-pagenum="${num}">${num}</a></li>
                        </c:forEach>
                        
                        <c:if test="${pc.next}">
                        	<li><a href="#" data-pagenum="${pc.endPage+1}">다음</a></li>
                        </c:if>
                    </ul>
                    <button type="button" class="btn btn-info" onclick="location.href='<c:url value="/freeboard/freeRegist" />'">글쓰기</button>
                    </div>
                    
                    <!-- 페이지 관련 버튼(이전, 다음, 페이지번호)을 클릭 시 같이 숨겨서 보내줄 공통 값  -->
                    <input type="hidden" name="pageNum" value="${pc.paging.pageNum}">
                    <input type="hidden" name="cpp" value="${pc.paging.cpp}">
                    <input type="hidden" name="condition" value="${pc.paging.condition}">
                    <input type="hidden" name="keyword" value="${pc.paging.keyword}">
                    
		    </form>

                </div>
            </div>
        </div>
	</section>
	
	<%@ include file="../include/footer.jsp" %>
	
	<script>
		$(function() {
			
			const msg = '${msg}';
			if(msg !== '') {
				alert(msg);
			}
			
			
			//사용자가 페이지 관련 버튼을 클릭했을 때, 기존에는 각각의 a태그의 href에다가
			//각각 다른 url을 작성해서 요청을 보내줬다면, 이번에는 클릭한 그 버튼이 무엇인지를 확인해서
			//그 버튼에 맞는 페이지 정보를 자바스크립트로 끌고와서 요청을 보내 주겠습니다.
			$('#pagination').on('click', 'a', function(e) {
				e.preventDefault(); //a태그의 고유기능 중지.
				
				//현재 이벤트가 발생한 요소(버튼)의
				//data-pageNum의 값을 얻어서 변수에 저장.
				//const value = e.target.dataset.pageNum; -> Vanilla JS 
				const value = $(this).data('pagenum'); // -> jQuery
				console.log(value);
				
				//페이지 버튼들을 감싸고 있는 form태그를 name으로 지목하여
				//그 안에 숨겨져 있는 pageNum이라는 input태그의 value에
				//위에서 얻은 data-pageNum의 값을 삽입 한 후 submit
				document.pageForm.pageNum.value = value;
				document.pageForm.submit();
				
			});
			
			
		}); //end jQuery
	</script>

오늘 강의는 이것으로 종료되었다.

페이징 처리나 컨트롤러, 서비스 등은 사실 거의 다 빈 상태로 생성되었고, 채워진 건 다음 강의부터인 것 같다.

0개의 댓글