๐ ํ์ผ ์ ๋ก๋ ๋ฐฉ์์๋ โ ํ๋ก์ ํธ ๋ด๋ถ์ ํ์ผ์ ์ ๋ก๋ํ๋ ๋ฐฉ์๊ณผ โก ํ๋ก์ ํธ ์ธ๋ถ์ ๋ฐ๋ก ์ ๋ก๋ ํด๋๋ฅผ ๋ง๋ค์ด์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์ ๋ ๊ฐ์ง๊ฐ ์๋ค. ์ฐ๋ฆฌ๋ โก๋ฒ ๋ฐฉ์์ ์ฌ์ฉํ๋ค. ํด๋น ๋ฐฉ์์ ํ์ผ์ ๊ฒฝ๋ก๋ฅผ
http://localhost:8081/images/์ฌ์ง์ด๋ฆ
์ผ๋ก ์ง์ ํ๋ค.
src/main/java - com.example.demo.controller.rest - ProductRestController
package com.example.demo.controller.rest;
import java.io.*;
import java.nio.file.Files;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.*;
import com.example.demo.dto.*;
import com.example.demo.service.*;
import lombok.*;
@RequiredArgsConstructor
@RestController
public class ProductRestController {
private final ProductService service;
@Value("${ck.image.folder}")
private String CKImageFolder;
@Value("${product.image.folder}")
private String imageFolder;
@PostMapping(value="/product/image", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<CKResponse> ckImageUpload(MultipartFile upload) {
CKResponse ckResponse = service.ckImageUpload(upload);
return ResponseEntity.ok(ckResponse);
}
@GetMapping("/images/{imagename}")
public ResponseEntity<byte[]> showImage(@PathVariable String imagename) {
File file = new File(CKImageFolder, imagename);
if (file.exists() == false)
file = new File(imageFolder, imagename);
if (file.exists() == false)
return null;
HttpHeaders headers = new HttpHeaders();
// ํ์ผ ํ์ฅ์๋ฅผ ์๋ฅด๊ณ , ํ์ฅ์์ ๋ฐ๋ผ ์ด๋ฏธ์ง์ ContentType์ ์ง์ ํ๋ค.
String extension = imagename.substring(imagename.lastIndexOf(".")+1).toUpperCase();
MediaType type = null;
if (extension.equals("JPG") || extension.equals("JPEG"))
type = MediaType.IMAGE_JPEG;
else if (extension.equals("PNG"))
type = MediaType.IMAGE_PNG;
else if (extension.equals("GIF"))
type = MediaType.IMAGE_GIF;
else
return null;
headers.setContentType(type);
headers.add("Content-Disposition", "inline;filename=" + imagename);
try {
return ResponseEntity.ok().headers(headers).body(Files.readAllBytes(file.toPath()));
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
๐ ์ฌ์ง์ด ck ์ด๋ฏธ์ง ํด๋์ ์์ผ๋ฉด ์ํ ์ฌ์ง ์ด๋ฏธ์ง ํด๋์ ์๋์ง๋ ์ฐพ๋๋ค. ๋ ๋ค ์์ ๋๋ง null์ ๋ฆฌํดํ๋ค. ์ฐ๋ฆฌ๋ CK ์ด๋ฏธ์ง ํด๋์ ๊ทธ๋ฅ ์ด๋ฏธ์ง ํด๋์ ์ฒ๋ฆฌ๋ฅผ ํจ๊ป ํด ์ฃผ๊ณ ์๋ค. (๐ฃ๏ธ ๋ฐ๋ก ์ฒ๋ฆฌํ ์๋ ์์ง๋ง ์ฐ๋ฆฌ๊ฐ ํ๊ธฐ์ ๋๋ฌด ๋ณต์กํ๊ณ ์ด๋ ต๋ค.)
Product ํ ์ด๋ธ ์์ฑ
create table product (
pno number(7),
vendor varchar2(20 char),
name varchar2(20 char),
info clob,
imagename varchar2(100 char),
price number(7),
salesVolume number(7),
countOfStar number(7),
sumOfStar number(7),
countOfReview number(7),
stock number(7),
categoryCode varchar2(3 char),
constraint product_pk_pno primary key(pno)
);
์ํ์ค๋ ํจ๊ป ์์ฑํ๋ค.
create sequence product_seq;
src/main/java - com.example.demo.entity - Product
์์ฑ
package com.example.demo.entity;
import lombok.*;
import lombok.experimental.*;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Accessors(chain=true)
public class Product {
private Integer pno;
private String vendor;
private String name;
// ์ ํ ์ค๋ช
(ck์ ๋ค์ด๊ฐ๋ค.)
private String info;
private String imagename;
private Integer price;
// ํ๋งค๋
private Integer salesVolume;
// ๋ณ์ (5์ ๋ง์ ) - ์ถ๋ ฅํ ๋ ํ๊ท ์ ๊ณ์ฐ
private Integer sumOfStar;
private Integer countOfStar;
// ๋ฆฌ๋ทฐ ๊ฐ์
private Integer countOfReview;
// ์ฌ๊ณ
private Integer stock;
// ์นดํ
๊ณ ๋ฆฌ ์๋ถ๋ฅ ์ฝ๋
private String categoryCode;
}
src/main/resource - templates - product - add.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<title>Insert title here</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/main.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="/ckeditor/ckeditor.js"></script>
<script>
// ์ํ ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ ๋ณด๊ธฐ
function previewImage() {
const file = $("#imagename")[0].files[0];
// ์ด๋ฏธ์ง ํฌ๊ธฐ ์ ํ (1MB)
const maxSize = 1024*1024;
if (file.size > maxSize) {
Swal.fire("์ด๋ฏธ์ง ํฌ๊ธฐ ์ค๋ฅ", "์ํ ์ด๋ฏธ์ง๋ 1MB๋ฅผ ๋์ ์ ์์ต๋๋ค.", "error");
// input๊ณผ ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ ๋ณด๊ธฐ ์์ญ ์ด๊ธฐํ
$('#imagename').val('');
$('#show_image').removeAttr('src');
return false;
}
// ์น์ ๋ฌธ์์ด ๊ธฐ๋ฐ์ด๋ผ ๋ฐ์ด๋๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ผ๋ ค๋ฉด base64 ๋ฐฉ์์ผ๋ก ์ธ์ฝ๋ฉ์ ํ๋ค.
// ์๋ฐ์คํฌ๋ฆฝํธ์ FileReader ๋ด์ฅ ๊ฐ์ฒด๋ฅผ ์์ฑ
const reader = new FileReader();
// file ๊ฐ์ฒด๋ฅผ base64 ๋ฐฉ์์ผ๋ก ์ธ์ฝ๋ฉํด์ result ์์ฑ์ ์ ์ฅ
reader.readAsDataURL(file);
reader.onload = function() {
$('#show_image').attr("src", reader.result);
}
return true;
}
$(document).ready(function() {
$('#imagename').change(previewImage);
// ๋๋ถ๋ฅ ์ถ๋ ฅ
$.ajax("/categories").done((categories)=>{
const $mainCategory = $('#main_category');
$.each(categories, function(idx, c) {
$('<option>').text(c.name).val(c.code).appendTo($mainCategory);
});
});
// ๋๋ถ๋ฅ ์ ํ
$('#main_category').change(function() {
$.ajax('/categories?code='+$('#main_category').val()).done((categories)=>{
// ๋๋ถ๋ฅ๋ฅผ ๋ณ๊ฒฝํ ๊ฒฝ์ฐ ์ค๋ถ๋ฅ์ ์๋ถ๋ฅ๋ฅผ ๋ชจ๋ ์ด๊ธฐํ
const $mediumCategory = $('#medium_category');
$mediumCategory.empty();
$mediumCategory.append('<option value="-1" disabled selected>์ค๋ถ๋ฅ ์ ํ</option>');
$('#minor_category').empty().append('<option value="-1" disabled selected>์๋ถ๋ฅ ์ ํ</option>');
$.each(categories, function(idx, c) {
$('<option>').text(c.name).val(c.code).appendTo($mediumCategory);
});
});
});
// ์ค๋ถ๋ฅ ์ ํ
$('#medium_category').change(function() {
$.ajax('/categories?code='+$('#medium_category').val()).done((categories)=>{
const $minorCategory = $('#minor_category');
$minorCategory.empty();
$minorCategory.append('<option value="-1" disabled selected>์๋ถ๋ฅ ์ ํ</option>');
$.each(categories, function(idx, c) {
$('<option>').text(c.name).val(c.code).appendTo($minorCategory);
});
});
});
// ํ์ผ ์
๋ก๋ํ ๊ฒฝ๋ก๋ฅผ ์ง์
const $ckUploadPath = "/product/image?_csrf=" + $('#_csrf').val();
CKEDITOR.replace('info', {
filebrowserUploadUrl : $ckUploadPath
});
});
</script>
</head>
<body>
<div id="page">
<header th:replace="/fragments/header.html">
</header>
<nav th:replace="/fragments/nav.html">
</nav>
<div id="main">
<aside th:replace="/fragments/aside.html">
</aside>
<section>
<form id="add_form" method="post" action="/product/add" enctype="multipart/form-data">
<input type="hidden" name="_csrf" th:value="${_csrf.token}" id="_csrf" />
<div>
๋๋ถ๋ฅ <select id="main_category">
<option disabled selected value="-1">์ ํํ์ธ์.</option>
</select>
์ค๋ถ๋ฅ <select id="medium_category">
<option class="msg" disabled selected value="-1">๋๋ถ๋ฅ๋ฅผ ์ ํํ์ธ์.</option>
</select>
์๋ถ๋ฅ <select id="minor_category" name="categoryCode">
<option class="msg" disabled selected value="-1">๋๋ถ๋ฅ์ ์ค๋ถ๋ฅ๋ฅผ ์ ํํ์ธ์.</option>
</select>
</div>
<img id="show_image" height="240px">
<div class="form-group">
<label for="image">์ํ ์ด๋ฏธ์ง</label>
<input type="file" name="imagename" id="imagename" class="form-control" accept=".jpg,.jpeg,.png,.gif">
</div>
<div class="form-group">
<label for="manufacturer">์ ์กฐ์ฌ</label>
<span id="manufacturer_msg"></span>
<div class="form-group">
<input type="text" class="form-control" id="vendor" name="vendor">
</div>
</div>
<div class="form-group">
<label for="name">์ํ๋ช
</label>
<span id="name_msg"></span>
<div class="form-group">
<input type="text" class="form-control" id="name" name="name">
</div>
</div>
<div class="form-group">
<label for="price">๊ฐ๊ฒฉ</label>
<span id="price_msg"></span>
<div class="form-group">
<input type="text" class="form-control" id="price" name="price">
</div>
</div>
<div class="form-group">
<textarea class="form-control" rows="5" id="info" name="info"></textarea>
</div>
<div class="form-group">
<label for="stock">์ฌ๊ณ ๋ ์ ํ</label>
<select id="stock" name="stock" class="form-control">
<option selected="selected">0</option>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
</select>
</div>
<div class="form-group" style="text-align: center;">
<button id="add" class="btn btn-info">์ถ๊ฐ</button>
</div>
</form>
</section>
</div>
<footer th:replace="/fragments/footer.html">
</footer>
</div>
</body>
</html>
๐
$("#imagename")[0]
$(์ ํ์)๋ jQuery ๊ฐ์ฒด๋ฅผ ์ ํํ๋ ์ฝ๋์ด๊ณ , $(์ ํ์))[0]๋ ๋ถ์ฌ์ jQuery ์์ html ์์๋ฅผ ๋ฝ์๋ผ ์ ์๋ค.
๐files[0]
<input type='file' id='imagename'>
์์๋ multiple ์กฐ๊ฑด์ ์คฌ์ ๋ ์ฌ๋ฌ ๊ฐ์ ํ์ผ์ ์ ํํ ์ ์์ด์ผ ํ๋ฏ๋ก files๋ผ๋ ๋ฐฐ์ด์ ๊ฐ์ง๊ณ , ์ด ๋ฐฐ์ด์ ์ ํํ ํ์ผ์ ์ ๋ณด๋ฅผ ๋ด์ ๊ฐ์ฒด๋ค์ ์ ์ฅํ๋ค. multiple์ ์ฐ์ง ์๊ณ ์ด๋ฏธ์ง๋ฅผ ํ๋๋ง ์ฌ๋ฆด ๊ฒฝ์ฐ ํด๋น ํ์ผ์ files ๋ฐฐ์ด์ 0๋ฒ์งธ์ ๋ค์ด ์๋ค๊ณ ์๊ฐํ๋ฉด ๋๋ค.
src/main/java - com.example.demo.dto - ProductDto
์์ฑ
package com.example.demo.dto;
import org.springframework.web.multipart.*;
import com.example.demo.entity.*;
import lombok.*;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ProductDto {
// ์ ํ์ ์ถ๊ฐํ ๋ ์ฌ์ฉํ๋ DTO
@Data
public static class Add {
private String vendor;
private String name;
private String info;
private Integer price;
private Integer stock;
private String categoryCode;
private MultipartFile imagename;
public Product toEntity() {
return Product.builder().vendor(vendor).name(name).info(info).price(price).stock(stock)
.categoryCode(categoryCode).countOfReview(0).countOfStar(0).sumOfStar(0)
.salesVolume(0).build();
}
}
}
๐ ํ์ผ์ ์ ๋ก๋ํ๋ฉด ์๋ฒ ์ธก์์๋ MultipartFile๋ก ๋ฐ๋๋ค. ๊ทธ๋ฐ๋ฐ ์ฐ๋ฆฌ๋ ์ด๋ฏธ์ง๋ฅผ ํ ์ด๋ธ์ ์ง์ ์ ์ฅํ์ง ์๊ณ , ํ์ผ๋ช ์ ์ ์ฅํ๋ ค๊ณ ํ๋ค. ๊ทธ๋ฌ๊ธฐ ์ํด์๋ imagename์ด ๋ฐ์ ๋๋ multipart, ์ ์ฅํ ๋๋ String์ด์ด์ผ ํ๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ DTO๋ก ๋นผ ์ค ๊ฒ์ด๋ค.
src/main/java - com.example.demo.controller.mvc - ProductController
package com.example.demo.controller.mvc;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import com.example.demo.dto.ProductDto;
import com.example.demo.service.ProductService;
import lombok.AllArgsConstructor;
@AllArgsConstructor
@Controller
public class ProductController {
private ProductService service;
@GetMapping({"/", "/product/list"})
public String index() {
return "product/list";
}
// ์ํ ์ถ๊ฐ
@GetMapping("/product/add")
public void add() {
}
@PostMapping("/product/add")
public String add(ProductDto.Add dto) {
service.add(dto);
return "redirect:/";
}
}
src/main/java - com.example.demo.service - ProductService
package com.example.demo.service;
import java.io.*;
import java.util.List;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.example.demo.dao.ProductDao;
import com.example.demo.dto.*;
import com.example.demo.dto.ProductDto.Add;
import com.example.demo.entity.Product;
import lombok.*;
@RequiredArgsConstructor
@Service
public class ProductService {
@Value("${ck.image.folder}")
private String CKImageFolder;
@Value("${ck.image.path}")
private String ckImagepath;
@Value("${product.image.folder}")
private String imageFolder;
@Value("${product.image.path}")
private String imagePath;
@Value("${default.image.name}")
private String defaultImage;
private final ProductDao productDao;
public CKResponse ckImageUpload(MultipartFile image){
if (image != null && image.isEmpty() == false) {
String imageName = UUID.randomUUID() + "-" + image.getOriginalFilename();
File file = new File(CKImageFolder, imageName);
try {
image.transferTo(file);
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
}
return new CKResponse(1, imageName, ckImagepath + imageName);
}
return null;
}
public void add(Add dto) {
Product product = dto.toEntity();
// ์ด๋ฏธ์ง์ ๊ฐ์ฒด๋ฅผ ๊ฐ๋ฆฌํด
MultipartFile image = dto.getImagename();
product.setImagename(defaultImage);
// MultipartFile์ด null์ด ์๋๊ณ ์ด๋ฏธ์ง ํ์ผ(image/jpeg, image/png ๋ฑ)์ด๋ผ๋ฉด
if (image != null && image.isEmpty() == false && image.getContentType().toLowerCase().startsWith("image/")) {
String imagename = UUID.randomUUID() + "-" + image.getOriginalFilename();
File file = new File(imageFolder, imagename);
try {
image.transferTo(file);
product.setImagename(imagename);
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
}
}
productDao.save(product);
}
src/main/java - com.example.demo.dao - ProductDao
package com.example.demo.dao;
import java.util.List;
import org.apache.ibatis.annotations.*;
import com.example.demo.dto.ProductDto;
import com.example.demo.entity.*;
@Mapper
public interface ProductDao {
public void save(Product product);
public int count(String categoryCode);
}
src/main/resources - mapper - productMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.dao.ProductDao">
<insert id="save" useGeneratedKeys="true" keyProperty="pno">
<selectKey keyProperty="pno" order="BEFORE" resultType="int">
select product_seq.nextval from dual
</selectKey>
insert into product(pno, vendor, name, info, imagename, price, salesVolume, countOfStar, sumOfStar, countOfReview, stock, categoryCode)
values(#{pno}, #{vendor}, #{name}, #{info}, #{imagename}, #{price}, #{salesVolume}, #{countOfStar}, #{sumOfStar}, #{countOfReview}, #{stock}, #{categoryCode})
</insert>
๐ก selectKey
์ฌ์ ์ ์ด๋ค ํค ๊ฐ์ ๊ฐ์ ธ์ ์ฆ๊ฐ์ํจ ํ ์ ๋ ฅํ๊ฑฐ๋, ์ ๋ ฅ ํ์ ์ฆ๊ฐ๋ ํค ๊ฐ์ ๊ฐ์ ธ์ฌ ํ์๊ฐ ์์ ๋ ์ฌ์ฉํ๋ค. selectKey๋ฅผ ์ด์ฉํ๋ฉด ๋ณ๋์ ์ฟผ๋ฆฌ ๋ก์ง์ ๋ฑ๋กํ ํ์ ์์ด ํด๋น ๋ฉ์๋์์ ์ผ๊ด ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค.
์ถ์ฒ
๐ฃ๏ธ ์์ฑํ ์ํ์ค ๊ฐ์ด ์๋น์ค๋ ์ปจํธ๋กค๋ฌ์์ ํ์ํ ๊ฒฝ์ฐ selectKey๋ฅผ ์ฌ์ฉํ๋ค. ์ฐ๋ฆฌ๋ ์ฌ์ค ์ง๊ธ ํ์ ์๋๋ฐ selectKey ์จ ๋ณด๋ ค๊ณ ์ผ๋ค.