회원 - 게시글간 연관관계 설정
회원 - 게시글은 일대다(1 : N) 연관관계로 설계하였으며 그에 따라 게시글 객체에서 게시글을 작성한 회원의 닉네임 데이터를 외래키로 하여 보유하도록 만들었다.
여기서 게시글 - 댓글 또한 일대다 연관관계 이므로 댓글 객체에서 게시글 번호 값을 외래키로 하여 소지하고 있도록 설계하였다.
답글의 경우 현재 작성된 댓글에 대해 답글을 작성할 수 있는 기능이고, 한 댓글에 여러개의 답글이 달릴 수 있으므로
댓글 - 답글은 일대다(1 : N) 관계로서 답글 데이터의 경우 자신이 작성된 댓글의 댓글 번호 데이터를 외래키를 보관하게끔 설계하였다.
3가지 종류의 게시판 생성
게시판의 경우 자유 게시판, 팁 게시판, 팀원 모집 게시판 등 총 3가지 의 게시판을 개설했다.
게시글들이 각각 어느 게시판에 작성되는지에 대한 분류는 자유 게시판에 1, 팀원 모집 게시판에 2, 팁 게시판에 3으로 분류번호를 두고 각 게시판에서 글을 작성하고 저장할 때 해당 분류 번호가 데이터베이스에 함께 저장되는 것으로 처리했다.
처음엔 게시판 종류 별로 테이블을 따로 만들어서 관리를 해야하지 않나 생각했지만 한 테이블 안에 굉장히 많은 숫자의 데이터가 들어갈 수 있다는 것을 알고(레코드가 몇 천만개도 들어갈 수 있다고....) 굳이 테이블을 여러개 만들 필요가 없겠다고 판단 했기에, 그냥 한 테이블 안에서 전체 게시판에 작성되는 게시글들을 관리해주기로 했다.
* 게시글 및 댓글 객체 클래스 다이어그램
게시글 작성을 위한 CKEditor 채용....그리고 프로젝트의 최대 난관 봉착
게시글 작성 기능을 구현하기 위해 외부에서 가져와 쓸 수 있는 CKEditor 를 활용해보기로 했다.
에디터를 다운받아서 프로젝트에 적용하고 정상적으로 잘 동작하는지 확인해보는데 까지는 그리 오랜 시간이 걸리지 않았다.
에디터가 잘 만들어진걸 확인하고 난 후 단순히 텍스트만 작성해서 저장 버튼을 눌렀을때 게시글 작성 내용이 데이터베이스에 저장된 후, 작성된 게시글의 제목 링크를 클릭했을때 내용을 출력 시켜주는 기능까지는 크게 어렵지 않게 해결할 수 있었다.(CKEditor 는 게시글 작성내용을 html 형태로 저장해두기 때문에 게시글 내용을 가져오는 작업도 그닥 어렵지 않았다.)
하지만 문제는 게시글에 이미지를 업로드 해주는 기능을 구현하면서부터 발생했다.
단순히 텍스트만 에디터에 작성하면 게시글 저장 부터 출력까지 모두 잘 동작하는 것을 확인한 뒤에 마지막으로 이미지 업로드를 처리하기 위해 에디터 상에 이미지 업로드를 시도해보았는데, 잠깐 이미지가 에디터에 정상적으로 출력되는것 같더니 이내 사라지면서 오류 메세지가 출력되는 것이다.
지금까지는 프로젝트를 하면서 오류가 생기더라도 보통 오류가 발생한 당일안으로 해결법을 찾아서 오류를 수정했는데 이번엔 어떻게든 이 오류를 해결하려고 여러가지 가설을 생각해보고 이것저것 만져보고 살펴봐도 영 오류가 고쳐질 기미가 보이질 않았다.
그렇게 계속 오류를 해결하지 못하고 시간을 4일쯤 허비했을때 같이 작업하는 형이 문제를 해결한것 같다고 하길래 살펴봤더니, 프로젝트 내부에 정적 파일을 저장하는 static 폴더에 이미지를 적재 시키고 그 이미지를 불러오는 과정이 진행되어야 하는데 미처 static 폴더 내부에 이미지가 저장되기도 전에 그 이미지를 호출해 오려고 하는것 아닌가 하는 생각이 들어서 이미지를 호출해오기전에 sleep() 메소드를 통해 5초정도 딜레이를 줘서 이미지가 정적 파일 폴더에 저장될때 까지 시간을 벌어준 다음, 그 이후에 해당 이미지를 호출해 올 수 있게끔 해줬더니 잘 해결이 되더라는 것이었다.
그 말을 듣고 혹시나 하는 마음에 그대로 해봤는데 형쪽에서는 문제가 해결되었는데 반해 내 쪽에서는 sleep() 메소드를 활용해서 딜레이를 줘봐도 해결이 되지 않았다.
sleep() 메소드를 쓰는 방법으로도 해결을 못하고 또다시 이것저것 만져보고 있던 중 뭔가 오류를 해결하기에 유의미한 로그를 발견하게 되었다.
처음 이미지 업로드 기능을 구현하면서 return 으로 돌려주려고 한 적도 없는 image.jsp 파일이 없다고 알려주는 로그가 생긴것이다.
그래서 image.jsp 파일을 만들고, 해당 파일의 내용을 읽어서 이미지 경로를 찾는것 같다는 생각이 들어 static 폴더에 저장되는 이미지 경로를 image.jsp 파일에 그대로 전달해주는 방식으로 까지 구현을 해보았더니 그제서야 오류가 해결되는 것을 확인할 수 있었다.
- BoardController.java
@RequestMapping(value = "/image.do", headers = "content-type=multipart/form-data", method = {RequestMethod.GET, RequestMethod.POST})
public HttpServletRequest imgUpload(@RequestParam("boardNumber") int boardNumber, HttpServletRequest request,
HttpServletResponse response, @RequestParam MultipartFile upload) throws ServletException, IOException, JSONException {
// 파일이름 중복성 제거
UUID uuid = UUID.randomUUID();
//한글 인코딩
request.setCharacterEncoding("UTF-8");
//파라미터로 전달되는 한글 인코딩
response.setContentType("charset=utf-8");
// 업로드 파일 이름
String filename = uuid.toString() + "_" + upload.getOriginalFilename();
System.out.println("업로드 파일명 : " + filename);
//파일을 바이트로 변환
byte[] bytes = upload.getBytes();
// 이미지를 업로드 할 폴더(주의 : 개발자 폴더 이므로 반드시 ~/images 을 새로고침 해야함)
// String path = "/WEB-INF/freeUploadImage";
String path = "WEB-INF/classes/static/images/freeUploadImage/";
String real_save_path = request.getServletContext().getRealPath("").toString();
System.out.println(real_save_path);
// String real_save_path = request.getServletContext().getRealPath(path)+"\\";
StringBuilder save_path = new StringBuilder(real_save_path);
System.out.println(save_path);
save_path.append(path);
System.out.println(save_path);
// 서버로 업로드
// write 메소드의 매개값으로 파일의 총 바이트를 매개값으로 준다.
// 지정된 바이트를 출력 스트림에 쓴다.(출력하기 위해서)
FileOutputStream out = new FileOutputStream(new File(save_path+filename));
out.write(bytes);
try {
// 이미지 업로드 시간을 벌기 위한 시간 딜레이
Thread.sleep(5000);
}catch(Exception e) {
}
request.setAttribute("url", "/pro.gg/resources/images/freeUploadImage/" + filename);
request.setAttribute("uploaded", true);
return request;
}
- image.jsp
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" isELIgnored="false"%>
<% request.setCharacterEncoding("UTF-8"); String src = (String)request.getAttribute("url"); %>
<%= src %>
- writepost.jsp (CKEditor 적용 코드)
<script>
var writedPosting;
$(function(){
ClassicEditor
.create( document.querySelector('#postContent'),{
extraPlugins:[MyCustomUploadAdapterPlugin],
language: 'ko',
})
.then(editor =>{
writedPosting = editor ;
})
.catch( error => {
console.error( error );
});
});
function MyCustomUploadAdapterPlugin(editor){
var boardNumber = "${boardNumber}";
editor.plugins.get('FileRepository').createUploadAdapter = (loader) =>{
return new UploadAdapter(loader, boardNumber);
}
}
// ......
// ......
</script>
- UploadAdapter.js (CKEditor 동작 수행 소스 코드)
class UploadAdapter {
constructor(loader, boardNumber) {
this.loader = loader;
this.boardNumber = boardNumber;
}
upload() {
return this.loader.file.then( file => new Promise(((resolve, reject) => {
this._initRequest();
this._initListeners( resolve, reject, file );
this._sendRequest( file );
})))
}
_initRequest() {
const xhr = this.xhr = new XMLHttpRequest();
xhr.open('POST', 'http://progg.cf/pro.gg/image.do?boardNumber='+this.boardNumber, true);
xhr.responseType = '';
}
_initListeners(resolve, reject, file) {
const xhr = this.xhr;
const loader = this.loader;
const genericErrorText = '파일을 업로드 할 수 없습니다.'
xhr.addEventListener('error', () => {reject(genericErrorText)})
xhr.addEventListener('abort', () => reject())
xhr.addEventListener('load', () => {
encodeURI(xhr.response);
const response = xhr.response;
var responseText = xhr.responseText;
console.log(responseText);
if(!response || response.error) {
return reject( response && response.error ? response.error.message : genericErrorText );
}
resolve({
default: responseText //업로드된 파일 주소
// responseText 에 image.jsp 파일의 내용이 들어가있다.
})
})
if(xhr.upload){
xhr.upload.addEventListener('progress', evt =>{
if(evt.lengthComputable){
loader.uploadTotal = evt.total;
loader.uploaded = evt.loaded;
}
})
}
}
_sendRequest(file) {
const data = new FormData()
data.append('upload',file)
this.xhr.send(data);
}
}
오류를 해결하고 난 이후 여러가지 세부적인 기능 구현
이미지 업로드 오류를 해결하고 난 이후 아직 구현하지 못한 세부 기능들을 구현해주는 데는 그리 오랜 시간이 걸리지 않았다.
게시글 작성날짜가 당일 자정을 넘어가면 그 이전 날짜로 변하게끔 만들어주는 것 부터, 게시글 조회수 기능, 게시글 추천, 비추천 기능과 같은 것들은 각 기능들을 동작시켰을때 데이터베이스에 저장되어 있는 정보들과 비교하여 해당 기능을 동작시키는 사용자가 게시글의 사용자인지 그렇지 않은지, 또한 이미 이 게시글에 추천을 눌렀었는지 누르지 않았었는지를 판별하는 로직을 구현하고, 로직을 통과할 경우 기능이 정상적으로 수행되게끔 만들었다.
댓글에도 에디터를 쓰는건....
처음엔 댓글에도 게시글 처럼 이미지를 업로드 해줄 수 있게 만들어주려고 게시글 처럼 에디터를 적용시키는 방안을 고민해 보았으나, 이미 게시글 작성 기능을 구현하면서 이미지 업로드 오류때문에 제대로 홍역을 치른 이후였기 때문에 이미지는 제외하고 그냥 <textarea> 태그에 텍스트 형태로 작성하고자 하는 답글을 적고 등록하기 버튼을 클릭하면 작성한 답글이 정상적으로 등록되게끔 구현했다.
사실 CKEditor 를 댓글과 답글에도 적용시키려면 충분히 할 수 있었겠지만 원래 4명이서 하기로 하고 계획했던 프로젝트가 인원이 2명으로 줄어들면서 안 그래도 한 사람 한 사람이 감당해야할 작업량이 많아진 상황인데 댓글에도 에디터를 추가해줬다간 괜히 프로젝트 규모만 커져서 피곤해지지 않을까 하는 생각도 들었다.
기본적인 댓글 구현 기능은 내가 작업하고 댓글에 대한 답글 같은 경우는 같이 하는 형에게 맡겨서 구현을 완료했다.
댓글에서는 작성자의 닉네임과 작성 시간 또는 작성 일시, 댓글 내용과 추천 버튼, 비추천 버튼을 구현하여 각각 게시글에서 구현했던 것처럼 동작하도록 구현했다.
답글의 경우 댓글 오른쪽 끝에 있는 답글 링크를 클릭하면 댓글 아래쪽에 답글을 작성할 수 있는 영역이 출력되고 텍스트를 작성한 후 등록하기 버튼을 통해 답글이 등록되도록 구현했다.
여기서 댓글, 답글 둘 다 공통적으로 로그인을 하지 않은 상태일 경우 작성할 수 없도록 하여 완성도를 높였다.
* 댓글 객체 클래스 다이어그램
'프로젝트 > 협업' 카테고리의 다른 글
Pro.gg - 프로젝트 회고록(9) : HTTPS 연결을 위한 서버 재할당...그리고 삽질 (0) | 2022.03.16 |
---|---|
Pro.gg - 프로젝트 회고록(8) : 클래스 간 의존성 주입 코드 수정 (0) | 2022.03.15 |
Pro.gg - 프로젝트 회고록(6) : 팀 구성 및 매칭 (0) | 2021.10.22 |
Pro.gg - 프로젝트 회고록(5) : 소환사 정보 - 2 (0) | 2021.10.19 |
Pro.gg - 프로젝트 회고록(4) : 소환사 정보 - 1 (0) | 2021.10.19 |