[Spring Boot] 16. 사진 업로드 ②, 카테고리 생성

shr·2022년 2월 28일
0

Spring

목록 보기
15/23
post-thumbnail

사진 업로드 ②


💡 파일 업로드 방법

파일 업로드 방법에는 cos.jar를 이용하는 방법, apache commons를 이용하는 방법, Servlet의 자체 지원을 이용하는 방법이 있다. apache commons를 이용하면 설정을 따로 잡아 주어야 하고 csrf에 대한 설정 또한 추가해 주어야 한다. Servlet을 이용하게 되면 따로 설정을 잡아 줄 필요는 없지만 업로드 파일의 크기 제한 설정은 해 주어야 한다. 서버 측 메모리에 업로드된 다음 저장되는 방식이기 때문에 파일 크기가 커질수록 서버에 부담이 가기 때문이다.

우리는 Servlet 자체 지원을 이용해 사진을 업로드한다.


  1. src/main/resources - application.properties에 설정 추가
    ## 사용자 정의 상수 추가
    ck.image.folder = c:/java/upload/ckupload
    ck.image.path = /images/
    product.image.folder = c:/java/upload/image
    product.image.path = /images/
    default.image.name = default.jpg

  1. src/main/java - com.example.demo.service - ProductService 수정

        @Value("c:/java/upload/ckupload")
        private String CKImageFolder;
    
        @Value("/images/")
        private String ckImagepath;

    지난 글에서 이렇게 잡아 주었던 설정을 아래와 같이 수정한다.

    @Value("${ck.image.folder}")
        private String CKImageFolder;
    
        @Value("${ck.image.path}")
        private String ckImagepath;

    📝 application.properties에 사용자 정의 상수를 추가해 준 경로로 재설정해 준다.


  1. 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;
    
        @PostMapping("/product/image")
        public ResponseEntity<CKResponse> ckImageUpload(MultipartFile /* 파라미터명 upload로 고정 */ 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)
                return null;
    
            // 헤더 설정
            HttpHeaders headers = new HttpHeaders();
            String extension = imagename.substring(imagename.lastIndexOf(".")).toUpperCase();
            MediaType type = null;
            if (extension.equals("JPG"))
                type = MediaType.IMAGE_JPEG;
            else if (extension.equals("PNG"))
                type = MediaType.IMAGE_PNG;
            else if (extension.equals("GIF"))
                type = MediaType.IMAGE_GIF;
            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;
        }
    }

    CKEditor로 사진을 업로드하면, response에 사진 경로를 http://localhost:8081/images/사진이름으로 저장한다. 이 경로를 request하면 사진을 보내 주는 컨트롤러이다.

    💡 @PathVariable

    업로드된 사진 이름 {imagename}을 String imagename으로 받아 온다.

    💡 exists()

    자바 파일 객체는 파일이 있으면 열고, 파일이 없으면 0바이트 파일을 만든다. 따라서 file 생성에 실패하는 일은 없지만, 출력이 안 될 수 있다. 그래서 0 바이트 파일인지 확인하는 exists() 메소드를 사용한다.

    📝 파일은 헤더와 내용으로 구성되어 있다. 헤더에는 파일, 생성 날짜 등의 메타 데이터가 담겨 있다. 브라우저가 클라이언트에게 어떤 동작을 할지도 헤더에 담을 수 있다. Content-Dispositon을 이용해 미리 보기(inline)를 할 것인지 다운로드(attachment)를 할 것인지 설정이 가능하다.

    💡 Files.readAllBytes()

    file 객체를 내보낼 때는 byte[]로 변환해야 네트워크에서 출력이 가능하다. 이때 byte[]로 변환해 주는 메소드이다.


카테고리 생성


  1. category 테이블 생성

    create table category (
           code varchar2(3 char), 
           name varchar2(10 char), 
           parent varchar2(3 char), 
           constraint category_pk_code primary key(code)   
     );       
    
    insert into category values('1', '가전', null);    
    insert into category values('11', 'TV', '1');    
    insert into category values('12', '홈시어터', '1');   
    insert into category values('111', 'OLED', '11');    
    insert into category values('112', 'QLED', '11');    
    insert into category values('121', '스피커', '12');    
    insert into category values('122', '사운드바', '12');    
    insert into category values('2', 'PC', null);    
    insert into category values('21', 'CPU', '2');    
    insert into category values('22', '메모리', '2');    
    insert into category values('211', '인텔', '21');    
    insert into category values('212', 'AMD', '21');    
    insert into category values('221', 'DDR4', '22');    
    insert into category values('222', 'DDR5', '22');

  1. src/main/java - com.example.demo.entity - Category 생성

    package com.example.demo.entity;
    
    import lombok.*;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Category {
    
        private String code;
        private String name;
        private String parent;
    
    }

  1. src/main/java - com.example.demo.dao - CategoryDao 생성

    package com.example.demo.dao;
    
    import java.util.*;
    
    import org.apache.ibatis.annotations.*;
    
    import com.example.demo.entity.*;
    
    public interface CategoryDao {
    
        // 대분류 출력
        @Select("select * from category where parent is null")
        public List<Category> readMainCategory();
    
        // 부모 코드를 주고 자식 분류를 출력
        @Select("select * from category where parent=#{parent}")
        public List<Category> readMediumCategory(String parent);
    
    }

  1. src/main/java - com.example.demo.service - CategoryService 생성

    package com.example.demo.service;
    
    import java.util.*;
    
    import org.springframework.stereotype.*;
    import org.springframework.transaction.annotation.*;
    
    import com.example.demo.dao.*;
    import com.example.demo.entity.*;
    
    import lombok.*;
    
    @AllArgsConstructor
    @Service
    public class CategoryService {
    
        private CategoryDao dao;
    
        @Transactional(readOnly = true)
        public List<Category> readCategory(String parent) {
            if (parent == null)
                return dao.readMainCategory();
            return dao.readMediumCategory(parent);
        }
    
    }

    💡 @Transactional(readOnly = true)

    트랜잭션 옵션 중 하나로, 읽기 전용을 뜻한다. insert, update, delete 실행 시 예외가 발생한다. 약간의 속도 향상이 가능해진다.


  1. src/main/java - com.example.demo.controller.rest - CategoryRestController 생성

    package com.example.demo.controller.rest;
    
    import org.springframework.http.*;
    import org.springframework.web.bind.annotation.*;
    
    import com.example.demo.service.*;
    
    import lombok.*;
    
    @AllArgsConstructor
    @RestController
    public class CategoryRestController {
    
        private CategoryService service;
    
        @GetMapping(value="/categories", produces = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<?> readCategory(@RequestParam(required = false) String code) {
            return ResponseEntity.ok(service.readCategory(code));
        }
    
    }

    📝 ResponseEntity<?>라고 적으면 알아서 리턴 타입을 맞춰 준다.

    📝 매핑할 때 consumes, prduces로 입력 형식과 출력 형식을 지정할 수 있다. MVC의 경우 입력은 urlencoded 또는 form-data, 출력은 ModelAndView 또는 JSON일 때 따로 지정하지 않아도 괜찮다.

    💡 @RequestParam(required = false)

    쿼리스트링(querystring) 정보를 쉽게 가지고 올 때 @ResquestParam을 사용한다. 본문을 예시로 들어, 만약 @RequestParam만을 사용했을 때, code라는 필드가 쿼리스트링에 없다면 예외가 발생한다. 이때 (required = false)라는 조건을 붙여 주면 해당 필드가 쿼리스트링에 존재하지 않아도 예외가 발생하지 않는다. 따라서 선택 입력을 받을 때 사용하면 좋다.


    📝 @NotNull과 비슷해 보이지만 @NotNull은 검증을 통해 예외를 ConstraintViolationException으로 바꿔 준다는 점이 다르다.


  1. src/main/resources - template - 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>
            $(document).ready(function() {
                // 대분류 출력
                $.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>
                <div>
                    <select id="main_category">
                        <option value="-1" disabled selected>대분류 선택</option>
                    </select>
                    <select id="medium_category">
                        <option value="-1"  disabled selected>중분류 선택</option>
                    </select>
                    <select id="minor_category">
                        <option value="-1"  disabled selected>소분류 선택</option>
                    </select>
                </div>
                <input type="hidden" name="_csrf" id="_csrf" th:value="${_csrf.token}">
                <div class="form-group">
                    <textarea class="form-control" rows="5" id="info" name="info"></textarea>
                </div>			
            </section>
        </div>
        <footer th:replace="/fragments/footer.html">
        </footer>
    </div>
    </body>
    </html>
profile
못하다 보면 잘하게 되는 거야 ・ᴗ・̥̥̥

0개의 댓글