<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>woomin_s.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 21 May 2023 19:42:30 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>woomin_s.log</title>
            <url>https://velog.velcdn.com/images/woomin_s/profile/279401a2-33a4-4de4-bbfe-b6a71b5b8ac7/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. woomin_s.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/woomin_s" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[주문하기 - 단일 상품 주문]]></title>
            <link>https://velog.io/@woomin_s/%EC%A3%BC%EB%AC%B8%ED%95%98%EA%B8%B0-%EB%8B%A8%EC%9D%BC-%EC%83%81%ED%92%88-%EC%A3%BC%EB%AC%B8</link>
            <guid>https://velog.io/@woomin_s/%EC%A3%BC%EB%AC%B8%ED%95%98%EA%B8%B0-%EB%8B%A8%EC%9D%BC-%EC%83%81%ED%92%88-%EC%A3%BC%EB%AC%B8</guid>
            <pubDate>Sun, 21 May 2023 19:42:30 GMT</pubDate>
            <description><![CDATA[<h2 id="📜-미리보기">📜 미리보기</h2>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/22ee8c7c-9a5a-4320-8008-d54b388d0894/image.png" alt=""></p>
<ul>
<li>상품 상세 화면에서 상품을 바로 주문하는 기능을 구현하였습니다. </li>
</ul>
<br>

<h2 id="📜orderservice">📜OrderService</h2>
<pre><code class="language-java">    /**
     * 단일 주문
     */
    public Long order(Long memberId, Long itemId, int count) {

        List&lt;OrderItem&gt; orderItemList = new ArrayList&lt;&gt;();
        Item findItem = itemRepository.findById(itemId).orElseGet(() -&gt; null);
        Member findMember = memberRepository.findById(memberId).orElseGet(() -&gt; null);
        int orderPrice = findItem.getPrice() * count;

        OrderItem orderItem = OrderItem.createOrderItem(count, orderPrice, findItem);
        orderItemList.add(orderItem);
        Order order = Order.createOrder(findMember, orderItemList);

        Order save = orderRepository.save(order);
        return save.getId();

    }</code></pre>
<pre><code class="language-java">//OrderItem 클래스 
public static OrderItem createOrderItem(int count, int orderPrice, Item item) {
        item.minStock(count);
        return new OrderItem(count, orderPrice, item);
    }</code></pre>
<pre><code class="language-java">    //Item 클래스 
      //주문시 상품 재고 감소
    public void minStock(int quantity) {
        int restQuantity = stockQuantity - quantity;
        if (restQuantity &lt; 0) {
            throw new NotEnoughStockException(&quot;상품 재고가 부족합니다!!&quot;);
        }
        stockQuantity = restQuantity;
    }</code></pre>
<pre><code class="language-java">    //Order 클래스
    public static Order createOrder(Member member, List&lt;OrderItem&gt; orderItems) {  //List&lt;OrderItem&gt; list??
        Order order = new Order(member);
        for (OrderItem orderItem : orderItems) {
            order.addOrderItem(orderItem);
        }
        return order;
    }

      //==연관 관계 메서드==//
    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.changeOrder(this);
    }</code></pre>
<ul>
<li>파라미터로 넘어온 memberId, itemId, count를 이용해 주문 상품(OrderItem), 주문(order) 객체를 생성하고 OrderRepository에 저장하는 서비스 로직입니다. </li>
<li>주문시 상품 재고를 빼기 위해 OrderItem객체를 생성할 때 상품의 재고를 마이너스(minStock)하도록 하였습니다.</li>
<li>만약 상품 재고가 부족하면 NotEnoughStockException을 예외로 던져주도록 하였습니다.</li>
</ul>
<br>


<h2 id="📜ordercontroller">📜OrderController</h2>
<pre><code class="language-java">    /**
     * 단일 상품 바로 주문
     */
    @PostMapping(&quot;/order&quot;)
    public ResponseEntity&lt;String&gt; order(@ModelAttribute CartForm cartForm, HttpServletRequest request) {

        //CartController 에 작성해둔 세션 정보 조회하는 기능 공용으로 사용 
        Member member = CartController.getMember(request);

        if (member == null) {
            return new ResponseEntity&lt;String&gt;(&quot;로그인이 필요한 서비스입니다.&quot;, HttpStatus.UNAUTHORIZED);
        }

        try {
            orderService.order(member.getId(), cartForm.getItemId(), cartForm.getCount());
        } catch (NotEnoughStockException e) {
            return new ResponseEntity&lt;String&gt;(e.getMessage(), HttpStatus.BAD_REQUEST);
        }

        return ResponseEntity.ok(&quot;order Success&quot;);
    }</code></pre>
<pre><code class="language-java">@Getter
@Setter
public class CartForm {

    private Long itemId;
    private int count;
}</code></pre>
<ul>
<li>세션 정보를 조회해서 세션에 사용자가 존재하지 않으면 UNAUTHORIZED(401) 에러</li>
<li>상품 재고가 부족하면 BAD_REQUEST(400) 에러로 처리하였습니다. </li>
</ul>
<br>

<h2 id="📜itemviewhtml">📜itemView.html</h2>
<ul>
<li><a href="https://velog.io/@woomin_s/%EC%83%81%ED%92%88-%EC%83%81%EC%84%B8-%ED%8E%98%EC%9D%B4%EC%A7%80">상품 상세 페이지 </a></li>
<li><a href="https://github.com/woomin-Shim/e-commerce/blob/master/src/main/resources/templates/item/itemView.html">GITHUB</a><pre><code class="language-javascript">&lt;!-- 단일 상품 바로 주문 --&gt;
  function order() {

</code></pre>
</li>
</ul>
<pre><code>    var itemId = $(&quot;#id&quot;).val();
    var count = $(&quot;#count&quot;).val();
    var cartForm = {
        itemId : itemId,
        count : count
    };

    $.ajax({
        url: &quot;/order&quot;,
        data: cartForm,
        type: &#39;POST&#39;,
        success: function(result) {
            alert(&quot;상품 주문이 완료되었습니다.&quot;);
            location.href = &quot;/&quot;;
        },

        error: function (jqXHR, textStatus, errorThrown) {
            if (jqXHR.status === 401) {
                alert(jqXHR.responseText);
                location.href=&quot;/members&quot;
            }
            else {
                alert(jqXHR.responseText);
            }
        }
    })
}</code></pre><pre><code>- ajax를 활용해 상품 아이디와 상품 주문 수량을 cartForm에 매핑하여 컨트롤러에 전달해주었습니다. 
- 401에러(UNAUTHORIZED)가 발생할 시 로그인 화면으로 이동하도록 하고 
- 그 밖에 에러(상품 재고 부족으로 인한 에러)를 따로 처리하였습니다. 

&lt;br&gt;

## 📜결과 및 실행 화면 

- 데이터베이스 
![](https://velog.velcdn.com/images/woomin_s/post/37514c34-31ef-4ffa-aa80-7c83ea147223/image.png)


![](https://velog.velcdn.com/images/woomin_s/post/add4cb6d-2ae2-4d43-9ab0-7d6a7b8a2999/image.gif)
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Querydsl 프로젝션 결과 반환(DTO)]]></title>
            <link>https://velog.io/@woomin_s/Querydsl-%ED%94%84%EB%A1%9C%EC%A0%9D%EC%85%98-%EA%B2%B0%EA%B3%BC-%EB%B0%98%ED%99%98DTO</link>
            <guid>https://velog.io/@woomin_s/Querydsl-%ED%94%84%EB%A1%9C%EC%A0%9D%EC%85%98-%EA%B2%B0%EA%B3%BC-%EB%B0%98%ED%99%98DTO</guid>
            <pubDate>Mon, 15 May 2023 11:43:25 GMT</pubDate>
            <description><![CDATA[<h2 id="📝프로젝션-대상이-둘-이상">📝프로젝션 대상이 둘 이상</h2>
<ul>
<li>프로젝션 대상이 하나인 경우 타입을 명확하게 지정 가능합니다.</li>
<li>그러나 프로젝션 대상이 둘 이상인 경우에는 타입을 DTO나 Tuple로 지정해야 합니다. </li>
<li>DTO로 조회하는 법을 설명드립니다.</li>
</ul>
<br>


<h3 id="📝순수-jpa에서-dto-조회">📝순수 JPA에서 DTO 조회</h3>
<ul>
<li>쇼핑몰 프로젝트 진행 중에 순수 JPA에서 DTO를 조회하는 방법을 사용하였습니다. </li>
<li>[DTO 조회] (<a href="https://velog.io/@woomin_s/%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%EC%A1%B0%ED%9A%8C#cartqueryrepository">https://velog.io/@woomin_s/%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%EC%A1%B0%ED%9A%8C#cartqueryrepository</a>)</li>
</ul>
<br>

<h3 id="1-setter---프로퍼티-접근으로-dto-조회">1. Setter - (프로퍼티) 접근으로 DTO 조회</h3>
<pre><code class="language-java">     public void findDtoBySetter() {  //프로퍼티(setter) 접근
        List&lt;MemberDto&gt; result = queryFactory
                .select(Projections.bean(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();
    }</code></pre>
<h3 id="2-필드-직접-접근">2. 필드 직접 접근</h3>
<pre><code class="language-java">    public void findDtoByField() {  //필드 직접 접근
        List&lt;MemberDto&gt; result = queryFactory
                .select(Projections.fields(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();
    }</code></pre>
<h3 id="3-생성자-접근">3. 생성자 접근</h3>
<pre><code class="language-java">     @Test
    public void findDtoByConstructor() {  //생성자 사용
        List&lt;MemberDto&gt; result = queryFactory
                .select(Projections.constructor(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();
    }</code></pre>
<h3 id="4-queryprojection-이용-접근">4. @QueryProjection 이용 접근</h3>
<pre><code class="language-java">    public void findDtoByQueryProjection() {
        List&lt;MemberDto&gt; result = queryFactory
                .select(new QMemberDto(member.username, member.age))
                .from(member)
                .fetch();
    }</code></pre>
<pre><code class="language-java">@Data
@NoArgsConstructor
public class MemberDto {

    private String username;
    private int age;

    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}</code></pre>
<h2 id="📝3-4번-방법-장-단점">📝3, 4번 방법 장, 단점</h2>
<ul>
<li>3번과 4번은 둘 다 생성자를 사용한 방법으로서 객체 생성 시점에 값들을 한 번에 할당하기 때문에, 객체의 불변성을 유지하는 장점이 있습니다.</li>
<li>생성자의 인자가 잘못된 경우에 3번 방법의 경우 최악의 오류인 Runtime 오류가 발생하게 됩니다. </li>
<li>반면 4번 방법의 경우 Compile 오류가 발생합니다. Compile 시점에 오류가 발생하는 것이 가장 안전한 방법입니다.</li>
<li>4번 방법이 제일인것 같지만 DTO(MemberDto)에 @QueryProjection 어노테이션을 사용함으로써 DTO가 QueryDSl에 대한 의존성을 가지게 된다는 단점이 있습니다. </li>
<li>실무에서 사용할 경우 회사에서 지향하는 스타일을 사용하는 것이 현명하다고 생각합니다.  </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[벌크성 수정 쿼리 주의점 ]]></title>
            <link>https://velog.io/@woomin_s/%EB%B2%8C%ED%81%AC%EC%84%B1-%EC%88%98%EC%A0%95-%EC%BF%BC%EB%A6%AC-%EC%A3%BC%EC%9D%98%EC%A0%90</link>
            <guid>https://velog.io/@woomin_s/%EB%B2%8C%ED%81%AC%EC%84%B1-%EC%88%98%EC%A0%95-%EC%BF%BC%EB%A6%AC-%EC%A3%BC%EC%9D%98%EC%A0%90</guid>
            <pubDate>Sun, 07 May 2023 14:49:34 GMT</pubDate>
            <description><![CDATA[<h2 id="📝벌크성-수정-쿼리">📝벌크성 수정 쿼리</h2>
<ul>
<li>Spring Data Jpa를 배우고 공부하던 도중에 주의할 점이 있어 기록하려고 합니다.</li>
</ul>
<br>

<h2 id="📝member">📝Member</h2>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {&quot;id&quot;, &quot;username&quot;, &quot;age&quot;})  //team 은 출력 X!!! 연관 관계에 의한 무한 루프 발생
public class Member {

    @Id @GeneratedValue
    @Column(name = &quot;member_id&quot;)
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;team_id&quot;)
    private Team team;


    public Member(String userName, int age) {
        this.username = userName;
        this.age = age;
    }

    public Member(String userName, int age, Team team) {
        this.username = userName;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }

    //== 연관 관계 편의 메서드 ==//
    public void changeTeam(Team team) {
        this.team = team;
        team.getMemberList().add(this);
    }
}</code></pre>
<br>

<h2 id="📝벌크-수정-쿼리memberrepository">📝벌크 수정 쿼리(MemberRepository)</h2>
<pre><code class="language-java">     @Modifying
    @Query(&quot;update Member m set m.age = m.age + 1 where m.age &gt;= :age&quot;)
    int bulkAgePlus(@Param(&quot;age&quot;) int age);</code></pre>
<ul>
<li>bulkAgePlus Query : 파라미터로 받은 나이이거나 그 나이 이상이면 한 살을 더해주도록 하였습니다. </li>
</ul>
<br>

<h2 id="📝memberrepositorytest">📝MemberRepositoryTest</h2>
<pre><code class="language-java">     @Test
    public void bulkUpdate() {
        //given
        memberRepository.save(new Member(&quot;member1&quot;, 10));
        memberRepository.save(new Member(&quot;member2&quot;, 19));
        memberRepository.save(new Member(&quot;member3&quot;, 20));
        memberRepository.save(new Member(&quot;member4&quot;, 21));
        memberRepository.save(new Member(&quot;member5&quot;, 50));

        //when
        int resultCount = memberRepository.bulkAgePlus(20);   //벌크 연산 실행 직전 flush() 자동 호출

        Member findMember = memberRepository.findByUsername(&quot;member5&quot;).orElseGet(() -&gt; null);
        System.out.println(findMember);

        //then
        assertThat(resultCount).isEqualTo(3);
    }</code></pre>
<br>

<ul>
<li>&#39;member5&#39;로 예를 들겠습니다. </li>
<li>처음에 member5의 age가 51로 예상했습니다.</li>
</ul>
<ol>
<li><p><code>memberRepositoy.save</code> (em.persist)로 영속성 컨텍스트에 member5가 저장</p>
<table>
<thead>
<tr>
<th></th>
<th align="left">영속성 컨텍스트</th>
<th align="left">데이터베이스</th>
</tr>
</thead>
<tbody><tr>
<td>username</td>
<td align="left">member5</td>
<td align="left"></td>
</tr>
<tr>
<td>age</td>
<td align="left">50</td>
<td align="left"></td>
</tr>
</tbody></table>
</li>
<li><p><code>memberRepository.bulkAgePlus(20)</code>(JPQL) 실행 전 flush() 자동 호출</p>
<table>
<thead>
<tr>
<th></th>
<th align="left">영속성 컨텍스트</th>
<th align="left">데이터베이스</th>
</tr>
</thead>
<tbody><tr>
<td>username</td>
<td align="left">member5</td>
<td align="left">member5</td>
</tr>
<tr>
<td>age</td>
<td align="left">50</td>
<td align="left">50</td>
</tr>
</tbody></table>
</li>
</ol>
<ol start="3">
<li><p><code>memberRepository.bulkAgePlus(20)</code> 실행 후 </p>
<table>
<thead>
<tr>
<th></th>
<th align="left">영속성 컨텍스트</th>
<th align="left">데이터베이스</th>
</tr>
</thead>
<tbody><tr>
<td>username</td>
<td align="left">member5</td>
<td align="left">member5</td>
</tr>
<tr>
<td>age</td>
<td align="left">50</td>
<td align="left">51</td>
</tr>
</tbody></table>
</li>
<li><p><code>memberRepository.findByUsername(&quot;member5&quot;).orElseGet(() -&gt; null);</code></p>
<ul>
<li>데이터 베이스에서 값을 가져오기 때문에 51로 예상하였습니다.</li>
<li>하지만 <strong>결과는 50</strong>이었습니다. </li>
</ul>
<br>


</li>
</ol>
<h2 id="why">WHY?</h2>
<ul>
<li>em.find()나 지연로딩을 조회할 때는 영속성 컨텍스트(1차 캐시)에서 엔티티를 찾아옵니다.</li>
<li>반면 위의 bulkAgePlus나 findByUsername같은 JPQL은 항상 SQL로 번역되어서 데이터베이스를 통해 실행됩니다. </li>
<li>따라서 <strong>데이터베이스에서 username=&quot;member5&quot;인 member 데이터를 조회</strong>합니다. </li>
<li>하지만 이때 <strong>1차 캐시에 이미 username=&quot;member5&quot;가 있기 때문에</strong> 식별자 충돌이 일어나게 됩니다.</li>
<li><code>JPA는 영속성 컨텍스트의 동일성을 보장</code>하기 때문에 1차 캐시에 있는 값을 반환하게 됩니다. </li>
<li>그렇기 때문에 결과는 50이 나오게 됩니다.</li>
</ul>
<br>

<h2 id="해결방안">해결방안</h2>
<h3 id="방법1">방법1.</h3>
<pre><code class="language-java">     @Test
    public void bulkUpdate() {
        //given
        memberRepository.save(new Member(&quot;member1&quot;, 10));
        memberRepository.save(new Member(&quot;member2&quot;, 19));
        memberRepository.save(new Member(&quot;member3&quot;, 20));
        memberRepository.save(new Member(&quot;member4&quot;, 21));
        memberRepository.save(new Member(&quot;member5&quot;, 50));

        //when
        int resultCount = memberRepository.bulkAgePlus(20);   //벌크 연산 실행 직전 flush() 자동 호출
        em.clear();

        Member findMember = memberRepository.findByUsername(&quot;member5&quot;).orElseGet(() -&gt; null);
        System.out.println(findMember);

        //then
        assertThat(resultCount).isEqualTo(3);
    }</code></pre>
<h3 id="방법2">방법2.</h3>
<pre><code class="language-java">      @Modifying(clearAutomatically = true)
    @Query(&quot;update Member m set m.age = m.age + 1 where m.age &gt;= :age&quot;)
    int bulkAgePlus(@Param(&quot;age&quot;) int age);</code></pre>
<ul>
<li>EntityManager(em).clear를 통해 영속성 컨텍스트를 비워주는 작업을 진행합니다.</li>
<li>그렇게되면 데이터베이스의 결과값이 바로 반환되기 때문에 결과값은 51이 됩니다!</li>
<li><code>Member(id=5, username=member5, age=51)</code></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[장바구니 조회 ]]></title>
            <link>https://velog.io/@woomin_s/%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%EC%A1%B0%ED%9A%8C</link>
            <guid>https://velog.io/@woomin_s/%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%EC%A1%B0%ED%9A%8C</guid>
            <pubDate>Tue, 02 May 2023 19:10:08 GMT</pubDate>
            <description><![CDATA[<h2 id="📝server">📝Server</h2>
<h3 id="cartquerydto">cartQueryDto</h3>
<pre><code class="language-java">@Getter
@Setter
public class CartQueryDto {

    private Long cartItemId;
    private String itemName;  //상품명
    private int count;  //수량
    private int price; //상품 가격

    public CartQueryDto(Long cartItemId, String itemName, int count, int price) {
        this.cartItemId = cartItemId;
        this.itemName = itemName;
        this.count = count;
        this.price = price;
    }
}</code></pre>
<ul>
<li>CartQueryDto 클래스는 장바구니 조회에 쓰일 DTO입니다.</li>
</ul>
<hr>
<h3 id="cartqueryrepository">CartQueryRepository</h3>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class CartQueryRepository {

    private final EntityManager em;

     public List&lt;CartQueryDto&gt; findCartQueryDtos(Long cartId) {
        List&lt;CartQueryDto&gt; cartQueryDtoList = em.createQuery(
                        &quot;select new com.toyproject.ecommerce.repository.query.CartQueryDto(ci.id, i.name, ci.count, i.price, im.storeName)&quot; +
                                &quot; from CartItem ci&quot; +
                                &quot; join ci.item i&quot; +
                                &quot; join i.itemImageList im&quot; +
                                &quot; where ci.cart.id = :cartId and&quot; +
                                &quot; im.firstImage=&#39;Y&#39;&quot;, CartQueryDto.class)
                .setParameter(&quot;cartId&quot;, cartId)
                .getResultList();

        return cartQueryDtoList;
    }
}</code></pre>
<ul>
<li>각 사용자의 장바구니 목록을 조회하는 Repository입니다.</li>
<li>DTO를 직접 조회하는 방식을 선택하였습니다.</li>
<li>DTO를 직접 조회하는 방식은 말 그대로 엔티티를 조회하는 것이 아닌 DB를 조회해서 DTO의 필드에 값을 채워주는 방식입니다.</li>
<li>따라서 데이터를 선택적으로 조회 가능하다는 장점이 있습니다. </li>
<li>DTO를 직접 조회하는 방식은 엔티티 조회가 아니기 때문에 영속성 컨텍스트에 데이터가 들어가지 않습니다!!</li>
<li><a href="https://velog.io/@woomin_s/%EA%B2%BD%EB%A1%9C-%ED%91%9C%ED%98%84%EC%8B%9D-%EB%AC%B5%EC%8B%9C%EC%A0%81-%EC%A1%B0%EC%9D%B8-%EB%AA%85%EC%8B%9C%EC%A0%81-%EC%A1%B0%EC%9D%B8">묵시적 조인</a> 조심하기!! </li>
<li>상품 이미지를 출력하기 위해 itemImage 테이블에 firstImage 필드(대표 이미지 추가 여부)를 추가했습니다. </li>
</ul>
<blockquote>
<h3 id="cartqueryrepository를-따로-만든-이유">CartQueryRepository를 따로 만든 이유</h3>
</blockquote>
<ul>
<li>CartItemRepository에 쿼리를 작성하지 않고 CartQueryRepository를 따로 만들고 쿼리를 작성하였습니다.</li>
<li>CartItemRepository는 cartItem 엔티티를 조회하는 용도(핵심 비지니스)로만 사용하고 엔티티가 아닌 특정 화면을 조회하는 쿼리(장바구니 조회하는 쿼리 등), API 쿼리는 CartQueryRepository로 따로 작성하였습니다.</li>
<li>관심사의 분리 -&gt; 단일 책임 원칙(SRP, Single Reponsibility Principle)</li>
</ul>
<hr>
<h3 id="cartservice">CartService</h3>
<pre><code class="language-java">     @Transactional(readOnly = true)
    public List&lt;CartQueryDto&gt; findCartItems(Long memberId) {
        Cart cart = cartRepository.findByMemberId(memberId).orElseThrow(EntityNotFoundException::new);  // () -&gt; new EntityNotFoundException()
        List&lt;CartQueryDto&gt; cartQueryDtos = cartQueryRepository.findCartQueryDtos(cart.getId());
        return cartQueryDtos;
    }</code></pre>
<ul>
<li>회원의 장바구니 상품을 조회하는 서비스 로직입니다. </li>
</ul>
<hr>
<h3 id="cartcontroller">CartController</h3>
<pre><code class="language-java">     @GetMapping(&quot;/cart&quot;)
    public String cartView(Model model, HttpServletRequest request) {

        Member member = getMember(request);

        List&lt;CartQueryDto&gt; cartItemListForm = cartService.findCartItems(member.getId());
        model.addAttribute(&quot;cartItemListForm&quot;, cartItemListForm);

        return &quot;cart/cartView&quot;;
    }

    private Member getMember(HttpServletRequest request) {

        HttpSession session = request.getSession(false);

        //비로그인 사용자
        if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
            return null;
        }

        //세션에 저장되어있는 회원정보 가져오기
        Member member = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
        return member;
    }</code></pre>
<ul>
<li>세션을 조회해서 현재 세션에 저장되어있는 회원을 찾아서 서비스를 호출합니다.</li>
<li>장바구니 페이지(cartView)로 이동!</li>
</ul>
<br>

<hr>
<h2 id="📝장바구니-페이지-뷰">📝장바구니 페이지 뷰</h2>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/0b7f3917-6583-4209-aeeb-02d729b858d2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/92ffed45-cffb-4188-80c7-a37142fb19c2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/0eade683-9b87-495a-b7ac-3ccbb6ac4b9b/image.png" alt=""></p>
<ul>
<li><a href="https://github.com/woomin-Shim/e-commerce/blob/master/src/main/resources/templates/cart/cartView.html">GitHub</a></li>
<li>모델을 통해 전달된 장바구니에 담긴 상품정보를 화면에 출력해주는 작업을 진행하였습니다.</li>
<li>체크박스 속성을 설정해주고, Onchange을 이용하여 체크가 된 상품의 개수가 바뀌면 자동으로 계산되는 로직을 구현하였습니다.</li>
</ul>
<br>


<hr>
<h2 id="참고">참고</h2>
<p>체크박스 값 변화 감지 <a href="https://www.codingfactory.net/13044">https://www.codingfactory.net/13044</a>
<a href="https://carina16.tistory.com/143">https://carina16.tistory.com/143</a></p>
<p>다중 체크 값 <a href="https://doinge-coding.tistory.com/entry/jquery-check-box-%EB%8B%A4%EC%A4%91-%EC%B2%B4%ED%81%AC%EA%B0%92-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%AC%ED%8A%B8-%EC%B2%B4%ED%81%AC%EB%B0%95%EC%8A%A4-%EC%B2%B4%ED%81%AC%EA%B0%92-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0">https://doinge-coding.tistory.com/entry/jquery-check-box-%EB%8B%A4%EC%A4%91-%EC%B2%B4%ED%81%AC%EA%B0%92-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%AC%ED%8A%B8-%EC%B2%B4%ED%81%AC%EB%B0%95%EC%8A%A4-%EC%B2%B4%ED%81%AC%EA%B0%92-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[경로 표현식 (묵시적 조인, 명시적 조인)]]></title>
            <link>https://velog.io/@woomin_s/%EA%B2%BD%EB%A1%9C-%ED%91%9C%ED%98%84%EC%8B%9D-%EB%AC%B5%EC%8B%9C%EC%A0%81-%EC%A1%B0%EC%9D%B8-%EB%AA%85%EC%8B%9C%EC%A0%81-%EC%A1%B0%EC%9D%B8</link>
            <guid>https://velog.io/@woomin_s/%EA%B2%BD%EB%A1%9C-%ED%91%9C%ED%98%84%EC%8B%9D-%EB%AC%B5%EC%8B%9C%EC%A0%81-%EC%A1%B0%EC%9D%B8-%EB%AA%85%EC%8B%9C%EC%A0%81-%EC%A1%B0%EC%9D%B8</guid>
            <pubDate>Fri, 28 Apr 2023 06:34:57 GMT</pubDate>
            <description><![CDATA[<h2 id="📝경로-표현식">📝경로 표현식</h2>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class CartQueryRepository {

    private final EntityManager em;

    public List&lt;CartQueryDto&gt; findCartQueryDtos(Long cartId) {
        List&lt;CartQueryDto&gt; cartQueryDtoList = em.createQuery(
                        &quot;select new com.toyproject.ecommerce.repository.query.CartQueryDto(ci.id, i.name, ci.count, i.price)&quot; +
                                &quot; from CartItem ci&quot; +
                                &quot; join ci.item i&quot; +
                                &quot; where ci.cart.id = :cartId&quot;, CartQueryDto.class)
                .setParameter(&quot;cartId&quot;, cartId)
                .getResultList();

        return cartQueryDtoList;
    }
}</code></pre>
<p>실제 진행한 프로젝트 예제를 가지고 상태 필드, 묵시적 조인, 명시적 조인을 설명드립니다.</p>
<hr>
<h3 id="상태필드">상태필드</h3>
<ul>
<li>경로 탐색의 끝으로 더 이상 탐색을 할 수 없습니다.</li>
<li>위의 예제에서는 <code>ci.id, i.name, ci.count, i.price</code> 가 모두 상태필드입니다.</li>
</ul>
<br>

<h3 id="명시적-조인">명시적 조인</h3>
<ul>
<li>위의 예제와 같이 FROM절에서 명시적 조인(join ci.item i)을 통해 별칭을 얻고 </li>
<li>select 절에서 별칭을 통해 i.name, i.price를 탐색하고 있습니다. </li>
</ul>
<br>

<h3 id="묵시적-조인">묵시적 조인</h3>
<pre><code class="language-java">
    public List&lt;CartQueryDto&gt; findCartQueryDtos(Long cartId) {
        List&lt;CartQueryDto&gt; cartQueryDtoList = em.createQuery(
                        &quot;select new com.toyproject.ecommerce.repository.query.CartQueryDto(ci.id, ci.item.name, ci.count, ci.item.price)&quot; +
                                &quot; from CartItem ci&quot; +
                                &quot; where ci.cart.id = :cartId&quot;, CartQueryDto.class)
                .setParameter(&quot;cartId&quot;, cartId)
                .getResultList();</code></pre>
<ul>
<li>위의 예제는 묵시적 조인이 발생하는 예입니다. </li>
<li>ci.item.name, ci.item.price 에서 묵시적 내부 조인(inner) 조인이 발생합니다. </li>
<li>객체는 ci.item과 같이 접근이 가능하지만 데이터베이스의 경우에는 조인을 해야합니다. </li>
<li>그렇게 때문에 만일의 경우 의도하지 않은 JOIN이 발생하고 튜닝, 운영이 아주 힘들게 됩니다.</li>
</ul>
<br>

<h3 id="tip">TIP</h3>
<ul>
<li>묵시적 내부 조인이 발생하지 않게 명시적 조인을 사용하기!!</li>
<li>JPQL과 SQL를 비슷하게 짜기!!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ajax data Controller에서 받는 방법 ]]></title>
            <link>https://velog.io/@woomin_s/Ajax-data-Controller%EC%97%90%EC%84%9C-%EB%B0%9B%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@woomin_s/Ajax-data-Controller%EC%97%90%EC%84%9C-%EB%B0%9B%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 26 Apr 2023 15:27:12 GMT</pubDate>
            <description><![CDATA[<p>Ajax 데이터를 Controller에서 받는 몇 가지 방법을 소개해드립니다.
쇼핑몰 프로젝트에서 사용한 예시를 들어서 설명드리겠습니다.</p>
<h2 id="📝1-requestparam">📝1. @RequestParam</h2>
<ul>
<li><p>@RequestParam은 요청 파라미터나 Form으로 넘어온 데이터를 조회하는데 사용됩니다.</p>
<pre><code>&lt;!-- 장바구니 상품 삭제 --&gt;
&lt;script&gt;
  function deleteCartItem(itemId) {

      var cartItemForm = {
          cartItemId : itemId
      };

      $.ajax({
          url: &quot;/cart&quot;,
          type: &#39;DELETE&#39;,
          data: cartItemForm,

          &lt;!-- 성공시 --&gt;
          success: function (result) {
              alert(&quot;선택하신 상품이 삭제되었습니다.&quot;);
              location.href=&quot;/cart&quot;
          },
          &lt;!-- 실패시 --&gt;
          error: function (jqXHR) {
              alert(jqXHR.responseText);
              location.href = &quot;/cart&quot;;
          }
      })
  }
&lt;/script&gt;</code></pre><br>


</li>
</ul>
<pre><code class="language-java">    //Controller
     @DeleteMapping(&quot;/cart&quot;)
    public ResponseEntity&lt;String&gt; deleteCartItem(@RequestParam Long cartItemId) {

        log.info(&quot;itemId={}&quot;, cartItemId);

        if (cartService.findCartItem(cartItemId) == null) {
            return new ResponseEntity&lt;String&gt;(&quot;다시 시도해주세요.&quot;, HttpStatus.NOT_FOUND);
        }

        cartService.deleteCartItem(cartItemId);
        return ResponseEntity.ok(&quot;success&quot;);
    }</code></pre>
<ul>
<li>ajax는 &#39;itemId&#39;를 data에 매핑해서 controller로 보냅니다.</li>
<li>Controller의 @RequestParam을 이용하여 ajax에서 넘어온 &#39;itemId&#39;를 받을 수 있습니다.</li>
<li>ajax의 URL에 &#39;itemId&#39;를 파라미터로 추가해서 전송해도 되지만 특수문자가 들어올 경우를 대비해 data로 보내는 것이 맞다 생각합니다.</li>
</ul>
<hr>
<br>

<h2 id="📝2-modelattribute">📝2. @ModelAttribute</h2>
<ul>
<li>@ModelAttribute도 @RequestParam과 동일하게 요청 파라미터나 Form으로 넘어온 데이터를 조회하는데 사용됩니다.</li>
<li>Ajax 부분은 1번 방법과 동일합니다.</li>
</ul>
<pre><code class="language-java">    //Controller
     @DeleteMapping(&quot;/cart&quot;)
    public ResponseEntity&lt;String&gt; deleteCartItem(@ModelAttribute cartItemForm form) {

        log.info(&quot;itemId={}&quot;, form.getCartItemId());

        if (cartService.findCartItem(form.getCartItemId()) == null) {
            return new ResponseEntity&lt;String&gt;(&quot;다시 시도해주세요.&quot;, HttpStatus.NOT_FOUND);
        }

        cartService.deleteCartItem(form.getCartItemId());
        return ResponseEntity.ok(&quot;success&quot;);
    }</code></pre>
<pre><code class="language-java">@Getter
@Setter
public class cartItemForm {

    private Long cartItemId;

}</code></pre>
<ul>
<li>@ModelAttribute는 바로 객체로 받을 수 있습니다. </li>
</ul>
<hr>
<br>

<h2 id="📝3-requestbody">📝3. @RequestBody</h2>
<ul>
<li><p>@RequestBody의 경우 위의 @RequestParam, @ModelAttribute와 같이 요청 파라미터를 조회하는 것과는 상관이 없습니다,</p>
</li>
<li><p>@RequestBody는 메세지 바디를 직접 조회하는 기능입니다. 즉, Json 데이터를 조회할 수 있습니다. </p>
<pre><code> &lt;!-- 장바구니 상품 삭제 --&gt;
 &lt;script&gt;
  function deleteCartItem(itemId) {

      var cartItemForm = {
          cartItemId : itemId
      };

      $.ajax({
          url: &quot;/cart&quot;,
          type: &#39;DELETE&#39;,
          data: JSON.stringify(cartItemForm),
          contentType: &#39;application/json&#39;,

          &lt;!-- 성공시 --&gt;
          success: function (result) {
              alert(&quot;선택하신 상품이 삭제되었습니다.&quot;);
              location.href=&quot;/cart&quot;
          },
          &lt;!-- 실패시 --&gt;
          error: function (jqXHR) {
              alert(jqXHR.responseText);
              location.href = &quot;/cart&quot;;
          }
      })
  }
&lt;/script&gt;</code></pre></li>
<li><p>data를 Json으로 변환하여 Controller로 보내줍니다.</p>
<pre><code class="language-java">  //Controller
  @DeleteMapping(&quot;/cart&quot;)
  public ResponseEntity&lt;String&gt; deleteCartItem(@RequestBody cartItemForm form) {

      log.info(&quot;itemId={}&quot;, form.getCartItemId());

      if (cartService.findCartItem(form.getCartItemId()) == null) {
          return new ResponseEntity&lt;String&gt;(&quot;다시 시도해주세요.&quot;, HttpStatus.NOT_FOUND);
      }

      cartService.deleteCartItem(form.getCartItemId());
      return ResponseEntity.ok(&quot;success&quot;);
  }</code></pre>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/6fd3788f-21ca-4969-83d3-8937e0c73e3a/image.png" alt=""></p>
</li>
<li><p>JSON으로 넘어오는 데이터를 @RequestBody로 성공적으로 받았습니다.</p>
</li>
<li><p>@RequestBody 요청 흐름 : Ajax -&gt; JSON -&gt;  HTTP 메세지 컨버터 -&gt; 객체(CartItemForm)</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[장바구니 담기 기능 구현 ]]></title>
            <link>https://velog.io/@woomin_s/%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%EB%8B%B4%EA%B8%B0-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@woomin_s/%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%EB%8B%B4%EA%B8%B0-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 24 Apr 2023 18:17:24 GMT</pubDate>
            <description><![CDATA[<h2 id="📝상품-상세-페이지">📝상품 상세 페이지</h2>
<pre><code>&lt;!-- Product section--&gt;
&lt;section class=&quot;py-5&quot;&gt;
    &lt;div class=&quot;container px-4 px-lg-5 my-5&quot; th:object=&quot;${item}&quot;&gt;
        &lt;div class=&quot;row gx-4 gx-lg-5 align-items-center&quot; &gt;
            &lt;div class=&quot;col-md-6&quot;&gt;&lt;img class=&quot;card-img-top mb-5 mb-md-0&quot; th:src=&quot;|/images/${item.getItemImageListDto().get(0).getStoreName()}|&quot; alt=&quot;...&quot; /&gt;&lt;/div&gt;
            &lt;div class=&quot;col-md-6&quot;&gt;
                &lt;h1 class=&quot;display-5 fw-bolder&quot; th:text=&quot;${item.getName()}&quot;&gt;Shop item template&lt;/h1&gt;
                &lt;input type=&quot;hidden&quot; th:value=&quot;${item.itemId}&quot; id=&quot;id&quot; name=&quot;id&quot;&gt;
                &lt;div class=&quot;fs-5 mb-5&quot;&gt;
                    &lt;input type=&quot;hidden&quot; th:value=&quot;${item.price}&quot; id=&quot;price&quot; name=&quot;price&quot;&gt;
                    &lt;span class=&quot;text-decoration-none&quot; th:text=&quot;${item.getPrice()}&quot;&gt;&lt;/span&gt;원
                &lt;/div&gt;
                &lt;hr class=&quot;my-4&quot;&gt;


                &lt;div class=&quot;input-group fs-5 mb-5&quot;&gt;
                    &lt;div class=&quot;input-group-prepend&quot;&gt;
                        &lt;input type=&quot;hidden&quot; th:value=&quot;${item.stockQuantity}&quot; id=&quot;stockQuantity&quot; name=&quot;stockQuantity&quot;&gt;
                        &lt;span class=&quot;input-group-text&quot;&gt;주문 수량&lt;/span&gt;
                    &lt;/div&gt;
                    &lt;input class=&quot;form-control text-center me-3&quot; id=&quot;count&quot; name=&quot;count&quot; type=&quot;number&quot; value=&quot;1&quot; style=&quot; max-width: 5rem&quot; /&gt;
                &lt;/div&gt;


                &lt;div class=&quot;container bg-light&quot;&gt;
                    &lt;h6&gt;총 상품 금액&lt;/h6&gt;
                    &lt;h4 name=&quot;totalPrice&quot; id=&quot;totalPrice&quot; class=&quot;font-weight-bold&quot; &gt;&lt;/h4&gt;
                &lt;/div&gt;
                &lt;br&gt;

                &lt;div class=&quot;d-flex&quot;&gt;
                    &lt;form th:action=&quot;@{/logout}&quot; class=&quot;d-flex&quot; method=&quot;post&quot;&gt;
                        &lt;button class=&quot;btn btn-outline-dark&quot;
                                type=&quot;submit&quot;&gt;
                            바로 구매하기
                        &lt;/button&gt;
                    &lt;/form&gt;
                    &amp;nbsp
                    &lt;button class=&quot;btn btn-outline-dark flex-shrink-0&quot; type=&quot;button&quot; th:onclick=&quot;addCart()&quot;&gt;
                        &lt;i class=&quot;bi-cart-fill me-1&quot;&gt;&lt;/i&gt;
                        장바구니 담기
                    &lt;/button&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

    &lt;/div&gt;
&lt;/section&gt;


&lt;script th:inline=&quot;javascript&quot;&gt;

    &lt;!-- 장바구니 담기 --&gt;
    function addCart() {

        var count = $(&quot;#count&quot;).val();
        var itemId = $(&quot;#id&quot;).val();
        var cartForm = {
            itemId : itemId,
            count : count
        };

       $.ajax({
           url: &quot;/cart&quot;,
           data: cartForm,
           type: &#39;POST&#39;,
           success: function(result) {
               alert(&quot;상품을 담았습니다.&quot;);
               location.href = &quot;/&quot;;
           },

           error: function (jqXHR, textStatus, errorThrown) {
               alert(jqXHR.responseText);
               location.href=&quot;/members&quot;
           }
       })
    }
&lt;/script&gt;</code></pre><ul>
<li>장바구니 담기 버튼을 클릭하면 addCart 함수가 호출됩니다.</li>
<li>addCart() : <code>$(&quot;#count&quot;).val()</code>를 사용하여 구매자가 선택한 수량을 변수에 저장하고, <code>$(&quot;#id&quot;).val()</code>로 상품 아이디를 변수에 저장한 후 cartForm에 매치하였습니다.</li>
<li>서버와의 통신은 자바스크립트를 이용해서 비동기식으로 XML을 이용하여 서버와 통신하는 방식인 <a href="https://velog.io/@woomin_s/Ajax">Ajax</a>를 이용하였습니다.</li>
</ul>
<br>

<hr>
<h2 id="📝controller">📝Controller</h2>
<pre><code class="language-java"> //장바구니 담기
    @PostMapping(&quot;/cart&quot;)
    public ResponseEntity&lt;String&gt; addCart(@ModelAttribute CartForm cartForm, HttpServletRequest request) {

        Member member = getMember(request);
        //비로그인 회원은 장바구니를 가질 수 없다.
        if (member == null) {
            return new ResponseEntity&lt;String&gt;(&quot;로그인이 필요합니다.&quot;, HttpStatus.BAD_REQUEST);
        }

        cartService.addCart(member.getId(), cartForm.getItemId(), cartForm.getCount());
        return ResponseEntity.ok(&quot;success&quot;);
    }


        private Member getMember(HttpServletRequest request) {

        //세션 가져오기 
        HttpSession session = request.getSession(false);

        //비로그인 사용자
        if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
            return null;
        }

        //세션에 저장되어있는 회원정보 가져오기
        Member member = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
        return member;
    }
</code></pre>
<ul>
<li>비로그인 회원은 장바구니를 가질 수 없도록 구현하였습니다.</li>
<li>세션을 조회해서 회원 정보를 얻어와서 회원이 없으면 400 bad request로 응답하고, 회원 정보가 있으면 장바구니 담기 서비스를 호출합니다. </li>
</ul>
<br>

<h3 id="cartform">cartForm</h3>
<pre><code class="language-java">@Getter
@Setter
public class CartForm {

    private Long itemId;
    private int count;
}</code></pre>
<br>

<hr>
<h2 id="📝cartservice">📝cartService</h2>
<pre><code class="language-java">     /**
     * 장바구니 담기(추가)
     */
    public Long addCart(Long memberId, Long itemId, int count) {

        //엔티티 조회
        Member member = memberRepository.findById(memberId).get();
        Cart cart = cartRepository.findByMemberId(memberId).orElse(null);
        Item item = itemRepository.findById(itemId).get();

        //장바구니 없으면 생성
        if (cart == null) {
            log.info(&quot;장바구니 신규 생성&quot;);
            cart = Cart.createCart(member);
            cartRepository.save(cart);
        }

        //장바구니안에 장바구니 상품 조회
        CartItem cartItem = cartItemRepository.findByCartIdAndItemId(cart.getId(), item.getId()).orElse(null);



        //장바구니 상품이 없으면 생성
        if (cartItem == null) {
            cartItem = CartItem.createCartItem(count, cart, item);
            CartItem savedCartItem = cartItemRepository.save(cartItem);
            log.info(&quot;cartItemId={}&quot;, cartItem.getId());
            return savedCartItem.getId();
        }

        //장바구니 상품이 존재하면 수량 변경 (Dirty checking)
        cartItem.changeCount(count);
        return cartItem.getId();

    }</code></pre>
<ul>
<li>조회한 회원이 장바구니를 가지고 있지 않으면 새로 생성합니다.</li>
<li>장바구니 상품이 없으면 새로 생성하고 이미 존재한다면 수량만 변경해주도록 하였습니다.</li>
</ul>
<br>

<hr>
<h2 id="📝결과화면">📝결과화면</h2>
<br>

<h3 id="장바구니-담기-성공시">장바구니 담기 성공시</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/e16d16c4-74d5-4f22-be8a-1ebbd8633f72/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/2f5b5e3a-5e23-4990-91ce-763913df62cb/image.png" alt=""></p>
<br>

<h3 id="장바구니-담기-실패비로그인-회원">장바구니 담기 실패(비로그인 회원)</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/6718828e-e48d-4caa-9903-5a31fae573f4/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ajax]]></title>
            <link>https://velog.io/@woomin_s/Ajax</link>
            <guid>https://velog.io/@woomin_s/Ajax</guid>
            <pubDate>Mon, 24 Apr 2023 17:44:22 GMT</pubDate>
            <description><![CDATA[<h2 id="📝ajax란">📝Ajax란</h2>
<ul>
<li>빠르게 동작하는 동적인 웹 페이지를 만들기 위한 개발 기법의 하나로 자바스크립트를 이용해서 비동기식으로 XML을 이용하여 서버와 통신하는 방식입니다.</li>
<li>비동기식 : 여러가지 일이 동시적으로 발생한다는 뜻, 서버와 통신하는 동안 다른 작업을 할 수 있습니다.</li>
<li>Ajax를 이용하면 백그라운드 영역에서 서버와 통신하여, 그 결과를 웹 페이지의 일부분에서만 표시 가능합니다.</li>
</ul>
<br>

<h2 id="📝장점">📝장점</h2>
<ul>
<li>Ajax를 사용해서 서버와 데이터를 주고 받게 되면 서버에 보낼 필요한 핵심 데이터만 전송하기 때문에 새로고침 등 화면 깜빡임이 없고, 서버에 부담이 덜해 속도문제를 해결할 수 있습니다.</li>
</ul>
<br>

<h2 id="📝jquery를-이용한-ajax">📝Jquery를 이용한 Ajax</h2>
<ul>
<li>XMLHttpRequest를 직접 사용 하기 때문에, Ajax의 기본 method를 이용해서 서버와 통신을 하면 상당히 복잡합니다.</li>
<li>Jquery를 사용하면 단 몇 줄 만으로 간단하게 server와 Data를 주고받을 수 있습니다.</li>
</ul>
<br>

<h2 id="📝사용법">📝사용법</h2>
<ul>
<li><p>예제</p>
<pre><code>$.ajax({
         url: &quot;/cart&quot;,
         data: cartForm,
         type: &#39;POST&#39;,
         success: function(result) {
             alert(&quot;상품을 담았습니다.&quot;);
             location.href = &quot;/&quot;;
         },

         error: function (jqXHR, textStatus, errorThrown) {
             alert(jqXHR.responseText);
             location.href=&quot;/members&quot;
         }
     })</code></pre><table>
<thead>
<tr>
<th>key</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>url</td>
<td>데이터를 주고받을 url</td>
</tr>
<tr>
<td>data</td>
<td>보내는 데이터</td>
</tr>
<tr>
<td>type</td>
<td>데이터 전송 타입, HTTP 요청 방식(GET, POST)</td>
</tr>
<tr>
<td>dataType</td>
<td>Http 요청 후 return 하는 데이터의 Type을 지정(xml, Json ...)</td>
</tr>
<tr>
<td>success</td>
<td>Http 요청 성공시 발생하는 이벤트 핸들러</td>
</tr>
<tr>
<td>error</td>
<td>Http 요청 실패시 발생하는 이벤트 핸들러</td>
</tr>
</tbody></table>
</li>
</ul>
<br>

<ul>
<li>error키 영역에 3가지 파라미터(jqXHR, textStatus, errorThrown)을 사용가능합니다.</li>
<li>textStatus의 경우 &#39;error&#39;가 출력되고, 
errorThrown의 경우 &#39;Not Fount&#39; 등의 구체적인 HTTP 오류 메세지가 출력 됩니다.
jqXHR.responseText의 경우 서버측에서 보낸 메세지가 출력됩니다. </li>
</ul>
<br>

<h2 id="📝참고">📝참고</h2>
<ul>
<li><a href="https://www.tcpschool.com/ajax/ajax_intro_basic">https://www.tcpschool.com/ajax/ajax_intro_basic</a></li>
<li><a href="https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;blogId=afidev&amp;logNo=20184722536">https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;blogId=afidev&amp;logNo=20184722536</a></li>
<li><a href="https://api.jquery.com/jquery.ajax/">https://api.jquery.com/jquery.ajax/</a></li>
<li><a href="https://www.nextree.co.kr/p4771/">https://www.nextree.co.kr/p4771/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[상품 상세 페이지 ]]></title>
            <link>https://velog.io/@woomin_s/%EC%83%81%ED%92%88-%EC%83%81%EC%84%B8-%ED%8E%98%EC%9D%B4%EC%A7%80</link>
            <guid>https://velog.io/@woomin_s/%EC%83%81%ED%92%88-%EC%83%81%EC%84%B8-%ED%8E%98%EC%9D%B4%EC%A7%80</guid>
            <pubDate>Fri, 14 Apr 2023 06:03:47 GMT</pubDate>
            <description><![CDATA[<h2 id="📝상품-상세-페이지">📝상품 상세 페이지</h2>
<ul>
<li>상품 상세 페이지는 Bootstrap의 쇼핑몰 무료 템플릿을 참고하였습니다.</li>
</ul>
<hr>
<h3 id="itemcontroller">ItemController</h3>
<pre><code class="language-java">     /**
     * 상품 상세 조회
     */
    @GetMapping(&quot;items/{itemId}&quot;)
    public String itemView(@PathVariable(name = &quot;itemId&quot;) Long itemId, Model model) {
        Item item = itemService.findItem(itemId);
        List&lt;ItemImage&gt; itemImageList = itemImageService.findItemImageDetail(itemId, &quot;N&quot;);

        //엔티티 -&gt; DTO
        List&lt;ItemImageDto&gt; itemImageDtoList = itemImageList.stream()
                .map(ItemImageDto::new)
                .collect(Collectors.toList());

        ItemForm itemform = new ItemForm(
                item.getId(),
                item.getName(),
                item.getPrice(),
                item.getStockQuantity(),
                item.getDescription(),
                itemImageDtoList
        );

        model.addAttribute(&quot;item&quot;, itemform);

        return &quot;item/itemView&quot;;

    }</code></pre>
<ul>
<li>메인 페이지의 View Option을 클릭하면 상세화면으로 이동합니다.</li>
<li>메인 페이지에서 넘어오는 itemId를 이용하여 상품 정보와 상품 이미지를 데이터베이스에서 가져옵니다. </li>
<li><code>itemImageService.findItemImageDetail(itemId, &quot;N&quot;);</code> : 삭제 처리된 이미지는 화면에 반환하면 안되기 때문에 삭제 처리되지 않은 이미지들만 가져오도록 하였습니다. </li>
<li>엔티티를 View에 반환하지 않기 위해 DTO로 변환해주고 View에 전달하였습니다. <pre><code class="language-java">@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ItemForm {

</code></pre>
</li>
</ul>
<pre><code>private Long itemId;
@NotEmpty(message = &quot;상품 이름은 필수입니다.&quot;)
private String name;  //상품명
@NotNull(message = &quot;상품 가격은 필수입니다.&quot;)
private int price; //상품 가격
@NotNull(message = &quot;상품 재고 수량은 필수입니다.&quot;)
private int stockQuantity;  //재고 수량
private String description;  //상품 설명

//상품 수정, 장바구니,상품 상세에 사용
private List&lt;ItemImageDto&gt; itemImageListDto = new ArrayList&lt;&gt;();

public ItemServiceDTO toServiceDTO() {
    return ItemServiceDTO.builder()
            .id(itemId)
            .name(name)
            .price(price)
            .stockQuantity(stockQuantity)
            .description(description)
            .build();
}</code></pre><p>}</p>
<pre><code>
&lt;br&gt;

---

### 상품 상세 페이지</code></pre><!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="" />
    <meta name="author" content="" />
    <title>Roominis</title>
    <!-- Favicon-->
    <link rel="icon" type="image/x-icon" href="assets/favicon.ico" />
    <!-- Bootstrap icons-->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css" rel="stylesheet" />
    <!-- Core theme CSS (includes Bootstrap)-->
    <link href="/css/styles.css" rel="stylesheet" />

<pre><code>&lt;script src=&quot;https://code.jquery.com/jquery-3.5.1.min.js&quot;&gt;&lt;/script&gt;</code></pre></head>


<script th:inline="javascript">
    <!-- 처음 웹페이지 로딩시 호출 -->
    $(document).ready(function(){

        $("#description").val().replace()

        calculateTotalPrice();

        <!-- count 값이 변경될때마다 호출 -->
        $("#count").change(function(){
            calculateTotalPrice();
        });
    });

    <!-- 총 상품 금액 계산 -->
    function calculateTotalPrice(){

        var quantity = $("#stockQuantity").val()*1;
        var count = $("#count").val();
        var price = $("#price").val();


        <!-- 재고 부족 -->
        if (quantity < count) {
            alert("샹품 재고가 부족합니다. 재고:" + quantity + "개")
            return;
        }

        var totalPrice = price*count;
        $("#totalPrice").html(totalPrice + '원');
    }
</script>

<body>
<!-- Navigation-->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" style="background-color: #e3f2fd;">
    <div class="container px-4 px-lg-5">
        <a class="navbar-brand" th:href="@{/}">Home</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav me-auto mb-2 mb-lg-0 ms-lg-4">

<pre><code>        &lt;/ul&gt;
        &lt;form class=&quot;d-flex&quot;&gt;
            &lt;button class=&quot;btn btn-light btn-outline-secondary&quot; type=&quot;submit&quot;&gt;
                &lt;i class=&quot;bi-cart-fill me-1&quot;&gt;&lt;/i&gt;
                Cart
                &lt;span class=&quot;badge bg-dark text-white ms-1 rounded-pill&quot;&gt;0&lt;/span&gt;
            &lt;/button&gt;
        &lt;/form&gt;
    &lt;/div&gt;
&lt;/div&gt;</code></pre></nav>
<!-- Product section-->
<section class="py-5">
    <div class="container px-4 px-lg-5 my-5" th:object="${item}">
        <div class="row gx-4 gx-lg-5 align-items-center" >
            <div class="col-md-6"><img class="card-img-top mb-5 mb-md-0" th:src="|/images/${item.getItemImageListDto().get(0).getStoreName()}|" alt="..." /></div>
            <div class="col-md-6">
                <h1 class="display-5 fw-bolder" th:text="${item.getName()}">Shop item template</h1>
                <div class="fs-5 mb-5">
                    <input type="hidden" th:value="${item.price}" id="price" name="price">
                    <span class="text-decoration-none" th:text="${item.getPrice()}"></span>원
                </div>
                <hr class="my-4">


<pre><code>            &lt;div class=&quot;input-group fs-5 mb-5&quot;&gt;
                &lt;div class=&quot;input-group-prepend&quot;&gt;
                    &lt;input type=&quot;hidden&quot; th:value=&quot;${item.stockQuantity}&quot; id=&quot;stockQuantity&quot; name=&quot;stockQuantity&quot;&gt;
                    &lt;span class=&quot;input-group-text&quot;&gt;주문 수량&lt;/span&gt;
                &lt;/div&gt;
                &lt;input class=&quot;form-control text-center me-3&quot; id=&quot;count&quot; name=&quot;count&quot; type=&quot;number&quot; value=&quot;1&quot; style=&quot; max-width: 5rem&quot; /&gt;
            &lt;/div&gt;


            &lt;div class=&quot;container bg-light&quot;&gt;
                &lt;h6&gt;총 상품 금액&lt;/h6&gt;
                &lt;h4 name=&quot;totalPrice&quot; id=&quot;totalPrice&quot; class=&quot;font-weight-bold&quot; &gt;&lt;/h4&gt;
            &lt;/div&gt;
            &lt;br&gt;

            &lt;div class=&quot;d-flex&quot;&gt;
                &lt;form th:action=&quot;@{/logout}&quot; class=&quot;d-flex&quot; method=&quot;post&quot;&gt;
                    &lt;button class=&quot;btn btn-outline-dark&quot;
                            type=&quot;submit&quot;&gt;
                        바로 구매하기
                    &lt;/button&gt;
                &lt;/form&gt;
                &amp;nbsp
                &lt;button class=&quot;btn btn-outline-dark flex-shrink-0&quot; type=&quot;button&quot;&gt;
                    &lt;i class=&quot;bi-cart-fill me-1&quot;&gt;&lt;/i&gt;
                    장바구니 담기
                &lt;/button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

&lt;/div&gt;</code></pre></section>
<!-- Related items section-->
<section class="py-5 bg-light">
    <div class="container">
        <p class="lead" id="description" style="text-align: center" th:text="${item.getDescription()}">Lorem ipsum dolor sit amet consectetur adipisicing?</p>
    </div>
    <hr class="my-4">
    <div class="container px-4 px-lg-5 mt-5"  >
        <div th:each="itemImage : ${item.getItemImageListDto()}" class="text-center">
            <img class="card-img-top rounded mb-5 mb-md-0" style="padding-bottom: 50px; width: 550px; height:700px" th:src="|/images/${itemImage.getStoreName()}|" >
        </div>
    </div>
</section>
<!-- Footer-->
<footer class="py-5 bg-dark">
    <div class="container"><p class="m-0 text-center text-white">Copyright &copy; Roominis</p></div>
</footer>
<!-- Bootstrap core JS-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Core theme JS-->
<script src="js/scripts.js"></script>
</body>
</html>
```
- 컨트롤러에서 넘어온 상품 정보와 이미지를 이용해 화면에 표시되도록 하였습니다.
- JQuery를 이용하여 주문 수량이 변경(.change())되면 총 상품 금액도 그에 맞게 계산되도록 구현하였습니다.
- 주문 수량이 상품 재고를 초과하면 경고 메세지를 출력하도록 하였습니다. 

<br>

<hr>
<h3 id="결과-화면">결과 화면</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/4fbda5d7-2961-4189-af28-67c24755018e/image.gif" alt=""></p>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/734b527d-0ad9-4002-bd42-2565a1ce31e0/image.gif" alt=""></p>
<br>

<hr>
<h2 id="📝참고">📝참고</h2>
<p><a href="https://startbootstrap.com/template/shop-item">https://startbootstrap.com/template/shop-item</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[메인 페이지]]></title>
            <link>https://velog.io/@woomin_s/%EB%A9%94%EC%9D%B8-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%83%81%EC%84%B8-%ED%8E%98%EC%9D%B4%EC%A7%80</link>
            <guid>https://velog.io/@woomin_s/%EB%A9%94%EC%9D%B8-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%83%81%EC%84%B8-%ED%8E%98%EC%9D%B4%EC%A7%80</guid>
            <pubDate>Fri, 14 Apr 2023 05:40:59 GMT</pubDate>
            <description><![CDATA[<h2 id="📝메인-페이지">📝메인 페이지</h2>
<p>메인 페이지는 부트 스트랩의 무료 쇼핑몰 템플릿을 참고하였습니다!</p>
<h3 id="homecontroller">HomeController</h3>
<pre><code class="language-java">@Controller
@Slf4j
@RequiredArgsConstructor
public class HomeController {

    private final ItemService itemService;
    private final ItemImageService itemImageService;
    private final FileHandler fileHandler;

    @GetMapping(&quot;/&quot;)
    public String home(Model model, HttpServletRequest request) {
        List&lt;Item&gt; items = itemService.findItems();
           model.addAttribute(&quot;items&quot;, items);

        HttpSession session = request.getSession(false);

        //비로그인 사용자
        if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
            log.info(&quot;home controller&quot;);
            return &quot;home&quot;;
        }

        //로그인된 사용자
        log.info(&quot;userHome Controller&quot;);
        return &quot;userHome&quot;;

    }</code></pre>
<ul>
<li><a href="https://velog.io/@woomin_s/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84#postmappingmembers">로그인 기능구현</a> 에서 로그인에 성공하면 세션을 생성하고 세션에 회원정보를 보관하였습니다. </li>
<li>HomeController에서는 세션을 확인해서 세션이 없으면 비로그인 유저 페이지로 이동하고 세션이 생성되어있으면 로그인 유저 홈으로 이동합니다. </li>
</ul>
<br>

<hr>
<h3 id="비로그인-유저-메인-페이지">비로그인 유저 메인 페이지</h3>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&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, shrink-to-fit=no&quot; /&gt;
    &lt;meta name=&quot;description&quot; content=&quot;&quot; /&gt;
    &lt;meta name=&quot;author&quot; content=&quot;&quot; /&gt;
    &lt;title&gt;Roominis&lt;/title&gt;
    &lt;!-- Favicon--&gt;
    &lt;link rel=&quot;icon&quot; type=&quot;image/x-icon&quot; href=&quot;assets/favicon.ico&quot; /&gt;
    &lt;!-- Bootstrap icons--&gt;
    &lt;link href=&quot;https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css&quot; rel=&quot;stylesheet&quot; /&gt;
    &lt;!-- Core theme CSS (includes Bootstrap)--&gt;
    &lt;link href=&quot;css/styles.css&quot; rel=&quot;stylesheet&quot; /&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;!-- Navigation--&gt;
&lt;nav class=&quot;navbar navbar-expand-lg navbar-light bg-light&quot; &gt;
    &lt;div class=&quot;container px-4 px-lg-5&quot;&gt;
        &lt;a class=&quot;navbar-brand&quot; th:href=&quot;@{/}&quot;&gt;Roominis&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 ms-lg-4&quot;&gt;
            &lt;/ul&gt;

            &lt;form th:action=&quot;@{/members}&quot; class=&quot;d-flex&quot;&gt;
                &lt;button class=&quot;btn btn-outline-secondary&quot;
                        type=&quot;submit&quot;&gt;
                    로그인
                &lt;/button&gt;
            &lt;/form&gt;
            &amp;nbsp;
              &lt;form th:action=&quot;@{/members/new}&quot; class=&quot;d-flex&quot;&gt;
                &lt;button class=&quot;btn btn-outline-secondary&quot; type=&quot;submit&quot;&gt;
                    회원가입
                &lt;/button&gt;
            &lt;/form&gt;

        &lt;/div&gt;
    &lt;/div&gt;
&lt;/nav&gt;
&lt;!-- Header--&gt;
&lt;header class=&quot;bg-dark py-5&quot;&gt;
    &lt;div class=&quot;container px-4 px-lg-5 my-5&quot;&gt;
        &lt;div class=&quot;text-center text-white&quot;&gt;
            &lt;h1 class=&quot;display-4 fw-bolder&quot;&gt;Roominis&lt;/h1&gt;
            &lt;p class=&quot;lead fw-normal text-white-50 mb-0&quot;&gt;루미니스 쇼핑몰에 오신것을 환영합니다&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/header&gt;
&lt;!-- Section--&gt;
&lt;section class=&quot;py-5&quot;&gt;
    &lt;div class=&quot;container px-4 px-lg-5 mt-5&quot;&gt;
        &lt;div class=&quot;row gx-4 gx-lg-5 row-cols-2 row-cols-md-3 row-cols-xl-4 justify-content-center&quot;&gt;

            &lt;!-- 아무 상품도 등록하지 않았을 경우 --&gt;
            &lt;div th:if=&quot;${#lists.isEmpty(items)}&quot;&gt;
                &lt;div class=&quot;card h-100&quot;&gt;
                    &lt;!-- Product image--&gt;
                    &lt;img class=&quot;card-img-top&quot; src=&quot;https://dummyimage.com/450x300/dee2e6/6c757d.jpg&quot; alt=&quot;...&quot; /&gt;
                    &lt;!-- Product details--&gt;
                    &lt;div class=&quot;card-body p-4&quot;&gt;
                        &lt;div class=&quot;text-center&quot;&gt;
                            &lt;!-- Product name--&gt;
                            &lt;h5 class=&quot;fw-bolder&quot;&gt;상품 등록 대기중&lt;/h5&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class=&quot;col mb-5&quot; th:each=&quot;item : ${items}&quot;&gt;
                &lt;div class=&quot;card h-100&quot;&gt;
                    &lt;!-- Product image--&gt;
                    &lt;img class=&quot;card-img-top&quot; style=&quot;width: 268px; height: 300px&quot; th:src=&quot;|/images/${item.getItemImageList().get(0).getStoreName()}|&quot; alt=&quot;...&quot; /&gt;
                    &lt;!-- Product details--&gt;
                    &lt;div class=&quot;card-body p-4&quot;&gt;
                        &lt;div class=&quot;text-center&quot;&gt;
                            &lt;!-- Product name--&gt;
                            &lt;h5 class=&quot;fw-bolder&quot; th:text=&quot;${item.getName()}&quot;&gt;Product&lt;/h5&gt;
                            &lt;!-- Product price--&gt;
                            &lt;p th:text=&quot;|${item.price}₩|&quot;&gt;price&lt;/p&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;
                    &lt;!-- Product actions--&gt;
                    &lt;div class=&quot;card-footer p-4 pt-0 border-top-0 bg-transparent&quot;&gt;
                        &lt;div class=&quot;text-center&quot;&gt;
                            &lt;a class=&quot;btn btn-outline-dark mt-auto&quot; href=&quot;#&quot; th:href=&quot;@{/items/{id} (id=${item.id})}&quot;&gt;View options&lt;/a&gt;&lt;/div&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;

        &lt;/div&gt;
    &lt;/div&gt;
&lt;/section&gt;
&lt;!-- Footer--&gt;
&lt;footer class=&quot;py-5 bg-dark&quot;&gt;
    &lt;div class=&quot;container&quot;&gt;&lt;p class=&quot;m-0 text-center text-white&quot;&gt;Copyright &amp;copy; Your Website 2022&lt;/p&gt;&lt;/div&gt;
&lt;/footer&gt;
&lt;!-- Bootstrap core JS--&gt;
&lt;script src=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js&quot;&gt;&lt;/script&gt;
&lt;!-- Core theme JS--&gt;
&lt;script src=&quot;js/scripts.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><p><img src="https://velog.velcdn.com/images/woomin_s/post/95de974f-3968-449c-9781-94b2df6b9f7e/image.png" alt=""></p>
<br>

<hr>
<h3 id="로그인-유저-메인-페이지">로그인 유저 메인 페이지</h3>
<pre><code>&lt;!-- Navigation--&gt;
&lt;nav class=&quot;navbar navbar-expand-lg navbar-light bg-light&quot;&gt;
    &lt;div class=&quot;container px-4 px-lg-5&quot;&gt;
        &lt;a class=&quot;navbar-brand&quot; th:href=&quot;@{/}&quot;&gt;Roominis&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 ms-lg-4&quot;&gt;
                &lt;!--                &lt;li class=&quot;nav-item&quot;&gt;&lt;a class=&quot;nav-link&quot; href=&quot;#!&quot;&gt;About&lt;/a&gt;&lt;/li&gt;--&gt;
                &lt;li class=&quot;nav-item dropdown&quot;&gt;
                    &lt;a class=&quot;nav-link dropdown-toggle&quot; id=&quot;navbarDropdown&quot; href=&quot;#&quot; role=&quot;button&quot; data-bs-toggle=&quot;dropdown&quot; aria-expanded=&quot;false&quot;&gt;상품 관리&lt;/a&gt;
                    &lt;ul class=&quot;dropdown-menu&quot; aria-labelledby=&quot;navbarDropdown&quot;&gt;
                        &lt;li&gt;&lt;a class=&quot;dropdown-item&quot; th:href=&quot;@{/items/new}&quot;&gt;상품 등록&lt;/a&gt;&lt;/li&gt;
                        &lt;li&gt;&lt;hr class=&quot;dropdown-divider&quot; /&gt;&lt;/li&gt;
                        &lt;li&gt;&lt;a class=&quot;dropdown-item&quot; th:href=&quot;@{/items}&quot;&gt;상품 관리&lt;/a&gt;&lt;/li&gt;
                    &lt;/ul&gt;
                &lt;/li&gt;
            &lt;/ul&gt;
            &amp;nbsp
            &lt;form th:action=&quot;@{/logout}&quot; class=&quot;d-flex&quot; method=&quot;post&quot;&gt;
                &lt;button class=&quot;btn btn-outline-dark&quot;
                        type=&quot;submit&quot;&gt;
                    로그아웃
                &lt;/button&gt;
            &lt;/form&gt;
            &amp;nbsp;
            &lt;form class=&quot;d-flex&quot;&gt;
                &lt;button class=&quot;btn btn-outline-dark&quot; type=&quot;submit&quot;&gt;
                    &lt;i class=&quot;bi-cart-fill me-1&quot;&gt;&lt;/i&gt;
                    Cart
                    &lt;span class=&quot;badge bg-dark text-white ms-1 rounded-pill&quot;&gt;0&lt;/span&gt;
                &lt;/button&gt;
            &lt;/form&gt;

        &lt;/div&gt;
    &lt;/div&gt;
&lt;/nav&gt;</code></pre><br>

<h4 id="상품-등록-전">상품 등록 전</h4>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/9b437cf2-14c6-4ff2-913b-3cea4f43c874/image.png" alt=""></p>
<br>
<br>

<h4 id="상품-등록-후">상품 등록 후</h4>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/27abcda5-3fea-4b96-b0d2-ccd09e28f1d8/image.png" alt=""></p>
<br>

<hr>
<h2 id="참고">참고</h2>
<p><a href="https://startbootstrap.com/template/shop-homepage">https://startbootstrap.com/template/shop-homepage</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[상품 수정 ]]></title>
            <link>https://velog.io/@woomin_s/%EC%83%81%ED%92%88-%EC%88%98%EC%A0%95</link>
            <guid>https://velog.io/@woomin_s/%EC%83%81%ED%92%88-%EC%88%98%EC%A0%95</guid>
            <pubDate>Tue, 11 Apr 2023 08:23:30 GMT</pubDate>
            <description><![CDATA[<h2 id="📝상품-수정">📝상품 수정</h2>
<p>등록된 상품 정보, 이미지를 수정하는 기능을 구현하도록 하겠습니다.</p>
<hr>
<br>

<h2 id="📝itemform">📝ItemForm</h2>
<pre><code class="language-java">@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ItemForm {


    private Long itemId;
    @NotEmpty(message = &quot;상품 이름은 필수입니다.&quot;)
    private String name;  //상품명
    @NotNull(message = &quot;상품 가격은 필수입니다.&quot;)
    private int price; //상품 가격
    @NotNull(message = &quot;상품 재고 수량은 필수입니다.&quot;)
    private int stockQuantity;  //재고 수량
    private String description;  //상품 설명

    //상품 수정, 장바구니에 사용
    private List&lt;ItemImageDto&gt; itemImageListDto = new ArrayList&lt;&gt;();

    public ItemServiceDTO toServiceDTO() {
        return ItemServiceDTO.builder()
                .id(itemId)
                .name(name)
                .price(price)
                .stockQuantity(stockQuantity)
                .description(description)
                .build();
    }
</code></pre>
<ul>
<li>컨트롤러와 뷰와 통신할 때 쓰기위한 DTO </li>
</ul>
<br>

<hr>
<h2 id="📝itemcontroller">📝ItemController</h2>
<br>

<h3 id="getmappingitemsitemidedit">@GetMapping(&quot;/items/{itemId}/edit&quot;)</h3>
<pre><code class="language-java">@GetMapping(&quot;/items/{itemId}/edit&quot;)
    public String updateItemForm(@PathVariable(name = &quot;itemId&quot;) Long itemId, Model model) {

        Item findItem = itemService.findItem(itemId);
        List&lt;ItemImage&gt; itemImageList = itemImageService.findItemImageDetail(itemId, &quot;N&quot;);

        //엔티티 -&gt; DTO로 변환
        List&lt;ItemImageDto&gt; itemImageListDto = itemImageList.stream()
                .map(ItemImageDto::new)
                .collect(Collectors.toList());


        ItemForm itemForm = new ItemForm(
                findItem.getId(),
                findItem.getName(),
                findItem.getPrice(),
                findItem.getStockQuantity(),
                findItem.getDescription(),
                itemImageListDto
        );

        model.addAttribute(&quot;itemForm&quot;, itemForm);

        return &quot;item/updateItemForm&quot;;
    }</code></pre>
<ul>
<li><p>상품 수정 폼 </p>
</li>
<li><p>바로 이전 포스트(<a href="https://velog.io/@woomin_s/%EC%83%81%ED%92%88-%EB%AA%A9%EB%A1%9D">상품 목록</a>) 에서 상품 수정 버튼에 각자 아이템의 Primary key인 itemId를 넘기도록 구현하였습니다.</p>
</li>
<li><p>상품 수정화면에는 기존의 상품 정보를 화면에 보여주어야 하기 때문에 </p>
</li>
<li><p>Controller에서는 itemId를 받아와서 특정 아이템의 정보를 데이터베이스에서 찾아옵니다. </p>
<pre><code class="language-java">  //ItemService
  @Transactional(readOnly=true)
  public Item findItem(Long ItemId) {
      return itemRepository.findById(ItemId).orElse(null);
  }

  //== select * from Item where Item_Id = ItemId;</code></pre>
</li>
<li><p>itemId와 삭제 여부를 통해 상품 이미지 정보를 데이터베이스에서 찾아오도록 하였습니다. </p>
<pre><code class="language-java">  //ItemImageService  
  //삭제 여부를 판단하여 상품 이미지 정보를 조회한다
  @Transactional(readOnly = true)
  public List&lt;ItemImage&gt; findItemImageDetail(Long itemId, String YN) {
      return itemImageRepository.findByItemIdAndDeleteYN(itemId, YN);
  }</code></pre>
</li>
<li><p>엔티티를 외부(View)로 반환하지 않기 위해 DTO로 변환해주는 작업을 진행하였습니다. (Item-&gt;ItemForm, ItemImageList -&gt; ItemImageListDto)</p>
</li>
</ul>
<br>

<h3 id="postmapping">@PostMapping</h3>
<pre><code class="language-java">    @PostMapping(&quot;/items/{itemId}/edit&quot;)
    public String updateItem(@ModelAttribute ItemForm itemForm, Model model,
                             @RequestPart(name = &quot;itemImages&quot;) List&lt;MultipartFile&gt; multipartFiles) throws IOException {

        List&lt;ItemImage&gt; findItemImages = itemImageService.findItemImageDetail(itemForm.getItemId(), &quot;N&quot;);

        //상품 이미지를 등록안하면
        if (findItemImages.isEmpty() &amp;&amp; multipartFiles.get(0).isEmpty()) {
            model.addAttribute(&quot;errorMessage&quot;, &quot;상품 사진을 등록해주세요!&quot;);
            return &quot;item/itemForm&quot;;
        }

        //상품 정보 수정
        itemService.updateItem(itemForm.toServiceDTO(), multipartFiles);

        return &quot;redirect:/userHome&quot;;
    }</code></pre>
<ul>
<li><p>관리자가 입력한 상품 수정 정보(ItemForm)를 받아와서 상품 수정을 진행합니다. </p>
<pre><code class="language-java">  //ItemService 
  //상품 정보 업데이트 (Dirty Checking, 변경감지)
  public void updateItem(ItemServiceDTO itemServiceDTO,  List&lt;MultipartFile&gt; multipartFileList) throws IOException {

      Item findItem = itemRepository.findById(itemServiceDTO.getId()).orElse(null);  //DB에서 찾아옴 -&gt; 영속 상태
      findItem.updateItem(itemServiceDTO.getName(), itemServiceDTO.getDescription(), itemServiceDTO.getPrice(), itemServiceDTO.getStockQuantity());

      log.info(&quot;=====findItem={}&quot;, findItem.getName());

      //상품 이미지를 수정(삭제, 추가) 하지 않으면 실행 x 
      if(!multipartFileList.get(0).isEmpty()) {
          itemImageService.addItemImage(multipartFileList, findItem);
      }

  }</code></pre>
</li>
<li><p>데이터베이스에서 찾아온 findItem은 영속상태입니다.</p>
</li>
<li><p>findItem을 updateItem으로 값을 세팅해주면 스프링의 @Transactional에 의해서 트랜잭션이 commit됩니다. </p>
</li>
<li><p>그 후 영속성 컨텍스트에서 변경된 건이 있는지 찾기 위해 flush(EntityManager.flush)를 진행하고 위와 같이 변경된 값이 있으면 Update Query를 데이터베이스에 날려고 업데이트를 진행합니다. </p>
</li>
<li><p>상품 사진을 추가하기 위한 메소드 : addItemImage</p>
</li>
<li><p>데이터베이스의 값 업데이트 완료</p>
<pre><code class="language-java">//상품 이미지 추가
  public void addItemImage(List&lt;MultipartFile&gt; multipartFiles, Item item) throws IOException {
      List&lt;ItemImage&gt; itemImages = fileHandler.storeImages(multipartFiles);

      for (ItemImage itemImage : itemImages) {
          item.addItemImage(itemImageRepository.save(itemImage));
      }
  }</code></pre>
</li>
</ul>
<br>

<h3 id="postmappingitemdelete">@postMapping(&quot;item/delete&quot;)</h3>
<pre><code class="language-java">    /**
     * 아이템 이미지 삭제 처리
     */
    @PostMapping(&quot;item/delete&quot;)
    public String deleteItemImage(@RequestParam(&quot;itemImageId&quot;) Long itemImageId, @RequestParam(&quot;itemId&quot;) Long itemId) {

        itemImageService.delete(itemImageId);

        return &quot;redirect:/items/ &quot; + itemId + &quot;/edit&quot;;
    }</code></pre>
<ul>
<li>상품 이미지를 삭제처리 하는 컨트롤러입니다.</li>
<li>이미지를 삭제하는 방법은 데이터베이스의 내용을 실제로 삭제하는 법과 데이터베이스의 내용을 삭제하지 않고 필드의 삭제 플래그를 통해 삭제 로직을 구현하는 방법이 있습니다. 둘 다 장 단점이 있다고 하는데 저는 이번에 후자의 방법을 택했습니다.</li>
<li>상품 수정 화면에서 삭제 버튼을 클릭(아래 쪽 참고)하면 삭제 처리되는 로직입니다.</li>
<li>등록된 상품 이미지를 조회하고 싶을 때 ItemImage의 deleteYN이 &#39;N&#39;인 것을 조회하면 됩니다. <pre><code class="language-java">  //ItemImageSerivce
    //데이터베이스를 삭제하지 않고 flag를 이용하여 처리
  public void delete(Long itemImageId) {
      ItemImage itemImage = itemImageRepository.findById(itemImageId).get();
      itemImage.deleteSet(&quot;Y&quot;);
  }</code></pre>
</li>
</ul>
<br>

<hr>
<h2 id="📝테스트">📝테스트</h2>
<pre><code class="language-java">@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class ItemServiceTest {

    @Autowired
    ItemRepository itemRepository;
    @Autowired
    ItemService itemService;

    private Item createItem() {
        return Item.createItem(&quot;후드 집업&quot;, &quot;데상트 후드 집업&quot;, 70000, 300);
    }

    @Test
    @DisplayName(&quot;상품 수정 테스트&quot;)
    public void updateItem() {
        //given
        Item item = this.createItem();
        itemRepository.save(item);

        //when
        Item findItem = itemRepository.findById(item.getId()).orElse(null);
        findItem.updateItem(&quot;후드 집업&quot;, &quot;지프 후드 집업&quot;, 55000, 300);

        //then
        Assert.assertEquals(item.getName(), &quot;후드 집업&quot;);
        Assert.assertEquals(item.getPrice(), 55000);

    }
}</code></pre>
<br>

<hr>
<h2 id="📝상품-수정-화면">📝상품 수정 화면</h2>
<pre><code>&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;th:block layout:fragment=&quot;script&quot;&gt;
        &lt;script th:inline=&quot;javascript&quot;&gt;
            var error = [[${errorMessage}]];
            if(error != null){
                alert(error);
            }
        &lt;/script&gt;
    &lt;/th:block&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;section&gt;
    &lt;div layout:fragment=&quot;content&quot; class=&quot;container&quot;&gt;
        &lt;div class=&quot;py-5 text-center&quot;&gt;
            &lt;h2&gt;상품 수정&lt;/h2&gt;
        &lt;/div&gt;

        &lt;form th:action th:object=&quot;${itemForm}&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&gt;
            &lt;div th:if=&quot;${#fields.hasGlobalErrors()}&quot;&gt;
                &lt;p class=&quot;field-error&quot; th:each=&quot;err : ${#fields.globalErrors()}&quot;
                   th:text=&quot;${err}&quot;&gt;전체 오류 메시지&lt;/p&gt;
            &lt;/div&gt;

            &lt;input type=&quot;hidden&quot; th:field=&quot;*{itemId}&quot;&gt;

            &lt;div&gt;
                &lt;label for=&quot;name&quot;&gt;상품명&lt;/label&gt;
                &lt;input type=&quot;text&quot; id=&quot;name&quot; th:field=&quot;*{name}&quot; class=&quot;form-control&quot; placeholder=&quot;상품명을 입력해주세요&quot;
                       th:errorclass=&quot;field-error&quot;&gt;
                &lt;div class=&quot;field-error&quot; th:errors=&quot;*{name}&quot; /&gt;
            &lt;/div&gt;
            &lt;br&gt;
            &lt;div&gt;
                &lt;label for=&quot;price&quot;&gt;가격&lt;/label&gt;
                &lt;input type=&quot;number&quot; id=&quot;price&quot; th:field=&quot;*{price}&quot; class=&quot;form-control&quot; placeholder=&quot;가격을 입력해주세요&quot;
                       th:errorclass=&quot;field-error&quot;&gt;
                &lt;div class=&quot;field-error&quot; th:errors=&quot;*{price}&quot; /&gt;
            &lt;/div&gt;
            &lt;br&gt;
            &lt;div&gt;
                &lt;label for=&quot;stockQuantity&quot;&gt;재고 수량&lt;/label&gt;
                &lt;input type=&quot;number&quot; id=&quot;stockQuantity&quot; th:field=&quot;*{stockQuantity}&quot; class=&quot;form-control&quot; placeholder=&quot;재고 수량을 입력해주세요&quot;
                       th:errorclass=&quot;field-error&quot;&gt;
                &lt;div class=&quot;field-error&quot; th:errors=&quot;*{stockQuantity}&quot; /&gt;
            &lt;/div&gt;
            &lt;br&gt;
            &lt;div&gt;
                &lt;label for=&quot;description&quot;&gt;상품 상세 설명&lt;/label&gt;
                &lt;textarea id=&quot;description&quot; th:field=&quot;*{description}&quot; class=&quot;form-control&quot; aria-label=&quot;With textarea&quot;&gt;&lt;/textarea&gt;
            &lt;/div&gt;
            &lt;br&gt;
            &lt;br&gt;
            &lt;div&gt;
                &lt;label&gt;상품 이미지&lt;/label&gt;
                &lt;div class=&quot;form-group&quot; th:each=&quot;itemImage : ${itemForm.itemImageListDto}&quot;&gt;
                    &lt;div class=&quot;custom-file img-div&quot;&gt;
                         &lt;span th:text=&quot;${itemImage.originalName}&quot;&gt;파일이름1.png&lt;/span&gt;
                        &lt;span&gt;
                            &lt;button th:fileId=&quot;${itemImage.id}&quot; th:onclick=&quot;itemImgageDelete(this.getAttribute(&#39;fileId&#39;))&quot;
                             type=&quot;button&quot; class=&quot;btn btn-outline-danger&quot;&gt;삭제&lt;/button&gt;
                        &lt;/span&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;

             &lt;div class=&quot;mb-3&quot;&gt;
                &lt;label for=&quot;formFileMultiple&quot; class=&quot;form-label&quot;&gt;파일업로드&lt;/label&gt;
                &lt;input class=&quot;form-control&quot; type=&quot;file&quot; id=&quot;formFileMultiple&quot; name=&quot;itemImages&quot; multiple&gt;
            &lt;/div&gt;


            &lt;br&gt;
            &lt;br&gt;
            &lt;div style=&quot;text-align:center&quot;&gt;
                &lt;button class=&quot;w-50 btn btn-primary btn-lg&quot; th:align=&quot;center&quot; type=&quot;submit&quot;&gt;
                    상품 수정&lt;/button&gt;
            &lt;/div&gt;
            &lt;br&gt;
            &lt;br&gt;

        &lt;/form&gt;
    &lt;/div&gt; &lt;!-- /container --&gt;
&lt;/section&gt;
&lt;/body&gt;
&lt;/html&gt;

&lt;script&gt;
    function itemImgageDelete(fileId){
        if (confirm(&quot;정말로 삭제하시겠습니까?&quot;)) {
            //배열생성
            const form = document.createElement(&#39;form&#39;);
            form.setAttribute(&#39;method&#39;, &#39;post&#39;);
            form.setAttribute(&#39;action&#39;, &#39;/item/delete&#39;);

            //파일 id
            var input1 = document.createElement(&#39;input&#39;);
            input1.setAttribute(&quot;type&quot;, &quot;hidden&quot;);
            input1.setAttribute(&quot;name&quot;, &quot;itemImageId&quot;);
            input1.setAttribute(&quot;value&quot;, fileId);

            //게시판 id
            const selectedElements = document.querySelector(&quot;#itemId&quot;)
            var input2 = document.createElement(&#39;input&#39;);
            input2.setAttribute(&quot;type&quot;, &quot;hidden&quot;);
            input2.setAttribute(&quot;name&quot;, &quot;itemId&quot;);
            input2.setAttribute(&quot;value&quot;, selectedElements.value);

            form.appendChild(input1);
            form.appendChild(input2);
            document.body.appendChild(form);
            form.submit();
        }
    }
&lt;/script&gt;</code></pre><br>

<h3 id="상품-수정-전">상품 수정 전</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/3b03db08-d16f-4540-943d-fbf16a3a6947/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/9a938c10-7a57-4b1b-89bb-691183abb5c3/image.png" alt=""></p>
<br>

<h3 id="상품-수정-후">상품 수정 후</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/1caf2a7f-40f7-4454-b638-c9b5b661c295/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/1df22f47-0dc1-4226-84ad-3847b8634838/image.png" alt=""></p>
<hr>
<h2 id="📝-수정화면-참고">📝 수정화면 참고</h2>
<ul>
<li><a href="https://aamoos.tistory.com/689">https://aamoos.tistory.com/689</a></li>
<li><a href="https://to-dy.tistory.com/102">https://to-dy.tistory.com/102</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[상품 목록 ]]></title>
            <link>https://velog.io/@woomin_s/%EC%83%81%ED%92%88-%EB%AA%A9%EB%A1%9D</link>
            <guid>https://velog.io/@woomin_s/%EC%83%81%ED%92%88-%EB%AA%A9%EB%A1%9D</guid>
            <pubDate>Tue, 11 Apr 2023 06:42:54 GMT</pubDate>
            <description><![CDATA[<h2 id="📝상품-목록-화면-미리보기">📝상품 목록 화면 미리보기</h2>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/65c3c763-edf2-43d7-a59b-b5402b2dfbea/image.png" alt=""></p>
<br>

<hr>
<h2 id="📝itemservice">📝ItemService</h2>
<pre><code class="language-java">    @Transactional(readOnly=true)
    public List&lt;Item&gt; findItems() {
        return itemRepository.findAll();
    }</code></pre>
<ul>
<li>데이터베이스에 저장되어있는 모든 상품 정보를 Select 합니다.</li>
<li>SELECT * FROM ITEM;</li>
</ul>
<br>

<hr>
<h2 id="📝itemcontroller">📝ItemController</h2>
<pre><code class="language-java">    /**
     * 상품 목록 조회
     */
    @GetMapping(&quot;/items&quot;)
    public String items(Model model) {

        List&lt;Item&gt; itemList = itemService.findItems();

        //엔티티 -&gt; DTO
        List&lt;ItemDto&gt; itemListDto = itemList.stream()
                .map(item -&gt; new ItemDto(item))
                .collect(Collectors.toList());

        model.addAttribute(&quot;itemList&quot;, itemListDto);

        return &quot;item/itemList&quot;;
    }</code></pre>
<ul>
<li>DI한 itemService를 통해 모든 아이템 정보를 찾아옵니다.</li>
<li>view layer에 모델로 데이터를 넘겨줄 때는 엔티티가 노출되지 않도록 DTO로 변환해서 넘겨주었습니다. </li>
</ul>
<br>

<hr>
<h2 id="📝itemlisthtml">📝itemList.html</h2>
<pre><code>&lt;body&gt;

&lt;div layout:fragment=&quot;content&quot; class=&quot;container&quot;&gt;
    &lt;div  class=&quot;py-5 text-center&quot;&gt;
        &lt;h2&gt;등록된 상품 목록&lt;/h2&gt;
    &lt;/div&gt;
    &lt;br&gt;


    &lt;div&gt;
        &lt;table th:name=&quot;dd&quot; class=&quot;table table-striped&quot;&gt;
            &lt;thead&gt;
            &lt;tr&gt;
                &lt;th&gt;상품명&lt;/th&gt;
                &lt;th&gt;가격&lt;/th&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;item : ${itemList}&quot;&gt;
                &lt;tg th:hidden th:field=&quot;${item.id}&quot;&gt;&lt;/tg&gt;
                &lt;td th:text=&quot;${item.name}&quot;&gt;&lt;/td&gt;
                &lt;td th:text=&quot;${item.price}&quot;&gt;&lt;/td&gt;
                &lt;td th:text=&quot;${item.stockQuantity}&quot;&gt;&lt;/td&gt;
                &lt;td th:text=&quot;${item.description}&quot;&gt;&lt;/td&gt;
                &lt;td&gt;
                    &lt;a href=&quot;#&quot; th:href=&quot;@{/items/{id}/edit (id=${item.id})}&quot;
                       class=&quot;btn btn-primary&quot; role=&quot;button&quot;&gt;수정&lt;/a&gt;
                &lt;/td&gt;
            &lt;/tr&gt;
            &lt;/tbody&gt;
        &lt;/table&gt;
    &lt;/div&gt;
&lt;/div&gt; &lt;!-- /container --&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><ul>
<li>상품 목록을 보여주는 html을 작성하였습니다.</li>
<li>상품 수정은 상품 목록을 통해 수정 가능하도록 하였습니다. </li>
<li>각각 다른 아이템들을 수정하기 위해 수정 버튼에 primary key인 아이디를 링크로 달아주었습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[상품 등록 ]]></title>
            <link>https://velog.io/@woomin_s/%EC%83%81%ED%92%88-%EB%93%B1%EB%A1%9D</link>
            <guid>https://velog.io/@woomin_s/%EC%83%81%ED%92%88-%EB%93%B1%EB%A1%9D</guid>
            <pubDate>Wed, 05 Apr 2023 06:37:18 GMT</pubDate>
            <description><![CDATA[<h2 id="📝상품-엔티티">📝상품 엔티티</h2>
<br>

<p>[상품(item) 엔티티]
(<a href="https://velog.io/@woomin_s/%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%84%A4%EA%B3%84#-%EC%83%81%ED%92%88">https://velog.io/@woomin_s/%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%84%A4%EA%B3%84#-%EC%83%81%ED%92%88</a>)
[상품 사진(item_image) 엔티티]
(<a href="https://velog.io/@woomin_s/%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%84%A4%EA%B3%84#-%EC%83%81%ED%92%88-%EC%82%AC%EC%A7%84">https://velog.io/@woomin_s/%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%84%A4%EA%B3%84#-%EC%83%81%ED%92%88-%EC%82%AC%EC%A7%84</a>)</p>
<hr>
<h2 id="📝repository">📝Repository</h2>
<br>

<h3 id="itemrepository">ItemRepository</h3>
<pre><code class="language-java">public interface ItemRepository extends JpaRepository&lt;Item, Long&gt; {
}</code></pre>
<br>

<h3 id="itemimagerepository">ItemImageRepository</h3>
<pre><code class="language-java">public interface ItemImageRepository extends JpaRepository&lt;ItemImage, Long&gt; {
}</code></pre>
<br>

<hr>
<h2 id="📝applicationproperties">📝application.properties</h2>
<pre><code>file.dir=C:/Users/Woomin/Desktop/study/ImageStorage/</code></pre><ul>
<li>application.properties파일에 이미지 업로드 경로를 지정해주었습니다. </li>
<li>이미지 파일들은 데이터베이스에 저장하지 않고 로컬 컴퓨터의 스토리지에 저장(AWS 같은 경우 AWS S3)하고, 데이터베이스에는 이미지 파일의 정보(경로)만 저장되도록 합니다.</li>
</ul>
<hr>
<br>

<h2 id="📝service">📝Service</h2>
<h3 id="filehandler">fileHandler</h3>
<pre><code class="language-java">@Component
public class FileHandler {

    @Value(&quot;${file.dir}&quot;)
    private String fileDir;

    //파일 경로명
    public String getFullPath(String filename) {
        return fileDir + filename;
    }

    //파일 경로명(스토리지)에 사진 저장
    public List&lt;ItemImage&gt; storeImages(List&lt;MultipartFile&gt; multipartFiles) throws IOException {
        List&lt;ItemImage&gt; storeResult = new ArrayList&lt;&gt;();
        for (MultipartFile multipartfile : multipartFiles) {
            storeResult.add(storeImage(multipartfile));
        }
        return storeResult;
    }

    public ItemImage storeImage(MultipartFile multipartFile) throws IOException {
        if (multipartFile.isEmpty()) {
            return null;
        }

        // ex) image1.jpeg
        String oriImageName = multipartFile.getOriginalFilename();

        //서버에 저장될 파일명
        String storeImageName = createStoreImageName(oriImageName);

        //스토리지에 저장
        multipartFile.transferTo(new File(getFullPath(storeImageName)));

        return ItemImage.builder()
                .originalName(oriImageName)
                .storeName(storeImageName)
                .build();
    }

    private String createStoreImageName(String oriImageName) {
        String ext = extractExt(oriImageName);  //jpeg
        String uuid = UUID.randomUUID().toString();
        return uuid + &quot;.&quot; + ext;
    }

    //확장자 추출
    private String extractExt(String oriImageName) {
        int pos = oriImageName.lastIndexOf(&quot;.&quot;);
        return oriImageName.substring(pos + 1);
    }
}</code></pre>
<ul>
<li>이미지 파일 저장과 관련된 업무를 처리하기 위한 component입니다.</li>
<li><code>@Value(&quot;${file.dir}&quot;)</code> 로 application.properties에 작성해둔 이미지 업로드 경로를 쉽게 불러올 수 있습니다.</li>
<li><code>public ItemImage storeImage(MultipartFile multipartFile)</code> 메소드는 업로드된 이미지인 multipartFile로 원본 파일명, 서버에 저장될 파일명을 추출하고 스토리지에 저장하는 업무를 처리합니다.<blockquote>
<p>굳이 원본 파일명과 서버에 저장하는 파일명을 구분하는 이유?
사용자가 얼마 없다면 파일명이 중복되는 문제가 발생할 가능성이 없지만 만약 사용자가 수십만명이 된다면 파일명이 충분히 중복될 가능성이 높고 기존 파일 이름과 충돌이 나기 때문에 문제가 발생될 수 있습니다. 때문에 중복이 불가능한 UUID를 생성하고 서버에서 관리하는 별도의 파일명으로 저장합니다.</p>
</blockquote>
</li>
</ul>
<br>

<h3 id="itemservice">ItemService</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional
public class ItemService {

    private final ItemRepository itemRepository;
    private final ItemImageRepository itemImageRepository;
    private final FileHandler filehandler;

    public Long saveItem(ItemServiceDTO itemServiceDTO, List&lt;MultipartFile&gt; multipartFileList) throws IOException {
        Item item = Item.createItem(itemServiceDTO.getName(),
                itemServiceDTO.getDescription(),
                itemServiceDTO.getPrice(),
                itemServiceDTO.getStockQuantity());

        List&lt;ItemImage&gt; itemImages = filehandler.storeImages(multipartFileList);


        for (ItemImage itemImage : itemImages) {
            item.addItemImage(itemImageRepository.save(itemImage));
        }

        return itemRepository.save(item).getId();
    }
}</code></pre>
<ul>
<li>상품 정보를 저장하는 서비스 로직입니다.</li>
<li>DI(Dependecy Injection) : ItemRepository, ItemImageRepository, FileHandler</li>
<li>filehandler.storeImages 메소드를 통해 이미지들을 넘겨 스토리지에 저장하고 원본 파일명, 서버에 저장될 파일명을 추출하고 ItemImage list로 반환받습니다. </li>
<li>itemImageRepository에 상품 이미지 정보를 저장함과 동시에 연관 관계 편의 메소드를 호출해 양쪽에 값을 세팅해줍니다. <pre><code class="language-java">  public void addItemImage(ItemImage itemImage) {
      itemImageList.add(itemImage);
      itemImage.changeItem(this);
  }
</code></pre>
</li>
</ul>
<pre><code>
&lt;br&gt;

----

## 📝Controller

```java
@Controller
@Slf4j
@RequiredArgsConstructor
public class ItemController {

    private final ItemService itemService;

    @GetMapping(&quot;/items/new&quot;)
    public String createItemForm(Model model) {
        model.addAttribute(&quot;itemForm&quot;, new ItemForm());
        return &quot;item/itemForm&quot;;
    }

    @PostMapping(&quot;/items/new&quot;)
    public String createItem(@Valid @ModelAttribute ItemForm itemForm, BindingResult bindingResult, Model model,
                            @RequestPart(name = &quot;itemImages&quot;) List&lt;MultipartFile&gt; multipartFiles
    ) throws IOException {

        if (bindingResult.hasErrors()) return &quot;item/itemForm&quot;;

        //상품 이미지를 등록안하면
        if (multipartFiles.get(0).isEmpty()) {
            model.addAttribute(&quot;errorMessage&quot;, &quot;상품 사진을 등록해주세요!&quot;);
            return &quot;item/itemForm&quot;;
        }

        itemService.saveItem(itemForm.toServiceDTO(), multipartFiles);

        return &quot;redirect:/userHome&quot;;
    }

    /**
     * 컨트롤러와 서비스간 통신을 할 때, 컨트롤러가 뷰와 통신할 때 사용한 DTO를 그대로 사용하면
     * 강한 의존이 생겨 위험!!
     */
}</code></pre><br>


<h3 id="getmappingitemsnew">GetMapping(&quot;/items/new&quot;)</h3>
<ul>
<li>상품 등록 폼</li>
<li>ItemForm을 Model을 통해 뷰로 넘겨주었습니다. thymeleaf는 이 객체를 통해 유효성 검증을 할 수 있습니다.</li>
</ul>
<h3 id="postmappingitemsnew">PostMapping(&quot;/items/new&quot;)</h3>
<ul>
<li><a href="https://velog.io/@woomin_s/MultipartFile-%EC%8B%9C%ED%96%89%EC%B0%A9%EC%98%A4">시행착오</a></li>
<li>상품 이름, 가격, 수량을 입력하지 않거나 상품 이미지를 등록하지 않으면 다시 상품 입력 화면으로 오도록 구현하였습니다.</li>
<li>DTO의 사용 범위 : 컨트롤러에서 DTO를 엔티티로 바꾸어서 서비스로 넘겨줄 지 서비스에서 엔티티로 바꿀지 고민을 하였는데 현재 프로젝트에서는 서비스에서 엔티티로 변환하게 구현하였습니다. </li>
<li>DTO에 관해서는 다음에 자세하게 게시글로 작성하겠습니다!</li>
</ul>
<br>

<hr>
<h2 id="📝test">📝Test</h2>
<pre><code class="language-java">  @PostMapping(&quot;/items/new&quot;)
    @ResponseBody
    public ResponseEntity&lt;String&gt; createItem(@ModelAttribute(name = &quot;itemForm&quot;) ItemForm form,
                                             @RequestPart(name = &quot;itemImages&quot;) List&lt;MultipartFile&gt; multipartFiles) throws IOException {

        itemService.saveItem(form.toServiceDTO(), multipartFiles);

        return ResponseEntity.ok(&quot;이미지 업로드 성공&quot;);
    }</code></pre>
<ul>
<li><p>상품 등록 화면 없이 PostMan을 이용해서 테스트를 진행해보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/d060545d-916d-46e2-ab10-df38f0e740a5/image.png" alt="">
<img src="https://velog.velcdn.com/images/woomin_s/post/ca3e81b5-6acf-4d0a-8bf2-1fe386d521cd/image.png" alt=""></p>
</li>
</ul>
<ul>
<li>성공적으로 스토리지에도 사진들이 업로드 된것을 확인하였습니다.</li>
</ul>
<br>

<hr>
<h2 id="📝결과-화면">📝결과 화면</h2>
<h3 id="상품-이름-가격-수량-등을-누락했을-경우">상품 이름, 가격, 수량 등을 누락했을 경우</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/f0cff0f3-28e7-4f6a-bd55-2aa2df7c7e2e/image.png" alt=""></p>
<br>


<h3 id="상품-이미지를-등록-안했을-경우">상품 이미지를 등록 안했을 경우</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/562e8d8e-f580-4b9b-b875-0f8f15ee9552/image.png" alt=""></p>
<br>


<h3 id="등록-후-데이터베이스-저장-내역">등록 후 데이터베이스 저장 내역</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/db2c0d95-9875-445e-8997-72642d80810a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MultipartFile 시행착오 ]]></title>
            <link>https://velog.io/@woomin_s/MultipartFile-%EC%8B%9C%ED%96%89%EC%B0%A9%EC%98%A4</link>
            <guid>https://velog.io/@woomin_s/MultipartFile-%EC%8B%9C%ED%96%89%EC%B0%A9%EC%98%A4</guid>
            <pubDate>Wed, 05 Apr 2023 06:05:08 GMT</pubDate>
            <description><![CDATA[<h2 id="📝multipartfile">📝MultipartFile</h2>
<ul>
<li>쇼핑몰 프로젝트를 진행하면서 이미지 업로드 부분에서 겪었던 시행착오를 기록합니다.</li>
<li><a href="">쇼핑몰 프로젝트</a></li>
</ul>
<br>

<h3 id="문제점">문제점</h3>
<pre><code class="language-java"> @PostMapping(&quot;/items/new&quot;)
    public String createItem(@Valid @ModelAttribute ItemForm itemForm, BindingResult bindingResult, Model model,
                            @RequestPart(name = &quot;itemImages&quot;) List&lt;MultipartFile&gt; multipartFiles
    ) throws IOException {

        if (bindingResult.hasErrors()) return &quot;item/itemForm&quot;;

        log.info(&quot;multipartFiles={}&quot;, multipartFiles);
        log.info(&quot;multipartFiles.size={}&quot;, multipartFiles.size());


        //상품 이미지를 등록안하면
        if (multipartFiles.isEmpty()) {
            model.addAttribute(&quot;errorMessage&quot;, &quot;상품 사진을 등록해주세요!&quot;);
            return &quot;item/itemForm&quot;;
        }</code></pre>
<ul>
<li>처음에 구현한 내용입니다. </li>
<li>처음 저의 생각은 상품 등록 화면에서 상품 이미지를 등록하지 않으면 multipartFiles에 비어있는 값이 나올 줄 알고 <code>multipartFiles.isEmpty()</code>로 검증 후 오류 메세지를 출력하려고 헀습니다.</li>
<li>하지만 오류메세지는 나오지 않고 Internal Server Error(500)가 떠서 넘어오는 multipartFiles를 로그를 찍어서 보았습니다. 
<img src="https://velog.velcdn.com/images/woomin_s/post/58005853-f257-445d-9616-442fc29f11ed/image.png" alt=""></li>
<li>사이즈도 1이고 객체가 넘어오는 것을 확인했습니다. </li>
</ul>
<br>

<ul>
<li>개발자 도구의 form-data를 봐도 생성되는 멀티파트 요청에는 &#39;file&#39;이 들어있는 것을 확인했습니다.
<img src="https://velog.velcdn.com/images/woomin_s/post/5a0b37a0-aeb3-446c-9467-c31306d1dce0/image.png" alt=""></li>
<li>즉, 객체가 넘어오기는 하지만 길이가 0인 비어있는 상태로 넘어온다는 것을 알 수 있었습니다. </li>
</ul>
<br>

<h3 id="해결">해결</h3>
<pre><code class="language-java">        //상품 이미지를 등록안하면
        if (multipartFiles.get(0).isEmpty()) {
            model.addAttribute(&quot;errorMessage&quot;, &quot;상품 사진을 등록해주세요!&quot;);
            return &quot;item/itemForm&quot;;
        }</code></pre>
<ul>
<li>multipartFiles에는 객체가 바인딩되어서 넘어오기 때문에 리스트 그 자체를 isEmpty()하면 안됨</li>
<li>반복문으로 multipartFiles의 이미지 요소들을 isEmpty()로 검증하거나 위와 같이 리스트 중 하나의 이미지 파일을 검증함으로써 해결 가능하였습니다. </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[로그인, 로그아웃 기능 구현 ]]></title>
            <link>https://velog.io/@woomin_s/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@woomin_s/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sun, 02 Apr 2023 15:49:24 GMT</pubDate>
            <description><![CDATA[<h2 id="📖로그인-기능">📖로그인 기능</h2>
<h3 id="getmappingmembers">@GetMapping(&quot;/members&quot;)</h3>
<pre><code class="language-java">   public String loginForm(Model model) {
        model.addAttribute(&quot;loginForm&quot;, new LoginForm());
        return &quot;members/loginForm&quot;;
    }</code></pre>
<ul>
<li>loginForm(DTO)를 모델을 통해 뷰로 전달해주었습니다. </li>
</ul>
<br>

<hr>
<h3 id="loginform">LoginForm</h3>
<pre><code class="language-java">@Getter
@Setter
public class LoginForm {

    @NotEmpty(message = &quot;이메일 주소는 필수입니다.&quot;)
    @Email  //이메일 형식 validation
    private String email;
    @NotEmpty(message = &quot;비밀번호를 입력해주세요&quot;)
    private String password;
}
</code></pre>
<br>

<hr>
<h3 id="postmappingmembers">@PostMapping(&quot;/members&quot;)</h3>
<pre><code class="language-java">@PostMapping(&quot;/members&quot;)
    public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {

        //이메일 또는 비밀번호를 누락시
        if(bindingResult.hasErrors()) {
            log.info(&quot;error={}&quot;, bindingResult);
            return &quot;members/loginForm&quot;;
        }

        Member loginMember = memberService.login(form.getEmail(), form.getPassword());
        log.info(&quot;login? {}&quot;, loginMember);

        if (loginMember == null) {
            bindingResult.reject(&quot;loginfail&quot;, &quot;이메일 또는 비밀번호가 맞지 않습니다.&quot;);
            return &quot;members/loginForm&quot;;
        }

        /**
         * HttpSession 생성
         */
        HttpSession session = request.getSession();  //만약 세션이 있으면 기존 세션을 반환하고, 없으면 신규 세션 생성
        session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);  //세션에 회원 정보 보관

        //판매자, 구매자 역할 뷰 나누기 TODO
        return &quot;redirect:/userHome&quot;;
    }</code></pre>
<ul>
<li>LoginForm의 @NotEmpty, @Email 어노테이션으로 인해 post 로 넘어올 때 bindResult에 오류 값이 넘어오게 됩니다. 만약 이메일 형식이 아니거나 이메일이나 비밀번호를 누락시 bindingResult에는 오류 값이 넘어오게 됩니다. </li>
<li>memberService.login을 통해 데이터베이스에 저장되어있는 회원의 이메일, 비밀번호를 검증합니다.</li>
<li>memberService.login은 회원의 이메일과 비밀번호가 일치 하지 않을 시 null을 반환하게 하였습니다. </li>
<li>로그인 검증의 경우 객체 필드의 오류(@NotEmpty, @Email ...)만으로는 판단이 불가합니다. 즉, 데이터베이스 안까지 뒤져서 검증을 해야합니다. 
Null을 반환하게 되면 bindingResult.reject로 뷰에서 글로벌 오류를 출력하게 합니다. </li>
<li>서블릿이 제공하는 request.getSession()으로 신규 세션을 생성하였고 세션에 회원정보를 보관하였습니다. <blockquote>
<p>request.getSession(true) : 세션이 있으면 기존 세션을 반환, 세션이 없으면 신규 세션 생성해서 반환
request.getSession(false) : 세션이 있으면 기존 세션을 반환, 세션이 없어도 신규 세션을 생성하지 않고 null을 반환 </p>
</blockquote>
</li>
<li>세션을 생성하면 이름이 JSESSIONID인 쿠키를 생성합니다. 쿠키의 값은 거의 추정이 불가능한 랜덤 값으로 할당됩니다. </li>
</ul>
<br>


<pre><code>server.servlet.session.tracking-modes=cookie</code></pre><ul>
<li>URL에 jessionid가 노출되는 것을 방지하고 URL 전달 방식이 아니 항상 쿠키를 통해서만 세션을 유지하기 위해 application.properties 파일에 추가해줍니다.</li>
<li>세션의 종료 시점 : 서블릿이 제공하는 HttpSession의 세션은 기본적으로 사용자가 서버에 최근에 요청한 시간을 기준으로 30분 정도를 유지합니다. </li>
<li>세션의 타임아웃을 따로 설정도 가능합니다. </li>
</ul>
<br>

<hr>
<h3 id="memberserivce">MemberSerivce</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)  //조회하는 곳에서 성능 최적화
public class MemberService {

    private final MemberRepository memberRepository;


    //회원가입
    @Transactional(readOnly = false)
    public Long join(Member member) {
        validateDuplicateMember(member);
        Member savedMember = memberRepository.save(member);
        return savedMember.getId();
    }

    //중복 회원 검증
    private void validateDuplicateMember(Member member) {
        Member findMember = memberRepository.findByEmail(member.getEmail()).orElse(null);
        if (findMember != null) {
            throw new IllegalStateException(&quot;이미 존재하는 회원입니다&quot;);
        }
    }

    //로그인 체크(null 이면 로그인 실패)
    public Member login(String email, String password) {
        return memberRepository.findByEmail(email)
                .filter(m -&gt; m.getPassword().equals(password))
                .orElse(null);
    }

    public Member findMember(Long id) {
        return memberRepository.findById(id).get();
    }

    public List&lt;Member&gt; findMembers() {
        return memberRepository.findAll();
    }
}</code></pre>
<br>

<hr>
<h2 id="📖로그아웃-기능">📖로그아웃 기능</h2>
<h3 id="getmappinglogout">@GetMapping(&quot;/logout&quot;)</h3>
<pre><code class="language-java"> /**
     * 로그아웃
     */
    @GetMapping(&quot;/logout&quot;)
    public String logout(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        return &quot;redirect:/&quot;;
    }</code></pre>
<br>

<hr>
<h2 id="📖결과-화면">📖결과 화면</h2>
<h3 id="1-이메일-또는-비밀번호-누락시">1. 이메일 또는 비밀번호 누락시</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/a43a3dd9-aeb7-4a0e-93ec-bd1351d97235/image.png" alt=""></p>
<br>


<h3 id="2-이메일-또는-비밀번호가-맞지-않을-때">2. 이메일 또는 비밀번호가 맞지 않을 때</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/10603c74-f40f-4f25-ad23-61c30ade0712/image.png" alt=""></p>
<br>

<h2 id="3-로그인-성공">3. 로그인 성공</h2>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/ea6cb315-e960-47a5-9029-a0771c3e0cfe/image.png" alt=""></p>
<ul>
<li>메인 화면은 아직 미완성입니다!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[회원가입 기능 구현 2]]></title>
            <link>https://velog.io/@woomin_s/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-2</link>
            <guid>https://velog.io/@woomin_s/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-2</guid>
            <pubDate>Thu, 30 Mar 2023 22:02:01 GMT</pubDate>
            <description><![CDATA[<h2 id="📖membercontroller">📖MemberController</h2>
<h3 id="1getmappingmembersnew">1.GetMapping(&quot;/members/new&quot;)</h3>
<pre><code class="language-java">@Controller
@Slf4j
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    /**
     * 회원가입
     */
    @GetMapping(&quot;/members/new&quot;)
    public String createMemberForm(@ModelAttribute(&quot;memberForm&quot;) MemberForm memberForm, Model model) {
        List&lt;RoleCode&gt; roleCodes = new ArrayList&lt;&gt;();
        roleCodes.add(new RoleCode(&quot;admin&quot;, &quot;판매자&quot;));
        roleCodes.add(new RoleCode(&quot;user&quot;, &quot;구매자&quot;));
        model.addAttribute(&quot;roleCodes&quot;, roleCodes);

        return &quot;members/createMemberForm&quot;;
    }

    @Data
    @AllArgsConstructor
    static class RoleCode {
        private String code;
        private String displayName;
    }

}</code></pre>
<ul>
<li>RequiredArgsConstructor 어노테이션을 사용하여 meberService 의존 관계 주입(Dependency Injection)을 해주었습니다.</li>
<li>판매자, 관리자 역할을 셀렉트 박스로 정하게 하려고 RoleCode 클래스를 하나 만들어서 roleCodes를 모델을 통해 뷰로 전달해 주었습니다.</li>
<li>MemberForm(DTO) 또한 모델을 통해 뷰로 전달해 주었습니다. (@ModelAttribute 어노테이션은 자동으로 model.addAttribute를 수행해 줍니다.)</li>
</ul>
<pre><code class="language-java">@Getter
@Setter
public class MemberForm {

    @NotEmpty(message = &quot;이름은 필수입니다.&quot;)
    private String name;
    @NotEmpty(message = &quot;이메일은 필수입니다.&quot;)
    @Email  //이메일 형식 validation
    private String email;
    @NotEmpty(message = &quot;비밀번호는 필수입니다.&quot;)
    private String password;

    private String city;
    private String street;
    private String zipcode;

    private String role;

}</code></pre>
<ul>
<li>@NotEmpty, @Email, @Length, @Max 등 validation 어노테이션을 통해 좀 더 간편하게 유효성 검증을 할 수 있습니다. </li>
</ul>
<br>

<hr>
<h3 id="2postmappingmembersnew">2.postMapping(&quot;members/new&quot;)</h3>
<pre><code class="language-java">@PostMapping(&quot;/members/new&quot;)
    public String createMember(@Valid @ModelAttribute MemberForm memberForm, BindingResult bindingResult, Model model,
                               @RequestParam(&quot;role&quot;) String role) {

        //이름, 이메일, 패스워드 중 하나라도 입력을 안했을 시
        if (bindingResult.hasErrors()) {
            List&lt;RoleCode&gt; roleCodes = new ArrayList&lt;&gt;();
            roleCodes.add(new RoleCode(&quot;admin&quot;, &quot;판매자&quot;));
            roleCodes.add(new RoleCode(&quot;user&quot;, &quot;구매자&quot;));
            model.addAttribute(&quot;roleCodes&quot;, roleCodes);
            return &quot;/members/createMemberForm&quot;;
        }

        Address address = new Address(memberForm.getCity(), memberForm.getStreet(), memberForm.getZipcode());

        try {
            Member member = Member.builder()
                    .name(memberForm.getName())
                    .email(memberForm.getEmail())
                    .password(memberForm.getPassword())
                    .address(address)
                    .build();

            member.changeRole(role);  //사용자에게 권한 설정(판매자 또는 구매자)
            memberService.join(member);
        } catch (IllegalStateException e) {  //예외가 발생하면(회원 이메일이 중복)
            model.addAttribute(&quot;errorMessage&quot;, e.getMessage());
            return &quot;members/createMemberForm&quot;;
        }

        return &quot;redirect:/members&quot;;
    }</code></pre>
<ul>
<li>회원가입 화면에서 Post 요청으로 넘어온 회원가입 정보들을 memberForm 객체로 받고 , 판매자/구매자 정보를 Requestparam(&quot;role&quot;)을 통해 받습니다.</li>
<li>만약 회원 가입 정보에서 이름, 이메일, 패스워드가 누락된다면 bindingResult로 값이 전달됩니다. 이를 이용해 누락이 발생하면 다시 회원가입 화면으로 넘기도록 하였습니다. </li>
<li>정상적으로 회원 가입 정보가 memberForm 객체를 통해 들어오면 이를 이용해 member 객체를 생성하고 데이터베이스에 저장시켜주기 위해 memberService.join(member)를 호출합니다. </li>
<li>이 때 memberService에서 중복 회원을 검증하게 되고 중복이 있다면 예외를 발생시키고 없다면 정상적으로 redirect 시켜줍니다. </li>
</ul>
<br>

<hr>
<br>

<h2 id="📖회원가입-화면">📖회원가입 화면</h2>
<h3 id="1thymeleaf">1.thymeleaf</h3>
<ul>
<li>layout : 화면에서 공통으로 사용합니다.</li>
<li>회원가입 페이지, 로그인 페이지, 상품 등록 페이지 등에 공통으로 적용되는 머리(header)와 몸통(body)를 만들기 위한 것입니다.</li>
</ul>
<br>



<pre><code class="language-java">implementation &#39;nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect&#39; </code></pre>
<p>타임 리프의 레이아웃을 사용하기 위해 build.gradle에 추가해 줍니다.</p>
<br>

<ul>
<li><p>th:block : 동적인 처리가 필요할 때 사용됩니다. layout:fragment 속성에 이름을 지정해서 실제 컨텐츠 페이지의 내용을 채워줍니다.</p>
</li>
<li><p>자바스크립트 인라인 : 자바스크립트에서 타임리프를 편리하게 사용할 수 있는 기능입니다.</p>
<pre><code>&lt;head&gt;
  &lt;th:block layout:fragment=&quot;script&quot;&gt;
      &lt;script th:inline=&quot;javascript&quot;&gt;
          var error = [[${errorMessage}]];
          if(error != null){
              alert(error);
          }
      &lt;/script&gt;
  &lt;/th:block&gt;
&lt;/head&gt;</code></pre></li>
</ul>
<br>
<br>


<h3 id="2화면개발">2.화면개발</h3>
<pre><code>&lt;!DOCTYPE HTML&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;
      xmlns:layout=&quot;http://www.ultraq.net.nz/thymeleaf/layout&quot;
      th:replace=&quot;~{layout/base :: layout(~{::section})}&quot;
      layout:decorate=&quot;~{layout/base}&quot;&gt;
&lt;head&gt;
    &lt;th:block layout:fragment=&quot;script&quot;&gt;
        &lt;script th:inline=&quot;javascript&quot;&gt;
            var error = [[${errorMessage}]];
            if(error != null){
                alert(error);
            }
        &lt;/script&gt;
    &lt;/th:block&gt;
&lt;/head&gt;

&lt;section&gt;
&lt;div layout:fragnemt=&quot;content&quot; style=&quot;padding:30px; padding-left:50px&quot;  &gt;
    &lt;form th:action th:object=&quot;${memberForm}&quot; method=&quot;post&quot;&gt;
        &lt;div th:if=&quot;${#fields.hasGlobalErrors()}&quot;&gt;
            &lt;p class=&quot;field-error&quot; th:each=&quot;err : ${#fields.globalErrors()}&quot;
               th:text=&quot;${err}&quot;&gt;전체 오류 메시지&lt;/p&gt;
        &lt;/div&gt;
        &lt;div&gt;
            &lt;label for=&quot;name&quot;&gt;&amp;nbsp&amp;nbsp&amp;nbsp 이름 &amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&lt;/label&gt;
            &lt;input type=&quot;text&quot; id=&quot;name&quot; th:field=&quot;*{name}&quot; class=&quot;form-control&quot; placeholder=&quot;이름을 입력해주세요&quot;&gt;
            &lt;div class=&quot;field-error&quot; th:errors=&quot;*{name}&quot; /&gt;
        &lt;/div&gt;
        &lt;br&gt;
        &lt;div&gt;
            &lt;label for=&quot;email&quot;&gt;&amp;nbsp&amp;nbsp 이메일 &amp;nbsp&amp;nbsp&lt;/label&gt;
            &lt;input type=&quot;text&quot; id=&quot;email&quot; th:field=&quot;*{email}&quot; class=&quot;form-control&quot; placeholder=&quot;이메일 형식으로 입력해주세요&quot;&gt;
            &lt;div class=&quot;field-error&quot; th:errors=&quot;*{email}&quot; /&gt;
        &lt;/div&gt;
        &lt;div&gt;
            &lt;label for=&quot;password&quot;&gt;&amp;nbsp 비밀번호 &lt;/label&gt;
            &lt;input type=&quot;password&quot; id=&quot;password&quot; th:field=&quot;*{password}&quot; class=&quot;form-control&quot;  placeholder=&quot;비밀번호를 입력해주세요&quot;&gt;
            &lt;div class=&quot;field-error&quot; th:errors=&quot;*{password}&quot; /&gt;
        &lt;/div&gt;
        &lt;br&gt;
        &lt;div&gt;
            &lt;label for=&quot;city&quot;&gt;&amp;nbsp&amp;nbsp  지역명 &amp;nbsp&amp;nbsp&lt;/label&gt;
            &lt;input type=&quot;text&quot; id=&quot;city&quot; th:field=&quot;*{city}&quot;
                   class=&quot;formcontrol&quot;
            &gt;
        &lt;/div&gt;
        &lt;div&gt;
            &lt;label for=&quot;street&quot;&gt;&amp;nbsp&amp;nbsp  도로명 &amp;nbsp&amp;nbsp&lt;/label&gt;
            &lt;input type=&quot;text&quot; id=&quot;street&quot; th:field=&quot;*{street}&quot;
                   class=&quot;formcontrol&quot;
            &gt;
        &lt;/div&gt;
        &lt;div&gt;
            &lt;label for=&quot;zipcode&quot;&gt;&amp;nbsp 우편번호 &lt;/label&gt;
            &lt;input type=&quot;text&quot; id=&quot;zipcode&quot; th:field=&quot;*{zipcode}&quot;
                   class=&quot;formcontrol&quot;
            &gt;
        &lt;/div&gt;
        &lt;br&gt;
        &amp;nbsp
        &lt;select name=&quot;role&quot; id=&quot;role&quot; class=&quot;formcontrol&quot;&gt;
            &lt;option value=&quot;&quot;&gt;판매자, 구매자 등록&lt;/option&gt;
            &lt;option th:each=&quot;roleCode : ${roleCodes}&quot;
                    th:value=&quot;${roleCode.code}&quot;
                    th:text=&quot;${roleCode.displayName}&quot; /&gt;
        &lt;/select&gt;
        &lt;hr class=&quot;my-4&quot;&gt;
        &lt;div class=&quot;row&quot;&gt;
            &lt;div class=&quot;col&quot; style=&quot;text-align: center&quot;&gt;
                &lt;button class=&quot;w-100 btn btn-primary btn-lg&quot; type=&quot;submit&quot;&gt;
                    회원가입&lt;/button&gt;
            &lt;/div&gt;
            &lt;div class=&quot;col&quot; style=&quot;text-align: center&quot;&gt;
                &lt;button class=&quot;w-100 btn btn-secondary btn-lg&quot;
                        onclick=&quot;location.href=&#39;home.html&#39;&quot;
                        th:onclick=&quot;|location.href=&#39;@{/members}&#39;|&quot;
                        type=&quot;button&quot;&gt;취소&lt;/button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/form&gt;
&lt;/div&gt; 
&lt;/section&gt;
&lt;/html&gt;
</code></pre><ul>
<li>layout을 사용해 header 부분과 footer부분은 통일 시켜주었습니다. </li>
<li>th:inline을 통해 중복 회원이 발생하면 alert창을 띄어주도록 하였습니다. 
<img src="https://velog.velcdn.com/images/woomin_s/post/d47193bc-30ad-4134-b022-52ef03576993/image.png" alt=""></li>
</ul>
<br>

<hr>
<br>

<h2 id="결과-화면">결과 화면</h2>
<h3 id="1-이름-이메일-비밀번호-등이-누락되었을-경우">1. 이름, 이메일, 비밀번호 등이 누락되었을 경우</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/a553aac1-c205-48d4-9756-6f389fb8d002/image.png" alt=""></p>
<ul>
<li>MemberForm에 설정해주었던 @NotEmpty 어노테이션으로 인해 유효성 검증을 정상적으로 진행하고 있습니다. </li>
</ul>
<br>

<hr>
<h3 id="2-회원-이메일이-중복-되었을-경우">2. 회원 이메일이 중복 되었을 경우</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/001b3a28-17ae-46c1-9607-376c0c7485bb/image.png" alt=""></p>
<ul>
<li>alert창을 정상적으로 띄어주고 있습니다.</li>
</ul>
<br>

<hr>
<h3 id="3-회원-가입에-성공-하였을-경우">3. 회원 가입에 성공 하였을 경우</h3>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/c0ab1872-1f88-4fc8-ba49-fbd9aa9ea308/image.png" alt=""></p>
<ul>
<li>데이터베이스에 회원 정보가 정상적으로 저장되었습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[회원가입 기능 구현 1]]></title>
            <link>https://velog.io/@woomin_s/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@woomin_s/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Thu, 30 Mar 2023 21:17:10 GMT</pubDate>
            <description><![CDATA[<h2 id="📖회원">📖회원</h2>
<p><a href="https://velog.io/@woomin_s/%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%84%A4%EA%B3%84">회원 엔티티</a></p>
<br>

<hr>
<br>

<h2 id="📖memberrepository">📖MemberRepository</h2>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {


    List&lt;Member&gt; findByName(String name);
    Optional&lt;Member&gt; findByEmail(String email);
}</code></pre>
<ul>
<li>findyByName : 이름으로 회원을 찾습니다. 이름은 중복 될 수 있기 때문에 List<Member>로 설정하였습니다.</li>
<li>findByEmail : 이메일로 회원을 찾습니다. 이메일은 중복 불가 하기 때문에 중복 회원을 검증할 때 사용됩니다.</li>
</ul>
<br>

<hr>
<br>


<h2 id="📖memberservice">📖MemberService</h2>
<pre><code class="language-java">  @Service
@RequiredArgsConstructor
@Transactional(readOnly = true)  //조회하는 곳에서 성능 최적화
public class MemberService {

    private final MemberRepository memberRepository;


    //회원가입
    @Transactional(readOnly = false)
    public Long join(Member member) {
        validateDuplicateMember(member);
        Member savedMember = memberRepository.save(member);
        return savedMember.getId();
    }

    //중복 회원 검증
    private void validateDuplicateMember(Member member) {
        Member findMember = memberRepository.findByEmail(member.getEmail()).orElse(null);
        if (findMember != null) {
            throw new IllegalStateException(&quot;이미 존재하는 회원입니다&quot;);
        }
    }

    public Member findMember(Long id) {
        return memberRepository.findById(id).get();
    }

    public List&lt;Member&gt; findMembers() {
        return memberRepository.findAll();
    }
}</code></pre>
<ul>
<li>validateDuplicateMember : 회원의 이메일로 중복 회원을 검증하는 로직입니다. 만약 회원 이메일이 중복된다면 IllegalStateException 예외를 발생시킵니다.</li>
<li>join : validateDuplicateMember로 중복 회원을 검증하고 회원을 데이터베이스에 저장시킵니다.<br>
</li>
</ul>
<hr>
<br>



<h2 id="📖memberservicetest">📖MemberServiceTest</h2>
<pre><code class="language-java">  @RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class MemberServiceTest {

    @Autowired
    MemberRepository memberRepository;
    @Autowired
    MemberService memberService;

    private Member createMember() {
        return Member.builder()
                .name(&quot;woomin&quot;)
                .address(new Address(&quot;Incheon&quot;, &quot;Ieum-ro&quot;, &quot;123&quot;))
                .email(&quot;woomin@google.com&quot;)
                .password(&quot;123&quot;)
                .build();
    }

    @Test
    @DisplayName(&quot;회원가입 테스트&quot;)
    public void join() {
        //given
        Member member = this.createMember();

        //when
        Long savedId = memberService.join(member);

        //then
        Assert.assertEquals(member, memberService.findMember(savedId));
    }

    @Test
    @DisplayName(&quot;중복회원 검증 테스트&quot;)
    public void duplicateMember() {
        //given
        Member member = this.createMember();

        //when
        memberService.join(member);
        try {
            memberService.join(member); //예외가 발생해야 함
        } catch (IllegalStateException e) {
            return;
        }

        //then
        Assert.fail(&quot;예외가 발생해야 함!!&quot;);
    }

    @Test
    @DisplayName(&quot;관리자, 사용자 역할 테스트&quot;)
    public void setRole() {
        //given
        Member member = createMember();
        member.changeRole(&quot;admin&quot;);  //관리자(판매자)

        //when
        Long savedId = memberService.join(member);

        //then
        Assert.assertEquals(member.getRole(), memberService.findMember(savedId).getRole());
    }

}</code></pre>
<ul>
<li>RunWith(SpringRunner.class) : 스프링과 테스트를 통합합니다.</li>
<li>SpringBootTest : 스프링 부트를 띄우고 테스트를 진행합니다. 만약 이 어노테이션이 없으면 @Autowired는 모두 실패합니다.</li>
<li>@Transactional : default값은 readOnly=false, 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백합니다. (단, 테스트 케이스에서 사용될 때만 롤백합니다.)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[엔티티 설계 ]]></title>
            <link>https://velog.io/@woomin_s/%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@woomin_s/%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 27 Mar 2023 21:07:19 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-회원">📌 회원</h2>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)  // new 생성자 생성 못하게 -&gt; Member member = new Member()
public class Member {

    @Id
    @GeneratedValue
    @Column(name = &quot;member_id&quot;)
    private Long id;
    private String name;

    private String email;
    private String password;

    @Embedded
    private Address address;

    @Enumerated(value = EnumType.STRING)
    private Role role;


    @Builder
    private Member(String name, Address address, String email, String password, Role role) {
        this.name = name;
        this.address = address;
        this.email = email;
        this.password = password;
        this.role = role;
    }

} </code></pre>
<br>


<h3 id="noargsconstructoraccess-accesslevelprotected">@NoArgsConstructor(access =AccessLevel.PROTECTED)</h3>
<p>무분별한 Setter 사용은 객체의 일관성을 유지하기 힘들게 합니다. 때문에 new 생성자(new Member())를 사용할 수 없도록 접근 제어를 PROTECTED로 설정해주었습니다. 
객체의 일관성을 유지할 수 있어야 프로그램의 유지 보수성을 끌어 올릴 수 있기 때문에 Setter는 최대한 지양해야 한다고 생각합니다.
<br></p>
<h3 id="address-클래스">Address 클래스</h3>
<pre><code class="language-java">@Embeddable
@Getter
public class Address {

    private String city;  //지역명
    private String street;   //도로명
    private String zipcode;  //우편번호

    protected Address() {}

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}</code></pre>
<ul>
<li>회원 엔티티는 지역명, 도로명, 우편번호의 공통된 속성을 가지고 있기 때문에 값 타입인 &#39;Embedded 타입&#39;을 사용하였습니다. </li>
<li>Embedded 타입 장점 : 높은 응집도, 재사용 </li>
</ul>
<br>
<br>

<hr>
<h2 id="📌-장바구니">📌 장바구니</h2>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Cart {

    @Id
    @GeneratedValue
    @Column(name = &quot;cart_id&quot;)
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)  //모든 ~ToOne 관계는 LAZY로딩으로 설정(EAGER 쓰지 말기!!)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    private Cart(Member member) {
        this.member = member;
    }

    public static Cart createCart(Member member) {
        return new Cart(member);
    }
}</code></pre>
<br>

<h3 id="회원과-장바구니의-관계">회원과 장바구니의 관계</h3>
<p>한명의 회원은 하나의 장바구니를 가질 수 있고 하나의 장바구니는 하나의 회원에만 설정되므로  일대일 관계로 설정하였습니다.
<br></p>
<h3 id="지연-로딩">지연 로딩</h3>
<p>@ManyToOne, @OneToOne 의 관계는 디폴트값이 FetchType.Eager로 되어있습니다. 즉시로딩을 사용하게 되면 연관된 모든 엔티티 테이블에 대하여 조인하기 때문에 예상하지 못한 SQL이 발생합니다.
이로 인해 JPQL에서는 1+N 문제가 발생하게 되고 성능 저하 문제가 발생합니다.
따라서 지연 로딩(FetchType.Lazy)으로 설정하였습니다. 
<br>
<br></p>
<hr>
<h2 id="📌-상품">📌 상품</h2>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item {

    @Id
    @GeneratedValue
    @Column(name = &quot;item_id&quot;)
    private Long id;
    private String name;
    private int price;
    private int stockQuantity;
    private String description;

    @Builder
    private Item(String name, int price, int stockQuantity, String description) {
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
        this.description = description;
    }

     public static Item createItem(String name, String description, int price, int stockQuantity) {
        return new Item(name, description, price, stockQuantity);
    }

    public void updateItem(String name, String description, int price, int stockQuantity) {
        this.name = name;
        this.description = description;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }

}</code></pre>
<br>

<hr>
<br>

<h2 id="📌-상품-사진">📌 상품 사진</h2>
<pre><code class="language-java">@Entity
@Table(name = &quot;item_image&quot;)
@Getter
public class ItemImage {

    @Id
    @GeneratedValue
    @Column(name = &quot;item_image_id&quot;)
    private Long id;
    private String originalName; //원본 파일명
    private String storeName; //서버에 저장될 경로명
    private String deleteYN; //이미지 파일 삭제 여부 

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;item_id&quot;)
    private Item item;

     @Builder
    private ItemImage(String originalName, String storeName, String deleteYN) {
        this.originalName = originalName;
        this.storeName = storeName;
        this.deleteYN = &quot;N&quot;;
    }

    public void changeItem(Item item) {
        this.item = item;
    }

    public void deleteSet(String deleteYN) {
        this.deleteYN = deleteYN;
    }
}</code></pre>
<br>

<p>하나의 상품에는 여러 이미지가 등록되기 때문에 다대일의 관계로 설정해주었습니다. </p>
<br>

<hr>
<br>


<h2 id="📌-장바구니-상품">📌 장바구니 상품</h2>
<pre><code class="language-java">@Entity
@Table(name = &quot;cart_item&quot;)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CartItem {

    @Id
    @GeneratedValue
    @Column(name = &quot;cart_item_id&quot;)
    private Long id;
    private int count;  //장바구니 아이템 수량

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;cart_id&quot;)
    private Cart cart;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;item_id&quot;)
    private Item item;

    private CartItem(int count, Cart cart, Item item) {
        this.count = count;
        this.cart = cart;
        this.item = item;
    }

    public static CartItem createCartItem(int count, Cart cart, Item item) {
        return new CartItem(count, cart, item);
    }
}
</code></pre>
<br>

<h3 id="다대다-관계">다대다 관계</h3>
<p>장바구니(Cart)와 상품(Item) : 하나의 장바구니에는 여러 상품이 들어갈 수 있고 하나의 상품은 여러 장바구니에 들어갈 수 있으므로 다대다의 관계가 됩니다. 따라서 Cart_Item 엔티티를 만들어서 일대다(Item:CartItem), 다대일(CartItem, cart)의 관계로 설정했습니다. </p>
<br>

<hr>
<br>

<h2 id="📌-주문">📌 주문</h2>
<pre><code class="language-java">@Entity
@Table(name = &quot;orders&quot;)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

    @Id
    @GeneratedValue
    @Column(name = &quot;order_id&quot;)
    private Long id;
    private LocalDateTime orderDate;

    @Enumerated(value = EnumType.STRING)
    private OrderStatus status;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    @OneToMany(mappedBy = &quot;order&quot;)
    private List&lt;OrderItem&gt; orderItems = new ArrayList&lt;&gt;();

    private Order(OrderStatus status, Member member) {
        this.status = status;
        this.member = member;
        this.orderDate = LocalDateTime.now();
    }

    //==연관 관계 메서드==//
    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.changeOrder(this);
    }

    public static Order createOrder(OrderStatus status, Member member, OrderItem... orderItems) {  //List&lt;OrderItem&gt; list??
        Order order = new Order(status, member);
        for (OrderItem orderItem : orderItems) {
            order.addOrderItem(orderItem);
        }
        return order;
    }
}</code></pre>
<h3 id="연관-관계">연관 관계</h3>
<ul>
<li>회원 : 한명의 회원은 여러 주문을 할 수 있기 때문에 다대일의 관계로 설정하였습니다.</li>
<li>주문상품 : 하나의 주문에 여러 상품이 들어가기 때문에 다대일의 관게로 설정하였습니다.<br>


</li>
</ul>
<h3 id="양방향-관계">양방향 관계</h3>
<p>최종 주문을 진행하려면 주문 상품이 있어야 하기 때문에 단방향이 아닌 양방향 관계로 설정해주었습니다.
이 때 연관관계의 주인은 ManyToOne에서 Many쪽인 Order를 연관관계의 주인으로 설정해주었습니다.</p>
<blockquote>
<p>연관관계의 주인 : 객체와 테이블간에 연관관계를 맺는 차이에서 비롯되었는데 테이블은 외래키, 주키로 조인을 통해 양방향 참조가 가능하지만 객체의 경우 위의 코드와 같이 List를 통해 연결시켜주어야만 양방향이 가능합니다. 
즉, 테이블은 외래키 하나로 두 테이블의 연관관계를 관리를 하지만 객체의 경우는 아닙니다. 
위의 경우를 보았을 때 OrderItem, Order 둘 중 하나로 외래키를 관리해야 하는데 OrderItem엔티티의 order를 바꿧을 때 외래키(order_id)값을 업데이트 할지 Order엔티티의 orderItems의 값을 변경했을 때 외래키(order_id)값을 업데이트 할지 설정해야 합니다.<br>이것이 바로 연관관계의 주인으로 연관관계의 주인만이 외래키를 관리할 수 있도록 하고 주인이 아닌쪽은 읽기만 가능하도록 합니다. </p>
</blockquote>
<br>

<pre><code class="language-java"> //==연관 관계 메서드==//
    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.changeOrder(this);
    }</code></pre>
<p>양방향 관계의 경우 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정할 수 있도록 연관 관계 메서드를 작성해주었습니다. </p>
<br>

<hr>
<br>

<h2 id="📌-주문-상품">📌 주문 상품</h2>
<pre><code class="language-java">@Entity
@Table(name = &quot;order_item&quot;)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {

    @Id
    @GeneratedValue
    @Column(name = &quot;order_item_id&quot;)
    private Long id;
    private int count;  //주문 수량
    private int orderPrice;  //주문 총 가격

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;order_id&quot;)
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;item_id&quot;)
    private Item item;

    private OrderItem(int count, int orderPrice, Item item) {
        this.count = count;
        this.orderPrice = orderPrice;
        this.item = item;
    }

    public void changeOrder(Order order) {
        this.order = order;
    }

    public static OrderItem createOrderItem(int count, int orderPrice, Item item) {
        return new OrderItem(count, orderPrice, item);
    }
}</code></pre>
<br>

<h3 id="다대다-관계-1">다대다 관계</h3>
<ul>
<li>상품과 주문의 관계 : 하나의 주문에는 여러 상품이 들어 갈 수 있고 하나의 상품은 여러 주문에 들어갈 수 있으므로 다대다의 관계입니다. 따라서 OrderItem 엔티티를 만들어 일대다(Order:OrderItem), 다대일(OrderItem:Item)의 관계로 설정하였습니다. </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[기획 및 설계 ]]></title>
            <link>https://velog.io/@woomin_s/Spring-boot</link>
            <guid>https://velog.io/@woomin_s/Spring-boot</guid>
            <pubDate>Sat, 25 Mar 2023 16:34:08 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-e-commerce-프로젝트-시작">📌 E-Commerce 프로젝트 시작</h2>
<p>Spring, Spring Boot, JPA, Thymeleaf를 이용하여 기본적인 쇼핑몰 기능을 구현할 계획입니다.
먼저 기본적인 CRUD 기능, 로그인 기능(Interceptor)을 구현하고 그 후 Spring security를 적용할 계획입니다. 또한 정렬(인기순, 최신순), 검색기능 등을 하나씩 추가 개발하며 점차 완벽한 쇼핑몰 E-commerce 사이트를 만들 계획입니다. 
화면(Front)보다는 서버(Back)에 더 집중할 계획입니다. 
<br>
<br></p>
<h2 id="📌-구현-기능">📌 구현 기능</h2>
<ul>
<li><p>Member(회원) 
회원 가입 / 로그인, 로그아웃</p>
</li>
<li><p>Item(상품)
상품 등록 / 상품 전체 조회 / 상품 수정 / 상품 상세 조회</p>
</li>
<li><p>Cart(장바구니)
장바구니 담기 / 장바구니 조회 / 장부구니 수정 / 장바구니 상품 주문</p>
</li>
<li><p>Order(주문)
상품 주문 / 주문 내역 조회 / 주문 취소 </p>
<br>
<br>


</li>
</ul>
<h2 id="📌-erd">📌 ERD</h2>
<p><img src="https://velog.velcdn.com/images/woomin_s/post/272b4885-37a5-4c2b-93b0-b02fb4034af8/image.png" alt=""></p>
<br>


<h2 id="📌-api-명세서">📌 API 명세서</h2>
<table>
<thead>
<tr>
<th>Method</th>
<th>URI</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>GET</td>
<td>members/new</td>
<td>회원가입 폼</td>
</tr>
<tr>
<td>POST</td>
<td>members/new</td>
<td>회원가입</td>
</tr>
<tr>
<td>GET</td>
<td>members</td>
<td>로그인 폼</td>
</tr>
<tr>
<td>POST</td>
<td>members</td>
<td>로그인</td>
</tr>
<tr>
<td>GET</td>
<td>main</td>
<td>로그인한 사용자 메인</td>
</tr>
<tr>
<td>GET</td>
<td>items/new</td>
<td>아이템 등록 폼</td>
</tr>
<tr>
<td>POST</td>
<td>items/new</td>
<td>아이템 등록</td>
</tr>
<tr>
<td>GET</td>
<td>items/{itemId}/edit</td>
<td>아이템 수정 폼</td>
</tr>
<tr>
<td>POST</td>
<td>items/{itemId}/edit</td>
<td>아이템 수정</td>
</tr>
<tr>
<td>GET</td>
<td>items/{itemId}</td>
<td>아이템 조회</td>
</tr>
<tr>
<td></td>
<td>items</td>
<td>아이템 목록 조회</td>
</tr>
<tr>
<td>GET</td>
<td>cart</td>
<td>장바구니 폼</td>
</tr>
<tr>
<td>POST</td>
<td>cart</td>
<td>장바구니 등록(장바구니 담기)</td>
</tr>
<tr>
<td>GET</td>
<td>cart/edit</td>
<td>장바구니 수정 폼</td>
</tr>
<tr>
<td>POST</td>
<td>cart/eidt</td>
<td>장바구니 수정</td>
</tr>
<tr>
<td>POST</td>
<td>cart/order</td>
<td>장바구니 상품 주문</td>
</tr>
<tr>
<td>POST</td>
<td>order</td>
<td>주문하기</td>
</tr>
<tr>
<td>GET</td>
<td>orders</td>
<td>주문 내역 조회</td>
</tr>
<tr>
<td>POST</td>
<td>orders/{orderId}/cancel</td>
<td>주문 취소</td>
</tr>
</tbody></table>
<br>
<br>

<h2 id="📌-데이터-베이스">📌 데이터 베이스</h2>
<p>데이터 베이스 Tool은 비교적 가볍고 쉽게 테스트 가능한 H2로 시작합니다!</p>
]]></description>
        </item>
    </channel>
</rss>