풀스택 개발자 과정 33일차

너구·2026년 6월 23일

풀스택 성장과정

목록 보기
33/79

dto/BookDetailsResponse.java

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() : "";
    }    
}

dto/InstanceOfBookDetailsResponse.java

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;
    }   
}

dto/BookSearchResponse.java

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;
        }       
    }
}

dto/AuthorDetailsResponse.java

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() : "";
    }    
}

controller/BookController.java

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 페이지를 전송
    }
}

controller/BookInstanceController.java

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";
    }
}

controller/AuthorController.java

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";
    }
}

controller/HomeController.java

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";
    }
    
}

exception/GlobalExceptionHandler.java

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에 의존하지 않고 직접 하나씩 찾아보는 게 공부에 도움이 꽤 많이 될 거 같다.
생각보다 오류 잡는 게 재밌었다.
물론 오류가 아예 안나는 게 좋겠지만 아직은 그럴 수 없으니 오류 잡는 부분도 내 것으로 만들어야겠다.

0개의 댓글