해당 프로젝트는 2023/01/25 ~ 2023/03/12 내에 진행되는
아카데미 내 수강생들끼리 팀을 나누어 진행한 모의 프로젝트입니다.
팀원은 5명이었으며, 프로젝트 리더를 맡았습니다.
이전 글 목록
1) 주어진 RFP를 바탕으로 주제 선정 - Spring Project(OTT 서비스)
2) ERD 설계 - Spring Project(OTT 서비스)
3) 회원 가입 기능 구현 - Spring Project (OTT 서비스)
4) 로그인, 로그아웃 기능 구현 - Spring Project (OTT 서비스)
5) 상세 페이지 및 회원 정보 수정 - Spring Project (OTT 서비스)
프로젝트 진행이 겹쳐서 블로그 업로드가 많이 밀려있다..
우선 완성된 게시판을 보며 하나하나 뜯어보는 형식, 리뷰 형식으로 작성하며 복습해야 할 것 같다.
우선 게시판 메인이다.
게시판 보드와 검색 창은 https://colorlib.com/ 에서 소스를 가져왔으며, Navbar는 디자인을 맡은 팀원의 디자인이다.
CRUD 및 페이징, 검색 시 페이징 유지기능, 비밀 게시글 등등 위에 보이는 형태처럼 기능 구현을 완료하였다.
또한 게시물 체크박스는 관리자로 접속하였을 경우, 다중 삭제를 위해 구현 해 두었다. 체크 된 게시물을 삭제할 수 있다.
Create
QnaController
질문 생성 버튼을 눌렸을 때, 해당 페이지로 이동하는 메서드와
질문을 작성한 뒤 완료버튼을 눌렸을 때, Model을 타고 DB까지 조건을 충족할 경우 등록되어 나올 것이다.
//질문생성 페이지
@RequestMapping("create")
public String qCreate() {
return "board/qna/qna_create";
}
//질문생성하기
@RequestMapping(value = "create", method = RequestMethod.POST)
public String qCreate(QnaDto qnaDto) {
qnaService.qCreate(qnaDto);
return "redirect:/qna/list";
}
QnaService / Impl / Dao / qna_SQL.xml
딱히 백단에서의 조건은 Create 시 필요하지 않을 것 같아서 처리하지 않았다.
빈 값을 입력했을 때의 방지는 JavaScript를 활용하여 alert처리를 했다.
Mybatis의 jdbcType 설정 이유는 혹시 패스워드를 입력하지 않았을 경우 NULL로 처리하기 위해서이다.
//QnaService
void qCreate(QnaDto qnaDto);
//QnaServiceImpl
@Override
public void qCreate(QnaDto qnaDto) {
qnaDao.qCreate(qnaDto);
}
//QnaDao
public void qCreate(QnaDto qnaDto) {
ss.insert("qna.q_create", qnaDto);
}
//qna_SQL.xml
<insert id="q_create">
insert into QNA(ID, Q_SUBJECT, Q_CONTENT, PASSWORD)
values (#{id}, #{q_subject}, #{q_content}, #{password, jdbcType=VARCHAR})
</insert>
JavaScript
사이트 정책 상 비로그인 유저도 QNA를 통한 문의가 가능하도록 설계를 수정하였다. 그에 따른 테이블 수정도 진행되었다. 프로젝트 진행을 하면서, 미숙한 점이 많아 기존 ERD 설계에서 수정을 어느정도 진행하였다. 나중에 포스팅 하도록 하겠다.
버튼 클릭 시 공란이 없을 때만 submit되도록 코드를 작성하였다.
const title = document.querySelector('input[id=title]');
const content = document.querySelector('textarea[id=content]');
const idInput = document.querySelector('input[id=id]');
/* 작성자가 회원일 때, 자동으로 세션에서 값을 받아 작성자 란 입력 못하게 */
window.onload = function(){
if(idInput.value != ''){
idInput.readOnly = true;
idInput.style.backgroundColor = "#F5F5F5";
}
}
// 등록 버튼 클릭 시
function qCreate(){
if(title.value == "") {
alert("제목을 입력하세요");
document.form1.q_subject.focus();
return;
}
if(idInput.value == ""){
alert("작성자를 입력하세요");
document.form1.id.focus();
return;
}
if(content.value == ""){
alert("내용을 입력하세요");
document.form1.q_content.focus();
return;
}
document.form1.action="create"
document.form1.submit();
}
Read
상세목록만 살펴보고, 페이징 처리는 따로 포스팅을 하도록 하겠다.
QnaController
전체 리스트에서 해당 tr을 클릭 시, 상세 페이지로 이동하게 설계하였다.
HttpServletRequest의 getHeader("referer")를 이용하여, URL로 접근 시 접근을 차단하였다.
해당 인터페이스는 현재 관리자 페이지 전체 접근을 차단하거나, 원하는 페이지의 URL접근 차단 용도로 잘 사용하고 있다.
PathVariable 애너테이션을 사용하여 question_id를 URL값으로 사용하였다.
//QNA 상세
//상세페이지 접근 시 비밀글이면 접근 불가
@RequestMapping("list/{question_id}")
public ModelAndView qnaDetail(ModelAndView mv, @PathVariable int question_id, HttpServletRequest request) {
//이전 주소 경로
String result = request.getHeader("REFERER");
if(result == null) {
mv.addObject("error", "잘못된 접근입니다");
mv.setViewName("redirect:/qna/list");
}
else {
mv.addObject("data", qnaService.detail(question_id));
mv.setViewName("board/qna/qna_detail");
}
return mv;
}
HttpServletRequest Interface
JSP기본 내장 객체. 클라이언트로부터 서버로 요청이 들어오면 생성되며 요청 정보에 있는 매핑값을 서블릿에게 전달하며, 해당 내용들을 GET, POST 형식으로 클라이언트에게 전달한다.
getHeader로 header의 정보를 뽑아낼 수 있으며, "referer"을 사용하여 이전 페이지의 URL을 가져올 수 있다.
QnaService / Impl / qna_SQL.xml
PK값인 question_id를 이용하여 상세 페이지를 조회하였다.
//QnaService
QnaDto detail(int question_id);
//QnaServiceImpl
@Override
public QnaDto detail(int question_id) {
return qnaDao.detail(question_id);
}
//qna_SQL.xml
public QnaDto detail(int question_id) {
return ss.selectOne("qna.detail", question_id);
}
qna_detail.jsp
view에서 여러 조건들을 사용하여 게시판 버튼의 활성화 또는 비활성화를 진행하였다
관리자의 답변 등록 및 변경, 답변이 달렸을 시 일반 사용자가 답변을 볼 수 있는 버튼도 상세 페이지에 구현하였다.
관리자의 답변은 모달 창 형태로 해당 페이지에 뷰 되게 구현하였다.
<div class="page-wrapper p-t-100 p-b-50">
<div class="wrapper wrapper--w900">
<div class="card card-6">
<div class="card-heading">
<h2 class="title">내용</h2>
</div>
<div class="card-body">
<div class="form-row">
<div class="name">제목</div>
<div class="value">
<div class="input-group">
<input id="title" class="input--style-6" type="text" name="q_subject" value="${data.q_subject}" readonly>
</div>
</div>
</div>
<div class="form-row">
<div class="name">작성자</div>
<div class="value">
<div class="input-group">
<input id="id" class="input--style-6" type="text" name="id" value="${data.id}" readonly>
</div>
</div>
</div>
<div class="form-row">
<div class="name">내용</div>
<div class="value">
<div class="input-group">
<textarea class="textarea--style-6" id="content" name="q_content">${data.q_content}</textarea>
</div>
</div>
<div class="centerBtn">
<form id="centerForm" name="form1" onsubmit="return false">
<input type="hidden" name="question_id" value="${question_id}">
<c:if test="${sessionScope.user_id == data.id}">
<button class="btn btn--radius-2 btn--blue-2" type="button" onclick="qModify()">수정하기</button>
<button class="btn btn--radius-2 btn--blue-2" type="button" onclick="qnaDelete()">삭제하기</button>
</c:if>
</form>
<button class="btn btn--radius-2 btn--blue-2" type="button" onclick="location.href='/qna/list'">목록으로</button>
<c:if test="${sessionScope.user_id == 'admin' && sessionScope.nickname == 'admin'}">
<button id="adminCreate1" class="btn btn--radius-2 btn--blue-2" type="button" onclick="modal(this)">답변 등록</button>
</c:if>
<c:if test="${data.answer != null}">
<button id="adminCreate2" class="btn btn--radius-2 btn--blue-2" type="button" onclick="modal(this)">답변 보기</button>
</c:if>
</div>
</div>
</div>
</div>
</div>
</div>
qna_detail.js
View에서 설계한 것을 바탕으로 스크립트 코드를 썻다.
const input = document.querySelectorAll('input');
const answerValue = document.querySelector('#answerValue');
const adminCreate1 = document.querySelector('#adminCreate1');
/* 창 로드 시 */
window.onload = function(){
/* 본인이면 내용 수정 가능, 아니면 불가 */
if($('#sessionID').val() != $('#userID').val()){
$('#content').attr('readonly', true);
$('#content').css("background-color", "#F5F5F5");
};
/* readonly 색상 수정 */
input.forEach(function(e){
if(e.readOnly){
e.style.backgroundColor = "#F5F5F5";
}
});
/* 답변 완료 시 수정 불가 및 관리자 버튼 수정 */
if(answerValue.value != ''){
$('#content').attr('readonly', true);
$('#content').css("background-color", "#F5F5F5");
adminCreate1.textContent = "답변 수정";
}
}
function qnaDelete(){
if(confirm("삭제하시겠습니까?")){
document.form1.action="qnaDelete"
document.form1.submit();
}
}
1. 작성자 본인일 경우 수정할 수 있다. 하지만 관리자가 답변을 달았을 경우 수정이 불가능하다.
2. 관리자의 경우 답변을 등록할 수 있으며, 답변 수정 또한 등록 후 가능하다.
Update
QnaController
해당 페이지에서 바로 수정이 완료되게 하기 위해 ResponseBody 애너테이션을 사용했다.
잘 이해하고 사용한 건지 모르겠다.
글 수정 시 ajax를 이용했는데, 리턴값이 ?? 로 콘솔에 찍혀서 charset을 설정해주었다.
//수정하기
@RequestMapping(value="modify", method=RequestMethod.POST, produces="application/text; charset=UTF-8;")
@ResponseBody
public String qnaModify(ModelAndView mv, QnaDto qnaDto) {
System.out.println(qnaDto);
qnaService.modify(qnaDto);
return "수정완료";
}
@Responsebody
자바 객체를 HttpResponse의 본문 responseBody의 내용으로 매핑한다.
QnaService / Impl / qna_SQL.xml
수정을 위해 View에서 받아온 질문 내용들을 Dto에 잘 담아서 싣고 가자
//QnaService
void modify(QnaDto qnaDto);
//QnaServiceImpl
@Override
public void modify(QnaDto qnaDto) {
qnaDao.modify(qnaDto);
}
//qna_SQL.xml
public void modify(QnaDto qnaDto) {
ss.update("qna.modify", qnaDto);
}
JavaScript
수정 버튼 클릭 시 조건들을 만족한다면 ajax를 통하여 질문의 PK값과 내용이 잘 전달된다.
const content = document.querySelector('textarea[id=content]');
const question_id2 = document.querySelector('input[name=question_id]').value;
// 등록 버튼 클릭 시
function qModify(){
if(content.value == ""){
alert("내용을 입력하세요");
content.focus();
return;
}
else{
const modifying = content.value;
$.ajax({
url : 'modify',
type : 'post',
data : {'question_id' : question_id2, 'q_content' : modifying},
dataType : 'text',
success : function(data){
if(data == "수정완료"){
alert("수정 완료");
location.href = "/qna/list";
}
}
});
}
}
Delete
CRUD의 마지막 D다.
QnaController
1. 일반 유저의 경우, 상세 페이지에서 PK값으로 삭제가 가능하다.
2. 관리자의 경우, List 페이지에서 SELECTBOX를 활용한 삭제가 가능하다.
SELECTBOX를 통한 삭제는, 체크 된 SELECTBOX의 question_id들을 배열형태로 받아와서 사용했다.
스크립트에서 Array() 객체를 사용하여 전송하였기 때문에, 자바 객체로의 변환을 위해 @RequestBody를 사용했다.
//삭제하기 - 02.07
@RequestMapping("qnaDelete")
public String qnaDelete(int question_id) {
qnaService.delete(question_id);
return "redirect:/qna/list";
}
//selectbox를 통한 다중 삭제 - 02.17
@RequestMapping(value="qnaDeletes", method=RequestMethod.POST)
public String qnaDeletes(@RequestBody List<Integer> delArr) {
qnaService.deletes(delArr);
return "redirect:/qna/list";
}
QnaService / Impl / qna_SQL.xml
//QnaService
void delete(int question_id);
void deletes(List<Integer> delArr);
//QnaServiceImpl
@Override
public void delete(int question_id) {
qnaDao.delete(question_id);
}
@Override
public void deletes(List<Integer> delArr) {
qnaDao.deletes(delArr);
}
//qna_SQL.xml
public void delete(int question_id) {
ss.delete("qna.delete", question_id);
}
public void deletes(List<Integer> delArr) {
ss.delete("qna.deletes", delArr);
}
JavaScript
기존 유저의 단일 삭제는 Update의 Javascript 코드에 있다.
삭제 할 PK값을 Array로 담아 Controller에 전송했다.
/* 질문 삭제(체크박스 기능) - 02.17 */
function qnaDelete(){
const delArr = new Array();
checkbox.forEach(function(e){
//체크 된 녀석의 질문번호 받아오기
if(e.checked){
delArr.push(e.parentElement.nextElementSibling.value);
}
})
//삭제 선택 안하고 삭제 버튼만 클릭 시 막기
if(delArr.length == 0){
alert("삭제할 게시물을 선택해 주세요");
return;
}
//삭제 게시물 PK 전송
if(confirm("삭제하시겠습니까?")){
$.ajax({
url : 'qnaDeletes',
data : JSON.stringify(delArr),
type : 'post',
contentType: 'application/json; charset=utf-8',
success:function(data){
location.href="/qna/list";
}
});
}
}
다음 포스팅엔 페이징 처리를 리뷰하도록 하겠습니다.
게시판 비밀번호 생성은 아래와 같이 처리했습니다.
1. 테이블 조회 시 비밀번호 여부 확인
2. 있으면 VIEW에서 비밀번호 있으면 잠금표시 및 input hidden으로 해당 비밀번호 보유
3. 비밀번호 입력 시 해당 input value와 같으면 페이지 넘김, 아니면 리턴
4. 혹시 URL로 접근할 수 있으니 Controller에서 접근 제한.
부족한 부분이 아직 너무 많은 것 같다.
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!