티스토리 뷰

초보 웹 개발자를 위한 스프링 5 프로그램 입문을 보고 복습 겸 개인 학습 정리입니다. (windows 기준)

 

 

날짜를 이용한 회원 검색 기능과 회원 리스트를 볼 수 있는 뷰 구현

회원 가입 일자를 기준으로 검색하는 기능과 @PathVariable를 이용한 회원 조회 구현을 하면서 발생하는 익셉션처리까지 chap 14에서 다뤄본다. 

 

 

(Django에서 구현했던 패스 파라미터, 쿼리파라미터 구조랑 비슷한듯 복잡(?)한 것 같다.)

 

 

 

예제 프로젝트 만들기 (메이븐 기준)  예제 코드

sp5-chap14 프로젝트 폴더 생성

+ chap13의 src 파일 복붙

chap13의 pom.xml 복붙 후 <artifactId> 14로 변경

이클립스에서 sp5-chap14 폴더에 생성한 메이븐 프로젝트 import

 

 

 

1. MemberDao 클래스에 selectByRegdate() 메서드 추가

selectByRegdate() 메서드는 REGDATE 값이 두 파라미터로 전달받은 from과 to 사이에 있는 Member 목록을 구한다.

이 메서드로 특정 기간 동안에 가입한 회원 목록을 보여주는 기능을 구현할 것

 

sp5-chp14/src/main/java/spring/MemberDao

package spring;
... import 생략...

public class MemberDao {

	private JdbcTemplate jdbcTemplate;

	public MemberDao(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}
    .. 생략..
	// 회원 가입 일자를 기준으로 멤버 검색하는 기능 추가 selectByRegdate() 메서드 추가
	public List<Member> selectByRegdate(LocalDateTime from, LocalDateTime to) { 
		List<Member> results = jdbcTemplate.query(
			"select * from MEMBER where REGDATE between ? and ?" + "order by REGDATE desc", 
			new RowMapper<Member>() {
				@Override
				public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
					Member member = new Member (
						rs.getString("EMAIL"),
						rs.getString("PASSWORD"),
						rs.getString("NAME"),
						rs.getTimestamp("REGDATE").toLocalDateTime());
					member.setId(rs.getLong("ID"));
					return member;
				}
			}, 
			from, to);
	return results;
	}

}

 

 

2. 커맨드 객체 Date 타입 프로퍼티 변환 처리

회원 가입 일시를 기준으로 검색하기 위해 시작 시간과 끝 시간 기준을 파라미터로 전달받는다.

검색 기준 기간을 표현하기 위해 커맨드 클래스에 구현한다.

 

controller 패키지에 ListCommand 클래스를 추가한다.

LocalDateTime 타입은 변환 추가 설정이 필요하기 때문에

@DateTimeFormat 애노테이션을 적용되어있으면 @DateTimeFormat에서 지정한 형식을 이용해 LocalDateTime 타입으로 변환한다. 

 

아래 코드의 경우 "2023022715"의 문자열을 "2023년 2월 27일 15시" 값을 갖는 LocalDateTime 객체로 변환해 준다.

 

sp5-chp14/src/main/java/controller/ListCommand

package controller;

import java.time.LocalDateTime;

public class ListCommand {
	
	@DateTimeFormat(pattern="yyyyMMddHH")
	private LocalDateTime from;
	@DateTimeFormat(pattern="yyyyMMddHH")
	private LocalDateTime to;
	
	public LocalDateTime getFrom() {
		return from;
	}
	public void setFrom(LocalDateTime from) {
		this.from = from;
	}
	public LocalDateTime getTo() {
		return to;
	}
	public void setTo(LocalDateTime to) {
		this.to = to;
	}

}

 

 

3. 새로운 멤버 리스트 컨트롤러를 작성한다

ListCommad에서 받아온 from과 to가 null 값인 경우 익셉션을 발생시킨다.

 

sp5-chp14/src/main/java/controller/MemberListController.java

package controller;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

import spring.Member;
import spring.MemberDao;

@Controller
public class MemberListController {

	private MemberDao memberDao;

	public void setMemberDao(MemberDao memberDao) {
		this.memberDao = memberDao;
	}

	@RequestMapping("/members")
	public String list(
			@ModelAttribute("cmd") ListCommand listCommand,
			Errors errors, Model model) {
		if (errors.hasErrors()) {
			return "member/memberList";
		}
		if (listCommand.getFrom() != null && listCommand.getTo() != null) {
			List<Member> members = memberDao.selectByRegdate(
					listCommand.getFrom(), listCommand.getTo());
			model.addAttribute("members", members);
		}
		return "member/memberList";
	}

}

 

 

4. ControllerConfig 설정 클래스에 MemberListController 빈 등록하기 

ControllerConfig 설정 클래스에 3번에서 만든 MemberListController 클래스를 빈으로 등록한다.

 

→ sp5-chap14/src/main/java/config/ControllerConfig.java

package config;
... import 생략...

@Configuration
public class ControllerConfig {

    @Autowired
    private MemberRegisterService memberRegSvc;
    @Autowired
    private AuthService authService;
    @Autowired
    private ChangePasswordService changePasswordService;
    // MemberDao 추가
    @Autowired
    private MemberDao memberDao;
	... 생략..
	// MemberListController 빈 등록하기
    @Bean
    public MemberListController memberListController() {
    	MemberListController controller = new MemberListController();
    	controller.setMemberDao(memberDao);
    	return controller;
    }
}

 

5. LocalDateTime 값을 원하는 형식으로 출력해 주는 커스텀 태그 파일 작성  (접은 글로 대체)

JSTL이 제공하는 날짜 형식 태그는 자바 8 LocalDateTime  타입은 지원하지 않아, 다음과 같은 태그 파일을 이용해서 LocalDateTime 값을 지정한 형식으로 출력한다. 

태그 파일은 jsp 파일에 <%@ taglib prefix="tf" tagdir="/WEB-INF/tags" %>로 넣어 사용한다.

 

→ sp5-chap14/src/main/WEB-INF/tags/formatDateTime.java

더보기
<%@ tag body-content="empty" pageEncoding="utf-8" %>
<%@ tag import="java.time.format.DateTimeFormatter" %>
<%@ tag trimDirectiveWhitespaces="true" %>
<%@ attribute name="value" required="true" 
              type="java.time.temporal.TemporalAccessor" %>
<%@ attribute name="pattern" type="java.lang.String" %>
<%
	if (pattern == null) pattern = "yyyy-MM-dd";
%>
<%= DateTimeFormatter.ofPattern(pattern).format(value) %>

 

6. Member 목록을 출력하도록 JSP 파일을 작성한다 (접은 글로 대체)

MemberListController 클래스의 list() 메서드는 커맨드 객체로 받은 ListCommand의 from과 to 프로퍼티를 이용해 검색한 기간의 Member 목록을 구하고, 뷰에 "members" 속성으로 전달하기 때문에 뷰 코드는 이에 맞게 ListCommand 객체를 위한 폼을 제공하고 member 속성을 이용해서 회원 목록을 출력하도록 구현한다. 

 

→ sp5-chap14/src/main/WEB-INF/view/member/memberList.jsp

더보기
<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="tf" tagdir="/WEB-INF/tags" %>
<!DOCTYPE html>
<html>
<head>
    <title>회원 조회</title>
</head>
<body>
    <form:form modelAttribute="cmd">
    <p>
        <label>from: <form:input path="from" /></label>
        <label>to:<form:input path="to" /></label>
        <input type="submit" value="조회">
    </p>
    </form:form>
    
    <c:if test="${! empty members}">
    <table>
        <tr>
            <th>아이디</th><th>이메일</th>
            <th>이름</th><th>가입일</th>
        </tr>
        <c:forEach var="mem" items="${members}">
        <tr>
            <td>${mem.id}</td>
            <td><a href="<c:url value="/members/${mem.id}"/>">
                ${mem.email}</a></td>
            <td>${mem.name}</td>
            <td><tf:formatDateTime value="${mem.registerDateTime }" 
                                   pattern="yyyy-MM-dd" /></td>
        </tr>
        </c:forEach>
    </table>
    </c:if>
</body>
</html>

 

 

7. 서버 실행 후 "localhost:8090/sp5-chap14/members " 접속 후 기간 설정 후 멤버 조회하기

[Run As] → [Run on Server] → [톰캣😺 서버 실행] → [브라우저에 "localhost:8090/sp5-chap14/members" 접속] 

 

 

지정한 형식("yyyyMMddHH" )을 이용한 기간 입력 후 멤버 조회 시 

 

지정한 형식("yyyyMMddHH")이 아닌 입력 시 오류 화면

 

8. 오류 화면 (400 Error) 대신 알맞은 에러 메세지를 보여주기 위해 MemberListController 클래스에 Errors 타입 파라미터를추가한다.

 

sp5-chp14/src/main/java/controller/MemberListController.java

 

package controller;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

import spring.Member;
import spring.MemberDao;

@Controller
public class MemberListController {

	private MemberDao memberDao;

	public void setMemberDao(MemberDao memberDao) {
		this.memberDao = memberDao;
	}

	@RequestMapping("/members")
	public String list(
			@ModelAttribute("cmd") ListCommand listCommand,
            // Errors errors 파라미터 추가
			Errors errors, Model model) {
		if (errors.hasErrors()) {
			return "member/memberList";
		}
		if (listCommand.getFrom() != null && listCommand.getTo() != null) {
			List<Member> members = memberDao.selectByRegdate(
					listCommand.getFrom(), listCommand.getTo());
			model.addAttribute("members", members);
		}
		return "member/memberList";
	}

}

 

 

9. 에러코드 대신 사용할 메시지 label.properties 파일에 추가  (접은 글로 대체)

 

→ sp5-chap14/src/main/resources/message/label.properties

더보기
change.pwd.title=비밀번호 변경
currentPassword=현재 비밀번호
newPassword=새 비밀번호
change.btn=변경하기
notMatching.currentPassword=비밀번호를 잘못 입력했습니다.
change.pwd.done=비밀번호를 변경했습니다.

typeMismatch.java.time.LocalDateTime=잘못된 형식 입니다.

 

 

10. memberList.jsp에 에러 메세지 출력 코드 추가  (접은 글로 대체)

 

→ sp5-chap14/src/main/WEB-INF/view/member/memberList.jsp

더보기
<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="tf" tagdir="/WEB-INF/tags" %>
<!DOCTYPE html>
<html>
<head>
    <title>회원 조회</title>
</head>
<body>
    <form:form modelAttribute="cmd">
    <p>
        <label>from: <form:input path="from" /></label>
        <form:errors path="from" /> 
        ~
        <label>to:<form:input path="to" /></label>
        <form:errors path="to" />
        <input type="submit" value="조회">
    </p>
    </form:form>
    
    <c:if test="${! empty members}">
    <table>
        <tr>
            <th>아이디</th><th>이메일</th>
            <th>이름</th><th>가입일</th>
        </tr>
        <c:forEach var="mem" items="${members}">
        <tr>
            <td>${mem.id}</td>
            <td><a href="<c:url value="/members/${mem.id}"/>">
                ${mem.email}</a></td>
            <td>${mem.name}</td>
            <td><tf:formatDateTime value="${mem.registerDateTime }" 
                                   pattern="yyyy-MM-dd" /></td>
        </tr>
        </c:forEach>
    </table>
    </c:if>
</body>
</html>

 

 

11. Errors 파라미터가 적용되었는지 확인하기

[Run As] → [Run on Server] → [톰캣😺 서버 실행] → [브라우저에 "localhost:8090/sp5-chap14/members" 접속] 

 

기존 포멧 오류 화면

Errors 파라미터 추가 이후 오류 화면

 

📌 WebDataBinder

스프링 MVC는 요청 매핑 애노테이션 적용 메서드와 DispatcherSerblet 사이를 연결하기 위해 RequestMappingHandlerAdapter 객체를 사용한다. 이 핸들러 어댑터 객체는 요청 파라미터와 커맨드 객체 사이의 변환처리를 위한 WebDataBinder를 이용한다.

 

WebDataBinder는 커맨드 객체럴 생성하고, 커맨드 객체의 프로퍼티와 같은 이름을 같는 요청 파라미터를 이용해 프로퍼티 값을 생성한다.

 

 

 

12. MemberDao 클래스 중복 코드 정리

각 메서드에 RowMapper 객체를 생성하는 부분의 코드가 중복되어있기 때문에 RowMapper를 매서드로 정의한 후  각 메서드에 쿼리로 넣어 준다.

 

sp5-chp14/src/main/java/spring/MemberDao

package spring;
...import 생략...
public class MemberDao {

	private JdbcTemplate jdbcTemplate;
	// RowMapper 메서드 따로 처리
	private RowMapper<Member> memRowMapper =
			new RowMapper<Member>() {
		@Override
		public Member mapRow(ResultSet rs, int rowNum)throws SQLException{
			Member member = new Member(rs.getString("EMAIL"),
					rs.getString("PASSWORD"),
					rs.getString("NAME"),
					rs.getTimestamp("REGDATE").toLocalDateTime());
				member.setId(rs.getLong("ID"));
				return member;
					
		}
	};
	
	public MemberDao(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}
	
	public Member selectById(Long memId) { // query()메서드를 이용해 쿼리 실행 
		List<Member> results = jdbcTemplate.query( // memRowMapper 추가
			"select*from MEMBER where ID = ?", memRowMapper, memId);
		return results.isEmpty() ? null : results.get(0);
	}

	public Member selectByEmail(String email) { 
		List<Member> results = jdbcTemplate.query(
			"select*from MEMBER where EMAIL = ?", memRowMapper);
		return results.isEmpty() ? null : results.get(0); 
	}
	public void insert(final Member member) {
		KeyHolder keyHolder = new GeneratedKeyHolder();
		jdbcTemplate.update(new PreparedStatementCreator() {
			@Override
			public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
				// 파라미터로 전달받은 Connection을 이용해서 PreparedStatement 생성
				PreparedStatement pstmt = con.prepareStatement(
					"insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE)" + "values (?, ?, ?, ?)", new String[] {"ID"});
					// 인덱스 파라미터 값 설정
					pstmt.setString(1,member.getEmail());
					pstmt.setString(2,member.getPassword());
					pstmt.setString(3,member.getName());
					pstmt.setTimestamp(4, Timestamp.valueOf(member.getRegisterDateTime()));
					// 생성한 PreparedStatement 객체 리턴
					return pstmt;
			}
		}, keyHolder);
		Number keyValue = keyHolder.getKey();
		member.setId(keyValue.longValue());	// longValue() 메서드로 키를 long 타입으로 변환
	}

	public void update(Member member) {
		jdbcTemplate.update("update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?", 
							member.getName(), member.getPassword(), member.getEmail());
	}

	public List<Member> selectAll() {
		List<Member> results = jdbcTemplate.query("select * from MEMBER", memRowMapper);
		return results;
	}

	public int count() {
		Integer count = jdbcTemplate.queryForObject(
				"select count(*) from MEMBER", Integer.class);
		return count;
	}
	
	public List<Member> selectByRegdate(LocalDateTime from, LocalDateTime to) { 
		List<Member> results = jdbcTemplate.query(
			"select * from MEMBER where REGDATE between ? and ?" + "order by REGDATE desc", memRowMapper);
	return results;
	}
}

 

 

13. @PathVariable을 이용한 경로 변수 처리

위 이미지와 같이 경로의 일부가 고정되어있지 않고 달라질 때 사용할 수 있는 것이 @PathVariable 애노테이션이다.

이 애노테이션을 사용하면 '{경로변수}'에 해당하는 경로값으로 이동할 수 있다.

 

아래 코드에서는 @GetMapping("/members/{id}")  {id}에 해당하는 부분의 경로값을 @PathVariable("id") 애노테이션이 적용된 memId 파라미터에 전달한다. 만약 members/10 이면 {id}에 해당하는 '10'이 memId 파라미터에 값으로 전달된다. memId 파라미터 타입은 Long 인데 이 경우 String 타입 값"0"을 알맞게 Long 타입으로 변환한다.

 

 sp5-chp14/src/main/java/controller/MemberDetailController.java

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import spring.Member;
import spring.MemberDao;
import spring.MemberNotFoundException;

@Controller
public class MemberDetailController {
	
	private MemberDao memberDao;
	
	public void setMemberDao(MemberDao memberDao) {
		this.memberDao=memberDao;
	}

	@GetMapping("/members/{id}") 
	public String detail(@PathVariable("id") Long memId, Model model) {
		Member member =memberDao.selectById(memId);
		if (member == null) {
			throw new MemberNotFoundException();
			
		}
		model.addAttribute("member", member);
		return "member/memberDetail";
	}
	
}

 

 

13. ControllerConfig 설정 클래스에 MemberDetailController 빈 등록하기 

MemberDetailController 클래스를 빈으로 등록한 뒤 JSP 코드를 작성한다

 

→ sp5-chap14/src/main/java/config/ControllerConfig.java

package config;
..import 생략..
@Configuration
public class ControllerConfig {

    @Autowired
    private MemberRegisterService memberRegSvc;
    @Autowired
    private AuthService authService;
    @Autowired
    private ChangePasswordService changePasswordService;
    @Autowired
    private MemberDao memberDao;
.. 생략..
    // MemberDetailController 추가
    @Bean
    public MemberDetailController memberDetailController() {
    	MemberDetailController controller = new MemberDetailController();
    	controller.setMemberDao(memberDao);
    	return controller;
    }
}

 

 

14. Member 목록을 출력하도록 JSP 파일을 작성한다 (접은 글로 대체)

 

→ sp5-chap14/src/main/WEB-INF/view/member/memberDetail.jsp

더보기
<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="tf" tagdir="/WEB-INF/tags" %>
<!DOCTYPE html>
<html>
<head>
    <title>회원 정보</title>
</head>
<body>
    <p>아이디: ${member.id}</p>
    <p>이메일: ${member.email}</p>
    <p>이름: ${member.name}</p>
    <p>가입일: <tf:formatDateTime value="${member.registerDateTime}" 
                                  pattern="yyyy-MM-dd HH:mm" /> </p>
</body>
</html>

 

15. @PathVariable을 이용한 경로 맵핑이 적용되었는지 확인하기

[Run As] → [Run on Server] → [톰캣😺 서버 실행] → [브라우저에 "localhost:8090/sp5-chap14/members/1" 접속] 

 

 

DB에 없는 memberId 값일 경우

 

유효하지 않은 타입 값일 경우

 

 

 

16. @ExceptionHandler 애노테이션을 적용하여 컨트롤러에서 발생한 익셉션 직접 처리

오류에 따른 알맞은 익셉션 안내를 위해 @ExceptionHandler 애노테이션을 사용하면 직접 익셉션을 처리할 수 있다.

 

 sp5-chp14/src/main/java/controller/MemberDetailController.java

package controller;

import org.springframework.beans.TypeMismatchException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import spring.Member;
import spring.MemberDao;
import spring.MemberNotFoundException;

@Controller
public class MemberDetailController {

	private MemberDao memberDao;

	public void setMemberDao(MemberDao memberDao) {
		this.memberDao = memberDao;
	}

	@GetMapping("/members/{id}")
	public String detail(@PathVariable("id") Long memId, Model model) {
		Member member = memberDao.selectById(memId);
		if (member == null) {
			throw new MemberNotFoundException();
		}
		model.addAttribute("member", member);
		return "member/memberDetail";
	}
	// @ExceptionHandler를 적용하여 익센셥 처리 
	@ExceptionHandler(TypeMismatchException.class)
	public String handleTypeMismatchException() {
		return "member/invalidId";
	}

	@ExceptionHandler(MemberNotFoundException.class)
	public String handleNotFoundException() {
		return "member/noMember";
	}

	
}

 

 

17. @ExceptionHandler 애노테이션을 적용한 메서드의 return 뷰를 작성한다 (접은 글로 대체)

 

 

→ sp5-chap14/src/main/WEB-INF/view/member/invalidld.jsp

더보기
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<title>에러</title>
<body>
 잘못된 요청입니다.
</body>
</html>

 

 

18. @ExceptionHandler 애노테이션을 적용한 메서드의 return 뷰를 작성한다. (접은 글로 대체)

 

→ sp5-chap14/src/main/WEB-INF/view/member/noMember.jsp

더보기
<%@ page contentType="text/html; charset=utf-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>에러</title>
</head>
<body>
    존재하지 않는 회원입니다.
</body>
</html>

 

 

 

19. @ExceptionHandler 익셉션 확인

[Run As] → [Run on Server] → [톰캣😺 서버 실행] → [브라우저에 "localhost:8090/sp5-chap14/members/5" 접속] 

 

DB에 없는 memberId 값일 경우 " 존재하지 않는 회원입니다." 안내 

 

 

[Run As] → [Run on Server] → [톰캣😺 서버 실행] → [브라우저에 "localhost:8090/sp5-chap14/members/a" 접속] 

 

유효하지 않은 타입 값일 경우 "잘못된 요청입니다" 안내 

 

 

 

'study > Spring' 카테고리의 다른 글

웹 페이지 만들기 - 2 (복습)  (0) 2023.04.07
웹 페이지 만들기 - 1 (복습)  (0) 2023.03.28
chap 13 - Cookie  (0) 2023.02.23
chap 13 - HandlerInterceptor  (0) 2023.02.23
chap 13 - HttpSession  (0) 2023.02.21
댓글