커뮤니티 - 게시글 작성 기능 구현 (23.07.26)

·2023년 7월 26일
0

Server

목록 보기
31/35
post-thumbnail

📝 게시글 작성 기능


💡 VS Code

🔎 boardWriteForm.jsp

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>게시글 등록</title>

    <link rel="stylesheet" href="${contextPath}/resources/css/boardWriteForm-style.css">

    <link rel="stylesheet" href="${contextPath}/resources/css/main-style.css">

    <script src="https://kit.fontawesome.com/4dca1921b4.js" crossorigin="anonymous"></script>
</head>
<body>
    <main>

        <jsp:include page="/WEB-INF/views/common/header.jsp"/>

        <form action="write" enctype="multipart/form-data" method="POST" class="board-write"
            onsubmit="return writeValidate()">

            <!-- 제목 -->
            <h1 class="board-title">
                <input type="text" name="boardTitle" placeholder="제목을 입력해 주세요.">
            </h1>

            <!-- 썸네일 -->
            <h5>썸네일</h5>
            <div class="img-box">
                <div class="boardImg thumbnail">
                    <label for="img0">
                        <img class="preview">
                    </label>
                    <input type="file" class="inputImage" id="img0" name="0" accept="image/*">
                    <span class="delete-image">&times;</span>
                    <!-- &times; : x 모양의 문자 -->
                </div>
            </div>

            <!-- 업로드 이미지 -->
            <h5>업로드 이미지</h5>
            <div class="img-box">

                <div class="boardImg">
                    <label for="img1">
                        <img class="preview">
                    </label>
                    <input type="file" class="inputImage" id="img1" name="1" accept="image/*">
                    <span class="delete-image">&times;</span>
                </div>

                <div class="boardImg">
                    <label for="img2">
                        <img class="preview">
                    </label>
                    <input type="file" class="inputImage" id="img2" name="2" accept="image/*">
                    <span class="delete-image">&times;</span>
                </div>

                <div class="boardImg">
                    <label for="img3">
                        <img class="preview">
                    </label>
                    <input type="file" class="inputImage" id="img3" name="3" accept="image/*">
                    <span class="delete-image">&times;</span>
                </div>

                <div class="boardImg">
                    <label for="img4">
                        <img class="preview">
                    </label>
                    <input type="file" class="inputImage" id="img4" name="4" accept="image/*">
                    <span class="delete-image">&times;</span>
                </div>

            </div>

            <!-- 내용 -->
            <div class="board-content">
                <textarea name="boardContent"></textarea>
            </div>

            <!-- 버튼 영역 -->
            <div class="board-btn-area">
                <button type="submit" id="writeBtn">등록</button>
                <button type="button" id="goToListBtn">목록으로</button>
            </div>

            <!-- 숨겨진 값(hidden) -->
            <!-- 동작 구분 -->
            <input type="hidden" name="mode" value="${param.mode}">

            <!-- 게시판 구분 -->
            <input type="hidden" name="type" value="${param.type}">

        </form>

    </main>

    <jsp:include page="/WEB-INF/views/common/footer.jsp"/>

    <script src="${contextPath}/resources/js/board/board.js"></script>
    <script src="${contextPath}/resources/js/board/boardWriteForm.js"></script>
</body>
</html>

🔎 boardWriteForm-style.css

/* 게시글 작성 전체 영역 */
.board-detail{
    width: 1000px;
    min-height: 700px;
    border: 1px solid #ccc;
    margin: 50px auto;
    padding: 20px;
}

/* 게시글 제목 */
.board-title{
    margin: 0;
    padding: 20px 0;
    border-bottom: 3px solid #ccc;
}

/* 게시글 제목 input */
.board-title > input{
    width: 100%;
    font-size: 24px;
    padding: 5px 0;
    border: 0;
    outline: 0;
}

/* 게시글 제목 input에 포커스가 맞춰졌을 때 */
/* :focus-within : 자식 input요소에 포커스가 맞춰졌을 때 */
.board-title:focus-within{
    border-bottom-color: #455ba8;
}

/* 이미지 영역 */
.img-box{
    display: flex;
    justify-content: space-between;
}

/* 이미지를 감싸는 div */
.boardImg{
    width: 230px;
    height: 230px;
    display: inline-block;
    text-align: center;

    position: relative;
}

/* 미리보기 영역 */
.boardImg > label{
    width: 100%;
    height: 100%;

    display: flex;
    justify-content: center;
    align-items: center;

    border: 1px solid #ccc;
    cursor: pointer;
}

/* file 타입 input */
.inputImage{ display: none; }

/* 미리보기 이미지 */
.preview{
    max-width: 100%;
    max-height: 100%;
    /* 지정된 칸 내부에 비율을 변경하지 않고 이미지 배치 */
}

/* 이미지 미리보기 삭제 */
.delete-image{
    position: absolute;
    top: 0;
    right: 7px;
    font-size: 20px;
    cursor: pointer;
}

/* 썸네일만 영역 크기 증가 */
.thumbnail{
    width: 300px;
    height: 300px;
}

/* 내용 */
.board-content{
    padding: 30px 0;
    margin: 30px 0;
    border-top: 3px solid #ccc ;
    border-bottom: 3px solid #ccc ;
}

.board-content > textarea{
    width: 100%;
    height: 400px;
    resize: none;
    font-size: 18px;
}

/* 버튼 영역 */
.board-btn-area{
    text-align: right;
}

/* 버튼 */
.board-btn-area button{
    width: 80px;
    height: 30px;

    font-weight: bold;
    border: 0;
    background-color: #455ba8;
    color : white;

    cursor: pointer;
}

.board-btn-area button:hover{
    background-color: white;
    color: #455ba8;
    border: 2px solid #455ba8;
}

🔎 boardWriteForm.js

// 미리보기 관련 요소 모두 얻어오기

const inputImage = document.getElementsByClassName("inputImage");
const preview = document.getElementsByClassName("preview");
const deleteImage = document.getElementsByClassName("delete-image");

for(let i=0; i<inputImage.length; i++){

    // 파일이 선택되었을 때
    inputImage[i].addEventListener("change", function(){

        if(this.files[0] != undefined){ // 파일이 선택되었을 때          
            const reader = new FileReader(); // 선택된 파일을 읽을 객체 생성
            reader.readAsDataURL(this.files[0]);
            // 지정된 파일을 읽음 -> result에 저장(URL 포함) -> URL을 이용해서 이미지 볼 수 있음

            reader.onload = function(e){ // reader가 파일을 다 읽어온 경우
                // e.target == reader
                // e.target.result == 읽어들인 이미지의 URL
                // preview[i] == 파일이 선택된 input태그와 인접한 preview 이미지 태그
                preview[i].setAttribute("src", e.target.result);
            }
        } else { // 파일이 선택되지 않았을 때 (취소)
            preview[i].removeAttribute("src"); // src 속성 제거

        }

    });

    // 미리보기 삭제 버튼(x)이 클릭되었을 때의 동작
    deleteImage[i].addEventListener("click", function(){

        // 미리보기 삭제
        preview[i].removeAttribute("src");

        // input의 값을 "" 만들기
        inputImage[i].value = "";
    })

}

// 게시글 작성 유효성 검사
function writeValidate(){
    const boardTitle = document.getElementsByName("boardTitle")[0];
    const boardContent = document.querySelector("[name='boardContent']");

    if(boardTitle.value.trim().length == 0){
        alert("제목을 입력해 주세요!!!");
        boardTitle.value = "";
        boardTitle.focus();
        return false;
    }

    if(boardContent.value.trim().length == 0){
        alert("내용을 입력해 주세요!!!");
        boardContent.value = "";
        boardContent.focus();
        return false;
    }

    return true;
}

🔎 boardList.jsp

...
            <div class="btn-area">

                <c:if test="${!empty loginMember}">
                    <!-- /community/board/write -->
                    <button id="insertBtn" onclick="location.href='write?mode=insert&type=${param.type}'">글쓰기</button>
                </c:if>

            </div>
...

🔎 board.js

...
            const type = "type=" + params.get("type"); // type=1
            let cp;
            
            if(params.get("cp") != null){ // 쿼리스트링에 cp가 있을 경우
               cp = "cp=" + params.get("cp");
            } else{
                cp = "cp=1";
            }
...

🔎 boardDetail.jsp

...
            <!-- 내용 -->
            <div class="board-content">
                ${detail.boardContent}
            </div>
...

🔎 boardList.jsp

...
            <div class="btn-area">

                <c:if test="${!empty loginMember}">
                    <!-- /community/board/write -->
                    <button id="insertBtn" onclick="location.href='write?mode=insert&type=${param.type}&cp=${param.cp}'">글쓰기</button>
                </c:if>

            </div>
...

💡 Eclipse

🔎 BoardWriteController.java

package edu.kh.community.board.controller;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.oreilly.servlet.MultipartRequest;

import edu.kh.community.board.model.service.BoardService;
import edu.kh.community.board.model.vo.BoardDetail;
import edu.kh.community.board.model.vo.BoardImage;
import edu.kh.community.common.MyRenamePolicy;
import edu.kh.community.member.model.vo.Member;

// 컨트롤러 : 요청에 따라 알맞은 Service를 호출하고 결과에 따라 응답을 제어
@WebServlet("/board/write")
public class BoardWriteController extends HttpServlet {

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		
		try {
			String mode = req.getParameter("mode"); // insert / update 구분
			
			// insert는 별도 처리 없이 jsp로 위임
			
			// update는 기존 게시글 내용을 조회하는 처리가 필요함
			if(mode.equals("update")) {
				
			}
			
			String path = "/WEB-INF/views/board/boardWriteForm.jsp";
			req.getRequestDispatcher(path).forward(req, resp);
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		
		try {
			// insert/update 구분 없이 전달받은 파라미터 모두 꺼내서 정리하기
			
			// *** enctype="multipart/form-data" 인코딩 미지정 형식의 요청
			// -> HttpServletRequest로 파라미터 얻어오기 불가능
			// --> MultipartRequest를 이용(cos.jar 라이브러리 제공)			
			// ---> 업로드 최대 용량, 저장 실제 경로, 파일명 변경 정책, 문자 파라미터 인코딩 설정 필요
			
			int maxSize = 1024 * 1024 * 100; // 업로드 최대 용량 (100MB)

			HttpSession session = req.getSession(); // session 얻어오는 것은 지장 없음(사용 가능)
			String root = session.getServletContext().getRealPath("/"); // webapp 폴더까지 경로
			String folderPath = "/resources/images/board/"; // 파일 저장 폴더 경로
			String filePath = root + folderPath;

			String encoding = "UTF-8"; // 파라미터 중 파일 제외 파라미터(문자열)의 인코딩 지정
			
			// ** MultipartRequest 객체 생성 **
			// -> 객체가 생성됨과 동시에 파라미터로 전달된 파일이 지정된 경로에 저장(업로드)된다.
			MultipartRequest mpReq = new MultipartRequest(req, filePath, maxSize, encoding, new MyRenamePolicy());
			
			// MultipartRequest.getFileNames()
			// - 요청 파라미터 중 모든 file 타입 태그의 name 속성 값을 얻어와
			// 	 Enumeration 형태로 반환 (Iterator의 과거 버전)
			// 	 --> 해당 객체에 여러 값이 담겨 있고 이를 순서대로 얻어오는 방법을 제공
			//		 (보통 순서가 없는 모음(Set과 같은 경우)에서 하나씩 꺼낼 때 사용)
			
			Enumeration<String> files = mpReq.getFileNames();
			// file 타입 태그의 name 속성 0,1,2,3,4가 순서가 섞인 상태로 얻어와짐
			
			// * 업로드된 이미지의 정보를 모아둘 List 생성
			List<BoardImage> imageList = new ArrayList<BoardImage>();
			
			while(files.hasMoreElements()) { // 다음 요소가 있으면 true
				String name = files.nextElement(); // 다음 요소(name 속성 값)를 얻어옴
				
//				System.out.println("name : " + name);
				// file 타입 태그의 name 속성 값이 얻어와짐
				// + 업로드가 안 된 file 타입 태그의 name도 얻어와짐
				
				String rename = mpReq.getFilesystemName(name); 	   	// 변경된 파일명
				String original = mpReq.getOriginalFileName(name); 	// 원본 파일명
				
//				System.out.println("rename : " + rename);
//				System.out.println("original : " + original);
			
				if(rename != null) { // 업로드된 파일이 있을 경우 ==
									 // 현재 files에서 얻어온 name속성을 이용해
									 // 원본 또는 변경을 얻어왔을 때 그 값이 null이 아닌 경우
					
					// 이미지 정보를 담은 객체(BoardImage)를 생성
					BoardImage image = new BoardImage();
					
					image.setImageOriginal(original); // 원본명 (다운로드 시 사용)
					image.setImageReName(folderPath + rename); // 폴더 경로 + 변경명
					image.setImageLevel( Integer.parseInt(name) ); // 이미지 위치(0은 썸네일)
					
					imageList.add(image); // 리스트에 추가
					
				} // if 끝
				
			} // while 끝
			
			// * 이미지를 제외한 게시글 관련 정보 *
			String boardTitle = mpReq.getParameter("boardTitle");
			String boardContent = mpReq.getParameter("boardContent");
			int boardCode = Integer.parseInt(mpReq.getParameter("type")); // hidden (게시판 구분을 위해 만들어 놓음)
			
			Member loginMember = (Member)session.getAttribute("loginMember");
			int memberNo = loginMember.getMemberNo(); // 회원 번호
			
			// 게시글 관련 정보를 하나의 객체(BoardDetail)에 담기
			BoardDetail detail = new BoardDetail();
			
			detail.setBoardTitle(boardTitle);
			detail.setBoardContent(boardContent);
			detail.setMemberNo(memberNo);
			// boardCode는 별도 매개변수로 전달 예정

			// ----------------- 게시글 작성에 필요한 기본 파라미터 얻어오기 끝 ----------------- //
			
			BoardService service = new BoardService();

			// 모드 (insert/update)에 따라 추가 파라미터 얻어오기 및 서비스 호출
			String mode = mpReq.getParameter("mode"); // hidden
			
			if(mode.equals("insert")) { // 삽입
				
				// 게시글 삽입 서비스 호출 후 결과 반환 받기
				// -> 반환된 게시글 번호를 이용해서 상세조회로 리다이렉트 예정
				int boardNo = service.insertBoard(detail, imageList, boardCode);
				
				String path = null;
				
				if(boardNo > 0) { // 성공
					session.setAttribute("message", "게시글이 등록되었습니다.");
					path = "detail?no=" + boardNo + "&type=" + boardCode;
					
				} else { // 실패
					session.setAttribute("message", "게시글 등록 실패");
					
					path = "write?mode=" + mode + "&type=" + boardCode;
				}
				
				resp.sendRedirect(path); // 리다이렉트
			}
			
			if(mode.equals("update")) { // 수정
				
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	
	}
}

🔎 BoardService.java

	/** 게시글 등록 Service
	 * @param detail
	 * @param imageList
	 * @param boardCode
	 * @return boardNo
	 * @throws Exception
	 */
	public int insertBoard(BoardDetail detail, List<BoardImage> imageList, int boardCode) throws Exception {
		
		Connection conn = getConnection();
		
		// 1. 다음 작성할 게시글 번호 얻어오기
		// -> BOARD 테이블 INSERT / BOARD_IMG 테이블 INSERT / 반환값 (상세조회 번호)
		int boardNo = dao.nextBoardNo(conn);
		
		// 2. 게시글 부분만 삽입(detail, boardCode 사용)
		detail.setBoardNo(boardNo); // 조회된 다음 게시글 번호 세팅
		
		// 1) XSS 방지 처리(제목/내용)
		detail.setBoardTitle( Util.XSSHandling(detail.getBoardTitle()) );
		detail.setBoardContent( Util.XSSHandling(detail.getBoardContent()) );
		
		// 2) 개행문자 처리
		detail.setBoardContent( Util.newLineHandling(detail.getBoardContent()) );
		
		int result = dao.insertBoard(conn, detail, boardCode);
		
		if(result > 0) { // 게시글 삽입 성공 시
			
			// 3. 이미지 정보만 삽입(imageList 사용)
			for(BoardImage image : imageList) { // 하나씩 꺼내서 DAO 수행
				image.setBoardNo(boardNo); // 게시글 번호 세팅
				
				result = dao.insertBoardImage(conn, image);
				
				if(result == 0) { // 이미지 삽입 실패
					break;
					
				}
			} // for문 끝
		} // if문 끝
		
		// 트랜잭션
		if(result > 0) {
			commit(conn);
			
		} else { // 2,3번에서 한 번이라도 실패한 경우
			rollback(conn);
			boardNo = 0; // 게시글 번호를 0으로 바꿔서 실패했음을 컨트롤러로 전달
		}
		
		close(conn);
		
		return boardNo;
	}

🔎 BoardDAO.java

...
	/** 게시글에 첨부된 이미지 리스트 조회 DAO
	 * @param conn
	 * @param boardNo
	 * @return imageList
	 * @throws Exception
	 */
	public List<BoardImage> selectImageList(Connection conn, int boardNo) throws Exception {
		
		List<BoardImage> imageList = new ArrayList<>();
		
		try {
			String sql = prop.getProperty("selectImageList");
			
			pstmt = conn.prepareStatement(sql);
			
			pstmt.setInt(1, boardNo);
			
			rs = pstmt.executeQuery();
			
			while(rs.next()) {
				
				BoardImage image = new BoardImage();
	            
	            image.setImageNo(rs.getInt(1));
	            image.setImageReName(rs.getString(2));
	            image.setImageOriginal(rs.getString(3));
	            image.setImageLevel(rs.getInt(4));
	            image.setBoardNo(rs.getInt(5));
	            
	            imageList.add(image);
			}
			
		} finally {
			close(rs);
			close(pstmt);
		}
		
		return imageList;
	}

	/** 다음 게시글 번호 조회 DAO
	 * @param conn
	 * @return boardNo
	 * @throws Exception
	 */
	public int nextBoardNo(Connection conn) throws Exception {
		
		int boardNo = 0;
		
		try {
			String sql = prop.getProperty("nextBoardNo");
			
			stmt = conn.createStatement();
			rs = stmt.executeQuery(sql);
			
			if(rs.next()) {
				boardNo = rs.getInt(1);
			}
			
		} finally {
			close(rs);
			close(stmt);
		}
		
		return boardNo;
	}

	/** 게시글 삽입 DAO
	 * @param conn
	 * @param detail
	 * @param boardCode
	 * @return result
	 * @throws Exception
	 */
	public int insertBoard(Connection conn, BoardDetail detail, int boardCode) throws Exception {
		
		int result = 0;
		
		try {
			String sql = prop.getProperty("insertBoard");
		
			pstmt = conn.prepareStatement(sql);
			
			pstmt.setInt(1, detail.getBoardNo());
			pstmt.setString(2, detail.getBoardTitle());
			pstmt.setString(3, detail.getBoardContent());
			pstmt.setInt(4, detail.getMemberNo());
			pstmt.setInt(5, boardCode);
			
			result = pstmt.executeUpdate();
			
		} finally {
			close(pstmt);
		}
		
		return result;
	}

	/** 게시글 이미지 삽입 DAO
	 * @param conn
	 * @param image
	 * @return result
	 * @throws Exception
	 */
	public int insertBoardImage(Connection conn, BoardImage image) throws Exception {
		int result = 0;
		
		try {
			String sql = prop.getProperty("insertBoardImage");
			
			pstmt = conn.prepareStatement(sql);
			
			pstmt.setString(1, image.getImageReName());
			pstmt.setString(2, image.getImageOriginal());
			pstmt.setInt(3, image.getImageLevel());
			pstmt.setInt(4, image.getBoardNo());
			
			result = pstmt.executeUpdate();
			
		} finally {
			close(pstmt);
		}
		
		return result;
	}

🔎 board-sql.xml

...
	<!-- 게시글에 첨부된 이미지 리스트 조회 -->
	<entry key="selectImageList">
		SELECT * FROM BOARD_IMG
		WHERE BOARD_NO = ?
		ORDER BY IMG_LEVEL
	</entry>
	
	<!-- 다음 게시글 번호 조회 -->
	<entry key="nextBoardNo">
		SELECT SEQ_BOARD_NO.NEXTVAL FROM DUAL
	</entry>
	
	<!-- 게시글 삽입 -->
	<entry key="insertBoard">
		INSERT INTO BOARD
		VALUES(?, ?, ?, DEFAULT, DEFAULT, DEFAULT, DEFAULT, ?, ?)
	</entry>
	
	<!-- 게시글 이미지 삽입  -->
	<entry key="insertBoardImage">
		INSERT INTO BOARD_IMG
		VALUES(SEQ_IMG_NO.NEXTVAL, ?, ?, ?, ?)
	</entry>

📌 출력 화면

게시글을 작성하고 '등록' 버튼을 클릭하면

위와 같은 알림창이 출력된다.

이후 목록으로 돌아가 보면 방금 작성한 게시글이 게시되어 있는 모습을 볼 수 있다. 👌

profile
풀스택 개발자 기록집 📁

0개의 댓글