package com.example.locallibrary.dto;
import java.time.LocalDate;
// 저자 상세보기 DTO
public class AuthorDetailsResponse {
private String name; // 저자 이름
private String birthDate;
private String deathDate;
// 생성자
public AuthorDetailsResponse() {}
// Getter/Setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBirthDate() {
return birthDate;
}
public void setBirthDate(LocalDate birthDate) {
// 날짜 객체를 바로 출력가능하도록 문자열로 변환
// 출생일/사망일 정보가 없으면 빈 문자열을 출력
this.birthDate = birthDate != null ? birthDate.toString() : "";
}
public String getDeathDate() {
return deathDate;
}
public void setDeathDate(LocalDate deathDate) {
this.deathDate = deathDate != null ? deathDate.toString() : "";
}
}
package com.example.locallibray.dto;
import com.example.locallibray.enums.Status;
// 책 사본 상세페이지 DTO
public class InstanceOfBookDetailsResponse {
private String title; // 책 제목
private String imprint; // 출판사
private Status status; // 책 상태
private String dueBack; // 반납 예정일 (대출중인 경우에만 출력)
// 생성자
public InstanceOfBookDetailsResponse() {}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImprint() {
return imprint;
}
public void setImprint(String imprint) {
this.imprint = imprint;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public String getDueBack() {
return dueBack;
}
// LocalDate: 날짜 객체. 바로 출력할 수 없다
public void setDueBack(String dueBack) {
// 날짜 객체.toString(): 날짜 객체를 문자열 타입으로 변환 예) 2020-01-12
this.dueBack = dueBack != null? dueBack.toString(): null;
}
}
package com.example.locallibrary.dto;
import java.util.List;
// 책 검색 결과 DTO
public class BookSearchResponse {
// 검색 결과 수
private Long resultCount;
private List<ResultBook> resultBooks;
// 생성자
public BookSearchResponse() {}
// Getter/Setter
public Long getResultCount() {
return resultCount;
}
public void setResultCount(Long resultCount) {
this.resultCount = resultCount;
}
public List<ResultBook> getResultBooks() {
return resultBooks;
}
public void setResultBooks(List<ResultBook> resultBooks) {
this.resultBooks = resultBooks;
}
// 검색 결과 나오는 책 정보
public static class ResultBook{
private String title; // 책 제목
private String authorName; // 저자
private Long instanceCount; // 책에 딸린 사본의 개수
private String url; // 책 상세페이지 링크
// 생성자
public ResultBook() {}
// Getter/Setter
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthorName() {
return authorName;
}
public void setAuthorName(String authorName) {
this.authorName = authorName;
}
public Long getInstanceCount() {
return instanceCount;
}
public void setInstanceCount(Long instanceCount) {
this.instanceCount = instanceCount;
}
public String getUrl() {
return url;
}
public void setUrl(Long id) {
this.url = "/book/" + id;
}
}
}
package com.example.locallibrary.dto;
import java.time.LocalDate;
// 저자 상세보기 DTO
public class AuthorDetailsResponse {
private String name; // 저자 이름
private String birthDate;
private String deathDate;
// 생성자
public AuthorDetailsResponse() {}
// Getter/Setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBirthDate() {
return birthDate;
}
public void setBirthDate(LocalDate birthDate) {
// 날짜 객체를 바로 출력가능하도록 문자열로 변환
// 출생일/사망일 정보가 없으면 빈 문자열을 출력
this.birthDate = birthDate != null ? birthDate.toString() : "";
}
public String getDeathDate() {
return deathDate;
}
public void setDeathDate(LocalDate deathDate) {
this.deathDate = deathDate != null ? deathDate.toString() : "";
}
}
package com.example.locallibrary.controller;
import java.util.List;
import java.util.stream.Collectors;
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 com.example.locallibrary.dto.BookDetailsResponse;
import com.example.locallibrary.dto.BookDetailsResponse.InstanceOfBookListResponse;
import com.example.locallibrary.entity.Book;
import com.example.locallibrary.entity.Genre;
import com.example.locallibrary.repository.BookGenreRepository;
import com.example.locallibrary.repository.BookInstanceRepository;
import com.example.locallibrary.repository.BookRepository;
@Controller // 컨트롤러 빈으로 등록
public class BookController { // 책 요청/응답을 처리
// 의존성
private final BookRepository bookRepository;
private final BookInstanceRepository bookInstanceRepository;
private final BookGenreRepository bookGenreRepository;
public BookController(BookRepository bookRepository, BookInstanceRepository bookInstanceRepository, BookGenreRepository bookGenreRepository) {
this.bookRepository = bookRepository;
this.bookInstanceRepository = bookInstanceRepository;
this.bookGenreRepository = bookGenreRepository;
}
// 책 상세보기 요청을 처리하는 핸들러
@GetMapping("/book/{id}") // GET /book/99
// id: 클라이언트가 요청한 책의 ID
// @PathVariable: 경로 변수를 지정하는 어노테이션
// URL 경로에서 지정한 자리에 있는 데이터를 변수로 인식하게 만든다
// 경로 변수도 요청 매개변수 (@RequestParm)처럼 요청 주소(URL)에 서버에 전송할 데이터를 담는 방식이다.
public String getOne(@PathVariable("id") Long id, Model model) {
// DB에서 전송받은 id와 일치하는 책을 찾는다.
Book book = bookRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("도서 정보가 존재하지 않습니다. " + id));
// 이 책의 사본목록 DTO를 만든다
// findByBookId: book_id 컬럼 값이 전달받은 id와 일치하는 행을 조회
List<InstanceOfBookListResponse> bookInstances = bookInstanceRepository.findByBookId(id)
.stream()
.map((bookInstance) -> {
InstanceOfBookListResponse dto = new InstanceOfBookListResponse();
dto.setTitle(bookInstance.getBook().getTitle());
dto.setUrl(bookInstance.getId());
// getLabel: 이넘의 상수로부터 label값(문자열)을 반환
dto.setStatus(bookInstance.getStatus().getLabel());
return dto;
}).collect(Collectors.toList());
// 이 책의 장르목록 DTO를 만든다
List<String> genres = bookGenreRepository.findByBookId(id).stream()
.map((bookGenre) -> {
Genre genre = bookGenre.getGenre();
return genre.getName();
}).collect(Collectors.toList());
// 최종 DTO 책 상세보기 DTO
BookDetailsResponse dto = new BookDetailsResponse();
dto.setTitle(book.getTitle());
dto.setSummary(book.getSummary());
dto.setAuthorName(book.getAuthor().getName());
dto.setAuthorUrl(book.getAuthor().getId());
dto.setIsbn(book.getIsbn());
dto.setGenres(genres);
// countByBookId(id): book_id 컬럼값이 전달받은 id와 일치하는 행의 개수를 센다
dto.setBookInstanceCount(bookInstanceRepository.countByBookId(id));
dto.setBookInstances(bookInstances);
// 뷰에게 DTO 전달
model.addAttribute("book", dto);
// 서버의 응답
return "book_detail"; //book_detail 페이지를 전송
}
}
package com.example.locallibrary.controller;
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 com.example.locallibrary.dto.InstanceOfBookDetailsResponse;
import com.example.locallibrary.entity.BookInstance;
import com.example.locallibrary.repository.BookInstanceRepository;
@Controller
public class BookInstanceController {
// 의존성 선언
private final BookInstanceRepository bookInstanceRepository;
public BookInstanceController(BookInstanceRepository bookInstanceRepository) {
this.bookInstanceRepository = bookInstanceRepository;
}
// 책 사본 상세보기 요청을 처리하는 핸들러
@GetMapping("/bookinstance/{id}")
public String getOne(@PathVariable("id") Long id, Model model) {
// DB에서 책사본을 찾는다
BookInstance bookInstance = bookInstanceRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("책 사본이 존재하지 않습니다: " + id));
// 책 사본 상세보기 DTO를 생성
InstanceOfBookDetailsResponse dto = new InstanceOfBookDetailsResponse();
dto.setTitle(bookInstance.getBook().getTitle());
dto.setImprint(bookInstance.getImprint());
dto.setStatus(bookInstance.getStatus()); // 상수 전달
dto.setDueBack(bookInstance.getDueBack()); // 날짜 객체 전달
// 뷰 생성
model.addAttribute("bookInstance", dto); // bookinstance_detail 문서(HTML 문서)를 전송한다.
// templates/bookinstance_detail.html
return "bookinstance_detail";
}
}
package com.example.locallibrary.controller;
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 com.example.locallibrary.dto.AuthorDetailsResponse;
import com.example.locallibrary.entity.Author;
import com.example.locallibrary.repository.AuthorRepository;
@Controller
public class AuthorController {
// 의존성 선언
private final AuthorRepository authorRepository;
public AuthorController(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
// 저자 상세보기 요청을 처리하는 핸들러
@GetMapping("/author/{id}")
public String getOne(@PathVariable("id") Long id, Model model) {
// DB에서 저자 검색
Author author = authorRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("저자 정보가 존재하지 않습니다: " + id));
// 저자 상세보기 DTO 생성
AuthorDetailsResponse dto = new AuthorDetailsResponse();
dto.setName(author.getName());
dto.setBirthDate(author.getBirthDate());
dto.setDeathDate(author.getDeathDate());
// 뷰 완성하기
model.addAttribute("author", dto);
// 서버의 응답
return "author_detail";
}
}
package com.example.locallibrary.controller;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.locallibrary.dto.BookListResponse;
import com.example.locallibrary.dto.BookSearchResponse;
import com.example.locallibrary.dto.BookSearchResponse.ResultBook;
import com.example.locallibrary.repository.BookInstanceRepository;
import com.example.locallibrary.repository.BookRepository;
@Controller
public class HomeController {
// 의존성 선언
private final BookRepository bookRepository;
private final BookInstanceRepository bookInstanceRepository;
public HomeController(BookRepository bookRepository, BookInstanceRepository bookInstanceRepository) {
this.bookRepository = bookRepository;
this.bookInstanceRepository = bookInstanceRepository;
}
// 메인페이지 요청 핸들러
@GetMapping("/")
public String home(Model model) {
// 메인페이지 전시용 책들
List<BookListResponse> books = bookRepository.findAll().stream()
.map((book) -> {
BookListResponse dto = new BookListResponse();
dto.setTitle(book.getTitle());
dto.setAuthorName(book.getAuthor().getName());
dto.setUrl(book.getId());
return dto;
}).collect(Collectors.toList());
// 뷰 완성하기
model.addAttribute("books", books);
// 서버의 응답
return "home";
}
// 검색 결과 페이지 요청을 처리하는 핸들러
@GetMapping("/search") // Get /search?keyword=foo
// keyword는 사용자가 입력한 검색어이다
public String search(@RequestParam(name = "keyword") String keyword, Model model) {
// 검색어를 포함하는 책들을 DB에서 가져온다
// 검색결과 DTO로 변환한다
List<ResultBook> resultBooks = bookRepository.findByTitleContainingIgnoreCase(keyword).stream()
.map(book -> {
ResultBook dto = new ResultBook();
dto.setTitle(book.getTitle());
dto.setAuthorName(book.getAuthor().getName());
dto.setInstanceCount(bookInstanceRepository.countByBookId(book.getId()));
dto.setUrl(book.getId());
return dto;
}).collect(Collectors.toList());
// [최종 DTO] 검색결과 DTO
BookSearchResponse dto = new BookSearchResponse();
dto.setResultBooks(resultBooks);
dto.setResultCount(bookRepository.countByTitleContainingIgnoreCase(keyword));
// 뷰 완성하기
model.addAttribute("keyword", keyword);
model.addAttribute("result", dto);
// 서버 응답
return "search_result";
}
}
package com.example.locallibrary.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
// 컨트롤러에서 던진 예외를 처리하는 객체
@ControllerAdvice // 예외처리기 빈으로 등록
public class GlobalExceptionHandler {
// 기록기: 서버에서 발생한 중요한 일들을 기록하는 객체
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 잘못된 인자 예외(IllegalArgmentException)을 처리할 핸들러
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) // 이 예외를 400(Bad Request)로 처리
String resourceNotFoundHandler(IllegalArgumentException e, Model model) {
// 예외상황을 콘솔에 출력 (개발자 확인용)
logger.error("잘못된 요청 발생: ", e);
// 에러페이지를 생성한다
model.addAttribute( "status", HttpStatus.BAD_REQUEST.value());
model.addAttribute("error", HttpStatus.BAD_REQUEST.getReasonPhrase());
model.addAttribute("message", e.getMessage());
// 서버 응답
return "error"; // error 페이지를 전송
}
// 기타 예외를 처리하는 핸들러
@ExceptionHandler(Exception.class)
// 500(서버 내부 오류)로 처리하겠다
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleAllException(Exception e, Model model) {
// 개발자 확인용
logger.error("서버 오류 발생: ", e);
// 에러페이지 생성
model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
model.addAttribute("error", HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
model.addAttribute("Message", "문제가 발생했습니다. 나중에 다시 시도하세요.");
// 서버 응답
return "error";
}
}
선생님께 받은 templates와 SeedDataBase.java 파일을 넣어주면 실행이 된다.
저번 프로젝트와 달리 xml 파일을 쓰지 않으니 편했다.
처음부터 실행이 되지 않아 오류 잡는데만 2시간 정도 걸린 거 같다.
처음엔 막막했지만 하나씩 오류를 해결해가면서 어떤 부분이 어떤 부분과 관계가 있고 상호작용을 하는지 직접 확인할 수 있어 유익한 시간이었다.
오류 잡는 부분을 AI에 의존하지 않고 직접 하나씩 찾아보는 게 공부에 도움이 꽤 많이 될 거 같다.
생각보다 오류 잡는 게 재밌었다.
물론 오류가 아예 안나는 게 좋겠지만 아직은 그럴 수 없으니 오류 잡는 부분도 내 것으로 만들어야겠다.