개발/Project(Spring-쇼핑몰)

Project (5-1) - 상품 등록 및 수정 (1)

잇(IT) 2023. 8. 8. 14:44
728x90
- 상품 등록하기

 

- ItemImg.java

@Entity
@Table(name = "item_img")
@Getter
@Setter
public class ItemImg extends BaseEntity{

    @Id
    @Column(name = "item_img_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String imgName;

    private String oriImgName;

    private String imgUrl;

    private String repImgYn;

    @ManyToOne(fetch = FetchType.LAZY)
    // 상품 엔티티와 다대일 단방향 관계로 매핑한다.
    // 지연 로딩을 설정하여 매핑된 상품 엔티티 정보가 필요할 경우 데이터를 조회하도록 한다.
    private Item item;

    public void updateItemImg(String oriImgName, String imgName, String imgUrl) {
        this.oriImgName = oriImgName;
        this.imgName = imgName;
        this.imgUrl = imgUrl;
    }
    //원본 이미지 파일명, 업데이트할 이미지 파일명, 이미지 경로를 파라미터로 입력 받아서 이미지 정보를 업데이트 하는 메소드이다.
}

- 항상 엔티티를 직접 넘기는 것 보다 DTO 클래스를 생성하여 데이터를 전달하는 것이 좋다.

 

- pom.xml

<dependency>
			<groupId>org.modelmapper</groupId>
			<artifactId>modelmapper</artifactId>
			<version>2.3.9</version>
		</dependency>

- modelmapper를 추가하여 두 개의 서로 다른 클래스나 객체 사이에서 데이터를 복사하거나 매핑하는 작업을 간단하게 처리할 수 있도록 도와준다.

 

- ItemImgDto.java

package com.shop.dto;

import com.shop.entity.ItemImg;
import lombok.Getter;
import lombok.Setter;
import org.modelmapper.ModelMapper;

@Getter @Setter
public class ItemImgDto {

    private Long id;

    private String imgName;

    private String oriImgName;

    private String imgUrl;

    private String repImgYn;

    private static ModelMapper modelMapper = new ModelMapper();
    //멤버 변수로 ModelMapper 객체를 추가한다.

    public static ItemImgDto of(ItemImg itemImg) {
        return modelMapper.map(itemImg, ItemImgDto.class);
    }
    // ItemImg 엔티티 객체를 파라미터로 받아서 ItemImg 객체의 자료형과 멤버변수의 이름이 같을 때
    // ItemImgDto로 값을 복사해서 반환한다. static 메소드로 선언해
    // ItemImgDto객체를 생성하지 않아도 호출할 수 있도록 하겠다.
}

- modelMapper.map(객체, 클래스)를 이용하여 ItemImg 객체의 자료형과 멤버변수의 이름이 같을 때 ItemImgDto로 값을 복사해서 반환한다.

 

- ItemFormDto.java

@Getter
@Setter
public class ItemFormDto {

    private Long id;

    @NotBlank(message = "상품명은 필수 입력 값입니다.")
    private String itemNm;

    @NotNull(message = "가격은 필수 입력 값입니다.")
    private Integer price;

    @NotBlank(message = "이름은 필수 입력 값입니다.")
    private String itemDetail;

    @NotNull(message = "재고는 필수 입력 값입니다.")
    private Integer stockNumber;

    private ItemSellStatus itemSellStatus;

    private List<ItemImgDto> itemImgDtoList = new ArrayList<>();
    // 상품 저장 후 수정할 때 상품 이미지 정보를 저장하는 리스트이다.

    private List<Long> itemImgIds = new ArrayList<>();
    // 상품의 이미지 아이디를 저장하는 리스트이다. 상품 등록 시에는 아직 상품의 이미지를 저장하지 않았기 때문에
    // 아무 값도 들어가 있지 않고 수정 시에 이미지 아이디를 담아둘 용도로 사용한다.

    private static ModelMapper modelMapper = new ModelMapper();

    public Item createItem() {
        return modelMapper.map(this, Item.class);
        //modelMapper를 이용하여 엔티티 객체와 DTO 객체 간의 데이터를 복사하여 복사한 객체를 반환해주는 메서드이다.
    }

    public static ItemFormDto of(Item item) {
        return modelMapper.map(item, ItemFormDto.class);
        //modelMapper를 이용하여 엔티티 객체와 DTO 객체 간의 데이터를 복사하여 복사한 객체를 반환해주는 메서드이다.
    }
}

 - createItem을 통해 ItemFormDto 객체의 데이터를 Item 클래스의 객체로 매핑하고, 그 매핑된 객체를 반환한다.

- of 메서드도 마찬가지로 item객체를 itemFormDto 클래스의 객체로 매핑하고, 그 매핑된 객체를 반환한다.

 

- ItemController.java

@GetMapping(value = "/admin/item/new")
    public String itemForm(Model model) {
        model.addAttribute("itemFormDto", new ItemFormDto());
        return "/item/itemForm";

- /item/itemForm view로 itemFormDto 객체를 model에 담아서 전달한다. 해당 view에선 itemFormDto의 이름으로 ItemFormDto 객체를 사용할 수 있게된다.

 

- itemForm.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">

    <script th:inline="javascript">
        $(document).ready(function(){
            var errorMessage = [[${errorMessage}]];
        // 상품 등록 시 실패 메시지를 받아서 상품 등록 페이지에 재진입 시 alert를 통해서 실패 사유를 보여줍니다.
            if(errorMessage != null){
                alert(errorMessage);
            }

            bindDomEvent();

        });

        function bindDomEvent(){
            $(".custom-file-input").on("change", function() {
                // 파일 업로드(input) 요소는 사용자가 파일을 선택하거나 변경할 때마다 change 이벤트가 발생합니다.
                // $(".custom-file-input").on("change", function() { ... }); 코드는
                // .custom-file-input 클래스를 가진 모든 요소에 대해 change 이벤트를 바인딩하고,
                // 해당 이벤트가 발생했을 때 함수를 실행합니다.
                // change 이벤트는 HTML 폼 요소 중 하나인 <input>, <select>, <textarea>와 같은
                // 입력 요소의 값이 변경되었을 때 발생하는 이벤트입니다.
                var fileName = $(this).val().split("\\").pop();  //이미지 파일명
                // val()은 jQuery에서 사용되는 메서드로, 폼 요소(<input>, <select>, <textarea> 등)의
                // 값을 가져오거나 설정하는 데 사용됩니다.
                var fileExt = fileName.substring(fileName.lastIndexOf(".")+1); // 확장자 추출
                fileExt = fileExt.toLowerCase(); //소문자 변환

                if(fileExt != "jpg" && fileExt != "jpeg" &&
                    fileExt != "gif" && fileExt != "png" && fileExt != "bmp"){
                    // 파일 첨부 시 이미지 파일인지 검사를 한다.
                    // 보통 데이터를 검증할 때는 스크립트에서 벨리데이션을 한 번 하고,
                    // 스크립트는 사용자 변경이 가능하므로 서버에서 한 번 더 벨리데이션을 한다.
                    // 스크립트에서 벨리데이션을 하는 이유는 서버쪽으로 요청을 하면 네트워크를 통해 서버에 요청이 도착하고
                    // 다시 그 결과를 클라이언트에 반환하는 등 리소스를 소모하기 때문이다.
                    alert("이미지 파일만 등록이 가능합니다.");
                    return;
                }

                $(this).siblings(".custom-file-label").html(fileName);
                // label 태그 안의 내용을 jquery의 .html()을 이용하여 파일명을 입력해준다.
                // siblings이란 주어진 요소의 형제 요소들을 선택하는데 사용된다.
                // 형제 요소란 같은 부모를 공유하는 요소들을 의미한다.
            });
        }

    </script>

</th:block>

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
    <style>
        .input-group {
            margin-bottom : 15px
        }
        .img-div {
            margin-bottom : 10px
        }
        .fieldError {
            color: #bd2130;
        }
    </style>
</th:block>

<div layout:fragment="content">

    <form role="form" method="post" enctype="multipart/form-data" th:object="${itemFormDto}">
<!--파일을 전송할 때 enctype(인코딩 타입) 값으로 "multipart/form-data"를 입력한다. 모든 문자를 인코딩하지 않는다.
이 속성은 method 속성값이 "post"인 경우에만 사용할 수 있다.-->
        <p class="h2">
            상품 등록
        </p>

        <input type="hidden" th:field="*{id}">

        <div class="form-group">
            <select th:field="*{itemSellStatus}" class="custom-select">
                <option value="SELL">판매중</option>
                <option value="SOLD_OUT">품절</option>
            </select>
        </div>

        <div class="input-group">
            <div class="input-group-prepend">
                <span class="input-group-text">상품명</span>
            </div>
            <input type="text" th:field="*{itemNm}" class="form-control" placeholder="상품명을 입력해주세요">
        </div>
        <p th:if="${#fields.hasErrors('itemNm')}" th:errors="*{itemNm}" class="fieldError">Incorrect data</p>

        <div class="input-group">
            <div class="input-group-prepend">
                <span class="input-group-text">가격</span>
            </div>
            <input type="number" th:field="*{price}" class="form-control" placeholder="상품의 가격을 입력해주세요">
        </div>
        <p th:if="${#fields.hasErrors('price')}" th:errors="*{price}" class="fieldError">Incorrect data</p>

        <div class="input-group">
            <div class="input-group-prepend">
                <span class="input-group-text">재고</span>
            </div>
            <input type="number" th:field="*{stockNumber}" class="form-control" placeholder="상품의 재고를 입력해주세요">
        </div>
        <p th:if="${#fields.hasErrors('stockNumber')}" th:errors="*{stockNumber}" class="fieldError">Incorrect data</p>

        <div class="input-group">
            <div class="input-group-prepend">
                <span class="input-group-text">상품 상세 내용</span>
            </div>
            <textarea class="form-control" aria-label="With textarea" th:field="*{itemDetail}"></textarea>
        </div>
        <p th:if="${#fields.hasErrors('itemDetail')}" th:errors="*{itemDetail}" class="fieldError">Incorrect data</p>

        <div th:if="${#lists.isEmpty(itemFormDto.itemImgDtoList)}">
            <!--itemFormDto.itemImgDtoList가 비어 있다면-->
            <!--상품 이미지 정보를 담고 있는 리스트가 비어 있다면 상품을 등록하는 경우이다.-->
            <div class="form-group" th:each="num: ${#numbers.sequence(1,5)}">
                <!--타임리프의 유틸리티 객체 #numbers.sequence(start, end)를 이용하면 start부터 end까지
                반복 처리를 할 수 있다. 상품 등록 시 이미지의 개수를 최대 5개로 한다.-->
                <div class="custom-file img-div">
                    <input type="file" class="custom-file-input" name="itemImgFile">
                    <label class="custom-file-label" th:text="상품이미지 + ${num}"></label>
                    <!--label 태그에는 몇 번째 상품 이미지인지 표시를 합니다.-->
                </div>
            </div>
        </div>

        <div th:if = "${not #lists.isEmpty(itemFormDto.itemImgDtoList)}">
            <!--itemFormDto.itemImgDtoList가 비어 있지 않다면-->
            <!--상품 이미지 정보를 담고 있는 리스트가 비어 있지 않다면 상품을 수정하는 경우다.-->
            <div class="form-group" th:each="itemImgDto, status: ${itemFormDto.itemImgDtoList}">
                <div class="custom-file img-div">
                    <input type="file" class="custom-file-input" name="itemImgFile">
                    <input type="hidden" name="itemImgIds" th:value="${itemImgDto.id}">
                    <!--상품 수정 시 어떤 이미지가 수정됐는지를 알기 위해서 상품 이미지의 아이디를 hidden 값으로 숨긴다.-->
                    <label class="custom-file-label" th:text="${not #strings.isEmpty(itemImgDto.oriImgName)}
                    ? ${itemImgDto.oriImgName} : '상품이미지' + ${status.index+1}"></label>
                    <!--${not #strings.isEmpty(itemImgDto.oriImgName)}는 Thymeleaf에서 oriImgName이라는 변수가 비어있지 않을 때
                    (true)에 해당하는 부분을 실행하는 조건식입니다. ${not ...}은 ... 안에 있는 조건식의 부정을 의미합니다.-->
                    <!--타임리프의 유틸리티 객체인 #strings.isEmpty(string)을 이용하여 저장된 이미지 정보가 있다면
                    파일의 이름을 보여주고, 없다면 '상품 이미지+번호'를 출력한다.-->
                </div>
            </div>
        </div>

        <div th:if="${#strings.isEmpty(itemFormDto.id)}" style="text-align: center">
            <!--상품 아이디가 없는 경우(상품을 처음 등록할 경우) 저장 로직을 호출하는 버튼을 보여준다.-->
            <button th:form action="@{/admin/item/new}" type="submit" class="btn btn-primary">저장</button>
        </div>
        <div th:unless="${#strings.isEmpty(itemFormDto.id)}" style="text-align: center">
            <!--상품의 아이디가 있는 경우 수정 로직을 호출하는 버튼을 보여준다.-->
            <button th:form action="@{'/admin/item/' + ${itemFormDto.id} }" type="submit" class="btn btn-primary">수정</button>
        </div>
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

    </form>

</div>

</html>
<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">

    <script th:inline="javascript">
        $(document).ready(function(){
            var errorMessage = [[${errorMessage}]];
        // 상품 등록 시 실패 메시지를 받아서 상품 등록 페이지에 재진입 시 alert를 통해서 실패 사유를 보여줍니다.
            if(errorMessage != null){
                alert(errorMessage);
            }

            bindDomEvent();

        });

- $(document).ready(function)은 HTML 문서가 로드되었을 때 실행할 함수를 정의하는 역할을 한다. bindDomEvent();를 실행시킨다.

 

function bindDomEvent(){
            $(".custom-file-input").on("change", function() {
                // 파일 업로드(input) 요소는 사용자가 파일을 선택하거나 변경할 때마다 change 이벤트가 발생합니다.
                // $(".custom-file-input").on("change", function() { ... }); 코드는
                // .custom-file-input 클래스를 가진 모든 요소에 대해 change 이벤트를 바인딩하고,
                // 해당 이벤트가 발생했을 때 함수를 실행합니다.
                // change 이벤트는 HTML 폼 요소 중 하나인 <input>, <select>, <textarea>와 같은
                // 입력 요소의 값이 변경되었을 때 발생하는 이벤트입니다.
                var fileName = $(this).val().split("\\").pop();  //이미지 파일명
                // val()은 jQuery에서 사용되는 메서드로, 폼 요소(<input>, <select>, <textarea> 등)의
                // 값을 가져오거나 설정하는 데 사용됩니다.
                var fileExt = fileName.substring(fileName.lastIndexOf(".")+1); // 확장자 추출
                fileExt = fileExt.toLowerCase(); //소문자 변환

                if(fileExt != "jpg" && fileExt != "jpeg" &&
                    fileExt != "gif" && fileExt != "png" && fileExt != "bmp"){
                    // 파일 첨부 시 이미지 파일인지 검사를 한다.
                    // 보통 데이터를 검증할 때는 스크립트에서 벨리데이션을 한 번 하고,
                    // 스크립트는 사용자 변경이 가능하므로 서버에서 한 번 더 벨리데이션을 한다.
                    // 스크립트에서 벨리데이션을 하는 이유는 서버쪽으로 요청을 하면 네트워크를 통해 서버에 요청이 도착하고
                    // 다시 그 결과를 클라이언트에 반환하는 등 리소스를 소모하기 때문이다.
                    alert("이미지 파일만 등록이 가능합니다.");
                    return;
                }

                $(this).siblings(".custom-file-label").html(fileName);
                // label 태그 안의 내용을 jquery의 .html()을 이용하여 파일명을 입력해준다.
                // siblings이란 주어진 요소의 형제 요소들을 선택하는데 사용된다.
                // 형제 요소란 같은 부모를 공유하는 요소들을 의미한다.
            });
        }

- 해당 메서드는 파일의 확장자를 확인하여 개발자가 지정한 확장자가 아닌 파일이 업로드 되었을 경우를 방지하기 위한 코드다.

- .custom-file-input 클래스에서 input, select 등의 이벤트가 발생하게 되면 실행된다. 파일이 업로드 되면 해당 파일 이름의 확장자 부분만 떼어내여 jpg, jpeg, gif 등 일치하는지 확인하고 일치하지 않으면, 경고창을 띄운다.

 

<form role="form" method="post" enctype="multipart/form-data" th:object="${itemFormDto}">
<!--파일을 전송할 때 enctype(인코딩 타입) 값으로 "multipart/form-data"를 입력한다. 모든 문자를 인코딩하지 않는다.
이 속성은 method 속성값이 "post"인 경우에만 사용할 수 있다.-->

- 파일을 등록할때는 보통 multipart/form-data의 enctype를 지정한다. 일반적으로 파일을 전송할때 한가지 타입이 아닌 여러가지 타입을 함께 전송하기 때문이다. 이미지의 경우 웹에서 바이너리 데이터 MIME 타입으로 전송된다. 이렇듯 multipart는 여러 타입을 구분하여 데이터를 전송하기 때문에 사용하기 좋다.

 

<div th:if="${#lists.isEmpty(itemFormDto.itemImgDtoList)}">
            <!--itemFormDto.itemImgDtoList가 비어 있다면-->
            <!--상품 이미지 정보를 담고 있는 리스트가 비어 있다면 상품을 등록하는 경우이다.-->
            <div class="form-group" th:each="num: ${#numbers.sequence(1,5)}">
                <!--타임리프의 유틸리티 객체 #numbers.sequence(start, end)를 이용하면 start부터 end까지
                반복 처리를 할 수 있다. 상품 등록 시 이미지의 개수를 최대 5개로 한다.-->
                <div class="custom-file img-div">
                    <input type="file" class="custom-file-input" name="itemImgFile">
                    <label class="custom-file-label" th:text="상품이미지 + ${num}"></label>
                    <!--label 태그에는 몇 번째 상품 이미지인지 표시를 합니다.-->
                </div>
            </div>
        </div>

- itemFormDto.itemImgDtoList 리스트를 검사하여 비어있으면 true를 비어있지 않으면 false를 반환하여 아래 코드들을 실행할지 말지 지정한다.

- 리스트가 비어 있다면 numbers.sequence(1,5) 코드에 의해 각각 5개의 파일을 등록할 수 있는 화면을 생성한다.

- custom-file-input 클래스이기 때문에 해당 부분이 실행되면 위에서 지정한 JS 코드가 실행되어 파일의 확장자를 확인한다. 

 

<div th:if = "${not #lists.isEmpty(itemFormDto.itemImgDtoList)}">
            <!--itemFormDto.itemImgDtoList가 비어 있지 않다면-->
            <!--상품 이미지 정보를 담고 있는 리스트가 비어 있지 않다면 상품을 수정하는 경우다.-->
            <div class="form-group" th:each="itemImgDto, status: ${itemFormDto.itemImgDtoList}">
                <div class="custom-file img-div">
                    <input type="file" class="custom-file-input" name="itemImgFile">
                    <input type="hidden" name="itemImgIds" th:value="${itemImgDto.id}">
                    <!--상품 수정 시 어떤 이미지가 수정됐는지를 알기 위해서 상품 이미지의 아이디를 hidden 값으로 숨긴다.-->
                    <label class="custom-file-label" th:text="${not #strings.isEmpty(itemImgDto.oriImgName)}
                    ? ${itemImgDto.oriImgName} : '상품이미지' + ${status.index+1}"></label>
                    <!--${not #strings.isEmpty(itemImgDto.oriImgName)}는 Thymeleaf에서 oriImgName이라는 변수가 비어있지 않을 때
                    (true)에 해당하는 부분을 실행하는 조건식입니다. ${not ...}은 ... 안에 있는 조건식의 부정을 의미합니다.-->
                    <!--타임리프의 유틸리티 객체인 #strings.isEmpty(string)을 이용하여 저장된 이미지 정보가 있다면
                    파일의 이름을 보여주고, 없다면 '상품 이미지+번호'를 출력한다.-->
                </div>
            </div>
        </div>

- itemImgDtoList가 비어있지 않으면 아래 코드들이 실행된다.

- itemImgDtoList를 확인하고, each를 통해 해당 리스트를 돌면서 해당 이미지의 id를 확인하고, 해당 이미지의 이름을 출력한다.

- 만약 이미지가 없다면 기존의 상품이미지 + num에 해당하는 값을 보여준다.

 

<div th:if="${#strings.isEmpty(itemFormDto.id)}" style="text-align: center">
            <!--상품 아이디가 없는 경우(상품을 처음 등록할 경우) 저장 로직을 호출하는 버튼을 보여준다.-->
            <button th:form action="@{/admin/item/new}" type="submit" class="btn btn-primary">저장</button>
        </div>
        <div th:unless="${#strings.isEmpty(itemFormDto.id)}" style="text-align: center">
            <!--상품의 아이디가 있는 경우 수정 로직을 호출하는 버튼을 보여준다.-->
            <button th:form action="@{'/admin/item/' + ${itemFormDto.id} }" type="submit" class="btn btn-primary">수정</button>

- itemFormDto에 id가 비어있으면 등록된 값이 없는 상태이기 때문에 "저장" 버튼을 보여주고 "/admin/item/new" 의 경로로 데이터를 전송하고, itemFormDto에 id가 비어있지 않으면 등록된 이미지가 있는 것이므로 "수정" 버튼을 보여주고 "/admin/item/{id}의 경로로 데이터를 전송한다.

 

- application.properties / application-test.properties

- application.properties의 설정 중 ddl-auto에는 여러가지 옵션이 있다.

- application.properties

1. validate : 테스트 코드 실행 시 테이블이 자동으로 생성되지 않으므로 테스트 환경에서는 ddl-auto = create로 설정한다.

- application-test.properties

1. create : 코드 실행 시 모든 테이블이 drop 되었다가 다시 생성된다.

 

- 상품 등록

 

- application.properties

#파일 한 개당 최대 사이즈
spring.servlet.multipart.max-file-size=20MB

#요청당 최대 파일 크기
spring.servlet.multipart.max-request-size=100MB

#상품 이미지 업로드 경로
itemImgLocation=C:/shop/item

#리소스 업로드 경로
uploadPath=file:/C:/shop/

- 파일 한 개당 최대 사이즈, 요청당 최대 파일 크기를 설정한다.

- 또한 상품 이미지 업로드 경로, 및 리소스 업로드 경로를 설정한다.

 

- WebMvcConfigurer

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Value("${uploadPath}")
            //application.properties에 설정한 "uploadPath" 프로퍼티 값을 읽어옵니다.
    String uploadPath;

    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/images/**")
                //웹 브라우저에 입력하는 url에 /images로 시작하는 경우 uploadPath에 설정한 폴더를 기준으로
//                파일을 읽어오도록 설정한다.
                .addResourceLocations(uploadPath);
        //로컬 컴퓨터에 저장된 파일을 읽어올 root 경로를 설정한다.
    }
}

- 업로드한 파일을 읽어올 경로를 설정한다. addResourceHandlers 메소드를 통해 로컬 컴퓨터에 업로드한 파일을 찾을 위치를 설정한다.

- addResourceHandler("/images/**") 경로로 요청이 들어오게 되면 uploadPath에 해당하는 경로에서 리소스를 가져온다.

 

- FileService.java

package com.shop.service;

import lombok.extern.java.Log;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.FileOutputStream;
import java.util.UUID;

@Service
@Log
public class FileService {

    public String uploadFile(String uploadPath, String originalFileName,
                             byte[] fileData) throws Exception {
        UUID uuid = UUID.randomUUID();
        //1. UUID는 서로 다른 개체들을 구분하기 위해서 이름을 부여할 때 사용한다.
        //실제 사용 시 중복될 가능성이 거의 없기 때문에 파일의 이름으로 사용하면 파일명 중복 문제를 해결할 수 있다.
        String extension = originalFileName.substring(originalFileName
                .lastIndexOf("."));
        // originalFileName의 .을 기준으로 뒷 부분을 substring으로 추출한다.
        String savedFileName = uuid.toString() + extension;
        //2. UUID로 받은 값과 원래 파일의 이름의 확장자를 조합해서 저장될 파일 이름을 만듭니다.
        String fileUploadFullUrl = uploadPath + "/" + savedFileName;
        FileOutputStream fos = new FileOutputStream(fileUploadFullUrl);
        //3. FileOutputStream 클래스는 바이트 단위의 출력을 내보내는 클래스입니다.
        //생성자로 파일이 저장될 위치와 파일의 이름을 넘겨 파일에 쓸 파일 출력 스트림을 만듭니다.
        fos.write(fileData);
        //4. fileData를 파일 출력 스트림에 입력합니다.
        fos.close();
        return savedFileName;
        //5. 업로드된 파일의 이름을 반환합니다.
    }

    public void deleteFile(String filePath) throws Exception {
        File deleteFile = new File(filePath);
        //6. 파일의 저장된 경로를 이용하여 파일 객체를 생성합니다.

        if (deleteFile.exists()) {
            //7. 해당 파일이 존재하면 파일을 삭제합니다.
            deleteFile.delete();
            log.info("파일을 삭제하였습니다");
        } else {
            log.info("파일이 존재하지 않습니다.");
        }
    }
}

- uploadFile 메서드 : 파일을 업로드하는 메서드로 파라미터로 1. 리소스 업로드 경로, 2. 원본 파일 이름, 3. fileData를 받아 파일을 저장한다.

- 파일을 등록할 때와 다르게 저장할 때는 파일의 이름이 달라야하기 때문에 UUID를 사용하여 파일 이름을 변경시켜 지정된 경로에 파일을 저장한다.

- String fileUploadFullUrl = uploadPath + "/" + savedFileName; : 해당 코드를 토대로 파일이 저장될 위치를 지정한다.

- FIleOutputStream을 통해 파일을 저장한다.

- deleteFile의 경우 파일 new File(경로)를 통해 해당 위치의 파일을 가르키고 deleteFile.delete()를 통해 파일을 삭제한다.

 

- ItemImgRepository.java

public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {
}

- 상품의 이미지 정보를 저장하기 위해서 JpaRepository를 상속받는 ItemImgRepository 인터페이스를 만든다.

 

- ItemImgService.java

@Service
@RequiredArgsConstructor
@Transactional
public class ItemImgService {

    @Value("${itemImgLocation}")
    //@Value 어노테이션을 통해 application.properties 파일에 등록한
    // itemImgLocation 값을 불러와서 itemImgLocation 변수에 넣어준다.
    private String itemImgLocation;

    private final ItemImgRepository itemImgRepository;

    private final FileService fileService;

    public void saveItemImg(ItemImg itemImg, MultipartFile itemImgFile)
            throws Exception {
        String oriImgName = itemImgFile.getOriginalFilename();
        String imgName = "";
        String imgUrl = "";

        //파일 업로드
        if (!StringUtils.isEmpty(oriImgName)) {
            imgName = fileService.uploadFile(itemImgLocation, oriImgName,
                    itemImgFile.getBytes());
            //사용자가 상품의 이미지를 등록했다면 저장할 경로와 파일의 이름, 파일을 파일의 바이트 배열을
            //파일 업로드 파라미터로 uploadFile 메소드를 호출한다.
            //호출 결과 로컬에 저장된 파일의 이름을 imgName 변수에 저장한다.
            imgUrl = "/images/item/" + imgName;
            //저장한 상품 이미지를 불러올 경로를 설정한다. 외부 리소스를 불러오는 urlPatterns로
            //WebMvcConfig 클래스에서 "/images/**"를 설정해주었다.
            //application.properties에서 설정한 uploadPath 프로퍼티 경로인 "C:/shop/" 아래
            //item 폴더에 이미지를 저장하므로 상품 이미지를 불러오는 경로로 "/images/item/"를 붙여준다.
        }

        //상품 이미지 정보 저장
        itemImg.updateItemImg(oriImgName, imgName, imgUrl);
        itemImgRepository.save(itemImg);
        //입력받은 상품 이미지 정보를 저장한다.
        //imgName : 실제 로컬에 저장된 상품 이미지 파일의 이름
        //oriImgName : 업로드했던 상품 이미지 파일의 원래 이름
        //imgUrl : 업로드 결과 로컬에 저장된 상품 이미지 파일을 불러오는 경로
    }

- 상품 이미지를 업로드하고, 상품 이미지 정보를 저장하는 ItemImgService 클래스를 생성한다.

 

public void saveItemImg(ItemImg itemImg, MultipartFile itemImgFile)
            throws Exception {
        String oriImgName = itemImgFile.getOriginalFilename();
        String imgName = "";
        String imgUrl = "";

- saveItemImg 메서드를 통해 MultipartFile(파일을 업로드하게 되면 해당 참조형을 받게된다.)의 객체로부터 getOriginalFilename() 메서드를 통해 파일 이름을 받아온다.

 

if (!StringUtils.isEmpty(oriImgName)) {
            imgName = fileService.uploadFile(itemImgLocation, oriImgName,
            itemImgFile.getBytes());
            imgUrl = "/images/item/" + imgName;

- FileService 클래스의 uploadFile 메서드를 통해 파일을 업로드한다. 추가로 FileService의 uploadFile 메서드는 반환값이 UUID와 파일 확장자를 합친 새롭고 유일한 파일 이미지의 이름이다.

- imgUrl은 "images/item/경로에 imgName을 합친 경로다.

- HTTP는 로컬 내에 있는 데이터 경로를 받을 수 없기 때문에 WebMvcConfig 클래스를 통해 해당 경로에 있는 데이터를 가져올 수 있게 된다.

 

itemImg.updateItemImg(oriImgName, imgName, imgUrl);
        itemImgRepository.save(itemImg);

- updateItemImg메서드를 통해 itemImg 정보에 대해 업데이트하고, itemImg를 DB에 저장한다.

 

- ItemService.java

@Service
@Transactional
@RequiredArgsConstructor
public class ItemService {

    private final ItemRepository itemRepository;
    private final ItemImgService itemImgService;
    private final ItemImgRepository itemImgRepository;

    public Long saveItem(ItemFormDto itemFormDto, List<MultipartFile> itemImgFileList)
            throws Exception {

        //상품 등록
        Item item = itemFormDto.createItem();
        //1. 상품 등록 폼으로부터 입력 받은 데이터를 이용하여 item 객체를 생성합니다.
        itemRepository.save(item);
        //2. 상품 데이터를 저장합니다.

        //이미지 등록
        for (int i = 0; i < itemImgFileList.size(); i++) {
            ItemImg itemImg = new ItemImg();
            itemImg.setItem(item);
            if (i == 0)
                //첫 번째 이미지일 경우 대표 상품 이미지 여부 값을 "Y"로 세팅한다.
                //나머지 상품 이미지는 "N"으로 설정한다.
                itemImg.setRepImgYn("Y");
            else
                itemImg.setRepImgYn("N");
            itemImgService.saveItemImg(itemImg, itemImgFileList.get(i));
            //상품의 이미지 정보를 저장한다.
        }
        return item.getId();
    }

- ItemService는 상품을 등록하는 서비스 코드이다.

 

Item item = itemFormDto.createItem();
itemRepository.save(item);

- createItem() 메서드는 Mapper를 통해 ItemFormDto 객체의 데이터를 Item 객체의 데이터에 복사하는 메서드이다.

- 추가로 복사한 item의 값에 Repository 인터페이스를 통해 Jpa를 활용하여 DB에 데이터를 저장한다.

 

for (int i = 0; i < itemImgFileList.size(); i++) {
            ItemImg itemImg = new ItemImg();
            itemImg.setItem(item);
            if (i == 0)
                //첫 번째 이미지일 경우 대표 상품 이미지 여부 값을 "Y"로 세팅한다.
                //나머지 상품 이미지는 "N"으로 설정한다.
                itemImg.setRepImgYn("Y");
            else
                itemImg.setRepImgYn("N");
            itemImgService.saveItemImg(itemImg, itemImgFileList.get(i));
            //상품의 이미지 정보를 저장한다.
        }
        return item.getId();
    }

- 클라이언트에 의해 업로드된 이미지들은 List<MultipartFile>에 담겨 넘어오게 되는데 해당 List를 돌면서 이미지들을 ItemImg 객체에 저장한다. 또 여러 이미지들은 결국 하나에 아이템에 의한 것이기 때문에 또한 ItemImg <-> Item의 경우 @ManyToOne의 관계에 있기 때문에 이미지들의 Item이 어떤 것인지 설정해준다.

- 첫번째 이미지일 경우 setRepimgYn 즉, 대표 이미지로 등록함으로서 메인 화면에서 해당 이미지를 보여줄 수 있도록 도와준다.

- 나머지 이미지들은 대표이미지가 아닌것으로 등록한다.

-ItemImgService의 saveItemImg 메서드를 통해 해당 이미지들을 지정 경로에 저장과 동시에 DB에 저장한다. 

 

- ItemController.java

@PostMapping(value = "/admin/item/new")
    public String itemNew(@Valid ItemFormDto itemFormDto, BindingResult bindingResult,
                          Model model, @RequestParam("itemImgFile") List<MultipartFile> itemImgFileList) {

        if (bindingResult.hasErrors()) {
            //상품 등록 시 필수 값이 없다면 다시 상품 등록 페이지로 전환합니다.
            return "item/itemForm";
        }

        if (itemImgFileList.get(0).isEmpty() && itemFormDto.getId() == null) {
            //상품 등록 시 첫 번째 이미지가 없다면 에러 메시지와 함게 상품 등록 페이지로 전환합니다.
            //상품의 첫 번째 이미지는 메인 페이지에서 보여줄 상품 이미지로 사용하기 위해서 필수 값으로 지정하겠습니다.
            model.addAttribute("errorMessage", "첫번째 상품 이미지는 필수 입력 값 입니다.");
            return "item/itemForm";
        }

        try {
            itemService.saveItem(itemFormDto, itemImgFileList);
            //상품 저장 로직을 호출한다. 매개 변수로 상품 정보와 상품 이미지 정보를 담고 있는 itemImgFileList를 넘겨준다.
        } catch (Exception e) {
            model.addAttribute("errorMessage", "상품 등록 중 에러가 발생하였습니다.");
            return "item/itemForm";
        }

        return "redirect:/";
        // 상품이 정상적으로 등록되었다면 메인 페이지로 이동한다.
    }

- 상품을 등록하는 url을 ItemController 클래스에 추가한다.

@Valid ItemFormDto itemFormDto, BindingResult bindingResult,
                          Model model, @RequestParam("itemImgFile") List<MultipartFile> itemImgFileList

- 파라미터로 View에서 넘어온 Dto, List<MultipartFile> 리스트를 받아오고

itemService.saveItem(itemFormDto, itemImgFileList);

- 검증에 있어 아무런 이상이 없으면 ItemService의 saveItem 메서드를 통해 파일과 이미지를 저장한다.

- 경로 및 이미지의 ID가 동일한 것을 확인할 수 있다.

728x90