Skip to content

Commit 9aa37e8

Browse files
authored
�feat: 봉사 모집글 생성, 수정, 삭제 컨트롤러 생성 (#101)
* refactor(recruit-board): ApiController 네이밍 변경 - 다른분들과 동일하게 작성 * feat(image-upload): multi-part 파일을 받기 위한 컨버터 클래스 생성 * feat(image-upload): 빈 이미지 파일일 경우 기본 이미지 URL 반환 하도록 변경 * test(image-upload): 빈 이미지 파일일 경우 기본 이미지 URL 반환 하도록 변경에 따른 테스트 * chore: 기본 이미지 링크를 관리하기 위한 속성 추가 * feat(recruit-board): 생성 수정 삭제 컨트롤러 생성 * test(recruit-board): 생성 수정 삭제 컨트롤러 생성에 따른 테스트 * refactor(recruit-board): 와일드 카드 제거 * refactor(recruit-board): 와일드 카드 제거 * feat(test): 인증이 필요한 컨트롤러 테스트시 사용할 어노테이션 권한 추가 * feat(recruit-board): RecruitBoard Command Controller 추가 * test(recruit-board): RecruitBoard Command Controller 테스트
1 parent 28badd4 commit 9aa37e8

15 files changed

+488
-10
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.somemore.imageupload.converter;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import java.lang.reflect.Type;
5+
import org.springframework.http.MediaType;
6+
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
11+
12+
/**
13+
* Converter for support http request with header Content-Type: multipart/form-data
14+
*/
15+
public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
16+
super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
17+
}
18+
19+
@Override
20+
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
21+
return false;
22+
}
23+
24+
@Override
25+
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
26+
return false;
27+
}
28+
29+
@Override
30+
protected boolean canWrite(MediaType mediaType) {
31+
return false;
32+
}
33+
}

src/main/java/com/somemore/imageupload/service/ImageUploadService.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.somemore.imageupload.usecase.ImageUploadUseCase;
66
import com.somemore.imageupload.util.ImageUploadUtils;
77
import com.somemore.imageupload.validator.ImageUploadValidator;
8+
import jakarta.annotation.PostConstruct;
89
import lombok.RequiredArgsConstructor;
910
import org.springframework.beans.factory.annotation.Value;
1011
import org.springframework.stereotype.Service;
@@ -30,8 +31,22 @@ public class ImageUploadService implements ImageUploadUseCase {
3031
@Value("${cloud.aws.s3.base-url}")
3132
private String baseUrl;
3233

34+
@Value("${default.image.url}")
35+
private String defaultImageUrl;
36+
37+
public static String DEFAULT_IMAGE_URL;
38+
39+
@PostConstruct
40+
private void init() {
41+
DEFAULT_IMAGE_URL = defaultImageUrl;
42+
}
43+
3344
@Override
3445
public String uploadImage(ImageUploadRequestDto requestDto) {
46+
if (imageUploadValidator.isEmptyFile(requestDto.imageFile())) {
47+
return DEFAULT_IMAGE_URL;
48+
}
49+
3550
imageUploadValidator.validateFileSize(requestDto.imageFile());
3651
imageUploadValidator.validateFileType(requestDto.imageFile());
3752

src/main/java/com/somemore/imageupload/validator/DefaultImageUploadValidator.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ public class DefaultImageUploadValidator implements ImageUploadValidator {
1212
private static final long MAX_FILE_SIZE = 8L * 1024 * 1024; // 8MB
1313

1414
public void validateFileSize(MultipartFile file) {
15-
if (file == null || file.isEmpty()) {
16-
throw new ImageUploadException(EMPTY_FILE.getMessage());
17-
}
18-
1915
if (file.getSize() > MAX_FILE_SIZE) {
2016
throw new ImageUploadException(FILE_SIZE_EXCEEDED.getMessage());
2117
}
@@ -28,6 +24,11 @@ public void validateFileType(MultipartFile file) {
2824
}
2925
}
3026

27+
@Override
28+
public boolean isEmptyFile(MultipartFile file) {
29+
return file == null || file.isEmpty();
30+
}
31+
3132
private boolean isAllowedImageType(String contentType) {
3233
return contentType != null && (
3334
contentType.equals("image/jpeg") ||

src/main/java/com/somemore/imageupload/validator/ImageUploadValidator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ public interface ImageUploadValidator {
66

77
void validateFileSize(MultipartFile file);
88
void validateFileType(MultipartFile file);
9+
boolean isEmptyFile(MultipartFile file);
910
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package com.somemore.recruitboard.controller;
2+
3+
4+
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
5+
6+
import com.somemore.global.common.response.ApiResponse;
7+
import com.somemore.imageupload.dto.ImageUploadRequestDto;
8+
import com.somemore.imageupload.usecase.ImageUploadUseCase;
9+
import com.somemore.recruitboard.dto.request.RecruitBoardCreateRequestDto;
10+
import com.somemore.recruitboard.dto.request.RecruitBoardLocationUpdateRequestDto;
11+
import com.somemore.recruitboard.dto.request.RecruitBoardStatusUpdateRequestDto;
12+
import com.somemore.recruitboard.dto.request.RecruitBoardUpdateRequestDto;
13+
import com.somemore.recruitboard.usecase.command.CreateRecruitBoardUseCase;
14+
import com.somemore.recruitboard.usecase.command.DeleteRecruitBoardUseCase;
15+
import com.somemore.recruitboard.usecase.command.UpdateRecruitBoardUseCase;
16+
import io.swagger.v3.oas.annotations.Operation;
17+
import io.swagger.v3.oas.annotations.tags.Tag;
18+
import jakarta.validation.Valid;
19+
import java.time.LocalDateTime;
20+
import java.util.UUID;
21+
import lombok.RequiredArgsConstructor;
22+
import org.springframework.security.access.annotation.Secured;
23+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
24+
import org.springframework.web.bind.annotation.DeleteMapping;
25+
import org.springframework.web.bind.annotation.PatchMapping;
26+
import org.springframework.web.bind.annotation.PathVariable;
27+
import org.springframework.web.bind.annotation.PostMapping;
28+
import org.springframework.web.bind.annotation.PutMapping;
29+
import org.springframework.web.bind.annotation.RequestBody;
30+
import org.springframework.web.bind.annotation.RequestMapping;
31+
import org.springframework.web.bind.annotation.RequestPart;
32+
import org.springframework.web.bind.annotation.RestController;
33+
import org.springframework.web.multipart.MultipartFile;
34+
35+
@Tag(name = "Recruit Board Command API", description = "봉사 활동 모집글 생성 수정 삭제 API")
36+
@RequiredArgsConstructor
37+
@RequestMapping("/api")
38+
@RestController
39+
public class RecruitBoardCommandApiController {
40+
41+
private final CreateRecruitBoardUseCase createRecruitBoardUseCase;
42+
private final UpdateRecruitBoardUseCase updateRecruitBoardUseCase;
43+
private final DeleteRecruitBoardUseCase deleteRecruitBoardUseCase;
44+
private final ImageUploadUseCase imageUploadUseCase;
45+
46+
@Secured("ROLE_CENTER")
47+
@Operation(summary = "봉사 활동 모집글 등록", description = "봉사 활동 모집글을 등록합니다.")
48+
@PostMapping(value = "/recruit-board", consumes = MULTIPART_FORM_DATA_VALUE)
49+
public ApiResponse<Long> createRecruitBoard(
50+
@AuthenticationPrincipal String userId,
51+
@Valid @RequestPart("data") RecruitBoardCreateRequestDto requestDto,
52+
@RequestPart(value = "img_file", required = false) MultipartFile image
53+
) {
54+
55+
String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image));
56+
return ApiResponse.ok(
57+
201,
58+
createRecruitBoardUseCase.createRecruitBoard(requestDto, getCenterId(userId),
59+
imgUrl),
60+
"봉사 활동 모집글 등록 성공"
61+
);
62+
}
63+
64+
@Secured("ROLE_CENTER")
65+
@Operation(summary = "봉사 활동 모집글 수정", description = "봉사 활동 모집글을 수정합니다.")
66+
@PutMapping(value = "/recruit-board/{id}", consumes = MULTIPART_FORM_DATA_VALUE)
67+
public ApiResponse<String> updateRecruitBoard(
68+
@AuthenticationPrincipal String userId,
69+
@PathVariable Long id,
70+
@Valid @RequestPart("data") RecruitBoardUpdateRequestDto requestDto,
71+
@RequestPart("img_file") MultipartFile image
72+
) {
73+
String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image));
74+
updateRecruitBoardUseCase.updateRecruitBoard(requestDto, id, getCenterId(userId), imgUrl);
75+
76+
return ApiResponse.ok("봉사 활동 모집글 수정 성공");
77+
}
78+
79+
@Secured("ROLE_CENTER")
80+
@Operation(summary = "봉사 활동 모집글 위치 수정", description = "봉사 활동 모집글의 위치를 수정합니다.")
81+
@PutMapping(value = "/recruit-board/{id}/location")
82+
public ApiResponse<String> updateRecruitBoardLocation(
83+
@AuthenticationPrincipal String userId,
84+
@PathVariable Long id,
85+
@Valid @RequestBody RecruitBoardLocationUpdateRequestDto requestDto
86+
) {
87+
88+
updateRecruitBoardUseCase.updateRecruitBoardLocation(requestDto, id, getCenterId(userId));
89+
return ApiResponse.ok("봉사 활동 모집글 위치 수정 성공");
90+
}
91+
92+
@Secured("ROLE_CENTER")
93+
@Operation(summary = "봉사 활동 모집글 상태 수정", description = "봉사 활동 모집글의 상태를 수정합니다.")
94+
@PatchMapping(value = "/recruit-board/{id}")
95+
public ApiResponse<String> updateRecruitBoardStatus(
96+
@AuthenticationPrincipal String userId,
97+
@PathVariable Long id,
98+
@RequestBody RecruitBoardStatusUpdateRequestDto requestDto
99+
) {
100+
LocalDateTime now = LocalDateTime.now();
101+
updateRecruitBoardUseCase.updateRecruitBoardStatus(requestDto.status(), id,
102+
getCenterId(userId),
103+
now);
104+
105+
return ApiResponse.ok("봉사 활동 모집글 상태 수정 성공");
106+
}
107+
108+
@Secured("ROLE_CENTER")
109+
@Operation(summary = "봉사 활동 모집글 삭제", description = "봉사 활동 모집글을 삭제합니다.")
110+
@DeleteMapping(value = "/recruit-board/{id}")
111+
public ApiResponse<String> deleteRecruitBoard(
112+
@AuthenticationPrincipal String userId,
113+
@PathVariable Long id
114+
) {
115+
deleteRecruitBoardUseCase.deleteRecruitBoard(getCenterId(userId), id);
116+
return ApiResponse.ok("봉사 활동 모집글 삭제 성공");
117+
}
118+
119+
private static UUID getCenterId(String userId) {
120+
return UUID.fromString(userId);
121+
}
122+
123+
}

src/main/java/com/somemore/recruitboard/controller/RecruitBoardQueryController.java renamed to src/main/java/com/somemore/recruitboard/controller/RecruitBoardQueryApiController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
@RequiredArgsConstructor
3030
@RequestMapping("/api")
3131
@RestController
32-
public class RecruitBoardQueryController {
32+
public class RecruitBoardQueryApiController {
3333

3434
private final RecruitBoardQueryUseCase recruitBoardQueryUseCase;
3535

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.somemore.recruitboard.dto.request;
2+
3+
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
4+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
5+
import com.somemore.recruitboard.domain.RecruitStatus;
6+
import io.swagger.v3.oas.annotations.media.Schema;
7+
import lombok.Builder;
8+
9+
@Builder
10+
@JsonNaming(SnakeCaseStrategy.class)
11+
@Schema(description = "봉사 활동 모집글 상태 수정 요청 DTO")
12+
public record RecruitBoardStatusUpdateRequestDto(
13+
@Schema(description = "변경할 봉사 활동 모집글의 상태", example = "CLOSED")
14+
RecruitStatus status
15+
) {
16+
17+
}

src/main/resources/application.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,7 @@ server:
101101
charset: UTF-8
102102
enabled: true
103103
force: true
104+
105+
default:
106+
image:
107+
url: ${DEFAULT_IMG_URL}

src/test/java/com/somemore/CustomSecurityContextFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public SecurityContext createSecurityContext(WithMockCustomUser annotation) {
1717
Authentication auth = new UsernamePasswordAuthenticationToken(
1818
annotation.username(),
1919
"password",
20-
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
20+
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + annotation.role()))
2121
);
2222

2323
context.setAuthentication(auth);

src/test/java/com/somemore/WithMockCustomUser.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
@WithSecurityContext(factory = CustomSecurityContextFactory.class)
1010
public @interface WithMockCustomUser {
1111
String username() default "123e4567-e89b-12d3-a456-426614174000";
12+
String role() default "VOLUNTEER";
1213
}

0 commit comments

Comments
 (0)