diff --git a/build.gradle b/build.gradle index f4e19389..4e48378d 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ sonar { property "sonar.host.url", "https://sonarcloud.io" property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/test/jacocoTestReport.xml" property "sonar.exclusions", "**/*Application*.java, **/*Config*.java, **/*GlobalExceptionHandler.java, **/Q*.java, **/DynamicQuery.java, " + - "**/*Exception.java, **/*Adapter.java, **/CustomOAuth2UserService.java, **/*SearchRepository.java, **/*Filter.java" + "**/*Exception.java, **/*Adapter.java, **/CustomOAuth2UserService.java, **/*Filter.java" property "sonar.java.coveragePlugin", "jacoco" } } @@ -123,7 +123,6 @@ jacocoTestCoverageVerification { '*.Q*', '*.DynamicQuery', '*.*Adapter', - '*.*SearchRepository', '*.*.CustomUserDetails', 'scouter.*', 'reactor.*', diff --git a/src/main/java/com/tnt/common/jpa/DynamicQuery.java b/src/main/java/com/tnt/common/jpa/DynamicQuery.java index f4776e7a..912657c4 100644 --- a/src/main/java/com/tnt/common/jpa/DynamicQuery.java +++ b/src/main/java/com/tnt/common/jpa/DynamicQuery.java @@ -26,7 +26,7 @@ public static BooleanExpression generateIsNull(Bool return null; } - if (Boolean.TRUE.equals(value)) { + if (value) { return field.isNull(); } diff --git a/src/main/java/com/tnt/image/application/S3Service.java b/src/main/java/com/tnt/image/application/S3Service.java index 9b2d07c5..cdd282db 100644 --- a/src/main/java/com/tnt/image/application/S3Service.java +++ b/src/main/java/com/tnt/image/application/S3Service.java @@ -32,8 +32,9 @@ import com.drew.metadata.MetadataException; import com.drew.metadata.exif.ExifIFD0Directory; import com.tnt.common.error.exception.ImageException; -import com.tnt.image.S3Adapter; +import com.tnt.image.infrastructure.S3Adapter; import com.tnt.member.domain.MemberType; +import com.tnt.member.dto.ProfileUpdate; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -179,4 +180,26 @@ private BufferedImage rotateImageIfRequired(BufferedImage image, MultipartFile m return image; } + + public String handleProfileImage(ProfileUpdate profileUpdate, @Nullable MultipartFile profileImage, + MemberType memberType) { + // 새 이미지 없음 + if (isNull(profileImage)) { + // 이미지 삭제 요청 - 현재 이미지가 기본 이미지가 아닌 경우 + if (profileUpdate.removeCurrentImage() && !profileUpdate.isCurrentImageDefault()) { + deleteProfileImage(profileUpdate.currentImageUrl()); + + return profileUpdate.changeImageUrl(); + } else { // 이미지 유지 요청 + return profileUpdate.currentImageUrl(); + } + } else { // 새 이미지 있음 + // 이미지 수정 요청 - 현재 이미지가 기본 이미지가 아닌 경우 + if (profileUpdate.removeCurrentImage() && !profileUpdate.isCurrentImageDefault()) { + deleteProfileImage(profileUpdate.currentImageUrl()); + } + + return uploadProfileImage(profileImage, memberType); + } + } } diff --git a/src/main/java/com/tnt/image/S3Adapter.java b/src/main/java/com/tnt/image/infrastructure/S3Adapter.java similarity index 97% rename from src/main/java/com/tnt/image/S3Adapter.java rename to src/main/java/com/tnt/image/infrastructure/S3Adapter.java index 7081d21b..869efddb 100644 --- a/src/main/java/com/tnt/image/S3Adapter.java +++ b/src/main/java/com/tnt/image/infrastructure/S3Adapter.java @@ -1,4 +1,4 @@ -package com.tnt.image; +package com.tnt.image.infrastructure; import static com.tnt.common.error.model.ErrorMessage.S3_DELETE_ERROR; import static com.tnt.common.error.model.ErrorMessage.S3_UPLOAD_ERROR; diff --git a/src/main/java/com/tnt/member/application/MemberService.java b/src/main/java/com/tnt/member/application/MemberService.java index ea3bedd2..6ec73cd8 100644 --- a/src/main/java/com/tnt/member/application/MemberService.java +++ b/src/main/java/com/tnt/member/application/MemberService.java @@ -1,27 +1,37 @@ package com.tnt.member.application; +import static com.tnt.common.constant.ImageConstant.TRAINEE_DEFAULT_IMAGE; +import static com.tnt.common.constant.ImageConstant.TRAINER_DEFAULT_IMAGE; import static com.tnt.common.error.model.ErrorMessage.MEMBER_CONFLICT; import static com.tnt.member.domain.MemberType.TRAINEE; import static com.tnt.member.domain.MemberType.TRAINER; import static com.tnt.member.dto.MemberProjection.MemberTypeDto; +import static java.util.Objects.isNull; +import static java.util.stream.Collectors.toSet; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import com.tnt.common.error.exception.ConflictException; import com.tnt.gateway.dto.response.CheckSessionResponse; import com.tnt.member.application.repository.MemberRepository; import com.tnt.member.domain.Member; import com.tnt.member.domain.SocialType; -import com.tnt.member.dto.response.GetMemberInfoResponse; -import com.tnt.member.dto.response.GetMemberInfoResponse.TraineeInfo; -import com.tnt.member.dto.response.GetMemberInfoResponse.TrainerInfo; +import com.tnt.member.dto.ProfileUpdate; +import com.tnt.member.dto.request.UpdateMemberInfoRequest; +import com.tnt.member.dto.response.MemberInfoResponse; import com.tnt.pt.application.PtService; import com.tnt.pt.domain.PtTrainerTrainee; import com.tnt.trainee.application.PtGoalService; import com.tnt.trainee.application.TraineeService; +import com.tnt.trainee.application.repository.PtGoalRepository; +import com.tnt.trainee.application.repository.TraineeRepository; import com.tnt.trainee.domain.PtGoal; import com.tnt.trainee.domain.Trainee; import com.tnt.trainer.application.TrainerService; @@ -39,11 +49,13 @@ public class MemberService { private final PtService ptService; private final MemberRepository memberRepository; + private final TraineeRepository traineeRepository; + private final PtGoalRepository ptGoalRepository; @Transactional(readOnly = true) - public GetMemberInfoResponse getMemberInfo(Long memberId) { + public MemberInfoResponse getMemberInfo(Long memberId) { Member member = getByMemberId(memberId); - GetMemberInfoResponse memberInfo = null; + MemberInfoResponse memberInfoResponse = null; if (member.getMemberType() == TRAINER) { Trainer trainer = trainerService.getByMemberId(memberId); @@ -56,24 +68,33 @@ public GetMemberInfoResponse getMemberInfo(Long memberId) { int totalTraineeCount = ptTrainerTrainees.size(); - TrainerInfo trainerInfo = new TrainerInfo(activeTraineeCount, totalTraineeCount); + MemberInfoResponse.TrainerInfo trainerInfo = new MemberInfoResponse.TrainerInfo(activeTraineeCount, + totalTraineeCount); - memberInfo = new GetMemberInfoResponse(member.getName(), member.getEmail(), member.getProfileImageUrl(), + memberInfoResponse = new MemberInfoResponse(member.getName(), member.getEmail(), + member.getProfileImageUrl(), member.getMemberType(), member.getSocialType(), trainerInfo, null); - } else if (member.getMemberType() == TRAINEE) { + } + + if (member.getMemberType() == TRAINEE) { Trainee trainee = traineeService.getByMemberId(memberId); - List ptGoals = ptGoalService.getAllByTraineeId(trainee.getId()).stream().map( - PtGoal::getContent).toList(); + List ptGoals = ptGoalService.getAllByTraineeId(trainee.getId()) + .stream() + .map(PtGoal::getContent) + .toList(); boolean isConnected = ptService.isPtTrainerTraineeExistWithTraineeId(trainee.getId()); - TraineeInfo traineeInfo = new TraineeInfo(isConnected, member.getBirthday(), - member.getAge(), trainee.getHeight(), trainee.getWeight(), trainee.getCautionNote(), ptGoals); + MemberInfoResponse.TraineeInfo traineeInfo = new MemberInfoResponse.TraineeInfo(isConnected, + member.getBirthday(), + member.getAge(), + trainee.getHeight(), trainee.getWeight(), trainee.getCautionNote(), ptGoals); - memberInfo = new GetMemberInfoResponse(member.getName(), member.getEmail(), member.getProfileImageUrl(), + memberInfoResponse = new MemberInfoResponse(member.getName(), member.getEmail(), + member.getProfileImageUrl(), member.getMemberType(), member.getSocialType(), null, traineeInfo); } - return memberInfo; + return memberInfoResponse; } @Transactional(readOnly = true) @@ -92,6 +113,59 @@ public CheckSessionResponse getMemberType(Long memberId) { return new CheckSessionResponse(memberTypeDto.memberType(), isConnected); } + @Transactional + public ProfileUpdate checkMemberProfileImage(Long memberId, boolean removeImage, + @Nullable MultipartFile profileImage) { + Member member = getByMemberId(memberId); + String currentImageUrl = member.getProfileImageUrl(); + String changeImageUrl = ""; + boolean removeCurrentImage = true; + boolean isCurrentImageDefault = + currentImageUrl.equals(TRAINER_DEFAULT_IMAGE) || currentImageUrl.equals(TRAINEE_DEFAULT_IMAGE); + + // 새 이미지 없음 + if (isNull(profileImage)) { + // 이미지 삭제 요청 - 현재 이미지가 기본 이미지가 아닌 경우 + if (removeImage && !isCurrentImageDefault) { + changeImageUrl = + member.getMemberType() == TRAINER ? TRAINER_DEFAULT_IMAGE : TRAINEE_DEFAULT_IMAGE; + } else if (!removeImage && isCurrentImageDefault) { // 이미지 유지 요청 - 현재 이미지가 기본 이미지인 경우 + changeImageUrl = currentImageUrl; + removeCurrentImage = false; + } else { // 이미지 유지 요청 - 현재 이미지가 기본 이미지가 아닌 경우 + removeCurrentImage = false; + } + } else { // 새 이미지 있음 + // 이미지 수정 요청 - 현재 이미지가 기본 이미지인 경우 + if (isCurrentImageDefault) { + removeCurrentImage = false; + } + } + + return new ProfileUpdate(currentImageUrl, changeImageUrl, removeCurrentImage, + isCurrentImageDefault); + } + + @Transactional + public void updateMemberInfo(Long memberId, UpdateMemberInfoRequest request, String profileImageUrl) { + Member member = getByMemberId(memberId); + + if (member.getMemberType() == TRAINEE) { + Trainee trainee = traineeService.getByMemberId(memberId); + + member.updateBirthday(request.birthday()); + trainee.updateTraineeInfo(request.height(), request.weight(), request.cautionNote()); + updatePtGoals(trainee, new HashSet<>(request.goalContents())); + + traineeRepository.save(trainee); + } + + member.updateName(request.name()); + member.updateProfileImageUrl(profileImageUrl); + + memberRepository.save(member); + } + public void validateMemberNotExists(String socialId, SocialType socialType) { if (memberRepository.existsBySocialIdAndSocialType(socialId, socialType)) { throw new ConflictException(MEMBER_CONFLICT); @@ -101,4 +175,32 @@ public void validateMemberNotExists(String socialId, SocialType socialType) { public Member getByMemberId(Long memberId) { return memberRepository.findById(memberId); } + + private void updatePtGoals(Trainee trainee, HashSet newGoalContents) { + // 기존 PT 목표들 조회 + List currentPtGoals = ptGoalService.getAllByTraineeId(trainee.getId()); + + // 기존 목표 중 더 이상 필요없는 목표 삭제 + List goalsToDelete = currentPtGoals.stream() + .filter(goal -> !newGoalContents.contains(goal.getContent())) + .toList(); + + if (!goalsToDelete.isEmpty()) { + ptGoalRepository.deleteAll(goalsToDelete); + } + + // 새로운 목표 추가 (기존에 없는 것만) + Set existingContents = currentPtGoals.stream() + .map(PtGoal::getContent) + .collect(toSet()); + + List newPtGoals = newGoalContents.stream() + .filter(content -> !existingContents.contains(content)) + .map(content -> PtGoal.builder().traineeId(trainee.getId()).content(content).build()) + .toList(); + + if (!newPtGoals.isEmpty()) { + ptGoalRepository.saveAll(newPtGoals); + } + } } diff --git a/src/main/java/com/tnt/member/application/WithdrawService.java b/src/main/java/com/tnt/member/application/WithdrawService.java index 1aa27c03..097048be 100644 --- a/src/main/java/com/tnt/member/application/WithdrawService.java +++ b/src/main/java/com/tnt/member/application/WithdrawService.java @@ -21,6 +21,8 @@ import com.tnt.trainee.application.DietService; import com.tnt.trainee.application.PtGoalService; import com.tnt.trainee.application.TraineeService; +import com.tnt.trainee.application.repository.DietRepository; +import com.tnt.trainee.application.repository.PtGoalRepository; import com.tnt.trainee.application.repository.TraineeRepository; import com.tnt.trainee.domain.Diet; import com.tnt.trainee.domain.PtGoal; @@ -43,11 +45,13 @@ public class WithdrawService { private final DietService dietService; private final PtService ptService; + private final PtGoalRepository ptGoalRepository; private final MemberRepository memberRepository; private final TrainerRepository trainerRepository; private final TraineeRepository traineeRepository; private final PtLessonRepository ptLessonRepository; private final PtTrainerTraineeRepository ptTrainerTraineeRepository; + private final DietRepository dietRepository; @Transactional public WithdrawDto withdraw(Long memberId) { @@ -104,9 +108,10 @@ private void deleteMemberData(Member member) { } } - ptGoals.forEach(PtGoal::softDelete); + ptGoalRepository.deleteAll(ptGoals); diets.forEach(Diet::softDelete); + dietRepository.saveAll(diets); trainee.softDelete(); traineeRepository.save(trainee); diff --git a/src/main/java/com/tnt/member/application/repository/MemberRepository.java b/src/main/java/com/tnt/member/application/repository/MemberRepository.java index 0ff7c2cd..cc4d5b93 100644 --- a/src/main/java/com/tnt/member/application/repository/MemberRepository.java +++ b/src/main/java/com/tnt/member/application/repository/MemberRepository.java @@ -1,5 +1,7 @@ package com.tnt.member.application.repository; +import java.util.List; + import com.tnt.member.domain.Member; import com.tnt.member.domain.SocialType; import com.tnt.member.dto.MemberProjection; @@ -15,4 +17,6 @@ public interface MemberRepository { Member findById(Long memberId); MemberProjection.MemberTypeDto findMemberType(Long memberId); + + List findAll(); } diff --git a/src/main/java/com/tnt/member/domain/Member.java b/src/main/java/com/tnt/member/domain/Member.java index 74c69c1f..5d09cbd5 100644 --- a/src/main/java/com/tnt/member/domain/Member.java +++ b/src/main/java/com/tnt/member/domain/Member.java @@ -26,13 +26,13 @@ public class Member { private final Long id; private final String email; - private final String name; - private final LocalDate birthday; private final Boolean serviceAgreement; private final Boolean collectionAgreement; private final Boolean advertisementAgreement; private final SocialType socialType; private final MemberType memberType; + private String name; + private LocalDate birthday; private String socialId; private String fcmToken; private String profileImageUrl; @@ -60,15 +60,21 @@ public Member(Long id, String socialId, String fcmToken, String email, String na this.deletedAt = deletedAt; } - public void updateFcmTokenIfExpired(String fcmToken) { - if (!isBlank(fcmToken) && !this.fcmToken.equals(fcmToken)) { - this.fcmToken = fcmToken; - } + public void updateName(String name) { + this.name = validateName(name); } public void updateProfileImageUrl(String profileImageUrl) { - if (!isBlank(profileImageUrl) && !this.profileImageUrl.equals(profileImageUrl)) { - this.profileImageUrl = profileImageUrl; + this.profileImageUrl = validateProfileImageUrl(profileImageUrl); + } + + public void updateBirthday(LocalDate birthday) { + this.birthday = birthday; + } + + public void updateFcmTokenIfExpired(String fcmToken) { + if (!isBlank(fcmToken) && !this.fcmToken.equals(fcmToken)) { + this.fcmToken = fcmToken; } } diff --git a/src/main/java/com/tnt/member/dto/ProfileUpdate.java b/src/main/java/com/tnt/member/dto/ProfileUpdate.java new file mode 100644 index 00000000..17df5768 --- /dev/null +++ b/src/main/java/com/tnt/member/dto/ProfileUpdate.java @@ -0,0 +1,10 @@ +package com.tnt.member.dto; + +public record ProfileUpdate( + String currentImageUrl, + String changeImageUrl, + boolean removeCurrentImage, + boolean isCurrentImageDefault +) { + +} diff --git a/src/main/java/com/tnt/member/dto/request/UpdateMemberInfoRequest.java b/src/main/java/com/tnt/member/dto/request/UpdateMemberInfoRequest.java new file mode 100644 index 00000000..ee0f2a65 --- /dev/null +++ b/src/main/java/com/tnt/member/dto/request/UpdateMemberInfoRequest.java @@ -0,0 +1,46 @@ +package com.tnt.member.dto.request; + +import java.time.LocalDate; +import java.util.List; + +import com.tnt.member.domain.MemberType; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Past; + +@Schema(description = "회원 정보 수정 API 요청") +public record UpdateMemberInfoRequest( + @Schema(description = "기존 프로필 사진 삭제 여부", example = "true", nullable = false) + Boolean removeImage, + + @Schema(description = "회원 타입", example = "TRAINER", nullable = false) + @NotNull(message = "회원 타입은 필수입니다.") + MemberType memberType, + + @Schema(description = "회원 이름", example = "홍길동", nullable = false) + @NotBlank(message = "회원 이름은 필수입니다.") + String name, + + @Schema(description = "생년월일", example = "2025-01-01", nullable = true) + @Past(message = "생년월일은 과거 날짜여야 합니다.") + LocalDate birthday, + + @Schema(description = "키 (cm)", example = "180.5", nullable = true) + @Digits(integer = 3, fraction = 2, message = "키는 정수부 3자리, 소수점 2자리까지 입력 가능합니다.") + Double height, + + @Schema(description = "몸무게 (kg)", example = "75.5", nullable = true) + @Digits(integer = 3, fraction = 2, message = "몸무게는 정수부 3자리, 소수점 2자리까지 입력 가능합니다.") + Double weight, + + @Schema(description = "주의사항", example = "가냘퍼요", nullable = true) + String cautionNote, + + @Schema(description = "PT 목적들", example = "[\"체중 감량\", \"근력 향상\"]", nullable = false) + List goalContents +) { + +} diff --git a/src/main/java/com/tnt/member/dto/response/GetMemberInfoResponse.java b/src/main/java/com/tnt/member/dto/response/MemberInfoResponse.java similarity index 95% rename from src/main/java/com/tnt/member/dto/response/GetMemberInfoResponse.java rename to src/main/java/com/tnt/member/dto/response/MemberInfoResponse.java index ef24dccc..3c0bb179 100644 --- a/src/main/java/com/tnt/member/dto/response/GetMemberInfoResponse.java +++ b/src/main/java/com/tnt/member/dto/response/MemberInfoResponse.java @@ -8,8 +8,8 @@ import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "회원 조회 API 응답") -public record GetMemberInfoResponse( +@Schema(description = "회원 정보") +public record MemberInfoResponse( @Schema(description = "회원 이름", example = "홍길동", nullable = false) String name, @@ -67,3 +67,4 @@ public record TraineeInfo( } } + diff --git a/src/main/java/com/tnt/member/infrastructure/MemberRepositoryImpl.java b/src/main/java/com/tnt/member/infrastructure/MemberRepositoryImpl.java index 9055484a..544ecbb9 100644 --- a/src/main/java/com/tnt/member/infrastructure/MemberRepositoryImpl.java +++ b/src/main/java/com/tnt/member/infrastructure/MemberRepositoryImpl.java @@ -3,6 +3,7 @@ import static com.tnt.common.error.model.ErrorMessage.MEMBER_NOT_FOUND; import static com.tnt.member.infrastructure.QMemberJpaEntity.memberJpaEntity; +import java.util.List; import java.util.Optional; import org.springframework.stereotype.Repository; @@ -58,4 +59,11 @@ public MemberProjection.MemberTypeDto findMemberType(Long memberId) { .fetchOne()) .orElseThrow(() -> new NotFoundException(MEMBER_NOT_FOUND)); } + + @Override + public List findAll() { + return memberJpaRepository.findAll().stream() + .map(MemberJpaEntity::toModel) + .toList(); + } } diff --git a/src/main/java/com/tnt/member/presentation/MemberController.java b/src/main/java/com/tnt/member/presentation/MemberController.java index 58670ed7..94fe863b 100644 --- a/src/main/java/com/tnt/member/presentation/MemberController.java +++ b/src/main/java/com/tnt/member/presentation/MemberController.java @@ -4,8 +4,10 @@ import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; +import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +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.ResponseStatus; @@ -17,9 +19,11 @@ import com.tnt.member.application.MemberService; import com.tnt.member.application.SignUpService; import com.tnt.member.application.WithdrawService; +import com.tnt.member.dto.ProfileUpdate; import com.tnt.member.dto.WithdrawDto; import com.tnt.member.dto.request.SignUpRequest; -import com.tnt.member.dto.response.GetMemberInfoResponse; +import com.tnt.member.dto.request.UpdateMemberInfoRequest; +import com.tnt.member.dto.response.MemberInfoResponse; import com.tnt.member.dto.response.SignUpResponse; import io.swagger.v3.oas.annotations.Operation; @@ -42,7 +46,7 @@ public class MemberController { @PostMapping(value = "/sign-up", consumes = MULTIPART_FORM_DATA_VALUE) @ResponseStatus(CREATED) public SignUpResponse signUp(@RequestPart("request") @Valid SignUpRequest request, - @RequestPart(value = "profileImage", required = false) MultipartFile profileImage) { + @RequestPart(value = "profileImage", required = false) @Nullable MultipartFile profileImage) { Long memberId = signUpService.signUp(request); String profileImageUrl = s3Service.uploadProfileImage(profileImage, request.memberType()); @@ -52,10 +56,24 @@ public SignUpResponse signUp(@RequestPart("request") @Valid SignUpRequest reques @Operation(summary = "회원 조회 API") @GetMapping @ResponseStatus(OK) - public GetMemberInfoResponse getMemberInfo(@AuthMember Long memberId) { + public MemberInfoResponse getMemberInfo(@AuthMember Long memberId) { return memberService.getMemberInfo(memberId); } + @Operation(summary = "회원 정보 수정 API") + @PutMapping(consumes = MULTIPART_FORM_DATA_VALUE) + @ResponseStatus(OK) + public void updateMemberInfo(@AuthMember Long memberId, + @RequestPart("request") @Valid UpdateMemberInfoRequest request, + @RequestPart(value = "profileImage", required = false) @Nullable MultipartFile profileImage) { + ProfileUpdate profileUpdate = memberService.checkMemberProfileImage(memberId, request.removeImage(), + profileImage); + + String profileImageUrl = s3Service.handleProfileImage(profileUpdate, profileImage, request.memberType()); + + memberService.updateMemberInfo(memberId, request, profileImageUrl); + } + @Operation(summary = "회원 탈퇴 API") @PostMapping("/withdraw") @ResponseStatus(OK) diff --git a/src/main/java/com/tnt/pt/application/PtService.java b/src/main/java/com/tnt/pt/application/PtService.java index 858895fe..c998fbb6 100644 --- a/src/main/java/com/tnt/pt/application/PtService.java +++ b/src/main/java/com/tnt/pt/application/PtService.java @@ -213,6 +213,7 @@ public void completePtLesson(Long memberId, Long ptLessonId) { } }); + ptLessonRepository.save(ptLesson); ptLessonRepository.saveAll(lessonsNotCompleted); ptTrainerTraineeRepository.save(ptTrainerTrainee); } @@ -236,6 +237,7 @@ public void cancelPtLesson(Long memberId, Long ptLessonId) { ptTrainerTrainee.cancelLesson(); ptLesson.cancel(ptTrainerTrainee.getCurrentPtSession()); + ptLessonRepository.save(ptLesson); ptLessonRepository.saveAll(lessonsNotCompleted); ptTrainerTraineeRepository.save(ptTrainerTrainee); } diff --git a/src/main/java/com/tnt/trainee/application/repository/DietRepository.java b/src/main/java/com/tnt/trainee/application/repository/DietRepository.java index 55152797..3fcd956d 100644 --- a/src/main/java/com/tnt/trainee/application/repository/DietRepository.java +++ b/src/main/java/com/tnt/trainee/application/repository/DietRepository.java @@ -10,7 +10,7 @@ public interface DietRepository { Diet save(Diet diet); - void saveAll(List diets); + List saveAll(List diets); Diet findByIdAndTraineeId(Long id, Long traineeId); diff --git a/src/main/java/com/tnt/trainee/application/repository/PtGoalRepository.java b/src/main/java/com/tnt/trainee/application/repository/PtGoalRepository.java index 634159e2..6f34649e 100644 --- a/src/main/java/com/tnt/trainee/application/repository/PtGoalRepository.java +++ b/src/main/java/com/tnt/trainee/application/repository/PtGoalRepository.java @@ -9,4 +9,6 @@ public interface PtGoalRepository { List saveAll(List ptGoals); List findAllByTraineeId(Long traineeId); + + void deleteAll(List goalsToDelete); } diff --git a/src/main/java/com/tnt/trainee/application/repository/TraineeRepository.java b/src/main/java/com/tnt/trainee/application/repository/TraineeRepository.java index 3911c8e5..5afc45d6 100644 --- a/src/main/java/com/tnt/trainee/application/repository/TraineeRepository.java +++ b/src/main/java/com/tnt/trainee/application/repository/TraineeRepository.java @@ -1,5 +1,7 @@ package com.tnt.trainee.application.repository; +import java.util.List; + import org.springframework.lang.Nullable; import com.tnt.trainee.domain.Trainee; @@ -11,4 +13,6 @@ public interface TraineeRepository { Trainee findByMemberId(Long memberId); Trainee find(@Nullable Long memberId, @Nullable Long traineeId); + + List findAll(); } diff --git a/src/main/java/com/tnt/trainee/domain/PtGoal.java b/src/main/java/com/tnt/trainee/domain/PtGoal.java index 230f9e25..7f3ffd5a 100644 --- a/src/main/java/com/tnt/trainee/domain/PtGoal.java +++ b/src/main/java/com/tnt/trainee/domain/PtGoal.java @@ -4,8 +4,6 @@ import static io.micrometer.common.util.StringUtils.isBlank; import static java.util.Objects.requireNonNull; -import java.time.LocalDateTime; - import lombok.Builder; import lombok.Getter; @@ -17,14 +15,12 @@ public class PtGoal { private final Long id; private final Long traineeId; private final String content; - private LocalDateTime deletedAt; @Builder - public PtGoal(Long id, Long traineeId, String content, LocalDateTime deletedAt) { + public PtGoal(Long id, Long traineeId, String content) { this.id = id; this.traineeId = requireNonNull(traineeId); this.content = validateContent(content); - this.deletedAt = deletedAt; } private String validateContent(String content) { @@ -34,8 +30,4 @@ private String validateContent(String content) { return content; } - - public void softDelete() { - this.deletedAt = LocalDateTime.now(); - } } diff --git a/src/main/java/com/tnt/trainee/domain/Trainee.java b/src/main/java/com/tnt/trainee/domain/Trainee.java index 6e8a7d95..a3b3b5f5 100644 --- a/src/main/java/com/tnt/trainee/domain/Trainee.java +++ b/src/main/java/com/tnt/trainee/domain/Trainee.java @@ -17,8 +17,8 @@ public class Trainee { private final Long id; private final Member member; - private final Double height; - private final Double weight; + private Double height; + private Double weight; private String cautionNote; private LocalDateTime deletedAt; @@ -32,6 +32,12 @@ public Trainee(Long id, Member member, Double height, Double weight, String caut validateAndSetCautionNote(cautionNote); } + public void updateTraineeInfo(Double height, Double weight, String cautionNote) { + this.height = height; + this.weight = weight; + validateAndSetCautionNote(cautionNote); + } + private void validateAndSetCautionNote(String cautionNote) { if (isNull(cautionNote)) { return; diff --git a/src/main/java/com/tnt/trainee/infrastructure/DietRepositoryImpl.java b/src/main/java/com/tnt/trainee/infrastructure/DietRepositoryImpl.java index fd099ce7..43d73126 100644 --- a/src/main/java/com/tnt/trainee/infrastructure/DietRepositoryImpl.java +++ b/src/main/java/com/tnt/trainee/infrastructure/DietRepositoryImpl.java @@ -30,12 +30,12 @@ public Diet save(Diet diet) { } @Override - public void saveAll(List diets) { + public List saveAll(List diets) { List dietJpaEntities = diets.stream() .map(DietJpaEntity::from) .toList(); - dietJpaRepository.saveAll(dietJpaEntities); + return dietJpaRepository.saveAll(dietJpaEntities).stream().map(DietJpaEntity::toModel).toList(); } @Override diff --git a/src/main/java/com/tnt/trainee/infrastructure/PtGoalJpaEntity.java b/src/main/java/com/tnt/trainee/infrastructure/PtGoalJpaEntity.java index ed163e9e..c89ce0db 100644 --- a/src/main/java/com/tnt/trainee/infrastructure/PtGoalJpaEntity.java +++ b/src/main/java/com/tnt/trainee/infrastructure/PtGoalJpaEntity.java @@ -1,7 +1,5 @@ package com.tnt.trainee.infrastructure; -import java.time.LocalDateTime; - import com.tnt.common.jpa.BaseTimeEntity; import com.tnt.trainee.domain.PtGoal; @@ -33,15 +31,11 @@ public class PtGoalJpaEntity extends BaseTimeEntity { @Column(name = "content", nullable = false) private String content; - @Column(name = "deleted_at", nullable = true) - private LocalDateTime deletedAt; - @Builder - public PtGoalJpaEntity(Long id, Long traineeId, String content, LocalDateTime deletedAt) { + public PtGoalJpaEntity(Long id, Long traineeId, String content) { this.id = id; this.traineeId = traineeId; this.content = content; - this.deletedAt = deletedAt; } public static PtGoalJpaEntity from(PtGoal ptGoal) { @@ -49,7 +43,6 @@ public static PtGoalJpaEntity from(PtGoal ptGoal) { .id(ptGoal.getId()) .traineeId(ptGoal.getTraineeId()) .content(ptGoal.getContent()) - .deletedAt(ptGoal.getDeletedAt()) .build(); } @@ -58,7 +51,6 @@ public PtGoal toModel() { .id(id) .traineeId(traineeId) .content(content) - .deletedAt(deletedAt) .build(); } } diff --git a/src/main/java/com/tnt/trainee/infrastructure/PtGoalJpaRepository.java b/src/main/java/com/tnt/trainee/infrastructure/PtGoalJpaRepository.java index 1339f805..8e7598d4 100644 --- a/src/main/java/com/tnt/trainee/infrastructure/PtGoalJpaRepository.java +++ b/src/main/java/com/tnt/trainee/infrastructure/PtGoalJpaRepository.java @@ -6,5 +6,5 @@ public interface PtGoalJpaRepository extends JpaRepository { - List findAllByTraineeIdAndDeletedAtIsNull(Long traineeId); + List findAllByTraineeId(Long traineeId); } diff --git a/src/main/java/com/tnt/trainee/infrastructure/PtGoalRepositoryImpl.java b/src/main/java/com/tnt/trainee/infrastructure/PtGoalRepositoryImpl.java index 7abd1889..398ca69d 100644 --- a/src/main/java/com/tnt/trainee/infrastructure/PtGoalRepositoryImpl.java +++ b/src/main/java/com/tnt/trainee/infrastructure/PtGoalRepositoryImpl.java @@ -28,10 +28,17 @@ public List saveAll(List ptGoals) { @Override public List findAllByTraineeId(Long traineeId) { - return ptGoalJpaRepository.findAllByTraineeIdAndDeletedAtIsNull( - traineeId) - .stream() + return ptGoalJpaRepository.findAllByTraineeId(traineeId).stream() .map(PtGoalJpaEntity::toModel) .toList(); } + + @Override + public void deleteAll(List goalsToDelete) { + List ptGoalJpaEntities = goalsToDelete.stream() + .map(PtGoalJpaEntity::from) + .toList(); + + ptGoalJpaRepository.deleteAll(ptGoalJpaEntities); + } } diff --git a/src/main/java/com/tnt/trainee/infrastructure/TraineeRepositoryImpl.java b/src/main/java/com/tnt/trainee/infrastructure/TraineeRepositoryImpl.java index 18e2e161..b10d24e8 100644 --- a/src/main/java/com/tnt/trainee/infrastructure/TraineeRepositoryImpl.java +++ b/src/main/java/com/tnt/trainee/infrastructure/TraineeRepositoryImpl.java @@ -3,6 +3,7 @@ import static com.tnt.member.infrastructure.QMemberJpaEntity.memberJpaEntity; import static com.tnt.trainee.infrastructure.QTraineeJpaEntity.traineeJpaEntity; +import java.util.List; import java.util.Optional; import org.springframework.lang.Nullable; @@ -35,6 +36,13 @@ public Trainee findByMemberId(Long memberId) { .orElseThrow(() -> new NotFoundException(ErrorMessage.TRAINEE_NOT_FOUND)).toModel(); } + @Override + public List findAll() { + return traineeJpaRepository.findAll().stream() + .map(TraineeJpaEntity::toModel) + .toList(); + } + @Override public Trainee find(@Nullable Long memberId, @Nullable Long traineeId) { return Optional.ofNullable(jpaQueryFactory diff --git a/src/test/java/com/tnt/image/S3AdapterTest.java b/src/test/java/com/tnt/image/S3AdapterTest.java index 23611296..f09d3cbf 100644 --- a/src/test/java/com/tnt/image/S3AdapterTest.java +++ b/src/test/java/com/tnt/image/S3AdapterTest.java @@ -16,6 +16,7 @@ import org.springframework.test.util.ReflectionTestUtils; import com.tnt.common.error.exception.ImageException; +import com.tnt.image.infrastructure.S3Adapter; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; diff --git a/src/test/java/com/tnt/image/application/S3ServiceTest.java b/src/test/java/com/tnt/image/application/S3ServiceTest.java index 16100138..c9f8ad7b 100644 --- a/src/test/java/com/tnt/image/application/S3ServiceTest.java +++ b/src/test/java/com/tnt/image/application/S3ServiceTest.java @@ -33,7 +33,7 @@ import org.springframework.test.util.ReflectionTestUtils; import com.tnt.common.error.exception.ImageException; -import com.tnt.image.S3Adapter; +import com.tnt.image.infrastructure.S3Adapter; @ExtendWith(MockitoExtension.class) class S3ServiceTest { @@ -139,8 +139,8 @@ void rotate_image_orientation_6_success() throws IOException { MockMultipartFile image = new MockMultipartFile("image", "test.jpg", IMAGE_JPEG_VALUE, createDummyImageData(6)); // when - BufferedImage rotatedImage = ReflectionTestUtils.invokeMethod(s3Service, "rotateImageIfRequired", originalImage, - image); + BufferedImage rotatedImage = ReflectionTestUtils.invokeMethod(s3Service, "rotateImageIfRequired", + originalImage, image); // then assertThat(requireNonNull(rotatedImage).getRGB(25, 50)).isEqualTo(Color.BLACK.getRGB()); diff --git a/src/test/java/com/tnt/member/application/WithdrawServiceTest.java b/src/test/java/com/tnt/member/application/WithdrawServiceTest.java index 8dfa22e3..fa9aae20 100644 --- a/src/test/java/com/tnt/member/application/WithdrawServiceTest.java +++ b/src/test/java/com/tnt/member/application/WithdrawServiceTest.java @@ -32,6 +32,8 @@ import com.tnt.trainee.application.DietService; import com.tnt.trainee.application.PtGoalService; import com.tnt.trainee.application.TraineeService; +import com.tnt.trainee.application.repository.DietRepository; +import com.tnt.trainee.application.repository.PtGoalRepository; import com.tnt.trainee.application.repository.TraineeRepository; import com.tnt.trainee.domain.Diet; import com.tnt.trainee.domain.PtGoal; @@ -79,6 +81,12 @@ class WithdrawServiceTest { @Mock private PtLessonRepository ptLessonRepository; + @Mock + private PtGoalRepository ptGoalRepository; + + @Mock + private DietRepository dietRepository; + @InjectMocks private WithdrawService withdrawService; @@ -115,6 +123,7 @@ void withdraw_trainee_success() { given(traineeService.getByMemberId(traineeMember.getId())).willReturn(trainee); given(ptGoalService.getAllByTraineeId(trainee.getId())).willReturn(ptGoals); given(dietService.getAllByTraineeId(trainee.getId())).willReturn(diets); + given(dietRepository.saveAll(diets)).willReturn(diets); // when withdrawService.withdraw(traineeMember.getId()); @@ -176,6 +185,7 @@ void withdraw_trainee_with_pt_success() { given(dietService.getAllByTraineeId(trainee.getId())).willReturn(diets); given(ptService.getPtTrainerTraineeWithTraineeId(trainee.getId())).willReturn(ptTrainerTrainee); given(ptService.getPtLessonWithPtTrainerTrainee(ptTrainerTrainee)).willReturn(ptLessons); + given(dietRepository.saveAll(diets)).willReturn(diets); // when withdrawService.withdraw(traineeMember.getId()); @@ -227,6 +237,7 @@ void withdraw_trainee_without_pt_success() { given(dietService.getAllByTraineeId(trainee.getId())).willReturn(diets); given(ptService.getPtTrainerTraineeWithTraineeId(trainee.getId())).willThrow(NotFoundException.class); given(traineeRepository.save(trainee)).willReturn(trainee); + given(dietRepository.saveAll(diets)).willReturn(diets); given(memberRepository.save(traineeMember)).willReturn(traineeMember); // when diff --git a/src/test/java/com/tnt/member/presentation/MemberControllerTest.java b/src/test/java/com/tnt/member/presentation/MemberControllerTest.java index bded116b..cbd2af41 100644 --- a/src/test/java/com/tnt/member/presentation/MemberControllerTest.java +++ b/src/test/java/com/tnt/member/presentation/MemberControllerTest.java @@ -5,6 +5,8 @@ import static com.tnt.member.domain.MemberType.TRAINEE; import static com.tnt.member.domain.MemberType.TRAINER; import static com.tnt.member.domain.SocialType.KAKAO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpMethod.PUT; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.IMAGE_JPEG_VALUE; import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; @@ -52,6 +54,7 @@ import com.tnt.member.application.repository.MemberRepository; import com.tnt.member.domain.Member; import com.tnt.member.dto.request.SignUpRequest; +import com.tnt.member.dto.request.UpdateMemberInfoRequest; import com.tnt.pt.application.repository.PtTrainerTraineeRepository; import com.tnt.pt.domain.PtTrainerTrainee; import com.tnt.trainee.application.repository.PtGoalRepository; @@ -321,6 +324,211 @@ void get_member_info_trainee_success() throws Exception { .andDo(print()); } + @Test + @DisplayName("통합 테스트 - 회원 프로필 사진 수정 성공") + void update_member_profile_success() throws Exception { + // given + Member trainerMember = MemberFixture.getTrainerMember1(); + + trainerMember = memberRepository.save(trainerMember); + + CustomUserDetails traineeUserDetails = new CustomUserDetails(trainerMember.getId(), + String.valueOf(trainerMember.getId()), List.of(new SimpleGrantedAuthority("ROLE_USER"))); + + Authentication authentication = new UsernamePasswordAuthenticationToken(traineeUserDetails, null, + authoritiesMapper.mapAuthorities(traineeUserDetails.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + Trainer trainer = TrainerFixture.getTrainer1(trainerMember); + + trainerRepository.save(trainer); + + UpdateMemberInfoRequest request = new UpdateMemberInfoRequest(true, TRAINER, "홍길동", null, null, null, null, + null); + + // when & then + var jsonRequest = new MockMultipartFile("request", "", APPLICATION_JSON_VALUE, + objectMapper.writeValueAsString(request).getBytes()); + + mockMvc.perform(multipart(PUT, "/members") + .file(jsonRequest) + .file(profileImage) + .contentType(MULTIPART_FORM_DATA_VALUE)) + .andExpect(status().isOk()) + .andDo(print()); + + Member updateMember = memberRepository.findById(trainerMember.getId()); + assertThat(updateMember).isNotNull(); + assertThat(updateMember.getName()).isEqualTo(request.name()); + assertThat(updateMember.getProfileImageUrl()).isEqualTo(TRAINER_DEFAULT_IMAGE); + } + + @Test + @DisplayName("통합 테스트 - 회원 프로필 사진 유지 성공") + void maintain_member_profile_success() throws Exception { + // given + Member trainerMember = MemberFixture.getTrainerMember1(); + + trainerMember = memberRepository.save(trainerMember); + + CustomUserDetails traineeUserDetails = new CustomUserDetails(trainerMember.getId(), + String.valueOf(trainerMember.getId()), List.of(new SimpleGrantedAuthority("ROLE_USER"))); + + Authentication authentication = new UsernamePasswordAuthenticationToken(traineeUserDetails, null, + authoritiesMapper.mapAuthorities(traineeUserDetails.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + Trainer trainer = TrainerFixture.getTrainer1(trainerMember); + + trainerRepository.save(trainer); + + UpdateMemberInfoRequest request = new UpdateMemberInfoRequest(false, TRAINER, "홍길동", null, null, null, null, + null); + + // when & then + var jsonRequest = new MockMultipartFile("request", "", APPLICATION_JSON_VALUE, + objectMapper.writeValueAsString(request).getBytes()); + + mockMvc.perform(multipart(PUT, "/members") + .file(jsonRequest) + .contentType(MULTIPART_FORM_DATA_VALUE)) + .andExpect(status().isOk()) + .andDo(print()); + + Member updateMember = memberRepository.findById(trainerMember.getId()); + assertThat(updateMember).isNotNull(); + assertThat(updateMember.getName()).isEqualTo(request.name()); + assertThat(updateMember.getProfileImageUrl()).isEqualTo(trainerMember.getProfileImageUrl()); + } + + @Test + @DisplayName("통합 테스트 - 회원 프로필 사진 삭제 성공") + void delete_member_profile_success() throws Exception { + // given + Member trainerMember = MemberFixture.getTrainerMember1(); + + trainerMember = memberRepository.save(trainerMember); + + CustomUserDetails traineeUserDetails = new CustomUserDetails(trainerMember.getId(), + String.valueOf(trainerMember.getId()), List.of(new SimpleGrantedAuthority("ROLE_USER"))); + + Authentication authentication = new UsernamePasswordAuthenticationToken(traineeUserDetails, null, + authoritiesMapper.mapAuthorities(traineeUserDetails.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + Trainer trainer = TrainerFixture.getTrainer1(trainerMember); + + trainerRepository.save(trainer); + + UpdateMemberInfoRequest request = new UpdateMemberInfoRequest(true, TRAINER, "홍길동", null, null, null, null, + null); + + // when & then + var jsonRequest = new MockMultipartFile("request", "", APPLICATION_JSON_VALUE, + objectMapper.writeValueAsString(request).getBytes()); + + mockMvc.perform(multipart(PUT, "/members") + .file(jsonRequest) + .contentType(MULTIPART_FORM_DATA_VALUE)) + .andExpect(status().isOk()) + .andDo(print()); + + Member updateMember = memberRepository.findById(trainerMember.getId()); + assertThat(updateMember).isNotNull(); + assertThat(updateMember.getName()).isEqualTo(request.name()); + assertThat(updateMember.getProfileImageUrl()).isEqualTo(TRAINER_DEFAULT_IMAGE); + } + + @Test + @DisplayName("통합 테스트 - 트레이너 회원 정보 수정 성공") + void update_member_info_trainer_success() throws Exception { + // given + Member trainerMember = MemberFixture.getTrainerMember1(); + + trainerMember = memberRepository.save(trainerMember); + + CustomUserDetails traineeUserDetails = new CustomUserDetails(trainerMember.getId(), + String.valueOf(trainerMember.getId()), List.of(new SimpleGrantedAuthority("ROLE_USER"))); + + Authentication authentication = new UsernamePasswordAuthenticationToken(traineeUserDetails, null, + authoritiesMapper.mapAuthorities(traineeUserDetails.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + Trainer trainer = TrainerFixture.getTrainer1(trainerMember); + + trainerRepository.save(trainer); + + UpdateMemberInfoRequest request = new UpdateMemberInfoRequest(true, TRAINER, "홍길동", null, null, null, null, + null); + + // when & then + var jsonRequest = new MockMultipartFile("request", "", APPLICATION_JSON_VALUE, + objectMapper.writeValueAsString(request).getBytes()); + + mockMvc.perform(multipart(PUT, "/members") + .file(jsonRequest) + .file(profileImage) + .contentType(MULTIPART_FORM_DATA_VALUE)) + .andExpect(status().isOk()) + .andDo(print()); + + Member updateMember = memberRepository.findAll().getFirst(); + assertThat(updateMember).isNotNull(); + assertThat(updateMember.getName()).isEqualTo(request.name()); + } + + @Test + @DisplayName("통합 테스트 - 트레이니 회원 정보 수정 성공") + void update_member_info_trainee_success() throws Exception { + // given + Member traineeMember = MemberFixture.getTraineeMember1(); + + traineeMember = memberRepository.save(traineeMember); + + CustomUserDetails traineeUserDetails = new CustomUserDetails(traineeMember.getId(), + String.valueOf(traineeMember.getId()), List.of(new SimpleGrantedAuthority("ROLE_USER"))); + + Authentication authentication = new UsernamePasswordAuthenticationToken(traineeUserDetails, null, + authoritiesMapper.mapAuthorities(traineeUserDetails.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + Trainee trainee = TraineeFixture.getTrainee2(traineeMember); + + trainee = traineeRepository.save(trainee); + + List ptGoals = PtGoalsFixture.getPtGoals(trainee.getId()); + + ptGoalRepository.saveAll(ptGoals); + + UpdateMemberInfoRequest request = new UpdateMemberInfoRequest(true, TRAINEE, "홍길동", LocalDate.of(1990, 1, 1), + 175.0, 70.0, "테스트 주의사항", List.of("체중 감량", "건강 관리")); + + // when & then + var jsonRequest = new MockMultipartFile("request", "", APPLICATION_JSON_VALUE, + objectMapper.writeValueAsString(request).getBytes()); + + mockMvc.perform(multipart(PUT, "/members") + .file(jsonRequest) + .file(profileImage) + .contentType(MULTIPART_FORM_DATA_VALUE)) + .andExpect(status().isOk()) + .andDo(print()); + + Member updateMember = memberRepository.findById(traineeMember.getId()); + Trainee updateTrainee = traineeRepository.find(null, trainee.getId()); + assertThat(updateMember).isNotNull(); + assertThat(updateMember.getName()).isEqualTo(request.name()); + assertThat(updateMember.getBirthday()).isEqualTo(request.birthday()); + assertThat(updateTrainee.getHeight()).isEqualTo(request.height()); + assertThat(updateTrainee.getWeight()).isEqualTo(request.weight()); + assertThat(updateTrainee.getCautionNote()).isEqualTo(request.cautionNote()); + } + @TestConfiguration static class TestConfig {