SpringBoot : 게시판(게시글 목록 조회)
package edu.kh.project.board.controller;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.service.BoardService;
import edu.kh.project.member.model.dto.Member;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@SessionAttributes({"loginMember"})
@RequestMapping("/board")
@Controller
public class BoardController {
@Autowired
private BoardService service;
/* 목록 조회 : /board/1?cp=1 (cp: current page(현재페이지))
* 상세 조회 : /board/1/1500?cp=1
*
* ** 컨트롤러 따로 생성 **
* 삽입 : /board2/1/insert
* 수정 : /board2/1/1500/update
* 삭제 : /board2/1/1500/delete
* */
/*
* ******** @PathVariable 사용 시 문제점과 해결법 ********
*
* 문제점 : 요청 주소와 @PathVariable로 가져다 쓸 주소와 레벨이 같다면
* 구분하지 않고 모두 매핑되는 문제가 발생
*
* 해결방법 : @PathVariable 지정 시 정규 표현식 사용
* {키:정규표현식}
*
*
* @PathVariable : URL 경로에 있는 값을 매개변수로 이용할 수 있게하는 어노테이션
* + request scope에 세팅
*
* /board/1 /board?code=1 -> 용도의 차이점이 존재
*
* - 자원(resource) 구분/식별
* ex) github.com/cmhinst
* ex) github.com/testUser
* ex) /board/1 -> 1번(공지사항) 게시판
* ex) /board/2 -> 2번(자유 게시판) 게시판
*
* query string 을 사용하는 경우 -> 부가적인 옵션이라고 생각하기!
* - 검색, 정렬, 필터링
* ex) search.naver.com?query=날씨
* ex) search.naver.com?query=종로맛집
* ex) /board2/insert?code=1
* ex) /board2/insert?code=2
* -> 삽입이라는 공통된 동작 수행
* 단, code에 따라 어디에 삽입할지 지정(필터링)
*
* ex) /board/list?order=recent (최신순)
* ex) /board/list?order=most (인기순)
*
* */
// 게시글 목록 조회
@GetMapping("/{boardCode:[0-9]+}") // boardCode는 1자리 이상 숫자
// @PathVariable : 주소를 값 자체로 쓸 수 있는 것
public String selectBoardList( @PathVariable("boardCode") int boardCode,
@RequestParam(value="cp", required = false, defaultValue = "1") int cp,
Model model, // 데이터 전달용 객체
@RequestParam Map<String, Object> paramMap // 파라미터 전부 다 담겨있음(검색 시) // {"key":"t", "query":"test"}
) {
// boardCode 확인
//System.out.println("boardCode : " + boardCode);
if( paramMap.get("key") == null ) { // 검색어가 없을 때 (검색 X)
// 게시글 목록 조회 서비스 호출
Map<String, Object> map = service.selectBoardList(boardCode, cp);
// 조회 결과를 request scope에 세팅 후 forward
model.addAttribute("map", map);
} else { // 검색어가 있을 때 (검색 O)
paramMap.put("boardCode", boardCode); // paramMap = key, query, boardCode
Map<String, Object> map = service.selectBoardList(paramMap, cp); // 오버로딩 적용
model.addAttribute("map", map);
}
return "board/boardList";
}
// @PathVariable : 주소에 지정된 부분을 변수에 저장
// + request scope 세팅
// 게시글 상세 조회 // /board/1/1500
@GetMapping("/{boardCode}/{boardNo}")
public String boardDetail(
@PathVariable("boardCode") int boardCode,
@PathVariable("boardNo") int boardNo,
Model model, // 데이터 전달용 객체
RedirectAttributes ra, // 리다이렉트 시 데이터 전달 객체
@SessionAttribute(value="loginMember", required = false) Member loginMember,
// 세션에서 loginMember를 얻어오는데 없으면 null, 있으면 회원정보 저장
// 쿠키를 이용한 조회 수 증가에서 사용
HttpServletRequest req,
HttpServletResponse resp
) throws ParseException {
Map<String, Object> map = new HashMap<String, Object>();
map.put("boardCode", boardCode);
map.put("boardNo", boardNo);
// 게시글 상세 조회 서비스 호출
Board board = service.selectBoard(map);
String path = null;
if(board != null) { // 조회 결과가 있을 경우
// ----------------------------------------------------------
// 현재 로그인 상태인 경우
// 로그인한 회원이 해당 게시글에 좋아요를 눌렀는지 확인
// 로그인 상태인 경우
if(loginMember != null) {
// 회원번호를 얻어와야해요
// map(boardCode, boardNo, memberNo)
map.put("memberNo", loginMember.getMemberNo());
// 좋아요 여부 확인 서비스 호출
int result = service.boardLikeCheck(map);
// 결과값을 통해 분기처리
// 누른적이 있을 경우 처리
// "likeCheck"
if(result > 0) model.addAttribute("likeCheck", "on"); // 누른적이 있을 경우 "likeCheck" 키 값에 "on" 값을 넣겠다.
}
// ---------------------------------------------------------
// 쿠키를 이용한 조회 수 증가 처리
// 1) 비회원(로그인 안한 상태인 사람) 또는 로그인한 회원의 글이 아닌경우
if(loginMember == null ||
loginMember.getMemberNo() != board.getMemberNo() ) {
// 2) 쿠키 얻어오기
Cookie c = null;
// 요청에 담겨있는 모든 쿠키 얻어오기
Cookie[] cookies = req.getCookies(); // Cookies ['아이디 저장', '다른 쿠키', ..., 'readBoardNo']
if(cookies != null) { // 쿠키가 존재할 경우
// 쿠키 중 "readBoardNo" 라는 쿠키를 찾아서 c에 대입
for(Cookie cookie : cookies) {
if(cookie.getName().equals("readBoardNo")) {
c = cookie;
break;
}
}
}
// 3) 기존 쿠키가 없거나 ( c == null )
// 존재는 하나 현재 게시글 번호가
// 쿠키에 저장되지 않은 경우 ( 해당 게시글 본적 없음 )
// 결과 저장용 변수 선언
int result = 0;
if(c == null) {
// 쿠키가 존재 X -> 하나 새로 생성
c = new Cookie("readBoardNo", "|" + boardNo + "|");
// 조회수 증가 서비스 호출
result = service.updateReadCount(boardNo);
} else {
// 현재 게시글 번호가 쿠키에 있는지 확인
// Cookie.getValue() : 쿠키에 저장된 모든 값을 읽어옴 -> String으로 반환
// String.indexOf("문자열")
// : 찾는 문자열이 String 몇번 인덱스에 존재하는지 반환
// 단, 없으면 -1 반환
if(c.getValue().indexOf("|" + boardNo + "|") == -1) {
// 현재 게시글 번호가 쿠키에 없다면
// 기존 값에 게시글 번호 추가해서 다시 세팅
c.setValue( c.getValue() + "|" + boardNo + "|" );
// 조회수 증가 서비스 호출
result = service.updateReadCount(boardNo);
}
} // 3) 종료
// 쿠키의 수명 세팅
if(result > 0) {
board.setReadCount(board.getReadCount() + 1);
// 조회된 board 조회 수와 DB 조회 수 동기화
// 적용 경로 설정
c.setPath("/"); // "/" 이하 경로 요청 시 쿠키 서버로 전달
// 수명 지정
Calendar cal = Calendar.getInstance(); // 싱글톤 패턴(미리 만들어진 하나의 객체만을 이용)
cal.add(cal.DATE, 1);
// 날짜 표기법 변경 객체 (DB의 TO_CHAR()와 비슷)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// java.util.Date
Date a = new Date(); // 현재 시간
Date temp = new Date(cal.getTimeInMillis()); // 내일 (24시간 후)
// 2023-05-11 12:16:10
Date b = sdf.parse(sdf.format(temp)); // 내일 0시 0분 0초
// 내일 0시 0분 0초 - 현재 시간
long diff = (b.getTime() - a.getTime()) / 1000;
// -> 내일 0시 0분 0초까지 남은 시간을 초단위로 반환
c.setMaxAge((int)diff); // 수명 설정
resp.addCookie(c); // 응답 객체를 이용해서
// 클라이언트에게 전달
}
}
// ---------------------------------------------------------
path = "board/boardDetail"; // forward 할 jsp 경로
model.addAttribute("board", board);
} else { // 조회 결과가 없을 경우
path = "redirect:/board/" + boardCode;
// 게시판 첫페이지로 리다이렉트
ra.addFlashAttribute("message", "해당 게시글이 존재하지 않습니다");
}
return path;
}
// 좋아요 처리
@PostMapping("/like")
@ResponseBody // 반환되는 값이 비동기 요청한 곳으로 돌아가게 함
public int like(@RequestBody Map<String, Integer> paramMap) {
return service.like(paramMap);
}
}
package edu.kh.project.board.model.service;
import java.util.List;
import java.util.Map;
import edu.kh.project.board.model.dto.Board;
public interface BoardService {
/** 게시판 종류 조회
* @return boardTypeList
*/
List<Map<String, Object>> selectBoardTypeList();
/** 게시글 목록 조회
* @param boardCode
* @param cp
* @return map
*/
Map<String, Object> selectBoardList(int boardCode, int cp);
/** 게시글 상세 조회
* @param map
* @return board
*/
Board selectBoard(Map<String, Object> map);
/** 좋아요 여부 확인 서비스
* @param map
* @return result
*/
int boardLikeCheck(Map<String, Object> map);
/** 조회수 증가 서비스
* @param boardNo
* @return result
*/
int updateReadCount(int boardNo);
int like(Map<String, Integer> paramMap);
/** 게시글 목록 조회 (검색)
* @param paramMap
* @param cp
* @return boardList
*/
Map<String, Object> selectBoardList(Map<String, Object> paramMap, int cp);
/** DB 이미지(파일) 목록 조회
* @return
*/
List<String> selectImageList();
}
package edu.kh.project.board.model.service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import edu.kh.project.board.model.dao.BoardMapper;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.Pagination;
@Service
public class BoardServiceImpl implements BoardService{
@Autowired
private BoardMapper mapper;
// 게시판 종류 조회
@Override
public List<Map<String, Object>> selectBoardTypeList() {
return mapper.selectBoardTypeList();
}
// alt + shift + r = 원하는 곳 커서 올린 후 지우면 다 같이 수정됨.
// 게시글 목록 조회
@Override
public Map<String, Object> selectBoardList(int boardCode, int cp) {
// 1. 특정 게시판의 삭제되지 않은 게시글 수 조회
int listCount = mapper.getListCount(boardCode);
// 2. 1번 조회 결과 + cp 를 이용해서 Pagination 객체 생성
// -> 내부 필드가 모두 계산되어 초기화됨
Pagination pagination = new Pagination(listCount, cp);
// 3. 특정 게시판에서
// 현재 페이지에 해당하는 부분에 대한 게시글 목록 조회
// ex) 100개
// 10개 씩 보여준다
// 1page -> 100 ~ 91
// 2page -> 90 ~ 81
// 어떤 게시판에서(boardCode)
// 몇페이지(pagination.currentPage)에 대한
// 게시글 몇개(pagination.limit)인지 조회
// RowBounds 객체
// - 마이바티스에서 페이징처리를 위해 제공하는 객체
// - offset 만큼 건너뛰고
// 그 다음 지정된 행 개수만큼(limit) 만큼 조회
// 1) offset 계산
int offset
= (pagination.getCurrentPage() - 1) * pagination.getLimit();
// 2) RowBounds 객체 생성
RowBounds rowBounds = new RowBounds(offset, pagination.getLimit());
List<Board> boardList = mapper.selectBoardList(boardCode, rowBounds);
// 4. pagination, boardList를 Map에 담아서 반환
Map<String, Object> map = new HashMap<String, Object>();
map.put("pagination", pagination);
map.put("boardList", boardList);
return map;
}
// 게시글 상세 조회
@Override
public Board selectBoard(Map<String, Object> map) {
return mapper.selectBoard(map);
}
// 좋아요 여부 확인 서비스
@Override
public int boardLikeCheck(Map<String, Object> map) {
return mapper.boardLikeCheck(map);
}
// 조회수 증가 서비스
@Transactional(rollbackFor = Exception.class)
@Override
public int updateReadCount(int boardNo) {
return mapper.updateReadCount(boardNo);
}
// 좋아요 처리 서비스
@Transactional(rollbackFor = Exception.class)
@Override // paramMap => boardNo, loginMemberNo, check
public int like(Map<String, Integer> paramMap) {
// check == 0 / 1
// check 값이 무엇이냐에 따라서 BOARD_LIKE 테이블 INSERT / DELETE
// 결과 저장용 변수선언
int result = 0;
if(paramMap.get("check") == 0) { // 좋아요 상태 X
// BOARD_LIKE 테이블 INSERT ( dao.insertBoardLike() )
result = mapper.insertBoardLike(paramMap);
} else { // 좋아요 상태 O
// BOARD_LIKE 테이블 DELETE ( dao.deleteBoardLike() )
result = mapper.deleteBoardLike(paramMap);
}
if(result == 0) return -1;
// 현재 게시글의 좋아요 개수 조회
int count = mapper.countBoardLike(paramMap.get("boardNo"));
return count;
}
// 게시글 목록 조회(검색)
@Override
public Map<String, Object> selectBoardList(Map<String, Object> paramMap, int cp) {
// 1. 특정 게시판의 삭제되지 않았고, 검색 조건이 일치하는 게시글 수 조회
int listCount = mapper.getSearchListCount(paramMap); // 오버로딩
// 2. 1번 조회 결과 + cp 를 이용해서 Pagination 객체 생성
// -> 내부 필드가 모두 계산되어 초기화됨
Pagination pagination = new Pagination(listCount, cp);
// 3. 특정 게시판에서
// 현재 페이지에 해당하는 부분에 대한 게시글 목록 조회
// 단 , 검색 조건 일치하는 글만
// 1) offset 계산
int offset
= (pagination.getCurrentPage() - 1) * pagination.getLimit();
// 2) RowBounds 객체 생성
RowBounds rowBounds = new RowBounds(offset, pagination.getLimit());
List<Board> boardList = mapper.selectSearchBoardList(paramMap, rowBounds);
// 4. pagination, boardList를 Map에 담아서 반환
Map<String, Object> map = new HashMap<String, Object>();
map.put("pagination", pagination);
map.put("boardList", boardList);
return map;
}
// DB 이미지 파일 목록 조회
@Override
public List<String> selectImageList() {
return mapper.selectImageList();
}
}
package edu.kh.project.board.model.dao;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.session.RowBounds;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.Pagination;
@Mapper
public interface BoardMapper {
/** 게시판 종류 조회
* @return boardTypeList
*/
List<Map<String, Object>> selectBoardTypeList();
/** 특정 게시판의 삭제되지 않은 게시글 수 조회
* @param boardCode
* @return listCount
*/
public int getListCount(int boardCode);
/** 특정 게시판에서 현재 페이지에 해당하는 부분에 대한 게시글 목록 조회
* @param pagination
* @param boardCode
* @return
*/
public List<Board> selectBoardList(int boardCode, RowBounds rowBounds);
/** 게시글 상세 조회
* @param map
* @return board
*/
public Board selectBoard(Map<String, Object> map);
/** 좋아요 여부 확인 DAO
* @param map
* @return result
*/
public int boardLikeCheck(Map<String, Object> map);
/** 조회수 증가 DAO
* @param boardNo
* @return result
*/
public int updateReadCount(int boardNo);
/** 좋아요 테이블 삽입
* @param paramMap
* @return
*/
public int insertBoardLike(Map<String, Integer> paramMap);
/** 좋아요 삭제
* @param paramMap
* @return
*/
public int deleteBoardLike(Map<String, Integer> paramMap);
/** 좋아요 개수 조회
* @param integer
* @return
*/
public int countBoardLike(Integer boardNo);
/** 게시글 수 조회(검색)
* @param paramMap
* @return
*/
public int getSearchListCount(Map<String, Object> paramMap);
// 원하는 곳 커서 올린 후 alt + shift + r -> Enter
// = 호출하는 부분도 자동으로 이름 바꿔줌.
/** 게시글 목록 조회 (검색)
* @param pagination
* @param paramMap
* @return
*/
public List<Board> selectSearchBoardList(Map<String, Object> paramMap, RowBounds rowBounds);
/** DB 이미지 파일 목록 조회
* @return
*/
public List<String> selectImageList();
}
<?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="edu.kh.project.board.model.dao.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>
<!-- 게시판 종류 목록 조회 -->
<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 필드에다가 조회 결과를 저장하겠다.
-->
<!-- 특정 게시판의 삭제되지 않고, 검색 조건이 일치하는 게시글 수 조회 -->
<select id="getSearchListCount" resultType="_int">
SELECT COUNT(*) FROM BOARD
<!-- 작성자 검색 -->
<if test='key == "w"'>
JOIN "MEMBER" USING(MEMBER_NO)
</if>
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = #{boardCode}
<choose>
<when test='key == "t"'>
<!-- 제목 -->
AND BOARD_TITLE LIKE '%${query}%'
</when>
<when test='key == "c"'>
<!-- 내용 -->
AND BOARD_CONTENT LIKE '%${query}%'
</when>
<when test='key == "tc"'>
<!-- 제목 + 내용 -->
AND (BOARD_TITLE LIKE '%${query}%' OR BOARD_CONTENT LIKE '%${query}%')
</when>
<when test='key == "w"'>
<!-- 작성자(닉네임) -->
AND MEMBER_NICKNAME LIKE '%${query}%'
</when>
</choose>
</select>
<!-- CDATA 태그 : 해당 태그 내부에 작성된 것은 모두 문자로 취급 -->
<!-- 게시글 목록 조회(검색) -->
<select id="selectSearchBoardList" 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}
<choose>
<when test='key == "t"'>
<!-- 제목 -->
AND BOARD_TITLE LIKE '%${query}%'
</when>
<when test='key == "c"'>
<!-- 내용 -->
AND BOARD_CONTENT LIKE '%${query}%'
</when>
<when test='key == "tc"'>
<!-- 제목 + 내용 -->
AND (BOARD_TITLE LIKE '%${query}%' OR BOARD_CONTENT LIKE '%${query}%')
</when>
<when test='key == "w"'>
<!-- 작성자(닉네임) -->
AND MEMBER_NICKNAME LIKE '%${query}%'
</when>
</choose>
ORDER BY BOARD_NO DESC
</select>
<!-- 이미지 목록 조회 -->
<select id="selectImageListAll" resultType="string">
SELECT IMG_RENAME FROM BOARD_IMG
</select>
</mapper>
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://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="${boardName}">게시판 이름</title>
<link rel="stylesheet" th:href="@{/css/board/boardList-style.css}">
</head>
<body>
<!-- 타임리프 변수 선언 --> <!-- 변수명 : pagination, boardList == main 태그 닫히기 전까지만 사용 가능함
전역에서 사용하고 싶다면? body 태그 안에 선언하면 됨. -->
<main th:with="pagination=${map.pagination}, boardList=${map.boardList}">
<!-- 타임리프 조각을 이용한 변경 -->
<!-- /templates/ common/header .html -->
<th:block th:replace="~{common/header}"></th:block>
<!-- 검색을 진행한 경우에 파라미터를 쿼리스트링 형태로 저장한 변수를 선언 -->
<!-- <c:if test="${not empty param.key}"> param: EL구문 내장객체
<c:set var="kq" value="&key=${param.key}&query=${param.query}" />
</c:if> -->
<section class="board-list">
<h1 class="board-name" th:text="${boardName}">게시판 이름</h1>
<h3 style="margin:30px">"${param.query}" 검색 결과</h3>
<div class="list-wrapper">
<table class="list-table">
<thead>
<tr>
<th>글번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회수</th>
<th>좋아요</th>
</tr>
</thead>
<tbody>
<!-- 타임리프 내장 객체 중 #lists -->
<!-- #lists.size(boardList) : boardList의 길이 반환 -->
<th:block th:if="${#lists.size(boardList) == 0}">
<!-- 조회된 게시글 목록이 비어있거나 null 경우 -->
<!-- 게시글 목록 조회 결과가 비어있다면 -->
<tr>
<th colspan="6">게시글이 존재하지 않습니다.</th>
</tr>
</th:block>
<th:block th:unless="${#lists.size(boardList) == 0}">
<!-- 게시글 목록 조회 결과가 있다면 -->
<tr th:each="board : ${boardList}"> <!-- tr 태그 포함해서 반복됨. -->
<td th:text="${board.boardNo}">게시글 번호</td> <!-- 게시글 번호가 뜨는 것이 아닌 th 안 속성 값이 대입되서 뜸. -->
<td>
<!-- 썸네일이 있을 경우 -->
<img th:if="${board.thumbnail}" class="list-thumbnail" th:src="${board.thumbnail}">
<!-- 타임리프 자체로 null, null 아닌지 따짐. -->
<!-- ${boardCode} : @Pathvariable 로 request scope에 추가된 값 -->
<!-- <a th:href="/board/${boardCode}/${board.boardNo}?cp=${pagination.currentPage}">${board.boardTitle}</a> -->
<a th:href="@{/board/{boardCode}/{boardNo}(boardCode=${boardCode}, boardNo=${board.boardNo}, cp=${pagination.currentPage})}" th:text="${board.boardTitle}">게시글 제목</a>
<!-- 자리가 마련되지 않았는데, 소괄호 안에 쓴다? 자동으로 쿼리스트링으로 붙는다! -->
<th:block th:text="|[${board.commentCount}]|">댓글 수</th:block>
<!-- | 버티컬 바 안 사이에 작성하면 타임리프와 관련된 내용은 값 자체로 인식! | -->
</td>
<td>[[${board.memberNickname}]]</td> <!-- 텍스트 랜더링 -->
<td>[[${board.boardCreateDate}]]</td>
<td>[[${board.readCount}]]</td>
<td>[[${board.likeCount}]]</td>
</tr>
</th:block>
</tbody>
</table>
</div>
<div class="btn-area">
<!-- 로그인 상태일 경우 글쓰기 버튼 노출 -->
<button th:if="${session.loginMember}" id="insertBtn">글쓰기</button>
</div>
<!-- 게시글이 있을 때만 페이지네이션 보이기 -->
<div th:if="${#lists.size(boardList) > 0}" class="pagination-area">
<ul class="pagination">
<!-- 첫 페이지로 이동 -->
<li><a th:href="@{/board/{code}(code=${boardCode}, cp=1)}"><<</a></li>
<!-- 이전 목록 마지막 번호로 이동 -->
<li><a th:href="@{/board/{code}(code=${boardCode}, cp=${pagination.prevPage})}"><</a></li>
<!-- 특정 페이지로 이동 -->
<!-- #numbers.sequence(시작, 끝 [,step]) : 시작 이상, 끝 이하 까지 step 만큼 증가하는 숫자를 발생시켜 리스트로 반환 -->
<th:block th:each="i : ${#numbers.sequence(pagination.startPage, pagination.endPage, 1)}">
<li th:if="${i == pagination.currentPage}">
<a class="current" th:text="${i}">현재 페이지</a>
</li>
<li th:unless="${i == pagination.currentPage}">
<a th:href="@{/board/{code}(code=${boardCode}, cp=${i})}"
th:text="${i}">나머지 페이지</a>
</li>
</th:block>
<!-- 다음 목록 시작 번호로 이동 -->
<li><a th:href="@{/board/{code}(code=${boardCode}, cp=${pagination.nextPage})}">></a></li>
<!-- 끝 페이지로 이동 -->
<li><a th:href="@{/board/{code}(code=${boardCode}, cp=${pagination.maxPage})}">>></a></li>
</ul>
</div>
<!-- 검색창 -->
<!-- 상대주소 -->
<form action="${boardCode}" method="get" id="boardSearch">
<select name="key" id="searchKey">
<option value="t">제목</option>
<option value="c">내용</option>
<option value="tc">제목+내용</option>
<option value="w">작성자</option>
</select>
<input type="text" name="query" id="searchQuery" placeholder="검색어를 입력해주세요.">
<button>검색</button>
</form>
</section>
</main>
<!--<div>
main 밖에 있는 거에서는 타임리프 변수 사용 불가능!
</div>-->
<th:block th:replace="~{common/footer}"></th:block>
<script th:src="@{/js/board/boardList.js}"></script>
</body>
</html>
package edu.kh.project.common.filter;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
public class BoardFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
// /board/1 -> {"", "board", "1"}
// /board2/1/insert -> {"", "board2", "1", "insert"}
// /board2/1/update -> {"", "board2", "1", "update"}
String[] arr = req.getRequestURI().split("/");
try {
String boardCode = arr[2];
List<Map<String, Object>> boardTypeList
= (List<Map<String, Object>>)(req.getServletContext().getAttribute("boardTypeList")); // 다운캐스팅
for(Map <String,Object> boardType : boardTypeList) {
if((boardType.get("BOARD_CODE") + "").equals(boardCode)) {
req.setAttribute("boardName", boardType.get("BOARD_NAME"));
}
}
} catch(Exception e) { }
chain.doFilter(request, response);
}
}
package edu.kh.project.common.config;
import java.util.Arrays;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import edu.kh.project.common.filter.BoardFilter;
import edu.kh.project.common.filter.LoginFilter;
@Configuration // 설정 관련 파일에서 꼭 작성해줘야 하는 어노테이션!
public class FilterConfig {
@Bean
public FilterRegistrationBean<LoginFilter> loginFilter(){
FilterRegistrationBean<LoginFilter> resiRegistrationBean = new FilterRegistrationBean<LoginFilter>();
resiRegistrationBean.setFilter(new LoginFilter());
String[] url = {"/myPage/*", "/board2/*"}; // , 로 추가 계속 가능함!
resiRegistrationBean.setUrlPatterns(Arrays.asList(url)); // url 패턴 여러 개 지정 --> 컬렉션 중 리스트 형태로 만들어줌
resiRegistrationBean.setName("loginFilter"); // 이름
resiRegistrationBean.setOrder(1); // 여러 필터가 있을 때 순서
return resiRegistrationBean;
}
@Bean
public FilterRegistrationBean<BoardFilter> boardFilter(){
FilterRegistrationBean<BoardFilter> resiRegistrationBean = new FilterRegistrationBean<BoardFilter>();
resiRegistrationBean.setFilter(new BoardFilter());
String[] url = {"/board/*", "/board2/*"}; // , 로 추가 계속 가능함!
resiRegistrationBean.setUrlPatterns(Arrays.asList(url)); // url 패턴 여러 개 지정 --> 컬렉션 중 리스트 형태로 만들어줌
resiRegistrationBean.setName("boardFilter"); // 이름
resiRegistrationBean.setOrder(2); // 여러 필터가 있을 때 순서
return resiRegistrationBean;
}
}
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://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="${boardName}">게시판 이름</title>
<link rel="stylesheet" th:href="@{/css/board/boardList-style.css}">
</head>
<body>
<!-- 타임리프 변수 선언 --> <!-- 변수명 : pagination, boardList == main 태그 닫히기 전까지만 사용 가능함
전역에서 사용하고 싶다면? body 태그 안에 선언하면 됨. -->
<main th:with="pagination=${map.pagination}, boardList=${map.boardList}">
<!-- 타임리프 조각을 이용한 변경 -->
<!-- /templates/ common/header .html -->
<th:block th:replace="~{common/header}"></th:block>
<!-- 검색을 진행한 경우에 파라미터를 쿼리스트링 형태로 저장한 변수를 선언 -->
<!-- <c:if test="${not empty param.key}"> param: EL구문 내장객체
<c:set var="kq" value="&key=${param.key}&query=${param.query}" />
</c:if> -->
<section class="board-list">
<h1 class="board-name" th:text="${boardName}">게시판 이름</h1>
<h3 style="margin:30px" th:if="${param.query}" th:text='|"${param.query}" 검색 결과|'></h3>
<div class="list-wrapper">
<table class="list-table">
<thead>
<tr>
<th>글번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회수</th>
<th>좋아요</th>
</tr>
</thead>
<tbody>
<!-- 타임리프 내장 객체 중 #lists -->
<!-- #lists.size(boardList) : boardList의 길이 반환 -->
<th:block th:if="${#lists.size(boardList) == 0}">
<!-- 조회된 게시글 목록이 비어있거나 null 경우 -->
<!-- 게시글 목록 조회 결과가 비어있다면 -->
<tr>
<th colspan="6">게시글이 존재하지 않습니다.</th>
</tr>
</th:block>
<th:block th:unless="${#lists.size(boardList) == 0}">
<!-- 게시글 목록 조회 결과가 있다면 -->
<tr th:each="board : ${boardList}"> <!-- tr 태그 포함해서 반복됨. -->
<td th:text="${board.boardNo}">게시글 번호</td> <!-- 게시글 번호가 뜨는 것이 아닌 th 안 속성 값이 대입되서 뜸. -->
<td>
<!-- 썸네일이 있을 경우 -->
<img th:if="${board.thumbnail}" class="list-thumbnail" th:src="${board.thumbnail}">
<!-- 타임리프 자체로 null, null 아닌지 따짐. -->
<!-- ${boardCode} : @Pathvariable 로 request scope에 추가된 값 -->
<!-- <a th:href="/board/${boardCode}/${board.boardNo}?cp=${pagination.currentPage}">${board.boardTitle}</a> -->
<!-- 검색 X -->
<a th:unless="${param.query}" th:href="@{/board/{boardCode}/{boardNo}(boardCode=${boardCode}, boardNo=${board.boardNo}, cp=${pagination.currentPage})}" th:text="${board.boardTitle}">게시글 제목</a>
<!-- 자리가 마련되지 않았는데, 소괄호 안에 쓴다? 자동으로 쿼리스트링으로 붙는다! -->
<!-- 검색 O -->
<a th:if="${param.query}" th:href="@{/board/{boardCode}/{boardNo}(boardCode=${boardCode}, boardNo=${board.boardNo}, cp=${pagination.currentPage}, key=${param.key}, query=${param.query})}" th:text="${board.boardTitle}">게시글 제목</a>
<th:block th:text="|[${board.commentCount}]|">댓글 수</th:block>
<!-- | 버티컬 바 안 사이에 작성하면 타임리프와 관련된 내용은 값 자체로 인식! | -->
</td>
<td>[[${board.memberNickname}]]</td> <!-- 텍스트 랜더링 -->
<td>[[${board.boardCreateDate}]]</td>
<td>[[${board.readCount}]]</td>
<td>[[${board.likeCount}]]</td>
</tr>
</th:block>
</tbody>
</table>
</div>
<div class="btn-area">
<!-- 로그인 상태일 경우 글쓰기 버튼 노출 -->
<button th:if="${session.loginMember}" id="insertBtn">글쓰기</button>
</div>
<!-- 게시글이 있을 때만 페이지네이션 보이기 -->
<div th:if="${#lists.size(boardList) > 0}" class="pagination-area">
<!-- 검색 X인 경우의 페이지네이션 -->
<ul th:unless="${param.query}" class="pagination">
<!-- 첫 페이지로 이동 -->
<li><a th:href="@{/board/{code}(code=${boardCode}, cp=1)}"><<</a></li>
<!-- 이전 목록 마지막 번호로 이동 -->
<li><a th:href="@{/board/{code}(code=${boardCode}, cp=${pagination.prevPage})}"><</a></li>
<!-- 특정 페이지로 이동 -->
<!-- #numbers.sequence(시작, 끝 [,step]) : 시작 이상, 끝 이하 까지 step 만큼 증가하는 숫자를 발생시켜 리스트로 반환 -->
<th:block th:each="i : ${#numbers.sequence(pagination.startPage, pagination.endPage, 1)}">
<li th:if="${i == pagination.currentPage}">
<a class="current" th:text="${i}">현재 페이지</a>
</li>
<li th:unless="${i == pagination.currentPage}">
<a th:href="@{/board/{code}(code=${boardCode}, cp=${i})}"
th:text="${i}">나머지 페이지</a>
</li>
</th:block>
<!-- 다음 목록 시작 번호로 이동 -->
<li><a th:href="@{/board/{code}(code=${boardCode}, cp=${pagination.nextPage})}">></a></li>
<!-- 끝 페이지로 이동 -->
<li><a th:href="@{/board/{code}(code=${boardCode}, cp=${pagination.maxPage})}">>></a></li>
</ul>
<!-- 검색 O인 경우의 페이지네이션 --> <!-- key, query 추가! -->
<ul th:if="${param.query}" class="pagination">
<!-- 첫 페이지로 이동 -->
<li><a th:href="@{/board/{boardCode}(boardCode=${boardCode}, cp=1, key=${param.key}, query=${param.query})}"><<</a></li>
<!-- 이전 목록 마지막 번호로 이동 -->
<li><a th:href="@{/board/{boardCode}(boardCode=${boardCode}, cp=${pagination.prevPage}, key=${param.key}, query=${param.query})}"><</a></li>
<!-- #numbers.sequence(시작, 끝 [,step]) -->
<!-- 특정 페이지로 이동 -->
<th:block th:each="i : ${#numbers.sequence(pagination.startPage, pagination.endPage, 1)}">
<!-- 현재 보고있는 페이지 -->
<li th:if="${i == pagination.currentPage}">
<a class="current" th:text="${i}">현재 페이지</a>
</li>
<!-- 현재 페이지를 제외한 나머지 -->
<li th:unless="${i == pagination.currentPage}">
<a th:href="@{/board/{boardCode}(boardCode=${boardCode}, cp=${i}, key=${param.key}, query=${param.query})}" th:text="${i}">현재 페이지</a>
</li>
</th:block>
<!-- 다음 목록 시작 번호로 이동 -->
<li><a th:href="@{/board/{boardCode}(boardCode=${boardCode}, cp=${pagination.nextPage}, key=${param.key}, query=${param.query})}">></a></li>
<!-- 끝 페이지로 이동 -->
<li><a th:href="@{/board/{boardCode}(boardCode=${boardCode}, cp=${pagination.maxPage}, key=${param.key}, query=${param.query})}">>></a></li>
</ul>
</div>
<!-- 검색창 -->
<!-- 상대주소 -->
<form th:action="${boardCode}" method="get" id="boardSearch">
<select name="key" id="searchKey">
<option value="t">제목</option>
<option value="c">내용</option>
<option value="tc">제목+내용</option>
<option value="w">작성자</option>
</select>
<input type="text" name="query" id="searchQuery" placeholder="검색어를 입력해주세요.">
<button>검색</button>
</form>
</section>
</main>
<!--<div>
main 밖에 있는 거에서는 타임리프 변수 사용 불가능!
</div>-->
<th:block th:replace="~{common/footer}"></th:block>
<script th:src="@{/js/board/boardList.js}"></script>
</body>
</html>