ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 0926. 답글 처리(서버 + 클라이언트)
    Spring 2023. 9. 27. 17:42

    보호발행인데 왜 보이지..

     

    🌱
    서버
    - REST API 컨트롤러 만들어서 @RestController 사용
    - RequestBody : 모델객체 만들 때 application/json 파일 복원

    클라이언트
    - Ajax : $.ajax()
     promise 객체 / fetch()
     async/await

    📍서버 측 설계

    비버 열어서

    comment에 대한 foreign key 를 cno로 등록하기

    필수요소 : cno, content, writer

     

    tbl_reply 답글 테이블 생성

     

    tbl_reply.sql

    drop table if exists tbl_reply;
    
    create table tbl_reply (
    no integer auto_increment primary key,
    cno integer not null, -- comment의 no! foreign key
    content varchar(1000) not null,
    writer varchar(50) not null,
    reg_date datetime default now(),
    update_date datetime default now(),
    
    constraint fk_reply_comment foreign key(cno) references tbl_comment(no)
    );
    
    select * from tbl_reply;

    답글 입력해 보기 

    alias 작업 전에 *로 테스트

    select 
    	c.*, r.*
    from tbl_comment c left join tbl_reply r
    	on c.no = r.cno
    where bno = 36;

     

     

    순서 정렬

    오름차순은 그냥 쓰면 default니까 r.no

     

    이제 각각 별칭 붙여주기

    /* 실제 sql 문 */
    select 
    	c.no, c.bno, c.content c_content, c.writer c_writer, 
    	c.reg_date c_reg_date, c.update_date c_update_date,
    	r.no r_no, r.content r_content, r.writer r_writer, 
    	r.reg_date r_reg_date, r.update_date r_update_date
    from tbl_comment c left join tbl_reply r
    on c.no = r.cno
    where bno = 36;

    4,5 번 답글 때문에 헷갈릴까봐 어떻게 추가했었는지 같이 캡쳐 (답글 4가 2번 댓글이고 답글 5를 1번 댓글에 등록해서 그렇다)

    sql문 

    더보기
    drop table if exists tbl_reply;
    
    create table tbl_reply (
    no integer auto_increment primary key,
    cno integer not null, -- comment의 no! foreign key
    content varchar(1000) not null,
    writer varchar(50) not null,
    reg_date datetime default now(),
    update_date datetime default now(),
    
    constraint fk_reply_comment foreign key(cno) references tbl_comment(no)
    );
    
    insert into tbl_reply(cno, writer, content)
    values	(4, 'admin', '답글'),
    		(4, 'admin', '답글2'),
    		(4, 'quokka', '답글3');
    	
    insert into tbl_reply(cno, writer, content)
    values	(2, 'admin', '답글4'),
    		(1, 'admin', '답글5');
    /* 테스트 */
    select 
    	c.*, r.*
    from tbl_comment c left join tbl_reply r
    	on c.no = r.cno
    where bno = 36
    -- 댓글은 내림차순, 답글은 오름차순(순서대로 밑에 생기게) 
    order by c.no desc, r.no; 
    
    /* 실제 sql 문 */
    select 
    	c.no, c.bno, c.content c_content, c.writer c_writer, 
    	c.reg_date c_reg_date, c.update_date c_update_date,
    	r.no r_no, r.content r_content, r.writer r_writer, 
    	r.reg_date r_reg_date, r.update_date r_update_date
    from tbl_comment c left join tbl_reply r
    on c.no = r.cno
    where bno = 36;
    
    select * from tbl_reply;

     


     

     

    1. Domain 작업

     

    ReplyVO

    * CommentVO : ReplyVO = 1 : N 관계

    => 여러 개의 답글을 한 댓글이 담아야 하니, CommentVO에 property가 List<ReplyVO>로 추가되어야 한다.

    package org.galapagos.domain;
    
    import java.util.Date;
    
    import lombok.Data;
    
    @Data
    public class ReplyVO {
    	
    	private Long no;
    	private Long cno; // Comment의 no
    	
    	private String writer;
    	private String content;
    	
    	private Date regDate;
    	private Date updateDate;
    
    }

    CommentVO 

     

     

    2. XML 수정

    CommentMapper.xml 

     

    원본 

     

    1) collection의 자식태그로 정의하기

    * collection은 java에서 List이므로 JSON에서는 배열[]처리해야 한다. 

     

    2) 비버의 select 조인문 가져와서 넣기

    CommentMapper.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="org.galapagos.mapper.CommentMapper">
    
    	<resultMap id="CommentMap" type="CommentVO">
    		<id property="no" column="no" />
    		<result property="bno" column="bno" />
    		<result property="content" column="c_content" />
    		<result property="writer" column="c_writer" />
    		<result property="regDate" column="c_reg_date" />
    		<result property="updateDate" column="c_update_date" />
    
    <!-- collection의 자식태그로 정의 -->
    <!-- collection은 java에서 List이므로 JSON에서는 배열[]처리해야 한다. -->
    		<collection property="replyList" ofType="org.galapagos.domain.ReplyVO">
    			<id property="no" column="r_no" />
    			<result property="cno" column ="cno"/>
    			<result property="content" column="r_content" />
    			<result property="writer" column="r_writer" />
    			<result property="regDate" column="r_reg_date" />
    			<result property="updateDate" column="r_update_date" />
    		</collection>
    	</resultMap>
    	
    	<insert id="create">
    		insert into tbl_comment (bno, writer, content)
    		values(#{bno},
    		#{writer}, #{content})
    		<!-- insert로 인해 property랑 column에 auto_increment 된 값이 무엇인지 확인용 -->
    		<selectKey resultType="Long" keyProperty="no" keyColumn="no"
    			order="AFTER">
    			SELECT LAST_INSERT_ID()
    		</selectKey>
    	</insert>
    	<update id="update">
    		update tbl_comment
    		set
    		content = #{content},
    		update_date = now()
    		where no = #{no}
    	</update>
    	<delete id="delete">
    		delete from tbl_comment
    		where no = #{no}
    	</delete>
    	
    	<!-- SQL에서 select 조인문 가져오기 -->
    	<select id="readAll" resultMap="CommentMap">
    		select
    			c.no, c.bno, c.content c_content, c.writer c_writer, 
    			c.reg_date c_reg_date, c.update_date c_update_date,
    			r.no r_no, r.cno, r.content r_content, r.writer r_writer,
    			r.reg_date r_reg_date, r.update_date r_update_date
    		from tbl_comment c left join tbl_reply r
    			on c.no = r.cno
    		where bno = #{bno}
    	</select>
    	<select id="get" resultType="CommentVO">
    		select
    			c.no, c.bno, c.content c_content, c.writer c_writer,
    			c.reg_date c_reg_date, c.update_date c_update_date,
    			r.no r_no, r.cno, r.content r_content, r.writer r_writer,
    			r.reg_date r_reg_date, r.update_date r_update_date
    		from tbl_comment c left join tbl_reply r
    			on c.no = r.cno
    		where c.no = #{no}
    	</select>
    </mapper>

     

    talend로 가서 테스트 해 보기

    답글 안 달아놓은 곳엔 cno : null로 나옴 

     

    3. ReplyMapper 인터페이스

    package org.galapagos.mapper;
    
    import org.galapagos.domain.ReplyVO;
    
    public interface ReplyMapper {
    	ReplyVO get(Long no);
    	
    	void create(ReplyVO vo);
    
    	void update(ReplyVO vo);
    
    	void delete(Long no);
    }

     

    4. ReplyMapper.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="org.galapagos.mapper.ReplyMapper">
    
    	<select id="get" resultType="ReplyVO">
    		select * from tbl_reply
    		where no = #{no}
    	</select>
    
    	<insert id="create">
    		<selectKey resultType="Long" keyProperty="no" keyColumn="no"
    			order="AFTER">
    			SELECT LAST_INSERT_ID()
    		</selectKey>
    			insert into tbl_reply (cno, writer, content)
    			values(#{cno}, #{writer}, #{content})
    	</insert>
    
    	<update id="update">
    		update tbl_reply
    			set
    				content = #{content},
    				update_date = now()
    			where no = #{no}
    	</update>
    	<delete id="delete">
    		delete from tbl_reply
    		where no = #{no}
    	</delete>
    </mapper>

     

    5. ReplyController

    @RestController 로 REST에 있는 body를 자동으로 등록해 준다.

    package org.galapagos.controller;
    
    import org.galapagos.domain.ReplyVO;
    import org.galapagos.mapper.ReplyMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.DeleteMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.PutMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/api/board/{bno}/reply")
    public class ReplyController {
    	@Autowired
    	private ReplyMapper mapper;
    
    	@PostMapping("")
    	public ReplyVO create(@RequestBody ReplyVO vo) {
    		mapper.create(vo);
    		return mapper.get(vo.getNo());
    	}
    
    	@PutMapping("/{no}")
    	public ReplyVO update(@PathVariable Long no, @RequestBody ReplyVO vo) {
    		System.out.println("==> " + vo);
    		mapper.update(vo);
    		return mapper.get(vo.getNo());
    	}
    
    	@DeleteMapping("/{no}")
    	public String delete(@PathVariable Long no) {
    		mapper.delete(no);
    		return "OK";
    	}
    }

     


    📍클라이언트 측 설계

    - 화면 처리 - 

     

    - 본인 댓글에 본인이 답글 못 달게 하기

    - 댓글에 답글이 있으면 들여쓰기로 보이게 하기❗

     

    1. Comment.js 기존 코드 수정

    const replyAddable = `
    <button class="btn btn-light btn-sm reply-add-show-btn">
    <i class="fa-solid fa-pen-to-square"></i> 답글
    </button>
    `;

    상단에 답글 위치 (+버튼)

     

    답글에도 로그인 사용자 조건 추가 (헷갈리니 이전에 있던 행까지 두 줄 같이 코드 첨부)

    	${writer && (writer == comment.writer) ? commentUpdatable : ''} 
    			${writer && (writer != comment.writer) ? replyAddable : ''}

    ⭐들여쓰기 부분 margin left 

    ms 에서 ml로 변경 (들여쓰기)

     

    el태그 위치 변경 (commentEl => createCommentTemplate() 로)

    변경 전
    변경 후

    reply 답글이 있다면 화면에 나오게 콘솔로 찍어보기

    console 화면

     


    2. 답글 화면  

    - 답글 입력칸 화면 보이기 -

    reply.js 생성

    function createReplyEditTemplate(reply) {
    	return `
    		<div class="bg-light p-2 rounded reply-edit-block" 
    			data-no = "${reply.no}"
    			data-cno="${reply.cno}" 
    			data-writer="${reply.writer}">
    				
    			<div>${reply.no ? '' : ' 답글 작성'}</div>
    				
    				<textarea class="form-control mb-1 reply-editor">
    					${reply.content || '' }
    				</textarea>
    				
    				<div class="text-end">
    					<button class="btn btn-light btn-sm py-1 
    						${reply.no ? 'reply-update' : 'reply-add-btn'} ">
    						<i class="fa-solid fa-check"></i> 확인
    					</button>
    						
    					<button class="btn btn-light btn-sm py-1 
    						${reply.no ? 'reply-update-cancel' : 'reply-add-cancel-btn'} ">
    						<i class="fa-solid fa-undo"></i> 최소
    					</button>
    			</div>
    		</div>
    	`;
    }
    
    function showReplyAdd(el, writer) {
    	const commentEl = el.closest('.comment');
    	const cno = commentEl.data("no");
    	const reply = { cno, writer };
    	const template = createReplyEditTemplate(reply);
    
    	commentEl.find('.reply-list').append($(template));
    	commentEl.find('.btn-group').hide();
    	commentEl.find('.reply-editor').focus();
    }

     

    board의 get.jsp 수정

    reply.js 만들었으니 스크립트 처리 + url 전역 상수 

     get.jsp 하단에 "답글 버튼" 이벤트 핸들링

    $(this)가 답글버튼

    (jquery객체로 해서 로그인 사용자정보를 같이 전달~)

    답글 버튼 생김
    답글 작성칸


    -  답글 등록 버튼 만들기 -

     

    역시 등록하면 작성칸 사라지면서 처리해야 함

     

    reply.js

    답글 버튼 그룹에 들어갈 항목준비

    윗부분에 추가해 주자(comment.js와 비슷하게 - 관리용이)

    //답글 버튼 관리
    const replyUpdatable = `
    	<button class="btn btn-light btn-sm reply-update-show-btn">
    		<i class="fa-solid fa-pen-to-square"></i> 수정
    	</button>
    	<button class="btn btn-light btn-sm reply-delete-btn">
    		<i class="fa-solid fa-times"></i> 삭제
    	</button>
    `;

    답글 생성(추가)을 위한 template 

    class 명과 매개변수가 다르다.

    //답글 생성
    function createReplyTemplate(reply, writer) {
    	return `
    		<div class="reply my-3" 
    		data-no="${reply.no}" 
    		data-writer="${reply.writer}">
    			<div class="reply-title my-2 d-flex justify-content-between">
    				<div class="reply-head">
    					<strong class="reply-writer">
    						<img src="/security/avatar/sm/${reply.writer}" class="avatar-sm">${reply.writer}
    					</strong>
    					<span class="text-muted ml-3 reply-date">
    						${moment(reply.regDate).format('YYYY-MM-DD hh:mm')}
    					</span>
    				</div>
    				
    				<div class="btn-group">
    					${writer && (writer == reply.writer) ? replyUpdatable : ''} 
    				</div>
    			</div>
    			<div class="reply-body">
    				<div class="reply-content">${ reply.content || '' }</div>
    			</div>
    		</div>
    	`;
    }

     

    3. 답글 생성(추가) 기능 만들기 

     

    async 문에서 el : 답글 작성 "확인"버튼임

    const content = replyeBlock.find에서 .reply-editor << textarea 부분임

    DB 처리 안 해서 새로고침 하면 사라짐 

    reply.js

    // 답글 추가
    async function addReply(el, writer) { //el이 답글 작성하고 누르는 "확인"버튼
    	console.log('reply 추가');
    
    	// cno 추출, writer 추출
    	const commentEl = el.closest('.comment');
    	const replyBlock = commentEl.find('.reply-edit-block');
    	
    	const cno = parseInt(commentEl.data("no"));
    	const content =replyBlock.find('.reply-editor').val(); 
    	let reply = { cno, writer, content };
    
    	// REPLY POST API 호출
    	reply = await rest_create(REPLY_URL, reply);
    	console.log(reply);
    
    	const replyEl = $(createReplyTemplate(reply, writer));
    	commentEl.find('.reply-list').append(replyEl);
    	commentEl.find('.reply-edit-block').remove();
    	commentEl.find('.btn-group').show();
    }

     

     get.jsp 에서 답글 작성 "확인" 버튼 이벤트 핸들링 

    	// 답글 추가해서 작성 후 "확인" 버튼
    		$('.comment-list').on('click', '.reply-add-btn', function(e){
    		addReply($(this), writer);
    		});

    reply-add-btn = 답글 추가 확인 버튼 = $(this)

    된다.


    4. 답글 추가 "취소" 및 이벤트핸들링

    reply.js

    // 답글 취소
    function cancelReply(e) {
    	const commentEl = $(this).closest('.comment');
    	commentEl.find('.reply-edit-block').remove();
    	commentEl.find('.btn-group').show();
    }

    get.jsp

    // 답글 "취소"
    		$('.comment-list').on('click', '.reply-add-cancel-btn', cancelReply);

    ❤️+) 답글 등록(+수정)할 때는 writer 정보가 필요해서 매개변수(지역변수)로 받아야 하므로 함수를 호출까지 해야 하고

    삭제는 필요없어서 이름만 설정하면 된다. (밑에 코드 참고해서 비교)

    	// 답글 추가해서 작성 후 "확인" 버튼
    		$('.comment-list').on('click', '.reply-add-btn', function(e){
    		addReply($(this), writer);
    		});

    5. 추가된 답글들도 화면에 다 보여줘야하니 답글 목록 만들기

    댓글 목록 불러올 때 같이 불러와야 하니까 그 안에 넣어주면 된다.

    Comment.js

    (위에서 console로 테스트해 본 위치)

    let replyEl = $(createReplyTemplate(reply, writer));
    replyListEl.append(replyEl);

    잘 나온다.(접었다 폈다 기능이 매우 필요해 보인다.)


     - 답글 수정 화면 보여주기 -

    수정요청에 필요한 부분 : no, content 

     

    reply.js

    // 답글 수정 화면 보여주기
    function showUpdateReply(el) {
    	const replyEl = el.closest('.reply');
    	
    	const no = replyEl.data("no");
    	const content = replyEl.find('.reply-content').html();
    	
    	const reply = { no, content };
    	const editor = $(createReplyEditTemplate(reply));
    	
    	replyEl.find('.reply-content').hide();
    	replyEl.find('.reply-body').append(editor);
    }

     

     답글 수정한 화면 이벤트 핸들링 

    get.jsp

    // 답글 수정 화면 보이기
    $('.comment-list').on('click', '.reply-update-show-btn', function(e) {
    showUpdateReply($(this));
    });

    하고 보니 수정버튼 누르면 답글입력칸(+확인,취소버튼)만 나와야 하는데 댓글수정삭제버튼이 안 숨겨짐

    처리하기

    replyEl.find('.btn-group').hide();


    - 답글 수정한 내용 등록되게 처리 -

    reply .js

    // 답글 수정한 것 등록 처리
    async function updateReply(el) {
    	if(!confirm('답글을 수정할까요?')) return;
    
    	const replyEl = el.closest('.reply');
    	const replyContent = replyEl.find('.reply-content');
    	const content = replyEl.find('.reply-editor').val();
    	
    	const no = replyEl.data("no"); 
    	let reply = { no, content };
    	
    	reply = await rest_modify(REPLY_URL + no, reply);
    	
    	replyContent.html(content);
    	replyContent.show();
    	replyEl.find('.reply-edit-block').remove();
    }

    get.jsp 수정 등록 처리 

    // 답글 수정 등록
    $('.comment-list').on('click', '.reply-update', function(e) {
    updateReply($(this));
    });

     

    수정하고 다시 버튼 보이게 하는 기능이 빠졌다.

    reply.js remove 다음 줄에 추가 

    replyEl.find('.btn-group').show();


    - 답글 수정 취소 화면 -

    reply.js

    // 답글 수정 화면 취소
    function cancelReplyUpdate() {
    	const replyEl = $(this).closest('.reply');
    	replyEl.find('.reply-content').show(); //취소니까 원래 화면 복원한 것
    	replyEl.find('.reply-edit-block').remove();
    }

    get.jsp

    // 답글 수정 취소
    $('.comment-list').on('click', '.reply-update-cancel', cancelReplyUpdate);

    6. 답글 삭제

    reply.js

    // 답글 삭제
    async function deleteReply(e) {
    	if(!confirm('답글을 삭제할까요?')) return;
    
    	const replyEl = $(this).closest('.reply');
    	const no = parseInt(replyEl.data("no"));
    	
    	await rest_delete(REPLY_URL + no);
    	replyEl.remove();
    }

    get.jsp

    // 답글 삭제
    $('.comment-list').on('click', '.reply-delete-btn', deleteReply);

    삭제 전 화면
    사라짐


    [소스코드]

     

    Comment.js ⬇️

    더보기
    //같이 보여줄 답글 버튼
    const replyAddable = `
    <button class="btn btn-light btn-sm reply-add-show-btn">
    <i class="fa-solid fa-pen-to-square"></i> 답글
    </button>
    `;
    
    //답글 버튼 구성
    const commentUpdatable = `
    	<button class="btn btn-light btn-sm comment-update-show-btn">
    		<i class="fa-solid fa-pen-to-square"></i> 수정
    	</button>
    	<button class="btn btn-light btn-sm comment-delete-btn">
    		<i class="fa-solid fa-times"></i> 삭제
    	</button>
    `;
    
    //댓글 생성 하기 화면
    function createCommentTemplate(comment, writer) {
    	console.log(comment, comment.writer, comment.content)
    	console.log(writer)
    	
    	return `
    	<div class="comment my-3" data-no="${comment.no}" data-writer = "${comment.writer}">
    		<div class="comment-title my-2 d-flex justify-content-between">
    			<div >
    				<strong class="writer">
    					<img src="/security/avatar/sm/${comment.writer}" class="avatar-sm">
    					${comment.writer}
             		</strong>
    				<span class="text-muted ms-3 comment-date">
    					${moment(comment.regDate).format('YYYY-MM-DD hh:mm')}
    				</span>
    			</div>
    			
    			<div  class="btn-group">
    			${writer && (writer == comment.writer) ? commentUpdatable : ''} 
    			${writer && (writer != comment.writer) ? replyAddable : ''}      		
    			</div>
    		</div>
    		<div class="comment-body">
    			<div class="comment-content">${comment.content}</div>
    		</div>
    		<div class="reply-list ml-5">
    		<!-- 답글 목록 출력 영역 -->
    		</div>
    	</div>
    	`;
    }
    
    //댓글 목록 불러오기
    async function loadComments(bno, writer) {
    	let comments = [];
    	
    	// API로 불러오기
    	comments = await rest_get(COMMENT_URL);
    
    	for(let comment of comments) {
    		const commentEl = $(createCommentTemplate(comment, writer));
    		$('.comment-list').append(commentEl);
    		
    		let replyListEl = commentEl.find('.reply-list');
    		// 답글 목록 처리
    		for(let reply of comment.replyList) {
    			let replyEl = $(createReplyTemplate(reply, writer));
    			replyListEl.append(replyEl);
    
    		};
    	}
    	
    }
    
    // 댓글 입력 가이드 이벤트 처리
    
    async function createComment(bno, writer) {
    	const content = $('.new-comment-content').val();
    	console.log(content);
    	
    	if(!content) {
    		alert('내용을 입력하세요.');
    		$('.new-comment-content').focus();
    		return;
    	}
    	
    	if(!confirm('댓글을 추가할까요?')) return;
    	let comment  = { bno, writer , content }
    	console.log(comment);
    	
    	// REST로 등록
    	comment = await rest_create(COMMENT_URL, comment);
    		
    
    	// 등록 성공 후 DOM 처리
    	const commentEl = createCommentTemplate(comment, writer);
    	$('.comment-list').prepend($(commentEl));	
    	$('.new-comment-content').val('');
    		
    }
    
    //댓글 수정 입력칸 만들기
    function createCommentEditTemplate(comment) {
    	return `
    		<div class="bg-light p-2 rounded comment-edit-block">
    			<textarea class="form-control mb-1 comment-editor"
    				>${comment.content}</textarea>
    			<div class="text-end">
    				<button class="btn btn-light btn-sm py-1 comment-update-btn">
    					<i class="fa-solid fa-check"></i> 확인</button>
    				<button class="btn btn-light btn-sm  py-1 comment-update-cancel-btn">
    					<i class="fa-solid fa-undo"></i> 최소</button>
    			</div>
    		</div>
    	`;
    }
    
    //댓글 수정 화면 보여주기
    //
    function showUpdateComment(e) {
    	const commentEl = $(this).closest('.comment');
    	const no = commentEl.data("no");
    	
    	const contentEl = commentEl.find('.comment-content');
    	const comment = { no, content: contentEl.html().trim() };
    	
    	console.log(comment);
    	
    	contentEl.hide();
    	commentEl.find('.btn-group').hide();
    	
    	
    	const template = createCommentEditTemplate(comment);
    	const el = $(template);	
    	commentEl.find('.comment-body').append(el);
    	
    }
    
    
    
    
    // 댓글 수정 처리하기
    async function updateComment(commentEl, writer) {
    	if(!confirm('수정할까요?')) return;
    
    	const editContentEl = commentEl.find('.comment-edit-block');	// 수정 창 
    	const content = editContentEl.find('.comment-editor').val();	// 수정 내용
    	const no = parseInt(commentEl.data("no"));
    	let comment = { no, writer,	content };
    	
    	
    	comment = await rest_modify(COMMENT_URL + comment.no, comment);
    	console.log('수정', comment);	
    		
    	const contentEl = commentEl.find('.comment-content')	
    	editContentEl.remove();
    	contentEl.html(comment.content);	// 변경된 내용으로 화면 내용 수정
    	contentEl.show();	
    	
    	commentEl.find('.btn-group').show();
    }
    
    
    // 댓글 수정 취소 처리
    function cancelCommentUpdate(e) {
    	const commentEl = $(this).closest('.comment');
    	commentEl.find('.comment-content').show();
    			//.css('display', 'block');
    	
    	commentEl.find('.comment-edit-block').remove();
    	commentEl.find('.btn-group').show();
    }
    
    
    
    // 댓글 삭제
    async function deleteComment(e) {
    	if(!confirm('댓글을 삭제할까요?')) return;
    	
    	const comment = $(this).closest('.comment')
    	const no = comment.data("no");	
    
    	await rest_delete(COMMENT_URL + no);
    		
    	// api 호출	
    	comment.remove();
    
    }

     

    reply.js ⬇️

    더보기
    //답글 버튼 관리
    const replyUpdatable = `
    	<button class="btn btn-light btn-sm reply-update-show-btn">
    		<i class="fa-solid fa-pen-to-square"></i> 수정
    	</button>
    	<button class="btn btn-light btn-sm reply-delete-btn">
    		<i class="fa-solid fa-times"></i> 삭제
    	</button>
    `;
    
    //답글 생성
    function createReplyTemplate(reply, writer) {
    	return `
    		<div class="reply my-3" 
    		data-no="${reply.no}" 
    		data-writer="${reply.writer}">
    			<div class="reply-title my-2 d-flex justify-content-between">
    				<div class="reply-head">
    					<strong class="reply-writer">
    						<img src="/security/avatar/sm/${reply.writer}" class="avatar-sm">${reply.writer}
    					</strong>
    					<span class="text-muted ml-3 reply-date">
    						${moment(reply.regDate).format('YYYY-MM-DD hh:mm')}
    					</span>
    				</div>
    				
    				<div class="btn-group">
    					${writer && (writer == reply.writer) ? replyUpdatable : ''} 
    				</div>
    			</div>
    			<div class="reply-body">
    				<div class="reply-content">${ reply.content || '' }</div>
    			</div>
    		</div>
    	`;
    }
    
    
    //답글 입력칸 생성
    function createReplyEditTemplate(reply) {
    	return `
    		<div class="bg-light p-2 rounded reply-edit-block" 
    			data-no = "${reply.no}"
    			data-cno="${reply.cno}" 
    			data-writer="${reply.writer}">
    				
    			<div>${reply.no ? '' : ' 답글 작성'}</div>
    				
    				<textarea class="form-control mb-1 reply-editor">
    					${reply.content || '' }
    				</textarea>
    				
    				<div class="text-end">
    					<button class="btn btn-light btn-sm py-1 
    						${reply.no ? 'reply-update' : 'reply-add-btn'} ">
    						<i class="fa-solid fa-check"></i> 확인
    					</button>
    						
    					<button class="btn btn-light btn-sm py-1 
    						${reply.no ? 'reply-update-cancel' : 'reply-add-cancel-btn'} ">
    						<i class="fa-solid fa-undo"></i> 최소
    					</button>
    			</div>
    		</div>
    	`;
    }
    
    //답글 화면에 보여주기
    function showReplyAdd(el, writer) {
    	const commentEl = el.closest('.comment');
    	const cno = commentEl.data("no");
    	const reply = { cno, writer };
    	const template = createReplyEditTemplate(reply);
    
    	commentEl.find('.reply-list').append($(template));
    	commentEl.find('.btn-group').hide();
    	commentEl.find('.reply-editor').focus();
    }
    
    // 답글 추가
    async function addReply(el, writer) { //el이 답글 작성하고 누르는 "확인"버튼
    	console.log('reply 추가');
    
    	// cno 추출, writer 추출
    	const commentEl = el.closest('.comment');
    	const replyBlock = commentEl.find('.reply-edit-block');
    	
    	const cno = parseInt(commentEl.data("no"));
    	const content =replyBlock.find('.reply-editor').val(); 
    	let reply = { cno, writer, content };
    
    	// REPLY POST API 호출
    	reply = await rest_create(REPLY_URL, reply);
    	console.log(reply);
    
    	const replyEl = $(createReplyTemplate(reply, writer));
    	commentEl.find('.reply-list').append(replyEl);
    	commentEl.find('.reply-edit-block').remove();
    	commentEl.find('.btn-group').show();
    }
    
    // 답글 수정 화면 보여주기
    function showUpdateReply(el) {
    	const replyEl = el.closest('.reply');
    	
    	const no = replyEl.data("no");
    	const content = replyEl.find('.reply-content').html();
    	
    	const reply = { no, content };
    	const editor = $(createReplyEditTemplate(reply));
    
    	
    	replyEl.find('.reply-content').hide();
    	replyEl.find('.btn-group').hide();
    	replyEl.find('.reply-body').append(editor);
    }
    
    // 답글 수정한 것 등록 처리
    async function updateReply(el) {
    	if(!confirm('답글을 수정할까요?')) return;
    
    	const replyEl = el.closest('.reply');
    	const replyContent = replyEl.find('.reply-content');
    	const content = replyEl.find('.reply-editor').val();
    	
    	const no = replyEl.data("no"); 
    	let reply = { no, content };
    	
    	reply = await rest_modify(REPLY_URL + no, reply);
    	
    	replyContent.html(content);
    	replyContent.show();
    	replyEl.find('.reply-edit-block').remove();
    	
    	replyEl.find('.btn-group').show();
    }
    
    // 답글 수정 화면 취소
    function cancelReplyUpdate() {
    	const replyEl = $(this).closest('.reply');
    	replyEl.find('.reply-content').show(); //취소니까 원래 화면 복원한 것
    	replyEl.find('.reply-edit-block').remove();
    }
    
    
    
    // 답글 취소
    function cancelReply(e) {
    	const commentEl = $(this).closest('.comment');
    	commentEl.find('.reply-edit-block').remove();
    	commentEl.find('.btn-group').show();
    }
    
    // 답글 삭제
    async function deleteReply(e) {
    	if(!confirm('답글을 삭제할까요?')) return;
    
    	const replyEl = $(this).closest('.reply');
    	const no = parseInt(replyEl.data("no"));
    	
    	await rest_delete(REPLY_URL + no);
    	replyEl.remove();
    }

     

    get.jsp ⬇️

    더보기
    <%@ page language="java" contentType="text/html; charset=UTF-8"
    	pageEncoding="UTF-8"%>
    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
    <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
    
    <%@include file="../layouts/header.jsp"%>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
    <script src="/resources/js/comment.js"></script>
    <script src="/resources/js/rest.js"></script>
    <script src="/resources/js/reply.js"></script>
    
    <script>
    //댓글, 답글 기본 URL 상수 - 전역 상수
    const COMMENT_URL = '/api/board/${param.bno}/comment/';
    const REPLY_URL = '/api/board/${param.bno}/reply/';
    
    
    $(document).ready(async function() {
    
    	$('.remove').click(function(){
    		if(!confirm('정말 삭제할까요?')) return;		
    		document.forms.removeForm.submit();
    	});	
    	
    	let bno = ${param.bno}; // 글번호
    	let writer = '${username}'; // 작성자(로그인 유저)
    	
    	loadComments(bno, writer); // 댓글 목록 불러오기
    	
    	//댓글 추가 버튼 처리
    	$('.comment-add-btn').click(function(e) {
    		createComment(bno, writer);
    	});
    	
    	$('.comment-list').on('click', '.comment-update-show-btn', showUpdateComment);
    		//console.log('수정 버튼 클릭!', $(this)); //this는 comment-update-show-btn 클래스 중 하나가 선택된다.
    		
    	// 댓글 수정, 삭제 버튼 처리 - 이벤트 버블링(이벤트 처리 위임)
    	
    		// 댓글 수정 확인 버튼 클릭
    		$('.comment-list').on('click', '.comment-update-btn', function (e){
    		const el = $(this).closest('.comment');
    		updateComment(el, writer);
    		});
    		
    		// 댓글 수정 취소 버튼 클릭
    		$('.comment-list').on('click', '.comment-update-cancel-btn', 
    		cancelCommentUpdate);
    	
    		// 댓글 삭제 버튼
    		$('.comment-list').on('click', '.comment-delete-btn', 
    				deleteComment);
    		
    	/////// 답글 버튼 이벤트 핸들링
    		// 답글 추가버튼 인터페이스 보이기
    		$('.comment-list').on('click', '.reply-add-show-btn', function(e) {
    		showReplyAdd($(this), writer);
    		});
    		
    		// 답글 추가해서 작성 후 "확인" 버튼
    		$('.comment-list').on('click', '.reply-add-btn', function(e){
    		addReply($(this), writer);
    		});
    		
    		// 답글 수정 화면 보이기
    		$('.comment-list').on('click', '.reply-update-show-btn', function(e) {
    		showUpdateReply($(this));
    		});
    		
    		// 답글 수정 등록
    		$('.comment-list').on('click', '.reply-update', function(e) {
    		updateReply($(this));
    		});
    		
    		// 답글 수정 취소
    		$('.comment-list').on('click', '.reply-update-cancel', cancelReplyUpdate);
    
    		
    		// 답글 "취소"
    		$('.comment-list').on('click', '.reply-add-cancel-btn', cancelReply);
    		
    		// 답글 삭제
    		$('.comment-list').on('click', '.reply-delete-btn', deleteReply);
    
    
    });
    
    </script>
    
    <h1 class="page-header">
    	<i class="far fa-file-alt"></i> ${board.title}
    </h1>
    
    <div class="d-flex justify-content-between">
    	<div>
    		<i class="fas fa-user"></i> ${board.writer}
    	</div>
    	<div>
    		<i class="fas fa-clock"></i>
    		<fmt:formatDate pattern="yyyy-MM-dd" value="${board.regDate}" />
    	</div>
    </div>
    
    <hr>
    
    <div>${board.content}</div>
    
    <!-- 새 댓글 작성 (작성자 아니어야 가능)-->
    <c:if test="${username != board.writer }">
    	<div class="bg-light p-2 rounded my-5">
    		<div>${username == null ? '댓글을 작성하려면 먼저 로그인하세요' : '댓글 작성' }</div>
    		<div>
    			<textarea class="form-control new-comment-content" rows="3"
    				${username == null ? 'disabled' : '' }></textarea>
    			<div class="text-right">
    				<button class="btn btn-primary btn-sm my-2 comment-add-btn" 
    					${username == null ? 'disabled' : '' } >
    						<i class="fa-regular fa-comment"></i> 확인
    				</button>
    			</div>
    		</div>
    	</div>
    </c:if>
    
    
    <div class="my-5"><i class="fa-regular fa-comments"></i>
    	댓글 목록
    	<hr>
    	<div class="comment-list">
    	</div>
    </div>
    
    
    <div class="mt-4">
    	<a href="${cri.getLink('list')}" class="btn btn-primary list"> <i
    		class="fas fa-list"></i> 목록
    	</a>
    
    	<c:if test="${username == board.writer}">
    		<a href="${cri.getLinkWithBno('modify', board.bno) }"
    			class="btn btn-primary modify"> <i class="far fa-edit"></i> 수정
    		</a>
    		<a href="#" class="btn btn-danger remove"> <i
    			class="fas fa-trash-alt"></i> 삭제
    		</a>
    	</c:if>
    </div>
    
    <%-- 
    <form id="listForm" action="/board/list" method="get" >
    	<input type="hidden" name="pageNum" value="${cri.pageNum}"/>
    	<input type="hidden" name="amount" value="${cri.amount}"/>
    	<input type="hidden" name="type" value="${cri.type}"/>
    	<input type="hidden" name="keyword" value="${cri.keyword}"/>
    </form>
    
    <form id="modifyForm" action="/board/modify" method="get" >
    	<input type="hidden" id="bno" name="bno" value="${board.bno}"/>
    	<input type="hidden" name="pageNum" value="${cri.pageNum}"/>
    	<input type="hidden" name="amount" value="${cri.amount}"/>
    	<input type="hidden" name="type" value="${cri.type}"/>
    	<input type="hidden" name="keyword" value="${cri.keyword}"/>
    </form>
     --%>
    
    <form action="remove" method="post" name="removeForm">
    	<!-- csrf 토큰 안 넣으면 404 에러 -->
    	<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token }" />
    		<input type="hidden" name="bno" value="${board.bno}" />
    		<input type="hidden" name="pageNum" value="${cri.pageNum}" />
    		<input type="hidden" name="amount" value="${cri.amount}" />
    		<input type="hidden" name="type" value="${cri.type}" />
    		<input type="hidden" name="keyword" value="${cri.keyword}" />
    </form>
    
    <%@include file="../layouts/footer.jsp"%>

     

     


     

Designed by Tistory.