회원, 게시글, 댓글을 관리하기위해서 따로 관리자 페이지를 만들어보자
IndexController 수정
package com.example.board_project.controller;
import com.example.board_project.config.auth.PrincipalDetail;
import com.example.board_project.entity.Board;
import com.example.board_project.service.BoardService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
import static com.example.board_project.entity.RoleType.ADMIN;
import static com.example.board_project.entity.RoleType.USER;
@Controller
public class IndexController {
@Autowired
private BoardService boardService;
@GetMapping("/")
public String index(Model model, //페이지 설정을 위한 @PageableDefault 설정
@PageableDefault(size = 8, page = 0, sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
@AuthenticationPrincipal PrincipalDetail principalDetail) {
//추가한 내용
if (principalDetail!=null&&principalDetail.getUser().getRole()==ADMIN) {
return "/admin/index";
}
Page<Board> boardList = boardService.takeAll(pageable);
int nowPage = boardList.getPageable().getPageNumber() + 1;
int startPage = (nowPage - 1) / 5 * 5 + 1;
int endPage = (startPage + 4 > boardList.getTotalPages()) ? boardList.getTotalPages() : startPage + 4;
model.addAttribute("boardList", boardList);
model.addAttribute("startPage", startPage);
model.addAttribute("nowPage", nowPage);
model.addAttribute("endPage", endPage);
model.addAttribute("searchTitle", null);
return "index";
}
}
사용자의 역할이 ADMIN이라면 다른 메인페이지로 이동하게 설정
<th:block sec:authorize="hasRole('ROLE_ADMIN')">
<li class="nav-item">
<a class="nav-link" href="/admin/user">회원관리</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/board">게시글 관리</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/reply">댓글 관리</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout">로그아웃</a>
</li>
</th:block>
<th:block sec:authorize="hasRole('ROLE_USER')">
<li class="nav-item">
<a class="nav-link" href="/board/write">글 작성</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/user/detail">내정보 보기</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout">로그아웃</a>
</li>
</th:block>
sec:authorize의 hasRole()함수를 사용해서 역할을 확인해서 메뉴를 다르게 보여준다.
AdminController 생성
package com.example.board_project.controller;
import com.example.board_project.entity.Board;
import com.example.board_project.entity.Reply;
import com.example.board_project.entity.User;
import com.example.board_project.service.BoardService;
import com.example.board_project.service.ReplyService;
import com.example.board_project.service.UserService;
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 java.util.List;
@Controller
public class AdminController {
private final UserService userService;
private final BoardService boardService;
private final ReplyService replyService;
public AdminController(UserService userService, BoardService boardService, ReplyService replyService) {
this.userService = userService;
this.boardService = boardService;
this.replyService = replyService;
}
@GetMapping("/admin/user")
public String userManage(Model model) {
List<User> userList = userService.findAll();
model.addAttribute("userList", userList);
return "admin/user";
}
@GetMapping("/admin/board")
public String boardManage(Model model) {
List<Board> boardList = boardService.findAll();
model.addAttribute("boardList", boardList);
return "admin/board";
}
@GetMapping("/admin/reply")
public String replyManage(Model model) {
List<Reply> replyList = replyService.findAll();
model.addAttribute("replyList", replyList);
return "admin/reply";
}
}
각각의 메뉴를 클릭했을때 모든 관련정보를 넘겨주게 설정
UserList 정보를 넘겨줄때에 사용자가 작성한 글과 댓글을 가져오고 싶었지만 처음에 Entity를 만들때에 연관관계를 설정해주지 않아서 갖고오지 못한다.
관리자페이지에서 User 정보를 갖고 해당 사용자가 쓴 글과 쓴 댓글을 가져오기 위해서 연관관계를 설정하자.
MultipleBagFetchException 이 발생했는데, 간략하게 말하자면 Eager fetch타입을 2개이상 써서 발생한 문제이다.
Eager fetch 타입이아닌 LAZY fetch 타입으로 변경
package com.example.board_project.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.*;
import java.sql.Timestamp;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(unique = true,nullable = false)
private String loginId; //로그인할때 id
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String phoneNumber;
@Column(nullable = false)
private String username; //별명
@Enumerated(EnumType.STRING) //해당 Enum이 스트링이 라고 알려준다
private RoleType role; //Enum을 쓰는게 좋다
@JsonIgnoreProperties({"user"})//는 특정 필드가 직렬화는 허용하지만 역직렬화는 허용하지 않게 해주는 어노테이션입니다.
@OneToMany(mappedBy = "user",fetch = FetchType.LAZY,cascade = CascadeType.REMOVE) // // 1:n 관계에서 1을 replylist 에 써줌
//fetch = FetchType.EAGER는 즉시 로딩 기능.
//mappedBy도 마찬가지로 주인설정하는것
private List<Reply> replyList;
@JsonIgnoreProperties({"user"})//는 특정 필드가 직렬화는 허용하지만 역직렬화는 허용하지 않게 해주는 어노테이션입니다(무한참조 방지).
@OneToMany(mappedBy = "user",fetch = FetchType.LAZY,cascade = CascadeType.REMOVE) // // 1:n 관계에서 1을 replylist 에 써줌
//fetch = FetchType.EAGER는 즉시 로딩 기능.
//mappedBy도 마찬가지로 주인설정하는것
private List<Board> boardList;
@CreationTimestamp
private Timestamp createTime;
}
net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 이 오류는
/*<![CDATA[*/
let replyList = [[${replyList}]];
let loginUser = [[${loginUser}]];
/*]]*/
이부분에서 loginUser의 정보를 갖고오는데서 발생하는거같다.
따라서 컨트롤러로 가서 loginUser의 정보를 보려고 테스트를 해봤는데 org.hibernate.LazyInitializationException 의 오류가 발생했다.
org.hibernate.LazyInitializationException 이 오류는 member를 조회까지는 성공했는데, member.get MemberSocialProfiles() 를 호출해서 사용할 때 영속성 컨텍스트가 종료되어 버려서, 지연 로딩을 할 수 없어서 발생하는 오류 입니다.
컨트롤러에서 클라이언트와 직접적으로 데이터 교환을 할때에 entity 객체를 사용해서 데이터를 넘겨주었기때문에 오류가 발생했습니다. 주로 View 와 Controller 사이에서 데이터를 주고받을 때에는 dto로 변환해주어서 교환을 한다. dto객체를 넘겨주어서 해결
UserDto
package com.example.board_project.dto;
import com.example.board_project.entity.Board;
import com.example.board_project.entity.Reply;
import com.example.board_project.entity.RoleType;
import com.example.board_project.entity.User;
import lombok.*;
import java.sql.Timestamp;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
public class UserDto {
private int id;
private String loginId; //로그인할때 id
private String password;
private String name;
private String phoneNumber;
private String username; //별명
private RoleType role; //Enum을 쓰는게 좋다
private List<Reply> replyList;
private List<Board> boardList;
private Timestamp createTime;
static public UserDto userToRoxyUser(User user){
UserDto proxyUser = UserDto.builder()
.id(user.getId())
.loginId(user.getLoginId())
.password(user.getPassword())
.name(user.getName())
.phoneNumber(user.getPhoneNumber())
.role(user.getRole())
.replyList(user.getReplyList())
.boardList(user.getBoardList())
.createTime(user.getCreateTime())
.build();
return proxyUser;
}
}
만들어서 해결해주려했지만 계속 해서 오류가 발생하였고 긴 시간끝에 알게된것은 dto끼리에도 연관관계가 적용되어있어서 참조오류가 발생한것이다 dto를 사용할때는 원하는 정보만 넘어갈 수 있게 설정하자!
서버와 클라이언트 사이에서 정보교환을 Entity로 했던 잘못을 깨우치면서 모든 컨트롤러 서비스를 Entity에서 Dto로 바꾼다.
이름을 결정
BoardDto는 오로지 Board의 정보를 갖고있다 다른 객체와 연관되지않은
BoardSelectDto는 연관된 정보를 갖고있지만 SelectDto를 갖고있다.
때문에 view로 데이터를 넘길때는 BoardSelectDto를 사용한다.
(코드참고)
BoardDto
package com.example.board_project.dto;
import com.example.board_project.entity.Board;
import com.example.board_project.entity.Reply;
import com.example.board_project.entity.User;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import javax.persistence.*;
import java.sql.Timestamp;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
public class BoardDto { //오직 Board의 정보만 갖고있다.
private int id;
private String title;
private String content;
private int hit; //board 가 작성되는 순간 0 으로 설정
private Timestamp createTime;
static public BoardDto boardToBoardDto(Board board) {
BoardDto boardDto = BoardDto.builder()
.id(board.getId())
.title(board.getTitle())
.content(board.getContent())
.hit(board.getHit())
.createTime(board.getCreateTime())
.build();
return boardDto;
}
}
BoardSelectDto
package com.example.board_project.dto;
import com.example.board_project.entity.Board;
import com.example.board_project.entity.Reply;
import lombok.*;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
public class BoardSelectDto { //오직 Board의 정보만 갖고있다.
private int id;
private String title;
private String content;
private int hit; //board 가 작성되는 순간 0 으로 설정
private UserDto userDto;
private List<ReplyDto> replyDtoList;
private Timestamp createTime;
static public BoardSelectDto boardToBoardSelectDto(Board board) {
List<Reply> replyList = board.getReplyList();
List<ReplyDto> replyDtoList1 = new ArrayList<>();
for (Reply reply : replyList) {
replyDtoList1.add(ReplyDto.replyToReplyDto(reply));
}
BoardSelectDto boardDto = BoardSelectDto.builder()
.id(board.getId())
.title(board.getTitle())
.content(board.getContent())
.hit(board.getHit())
.userDto(UserDto.userToUserDto(board.getUser()))
.replyDtoList(replyDtoList1)
.createTime(board.getCreateTime())
.build();
return boardDto;
}
}
중요!
view로 데이터를 넘길때는 BoardSelectDto를 사용한다.
왜냐 -> SelectDto들은 연관된 정보들까지 갖고있다.
하지만 연관된 객체가 아닌 UserDto(오로지 user의 정보를 갖고있는), ReplyDto(오로지 reply의 정보를 갖고있는) 등과 같은 정보와 연결되어있기때문에 중복참조 오류가 발생할 일이없다.
수정하는 부분에서 Page를 초기화 할수있는 방법이 있을까 해서 찾아보게되었다.
알고보니 Page는 JPA에서 관리하고 JPA에서 받아온 Entity객체를 Dto로 변환해서 받을 수 있는 방법이 있었다. -> map 함수를 사용하는 것이다.
public Page<BoardSelectDto> searchBoard(String searchTitle,Pageable pageable) {
//boardRepository의 findByTitleContaining함수로 Page<Board> 객체를 갖고오고
// 이 객체를 BoardSelectDto의 boardToBoardSelectDto함수를 사용해서 매핑한 객체를 갖고와라
//맵 함수 기억하자!
Page<BoardSelectDto> boardSelectDtoPage = boardRepository.findByTitleContaining(searchTitle, pageable)
.map(BoardSelectDto::boardToBoardSelectDto);
return boardSelectDtoPage;
}
문제점이 발생하지않게 클라이언트와 서버간에 데이터교환은 Dto로 진행하자