[Framework] TIL 067 - 23.10.24

유진·2023년 10월 23일
0

07_Framework

댓글

== ( 예시 ) 공지사항 1998 게시물

DB

-- 댓글 목록 조회
SELECT COMMENT_NO, COMMENT_CONTENT,
TO_CHAR(C_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') C_CREATE_DATE,
BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
FROM "COMMENT"
JOIN MEMBER USING(MEMBER_NO)
WHERE BOARD_NO = 1998
ORDER BY COMMENT_NO
;


SELECT * FROM "BOARD"
ORDER BY BOARD_NO DESC;




INSERT INTO "COMMENT"									-- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '부모 댓글 1', DEFAULT, DEFAULT, 1998, 2, NULL);
INSERT INTO "COMMENT"									-- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '부모 댓글 2', DEFAULT, DEFAULT, 1998, 2, NULL);


--  만약 INSERT 한 댓글  넘버가 1002 ,1003이라면 


INSERT INTO "COMMENT"									-- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '자식 댓글 1-1', DEFAULT, DEFAULT, 1998, 2, 1001);
INSERT INTO "COMMENT"									-- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '자식 댓글 1-2', DEFAULT, DEFAULT, 1998, 2, 1001);
INSERT INTO "COMMENT"									-- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '자식 댓글 1-3', DEFAULT, DEFAULT, 1998, 2, 1001);
INSERT INTO "COMMENT"									-- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '자식 댓글 2-1', DEFAULT, DEFAULT, 1998, 2, 1002);
INSERT INTO "COMMENT"									-- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '자식 댓글 2-2', DEFAULT, DEFAULT, 1998, 2, 1002);
INSERT INTO "COMMENT"									-- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '자식의 자식 댓글 2-1-1', DEFAULT, DEFAULT, 1998, 2, 1006);
COMMIT;


------------ *** 계층이 나눠진 상태로 조회하는 쿼리 == 계층형 쿼리 *** ------------
-- 부모댓글 2
  -- 자식댓글 2-1
    -- 자식의 자식댓글 2-1-1

-- 부모 1
  -- 자식 1-1
  -- 자식 1-2
  -- 자식 1-3
-- 부모 2
  -- 자식 2-1
    -- 자식의 자식 2-1-1
  -- 자식 2-2

-----------------------------------------------------------------------------------


/* 계층형 쿼리(START WITH, CONNECT BY, ORDER SIBLINGS BY)
 * 
- 상위 타입과 하위 타입간의 관계를 계층식으로 표현할 수 있게하는 질의어(SELECT)
- START WITH : '상위 타입(최상위 부모)'으로 사용될 행을 지정 (서브쿼리로 지정 가능)
- CONNECT BY
-> '상위 타입과 하위 타입 사이의 관계'를 규정
-> PRIOR(이전의) 연산자와 같이 사용하여

현재 행 이전에 상위 타입 또는 하위 타입이 있을지 규정

1) 부모 -> 자식 계층 구조
CONNECT BY PRIOR 자식 컬럼 = 부모 컬럼

2) 자식 -> 부모 계층 구조
CONNECT BY PRIOR 부모 컬럼 = 자식 컬럼
- ORDER SIBLINGS BY : '계층 구조 정렬' == 형제,자매 순서를 나타내는 것

** 계층형 쿼리가 적용 SELECT 해석 순서 **
5 : SELECT
1 : FROM (+JOIN)
4 : WHERE
2 : START WITH
3 : CONNECT BY
6 : ORDER SIBLINGS BY

- 'WHERE절이 계층형 쿼리보다 해석 순서가 늦기' 때문에
'먼저 조건을 반영하고 싶은 경우 FROM절 서브쿼리(인라인뷰)'를 이용        *** 인라인뷰? 서브쿼리를 만들어 하나의 테이블처럼 사용하는 것
* */


-- 댓글 목록 조회 (계층형 쿼리 적용)
SELECT LEVEL, C.* FROM
	(SELECT COMMENT_NO, COMMENT_CONTENT,
	TO_CHAR(C_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') C_CREATE_DATE,
	BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
	FROM "COMMENT"
	JOIN MEMBER USING(MEMBER_NO)
	WHERE BOARD_NO = 1998) C
WHERE COMMENT_DEL_FL = 'N'
START WITH PARENT_NO IS NULL
CONNECT BY PRIOR COMMENT_NO = PARENT_NO
ORDER SIBLINGS BY COMMENT_NO
;

board-mapper.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="boardMapper">


	<!-- Board DTO에 대한 resultMap -->
	<resultMap type="Board" id="board_rm">
	
		<id property="boardNo" column="BOARD_NO"/>
		
		<result property="boardTitle" column="BOARD_TITLE"/>
		<result property="boardContent" column="BOARD_CONTENT"/>
		<result property="boardCreateDate" column="B_CREATE_DATE"/>
		<result property="boardUpdateDate" column="B_UPDATE_DATE"/>
		<result property="readCount" column="READ_COUNT"/>
		<result property="commentCount" column="COMMENT_COUNT"/>
		<result property="likeCount" column="LIKE_COUNT"/>
		<result property="memberNickname" column="MEMBER_NICKNAME"/>
		
		<result property="memberNo" column="MEMBER_NO"/>
		<result property="profileImage" column="PROFILE_IMG"/>
		
		<result property="thumbnail" column="THUMBNAIL"/>
		
		<!-- collection 태그 
			select로 조회된 결과를 컬렉션(List)에 담아
			지정된 필드에 세팅
			
			property : List를 담을 DTO의 필드명
			select : 실행할 select의 id
			column : 조회 결과 중 지정된 컬럼의 값을 파라미터로 전달
			javaType : List(컬렉션)의 타입을 지정
			ofType : List(컬렉션)의 제네릭(타입 제한) 지정
		-->
		<collection property="imageList" 
					select="selectImageList"
					column="BOARD_NO"
					javaType="java.util.ArrayList"
					ofType="BoardImage"
		/>
		
		<collection property="commentList" 
					select="selectCommentList"
					column="BOARD_NO"
					javaType="java.util.ArrayList"
					ofType="Comment"
		/>
	</resultMap>
	
	
	<!-- BoardImage resultMap -->
	<resultMap type="BoardImage" id="boardImage_rm">
		<id property="imageNo" column="IMG_NO"/>
		
		<result property="imagePath" column="IMG_PATH"/>
		<result property="imageReName" column="IMG_RENAME"/>
		<result property="imageOriginal" column="IMG_ORIGINAL"/>
		<result property="imageOrder" column="IMG_ORDER"/>
		<result property="boardNo" column="BOARD_NO"/>
	</resultMap>
	
	
	<!-- Comment resultMap -->
	<resultMap type="Comment" id="comment_rm">
		<id property="commentNo" column="COMMENT_NO"/>
		<result property="commentContent" column="COMMENT_CONTENT"/>
		<result property="commentCreateDate" column="C_CREATE_DATE"/>
		<result property="boardNo" column="BOARD_NO"/>
		<result property="memberNo" column="MEMBER_NO"/>
		<result property="commentDeleteFlag" column="COMMENT_DEL_FL"/>
		<result property="parentNo" column="PARENT_NO"/>
		<result property="memberNickname" column="MEMBER_NICKNAME"/>
		<result property="profileImage" column="PROFILE_IMG"/>
	</resultMap>
	
	

	<!-- 
		resultType이 "map"인 경우
		K : 컬럼명(BOARD_CODE, BOARD_NAME)
		V : 컬럼 값(	  1	   ,   공지 사항  )
		
		[{BOARD_NAME= 공지사항, BOARD_CODE=1}, {BOARD_NAME= 자유게시판, BOARD_CODE=2}, ...]
	 -->

	<!-- 게시판 종류 목록 조회 -->
	<select id="selectBoardTypeList" resultType="map">
		SELECT * FROM "BOARD_TYPE" ORDER BY 1
	</select>
	
	<!-- 특정 게시판의 삭제되지 않은 게시글 수 조회 -->
	<select id="getListCount" resultType="_int">
		SELECT COUNT(*) FROM BOARD
		WHERE BOARD_DEL_FL = 'N'
		AND BOARD_CODE = #{boardCode}
	</select>

	<!-- CDATA 태그 : 해당 태그 내부에 작성된 것은 모두 문자로 취급 -->
	<select id="selectBoardList" resultMap="board_rm">
		SELECT BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME, READ_COUNT,
		
			<![CDATA[
				CASE
					WHEN SYSDATE - B_CREATE_DATE < 1/24/60
					THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60 * 60 ) || '초 전'
					WHEN SYSDATE - B_CREATE_DATE < 1/24
					THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60) || '분 전'
					WHEN SYSDATE - B_CREATE_DATE < 1
					THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24) || '시간 전'
					ELSE TO_CHAR(B_CREATE_DATE, 'YYYY-MM-DD')
				END B_CREATE_DATE,
			]]>
			(SELECT COUNT(*) FROM "COMMENT" C
			 WHERE C.BOARD_NO = B.BOARD_NO) COMMENT_COUNT,
			(SELECT COUNT(*) FROM BOARD_LIKE L
			 WHERE L.BOARD_NO = B.BOARD_NO) LIKE_COUNT,
			(SELECT IMG_PATH || IMG_RENAME FROM BOARD_IMG I
			WHERE I.BOARD_NO = B.BOARD_NO
			AND IMG_ORDER = 0) THUMBNAIL
		FROM "BOARD" B
		JOIN "MEMBER" USING(MEMBER_NO)
		WHERE BOARD_DEL_FL = 'N'
		AND BOARD_CODE = #{boardCode}
		ORDER BY BOARD_NO DESC
	</select>
	
	
	<select id="selectBoard" resultMap="board_rm">
		SELECT BOARD_NO, BOARD_TITLE, BOARD_CONTENT, BOARD_CODE,
			READ_COUNT, MEMBER_NICKNAME, MEMBER_NO, PROFILE_IMG,
			TO_CHAR(B_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24:MI:SS') B_CREATE_DATE,
			TO_CHAR(B_UPDATE_DATE, 'YYYY"년" MM"월" DD"일" HH24:MI:SS') B_UPDATE_DATE,
			(SELECT COUNT(*)
			FROM "BOARD_LIKE" L
			WHERE L.BOARD_NO = B.BOARD_NO) LIKE_COUNT
		FROM "BOARD" B
		JOIN "MEMBER" USING(MEMBER_NO)
		WHERE BOARD_DEL_FL = 'N'
		AND BOARD_CODE = #{boardCode}
		AND BOARD_NO = #{boardNo}
	</select>
	
	<!-- 특정 게시글 이미지 조회 -->
	<select id="selectImageList" resultMap="boardImage_rm">
		SELECT * FROM BOARD_IMG
		WHERE BOARD_NO = #{boardNo}
		ORDER BY IMG_ORDER
	</select>
	
	<!-- 특정 게시글 댓글 조회(바뀔 예정) -->
	<select id="selectCommentList" resultMap="comment_rm">
		SELECT LEVEL, C.* FROM
			(SELECT COMMENT_NO, COMMENT_CONTENT,
			TO_CHAR(C_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') C_CREATE_DATE,
			BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
			FROM "COMMENT"
			JOIN MEMBER USING(MEMBER_NO)
			WHERE BOARD_NO = #{boardNo}) C
		WHERE COMMENT_DEL_FL = 'N'
		START WITH PARENT_NO IS NULL
		CONNECT BY PRIOR COMMENT_NO = PARENT_NO
		ORDER SIBLINGS BY COMMENT_NO
	</select>
	
	
	<!-- 좋아요 여부 확인 -->
	<select id="boardLikeCheck" resultType="_int">
		SELECT COUNT(*) FROM BOARD_LIKE
		WHERE BOARD_NO = #{boardNo}
		AND MEMBER_NO = #{memberNo}
	</select>
	
	<!-- 조회 수 증가 -->
	<update id="updateReadCount">
		UPDATE "BOARD" SET
		READ_COUNT = READ_COUNT + 1
		WHERE BOARD_NO = #{boardNo}
	</update>
	
	<!-- 좋아요 삽입 -->
	<insert id="insertBoardLike">
		INSERT INTO "BOARD_LIKE" VALUES (#{boardNo}, #{memberNo})
	</insert>
	
	<!-- 좋아요 삭제 -->
	<delete id="deleteBoardLike">
		DELETE FROM "BOARD_LIKE"
		WHERE BOARD_NO = #{boardNo}
		AND MEMBER_NO = #{memberNo}
	</delete>
	
	<!-- 좋아요 조회 -->
	<select id="countBoardLike" resultType="_int">
		SELECT COUNT(*) FROM "BOARD_LIKE" WHERE BOARD_NO = #{boardNo}
	</select>
	
	
	<!-- 게시글 삽입 -->
	<!-- 
		동적 SQL
		- 프로그램 수행 중 SQL을 변경하는 기능
		
		<selectKey> 태그 : INSERT/UPDATE 시 사용할 키(시퀀스)를
					조회해서 파라미터의 지정된 필드 대입
	
		useGeneratedKeys : DB 내부적으로 생성한키 (시퀀스) 를
					전달된 파라미터의 필드로 대입 가능 여부 지정
					
		order="BEFORE" : 메인 SQL이 수행되기 전에 selectKey가 수행되도록 지정
		
		keyProperty : selectKey 조회 결과를 저장할 파라미터의 필드
					Board의 boardNo 필드에다가 조회 결과를 저장하겠다.			
	
	 -->
	
	
	<insert id="boardInsert" parameterType="Board" useGeneratedKeys="true">
	
		<selectKey order="BEFORE" resultType="_int" keyProperty="boardNo">
			SELECT SEQ_BOARD_NO.NEXTVAL FROM DUAL
		</selectKey>
		
			INSERT INTO BOARD
			VALUES( #{boardNo},
					#{boardTitle},
					#{boardContent},
					DEFAULT, DEFAULT, DEFAULT, DEFAULT,
					#{memberNo},
					#{boardCode} )
	</insert>
	
	
	<!-- 
		동적 SQL 중 <foreach>
		- 특정 sql 구문을 반복할 때 사용
		- 반복되는 사이에 구분자를 추가할 수 있음.
	
	 -->
	 
	 <!-- 이미지 리스트(여러개)삽입 -->
	 <insert id="insertImageList" parameterType="list">
	 	INSERT INTO "BOARD_IMG"
	 	SELECT SEQ_IMG_NO.NEXTVAL, A.*
	 	FROM (
	 		
	 		<foreach collection="list" item="img" separator=" UNION ALL ">
	 			SELECT #{img.imagePath} IMG_PATH,
	 				#{img.imageReName} IMG_RENAME,
	 				#{img.imageOriginal} IMG_ORIGINAL,
	 				#{img.imageOrder} IMG_ORDER,
	 				#{img.boardNo} BOARD_NO
	 			FROM DUAL
	 		</foreach>
	 	) A
	 	
	 </insert>
	 
	 
	 <!-- 게시글 수정 -->
	 <update id="boardUpdate">
	 	UPDATE "BOARD" SET
	 	BOARD_TITLE = #{boardTitle},
	 	BOARD_CONTENT = #{boardContent},
	 	B_UPDATE_DATE = SYSDATE
	 	WHERE BOARD_CODE = #{boardCode}
	 	AND BOARD_NO = #{boardNo}
	 </update>
	 
	 
	 <!-- 이미지 삭제 -->
	 <delete id="imageDelete">
	 	DELETE FROM "BOARD_IMG"
	 	WHERE BOARD_NO = #{boardNo}
	 	AND IMG_ORDER IN ( ${deleteList} )
	 </delete>
	 
	 
	<!-- 이미지 수정 -->
	<update id="imageUpdate">
		UPDATE "BOARD_IMG" SET
		IMG_PATH = #{imagePath},
		IMG_ORIGINAL = #{imageOriginal},
		IMG_RENAME = #{imageReName}
		WHERE BOARD_NO = #{boardNo}
		AND IMG_ORDER = #{imageOrder}
	</update>
	
	<!-- 이미지 삽입 -->
	<insert id="imageInsert">
		INSERT INTO "BOARD_IMG"
		VALUES(SEQ_IMG_NO.NEXTVAL, #{imagePath}, #{imageReName},
			#{imageOriginal}, #{imageOrder}, #{boardNo}
		)
	</insert>
	
	<!-- 게시글 삭제 -->
	<update id="boardDelete">
		UPDATE BOARD SET
		BOARD_DEL_FL = 'Y'
		WHERE BOARD_CODE = #{boardCode}
		AND BOARD_NO = #{boardNo}
	</update>
	
	
	 

</mapper>

comment.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"  %>

<div id="commentArea">
    <!-- 댓글 목록 -->
    <div class="comment-list-area">
        
        <ul id="commentList">
        
        	<c:forEach items="${board.commentList}" var="comment">
	            <!-- 부모/자식 댓글 -->             <!-- null이 아닌 0으로 작성해줘야 함. -->
	            <li class="comment-row <c:if test='${comment.parentNo != 0}'>child-comment</c:if>">
	                <p class="comment-writer">
	
	                    <!-- 프로필 이미지 -->
	                    <c:if test="${empty comment.profileImage}">
	                    	<img src="/resources/images/user.png">
						</c:if>
						
						<c:if test="${not empty comment.profileImage}">
							<img src="${comment.profileImage}">
						</c:if>
						
	                    <!-- 닉네임 -->
	                    <span>${comment.memberNickname}</span>
	                    
	                    <!-- 작성일 -->
	                    <span class="comment-date">${comment.commentCreateDate}</span>
	                </p>
	                
	                <!-- 댓글 내용 -->
	                <p class="comment-content">${comment.commentContent}</p>
	
	
	                <!-- 버튼 영역 -->
	                <div class="comment-btn-area">
	                    <button>답글</button>   
	                        
	                    <!-- 로그인한 회원과 댓글 작성자가 같은 경우 -->
	                    <c:if test="${loginMember.memberNo == comment.memberNo}">
	                    	<button>수정</button>
	                    	<button>삭제</button>
	                    </c:if>
	                    
	                </div>
	            </li>
        	
        	</c:forEach>

        </ul>
    </div>
    

    <!-- 댓글 작성 부분 -->
    <div class="comment-write-area">
        <textarea id="commentContent"></textarea>
        <button id="addComment">
            댓글<br>
            등록
        </button>
 
    </div>

</div>

공지사항 1998 게시물에서 보이는 화면


댓글 등록/수정/삭제 = AJAX 처리

boardDetail.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"  %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"  %>

<c:forEach items="${boardTypeList}" var="boardType">
    <c:if test="${boardType.BOARD_CODE == boardCode}" >
        <c:set var="boardName" value="${boardType.BOARD_NAME}"/>
    </c:if>
</c:forEach>

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${boardName}</title>

    <link rel="stylesheet" href="/resources/css/board/boardDetail-style.css">
    <link rel="stylesheet" href="/resources/css/board/comment-style.css">

    <script src="https://kit.fontawesome.com/f7459b8054.js" crossorigin="anonymous"></script>
</head>
<body>
    <main>
        <jsp:include page="/WEB-INF/views/common/header.jsp"/>

        <section class="board-detail">  
            <!-- 제목 -->
            <h1 class="board-title">${board.boardTitle}  <span> - ${boardName}</span>    </h1>

            <!-- 프로필 + 닉네임 + 작성일 + 조회수 -->
            <div class="board-header">
                <div class="board-writer">

                    <!-- 프로필 이미지 -->
                    <c:choose>
                    	<%-- 프로필 이미지가 없을 경우 기본 이미지 출력 --%>
                    	<c:when test="${empty board.profileImage}">
		                    <img src="/resources/images/user.png">                    	
                    	</c:when>
                    	
                    	<%-- 프로필 이미지가 있을 경우 등록한 이미지 출력 --%>
                    	<c:otherwise>
                    		<img src="${board.profileImage}" />
                    	</c:otherwise>
                    
                    </c:choose>
                    
                	    

                    <span>${board.memberNickname}</span>

                    
                    <!-- 좋아요 하트 -->
                    <span class="like-area">
                    
                    	<%-- 좋아요 누른적이 없다, 로그인 하지 않았다 --%>
                    	<c:if test="${empty likeCheck}">
                        	<i class="fa-regular fa-heart" id="boardLike"></i>
						</c:if>
						
						<%-- 좋아요 누른적이 있다 --%>
						<c:if test="${not empty likeCheck}">
                        	<i class="fa-solid fa-heart" id="boardLike"></i>
                        </c:if>


                        <span>${board.likeCount}</span>
                    </span>

                </div>

                <div class="board-info">
                    <p> <span>작성일</span>${board.boardCreateDate}</p>     

                    <!-- 수정한 게시글인 경우 -->
                    <c:if test="${not empty board.boardUpdateDate}">
                    	<p> <span>마지막 수정일</span>${board.boardUpdateDate}</p>   
					</c:if>
					
                    <p> <span>조회수</span>${board.readCount}</p>                    
                </div>
            </div>

            <!-- 이미지가 있을 경우 -->
            <c:if test="${not empty board.imageList}">
            
	            <!-- 썸네일 영역(썸네일이 있을 경우) -->
	            <%--
	            	- 이미지는 IMG_ORDER 오름차순 정렬된다
	            	- IMG_ORDER의 값이 0인 이미지 썸네일이다
	            	-> imageList에 썸네일이 있다면
	            		조회되었을때 IMG_ORDER가 0인 이미지가
	            		imageList[0]에 저장되어 있을 것이다.
	             --%>
	             <c:if test="${board.imageList[0].imageOrder == 0}">
		            <h5>썸네일</h5>
		            <div class="img-box">
		                <div class="boardImg thumbnail">
		                    <img src="${board.imageList[0].imagePath}${board.imageList[0].imageReName}">
		                    <a href="${board.imageList[0].imagePath}${board.imageList[0].imageReName}" 
		                    	download="${board.imageList[0].imageOriginal}"
		                    >다운로드</a>         
		                </div>
		            </div>
	             </c:if>
	             
            </c:if>
            
            <%-- 썸네일이 있을 경우 --%>
            <c:if test="${board.imageList[0].imageOrder == 0}">
            	<c:set var="start" value="1" />
            </c:if>
            
            <%-- 썸네일이 없을 경우 --%>
        	<c:if test="${board.imageList[0].imageOrder != 0}">
            	<c:set var="start" value="0" />
            </c:if>

			
			<%-- fn:length(board.imageList) : imageList의 길이 반환 --%>
            <!-- 일반 이미지가 있는 경우 -->
            <c:if test="${fn:length(board.imageList) > start}">
            
	            <!-- 업로드 이미지 영역 -->
	            <h5>업로드 이미지</h5>
	            <div class="img-box">
	                
	                <c:forEach var="i" begin="${start}" end="${fn:length(board.imageList) - 1}">
		                <div class="boardImg">
		                
		                	<c:set var="path"
		                		value="${board.imageList[i].imagePath}${board.imageList[i].imageReName}" />
			 				               
				                
		                    <img src="${path}">
		                    <a href="${path}" download="${board.imageList[i].imageOriginal}">다운로드</a>                
		                </div>
	                </c:forEach>
	                
	
	            </div>
            
            </c:if>
            

            <!-- 내용 (cf) white-space: pre-wrap; 설정이 되어 있어서 div 안 공백이 있어선 안됨. -->
            <div class="board-content">${board.boardContent}</div>


            <!-- 버튼 영역-->
            <div class="board-btn-area">
            
            	<!-- 로그인한 회원과 게시글 작성자 번호가 같은 경우 -->
            	<c:if test="${loginMember.memberNo == board.memberNo}">
	                <button id="updateBtn">수정</button>
	                <button id="deleteBtn">삭제</button>
            	</c:if>


                <button id="goToListBtn">목록으로</button>
            </div>


        </section>

        <!-- 댓글 include-->
        <jsp:include page="comment.jsp"/>
    </main>

    <jsp:include page="/WEB-INF/views/common/footer.jsp"/>

	<script>
		// JSP에서 작성 가능한 언어/라이브러리
		// -> html, css, js, java, EL, JSTL
		
		// JSP 해석 우선 순위 : Java/EL/JSTL > HTML,CSS,JS
		
		const boardNo = "${board.boardNo}"
		
		const loginMemberNo = "${loginMember.memberNo}"
	
		console.log(boardNo);
		console.log(loginMemberNo);
	
	</script>
	
	<script src="/resources/js/board/boardDetail.js"></script>
	<script src="/resources/js/board/comment.js"></script>
	
</body>
</html>

comment.js

처음엔 댓글 동기식 조회 -> 댓글 등록 시에는 AJAX로 비동기식 조회

// 댓글 목록 조회
function selectCommentList(){

	// REST(REpresentational State Transfer) API 
	// - 자원을 이름(주소)으로 구분하여
	// 	 자원의 상태를 주고 받는 것
	
	// -> 주소를 명시하고
	// Http Method (GET, POST, PUT, DELETE) 를 이용해
	// 지정된 자원에 대한 CRUD 진행
	
	
    fetch("/comment?boardNo=" + boardNo)
    .then(response => response.json())
    .then(cList => {
        console.log(cList);

        // 화면에 출력되어 있는 댓글 목록 삭제
        const commentList = document.getElementById("commentList"); // ul태그
        commentList.innerHTML = "";

        // cList에 저장된 요소를 하나씩 접근
        for(let comment of cList){

            // 행
            const commentRow = document.createElement("li");
            commentRow.classList.add("comment-row");

            // 답글일 경우 child-comment 클래스 추가
            if(comment.parentNo != 0)  commentRow.classList.add("child-comment");


            // 작성자
            const commentWriter = document.createElement("p");
            commentWriter.classList.add("comment-writer");

            // 프로필 이미지
            const profileImage = document.createElement("img");

            if( comment.profileImage != null ){ // 프로필 이미지가 있을 경우
                profileImage.setAttribute("src", comment.profileImage);
            }else{ // 없을 경우 == 기본이미지
                profileImage.setAttribute("src", "/resources/images/user.png");
            }

            // 작성자 닉네임
            const memberNickname = document.createElement("span");
            memberNickname.innerText = comment.memberNickname;
            
            // 작성일
            const commentDate = document.createElement("span");
            commentDate.classList.add("comment-date");
            commentDate.innerText =  "(" + comment.commentCreateDate + ")";

            // 작성자 영역(p)에 프로필,닉네임,작성일 마지막 자식으로(append) 추가
            commentWriter.append(profileImage , memberNickname , commentDate);

            

            // 댓글 내용
            const commentContent = document.createElement("p");
            commentContent.classList.add("comment-content");
            commentContent.innerHTML = comment.commentContent;

            // 행에 작성자, 내용 추가
            commentRow.append(commentWriter, commentContent);

            
            // 로그인이 되어있는 경우 답글 버튼 추가
            if(loginMemberNo != ""){
                // 버튼 영역
                const commentBtnArea = document.createElement("div");
                commentBtnArea.classList.add("comment-btn-area");

                // 답글 버튼
                const childCommentBtn = document.createElement("button");
                childCommentBtn.setAttribute("onclick", "showInsertComment("+comment.commentNo+", this)");
                childCommentBtn.innerText = "답글";

                // 버튼 영역에 답글 버튼 추가
                commentBtnArea.append(childCommentBtn);

                // 로그인한 회원번호와 댓글 작성자의 회원번호가 같을 때만 버튼 추가
                if( loginMemberNo == comment.memberNo   ){

                    // 수정 버튼
                    const updateBtn = document.createElement("button");
                    updateBtn.innerText = "수정";

                    // 수정 버튼에 onclick 이벤트 속성 추가
                    updateBtn.setAttribute("onclick", "showUpdateComment("+comment.commentNo+", this)");                        


                    // 삭제 버튼
                    const deleteBtn = document.createElement("button");
                    deleteBtn.innerText = "삭제";
                    // 삭제 버튼에 onclick 이벤트 속성 추가
                    deleteBtn.setAttribute("onclick", "deleteComment("+comment.commentNo+")");                       


                    // 버튼 영역 마지막 자식으로 수정/삭제 버튼 추가
                    commentBtnArea.append(updateBtn, deleteBtn);

                } // if 끝
                

                // 행에 버튼영역 추가
                commentRow.append(commentBtnArea); 
            }
            

            // 댓글 목록(ul)에 행(li)추가
            commentList.append(commentRow);
        }


    })
    .catch(err => console.log(err));

}


//-------------------------------------------------------------------------------------------------


// 댓글 등록
const addComment = document.getElementById("addComment");
const commentContent = document.getElementById("commentContent");

addComment.addEventListener("click", e => { // 댓글 등록 버튼이 클릭이 되었을 때

    // 1) 로그인이 되어있나? -> 전역변수 memberNo 이용
    if(loginMemberNo == ""){ // 로그인 X
        alert("로그인 후 이용해주세요.");
        return;
    }

    // 2) 댓글 내용이 작성되어있나?
    if(commentContent.value.trim().length == 0){ // 미작성인 경우
        alert("댓글을 작성한 후 버튼을 클릭해주세요.");

        commentContent.value = ""; // 띄어쓰기, 개행문자 제거
        commentContent.focus();
        return;
    }

    // 3) AJAX를 이용해서 댓글 내용 DB에 저장(INSERT)
    fetch()
    .then()
    .then(result => {
        if(result > 0){ // 등록 성공
            alert("댓글이 등록되었습니다.");

            commentContent.value = ""; // 작성했던 댓글 삭제

            selectCommentList(); // 비동기 댓글 목록 조회 함수 호출
            // -> 새로운 댓글이 추가되어짐

        } else { // 실패
            alert("댓글 등록에 실패했습니다...");
        }
    })
    .catch(err => console.log(err));
});


// -----------------------------------------------------------------------------------
// 댓글 삭제
function deleteComment(commentNo){

    if( confirm("정말로 삭제 하시겠습니까?") ){

        fetch()
        .then()
        .then(result => {
            if(result > 0){
                alert("삭제되었습니다");
                selectCommentList(); // 목록을 다시 조회해서 삭제된 글을 제거
            }else{
                alert("삭제 실패");
            }
        })
        .catch(err => console.log(err));

    }
}




// ------------------------------------------------------------------------------------------
// 댓글 수정 화면 전환 

let beforeCommentRow; // 수정 전 원래 행의 상태를 저장할 변수


function showUpdateComment(commentNo, btn){
                     // 댓글번호, 이벤트발생요소(수정버튼)

    // ** 댓글 수정이 한 개만 열릴 수 있도록 만들기 **
    // 댓글 수정을 위한 textarea를 모두 얻어옴 -> 수정이 활성화 되어 있을 경우 1개, 없으면 0개
    const temp = document.getElementsByClassName("update-textarea");  

    if(temp.length > 0){ // 수정이 한 개 이상 열려 있는 경우

        if(confirm("다른 댓글이 수정 중입니다. 현재 댓글을 수정 하시겠습니까?")){ // 확인

            temp[0].parentElement.innerHTML = beforeCommentRow;
            // comment-row                       // 백업한 댓글
            // 백업 내용으로 덮어 씌워 지면서 textarea 사라짐
       
        }else{ // 취소
            return;
        }
    }


    // 1. 댓글 수정이 클릭된 행을 선택
    const commentRow = btn.parentElement.parentElement; // 수정 버튼의 부모의 부모

    // 2. 행 내용 삭제 전 현재 상태를 저장(백업) (문자열)
    //    (전역변수 이용)
    beforeCommentRow = commentRow.innerHTML;


    // 3. 댓글에 작성되어 있던 내용만 얻어오기 -> 새롭게 생성된 textarea 추가될 예정
    
    let beforeContent = commentRow.children[1].innerHTML;

    // 이것도 가능!
    //let beforeContent = btn.parentElement.previousElementSibling.innerHTML;


    // 4. 댓글 행 내부 내용을 모두 삭제
    commentRow.innerHTML = "";

    // 5. textarea 요소 생성 + 클래스 추가  +  **내용 추가**
    const textarea = document.createElement("textarea");
    textarea.classList.add("update-textarea");

    // ******************************************
    // XSS 방지 처리 해제
    beforeContent =  beforeContent.replaceAll("&amp;", "&");
    beforeContent =  beforeContent.replaceAll("&lt;", "<");
    beforeContent =  beforeContent.replaceAll("&gt;", ">");
    beforeContent =  beforeContent.replaceAll("&quot;", "\"");
    
    // ******************************************
    textarea.value = beforeContent; // 내용 추가

    // 6. commentRow에 생성된 textarea 추가
    commentRow.append(textarea);


    // 7. 버튼 영역 + 수정/취소 버튼 생성
    const commentBtnArea = document.createElement("div");
    commentBtnArea.classList.add("comment-btn-area");
    

    const updateBtn = document.createElement("button");
    updateBtn.innerText = "수정";
    updateBtn.setAttribute("onclick", "updateComment("+commentNo+", this)");


    const cancelBtn = document.createElement("button");
    cancelBtn.innerText = "취소";
    cancelBtn.setAttribute("onclick", "updateCancel(this)");


    // 8. 버튼영역에 버튼 추가 후 
    //    commentRow(행)에 버튼영역 추가
    commentBtnArea.append(updateBtn, cancelBtn);
    commentRow.append(commentBtnArea);

}


// -----------------------------------------------------------------------------------
// 댓글 수정 취소
function updateCancel(btn){
    // 매개변수 btn : 클릭된 취소 버튼
    // 전역변수 beforeCommentRow : 수정 전 원래 행(댓글)을 저장한 변수

    if(confirm("댓글 수정을 취소하시겠습니까?")){
        btn.parentElement.parentElement.innerHTML = beforeCommentRow;
    }
}

// -----------------------------------------------------------------------------------
// 댓글 수정(AJAX)
function updateComment(commentNo, btn){

    // 새로 작성된 댓글 내용 얻어오기
    const commentContent = btn.parentElement.previousElementSibling.value;

    fetch()
    .then()
    .then(result => {
        if(result > 0){
            alert("댓글이 수정되었습니다.");
            selectCommentList();
        }else{
            alert("댓글 수정 실패");
        }
    })
    .catch(err => console.log(err));

}

// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------

// 답글 작성 화면 추가 
// -> 답글 작성 화면은 전체 화면에 1개만 존재 해야한다!

function showInsertComment(parentNo, btn){
                        // 부모 댓글 번호, 클릭한 답글 버튼


    // ** 답글 작성 textarea가 한 개만 열릴 수 있도록 만들기 **
    const temp = document.getElementsByClassName("commentInsertContent");

    if(temp.length > 0){ // 답글 작성 textara가 이미 화면에 존재하는 경우

        if(confirm("다른 답글을 작성 중입니다. 현재 댓글에 답글을 작성 하시겠습니까?")){
            temp[0].nextElementSibling.remove(); // 버튼 영역부터 삭제
            temp[0].remove(); // textara 삭제 (기준점은 마지막에 삭제해야 된다!)
        
        } else{
            return; // 함수를 종료시켜 답글이 생성되지 않게함.
        }
    }
    
    // 답글을 작성할 textarea 요소 생성
    const textarea = document.createElement("textarea");
    textarea.classList.add("commentInsertContent");
    
    // 답글 버튼의 부모의 뒤쪽에 textarea 추가
    // after(요소) : 뒤쪽에 추가
    btn.parentElement.after(textarea);


    // 답글 버튼 영역 + 등록/취소 버튼 생성 및 추가
    const commentBtnArea = document.createElement("div");
    commentBtnArea.classList.add("comment-btn-area");


    const insertBtn = document.createElement("button");
    insertBtn.innerText = "등록";
    insertBtn.setAttribute("onclick", "insertChildComment("+parentNo+", this)");


    const cancelBtn = document.createElement("button");
    cancelBtn.innerText = "취소";
    cancelBtn.setAttribute("onclick", "insertCancel(this)");

    // 답글 버튼 영역의 자식으로 등록/취소 버튼 추가
    commentBtnArea.append(insertBtn, cancelBtn);

    // 답글 버튼 영역을 화면에 추가된 textarea 뒤쪽에 추가
    textarea.after(commentBtnArea);

}


// 답글 취소
function insertCancel(btn){
                    // 취소
    btn.parentElement.previousElementSibling.remove(); // 취소의 부모의 이전 요소(textarea) 제거
    btn.parentElement.remove(); // 취소의 부모 요소(comment-btn-area) 제거
}


// 답글 등록
function insertChildComment(parentNo, btn){
                        // 부모 댓글 번호, 답글 등록 버튼

    // 누가?                loginMemberNo(로그인한 회원의 memberNo )(전역변수)
    // 어떤 내용?           textarea에 작성된 내용
    // 몇번 게시글?         현재 게시글 boardNo (전역변수)
    // 부모 댓글은 누구?    parentNo (매개변수)

    // 답글 내용
    const commentContent = btn.parentElement.previousElementSibling.value;

    // 답글 내용이 작성되지 않은 경우
    if(commentContent.trim().length == 0){
        alert("답글 작성 후 등록 버튼을 클릭해주세요.");
        btn.parentElement.previousElementSibling.value = "";
        btn.parentElement.previousElementSibling.focus();
        return;
    }



    fetch()
    .then()
    .then(result => {
        if(result > 0){ // 등록 성공
            alert("답글이 등록되었습니다.");
            selectCommentList(); // 비동기 댓글 목록 조회 함수 호출

        } else { // 실패
            alert("답글 등록에 실패했습니다...");
        }
    })
    .catch(err => console.log(err));


}

CommentController.java

package edu.kh.project.board.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import edu.kh.project.board.model.dto.Comment;
import edu.kh.project.board.model.service.CommentService;

// @Controller + @ResponseBody

@RestController // 요청/응답 처리(단, 모든 요청 응답은 비동기)
				// -> REST API를 구축하기 위한 Controller
public class CommentController {

	@Autowired
	private CommentService service;
	
	// 댓글 목록 조회					// json 통신 시 한글깨짐 방지
	@GetMapping(value="/comment", produces="application/json; charset=UTF-8")
	public List<Comment> select(int boardNo) {
		
		return service.select(boardNo);
		// 동기 시 return : forward / redirect
		// 비동기 시 return : 값 자체
	}
	
	// 댓글 삽입
	
	
	
	// 댓글 삭제
	
	
	
	// 댓글 수정
	
	
}

댓글 목록 조회, 삽입, 삭제, 수정 내일까지 과제!

CF ) 댓글 삽입 시, 참고하면 유용한 사이트
https://mybatis.org/mybatis-3/ko/dynamic-sql.html

0개의 댓글