
라이브러리 추가
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.18.Final</version>
</dependency>
package kr.spring.ch09.controller;
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import kr.spring.ch09.vo.MemberVO;
@Controller
public class MemberWriteController {
// 유효성 체크를 위한 자바빈(VO) 초기화
@ModelAttribute("command")
public MemberVO initCommand() {
return new MemberVO();
}
// 폼 호출하기
@GetMapping("/member/write.do")
public String form() {
return "member/write";
}
@PostMapping("/member/write.do")
// valid 어노테이션을 작성하지 않으면 bindingresult의 값이 포함이 되지 않기 때문에 꼭 작성해야 한다
public String submit(@ModelAttribute("command") @Valid MemberVO vo, BindingResult result) {
System.out.println("전송된 데이터 : " + vo);
if(result.hasErrors()) {
return "member/write";
}
return "redirect:/index.jsp";
}
}
package kr.spring.ch09.vo;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Range;
public class MemberVO {
// 정규표현식으로 패턴 검사
// 0~9, a~z, A~Z, + : 최소 한 번 이상 반복하라는 의미
@Pattern(regexp="^[0-9a-zA-Z]+$")
private String id;
// 문자열의 길이를 지정한다
// 최소 4자리, 최대 10자리까지
@Size(min = 4, max = 10)
private String password;
// 필수 입력하도록 지정하기
@NotEmpty
private String name;
// 이메일은 필수 입력, 이메일 형식에 맞게 작성하기
@Email
@NotEmpty
private String email;
// pom.xml에 라이브러리 추가한 이유는 range 어노테이션을 사용하기 위해서
// 숫자 데이터 체크하기 - 최소 1, 최대 200
@Range(min = 1 , max = 200)
private int age;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "MemberVO [id=" + id + ", password=" + password + ", name=" + name + ", email=" + email + ", age=" + age
+ "]";
}
}
<!-- 어노테이션을 이용한 유효성 체크 -->
<beans:bean class="kr.spring.ch09.controller.MemberWriteController"/>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<form:form action="write.do" modelAttribute="command">
아이디: <form:input path="id"/>
<form:errors path="id" cssStyle="color:red;"/>
<br>
비밀번호: <form:password path="password"/>
<form:errors path="password" cssStyle="color:red;"/>
<br>
이름: <form:input path="name"/>
<form:errors path="name" cssStyle="color:red;"/>
<br>
이메일: <form:input path="email"/>
<form:errors path="email" cssStyle="color:red;"/>
<br>
나이: <form:input path="age"/>
<form:errors path="age" cssStyle="color:red;"/>
<br>
<form:button>회원 가입</form:button>
</form:form>
</body>
</html>
# 어노테이션을 이용한 유효성 체크시 에러 코드 - 에러 문구 지정
# 두가지 방법으로 가능함
#1) 어노테이션명 . 커맨드 객체 . 필드
#2) 어노테이션명 . 필드
Pattern.id = 영문자와 숫자만 입력 가능합니다.
Size.password = 비밀번호는 최소 4자 ~ 최대 10자까지 입력 가능합니다.
NotEmpty.name = 이름은 필수 입력입니다.
Email.email = 이메일 형식에 맞게 입력 바랍니다.
NotEmpty.email = 이메일은 필수 입력입니다.
Range.age = 나이는 최소 1 ~ 최대 200까지 입력 가능합니다.
typeMismatch.age = 숫자만 입력 가능합니다(1~200)
Age를 integer형으로 입력 받았을 때
Integer는 필수 체크가 불가능
필수체크가 가능하도록 InitBinder 사용
package kr.spring.ch09.vo;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Range;
public class MemberVO {
// 정규표현식으로 패턴 검사
// 0~9, a~z, A~Z, + : 최소 한 번 이상 반복하라는 의미
@Pattern(regexp="^[0-9a-zA-Z]+$")
private String id;
// 문자열의 길이를 지정한다
// 최소 4자리, 최대 10자리까지
@Size(min = 4, max = 10)
private String password;
// 필수 입력하도록 지정하기
@NotEmpty
private String name;
// 이메일은 필수 입력, 이메일 형식에 맞게 작성하기
@Email
@NotEmpty
private String email;
// pom.xml에 라이브러리 추가한 이유는 range 어노테이션을 사용하기 위해서
// 숫자 데이터 체크하기 - 최소 1, 최대 200
@Range(min = 1 , max = 200)
private Integer age;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "MemberVO [id=" + id + ", password=" + password + ", name=" + name + ", email=" + email + ", age=" + age
+ "]";
}
}
package kr.spring.ch09.controller;
import javax.validation.Valid;
import org.springframework.beans.propertyeditors.CustomNumberEditor;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import kr.spring.ch09.vo.MemberVO;
@Controller
public class MemberWriteController {
// 유효성 체크를 위한 자바빈(VO) 초기화
@ModelAttribute("command")
public MemberVO initCommand() {
return new MemberVO();
}
// 폼 호출하기
@GetMapping("/member/write.do")
public String form() {
return "member/write";
}
@PostMapping("/member/write.do")
// valid 어노테이션을 작성하지 않으면 bindingresult의 값이 포함이 되지 않기 때문에 꼭 작성해야 한다
public String submit(@ModelAttribute("command") @Valid MemberVO vo, BindingResult result) {
System.out.println("전송된 데이터 : " + vo);
if(result.hasErrors()) {
return "member/write";
}
return "redirect:/index.jsp";
}
@InitBinder
public void initBinder(WebDataBinder binder) {
// true - 요청 파라미터의 값이 null이거나 빈 문자열("")일 때
// 변환 처리를 하지 않고 null 값으로 할당한다.
// false - 변환 과정에서 에러가 발생하고 에러 코드로 "typeMismatch"가 추가된다.
binder.registerCustomEditor(Integer.class, new CustomNumberEditor(Integer.class, false));
}
}
서버에서 스트림을 만들어서 -> 클라이언트에 보내야 함
클래스에서 스트림을 만들어서 전송하는 작업을 해줘야함
뷰 작업은 뷰 리솔버 때문에 jsp를 사용해야 뷰라고 인식하기 때문에 설정을 바꿔줘야 한다
package kr.spring.ch10.controller;
import java.io.File;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class DownloadController {
@RequestMapping("/file.do")
// session을 통해서 절대 경로를 받을 수 있음
public ModelAndView download(HttpSession session) {
// file.txt의 컨텍스 경로상의 절대 경로를 구하기
String path = session.getServletContext().getRealPath("/WEB-INF/file.txt");
// 경로를 구한 후 파일 객체를 생성
File downloadFile = new File(path);
// 뷰 이름 속성명 속성값
return new ModelAndView("download","downloadFile",downloadFile);
// 새로운 뷰 리솔버 등록
}
}
먼지댜
viewResolver의 기존 설정에 order로 설정 1순위로 변경을 해줘서 0순위인 viewResolver가 먼저 실행될 수 있도록 해준다.
파일 다운로드를 위한 viewResolver는 Bean의 이름과 View의 이름이 동일하다면 view가 생성될 수 있도록 설정을 해준다.
<!--============================== viewResolver 설정============================= -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
<beans:property name="order" value="1" />
</beans:bean>
<!-- 파일 다운로드를 위한 viewResolver 설정 0순위로 설정(늘 먼저 동작) -->
<!-- 빈의 이름과 뷰의 이름이 동일하다면 클래스더라도 해당 클래스를 호출 -->
<beans:bean id="viewResolver" p:order = "0"
class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
<!-- 파일 다운로드 -->
<beans:bean class="kr.spring.ch10.controller.DownloadController"/>
<beans:bean id="download" class="kr.spring.ch10.view.DownloadView"/>
package kr.spring.ch10.view;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.servlet.view.AbstractView;
public class DownloadView extends AbstractView{
public DownloadView() {
setContentType("application/download;charset=utf-8");
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
File file = (File)model.get("downloadFile");
response.setContentType(getContentType());
response.setContentLength((int) file.length());
String fileName = new String(file.getName().getBytes("utf-8"),"iso-8859-1");
response.setHeader("Content-Disposition", "attachment;filename=\"" +fileName+ "\";");
response.setHeader("Content-Transfer-Encoding", "binary");
OutputStream out = response.getOutputStream();
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
FileCopyUtils.copy(fis, out);
} finally {
if(fis != null) try {fis.close();} catch (IOException e) {}
}
out.flush();
}
}
실제로 뷰가 아니기 때문에 Abstract View를 상속받아서 작성을 해야한다.
Content 타입
public DownloadView() {
setContentType("application/download;charset=utf-8");
}
다운로드 하는 파일의 경로 정보가 저장된 File 객체를 반환해줘야 하는데 model에서 반환이 된다. 속성명을 통해서 속성값을 불러올 수 있게 해준다.
File file = (File)model.get("downloadFile");
위에서 지정했던 Content Type를 불러와서 지정해준다
response.setContentType(getContentType());
해당하는 content 의 용량을 지정하는데 파일의 길이만큼 지정을 해준다.
response.setContentLength((int) file.length());
스트림은 목적지로부터 파일을 읽을 때 생성을 한다.
파일로부터 정보를 가져올 때는 fileinputstream, 파일을 생성할 때는 fileoutputstream을 사용한다.
경로로부터 파일을 읽어오기 때문에 스트림을 생성해야 한다.
파일명을 구하는 방법
getBytes로 가져올 때는 utf-8, 전체로는 iso-8859-1
String fileName = new String(file.getName().getBytes("utf-8"),"iso-8859-1");
HTTP 응답 메세지 헤더를 세팅한다.
content-Disposition은 HTTP Response Body에 오는 컨텐츠의 기질/성향을 알려주는 속성이고, attachment를 주는 경우에는 body에 오는 값을 다운받으라는 뜻이다.
content-transfer-encoding은 전송되는 데이터의 안의 내용물들의 인코딩 방식이다.
response.setHeader("Content-Disposition", "attachment;filename=\"" +fileName+ "\";");
response.setHeader("Content-Transfer-Encoding", "binary");
파일을 읽어와서 보낼 때 outputStream 사용(파일 쓰기)
OutputStream out = response.getOutputStream();
파일을 읽어오는 과정 - 이미 위에 예외 던지는게 있기 때문에 catch 생성 X
FileInputStream fis = null;
try {
// 경로로부터 파일을 읽어옴
fis = new FileInputStream(file);
// inputstream에 있는걸 outputstream으로 쉽게 넘길 수 있음
// 읽은 정보(fis)를 쓰기 정보(out)로 변환시켜 준다
FileCopyUtils.copy(fis, out);
} finally {
if(fis != null) try {fis.close();} catch (IOException e) {}
}
// 전송 작업 진행 (파일 전송)
out.flush();
라이브러리에 추가
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.13</version>
</dependency>
package kr.spring.ch11.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import kr.spring.ch11.vo.PageRank;
@Controller
public class PageRanksController {
// 엑셀 다운로드
@RequestMapping("/pageRanksExcel.do")
public ModelAndView handle() {
List<PageRank> pageRanks = new ArrayList<PageRank>();
pageRanks.add(new PageRank(1,"/board/list.do")); // 1위
pageRanks.add(new PageRank(2,"/member/login.do")); // 2위
pageRanks.add(new PageRank(3,"/cart/list.do")); // 3위
// 뷰 이름 속성명 속성값
return new ModelAndView("pageRanks", "pageRank" ,pageRanks);
}
// JSON 문자열 처리하기
@RequestMapping("/pageJson.do")
// view를 자동으로 생성해준다. list, map으로 만들어주면 JSON문자열로 처리를 자동해주는 어노테이션
@ResponseBody
public List<PageRank> parseJson() {
List<PageRank> pageRanks = new ArrayList<PageRank>();
pageRanks.add(new PageRank(1,"/file.do"));
pageRanks.add(new PageRank(2,"/pageRanksExcel.do"));
pageRanks.add(new PageRank(3,"/pageJson.do"));
return pageRanks;
}
}
package kr.spring.ch11.vo;
public class PageRank {
// 접속하는 횟수 DB 저장
private int rank;
private String page;
public PageRank() {}
// 생성자 정의
public PageRank(int rank, String page) {
this.rank = rank;
this.page = page;
}
public int getRank() {
return rank;
}
public void setRank(int rank) {
this.rank = rank;
}
public String getPage() {
return page;
}
public void setPage(String page) {
this.page = page;
}
@Override
public String toString() {
return "PageRank [rank=" + rank + ", page=" + page + "]";
}
}
package kr.spring.ch11.view;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.web.servlet.view.document.AbstractXlsView;
import kr.spring.ch11.vo.PageRank;
public class PageRanksView extends AbstractXlsView{
// 엑셀 파일을 다운 받을 수 있는 abstractxlsview를 상속받도록한다
@Override
protected void buildExcelDocument(Map<String, Object> model, Workbook workbook, HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 메소드 단위로 명시하기! (코드 읽기가 쉬워짐)
// 시트 생성
HSSFSheet sheet = createFirstSheet((HSSFWorkbook) workbook);
// 열 이름 생성
createColumnLabel(sheet);
// 표시할 데이터 생성
List<PageRank> pageRanks = (List<PageRank>)model.get("pageRank");
// 0번은 이미 사용되었기 때문에 1번부터 시작할 수 있도록 지정
int rowNum = 1;
for(PageRank rank : pageRanks) {
// 시트 불러오고, 자바빈 불러오고, rowNum은 증가해야하기 때문에 ++도 함께 표시
createPageRankRow(sheet, rank, rowNum++);
}
// 현재 메소드에서 알아서 다 처리 해주기 때문에 스트림 처리는 할 필요가 없음
String fileName = "pageRanks2024.xls";
response.setHeader("Content-Disposition", "attachment;filename=\"" +fileName+ "\";");
response.setHeader("Content-Transfer-Encoding", "binary");
}
// 시트 생성
private HSSFSheet createFirstSheet(HSSFWorkbook workbook) {
// sheet 객체 생성
HSSFSheet sheet = workbook.createSheet();
// sheet 이름 지정 // sheet index, 이름
workbook.setSheetName(0, "페이지 순위");
// 특정 컬럼에 넓이를 지정
// column index, width
sheet.setColumnWidth(1, 256*20);
return sheet;
}
// 열 이름 생성 -> 생성된 시트를 받아서 열 이름 생성
private void createColumnLabel(HSSFSheet sheet) {
HSSFRow firstRow = sheet.createRow(0);
HSSFCell cell = firstRow.createCell(0);
cell.setCellValue("순위");
cell=firstRow.createCell(1);
cell.setCellValue("페이지");
}
// 표시할 데이터 생성 // 순서값을 받아 순서에 맞게 데이터 정렬
private void createPageRankRow(HSSFSheet sheet, PageRank rank, int rowNum) {
HSSFRow row = sheet.createRow(rowNum);
HSSFCell cell = row.createCell(0);
cell.setCellValue(rank.getRank());
cell = row.createCell(1);
cell.setCellValue(rank.getPage());
}
}
<!-- 엑셀 파일 다운로드 -->
<beans:bean class="kr.spring.ch11.controller.PageRanksController"/>
<beans:bean id="pageRank" class="kr.spring.ch11.view.PageRanksView"/>

JSON 문자열 처리하기 결과 화면

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>19.7.0.0</version>
</dependency>
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions</artifactId>
<version>3.9.3</version>
</dependency>
webapp > resources 폴더 생성, sql 폴더 생성, index 생성
resources > css 폴더 생성 후 style.css 추가
sql > table.sql 추가
CREATE table aboard(
num number primary key,
writer varchar2(30) not null,
title varchar2(60) not null,
passwd varchar2(12) not null,
content clob not null,
reg_date date not null
);
create sequence aboard_seq;
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
response.sendRedirect(request.getContextPath()+"/list.do");
%>
package kr.spring.board.vo;
import java.sql.Date;
public class BoardVO {
private int num;
private String writer;
private String title;
private String passwd;
private String content;
private Date reg_date;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getWriter() {
return writer;
}
public void setWriter(String writer) {
this.writer = writer;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getReg_date() {
return reg_date;
}
public void setReg_date(Date reg_date) {
this.reg_date = reg_date;
}
@Override
public String toString() {
return "BoardVO [num=" + num + ", writer=" + writer + ", title=" + title + ", passwd=" + passwd + ", content="
+ content + ", reg_date=" + reg_date + "]";
}
}
package kr.spring.board.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import kr.spring.board.vo.BoardVO;
@Controller
public class BoardController {
// 유효성 체크를 위한 폼 초기화
@ModelAttribute
public BoardVO initCommand() {
return new BoardVO();
}
@RequestMapping("/list.do")
public ModelAndView process() {
ModelAndView mav = new ModelAndView();
mav.setViewName("selectList");
return mav;
}
// 폼 호출하기
@GetMapping("/insert.do")
public String form() {
return "insertForm";
}
}
<%@ page language="java" contentType="text/html; charset=EUC-KR"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시판 목록</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/css/style.css" type="text/css">
</head>
<body>
<div class="page-main">
<h2>게시판 목록</h2>
<div class="align-right">
<input type="button" value="글쓰기" onclick="location.href='insert.do'">
</div>
</div>
</body>
</html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>글 쓰기</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/css/style.css" type="text/css">
</head>
<body>
<div class="page-main">
<h2>글 쓰기</h2>
<form:form action="insert.do" modelAttribute="boardVO">
<ul>
<li>
<form:label path="writer">작성자</form:label>
<form:input path="writer"/>
<form:errors path="writer" cssClass="errror-color"/>
</li>
<li>
<form:label path="title">제목</form:label>
<form:input path="title"/>
<form:errors path="title" cssClass="errror-color"/>
</li>
<li>
<form:label path="passwd">비밀번호</form:label>
<form:password path="passwd"/>
<form:errors path="passwd" cssClass="errror-color"/>
</li>
<li>
<form:label path="content" >내용</form:label>
<form:textarea path="content" cols="5" rows="20"/>
<form:errors path="content" cssClass="errror-color"/>
</li>
</ul>
<div class="align-center">
<form:button>등록</form:button>
<input type="button" value="목록" onclick="location.href='list.do'">
</div>
</form:form>
</div>
</body>
</html>
빈 자동 스캔
<!-- Controller 빈 자동 스캔 -->
<context:component-scan base-package="kr.spring.board.controller"/>
<resources mapping="/resources/**" location="/resources/" />
index 실행시켰을 때 -> list.do로 이동(selectList.jsp가 실행)

글쓰기 버튼 눌르면 insert.do로 이동(insertForm.jsp가 실행)
