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