12주차(S3 업로드 완료, Sesstion Storage에 저장하기)
이번주는 낮밤이 바뀐 주인 것 같다. 특히 주말..
주말에 코드 작성하다가 시간이 금방가는데 9시에 출석체크만 아니면 맘놓고 밤샐 것 같긴 하다..
S3 업로드 완료
3일 동안 S3 업로드에 매진했지만
Servlet.service() for servlet [dispatcherServlet] in context with path [] ~
오류에 막혀 결국 다른 팀분들의 도움을 받아 해결했다.
코드의 전체적인 부분은 고칠게 자잘자잘하게 있고 크게는 아래 부분을 수정했어야 했다.
1. 버킷 정책 편집하기(AWS Policy Generator)
2. ACL 모든 사람(퍼블릭 액세스) 권한 허용해주기
3. accessKey:
secretKey:
* 키 입력할 때 띄어쓰기 하기..!!
ex) accessKey:dkajflsdf ❌
secretKey: skdjflsdkjf ⭕
그리고 몇번의 실행을 하면서 코드를 살짝 씩 수정했는데
어떤 이미지는 s3에 저장되고 어떤 이미지는 로컬에만 저장되었다.
용량차이인가 했는데 더 큰 용량도 s3에 업로드 되었다..
그래서 파일명을 바꿔봤는데 업로드가 되었다 ...????
코드에 System.out.println으로 S3Uploader.java에 하나씩 출력시켜서 코드의 흐름을 파악했다.
S3Uploader.java에 있는 코드가 로컬에 이미지를 저장하고 s3에 이미지를 업로드 시키고
로컬에 있는 이미지는 삭제하는 형태로 파악을 했는데
s3 이미지 업로드가 실패하면 로컬에만 저장되었기 때문에 이미 로컬에 있는 파일이라서 업로드가 안된 것이었다.
s3 이미지 업로드가 실패했었던 이유는 위의 3번에 해당한다.
accessKey와 secretKey 입력할 때 한 칸 띄어쓰지 않고 입력해서 한 줄로 인식했다.
그래서 로컬에만 저장하게 된 것이다.
그래서 로컬에 저장된 파일을 삭제하고 다시 재 업로드 했더니 업로드가 되었다..!!!
파일 명에 따라서 저장되는게 있고 없는게 있는건가 싶었는데 그게 아니었다. 😅
S3Uploader.java
package shop.dangdang.service;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@RequiredArgsConstructor
@Component
public class S3Uploader { // 로컬에 이미지를 저장하고 s3에 업로드 시키고 로컬에 있는 이미지는 삭제
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
public String bucket; // S3 버킷 이름
// 파일 형식으로 로컬에 저장
public String upload(MultipartFile multipartFile, String dirName) throws IOException {
File uploadFile = convert(multipartFile) // 파일 변환할 수 없으면 에러
.orElseThrow(() -> new IllegalArgumentException("error: MultipartFile -> File convert fail"));
return upload(uploadFile, dirName);
}
// S3로 파일 업로드하기(업로드 과정)
private String upload(File uploadFile, String dirName) {
String fileName = dirName + "/" + UUID.randomUUID() + uploadFile.getName(); // S3에 저장된 파일 이름
String uploadImageUrl = putS3(uploadFile, fileName); // s3로 업로드
removeNewFile(uploadFile);
return uploadImageUrl;
}
// S3로 업로드
private String putS3(File uploadFile, String fileName) {
amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile).withCannedAcl(CannedAccessControlList.PublicRead));
return amazonS3Client.getUrl(bucket, fileName).toString();
}
// 로컬에 저장된 이미지 지우기
private void removeNewFile(File targetFile) {
if (targetFile.delete()) {
log.info("File delete success");
return;
}
log.info("File delete fail");
}
// 로컬에 파일 업로드 하기 (user dir : C:\Users\lulu\Desktop\develop\backend\imageupload )
private Optional<File> convert(MultipartFile file) throws IOException {
File convertFile = new File(System.getProperty("user.dir") + "/" + file.getOriginalFilename());
if (convertFile.createNewFile()) { // 바로 위에서 지정한 경로에 File이 생성됨 (경로가 잘못되었다면 생성 불가능)
try (FileOutputStream fos = new FileOutputStream(convertFile)) { // FileOutputStream 데이터를 파일에 바이트 스트림으로 저장하기 위함
fos.write(file.getBytes());
}
return Optional.of(convertFile);
}
return Optional.empty();
}
}
Spring 공부의 재미😆😆
S3에 이미지 파일을 저장하고 db에 url이 h2-console에 저장되는걸 확인했다.
이 이미지 url 주소를 브라우저에 입력하면 해당 이미지 파일이 뜨는 것을 볼 수 있다.
이제 이 이미지 url을 가져와서 띄워보는 것을 하려고 했다.
경중뉨이랑 code with me로 같이 진행했는데 이 과정에서 모르고 쓰던 spring 코드를 어느정도 알게 되면서
spring이 재미있어졌다. 뭔가 새로 알게 되는건 언제나 신기하고 재밌다.😆😆😆😆
경중뉨은 어떻게 다 아시고 설명도 잘 하실까..😲 나도 누군가에게 이렇게 잘 알려주고 싶다...🥲
아래는 내가 터득한 지식들을 적어봤다.
Sesstion Storage에 저장하기
로그인을 구현하지 않아서 값을 가져올 수 있는 방법이 idx를 이용해서 url을 가져오는 방법말고는 보이지 않았다.
그래서 idx을 이용하여 url을 가져오고 화면에 띄우는 코드를 작성하기로 했다.
sessionStorage.setItem("image_idx", response['idx']);
console.log로 입력해서 브라우저 console창에 띄울 수도 있지만
sesstion storage를 이용해서 임시 저장소를 만들고 브라우저 창을 닫기 전까지 존재하는 저장소를 이용해
idx를 image_idx라는 변수로 임시 저장했다.
참고로 Local Storage는 브라우저를 닫아도 저장이 되는 곳이라고 보면 된다.
그래서 보안이 중요한 것들은 Session Storage에 넣어 임시 저장을 한다고 한다.
Session Storage에 저장을 했으면 가져와야 한다.
let test = sessionStorage.getItem("image_idx");
위 코드를 이용해서 idx값을 받아오면 Controller에 보내준다.
return 값
UploadController.java
@GetMapping("/test") // ajax에서 보낸 값은 다 써주기
public Upload doTest(@RequestParam Long idx) {
return uploadService.doTest(idx);
}
Controller와 Service는 return값을 왜 써줘야 할까?
UploadService.java
public Upload doTest(Long idx) {
Upload test = uploadRepository.findById(idx).orElseThrow(
() -> new NullPointerException("해당 게시글 없음")
);
return test;
}
Service의 return 값이 ajax의 response이다.
why? Controller의 retrurn 값이 Service 코드 내의 내용과 같은 것이기 때문이다.
위 2개 코드는 아래처럼 나타낼 수 있다.
UploadController.java
@GetMapping("/test") // ajax에서 보낸 값은 다 써주기
public Upload doTest(@RequestParam Long idx) {
Upload test = uploadRepository.findById(idx).orElseThrow(
() -> new NullPointerException("해당 게시글 없음")
);
return test;
}
Controller와 Service는 코드 분리를 위해 따로 작성한 것이다.
다시 UploadService.java를 살펴본다.
public Upload doTest(Long idx) {
Upload test = uploadRepository.findById(idx).orElseThrow(
() -> new NullPointerException("해당 게시글 없음")
);
return test;
}
Repository에서 idx값을 찾아 idx값에 해당하는 데이터를 return값에 test 변수로 넣어 보냈다.
이것을 ajax에서 response로 받고
response로 받은 데이터 중 image만 html에 띄우면 해당 사진만 뜨는 것을 확인할 수 있다. 😲
🙋🏻♀️ 여기서 궁금한 점!
왜 Service에서 Repository를 가져와서 바로 보내줬을까? 어디서는 Repository에서 가져오던데??
💡 Repository에서 가져오는 데이터는 커스터마이징하는 것이다.
방금 db에서 가져올 때 지정한 idx값은 primary key로 고유한 값이다.
그 외에 작성한 글을 이용해 가져오고 싶을 때는 Repository에 작성한다.
public interface UploadRepository extends JpaRepository<Upload, Long> {
Upload findByContent(String content);
}
이렇게 쓰려면 ajax에서 데이터를 보낼 때 그 글에 해당하는 값을 content 변수로 저장해 data에 보낼 것이다.
그렇게 되면 Controller와 Service에 코드도 달라진다.
@GetMapping("/test") // ajax에서 보낸 값은 다 써주기
public Upload doTest(@RequestParam Long idx, @RequestParam String content) {
return uploadService.doTest(idx);
}
이렇게 @RequestParam 값이 많아지면 @ModelAttribute를 쓴다.
@RequestParam이 여러개 있다. → @ModelAttribute
* 아래는 예시코드이다.
@PostMapping("/api/upload") //@RequestParam이 여러개 있다. -> @ModelAttribute
public Upload setUpload(@ModelAttribute UploadDto uploadDto) throws IOException{
System.out.println(uploadDto.getImage().getOriginalFilename());
return uploadService.setUpload(uploadDto);
}
전체 코드 보면서 흐름 파악하기
dang.html에서 사진 업로드 버튼을 클릭하면 saveArticle이 실행된다.
function saveArticle() {
let form_data = new FormData()
form_data.append("content", $("#writerArticle").val())
form_data.append("image", $("#product_image")[0].files[0])
$.ajax({
type: "POST",
url: "/api/upload",
data: form_data,
contentType: false,
processData: false,
success: function (response) {
alert("글 저장 성공!!");
sessionStorage.setItem("image_idx", response['idx']);
location.href = "/"; // 페이지 변환
}
});
}
글 저장 성공 alert 창이 뜨면 index.html로 변환된다.
index.html로 변환이 되면 test()가 실행한다.
function test() {
let test = sessionStorage.getItem("image_idx");
$.ajax({
type: "GET",
url: `/test?idx=${test}`,
data: {},
success: function (response) {
let tt = response['image'];
let temp_html = `<img src="${tt}">`;
$('#abc').append(temp_html);
}
});
}
$(document).ready(function () {
test();
}
UploadController.java에서 id값을 받는다.
// 테스트
@GetMapping("/test") // ajax에서 보낸 값은 다 써주기
public Upload doTest(@RequestParam Long idx) {
return uploadService.doTest(idx);
}
UploadService.java에서 Repository에 해당 idx를 가져오고 return 값으로 test 변수를 보내준다.
// 테스트
public Upload doTest(Long idx) {
Upload test = uploadRepository.findById(idx).orElseThrow(
() -> new NullPointerException("해당 게시글 없음")
);
return test;
}
response로 받아서 image값을 tt 변수에 저장한다.
function test() {
let test = sessionStorage.getItem("image_idx");
$.ajax({
type: "GET",
url: `/test?idx=${test}`,
data: {},
success: function (response) {
let tt = response['image'];
let temp_html = `<img src="${tt}">`;
$('#abc').append(temp_html);
}
});
}
리스트 형태로 이미지 모두 가져오기
db에서 모두 가져오는 것이기 때문에 아래 코드는 주석처리 했다.
sessionStorage.setItem("image_idx", response['idx']);
function saveArticle() {
let form_data = new FormData()
form_data.append("content", $("#writerArticle").val())
form_data.append("image", $("#product_image")[0].files[0])
$.ajax({
type: "POST",
url: "/api/upload",
data: form_data,
contentType: false,
processData: false,
success: function (response) {
alert("글 저장 성공!!");
// sessionStorage.setItem("image_idx", response['idx']);
location.href = "/"; // 페이지 변환
}
});
}
연결 방법은 위와 동일하다.
UploadController.java
// 테스트2
@GetMapping("/test2")
public List<Upload> doTest2() {
return uploadService.doTest2();
}
UploadService.java
// 테스트2
public List<Upload> doTest2() {
List<Upload> allTest = uploadRepository.findAll();
return allTest;
}
index.html
function test2() {
$.ajax({
type: "GET",
url: `/test2`,
data: {},
success: function (response) {
for (let i = 0; i < response.length; i++) {
let idx = response[i]['idx'];
let content = response[i]['content'];
let image = response[i]['image'];
let date = response[i]['createdAt'];
let temp_html = `<img src="${image}" width="300" height="200">
<h3>${idx}</h3>
<h3>${content}</h3>
<h3>${date}</h3>
<hr>`;
$('#abc').append(temp_html);
}
}
});
}
$(document).ready(function () {
// test();
test2();
});