Skip to content

Commit f14f2f1

Browse files
authored
�feat(updateVolunteerProfile): 봉사자 프로필 수정 (#106)
* refactor(VolunteerProfile): 봉사자 프로필을 명시 * build(gradle): validation 의존성 추가 * feat(GlobalExceptionHandler): 유효성 예외를 전역 예외 핸들러로 처리 - Problem Detail 사용. * feat(Volunteer): update 추가 - 닉네임과 소개 필드로 구성된 RequestDto와 ImgUrl로 프로필 업데이트 - Dto의 각 필드 유효한 값인지 검사 * feat(UpdateVolunteerProfile): update service/usecase * feat(VolunteerProfileCommandController): 프로필 업데이트 컨트롤러 추가 * test(UpdateVolunteerProfileService): 업데이트 테스트 추가 * test(VolunteerProfileCommandController): 입력 값 유효성 검사를 포함한 컨트톨러 테스트 * refactor(VolunteerProfileResponseDto): 내부 private record를 VolunteerDetailProfileResponseDto에서 'detail'로 변경 - API 명세에 맞게 응답 값을 수정 - 내부 private record 이므로 과감한 네이밍 개선 * test(VolunteerProfileCommandController): 불필요한 의존성 주입 삭제 - 상속받는 서포트 클래스에 존재하는 의존성 주입을 테스트 클래스에서 삭제
1 parent 9aa37e8 commit f14f2f1

14 files changed

+417
-35
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies {
3434
// Data Layer: JPA, Redis, Database Drivers
3535
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
3636
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
37+
implementation 'org.springframework.boot:spring-boot-starter-validation'
3738
runtimeOnly 'com.h2database:h2'
3839
runtimeOnly 'com.mysql:mysql-connector-j'
3940

src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
import com.somemore.global.exception.ImageUploadException;
66
import org.springframework.http.HttpStatus;
77
import org.springframework.http.ProblemDetail;
8+
import org.springframework.web.bind.MethodArgumentNotValidException;
89
import org.springframework.web.bind.annotation.ExceptionHandler;
910
import org.springframework.web.bind.annotation.RestControllerAdvice;
10-
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
1111

1212
@RestControllerAdvice
13-
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
13+
public class GlobalExceptionHandler {
1414

1515
//예시 코드
1616
@ExceptionHandler(BadRequestException.class)
@@ -45,4 +45,15 @@ ProblemDetail handleDuplicateException(final DuplicateException e) {
4545
return problemDetail;
4646
}
4747

48+
@ExceptionHandler(MethodArgumentNotValidException.class)
49+
ProblemDetail handleMethodArgumentNotValid(final MethodArgumentNotValidException e) {
50+
51+
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());
52+
53+
problemDetail.setTitle("유효성 예외");
54+
problemDetail.setDetail("입력 데이터 유효성 검사가 실패했습니다. 각 필드를 확인해주세요.");
55+
56+
return problemDetail;
57+
}
58+
4859
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.somemore.volunteer.controller;
2+
3+
import com.somemore.global.common.response.ApiResponse;
4+
import com.somemore.imageupload.dto.ImageUploadRequestDto;
5+
import com.somemore.imageupload.usecase.ImageUploadUseCase;
6+
import com.somemore.volunteer.dto.request.VolunteerProfileUpdateRequestDto;
7+
import com.somemore.volunteer.usecase.UpdateVolunteerProfileUseCase;
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import jakarta.validation.Valid;
11+
import lombok.RequiredArgsConstructor;
12+
import lombok.extern.slf4j.Slf4j;
13+
import org.springframework.security.access.annotation.Secured;
14+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
15+
import org.springframework.web.bind.annotation.PutMapping;
16+
import org.springframework.web.bind.annotation.RequestMapping;
17+
import org.springframework.web.bind.annotation.RequestPart;
18+
import org.springframework.web.bind.annotation.RestController;
19+
import org.springframework.web.multipart.MultipartFile;
20+
21+
import java.util.UUID;
22+
23+
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
24+
25+
@RestController
26+
@Slf4j
27+
@RequiredArgsConstructor
28+
@RequestMapping("/api/profile")
29+
@Tag(name = "PUT Volunteer", description = "봉사자 프로필 수정")
30+
public class VolunteerProfileCommandController {
31+
32+
private final UpdateVolunteerProfileUseCase updateVolunteerProfileUseCase;
33+
private final ImageUploadUseCase imageUploadUseCase;
34+
35+
@Secured("ROLE_VOLUNTEER")
36+
@Operation(summary = "프로필 수정", description = "현재 로그인된 사용자의 프로필을 수정합니다.")
37+
@PutMapping(consumes = MULTIPART_FORM_DATA_VALUE)
38+
public ApiResponse<String> updateProfile(
39+
@AuthenticationPrincipal String volunteerId,
40+
@Valid @RequestPart("data") VolunteerProfileUpdateRequestDto requestDto,
41+
@RequestPart(value = "img_file", required = false) MultipartFile image) {
42+
43+
String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image));
44+
45+
updateVolunteerProfileUseCase.update(
46+
UUID.fromString(volunteerId),
47+
requestDto,
48+
imgUrl
49+
);
50+
51+
return ApiResponse.ok("프로필 수정 성공");
52+
}
53+
}

src/main/java/com/somemore/volunteer/controller/VolunteerQueryController.java renamed to src/main/java/com/somemore/volunteer/controller/VolunteerProfileQueryController.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.somemore.volunteer.controller;
22

33
import com.somemore.global.common.response.ApiResponse;
4-
import com.somemore.volunteer.dto.response.VolunteerResponseDto;
4+
import com.somemore.volunteer.dto.response.VolunteerProfileResponseDto;
55
import com.somemore.volunteer.usecase.VolunteerQueryUseCase;
66
import io.swagger.v3.oas.annotations.Operation;
77
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -21,14 +21,14 @@
2121
@RequiredArgsConstructor
2222
@RequestMapping("/api/profile")
2323
@Tag(name = "GET Volunteer", description = "봉사자 조회")
24-
public class VolunteerQueryController {
24+
public class VolunteerProfileQueryController {
2525

2626
private final VolunteerQueryUseCase volunteerQueryUseCase;
2727

2828
@Operation(summary = "본인 상세 프로필 조회", description = "현재 로그인된 사용자의 상세 프로필을 조회합니다.")
2929
@Secured("ROLE_VOLUNTEER")
3030
@GetMapping("/me")
31-
public ApiResponse<VolunteerResponseDto> getMyProfile(
31+
public ApiResponse<VolunteerProfileResponseDto> getMyProfile(
3232
@AuthenticationPrincipal String volunteerId) {
3333

3434
return ApiResponse.ok(
@@ -39,7 +39,7 @@ public ApiResponse<VolunteerResponseDto> getMyProfile(
3939

4040
@GetMapping("/{volunteerId}")
4141
@Operation(summary = "타인 프로필 조회", description = "특정 봉사자의 프로필을 조회합니다. 상세 정보는 포함되지 않습니다.")
42-
public ApiResponse<VolunteerResponseDto> getVolunteerProfile(
42+
public ApiResponse<VolunteerProfileResponseDto> getVolunteerProfile(
4343
@PathVariable UUID volunteerId) {
4444

4545
return ApiResponse.ok(
@@ -52,7 +52,7 @@ public ApiResponse<VolunteerResponseDto> getVolunteerProfile(
5252
@GetMapping("/{volunteerId}/detailed")
5353
@Secured("ROLE_CENTER")
5454
@Operation(summary = "지원자 상세 프로필 조회", description = "기관이 작성한 모집 글에 지원한 봉사자의 상세 프로필을 조회합니다.")
55-
public ApiResponse<VolunteerResponseDto> getVolunteerDetailedProfile(
55+
public ApiResponse<VolunteerProfileResponseDto> getVolunteerDetailedProfile(
5656
@PathVariable UUID volunteerId,
5757
@AuthenticationPrincipal String centerId) {
5858

src/main/java/com/somemore/volunteer/domain/Volunteer.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.somemore.auth.oauth.OAuthProvider;
44
import com.somemore.global.common.BaseEntity;
5+
import com.somemore.volunteer.dto.request.VolunteerProfileUpdateRequestDto;
56
import jakarta.persistence.*;
67
import lombok.*;
78

@@ -58,6 +59,12 @@ public static Volunteer createDefault(OAuthProvider oauthProvider, String oauthI
5859
.build();
5960
}
6061

62+
public void updateWith(VolunteerProfileUpdateRequestDto dto, String imgUrl) {
63+
this.nickname = dto.nickname();
64+
this.introduce = dto.introduce();
65+
this.imgUrl = imgUrl;
66+
}
67+
6168
@Builder
6269
private Volunteer(
6370
OAuthProvider oauthProvider,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.somemore.volunteer.dto.request;
2+
3+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
4+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import jakarta.validation.constraints.NotBlank;
7+
import jakarta.validation.constraints.Size;
8+
import lombok.Builder;
9+
10+
@Builder
11+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
12+
public record VolunteerProfileUpdateRequestDto(
13+
14+
@Schema(description = "봉사자 닉네임", example = "making")
15+
@NotBlank(message = "닉네임은 필수 값입니다.")
16+
@Size(max = 10, message = "닉네임은 최대 10자까지 입력 가능합니다.")
17+
String nickname,
18+
19+
@Schema(description = "봉사자 소개글", example = "저는 다양한 봉사활동에 관심이 많은 봉사자입니다.")
20+
@NotBlank(message = "소개글은 필수 값입니다.")
21+
@Size(max = 100, message = "소개글은 최대 100자까지 입력 가능합니다.")
22+
String introduce
23+
) {
24+
}

src/main/java/com/somemore/volunteer/dto/response/VolunteerResponseDto.java renamed to src/main/java/com/somemore/volunteer/dto/response/VolunteerProfileResponseDto.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import io.swagger.v3.oas.annotations.media.Schema;
88

99
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
10-
@Schema(description = "봉사자 응답 DTO")
11-
public record VolunteerResponseDto(
10+
@Schema(description = "봉사자 프로필 응답 DTO")
11+
public record VolunteerProfileResponseDto(
1212
@Schema(description = "봉사자 ID", example = "123e4567-e89b-12d3-a456-426614174000")
1313
String volunteerId,
1414

@@ -30,30 +30,30 @@ public record VolunteerResponseDto(
3030
@Schema(description = "총 봉사 횟수", example = "20")
3131
Integer totalVolunteerCount,
3232

33-
@Schema(description = "봉사자 상세 정보", implementation = VolunteerDetailResponseDto.class)
34-
VolunteerDetailResponseDto volunteerDetailResponseDto
33+
@Schema(description = "봉사자 상세 정보", implementation = Detail.class)
34+
Detail detail
3535
) {
3636

37-
public static VolunteerResponseDto from(
37+
public static VolunteerProfileResponseDto from(
3838
Volunteer volunteer,
3939
VolunteerDetail volunteerDetail
4040
) {
41-
return new VolunteerResponseDto(
41+
return new VolunteerProfileResponseDto(
4242
volunteer.getId().toString(),
4343
volunteer.getNickname(),
4444
volunteer.getImgUrl(),
4545
volunteer.getIntroduce(),
4646
volunteer.getTier().name(),
4747
volunteer.getTotalVolunteerHours(),
4848
volunteer.getTotalVolunteerCount(),
49-
VolunteerDetailResponseDto.from(volunteerDetail)
49+
Detail.from(volunteerDetail)
5050
);
5151
}
5252

53-
public static VolunteerResponseDto from(
53+
public static VolunteerProfileResponseDto from(
5454
Volunteer volunteer
5555
) {
56-
return new VolunteerResponseDto(
56+
return new VolunteerProfileResponseDto(
5757
volunteer.getId().toString(),
5858
volunteer.getNickname(),
5959
volunteer.getImgUrl(),
@@ -66,8 +66,8 @@ public static VolunteerResponseDto from(
6666
}
6767

6868
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
69-
@Schema(description = "봉사자 상세 응답 DTO")
70-
private record VolunteerDetailResponseDto(
69+
@Schema(description = "봉사자 상세 프로필")
70+
private record Detail(
7171
@Schema(description = "이름", example = "홍길동")
7272
String name,
7373

@@ -83,10 +83,10 @@ private record VolunteerDetailResponseDto(
8383
@Schema(description = "연락처", example = "010-1234-5678")
8484
String contactNumber
8585
) {
86-
public static VolunteerDetailResponseDto from(
86+
public static Detail from(
8787
VolunteerDetail volunteerDetail
8888
) {
89-
return new VolunteerDetailResponseDto(
89+
return new Detail(
9090
volunteerDetail.getName(),
9191
volunteerDetail.getEmail(),
9292
volunteerDetail.getGender().name(),
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.somemore.volunteer.service;
2+
3+
import com.somemore.global.exception.BadRequestException;
4+
import com.somemore.volunteer.domain.Volunteer;
5+
import com.somemore.volunteer.dto.request.VolunteerProfileUpdateRequestDto;
6+
import com.somemore.volunteer.repository.VolunteerRepository;
7+
import com.somemore.volunteer.usecase.UpdateVolunteerProfileUseCase;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.stereotype.Service;
11+
import org.springframework.transaction.annotation.Transactional;
12+
13+
import java.util.UUID;
14+
15+
import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_VOLUNTEER;
16+
17+
@Slf4j
18+
@Service
19+
@RequiredArgsConstructor
20+
@Transactional
21+
public class UpdateVolunteerProfileService implements UpdateVolunteerProfileUseCase {
22+
23+
private final VolunteerRepository volunteerRepository;
24+
25+
@Override
26+
public void update(UUID volunteerId, VolunteerProfileUpdateRequestDto requestDto, String imgUrl) {
27+
Volunteer volunteer = volunteerRepository.findById(volunteerId)
28+
.orElseThrow(() -> new BadRequestException(NOT_EXISTS_VOLUNTEER));
29+
30+
volunteer.updateWith(requestDto, imgUrl);
31+
}
32+
33+
}

src/main/java/com/somemore/volunteer/service/VolunteerQueryService.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import com.somemore.global.exception.BadRequestException;
55
import com.somemore.volunteer.domain.Volunteer;
66
import com.somemore.volunteer.domain.VolunteerDetail;
7-
import com.somemore.volunteer.dto.response.VolunteerResponseDto;
7+
import com.somemore.volunteer.dto.response.VolunteerProfileResponseDto;
88
import com.somemore.volunteer.repository.VolunteerDetailRepository;
99
import com.somemore.volunteer.repository.VolunteerRepository;
1010
import com.somemore.volunteer.usecase.VolunteerQueryUseCase;
@@ -28,27 +28,27 @@ public class VolunteerQueryService implements VolunteerQueryUseCase {
2828
private final VolunteerDetailAccessValidator volunteerDetailAccessValidator;
2929

3030
@Override
31-
public VolunteerResponseDto getMyProfile(UUID volunteerId) {
31+
public VolunteerProfileResponseDto getMyProfile(UUID volunteerId) {
3232

33-
return VolunteerResponseDto.from(
33+
return VolunteerProfileResponseDto.from(
3434
findVolunteer(volunteerId),
3535
findVolunteerDetail(volunteerId)
3636
);
3737
}
3838

3939
@Override
40-
public VolunteerResponseDto getVolunteerProfile(UUID volunteerId) {
40+
public VolunteerProfileResponseDto getVolunteerProfile(UUID volunteerId) {
4141

42-
return VolunteerResponseDto.from(
42+
return VolunteerProfileResponseDto.from(
4343
findVolunteer(volunteerId)
4444
);
4545
}
4646

4747
@Override
48-
public VolunteerResponseDto getVolunteerDetailedProfile(UUID volunteerId, UUID centerId) {
48+
public VolunteerProfileResponseDto getVolunteerDetailedProfile(UUID volunteerId, UUID centerId) {
4949
volunteerDetailAccessValidator.validateByCenterId(centerId, volunteerId);
5050

51-
return VolunteerResponseDto.from(
51+
return VolunteerProfileResponseDto.from(
5252
findVolunteer(volunteerId),
5353
findVolunteerDetail(volunteerId)
5454
);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.somemore.volunteer.usecase;
2+
3+
import com.somemore.volunteer.dto.request.VolunteerProfileUpdateRequestDto;
4+
5+
import java.util.UUID;
6+
7+
public interface UpdateVolunteerProfileUseCase {
8+
9+
void update(UUID volunteerId, VolunteerProfileUpdateRequestDto requestDto, String imgUrl);
10+
}

0 commit comments

Comments
 (0)