Skip to content

Commit

Permalink
[Feature] 게시글 등록에 이미지 업로드 로직 추가 #119 (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
yschoi123 authored Jan 22, 2025
2 parents f815055 + fd67bc4 commit 58b57c3
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public enum ResponseCode implements Codable {
EMPTY_PARAM_08("6308", HttpStatus.BAD_REQUEST, "08번 Parameter 데이터 누락"),
EMPTY_PARAM_09("6309", HttpStatus.BAD_REQUEST, "09번 Parameter 데이터 누락"),

// 파일 관련 ( 값: 7xxx)
FILE_TOO_LARGE("7000", HttpStatus.PAYLOAD_TOO_LARGE, "파일 크기가 최대 허용치를 초과했습니다"),

// 강제 에러
TEST_ERROR("9999", HttpStatus.INTERNAL_SERVER_ERROR, "강제 발생 ERROR");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import com.palbang.unsemawang.common.constants.ResponseCode;
import com.palbang.unsemawang.common.response.ErrorResponse;
Expand Down Expand Up @@ -141,4 +142,14 @@ public ResponseEntity<ErrorResponse> handleInvalidFileFormatException(InvalidFil
.status(HttpStatus.BAD_REQUEST)
.body(response);
}

@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<ErrorResponse> handleMaxSizeException(MaxUploadSizeExceededException exc) {
log.error("Maximum upload size exceeded: {}", exc.getMessage());
ErrorResponse response = ErrorResponse.of(ResponseCode.FILE_TOO_LARGE);

return ResponseEntity
.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ public interface FileService {
*/
void uploadImage(MultipartFile file, FileRequest fileRequest);

/**
* 이미지 업로드
* @param files 업로드 할 이미지 파일 리스트
* @param fileRequest ReferenceType과 ReferenceId 입력
*/
void uploadImagesAtOnce(List<MultipartFile> files, FileRequest fileRequest);

/**
* 프로필 이미지 url 반환
* @param referenceId 참조된 회원 id, referenceId로 사용된 id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public void uploadImage(MultipartFile file, FileRequest fileRequest) {
saveFileOrRollback(fileEntity, path, fileRequest);
}

@Override
public void uploadImagesAtOnce(List<MultipartFile> files, FileRequest fileRequest) {
files.forEach(f -> uploadImage(f, fileRequest));
}

/*
upload 업로드
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,48 @@
package com.palbang.unsemawang.community.controller;

import java.util.List;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.multipart.MultipartFile;

import com.palbang.unsemawang.community.dto.request.PostRegisterRequest;
import com.palbang.unsemawang.community.dto.request.PostUpdateRequest;
import com.palbang.unsemawang.community.dto.response.PostRegisterResponse;
import com.palbang.unsemawang.oauth2.dto.CustomOAuth2User;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;

@Tag(name = "커뮤니티 게시글")
@Tag(name = "커뮤니티")
public interface PostController {
/* 게시글 등록 */
@Operation(
description = "커뮤니티 게시글 등록 API 입니다. 인증 토큰이 담긴 쿠키를 직접 보내셔야 테스트가 가능합니다!",
summary = "커뮤니티 게시글 등록"
)
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponse(responseCode = "201", description = "이미지 업로드 및 게시글 등록 성공")
@ApiResponse(responseCode = "413", description = "이미지 용량 초과로 등록 실패")
@ApiResponse(responseCode = "400", description = "권한이 없는 회원이거나 유효하지 않는 데이터로 등록 실패")
ResponseEntity<PostRegisterResponse> write(
@AuthenticationPrincipal CustomOAuth2User auth,
@Valid @RequestBody PostRegisterRequest postRegisterRequest
CustomOAuth2User auth,
List<MultipartFile> fileList,
PostRegisterRequest postRegisterRequest
);

/* 게시글 수정 */
@Operation(
description = "커뮤니티 게시글 수정 API 입니다. 인증 토큰이 담긴 쿠키를 보내셔야 테스트가 가능합니다!",
summary = "커뮤니티 게시글 수정"
)
@PutMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
@PutMapping(path = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity modify(
@AuthenticationPrincipal CustomOAuth2User auth,
@PathVariable("id") Long postId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.palbang.unsemawang.community.controller;

import java.net.URI;
import java.util.List;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand All @@ -9,14 +12,17 @@
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.palbang.unsemawang.common.constants.ResponseCode;
import com.palbang.unsemawang.common.exception.GeneralException;
import com.palbang.unsemawang.community.dto.request.PostDeleteRequest;
import com.palbang.unsemawang.community.dto.request.PostRegisterRequest;
import com.palbang.unsemawang.community.dto.request.PostUpdateRequest;
import com.palbang.unsemawang.community.dto.response.PostRegisterResponse;
import com.palbang.unsemawang.community.entity.Post;
import com.palbang.unsemawang.community.service.PostService;
import com.palbang.unsemawang.oauth2.dto.CustomOAuth2User;

Expand All @@ -31,25 +37,26 @@ public class PostControllerImpl implements PostController {
private final PostService postService;

/* 게시글 등록 */
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Override
public ResponseEntity<PostRegisterResponse> write(
@AuthenticationPrincipal CustomOAuth2User auth,
@Valid @RequestBody PostRegisterRequest postRegisterRequest
@RequestPart(value = "imageFiles", required = false) List<MultipartFile> imageFileList,
@Valid @RequestPart(value = "postDetail") PostRegisterRequest postRegisterRequest
) {
if (auth == null || auth.getId() == null) {
throw new GeneralException(ResponseCode.EMPTY_TOKEN);
}

postRegisterRequest.updateMemberId(auth.getId());

PostRegisterResponse postRegisterResponse = postService.register(postRegisterRequest);
Post savedPost = postService.register(postRegisterRequest, imageFileList);

return ResponseEntity.ok(postRegisterResponse);
return ResponseEntity.created(URI.create("/posts/" + savedPost.getId())).build();
}

/* 게시글 수정 */
@PutMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
@PutMapping(path = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
@Override
public ResponseEntity modify(
@AuthenticationPrincipal CustomOAuth2User auth,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.palbang.unsemawang.community.service;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import com.palbang.unsemawang.common.constants.ResponseCode;
import com.palbang.unsemawang.common.exception.GeneralException;
import com.palbang.unsemawang.common.util.file.dto.FileReferenceType;
import com.palbang.unsemawang.common.util.file.dto.FileRequest;
import com.palbang.unsemawang.common.util.file.service.FileService;
import com.palbang.unsemawang.community.dto.request.PostDeleteRequest;
import com.palbang.unsemawang.community.dto.request.PostRegisterRequest;
import com.palbang.unsemawang.community.dto.request.PostUpdateRequest;
import com.palbang.unsemawang.community.dto.response.PostRegisterResponse;
import com.palbang.unsemawang.community.entity.Post;
import com.palbang.unsemawang.community.repository.PostRepository;
import com.palbang.unsemawang.member.entity.Member;
Expand All @@ -19,11 +24,12 @@
@Service
@RequiredArgsConstructor
public class PostService {
private final FileService fileService;
private final PostRepository postRepository;
private final MemberRepository memberRepository;

@Transactional(rollbackFor = Exception.class)
public PostRegisterResponse register(PostRegisterRequest postRegisterRequest) {
public Post register(PostRegisterRequest postRegisterRequest) {

// 0. 유효한 회원인지 확인
Member member = memberRepository.findById(postRegisterRequest.getMemberId())
Expand All @@ -39,7 +45,19 @@ public PostRegisterResponse register(PostRegisterRequest postRegisterRequest) {
throw new GeneralException(ResponseCode.ERROR_INSERT, "게시글 등록에 실패했습니다");
}

return PostRegisterResponse.of("게시글 등록이 성공했습니다!");
return savedPost;
}

@Transactional(rollbackFor = Exception.class)
public Post register(PostRegisterRequest postRegisterRequest, List<MultipartFile> fileList) {

// 1. 게시글 등록
Post savedPost = register(postRegisterRequest);

// 2. 이미지 업로드
fileService.uploadImagesAtOnce(fileList, FileRequest.of(FileReferenceType.COMMUNITY_BOARD, savedPost.getId()));

return savedPost;
}

@Transactional(rollbackFor = Exception.class)
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/palbang/unsemawang/config/ConverterConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.palbang.unsemawang.config;

import java.util.ArrayList;
import java.util.List;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

@Configuration
public class ConverterConfig {

public ConverterConfig(MappingJackson2HttpMessageConverter converter) {
// octet-stream 타입을 허용하도록 설정
List<MediaType> supportedMediaTypes = new ArrayList<>(converter.getSupportedMediaTypes());
supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
converter.setSupportedMediaTypes(supportedMediaTypes);
}
}
6 changes: 6 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,9 @@ cloud.aws.credentials.access-key=${AWS_ACCESS_KEY_ID}
cloud.aws.credentials.secret-key=${AWS_SECRET_ACCESS_KEY}
cloud.aws.region.static=${AWS_REGION}
cloud.aws.s3.bucket=${S3_BUCKET_NAME}
# MultipartFile
spring.servlet.multipart.max-file-size=3MB
spring.servlet.multipart.max-request-size=15MB
# HTTP form ?? ???
server.tomcat.max-http-form-post-size=15MB

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockPart;
import org.springframework.test.web.servlet.MockMvc;

import com.fasterxml.jackson.databind.ObjectMapper;
Expand Down Expand Up @@ -48,10 +50,11 @@ public void post_failedValidation_titleSize() throws Exception {
.build();

// when, then : 요청을 보내면 Vaild 예외가 발생해야한다
mockMvc.perform(post("/posts")
.contentType("application/json")
.content(objectMapper.writeValueAsString(postRegisterRequest))
.with(csrf())
mockMvc.perform(
multipart("/posts")
.part(new MockPart("postDetail", null, objectMapper.writeValueAsBytes(postRegisterRequest),
MediaType.APPLICATION_JSON))
.with(csrf())
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("제목은 30자 이내여야 합니다"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
import org.springframework.boot.test.mock.mockito.MockBean;

import com.palbang.unsemawang.common.exception.GeneralException;
import com.palbang.unsemawang.common.util.file.service.FileService;
import com.palbang.unsemawang.community.constant.CommunityCategory;
import com.palbang.unsemawang.community.dto.request.PostRegisterRequest;
import com.palbang.unsemawang.community.dto.request.PostUpdateRequest;
import com.palbang.unsemawang.community.dto.response.PostRegisterResponse;
import com.palbang.unsemawang.community.entity.Post;
import com.palbang.unsemawang.community.repository.PostRepository;
import com.palbang.unsemawang.member.constant.MemberRole;
Expand All @@ -34,6 +34,9 @@ class PostServiceTest {
@MockBean
private MemberRepository memberRepository;

@MockBean
private FileService fileService;

@Test
@DisplayName(value = "게시글 등록 - 모든 값이 정상적으로 들어온 경우")
public void postRegisterTest() {
Expand Down Expand Up @@ -61,9 +64,9 @@ public void postRegisterTest() {
when(postRepository.save(any(Post.class))).thenReturn(post);

// then
PostRegisterResponse postRegisterResponse = postService.register(postRegisterRequest);
assertNotNull(postRegisterResponse);
assertEquals("게시글 등록이 성공했습니다!", postRegisterResponse.getMessage());
Post registeredPost = postService.register(postRegisterRequest);
assertNotNull(registeredPost.getId());

verify(postRepository, times(1)).save(any());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ private FortuneContent initFortuneContentEntity(int i, FortuneCategory category)
.nameKo("테스트명" + i)
.fortuneCategory(category)
.path("/test-path" + i)
.isVisible(false)
.isVisible(true)
.isDeleted(true)
.registeredAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
Expand Down

0 comments on commit 58b57c3

Please sign in to comment.