프로젝트

11. 공지사항 게시판 (유효성 검사, 파일 업로드)

하차모 2023. 5. 20. 01:09

글 제목, 내용 유효성 검사

notice_form.js

function noticeFormCheck() {
	const boardTitle = document.querySelector('#boardTitle').value;
	const boardContent = document.querySelector('#boardContent').value;

	
	if(boardTitle == '' || boardTitle == null) {
		alert('제목을 입력해주세요.')
		return false;
	};
	
	if(boardContent == '' || boardContent == null) {
		alert('내용을 입력해주세요.')
		return false;
	};
	
	document.querySelector('#noticeForm').submit();
}

글 작성 시 유효성 검사는 간단하게 자바스크립트로 구현했다.

작성되지 않았을 때는 alert창이 뜨도록 했고, 다 작성되었을 경우 id값을 준 form태그를 지정해 submit 해주었음

 

 

파일 첨부 (업로드)

내용 밑 칸에 첨부파일 칸을 만들었다.

파일 추가 버튼을 누르면 파일 선택 input태그가 있는 div 태그가 하나씩 추가되도록 하고, 삭제를 누르면 해당 인풋 태그를 포함하는 div가 삭제된다.

이때 input 태그의 type은 file으로, name값은 controller에서 받을 이름인 files로 넣어준다.

form태그에 enctype="multipart/form-data” 속성을 추가한다.

 

notice_form.js

//파일 추가 버튼
function addFileInputDiv(buttonTag) {
	fileTd = buttonTag.parentElement;
	
	let str = '';
	str += `<div class="input-group my-2">                                              `;
	str += `	<input class="form-control form-control-sm" type="file" name="files" id="fileInput">                   `;
	str += `	<button class="btn btn-outline-secondary btn-sm" type="button" onclick="deleteFileInputDiv(this);">삭제</button>`;
	str += `</div>                                                                      `;
	
	fileTd.insertAdjacentHTML('beforeend', str);
};

//파일 태그 삭제 버튼
function deleteFileInputDiv(deleteBtn) {
	fileInputDiv = deleteBtn.parentElement;
	
	fileInputDiv.remove();
};

 

 

 

이제 파일 첨부 기능에 사용할 UploadUtil 클래스를 만든다.

MultipartFile 객체를 사용하여 파일의 원본 파일명, 사이즈를 알 수 있음

서버에 올라갈 파일명은 UUID 객체를 사용해 생성한 랜덤한 문자열에 확장자를 더해 줌

File 객체를 이용해 파일 업로드를 해주고 파일 정보를 모두 담은 객체를 반환한다.

 

UploadUtil.java

package com.bykh.groupware.util;

import java.io.File;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import org.springframework.web.multipart.MultipartFile;

import com.bykh.groupware.notice.vo.BoardFileVO;

public class UploadUtil {

	//단일 파일 업로드 메소드
	public static BoardFileVO fileUpload(MultipartFile boardFile) {
		//반환할 파일 객체 생성
		BoardFileVO boardFileVO = null;
		
		if(!boardFile.isEmpty()) {
			boardFileVO = new BoardFileVO();
			
			//원본 파일명
			String originFileName = boardFile.getOriginalFilename();
			
			//서버에 올라갈 파일명 생성(랜덤한 문자열 생성)
			String uuid = UUID.randomUUID().toString();
			
			//첨부된 파일의 확장자 추출
			int index = originFileName.lastIndexOf(".");
			String extension = originFileName.substring(index);
			
			//첨부될 파일명
			String attachedFileName = uuid + extension;
			
			//파일 사이즈
			long fileSize = boardFile.getSize();
			String fileFancySize = fancySize(fileSize);
			
			
			//파일 업로드
			try {
				File file = new File(ConstVariable.BOARD_UPLOAD_PATH + attachedFileName);
				boardFile.transferTo(file);
				
				boardFileVO.setOriginFileName(originFileName);
				boardFileVO.setAttachedFileName(attachedFileName);
				boardFileVO.setFileSize(fileFancySize);
			} catch (Exception e) {
				e.printStackTrace();
			}
			
		}
		
		return boardFileVO;
	}
	
	
	//다중 파일 업로드 메소드
	public static List<BoardFileVO> multiFileUpload(MultipartFile[] boardFiles) {
		List<BoardFileVO> boardFileVOList = new ArrayList<>();
		
		for(MultipartFile boardFile : boardFiles) {
		  	BoardFileVO vo = fileUpload(boardFile);
			boardFileVOList.add(vo);
		}
		
		return boardFileVOList;
	}
	
	private static DecimalFormat df = new DecimalFormat("#,###.0");

	private static String fancySize(long size) {
		if (size < 1024) { // 1k 미만
			return size + " Bytes";
		} else if (size < (1024 * 1024)) { // 1M 미만
			return df.format(size / 1024.0) + " KB";
		} else if (size < (1024 * 1024 * 1024)) { // 1G 미만
			return df.format(size / (1024.0 * 1024.0)) + " MB";
		} else {
			return df.format(size / (1024.0 * 1024.0 * 1024.0)) + " GB";
		}
	}
	
	
	
	
}

 

 

 

글 등록 시 파일 첨부 여부도 자바스크립트를 통해 유효성 검사를 해준다.

파일 input 태그를 모두 가져와서 value 값이 없다면 false를 반환한다.

 

notice_form.js

function noticeFormCheck() {
	const boardTitle = document.querySelector('#boardTitle').value;
	const boardContent = document.querySelector('#boardContent').value;

	
	if(boardTitle == '' || boardTitle == null) {
		alert('제목을 입력해주세요.')
		return false;
	};
	
	if(boardContent == '' || boardContent == null) {
		alert('내용을 입력해주세요.')
		return false;
	};
	
	document.querySelector('#noticeForm').submit();
}

 

 

 

 

form 태그의 post 메소드로 넘기면 controller에서 @PostMapping으로 받아준다.
메소드의 매개변수에 MultipartFile[] 을 받아준다.
첨부파일이 없을 때, 1개일 때, 여러 개일때 모두 배열로 받아준다.

만들었던 UploadUtil의 multiFileUpload 메소드를 사용해 파일을 업로드하고, 반환된 파일 정보 리스트를 받아 글 번호 데이터를 넣어준다.
(원래는 글 등록 insert 쿼리 안에 다음으로 들어갈 글 번호를 조회하는 select 쿼리를 넣어 한 번에 실행했는데, 첨부파일 VO에 글 번호를 넣어주기 위해서 select 쿼리를 따로 만들었다.)

첨부파일이 없는 경우 null 체크는 쿼리를 실행하는 serviceImpl 클래스에서 해준다.

NoticeController.java

//글 등록
@PostMapping("/regNotice")
public String regNotice(BoardVO boardVO, MultipartFile[] files) {
    // 글 등록
    String boardNum = noticeService.getNextBoardNum();
    boardVO.setBoardNum(boardNum);

    // 첨부파일
    if(files != null) {
        List<BoardFileVO> attachedBoardFileList = UploadUtil.multiFileUpload(files);

        // 첨부파일 등록 쿼리 실행 시 빈 값을 채워줄 데이터를 저장할 리스트에 boardNum 데이터 추가
        for(BoardFileVO boardfile : attachedBoardFileList) {
            boardfile.setBoardNum(boardNum);
        }

        // boardVO에 리스트 set
        boardVO.setBoardFileList(attachedBoardFileList);
    }

		noticeService.regNotice(boardVO);
		
		return "redirect:/notice/list";
	}

 

 

넘겨진 boardVO의 첨부파일 리스트가 null이 아닐 때만 파일 첨부 쿼리를 실행하도록 한다.

 

NoticeServiceImpl.java

//글 등록
@Override
@Transactional(rollbackFor = Exception.class)
public void regNotice(BoardVO boardVO) {
    //글 등록
    sqlSession.insert("boardMapper.regNotice", boardVO);

    //첨부파일 등록
    if(boardVO.getBoardFileList() != null) {
        sqlSession.insert("boardMapper.regFiles", boardVO);
    }
}

 

 

다음 글 번호 조회, 글 등록, 파일 등록 쿼리

 

board-mapper.xml

<!-- 다음으로 들어갈 글 번호 조회 -->
<select id="getNextBoardNum" resultType="String">
    SELECT 'BOARD_'||LPAD(NVL(MAX(TO_NUMBER(SUBSTR(BOARD_NUM, 7))), 0) + 1, 3, '0')
    FROM BOARD
</select>

<!-- 공지사항 글 등록 -->
<insert id="regNotice">
    INSERT INTO BOARD (
        BOARD_NUM
        , BOARD_TITLE
        , BOARD_CONTENT
        , BOARD_WRITER
        , BOARD_MENU_CODE
        <if test="isImportant != null">
        , IS_IMPORTANT
        </if>
    ) VALUES (
        #{boardNum}
        , #{boardTitle}
        , #{boardContent}
        , 20230517
        , 'BOARD_MENU_001'
        <if test="isImportant != null">
        , #{isImportant}
        </if>
    )
</insert>

<!-- 글 첨부파일 정보 등록 -->
<insert id="regFiles">
    INSERT INTO BOARD_FILE (
        FILE_NUM
        , ORIGIN_FILE_NAME
        , ATTACHED_FILE_NAME
        , FILE_SIZE
        , BOARD_NUM
    ) 
    <foreach collection="boardFileList" item="boardFile" index="i" separator="UNION ALL">
        SELECT
            (SELECT 'FILE_'||LPAD(NVL(MAX(TO_NUMBER(SUBSTR(FILE_NUM, 6))), 0) + 1 + #{i}, 3, '0')
                FROM BOARD_FILE)
            , #{boardFile.originFileName}
            , #{boardFile.attachedFileName}
            , #{boardFile.fileSize}
            , #{boardFile.boardNum}
        FROM DUAL
    </foreach>
</insert>

 

 

 

글 상세 조회 + 첨부파일 조회

 

 

첨부 파일 리스트를 조회하여 반환할 게시글 객체의 boardFileList 변수에 담아줌

 

NoticeServiceImpl.java

//공지글 상세 조회
@Override
@Transactional(rollbackFor = Exception.class)
public BoardVO getNoticeDetail(BoardVO boardVO) {
    //조회수 증가
    sqlSession.update("boardMapper.updateBoardView", boardVO);

    //첨부파일 조회
    List<BoardFileVO> selectedFileList = sqlSession.selectList("boardMapper.getBoardFile", boardVO);

    BoardVO selectedBoard = sqlSession.selectOne("boardMapper.getNoticeDetail", boardVO);

    selectedBoard.setBoardFileList(selectedFileList);

    //글 상세 조회 정보 반환
    return selectedBoard;
}

 

 

 

원래는 글 상세 조회 쿼리에 파일 정보까지 조회하는 쿼리를 짜다가..

이전글, 다음글 조회 기능과 합쳐지니까 파일이 없는 글을 조회하기가 까다로워서 아예 분리시켰다.

첨부한 파일이 없어서 빈 리스트가 넘어가더라도 문제 없다.

대신 html에서 타임리프 if문으로 ‘첨부파일이 없습니다.’ 문구가 뜨도록 했음

 

board-mapper.xml

<!-- 글 상세 조회 -->
<select id="getNoticeDetail" resultMap="board">
SELECT * FROM
    (SELECT BOARD_NUM
        , BOARD_TITLE
        , BOARD_CONTENT
        , BOARD_WRITER
        , TO_CHAR(BOARD_DATE, 'YYYY-MM-DD HH24:MI') BOARD_DATE
        , BOARD_VIEW
        , IS_IMPORTANT
        , ENAME
        , BOARD_MENU_CODE
        , LAG(BOARD_NUM, 1, 'BOARD_000') OVER(PARTITION BY BOARD_MENU_CODE ORDER BY BOARD_NUM) AS BOARD_PREV_NUM
        , LAG(BOARD_TITLE, 1, '이전 글이 없습니다.') OVER(PARTITION BY BOARD_MENU_CODE ORDER BY BOARD_NUM) AS BOARD_PREV_TITLE
        , LEAD(BOARD_NUM, 1, 'BOARD_000') OVER(PARTITION BY BOARD_MENU_CODE ORDER BY BOARD_NUM) AS BOARD_NEXT_NUM
        , LEAD(BOARD_TITLE, 1, '다음 글이 없습니다.') OVER(PARTITION BY BOARD_MENU_CODE ORDER BY BOARD_NUM) AS BOARD_NEXT_TITLE
    FROM BOARD, EMP
    WHERE BOARD_WRITER = EMPNO)
    WHERE BOARD_NUM = #{boardNum}
</select>

<!-- (상세 조회) + 첨부파일 조회 -->
<select id="getBoardFile" resultMap="boardFile">
    SELECT FILE_NUM
        , ORIGIN_FILE_NAME
        , ATTACHED_FILE_NAME
        , FILE_SIZE
    FROM BOARD, BOARD_FILE
    WHERE BOARD.BOARD_NUM = BOARD_FILE.BOARD_NUM
    AND BOARD.BOARD_NUM = #{boardNum}
</select>