๐Ÿ’ท์ƒํ’ˆ ๋“ฑ๋ก(์ด๋ฏธ์ง€ ํฌํ•จ) ํŽ˜์ด์ง€์™€ ์ˆ˜์ • ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ

gdhiยท2023๋…„ 12์›” 12์ผ
post-thumbnail

๐Ÿ’ท์ƒํ’ˆ ๋“ฑ๋ก ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ

Item๊ณผ ItemImage ๐Ÿ‘‰ 1 : N, ์ฆ‰ ItemImage ๊ฐ€ ์ฃผ์ธ. Item ์™ธ๋ž˜ํ‚ค๋ฅผ ๊ฐ–๊ณ  ์˜ค๋ฉด ๋œ๋‹ค.


๐Ÿ“ŒItemImg Entity ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

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

    @Id
    @Column(name = "item_img_id")
    @GeneratedValue
    private Long id;

    private String imgName; // ์ด๋ฏธ์ง€ ํŒŒ์ผ๋ช… (๋žœ๋ค)
    private String oriImgName; // ์›๋ณธ ์ด๋ฏธ์ง€ ํŒŒ์ผ๋ช… (์ฒ˜์Œ ์˜ฌ๋ฆฌ๋Š” ์ด๋ฏธ์ง€)
    private String imgUrl; // ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ
    private String repImgYn; // ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ Y/N ์—ฌ๋ถ€

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

    public void updateItemImg(String oriImgName, String imgName, String imgUrl){
        this.oriImgName = oriImgName;
        this.imgName = imgName;
        this.imgUrl = imgUrl;
    }

}



๐Ÿ“Œmodelmapper ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

DTO์™€ Entity๋ฅผ ์ž๋™์œผ๋กœ ์—ฐ๊ฒฐํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ.

  • ์ƒํ’ˆ ๋“ฑ๋ก ํ™”๋ฉด์œผ๋กœ๋ถ€ํ„ฐ ์ „๋‹ฌ๋ฐ›์€ DTO ๊ฐ์ฒด๋ฅผ Entity ๋กœ ๋ณ€ํ™˜ํ•ด์•ผํ•จ
  • ์ƒํ’ˆ ์กฐํšŒ ๊ฒฐ๊ณผ๋ฅผ ํ™”๋ฉด์— ๋ฐ˜ํ™˜ํ•  ๋•Œ Entity ๋ฅผ DTO ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ด์•ผํ•จ
  • modelmapper ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ด์„œ ์„œ๋กœ ๋‹ค๋ฅธ ํด๋ž˜์Šค์˜ ๊ฐ’์„ ํ•„๋“œ์˜ ์ด๋ฆ„๊ณผ ์ž๋ฃŒํ˜•์ด ๊ฐ™์œผ๋ฉด(๋‹ค๋ฅด๋ฉด ์‚ฌ์šฉ X) getter, setter ๋ฅผ ํ†ตํ•ด ๊ฐ’์„ ๋ณต์‚ฌํ•ด์„œ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ค€๋‹ค.

ModelMapper

<!-- https://mvnrepository.com/artifact/org.modelmapper/modelmapper -->
<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>3.2.0</version>
</dependency>

๐Ÿ‘‰ ์˜์กด์„ฑ ์ถ”๊ฐ€



๐Ÿ“ŒItemImgDto ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.dto;

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

// ์ƒํ’ˆ ์ด๋ฏธ์ง€์— ๋Œ€ํ•œ DTO
@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();

    public static ItemImgDto of(ItemImg itemImg){

        // ItemImg ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด๋ฅผ ItemImgDto ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜
        return modelMapper.map(itemImg, ItemImgDto.class); // itemImg Entity์™€ ItemImgDto ๋งคํ•‘

    }

}



๐Ÿ“ŒItemFormDto ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.dto;

import com.shop.constant.ItemSellStatus;
import com.shop.entity.Item;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import org.modelmapper.ModelMapper;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

// View๋กœ ๋ถ€ํ„ฐ ์ž…๋ ฅ ๋ฐ›์€ ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ์ •๋ณด DTO
@Getter
@Setter
public class ItemFormDto {

    //------------------------- Item -------------------------//

    private Long id;

    @NotBlank(message = "์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.")
    private String itemNm;

    @NotNull(message = "๊ฐ€๊ฒฉ์€ ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") // Integer ํƒ€์ž…์€ @NotNull
    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(){
        // ItemFormDto ๐Ÿ‘‰ Entity Item ์—ฐ๊ฒฐ. ๋ฐ”๋กœ ๋ฐ”๊ฟ”์ค€๋‹ค
        return modelMapper.map(this, Item.class);
    }

    public static ItemFormDto of(Item item){
        // Entity Item ๐Ÿ‘‰ ItemFormDto ์—ฐ๊ฒฐ. ๋ฐ”๋กœ ๋ฐ”๊ฟ”์ค€๋‹ค
        return modelMapper.map(item, ItemFormDto.class);
    }

}



๐Ÿ“ŒItemController ํด๋ž˜์Šค ์ˆ˜์ •

package com.shop.controller;

import com.shop.dto.ItemFormDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ItemController {

    @GetMapping(value = "/admin/item/new") // ADMIN ROLE ๋งŒ ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ๋‹ค
    // ItemFormDto ๊ฐ์ฒด๋ฅผ model ๊ฐ์ฒด์— ๋‹ด์•„์„œ ๋ทฐ๋กœ ์ „๋‹ฌ
    public String itemForm(Model model){
        
        model.addAttribute("itemFormDto", new ItemFormDto());
        
        return "/item/itemForm";
    }

}



๐Ÿ“ŒitemForm.html ์ˆ˜์ •

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

<!-- ์ƒํ’ˆ ๋“ฑ๋ก ํŽ˜์ด์ง€์—์„œ ์ƒํ’ˆ ์ด๋ฏธ์ง€๋ฅผ browse ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ์„ ํƒํ–ˆ์„ ๋•Œ (change) ์ˆ˜ํ–‰๋˜๋Š” ์Šคํฌ๋ฆฝํŠธ -->
<th:block layout:fragment="script">
    <script th:inline="javascript">
        $(document).ready(function(){
            var errorMessage = [[${errorMessage}]];
            if(errorMessage != null){
                alert(errorMessage);
            }
            bindDomEvent();
        });

        function bindDomEvent(){
            // ํด๋ž˜์Šค ์„ค์ • <input type="file" class="imageFile form-control" name="itemImgFile"> ์—ฌ๊ธฐ์„œ ์‚ฌ์šฉ
            $(".imageFile.form-control").on("change", function(){
                // ์ด๋ฏธ์ง€ ํŒŒ์ผ๋ช…. ex) a.jpg
                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("์ด๋ฏธ์ง€ ํŒŒ์ผ๋งŒ ๋“ฑ๋ก์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.");
                    $(this).val("");
                    return;
                }
            });
        }

    </script>
</th:block>

<!-- ์‚ฌ์šฉ์ž CSS ์ถ”๊ฐ€ -->
<th:block layout:fragment="css">
    <style>
        .input-group {
            margin-bottom : 15px;
        }
        .img-div {
            margin-bottom : 10px;
        }
        .fieldError {
            color: red;
        }
    </style>
</th:block>

<!-- ์ƒํ’ˆ ์ด๋ฏธ์ง€ ๋ถ€๋ถ„์€ ์ƒํ’ˆ์„ ๋“ฑ๋กํ•˜๋Š” ๊ฒฝ์šฐ์™€ ์ƒํ’ˆ์„ ์ˆ˜์ •ํ•˜๋Š” ๊ฒฝ์šฐ๋กœ ๋‚˜๋‰จ -->
<div layout:fragment="content">
    <!-- ํŒŒ์ผ์„ ์ „์†กํ•  ๋•Œ๋Š” form ํƒœ๊ทธ์˜ enctype(์ธ์ฝ”๋”ฉ ํƒ€์ž…) ๊ฐ’์„ "multipart/form-data" ๋กœ ์„ค์ • -->
    <form role="form" method="post" enctype="multipart/form-data" th:object="${itemFormDto}">
        <p class="h2">์ƒํ’ˆ ๋“ฑ๋ก</p>

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

        <div class="input-group">
            <select th:field="*{itemSellStatus}" class="form-select">
                <option value="SELL">ํŒ๋งค์ค‘</option>
                <option value="SOLD_OUT">ํ’ˆ์ ˆ</option>
            </select>
        </div>

        <div class="input-group">
            <div class="input-group-text">
                <span>์ƒํ’ˆ๋ช…</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-text">
                <span>๊ฐ€๊ฒฉ</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-text">
                <span>์žฌ๊ณ </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-text">
                <span>์ƒํ’ˆ ์ƒ์„ธ ๋‚ด์šฉ</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>

        <!-- ์ƒํ’ˆ์„ ์ฒ˜์Œ ๋“ฑ๋กํ•  ๊ฒฝ์šฐ Controller ๋ฅผ ํ†ตํ•ด ์ „๋‹ฌ ๋ฐ›์€ itemFormDtoList ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Œ ๐Ÿ‘‰ ์‹คํ–‰ -->
        <div th:if="${#lists.isEmpty(itemFormDto.itemImgDtoList)}">
            <!-- Thymeleaf builtin ๋ฉ”์†Œ๋“œ #numbers.sequence() ๋ฅผ ํ†ตํ•ด์„œ 1 ๋ถ€ํ„ฐ 5 ๊นŒ์ง€ ์ƒํ’ˆ์ด๋ฏธ์ง€๋ช… ์ถœ๋ ฅ -->
            <div class="form-group" th:each="num: ${#numbers.sequence(1,5)}">
                <div class="custom-file img-div">
                    <label class="form-control-file" th:text="์ƒํ’ˆ์ด๋ฏธ์ง€ + ${num}"></label>
                    <input type="file" class="imageFile form-control" name="itemImgFile">
                </div>
            </div>
        </div>

        <!-- ์ƒํ’ˆ์„ ์ˆ˜์ •ํ•  ๊ฒฝ์šฐ๋Š” ์ด๋ฏธ ์กด์žฌํ•˜๋˜ ์ƒํ’ˆ ์ด๋ฏธ์ง€๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ itemFormDtoList๊ฐ€ ์กด์žฌ ๐Ÿ‘‰ ์‹คํ–‰
             ์ด๋ฏธ์ง€๊ฐ€ ์กด์žฌํ•˜๋ฉด ํ•ด๋‹น ์ด๋ฏธ์ง€์˜ ์ด๋ฆ„์„ ์ถœ๋ ฅํ•˜๊ณ 
             ๋นˆ์นธ์ด๋ฉด ์ƒํ’ˆ์ด๋ฏธ์ง€+index ๋ฅผ ์ถœ๋ ฅ -->
        <div th:if = "${not #lists.isEmpty(itemFormDto.itemImgDtoList)}">
            <div class="form-group" th:each="itemImgDto, status: ${itemFormDto.itemImgDtoList}">
                <div class="custom-file img-div">
                    <!-- itemImgDto.id ๐Ÿ‘‰ hidden์œผ๋กœ ์ˆจ๊ฒจ์„œ ํ™”๋ฉด์— ๋‚˜์˜ค์ง€ ์•Š๋Š”๋‹ค -->
                    <input type="hidden" name="itemImgIds" th:value="${itemImgDto.id}">
                    <!-- itemImgDto.oriImgName ์˜ค๋ฆฌ์ง€๋„ ์ด๋ฏธ์ง€ ๋„ค์ž„์ด ๋น„์–ด์žˆ์ง€ ์•Š์œผ๋ฉด
                            ? ์ฐธ(์ด๋ฏธ์ง€ ์ด๋ฆ„) : ๊ฑฐ์ง“(์ž๊ธฐ ์—ด ์ƒํ’ˆ์ด๋ฏธ์ง€ ์ธ๋ฑ์Šค)-->
                    <label class="form-control-file" th:text="${not #strings.isEmpty(itemImgDto.oriImgName)}
                     ? ${itemImgDto.oriImgName} : '์ƒํ’ˆ์ด๋ฏธ์ง€' + ${status.index+1}"></label>
                    <input type="file" class="imageFile form-control" name="itemImgFile">
                </div>
            </div>
        </div>

        <!-- String ๋ฌธ์ž์—ด์ด ๋น„์–ด์žˆ์œผ๋ฉด ์‹คํ–‰ ๐Ÿ‘‰ itemFormDto.id ํ˜„์žฌ ์ €์žฅ๋œ ์ƒํ’ˆ โŒ ๐Ÿ‘‰ ์ €์žฅ -->
        <div th:if="${#strings.isEmpty(itemFormDto.id)}" style="text-align: center">
            <!-- formaction=๏ผ‚@{/admin/item/new}๏ผ‚ href mapping /admin/item/new method ๐Ÿ‘‰ post -->
            <button th:formaction="@{/admin/item/new}" type="submit" class="btn btn-success">์ €์žฅ</button>
        </div>
        <!-- ์œ„ ์กฐ๊ฑด์ด ์•„๋‹ˆ๋ฉด ์‹คํ–‰ ="${#strings.isEmpty(itemFormDto.id)}" ๊ฐ€ ์žˆ์œผ๋ฉด ์ €์žฅ์ด ๋˜๋ฏ€๋กœ ์ƒํ’ˆ โญ• ๐Ÿ‘‰ ์ˆ˜์ • -->
        <!-- ="@{'/admin/item/'+${itemFormDto.id} ๐Ÿ‘‰ /admin/item/3  -->
        <div th:unless="${#strings.isEmpty(itemFormDto.id)}" style="text-align: center">
            <button th:formaction="@{'/admin/item/' + ${itemFormDto.id} }" type="submit" class="btn btn-warning">์ˆ˜์ •</button>
        </div>

        <!-- CSRF ํ† ํฐ -->
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

    </form>

</div>

</html>



๐Ÿ“๊ฒฐ๊ณผ

๐Ÿ‘‰ Member ํด๋ž˜์Šค ๋ณ€๊ฒฝ

๐Ÿ‘‰ ์–ด๋–ค ๊ตฌ์กฐ๋กœ ์‹คํ–‰ ๋˜๋Š”์ง€๋Š” ํŒŒ์ผ ์—…๋กœ๋“œ์™€ ํ•จ๊ป˜









๐Ÿ’ทํŒŒ์ผ ์—…๋กœ๋“œ ํ•˜๊ธฐ

๐Ÿ“Œapplication.properties ์ˆ˜์ •

spring.servlet.multipart.maxFileSize=20MB
spring.servlet.multipart.maxRequestSize=100MB
itemImgLocation=C:/shop1/item
uploadPath=file:///C:/shop1/

๐Ÿ‘‰ ์ถ”๊ฐ€. ํŒŒ์ผ ํฌ๊ธฐ ๋ฐ ๊ฒฝ๋กœ ์ง€์ •

๐Ÿ‘‰ ์œ„ ๊ฒฝ๋กœ์— ๋งž๋Š” ํŒŒ์ผ ์ƒ์„ฑ



๐Ÿ“ŒWebMvcConfig ํด๋ž˜์Šค ์ƒ์„ฑ

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 {

    // "${uploadPath}" ๐Ÿ‘‰ application.properties ์— ์„ค์ •ํ•œ "uploadPath" ํ”„๋กœํผํ‹ฐ ๊ฐ’
    @Value("${uploadPath}") // application.properties ์„ค์ •ํ•œ uploadPath
    String uploadPath;

    // addResourceHandlers ๋ฉ”์†Œ๋“œ Override ๐Ÿ‘‰ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ฒฝ๋กœ ์ง€์ •
    // uploadPath = "C:/shop
    // images/item/xxx.jpg
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry){
        // " /images/** " ํŒจํ„ด์˜ URL ์€ uploadPath ํด๋”๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํƒ์ƒ‰
        registry.addResourceHandler("/images/**")
                // /images๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ uploadPath์— ์„ค์ •ํ•œ ํด๋”๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํŒŒ์ผ์„
                // ์ฝ์–ด ์˜ค๋„๋ก ์„ค์ •
                .addResourceLocations(uploadPath); // ๋กœ์ปฌ ์ปดํ“จํ„ฐ ์—์„œ root ๊ฒฐ๋กœ๋ฅผ ์„ค์ •
    }
}



๐Ÿ“ŒFileService ํด๋ž˜์Šค ์ƒ์„ฑ

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 ๊ฐ์ฒด
// ํŒŒ์ผ ์ €์žฅ์€ DB ์— ์ €์žฅ๋˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— Repository๊ฐ€ ํ•„์š” ์—†์Œ (FileOutputStream ๊ฐ€ ๋Œ€์‹ ํ•จ)
@Service
@Log
public class FileService {
    public String uploadFile(String uploadPath, String originalFileName, byte[] fileData)
            throws Exception{
        // UUID(Universally Unique IDentifier) - ์„œ๋กœ ๋‹ค๋ฅธ ๊ฐœ์ฒด๋“ค์„ ๊ตฌ๋ณ„ํ•˜๊ธฐ ์œ„ํ•œ ํด๋ž˜์Šค. ๋žœ๋ค์œผ๋กœ UUID๋ฅผ ์ƒ์„ฑ
        UUID uuid = UUID.randomUUID();
        String extension = originalFileName.substring(originalFileName.lastIndexOf("."));
        String savedFileName = uuid.toString() + extension;
        String fileUploadFullUrl = uploadPath+"/"+savedFileName;
        System.out.println(fileUploadFullUrl);
        // FileOutputStream ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•˜์—ฌ ํŒŒ์ผ์„ ์ €์žฅ
        FileOutputStream fos = new FileOutputStream(fileUploadFullUrl);
        fos.write(fileData);
        fos.close();
        return savedFileName;
    }

    // ์ด๋ฏธ์ง€ ํŒŒ์ผ ์‚ญ์ œ
    public void deleteFile(String filePath) throws Exception{
        File deleteFile = new File(filePath);

        if(deleteFile.exists()) { //deleteFile ๊ฐ์ฒด ์—ฌ๋ถ€๋ฅผ ํ™•์ธ
            deleteFile.delete();
            log.info("ํŒŒ์ผ์„ ์‚ญ์ œํ•˜์˜€์Šต๋‹ˆ๋‹ค.");
        }else{
            log.info("ํŒŒ์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
        }
    }
}



๐Ÿ“ŒItemImgRepository ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ

package com.shop.repository;

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

// ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ •๋ณด ์ฟผ๋ฆฌ๋ฌธ ๋‚ ๋ฆฌ๋Š” Repository
public interface ItemImgRepository extends JpaRepository<ItemImg,Long> {
}



๐Ÿ“ŒItemImgService ํด๋ž˜์Šค ์ƒ์„ฑ

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 java.io.IOException;

// ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ, ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ •๋ณด ์ €์žฅ Service
@Service
@RequiredArgsConstructor
@Transactional
public class ItemImgService {

    @Value("${itemImgLocation}") // application.properties์˜ itemImgLocation
    private String itemImgLocation; // @Value("${itemImgLocation}") ๋ฅผ String itemImgLocation๋กœ ์น˜ํ™˜
    private final ItemImgRepository itemImgRepository; // ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ •๋ณด ์ €์žฅ
    private final FileService fileService; // ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ


    // MultipartFile : ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ณด๋‚ธ ์ด๋ฏธ์ง€ ์ •๋ณด
    // ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ €์žฅ (๋‚ด๋ถ€์— ์‚ฌ์ง„ ์—…๋กœ๋“œ ํฌํ•จ)
    public void saveItemImg(ItemImg itemImg, MultipartFile itemImgFile) throws Exception {

        // a.jpg๊ฐ€ ๊ทธ๋ƒฅ ์˜จ๋‹ค
        String oriImgName = itemImgFile.getOriginalFilename(); // ์˜ค๋ฆฌ์ง€๋„ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ
        String imgName = "";
        String imgUrl = "";
        System.out.println(oriImgName);

        // ํŒŒ์ผ ์—…๋กœ๋“œ
        if (!StringUtils.isEmpty(oriImgName)){ // oriImgName ์ด ์กด์žฌํ•˜๋ฉด ์‹คํ–‰
            System.out.println("******");

            // fileService.uploadFile() ๋ฉ”์†Œ๋“œ๋กœ ํŒŒ์ผ ์—…๋กœ๋“œ ํ›„ UUID ๋ฅผ ํ†ตํ•ด ๋ณ€๊ฒฝ๋œ ํŒŒ์ผ๋ช… return
            imgName = fileService.uploadFile(itemImgLocation, oriImgName,
                    itemImgFile.getBytes()); // Byte ๋ฐฐ์—ด ๐Ÿ‘‰ ์‹ค์ œ ์ด๋ฏธ์ง€ ํŒŒ์ผ

            System.out.println(imgName);

            imgUrl = "/images/item/" + imgName; // ์ด๋ฏธ์ง€ URL์„ ๋งŒ๋“ ๋‹ค.
        }

        System.out.println("1111");

        // ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ •๋ณด ์ €์žฅ
        // update ํ•œ ์ด์œ ? ๐Ÿ‘‰ ์œ„ 3๊ฐœ๊ฐ€ ์ฑ„์›Œ์ง€์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฑ„์šฐ๋Š” ๊ฒƒ
        // updateItemImg ๋ฉ”์†Œ๋“œ์™€ itemImgRepository.save() ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•ด์„œ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ •๋ณด ์ €์žฅ
        itemImg.updateItemImg(oriImgName, imgName, imgUrl);

        System.out.println("(((((");

        itemImgRepository.save(itemImg); // ์œ„์—์„œ ์‹น ์ •๋ฆฌํ•˜๊ณ  DB์— ์ €์žฅ. ๋

    }
}



๐Ÿ“ŒItemService ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.service;

import com.shop.dto.ItemFormDto;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional
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 {

        // ์ƒํ’ˆ ๋“ฑ๋ก. return modelMapper.map(this, Item.class); ๋ฐ”๋กœ ๋ฐ”๊ฟ”์ค€๋‹ค.
        // ItemFormDto ๊ฐ์ฒด๋ฅผ DB ์— ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด Item ๊ฐ์ฒด๋กœ ๋ฐ”๊ฟˆ
        Item item = itemFormDto.createItem();
        itemRepository.save(item);

        // ์ด๋ฏธ์ง€ ๋“ฑ๋ก
        for (int i = 0; i < itemImgFileList.size(); i++) {

            ItemImg itemImg = new ItemImg();
            itemImg.setItem(item);

            if(i == 0){ // ์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€ ๐Ÿ‘‰ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€
                itemImg.setRepImgYn("Y");
            }
            else { // ์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€๋ฅผ ์ œ์™ธํ•œ ์ด๋ฏธ์ง€๋“ค์€ ๋‹ค NO
                itemImg.setRepImgYn("N");
            }
            itemImgService.saveItemImg(itemImg, itemImgFileList.get(i));
        }
        
        return item.getId(); // item_id๋ฅผ ๋„˜๊ฒจ์ค€๋‹ค

    }
}



๐Ÿ“ŒItemController ํด๋ž˜์Šค ์ˆ˜์ •

package com.shop.controller;

import com.shop.dto.ItemFormDto;
import com.shop.service.ItemService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Controller
@RequiredArgsConstructor
public class ItemController {

    private final ItemService itemService;

    // ์ƒํ’ˆ ๋“ฑ๋ก ํŽ˜์ด์ง€ GET์œผ๋กœ ์ ‘๊ทผ
    @GetMapping(value = "/admin/item/new") // ADMIN ROLE ๋งŒ ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ๋‹ค
    // ItemFormDto ๊ฐ์ฒด๋ฅผ model ๊ฐ์ฒด์— ๋‹ด์•„์„œ ๋ทฐ๋กœ ์ „๋‹ฌ
    public String itemForm(Model model){

        model.addAttribute("itemFormDto", new ItemFormDto());

        return "/item/itemForm";
    }

    // ์ƒํ’ˆ ๋“ฑ๋ก ํŽ˜์ด์ง€ POST์œผ๋กœ ์ ‘๊ทผ
    @PostMapping(value = "/admin/item/new")
    // ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ ๋ฐ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
    public String itemNew(@Valid ItemFormDto itemFormDto, BindingResult bindingResult, Model model,
                          // POST ์ž…๋ ฅ์œผ๋กœ ๋“ค์–ด์˜จ ์ด๋ฏธ์ง€ ํŒŒ์ผ (name = "itemImgFile")์„ MultipartFile ๊ฐ์ฒด๋กœ ๋ฐ›์Œ
                          @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);

        }catch (Exception e){
            model.addAttribute("errorMessage",
                    "์ƒํ’ˆ ๋“ฑ๋ก ์ค‘ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.");

            return "item/itemForm";
        }

        return "redirect:/";

    }


}

    



๐Ÿ“ŒItemImgRepository ์ธํ„ฐํŽ˜์ด์Šค ์ˆ˜์ •

package com.shop.repository;

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

import java.util.List;

// ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ •๋ณด ์ฟผ๋ฆฌ๋ฌธ ๋‚ ๋ฆฌ๋Š” Repository
public interface ItemImgRepository extends JpaRepository<ItemImg,Long> {
    // ์ด๋ฏธ์ง€๊ฐ€ ์ž˜ ์ €์žฅ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์ฟผ๋ฆฌ ์กฐํšŒ๋ฌธ ์ถ”๊ฐ€
    List<ItemImg> findByItemIdOrderByIdAsc(Long itemId);
}



๐Ÿคฆโ€โ™€๏ธItemService Test ํ•˜๊ธฐ

package com.shop.service;

import com.shop.constant.ItemSellStatus;
import com.shop.dto.ItemFormDto;
import com.shop.dto.ItemImgDto;
import com.shop.entity.Item;
import com.shop.entity.ItemImg;
import com.shop.repository.ItemImgRepository;
import com.shop.repository.ItemRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class ItemServiceTest {

    @Autowired
    ItemService itemService;

    @Autowired
    ItemRepository itemRepository;

    @Autowired
    ItemImgRepository itemImgRepository;

    // MockMultipartFile ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•ด ๊ฐ€์ƒ์˜ ์ด๋ฏธ์ง€ ํŒŒ์ผ 5๊ฐœ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์†Œ๋“œ
    List<MultipartFile> createMultipartFiles(){

        List<MultipartFile> multipartFileList = new ArrayList<>();

        for (int i = 0; i < 5; i++) {

            String path = "C:/shop1/item";
            String imageName = "image" + i + ".jpg";

            MockMultipartFile multipartFile =
                    new MockMultipartFile(path, imageName,
                            "image/jpg", new byte[]{1, 2, 3, 4});

            multipartFileList.add(multipartFile);

        }

        return multipartFileList;

    }

    @Test
    @DisplayName("์ƒํ’ˆ ๋“ฑ๋ก ํ…Œ์ŠคํŠธ")
    // ์ƒํ’ˆ ๋“ฑ๋ก์€ ADMIN๋งŒ
    @WithMockUser(username = "admin", roles = "ADMIN")
    // ItemFormDto ๊ฐ์ฒด์™€ MultipartFile ๊ฐ์ฒด๋ฅผ ์ž„์˜๋กœ ์ƒ์„ฑ ํ›„ itemService.saveItem ์ˆ˜ํ–‰
    void saveItem() throws Exception {

        ItemFormDto itemFormDto = new ItemFormDto();
        itemFormDto.setItemNm("ํ…Œ์ŠคํŠธ ์ƒํ’ˆ");
        itemFormDto.setItemSellStatus(ItemSellStatus.SELL);
        itemFormDto.setItemDetail("ํ…Œ์ŠคํŠธ ์ƒํ’ˆ ์ƒ์„ธ ์„ค๋ช….");
        itemFormDto.setPrice(1000);
        itemFormDto.setStockNumber(100);

        List<MultipartFile> multipartFileList = createMultipartFiles();
        Long itemId = itemService.saveItem(itemFormDto, multipartFileList);
        // ์ €์žฅ๋œ ์ด๋ฏธ์ง€๋ฅผ itemImgRepository.findByItemIdOrderByIdAsc(itemId) ๋กœ ์กฐํšŒ
        List<ItemImg> itemImgList = itemImgRepository.findByItemIdOrderByIdAsc(itemId);
        // ์ €์žฅ์„ ํ•˜๋ฉด itemId๋ฅผ ์ž‰์šฉํ•ด Item ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„ item์— ๋Œ€์ž…
        Item item = itemRepository.findById(itemId)
                .orElseThrow(EntityNotFoundException::new);

        // itemFormDto ๋‚ด์šฉ๊ณผ Item ๋‚ด์šฉ์„ ๋น„๊ตํ•ด ๊ฐ™์€์ง€ ํ™•์ธ
        assertEquals(itemFormDto.getItemNm(), item.getItemNm());
        assertEquals(itemFormDto.getItemSellStatus(), item.getItemSellStatus());
        assertEquals(itemFormDto.getItemDetail(), item.getItemDetail());
        assertEquals(itemFormDto.getPrice(), item.getPrice());
        assertEquals(itemFormDto.getStockNumber(), item.getStockNumber());
        // multipartFileList์˜ 0๋ฒˆ ์ธ๋ฑ์Šค ๋‚ด์šฉ๊ณผ itemImgList 0๋ฒˆ ์ธ๋ฑ์Šค ๋‚ด์šฉ์„ ๋น„๊ตํ•ด ๊ฐ™์€์ง€ ํ™•์ธ
        assertEquals(multipartFileList.get(0).getOriginalFilename(), itemImgList.get(0).getOriImgName());

    }

}



๐Ÿ“๊ฒฐ๊ณผ


Hibernate: 
    select
        next value for item_seq
image0.jpg
******
C:/shop1/item/e4fea683-7aec-4d9b-bb30-687ad442cc64.jpg
e4fea683-7aec-4d9b-bb30-687ad442cc64.jpg
1111
(((((
Hibernate: 
    select
        next value for item_img_seq
image1.jpg
******
C:/shop1/item/0c340b74-bb36-4a8e-94c8-5f139b947eb0.jpg
0c340b74-bb36-4a8e-94c8-5f139b947eb0.jpg
1111
(((((
Hibernate: 
    select
        next value for item_img_seq
image2.jpg
******
C:/shop1/item/25f4801c-3e2d-4b29-bfb7-387e41ef3f9a.jpg
25f4801c-3e2d-4b29-bfb7-387e41ef3f9a.jpg
1111
(((((
image3.jpg
******
C:/shop1/item/a5114d7f-a79f-497a-97fb-91ef8e9972ab.jpg
a5114d7f-a79f-497a-97fb-91ef8e9972ab.jpg
1111
(((((
image4.jpg
******
C:/shop1/item/983331d1-1576-44e7-b26b-8dca4e029ef8.jpg
983331d1-1576-44e7-b26b-8dca4e029ef8.jpg
1111
(((((



๐Ÿ’ท์‹ค์ œ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ๋“ฑ๋กํ•˜๊ธฐ

๐Ÿ‘‰ ํšŒ์› ๊ฐ€์ž…

๐Ÿ‘‰ ItemFormDto ๋ฅผ ํ†ตํ•œ ์œ ํšจ์„ฑ ์ฒ˜๋ฆฌ

๐Ÿ‘‰ ์ƒํ’ˆ ๋“ฑ๋ก ์ „

๐Ÿ‘‰ ์ด๋ฏธ์ง€ ๋“ฑ๋ก์„ ํ•˜์ง€ ์•Š์•˜์„ ๋•Œ

๐Ÿ‘‰ ์„ฑ๊ณต. ๋นˆ 4์นธ์€ ์ˆ˜์ •์„ ์œ„ํ•ด ์ƒ์„ฑ ๋œ ๊ฒƒ



๐Ÿ‘โ€๐Ÿ—จ๋‹ค๋ฅธ ์‚ฌ๋žŒ ์›น์—์„œ ๋„ฃ์–ด๋ณด๊ธฐ

๐Ÿ‘‰ ์ƒ๋Œ€๋ฐฉ ๊ฒฝ๋กœ์— ์ž˜ ๋“ค์–ด๊ฐ



โ“๐Ÿ”ฅ์–ด๋–ค ๊ตฌ์กฐ๋กœ ์‹คํ–‰?๐Ÿ”ฅ

์ฐธ๊ณ 

1. "ADMIN" ๊ถŒํ•œ์„ ๊ฐ€์ง„ ์•„์ด๋””๋กœ ์ƒํ’ˆ ๋“ฑ๋ก ํŽ˜์ด์ง€ Get ์š”์ฒญ

2. Item Controller ์—์„œ ์ƒํ’ˆ ๋“ฑ๋ก ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด์„œ itemFormDto ๊ฐ์ฒด๋„ ๋„˜๊น€

3. ์ƒํ’ˆ ๋“ฑ๋ก ํŽ˜์ด์ง€์—์„œ ์ƒํ’ˆ ์ •๋ณด ๋ฐ ์ด๋ฏธ์ง€๋ฅผ ์ž…๋ ฅํ•˜๊ณ  "์ €์žฅ" (POST ์š”์ฒญ)

4. Item Controller ์—์„œ ์ž…๋ ฅ๊ฐ’์„ ๊ฒ€์ฆํ•˜๊ณ  itemService.saveItem() ๋ฉ”์†Œ๋“œ๋ฅผ ์ˆ˜ํ–‰. ์ด ๋•Œ, ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ์ž…๋ ฅ๋ฐ›์€ itemFormDto ๊ฐ์ฒด์™€ ์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ ๋‹ด๊ณ ์žˆ๋Š” itemImgFileList ๋ฅผ ๋„˜๊น€

5. itemService ์—์„œ itemFormDto ๊ฐ์ฒด๋ฅผ item ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  itemRepository.save() ๋ฉ”์†Œ๋“œ ์ˆ˜ํ–‰

6. itemService ์—์„œ itemImg ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  itemImgService.saveItemImg() ๋ฉ”์†Œ๋“œ ์ˆ˜ํ–‰. ์ด ๋•Œ, ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” itemImg ๊ฐ์ฒด์™€ ์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ ๋‹ด๊ณ ์žˆ๋Š” itemImgFileList.get(i) ๊ฐ์ฒด ํ•˜๋‚˜๋ฅผ ์ง€์ •

7. ItemImgService ์—์„œ ์ƒํ’ˆ ์ด๋ฏธ์ง€๊ฐ€ ์กด์žฌํ•œ๋‹ค๋ฉด fileService.uploadFile() ๋ฉ”์†Œ๋“œ ์ˆ˜ํ–‰. ์ด ๋•Œ, ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” "์ €์žฅ์œ„์น˜", "์›๋ž˜ ํŒŒ์ผ๋ช…", "์ด๋ฏธ์ง€ Byte ํŒŒ์ผ"

8. FileService ์—์„œ UUID ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•ด ํŒŒ์ผ๋ช…์„ ์ƒˆ๋กœ ๋งŒ๋“ค๊ณ  FileOutputStream ์„ ์ด์šฉํ•˜์—ฌ ์ €์žฅ

9. ItemImgService ์—์„œ itemImgRepository.save() ๋ฉ”์†Œ๋“œ ์ˆ˜ํ–‰

10. ์ €์žฅ ์™„๋ฃŒ

์ƒํ’ˆ ๊ฐ์ฒด๋งŒ ํ•„์š”ํ•˜๋‹ค? โŒ

์ƒํ’ˆ์„ ๋“ฑ๋กํ•œ๋‹ค๊ณ  ํ•ด์„œ ์ƒํ’ˆ Entity ๋งŒ ์žˆ์œผ๋ฉด ๋˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ƒํ’ˆ์„ ๋ณด์—ฌ์ค„ ์ด๋ฏธ์ง€๋„ ํ•„์š”ํ•˜๋‹ค.

๊ทธ๋ ‡๋‹ค๊ณ ํ•ด์„œ ์ƒํ’ˆ๊ณผ ์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ ํ•˜๋‚˜์˜ ๊ฐ์ฒด๋กœ ๊ตฌํ˜„ํ•˜์ง€๋ง๊ณ ,
๊ฐ๊ฐ ๋”ฐ๋กœ ๊ตฌํ˜„ํ•ด์•ผ ์˜ฌ๋ฐ”๋ฅธ ์„ค๊ณ„์ด๋‹ค.

์ถ”๊ฐ€์ ์œผ๋กœ ์ƒํ’ˆ๊ณผ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ •๋ณด๋Š” DB์— ์ €์žฅ๋˜์ง€๋งŒ ์‹ค์ œ ์ด๋ฏธ์ง€ File ์€ ๋กœ์ปฌ๋‚ด์— ์ €์žฅ๋˜๋Š” ๊ตฌ์กฐ๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์ฒ˜๋Ÿผ ์ด 3๊ฐœ์˜ ๋ถ€๋ถ„์œผ๋กœ ๋‚˜๋ˆ ์•ผํ•จ

  • ์ƒํ’ˆ Entity
  • ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ •๋ณด Entity
  • ์ƒํ’ˆ ์ด๋ฏธ์ง€ File

3๊ฐœ์˜ ๋ถ€๋ถ„์œผ๋กœ ๋‚˜๋ˆด๋‹ค๋ฉด Service ๋กœ์ง๋„ ์•„๋ž˜์ฒ˜๋Ÿผ 3๊ฐœ๋กœ ๋‚˜๋ˆ ์•ผํ•˜๊ณ ,
์ƒํ’ˆ ์ด๋ฏธ์ง€ File ์€ DB ์— ์ €์žฅ๋˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— Repository ๊ฐ€ ์•„๋‹Œ FileOutputStream ์œผ๋กœ ์ €์žฅ

  • ์ƒํ’ˆ Entity ๐Ÿ‘‰ ์ƒํ’ˆ Service ๐Ÿ‘‰ ์ƒํ’ˆ Repository

  • ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ •๋ณด Entity ๐Ÿ‘‰ ์ƒํ’ˆ ์ด๋ฏธ์ง€ Service ๐Ÿ‘‰ ์ƒํ’ˆ ์ด๋ฏธ์ง€ Repository

  • ์ƒํ’ˆ ์ด๋ฏธ์ง€ File ๐Ÿ‘‰ File Service ๐Ÿ‘‰ FileOutputStream

์ฆ‰, ์ƒํ’ˆ์„ ๋“ฑ๋กํ•œ๋‹ค๊ณ  ํ–ˆ์„ ๋•Œ ์ƒํ’ˆ ๊ฐ์ฒด ํ•˜๋‚˜๋งŒ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ตœ๋Œ€ํ•œ ์„ธ๋ถ„ํ™” ํ•œ ๋’ค์— Service ๋กœ์ง๋„ ๋‚˜๋ˆ ์„œ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.



๐Ÿ’ท์ƒํ’ˆ ์ˆ˜์ •ํ•˜๊ธฐ


๐Ÿ“ŒItemService ํด๋ž˜์Šค ์ˆ˜์ •


...

// ์ƒํ’ˆ ์ˆ˜์ •
    // ์ƒํ’ˆ์„ ์ˆ˜์ •ํ•˜๋ ค๋ฉด ํ•ด๋‹น ์ƒํ’ˆ์„ ๋ถˆ๋Ÿฌ์™€์•ผ ํ•จ
    // ๐Ÿ‘‰ ItemService ์— ์ƒํ’ˆ ์กฐํšŒ ๋ฉ”์†Œ๋“œ getItemDtl ์ถ”๊ฐ€ (์กฐํšŒ ๊ธฐ๋Šฅ์ด๋ฏ€๋กœ ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ๋ถˆ๋Ÿฌ์˜ด)
    @Transactional(readOnly = true)
    public ItemFormDto getItemDtl(Long itemId){

        List<ItemImg> itemImgList = itemImgRepository.findByItemIdOrderByIdAsc(itemId);

        // DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ ์˜ด
        // DTO
        List<ItemImgDto> itemImgDtoList = new ArrayList<>();

        for(ItemImg itemImg : itemImgList){

            // Entity ๐Ÿ‘‰ DTO (modelMapper)
            ItemImgDto itemImgDto = ItemImgDto.of(itemImg);
            itemImgDtoList.add(itemImgDto);

        }

        // item ์—”ํ‹ฐํ‹ฐ์™€ img ์ •๋ณด ์—”ํ‹ฐํ‹ฐ๋ฅผ itemFormDto ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ ํ›„ ๋ฐ˜ํ™˜ํ•˜๋Š” ์กฐํšŒ ๊ธฐ๋Šฅ
        Item item = itemRepository.findById(itemId).orElseThrow(EntityNotFoundException::new);
        // Item ๐Ÿ‘‰ ItemFormDto (modelMapper)
        ItemFormDto itemFormDto = ItemFormDto.of(item);
        itemFormDto.setItemImgDtoList(itemImgDtoList);

        return itemFormDto;

    }

}



๐Ÿ“ŒItemController ํด๋ž˜์Šค ์ˆ˜์ •


...


    @GetMapping(value = "/admin/item/{itemId}")
    public String itemDtl(@PathVariable("itemId") Long itemId, Model model){

        try {

            ItemFormDto itemFormDto = itemService.getItemDtl(itemId);
            model.addAttribute("itemFormDto", itemFormDto);

        }catch (EntityNotFoundException e){

            model.addAttribute("errorMessage", "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ์ž…๋‹ˆ๋‹ค.")
            model.addAttribute("itemFormDto", new ItemFormDto());

            return "item/itemForm";
            
        }

        return "item/itemForm";

    }


}



๐Ÿ“๊ฒฐ๊ณผ

๐Ÿ‘‰ ๋กœ๊ทธ์ธ

๐Ÿ‘‰ item_id๋ฅผ ์ด์šฉํ•ด /admin/item/{itemId} ์ธ /admin/item/1 ๋กœ ๊ฐ€๋ณด์ž

๐Ÿ‘‰ item_id์— ๋”ฐ๋ฅธ ์ˆ˜์ • ํŽ˜์ด์ง€๋กœ ์ž˜ ์ด๋™ ํ–ˆ๋‹ค. ์ด์ œ ์‹ค์ œ ์ˆ˜์ •์„ ํ•ด๋ณด์ž



๐Ÿ“Œ์ƒํ’ˆ ์ˆ˜์ •ํ•˜๊ธฐ


๐Ÿ“ItemImgService ํด๋ž˜์Šค ์ˆ˜์ •


...

    public void updateItemImg(Long itemImgId, MultipartFile itemImgFile) throws Exception {
        // ์ƒํ’ˆ์˜ ์ด๋ฏธ์ง€๋ฅผ ์ˆ˜์ •ํ•œ ๊ฒฝ์šฐ, ๊ธฐ์กด์˜ ์ด๋ฏธ์ง€ ์ •๋ณด ๊ฐ์ฒด๋ฅผ ๋ถˆ๋Ÿฌ์™€ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์—…๋ฐ์ดํŠธ
        if (!itemImgFile.isEmpty()){
            ItemImg savedItemImg = itemImgRepository.findById(itemImgId).
                    orElseThrow(EntityExistsException::new); // ๊ธฐ์กด ์—”ํ‹ฐํ‹ฐ ์กฐํšŒ

            // ๊ธฐ์กด์— ๋“ฑ๋ก๋œ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ํŒŒ์ผ์ด ์žˆ๋Š” ๊ฒฝ์šฐ ํŒŒ์ผ ์‚ญ์ œ(์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์ด๋ฏธ์ง€๋ฅผ ์ถ”๊ฐ€ํ•œ ๊ฒƒ)
            if(!StringUtils.isEmpty(savedItemImg.getImgName())){
                fileService.deleteFile(itemImgLocation + "/" + savedItemImg.getImgName());
            }

            String oriImgName = itemImgFile.getOriginalFilename();
            // ์ˆ˜์ •ํ•œ ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ fileService.uploadFile() ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ์—…๋กœ๋“œ
            String imgName = fileService.uploadFile(itemImgLocation, oriImgName,
                    itemImgFile.getBytes()); // ํŒŒ์ผ ์—…๋กœ๋“œ
            String imgUrl = "/images/item/" + imgName;
            // ๋ณ€๊ฒฝ๋œ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ •๋ณด ์„ธํŒ…
            // ์ƒํ’ˆ ๋“ฑ๋ก์„ ํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ItemImgRepository.save()๋กœ์ง์„ ํ˜ธ์ถœ ํ•˜์ง€๋งŒ
            // ํ˜ธ์ถœ์„ ํ•˜์ง€ ์•Š์•˜๋‹ค. itemImgRepository.save() ๊ฐ€ ์•„๋‹Œ
            // savedItemImg.updateItemImg() ๋ฉ”์†Œ๋“œ๋กœ ๊ฐ์ฒด๋ฅผ ์ˆ˜์ • ๐Ÿ‘‰ ๋ณ€๊ฒฝ ๊ฐ์ง€
            // savedItemimg ์—”ํ‹ฐํ‹ฐ๋Š” ํ˜„์žฌ ์˜์†์„ฑ ์ƒํƒœ์ด๋ฏ€๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•œ๋‹ค.
            // ํŠธ๋žœ์žญ์…˜์ด ๋๋‚  ๋•Œ update ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰ ๋œ๋‹ค.
            // โ— ์˜์†์„ฑ ์ƒํƒœ์—ฌ์•ผ ์‚ฌ์šฉ ๊ฐ€๋Šฅ!
            // ์ƒํ’ˆ ์ˆ˜์ • ํŽ˜์ด์ง€์—์„œ ๊ธฐ์กด ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด "null" ๊ฐ์ฒด๋กœ ๋„˜์–ด์˜ด
            // ๐Ÿ‘‰ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ๋ผ๋ฒจ์—๋Š” ์ด๋ฆ„์ด ํ‘œ์‹œ๋˜์–ด ์žˆ์ง€๋งŒ ํŒŒ์ผ์ด ๋“ฑ๋ก๋œ ๊ฒƒ์€ ์•„๋‹˜
            savedItemImg.updateItemImg(oriImgName, imgName, imgUrl);

        }
    }

โ— ๋ณ€๊ฒฝ๊ฐ์ง€?

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ Entity ๊ฐ์ฒด์™€ 1์ฐจ ์บ์‹œ์— ์ €์žฅ๋œ ํ•ด๋‹น Entity์˜ ์Šค๋ƒ…์ƒท์„ ๋น„๊ตํ•˜์—ฌ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์žˆ๋‹ค๋ฉด ์ž๋™์œผ๋กœ UPDATE ๋ฌธ์„ ์“ฐ๊ธฐ ์ง€์—ฐ SQL ์ €์žฅ์†Œ์— ๋‹ด์•„ ๋‘์–ด ์ปค๋ฐ‹ ์‹œ์ ์— ๋ณ€๊ฒฝ ๋‚ด์šฉ์„ ์ž๋™์œผ๋กœ ๋ฐ˜ํ™˜

  • JPA ๋Š” 1์ฐจ ์บ์‹œ์— DB ์—์„œ ์ฒ˜์Œ ๋ถˆ๋Ÿฌ์˜จ ์—”ํ‹ฐํ‹ฐ์˜ ์Šค๋ƒ…์ƒท์„ ์ €์žฅ
  • ์Šค๋ƒ…์ƒท๊ณผ ๋น„๊ตํ•˜์—ฌ ๋ณ€๊ฒฝ ๋‚ด์šฉ์ด ์žˆ๋”ฐ๋ฉด UPDATE SQL ์ˆ˜ํ–‰



๐Ÿ“Item ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ˆ˜์ •


...

// ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์— ๋กœ์ง์„ ์ถ”๊ฐ€ํ•œ๋‹ค๋ฉด ์ข€ ๋” ๊ฐ์ฒด์ง€ํ–ฅ์ ์ด๊ณ  ์ฝ”๋“œ ์žฌํ™œ์šฉ์ด ๊ฐ€๋Šฅ
// ๋˜ํ•œ ๋ณ€๊ฒฝ ํฌ์ธํŠธ๋ฅผ ํ•œ๊ตฐ๋ฐ์—์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ
    public void updateItem(ItemFormDto itemFormDto){
        this.itemNm = itemFormDto.getItemNm();
        this.price = itemFormDto.getPrice();
        this.stockNumber = itemFormDto.getStockNumber();
        this.itemDetail = itemFormDto.getItemDetail();
        this.itemSellStatus = itemFormDto.getItemSellStatus();
    }
}

๐Ÿ‘‰ ๋ณ€๊ฒฝ ๊ฐ์ง€๋ฅผ ์œ„ํ•ด ๋งŒ๋“ฆ



๐Ÿ“ItemService ํด๋ž˜์Šค ์ˆ˜์ •


...

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

        // ์ƒํ’ˆ Id ๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ธฐ์กด์˜ ์ƒํ’ˆ Entity ๊ฐ์ฒด๋ฅผ ๋ถˆ๋Ÿฌ์˜จ ๋’ค
        // Post ์ž…๋ ฅ๊ฐ’์œผ๋กœ ๋“ค์–ด์˜จ ์ƒํ’ˆ ์ •๋ณด๋กœ ์ˆ˜์ •
        Item item = itemRepository.findById(itemFormDto.getId())
                .orElseThrow(EntityNotFoundException::new);
        item.updateItem(itemFormDto);

        List<Long> itemImgIds = itemFormDto.getItemImgIds();

        // itemImgService.updateItemImg() ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•˜์—ฌ for๋ฌธ์„ ๋Œ๋ ค ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ˆ˜์ •
        for (int i = 0; i < itemImgFileList.size(); i++) {
            itemImgService.updateItemImg(itemImgIds.get(i), itemImgFileList.get(i));
        }

        return item.getId();

    }

}



๐Ÿ“ItemController ํด๋ž˜์Šค ์ˆ˜์ •


...

// ์ƒํ’ˆ ๋“ฑ๋ก๊ณผ update๋งŒ ๋‹ค๋ฅด๋‹ค. ์—”ํ‹ฐํ‹ฐ ๋นผ์„œ ๋ณ€๊ฒฝ ๊ฐ์ง€ํ•˜๊ณ  ๋ฐ”๊ฟˆ
    @PostMapping(value = "/admin/item/{itemId}")
    public String itemUpdate(@Valid ItemFormDto itemFormDto, BindingResult bindingResult,
                             @RequestParam("itemImgFile") List<MultipartFile> itemImgFileList,
                             Model model){
        if (bindingResult.hasErrors()){
            return "item/itemForm";
        }

        if (itemImgFileList.get(0).isEmpty() && itemFormDto.getId() == null){
            model.addAttribute("errorMessage", "์ฒซ ๋ฒˆ์งธ ์ƒํ’ˆ ์ด๋ฏธ์ง€๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’ ์ž…๋‹ˆ๋‹ค.");
            return "item/itemForm";
        }

        try {
            itemService.updateItem(itemFormDto, itemImgFileList);
        }catch (Exception e){
            model.addAttribute("errorMessage", "์ƒํ’ˆ ์ˆ˜์ • ์ค‘ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.");
            return "item/itemForm";
        }

        return "redirect:/";

    }

}



๐Ÿ“๊ฒฐ๊ณผ

๐Ÿ‘‰ ํ‚คํ‹ฐ์–ผ๊ตด์„ R๋กœ ๋ณ€๊ฒฝ

๐Ÿ‘‰ ์žˆ๋˜ ํŒŒ์ผ์„ ์‚ญ์ œํ•˜๊ณ  ๋‹ค์‹œ ๋„ฃ์Œ (๋ณ€๊ฒฝ ๊ฐ์ง€)

๐Ÿ‘‰ ์ˆ˜์ • ๋‚ ์งœ ๋“ฑ ์ž˜ ๋ฐ”๋€ ๊ฒƒ์„ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ๋‹ค.



โ“์–ด๋–ค ๊ตฌ์กฐ๋กœ ์‹คํ–‰?

โ— ์ƒํ’ˆ ๋“ฑ๋ก์€ save() ๋ฉ”์†Œ๋“œ๋งŒ ์ˆ˜ํ–‰
์ƒํ’ˆ ์ˆ˜์ •์€ ์กฐํšŒ ๐Ÿ‘‰ update() ๋ฉ”์†Œ๋“œ๋ฅผ ์ˆ˜ํ–‰. ๋ณ€๊ฒฝ ๊ฐ์ง€ ์ด์šฉ

1. "ADMIN" ๊ถŒํ•œ์„ ๊ฐ€์ง„ ์•„์ด๋””๋กœ ์ƒํ’ˆ ์ˆ˜์ • ํŽ˜์ด์ง€ Get ์š”์ฒญ th:formaction="@{'/admin/item/' + ${itemFormDto.id} } ๐Ÿ‘‰ Controller ("/admin/item/{itemId}")

2. Item Controller ์—์„œ itemService.getItemDtl(itemId) ์ˆ˜ํ–‰ํ•˜์—ฌ ํ•ด๋‹น ์ƒํ’ˆ ์กฐํšŒ

3. Item Controller ์—์„œ ์ƒํ’ˆ ์ˆ˜์ • ํŽ˜์ด์ง€ ๋ฐ˜ํ™˜ํ•˜๋ฉด์„œ ํ•ด๋‹น ์ƒํ’ˆ Dto ๊ฐ์ฒด ๋„˜๊น€

4. ์ƒํ’ˆ ์ˆ˜์ • ํŽ˜์ด์ง€์—์„œ ์ˆ˜์ •ํ•œ ํ›„ "์ˆ˜์ •" (POST ์š”์ฒญ)

5. Item Controller ์—์„œ ์ž…๋ ฅ๊ฐ’์„ ๊ฒ€์ฆํ•˜๊ณ  itemService.updateItem() ๋ฉ”์†Œ๋“œ๋ฅผ ์ˆ˜ํ–‰. ์ด ๋•Œ, ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ์ž…๋ ฅ๋ฐ›์€ itemFormDto ๊ฐ์ฒด์™€ ์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ ๋‹ด๊ณ ์žˆ๋Š” itemImgFileList ๋ฅผ ๋„˜๊น€

6. itemService ์—์„œ Item ๊ฐ์ฒด๋ฅผ ์กฐํšŒํ•˜์—ฌ ๋ถˆ๋Ÿฌ์˜จ ๋’ค item.updateItem(itemFormDto) ์ˆ˜ํ–‰

7. itemService ์—์„œ itemImgService.updateItemImg() ๋ฉ”์†Œ๋“œ ์ˆ˜ํ–‰. ์ด ๋•Œ, ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” "์ƒํ’ˆ ์ด๋ฏธ์ง€์˜ id", "์ƒํ’ˆ ์ด๋ฏธ์ง€ ํŒŒ์ผ"

8. itemImgService ์—์„œ ๊ธฐ์กด ์ด๋ฏธ์ง€ ํŒŒ์ผ ์‚ญ์ œ ํ›„ savedItemImg.updateItemImg () ์ˆ˜ํ–‰

9. ์ด๋ฏธ์ง€ ์ˆ˜์ • ์™„๋ฃŒ

0๊ฐœ์˜ ๋Œ“๊ธ€