시작하기에 앞서 테이블을 생성해야 합니다.
CREATE TABLE `tb_board_file` (
  `BOARD_FILE_IDX` int NOT NULL AUTO_INCREMENT,
  `BOARD_IDX` int DEFAULT NULL,
  `FILE_ID` int DEFAULT NULL,
  `USE_YN` varchar(1) DEFAULT 'Y',
  PRIMARY KEY (`BOARD_FILE_IDX`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='게시판 파일테이블'

 

CREATE TABLE `tb_file` (
  `FILE_ID` int NOT NULL AUTO_INCREMENT COMMENT '시퀀스',
  `ORIG_NM` varchar(200) DEFAULT NULL COMMENT '파일 오리지널 이름',
  `LOGI_NM` varchar(200) DEFAULT NULL COMMENT '파일 서버에 올라갔을때 이름',
  `LOGI_PATH` varchar(200) DEFAULT NULL COMMENT '파일 서버에 올라갔을때 path',
  `EXT` varchar(10) DEFAULT NULL COMMENT '파일 확장자',
  `SIZE` int DEFAULT NULL COMMENT '사이즈',
  `REG_DT` datetime DEFAULT NULL,
  PRIMARY KEY (`FILE_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=83 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='파일 저장'

 

Url.JAVA
package com.board.common;

/* api url 정의 */
public final class Url {
	public static final String TILES_ROOT = "/tiles/view";
	public static final String TILES_SINGLE = "/tiles/single";
	public static final String TILES_AJAX = "/tiles/ajax";
	
	/* 로그인 */
	public static final class AUTH {
		
		/* 로그인 url */
		public static final String LOGIN = "/auth/login";
		
		/* 로그인 jsp */
		public static final String LOGIN_JSP = TILES_SINGLE + "/auth/login";
		
		/* 회원가입 url */
		public static final String JOIN = "/auth/join";
		
		/* 회원가입 jsp */
		public static final String JOIN_JSP = TILES_ROOT + "/auth/join";
	
		/* 사용자 등록 */
		public static final String INSERT_USER = "/auth/insertUser";
		
		/* 로그인 인증 요청 */
		public static final String LOGIN_PROC = "/auth/login-proc";
		
		/* 로그아웃 요청 */
		public static final String LOGOUT_PROC = "/auth/logout-proc";
	}
	
	/** 공통 */
	public static final class COMMON{
		
		/* 파일 업로드 */
		public static final String FILE_UPLOAD = "/file-upload";
		
		/** 파일 다운로드 */
		public static final String FILE_DOWNLOAD = "/file-download";
		
	}
	
	/* 메인 화면 */
	public static final class MAIN {
		
		public static final String _MAIN_AJAX_ROOT_PATH = "/main/ajax";
		
		/* 메인 url */
		public static final String MAIN = "/";
		
		/* 메인 jsp */
		public static final String MAIN_JSP = TILES_ROOT + "/main/list";
		
		/* 메인 리스트 ajax */
		public static final String MAIN_LIST_AJAX = _MAIN_AJAX_ROOT_PATH + "/list-view";
		
		/* 메인 글쓰기 */
		public static final String MAIN_WRITE = "/board/write";
		
		/* 메인 글쓰기 jsp */
		public static final String MAIN_WRITE_JSP = TILES_ROOT + "/main/write";
		
		/* 메인 수정화면 */
		public static final String MAIN_UPDATE = "/board/update";
		
		/* 메인 글쓰기 jsp */
		public static final String MAIN_UPDATE_JSP = TILES_ROOT + "/main/update";
		
		/** 게시판 삭제 */
		public static final String MAIN_DELETE = "/board/delete";
		
	}
	
}

 

MainController.java
package com.board.controller;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.board.common.CoTopComponent;
import com.board.common.Constants;
import com.board.common.Url;
import com.board.common.Url.MAIN;
import com.board.dao.FileMapper;
import com.board.dao.MainMapper;
import com.board.service.MainService;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
public class MainController extends CoTopComponent {

	@Autowired MainService mainService;
	@Autowired MainMapper mainMapper;
	@Autowired FileMapper fileMapper;
	
	//profile
	@Value("${spring.profile.active")
	private String profile;
	
	//메인화면
	@GetMapping(Url.MAIN.MAIN)
	public String main() {
		
		return Url.MAIN.MAIN_JSP;
	}
	
	//메인화면 리스트 ajax
	@GetMapping(Url.MAIN.MAIN_LIST_AJAX)
	public String mainListAjax(@RequestParam Map<String, Object> params
								,Pageable pageable
								,Model model
			) {
		
		model.addAttribute("boardList", mainService.selectBoardList(params, pageable, Integer.parseInt(params.get("size").toString())));
		model.addAttribute("resultDataTotal", mainMapper.selectBoardListCnt(params));
		
		return makePageDispatcherUrl(MAIN.MAIN_LIST_AJAX, MAIN._MAIN_AJAX_ROOT_PATH);
	}
	
	
	
	//글쓰기화면
	@GetMapping(Url.MAIN.MAIN_WRITE)
	public String write() {
		return Url.MAIN.MAIN_WRITE_JSP;
	}
	
	//글등록
	@ResponseBody
	@PostMapping(Url.MAIN.MAIN_WRITE)
	public Map<String, Object> writeSubmit(@RequestBody Map<String, Object> params) {
		log.info("params={}", params);
		
		//글등록
		mainMapper.insertBoard(params);
		
		//파일 등록할게 있을경우만
		insertBoardFile(params);
		
		return params;
	}
	
	//수정화면
	@GetMapping(Url.MAIN.MAIN_UPDATE+"/{boardIdx}")
	public String update(@PathVariable("boardIdx") int boardIdx, Model model) {
		
		log.info("boardIdx={}", boardIdx);
		
		//게시판 상세 데이터 조회
		Map<String, Object> boardInfo = mainMapper.selectBoard(boardIdx);
		
		//게시판 상세 파일 리스트 조회
		List<Map<String, Object>> boardFileInfo = mainMapper.selectBoardFile(boardIdx);
		
		if(boardInfo != null) {
			model.addAttribute("boardInfo", boardInfo);
			model.addAttribute("boardIdx", boardIdx);
			model.addAttribute("boardFileInfo", boardFileInfo);
			
			//조회수 업데이트
			mainService.updateViewCount(boardIdx);
		}
		
		else {
			model.addAttribute("boardIdx", "");
		}
		
		return Url.MAIN.MAIN_UPDATE_JSP;
	}
	
	//글수정
	@ResponseBody
	@PostMapping(Url.MAIN.MAIN_UPDATE)
	public Map<String, Object> updateSubmit(@RequestBody Map<String, Object> params) {
		log.info("params={}", params);
		mainMapper.updateBoard(params);
		
		//파일 등록할게 있을경우만
		insertBoardFile(params);
		
		//넘어온 파일 삭제 시퀀스 삭제처리
		if(params.get("deleteFileIdxs") != null) {
			String deleteFileIdxs = (String) params.get("deleteFileIdxs"); 
			String[] fileIdxsArray = deleteFileIdxs.split(",");
			
			//해당 시퀀스 삭제처리
			for(int i=0; i<fileIdxsArray.length; i++) {
				String fileId = fileIdxsArray[i];
				fileMapper.deleteFile(fileId);
			}
		}
		return params;
	}
	
	/** 게시판 삭제 */
	@ResponseBody
	@PostMapping(Url.MAIN.MAIN_DELETE)
	public List<String> deleteSubmit(@RequestBody List<String> boardIdxArray) {
		
		log.info("boardIdxArray={}", boardIdxArray);
		mainService.deleteBoard(boardIdxArray);
		return boardIdxArray;
	}
	
	//server health check
	@RequestMapping(value= { Constants.HEALTH_CHECK_URL }, produces=MediaType.TEXT_HTML_VALUE)
	   public void healthCheck( HttpServletRequest req, HttpServletResponse res ) throws IOException {

	      String ip = req.getHeader("X-FORWARDED-FOR");
	      if (ip == null) ip = req.getRemoteAddr();

	      PrintWriter pw = res.getWriter();
	      pw.write(" - Active Profile : " + profile + "\n");
	      pw.write(" - Client IP : " + ip);
	      pw.close();
	}
	
	//게시판 파일 등록
	private void insertBoardFile(Map<String, Object> params) {
		//파일 등록할게 있을경우만
		if(params.get("fileIdxs") != null) {
			//파일 등록
			String fileIdxs = ((String) params.get("fileIdxs")).replace("[", "").replace("]", "");
			String[] fileIdxArray = fileIdxs.split(",");
			
	        for (int i=0; i<fileIdxArray.length; i++) {
	        	params.put("fileId", fileIdxArray[i]);
	        	mainMapper.insertBoardFile(params);
	        }
		}
	}
	
}

 

MainMapper.java
package com.board.dao;


import java.util.List;
import java.util.Map;
import org.springframework.data.domain.Pageable;
import org.apache.ibatis.annotations.Param;

public interface MainMapper {

	//글쓰기 리스트 조회
	public List<Map<String, Object>> selectBoardList(
			 @Param(value = "params") Map<String, Object> params
			,@Param(value = "pageable") Pageable pageable
			,@Param(value = "size") int size);
	
	//글쓰기 리스트 count
	int selectBoardListCnt(@Param(value = "params") Map<String, Object> params);
	
	//글쓰기 등록
	public int insertBoard(Map<String, Object> params);
	
	//글 수정
	public int updateBoard(Map<String, Object> params);
	
	//글쓰기 상세조회
	public Map<String, Object> selectBoard(int boardIdx);
	
	//게시판 파일리스트 조회
	public List<Map<String, Object>> selectBoardFile(int boardIdx);
	
	//조회수 업데이트
	public int updateViewCount(Map<String, Object> params);
	
	//겍시판 삭제
	public int deleteBoard(String boardIdx);

	//글쓰기 파일 등록
	public int insertBoardFile(Map<String, Object> params);
	
}

 

MainMapper.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">
<!-- FAQ SQL Mapper -->
<mapper namespace="com.board.dao.MainMapper">
	
	<!-- 게시판 리스트 조회  -->
	<select id="selectBoardList" resultType="CamelMap">
		SELECT
			 TB.BOARD_IDX
			,TB.BOARD_TITLE
			,TB.BOARD_CONTENT
			,TB.REG_ID
			,TB.VIEW_COUNT
			,TB.USE_YN
			,TB.REG_DATE
			,(SELECT TC.CODE_EXP FROM TB_CODE TC WHERE TC.CODE_NO = '200' AND TC.CODE_NAME = TA.AUTHORITY) AS REG_NAME
			,(SELECT COUNT(*) FROM TB_BOARD_FILE TBF WHERE TBF.BOARD_IDX = TB.BOARD_IDX ) AS FILE_COUNT
		FROM
			 TB_BOARD TB
			,TB_AUTHORITIES TA
		WHERE
			USE_YN = 'Y'
		AND
			TB.REG_ID = TA.USER_ID
		
		ORDER BY TB.BOARD_IDX DESC		
		<if test="pageable != null">
			LIMIT #{pageable.offset}, #{size}
		</if>
				
	</select>
	
	<!-- 게시판 count -->
	<select id="selectBoardListCnt" resultType="int">
		SELECT
			COUNT(*)
		FROM
			TB_BOARD
		WHERE
			USE_YN = 'Y'
	</select>
	
	<!-- 게시판 상세 조회 -->
	<select id="selectBoard" resultType="CamelMap">
		SELECT
			 TB.BOARD_IDX
			,TB.BOARD_TITLE
			,TB.BOARD_CONTENT
			,TB.REG_ID
			,TB.VIEW_COUNT
			,TB.USE_YN
			,TB.REG_DATE
			,(SELECT TC.CODE_EXP FROM TB_CODE TC WHERE TC.CODE_NO = '200' AND TC.CODE_NAME = TA.AUTHORITY) AS REG_NAME
		FROM
			TB_BOARD TB
			,TB_AUTHORITIES TA
		WHERE
			USE_YN = 'Y'
		AND
			TB.REG_ID = TA.USER_ID
		AND
			TB.BOARD_IDX = #{boardIdx}		
	</select>
	
	<!-- 게시판 파일 리스트 조회 -->	
	<select id="selectBoardFile" resultType="CamelMap">
		SELECT
			 TBF.BOARD_FILE_IDX
			,TBF.BOARD_IDX
			,TBF.FILE_ID
			,TBF.USE_YN
			,TF.ORIG_NM
			,TF.LOGI_PATH
		FROM
			 TB_BOARD_FILE TBF
			,TB_FILE TF
		WHERE
			TBF.USE_YN = 'Y'
		AND
			TBF.BOARD_IDX = #{boardIdx}
		AND
			TBF.FILE_ID = TF.FILE_ID	
	</select>

	<!-- 게시판 등록 -->
	<insert id="insertBoard" useGeneratedKeys="true" keyProperty="boardIdx">
		INSERT INTO
			TB_BOARD(
				  BOARD_TITLE
				 ,BOARD_CONTENT
				 ,REG_ID
				 ,REG_DATE
			)
			VALUES(
				  #{boardTitle}
				 ,#{boardContent}
				 ,#{userId}
				 ,NOW()
			)
	</insert>
	
	<!-- 게시판 수정 -->
	<update id="updateBoard">
		UPDATE 
			TB_BOARD
		SET
			 BOARD_TITLE = #{boardTitle}
			,BOARD_CONTENT = #{boardContent}
		WHERE
			BOARD_IDX = #{boardIdx}		
	</update>
	
	<!-- 조회수 업데이트 -->
	<update id="updateViewCount">
		UPDATE
			TB_BOARD T1
		SET
			T1.VIEW_COUNT = #{viewCount}
		WHERE
			T1.BOARD_IDX = #{boardIdx}		
	</update>
	
	<!-- 게시판 삭제 -->
	<update id="deleteBoard">
		UPDATE
			TB_BOARD
		SET
			USE_YN = 'N'
		WHERE
			BOARD_IDX = #{boardIdx}		
	</update>
	
	<!-- 게시판 파일 테이블 insert -->
	<insert id="insertBoardFile">
		INSERT INTO
			TB_BOARD_FILE(
				  BOARD_IDX
				 ,FILE_ID
			)
			VALUES(
				  #{boardIdx}
				 ,#{fileId}
			)
	</insert>
	
	
</mapper>

 

FileController.java
package com.board.controller;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import com.board.common.Url.COMMON;
import com.board.dao.FileMapper;
import com.board.service.FileService;

import lombok.extern.slf4j.Slf4j;

/* 파일 controller */
@Controller
@Slf4j
public class FileController {
	
	@Autowired FileService fileService;
	@Autowired FileMapper fileMapper;
	
	/** 멀티파일 업로드 */
	@PostMapping(value={COMMON.FILE_UPLOAD})
	@ResponseBody
	public Map<String, Object> fileUpload(
			@RequestParam("article_file") List<MultipartFile> multipartFile
			, HttpServletRequest request) throws IOException {
		
		log.info("파일 컨트롤러 진입");
		return fileService.uploadFile(request, multipartFile);
	}
	
	/** 파일 다운로드 
	 * @throws UnsupportedEncodingException */
	@GetMapping(value = {COMMON.FILE_DOWNLOAD+"/{fileIdx}"})
	@ResponseBody
	public void downloadFile(HttpServletResponse res, @PathVariable String fileIdx) throws UnsupportedEncodingException {
		
		//파일 조회
		Map<String, Object> fileInfo = fileMapper.getFileInfo(fileIdx); 
		
		//파일 경로
		Path saveFilePath = Paths.get(fileInfo.get("logiPath") + File.separator + fileInfo.get("logiNm"));
		
		//해당 경로에 파일이 없으면 
		if(!saveFilePath.toFile().exists()) {
			throw new RuntimeException("file not found");
		}
		
		res.setHeader("Content-Disposition", "attachment; filename=\"" +  URLEncoder.encode((String) fileInfo.get("origNm"), "UTF-8") + "\";");
		res.setHeader("Content-Transfer-Encoding", "binary");
		res.setHeader("Content-Type", "application/download; utf-8");
		res.setHeader("Pragma", "no-cache;");
		res.setHeader("Expires", "-1;");
		
		FileInputStream fis = null;
		
		try {
			fis = new FileInputStream(saveFilePath.toFile());
			FileCopyUtils.copy(fis, res.getOutputStream());
			res.getOutputStream().flush();
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}
		finally {
			try {
				fis.close();
			}
			catch (Exception e) {
				e.printStackTrace();
			}
			
		}
		
	}
	

}

 

FileService.java
package com.board.service;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import com.board.dao.FileMapper;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service("fileService")
public class FileService {

	@Autowired Environment env;
	@Autowired FileService fileService;
	@Autowired FileMapper fileMapper;
	
	private final String UPLOAD_FILE_PATH = "common.public_upload_local_path";
	
	/** 파일 업로드 */
	public Map<String, Object> uploadFile(HttpServletRequest request, List<MultipartFile> multipartFile) throws IOException{
		
		Map<String, Object> result = new HashMap<String, Object>();
		
		//파일 시퀀스 리스트
		List<String> fileIds = new ArrayList<String>();
	
		result.put("result", "FAIL");
		
		String _filePath = String.valueOf(request.getParameter("filePath")).equals("null") ? env.getProperty(UPLOAD_FILE_PATH) : env.getProperty(UPLOAD_FILE_PATH)+String.valueOf(request.getParameter("filePath")+"/");
		
		try {
			// 파일이 있을때 탄다.
			if(multipartFile.size() > 0 && !multipartFile.get(0).getOriginalFilename().equals("")) {
				
				for(MultipartFile file : multipartFile) {
					
					String originalFileName = file.getOriginalFilename();	//오리지날 파일명
					String extension = originalFileName.substring(originalFileName.lastIndexOf("."));	//파일 확장자
					String savedFileName = UUID.randomUUID() + extension;	//저장될 파일 명
					
					File targetFile = new File(_filePath + savedFileName);	
					
					//파일 저장후 db insert
					result.put("pyscPath", _filePath);
					result.put("pyscNm", savedFileName);
					result.put("origNm", originalFileName);
					result.put("fileExt", extension);
					result.put("contentType", file.getContentType());
					result.put("fileSize", file.getSize());
					
					//파일 insert
					fileService.insertFile(result);
					log.info("fileId={}", result.get("fileId"));
					
					//배열에 담기
					fileIds.add(String.valueOf(result.get("fileId")));
					
					try {
						InputStream fileStream = file.getInputStream();
						FileUtils.copyInputStreamToFile(fileStream, targetFile); //파일 저장
						
					} catch (Exception e) {
						//파일삭제
						FileUtils.deleteQuietly(targetFile);	//저장된 현재 파일 삭제
						e.printStackTrace();
						result.put("result", "FAIL");
						break;
					}
				}
				
				result.put("fileIdxs", fileIds.toString());
				result.put("result", "OK");
				
			}
			// 파일 아무것도 첨부 안했을때 탄다.(게시판일때, 업로드 없이 글을 등록하는경우)
			else {
				result.put("result", "OK");
			}
		}catch(Exception e){
			e.printStackTrace();
			result.put("result", "FAIL");
		}
		
		return result;
	}
	
	/** 파일 저장 db */
	@Transactional
	public int insertFile(Map<String, Object> params) {
		return fileMapper.insertFile(params);
	}
	
}

 

FileMapper.java
package com.board.dao;

import java.util.Map;

/* file dao */
public interface FileMapper {
	
	/** 파일 등록 */
	int insertFile(Map<String, Object> file);
	
	/** 파일 조회 */
	Map<String, Object> getFileInfo(String fileId);
	
	/** 해당 파일 삭제처리 */
	int deleteFile(String fileId);
}

 

FileMapper.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">

<!-- FAQ SQL Mapper -->
<mapper namespace="com.board.dao.FileMapper">
	
	<insert id="insertFile" useGeneratedKeys="true" keyProperty="fileId" >
		INSERT INTO TB_FILE (
			 ORIG_NM
			,LOGI_NM
			,LOGI_PATH
			,EXT
			,SIZE
			,REG_DT
		)VALUES(
			 #{origNm}
			,#{pyscNm}
			,#{pyscPath}
			,#{fileExt}
			,#{fileSize}
			,NOW()
		)
	</insert>
	
	<!-- 파일 조회 -->
	<select id="getFileInfo" resultType="CamelMap">
		SELECT
			 FILE_ID
			,ORIG_NM
			,LOGI_NM
			,LOGI_PATH
			,EXT
			,SIZE
			,REG_DT
		FROM
			TB_FILE
		WHERE
			FILE_ID = #{fileId}	
	</select>
	
	<!-- 파일 논리삭제 -->
	<update id="deleteFile">
		UPDATE
			TB_BOARD_FILE
		SET
			USE_YN = 'N'
		WHERE
			FILE_ID = #{fileId}		
	</update>
	
	
</mapper>

 

SecurityConfig.java
package com.board.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import com.board.common.Constants;
import com.board.common.Url;
import com.board.common.Url.COMMON;

@Configuration
@EnableWebSecurity
@ComponentScan(value = Constants.APP_DEFAULT_PACKAGE_NAME)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Autowired private LoginSuccessHandler loginSuccessHandler;
   @Autowired private LoginFailureHandler loginFailureHandler;
//   @Autowired private CustomerUserDetailsService customUserDetailsService;

   /**
    * Configure.
    *
    * @param auth the auth
    * @throws Exception the exception
    */
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {

//      auth.userDetailsService(customUserDetailsService);

      auth.authenticationProvider(new AdminAuthenticationProvider());
      super.configure(auth);
   }

   /**
    * http 요청 검사.
    *
    * @param http the http
    * @throws Exception the exception
    */
   @Override
   protected void configure(HttpSecurity http) throws Exception {

      http
      // replay 어택을 막기 위한 csrf 토큰의 생성을 비활성화(disabled) 처리
      .csrf().disable()
      // pdf viewer 에서 'X-Frame-Options' to 'DENY' 대응
      .headers().frameOptions().disable().and()
      // 요청에 대한 권한 매핑
      .authorizeRequests()
         .antMatchers( "/auth/**" ).permitAll()                // 패스워드찾기,회원가입
         .antMatchers( "/" ).permitAll()
         .antMatchers( COMMON.FILE_DOWNLOAD + "/**" ).permitAll()		//파일다운로드
         .antMatchers( "/**/ajax/**" ).permitAll()
         .antMatchers( "/board/**" ).permitAll()
         .antMatchers( "/resource/**/images/**" ).permitAll()   // image
         .anyRequest().authenticated()                     // 모든 요청에 대해 권한 확인이 필요
//         .anyRequest().permitAll()
      .and()
      // 로그인 화면 설정
      .formLogin()
         .permitAll()
         .loginPage( Url.AUTH.LOGIN )
         .loginProcessingUrl( Url.AUTH.LOGIN_PROC )
         .successHandler( loginSuccessHandler )
         .failureHandler( loginFailureHandler )
         .usernameParameter( USERNAME_PARAM )
         .passwordParameter( PASSWORD_PARAM )
      .and()
      .logout()
         .logoutUrl( Url.AUTH.LOGOUT_PROC )
         .invalidateHttpSession(true)
         .deleteCookies("JSESSIONID")
      .and()
      // 세션 관리
      .sessionManagement()
         .maximumSessions(200) /* session 허용 갯수 */
         .expiredUrl(Url.AUTH.LOGIN) /* session 만료시 이동 페이지*/
         .sessionRegistry(sessionRegistry())                     // 세션을 목록에 담아둠
         .maxSessionsPreventsLogin(true) /* 동일한 사용자 로그인시 x, false 일 경우 기존 사용자 */
   ;
   }

   /**
    * web요청 검사.
    *
    * @param web the web
    * @throws Exception the exception
    */
   @Override
   public void configure(WebSecurity web) throws Exception {
      // Security Debug
//      web.debug(true);

      web
         .ignoring()
            // static 리소스 경로는 webSecurity 검사 제외
            .antMatchers( Constants.STATIC_RESOURCES_URL_PATTERNS )
            .antMatchers( Constants.HEALTH_CHECK_URL )
            .antMatchers(HttpMethod.GET, "/exception/**")
      ;
      super.configure(web);
   }

   /**
    * PasswordEncoder.
    *
    * @return the password encoder
    */
   @Bean
   public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
   }

   @Bean
   public SessionRegistry sessionRegistry() {
      return new SpringSecuritySessionRegistImpl();
   }

   /**
    * AuthenticationProvider
    * <br>관리자의 계정정보를 통해 로그인 인증을 처리합니다.
    *
    * @return the authentication provider
    * @see kr.mediaflow.fdwm.config.DatabaseConfig
    */
//   @Bean
//   public AuthenticationProvider daoAuthenticationProvider() {
//      DaoAuthenticationProvider impl = new DaoAuthenticationProvider();
//      impl.setUserDetailsService(customUserDetailsService);
//      impl.setPasswordEncoder(new BCryptPasswordEncoder());
//      impl.setHideUserNotFoundExceptions(false);
//      return impl;
//   }

   /**  관리자 아이디 파라미터 이름 : {@value #USERNAME_PARAM}. */
   public static final String USERNAME_PARAM = "un";

   /**  관리자 비밀번호 파라미터 이름 : {@value #PASSWORD_PARAK}. */
   public static final String PASSWORD_PARAM = "up";
}

 

list-view.jsp
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles" %>
<%@ include file="/WEB-INF/template/constants.jsp"%>

<form>
    <table class="table table-hover">
    	<colgroup>
		   <col width="2%" />
		   <col width="5%" />
		   <col width="20%" />
		   <col width="5%" />
		   <col width="5%" />
		   <col width="5%" />
		   <col width="5%" />
		</colgroup>
        <thead>
            <tr>
            	<th>
            		<label class="checkbox-inline">
						<input type="checkbox" id="allCheckBox" class="chk" onclick="allChecked(this)">
					</label>
            	</th>
                <th>번호</th>
                <th>제목</th>
                <th>작성자</th>
                <th>날짜</th>
                <th>조회수</th>
                <th>파일유무</th>
            </tr>
        </thead>
        <tbody>
        	<c:forEach var="board" items="${boardList.content}" varStatus="vs">
				<tr>
					<td>
						<label class="checkbox-inline">
							<input type="checkbox" class="chk" name="cchk" onclick="cchkClicked()" value="${board.boardIdx}">
						</label>
					<td>${resultDataTotal - (boardList.size * boardList.number) - vs.index}</td>
				    <td><a href='/board/update/${board.boardIdx}'>${board.boardTitle}</a></td>
				    <td>${board.regName}</td>
				    <td>
				    	<fmt:parseDate value="${board.regDate}" var="regDate" pattern="yyyy-MM-dd HH:mm:ss.s"/>
						<fmt:formatDate value="${board.regDate}" pattern="yyyy-MM-dd"/>
					</td>
				    <td>${board.viewCount}</td>
				    <td ${board.fileCount }>
				    	<c:if test="${board.fileCount != 0}">
				    		<img src="/images/file_icon.png" style="width:20px; height:auto; vertical-align: center; "/>
				    	</c:if>
				    </td>
				</tr>
			</c:forEach>
			
			<c:if test="${resultDataTotal == 0}">
				<tr>
					<center><td colspan="6" style="text-align: center;">등록된 게시판 리스트가 없습니다.</td></center>
				</tr>
			</c:if>
				
        </tbody>
    </table>
   	
   	<!-- ADMIN 권한일경우에만 글쓰기 권한있음 -->
   	<c:if test="${sessUserInfo.authority == 'ADMIN'}">
		<div class="text-right">            
			<a href='javascript:boardDelete();' class="btn btn-danger">글삭제</a>      
            <a href='/board/write' class="btn btn-primary">글쓰기</a>            
		</div>
    </c:if>
   	 
    <div class="text-center">
    	<c:if test="${resultDataTotal != 0}">
         <ul class="pagination">
            <paginator:print goPageScript="goPage" curPage="${boardList.number}" totPages="${boardList.totalPages}"/>
         </ul>
      </c:if>
    
    	<!--
    	<ul class="pagination">
    		<li><a href="#">prev</a></li>
    		<li><a href="#">1</a></li>
    		<li><a href="#">2</a></li>
    		<li><a href="#">3</a></li>
    		<li><a href="#">4</a></li>
    		<li><a href="#">5</a></li>
    		<li><a href="#">next</a></li>
    	</ul>
    	-->
    </div>	
</form>

 

 

write.jsp
<%@ page language="java" session="true" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ include file="/WEB-INF/template/constants.jsp"%>

<body>
	<article>
		<div class="container" role="main">
			<h2>게시판 글쓰기</h2>
				<div class="mb-3">
					<label for="title">제목</label>
					<input type="text" class="form-control" id="boardTitle" name="boardTitle" placeholder="제목을 입력해 주세요">
				</div>
				<br>
				<div class="mb-3">
					<label for="reg_id">작성자</label>
					<input type="text" class="form-control" id="regId" name="regId"  value="${sessUserInfo.authorityNm}" readonly>
				</div>
				<br>
				<div class="mb-3">
					<label for="content">내용</label>
					<textarea class="form-control" rows="5" id="boardContent" name="boardContent" placeholder="내용을 입력해 주세요"></textarea>
				</div>	
				<br>
				<div class="mb-3">
					<form name="dataForm" id="dataForm" onsubmit="return registerAction()">
						<button id="btn-upload" onclick="fileClick()" type="button" style="border: 1px solid #ddd; outline: none;">파일 추가</button>
					  	<input id="input_file" onChange="fileChange(this)" multiple="multiple" type="file" style="display:none;">
					  	<span style="font-size:10px; color: gray;">※첨부파일은 최대 10개까지 등록이 가능합니다.</span>
					  	<br>
					  	<br>
					  	<br>
					  	<div class="data_file_txt" id="data_file_txt">
							<span>첨부 파일</span>
							<br />
							<div id="articlefileChange">
							</div>
						</div>
					</form>
				</div>
			<br>
			<div>
				<button onclick="registerAction()" type="button" class="btn btn-sm btn-primary" id="btnSave">저장</button>
				<!-- <button onclick="registerAction()" type="button" class="btn btn-sm btn-primary" id="btnSave">파입업로드 테스트</button> -->
				<button onclick="location.href='/'" type="button" class="btn btn-sm btn-primary" id="btnList">목록</button>
			</div>
		</div>
	</article>
</body>

 

write-js.jsp
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<script>

	//파일 현재 필드 갯수
	var fileCount = 0;
	
	//전체 업로드 갯수
	var totalCount = 10;
	
	//파일 고유 넘버
	var fileNum = 0;
	
	//첨부파일 배열
	var content_files = new Array();
	
	//파일 시퀀스들
	var fileIdxs = "";
	
	//파일 추가 클릭
	function fileClick(){
		$("#input_file").click();
	}
	
	//파일 change 이벤트
	function fileChange(target){
		 
		var files = target.files;
		
		//파일 배열 담기
		var filesArr = Array.prototype.slice.call(files);
		
		if(fileCount + filesArr.length > totalCount){
			alert("파일은 최대 "+totalCount+"개까지 업로드 할 수 있습니다.");
			return false;
		}
		
		else{
			fileCount = fileCount + filesArr.length;
		}
		
		// 각각의 파일 배열담기 및 기타
		filesArr.forEach(function (f) {
			var reader = new FileReader();
			reader.onload = function (e) {
			content_files.push(f);
			console.log(content_files)
			$('#articlefileChange').append(
	       		'<div id="file' + fileNum + '" onclick="fileDelete(\'file' + fileNum + '\')">'
	       		+ '<font style="font-size:12px">' + f.name + '</font>'  
	       		+ '<img src="/images/icon_minus.png" style="width:20px; height:auto; vertical-align: middle; cursor: pointer;"/>' 
	       		+ '<div/>'
			);
	        fileNum++;
	      };
	      reader.readAsDataURL(f);
	    });
		
	    //초기화 한다.
	    $("#input_file").val(""); 	
	    
	}
	
	//파일 삭제
	function fileDelete(fileNum){
		var no = fileNum.replace(/[^0-9]/g, "");
	    content_files[no].is_delete = true;
		$('#' + fileNum).remove();
		fileCount --;
	}
	
	//게시판 등록	
	function writeSubmit(){
		
		//step2. 게시판 등록
		var params = {
			 boardTitle : $.trim($("#boardTitle").val())
			,boardContent : $.trim($("#boardContent").val())
			,userId : "${sessUserInfo.userId}"
			,fileIdxs : fileIdxs
		}
		
		if(params.boardTitle == ""){
			alert("제목을 입력해주세요.");
			return false;
		}
		
		else if(params.boardContent == ""){
			alert("내용을 입력해주세요.");
			return false;
		} 
		
		$.ajax({
	         type : 'POST'
	        ,url : '/board/write'
	        ,dataType : 'json'
	        ,data : JSON.stringify(params) 
	        ,contentType: 'application/json'
	        ,success : function(result) {
				alert("해당글이 정상적으로 등록되었습니다.");
				location.href="/";
	        },
	        error: function(request, status, error) {
	          
	        }
	    }) 
	}
	
	//파일 저장
	function registerAction(){
		console.log(content_files);
		var form = $("form")[0];        
	 	var formData = new FormData(form);
		for (var x = 0; x < content_files.length; x++) {
			// 삭제 안한것만 담아 준다. 
			if(!content_files[x].is_delete){
				 formData.append("article_file", content_files[x]);
				 formData.append("filePath", "/main");
			}
		}
		
	   /*
	   * 파일업로드 multiple ajax처리
	   */    
		$.ajax({
	   	      type: "POST",
	   	   	  enctype: "multipart/form-data",
	   	      url: "/file-upload",
	       	  data : formData,
	       	  processData: false,
	   	      contentType: false,
	   	      success: function (data) {
	   	    	
	   	    	console.log(data)

	   	    	//파일 시퀀스들
	   	    	fileIdxs = data.fileIdxs;
	   	    	if(data.result == "OK"){
	   	    		writeSubmit();
				} else
					alert("서버내 오류로 처리가 지연되고있습니다. 잠시 후 다시 시도해주세요");
	   	      },
	   	      error: function (xhr, status, error) {
	   	    	alert("서버오류로 지연되고있습니다. 잠시 후 다시 시도해주시기 바랍니다.");
			}
		});
	}
	
	
</script>

 

update.jsp
<%@ page language="java" session="true" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ include file="/WEB-INF/template/constants.jsp"%>

<body>
	<article>
		<div class="container" role="main">
			<h2>게시판 상세</h2>
			<form name="form" id="form" role="form" method="post" action="${pageContext.request.contextPath}/board/saveBoard">
				<div class="mb-3">
					<label for="title">제목</label>
					<input type="text" class="form-control" id="boardTitle" name="boardTitle" placeholder="제목을 입력해 주세요" value="${boardInfo.boardTitle}" <c:if test="${sessUserInfo.authority != 'ADMIN'}">readonly</c:if>>
					<input type="hidden" id="boardIdx" value="${boardIdx}" />
				</div>
			
				<div class="mb-3">
					<label for="reg_id">작성자</label>
					<input type="text" class="form-control" id="regId" name="regId"  value="${boardInfo.regName}" readonly>
				</div>

				
				<div class="mb-3">
					<label for="content">내용</label>
					<textarea class="form-control" rows="5" id="boardContent" name="boardContent" placeholder="내용을 입력해 주세요" <c:if test="${sessUserInfo.authority != 'ADMIN'}">readonly</c:if>>${boardInfo.boardContent}</textarea>
				</div>	
				
				<!-- 관리자가 아닐경우 -->
				<c:if test="${sessUserInfo.authority != 'ADMIN'}">
					<div class="mb-3">
						<div class="data_file_txt" id="data_file_txt">
							<span>첨부 파일</span>
							<br>
							<div id="articlefileChange">
								<c:forEach var="boardFileInfo" items="${boardFileInfo}" varStatus="vs">
										<div id="file${vs.index}">
											<a href="/file-download/${boardFileInfo.fileId}">
												<font style="font-size:12px">${boardFileInfo.origNm}</font>
												<img src="/images/file_icon.png" style="width:20px; height:auto; vertical-align: middle; cursor: pointer;">
											</a>
										</div>
								</c:forEach>
							</div>
						</div>
					</div>
				</c:if>
				
				<!-- 관리자일경우 -->
				<c:if test="${sessUserInfo.authority == 'ADMIN'}">
					<div class="mb-3">
						<form name="dataForm" id="dataForm" onsubmit="return registerAction()">
							<button id="btn-upload" onclick="fileClick()" type="button" style="border: 1px solid #ddd; outline: none;">파일 추가</button>
						  	<input id="input_file" onChange="fileChange(this)" multiple="multiple" type="file" style="display:none;">
						  	<span style="font-size:10px; color: gray;">※첨부파일은 최대 10개까지 등록이 가능합니다.</span>
						  	<br>
						  	<br>
						  	<br>
						  	<div class="data_file_txt" id="data_file_txt">
								<span>첨부 파일</span>
								<br />
								<div id="articlefileChange">
									<c:forEach var="boardFileInfo" items="${boardFileInfo}" varStatus="vs">
										<div class="attachDiv">
											<a href="/file-download/${boardFileInfo.fileId}">
												<font style="font-size:12px">${boardFileInfo.origNm}</font>
												<img src="/images/file_icon.png" style="width:20px; height:auto; vertical-align: middle; cursor: pointer;">
											</a>
											
											<a class="beforeDeleteFile" data-attr="${boardFileInfo.fileId}">
												<img src="/images/icon_minus.png" style="width:20px; height:auto; vertical-align: middle; cursor: pointer;">
											</a>
										</div>
								</c:forEach>
								</div>
							</div>
						</form>
					</div>
				</c:if>
				
			</form>
			
			<br>
			<c:if test="${sessUserInfo.authority == 'ADMIN'}">
				<div>
					<button onclick="registerAction()" type="button" class="btn btn-sm btn-primary" id="btnSave">수정</button>
					<button onclick="boardDelete()" type="button" class="btn btn-sm btn-danger" >삭제</button>
					<button onclick="location.href='/'" type="button" class="btn btn-sm btn-primary" id="btnList">목록</button>
				</div>
			</c:if>
		</div>
	</article>
</body>

 

update-js.jsp
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<script>
	
	//파일 현재 필드 갯수
	var fileCount = 0;
	
	//전체 업로드 갯수
	var totalCount = 10;
	
	//파일 고유 넘버
	var fileNum = 0;
	
	//첨부파일 배열
	var content_files = new Array();
	
	//파일 시퀀스들
	var fileIdxs = "";
	
	//이전에 등록한 파일 삭제 클릭시 시퀀스
	var deleteFileIdxs = [];
	
	$(document).ready(function(){
		updateCtrl.init();
	});
	
	var updateCtrl = {
		init : function(){
			this.bindData();
			this.bindEvent();
		},
		
		bindData : function(){
			//게시물번호가 없을경우
			if("${boardIdx}" == ""){
				alert("해당 게시물은 없는 번호입니다.");
				location.href="/";
			}
		},
		
		bindEvent : function(){
			
			$(".beforeDeleteFile").click(function(){
				deleteFileIdxs.push($(this).attr("data-attr"));
				$(this).parents(".attachDiv").remove();
			});
			
		}
	}
	
	
	//수정 버튼
	function updateSubmit(){
		
		var params = {
			 boardTitle : $.trim($("#boardTitle").val())
			,boardContent : $.trim($("#boardContent").val())
			,userId : "${sessUserInfo.userId}"
			,boardIdx : $("#boardIdx").val()
			,fileIdxs : fileIdxs
			,deleteFileIdxs : deleteFileIdxs.toString()
		}
		
		console.log(params);
		
		if(params.boardTitle == ""){
			alert("제목을 입력해주세요.");
			return false;
		}
		
		else if(params.boardContent == ""){
			alert("내용을 입력해주세요.");
			return false;
		} 
		
		$.ajax({
	         type : 'POST'
	        ,url : '/board/update'
	        ,dataType : 'json'
	        ,data : JSON.stringify(params) 
	        ,contentType: 'application/json'
	        ,success : function(result) {
				alert("해당글이 정상적으로 수정되었습니다.");
				location.href="/";
	        },
	        error: function(request, status, error) {
	          
	        }
	    }) 
	}

	//게시판 삭제하기
	function boardDelete(){
		
		var boardIdxArray = [];
		
		boardIdxArray.push("${boardIdx}");
		
		console.log(boardIdxArray);
		
		if(boardIdxArray == ""){
			alert("삭제할 항목을 선택해주세요.");
			return false;
		}
		
		var confirmAlert = confirm('정말로 삭제하시겠습니까?');

		if(confirmAlert){
			
			$.ajax({
		        type : 'POST'
		       ,url : '/board/delete'
		       ,dataType : 'json'
		       ,data : JSON.stringify(boardIdxArray)
		       ,contentType: 'application/json'
		       ,success : function(result) {
					alert("해당글이 정상적으로 삭제되었습니다.");
					location.href="/";
		       },
		       error: function(request, status, error) {
		         
		       }
		   })	
		}
	}
	
	//파일 추가 클릭
	function fileClick(){
		$("#input_file").click();
	}
	
	//파일 change 이벤트
	function fileChange(target){
		 
		var files = target.files;
		
		//파일 배열 담기
		var filesArr = Array.prototype.slice.call(files);
		
		if(fileCount + filesArr.length > totalCount){
			alert("파일은 최대 "+totalCount+"개까지 업로드 할 수 있습니다.");
			return false;
		}
		
		else{
			fileCount = fileCount + filesArr.length;
		}
		
		// 각각의 파일 배열담기 및 기타
		filesArr.forEach(function (f) {
			var reader = new FileReader();
			reader.onload = function (e) {
			content_files.push(f);
			console.log(content_files)
			$('#articlefileChange').append(
	       		'<div id="file' + fileNum + '" onclick="fileDelete(\'file' + fileNum + '\')">'
	       		+ '<font style="font-size:12px">' + f.name + '</font>'  
	       		+ '<img src="/images/icon_minus.png" style="width:20px; height:auto; vertical-align: middle; cursor: pointer;"/>' 
	       		+ '<div/>'
			);
	        fileNum++;
	      };
	      reader.readAsDataURL(f);
	    });
		
	    //초기화 한다.
	    $("#input_file").val(""); 	
	    
	}
	
	//파일 저장
	function registerAction(){
		console.log(content_files);
		var form = $("form")[0];        
	 	var formData = new FormData(form);
		for (var x = 0; x < content_files.length; x++) {
			// 삭제 안한것만 담아 준다. 
			if(!content_files[x].is_delete){
				 formData.append("article_file", content_files[x]);
				 formData.append("filePath", "/main");
			}
		}
		
	   /*
	   * 파일업로드 multiple ajax처리
	   */    
		$.ajax({
	   	      type: "POST",
	   	   	  enctype: "multipart/form-data",
	   	      url: "/file-upload",
	       	  data : formData,
	       	  processData: false,
	   	      contentType: false,
	   	      success: function (data) {
	   	    	
	   	    	console.log(data)

	   	    	//파일 시퀀스들
	   	    	fileIdxs = data.fileIdxs;
	   	    	if(data.result == "OK"){
	   	    		updateSubmit();
				} else
					alert("서버내 오류로 처리가 지연되고있습니다. 잠시 후 다시 시도해주세요");
	   	      },
	   	      error: function (xhr, status, error) {
	   	    	alert("서버오류로 지연되고있습니다. 잠시 후 다시 시도해주시기 바랍니다.");
			}
		});
	}
	
	//파일 삭제
	function fileDelete(fileNum){
		var no = fileNum.replace(/[^0-9]/g, "");
	    content_files[no].is_delete = true;
		$('#' + fileNum).remove();
		fileCount --;
	}
	
	//이전에 등록한 파일 삭제 버튼 클릭시
	function deleteFileId(target, fileId){
		
		console.log(target);
		//배열에 삭제 시퀀스 넣기
		deleteFileIdxs.push(fileId);
		
	}
	
</script>

 

 

리스트 화면 (로그인하지 않았을경우)

-> 파일있는 항목은 파일유무에 이미지가 보입니다.

 

상세화면 (로그인하지 않았을경우)

-> 상세화면과 업로드한 파일을 다운로드 할수 있습니다.

 

글쓰기 화면

 

파일추가 클릭시

 

 

게시판 수정화면 (관리자 로그인일경우)

 

파일다운로드

복사했습니다!