return URLEncoder.encode(folderPath+"/"+uuid+"_"+fileName, "UTF-8");
파일 업로드 처리 방법
- 파일 업로드 라이브러리 (commos-fileupload)
- 자체 파일 업로드 라이브러리
서블릿 기반 자체 파일 업로드 라이브러리 사용해 파일 업로드 처리
1. 섬네일을 만들어 이미지 파일 업로드 처리
2. 목록, 조회 화면에 섬네일 사용, 조회 화면에서 섬네일 클릭시 원본 파일 보이도록 작성
진행 순서
1. 파일 업로드를 위한, 애플리케이션 설정과 UploadController 작성
2. 업로드된 파일 저장할 때의 고려사항 적용과 Ajax를 통한 실제 업로드 이벤트 작성
3. 업로드 결과 반환과 화면 처리
1. 파일 업로드을 위한 애플리케이션 설정
#파일 업로드를 위한 설정
spring.servlet.multipart.enabled=true
#파일 업로드 가능 여부
spring.servlet.multipart.location=/Users/------/desktop/upload
#업로드된 파일의 임시 저장 경로
spring.servlet.multipart.max-request-size=30MB
#한 번에 최대 업로드 가능 용량
spring.servlet.multipart.max-file-size=10MB
#파일 하나의 최대 크기
(1) 파일 업로드를 위한 UploadController 작성
: Ajax를 이용해 JSON으로 처리하고, 별도의 화면처리는 하지않는다.
-> MultipartFile을 이용해 파일 처리
package com.movie.boot4.controller;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
//파일 업로드를 위한 클래스
@RestController
@Log4j2
public class UploadController {
//MultipartFile 타입을 이용한 파일 사용
//: Ajax를 이용한 파일 업로드 처리 -> 업로드 결과에 대한 화면 작성 X
//따라서 업로드 결과는 JSON 형태로 제공
@PostMapping("/uploadAjax")
public void uploadFile(MultipartFile[] uploadFiles){
for(MultipartFile uploadFile : uploadFiles){
//실제 파일 이름이 전체 경로가 들어오기 때문에 원본 이름 처리와 실제 파일 이름 처리
String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\")+1);
log.info("filename: "+fileName);
}//end for
}
}
-> uploadFile() 메서드가 파라미터로 MultipartFile 배열을 받도록 설계하고, 배열을 활용해 여러 개 파일 정보 처리하므로
화면에서 여러 개의 파일을 동시에 업로드 가능
(2) 업로드 테스트를 위한 UploadTestController 클래스 작성
: GET 방식으로 화면을 볼 수 있도록 구성
UploadTestController
package com.movie.boot4.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class UploadTestController {
@GetMapping("/uploadEx")
public void uploadEx(){
}
}
uploadEx.html : 파일 업로드 처리
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<input name="uploadFiles" type="file" multiple>
<button class="uploadBtn">Upload</button>
<div class="uploadResult">
</div>
<script
src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
crossorigin="anonymous"></script>
<script>
//업로드 버튼 이벤트 처리 순서
//(1) Upload 클릭시 FormData 생성
//(2) 컨트롤러의 uploadFiles 이름으로 파일 데이터 추가
//(3) 여러 개 파일 업로드할 수 있는 화면 생성
//(4) 파일 선택 후, 업로드 버튼 클릭시 선택 파일 정보가 콘솔창에 출력
//업로드 이벤트 처리 구성
//(1) 버튼 클릭시 사용할 함수 작성
//(2) 변수 : fromData, inputFile, files
//(3) 반복문을 통한 메서드 호출 : console에 출력, formData에 파일 추가
$('.uploadBtn').click(function( ) {
var formData = new FormData();
var inputFile = $("input[type='file']");
var files = inputFile[0].files;
for (var i = 0; i < files.length; i++) {
console.log(files[i]);
formData.append("uploadFiles", files[i]);
}
//업로드 버튼 클릭시 실제 업로드 처리 부분은 ajax를 이용해 처리
//(1) 사용할 속성 지정
//: contentType 속성은 false로 지정해, 'multipart/form-data'타입을 사용
//: dataType은 json을 이용해 컨트롤러의 메서드에서 데이터 반환해 화면에 처리
//(2) 파일 업로드 실행시 출력 하는 조건
//1. 파일 업로드 성공
//: success : function(result)
//-> console.log(result);
//2. 파일 업로드 실패
//: error : function(jqXHR, textStatus, errorThrown)
//-> console.log(textStatus);
//실제 업로드 부분
//upload ajax
$.ajax({
url: '/uploadAjax',
processData: false,
contentType: false,
data: formData,
type: 'POST',
dataType:'json',
success: function(result){
console.log(result);
//나중에 화면 처리
},
error: function(jqXHR, textStatus, errorThrown){
console.log(textStatus);
}
}); //$.ajax
}); //end click
</script>
</body>
</html>
(2) 업로드된 파일의 저장
파일 저장 방법
- 스프링에서 제공하는 FileCopyUtils 이용
- MultipartFile 자체의 transferTo() 이용
com.movie.upload.path=/Users/------/desktop/upload
#업로드 파일 저장 경로의 설정값 추가해, Upload Controller에서 이용하도록 작성
UploadController에 파일 저장 경로 변수 추가
package com.movie.boot4.controller;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
//파일 업로드를 위한 클래스
@RestController
@Log4j2
public class UploadController {
//업로드된 파일 저장 결로 설정
@Value("${com.movie.upload.path") //애플리케이션 설정 변수
private String uploadPath;
//MultipartFile 타입을 이용한 파일 사용
//: Ajax를 이용한 파일 업로드 처리 -> 업로드 결과에 대한 화면 작성 X
//따라서 업로드 결과는 JSON 형태로 제공
@PostMapping("/uploadAjax")
public void uploadFile(MultipartFile[] uploadFiles){
for(MultipartFile uploadFile : uploadFiles){
//실제 파일 이름이 전체 경로가 들어오기 때문에 원본 이름 처리와 실제 파일 이름 처리
String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\")+1);
log.info("filename: "+fileName);
}//end for
}
}
2. 파일 저장 단계에서 고려사항
- 동일한 이름의 파일 업로드시 기존 파일 덮어쓰는 문제 방지
- 업로드된 파일 저장 폴더의 용량 고려
- 업로드된 확장자가 이미지만 가능하도록 검사 [첨부파일을 이용한 원격 셀]
1. 동일한 이름 파일 문제
: 고유한 이름 생성해 파일 이름으로 사용
고유한 이름 생성하는 방식
- 시간 값을 파일 이름에 추가
- UUID를 이용해 고유한 값을 만들어 사용
UUID를 이용해, 파일이름에 "UUID값_파일명" 형태로 저장
2. 동일한 폴더에 너무 많은 파일 존재하는 문제
: 파일이 저장되는 시점의 폴더를 생성해 분할
-> '년/월/일' 폴더로 생성
3. 파일의 확장자 체크
: 쉘 스크립트 파일을 업로드해 공격하는 기법이 존재하므로, 이를 방지하기 위한 검사 과정
-> MultipartFile에서 제공하는 getContentType()이용해 처리
적용 전의 UploadController 클래스
package com.movie.boot4.controller;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
//파일 업로드를 위한 클래스
@RestController
@Log4j2
public class UploadController {
//업로드된 파일 저장 결로 설정
@Value("${com.movie.upload.path}") //애플리케이션 설정 변수
private String uploadPath;
//MultipartFile 타입을 이용한 파일 사용
//: Ajax를 이용한 파일 업로드 처리 -> 업로드 결과에 대한 화면 작성 X
//따라서 업로드 결과는 JSON 형태로 제공
@PostMapping("/uploadAjax")
public void uploadFile(MultipartFile[] uploadFiles){
for(MultipartFile uploadFile : uploadFiles){
//실제 파일 이름이 전체 경로가 들어오기 때문에 원본 이름 처리와 실제 파일 이름 처리
String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\")+1);
log.info("filename: "+fileName);
}//end for
}
}
적용 후의 UploadController 클래스
package com.movie.boot4.controller;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
//파일 업로드를 위한 클래스
@RestController
@Log4j2
public class UploadController {
//업로드된 파일 저장 결로 설정
@Value("${com.movie.upload.path}") //애플리케이션 설정 변수
private String uploadPath;
//MultipartFile 타입을 이용한 파일 사용
//: Ajax를 이용한 파일 업로드 처리 -> 업로드 결과에 대한 화면 작성 X
//따라서 업로드 결과는 JSON 형태로 제공
@PostMapping("/uploadAjax")
public void uploadFile(MultipartFile[] uploadFiles) {
for (MultipartFile uploadFile : uploadFiles) {
//1. 확장자 검사
if (uploadFile.getContentType().startsWith("image") == false) {
log.warn("this is not image type");
return;
}
//실제 파일 이름이 전체 경로가 들어오기 때문에 원본 이름 처리와 실제 파일 이름 처리
String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
log.info("filename: " + fileName);
//2. 동일한 폴더에 많은 파일 방지
//(1) 날짜 폴더 생성
String folderPath = makeFolder();
//(2) UUID 클래스를 이용한 고유한 파일 이름 생성
String uuid = UUID.randomUUID().toString();
//(3) UUID를 이용해 저장 파일 이름 중간에 "_"를 이용해 구분
String saveName = uploadPath + File.separator + folderPath + File.separator + uuid + "_" + fileName;
Path savePath = Paths.get(saveName);
try {
uploadFile.transferTo(savePath);
} catch (IOException e) {
e.printStackTrace();
}
}//end for
}
//(3) 폴더 생성 메서드 작성
private String makeFolder(){
String str= LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String folderPath = str.replace("/", File.separator);
//경로에 폴더가 없을 경우 폴더 생성
File uploadPathFolder =new File(uploadPath, folderPath);
if(uploadPathFolder.exists()==false){
uploadPathFolder.mkdirs();
}
return folderPath;
}
}
3. 업로드 결과 반환과 화면 처리
: 업로드 결과를 화면에 나타내기 위해, 결과 데이터를 JSON으로 전송
화면 반환에 필요한 결과 데이터 구조
- 업로드된 파일의 원래 이름
- 파일의 UUID 값
- 업로드된 파일의 저장 경로
UploadController에서 문자열로 처리가 가능하지만, 브라우저에서 화면의 처리를 간단하게 할 수 있도록
-> 클래스와 객체를 구성해 처리
(1) UploadResultDTO 클래스 작성
: 결과 데이터를 위한 DTO클래스로, 컨트롤러에서 Entity로 받아서 데이터를 전달한다.
[데이터의 전체 경로가 필요할 경우 사용하기 위한 getImageURL 메서드를 미리 작성해 둔다.]
package com.movie.boot4.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@Data
@AllArgsConstructor
//직렬화 인터페이스 구현
//: 객체 데이터를 바이트 형태로 반환해 JSON으로 결과 데이터를 반환하기 위해서
//-> 직렬화 대상은 필드만 적용되며, 생성자와 메서드는 대상에 포함되지 않는다.
//JPA 엔티티를 사용한다면, 기본값으로 직렬화가 적용되어 있는것을 알 수 있다.
public class UploadResultDTO implements Serializable {
private String fileName;
private String uuid;
private String folderPath;
//해당 DTO는 실제 파일과 관련된 모든 정보를 가지는데, 나중에 전체 경로가 필요할 경우 사용하기 위한 메서드
//: 컨트롤러에서 업로드 결과 반환하기 위해 DTO를 엔티티로 반환해 사용
public String getImageURL(){
try{
return URLEncoder.encode(folderPath+"/"+uuid+"_"+fileName, "UTF-8");
}catch (UnsupportedEncodingException e){
e.printStackTrace();
}
return "";
}
}
:직렬화를 하면, 직접 데이터를 문자열 형태로 확인 가능해 JSON으로 데이터를 저장하기 용이해지므로 보통 범용적인 API나 데이터를 변환하여 추출할 때 사용한다.
UploadController의 uploadFile 메서드를 ResponseEntity를 반환하도록 변경
변경 전, uploadFile
//MultipartFile 타입을 이용한 파일 사용
//: Ajax를 이용한 파일 업로드 처리 -> 업로드 결과에 대한 화면 작성 X
//따라서 업로드 결과는 JSON 형태로 제공
@PostMapping("/uploadAjax")
public void uploadFile(MultipartFile[] uploadFiles) {
for (MultipartFile uploadFile : uploadFiles) {
//1. 확장자 검사
if (uploadFile.getContentType().startsWith("image") == false) {
log.warn("this is not image type");
return;
}
//실제 파일 이름이 전체 경로가 들어오기 때문에 원본 이름 처리와 실제 파일 이름 처리
String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
log.info("filename: " + fileName);
//2. 동일한 폴더에 많은 파일 방지
//(1) 날짜 폴더 생성
String folderPath = makeFolder();
//(2) UUID 클래스를 이용한 고유한 파일 이름 생성
String uuid = UUID.randomUUID().toString();
//(3) UUID를 이용해 저장 파일 이름 중간에 "_"를 이용해 구분
String saveName = uploadPath + File.separator + folderPath + File.separator + uuid + "_" + fileName;
Path savePath = Paths.get(saveName);
try {
uploadFile.transferTo(savePath);
} catch (IOException e) {
e.printStackTrace();
}
}//end for
}
//(3) 폴더 생성 메서드 작성
private String makeFolder(){
String str= LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String folderPath = str.replace("/", File.separator);
//경로에 폴더가 없을 경우 폴더 생성
File uploadPathFolder =new File(uploadPath, folderPath);
if(uploadPathFolder.exists()==false){
uploadPathFolder.mkdirs();
}
return folderPath;
}
변경 후, uploadFile
package com.movie.boot4.controller;
import com.movie.boot4.dto.UploadResultDTO;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
//파일 업로드를 위한 클래스
@RestController
@Log4j2
public class UploadController {
//업로드된 파일 저장 결로 설정
@Value("${com.movie.upload.path}") //애플리케이션 설정 변수
private String uploadPath;
//MultipartFile 타입을 이용한 파일 사용
//: Ajax를 이용한 파일 업로드 처리 -> 업로드 결과에 대한 화면 작성 X
//따라서 업로드 결과는 JSON 형태로 제공
@PostMapping("/uploadAjax")
//ResponseEntity는 HttpHeader와 HttpBody를 포함하는 HttpEntity를 상속받아 구현한 클래스
//즉, 사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스로 HttpStatus, HttpHeaders, HttpBody를 포함
public ResponseEntity<List<UploadResultDTO>> uploadFile(MultipartFile[] uploadFiles) {
List<UploadResultDTO> resultDTOList = new ArrayList<>();
for (MultipartFile uploadFile : uploadFiles) {
//1. 확장자 검사
if (uploadFile.getContentType().startsWith("image") == false) {
log.warn("this is not image type");
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
//이미지가 아닐경우 HttpRequest에 포함하는 HttpStatus 403을 포함해 반환
}
//실제 파일 이름이 전체 경로가 들어오기 때문에 원본 이름 처리와 실제 파일 이름 처리
String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
log.info("filename: " + fileName);
//2. 동일한 폴더에 많은 파일 방지
//(1) 날짜 폴더 생성
String folderPath = makeFolder();
//(2) UUID 클래스를 이용한 고유한 파일 이름 생성
String uuid = UUID.randomUUID().toString();
//(3) UUID를 이용해 저장 파일 이름 중간에 "_"를 이용해 구분
String saveName = uploadPath + File.separator + folderPath + File.separator + uuid + "_" + fileName;
Path savePath = Paths.get(saveName);
try {
uploadFile.transferTo(savePath); //실제 이미지 저장부
//이미지 저장 후, DTOList에 추가한다.
resultDTOList.add(new UploadResultDTO(fileName, uuid, folderPath));
} catch (IOException e) {
e.printStackTrace();
}
}//end for
return new ResponseEntity<>(resultDTOList, HttpStatus.OK);
//성공할 경우엔 HttpStatus OK를 담아 resultDTOList를 반환한다.
}
//(3) 폴더 생성 메서드 작성
private String makeFolder(){
String str= LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String folderPath = str.replace("/", File.separator);
//경로에 폴더가 없을 경우 폴더 생성
File uploadPathFolder =new File(uploadPath, folderPath);
if(uploadPathFolder.exists()==false){
uploadPathFolder.mkdirs();
}
return folderPath;
}
}
: 브라우저에서는 업로드 처리 후에 JSON의 배열 형태로 결과를 전달받는다.
(2) 업로드 이미지 출력
JSON으로 반환된 업로드 결과를 화면에 출력 조건
- 브라우저에서 링크를 통해 <img> 태그를 추가
- 서버에서 해당 URL이 호출되는 경우, 이미지 파일 데이터를 브라우저로 전송
: 컨트롤러에서 '/display?fileName=xxxx'와 같은 URL 호출 시에 이미지 전송되도록 메서드 추가
URL 호출 시, 컨트롤러에서 이미지 전송되도록 getFile 메서드 추가
//URL로 이미지 전송을 위한 메서드
@GetMapping("/display")
public ResponseEntity<byte[]> getFile(String fileName){
ResponseEntity<byte[]> result = null;
try{
String srcFileName = URLDecoder.decode(fileName, "UTF-8");
log.info("fileName : "+ srcFileName);
File file = new File(uploadPath+File.separator+srcFileName);
log.info("file : " file);
HttpHeaders header = new HttpHeaders();
//MIME타입 처리
//: 마임 타입이란 클라이언트에게 전송된 문서의 다양성을 알려주기 위한 메커니즘 -> 올바른 마임타입 전송하도록 설정필요
header.add("Content-Type", Files.probeContentType(file.toPath()));
//: 확장자를 통해 마임타입을 판단하는데, 확장자가 없으면 null을 반환한다.
//파일 데이터 처리
result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK);
//ResponseEntity에 바이트 배열로 만든 파일, 헤더, 상태코드 전달
}
catch (Exception e){
log.error(e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return result;
}
: URL 인코딩된 파일 이름을 파라미터로 받아서 해당 파일을 byte[]로 만들어 브라우저로 전송
getFile() 작성 시, 고려사항
(1) 파일의 확장자에 따라서 브라우저에 전송하는 마임타입이 달라지는 문제
: 해당 마임타입은 probeContentType()을 이용해 처리
(2) 파일 데이터를 처리하는 문제
: 스프링 제공 메서드인 FileCopyUtils 메서드를 이용
(3) DTO의 getImageURL을 사용해야하는 업로드 결과의 imageURL 속성
(3) 실제 화면 구성
: uploadEx에 업로드된 이미지를 보여줄 수 있는 <div>를 추가해, imageURL로 <img> 태그 작성
[imageURL은 URL 인코딩된 파일 경로와 UUID가 결합된 정보]
uploadEx
<!--imageURL을 이용한 <img>태그 작성하는데, 업로드된 이미지를 보여줄 수 있는 div-->
<input name="uploadFiles" type="file" multiple>
<button class="uploadBtn">Upload</button>
<div class="uploadResult">
</div>
Ajax 업로드 이후 이미지 호출하는 showUploadedImages() 함수
//Ajax 업로드 이후 이미지 호출하는 showUploadedImages() 함수 작성
//: Ajax 호출 성공시, '/display?fileName=xxx' 호출
function showUploadedImages(arr){
console.log(arr);
var divArea = $(".uploadResult");
for(var i = 0; i < arr.length; i++){
divArea.append("<img src='/display?fileName="+arr[i].imageURL+"'>");
}
}
Ajax 호출부
: 성공시, showUploadedImages() 호출
$.ajax({
url: '/uploadAjax',
processData: false,
contentType: false,
data: formData,
type: 'POST',
dataType:'json',
success: function(result){
console.log(result);
//나중에 화면 처리
showUploadedImages(result);
},
error: function(jqXHR, textStatus, errorThrown){
console.log(textStatus);
}
}); //$.ajax
}); //end click
: 브라우저 처리 완료 후에는
화면에서 파일 선택하고 업로드 이후, 다시 브라우저 통해 업로드된 파일 조회 가능
파일 업로드 순서
1. Ajax를 통한 업로드 처리를 하는데, JSON으로 결과 데이터 전송하기 위한 RestController 이용
2. 따라서, ResponseEntity<List<UploadResultDTO>>로 반환타입을 변경하고, DTO에서 JSON 데이터로 업로드 결과를 반환한다.
return URLEncoder.encode(folderPath+"/"+uuid+"_"+fileName, "UTF-8");
3. 컨트롤러에서는 파일을 오류 없이 저장하기 위해 고려사항이 있는데
(1) 이미지 파일인지 확장자 검사
(2) 동일한 폴더에 많은 파일 방지하기 위해 년/월/일로 저장 폴더 생성
(3) 동일한 이름의 파일 업로드시 덮어쓰는 문제를 해결하기 위해 UUID 결합
4. imageURL을 이용하므로, URL 인코딩된 파일 경로와 UUID를 결합한 정보로 이를 이용해 태그를 작성한다.
5. 또한, Ajax를 이용해 업로드 처리를 하는데, 업로드한 이미지를 보여주는 화면 처리를 위한 함수도 작성한다.
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
[Spring 부트 - 영화 리뷰 프로젝트] 3. 영화 등록 처리 (0) | 2022.10.18 |
---|---|
[Spring 부트 - 영화 리뷰 프로젝트] 2. 파일 업로드 처리 (2) 섬네일 이미지를 통한 화면 처리와 파일 삭제 (0) | 2022.10.18 |
[Spring 부트 - 영화 리뷰 프로젝트] 1. M:N (다대다) 관계 설계와 구현 [+ N+1 문제와 엔티티의 특정 속성 로딩 방법] (0) | 2022.10.17 |
[Spring 부트 - 댓글 프로젝트] 3-4. 댓글 비동기처리를 위한 @RestController와 JSON 처리 (1) | 2022.10.04 |
[Spring 부트 - 댓글 프로젝트] 3-2. 게시물과 댓글, 컨트롤러와 화면 처리 [자바스크립트] (1) | 2022.10.03 |