summernote 텍스트 에디터를 활용한 게시글 쓰기 서비스(3) 구현 코드

불바다·2023년 8월 29일

KpopGeneration

목록 보기
7/10
post-thumbnail

자바스크립트 코드

	let jsonArray = [];
	//게시글 수정 시 버퍼를 초기화 
    if(postSaveViewDto.id != null ){
        fetch("/api/temp/savedImages?id="+postSaveViewDto.id)
        .then(response => response.json())
        .then(data =>{
            let srcs = data.data;
            console.log(srcs);
            
            if(srcs == null ){
                return;
            }
            srcs.forEach( src =>{
                jsonArray.push(src);
                console.log(jsonArray);
            });
        })
    }
    $(document).ready(function() {

        // summernote 초기화
        $('#summernote').summernote({
        tabsize: 2,
        height: 500,
        lang: "ko-KR",
        callbacks: {
                //사진 업로드 시 작동하는 콜백 함수
                onImageUpload : function(files, editor, welEditable){
                    for (var i = files.length - 1; i >= 0; i--) {
                        uploadSummernoteImageFile(files[i], this);
                        }
                    },
                //사진 제거 시 작동하는 콜백 함수
                onMediaDelete : function(target) {
                    if(postSaveViewDto.id == null){ // 포스트를 새로 작성하는 경우!
                        deleteFile(target[0].src);
                    }else{
                        deleteFileTemp(target[0].src);
                    } 
                }      
            }
        });
        //summernote 텍스트 부분에 기존에 저장한 값을 불러온다.
         if(postSaveViewDto.body != null){
            $('#summernote').summernote('pasteHTML', postSaveViewDto.body);
        }
		//이미지 업로드 시 동작하는 콜백함수
        function uploadSummernoteImageFile(file, summernote) {
            var data = new FormData();	
            data.append("file",file);
            $.ajax({
                url: '/api/temp/upload',
                type: "POST",
                enctype: 'multipart/form-data',
                data: data,
                cache: false,
                contentType : false,
                processData : false,
                success : function(json) {
                        $(summernote).summernote('editor.insertImage', json.data);
                        jsonArray.push(json.data);
                    },
                    error : function(e) {
                        console.log(e);
                    }
            });
        };
		
      	// 게시글 수정 시 - 이미지 제거 시 동작하는 함수
        function deleteFileTemp(origin){
            let index = origin.lastIndexOf("/images/");
            let fileName = origin.substring(index);
            jsonArray = jsonArray.filter( (src)=> src !== fileName);
        };
      
		// 게시글 최초 생성 시 - 이미지 제거 시 동작하는 함수
        function deleteFile(src) {
            $.ajax({
                data: JSON.stringify({src : src}),
                type: "POST",
                url: "/api/temp/delete", // replace with your url
                contentType: 'application/json',
                cache: false,
                success: function(json) {
                    jsonArray = jsonArray.filter( (src)=>{ src !== json.data});
                }
            });
        };
    });

deletFileTemp는 게시글 수정 작업 시 발동하는 콜백 함수이며, deleteFile은 게시글 최초 작성 시 발동하는 콜백 함수이다.
deleteFileTemp는 버퍼에서만 이미지 파일을 삭제하는 역할을 하고 실제 이미지 파일을 삭제하지 않는다. 반면 deletFile은 실제 이미지 파일을 삭제하는 역할으 한다.

자바 코드

WebConfig

테스트 환경을 위해서 AWS S3가 아니라 로컬에 이미지 파일을 저장할 때 필요한 설정이다.
실제 파일을 저장할 때는 물리적인 경로('C:/Users/hs/Desktop/images/')로 저장하지만, 브라우저에서 이미지를 읽을 때는 논리적인 경로('localhost:8080/images/이미지이름.png')로 이미지를 읽어올 수 있다.

@Configuration
@RequiredArgsConstructor
public class WebConfig  implements WebMvcConfigurer {
    private final ModelInterceptor modelInterceptor;

    @Value("${custom.path.upload-images}")
    private String uploadImagePath;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/images/**")
                .addResourceLocations("file:///" + uploadImagePath)
                .setCachePeriod(0)
                .resourceChain(true)
                .addResolver(new PathResourceResolver());
    }
}

FileStore

	@Component
public class FileStore {
    @Value("${custom.path.upload-images}")
    private String fileDir;

    // 파일의 저장 위치를 지정한다
    public String getLocalSavePath(String filename){
        return fileDir + filename;
    }
    
    // 브라우저에서 파일을 요청할 때 필요한 url을 제공한다
    public String getViewPath(String filename){  return "/images/"+filename;}


    public String storeFile(MultipartFile multipartFile)throws IOException{
        if(multipartFile.isEmpty()){
            return null;
        }

        String originalFilename = multipartFile.getOriginalFilename();
        String storeFileName = createStoreFilename(originalFilename);
        multipartFile.transferTo(new File(getLocalSavePath(storeFileName))); // 로컬 디렉토리에 파일을 저장한다
        return  getViewPath(storeFileName); // 이미지
    }


    public void deleteFile(String src) {
        int index = src.lastIndexOf("/")+1;
        String storeFileName = src.substring(index);

        Path filePath = Paths.get(getLocalSavePath(storeFileName));
        try {
            Files.delete(filePath);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * UUID를 사용해 파일의 이름이 겹치지 않게 만들어준다
     * 원본 파일의 확장자를 그대로 유지하고 사용할 수 있게 해준다
     */
    private String createStoreFilename(String originalFilename) {
        String ext = extractExt(originalFilename);
        String uuid = UUID.randomUUID().toString();
        return uuid+"."+ext;
    }

    private String extractExt(String originalFilename) {
        int pos = originalFilename.lastIndexOf(".");
        return originalFilename.substring(pos + 1);
    }

}

실제로 이미지 파일을 저장하고 삭제하는 역할을 담당하는 클래스이다. 서비스단에서 호출하여 사용한다.
이미지 파일 저장 시 이미지 파일 이름이 겹치지 않도록 UUID로 중복되지 않는 새로운 이름을 부여하여 저장한다.

PostServiceImpl

다음은 Post와 관련된 로직을 담당하는 Service에서 새로운 게시글을 저장하는 메소드이다.
새로운 게시글 작성 시 이미 업로드된 이미지들은 이미 외부 디렉터리에 저장되어 있는 상태이다. 따라서 PostImage 테이블에 해당 이미지들과 관련된 정보(상태)들을 저장하기만 하면 된다.
또한 첫 번째 사진을 썸네일로 지정하여, 향후 썸네일 노출이 가능하도록 하고 있다.

@Override
    @Transactional
    public Long savePost(PostSaveDto postDto, String username) {
        // DB에 저장되어 있는 멤버 정보를 가지고 온다
        Optional<Member> optional = memberRepository.findByUsername(username);
        Member member = optional.orElseThrow(() -> new NotExistedPostException());

        // 포스트를 저장한다
        Post savedPost = postRepository.save(new Post(postDto.getTitle(), postDto.getBody(), member, postDto.getCategory()));

        // 포스트의 이미지들을 저장한다.
        List<String> images = postDto.getImages();
        if (images != null) {
            for (int i = 0; i < images.size(); i++) {
                PostImage postImage = new PostImage(savedPost, images.get(i));
                if(i == 0){ //첫 번째 사진을 썸네일로 지정
                    postImage.changeThumbnail(true);
                }
                postImageRepository.save(postImage); 
            }
        }

        // 멤버가 작성한 포스트 갯수를 증가시킨다
        member.increasePostCnt();

        return savedPost.getId();
    }

다음은 게시글을 수정하는 메서드이다

@Override
    @Transactional
    public Long updatePost(Long id,  PostSaveDto postSaveDto) {
        // DB에서 Post를 찾아온다
        Post savedPost = postRepository.findPostById(id).orElseThrow(() -> new NotExistedPostException());

         // 변경 감지를 통해 post의 내용을 수정한다(제목, 본문, 카테고리)
        savedPost.updatePost(postSaveDto);

        
        // 수정된 포스트에 이미지가 존재하지 않으면, 기존에 저장해두었던 모든 이미지 파일과 DB의 데이터를 삭제한다.
        List<String> newSrc = postSaveDto.getImages();
        List<PostImage> savedImages = postImageRepository.findAllPostImage(savedPost);
        if (newSrc == null || newSrc.size() == 0) {
            postImageRepository.deleteAllPostImage(savedPost); // DB에 해당 포스트의 모든 이미지 정보들을 삭제
            savedImages.forEach( savedInfo ->{
                fileStore.deleteFile(savedInfo.getSrc()); // 외부 디렉토리에서 해당 포스트의 이미지 파일들 모두 삭제
            });
            return savedPost.getId();
        }
        // 수정된 포스트에 이미지가 존재한다면
        // 1. 수정되기 전에 저장됐던 이미지 리스트와 수정된 후에 존재할 이미지 리스트를 비교한다
        // 2. 수정된 후에도 유지되어야 할 이미지들을 제외하고, 삭제되어야 할 이미지들을 삭제한다 
        // 3. 새롭게 등장한 이미지 데이터를 DB에 저장해야 된다(이미지 파일 자체는 외부 디렉토리에 먼저 저장되어 있는 상태)
        // 4. 썸네일을 변경해준다  1) 썸네일 유지되거나 2) 기존 썸네일이 새로운 썸내일로 바뀌거나 3) 아예 새로운 썸내일 생기거나

        Map<String, Boolean> map = new HashMap<>(); // 유지될 이미지들과 삭제될 이미지들을 비교
        List<String> survival = new ArrayList<>(); // 유지될 이미지들
        savedImages.forEach( savedInfo ->{
            map.put(savedInfo.getSrc(), false);
        });
        newSrc.forEach( image ->{
            map.put(image, true); // 중첩되는 이미지들(유지될 이미지들)의 value는 true로 바꾼다.
        });
        map.keySet().forEach( src ->{
            if(map.get(src) == true){ // 중첩되는 이미지들은 survival에 담는다
                survival.add(src);
            }else{
                postImageRepository.deletePostImageBySrc(src); // 중첩되지 않은 이미지들을 수정 과정에서 삭제된 이미지들이므로 삭제해주어야 한다.
                fileStore.deleteFile(src);
            }
        });

        List<String> savedSrc = new ArrayList<>();
        savedImages.forEach( savedImage ->{
            savedSrc.add(savedImage.getSrc());
        });
        newSrc.forEach( src ->{
            if(!savedSrc.contains(src)){ // 새롭게 추가된 이미지들  정보를 DB에 저장한다.
                postImageRepository.save( new PostImage(savedPost, src));
            }
        });

        Optional<PostImage> thumbNail = postImageRepository.findThumbNail(savedPost);
        if(thumbNail.isPresent()){
            PostImage oldThumbnail = thumbNail.get();
            if(!oldThumbnail.getSrc().equals(newSrc.get(0))){
                oldThumbnail.changeThumbnail(false);
                PostImage newThumbnail = postImageRepository.findPostImageBySrc(newSrc.get(0), savedPost).get();
                newThumbnail.changeThumbnail(true);
            }
        }else{
            PostImage newThumbnail = postImageRepository.findPostImageBySrc(newSrc.get(0),savedPost).get();
            newThumbnail.changeThumbnail(true);
        }
        return savedPost.getId();
    }
  1. 먼저 수정된 게시글에 이미지들이 존재하지 않으면, DB에서 이미지 정보들을 모두 지우고, 또한 해당 정보들을 바탕으로 외부 디렉토리에서 실제 이미지 파일들을 지운다.
  2. 만약 수정된 게시글에 이미지가 존재한다면 여러가지 경우의 수가 존재한다.

삭제할 이미지
수정 작업 중에서 삭제된 이미지들.
DB에서 해당 이미지 상태 정보를 삭제하고, 외부 디렉토리에서 실제 이미지 파일를 삭제한다
유지되는 이미지
수정 작업 후에도 유지된는 이미지들.
이미 이미지 상태 정보도 DB에 저장되어 있고, 외부 디렉토리에도 실제 이미지 파일이 저장되어 있다.
추가할 이미지
수정 작업에서 새로 추가된 이미지들.
이미지 업로드 시 실제 이미지 파일은 외부 디렉토리에 저장되어 있으므로, DB에 이미지 상태 정보를 저장하기만 하면 된다.

profile
코딩 불바다, 불 같은 코딩, 화끈하게 코딩하자

0개의 댓글