
// 검색으로 조회할 경우 -> 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);
@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;
}
<!-- 검색 조건이 맞는 게시글 수 조회 -->
<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>
동적 SQL : <choose>, <when>, <otherwise>
<choose> : 조건문을 작성할 영역 지정
내부에 <when>, <otherwise> 태그 작성 가능
<when> : if, else if 역할의 태그
- test 속성 : 조건식을 작성하는 속성
<otherwise> : else 역할의 태그

# 게시글 이미지 요청 주소 : 클라이언트가 어떤 이미지를 보고싶어할 때 보낼 요청 주소를 의미
# (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/
// 프로필 이미지관련 경로
@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을 실행하는 방법
- 1. 하나의 Service 메서드에서 여러 mapper 메서드를 호출하는 방법
- 2. 수행하려는 SQL이
1) 모두 SELECT 이면서
2) 먼저 조회된 결과 중 일부를 이용해서
나중에 수행되는 SQL의 조건으로 삼을 수 있는 경우
-> MyBatis의 <resultMap>, <collection> 태그를 이용해서
Mapper 메서드 1회 호출만으로 여러 SELECT 한 번에 수행 가능
@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;
}
<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>
<!-- 상세 조회한 게시글의 이미지 목록 조회 -->
<!-- ${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해주면 된다.
