백엔드/웹개발

[이클립스/웹개발] MVC를 활용한 답변형 게시판 구현 (1) - 게시판 글목록 보기

김곰댕 2022. 3. 3. 10:03
728x90

게시판 기능은 모든 웹 페이지의 기본 기능을 포함한다. 구현할 답변형 게시판의 글 목록은 아래의 사진과 같고, 부모 글이 목록에 나열되면 각 부모 글에 대해 답변 글(자식 글)이 계층 구조로 나열되어 표시된다. 그리고 답변 글에 대한 답변 글은 또 다시 계층 구조로 표시된다. 즉, 답변 글에 또 답변글을 올릴 수 있는 기능을 하는 게시판이다.


1. 답변형 게시판 설계

  • 답변형 게시판 테이블(t_board) 구조

게시판의 글을 작성하려면 회원이 로그인 상태여야 한다. 즉, 각 글에는 작성자 ID가 저장되며, 게시판 테이블의 ID 컬럼은 회원 테이블의 ID 컬럼에 대해 외래키를 속성으로 가진다.

no 칼럼이름 속성 자료형 크기 유일키 여부 NULL여부 기본값
1 ariticleNO 글 번호 number 10 Y N 기본키  
2 parentNO 부모 글 번호 number 10 N N   O
3 title 글 제목 varchar2 100 N N    
4 content 글 내용 varchar2 4000 N N    
5 imageFileName 이미지 파일 이름 varchar2 100 N      
6 writeDate 작성일 date   N N   sysdate
7 id 작성자ID varchar2 20 N N 외래키  

 

  • 테이블의 주요 속성
속성 컬럼 설명
글번호 articleNO 글이 추가될 때마다 1씩 증가되면서 고유 값을 부여
부모 글 번호 parentNO 답변을 단 부모 글의 번호를 나타냄. 부모 글 번호가 0이면 자신이 부모글
첨부 파일 이름 imageFileName 글 작성 시 첨부한 이미지 파일 이름
작성자 ID id 글을 작성한 작성자의 ID

 

  • 답변형 게시판 기능을 담당하는 클래스와 JSP의 MVC 구조

답변형 게시판 MVC 구조

뷰와 컨트롤러는 그대로 JSP와 서블릿이 기능을 수행하지만 모델은 기존의 DAO 클래스 외에 BoardService 클래스가 추가된 것을 볼 수 있다. DAO는 데이터베이스에 접근하는 기능을 수행하고 Service는 실제 프로그램을 업무에 적용하는 사용자 입장에서 업무 단위, 즉 트랜젝션(Transaction)으로 작업을 수행한다. 여기서 업무 단위란 '단위 기능'이라고도 하며, 사용자 입장에서는 하나의 논리적인 기능을 의미한다.

 

웹 애플리케이션에서 일반적으로 묶어서 처리하는 단위 기능

  • 게시판 글 조회 시 해당 글을 조회하는 기능과 조회수를 갱신하는 기능
  • 쇼핑몰에서 상품 주문 시 주문 상품을 테이블에 등록 후 주문자의 포인트를 갱신하는 기능
  • 은행에서 송금 시 송금자의 잔고를 갱신하는 기능과 수신자의 잔고를 갱신하는 기능

 

가장 흔한 게시판 기능

  • 새 글 쓰기
  • 글 보기
  • 글 수정하기
  • 글 삭제하기

2. 테이블 준비

1. 오라클에서 t_board테이블 생성

CREATE TABLE t_board(
    articleNO NUMBER(10) PRIMARY KEY,
    parentNO NUMBER(10) DEFAULT 0,
    title VARCHAR2(500) NOT NULL,
    content VARCHAR2(4000),
    imageFileName VARCHAR2(100),
    writedate DATE DEFAULT SYSDATE NOT NULL,
    id VARCHAR2(10),
    CONSTRAINT FK_ID FOREIGN KEY(id)
    REFERENCES t_member(id)
);
2. 내용 삽입

INSERT INTO T_BOARD(articleNO, parentNO, title, content, imageFileName, writedate, id) 
	values (1, 0, '테스트글입니다.', '테스트글입니다.', null, sysdate, 'hong');
INSERT INTO T_BOARD(articleNO, parentNO, title, content, imageFileName, writedate, id) 
	values (2, 0, '안녕하세요', '상품 후기입니다.', null, sysdate, 'hong');
INSERT INTO T_BOARD(articleNO, parentNO, title, content, imageFileName, writedate, id) 
	values (3, 2, '답변입니다.', '상품 후기에 대한 답변입니다.', null, sysdate, 'hong');
INSERT INTO T_BOARD(articleNO, parentNO, title, content, imageFileName, writedate, id) 
	values (5, 3, '답변입니다.', '상품 좋습니다.', null, sysdate, 'lee');
INSERT INTO T_BOARD(articleNO, parentNO, title, content, imageFileName, writedate, id) 
	values (4, 0, '김유신입니다.', '김유신 테스트글입니다.', null, sysdate, 'kim');
INSERT INTO T_BOARD(articleNO, parentNO, title, content, imageFileName, writedate, id) 
	values (6, 2, '상품 후기입니다...', '이순신씨의 상품 사용 후기 올립니다!!!.', null, sysdate, 'lee');
3. 나중에 자바에서 사용할 SQL문 점검해보기

SELECT LEVEL,
       articleNO,
       parentNO,
       LPAD(' ', 4*(LEVEL-1)) || title as title,
       content,
       writeDate,
       id
   FROM t_board
   START WITH parentNO=0
   CONNECT BY PRIOR articleNO = parentNO
   ORDER SIBLINGS BY articleNO DESC;
START WITH parentNO=0

계층형 구조에서 최상위 계층의 로우(row)를 식별하는 조건을 명시, parentNO가 0, 즉 부모글부터 시작해 계층형 구조를 만든다는 의미이다.

CONNECT BY PRIOR articleNO = parentNO

계층 구조가 어떤 식으로 연결되는지를 기술하는 부분,

PRIOR 자식컬럼 = 부모컬럼

ORDER SIBLINGS BY articleNO DESC;

ORDER SIGLINGS BY : 형제 노드 사이에서 정렬을 수행

계층 구조로 조회된 정보를 다시 articleNO를 이용해 내림차순으로 정렬하여 최종 출력


3. 게시판 글 목록 보기 구현

1. 아래와 같이 파일을 생성

2. BoardController 클래스에 다음과 같이 작성

/board/listAriticles.do로 요청시 화면에 글 목록을 출력하는 역할을 한다. getPathInfo() 메서드를 이용해 action 값을 가져오고 action 값이 null이거나 /listArticles.do일 경우 BoardService 클래스의 listAriticles() 메서드를 호출해 전체 글을 조회한다. 그리고 조회한 글을 articlesList 속성으로 바인딩하고 글 목록창(listArticles.jsp)로 포워딩 한다.

package sec03.brd01;

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

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/board/*")
public class BoardController extends HttpServlet {
	private static final long serialVersionUID = 1L;
	BoardService boardService;
	ArticleVO articleVO;

	public void init(ServletConfig config) throws ServletException {
		boardService = new BoardService(); //서블릿 초기화 시 BoardService 객체를 생성
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response)	throws ServletException, IOException {
		doHandle(request, response);
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response)	throws ServletException, IOException {
		doHandle(request, response);
	}

	private void doHandle(HttpServletRequest request, HttpServletResponse response)	throws ServletException, IOException {
		String nextPage = "";
		request.setCharacterEncoding("utf-8");
		response.setContentType("text/html; charset=utf-8");
		String action = request.getPathInfo(); //요청명을 가져옴
		System.out.println("action:" + action);
		try {
			List<ArticleVO> articlesList = new ArrayList<ArticleVO>();
			if (action == null) {
				articlesList = boardService.listArticles();
				request.setAttribute("articlesList", articlesList);
				nextPage = "/board01/listArticles.jsp";
             	 	 //action값이 /listArticles.do이면 전체글 조회
			} else if (action.equals("/listArticles.do")) { 

				articlesList = boardService.listArticles(); //전체 글 조회
              	 		 //조회된 글 목록을 articlesList로 바인딩한 후 listAriticles.jsp로 포워딩
				request.setAttribute("articlesList", articlesList); 
				nextPage = "/board01/listArticles.jsp";
			}else {
				nextPage = "/board01/listArticles.jsp";
			}
			
			RequestDispatcher dispatch = request.getRequestDispatcher(nextPage);
			dispatch.forward(request, response);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}
3. BoardService 클래스를 다음과 같이 작성

BoardDAO 객체를 생성한 후 select AllArticle() 메서드를 호출해 전체 글을 가져옴

package sec03.brd01;

import java.util.List;

public class BoardService {
	BoardDAO boardDAO;
	public BoardService() {
		boardDAO = new BoardDAO(); //생성자 호출 시 BoardDAO 객체를 생성
	}

	public List<ArticleVO> listArticles() {
		List<ArticleVO> articlesList = boardDAO.selectAllArticles();
		return articlesList;
	}
}
4. BoardDAO 클래스를 다음과 같이 작성

BoardService 클래스에서 BoardDAO의 selectAllArticles() 메서드를 호출하면 계층형 SQL문을 이용해 계층형 구조로 전체 글을 조회한 후 반환

package sec03.brd01;

import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;

public class BoardDAO {
	private DataSource dataFactory;
	Connection conn;
	PreparedStatement pstmt;

	public BoardDAO() {
		try {
			Context ctx = new InitialContext();
			Context envContext = (Context) ctx.lookup("java:/comp/env");
			dataFactory = (DataSource) envContext.lookup("jdbc/oracle");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public List selectAllArticles() {
		List articlesList = new ArrayList();
		try {
			conn = dataFactory.getConnection();
            //오라클의 계층형 SQL문을 실행
			String query = "SELECT LEVEL,articleNO,parentNO,title,content,id,writeDate" 
			             + " from t_board"
					     + " START WITH  parentNO=0" + " CONNECT BY PRIOR articleNO=parentNO"
					     + " ORDER SIBLINGS BY articleNO DESC";
			System.out.println(query);
			pstmt = conn.prepareStatement(query);
			ResultSet rs = pstmt.executeQuery();
			while (rs.next()) {
				int level = rs.getInt("level"); //각 글의 깊이(계층)을 level속성에 저장
				int articleNO = rs.getInt("articleNO"); //글 번호는 숫자형이므로 getInt로 가져옴
				int parentNO = rs.getInt("parentNO");
				String title = rs.getString("title");
				String content = rs.getString("content");
				String id = rs.getString("id");
				Date writeDate = rs.getDate("writeDate");
                		//글 정보를 AriticleVO 객체의 속성에 설정
				ArticleVO article = new ArticleVO();
				article.setLevel(level);
				article.setArticleNO(articleNO);
				article.setParentNO(parentNO);
				article.setTitle(title);
				article.setContent(content);
				article.setId(id);
				article.setWriteDate(writeDate);
				articlesList.add(article);
			}
			rs.close();
			pstmt.close();
			conn.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return articlesList;
	}
}
5. ArticleVO 클래스를 다음과 같이 작성

조회한 글을 저장하는 ArticleVO 클래스에 글의 깊이를 저장하는 level 속성을 추가

package sec03.brd01;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.sql.Date;

public class ArticleVO {
	private int level;
	private int articleNO;
	private int parentNO;
	private String title;
	private String content;
	private String imageFileName;
	private String id;
	private Date writeDate;

	public ArticleVO() {
		
	}
	public ArticleVO(int level, int articleNO, int parentNO, String title, String content, String imageFileName,
			String id) {
		super();
		this.level = level;
		this.articleNO = articleNO;
		this.parentNO = parentNO;
		this.title = title;
		this.content = content;
		this.imageFileName = imageFileName;
		this.id = id;
	}
	public int getLevel() {
		return level;
	}
	public void setLevel(int level) {
		this.level = level;
	}
	public int getArticleNO() {
		return articleNO;
	}
	public void setArticleNO(int articleNO) {
		this.articleNO = articleNO;
	}
	public int getParentNO() {
		return parentNO;
	}
	public void setParentNO(int parentNO) {
		this.parentNO = parentNO;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getContent() {
		return content;
	}
	public void setContent(String content) {
		this.content = content;
	}
	public String getImageFileName() {
		try {
			if (imageFileName != null && imageFileName.length() != 0) {
				imageFileName = URLDecoder.decode(imageFileName, "UTF-8");
			}
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		return imageFileName;
	}
	public void setImageFileName(String imageFileName) {
		try {
			this.imageFileName = URLEncoder.encode(imageFileName, "UTF-8");//파일이름에 특수문자가 있을 경우 인코딩합니다.
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
	}
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public Date getWriteDate() {
		return writeDate;
	}
	public void setWriteDate(Date writeDate) {
		this.writeDate = writeDate;
	}
}
6. JSP에 글 목록을 표시하기 위해 아래와 같이 코드 작성

첫 번째 <forEach> 태그를 이용해 articlesList 속성으로 포워딩된 글 목록을 차례로 전달받아 표시한다. <forEach> 태그 반복 시 각 글의 level 값이 1보다 크면 답글이므로 다시 내부 <forEach> 태그를 이용해 1부터 level 값까지 반복함녀서 공백을 만들고 (들여쓰기) 답글을 표시한다. 이 때 level 값이 1보다 크지 않으면 부모 글이므로 공백 없이 표시한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
    isELIgnored="false" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>    
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="contextPath"  value="${pageContext.request.contextPath}"  />
<%
  request.setCharacterEncoding("UTF-8");
%>  
<!DOCTYPE html>
<html>
<head>
	<style>
		.cls1 {text-decoration:none;}
		.cls2{text-align:center; font-size:30px;}
	</style>
	<meta charset="UTF-8">
	<title>글목록창</title>
</head>
<body>
	<table align="center" border="1"  width="80%"  >
		<tr height="10" align="center"  bgcolor="lightgreen">
     		<td >글번호</td>
     		<td >작성자</td>              
     		<td >제목</td>
     		<td >작성일</td>
  		</tr>
		<c:choose>
	  		<c:when test="${empty articlesList }" >
	    		<tr  height="10">
	      			<td colspan="4">
	         			<p align="center">
	            			<b><span style="font-size:9pt;">등록된 글이 없습니다.</span></b>
	        			</p>
	      			</td>  
	    		</tr>
	  		</c:when>
			<c:when test="${!empty articlesList}" >
	    		<c:forEach  var="article" items="${articlesList }" varStatus="articleNum" >
	     			<tr align="center">
						<td width="5%">${articleNum.count}</td>
						<td width="10%">${article.id }</td>
						<td align='left'  width="35%">
		  					<span style="padding-right:30px"></span>
		   					<c:choose>
		      					<c:when test='${article.level > 1 }'>  
		         					<c:forEach begin="1" end="${article.level }" step="1">
		              					<span style="padding-left:20px"></span>    
		         					</c:forEach>
		         					<span style="font-size:12px;">[답변]</span>
		         					<a class='cls1' href="${contextPath}/board/viewArticle.do?articleNO=${article.articleNO}">${article.title}</a>
		          				</c:when>
		         				<c:otherwise>
		            				<a class='cls1' href="${contextPath}/board/viewArticle.do?articleNO=${article.articleNO}">${article.title }</a>
		          				</c:otherwise>
		        			</c:choose>
		  				</td>
		  				<td  width="10%"><fmt:formatDate value="${article.writeDate}" /></td> 
					</tr>
	    		</c:forEach>
			</c:when>
		</c:choose>
	</table>
	<a  class="cls1"  href="#"><p class="cls2">글쓰기</p></a>
</body>
</html>
7. 아래의 주소를 입력하여 글 목록이 원하는 대로 출력되는지 확인

자식 글은 앞에 level 값만큼 들여쓰기가 되고 [답변]이라는 텍스트 다음에 표시된다.

728x90