코드 훔쳐보는 변태 코더
춤 좋아하는 백엔드 개발자(였으면 좋겠다)
TOASTUI (2)
스프링부트 Toast UI 에디터 / 뷰어 적용 + 이미지 입출력 -2

https://codinghentai.tistory.com/2

 

스프링부트 Toast UI 에디터 / 뷰어 적용 + 이미지 입출력 -1

참고한 게시글 : https://velog.io/@io_/Toast-UI-Editor-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C Toast UI Editor 이미지 업로드 Spring Boots를 사용해 Toast UI Editor에 이미지 업로드하기!!!! 어느날 미래에서 삽질

codinghentai.tistory.com

해당 글과 이어집니다.

 


저번 글에서 비동기 통신으로 즉각적으로 이미지를 업로드/붙여 넣기를 하면 이미지를 서버에 업로드 후 db에 저장, 그리고 출력까지 해오는 로직을 구현했었는데요,

 

이제부터는 게시글과 파일의 연관관계를 디테일하게 설정해 잉여파일까지 관리할 수 있는 로직을 구현해봅니다.

 

게시글 등록시에 중간 관계 엔티티 영속해주기

어떠한 기능을 구현하고자 할 때 가장 1차원적으로 생각해보는 게 큰 도움이 되는 것 같습니다.

지금 프로젝트의 구조는 게시글을 등록할때 리퀘스트 dto를 전달하는 방식으로 글을 등록했기 때문에,

 

단순히 파일 업로드 후 리스폰스 엔티티로 객체를 반환해준다면, 게시글 등록시에 리퀘스트 dto에 반환된 객체의 id값을 같이 받아오면 되겠구나! 하고 생각했습니다.

 

                    success: function(data) {
                        //console.log('ajax 이미지 업로드 성공');
                        url += data.fileName;
                        fileIds += data.id + ",";
                        // callback : 에디터(마크다운 편집기)에 표시할 텍스트, 뷰어에는 imageUrl 주소에 저장된 사진으로 나옴
                        // 형식 : ![대체 텍스트](주소)
                        callback(url, '사진 대체 텍스트 입력');
                    },

이렇게 에디터의 success 부분에 받아온 객체의 id를 문자열 변수에 append 하는 형태로 구현후에 같이 전달해줍니다.

 

그리고 비즈니스 로직을 수정해줄 건데요, ArticleSaveFile 엔티티 클래스는 게시글과 파일의 연관관계를 위해서 생성했기 때문에 repository는 게시글과 파일의 서비스 클래스에 둘 다 주입해줄 겁니다.

 

 

그전에 SaveFile 서비스에서 비즈니스 로직을 구현해주어야 합니다. 받아온 fileIds로 file set을 반환해주는 로직을 구현합니다.

 

    public Set<SaveFile.SaveFileDto> getFileDtosFromRequestsFileIds(Article.ArticleRequest dto) {
        StringTokenizer st = new StringTokenizer(dto.getFileIds(), ",");
        Set<SaveFile.SaveFileDto> saveFileDtos = new HashSet<>();
        while(st.hasMoreTokens()){
            Long fileId = Long.valueOf(st.nextToken());
            log.info("fileId: {}", fileId);
            saveFileDtos.add(saveFileRepository.findById(fileId).map(SaveFile.SaveFileDto::from).orElseThrow(() -> new EntityNotFoundException("파일이 없습니다 - fileId: " + fileId)));
        }
        return saveFileDtos;
    }

이렇게 HashSet을 생성해 StringTokenizer로 잘라서 dto로 가져와서 set에 담아 반환해줍니다.

 

이제 게시글 컨트롤러에서 리퀘스트 Dto 에 담긴 파일 아이디로 파일 dto를 가져와 같이 비즈니스 로직으로 넘겨줍니다.

 

    @ResponseBody
    @PostMapping
    public ResponseEntity<String> saveArticle(@RequestBody @Valid Article.ArticleRequest dto, BindingResult bindingResult,
                                              @AuthenticationPrincipal UserAccount.BoardPrincipal boardPrincipal) {
        if (bindingResult.hasErrors()) {
            return new ResponseEntity<>(ErrorMessages.ENTITY_NOT_VALID, HttpStatus.BAD_REQUEST);
        }
        if (boardPrincipal == null) {
            return new ResponseEntity<>(ErrorMessages.ACCESS_TOKEN_NOT_FOUND, HttpStatus.BAD_REQUEST);
        }
        Set<SaveFile.SaveFileDto> saveFileDtos = saveFileService.getFileDtosFromRequestsFileIds(dto);
        saveFileService.deleteUnuploadedFilesFromArticleContent(dto.getContent(), dto.getFileIds());
        return new ResponseEntity<>(articleService.saveArticle(dto.toDto(boardPrincipal.toDto()), Hashtag.HashtagDto.from(dto.getHashtag()), saveFileDtos).id().toString(), HttpStatus.OK);
    }
    public Article.ArticleDto saveArticle(Article.ArticleDto dto, List<Hashtag.HashtagDto> hashtagDtos, Set<SaveFile.SaveFileDto> saveFileDtos) {
        Article article =articleRepository.save(dto.toEntity());
        for (Hashtag.HashtagDto hashtag : hashtagDtos) {
                Hashtag hashtag1= hashtagRepository.findByHashtag(hashtag.hashtag()).orElseGet(()-> hashtagRepository.save(hashtag.toEntity()));
                articlehashtagrepository.save(ArticleHashtag.of(article,hashtag1));
        }
        for (SaveFile.SaveFileDto saveFileDto : saveFileDtos) {
            if(dto.content().contains(saveFileDto.fileName())){
                articleSaveFileRepository.save(ArticleSaveFile.of(article, saveFileDto.toEntity()));
            }
        }
        return Article.ArticleDto.from(article);
    }

저는 해시태그도 양방향 관계를 중간 엔티티로 맺어주어서 등록하도록 했는데요, 파일도 마찬가지로 똑같은 로직으로 구현했습니다.

 

이렇게 구현하면 게시글에 이미지가 첨부되었을 때 중간 테이블에서 외래 키를 참조하여 데이터를 생성하게 됩니다.

 

중간 엔티티에 맵핑이 된 모습(1에 2,3 이렇게 생성되는게 아닙니다 :()

 

하지만 게시글을 등록할 시점에 게시글 내용에 이미지가 존재하지 않는다면?(지워진 상태라면?)

위에 코드에서 각각의 파일 이름이 게시글의 내용에 포함되지 않으면 맵핑을 하지 않는 방향으로 구현할 수 있는데요,

 

그렇게 해버리면 단순히 파일만 업로드되고 서버상에 잔여 파일이 남고 DB에도 유령 데이터가 존재하게 됩니다.

 

저는 그래서 파일 엔티티를 관리하는 비즈니스 로직에 게시글의 콘텐츠와 각 파일의 이름을 비교해 삭제해버리는 로직을 구현해보았습니다.

 -> 해당 로직을 응용해서 게시글 내에 '#'를 포함해 작성한다면 해시태그를 자동으로 등록하도록 하는 로직도 구현할 수 있지 않을까? 하는 생각도 들었습니다. 🤔

 

    public void deleteUnuploadedFilesFromArticleContent(String content,String fileIds){
        if(Objects.isNull(content) || Objects.isNull(fileIds) || fileIds.equals("")){
            return;
        }
        for(String fileId : fileIds.split(",")){
            if(!content.contains(saveFileRepository.getReferenceById(Long.parseLong(fileId)).getFileName())){
                deleteFile(Long.parseLong(fileId));
            }
        }
    }

이렇게 파일 서비스에 비즈니스 로직을 구현해주었는데요, delete를 담당하는 로직 자체에서 서버에 남아있는 파일까지 삭제해버리기 때문에 글 내용과 파일 아이디를 전달하면 작업할 수 있도록 구현해주었습니다.

 

이렇게 구현해주면 글을 등록할 시에 글 내용에 이미지 파일이 존재하지 않는다면 서버에 잔여 파일도 존재하지 않게 되고 쓸데없는 데이터를 입력하지도 않게 됩니다 🥸

 

시연 화면

 

이렇게 에디터에 이미지를 붙여 넣기 해주고 삭제 후 업로드해봅니다.

 

2번 아이디로 게시글이 등록이 되었다.
이미지를 내용에 포함시키지 않자 연관관계도 맺어지지 않는다!
그리고 마찬가지로 파일 테이블에도 데이터가 삽입되지 않았다.

이렇게 포함을 시키지 않아도 잔여 이미지가 남지 않게 됩니다. 

 

수정과 삭제의 경우엔 어떻게 대처를 할 수 있을까?

 

수정 같은 경우에는 글에 이미지가 포함되지 않았을 경우에 삭제해버리는 로직을 비슷하게 구현하면 됩니다.

다만 게시글을 등록할 때는 단순 즉각적으로 전달받은 업로드된 이미지의 id를 참조하여 삭제하면 됐지만

 

수정 같은 경우에는 파일 엔티티나 게시글 엔티티 자체가 각각 엔티티 자체에 서로의 엔티티를 참조하지 않고 중간 테이블이 존재하는 구조이기 때문에

 

수정 페이지 뷰에 기존에 게시글에 등록돼있던 fileId를 addattribute로 전달하고, 이미지를 업로드할 때마다 append 하는 방식으로 id를 받아와 마찬가지로 게시글에 포함이 안된 이미지는 삭제 후에 수정하는 방식으로 구현했습니다.

 

    public static String fileIdsToString(Set<SaveFile.SaveFileDto> saveFiles){
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < saveFiles.size(); i++) {
            sb.append(saveFiles.stream().toList().get(i).id()).append(",");
        }
        return  sb.toString();
    }

저는 이렇게 정적 유틸 메서드를 컨트롤러 유틸 클래스에 선언하고 호출했습니다.  

 

    @ResponseBody
    @PutMapping
    public ResponseEntity<?> updateArticle(
            @RequestBody @Valid Article.ArticleRequest articleRequest,
            BindingResult bindingResult,
            @AuthenticationPrincipal UserAccount.BoardPrincipal boardPrincipal) {
        try {
            if (bindingResult.hasErrors()) {
                return new ResponseEntity<>(ControllerUtil.getErrors(bindingResult), HttpStatus.BAD_REQUEST);
            }
            if (!Objects.equals(articleService.getWriterFromArticle(articleRequest.getArticleId()), boardPrincipal.username())) {
                return new ResponseEntity<>(ErrorMessages.ACCOUNT_NOT_MATCH, HttpStatus.BAD_REQUEST);
            }
            Set<SaveFile.SaveFileDto> saveFileDtos = saveFileService.getFileDtosFromRequestsFileIds(articleRequest);
            saveFileService.deleteUnuploadedFilesFromArticleContent(articleRequest.getContent(), Objects.requireNonNull(articleRequest.getFileIds()));
            articleService.updateArticle(articleRequest.getArticleId(), articleRequest, Hashtag.HashtagDto.from(articleRequest.getHashtag()), saveFileDtos);
            return new ResponseEntity<>("articleUpdating Success", HttpStatus.OK);
        }
        catch (EntityNotFoundException e){
            return new ResponseEntity<>(ErrorMessages.ENTITY_NOT_FOUND, HttpStatus.NOT_FOUND);
        }
    }

저는 이렇게 현재 등록된 이미지와 새로 등록된 이미지 id를 받아와서 내용에 포함되어있지 않으면 파일을 삭제하고 수정하는 방식으로 구현해 잔여 파일을 처리해주었습니다 💆🏻‍♂️

 

삭제 같은 경우에도 마찬가지로 중간 관계 엔티티에서 id를 가져온후 중간관계 엔티티를 삭제하고 파일 엔티티를 게시글 엔티티 id를 토대로 불러와 삭제를 진행하고 게시글 엔티티를 삭제하도록 구현했습니다 :D

 


마치며

  • 해당 기능을 구현하면서 처음으로 성능에 대한 고민을 할 수 있었습니다. (반복적인 쿼리 실행을 줄이기 위해 노력했습니다.)
  • 해당 기능을 구현하면서 가장 많이 했던 고민은 역시나 역할 분담이었습니다. 파일 엔티티는 어떻게 보면 지금 프로젝트상 단독으로 처리되지 못하고 참조가 되는 구조였기 때문에 고민을 하게 되었습니다.
  • 이번 기능 구현도 기본적인 에디터 옵션 사용을 제외한 나머지는 제가 직접 구현하게 되었습니다. 이제는 전체적인 프로젝트의 흐름을 어느 정도 머릿속으로 그릴 수 있게 된 것 같아서 기쁩니다 :D

 

해당 코드는 제 토이 프로젝트에서 작성한 코드들이며 구현 글들은 프로젝트의 문서화를 위해 작성한 글들입니다. 태클은 언제나 환영합니다!

  Comments,     Trackbacks
스프링부트 Toast UI 에디터 / 뷰어 적용 + 이미지 입출력 -1

참고한 게시글 : https://velog.io/@io_/Toast-UI-Editor-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C

 

Toast UI Editor 이미지 업로드

Spring Boots를 사용해 Toast UI Editor에 이미지 업로드하기!!!! 어느날 미래에서 삽질하고 있을 나를 위해...

velog.io

서론

  • 제이쿼리를 이용해 게시판 프로젝트에 Toast UI 에디터 / 뷰어를 적용시킵니다.
  • 이미지 파일 업로드 기능을 구현합니다. (에디터의 hooks 옵션을 통해 이미지를 비동기통신으로 업로드 하고 출력합니다.)
  • JPA로 게시글과 파일 사이의 맵핑용 엔티티 클래스를 생성해 양방향 관계를 맺어줍니다.
  • 게시글 등록시에 글에 첨부되지 않은 이미 서버에 업로드된 이미지 파일을 삭제하고 첨부된 이미지와 게시글을 양방향 관계로 맺어주는 로직을 구현해봅니다.
  • 게시글 수정시에 글에서 지워진 이미지나 마찬가지로 서버에 업로드되었지만 첨부되지 않은 잉여 이미지파일을 삭제해주고 연결관계를 끊어주는 로직을 구현합니다.
  • 게시글 삭제시에 등록되있던 이미지까지 통째로 삭제해주는 로직을 구현합니다.

본론

벨로그 글을 작성할 때 , 이미지를 등록하거나 복사 후 붙여넣기 할때 어떻게 업로드가 되고 첨부가 될까요?

제 깃허브 프로필 사진을 캡처 후 붙여 넣기를 해보겠습니다

이렇게 같은 이미지를 여러 번 붙여 넣기 해도 매번 새로 파일이 업로드되고 바로 게시글에서 조회가 가능한데요,

해당 로직의 흐름은 이런 것 같습니다..

  • 게시글에 이미지를 붙여 넣기 혹은 업로드 시에 비동기 통신으로 컨트롤러로 해당 이미지를 전송
  • 전송된 이미지는 서버에 저장되고, DB에 이미지의 정보 저장 후 해당 파일 이름+@ 랜덤 문자열을 반환
  • 앞단은 전달받은 파일 이름으로 GET method 로 이미지 출력을 요청
  • 뒷단은 이미지 파일을 바이트 배열로 전달해 작성 폼에서 바로 이미지 출력

만약 이미지를 붙여 넣기 혹은 업로드 후에 해당 글에서 다시 이미지를 삭제 후 업로드하거나, 게시글이 삭제되었거나 수정 시에도 이미지가 해당 글에서 지워진다면 남은 잉여 파일은 어떻게 처리할까?🤔

가 해당 글의 핵심이 될 것 같습니다. 🤔

게시판 프로젝트에 Toast UI 에디터/뷰어 적용시키기 💁🏻

기존에 제가 진행하던 토이 프로젝트의 글 작성과 뷰어는 단순히 textarea 태그와 타임리프의 th:text 문법을 이용해서 작성/출력을 했었는데요,

게시판에 에디터를 적용시켜보면 얼마나 깔끔해 보일까? 혹은 이것도 어떻게 보면 open api를 활용하는 것과 비슷하다고 생각하여 적용시켜보게 되었습니다.

저는 프런트를 전혀 알지 못하기 때문에 제이쿼리로 진행했습니다.

    <!-- TOAST UI Editor CDN(JS) -->
    <script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
    <!-- TOAST UI Editor CDN(CSS) -->
    <link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" />

적용을 시킬 작성용 폼과 게시글 출력을 시킬 html 파일의 head에 저것들을 다 넣어줍니다.

    const editor = new toastui.Editor({
        el: document.querySelector('#editor1'),
        initialEditType: 'markdown',
        previewStyle: 'vertical',
        height: '500px',
        initialValue: '내용을 입력해주세요',
        events: {
            change: function () {
                console.log(editor.getMarkdown());
            }
        },
        hooks: {
            addImageBlobHook: (blob, callback) => {
                // blob : Java Script 파일 객체
                //console.log(blob);

                const formData = new FormData();
                formData.append('file', blob);

                let url = '/files/';
                $.ajax({
                    type: 'POST',
                    enctype: 'multipart/form-data',
                    url: '/files/upload',
                    data: formData,
                    dataType: 'json',
                    processData: false,
                    contentType: false,
                    cache: false,
                    timeout: 600000,
                    success: function(data) {
                        //console.log('ajax 이미지 업로드 성공');
                        url += data.fileName;
                        fileIds += data.id + ",";
                        // callback : 에디터(마크다운 편집기)에 표시할 텍스트, 뷰어에는 imageUrl 주소에 저장된 사진으로 나옴
                        // 형식 : ![대체 텍스트](주소)
                        callback(url, '사진 대체 텍스트 입력');
                    },
                    error: function(e) {
                        //console.log('ajax 이미지 업로드 실패');
                        //console.log(e.abort([statusText]));

                        callback('image_load_fail', '사진 대체 텍스트 입력');
                    }
                });
            }
        }
    });

출처 :Toast UI Editor 이미지 업로드 - io_
해당 코드는 io_님 블로그에서 긁어왔습니다.

 

Toast UI Editor 이미지 업로드

Spring Boots를 사용해 Toast UI Editor에 이미지 업로드하기!!!! 어느날 미래에서 삽질하고 있을 나를 위해...

velog.io

에디터로 별다른 코드를 추가하지 않고 이미지를 업로드하거나 붙여 넣기 하면 에디터에 몇천 자+@ 의 base64형식으로 이미지 파일이 입력되는데요, 에디터 자체에 hooks 옵션이 존재합니다.

hooks 옵션은 해당 base64 형식의 이미지를 바로 처리하고 에디터에 리턴해줍니다.

    $(document).ready(function() {
        const viewer = toastui.Editor.factory({
            el: document.querySelector("#articleContent"),
            viewer: true,
            height: "500px",
            initialValue: document.getElementById("content").value,
        });
    }
    );

해당 코드는 게시글을 조회하는 뷰 script 태그에 붙여 넣으면 됩니다.

뷰어를 따로 또 적용시키는 이유는 , 에디터에서 지원하는 기능들이 단순 html 태그로는 출력이 되지 않는 경우가 있기 때문에 뷰어도 적용해주었습니다.

게시판에서 이미지를 업로드하면 불필요한 장문의 글자를 가로 채주는 기능이 구현됐으니 이제 이미지/파일을 업로드하고 받아오는 로직을 구현해야겠죠? 🤔

파일 컨트롤러 / 서비스 생성하기

연관관계 맵핑용 중간 엔티티 클래스 생성하기

기존에 File 엔티티 클래스는 작성이 되어있다는 가정하에, 게시글 엔티티와 파일 엔티티를 연결해줄 중간 엔티티 클래스를 생성해줍니다.

@Entity
@Getter
@NoArgsConstructor
public class ArticleSaveFile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Setter
    @OneToOne
    private Article article;

    @Setter
    @ManyToOne
    private SaveFile saveFile;

    private ArticleSaveFile(Long id, Article article, SaveFile saveFile) {
        this.id = id;
        this.article = article;
        this.saveFile = saveFile;
    }

    public static ArticleSaveFile of (Article article, SaveFile saveFile) {
        return new ArticleSaveFile(null, article, saveFile);
    }
}

해당 맵핑은 한 게시글에 여러 개의 파일이 첨부될 수 있기 때문에, 단순 게시글 엔티티와 파일 엔티티에 연관관계를 설정해주지 않고 중간 엔티티 클래스를 생성해 맵핑을 해주었습니다.

그리고 맵핑 엔티티를 저장할 리포지토리도 따로 생성해주었는데요, 여기서 고민할 수 있는 부분은 두 가지였습니다.

  • 파일 엔티티는 단순히 혼자서 업로드되고 삭제될 수 없는 것 같은데 파일 서비스가 단독적으로 동작할 수 있게 해도 될까?
    • 해당 고민은 파일 엔티티는 파일 저장소를 구현하는 프로젝트가 아닌 이상 단독으로 다뤄질 일이 없다고 생각했기 때문에 파일 CRUD가 필요한 컨트롤러에 주입해주는 것으로 타협했습니다.
      • 그러면 애초에 다른 비즈니스 로직에서 파일 리포지토리를 사용하면 되지 않나? 에 대한 고민은 지금 프로젝트는 서비스 클래스 단위로 트랜잭션이 이뤄지기 때문에 컨트롤러에서 파일 서비스로 파일을 다룰 수 있도록 역할을 분담해주었습니다.
  • 이런 중간 엔티티용 서비스와 컨트롤러도 만들어서 역할을 분리해야 할까?
    • 연관관계 맵핑 엔티티는 마찬가지로 각각의 엔티티가 존재하지 않으면 존재할 이유도 없기 때문에, 각각 비즈니스 로직에 리포지토리를 주입해 사용하도록 했습니다.

파일 엔티티 비즈니스 로직 설계하기

파일을 등록/조회/삭제 처리를 해줄 비즈니스 로직을 설계해줍니다.

@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class SaveFileService {
    private final SaveFileRepository saveFileRepository;
    private final ArticleSaveFileRepository articleSaveFileRepository;

    @Transactional(readOnly = true)
    public SaveFile.SaveFileDto getFile(Long fileId) {
        log.info("getFile() fileId: {}", fileId);
        return saveFileRepository.findById(fileId).map(SaveFile.SaveFileDto::from).orElseThrow(()-> new EntityNotFoundException("파일이 없습니다 - fileId: " + fileId));
    }
    
    @Transactional(readOnly = true)
    public SaveFile.SaveFileDto getFileByFileName(String fileName) {
        log.info("getFile() fileId: {}", fileName);
        return saveFileRepository.findByFileName(fileName).toDto();
    }

    public void deleteFile(Long fileId) {
        log.info("deleteFile() fileId: {}", fileId);
        File file = new File(saveFileRepository.getReferenceById(fileId).getFilePath());
        if (file.exists()) {
            if(file.delete()){
                log.info("파일삭제 성공");
            }
        }
        articleSaveFileRepository.deleteBySaveFileId(fileId);
        saveFileRepository.deleteById(fileId);
    }

    public SaveFile.SaveFileDto saveFile(SaveFile.SaveFileDto saveFile) {
        log.info("saveFile() saveFile: {}", saveFile);
        return SaveFile.SaveFileDto.from(saveFileRepository.save(saveFile.toEntity()));
    }

일단 해당 파일들을 저장/삭제/조회하는 로직만 구현해주었는데요, 파일 업로드 시에 db가 아닌 서버에 저장되는 로직은 유틸 클래스를 생성해서 정적 메서드로 처리하도록 했습니다.

    public static File getMultipartFileToFile(MultipartFile multipartFile) throws IOException {
        File file = new File(uploadPath,getFileNameWithUUID(multipartFile.getOriginalFilename()));
        multipartFile.transferTo(file);
        return file;
    }

이렇게 해당 메서드를 선언하여 서버에 저장하고, 또 따로 File 타입을 (해당 File타입은 엔티티가 아닙니다) File엔티티 dto 로 변환해주는 메소드를 선언해 편의성을 챙겼습니다.

 

여기서 신경 쓸건 파일이 생성되지 않거나 제대로 된 파일을 전달받지 못해서 비어있는 파일 정보를 가지고 db에 저장하는 걸 방지하기 위해 해당 메서드들은 파일 컨트롤러에서 호출하도록 구현했습니다.

 

파일 컨트롤러 생성하기

비즈니스 로직을 구현해주었으니 이제 앞단에서 전달받은 파일을 어떻게 처리할지를 컨트롤러에서 구현해줍니다.

@RestController
@Slf4j
@RequiredArgsConstructor
public class SaveFileController {
    private final SaveFileService saveFileService;

    @PostMapping("/files")
    public ResponseEntity<?> uploadFile(@RequestPart("file")MultipartFile file, @AuthenticationPrincipal UserAccount.BoardPrincipal principal) throws IOException {
        try{
        SaveFile.SaveFileDto fileDto = saveFileService.saveFile(FileUtil.getFileDtoFromMultiPartFile(file,principal!=null ? principal.getUsername() : "anonymous"));
        return new ResponseEntity<>(fileDto, HttpStatus.OK);
        }catch (Exception e){
            log.error("uploadFile() error: {}", e.getMessage());
            return new ResponseEntity<>(ErrorMessages.FILE_UPLOAD_FAIL, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @GetMapping("/files/{fileName}")
    public ResponseEntity<?> getFile(@PathVariable String fileName) throws IOException {
        try{
        File file = FileUtil.getFileFromSaveFile(saveFileService.getFileByFileName(fileName));
        byte[] imgArray = IOUtils.toByteArray(new FileInputStream(file));
        return new ResponseEntity<>(imgArray,HttpStatus.OK);
    }
        catch (EntityNotFoundException e){
            return new ResponseEntity<>(ErrorMessages.NOT_FOUND,HttpStatus.NOT_FOUND);
        }
    }
}

파일이 비어있다면(그럴리는 없지만) IOException 이 발생할 것이기 때문에 해당 예외 처리를 컨트롤러에서 진행해줍니다.

 

하지만 이미지가 없다고 출력이 안되면 안 되기 때문에 catch 문으로 에러 메시지를 반환해줍니다.

 

이미지 업로드 후 db 반영 결과 조회하기

 

이제 이미지를 업로드/붙여 넣기 를 하면 즉시 작성 폼에는 이미지 출력용 url과 대체 텍스트가 입력됩니다.

 

맥북 배경화면을 캡쳐해서 붙여넣기 해보았습니다.

이렇게 이미지를 붙여넣기 하면 그 즉시 POST 메서드로 비동기 통신을 하게 되는데요 , 

 

이렇게 바로 DB에 반영이 됩니다.

 

이제 여기서 파일 컨트롤러와 게시글 컨트롤러는 분리되어있는데 게시글 서비스 클래스에서 연관관계를 맺을 수 있을까?

에 대한 로직 구현이 필요합니다.

 

해당 내용은 글이 길어졌기 때문에 다음 글에서 설명하도록 하겠습니다 :)

  Comments,     Trackbacks