프로젝트 구조
오늘은 Spring을 사용해서 게시판 목록/글쓰기 기능 구현을 해보겠습니다.
파일 구조는 위 사진과 같고, 다음은 테이블을 생성합니다.
SQL Table 생성
Board 테이블을 생성해서 연습용 데이터를 추가합니다.
--게시판 테이블
create table board (
bno number not null, --게시물번호
title varchar2(200) not null, --제목
content clob, --본문
writer varchar2(50) not null, --작성자
regdate date default sysdate, --작성일자
viewcnt number default 0, --조회수
primary key(bno)
);
insert into board (bno,title,content,writer) values
(1,'제목','내용','kim');
select * from board;
데이터가 제대로 들어갔는지 확인해 보고 코드 작업 전에 작성자와 유저 아이디를 조인시켜주는데, 즉, board 테이블과 이전 포스팅에서 만들었던 member 테이블을 조인 시켜줍니다.
select bno,title,writer,name,content,regdate,viewcnt
from board b, member m
where b.writer=m.userid
order by bno desc;
다음으로 DTO 생성해 줍니다.
BoardDTO
테이블 칼럼 조건과 동일한 타입 설정을 해주고 getter/setter , toString 까지만 생성합니다.
추가로 member 테이블과 조인시켜서 데이터를 가져올 수 있게 name 변수와 다음 포스팅을 위해 게시글 삭제했을 때,
화면에 표출 여부를 정할 수 있는 show , 첨부파일 변수를 배열로 생성합니다.
package com.example.spring.model.board.dto;
import java.util.Arrays;
import java.util.Date;
public class BoardDTO {
private int bno; //게시물번호
private String title; //제목
private String content; //본문
private String writer; //작성자 id
private Date regdate; //작성일자
private int viewcnt;
private String name; //작성자 이름
private int cnt; // 댓글 갯수
private String show; // 화면 표시 여부
private String[] files; // 첨부파일 이름 배열
public int getBno() {
return bno;
}
public void setBno(int bno) {
this.bno = bno;
}
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 getWriter() {
return writer;
}
public void setWriter(String writer) {
this.writer = writer;
}
public Date getRegdate() {
return regdate;
}
public void setRegdate(Date regdate) {
this.regdate = regdate;
}
public int getViewcnt() {
return viewcnt;
}
public void setViewcnt(int viewcnt) {
this.viewcnt = viewcnt;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCnt() {
return cnt;
}
public void setCnt(int cnt) {
this.cnt = cnt;
}
public String getShow() {
return show;
}
public void setShow(String show) {
this.show = show;
}
public String[] getFiles() {
return files;
}
public void setFiles(String[] files) {
this.files = files;
}
@Override
public String toString() {
return "BoardDTO [bno=" + bno + ", title=" + title + ", content=" + content + ", writer=" + writer
+ ", regdate=" + regdate + ", viewcnt=" + viewcnt + ", name=" + name + ", cnt=" + cnt + ", show=" + show
+ ", files=" + Arrays.toString(files) + "]";
}
}
다음으론 DAO 인터페이스를 생성해 주는데, 미리 기능에 필요한 메서드를 생성해 줍니다.
BoardDAO, Impl
package com.example.spring.model.board.dao;
import java.util.List;
import com.example.spring.model.board.dto.BoardDTO;
public interface BoardDAO {
public void deleteFile(String fullName); //첨부파일 삭제
public List<String> getAttach(int bno); //첨부파일 정보
public void addAttach(String fullName);// 첨부파일 저장
public void updateAttach(String fullName, int bno); //첨부파일 수정
public void create(BoardDTO dto) throws Exception; // 글쓰기
public void update(BoardDTO dto) throws Exception; //글수정
public void delete(int bno) throws Exception; // 글 삭제
public List<BoardDTO> listAll() throws Exception; // 목록
public void increaseViewcnt(int bno) throws Exception; // 조회수 증가 처리
public int countArticle() throws Exception; //레코드 개수 계산
public BoardDTO read(int bno) throws Exception; //레코드 조회
}
이전 포스팅과 다르게 이번 포스팅은 순서를 역순으로 작업했습니다.
오늘 포스팅에 필요한 기능은 목록/ 글쓰기 기능이라서 각 메서드에 세션 처리를 해주고 mapper에 요청을 보내는 코드까지만 작성해 줍니다.
package com.example.spring.model.board.dao;
import java.util.List;
import javax.inject.Inject;
import org.apache.ibatis.session.SqlSession;
import org.springframework.stereotype.Repository;
import com.example.spring.model.board.dto.BoardDTO;
@Repository
public class BoardDAOImpl implements BoardDAO {
@Inject
SqlSession sqlsession;
@Override
public void deleteFile(String fullName) {
// TODO Auto-generated method stub
}
@Override
public List<String> getAttach(int bno) {
// TODO Auto-generated method stub
return null;
}
@Override
public void addAttach(String fullName) {
// TODO Auto-generated method stub
}
@Override
public void updateAttach(String fullName, int bno) {
// TODO Auto-generated method stub
}
@Override
public void create(BoardDTO dto) throws Exception { //글쓰기 기능
sqlsession.insert("board.insert", dto);
}
@Override
public void update(BoardDTO dto) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void delete(int bno) throws Exception {
// TODO Auto-generated method stub
}
@Override
public List<BoardDTO> listAll() throws Exception { //목록 기능
return sqlsession.selectList("board.listAll") ;
}
@Override
public void increaseViewcnt(int bno) throws Exception {
// TODO Auto-generated method stub
}
@Override
public int countArticle() throws Exception {
// TODO Auto-generated method stub
return 0;
}
@Override
public BoardDTO read(int bno) throws Exception {
// TODO Auto-generated method stub
return null;
}
}
다음으론 Service와 DAO를 DI 해줍니다.
Service 단에서는 DAO에서 생성한 메서드를 동일하게 Service단에 적용시키고, 간단하게 DAO와 의존 관계를 맺고, 요청을 보내는 역할만 해줍니다.
BoardService, Impl
package com.example.spring.service.board;
import java.util.List;
import com.example.spring.model.board.dto.BoardDTO;
public interface BoardService {
public void deleteFile(String fullName); //첨부파일 삭제
public List<String> getAttach(int bno); //첨부파일 정보
public void addAttach(String fullName);// 첨부파일 저장
public void updateAttach(String fullName, int bno); //첨부파일 수정
public void create(BoardDTO dto) throws Exception; // 글쓰기
public void update(BoardDTO dto) throws Exception; //글수정
public void delete(int bno) throws Exception; // 글 삭제
public List<BoardDTO> listAll() throws Exception; // 목록
public void increaseViewcnt(int bno) throws Exception; // 조회수 증가 처리
public int countArticle() throws Exception; //레코드 개수 계산
public BoardDTO read(int bno) throws Exception; //레코드 조회
}
Impl 클래스에서 DAO에 요청을 보내줍니다.
package com.example.spring.model.board.dao;
import java.util.List;
import javax.inject.Inject;
import org.apache.ibatis.session.SqlSession;
import org.springframework.stereotype.Repository;
import com.example.spring.model.board.dto.BoardDTO;
@Repository
public class BoardDAOImpl implements BoardDAO {
@Inject
SqlSession sqlsession;
@Override
public void deleteFile(String fullName) {
// TODO Auto-generated method stub
}
@Override
public List<String> getAttach(int bno) {
// TODO Auto-generated method stub
return null;
}
@Override
public void addAttach(String fullName) {
// TODO Auto-generated method stub
}
@Override
public void updateAttach(String fullName, int bno) {
// TODO Auto-generated method stub
}
@Override
public void create(BoardDTO dto) throws Exception {
sqlsession.insert("board.insert", dto); //글쓰기 기능
}
@Override
public void update(BoardDTO dto) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void delete(int bno) throws Exception {
// TODO Auto-generated method stub
}
@Override
public List<BoardDTO> listAll() throws Exception {
return sqlsession.selectList("board.listAll") ; // 목록 기능
}
@Override
public void increaseViewcnt(int bno) throws Exception {
// TODO Auto-generated method stub
}
@Override
public int countArticle() throws Exception {
// TODO Auto-generated method stub
return 0;
}
@Override
public BoardDTO read(int bno) throws Exception {
// TODO Auto-generated method stub
return null;
}
}
여기까지 역순으로 코드 작업을 하고,
다음으로는 이전에 생성했던 메뉴 페이지에서 게시판 페이지를 요청할 수 있는 메뉴를 생성해 줍니다.
menu.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<c:set var="path" value="${pageContext.request.contextPath}" />
<div style="text-align: center;">
<a href="${path}">Home</a> |
<c:if test="${sessionScope.admin_userid == null}">
<a href="${path}/memo/list.do">메모장</a> |
<a href="${path}/board/list.do">게시판</a> | <!-- 게시판 페이지 추가 url 요청 -->
<a href="${path}/upload/uploadForm">업로드 테스트</a> |
<a href="${path}/upload/uploadAjax">업로드(Ajax)</a> |
</c:if>
<c:if test="${sessionScope.userid != null && sessionScope.admin_userid == null}">
<a href="${path}/shop/cart/list.do">장바구니</a> |
</c:if>
<div style="text-align: right;">
<c:choose>
<c:when test="${sessionScope.userid == null}">
<!-- 로그인하지 않은 상태 -->
<a href="${path}/member/login.do">로그인</a> |
<a href="${path}/admin/login.do">관리자 로그인</a> |
</c:when>
<c:otherwise>
<!-- 로그인한 상태 -->
${sessionScope.name}님이 로그인중입니다.
<a href="${path}/member/logout.do">로그아웃</a>
</c:otherwise>
</c:choose>
</div>
</div>
<hr>
메뉴 페이지에서 Controller로 url 요청을 보냈으니, Controller를 생성해서 요청 처리를 받고, Service 단에 보내줍니다.
BoardController
위 작업에서 미리 Service, DAO 작업을 끝냈으니, Contrller 에서는 Service 와 @Inject 시켜주고 각 기능 코드를 생성해서 요청받은 url 처리를 해줘서 Service 단에 보내줍니다.
package com.example.spring.controller.board;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import com.example.spring.model.board.dto.BoardDTO;
import com.example.spring.service.board.BoardService;
@Controller
@RequestMapping("board/*")
public class BoardController {
@Inject
BoardService boardService;
//로깅을 위한 변수
private static final Logger logger=
LoggerFactory.getLogger(BoardController.class);
@RequestMapping("list.do") // 세부 url
public ModelAndView list() throws Exception{
List<BoardDTO> list=boardService.listAll();
logger.info(list.toString());
ModelAndView mav = new ModelAndView();
Map<String, Object> map = new HashMap<>();
map.put("list", list); // map에 자료 저장
map.put("count", list.size()); //레코드 갯수 파악
mav.setViewName("board/list"); // 포워딩 뷰
mav.addObject("map",map); //전달 데이터
return mav;
}
@RequestMapping("write.do")
public String write() {
//글쓰기 폼 페이지 이동
return "board/write";
}
@RequestMapping("insert.do") //@ModelAttribute 생략 가능
public String insert(@ModelAttribute BoardDTO dto, HttpSession session) throws Exception{
//이름이 없기 때문에 대신 세션에서 사용자 id 를 가져옴
String writer = (String)session.getAttribute("userid");
dto.setWriter(writer);
//레코드 저장
boardService.create(dto);
//게시물 목록 이동
return "redirect:/board/list.do";
}
}
그럼 이제 포워딩 설정한 뷰단 페이지를 생성합니다.
view단
먼저 목록 페이지를 생성해 주고, 글쓰기 버튼을 구현해서 해당 페이지로 이동할 수 있는 url 요청을 Controller에서 요청받은 url 주소와 동일하게 세팅해줍니다.
< list.jsp >
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<%@ include file="../include/header.jsp" %>
<script type="text/javascript">
$(function() {
$("#btnWrite").click(function() {
location.href="${path}/board/write.do";
});
});
</script>
</head>
<body>
<%@ include file="../include/menu.jsp" %>
<h2>게시판</h2>
<button type="button" id="btnWrite">글쓰기</button>
${map.count}개의 게시물이 있습니다.
<table border="1">
<tr>
<th>번호</th>
<th>제목</th>
<th>이름</th>
<th>내용</th>
<th>날짜</th>
<th>조회수</th>
</tr>
<c:forEach var="row" items="${map.list }">
<tr>
<td>${row.bno}</td>
<td>${row.title }</td>
<td>${row.name }</td>
<td>${row.content }</td>
<td><fmt:formatDate value="${row.regdate }" pattern="yyyy-MM-dd HH:mm:ss"/></td>
<td>${row.viewcnt }</td>
</tr>
</c:forEach>
</table>
</body>
</html>
글쓰기 페이지를 생성하기 전에 insert 페이지에서 첨부파일 처리를 해야 합니다.
js 페이지에서 파일 처리 코드를 구현해서 스크립트로 가져옵니다.
< js >
/**
*
*/
function checkImageType(fileName) {
var pattern=/jpg|gif|png|jpeg/;
return fileName.match(pattern);
}
function getFileInfo(fullName) {
var fileName, imgsrc, getLink, fileLink;
if(checkImageType(fullName)) {//이미지 파일인 경우
imgsrc="/spring02/upload/displayFile?fileName="+fullName;
fileLink=fullName.substr(14); //14인덱스 ~끝
var front=fullName.substr(0,12); //0~11
var end=fullName.substr(14);
getLink="/spring02/upload/displayFile?fileName="+front+end;
}else{//이미지가 아닌 경우
fileLink=fullName.substr(12);
getLink="/spring02/upload/displayFile?fileName="+fullName;
}
// uuid_filename (+1은 _다음의 filename을 뽑아내기 위함)
fileName=fileLink.substr(fileLink.indexOf("_")+1);
//json으로 리턴
return {fileName: fileName, imgsrc: imgsrc, getLink: getLink, fullName: fullName};
}
js 코드 작업이 끝났다면 글쓰기 페이지에 스크립트 처리 해줍니다.
< write.jsp >
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<%@ include file="../include/header.jsp" %>
<script src="${path}/include/js/common.js"></script> <!-- js 파일 -->
<!-- ckeditor의 라이브러리 -->
<script src="${path}/ckeditor/ckeditor.js"></script>
<script type="text/javascript">
$(function() {
$("#btnSave").click(function(){
var str="";
//uploadedList영역에 클래스 이름이 file인 히든타입의 태그를 각각 반복
$("#uploadedList .file").each(function(i){
console.log(i);
//hidden태그 구성
str += "<input type='hidden' name='files["+i+"]' value='"
+ $(this).val()+"'>";
});
//폼에 hidden 태그를 붙임
$("#form1").append(str);
document.form1.submit();
});
//파일을 마우스로 드래그해서 업로드 영역에 올릴때 파일이 열리는 기본효과 막는 처리
$(".fileDrop").on("dragenter dragover", function(e) {
e.preventDefault();
});
//마우스로 파일을 드롭할 때 파일이 열리는 기본 효과 막음
$(".fileDrop").on("drop", function(e){
e.preventDefault();
//첫번째 첨부 파일
var files=e.originalEvent.dataTransfer.files;
var file=files[0];
//폼 데이터에 첨부파일 추가
var formData=new FormData();
formData.append("file",file);
$.ajax({
url: "${path}/upload/uploadAjax",
data: formData,
dataType: "text",
processData: false,
contentType: false,
type: "post",
success: function(data){
//data: 업로드한 파일 정보와 Http 상태 코드
var fileInfo=getFileInfo(data);
console.log(fileInfo);
var html="<a href='"+fileInfo.getLink+"'>"+fileInfo.fileName+"</a><br>";
html += "<input type='hidden' class='file' value='"+fileInfo.fullName+"'>";
$("#uploadedList").append(html);
}
});
});
});
</script>
<style type="text/css">
.fileDrop {
width: 600px;
height: 100px;
border: 1px dotted gray;
background-color: gray;
}
</style>
</head>
<body>
<%@ include file="../include/menu.jsp" %>
<h2>글쓰기</h2>
<form id="form1" name="form1" method="post" action="${path}/board/insert.do">
<div>제목 <input name="title" id="title" size="80" placeholder="제목을 입력하세요"></div>
<div style="width: 800px">
내용 <textarea id="content" name="content" rows="3" cols="80"
placeholder="내용을 입력하세요"></textarea>
<script>
//ckeditor 적용
CKEDITOR.replace("content", {
filebrowserUploadUrl: "${path}/imageUpload.do"
});// ImageUploadController.java에서 처리
</script>
</div>
<div> 첨부파일을 등록하세요
<div class="fileDrop"></div>
<div id="uploadedList"></div>
</div>
<div style="width: 700px; text-align: center;">
<button type="button" id="btnSave">확인</button>
</div>
</form>
</body>
</html>
마지막으로 mapper 파일에서 기능에 맞게 쿼리문 작성만 해주면 끝나는데, 간단한 유효성 검사를 해주기 위해 이전 포스팅에서 생성해 둔 interceptor 태그에 mapping 시켜줍니다.
<interceptor >
<mapping path="/board/write.do"/>
<mapping path="/board/insert.do"/>
</interceptor >
boardMapper.xml
목록 페이지 데이터를 가져올 땐 처음에 테이블 생성할 때 조인 시켜둔 select문을 그대로 가져와서 적어주고,
insert문에서는 들어오는 값을 함수 처리 해줍니다.
<!-- 다른 mapper와 중복되지 않도록 네임스페이스 기재 -->
<mapper namespace="board">
<select id="listAll" resultType="com.example.spring.model.board.dto.BoardDTO">
select bno,title,writer,name,content,regdate,viewcnt
from board b, member m
where b.writer=m.userid
order by bno desc
</select>
<insert id="insert">
insert into board (bno,title,content,writer) values
(seq_board.nextval,#{title},#{content},#{writer})
</insert>
</mapper>
출력
< 목록 >
< interceptor >
< insert >
< list >
마치며
오늘은 간단한 게시판 목록/ 글쓰기 기능 구현에 대해서 알아보았습니다.
다음 포스팅에서 뵙겠습니다.
'[ JAVA ] > JAVA Spring' 카테고리의 다른 글
[ Spring ] 도로명주소 API 연동 (0) | 2023.04.27 |
---|---|
[ Spring ] 게시판 02 - 회원 목록 페이지 나누기 기능 구현 (0) | 2023.04.26 |
[ Spring ] Ajax 파일업로드 (0) | 2023.04.20 |
[ Spring ] 코드 난독화 (1) | 2023.04.19 |
[ Spring ] Interceptor (0) | 2023.04.17 |