본문 바로가기
오답노트

댓글 기능 게시판 만들기

by titlejjk 2023. 6. 28.

이번 게시판 만드는 목표는 댓글 기능 + 이전글/ 다음글 만들기 이다.

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;
	}

 

댓글