Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,8 @@ include::auth-api.adoc[]

= **그루밍 테스트**

include::grooming-test-api.adoc[]
include::grooming-test-api.adoc[]

= **유저픽 게시글**

include::post-api.adoc[]
43 changes: 43 additions & 0 deletions src/docs/asciidoc/post-api.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
=== **1. 게시글 저장**

유저픽 게시글 저장, 등록 api 입니다. +
게시글, 게시글 이미지, 상품 , 상품 이미지를 함께 저장합니다. +
상품 요청 데이터의 `imageIndex` 는 해당 상품의 이미지가 `productImageFiles` 리스트 중 몇 번째에 위치하는지를 나타냅니다. +
`imageIndex` 는 **1부터 시작하는 인덱스**이며, 이미지가 없는 경우 `-1` 을 입력해야 합니다. +
`-1`, `1 ~ 이미지 총 개수` 를 제외한 인덱스로 요청 시 검증 로직에서 필터링되면서 예외발생 합니다. +
클라이언트는 `productImageFiles` 의 순서와 각 상품의 `imageIndex` 값이 정확히 매핑되도록 주의해야 합니다.

==== Request
include::{snippetsDir}/savePost/1/curl-request.adoc[]

==== Request Parts
include::{snippetsDir}/savePost/1/request-parts.adoc[]

==== Request Parts : **data** - Detail Fields
include::{snippetsDir}/savePost/1/request-part-data-fields.adoc[]

==== 성공 Response
include::{snippetsDir}/savePost/1/http-response.adoc[]

==== 실패 Response
실패 1. 인증되지 않은 유저일 경우

include::{snippetsDir}/savePost/2/http-response.adoc[]

실패 2. 이미지와 상품 정보가 매핑되지 않을 경우 +
- 상품 정보 imageIndex 중복 +
- 이미지 개수와 상품 개수 불일치 (이미지를 등록하지 않은 상품을 제외한 개수로 비교) +

include::{snippetsDir}/savePost/3/http-response.adoc[]

실패 3. 유효하지 않은 요청 데이터일 경우

include::{snippetsDir}/savePost/4/http-response.adoc[]

실패 4. 요청한 파일 목록 검증에 실패할 경우 (이미지 파일 X, 사이즈)

include::{snippetsDir}/savePost/5/http-response.adoc[]

실패 5. 이미지 파일 업로드에 실패할 경우

include::{snippetsDir}/savePost/6/http-response.adoc[]
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.ftm.server.adapter.in.web.post.controller;

import com.ftm.server.adapter.in.web.post.dto.request.SavePostRequest;
import com.ftm.server.application.command.post.SavePostCommand;
import com.ftm.server.application.port.in.post.SavePostUseCase;
import com.ftm.server.common.response.ApiResponse;
import com.ftm.server.common.response.enums.SuccessResponseCode;
import com.ftm.server.infrastructure.security.UserPrincipal;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequiredArgsConstructor
public class SavePostController {

private final SavePostUseCase savePostUseCase;

@PostMapping("/api/posts")
public ResponseEntity<ApiResponse<Void>> savePost(
@RequestPart(value = "data") @Valid SavePostRequest request,
@RequestPart(value = "postImageFiles", required = false)
List<MultipartFile> postImageFiles,
@RequestPart(value = "productImageFiles", required = false)
List<MultipartFile> productImageFiles,
@AuthenticationPrincipal UserPrincipal userPrincipal) {
savePostUseCase.execute(
SavePostCommand.from(
userPrincipal.getId(), request, postImageFiles, productImageFiles));
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(SuccessResponseCode.CREATED));
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ftm.server.adapter.in.web.post.dto.request;

import com.ftm.server.domain.enums.HashTag;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class SavePostProductRequest {

private int imageIndex;
@NotEmpty private String name;
private String brand;
private List<HashTag> hashtags;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ftm.server.adapter.in.web.post.dto.request;

import com.ftm.server.domain.enums.GroomingCategory;
import com.ftm.server.domain.enums.HashTag;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class SavePostRequest {

@NotEmpty private String title;
private GroomingCategory groomingCategory;
private List<HashTag> hashtags;
@NotEmpty private String content;
private List<SavePostProductRequest> products;
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.ftm.server.adapter.out.persistence.adapter.post;

import com.ftm.server.adapter.out.persistence.mapper.PostImageMapper;
import com.ftm.server.adapter.out.persistence.mapper.PostMapper;
import com.ftm.server.adapter.out.persistence.mapper.PostProductImageMapper;
import com.ftm.server.adapter.out.persistence.mapper.PostProductMapper;
import com.ftm.server.adapter.out.persistence.model.*;
import com.ftm.server.adapter.out.persistence.repository.*;
import com.ftm.server.application.port.out.persistence.post.SavePostImagePort;
import com.ftm.server.application.port.out.persistence.post.SavePostPort;
import com.ftm.server.application.port.out.persistence.post.SavePostProductImagePort;
import com.ftm.server.application.port.out.persistence.post.SavePostProductPort;
import com.ftm.server.common.annotation.Adapter;
import com.ftm.server.common.exception.CustomException;
import com.ftm.server.common.response.enums.ErrorResponseCode;
import com.ftm.server.domain.entity.Post;
import com.ftm.server.domain.entity.PostImage;
import com.ftm.server.domain.entity.PostProduct;
import com.ftm.server.domain.entity.PostProductImage;
import java.util.List;
import lombok.RequiredArgsConstructor;

@Adapter
@RequiredArgsConstructor
public class PostDomainPersistenceAdapter
implements SavePostPort, SavePostImagePort, SavePostProductPort, SavePostProductImagePort {

private final PostRepository postRepository;
private final PostImageRepository postImageRepository;
private final PostProductRepository postProductRepository;
private final PostProductImageRepository postProductImageRepository;
private final UserRepository userRepository;

private final PostMapper postMapper;
private final PostImageMapper postImageMapper;
private final PostProductMapper postProductMapper;
private final PostProductImageMapper postProductImageMapper;

@Override
public Post savePost(Post post) {
UserJpaEntity userJpaEntity =
userRepository
.findById(post.getUserId())
.orElseThrow(() -> CustomException.USER_NOT_FOUND);
PostJpaEntity postJpaEntity =
postRepository.save(postMapper.toJpaEntity(post, userJpaEntity));

return postMapper.toDomainEntity(postJpaEntity);
}

@Override
public List<PostImage> savePostImages(List<PostImage> postImages) {
List<PostImageJpaEntity> postImageJpaEntities =
postImages.stream()
.map(
postImage -> {
PostJpaEntity postJpaEntity =
postRepository
.findById(postImage.getPostId())
.orElseThrow(
() ->
new CustomException(
ErrorResponseCode
.POST_NOT_FOUND));

return postImageMapper.toJpaEntity(postImage, postJpaEntity);
})
.toList();

return postImageRepository.saveAll(postImageJpaEntities).stream()
.map(postImageMapper::toDomainEntity)
.toList();
}

@Override
public List<PostProduct> savePostProducts(List<PostProduct> postProducts) {
List<PostProductJpaEntity> productJpaEntities =
postProducts.stream()
.map(
postProduct -> {
PostJpaEntity postJpaEntity =
postRepository
.findById(postProduct.getPostId())
.orElseThrow(
() ->
new CustomException(
ErrorResponseCode
.POST_NOT_FOUND));
return postProductMapper.toJpaEntity(
postProduct, postJpaEntity);
})
.toList();

return postProductRepository.saveAll(productJpaEntities).stream()
.map(postProductMapper::toDomainEntity)
.toList();
}

@Override
public List<PostProductImage> savePostProductImages(List<PostProductImage> productImages) {
List<PostProductImageJpaEntity> postProductImageJpaEntities =
productImages.stream()
.map(
postProductImage -> {
PostProductJpaEntity postProductJpaEntity =
postProductRepository
.findById(postProductImage.getPostProductId())
.orElseThrow(
() ->
new CustomException(
ErrorResponseCode
.POST_PRODUCT_NOT_FOUND));
return postProductImageMapper.toJpaEntity(
postProductImage, postProductJpaEntity);
})
.toList();

return postProductImageRepository.saveAll(postProductImageJpaEntities).stream()
.map(postProductImageMapper::toDomainEntity)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public class PostJpaEntity extends BaseTimeJpaEntity {
@Column(nullable = false)
private String title;

@Lob
@Column(nullable = false)
private String content;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.ftm.server.application.port.out.s3.S3ImageDeletePort;
import com.ftm.server.common.annotation.Adapter;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import software.amazon.awssdk.services.s3.S3Client;
Expand All @@ -24,4 +25,15 @@ public void deleteImage(String objectKey) {

s3Client.deleteObject(deleteRequest);
}

@Override
public void deleteImages(List<String> objectKeys) {
if (objectKeys == null || objectKeys.isEmpty()) return;
for (String objectKey : objectKeys) {
DeleteObjectRequest deleteRequest =
DeleteObjectRequest.builder().bucket(bucket).key(objectKey).build();

s3Client.deleteObject(deleteRequest);
}
}
}
Loading
Loading