17장. 모델2 방식으로 효율적으로 개발하기 - ② 답변형 게시판(CRUD 구현)

17.4 모델2로 답변형 게시판 구현하기

- 글 목록이 나열되고, 부모 글에 대한 답변 글(자식 글)이 계층 구조로 나열되어있고 답변 글에 대한 답변 글도 허용되는 게시판을 만들어보자

- 각 글에는 작성자 ID(id)가 있는데 이것은 회원 테이블의 ID컬럼에 대해 외래키를 속성으로 가진다.

- parentNO 컬럼은 답변이 달린 부모글의 번호를 나타냄, 0이면 자신이 부모글임

-구현 절차

1. 게시판 기능 테이블을 생성하고 데이터 넣기

2. model(service,dao) - view(jsp) - controller(servlet) 구현

 

 

1.게시판 기능 테이블을 생성하고 데이터 넣기- sql(오라클) 코드

DROP TABLE t_Board CASCADE CONSTRAINTS;

create table t_Board(
    articleNO number(10) primary key,
    parentNO number(10) default 0,
    title varchar2(500) not null,
    content varchar2(100),
    imageFileName varchar2(100),
    writedate date default sysdate not null,
    id varchar2(10),
    CONSTRAINT FK_ID FOREIGN KEY(id) REFERENCES t_member(id)
);

insert into t_board values(1,0,'테스트제목입니다.','테스트글입니다',null,sysdate,'hong');
insert into t_board values(2,0,'안녕하새오.','인사드립니다',null,sysdate,'lee');
insert into t_board values(3,2,'네 안녕하세요.','답인사 입니다',null,sysdate,'hong');
insert into t_board values(5,3,'답인사 감사요.','^^',null,sysdate,'lee');
insert into t_board values(4,0,'김유신입니다.','김유신 테스트글입니다',null,sysdate,'kim');
insert into t_board values(6,2,'이순신님하이','순-하!',null,sysdate,'kim');

commit;

 

2. model(service,dao) - view(jsp) - controller(servlet) 구현

- 실제 개발할 때는 아까처럼 컨트롤러에서 DAO 클래스에 바로 접근하는 것이 아니라 Service 클래스를 거쳐서 여기서 DAO클래스에 접근함

- 모델에서 Service 클래스를 두는 이유 : Service는 실제 프로그램을 트랜잭션(=단위기능, 사용자 입장에서 하나의 논리적인 기능)으로 작업을 수행

- 단위기능이 DAO와 연동해서 여러 SQL문으로 이루어진 경우가 많다.

- 실제 개발을 할 때에도 Service의 클래스 메서드를 이용해 큰 기능을 단위 기능으로 나눈 후 그 메서드들 내에서 세부적인 기능을 하는 DAO의 SQL 문들을 조합해서 구현

- 유지보수나 확장성이 좋음

 

 

<게시판 글 목록 보기 구현>

- 오라클에서 제공하는 계층형 SQL문 기능을 이용해서 부모글에 대한 답변 글을 계층 구조로 보여주기

 

- 글 목록 조회 계층형 SQL문 >> 이것을 dao의 리스트 조회하는 메서드에서

SELECT LEVEL, 			//오라클에서 제공하는 가상 컬럼으로 글의 깊이를 나타냄(부모글은 1)
    articleNO,
    parentNO,
    lpad(' ', 4*(level01)) ||title title,
    content,
    writeDate,
    id
FROM t_board
START WITH parentNO=0			//parentNO=0인 글이 루트글
CONNECT BY PRIOR articleNO=parentNO	 //루트의 articleNO의 값을 parentNO의 값으로 가지는 애들을 찾아 자식으로 관계맺음
ORDER SIBLINGS BY articleNO DESC;	//부모가 같으면 articleNO의 내림차순으로 정렬

 

- listArticles.jsp

...
<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:otherwise>
    <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/viewArticles.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>안에서는 <c:when>과 <c:otherwise>태그 내에서만 코드를 쓸 수 있음.. 위처럼 공통되더라도 따로 써줘야 함 --%>
             </c:choose>
            </td>
            <td  width="10%"><fmt:formatDate value="${article.writeDate }"/>

- Controller.java

BoardService boardService;
ArticleVO articleVO;

public void init() throws ServletException {
    boardService = new BoardService();
}
....
    String action = request.getPathInfo();

    if (action == null || action.equals("/listArticles.do")) {
        List<ArticleVO> articlesList = boardService.listArticles();
        request.setAttribute("articlesList", articlesList);
        nextPage = "/board01/listArticles.jsp";
    } 
...

- Service

public class BoardService {

	BoardDAO boardDAO;
	
	public BoardService() {
		boardDAO=new BoardDAO();
	}
	
	
	public List<ArticleVO> listArticles() {
		List<ArticleVO> articleList=boardDAO.selectAllArticles();
		return articleList;
	}

}

- DAO

public List<ArticleVO> selectAllArticles() {

    List<ArticleVO> articlesList=new ArrayList();
    try {
        con=dataFactory.getConnection();
        String query="SELECT LEVEL, articleNO, parentNO, title, content, writeDate, id " + 
                "FROM t_board " + 
                "START WITH parentNO=0 " + 
                "CONNECT BY PRIOR articleNO=parentNO " + 
                "ORDER SIBLINGS BY articleNO DESC";
        System.out.println(query);
        pstmt=con.prepareStatement(query);
        ResultSet rs=pstmt.executeQuery();

        while(rs.next()) {
            int level=rs.getInt("level");
            int articleNO=rs.getInt("articleNO");
            int parentNO=rs.getInt("parentNO");
            String title=rs.getString("title");
            String content=rs.getString("content");
            Date writeDate=rs.getDate("writeDate");
            String id=rs.getString("id");

            ArticleVO articleVO=new ArticleVO(level,articleNO,parentNO,title,content,null,id);
            articleVO.setWriteDate(writeDate);

            articlesList.add(articleVO);
            ...

- VO

public class ArticleVO {
	private int level;
	private int parentNO;
	private int articleNO;
	private String title;
	private String content;
	private String imageFileName;
	private String id;
	private Date writeDate;
    ...
    	public String getImageFileName() {
		try {
			if (imageFileName != null && imageFileName.length() != 0) {
				imageFileName = URLDecoder.decode(imageFileName, "UTF-8"); //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();
		}
     ...

 

 

2) 게시판 글쓰기 구현

- 이미지 파일이 업로드 될 저장소 폴더 만들기

 

- 첫화면은 게시글 리스트 > 글쓰기 버튼 누르면 board/ArticleForm.do로 이동 > 컨트롤러에서 ArticleForm.jsp로 이동 > jsp에선 글쓰기 양식을 보여주고 글과 이미지를 작성한 후 글쓰기 버튼을 눌러 board/addArticle.do로 이동

>  Service호출하고 DAO를 호출해 db에 insert처리

> 다시 컨트롤러로 돌아와 board/listArticles.do로 포워드하여 게시글 리스트 화면으로~

 

- 사진을 게시글 별로 구분하기 위해 글 번호를 이름으로 하는 폴더에 각각 저장하는것 까지 구현해보자.

   ㄴ (임시폴더 생성 후) 임시폴더에 파일 저장 > 번호 폴더를 .mkdirs()로 생성 > 임시폴더에서 번호폴더로 이동

       > 자바스크립트로 알람창 띄우기+ 글 목록으로 이동

   ㄴ 글 번호는 현재 최신글에서 +1 한 것이므로 서비스와 dao를 호출할 때 글 번호를 반환하도록 해서 얻어옴

 

- Controller

else if(action.equals("/articleForm.do")) {
        nextPage="/board02/articleForm.jsp";
    }
    else if(action.equals("/addArticle.do")) {
        Map<String, String> articleMap=upload(request,response);
        String title=articleMap.get("title");
        String content=articleMap.get("content");
        String imageFileName=articleMap.get("imageFileName");

        articleVO.setParentNO(0);
        articleVO.setId("an");
        articleVO.setTitle(title);
        articleVO.setContent(content);
        articleVO.setImageFileName(imageFileName);

        int articleNO=boardService.addArticle(articleVO);

        File srcFile=new File(ARTICLE_IMAGE_REPO+"\\temp\\"+imageFileName);

        File destDir=new File(ARTICLE_IMAGE_REPO+"\\"+articleNO);
        destDir.mkdir();
        FileUtils.moveFileToDirectory(srcFile, destDir,true);

        PrintWriter pw=response.getWriter();
        pw.print("<script> alert('새 글을 추가하였습니다');   location.href='"+request.getContextPath()+"/board/listArticles.do'; </script>");

        return; //알림창에서 확인 누르면 바로 이동하게 하기 위해선 location.href만 써야 한다..ㅎ

//			nextPage="/board/listArticles.do";
    }
    ...
    
private Map<String, String> upload(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
    Map<String,String> articleMap=new HashMap<String,String>();
    String encoding="utf-8";

    File currentDirPath=new File(ARTICLE_IMAGE_REPO); //저장 폴더에 대한 파일 객체 생성
    DiskFileItemFactory factory=new DiskFileItemFactory();
    factory.setRepository(currentDirPath);
    factory.setSizeThreshold(1024*1024);

    ServletFileUpload upload=new ServletFileUpload(factory);

    try {
        List items=upload.parseRequest(request);
        for(int i=0;i<items.size();i++) {
            FileItem fileItem=(FileItem)items.get(i);

            if(fileItem.isFormField()) {
                articleMap.put(fileItem.getFieldName(),fileItem.getString(encoding));
            }
            else {
                if(fileItem.getSize()>0) {
                    int idx=fileItem.getName().lastIndexOf("\\");

                    String fileName=fileItem.getName().substring(idx+1);
                    articleMap.put("imageFileName", fileName);

                    File uploadFile=new File(currentDirPath+"\\temp\\"+fileName);
                    fileItem.write(uploadFile);
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

    return articleMap;
}

 

- addArticle.jsp

<script  src="http://code.jquery.com/jquery-latest.min.js"></script>
<script type="text/javascript">
function readURL(input) { <%-- 제이쿼리로 이미지 파일 첨부 시(onChange) 미리보기 기능 구현--%>
    if (input.files && input.files[0]) {
	      var reader = new FileReader();
	      reader.onload = function (e) {
	        $('#preview').attr('src', e.target.result);
        }
       reader.readAsDataURL(input.files[0]);
    }
} 
function backToList(obj){
  obj.action="${contextPath}/board/listArticles.do";
  obj.submit();
}
...
<h1 style="text-align:center">새글 쓰기</h1>
  <form name="articleForm" method="post"   action="${contextPath}/board/addArticle.do"   enctype="multipart/form-data">
    <table border=0 align="center">
     <tr>
	   <td align="right">글제목: </td>
	   <td colspan="2"><input type="text" size="67"  maxlength="500" name="title" /></td>
	 </tr>
	 <tr>
		<td align="right" valign="top"><br>글내용: </td>
		<td colspan=2><textarea name="content" rows="10" cols="65" maxlength="4000"></textarea> </td>
     </tr>
     <tr>
        <td align="right">이미지파일 첨부:  </td>
        	 <td> <input type="file" name="imageFileName"  onchange="readURL(this);" /></td>
	   	 	<td><img  id="preview"   width=200 height=200/></td>
	 </tr>
	 <tr>
	    <td align="right"> </td>
	    <td colspan="2">
	        <input type="submit" value="글쓰기" />
	       <input type=button value="목록보기"onClick="backToList(this.form)" />
	    </td>
     </tr>
    </table>
  </form>

 

- Service / DAO

//service
public int addArticle(ArticleVO articleVO) {
    return boardDAO.isertNewArticle(articleVO);
}


//////////////////////////////////////////////////////////////////////dao
private int getNewArticleNO() {
    try {
        con=dataFactory.getConnection();
        String query="select max(articleNO) from t_board";
        pstmt=con.prepareStatement(query);

        ResultSet rs=pstmt.executeQuery();
        if(rs.next())
            return (rs.getInt(1)+1);

        rs.close();
        pstmt.close();
        con.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return 0;
}

public int isertNewArticle(ArticleVO articleVO) {
    int articleNO=getNewArticleNO();
    try {
        con=dataFactory.getConnection();
        String query="insert into t_board(articleNO, parentNO, title, content, imageFileName, id)"
                +"values(?,?,?,?,?,?)";
        pstmt=con.prepareStatement(query);
        pstmt.setInt(1, articleNO);
        pstmt.setInt(2, articleVO.getParentNO());
        pstmt.setString(3, articleVO.getTitle());
        pstmt.setString(4, articleVO.getContent());
        pstmt.setString(5, articleVO.getImageFileName());
        pstmt.setString(6, articleVO.getId());

        pstmt.executeUpdate();

        pstmt.close();
        con.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }

    return articleNO;
}

 

 

3) 글 내용 읽기 구현

ㄴ 게시글 클릭 href에 요청 매개변수로 글 번호를 넣고, 그것을 이용해 글 객체를 얻어와서 화면에 표시

ㄴ 사진은 FileDownloadController.java에서 요청 바인딩 된 이름을 가져오고 inputStream + outputStream으로 출력한다.

 

-controller

else if(action.equals("/viewArticle.do")){
    String articleNO=request.getParameter("articleNO");
    articleVO=boardService.viewArticle(Integer.parseInt(articleNO));
    request.setAttribute("article", articleVO);

    nextPage="/board03/viewArticle.jsp";
}

-service/dao

////service
public ArticleVO viewArticle(int articleNO) {
    ArticleVO article=boardDAO.selectArticle(articleNO);
    return article;

}
///////////////////dao
public ArticleVO selectArticle(int articleNO) {
    ArticleVO article=new ArticleVO();
    try {
        con=dataFactory.getConnection();
        String query="select * from t_board where articleNO=?";
        pstmt=con.prepareStatement(query);
        pstmt.setInt(1, articleNO);

        ResultSet rs=pstmt.executeQuery();
        rs.next();


        article.setArticleNO(articleNO);
        article.setParentNO(rs.getInt("parentNO"));
        article.setTitle(rs.getString("title"));
        article.setContent(rs.getString("content"));
        article.setImageFileName(rs.getString("imageFileName"));
        article.setWriteDate(rs.getDate("writeDate"));
        article.setId(rs.getString("id"));

    } catch (SQLException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return article;
}

-viewArticle.jsp

<body>
  <form name="frmArticle" method="post"   enctype="multipart/form-data">
  ...
  <c:if test="${not empty article.imageFileName && article.imageFileName!='null' }">  
<tr>
   <td width="20%" align="center" bgcolor="#FF9933"  rowspan="2">
      이미지
   </td>
   <td>
    <img src="${contextPath}/download.do?imageFileName=${article.imageFileName}&articleNO=${article.articleNO }" width=400  id="preview"  /><br>
       
   </td>   
   ...

 

-FileDownloadController.java

...
    String imageFileName=request.getParameter("imageFileName");
    String articleNO=request.getParameter("articleNO");

    response.setHeader("Cache-Control", "no-cache");
    response.addHeader("Content-disposition", "attachment;fileName="+imageFileName);

    File imageFile=new File(ARTICLE_IMAGE_REPO+"\\"+articleNO+"\\"+imageFileName);
    FileInputStream in=new FileInputStream(imageFile);
    OutputStream out=response.getOutputStream();

    byte[] buffer=new byte[1024*8];	//8kb 크기의 버퍼
    while(true) {
        int count=in.read(buffer);
        if (count==-1)
            break;
        out.write(buffer,0,count);
    }
    in.close();
    out.close();
...

 

 

4) 게시글 수정 구현

- view.jsp에서 수정 버튼을 누르면 해당 게시글의 매개변수-값이 컨트롤러의 mod.do로 전송된다.

- upload를 통해 매핑하고, 객체를 생성하면 service/dao를 통해 update한다.

 

- view.jsp에서 글번호를 제외한 disabled되었던 태그를 자바스크립트를 통해 풀어주고, 글번호는 hidden으로 전송

- view.jsp에서 원래 파일 이름을 hidden 태그로 전송해서 원래 파일을 삭제해준다.

 

- viewArticle.jsp

<title>글 보기</title>
   <style>
     #tr_btn_modify{
       display:none;
     }
   </style>
   <script  src="http://code.jquery.com/jquery-latest.min.js"></script> 
   <script type="text/javascript" >
     function backToList(obj){
	    obj.action="${contextPath}/board/listArticles.do";
	    obj.submit();
     }
 
	 function fn_enable(obj){
		 document.getElementById("i_title").disabled=false;
		 document.getElementById("i_content").disabled=false;
		 document.getElementById("i_imageFileName").disabled=false;
		 document.getElementById("tr_btn_modify").style.display="block";
		 document.getElementById("tr_btn").style.display="none";
	 }
	 
	 function fn_modify_article(obj){
		 obj.action="${contextPath}/board/modArticle.do";
		 obj.submit();
	 }
	 
	 function readURL(input) {
	     if (input.files && input.files[0]) {
	         var reader = new FileReader();
	         reader.onload = function (e) {
	             $('#preview').attr('src', e.target.result);
	         }
	         reader.readAsDataURL(input.files[0]);
	     }
	 } 
	</script>
</head>
...
    <input type="text"  value="${article.articleNO }"  disabled />
    <input type="hidden" name="articleNO" value="${article.articleNO}"  />
    ...
    <input  type= "hidden"   name="originalFileName" value="${article.imageFileName }" />
    <img src="${contextPath}/download.do?imageFileName=${article.imageFileName}&articleNO=${article.articleNO }" width=400  id="preview"  /><br>
...
	  <input type=button value="수정하기" onClick="fn_enable(this.form)">
	  <input type=button value="리스트로 돌아가기"  onClick="backToList(this.form)">

- Controller

else if(action.contentEquals("/modArticle.do")) {
    Map<String,String> articleMap=upload(request,response);

    String articleNO=articleMap.get("articleNO");
    String title=articleMap.get("title");
    String content=articleMap.get("content");
    String imageFileName=articleMap.get("imageFileName");
    String originalFileName=articleMap.get("originalFileName");
    articleVO.setArticleNO(Integer.parseInt(articleNO));
    articleVO.setTitle(title);
    articleVO.setContent(content);
    articleVO.setImageFileName(imageFileName);

    boardService.modArticle(articleVO);

    if(imageFileName!=null && imageFileName.length()!=0) {
        File srcFile= new File(ARTICLE_IMAGE_REPO+"\\temp\\"+imageFileName);
        File destDir=new File(ARTICLE_IMAGE_REPO+"\\"+articleNO);
        destDir.mkdirs();
        FileUtils.moveFileToDirectory(srcFile, destDir, true);

        File originalFile=new File(ARTICLE_IMAGE_REPO+"\\"+articleNO+"\\"+originalFileName);
        originalFile.delete();
    }
    PrintWriter pw=response.getWriter();
    pw.print("<script>alert('글을 수정했습니다.'); location.href='"+request.getContextPath()
    +"/board/viewArticle.do?articleNO="+articleNO+"'; </script>");

    return;
}

- service/dao

///service

public void modArticle(ArticleVO article) {
    boardDAO.updateArticle(article);
}


/////dao
public void updateArticle(ArticleVO article) {
    try {
        con=dataFactory.getConnection();
        String query="update t_board set title=?,content=?";
        if(article.getImageFileName()!=null && article.getImageFileName().length()!=0) {
            query+=", imageFileName=? ";
        }
        query+="where articleNO=?";
        pstmt=con.prepareStatement(query);
        pstmt.setString(1, article.getTitle());
        pstmt.setString(2, article.getContent());

        if(article.getImageFileName()!=null && article.getImageFileName().length()!=0) {
            pstmt.setString(3, article.getImageFileName());
            pstmt.setInt(4, article.getArticleNO());
        }
        else
            pstmt.setInt(3, article.getArticleNO());

        pstmt.executeUpdate();
        pstmt.close();
        con.close();

    } catch (SQLException e) {
        e.printStackTrace();
    }
}

 

 

5) 게시글 삭제 구현

- view.jsp에서 삭제버튼을 누르면 동적으로 form, input 태그를 생성 해서 post로 글 번호를 넘겨주며 delete.do로 넘어감

- delete.do에서는 서비스/dao에서 삭제 후 넘겨준 자식 게시물 번호를 넘겨 받아서 해당 폴더까지 삭제한다.

- dao에서는 오라클의 계층형 SQL을 통해 해당하는 부모글의 자식글들을 select해서 넘겨주고, 부모글과 자식글을 delete 한다.

 

- controller

else if(action.contentEquals("/removeArticle.do")) {
    int articleNO=Integer.parseInt(request.getParameter("articleNO"));

    List<Integer> childNOList=boardService.removeArticle(articleNO);

    for(int childNO:childNOList) {
        File imageDir=new File(ARTICLE_IMAGE_REPO+"\\"+childNO);
        if(imageDir.exists()) {
            FileUtils.deleteDirectory(imageDir);
        }
    }
    PrintWriter pw=response.getWriter();
    pw.print("<script> alert('글을 삭제했습니다'); location.href='"+request.getContextPath()+"/board/listArticles.do'; </script>");

    return;
}

- service/dao

////service

public List<Integer> removeArticle(int articleNO) {
    return boardDAO.deleteArticle(articleNO);
}


////dao
public List<Integer> deleteArticle(int articleNO) {
    List<Integer> childNOList=selectRemovedArticles(articleNO);

    try {
        con=dataFactory.getConnection();
        String query="delete from t_board where articleNO in ("
                + "select articleNO from t_board start with articleNO=? connect by prior articleNO=parentNO)";
        pstmt=con.prepareStatement(query);
        pstmt.setInt(1, articleNO);
        pstmt.executeUpdate();


        pstmt.close();
        con.close();

    } catch (SQLException e) {
        e.printStackTrace();
    }

    return childNOList;
}


private List<Integer> selectRemovedArticles(int articleNO) {
    List<Integer> articleNOList = new ArrayList<Integer>();

    try {
        con=dataFactory.getConnection();
        String query="select articleNO from t_board start with articleNO=? connect by prior articleNO=parentNO";

        pstmt=con.prepareStatement(query);
        pstmt.setInt(1, articleNO);

        ResultSet rs=pstmt.executeQuery();
        while (rs.next()) {
            articleNOList.add(rs.getInt("articleNO"));
        }

        rs.close();
        pstmt.close();
        con.close();

    } catch (SQLException e) {
        e.printStackTrace();
    }

    return articleNOList;
}

- view.jsp

 function fn_remove_article(url, articleNO){
     var form=document.createElement("form");
     form.setAttribute("method","post");
     form.setAttribute("action",url);

     var inputArticleNO=document.createElement("input");
     inputArticleNO.setAttribute("type","hidden");
     inputArticleNO.setAttribute("name","articleNO");
     inputArticleNO.setAttribute("value",articleNO);

     form.appendChild(inputArticleNO);
     document.body.appendChild(form);
     form.submit();
 }
 ...
 <input type=button value="삭제하기" onClick="fn_remove_article('${contextPath}/board/removeArticle.do', ${article.articleNO})">