[홈페이지 제작] 게시판 만들기 2 - 게시글 상세보기

Feb 04, 2024
[홈페이지 제작] 게시판 만들기 2 - 게시글 상세보기
 

1. 전체 코드

config

/shop.mtcoding.blog/config/Constant
package shop.mtcoding.blog._core; // ValueObject public class Constant { public static final int PAGING_COUNT = 3; // 한 페이지에 3개씩 글 표시함 }
/shop.mtcoding.blog/config/PagingUtil
package shop.mtcoding.blog._core; public class PagingUtil { public static boolean isFirst(int currentPage){ return currentPage == 0 ? true : false; } // 책임 : 마지막 페이지인지 여부 public static boolean isLast(int currentPage, int totalCount){ int totalPageCount = getTotalPageCount(totalCount); return currentPage+1 == totalPageCount ? true : false; } // 책임 : 전체 페이지 개수 리턴 public static int getTotalPageCount(int totalCount){ // 1. 나머지 여부 확인 int remainCount = totalCount % shop.mtcoding.blog._core.Constant.PAGING_COUNT; int totalPageCount = totalCount / shop.mtcoding.blog._core.Constant.PAGING_COUNT; // 2. 나머지가 있다면? if(remainCount > 0){ totalPageCount = totalPageCount + 1; } return totalPageCount; } }
 

user

/shop.mtcoding.blog/user/User
package shop.mtcoding.blog.user; import jakarta.persistence.*; import lombok.Data; import org.hibernate.annotations.CreationTimestamp; import java.time.LocalDate; // 여기 있는 @는 컨포넌트 스캔은 안됨 //db에서 조회된 user 데이터베이스 값을 여기에 받음 @Data //게터세터 @Entity // 이게 붙으면 테이블 생성됨 @Table(name="user_tb") // 테이블명 public class User { @Id // 프라이머리키 설정 @GeneratedValue(strategy = GenerationType.IDENTITY) //auto_increment private int id ; @Column(unique = true) // 유저네임은 유니크 private String username; @Column(length = 60,nullable = false) //비밀번호 길이는 60, null 불가 private String password; private String email; @CreationTimestamp private LocalDate createdAt ; }
/shop.mtcoding.blog/user/UserController
package shop.mtcoding.blog.user; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; //컨트롤러의 역할 // 1. 요청받기(URL, URI) // 2. 바디는 DTO로 받음 // 3. 기본 MIME 전략 : x-www-form-urlencoded // 4. 유효성 검사 (바디데이터가 있드면) // 5. 클라이언트가 뷰만 원하는지, 혹은 db 처리 후 뷰(여기서는 머스타치) 도 원하는지 // 6. 뷰만 원하면 뷰만 응답, db처리를 원하면 모델에게 위임 후 뷰만 응답하면 끝 //@AllArgsConstructor //이걸 하면 생성자는 필요없으나, int id = 1 ; 이런 식의 변수가 있으면 터짐 @RequiredArgsConstructor //final 을 붙이고 이걸 사용하면 됨 @Controller public class UserController { private final UserRepository userRepository; // 의존성 주입 받기 위해 만듬. 의존성 주입을 받을 떄 final을 붙여서 사용함 private final HttpSession session ; // // public UserController(UserRepository userRepository) { // this.userRepository = userRepository; // 생성자를 만들어서 디폴트 생성자를 제거함. // } // 의존성 주입하는 법 // 1. 내가 생성자 만든느법 // 2. @AllArgsConstructor 만들기 // 3. final 붙이고 @RequiredArgsConstructor 붙이기 // @Transactional // 여기 트랜잭션을 걸게 되면 유효성 검사부터 트랜잭션 걸림. 그래서 트랜잭션을 묶는 레이어가 필요. @PostMapping("/join") public String join(UserRequest.JoinDTO requsetDTO){ // 클래스로 매개변수로 한방에 받기. System.out.println(requsetDTO); //1. 유효성 검사, 데이터베이스 없이 검사해야 됨. 근데 아이디가 중복되면 db를 조회해야됨 if(requsetDTO.getUsername().length()<3){ return "error/400"; } //2.동일 유저네임 체크, 트라이캐치로 잡을 수도 있음. 근데 트라이캐치 전에 잡을 수 있다면 잡는게 좋다. // 2, 3번을 동시에 트랜잭션을 걸어야 가입을 할때 동일한 아이디 여부를 확인하고 회원가입할 수 있음. 아니면 중복확인은 했는데 가입할 때 사용한 아이디가 되 수 있음 // 나중에 2 3 번을 하나의 레이어로 빼서 만드ㅡ는게 좋음 // 나중에 하나의 트랜잭션으로 묶는게 좋다. User user = userRepository.findByUsername(requsetDTO.getUsername()); if(user == null){ userRepository.save(requsetDTO) ; // 위임 }else { return "error/400"; } //3.DB 인서트 - 모델에게 위임하기 return"redirect:/loginForm"; } @PostMapping("/login")// select 는 get 요청을 해야함. 하지만 로그인은 민감한 정보기 때문에 get 요청을 하면 쿼리스트링으로 오기 때문에 post 요청으로 함 public String login(UserRequest.JoinDTO requsetDTO){ if(requsetDTO.getUsername().length()<3){ return "error/400"; } // 2. 모델 연결 User user = userRepository.findByUsernameAndPaaword(requsetDTO); if(user==null){ return "error/401"; }else { session.setAttribute("sessionUser",user); // setAttribute 해쉬맵 키 : 값 return "redirect:/"; // 메인으로 연결 } } @GetMapping("/joinForm") public String joinForm() { return "user/joinForm"; } @GetMapping("/loginForm") public String loginForm() { return "user/loginForm"; } @GetMapping("/user/updateForm") public String updateForm() { return "user/updateForm"; } @GetMapping("/logout") public String logout() { session.invalidate(); // 로그아웃. 서랍의 내용을 다 삭제 // 깃 테스트 코드 return "redirect:/"; // 깃테스트 } }
/shop.mtcoding.blog/user/UserRepository
package shop.mtcoding.blog.user; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; //DAO @Repository // 레파지토리가 new 됨 public class UserRepository { private EntityManager em ; public UserRepository(EntityManager em) { this.em = em; // 의존성 주입, ioc 컨테이너에 EntityManager가 존재함. } @Transactional // 이게 없으면 쿼리문을 전송안한다. select 조회하는거기 때문에 상관없음. public void save(UserRequest.JoinDTO requestDTO){ System.out.println("userRepository에 save 메서드 호출됨"); Query query = em.createNativeQuery("insert into user_tb(username,password, email) values(?,?,?)"); query.setParameter(1,requestDTO.getUsername()); query.setParameter(2,requestDTO.getPassword()); query.setParameter(3,requestDTO.getEmail()); query.executeUpdate(); } @Transactional //하이버네이트 사용, 셀렉트에는 필요없음 public void saveV2(UserRequest.JoinDTO requestDTO){ User user = new User(); user.setUsername(requestDTO.getUsername()); user.setPassword(requestDTO.getPassword()); user.setEmail(requestDTO.getEmail()); // System.out.println("UserRepository " +1); 디버깅할 때 번호를 매겨서 어디서 터지는지 확인 em.persist(user); } public User findByUsernameAndPaaword(UserRequest.JoinDTO requsetDTO) { Query query = em.createNativeQuery("select * from user_tb where username=? and password=?",User.class); //User.class 알아서 맵핑.@Entity가 있어야됨 ResultSet해서 하나씩 파싱 안해도 됨. query.setParameter(1,requsetDTO.getUsername()); query.setParameter(2,requsetDTO.getPassword()); // getSingleResult는 내부적으로 트라이캐치가 구현되어 있음. 내가 원하는 트라이캐치를 원하면 내가 새로 만들면 됨 // 내부적으로 터지면 터지는 위치를 내가 찾아서 트라이캐치해야 됨/ // 터지는 위치를 찾고 싶다면 system.out.print(1) 이런 식으로 번호를 넣어서 어디서 터지는지 찾아야됨. try{ User user = (User) query.getSingleResult(); // getSingleResult()의 리턴값이 오브젝트라 다운캐스팅 return user ; }catch(Exception e){ return null ; } } public User findByUsername(String username) { Query query = em.createNativeQuery("select * from user_tb where username=?",User.class); //User.class 알아서 맵핑.@Entity가 있어야됨 ResultSet해서 하나씩 파싱 안해도 됨. query.setParameter(1,username); // getSingleResult는 내부적으로 트라이캐치가 구현되어 있음. 내가 원하는 트라이캐치를 원하면 내가 새로 만들면 됨 // 내부적으로 터지면 터지는 위치를 내가 찾아서 트라이캐치해야 됨/ // 터지는 위치를 찾고 싶다면 system.out.print(1) 이런 식으로 번호를 넣어서 어디서 터지는지 찾아야됨. try{ User user = (User) query.getSingleResult(); // getSingleResult()의 리턴값이 오브젝트라 다운캐스팅 return user ; }catch(Exception e){ return null ; } } }
/shop.mtcoding.blog/user/UserResponse
package shop.mtcoding.blog.user; import lombok.Data; // 요청DTO - data transfer object public class UserRequest { // 요청데이터를 여기 받음 @Data // 얘가 게터세터 다 가지고 있음. public static class JoinDTO{ private String username ; private String password; private String email ; } @Data // public static class LoginDTO{ private String username ; private String password; } }
 
 

board

/shop.mtcoding.blog/board/Board
package shop.mtcoding.blog.board; import jakarta.persistence.*; import lombok.Data; import org.hibernate.annotations.CreationTimestamp; import java.time.LocalDateTime; @Data //게터세터,toString @Entity // entity로 만든 것만 파싱함. @Table(name="board_tb") // 테이블명 public class Board { @Id // 프라이머리키 설정 @GeneratedValue(strategy = GenerationType.IDENTITY) //auto_increment //포링키 테이블에 제약조건은 안넣는게 좋다. 삭제할 때 문제 생김 private int id ; private String title; private String content; private int userId ; // 포링키 , 포링키에 //타입이 스칼라가 아닌 여러개면 쪼개야됨. @CreationTimestamp private LocalDateTime createdAt ; }
/shop.mtcoding.blog/board/BoardController
package shop.mtcoding.blog.board; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import shop.mtcoding.blog.user.User; import java.util.List; @RequiredArgsConstructor @Controller public class BoardController { //Ioc컨테이너에 세션에 접근할 수 있는 변수가 들어가 있음. DI 하면 됨 private final HttpSession session; private final BoardRepository boardRepository ; //@RequestParam(defaultValue = "0") 이거는 값을 안넣으면 페이지=0 으로 설정 @GetMapping({ "/", "/board" }) public String index(HttpServletRequest request, @RequestParam(defaultValue = "0") int page) { List<Board> boardList = boardRepository.findAll(page); request.setAttribute("boardList", boardList); int currentPage = page; int nextPage = currentPage+1; int prevPage = currentPage-1; request.setAttribute("nextPage", nextPage); request.setAttribute("prevPage", prevPage); boolean first = shop.mtcoding.blog._core.PagingUtil.isFirst(currentPage); boolean last = shop.mtcoding.blog._core.PagingUtil.isLast(currentPage, 4); request.setAttribute("first", first); request.setAttribute("last", last); return "index"; } @GetMapping("/board/saveForm") public String saveForm() { return "board/saveForm"; } @GetMapping("/board/{id}") // board 뒤에 1은 pk. pk는 게시글 뒤에 바로 이름을 작성함 public String detail(@PathVariable int id, HttpServletRequest request) { // {} 를 알아서 파싱해줌 BoardResponse.DetailDTO responseDTO = boardRepository.findById(id); request.setAttribute("board",responseDTO); //1. 해당 페이지의 주인 여부 boolean owner = false ; //2 . 작성자 userId 확읺기 int boardUserId = responseDTO.getUserId(); //3. 로그인 여부 체크 / 로그인이 안되면 무조건 false, 권한 체크 User sessionUser = (User) session.getAttribute("sessionUser"); // setAttribute 에서 키를 sessionUser 로 정함 if(sessionUser !=null){ // 로그인을 했음 if(boardUserId == sessionUser.getId()){ owner =true ; } } request.setAttribute("owner",owner); return "board/detail"; } }
/shop.mtcoding.blog/board/BoardRepository
package shop.mtcoding.blog.board; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import lombok.RequiredArgsConstructor; import org.qlrm.mapper.JpaResultMapper; import org.springframework.stereotype.Controller; import shop.mtcoding.blog._core.Constant; import java.util.List; @RequiredArgsConstructor @Controller public class BoardRepository { private final EntityManager em ; public int count(){ Query query = em.createNativeQuery("select count(*) from board_tb"); Long count = (Long) query.getSingleResult(); return count.intValue(); } public List<Board> findAll(int page){ final int COUNT = 3; int value = page * COUNT ; Query query = em.createNativeQuery("select * from board_tb order by id desc limit ?,?",Board.class); // 한 페이지에 3개씩 뿌림 query.setParameter(1,value); query.setParameter(2,Constant.PAGING_COUNT); List<Board> boardList = query.getResultList(); return boardList; } public BoardResponse.DetailDTO findById(int id) { //entity 가 아닌 것은 jpa가 파싱 안해줌. join 하면 entity가 아님. 그래서 qlrm 라이브러리를 받음 Query query = em.createNativeQuery("select bt.id, bt.title, bt.content, bt.created_at, bt.user_id, ut.username from board_tb bt inner join user_tb ut on bt.user_id = ut.id where bt.id =?"); // 한 페이지에 3개씩 뿌림 query.setParameter(1,id); // 쿼리문에서 만든 값 순서대로 DTO 만들어야됨. * 쓰지말고 순서대로 적어야됨. JpaResultMapper rm = new JpaResultMapper() ; // join 으로 임시테이블을 만들었을 때 받을 클래스 and 엔티티가 아닐 떄 사용 BoardResponse.DetailDTO responseDTO = rm.uniqueResult(query,BoardResponse.DetailDTO.class); return responseDTO ; } }
/shop.mtcoding.blog/board/BoardResponse
package shop.mtcoding.blog.board; import lombok.AllArgsConstructor; import lombok.Data; import java.sql.Timestamp; public class BoardResponse { @AllArgsConstructor // 디폴트 생성자를 때리는게 아니라 풀생성자를 때림. qmrl 은 풀생성자가 있어야됨. @Data public static class DetailDTO{ //bt.id, bt.content, bt.title, bt.created_at, ut.id uid, ut.username //bt.id, bt.title, bt.content, bt.created_at, bt.user_id, ut.username private Integer id ; private String title; private String content ; private Timestamp createdAt; private Integer userId ; private String username ; } }
 

DB

resources/DB/data.sql
insert into user_tb(username, password, email, created_at) values('ssar', '1234', 'ssar@nate.com', now()); insert into user_tb(username, password, email, created_at) values('cos', '1234', 'cos@nate.com', now()); insert into board_tb(title,content,user_id,created_at) values ('제목1','내용1',1,now()); insert into board_tb(title,content,user_id,created_at) values ('제목2','내용2',1,now()); insert into board_tb(title,content,user_id,created_at) values ('제목3','내용3',1,now()); insert into board_tb(title,content,user_id,created_at) values ('제목4','내용4',2,now());
 

templates

templates/board/detail.mustache
{{> layout/header}} <div class="container p-5"> {{#owner}} <!-- 수정삭제버튼 --> <div class="d-flex justify-content-end"> <button class="btn btn-warning me-1">수정</button> <button class="btn btn-danger">삭제</button> </div> {{/owner}} <div class="d-flex justify-content-end mt-2"> <b>작성자</b> : {{board.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{board.title}}</b></h2> <hr /> <div class="m-4 p-2"> {{board.content}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/reply/save" method="post"> <textarea class="form-control" rows="2" name="comment"></textarea> <div class="d-flex justify-content-end"> <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">cos</div> <div>댓글 내용입니다</div> </div> <form action="/reply/1/delete" method="post"> <button class="btn">🗑</button> </form> </div> <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">ssar</div> <div>댓글 내용입니다</div> </div> <form action="/reply/1/delete" method="post"> <button class="btn">🗑</button> </form> </div> </div> </div> </div> {{> layout/footer}}
templates/board/saveForm.mustache
{{> layout/header}} <div class="container p-5"> <!-- 요청을 하면 localhost:8080/board/save POST로 요청됨 title=사용자입력값&content=사용자값 --> <div class="card"> <div class="card-header"><b>글쓰기 화면입니다</b></div> <div class="card-body"> <form action="/board/save" method="post" enctype="application/x-www-form-urlencoded"> <div class="mb-3"> <input type="text" class="form-control" placeholder="Enter title" name="title"> </div> <div class="mb-3"> <textarea class="form-control" rows="5" name="content"></textarea> </div> <button type="submit" class="btn btn-primary form-control">글쓰기완료</button> </form> </div> </div> </div> {{> /layout/footer}}
 
templates/error/400.mustache
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>클라이언트가 요청을 잘못하였습니다.</h1> </body> </html>
templates/error/401.mustache
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>클라이언트가 요청을 잘못하였습니다.</h1> </body> </html>
 
templates/layout/footer.mustache
<div class="bg-light p-5 text-center"> <h4>Created by Metacoding</h4> <h5>☎ 010-1111-2222</h5> <button class="btn btn-outline-primary">고객센터</button> <button class="btn btn-outline-primary">오시는길</button> </div> </body> </html>
templates/layout/header.mustache
<!DOCTYPE html> <html lang="en"> <head> <title>Blog</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" /> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </head> <body> <nav class="navbar navbar-expand-sm bg-primary navbar-primary"> <div class="container-fluid"> <a class="navbar-brand" href="/">Home</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#collapsibleNavbar"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="collapsibleNavbar"> <ul class="navbar-nav"> {{#sessionUser}} <li class="nav-item"> <a class="nav-link" href="/board/saveForm">글쓰기</a> </li> <li class="nav-item"> <a class="nav-link" href="/user/updateForm">회원정보보기</a> </li> <li class="nav-item"> <a class="nav-link" href="/logout">로그아웃</a> </li> {{/sessionUser}} {{^sessionUser}} <li class="nav-item"> <a class="nav-link" href="/joinForm">회원가입</a> </li> <li class="nav-item"> <a class="nav-link" href="/loginForm">로그인</a> </li> {{/sessionUser}} <!-- 이렇게 적으면 세션, 리퀘스트에 접근. #은 if문 ^는 else 문 --> </ul> </div> </div> </nav>
 
templates/user/index.mustache
{{> layout/header}} <!--머스태치 문법/ 이렇게 적으면 헤더를 매번 포함할 수 있음--> <!--리퀘스트에 있는걸 꺼내옴 템플릿 엔진은 리퀘스트에 담음. 데이터르 화면에 전달한게 아님 데이터는 가방에 담음. 화면은 가방에 있는걸 꺼냄. 오브젝트면 if문이 되고, 컬렉션일 떄는 for 문으로 바뀜--> {{#boardList}} <div class="card mb-3"> <div class="card-body"> <h4 class="card-title mb-3">{{title}}</h4> <a href="/board/{{id}}" class="btn btn-primary">상세보기</a> </div> </div> {{/boardList}} <div class="container p-5"> <ul class="pagination d-flex justify-content-center"> <li class="page-item {{#first}}disabled{{/first}} "><a class="page-link" href="?page={{prevPage}}">Previous</a></li> <li class="page-item {{#last}}disabled{{/last}} "><a class="page-link" href="?page={{nextPage}}">Next</a></li> </ul> </div> {{> layout/footer}}
 

yml

application.yml
spring: profiles: active: - dev
application-dev.yml
server: servlet: encoding: charset: utf-8 force: true session: timeout: 30m port: 8080 spring: datasource: driver-class-name: org.h2.Driver url : jdbc:h2:mem:test;MODE=MySQL username : sa password : mustache: servlet: expose-session-attributes: true expose-request-attributes: true # 머스터치에서 세션과 리퀘스트에 접근할 수 있도록 하는 코드 #db 연결 h2: console: enabled: true sql: init: data-locations: - classpath:db/data.sql # 클레스패스는 리소스폴더, data-locations 는 리스트타입/ 야물 문법 #웹에 연결될 수 있게 jpa: hibernate: ddl-auto: create show-sql: true properties: hibernate: format_sql: true defer-datasource-initialization: true #서버가 실행될 때 @entity 되어있는걸 크리에이트함. #hibernate 가 실행될 때 show-sql: true 면 내용 띄워줌
application-prod.yml
server: servlet: encoding: charset: utf-8 force: true port: 5000
 
 

2. 게시글 상세보기 보기

 
notion image
지난 블로그에서 기본 페이지에 게시글을 표시하는 것과 페이지를 넘기기를 만들었다.
 
 
이번 블로그에서는 게시글 상세보기 페이지를 만들어보자.
 
우선 처음으로 게시글에 표현할 데이터 쿼리문을 만들어보자.
 
 
게시글에서 필요한 데이터는 작성자 id, 게시판 제목, 게시판 내용, 게시판의 번호, 작성 시간이 필요하다.
여기서 게시판의 번호는 primary key 이기 때문에 드라이빙 테이블은 board 테이블이다.
 
select 이너 조인문으로 쿼리를 작성했다.
select bt.id, bt.title, bt.content, bt.created_at, bt.user_id, ut.username from board_tb bt inner join user_tb ut on bt.user_id = ut.id where bt.id =ut.id
 
bt.id (테이블 id, primary key) , bt.title(게시글 제목), bt.content(게시글 내용) , bt.created_at(게시글 작성시간), bt.user_id(유저 id) , ut.username(유저네임)을 조회하기로 정하고 , 게시글 id 와 유저 id 가 동일한 데이터를 조회한다.
 
notion image
 
 
쿼리문으로 DB 에 데이터를 요청한다. 데이터를 받을 오브젝트가 없기 때문에 오브젝트를 만든다.
 
package shop.mtcoding.blog.board; import lombok.AllArgsConstructor; import lombok.Data; import java.sql.Timestamp; public class BoardResponse { @AllArgsConstructor // 디폴트 생성자를 때리는게 아니라 풀생성자를 때림. qmrl 은 풀생성자가 있어야됨. @Data public static class DetailDTO{ //select bt.id, bt.title, bt.content, bt.created_at, bt.user_id, ut.username from board_tb bt inner join user_tb ut on bt.user_id = ut.id where bt.id =? private Integer id ; private String title; private String content ; private Timestamp createdAt; private Integer userId ; private String username ; } }
 
💡
DTO 의 변수는 쿼리문의 변수 순서대로 작성해야 한다. @Data 는 디폴트 생성자와 Getter, Setter 를 만들어주는데, 풀생성자가 필요하다. 그래서 @AllArgsConstructor 가 필요하다.
 
 
그리고 QLRM 라이브러리를 설치한다.
// https://mvnrepository.com/artifact/org.qlrm/qlrm implementation group: 'org.qlrm', name: 'qlrm', version: '4.0.1'
 
QLRM은 SQL 쿼리 결과를 자바 객체로 매핑하는 라이브러리 중 하나이다.
 
public BoardResponse.DetailDTO findById(int id) { Query query = em.createNativeQuery("select bt.id, bt.title, bt.content, bt.created_at, bt.user_id, ut.username from board_tb bt inner join user_tb ut on bt.user_id = ut.id where bt.id =?"); // 한 페이지에 3개씩 뿌림 query.setParameter(1,id); // 쿼리문에서 만든 값 순서대로 DTO 만들어야됨. * 쓰지말고 순서대로 적어야됨. JpaResultMapper rm = new JpaResultMapper() ; // join 으로 임시테이블을 만들었을 때 받을 클래스 and 엔티티가 아닐 떄 사용 BoardResponse.DetailDTO responseDTO = rm.uniqueResult(query,BoardResponse.DetailDTO.class); return responseDTO ; }
 
그리고 BoardRepository에 findById 메서드를 만든다.
JPA 라이브러리는 @Entity 로 되어있는 오브젝트만 파싱한다.
 
notion image
 
따라서 Board 클래스나, User 클래스는 @Entity가 있기 떄문에 파싱 가능하지만, join 문을 사용한 테이블은 실제 존재하는 테이블이 아니기 때문에 @Entity 를 사용할 수 없다.
그래서 QLRM 라이브러리를 활용한다.
 
JpaResultMapper 는 특정 쿼리 결과를 엔터티가 아닌 별도의 객체로 매핑하고자 할 때 사용한다.
JpaResultMapper 를 사용해 쿼리문의 결과값을 DTO 타입으로 받을 수 있다.
 
uniqueResult 는 쿼리문에서 유일한 데이터 값을 반환할 때 사용한다.
게시글 id와 유저 id가 일치하는 데이터는 하나 밖에 없기 때문이다.
 
 
BoardController 코드
@GetMapping("/board/{id}") // board 뒤에 1은 pk. pk는 게시글 뒤에 바로 이름을 작성함 public String detail(@PathVariable int id, HttpServletRequest request) { // {} 를 알아서 파싱해줌 BoardResponse.DetailDTO responseDTO = boardRepository.findById(id); request.setAttribute("board",responseDTO); return "board/detail"; }
 
@PathVariable 를 사용해 식별자의 변수를 추적해준다.
 
board/detail.mustache
<div class="d-flex justify-content-end mt-2"> <b>작성자</b> : {{board.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{board.title}}</b></h2> <hr /> <div class="m-4 p-2"> {{board.content}} </div> </div>
 
findById 에서 responseDTO 를 리턴하면 board.mustache 에서 사용할 수 있다.
mustache 문법 중 {{board.username}} 처럼 객체명.속성값을 넣을 수 있다.
 
 
 
notion image
 
 
 

3. DTO 를 활용한 매핑

 
public BoardResponse.DetailDTO findByIdJoinUser(int boardId) { String q = """ select b.id,b.title,b.content,b.user_id,u.username from board_tb b inner join user_tb u on u.id = b.user_id where b.id =? """; Query query = em.createNativeQuery(q); query.setParameter(1,boardId); Object[] row = (Object[]) query.getSingleResult(); Integer id = (Integer) row[0]; String title= (String) row[1]; String content = (String) row[2]; Integer userId = (Integer) row[3]; String username = (String) row[4]; BoardResponse.DetailDTO responseDTO = new BoardResponse.DetailDTO(); responseDTO.setId(id); responseDTO.setTitle(title); responseDTO.setContent(content); responseDTO.setUserId(userId); responseDTO.setUsername(username); return responseDTO; }
 
💡
라이브러리를 사용하지 않고 오브젝트 타입으로 받은 데이터를 하나씩 DTO 에 담아서 리턴한다.
 
@GetMapping("/board/{id}") public String detail(@PathVariable int id, HttpServletRequest request){ BoardResponse.DetailDTO responseDTO = boardRepository.findByIdJoinUser(id); request.setAttribute("responseDTO",responseDTO); return "board/detail"; }
Share article

{CODE-RYU};