SpringBoot - 04

월요일좋아·2022년 11월 16일
0

SpringBoot

목록 보기
4/10

파일 업로딩

테이블 생성 (t_file)

메이븐 리포지토리 접속 -. 아래의 두 코드 gradle에 추가

	// https://mvnrepository.com/artifact/commons-io/commons-io
	implementation 'commons-io:commons-io:2.11.0'

	// https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload
	implementation 'commons-fileupload:commons-fileupload:1.4'

configuration폴더 아래에 webMvcConfiguration 자바 클래스 파일 생성

  • webMvcConfiguration
package com.bitc.board.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Bean
    public CommonsMultipartResolver multipartResolver() {
        CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();

//        기본 문자셋 설정
        commonsMultipartResolver.setDefaultEncoding("UTF-8");
//        업로드 파일 최대 크기 설정, byte 크기로 설정하기 때문에 5 * 1024 * 1024 = 5MB
        commonsMultipartResolver.setMaxUploadSizePerFile(5 * 1024 * 1024);

        return commonsMultipartResolver;
    }
}

Board1Application 기본설정 제거

  • 제거 전
package com.bitc.board;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Board1Application {

	public static void main(String[] args) {
		SpringApplication.run(Board1Application.class, args);
	}

}
  • 제거 및새로운 어노테이션
    (@SpringBootApplication(exclude = {MultipartAutoConfiguration.class})) 추가
package com.bitc.board;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration;

//@SpringBootApplication <- 주석처리
// exclude : 옵션을 사용하여 MultipartAutoConfiguration 클래스의 자동 구성을 사용하지 않도록 설정
@SpringBootApplication(exclude = {MultipartAutoConfiguration.class})
public class Board1Application {

	public static void main(String[] args) {
		SpringApplication.run(Board1Application.class, args);
	}

}

boardWrite.html에서 파일등록하는 부분 추가(textarea 바로 밑에 div 생성)
input태그에 multiple 추가해주면 파일 여러개 선택해서 업로드 가능함

  • boardWrite.html
<!--                파일 업로드를 위해서 추가 -->
                <div class="my-3">
                    <input type="file" class="form-control" id="files" name="files" multiple>

boardWrite 파일의 form 태그에 enctype="multipart/form-data" 추가

<form action="/board/insertBoard" method="post" enctype="multipart/form-data">

formController 파일 수정
MultipartHttpServletRequest multipart , multipart 추가

//    boardWrite 등록 페이지
//    클라이언트에서 업로드된 파일 데이터를 받기 위해서 매개변수로 MultipartHttpServletRequest를 추가함
    @RequestMapping("/board/insertBoard")
    public String insertBoard(BoardDto board, MultipartHttpServletRequest multipart) throws Exception {
//        업로드된 파일 데이터를 서비스 영역에서 처리하기 위해서 매개변수를 추가
        boardService.insertBoard(board, multipart);

        return "redirect:/board/openBoardList";
    }

multipart에 빨간 밑줄 -> Alt+Enter로 처리하면 BoardServiceImpl 파일에 반영이 됨

BoardServiceImpl로 가서 아래 insertBoard 부분 수정

@Override
    public void insertBoard(BoardDto board, MultipartHttpServletRequest multipart) throws Exception {
        // 나중에 파일 업로드 부분이 추가되는 곳
//        boardMapper.insertBoard(board);

//        ObjectUtils 는 타임리프버전/스프링버전 두개가 있는데 스프링버전으로 선택해서 import 하기
//        ObjectUtils : SpringFrameWork 에서 추가된 클래스로 isEmpty() 는 지정한 객체가 비었는지 아닌지 확인해줌(비었으면 true, 있으면 false)
        if (ObjectUtils.isEmpty(multipart) == false) {
//            MultipartHttpServletRequest 클래스 타입의 변수 multipart 에 저장된 파일 데이터 중 파일 이름만 모두 가져옴
            Iterator<String> iterator = multipart.getFileNames();
            String name; // 파일명을 저장할 변수

//            Iterator 타입의 변수에 저장된 모든 내용을 출력할 때까지 반복 실행
            while (iterator.hasNext()) {
                name = iterator.next(); // 실제 데이터 가져옴(name 에 저장되는것 : 파일명)
//                multipart 에서 해당 파일의 이름을 기준으로 해서 가져온 실제 파일 데이터를 MultipartFile 클래스 타입의 fileInfoList 에 넣음
                List<MultipartFile> fileInfoList = multipart.getFiles(name);

                for (MultipartFile fileInfo : fileInfoList) {
                    System.out.println("start file info...");
                    System.out.println("file name : " + fileInfo.getOriginalFilename());
                    System.out.println("file size : " + fileInfo.getSize());
                    System.out.println("file content type : " + fileInfo.getContentType());
                    System.out.println("end file info...");
                    System.out.println("------------------------------------");
                }
            }
        }
    }

웹에서 파일 올리기 -> 확인 클릭 후 콘솔 실행창에서 결과 확인 가능. 아래의 코드가 뜨면 정상적으로 서버로 전달이 되었다는 뜻이다.


파일 정보를 담을 Dto 파일이 필요함 -> Dto 폴더에 BoardFileDto 자바 클래스 파일 생성

  • BoardFileDto.java
package com.bitc.board.dto;

import lombok.Data;

@Data
public class BoardFileDto {
    private int idx;
    private int boardIdx;
    private String originalFileName;
    private String storedFilepath;
    private long fileSize;
    
}

서버 컴퓨터에 저장하는 부분 입력
com.bitc.board 폴더에 com.bitc.board.common 패키지 생성 -> 패키지 내부에 FileUtils 자바 클래스 파일 생성

impl

@Override
    public void insertBoard(BoardDto board, MultipartHttpServletRequest multipart) throws Exception {
        // 파일 업로드
        boardMapper.insertBoard(board);

        List<BoardFileDto> fileList = fileUtils.parseFileInfo(board.getBoardIdx(), multipart);

        if(CollectionUtils.isEmpty(fileList) == false) { // springFrameWork util 로 선택해서 import
            boardMapper.insertBoardFileList(fileList);
        }


맵퍼에 추가됨 -> throws Exception 추가

void insertBoardFileList(List<BoardFileDto> fileList) throws Exception;
}

sql-board.xml 파일에 코드 추가

<insert id="insertBoardFileList" parameterType="com.bitc.board.dto.BoardFileDto">
        <![CDATA[
            INSERT INTO t_file
                (board_idx, original_file_name, stored_file_path, file_size, create_id, create_date)
            VALUES
                (#{boardIdx}, #{originalFileName}, #{storedFilePath}, #{fileSize}, 'admin', NOW())
        ]]>
    </insert>

sql-board.xml 파일에 코드 수정

<!--    userGeneratedKeys : DBMS가 자동 키 생성을 지원할 경우 자동 키 생성을 사용하겠다는 의미 -->
<!--    keyProperty : 자동으로 생성된 키를 받아서 지정한 컬럼으로 되돌려 줌 -->
    <insert id="insertBoard" parameterType="com.bitc.board.dto.BoardDto" useGeneratedKeys="true" keyProperty="idx">
        <![CDATA[
        INSERT INTO t_board (title, contents, user_id, pwd, create_dt)
        VALUES (#{title}, #{contents}, #{userId}, '1234', NOW())
        ]]>
    </insert>

게시물에 저장된 파일 목록 보기

sql파일에 select 추가

<select id="selectBoardFileList" resultType="com.bitc.board.dto.BoardFileDto">
        <![CDATA[
            SELECT
                idx, board_idx, original_file_name,
                FORMAT(ROUND(file_size / 1024), 0) AS file_size
            FROM
                t_file
            WHERE board_idx = #{boardIdx}
            AND
                deleted_yn = 'N'
        ]]>
    </select>

mapper에 List<BoardFileDto> selectBoardFileList(int boardIdx) throws Exception; 추가

BoardDto에 private List<BoardFileDto> fileList; 추가

impl수정(추가)

    @Override
    public BoardDto selectBoardDetail(int idx) throws Exception {
//        조회수 증가
        boardMapper.updateHitCount(idx);
//        지정한 게시물 상세 정보(현재 첨부 파일 목록은 없음)
        BoardDto board = boardMapper.selectBoardDetail(idx);
//        지정한 게시물의 첨부 파일 목록 가져오기
        List<BoardFileDto> fileList = boardMapper.selectBoardFileList(idx);
//        가져온 게시물 상세 정보에 가져온 첨부 파일 목록을 추가함
        board.setFileList(fileList);

        return board;
    }

클릭하면 파일 다운가능하도록

boardDetail에 추가

<!--    파일 목록 출력하기 -->
    <div class="row">
      <div class="col-sm my-3">
        <a class="btn btn-link" th:each="list : ${board.fileList}" th:text="|${list.originalFileName} (${list.fileSize}kb)|" th:href="@{/board/downloadBoardFile(idx=${list.idx}, boardIdx=${list.boardIdx})}"></a>
      </div>
    </div>

boardController 파일 제일 아래에 추가

    @RequestMapping("/board/downloadBoardFile")
    public void downloadBoardFile(@RequestParam int idx, @RequestParam int boardIdx, HttpServletResponse response) throws Exception {
        BoardFileDto boardFile = boardService.selectBoardFileInfo(idx, boardIdx);

        if (ObjectUtils.isEmpty(boardFile) == false) {
            String fileName = boardFile.getOriginalFileName();

            byte[] files = FileUtils.readFileToByteArray(new File(boardFile.getStoredFilePath()));

            response.setContentType("application/octet-stream");
            response.setContentLength(files.length);
            response.setHeader("Content-Disposition", "attachment; fileName=\"" + URLEncoder.encode(fileName, "UTF-8") + "\";");
            response.getOutputStream().write(files);
            response.getOutputStream().flush();
            response.getOutputStream().close();
        }
    }


boardController

package com.bitc.board.controller;

import com.bitc.board.dto.BoardDto;
import com.bitc.board.dto.BoardFileDto;
import com.bitc.board.service.BoardService;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.net.URLEncoder;
import java.util.List;
//         @Controller : 사용자가 웹브라우저를 통하여 어떠한 요청을 할 경우 해당 요청을 처리하기 위한 비즈니스 로직을 가지고 있는 어노테이션.
//                       클래스에 해당 어노테이션을 사용하면 해당 클래스는 사용자 요청을 처리하기 위한 클래스라는 것을 스프링 프레임워크에 알림
// 컨트롤러가 하는 일 : 1. 사용자가 서버에 요청한 주소를 기반으로 사용자가 전송한 데이터를 받음
//                      2. 사용자에게 제공할 View 파일을 연동
//                      3. 사용자에게 전송한 데이터를 바탕으로 서비스에게 내부 연산을 요청함
@Controller
public class BoardController {

//  @Autowired : 사용자가 해당 타입의 객체를 생성하는 것이 아니라 스프링프레임워크가 해당 타입의 객체를 생성하고,
//               사용자는 이용만 하도록 하는 어노테이션
    @Autowired
    private BoardService boardService;
//  @RequestMapping : 사용자가 웹브라우저를 통해서 접속하는 실제 주소와 메서드를 매칭하기 위한 어노테이션
//       value 속성 : 사용자가 접속할 주소 설정, 2개 이상의 주소를 하나의 메서드와 연결하려면 {주소1, 주소2, ...} 형태로 사용,
//                    value 속성만 사용할 경우 생략 가능 (value="주소" --(생략)--> "주소")
//      method 속성 : 클라이언트에서 서버로 요청 시 사용하는 통신 방식을 설정하는 속성 (GET/POST),
//                    RequestMethod 타입을 사용, Restful 방식을 사용할 경우 GET/POST/UPDATE/DELETE 를 사용할 수 있음, 기본값 = GET
//                    ( @RequestMapping("/board/openBoardList.do", method = RequestMethod.GET/POST) )

    @RequestMapping("/")
    public String index() throws Exception {
        return "index";
    }

    // 게시물 목록 페이지
    @RequestMapping(value = "/board/openBoardList", method = RequestMethod.GET)
    public ModelAndView openBoardList() throws Exception {
        //html 파일이 있는 위치(resources-templates 는 스프링에서 고정이기 때문에 그 아래의 폴더만 써주면 됨)
        ModelAndView mv = new ModelAndView("board/boardList");

        // 필요한 데이터 객체 실어주기
        // 실제 데이터베이스에서 넘어온 데이터를 BoardDto타입으로 만들어진 dataList에 저장
        List<BoardDto> dataList = boardService.selectBoardList();
        // 실제 데이터를 addObject를 통해 밀어넣음(이름은 datatList 로(html에서 구별하기 위한 구분자), 실제 변수명은 dataList)
        mv.addObject("dataList", dataList);

        return mv; // html 파일의 데이터가 들어가면서 그것을 클라이언트에 보낸다 -> 웹 브라우저로 다시 뿌림
    }

    // 게시물 상세 보기
    //  @RequestParam : jsp의 request.getParameter()와 같은 기능을 하는 어노테이션, 클라이언트에서 서버로 전송된 데이터를 가져오는 어노테이션
    @RequestMapping("/board/openBoardDetail") // do 는 생략 가능. EJB 하던 사람들의 버릇에 의해 생긴 관습일 뿐
    public ModelAndView openBoardDetail(@RequestParam int idx) throws Exception{
        // 리눅스에서는 첫번째문자가 / 일때 인식을 못해서 없애줘야함. @RequestMapping()의 주소는 제일앞에 / 있어야 함
        ModelAndView mv = new ModelAndView("board/boardDetail");

//        mv.setViewName("board/boardDetail"); <- 위 코드와 동일한 의미임. 업체/개인 등의 사용자에 따라 다른 화면을 보여줘야 할때 뷰네임 이용
        BoardDto board = boardService.selectBoardDetail(idx); // boardService를 통해 idx를 기준으로 DB연동
        mv.addObject("board", board);

        return mv; // 사용자에게 보여줄 뷰 던져줌
    }

    // boardWrite 뷰 페이지 : 단순히 View 만 보여줄 페이지임
    @RequestMapping("/board/boardWrite")
    public String boardWrite() throws Exception {
        return "board/boardWrite";
    }

//    boardWrite 등록 페이지
//    클라이언트에서 업로드된 파일 데이터를 받기 위해서 매개변수로 MultipartHttpServletRequest를 추가함
    @RequestMapping("/board/insertBoard")
    public String insertBoard(BoardDto board, MultipartHttpServletRequest multipart) throws Exception {
//        업로드된 파일 데이터를 서비스 영역에서 처리하기 위해서 매개변수를 추가
        boardService.insertBoard(board, multipart);

        return "redirect:/board/openBoardList";
    }

    // boardWrite 수정 페이지
    @RequestMapping("/board/updateBoard")
    public String updateBoard(BoardDto board) throws Exception {
        boardService.updateBoard(board);

        return "redirect:/board/openBoardList";
    }

    // boardWrite 삭제 페이지
    @RequestMapping("/board/deleteBoard")
    public String deleteBoard(@RequestParam int idx) throws Exception {
        boardService.deleteBoard(idx);

        return "redirect:/board/openBoardList";
    }

    @RequestMapping("/board/downloadBoardFile")
    public void downloadBoardFile(@RequestParam int idx, @RequestParam int boardIdx, HttpServletResponse response) throws Exception {
        BoardFileDto boardFile = boardService.selectBoardFileInfo(idx, boardIdx);

        if (ObjectUtils.isEmpty(boardFile) == false) {
            String fileName = boardFile.getOriginalFileName();

            byte[] files = FileUtils.readFileToByteArray(new File(boardFile.getStoredFilePath()));

            response.setContentType("application/octet-stream");
            response.setContentLength(files.length);
            response.setHeader("Content-Disposition", "attachment; fileName=\"" + URLEncoder.encode(fileName, "UTF-8") + "\";");
            response.getOutputStream().write(files);
            response.getOutputStream().flush();
            response.getOutputStream().close();
        }
    }
}

BoardDto

package com.bitc.board.dto;

import lombok.Data;

import java.util.List;

//  @Data : lombok 라이브러리에서 지원하는 어노테이션으로,
//          해당 클래스의 멤버 변수에 대한 getter/setter/toString() 메서드를 자동으로 생성하는 어노테이션
//          @Getter, @Setter, @ToString 어노테이션을 모두 사용한것과 같은 효과임

//  DTO (Data Transfer Object) : 데이터 전송 시 사용하기 위한 Java Class 객체, DB의 table 과 매칭하는 데 사용함
//  DTO 클래스의 멤버 변수는 매칭되는 DB 테이블의 컬럼명과 똑같이 쓰지만, 스네이크명명법을 -> 카멜명명법으로만 바꿔주면 됨(알아서 맞게 데이터를 가져옴)
@Data

//@Getter : 자동 Getter 생성(@Data 없애고 사용 가능)
//@Setter : 자동 Setter 생성(@Data 없애고 사용 가능)
public class BoardDto {
    // 컬럼명과 동일하게 써주면 됨
    // (언더바 들어가는 컬럼은 _를 지우고 그 뒤의 첫 글자를 대문자로 바꿔줘야함)
    private int idx;
    private String title;
    private String contents;
    private String userId;
    private String pwd;
    private String createDt;
    private String updateDt;
    private int hitCnt;
    private List<BoardFileDto> fileList;
}

BoardFileDto

package com.bitc.board.dto;

import lombok.Data;

@Data
public class BoardFileDto {
    private int idx;
    private int boardIdx;
    private String originalFileName;
    private String storedFilePath;
    private long fileSize;
}

BoardServiceImpl

package com.bitc.board.service;

import com.bitc.board.common.FileUtils;
import com.bitc.board.dto.BoardDto;
import com.bitc.board.dto.BoardFileDto;
import com.bitc.board.mapper.BoardMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import java.util.List;

//          @Service : 해당 파일이 서비스 Interface 파일(= 컨트롤러에서 @Autowired로 만들어진것)을
//                     구현하는 구현체라는 것을 알려주는 어노테이션
// 서비스가 하는 일 : 1. 컨트롤러에서 전달받은 데이터를 기반으로 [연산]을 진행
//                    2. ORM(mapper/repository) 을 통해서 DB에 접근 : boardMapper.selectBoardList()
//                    3. ORM을 통해서 가져온 데이터를 가공 :
//                    4. 컨트롤러로 가공된 데이터를 전달
@Service
public class BoardServiceImpl implements BoardService {
    // 빨간밑줄 -> 구현... 클릭 -> OK

    @Autowired
    private BoardMapper boardMapper;

    @Autowired
    private FileUtils fileUtils;

    @Override
    public List<BoardDto> selectBoardList() throws Exception {
        return boardMapper.selectBoardList();
    }

    @Override
    public BoardDto selectBoardDetail(int idx) throws Exception {
//        조회수 증가
        boardMapper.updateHitCount(idx);
//        지정한 게시물 상세 정보(현재 첨부 파일 목록은 없음)
        BoardDto board = boardMapper.selectBoardDetail(idx);
//        지정한 게시물의 첨부 파일 목록 가져오기
        List<BoardFileDto> fileList = boardMapper.selectBoardFileList(idx);
//        가져온 게시물 상세 정보에 가져온 첨부 파일 목록을 추가함
        board.setFileList(fileList);

        return board;
    }

    @Override
    public void insertBoard(BoardDto board, MultipartHttpServletRequest uploadFiles) throws Exception {
//        게시물 정보를 데이터베이스에 저장
        boardMapper.insertBoard(board);

//        업로드 된 파일 정보를 가지고 BoardFileDto 클래스 타입의 리스트를 생성
        List<BoardFileDto> fileList = fileUtils.parseFileInfo(board.getIdx(), uploadFiles);

//        파일 리스트가 비었는지 확인 후 데이터베이스에 저장
//        if(CollectionUtils.isEmpty(fileList) == false) { // springFrameWork util 로 선택해서 import
            boardMapper.insertBoardFileList(fileList);
        }


//        ObjectUtils 는 타임리프버전/스프링버전 두개가 있는데 스프링버전으로 선택해서 import 하기
//        ObjectUtils : SpringFrameWork 에서 추가된 클래스로 isEmpty() 는 지정한 객체가 비었는지 아닌지 확인해줌(비었으면 true, 있으면 false)
//        if (ObjectUtils.isEmpty(multipart) == false) {
////            MultipartHttpServletRequest 클래스 타입의 변수 multipart 에 저장된 파일 데이터 중 파일 이름만 모두 가져옴
//            Iterator<String> iterator = multipart.getFileNames();
//            String name; // 파일명을 저장할 변수
//
////            Iterator 타입의 변수에 저장된 모든 내용을 출력할 때까지 반복 실행
//            while (iterator.hasNext()) {
//                name = iterator.next(); // 실제 데이터 가져옴(name 에 저장되는것 : 파일명)
////                multipart 에서 해당 파일의 이름을 기준으로 해서 가져온 실제 파일 데이터를 MultipartFile 클래스 타입의 fileInfoList 에 넣음
//                List<MultipartFile> fileInfoList = multipart.getFiles(name);
//
//                for (MultipartFile fileInfo : fileInfoList) {
//                    System.out.println("start file info...");
//                    System.out.println("file name : " + fileInfo.getOriginalFilename());
//                    System.out.println("file size : " + fileInfo.getSize());
//                    System.out.println("file content type : " + fileInfo.getContentType());
//                    System.out.println("end file info...");
//                    System.out.println("------------------------------------");
//                }
//            }
//        }
//    }

    @Override
    public void updateBoard(BoardDto board) throws Exception {
        boardMapper.updateBoard(board);
    }

    @Override
    public void deleteBoard(int idx) throws Exception {
        boardMapper.deleteBoard(idx);
    }

    @Override
    public BoardFileDto selectBoardFileInfo(int idx, int boardIdx) throws Exception {
        return boardMapper.selectBoardFileInfo(idx, boardIdx);
    }
}

boardDetail

<!DOCTYPE html>
<!-- xmlns 추가 해주기! -->
<html lang="ko" xmlns:th="http://www.thymleaf.org">
<head>
  <meta charset="UTF-8">
  <title>게시물 상세 페이지</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script>

  <script>
    $(document).ready(function () {
      $('#list').on('click', function () {
        history.back();
      });

      $('#edit').on('click', function () {
        const frm = $('#frm')[0]; // [0] <- form 객체의 첫번째 것을 찾는다는 뜻. 안써도 상관없음(form 태그가 여러개 사용하는 페이지에서 사용)
        frm.action = '/board/updateBoard';
        frm.submit();
      });

      $('#delete').on('click', function () {
        const frm = $('#frm')[0];
        frm.action = '/board/deleteBoard';
        frm.submit();
      });
    });
  </script>
</head>
<body>
<header class="container mt-2">
  <div class="bg-light rounded-3 p-4">
    <h1 class="display-3">게시물 상세 페이지</h1>
  </div>
</header>
<main class="container">
<!--  form 태그 선언 ~> textarea 까지 감싸기 -->
  <form id="frm" method="post">
    <div class="row">
      <div class="col-sm-2">
        <div class="form-floating my-3">
          <input type="text" class="form-control" id="idx" name="idx" readonly th:value="${board.idx}" placeholder="글번호">
          <label for="idx">글번호</label>
        </div>
      </div>
      <div class="col-sm">
        <div class="form-floating my-3">
          <input type="text" class="form-control" id="title" name="title" th:value="${board.title}" placeholder="글제목">
          <label for="title">글제목</label>
        </div>
      </div>
    </div>
    <div class="row">
      <div class="col-sm">
        <div class="form-floating my-3">
          <input type="text" class="form-control" readonly id="user-id" th:value="${board.userId}" placeholder="글쓴이">
          <label for="user-id">글쓴이</label>
        </div>
      </div>
      <div class="col-sm">
        <div class="form-floating my-3">
          <input type="text" class="form-control" readonly id="hit-cnt" th:value="${board.hitCnt}" placeholder="조회수">
          <label for="hit-cnt">조회수</label>
        </div>
      </div>
      <div class="col-sm">
        <div class="form-floating my-3">
          <input type="text" class="form-control" id="create-dt" readonly th:value="${board.createDt}" placeholder="등록일">
          <label for="create-dt">등록일</label>
        </div>
      </div>
    </div>
    <div class="row">
      <div class="col-sm my-3">
        <textarea class="form-control" rows="5" id="contents" name="contents" th:text="${board.contents}" placeholder="글내용"></textarea>
      </div>
    </div>

<!--    파일 목록 출력하기 -->
    <div class="row">
      <div class="col-sm my-3">
        <a class="btn btn-link" th:each="list : ${board.fileList}" th:text="|${list.originalFileName} (${list.fileSize}kb)|" th:href="@{/board/downloadBoardFile(idx=${list.idx}, boardIdx=${list.boardIdx})}"></a>
      </div>
    </div>
  </form>
  <div class="row">
    <div class="col-sm">
      <button type="button" class="btn btn-outline-secondary" id="list">목록</button>
    </div>
    <div class="col-sm d-flex justify-content-end">
      <button type="button" class="btn btn-outline-warning me-2" id="edit">수정</button>
      <button type="button" class="btn btn-outline-danger" id="delete">삭제</button>
    </div>
  </div>
</main>
<footer class="container-fluid mt-5 border-top p-5">
  <p class="lead text-muted text-center">made by Gwak</p>
</footer>
</body>
</html>

BoardMapper

package com.bitc.board.mapper;

import com.bitc.board.dto.BoardDto;
import com.bitc.board.dto.BoardFileDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

//                @Mapper : mybatis orm을 사용하여 xml 파일과 연동된 인터페이스임을 알려주는 어노테이션
//  Mapper 파일이 하는 일 : 1. DB의 데이터 객체와 Java 객체의 형태가 다르기 때문에 데이터를 변환
//                          2. Java에서 DB에 명령을 보낼 수 있는 형태의 메서드를 제공(자바 메서드처럼 보이지만 실제로는 SQL 쿼리가 전송됨)
//  ex) 데이터 형태가 다르다?
//      숫자: Mysql : INT, Java : INT
//      문자: Mysql : VARCHAR, Java : String
//      날짜: Mysql : datetime, Java : format...
@Mapper
public interface BoardMapper {
    List<BoardDto> selectBoardList() throws Exception; // boardDto타입으로 여러개가 넘어옴(List)

    BoardDto selectBoardDetail(int idx) throws Exception;

    void insertBoard(BoardDto board) throws Exception;

    void updateBoard(BoardDto board) throws Exception;

    void deleteBoard(int idx) throws Exception;

    void updateHitCount(int idx) throws Exception;

    void insertBoardFileList(List<BoardFileDto> fileList) throws Exception;

    List<BoardFileDto> selectBoardFileList(int boardIdx) throws Exception;

    BoardFileDto selectBoardFileInfo(@Param("idx") int idx, @Param("boardIdx") int boardIdx) throws Exception;
}

sql-board.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- sql-board.xml : 마이바티스 실행을 위해 필요한 파일 -->

<!-- mybatis SQL 매핑 파일을 뜻하는 지시문 -->
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace 속성에 지정한 패키지명을 가지고 있는 파일과 아래의 xml 구문을 연동한다는 의미 -->
<!-- ★★★ xml 파일이기 때문에 스프링 프로젝트의 전체 구조를 모르기 때문에 전체 패키지 명을 다 입력해야 함 ★★★ -->
<mapper namespace="com.bitc.board.mapper.BoardMapper">
<!--    실제 sql 쿼리문을 입력하는 부분 -->
<!--        id 속성 : 위에서 지정한 파일에 존재하는 메서드 명과 동일하게 입력해야 함(오타->오류). 오버로딩을 지원하지 않기 때문   -->
<!--     resultType : 지정한 메서드의 반환값, 자바 기본 타입은 그대로 입력 가능(ex-int, String 등),                               -->
<!--                  사용자가 지정한 데이터 타입은 xml 파일에서 인식하지 못하기 때문에 전체 패키지명을 다 입력해야 함            -->
<!--  parameterType : 지정한 메서드의 매개변수가 가지고 있는 데이터 타입, 자바 기본 타입은 그대로 입력,                           -->
<!--                  사용자가 지정한 데이터 타입은 전체 패키지명을 다 입력해야 함.                                               -->
<!--      변수 선언 : PreparedStatement 방식을 사용하여 지정한 위치에 데이터를 입력하기 위해서 #{변수명} 형태를 사용              -->
<!--                  매개변수가 기본 타입을 경우 mapper 파일의 메서드의 변수명을 그대로 사용 가능                                -->
<!--                  @Param 어노테이션을 사용하여 매개변수의 이름을 지정할 수 있음                                               -->
<!--                  매개변수가 사용자 지정 타입일 경우(ex-dto) 해당 타입의 멤버 변수명을 그대로 사용                            -->
    <select id="selectBoardList" resultType="com.bitc.board.dto.BoardDto">
        <![CDATA[
        SELECT idx, title, user_id, create_dt, hit_cnt FROM t_board
        WHERE deleted_yn = 'N'
        order by idx DESC
        ]]>
    </select>

    <select id="selectBoardDetail" parameterType="int" resultType="com.bitc.board.dto.BoardDto">
        <![CDATA[
        SELECT idx, title, contents, user_id, create_dt, hit_cnt, update_dt, hit_cnt FROM t_board
        WHERE idx  = #{idx}
        ]]>
    </select>
<!--    sql 쿼리문 안에서 컬럼명이 각각 달라지는 이유                                                                                   -->
<!--        1. DB에서 사용하는 명명법(스네이크 명명법)과 java에서 사용하는 명명법(카멜 명명법)이 다름                                   -->
<!--        2. application.properties 를 통해서 카멜명명법을 사용한다고 설정 (mybatis.configuration.map-underscore-to-camel-case=true)  -->
<!--        3. parameterType, resultType에 Java dto class를 사용한다고 설정했기 때문에 해당 클래스의 멤버 변수명을 사용                 -->
<!--        4. Mapper 파일에서 @Param 어노테이션을 사용하여 사용하여 변수명을 설정할 경우 해당 변수명을 사용해야 함(parameterType="com.bitc.board.dto.BoardDto") -->
<!--    userGeneratedKeys : DBMS가 자동 키 생성을 지원할 경우 자동 키 생성을 사용하겠다는 의미 -->
<!--    keyProperty : 자동으로 생성된 키를 받아서 지정한 컬럼으로 되돌려 줌 -->
    <insert id="insertBoard" parameterType="com.bitc.board.dto.BoardDto" useGeneratedKeys="true" keyProperty="idx">
        <![CDATA[
        INSERT INTO t_board (title, contents, user_id, pwd, create_dt)
        VALUES (#{title}, #{contents}, #{userId}, '1234', NOW())
        ]]>
    </insert>

    <update id="updateBoard" parameterType="com.bitc.board.dto.BoardDto">
        <![CDATA[
        UPDATE t_board
        SET
            title = #{title},
            contents = #{contents},
            Update_dt = NOW()
        WHERE
            idx = #{idx}
        ]]>
    </update>

    <update id="deleteBoard" parameterType="int">
        <![CDATA[
        UPDATE t_board SET deleted_yn = 'Y'
        WHERE idx = #{idx}
        ]]>
    </update>

    <update id="updateHitCount" parameterType="int">
        <![CDATA[
        UPDATE t_board SET hit_cnt = hit_cnt + 1
        WHERE idx = #{idx}
        ]]>
    </update>

<!--    list형으로 받아주기때문에 수정이 필요함 : VALUES 에서 ']]>' 으로 닫아주고 foreach 태그 적용 -->
<!--    insertBoardFileList 메서드는 매개변수로 BoardFileDto 클래스 타입의 ArrayList 를 받아서 사용함 -->
<!--    해당 리스트가 가지고 있는 모든 내용을 사용하기 위해서 foreach 를 사용하여 반복 실행해야 함 -->
<!--    collection : 매개변수로 사용하는 반복가능한 객체의 타입을 설정, list/array 사용 가능 -->
<!--    item : collection 을 통해서 생성되는 객체의 변수명 설정 -->
<!--    separator : 데이터의 구분자 설정 -->
    <insert id="insertBoardFileList" parameterType="com.bitc.board.dto.BoardFileDto">
        <![CDATA[
            INSERT INTO t_file
                (board_idx, original_file_name, stored_file_path, file_size, create_id, create_date)
            VALUES
        ]]>
        <foreach collection = "list" item="item" separator=",">
            (#{item.boardIdx}, #{item.originalFileName}, #{item.storedFilePath}, #{item.fileSize}, 'admin', NOW())
        </foreach>
    </insert>

    <select id="selectBoardFileList" resultType="com.bitc.board.dto.BoardFileDto">
        <![CDATA[
            SELECT
                idx, board_idx, original_file_name,
                FORMAT(ROUND(file_size / 1024), 0) AS file_size
            FROM
                t_file
            WHERE board_idx = #{boardIdx}
            AND
                deleted_yn = 'N'
        ]]>
    </select>

    <select id="selectBoardFileInfo" parameterType="map" resultType="com.bitc.board.dto.BoardFileDto">
        <![CDATA[
        SELECT
            original_file_name, stored_file_path, file_size
        FROM
            t_file
        WHERE
            idx = #{idx}
        AND
            board_idx = #{boardIdx}
        AND
            deleted_yn = 'N'
        ]]>
    </select>

</mapper>

FileUtils

package com.bitc.board.common;

import com.bitc.board.dto.BoardFileDto;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import java.io.File;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

@Component
public class FileUtils {
    public List<BoardFileDto> parseFileInfo(int boardIdx, MultipartHttpServletRequest uploadFiles) throws Exception {
//        업로드된 파일 정보 확인
        if (ObjectUtils.isEmpty(uploadFiles)) {
            return null; // 파일 정보가 없으면 종료
        }
        
        List<BoardFileDto> fileList = new ArrayList<>();
        
//        파일 저장 폴더 생성
//        날짜를 년월을 형태로 생성
        DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyyMMdd");
//        현재 서버에 설정된 지역 정보를 기반으로 현재 시간을 가져옴
        ZonedDateTime current = ZonedDateTime.now();
//        전체 경로 생성, images 폴더 아래에 현재 날짜로 된 폴더를 경로로 지정
        String path = "images/" + current.format(format);

//        지정한 경로로 파일 클래스 타입의 객체 생성
        File file = new File(path);
//        지정한 경로가 존재하는지 여부 확인
        if (file.exists() == false) { // file.exists() : 있으면 true, 없으면 false
            file.mkdirs(); // 파일이 있으면 그냥 넘어가고, 없으면 폴더 생성
        }

//        업로드 된 파일 정보에서 모든 파일 이름을 가져옴
        Iterator<String> iterator = uploadFiles.getFileNames();

        String newFileName; // 새로운 파일 이름을 저장하기 위한 변수
        String originalFileExtension; // 확장자를 저장하기 위한 변수
        String contentType; // 파일 타입을 저장하기 위한 변수

//        업로드 된 모든 파일 정보를 가져옴
        while (iterator.hasNext()) {
//            String name = iterator.next();
//            uploadFiles. ~~  방식 안쓰고 아래 방식으로 편하게 이용
//            ↓ 가져온 이름을 기반으로 파일 정보를 가져옴
            List<MultipartFile> list = uploadFiles.getFiles(iterator.next()); // uploadFiles에 들어있는 데이터를 리스트에 저장

            for (MultipartFile multipartFile : list) {
                if (multipartFile.isEmpty() == false) { // multipartFile 이 비었는지 확인
                    contentType = multipartFile.getContentType(); // 안비었으면 getContentType() : 확장자 정보를 가져옴

                    if (ObjectUtils.isEmpty(contentType)) { // 비어있으면 break
                        break;
                    }
//                    안 비어있으면 확인된 확장자 타입에 따라서 확장자명 추가
                    else {
                        if (contentType.contains("image/jpeg")) {
                            originalFileExtension = ".jpeg";
                        } else if (contentType.contains("image/png")) {
                            originalFileExtension = ".png";
                        } else if (contentType.contains("image/gif")) {
                            originalFileExtension = ".gif";
                        }
                        else {
                            break;
                        }
                    }

//                   System.nanoTime() : 자바 언어에서 현재시간을 기준으로 1/1000초 까지 계산된 시간 정보를 가져옴
//   + originalFileExtension(.확장자명)   서버에 파일 저장 시, 동일한 이름의 파일을 저장할 수 없으므로,
//                                       현재 시간을 기준으로 파일 이름을 변경하여 저장하는 방식을 사용함.
//                                       동일한 시간에 서버에 접속하여 파일을 업로드 하는 경우가 발생할 수 있으므로
//                                       최대한 파일 이름의 중복을 피하기 위해서 nanoTime()을 사용하여 이름의 중복을 피함
//

                    newFileName = Long.toString(System.nanoTime()) + originalFileExtension;

//                    파일 정보를 저장하기 위한 BoardFileDto 클래스 타입의 객체 생성
                    BoardFileDto boardFile = new BoardFileDto();
                    boardFile.setBoardIdx(boardIdx); // 현재 게시물 번호 저장
                    boardFile.setFileSize(multipartFile.getSize()); // 파일 크기 저장
                    boardFile.setOriginalFileName(multipartFile.getOriginalFilename()); // 실제 파일명 저장
                    // 위에서 생성한 파일 폴더 경로와 새로 생성된 중복방지를 위한 이름을 합해서 저장 : 전체 경로
                    boardFile.setStoredFilePath(path + "/" + newFileName);

//                    실제 데이터베이스에 저장하기 위한 파일 정보를 가지고 잇는 개체 리스트에 저장
                    fileList.add(boardFile);

//                    파일 객체 생성
                    file = new File(path + "/" + newFileName);
//                    실제 파일 정보를 바탕으로 서버에 실제로 파일을 저장
                    multipartFile.transferTo(file);
                }
            }
        }
//        위에서 생성한 파일 정보 리스트를 반환
        return fileList;
    }
}

WebMvcConfiguration

package com.bitc.board.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Bean
    public CommonsMultipartResolver multipartResolver() {
        CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();

//        기본 문자셋 설정
        commonsMultipartResolver.setDefaultEncoding("UTF-8");
//        업로드 파일 최대 크기 설정, byte 크기로 설정하기 때문에 5 * 1024 * 1024 = 5MB
        commonsMultipartResolver.setMaxUploadSizePerFile(5 * 1024 * 1024);

        return commonsMultipartResolver;
    }
}

BoardService

package com.bitc.board.service;

import com.bitc.board.dto.BoardDto;
import com.bitc.board.dto.BoardFileDto;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import java.util.List;

public interface BoardService {

    List<BoardDto> selectBoardList() throws Exception; // 구현체 : xml파일

    BoardDto selectBoardDetail(int idx) throws Exception;

    void insertBoard(BoardDto board, MultipartHttpServletRequest multipart) throws Exception;

    void updateBoard(BoardDto board) throws Exception;

    void deleteBoard(int idx) throws Exception;

    BoardFileDto selectBoardFileInfo(int idx, int boardIdx) throws Exception;
}

Board1Application

package com.bitc.board;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration;

//@SpringBootApplication <- 주석처리
// exclude : 옵션을 사용하여 MultipartAutoConfiguration 클래스의 자동 구성을 사용하지 않도록 설정
@SpringBootApplication(exclude = {MultipartAutoConfiguration.class})
public class Board1Application {

	public static void main(String[] args) {
		SpringApplication.run(Board1Application.class, args);
	}

}

0개의 댓글