관리 메뉴

거니의 velog

231206_SPRING 2 (11-1) 본문

대덕인재개발원/대덕인재개발원_웹기반 애플리케이션

231206_SPRING 2 (11-1)

Unlimited00 2023. 12. 6. 08:25

[noticeBoard_SQL.xml]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.or.ddit.mapper.INoticeMapper">
	
	<resultMap type="noticeVO" id="noticeMap">
		<id property="boNo" column="bo_no" />
		<result property="boNo" column="bo_no" />
		<result property="boTitle" column="bo_title" />
		<result property="boContent" column="bo_content" />
		<result property="boWriter" column="bo_writer" />
		<result property="boDate" column="bo_date" />
		<result property="boHit" column="bo_hit" />
		<collection property="noticeFileList" resultMap="noticefileMap"></collection>
	</resultMap>
	
	<resultMap type="noticefileVO" id="noticefileMap">
		<id property="fileNo" column="file_no" />
		<result property="fileNo" column="file_no" />
		<result property="fileName" column="file_name" />
		<result property="fileSize" column="file_size" />
		<result property="fileFancysize" column="file_fancysize" />
		<result property="fileMime" column="file_mime" />
		<result property="fileSavepath" column="file_savepath" />
		<result property="fileDowncount" column="file_downcount" />
	</resultMap>
	
	<sql id="noticeSearch">
		<if test="searchType != null and searchType == 'title'">
			and (bo_title like '%' || #{searchWord} || '%')
		</if>
		<if test="searchType != null and searchType == 'writer'">
			and (bo_writer like '%' || #{searchWord} || '%')
		</if>
	</sql>
	
	<select id="selectNoticeCount" parameterType="pagingVO" resultType="int">
		select count(bo_no) from notice
		where 1=1 
		<include refid="noticeSearch" />
	</select>
	
	<select id="selectNoticeList" parameterType="pagingVO" resultType="noticeVO">
		select
			b.*
		from (
			select
				a.*, row_number() over (order by a.bo_no desc) rnum
			from (
				select 
					bo_no, bo_title, bo_content, bo_writer, bo_date, bo_hit 
				from notice 
				where 1=1 
				<include refid="noticeSearch" />
				order by bo_no desc
			) a
		) b
		<![CDATA[
			where b.rnum >= #{startRow} and b.rnum <= #{endRow}
		]]>
	</select>
	
	<insert id="insertNotice" parameterType="noticeVO" useGeneratedKeys="true">
		<selectKey keyProperty="boNo" resultType="int" order="BEFORE">
			select seq_notice.nextval from dual
		</selectKey>
		insert into notice (
			bo_no, bo_title, bo_content, bo_writer, bo_date
		) values (
			#{boNo}, #{boTitle}, #{boContent}, #{boWriter}, sysdate
		)
	</insert>
	
	<insert id="insertNoticeFile" parameterType="noticefileVO">
		insert into noticefile (
			file_no,
			bo_no,
			file_name,
			file_size,
			file_fancysize,
			file_mime,
			file_savepath,
			file_downcount
		) values (
			seq_noticefile.nextval,
			#{boNo},
			#{fileName},
			#{fileSize},
			#{fileFancysize},
			#{fileMime},
			#{fileSavepath},
			0
		)
	</insert>
	
	<update id="incrementHit" parameterType="int">
		update notice 
		set 
			bo_hit = bo_hit + 1 
		where bo_no = #{boNo}
	</update>
	
	<!-- <select id="selectNotice" parameterType="int" resultType="noticeVO">
		select 
			bo_no, bo_title, bo_content, bo_writer, bo_date, bo_hit 
		from notice 
		where bo_no = #{boNo}
	</select> -->
	<select id="selectNotice" parameterType="int" resultMap="noticeMap">
		select 
			n.bo_no, bo_title, bo_content, bo_writer, bo_date, bo_hit, 
			file_no, file_name, file_size, file_fancysize, file_mime, file_savepath, file_downcount 
		from notice n left outer join noticefile nf on(n.bo_no = nf.bo_no) 
		where n.bo_no = #{boNo}
	</select>
	
	<update id="updateNotice" parameterType="noticeVO">
		update notice 
		set 
			bo_title = #{boTitle},
			bo_content = #{boContent} 
		where bo_no = #{boNo}
	</update>
	
	<delete id="deleteNotice" parameterType="int">
		delete from notice 
		where bo_no = #{boNo}
	</delete>
	
</mapper>

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<section class="content-header">
    <div class="container-fluid">
        <div class="row mb-2">
            <div class="col-sm-6">
                <h1>공지사항 상세보기</h1>
            </div>
            <div class="col-sm-6">
                <ol class="breadcrumb float-sm-right">
                    <li class="breadcrumb-item"><a href="#">DDIT HOME</a></li>
                    <li class="breadcrumb-item active">공지사항 상세보기</li>
                </ol>
            </div>
        </div>
    </div>
</section>

<section class="content">
    <div class="container-fluid">
        <div class="row">
            <div class="col-md-12">
                <div class="card card-dark">
                    <div class="card-header">
                        <h3 class="card-title">${notice.boTitle }</h3>
                        <div class="card-tools">
                         	${notice.boWriter } ${notice.boDate } ${notice.boHit }
                        </div>
                    </div>
                    <form id="quickForm" novalidate="novalidate">
                        <div class="card-body">${notice.boContent }</div>
                        <div class="card-footer bg-white">
                            <ul class="mailbox-attachments d-flex align-items-stretch clearfix">
                            	<c:if test="${not empty notice.noticeFileList }">
                            		<c:forEach items="${notice.noticeFileList }" var="noticeFile">
		                                <li>
		                                    <span class="mailbox-attachment-icon">
		                                        <i class="far fa-file-pdf"></i>
		                                    </span>
		                                    <div class="mailbox-attachment-info">
		                                        <a href="#" class="mailbox-attachment-name">
		                                            <i class="fas fa-paperclip"></i>
		                                           	 ${noticeFile.fileName }
		                                        </a>
		                                        <span class="mailbox-attachment-size clearfix mt-1">
		                                            <span>${noticeFile.fileFancysize }</span>
		                                            <c:url value="/notice/download.do" var="downloadURL">
		                                            	<c:param name="fileNo" value="${noticeFile.fileNo }"></c:param>
		                                            </c:url>
		                                            <a href="${downloadURL }">
		                                                <span class="btn btn-default btn-sm float-right">
		                                                    <i class="fas fa-download"></i>
		                                                </span>
		                                            </a>
		                                        </span>
		                                    </div>
		                                </li>
                            		</c:forEach>
                            	</c:if>
                            </ul>
                        </div>
                        <div class="card-footer">
                            <button type="button" id="listBtn" class="btn btn-secondary">목록</button>
                            <button type="button" id="updateBtn" class="btn btn-dark">수정</button>
                            <button type="button" id="deleteBtn" class="btn btn-danger">삭제</button>
                        </div>
                    </form>
                    <form action="/notice/delete.do" method="post" id="delForm">
                    	<input type="hidden" name="boNo" value="${notice.boNo }" />
                    </form>
                </div>
            </div>
            <div class="col-md-6"></div>
        </div>
    </div>
</section>
<script>
	$(function(){
		var delForm = $("#delForm"); 		// 수정 및 삭제를 처리할 Form Element
		var listBtn = $("#listBtn");		// 목록 버튼 Element
		var updateBtn = $("#updateBtn");	// 수정 버튼 Element
		var deleteBtn = $("#deleteBtn");	// 삭제 버튼 Element
		
		// 목록 버튼 클릭 시, 목록 페이지로 이동
		listBtn.on("click", function(){
			location.href = "/notice/list.do";
		});
		
		// 수정 버튼 클릭 시, 수정 페이지 이동
		updateBtn.on("click", function(){
			delForm.attr("action", "/notice/update.do");
			delForm.attr("method", "get");
			delForm.submit();
		});
		
		// 삭제 버튼 클릭 시, confirm 메시지 출력 후 삭제
		deleteBtn.on("click", function(){
			if(confirm("정말로 삭제하시겠습니까?")) {
				delForm.submit();
			}
		});
	});
</script>

- http://localhost/notice/detail.do?boNo=82


package kr.or.ddit.controller.crud.notice;

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

import javax.inject.Inject;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.View;

import kr.or.ddit.service.INoticeService;
import kr.or.ddit.vo.crud.NoticeFileVO;

@Controller
public class NoticeDownloadController {

	@Inject
	private INoticeService noticeService;
	
	@RequestMapping(value = "/notice/download.do")
	public View noticeProcess(
			int fileNo,
			ModelMap model
			) {
		
		// 선택한 파일을 다운로드 하기 위한 정보를 파일번호에 해당하는 파일 정보로 얻어온다.
		NoticeFileVO noticeFileVO = noticeService.noticeDownload(fileNo);
		
		// 데이터 전달자를 통해서 파일정보를 전달하기 위한 Map 선언
		Map<String, Object> noticeFileMap = new HashMap<String, Object>();
		noticeFileMap.put("fileName", noticeFileVO.getFileName());
		noticeFileMap.put("fileSize", noticeFileVO.getFileSize());
		noticeFileMap.put("fileSavepath", noticeFileVO.getFileSavepath());
		model.addAttribute("noticeFileMap", noticeFileMap);
		
		// 리턴되는 NoticeDownloadView는 jsp 페이지로 존재하는 페이지 Name을 요청하는게 아니라,
		// 클래스를 요청하는 것인데, 해당 클래스가 스프링에서 제공하는 AbstractView 클래스를 상속받은 클래스인데
		// 그 클래스는 AbstractView를 상속받아 renderMergedOutputModel 메서드를 재정의할 때 View로 취급될 수 있게 해준다.
		return new NoticeDownloadView();
		
	}
	
}
package kr.or.ddit.service;

import java.util.List;

import javax.servlet.http.HttpServletRequest;

import kr.or.ddit.ServiceResult;
import kr.or.ddit.vo.crud.NoticeFileVO;
import kr.or.ddit.vo.crud.NoticeMemberVO;
import kr.or.ddit.vo.crud.NoticeVO;
import kr.or.ddit.vo.crud.PaginationInfoVO;

public interface INoticeService {

	public int selectNoticeCount(PaginationInfoVO<NoticeVO> pagingVO);
	public List<NoticeVO> selectNoticeList(PaginationInfoVO<NoticeVO> pagingVO);
	public ServiceResult insertNotice(HttpServletRequest req, NoticeVO noticeVO);
	public NoticeVO selectNotice(int boNo);
	public ServiceResult updateNotice(NoticeVO noticeVO);
	public ServiceResult deleteNotice(int boNo);
	
	public NoticeMemberVO loginCheck(NoticeMemberVO member);
	public ServiceResult idCheck(String memId);
	public ServiceResult signup(HttpServletRequest req, NoticeMemberVO memberVO);
	
	public NoticeFileVO noticeDownload(int fileNo);

}
package kr.or.ddit.service.impl;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import kr.or.ddit.ServiceResult;
import kr.or.ddit.mapper.ILoginMapper;
import kr.or.ddit.mapper.INoticeMapper;
import kr.or.ddit.service.INoticeService;
import kr.or.ddit.vo.crud.NoticeFileVO;
import kr.or.ddit.vo.crud.NoticeMemberVO;
import kr.or.ddit.vo.crud.NoticeVO;
import kr.or.ddit.vo.crud.PaginationInfoVO;

@Service
public class NoticeServiceImpl implements INoticeService {

	@Inject
	private INoticeMapper noticeMapper;
	@Inject
	private ILoginMapper loginMapper;

	@Override
	public int selectNoticeCount(PaginationInfoVO<NoticeVO> pagingVO) {
		return noticeMapper.selectNoticeCount(pagingVO);
	}

	@Override
	public List<NoticeVO> selectNoticeList(PaginationInfoVO<NoticeVO> pagingVO) {
		return noticeMapper.selectNoticeList(pagingVO);
	}

	@Override
	public ServiceResult insertNotice(HttpServletRequest req, NoticeVO noticeVO) {
		ServiceResult result = null;

		int status = noticeMapper.insertNotice(noticeVO);
		if (status > 0) {	// 게시글 등록이 성공했을 때
			List<NoticeFileVO> noticeFileList = noticeVO.getNoticeFileList();
			try {
				// 공지사항 파일 업로드 처리
				noticeFileUpload(noticeFileList, noticeVO.getBoNo(), req);
			}catch(IOException e) {
				e.printStackTrace();
			}
			
			result = ServiceResult.OK;
		} else {
			result = ServiceResult.FAILED;
		}

		return result;
	}

	@Override
	public NoticeVO selectNotice(int boNo) {
		noticeMapper.incrementHit(boNo); // 게시글 조회수 증가
		return noticeMapper.selectNotice(boNo); // 게시글 번호에 해당하는 게시글 정보 가져오기
	}

	@Override
	public ServiceResult updateNotice(NoticeVO noticeVO) {
		ServiceResult result = null;

		int status = noticeMapper.updateNotice(noticeVO);
		if (status > 0) {
			result = ServiceResult.OK;
		} else {
			result = ServiceResult.FAILED;
		}

		return result;
	}

	@Override
	public ServiceResult deleteNotice(int boNo) {
		ServiceResult result = null;

		int status = noticeMapper.deleteNotice(boNo);
		if (status > 0) {
			result = ServiceResult.OK;
		} else {
			result = ServiceResult.FAILED;
		}

		return result;
	}

	@Override
	public NoticeMemberVO loginCheck(NoticeMemberVO member) {
		return loginMapper.loginCheck(member);
	}

	@Override
	public ServiceResult idCheck(String memId) {
		ServiceResult result = null;

		NoticeMemberVO member = loginMapper.idCheck(memId);
		if (member != null) {
			result = ServiceResult.EXIST;
		} else {
			result = ServiceResult.NOTEXIST;
		}

		return result;
	}

	@Override
	public ServiceResult signup(HttpServletRequest req, NoticeMemberVO memberVO) {
		ServiceResult result = null;

		// 회원가입 시, 프로필 이미지로 파일을 업로드 하는데 이때 업로드 할 서버 경로
		String uploadPath = req.getServletContext().getRealPath("/resources/profile");
		File file = new File(uploadPath);
		if (!file.exists()) {
			file.mkdirs();
		}

		String proFileImg = ""; // 회원정보에 추가될 프로필 이미지 경로
		try {
			// 넘겨받은 회원정보에서 파일 데이터 가져오기
			MultipartFile proFileImgFile = memberVO.getImgFile();

			// 넘겨받은 파일 데이터가 존재할 때
			if (proFileImgFile.getOriginalFilename() != null && !proFileImgFile.getOriginalFilename().equals("")) {
				String fileName = UUID.randomUUID().toString(); // UUID 파일명 생성
				fileName += "_" + proFileImgFile.getOriginalFilename(); // UUID_원본파일명으로 파일명 생성
				uploadPath += "/" + fileName; // /resources/profile/uuid_원본파일명

				proFileImgFile.transferTo(new File(uploadPath)); // 해당 위치에 파일 복사
				proFileImg = "/resources/profile/" + fileName; // 파일 복사가 일어난 파일의 위치로 접근하기 위한 URI 설정
			}

			memberVO.setMemProfileImg(proFileImg);
		} catch (Exception e) {
			e.printStackTrace();
		}

		int status = loginMapper.signup(memberVO);

		if (status > 0) { // 등록 성공
			result = ServiceResult.OK;
		} else {
			result = ServiceResult.FAILED;
		}

		return result;
	}
	
	private void noticeFileUpload(
			List<NoticeFileVO> noticeFileList, 
			int boNo, 
			HttpServletRequest req
			) throws IOException {
		
		String savePath = "/resources/notice/";
		
		if(noticeFileList != null) { // 넘겨받은 파일 데이터가 존재할 때
			if(noticeFileList.size() > 0) {
				for(NoticeFileVO noticeFileVO : noticeFileList) {
					String saveName = UUID.randomUUID().toString(); // UUID의 랜덤 파일명 생성
					
					// 파일명을 설장할 때, 원본 파일명의 공백을 '_'로 변경한다.
					saveName = saveName + "_" + noticeFileVO.getFileName().replaceAll(" ", "_");
					// 디버깅 및 확장자 추출 참고
					String endFileName = noticeFileVO.getFileName().split("\\.")[1];
					
					String saveLocate = req.getServletContext().getRealPath(savePath + boNo);
					File file = new File(saveLocate);
					if(!file.exists()) {
						file.mkdirs();
					}
					saveLocate += "/" + saveName;
					
					noticeFileVO.setBoNo(boNo);						// 게시글 번호 설정
					noticeFileVO.setFileSavepath(saveLocate); 		// 파일 업로드 경로 설정
					noticeMapper.insertNoticeFile(noticeFileVO);	// 게시글 파일 데이터 추가
					
					File saveFile = new File(saveLocate);
					
					// 방법 1
//					InputStream is = noticeFileVO.getItem().getInputStream();
//					FileUtils.copyInputStreamToFile(is, saveFile);
					
					// 방법 2
					noticeFileVO.getItem().transferTo(saveFile); // 파일 복사
				}
			}
		}
		
	}

	@Override
	public NoticeFileVO noticeDownload(int fileNo) {
		NoticeFileVO noticeFileVO = noticeMapper.noticeDownload(fileNo);
		if(noticeFileVO == null) {
			throw new RuntimeException();
		}
		noticeMapper.incrementNoticeDowncount(fileNo); // 다운로드 횟수 증가
		return noticeFileVO;
	}

}
package kr.or.ddit.mapper;

import java.util.List;

import kr.or.ddit.vo.crud.NoticeFileVO;
import kr.or.ddit.vo.crud.NoticeVO;
import kr.or.ddit.vo.crud.PaginationInfoVO;

public interface INoticeMapper {

	public int selectNoticeCount(PaginationInfoVO<NoticeVO> pagingVO);
	public List<NoticeVO> selectNoticeList(PaginationInfoVO<NoticeVO> pagingVO);
	public int insertNotice(NoticeVO noticeVO);
	public void incrementHit(int boNo);
	public NoticeVO selectNotice(int boNo);
	public int updateNotice(NoticeVO noticeVO);
	public int deleteNotice(int boNo);
	public void insertNoticeFile(NoticeFileVO noticeFileVO);
	public NoticeFileVO noticeDownload(int fileNo);
	public void incrementNoticeDowncount(int fileNo);

}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.or.ddit.mapper.INoticeMapper">
	
	<resultMap type="noticeVO" id="noticeMap">
		<id property="boNo" column="bo_no" />
		<result property="boNo" column="bo_no" />
		<result property="boTitle" column="bo_title" />
		<result property="boContent" column="bo_content" />
		<result property="boWriter" column="bo_writer" />
		<result property="boDate" column="bo_date" />
		<result property="boHit" column="bo_hit" />
		<collection property="noticeFileList" resultMap="noticefileMap"></collection>
	</resultMap>
	
	<resultMap type="noticefileVO" id="noticefileMap">
		<id property="fileNo" column="file_no" />
		<result property="fileNo" column="file_no" />
		<result property="fileName" column="file_name" />
		<result property="fileSize" column="file_size" />
		<result property="fileFancysize" column="file_fancysize" />
		<result property="fileMime" column="file_mime" />
		<result property="fileSavepath" column="file_savepath" />
		<result property="fileDowncount" column="file_downcount" />
	</resultMap>
	
	<sql id="noticeSearch">
		<if test="searchType != null and searchType == 'title'">
			and (bo_title like '%' || #{searchWord} || '%')
		</if>
		<if test="searchType != null and searchType == 'writer'">
			and (bo_writer like '%' || #{searchWord} || '%')
		</if>
	</sql>
	
	<select id="selectNoticeCount" parameterType="pagingVO" resultType="int">
		select count(bo_no) from notice
		where 1=1 
		<include refid="noticeSearch" />
	</select>
	
	<select id="selectNoticeList" parameterType="pagingVO" resultType="noticeVO">
		select
			b.*
		from (
			select
				a.*, row_number() over (order by a.bo_no desc) rnum
			from (
				select 
					bo_no, bo_title, bo_content, bo_writer, bo_date, bo_hit 
				from notice 
				where 1=1 
				<include refid="noticeSearch" />
				order by bo_no desc
			) a
		) b
		<![CDATA[
			where b.rnum >= #{startRow} and b.rnum <= #{endRow}
		]]>
	</select>
	
	<insert id="insertNotice" parameterType="noticeVO" useGeneratedKeys="true">
		<selectKey keyProperty="boNo" resultType="int" order="BEFORE">
			select seq_notice.nextval from dual
		</selectKey>
		insert into notice (
			bo_no, bo_title, bo_content, bo_writer, bo_date
		) values (
			#{boNo}, #{boTitle}, #{boContent}, #{boWriter}, sysdate
		)
	</insert>
	
	<insert id="insertNoticeFile" parameterType="noticefileVO">
		insert into noticefile (
			file_no,
			bo_no,
			file_name,
			file_size,
			file_fancysize,
			file_mime,
			file_savepath,
			file_downcount
		) values (
			seq_noticefile.nextval,
			#{boNo},
			#{fileName},
			#{fileSize},
			#{fileFancysize},
			#{fileMime},
			#{fileSavepath},
			0
		)
	</insert>
	
	<update id="incrementHit" parameterType="int">
		update notice 
		set 
			bo_hit = bo_hit + 1 
		where bo_no = #{boNo}
	</update>
	
	<!-- <select id="selectNotice" parameterType="int" resultType="noticeVO">
		select 
			bo_no, bo_title, bo_content, bo_writer, bo_date, bo_hit 
		from notice 
		where bo_no = #{boNo}
	</select> -->
	<select id="selectNotice" parameterType="int" resultMap="noticeMap">
		select 
			n.bo_no, bo_title, bo_content, bo_writer, bo_date, bo_hit, 
			file_no, file_name, file_size, file_fancysize, file_mime, file_savepath, file_downcount 
		from notice n left outer join noticefile nf on(n.bo_no = nf.bo_no) 
		where n.bo_no = #{boNo}
	</select>
	
	<update id="updateNotice" parameterType="noticeVO">
		update notice 
		set 
			bo_title = #{boTitle},
			bo_content = #{boContent} 
		where bo_no = #{boNo}
	</update>
	
	<delete id="deleteNotice" parameterType="int">
		delete from notice 
		where bo_no = #{boNo}
	</delete>
	
	<select id="noticeDownload" parameterType="int" resultType="noticefileVO">
		select 
			file_no, file_name, file_size, file_fancysize, file_mime, file_savepath, file_downcount 
		from noticefile 
		where file_no = #{fileNo}
	</select>
	
	<update id="incrementNoticeDowncount" parameterType="int">
		update noticefile 
		set 
			file_downcount = file_downcount + 1 
		where file_no = #{fileNo}
	</update>
	
</mapper>

package kr.or.ddit.controller.crud.notice;

import java.io.File;
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.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.view.AbstractView;

// AbstractView 클래스를 상속받아 renderMergedOutputModel 메소드를 재정의하여 사용한다.
// 해당 클래스가 View의 역할을 하는 페이지의 형태가 된다.
public class NoticeDownloadView extends AbstractView {

	@Override
	protected void renderMergedOutputModel(
			Map<String, Object> model, 
			HttpServletRequest request,
			HttpServletResponse response
			) throws Exception {
		
		Map<String, Object> noticeFileMap = (Map<String, Object>) model.get("noticeFileMap");
		
		File saveFile = new File(noticeFileMap.get("fileSavepath").toString());
		String fileName = noticeFileMap.get("fileName").toString();
		String fileSize = noticeFileMap.get("fileSize").toString();
		
		// 요청 Header 정보들 중, User-Agent 영역 안에 여러 키워드 정보들을 가지고
		// 특정 키워드가 포함되어 있는지를 체크해서 파일명의 출력 인코딩 부분을 설정한다.
		// 사용 브라우저 또는 현상에 따라 발생하는 알고리즘 이므로, 내가 사용하게 되는 브라우저의 버전이나 얻어온 Header 정보들의 값에 따라
		// 차이가 발생할 수 있다.
		String agent = request.getHeader("User-Agent");
		if(StringUtils.containsIgnoreCase(agent, "msie") || StringUtils.containsIgnoreCase(agent, "trident")) {
			fileName = URLEncoder.encode(fileName, "UTF-8"); // IE, Chrome
		}else { // firefox, chrome
			fileName = new String(fileName.getBytes(), "ISO-8859-1");
		}
		
		response.setHeader("Content-Disposition", "attachment; fileName=\"" + fileName + "\"");
		response.setHeader("Content-Length", fileSize);
		
		// try with resource
		// () 안에 명시한 객체는 finally로 최종 열린 객체에 대한 close를 처리하지 않아도 자동 close()가 이루어진다.
		try (
			OutputStream os = response.getOutputStream();
		){
			FileUtils.copyFile(saveFile, os); // 파일을 다운로드
		}
		
	}
	
}

- http://localhost/notice/detail.do?boNo=82


<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<section class="content-header">

	<c:set value="등록" var="name" />
	<c:if test="${status eq 'u' }">
		<c:set value="수정" var="name" />
	</c:if>
	
    <div class="container-fluid">
        <div class="row mb-2">
            <div class="col-sm-6">
                <h1>공지사항 등록/수정</h1>
            </div>
            <div class="col-sm-6">
                <ol class="breadcrumb float-sm-right">
                    <li class="breadcrumb-item"><a href="#">DDIT HOME</a></li>
                    <li class="breadcrumb-item active">공지사항 등록/수정</li>
                </ol>
            </div>
        </div>
    </div>
</section>

<section class="content">
    <div class="row">
        <div class="col-md-12">
            <div class="card card-dark">
                <div class="card-header">
                    <h3 class="card-title">공지사항 ${name }</h3>
                    <div class="card-tools"></div>
                </div>
                <form action="/notice/insert.do" method="post" id="noticeForm" name="noticeForm" enctype="multipart/form-data">
                
                	<c:if test="${status eq 'u' }">
                		<input type="hidden" name="boNo" value="${notice.boNo }" />
                	</c:if>
                	
	                <div class="card-body">
	                    <div class="form-group">
	                        <label for="boTitle">제목을 입력해주세요</label>
	                        <input type="text" id="boTitle" name="boTitle" class="form-control" placeholder="제목을 입력해주세요" value="${notice.boTitle }">
	                    </div>
	                    <div class="form-group">
	                        <label for="boContent">내용을 입력해주세요</label>
	                        <textarea id="boContent" name="boContent" class="form-control" rows="14">${notice.boContent }</textarea>
	                    </div>
	                    <div class="form-group">
	                        <div class="custom-file">
	                            <input type="file" class="custom-file-input" id="boFile" name="boFile" multiple="multiple">
	                            <label class="custom-file-label" for="boFile">파일을 선택해주세요</label>
	                        </div>
	                    </div>
	                </div>
	                <c:if test="${status eq 'u' }">
		                <div class="card-footer bg-white">
		                    <ul class="mailbox-attachments d-flex align-items-stretch clearfix">
		                    	<c:if test="${not empty notice.noticeFileList }">
		                    		<c:forEach items="${notice.noticeFileList }" var="noticeFile">
				                        <li>
				                            <span class="mailbox-attachment-icon">
				                            	<i class="far fa-file-pdf"></i>
				                            </span>
				                            <div class="mailbox-attachment-info">
				                                <a href="#" class="mailbox-attachment-name">
				                                	<i class="fas fa-paperclip"></i> 
				                                	${noticeFile.fileName }
				                                </a>
				                                <span class="mailbox-attachment-size clearfix mt-1">
				                                    <span>${noticeFile.fileFancysize }</span>
				                                    <span class="btn btn-default btn-sm float-right attachmentFileDel" id="span_${noticeFile.fileNo }">
				                                    	<i class="fas fa-times"></i>
				                                    </span>
				                                </span>
				                            </div>
				                        </li>
		                    		</c:forEach>
		                    	</c:if>
		                    </ul>
		                </div>
	                </c:if>
	                <div class="card-footer bg-white">
	                    <div class="row">
	                        <div class="col-12">
	                            <input type="button" id="insertBtn" value="${name }" class="btn btn-secondary float-right">
	                            <c:if test="${status ne 'u' }">
		                            <input type="button" id="listBtn" value="목록" class="btn btn-dark float-right">
	                            </c:if>
	                            <c:if test="${status eq 'u' }">
		                            <input type="button" id="cancelBtn" value="취소" class="btn btn-dark float-right">
	                            </c:if>
	                        </div>
	                    </div>
	                </div>
                </form>
            </div>
        </div>
    </div>
</section>
<script>
	$(function(){
		CKEDITOR.replace("boContent", {
			filebrowserUploadUrl: '/imageUpload.do'
		});
		CKEDITOR.config.height = "500px"; // CKEDITOR 높이 설정
		
		var noticeForm = $("#noticeForm");	// 등록 폼 Element
		var insertBtn = $("#insertBtn");	// 등록 버튼 Element
		var listBtn = $("#listBtn");		// 목록 버튼 Element
		var cancelBtn = $("#cancelBtn");	// 취소 버튼 Element
		
		// 등록 버튼 클릭 시, 등록 진행
		insertBtn.on("click", function(){
			var title = $("#boTitle").val();
			
			// 일반적인 textarea일 때 가져오는 방법
			// var content = $("#boContent").val();
			
			// CKEDITOR를 이용한 내용 데이터 가져오는 방법
			var content = CKEDITOR.instances.boContent.getData();
			
			if(!title) {
				alert("제목을 입력해 주세요!");
				$("#boTitle").focus();
				return false;
			}
			
			if(!content) {
				alert("제목을 입력해 주세요!");
				$("#boContent").focus();
				return false;
			}
			
			if($(this).val() == "수정") {
				noticeForm.attr("action", "/notice/update.do");
			}
			
			noticeForm.submit();
		});
		
		// 목록 버튼 클릭 시, 게시판 목록 화면으로 이동
		listBtn.on("click", function(){
			location.href = "/notice/list.do";
		});
		
		// 취소 버튼 클릭 시, 상세보기 화면으로 이동
		cancelBtn.on("click", function(){
			location.href = "/notice/detail.do?boNo=${notice.boNo }";
		});
		
		// 삭제를 눌렀을 때의 아이콘
		$(".attachmentFileDel").on("click", function(){
			var id = $(this).prop("id");
			var idx = id.indexOf("_");
			var noticeFileNo = id.substring(idx + 1);
			var ptrn = "<input type='hidden' name='delNoticeNo' value='%V' />";
			$("#noticeForm").append(ptrn.replace("%V", noticeFileNo));
			$(this).parents("li:first").hide();
		});
	});
</script>


package kr.or.ddit.controller.crud.notice;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import kr.or.ddit.ServiceResult;
import kr.or.ddit.service.INoticeService;
import kr.or.ddit.vo.crud.NoticeVO;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
@RequestMapping("/notice")
public class NoticeModifyController {

	@Inject
	private INoticeService noticeService;
	
	@RequestMapping(value = "/update.do", method = RequestMethod.GET)
	public String noticeUpdateForm(int boNo, Model model) {
		NoticeVO noticeVO = noticeService.selectNotice(boNo);
		model.addAttribute("notice", noticeVO);
		model.addAttribute("status", "u"); // '수정입니다' flag
		return "notice/form";
	}
	
	@RequestMapping(value = "/update.do", method = RequestMethod.POST)
	public String noticeUpdate(
			HttpServletRequest req,
			NoticeVO noticeVO, 
			Model model, 
			RedirectAttributes ra
			) {
		String goPage = "";
		
		ServiceResult result = noticeService.updateNotice(req, noticeVO);
		if(result.equals(ServiceResult.OK)) { // 수정 성공
			goPage = "redirect:/notice/detail.do?boNo=" + noticeVO.getBoNo();
			ra.addFlashAttribute("message", "게시글 수정이 완료되었습니다!");
		}else { // 수정 실패
			model.addAttribute("notice", noticeVO);
			model.addAttribute("message", "수정에 실패했습니다!");
			model.addAttribute("status", "u");
			goPage = "notice/form";
		}
		
		return goPage;
	}
	
	@RequestMapping(value = "/delete.do", method = RequestMethod.POST)
	public String noticeDelete(int boNo, Model model, RedirectAttributes ra) {
		String goPage = "";
		
		ServiceResult result = noticeService.deleteNotice(boNo);
		if(result.equals(ServiceResult.OK)) { // 삭제 성공
			goPage = "redirect:/notice/list.do";
			ra.addFlashAttribute("message", "게시글 삭제가 완료되었습니다!");
		}else { // 삭제 실패
			goPage = "redirect:/notice/detail.do?boNo=" + boNo;
			ra.addFlashAttribute("message", "게시글 삭제에 실패했습니다!");
		}
		
		return goPage;
	}
	
}
package kr.or.ddit.service;

import java.util.List;

import javax.servlet.http.HttpServletRequest;

import kr.or.ddit.ServiceResult;
import kr.or.ddit.vo.crud.NoticeFileVO;
import kr.or.ddit.vo.crud.NoticeMemberVO;
import kr.or.ddit.vo.crud.NoticeVO;
import kr.or.ddit.vo.crud.PaginationInfoVO;

public interface INoticeService {

	public int selectNoticeCount(PaginationInfoVO<NoticeVO> pagingVO);
	public List<NoticeVO> selectNoticeList(PaginationInfoVO<NoticeVO> pagingVO);
	public ServiceResult insertNotice(HttpServletRequest req, NoticeVO noticeVO);
	public NoticeVO selectNotice(int boNo);
	public ServiceResult updateNotice(HttpServletRequest req, NoticeVO noticeVO);
	public ServiceResult deleteNotice(int boNo);
	
	public NoticeMemberVO loginCheck(NoticeMemberVO member);
	public ServiceResult idCheck(String memId);
	public ServiceResult signup(HttpServletRequest req, NoticeMemberVO memberVO);
	
	public NoticeFileVO noticeDownload(int fileNo);

}
	@Override
	public ServiceResult updateNotice(HttpServletRequest req, NoticeVO noticeVO) {
		ServiceResult result = null;

		int status = noticeMapper.updateNotice(noticeVO); // 게시글 수정
		if (status > 0) { // 게시글 수정 완료
			// 게시글 정보에서 파일 목록을 가져오기
			List<NoticeFileVO> noticeFileList = noticeVO.getNoticeFileList();
			try {
				// 공지사항 업로드 진행
				noticeFileUpload(noticeFileList, noticeVO.getBoNo(), req);
				
				// 기존에 등록되어 있는 파일 목록들 중, 수정하기 위해서 X버튼을 눌러 삭제 처리로 넘겨준 파일 번호들
				Integer[] delNoticeNo = noticeVO.getDelNoticeNo();
				if(delNoticeNo != null) {
					for(int i = 0; i < delNoticeNo.length; i++) {
						// 삭제할 파일 번호 목록들 중, 파일 번호에 해당하는 공지사항 파일 정보를 가져온다.
						NoticeFileVO noticeFileVO = noticeMapper.selectNoticeFile(delNoticeNo[i]);
						noticeMapper.deleteNoticeFile(delNoticeNo[i]); // 파일번호에 해당하는 파일 데이터를 삭제
						File file = new File(noticeFileVO.getFileSavepath());
						file.delete(); // 기존 파일이 업로드 되어 있던 경로에 파일 삭제
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			
			result = ServiceResult.OK;
		} else { // 게시글 수정 실패
			result = ServiceResult.FAILED;
		}

		return result;
	}
package kr.or.ddit.mapper;

import java.util.List;

import kr.or.ddit.vo.crud.NoticeFileVO;
import kr.or.ddit.vo.crud.NoticeVO;
import kr.or.ddit.vo.crud.PaginationInfoVO;

public interface INoticeMapper {

	public int selectNoticeCount(PaginationInfoVO<NoticeVO> pagingVO);
	public List<NoticeVO> selectNoticeList(PaginationInfoVO<NoticeVO> pagingVO);
	public int insertNotice(NoticeVO noticeVO);
	public void incrementHit(int boNo);
	public NoticeVO selectNotice(int boNo);
	public int updateNotice(NoticeVO noticeVO);
	public int deleteNotice(int boNo);
	public void insertNoticeFile(NoticeFileVO noticeFileVO);
	public NoticeFileVO noticeDownload(int fileNo);
	public void incrementNoticeDowncount(int fileNo);
	public NoticeFileVO selectNoticeFile(Integer integer);
	public void deleteNoticeFile(Integer integer);

}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.or.ddit.mapper.INoticeMapper">
	
	<resultMap type="noticeVO" id="noticeMap">
		<id property="boNo" column="bo_no" />
		<result property="boNo" column="bo_no" />
		<result property="boTitle" column="bo_title" />
		<result property="boContent" column="bo_content" />
		<result property="boWriter" column="bo_writer" />
		<result property="boDate" column="bo_date" />
		<result property="boHit" column="bo_hit" />
		<collection property="noticeFileList" resultMap="noticefileMap"></collection>
	</resultMap>
	
	<resultMap type="noticefileVO" id="noticefileMap">
		<id property="fileNo" column="file_no" />
		<result property="fileNo" column="file_no" />
		<result property="fileName" column="file_name" />
		<result property="fileSize" column="file_size" />
		<result property="fileFancysize" column="file_fancysize" />
		<result property="fileMime" column="file_mime" />
		<result property="fileSavepath" column="file_savepath" />
		<result property="fileDowncount" column="file_downcount" />
	</resultMap>
	
	<sql id="noticeSearch">
		<if test="searchType != null and searchType == 'title'">
			and (bo_title like '%' || #{searchWord} || '%')
		</if>
		<if test="searchType != null and searchType == 'writer'">
			and (bo_writer like '%' || #{searchWord} || '%')
		</if>
	</sql>
	
	<select id="selectNoticeCount" parameterType="pagingVO" resultType="int">
		select count(bo_no) from notice
		where 1=1 
		<include refid="noticeSearch" />
	</select>
	
	<select id="selectNoticeList" parameterType="pagingVO" resultType="noticeVO">
		select
			b.*
		from (
			select
				a.*, row_number() over (order by a.bo_no desc) rnum
			from (
				select 
					bo_no, bo_title, bo_content, bo_writer, bo_date, bo_hit 
				from notice 
				where 1=1 
				<include refid="noticeSearch" />
				order by bo_no desc
			) a
		) b
		<![CDATA[
			where b.rnum >= #{startRow} and b.rnum <= #{endRow}
		]]>
	</select>
	
	<insert id="insertNotice" parameterType="noticeVO" useGeneratedKeys="true">
		<selectKey keyProperty="boNo" resultType="int" order="BEFORE">
			select seq_notice.nextval from dual
		</selectKey>
		insert into notice (
			bo_no, bo_title, bo_content, bo_writer, bo_date
		) values (
			#{boNo}, #{boTitle}, #{boContent}, #{boWriter}, sysdate
		)
	</insert>
	
	<insert id="insertNoticeFile" parameterType="noticefileVO">
		insert into noticefile (
			file_no,
			bo_no,
			file_name,
			file_size,
			file_fancysize,
			file_mime,
			file_savepath,
			file_downcount
		) values (
			seq_noticefile.nextval,
			#{boNo},
			#{fileName},
			#{fileSize},
			#{fileFancysize},
			#{fileMime},
			#{fileSavepath},
			0
		)
	</insert>
	
	<update id="incrementHit" parameterType="int">
		update notice 
		set 
			bo_hit = bo_hit + 1 
		where bo_no = #{boNo}
	</update>
	
	<!-- <select id="selectNotice" parameterType="int" resultType="noticeVO">
		select 
			bo_no, bo_title, bo_content, bo_writer, bo_date, bo_hit 
		from notice 
		where bo_no = #{boNo}
	</select> -->
	<select id="selectNotice" parameterType="int" resultMap="noticeMap">
		select 
			n.bo_no, bo_title, bo_content, bo_writer, bo_date, bo_hit, 
			file_no, file_name, file_size, file_fancysize, file_mime, file_savepath, file_downcount 
		from notice n left outer join noticefile nf on(n.bo_no = nf.bo_no) 
		where n.bo_no = #{boNo}
	</select>
	
	<update id="updateNotice" parameterType="noticeVO">
		update notice 
		set 
			bo_title = #{boTitle},
			bo_content = #{boContent} 
		where bo_no = #{boNo}
	</update>
	
	<delete id="deleteNotice" parameterType="int">
		delete from notice 
		where bo_no = #{boNo}
	</delete>
	
	<select id="noticeDownload" parameterType="int" resultType="noticefileVO">
		select 
			file_no, file_name, file_size, file_fancysize, file_mime, file_savepath, file_downcount 
		from noticefile 
		where file_no = #{fileNo}
	</select>
	
	<update id="incrementNoticeDowncount" parameterType="int">
		update noticefile 
		set 
			file_downcount = file_downcount + 1 
		where file_no = #{fileNo}
	</update>
	
	<select id="selectNoticeFile" parameterType="int" resultType="noticefileVO">
		select 
			file_no, file_name, file_size, file_fancysize, file_mime, file_savepath, file_downcount 
		from noticefile 
		where file_no = #{fileNo}
	</select>
	
	<delete id="deleteNoticeFile" parameterType="int">
		delete from noticefile 
		where file_no = #{fileNo}
	</delete>
	
</mapper>

- http://localhost/notice/update.do?boNo=82


package kr.or.ddit.controller.crud.notice;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import kr.or.ddit.ServiceResult;
import kr.or.ddit.service.INoticeService;
import kr.or.ddit.vo.crud.NoticeVO;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
@RequestMapping("/notice")
public class NoticeModifyController {

	@Inject
	private INoticeService noticeService;
	
	@RequestMapping(value = "/update.do", method = RequestMethod.GET)
	public String noticeUpdateForm(int boNo, Model model) {
		NoticeVO noticeVO = noticeService.selectNotice(boNo);
		model.addAttribute("notice", noticeVO);
		model.addAttribute("status", "u"); // '수정입니다' flag
		return "notice/form";
	}
	
	@RequestMapping(value = "/update.do", method = RequestMethod.POST)
	public String noticeUpdate(
			HttpServletRequest req,
			NoticeVO noticeVO, 
			Model model, 
			RedirectAttributes ra
			) {
		String goPage = "";
		
		ServiceResult result = noticeService.updateNotice(req, noticeVO);
		if(result.equals(ServiceResult.OK)) { // 수정 성공
			goPage = "redirect:/notice/detail.do?boNo=" + noticeVO.getBoNo();
			ra.addFlashAttribute("message", "게시글 수정이 완료되었습니다!");
		}else { // 수정 실패
			model.addAttribute("notice", noticeVO);
			model.addAttribute("message", "수정에 실패했습니다!");
			model.addAttribute("status", "u");
			goPage = "notice/form";
		}
		
		return goPage;
	}
	
	@RequestMapping(value = "/delete.do", method = RequestMethod.POST)
	public String noticeDelete(
			HttpServletRequest req,
			int boNo, 
			Model model, 
			RedirectAttributes ra
			) {
		String goPage = "";
		
		ServiceResult result = noticeService.deleteNotice(req, boNo);
		if(result.equals(ServiceResult.OK)) { // 삭제 성공
			goPage = "redirect:/notice/list.do";
			ra.addFlashAttribute("message", "게시글 삭제가 완료되었습니다!");
		}else { // 삭제 실패
			goPage = "redirect:/notice/detail.do?boNo=" + boNo;
			ra.addFlashAttribute("message", "게시글 삭제에 실패했습니다!");
		}
		
		return goPage;
	}
	
}
package kr.or.ddit.service;

import java.util.List;

import javax.servlet.http.HttpServletRequest;

import kr.or.ddit.ServiceResult;
import kr.or.ddit.vo.crud.NoticeFileVO;
import kr.or.ddit.vo.crud.NoticeMemberVO;
import kr.or.ddit.vo.crud.NoticeVO;
import kr.or.ddit.vo.crud.PaginationInfoVO;

public interface INoticeService {

	public int selectNoticeCount(PaginationInfoVO<NoticeVO> pagingVO);
	public List<NoticeVO> selectNoticeList(PaginationInfoVO<NoticeVO> pagingVO);
	public ServiceResult insertNotice(HttpServletRequest req, NoticeVO noticeVO);
	public NoticeVO selectNotice(int boNo);
	public ServiceResult updateNotice(HttpServletRequest req, NoticeVO noticeVO);
	public ServiceResult deleteNotice(HttpServletRequest req, int boNo);
	
	public NoticeMemberVO loginCheck(NoticeMemberVO member);
	public ServiceResult idCheck(String memId);
	public ServiceResult signup(HttpServletRequest req, NoticeMemberVO memberVO);
	
	public NoticeFileVO noticeDownload(int fileNo);

}
	@Override
	public ServiceResult deleteNotice(HttpServletRequest req, int boNo) {
		ServiceResult result = null;

		// 공지사항 파일 데이터를 삭제하기 위한 준비(파일 적용시)
		NoticeVO noticeVO = noticeMapper.selectNotice(boNo); // 게시글 번호에 해당하는 공지사항 게시글 정보 가져오기
		noticeMapper.deleteNoticeFileByBoNo(boNo); // 게시글 번호에 해당하는 파일 데이터 삭제
		
		int status = noticeMapper.deleteNotice(boNo); // 일반적인 게시글 삭제
		if (status > 0) {
			List<NoticeFileVO> noticeFileList = noticeVO.getNoticeFileList();
			if(noticeFileList != null && noticeFileList.size() > 0) {
				// D:\99_Class...\workspace\.metadate\.plugins.....\Project명\resources\notice\boNo
				// 12d3uiy21euy1e2d.jpg
				// '/' 기준으로 잘라준다.
				String[] filePath = noticeFileList.get(0).getFileSavepath().split("/");
				
				String path = filePath[0];
				deleteFolder(req, path);
			}
			
			result = ServiceResult.OK;
		} else {
			result = ServiceResult.FAILED;
		}

		return result;
	}

	private void deleteFolder(HttpServletRequest req, String path) {
		// UUID + 원본파일명 전 폴더 경로를 folder 파일 객체로 잡는다.
		File folder = new File(path);
		
		if(folder.exists()) { // 경로가 존재한다면
			File[] folderList = folder.listFiles(); // 폴더 안에 있는 파일들의 목록을 가져온다.
			
			for(int i = 0; i < folderList.length; i++) {
				if(folderList[i].isFile()) { // 폴더 안에 있는 파일이 파일일 때
					// 폴더 안에 파일을 차례대로 삭제
					folderList[i].delete();
				}else {
					// 폴더 안에 있는 파일이 폴더일 때 재귀함수 호출(폴더 안으로 들어가서 재 처리할 수 있도록)
					deleteFolder(req, folderList[i].getPath());
				}
			}
			
			folder.delete(); // 폴더 삭제
		}
	}
package kr.or.ddit.mapper;

import java.util.List;

import kr.or.ddit.vo.crud.NoticeFileVO;
import kr.or.ddit.vo.crud.NoticeVO;
import kr.or.ddit.vo.crud.PaginationInfoVO;

public interface INoticeMapper {

	public int selectNoticeCount(PaginationInfoVO<NoticeVO> pagingVO);
	public List<NoticeVO> selectNoticeList(PaginationInfoVO<NoticeVO> pagingVO);
	public int insertNotice(NoticeVO noticeVO);
	public void incrementHit(int boNo);
	public NoticeVO selectNotice(int boNo);
	public int updateNotice(NoticeVO noticeVO);
	public int deleteNotice(int boNo);
	public void insertNoticeFile(NoticeFileVO noticeFileVO);
	public NoticeFileVO noticeDownload(int fileNo);
	public void incrementNoticeDowncount(int fileNo);
	public NoticeFileVO selectNoticeFile(Integer integer);
	public void deleteNoticeFile(Integer integer);
	public void deleteNoticeFileByBoNo(int boNo);

}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.or.ddit.mapper.INoticeMapper">
	
	<resultMap type="noticeVO" id="noticeMap">
		<id property="boNo" column="bo_no" />
		<result property="boNo" column="bo_no" />
		<result property="boTitle" column="bo_title" />
		<result property="boContent" column="bo_content" />
		<result property="boWriter" column="bo_writer" />
		<result property="boDate" column="bo_date" />
		<result property="boHit" column="bo_hit" />
		<collection property="noticeFileList" resultMap="noticefileMap"></collection>
	</resultMap>
	
	<resultMap type="noticefileVO" id="noticefileMap">
		<id property="fileNo" column="file_no" />
		<result property="fileNo" column="file_no" />
		<result property="fileName" column="file_name" />
		<result property="fileSize" column="file_size" />
		<result property="fileFancysize" column="file_fancysize" />
		<result property="fileMime" column="file_mime" />
		<result property="fileSavepath" column="file_savepath" />
		<result property="fileDowncount" column="file_downcount" />
	</resultMap>
	
	<sql id="noticeSearch">
		<if test="searchType != null and searchType == 'title'">
			and (bo_title like '%' || #{searchWord} || '%')
		</if>
		<if test="searchType != null and searchType == 'writer'">
			and (bo_writer like '%' || #{searchWord} || '%')
		</if>
	</sql>
	
	<select id="selectNoticeCount" parameterType="pagingVO" resultType="int">
		select count(bo_no) from notice
		where 1=1 
		<include refid="noticeSearch" />
	</select>
	
	<select id="selectNoticeList" parameterType="pagingVO" resultType="noticeVO">
		select
			b.*
		from (
			select
				a.*, row_number() over (order by a.bo_no desc) rnum
			from (
				select 
					bo_no, bo_title, bo_content, bo_writer, bo_date, bo_hit 
				from notice 
				where 1=1 
				<include refid="noticeSearch" />
				order by bo_no desc
			) a
		) b
		<![CDATA[
			where b.rnum >= #{startRow} and b.rnum <= #{endRow}
		]]>
	</select>
	
	<insert id="insertNotice" parameterType="noticeVO" useGeneratedKeys="true">
		<selectKey keyProperty="boNo" resultType="int" order="BEFORE">
			select seq_notice.nextval from dual
		</selectKey>
		insert into notice (
			bo_no, bo_title, bo_content, bo_writer, bo_date
		) values (
			#{boNo}, #{boTitle}, #{boContent}, #{boWriter}, sysdate
		)
	</insert>
	
	<insert id="insertNoticeFile" parameterType="noticefileVO">
		insert into noticefile (
			file_no,
			bo_no,
			file_name,
			file_size,
			file_fancysize,
			file_mime,
			file_savepath,
			file_downcount
		) values (
			seq_noticefile.nextval,
			#{boNo},
			#{fileName},
			#{fileSize},
			#{fileFancysize},
			#{fileMime},
			#{fileSavepath},
			0
		)
	</insert>
	
	<update id="incrementHit" parameterType="int">
		update notice 
		set 
			bo_hit = bo_hit + 1 
		where bo_no = #{boNo}
	</update>
	
	<!-- <select id="selectNotice" parameterType="int" resultType="noticeVO">
		select 
			bo_no, bo_title, bo_content, bo_writer, bo_date, bo_hit 
		from notice 
		where bo_no = #{boNo}
	</select> -->
	<select id="selectNotice" parameterType="int" resultMap="noticeMap">
		select 
			n.bo_no, bo_title, bo_content, bo_writer, bo_date, bo_hit, 
			file_no, file_name, file_size, file_fancysize, file_mime, file_savepath, file_downcount 
		from notice n left outer join noticefile nf on(n.bo_no = nf.bo_no) 
		where n.bo_no = #{boNo}
	</select>
	
	<update id="updateNotice" parameterType="noticeVO">
		update notice 
		set 
			bo_title = #{boTitle},
			bo_content = #{boContent} 
		where bo_no = #{boNo}
	</update>
	
	<delete id="deleteNotice" parameterType="int">
		delete from notice 
		where bo_no = #{boNo}
	</delete>
	
	<select id="noticeDownload" parameterType="int" resultType="noticefileVO">
		select 
			file_no, file_name, file_size, file_fancysize, file_mime, file_savepath, file_downcount 
		from noticefile 
		where file_no = #{fileNo}
	</select>
	
	<update id="incrementNoticeDowncount" parameterType="int">
		update noticefile 
		set 
			file_downcount = file_downcount + 1 
		where file_no = #{fileNo}
	</update>
	
	<select id="selectNoticeFile" parameterType="int" resultType="noticefileVO">
		select 
			file_no, file_name, file_size, file_fancysize, file_mime, file_savepath, file_downcount 
		from noticefile 
		where file_no = #{fileNo}
	</select>
	
	<delete id="deleteNoticeFile" parameterType="int">
		delete from noticefile 
		where file_no = #{fileNo}
	</delete>
	
	<delete id="deleteNoticeFileByBoNo" parameterType="int">
		delete from noticefile 
		where bo_no = #{boNo}
	</delete>
	
</mapper>

- http://localhost/notice/detail.do?boNo=82


[NoticeServiceImpl.java]

package kr.or.ddit.service.impl;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import kr.or.ddit.ServiceResult;
import kr.or.ddit.controller.crud.notice.TelegramSendController;
import kr.or.ddit.mapper.ILoginMapper;
import kr.or.ddit.mapper.INoticeMapper;
import kr.or.ddit.service.INoticeService;
import kr.or.ddit.vo.crud.NoticeFileVO;
import kr.or.ddit.vo.crud.NoticeMemberVO;
import kr.or.ddit.vo.crud.NoticeVO;
import kr.or.ddit.vo.crud.PaginationInfoVO;

@Service
public class NoticeServiceImpl implements INoticeService {

	@Inject
	private INoticeMapper noticeMapper;
	
	@Inject
	private ILoginMapper loginMapper;
	
	private TelegramSendController tst = new TelegramSendController();

	@Override
	public int selectNoticeCount(PaginationInfoVO<NoticeVO> pagingVO) {
		return noticeMapper.selectNoticeCount(pagingVO);
	}

	@Override
	public List<NoticeVO> selectNoticeList(PaginationInfoVO<NoticeVO> pagingVO) {
		return noticeMapper.selectNoticeList(pagingVO);
	}

	@Override
	public ServiceResult insertNotice(HttpServletRequest req, NoticeVO noticeVO) {
		ServiceResult result = null;

		int status = noticeMapper.insertNotice(noticeVO);
		if (status > 0) {	// 게시글 등록이 성공했을 때
			List<NoticeFileVO> noticeFileList = noticeVO.getNoticeFileList();
			try {
				// 공지사항 파일 업로드 처리
				noticeFileUpload(noticeFileList, noticeVO.getBoNo(), req);
			}catch(IOException e) {
				e.printStackTrace();
			}
			
			// Telegram Bot API를 이용한 실시간 메세지 처리
			try {
				tst.sendGet("홍길동", noticeVO.getBoTitle());
			} catch (Exception e) {
				e.printStackTrace();
			}
			
			result = ServiceResult.OK;
		} else {
			result = ServiceResult.FAILED;
		}

		return result;
	}

	@Override
	public NoticeVO selectNotice(int boNo) {
		noticeMapper.incrementHit(boNo); // 게시글 조회수 증가
		return noticeMapper.selectNotice(boNo); // 게시글 번호에 해당하는 게시글 정보 가져오기
	}

	@Override
	public ServiceResult updateNotice(HttpServletRequest req, NoticeVO noticeVO) {
		ServiceResult result = null;

		int status = noticeMapper.updateNotice(noticeVO); // 게시글 수정
		if (status > 0) { // 게시글 수정 완료
			// 게시글 정보에서 파일 목록을 가져오기
			List<NoticeFileVO> noticeFileList = noticeVO.getNoticeFileList();
			try {
				// 공지사항 업로드 진행
				noticeFileUpload(noticeFileList, noticeVO.getBoNo(), req);
				
				// 기존에 등록되어 있는 파일 목록들 중, 수정하기 위해서 X버튼을 눌러 삭제 처리로 넘겨준 파일 번호들
				Integer[] delNoticeNo = noticeVO.getDelNoticeNo();
				if(delNoticeNo != null) {
					for(int i = 0; i < delNoticeNo.length; i++) {
						// 삭제할 파일 번호 목록들 중, 파일 번호에 해당하는 공지사항 파일 정보를 가져온다.
						NoticeFileVO noticeFileVO = noticeMapper.selectNoticeFile(delNoticeNo[i]);
						noticeMapper.deleteNoticeFile(delNoticeNo[i]); // 파일번호에 해당하는 파일 데이터를 삭제
						File file = new File(noticeFileVO.getFileSavepath());
						file.delete(); // 기존 파일이 업로드 되어 있던 경로에 파일 삭제
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			
			result = ServiceResult.OK;
		} else { // 게시글 수정 실패
			result = ServiceResult.FAILED;
		}

		return result;
	}

	@Override
	public ServiceResult deleteNotice(HttpServletRequest req, int boNo) {
		ServiceResult result = null;

		// 공지사항 파일 데이터를 삭제하기 위한 준비(파일 적용시)
		NoticeVO noticeVO = noticeMapper.selectNotice(boNo); // 게시글 번호에 해당하는 공지사항 게시글 정보 가져오기
		noticeMapper.deleteNoticeFileByBoNo(boNo); // 게시글 번호에 해당하는 파일 데이터 삭제
		
		int status = noticeMapper.deleteNotice(boNo); // 일반적인 게시글 삭제
		if (status > 0) {
			List<NoticeFileVO> noticeFileList = noticeVO.getNoticeFileList();
			if(noticeFileList != null && noticeFileList.size() > 0) {
				// D:\99_Class...\workspace\.metadate\.plugins.....\Project명\resources\notice\boNo
				// 12d3uiy21euy1e2d.jpg
				// '/' 기준으로 잘라준다.
				String[] filePath = noticeFileList.get(0).getFileSavepath().split("/");
				
				String path = filePath[0];
				deleteFolder(req, path);
			}
			
			result = ServiceResult.OK;
		} else {
			result = ServiceResult.FAILED;
		}

		return result;
	}

	private void deleteFolder(HttpServletRequest req, String path) {
		// UUID + 원본파일명 전 폴더 경로를 folder 파일 객체로 잡는다.
		File folder = new File(path);
		
		if(folder.exists()) { // 경로가 존재한다면
			File[] folderList = folder.listFiles(); // 폴더 안에 있는 파일들의 목록을 가져온다.
			
			for(int i = 0; i < folderList.length; i++) {
				if(folderList[i].isFile()) { // 폴더 안에 있는 파일이 파일일 때
					// 폴더 안에 파일을 차례대로 삭제
					folderList[i].delete();
				}else {
					// 폴더 안에 있는 파일이 폴더일 때 재귀함수 호출(폴더 안으로 들어가서 재 처리할 수 있도록)
					deleteFolder(req, folderList[i].getPath());
				}
			}
			
			folder.delete(); // 폴더 삭제
		}
	}

	@Override
	public NoticeMemberVO loginCheck(NoticeMemberVO member) {
		return loginMapper.loginCheck(member);
	}

	@Override
	public ServiceResult idCheck(String memId) {
		ServiceResult result = null;

		NoticeMemberVO member = loginMapper.idCheck(memId);
		if (member != null) {
			result = ServiceResult.EXIST;
		} else {
			result = ServiceResult.NOTEXIST;
		}

		return result;
	}

	@Override
	public ServiceResult signup(HttpServletRequest req, NoticeMemberVO memberVO) {
		ServiceResult result = null;

		// 회원가입 시, 프로필 이미지로 파일을 업로드 하는데 이때 업로드 할 서버 경로
		String uploadPath = req.getServletContext().getRealPath("/resources/profile");
		File file = new File(uploadPath);
		if (!file.exists()) {
			file.mkdirs();
		}

		String proFileImg = ""; // 회원정보에 추가될 프로필 이미지 경로
		try {
			// 넘겨받은 회원정보에서 파일 데이터 가져오기
			MultipartFile proFileImgFile = memberVO.getImgFile();

			// 넘겨받은 파일 데이터가 존재할 때
			if (proFileImgFile.getOriginalFilename() != null && !proFileImgFile.getOriginalFilename().equals("")) {
				String fileName = UUID.randomUUID().toString(); // UUID 파일명 생성
				fileName += "_" + proFileImgFile.getOriginalFilename(); // UUID_원본파일명으로 파일명 생성
				uploadPath += "/" + fileName; // /resources/profile/uuid_원본파일명

				proFileImgFile.transferTo(new File(uploadPath)); // 해당 위치에 파일 복사
				proFileImg = "/resources/profile/" + fileName; // 파일 복사가 일어난 파일의 위치로 접근하기 위한 URI 설정
			}

			memberVO.setMemProfileImg(proFileImg);
		} catch (Exception e) {
			e.printStackTrace();
		}

		int status = loginMapper.signup(memberVO);

		if (status > 0) { // 등록 성공
			result = ServiceResult.OK;
		} else {
			result = ServiceResult.FAILED;
		}

		return result;
	}
	
	private void noticeFileUpload(
			List<NoticeFileVO> noticeFileList, 
			int boNo, 
			HttpServletRequest req
			) throws IOException {
		
		String savePath = "/resources/notice/";
		
		if(noticeFileList != null) { // 넘겨받은 파일 데이터가 존재할 때
			if(noticeFileList.size() > 0) {
				for(NoticeFileVO noticeFileVO : noticeFileList) {
					String saveName = UUID.randomUUID().toString(); // UUID의 랜덤 파일명 생성
					
					// 파일명을 설장할 때, 원본 파일명의 공백을 '_'로 변경한다.
					saveName = saveName + "_" + noticeFileVO.getFileName().replaceAll(" ", "_");
					// 디버깅 및 확장자 추출 참고
					String endFileName = noticeFileVO.getFileName().split("\\.")[1];
					
					String saveLocate = req.getServletContext().getRealPath(savePath + boNo);
					File file = new File(saveLocate);
					if(!file.exists()) {
						file.mkdirs();
					}
					saveLocate += "/" + saveName;
					
					noticeFileVO.setBoNo(boNo);						// 게시글 번호 설정
					noticeFileVO.setFileSavepath(saveLocate); 		// 파일 업로드 경로 설정
					noticeMapper.insertNoticeFile(noticeFileVO);	// 게시글 파일 데이터 추가
					
					File saveFile = new File(saveLocate);
					
					// 방법 1
//					InputStream is = noticeFileVO.getItem().getInputStream();
//					FileUtils.copyInputStreamToFile(is, saveFile);
					
					// 방법 2
					noticeFileVO.getItem().transferTo(saveFile); // 파일 복사
				}
			}
		}
		
	}

	@Override
	public NoticeFileVO noticeDownload(int fileNo) {
		NoticeFileVO noticeFileVO = noticeMapper.noticeDownload(fileNo);
		if(noticeFileVO == null) {
			throw new RuntimeException();
		}
		noticeMapper.incrementNoticeDowncount(fileNo); // 다운로드 횟수 증가
		return noticeFileVO;
	}

}

package kr.or.ddit.controller.crud.notice;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;

// TelegramSendController 클래스를 인스턴스로 사용하기 때문에 어노테이션을 따로 붙이지 않는다.
public class TelegramSendController {

	// 텔레그램 BOT이 초대되어 있는 방으로 메시지가 전송된다.
	public void sendGet(String name, String title) throws Exception {
		String chat_id = "-951855941";	// 채팅방 ID
		String urlName = "https://api.telegram.org/bot6310783618:AAEI3ptPtCArMaRYoKBHxfHhjOuYcDbVUaY/sendMessage";	// 메시지 전송 타겟 URL
		String text = "";		// 전송 메시지
		
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		Date time = new Date();
		String time1 = format.format(time);
		
		text = name + "님께서 글 작성을 완료하였습니다!\n" + 
				"[제목] : " + title + "\n" +
				"[작성일] : " + time1 + "\n";
		
		URL url = new URL(urlName + "?chat_id=" + chat_id + "&text=" + URLEncoder.encode(text, "UTF-8"));
		HttpURLConnection conn = (HttpURLConnection) url.openConnection();
		conn.setRequestMethod("POST");
		conn.setRequestProperty("User-Agent", "Mozila/5.0");
		int respCode = conn.getResponseCode();
		System.out.println("텔레그램 봇 API를 활용한 메시지 처리 결과 : " + respCode);
	}
	
}

- http://localhost/notice/form.do


https://core.telegram.org/bots

 

Bots: An introduction for developers

Bots are small applications that run entirely within the Telegram app. Users interact with bots through flexible interfaces…

core.telegram.org

https://core.telegram.org/bots

 

Bots: An introduction for developers

Bots are small applications that run entirely within the Telegram app. Users interact with bots through flexible interfaces…

core.telegram.org

토큰 값

https://api.telegram.org/bot6310783618:AAEI3ptPtCArMaRYoKBHxfHhjOuYcDbVUaY/getUpdates