+ 00 00 0000

Have any Questions?

03_Simple Coding – SI-MSA – 파일업로드 -실전예제 2

03_Simple Coding – SI-MSA – 파일업로드 -실전예제 2

📃 요약

네이버 카페 등의 게시판을 기본 기능을 제작하는 예제입니다.
기본적인 DB CRUD 를 사용해 제작합니다.
DB 프레임워크는 JPA 를 사용해 자동화기능을 강화합니다.
뷰(Vue) & 스프링부트 연동 기초 예제

  • axios CRUD 함수들과 스프링부트의 컨트롤러 함수들과 네트웍으로 연결됨
    이때 컨트롤러는 @RestController 어노테이션을 사용해야함
  • Vue : axios 라이브러리의 get(), post(), put(), delete() 함수 사용
  • 스프링부트 – @RestController 어노테이션 사용해서 컨트롤러 클래스 생성
    CRUD : @GetMapping, @PostMapping, @PutMapping, @DeleteMapping
    나머지 모델, 레포지토리, 서비스는 JSP 와 동일함

요소 기술 및 테이블 설계는 아래와 같습니다.

요소 기술 :

– 프론트엔드 : Vue

– 벡엔드 : 스프링부트 & JPA & Oracle 18xe(Oracle Cloud 19c)

결과 화면 :

프로젝트 탐색기 : Vue

프로젝트 탐색기 : String Boot

Rest API :

메소드URL설명
GET/fileDb전체 조회
GET/fileDb/get/{uuid}상세조회
GET/fileDb/{uuid}이미지 다운로드 함수
POST/fileDb/upload추가 함수
PUT/fileDb/edit/{uuid}수정
DELETE/fileDb/delete/{uuid}삭제

📃 기술 구현

스펙 :

- jdk 17
- spring boot 3.x
- gradle

테이블 설계

CREATE TABLE TB_FILE_DB
(
    UUID         VARCHAR2(1000) NOT NULL PRIMARY KEY, -- 파일 UUID
    FILE_TITLE   VARCHAR2(1000),           -- 제목
    FILE_CONTENT VARCHAR2(1000),           -- 내용
    FILE_NAME    VARCHAR2(1000),           -- 파일명
    FILE_DATA    BLOB,                     -- 바이너리 파일(이미지파일)
    FILE_URL     VARCHAR2(1000),           -- 파일 다운로드 URL
    DELETE_YN    VARCHAR2(1) DEFAULT 'N',
    INSERT_TIME  VARCHAR2(255),
    UPDATE_TIME  VARCHAR2(255),
    DELETE_TIME  VARCHAR2(255)
);

업로드 게시판 구현을 위한 테이블 설계입니다.

데이터베이스를 오라클을 사용하여 구현해 보겠습니다.

파일 업로드 로직

파일 업로드
파일을 업로드하는 방법은 대체적으로 2가지임
1) DB 에 파일을 업로드하는 방법과
2) 서버컴퓨터의 일정 폴더에 업로드하는 방법이 있음
DB 업로드를 진행하면 오라클 SQL 을 이용해서 간편하게 업로드가 가능하고, 수정/삭제 등 관리가 용이함
요즘은 DB 보다 가격이 싼 아마존 클라우드(AWS) 의 S3 공간을 임대해서 파일을 업로드하고 사용하기도 함 , 이때는 아마존에서 제공하는 함수를 이용해서 업로드 / 삭제를 진행함
이 예제에서는 과거부터 현재까지 이용되고 있는 DB 에 파일을 저장하는 방법으로 진행하기로 함

  • 특이사항 :
  • UUID 를 사용하여 유일한 값을 저장(시퀀스 사용 않함)
  • 프론트 서비스 함수
    1) 업로드(저장/수정) : FormData 객체를 사용하여 (키, 값) 형태로 저장 후 벡엔드 전송
    2) 헤더 : multipart/form-data 타입으로 전송
  • 벡엔드 엔티티 객체 정의
    1) DB 에 저장하는 이미지 는 BLOB 타입으로 테이블 정의
    2) 엔티티 정의 시 첨부파일 속성은 @Lob 를 붙이고, 타입은 byte[] 로 정의해야함
    3) 서비스 함수에서 저장시 이미지를 다운로드 받을 url 을 생성하여 테이블에 저장하고 프론트의 img 태그에서 그 url 넣어 화면에 이미지를 표시함
    4) 컨트롤러 함수에서 이미지 다운로드 함수를 추가 정의함
    이때 ResponseEntitiy 객체에 헤더/바디를 구분하여 프론트쪽으로 전달해야 이미지 다운로드가 됨
  • 헤더 : CONTENT_DISPOSITION, attachment; filename=”첨부파일명” 이 포함
  • 바디 : 이미지 데이터 저장

모델 : 엔티티

공통 모델

package com.example.jpaexam.model.common;

import lombok.Getter;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * packageName : com.example.jpaexam.model
 * fileName : BaseTimeEntity
 * author : GGG
 * date : 2023-10-16
 * description : JPA 에서 자동으로 생성일자/수정일자를 만들어 주는 클래스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-16         GGG          최초 생성
 */
@Getter
// todo: 자동으로 생성일자/수정일자 컬럼을 sql 문에 추가시키는 어노테이션 2개
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
//    todo: 공통속성 : yyyy-MM-dd HH:mm:ss 아니고 기본 패턴으로 보임
    private String insertTime;

    private String updateTime;

//    todo: 해당 테이블에 데이터가 만들어 질때(insert 문) 실행되는 이벤트 함수
    @PrePersist
    void OnPrePersist() {
        this.insertTime
                = LocalDateTime.now()
                    .format(DateTimeFormatter
                            .ofPattern("yyyy-MM-dd HH:mm:ss"));
    }

//    todo: 해당 테이블에 데이터가 수정 질때(update 문) 실행되는 이벤트 함수
    @PreUpdate
    void OnPreUpdate() {
        this.updateTime
                = LocalDateTime.now()
                .format(DateTimeFormatter
                        .ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.insertTime = this.updateTime; // 생성일시 == 수정일시 동일하게 처리
    }
}

1) DB 에 저장하는 이미지 는 BLOB 타입으로 테이블 정의
2) 엔티티 정의 시 첨부파일 속성은 @Lob 를 붙이고, 타입은 byte[] 로 정의해야함

파일 업로드 레포지토리

package com.example.jpaexam.repository.advanced;

import com.example.jpaexam.model.entity.advanced.FileDb;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * packageName : com.example.jpaexam.repository
 * fileName : DeptRepostory
 * author : kangtaegyung
 * date : 2022/10/16
 * description : JPA CRUD 인터페이스 ( DAO 역할 )
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/10/16         kangtaegyung          최초 생성
 */
@Repository
public interface FileDbRepository extends JpaRepository<FileDb, String> {

//    title like 검색
    Page<FileDb> findAllByFileTitleContaining(String fileTitle, Pageable pageable);
}

select : @Query 이용한 오라클 기반 쿼리(nativeQuery = true) 임

파일 업로드 게시판 서비스

package com.example.jpaexam.service.advanced;

import com.example.jpaexam.model.entity.advanced.FileDb;
import com.example.jpaexam.repository.advanced.FileDbRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.io.IOException;
import java.util.Optional;
import java.util.UUID;

/**
 * packageName : com.example.modelexam.service
 * fileName : DeptService
 * author : kangtaegyung
 * date : 2022/10/12
 * description : Qna 서비스 클래스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/10/12         kangtaegyung          최초 생성
 */
@Slf4j
@Service
public class FileDbService {

    @Autowired
    FileDbRepository fileDbRepository; // 샘플데이터 DB에 접근하는 객체

    public Page<FileDb> findAllByFileTitleContaining(String fileTitle, Pageable pageable) {
        Page<FileDb> page = fileDbRepository.findAllByFileTitleContaining(fileTitle, pageable);
        return page;
    }

    public Page<FileDb> findAll(Pageable pageable) {
        Page<FileDb> page = fileDbRepository.findAll(pageable);
        return page;
    }

    public Optional<FileDb> findById(String uuid) {
        return fileDbRepository.findById(uuid);
    }

    // TODO: uuid 를 생성해서 insert 또는 기존 uuid 를 전달받아 update
    public FileDb save(String uuid, String fileTitle, String fileContent, MultipartFile file) throws IOException {

        FileDb fileDb2 = null;

        try {
            //        기본키가 없으면 insert
            if (uuid == null) {
                //        램덤 고유값 구하기 : UUID, 중간에 - 있음, uuid 생성
                String tmpUuid = UUID.randomUUID().toString().replace("-", "");

                //        다운로드 url 만들기
                String fileDownloadUri = ServletUriComponentsBuilder
                        .fromCurrentContextPath()
                        .path("/advanced/fileDb/")
                        .path(tmpUuid)
                        .toUriString();

                FileDb fileDb = new FileDb(tmpUuid, fileTitle, fileContent, file.getOriginalFilename(), file.getBytes(), fileDownloadUri);
                fileDb2 = fileDbRepository.save(fileDb);
            } else {
//        기본키가 있으면 update
                //        다운로드 url 만들기, 기존 uuid 사용
                String fileDownloadUri = ServletUriComponentsBuilder
                        .fromCurrentContextPath()
                        .path("/advanced/fileDb/")
                        .path(uuid)
                        .toUriString();

                FileDb fileDb = new FileDb(uuid, fileTitle, fileContent, file.getOriginalFilename(), file.getBytes(), fileDownloadUri);
                fileDb2 = fileDbRepository.save(fileDb);
            }
        } catch (Exception e) {
            log.debug(e.getMessage());
        }

        return fileDb2;
    }

    public boolean removeById(String uuid) {

        if (fileDbRepository.existsById(uuid)) {
            fileDbRepository.deleteById(uuid);
            return true;
        }
        return false;
    }
}

3) save 서비스 함수에서 저장시 이미지를 다운로드 받을 url 을 생성하여 테이블에 저장하고 프론트의 img 태그에서 그 url 넣어 화면에 이미지를 표시함

파일 업로드 게시판 컨트롤러

package com.example.simpledms.controller.advanced;

import com.example.simpledms.model.entity.advanced.FileDb;
import com.example.simpledms.service.advanced.FileDbService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 * packageName : com.example.modelexam.controller
 * fileName : FileDbController
 * author : kangtaegyung
 * date : 2022/10/12
 * description : 부서 컨트롤러
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/10/12         kangtaegyung          최초 생성
 */
@Slf4j
@RestController
@RequestMapping("/api/advanced")
public class FileDbController {

    @Autowired
    FileDbService fileDbService;

    //  Todo:
    @GetMapping("/fileDb")
    public ResponseEntity<Object> findAllByFileTitleContaining(@RequestParam(defaultValue = "") String fileTitle,
                                               @RequestParam(defaultValue = "0") int page,
                                               @RequestParam(defaultValue = "3") int size)
    {
        try {
            //            페이지 변수 저장
            Pageable pageable = PageRequest.of(page, size);

//        다운로드 url 을 만들어 DTO 에 저장함
            Page<FileDb> fileDbPage = fileDbService.findAllByFileTitleContaining(fileTitle, pageable);

            Map<String, Object> response = new HashMap<>();
            response.put("fileDb", fileDbPage.getContent());
            response.put("currentPage", fileDbPage.getNumber());
            response.put("totalItems", fileDbPage.getTotalElements());
            response.put("totalPages", fileDbPage.getTotalPages());

            if (fileDbPage.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(response, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    //    fileDb fid로 조회
    @GetMapping("/fileDb/get/{uuid}")
    public ResponseEntity<Object> findById(@PathVariable String uuid) {
        try {
            //            Vue에서 전송한 매개변수 데이터 확인
            log.info("uuid {}", uuid);

            Optional<FileDb> fileDbOptional = fileDbService.findById(uuid);

            if (fileDbOptional.isPresent()) {
                return new ResponseEntity<Object>(fileDbOptional.get(), HttpStatus.OK);
            } else {
                return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);

            return new ResponseEntity<Object>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    //    화면 출력 시 자동적으로 ID로 조회됨(이미지 id 는 uuid 임)
//    react 에서  실행될때 자동 실행되는 함수
    @GetMapping("/fileDb/{uuid}")
    public ResponseEntity<byte[]> findByIdDownloading(@PathVariable String uuid) {
        FileDb fileDb = fileDbService.findById(uuid).get();

        return ResponseEntity.ok()
//           Todo : attachment: => attachment;
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileDb.getFileName() + "\"")
                .body(fileDb.getFileData());
    }

    @PostMapping("/fileDb/upload")
    public ResponseEntity<Object> create(
            @RequestParam(defaultValue = "") String fileTitle,
            @RequestParam(defaultValue = "") String fileContent,
            @RequestParam MultipartFile image
    ) {
        String message = "";

        log.info("title {} : ", fileTitle);
        log.info("content {} : ", fileContent);
        log.info("image {} : ", image);

        try {
            fileDbService.save(null, fileTitle, fileContent, image);

            return new ResponseEntity<>("upload the file successfully!", HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<>("Could not upload the file!", HttpStatus.EXPECTATION_FAILED);
        }
    }

    @PutMapping("/fileDb/{uuid}")
    public ResponseEntity<Object> update(
            @PathVariable String uuid,
            @RequestParam String fileTitle,
            @RequestParam String fileContent,
            @RequestParam MultipartFile image
    ) {
        String message = "";

        log.info("uuid {} : ", uuid);
        log.info("title {} : ", fileTitle);
        log.info("content {} : ", fileContent);
        log.info("image {} : ", image);

        try {
            fileDbService.save(uuid, fileTitle, fileContent, image);

            return new ResponseEntity<>( "upload the file successfully!", HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<>( "Could not upload the file!", HttpStatus.EXPECTATION_FAILED);
        }
    }

    @DeleteMapping("/fileDb/deletion/{uuid}")
    public ResponseEntity<Object> delete(@PathVariable String uuid) {

//        프론트엔드 쪽으로 상태정보를 보내줌
        try {
            boolean bSuccess = fileDbService.removeById(uuid);

            if (bSuccess == true) {
//                delete 문이 성공했을 경우
                return new ResponseEntity<>("delete the file successfully!", HttpStatus.OK);
            }
//            delete 실패했을 경우( 0건 삭제가 될경우 )
            return new ResponseEntity<>("delete no content", HttpStatus.NO_CONTENT);
        } catch (Exception e) {
//            DB 에러가 날경우
            return new ResponseEntity<>("Could not delete the file!", HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

4) findByIdDownloading 컨트롤러 함수에서 이미지 다운로드 함수를 추가 정의함
이때 ResponseEntitiy 객체에 헤더/바디를 구분하여 프론트쪽으로 전달해야 이미지 다운로드가 됨

  • 헤더 : CONTENT_DISPOSITION, attachment; filename=”첨부파일명” 이 포함
  • 바디 : 이미지 데이터 저장

Vue 페이지

1) 공통 js

– axios import 및 기본설정파일

  • utils/http-common.js : 생략

– axios 공통 함수 : get/post/put/delete 방식 함수들

  • advanced/FileDbService.js

    import http from "@/utils/http-common";

class FileDbService {
create(fileDb, image) {

console.log(fileDb);

// 이미지 upload 는 form-data 형식으로 전송해야 함
let formData = new FormData();

formData.append("fileTitle", fileDb.fileTitle);
formData.append("fileContent", fileDb.fileContent);
formData.append("image", image);

// 액시오스.post("url", 데이터, [ Content-Type headers ], post 요청 결과 끝에 덧붙여서 실행할 함수)
return http.post("/advanced/fileDb/upload", formData, {
  headers: {
    "Content-Type": "multipart/form-data",
  },
});

}

update(fileDb, image) {

console.log("update() fileDb ; ", fileDb);

let formData = new FormData();

formData.append("fileTitle", fileDb.fileTitle);
formData.append("fileContent", fileDb.fileContent);
formData.append("image", image);

return http.put(`/advanced/fileDb/${fileDb.uuid}`, formData, {
  headers: {
    "Content-Type": "multipart/form-data"
  },
});

}

getAll(fileTitle, page, size) {
console.log(fileTitle);
console.log(page);
console.log(size);
return http.get(`/advanced/fileDb?fileTitle=${fileTitle}&page=${page}&size=${size}`);
}

get(uuid) {
return http.get(`/advanced/fileDb/get/${uuid}`);
}

delete(uuid) {
return http.delete(`/advanced/fileDb/deletion/${uuid}`);
}
}

export default new FileDbService();

1) 첨부파일을 벡엔드에 전송하기 위해서는 2가지가 필요함
    - 헤더에 표시 : "Content-Type": "multipart/form-data"
    - form 태그 또는 FormData() 객체에 값을 넣어 전송해야함 , form 전송은 쿼리스트링 방식으로 
      벡엔드는 @RequestParam 을 사용해야함

```js
- 사용법 : FormData 객체 생성후 활용 방법
...
let 변수명 = new FormData();
변수명.append("키", 값);
...

Vue 전체조회 페이지

결과 화면 :

FileDbList.vue

<template>
  <!-- 3) -->
  <div>
    <div class="col-md-8">
      <!-- {/* 검색어 start */} -->
      <div class="input-group mb-3">
        <input
          type="text"
          class="form-control"
          placeholder="Search by title"
          v-model="searchTitle"
        />
        <div class="input-group-append">
          <button
            class="btn btn-outline-secondary"
            type="button"
            @click="retrieveFileDb"
          >
            Search
          </button>
        </div>
      </div>
      <!-- {/* 검색어 end */} -->
    </div>

    <div class="col-md-12 mt-3">
      <h4>FileDb List</h4>
      <!-- {/* page start */} -->
      <div class="mb-3">
        Items per Page:
        <select v-model="pageSize" @change="handlePageSizeChange($event)">
          <option v-for="size in pageSizes" :key="size" :value="size">
            {{ size }}
          </option>
        </select>
      </div>

      <!-- b-pagination : 부트스트랩 - 페이지 번호 컨트롤 -->
      <!-- total-rows : 전체 데이터 개수 -->
      <!-- per-page : 1페이지 당 개수 -->
      <!-- change : handlePageChange(), 페이지 번호 변경 시 실행되는 이벤트 -->
      <b-pagination
        v-model="page"
        :total-rows="count"
        :per-page="pageSize"
        prev-text="Prev"
        next-text="Next"
        @change="handlePageChange"
      ></b-pagination>
      <!-- {/* page end */} -->

      <!-- {/* 쇼핑카트 이미지 start */} -->
      <div class="row">
        <div v-for="(data, index) in fileDb" :key="index" class="col-sm-6">
          <div class="card">
            <img :src="data.fileUrl" class="card-img-top" alt="강의" />
            <div class="card-body">
              <h5 class="card-title">{{ data.fileTitle }}</h5>
              <p class="card-text">{{ data.fileContent }}</p>
              <router-link :to="'/fileDb/' + data.uuid">
                <span class="badge bg-warning">Edit</span>
              </router-link>
              <a
                style="
                   {
                    color: inherit;
                  }
                "
                class="ms-2"
                @click="deleteImage(data.uuid)"
              >
                <span class="badge bg-danger">Delete</span>
              </a>
            </div>
          </div>
        </div>
      </div>
      <!-- {/* 쇼핑카트 이미지 end */} -->
    </div>
  </div>
</template>

<script>
import FileDbService from "@/services/advanced/FileDbService";

export default {
  data() {
    return {
      fileDb: [],
      searchTitle: "",

      page: 1,
      count: 0,
      pageSize: 3,

      pageSizes: [3, 6, 9],
    };
  },
  methods: {
    retrieveFileDb() {
      FileDbService.getAll(this.searchTitle, this.page - 1, this.pageSize)
        .then((response) => {
          const { fileDb, totalItems } = response.data;
          this.fileDb = fileDb;
          this.count = totalItems;

          console.log(response.data);
        })
        .catch((e) => {
          console.log(e);
        });
    },

    //  페이지 번호 변경시 실행되는 함수
    //  부트스트랩-페이지 양식에 페이지번호만 매개변수로 전달하면 됨
    // 페이지번호를 변경한 숫자가 매개변수(value)로 전달됨
    handlePageChange(value) {
      this.page = value;
      this.retrieveFileDb();
    },

    // 셀렉트 박스 값 변경시 (페이지 크기 변경) 실행되는 함수
    // event.target.value : 셀렉트 박스에서 선택된 값
    handlePageSizeChange(event) {
      this.pageSize = event.target.value;
      this.page = 1;
      this.retrieveFileDb();
    },
    //  이미지 & 데이터 삭제
    deleteImage(uuid) {
      // alert(id)
      FileDbService.delete(uuid)
        .then((response) => {
          console.log(response);

          this.message = "정상적으로 삭제되었습니다. ";

          //  삭제 후 재조회
          this.retrieveFileDb();
        })
        .catch((e) => {
          console.log(e);

          this.message = "삭제 시 에러가 발생했습니다. " + e.message;
        });
    },
  },
  mounted() {
    this.retrieveFileDb();
  },
};
</script>

<style>
.list {
  text-align: left;
  max-width: 750px;
  margin: auto;
}
</style>

Vue 추가 페이지

결과 화면 :

AddFileDb.vue

<template>
  <div>
    <div class="col-6 mx-auto">
      <!-- {/* 이미지명(fileTitle) 입력 박스 시작 */} -->
      <div class="row g-3 align-items-center mb-3">
        <div class="col-3">
          <label htmlFor="fileTitle" class="form-label"> 이미지명 </label>
        </div>

        <div class="col-9">
          <input
            type="text"
            class="form-control"
            id="fileTitle"
            required
            name="fileTitle"
            v-model="fileDb.fileTitle"
          />
        </div>
      </div>
      <!-- {/* 이미지명 입력 박스 끝 */} -->

      <!-- {/* 이미지내용 입력 박스 시작 */} -->
      <div class="row g-3 align-items-center mb-3">
        <div class="col-3">
          <label htmlFor="fileContent" class="form-label"> 내용 </label>
        </div>

        <div class="col-9">
          <input
            type="text"
            class="form-control"
            id="fileContent"
            required
            name="fileContent"
            v-model="fileDb.fileContent"
          />
        </div>
      </div>
      <!-- {/* 이미지내용 입력 박스 끝 */} -->

      <div class="input-group mb-3">
        <!-- {/* upload 선택상자/버튼 start */} -->
        <input type="file" accept="image/*" ref="file" @change="selectImage" />

        <button
          class="btn btn-outline-secondary"
          type="button"
          id="inputGroupFileAddon04"
          :disabled="!currentImage"
          @click="create"
        >
          Upload
        </button>
      </div>
      <!-- {/* upload 선택상자/버튼 end */} -->

      <!-- {/* upload 성공/실패 메세지 출력 시작 */} -->
      <div v-if="message" class="alert alert-success" role="alert">
        {{ message }}
      </div>
      <!-- {/* upload 성공/실패 메세지 출력 끝 */} -->
    </div>
  </div>
</template>
<script>
import FileDbService from "@/services/advanced/FileDbService";

export default {
  data() {
    return {
      currentImage: undefined, // 현재이미지
      // previewImage: undefined, // 미리보기 이미지
      message: "", // 서버쪽 메세지 받는 변수

      fileDb: {
        uuid: null,
        fileTitle: "",
        fileContent: "",
        fileUrl: "",
      }, // 이미지 정보 객체배열

      // Todo : 아래 변수 추가
      page: 1,
      count: 0,
      pageSize: 3,

      pageSizes: [3, 6, 9],
    };
  },
  methods: {
    // 이미지를 선택하면 변수에 저장하는 메소드
    selectImage() {
      // 파일선택상자에서 첫번째로 선택한 이미지가 저장됨
      this.currentImage = this.$refs.file.files[0];
      // 아래는 미리보기 이미지를 위한 주소가 저장됨
      this.message = "";
    },
    // 파일 업로드를 위한 메소드
    create() {
      // 서버에 이미지 업로드 요청(insert 문 실행)
      FileDbService.create(this.fileDb, this.currentImage)
        // 성공하면 then 으로 들어옴(서버에서 응답한 객체)
        .then((response) => {
          // 서버쪽 응답 메시지 저장
          this.message = response.data;

          console.log(response.data);
        })
        // 실패하면 catch으로 들어옴
        // 화면에 실패메세지 출력
        .catch((e) => {
          this.message = "Could not upload the image!" + e;
          this.currentImage = undefined;

          console.log(e);
        });
    },
  },
};
</script>
<style lang=""></style>

1) Vue 에서 태그에 직접 접근 방법 :

  • html 태그 사용법 : ref=”변수명”
  • 활용법 : this.$refs.변수명
  • v-model 을 사용할 수 없는 태그에 사용
    2) 파일 선택상자에서 선택한 파일은 files 속성 배열에 저장됨
    • 파일선택상자는 파일을 여러개 선택할 수 있은 기능이 있음
    • 그래서 파일을 여러개 저장할 수 있는 속성으로 files[인덱스번호] 있음
<template>
  ...
    <input type="file" accept="image/*" ref="변수명" @change="selectImage" />
  ...
</template>

<script>
  ...
  this.currentImage = this.$refs.변수명.files[0];
  ...
</script>

Vue 수정 페이지

결과 화면 :

FileDbDetail.vue

<template>
  <div class="col-6 mx-auto">
    <!-- {/* 이미지명(fileTitle) 입력 박스 시작 */} -->
    <div class="mb-3 col-md-12">
      <label htmlFor="fileTitle" class="form-label"> 이미지명 </label>
      <input
        type="text"
        class="form-control"
        id="fileTitle"
        required
        name="fileTitle"
        v-model="fileDb.fileTitle"
      />
    </div>
    <!-- {/* 이미지명 입력 박스 끝 */} -->

    <!-- {/* 이미지내용 입력 박스 시작 */} -->
    <div class="mb-3 col-md-12">
      <label htmlFor="fileContent" class="form-label"> 내용 </label>
      <input
        type="text"
        class="form-control"
        id="fileContent"
        required
        name="fileContent"
        v-model="fileDb.fileContent"
      />
    </div>

    <!-- {/* 이미지내용 입력 박스 끝 */} -->
    <div class="mb-3 col-md-12">
      <img :src="fileDb.fileUrl" class="card-img-top" alt="강의" />
    </div>

    <!-- {/* upload 선택상자/버튼 start */} -->
    <div class="input-group mb-3">
      <!-- {/* upload 선택상자/버튼 start */} -->
      <label class="btn btn-default p-0 mb-3">
        <input type="file" accept="image/*" ref="file" @change="selectImage" />
      </label>

      <button
        class="btn btn-success mb-3"
        :disabled="!currentImage"
        @click="update"
      >
        Update
      </button>
    </div>
    <!-- {/* upload 선택상자/버튼 end */} -->

    <!-- {/* upload 성공/실패 메세지 출력 시작 */} -->
    <div v-if="message" class="alert alert-success" role="alert">
      {{ message }}
    </div>
    <!-- {/* upload 성공/실패 메세지 출력 끝 */} -->
  </div>
</template>
<script>
import FileDbService from "@/services/advanced/FileDbService";

export default {
  data() {
    return {
      currentImage: undefined, // 현재이미지
      message: "", // 서버쪽 메세지 받는 변수

      fileDb: {
        uuid: this.$route.params.uuid,
        fileTitle: "",
        fileContent: "",
        fileUrl: "",
      }, // 이미지 정보 객체배열

      // Todo : 아래 변수 추가
      page: 1,
      count: 0,
      pageSize: 3,

      pageSizes: [3, 6, 9],
    };
  },
  methods: {
    // 이미지를 선택하면 변수에 저장하는 메소드
    selectImage() {
      // 파일선택상자에서 첫번째로 선택한 이미지가 저장됨
      this.currentImage = this.$refs.file.files[0];
      // 아래는 미리보기 이미지를 위한 주소가 저장됨
      this.message = "";
    },
    // 파일 업로드를 위한 메소드
    update() {
      // 서버에 이미지 업로드 요청(insert 문 실행)
      FileDbService.update(this.fileDb, this.currentImage)
        // 성공하면 then 으로 들어옴(서버에서 응답한 객체)
        .then((response) => {
          // 서버쪽 응답 메시지 저장
          this.message = response.data.message;
        })
        // 실패하면 catch으로 들어옴
        // 화면에 실패메세지 출력
        .catch((err) => {
          this.message = "Could not update the image!" + err;
          this.currentImage = undefined;
        });
    },
    get(uuid) {
      FileDbService.get(uuid) // spring 요청
        //  성공/실패 모르겠지만
        //  성공하면 then안에 있는것이 실행
        .then((response) => {
          this.fileDb = response.data;
          console.log(response.data);
        })
        //  실패하면 catch안에 있는것이 실행
        .catch((e) => {
          console.log(e);
        });
    },
  },

  mounted() {
    this.message = "";
    this.get(this.$route.params.uuid);
  },
};
</script>
<style lang=""></style>

📃 결론

업로드 게시판 샘플 예제를 Vue & Spring boot 연동 예제를 살펴보았습니다.
기본적인 CRUD 기능을 구현했으며 게시판의 페이징 처리도 해봤습니다.
Spring Boot 는 @RestController 어노테이션을 이용해 구현했으며, 결과는 JSON 데이터로 리턴했으며,
Vue 에 axios 라이브러리를 이용해 전달했습니댜.

DB 프레임워크는 JPA 를 이용해서 sql 문을 직접 제작하지 않고 자동화기능을 이용해 구현했습니다.
Mybatis 의 직접 sql 문 제작 기능에 대응해 자주 반복되고 쉬운 기능은 JPA 의 sql 자동생성 기능을 이용하고,
복잡한 sql 문은 @Query 를 이용해 직접 작성할 수 있어 요즘 많이 서비스 업체 기준으로 사용되고 있습니다.

감사합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다