※ 본 글은 교재 [코드로 배우는 스프링 웹 프로젝트 - 구멍가게 코딩단]을 바탕으로 작성되었습니다.
REST 방식을 가장 많이 사용하는 형태는 브라우저나 모바일 App 등에서 Ajax를 이용해서 호출하는 것입니다.
Ajax( Asynchronous JavaScript and XML)란,
비동기적인 웹 애플리케이션의 제작을 위해 다음과 같은 조합을 이용하는 웹 개발 기법입니다.
*비동기적이란 어떤 작업이 순차적으로 실행되지 않고, 다른 작업과 독립적으로 병행적으로 실행될 수 있는 것을 말합니다.
- 표현 정보를 위한 HTML과 CSS
- 동적인 화면 출력 및 표시 정보와의 상호작용을 위한 DOM, 자바스크립트
- 웹 서버와 비동기적으로 데이터를 교환하고 조작하기 위한 XML, XSLT, XMLHttpRequest (Ajax 애플리케이션은 XML/XSLT 대신 미리 정의된 HTML이나 일반 텍스트, JSON, JSON-RPC를 이용 가능).
기존의 웹 애플리케이션은 페이지를 다시 로드할 때마다 서버로부터 새로운 HTML 페이지를 받아와야 했습니다(새로고침).
반면에 AJAX를 사용하면 브라우저는 페이지 전체를 새로고침하지 않고도 서버로부터 데이터를 비동기적으로 요청하고,
서버는 필요한 데이터만 응답으로 보내줍니다.
요약하자면, 필요한 데이터만을 독립적으로 요청하여 응답 받을 수 있게 하는 동작이라고 할 수 있겠습니다.
이를 바탕으로 댓글을 처리하는 실습을 해보도록 하겠습니다.
댓글 처리를 위한 영속 영역
0. 테이블 설계
기존에 사용하던 프로젝트를 사용할 것인데, 댓글을 추가하기 위해서는 댓글의 테이블을 설계해야 합니다.
테이블은 다음의 칼럼들을 사용하여 설계 하겠습니다.
- rno(댓글 번호)를 기본키 처리해 줍니다.
- bno(게시판 번호)를 외래키 처리해 줍니다(어떤 게시물의 댓글인지 명시).
- reply(댓글)
- replyer(댓 작성자)
- replyDate(댓 작성일)
- updateDate(댓 수정일)
테이블을 생성했으면 sequence도 만들어줍니다.
1. VO 클래스 추가
앞서 만든 테이블의 칼럼을 참고하여 필드를 선언해 줍니다.
2. Mapper 클래스(mapper 인터페이스)와 mapper xml 처리
public interface ReplyMapper { }
<mapper namespace="org.zerock.mapper.ReplyMapper"></mapper>
실제 SQL문은 mapper xml에서 처리합니다.
이 두 개를 작성했다면 mapper 테스트를 돌려보아야 합니다.
src/test/java에 mapper에 대한 테스트 패키지와 클래스를 생성합니다.
@RunWith(SpringRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j2
public class ReplyMapperTests {
@Setter(onMethod_ = @Autowired)
private ReplyMapper mapper;
@Test
public void testMapper() {
log.info(mapper);
}
}
3. CRUD 작업
1) 등록(create)
댓글 번호로 처리 가능합니다.
- 인터페이스 작성
- xml 작성
- 테스트
2) 조회(read)
- 인터페이스 작성
- xml 작성
- 테스트
3) 삭제(delete)
댓글 번호로 처리 가능합니다.
- 인터페이스 작성
- xml 작성
- 테스트
4) 수정(update)
댓글 번호로 처리 가능합니다.
- 인터페이스 작성
- xml 작성
- 테스트
4. @Param 어노테이션과 댓글 목록
댓글 목록과 페이징 처리는 기존의 게시물 페이징 처리에서 추가적으로 특정한 게시물의 댓글들만을 대상으로 하기 때문에 추가로 게시물의 번호가 필요합니다.
MyBatis는 두 개 이상의 데이터를 파라미터로 전달하려면
별도의 객체로 구성하거나,
Map을 이용하거나,
@Param을 이용해야 합니다.
@Param 속성값은 MyBatis에서 SQL을 이용할 때 '#{ }'의 이름으로 사용합니다.
- 인터페이스 작성
public List<ReplyVO> getListWithPaging(
@Param("cri") Criteria cri,
@Param("bno") Long bno);
xml로 처리할 때에는 지정된 'cri'와 'bno'를 모두 사용 가능합니다.이제 특정 게시물의 댓글을 가져오는 것을 작성합니다.
- xml 작성
<select id="getListWithPaging" resultType="org.zerock.domain.ReplyVO">
<!-- resultType 조회 결과 값을 저장하기 위한 데이터 타입 -->
select rno, bno, reply, replyer, replyDate, updatedate
from tbl_reply
where bno = #{bno}
order by rno asc
</select>
#{bno}가 @Param("bno")와 매칭되어 사용하고 있습니다.
- 테스트
댓글에 대한 처리도 화면상에서 페이징 처리가 필요할 수 있기 때문에 Criteria를 이용하여 처리할 것입니다.
@Test
public void testList() {
Criteria cri = new Criteria();
List<ReplyVO> replies = mapper.getListWithPaging(cri, bnoArr[0]);
replies.forEach(reply -> log.info(reply));
}
서비스 영역과 Controller 처리
1. Service
mapper 계층을 다뤘으니, 그 다음 계층인 Service 계층을 다뤄보겠습니다.
Service 인터페이스와 ServiceImpl 클래스가 필요합니다.
ServiceImpl는 Mapper 인터페이스에 의존적인 관계이므로 @Setter를 이용하여 자동주입 처리합니다.
2. Controller
@RestController를 사용해 줍니다.
REST 방식으로 동작하는 URL을 설계할 때, PK를 기준으로 작성해야 합니다. 그것만으로 조회, 수정, 삭제가 모두 가능합니다.
다만 댓글 목록은 PK를 사용할 수 없기 때문에 파라미터로 필요한 게시물의 번호와 페이지 번호 정보들을 URL에서 표현하는 방식을 사용합니다.
Controller는 Service 타입의 ReplyServiceImpl 객체를 주입 받도록 합니다(@Setter).
@RequestMapping("/replies/")
@RestController
@Log4j2
public class ReplyController {
@Setter(onMethod_ = @Autowired)
private ReplyService service;
@PostMapping(value = "/new",
consumes = "application/json",
produces = { MediaType.TEXT_PLAIN_VALUE})
public ResponseEntity<String> create(@RequestBody ReplyVO vo){
log.info("ReplyVO");
int insertCount = service.register(vo);
log.info("Reply INSERT COUNT: " + insertCount);
return insertCount == 1
? new ResponseEntity<>("success", HttpStatus.OK)
: new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
// 삼항 연산자 처리
}
댓글 등록의 경우 브라우저에는 JSON 타입으로 된 댓글 데이터를 전송하고
서버에서는 댓글의 처리 결과가 정상적으로 되었는지 문자열로 결과를 알려주도록 합니다.
create( ) 메서드는 @PostMapping으로 POST 방식으로만 동작하도록 설계 합니다.
consumes와 produces를 이용하여 JSON 방식의 데이터만 처리하도록 하고, 문자열을 반환하도록 설계합니다.
*consumes: 들어오는 데이터의 타입을 정함
*produces: 반환하는 데이터의 타입을 정함
*mediatype: 받고 싶은 데이터의 타입을 강제함
파라미터는 @RequestBody를 적용해서 JSON 데이터를 ReplyVO 타입으로 변환하도록 지정합니다.
*@RequestBody: 응답하고 요청할 때 본문에 데이터를 담아 보내야 하는데 이 본문을 body라고 하고 RequestBody는 요청 분문임
내부적으로 ServiceImpl을 호출하여 register( ) 메서드를 호출하고 댓글이 추가된 숫자를 확인해서
'200 OK' 또는 '500 Internal Server Error'를 반환합니다.
크롬 확장 프로그램으로 테스트 진행이 가능합니다.
테스트 시에는 POST 방식으로 전송하고, 'Content-Type'은 'application/json'으로 지정해야 합니다.
3. 특정 게시물의 댓글 목록 확인
@GetMapping(value="/pages/{bno}/{page}",
produces = {
MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_UTF8_VALUE })
public ResponseEntity<List<ReplyVO>> getList(
@PathVariable("page") int page,
@PathVariable("bno") Long bno) {
log.info("getList........");
Criteria cri = new Criteria(page, 10); //Criteria를 이용하여 파라미터 수집, {page} 값은 Criteria에서 직접 처리해야 함
log.info(cri);
return new ResponseEntity<>(service.getList(cri, bno), HttpStatus.OK);
}
Controller의 getList( ) 메서드는 Criteria를 이용해서 파라미터를 수집하는데,
/{bno}/{page}의 page는 Criteria를 생성해서 직접 처리하도록 합니다.
게시물의 번호는 @PathVariable로 파라미터를 처리하고 브라우저에서 테스트 합니다.
*@PathVariable(경로 변수) : 리소스 경로에 식별자를 넣어서 동적으로 URL에 정보를 담을 수 있음
4. 댓글 삭제/조회
JSON이나 문자열을 반환하도록 설계하겠습니다.
@GetMapping(value = "/{rno}",
produces = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_UTF8_VALUE })
public ResponseEntity<ReplyVO> get(@PathVariable("rno") Long rno){
log.info("get: " + rno);
return new ResponseEntity<>(service.get(rno), HttpStatus.OK);
}
삭제를 하려면 특정 댓글을 가져오는 것이 도리겠지요.
@GetMapping으로 HTTP GET 요청을 처리하며, /{rno} 경로에 대한 요청을 처리합니다.
{rno}은 경로 변수(Path Variable)로 사용됩니다.
반환하는 값의 타입을 xml과 json으로 강제하고, 댓글 번호(rno)를 @PathVariable로 처리합니다.
@PathVariable은 {rno} 경로 변수를 메서드의 파라미터로 바인딩합니다.
*바인딩: 웹 애플리케이션에서 클라이언트로부터 받은 데이터를 서버 사이드의 코드에서 사용할 수 있도록 연결(bind)하는 것
이것은 요청 URL에서 경로 변수를 추출하여 메서드 파라미터로 전달합니다. 여기서 rno은 Long 타입으로 변환됩니다.
get( ) 메서드는 ResponseEntity 객체를 반환합니다.
*ResponseEntity: HTTP 응답 상태 코드, 헤더 및 바디를 포함하는 객체
ReplyVO 객체는 HTTP 응답의 바디에 포함될 데이터를 나타냅니다.
@DeleteMapping(value="/{rno}", produces = {MediaType.TEXT_PLAIN_VALUE})
public ResponseEntity<String> remove(@PathVariable("rno") Long rno){
log.info("remove~~~~~~~~~~~~~~~~~~: " + rno);
return service.remove(rno) == 1
? new ResponseEntity<>("success", HttpStatus.OK)
: new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
produces를 이용하여 반환하는 값의 타입을 정해줍니다.
그리고 경로 변수인 @PathVariable로 rno를 파라미터 처리해 줍니다.
service 계층에 담긴 rno 파라미터 값을 가진 remove 메서드의 값이 1이면 '200 OK'를,
아니면 500 error를 문자열로 내도록 합니다.
지금쯤 되니 각 메서드의 구성이 비슷한 것을 볼 수 있습니다.
(설명도 점점 줄어들 예정입니다 ㅎ)
5. 댓글 수정
@RequestMapping(method = { RequestMethod.PUT,RequestMethod.PATCH },
value = "/{rno}", consumes = "application/json", produces = { MediaType.TEXT_PLAIN_VALUE })
public ResponseEntity<String> modify(@RequestBody ReplyVO vo, @PathVariable("rno") Long rno) {
vo.setRno(rno);
log.info("rno: " + rno);
log.info("modify: " + vo);
return service.modify(vo) == 1 ? new ResponseEntity<>("success", HttpStatus.OK)
: new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
modify( ) 메서드는 'PUT' 방식이나 'PATCH' 방식을 이용하도록 처리합니다.
그리고 실제 수정되는 데이터는 JSON 포맷이기 때문에 @RequestBody를 이용해 처리합니다.
@RequestBody로 처리되는 데이터는 일반 파라미터나 @PathVariable 파라미터로 처리할 수 없으므로
직접 처리해 주어야 합니다.
JavaScript 준비
1. JavaScript 모듈화
Ajax를 이용하는 경우, jQuery의 함수를 이용해 쉽게 처리할 수 있기 때문에 JS를 많이 사용합니다.
여러 종류의 처리를 쓰다보면 마구 섞여서 유지보수를 하기 힘든 경우가 많기 때문에 이를 대비해 JS를 하나의 모듈처럼 구성하는 방식을 이용하는 것이 좋습니다.
가장 많이 사용하는 패턴이 모듈 패턴인데, 관련있는 함수들을 하나의 모듈처럼 묶음으로 구성하는 것입니다.
webapp 안에 있는 resources 폴더에 js 파일(게시물 조회 페이지용)을 하나 작성해 주겠습니다.
아무 기능 없이 간단히 동작하는 코드만 넣어줄 것입니다.
<script type="text/javascript" src="/resources/js/reply.js"></script>
get.jsp에 js 파일을 추가해 줍니다.
js 파일을 테스트하기 위해 브라우저에 '/board/get?bno=xxx' 번호를 호출해서 잘 실행되는지 확인합니다.
F12를 눌러 개발자 도구를 사용하여 아무 문제 없이 로딩되는지, 콘솔에서는 console.log 내용이 실행되는지 확인해 줍니다.
확인 후에는 모듈을 구성해야 합니다.
모듈 패턴은 Java의 클래스와 같이 JS를 이용해 메서드를 가지는 객체를 구성합니다.
JS의 즉시 실행함수와 '{ }'를 이용해서 객체를 구성합니다.
js 파일을 수정해 줍니다.
console.log("Reply Module@@@");
var replyService = (function(){
return {name:"AAAA"};
})();
JS의 즉시 실행함수는 ( ) 안에 함수를 선언하고 바깥쪽에서 실행합니다. 즉, 함수의 실행 결과가 바깥쪽에 선언된 변수에 할당됩니다.
예제에서는 replyService라는 변수에 name이라는 속성에 'AAAA'라는 속성값을 가진 객체가 할당됩니다.
*즉시 실행함수(Immediately Invoked Rinction Expression | IIFE): function() { ... }를 ( )로 감싸면 즉시 실행됨
replyService가 잘 실행되는지는 get.jsp에서 확인하겠습니다.
<script type="text/javascript">
$(document).ready(function() {
console.log(replyService);
});
</script>
문서가 준비되었을 때(ready) 실행됩니다.
$(document).ready(function() { ... })는 문서 객체인 (document)가 준비(ready)되었을 때 실행할 함수를 정의합니다.
이 함수는 문서 객체의 모든 요소가 로드되고, DOM이 완전히 준비되었을 때 실행됩니다.
*DOM(Document Object Model(문서 객체 모델)): HTML, XML 및 XHTML 문서의 프로그래밍적인 인터페이스. 즉, 웹 페이지의 구조화된 표현 제공하고 프로그래밍 언어가 해당 문서 구조와 스타일 및 내용을 변경할 수 있도록 허용함. DOM을 이용하면 사용자 인터페이스를 동적으로 변경하고 이벤트를 처리할 수 있으며 웹 애플리케이션의 상태를 관리할 수 있음.
console.log(replyService)는 콘솔에 'replyService'라는 변수의 값을 출력합니다.
2. js 등록 처리
모듈 패턴을 다시 설명하자면, 즉시 실행하는 함수 내부에서 필요한 메서드를 구성하여 객체를 구성하는 방식을 말합니다.
console.log("Reply Module...><~!!!");
var replyService = (function(){
function add(reply, callback){
console.log("add reply............");
}
return {add : add};
})();
브라우저의 개발자 도구에서는 replyService 객체의 내부에 add 메서드가 존재하는 형태로 보이게 됩니다.
외부에서는 replyService.add(객체, 콜백)를 전달하는 형태로 호출할 수 있는데 Ajax 호출은 감춰져 있습니다.
모듈의 내부 구현이 외부로 노출되지 않으므로 보안상 좋습니다.
전체적으로 보면, IIFE를 사용하여 add 속성을 가진 add가 replyService 변수에 할당됩니다.
IIFE 패턴을 활용하여 replyService 모듈을 정의하고 외부에 노출될 변수인 replyService에 IIFE를 할당했다고도 할 수 있습니다.
function add(reply, callback){ ... }은 내부 함수인 add를 정의하는 메서드입니다.
reply와 callback을 파라미터로 받고 "add reply..."를 콘솔에 출력합니다.
결과적으로 외부에서 replyService.add()와 같이 해당 메서드를 호출 가능합니다.
js 파일 안의 add 함수는 Ajax를 이용하여 POST 방식으로 호출하는 코드를 작성합니다.
console.log("Reply Module...><~!!!");
var replyService = (function(){
function add(reply, callback, error){
console.log("add reply............");
$.ajax({
type : 'post',
url : '/replies/new',
data : JSON.stringify(reply),
contentType : "application/json; charset=utf-8",
success : function(result, status, xhr){
if(callback){
callback(result);
}
},
error : function(xhr, status, er){
if(error) {
error(er);
}
}
})
}
return {
add : add
};
})();
add( )에서 데이터 전송 타입은 'application/json; charset=utf-8' 방식입니다. 그리고 파라미터로 callback과 error를 함수로 받을 것입니다.
reply는 추가할 댓글 데이터입니다.
callback은 Ajax 요청이 성공했을 때 호출될 콜백 함수이고,
error는 요청이 실패했을 때 호출될 콜백 함수입니다.
add 함수 내에서는 jQuery의 $.ajax() 메서드를 사용하여 서버에 POST 요청을 보냅니다.
요청의 URL은 '/replies/new'이며, 데이터는 reply 객체를 JSON 문자열로 변환하여 전송합니다.
성공 또는 실패 시에는 각각 success와 error 콜백 함수가 호출됩니다.
result는 Ajax 요청이 성공한 경우, 서버에서 반환한 데이터를 포함합니다.
이는 Ajax 요청에 대한 응답으로 서버에서 전송된 데이터입니다.
status는 Ajax 요청의 상태를 나타냅니다
주로 HTTP 상태 코드와 관련이 있습니다. 예를 들어, 성공적인 요청의 경우 "success"라는 문자열이 포함됩니다.
xhr은 XMLHttpRequest 객체의 인스턴스입니다.
이 객체는 Ajax 요청을 수행하고 응답을 받는 데 사용됩니다.
jQuery Ajax 함수 내부에서 자동으로 생성되며, 필요한 경우에 사용할 수 있습니다.
XMLHttpRequest 객체에는 Ajax 요청 및 응답과 관련된 다양한 정보와 메서드가 포함되어 있습니다.
이제 get.jsp에서 replyService.add( )를 호출해 보겠습니다.
<script type="text/javascript" src="/resources/js/reply.js"></script>
<script>
console.log("==========");
console.log("JS TEST");
var bnoValue = '<c:out value="${board.bno}"/>';
//for replyService add test
replyService.add(
{reply:"JS Test", replyer:"tester", bno:bnoValue}
,
function(result){
alert("RESULT: " + result);
}
);
</script>
여기서 Ajax 호출은 replyService 객체에 감춰져 있기 때문에 필요한 파라미터들만 전달하는 형태가 됩니다.
add( )에 들어가야 하는 파라미터는 JS의 객체 타입으로 만들어 전송해 줍니다.
그리고 전송 결과를 처리하는 함수를 파라미터로 같이 전달합니다.
var bnoValue = '<c:out value="${board.bno}"/>';
${board.bno} 값이 포함된 HTML 코드를 JavaScript 변수 bnoValue에 할당합니다.
이 코드는 JSP 또는 서버 템플릿 엔진에서 생성된 HTML 페이지에서 JavaScript 변수를 설정하는 방식을 보여줍니다.
replyService.add(...);
replyService 모듈의 add 함수를 호출합니다.
이 함수는 댓글을 추가하는 Ajax 요청을 서버에 보냅니다.
함수의 첫 번째 매개변수는 추가할 댓글의 데이터 객체이며, 두 번째 매개변수는 성공 시 호출될 콜백 함수입니다.
{reply: "JS Test", replyer: "tester", bno: bnoValue}
- reply: 댓글 내용
- replyer: 댓글 작성자
- bno: 게시물 번호
function(result) { alert("RESULT: " + result); }
Ajax 요청이 성공하면 호출되는 콜백 함수입니다.
result 매개변수는 서버에서 반환된 데이터를 나타냅니다.
이 코드는 Ajax 요청이 성공했을 때 결과를 알림창으로 표시합니다.
이 때 브라우저에서는 JSON 형태로 데이터가 전송되고 있는 것을 확인할 수 있어야 하고
전송되는 데이터 또한 JSON 형태로 전송되는지 확인해야 합니다.
3. 댓글 목록 처리
jQuery의 getJSON( )을 사용해 보겠습니다.
function getList(param, callback, error){
var bno = param.bno;
var page = param.page || 1;
$.getJSON("/replies/pages/" + bno + "/" + page + ".json",
function(data) {
if (callback) {
callback(data);
}
}).fail(function(xhr, status, err) {
if (error){
error();
}
});
}
return {
add : add,
getList : getList
};
})();
getList( )는 param 객체를 통해 파라미터를 전달 받아서 JSON 목록으로 호출합니다('.json'으로 확장자를 요구).
JSP 파일을 통해 해당 게시물의 모든 댓글을 가져오는지 확인해 봅니다.
replyService.getList({bno:bnoValue, page:1}, function(list){
for(var i = 0, len = list.length||0; i < len; i++ ){
console.log(list[i]);
}
})
4. 댓글 삭제와 갱신
댓글 삭제는 DELETE 방식을 통해 해당 URL을 호출하는 것입니다.
function remove(rno, callback, error) {
$.ajax({
type : 'delete',
url : '/replies/' + rno,
success : function(deleteResult, status, xhr) {
if(callback){
callback(deleteResult);
}
},
error : function(xhr, status, er) {
if(error){
error(er);
}
}
});
}
return {
add : add,
getList : getList,
remove : remove
};
})();
remove( )는 DELETE 방식으로 데이터를 전달하기 때문에
$.ajax( )를 이용해 구체적으로 type 속성을 'delete'로 지정합니다.
JSP 파일에서 실제 DB에 있는 댓글 번호로 정상적으로 댓글이 삭제되는지 봅니다.
replyService.remove(17, function(count) {
console.log(count);
if (count === "success") {
alert("REMOVED");
}
}, function(err) {
alert('ERROR...');
});
5. 댓글 수정
수정하는 내용과 함께 댓글의 번호를 전송해야 합니다.
전송할 때는 등록과 마찬가지로 JSON 형태로 합니다.
function update(reply, callback, error) {
console.log("RNO: " + reply.rno);
$.ajax({
type : 'put',
url : '/replies/' + reply.rno,
data : JSON.stringify(reply),
contentType : "application/json; charset=utf-8",
success : function(result, status, xhr) {
if (callback) {
callback(result);
}
},
error : function(xhr, status, er) {
if (error) {
error(er);
}
}
});
}
replyService.update({
rno : 12,
bno : bnoValue,
reply : "Modified Reply...."
}, function(result) {
alert("수정 완료...");
});
6. 댓글 조회 처리
특정 댓글 조회는 get 방식으로 동작합니다.
function get(reply, callback, error) {
$.get("/replies/" + rno + ".json", function(result) {
if(callback) {
callback(result);
}
}).fail(function(xhr, status, err){
if (error) {
error();
}
});
}
return {
add : add,
getList : getList,
remove : remove,
update : update,
get : get
};
})();
JSP 파일에서는 댓글의 번호만 간단히 전달하면 됩니다.
replyService.get(10, function(data) {
console.log(data);
});
이벤트 처리와 HTML 처리
화면에서 버튼 등에서 발생하는 이벤트를 감지하고 Ajax 호출의 결과를 화면에 반영해야 합니다.
1. 댓글 목록 처리
댓글 목록에서는 별도의 <div> 처리를 해야 합니다.
<div class='row'>
<div class="col-lg-12">
<!-- /.panel -->
<div class="panel panel-default">
<div class="panel-heading">
<i class="fa fa-comments fa-fw"></i> Reply
</div>
<!-- /.panel-heading -->
<div class="panel-body">
<ul class="chat">
<!-- start reply -->
<li class="left clearfix" data-rno='12'>
<div>
<div class="header">
<strong class="primary-font">user00</strong>
<small class="pull-right text-muted">2018-01-01 13:13</small>
</div>
</li>
<!-- end reply -->
</ul>
<!-- ./ end ul -->
</div>
<!-- /.panel .chat-panel -->
</div>
</div>
<!-- ./ end row -->
</div>
댓글 목록은 <ul> 태그 안에 <li> 태그로 처리합니다.
<li> 태그는 하나의 댓글이라 수정이나 삭제 시 이것을 클릭하게 됩니다.
이벤트 처리
게시글의 조회 페이지가 열리면 자동으로 댓글 목록을 가져와 <li> 태그를 구성하는데
이에 대한 처리는 $(document).ready( ) 안에서 이루어지도록 해야 합니다.
<script>
$(document).ready(function () {
var bnoValue = '<c:out value="${board.bno}"/>';
var replyUL = $(".chat");
showList(1);
function showList(page){
console.log("show list " + page);
replyService.getList({bno:bnoValue,page: page|| 1 }, function(replyCnt, list) {
console.log("replyCnt: "+ replyCnt );
console.log("list: " + list);
console.log(list);
if(page == -1){
pageNum = Math.ceil(replyCnt/10.0);
showList(pageNum);
return;
}
var str="";
if(list == null || list.length == 0){
return;
}
for (var i = 0, len = list.length || 0; i < len; i++) {
str +="<li class='left clearfix' data-rno='"+list[i].rno+"'>";
str +=" <div><div class='header'><strong class='primary-font'>["
+list[i].rno+"] "+list[i].replyer+"</strong>";
str +=" <small class='pull-right text-muted'>"
+replyService.displayTime(list[i].replyDate)+"</small></div>";
str +=" <p>"+list[i].reply+"</p></div></li>";
}
replyUL.html(str);
showReplyPage(replyCnt);
});//end function
}//end showList
$(document).ready() 함수를 사용하여 문서가 준비되면 코드를 실행합니다.
그런 다음에는 showList(1) 함수를 호출하여 첫 번째 페이지의 데이터를 표시합니다.
bnoValue 변수는 문자열을 담고 있습니다.
${board.bno}는 JSP에서 게시물의 번호를 출력하는 표현식으로, 해당 게시물의 번호를 가져옵니다.
이 값을 JavaScript 변수에 할당합니다.
replyUL 변수는 jQuery를 사용하여 HTML 문서 내의 chat 클래스를 가진 요소들을 선택합니다.
$(".chat")는 HTML에서 class 속성이 chat인 모든 요소를 선택합니다.
이 변수는 나중에 이 요소에 댓글 목록을 추가할 때 사용됩니다.
showList(1)은 페이지가 로드되면 호출되는 함수입니다.
이 함수는 첫 번째 페이지의 댓글 목록을 표시하기 위해 호출됩니다.
함수 내에서는 Ajax를 사용하여 서버에서 데이터를 가져오는 로직이 구현되어 있습니다.
showList( )는 페이지 번호를 파라미터로 받도록 하고 만약 페이지 번호가 주어지지 않으면 기본값으로 1을 사용합니다.
댓글 목록은 replyService.getList() 함수를 호출하여 서버로부터 가져옵니다.
이 함수는 서버에 요청을 보내고, 응답으로 댓글 수와 댓글 목록을 받아옵니다.
그 후, 받아온 댓글 목록을 HTML 형식으로 생성하여 웹 페이지에 표시합니다.
이때, 댓글 목록이 비어있거나 null인 경우에는 표시하지 않습니다.
for문에 대한 해석은 다음과 같습니다.
list 배열의 각 요소에 대해 HTML 문자열을 생성합니다.
각 댓글은 <li> 요소 안에 들어가게 되며, 클래스 left clearfix가 추가되고, data-rno 속성을 통해 댓글의 고유 번호(rno)가 설정됩니다. 댓글의 내용, 작성자, 작성 일시 등의 정보가 해당 HTML 요소 안에 포함됩니다.
여기서 list[i].rno, list[i].replyer, list[i].replyDate, list[i].reply 등은 서버에서 가져온 댓글 객체의 속성들을 나타냅니다.
이들을 이용하여 각 댓글의 정보를 HTML 요소에 반영하여 화면에 표시합니다.
간단하게 설명하면 1페이지가 아닐 경우 기존 <ul>에 <li>들이 추가됩니다.
시간에 대한 처리
xml과 JSON 형태로 데이터를 받을 때는 순수하게 숫자로 표현되는 시간 값이 나오게 되어있습니다.
때문에 화면에서는 이것을 포맷해서 사용하는 게 좋습니다.
function displayTime(timeValue) {
var today = new Date();
var gap = today.getTime() - timeValue;
var dateObj = new Date(timeValue);
var str = "";
if(gap < (1000 * 60 * 60 * 24)) {
var hh = dateObj.getHours();
var mi = dateObj.getMinutes();
var ss = dateObj.getSeconds();
return [ (hh > 9 ? '' : '0') + hh, ':', (mi > 9 ? '' : '0') + mi,
':', (ss > 9 ? '' : '0') + ss ].join('');
} else {
var yy = dateObj.getFullYear();
var mm = dateObj.getMonth() + 1;
var dd = dateObj.getDate();
return [yy, '/', (mm > 9 ? '' : '0') + mm, '/',
(dd > 9 ? '' : '0') + dd ].join('');
}
};
return {
add : add,
getList : getList,
remove : remove,
update : update,
get : get,
displayTime : displayTime
};
})();
이 메서드를 적용하면 24시간이 지난 댓글은 날짜만 표시되고 24시간 이내의 글은 시간으로 표시됩니다.
2. 새로운 댓글 처리
댓글 목록 상단에 새 댓글을 작성할 수 있는 버튼을 추가합니다.
<!-- 기존 panel-heading 주석처리 후 추가 -->
<div class="panel-heading">
<i class="fa fa-comments fa-fw"?></i> Reply
<button id='addReplyBtn' class='btn btn-primary btn-xs pull-right'>New Reply</button>
</div>
댓글을 추가하는 동작은 모달창을 이용합니다.
<div class="modal fade" id="myModal" tabindex="-1" role="dialog"
aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">×</button>
<h4 class="modal-title" id="myModalLabel">REPLY MODAL</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label>Reply</label>
<input class="form-control" name='reply' value='New Reply!!!!'>
</div>
<div class="form-group">
<label>Replyer</label>
<input class="form-control" name='replyer' value='replyer'>
</div>
<div class="form-group">
<label>Reply Date</label>
<input class="form-control" name='replyDate' value='2018-01-01 13:13'>
</div>
</div>
<div class="modal-footer">
<button id='modalModBtn' type="button" class="btn btn-warning">Modify</button>
<button id='modalRemoveBtn' type="button" class="btn btn-danger">Remove</button>
<button id='modalRegisterBtn' type="button" class="btn btn-primary">Register</button>
<button id='modalCloseBtn' type="button" class="btn btn-default">Close</button>
</div> </div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
새로운 댓글의 추가 버튼 이벤트 처리
모달과 관련된 객체들은 여러 함수에서 사용할 것이기 때문에 바깥으로 빼두도록 한다(매번 jQuery를 호출하는 번거로움이 없어진다).
var modal = $(".modal");
var modalInputReply = modal.find("input[name='reply']");
var modalInputReplyer = modal.find("input[name='replyer']");
var modalInputReplyDate = modal.find("input[name='replyDate']");
var modalModBtn = $("#modalModBtn");
var modalRemoveBtn = $("#modalRemoveBtn");
var modalRegisterBtn = $("#modalRegisterBtn");
$("#modalCloseBtn").on("click", function(e){
modal.modal('hide');
});
$("#addReplyBtn").on("click", function(e){
modal.find("input").val("");
modalInputReplyDate.closest("div").hide();
modal.find("button[id !='modalCloseBtn']").hide();
modalRegisterBtn.show();
$(".modal").modal("show");
});
이렇게 해서 모달창에 댓글 작성자가 입력할 필요가 없는 날짜 같은 항목을 보이지 않게 처리합니다.
댓글 등록 및 목록 갱신
새로운 댓글은 Register 버튼으로 처리합니다.
modalRegisterBtn.on("click",function(e){
var reply = {
reply: modalInputReply.val(),
replyer:modalInputReplyer.val(),
bno:bnoValue
};
replyService.add(reply, function(result){
alert(result);
modal.find("input").val("");
modal.modal("hide");
//showList(1);
showList(-1);
});
});
댓글이 정상적으로 등록되면 성공을 알리는 알림창을 띄워줍니다.
하지만 목록이 갱신된 적은 없으므로 showList(-1)을 써주어서 그 사이에 추가되었을 새 댓글을 가져옵니다.
3. 특정 댓글의 클릭 이벤트 처리
우리는 해당 댓글을 수정하거나 삭제할 때 별도의 클릭을 합니다.
DOM에서 이벤트 리스너(이벤트가 발생할 때까지 기다린 후 이벤트에 응답하는 JavaScript의 함수)를 등록하려면
반드시 해당 DOM 요소가 존재해야지 가능합니다.
동적으로 Ajax를 통해 <li> 태그들이 만들어지면 이후에 이벤트를 등록해야 하기 때문에 '이벤트 위임(delegation)'의 형태로 작성해야 합니다.
이벤트 위임이란 이벤트가 동적으로 생성되는 요소가 아니라 이미 존재하는 요소에 이벤트를 걸어주고
나중에 이벤트의 대상을 변경해 주는 방식을 말합니다.
jQuery는 on( )을 이용해 쉽게 처리할 수 있습니다.
$(".chat").on("click", "li", function(e){
var rno = $(this).data("rno");
consloe.log(rno);
});
jQuery에서 이벤트를 위임하는 방식은
이미 존재하는 DOM 요소에 이벤트를 처리하고 나중에 동적으로 생기는 요소들에 대해 파라미터 형식으로 지정합니다.
<ul> 태그의 클래스 .chat을 이용해서 이벤트를 걸고, 실제 이벤트 대상은 <li> 태그가 되도록 합니다.
이렇게 되면 브라우저에서 이벤트는 <ul>에 걸었지만 각 댓글은 이벤트의 this가 됩니다.
$(".chat").on("click", "li", function(e){
var rno = $(this).data("rno");
replyService.get(rno, function(reply){
modalInputReply.val(reply.reply);
modalInputReplyer.val(reply.replyer);
modalInputReplyDate.val(replyService.displayTime( reply.replyDate))
.attr("readonly","readonly");
modal.data("rno", reply.rno);
modal.find("button[id !='modalCloseBtn']").hide();
modalModBtn.show();
modalRemoveBtn.show();
$(".modal").modal("show");
});
});
원칙적으로 Ajax로 댓글을 조회하고나서 수정/삭제하도록 합니다.
댓글을 가져온 후에는 필요한 항목들을 채우고 수정과 삭제에 필요한 댓글 번호는 'data-rno' 속성을 만들어 추가합니다.
4. 댓글의 수정/삭제 이벤트 처리
1) 수정
modalModBtn.on("click", function(e){
var reply = {rno:modal.data("rno"), reply: modalInputReply.val()};
replyService.update(reply, function(result){
alert(result);
modal.modal("hide");
showList(pageNum);
});
});
모달(modal)에서 수정 버튼(modalModBtn)이 클릭되었을 때 실행되는 함수입니다.
'click' 시 구현되는 기능을 정의합니다.
해당 댓글의 번호와 수정된 댓글 내용을 담은 JavaScript 객체(reply)를 생성합니다.
modal.data("rno")는 모달 창(modal)의 데이터 속성에서 댓글 번호를 가져오고,
modalInputReply.val()는 모달 창 내의 입력 필드에서 수정된 댓글 내용을 가져옵니다.
replyService.update(reply, function(result){ ... })
서버로 댓글을 업데이트하기 위해 replyService.update() 메서드를 호출합니다.
이 메서드는 서버로 댓글 객체(reply)를 전송하고, 업데이트 결과를 처리하는 콜백 함수를 인자로 받습니다.
2) 삭제
modalRemoveBtn.on("click", function (e){
var rno = modal.data("rno");
replyService.remove(rno, function(result){
alert(result);
modal.modal("hide");
showList(pageNum);
});
});
댓글 페이징 처리
댓글의 숫자가 많으면 DB에서 많은 양의 데이터를 가져와야 하기 때문에 성능상 문제가 될 수 있으므로
페이징 처리를 이용합니다.
1. DB의 인덱스 설계
댓글에서 우선적으로는 게시물의 번호가 중심이 되는 점을 고려해야 합니다.
댓글의 번호를 중심으로 한다면 중간에 있는 다른 게시물의 번호들은 건너뛰며 특정 게시물의 댓글을 찾아야 하기 때문에
데이터가 많아지면 성능에 문제가 될 수 있습니다.
게시물의 번호가 중심이게 되면
예를 들어 'bno=200 order by rno asc'와 같은 쿼리를 실행할 때 왼쪽의 구조에서 200에 해당하는 범위만 찾아서 사용하게 됩니다.
create index idx-reply on tbl_reply (bno desc, rno asc);
2. 인덱스를 이용한 페이징 쿼리
인덱스를 이용하면 정렬을 피할 수 있습니다.
특정한 게시물의 rno의 순번대로 데이터를 조회하고 싶다면
select /*+ INDEX(tbl_reply idx_reply) */
rownum rn, bno, rno, reply, replyer, replyDate, updatedate
from tbl_reply
where bno = 게시물 번호
and rno > 0
위와 같이 작성합니다.
'IDX_REPLY'를 이용해 테이블에 접근합니다.
테이블에 접근해서 결과를 만들 때 생성되는 ROWNUM은 가장 낮은 rno 값을 가지는 데이터가 1번이 되게 됩니다.
10개씩 2페이지를 가져온다면
select rno, bno, reply, replyer, replydate, updatedate
from
(
select /*+INDEX(tbl_reply idx_reply) */
rownum rn, bno, rno, reply, replyer, replyDate, updatedate
from tbl_reply
where bno = 게시물 번소
and rno > 0
and rownum <= 20
) where rn > 10;
위와 같이 작성합니다.
sql 파일에서 잘 도는지 확인한 다음 mapper.xml에 반영합니다.
<select id="getListWithPaging" resultType="org.zerock.domain.ReplyVO">
<![CDATA[
select rno, bno, reply, replyer, replyDate, updatedate
from
(
select /*+ INDEX(tbl_reply idx_reply) */
rownum rn, rno, bno, reply, replyer, replyDate, updatedate
from tbl_reply
where bno = #{bno}
and rno > 0
and rownum <= #{cri.pageNum} * #{cri.amount}
) where rn > (#{cri.pageNum} - 1) * #{cri.amount}
]]>
</select>
잘 도는지 테스트를 해볼까요?
@Test
public void testList2() {
Criteria cri = new Criteria(2, 10);
List<ReplyVO> replies = mapper.getListWithPaging(cri, 75L);
replies.forEach(reply -> log.info(reply));
}
3. 댓글 숫자 파악
댓글 페이징 처리를 위해서는 해당 게시물의 전체 댓글의 숫자를 파악해야 합니다.
mapper 인터페이스에 댓글 숫자를 파악하는 메서드인 getCountByBno( )를 추가합니다.
public int getCountByBno(Long bno);
그리고나서 mapper xml로 가 동일한 이름으로 쿼리문을 만들어줍니다.
<select id="getCountByBno" resultType="int">
<![CDATA[
select count(bno) from tbl_reply where bno = #{bno}
]]>
</select>
4. ServiceImpl에서 댓글과 댓글 수 처리
댓글의 페이징 처리에서는 댓글 목록과 전체 댓글 수를 전달해야 합니다.
ServiceImpl 클래스를 List<ReplyVO>와 댓글의 수를 같이 전달할 수 있는 구조로 바꿔야 합니다.
우선은 DTO를 생성합니다.
@Data
@AllArgsConstructor
@Getter
public class ReplyPageDTO {
private int replyCnt;
private List<ReplyVO> list;
}
@AllArgsConstructor를 이용해서 replyCnt와 list를 생성자의 파라미터로 처리합니다(생성자 주입).
그리고 Service 계층에 DTO를 반환하는 메서드를 추가합니다.
public ReplyPageDTO getListPage(Criteria cri, Long bno);
@Override
public ReplyPageDTO getListPage(Criteria cri, Long bno) {
return new ReplyPageDTO(mapper.getCountByBno(bno), mapper.getListWithPaging(cri, bno));
}
5. Controller 수정
Controller에서는 ReplyService에 새롭게 추가된 getListPage( )를 호출하고 데이터를 전송하는 형태로 수정합니다.
@GetMapping(value = "/pages/{bno}/{page}",
produces = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_UTF8_VALUE })
public ResponseEntity<ReplyPageDTO> getList(@PathVariable("page") int page, @PathVariable("bno") Long bno) {
Criteria cri = new Criteria(page, 10);
log.info("get Reply List bno: " + bno);
log.info("cri:" + cri);
return new ResponseEntity<>(service.getListPage(cri, bno), HttpStatus.OK);
}
DTO 객체를 JSON으로 전송하게 되므로 특정 게시물의 댓글 목록을 조회하면 replyCnt와 list 이름의 속성을 가지는 JSON 문자열이 전송됩니다(콘솔에서 확인 가능).
댓글 페이지의 화면 처리
게시물을 조회하는 페이지에 들어오면 가장 오래된 댓글들을 가져와서 1페이지에 보여줍니다.
1페이지의 게시물을 가져올 때 해당 게시물의 댓글의 숫자를 파악해서 댓글의 페이지 번호를 출력합니다.
댓글이 추가되면 댓글의 숫자만을 가져와 최종 페이지를 찾아서 이동합니다.
댓글의 수정과 삭제 후에는 다시 동일 페이지를 호출합니다.
1. 댓글 페이지 계산과 출력
Ajax로 가져오는 데이터가 replyCnt와 list라는 데이터로 구성되기 때문에 이것을 처리하는 js 파일 역시 수정해야 합니다.
function getList(param, callback, error){
var bno = param.bno;
var page = param.page || 1;
$.getJSON("/replies/pages/" + bno + "/" + page + ".json",
function(data) {
if (callback) {
//callback(data);
callback(data.replyCnt, data.list); //댓글 숫자와 목록을 가져오는 경우
}
}).fail(function(xhr, status, err) {
if (error){
error();
}
});
}
callback 함수에 해당 게시물의 댓글 수와 페이지에 해당하는 댓글 데이터를 전달하도록 변경합니다.
function showList(page){
console.log("show list " + page);
replyService.getList({bno:bnoValue,page: page|| 1 }, function(replyCnt, list) {
console.log("replyCnt: "+ replyCnt );
console.log("list: " + list);
console.log(list);
if(page == -1){
pageNum = Math.ceil(replyCnt/10.0);
showList(pageNum);
return;
}
var str="";
if(list == null || list.length == 0){
return;
}
for (var i = 0, len = list.length || 0; i < len; i++) {
str +="<li class='left clearfix' data-rno='"+list[i].rno+"'>";
str +=" <div><div class='header'><strong class='primary-font'>["
+list[i].rno+"] "+list[i].replyer+"</strong>";
str +=" <small class='pull-right text-muted'>"
+replyService.displayTime(list[i].replyDate)+"</small></div>";
str +=" <p>"+list[i].reply+"</p></div></li>";
}
replyUL.html(str);
showReplyPage(replyCnt);
});//end function
}//end showList
showList( ) 함수는 파라미터로 전달되는 page 변수를 이용해서 원하는 댓글 페이지를 가져옵니다.
이 때 page 번호가 -1로 전달되면 마지막 페이지를 찾아 다시 호출하게 됩니다.
사용자가 새로운 댓글을 추가하면 showList(-1)를 호출하여 우선 전체 댓글의 숫자를 파악하게 합니다.
이후 다시 마지막 페이지를 호출해 이동시키는 방식으로 동작시킵니다.
modalRegisterBtn.on("click",function(e){
var reply = {
reply: modalInputReply.val(),
replyer:modalInputReplyer.val(),
bno:bnoValue
};
replyService.add(reply, function(result){
alert(result);
modal.find("input").val("");
modal.modal("hide");
//showList(1);
showList(-1);
});
});
댓글은 화면상에서 댓글이 출력되는 영역의 아래쪽에 <div class='panel-footer'>를 하나 추가하고 <div>의 아래에 추가합니다.
<!-- /.panel-heading -->
<div class="panel-body">
<ul class="chat">
</ul>
<!-- ./ end ul -->
</div>
<!-- /.panel .chat-panel -->
<div class="panel-footer"></div>
</div>
</div>
<!-- ./ end row -->
</div>
var pageNum = 1;
var replyPageFooter = $(".panel-footer");
function showReplyPage(replyCnt){
var endNum = Math.ceil(pageNum / 10.0) * 10;
var startNum = endNum - 9;
var prev = startNum != 1;
var next = false;
if(endNum * 10 >= replyCnt){
endNum = Math.ceil(replyCnt/10.0);
}
if(endNum * 10 < replyCnt){
next = true;
}
var str = "<ul class='pagination pull-right'>";
if(prev){
str+= "<li class='page-item'><a class='page-link' href='"+(startNum -1)+"'>Previous</a></li>";
}
for(var i = startNum ; i <= endNum; i++){
var active = pageNum == i? "active":"";
str+= "<li class='page-item "+active+" '><a class='page-link' href='"+i+"'>"+i+"</a></li>";
}
if(next){
str+= "<li class='page-item'><a class='page-link' href='"+(endNum + 1)+"'>Next</a></li>";
}
str += "</ul></div>";
console.log(str);
replyPageFooter.html(str);
}
showReplyPage( )는 기존에 Java로 작성되는 PageMaker의 JS 버전에 해당합니다.
댓글 페이지를 문자열로 구성한 다음 <div>의 innerHTML로 추가합니다.
replyPageFooter.on("click","li a", function(e){
e.preventDefault();
console.log("page click");
var targetPageNum = $(this).attr("href");
console.log("targetPageNum: " + targetPageNum);
pageNum = targetPageNum;
showList(pageNum);
});
페이지 번호를 클릭했을 때 새로운 댓글을 가져오도록 합니다.
댓글의 페이지 번호는 <a> 태그 내에 존재하므로 이벤트 처리에서는 <a> 태그의 기본 동작을 제한하고 댓글 페이지 번호를 변경한 후 해당 페이지의 댓글을 가져옵니다.
2. 댓글의 수정과 삭제
댓글이 페이지 처리되면 댓글의 수정과 삭제 시에도 현재 댓글이 포함된 페이지로 이동하도록 수정합니다.
showList( )를 호출할 때 현재 보고 있는 댓글 페이지의 번호를 호출합니다. 브라우저에서 댓글의 등록, 수정, 삭제 작업은 모두 페이지 이동을 하게 됩니다.
modalModBtn.on("click", function(e){
var reply = {rno:modal.data("rno"), reply: modalInputReply.val()};
replyService.update(reply, function(result){
alert(result);
modal.modal("hide");
showList(pageNum);
});
});
modalRemoveBtn.on("click", function (e){
var rno = modal.data("rno");
replyService.remove(rno, function(result){
alert(result);
modal.modal("hide");
showList(pageNum);
});
});
'Backend > Spring' 카테고리의 다른 글
[Spring] AOP와 트랜잭션(@Transactional) (0) | 2024.03.08 |
---|---|
[Spring] AOP에 대하여 (0) | 2024.03.07 |
[Spring] MyBatis의 페이징 처리 (0) | 2024.03.05 |
[Spring] 의존성 주입 테스트 (0) | 2024.02.26 |
[Spring] 스프링의 개념과 특징 총정리 (0) | 2024.02.22 |