insert할 때 번호를 못불러와서 계속 실패했는데 dual을 썼어야 하는 거였음 ㅎㅎ 지금까지 배운 거 중간 총정리 하는 느낌으로 만들었는데 제법 빼먹은 게 많다니 아무래도 다시 만들어봐야..더보기
게시글 목록과 게시글 상세화면을 구현함. 이후에 답글/댓글, 조회수, 좋아요 등의 기능을 추가할 예정. 강사님 풀이와 비교해 되짚어 볼 내용만 아래에 정리함
board_writer varchar2(20) references member(member_id) on delete set null,
@ModelAttribute로 수신한 데이터는 자동으로 Model에 첨부됨
검색 옵션(ex. 작성자, 제목, 내용)과 키워드를 파라미터로 전달해야 하는 검색 기능일 경우에 유용
[참고] 옵션에 name을 지정하면 해당 이름으로 첨부됨. default=클래스 이름
@ModelAttribute(name="vo") BoardListSearchVo vo
전체 목록 조회와 검색 결과 목록 조회 기능을 Dao에서 오버라이딩으로(selectList()
, selectList(String type, String keyword)
) 구현한 상태
VO를 생성해 검색 옵션과 키워드를 멤버변수로 선언하고 검색인지 판정하는 getter()를 추가
컨트롤러에서 /board/list의 @RequestParam을 @ModelAttribute BoardListSearchVO vo
로 대체
Dao에서도 매개변수를 BoardListSearchVO vo
로 대체
//Dao, DaoImpl
@Override
public List<BoardDto> selectList(BoardListSearchVO vo) {
String sql = "select * from board "
+ "where instr(#1, ?) > 0 "
+ "order by board_no desc";
sql = sql.replace("#1", vo.getType());
Object[] param = {vo.getKeyword()};
return jdbcTemplate.query(sql, mapper, param);
}
//Controller
@GetMapping("/list")
public String list(Model model,
@ModelAttribute BoardListSearchVO vo) {
if(vo.isSearch()) {
model.addAttribute("list", boardDao.selectList(vo));
}else {
model.addAttribute("list", boardDao.selectList());
}
return "board/list";
}
<!-- list.jsp -->
<select name="type" required>
<option value="board_title"
<c:if test="${vo.type == 'board_title'}">selected</c:if>>
제목
</option>
<option value="board_writer"
<c:if test="${vo.type == 'board_writer'}">selected</c:if>>
작성자
</option>
</select>
session에 있는 회원 아이디를 작성자로 추가한 뒤 등록해야 함
//Controller
String memberId = (String)session.getAttribute(SessionConstant.ID);
boardDto.setBoardWriter(memberId);
boardDao.insert(boardDto);
<!-- write.jsp -->
<select name="boardHead">
<option value="" selected>선택안함</option>
<option>공지</option>
<option>정보</option>
<option>유머</option>
</select>
등록 후에 상세를 갈 때는 sql 구문 두 개 써야 함. 글 번호
를 알아내야 하기 때문
select * from board where
board_no = (select max(board_no) from board)
글을 여러 명이 동시에 작성한다면 번호가 똑같이 나올 수 있음. 그럴싸해보이지만 이 방식을 사용할 수 없음
//BoardTest2
@SpringBootTest
public class BoardTest2 {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void test() {
//번호 생성
String sql = "select board_seq.nextval from dual";
int boardNo = jdbcTemplate.queryForObject(sql, int.class);
//null가능성 없으니까 int / Integer는 null가능
System.out.println("boardNo = " + boardNo);
sql = "insert into board("
+ "board_no, board_title, board_content, "
+ "board_writer, board_head) "
+ "values(?, ?, ?, ?, ?)";
Object[] param = {
boardNo, "테스트", "테스트", "eclipse", null
};
jdbcTemplate.update(sql, param);
}
}
//Dao
int insert2(BoardDto boardDto); //번호를 알아야 하니까 반환형 int
//반환형은 오버로딩 조건에 포함되지 않으므로 이름을 다르게 해야 함
//DaoImpl
//시퀀스를 또 생성하는 구문을 넣으면 안됨
@Override
public int insert2(BoardDto boardDto) {
//번호를 미리 생성한 뒤 등록하는 기능
//번호 생성
String sql = "select board_seq.nextval from dual";
int boardNo = jdbcTemplate.queryForObject(sql, int.class);
//등록
sql = "insert into board("
+ "board_no, board_title, board_content, "
+ "board_writer, board_head) "
+ "values(?, ?, ?, ?, ?)";
Object[] param = {
boardNo, boardDto};
jdbcTemplate.update(sql, param);
return boardNo;
}
불러와
읽어와
//Dao
void viewUp(int boardNo); //조회 수 +
BoardDto read(int boardNo); //조회 수 증가까지 포함해서 단일 조회
//DaoImpl
@Override
public void viewUp(int boardNo) {
String sql = "update board "
+ "set board_read = board_read + 1 "
+ "where board_no = ?";
Object[] param = {boardNo};
jdbcTemplate.update(sql, param);
}
@Override
public BoardDto read(int boardNo) {
this.viewUp(boardNo);
return this.selectOne(boardNo);
}
//Controller
//1. 조회 수 증가시키고 데이터를 불러오기
//boardDao.viewUp(boardNo);
//model.addAttribute("dto", boardDao.selectOne(boardNo));
//2. 조회 수가 증가한 데이터를 읽어오기
model.addAttribute("dto", boardDao.read(boardNo));
중복 방지의 중요도에 따라 선택할 수 있는 방법이 달라짐. 중복 방지라는 것은 반드시 어딘가에 기록이 되어 있어야 하는데, 그 저장소에 따라 가성비가 달라지기 때문
세션에 내가 읽은 게시글의 번호를 저장할 수 있는 저장소를 구현
후보(숫자 여러 개를 저장할 수 있는): int[], List, Set
Set
: 순서x, 중복x -> 게시글 읽음 여부(중복 확인)를 알고 싶으므로 Set 선택정렬은 없어도 되므로 hash/tree set 무관
세션에 저장할 이름: history로 지정
현재 history라는 이름이 없을지 모르므로 꺼내서 없으면 생성(최초 1회)
session에 들어 있는 데이터는 모두 Object 타입 -> downcasting
+) 컬렉션은 Set까지는 변환해주는데 안에 Integer가 있을지 모르겠으니 감안하라는 에러 뜸. supresswarnings 굳이 할 필요 업음(어쩔 수 없는 부분)
현재 글 번호를 읽은 적이 있는지 중복 검사
set은 add, contains 모두 중복 검사를 함
history에 현재 글 번호를 추가 -> add가 true이면 추가가 된 것(= 처음 읽는 글)
갱신된 저장소를 세션에 다시 저장
//1. Set 선택
//2. history 생성(최초 1회)
Set<Integer> history = (Set<Integer>)session.getAttribute("history");
if(history == null) {//history가 없다면 신규 생성
history = new HashSet<>();
}
//3. 현재 글 번호를 읽은 적이 있는지 중복 검사
if(history.add(boardNo)) {//add가 true 처음 읽는 글
model.addAttribute("dto", boardDao.read(boardNo));
}else {//추가가 안된 경우 -> 읽은 적이 있는 번호
model.addAttribute("dto", boardDao.selectOne(boardNo));
}
//4. 갱신된 저장소를 세션에 다시 저장
session.setAttribute("history", history);
return "board/detail";
}
컴퓨터에게는 에러가 아니지만 개발자/사용자 입장에서는 에러일 때 처리 방법
상속
을 통한 자격 획득(Exception)RuntimeException
을 상속 받으면 추가 예외처리 생략 가능(Checking Exception)error 패키지 생성 > TargetNotFoundException 클래스 생성
@NoArgsConstructor
public class TargetNotFoundException extends RuntimeException {
//메시지를 처리할 수 있는 생성자
public TargetNotFoundException(String message) {
super(message);
}
}
@GetMapping("/delete")
public String delete(@RequestParam int boardNo) {
if(boardDao.delete(boardNo)) {
return "redirect:list";
}else {//구문은 실행됐지만 대상이 없는(바뀐 게 없는) 경우
//강제 예외 처리
throw new TargetNotFoundException();
//에러메시지 출력하고 싶으면 ()에 "문구" 추가
}
}
회원(관리자)만 등록/수정/삭제 페이지 접근
= 비회원은 조회/상세 제외하고 접근 불가
: 차단
이 기본
공지사항은 관리자만 작성 가능
아래 두 가지 중에 정책을 정해야 함 -- i) 선택
i) 관리자는 모든 글을 다 쓸 수 있는지?
ii) 관리자가 작성한 글은 모두 공지로 처리?
* 기능 구현 이후에 권한 설정을 추가하기 때문에 이전에 넣은 데이터 중에 일반 유저가 공지 글을 쓴 경우가 있다면 문제가 될 수 있음 → 기능을 다 만들고 나면 전체 데이터를 삭제해야 함
/board/write[POST], /board/edit[POST]에서 일반 회원이 ‘공지’글을 작성하는 것을 막아야 함
별도의 검사 인터셉터
를 구현해서 적용getMethod()
.getParameter()
//Interceptor
@Component //DB를 위한 도구에만 Repository 붙일 수 있음
public class MemberBoardPermissionCheckInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//0. POST 방식이 아니라면 통과
if(!request.getMethod().equals("POST")) {
return true;
}
//1. 관리자 여부 검사 - 관리자면 통과
HttpSession session = request.getSession();
String memberGrade = (String)session.getAttribute(SessionConstant.GRADE);
//memberGrade가 null일 가능성이 없음
if(memberGrade.equals("관리자")) {
return true;
}
//2. 1번이 아니라면, boardHead라는 파라미터 값이 '공지'이면 차단
//파라미터는 사용자가 요청한 정보에 있음
String boardHead = request.getParameter("boardHead");
if(boardHead != null && !boardHead.equals("공지")) {
return true;
}else {
response.sendError(403);
return false;
}
}
}
자신이 작성한 글만 수정/삭제 가능
관리자는 모든 글 삭제 가능
작성자만 통과시키는 로직으로 구현. 인터셉터에서 DB에 접근하면 성능 저하 이슈가 있지만, 남의 글 수정/삭제가 더 큰 문제이므로 그대로 진행함(DB 접근을 위해 BoardDao 주입)
관리자이면서 요청 URI가 /board/delete와 같으면 통과
System.out.println("uri = " + request.getRequestURI());
//관리자인데 삭제하는 경우
String memberGrade =
(String)session.getAttribute(SessionConstant.GRADE);
boolean isAdmin = memberGrade.equals("관리자");
boolean isDelete = request.getRequestURI().equals("/board/delete");
if(isAdmin && isDelete) {
return true;
}