project/쇼핑몰 프로젝트

[쇼핑몰] 상품 등록하기 - 2

혀끄니 2023. 8. 8. 09:00
728x90
<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">

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

            bindDomEvent();

        });

        function bindDomEvent(){
            $(".custom-file-input").on("change", function() {
                var fileName = $(this).val().split("\\").pop();  //이미지 파일명
                var fileExt = fileName.substring(fileName.lastIndexOf(".")+1); // 확장자 추출
                fileExt = fileExt.toLowerCase(); //소문자 변환

                //파일 첨부 시 이미지 파일인지 검사
                //보통 데이터를 검증할 때는 스크립트에서 밸리데이션을 한번
                //서버에서 한번 더 밸리데이션을 한다.
                //스크립트에서 밸리데이션을 하는 이유는
                //서버쪽으로 요청을 하면 네트워크를 통해 서버에 요청이 도착하고
                //다시 그 결과를 클라이언트에 반환하는 등 리소스를 소모하기 때문
                if(fileExt != "jpg" && fileExt != "jpeg" && fileExt != "gif" && fileExt != "png" && fileExt != "bmp"){
                    alert("이미지 파일만 등록이 가능합니다.");
                    return;
                }
                //label 태그 안의 내용을 jquery의 .html()을 이용하여 파일명을 입력
                $(this).siblings(".custom-file-label").html(fileName);
            });
        }

    </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>

- 파일을 전송할 때는 form 태그에 enctype(인코딩 타입) 값으로 "multipart/form-data"를 입력

- 모든 문자를 인코딩하지 않음을 명시, 속성은 method 속성값이 "post"인 경우 사용

<form role="form" method="post" enctype="multipart/form-data" th:object="${itemFormDto}">

- 상품 판매 상태의 경우 판매 중과 품절 상태가 있다.

- 상품주문이 많이 들어와서 재고가 없을 경우 주문 시 품절 상태로 바꿔줄 수 있다.

- 상품을 등록만 먼저 해놓고 팔지 않을 경우에도 이용

<div class="form-group">
            <select th:field="*{itemSellStatus}" class="custom-select">
                <option value="SELL">판매중</option>
                <option value="SOLD_OUT">품절</option>
            </select>
        </div>
//상품 이미지 정보를 담고 있는 리스트가 비어 있다면 상품을 등록
<div th:if="${#lists.isEmpty(itemFormDto.itemImgDtoList)}">

            //타임리프의 유틸리티 객체 #numbers.sequence(start,end)를 이용하면
            //start부터 end까지 반복 처리를 할 수 있다.
            //상품 등록 시 이미지 개수를 최대 5개
            <div class="form-group" th:each="num: ${#numbers.sequence(1,5)}">
                <div class="custom-file img-div">
                    <input type="file" class="custom-file-input" name="itemImgFile">

                    //label 태그에는 몇 번째 상품 이미지인지 표시
                    <label class="custom-file-label" th:text="상품이미지 + ${num}"></label>
                </div>
            </div>
        </div>

        //상품이미지 정보를 담고 있는 리스트가 비어있지 않다면 상품을 수정
        <div th:if = "${not #lists.isEmpty(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">

                    //상품수정 시 어떤 이미지가 수정됐는지 알기 위해서
                    //상품 이미지의 아이디를 hidden값으로 숨겨둠
                    <input type="hidden" name="itemImgIds" th:value="${itemImgDto.id}">

                    //타임리프의 유틸리티 객체인 #string.isEmpty(string)을 이용하여
                    //저장된 이미지 정보가 있다면 파일의 이름을 보여주고
                    //없다면 '상품 이미지 + 번호'를 출력
                    <label class="custom-file-label" th:text="${not #strings.isEmpty(itemImgDto.oriImgName)} ? ${itemImgDto.oriImgName} : '상품이미지' + ${status.index+1}"></label>
                </div>
            </div>
        </div>
//상품 아이디가 없는 경우(상품을 처음 등록할 경우) 저장 로직을 호출하는 버튼을 보여줌
<div th:if="${#strings.isEmpty(itemFormDto.id)}" style="text-align: center">
            <button th:formaction="@{/admin/item/new}" type="submit" class="btn btn-primary">저장</button>
        </div>
         //상품의 아이디가 있는 경우 수정로직을 호출하는 버튼을 보여줌
        <div th:unless="${#strings.isEmpty(itemFormDto.id)}" style="text-align: center">
            <button th:formaction="@{'/admin/item/' + ${itemFormDto.id} }" type="submit" class="btn btn-primary">수정</button>
        </div>

application.properties 설정 변경

//애플리케이션 실행 시점에 테이블을 삭제한 수 재생성하지 않는다.
//엔티티와 테이블이 매핑이 정상적으로 되어 있는지만 확인
//엔티티를 추가가 필요할 경우 create와 validate를 번갈아 가면서 사용
spring.jpa.hibernate.ddl-auto=validate

application-test.properties 설정 추가하기

//application.properties의 설정이 validate이면 테스트 코드 실행 시 테이블이 자동으로 생성되지 않음
//테스트 환경에서는 ddl-auto를 create로 설정
spring.jpa.hibernate.ddl-auto=create

- 이미지 파일을 등록할 때 서버에서 각 파일의 최대 사이즈와 한번에 다운 요청할 수 있는 파일의 크기를 지정

- 컴퓨터에서 어떤 경로에 저장할지를 관리하기 위해서 프로퍼티에 itemImgLocation을 추가

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 인터페이스를 구현하는 WebMvcConfig.java 파일을 작성

- addResourceHandlers 메소드를 통해서 자신의 로컬 컴퓨터에 업로드한 파일을 찾을 위치를 설정

WebMvcConfig.java

package com.shop.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    //application.properties에 설정한 "uploadPath"프로퍼티 값을 읽어옴
    @Value("${uploadPath}")
    String uploadPath;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //웹브라우저에 입력하는 url에 /images로 시작하는 경우
        //uploadPath에 설정한 폴더를 기준으로 파일을 읽어 오도록 설정
        registry.addResourceHandler("/images/**")
                //로컬 컴퓨터에 저장된 파일을 읽어올 root 경로를 설정
                .addResourceLocations(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(Universally Unique Identifier)는 서로 다른 개체들을 구별하기 위해서 이름을 부여할 때 사용
        //실제 사용 시 중복될 가능성이 거의 없기 때문에 파일의 이름으로 사용하면 
        //파일명 중복 문제를 해결
        UUID uuid = UUID.randomUUID();
        String extension = originalFileName.substring(originalFileName.lastIndexOf("."));

        //UUID로 받은 값과 원래 파일의 이름의 확장자를 조합해서
        //저장될 파일 이름을 만듬
        String savedFileName = uuid.toString() + extension;
        String fileUploadFullUrl = uploadPath + "/" + savedFileName;

        //FileOutpuStream 클래스는 바이트 단위의 출력을 내보내는 클래스
        //생성자로 파일이 저장될 위치와 파일의 이름을 넘겨
        //파일에 쓸 파일 출력 스트림을 만듬
        FileOutputStream fos = new FileOutputStream(fileUploadFullUrl);

        //fileData를 파일 출력 스트림에 입력
        fos.write(fileData);
        fos.close();

        //업로드된 파일의 이름을 반환
        return savedFileName;
    }

    public void deleteFile(String filePath) throws Exception{

        //파일이 저장된 경로를 이용하여 파일 객체를 생성
        File deleteFile = new File(filePath);

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

- 상품의 이미지 정보를 저장하기 위해서 repository 패키지 아래에 JpaRepository를 상속받는 ItemImgRepository인터페이스를 만듬

ItemImgRepository.java

package com.shop.repository;

import com.shop.entity.ItemImg;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

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

- 상품 이미지를 업로드하고, 상품 이미지 정보를 저장하는 ItemImgService 클래스를 service 패키지 아래에 생성

ItemImgService.java

package com.shop.service;

import com.shop.entity.ItemImg;
import com.shop.repository.ItemImgRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.thymeleaf.util.StringUtils;

import javax.persistence.EntityExistsException;
import javax.persistence.EntityNotFoundException;

@Service
@RequiredArgsConstructor
@Transactional
public class ItemImgService {

    //@Value 어노테이션을 통해 application.properties파일에 
    //등록한 itemImgLocation 값을 불러와서 itemImgLocation 변수에 넣어줌
    @Value("${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)){
   
            //사용자가 상품의 이미지를 등록했다면 저장할 경로와 
            //파일의 이름, 파일을 파일의 바이트 배열을 파일 업로드 파라미터로
            //uploadFile메소드를 호출한다.
            //호출 결과 로컬에 저장된 파일의 이름을 imgName변수에 저장
            imgName = fileService.uploadFile(itemImgLocation, oriImgName, itemImgFile.getBytes());

            //저장한 상품 이미지를 불러올 경로를 설정
            //외부 리소스를 불러오는 urlPatterns로 WebMvcConfig클래스에서
            //"/images.**"를 설정해준다.
            //또한 application.properties에서 설정한 uploadPath프로퍼티 경로인
            //"C:/shop/"아래 item폴더에 이미지를 저장하므로 상품 이미지를 불러오는 경로로
            //"/images/item"를 붙여준다.
            imgUrl = "/images/item/" + imgName;
        }

        //입력받은 상품 이미지 정보를 저장
        itemImg.updateItemImg(oriImgName,imgName,imgUrl);
        itemImgRepository.save(itemImg);
    }
}

ItemService.java

package com.shop.service;

import com.shop.dto.ItemFormDto;
import com.shop.dto.ItemImgDto;
import com.shop.dto.ItemSearchDto;
import com.shop.dto.MainItemDto;
import com.shop.entity.Item;
import com.shop.entity.ItemImg;
import com.shop.repository.ItemImgRepository;
import com.shop.repository.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.persistence.EntityExistsException;
import javax.persistence.EntityNotFoundException;
import java.util.ArrayList;
import java.util.List;

@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 item = itemFormDto.createItem();
  
        //상품데이터를 저장합니다.
        itemRepository.save(item);

        for(int i = 0 ; i<itemImgFileList.size();i++){
            ItemImg itemImg = new ItemImg();
            itemImg.setItem(item);

            //첫번째 이미지일 경우 대표 상품 이미지 여부 값을 "Y"로 세팅
            //나머지 상품 이미지는 "N"으로 설정
            if(i == 0)
                itemImg.setRepimgYn("Y");
            else
                itemImg.setRepimgYn("N");

            //상품의 이미지 정보를 저장
            itemImgService.saveItemImg(itemImg,itemImgFileList.get(i));
        }
        return item.getId();
    }
}

 

728x90