Spring Boot (+ RESTful)

Spring Boot - # Project 03 - Bootstrap / @PostMapping

wy-family 2024. 12. 19. 14:10

detail.html 을 만들어야 한다.

 

template.html 을 복사붙여넣기 해서 detail.html 로 이름을 변경해주자.

기존 template.html 코드는 아래와 같다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf Bootstrap Template</title>
    <!-- Bootstrap CSS -->
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<!-- Header Section -->
<div th:replace="fragments/header :: header"></div>
<!--MainContentSection-->
<div class="container my-5">
</div>
<!-- Footer Section -->
<div th:replace="fragments/footer :: footer"></div>
</body>
</html>

 

MainContentSection 부분을 손보면 되겠다.

 

- 상세보기,

- 리뷰목록,

- 리뷰 및 평점 쓰기,

- 리스트로 돌아가기(root page로 돌아가기)

 

을 만들자.

여기서부터는, html 을 공부해본 적이 없기 때문에 강의에서 알려주는 걸 일단은 따라해보고, chatGPT의 도움을 받아서 이해를 많이 해보고 공부해보려고 한다.


<detail.html>

<!--MainContentSection-->
<div class="container my-5">
    <!-- 상세보기 START -->
    <div class="card">
        <div class="card-header">
            <h2>상세보기</h2>
        </div>
        <div class="card-body">
            <div class="row">
                <div class="col-8">
                    <h3 th:text="${book.subject}"></h3>
                    <h6 th:text="${book.author}"></h6>
                    <p>
                        Price:<span th:text="${book.price}"></span><br/>
                        Page:<span th:text="${book.page}"></span><br/>
                        Created At:<span th:text="${book.createdAt}"></span>
                    </p>
                   <p>추가적인 설명을 출력하세요......</p>
                </div>
                <div class="col-4">
                    <img th:src="@{/images/book.jpg}" class="img-fluid" alt="Book Image">
                </div>
            </div>
        </div>
    </div>
    <!-- 상세보기 END -->

    <!-- 리뷰목록 START -->
    <div class="card">
        <div class="card-header">
            <h2>리뷰목록</h2>
            <span class="badge badge-secondary">
                <span th:text="${#lists.size(book.reviews)}"></span> 개,
                평점:<span th:text="${averageRating}">?</span>/5
            </span>
        </div>
        <div class="card-body">
            <div th:if="${book.reviews != null and #lists.size(book.reviews) > 0}">
                <div class="list-group" th:each="reviews : ${book.reviews}">
                    <div class="list-group-item">
                        <p th:text="${reviews.content}">리뷰내용</p>
                        <small th:text="${reviews.createdAt}">작성일자</small>
                        <p><small>평점:<span th:text="${reviews.rating}"></span>/5</small></p>
                    </div>
                </div>
            </div>
            <div th:if="${book.reviews == null and #lists.size(book.reviews) == 0}">
                <p>작성된 리뷰가 없습니다.</p>
            </div>
        </div>
    </div>
    <!-- 리뷰목록 END -->

    <!-- 리뷰 및 평점 쓰기 START -->
    <div class="card">
        <div class="card-header">
            <h2>리뷰 및 평점 쓰기</h2>
        </div>
        <div class="card-body">
            <form th:action="@{/register}" method="post">
                <input type="hidden" name="book_id" th:value="${book.id}">
                <div class="form-group">
                    <label for="content">리뷰내용</label>
                    <textarea class="form-control" id="content" name="content" rows="3" required></textarea>
                </div>
                <div class="form-group">
                    <label for="rating">평점</label>
                    <select class="form-control" id="rating" name="rating" required>
                        <option value="1">1 - Poor</option>
                        <option value="2">2 - Fair</option>
                        <option value="3">3 - Good</option>
                        <option value="4">4 - Very Good</option>
                        <option value="5">5 - Excellent</option>
                    </select>
                </div>
                <button type="submit" class="btn btn-secondary btn-sm">리뷰등록</button>
            </form>
        </div>
    </div>
    <!-- 리뷰 및 평점 쓰기 END -->

    <!-- 리스트로 돌아가기-->
    <div class="text-right mt-5">
        <a href="/" class="btn btn-primary btn-sm">Back to lists</a>
    </div>
</div>

 

 

<BookController.java>

@Controller
public class BookController {     // new HomeController

    @Autowired
    private BookService bookService;

    @Autowired
    private ReviewService reviewService;

    @GetMapping("/")  // / 이라고만 하면 root 페이지를 뜻함.
    public String home(Model model) {
        List<Book> books=bookService.getAll();
        // books -> 번지 (List<Book> books)
        model.addAttribute("books", books); //객체바인딩
        return "home";  // home.html
    }

    @GetMapping("/detail/{id}")
    public String getDetails(@PathVariable Long id, Model model) {
        Optional<Book> bookOptional = bookService.getById(id);
        if (bookOptional.isPresent()) {
            Book book = bookOptional.get();
            model.addAttribute("book", book);

            // 평균 평점을 계산하여 모델에 추가
            double averageRating = book.getReviews().stream().mapToInt(Review::getRating).average().orElse(0.0);
            String formattedAverageRating = String.format("%.1f", averageRating);
            model.addAttribute("averageRating", formattedAverageRating);
            return "detail"; // 상세보기 View(detail.html) : ${book}
        }else{
            return "redirect:/";
        }
    }

    @PostMapping("/register")
    public String reviewRegister(@RequestParam("book_id") Long book_id, Review review) {
        Book book = bookService.getById(book_id).orElseThrow(()->new IllegalArgumentException("Invalid book id: " + book_id));
        review.setBook(book);
        review.setCreatedAt(LocalDateTime.now());
        reviewService.save(review);  // 등록성공
        return "redirect:/detail/" + book.getId();
    }
}

 

이 코드의 핵심은 **상세보기 페이지(detail.html)**와 그것을 제어하는 BookController.java의 동작을 이해하는 것입니다. 단계별로 상세하게 설명하겠습니다.


1. HTML 파일: detail.html

이 HTML 파일은 책의 상세 정보를 보여주고, 리뷰를 출력하거나 등록할 수 있게 합니다.

1.1 메인 컨테이너 (<div class="container my-5">)

Bootstrap의 container 클래스를 사용해 웹 페이지의 레이아웃을 중앙에 정렬하고, 여백(my-5)을 추가합니다.


1.2 상세보기 섹션

  • card: Bootstrap 카드 컴포넌트입니다.
  • th:text: Thymeleaf 문법으로, 서버에서 넘겨준 book 객체의 속성을 출력합니다.
    • ${book.subject} → 책 제목
    • ${book.author} → 저자 이름
    • ${book.price}, ${book.page} → 가격과 페이지 수
  • 이미지 출력:
    • th:src="@{/images/book.jpg}" → /images/book.jpg 경로에 있는 이미지를 보여줍니다.
    • img-fluid: 이미지가 부모 컨테이너에 맞게 조절됩니다.

1.3 리뷰 목록

  • 리뷰 개수: #lists.size(book.reviews) → 리뷰 목록의 개수를 가져옵니다.
  • 평점 평균: ${averageRating} → BookController에서 계산된 평점 평균입니다.
  • th:each: 리뷰 리스트를 반복하며 각각의 리뷰를 출력합니다.
  • 조건부 렌더링:
    • th:if를 사용해 리뷰가 없으면 "작성된 리뷰가 없습니다"라는 메시지를 출력합니다.

1.4 리뷰 및 평점 쓰기

  • th:action="@{/register}": 폼 제출 시 POST 요청을 /register URL로 보냅니다.
  • hidden input: book_id를 숨겨서 같이 보냅니다.
  • textarea & select: 리뷰 내용과 평점을 입력받습니다.

1.5 뒤로 가기 버튼

  • 루트 페이지(/)로 이동합니다.

2. BookController.java

이 컨트롤러는 detail.html과 연동되어 데이터를 가져오고 처리합니다.

2.1 책 상세보기: @GetMapping("/detail/{id}")

  • @PathVariable: URL의 {id} 값을 받아옵니다.
  • 책 가져오기:
    • bookService.getById(id) → Optional<Book>으로 책 데이터를 가져옵니다.
    • book.isPresent() → 책이 존재하는지 확인합니다.
  • 평점 계산:
    • book.getReviews().stream() → 리뷰 리스트를 스트림으로 변환합니다.
    • .mapToInt(Review::getRating).average() → 평점을 평균 내고, 값이 없으면 0.0 반환.

2.2 리뷰 등록: @PostMapping("/register")

  • @RequestParam: 폼에서 전달된 book_id 값을 받습니다.
  • 리뷰 저장:
    • review.setBook(book) → 리뷰를 해당 책에 연결합니다.
    • review.setCreatedAt() → 현재 시간을 기록합니다.
    • reviewService.save(review) → 리뷰를 데이터베이스에 저장합니다.
  • 리다이렉트:
    • 저장 후 /detail/{book.id} 페이지로 돌아갑니다.

전체 흐름 정리

  1. 사용자가 /detail/{id} URL에 접근하면, BookController가 책 데이터를 조회하고 detail.html에 전달합니다.
  2. detail.html에서는 책의 정보와 리뷰 목록을 출력합니다.
  3. 사용자가 리뷰를 작성하면 POST 요청으로 /register가 호출되고, 리뷰가 저장된 후 다시 상세보기 페이지로 리다이렉트됩니다.

상세보기 부분에서 질문 1. img-fluid 이미지가 부모 컨테이너에 맞게 조절된다고 했는데, 부모 컨테이너가 어떤 걸 의미하는것인지, 어디에서 부모 컨테이너라고 지정이 된 것인지.
상세보기 부분에서 질문 2. card-body 다음에 row, 그리고 나서 col-8, col-4 가 있는데, 이게 어떤 의미인지, 그리고 왜 저런 순서로 작성을 한 것인지, 어떤 원리와 원칙이 있는 것인지.

상세보기 부분

질문 1: img-fluid의 부모 컨테이너란?

  • 부모 컨테이너란, 특정 HTML 요소를 감싸는 상위 요소를 의미합니다.
  • 이미지 <img> 태그는 다음과 같이 작성되어 있습니다:
<div class="col-4">
    <img th:src="@{/images/book.jpg}" class="img-fluid" alt="Book Image">
</div>
  • 여기서 부모 컨테이너는 바로 <div class="col-4">입니다.
  • **img-fluid**는 Bootstrap의 클래스이며, 부모 컨테이너의 너비에 맞게 이미지의 크기를 자동으로 조절합니다.
  • 즉, col-4가 화면의 4/12 너비를 차지하게 되면, img-fluid 이미지도 그 안에 맞게 축소 또는 확대됩니다.

질문 2: card-body, row, col-8, col-4의 의미와 순서

  • 원리: Bootstrap의 Grid 시스템을 사용하여 레이아웃을 만들었습니다.
    1. row: 하나의 "행(row)"을 생성합니다.
    2. col-8, col-4: "열(column)"을 의미하며, 12개의 그리드 중 8과 4를 차지하도록 나누었습니다.
  • 왜 이 순서인가?
    • card-body: 카드 내부의 내용 영역입니다.
    • row: 내용을 행으로 나누기 위해 사용됩니다.
    • col-8, col-4: 각각 8/12와 4/12 너비로 설정하여 내용과 이미지를 나란히 배치합니다.
  • 결과:
    • 왼쪽(col-8)에 책 제목과 설명
    • 오른쪽(col-4)에 책 이미지
리뷰 목록 부분에서 질문 1. badge badge-secondary 가 무슨 뜻인지
리뷰 목록 부분에서 질문 2. ${} 기호는 {} 안에 arguments, parameters 를 입력하기 위한 기호라는 걸로 이해하고 있어. 그런데, ${#} 이라고 되어있는데 # 은 뭐야?
리뷰 목록 부분에서 질문 3. list-group 은 뭐야? list-group-item은 뭐야?
리뷰 목록 부분에서 질문 4. <small></small> 은 무슨 뜻이야?

리뷰 목록 부분

질문 1: badge badge-secondary

  • badge: Bootstrap의 클래스이며, 작은 라벨이나 카운터를 표시하는 용도입니다.
  • badge-secondary: 회색 배경색을 가지는 보조 뱃지입니다.

질문 2: ${#}의 의미

  • ${}: Thymeleaf 문법으로 데이터를 출력할 때 사용합니다.
  • #: Thymeleaf의 유틸리티 객체를 나타냅니다.
    • 예: #lists → 리스트 관련 기능을 제공하는 객체입니다.
    • #lists.size(book.reviews): book.reviews의 크기(개수)를 가져옵니다.

질문 3: list-group과 list-group-item

  • list-group: Bootstrap에서 제공하는 클래스로, 리스트 형식의 그룹을 만듭니다.
  • list-group-item: 개별 항목을 나타내며, 디자인적으로 리스트의 아이템을 강조합니다.

질문 4: <small> 태그

  • <small>: 글씨를 작게 표시하기 위해 사용됩니다. 주로 보조 정보나 부가 설명을 나타낼 때 쓰입니다.
리뷰 및 평점 쓰기 부분에서 질문 1. <form> 은 어떤 거야? action은 뭐야?
리뷰 및 평점 쓰기 부분에서 질문 2. <input> 은 어떤 원리 인거지? type, name, th:value 가 어떤 의미이고 어떤 원리인거지?
리뷰 및 평점 쓰기 부분에서 질문 3. type=hidden? hidden 은 뭘까?
특히, 다른 건 다 <div></div> 이런식으로 시작과 끝을 알려주는데, 왜 <input>은 </input> 이 안 보이는걸까?
리뷰 및 평점 쓰기 부분에서 질문 4. <label> 은 뭐야?
리뷰 및 평점 쓰기 부분에서 질문 5. form-group 은 뭐야? from-control 은 뭐야?
리뷰 및 평점 쓰기 부분에서 질문 6. <textarea> 는 뭐야?
리뷰 및 평점 쓰기 부분에서 질문 7. class="form-group" id="content" name="content" rows="3" required 는 무슨 뜻이지? required 가 뭐야?
리뷰 및 평점 쓰기 부분에서 질문 8. <select> 는 뭐야? <option> 은?
<button> type="submit" class="btn btn-secondary btn-sm"> 에 대해서도 설명해줄래?

리뷰 및 평점 쓰기 부분

질문 1: <form>과 action

  • <form>: 사용자가 입력한 데이터를 서버로 보내기 위한 HTML 태그입니다.
  • action: 폼이 제출될 때 데이터를 보낼 URL입니다.

질문 2: <input>의 원리

  • <input>: 사용자가 값을 입력할 수 있는 태그입니다.
  • 속성:
    • type: 입력 타입 (e.g., text, number, hidden)
    • name: 서버로 전송할 데이터의 이름
    • th:value: Thymeleaf를 사용해 값을 설정합니다.

질문 3: type="hidden"

  • hidden: 화면에 표시되진 않지만, 폼에 데이터를 담아 전송할 때 사용됩니다.
  • 닫는 태그가 없는 이유: <input>은 self-closing 태그입니다.

질문 4: <label>

  • <label>: 입력 필드에 **이름(레이블)**을 붙이는 태그입니다.
  • for 속성은 특정 **id**를 가진 입력 필드와 연결됩니다.

질문 5: form-group과 form-control

  • form-group: Bootstrap에서 입력 필드와 레이블을 그룹화하는 클래스입니다.
  • form-control: 입력 필드에 스타일을 입혀서 가로 길이를 조절하고 일관된 디자인을 제공합니다.

질문 6: <textarea>

  • <textarea>: 여러 줄을 입력할 수 있는 텍스트 영역입니다.

질문 7: class, id, name, rows, required

  • class: CSS나 Bootstrap 스타일 적용
  • id: 요소를 고유하게 식별
  • name: 데이터를 전송할 때 이름으로 사용
  • rows: 텍스트 영역의 높이(줄 수)
  • required: 필수 입력 필드임을 나타냅니다.

질문 8: <select>와 <option>

  • <select>: 드롭다운 목록을 생성합니다.
  • <option>: 목록의 항목을 나타냅니다.

<button> 설명

<button type="submit" class="btn btn-secondary btn-sm">리뷰등록</button>
  • type="submit": 폼 데이터를 제출합니다.
  • class="btn btn-secondary btn-sm": Bootstrap 버튼 스타일을 적용합니다.
BookController 부분에서 질문 1. PathVariable 이 뭐고, 왜 필요한거지?
BookController 부분에서 질문 2. stream(), mapToInt(Review::getRating) 에 대해서 설명해줄래?
BookController 부분에서 질문 3. String.format(%.1f 는 무슨 뜻이야?
BookController 부분에서 질문 4. GetMapping과 PostMapping 의 차이가 뭐고, 어떤 차이가 있어? 어떤 상황에서 뭘 쓰는거야?
BookController 부분에서 질문 5. GetMapping 에서는 Model model 에 객체바인딩을 했는데, 왜 PostMapping 에서는 Model model 이 왜 안 쓰인거야? 이유가 뭐야?

BookController 부분

질문 1: @PathVariable

  • URL 경로에 포함된 값을 변수로 받아오는 데 사용됩니다.
  • 예: /detail/{id} → {id} 부분을 가져옵니다.

질문 2: stream()과 mapToInt

  • stream(): 컬렉션(리스트)을 스트림으로 변환하여 처리할 수 있습니다.
  • mapToInt(Review::getRating): 리뷰에서 평점을 숫자(int)로 변환합니다.

질문 3: String.format("%.1f", value)

  • %.1f: 소수점 첫째 자리까지 표시하라는 뜻입니다.

질문 4: @GetMapping과 @PostMapping의 차이

  • @GetMapping: 데이터를 조회할 때 사용 (예: 페이지 표시)
  • @PostMapping: 데이터를 서버에 제출할 때 사용 (예: 폼 제출)

질문 5: Model model이 PostMapping에서 안 쓰이는 이유

  • Post 요청은 데이터를 서버에 보내기 위한 목적이기 때문입니다. 페이지를 렌더링할 필요가 없으므로 Model을 사용하지 않습니다.

<추가질문 1> Bootstrap 의 Grid 시스템은 뭐야? 구체적이고 상세하게 설명해줄래? col은 12너비를 기준으로 한다면, row 는 그러면 어느 정도인거지?

<추가질문 2> 왜 다른 태그들은 다 closing 이 있는데 <input>만 self-closing 태그인거야? <input> 말고도 self-closing 태그가 있어?

<추가질문 3> required 가 있으면 필수입력항목인걸 알겠어. 그러면, required 가 충족되지 않으면 어디까지가 제한을 받는거야? 그러니까, required 도 부모 컨테이너 영역에서만 필수입력항목일건데, 그게 어디까지 영향을 주냐는거야.

<추가질문 4> stream 이 뭔데? 구체적이고 상세하게 설명해줄래? mapToInt 도 마찬가지야, 뭔지 모르겠어. 구체적이고 상세하게 설명해줄래? ::기호도 뭔지 이해가 안 되는데, 구체적이고 상세하게 설명해줄래?

<추가질문 5> String.format 으로 소숫점 표시 자리를 정한다는 말은 이해가 되는데, 소수는 실수의 영역이잖아. 숫자에 대한 것이라면 long, double 의 data type 이어야 할 거 같은데, 왜 String 이냐는 질문이었어.

<추가질문 6> 페이지를 렌더링한다는게 무슨뜻이야? 내가 이해하고 있는 Model model 객체바인딩은, view (any.html) 에서 어떤 값을 이용하고 싶을때, Model model 에다가 addAttribute 를 해주고 난 다음에 view 에서 그 값을 이용할 수 있도록 해주는 걸로 이해하고 있어.

추가질문 1: Bootstrap의 Grid 시스템

Bootstrap의 Grid 시스템은 **12개 열(column)**을 기반으로 레이아웃을 구성하는 시스템입니다.
화면을 행(row)과 열(column)로 나누어 반응형(responsive) 웹페이지를 만들 수 있게 해줍니다.


기본 개념

  1. container: 콘텐츠를 감싸는 부모 요소입니다. 레이아웃의 시작점입니다.
  2. row: 하나의 "행"을 의미하며, 이 안에 여러 개의 "열(column)"을 배치합니다.
  3. col-*: 개별 열의 너비를 설정합니다. 숫자는 12 너비 중 차지하는 비율을 의미합니다.

예시

<div class="container">
    <div class="row">
        <div class="col-8">왼쪽 영역</div>
        <div class="col-4">오른쪽 영역</div>
    </div>
</div>
  • row: 부모 행으로, 전체 너비는 12입니다.
  • col-8과 col-4: 8과 4를 합쳐서 12 너비를 꽉 채웁니다.

row의 너비는?

  • row의 너비는 부모 container의 너비에 맞게 자동 조절됩니다.
  • 즉, row는 컨테이너에 맞는 "가로 폭"을 가지며, 그 안에서 12 그리드로 나눠집니다.

추가질문 2: Self-closing 태그

Self-closing 태그란?

  • Self-closing 태그닫는 태그 없이도 유효한 태그입니다.
  • HTML5에서는 일부 태그가 내용이 필요하지 않아서 스스로 닫힙니다.

왜 <input>이 Self-closing 태그인가?

  • <input> 태그는 사용자의 입력만 받으면 되므로 내용이 필요하지 않기 때문입니다.
    예시: <input type="text" />

다른 Self-closing 태그들

다음은 대표적인 Self-closing 태그입니다:

  • <br>: 줄 바꿈
  • <hr>: 수평선
  • <img>: 이미지 삽입
  • <meta>: 메타데이터 제공
  • <link>: CSS 파일 연결
  • <input>: 사용자 입력 필드

추가질문 3: required의 제한 범위

  • **required**는 현재 <form> 요소 내에서만 유효합니다.
  • 입력 필드에 required가 적용되면:
    1. 폼을 제출할 때 필수 항목이 비어 있으면 제출이 막힙니다.
    2. 브라우저 레벨에서 검증이 실행됩니다.

브라우저의 검증

  • 영향 범위: <form> 요소에 포함된 required 입력 필드만 제한됩니다.
  • 동작 원리:
    • 폼을 제출할 때 필수 항목이 채워지지 않으면 브라우저가 폼 제출을 막고 경고 메시지를 표시합니다.

추가질문 4: Stream과 mapToInt

Stream이란?

  • Stream은 Java에서 데이터를 처리하기 위한 API입니다.
  • 컬렉션(리스트, 배열 등)의 요소를 한 번에 하나씩 처리하는 구조를 가지고 있습니다.

Stream의 특징

  1. 연속적 처리: 데이터 흐름을 생성 → 변환 → 결과로 출력.
  2. 불변성: 원본 데이터는 변경되지 않습니다.
  3. 중간 연산과 최종 연산:
    • 중간 연산: 데이터를 가공 (예: map, filter)
    • 최종 연산: 결과를 반환 (예: sum, average)

mapToInt란?

  • **mapToInt**는 스트림 요소를 int로 변환하는 중간 연산입니다.
  • 예제:
book.getReviews().stream()
    .mapToInt(Review::getRating) // 리뷰의 평점을 int로 변환
    .average().orElse(0.0);      // 평균값 계산
  • Review::getRating:
    :: 기호는 메서드 참조를 의미합니다.
    → Review 객체의 getRating() 메서드를 호출합니다.

추가질문 5: String.format()과 String 타입

왜 String 타입인가?

  • **String.format()**은 결과를 문자열(String)로 반환합니다.
  • 소수점 자릿수를 정렬한 결과를 화면에 표시하거나 전달하려면 문자열이 필요하기 때문입니다.

예제

double value = 4.56789;
String formatted = String.format("%.1f", value);
System.out.println(formatted); // 출력: 4.6
  • 숫자(실수)는 연산에 필요하지만, 화면 출력과 같은 표현에서는 문자열로 변환됩니다.

추가질문 6: 페이지 렌더링이란?

렌더링의 정의

  • 렌더링은 서버에서 데이터를 받아 화면에 보여주는 과정입니다.
  • View(HTML 파일)와 Model(데이터)를 결합해서 최종 웹페이지를 만듭니다.

Model 객체 바인딩

  • Model model: 데이터를 View로 전달하는 역할.
  • addAttribute: 데이터를 View에 바인딩합니다.

예시

  1. Controller에서 데이터를 준비:
model.addAttribute("book", book);
  1. HTML에서 데이터를 출력:
<h3 th:text="${book.subject}"></h3>
  • 결과: 렌더링된 페이지에 book.subject의 값이 표시됩니다.

 

이미지를 넣어줘야 하는데, static 에서 디렉토리로 images 를 생성하면, 원래는 static/images 이런식으로 표시가 되는데 아랫단에 들여진 저 상태가 되려면

Mark Directory as Excluded 를 눌러줘야 한다.

 

이미지 파일을 넣어주었다.

 

이미지가 적용된 걸 확인했다.

그런데, 아직은 구현되지 않는 기능들이 많이 있다. 다음 게시물에서 계속 해보겠다.