<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hi_do.log</title>
        <link>https://velog.io/</link>
        <description>하이도의 BackEnd 입문</description>
        <lastBuildDate>Thu, 07 Nov 2024 05:00:16 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hi_do.log</title>
            <url>https://velog.velcdn.com/images/hi_do/profile/fe5ef41e-9179-4776-b3f1-61d2b87fce9a/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hi_do.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hi_do" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Spring IntellIJ (HTML에 서버데이터 넣기)]]></title>
            <link>https://velog.io/@hi_do/Spring-IntellIJ-HTML%EC%97%90-%EC%84%9C%EB%B2%84%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%84%A3%EA%B8%B0</link>
            <guid>https://velog.io/@hi_do/Spring-IntellIJ-HTML%EC%97%90-%EC%84%9C%EB%B2%84%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%84%A3%EA%B8%B0</guid>
            <pubDate>Thu, 07 Nov 2024 05:00:16 GMT</pubDate>
            <description><![CDATA[<p>먼저 list.html을 수정할거다 
이미지를 넣는 것을 먼저 한다</p>
<pre><code class="language-html"> &lt;div class=&quot;card&quot;&gt;
        &lt;img src=&quot;https://placehold.co/300&quot;&gt;
        &lt;div&gt;
        &lt;h4 th:text=&quot;${name}&quot;&gt;바지&lt;/h4&gt;
        &lt;p&gt;7만&lt;/p&gt;</code></pre>
<p>이런식으로 <img>를 사용한다</p>
<p>그리고 css를 만들어 줄거다
<img src="https://velog.velcdn.com/images/hi_do/post/fa9056b0-7acf-4039-af34-711f0a05b784/image.png" alt="">
static에 main.css를 만들어준다</p>
<pre><code class="language-css">.card {
 margin : 30px auto;
 display : flex;
}
.card img {
 width : 150px;
 margin-right : 20px;
}</code></pre>
<p>그리고 이 css를 html에서 사용하려면 <title>아래에 </p>
<pre><code>  &lt;title&gt;Document&lt;/title&gt;
    &lt;link href=&quot;/main.css&quot; rel=&quot;stylesheet&quot;&gt;</code></pre><p> 이렇게 넣어주면 된다 </p>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot;
          content=&quot;width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0&quot;&gt;
    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot;&gt;
    &lt;title&gt;Document&lt;/title&gt;
    &lt;link href=&quot;/main.css&quot; rel=&quot;stylesheet&quot;&gt;

&lt;/head&gt;
&lt;body&gt;
    &lt;div class=&quot;card&quot;&gt;
        &lt;img src=&quot;https://placehold.co/300&quot;&gt;
        &lt;div&gt;
        &lt;h4 th:text=&quot;${name}&quot;&gt;바지&lt;/h4&gt;
        &lt;p&gt;7만&lt;/p&gt;
    &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;card&quot;&gt;
        &lt;img src=&quot;https://placehold.co/300&quot;&gt;
        &lt;div&gt;
            &lt;h4 th:text=&quot;${name}&quot;&gt;셔츠&lt;/h4&gt;
            &lt;p&gt;7만&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;


&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>  이제 실행을 하여 &#39;localhost:8080/list&#39;에 들어가보면
  <img src="https://velog.velcdn.com/images/hi_do/post/55713e06-76a0-4a39-9015-4385775b021a/image.png" alt="">
이렇게 나온다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring IntellIJ(DB데이터 출력)]]></title>
            <link>https://velog.io/@hi_do/Spring-IntellIJDB%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B6%9C%EB%A0%A5</link>
            <guid>https://velog.io/@hi_do/Spring-IntellIJDB%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B6%9C%EB%A0%A5</guid>
            <pubDate>Wed, 23 Oct 2024 07:12:54 GMT</pubDate>
            <description><![CDATA[<p>라이브러리를 먼저 하나 설치하자
Lombok인데 필요한 코드를 자동으로 생성해줘서 자바 코드 양을 매우 줄여주는 간단한 라이브러리다.</p>
<pre><code class="language-java">dependencies {
  compileOnly &#39;org.projectlombok:lombok&#39;
  annotationProcessor &#39;org.projectlombok:lombok&#39;
}</code></pre>
<p>Lombok(코드 자동생성 라이브러리)
<img src="https://velog.velcdn.com/images/hi_do/post/496b6b7e-6ef4-442c-9289-3575bda766ec/image.png" alt="">
하단에 이렇게 나오면 &#39;Configure plugins&#39;를 클릭하여 Lombok을 Install 해주자</p>
<ul>
<li>만약 나오지 않는 경우 &#39;Setting -&gt; Plugins&#39;에서 Lombok을 검색하여 Install 한다
<img src="https://velog.velcdn.com/images/hi_do/post/ab72dbf2-5a51-4aef-9f26-9917a905d0e1/image.png" alt="">
그리고 &#39;Setting&#39;에서 &#39;annotation process&#39;를 검색하여 &#39;Enable annotation processing&#39;을 체크해주자</li>
</ul>
<p>JPA로 데이터 입출력하려면 3가지 단계가 있다</p>
<ol>
<li>repository만들기</li>
<li>원하는 클래스에 repository 등록</li>
<li>repository.입출력문법()쓰기</li>
</ol>
<h4 id="1-repository만들기">1. repository만들기</h4>
<p><img src="https://velog.velcdn.com/images/hi_do/post/4f6d36c7-910b-410e-a622-609fc8940b59/image.png" alt="">
ItemRepository.java</p>
<pre><code class="language-java">package com.apple.shop;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ItemRepository  extends JpaRepository&lt;Item, Long&gt; {

}</code></pre>
<h4 id="2-원하는-클래스에-repository-등록">2. 원하는 클래스에 repository 등록</h4>
<p>ItemController.java</p>
<pre><code class="language-java">package com.apple.shop;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@RequiredArgsConstructor
public class ItemController {

   private final ItemRepository itemRepository;

    @GetMapping(&quot;/list&quot;)
    String list(Model model){
        model.addAttribute(&quot;name&quot;,&quot;홍길동&quot;);
        return &quot;list.html&quot;;
    }

}</code></pre>
<h4 id="3-repository입출력문법쓰기">3. repository.입출력문법()쓰기</h4>
<p>ItemController.java</p>
<pre><code class="language-java">package com.apple.shop;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.ArrayList;

@Controller
@RequiredArgsConstructor
public class ItemController {

   private final ItemRepository itemRepository;

    @GetMapping(&quot;/list&quot;)
    String list(Model model){
        var result = itemRepository.findAll();
        System.out.println(result);

        var a = new ArrayList&lt;&gt;();
        a.add(30);
        a.add(40);
        System.out.println(a);

        model.addAttribute(&quot;name&quot;,&quot;홍길동&quot;);
        return &quot;list.html&quot;;
    }

}</code></pre>
<p>(.findAll은 테이블의 모든 데이터를 가져온다)
출력을 해보면 
<img src="https://velog.velcdn.com/images/hi_do/post/28be2a28-d8b7-4e7c-b94d-5eefb5a6712f/image.png" alt="">
이렇게 나온다 (이게 ArrayList의 생김새다)</p>
<ul>
<li><p>ArrayList에서 하나만 출력하려면</p>
<pre><code class="language-java">var a = new ArrayList&lt;&gt;();
      a.add(30);
      a.add(40);
      System.out.println(a);</code></pre>
<p>에서 </p>
<pre><code class="language-java">System.out.println(a);</code></pre>
<p>이 부분을 </p>
<pre><code class="language-java">System.out.println(a.get(몇번째 자료를 출력할것인지 입력));</code></pre>
<p>이렇게 변경해준다
<img src="https://velog.velcdn.com/images/hi_do/post/b2ddb160-1318-47dd-9755-0e65bc86203d/image.png" alt=""></p>
</li>
<li><blockquote>
<p>위의 코드를 </p>
</blockquote>
<pre><code class="language-java">ArrayList&lt;ArrayList에 보관할 자료 타입&gt; a = new ArrayList&lt;&gt;();</code></pre>
<p>이렇게도 변경 가능하다</p>
</li>
<li><p>(참고) Lombok 없이 등록하는 법</p>
<pre><code class="language-java">package com.apple.shop;
</code></pre>
</li>
</ul>
<p>import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;</p>
<p>@Controller
public class ItemController {</p>
<p>   private final ItemRepository itemRepository;
   @Autowired
   public ItemController(ItemRepository itemRepository) {
       this.itemRepository = itemRepository;
   }</p>
<pre><code>@GetMapping(&quot;/list&quot;)
String list(Model model){

    model.addAttribute(&quot;name&quot;,&quot;홍길동&quot;);
    return &quot;list.html&quot;;
}</code></pre><p>}
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring IntellIJ (테이블 만들기 Entity)]]></title>
            <link>https://velog.io/@hi_do/Spring-IntellIJ-%ED%85%8C%EC%9D%B4%EB%B8%94-%EB%A7%8C%EB%93%A4%EA%B8%B0-Entity</link>
            <guid>https://velog.io/@hi_do/Spring-IntellIJ-%ED%85%8C%EC%9D%B4%EB%B8%94-%EB%A7%8C%EB%93%A4%EA%B8%B0-Entity</guid>
            <pubDate>Mon, 21 Oct 2024 06:40:49 GMT</pubDate>
            <description><![CDATA[<p>테이블을 만들고 싶다면 JPA에서는 @Entity클래스를 만들면 자동생성된다.
테이블을 만들때는 테이블에 어떤 컬럼이 들어갈지도 먼저 정해야한다.</p>
<p>Item.java</p>
<pre><code class="language-java">package com.apple.shop;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Item {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    String title;
    Integer price;
}</code></pre>
<p>컬럼용 변수에는 int 말고 Integer를 강요한다
(int/Integer는 20억까지 저장가능하다.)
id 컬럼에는 @Id를 사용해야한다.
 @GeneratedValue를 사용하면 id를 알아서 1씩 증가 시켜준다.(정확히는 @GeneratedValue(strategy = GenerationType.IDENTITY) 이다)</p>
<ul>
<li>SQL을 잘 아는 것처럼 보이려면 @Id는 primary key로 설정해주는 역할이고 @GeneratedValue는 auto increment 기능이라고 말한다.</li>
</ul>
<p>이제 실행을 해주고 잘 됬는지 확인하기 위해 DBeaver에서 확인한다.
<img src="https://velog.velcdn.com/images/hi_do/post/5d2da00d-d668-4b96-a7cb-b877f2b25c99/image.png" alt="">
짠! 모두 잘 나왔다!
이제 상품 몇개를 넣어보자
<img src="https://velog.velcdn.com/images/hi_do/post/233dbfdf-1ff1-4434-b20e-1b5e0de70d2e/image.png" alt="">
아래의 row추가 버튼을 클릭한다
이제 하나하나 입력해주면 된다.
근데 이게 귀찮다고한다면
<img src="https://velog.velcdn.com/images/hi_do/post/292e4cff-2218-4de4-a4cd-9187f41fc2e4/image.png" alt="">
상단의 SQL을 누른 후
<img src="https://velog.velcdn.com/images/hi_do/post/5095bf8e-f5d3-4562-b821-1c463f4bb1f9/image.png" alt="">
이렇게 입력해주고 실행버튼 누르면 된다.
(실행버튼은 저 주문서 같은 아이콘)
<img src="https://velog.velcdn.com/images/hi_do/post/092f9682-2304-49b8-a855-eea9bd7da31d/image.png" alt="">
짠! 3개 모두 추가되었다!</p>
<p>한가지 추가할게 생겼다.
아까 Entity코드 안의 변수에 public을 붙여준다.</p>
<p>Item.java</p>
<pre><code class="language-java">package com.apple.shop;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Item {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long id;
    public String title;
    public Integer price;
}</code></pre>
<p>이유는 public이 없으면 같은 폴더내의 클래스에서만 사용가능하기 때문에 다른 모든 클래스에서 문제없이 사용 가능하려면 public을 붙여준다.</p>
<p>이제 @Column()으로 컬럼에 제약설정을 해보자</p>
<pre><code class="language-@Column(nullable">    public String title;</code></pre>
<p>저기 &#39;nullable = false&#39;라는 것은 &#39;이 컬럼이 비어있으면 테이블에 입력을 막아달라&#39; 라는 의미다.
&#39;unique = true&#39;은 &#39;이 컬럼값이 유니크하지 않으면 테이블 입력을 막아달라&#39; 라는 의미다.
추가로 &#39;columnDefinition = &quot;TEXT&quot;&#39;는 컬럼타입을 강제로 지정이 가능하다.
이처럼 @Column 에너테이션을 사용하면 이렇게 제약설정을 할 수 있다.</p>
<ul>
<li>참고사항: 컬럼설정 변경사항은 DB에 반영이 자동으로 안될 수도 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring IntellIJ (Database 연결 및 JPA 설치)]]></title>
            <link>https://velog.io/@hi_do/Spring-IntellIJ-Database-%EC%97%B0%EA%B2%B0-%EB%B0%8F-JPA-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@hi_do/Spring-IntellIJ-Database-%EC%97%B0%EA%B2%B0-%EB%B0%8F-JPA-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Mon, 21 Oct 2024 06:01:09 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hi_do/post/cc017248-a128-477d-8add-7179eb09646b/image.png" alt="">
데이터베이스를 만드려면 &#39;Consoles-&gt;Connection&#39;으로 들어가서 sql코드를 작성하면 된다.
<img src="https://velog.velcdn.com/images/hi_do/post/6acafc21-f06c-4c4b-bc69-c0b09e95f3da/image.png" alt="">
대충 이렇게 작성하고 재생을 누르면 이렇게 창이 나오는데 파란 버튼을 눌러주자
<img src="https://velog.velcdn.com/images/hi_do/post/cc95710f-b19c-452d-9f7d-1c7b50e47c24/image.png" alt="">
&#39;Reload&#39;버튼을 누르면 
<img src="https://velog.velcdn.com/images/hi_do/post/cc1f4f0b-a36c-415b-9ca8-2003edaf96e5/image.png" alt="">
&#39;shop&#39;이 생성되었다
그리고 이번에는 라이브러리를 추가할거다.</p>
<pre><code>dependencies {
  runtimeOnly &#39;com.mysql:mysql-connector-j&#39;
  implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;
}</code></pre><p>이것은 JPA와 MySQL 접속도와주는 라이브러리다.
&#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;는 ORM라이브러리인데 이것을 사용하면 Java코드로 데이터 입출력이 가능해지고, 뽑은 데이터 타입체크도 편하고, 데이터를 Java 스타일로 관리가 가능하다.
단점은 데이터 입출력 속도가 저하된다는 거다.</p>
<p>이제 &#39;application.properties&#39;에</p>
<pre><code>spring.datasource.url=jdbc:mysql://호스팅받은곳엔드포인트주소/만든database이름
spring.datasource.username=DB접속아이디
spring.datasource.password=DB접속비번
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.hibernate.ddl-auto=update </code></pre><p>이 코드를 입력해준다.
&#39;application.properties&#39;라는 파일이 무엇이냐면 설정과 환경변수를 보관하는 파일이다.
그리고 이 코드가 위에 받았던 라이브러리가 필요로하는 설정들이다.
<img src="https://velog.velcdn.com/images/hi_do/post/797833b6-052a-4aaa-a274-be972c21da42/image.png" alt="">
이런식으로 입력해주면 된다.</p>
<p>위의 코드에서&#39;spring.jpa.properties.hibernate.show_sql=true&#39; 이 부분은 JPA 문법을 쓸 때 SQL 따로 출력해줄지의 여부 의미한다.</p>
<p>그리고&#39;spring.jpa.hibernate.ddl-auto=update&#39; 이 부분은 테이블 생성을 자동으로 할지의 여부를 의미한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring IntellIJ (MySQL)]]></title>
            <link>https://velog.io/@hi_do/Spring-IntellIJ-MySQL</link>
            <guid>https://velog.io/@hi_do/Spring-IntellIJ-MySQL</guid>
            <pubDate>Wed, 16 Oct 2024 16:30:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hi_do/post/2a1bd040-45e5-48ca-ab5e-10221c0c87bd/image.png" alt="">
portal.azure.com에서 클라우드를 사용하여 MySQL을 호스팅 받을거다.
실제 서비스를 운영할거라는 가정하에 클라우드 서비스에서 호스팅을 받는게 안정적이라고 생각이 들어서 이것을 선택했다.<img src="https://velog.velcdn.com/images/hi_do/post/3e6bf070-fbb8-4948-8af9-e3ccf8d44008/image.png" alt="">
MySQL을 검색하면 저렇게 나오는데 &#39;유동 서버&#39;를 클릭하자
<img src="https://velog.velcdn.com/images/hi_do/post/9bed7349-e3cd-428e-bac6-a59c6a60f52c/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/1bf054c2-dcae-4c95-81b9-15fa33065e99/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/9da6bc54-0987-411a-8692-3020375053ea/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/a0844689-c77e-46f3-b3a9-fddb66a6e917/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/4994ffaf-fa85-4596-813a-1d9669a05491/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/7c65e4d8-0e20-4a64-b375-697932d4586c/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/e1236811-172c-4cad-81d0-cedafe35c771/image.png" alt="">
일단 무료로 사용해야하니까 최소한으로 하기 위해 다음과 같이 설정하자</p>
<p>이렇게하여 만들면
<img src="https://velog.velcdn.com/images/hi_do/post/99c12523-ed84-481a-9126-61a0039ed4ac/image.png" alt="">
몇분 뒤에 만들어져서 이렇게 나온다
<img src="https://velog.velcdn.com/images/hi_do/post/cb157cb1-592b-4553-b6a1-32d6469c06db/image.png" alt="">
그리고 들어가서 &#39;서버 매개 변수&#39;에서 &#39;require&#39;을 검색하여 &#39;require_secure_transport&#39;의 값을 &#39;OFF&#39;로 변경해준다
*이 사이트에서 DB를 확인하지 못한다..!
따라서 IntellIJ에서 plugin을 받아줘야한다.
&#39;Setting&#39; -&gt; &#39;Plugins&#39; 에서 &#39;database&#39;를 검색하고
<img src="https://velog.velcdn.com/images/hi_do/post/4c16b6c3-0a2f-479f-bbd1-93e3b664d53b/image.png" alt="">
이녀석을 받아준다.
<img src="https://velog.velcdn.com/images/hi_do/post/c7a5756d-3a85-4d63-bf65-422e1517a4aa/image.png" alt="">
이것을 누르면 MySQL DB에 connect를 할 수 있다!
<img src="https://velog.velcdn.com/images/hi_do/post/8a0b07a9-6c54-4d5c-be08-f4a5f3421342/image.png" alt="">
여기서 &#39;host&#39;에
<img src="https://velog.velcdn.com/images/hi_do/post/ca25532f-4be1-446f-b95b-7e35d196a5e5/image.png" alt="">
여기의 서버 이름을 복사해서 붙여넣으면 된다.
<img src="https://velog.velcdn.com/images/hi_do/post/a110a7fc-9029-4795-82dd-53fff634d609/image.png" alt="">
그리고 사용할 아이디와 비밀번호(아까 만든거!)를 입력후 &#39;Test Connection&#39;을 클릭해보면<img src="https://velog.velcdn.com/images/hi_do/post/1bfa0d87-5594-43d4-b3d8-06b327264a11/image.png" alt="">
성공했다고 나온다
이제 &#39;OK&#39; 버튼을 누르면
<img src="https://velog.velcdn.com/images/hi_do/post/ec63db0e-034d-428d-8776-1f06210cc73c/image.png" alt="">
짠! DB완료!
근데 이렇게 보면 버그가 많이 나와서 DB보려면 DBeaver를 추천한다고 한다
<img src="https://velog.velcdn.com/images/hi_do/post/6ec4c0e0-528e-4943-928f-37b3c8c067ea/image.png" alt="">
DBeaver을 실행시켜서 데이터베이스-&gt;새 데이터베이스연결 을 클릭한다.
<img src="https://velog.velcdn.com/images/hi_do/post/26f8c48e-3a19-4797-98ad-6bfc47fb35ff/image.png" alt="">
여기서 MySQL을 선택해주고
<img src="https://velog.velcdn.com/images/hi_do/post/57d7a00f-d7f6-45ef-9d36-e700aecd59db/image.png" alt="">
다음으로 넘어가서 &#39;Server Host&#39;에는 아까의 주소를 적고 Username과 Password에는 아까 만든 아이디와 비밀번호를 적는다.
이제 Test를 돌렸더니</p>
<blockquote>
<p>...?!?!
 문제점 발생.....
<img src="https://velog.velcdn.com/images/hi_do/post/301480a5-012f-4fc4-ac8a-2fd87449932b/image.png" alt="">
확인해본 결과
<img src="https://velog.velcdn.com/images/hi_do/post/3f56dde5-8d6c-42bb-9273-ee9dd3571cbe/image.png" alt="">
이녀석을 &quot;OFF&quot;로 바꿔줘야 한다.
다시 Test를 돌려보면
<img src="https://velog.velcdn.com/images/hi_do/post/98a9ba71-3bf5-457e-807f-4c15be57bacc/image.png" alt="">
성공!!!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/c1b9411b-463c-479b-ba48-3c38485c784c/image.png" alt="">
이렇게 연결되면 성공이다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring IntellIJ (Thymeleaf)]]></title>
            <link>https://velog.io/@hi_do/Spring-IntellIJ-Thymeleaf</link>
            <guid>https://velog.io/@hi_do/Spring-IntellIJ-Thymeleaf</guid>
            <pubDate>Wed, 16 Oct 2024 09:18:40 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hi_do/post/3757ae47-159e-4b5f-866d-4ad9c56d6557/image.png" alt="">
먼저 Controller 파일을 하나 만들어준다.
첫번째 줄에 package라는 것은 꼭 있어야한다!
저 package라는 것이 없으면 import 사용이 불가능하기 때문!!
그리고 저기 class 앞에 public이 없으면 같은 폴더(패키지) 안에서만 사용가능하다!</p>
<p>&#39;상품페이지&#39;를 만들기 위해 static에 list.html파일을 만들어 준다.</p>
<blockquote>
<p>list.html</p>
</blockquote>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot;
          content=&quot;width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0&quot;&gt;
    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot;&gt;
    &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div&gt;
        &lt;img&gt;
        &lt;h4&gt;바지&lt;/h4&gt;
        &lt;p&gt;7만&lt;/p&gt;
    &lt;/div&gt;
&lt;div&gt;
    &lt;img&gt;
    &lt;h4&gt;셔츠&lt;/h4&gt;
    &lt;p&gt;8만&lt;/p&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;

이제 &#39;ItemController&#39;를 마저 작성해보자
&gt; ItemController.java
```java
package com.apple.shop;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ItemController {
    @GetMapping(&quot;/list&quot;)
    String list(){
        return &quot;list.html&quot;;
    }
}</code></pre>
<p>이렇게하면 결과가
<img src="https://velog.velcdn.com/images/hi_do/post/5e31370c-f699-4a87-a5be-6b9f91397bd0/image.png" alt="">
잘 나온다!</p>
<p>이제 템플릿엔진 중 &#39;Thymeleaf&#39;를 사용해보자!</p>
<blockquote>
<p>build.gradle</p>
</blockquote>
<pre><code>dependencies {
   implementation &#39;org.springframework.boot:spring-boot-starter-thymeleaf&#39;
}</code></pre><p><img src="https://velog.velcdn.com/images/hi_do/post/ec75808e-481d-48be-8d38-c520439e24eb/image.png" alt="">
여기서 
<img src="https://velog.velcdn.com/images/hi_do/post/1f5200bc-a852-4c22-9301-e02704feeec0/image.png" alt="">
이것을 클릭하면 라이브러리가 다운된다.
(완료됬으면 서버 껐다가 다시 실행해야한다.)</p>
<p>html에 서버데이터를 넣으려면 우선 templates 폴더에 .html 파일을 넣어둬야한다.</p>
<blockquote>
</blockquote>
<p>html에 서버 데이터를 넣으려면</p>
<ol>
<li>파라미터에 Model model을 넣고<pre><code class="language-java">package com.apple.shop;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ItemController {
 @GetMapping(&quot;/list&quot;)
 String list(Model model){
     return &quot;list.html&quot;;
 }
}</code></pre>
</li>
<li>model.addAttribute(작명,데이터)를 넣고
<img src="https://velog.velcdn.com/images/hi_do/post/3c01bc11-2bf2-4fc1-81dd-ceea85bf6ae0/image.png" alt=""><pre><code class="language-java">package com.apple.shop;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ItemController {
 @GetMapping(&quot;/list&quot;)
 String list(Model model){
     model.addAttribute(&quot;name&quot;,&quot;홍길동&quot;);
     return &quot;list.html&quot;;
 }
}</code></pre>
</li>
<li>html 파일에서는 th:text=&quot;${작명}&quot;
list.html<pre><code>&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
 &lt;meta charset=&quot;UTF-8&quot;&gt;
 &lt;meta name=&quot;viewport&quot;
       content=&quot;width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0&quot;&gt;
 &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot;&gt;
 &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
 &lt;div&gt;
     &lt;img&gt;
     &lt;h4 th:text=&quot;${name}&quot;&gt;바지&lt;/h4&gt;
     &lt;p&gt;7만&lt;/p&gt;
 &lt;/div&gt;
&lt;div&gt;
 &lt;img&gt;
 &lt;h4&gt;셔츠&lt;/h4&gt;
 &lt;p&gt;8만&lt;/p&gt;
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>(&#39;th:text=&quot;${작명}&quot;&#39;은 Thymeleaf문법이다)</li>
</ol>
<p>결과가 이렇게 나온다.
<img src="https://velog.velcdn.com/images/hi_do/post/94c8f690-e356-4df0-8edd-5f023bdfeacd/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring IntelliJ (Controller)]]></title>
            <link>https://velog.io/@hi_do/Spring-IntelliJ-Controller</link>
            <guid>https://velog.io/@hi_do/Spring-IntelliJ-Controller</guid>
            <pubDate>Wed, 16 Oct 2024 08:42:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>IntelliJ 에서 html을 작성할때 팁!
&#39;!&#39;를 입력후 &#39;tab&#39;을 누르면 자동으로 완성된다
<img src="https://velog.velcdn.com/images/hi_do/post/a0b4aea3-6c4d-4399-bc7a-20e8365dbcb3/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/d101569e-4b12-4690-a205-c13931e736fa/image.png" alt="">
짠!</p>
</blockquote>
<p>다음으로 컨트롤러에 관한 예시이다.
<img src="https://velog.velcdn.com/images/hi_do/post/b0af8734-f635-44af-b8fc-26b3b5877296/image.png" alt="">
@Controller는 Spring문법이다.
@Controller는 서버기능을 만들고 싶을때 Class위에 작성한다.
@GetMapping은 &#39;localhost:8080&#39;+()의 사이트로 이동하면 그 페이지에 대해 작성한 것들이 나온다.
그리고 @GetMapping 아래로 두가지 예시인데
@ResponseBody를 쓴것과 안쓴것이 있는데
@ResponseBody는 return을 문자 그대로 보여주기 때문에 html을 return할때는 사용하지 않는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring(초기 세팅)]]></title>
            <link>https://velog.io/@hi_do/%ED%98%BC%EC%9E%90%EC%84%9C-%ED%95%B4%EB%B3%B4%EB%8A%94-Spring</link>
            <guid>https://velog.io/@hi_do/%ED%98%BC%EC%9E%90%EC%84%9C-%ED%95%B4%EB%B3%B4%EB%8A%94-Spring</guid>
            <pubDate>Mon, 14 Oct 2024 14:42:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hi_do/post/54be5ff4-66a6-4199-a241-83dcdef3354a/image.png" alt="">
왠만하면 버전은 괄호가 없는것으로 하자(괄호 없는게 제일 안전하다고 한다)
<img src="https://velog.velcdn.com/images/hi_do/post/14e5502a-c2f9-4e7e-9b1e-8761e4f7c19b/image.png" alt="">
이제 압축파일 받아서 풀자
<img src="https://velog.velcdn.com/images/hi_do/post/c0bdf8b2-9bd3-4d49-9236-1f78262ea65f/image.png" alt="">
Spring Initiallizr에서 만들어서 하는 이유는 Intellij가 무료 버전이라 여기 자체에서 만드는게 유료이기 때문이다
&#39;열기&#39;를 클릭하자
<img src="https://velog.velcdn.com/images/hi_do/post/057ed6ce-48e7-468a-8364-2aee42c1ce07/image.png" alt="">
여기 있다!
<img src="https://velog.velcdn.com/images/hi_do/post/95a5e97d-9130-4eb7-87b7-d5be50635cf8/image.png" alt="">
처음에 몇가지 세팅해야 할게 있다.
먼저 File -&gt; Project Structure에서 java를 확인해야함
<img src="https://velog.velcdn.com/images/hi_do/post/06540793-9e43-4a24-9f50-86d5350b6de5/image.png" alt="">
이렇게 설정해놓은 버전보다 높은 버전을 설정해야 한다
나는 처음에 17로 해놔서 sdk는 22로 했다
다음으로 gradle!
File -&gt; Setting 으로 들어가서 gradle을 입력하여 확인한다.
<img src="https://velog.velcdn.com/images/hi_do/post/fafd1c72-87ea-4807-950e-2b10a7847beb/image.png" alt="">
여기서
<img src="https://velog.velcdn.com/images/hi_do/post/d806910f-abf8-4b5e-a5a5-cecf460f7f13/image.png" alt="">
이렇게 IntelliJ IDEA로 변경해준다.
또한 이렇게 변경하면 한가지를 더 변경해야한다.
<img src="https://velog.velcdn.com/images/hi_do/post/ff5fd283-4d22-41b0-85d5-2d59319ce96b/image.png" alt="">
똑같이 Setting에서 java compiler를 입력하여 들어가서
Additional command line parameters에
<img src="https://velog.velcdn.com/images/hi_do/post/705be87c-e20d-4158-b0b9-14bf62d80185/image.png" alt="">
&#39;-parameters&#39;를 입력해준다
<img src="https://velog.velcdn.com/images/hi_do/post/2f3adb42-5f25-4996-9455-362ae3c70dee/image.png" alt="">
그리고 Plugins에서 &#39;one dark native&#39;를 받아준다.
이제 기본적으로 있던
<img src="https://velog.velcdn.com/images/hi_do/post/1f1fef5f-bd0f-4dd2-81eb-4972e5220f4a/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/2d1ad610-95b1-484c-94be-185ecb145c33/image.png" alt="">
public class~~ 를 실행해보자<img src="https://velog.velcdn.com/images/hi_do/post/f5479879-a447-4edc-97e7-6cbcb18a572e/image.png" alt="">
이러면 일단 실행이 된거다
<img src="https://velog.velcdn.com/images/hi_do/post/8fb13dbb-6616-4f31-bf7a-bfd2e6f3cd33/image.png" alt="">
port 번호가 8080이기 때문에 &#39;localhost:8080&#39;으로 들어가보면 저런 화면이 나온다 이러면 서버가 실행된거다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Eclipse에서 sts 실행]]></title>
            <link>https://velog.io/@hi_do/Eclipse%EC%97%90%EC%84%9C-sts-%EC%8B%A4%ED%96%89</link>
            <guid>https://velog.io/@hi_do/Eclipse%EC%97%90%EC%84%9C-sts-%EC%8B%A4%ED%96%89</guid>
            <pubDate>Tue, 08 Oct 2024 07:55:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hi_do/post/bd0446f6-5b18-40ee-8660-05ba367e1e13/image.png" alt="">
Help에서 Eclipse Marketplace로 들어간다
<img src="https://velog.velcdn.com/images/hi_do/post/a22fd384-b961-4f76-910b-53c9fb5862a0/image.png" alt="">
Find에서 Spring을 검색하여
<img src="https://velog.velcdn.com/images/hi_do/post/d25d13e5-083f-46f9-9473-ec064aadaaae/image.png" alt="">
Spring Tool 4를 Install 해준다.
<img src="https://velog.velcdn.com/images/hi_do/post/84c9dbd4-aa91-45e8-b685-c9cf64d77cf7/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/170e4547-d3d0-4955-b180-02f4ebec0276/image.png" alt="">
다 받아지면 restart를 눌러 다시 시작해준다.
<img src="https://velog.velcdn.com/images/hi_do/post/26392bad-d727-4ce7-a0d2-e60e6c3669df/image.png" alt="">
이제 재실행된 Eclipse에서 File -&gt; New -&gt; Other로 들어가서
<img src="https://velog.velcdn.com/images/hi_do/post/4ccde260-4ee7-4ca1-8649-0a0ffef9eca3/image.png" alt="">
들어가보면 아까 없던 Spring Boot가 생겼다!
<img src="https://velog.velcdn.com/images/hi_do/post/66ca6509-fb5c-435c-a0ca-92b383873c70/image.png" alt="">
이제 설정해서 실행하면 된다!
Next를 눌러서
<img src="https://velog.velcdn.com/images/hi_do/post/fea84f49-9a33-4f52-aa57-d2d80cba76b1/image.png" alt="">
다음 화면으로 오는데 일단 저렇게만 선택해서 하자
Finish를 클릭하면 끝!
<img src="https://velog.velcdn.com/images/hi_do/post/f78fa6d8-180b-4a3b-8c3e-e9306f9c62e2/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/6d0a357d-779c-420d-aacf-adcc18e4eee3/image.png" alt="">
Run As -&gt; Spring Boot App을 눌러 실행해보자!
<img src="https://velog.velcdn.com/images/hi_do/post/ad8ae2f1-84d1-4929-9fa4-161e17bfeb31/image.png" alt="">
성공적으로 작동한다!
이제 &#39;<a href="http://localhost:8080/&#39;%EC%9D%84">http://localhost:8080/&#39;을</a> 주소에 입력하여 제대로 작동하는지 확인해보자
<img src="https://velog.velcdn.com/images/hi_do/post/9b0da478-f1b1-431c-b296-9b6e2110ca19/image.png" alt="">
이제 기본 페이지를 만들어보자
<img src="https://velog.velcdn.com/images/hi_do/post/1a33a0e6-f7af-4ec9-8a4f-4605496fc754/image.png" alt="">
resources -&gt; static -&gt; index.html을 만들어보자
<img src="https://velog.velcdn.com/images/hi_do/post/dfa1a2c0-47ad-41ad-9c13-ee9053baf983/image.png" alt="">
이제 종료하고 다시 실행해보면
<img src="https://velog.velcdn.com/images/hi_do/post/cda9d72f-e1d8-47c2-abc5-6cc54961f532/image.png" alt="">
이렇게 나오는걸 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[WSL 설치]]></title>
            <link>https://velog.io/@hi_do/WSL-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@hi_do/WSL-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Wed, 25 Sep 2024 15:39:02 GMT</pubDate>
            <description><![CDATA[<h2 id="wsl설치">WSL설치</h2>
<p><img src="https://velog.velcdn.com/images/hi_do/post/d747162b-7b5a-4c1c-923c-fa83c5497144/image.png" alt="">
찾기에서 &#39;Windows PowerShell&#39;을 찾아서 실행한다.
<img src="https://velog.velcdn.com/images/hi_do/post/d972c71d-066b-42e6-bf57-5f3ae13502df/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/a3f15a33-4e6a-4d21-beaa-ffd242547838/image.png" alt="">
여기서 &#39;wsl --install&#39;을 입력하면 wsl이 설치된다.
<img src="https://velog.velcdn.com/images/hi_do/post/675d2a9f-047f-4fe1-826f-e5b6d800e074/image.png" alt="">
이제 여기서
<img src="https://velog.velcdn.com/images/hi_do/post/871b185c-4a70-43f8-8f21-b1a2e220aaec/image.png" alt="">
이녀석을 찾아 설치하여 실행한다.
<img src="https://velog.velcdn.com/images/hi_do/post/56df0949-bff6-4d88-8a48-8f2c404b5d49/image.png" alt="">
와중에 갑자기 오류가 발생했다...
3트째 오류 발생이라 답답했다....
무엇이 문제인가 해서 알아보니 해결방법은 다음과 같았다.
제어판 -&gt; 프로그램 -&gt; Windows 기능 켜기/끄기 로 들어가서
<img src="https://velog.velcdn.com/images/hi_do/post/07ba0f33-0da6-4f60-9d74-b370e0ee8f3f/image.png" alt="">
이렇게 체크를 해야했다...
진작에 알아볼걸...
<img src="https://velog.velcdn.com/images/hi_do/post/5ed70daa-7ce9-45eb-aea0-16b9c7af0149/image.png" alt="">
해결되었다!</p>
<h3 id="ubuntu-제거">Ubuntu 제거</h3>
<p>아니 근데 다른 용무가 생겨서 꽤나 자리를 비운고 username을 입력하니 안되길래 그냥 삭제하고 다시 설치하려고한다.
<img src="https://velog.velcdn.com/images/hi_do/post/3d533096-c057-4aee-9b94-77ccfdd55104/image.png" alt="">
PowerShell을 실행하고 &#39;wsl --list --all&#39;을 입력해준다
<img src="https://velog.velcdn.com/images/hi_do/post/bcb266b7-7624-4321-9c98-c344a706bf0a/image.png" alt="">
&#39;wsl --unregister <DistributionName>&#39;을 입력한다.</p>
<h3 id="ubuntu-재실행">Ubuntu 재실행</h3>
<p>  이제 다시 실행하면
  <img src="https://velog.velcdn.com/images/hi_do/post/e4d85adc-0331-46c2-851e-eb9996cf1a9b/image.png" alt="">
  자 이제 차례대로 입력해보자
  <img src="https://velog.velcdn.com/images/hi_do/post/4ae64bc2-2a6e-4db8-b0c0-f207cc2c5c03/image.png" alt="">
입력하면 이렇게 성공창이 나오고
  <img src="https://velog.velcdn.com/images/hi_do/post/83f80aa2-0c40-4a22-ae9d-1b3f4458bf66/image.png" alt="">
오예! 완전 성공이야!
  이제 wsl을 실행해보자
  <img src="https://velog.velcdn.com/images/hi_do/post/414e2fcc-37f0-437f-a855-c6ad3666bed5/image.png" alt="">
전에는 실행마저 안되더니 이제는 실행되면서 저렇게 나온다!</p>
<h3 id="버전-확인">버전 확인</h3>
<p>Powershell에서 &#39;wsl -l -v&#39;를 입력하여 버전을 확인한다.
  <img src="https://velog.velcdn.com/images/hi_do/post/8d0c72d6-1cdc-4002-aedd-bd977581220f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Apache Tomcat 설치 및 설정]]></title>
            <link>https://velog.io/@hi_do/Apache-Tomcat-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@hi_do/Apache-Tomcat-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Tue, 24 Sep 2024 05:02:20 GMT</pubDate>
            <description><![CDATA[<h2 id="apache-tomcat-이란">Apache Tomcat 이란?</h2>
<p>자바 서블릿(Java Servlet)이나 자바 서버 페이지(Java Server Page, JSP)로 동적 웹 사이트(dynamic websites)를 구축하려면 웹 어플리케이션 서버(Web Application Server, WAS)가 필요하다.
WAS는 웹 서버(web server)가 전달해준 사용자의 요청을 받아서, 데이터베이스 서버와 통신하고 데이터를 가공하여 응답을 돌려주는 미들웨어 프로그램임</p>
<h3 id="버전-차이점">버전 차이점</h3>
<p>아파치 톰캣은 여러 버전이 있는데 서블릿 코드를 작성하거나 자바 8 버전 이상을 사용 또는 자바 엔터프라이즈 에디션(Java Enterprise Edition, JavaEE)를 구현한 버전을 설치하려면 톰캣 9 버전을 사용하고 자카르타 엔터프라이즈 에디션(Jakarta Enterprise Edition, JakartaEE)를 구현한 버전을 사용하려면 톰캣 10을 사용한다고 한다.
따라서!! 나는 9버전을 다운받을 예정!!</p>
<h2 id="apache-tomcat-설치">Apache Tomcat 설치</h2>
<p><a href="https://tomcat.apache.org/whichversion.html">https://tomcat.apache.org/whichversion.html</a>
여기 사이트로 들어가면 된다.
<img src="https://velog.velcdn.com/images/hi_do/post/01be1597-63c3-4fed-9d66-fba95501589d/image.png" alt="">
들어가면 이런 페이지가 나온다.
<img src="https://velog.velcdn.com/images/hi_do/post/8c9836b8-c0f4-4aec-adca-c4985fac9fcf/image.png" alt="">
여기서 &#39;Tomcat 9&#39;를 클릭하면
<img src="https://velog.velcdn.com/images/hi_do/post/638870a5-a038-429a-b8a7-4f8b7cf6c8cc/image.png" alt="">
이 페이지로 넘어가는데
<img src="https://velog.velcdn.com/images/hi_do/post/09c3485e-4cce-4479-ba9c-1cdbfd17c9ca/image.png" alt="">
여기서 나는 &#39;64-bit Windows zip&#39; 이거를 받을거다.
설치가 완료되면 &#39;C:\Program Files\Java&#39; 이 경로로 압축을 풀어준다.
<img src="https://velog.velcdn.com/images/hi_do/post/96af036d-1dc0-4311-96e0-e6dddf2c6624/image.png" alt="">
짠! 이런식으로 하면 일단 설치는 완료!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Eclipse Error - Launch configuration references non-existing project 해결 방법]]></title>
            <link>https://velog.io/@hi_do/Eclipse-Error-Launch-configuration-references-non-existing-project-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@hi_do/Eclipse-Error-Launch-configuration-references-non-existing-project-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 23 Sep 2024 16:02:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hi_do/post/d32e1b4c-97dd-4a5d-a281-4bc3bbd668f3/image.png" alt="">
실행을 하다가 갑자기 이런 창이 나왔다.
도대체 무엇인가 하면서 계속해도 이런 오류창이 계속 나와서 검색을 해봤다.
<img src="https://velog.velcdn.com/images/hi_do/post/7315cffb-1be4-4fbc-943f-6f27b7aa67f9/image.png" alt="">
실행하고자 하는 곳에 오른쪽 마우스키를 이용하여 &#39;Run As&#39;의 &#39;Run Configuration&#39;을 클릭한다.
<img src="https://velog.velcdn.com/images/hi_do/post/a2889432-6950-4fda-ba77-990524ab267d/image.png" alt="">
이런 창이 나오는데 여기서
<img src="https://velog.velcdn.com/images/hi_do/post/f1bce2cc-9bfb-41a7-a9a1-841b2b5ff500/image.png" alt="">
&#39;Java Application&#39; 하위에 있는것을 모두 삭제하면 된다고 한다.
<img src="https://velog.velcdn.com/images/hi_do/post/20c7b85d-b535-452d-a593-dd319e72f787/image.png" alt="">
이렇게 하면 됨
<img src="https://velog.velcdn.com/images/hi_do/post/c0b7e138-2936-4559-b3a5-e1d715003660/image.png" alt="">
그럼 해결!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java 환경변수 및 Eclipse 초기 설정]]></title>
            <link>https://velog.io/@hi_do/Temp-Title-4l33n8kr</link>
            <guid>https://velog.io/@hi_do/Temp-Title-4l33n8kr</guid>
            <pubDate>Mon, 23 Sep 2024 15:56:27 GMT</pubDate>
            <description><![CDATA[<h2 id="java-환경변수-설정">Java 환경변수 설정</h2>
<p>먼저 <a href="https://www.oracle.com/kr/java/technologies/downloads/">https://www.oracle.com/kr/java/technologies/downloads/</a> 에서 jdk를 설치한다.
<img src="https://velog.velcdn.com/images/hi_do/post/d9ddfac8-180e-4e91-9c3f-8d155184a1a5/image.png" alt="">
나는 jdk23을 설치했다.
<img src="https://velog.velcdn.com/images/hi_do/post/06e0bf7b-1c86-441b-8ccf-ffcc4c5f5e20/image.png" alt="">
저 세개 중 이것을 설치하면 된다.
<img src="https://velog.velcdn.com/images/hi_do/post/e91675e3-76bc-4376-9c4b-166825bfee1b/image.png" alt="">
설치가 완료되면 로컬 디스크 c에 Java 폴더가 생겨있다.
이제 설치가 완료됬으니 환경변수를 설정해보자
<img src="https://velog.velcdn.com/images/hi_do/post/05a12030-3206-47f6-aa01-f66c28746bf0/image.png" alt="">
여기 시스템 환경 변수 편집으로 이동한다.
<img src="https://velog.velcdn.com/images/hi_do/post/a93b06bb-8764-4bed-82ad-293c6ff4e0bf/image.png" alt="">
여기서 환경변수(N)을 클릭한다.
<img src="https://velog.velcdn.com/images/hi_do/post/1f44fc0d-a73e-432d-bff0-42c09a2694ce/image.png" alt="">
시스템 변수의 새로만들기를 클릭한다.
<img src="https://velog.velcdn.com/images/hi_do/post/c0439ad8-a834-4c89-8353-e8af122bf961/image.png" alt="">
변수 이름은 &#39;JAVA_HOME&#39;이라고 칭하겠다.
변수 값은 java를 설치했던 경로인 <img src="https://velog.velcdn.com/images/hi_do/post/3a7ba554-cfdd-425d-b4de-b8327f2abc99/image.png" alt="">
여기가 변수 값이다.
이제 확인을 클릭하면
<img src="https://velog.velcdn.com/images/hi_do/post/25fd4b39-d73b-4169-be1e-622a6d6c13b7/image.png" alt="">
이렇게 생겨진다.
다음으로 시스템 변수에서 새로만들기를 클릭하여
<img src="https://velog.velcdn.com/images/hi_do/post/bc99f21b-17e1-4ea2-a3d2-df1550b3acef/image.png" alt="">
이렇게 생성한다.
다음으로 시스템 변수의 Path를 더블클릭하여 환경변수편집에서
<img src="https://velog.velcdn.com/images/hi_do/post/c53372b4-9683-4074-9fc8-296e88a2a5ff/image.png" alt="">
새로만들기를 클릭한 후
<img src="https://velog.velcdn.com/images/hi_do/post/b8fd60f0-17d7-402e-acd2-354017a9d47a/image.png" alt="">
이렇게 생성해 준다 
그 후 위로 이동을 클릭하여 제일 위로 올려준다.
이제 제대로 됬는지 확인하기 위해 cmd를 실행한다.
<img src="https://velog.velcdn.com/images/hi_do/post/b0c89171-1dd9-49b5-b8cb-62a1f2bc1f79/image.png" alt="">
cmd 실행 후 &#39;java -version&#39;을 입력하여 확인한다.
<img src="https://velog.velcdn.com/images/hi_do/post/71f666b4-bf27-4153-83f1-e085dbf11ff9/image.png" alt="">
cmd에서 &#39;javac&#39;라고 입력하면 더 자세하게 나오긴한다.(참고)</p>
<h2 id="eclipse-설치">Eclipse 설치</h2>
<p><a href="https://www.eclipse.org/downloads/">https://www.eclipse.org/downloads/</a>
이 사이트에서 설치하면 된다.
<img src="https://velog.velcdn.com/images/hi_do/post/9bd406c0-611c-418e-a697-ad86ee61eca0/image.png" alt="">
여기서 &#39;Download Packages&#39;를 클릭한다.
<img src="https://velog.velcdn.com/images/hi_do/post/9a750974-d167-4d45-9430-df69970db1cb/image.png" alt="">
여기서 원하는 것을 설치하면 되는데 나는 windows라서 windows꺼를 설치한다.
<img src="https://velog.velcdn.com/images/hi_do/post/e0df6a24-2c5e-4dce-87d6-bec106eaa0bb/image.png" alt="">
설치가 완료되면 로컬디스크c에 압축을 풀어준다.
(굳이 이러는 이유는 경로가 길면 압축해제가 안되기 때문이라나 뭐라나;;)
압축이 풀리면 Eclipse를 실행한다.
<img src="https://velog.velcdn.com/images/hi_do/post/08871ea2-2218-4a61-8d6a-5afa01acf82e/image.png" alt="">
여기 이 창에서
<img src="https://velog.velcdn.com/images/hi_do/post/4550974f-b7b1-4228-b6a3-803f3220f037/image.png" alt="">
상단의 돋보기 옆의 이 아이콘을 클릭한다.
<img src="https://velog.velcdn.com/images/hi_do/post/b05eb49a-32e6-4827-a9f1-da8e2d5eef2a/image.png" alt="">
그럼 이런 창이 나오는데 여기서 &#39;Java&#39;를 클릭한 후 &#39;Open&#39;을 클릭한다.
<img src="https://velog.velcdn.com/images/hi_do/post/e6dffea7-bf1a-4657-85d1-8fc376f7fcb9/image.png" alt="">
그다음 &#39;Window&#39;탭의 &#39;Preferences&#39;를 클릭한다.
<img src="https://velog.velcdn.com/images/hi_do/post/67a42d3f-ccfa-41ff-bb10-32fe5311205b/image.png" alt="">
그럼 이런 창이 나오는데 여기서 처음에 &#39;Java&#39;를 찾아서 &#39;Insalled JRES&#39;를 클릭한다.
그 다음 &#39;Add&#39;를 클릭하면
<img src="https://velog.velcdn.com/images/hi_do/post/6323cdd8-d957-413b-9eea-59df19f5f466/image.png" alt="">
이런 창이 나오고 여기서 &#39;Standard VM&#39;을 클릭하고 &#39;Next&#39;를 클릭한다.
<img src="https://velog.velcdn.com/images/hi_do/post/c91b5623-0bac-4863-a30f-0de3bc75a97c/image.png" alt="">
그럼 이런 창이 나오는데 여기서 &#39;Directory&#39;를 클릭하여 경로를 이동한다.
<img src="https://velog.velcdn.com/images/hi_do/post/02de30c4-02f3-423f-9516-96ba8bf5323a/image.png" alt="">
그럼 폴더 선택창이 나오는데 여기까지 이동하여 &#39;폴더 선택&#39; 클릭!
<img src="https://velog.velcdn.com/images/hi_do/post/73825123-3765-4aa5-a3f5-4b404a03e3f5/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/bbaa69fc-0eac-452d-a497-2283cc15aaa8/image.png" alt="">
...................??!?!?!?
(아... 이미 있다고 한다...안녕...)
(그냥 원래 있는거 사용하면 될거 같다...)
<img src="https://velog.velcdn.com/images/hi_do/post/b660a015-f23b-4825-a369-38debbf45fb1/image.png" alt="">
짠! 완성!!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS]]></title>
            <link>https://velog.io/@hi_do/AWS</link>
            <guid>https://velog.io/@hi_do/AWS</guid>
            <pubDate>Tue, 25 Jun 2024 02:46:41 GMT</pubDate>
            <description><![CDATA[<h3 id="초기-설정">초기 설정</h3>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/6cf91747-3620-4617-ba06-f6f046b19b4b/image.png" alt="">
초기에 지역 설정은 서울로 해야한다.(아니면 과금이 나갈 수도 있음)
<img src="https://velog.velcdn.com/images/hi_do/post/35e04d3f-0b45-4283-ab53-3b5136513015/image.png" alt="">
EC2 검색 후 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/6cda6f61-7162-4de8-8053-a08bb83a299d/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/c9ee539f-f67d-4ac6-b635-0573eb47dc26/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/98e074c7-a2d0-4903-a021-ea9609156904/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/3a150368-8e65-4bb0-b96c-12e551001d34/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/6ae42129-1877-419e-810c-22316e887113/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/ee503dcf-96fe-494a-8c1b-1150e602d857/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/9c076d2a-03cd-4936-b1bb-ab847869b67b/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/dab333ee-aee4-4a33-9f5c-a57256062b72/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/946c2d64-8bcb-48ba-8221-a6eea695e35c/image.png" alt="">
추가 클릭 후 초기 화면
<img src="https://velog.velcdn.com/images/hi_do/post/b06ee909-98fc-4e66-8ee4-f3f0f747860a/image.png" alt="">
변경 해준다.
![]
(<a href="https://velog.velcdn.com/images/hi_do/post/5bcc563d-8eb4-4d73-a278-8f5cc29d7acc/image.png">https://velog.velcdn.com/images/hi_do/post/5bcc563d-8eb4-4d73-a278-8f5cc29d7acc/image.png</a>)
인스턴스 시작 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/8b9c215d-052a-4eaa-9db9-416590c1f8f4/image.png" alt="">
인스턴스 아이디 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/c72e1d8f-0a45-45a1-9151-047ec2a3747e/image.png" alt="">
연결 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/e50e9b13-fc06-410a-909e-2ab615e157d3/image.png" alt="">
원격 데스크톱 파일 다운로드 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/cfdaa35a-412e-446a-b35f-3c9a97d55a9a/image.png" alt="">
다운로드 완료
그후 암호 가져오기 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/f3272214-774b-498d-b4e8-7e3a9785bb7b/image.png" alt="">
프라이빗 파일 업로드 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/33cd83b3-f7bc-4d17-8447-42ac5ab19411/image.png" alt="">
업로드하기
<img src="https://velog.velcdn.com/images/hi_do/post/5830e922-d5cb-460c-a550-1bcced75cb75/image.png" alt="">
암호 해독 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/98b88fdd-9128-488e-9519-463b2750cdd7/image.png" alt="">
실행
<img src="https://velog.velcdn.com/images/hi_do/post/cf1d7f9c-cbde-456f-8d81-9e7f8b3aab12/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/f563a97f-f2f6-4f4e-b246-9b49492c29ec/image.png" alt="">
암호 입력 하면 실행 됨 일단 실행되면 종료하기
<img src="https://velog.velcdn.com/images/hi_do/post/c20348cf-9415-49dc-8950-7b33dd61e3ca/image.png" alt="">
인스턴스 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/72633d53-3e75-4e5b-b4d6-c2be0d435afb/image.png" alt="">
완전히 종료해야 한다(돈나간다...)</p>
<h4 id="bucket">bucket</h4>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/be592cbc-1211-4c09-83c2-9e383ea911dd/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hi_do/post/15952045-c1f1-4c9a-a9e4-af376d9b4203/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/bc64bd0b-dee0-47fe-a466-743c586d12ac/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/eaa2de01-f8f2-4307-b9c2-daf77862cac0/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/0ff5501d-ae19-4aa0-a31c-939e50ed28e6/image.png" alt="">
버킷만들기 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/0032c0be-2692-4cd2-a270-6d97f1f3f193/image.png" alt="">
이 화면에 파일을 끌어다 놓으면
<img src="https://velog.velcdn.com/images/hi_do/post/03bb2fed-7242-4b1a-bf19-cd0fabcbb58c/image.png" alt="">
이런화면이 나온다 그 후 업로드 클릭
후 닫기 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/adadfd5e-6eee-49e8-a2c4-3b62b75c9959/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/d437e4b9-5c46-4d89-9fb8-ddcde31c1924/image.png" alt="">
편집 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/534d118f-9870-41d2-8f34-841eab5aaa7c/image.png" alt="">
활성화 클릭 후 index.html 입력 후 변경사항 저장 클릭</p>
<ul>
<li><a href="https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/HostingWebsiteOnS3Setup.html">https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/HostingWebsiteOnS3Setup.html</a>    (aws 사용법)
<img src="https://velog.velcdn.com/images/hi_do/post/9dbdbfe3-d536-4ad6-8b4d-979294a672f6/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/f45eeb6f-0e7d-4cf5-85cf-aed94ab4c1d3/image.png" alt="">
편집 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/3fd66599-cf05-4835-8d12-f507d991e16f/image.png" alt="">
방금 전 코드 복사해서 붙여넣기
<img src="https://velog.velcdn.com/images/hi_do/post/2627f9c6-5f9c-4c8d-9bab-c0dd5a317930/image.png" alt="">
버킷 ARN 복사하여 코드 변경
<img src="https://velog.velcdn.com/images/hi_do/post/1c67383b-284c-4b49-a2fd-5e662131d05e/image.png" alt="">
저장 버튼 누르기
<img src="https://velog.velcdn.com/images/hi_do/post/c0d680db-0586-4267-bcc3-aed905c805f3/image.png" alt="">
객체 소유권 편집
<img src="https://velog.velcdn.com/images/hi_do/post/5efa4cf4-ea63-4a54-9bb1-8b4d6ab22659/image.png" alt="">
ACL 활성화됨 클릭(다른 사람도 접근 가능하게 하기위해)
<img src="https://velog.velcdn.com/images/hi_do/post/5a52198d-1c2b-4974-86ed-031dc3593e3f/image.png" alt="">
체크 후 저장
<img src="https://velog.velcdn.com/images/hi_do/post/3cf1c1ce-f11d-43e5-940a-7dc53969cc26/image.png" alt="">
ACL 편집하기
<img src="https://velog.velcdn.com/images/hi_do/post/86a2c433-d77c-41f8-a199-d286b0addd7f/image.png" alt="">
모든사람 (읽기)클릭 후 저장 후 버킷으로 복귀
<img src="https://velog.velcdn.com/images/hi_do/post/38592df3-bff3-4e0d-ac69-9f4c9c9ea160/image.png" alt="">
여기서 객체URL 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/7e32b57e-4dc1-49bc-a4f6-5844e4075468/image.png" alt="">
클릭 하면 해당 링크로 이동됨<h4 id="bucket-삭제">bucket 삭제</h4>
<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/hi_do/post/9c0c6604-8299-4d35-a10a-a499822d08ba/image.png" alt="">
체크 후 삭제 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/1d8741b0-c7cf-4c05-bb42-f110b4de2b70/image.png" alt="">
버킷 비우기 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/9ebfb388-c254-4e9f-9295-b589ef8df585/image.png" alt="">
영구 삭제 입력 후 비어있음 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/af21d6b7-8bd6-4299-971e-ca34aab6e9f1/image.png" alt="">
체크 후 삭제 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/a798d41d-d22a-4fd7-a7b0-9d6e0584bf54/image.png" alt="">
버킷 이름 입력 후 버킷 삭제 클릭 -&gt; 완료</li>
</ul>
<h4 id="인바운드와-아웃바운드">인바운드와 아웃바운드</h4>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/4a8ba510-548c-4035-be3f-1e2f501f99aa/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/48903241-b83f-4ae9-9ecc-fc1c06b11c33/image.png" alt="">
인스턴스 시작 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/67d5757a-6803-486c-a46d-5799735916bf/image.png" alt="">
기본 설정은 느려서 속도를 더 내기위해 AMI변경
<img src="https://velog.velcdn.com/images/hi_do/post/55d1cd5c-5737-4dbd-b2aa-87fb0071134d/image.png" alt="">
원래 사용하던 키 사용
<img src="https://velog.velcdn.com/images/hi_do/post/e3b3aea7-2a89-4a72-a6a3-ed088f714b52/image.png" alt="">
보안그룹 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/df3502ed-4f98-43eb-838b-9d7c7f6b4d1e/image.png" alt="">
인바운드 규칙 편집 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/3b813ba7-069f-4555-ada2-7eb47f7bedf0/image.png" alt="">
인바운드 추가하여 설정
<img src="https://velog.velcdn.com/images/hi_do/post/f44a7b35-aa75-45c5-a664-a7092b9ad393/image.png" alt="">
인바운드 추가하여 설정 후 규칙 저장
<img src="https://velog.velcdn.com/images/hi_do/post/731db623-b531-4c99-8f4e-e4664749af8d/image.png" alt="">
아웃바운드 규칙 편집 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/af3c1d61-438f-4d3d-acd4-c151bd347bfd/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/42462dde-e907-4cc7-ab66-8bb10338e551/image.png" alt="">
아웃바운드 추가하여 설정 후 규칙 저장
<img src="https://velog.velcdn.com/images/hi_do/post/6f22b3b0-f1de-4d59-91e3-a577a3c86f10/image.png" alt="">
그 후 인스턴스 들어가보면 실행중</p>
<h4 id="실행방법">실행방법</h4>
<blockquote>
</blockquote>
<p><a href="https://www.putty.org">https://www.putty.org</a>
<img src="https://velog.velcdn.com/images/hi_do/post/7e190191-88bc-423e-9888-199aa84e8c47/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/c5acc22d-12fe-465a-b072-d9acff4cb836/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/ec11ee24-6e25-43ca-8ce1-073b25228248/image.png" alt="">
두 파일 다운로드
<img src="https://velog.velcdn.com/images/hi_do/post/0e7a356f-3bf5-4651-bfcf-367d41936eba/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/f9427378-7c10-42f7-b1fa-b3740b0090e2/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/73978f24-9d20-4e53-8d32-05889ae846f8/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/330135e1-d0f2-4de8-9003-43777431a675/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/11e0f2e4-808a-41a8-9863-5167626a71cc/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/29bd264d-8a03-4fe9-b27d-7004e2c29599/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/9678a5df-0754-4e78-bc60-b1f69feb2120/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/4f358df6-180e-4cb1-bd5e-8e4c64a7f68e/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/38b9bc60-fd70-46dc-bf0f-fd004675ceeb/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/7e7750b7-bb0c-4ea3-870a-1f4d45226b80/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/d26712e3-603a-4026-8428-edd8c13fd55d/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/fc85364e-6aaa-46be-8661-23fdf1b96796/image.png" alt="">
yum list java<em>17\</em> 입력해서 -&gt; jdk17 설치
<img src="https://velog.velcdn.com/images/hi_do/post/80396422-d7d3-42bb-b031-0233fc0a24f6/image.png" alt="">
sudo yum install java-17-amazon-corretto.x86_64 입력하여 설치
그 후 y d입력하면 설치됨
<img src="https://velog.velcdn.com/images/hi_do/post/aaa6df1f-a8e9-4bfe-926f-aed6e943a05e/image.png" alt=""></p>
<p>####</p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/7d3bf95e-02a8-4ec6-b9e2-bd4f7f87c259/image.png" alt="">
버전이 나옴
<img src="https://velog.velcdn.com/images/hi_do/post/6af43865-1fc9-477e-8b03-d9ecc122e59c/image.png" alt="">
generate 클릭하여 생성된 압축파일 여기에 압출 풀기
<img src="https://velog.velcdn.com/images/hi_do/post/89071697-d166-4eed-8b36-fb0eb3659b98/image.png" alt="">
IntelliJ실행</p>
<p>####</p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/5b950ad0-4e71-400e-9ef4-3e50ca028921/image.png" alt="">
index.html과 SampleController.java 생성</p>
<ul>
<li>SampleController.java<pre><code class="language-java">package com.appEC2.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(&quot;/api/sample&quot;)
public class SampleController {
  @GetMapping(&quot;/getArr&quot;)
  public String[] getArr(){
      return new String[]{&quot;AAA&quot;,&quot;BBB&quot;,&quot;CCC&quot;};
  }
}</code></pre>
</li>
<li>실행 결과
<img src="https://velog.velcdn.com/images/hi_do/post/688e4463-7bec-4a57-8179-7a2760b76e3f/image.png" alt=""></li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>깃허브로 프로젝트를 올리고 ec2에서 이 깃을 이용해서 내려받은 후에 빌드해서 실행하는 방법.
<img src="https://velog.velcdn.com/images/hi_do/post/a52e859a-d608-4c96-9ff8-3db66bb81860/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/15cc0a05-1be7-4904-b461-fd511c270dce/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/affe3505-6915-4ec4-9c74-7698a78f92c2/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/4e58e1dc-21f1-4bd7-87fd-aa1c4c3f2693/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/3dbac9e8-65b0-4ebf-b605-6ec7b817e0fc/image.png" alt="">
오류가 나와서 -&gt;
<img src="https://velog.velcdn.com/images/hi_do/post/e8193a84-79fc-4462-9572-a0a63acb45e3/image.png" alt="">
이걸 다운 받아서 이용
<img src="https://velog.velcdn.com/images/hi_do/post/6ba0e16b-6779-47f1-81ef-de7721ea9e5c/image.png" alt="">
위에 방법과 동일하다</li>
<li>private을 public으로 바꾸는 방법
<img src="https://velog.velcdn.com/images/hi_do/post/411e57c0-33b0-4d5f-9ff9-d337168f98d2/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/734cf7ac-33dc-4389-8a71-c008b2441aeb/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/5f1d08f6-6f66-465a-af68-52d2f02b8dd7/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/dfc70d1b-d542-4a34-be03-0fc312bcc553/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/f19349a1-a293-47ac-aeb7-282e53027c12/image.png" alt="">
sudo yum install git 입력 -&gt; git 설치
<img src="https://velog.velcdn.com/images/hi_do/post/58713b8d-4528-495e-b9a3-a2bbd96a8429/image.png" alt="">
mkdir webtest 입력
<img src="https://velog.velcdn.com/images/hi_do/post/f0f49dc2-5839-434f-abab-8080f5230fd0/image.png" alt="">
cd webtest 입력
<img src="https://velog.velcdn.com/images/hi_do/post/4af54528-ae26-44f7-8e5e-c5ac37db9a85/image.png" alt="">
pwd 입력
<img src="https://velog.velcdn.com/images/hi_do/post/91f94109-0d7a-44c2-86d6-bac804d3a67d/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/b80b38f0-459f-4840-afc2-67e7fd4a460e/image.png" alt="">
클론생성
git clone <a href="https://github.com/*/">https://github.com/*/</a>* 본인 깃헙 주소 입력
ls -&gt; cd appEC2 -&gt; ls -&gt; ls -&gt; sudo chmod 777 ./gradlew -&gt; ./gradlew build 순서대로 입력
<img src="https://velog.velcdn.com/images/hi_do/post/1e5895a0-d76b-4cbd-9397-6d8415fd0f98/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/5b969919-4200-4c3a-8f05-e0820c6effb3/image.png" alt="">
ls -&gt; cd build -&gt; ls 입력
<img src="https://velog.velcdn.com/images/hi_do/post/286b9091-b752-4dc1-912b-6b3665e750e8/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/7836d7ea-23e3-4086-96fd-2a239b9b4d16/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/0e74356a-3e75-4311-93ca-01ba78e8c917/image.png" alt="">
java -jar ec2-0.0.1-SNAPSHOT.jar
<img src="https://velog.velcdn.com/images/hi_do/post/5debb6dc-8ed3-419a-b3dd-18a4cc7143d4/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SpringBoot - 로그인과 회원가입]]></title>
            <link>https://velog.io/@hi_do/SpringBoot-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85</link>
            <guid>https://velog.io/@hi_do/SpringBoot-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85</guid>
            <pubDate>Tue, 18 Jun 2024 06:38:22 GMT</pubDate>
            <description><![CDATA[<h4 id="스프링-시큐리티">스프링 시큐리티</h4>
<blockquote>
</blockquote>
<ul>
<li>스프링 기반 웹 애플리케이션의 인증과 권한을 담당하는 스프링의 하위 프레임워크이다</li>
<li>인증(authenticate)은 로그인과 같은 사용자의 신원을 확인하는 프로세스를, 권한(authorize)은 인증된 사용자가 어떤 일을 할 수 있는지(어떤 접근 권한이 있는지)를 관리하는 것을 의미한다.</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>build.gradle에 <pre><code class="language-java">implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
implementation &#39;org.thymeleaf.extras:thymeleaf-extras-springsecurity6&#39;</code></pre>
시큐리티와 타임리프를 연결해주는 라이브러리 추가 후 refresh</li>
<li><blockquote>
<p>적용후 실행 결과
<img src="https://velog.velcdn.com/images/hi_do/post/c1b808d9-97d1-4b20-bea3-7a2dc9e46f74/image.png" alt=""></p>
</blockquote>
</li>
<li>스프링 시큐리티는 기본적으로 인증되지 않은 사용자가 웹서비스를 사용할 수 없게끔 만드는게 기본값이다.</li>
<li>로그인을 하지 않아도 게시물을 조회할 수 있도록 설정을 변경해줘야 한다.</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>SecurityConfig.java 생성<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity    // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 된다.
public class SecurityConfig {
  @Bean
  SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
      http.authorizeHttpRequests((AuthorizeHttpRequests) -&gt;
      AuthorizeHttpRequests.requestMatchers(new AntPathRequestMatcher(&quot;/**&quot;)).permitAll());
      return http.build();
  }
}</code></pre>
@Configuration : 스프링의 환경 설정 파일
@EnableWebSECURITY : 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 에너테이션</li>
<li><blockquote>
<p>이 에너테이션을 통해 시큐리티를 활성화시킬 수 있는데 내부적으로 SecurityFilterChain 클래스 동작하여 모든 요청 URL에 이 클래스 필터로 적용되어 URL별로 특별한 설정을 할 수 있다.</p>
</blockquote>
</li>
</ul>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/2dc8019b-b220-4c09-9c82-418a9f4ae147/image.png" alt=""></p>
<ul>
<li>There was an unexpected error (type=Forbidden, status=403).
Forbidden</li>
<li><blockquote>
<p>스프링 시큐리티의 CSRF방어 기능에 의해 H2콘솔 접근이 거부된것.
CSRF는 웹 보안 공격 중 하나로, 조작된 정보로 웹 사이트가 실행되도록 속이는 공격 기술이다.
스프링 시큐리티는 이 공격을 방지하기 위해 CSRF 토큰을 세션을 통해 발행하고, 웹 페이지에서는 폼 전송시 해당 토큰을 함께 전송해서 실제 웹 페이지에서 작성한 데이터가 전달되는지를 검증한다.
== 토큰이란 요청을 식별하고 검증하는데 사용하는 특수한 문자열 또는 값을 의미한다.
== 세션이란 사용자의 상태를 유지하고 관리하는데 사용하는 기능이다.</p>
</blockquote>
</li>
</ul>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/145511b1-3da5-4b40-a6f8-7670483d8a84/image.png" alt=""></p>
<pre><code class="language-java">&lt;form action=&quot;/question/create&quot; method=&quot;post&quot;&gt;&lt;input type=&quot;hidden&quot; name=&quot;_csrf&quot; value=&quot;SzbJ15G-MjZdJl7BGAw995aTGZxZJhiGCPmKbRKOXCOA2O8BegD_7qiHBgZwETulLCEJwK-iNP06Qi2raZjsXXbqaBq46dk1&quot;/&gt;</code></pre>
<p>-&gt; 스프링 시큐리티에 의해 CSRF토큰이 자동으로 생성된다.
== CSRF토큰은 서버에서 생성되는 임의의값으로 페이지 요청시 항상 다른 값으로 생성한다.
스프링 시큐리티는 이런 식으로 페이지에 CSRF 토큰을 발행하여 이 값이 다시 서버로 정확하게 들어오는지 확인하는 과정을 거친다. 만약 CSRF 토큰이 없거나 해커가 임의의 CSRF 토큰을 강제로 만들어 전송하면 스프링 시큐리티에 의해 차단된다. </p>
<ul>
<li>SecurityConfig.java 수정<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity   // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 된다.
public class SecurityConfig {
 @Bean
 SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    http.authorizeHttpRequests((authorizeHttpRequests) -&gt;
    authorizeHttpRequests.requestMatchers(new AntPathRequestMatcher(&quot;/**&quot;)).permitAll())
    .csrf(csrf -&gt; csrf    //    /h2-console/로 시작하는 모든 URL은 CSRF 검증을 하지 않겠다.
          .ignoringRequestMatchers(new AntPathRequestMatcher(&quot;/h2-console/**&quot;)));    
    return http.build();
 }
}</code></pre>
</li>
</ul>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/1939805e-b19a-433c-95bc-387d14328c95/image.png" alt=""></p>
<ul>
<li>H2화면이 프레임 구조로 작성되어있다.</li>
<li>H2레이아웃이 화면처럼 작업 영역이 나눠져 있다.</li>
<li>스프링 시큐리티는 웹 사이트의 콘텐츠가 다른 사이트에 포함되지 않도록 하기 위해 X-Frame-Options 헤더의 기본값을 DENY로 사용하는데, 프레임 구조의 웹사이트는 이 헤더의 값을 DENY인 경우 오류가 발생한다.
== 스프링 부트에서 X-Frame-Options 헤더는 클릭재킹 공격을 막기 위해 사용한다.
클릭재킹 : 사용자의 의도와 다른 작업없이 수행되도록 속이는 보안 공격 기술</li>
<li>SecurityConfig.java 수정<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity   // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 된다.
public class SecurityConfig {
 @Bean
 SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    http.authorizeHttpRequests((authorizeHttpRequests) -&gt;
    authorizeHttpRequests.requestMatchers(new AntPathRequestMatcher(&quot;/**&quot;)).permitAll())
    .csrf(csrf -&gt; csrf    //    /h2-console/로 시작하는 모든 URL은 CSRF 검증을 하지 않겠다.
          .ignoringRequestMatchers(new AntPathRequestMatcher(&quot;/h2-console/**&quot;)))
    .headers(headers -&gt; headers
            .addHeaderWriter(new XFrameOptionsHeaderWriter(
                    XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)));      
    return http.build();
 }
}</code></pre>
</li>
<li>URL 요청시 X-Frame-Options 헤더를 DENY 대신 SAMEORIGIN 으로 설정하여 오류가 발생하지 않도록 정의</li>
<li>X-Frame-Options 헤더의 값으로 SAMEORIGIN을 설정하면 프레임에 포함된 웹 페이지가 동일한 사이트에서 제공할때만 사용이 허락된다.</li>
</ul>
<h4 id="회원가입-기능-구현">회원가입 기능 구현</h4>
<blockquote>
</blockquote>
<ul>
<li>회원 entity
username, password, email</li>
<li>SiteUser.java 생성<pre><code class="language-java">package com.mysite.sbb.user;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class SiteUser {    
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;    
  @Column(unique = true)
  private String username;    
  private String password;    
  @Column(unique = true)
  private String email;
}</code></pre>
</li>
<li>UserRepository.java(interface) 생성<pre><code class="language-java">package com.mysite.sbb.user;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository&lt;SiteUser, Long&gt;{
}</code></pre>
</li>
<li>UserService.java 생성<pre><code class="language-java">package com.mysite.sbb.user;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class UserService {
  private final UserRepository userRepository;
  public SiteUser create(String username, String email, String password) {
      SiteUser user = new SiteUser();
      user.setUsername(username);
      user.setEmail(email);
      // 스프링 시큐리티에서 BCrytPasswordEncoder를 사용
      // 해시함수를 사용하는데 비밀번호 같은 보안 정보를 안전하게 저장하고 검증할 때 사용하는 암호화 기술
      BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
      user.setPassword(passwordEncoder.encode(password));
      this.userRepository.save(user);
      return user;
  }
}</code></pre>
</li>
<li>SecurityConfig.java<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity   // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 된다.
public class SecurityConfig {
 @Bean
 SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    http.authorizeHttpRequests((authorizeHttpRequests) -&gt;
    authorizeHttpRequests.requestMatchers(new AntPathRequestMatcher(&quot;/**&quot;)).permitAll())
    .csrf(csrf -&gt; csrf    //    /h2-console/로 시작하는 모든 URL은 CSRF 검증을 하지 않겠다.
          .ignoringRequestMatchers(new AntPathRequestMatcher(&quot;/h2-console/**&quot;)))
    .headers(headers -&gt; headers
            .addHeaderWriter(new XFrameOptionsHeaderWriter(
                    XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)));   
    return http.build();
 }  
 @Bean    //Ioc(Inversion of Control)로 등록.
 PasswordEncoder passwordEncoder() {
     return new BCryptPasswordEncoder();
 }
}</code></pre>
</li>
<li>UserSerive.java 수정<pre><code class="language-java">package com.mysite.sbb.user;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class UserService {    
  private final UserRepository userRepository;
  private final PasswordEncoder passwordEncoder;
  public SiteUser create(String username, String email, String password) {
      SiteUser user = new SiteUser();
      user.setUsername(username);
      user.setEmail(email);
      // 스프링 시큐리티에서 BCrytPasswordEncoder를 사용
      // 해시함수를 사용하는데 비밀번호 같은 보안 정보를 안전하게 저장하고 검증할 때 사용하는 암호화 기술
      user.setPassword(passwordEncoder.encode(password));
      this.userRepository.save(user);
      return user;
  }
}</code></pre>
</li>
<li>UserCreateForm.java 생성<pre><code class="language-java">package com.mysite.sbb.user;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserCreateForm {
 @Size(min =3, max =25)
 @NotEmpty(message = &quot;사용자 ID는 필수 항목입니다.&quot;)
 private String username;  
 @NotEmpty(message = &quot;비밀번호는 필수 항목입니다.&quot;)
 private String password1;
 @NotEmpty(message = &quot;비밀번호는 필수 항목입니다.&quot;)
 private String password2;
 @NotEmpty(message = &quot;이메일은 필수 항목입니다.&quot;)
 @Email
 private String email;   
}</code></pre>
</li>
<li>UserController.java 생성<pre><code class="language-java">package com.mysite.sbb.user;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
@RequestMapping(&quot;/user&quot;)
public class UserController {
 private final UserService userService;
 @GetMapping(&quot;/signup&quot;)
 public String signup(UserCreateForm userCreateForm) {
    return &quot;signup_form&quot;;
 }
 @PostMapping(&quot;/signup&quot;)
 public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
       return &quot;signup_form&quot;;
    }
    if (!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())) {
                                       // 필드명, 오류코드, 오류메시지
       bindingResult.rejectValue(&quot;password2&quot;, &quot;passwordInCorrect&quot;, &quot;2개의 패스워드가 일치하지 않습니다.&quot;);
       return &quot;signup_form&quot;;
    }
    userService.create(userCreateForm.getUsername(), userCreateForm.getEmail(), userCreateForm.getPassword1());     
    return &quot;redirect:/&quot;;
 }
}</code></pre>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>signup_form.html 생성<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
 &lt;div class=&quot;my-3 border-bottom&quot;&gt;
    &lt;div&gt;
       &lt;h4&gt;회원가입&lt;/h4&gt;
    &lt;/div&gt;
 &lt;/div&gt;
 &lt;form th:action=&quot;@{/user/signup}&quot; th:object=&quot;${userCreateForm}&quot; method=&quot;post&quot;&gt;
    &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;username&quot; class=&quot;form-label&quot;&gt;사용자ID&lt;/label&gt;
       &lt;input type=&quot;text&quot; th:field=&quot;*{username}&quot; class=&quot;form-control&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;password1&quot; class=&quot;form-label&quot;&gt;비밀번호&lt;/label&gt;
       &lt;input type=&quot;password&quot; th:field=&quot;*{password1}&quot; class=&quot;form-control&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;password2&quot; class=&quot;form-label&quot;&gt;비밀번호 확인&lt;/label&gt;
       &lt;input type=&quot;password&quot; th:field=&quot;*{password2}&quot; class=&quot;form-control&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;email&quot; class=&quot;form-label&quot;&gt;이메일&lt;/label&gt;
       &lt;input type=&quot;email&quot; th:field=&quot;*{email}&quot; class=&quot;form-control&quot;&gt;
    &lt;/div&gt;
    &lt;button type=&quot;submit&quot; class=&quot;btn btn-primary&quot;&gt;회원가입&lt;/button&gt;
 &lt;/form&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre>
</li>
<li>layout.html 수정<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
 &lt;!-- Required meta tags --&gt;
 &lt;meta charset=&quot;utf-8&quot;&gt;
 &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1, shrink-to-fit=no&quot;&gt;
 &lt;!-- Bootstrap CSS --&gt;
 &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; th:href=&quot;@{/bootstrap.min.css}&quot;&gt;
 &lt;!-- sbb CSS --&gt;
 &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; th:href=&quot;@{/style.css}&quot;&gt;
 &lt;title&gt;Hello, sbb!&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
 &lt;nav th:fragment=&quot;navbarFragment&quot; class=&quot;navbar navbar-expand-lg navbar-light bg-light border-bottom&quot;&gt;
    &lt;div class=&quot;container-fluid&quot;&gt;
       &lt;a class=&quot;navbar-brand&quot; href=&quot;/&quot;&gt;SBB&lt;/a&gt;
       &lt;button class=&quot;navbar-toggler&quot; type=&quot;button&quot; data-bs-toggle=&quot;collapse&quot; data-bs-target=&quot;#navbarSupportedContent&quot;
          aria-controls=&quot;navbarSupportedContent&quot; aria-expanded=&quot;false&quot; aria-label=&quot;Toggle navigation&quot;&gt;
          &lt;span class=&quot;navbar-toggler-icon&quot;&gt;&lt;/span&gt;
       &lt;/button&gt;
       &lt;div class=&quot;collapse navbar-collapse&quot; id=&quot;navbarSupportedContent&quot;&gt;
          &lt;ul class=&quot;navbar-nav me-auto mb-2 mb-lg-0&quot;&gt;
             &lt;li class=&quot;nav-item&quot;&gt;
                &lt;a class=&quot;nav-link&quot; sec:authorize=&quot;isAnonymous()&quot; th:href=&quot;@{/user/login}&quot;&gt;로그인&lt;/a&gt;
                &lt;a class=&quot;nav-link&quot; sec:authorize=&quot;isAuthenticated()&quot; th:href=&quot;@{/user/logout}&quot;&gt;로그아웃&lt;/a&gt;
             &lt;/li&gt;
             &lt;li class=&quot;nav-item&quot;&gt;
                &lt;a class=&quot;nav-link&quot; th:href=&quot;@{/user/signup}&quot;&gt;회원가입&lt;/a&gt;
             &lt;/li&gt;
          &lt;/ul&gt;
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;/nav&gt;
 &lt;!-- 기본 템플릿 안에 삽입될 내용 Start --&gt;
 &lt;th:block layout:fragment=&quot;content&quot;&gt;&lt;/th:block&gt;
 &lt;!-- 기본 템플릿 안에 삽입될 내용 End --&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/6fa466e8-da12-47da-8bcc-2c80ac55118a/image.png" alt=""></li>
</ul>
<h4 id="로그인과-로그아웃">로그인과 로그아웃</h4>
<blockquote>
</blockquote>
<pre><code class="language-html">&lt;a class=&quot;nav-link&quot; sec:authorize=&quot;isAnonymous()&quot; th:href=&quot;@{/user/login}&quot;&gt;로그인&lt;/a&gt;
&lt;a class=&quot;nav-link&quot; sec:authorize=&quot;isAuthenticated()&quot; th:href=&quot;@{/user/logout}&quot;&gt;로그아웃&lt;/a&gt;</code></pre>
<ul>
<li>로그인하지 않은 상태라면 sec:authorize=&quot;isAnonymous()&quot; 가 &#39;참&#39;이 되어 &#39;로그인&#39;링크가 표시되고
로그인한 상태라면 sec:authorize=&quot;isAuthenticated()&quot; 가 &#39;참&#39;이 되어 &#39;로그아웃&#39;링크가 표시된다.</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>UserController.java 수정<pre><code class="language-java">package com.mysite.sbb.user;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
@RequestMapping(&quot;/user&quot;)
public class UserController {
 private final UserService userService;
 @GetMapping(&quot;/signup&quot;)
 public String signup(UserCreateForm userCreateForm) {
    return &quot;signup_form&quot;;
 }
 @PostMapping(&quot;/signup&quot;)
 public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
       return &quot;signup_form&quot;;
    }
    if (!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())) {                                          // 필드명, 오류코드, 오류메시지
       bindingResult.rejectValue(&quot;password2&quot;, &quot;passwordInCorrect&quot;, &quot;2개의 패스워드가 일치하지 않습니다.&quot;);
       return &quot;signup_form&quot;;
    }
    try {
    userService.create(userCreateForm.getUsername(), userCreateForm.getEmail(), 
            userCreateForm.getPassword1());
    // 사용자 id 또는 이메일 주소가 이미 존재할 경우에
    } catch (DataIntegrityViolationException e) {
        e.printStackTrace();
        // 필드, 오류코드, 오류메시지
        bindingResult.reject(&quot;signupFailed&quot;, &quot;이미 등록된 사용자입니다.&quot;);
        return &quot;signup_form&quot;;
    } catch (Exception e) {
        e.printStackTrace();
        bindingResult.reject(&quot;signupFailed&quot;, e.getMessage());
        return &quot;signup_form&quot;;
    }
    return &quot;redirect:/&quot;;
 }
}</code></pre>
</li>
<li>SecurityConfig.java 수정<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity   // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 된다.
public class SecurityConfig {
 @Bean
 SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    http.authorizeHttpRequests((authorizeHttpRequests) -&gt;
    authorizeHttpRequests.requestMatchers(new AntPathRequestMatcher(&quot;/**&quot;)).permitAll())
    .csrf(csrf -&gt; csrf   // /h2-console/로 시작하는 모든 URL은 CSRF 검증을 하지 않겠다.
          .ignoringRequestMatchers(new AntPathRequestMatcher(&quot;/h2-console/**&quot;)))
    .headers(headers -&gt; headers
          .addHeaderWriter(new XFrameOptionsHeaderWriter(
             XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
    .formLogin(formLogin -&gt; formLogin
            .loginPage(&quot;/user/login&quot;)
            .defaultSuccessUrl(&quot;/&quot;));      
    return http.build();
 }   
 @Bean   // Ioc(Inversion of Control)로 등록.
 PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
 }
}</code></pre>
<pre><code class="language-java">.formLogin(formLogin -&gt; formLogin
            .loginPage(&quot;/user/login&quot;)
            .defaultSuccessUrl(&quot;/&quot;));</code></pre>
</li>
<li><blockquote>
<p>.formLogin메서드는 스프링 시큐리티의 로그인 설정을 담당하는 메서드.
설정 내용은 로그인 페이지의 url은(/user/login)이고 로그인 성공시에 이동할 페이지를 루트url(/)임을 의미한다.</p>
</blockquote>
</li>
<li>시큐리티 cycle.
시큐리티가 /user/login 주소 요청이 오면 낚아채서 로그인을 진행시킨다.
로그인을 진행이 완료가 되면 시큐리티 session을 만들어준다.(Security ContextHolder)
오브젝트 타입 =&gt; Authentication 타입 객체로 변환
Authentication 안에 User 정보가 있어야 한다.
User오브젝트 타입 =&gt; UserDetails 타입 객체
즉, Security Session =&gt; Authentication =&gt; UserDetails(PrincipalDetails)</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>UserController.java 수정<pre><code class="language-java">package com.mysite.sbb.user;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
@RequestMapping(&quot;/user&quot;)
public class UserController {
 private final UserService userService;
 @GetMapping(&quot;/signup&quot;)
 public String signup(UserCreateForm userCreateForm) {
    return &quot;signup_form&quot;;
 }
 @PostMapping(&quot;/signup&quot;)
 public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
       return &quot;signup_form&quot;;
    }
    if (!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())) {
                                        // 필드명, 오류코드, 오류메시지
       bindingResult.rejectValue(&quot;password2&quot;, &quot;passwordInCorrect&quot;, &quot;2개의 패스워드가 일치하지 않습니다.&quot;);
       return &quot;signup_form&quot;;
    }
    try {
    userService.create(userCreateForm.getUsername(), userCreateForm.getEmail(), 
            userCreateForm.getPassword1());
    // 사용자 id 또는 이메일 주소가 이미 존재할 경우에
    } catch (DataIntegrityViolationException e) {
        e.printStackTrace();
        // 필드, 오류코드, 오류메시지
        bindingResult.reject(&quot;signupFailed&quot;, &quot;이미 등록된 사용자입니다.&quot;);
        return &quot;signup_form&quot;;
    } catch (Exception e) {
        e.printStackTrace();
        bindingResult.reject(&quot;signupFailed&quot;, e.getMessage());
        return &quot;signup_form&quot;;
    }
    return &quot;redirect:/&quot;;
 }   
 @GetMapping(&quot;/login&quot;)
 public String login() {
     return &quot;login_form&quot;;
 }
}</code></pre>
</li>
<li>login_form.html 생성<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
 &lt;form th:action=&quot;@{/user/login}&quot; method=&quot;post&quot;&gt;
    &lt;div th:if=&quot;${param.error}&quot;&gt;
       &lt;div class=&quot;alert alert-danger&quot;&gt;
          사용자ID 또는 비밀번호를 확인해 주세요.
       &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;username&quot; class=&quot;form-label&quot;&gt;사용자ID&lt;/label&gt;
       &lt;input type=&quot;text&quot; name=&quot;username&quot; id=&quot;username&quot; class=&quot;form-control&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;password&quot; class=&quot;form-label&quot;&gt;비밀번호&lt;/label&gt;
       &lt;input type=&quot;password&quot; name=&quot;password&quot; id=&quot;password&quot; class=&quot;form-control&quot;&gt;
    &lt;/div&gt;
    &lt;button type=&quot;submit&quot; class=&quot;btn btn-primary&quot;&gt;로그인&lt;/button&gt;
 &lt;/form&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre>
</li>
<li><a href="http://localhost:8080/user/login?error">http://localhost:8080/user/login?error</a>
<img src="https://velog.velcdn.com/images/hi_do/post/226fc38a-3a54-4d7d-b17e-5c389700034f/image.png" alt="">.</li>
<li><blockquote>
<p>로그인 실패시 매개변수로 error가 전달되는 것은 스프링 시큐리티의 규칙이다.</p>
</blockquote>
</li>
<li>login_form.html 파일에서<pre><code class="language-html">&lt;div th:if=&quot;${param.error}&quot;&gt;
       &lt;div class=&quot;alert alert-danger&quot;&gt;
          사용자ID 또는 비밀번호를 확인해 주세요.
       &lt;/div&gt;
    &lt;/div&gt;</code></pre>
의 ${param.error}로 error 매개변수가 전달된다.</li>
<li><blockquote>
<p>스프링 시큐리티에 무엇을 기준으로 로그인해야 하는지 아직 설정하지 않았기 때문,
DB에서 사용자를 조회하는 서비스(UserSecurityService)를 만들고, 그 서비스를 스프링 시큐리티에 등록하는 방법</p>
</blockquote>
</li>
<li>UserRepository.java 수정<pre><code class="language-java">package com.mysite.sbb.user;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository&lt;SiteUser, Long&gt;{
  Optional&lt;SiteUser&gt; findByUsename(String username);
}</code></pre>
</li>
<li>UserRole.java (enum) 생성<pre><code class="language-java">package com.mysite.sbb.user;
import lombok.Getter;
@Getter
public enum UserRole {
  ADMIN(&quot;ROLE_ADMIN&quot;), USER(&quot;ROLE_USER&quot;);    
  UserRole(String value) {
      this.value=value;
  }    
  private String value;
}</code></pre>
</li>
<li><blockquote>
<p>스프링 시큐리티는 인증뿐만 아니라 권한도 관리한다.
스프링 시큐리티는 사용자 인증 후에 사용자에게 부여할 권한과 관련된 처리가 필요하다
우리는 사용자가 로그인한 후, ADMIN인지 USER인지와 같은 권한을 부여해야 한다.</p>
</blockquote>
</li>
<li>UserSecurityService.java 생성<pre><code class="language-java">package com.mysite.sbb.user;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {
 private final UserRepository userRepository;
// 시큐리티 session(내부 Authentication(내부 UserDetails))
 @Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Optional&lt;SiteUser&gt; _siteUser = this.userRepository.findByUsername(username);
    if (_siteUser.isEmpty()) {
       throw new UsernameNotFoundException(&quot;사용자를 찾을수 없습니다.&quot;);
    }
    SiteUser siteUser = _siteUser.get();
    // 사용자의 권한 정보
    List&lt;GrantedAuthority&gt; authorities = new ArrayList&lt;&gt;();
    if (&quot;admin&quot;.equals(username)) {
       authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
    } else {
       authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
    }
    // User 객체 생성, 이 객체는 스프링 시큐리티에서 사용하며 User 생성자에는 id, pw, 권한 리스트가 전달된다
    return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
 }
}</code></pre>
</li>
<li>SecurityConfig.java 수정<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity   // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 된다.
public class SecurityConfig {
 @Bean
 SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    http.authorizeHttpRequests((authorizeHttpRequests) -&gt;
    authorizeHttpRequests.requestMatchers(new AntPathRequestMatcher(&quot;/**&quot;)).permitAll())
    .csrf(csrf -&gt; csrf   // /h2-console/로 시작하는 모든 URL은 CSRF 검증을 하지 않겠다.
          .ignoringRequestMatchers(new AntPathRequestMatcher(&quot;/h2-console/**&quot;)))
    .headers(headers -&gt; headers
          .addHeaderWriter(new XFrameOptionsHeaderWriter(
             XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
    .formLogin(formLogin -&gt; formLogin
            .loginPage(&quot;/user/login&quot;)
            .defaultSuccessUrl(&quot;/&quot;));  
    return http.build();
 }   
 @Bean   // Ioc(Inversion of Control)로 등록.
 PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
 }  
 @Bean    //스프링 시큐리티의 인증을 처리, UserSecurityService와 PasswordEncoder를 내부적으로 사용하여
 // 인증과 권한을 부여 처리한다.
 AuthenticationManager authenticationManager(AuthenticationConfiguration
         authenticationConfiguration) throws Exception{
     return authenticationConfiguration.getAuthenticationManager();
 }
}</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/58718794-6af4-4388-9724-fdbb532e3e91/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/1e58e43c-6485-4ee7-9a45-c38ce22024ac/image.png" alt=""></li>
<li>SecurityConfig.java 수정<pre><code class="language-java">  package com.mysite.sbb;
  import org.springframework.context.annotation.Bean;
  import org.springframework.context.annotation.Configuration;
  import org.springframework.security.authentication.AuthenticationManager;
  import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
  import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  import org.springframework.security.crypto.password.PasswordEncoder;
  import org.springframework.security.web.SecurityFilterChain;
  import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
  import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
  @Configuration
  @EnableWebSecurity   // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 된다.
  public class SecurityConfig {    
     @Bean
     SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http.authorizeHttpRequests((authorizeHttpRequests) -&gt;
        authorizeHttpRequests.requestMatchers(new AntPathRequestMatcher(&quot;/**&quot;)).permitAll())
        .csrf(csrf -&gt; csrf   // /h2-console/로 시작하는 모든 URL은 CSRF 검증을 하지 않겠다.
              .ignoringRequestMatchers(new AntPathRequestMatcher(&quot;/h2-console/**&quot;)))
        .headers(headers -&gt; headers
              .addHeaderWriter(new XFrameOptionsHeaderWriter(
                 XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
        .formLogin(formLogin -&gt; formLogin
                .loginPage(&quot;/user/login&quot;)
                .defaultSuccessUrl(&quot;/&quot;))
        .logout(logout -&gt; logout
                .logoutRequestMatcher(new AntPathRequestMatcher(&quot;/user/logout&quot;))    //로그아웃 url
                .logoutSuccessUrl(&quot;/&quot;)
                .invalidateHttpSession(true));    // 로그아웃하면 생성된 사용자의 세션도 삭제하도록 해라      
        return http.build();
     }       
     @Bean   // Ioc(Inversion of Control)로 등록.
     PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
     }       
     @Bean    //스프링 시큐리티의 인증을 처리, UserSecurityService와 PasswordEncoder를 내부적으로 사용하여
     // 인증과 권한을 부여 처리한다.
     AuthenticationManager authenticationManager(AuthenticationConfiguration
             authenticationConfiguration) throws Exception{
         return authenticationConfiguration.getAuthenticationManager();
     }
  }</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/4f17b855-5dd2-40fc-b412-391a3acd76e9/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/e679ed95-2466-4f53-9050-9e9f6e1d6e92/image.png" alt=""></li>
</ul>
<h4 id=""></h4>
<blockquote>
</blockquote>
<ul>
<li>질문 또는 답변을 작성할 때 사용자 정보도 DB에 함께 저장되어야 한다.
질문과 답변을 작성할 때 사용자는 반드시 로그인 되어 있어야 한다.
그래야 누가 작성한 글인지 알 수 있고, 수정 및 삭제도 가능하기 때문이다.</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>Question.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.time.LocalDateTime;
import java.util.List;
import com.mysite.sbb.answer.Answer;
import com.mysite.sbb.user.SiteUser;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class Question {
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Integer id;
 @Column(length = 200)
 private String subject;
 @Column(columnDefinition = &quot;TEXT&quot;)
 private String content;
 private LocalDateTime createDate;   
 @OneToMany(mappedBy = &quot;question&quot;, cascade = CascadeType.REMOVE)
 private List&lt;Answer&gt; answerList;   // question.getAnswerList()   
 @ManyToOne
 private SiteUser author;
}</code></pre>
</li>
<li>Answer.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import java.time.LocalDateTime;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class Answer {
 @Id  // 기본키. 각 데이터들을 구분하는 유효한 값.(중복 불가능)
 @GeneratedValue(strategy = GenerationType.IDENTITY)// 고유한 번호를 생성하는 방법.
 private Integer id;   
 // 열 이름 텍스트를 열 데이터로 넣을수 있다. 글자수를 제한할수 없는 경우에 사용.
 @Column(columnDefinition =&quot;TEXT&quot;)
 private String content;   
 private LocalDateTime createDate;    
 // Question를 참조해야 하기 위해.
 @ManyToOne
 private Question question;   
 @ManyToOne
 private SiteUser author;
}</code></pre>
</li>
<li>AnswerController.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import java.security.Principal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/answer&quot;)
@RequiredArgsConstructor
@Controller
public class AnswerController {
 private final QuestionService questionService;
 private final AnswerService answerService;   
 @PostMapping(&quot;/create/{id}&quot;)
 public String createAnswer(Model model, @PathVariable(&quot;id&quot;) Integer id, 
       @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
    Question question = this.questionService.getQuestion(id);
    if(bindingResult.hasErrors()) {
        model.addAttribute(&quot;question&quot;, question);
        return &quot;question_detail&quot;;
    }
    // TODO: 답변을 저장한다.
    this.answerService.create(question, answerForm.getContent());
    return String.format(&quot;redirect:/question/detail/%s&quot;, id);
 }
}</code></pre>
<pre><code class="language-java">public String createAnswer(Model model, @PathVariable(&quot;id&quot;) Integer id, 
  @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal)</code></pre>
</li>
<li><blockquote>
<p>Principal principal
현재 로그인한 사용자의 정보를 알려면 시큐리티가 제공하는 Principal 객체를 사용해야 한다
principal.getUsername()</p>
</blockquote>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>UserService.java 수정<pre><code class="language-java">package com.mysite.sbb.user;
import java.util.Optional;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.mysite.sbb.DataNotFoundException;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class UserService { 
 private final UserRepository userRepository;
 private final PasswordEncoder passwordEncoder; 
 public SiteUser create(String username, String email, String password) {
    SiteUser user = new SiteUser();
    user.setUsername(username);
    user.setEmail(email);
//      스프링시큐리티에서 BCryptPaswerdEncoder를 사용. 
    // 해시함수를 사용하는데 비밀번호 같은 보안 정보를 안전하게 저장하고 검증할때 사용하는 암호화 기술.
    user.setPassword(passwordEncoder.encode(password));
    this.userRepository.save(user);
    return user;
 }   
 public SiteUser getUser(String username) {
    Optional&lt;SiteUser&gt; siteUser = this.userRepository.findByUsername(username);
    if(siteUser.isPresent()) {
       return siteUser.get();               
    }else {
       throw new DataNotFoundException(&quot;siteuser not found&quot;);
    }
 }
}</code></pre>
</li>
<li>AnswerService.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import java.time.LocalDateTime;
import org.springframework.stereotype.Service;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class AnswerService {    
  private final AnswerRepository answerRepository;
  public void create(Question question, String content, SiteUser author) {
      Answer answer = new Answer();
      answer.setContent(content);
      answer.setCreateDate(LocalDateTime.now());
      answer.setQuestion(question);
      answer.setAuthor(author);
      this.answerRepository.save(answer);
  }
}</code></pre>
</li>
<li>AnswerController.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import java.security.Principal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/answer&quot;)
@RequiredArgsConstructor
@Controller
public class AnswerController {
 private final QuestionService questionService;
 private final AnswerService answerService;
 private final UserService userService;  
 @PostMapping(&quot;/create/{id}&quot;)
 public String createAnswer(Model model, @PathVariable(&quot;id&quot;) Integer id, 
       @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
    Question question = this.questionService.getQuestion(id);
    SiteUser siteUser = this.userService.getUser(principal.getName());
    if(bindingResult.hasErrors()) {
        model.addAttribute(&quot;question&quot;, question);
        return &quot;question_detail&quot;;
    }
    // TODO: 답변을 저장한다.
    this.answerService.create(question, answerForm.getContent(), siteUser);
    return String.format(&quot;redirect:/question/detail/%s&quot;, id);
 }
}</code></pre>
</li>
<li>QuestionService.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
import com.mysite.sbb.DataNotFoundException;
import com.mysite.sbb.user.SiteUser;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class QuestionService {
 private final QuestionRepository questionRepository; 
 public List&lt;Question&gt; getList(){
    return this.questionRepository.findAll();
 }   
 public Question getQuestion(Integer id) {
    Optional&lt;Question&gt; question = this.questionRepository.findById(id);
    if(question.isPresent()) {
       return question.get();
    }else {
       throw new DataNotFoundException(&quot;question not found&quot;);
    }
 }
 public void create(String subject, String content, SiteUser user) {
    Question q = new Question();
    q.setSubject(subject);
    q.setContent(content);
    q.setCreateDate(LocalDateTime.now());
    q.setAuthor(user);
    this.questionRepository.save(q);
 }
}</code></pre>
</li>
<li>QuestionController.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.security.Principal;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.mysite.sbb.answer.AnswerForm;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/question&quot;)
@RequiredArgsConstructor
@Controller
public class QuestionController {   
 private final QuestionService questionService;
 private final UserService userService;   
 @GetMapping(&quot;/list&quot;)
 public String list(Model model) {
    List&lt;Question&gt; questionList = this.questionService.getList();
    model.addAttribute(&quot;questionList&quot;, questionList);
    return &quot;question_list&quot;;
 }   
 @GetMapping(value = &quot;/detail/{id}&quot;)
 public String detail(Model model, @PathVariable(&quot;id&quot;) Integer id, AnswerForm answerForm) {
    Question question = this.questionService.getQuestion(id);
    model.addAttribute(&quot;question&quot;,question);
    return &quot;question_detail&quot;;
 }   
 @GetMapping(&quot;/create&quot;)
 public String questionCreate(QuestionForm questionForm) {
    return &quot;question_form&quot;;
 }
 @PostMapping(&quot;/create&quot;)
 public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult, Principal principal ) {
    if(bindingResult.hasErrors()) {
       return &quot;question_form&quot;;
    }
    SiteUser siteUser = this.userService.getUser(principal.getName());
    this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
    return &quot;redirect:/question/list&quot;;   // 질문 저장후 질문 목록으로 이동
 }
}</code></pre>
</li>
<li>SbbAplicationTests.java 수정<pre><code class="language-java">package com.mysite.sbb;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.mysite.sbb.question.QuestionService;
@SpringBootTest
class SbbApplicationTests {
  @Autowired
  private QuestionService questionService;
  @Test
  void testJpa() {
      for (int i = 1; i &lt;= 300; i++) {
          String subject = String.format(&quot;테스트 데이터입니다:[%03d]&quot;, i);
          String content = &quot;내용무&quot;;
          this.questionService.create(subject, content, null);
      }
  }
}</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/9ccd46a8-7e8b-47cb-b4e7-e7185714afbd/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/c39d7af4-c8b5-4337-ab69-bfd0ae7f15cf/image.png" alt=""><h4 id="로그아웃-상태에서-답변을-달-경우">로그아웃 상태에서 답변을 달 경우</h4>
<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/hi_do/post/684aabe1-d999-4d9b-a57f-26dcf08e8af8/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/4eac65de-77a2-4275-9318-472175082536/image.png" alt=""></li>
<li>because &quot;principal&quot; is null 오류
principal 객체는 로그인을 해야만 생성되는 객체인데 현재는 로그아웃 상태이므로 principal 객체에 값이 없어 오류가 발생</li>
<li><blockquote>
<p>해결하려면Authenticated: 인증됨
@PreAuthorize(&quot;isAuthenticated()&quot;) 로그인한 경우에만 실행한다.
로그인한 사용자만 호출할 수 있다.</p>
</blockquote>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>QuestionController.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.security.Principal;
import java.util.List;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.mysite.sbb.answer.AnswerForm;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/question&quot;)
@RequiredArgsConstructor
@Controller
public class QuestionController {   
 private final QuestionService questionService;
 private final UserService userService;
 @GetMapping(&quot;/list&quot;)
 public String list(Model model) {
    List&lt;Question&gt; questionList = this.questionService.getList();
    model.addAttribute(&quot;questionList&quot;, questionList);
    return &quot;question_list&quot;;
 }   
 @GetMapping(value = &quot;/detail/{id}&quot;)
 public String detail(Model model, @PathVariable(&quot;id&quot;) Integer id, AnswerForm answerForm) {
    Question question = this.questionService.getQuestion(id);
    model.addAttribute(&quot;question&quot;,question);
    return &quot;question_detail&quot;;
 }
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @GetMapping(&quot;/create&quot;)
 public String questionCreate(QuestionForm questionForm) {
    return &quot;question_form&quot;;
 }  
 @PreAuthorize(&quot;isAuthenticated()&quot;)// 로그아웃상태에서 호출하면 로그인페이지로 강제 이동.
 @PostMapping(&quot;/create&quot;)
 public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult, Principal principal ) {
    if(bindingResult.hasErrors()) {
       return &quot;question_form&quot;;
    }
    SiteUser siteUser = this.userService.getUser(principal.getName());
    this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
    return &quot;redirect:/question/list&quot;;   // 질문 저장후 질문 목록으로 이동
 }
}</code></pre>
</li>
<li>AnswerController.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import java.security.Principal;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/answer&quot;)
@RequiredArgsConstructor
@Controller
public class AnswerController {
 private final QuestionService questionService;
 private final AnswerService answerService;
 private final UserService userService;   
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @PostMapping(&quot;/create/{id}&quot;)
 public String createAnswer(Model model, @PathVariable(&quot;id&quot;) Integer id, 
       @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
    Question question = this.questionService.getQuestion(id);
    SiteUser siteUser = this.userService.getUser(principal.getName());
    if(bindingResult.hasErrors()) {
        model.addAttribute(&quot;question&quot;, question);
        return &quot;question_detail&quot;;
    }
    // TODO: 답변을 저장한다.
    this.answerService.create(question, answerForm.getContent(), siteUser);
    return String.format(&quot;redirect:/question/detail/%s&quot;, id);
 }
}</code></pre>
</li>
<li>SecurityConfig 수정<pre><code class="language-java">  package com.mysite.sbb;    
  import org.springframework.context.annotation.Bean;
  import org.springframework.context.annotation.Configuration;
  import org.springframework.security.authentication.AuthenticationManager;
  import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  import org.springframework.security.crypto.password.PasswordEncoder;
  import org.springframework.security.web.SecurityFilterChain;
  import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
  import org.springframework.security.web.util.matcher.AntPathRequestMatcher;    
  @Configuration
  @EnableWebSecurity      // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 된다.
  @EnableMethodSecurity(prePostEnabled = true)
  public class SecurityConfig {    
     @Bean
     SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http.authorizeHttpRequests((authorizeHttpRequests) -&gt;
        authorizeHttpRequests.requestMatchers(new AntPathRequestMatcher(&quot;/**&quot;)).permitAll())
        .csrf(csrf -&gt; csrf   // /h2-console/로 시작하는 모든 URL은 CSRF 검증을 하지 않겠다.
              .ignoringRequestMatchers(new AntPathRequestMatcher(&quot;/h2-console/**&quot;)))
        .headers(headers -&gt; headers
              .addHeaderWriter(new XFrameOptionsHeaderWriter(
                 XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
        .formLogin(formLogin -&gt; formLogin
                .loginPage(&quot;/user/login&quot;)
                .defaultSuccessUrl(&quot;/&quot;))
        .logout(logout -&gt; logout
                .logoutRequestMatcher(new AntPathRequestMatcher(&quot;/user/logout&quot;))    //로그아웃 url
                .logoutSuccessUrl(&quot;/&quot;)
                .invalidateHttpSession(true));    // 로그아웃하면 생성된 사용자의 세션도 삭제하도록 해라          
        return http.build();
     }       
     @Bean   // Ioc(Inversion of Control)로 등록.
     PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
     }       
     @Bean    //스프링 시큐리티의 인증을 처리, UserSecurityService와 PasswordEncoder를 내부적으로 사용하여
     // 인증과 권한을 부여 처리한다.
     AuthenticationManager authenticationManager(AuthenticationConfiguration
             authenticationConfiguration) throws Exception{
         return authenticationConfiguration.getAuthenticationManager();
     }    
  }</code></pre>
</li>
<li><blockquote>
<p>@EnableMethodSecurity(prePostEnabled = true)
시큐리티의 보안기능을 활성화하고 @PreAuthorieze, @PostAuthorize을 사용할 수 있게 한다.</p>
</blockquote>
</li>
<li><blockquote>
<p>질문 등록페이지에서는 사용자가 로그아웃 상태라면 아예 글을 작성할 수 없다.
하지만 답변 등록 페이지에서는 로그아웃 상태에서도 글을 작성할 수 있어서 답변을 작성한 후 등록버튼으 누르면 로그인 화면으로 이동된다.
이렇게 되면 문제가 사용자가 작성한 답변이 사라지는 경우가 있다.
이 문제를 해결하기 위한 방법으로 사용자가 로그아웃 상태인 경우 아예 답변 작성을 못하도록 막는 것으로 처리한다.</p>
</blockquote>
</li>
</ul>
<p>-&gt;</p>
<blockquote>
</blockquote>
<ul>
<li>question_detail.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
 &lt;!-- 질문--&gt;
 &lt;h2 class=&quot;border-bottom py-2&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/h2&gt; 
 &lt;div class=&quot;card my-3&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
              &lt;div th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 개수 표시 --&gt;
 &lt;h5 class=&quot;border-bottom my-3 py-2&quot; th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
 &lt;!-- 답변 반복 시작 --&gt;
 &lt;div class=&quot;card my-3&quot; th:each=&quot;answer: ${question.answerList}&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space:pre-line;&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt; 
             &lt;div th:text=&quot;${#temporals.format(answer.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;      
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 반복 끝 --&gt;
 &lt;!-- 답변 작성--&gt;
 &lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; th:object=&quot;${answerForm}&quot; method=&quot;post&quot; class=&quot;my-3&quot;&gt;
    &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;
    &lt;textarea sec:authorize=&quot;isAnonymous()&quot; disabled th:field=&quot;*{content}&quot; 
             class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
       &lt;textarea sec:authorize=&quot;isAuthenticated()&quot; th:field=&quot;*{content}&quot; 
          class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;            
       &lt;input type=&quot;submit&quot; value=&quot;답변 등록&quot; class=&quot;btn btn-primary my-2&quot;&gt; &lt;/form&gt;
 &lt;/div&gt;
 &lt;/html&gt;</code></pre>
<pre><code class="language-html">    &lt;textarea sec:authorize=&quot;isAnonymous()&quot; disabled th:field=&quot;*{content}&quot; 
             class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
       &lt;textarea sec:authorize=&quot;isAuthenticated()&quot; th:field=&quot;*{content}&quot; 
          class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;</code></pre>
<img src="https://velog.velcdn.com/images/hi_do/post/1eaffc8a-ffac-400f-8d25-ca64123b0413/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/32d1ef23-5d81-4083-a45a-2b56b4d3e9e2/image.png" alt=""></li>
<li><blockquote>
<p>로그인 상태가 아닌 경우 textarea 태그에 disable 속성을 넣어 사용자가 화면에서 아예 입력하지 못하게 만든다.
sec:authorize=&quot;isAnonymous()&quot; : 로그아웃 상태
sec:authorize=&quot;isAuthenticated()&quot; : 로그인 상태</p>
</blockquote>
</li>
<li>question_list.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
&lt;table class=&quot;table&quot;&gt;
 &lt;thead class=&quot;table-dark&quot;&gt;
    &lt;tr class=&quot;text-center&quot;&gt;
             &lt;th&gt;번호&lt;/th&gt;
             &lt;th style=&quot;width: 50%&quot;&gt;제목&lt;/th&gt;
             &lt;th&gt;글쓴이&lt;/th&gt;
             &lt;th&gt;작성일시&lt;/th&gt;
          &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
    &lt;tr th:each=&quot;question, loop : ${questionList}&quot;&gt;
       &lt;td th:text=&quot;${loop.count}&quot;&gt;&lt;/td&gt;
       &lt;td class=&quot;text-start&quot;&gt;
                   &lt;a th:href=&quot;@{|/question/detail/${question.id}|}&quot;
                   th:text=&quot;${question.subject}&quot;&gt;&lt;/a&gt;
                &lt;/td&gt; 
       &lt;td&gt;&lt;span th:if=&quot;${question.author !=null}&quot; 
          th:text=&quot;${question.author.username}&quot;&gt;&lt;/span&gt;&lt;/td&gt;
       &lt;td th:text=&quot;${#temporals.format(question.createDate,&#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/td&gt;
    &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;a th:href=&quot;@{/question/create}&quot; class = &quot;btn btn-primary&quot;&gt;질문 등록하기&lt;/a&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre>
</li>
<li>question_detail.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
 &lt;!-- 질문--&gt;
 &lt;h2 class=&quot;border-bottom py-2&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/h2&gt; 
 &lt;div class=&quot;card my-3&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
             &lt;div class=&quot;mb-2&quot;&gt;
                               &lt;span th:if=&quot;${question.author != null}&quot;
                               th:text =&quot;${question.author.username}&quot;&gt;&lt;/span&gt;
                            &lt;/div&gt;
              &lt;div th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 개수 표시 --&gt;
 &lt;h5 class=&quot;border-bottom my-3 py-2&quot; th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
 &lt;!-- 답변 반복 시작 --&gt;
 &lt;div class=&quot;card my-3&quot; th:each=&quot;answer: ${question.answerList}&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space:pre-line;&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt; 
             &lt;div class=&quot;mb-2&quot;&gt;
                               &lt;span th:if=&quot;${answer.author != null}&quot;
                               th:text =&quot;${answer.author.username}&quot;&gt;&lt;/span&gt;
                            &lt;/div&gt;
             &lt;div th:text=&quot;${#temporals.format(answer.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;      
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 반복 끝 --&gt;
 &lt;!-- 답변 작성--&gt;
 &lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; th:object=&quot;${answerForm}&quot; method=&quot;post&quot; class=&quot;my-3&quot;&gt;
    &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;      
    &lt;textarea sec:authorize=&quot;isAnonymous()&quot; disabled th:field=&quot;*{content}&quot; 
             class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
       &lt;textarea sec:authorize=&quot;isAuthenticated()&quot; th:field=&quot;*{content}&quot; 
          class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;           
       &lt;input type=&quot;submit&quot; value=&quot;답변 등록&quot; class=&quot;btn btn-primary my-2&quot;&gt; &lt;/form&gt;
 &lt;/div&gt;
 &lt;/html&gt;</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/7bc788fa-794c-4978-ab3e-00eaa6ce8f40/image.png" alt=""></li>
<li><blockquote>
<p>작성자가 표시된다</p>
</blockquote>
</li>
</ul>
<h4 id="질문-또는-답변을-작성한-후-글들을-수정하거나-삭제할-수-있어야-한다">질문 또는 답변을 작성한 후 글들을 수정하거나 삭제할 수 있어야 한다</h4>
<blockquote>
</blockquote>
<ul>
<li>Question.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.time.LocalDateTime;
import java.util.List;
import com.mysite.sbb.answer.Answer;
import com.mysite.sbb.user.SiteUser;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class Question {
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Integer id;
 @Column(length = 200)
 private String subject;
 @Column(columnDefinition = &quot;TEXT&quot;)
 private String content;
 private LocalDateTime createDate;  
 @OneToMany(mappedBy = &quot;question&quot;, cascade = CascadeType.REMOVE)
 private List&lt;Answer&gt; answerList;   // question.getAnswerList()   
 @ManyToOne
 private SiteUser author;  
 private LocalDateTime modifyDate;
}</code></pre>
</li>
<li>Answer.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import java.time.LocalDateTime;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class Answer {
 @Id  // 기본키. 각 데이터들을 구분하는 유효한 값.(중복 불가능)
 @GeneratedValue(strategy = GenerationType.IDENTITY)// 고유한 번호를 생성하는 방법.
 private Integer id;   
 // 열 이름 텍스트를 열 데이터로 넣을수 있다. 글자수를 제한할수 없는 경우에 사용.
 @Column(columnDefinition =&quot;TEXT&quot;)
 private String content;   
 private LocalDateTime createDate;     
 // Question를 참조해야 하기 위해.
 @ManyToOne
 private Question question;   
 @ManyToOne
 private SiteUser author;   
 private LocalDateTime modifyDate;
}</code></pre>
</li>
<li>question_detail.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
 &lt;!-- 질문--&gt;
 &lt;h2 class=&quot;border-bottom py-2&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/h2&gt; 
 &lt;div class=&quot;card my-3&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
             &lt;div class=&quot;mb-2&quot;&gt;
                               &lt;span th:if=&quot;${question.author != null}&quot;
                               th:text =&quot;${question.author.username}&quot;&gt;&lt;/span&gt;
                            &lt;/div&gt;
              &lt;div th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
       &lt;div class=&quot;my-3&quot;&gt;                           
          &lt;a th:href=&quot;@{|/question/modify/${question.id}|}&quot; class=&quot;btn btn-sm btn-outline-secondary&quot;
                sec:authorize=&quot;isAuthenticated()&quot; 
                th:if=&quot;${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}&quot; th:text=&quot;수정&quot;&gt;&lt;/a&gt;
                                     &lt;/div&gt;
                &lt;/div&gt;    
 &lt;!-- 답변 개수 표시 --&gt;
 &lt;h5 class=&quot;border-bottom my-3 py-2&quot; th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
 &lt;!-- 답변 반복 시작 --&gt;
 &lt;div class=&quot;card my-3&quot; th:each=&quot;answer: ${question.answerList}&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space:pre-line;&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt; 
             &lt;div class=&quot;mb-2&quot;&gt;
                               &lt;span th:if=&quot;${answer.author != null}&quot;
                               th:text =&quot;${answer.author.username}&quot;&gt;&lt;/span&gt;
                            &lt;/div&gt;
             &lt;div th:text=&quot;${#temporals.format(answer.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;      
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 반복 끝 --&gt;
 &lt;!-- 답변 작성--&gt;
 &lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; th:object=&quot;${answerForm}&quot; method=&quot;post&quot; class=&quot;my-3&quot;&gt;
    &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;    
    &lt;textarea sec:authorize=&quot;isAnonymous()&quot; disabled th:field=&quot;*{content}&quot; 
             class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
       &lt;textarea sec:authorize=&quot;isAuthenticated()&quot; th:field=&quot;*{content}&quot; 
          class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;            
       &lt;input type=&quot;submit&quot; value=&quot;답변 등록&quot; class=&quot;btn btn-primary my-2&quot;&gt; &lt;/form&gt;
 &lt;/div&gt;
 &lt;/html&gt;</code></pre>
<img src="https://velog.velcdn.com/images/hi_do/post/24e24840-c442-4deb-b4f0-9cbebbf4b83d/image.png" alt=""></li>
<li>#authentication.getPrincipal().getUsername() == question.author.username
#authentication.getPrincipal()은 타임리프에서 스프링 시큐리티와 함께 사용하는 표현식으로 현재 사용자가 인증되었다면 사용자 이름(id)을 알 수 있다. 로그인한 사용자와 글쓴이가 같다면 수정버튼이 보여라.</li>
<li>QuestionController.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.security.Principal;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;
import com.mysite.sbb.answer.AnswerForm;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/question&quot;)
@RequiredArgsConstructor
@Controller
public class QuestionController {   
 private final QuestionService questionService;
 private final UserService userService;  
 @GetMapping(&quot;/list&quot;)
 public String list(Model model) {
    List&lt;Question&gt; questionList = this.questionService.getList();
    model.addAttribute(&quot;questionList&quot;, questionList);
    return &quot;question_list&quot;;
 }  
 @GetMapping(value = &quot;/detail/{id}&quot;)
 public String detail(Model model, @PathVariable(&quot;id&quot;) Integer id, AnswerForm answerForm) {
    Question question = this.questionService.getQuestion(id);
    model.addAttribute(&quot;question&quot;,question);
    return &quot;question_detail&quot;;
 }   
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @GetMapping(&quot;/create&quot;)
 public String questionCreate(QuestionForm questionForm) {
    return &quot;question_form&quot;;
 }   
 @PreAuthorize(&quot;isAuthenticated()&quot;)// 로그아웃상태에서 호출하면 로그인페이지로 강제 이동.
 @PostMapping(&quot;/create&quot;)
 public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult, Principal principal ) {
    if(bindingResult.hasErrors()) {
       return &quot;question_form&quot;;
    }
    SiteUser siteUser = this.userService.getUser(principal.getName());
    this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
    return &quot;redirect:/question/list&quot;;   // 질문 저장후 질문 목록으로 이동
 }  
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @GetMapping(&quot;/modify/{id}&quot;)
 public String questionModify(QuestionForm questionForm, @PathVariable(&quot;id&quot;) Integer id, 
       Principal principal) {
    Question question = this.questionService.getQuestion(id);
    if (!question.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, &quot;수정권한이 없습니다.&quot;);
    }
    questionForm.setSubject(question.getSubject());
    questionForm.setContent(question.getContent());
    return &quot;question_form&quot;;
 }
}</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/fd68398d-08ce-4de8-a93e-30bd89eb5386/image.png" alt=""></li>
<li><blockquote>
<p>수정 후 시간 변화
<img src="https://velog.velcdn.com/images/hi_do/post/8f368b9a-8e61-469c-b3e0-ecd9af5168bc/image.png" alt=""></p>
</blockquote>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>question_form.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container&quot;&gt;
 &lt;h5 class=&quot;my-3 border-bottom pb-2&quot;&gt;질문등록&lt;/h5&gt;
 &lt;form th:object=&quot;${questionForm}&quot; method=&quot;post&quot;&gt;
       &lt;input type=&quot;hidden&quot; th:name=&quot;${_csrf.parameterName}&quot; th:value=&quot;${_csrf.token}&quot; /&gt;
       &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;alert alert-danger&quot; role=&quot;alert&quot; th:if=&quot;${#fields.hasAnyErrors()}&quot;&gt;
       &lt;div th:each=&quot;err : ${#fields.allErrors()}&quot; th:text=&quot;${err}&quot; /&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;subject&quot; class=&quot;form-label&quot;&gt;제목&lt;/label&gt;
       &lt;input type=&quot;text&quot; th:field=&quot;*{subject}&quot; id=&quot;subject&quot; class=&quot;form-control&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;content&quot; class=&quot;form-label&quot;&gt;내용&lt;/label&gt;
       &lt;textarea th:field=&quot;*{content}&quot; id=&quot;content&quot; class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
    &lt;/div&gt;
    &lt;input type=&quot;submit&quot; value=&quot;저장하기&quot; class=&quot;btn btn-primary my-2&quot;&gt;
 &lt;/form&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre>
<pre><code class="language-html">&lt;form th:object=&quot;${questionForm}&quot; method=&quot;post&quot;&gt;
 &lt;input type=&quot;hidden&quot; th:name=&quot;${_csrf.parameterName}&quot; th:value=&quot;${_csrf.token}&quot; /&gt;</code></pre>
</li>
<li><blockquote>
<p>기존에 있던 [form]의 action 속성을 삭제했다. action을 삭제하면 CSRF값이 자동으로 생성되지 않아서 CSRF값을 설정하기 위해 hidden 형태로 input 요소를 같이 추가한다
== CSRF값을 수동으로 추가해야 하는 이유는 스프링 시큐리티를 사용할때 CSRF 값이 반드시 필요하기 때문이다.</p>
</blockquote>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>QuestionService.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
import com.mysite.sbb.DataNotFoundException;
import com.mysite.sbb.user.SiteUser;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class QuestionService {
 private final QuestionRepository questionRepository; 
 public List&lt;Question&gt; getList(){
    return this.questionRepository.findAll();
 }   
 public Question getQuestion(Integer id) {
    Optional&lt;Question&gt; question = this.questionRepository.findById(id);
    if(question.isPresent()) {
       return question.get();
    }else {
       throw new DataNotFoundException(&quot;question not found&quot;);
    }
 }
 public void create(String subject, String content, SiteUser user) {
    Question q = new Question();
    q.setSubject(subject);
    q.setContent(content);
    q.setCreateDate(LocalDateTime.now());
    q.setAuthor(user);
    this.questionRepository.save(q);
 }
 public void modify(Question question, String subject, String content) {
     question.setSubject(subject);
     question.setContent(content);
     question.setModifyDate(LocalDateTime.now());
     this.questionRepository.save(question);
 }
}</code></pre>
</li>
<li>QuestionController.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.security.Principal;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;
import com.mysite.sbb.answer.AnswerForm;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/question&quot;)
@RequiredArgsConstructor
@Controller
public class QuestionController {
 private final QuestionService questionService;
 private final UserService userService;   
 @GetMapping(&quot;/list&quot;)
 public String list(Model model) {
    List&lt;Question&gt; questionList = this.questionService.getList();
    model.addAttribute(&quot;questionList&quot;, questionList);
    return &quot;question_list&quot;;
 }   
 @GetMapping(value = &quot;/detail/{id}&quot;)
 public String detail(Model model, @PathVariable(&quot;id&quot;) Integer id, AnswerForm answerForm) {
    Question question = this.questionService.getQuestion(id);
    model.addAttribute(&quot;question&quot;,question);
    return &quot;question_detail&quot;;
 }   
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @GetMapping(&quot;/create&quot;)
 public String questionCreate(QuestionForm questionForm) {
    return &quot;question_form&quot;;
 }   
 @PreAuthorize(&quot;isAuthenticated()&quot;)// 로그아웃상태에서 호출하면 로그인페이지로 강제 이동.
 @PostMapping(&quot;/create&quot;)
 public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult, Principal principal ) {
    if(bindingResult.hasErrors()) {
       return &quot;question_form&quot;;
    }
    SiteUser siteUser = this.userService.getUser(principal.getName());
    this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
    return &quot;redirect:/question/list&quot;;   // 질문 저장후 질문 목록으로 이동
 }   
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @GetMapping(&quot;/modify/{id}&quot;)
 public String questionModify(QuestionForm questionForm, @PathVariable(&quot;id&quot;) Integer id, 
       Principal principal) {
    Question question = this.questionService.getQuestion(id);
    if (!question.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, &quot;수정권한이 없습니다.&quot;);
    }
    questionForm.setSubject(question.getSubject());
    questionForm.setContent(question.getContent());
    return &quot;question_form&quot;;
 }   
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @PostMapping(&quot;/modify/{id}&quot;)
 public String questionModify(@Valid QuestionForm questionForm, BindingResult bindingResult, Principal principal,
       @PathVariable(&quot;id&quot;) Integer id) {
    if (bindingResult.hasErrors()) {
       return &quot;question_form&quot;;
    }
    Question question = this.questionService.getQuestion(id);
    if (!question.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, &quot;수정권한이 없습니다.&quot;);
    }
    this.questionService.modify(question, questionForm.getSubject(), 
          questionForm.getContent());
    return String.format(&quot;redirect:/question/detail/%s&quot;, id);
 }
}</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/40ab0540-b8f8-47e6-a849-40fbb4f6abff/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/6c701cfa-0bae-4536-87c4-9eafff333863/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/832a944e-c47c-46f7-8972-1c45dc000b7e/image.png" alt=""></li>
</ul>
<h4 id="삭제버튼-추가">삭제버튼 추가</h4>
<blockquote>
</blockquote>
<ul>
<li>question_detail.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
 &lt;!-- 질문--&gt;
 &lt;h2 class=&quot;border-bottom py-2&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/h2&gt; 
 &lt;div class=&quot;card my-3&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
             &lt;div class=&quot;mb-2&quot;&gt;
                               &lt;span th:if=&quot;${question.author != null}&quot;
                               th:text =&quot;${question.author.username}&quot;&gt;&lt;/span&gt;
                            &lt;/div&gt;
              &lt;div th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
       &lt;div class=&quot;my-3&quot;&gt;                           
          &lt;a th:href=&quot;@{|/question/modify/${question.id}|}&quot; class=&quot;btn btn-sm btn-outline-secondary&quot;
                sec:authorize=&quot;isAuthenticated()&quot; 
                th:if=&quot;${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}&quot; th:text=&quot;수정&quot;&gt;&lt;/a&gt;
                &lt;a href=&quot;javascript:void(0);&quot; th:data-uri=&quot;@{|/question/delete/${question.id}|}&quot;
                               class=&quot;delete btn btn-sm btn-outline-secondary&quot; sec:authorize=&quot;isAuthenticated()&quot;
                               th:if=&quot;${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}&quot;
                               th:text=&quot;삭제&quot;&gt;&lt;/a&gt;     
                                &lt;/div&gt;
                &lt;/div&gt;      
 &lt;!-- 답변 개수 표시 --&gt;
 &lt;h5 class=&quot;border-bottom my-3 py-2&quot; th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
 &lt;!-- 답변 반복 시작 --&gt;
 &lt;div class=&quot;card my-3&quot; th:each=&quot;answer: ${question.answerList}&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space:pre-line;&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt; 
             &lt;div class=&quot;mb-2&quot;&gt;
                               &lt;span th:if=&quot;${answer.author != null}&quot;
                               th:text =&quot;${answer.author.username}&quot;&gt;&lt;/span&gt;
                            &lt;/div&gt;
             &lt;div th:text=&quot;${#temporals.format(answer.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;    
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 반복 끝 --&gt;
 &lt;!-- 답변 작성--&gt;
 &lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; th:object=&quot;${answerForm}&quot; method=&quot;post&quot; class=&quot;my-3&quot;&gt;
    &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;      
    &lt;textarea sec:authorize=&quot;isAnonymous()&quot; disabled th:field=&quot;*{content}&quot; 
             class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
       &lt;textarea sec:authorize=&quot;isAuthenticated()&quot; th:field=&quot;*{content}&quot; 
          class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
       &lt;input type=&quot;submit&quot; value=&quot;답변 등록&quot; class=&quot;btn btn-primary my-2&quot;&gt; &lt;/form&gt;
 &lt;/div&gt;
 &lt;/html&gt;</code></pre>
</li>
<li><blockquote>
<p>로그인한 사용자가 자신이 작성한 질문을 삭제할 수 있도록 [삭제]버튼을 클릭하면 자바스크립트 임시링크의 코드가 실행되도록 구현했다.
[삭제]버튼은 [수정]버튼과 달리 href속성값을 javascript.void(0)로 설정해서 삭제를 실행할 url을 얻기위해 th:data-uri 속성을 추가한 뒤, [삭제] 버튼을 클릭하는 이벤트를 확인하기 위해 class 속성에 delete항목을 추가한 것
href에 삭제를 위한 url을 직접 사용하지 않고 이러한 방식을 사용한 이유는 [삭제]버튼을 클릭했을때 &#39;삭제하시겠습니까?&#39;와 같은 메시지와 함께 별도의 호가인 절차를 중간에 끼워 넣기 위해서이다.
(data-uri: 속성에 설정한 값은 클릭 이벤트 발생 시 자바스크립트 코드에서 this.dataset.uri를 사용하여 그 값을 알아서 실행할 수 있다.)
삭제 작업을 수행하는데 실제 url을 data_uri에 저장된다.
사용자가 링크를 클릭하면 자바스크립트 함수가 이 data-uri 속성에서 url을 가져와 ajax 호출 또는 다른 형태의 요청을 통해 페이지를 새로 고치지 않고 질문을 삭제할 수 있다.</p>
</blockquote>
<pre><code class="language-html">&lt;script type=&#39;text/javascript&quot;&gt;
const delete_elements = document.getElementsByClassName(&quot;delete&quot;);
Array.from(delete_elements).forEach(function(element) { element.addEventListener(&#39;click&#39;, function() {
  if(confirm(&quot;정말로 삭제하시겠습니까?&quot;)) {
     location.href= this.dataset.uri;   // [확인]을 클릭했을 경우 삭제를 위한 url을 호출하기 위해 작성.
  };
    });
});
&lt;/script&gt;</code></pre>
<ul>
<li>layout.html 수정<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
&lt;!-- Required meta tags --&gt;
&lt;meta charset=&quot;utf-8&quot;&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1, shrink-to-fit=no&quot;&gt;
&lt;!-- Bootstrap CSS --&gt;
&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; th:href=&quot;@{/bootstrap.min.css}&quot;&gt;
&lt;!-- sbb CSS --&gt;
&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; th:href=&quot;@{/style.css}&quot;&gt;
&lt;title&gt;Hello, sbb!&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;nav th:fragment=&quot;navbarFragment&quot; class=&quot;navbar navbar-expand-lg navbar-light bg-light border-bottom&quot;&gt;
   &lt;div class=&quot;container-fluid&quot;&gt;
      &lt;a class=&quot;navbar-brand&quot; href=&quot;/&quot;&gt;SBB&lt;/a&gt;
      &lt;button class=&quot;navbar-toggler&quot; type=&quot;button&quot; data-bs-toggle=&quot;collapse&quot; data-bs-target=&quot;#navbarSupportedContent&quot;
         aria-controls=&quot;navbarSupportedContent&quot; aria-expanded=&quot;false&quot; aria-label=&quot;Toggle navigation&quot;&gt;
         &lt;span class=&quot;navbar-toggler-icon&quot;&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;div class=&quot;collapse navbar-collapse&quot; id=&quot;navbarSupportedContent&quot;&gt;
         &lt;ul class=&quot;navbar-nav me-auto mb-2 mb-lg-0&quot;&gt;
            &lt;li class=&quot;nav-item&quot;&gt;
               &lt;a class=&quot;nav-link&quot; sec:authorize=&quot;isAnonymous()&quot; th:href=&quot;@{/user/login}&quot;&gt;로그인&lt;/a&gt;
               &lt;a class=&quot;nav-link&quot; sec:authorize=&quot;isAuthenticated()&quot; th:href=&quot;@{/user/logout}&quot;&gt;로그아웃&lt;/a&gt;
            &lt;/li&gt;
            &lt;li class=&quot;nav-item&quot;&gt;
               &lt;a class=&quot;nav-link&quot; th:href=&quot;@{/user/signup}&quot;&gt;회원가입&lt;/a&gt;
            &lt;/li&gt;
         &lt;/ul&gt;
      &lt;/div&gt;
   &lt;/div&gt;
&lt;/nav&gt;
&lt;!-- 기본 템플릿 안에 삽입될 내용 Start --&gt;
&lt;th:block layout:fragment=&quot;content&quot;&gt;&lt;/th:block&gt;
&lt;!-- 기본 템플릿 안에 삽입될 내용 End --&gt;
&lt;!-- 자바스크립트 Start --&gt;
   &lt;th:block layout:fragment=&quot;script&quot;&gt;&lt;/th:block&gt;
   &lt;!-- 자바스크립트 End --&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
&lt;!-- 질문--&gt;
&lt;h2 class=&quot;border-bottom py-2&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/h2&gt; 
&lt;div class=&quot;card my-3&quot;&gt;
   &lt;div class=&quot;card-body&quot;&gt;
      &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
         &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
            &lt;div class=&quot;mb-2&quot;&gt;
                              &lt;span th:if=&quot;${question.author != null}&quot;
                              th:text =&quot;${question.author.username}&quot;&gt;&lt;/span&gt;
                           &lt;/div&gt;
             &lt;div th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
         &lt;/div&gt;
      &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;my-3&quot;&gt;                           
         &lt;a th:href=&quot;@{|/question/modify/${question.id}|}&quot; class=&quot;btn btn-sm btn-outline-secondary&quot;
               sec:authorize=&quot;isAuthenticated()&quot; 
               th:if=&quot;${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}&quot; th:text=&quot;수정&quot;&gt;&lt;/a&gt;
         &lt;a href=&quot;javascript:void(0);&quot; th:data-uri=&quot;@{|/question/delete/${question.id}|}&quot;
                              class=&quot;delete btn btn-sm btn-outline-secondary&quot; sec:authorize=&quot;isAuthenticated()&quot;
                              th:if=&quot;${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}&quot;
                              th:text=&quot;삭제&quot;&gt;&lt;/a&gt;   
                                    &lt;/div&gt;
               &lt;/div&gt;
&lt;!-- 답변 개수 표시 --&gt;
&lt;h5 class=&quot;border-bottom my-3 py-2&quot; th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
&lt;!-- 답변 반복 시작 --&gt;
&lt;div class=&quot;card my-3&quot; th:each=&quot;answer: ${question.answerList}&quot;&gt;
   &lt;div class=&quot;card-body&quot;&gt;
      &lt;div class=&quot;card-text&quot; style=&quot;white-space:pre-line;&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
         &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt; 
            &lt;div class=&quot;mb-2&quot;&gt;
                              &lt;span th:if=&quot;${answer.author != null}&quot;
                              th:text =&quot;${answer.author.username}&quot;&gt;&lt;/span&gt;
                           &lt;/div&gt;
            &lt;div th:text=&quot;${#temporals.format(answer.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;    
         &lt;/div&gt;
      &lt;/div&gt;
      &lt;/div&gt;
   &lt;/div&gt;
&lt;!-- 답변 반복 끝 --&gt;
&lt;!-- 답변 작성--&gt;
&lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; th:object=&quot;${answerForm}&quot; method=&quot;post&quot; class=&quot;my-3&quot;&gt;
   &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;      
   &lt;textarea sec:authorize=&quot;isAnonymous()&quot; disabled th:field=&quot;*{content}&quot; 
            class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
      &lt;textarea sec:authorize=&quot;isAuthenticated()&quot; th:field=&quot;*{content}&quot; 
         class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;          
      &lt;input type=&quot;submit&quot; value=&quot;답변 등록&quot; class=&quot;btn btn-primary my-2&quot;&gt; &lt;/form&gt;
&lt;/div&gt;
&lt;script layout:fragment=&quot;script&quot; type=&#39;text/javascript&#39;&gt;
   const delete_elements = document.getElementsByClassName(&quot;delete&quot;);
   Array.from(delete_elements).forEach(function (element) {
      element.addEventListener(&#39;click&#39;, function () {
         if (confirm(&quot;정말로 삭제하시겠습니까?&quot;)) {
            location.href = this.dataset.uri;
         };
      });
   });
&lt;/script&gt;
&lt;/html&gt;</code></pre>
</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>QuestionService.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
import com.mysite.sbb.DataNotFoundException;
import com.mysite.sbb.user.SiteUser;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class QuestionService {
 private final QuestionRepository questionRepository;   
 public List&lt;Question&gt; getList(){
    return this.questionRepository.findAll();
 }  
 public Question getQuestion(Integer id) {
    Optional&lt;Question&gt; question = this.questionRepository.findById(id);
    if(question.isPresent()) {
       return question.get();
    }else {
       throw new DataNotFoundException(&quot;question not found&quot;);
    }
 }
 public void create(String subject, String content, SiteUser user) {
    Question q = new Question();
    q.setSubject(subject);
    q.setContent(content);
    q.setCreateDate(LocalDateTime.now());
    q.setAuthor(user);
    this.questionRepository.save(q);
 }
 public void modify(Question question, String subject, String content) {
    question.setSubject(subject);
    question.setContent(content);
    question.setModifyDate(LocalDateTime.now());
    this.questionRepository.save(question);
 }
 public void delete(Question question) {
    this.questionRepository.delete(question);
 }
}</code></pre>
</li>
<li>QuestionController.java 수정<pre><code class="language-html">package com.mysite.sbb.question;
import java.security.Principal;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;
import com.mysite.sbb.answer.AnswerForm;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/question&quot;)
@RequiredArgsConstructor
@Controller
public class QuestionController {   
 private final QuestionService questionService;
 private final UserService userService;
 @GetMapping(&quot;/list&quot;)
 public String list(Model model) {
    List&lt;Question&gt; questionList = this.questionService.getList();
    model.addAttribute(&quot;questionList&quot;, questionList);
    return &quot;question_list&quot;;
 }
 @GetMapping(value = &quot;/detail/{id}&quot;)
 public String detail(Model model, @PathVariable(&quot;id&quot;) Integer id, AnswerForm answerForm) {
    Question question = this.questionService.getQuestion(id);
    model.addAttribute(&quot;question&quot;,question);
    return &quot;question_detail&quot;;
 }   
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @GetMapping(&quot;/create&quot;)
 public String questionCreate(QuestionForm questionForm) {
    return &quot;question_form&quot;;
 }   
 @PreAuthorize(&quot;isAuthenticated()&quot;)// 로그아웃상태에서 호출하면 로그인페이지로 강제 이동.
 @PostMapping(&quot;/create&quot;)
 public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult, Principal principal ) {
    if(bindingResult.hasErrors()) {
       return &quot;question_form&quot;;
    }
    SiteUser siteUser = this.userService.getUser(principal.getName());
    this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
    return &quot;redirect:/question/list&quot;;   // 질문 저장후 질문 목록으로 이동
 }   
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @GetMapping(&quot;/modify/{id}&quot;)
 public String questionModify(QuestionForm questionForm, @PathVariable(&quot;id&quot;) Integer id, 
       Principal principal) {
    Question question = this.questionService.getQuestion(id);
    if (!question.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, &quot;수정권한이 없습니다.&quot;);
    }
    questionForm.setSubject(question.getSubject());
    questionForm.setContent(question.getContent());
    return &quot;question_form&quot;;
 }   
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @PostMapping(&quot;/modify/{id}&quot;)
 public String questionModify(@Valid QuestionForm questionForm, BindingResult bindingResult, Principal principal,
       @PathVariable(&quot;id&quot;) Integer id) {
    if (bindingResult.hasErrors()) {
       return &quot;question_form&quot;;
    }
    Question question = this.questionService.getQuestion(id);
    if (!question.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, &quot;수정권한이 없습니다.&quot;);
    }
    this.questionService.modify(question, questionForm.getSubject(), 
          questionForm.getContent());
    return String.format(&quot;redirect:/question/detail/%s&quot;, id);
 }
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @GetMapping(&quot;/delete/{id}&quot;)
 public String questionDelete(Principal principal, @PathVariable(&quot;id&quot;) Integer id) {
    Question question = this.questionService.getQuestion(id);
    if (!question.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, &quot;삭제권한이 없습니다.&quot;);
    }
    this.questionService.delete(question);
    return &quot;redirect:/&quot;;
 }
}</code></pre>
</li>
<li>question_detail.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
 &lt;!-- 질문--&gt;
 &lt;h2 class=&quot;border-bottom py-2&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/h2&gt; 
 &lt;div class=&quot;card my-3&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
             &lt;div class=&quot;mb-2&quot;&gt;
                               &lt;span th:if=&quot;${question.author != null}&quot;
                               th:text =&quot;${question.author.username}&quot;&gt;&lt;/span&gt;
                            &lt;/div&gt;
              &lt;div th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
       &lt;div class=&quot;my-3&quot;&gt;                           
          &lt;a th:href=&quot;@{|/question/modify/${question.id}|}&quot; class=&quot;btn btn-sm btn-outline-secondary&quot;
                sec:authorize=&quot;isAuthenticated()&quot; 
                th:if=&quot;${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}&quot; th:text=&quot;수정&quot;&gt;&lt;/a&gt;
          &lt;a href=&quot;javascript:void(0);&quot; th:data-uri=&quot;@{|/question/delete/${question.id}|}&quot;
                               class=&quot;delete btn btn-sm btn-outline-secondary&quot; sec:authorize=&quot;isAuthenticated()&quot;
                               th:if=&quot;${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}&quot;
                               th:text=&quot;삭제&quot;&gt;&lt;/a&gt;   
                                     &lt;/div&gt;
                &lt;/div&gt;      
 &lt;!-- 답변 개수 표시 --&gt;
 &lt;h5 class=&quot;border-bottom my-3 py-2&quot; th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
 &lt;!-- 답변 반복 시작 --&gt;
 &lt;div class=&quot;card my-3&quot; th:each=&quot;answer: ${question.answerList}&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space:pre-line;&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt; 
             &lt;div class=&quot;mb-2&quot;&gt;
                               &lt;span th:if=&quot;${answer.author != null}&quot;
                               th:text =&quot;${answer.author.username}&quot;&gt;&lt;/span&gt;
                            &lt;/div&gt;
             &lt;div th:text=&quot;${#temporals.format(answer.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;      
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;div class=&quot;my-3&quot;&gt;
                         &lt;a th:href=&quot;@{|/answer/modify/${answer.id}|}&quot; class=&quot;btn btn-sm btn-outline-secondary&quot;
                            sec:authorize=&quot;isAuthenticated()&quot;
                            th:if=&quot;${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}&quot;
                            th:text=&quot;수정&quot;&gt;&lt;/a&gt;
             &lt;/div&gt;   
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 반복 끝 --&gt;
 &lt;!-- 답변 작성--&gt;
 &lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; th:object=&quot;${answerForm}&quot; method=&quot;post&quot; class=&quot;my-3&quot;&gt;
    &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;     
    &lt;textarea sec:authorize=&quot;isAnonymous()&quot; disabled th:field=&quot;*{content}&quot; 
             class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
       &lt;textarea sec:authorize=&quot;isAuthenticated()&quot; th:field=&quot;*{content}&quot; 
          class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;   
       &lt;input type=&quot;submit&quot; value=&quot;답변 등록&quot; class=&quot;btn btn-primary my-2&quot;&gt; &lt;/form&gt;
 &lt;/div&gt;
 &lt;script layout:fragment=&quot;script&quot; type=&#39;text/javascript&#39;&gt;
    const delete_elements = document.getElementsByClassName(&quot;delete&quot;);
    Array.from(delete_elements).forEach(function (element) {
       element.addEventListener(&#39;click&#39;, function () {
          if (confirm(&quot;정말로 삭제하시겠습니까?&quot;)) {
             location.href = this.dataset.uri;
          };
       });
    });
 &lt;/script&gt;
 &lt;/html&gt;</code></pre>
</li>
<li>AnswerService.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import java.time.LocalDateTime;
import java.util.Optional;
import org.springframework.stereotype.Service;
import com.mysite.sbb.DataNotFoundException;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class AnswerService {
private final AnswerRepository answerRepository;   
 public void create(Question question, String content, SiteUser author) {
    Answer answer = new Answer();
    answer.setContent(content);
    answer.setCreateDate(LocalDateTime.now());
    answer.setQuestion(question);
    answer.setAuthor(author);
    this.answerRepository.save(answer);
 }
 public Answer getAnswer(Integer id) {
    Optional&lt;Answer&gt; answer = this.answerRepository.findById(id);
    if (answer.isPresent()) {
       return answer.get();
    } else {
       throw new DataNotFoundException(&quot;answer not found&quot;);
    }
 }
 public void modify(Answer answer, String content) {
    answer.setContent(content);
    answer.setModifyDate(LocalDateTime.now());
    this.answerRepository.save(answer);
 }
}</code></pre>
</li>
<li>AnswerController.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import java.security.Principal;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/answer&quot;)
@RequiredArgsConstructor
@Controller
public class AnswerController {
 private final QuestionService questionService;
 private final AnswerService answerService;
 private final UserService userService;   
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @PostMapping(&quot;/create/{id}&quot;)
 public String createAnswer(Model model, @PathVariable(&quot;id&quot;) Integer id,
       @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
    Question question = this.questionService.getQuestion(id);
    SiteUser siteUser = this.userService.getUser(principal.getName());
    if(bindingResult.hasErrors()) {
       model.addAttribute(&quot;question&quot;,question);
       return &quot;question_detail&quot;;
    }
    // TODO: 답변을 저장한다.
    this.answerService.create(question, answerForm.getContent(), siteUser);
    return String.format(&quot;redirect:/question/detail/%s&quot;, id);
 }
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @GetMapping(&quot;/modify/{id}&quot;)
 public String answerModify(AnswerForm answerForm, @PathVariable(&quot;id&quot;) Integer id, 
       Principal principal) {
    Answer answer = this.answerService.getAnswer(id);
    if (!answer.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, &quot;수정 권한이 없습니다.&quot;);
    }
    answerForm.setContent(answer.getContent());
    return &quot;answer_form&quot;;
 }
}</code></pre>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>answer_form.html 생성<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container&quot;&gt;
 &lt;h5 class=&quot;my-3 border-bottom pb-2&quot;&gt;답변 수정&lt;/h5&gt;
 &lt;form th:object=&quot;${answerForm}&quot; method=&quot;post&quot;&gt;
    &lt;input type=&quot;hidden&quot; th:name=&quot;${_csrf.parameterName}&quot; th:value=&quot;${_csrf.token}&quot; /&gt;
    &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;content&quot; class=&quot;form-label&quot;&gt;내용&lt;/label&gt;
       &lt;textarea th:field=&quot;*{content}&quot; class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
    &lt;/div&gt;
    &lt;input type=&quot;submit&quot; value=&quot;저장하기&quot; class=&quot;btn btn-primary my-2&quot;&gt;
 &lt;/form&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre>
</li>
<li>AnswerController.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import java.security.Principal;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/answer&quot;)
@RequiredArgsConstructor
@Controller
public class AnswerController {
 private final QuestionService questionService;
 private final AnswerService answerService;
 private final UserService userService;
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @PostMapping(&quot;/create/{id}&quot;)
 public String createAnswer(Model model, @PathVariable(&quot;id&quot;) Integer id,
       @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
    Question question = this.questionService.getQuestion(id);
    SiteUser siteUser = this.userService.getUser(principal.getName());
    if(bindingResult.hasErrors()) {
       model.addAttribute(&quot;question&quot;,question);
       return &quot;question_detail&quot;;
    }
    // TODO: 답변을 저장한다.
    this.answerService.create(question, answerForm.getContent(), siteUser);
    return String.format(&quot;redirect:/question/detail/%s&quot;, id);
 }
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @GetMapping(&quot;/modify/{id}&quot;)
 public String answerModify(AnswerForm answerForm, @PathVariable(&quot;id&quot;) Integer id, 
       Principal principal) {
    Answer answer = this.answerService.getAnswer(id);
    if (!answer.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, &quot;수정 권한이 없습니다.&quot;);
    }
    answerForm.setContent(answer.getContent());
    return &quot;answer_form&quot;;
 }
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @PostMapping(&quot;/modify/{id}&quot;)
 public String answerModify(@Valid AnswerForm answerForm, BindingResult bindingResult,
       @PathVariable(&quot;id&quot;) Integer id, Principal principal) {
    if (bindingResult.hasErrors()) {
       return &quot;answer_form&quot;;
    }
    Answer answer = this.answerService.getAnswer(id);
    if (!answer.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, &quot;수정 권한이 없습니다.&quot;);
    }
    this.answerService.modify(answer, answerForm.getContent());
    return String.format(&quot;redirect:/question/detail/%s#answer_%s&quot;, 
          answer.getQuestion().getId(), answer.getId());
 }
}</code></pre>
</li>
<li>question_detail.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
 &lt;!-- 질문--&gt;
 &lt;h2 class=&quot;border-bottom py-2&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/h2&gt; 
 &lt;div class=&quot;card my-3&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
             &lt;div class=&quot;mb-2&quot;&gt;
                               &lt;span th:if=&quot;${question.author != null}&quot;
                               th:text =&quot;${question.author.username}&quot;&gt;&lt;/span&gt;
                            &lt;/div&gt;
              &lt;div th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
       &lt;div class=&quot;my-3&quot;&gt;                           
          &lt;a th:href=&quot;@{|/question/modify/${question.id}|}&quot; class=&quot;btn btn-sm btn-outline-secondary&quot;
                sec:authorize=&quot;isAuthenticated()&quot; 
                th:if=&quot;${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}&quot; th:text=&quot;수정&quot;&gt;&lt;/a&gt;
          &lt;a href=&quot;javascript:void(0);&quot; th:data-uri=&quot;@{|/question/delete/${question.id}|}&quot;
                               class=&quot;delete btn btn-sm btn-outline-secondary&quot; sec:authorize=&quot;isAuthenticated()&quot;
                               th:if=&quot;${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}&quot;
                               th:text=&quot;삭제&quot;&gt;&lt;/a&gt;   
                                     &lt;/div&gt;
                &lt;/div&gt;   
 &lt;!-- 답변 개수 표시 --&gt;
 &lt;h5 class=&quot;border-bottom my-3 py-2&quot; th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
 &lt;!-- 답변 반복 시작 --&gt;
 &lt;div class=&quot;card my-3&quot; th:each=&quot;answer: ${question.answerList}&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space:pre-line;&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt; 
             &lt;div class=&quot;mb-2&quot;&gt;
                               &lt;span th:if=&quot;${answer.author != null}&quot;
                               th:text =&quot;${answer.author.username}&quot;&gt;&lt;/span&gt;
                            &lt;/div&gt;
             &lt;div th:text=&quot;${#temporals.format(answer.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;      
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;div class=&quot;my-3&quot;&gt;
                         &lt;a th:href=&quot;@{|/answer/modify/${answer.id}|}&quot; class=&quot;btn btn-sm btn-outline-secondary&quot;
                            sec:authorize=&quot;isAuthenticated()&quot;
                            th:if=&quot;${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}&quot;
                            th:text=&quot;수정&quot;&gt;&lt;/a&gt;
                            &lt;a href=&quot;javascript:void(0);&quot; th:data-uri=&quot;@{|/answer/delete/${answer.id}|}&quot;
                                                 class=&quot;delete btn btn-sm btn-outline-secondary&quot; sec:authorize=&quot;isAuthenticated()&quot;
                                                 th:if=&quot;${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}&quot;
                                                 th:text=&quot;삭제&quot;&gt;&lt;/a&gt;
             &lt;/div&gt;   
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 반복 끝 --&gt;
 &lt;!-- 답변 작성--&gt;
 &lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; th:object=&quot;${answerForm}&quot; method=&quot;post&quot; class=&quot;my-3&quot;&gt;
    &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;      
    &lt;textarea sec:authorize=&quot;isAnonymous()&quot; disabled th:field=&quot;*{content}&quot; 
             class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
       &lt;textarea sec:authorize=&quot;isAuthenticated()&quot; th:field=&quot;*{content}&quot; 
          class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;            
       &lt;input type=&quot;submit&quot; value=&quot;답변 등록&quot; class=&quot;btn btn-primary my-2&quot;&gt; &lt;/form&gt;
 &lt;/div&gt;   
 &lt;script layout:fragment=&quot;script&quot; type=&#39;text/javascript&#39;&gt;
    const delete_elements = document.getElementsByClassName(&quot;delete&quot;);
    Array.from(delete_elements).forEach(function (element) {
       element.addEventListener(&#39;click&#39;, function () {
          if (confirm(&quot;정말로 삭제하시겠습니까?&quot;)) {
             location.href = this.dataset.uri;
          };
       });
    });
 &lt;/script&gt;
 &lt;/html&gt;</code></pre>
</li>
<li>AnswerService.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import java.time.LocalDateTime;
import java.util.Optional;
import org.springframework.stereotype.Service;
import com.mysite.sbb.DataNotFoundException;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class AnswerService {
 private final AnswerRepository answerRepository;   
 public void create(Question question, String content, SiteUser author) {
    Answer answer = new Answer();
    answer.setContent(content);
    answer.setCreateDate(LocalDateTime.now());
    answer.setQuestion(question);
    answer.setAuthor(author);
    this.answerRepository.save(answer);
 }
 public Answer getAnswer(Integer id) {
    Optional&lt;Answer&gt; answer = this.answerRepository.findById(id);
    if (answer.isPresent()) {
       return answer.get();
    } else {
       throw new DataNotFoundException(&quot;answer not found&quot;);
    }
 }
 public void modify(Answer answer, String content) {
    answer.setContent(content);
    answer.setModifyDate(LocalDateTime.now());
    this.answerRepository.save(answer);
 }
 public void delete(Answer answer) {
    this.answerRepository.delete(answer);
 }
}</code></pre>
</li>
<li>AnswerController.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import java.security.Principal;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/answer&quot;)
@RequiredArgsConstructor
@Controller
public class AnswerController {
 private final QuestionService questionService;
 private final AnswerService answerService;
 private final UserService userService;   
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @PostMapping(&quot;/create/{id}&quot;)
 public String createAnswer(Model model, @PathVariable(&quot;id&quot;) Integer id,
       @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
    Question question = this.questionService.getQuestion(id);
    SiteUser siteUser = this.userService.getUser(principal.getName());
    if(bindingResult.hasErrors()) {
       model.addAttribute(&quot;question&quot;,question);
       return &quot;question_detail&quot;;
    }
    // TODO: 답변을 저장한다.
    this.answerService.create(question, answerForm.getContent(), siteUser);
    return String.format(&quot;redirect:/question/detail/%s&quot;, id);
 }
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @GetMapping(&quot;/modify/{id}&quot;)
 public String answerModify(AnswerForm answerForm, @PathVariable(&quot;id&quot;) Integer id, 
       Principal principal) {
    Answer answer = this.answerService.getAnswer(id);
    if (!answer.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, &quot;수정 권한이 없습니다.&quot;);
    }
    answerForm.setContent(answer.getContent());
    return &quot;answer_form&quot;;
 }   
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @PostMapping(&quot;/modify/{id}&quot;)
 public String answerModify(@Valid AnswerForm answerForm, BindingResult bindingResult,
       @PathVariable(&quot;id&quot;) Integer id, Principal principal) {
    if (bindingResult.hasErrors()) {
       return &quot;answer_form&quot;;
    }
    Answer answer = this.answerService.getAnswer(id);
    if (!answer.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, &quot;수정 권한이 없습니다.&quot;);
    }
    this.answerService.modify(answer, answerForm.getContent());
    return String.format(&quot;redirect:/question/detail/%s#answer_%s&quot;, 
          answer.getQuestion().getId(), answer.getId());
 }  
 @PreAuthorize(&quot;isAuthenticated()&quot;)
 @GetMapping(&quot;/delete/{id}&quot;)
 public String answerDelete(Principal principal, @PathVariable(&quot;id&quot;) Integer id) {
    Answer answer = this.answerService.getAnswer(id);
    if (!answer.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, &quot;삭제권한이 없습니다.&quot;);
    }
    this.answerService.delete(answer);
    return String.format(&quot;redirect:/question/detail/%s&quot;, answer.getQuestion().getId());
 }
}</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/d218bb8a-4b70-4fb4-b08d-0b8eac4f4880/image.png" alt=""></li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>question_detail.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
 &lt;!-- 질문--&gt;
 &lt;h2 class=&quot;border-bottom py-2&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/h2&gt; 
 &lt;div class=&quot;card my-3&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div th:if=&quot;${question.modifyDate != null}&quot; class=&quot;badge bg-light text-dark p-2 text-start mx-3&quot;&gt;
                                  &lt;div class=&quot;mb-2&quot;&gt;modified at&lt;/div&gt;
                                  &lt;div th:text=&quot;${#temporals.format(question.modifyDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
                               &lt;/div&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
             &lt;div class=&quot;mb-2&quot;&gt;
                               &lt;span th:if=&quot;${question.author != null}&quot;
                               th:text =&quot;${question.author.username}&quot;&gt;&lt;/span&gt;
                            &lt;/div&gt;
              &lt;div th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
       &lt;div class=&quot;my-3&quot;&gt;                           
          &lt;a th:href=&quot;@{|/question/modify/${question.id}|}&quot; class=&quot;btn btn-sm btn-outline-secondary&quot;
                sec:authorize=&quot;isAuthenticated()&quot; 
                th:if=&quot;${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}&quot; th:text=&quot;수정&quot;&gt;&lt;/a&gt;
          &lt;a href=&quot;javascript:void(0);&quot; th:data-uri=&quot;@{|/question/delete/${question.id}|}&quot;
                               class=&quot;delete btn btn-sm btn-outline-secondary&quot; sec:authorize=&quot;isAuthenticated()&quot;
                               th:if=&quot;${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}&quot;
                               th:text=&quot;삭제&quot;&gt;&lt;/a&gt;   
                                     &lt;/div&gt;
                &lt;/div&gt;      
 &lt;!-- 답변 개수 표시 --&gt;
 &lt;h5 class=&quot;border-bottom my-3 py-2&quot; th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
 &lt;!-- 답변 반복 시작 --&gt;
 &lt;div class=&quot;card my-3&quot; th:each=&quot;answer: ${question.answerList}&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space:pre-line;&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div th:if=&quot;${answer.modifyDate != null}&quot; class=&quot;badge bg-light text-dark p-2 text-start mx-3&quot;&gt;
                                  &lt;div class=&quot;mb-2&quot;&gt;modified at&lt;/div&gt;
                                  &lt;div th:text=&quot;${#temporals.format(answer.modifyDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
                               &lt;/div&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt; 
             &lt;div class=&quot;mb-2&quot;&gt;
                               &lt;span th:if=&quot;${answer.author != null}&quot;
                               th:text =&quot;${answer.author.username}&quot;&gt;&lt;/span&gt;
                            &lt;/div&gt;
             &lt;div th:text=&quot;${#temporals.format(answer.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;div class=&quot;my-3&quot;&gt;
                         &lt;a th:href=&quot;@{|/answer/modify/${answer.id}|}&quot; class=&quot;btn btn-sm btn-outline-secondary&quot;
                            sec:authorize=&quot;isAuthenticated()&quot;
                            th:if=&quot;${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}&quot;
                            th:text=&quot;수정&quot;&gt;&lt;/a&gt;
                            &lt;a href=&quot;javascript:void(0);&quot; th:data-uri=&quot;@{|/answer/delete/${answer.id}|}&quot;
                                                 class=&quot;delete btn btn-sm btn-outline-secondary&quot; sec:authorize=&quot;isAuthenticated()&quot;
                                                 th:if=&quot;${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}&quot;
                                                 th:text=&quot;삭제&quot;&gt;&lt;/a&gt;
             &lt;/div&gt;   
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 반복 끝 --&gt;
 &lt;!-- 답변 작성--&gt;
 &lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; th:object=&quot;${answerForm}&quot; method=&quot;post&quot; class=&quot;my-3&quot;&gt;
    &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;      
    &lt;textarea sec:authorize=&quot;isAnonymous()&quot; disabled th:field=&quot;*{content}&quot; 
             class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
       &lt;textarea sec:authorize=&quot;isAuthenticated()&quot; th:field=&quot;*{content}&quot; 
          class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;            
       &lt;input type=&quot;submit&quot; value=&quot;답변 등록&quot; class=&quot;btn btn-primary my-2&quot;&gt; &lt;/form&gt;
 &lt;/div&gt;   
 &lt;script layout:fragment=&quot;script&quot; type=&#39;text/javascript&#39;&gt;
    const delete_elements = document.getElementsByClassName(&quot;delete&quot;);
    Array.from(delete_elements).forEach(function (element) {
       element.addEventListener(&#39;click&#39;, function () {
          if (confirm(&quot;정말로 삭제하시겠습니까?&quot;)) {
             location.href = this.dataset.uri;
          };
       });
    });
 &lt;/script&gt;
 &lt;/html&gt;</code></pre>
</li>
<li>실행 및 결과 (수정시간이 나옴)
<img src="https://velog.velcdn.com/images/hi_do/post/1665ab68-abac-46d1-83f9-a120d84095ab/image.png" alt=""></li>
</ul>
<h4 id="파일-내보내는-방법">파일 내보내는 방법</h4>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/348f1126-756f-4111-a932-d6c0cb414e62/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/039355c9-2ccd-4426-8d41-d91a0da0d068/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/0010135b-339f-41d1-8d94-c84ff3291bde/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/0f870253-56a3-45b5-a9dc-bead51b3cbbc/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SpringBoot - 게시판 질문과 답변]]></title>
            <link>https://velog.io/@hi_do/SpringBoot-</link>
            <guid>https://velog.io/@hi_do/SpringBoot-</guid>
            <pubDate>Tue, 18 Jun 2024 03:09:36 GMT</pubDate>
            <description><![CDATA[<h4 id="질문-목록-만들기">질문 목록 만들기</h4>
<blockquote>
</blockquote>
<ul>
<li>QuestionController.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class QuestionController {
 private final QuestionRepository questionRepository;
 @GetMapping(&quot;/question/list&quot;)
 public String list(Model model) {
    List&lt;Question&gt; questionList = this.questionRepository.findAll();
    model.addAttribute(&quot;questionList&quot;, questionList);
    return &quot;question_list&quot;;
 }
}</code></pre>
</li>
<li>templates 에 question_list.html 생성<pre><code class="language-html">&lt;tr th:each=&quot;question : ${questionList}&quot;&gt;
    &lt;td th:text=&quot;${question.subject}&quot;&gt;&lt;/td&gt;
    &lt;td th:text=&quot;${question.createDate}&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr th:each=&quot;question : ${questionList}&quot;&gt;
 &lt;td&gt;[[$question.subject]]]&lt;/td&gt;
 &lt;td&gt;[[$question.createDate]]]&lt;/td&gt;
&lt;/td&gt;</code></pre>
</li>
<li><blockquote>
<p>th : text 속성 대신에 대괄호를 사용하여 값을 직접 출력할 수 있다</p>
</blockquote>
<h4 id="service서비스">Service(서비스)</h4>
</li>
<li>대부분의 규모 있는 부트 프로젝트는 컨트롤러에서 라포지터리를 직접 호출하지 않고 중간에 서비스를 두어 데이터를 처리<h4 id="서비스가-필요한-이유">서비스가 필요한 이유</h4>
</li>
<li>복잡한 코드를 모듈화하는 것
A,B 컨트롤러가 중복된 코드를 각 호출하느 것보다 Service에 중복된 기능을 넣어 놓고 A,B가 Service를 호출하게 만든다<h4 id="dtodata-transfer-object">DTO(Data Transfer Object)</h4>
</li>
<li>entity 객체를 DTO객체로 변환해야 한다
Question, Answer 엔티티 클래스는 데이터 베이스와 직접 맞닿아 있는 클래스
컨트롤러 또는 타임리프와 같은 템플릿 엔진에 전달해 직접적으로 사용하는 것은 좋지 않다</li>
<li><blockquote>
<p>왜냐하면 엔티티 객체는 민감한 데이터가 포함될 수 있는데 타임리프에서 엔티티 객체를 직접 사용하면 민감한 데이터가 노출될 위험이 있기 때문이다.
Question, Answer같은 엔티티 클래스는 컨트롤러에서 사용하지 않도록 설계하는 것이 좋다. 얘네를 대신해서 DTO를 사용한다.</p>
</blockquote>
</li>
<li>엔티티객체를 DTO객체로 변환하려면??? 이때도 서비스가 필요하다.</li>
<li><blockquote>
<p>서비스는 컨트롤러와 리포지터리의 중간에서 엔티티 객체와 DTO객체를 서로 변환하여 양방향에 전달하는 역할을 한다.</p>
</blockquote>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>QuestionService.java 생성<pre><code class="language-java">package com.mysite.sbb.question;
import java.util.List;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class QuestionService {    
  private final QuestionRepository questionRepository;
  public List&lt;Question&gt; getList() {
      return this.questionRepository.findAll();
  }
}</code></pre>
</li>
<li>QuestionController.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class QuestionController {   
 private final QuestionService questionservice;   
 @GetMapping(&quot;/question/list&quot;)
 public String list(Model model) {
    List&lt;Question&gt; questionList = this.questionservice.getList();
    model.addAttribute(&quot;questionList&quot;, questionList);
    return &quot;question_list&quot;;
 }
}</code></pre>
</li>
<li>다른 컨트롤러들도 이러한 순서로 접근하여 데이터를 처리한다. 
Controller -&gt; service -&gt; repository</li>
<li>QuestionList.html 수정<pre><code class="language-html">&lt;table&gt;
 &lt;thead&gt;
    &lt;tr&gt;
       &lt;th&gt;제목&lt;/th&gt;
       &lt;th&gt;작성일시&lt;/th&gt;
    &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
    &lt;tr th:each=&quot;question : ${questionList}&quot;&gt;
       &lt;td&gt;
          &lt;a th:href=&quot;@{|/question/detail/${question.id}|}&quot;
          th:text=&quot;${question.subject}&quot;&gt;&lt;/a&gt;
       &lt;/td&gt; 
       &lt;td th:text=&quot;${question.createDate}&quot;&gt;&lt;/td&gt;
    &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;</code></pre>
<pre><code>&lt;a th:href=&quot;@{|/question/detail/${question.id}|}&quot;</code></pre></li>
<li><blockquote>
<p>URL 연결할때 타임리프에서 th:href 속성을 사용한다. 이때, URL은 반드시@{와} 문자 사이에 입력해야 한다. &#39;|&#39;를 앞뒤로 감싸는 경우는 변수와 텍스트의 조합일 경우에 감싼다.
/question/list 와 같은 문자열과 ${question.id}는 자바객체의 값을 같이 사용할때는 반드시 &#39;|&#39;를 좌우로 감싸준다.</p>
</blockquote>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>QuestionController.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.util.List;
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 lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class QuestionController {  
 private final QuestionService questionService;  
 @GetMapping(&quot;/question/list&quot;)
 public String list(Model model) {
    List&lt;Question&gt; questionList = this.questionService.getList();
    model.addAttribute(&quot;questionList&quot;, questionList);
    return &quot;question_list&quot;;
 }
 @GetMapping(value = &quot;/question/detail/{id}&quot;)
 public String detail(Model model, @PathVariable(&quot;id&quot;) Integer id) {
    return &quot;question_detail&quot;;
 }
}</code></pre>
</li>
<li>둘 다 데이터를 받아오기 위한 쿼리스트링, 파라미터 처리
@PathVariable : 파라미터값으로 하나만 받아올 수 있다.
@RequestParam : 쿼리스트링 같은 여러개의 데이터를 받아올때 사용 (key-value 형태로 받아올 수 있다)</li>
<li>QuestionService.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
import com.mysite.sbb.DataNotFoundException;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class QuestionService {    
  private final QuestionRepository questionRepository;    
  public List&lt;Question&gt; getList() {
      return this.questionRepository.findAll();
  }    
  public Question getQuestion(Integer id) {
      Optional&lt;Question&gt; question = this.questionRepository.findById(id);
      if(question.isPresent()) {
          return question.get();
      }else {
          throw new DataNotFoundException(&quot;question not found&quot;);
      }
  }
}</code></pre>
</li>
<li>DataNotFoundException.java 생성<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = &quot;entity not found&quot;)
public class DataNotFoundException extends RuntimeException{
// RuntimeException: 실행시 발생하는 예외
  private static final long serialVersionUID = 1L;
  public DataNotFoundException(String message) {
      super(message);
  }
}</code></pre>
</li>
<li>QuestionController.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.util.List;
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 lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class QuestionController {   
 private final QuestionService questionService;
 @GetMapping(&quot;/question/list&quot;)
 public String list(Model model) {
    List&lt;Question&gt; questionList = this.questionService.getList();
    model.addAttribute(&quot;questionList&quot;, questionList);
    return &quot;question_list&quot;;
 }
 @GetMapping(value = &quot;/question/detail/{id}&quot;)
 public String detail(Model model, @PathVariable(&quot;id&quot;) Integer id) {
     Question question = this.questionService.getQuestion(id);
     model.addAttribute(&quot;question&quot;,question);
    return &quot;question_detail&quot;;
 }
}</code></pre>
<img src="https://velog.velcdn.com/images/hi_do/post/62dbf945-c22e-4610-be3e-f5533fd6a758/image.png" alt=""></li>
<li><blockquote>
<p> DataNotFoundException.javad의 @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = &quot;entity not found&quot;)
QuestionService.javad 의 throw new DataNotFoundException(&quot;question not found&quot;); 가 실행됨</p>
</blockquote>
</li>
<li><img src="https://velog.velcdn.com/images/hi_do/post/85cc7a8b-79e1-4c40-911e-a1bf6b113af6/image.png" alt=""></li>
<li><blockquote>
<p>클릭시 <img src="https://velog.velcdn.com/images/hi_do/post/5b1d4e9e-088c-4a74-bace-1d312171d9f5/image.png" alt="">
로 이동</p>
</blockquote>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>QuestionController.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.util.List;
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 org.springframework.web.bind.annotation.RequestMapping;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/question&quot;)
@RequiredArgsConstructor
@Controller
public class QuestionController {  
 private final QuestionService questionService;   
 @GetMapping(&quot;/list&quot;)
 public String list(Model model) {
    List&lt;Question&gt; questionList = this.questionService.getList();
    model.addAttribute(&quot;questionList&quot;, questionList);
    return &quot;question_list&quot;;
 }
 @GetMapping(value = &quot;/detail/{id}&quot;)
 public String detail(Model model, @PathVariable(&quot;id&quot;) Integer id) {
     Question question = this.questionService.getQuestion(id);
     model.addAttribute(&quot;question&quot;,question);
    return &quot;question_detail&quot;;
 }
}</code></pre>
</li>
<li>question_detail.html 생성<pre><code class="language-html">&lt;h1 th:text=&quot;${question.subject}&quot;&gt;&lt;/h1&gt;
&lt;div th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
&lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; method=&quot;post&quot;&gt;
 &lt;textarea name=&quot;content&quot; id=&quot;content&quot; rows=&quot;15&quot;&gt;&lt;/textarea&gt;
 &lt;input type=&quot;submit&quot; value=&quot;답변 등록 &quot;&gt;
&lt;/form&gt;</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/7fb72a74-0f0a-492c-92c7-ac4d2ef9e13a/image.png" alt=""></li>
<li>AnswerController.java 생성<pre><code class="language-java">package com.mysite.sbb.answer;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/answer&quot;)
@RequiredArgsConstructor
@Controller
public class AnswerController {
 private final QuestionService questionService;   
 @PostMapping(&quot;/create/{id}&quot;)
 public String createAnswer(Model model, @PathVariable(&quot;id&quot;) Integer id, 
       @RequestParam(value=&quot;content&quot;) String content) {
    Question question = this.questionService.getQuestion(id);
    // TODO: 답변을 저장한다.
    return String.format(&quot;redirect:/questiohn/detail/%s&quot;, id);
 }
}</code></pre>
</li>
<li>AnswerService.java 생성<pre><code class="language-java">package com.mysite.sbb.answer;
import java.time.LocalDateTime;
import org.springframework.stereotype.Service;
import com.mysite.sbb.question.Question;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class AnswerService {
  private final AnswerRepository answerRepository;
  public void create(Question question, String content) {
      Answer answer = new Answer();
      answer.setContent(content);
      answer.setCreateDate(LocalDateTime.now());
      answer.setQuestion(question);
      this.answerRepository.save(answer);
  }
}</code></pre>
</li>
<li>AnswerController.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/answer&quot;)
@RequiredArgsConstructor
@Controller
public class AnswerController {
 private final QuestionService questionService;
 private final AnswerService answerService;   
 @PostMapping(&quot;/create/{id}&quot;)
 public String createAnswer(Model model, @PathVariable(&quot;id&quot;) Integer id, 
       @RequestParam(value=&quot;content&quot;) String content) {
    Question question = this.questionService.getQuestion(id);
    // TODO: 답변을 저장한다.
    this.answerService.create(question, content);
    return String.format(&quot;redirect:/questiohn/detail/%s&quot;, id);
 }
}</code></pre>
</li>
<li>question_detail.html 수정
````html<h1 th:text="${question.subject}"></h1>
<div th:text="${question.content}"></div>
<h5 th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
// th : thymeleaf
//# : thymeleaf의 내장함수
// #의 의미 : thymeleaf 가 lists 내장 객체를 가지고 있고 lists 가 size 하는 내장 메서드를 가지고 있다.
<div>
 <ul>
    <li th:each="answer : ${question.answerList}" th:text="${answer.content}"></li>
 </ul>
</div>
<form th:action="@{|/answer/create/${question.id}|}" method="post">
 <textarea name="content" id="content" rows="15"></textarea>
 <input type="submit" value="답변 등록 ">
</form>
```</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/679b03a8-40e7-464b-8b42-847907f84b5c/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/96622786-832a-4f5c-b8e5-0dc263632191/image.png" alt=""></li>
</ul>
<blockquote>
</blockquote>
<p>static에 style.css 생성</p>
<pre><code>textarea{
   width: 100%;
}
input[type=submit]{
   margin-top: 10px;
}</code></pre><ul>
<li>css 다운받는곳 : <a href="https://getbootstrap.com/docs/5.3/getting-started/download/">https://getbootstrap.com/docs/5.3/getting-started/download/</a></li>
<li>압축 푼 후 bootstrap.min.css 복사<img src="https://velog.velcdn.com/images/hi_do/post/cbed0cfc-82e4-4f17-90bf-20a43bada501/image.png" alt="">
static에 붙여넣기
<img src="https://velog.velcdn.com/images/hi_do/post/1849549e-fcec-495a-b9b4-4827802e7fb1/image.png" alt=""></li>
<li>question_detail.html 수정<pre><code class="language-html">&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; th:href=&quot;@{/style.css}&quot;&gt;
&lt;h1 th:text=&quot;${question.subject}&quot;&gt;&lt;/h1&gt;
&lt;div th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
&lt;h5 th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
&lt;div&gt;
 &lt;ul&gt;
    &lt;li th:each=&quot;answer : ${question.answerList}&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/li&gt;
 &lt;/ul&gt;
&lt;/div&gt;
&lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; method=&quot;post&quot;&gt;
 &lt;textarea name=&quot;content&quot; id=&quot;content&quot; rows=&quot;15&quot;&gt;&lt;/textarea&gt;
 &lt;input type=&quot;submit&quot; value=&quot;답변 등록 &quot;&gt;
&lt;/form&gt;</code></pre>
</li>
<li>question_list.html 수정<pre><code class="language-html">&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; th:href=&quot;@{/bootstrap.min.css}&quot;&gt;
&lt;div class=&quot;container my-3&quot;&gt;
&lt;table class=&quot;table&quot;&gt;
 &lt;thead class=&quot;table-dark&quot;&gt;
    &lt;tr&gt;
       &lt;th&gt;번호&lt;/th&gt;
       &lt;th&gt;제목&lt;/th&gt;
       &lt;th&gt;작성일시&lt;/th&gt;
    &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
    &lt;tr th:each=&quot;question, loop : ${questionList}&quot;&gt;
       &lt;td th:text=&quot;${loop.count}&quot;&gt;&lt;/td&gt;
       &lt;td&gt;
          &lt;a th:href=&quot;@{|/question/detail/${question.id}|}&quot;
          th:text=&quot;${question.subject}&quot;&gt;&lt;/a&gt;
       &lt;/td&gt; 
       &lt;td th:text=&quot;${#temporals.format(question.createDate,&#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/td&gt;
    &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;</code></pre>
</li>
<li>question_detail.html 수정<pre><code class="language-html">&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; th:href=&quot;@{/bootstrap.min.css}&quot;&gt;
&lt;div class=&quot;container my-3&quot;&gt;
 &lt;!-- 질문--&gt;
 &lt;h2 class=&quot;border-bottom py-2&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/h2&gt; 
 &lt;div class=&quot;card my-3&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
              &lt;div th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 개수 표시 --&gt;
 &lt;h5 class=&quot;border-bottom my-3 py-2&quot; th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
 &lt;!-- 답변 반복 시작 --&gt;
 &lt;div class=&quot;card my-3&quot; th:each=&quot;answer: ${question.answerList}&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space:pre-line;&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt; 
             &lt;div th:text=&quot;${#temporals.format(answer.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;      
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 반복 끝 --&gt;
 &lt;!-- 답변 작성--&gt;
 &lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; method=&quot;post&quot; class=&quot;my-3&quot;&gt;
    &lt;textarea name=&quot;content&quot; id=&quot;content&quot; rows=&quot;10&quot; class=&quot;form-control&quot;&gt;&lt;/textarea&gt;
    &lt;input type=&quot;submit&quot; value=&quot;답변 등록&quot; class=&quot;btn btn-primary my-2&quot;&gt; &lt;/form&gt;
&lt;/div&gt;</code></pre>
<pre><code>&lt;html layout:decorate=&quot;~{layout}&quot;&gt;</code></pre></li>
<li>question_list 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
&lt;table class=&quot;table&quot;&gt;
 &lt;thead class=&quot;table-dark&quot;&gt;
    &lt;tr&gt;
       &lt;th&gt;번호&lt;/th&gt;
       &lt;th&gt;제목&lt;/th&gt;
       &lt;th&gt;작성일시&lt;/th&gt;
    &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
    &lt;tr th:each=&quot;question, loop : ${questionList}&quot;&gt;
       &lt;td th:text=&quot;${loop.count}&quot;&gt;&lt;/td&gt;
       &lt;td&gt;
          &lt;a th:href=&quot;@{|/question/detail/${question.id}|}&quot;
          th:text=&quot;${question.subject}&quot;&gt;&lt;/a&gt;
       &lt;/td&gt; 
       &lt;td th:text=&quot;${#temporals.format(question.createDate,&#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/td&gt;
    &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;</code></pre>
<ul>
<li>layout.html 생성<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
&lt;!-- Required meta tags --&gt;
&lt;meta charset=&quot;utf-8&quot;&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1, shrink-to-fit=no&quot;&gt;
&lt;!-- Bootstrap CSS --&gt;
&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; th:href=&quot;@{/bootstrap.min.css}&quot;&gt;
&lt;!-- sbb CSS --&gt;
&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; th:href=&quot;@{/style.css}&quot;&gt;
&lt;title&gt;Hello, sbb!&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;!-- 기본 템플릿 안에 삽입될 내용 Start --&gt;
&lt;th:block layout:fragment=&quot;content&quot;&gt;&lt;/th:block&gt;
&lt;!-- 기본 템플릿 안에 삽입될 내용 End --&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
</li>
</ul>
</li>
<li><blockquote>
<p>현재 페이지에 레이웃 템플릿을 적용(여기에서는 layout.html)
 ~{} 구문은 thymeleaf에서 템플릿 경로를 나타낸다</p>
</blockquote>
</li>
<li>부모 템플릿은 layout.html인<pre><code class="language-html"> &lt;!-- 기본 템플릿 안에 삽입될 내용 Start --&gt;
 &lt;th:block layout:fragment=&quot;content&quot;&gt;&lt;/th:block&gt;
 &lt;!-- 기본 템플릿 안에 삽입될 내용 End --&gt;
-&gt; 부모 템플릿에 작성된 부분를 자식 템플릿의 내용으로 적용될 수 있도록
&lt;div layout.fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
  ..생략
&lt;/div&gt;</code></pre>
<ul>
<li>question_detail.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
&lt;!-- 질문--&gt;
&lt;h2 class=&quot;border-bottom py-2&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/h2&gt; 
&lt;div class=&quot;card my-3&quot;&gt;
  &lt;div class=&quot;card-body&quot;&gt;
     &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
     &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
        &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
            &lt;div th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
     &lt;/div&gt;
     &lt;/div&gt;
  &lt;/div&gt;
&lt;!-- 답변 개수 표시 --&gt;
&lt;h5 class=&quot;border-bottom my-3 py-2&quot; th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
&lt;!-- 답변 반복 시작 --&gt;
&lt;div class=&quot;card my-3&quot; th:each=&quot;answer: ${question.answerList}&quot;&gt;
  &lt;div class=&quot;card-body&quot;&gt;
     &lt;div class=&quot;card-text&quot; style=&quot;white-space:pre-line;&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/div&gt;
     &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
        &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt; 
           &lt;div th:text=&quot;${#temporals.format(answer.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;      
        &lt;/div&gt;
     &lt;/div&gt;
     &lt;/div&gt;
  &lt;/div&gt;
&lt;!-- 답변 반복 끝 --&gt;
&lt;!-- 답변 작성--&gt;
&lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; method=&quot;post&quot; class=&quot;my-3&quot;&gt;
  &lt;textarea name=&quot;content&quot; id=&quot;content&quot; rows=&quot;10&quot; class=&quot;form-control&quot;&gt;&lt;/textarea&gt;
  &lt;input type=&quot;submit&quot; value=&quot;답변 등록&quot; class=&quot;btn btn-primary my-2&quot;&gt; &lt;/form&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre>
</li>
<li>question_list.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
&lt;table class=&quot;table&quot;&gt;
&lt;thead class=&quot;table-dark&quot;&gt;
  &lt;tr&gt;
     &lt;th&gt;번호&lt;/th&gt;
     &lt;th&gt;제목&lt;/th&gt;
     &lt;th&gt;작성일시&lt;/th&gt;
  &lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
  &lt;tr th:each=&quot;question, loop : ${questionList}&quot;&gt;
     &lt;td th:text=&quot;${loop.count}&quot;&gt;&lt;/td&gt;
     &lt;td&gt;
        &lt;a th:href=&quot;@{|/question/detail/${question.id}|}&quot;
        th:text=&quot;${question.subject}&quot;&gt;&lt;/a&gt;
     &lt;/td&gt; 
     &lt;td th:text=&quot;${#temporals.format(question.createDate,&#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/td&gt;
  &lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;a th:href=&quot;@{/question/create}&quot; class = &quot;btn btn-primary&quot;&gt;질문 등록하기&lt;/a&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre>
</li>
<li>QuestionController.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.util.List;
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 org.springframework.web.bind.annotation.RequestMapping;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/question&quot;)
@RequiredArgsConstructor
@Controller
public class QuestionController {  
private final QuestionService questionService;
@GetMapping(&quot;/list&quot;)
public String list(Model model) {
  List&lt;Question&gt; questionList = this.questionService.getList();
  model.addAttribute(&quot;questionList&quot;, questionList);
  return &quot;question_list&quot;;
}
@GetMapping(value = &quot;/detail/{id}&quot;)
public String detail(Model model, @PathVariable(&quot;id&quot;) Integer id) {
   Question question = this.questionService.getQuestion(id);
   model.addAttribute(&quot;question&quot;,question);
  return &quot;question_detail&quot;;
}
@GetMapping(&quot;/create&quot;)
public String questionCreate() {
   return &quot;question_form&quot;;
}
}</code></pre>
</li>
</ul>
</li>
<li>question_form.html 생성<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container&quot;&gt;
 &lt;h5 class=&quot;my-3 border-bottom pb-2&quot;&gt;질문등록&lt;/h5&gt;
 &lt;form th:action=&quot;@{/question/create}&quot; method=&quot;post&quot;&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;subject&quot; class=&quot;form-label&quot;&gt;제목&lt;/label&gt;
       &lt;input type=&quot;text&quot; name=&quot;subject&quot; id=&quot;subject&quot; class=&quot;form-control&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;content&quot; class=&quot;form-label&quot;&gt;내용&lt;/label&gt;
       &lt;textarea name=&quot;content&quot; id=&quot;content&quot; class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
    &lt;/div&gt;
    &lt;input type=&quot;submit&quot; value=&quot;저장하기&quot; class=&quot;btn btn-primary my-2&quot;&gt;
 &lt;/form&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre>
</li>
<li>QuestionController.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.util.List;
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 org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/question&quot;)
@RequiredArgsConstructor
@Controller
public class QuestionController {  
 private final QuestionService questionService;   
 @GetMapping(&quot;/list&quot;)
 public String list(Model model) {
    List&lt;Question&gt; questionList = this.questionService.getList();
    model.addAttribute(&quot;questionList&quot;, questionList);
    return &quot;question_list&quot;;
 }
 @GetMapping(value = &quot;/detail/{id}&quot;)
 public String detail(Model model, @PathVariable(&quot;id&quot;) Integer id) {
     Question question = this.questionService.getQuestion(id);
     model.addAttribute(&quot;question&quot;,question);
    return &quot;question_detail&quot;;
 }
 @GetMapping(&quot;/create&quot;)
 public String questionCreate() {
     return &quot;question_form&quot;;
 }
 @PostMapping(&quot;/create&quot;)
 public String questionCreate(@RequestParam(value=&quot;subject&quot;) String subject,
         @RequestParam(value=&quot;content&quot;) String content) {
     //TODO: 질문을 저장한다.
     this.questionService.create(subject, content);
     return &quot;redirect:/question/list&quot;;    // 질문 저장 후 질문 목록으로 이동
 }
}</code></pre>
</li>
<li>QuestionService.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
import com.mysite.sbb.DataNotFoundException;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class QuestionService {
 private final QuestionRepository questionRepository;  
 public List&lt;Question&gt; getList(){
    return this.questionRepository.findAll();
 }
 public Question getQuestion(Integer id) {
    Optional&lt;Question&gt; question = this.questionRepository.findById(id);
    if(question.isPresent()) {
       return question.get();
    }else {
       throw new DataNotFoundException(&quot;question not found&quot;);
    }
 }
 public void create(String subject, String content) {
    Question q = new Question();
    q.setSubject(subject);
    q.setContent(content);
    q.setCreateDate(LocalDateTime.now());
    this.questionRepository.save(q);
 }
}</code></pre>
</li>
<li>build.gradle에 <pre><code>implementation &#39;org.springframework.boot:spring-boot-starter-validation&#39;</code></pre>추가 후 refresh</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>QuestionController.java 수정<pre><code class="language-java">package com.mysite.sbb.question;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/question&quot;)
@RequiredArgsConstructor
@Controller
public class QuestionController {
 private final QuestionService questionService;   
 @GetMapping(&quot;/list&quot;)
 public String list(Model model) {
    List&lt;Question&gt; questionList = this.questionService.getList();
    model.addAttribute(&quot;questionList&quot;, questionList);
    return &quot;question_list&quot;;
 }   
 @GetMapping(value = &quot;/detail/{id}&quot;)
 public String detail(Model model, @PathVariable(&quot;id&quot;) Integer id) {
    Question question = this.questionService.getQuestion(id);
    model.addAttribute(&quot;question&quot;,question);
    return &quot;question_detail&quot;;
 }
 @GetMapping(&quot;/create&quot;)
 public String questionCreate() {
    return &quot;question_form&quot;;
 }
 @PostMapping(&quot;/create&quot;)
 public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult) {
    if(bindingResult.hasErrors()) {
       return &quot;question_form&quot;;
    }
    this.questionService.create(questionForm.getSubject(), questionForm.getContent());
    return &quot;redirect:/question/list&quot;;   // 질문 저장후 질문 목록으로 이동
 }
}</code></pre>
</li>
<li>question_form.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container&quot;&gt;
 &lt;h5 class=&quot;my-3 border-bottom pb-2&quot;&gt;질문등록&lt;/h5&gt;
 &lt;form th:action=&quot;@{/question/create}&quot; th:object=&quot;${questionForm}&quot; method=&quot;post&quot;&gt;
       &lt;div class=&quot;alert alert-danger&quot; role=&quot;alert&quot; th:if=&quot;${#fields.hasAnyErrors()}&quot;&gt;
          &lt;div th:each=&quot;err : ${#fields.allErrors()}&quot; th:text=&quot;${err}&quot; /&gt;
       &lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;content&quot; class=&quot;form-label&quot;&gt;내용&lt;/label&gt;
       &lt;textarea name=&quot;content&quot; id=&quot;content&quot; class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
    &lt;/div&gt;
    &lt;input type=&quot;submit&quot; value=&quot;저장하기&quot; class=&quot;btn btn-primary my-2&quot;&gt;
 &lt;/form&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre>
</li>
<li>AnswerForm.java 생성<pre><code class="language-java">package com.mysite.sbb.answer;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AnswerForm {    
  @NotEmpty(message = &quot;내용은 필수 항목입니다&quot;)
  private String content;
}</code></pre>
</li>
<li>AnswerController.java 수정<pre><code class="language-java">package com.mysite.sbb.answer;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping(&quot;/answer&quot;)
@RequiredArgsConstructor
@Controller
public class AnswerController {
 private final QuestionService questionService;
 private final AnswerService answerService;  
 @PostMapping(&quot;/create/{id}&quot;)
 public String createAnswer(Model model, @PathVariable(&quot;id&quot;) Integer id, 
       @Valid AnswerForm answerForm, BindingResult bindingResult) {
    Question question = this.questionService.getQuestion(id);
    if(bindingResult.hasErrors()) {
        model.addAttribute(&quot;question&quot;, question);
        return &quot;question_detail&quot;;
    }
    // TODO: 답변을 저장한다.
    this.answerService.create(question, answerForm.getContent());
    return String.format(&quot;redirect:/questiohn/detail/%s&quot;, id);
 }
}</code></pre>
*{...}현재 선택된 객체에 대한 속성에 접근할 때 사용된다.
th.object=&quot;${</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>form_errors.html 생성<pre><code class="language-html">&lt;div th:fragment=&quot;formErrorsFragment&quot; class=&quot;alert alert-danger&quot; role=&quot;alert&quot; th:if=&quot;${#fields.hasAnyErrors()}&quot;&gt;
 &lt;div th:each=&quot;err : ${#fields.allErrors()}&quot; th:text=&quot;${err}&quot; /&gt;
&lt;/div&gt;</code></pre>
</li>
<li>question_form.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container&quot;&gt;
 &lt;h5 class=&quot;my-3 border-bottom pb-2&quot;&gt;질문등록&lt;/h5&gt;
 &lt;form th:action=&quot;@{/question/create}&quot; th:object=&quot;${questionForm}&quot; method=&quot;post&quot;&gt;
  &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;subject&quot; class=&quot;form-label&quot;&gt;제목&lt;/label&gt;
       &lt;input type=&quot;text&quot; th:field=&quot;*{subject}&quot; id=&quot;subject&quot; class=&quot;form-control&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
       &lt;label for=&quot;content&quot; class=&quot;form-label&quot;&gt;내용&lt;/label&gt;
       &lt;textarea th:field=&quot;*{content}&quot; id=&quot;content&quot; class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
    &lt;/div&gt;
    &lt;input type=&quot;submit&quot; value=&quot;저장하기&quot; class=&quot;btn btn-primary my-2&quot;&gt;
 &lt;/form&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre>
</li>
<li>question_detail.html 수정<pre><code class="language-html">&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
 &lt;!-- 질문--&gt;
 &lt;h2 class=&quot;border-bottom py-2&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/h2&gt; 
 &lt;div class=&quot;card my-3&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
              &lt;div th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 개수 표시 --&gt;
 &lt;h5 class=&quot;border-bottom my-3 py-2&quot; th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
 &lt;!-- 답변 반복 시작 --&gt;
 &lt;div class=&quot;card my-3&quot; th:each=&quot;answer: ${question.answerList}&quot;&gt;
    &lt;div class=&quot;card-body&quot;&gt;
       &lt;div class=&quot;card-text&quot; style=&quot;white-space:pre-line;&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/div&gt;
       &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
          &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt; 
             &lt;div th:text=&quot;${#temporals.format(answer.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;   
          &lt;/div&gt;
       &lt;/div&gt;
       &lt;/div&gt;
    &lt;/div&gt;
 &lt;!-- 답변 반복 끝 --&gt;
 &lt;!-- 답변 작성--&gt;
 &lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; th:object=&quot;${answerForm}&quot; method=&quot;post&quot; class=&quot;my-3&quot;&gt;
    &lt;div th:replace=&quot;~{form_errors :: formErrorsFragment}&quot;&gt;&lt;/div&gt;
       &lt;textarea th:field=&quot;*{content}&quot; rows=&quot;10&quot; class=&quot;form-control&quot;&gt;&lt;/textarea&gt;
       &lt;input type=&quot;submit&quot; value=&quot;답변 등록&quot; class=&quot;btn btn-primary my-2&quot;&gt; &lt;/form&gt;
 &lt;/div&gt;
 &lt;/html&gt;</code></pre>
</li>
<li>파일 위치
<img src="https://velog.velcdn.com/images/hi_do/post/e98c6ea5-b737-4d70-9871-5af9322870cd/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot - JPA와 ORM]]></title>
            <link>https://velog.io/@hi_do/Spring-Boot-JPA%EC%99%80-ORM</link>
            <guid>https://velog.io/@hi_do/Spring-Boot-JPA%EC%99%80-ORM</guid>
            <pubDate>Mon, 17 Jun 2024 04:02:22 GMT</pubDate>
            <description><![CDATA[<h4 id="jpa로-데이터베이스-사용">JPA로 데이터베이스 사용</h4>
<blockquote>
</blockquote>
<h4 id="jpa란">JPA란?</h4>
<ul>
<li>자바 애플리케이션에서 관계형 데이터베이스와 상호작용하기 위한 표준 API</li>
<li>스프링부트는 JPA(Jav Persistence API)를 사용하여 데이터베이스를 관리한다.</li>
<li>스프링부트는 JPA를 ORM의 기술 표준으로 사용한다.</li>
<li>JPA는 인터페이스 모음이므로 이 인터페이스를 구현한 실제 클레스가 필요하다.</li>
<li>JPA를 구현한 실제 클래스에는 대표적으로 하이버네이트(Hibernate)가 있다.</li>
<li>하이버네이트는 JPA의 인터페이스를 구현한 실제 클래스이자 자바의 ORM 프레임워크로 부트에서 데이터베이스를 관리하기 쉽게 도와준다.</li>
</ul>
<blockquote>
<h4 id="orm이란">ORM이란?</h4>
</blockquote>
<ul>
<li>ORM은 SQL을 사용하지 않고 데이터베이스를 관리할 수 있는 도구</li>
<li>ORM은 데이터베이스의 테이블을 자바 클래스로 만들어 관리할 수 있다.<h4 id="sql로-작성시">SQL로 작성시</h4>
<pre><code>insert into question (id, subject, content) values (1, &#39;안녕하세요&#39;, &#39;가입 인사드립 니다 ^^);
insert into question (id, subject, content) values (2, &#39;질문 있습니다&#39;, &#39;ORM이 궁금합니다&#39;);</code></pre><h4 id="orm으로-작성시">ORM으로 작성시</h4>
<pre><code>Question q1 = new Question();
q1.setId(1);
q1.setSubject(&quot;안녕하세요&quot;);
q1.setContent(&quot;가입 인사드립니다 ^^);
this.questionRepository.save(q1);
Question g2 = new Question();
q2.setId(2);
q2.setSubject(&quot;질문 있습니다&quot;);
q2.setContent(&quot;ORM이 궁금합니다&quot;); this.questionRepository.save(q2);</code></pre></li>
</ul>
<blockquote>
<h4 id="orm의-장점">ORM의 장점</h4>
</blockquote>
<ul>
<li>ORM을 이용하면 MySQL, 오라클, db, MSSQL과 같은 DBMS의 종류에 관계없이 일관된 자바코드로 사용할 수 있어서 프로그램의 유지보수가 편리하다.<h4 id="dbms란">DBMS란?</h4>
</li>
<li>DBMS(Database Management System) 데이터베이스를 관리하는 소프트웨어이다.</li>
<li>DB와 DBMS를 구분하지 않고 사용하는 경우가 많은데 엄밀히 말하면 DB는 데이터를 담은 통이라고 할 수 있고, DBMS는 이 통을 관리하는 소프트웨어이다.</li>
</ul>
<h4 id="방문자들이-질문과-답변을-남길수-있는-게시판을-제작">방문자들이 질문과 답변을 남길수 있는 게시판을 제작</h4>
<blockquote>
</blockquote>
<ul>
<li>사용자의 질문이나 답변을 작성하면 데이터가 생성되는데 이러한 데이터를 관리하려면, 저장, 조회, 수정하는 등의 기능을 구현해야 한다.</li>
<li><blockquote>
<p>데이터를 모으고 관리하는 저장소를 데이터베이스라고 한다.</p>
</blockquote>
</li>
<li>데이터베이스를 관리하려면 SQL이라는 언어를 사용해야 한다.</li>
<li><blockquote>
<p>스프링부트와 달리 데이터베이스는 자바를 이해하지 못한다.</p>
</blockquote>
</li>
<li><blockquote>
<p>하지만 ORM(Object Relational Mapping)이라는 도구를 사용하면 자바 문법으로도 데이터베이스를 다룰 수 있다. ORM을 사용하면 개발자는 SQL을 직접 작성하지 않아도 데이터베이스의 데이터를 처리할 수 있다.</p>
</blockquote>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>build.gradle에<pre><code>runtimeOnly &#39;com.h2database:h2&#39;</code></pre>추가하고 build.gradle refresh하기</li>
<li>application.properties 수정<pre><code class="language-java">spring.application.name=sbb
# DATABASE
#h2콘솔로 접속할 것인지 여부
spring.h2.console.enabled=true
#h2콜솔로 접속하기 위한 경로
spring.h2.console.path=/h2-console
#데이터베이스에 접속하기 위한 경로
spring.datasource.url=jdbc:h2:~/local
#데이터베이스에 접속할때 사용하는 드라이버 클래스 명
spring.datasource.driver-class-name=org.h2.Driver
#데이터베이스에 접속할때 사용하는 아이디
spring.datasource.username=sa
#데이터베이스에 접속할때 사용하는 패스워드 
spring.datasource.password=</code></pre>
</li>
<li>database 만들기
<img src="https://velog.velcdn.com/images/hi_do/post/05b9d252-d48c-4638-ae6b-579354594713/image.png" alt=""></li>
<li>build.gradle refresh
<img src="https://velog.velcdn.com/images/hi_do/post/52ee9878-9bb7-4904-9e19-a35bf25256f8/image.png" alt=""></li>
<li>결과
<img src="https://velog.velcdn.com/images/hi_do/post/681857a7-3fc7-4af3-806b-a807e8383dfe/image.png" alt=""></li>
<li>변경 후 연결 클릭
<img src="https://velog.velcdn.com/images/hi_do/post/4d42c513-08bd-476e-8614-00d7beba0c45/image.png" alt=""></li>
<li>데이터베이스 연결된 결과
<img src="https://velog.velcdn.com/images/hi_do/post/f3b8ab3d-79c2-4945-9f37-8e4003b5c2ae/image.png" alt=""></li>
<li>build.gradle에<pre><code>implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;</code></pre></li>
<li><blockquote>
<p>추가후 refreash</p>
</blockquote>
</li>
<li>application.properties 에 JPA추가<pre><code class="language-java">spring.application.name=sbb
# DATABASE
#h2콘솔로 접속할 것인지 여부
spring.h2.console.enabled=true
#h2콜솔로 접속하기 위한 경로
spring.h2.console.path=/h2-console
#데이터베이스에 접속하기 위한 경로
spring.datasource.url=jdbc:h2:~/local
#데이터베이스에 접속할때 사용하는 드라이버 클래스 명
spring.datasource.driver-class-name=org.h2.Driver
#데이터베이스에 접속할때 사용하는 아이디
spring.datasource.username=sa
#데이터베이스에 접속할때 사용하는 패스워드 
spring.datasource.password=
# JPA
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update</code></pre>
</li>
</ul>
<h4 id="springjpahibernateddl-auto의-규칙">spring.jpa.hibernate.ddl-auto의 규칙</h4>
<blockquote>
</blockquote>
<ul>
<li>none: 엔티티가 변경되더라도 데이터베이스를 변경하지 않는다.</li>
<li>update: 엔티티의 변경된 부분만 데이터베이스에 적용한다.</li>
<li>validate: 엔티티와 테이블 간에 차이점이 있는지 검사만 한다.</li>
<li>create: 스프링 부트 서버를 시작할 때 테이블을 모두 삭제한 후 다시 생성한다.</li>
<li>create-drop: create와 동일하지만 스프링 부트 서버를 종료할 때에도 테이블을 모두 삭제한다.</li>
</ul>
<h4 id="entitiy구성">Entitiy구성</h4>
<blockquote>
</blockquote>
<p>Question(id, subject, content, createDate)
Answer</p>
<ul>
<li>Question.java 생성<pre><code class="language-java">package com.mysite.sbb;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class Question {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;    
  @Column(length = 200)
  private String subject;
  @Column(columnDefinition = &quot;TEXT&quot;)
  private String content;    
  private LocalDateTime createDate;
}</code></pre>
</li>
<li>Answer.java 생성<pre><code class="language-java">package com.mysite.sbb;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class Answer {
  @Id        // 기본키. 각 데이터들을 구분하는 유효한 값.(중복 불가능)
  @GeneratedValue(strategy = GenerationType.IDENTITY)    //고유한 번호를 생성하는 방법.
  private Integer id;
  //열 이름 텍스트를 열 데이터로 넣을 수 있다. 글자수를 제한할 수 없는 경우에 사용.
  @Column(columnDefinition = &quot;TEXT&quot;)
  private String content;
  private LocalDateTime createDate;
  // Question을 참조해야 하기 위해.
  @ManyToOne
  private Question question;
}</code></pre>
</li>
<li>결과
<img src="https://velog.velcdn.com/images/hi_do/post/492dec71-90d6-46cf-96d6-4828903cdf53/image.png" alt=""><h4 id="답변을-통해-질문의-제목을-알고-싶다">답변을 통해 질문의 제목을 알고 싶다</h4>
</li>
<li>answer.getQuestion().getSubject() -&gt; 접근할 수 있다.<pre><code>@ManyToOne
private Question question;</code></pre></li>
<li><blockquote>
<p>하나의 질문에 답변은 여러개가 달릴 수 있다. 
따라서 답변은 Many가 되고 질문은 One이 된다. 
@ManyToOne을 통해서 N:1관계를 나타낼 수 있다.
이렇게 @ManyToOne을 설정하면 Answer 엔티티의 question속성과 Question 엔티티가 서로 연결된다(실제 데이터베이스에서는 (외래키_foreign key)관계가 생성된다.)</p>
</blockquote>
<h4 id="manytoone은-부모-자식-관계를-갖는-구조에서-사용한다">@ManyToOne은 부모 자식 관계를 갖는 구조에서 사용한다.</h4>
</li>
<li>여기서 부모는 Question, 자식은 Answer라고 할 수 있다.</li>
<li><blockquote>
<p>외래키란 테이블과 테이블 사이의 관계를 구성할때 연결되는 열을 의미한다.</p>
</blockquote>
<h4 id="반대로-질문에서-답변을-참조할-수-없을까---있다">반대로 질문에서 답변을 참조할 수 없을까? -&gt; 있다</h4>
</li>
<li>답변과 질문이 N:1관계라면 질문과 답변은 1:N관계라고 할 수 있다.</li>
<li><blockquote>
<p>@OneToMany를 사용하면 된다
질문 하나에 답변은 여러개 이므로 Question 추가할 Answer속성 타입은 List형태로 구성해야 한다.</p>
</blockquote>
</li>
<li>Question.java에 <pre><code class="language-java">@OneToMany(mappedBy = &quot;question&quot;, cascade = CascadeType.REMOVE)
private List&lt;Answer&gt; answerList;</code></pre>
추가
Qustion.java<pre><code class="language-java">package com.mysite.sbb;
import java.time.LocalDateTime;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class Question {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;    
  @Column(length = 200)
  private String subject;    
  @Column(columnDefinition = &quot;TEXT&quot;)
  private String content;    
  private LocalDateTime createDate;    
  @OneToMany(mappedBy = &quot;question&quot;, cascade = CascadeType.REMOVE)
  private List&lt;Answer&gt; answerList;    // question.getAnswerList()
}</code></pre>
</li>
<li><blockquote>
<p>질문에 답변을 참조하려면 question.getAnswerList()를 호출하면 된다.
@OneToMany에 사용된 mappedBy 속성은 참조 엔티티의 속성명을 정의한다.</p>
</blockquote>
</li>
<li><blockquote>
<p>즉, Answer 엔티티에서 Question 엔티티를 참조한 속성인 question을 mappedBy에 전달해야 한다.</p>
</blockquote>
</li>
<li>Cascade = CascadeType.REMOVE
게시판에서는 질문 하나에 답변이 여러개 작성될 수 있다. 그런데 보통 게시판 서비스에서는 질문을 삭제하면 그에 달린 답변들도 함께 삭제된다. 그렇게 만들기 위해 적용하는 속성이다.</li>
</ul>
<h4 id="repository로-데이터베이스-관리">repository로 데이터베이스 관리</h4>
<blockquote>
</blockquote>
<ul>
<li>entity만으로는 테이블의 데이터를 저장, 조회, 수정, 삭제 등을 할 수 없다.
이런 데이터를 관리하려면 데이터베이스와 연동하는 JPA repository가 있어야 한다.</li>
<li>QuestionRepository.java(interface) 생성<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
// CRUD함수를 JpaRepository가 들고 있음.
// @Repository라는 에너테이션이 없어도 IoC된다. 이유는 JpaRepository를 상속했기 때문
public interface QuestionRepository  extends JpaRepository&lt;Question, Integer&gt;{
}</code></pre>
</li>
<li><blockquote>
<p>Question 엔티티를 리포지터리로 생성한다는 의미. Question 엔티티의 기본키가 integer타입.</p>
</blockquote>
</li>
<li>AnswerRepository.java(interface) 생성<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AnswerRepository extends JpaRepository&lt;Answer, Integer&gt;{
}</code></pre>
</li>
<li>build.gradle에<pre><code class="language-java">testImplementation &#39;org.junit.jupiter:junit-jupiter&#39; 
 testRuntimeOnly &#39;org.junit.platform:junit-platform-launcher&#39;</code></pre>
추가 후 refresh</li>
<li>IoC(Inversoin of Control) : Spring에서 객체의 생성과 관리를 Spring 컨테이너가 담당
(이전에는 개발자가 객체의 생성과 생명주기를 직접 관리)</li>
<li>DI(Dependency Injection) : 필요로하는 객체를 생성하거나 검색하지 않고 외부로부터 주입.
Spring의 IoC컨테이너는 애플리케이션의 객체를 생성하고 관리하는데, 이때 의존성주입(DI)를 통해 필요한 Bean을 주입한다.</li>
<li>SbbApplicationTests.java 수정<pre><code class="language-java">package com.mysite.sbb;
import java.time.LocalDateTime;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {    
  @Autowired
  private QuestionRepository questionRepository;
  @Test
  void testJpa() {
      Question q1 = new Question();
      q1.setSubject(&quot;sbb가 무엇인가요?&quot;);
      q1.setContent(&quot;sbb에 대해서 알고 싶습니다&quot;);
      q1.setCreateDate(LocalDateTime.now());
      this.questionRepository.save(q1);
      Question q2 = new Question();
      q2.setSubject(&quot;스프링 부트 모델 질문입니다&quot;);
      q2.setContent(&quot;id는 자동으로 생성되나요?&quot;);
      q2.setCreateDate(LocalDateTime.now());
      this.questionRepository.save(q2);
  }
}</code></pre>
<img src="https://velog.velcdn.com/images/hi_do/post/0cbac4ed-dd20-4088-99ac-fcd4f0ff015e/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/c639451e-fe3b-4579-8b1d-c11e9c497dfe/image.png" alt=""></li>
<li><blockquote>
<p>서버가 이미 켜져있기 때문에 에러
<img src="https://velog.velcdn.com/images/hi_do/post/247ddfe4-2156-46db-9b70-47b2c3844fb8/image.png" alt=""></p>
</blockquote>
</li>
<li><blockquote>
<p>서버 종료 후 다시 실행하면 에러 없음 -&gt; JUnit Test 실행 후 서버 다시 실행</p>
</blockquote>
</li>
<li>입력 및 실행 결과
<img src="https://velog.velcdn.com/images/hi_do/post/197f2cd3-0376-4360-961e-76c0cb3153dd/image.png" alt=""></li>
<li>SbbApplicationTests.java 수정<pre><code class="language-java">package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {    
  @Autowired
  private QuestionRepository questionRepository;
  @Test
  void testJpa() {
          List&lt;Question&gt; all = this.questionRepository.findAll();
          // assertEquals(기댓값, 실제값) 동일한지 조사.
      assertEquals(2, all.size());        // 데이터개수        
      Question q = all.get(0);        // 첫번째
      assertEquals(&quot;sbb가 무엇인가요?&quot;, q.getSubject());
      }        
  }</code></pre>
</li>
<li>SbbApplicationTests.java 수정<pre><code class="language-java">package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {    
  @Autowired
  private QuestionRepository questionRepository;
  @Test
  void testJpa() {
      // 값이 있는지 없는지에 대한 처리를 확인하는 클래스. (null값을 유연하게 처리)
      // isPresent로 값이 존재하는지 확인할 수 있다.
      // isPresent를 통해 값이 존재한다면 get()을 통해 실제 Question의 값을 얻는다.
      Optional&lt;Question&gt; oq = this.questionRepository.findById(1);
      if(oq.isPresent()) {
          Question q = oq.get();
          assertEquals(&quot;sbb가 무엇인가요?&quot;, q.getSubject());
      }
      }        
  }</code></pre>
</li>
<li>QuestionRepository.java (interface) 수정<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
// CRUD함수를 JpaRepository가 들고 있음.
// @Repository라는 에너테이션이 없어도 IoC된다. 이유는 JpaRepository를 상속했기 때문
public interface QuestionRepository  extends JpaRepository&lt;Question, Integer&gt;{
  // findBy~
  // select * from question where subject = ?
  Question findBySubject(String subject);    //Jpa Query methods
}</code></pre>
</li>
<li>SbbApplicationTests.java 수정<pre><code class="language-java">package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {    
  @Autowired
  private QuestionRepository questionRepository;
  @Test
  void testJpa() {
      Question q = this.questionRepository.findBySubject(&quot;sbb가 무엇인가요?&quot;);
      assertEquals(1, q.getId());
      }
  }</code></pre>
</li>
<li>application.properties에<pre><code class="language-java">spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true</code></pre>
<pre><code class="language-java">spring.application.name=sbb
# DATABASE
#h2콘솔로 접속할 것인지 여부
spring.h2.console.enabled=true
#h2콜솔로 접속하기 위한 경로
spring.h2.console.path=/h2-console
#데이터베이스에 접속하기 위한 경로
spring.datasource.url=jdbc:h2:~/local
#데이터베이스에 접속할때 사용하는 드라이버 클래스 명
spring.datasource.driver-class-name=org.h2.Driver
#데이터베이스에 접속할때 사용하는 아이디
spring.datasource.username=sa
#데이터베이스에 접속할때 사용하는 패스워드 
spring.datasource.password=
# JPA
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true</code></pre>
추가 후 결과 -&gt; console창에 결과가 나옴
<img src="https://velog.velcdn.com/images/hi_do/post/ac7ca2d4-715d-4b5f-a9f3-7a7348b407d0/image.png" alt=""></li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>subject와 content를 함께 조회</li>
<li><blockquote>
<p>findBySubjectAndContent</p>
</blockquote>
</li>
<li>QuestionRepository.java(interface) 수정<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
// CRUD함수를 JpaRepository가 들고 있음.
// @Repository라는 에너테이션이 없어도 IoC된다. 이유는 JpaRepository를 상속했기 때문
public interface QuestionRepository  extends JpaRepository&lt;Question, Integer&gt;{
  // findBy~
  // select * from question where subject = ?
  Question findBySubject(String subject);    //Jpa Query methods
  Question findBySubjectAndContent(String subject, String content);
}</code></pre>
</li>
<li>SbbApplicationTests.java 수정<pre><code class="language-java">package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {    
  @Autowired
  private QuestionRepository questionRepository;
  @Test
  void testJpa() {
      Question q = this.questionRepository
              .findBySubjectAndContent(&quot;sbb가 무엇인가요?&quot;, &quot;sbb에 대해서 알고 싶습니다.&quot;);
      assertEquals(1, q.getId());
      }
  }</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/eaec78ec-b938-453e-9663-df9da92a49a6/image.png" alt=""></li>
<li>쿼리메소드 검색사이트
<a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html">https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html</a></li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>subject 열값들 중에 특정 문자열을 포함하는 데이터를 조회</li>
<li><blockquote>
<p>Like를 사용
List[Question] findBySubjectLike(String subject);</p>
</blockquote>
</li>
<li>QuestionRepository.java(interface) 수정<pre><code class="language-java">package com.mysite.sbb;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
// CRUD함수를 JpaRepository가 들고 있음.
// @Repository라는 에너테이션이 없어도 IoC된다. 이유는 JpaRepository를 상속했기 때문
public interface QuestionRepository  extends JpaRepository&lt;Question, Integer&gt;{
  // findBy~
  // select * from question where subject = ?
  Question findBySubject(String subject);    //Jpa Query methods
  Question findBySubjectAndContent(String subject, String content);
  List&lt;Question&gt; findBySubjectLike(String subject);
}</code></pre>
</li>
<li>SbbApplicationTests.java 수정<pre><code class="language-java">package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {    
  @Autowired
  private QuestionRepository questionRepository;
  @Test
  void testJpa() {
      List&lt;Question&gt; qList = this.questionRepository.findBySubjectLike(&quot;sbb%&quot;);
      Question q = qList.get(0);
      assertEquals(&quot;sbb가 무엇인가요?&quot;, q.getSubject());
      }
  }</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/05878e76-4d3f-4547-afec-2f68e3e54d69/image.png" alt=""></li>
<li><blockquote>
<p>q1_0.subject like ? escape&#39;&#39;
escape&#39;&#39; 구문은 SQL쿼리에서 사용되는 LIKE 연산자와 함께 쓰이는데, 특정 문자가 와일드 카드로 작동하는 것을 방지하기 위해 사용된다. &quot;sbb%&quot; % 기호가 텍스트로 들어가는 것을 방지하기 위해.</p>
</blockquote>
</li>
<li>sbb% : sbb로 시작하는 문자열</li>
<li>%sbb : sbb로 끝나는 문자열</li>
<li>%sbb% : sbb를 포함하는 문자열</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>SbbApplicationTests.java 수정<pre><code class="language-java">package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {    
  @Autowired
  private QuestionRepository questionRepository;
  @Test
  void testJpa() {
      Optional&lt;Question&gt; oq = this.questionRepository.findById(1);
      assertTrue(oq.isPresent());    //괄호 안의 값이 TRUE인지를 테스트
      Question q = oq.get();
      q.setSubject(&quot;수정된 제목&quot;);
      this.questionRepository.save(q);
      }
  }</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/f58ee218-dfd6-4db5-899c-1b83030bd374/image.png" alt=""></li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>SbbApplicationTests.java 수정<pre><code class="language-java">package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDateTime;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
 @Autowired
 private QuestionRepository questionRepository;   
 @Autowired
 private AnswerRepository answerRepository;
 @Test
 void testJpa() {
    Optional&lt;Question&gt; oq = this.questionRepository.findById(2);
    assertTrue(oq.isPresent());
    Question q = oq.get();  
    Answer a = new Answer();
    a.setContent(&quot;네 자동으로 생성됩니다.&quot;);
    a.setQuestion(q);
    a.setCreateDate(LocalDateTime.now());
    this.answerRepository.save(a);
    }
 }</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/cf11f0cc-9926-4caa-ab08-0ee8a8335016/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/e8728f66-edd5-437e-9195-6cb90e87d566/image.png" alt=""></li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>SbbApplicationTests.java 수정<pre><code class="language-java">package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
 @Autowired
 private QuestionRepository questionRepository;  
 @Autowired
 private AnswerRepository answerRepository;
 @Test
 void testJpa() {
   Optional&lt;Answer&gt; oq = this.answerRepository.findById(1);
   assertTrue(oq.isPresent());
   Answer a = oq.get();
   assertEquals(2, a.getQuestion().getId());
    }
 }</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/845b1c62-f609-4581-ae4f-9b0bd256f946/image.png" alt=""></li>
<li><blockquote>
<p>left join : 연관된 데이터를 접근하기 위한 방법
Answer와 Question은 데이터베이스 내에서 연관관계를 가지고 있다.
Answer가 Question을 참조하는 외래키(question_id)를 포함한다. left join을 사용하면 Answer객체를 조회할때 연관된 Question 객체의 정보도 함께 불러올 수 있다.</p>
</blockquote>
</li>
</ul>
<h4 id="답변-데이터를-통해-질문-데이터-찾기-vs-질문-데이터를-통해-답변-데이터-찾기">답변 데이터를 통해 질문 데이터 찾기 vs 질문 데이터를 통해 답변 데이터 찾기</h4>
<blockquote>
</blockquote>
<ul>
<li>a.getQuesetion() : 답변을 통해서 질문 찾기<h4 id="질문-데이터를-통해-답변-찾기---answerlist를-사용">질문 데이터를 통해 답변 찾기 -&gt; answerList를 사용</h4>
<blockquote>
</blockquote>
</li>
<li>org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.mysite.sbb.Question.answerList: could not initialize proxy - no Session</li>
<li><blockquote>
<p>QuestionRepository가 findById메서드를 통해 Question 객체를 조회하면 DB세션이 끊어진다.
== DB세션이란 스프링부트 애플리케이션과 데이터베이스 간의 연결</p>
</blockquote>
</li>
<li>answerList는 앞서 q객체를 조회할때가 아니라 q.getAnswerList()메서드를 호출하는 시점에 가져오기 때문에 이와 같은 오류가 발생하는 것이다.
== 이렇게 데이터를 필요한 시점에 가져오는 방식을 지연(Lazy)방식이라고 한다.</li>
<li>이와 반대로 q객체를 조회할 때 미리 answer 리스트를 모두 가져오는 방식은 즉시(Eager)방식이라고 한다.
@OneToMany, @ManyToOne 옵션으로 fetch=FetchType.Lazy 또는 fetch=Fetchtype.EAGER 가져오는 방식을 설정할 수 있다.</li>
<li><blockquote>
<p>이러한 문제는 테스트 코드에서만 발생한다. 실제 서버에서 JPA프로그램들을 실행할때는 DB세션이 종료되지 않아 오류가 발생하지 않는다.
테스트 코드를 수행할때 이런 오류를 방지할 수 있는 방법은 @Transactional 사용하면 된다.</p>
</blockquote>
</li>
<li>@Transactional은 메서드가 종료될 때까지 DB세션을 유지한다.</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>SbbApplicationTests.java 수정<pre><code class="language-java">package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import jakarta.transaction.Transactional;
@SpringBootTest
class SbbApplicationTests {   
 @Autowired
 private QuestionRepository questionRepository;  
 @Autowired
 private AnswerRepository answerRepository;
 @Transactional
 @Test
 void testJpa() {
   Optional&lt;Question&gt; oq = this.questionRepository.findById(2);
   assertTrue(oq.isPresent());
   Question q = oq.get();    
   List&lt;Answer&gt; answerList = q.getAnswerList();
   assertEquals(1, answerList.size());
   assertEquals(&quot;네 자동으로 생성됩니다.&quot;, answerList.get(0).getContent());
    }
 }</code></pre>
</li>
<li>실행 및 결과
<img src="https://velog.velcdn.com/images/hi_do/post/5bb25a48-6dfe-48d1-a025-d8971b789953/image.png" alt=""></li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>QuestionController.java 생성<pre><code class="language-java">package com.mysite.sbb.question;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class QuestionController {  
 @GetMapping(&quot;/question/list&quot;)
 public String list() {
    return &quot;question_list&quot;;
 }
}</code></pre>
</li>
<li>build.gradle에 <pre><code class="language-java"> implementation &#39;org.springframework.boot:spring-boot-starter-thymeleaf&#39;
 implementation &#39;nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect&#39;</code></pre>
추가 후 refresh</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SpringBoot - 초기 설정]]></title>
            <link>https://velog.io/@hi_do/SpringBoot-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@hi_do/SpringBoot-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Mon, 17 Jun 2024 03:36:15 GMT</pubDate>
            <description><![CDATA[<h4 id="스프링-부트">스프링 부트?</h4>
<blockquote>
</blockquote>
<ul>
<li>웹 프로그램(웹 애플리케이션)을 쉽고 빠르게 만들 수 있도록 도와주는 자바의 웹 프레임워크</li>
<li>웹 프로젝트를 완성하기 위해서 많은 기능을 생성해야 한다.
(쿠키처리, 로그아웃, 로그인, 데이터베이스 처리 등등)</li>
<li><blockquote>
<p>웹 프레임워크에는 기능들이 이미 만들어져 있다.</p>
</blockquote>
</li>
<li><blockquote>
<p>웹 프레임워크에 있는 기능을 익혀서 사용하기만 하면되고 흐름을 정확하게 파악하면 된다.</p>
</blockquote>
<h4 id="스프링-부트는-웹-보안적인-부분에서-굉장히-튼튼한-웹-프레임워크이다">스프링 부트는 웹 보안적인 부분에서 굉장히 튼튼한 웹 프레임워크이다.</h4>
</li>
<li><blockquote>
<p>SQL 인젝션 : 악의적인 SQL을 주입하여 공격하는 방법,
XSS(crose site scripting) : 자바스크립트를 삽입해 공격하는 방법,
CSRF(Cross Site Request Forgery) : 위조된 요청을 보내는 공격방법,
클릭재킹(clickjacking): 사용자가 의도하지 않은 클릭을 유도하는 공격방법
보안공격을 막아준다.</p>
</blockquote>
</li>
</ul>
<h4 id="프레임워크와-라이브러리의-차이점">프레임워크와 라이브러리의 차이점?</h4>
<blockquote>
</blockquote>
<ul>
<li>프레임워크 </li>
<li><blockquote>
<p>구조 제공: 프레임워크는 소프트웨어의 기본 구조를 제공하며 개발자가 그 구조에 맞춰 코드를 작성
개발의 뼈대를 제공하며, 개발자가 프레임워크의 규칙과 흐름에 따라 개발을 진행</p>
</blockquote>
</li>
<li>라이브러리</li>
<li><blockquote>
<p>도구 모음: 라이브러리는 특정 기능을 수행하는 도구나 함수들의 모음
개발자가 필요에 따라 호출해서 사용</p>
</blockquote>
</li>
</ul>
<h4 id="스프링-부트는-was가-필요없다">스프링 부트는 WAS가 필요없다.</h4>
<blockquote>
</blockquote>
<ul>
<li>스프링만 사용하여 웹 애플리케이션을 개발한다면 실행할 수 있는 톰캣과 같은 WAS(Web Application Server)가 필요하다. 스프링부트에서는 톰캣 서버가 내장되어 있고 설정도 자동 적용이 되기때문에 WAS에 대해서 전혀 신경쓸 필요가 없다.</li>
</ul>
<h4 id="프로젝트-생성">프로젝트 생성</h4>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/35ef9ecc-9661-4425-8d26-6d8c12430ff4/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/92abd961-bf8b-43fb-aed1-9cb5dab0b217/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/336f3486-283f-4de2-b885-ccd7bedf3fee/image.png" alt=""></p>
<h4 id="웹에서-만들어서-프로그램으로-실행할-경우">웹에서 만들어서 프로그램으로 실행할 경우</h4>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/469344c0-34b4-4a2a-b7c6-235b8d5277d1/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/f480b05c-5073-4e1e-a91a-e60eca2af99d/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/10fd9499-a135-42c5-a5a0-1a3402e3c419/image.png" alt=""></p>
<h4 id="초기-에러가-나올-경우">초기 에러가 나올 경우</h4>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/219016f9-1112-4d34-a16d-afd6bb3937d2/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/b32a957a-73cd-4d95-be7a-dccca6545dfd/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/ee93bc17-882f-445f-a63e-3bafe58fdfcd/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/415b84c5-3d72-4fc2-8a3d-bebd0f85d0b3/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/10bf2ee2-a57b-4bb5-aa38-6c536d04e7c0/image.png" alt=""></p>
<h4 id="클라이언트와-서버-구조">클라이언트와 서버 구조</h4>
<blockquote>
</blockquote>
<ul>
<li>웹브라우저(요청) -&gt; 웹서버가 가지고 있는 html문서 (응답) -&gt; html문서를 클라이언트
예를 들어 크롬 브라우저에서 서버에 요청을 보낼때는 서버의 주소(IP주소) 또는 서버의 주소를 대체할 수 있는 도메인명을 알아야 한다.
예를 들어 주소창에 naver.com을 입력하면 네이버에서 운용하는 웹 서버가 호출되고 서버는 요청에 대한 응답을 브라우저에 돌려준다.
즉, 웹 서버는 요청에 대한 응답으로 html문서나 다른 resourcese들을 브라우저에 표시한다.</li>
<li>프로토콜 : http의 서비스 내용은 웹서비스 포트번호 80</li>
<li>https의 서비스 내용 SSL을 적용한 웹 서비스 포트번호 443</li>
<li>FTP의 서비스 내용 파일 전송 포트 21
ex) <a href="https://www.naver.com/:443">https://www.naver.com/:443</a></li>
<li><blockquote>
<p>필요에 따라 포트 번호를 변경할 수 있다. 예를 들어 HTTP의 포트번호는 80대신 8080을 사용할 수도 있다.</p>
</blockquote>
</li>
</ul>
<h4 id="localhost8080">localhost:8080</h4>
<blockquote>
</blockquote>
<ul>
<li>localhost라는 도메인명이 127.0.0.1이라는 ip주소를 의미</li>
</ul>
<p>127.0.0.1 ip주소는 내컴퓨터를 의미한다.</p>
<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
    @GetMapping(&quot;hello&quot;) //localhost:8080/hello
    @ResponseBody
    public String hello() {
        return &quot;Hello World&quot; ;
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/hi_do/post/349cf1dd-cb08-492a-958e-458011b2ed95/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/12639c08-edad-4bc3-acd9-b0e995f34165/image.png" alt=""></p>
<ul>
<li>developmentOnly : 해당 라이브러리는 개발 환경에만 적용된다는 의미
운영 환경에 배포되는 jar, war 파일에는 이 라이브러리가 포함되지 않는다.
<img src="https://velog.velcdn.com/images/hi_do/post/a612dd51-3ba6-4987-95dd-d083074d55af/image.png" alt=""></li>
<li><blockquote>
<p>intellij에서는 필수
<img src="https://velog.velcdn.com/images/hi_do/post/cae19aff-8a4f-49cd-a0d7-31e673cf048c/image.png" alt=""></p>
</blockquote>
</li>
<li><blockquote>
<p>이렇게 나와야 적용완료</p>
</blockquote>
</li>
</ul>
<h4 id="lombok-다운로드">Lombok 다운로드</h4>
<blockquote>
</blockquote>
<ul>
<li><p>Lombok 이란?
Lombok은 자바 개발을 위한 라이브러리로, 반복적인 코드를 자동으로 생성해 주어 개발 생산성을 높여주는 도구이다. 자바 개발에서는 보일러플레이트 코드(반복적이고 지루한 코드)가 많이 발생하는데, Lombok을 사용하면 이러한 코드를 줄일 수 있다.
<a href="https://projectlombok.org/download">https://projectlombok.org/download</a>
<img src="https://velog.velcdn.com/images/hi_do/post/baff549a-6b8f-4418-8ac8-2cf9ff1c0adc/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/9da9aac0-d971-409e-9d2e-50ad9181a871/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/f927b6d9-00f3-4aac-9a4a-d3aae4ebc879/image.png" alt=""></p>
</li>
<li><blockquote>
<p>입력시 설치 창이 뜬다
<img src="https://velog.velcdn.com/images/hi_do/post/286fd58b-5165-44b4-8d64-ddb37841319e/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/f93fdd57-8177-4230-bef3-65e02826119a/image.png" alt="">
<img src="https://velog.velcdn.com/images/hi_do/post/fa90949f-2bf8-4be1-88fb-af6d6cf2d767/image.png" alt=""></p>
</blockquote>
</li>
<li><blockquote>
<p>Install successful 확인</p>
</blockquote>
</li>
<li><p>build.grade에 </p>
<pre><code>compileOnly &#39;org.projectlombok:lombok&#39;
annotationProcessor &#39;org.projectlombok:lombok&#39;</code></pre><p>추가 후 </p>
</li>
<li><blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/9e2bc789-a56a-4bb8-a913-7d6e5f33d953/image.png" alt=""></p>
</li>
<li><p>HelloLombok.java 생성
<img src="https://velog.velcdn.com/images/hi_do/post/ee6eb668-1343-448e-97ad-90ad74575a9f/image.png" alt=""></p>
<pre><code class="language-java">package com.mysite.sbb;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public class HelloLombok {
 private final String hello;
 private final int lombok;  
//    // 모든 final 필드를 초기화하는 생성자
//    public HelloLombok(String hello, int lombok) {
//        this.hello = hello;
//        this.lombok = lombok;
//    }
//   
 public static void main(String[] args) {
    HelloLombok helloLombok = new HelloLombok(&quot;헬로&quot;,5);     
    System.out.println(helloLombok.getHello());
    System.out.println(helloLombok.getLombok());
 }
}</code></pre>
</li>
<li><blockquote>
<p>lombok의 @RequiredArgsConstructor 덕분에 초기화하는 생성자를 따로 입력하지 않아도 된다.</p>
</blockquote>
</li>
</ul>
<h4 id="srcmainresources-디렉터리">src/main/resources 디렉터리</h4>
<blockquote>
</blockquote>
<ul>
<li>templates: html 형식 파일</li>
<li>static: css, javascript, jpg 등의 이미지 파일</li>
<li>application.properties 파일: 프로젝트의 환경을 설정 (데이터베이스 설정 등)</li>
<li>gradle의 Groovy를 기반으로 빌드: Ant, maven과 같은 이전 세대의 단점을 보안하고 장점을 취합해서 만든 환경 파일</li>
</ul>
<p>####</p>
<blockquote>
</blockquote>
<ul>
<li>MainController.java 생성<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class MainController {
  @GetMapping(&quot;/sbb&quot;)
  public void index() {
      System.out.println(&quot;index&quot;);
  }
}</code></pre>
</li>
<li>결과
<img src="https://velog.velcdn.com/images/hi_do/post/3aec48cb-e506-4e6b-8cdd-c9d8a4e7d4ae/image.png" alt="">
There was an unexpected error (type=Internal Server Error, status=500).</li>
<li><blockquote>
<p>localhost:8080/sbb를 요청하면 결과값을 리턴을 해야 하는데 아무값도 리턴하지 않아 500오류가 발생한 것이다. 즉, 오류를 해결하려면 응답을 리턴해야 한다.</p>
</blockquote>
</li>
<li>MainController.java 수정<pre><code class="language-java">package com.mysite.sbb;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class MainController {
  @GetMapping(&quot;/sbb&quot;)
  @ResponseBody
  public String index() {
      return &quot;index&quot;;
  }
}</code></pre>
</li>
<li>결과
<img src="https://velog.velcdn.com/images/hi_do/post/de1575b7-5971-463f-851c-b618523555a8/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring -  CRUD]]></title>
            <link>https://velog.io/@hi_do/Spring-CRUD</link>
            <guid>https://velog.io/@hi_do/Spring-CRUD</guid>
            <pubDate>Thu, 13 Jun 2024 08:58:56 GMT</pubDate>
            <description><![CDATA[<h3 id="스프링-프레임워크와-mybatis-스프링-mvc를-모두-결합한-프로젝트-진행">스프링 프레임워크와 MyBatis, 스프링 MVC를 모두 결합한 프로젝트 진행</h3>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hi_do/post/308fd6b7-ceaf-410b-9fd7-5f82622443e9/image.png" alt=""></p>
<h4 id="mybatis와-스프링을-이용한-영속처리">MyBatis와 스프링을 이용한 영속처리</h4>
<blockquote>
</blockquote>
<ul>
<li>영속처리: 데이터를 영구적으로 저장하고 유지
1) VO 선언
2) Mapper
3) xml
4) 테스트 코드</li>
</ul>
<h4 id="todo기능-개발">Todo기능 개발</h4>
<blockquote>
</blockquote>
<ul>
<li>등록작업 TodoMapper -&gt; TodoService -&gt; TodoController -&gt;jsp 순서로 진행<h4 id="todomapper">TodoMapper</h4>
</li>
<li>TodoMapper.java
<img src="https://velog.velcdn.com/images/hi_do/post/3c0e700f-4192-4fbf-ab62-c48a60998c19/image.png" alt=""></li>
<li>TodoMapper.xml
<img src="https://velog.velcdn.com/images/hi_do/post/2dfa67f9-31cc-4e25-b424-57cefc6b2fba/image.png" alt=""></li>
<li>TodoMapperTests.java
<img src="https://velog.velcdn.com/images/hi_do/post/f4562454-9040-4555-b923-375cfbde0fe6/image.png" alt=""><h4 id="todoservice">TodoService</h4>
</li>
<li>TodoService.java
<img src="https://velog.velcdn.com/images/hi_do/post/cedf402b-e5c7-4c74-871a-7afb45872769/image.png" alt=""></li>
<li>TodoServiceImpl.java
<img src="https://velog.velcdn.com/images/hi_do/post/b565d79b-8278-4fbf-9a64-dc146fec5214/image.png" alt=""></li>
<li>TodoServiceTests.java
<img src="https://velog.velcdn.com/images/hi_do/post/51ae2a1e-0fa0-4056-9a91-722f36babb95/image.png" alt=""><h4 id="todocontroller">TodoController</h4>
</li>
<li>TodoController.java<pre><code class="language-java">package org.zerock.springex.controller;
import org.zerock.springex.dto.TodoDTO;
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping(&quot;/todo&quot;)
@Log4j2
public class TodoController {
  @RequestMapping(&quot;/list&quot;)
  public void list() {
      log.info(&quot;todo list...............&quot;);
  }
  @GetMapping(&quot;/register&quot;)
  public void registerGET() {
      log.info(&quot;GET todo register~~~~~&quot;);
  }
  @PostMapping(&quot;/register&quot;)
  public String registerPost(@Valid TodoDTO todoDTO, BindingResult bindingResult,
                             RedirectAttributes redirectAttributes ) {
      log.info(&quot;POST todo register~~~~~&quot;);
      if(bindingResult.hasErrors()) {
          log.info(&quot;has errors!!!!!!!&quot;);
          redirectAttributes.addFlashAttribute(&quot;errors&quot;, bindingResult.getAllErrors());
          return &quot;redirect:/todo/register&quot;;
      }
      log.info(todoDTO);
      return &quot;redirect:/todo/list&quot;;
  }
}</code></pre>
<h4 id="jsp">jsp</h4>
</li>
<li>register.jsp<pre><code>&lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&gt;
&lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&gt;
&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;utf-8&quot;&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;
  &lt;!-- Bootstrap CSS --&gt;
  &lt;link href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot; integrity=&quot;sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3&quot; crossorigin=&quot;anonymous&quot;&gt;
  &lt;title&gt;Hello, world!&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;container-fluid&quot;&gt;
  &lt;div class=&quot;row&quot;&gt;
      &lt;!-- 기존의 &lt;h1&gt;Header&lt;/h1&gt; --&gt;
      &lt;div class=&quot;row&quot;&gt;
          &lt;div class=&quot;col&quot;&gt;
              &lt;nav class=&quot;navbar navbar-expand-lg navbar-light bg-light&quot;&gt;
                  &lt;div class=&quot;container-fluid&quot;&gt;
                      &lt;a class=&quot;navbar-brand&quot; href=&quot;#&quot;&gt;Navbar&lt;/a&gt;
                      &lt;button class=&quot;navbar-toggler&quot; type=&quot;button&quot; data-bs-toggle=&quot;collapse&quot; data-bs-target=&quot;#navbarNavAltMarkup&quot; aria-controls=&quot;navbarNavAltMarkup&quot; aria-expanded=&quot;false&quot; aria-label=&quot;Toggle navigation&quot;&gt;
                          &lt;span class=&quot;navbar-toggler-icon&quot;&gt;&lt;/span&gt;
                      &lt;/button&gt;
                      &lt;div class=&quot;collapse navbar-collapse&quot; id=&quot;navbarNavAltMarkup&quot;&gt;
                          &lt;div class=&quot;navbar-nav&quot;&gt;
                              &lt;a class=&quot;nav-link active&quot; aria-current=&quot;page&quot; href=&quot;#&quot;&gt;Home&lt;/a&gt;
                              &lt;a class=&quot;nav-link&quot; href=&quot;#&quot;&gt;Features&lt;/a&gt;
                              &lt;a class=&quot;nav-link&quot; href=&quot;#&quot;&gt;Pricing&lt;/a&gt;
                              &lt;a class=&quot;nav-link disabled&quot;&gt;Disabled&lt;/a&gt;
                          &lt;/div&gt;
                      &lt;/div&gt;
                  &lt;/div&gt;
              &lt;/nav&gt;
          &lt;/div&gt;
      &lt;/div&gt;
      &lt;!-- header end --&gt;
      &lt;!-- 기존의 &lt;h1&gt;Header&lt;/h1&gt;끝 --&gt;
      &lt;div class=&quot;row content&quot;&gt;
          &lt;div class=&quot;col&quot;&gt;
              &lt;div class=&quot;card&quot;&gt;
                  &lt;div class=&quot;card-header&quot;&gt;
                      Featured
                  &lt;/div&gt;
                  &lt;div class=&quot;card-body&quot;&gt;
                      &lt;form action=&quot;/todo/register&quot; method=&quot;post&quot;&gt;
                          &lt;div class=&quot;input-group mb-3&quot;&gt;
                              &lt;span class=&quot;input-group-text&quot;&gt;Title&lt;/span&gt;
                              &lt;input type=&quot;text&quot; name=&quot;title&quot; class=&quot;form-control&quot; placeholder=&quot;Title&quot;&gt;
                          &lt;/div&gt;
                          &lt;div class=&quot;input-group mb-3&quot;&gt;
                              &lt;span class=&quot;input-group-text&quot;&gt;DueDate&lt;/span&gt;
                              &lt;input type=&quot;date&quot; name=&quot;dueDate&quot; class=&quot;form-control&quot; placeholder=&quot;Writer&quot;&gt;
                          &lt;/div&gt;
                          &lt;div class=&quot;input-group mb-3&quot;&gt;
                              &lt;span class=&quot;input-group-text&quot;&gt;Writer&lt;/span&gt;
                              &lt;input type=&quot;text&quot; name=&quot;writer&quot; class=&quot;form-control&quot; placeholder=&quot;Writer&quot;&gt;
                          &lt;/div&gt;
                          &lt;div class=&quot;my-4&quot;&gt;
                              &lt;div class=&quot;float-end&quot;&gt;
                                  &lt;button type=&quot;submit&quot; class=&quot;btn btn-primary&quot;&gt;Submit&lt;/button&gt;
                                  &lt;button type=&quot;result&quot; class=&quot;btn btn-secondary&quot;&gt;Reset&lt;/button&gt;
                              &lt;/div&gt;
                          &lt;/div&gt;
                      &lt;/form&gt;
                      &lt;script&gt;
                          const serverValidResult = {}
                          &lt;c:forEach items=&quot;${errors}&quot; var=&quot;error&quot;&gt;
                          serverValidResult[&#39;${error.getField()}&#39;] = &#39;${error.defaultMessage}&#39;
                          &lt;/c:forEach&gt;
                          console.log(serverValidResult)
                      &lt;/script&gt;
                  &lt;/div&gt;
              &lt;/div&gt;
          &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;row content&quot;&gt;
      &lt;h1&gt;Content&lt;/h1&gt;
  &lt;/div&gt;
  &lt;div class=&quot;row footer&quot;&gt;
      &lt;!--&lt;h1&gt;Footer&lt;/h1&gt;--&gt;
      &lt;div class=&quot;row   fixed-bottom&quot; style=&quot;z-index: -100&quot;&gt;
          &lt;footer class=&quot;py-1 my-1 &quot;&gt;
              &lt;p class=&quot;text-center text-muted&quot;&gt;Footer&lt;/p&gt;
          &lt;/footer&gt;
      &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;script src=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js&quot; integrity=&quot;sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p&quot; crossorigin=&quot;anonymous&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre></li>
<li>web.xml<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;web-app xmlns=&quot;https://jakarta.ee/xml/ns/jakartaee&quot;
       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
       xsi:schemaLocation=&quot;https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd&quot;
       version=&quot;5.0&quot;&gt;
  &lt;context-param&gt;
      &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;
      &lt;param-value&gt;/WEB-INF/root-context.xml&lt;/param-value&gt;
  &lt;/context-param&gt;
  &lt;listener&gt;
      &lt;listener-class&gt;org.springframework.web.context.ContextLoaderListener&lt;/listener-class&gt;
  &lt;/listener&gt;
  &lt;servlet&gt;
      &lt;servlet-name&gt;appServlet&lt;/servlet-name&gt;
      &lt;servlet-class&gt;org.springframework.web.servlet.DispatcherServlet&lt;/servlet-class&gt;
      &lt;init-param&gt;
          &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;
          &lt;param-value&gt;/WEB-INF/servlet-context.xml&lt;/param-value&gt;
      &lt;/init-param&gt;
      &lt;init-param&gt;
          &lt;param-name&gt;throwExceptionIfNoHandlerFound&lt;/param-name&gt;
          &lt;param-value&gt;true&lt;/param-value&gt;
      &lt;/init-param&gt;
      &lt;load-on-startup&gt;1&lt;/load-on-startup&gt;
  &lt;/servlet&gt;
  &lt;servlet-mapping&gt;
      &lt;servlet-name&gt;appServlet&lt;/servlet-name&gt;
      &lt;url-pattern&gt;/&lt;/url-pattern&gt;
  &lt;/servlet-mapping&gt;
  &lt;filter&gt;
      &lt;filter-name&gt;encoding&lt;/filter-name&gt;
      &lt;filter-class&gt;org.springframework.web.filter.CharacterEncodingFilter&lt;/filter-class&gt;
      &lt;init-param&gt;
          &lt;param-name&gt;encoding&lt;/param-name&gt;
          &lt;param-value&gt;UTF-8&lt;/param-value&gt;
      &lt;/init-param&gt;
  &lt;/filter&gt;
  &lt;filter-mapping&gt;
      &lt;filter-name&gt;encoding&lt;/filter-name&gt;
      &lt;servlet-name&gt;appServlet&lt;/servlet-name&gt;
  &lt;/filter-mapping&gt;
&lt;/web-app&gt;</code></pre></li>
</ul>
<h4 id=""></h4>
<blockquote>
</blockquote>
<p>hibernate-validator
@NotNull: 필드나 메서드 매개변수가 null이 아니어야 함을 나타냅니다.
@Null: Null만 입력 가능
@NotEmpty: 문자열, 배열, 컬렉션 등이 비어 있지 않아야 함을 나타냅니다.
@NotBlank: 문자열 필드가 비어 있지 않아야 함을 나타냅니다.
@Pattern(regexp): 정규식 패턴을 사용하여 문자열의 형식을 검사합니다.
@Size(min, max): 문자열, 배열, 컬렉션의 길이 또는 크기에 대한 제약을 설정합니다.
@Min(value): 숫자 형식의 필드가 지정된 최소 값 이상이어야 함을 나타냅니다.
@Max(value): 숫자 형식의 필드가 지정된 최대 값 이하여야 함을 나타냅니다.
@Past: 날짜와 시간 필드가 현재 날짜 이전인지 확인합니다.
@Future:  날짜와 시간 필드가 이후인지 확인합니다.
@Positive: 양수만 가능
@PositiveOrZero: 양수와 0만 가능
@Negative: 음수만 가능
@NegativeOrZero: 음수와 0만 가능
@Email: 이메일 주소의 유효성을 검사합니다.
@AssertTrue, @AssertFalse: 논리 값 (true 또는 false)를 검사합니다.
@CreditCardNumber: 신용 카드 번호의 유효성을 검사합니다.
@Length(min, max): 문자열의 길이 제한을 설정합니다.
@Range(min, max): 숫자 필드가 지정된 범위 내에 있어야 함을 나타냅니다.</p>
<h4 id="valid를-이용한-검증">valid를 이용한 검증</h4>
<blockquote>
</blockquote>
<ul>
<li>유효성 검사를 자바스크립트를 했었지만 모바일과 같은 다양한 환경에서 서버를 이용하는 현재에는 브라우저를 사용하는 프론트쪽에서의 검증과 더불어 백서버에서도 입력되는 값들을 검증하는 것이 일반적이다.</li>
<li>이러한 검증 작업은 컨트롤러에서 진행하는데 스프링 MVC의 경우 @Valid와 BindingResult라는 것을 이용해서 간단하게 처리할 수 있다.</li>
<li>@Valid의 애너테이션을 이용해서 검증하고, BindingResult는 스프링 프레임워크에서 유효성 검사 후에 발생하는 오류 및 검증 결과를 저장</li>
</ul>
<ol>
<li>@Valid annotion을 사용한 모델 객체 검증</li>
<li>BindingResult에 검증에 대한 결과를 저장</li>
<li>RedirectAttribute 리다이렉트 한 후 데이터 전달
순서를 지켜 사용하면 스프링 mvc는 유효성 검사를 수행하고 유효성 검사 결과를BindingResult 에 저장한 후 리다이렉트 한 후에 데이터를 RedirectAttributes를 통해 안전하게 전달할 수 있다</li>
</ol>
<h4 id="checkbox를-formatter">checkbox를 Formatter</h4>
<blockquote>
</blockquote>
<ul>
<li>화면에서 체크박스를 이용해서 완료여부(finished)를 처리.
문제는 브라우저가 체크박스가 클릭된 상태 전송되는값이 &#39;on&#39;이라는 값을 전달한다.
TodoDTO로 데이터를 수집할때 문자열 &#39;on&#39;을 boolean타입으로 처리할 수 있어야 한다.</li>
<li>컨트롤러에서 데이터를 수집할 때 타입을 변경해줘야 한다.</li>
</ul>
<h4 id="페이징-처리">페이징 처리</h4>
<blockquote>
</blockquote>
<ul>
<li>가져오는수
select * from tbl_todo order by tno desc limit 10;</li>
<li>첫번째 10 건너뛰는 데이터수(skip), 두번째 10가져오는 데이터수 (fetch)
2페이지 == select * from tbl_todo order by tno desc limit 10, 10;
5페이지 == select * from tbl_todo order by tno desc limit 40, 10;</li>
<li>전체 데이터 개수
select count(tno) from tbl_todo;</li>
</ul>
<h4 id="페이징-처리를-위한-dtopagerequestdto">페이징 처리를 위한 DTO(PageRequestDTO)</h4>
<blockquote>
</blockquote>
<ul>
<li>현재 페이지의 번호(page)</li>
<li>한페이지당 보여주는 데이터의 수(size)<pre><code class="language-java">package com.springex.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
  @Builder.Default    // 필드에 기본값 설정
  @Min(value = 1)     // 최솟값
  @Positive           // 양수여야 한다.
  private int page = 1;// 현재 페이지 번호
  @Builder.Default    // 필드에 기본값을 설정
  @Min(value = 10)    // 최솟값 10
  @Max(value = 100)   // 최댓값 100
  @Positive
  private int size = 10;  //한페이지당 보여주는 데이터의 수
  public int getSkip() {
      // 일반적으로 데이터베이스나 컬렉션에서 데이터를 가져올때
      // 0부터 시작하는 인덱스를 사용하는 경우가 많아서
      // 페이지를 가져오려면 인덱스 0번부터 시작해야 한다.
      // ex)페이지번호 1이면 (1-1)*10 은 0 -&gt; 첫번째 페이지의 데이터를 가져올때 사용
      // 페이지번호가 2이면 (2-1)*10 -&gt; 10부터는 두번째 페이지의 데이터를 가져올때 사용
      return (page -1) * 10;
  }
}</code></pre>
</li>
</ul>
<h4 id="todomapper의-count-처리">TodoMapper의 count 처리</h4>
<blockquote>
</blockquote>
<ul>
<li>화면에 페이지 번호들을 구성하기 위해서는 전체 데이터의 수를 알아야만 가능하다. 예를 들어 마지막 페이지가 7에서 끝나야 하는 상황이 생긴다면 화면상에서도 페이지 번호를 조절해야 하기 때문이다.</li>
</ul>
<h4 id="pageresponsedto">PageResponseDTO</h4>
<blockquote>
</blockquote>
<ul>
<li>TodoDTO의 목록</li>
<li>전체 데이터 수</li>
<li>페이지 번호의 처리를 위한 데이터들(시작 페이지 번호/ 끝 페이지 번호)</li>
<li>화면상에서 페이지 번호들을 출력하려면 현재 페이지번호(page)와 페이지당 데이터 수(size)를 이용해서 계산해야 한다.</li>
<li>PageResponseDTO는 생성자를 통해서 필요한 page나 size등을 전달받도록 구성한다</li>
</ul>
<h4 id="페이지-번호의-계산">페이지 번호의 계산</h4>
<blockquote>
</blockquote>
<ul>
<li>페이지 번호를 계산하려면 현재 페이지의 번호(page)가 필요</li>
<li>화면에 10개의 페이지 번호를 출력한다고 했을 때 경우의 수들이 발생한다
1) page가 1인 경우: 시작페이지(start)는 1, 마지막페이지(end)는 10
1) page가 10인 경우: 시작페이지(start)는 1, 마지막페이지(end)는 10
1) page가 11인 경우: 시작페이지(start)는 11, 마지막페이지(end)는 20</li>
<li>마지막 페이지/시작페이지 번호의 계산
흔히들 처음에 구해야 하는 것이 start라고 생각하지만, 마지막 패아자(end)를 구하는 계산이 더 편할 수 있다.</li>
<li>end는 현재 페이지 번호를 기준으로 계산한다.</li>
<li>마지막 페이지를 먼저 계산한 이유가 시작페이지(start)의 계산을 쉽게 하기 위함. 시작페이지(start)의 경우 계산한 마지막 페이지에서 9를 빼면 된다.
this.start = this.end -9
10-9=1
20-9=11</li>
<li>시작페이지의 구성은 끝났지만 마지막 페이지의 경우 다시 전체 개수(total)를 고려해야 한다. 만일 10개씩(size) 보여주는 경우 전체 개수(total)가 75라면 마지막 페이지는 10이 아닌 8이 되어야 한다.<pre><code class="language-java">int last = (int)(Math.ceil((total/(double)size)));
123 / 10.0 =&gt; 12.3 =&gt; 13
100 / 10.0 =&gt; 10.0 =&gt; 10
75 / 10.0 =&gt; 7.5 =&gt; 8</code></pre>
</li>
<li>마지막 페이지(end)는 앞에서 구한 last 값보다 작은 경우에는 last값이 end가 되어야한다<pre><code class="language-java">end 7
last9
7 &gt; 8
// last가 최종이어야한다
int last = (int)(Math.ceil((total/(double)size)));
this.end = end &gt; last ? last: end;</code></pre>
</li>
<li>이전(prev)/다음(next) 계산
이전페이지의 존재 여부는 시작 페이지(start)가 1이 아니라면 무조건 true가 되어야 한다
(1페이지부터 시작이고 아니라면 2,3,4,, 이기 때문에 페이지가 존재하고 있다)</li>
<li>다음(next)은 마지막 페이지(end)와 페이지당 개수(size)를 곱한 값보다 전체 개수(total)가 더 많으면 다음(next)가 더 있다고 판단해야 한다.<pre><code class="language-java">this.prev = this.start &gt;1;
this.next = total &gt; this.end * this.size;</code></pre>
</li>
</ul>
<h4 id="조회페이지로의-이동">조회페이지로의 이동</h4>
<blockquote>
</blockquote>
<ul>
<li>목록 페이지는 특정 Todo의 제목(title)을 누르면 조회 페이지로 이동됨</li>
<li>기존에는 단순히 tno만을 전달해서 &#39;/todo/read?tno=22&#39;과 같은 방식으로 이동했지만 페이지 번호가 붙을 때는 page와 size 같이 전달이 되어야만 조회 페이지에서 다시 목록 이동할대 기존 페이지를 볼 수 있게 된다</li>
</ul>
<h4 id="수정삭제-처리-후-페이지-이동">수정/삭제 처리 후 페이지 이동</h4>
<blockquote>
</blockquote>
<ul>
<li>수정/삭제 작업은 POST방식으로 처리되고 삭제 처리가 된 후에는 다시 목록으로 이동해야 한다.
그렇기 때문에 수정 화면에서[form] 태그로 데이터를 전송할때 관련 정보를 같이 추가해서 전달해야만 한다.<pre><code class="language-java">&lt;input type=&quot;hidden&quot; name=&quot;page&quot; value=&quot;${pageRequestDTO.page}&quot;&gt;
&lt;input type=&quot;hidden&quot; name=&quot;size&quot; value=&quot;${pageRequestDTO.size}&quot;&gt;</code></pre>
post 방식으로 이루어지는 삭체 처리도 PageRequestDTO를 이용해서 [form] 태그로 전송되는 태그들을 수집하고 수정 후에 목록페이지로 이동할 때 page는 무조건 1페이지로 이동하도록 만들 수 있다</li>
<li>수정 처리 후 이동
Todo를 수정한 후에 목록으로 이동할때 페이지 정보를 이용해야
TodoController의 Modify()에서 PageRequestDTO를 받도록 구현한다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 2024.06.12]]></title>
            <link>https://velog.io/@hi_do/Spring</link>
            <guid>https://velog.io/@hi_do/Spring</guid>
            <pubDate>Wed, 12 Jun 2024 04:03:47 GMT</pubDate>
            <description><![CDATA[<h4 id="applicationcontext">ApplicationContext</h4>
<blockquote>
</blockquote>
<ul>
<li>서블릿이 존재하는 공간을 서블릿 컨텍스트라고 했다. 스프링에서는 빈(Bean)이라고 부르는 객체를 관리하기 위해서 ApplicationContext라는 존재를 활용해야 한다.</li>
<li>ApplicationContext는 root-context.xml을 이용해서 스프링이 실행되고 ApplicationContext 객체가 생성된다.</li>
<li>root-context.xml을 읽으면 SampleService와 SampleDAO가 [bean]으로 지정 되어있어 해당 클래스의 객체를 생성해서 관리하기 시작한다.
<img src="https://velog.velcdn.com/images/hi_do/post/ddb3e704-3894-47ef-a4f9-8d076f1777f5/image.png" alt=""></li>
</ul>
<h4 id="contextcomponent-scan">Context:component-scan</h4>
<blockquote>
</blockquote>
<ul>
<li>스프링을 이용할때는 클래스를 작성하거나 객체를 직접 생성하지 않는다. 이 역할은 스프링 내부에서 이루어지며 ApplicationContext가 생성된 객체들을 관리한다. </li>
<li>개발자가 직접 객체를 생성하지 않는 방식이 서블릿과도 유사하다. 서블릿을 생성하면 톰캣이 웹 애플리케이션을 실행하고 필요할 때 서블릿 객체를 만드는 것과 비슷한 방식이다.</li>
</ul>
<h4 id="service-repository">@Service, @Repository</h4>
<blockquote>
</blockquote>
<ul>
<li>서블릿에서도 @WebServlet이나 @WebFilter와 같은 다양한 에너테이션이 존재하듯이 스피링 프레임워크는 애플리케이션 전체를 커버하기 때문에 다양한 종류의 네너테이션을 사용하도록 작성되어 있다.</li>
<li>@Controller: MVC의 컨트롤러를 위한 에너테이션
@Serive: 서비스 계층의 객체를 위한 에너테이션
@Repository: DAO와 같은 객체를 위한 에너테이션
@Component: 일반 객체나 유틸리티객체를 위한 에너테이션</li>
<li>에너테이션 이름으로 스프링이 사용하는 웹 영역뿐만 아니라 애플리케이션 전체에 사용할수 있는 객체들을 망라하고 있다. </li>
</ul>
<h4 id="생성자-주입-방식">생성자 주입 방식</h4>
<blockquote>
</blockquote>
<ul>
<li>초기 스프링에서는 @Autowired를 멤버 변수로 할당하거나 Setter를 작성하는 방식으로 이용했지만, 스피링 3 이후에는 생성자 주입 방식이라고 부르는 방식을 더 많이 활용하고 있다.</li>
<li>생성자 주입 방식 규칙
1) 주입 받아야하는 객체의 변수는 final로 작성되어야 한다.
2) 생성자를 이용해서 해당 변수를 생성자의 파라미터로 지정한다.</li>
<li>Lombok에는 생성자 주입을ㅇ 간단히 작성할 수 있는데 @RequiredArsContructor를 이용해서 필요한 생성자를 자동으로 작성할 수 있다.</li>
</ul>
<h4 id="인터페이스를-이용한-느슨한-결합">인터페이스를 이용한 느슨한 결합</h4>
<blockquote>
</blockquote>
<ul>
<li>스프링이 의존성 주입을 가능하게 하지만 좀 더 근본적으로 유연한 프로그램을 설계하기 위해서는 인터페이스를 이용해서 나중에 다른 클래스의 객체로 쉽게 변경할 수 있도록 하는 것이 좋다.</li>
<li>예를 들어 SampleDAO를 다른 객체로 변경하려면 결론적으로 SampleSerice 코드 역시 수정되어야 한다. 추상화 된 타입을 이용하면 이러한 문제를 피할 수 있는데 가장 대표적인 것이 인터페이스이다. 인터페이스를 이용하면 실제 객체를 모르고 타입만을 이용해서 코드를 작성하는 일이 가능하다.</li>
<li>객체와 객체의 의존 관계의 실제 객체를 몰라도 가능하게 하는 방식을 느슨한 결합이라고 한다.</li>
<li>느슨한 결합을 이용하면 나중에 SampleDAO 타입의 객체를 다른 객체로 변경하더라도 SampleService 타입을 이용하는 코드는 수정할 일이 없기때문에 보다 유연한 구조가 된다.</li>
</ul>
<h4 id="qualifier">@Qualifier</h4>
<blockquote>
</blockquote>
<ul>
<li>이름을 지정해서 특정한 이름의 객체를 주입받는 방식.</li>
<li>Lombok과 @Qualifier를 같이 사용하기 위한 방법.</li>
<li>main &gt; java &gt; lombok.config 파일 생성</li>
<li>lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier</li>
</ul>
<h4 id="스프링의-빈bean으로-지정되는-객체들">스프링의 빈(Bean)으로 지정되는 객체들</h4>
<blockquote>
</blockquote>
<ul>
<li>객체들은 쉽게 말해서 ‘핵심 역할’을 하는 객체들이다</li>
<li>스프링의 빈으로 등록되는 객체들은 오랜 시간 동안 프로그램 내에서 상주하면서 중요한 역할을 하는 객체들이다</li>
<li>반대로 말하면, 역할이 아닌 데이터를 중심으로 두고 설계되는 객체들(DTO,VO)은 Bean으로 등록하지 않는다. 특히 DTO의 경우에는 생명주기가 짧고, 데이터 보관이 주된 역할이기 때문에 빈으로 처리하지 않는다</li>
<li>ApplicationContext가 웹 애플리케이션에서 동작하려면 웹 애플리케이션이 실행될때 스프링을 로딩해서 해당 웹 애플리케이션 내부에 스프링의 ApplicationContext를 생성하는 작업이 필요하게 되는데 이를 위해서는 web.xml을 이용해서 리스너를 설정한다.<img src="https://velog.velcdn.com/images/hi_do/post/b231fe59-fb26-4fb2-88ac-b03694ab497c/image.png" alt=""></li>
<li>톰캣과 스프링을 연동하는 구조를 완성</li>
<li>데이터베이스 관련 설정을 추가해야 한다.<pre><code class="language-java">public enum ConnectionUtil {
  INSTANCE;
  private HikariDataSource ds;
  ConnectionUtil() {
      HikariConfig config = new HikariConfig();       config.setDriverClassName(&quot;org.mariadb.jdbc.Driver&quot;);        config.setJdbcUrl(&quot;jdbc:mariadb://localhost:3307/webdb&quot;);
      config.setUsername(&quot;webuser&quot;);
      config.setPassword(&quot;webuser&quot;);  config.addDataSourceProperty(&quot;cachePrepStmts&quot;, &quot;true&quot;);      config.addDataSourceProperty(&quot;prepStmtCacheSize&quot;, &quot;250&quot;);      config.addDataSourceProperty(&quot;prepStmtCacheSqlLimit&quot;, &quot;2048&quot;);
      ds = new HikariDataSource(config);
  }
      public Connection getConnection()throws Exception{
          return ds.getConnection();
      }
}</code></pre>
</li>
<li><blockquote>
<p>스프링을 이용한다면 이 설정을 스프링의 빈으로 처리되어야 한다</p>
</blockquote>
</li>
<li>root-context.xml</li>
<li><blockquote>
<p>빈으로 설정이 되어야 하는 패키지 처리.
데이터베이스의 설정 연결</p>
</blockquote>
</li>
<li>스프링은 필요한 객체를 스프링에서 주입해 주기 때문에 개별적으로 클래스를 작성해서 빈(bean)으로 등록해 두기만 하면 원하느 곳에서 쉽게 다른 객체를 사용할 수 있다.</li>
<li>이런 특징으로 인해 스프링은 웹이나 데이터베이스와 같은 특정한 영역이 아닌 전체 애플리케이션의 구조를 설계할 때 사용된다.</li>
</ul>
<h4 id="jdbc---mybatis를-이용해서-프로젝트에-적용">JDBC -&gt; MyBatis를 이용해서 프로젝트에 적용</h4>
<blockquote>
</blockquote>
<ul>
<li>PreparedStatement, ResultSet, Connection의 처리를 안해도 된다,</li>
<li>SQL을 분리하여 선언할 수 있다.</li>
<li>MyBatis를 위한 라이브러리</li>
<li><blockquote>
<p>스프링관련: spring-jbdc, spring-tx
MyBatis: mybatis, mybatix-spring</p>
</blockquote>
</li>
<li>MyBatis를 위한 스프링의 설정(SqlSessionFactory)_root-context.xml에 MyBatis와 스프링을 연동하고 Mapper 인터페이스를 이용하는 방식</li>
<li>스프링과 MyBatis 사이에 &#39;mybatis-spring&#39;이라는 라이브러리를 이용해서 스프링이 데이터베이스 전체에 대한 처리를 하고 MyBatis는 일부 기능 개발에 활용하는 방식. 개발 시에는 Mapper 인터페이스 방식을 이용해서 인터페이스만으로 모든 개발이 가능한 방식</li>
</ul>
<h4 id="autowiredrequired--false">@AutoWired(required = false)</h4>
<blockquote>
</blockquote>
<ul>
<li>해당 객체를 주입받지 못하더라도 예외가 발생하지 않는데, 인텔리제이, 이클립스의 경우 @Service, @Repository와 같이 직접 스프링의 빈(Bean)으로 등록된 경우가 아니면 경고가 발생하므로 이를 방지하기 위해 사용된다.</li>
</ul>
<h4 id="xml로-sql-분리하기">xml로 SQL 분리하기</h4>
<blockquote>
</blockquote>
<ul>
<li><p>MyBatis를 이용할때 sql은 @Select와 같은 에너테이션을 이용해서 사용하기도 하지만 대부분은 sql을 별도의 파일로 분리하는 것을 권장한다.</p>
</li>
<li><p>xml을 사용하는 이유는 sql이 길어지면 이를 에너테이션으로 처리하기가 복잡하기 때문이기도 하고 에너테이션이 나중에 변경되면 프로젝트 전체를 다시 빌드하는 작업이 필요하기 때문에 단순 파일로 사용하는 것이 편리하다.
[property name=&quot;mapperLocations&quot; value=&quot;classpath:/mappers/<em>*/</em>.xml&quot;][/property]
xml mapper 파일의 위치</p>
</li>
<li><p>classpath 접두어를 이용해서 인식되는 경로이고 mappers 폴더 밑에 폴더가 있어도 관계없도록 &#39;<em>*&#39;와 &#39;</em>.xml&#39;을 지정</p>
</li>
</ul>
<h4 id="순서">순서</h4>
<blockquote>
</blockquote>
<p>1.</p>
<pre><code>    implementation &#39;org.mybatis:mybatis:3.5.15&#39;
    implementation &#39;org.mybatis:mybatis-spring:3.0.3&#39;
    implementation &#39;org.springframework:spring-jdbc:6.1.2&#39;
    implementation &#39;org.springframework:spring-tx:6.1.2&#39;</code></pre><ol start="2">
<li>root-context.xml에<pre><code class="language-java"> &lt;bean id=&quot;sqlSessionFactory&quot; class=&quot;org.mybatis.spring.SqlSessionFactoryBean&quot;&gt;
     &lt;property name=&quot;dataSource&quot; ref=&quot;dataSource&quot; /&gt;
 &lt;/bean&gt;</code></pre>
</li>
<li>애너테이션으로 설정<pre><code>package springex.mapper;
import org.apache.ibatis.annotations.Select;
public interface TimeMapper {
@Select(&quot;select now()&quot;)
String getTime();
}</code></pre></li>
<li>root-context.xml에 <a href="mybatis:scan">mybatis:scan</a>을 인식할수 있도록 등록<pre><code>xmlns:mybatis=&quot;http://mybatis.org/schema/mybatis-spring&quot;
http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd&quot;&gt;
&lt;mybatis:scan base-package=&quot;com.springex.mapper&quot;&gt;&lt;/mybatis:scan&gt;</code></pre></li>
<li>인식하는지 tests 작성<pre><code>@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations=&quot;file:src/main/webapp/WEB-INF/root-context.xml&quot;)
public class TimeMapperTests 
@Autowired(required = false)
private TimeMapper timeMapper;
@Test
public void testGetTime() {
   log.info(timeMapper.getTime());
}
}</code></pre></li>
<li>애너테이션으로 하는 방법말고 xml로 sql분리하는 방법을 사용.<pre><code>springex &gt; mapper 패키지생성 &gt; TimeMapper2.interface
package springex.mapper;
public interface TimeMapper2 {
String getNow();
}</code></pre></li>
<li>main &gt; resources &gt; mappers 디렉터리 추가후  TimeMapper2.xml 생성<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;
&lt;!DOCTYPE mapper
     PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
     &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt;
&lt;mapper namespace=&quot;com.springex.mapper.TimeMapper2&quot;&gt;
 &lt;select id=&quot;getNow&quot; resultType=&quot;string&quot;&gt;
     select now()
 &lt;/select&gt;
&lt;/mapper&gt;</code></pre></li>
<li>root-context.xml에 MyBatis 설정에 xml 파일을 인식하도록 설정 추가<pre><code>&lt;property name=&quot;mapperLocations&quot; value=&quot;classpath:/mappers/**/*.xml&quot;&gt;&lt;/property&gt;</code></pre></li>
</ol>
<h4 id="스프링-웹-mvc">스프링 웹 MVC</h4>
<blockquote>
</blockquote>
<ul>
<li>DispatcherServlet과 Front Controller</li>
<li><blockquote>
<p>요청(Request)이 오면 반드시 DispatcherServlet라는 존재를 통해서 실행이 된다.
<img src="https://velog.velcdn.com/images/hi_do/post/a6ddaa13-c0bf-48b2-9b8b-312bb3db7eb1/image.png" alt=""></p>
</blockquote>
</li>
<li>결론은 스프링 MVC에서는 DispatcherServlet이라는 객체가 프론트 컨트롤러의 역할을 수행한다</li>
</ul>
<h4 id="root-contextxml">root-context.xml</h4>
<blockquote>
</blockquote>
<ul>
<li>애플리케이션의 핵심적인 빈</li>
<li>데이터베이스 연결, 서비스 계층 구성 등의 애플리케이션의 전반에 걸친 설정 정의</li>
</ul>
<h4 id="servlet-contextxml">servlet-context.xml</h4>
<ul>
<li>스프링 mvc의 DispatcherServlet에 의한 처리되는 설정을 담당</li>
</ul>
<h4 id="webxml">web.xml</h4>
<blockquote>
</blockquote>
<ul>
<li>웹 애플리케이션의 배치 설명자(Deployment Descriptor)로서, 서블릿 컨테이너에 애플리케이션을 배포할때 필요한 구성 요소를 정의</li>
<li>Servlet, Filter, Listener와 같은 웹 구성 요소의 매핑 및 설정</li>
<li>ApplicationContext 구성하기 위해[Listener] 초기화 설정</li>
<li>[servlet] 설정은 DispatcherServlet을 등록하는데 DispatcherServlet이 로딩할 때 servlet-context.xml을 이용하도록 설정한다.</li>
</ul>
<h4 id="requestmapping">@RequestMapping</h4>
<blockquote>
</blockquote>
<ul>
<li>스프링 컨트롤러에서 가장 많이 사용하는 애너테이션</li>
<li>특정 경로의 요청을 지정하기 위해서 사용</li>
<li>참고로 servlet 중심의 MVC는 Servlt의 상속받아서 doGET(), doPost()와 같은 제한적인 메서드를 오버라이드해서 사용한다</li>
<li>스프링 MVC의 경우 하나의 컨트롤러를 이용해서 여러 경로의 호출 모두 처리할 수 있다.</li>
</ul>
<h4 id="파라미터-자동-수집과-변환">파라미터 자동 수집과 변환</h4>
<blockquote>
</blockquote>
<ul>
<li>파라미터 자동 수집은 DTO나 VO등을 메서드의 파라미터로 설정하면 자동으로 전달되는 HttpServletRequest의 파라미터들을 수집해 주는 기능이다.</li>
</ul>
<blockquote>
<ul>
<li>(Failed to convert value of type &#39;java.lang.String&#39;to required type &#39;java.time.LocalDate&#39;) -&gt; 특정한 타입을 처리하는 Formatter라는 것을 이용해야 한다. 문자열을 포맷해서 특정한 객체로 변환하는 경우에 사용할 수 있다.</li>
</ul>
</blockquote>
<ul>
<li>&lt;mvc:annotation-friven conversion-service=&quot;conversionService&quot;/&gt; conversionServie라는 빈(Bean)을 등록한 후에 스프링MVC를 처리할 때 이용하겠다.</li>
</ul>
<h4 id="객체-자료형의-파라미터-수집">객체 자료형의 파라미터 수집</h4>
<blockquote>
</blockquote>
<ul>
<li>기본 자료형과 달리 객체 자료형을 파라미터로 처리하기 위해서는 객체가 생성되고 setXXX()을 이용해서 처리한다</li>
<li>Lombok을 활용하여 @Setter, @Date를 이용하여 처리할 수 있다.</li>
</ul>
<h4 id="model">Model</h4>
<blockquote>
</blockquote>
<ul>
<li>서블릿 request.setAttibute()를 이용해서 데이터 담아서 jsp까지 전달</li>
<li>스프링 MVC에서는 Model이라는 객체를 이용해서 처리한다</li>
<li>Java Beans와 @ModelAttribute
스프링 MVC 컨트롤러는 특이하게도 파라미터로 getter/setter를 이용하는 Java Beans의 형식의 사용자 정의 클래스가 파라미터 인 경우에는 자동으로 화면까지 객체를 전달한다.
파라미터로 TodoDTO를 받는 경우에</li>
</ul>
<h4 id="redirectattributes">RedirectAttributes</h4>
<blockquote>
</blockquote>
<ul>
<li>Post 방식으로 처리를 하고 Redirect를 해서 get방식으로 특정한 페이지로 이동하는 PRG패턴</li>
<li>스프링 MVC에서는 RedirectAttributes 로 처리한다
1) addAttribute(키,값): 리다이렉트할 때 쿼리 스트링이 되는 값을 지정
2) addFlashAttribute(키,값): 일회용으로만 데이터를 전달하고 삭제되는 값을 지정</li>
<li>addAttribute()로 데이터를 추가하면 리다이렉트할 URL에 쿼리 스트링 추가되고, addFlashAttribute()를 이용하면 URL에 보이지는 않지만, JSP에서는 일회용으로 사용할 수 있다.</li>
</ul>
<h4 id="스프링-mvc의-예외-처리">스프링 MVC의 예외 처리</h4>
<blockquote>
</blockquote>
<ul>
<li>컨트롤러에서 발생하는 예외를 처리할 때 가장 일반적인 방식은 @ControllerAdvice를 사용하는 방식이다.</li>
<li>@ControllerAdvice가 선언된 클래스는 스프링의 Bean으로 처리된다</li>
<li>@ExceptionHandler: 어떤 exception인지 인지하는 에너테이션</li>
<li>@ResponseBody: 문자열,JSON타입을 그대로 브라우저에 전송하는 방식</li>
</ul>
<h4 id="404상태에-맞는-화면을-별도로-맘ㄴ들-경우">404상태에 맞는 화면을 별도로 맘ㄴ들 경우</h4>
<blockquote>
</blockquote>
<ul>
<li><p>CommonExceptionAdvice에 추가</p>
<pre><code class="language-java">@ExceptionHandler(NoHandlerFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public String notFound(){
      return &quot;custom404&quot;;
  }</code></pre>
<blockquote>
</blockquote>
</li>
<li><p>views &gt; custom404.jsp 파일 생성
WEB-INF &gt; web.xml 수정</p>
<pre><code>&lt;h1&gt;페이지를 찾을 수 없습니다!&lt;/h1&gt;

&lt;servlet&gt;

      &lt;servlet-name&gt;appServlet&lt;/servlet-name&gt;
      &lt;servlet-class&gt;org.springframework.web.servlet.DispatcherServlet&lt;/servlet-class&gt;
      &lt;init-param&gt;

          &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;
          &lt;param-value&gt;/WEB-INF/servlet-context.xml&lt;/param-value&gt;
      &lt;/init-param&gt;

      &lt;init-param&gt;
          &lt;param-name&gt;throwExceptionIfNoHandlerFound&lt;/param-name&gt;
          &lt;param-value&gt;true&lt;/param-value&gt;
      &lt;/init-param&gt;

</code></pre></li>
</ul>
<pre><code>    &lt;load-on-startup&gt;1&lt;/load-on-startup&gt;
&lt;/servlet&gt;</code></pre><p>서버 다시 돌리고 주소 잘못치면 &quot;페이지를 찾을수 없습니다!&quot; 나온다.
```</p>
]]></description>
        </item>
    </channel>
</rss>