이번 게시판 만드는 목표는 댓글 기능 + 이전글/ 다음글 만들기 이다.
cafe관련 dao/dto/service/controller만들기
cafe.dto package에 num, writer, title, content, viewCount, regdate 등등의 필드와 setter, getter를 만들어준다.
CafeMaapper.xml폴더를 만들어준다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cafe">
</mapper>
Configuration.xml에서 CafeMapper.xml를 추가한다.
이전글 다음글을 할때는 글의 번호가 순서대로 1,2,3,4 일수는 없다👉
중간에 글이 삭제 되거나 조건을 걸어서 검색을 할 경우에 글 번호는 뒤죽박죽일 수 도있으니까😶🌫️
EMPNO "7902" ENAME "FORD" SAL "3000" 보다 SAL이 많은 EMPNO "7839" 가 LAG 칼럼에 들어와져있다.
위에 걸 응용해서 NEXTNO, PREVNO 를 사용해 이전글 과 다음글을 구현 할 수 있다.(있을 것 같다....)
CafeMapper.xml에 추가한 이전글/다음글 기능
<select id="getData2" parameterType="cafeDto" resultType="cafeDto">
SELECT result1.*
FROM
(SELECT num,writer,title,content,viewCount,TO_CHAR(regdate, 'YY.MM.DD HH24:MI') AS regdate,
LAG(num, 1, 0) OVER (ORDER BY num DESC) nextNum,
LEAD(num, 1, 0) OVER (ORDER BY num DESC) prevNum
FROM board_cafe
<include refid="searchCondition"/>
) result1
WHERE num=#{num}
</select>
XML문서는 기호에 민감하다. 그래서 비교 연산자를 사용할 때는
<![CDATA[ 비교 연산자 ]]>
<![CDATA[
WHERE rnum >= #{startRowNum} AND rnum <= #{endRowNum}
]]>
라는 식으로 사용하면 된다.
그렇게 완성된 CafeMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cafe">
<sql id="searchCondition">
<choose>
<when test="title != null and content != null">
WHERE title LIKE '%'||#{title}||'%' OR content LIKE '%'||#{content}||'%'
</when>
<when test="title != null">
WHERE title LIKE '%'||#{title}||'%'
</when>
<when test="writer != null">
WHERE writer LIKE '%'||#{writer}||'%'
</when>
</choose>
</sql>
<select id="getList" parameterType="cafeDto" resultType="cafeDto">
SELECT *
FROM
(SELECT result1.*, ROWNUM AS rnum
FROM
(SELECT num,writer,title,content,viewCount,regdate
FROM board_cafe
<include refid="searchCondition"/>
ORDER BY num DESC) result1)
<![CDATA[
WHERE rnum >= #{startRowNum} AND rnum <= #{endRowNum}
]]>
</select>
<select id="getCount" parameterType="cafeDto" resultType="int">
SELECT NVL(MAX(ROWNUM), 0)
FROM board_cafe
<include refid="searchCondition"/>
</select>
<insert id="insert" parameterType="cafeDto">
INSERT INTO board_cafe
(num,writer,title,content,viewCount,regdate)
VALUES(board_cafe_seq.NEXTVAL, #{writer}, #{title},
#{content}, #{viewCount}, SYSDATE)
</insert>
<select id="getData" parameterType="int" resultType="cafeDto">
SELECT result1.*
FROM
(SELECT num,writer,title,content,viewCount,TO_CHAR(regdate, 'YY.MM.DD HH24:MI') AS regdate,
LAG(num, 1, 0) OVER (ORDER BY num DESC) nextNum,
LEAD(num, 1, 0) OVER (ORDER BY num DESC) prevNum
FROM board_cafe) result1
WHERE num=#{num}
</select>
<select id="getData2" parameterType="cafeDto" resultType="cafeDto">
SELECT result1.*
FROM
(SELECT num,writer,title,content,viewCount,TO_CHAR(regdate, 'YY.MM.DD HH24:MI') AS regdate,
LAG(num, 1, 0) OVER (ORDER BY num DESC) nextNum,
LEAD(num, 1, 0) OVER (ORDER BY num DESC) prevNum
FROM board_cafe
<include refid="searchCondition"/>
) result1
WHERE num=#{num}
</select>
<update id="addViewCount" parameterType="int">
UPDATE board_cafe
SET viewCount=viewCount+1
WHERE num=#{num}
</update>
<delete id="delete" parameterType="int">
DELETE FROM board_cafe
WHERE num=#{num}
</delete>
<update id="update" parameterType="cafeDto">
UPDATE board_cafe
SET title=#{title}, content=#{content}
WHERE num=#{num}
</update>
</mapper>
다음으로 CafeDao만들기
package com.gura.spring04.cafe.dao;
import java.util.List;
import com.gura.spring04.cafe.dto.CafeDto;
public interface CafeDao {
//글목록
public List<CafeDto> getList(CafeDto dto);
//글의 갯수
public int getCount(CafeDto dto);
//글 추가
public void insert(CafeDto dto);
//글정보 얻어오기
public CafeDto getData(int num);
//키워드를 활용한 글정보 얻어오기(키워드에 부합하는 글 중에서 이전글, 다음글의 글번호도 얻어올 수 있도록)
public CafeDto getData(CafeDto dto);
//조회수 증가 시키기
public void addViewCount(int num);
//글 삭제
public void delete(int num);
//글 수정
public void update(CafeDto dto);
}
글의 목록, 갯수 글 추가, 글의 정보, 검색기능, 조회수 증가, 글 삭제, 수정의 기능을 가지고 있다.
CafeDaoImpl 클래스가 객체에 대한 저장, 검색, 검색, 업데이트 및 삭제 작업을 위한 메커니즘을 제공함을 나타내는 데 사용된다.
CafeDaoImpl Class완성하기
package com.gura.spring04.cafe.dao;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.gura.spring04.cafe.dto.CafeDto;
@Repository
public class CafeDaoImpl implements CafeDao{
@Autowired
private SqlSession session;
@Override
public List<CafeDto> getList(CafeDto dto) {
return session.selectList("cafe.getList", dto);
}
@Override
public int getCount(CafeDto dto) {
return session.selectOne("cafe.getCount", dto);
}
@Override
public void insert(CafeDto dto) {
session.insert("cafe.insert", dto);
}
@Override
public CafeDto getData(int num) {
return session.selectOne("cafe.getData", num);
}
@Override
public CafeDto getData(CafeDto dto) {
return session.selectOne("cafe.getData2", dto);
}
@Override
public void addViewCount(int num) {
session.update("cafe.addViewCount", num);
}
@Override
public void delete(int num) {
session.delete("cafe.delete", num);
}
@Override
public void update(CafeDto dto) {
session.update("cafe.update", dto);
}
}
그 다음으로 Service를 만들러 가보겠다.
package com.gura.spring04.cafe.service;
import javax.servlet.http.HttpServletRequest;
import com.gura.spring04.cafe.dto.CafeDto;
public interface CafeService {
public void getList(HttpServletRequest request);
public void getDetail(HttpServletRequest request);
public void saveContent(CafeDto dto);
public void updateContent(CafeDto dto);
public void deleteContent(int num, HttpServletRequest request);
public void getData(HttpServletRequest request);//글 수정하기 위해 정보 불러오는 기능
}
Serivce 인터페이스를 구동해줄 SerivceImpl만들기(CafeDao에 의존객체를 주입받는다)
Controller만들기(Service에 의존객체를 주입받는다.)
package com.gura.spring04.cafe.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import com.gura.spring04.cafe.service.CafeService;
@Controller
public class CafeController {
@Autowired
private CafeService service;
}
servlet-context.xml에서 interceptor추가해주기
<interceptor>
<mapping path="/cafe/*"/>
<exclude-mapping path="/cafe.list"/>
<exclude-mapping path="/cafe/detail"/>
<exclude-mapping path="/cafe/ajax_comment_list"/>
<beans:ref bean="loginInterceptor"/>
</interceptor>
CafeController
@RequestMapping("/cafe/list")
public String list(HttpServletRequest request) {
service.getList(request);
return "cafe/list";
}
@RequestMapping("/cafe/insertform")
public String insertform() {
return "cafe/insertform";
}
@RequestMapping("/cafe/insert")
public String insert(CafeDto dto, HttpSession session) {
//글 작성자는 세션에서 얻어낸다.
String writer=(String)session.getAttribute("id");
//dto 는 글의 제목과 내용만 있으므로 글작성자는 직접 넣어준다.
dto.setWriter(writer);
service.saveContent(dto);
return "cafe/insert";
}
@RequestMapping("/cafe/detail")
public String detail(HttpServletRequest request) {
service.getDetail(request);
return "cafe/detail";
}
@RequestMapping("/cafe/delete")
public String delete(int num, HttpServletRequest request) {
service.deleteContent(num, request);
return "redirect:/cafe/list";
}
@RequestMapping("/cafe/updateform")
public String updateForm(HttpServletRequest request) {
service.getData(request);
return "cafe/updateform";
}
@RequestMapping("/cafe/update")
public String update(CafeDto dto) {
service.updateContent(dto);
return "cafe/update";
}
list() 메서드
/cafe/list요청에 대해서 servie.getList에 /서브시에 HttpServletRequest 객체를 전달해서 응답에 필요한 데이터가 request에담기도록 하고 return에서 view page로 forward이동해서 응답하기
CafeServiceImpl 에서 검색기능이 구현된 getList()메소드
@Override
public void getList(HttpServletRequest request) {
//한 페이지에 몇개씩 표시할 것인지
final int PAGE_ROW_COUNT=5;
//하단 페이지를 몇개씩 표시할 것인지
final int PAGE_DISPLAY_COUNT=5;
//보여줄 페이지의 번호를 일단 1이라고 초기값 지정
int pageNum=1;
//페이지 번호가 파라미터로 전달되는지 읽어와 본다.
String strPageNum=request.getParameter("pageNum");
//만일 페이지 번호가 파라미터로 넘어 온다면
if(strPageNum != null){
//숫자로 바꿔서 보여줄 페이지 번호로 지정한다.
pageNum=Integer.parseInt(strPageNum);
}
//보여줄 페이지의 시작 ROWNUM
int startRowNum=1+(pageNum-1)*PAGE_ROW_COUNT;
//보여줄 페이지의 끝 ROWNUM
int endRowNum=pageNum*PAGE_ROW_COUNT;
/*
[ 검색 키워드에 관련된 처리 ]
-검색 키워드가 파라미터로 넘어올수도 있고 안넘어 올수도 있다.
*/
String keyword=request.getParameter("keyword");
String condition=request.getParameter("condition");
//만일 키워드가 넘어오지 않는다면
if(keyword==null){
//키워드와 검색 조건에 빈 문자열을 넣어준다.
//클라이언트 웹브라우저에 출력할때 "null" 을 출력되지 않게 하기 위해서
keyword="";
condition="";
}
//특수기호를 인코딩한 키워드를 미리 준비한다.
String encodedK=URLEncoder.encode(keyword);
//CafeDto 객체에 startRowNum 과 endRowNum 을 담는다.
CafeDto dto=new CafeDto();
dto.setStartRowNum(startRowNum);
dto.setEndRowNum(endRowNum);
//만일 검색 키워드가 넘어온다면
if(!keyword.equals("")){
//검색 조건이 무엇이냐에 따라 분기 하기
if(condition.equals("title_content")){//제목 + 내용 검색인 경우
//검색 키워드를 CafeDto 에 담아서 전달한다.
dto.setTitle(keyword);
dto.setContent(keyword);
}else if(condition.equals("title")){ //제목 검색인 경우
dto.setTitle(keyword);
}else if(condition.equals("writer")){ //작성자 검색인 경우
dto.setWriter(keyword);
} // 다른 검색 조건을 추가 하고 싶다면 아래에 else if() 를 계속 추가 하면 된다.
}
//글 목록 얻어오기
List<CafeDto> list=cafeDao.getList(dto);
//전체글의 갯수
int totalRow=cafeDao.getCount(dto);
//하단 시작 페이지 번호
int startPageNum = 1 + ((pageNum-1)/PAGE_DISPLAY_COUNT)*PAGE_DISPLAY_COUNT;
//하단 끝 페이지 번호
int endPageNum=startPageNum+PAGE_DISPLAY_COUNT-1;
//전체 페이지의 갯수
int totalPageCount=(int)Math.ceil(totalRow/(double)PAGE_ROW_COUNT);
//끝 페이지 번호가 전체 페이지 갯수보다 크다면 잘못된 값이다.
if(endPageNum > totalPageCount){
endPageNum=totalPageCount; //보정해 준다.
}
//view page 에서 필요한 값을 request 에 담아준다.
request.setAttribute("pageNum", pageNum);
request.setAttribute("startPageNum", startPageNum);
request.setAttribute("endPageNum", endPageNum);
request.setAttribute("condition", condition);
request.setAttribute("keyword", keyword);
request.setAttribute("encodedK", encodedK);
request.setAttribute("totalPageCount", totalPageCount);
request.setAttribute("list", list);
request.setAttribute("totalRow", totalRow);
}
글을 읽는 메소드
@Override
public void getDetail(HttpServletRequest request) {
//자세히 보여줄 글번호를 읽어온다.
int num=Integer.parseInt(request.getParameter("num"));
//조회수 올리기
cafedao.addViewCount(num);
/*
[ 검색 키워드에 관련된 처리 ]
-검색 키워드가 파라미터로 넘어올수도 있고 안넘어 올수도 있다.
*/
String keyword=request.getParameter("keyword");
String condition=request.getParameter("condition");
//만일 키워드가 넘어오지 않는다면
if(keyword==null){
//키워드와 검색 조건에 빈 문자열을 넣어준다.
//클라이언트 웹브라우저에 출력할때 "null" 을 출력되지 않게 하기 위해서
keyword="";
condition="";
}
//CafeDto 객체를 생성해서
CafeDto dto=new CafeDto();
//자세히 보여줄 글번호를 넣어준다.
dto.setNum(num);
//만일 검색 키워드가 넘어온다면
if(!keyword.equals("")){
//검색 조건이 무엇이냐에 따라 분기 하기
if(condition.equals("title_content")){//제목 + 내용 검색인 경우
//검색 키워드를 CafeDto 에 담아서 전달한다.
dto.setTitle(keyword);
dto.setContent(keyword);
}else if(condition.equals("title")){ //제목 검색인 경우
dto.setTitle(keyword);
}else if(condition.equals("writer")){ //작성자 검색인 경우
dto.setWriter(keyword);
} // 다른 검색 조건을 추가 하고 싶다면 아래에 else if() 를 계속 추가 하면 된다.
}
//글하나의 정보를 얻어온다.
CafeDto resultDto=cafedao.getData(dto);
//특수기호를 인코딩한 키워드를 미리 준비한다.
String encodedK=URLEncoder.encode(keyword);
//request scope 에 글 하나의 정보 담기
request.setAttribute("dto", resultDto);
request.setAttribute("condition", condition);
request.setAttribute("keyword", keyword);
request.setAttribute("encodedK", encodedK);
}
댓글을 저장할 테이블 sql문
-- 댓글을 저장할 테이블
CREATE TABLE board_cafe_comment(
num NUMBER PRIMARY KEY, --댓글의 글번호
writer VARCHAR2(100), --댓글 작성자의 아이디
content VARCHAR2(500), --댓글 내용
target_id VARCHAR2(100), --댓글의 대상자 아이디
ref_group NUMBER,
comment_group NUMBER,
deleted CHAR(3) DEFAULT 'no',
regdate DATE
);
위에 모든 기능들은 하나의 테이블에 들어가야한다.
그리고 원글의 글번호만 그 번호에 해당하는 댓글의 목록만을 가지고 와야한다.(999번의 글에서 다른 번호의 글의 댓글을 select해서는 안된다.)
대댓글은 하나의 댓글에서 파생된것이기 때문에 하나의 댓글에서 몰려있게끔 해주어야 한다.=>댓글의 그룹번호
그로인해 댓글은 그룹번호를 가지고 있어야 한다.
SQL문을 넣어줄 CafeCommentMapper.xml을 만들어준 후에
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cafeComment">
<!--
댓글에 프로필 이미지도 같이 출력하기 위해 users 테이블과 join 을 한다.
댓글도 paging 처리가 필요함으로 select 할때 startRowNum 과 endRowNum 이 있어야 한다.
-->
<select id="getList" parameterType="cafeCommentDto" resultType="cafeCommentDto">
SELECT *
FROM
(SELECT result1.*, ROWNUM AS rnum
FROM
(SELECT num, writer, content, target_id, ref_group,
comment_group, deleted, board_cafe_comment.regdate, profile
FROM board_cafe_comment
INNER JOIN users
ON board_cafe_comment.writer=users.id
WHERE ref_group=#{ref_group}
ORDER BY comment_group ASC, num ASC) result1)
WHERE rnum BETWEEN #{startRowNum} AND #{endRowNum}
</select>
<!-- 댓글은 실제로 삭제 하지 않고 deleted 칼럼에 저장된값을 no 에서 yes 로 수정하는 작업만한다. -->
<update id="delete" parameterType="int">
UPDATE board_cafe_comment
SET deleted='yes'
WHERE num=#{num}
</update>
<!-- 댓글을 저장할때 댓글의 글번호도 미리 CafeCommentDto 객체에 담아 와야 한다. -->
<insert id="insert" parameterType="cafeCommentDto">
INSERT INTO board_cafe_comment
(num, writer, content, target_id, ref_group, comment_group, regdate)
VALUES(#{num}, #{writer}, #{content}, #{target_id}, #{ref_group},
#{comment_group}, SYSDATE)
</insert>
<!-- 저장 예정인 댓글의 글번호를 미리 얻어내기 -->
<select id="getSequence" resultType="int">
SELECT board_cafe_comment_seq.NEXTVAL
FROM DUAL
</select>
<update id="update" parameterType="cafeCommentDto">
UPDATE board_cafe_comment
SET content=#{content}
WHERE num=#{num}
</update>
<select id="getData" parameterType="int" resultType="cafeCommentDto">
SELECT num,writer,content,ref_group,comment_group,deleted,regdate
FROM board_cafe_comment
WHERE num=#{num}
</select>
<select id="getCount" parameterType="int" resultType="int">
SELECT NVL(MAX(ROWNUM), 0)
FROM board_cafe_comment
WHERE ref_group=#{ref_group}
</select>
</mapper>
Configuration.xml에 CafeCommentDto와 Mapper.xml을 등록해준다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 자주 사용하는 type 의 별칭을 등록 해 놓고 Mapper xml 에서 사용할수 있다. -->
<typeAliases>
<typeAlias type="com.gura.spring04.users.dto.UsersDto" alias="usersDto"/>
<typeAlias type="com.gura.spring04.file.dto.FileDto" alias="fileDto"/>
<typeAlias type="com.gura.spring04.cafe.dto.CafeDto" alias="cafeDto"/>
<typeAlias type="com.gura.spring04.cafe.dto.CafeCommentDto" alias="cafeCommentDto"/>
</typeAliases>
<!-- sql 문을 작성한 Mapper xml 문서가 어디에 있는지 목록을 작성해야 한다. -->
<mappers>
<mapper resource="mapper/UsersMapper.xml"/>
<mapper resource="mapper/FileMapper.xml"/>
<mapper resource="mapper/CafeMapper.xml"/>
<mapper resource="mapper/CafeCommentMapper.xml/>"
</mappers>
</configuration>
users Table에 있는 profile을 가져오기 위해 join을 해야한다.
coment_group를 오름차순(ASP)로 꺼내면서 num 을 기준으로 오름차순으로 두번 정렬해준다.이럴 경우에는 무엇을 먼저 정렬할 것인지 결정하는 것도 중요하다.
댓글하나를 insert할 때 댓글하나의 글번호가 comment글번호로 부여할때가 있어야한다.
원글의 글 번호가 10번일 때👉
SELECT*
FROM board_cafe_comment
WHERE ref_group=10
//새로운 댓글 저장 요청처리
@RequestMapping("/cafe/comment_insert")
public String commentInsert(HttpServletRequest request, int ref_group) {
//새로운 댓글을 저장하는 로직을 수행한다.
service.saveComment(request);
//ref_group은 원글의 글번호이기 때문에 원글 자세히 보기로 리다이렉트 이동된다.
return "redirect:/cafe/detail?num="+ref_group;
}
댓글 페이징 처리에 관련된 로직 CafeServiceImpl Class의 getDetail()메서드의 추가
/*
[ 댓글 페이징 처리에 관련된 로직 ]
*/
//한 페이지에 몇개씩 표시할 것인지
final int PAGE_ROW_COUNT=10;
//detail.jsp 페이지에서는 항상 1페이지의 댓글 내용만 출력한다.
int pageNum=1;
//보여줄 페이지의 시작 ROWNUM
int startRowNum=1+(pageNum-1)*PAGE_ROW_COUNT;
//보여줄 페이지의 끝 ROWNUM
int endRowNum=pageNum*PAGE_ROW_COUNT;
//원글의 글번호를 이용해서 해당글에 달린 댓글 목록을 얻어온다.
CafeCommentDto commentDto=new CafeCommentDto();
commentDto.setRef_group(num);
//1페이지에 해당하는 startRowNum 과 endRowNum 을 dto 에 담아서
commentDto.setStartRowNum(startRowNum);
commentDto.setEndRowNum(endRowNum);
//1페이지에 해당하는 댓글 목록만 select 되도록 한다.
List<CafeCommentDto> commentList=cafeCommentDao.getList(commentDto);
PAGE_DISPLAY_COUNT가 없는 이유는 밑으로 마우스 스크롤을 내릴 때 로딩이 되도록 구현할 것이기에 구현하지 않았다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>/views/cafe/detail.jsp</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
<style>
.content{
border: 1px dotted gray;
}
/* 댓글 프로필 이미지를 작은 원형으로 만든다. */
.profile-image{
width: 50px;
height: 50px;
border: 1px solid #cecece;
border-radius: 50%;
}
/* ul 요소의 기본 스타일 제거 */
.comments ul{
padding: 0;
margin: 0;
list-style-type: none;
}
.comments dt{
margin-top: 5px;
}
.comments dd{
margin-left: 50px;
}
.comment-form textarea, .comment-form button{
float: left;
}
.comments li{
clear: left;
}
.comments ul li{
border-top: 1px solid #888;
}
.comment-form textarea{
width: 84%;
height: 100px;
}
.comment-form button{
width: 14%;
height: 100px;
}
/* 댓글에 댓글을 다는 폼과 수정폼은 일단 숨긴다. */
.comments .comment-form{
display: none;
}
/* .reply_icon 을 li 요소를 기준으로 배치 하기 */
.comments li{
position: relative;
}
.comments .reply-icon{
position: absolute;
top: 1em;
left: 1em;
color: red;
}
pre {
display: block;
padding: 9.5px;
margin: 0 0 10px;
font-size: 13px;
line-height: 1.42857143;
color: #333333;
word-break: break-all;
word-wrap: break-word;
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 4px;
}
.loader{
/* 로딩 이미지를 가운데 정렬하기 위해 */
text-align: center;
/* 일단 숨겨 놓기 */
display: none;
}
.loader svg{
animation: rotateAni 1s ease-out infinite;
}
@keyframes rotateAni{
0%{
transform: rotate(0deg);
}
100%{
transform: rotate(360deg);
}
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" />
</head>
</head>
<body>
<div class="container">
<%-- 만일 이전글(더 옛날글)의 글번호가 0 가 아니라면(이전글이 존재 한다면) --%>
<c:if test="${dto.prevNum ne 0}">
<a href="detail?num=${dto.prevNum }&condition=${condition}&keyword=${encodedK}">이전글</a>
</c:if>
<%-- 만일 다음글(더 최신글)의 글번호가 0 가 아니라면(다음글이 존재 한다면) --%>
<c:if test="${dto.nextNum ne 0 }">
<a href="detail?num=${dto.nextNum }&condition=${condition}&keyword=${encodedK}">다음글</a>
</c:if>
<%-- 만일 검색 키워드가 있다면 --%>
<c:if test="${not empty keyword }">
<p>
<strong>${condition }</strong> 조건
<strong>${keyword }</strong> 검색어로 검색된 내용 자세히 보기
</p>
</c:if>
<h3>글 상세 보기</h3>
<table class="table table-bordered">
<tr>
<th>글번호</th>
<td>${dto.num }</td>
</tr>
<tr>
<th>작성자</th>
<td>${dto.writer }</td>
</tr>
<tr>
<th>제목</th>
<td>${dto.title }</td>
</tr>
<tr>
<th>조회수</th>
<td>${dto.viewCount }</td>
</tr>
<tr>
<th>작성일</th>
<td>${dto.regdate }</td>
</tr>
<tr>
<td colspan="2">
<div>${dto.content }</div>
</td>
</tr>
</table>
<!-- 로그인된 아이디와 글의 작성자가 같으면 수정, 삭제 링크를 제공한다. -->
<c:if test="${sessionScope.id eq dto.writer }">
<a href="updateform?num=${dto.num }">수정</a>
<a href="javascript:" onclick="deleteConfirm()">삭제</a>
<script>
function deleteConfirm(){
const isDelete=confirm("이 글을 삭제 하겠습니까?");
if(isDelete){
location.href="delete?num=${dto.num}";
}
}
</script>
</c:if>
<h4>댓글을 입력해 주세요</h4>
<!-- 원글에 댓글을 작성할 폼 -->
<form class="comment-form insert-form" action="comment_insert" method="post">
<!-- 원글의 글번호가 댓글의 ref_group 번호가 된다. -->
<input type="hidden" name="ref_group" value="${dto.num }"/>
<!-- 원글의 작성자가 댓글의 대상자가 된다. -->
<input type="hidden" name="target_id" value="${dto.writer }"/>
<textarea name="content">${empty id ? '댓글 작성을 위해 로그인이 필요 합니다.' : '' }</textarea>
<button type="submit">등록</button>
</form>
<!-- 댓글 목록 -->
<!-- 댓글 목록 -->
<div class="comments">
<ul>
<c:forEach var="tmp" items="${commentList }">
<c:choose>
<c:when test="${tmp.deleted eq 'yes' }">
<li>삭제된 댓글 입니다.</li>
</c:when>
<c:otherwise>
<c:if test="${tmp.num eq tmp.comment_group }">
<li id="reli${tmp.num }">
</c:if>
<c:if test="${tmp.num ne tmp.comment_group }">
<li id="reli${tmp.num }" style="padding-left:50px;">
<svg class="reply-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-return-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"/>
</svg>
</c:if>
<dl>
<dt>
<c:if test="${ empty tmp.profile }">
<svg class="profile-image" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-circle" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
</svg>
</c:if>
<c:if test="${not empty tmp.profile }">
<img class="profile-image" src="${pageContext.request.contextPath}${tmp.profile }"/>
</c:if>
<span>${tmp.writer }</span>
<c:if test="${tmp.num ne tmp.comment_group }">
@<i>${tmp.target_id }</i>
</c:if>
<span>${tmp.regdate }</span>
<a data-num="${tmp.num }" href="javascript:" class="reply-link">답글</a>
<c:if test="${ (id ne null) and (tmp.writer eq id) }">
<a data-num="${tmp.num }" class="update-link" href="javascript:">수정</a>
<a data-num="${tmp.num }" class="delete-link" href="javascript:">삭제</a>
</c:if>
</dt>
<dd>
<pre id="pre${tmp.num }">${tmp.content }</pre>
</dd>
</dl>
<form id="reForm${tmp.num }" class="animate__animated comment-form re-insert-form" action="comment_insert" method="post">
<input type="hidden" name="ref_group" value="${dto.num }"/>
<input type="hidden" name="target_id" value="${tmp.writer }"/>
<input type="hidden" name="comment_group" value="${tmp.comment_group }"/>
<textarea name="content"></textarea>
<button type="submit">등록</button>
</form>
<c:if test="${tmp.writer eq id }">
<form id="updateForm${tmp.num }" class="comment-form update-form" action="comment_update" method="post">
<input type="hidden" name="num" value="${tmp.num }" />
<textarea name="content">${tmp.content }</textarea>
<button type="submit">수정</button>
</form>
</c:if>
</li>
</c:otherwise>
</c:choose>
</c:forEach>
</ul>
</div>
<div class="loader">
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg>
</div>
</div>
<script src="${pageContext.request.contextPath}/resources/js/gura_util.js"></script>
<script>
//클라이언트가 로그인 했는지 여부
let isLogin=${ not empty id };
document.querySelector(".insert-form")
.addEventListener("submit", function(e){
//만일 로그인 하지 않았으면
if(!isLogin){
//폼 전송을 막고
e.preventDefault();
//로그인 폼으로 이동 시킨다.
location.href=
"${pageContext.request.contextPath}/users/loginform?url=${pageContext.request.contextPath}/cafe/detail?num=${dto.num}";
}
});
/*
detail
페이지 로딩 시점에 만들어진 1 페이지에 해당하는
댓글에 이벤트 리스너 등록 하기
*/
addUpdateFormListener(".update-form");
addUpdateListener(".update-link");
addDeleteListener(".delete-link");
addReplyListener(".reply-link");
//댓글의 현재 페이지 번호를 관리할 변수를 만들고 초기값 1 대입하기
let currentPage=1;
//마지막 페이지는 totalPageCount 이다.
let lastPage=${totalPageCount};
//추가로 댓글을 요청하고 그 작업이 끝났는지 여부를 관리할 변수
let isLoading=false; //현재 로딩중인지 여부
/*
window.scrollY => 위쪽으로 스크롤된 길이
window.innerHeight => 웹브라우저의 창의 높이
document.body.offsetHeight => body 의 높이 (문서객체가 차지하는 높이)
*/
window.addEventListener("scroll", function(){
//바닥 까지 스크롤 했는지 여부
const isBottom =
window.innerHeight + window.scrollY >= document.body.offsetHeight;
//현재 페이지가 마지막 페이지인지 여부 알아내기
let isLast = currentPage == lastPage;
//현재 바닥까지 스크롤 했고 로딩중이 아니고 현재 페이지가 마지막이 아니라면
if(isBottom && !isLoading && !isLast){
//로딩바 띄우기
document.querySelector(".loader").style.display="block";
//로딩 작업중이라고 표시
isLoading=true;
//현재 댓글 페이지를 1 증가 시키고
currentPage++;
/*
해당 페이지의 내용을 ajax 요청을 통해서 받아온다.
"pageNum=xxx&num=xxx" 형식으로 GET 방식 파라미터를 전달한다.
*/
ajaxPromise("ajax_comment_list","get",
"pageNum="+currentPage+"&num=${dto.num}")
.then(function(response){
//json 이 아닌 html 문자열을 응답받았기 때문에 return response.text() 해준다.
return response.text();
})
.then(function(data){
//data 는 html 형식의 문자열이다.
console.log(data);
// beforebegin | afterbegin | beforeend | afterend
document.querySelector(".comments ul")
.insertAdjacentHTML("beforeend", data);
//로딩이 끝났다고 표시한다.
isLoading=false;
//새로 추가된 댓글 li 요소 안에 있는 a 요소를 찾아서 이벤트 리스너 등록 하기
addUpdateListener(".page-"+currentPage+" .update-link");
addDeleteListener(".page-"+currentPage+" .delete-link");
addReplyListener(".page-"+currentPage+" .reply-link");
//새로 추가된 댓글 li 요소 안에 있는 댓글 수정폼에 이벤트 리스너 등록하기
addUpdateFormListener(".page-"+currentPage+" .update-form");
//로딩바 숨기기
document.querySelector(".loader").style.display="none";
});
}
});
//인자로 전달되는 선택자를 이용해서 이벤트 리스너를 등록하는 함수
function addUpdateListener(sel){
//댓글 수정 링크의 참조값을 배열에 담아오기
// sel 은 ".page-xxx .update-link" 형식의 내용이다
let updateLinks=document.querySelectorAll(sel);
for(let i=0; i<updateLinks.length; i++){
updateLinks[i].addEventListener("click", function(){
//click 이벤트가 일어난 바로 그 요소의 data-num 속성의 value 값을 읽어온다.
const num=this.getAttribute("data-num"); //댓글의 글번호
document.querySelector("#updateForm"+num).style.display="block";
});
}
}
function addDeleteListener(sel){
//댓글 삭제 링크의 참조값을 배열에 담아오기
let deleteLinks=document.querySelectorAll(sel);
for(let i=0; i<deleteLinks.length; i++){
deleteLinks[i].addEventListener("click", function(){
//click 이벤트가 일어난 바로 그 요소의 data-num 속성의 value 값을 읽어온다.
const num=this.getAttribute("data-num"); //댓글의 글번호
const isDelete=confirm("댓글을 삭제 하시겠습니까?");
if(isDelete){
// gura_util.js 에 있는 함수들 이용해서 ajax 요청
ajaxPromise("comment_delete", "post", "num="+num)
.then(function(response){
return response.json();
})
.then(function(data){
//만일 삭제 성공이면
if(data.isSuccess){
//댓글이 있는 곳에 삭제된 댓글입니다를 출력해 준다.
document.querySelector("#reli"+num).innerText="삭제된 댓글입니다.";
}
});
}
});
}
}
function addReplyListener(sel){
//댓글 링크의 참조값을 배열에 담아오기
let replyLinks=document.querySelectorAll(sel);
//반복문 돌면서 모든 링크에 이벤트 리스너 함수 등록하기
for(let i=0; i<replyLinks.length; i++){
replyLinks[i].addEventListener("click", function(){
if(!isLogin){
const isMove=confirm("로그인이 필요 합니다. 로그인 페이지로 이동 하시겠습니까?");
if(isMove){
location.href=
"${pageContext.request.contextPath}/users/loginform?url=${pageContext.request.contextPath}/cafe/detail?num=${dto.num}";
}
return;
}
//click 이벤트가 일어난 바로 그 요소의 data-num 속성의 value 값을 읽어온다.
const num=this.getAttribute("data-num"); //댓글의 글번호
const form=document.querySelector("#reForm"+num);
//현재 문자열을 읽어온다 ( "답글" or "취소" )
let current = this.innerText;
if(current == "답글"){
//번호를 이용해서 댓글의 댓글폼을 선택해서 보이게 한다.
form.style.display="block";
form.classList.add("animate__flash");
this.innerText="취소";
form.addEventListener("animationend", function(){
form.classList.remove("animate__flash");
}, {once:true});
}else if(current == "취소"){
form.classList.add("animate__fadeOut");
this.innerText="답글";
form.addEventListener("animationend", function(){
form.classList.remove("animate__fadeOut");
form.style.display="none";
},{once:true});
}
});
}
}
function addUpdateFormListener(sel){
//댓글 수정 폼의 참조값을 배열에 담아오기
let updateForms=document.querySelectorAll(sel);
for(let i=0; i<updateForms.length; i++){
//폼에 submit 이벤트가 일어 났을때 호출되는 함수 등록
updateForms[i].addEventListener("submit", function(e){
//submit 이벤트가 일어난 form 의 참조값을 form 이라는 변수에 담기
const form=this;
//폼 제출을 막은 다음
e.preventDefault();
//이벤트가 일어난 폼을 ajax 전송하도록 한다.
ajaxFormPromise(form)
.then(function(response){
return response.json();
})
.then(function(data){
if(data.isSuccess){
/*
document.querySelector() 는 html 문서 전체에서 특정 요소의
참조값을 찾는 기능
특정문서의 참조값.querySelector() 는 해당 문서 객체의 자손 요소 중에서
특정 요소의 참조값을 찾는 기능
*/
const num=form.querySelector("input[name=num]").value;
const content=form.querySelector("textarea[name=content]").value;
//수정폼에 입력한 value 값을 pre 요소에도 출력하기
document.querySelector("#pre"+num).innerText=content;
form.style.display="none";
}
});
});
}
}
</script>
</body>
</html>
댓글 삭제 요청처리 메서드를 CafeController에 추가해준다.
//댓글 삭제 요청 처리
@RequestMapping("/cafe/comment_delete")
@ResponseBody
public Map<String, Object> commentDelete(HttpServletRequest request){
service.deleteComment(request);
Map<String, Object> map = new HashMap<String, Object>();
map.put("isSuccess", true);
//{"isSuccess":true}형식의 JSON문자열이 응답되도록 한다.
return map;
}
다음으로 CafeServiceImpl에서 삭제 메서드를 구현해 준다.
@Override
public void deleteComment(HttpServletRequest request) {
int num = Integer.parseInt(request.getParameter("num"));
//삭제할 댓글 정보를 읽어와서
CafeCommentDto dto = cafeCommentDao.getData(num);
String id=(String)request.getSession().getAttribute("id");
//글 작성자와 로그인된 아이디와 일치하지 않으면
if(!dto.getWriter().contentEquals(id)) {
throw new NotDeleteException("응 앙대");
}
//dao를 이용해서 DB에서 삭제하기
cafeCommentDao.delete(num);
}
댓글을 삭제할 경우 DB에서 yes를 반환하게 해두었다.
댓글의 수정 구현하기
updateForm은 이미 만들어져있다. 수정 버튼을 눌렀을 때 구현되도록 만들어져있는데.
수정 button을 click했을 때 수정 폼이 출력 되도록한다.
해당 번호의 수정 버튼을 누르면 해당 번호의 수정폼의 display가 none에서 block으로 바뀌며 출력이 된다.
수정 글을 작성하면
CafeCommentMapper의 update sql구문을 통해 수정이 되고.
<update id="update" parameterType="cafeCommentDto">
UPDATE board_cafe_comment
SET content=#{content}
WHERE num=#{num}
</update>
CafeController에 있는 수정 요청처리 메서드를 통해 저장이 된다.
//댓글 수정 요청처리(JSON을 응답하도록 한다)
@RequestMapping("/cafe/comment_update")
@ResponseBody
public Map<String, Object> commentUpdate(CafeCommentDto dto){
service.updateComment(dto);
Map<String, Object> map=new HashMap<String, Object>();
map.put("isSuccess", true);
//{"isSuccess":true} 형식의 JSON문자열이 응답되도록 한다.
return map;
}
'오답노트' 카테고리의 다른 글
게시판 하단 페이징처리에 대한 고찰 (0) | 2023.06.29 |
---|---|
Jdbc연결시 DB계정명 잘 입력해주기 (0) | 2023.06.29 |
Vue Computed 이해하기 (0) | 2023.06.26 |
SpringBoot 서버 정지시 Build cancelled while executing task 에러 메세지 (0) | 2023.06.25 |
Spring으로 방명록 만들기 (0) | 2023.06.20 |
댓글