Study Web Development

3월 11일

이전으로

Spring

10. springPage

Interceptor

  1. 새 클래스 AdminCheckInterceptor 생성
package kr.spring.interceptor;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class AdminCheckInterceptor extends HandlerInterceptorAdapter {
	private static final Logger logger = LoggerFactory.getLogger(AdminCheckInterceptor.class);
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		logger.info("<<AdminCheckInterceptor 진입>>");
		
		HttpSession session = request.getSession();
		Integer user_auth = (Integer)session.getAttribute("user_auth");
		if(user_auth==null || user_auth!=3) { // 관리자 권한으로 접속하지 않은 경우
			RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/common/notice.jsp");
			dispatcher.forward(request, response);
			
			return false;
		}
		
		return true;
	}
}

프로젝트 설정

XML
  1. servlet-context.xml에서 <interceptor> 태그 사이에 다음의 내용을 추가
			<mapping path="/member/admin_list.do"/>
			<mapping path="/member/admin_update.do"/>
			<mapping path="/board/write.do"/>
			<mapping path="/board/update.do"/>
			<mapping path="/board/delete.do"/>
  1. servlet-context.xml에서 <interceptors> 태그 사이에 다음의 내용을 추가
		<interceptor>
			<mapping path="/member/admin_list.do"/>
			<mapping path="/member/admin_update.do"/>
			<beans:bean class="kr.spring.interceptor.AdminCheckInterceptor"/>
		</interceptor>
  1. root-context.xml에서 <beans> 태그 사이의 내용을 다음처럼 수정
	<!-- 
	빈 자동 스캔 - servlet-context.xml에서 Controller를 자동 스캔 설정해서 아래 설정에서는
	Controller 자동 스캔 제외 
	 -->
	<context:component-scan base-package="kr.spring.**">
		<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
	</context:component-scan>

	<!-- mybatis 설정 -->
	<bean id="sqlSessionFactory"
	     class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource"/>
		<property name="typeAliasesPackage" value="kr.spring.member.vo,kr.spring.board.vo"/> <!-- **을 인식하지 못해서 ,로 패키지명을 나열해야 함 -->
	</bean>
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="kr.spring.**.dao"/>
	</bean>
  1. servlet-context.xml에서 <beans:property name="definitions"> 태그 사이의 내용을 다음처럼 수정
			<beans:list>
				<beans:value>/WEB-INF/tiles-def/main.xml</beans:value>
				<beans:value>/WEB-INF/tiles-def/member.xml</beans:value>
				<beans:value>/WEB-INF/tiles-def/board.xml</beans:value>
			</beans:list>
  1. servlet-context.xml에서 <beans:beans> 태그 사이에 다음의 내용을 추가
	<!-- 파일 다운로드 -->
	<beans:bean id="downloadView" class="kr.spring.view.DownloadView"/>
CSS
  1. css 폴더의 style.css 하단에 다음의 내용을 추가
/* 본문 */
.page-one {
	width: 600px;
	margin: 0 auto;
}
SQL
  1. table.sql 하단에 다음의 내용을 추가
/* 게시판 */
CREATE TABLE spboard(
	board_num NUMBER NOT NULL,
	title VARCHAR2(100) NOT NULL,
	content CLOB not null,
	hit NUMBER(5) DEFAULT 0 NOT NULL,
	reg_date DATE DEFAULT SYSDATE NOT NULL,
	modify_date DATE,
	uploadfile BLOB,
	filename VARCHAR2(100),
	ip VARCHAR2(40) NOT NULL,
	mem_num NUMBER NOT NULL,
	CONSTRAINT spboard_pk PRIMARY KEY (board_num),
	CONSTRAINT spboard_spmember_fk FOREIGN KEY (mem_num) REFERENCES spmember (mem_num)
);

CREATE SEQUENCE spboard_seq;
Tiles
Definition
  1. tiles-def 폴더에 새 XML 파일 board.xml 생성
<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
       "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">

<tiles-definitions>
	<definition name="boardList" extends="main">
		<put-attribute name="title" value="게시판 목록"/>
		<put-attribute name="body" value="/WEB-INF/views/board/boardList.jsp"/>
	</definition>
	<definition name="boardWrite" extends="main">
		<put-attribute name="title" value="글쓰기"/>
		<put-attribute name="body" value="/WEB-INF/views/board/boardWrite.jsp"/>
	</definition>
	<definition name="boardView" extends="main">
		<put-attribute name="title" value="게시판 상세 정보"/>
		<put-attribute name="body" value="/WEB-INF/views/board/boardView.jsp"/>
	</definition>
	<definition name="boardModify" extends="main">
		<put-attribute name="title" value="글 상세 정보 수정"/>
		<put-attribute name="body" value="/WEB-INF/views/board/boardModify.jsp"/>
	</definition>
</tiles-definitions>
Properties
  1. validation.properties 하단에 다음의 내용을 추가
# 게시판
NotEmpty.title=제목은 필수 항목입니다
NotEmpty.content=내용은 필수 항목입니다

Member

JSP
  1. views 폴더의 하위 폴더로 common 생성하고 새 JSP 파일 notice.jsp 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>안내</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/css/style.css">
</head>
<body>
<div class="page-one">
	<h2>안내</h2>
	<div class="result-display">
		<div class="align-center">
			<c:if test="${!empty accessMsg}">
			${accessMsg}
			</c:if>
			<c:if test="${empty accessMsg}">
			잘못된 접근입니다.
			</c:if>
		</div>
		<p>
		<c:if test="${!empty accessUrl}">
		<input type="button" value="이동" onclick="location.href = '${accessUrl}';"> 
		</c:if>
		<c:if test="${empty accessUrl}">
		<input type="button" value="홈으로" onclick="location.href = '${pageContext.request.contextPath}/main/main.do';">
		</c:if>
	</div>
</div>
</body>
</html>

Board

VO
  1. 새 패키지 kr.spring.board.vo 생성하고 새 클래스 BoardVO 생성
package kr.spring.board.vo;

import java.io.IOException;
import java.sql.Date;

import javax.validation.constraints.NotEmpty;

import org.springframework.web.multipart.MultipartFile;

public class BoardVO {
	private int board_num; // 게시글 번호
	@NotEmpty
	private String title; // 제목
	@NotEmpty
	private String content; // 내용
	private int hit; // 조회수
	private Date reg_date; // 등록일
	private Date modify_date; // 수정일
	private MultipartFile upload;
	private byte[] uploadfile; // 파일
	private String filename; // 파일명
	private String ip; // IP 주소
	private int mem_num; // 작성자 회원 번호
	private String id; // 작성자 아이디
	
	// 업로드 파일 처리
	public void setUpload(MultipartFile upload) throws IOException {
		this.upload = upload;
		// MultipartFile을 byte[]로 변환
		setUploadfile(upload.getBytes());
		// 파일명 구하기
		setFilename(upload.getOriginalFilename());
	}
	
	public int getBoard_num() {
		return board_num;
	}
	public void setBoard_num(int board_num) {
		this.board_num = board_num;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getContent() {
		return content;
	}
	public void setContent(String content) {
		this.content = content;
	}
	public int getHit() {
		return hit;
	}
	public void setHit(int hit) {
		this.hit = hit;
	}
	public Date getReg_date() {
		return reg_date;
	}
	public void setReg_date(Date reg_date) {
		this.reg_date = reg_date;
	}
	public Date getModify_date() {
		return modify_date;
	}
	public void setModify_date(Date modify_date) {
		this.modify_date = modify_date;
	}
	public MultipartFile getUpload() {
		return upload;
	}

	public byte[] getUploadfile() {
		return uploadfile;
	}
	public void setUploadfile(byte[] uploadfile) {
		this.uploadfile = uploadfile;
	}
	public String getFilename() {
		return filename;
	}
	public void setFilename(String filename) {
		this.filename = filename;
	}
	public String getIp() {
		return ip;
	}
	public void setIp(String ip) {
		this.ip = ip;
	}
	public int getMem_num() {
		return mem_num;
	}
	public void setMem_num(int mem_num) {
		this.mem_num = mem_num;
	}
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}

	// byte[] 자료형의 데이터를 출력시 양이 많아 프로그램이 느려지므로 uploadfile을 제외한 다른 필드들만 출력하도록 toString() 재정의
	@Override
	public String toString() {
		return "BoardVO [board_num=" + board_num + ", title=" + title + ", content=" + content + ", hit=" + hit
				+ ", reg_date=" + reg_date + ", modify_date=" + modify_date + ", upload=" + upload + ", filename="
				+ filename + ", ip=" + ip + ", mem_num=" + mem_num + ", id=" + id + "]";
	}
}
DAO
  1. 새 패키지 kr.spring.board.dao 생성하고 새 인터페이스 BoardMapper 생성
package kr.spring.board.dao;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import kr.spring.board.vo.BoardVO;

public interface BoardMapper {
	// 부모글
	public List<BoardVO> selectList(Map<String, Object> map);
	public int selectRowCount(Map<String, Object> map);
	@Insert("INSERT INTO spboard (board_num, title, content, uploadfile, filename, ip, mem_num) "
			+ "VALUES (spboard_seq.NEXTVAL, #{title}, #{content}, #{uploadfile}, #{filename}, #{ip}, #{mem_num})")
	public void insertBoard(BoardVO board);
	@Select("SELECT * FROM spboard JOIN spmember USING (mem_num) WHERE board_num=#{board_num}")
	public BoardVO selectBoard(Integer board_num);
	@Update("UPDATE spboard SET hit=hit+1 WHERE board_num=#{board_num}")
	public void updateHit(Integer board_num);
	public void updateBoard(BoardVO board);
	@Delete("DELETE FROM spboard WHERE board_num=#{board_num}")
	public void deleteBoard(Integer board_num);
	@Update("UPDATE spboard SET uploadfile='', filename='' WHERE board_num=#{board_num}")
	public void deleteFile(Integer board_num);
}
  1. kr.spring.board.dao 패키지에 새 XML 파일 BoardMapper.xml 생성
<?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="kr.spring.board.dao.BoardMapper">
	<!-- 목록 -->
	<select id="selectList" parameterType="map" resultType="boardVO">
		SELECT
			*
		FROM (SELECT
					a.*,
					ROWNUM AS rnum 
				FROM (SELECT 
						board_num,
						<![CDATA[
						REPLACE(REPLACE(title, '<', '&lt;'), '>', '&gt;') AS title,
						]]>
						hit,
						reg_date,
						id
					FROM spboard
					JOIN spmember
					USING (mem_num)
					<where>
						<if test="keyword!='' and keyfield==1">
						title LIKE '%' || #{keyword} || '%'
						</if>
						<if test="keyword!='' and keyfield==2">
						id LIKE '%' || #{keyword} || '%'
						</if>
						<if test="keyword!='' and keyfield==3">
						content LIKE '%' || #{keyword} || '%'
						</if>
						<if test="keyword!='' and keyfield==4">
						title LIKE '%' || #{keyword} || '%'
						OR
						content LIKE '%' || #{keyword} || '%'
						</if>
					</where>
					ORDER BY board_num DESC) a)
		<![CDATA[
		WHERE rnum>=#{start} AND rnum<=#{end}
		]]>
	</select>
	<select id="selectRowCount" parameterType="map" resultType="integer">
		SELECT
			COUNT(*)
		FROM spboard
		JOIN spmember
		USING (mem_num)
		<where>
			<if test="keyword!='' and keyfield==1">
			title LIKE '%' || #{keyword} || '%'
			</if>
			<if test="keyword!='' and keyfield==2">
			id LIKE '%' || #{keyword} || '%'
			</if>
			<if test="keyword!='' and keyfield==3">
			content LIKE '%' || #{keyword} || '%'
			</if>
			<if test="keyword!='' and keyfield==4">
			title LIKE '%' || #{keyword} || '%'
			OR
			content LIKE '%' || #{keyword} || '%'
			</if>
		</where>
	</select>
	<!-- 수정 -->
	<update id="updateBoard" parameterType="boardVO">
		UPDATE spboard SET
			<if test="filename!=''">
				uploadfile=#{uploadfile},
				filename=#{filename},
			</if>
			title=#{title},
			content=#{content},
			ip=#{ip},
			modify_date=SYSDATE
		WHERE board_num=#{board_num}
	</update>
</mapper>
Service
  1. 새 패키지 kr.spring.board.service 생성하고 새 인터페이스 BoardService 생성
package kr.spring.board.service;

import java.util.List;
import java.util.Map;

import kr.spring.board.vo.BoardVO;

public interface BoardService {
	// 부모글
	public List<BoardVO> selectList(Map<String, Object> map);
	public int selectRowCount(Map<String, Object> map);
	public void insertBoard(BoardVO board);
	public BoardVO selectBoard(Integer board_num);
	public void updateHit(Integer board_num);
	public void updateBoard(BoardVO board);
	public void deleteBoard(Integer board_num);
	public void deleteFile(Integer board_num);
}
  1. 새 클래스 BoardServiceImpl 생성
package kr.spring.board.service;

import java.util.List;
import java.util.Map;

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

import kr.spring.board.dao.BoardMapper;
import kr.spring.board.vo.BoardVO;

@Service
public class BoardServiceImpl implements BoardService {
	@Autowired
	private BoardMapper boardMapper;
	
	@Override
	public List<BoardVO> selectList(Map<String, Object> map) {
		return boardMapper.selectList(map);
	}

	@Override
	public int selectRowCount(Map<String, Object> map) {
		return boardMapper.selectRowCount(map);
	}

	@Override
	public void insertBoard(BoardVO board) {
		boardMapper.insertBoard(board);
	}

	@Override
	public BoardVO selectBoard(Integer board_num) {
		return boardMapper.selectBoard(board_num);
	}

	@Override
	public void updateHit(Integer board_num) {
		boardMapper.updateHit(board_num);
	}

	@Override
	public void updateBoard(BoardVO board) {
		boardMapper.updateBoard(board);
	}

	@Override
	public void deleteBoard(Integer board_num) {
		boardMapper.deleteBoard(board_num);
	}

	@Override
	public void deleteFile(Integer board_num) {
		boardMapper.deleteFile(board_num);
	}
}
Controller
  1. 새 패키지 kr.spring.board.controller 생성하고 새 클래스 BoardController 생성
package kr.spring.board.controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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 org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import kr.spring.board.service.BoardService;
import kr.spring.board.vo.BoardVO;
import kr.spring.util.PagingUtil;
import kr.spring.util.StringUtil;

@Controller
public class BoardController {
	private static final Logger logger = LoggerFactory.getLogger(BoardController.class);
	
	@Autowired
	private BoardService boardService;

	@ModelAttribute
	public BoardVO initCommand() {
		return new BoardVO();
	}

	// 글 등록 폼
	@GetMapping("/board/write.do")
	public String form() {
		// Tiles 설정 반환
		return "boardWrite";
	}
	
	// 글 등록 폼에서 전송된 데이터 처리
	@PostMapping("/board/write.do")
	public String submit(@Valid BoardVO boardVO, BindingResult result, HttpSession session, HttpServletRequest request) {
		logger.info("<<게시판 글 저장>> : " + boardVO);
		
		// 유효성 검증 결과 오류가 있으면 폼 호출
		if(result.hasErrors()) {
			return form();
		}
		
		Integer user_num = (Integer)session.getAttribute("user_num");
		// 작성자 회원 번호 세팅
		boardVO.setMem_num(user_num);
		// IP 주소 세팅
		boardVO.setIp(request.getRemoteAddr());
		// 글쓰기
		boardService.insertBoard(boardVO);
		
		return "redirect:/board/list.do";
	}
	
	// 게시판 글 목록
	@RequestMapping("/board/list.do")
	public ModelAndView process(@RequestParam(value="pageNum", defaultValue="1") int currentPage,
			@RequestParam(value="keyfield", defaultValue="") String keyfield,
			@RequestParam(value="keyword", defaultValue="") String keyword) {
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("keyfield", keyfield);
		map.put("keyword", keyword);
		
		// 총 게시글 수 또는 검색된 게시글 수
		int count = boardService.selectRowCount(map);
		
		// 페이지 처리
		PagingUtil page = new PagingUtil(keyfield, keyword, currentPage, count, 20, 10, "list.do");
		map.put("start", page.getStartCount());
		map.put("end", page.getEndCount());
		
		List<BoardVO> list = null;
		if(count>0) {
			list = boardService.selectList(map);
		}
		
		ModelAndView mav = new ModelAndView();
		mav.setViewName("boardList"); // Tiles 설정명을 view명으로 지정
		mav.addObject("count", count);
		mav.addObject("list", list);
		mav.addObject("pagingHtml", page.getPagingHtml());
		
		return mav;
	}
	
	// 게시판 글 상세
	@RequestMapping("/board/detail.do")
	public ModelAndView process(@RequestParam int board_num) {
		logger.info("<<게시판 글 상세 : 글 번호 >> : " + board_num);
		
		// 해당 글의 조회수 증가
		boardService.updateHit(board_num);
		
		BoardVO board = boardService.selectBoard(board_num);
		// 타이틀 HTML 불허
		board.setTitle(StringUtil.useNoHtml(board.getTitle()));
		
		return new ModelAndView("boardView", "board", board); // Tiles 설정명, 속성명, 속성값을 저장해 반환
	}
	
	// 이미지 출력
	@RequestMapping("/board/imageView.do")
	public ModelAndView viewImage(@RequestParam int board_num) {
		BoardVO board = boardService.selectBoard(board_num);
		
		ModelAndView mav = new ModelAndView();
		mav.setViewName("imageView");
		mav.addObject("imageFile", board.getUploadfile());
		mav.addObject("filename", board.getFilename());
		
		return mav;
	}
	
	// 파일 다운로드
	@RequestMapping("/board/file.do")
	public ModelAndView download(@RequestParam int board_num) {
		BoardVO board = boardService.selectBoard(board_num);
		
		ModelAndView mav = new ModelAndView();
		mav.setViewName("downloadView");
		mav.addObject("downloadFile", board.getUploadfile());
		mav.addObject("filename", board.getFilename());
		
		return mav;
	}
	
	// 수정 폼
	@GetMapping("/board/update.do")
	public String formUpdate(@RequestParam int board_num, Model model) {
		BoardVO boardVO = boardService.selectBoard(board_num);
		
		model.addAttribute("boardVO", boardVO);
		
		return "boardModify";
	}
	
	// 수정 폼에서 전송된 데이터 처리
	@PostMapping("/board/update.do")
	public String submitUpdate(@Valid BoardVO boardVO, BindingResult result, HttpServletRequest request, Model model) {
		logger.info("<<글 정보 수정>> : " + boardVO);
		
		// 유효성 검증 결과에 오류가 있으면 폼 호출
		if(result.hasErrors()) {
			// title 또는 content가 입력되지 않으면 유효성 검증시 오류가 발생하고 파일 정보를 잃어버리기 때문에 폼을 호출할 때 다시 세팅
			BoardVO vo = boardService.selectBoard(boardVO.getBoard_num());
			boardVO.setFilename(vo.getFilename());
			return "boardModify";
		}
		
		// ip 세팅
		boardVO.setIp(request.getRemoteAddr());
		
		// 글 수정
		boardService.updateBoard(boardVO);
		
		// view에 표시할 메시지
		model.addAttribute("message", "글 수정 완료!");
		model.addAttribute("url", request.getContextPath() + "/board/list.do");
		
		return "common/resultView"; // JSP 반환
	}
	
	// 게시판 글 삭제
	@RequestMapping("/board/delete.do")
	public String submitDelete(@RequestParam int board_num) {
		boardService.deleteBoard(board_num);
		
		return "redirect:/board/list.do";
	}
}
  1. 새 클래스 BoardAjaxController 생성
package kr.spring.board.controller;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import kr.spring.board.service.BoardService;

@Controller
public class BoardAjaxController {
	private static final Logger logger = LoggerFactory.getLogger(BoardAjaxController.class);
	
	@Autowired
	private BoardService boardService;
	
	// 부모글 업로드된 파일 삭제
	@RequestMapping("/board/deleteFile.do")
	@ResponseBody
	public Map<String, String> processFile(int board_num, HttpSession session) {
		Map<String, String> map = new HashMap<String, String>();
		
		Integer user_num = (Integer)session.getAttribute("user_num");
		if(user_num==null) { // 로그인되어 있지 않은 경우
			map.put("result", "logout");
		}
		else {
			boardService.deleteFile(board_num);
			map.put("result", "success");
		}
		
		return map;
	}
}
JSP
  1. views 폴더의 하위 폴더로 board 생성하고 새 JSP 파일 boardList.jsp 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!-- 중앙 컨텐츠 시작 -->
<script type="text/javascript" src="${pageContext.request.contextPath}/resources/js/jquery-3.6.0.min.js"></script>
<script type="text/javascript">
	$(function() {
		$('#search_form').submit(function() {
			if($('#keyword').val().trim()=='') {
				alert('검색어를 입력하세요!');
				$('#keyword').val('').focus();
				return false;
			}
		}); // end of submit
	});
</script>
<div class="page-main">
	<h2>게시판 목록</h2>
	<form action="list.do" id="search_form" method="get">
		<ul class="search">
			<li>
				<select name="keyfield" id="keyfield">
					<option value="1" <c:if test="${param.keyfield==1}">selected</c:if>>제목</option>
					<option value="2" <c:if test="${param.keyfield==2}">selected</c:if>>아이디</option>
					<option value="3" <c:if test="${param.keyfield==3}">selected</c:if>>내용</option>
					<option value="4" <c:if test="${param.keyfield==4}">selected</c:if>>제목+내용</option>
				</select>
			</li>
			<li>
				<input type="search" name="keyword" id="keyword">
			</li>
			<li>
				<input type="submit" value="검색">
				<input type="button" value="목록" onclick="location.href = 'list.do';">
			</li>
		</ul>
	</form>
	<c:if test="${!empty user_num}">
	<div class="align-right">
		<input type="button" value="글쓰기" onclick="location.href = 'write.do';">
	</div>
	</c:if>
	<c:if test="${count==0}">
	<div class="result-display">표시할 게시물이 없습니다.</div>
	</c:if>
	<c:if test="${count>0}">
	<table>
		<tr>
			<th>번호</th>
			<th width="400">제목</th>
			<th>작성자</th>
			<th>작성일</th>
			<th>조회수</th>
		</tr>
		<c:forEach var="board" items="${list}">
		<tr>
			<td>${board.board_num}</td>
			<td><a href="detail.do?board_num=${board.board_num}">${board.title}</a></td>
			<td>${board.id}</td>
			<td>${board.reg_date}</td>
			<td>${board.hit}</td>
		</tr>
		</c:forEach>
	</table>
	<div class="align-center">${pagingHtml}</div>
	</c:if>
</div>
<!-- 중앙 컨텐츠 끝 -->
  1. board 폴더에 새 JSP 파일 boardWrite.jsp 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<!-- 중앙 컨텐츠 시작 -->
<div class="page-main">
	<h2>글쓰기</h2>
	<form:form modelAttribute="boardVO" action="write.do" enctype="multipart/form-data" id="register_form">
		<form:errors element="div" cssClass="error-color"/>
		<ul>
			<li>
				<form:label path="title">제목</form:label>
				<form:input path="title"/>
				<form:errors path="title" cssClass="error-color"/>
			</li>
			<li>
				<form:label path="content">내용</form:label>
				<form:textarea path="content"/>
				<form:errors path="content" cssClass="error-color"/>
			</li>
			<li>
				<form:label path="upload">파일 업로드</form:label>
				<input type="file" name="upload" id="upload">
				<form:errors path="upload" cssClass="error-color"/>
			</li>
		</ul>
		<div class="align-center">
			<form:button>전송</form:button>
			<input type="button" value="목록" onclick="location.href = 'list.do';">
		</div>
	</form:form>
</div>
<!-- 중앙 컨텐츠 끝 -->
  1. board 폴더에 새 JSP 파일 boardView.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/functions" prefix="fn" %>
<!-- 중앙 컨텐츠 시작 -->
<div class="page-main">
	<h2>${board.title}</h2>
	<ul>
		<li>번호 : ${board.board_num}</li>
		<li>작성자 : ${board.id}</li>
		<li>조회수 : ${board.hit}</li>
		<c:if test="${!empty board.modify_date}">
		<li>최근 수정일 : ${board.modify_date}</li>
		</c:if>
		<li>작성일 : ${board.reg_date}</li>
		<c:if test="${!empty board.filename}">
		<li>첨부 파일 : <a href="file.do?board_num=${board.board_num}">${board.filename}</a></li>
		</c:if>
	</ul>
	<hr size="1" width="100%" noshade>
	<c:if test="${fn:endsWith(board.filename, '.jpg') ||
				fn:endsWith(board.filename, '.JPG') ||
				fn:endsWith(board.filename, '.png') ||
				fn:endsWith(board.filename, '.PNG') ||
				fn:endsWith(board.filename, '.gif') ||
				fn:endsWith(board.filename, '.GIF')}">
	<div class="align-center">
		<img src="imageView.do?board_num=${board.board_num}" style="max-width: 500px">
	</div>
	</c:if>
	<p>
		${board.content}
	</p>
	<hr size="1" width="100%" noshade>
	<div class="align-right">
		<c:if test="${!empty user_num && user_num==board.mem_num}">
		<input type="button" value="수정" onclick="location.href = 'update.do?board_num=${board.board_num}';">
		<input type="button" value="삭제" id="delete_btn">
		<script type="text/javascript">
			let delete_btn = document.getElementById('delete_btn');
			delete_btn.onclick = function() {
				let choice = confirm('삭제하시겠습니까?');
				if(choice) {
					location.replace('delete.do?board_num=${board.board_num}');
				}
			};
		</script>
		</c:if>
		<input type="button" value="목록" onclick="location.href = 'list.do';">
	</div>
</div>
<!-- 중앙 컨텐츠 끝 -->
  1. board 폴더에 새 JSP 파일 boardModify.jsp 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!-- 중앙 컨텐츠 시작 -->
<script type="text/javascript" src="${pageContext.request.contextPath}/resources/js/jquery-3.6.0.min.js"></script>
<div class="page-main">
	<h2>글 수정</h2>
	<form:form modelAttribute="boardVO" action="update.do" enctype="multipart/form-data" id="update_form">
		<form:hidden path="board_num"/>
		<form:errors element="div" cssClass="error-color"/>
		<ul>
			<li>
				<form:label path="title">제목</form:label>
				<form:input path="title"/>
				<form:errors path="title" cssClass="error-color"/>
			</li>
			<li>
				<form:label path="content">내용</form:label>
				<form:textarea path="content"/>
				<form:errors path="content" cssClass="error-color"/>
			</li>
			<li>
				<form:label path="upload">파일 업로드</form:label>
				<input type="file" name="upload" id="upload">
				<c:if test="${!empty boardVO.filename}">
				<br>
				<span id="file_detail">(${boardVO.filename}) 파일이 등록되어 있습니다.
				다시 업로드하면 기존 파일은 삭제됩니다.
				<input type="button" value="파일 삭제" id="file_del">
				</span>
<script type="text/javascript">
	$(function() {
		$('#file_del').click(function() {
			let choice = confirm('삭제하시겠습니까?');
			if(choice) {
				$.ajax({
					url:'deleteFile.do',
					data:{board_num:${boardVO.board_num}},
					type:'post',
					dataType:'json',
					cache:false,
					timeout:30000,
					success:function(param) {
						if(param.result=='logout') {
							alert('로그인 후 사용하세요!');
						}
						else if(param.result=='success') {
							$('#file_detail').hide();
						}
						else {
							alert('파일 삭제 오류 발생!');
						}
					},
					error:function() {
						alert('네트워크 오류 발생!');
					}
				}); // end of ajax
			}
		}); // end of click
	});
</script>
				</c:if>
				<form:errors path="upload" cssClass="error-color"/>
			</li>
		</ul>
		<div class="align-center">
			<form:button>전송</form:button>
			<input type="button" value="목록" onclick="location.href = 'list.do';">
		</div>
	</form:form>
</div>
<!-- 중앙 컨텐츠 끝 -->
  1. common 폴더에 새 JSP 파일 resultView.jsp 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<script type="text/javascript">
	alert('${message}');
	location.href = '${url}';
</script>

Util

  1. kr.spring.util 패키지에 새 클래스 StringUtil 생성
package kr.spring.util;

public class StringUtil {
	//HTML 태그를 허용하면서 줄바꿈
	public static String useBrHtml(String str) {
		if(str == null) return null;
		
		return str.replaceAll("\r\n", "<br>")
				  .replaceAll("\r", "<br>")
				  .replaceAll("\n", "<br>");
	}
	
	//HTML 태그를 허용하지 않으면서 줄바꿈
	public static String useBrNoHtml(String str) {
		if(str == null) return null;
		
		return str.replaceAll("<", "&lt;")
				  .replaceAll(">", "&gt;")
				  .replaceAll("\r\n", "<br>")
				  .replaceAll("\r", "<br>")
				  .replaceAll("\n", "<br>");
	}
	
	//HTML 태그를 허용하지 않음
	public static String useNoHtml(String str) {
		if(str == null) return null;
		
		return str.replaceAll("<", "&lt;")
				  .replaceAll(">", "&gt;");
	}
	
	//특정 문자열 이후에 ...으로 처리
	public static String shortWords(int length, String content) {
		if(content == null) return null;
		
		if(content.length() > length) {
			return content.substring(0,length) + " ...";
		}
		return content;
	}
	
}

View

  1. 새 클래스 DownloadView 생성
package kr.spring.view;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Map;

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

import org.apache.commons.io.IOUtils;
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 {
		byte[] file = (byte[])model.get("downloadFile");
		String filename = (String)model.get("filename");

		String userAgent = request.getHeader("User-Agent");
		boolean ie = userAgent.indexOf("MSIE") > -1;
		String fileName = null;
		if (ie) {
			fileName = URLEncoder.encode(filename, "utf-8");
		} else {
			fileName = new String(filename.getBytes("utf-8"),
					"iso-8859-1");
		}
		response.setHeader("Content-Disposition", "attachment; filename=\""
				+ fileName + "\";");
		response.setHeader("Content-Transfer-Encoding", "binary");
		OutputStream out = response.getOutputStream();
		
		InputStream input = new ByteArrayInputStream(file);
		IOUtils.copy(input, out);
		out.flush();
		out.close();
		input.close();
	}

}

다음으로