국비교육(22-23)

56일차(3)/Spring(19) : 게시판 댓글 기능 구현

서리/Seori 2022. 12. 27. 01:08

56일차(3)/Spring(19) : 게시판 댓글 기능 구현

 

 

table.sql에 추가

- Oracle DB에 Cafe Comment 관련 새 테이블, 시퀀스 추가

 

- ref_group은 원래 글의 글번호

 

 

- comment_group : 한 덩어리로 몰려있는 댓글 (댓글의 댓글 묶음)

 묶음 맨 위의 lead되는 코멘트의 번호를 그룹 번호로 부여한다.

ex) comment1,2,3의 그룹번호는 1 / comment5,6,7의 그룹번호는 5

 

- deleted는 댓글을 완전히 삭제하는 것이 아니고, 그냥 삭제된 댓글은 보이지 않게 하려고 한다.

- deleted에서 눈에 보이는 조건을 yes로 바꾸어서 보이지 않게 하는 것.

- 완전히 삭제하면 댓글구조가 망가질 수 있다.

 

 

- 댓글하나에 정보를 담을 dto를 만들 예정

 

CafeCommentDto 생성

package com.sy.spring04.cafe.dto;

public class CafeCommentDto {
	private int num;
	private String writer;
	private String content;
	private String target_id;
	private int ref_group;
	private int comment_group;
	private String deleted;
	private String regdate;
	private String profile;
	private int startRowNum;
	private int endRowNum;
	
	public CafeCommentDto() {};

	public CafeCommentDto(int num, String writer, String content, String target_id, int ref_group, int comment_group,
			String deleted, String regdate, String profile, int startRowNum, int endRowNum) {
		super();
		this.num = num;
		this.writer = writer;
		this.content = content;
		this.target_id = target_id;
		this.ref_group = ref_group;
		this.comment_group = comment_group;
		this.deleted = deleted;
		this.regdate = regdate;
		this.profile = profile;
		this.startRowNum = startRowNum;
		this.endRowNum = endRowNum;
	}

	public int getNum() {
		return num;
	}

	public void setNum(int num) {
		this.num = num;
	}

	public String getWriter() {
		return writer;
	}

	public void setWriter(String writer) {
		this.writer = writer;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}

	public String getTarget_id() {
		return target_id;
	}

	public void setTarget_id(String target_id) {
		this.target_id = target_id;
	}

	public int getRef_group() {
		return ref_group;
	}

	public void setRef_group(int ref_group) {
		this.ref_group = ref_group;
	}

	public int getComment_group() {
		return comment_group;
	}

	public void setComment_group(int comment_group) {
		this.comment_group = comment_group;
	}

	public String getDeleted() {
		return deleted;
	}

	public void setDeleted(String deleted) {
		this.deleted = deleted;
	}

	public String getRegdate() {
		return regdate;
	}

	public void setRegdate(String regdate) {
		this.regdate = regdate;
	}

	public String getProfile() {
		return profile;
	}

	public void setProfile(String profile) {
		this.profile = profile;
	}

	public int getStartRowNum() {
		return startRowNum;
	}

	public void setStartRowNum(int startRowNum) {
		this.startRowNum = startRowNum;
	}

	public int getEndRowNum() {
		return endRowNum;
	}

	public void setEndRowNum(int endRowNum) {
		this.endRowNum = endRowNum;
	};		
	 
}

 

CafeCommentDao 인터페이스

package com.sy.spring04.cafe.dao;

import java.util.List;

import com.sy.spring04.cafe.dto.CafeCommentDto;

public interface CafeCommentDao {
	//댓글 목록 얻어오기
	public List<CafeCommentDto> getList(CafeCommentDto dto);
	//댓글 삭제
	public void delete(int num);
	//댓글 추가
	public void insert(CafeCommentDto dto);
	//추가할 댓글의 글번호를 리턴하는 메소드
	public int getSequence();
	//댓글 수정
	public void update(CafeCommentDto dto);
	//댓글 하나의 정보를 리턴하는 메소드
	public CafeCommentDto getData(int num);
	//댓글의 갯수를 리턴하는 메소드
	public int getCount(int ref_group);
}

 

CafeCommentDaoImpl 클래스

package com.sy.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.sy.spring04.cafe.dto.CafeCommentDto;

@Repository
public class CafeCommentDaoImpl implements CafeCommentDao{

	@Autowired
	private SqlSession session;
	
	   @Override
	   public List<CafeCommentDto> getList(CafeCommentDto dto) {
	      
	      return session.selectList("cafeComment.getList", dto);
	   }

	   @Override
	   public void delete(int num) {
	      session.update("cafeComment.delete", num);
	   }

	   @Override
	   public void insert(CafeCommentDto dto) {
	      session.insert("cafeComment.insert", dto);
	   }

	   //저장될 예정인 댓글의 글번호를 얻어내서 리턴해주는 메소드
	   @Override
	   public int getSequence() {
	      
	      return session.selectOne("cafeComment.getSequence");
	   }

	   @Override
	   public void update(CafeCommentDto dto) {
	      session.update("cafeComment.update", dto);
	   }

	   @Override
	   public CafeCommentDto getData(int num) {
	      
	      return session.selectOne("cafeComment.getData", num);
	   }

	   @Override
	   public int getCount(int ref_group) {
	      
	      return session.selectOne("cafeComment.getCount", ref_group);
	   }
}

 

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>

 

- 번호를 이용한 삭제 기능은 deleted 칼럼을 'yes'로 바꿔주는 것으로 대체한다.

 

 

- 댓글을 저장하기 전에 미리 글번호를 얻어와서 저장해야 할 때가 있다.

- 댓글의 글번호와 comment 그룹번호를 똑같이 넣어서 저장해야 한다.

 

- 번호가 무엇인지를 알아야 같이 부여할 수 있기 때문에!!

- 다 넣어서 준비해 와야 된다.

 

 

- getSequence를 사용해서 얻어낸 글번호가 여기에 들어간다.

- 원글의 댓글인 경우에는 num=comment_group 번호이고, 원글의 댓글이 아닌 경우에는 두 값이 다르다.

 

- 이 번호가 같은가 다른가에 따라서 원글의 댓글인지 대댓글인지가 구분된다.

- 대댓글은 들여쓰기해서 보여줄 것이기 때문에! 원글의 댓글과는 구분해주어야 한다.

 

- 숫자이므로 resultType은 int이다.

 

- getData : 패러미터로 숫자를 가져와서 특정 값을 가져오도록 select하기

- getCount : 페이징 처리를 위해 전체 글 수를 읽어와서, 패러미터 값으로 가져온 숫자로 추려내기 (ref_group을사용)

 

 

- join 일어난다. 댓글에서 프로필 이미지를 가져오기 때문에...! 이미지 경로를 출력하려고.

- 댓글에 프로필사진을 같이 출력하려면 댓글 작성자(writer)와 유저 테이블의 아이디(id)를 join해서 가져와야 한다.

 

 

- 정렬된, 페이지 처리된 댓글 목록을 얻어오려면

 원글의 글번호를 넣어서 select

→ 코멘트 그룹, 번호에 따라 두번 정렬

→ 필요한 row만 where절을 사용해서 select하는 과정이 필요하다.

1) select, 2) rowNum부여, 3) 원하는 row만 남기기

 

- dto는 3가지 값만 남긴다.

 

- configuration에 새로 추가한 CafeCommentMapper, alias 추가해주기

 


 

daoImpl - 메소드

 

- getSequence : 글번호를 미리 가지고 가서 리턴해주는 메소드

 

- 나머지는 인자 값을 가져가서 리턴해주는 메소드들이다.

 

 

- CommentService를 따로 만들지는 않고, cafeService에 합칠 예정

 

CafeService interface

package com.sy.spring04.cafe.service;

import javax.servlet.http.HttpServletRequest;

import com.sy.spring04.cafe.dto.CafeCommentDto;
import com.sy.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); //글 수정하기 위해 정보 불러오는 기능

	public void saveComment(HttpServletRequest request); //댓글 저장
	public void deleteComment(HttpServletRequest request); //댓글 삭제
	public void updateComment(CafeCommentDto dto); //댓글 수정
	public void moreCommentList(HttpServletRequest request); //댓글 더보기 기능
}

 

- 추상메소드 추가

- 해당 Service를 구현한 CafeServiceImpl에도 메소드 추가

 

- @Autowired, CommentDao 추가해주기

- 이렇게 한 클래스에서 여러 개의 DAO에 의존할 수도 있다.

 

 

- saveComment 메소드

@Override
	public void saveComment(HttpServletRequest request) {
		//폼 전송되는 파라미터 추출 
	    int ref_group=Integer.parseInt(request.getParameter("ref_group")); //원글의 글번호
	    String target_id=request.getParameter("target_id"); //댓글 대상자의 아이디
	    String content=request.getParameter("content"); //댓글의 내용
	    /*
	     *  원글의 댓글은 comment_group 번호가 전송이 안되고
	     *  댓글의 댓글은 comment_group 번호가 전송이 된다.
	     *  따라서 null 여부를 조사하면 원글의 댓글인지 댓글의 댓글인지 판단할수 있다. 
	     */
	    String comment_group=request.getParameter("comment_group");

	    //댓글 작성자는 session 영역에서 얻어내기
	    String writer=(String)request.getSession().getAttribute("id");
	    //댓글의 시퀀스 번호 미리 얻어내기
	    int seq=cafeCommentDao.getSequence();
	    //저장할 댓글의 정보를 dto 에 담기
	    CafeCommentDto dto=new CafeCommentDto();
	    dto.setNum(seq);
	    dto.setWriter(writer);
	    dto.setTarget_id(target_id);
	    dto.setContent(content);
	    dto.setRef_group(ref_group);
	    //원글의 댓글인경우
	    if(comment_group == null){
	       //댓글의 글번호를 comment_group 번호로 사용한다.
	       dto.setComment_group(seq);
	    }else{
	       //전송된 comment_group 번호를 숫자로 바꾸서 dto 에 넣어준다. 
	       dto.setComment_group(Integer.parseInt(comment_group));
	    }
	    //댓글 정보를 DB 에 저장하기
	    cafeCommentDao.insert(dto);
	}

- 3가지 값의 정보는 확실히 넘어오고,

아래의 댓글 값(comment_group)은 경우에 따라 넘어올 수도 있고 아닐 수도 있다.

 

- session에서 id값을 구해 writer 에 넣어준다.

 

- 댓글의 글 번호를 미리 알아낸 이유!! 여기에서 사용하기 위해.

- 원글의 댓글은 댓글번호와 comment_group 번호가 같다. 댓글 자기의 글번호가 자기자신의 그룹번호도 된다!

 

- comment group 번호가 넘어오므로 이 번호를 사용해서 DB에 전달한다.

 

delete

@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().equals(id)) {
	       throw new NotDeleteException("남의 댓글 지우면 혼난당!");
	    }
	    
	    cafeCommentDao.delete(num);
		
	}

 

- int값으로 댓글 정보를 읽어와서 id와 비교하고, 본인이 쓴 댓글이 아니면 예외를 발생시킨다.

- dao delete 메소드를 사용해서 삭제작업(deleted)를 yes로 바꾸기

 

 

글 자세히보기(getDetail)에서 댓글보기 사용

- getDetail에 내용추가

@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);

     /*
     * [ 댓글 페이징 처리에 관련된 로직 ]
     */
     //한 페이지에 몇개씩 표시할 것인지
     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);

     //원글의 글번호를 이용해서 댓글 전체의 갯수를 얻어낸다.
     int totalRow=cafeCommentDao.getCount(num);
     //댓글 전체 페이지의 갯수
     int totalPageCount=(int)Math.ceil(totalRow/(double)PAGE_ROW_COUNT);


    //request scope에 글 하나의 정보 담기
    request.setAttribute("dto", resultDto);
    request.setAttribute("condition", condition);
    request.setAttribute("keyword", keyword);
    request.setAttribute("encodedK", encodedK);
    request.setAttribute("tSotalRow", totalRow);
    request.setAttribute("commentList", commentList);
    request.setAttribute("totalPageCount", totalPageCount);
}

 

- 페이징처리, dto에 원글의 글번호를 담고, dto를 사용해서 댓글을 select하는 것이다.

 

- totalRow를 사용해서 댓글이 전체 몇 페이지까지 있는지 알아내고, view page에 전달한다.

 

 

- request Scope에 3개의 내용을 추가로 담아준다.(totalRow, commentList, totalPageCount)

 

 


 

- 댓글 더보기 기능은 ajax로 구현할 예정

 

Service메소드- moreCommentList

@Override
public void moreCommentList(HttpServletRequest request) {
      //로그인된 아이디
      String id=(String)request.getSession().getAttribute("id");
      //ajax 요청 파라미터로 넘어오는 댓글의 페이지 번호를 읽어낸다
      int pageNum=Integer.parseInt(request.getParameter("pageNum"));
      //ajax 요청 파라미터로 넘어오는 원글의 글 번호를 읽어낸다
      int num=Integer.parseInt(request.getParameter("num"));
      /*
         [ 댓글 페이징 처리에 관련된 로직 ]
      */
      //한 페이지에 몇개씩 표시할 것인지
      final int PAGE_ROW_COUNT=10;

      //보여줄 페이지의 시작 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);

      //pageNum에 해당하는 댓글 목록만 select 되도록 한다. 
      List<CafeCommentDto> commentList=cafeCommentDao.getList(commentDto);
      //원글의 글번호를 이용해서 댓글 전체의 갯수를 얻어낸다.
      int totalRow=cafeCommentDao.getCount(num);
      //댓글 전체 페이지의 갯수
      int totalPageCount=(int)Math.ceil(totalRow/(double)PAGE_ROW_COUNT);

      //view page 에 필요한 값 request 에 담아주기
      request.setAttribute("commentList", commentList);
      request.setAttribute("num", num); //원글의 글번호
      request.setAttribute("pageNum", pageNum); //댓글의 페이지 번호			
}

- 댓글 리스트를 리턴하는 메소드

- dto를 이용해서 댓글목록 얻어오기

- 댓글리스트, 원글의 글번호, 댓글의 페이지 번호를 request 영역에 넣어주기

 

 

detail.jsp 페이지 내용 추가

<%@ 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>
</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>
         <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>
      <!-- 댓글 목록 -->
      <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>
   
      <!-- 원글에 댓글을 작성할 폼 -->
      <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>
   
   <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.do", "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>

 

- ajax사용을 위해 sy_util.js 로딩해주기

- ajax로직 추가

- 댓글 작성 폼 추가

- style 요소 추가

 


 

CafeController에 댓글 관련 메소드 추가

//새로운 댓글 저장 요청 처리
@RequestMapping("/cafe/comment_insert")
public String commentInsert(HttpServletRequest request, int ref_group) {

  service.saveComment(request);

  return "redirect:/cafe/detail?num="+ref_group;
}
//댓글 더보기 요청 처리
@RequestMapping("/cafe/ajax_comment_list")
public String commentList(HttpServletRequest request) {

    //테스트를 위해 시간 지연시키기
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    service.moreCommentList(request);

    return "cafe/ajax_comment_list";
}
//댓글 삭제 요청 처리
@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;
}
//댓글 수정 요청처리 (JSON 을 응답하도록 한다)
@RequestMapping("/cafe/comment_update")
@ResponseBody
public Map<String, Object> commentUpdate(CafeCommentDto dto, HttpServletRequest request){
  service.updateComment(dto);
  Map<String, Object> map=new HashMap<String, Object>();
  map.put("isSuccess", true);
  // {"isSuccess":true} 형식의 JSON 문자열이 응답되도록 한다. 
  return map;
}

 

- 컨트롤러에 추가한 ajax_comment_list 를 interceptor에서 제외해 주어야 한다.(/cafe/detail 페이지도)

- 이렇게 하면 로그인하지 않아도 댓글목록은 볼 수 있게 된다.

 

- 댓글이 이렇게 보인다.

 

 

뷰 페이지 추가

ajax_comment_list.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<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 }" class="page-${pageNum }">
         </c:if>
         <c:if test="${tmp.num ne tmp.comment_group }">
            <li id="reli${tmp.num }" class="page-${pageNum }" 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="${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>

 

- ajax 요청을 html형식으로 작성할 예정

- react나 view를 사용하지 않으면 좀 복잡해진다...

- ajax 요청을 통해 댓글이 10개가 넘으면 스크롤하면 불러와지도록 만들었다.

 

 

- 3초 걸리도록 지연시켜보면 이렇게 나온다. 3초간 화살표가 돌아가며 로딩한 후에 다음 댓글이 노출된다.

- 한번에 10개씩 불러오게 했다.

 

 


 

- 댓글 기능 구현 코드리뷰

 

페이지 소스보기

 

- 댓글이 없는 글의경우, comment의 ul 안쪽이 비어있다.

- 댓글 하나에 <li> 하나가 추가되는 것이라고 보면 된다.

 

- loader는 로딩하는 이미지(화살표0이다.

- 평소에는 보이지 않게 되어있지만, 필요시에 나타난다.

 

 

 

- 댓글 폼이 하나 준비되어 있다.

- ref_group과 target_id 의 value 값이 미리 준비되어 있다.

 여기에 내용content만 입력해서 등록버튼을 누르면 댓글이 등록된다.

 

 

- 댓글이 하나 추가되면 페이지가 전환되고, 다시 detail로 돌아온다.(리다이렉트 이동)

 

- 댓글이 하나 있는 상태이면 <li>가 하나 추가되는것을 볼 수 있다.

 

- 댓글은 dt, dd로 이루어져있다.

- 답글, 수정, 삭제 링크에 이벤트를 걸어서 javascript가 출력되도록 했다.

 

- dt : 개인정보(이미지, 닉네임, 등록날짜 등) 영역

- dd : 댓글 내용 content 영역

- 공백과 개행기호를 해석해주는 요소인 pre 요소를 사용해서 댓글을 출력하고 있다.

 

 

- 답글, 수정폼도 미리 다 만들어 놓았다. 단 숨겨놓았을 뿐이다.

- 답글/수정 링크를 클릭하면 보이게 하기/숨기기 만을 처리하고 있다.

 

- 대댓글 작성후에 소스보기로 보면, 대댓글에는 빨간색 화살표(reply-icon)를 출력, 왼쪽 패딩 추가 서식이 들어가있다.

- padding-left를 조건부로 부여한다.

 

- 대댓글에는 상대방의 아이디를 <i> 안에 출력해서 @id 로 표시하게 되어있다.

 


 

<c:if test="${tmp.num eq tmp.comment_group }">

- detail.jsp 에서 위 코드는 댓글 번호와 댓글 그룹번호가 같으면, 이라는 뜻!

 

- DB에서 댓글을 확인해보면 아래왁 같다.

 

- 표시한 부분은 2번 글(원글의 글번호)에 대한 댓글!

- 현 상태에서는 아직 순서가 없다. 무작위로 되어있다.

 

- 그러므로 위와같은 순서로 출력하면 안된다.

 

SELECT num, writer, content, target_id, ref_group, comment_group

FROM board_cafe_comment

WHERE ref_group=8; 

 

- 이렇게 하면 8번 글의 댓글을 모아놓은 것. 정렬되지 않은 상태.

 

 

- order by comment_group ASC, num ASC; 를 추가.

- comment group 에 대해서 오름차순 정렬을 하면 같은 그룹끼리 몰려있게 된다.

 

- 댓글번호와 comment group번호가 같은 댓글이 있고, 아닌 것도 있다.

- 같으면=원글의 댓글 / 다르면=댓글의 댓글

→ 댓글의 댓글인 경우 들여쓰기가 되도록 구분해서 처리하기

 

 

- 원글의 댓글은 제일 위에, 대댓글은 작성 순서대로 되도록 하려면 두번 정렬해야 한다.

- 대댓글끼리 몰려 있게 하고, 그 안에서도 번호순으로 정렬될 수 있도록 하기.

 

- 오름차순 정렬하면 최신댓글이 위에 있게 된다.

 

ex) 부서번호에 대해 오름차순 : 같은 부서끼리 몰려있게 하는 효과! (deptno ASC)

  그리고 몰려있는 상태에서 사원 이름에 따라 오름차순 정렬되도록 했다. (ename ASC)

 

 

- detail.jsp 코드

 

- 글번호와 댓글의 그룹번호가 같은지 다른지 판단해서,

 패딩여부 처리+reply 아이콘(화살표) 나오도록 처리

 

- 어떤 댓글은 id값만 있고, 어떤 댓글은 id와 padding, 이미지도 같이 출력되어 있다.

- 이것을 c:if로 처리한 것이다. 댓글번호와 comment 그룹번호가 같은지 비교하는 것으로!

 

- 정렬을 다 해서 가져온 다음에, 출력할때 들여쓰기를 할 것인가 말것인가를 정했다.

 

 

- 대댓글의 폼은 미리 준비해 두었다.

- 대댓글의 그룹번호를 들고 갈 준비를 전부 해두었다.

- 일부 정보는 이미 정해진 상태에서 댓글을 추가하는 것!

 

 

- 원글의 댓글은 번호가 없다. 아직 부여되지 않은 상태이다.

 댓글을 작성하면 그때 부여된다!

 

 

 

- service 메소드에서 보면 이 받아온 sequence 번호를 이렇게 사용하는 것을 볼 수 있다.

 

- 댓글이 여러개 있을때 대상자를 명시하는 이유: 없으면 누구에게 댓글을 작성하는 것인지 알 수 없으므로 필요하다!

- 들여쓰기는 한번만 하므로 id가 표시되지 않았으면 대상이 누구인지 명확히 알 수 없다.

 


 

- 로그아웃한 상태에서 글목록보기, 댓글보기는 가능하도록

(글쓰기, 댓글쓰기는 안된다.)

 

- 로그인하지 않았을 경우에는 댓글 작성이 막혀있다.

 javascript에서 로그인여부 값을 읽어와서 사용하고 있다.