
📃 요약
네이버 카페 등의 게시판을 기본 기능을 제작하는 예제입니다.
기본적인 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 를 이용해 직접 작성할 수 있어 요즘 많이 서비스 업체 기준으로 사용되고 있습니다.
감사합니다.



