Spring Boot Board Project_19 게시판 목록 조회

송지윤·2024년 4월 23일
0

Spring Framework

목록 보기
52/65

게시글 이미지 올리기 위해서 필요한 설정

1. 연결해주기 config.properties fileConfig

config.properties

# 게시글 이미지 요청 주소 : 클라이언트가 어떤 이미지를 보고싶어 할 때 요청 보낼 주소
my.board.resource-handler=/images/board/**

# 게시글 이미지 요청 시 연결할 서버 폴더 경로
my.board.resource-location=file:///C:/uploadFiles/board/

# 게시글 이미지 요청 주소 (DB 저장용)
my.board.web-path=/images/board/

# 게시글 이미지를 서버에 저장할 때 사용할 경로 ( transferTo() )
my.board.folder-path=C:/uploadFiles/board/

FileConfig

	// 게시판 이미지
	@Value("${my.board.resource-handler}")
	private String boardResourceHandler; // 요청 주소
	
	@Value("${my.board.resource-location}")
	private String boardResourceLocation; // 연결될 서버 폴더 경로

	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		
		// 게시글 이미지 요청 - 서버 폴더 연결 추가
		registry.addResourceHandler(boardResourceHandler)
		.addResourceLocations(boardResourceLocation);
		
	}

기존에 있던 메서드에 게시글 이미지 요청 -서버 폴더 연결 추가 하는 구문 추가 작성

2. DB 테이블 샘플 데이터 넣어두기

/* BOARD_IMG 테이블용 시퀀스 생성 */
CREATE SEQUENCE SEQ_IMG_NO NOCACHE;

/* BOARD_IMG 테이블에 샘플 데이터 삽입 */
INSERT INTO "BOARD_IMG" VALUES(
	SEQ_IMG_NO.NEXTVAL, '/images/board/', '원본1.jpg', 'test1.jpg', 0, 1950
);

INSERT INTO "BOARD_IMG" VALUES(
	SEQ_IMG_NO.NEXTVAL, '/images/board/', '원본2.jpg', 'test2.jpg', 1, 1950
);

INSERT INTO "BOARD_IMG" VALUES(
	SEQ_IMG_NO.NEXTVAL, '/images/board/', '원본3.jpg', 'test3.jpg', 2, 1950
);

INSERT INTO "BOARD_IMG" VALUES(
	SEQ_IMG_NO.NEXTVAL, '/images/board/', '원본4.jpg', 'test4.jpg', 3, 1950
);

INSERT INTO "BOARD_IMG" VALUES(
	SEQ_IMG_NO.NEXTVAL, '/images/board/', '원본5.jpg', 'test5.jpg', 4, 1950
);


COMMIT;

3. 게시글 제목 클릭하면 상세 조회로 연결

boardList.html

							<tr th:each="board : ${boardList}" th:object="${board}">
									
								<td th:text="*{boardNo}">게시글 번호</td>
								
								<td>
									<!-- 썸네일 추가 예정 -->
	
									<a th:text="*{boardTitle}"
									th:href="@{/board/{boardCode}/{boardNo}(boardCode=${boardCode}, boardNo=*{boardNo}, cp=${pagination.currentPage})}">게시글 제목</a>
	
									<th:block th:text="|[*{commentCount}]|">댓글 수</th:block>
								</td>
	
								<!-- 작성자 닉네임 -->
								<td th:text="*{memberNickname}">닉네임</td>
	
								<!-- 작성일 -->
								<td th:text="*{boardWriteDate}">2023-10-26</td>
	
								<!-- 조회수 -->
								<td th:text="*{readCount}">0</td>
	
								<!-- 좋아요 수 -->
								<td th:text="*{likeCount}">0</td>
	
							</tr>

href () 안에서 {}로 자리 마련해둔 거 뒤에 , 뒤에 적힌 값은 쿼리스트링으로 넘어감
뒤에 cp 는 상세조회에서 목록으로 버튼 눌렀을 때 돌아오기 위해서

주소 확인 http://localhost/board/1/1950?cp=2

4. 상세페이지로 보내줄 Controller @PathVariable

	@GetMapping("{boardCode:[0-9]+}/{boardNo:[0-9]+}")
	public String boardDetail(@PathVariable("boardCode") int boardCode,
			@PathVariable("boardNo") int boardNo,
			Model model,
			RedirectAttributes ra) {
		
		return "board/boardDetail"; // board/boardDetail.html 로 forward
	}

5. 돌아올 값이 뭔지 보기 위해 SQL 문 먼저 작성

내용이 복잡해서 3개로 나눠서 조회

-------------------------------------------------------
/* 게시글 상세 조회 */
SELECT BOARD_NO, BOARD_TITLE, BOARD_CONTENT, BOARD_CODE, READ_COUNT,
	MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG,
	TO_CHAR(BOARD_WRITE_DATE, 'YYYY"년" MM"월" DD"일" HH24:MI:SS') BOARD_WRITE_DATE, 
	TO_CHAR(BOARD_UPDATE_DATE, 'YYYY"년" MM"월" DD"일" HH24:MI:SS') BOARD_UPDATE_DATE,
	(SELECT COUNT(*)
	 FROM "BOARD_LIKE"
	 WHERE BOARD_NO = 1950) LIKE_COUNT,
	(SELECT IMG_PATH || IMG_RENAME
	 FROM "BOARD_IMG"
	 WHERE BOARD_NO = 1950
	 AND   IMG_ORDER = 0) THUMBNAIL
FROM "BOARD"
JOIN "MEMBER" USING(MEMBER_NO)
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = 1
AND BOARD_NO = 1950;

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

/* 상세조회 되는 게시글의 모든 이미지 조회 */
SELECT *
FROM "BOARD_IMG"
WHERE BOARD_NO = 1950
ORDER BY IMG_ORDER;

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

/* 상세조회 되는 게시글의 모든 댓글 조회 */
/*계층형 쿼리*/
SELECT LEVEL, C.* FROM
	(SELECT COMMENT_NO, COMMENT_CONTENT,
	    TO_CHAR(COMMENT_WRITE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') COMMENT_WRITE_DATE,
	    BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_COMMENT_NO, COMMENT_DEL_FL
	FROM "COMMENT"
	JOIN MEMBER USING(MEMBER_NO)
	WHERE BOARD_NO = 1950) C
WHERE COMMENT_DEL_FL = 'N'
OR 0 != (SELECT COUNT(*) FROM "COMMENT" SUB
				WHERE SUB.PARENT_COMMENT_NO = C.COMMENT_NO
				AND COMMENT_DEL_FL = 'N')
START WITH PARENT_COMMENT_NO IS NULL
CONNECT BY PRIOR COMMENT_NO = PARENT_COMMENT_NO
ORDER SIBLINGS BY COMMENT_NO;

계층형 쿼리 (계단식 구조 부모 댓글, 자식 댓글)
SELECT LEVEL

  • START WITH PARENT_COMMENT_NO IS NULL
    -> 부모 댓글이 없는 애들을 1레벨로 사용

  • CONNECT BY PRIOR COMMENT_NO = PARENT_COMMENT_NO
    -> 누군가의 PARENT_COMMNET_NO 가 COMMENT_NO 라면 그 아래 레벨로 삼겠다.(대댓글)

부모 아래 자식 계층

  • SIBLINGS 형제
    ORDER SIBLINGS BY COMMENT_NO; -> 같은 레벨 안에서 COMMENT_NO 순서로 정렬

inline view = 조회된 결과를 하나의 테이블처럼 쓰는 것
C.* : C 테이블에 대한 모든 것

6. 보드 이미지에 대한 값을 담아줄 DTO 생성 + 댓글 DTO 생성

BoardImg 클래스

import org.springframework.web.multipart.MultipartFile;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Builder
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
public class BoardImg {

	private int imgNo;
	private String imgPath;
	private String imgOriginalName;
	private String imgRename;
	private int imgOrder;
	private int boardNo;
	
	// 게시글 이미지 삽입/수정할 때 사용
	private MultipartFile uploadFile;
}

Comment 클래스

@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Comment {

	private int commentNo;
	private String commentContent;
	private String commentWriteDate;
	private String commentDelFl;
	private int boardNo;
	private int memberNo;
	private int parentCommentNo;
	
	// 댓글 조회 시 회원 프로필, 닉네임
	private String memberNickname;
	private String profileImg;
}

Board 클래스 필드 추가

	// 특정 게시글 이미지 목록 (xml collection property와 똑같이)
	private List<BoardImg> imageList;
	
	// 특정 게시글에 작성된 댓글 목록
	private List<Comment> commentList;

7. Controller 로 돌아와 게시글 상세조회 Service 호출

BoardController

	@GetMapping("{boardCode:[0-9]+}/{boardNo:[0-9]+}")
	public String boardDetail(@PathVariable("boardCode") int boardCode,
			@PathVariable("boardNo") int boardNo,
			Model model,
			RedirectAttributes ra) {
		
		// 게시글 상세 조회 서비스 호출
		
		// 1) Map 으로 전달할 파라미터 묶기
		Map<String, Integer> map = new HashMap<>();
		map.put("boardCode", boardCode);
		map.put("boardNo", boardNo);
		
		// 2) 서비스 호출 (하나의 게시글 돌려받을 거임)
		Board board = service.selectOne(map);

동적 SQL 이용 (1번의 mapper 호출 -> 3번의 SELECT)

이전에는 SQL 여러 개면 mapper 여러 번 호출해서 사용
Service 단에서 1번의 mapper 호출로 3개 SELECT 조회

수행하려는 SQL이
1) 모두 SELECT 이면서
2) 먼저 조회된 결과 중 일부를 이용해서 나중에 수행되는 SQL의 조건으로 삼을 수 있을 때
--> Mybatis 에서 제공하는 태그 중 <resultMap>, resultMap 안에서 사용하는 <collection> 태그를 이용해서 Mapper 메서드 1회 호출로 여러 SELECT 한 번에 수행 가능

8. Service 에서 mapper 호출하기

BoardServiceImpl

	@Override
	public Board selectOne(Map<String, Integer> map) {
		
		return mapper.selectOne(map);
	}

9. mapper.xml SQL 작성 resultMap 사용

resultMap 태그

1) 조회된 컬럼명과 DTO의 필드명이 일치하지 않을 때
매핑(연결) 시켜주는 역할

사용 예)

	<resultMap type="Board" id="board_rm">
		<result javaType="title" column="BOARD_TITLE"/>
	</resultMap>

2) <collection> 태그를 추가 작성하여 여러 행 결과가 조회되는 다른 SELECT를 수행한 후 그 결과를 지정된 DTO의 필드에 대입

type 속성 : 연결할 DTO 경로 또는 별칭
id 속성 : 해당 태그를 식별할 값(이름 지정)

	<!-- 게시글 상세 조회 -->
	<select id="selectOne" resultMap="board_rm">
		SELECT BOARD_NO, BOARD_TITLE, BOARD_CONTENT, BOARD_CODE, READ_COUNT,
			MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG,
			TO_CHAR(BOARD_WRITE_DATE, 'YYYY"년" MM"월" DD"일" HH24:MI:SS') BOARD_WRITE_DATE, 
			TO_CHAR(BOARD_UPDATE_DATE, 'YYYY"년" MM"월" DD"일" HH24:MI:SS') BOARD_UPDATE_DATE,
			(SELECT COUNT(*)
			 FROM "BOARD_LIKE"
			 WHERE BOARD_NO = #{boardNo}) LIKE_COUNT,
			(SELECT IMG_PATH || IMG_RENAME
			 FROM "BOARD_IMG"
			 WHERE BOARD_NO = #{boardNo}
			 AND   IMG_ORDER = 0) THUMBNAIL
		FROM "BOARD"
		JOIN "MEMBER" USING(MEMBER_NO)
		WHERE BOARD_DEL_FL = 'N'
		AND BOARD_CODE = #{boardCode}
		AND BOARD_NO = #{boardNo}
	</select>

수행한 결과값 가지고

resultMap 의 id 값을 써줌
resultMap 은 최상단에 만듦

collection 태그

select로 조회된 결과를 컬렉션(List)에 담아 지정된 필드에 세팅

property : List를 담을 DTO의 필드명
select : 실행할 select의 id
column : 조회 결과 중 지정된 컬럼의 값을 파라미터로 전달
javaType : List(컬렉션)의 타입을 지정
ofType : List(컬렉션)의 제네릭(타입 제한) 지정

	<resultMap type="Board" id="board_rm">
	
		<!-- id 태그 : PK 역할을 하는 컬럼, 필드를 작성하는 태그 -->
		<id property="boardNo" column="BOARD_NO" />
		
		<!-- 해당 게시글 이미지 목록 조회 후 필드에 저장 -->
		<collection 
			property="imageList"
			select="selectImageList"
			column="BOARD_NO"
			javaType="java.util.ArrayList"
			ofType="BoardImg" />
	</resultMap>

2번째 SQL문

	<!-- 상세 조회한 게시글의 이미지 목록 조회 (resultType 꼭 써줘야함) -->
	<select id="selectImageList" resultType="BoardImg">
		SELECT *
		FROM "BOARD_IMG"
		WHERE BOARD_NO = #{boardNo}
		ORDER BY IMG_ORDER
	</select>

10. xml 에서 selectImageList 조회한 결과를 담아줄 DTO (Board 클래스) 필드 추가

Board

	// 특정 게시글 이미지 목록 (xml collection property와 똑같이)
	private List<BoardImg> imageList;
	
	// 특정 게시글에 작성된 댓글 목록
	private List<Comment> commentList;

3번째 SQL문

	<!-- 상세 조회한 게시글의 댓글 목록 조회 -->
	<select id="selectCommentList" resultType="Comment">
		SELECT LEVEL, C.* FROM
			(SELECT COMMENT_NO, COMMENT_CONTENT,
			    TO_CHAR(COMMENT_WRITE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') COMMENT_WRITE_DATE,
			    BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_COMMENT_NO, COMMENT_DEL_FL
			FROM "COMMENT"
			JOIN MEMBER USING(MEMBER_NO)
			WHERE BOARD_NO = #{boardNo}) C
		WHERE COMMENT_DEL_FL = 'N'
		OR 0 != (SELECT COUNT(*) FROM "COMMENT" SUB
						WHERE SUB.PARENT_COMMENT_NO = C.COMMENT_NO
						AND COMMENT_DEL_FL = 'N')
		START WITH PARENT_COMMENT_NO IS NULL
		CONNECT BY PRIOR COMMENT_NO = PARENT_COMMENT_NO
		ORDER SIBLINGS BY COMMENT_NO
	</select>

collection 추가

		<collection 
			property="commentList"
			select="selectCommentList"
			column="BOARD_NO"
			javaType="java.util.ArrayList"
			ofType="Comment" />

11. SQL 문 실행 후 반환 받은 값 Controller 에서 처리해주기

		String path = null;
		
		// 조회 결과가 없는 경우
		if(board == null) {
			path = "redirect:/board/" + boardCode; // 목록 재요청
			ra.addFlashAttribute("message", "게시글이 존재하지 않습니다.");
			
		// 조회 결과가 있는 경우
		} else {
			path = "board/boardDetail"; // board/boardDetail.html 로 forward
			
			// board - 게시글 일반내용 + imageList + commentList
			model.addAttribute("board", board);
			
			// 이미지가 없는 글일 수 도 있음
			// 조회된 이미지 목록(imageList)가 있을 경우
			if(!board.getImageList().isEmpty()) {
				
				BoardImg thumbnail = null;
				
				// imageList 의 0번 인덱스 == 가장 빠른 순서(imgOrder)
				
				// 이미지 목록의 첫번째 행이 순서 0 == 썸네일인 경우
				if(board.getImageList().get(0).getImgOrder() == 0) {
					
					thumbnail = board.getImageList().get(0);
				}
				
				model.addAttribute("thumbnail", thumbnail);
				model.addAttribute("start", thumbnail != null ? 1 : 0);
			}
			
		}
		
		return path; 
	}

12. 받아온 값 Thymeleaf 이용해서 html 에 값 세팅

comment.html

<div id="commentArea">
	<!-- 댓글 목록 -->
	<div class="comment-list-area">

		<ul id="commentList">

			<!-- 대댓글(자식)인 경우 child-comment 클래스 추가 -->
			<li class="comment-row" 
					th:each="comment : ${board.commentList}" 
					th:classappend="${comment.parentCommentNo} != 0 ? child-comment"
					th:object="${comment}">

				<th:block th:if="*{commentDelFl} == 'Y'">
					삭제된 댓글 입니다
				</th:block>

				<th:block th:if="*{commentDelFl} == 'N'">
					<p class="comment-writer">
						<!-- 프로필 이미지 없을 경우 -->
						<img th:unless="*{profileImg}" th:src="#{user.default.image}">
						<!-- 프로필 이미지 있을 경우 -->
						<img th:if="*{profileImg}" th:src="*{profileImg}">
	
						<span th:text="*{memberNickname}">닉네임</span>
						<span class="comment-date" th:text="*{commentWriteDate}">작성일</span>
					</p>
	
					<p class="comment-content" th:text="*{commentContent}">댓글 내용</p>
	
					<!-- 버튼 영역 -->
					<div class="comment-btn-area">
						<button th:onclick="|showInsertComment(*{commentNo}, this)|">답글</button>
	
						<th:block th:if="${session.loginMember != null and session.loginMember.memberNo == comment.memberNo}">
							<button th:onclick="|showUpdateComment(*{commentNo}, this)|">수정</button>
							<button th:onclick="|deleteComment(*{commentNo})|">삭제</button>
						</th:block>
						<!-- 로그인 회원과 댓글 작성자가 같은 경우 -->
	
					</div>
				</th:block>

			</li>

		</ul>
	</div>


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

</div>

boardDetail.html

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<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 th:text="|${board.boardNo}번 글|">게시판 이름</title>

    <th:block th:replace="~{common/common}"></th:block>

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

</head>
<body>
    <main>
        <th:block th:replace="~{common/header}"></th:block>

        <section class="board-detail" th:object="${board}">  
            <!-- 제목 -->
            <h1 class="board-title"
            th:text="*{boardTitle}">  게시글 제목  </h1>

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

                    <!-- 프로필 이미지 -->

                    <!-- 프로필 이미지가 없을 경우 기본 이미지 출력 -->
                    <img th:unless="*{profileImg}"
                    th:src="#{user.default.image}">
                    
                    <!-- 프로필 이미지가 있을 경우 출력-->
                    <img th:if="*{profileImg}"
                    th:src="*{profileImg}">

                    <span th:text="*{memberNickname}">작성자 닉네임</span>

                    
                    <!-- 좋아요 하트 -->
                    <span class="like-area">
                        
                        <!-- 비동기로 좋아요 누를 때 동작 (4/22 월요일 예정) -->
                        <!-- 좋아요 누른적이 있으면 fa-solid, 없으면 fa-regular 클래스 추가 -->
                        <i class="fa-heart fa-regular" id="boardLike"></i>

                        <!-- 좋아요 개수 -->
                        <span th:text="*{likeCount}">0</span>
                    </span>

                </div>

                <div class="board-info">
                    <p> <span>작성일</span>[[*{boardWriteDate}]]</p>     

                    <!-- 수정한 게시글인 경우 -->

                    <!-- 참조하는 객체가 있으면 true, 없으면 false -->
                    <p th:if="*{boardUpdateDate}"> 
                        <span>마지막 수정일</span> [[*{boardUpdateDate}]]
                    </p>   

                    <!--  (4/22 월요일 예정) -->
                    <p> <span>조회수</span> [[*{readCount}]]</p>                    
                </div>
            </div>

            <!-- ====================== 이미지가 있을 경우 출력하는 구문  ====================== -->

            <th:block th:if="${ #lists.size(board.imageList) > 0}">

                <!-- 썸네일이 있을 경우 -->
                <th:block th:if="*{thumbnail}">
                    <!-- board DTO 에 thumbnail DB 조회 /images/board/test1.jpg -->
                
                    <h5>썸네일</h5>
                    <div class="img-box">
                        <div class="boardImg thumbnail">
                            <img th:src="|${thumbnail.imgPath}${thumbnail.imgRename}|">                     
                            <!-- BoardImg 이용해 setting 한 이미지 imgPath == /images/board imgRename == /test1.jpg -->
                            
                            <a th:href="|${thumbnail.imgPath}${thumbnail.imgRename}|"
                                th:download="${thumbnail.imgOriginalName}">다운로드</a>
                        </div>
                    </div>
                
                </th:block>

                <th:block th:if="${#lists.size(board.imageList) > start}">
                <!-- 썸네일이 있으면 start = 1 / 썸네일이 없다면 start = 0 -->

                    <h5>업로드 이미지</h5>

                    <th:block th:each="i : ${#numbers.sequence(start, #lists.size(board.imageList) - 1)}">

                        <div class="img-box">
                            <div class="boardImg">
        
                                <img th:src="|${board.imageList[i].imgPath}${board.imageList[i].imgRename}|">  
        
                                <a th:href="|${board.imageList[i].imgPath}${board.imageList[i].imgRename}|"
                                    th:download="${board.imageList[i].imgOriginalName}">다운로드</a>                
                            </div>
                        </div>

                    </th:block>

                </th:block>

            </th:block>   
            <!-- ====================== 이미지가 있을 경우 출력하는 구문 ====================== -->
            
            <!-- 내용 -->
            <div class="board-content" th:text="*{boardContent}">내용</div>

            <!-- 버튼 영역-->
            <div class="board-btn-area">
                
                <!-- 로그인한 회원과 게시글을 작성한 회원의 번호가 같은 경우 -->
                <!-- <th:block th:if="${session.loginMember != null and board.memberNo == session.loginMember.memberNo}"> -->
                <!-- <th:block th:if="${board.memberNo == session.loginMember?.memberNo}"> 안전 탐색 연산자 null 이 아닌 경우에만 memberNo 접근 -->
                <th:block th:if="${board.memberNo == session.loginMember?.memberNo}">
                    <button id="updateBtn">수정</button>
                    <button id="deleteBtn">삭제(GET)</button>
                    <button id="deleteBtn2">삭제(POST)</button>
                </th:block>
				
                <button id="goToListBtn">목록으로</button>
				
            </div>

        </section>

        <!-- 댓글 영역-->
        <th:block th:replace="~{board/comment}"></th:block>

    </main>

    <th:block th:replace="~{common/footer}"></th:block>

</body>
</html>

0개의 댓글