Spring Framework (15) 게시판 detail - Day 62

jaehoon ahn·2025년 12월 16일

Spring boot

목록 보기
13/20
post-thumbnail

검색기능

Controller

  • service에게 넘겨줄 때,
    • paramMap은 {key=w, query=짱구, boardCode=1}
      • key는 제목, 작성자 등 타입 선택
      • query = 검색어
      • boardCode : 게시판 유형
    • map = service.searchList(paramMap, cp);
      • 현재 페이지(cp) 필요
// 검색으로 조회할 경우 -> paramMap에 key라는 k에 접근하면 매핑된 value 반환
			// -> ex) {key=w, query=짱구}
			// -> w 반환됨

			// boardCode를 paramMap에 추가
			paramMap.put("boardCode", boardCode);
			// -> paramMap은 {key=w, query=짱구, boardCode=1}
			// 검색(내가 검색하고 싶은 게시글 목록 조회) 서비스 호출
			map = service.searchList(paramMap, cp);

Service

@Override
	public Map<String, Object> searchList(Map<String, Object> paramMap, int cp) {
		// 1. 지정된 게시판(boardCode)에서
		// 검색 조건에 맞으면서
		// 삭제되지 않은 게시글 수를 조회
		
		int listCount = mapper.getSearchCount(paramMap);
		
		// 2. 1번의 결과 + cp를 이용해서
		// Pagination 객체 생성
		Pagination pagination = new Pagination(cp, listCount);
		
		// 3. 특정 게시판의 지정된 페이지 목록 조회 (검색 포함)
		int limit = pagination.getLimit();
		int offset = (cp - 1) * limit;
		RowBounds rowBounds = new RowBounds(offset, limit);
		
		List<Board> boardList = mapper.selectSearchList(paramMap, rowBounds);
		
		// 4. 검색 목록 조회 결과 + Pagination 객체를 Map으로 묶음
		
		Map<String, Object> map = new HashMap<>();
		map.put("pagination", pagination);
		map.put("boardList", boardList);
		
		return map;
	}

mapper.xml

<!-- 검색 조건이 맞는 게시글 수 조회 -->
   <select id="getSearchCount">
      SELECT COUNT(*)
      FROM "BOARD"
      <!-- 작성자 검색인 경우 -->
      <if test='key == "w"'>
         JOIN "MEMBER" USING(MEMBER_NO)
      </if>
      
	      WHERE BOARD_DEL_FL = 'N'
	      AND BOARD_CODE = #{boardCode}
      <choose>
         <!-- 제목 검색 (key 값 "t") -->
         <when test='key == "t"'>
            AND BOARD_TITLE LIKE '%' || #{query} || '%'
         </when>
         
         <!-- 내용 검색 (key 값 "c") -->
         <when test='key == "t"'>
            AND BOARD_TITLE LIKE '%' || #{query} || '%'
         </when>
         
         <!-- 제목+내용 검색 (key 값 "tc") -->
         <when test='key == "t"'>
            AND (
               BOARD_CONTENT LIKE '%' || #{query} || '%'
               OR
               BOARD_TITLE LIKE '%' || #{query} || '%'
            )
         </when>
         
         <!-- 작성자 검색 (key 값 "w") -->
         <otherwise>
            AND MEMBER_NICKNAME LIKE '%' || #{query} || '%'
         </otherwise>
      </choose>
   </select>
	<!-- 검색 결과 목록 조회 -->
   <select id="selectSearchList">
   		SELECT BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME,
		READ_COUNT,
		(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,
		<![CDATA[
		CASE
		WHEN SYSDATE - BOARD_WRITE_DATE < 1 / 24 / 60
		THEN FLOOR((SYSDATE - BOARD_WRITE_DATE) * 24 * 60 * 60) || '초 전'
		WHEN SYSDATE - BOARD_WRITE_DATE < 1 / 24
		THEN FLOOR((SYSDATE - BOARD_WRITE_DATE) * 24 * 60) || '분 전'
		WHEN SYSDATE - BOARD_WRITE_DATE < 1
		THEN FLOOR((SYSDATE - BOARD_WRITE_DATE) * 24) || '시간 전'
		ELSE TO_CHAR(BOARD_WRITE_DATE, 'YYYY-MM-DD')
		END BOARD_WRITE_DATE
		]]>
		FROM "BOARD" B
		JOIN "MEMBER" USING(MEMBER_NO)
		WHERE BOARD_DEL_FL = 'N'
		AND BOARD_CODE = #{boardCode}
		<choose>
	         <!-- 제목 검색 (key 값 "t") -->
	         <when test='key == "t"'>
	            AND BOARD_TITLE LIKE '%' || #{query} || '%'
	         </when>
	         
	         <!-- 내용 검색 (key 값 "c") -->
	         <when test='key == "t"'>
	            AND BOARD_TITLE LIKE '%' || #{query} || '%'
	         </when>
	         
	         <!-- 제목+내용 검색 (key 값 "tc") -->
	         <when test='key == "t"'>
	            AND (
	               BOARD_CONTENT LIKE '%' || #{query} || '%'
	               OR
	               BOARD_TITLE LIKE '%' || #{query} || '%'
	            )
	         </when>
	         
	         <!-- 작성자 검색 (key 값 "w") -->
	         <otherwise>
	            AND MEMBER_NICKNAME LIKE '%' || #{query} || '%'
	         </otherwise>
      </choose>
		ORDER BY BOARD_NO DESC
   	
   </select>

Mybatis가 제공하는 동적 SQL

동적 SQL : <choose>, <when>, <otherwise> 
		
		<choose> : 조건문을 작성할 영역 지정
			내부에 <when>,  <otherwise> 태그 작성 가능
		<when> : if, else if 역할의 태그
			- test 속성 : 조건식을 작성하는 속성
		<otherwise> : else 역할의 태그

상세 페이지 조회(게시물 클릭 시)

이미지

config.properties 만들어서 서버 컴퓨터에 경로 연결

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

# 게시글 이미지 요청 시 연결될 서버 폴더 경로(FileConfig에서 이용할 주소)
my.board.resource-location=file:///C:/uploadFiles/board/
# 위 2개는 FileConfig에서 사용할 것들

# 게시글 이미지 요청 주소 (DB 저장용) -> ServiceImpl에서 사용할 경로
my.board.web-path=/images/board/

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

FileConfig.java

// 프로필 이미지관련 경로
	@Value("${my.profile.resource-handler}")
	private String profileResourceHandler;

	@Value("${my.profile.resource-location}")
	private String profileResourceLocation;
.
.
registry.addResourceHandler(boardResourceHandler).addResourceLocations(boardResourceLocation);
		// -> 클라이언트가 /images/board/** 패턴으로 이미지 요청할 때
		// 서버 폴더 경로 중 C:/uploadFiles/board/로 연결

계층형쿼리

→ 댓글의 관련이 있는 데이터들끼리 묶여서 조회가 됌

전체 코드

SELECT LEVEL, C.* FROM -- LEVEL : 계층형 쿼리에서 사용할 수 있는 가상컬럼
		(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 = 1997) 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 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 = 1997) 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')
		-- 자식과 부모간의 관계가 있는지 확인하는 것
		-- 부모 댓글을 삭제했다면, 자식 댓글이 삭제되지않음
		-- 부모 댓글은 -> 삭제된 댓글입니다. 
		-- 부모1 댓글 -> 삭제되도 아래 자식 댓글은 남아있음
		--   ㄴ  자식1 댓글					
	
		-- T_memo
		-- 자식 댓글 중 삭제되지 않은 행이 하나라도 있으면 부모 댓글을 조회 대상에 포함
		-- 현재 댓글(C.COMMENT_NO)을 부모로 하는 자식 댓글 중
		-- 삭제되지 않은 댓글의 행의 갯수 카운트
		--> 삭제되지 않은 댓글이거나, 삭제된 댓글이라도 그 아래에 활성 상태의 자식 댓글이
		-- 존재하면 조회되도록 하는 목적의 SQL

시작, 부모NO와 연결, 오름차순 정렬

START WITH PARENT_COMMENT_NO IS NULL -- PARENT_COMMENT_NO, 즉 1레벨인 자식 댓글이 없는 댓글부터 시작
		CONNECT BY PRIOR COMMENT_NO = PARENT_COMMENT_NO -- 부모 NO와 같다면 연결해주겠다는 코드
		ORDER SIBLINGS BY COMMENT_NO; -- LEVEL이 같은 것들끼리 오름차순으로 정렬하겠다는 의미

여러 SQL을 실행하는 방법

- 여러 SQL을 실행하는 방법
		- 1. 하나의 Service 메서드에서 여러 mapper 메서드를 호출하는 방법
		
		- 2. 수행하려는 SQL1) 모두 SELECT 이면서
		   2) 먼저 조회된 결과 중 일부를 이용해서
		     나중에 수행되는 SQL의 조건으로 삼을 수 있는 경우
		    -> MyBatis의 <resultMap>, <collection> 태그를 이용해서 
		    Mapper 메서드 1회 호출만으로 여러 SELECT 한 번에 수행 가능
		
		

boardDetail (Controller)

@GetMapping("{boardCode:[0-9]+}/{boardNo:[0-9]+}")
	public String boardDetail(@PathVariable("boardCode") int boardCode, @PathVariable("boardNo") int boardNo,
			@RequestParam(value = "cp", required = false, defaultValue = "1") int cp,
			@SessionAttribute(value = "loginMember", required = false) Member loginMember, Model model,
			RedirectAttributes ra) {
		// 게시글 상세 조회 서비스 호출
		// loginMember는 좋아요를 로그인한 사람이 눌렀는지 확인하기 위해서 세션에서 가져옴
		// value, required로 loginMember를 가져온 이유는
		// 없다면 로그인을 꼭 했을 때를 의미하고, 위 조건을 추가한 이유는
		// 로그인 안한 사람들도 접근할 수 있는 페이지여야 하기 때문

		// 1) Map으로 전달 파라미터 묶기
		Map<String, Integer> map = new HashMap<>();
		map.put("boardCode", boardCode);
		map.put("boardNo", boardNo);

		// 로그인 상태인 경우에만 memberNo를 map에 추가
		// LIKE_CHECK시 이용 (로그인한 사람이 좋아요 누른 게시글인지 체크하기 위함)
		if (loginMember != null) {
			map.put("memberNo", loginMember.getMemberNo());
		}

		// 2) 서비스 호출

		Board board = service.selectOne(map);

		// log.debug("조회된 board : " + board);

		String path = null;

		// 조회 결과가 없는 경우
		if (board == null) {
			path = "redirect:/board/" + boardCode;
			// 내가 현재 보고있는 게시판목록으로 재요청
			ra.addFlashAttribute("message", "게시글이 존재하지 않습니다");
		} else { // 조회 결과가 있는 경우
			path = "board/boardDetail";
			// src/main/resources/tempaltes/board/boardDetail.html

			// board - 게시글 일반 내용 + imageList + commentList
			model.addAttribute("board", board);

			// 조회된 이미지 목록(imageList)이 있을 경우
			if (!board.getImageList().isEmpty()) {
				BoardImg thumbnail = null;
				// imageList의 0번 인덱스 == IMG_ORDER가 가장 빠른 순서

				// 만약 이미지 목록의 0번째 요소의 IMG_ORDER가 0이면 == 썸네일
				if (board.getImageList().get(0).getImgOrder() == 0) {
					thumbnail = board.getImageList().get(0);
					// board.getImageList() -> List<BoardImg>
				}
				// thumbnail 변수에는 
				// - 이미지 목록의 0번째 요소가 썸네일이면 썸네일 이미지의 BoardImg 객체
				// - 썸네일이 아니라면 null
				model.addAttribute("thumbnail", thumbnail);
				
				// start라는 key에 thumbnail이 null이 아닐 때 1 저장, null이면 0 저장
				model.addAttribute("start", thumbnail != null ? 1 : 0);
				// 썸네일 있을 때 : start=1
				// 썸네일 없을 때(일반이미지만 있거나, 등록된 이미지가 아예 없을 때)
				// : start=0
			}
		}
		return path;
	}

실행 순서

💡
  1. selectOne을 수행 후=> imageList, commentList 안채워짐
  2. resultMap에 작성된 resultMap으로 이동
  3. id를 정의해주고, 위 2개의 List를 수행하기 위한 쿼리를 수행 => collection
    -->

1) imageList, commentList를 제외한 나머지 필드 가져오기

<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,
		
		(SELECT COUNT(*)
		FROM "BOARD_LIKE"
		WHERE BOARD_NO = #{boardNo}
		AND MEMBER_NO = #{memberNo}) LIKE_CHECK
		
		FROM "BOARD"
		JOIN "MEMBER" USING(MEMBER_NO)
		WHERE BOARD_DEL_FL = 'N'
		AND BOARD_CODE = #{boardCode}
		AND BOARD_NO = #{boardNo}
 </select>

resultMap의 역할

resultMap 태그 역할
   	1) 조회된 컬럼명과 DTO의 필드명이 일치하지 않을 때 매핑 시켜주는 역할
   	2) <collection> 태그를 추가 작성하여
       여러행 결과가 조회되는 다른 SELECT를 수행한 후
       그 결과를 지정된 DTO의 필드에 대입
   <resultMap type="Board" id="board_rm">
   	<!-- type은 연결한 쿼리의 return type을 의미 
   		id는 resultMap으로 정의된 것과 연결
   	-->
   		<id property="boardNo" column="BOARD_NO" />
   		<!-- column에서는 pk 역할을 넣어주면 됌 
   			=> 즉, from에서 정의된 테이블의 pk를 작성
   			property는 column을 어떻게 사용하는지 작성 => DTO에 정의된 필드명
   			즉, id는 pk를 연결
   		-->
   		
   		<!-- 
   			collection태그
   			SELECT로 조회된 결과를 컬렉션에 담아
   			지정된 필드에 세팅
   			
   			property : List를 담을 DTO의 필드명
			select : 실행할 select 의 id
			column : 조회 결과(부모쿼리 수행결과) 중 지정된 컬럼의 값을 파라미터로 전달
			javaType : List(컬렉션)의 타입을 지정
			ofType : List(컬렉션)의 제네릭(타입제한) 지정
   		 -->
   		 <!-- 해당 게시글 이미지 목록 조회 후 필드에 저장 -->
   		<!-- BoardImg 필드에 저장하고 리스트로 받아서 Board DTO의 imageList를 넣어준다-->
   		<collection 
   			property="imageList"
   			column="BOARD_NO"
   			select="selectImageList"
   			javaType="java.util.ArrayList" 
   			ofType="BoardImg"
   		/>
   		<!-- Comment DTO에 담고, List에 담아서 commentList에 넘겨주겠다는 의미 -->
   		<collection 
	   		property="commentList" 
	   		column="BOARD_NO" 
	   		select="selectCommentList"
	   		javaType="java.util.ArrayList"
	   		ofType="Comment"
   		/>
   		 
   </resultMap>
  • collection에 정의된 select를 다음과 같이 정의해준다.
<!-- 상세 조회한 게시글의 이미지 목록 조회 -->
   <!-- ${boardNo}는 id에서 정의한 boardNo, 즉 첫번 째 SQL문에서 넘어온 BOARD_NO을 의미 -->
   <select id="selectImageList" resultType="BoardImg">
   		SELECT * FROM "BOARD_IMG"
   		WHERE BOARD_NO = #{boardNo}
   		ORDER BY IMG_ORDER
   </select>
   <!-- 상세 조회한 게시글의 댓글 목록 조회(계층형 쿼리) -->
   <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>

⇒ 이후, mapper → Service → Controller로 전달 후, forward해주면 된다.

0개의 댓글