投稿の作成+ファイルのアップロード(SpringBoot+JPA+AWS 3)


この記事では、投稿の作成+ファイルのアップロード(Awss 3)について説明します.🕌
  • Post
  • @Getter
    @NoArgsConstructor(access = PROTECTED)
    @Entity
    @EntityListeners(AuditListener.class)
    public class Post implements Auditable {
    
        @Id
        @GeneratedValue(strategy = IDENTITY)
        @Column(name = "post_id")
        private Long id;
    
        @Column(nullable = false)
        private String title;
    
        @Column(nullable = false, length = 1000)
        private String content;
    
        @Embedded
        private TimeEntity timeEntity;
    
        @ColumnDefault("0")
        @Column(name = "view_count",nullable = false)
        private Integer viewCount;
    
        @ManyToOne(fetch = LAZY)
        @JoinColumn(name = "user_id")
        private User user;
    
        @ManyToOne(fetch = LAZY,cascade = CascadeType.PERSIST)
        @JoinColumn(name = "post_category_id")
        private PostCategory postCategory;
    
        @OneToMany(mappedBy = "post", orphanRemoval = true)
        private List<Comment> comments = new ArrayList<>();
    
        @OneToMany(mappedBy = "post", orphanRemoval = true)
        private List<PostLike> postLikes = new ArrayList<>();
    
        @OneToMany(mappedBy = "post", orphanRemoval = true)
        private List<Scrap> scraps = new ArrayList<>();
    
        @OneToMany(mappedBy = "post", orphanRemoval = true)
        private List<PostImage> postImages = new ArrayList<>();
    
    
        @Override
        public void setTimeEntity(TimeEntity timeEntity) {
            this.timeEntity = timeEntity;
        }
    
        @Builder
        public Post(String title, String content, Integer viewCount, User user, List<Comment> comments, PostCategory postCategory) {
            this.title = title;
            this.content = content;
            this.viewCount = viewCount;
            this.user = user;
            this.comments = comments;
            this.postCategory = postCategory;
        }
    
        /**
         * 생성 메서드
         */
        public static Post createPost(String title, String content, User user, PostCategory postCategory){
            return Post.builder()
                    .title(title)
                    .content(content)
                    .user(user)
                    .postCategory(postCategory)
                    .build();
        }
    
        public void updatePost(String title, String content) {
            this.title = title;
            this.content = content;
        }
    
        /**
         * 초기화 값이 DB 에 추가되지 않는 오류가 있어서
         * persist 하기 전에 초기화
         */
        @PrePersist
        public void prePersistCount(){
            this.viewCount = this.viewCount == null ? 0 : this.viewCount;
        }
    }
  • Postエンティティについては前述のとおり、詳細な説明を省略する.

  • まず、投稿を生成するには、フロントからタイトル、コンテンツ、カテゴリ名、画像ファイルを取得する必要があります.
  • PostRequest
  • @ApiModel(description = "게시글 생성 요청 데이터 모델")
    @Getter
    @Setter
    @NoArgsConstructor
    public class PostRequest {
    
        @ApiModelProperty(value = "게시글 제목", example = "모아모아 화이팅!", required = true)
        private String title;
    
        @ApiModelProperty(value = "게시글 내용", example = "무야호", required = true)
        private String content;
    
        @ApiModelProperty(value = "커뮤니티 게시글의 카테고리 이름", example = "모아모아", required = true)
        private String categoryName;
    
        @ApiModelProperty(value = "이미지 파일", required = false)
        private List<MultipartFile> imageFiles = new ArrayList<>();
    
    }
  • 画像ファイルはList複数のファイルを受信する.
  • PostController
  • @ApiOperation(value = "게시글 생성", notes = "Form Data 값을 받아와서 글을 생성하는 API",
                consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
        @ApiResponses({
                @ApiResponse(responseCode = "200", description = "해당 게시글이 정상적으로 생성된 경우"),
                @ApiResponse(responseCode = "404", description = "회원의 Id를 찾지 못한 경우")
        })
        @PostMapping
        public PostCreateResponse createPost(@ModelAttribute PostRequest request){
            return postService.createPost(request);
        }

  • 筆者使用Swagger行いAPI文書化.

  • 画像ファイルを転送する場合、FormData方式で転送する必要があり、Jsonタイプのタイトル、内容、カテゴリ名を一緒に送信するので、@RequestPart受信は可能ですが、明確には見えませんSwaggerということで使用しました@ModelAttribut
  • PostService
  • @Transactional
        public PostCreateResponse createPost(PostRequest postRequest) {
            PostCategory postCategory = postCategoryRepository.findByCategoryName(postRequest.getCategoryName())
                    .orElseGet(() -> PostCategory.createCategory(postRequest.getCategoryName()));
            User user = userUtil.findCurrent();
            Post post = postRepository.save(Post.createPost(postRequest.getTitle(), postRequest.getContent(), user, postCategory));
            List<String> postImages = uploadPostImages(postRequest, post);
    
            return new PostCreateResponse(post.getId(), "게시글 작성이 완료되었습니다.", postImages);
        }

  • まず、上のpostCategoryRepository.findByCategoryName()投稿にカテゴリが存在するかどうかを確認し、存在しない場合はカテゴリインポートロジックを再生成し、上のコードは再変更されます.(DBで予め作成・使用される)

  • ここで重要なのは、筆者が使用しているJWTは、ユーザの情報を単独で伝達するのではなく、HttpHeaderAccessTokenから情報を受信し、それを用いてユーザの情報を検証し、検証の結果をSecurityContextに含めることである.
  • userUtil.findCurrent()受け取った価格からRequestの投稿が生成されます.
  • uploadImages()
        private List<String> uploadPostImages(PostRequest postRequest, Post post) {
            return postRequest.getImageFiles().stream()
                    .map(image -> s3Uploader.upload(image, "post"))
                    .map(url -> createPostImage(post, url))
                    .map(postImage -> postImage.getImageUrl())
                    .collect(Collectors.toList());
        }
    createPostImage()
     private PostImage createPostImage(Post post, String url) {
            return postImageRepository.save(PostImage.builder()
                    .imageUrl(url)
                    .storeFilename(StringUtils.getFilename(url))
                    .post(post)
                    .build());
        }
  • まず使用PostRepository.save()受信した画像ファイルを予め設定されたstream()パスに保存した後、S3エンティティを生成し、その画像パスを戻すPostImage
  • PostImage
  • @Entity
    @Getter
    @NoArgsConstructor(access = PROTECTED)
    @EntityListeners(AuditListener.class)
    public class PostImage implements Auditable {
    
        @Id
        @GeneratedValue(strategy = IDENTITY)
        @Column(name = "post_image_id")
        private Long id;
    
        private String imageUrl;
    
        private String storeFilename;
    
        @Embedded
        private TimeEntity timeEntity;
    
        @ManyToOne
        @JoinColumn(name = "post_id")
        private Post post;
    
        @Builder
        public PostImage(String imageUrl, String storeFilename, Post post) {
            this.imageUrl = imageUrl;
            this.storeFilename = storeFilename;
            this.post = post;
        }
    
        @Override
        public void setTimeEntity(TimeEntity timeEntity) {
            this.timeEntity = timeEntity;
        }
    }
    Postmanテスト
  • Listと指定されている場合は、上記@ModelAttribute方式にてkey+value方式で送信する.

  • ファイルの内容タイプ=multiparty/form-data

  • 残りはform-data方式で送信するので、上記の設定を行いました.
  • レスポンス結果

    投稿の変更🏡

  • PostUpdateRequest
  • @ApiModel(description = "게시글 수정 요청 데이터 모델")
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public class PostUpdateRequest {
    
        @ApiModelProperty(value = "게시글 PK", example = "1", required = true)
        @NotNull
        private Long postId;
    
        @ApiModelProperty(value = "게시글 제목", example = "모아모아 화이팅!")
        private String title;
    
        @ApiModelProperty(value = "게시글 내용", example = "무야호")
        private String content;
    
        @ApiModelProperty(value = "삭제한 이미지 경로를 제외한 남아있는 게시글 이미지 경로")
        private List<String> saveImageUrl = new ArrayList<>();
    
        @ApiModelProperty(value = "게시글 이미지", required = false)
        private List<MultipartFile> imageFiles = new ArrayList<>();
    
    }
  • 修正時は、上記削除した画像経路以外の投稿画像経路をJsonに送信する.
  • PostController
     @ApiOperation(value = "게시글 수정", notes = "Request Body 값을 받아와서 글을 수정하는 API")
        @ApiResponses({
                @ApiResponse(responseCode = "200", description = "해당 게시글이 정상적으로 수정된 경우"),
                @ApiResponse(responseCode = "404", description = "회원 OR 게시글의 Id를 찾지 못한 경우")
        })
        @PatchMapping
        public PostUpdateResponse updatePost(@ModelAttribute PostUpdateRequest request) {
            return postService.updatePost(request);
        }
    postService
    @Transactional
        public PostUpdateResponse updatePost(PostUpdateRequest request) {
            User user = userUtil.findCurrent();
            Post post = postRepository.findByIdAndUser(request.getPostId(), user.getId())
                    .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_POST));
    
            validateDeletedImages(request);
            uploadPostImages(request, post);
            List<String> saveImages = getSaveImages(request);
    
            post.updatePost(request.getTitle(), request.getContent());
    
            return new PostUpdateResponse(post.getId(), "게시글 변경이 완료되었습니다.", saveImages);
        }
  • 投稿の修正は投稿の生成よりも難しい.

  • プレイヤー情報を探す

  • ユーザーと投稿の関係を検索します(変更権限があるかどうか)
  • List受信した画像経路が記憶されている画像経路と一致しない場合は、全て削除

  • ファイルの追加時に、ファイルをアップロードRequestで作成S3
  • PostImageテーブルに格納されている画像パスを抽出する

  • JPAの使用PostImage機能変更投稿변경감지
  • validateDeletedImages()
        /**
         * @Request로 받아온 이미지 경로랑 저장 되어있던 이미지 경로랑 일치하지 않는다면 모두 삭제
         */
        private void validateDeletedImages(PostUpdateRequest request) {
            postImageRepository.findBySavedImageUrl(request.getPostId()).stream()
                    .filter(image -> !request.getSaveImageUrl().stream().anyMatch(Predicate.isEqual(image.getImageUrl())))
                    .forEach(url -> {
                        postImageRepository.delete(url);
                        s3Uploader.deleteImage(url.getImageUrl());
                    });
        }
  • updateを使用して、stream受信した画像経路とrequestテーブルに格納されている経路が一致しているかどうかを検証した後、一致しなければ全て削除します.
  • uploadPostImages()
        /**
         * S3에 업로드 및 PostImage 생성
         */
        private void uploadPostImages(PostUpdateRequest request, Post post) {
            request.getImageFiles()
                    .stream()
                    .forEach(file -> {
                        String url = s3Uploader.upload(file, "post");
                        createPostImage(post, url);
                    });
        }
    getSaveImages()
        /**
         * PostImage 테이블에 저장 되어있는 이미지 경로를 추출
         */
      private List<String> getSaveImages(PostUpdateRequest request) {
            return postImageRepository.findBySavedImageUrl(request.getPostId())
                    .stream()
                    .map(image -> image.getImageUrl())
                    .collect(Collectors.toList());
        }
    Postmanテスト
  • 既存に2つのファイルが格納されている場合、1つの画像パスを入れて削除し、2つのファイルを入れるだけで、合計3つの画像パスを返す必要があります.
  • 結果画面

    S 3の設定やロジックについては他のブログでいろいろありますが、参考にしてください...🍎