17.1 웹 애플리케이션 모델
17.2 MVC 디자인 패턴
17.3 MVC를 이용한 회원관리
17.4 모델2로 답변형 게시판 구현하기
17.4 모델2로 답변형 게시판 구현하기
695p
게시판 기능은 모든 웹 페이지의 기본 기능을 포함함
답변형 게시판에서는 부모글이 목록에 나열되고, 각 부모글에 대해 답변 글(자식 글)이 계층 구조로 나열되는 구조임. 즉, 답변 글에 또 답변 글을 올릴 수 있는 기능을 하는 게시판임.
▼답변형 게시판 글을 저장하는 테이블(t_board)의 컬럼
no | 컬럼 이름 | 속성 | 자료형 | 크기 | 유일키 여부 | NULL 여부 | 키 | 기본값 |
1 | articleNO | 글 번호 | number | 10 | Y | N | 기본키 | |
2 | parentNO | 부모글 번호 | number | 10 | N | N | 0 | |
5 | title | 글 제목 | varchar2 | 500 | N | N | ||
6 | content | 글 내용 | varchar2 | 4000 | N | N | ||
8 | imageFileName | 첨부 파일 이름 | varchar2 | 30 | N | N | ||
9 | writeDate | 작성일 | date | N | N | sysdate | ||
10 | id | 작성자 ID | varchar2 | 20 | N | N | 외래키 |
주요 속성
articleNO (글 번호) : 글이 추가될 때마다 1씩 증가되면서 고유값을 부여함
parentNO (부모글 번호) : 답변을 단 부모글의 번호. 부모글의 번호가 0(해당 컬럼의 기본값)이면 자신이 부모글임.
imageFileName (첨부 파일 이름) : 글 작성 시 첨부한 이미지 파일 이름
id (작성자 ID) : 글을 작성한 작성자의 ID. 항상 t_member 테이블의 ID 컬럼을 참조함.
테이블 생성 쿼리
create table t_board(
articleNO number(10) primary key,
parentNO number(10) default 0,
title varchar2(500) not null,
content varchar2(4000),
imageFileName varchar2(30),
writeDate date default sysdate not null,
id varchar2(10),
constraint FK_ID foreign key(id)
references t_member(id)
);
*** 테이블 제약조건
NOT NULL | 필수 입력 사항임. |
UNIQUE | 중복성 배제의 의미. 테이블 내의 유일한 값으로 존재해야 함. |
PRIMARY KEY | 기본 키. NOT NULL + UNIQUE. |
FOREIGN KEY | 참조하는 테이블에 존재하는 값만을 사용함. 참조하는 테이블의 키 값. |
CHECK | 주어진 조건에 해당하는 값만 입력할 수 있음. |
- NOT NULL
not null로 정의된 컬럼은 데이터 입력시 값이 필수적으로 들어가야 하는 것을 의미함.
해당 컬럼에 데이터가 입력되지 않으면(null이 들어가면) 오류 발생함.
컬럼을 NULL 로 정의하는 것은 값을 입력하지 않을 시 null이 default 값임을 의미함. NOT NULL을 주지 않은 것과 같은 의미(=null을 허용함). 공백은 null 과 다른 데이터임. 공백이 입력된 것은 null이 아니라 값이 있는 것으로 인식됨
- UNIQUE
고유키.
insert 나 update 시 제약이 걸려있는 컬럼에 동일한 데이터가 나오면 오류가 발생함.
null이 중복되는 것(null이 여러 행인 것)은 중복으로 인식하지 않음. 따라서 unique 컬럼도 nullable이면 여러 행에 null이 존재할 수 있음.
UNIQUE CONSTRAINTS constraints_name UNIQUE (col2, col3);
= (col2, col3) 의 조합이 유일해야 한다는 의미임. (1,2)와 (1,1)은 가능함, 오직 (1,2)가 또 들어가는 것이 안됨.
- PRIMARY KEY
기본키는 해당 테이블 내에서 데이터를 식별하기 위한 제약 조건임.
UNIQUE와는 달리 한 테이블 내에 한 컬럼에만 지정할 수 있음.
NOT NULL + UNIQUE 이므로 해당 컬럼의 데이터는 NULL값을 가질 수 없으며 테이블 내에서 유일해야 함
- FOREIGN KEY
외래키가 지정된 컬럼은 참조하는 테이블의 컬럼에 존재하는 값만을 가질 수 있음.
NULL 허용함(그러나 데이터 무결성을 위해 피하는 것이 좋음)
(예. 사원은 하나의 부서를 가짐/부서 테이블을 참조/ but 신입사원은 부서가 미정일 수 있음 >O-----O|-)
부모 테이블의 열이 PRIMARY KEY 또는 UNIQUE 제약조건이 설정되어 있어야 함.
값을 입력하려는데 참조 테이블에 해당 값이 존재하지 않을 경우 INSERT 또는 UPDATE 시 오류 발생함.
t_member의 id='hong' 이 글을 써서 t_board에 id='hong' 인 행이 있다면
t_member에서 id='hong'인 행을 바로 삭제할 수 없음. t_board에서 id='hong'인 데이터를 모두 삭제 후 삭제 가능
or CASCADE 옵션을 사용해야 t_member에서 참조되는 id 한번에 삭제 할 수 있음
- CHECK
조건에 부합하는 데이터만 입력이 가능하도록 하는 제약조건.
조건에는 기본연산자, 비교연산자, IN, NOT IN 등 다양한 연산자가 사용가능함.
출처 https://m.blog.naver.com/15elly/221853889315
뷰와 컨트롤러는 그대로 JSP와 서블릿이 기능을 수행하지만
모델은 기존의 DAO 클래스 외에 BoardService 클래스가 추가되었음
DAO는 데이터베이스에 접근하는 기능을 수행하고,
Service는 트랜잭션(Transaction)으로 작업을 수행함,
트랜잭션이란 실제 프로그램을 업무에 적용하는 사용자 입장에서 '업무 단위'를 말함
'업무 단위' 란 '단위 기능' 이라고도 하며, 사용자 입장에서 하나의 논리적인 기능을 의미함
웹 애플리케이션에서 묶어서 처리하는 단위 기능에는 다음과 같은 것들이 있음
- 게시판 글 조회 시 해당 글을 조회하는 기능 + 조회수를 갱신하는 기능
- 쇼핑몰에서 상품 주문 시 주문 상품을 테이블에 등록 + 주문자의 포인트를 갱신
- 은행에서 송금 시 송금자의 잔고를 갱신하는 기능 + 수신자의 잔고를 갱신하는 기능
단위기능(Service)마다 자신의 기능을 수행할 때 DAO와 연동해 하나의 SQL문으로 기능을 수행하기도 하지만
여러 SQL문을 묶어서 하나의 단위 기능을 수행하기도 함
실제 개발 시 Service 클래스의 메서드를 이용해 큰 기능을 단위 기능으로 나눈 후
Service 클래스와 각 메서드는 자신의 기능을 더 세부적인 기능을 하는 DAO의 SQL문들을 조합해서 구현함
=> 유지보수, 시스템의 확장성 면에서 훨씬 유리함
: 세부 기능을 수행하는 SQL문들을 DAO에서 구현
-> Service 클래스의 단위 기능 메서드에서 DAO에 만들어놓은 SQL문들을 조합해서 단위기능을 구현함
1. 게시판 글 목록 보기 구현 700p
2. 게시판 글쓰기 구현 709p + 업로드 파일 관리 718p
3. 글 상세 기능 구현 723p
4. 글 수정 기능 구현 729p
5. 글 삭제 기능 구현 737p
6. 답글 쓰기 기능 구현 743p
7. 게시판 페이징 기능 구현 750p
1. 게시판 글 목록 보기 구현
700p
1. 브라우저에서 /board/lstArticles.do 로 요청
2. Controller 가 전달받아서 Service와 DAO를 거쳐 DB에서 글 목록 정보를 조회
3. listArticles.jsp 로 전달하여 화면에 글 목록을 보여줌
이때, 부모 글에 대한 답변 글을 계층 구조로 보여주는 기능을 구현하기 위해 계층형 SQL문 기능을 이용함
SELECT LEVEL, articleNO, parentNO, LPAD(' ', 4*(LEVEL-1)) || title title, content, writeDate, id
FROM t_board
START WITH parentNO=0
CONNECT BY PRIOR articleNO = parentNO
ORDER SIBLINGS BY articleNO DESC;
* LEVEL : 오라클에서 제공하는 가상 컬럼임. 부모글은 1 이며, 자식 글은 2, 3, ....
* LPAD(' ', 4*(LEVEL)) || tile : 레벨에 따라 자식글이면 글제목 왼쪽에 공백을 4칸씩 추가해주기 위함. 공백 + 글제목.
* START WITH : 최상위 계층의 ROW(행)을 식별하는 기준
* CONNECT BY PRIOR 기준컬럼 = 하위컬럼 : 부모->자식 으로 계층구조 연결됨
* ORDER SIBLINGS BY articleNO DESC : 계층구조로 조회된 정보를 다시 articleNO 기준 내림차순 정렬(부모글 기준 최신순으로 정렬됨... 최신글일수록 articleNO가 크니까 내림차순 정렬 하는 것임)
pro17에 sec03.br01 패키지를 새로 만들고
관련된 클래스(ArticleVO, BoardController, BoardDAO, BoardService)를 추가함
WebContent 폴더에 board01 폴더를 새로 생성함 > listArticles.jsp 추가함
[ BoardController 클래스 ]
/board/listArticles.do 로 요청 시 화면에 글 목록을 출력하는 역할을 함.
getPathInfo() 메서드로 action 값을 가져오고
action 값이 null (최초요청) 또는 /listArticles.do 인 경우
BoardService 클래스의 listArticles() 메서드를 호출해 전체 글을 조회함
> 조회한 글을 articlesList 속성으로 바인딩하고 글 목록창(listArticles.jsp)으로 포워딩함
@WebServlet(" /board/ * ") /board/~~~ 로 들어오는 요청은 다 이 컨트롤러가 처리함
...
{
BoardService boardService;
ArticleVO articleVO;
...init()
{
boardService = new BoardService(); // 서블릿 초기화 시 BoardService 객체를 생성함
}
...doHandle()
{
String nextPage = ""; // 포워딩할 페이지 저장할 변수
...
String action = request.getPathInfo(); // 요청명 가져옴
...
// try-catch문
List<ArticleVO> articlesList = new ArrayList<ArticleVO> // 게시글 VO 담을 list 생성
if ( action == null ) // 최초요청인 경우
{
articlesList = boardService.listArticles(); // 게시글VO를 요소로 하는 list 반환
request.setAttribute("articlesList", articlesList) // 바인딩
nextPage = "/board01/listArticles.jsp"; // 포워딩 페이지 : 게시글 목록창(jsp)
} else if ( action.equals("/listArticles.do"))
{
// if 수행문과 동일함
}
RequestDispatcher dispatch = request.getRequestDispatcher(nextPage);
dispatch(request, response); // nextPage로 포워딩
} // doHandle() 끝
}
[ BoardService 클래스 ]
생성자 호출 시 BoardDAO 객체를 생성함,
boardDAO.selectAllArticle() 메서드를 호출함 > articlesList가 return됨
...
{
BoardDAO boardDAO;
public BoardService() // BoardService의 기본 생성자
{
boardDAO = new BoardDAO(); // 생성자 호출 시 BoardDAO 객체를 생성함
}
public List<ArticleVO> listArticles()
{
List<ArticleVO> articlesList = boardDAO.selectAllArticles();
return articlesList;
}
}
[ BoardDAO 클래스 ]
BoardService 클래스에서 BoardDAO의 selectAllArticles() 메서드를 호출하면
계층형 SQL문을 이용해 계층형 구조로 전체 글을 조회 후 반환함
public class BoardDAO
{
private DataSource dataFactory;
Connection conn;
PreparedStatement pstmt;
public BoardDAO() // 기본생성자 - DB 연결 설정함
{
// try-catch문
Context ctx = new InitialContext();
Context envContext = (Context)ctx.lookup("java:/comp/env");
dataFactory = (DataSource)envContext.lookup("jdbc/oracle");
}
public List selectAllArticles() // 전체 글 정보 조회하는 SQL문을 실행함
{
List articlesList = new ArrayList(); // 게시글VO 담을 list 생성
// try-catch문
conn = dataFactory.getConnection(); // connection 가져옴
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"; // 위에서 계층형 sql문 사용한 것과 같음, lpad() 만 없음
pstmt = conn.pareparedStatement(query);
ResultSet rs = pstmt.executeQuery(); // 쿼리문 실행함
while(rs.next()) // 조회 결과 한 행씩 가져와서 VO객체 형태로 데이터 저장 -> list에 추가
{
int level = rs.getInt("level"); // 각 글의 깊이(계층)을 의미. number(숫자형) 이므로 getInt() 로 가져옴
int articleNO = rs.getInt("articleNO");
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");
ArticleVO article = new ArticleVO(); // 위의 변수들 담을 게시글VO 객체 생성
article.setLevel(level);
... // 등 setter로 VO객체의 속성에 값 설정함
articlesList.add(article);
}
...//조회된 모든 행 VO로 List에 담았으면 자원 해제 (rs, pstmt, conn)
return articlesList; // 게시글VO 객체 담긴 List 반환함
}
}
[ ArticleVO 클래스 ]
멤버변수로 테이블 t_board의 속성(컬럼)들을 지정함,
인자가 없는 기본생성자와
멤버변수들을 초기화 할 수 있는 인자들을 갖는 생성자를 추가,
각 속성에 대한 setter, getter 추가
[ listArticles.jsp ]
글 목록 표시 페이지,
table 태그 이용해 출력함, 표시 항목은 글번호, 작성자, 제목, 작성일 총 4개의 컬럼으로 표현.
조건문(c:when)으로 articlesList가 null이면 '등록된 글 없음' 출력,
요소(게시글VO)가 하나라도 있으면
반복문(c:forEach)으로 item = articlsList에서 요소 하나씩 꺼내고 (var=article), 상태변수도 지정(varStatus=articleNum)
<td>${articleNum.count}</td> // 글번호 자동 지정. 상태변수 관련 추가 설명 : https://shimmering-sea.tistory.com/131
<td>${article.id}</td> // 작성자id
<td> // 글 제목, 왼쪽 정렬, 들여쓰기 약간(padding-left:30px)
// 제목은 부모글/자식글 여부에 따라 들여쓰기 추가함
<c:choose>
<c:when test='${article.level > 1}'> // level이 1보다 큼 = 자식글 인 경우
<c:forEach begin="1" end="${article.level}" step="1">
<sapn style="padding-left=20px"></span>
</c:forEach> // 자식글이면 level에 따라 20px씩 들여쓰기 추가함
<span 폰트크기 12px>[답변]</span> // 자식글은 '답변' 글임을 명시함
<a class="~~" href="${contextPath}/board/viewArticle.do?articleNO=${article.articleNO}">${article.title}</a>
// href 속성으로 '상세글보기' 요청+parameter로 id를 전달함, 글 상세 기능은 723p~ 부터 구현함
</c:when>
<c:otherwise> // 부모글인 경우
<a class='~~~' href="${contextPath}/board/viewArticle.do?articleNO=${article.articleNO}">${article.title}</a>
</c:otherwise>
</td>
<td>
<fmt:formatDate value="${article.writeDate}" /> // 작성일은 날짜만 출력(formatDate)
</td>
테이블 태그가 끝나면 아래에 a태그로 '글쓰기' 링크 표시, 글쓰기 기능 구현에서 사용할 것.(709p~)
2. 게시판 글쓰기 구현
709p
게시판의 글쓰기 기능 구현 과정
1. 글 목록창(listArticles.jsp) 에서 글쓰기 창을 요청함(a태그로 '글쓰기' 링크 만든것 이용함)
2. 글쓰기 창에서 글을 입력하고 컨트롤러에 /board/addArticle.do 로 글쓰기를 요청함
3. 컨트롤러 - Service 클래스로 글쓰기창에서 입력한 글 정보를 전달해 테이블에 글을 추가함
4. 새 글이 추가되면 컨트롤러에 다시 /board/listArticles.do 로 요청해서 전체 글 목록을 표시함
클래스와 JSP 구현하기 전에 WebContent\lib 폴더에 파일 업로드와 관련된 라이브러리를 미리 복사해 붙여넣음(Apache Commons의 FileUpload 라이브러리, 606p, 참고 https://shimmering-sea.tistory.com/133)
업로드된 파일을 저장할 파일 저장소로 C:\board\article_image 폴더를 만듦
글쓰기 창에서 글과 이미지 파일을 업로드할 수 있는 기능을 구현해보자
sec03.brd02 패키지를 만들고 ArticleVO, BoardController, BoardDAO, BoardService를 복붙,
WebContent의 board2 폴더에 listArticles.jsp를 복붙, articleForm.jsp를 생성
[ BoardController 클래스 ] - 추가
public class BoardController ...
{
private static String ARTICLE_IMAGE_REPO = "C:\\board\\article_image"; // 첨부파일 저장 위치 상수로 선언
// 이스케이프 문자-> " \ " 표현하기 위해 \\ 로 사용했음
...init()
{
서블릿이 초기화될 때 기존의 boardService에 추가로 articleVO 객체도 생성함
}
...
...doHandle()
{
...// action이 null(최초요청) or /listArticles.do 인 경우에 대해 작성했음
else if (action.equals("/articleForm.jsp")) // 글쓰기 창을 요청함
{
nextPage = "/board02/articleForm.jsp";
}
else if (action.equals("/addArticle.do")) // 글 작성 완료 > 글 업로드 요청함
{
Map<String, String> articleMap = upload(request, response);
// upload() : doHandle() 밖에서 작성함, 작성한 글 가져와서 title, content, 첨부 파일 등을 name과 내용(value) 쌍으로 저장해서 반환하는 메서드임
String title = articleMap.get("title"); // Map에서 name이 title인 value를 가져옴
String content = articleMap.get("content");
String imageFileName = articleMap.get("imageFileName");
articleVO.setParentNO(0); // setter로 게시글VO에 속성값 설정함, 새글쓰기 이므로 해당 글이 부모글임(parentNO=0)
articleVO.setId("hong"); // 글 작성자 임의로 hong으로 지정
... // setter로 ParentNO, id 및 title, content, imageFileName 을 설정
boardService.addArticle(articleVO); // Service에서 addArticle() 메서드로 게시글 추가 SQL문을 처리함
nextPage = "/board/listArticles.do"; // 게시글 업로드 후 글 목록으로 돌아감
}
... // RequestDispatcher 이용해 nextPage로 포워딩 처리함
} // doHandle() 끝
private Map<String, String> upload(...request, ... response)
{
Map<String, String> articleMap = new HashMap<String, String>(); // 게시글 정보를 저장하기 위한 객체
String encoding = "utf-8"; // 인코딩 방식 미리 변수로 지정함, 이후 FileItem.getString() 등 form 입력값 가져올때 사용됨
File currentDirPath = new File(ARTICLE_IMAGE_REPO); //파일 저장소 경로 저장된 String 상수 이용해 File 객체 생성
DistFileItemFactory factory = new DiskFileItemFactory(); // DistFileItemFactory : 파일 저장소 관련 클래스
factory.setRepository(currentDirPath); // 저장소 경로 저장된 File 객체를 인자로 하여 첨부파일 저장소가 설정됨
factory.setSizeThreshold(1024*1024); // 1mb 가 threshold임
ServletFileUpload upload = new ServletFileUpload(factory);
// 브라우저에서 request로 들어온 HTTP body 부분(multipart/form-data)를 다루기 쉽게 파싱해줌,
... //try-catch문
List items = upload.parseRequest(request);
// ServletFileUpload#parseRequest() : 파싱된 데이터를 FileItem 형식으로 변환해서 List에 담아서 반환함
for (int i=0; i <items.size(); i++)
{
FileItem fileItem = (FileItem) items.get(i);
if (fileItem.isFormField()) // 해당 요소가 Form 태그로 입력된 값이면 (no 첨부파일)
{
...
articleMap.put(fileItem.getFileName(), fileItem.getString(encoding));
// getFileName() : input의 name값을 가져옴, getString(): 작성된 값을 지정한대로 encoding 해서 반환
}
else // 해당 요소가 첨부파일일때
{
articleMap.put(fileItem.getFieldName(), fileItem.getName());
// input 태그 name값 / 파일명(파일경로) 쌍으로 map에 저장
// 업로드 된 파일 이름을 Map에 ("imageFileName", "파일이름") 으로 저장함
if (fileItem.getSize() > 0)
{
int idx = fileItem.getName().lastIndexOf("\\"); // 파일경로에서 마지막 "\"를 찾아 인덱스 반환(절대경로)
if (idx == -1) // 경로에 \ 가 없음 = 상대경로임
{
idx = fileItem.getName().lastIndexOf("/");
}
String fileName = fileItem.getName().substring(idx+1); // only 파일이름만을 가져옴
File uploadFile = new File(currentDirPath + "\\" + fileName); // 파일저장소 경로 뒤에 파일이름을 붙여줌
fileItem.write(uploadFile); // 파일저장소에 첨부파일 업로드함
} // if문 끝(업로드한 파일이 존재하는 경우에 대해 처리됨)
} // else 끝 (fileItem이 첨부파일인 경우에 대해 다 처리됨)
} // for문 끝 (request로 넘어온 모든 요소 다 처리해서 Map에 추가했음)
return articleMap;
} // upload() 메서드 작성 끝
}
[ BoardService 클래스 ] - 추가
public void addArticle(ArticleVO article) 메서드를 추가함
: boardDAO.insertNewArticle(article); 을 호출함
[ BoardDAO 클래스 ] - 추가
...
private int getNewArticleNO() // 새 글에 대한 글 번호를 반환해줌
{
// try-catch문
conn = dataFactory.getConnection();
String query = "SELECT max(articleNO) from t_board"; // 기존 articleNO 중 가장 큰 번호를 조회함
pstmt = conn.preparedStatement(query);
ResultSet rs = pstmt.executeQuery();
if (rs.next())
{
return (rs.getInt(1) + 1); // 조회한 가장 큰 글번호 + 1을 반환함... return되면 아래 코드는 다 실행안됨
}
rs, pstmt, conn close(); return 0;
}
public void insertNewArticle(ArticleVO article) // 새 글을 추가하기 위한 SQL문을 실행함
{
// try-catch문
conn = dataFactory.getConnection();
int articleNO = getNewArticleNO(); // 위의 메서드를 이용해 새 글 추가 전에 추가할 글의 글번호를 가져옴
int parentNO = article.getParentNO();
... // articleNO 외에 parentNO, title, content, id, imageFileName 은 다 VO 객체인 article에서 getter로 가져옴
String query = "INSERT INTO t_board (articleNO, parentNO, title, content, imageFileName, id)"
+ "VALUES (?, ?, ?, ?, ?, ?)";
pstmt = conn.preparedStatement(query);
pstmt.setInt(1, articleNO);
...// setInt, setString 등을 이용해 물음표에 값 설정함
pstmt.executeUpdate(); // 쿼리 실행
pstmt, conn close();
}
[ listArticles.jsp ] - 수정
'글쓰기' 링크를 표시하는 a 태그의 href 를 # 에서 수정함
<a class="~~" href="${contextPath}/board/articleForm.do">글쓰기</a> // 글쓰기창을 요청함 -> 컨트롤러가 처리, 포워딩됨
[ articleForm.jsp ]
<head>
...<script type="text/javascript">
function readURL(input) { // input type="file"에서 첨부된 파일을 가지고 와서 미리보기 수행함
if(input.files && input.file[0]) { // 첨부파일이 존재하는 경우
var reader = new FileReader(); // FileReader 객체 : input으로 넘어온 file 객체 다루기 위한 객체임
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(); // action으로 요청
}
</script>
...
</head>
<body>
...
<form ... action="${contextPath}/board/addArticle.do" enctype="multipart/form-data"> // 파일 업로드 시 enctype 지정해줘야함
... // 글제목, 글 내용 입력창 만들어좀
<tr>
<td align="right">이미지파일 첨부</td>
<td><input type="file" name="imageFileName" onchange="readURL(this);" /></td>
// onchange : input 태그에서 포커스를 벗어나는 이벤트 = (첨부파일) 입력이 끝났을 때 발생
<td><img id="preview" src="#" 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>
=>
지금의 글쓰기 기능에는 문제가 있음
새 글에 첨부한 파일들이 모두 같은 폴더에 저장된다는 것임. 따라서 다른 사용자가 첨부한 파일과 이름이 같은 파일이 생기면 구별이 어려움.
-> 이를 해결하기 위해 업로드한 파일이 각각의 글번호(articleNO)를 이름으로 하는 폴더를 생성하고 저장할 수 있도록 해보자
1. 글쓰기 창에서 새 글 전송 시('글쓰기' 버튼 누름) 컨트롤러의 upload() 메서드를 호출해 새 글 정보를 Map으로 반환받고, 첨부한 파일은 임시로 temp 폴더에 업로드함
2. 컨트롤러는 Service 클래스의 addNewArticle() 메서드를 호출하면서 새 글의 정보를 인자로 전달해 테이블에 추가한 후 새 글 번호(articleNO)를 반환받음
3. 컨트롤러에서 반환받은 새 글 번호를 이용해 파일 저장소에 새 글 번호로 폴더를 생성하고, temp 폴더의 파일을 새 글 번호 폴더로 이동시킴
로컬PC의 파일 저장소(C:\board\article_image)에 temp 폴더를 생성함,
sec03.brd03 패키지를 만들고, ArticleVO, BoardController, BoardDAO, BoardService 를 복붙함
719p
[ BoardController 클래스 ] - 수정
upload() 메서드를 호출해서 첨부한 파일을 temp 폴더에 업로드 후 새 글 정보를 Map으로 가져옴,
새 글을 테이블에 추가 후 반환받은 새 글 번호로 폴더 생성, temp 폴더의 이미지를 새글 번호 폴더로 이동함
... doHandle()
{
...
else if (action.equals("/addArticle.do")) // 글쓰기 버튼 누른 후 들어온 요청 처리함
{
int articleNO = 0;
Map<String, String> articleMap = upload(request, response);
// upload() : 새 글 정보를 Map으로 반환하는 메서드, 이전과 달리 첨부파일을 지정한 파일 저장소가 아닌 그 안의 temp에 업로드되도록 변경됨
...// title, content, imageFileName 은 articleMap에서 get()으로 가져옴, init()에서 생성된 VO객체에 setter로 parentNO, id, title, content,imageFileName 설정함
// articleNO는 내가 정하거나 사용자가 정하는 것이 아니라 쿼리를 통해 자동으로 정해지므로 내가 설정하지 않음
articleNO = boardService.addArticle(articleVO);
// 원래 addArticle()은 반환값이 void였는데 articleNO를 반환하도록 아래에서 수정할 것임
if (imageFileName != null && imageFileName.length() != 0) // 새 글에 첨부파일이 있는 경우에 수행됨
{ // upload()에서 temp에 저장한 이미지 파일을 다시 원하는 폴더 생성 후 거기로 옮기기 위한 코드임
File srcFile = new File(ARTICLE_IMAGE_REPO + "\\" + "temp" + "\\" + imageFileName);
// temp 폴더에 임시로 업로드된 파일 객체를 생성함
File destDir = new File(ARTICLE_IMAGE_REPO + "\\" + articleNO);
destDir.mkdirs(); // 새로 파일을 저장할 디렉터리를 지정 후 생성해줌
FileUtils.moveFileToDirectory(srcFile, destDir, true);
// 파일을 이동시키는 메서드, 인자 : (srcFile(옮길파일객체), destDir(목적지 디렉터리), true(boolean)), boolean이 true이면 디렉터리가 존재하지 않을 시 생성해서 파일을 이동시킴, false이면 디렉터리 존재하지 않을 시 예외 발생함
} // 첨부파일 옮기기 끝
PrintWriter pw = response.getWriter();
pw.print("<script>" + "alert('새 글을 추가했습니다');" + "location.href=' " + request.getContextPath() + "/board/listArticles.do'; " + "</script>" );
// 자바스크립트를 통해 새 글 등록 메세지를 나타내고, location 객체의 href 속성을 이용해 글 목록을 요청함
// *** 자바스크립트에서 페이지 이동 : location.href = "이동할 페이지"
return; // if문을 다 빠져나가면 하단의 Dispatcher 통해서 nextPage로 포워딩하는 부분이 있는데, return으로 doHandle()이 종료되므로 Dispatcher 부분 수행x
} // else if문 종료(action="/addArticle.do" 처리)
... // RequestDispatcher 통해 nextPage로 포워딩 처리
} // doHandle() 종료
private Map<String, String> upload(~~)
// form 태그로 전송된 작성글 데이터를 Map 형태로 반환, 이전에는 첨부파일을 파일저장소에 그대로 업로드 했으나 파일저장소/temp에 임시 업로드하도록 수정됨
{
...
// 첨부파일 업로드 하는 부분까지 내려감, 경로명을 제외하기 위해 \ or / 의 인덱스 찾는 부분 지나서
String fileName = fileItem.getName().subString(idx + 1); // 경로를 제외한 only 파일이름만 가져옴
File uploadFile = new File(currentDirPath + "\\temp\\" + fileName);
fileItem.write(uploadFile); // temp 폴더에 첨부파일 임시 업로드함
}
[ BoardService 클래스 ] - 수정
addArticle() 메서드는 원래 반환타입이 void
=> 원래 수행 내용이던 boardDAO.insertNewArticle(article)을 return문으로 작성해서 글번호(articleNO)를 (컨트롤러로) 반환하도록 수정함 (insertNewArticle()도 반환타입이 void였지만 글번호(articleNO)를 반환하도록 수적함)
...
public int addArticle(ArticleVO article) {
return boardDAO.insertNewArticle(article);
}
...
[ BoardDAO 클래스 ] - 수정
원래 반환타입 void 였으나 articleNO를 반환하도록 수정해줌
int articleNO = getNewArticleNO(); 는
원래 try-catch 문 안에 있었는데, try-catch문 밖에 있는 return 에서 사용하기 위해 try 밖으로 빼줌,
return articleNO; 추가해줌
=> 첨부파일 업로드 시 각각의 글 번호로 폴더가 생성되어 그 안에 첨부파일이 저장되는 것 확인할 수 있음
3. 글 상세 기능 구현
723p
글 목록에서 글 제목을 클릭했을 때 글의 상세 내용을 보여주는 기능을 구현해보자
1. 글 목록창(listArticles.jsp)에서 글 제목을 클릭하면 컨트롤러에 ' /board/viewArticle.do?articleNO=글번호 ' 로 요청함
2. 컨트롤러는 전송된 글 번호로 글 정보를 조회하여 글 상세창(viewArticle.jsp)으로 포워딩 함
3. 글 상세창에 글 정보와 이미지 파일이 표시됨
brd04 패키지를 만들고 ArticleVO, BoardController, BoardDAO, BoardService를 복붙함,
common 패키지를 만들고 FildeDownloadController 를 생성함(첨부 이미지 표시 기능)
WebContent에 board03 폴더를 만들고 articleForm.jsp, listArticles.jsp 를 복붙, viewArticle.jsp 를 생성
[ FileDownloadController 클래스]
viewArticle.jsp에서 전송한 글 번호와 이미지 파일 이름으로 파일 경로를 만든 후 해당 파일을 내려 받음
(첨부파일 경로 : 저장소/글번호 이름 폴더/ 이미지 파일)
@WebServlet("/download.do")
...
doHandle(~~)
{
...
String imageFileName = (String) request.getParameter("imageFileName");
String articleNO = request.getParameter("articleNO"); // viewArticle.jsp에서 전송된 파일이름, 글번호 가져옴
OutputStream out = response.getOutputStream();
String path = ARTICLE_IMAGE_REPO + "\\" + articleNO + "\\" + imageFileName;
File imageFile = new File(path); // 업로드 파일 객체를 생성함
response.setHeader("Cache-Control", "no-cache"); // Cache-control : 개인캐시를 사용해도 되는지 등에 대한 설정
response.addHeader("Content-disposition", "attachment;fileName=" + imageFileName)
// content-disposition : http response body에 오는 컨텐츠의 기질/성향을 알려줌.
// 기본값은 inline으로 web에 전달되는 data임,
// 첨부파일을 읽어오는 경우 이 값을 attachment 로 주고, fileName과 함께 주면 body에 오는 값을 다운받으라는 뜻이 됨
FileInputStream in = new FileInputStream(읽어올 파일);
byte[] buffer = new byte[파일을 한번에 읽어올 바이트 수];
while (true)
{
int count = in.read(buffer);
// read()는 원래 1byte씩 읽어옴,
//너무 작으니까 원하는 byte만큼 읽어올 수 있도록 원하는 크기의 바이트 배열을 인자로 넣어줌
// 읽은 바이트 수를 반환함, 더이상 읽을 데이터가 없으면 -1 반환함
if (count == -1)
break; // 데이터 다 읽어왔으면 출력 멈춤
out.write(buffer, 0, count);
// write(String s, int start, int len) : s의 start 지점부터 len 길이(버퍼로 읽은 크기, 여기선 보통 8kb)만큼 출력시킴
}
in.close(); out.close();
[ BoardController 클래스 ] - 추가
글 상세 조회 요청이 들어오면 getParameter() 로 조회할 글 번호를 가져오고
BoardService.viewArticle() 메서드를 이용해 해당 글 번호를 가진 게시글VO를 받아와 reqeust에 바인딩,
글 상세조회 창(viewArticle.jsp)으로 포워딩함
...
else if (action.equals("/viewArticle.do")) // 글 목록창에서 상세조회를 요청하는 경우
{
String articleNO = request.getParameter("articleNO"); // 넘겨받은 조회할 글 번호를 가져옴
articleVO = boardService.viewArticle(Integer.parseInt(articleNO)); // 글번호 int로 형변환 해서 해당 글번호를 가진 게시글VO를 가져옴 by viewArticle() 메서드
request.setAttribute("articleVO", articleVO); // 게시글VO를 바인딩
nextPage = "/board03/viewArticle.jsp"; // 글 상세 조회 창으로 포워딩함
}
... // RequestDispatcher 이용해 nextPage로 포워딩함
[ BoardService 클래스 ] - 추가
컨트롤러에서 전달받은 글 번호로 BoardDAO의 selectArticle() 메서드를 호출해서 articleVO를 반환받음
...
public ArticleVO viewArticle(int articleNO)
{
ArticleVO article = null;
article = boardDAO.selectArticle(articleNO); // DAO의 selectArticle() 메서드를 호출해서 해당 글번호의 글VO를 반환받음
return article;
}
[ BoardDAO 클래스 ] - 추가
전달받은 글 번호를 이용해 글 정보를 조회함
...
public ArticleVO selectArticle(int articleNO)
{
ArticleVO 객체 생성,
인자로 받은 게시글 번호 갖는 게시글 정보를 조회하는 쿼리문을 날림,
조회결과에서 getInt(), getString(), getDate() 등으로 조회 결과 가져와서 변수에 저장함,
생성했던 ArticleVO 객체에 setter로 저장해둔 변수 값들을 속성에 설정함
rs, pstmt, conn 등 자원 해제 후 articleVO 객체 return함
}
[ viewArticle.jsp ]
BoardController 에서 바인딩한 글 정보(ArticleVO) 를 이용해 글 상세 내용을 출력함(글번호, 작성자id, 제목, 내용, 등록일자 등)
단, 이미지파일이 존재하는 경우 img 태그의 src 속성에서 FileDownloadController로 요청해서 이미지 파일을 표시하도록 함.
...
글번호, 작성자id, 글제목은 input 태그 value 속성을 이용해서 출력, disabled 속성을 줘서 사용자가 수정할 수 없도록함
글 내용은 textarea 태그를 이용, 마찬가지로 disabled로 설정. <textarea>~</textarea> 사이에 표현언어로 내용 출력
//첨부파일이 있는 경우, 이를 출력할 수 있도록 함
<c:if test="${not empty article.imageFileName && article.imagFileName != 'null' }">
<tr>
<td ~~~~~~>이미지</td>
<td>
<input type="hidden" name="originalFileName" value="${article.imageFileName}" /> // 원래 이미지 파일 이름을 저장함
<img scr="${contextPath}/download.do?imageFileName=${article.imageFileName}&articleNO=${article.articleNO}"
id="preview" /><br> // FileDownloadController로 요청해서 이미지파일의 src를 가져옴
</td>
</tr>
// 아래에 수정하기, 삭제하기, 리스트로 돌아가기, 답글쓰기 버튼이 있음
4. 글 수정 기능 구현
729p
1. 글 상세창(viewArticle.jsp)에서 '수정하기' 를 클릭해 글 정보를 표시하는 입력창들을 활성화함
2. 글 정보와 이미지를 수정한 수 '수정반영하기' 를 클릭해 /board/modArticle.do 로 요청함
3. 컨트롤러는 요청에 대해 upload() 메서드를 이용하여 수정된 데이터를 Map에 저장, 반환함
4. 컨트롤러는 수정된 데이터를 테이블에 반영 후 temp 폴더에 업로드된 수정 이미지를 글 번호 폴더로 이동함
5. 글 번호 폴더에 있던 원래 이미지 파일을 삭제함
sec03.brd05 패키지를 만들고 ArticleVO, BoardController, BoardDAO, BoardService를 복붙함
??? 1. 같은 파일 재업로드 / 2. 파일명이 같은 파일 업로드 / 3. 수정시에 첨부 파일은 바꾸지 않는 경우
어떻게 처리하는지
-> 3. 은 input type="file"에서 imageFileName이 null 이므로 아래 작업 중 첨부파일에 대해서는 수행x
[ BoardController 클래스 ] - 추가
...
else if (action.equals("/modArticle.do")) // '수정반영하기' 요청이 들어온 경우
{
upload() 메서드로 수정된 게시글 정보 Map으로 반환-> articleMap에 저장
(수정할 때 첨부파일 있었으면 수행함)
articleMap에서 get()으로 articleNO가져와서 int로 변환 후 articleVO에 setter로 값 설정
마찬가지로 title, content, imageFileName 다 articleMap.get()으로 가져온 후 변수에 저장,
articleVO에 setter로 값 설정
boardService.modArticle(articleVO); // BoardDAO.updateArticle(articleVO) 호출해 테이블에서 게시글 정보 update
if (imageFileName != null && imageFileName.length() != 0) // 첨부파일 이름이 넘어왔으면
{
String originalFileName = articleMap.get("originalFileName"); // 기존 파일이름 가져와 저장
File srcFile = new File(ARTICLE_IMAGE_REPO + "\\" + "temp" + "\\" + imageFileName);
// temp에 업로드된 바뀐 파일 객체 생성
File destDir = new File(ARTICLE_IMAGE_REPO + "\\" + articleNO); // 옮겨질 디렉토리
destDir.mkdirs(); // 디렉토리를 생성, 이미 존재할 경우 false를 return(예외발생하지 않음)
FileUtils.moveFileToDirectory(srcFile, destDir, true); // 바뀐 이미지 파일을 폴더로 이동시킴
File oldFile = new File(ARTICLE_IMAGE_REPO + "\\" + articleNO + "\\" + originalFileName);
oldFile.delete(); // 기존 이미지 파일은 삭제함
}
PrintWriter pw = response.getWriter();
pw.print("<script>" + "alert('글을 수정했습니다');"
+ " location.href='" + request.getContextPath() + "/board/viewArticle.do?articleNO=" + articleNO + "';" + </script>);
// 수정 완료 시 alert창을 띄우고, location 객체의 href 속성을 이용해 글 상세 창으로 이동함
return; // doHandle() 종료
}
... // RequestDispatcher 로 포워딩 하는 부분
[ BoardService 클래스 ] - 추가
컨트롤러 -> Service.modArticle() -> DAO.updateArticle() 순으로 호출됨
...
public void modArticle(ArticleVO article)
{
boardDAO.updateArticle(article);
}
[ boardDAO 클래스 ] - 추가
전달된 수정 데이터에 대해 이미지 파일을 수정하는 경우와 수정하지 않는 경우로 나누어 동적으로 SQL문을 생성해서 수정 데이터를 반영함
...
public void updateArticle(ArticleVO article)
{
articleVO에서 getter로 articleNO, title, content, imageFileName을 가져와 변수에 저장함
try-catch문으로 쿼리 실행,
...
String query = "update t_board set title=?, content=?";
if (imageFileName != null && imageFileName.length() != 0)
{
query += ", imageFileName=?";
}
query += " where articleNO=?";
}
...
pstmt.setString(1, title);
pstmt.setString(2, content);
if (imageFileName != null && imageFileName.length() != 0)
{
pstmt.setString(3, imageFileName);
pstmt.setInt(4, articleNO);
} else {
pstmt.setInt(3, articleNO);
}
...
pstmt.executeUpdate(); pstmt, conn close();
}
[ viewArticle.jsp ] - 수정
글 상세 조회 창,
'수정하기' 클릭하면
-> fn_enable() 함수를 호출해서 비활성화됐던 input 등을 활성화시킴(input 텍스트박스, textarea, input file 등에 id로 접근해서 disabled 속성을 false로 설정함) & '수정반영하기', '취소' 버튼 나타나도록함(id로 접근해서 style.display 속성을 block으로 바꿔줌) & '수정하기', '삭제하기', '리스트로 돌아가기', '답글쓰기' 버튼은 사라지도록 함(id로 접근해서 style.display 속성을 none으로 바꿔줌)
'수정 반영하기' 클릭하면
-> fn_modify_article() 함수를 호출해서 수정된 데이터를 컨트롤러에 전송함
...
<head>
...
<style>
#tr_btn_modify { display: none; } // 수정반영하기, 취소 버튼을 화면에 표시하지 않음
</style>
...
<c:choose>
<c:when test="${not empty article.imageFileName && article.imageFileName != 'null' }">
// 기존에 이미지 파일을 첨부했던 경우
<script type="text/javascript">
function fn_enable(obj) { // (상세 창)수정하기
document.getElementById("i_title").disabled=false;
등 document.getElementById() 를 이용해서
"i_content", "i_imageFileName" 도 disabled=false로 설정(글 제목, 내용, 첨부파일 선택 모두 변경할 수 있게 설정)
"tr_btn_modify" 의 style.display="block" 으로 설정('수정반영하기', '취소' 버튼을 화면에 나타나도록 함)
"tr_btn" 의 style.display="none" 으로 설정('수정하기','삭제하기','리스트로 돌아가기','답글쓰기' 버튼 화면에 표시하지않음)
}
</script>
</c:when>
<c:otherwise> // 기존에 이미지 파일을 첨부하지 않았던 경우
<script type="text/javascript">
function fn_enable(obj) { // (상세 창)수정하기
document.getElementById() 를 이용해 "i_title", "i_content" 등을 disabled=false; 로 설정
등 위의 c:when과 수행하는 내용 거의 같음, 단, 첨부파일 선택칸은 변경할 수 없도록 그대로 disabled로 둠
}
</script>
</c:otherwise>
</c:choose>
<script type="text/javascript">
function backToList(obj) { // (수정 창)취소, (상세 창)리스트로 돌아가기
obj.action="${contextPath}/board/listArticles.do";
obj.submit();
}
function fn_modify_article(obj) { (수정 창)수정 반영하기
obj.action="${contextPath}/board/modArticle,do";
obj.submit();
}
function fn_remove_article(url, articleNO) { (상세 창)삭제하기 ... 741p, 아래에서 추가 설명함
var form = document.createElement("form");
form.setAttribute("method", "post");
form.setAttribute("action", url);
var articleNOInput = document.createElement("input");
articleNOInput.setAttribute("type", "hidden");
articleNOInput.setAttribute("name", "articleNO");
articleNOInput.setAttribute("value", articleNO);
form.appendChild(articleNOInput);
document.body.appendChild(form);
form.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>
<body>
<form name="frmArticle" method="post" action="${contextPath}" enctype="multipart/form">
...
// 글 번호
<td>
<input type="text" value="${article.articleNO}" disabled />
<input type="hidden" name="articleNO" value="${article.articleNO}" />
</td>
// 작성자 아이디
<td>
<input type="text" value="${article.id}" name="writer" disabled />
</td>
// 제목
<td>
<input type="text" value="${article.title}" name="title" id="i_title" disabled />
</td>
// 내용
<td>
<textarea ,,, name="content" id="i_content" disabled >${article.content}</textarea>
</td>
// 이미지
<c:if test="${not empty article.imageFileName && article.imageFileName != 'null' }" > // 첨부파일 있을때만 이미지칸 표시
<td>
<input type="hidden" name="originalFileName" value="${article.imageFileName}" />
<img src="${contentPath}/download.do?articleNO=${article.articleNO}&imageFileName=${article.imageFileName}"
id="preview" /></br>
</td>
<td>
<input type="file" name="imageFileName" id="i_imageFileName" disabled onchange="readURL(this)" />
</td>
</c:if>
// 등록일자
<td>
<input type=text value="<fmt:formatDate value='${article.writeDate}' />" disabled />
</td>
<tr id="tr_btn_modify">
<td>
<input type="button" value="수정반영하기" onclick="fn_modify_article(frmArticle)" >
<input type="button" value="취소" onclick="backToList(frmArticle)">
</td>
</tr>
<tr id="tr_btn">
<td>
<input type="button" value="수정하기" onclick="fn_enable(this.form)">
<input type="button" value="삭제하기" onclick="fn_remove_article('${contextPath}/board/removeArticle.do', ${article.articleNO})">
<input type="button" vlaue="리스트로 돌아가기" onclick="backToList(this.form)">
<input thpe="button" value="답글쓰기" onclick="fn_reply_form('${contextPath}/board/replyForm.do', ${article.articleNO})">
</td>
</tr>
5. 글 삭제 기능 구현
737p
글을 삭제할 때는 테이블의 글 뿐만 아니라 그 글의 자식글과 이미지 파일도 함께 삭제해야함
1. 글 상세창(viewArticle.jsp)에서 '삭제하기' 를 클릭하면 /board/removeArticle.do 로 요청함
2. 컨트롤러에서는 글 상세창에서 전달받은 글 번호에 대한 글과 이에 관련된 자식 글들을 삭제함
3. 삭제된 글에 대한 이미지 파일 저장 폴더도 삭제함
자식 글이 있는 부모 글 삭제 SQL문
DELETE FROM t_board
WHERE articleNO in (
SELECT articleNO
FROM t_board
START WITH articleNO=2 -- 글번호가 2인 부모글과 그 아래 자식글을 조회함 --
CONNECT BY PRIOR articleNO = parentNO
);
START WITH : 계층구조에서 최상위에 뭐가 올지 지정함,
여기서는 connect by 절에 의해 부모->자식으로 내려가는 계층구조이고,
최상위에 글번호2인 항목이 위치한다면, IN 안의 SELECT절은 글번호2인 글과 그 아래 자식글을 조회하게 됨
따라서 글번호 2인 글과 그 자식글들의 articleNO를 가져와서 그 행들을 테이블에서 삭제함
sec03.brd06 패키지 생성-> ArticleVO, BoardController, BoardDAO, BoardService 클래스 복붙
WebContent에 board05 폴더 생성 -> articleForm.jsp, listArticles.jsp, viewArticle.jsp 복붙
[ BoardController 클래스 ] - 추가
브라우저에서 삭제를 요청하면(글 상세 창에서 '삭제하기' 클릭)
글 번호를 getParameter()로 가져온 후 boardService.removeArticle() 의 인자로 전달함,
-> 글을 삭제하기 전에 삭제할 글 번호들을 ArrayList 객체에 저장해서 return 받아 컨트롤러로 돌려주고(boardDAO.selectRemovedArticles(articleNO))
해당 글~자식글을 삭제하도록함(boardDAO.deleteArticle(articleNO) 호출),
...
else if (action.equals("/removeArticle.do"))
{
int articleNO = Integer.parseInt(request.getParameter("articleNO")); // 삭제요청에서 게시글 번호를 받아옴
List<Integer> articleNOList = boardService.removeArticle(articleNO);
// service의 삭제 메서드 호출, 삭제될 게시글들의 articleNO를 List로 반환받아서 저장함
for (int _articleNO : articleNOList)
{
File imgDir = new File(ARTICLE_IMAGE_REPO + "\\" + _articleNO);
if (imgDir.exists()) // 해당 게시글 번호 이름으로 된 업로드 폴더가 존재하면
{
FileUtils.deleteDirectory(imgDir); // 업로드 폴더를 삭제해서 업로드했던 파일까지도 삭제함
}
}
PrintWriter pw = response.getWriter();
pw.print("<script>" + "alert('글을 삭제했습니다');" + " location.href = '" + request.getContextPath() + "/board/listArticles.do';" + "</script>"); // 삭제 후 alert창을 띄우고 게시글 목록 창으로 이동함
return; // doHandle() 종료, dispatcher 이용하지 않도록 함
}
[ BoardService 클래스 ] - 추가
컨트롤러에서 BoardService의 removeArticle() 메서드를 호출하면
매개변수 articleNO로 삭제할 글 번호를 전달받아
BoardDAO의 selectRemovedArticles()를 먼저 호출해서 인자로 받은 글 번호와 그 자식글의 글번호를 articleNOList에 저장함, 그 다음 deleteArticle() 메서드를 호출해 글 번호의 글과 그 자식글을 삭제함
...
public List<Integer> removeArticle(int articleNO)
{
List<Integer> articleNOList = boardDAO.selectRemovedArticles(articleNO);
// 글 삭제 전 삭제될 글들의 글번호를 list로 저장함
boardDAO.deleteArticle(articleNO);
return articleNOList; // 삭제한 글 번호 List를 컨트롤러로 return함
}
[ BoardDAO 클래스 ]
selectRemovedArticles() : 삭제할 글에 대한 글 번호들을 가져옴
deleteArticle() : 전달된 articleNO에 대한 글~자식글까지 삭제함
...
public void deleteArticle(int articleNO)
{
// try-catch문
conn = dataFactory.getConnection();
String query = "DELETE FROM t_board ";
query += "WHERE articleNO IN ("
query += "SELECT articleNO FROM t_board ";
query += "START WITH articleNO=? "; // 삭제할 글을 계층의 최상위로 둠
query += "CONNECT BY PRIOR articleNO = parentNO )"; // 계층형 SQL문을 이욯해 삭제글과 관련된 자식글까지 삭제함
pstmt = conn.preparedStatement(query);
pstmt.setInt(1, articleNO);
pstmt.executeUpdate();
pstmt, conn close()함
}
public List<Integer> selectRemovedArticles(int articleNO)
{
List<Integer> articleNOList = new ArrayList<Integer>();
// try-catch문
conn = dataFactory.getConnection();
String query = "SELECT articleNO FROM t_board ";
query += "START WITH articleNO=? ";
query += "CONNECT BY PRIOR articleNO = parentNO"; // 삭제한 글들의 articleNO를 조회함
pstmt = conn.preparedStatement(query);
pstmt.setInt(1, articleNO);
ResultSet rs = pstmt.executeQuery();
while(rs.next())
{
articleNO = rs.getInt("articleNO");
articleNOList.add(articleNO);
}
pstmt, conn close();
...
return articleNOList;
}
[ viewArticle.jsp ] - 추가
* 위의 수정 구현에서 이미 작성했음
글 상세 창에서 '삭제하기' 를 클릭하면 fn_remove_article() 함수를 호출해서 글 번호 articleNO를 컨트롤러로 전송하도록 구현함
function fn_remove_article(url, articleNO) { (상세 창)'삭제하기' 클릭 시 호출됨
var form = document.createElement("form"); // javascript 이용해서 동적으로 <form> 태그를 생성함
form.setAttribute("method", "post");
form.setAttribute("action", url); // 인자로 넘어온 url(${contextPath}/board/removeArticle.do)로 전송함
var articleNOInput = document.createElement("input"); // javascript 이용해서 동적으로 <input> 태그를 생성함
articleNOInput.setAttribute("type", "hidden");
articleNOInput.setAttribute("name", "articleNO");
articleNOInput.setAttribute("value", articleNO); // name/value를 이용해 삭제할 articleNO를 설정함->컨트롤러로 전송함
form.appendChild(articleNOInput); // <input> 태그를 <form> 태그에 append함
document.body.appendChild(form); // <form> 태그를 <body>에 append함
form.submit(); // 서버에 요청함(action에서 지정한대로 요청 넘어감)
}
...
<input type="button" value="삭제하기"
onclick="fn_remove_article( '${contextPath}/board/removeArticle.do' , ${article.articleNO})">
...
6. 답글 쓰기 기능 구현
743p
1. 글 상세창(viewArticle.jsp)에서 '답글쓰기' 를 클릭하면 요청명을 /board/replyForm.do 로 해서 부모글 번호(parentNO)를 컨트롤러로 전송함
2. 답글 쓰기창(replyForm.jsp)에서 답변 글을 작성한 후 요청명을 /board/addReply.do 로 해서 컨트롤러로 요청함
3. 컨트롤러에서는 전송된 답글 정보를 게시판 테이블에 부모글 번호와 함께 추가함
sec03.brd07 패키지 생성 -> ArticleVO, BoardController, BoardDAO, BoardService를 복붙
WebContent에 board06 폴더 생성 -> articleForm.jsp, listArticles.jsp, viewArticle.jsp를 복붙, replyForm.jsp를 추가
[ BoardController 클래스] - 추가
답글 기능도 새 글 쓰기 기능과 비슷함.
다만 답글창 요청 시(/replyForm.do) 부모글 번호를 미리 세션에 저장해놓고,
답글을 작성한 후 등록을 요청하면(/addReply.do) 세션에서 부모글 번호를 가져와 테이블에 추가한다는 차이가 있음
doHandle(...)
{
String nextPage="";
..
HttpSession session; // 답글의 부모글 번호를 저장하기 위해 세션을 선언함
...
else if (action.equals("/reqlyForm.do"))
{
int parentNO = Integer.parseInt(request.getParameter("parentNO"));
session = request.getSession();
session.setArrtibute("parentNO", parentNO); // 세션에 부모글 번호를 속성으로 저장함
nextPage = "/board06/replyForm.jsp"; // 답글 작성 창으로 포워딩
}
else if (action.equals("addReply.do"))
{
session = request.getSession();
int parentNO = (Integer)session.getAttribute("parentNO");
session.removeAttribute("parentNO"); // 부모글 번호 가져온 후에는 세션에서 삭제해줌
... 이후로는 새 글 쓰기와 동일
upload() 메서드로 게시글 정보 담은 Map 반환받아서
Map.get("key") 로 title, content, imageFileName 값 가져와서 변수에 저장
articleVO 객체에 setter로 값 설정, parentNO는 위에서 session으로 받아온 값을 이용함
답글 추가시에는 boardService.addReply(articleVO) 를 호출함, 게시글 번호를 반환받아 변수에 저장함
답글쓰기 창에서 넘어온 imageFileName이 있으면
temp 폴더에서 반환받은 게시글번호 이름으로 폴더를 만들어서 옮겨줌
PrintWriter 객체 생성 후 print로 자바스크립트를 이용해서 답글을 추가했다는 alert창을 띄우고 location.href로 viewArticle.do 요청해서 페이지 이동함,
return;
} // else if 끝
... // dispatcher 부분
} // doHandle() 끝
[ BoardService 클래스 ] - 추가
새글쓰기와 동일하게 BoardDAO.insertNewArticle() 메서드를 이용함
public int addReply(ArticleVO article)
{
return boardDAO insertNewArticle(article); // 답글 추가시에도 새글쓰기와 같은 메서드 사용함, 첨부파일 폴더 생성을 위해 게시글 번호를 return받아서 컨트롤러로 전달함
}
[ viewArticle.jsp ] - 추가
글 상세창, '답글쓰기'를 클릭하면 fn_reply_form() 함수를 호출하면서 인자로 요청명과 글번호가 전달되고,
함수는 <form> 태그와 <input> 태그를 동적으로 만들어서 인자의 요청명으로 글번호를 전달함
...
<script>
function fn_reply_form(url, parentNO) {
var form = document.createElement("form");
form.setAttribute("method", "post");
form.setAttribute("action", url); // 요청명 저장
var parentNOInput = document.createElement("input");
parentNOInput.setAttribute("type", "hidden");
parentNOInput.setAttribute("name", "parentNO");
parentNOInput.setAttribute("value", parentNO);
form.appendChild(parentNOInput);
document.body.appendChild(form);
form.submit();
}
...
</script>
...
<input type="button" value="답글쓰기"
onclick="fn_reply_form('${contextPath}/board/replyForm.do', ${article.articleNO})">
...
[ replyForm.jsp ]
...
<form name="frmReply" method="post" action="${contextPath}/board/addReply.do" enctype="multipart/form-data">
... table 태그를 이용해 게시글 입력칸을 만들어줌
글쓴이(<input> text, disabled), 글 제목(<input> text), 글 내용(<textarea>), 이미지 파일 첨부(<input> file, onchange="readURL(this)"), 첨부파일미리보기(<img>)
답글 반영하기 submit 버튼과 취소 버튼(onclick="backToList(this.form)")이 있음
*** 함수의 인자 this : <form>객체 내의 해당 객체를 전송, this.form : <form>객체 전체를 전송함
7. 게시판 페이징 기능 구현
750p
한 페이지마다 10개의 글이 표시되고, 이 페이지 10개가 모여 한 개의 섹션(section)이 됨.
섹션 하나는 첫번째 페이지~열번째 페이지까지임.
두번째 섹션은 11번째 페이지~20번째 페이지까지임,
사용자가 글목록 페이지 하단 페이지[2] 를 클릭 시 -> 서버에 section 값은 1, pageNum값은 2를 전송함
-> 두번째 페이지에 해당하는 글인 11~20번째 글을 테이블에서 조회 후 표시됨
페이징 기능을 추가한 글 목록 조회 SQL문(section과 pageNum 이용)
SELECT *
FROM (
SELECT ROWNUM as recNum, --조회 결과에 대해 오라클이 순서를 부여해서 할당함 --
LVL, articleNO, parentNO, title, content, id, writeDate
FROM (
SELECT LEVEL as LVL, articleNO, parentNO, title, content, id, writeDate
FROM t_board
START WITH parentNO=0
CONNECT BY PRIOR articleNO=parentNO
ORDER SIBLINGS BY articleNO DESC )
)
WHERE recNum between (section-1)*100+(pageNum-1)*10+1 and (section-1)*100+pagenum*10;
-- section값이 1이고 pageNum값이 1인 경우, recNum between 1 and 10
실행순서
: 기존 계층형 구조로 글 목록을 조회함
-> 그 결과에 대해 다시 ROWNUM(recNum) 이 표시되도록 조회함
-> 그 결과에 대해 WHERE 절의 between 연산자 사이의 사이의 값을 갖는 ROWNUM만 조회함
[ BoardController 클래스 ] -수정
/board(최초요청) 또는 /board/listArticles.do 로 요청이 들어오면
전달된 section, pageNum 값을 받아오거나, 없으면 둘다 1로 초기화함,
pagingMap에 section과 pageNum을 추가하고
boardService.listArticles(pagingMap); 을 호출해서 해당 페이지의 게시글VO가 담긴 List(articlesList)와 총 게시글 수(totArticles) 가 들어있는 articlesMap을 받아옴
articlesMap에 section과 pageNum고 추가하고, request에 바인딩함
nextPage="/board07/listArticles.jsp" 로 포워딩함
[ BoardService 클래스 ] - 추가
public List<ArticleVO> listArticles() 와 별개로
pagingMap을 인자로 받아서 게시글목록 List와 전체 게시글수가 저장된 Map을 반환받아 컨트롤러로 넘겨주는 public Map listArticles(Map pagingMap) 을 작성함
public Map listArticles(Map pagingMap)
{
Map articlesMap = new HashMap();
List<ArticleVO> articlesList = boardDAO.selectAllArticles(pagingMap);
// pagingMap 안에 든 section과 pageNum으로 필요한 글 목록을 조회해서 list에 저장함
int totArticles = boardDAO.selectTotArticles(); // 테이블에 존재하는 전체 글 수를 조회함
articlesMap.put("articlesList", articlesList);
articlesMap.put("totArticles", totArticles);
return articlesMap; // 해당 페이지의 게시글목록과 전체 게시글 수가 들어있음, 컨트롤러로 반환됨
}
[ BoardDAO 클래스 ] - 추가
public List selectAllArticles(Map pagingMap)
{
List articlesList = new ArrayList();
int section = (Integer)pagingMap.get("section");
int pageNum = (Integer)pagingMap.get("pageNum");
// try-catch문
conn = dataFactory.getConnection();
String query = "SELECT * FROM ( "
+ "SELECT ROWNUM as recNum, LVL, articleNO, parentNO, title, id, writeDate FROM ("
+ " SELECT LEVEL as LVL, articleNO, parentNO, title, id, writeDate FROM t_board "
+ " START WITH parentNO=0 "
+ " CONNECT BY PRIOR articleNO = parentNO "
+ " ORDER SIBLINGS BY articleNO DESC)" + " ) "
+ "WHERE recNum BETWEEN (?-1)*100+(?-1)*10+1 AND (?-1)*100+?*10";
...
pstmt.setInt(1, section);
pstmt.setInt(2, pageNum);
pstmt.setInt(3, section);
pstmt.setInt(4, pageNum);
...
반복문을 통해 rs에서 level, articleNO, parentNO, title, id, writeDate 가져와서 변수에 저장,
ArticleVO 객체 생성 후 setter로 속성값 설정,
articlesList.add(article)로 게시글 하나씩 추가함
rs, pstmt, conn 자원 해제
return articlesList;
}
...
public int selectTotArticles()
{
// try-catch문
conn = dataFactory.getConnection();
String query = "select count(articleNO) from t_board"; // 전체 글 수를 조회함
쿼리문 실행해서 조회결과 ResultSet에 저장,
if (rs.next())
return (rs.getInt(1)); // 조회 결과 테이블의 첫번째 컬럼의 값을 가져와서 반환함
rs, pstmt, conn 자원 해제
return 0;
}
[ listArticles.jsp ] - 추가
게시글 목록을 출력하는 부분은 동일함,
목록 하단에 페이지 숫자 등을 출력하는 내용이 추가됨
전체 게시글 수가 100개 초과하는 경우/ 100개인 경우/ 100개 미만인 경우로 나누어서 페이지 번호를 표시하도록 함
100개 초과 -> 이전, 다음 section으로 이동할 수 있는 pre, next 가 페이지번호 양옆에 표시되게 함
100개 -> 정확히 10개의 페이지가 표시됨
100개 미만 -> 전체 글 수 /100 의 몫 + 1 까지의 페이지 번호가 표시됨 (전체글 수 81개 -> 9페이지까지 있음)
...
<c:set>으로 ${articlesMap.articlesList} 등 articlesMap 안의 요소들을 짧은 이름의 변수로 사용할 수 있게 바꿔줌
<style> 에서는 클래스 선택자를 이용해서 a태그 아래에 밑줄이 나타나지 않게, 페이지 번호가 현재 보여지는 페이지이면 페이지 번호를 빨간색으로 출력하도록 하는 등...
게시글 출력 부분은 동일함,
// 그 아래 페이지 출력하는 부분
<c:if test="${totArticles != null}" >
<c:choose>
<c:when test="${totArticles > 100}"> // 전체 글 수가 100개 초과 시
<c:forEach var="page" begin="1" end="10" step="1">
<c:if test="${ section > 1 && page == 1 }"> // section이 2 이상이면 페이지번호 왼쪽에 pre 가 나타나게 함
<a href="${contextPath}/board/listArticles.do?section=${section-1}&pageNum=${section-1)*10 + 1}"> pre </a>
</c:if>
<a href="${contextPath}/board/listArticles.do?section=${section}&pageNum=${page}"> ${(section-1)*10 + page} </a>
<c:if test="${page == 10}">
<a href="${contextPath}/board/listArticles.do?section=${section+1}&pageNum=${section*10+1}"> next </a>
</c:if>
</c:forEach>
</c:when>
<c:when test="${totArticles == 100}"> // 전체 글 수가 100개
<c:forEach var="page" begin="1" end="10" step="1">
<a href="${contextPath}/board/listArticles.do?section=${section}&pageNum=${page}" > ${page}</a>
</c:forEach>
</c:when>
<c:when test="${totArticles < 100}"> // 전체 글 수가 100개 미만
<c:forEach var="page" begin="1" end="${totArticles/10 +1}" step="1" >
<c:choose>
<c:when test="${page == pageNum}" > // 출력하는 페이지 번호가 현재 보고있는 페이지이면 빨간색으로 출력
<a class=""sel-page" href="${contextPath}/board/listArticles.do?section=${section}&pageNum=${page}">${page}</a>
</c:when>
<c:otherwise>
<a href="${contextPath}/board/listArticles.do?section=${section}&pageNum=${page}">${page}</a>
</c:otherwise>
</c:choose>
</c:forEach>
</c:when>
</c:choose>
</c:if>
'(책) 자바 웹을 다루는 기술' 카테고리의 다른 글
Chap 19 스프링 의존성 주입과 제어 역전 기능 (0) | 2023.09.14 |
---|---|
Chap 18 스프링 프레임워크 시작하기 (0) | 2023.09.13 |
Chap 17 모델2 방식으로 효율적으로 개발하기 -1 (0) | 2023.09.09 |
Chap 16 HTML5와 제이쿼리 (0) | 2023.09.07 |
Chap 15 JSP 페이지를 풍부하게 하는 오픈 소스 기능 (0) | 2023.09.01 |