본문 바로가기

AWS
[AWS] S3 이미지 삭제(+ CKEditor5)

https://sorrel012.tistory.com/309

 

Chapter 1 : AWS S3 이미지 업로드(+ CKEditor5)

로컬 서버에 사진을 저장할 경우 다른 곳에서 실행할 때 사진이 안 나온다던가.. 하는 불편함을 해결하기 위해 클라우드에 파일 저장을 시도했다. 개인 프로젝트이기 때문에 과금은 안 될 걸로

sorrel012.tistory.com


이전 글에서 CKEditor 통해 S3에 이미지 업로드 하는 것까지 기록하였다.

글의 마지막에 삭제는 별도로 처리해주어야 한다고 했는데, 생각보다 어렵지 않아 바로 구현할 수 있었다.

먼저 해결하고자 했던 문제에 대해서 자세히 설명하자면, CKEditor 를 통해 이미지를 첨부하는 순간 클라우드에 업로드가 되고, 계속 글을 작성하다가 첨부한 사진을 삭제하면 S3에는 사진이 그대로 남아버리게 된다.

이를 해결하기 위해서 첨부한 사진의 URL을 배열에 넣어두고, 최종적으로 글을 등록할 때 서버로 함께 넘겨서 비교하였다.


class MyUploadAdapter {
  constructor(loader, uploadUrl, component) {
    this.loader = loader;
    this.uploadUrl = uploadUrl;
    this.component = component;
  }

  upload() {
    return new Promise(async (resolve, reject) => {
      const data = new FormData();
      const file = await this.loader.file;
      data.append('upload', file);

      axios.post(this.uploadUrl, data)
          .then(response => {
            this.component.images.push(response.data.url);
            resolve({
              default: response.data.url
            });
          })
          .catch(error => {
            console.error('File upload error:', error);
            reject(error);
          });
    });
  }
}


CKEditor 컴포넌트에 구현한 커스텀 업로드 어댑터이다.

그냥 이미지를 올리기만 할 때는 CKEditor에서 제공해주는 대로 간편하게 쓰면 됐지만, response에서 첨부한 이미지의 url을 받아와야 하기 때문에 어댑터를 별도의 class로 구현해주었다.

mounted() {
    const component = this;
    const uploadUrl = this.$store.state.url + 'treasure/image';

    CustomEditor.create(document.querySelector('#editor'), {})
        .then(editor => {
          component.editor = editor;

          editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
            return new MyUploadAdapter(loader, uploadUrl, component);
          };

          editor.setData(component.initialData);

          editor.model.document.on('change:data', () => {
            component.initialData = editor.getData();
            component.$emit('write', component.initialData);
            component.$emit('images', component.images);
          });

        })
        .catch(error => {
          console.error('There was a problem initializing the editor.', error);
        });
  },


mounted에서 위에서 구현한 어댑터를 사용해 이미지를 저장하도록 하고, 데이터가 변할 때마다 상위 컴포넌트인 글쓰기 컴포넌트에 본문 내용과 함께 image 배열도 보내준다.

const formData = new FormData();
formData.append('content', this.content);
formData.append('title', this.title);
formData.append('id', sessionStorage.getItem('id'));
formData.append('images', JSON.stringify(this.images));


게시글을 저장할 때 원래는 id까지만 전송했는데, images 배열도 json.stringify 사용 후 전송해준다.


이제 서버에서는 images를 받은 후 List 형식으로 변환해야 한다.

JSONArray jsonArray = new JSONArray(images);
List<String> imageList = new ArrayList<>();

if(jsonArray.length() > 0) {
    for(int i = 0; i < jsonArray.length(); i++){
        String imageUrl = jsonArray.getString(i);
        imageList.add(imageUrl);
    }
}


사진을 아예 첨부하지 않았을 수도 있기 때문에 length 조건을 걸어주었다.


//이미지를 업로드 한 적 있는지 확인
if(!imageList.isEmpty()){

    //게시글 이미지 추출
    List<String> contentImages = new ArrayList<>();
    Document doc = Jsoup.parse(writing.getContent());
    Elements imageElements = doc.select("img");

    for (Element imageElement : imageElements) {
        String imageUrl = imageElement.attr("src");
        contentImages.add(imageUrl);
    }

    //업로드 후 삭제한 이미지 추출
    List<String> deletedImg = new ArrayList<>(imageList);
    deletedImg.removeAll(contentImages);

    //AWS S3에서 이미지 삭제
    s3Service.deleteImage(deletedImg);

}


이미지를 업로드 한 적이 있을 경우에만 로직을 실행한다.

먼저, 게시글 안에 포함되어 있는 이미지 URL을 추출해 준다.

CKEditor는 html 형식으로 저장되기 때문에 이미지 url을 추출할 때 jsoup 라이브러리를 활용했다.

문자열 함수로 파싱하려면 할 수는 있지만, 더 편하게 라이브러리 활용!

jsoup 라이브러리 사용하려면 pom.xml에 의존성도 추가해 주어야 한다.

<!--Jsoup-->
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.16.1</version>
</dependency>


* 최신 버전 참고
https://central.sonatype.com/artifact/org.jsoup/jsoup

 

Maven Central: org.jsoup:jsoup

Discover jsoup in the org.jsoup namespace. Explore metadata, contributors, the Maven POM file, and more.

central.sonatype.com


본문에서 이미지 url들만 골라낸 후에는 첨부한 적이 있는 모든 이미지의 url을 담아둔 List(imageList)와 비교한다.

imageList에는 있지만 본문에서 골라낸 이미지 url에는 해당하지 않는 아이들을 찾으면 삭제된 이미지 url들을 얻을 수 있다.


이제 S3FileUploadService (직접적으로 aws와 연계되는 서비스 클래스)에 삭제 메소드를 구현해주면 된다.

for (String img : deletedImg) {

    String decodedUrl = "";
    
    //URL 디코딩
    try {
        decodedUrl = URLDecoder.decode(img, "UTF-8");
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }

    String awsUrl = "https://postcard17.s3.ap-northeast-2.amazonaws.com/";
    img = decodedUrl.substring(awsUrl.length());

    amazonS3.deleteObject(bucket, img);
}


받아온 url들이 인코딩되어 있기 때문에 먼저 디코딩해주고, 디코딩된 url에서 파일명만 추출해준다.

그 파일명을 넘겨주면 바로 삭제 완료~

로직을 생각해 내는 게 좀 오래 걸렸고, 구현 자체는 어렵지 않았다.



사진 두 개를 첨부한 후 다시 하나를 지우고 등록하면?

vue 이미지만 남고 스프링 부트 이미지는 삭제 완료!