Skip to content

Commit

Permalink
[BE] feat: 반려견 정보 수정 API (#398)
Browse files Browse the repository at this point in the history
  • Loading branch information
takoyakimchi authored Aug 14, 2024
1 parent bde5fec commit e1b3fa0
Show file tree
Hide file tree
Showing 12 changed files with 407 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.happy.friendogly.infra;

import com.happy.friendogly.exception.FriendoglyException;
import java.util.Arrays;
import java.util.stream.Collectors;

public enum ImageUpdateType {

UPDATE,
NOT_UPDATE,
DELETE;

public static ImageUpdateType from(String rawImageUpdateType) {
try {
return valueOf(rawImageUpdateType);
} catch (IllegalArgumentException e) {
throw new FriendoglyException(
String.format("존재하지 않는 ImageUpdateType 입니다. %s 중 하나로 입력해주세요.", createAllowedValues())
);
}
}

private static String createAllowedValues() {
return Arrays.stream(values())
.map(Enum::name)
.collect(Collectors.joining(", "));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.happy.friendogly.auth.Auth;
import com.happy.friendogly.common.ApiResponse;
import com.happy.friendogly.pet.dto.request.SavePetRequest;
import com.happy.friendogly.pet.dto.request.UpdatePetRequest;
import com.happy.friendogly.pet.dto.response.FindPetResponse;
import com.happy.friendogly.pet.dto.response.SavePetResponse;
import com.happy.friendogly.pet.service.PetCommandService;
Expand All @@ -12,6 +13,7 @@
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down Expand Up @@ -60,4 +62,14 @@ public ApiResponse<List<FindPetResponse>> findByMemberId(@RequestParam Long memb
List<FindPetResponse> response = petQueryService.findByMemberId(memberId);
return ApiResponse.ofSuccess(response);
}

@PatchMapping("/{petId}")
public void update(
@PathVariable Long petId,
@Auth Long memberId,
@RequestPart @Valid UpdatePetRequest request,
@RequestPart(required = false) MultipartFile image
) {
petCommandService.update(memberId, petId, request, image);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
public class Description {

private static final int MIN_DESCRIPTION_LENGTH = 1;
private static final int MAX_DESCRIPTION_LENGTH = 15;
private static final int MAX_DESCRIPTION_LENGTH = 20;

@Column(name = "description", nullable = false)
private String value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
public class Name {

private static final int MIN_NAME_LENGTH = 1;
private static final int MAX_NAME_LENGTH = 15;
private static final int MAX_NAME_LENGTH = 8;

@Column(name = "name", nullable = false)
private String value;
Expand Down
18 changes: 17 additions & 1 deletion backend/src/main/java/com/happy/friendogly/pet/domain/Pet.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public Pet(
String imageUrl
) {
validateMember(member);

this.member = member;
this.name = new Name(name);
this.description = new Description(description);
Expand All @@ -80,4 +80,20 @@ private void validateMember(Member member) {
public boolean isOwner(Member member) {
return this.member.getId().equals(member.getId());
}

public void update(
String name,
String description,
LocalDate birthDate,
String sizeType,
String gender,
String imageUrl
) {
this.name = new Name(name);
this.description = new Description(description);
this.birthDate = new BirthDate(birthDate);
this.sizeType = SizeType.toSizeType(sizeType);
this.gender = Gender.toGender(gender);
this.imageUrl = imageUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
public record SavePetRequest(

@NotBlank(message = "name은 빈 문자열이나 null을 입력할 수 없습니다.")
@Size(max = 15, message = "이름은 1글자 이상 15글자 이하여야 합니다.")
@Size(max = 8, message = "이름은 1글자 이상 8글자 이하여야 합니다.")
String name,

@NotBlank(message = "description은 빈 문자열이나 null을 입력할 수 없습니다.")
@Size(max = 15, message = "설명은 1글자 이상 15글자 이하여야 합니다.")
@Size(max = 20, message = "설명은 1글자 이상 20글자 이하여야 합니다.")
String description,

@NotNull(message = "birthDate는 빈 문자열이나 null을 입력할 수 없습니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.happy.friendogly.pet.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;

public record UpdatePetRequest(
@NotBlank(message = "name은 빈 문자열이나 null을 입력할 수 없습니다.")
@Size(max = 8, message = "이름은 1글자 이상 8글자 이하여야 합니다.")
String name,

@NotBlank(message = "description은 빈 문자열이나 null을 입력할 수 없습니다.")
@Size(max = 20, message = "설명은 1글자 이상 20글자 이하여야 합니다.")
String description,

@NotNull(message = "birthDate는 빈 문자열이나 null을 입력할 수 없습니다.")
@PastOrPresent(message = "birthDate는 현재 날짜와 같거나 이전이어야 합니다.")
LocalDate birthDate,

@NotBlank(message = "sizeType은 빈 문자열이나 null을 입력할 수 없습니다.")
String sizeType,

@NotBlank(message = "gender는 빈 문자열이나 null을 입력할 수 없습니다.")
String gender,

@NotBlank(message = "imageUpdateType는 빈 문자열이나 null을 입력할 수 없습니다.")
String imageUpdateType
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import com.happy.friendogly.exception.FriendoglyException;
import com.happy.friendogly.infra.FileStorageManager;
import com.happy.friendogly.infra.ImageUpdateType;
import com.happy.friendogly.member.domain.Member;
import com.happy.friendogly.member.repository.MemberRepository;
import com.happy.friendogly.pet.domain.Gender;
import com.happy.friendogly.pet.domain.Pet;
import com.happy.friendogly.pet.domain.SizeType;
import com.happy.friendogly.pet.dto.request.SavePetRequest;
import com.happy.friendogly.pet.dto.request.UpdatePetRequest;
import com.happy.friendogly.pet.dto.response.SavePetResponse;
import com.happy.friendogly.pet.repository.PetRepository;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -64,4 +66,41 @@ private void validatePetCapacity(Long size) {
"강아지는 최대 %d 마리까지만 등록할 수 있습니다.", MAX_PET_CAPACITY));
}
}

public void update(Long memberId, Long petId, UpdatePetRequest request, MultipartFile image) {
Member member = memberRepository.getById(memberId);
Pet pet = petRepository.getById(petId);

if (!pet.isOwner(member)) {
throw new FriendoglyException("자신의 강아지만 수정할 수 있습니다.");
}

ImageUpdateType imageUpdateType = ImageUpdateType.from(request.imageUpdateType());

String oldImageUrl = pet.getImageUrl();
String newImageUrl = "";

if (imageUpdateType == ImageUpdateType.UPDATE) {
// TODO: 기존 이미지 S3에서 삭제
newImageUrl = fileStorageManager.uploadFile(image);
}

if (imageUpdateType == ImageUpdateType.NOT_UPDATE) {
newImageUrl = oldImageUrl;
}

if (imageUpdateType == ImageUpdateType.DELETE) {
// TODO: 기존 이미지 S3에서 삭제
newImageUrl = "";
}

pet.update(
request.name(),
request.description(),
request.birthDate(),
request.sizeType(),
request.gender(),
newImageUrl
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.happy.friendogly.pet.domain.Gender;
import com.happy.friendogly.pet.domain.SizeType;
import com.happy.friendogly.pet.dto.request.SavePetRequest;
import com.happy.friendogly.pet.dto.request.UpdatePetRequest;
import com.happy.friendogly.pet.dto.response.FindPetResponse;
import com.happy.friendogly.pet.dto.response.SavePetResponse;
import com.happy.friendogly.pet.service.PetCommandService;
Expand All @@ -28,10 +29,12 @@
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;

public class PetApiDocsTest extends RestDocsTest {

Expand Down Expand Up @@ -305,6 +308,101 @@ void findByMemberId_Success() throws Exception {
);
}

@DisplayName("반려견 정보 업데이트 문서화")
@Test
void update_Success() throws Exception {
// MultipartFile + PATCH Method 같이 사용하기 위한 코드
MockMultipartHttpServletRequestBuilder patchBuilder
= RestDocumentationRequestBuilders.multipart("/pets/{id}", 1L);
patchBuilder.with(request -> {
request.setMethod(HttpMethod.PATCH.name());
return request;
});

Mockito.doNothing()
.when(petCommandService)
.update(any(), any(), any(), any());

UpdatePetRequest requestDto = new UpdatePetRequest(
"도토리",
"도토리 설명",
LocalDate.now().minusYears(1),
"SMALL",
"MALE",
"UPDATE"
);

MockMultipartFile image = new MockMultipartFile(
"image", "image.jpg", MediaType.MULTIPART_FORM_DATA.toString(), "asdf".getBytes());
MockMultipartFile request = new MockMultipartFile(
"request", "request", MediaType.APPLICATION_JSON_VALUE, objectMapper.writeValueAsBytes(requestDto));

mockMvc.perform(patchBuilder
.file(image)
.file(request)
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, getMemberToken()))
.andExpect(status().isOk())
.andDo(MockMvcRestDocumentationWrapper.document("pet-update-200",
getDocumentRequest(),
getDocumentResponse(),
requestParts(
partWithName("image").description("강아지 프로필 이미지 파일"),
partWithName("request").description("강아지 등록 정보")
),
requestPartFields(
"request",
fieldWithPath("name").type(JsonFieldType.STRING)
.description("반려견 이름"),
fieldWithPath("description").type(JsonFieldType.STRING)
.description("반려견 한 줄 소개"),
fieldWithPath("birthDate").type(JsonFieldType.STRING)
.description("반려견 생년월일: yyyy-MM-dd"),
fieldWithPath("sizeType").type(JsonFieldType.STRING)
.description("반려견 크기: SMALL, MEDIUM, LARGE"),
fieldWithPath("gender").type(JsonFieldType.STRING)
.description("반려견 성별: MALE, FEMALE, MALE_NEUTERED, FEMALE_NEUTERED"),
fieldWithPath("imageUpdateType").type(JsonFieldType.STRING)
.description("이미지 업데이트 여부: UPDATE, NOT_UPDATE, DELETE")
),
resource(ResourceSnippetParameters.builder()
.tag("Pet API")
.summary("""
반려견 정보 수정 API
요청 이미지
multipart/form-data "image" (null인 경우 이미지 변경을 하지 않음)
요청 데이터
application/json "request"
{
"name": "보리", // 변경할 강아지 이름
"description": "귀여운 보리입니다", // 변경할 강아지 설명
"birthDate": "2011-01-01", // 강아지 생일 yyyy-MM-dd
"sizeType": "SMALL", // 변경할 강아지 크기 (SMALL, MEDIUM, LARGE)
"gender": "MALE", // 변경할 반려견 성별 (MALE, FEMALE, MALE_NEUTERED, FEMALE_NEUTERED)
"imageUpdateType": "UPDATE" // 이미지 업데이트 여부 -> UPDATE(이미지 변경), NOT_UPDATE(이미지 변경 없음), DELETE(기본 이미지로 변경)
}
""")
.requestHeaders(
headerWithName(HttpHeaders.AUTHORIZATION).description("로그인한 회원의 accessToken")
)
.pathParameters(
parameterWithName("id").description("수정하려는 Pet ID"))
.build()))
);
}

@Override
protected Object controller() {
return new PetController(petQueryService, petCommandService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ void savePet() {
.statusCode(HttpStatus.CREATED.value());
}

@DisplayName("닉네임 길이가 15자를 초과하는 경우 400을 반환한다.")
@DisplayName("닉네임 길이가 8자를 초과하는 경우 400을 반환한다.")
@Test
void savePet_Fail_NameLengthOver() {
SavePetRequest request = new SavePetRequest(
"1234567890123456",
"123456789",
"땡이입니다.",
LocalDate.now().minusDays(1L),
"SMALL",
Expand All @@ -82,12 +82,12 @@ void savePet_Fail_NameLengthOver() {
.statusCode(HttpStatus.BAD_REQUEST.value());
}

@DisplayName("한 줄 설명의 길이가 15자를 초과하는 경우 400을 반환한다.")
@DisplayName("한 줄 설명의 길이가 20자를 초과하는 경우 400을 반환한다.")
@Test
void savePet_Fail_DescriptionLengthOver() {
SavePetRequest request = new SavePetRequest(
"땡이",
"1234567890123456",
"123456789012345678901",
LocalDate.now().minusDays(1L),
"SMALL",
"FEMALE_NEUTERED",
Expand Down
Loading

0 comments on commit e1b3fa0

Please sign in to comment.