From 0bc38be37e10d7bf2fcee89898994ea033a5c556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Sun, 1 Dec 2024 18:31:19 +0900 Subject: [PATCH 01/10] =?UTF-8?q?refactor(VolunteerProfile):=20=EB=B4=89?= =?UTF-8?q?=EC=82=AC=EC=9E=90=20=ED=94=84=EB=A1=9C=ED=95=84=EC=9D=84=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...a => VolunteerProfileQueryController.java} | 10 +++---- ....java => VolunteerProfileResponseDto.java} | 26 +++++++++---------- .../service/VolunteerQueryService.java | 14 +++++----- .../usecase/VolunteerQueryUseCase.java | 8 +++--- .../service/VolunteerQueryServiceTest.java | 8 +++--- 5 files changed, 33 insertions(+), 33 deletions(-) rename src/main/java/com/somemore/volunteer/controller/{VolunteerQueryController.java => VolunteerProfileQueryController.java} (87%) rename src/main/java/com/somemore/volunteer/dto/response/{VolunteerResponseDto.java => VolunteerProfileResponseDto.java} (80%) diff --git a/src/main/java/com/somemore/volunteer/controller/VolunteerQueryController.java b/src/main/java/com/somemore/volunteer/controller/VolunteerProfileQueryController.java similarity index 87% rename from src/main/java/com/somemore/volunteer/controller/VolunteerQueryController.java rename to src/main/java/com/somemore/volunteer/controller/VolunteerProfileQueryController.java index 3f9a1043a..7ff1459c8 100644 --- a/src/main/java/com/somemore/volunteer/controller/VolunteerQueryController.java +++ b/src/main/java/com/somemore/volunteer/controller/VolunteerProfileQueryController.java @@ -1,7 +1,7 @@ package com.somemore.volunteer.controller; import com.somemore.global.common.response.ApiResponse; -import com.somemore.volunteer.dto.response.VolunteerResponseDto; +import com.somemore.volunteer.dto.response.VolunteerProfileResponseDto; import com.somemore.volunteer.usecase.VolunteerQueryUseCase; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -21,14 +21,14 @@ @RequiredArgsConstructor @RequestMapping("/api/profile") @Tag(name = "GET Volunteer", description = "봉사자 조회") -public class VolunteerQueryController { +public class VolunteerProfileQueryController { private final VolunteerQueryUseCase volunteerQueryUseCase; @Operation(summary = "본인 상세 프로필 조회", description = "현재 로그인된 사용자의 상세 프로필을 조회합니다.") @Secured("ROLE_VOLUNTEER") @GetMapping("/me") - public ApiResponse getMyProfile( + public ApiResponse getMyProfile( @AuthenticationPrincipal String volunteerId) { return ApiResponse.ok( @@ -39,7 +39,7 @@ public ApiResponse getMyProfile( @GetMapping("/{volunteerId}") @Operation(summary = "타인 프로필 조회", description = "특정 봉사자의 프로필을 조회합니다. 상세 정보는 포함되지 않습니다.") - public ApiResponse getVolunteerProfile( + public ApiResponse getVolunteerProfile( @PathVariable UUID volunteerId) { return ApiResponse.ok( @@ -52,7 +52,7 @@ public ApiResponse getVolunteerProfile( @GetMapping("/{volunteerId}/detailed") @Secured("ROLE_CENTER") @Operation(summary = "지원자 상세 프로필 조회", description = "기관이 작성한 모집 글에 지원한 봉사자의 상세 프로필을 조회합니다.") - public ApiResponse getVolunteerDetailedProfile( + public ApiResponse getVolunteerDetailedProfile( @PathVariable UUID volunteerId, @AuthenticationPrincipal String centerId) { diff --git a/src/main/java/com/somemore/volunteer/dto/response/VolunteerResponseDto.java b/src/main/java/com/somemore/volunteer/dto/response/VolunteerProfileResponseDto.java similarity index 80% rename from src/main/java/com/somemore/volunteer/dto/response/VolunteerResponseDto.java rename to src/main/java/com/somemore/volunteer/dto/response/VolunteerProfileResponseDto.java index 9884804b9..2ed915645 100644 --- a/src/main/java/com/somemore/volunteer/dto/response/VolunteerResponseDto.java +++ b/src/main/java/com/somemore/volunteer/dto/response/VolunteerProfileResponseDto.java @@ -7,8 +7,8 @@ import io.swagger.v3.oas.annotations.media.Schema; @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -@Schema(description = "봉사자 응답 DTO") -public record VolunteerResponseDto( +@Schema(description = "봉사자 프로필 응답 DTO") +public record VolunteerProfileResponseDto( @Schema(description = "봉사자 ID", example = "123e4567-e89b-12d3-a456-426614174000") String volunteerId, @@ -30,15 +30,15 @@ public record VolunteerResponseDto( @Schema(description = "총 봉사 횟수", example = "20") Integer totalVolunteerCount, - @Schema(description = "봉사자 상세 정보", implementation = VolunteerDetailResponseDto.class) - VolunteerDetailResponseDto volunteerDetailResponseDto + @Schema(description = "봉사자 상세 정보", implementation = VolunteerDetailProfileResponseDto.class) + VolunteerDetailProfileResponseDto volunteerDetailProfileResponseDto ) { - public static VolunteerResponseDto from( + public static VolunteerProfileResponseDto from( Volunteer volunteer, VolunteerDetail volunteerDetail ) { - return new VolunteerResponseDto( + return new VolunteerProfileResponseDto( volunteer.getId().toString(), volunteer.getNickname(), volunteer.getImgUrl(), @@ -46,14 +46,14 @@ public static VolunteerResponseDto from( volunteer.getTier().name(), volunteer.getTotalVolunteerHours(), volunteer.getTotalVolunteerCount(), - VolunteerDetailResponseDto.from(volunteerDetail) + VolunteerDetailProfileResponseDto.from(volunteerDetail) ); } - public static VolunteerResponseDto from( + public static VolunteerProfileResponseDto from( Volunteer volunteer ) { - return new VolunteerResponseDto( + return new VolunteerProfileResponseDto( volunteer.getId().toString(), volunteer.getNickname(), volunteer.getImgUrl(), @@ -66,8 +66,8 @@ public static VolunteerResponseDto from( } @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - @Schema(description = "봉사자 상세 응답 DTO") - private record VolunteerDetailResponseDto( + @Schema(description = "봉사자 상세 프로필 응답 DTO") + private record VolunteerDetailProfileResponseDto( @Schema(description = "이름", example = "홍길동") String name, @@ -83,10 +83,10 @@ private record VolunteerDetailResponseDto( @Schema(description = "연락처", example = "010-1234-5678") String contactNumber ) { - public static VolunteerDetailResponseDto from( + public static VolunteerDetailProfileResponseDto from( VolunteerDetail volunteerDetail ) { - return new VolunteerDetailResponseDto( + return new VolunteerDetailProfileResponseDto( volunteerDetail.getName(), volunteerDetail.getEmail(), volunteerDetail.getGender().name(), diff --git a/src/main/java/com/somemore/volunteer/service/VolunteerQueryService.java b/src/main/java/com/somemore/volunteer/service/VolunteerQueryService.java index 5e69dddbc..4e57e2bef 100644 --- a/src/main/java/com/somemore/volunteer/service/VolunteerQueryService.java +++ b/src/main/java/com/somemore/volunteer/service/VolunteerQueryService.java @@ -4,7 +4,7 @@ import com.somemore.global.exception.BadRequestException; import com.somemore.volunteer.domain.Volunteer; import com.somemore.volunteer.domain.VolunteerDetail; -import com.somemore.volunteer.dto.response.VolunteerResponseDto; +import com.somemore.volunteer.dto.response.VolunteerProfileResponseDto; import com.somemore.volunteer.repository.VolunteerDetailRepository; import com.somemore.volunteer.repository.VolunteerRepository; import com.somemore.volunteer.usecase.VolunteerQueryUseCase; @@ -28,27 +28,27 @@ public class VolunteerQueryService implements VolunteerQueryUseCase { private final VolunteerDetailAccessValidator volunteerDetailAccessValidator; @Override - public VolunteerResponseDto getMyProfile(UUID volunteerId) { + public VolunteerProfileResponseDto getMyProfile(UUID volunteerId) { - return VolunteerResponseDto.from( + return VolunteerProfileResponseDto.from( findVolunteer(volunteerId), findVolunteerDetail(volunteerId) ); } @Override - public VolunteerResponseDto getVolunteerProfile(UUID volunteerId) { + public VolunteerProfileResponseDto getVolunteerProfile(UUID volunteerId) { - return VolunteerResponseDto.from( + return VolunteerProfileResponseDto.from( findVolunteer(volunteerId) ); } @Override - public VolunteerResponseDto getVolunteerDetailedProfile(UUID volunteerId, UUID centerId) { + public VolunteerProfileResponseDto getVolunteerDetailedProfile(UUID volunteerId, UUID centerId) { volunteerDetailAccessValidator.validateByCenterId(centerId, volunteerId); - return VolunteerResponseDto.from( + return VolunteerProfileResponseDto.from( findVolunteer(volunteerId), findVolunteerDetail(volunteerId) ); diff --git a/src/main/java/com/somemore/volunteer/usecase/VolunteerQueryUseCase.java b/src/main/java/com/somemore/volunteer/usecase/VolunteerQueryUseCase.java index e41b69be6..18b00a044 100644 --- a/src/main/java/com/somemore/volunteer/usecase/VolunteerQueryUseCase.java +++ b/src/main/java/com/somemore/volunteer/usecase/VolunteerQueryUseCase.java @@ -1,16 +1,16 @@ package com.somemore.volunteer.usecase; -import com.somemore.volunteer.dto.response.VolunteerResponseDto; +import com.somemore.volunteer.dto.response.VolunteerProfileResponseDto; import java.util.UUID; public interface VolunteerQueryUseCase { - VolunteerResponseDto getMyProfile(UUID volunteerId); + VolunteerProfileResponseDto getMyProfile(UUID volunteerId); - VolunteerResponseDto getVolunteerProfile(UUID volunteerId); + VolunteerProfileResponseDto getVolunteerProfile(UUID volunteerId); - VolunteerResponseDto getVolunteerDetailedProfile(UUID volunteerId, UUID centerId); + VolunteerProfileResponseDto getVolunteerDetailedProfile(UUID volunteerId, UUID centerId); UUID getVolunteerIdByOAuthId(String oAuthId); diff --git a/src/test/java/com/somemore/volunteer/service/VolunteerQueryServiceTest.java b/src/test/java/com/somemore/volunteer/service/VolunteerQueryServiceTest.java index 25fc6cbdb..ec0f6dbea 100644 --- a/src/test/java/com/somemore/volunteer/service/VolunteerQueryServiceTest.java +++ b/src/test/java/com/somemore/volunteer/service/VolunteerQueryServiceTest.java @@ -6,7 +6,7 @@ import com.somemore.volunteer.domain.Volunteer; import com.somemore.volunteer.domain.VolunteerDetail; import com.somemore.volunteer.dto.request.VolunteerRegisterRequestDto; -import com.somemore.volunteer.dto.response.VolunteerResponseDto; +import com.somemore.volunteer.dto.response.VolunteerProfileResponseDto; import com.somemore.volunteer.repository.VolunteerDetailRepository; import com.somemore.volunteer.repository.VolunteerRepository; import org.junit.jupiter.api.DisplayName; @@ -103,7 +103,7 @@ void getMyProfile() { volunteerDetailRepository.save(volunteerDetail); // when - VolunteerResponseDto response = volunteerQueryService.getMyProfile(volunteerId); + VolunteerProfileResponseDto response = volunteerQueryService.getMyProfile(volunteerId); // then assertThat(response).isNotNull(); @@ -123,13 +123,13 @@ void getVolunteerProfile() { volunteerDetailRepository.save(volunteerDetail); // when - VolunteerResponseDto response = volunteerQueryService.getVolunteerProfile(volunteerId); + VolunteerProfileResponseDto response = volunteerQueryService.getVolunteerProfile(volunteerId); // then assertThat(response).isNotNull(); assertThat(response.volunteerId()).isEqualTo(volunteerId.toString()); assertThat(response.nickname()).isEqualTo(volunteer.getNickname()); - assertThat(response.volunteerDetailResponseDto()).isNull(); + assertThat(response.volunteerDetailProfileResponseDto()).isNull(); } @DisplayName("권한이 없는 기관의 봉사자 상세 프로필 조회 실패") From 8d674f82c0b4e47cf2fc9edffbb92606768c72a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Sun, 1 Dec 2024 21:14:01 +0900 Subject: [PATCH 02/10] =?UTF-8?q?build(gradle):=20validation=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 494c337f5..4ba8eef76 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ dependencies { // Data Layer: JPA, Redis, Database Drivers implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-validation' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' From 84b18beff55d60404c79f3a181322951bb410eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Sun, 1 Dec 2024 21:14:52 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat(GlobalExceptionHandler):=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EC=98=88=EC=99=B8=EB=A5=BC=20=EC=A0=84?= =?UTF-8?q?=EC=97=AD=20=EC=98=88=EC=99=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC?= =?UTF-8?q?=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Problem Detail 사용. --- .../global/handler/GlobalExceptionHandler.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java b/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java index 48fb90dfc..d734b3d3d 100644 --- a/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java @@ -5,12 +5,12 @@ import com.somemore.global.exception.ImageUploadException; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @RestControllerAdvice -public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { +public class GlobalExceptionHandler { //예시 코드 @ExceptionHandler(BadRequestException.class) @@ -45,4 +45,15 @@ ProblemDetail handleDuplicateException(final DuplicateException e) { return problemDetail; } + @ExceptionHandler(MethodArgumentNotValidException.class) + ProblemDetail handleMethodArgumentNotValid(final MethodArgumentNotValidException e) { + + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); + + problemDetail.setTitle("유효성 예외"); + problemDetail.setDetail("입력 데이터 유효성 검사가 실패했습니다. 각 필드를 확인해주세요."); + + return problemDetail; + } + } From 6a5a28690ae508a2be8419cae8ff36773145c362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Sun, 1 Dec 2024 21:15:50 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat(Volunteer):=20update=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 닉네임과 소개 필드로 구성된 RequestDto와 ImgUrl로 프로필 업데이트 - Dto의 각 필드 유효한 값인지 검사 --- .../somemore/volunteer/domain/Volunteer.java | 7 ++++++ .../VolunteerProfileUpdateRequestDto.java | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/main/java/com/somemore/volunteer/dto/request/VolunteerProfileUpdateRequestDto.java diff --git a/src/main/java/com/somemore/volunteer/domain/Volunteer.java b/src/main/java/com/somemore/volunteer/domain/Volunteer.java index a8125c9c7..e98872fa1 100644 --- a/src/main/java/com/somemore/volunteer/domain/Volunteer.java +++ b/src/main/java/com/somemore/volunteer/domain/Volunteer.java @@ -2,6 +2,7 @@ import com.somemore.auth.oauth.OAuthProvider; import com.somemore.global.common.BaseEntity; +import com.somemore.volunteer.dto.request.VolunteerProfileUpdateRequestDto; import jakarta.persistence.*; import lombok.*; @@ -58,6 +59,12 @@ public static Volunteer createDefault(OAuthProvider oauthProvider, String oauthI .build(); } + public void updateWith(VolunteerProfileUpdateRequestDto dto, String imgUrl) { + this.nickname = dto.nickname(); + this.introduce = dto.introduce(); + this.imgUrl = imgUrl; + } + @Builder private Volunteer( OAuthProvider oauthProvider, diff --git a/src/main/java/com/somemore/volunteer/dto/request/VolunteerProfileUpdateRequestDto.java b/src/main/java/com/somemore/volunteer/dto/request/VolunteerProfileUpdateRequestDto.java new file mode 100644 index 000000000..88e480ac4 --- /dev/null +++ b/src/main/java/com/somemore/volunteer/dto/request/VolunteerProfileUpdateRequestDto.java @@ -0,0 +1,24 @@ +package com.somemore.volunteer.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Builder; + +@Builder +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record VolunteerProfileUpdateRequestDto( + + @Schema(description = "봉사자 닉네임", example = "making") + @NotBlank(message = "닉네임은 필수 값입니다.") + @Size(max = 10, message = "닉네임은 최대 10자까지 입력 가능합니다.") + String nickname, + + @Schema(description = "봉사자 소개글", example = "저는 다양한 봉사활동에 관심이 많은 봉사자입니다.") + @NotBlank(message = "소개글은 필수 값입니다.") + @Size(max = 100, message = "소개글은 최대 100자까지 입력 가능합니다.") + String introduce +) { +} \ No newline at end of file From 80a5e62bb6578cc281f59e1d9a1cfd734bfebc2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Sun, 1 Dec 2024 21:18:25 +0900 Subject: [PATCH 05/10] feat(UpdateVolunteerProfile): update service/usecase --- .../UpdateVolunteerProfileService.java | 33 +++++++++++++++++++ .../UpdateVolunteerProfileUseCase.java | 10 ++++++ 2 files changed, 43 insertions(+) create mode 100644 src/main/java/com/somemore/volunteer/service/UpdateVolunteerProfileService.java create mode 100644 src/main/java/com/somemore/volunteer/usecase/UpdateVolunteerProfileUseCase.java diff --git a/src/main/java/com/somemore/volunteer/service/UpdateVolunteerProfileService.java b/src/main/java/com/somemore/volunteer/service/UpdateVolunteerProfileService.java new file mode 100644 index 000000000..fd2dd5074 --- /dev/null +++ b/src/main/java/com/somemore/volunteer/service/UpdateVolunteerProfileService.java @@ -0,0 +1,33 @@ +package com.somemore.volunteer.service; + +import com.somemore.global.exception.BadRequestException; +import com.somemore.volunteer.domain.Volunteer; +import com.somemore.volunteer.dto.request.VolunteerProfileUpdateRequestDto; +import com.somemore.volunteer.repository.VolunteerRepository; +import com.somemore.volunteer.usecase.UpdateVolunteerProfileUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_VOLUNTEER; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class UpdateVolunteerProfileService implements UpdateVolunteerProfileUseCase { + + private final VolunteerRepository volunteerRepository; + + @Override + public void update(UUID volunteerId, VolunteerProfileUpdateRequestDto requestDto, String imgUrl) { + Volunteer volunteer = volunteerRepository.findById(volunteerId) + .orElseThrow(() -> new BadRequestException(NOT_EXISTS_VOLUNTEER)); + + volunteer.updateWith(requestDto, imgUrl); + } + +} diff --git a/src/main/java/com/somemore/volunteer/usecase/UpdateVolunteerProfileUseCase.java b/src/main/java/com/somemore/volunteer/usecase/UpdateVolunteerProfileUseCase.java new file mode 100644 index 000000000..330cd502e --- /dev/null +++ b/src/main/java/com/somemore/volunteer/usecase/UpdateVolunteerProfileUseCase.java @@ -0,0 +1,10 @@ +package com.somemore.volunteer.usecase; + +import com.somemore.volunteer.dto.request.VolunteerProfileUpdateRequestDto; + +import java.util.UUID; + +public interface UpdateVolunteerProfileUseCase { + + void update(UUID volunteerId, VolunteerProfileUpdateRequestDto requestDto, String imgUrl); +} From 74b535f44ea0abced61faf5b18b37b1877bef375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Sun, 1 Dec 2024 21:19:39 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat(VolunteerProfileCommandController):?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VolunteerProfileCommandController.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/main/java/com/somemore/volunteer/controller/VolunteerProfileCommandController.java diff --git a/src/main/java/com/somemore/volunteer/controller/VolunteerProfileCommandController.java b/src/main/java/com/somemore/volunteer/controller/VolunteerProfileCommandController.java new file mode 100644 index 000000000..dfa9fae6b --- /dev/null +++ b/src/main/java/com/somemore/volunteer/controller/VolunteerProfileCommandController.java @@ -0,0 +1,53 @@ +package com.somemore.volunteer.controller; + +import com.somemore.global.common.response.ApiResponse; +import com.somemore.imageupload.dto.ImageUploadRequestDto; +import com.somemore.imageupload.usecase.ImageUploadUseCase; +import com.somemore.volunteer.dto.request.VolunteerProfileUpdateRequestDto; +import com.somemore.volunteer.usecase.UpdateVolunteerProfileUseCase; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PutMapping; +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 java.util.UUID; + +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; + +@RestController +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/api/profile") +@Tag(name = "PUT Volunteer", description = "봉사자 프로필 수정") +public class VolunteerProfileCommandController { + + private final UpdateVolunteerProfileUseCase updateVolunteerProfileUseCase; + private final ImageUploadUseCase imageUploadUseCase; + + @Secured("ROLE_VOLUNTEER") + @Operation(summary = "프로필 수정", description = "현재 로그인된 사용자의 프로필을 수정합니다.") + @PutMapping(consumes = MULTIPART_FORM_DATA_VALUE) + public ApiResponse updateProfile( + @AuthenticationPrincipal String volunteerId, + @Valid @RequestPart("data") VolunteerProfileUpdateRequestDto requestDto, + @RequestPart(value = "img_file", required = false) MultipartFile image) { + + String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image)); + + updateVolunteerProfileUseCase.update( + UUID.fromString(volunteerId), + requestDto, + imgUrl + ); + + return ApiResponse.ok("프로필 수정 성공"); + } +} From 9c15bec0ad44c693aaf74f9b0bbf51892cc530d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Sun, 1 Dec 2024 21:20:23 +0900 Subject: [PATCH 07/10] =?UTF-8?q?test(UpdateVolunteerProfileService):=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UpdateVolunteerProfileServiceTest.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/test/java/com/somemore/volunteer/service/UpdateVolunteerProfileServiceTest.java diff --git a/src/test/java/com/somemore/volunteer/service/UpdateVolunteerProfileServiceTest.java b/src/test/java/com/somemore/volunteer/service/UpdateVolunteerProfileServiceTest.java new file mode 100644 index 000000000..e7b84d27d --- /dev/null +++ b/src/test/java/com/somemore/volunteer/service/UpdateVolunteerProfileServiceTest.java @@ -0,0 +1,64 @@ +package com.somemore.volunteer.service; + +import com.somemore.IntegrationTestSupport; +import com.somemore.auth.oauth.OAuthProvider; +import com.somemore.volunteer.domain.Volunteer; +import com.somemore.volunteer.dto.request.VolunteerProfileUpdateRequestDto; +import com.somemore.volunteer.repository.VolunteerRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_VOLUNTEER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Transactional +class UpdateVolunteerProfileServiceTest extends IntegrationTestSupport { + + @Autowired + private UpdateVolunteerProfileService updateVolunteerProfileService; + + @Autowired + private VolunteerRepository volunteerRepository; + + final String oAuthId = "example-oauth-id"; + final OAuthProvider oAuthProvider = OAuthProvider.NAVER; + final String imgUrl = "http://example.com/updated-image.jpg"; + final VolunteerProfileUpdateRequestDto requestDto = new VolunteerProfileUpdateRequestDto( + "Updated Nickname", + "Updated Introduction" + ); + + + @Test + @DisplayName("봉사자 프로필을 성공적으로 업데이트한다") + void updateVolunteerProfileSuccess() { + // given + Volunteer volunteer = Volunteer.createDefault(oAuthProvider, oAuthId); + volunteerRepository.save(volunteer); + + // when + updateVolunteerProfileService.update(volunteer.getId(), requestDto, imgUrl); + + // then + Volunteer updatedVolunteer = volunteerRepository.findById(volunteer.getId()).orElseThrow(); + assertThat(updatedVolunteer.getNickname()).isEqualTo(requestDto.nickname()); + assertThat(updatedVolunteer.getIntroduce()).isEqualTo(requestDto.introduce()); + assertThat(updatedVolunteer.getImgUrl()).isEqualTo(imgUrl); + } + + @Test + @DisplayName("존재하지 않는 봉사자 ID로 업데이트 시 예외를 던진다") + void updateVolunteerProfileThrowsWhenNotFound() { + // given + // when + // then + assertThatThrownBy(() -> updateVolunteerProfileService.update(UUID.randomUUID(), requestDto, imgUrl)) + .isInstanceOf(com.somemore.global.exception.BadRequestException.class) + .hasMessage(NOT_EXISTS_VOLUNTEER.getMessage()); + } +} \ No newline at end of file From 8ce6885150870e84789fb4ad78c6a74ff4d8fc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Sun, 1 Dec 2024 21:26:53 +0900 Subject: [PATCH 08/10] =?UTF-8?q?test(VolunteerProfileCommandController):?= =?UTF-8?q?=20=EC=9E=85=EB=A0=A5=20=EA=B0=92=20=EC=9C=A0=ED=9A=A8=EC=84=B1?= =?UTF-8?q?=20=EA=B2=80=EC=82=AC=EB=A5=BC=20=ED=8F=AC=ED=95=A8=ED=95=9C=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=ED=86=A8=EB=9F=AC=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...VolunteerProfileCommandControllerTest.java | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/test/java/com/somemore/volunteer/controller/VolunteerProfileCommandControllerTest.java diff --git a/src/test/java/com/somemore/volunteer/controller/VolunteerProfileCommandControllerTest.java b/src/test/java/com/somemore/volunteer/controller/VolunteerProfileCommandControllerTest.java new file mode 100644 index 000000000..dca6992bc --- /dev/null +++ b/src/test/java/com/somemore/volunteer/controller/VolunteerProfileCommandControllerTest.java @@ -0,0 +1,182 @@ +package com.somemore.volunteer.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.somemore.ControllerTestSupport; +import com.somemore.WithMockCustomUser; +import com.somemore.imageupload.usecase.ImageUploadUseCase; +import com.somemore.volunteer.dto.request.VolunteerProfileUpdateRequestDto; +import com.somemore.volunteer.usecase.UpdateVolunteerProfileUseCase; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class VolunteerProfileCommandControllerTest extends ControllerTestSupport { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private UpdateVolunteerProfileUseCase updateVolunteerProfileUseCase; + + @MockBean + private ImageUploadUseCase imageUploadUseCase; + + @DisplayName("봉사자 프로필 수정 성공 테스트") + @Test + @WithMockCustomUser(role = "VOLUNTEER") + void updateVolunteerProfile() throws Exception { + // given + VolunteerProfileUpdateRequestDto requestDto = VolunteerProfileUpdateRequestDto.builder() + .nickname("making") + .introduce("making is making") + .build(); + + MockMultipartFile imageFile = createMockImageFile(); + MockMultipartFile requestData = createMockRequestData(requestDto); + + String mockImageUrl = "http://example.com/image/profile-image.jpg"; + + given(imageUploadUseCase.uploadImage(any())).willReturn(mockImageUrl); + willDoNothing().given(updateVolunteerProfileUseCase) + .update(any(UUID.class), any(VolunteerProfileUpdateRequestDto.class), anyString()); + + MockMultipartHttpServletRequestBuilder builder = createMockRequestBuilder(); + + // when + mockMvc.perform(builder + .file(requestData) + .file(imageFile) + .contentType(MULTIPART_FORM_DATA) + .header("Authorization", "Bearer access-token")) + // then + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isEmpty()) + .andExpect(jsonPath("$.message").value("프로필 수정 성공")); + } + + @DisplayName("봉사자 프로필 수정의 닉네임 유효성 검사 실패 테스트") + @Test + @WithMockCustomUser(role = "VOLUNTEER") + void testNicknameValidation() throws Exception { + // given + VolunteerProfileUpdateRequestDto invalidRequestDto = VolunteerProfileUpdateRequestDto.builder() + .nickname("TooLongNickname") // 10자를 초과 + .introduce("Valid introduction") + .build(); + + MockMultipartFile imageFile = createMockImageFile(); + MockMultipartFile requestData = createMockRequestData(invalidRequestDto); + + MockMultipartHttpServletRequestBuilder builder = createMockRequestBuilder(); + + // when + mockMvc.perform(builder + .file(requestData) + .file(imageFile) + .contentType(MULTIPART_FORM_DATA) + .header("Authorization", "Bearer access-token")) + .andExpect(status().isBadRequest()) + .andDo(result -> { + String responseBody = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + + Map jsonResponse = objectMapper.readValue(responseBody, new TypeReference<>() { + }); + String detail = (String) jsonResponse.get("detail"); + + assertThat(detail).isEqualTo("입력 데이터 유효성 검사가 실패했습니다. 각 필드를 확인해주세요."); + }); + } + + @DisplayName("봉사자 프로필 수정의 소개 유효성 검사 실패 테스트") + @Test + @WithMockCustomUser(role = "VOLUNTEER") + void testIntroduceValidation() throws Exception { + // given + VolunteerProfileUpdateRequestDto invalidRequestDto = VolunteerProfileUpdateRequestDto.builder() + .nickname("making") + .introduce(""" + TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_ + TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_ + TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_ + TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_ + TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_ + TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_TOOLONG_ + """) + .build(); + + MockMultipartFile imageFile = createMockImageFile(); + MockMultipartFile requestData = createMockRequestData(invalidRequestDto); + + MockMultipartHttpServletRequestBuilder builder = createMockRequestBuilder(); + + // when + mockMvc.perform(builder + .file(requestData) + .file(imageFile) + .contentType(MULTIPART_FORM_DATA) + .header("Authorization", "Bearer access-token")) + .andExpect(status().isBadRequest()) + .andDo(result -> { + String responseBody = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + + Map jsonResponse = objectMapper.readValue(responseBody, new TypeReference<>() { + }); + String detail = (String) jsonResponse.get("detail"); + + assertThat(detail).isEqualTo("입력 데이터 유효성 검사가 실패했습니다. 각 필드를 확인해주세요."); + }); + } + + private MockMultipartFile createMockImageFile() { + return new MockMultipartFile( + "img_file", + "profile-image.jpg", + MediaType.IMAGE_JPEG_VALUE, + "profile image content".getBytes() + ); + } + + private MockMultipartFile createMockRequestData(Object dto) throws Exception { + return new MockMultipartFile( + "data", + "", + MediaType.APPLICATION_JSON_VALUE, + objectMapper.writeValueAsBytes(dto) + ); + } + + private MockMultipartHttpServletRequestBuilder createMockRequestBuilder() { + MockMultipartHttpServletRequestBuilder builder = multipart("/api/profile"); + builder.with(request -> { + request.setMethod("PUT"); + return request; + }); + return builder; + } +} \ No newline at end of file From 0801d5e6a720f2420c35535d63f182e85be64982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:45:10 +0900 Subject: [PATCH 09/10] =?UTF-8?q?refactor(VolunteerProfileResponseDto):=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20private=20record=EB=A5=BC=20VolunteerDetai?= =?UTF-8?q?lProfileResponseDto=EC=97=90=EC=84=9C=20'detail'=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API 명세에 맞게 응답 값을 수정 - 내부 private record 이므로 과감한 네이밍 개선 --- .../dto/response/VolunteerProfileResponseDto.java | 14 +++++++------- .../service/VolunteerQueryServiceTest.java | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/somemore/volunteer/dto/response/VolunteerProfileResponseDto.java b/src/main/java/com/somemore/volunteer/dto/response/VolunteerProfileResponseDto.java index 2ed915645..a2ee12dfa 100644 --- a/src/main/java/com/somemore/volunteer/dto/response/VolunteerProfileResponseDto.java +++ b/src/main/java/com/somemore/volunteer/dto/response/VolunteerProfileResponseDto.java @@ -30,8 +30,8 @@ public record VolunteerProfileResponseDto( @Schema(description = "총 봉사 횟수", example = "20") Integer totalVolunteerCount, - @Schema(description = "봉사자 상세 정보", implementation = VolunteerDetailProfileResponseDto.class) - VolunteerDetailProfileResponseDto volunteerDetailProfileResponseDto + @Schema(description = "봉사자 상세 정보", implementation = Detail.class) + Detail detail ) { public static VolunteerProfileResponseDto from( @@ -46,7 +46,7 @@ public static VolunteerProfileResponseDto from( volunteer.getTier().name(), volunteer.getTotalVolunteerHours(), volunteer.getTotalVolunteerCount(), - VolunteerDetailProfileResponseDto.from(volunteerDetail) + Detail.from(volunteerDetail) ); } @@ -66,8 +66,8 @@ public static VolunteerProfileResponseDto from( } @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - @Schema(description = "봉사자 상세 프로필 응답 DTO") - private record VolunteerDetailProfileResponseDto( + @Schema(description = "봉사자 상세 프로필") + private record Detail( @Schema(description = "이름", example = "홍길동") String name, @@ -83,10 +83,10 @@ private record VolunteerDetailProfileResponseDto( @Schema(description = "연락처", example = "010-1234-5678") String contactNumber ) { - public static VolunteerDetailProfileResponseDto from( + public static Detail from( VolunteerDetail volunteerDetail ) { - return new VolunteerDetailProfileResponseDto( + return new Detail( volunteerDetail.getName(), volunteerDetail.getEmail(), volunteerDetail.getGender().name(), diff --git a/src/test/java/com/somemore/volunteer/service/VolunteerQueryServiceTest.java b/src/test/java/com/somemore/volunteer/service/VolunteerQueryServiceTest.java index ec0f6dbea..000cfe1a5 100644 --- a/src/test/java/com/somemore/volunteer/service/VolunteerQueryServiceTest.java +++ b/src/test/java/com/somemore/volunteer/service/VolunteerQueryServiceTest.java @@ -129,7 +129,7 @@ void getVolunteerProfile() { assertThat(response).isNotNull(); assertThat(response.volunteerId()).isEqualTo(volunteerId.toString()); assertThat(response.nickname()).isEqualTo(volunteer.getNickname()); - assertThat(response.volunteerDetailProfileResponseDto()).isNull(); + assertThat(response.detail()).isNull(); } @DisplayName("권한이 없는 기관의 봉사자 상세 프로필 조회 실패") From bd13fb8d6767e8629d37b47f1ea290a3f4ad172e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Mon, 2 Dec 2024 02:33:46 +0900 Subject: [PATCH 10/10] =?UTF-8?q?test(VolunteerProfileCommandController):?= =?UTF-8?q?=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A3=BC=EC=9E=85=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상속받는 서포트 클래스에 존재하는 의존성 주입을 테스트 클래스에서 삭제 --- .../controller/VolunteerProfileCommandControllerTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/java/com/somemore/volunteer/controller/VolunteerProfileCommandControllerTest.java b/src/test/java/com/somemore/volunteer/controller/VolunteerProfileCommandControllerTest.java index dca6992bc..4372fae9b 100644 --- a/src/test/java/com/somemore/volunteer/controller/VolunteerProfileCommandControllerTest.java +++ b/src/test/java/com/somemore/volunteer/controller/VolunteerProfileCommandControllerTest.java @@ -33,9 +33,6 @@ class VolunteerProfileCommandControllerTest extends ControllerTestSupport { - @Autowired - private MockMvc mockMvc; - @Autowired private ObjectMapper objectMapper;