diff --git a/src/main/java/com/e2i/wemeet/config/log/LogExceptionPattern.java b/src/main/java/com/e2i/wemeet/config/log/LogExceptionPattern.java index a04183f0..88c3e457 100644 --- a/src/main/java/com/e2i/wemeet/config/log/LogExceptionPattern.java +++ b/src/main/java/com/e2i/wemeet/config/log/LogExceptionPattern.java @@ -12,11 +12,13 @@ private LogExceptionPattern() { public static final Pattern SWAGGER_UI_RESOURCE_PATTERN = Pattern.compile("^/static/dist.*"); public static final Pattern SWAGGER_SPECIFICATION_PATTERN = Pattern.compile("^/static/swagger-ui.*"); public static final Pattern SWAGGER_API_PATTERN = Pattern.compile("^/api-docs.*"); + public static final Pattern H2_CONSOLE_PATTERN = Pattern.compile("^/h2-console.*"); public static final List LOG_EXCEPTION_LIST = List.of( SWAGGER_SPECIFICATION_PATTERN, SWAGGER_UI_RESOURCE_PATTERN, - SWAGGER_API_PATTERN + SWAGGER_API_PATTERN, + H2_CONSOLE_PATTERN ); public static boolean isMatchedExceptionUrls(final String requestUrl) { diff --git a/src/main/java/com/e2i/wemeet/controller/member/RecommendController.java b/src/main/java/com/e2i/wemeet/controller/member/RecommendController.java new file mode 100644 index 00000000..fb4bb51f --- /dev/null +++ b/src/main/java/com/e2i/wemeet/controller/member/RecommendController.java @@ -0,0 +1,27 @@ +package com.e2i.wemeet.controller.member; + +import com.e2i.wemeet.config.resolver.member.MemberId; +import com.e2i.wemeet.dto.request.member.RecommenderRequestDto; +import com.e2i.wemeet.dto.response.ResponseDto; +import com.e2i.wemeet.service.member.RecommendService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/v1/recommend") +@RestController +public class RecommendController { + + private final RecommendService recommendService; + + @PostMapping + public ResponseDto recommend(@MemberId Long memberId, + @RequestBody @Valid RecommenderRequestDto requestDto) { + recommendService.recommend(memberId, requestDto.phoneNumber()); + return ResponseDto.success("Recommend Success", null); + } +} diff --git a/src/main/java/com/e2i/wemeet/controller/team/TeamImageController.java b/src/main/java/com/e2i/wemeet/controller/team/TeamImageController.java new file mode 100644 index 00000000..ca0cc115 --- /dev/null +++ b/src/main/java/com/e2i/wemeet/controller/team/TeamImageController.java @@ -0,0 +1,44 @@ +package com.e2i.wemeet.controller.team; + +import com.e2i.wemeet.config.resolver.member.MemberId; +import com.e2i.wemeet.dto.request.team.DeleteTeamImageRequestDto; +import com.e2i.wemeet.dto.response.ResponseDto; +import com.e2i.wemeet.service.team.TeamImageService; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +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; + +@RequiredArgsConstructor +@RequestMapping("/v1/team/image") +@RestController +public class TeamImageController { + + private final TeamImageService teamImageService; + + @PostMapping + public ResponseDto> upload(@MemberId Long memberId, @RequestPart("images") List images) { + List uploadedImageUrls = teamImageService.uploadTeamImage(memberId, images); + return ResponseDto.success("Team Image Upload Success", uploadedImageUrls); + } + + @PutMapping + public ResponseDto> update(@MemberId Long memberId, @RequestPart("images") List images) { + List uploadedImageUrls = teamImageService.updateTeamImage(memberId, images); + return ResponseDto.success("Team Image Update Success", uploadedImageUrls); + } + + @DeleteMapping + public ResponseDto delete(@Valid @RequestBody DeleteTeamImageRequestDto requestDto) { + teamImageService.deleteTeamImage(requestDto.deleteImageUrls()); + return ResponseDto.success("Delete Team Image Success"); + } + +} diff --git a/src/main/java/com/e2i/wemeet/domain/cost/Earn.java b/src/main/java/com/e2i/wemeet/domain/cost/Earn.java index 67098f05..27395476 100644 --- a/src/main/java/com/e2i/wemeet/domain/cost/Earn.java +++ b/src/main/java/com/e2i/wemeet/domain/cost/Earn.java @@ -5,6 +5,7 @@ @Getter public enum Earn { EVENT("이벤트"), + RECOMMEND("친구에게 추천하여 가입"), ADVERTISEMENT("광고 시청"); private final String detail; diff --git a/src/main/java/com/e2i/wemeet/domain/member/Member.java b/src/main/java/com/e2i/wemeet/domain/member/Member.java index 2f2e3b22..7dad1261 100644 --- a/src/main/java/com/e2i/wemeet/domain/member/Member.java +++ b/src/main/java/com/e2i/wemeet/domain/member/Member.java @@ -15,6 +15,7 @@ import com.e2i.wemeet.dto.request.member.UpdateMemberRequestDto; import com.e2i.wemeet.exception.badrequest.MemberHasBeenDeletedException; import com.e2i.wemeet.exception.badrequest.ProfileImageNotExistsException; +import com.e2i.wemeet.exception.badrequest.RecommenderAlreadyExist; import com.e2i.wemeet.exception.badrequest.TeamExistsException; import com.e2i.wemeet.exception.badrequest.TeamNotExistsException; import com.e2i.wemeet.exception.unauthorized.CreditNotEnoughException; @@ -85,6 +86,10 @@ public class Member extends BaseTimeEntity { @Embedded private ProfileImage profileImage; + @Convert(converter = CryptoConverter.class) + @Column(length = 24) + private String recommenderPhone; + @Enumerated(EnumType.STRING) @Column(nullable = false) private Role role; @@ -101,11 +106,10 @@ public class Member extends BaseTimeEntity { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List history = new ArrayList<>(); - @Builder public Member(String nickname, Gender gender, String phoneNumber, String email, - CollegeInfo collegeInfo, Mbti mbti, Integer credit, Boolean imageAuth, - Boolean allowMarketing, ProfileImage profileImage, Role role) { + CollegeInfo collegeInfo, Mbti mbti, Integer credit, Boolean allowMarketing, + ProfileImage profileImage, Role role) { this.nickname = nickname; this.gender = gender; this.phoneNumber = phoneNumber; @@ -223,5 +227,12 @@ public boolean isProfileImageExists() { public String getCollegeName() { return this.collegeInfo.getCollegeCode().getCodeValue(); } + + public void registerRecommender(final String recommenderPhone) { + if (this.recommenderPhone != null) { + throw new RecommenderAlreadyExist(); + } + this.recommenderPhone = recommenderPhone; + } } diff --git a/src/main/java/com/e2i/wemeet/domain/team_image/TeamImageRepository.java b/src/main/java/com/e2i/wemeet/domain/team_image/TeamImageRepository.java index 6b4bee5c..04aad4e0 100644 --- a/src/main/java/com/e2i/wemeet/domain/team_image/TeamImageRepository.java +++ b/src/main/java/com/e2i/wemeet/domain/team_image/TeamImageRepository.java @@ -1,6 +1,13 @@ package com.e2i.wemeet.domain.team_image; +import com.e2i.wemeet.domain.team.Team; +import java.util.Collection; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface TeamImageRepository extends JpaRepository { @@ -8,4 +15,17 @@ public interface TeamImageRepository extends JpaRepository { * 팀 이미지 전체 삭제 */ void deleteAllByTeamTeamId(Long teamId); + + @Query("SELECT t FROM Team t JOIN t.teamLeader m WHERE m.memberId = :memberId") + Optional findTeamByMemberId(@Param("memberId") Long memberId); + + @Query("SELECT ti FROM TeamImage ti JOIN ti.team t WHERE t.teamId = :teamId") + List findTeamImagesByTeamId(@Param("teamId") Long teamId); + + int countByTeamTeamId(Long teamId); + + @Modifying(clearAutomatically = true) + @Query("DELETE FROM TeamImage ti WHERE ti.teamImageUrl IN :teamImageUrls") + void deleteAllByTeamImageUrl(@Param("teamImageUrls") Collection teamImageUrls); + } diff --git a/src/main/java/com/e2i/wemeet/dto/request/member/CreateMemberRequestDto.java b/src/main/java/com/e2i/wemeet/dto/request/member/CreateMemberRequestDto.java index 278ed65e..39aa6f2c 100644 --- a/src/main/java/com/e2i/wemeet/dto/request/member/CreateMemberRequestDto.java +++ b/src/main/java/com/e2i/wemeet/dto/request/member/CreateMemberRequestDto.java @@ -51,7 +51,6 @@ public Member toEntity(Code collegeCode) { .mbti(Mbti.valueOf(this.mbti)) .allowMarketing(this.allowMarketing) .credit(40) - .imageAuth(false) .role(Role.USER) .build(); } diff --git a/src/main/java/com/e2i/wemeet/dto/request/member/RecommenderRequestDto.java b/src/main/java/com/e2i/wemeet/dto/request/member/RecommenderRequestDto.java new file mode 100644 index 00000000..5eebeb76 --- /dev/null +++ b/src/main/java/com/e2i/wemeet/dto/request/member/RecommenderRequestDto.java @@ -0,0 +1,14 @@ +package com.e2i.wemeet.dto.request.member; + +import com.e2i.wemeet.util.validator.bean.PhoneValid; +import jakarta.validation.constraints.NotNull; + +public record RecommenderRequestDto( + + @NotNull + @PhoneValid + String phoneNumber + +) { + +} diff --git a/src/main/java/com/e2i/wemeet/dto/request/team/DeleteTeamImageRequestDto.java b/src/main/java/com/e2i/wemeet/dto/request/team/DeleteTeamImageRequestDto.java new file mode 100644 index 00000000..cb04d1eb --- /dev/null +++ b/src/main/java/com/e2i/wemeet/dto/request/team/DeleteTeamImageRequestDto.java @@ -0,0 +1,13 @@ +package com.e2i.wemeet.dto.request.team; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +public record DeleteTeamImageRequestDto( + + @NotEmpty + List deleteImageUrls + +) { + +} diff --git a/src/main/java/com/e2i/wemeet/dto/request/team/UpdateTeamRequestDto.java b/src/main/java/com/e2i/wemeet/dto/request/team/UpdateTeamRequestDto.java index bb6926f4..5883a974 100644 --- a/src/main/java/com/e2i/wemeet/dto/request/team/UpdateTeamRequestDto.java +++ b/src/main/java/com/e2i/wemeet/dto/request/team/UpdateTeamRequestDto.java @@ -3,7 +3,6 @@ import com.e2i.wemeet.util.validator.bean.AdditionalActivityValid; import com.e2i.wemeet.util.validator.bean.DrinkRateValid; import com.e2i.wemeet.util.validator.bean.DrinkWithGameValid; -import com.e2i.wemeet.util.validator.bean.KakaoOpenChatLinkValid; import com.e2i.wemeet.util.validator.bean.RegionValid; import jakarta.annotation.Nullable; import jakarta.validation.Valid; @@ -37,7 +36,6 @@ public record UpdateTeamRequestDto( String introduction, @NotNull - @KakaoOpenChatLinkValid String chatLink, @NotNull diff --git a/src/main/java/com/e2i/wemeet/dto/response/credential/SmsCredentialResponse.java b/src/main/java/com/e2i/wemeet/dto/response/credential/SmsCredentialResponse.java index 33409e96..ec5c2b32 100644 --- a/src/main/java/com/e2i/wemeet/dto/response/credential/SmsCredentialResponse.java +++ b/src/main/java/com/e2i/wemeet/dto/response/credential/SmsCredentialResponse.java @@ -4,11 +4,11 @@ import java.util.Collection; import org.springframework.security.core.GrantedAuthority; -// TODO :: service refactoring // SMS 인증 응답 public record SmsCredentialResponse( Long memberId, boolean isRegistered, + boolean withdrawal, Collection role ) { @@ -16,6 +16,7 @@ public static SmsCredentialResponse of(final MemberPrincipal principal) { return new SmsCredentialResponse( principal.getMemberId(), principal.isRegistered(), + principal.isWithdrawal(), principal.getAuthorities() ); } diff --git a/src/main/java/com/e2i/wemeet/exception/ErrorCode.java b/src/main/java/com/e2i/wemeet/exception/ErrorCode.java index fd46999e..956aab14 100644 --- a/src/main/java/com/e2i/wemeet/exception/ErrorCode.java +++ b/src/main/java/com/e2i/wemeet/exception/ErrorCode.java @@ -49,6 +49,8 @@ public enum ErrorCode { SUGGESTION_HISTORY_EXISTS(40035, "suggestion.history.exists"), DUPLICATE_MEETING_REQUEST(40041, "duplicate.meeting.request"), MEETING_ALREADY_EXIST(40042, "meeting.already.exist"), + RECOMMENDER_ALREADY_EXIST(40043, "recommender.already.exist"), + IMAGE_COUNT_EXCEEDED(40044, "image.count.exceeded"), NOTFOUND_SMS_CREDENTIAL(40100, "notfound.sms.credential"), MEMBER_NOT_FOUND(40101, "member.not.found"), diff --git a/src/main/java/com/e2i/wemeet/exception/badrequest/ImageCountExceedException.java b/src/main/java/com/e2i/wemeet/exception/badrequest/ImageCountExceedException.java new file mode 100644 index 00000000..efcd79c3 --- /dev/null +++ b/src/main/java/com/e2i/wemeet/exception/badrequest/ImageCountExceedException.java @@ -0,0 +1,11 @@ +package com.e2i.wemeet.exception.badrequest; + +import com.e2i.wemeet.exception.ErrorCode; + +public class ImageCountExceedException extends BadRequestException { + + public ImageCountExceedException() { + super(ErrorCode.IMAGE_COUNT_EXCEEDED); + } + +} diff --git a/src/main/java/com/e2i/wemeet/exception/badrequest/RecommenderAlreadyExist.java b/src/main/java/com/e2i/wemeet/exception/badrequest/RecommenderAlreadyExist.java new file mode 100644 index 00000000..748c168b --- /dev/null +++ b/src/main/java/com/e2i/wemeet/exception/badrequest/RecommenderAlreadyExist.java @@ -0,0 +1,11 @@ +package com.e2i.wemeet.exception.badrequest; + +import static com.e2i.wemeet.exception.ErrorCode.RECOMMENDER_ALREADY_EXIST; + +public class RecommenderAlreadyExist extends BadRequestException { + + public RecommenderAlreadyExist() { + super(RECOMMENDER_ALREADY_EXIST); + } + +} diff --git a/src/main/java/com/e2i/wemeet/security/model/MemberPrincipal.java b/src/main/java/com/e2i/wemeet/security/model/MemberPrincipal.java index f7dda7b1..835f30e9 100644 --- a/src/main/java/com/e2i/wemeet/security/model/MemberPrincipal.java +++ b/src/main/java/com/e2i/wemeet/security/model/MemberPrincipal.java @@ -17,6 +17,7 @@ public class MemberPrincipal implements UserDetails { private final Long memberId; + private final boolean withdrawal; /* * Authority 는 인가 정책을 적용할 때 필요함 @@ -27,21 +28,25 @@ public class MemberPrincipal implements UserDetails { public MemberPrincipal() { this.memberId = null; + this.withdrawal = false; this.authorities = List.of(Role.GUEST::getRoleAttachedPrefix); } public MemberPrincipal(final Member member) { this.memberId = member.getMemberId(); + this.withdrawal = member.getDeletedAt() != null; this.authorities = getAuthorities(member.getRole().name()); } public MemberPrincipal(final Payload payload) { this.memberId = payload.getMemberId(); + this.withdrawal = false; this.authorities = getAuthorities(payload.getRole()); } public MemberPrincipal(final Long memberId, final String role) { this.memberId = memberId; + this.withdrawal = false; this.authorities = getAuthorities(role); } @@ -59,6 +64,10 @@ public boolean isRegistered() { .orElseGet(() -> null) == null; } + public boolean isWithdrawal() { + return this.withdrawal; + } + public boolean hasManagerRole() { return AuthorityUtils.authorityListToSet(getAuthorities()) .contains(Role.getRoleAttachedPrefix(Role.MANAGER.name())); diff --git a/src/main/java/com/e2i/wemeet/service/aws/s3/S3Service.java b/src/main/java/com/e2i/wemeet/service/aws/s3/S3Service.java index d4440695..93990f89 100644 --- a/src/main/java/com/e2i/wemeet/service/aws/s3/S3Service.java +++ b/src/main/java/com/e2i/wemeet/service/aws/s3/S3Service.java @@ -8,4 +8,9 @@ public interface S3Service { * S3 오브젝트 업로드 * */ void upload(MultipartFile multipartFile, String objectKey, String bucket); + + /* + * S3 오브젝트 삭제 + * */ + void delete(String bucket, String targetObjectKeyPrefix); } diff --git a/src/main/java/com/e2i/wemeet/service/aws/s3/S3ServiceImpl.java b/src/main/java/com/e2i/wemeet/service/aws/s3/S3ServiceImpl.java index 7a2a5a07..da1f527c 100644 --- a/src/main/java/com/e2i/wemeet/service/aws/s3/S3ServiceImpl.java +++ b/src/main/java/com/e2i/wemeet/service/aws/s3/S3ServiceImpl.java @@ -9,24 +9,30 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.model.S3Object; @RequiredArgsConstructor @Service public class S3ServiceImpl implements S3Service { private final S3Client s3Client; + private static final String WHOLE_PATH = "v1/"; @Override - public void upload(MultipartFile multipartFile, String objectKey, String bucket) { + public void upload(final MultipartFile multipartFile, final String objectKey, final String bucket) { File file = convertMultipartFileToFile(multipartFile); Map metadata = new HashMap<>(); @@ -46,6 +52,46 @@ public void upload(MultipartFile multipartFile, String objectKey, String bucket) } } + @Override + public void delete(final String bucket, final String targetObjectKeyPrefix) { + checkTargetKeyPrefixValid(targetObjectKeyPrefix); + List targetKeys = getObjectKeys(bucket, targetObjectKeyPrefix); + + targetKeys.stream() + .map(key -> DeleteObjectRequest.builder() + .bucket(bucket) + .key(key) + .build() + ) + .forEach(s3Client::deleteObject); + } + + // S3 전체 리소스 삭제 방지 + private void checkTargetKeyPrefixValid(String targetObjectKeyPrefix) { + if (!StringUtils.hasText(targetObjectKeyPrefix) || targetObjectKeyPrefix.equals("/")) { + throw new IllegalArgumentException("S3 object key prefix must not be empty"); + } + + for (int i = 1; i <= WHOLE_PATH.length(); i++) { + if (targetObjectKeyPrefix.equals(WHOLE_PATH.substring(0, i))) { + throw new IllegalArgumentException("S3 object key prefix must not be whole path"); + } + } + } + + // 주어진 버킷에서 주어진 prefix 가 붙은 object의 key 목록을 조회한다. + private List getObjectKeys(String bucket, String targetObjectKeyPrefix) { + ListObjectsV2Request listObjectsV2Request = ListObjectsV2Request.builder() + .bucket(bucket) + .prefix(targetObjectKeyPrefix) + .build(); + + return s3Client.listObjectsV2(listObjectsV2Request).contents() + .stream() + .map(S3Object::key) + .toList(); + } + private static File convertMultipartFileToFile(MultipartFile multipartFile) { String uniqueFileName = UUID.randomUUID().toString(); diff --git a/src/main/java/com/e2i/wemeet/service/cost/CostService.java b/src/main/java/com/e2i/wemeet/service/cost/CostService.java index 8430b7dc..8e2bd6e3 100644 --- a/src/main/java/com/e2i/wemeet/service/cost/CostService.java +++ b/src/main/java/com/e2i/wemeet/service/cost/CostService.java @@ -49,7 +49,8 @@ public void spend(final SpendEvent event) { @EventListener(classes = EarnEvent.class) public void earn(final EarnEvent event) { Member member = memberRepository.findByMemberId(event.memberId()) - .orElseThrow(MemberNotFoundException::new); + .orElseThrow(MemberNotFoundException::new) + .checkMemberValid(); member.addCredit(event.value()); diff --git a/src/main/java/com/e2i/wemeet/service/member/RecommendService.java b/src/main/java/com/e2i/wemeet/service/member/RecommendService.java new file mode 100644 index 00000000..b452e0d0 --- /dev/null +++ b/src/main/java/com/e2i/wemeet/service/member/RecommendService.java @@ -0,0 +1,9 @@ +package com.e2i.wemeet.service.member; + +public interface RecommendService { + + // 추천인 입력 (추천인 전화번호) + // @return 추천인의 memberId + public void recommend(final Long memberId, final String recommenderPhone); + +} diff --git a/src/main/java/com/e2i/wemeet/service/member/RecommendServiceImpl.java b/src/main/java/com/e2i/wemeet/service/member/RecommendServiceImpl.java new file mode 100644 index 00000000..fd08d22c --- /dev/null +++ b/src/main/java/com/e2i/wemeet/service/member/RecommendServiceImpl.java @@ -0,0 +1,38 @@ +package com.e2i.wemeet.service.member; + +import static com.e2i.wemeet.domain.cost.Earn.RECOMMEND; + +import com.e2i.wemeet.domain.member.Member; +import com.e2i.wemeet.domain.member.MemberRepository; +import com.e2i.wemeet.exception.notfound.MemberNotFoundException; +import com.e2i.wemeet.service.cost.EarnEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class RecommendServiceImpl implements RecommendService { + + private static final int RECOMMEND_COST = 20; + + private final MemberRepository memberRepository; + private final ApplicationEventPublisher publisher; + + @Transactional + public void recommend(final Long memberId, final String recommenderPhone) { + Member member = memberRepository.findByMemberId(memberId) + .orElseThrow(MemberNotFoundException::new) + .checkMemberValid(); + Member recommender = memberRepository.findByPhoneNumber(recommenderPhone) + .orElseThrow(MemberNotFoundException::new) + .checkMemberValid(); + member.registerRecommender(recommenderPhone); + + publisher.publishEvent( + EarnEvent.of(RECOMMEND, RECOMMEND_COST, recommender.getMemberId()) + ); + } +} diff --git a/src/main/java/com/e2i/wemeet/service/team/S3TeamImageService.java b/src/main/java/com/e2i/wemeet/service/team/S3TeamImageService.java new file mode 100644 index 00000000..70b9789a --- /dev/null +++ b/src/main/java/com/e2i/wemeet/service/team/S3TeamImageService.java @@ -0,0 +1,117 @@ +package com.e2i.wemeet.service.team; + +import com.e2i.wemeet.domain.team.Team; +import com.e2i.wemeet.domain.team_image.TeamImage; +import com.e2i.wemeet.domain.team_image.TeamImageRepository; +import com.e2i.wemeet.exception.badrequest.ImageCountExceedException; +import com.e2i.wemeet.exception.notfound.TeamNotFoundException; +import com.e2i.wemeet.security.manager.IsManager; +import com.e2i.wemeet.service.aws.s3.S3Service; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class S3TeamImageService implements TeamImageService { + + private final TeamImageRepository teamImageRepository; + private final S3Service s3Service; + + public static final String TEAM_IMAGE_PATH = "v1/%d/"; + public static final String TEAM_IMAGE_KEY = TEAM_IMAGE_PATH + "%d/%s.jpg"; + public static final int MAX_IMAGE_COUNT = 10; + + @Value("${aws.s3.teamImageBucket}") + private String teamImageBucket; + + @Value("${aws.cloudFront.teamImageDomain}") + private String teamImageDomain; + + /* + * 팀 이미지 업로드 + * - 새로운 이미지 업로드 (기존 이미지 순서 뒤에 위치) + * */ + @Transactional + @IsManager + @Override + public List uploadTeamImage(final Long memberId, final List images) { + Team team = teamImageRepository.findTeamByMemberId(memberId) + .orElseThrow(TeamNotFoundException::new) + .checkTeamValid(); + final int imageCount = teamImageRepository.countByTeamTeamId(team.getTeamId()); + + if (imageCount + images.size() > MAX_IMAGE_COUNT) { + throw new ImageCountExceedException(); + } + + // 이미지 업로드 & TeamImage 테이블에 이미지 정보 저장 + int startIndex = imageCount + 1; + return uploadImagesAndSaveTeamImage(images, team, startIndex); + } + + /* + * 팀 이미지 수정 + * - 기존 이미지들 삭제 후, 새로운 이미지 추가 + * */ + @Transactional + @IsManager + @Override + public List updateTeamImage(final Long memberId, final List images) { + Team team = teamImageRepository.findTeamByMemberId(memberId) + .orElseThrow(TeamNotFoundException::new) + .checkTeamValid(); + + // 기존 이미지 삭제 + final String teamImageUrlPath = String.format(TEAM_IMAGE_PATH, team.getTeamId()); + s3Service.delete(teamImageBucket, teamImageUrlPath); + teamImageRepository.deleteAllByTeamTeamId(team.getTeamId()); + + // 이미지 업로드 & TeamImage 테이블에 이미지 정보 저장 + return uploadImagesAndSaveTeamImage(images, team, 1); + } + + /* + * 팀 이미지 삭제 + * - 지정된 URL에 해당하는 이미지 삭제 + * */ + @Transactional + @IsManager + @Override + public void deleteTeamImage(final List imageUrls) { + // 이미지 삭제 + imageUrls.stream() + .map(imageUrl -> imageUrl.replace(teamImageDomain, "")) + .forEach(imageUrl -> s3Service.delete(teamImageBucket, imageUrl)); + teamImageRepository.deleteAllByTeamImageUrl(imageUrls); + } + + // S3에 이미지 업로드 & TeamImage 테이블에 이미지 정보 저장 + private List uploadImagesAndSaveTeamImage(final List images, final Team team, int startIndex) { + List uploadedImageUrls = new ArrayList<>(); + + for (MultipartFile image : images) { + final String randomKey = UUID.randomUUID().toString(); + final String imagePath = String.format(TEAM_IMAGE_KEY, team.getTeamId(), startIndex, randomKey); + s3Service.upload(image, imagePath, teamImageBucket); + + final String imageUrl = teamImageDomain + imagePath; + teamImageRepository.save(TeamImage.builder() + .team(team) + .teamImageUrl(imageUrl) + .sequence(startIndex) + .build()); + startIndex++; + uploadedImageUrls.add(imageUrl); + } + + return uploadedImageUrls; + } + +} diff --git a/src/main/java/com/e2i/wemeet/service/team/TeamImageService.java b/src/main/java/com/e2i/wemeet/service/team/TeamImageService.java new file mode 100644 index 00000000..287ca9d0 --- /dev/null +++ b/src/main/java/com/e2i/wemeet/service/team/TeamImageService.java @@ -0,0 +1,23 @@ +package com.e2i.wemeet.service.team; + +import java.util.List; +import org.springframework.web.multipart.MultipartFile; + +public interface TeamImageService { + + /* + * 팀 이미지 upload + * */ + public List uploadTeamImage(Long memberId, List images); + + /* + * 팀 이미지 update + * */ + public List updateTeamImage(Long memberId, List images); + + /* + * 팀 이미지 delete + * */ + public void deleteTeamImage(List imageUrls); + +} diff --git a/src/main/java/com/e2i/wemeet/service/team/TeamServiceImpl.java b/src/main/java/com/e2i/wemeet/service/team/TeamServiceImpl.java index 70df0c4d..588706b7 100644 --- a/src/main/java/com/e2i/wemeet/service/team/TeamServiceImpl.java +++ b/src/main/java/com/e2i/wemeet/service/team/TeamServiceImpl.java @@ -134,6 +134,9 @@ public void deleteTeam(Long memberId) { private void updateTeamImages(List images, Team team) { + if (images.isEmpty()) { + return; + } teamImageRepository.deleteAllByTeamTeamId(team.getTeamId()); uploadTeamImages(images, team); } diff --git a/src/main/resources/db/migration/V2306210150__create_member.sql b/src/main/resources/db/migration/V2306210150__create_member.sql index 77bab7ef..ffc3a663 100644 --- a/src/main/resources/db/migration/V2306210150__create_member.sql +++ b/src/main/resources/db/migration/V2306210150__create_member.sql @@ -11,10 +11,10 @@ CREATE TABLE IF NOT EXISTS `member` `email` varchar(70), `mbti` tinyint NOT NULL, `credit` smallint UNSIGNED NOT NULL, - `image_auth` tinyint DEFAULT 0, + `image_auth` tinyint DEFAULT 0, `basic_url` varchar(150), `low_url` varchar(150), - `allow_marketing` tinyint NOT NULL DEFAULT 1, + `allow_marketing` tinyint NOT NULL DEFAULT 1, `created_at` datetime(6), `modified_at` datetime(6), `deleted_at` datetime(6), diff --git a/src/main/resources/db/migration/V2309251950__update_member.sql b/src/main/resources/db/migration/V2309251950__update_member.sql new file mode 100644 index 00000000..de1ba6d3 --- /dev/null +++ b/src/main/resources/db/migration/V2309251950__update_member.sql @@ -0,0 +1,2 @@ +ALTER TABLE `member` + ADD `recommender_phone` char(24) default null AFTER `deleted_at`; \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 4843fec5..8583dbe8 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -85,6 +85,8 @@ meeting.request.not.found=미팅 요청을 찾을 수 없습니다. meeting.not.found=미팅을 찾을 수 없습니다. cost.not.found=Cost 정보를 찾을 수 없습니다. refresh.token.not.exist=저장소에 해당 유저의 RefreshToken이 존재하지 않습니다. +recommender.already.exist=유저 추천은 계정당 1번만 가능합니다. +image.count.exceeded=이미지 최대 등록 가능 개수를 초과하였습니다. # Expired expired=요청 대상이 만료되어 더 이상 사용할 수 없습니다. expired.meeting=미팅이 유효 기간이 만료되었습니다. diff --git a/src/main/resources/static/swagger-ui/openapi3.yaml b/src/main/resources/static/swagger-ui/openapi3.yaml index 8cc1f1e6..046b35fc 100644 --- a/src/main/resources/static/swagger-ui/openapi3.yaml +++ b/src/main/resources/static/swagger-ui/openapi3.yaml @@ -686,6 +686,36 @@ paths: 회원 Role 조회: value: "{\"status\":\"SUCCESS\",\"message\":\"Get Member Role Success\"\ ,\"data\":{\"isManager\":true,\"hasTeam\":true}}" + /v1/recommend: + post: + tags: + - 회원 관련 API + summary: 추천인을 등록합니다. + description: |2 + 위밋을 추천해준 추천인의 전화번호를 입력하여 추천인으로 등록합니다. + 계정당 1번만 가능하고 추천인 상대에게 20코인을 지급합니다. + operationId: 추천인 등록 + security: + - AccessToken: [ ] + requestBody: + content: + application/json;charset=UTF-8: + schema: + $ref: '#/components/schemas/v1-recommend-330443167' + examples: + 추천인 등록: + value: "{\"phoneNumber\":\"+821012345678\"}" + responses: + "200": + description: "200" + content: + application/json;charset=UTF-8: + schema: + $ref: '#/components/schemas/v1-member-1162606117' + examples: + 추천인 등록: + value: "{\"status\":\"SUCCESS\",\"message\":\"Recommend Success\"\ + ,\"data\":null}" /v1/suggestion/check: get: tags: @@ -753,6 +783,94 @@ paths: :\"카이\",\"mbti\":\"INFJ\",\"collegeName\":\"안양대\",\"collegeType\"\ :\"SOCIAL\",\"admissionYear\":\"17\",\"leaderLowProfileImageUrl\"\ :\"/v1/kai\",\"imageAuth\":false}}}" + /v1/team/image: + put: + tags: + - 팀 관련 API + summary: 팀 이미지를 업데이트합니다. + + description: |2 + 팀 이미지를 업데이트합니다. + 기존 이미지를 모두 삭제한 뒤, 새로운 이미지를 저장합니다. + + - multipart/form-data 데이터로 보내주어야함 (data는 json!) + - "images": File[], // 최소 1장, 최대 10장 + operationId: 팀 이미지 업데이트 + security: + - AccessToken: [ ] + responses: + "200": + description: "200" + content: + application/json;charset=UTF-8: + schema: + $ref: '#/components/schemas/v1-team-image-453865333' + examples: + 팀 이미지 업데이트: + value: "{\"status\":\"SUCCESS\",\"message\":\"Team Image Update\ + \ Success\",\"data\":[\"https://wemeet-bucket.s3.ap-northeast-2.amazonaws.com/teams/1/1.jpg\"\ + ,\"https://wemeet-bucket.s3.ap-northeast-2.amazonaws.com/teams/1/2.jpg\"\ + ,\"https://wemeet-bucket.s3.ap-northeast-2.amazonaws.com/teams/1/3.jpg\"\ + ]}" + post: + tags: + - 팀 관련 API + summary: 팀 이미지를 업로드합니다. + description: |2 + 팀 이미지를 업로드 합니다. + 새로 업로드하는 이미지는 기존 이미지 뒤에 위치합니다. + 한 팀당 최대 10개의 이미지를 업로드 할 수 있습니다. + + - multipart/form-data 데이터로 보내주어야함 (data는 json!) + - "images": File[], // 기존 이미지 + 업로드 이미지 개수가 10개를 넘지 않아야 함 + operationId: 팀 이미지 업로드 + security: + - AccessToken: [ ] + responses: + "200": + description: "200" + content: + application/json;charset=UTF-8: + schema: + $ref: '#/components/schemas/v1-team-image-453865333' + examples: + 팀 이미지 업로드: + value: "{\"status\":\"SUCCESS\",\"message\":\"Team Image Upload\ + \ Success\",\"data\":[\"https://wemeet-bucket.s3.ap-northeast-2.amazonaws.com/teams/1/1.jpg\"\ + ,\"https://wemeet-bucket.s3.ap-northeast-2.amazonaws.com/teams/1/2.jpg\"\ + ,\"https://wemeet-bucket.s3.ap-northeast-2.amazonaws.com/teams/1/3.jpg\"\ + ]}" + delete: + tags: + - 팀 관련 API + summary: 팀 이미지를 삭제합니다. + description: |2 + 팀 이미지를 삭제합니다. + 삭제할 url을 전달받아, 해당 이미지를 팀에서 삭제합니다. + operationId: 팀 이미지 삭제 + security: + - AccessToken: [ ] + requestBody: + content: + application/json;charset=UTF-8: + schema: + $ref: '#/components/schemas/v1-team-image486549215' + examples: + 팀 이미지 삭제: + value: "{\"deleteImageUrls\":[\"https://image.s3.ap-northeast-2.amazonaws.com/v1/teams/1.png\"\ + ,\"https://image.s3.ap-northeast-2.amazonaws.com/v1/teams/2.png\"\ + ]}" + responses: + "200": + description: "200" + content: + application/json;charset=UTF-8: + schema: + $ref: '#/components/schemas/v1-member-1162606117' + examples: + 팀 이미지 삭제: + value: "{\"status\":\"SUCCESS\",\"message\":\"Delete Team Image\ + \ Success\",\"data\":null}" /v1/auth/mail/request: post: tags: @@ -856,19 +974,19 @@ paths: $ref: '#/components/schemas/v1-auth-phone-validate1564587931' examples: 휴대폰 인증번호 검증: - value: "{\"phone\":\"+821088990011\",\"credential\":\"868922\"}" + value: "{\"phone\":\"+821088990011\",\"credential\":\"155201\"}" responses: "200": description: "200" content: application/json;charset=UTF-8: schema: - $ref: '#/components/schemas/v1-auth-phone-validate1303346532' + $ref: '#/components/schemas/v1-auth-phone-validate-214253953' examples: 휴대폰 인증번호 검증: value: "{\"status\":\"SUCCESS\",\"message\":\"인증에 성공하였습니다.\",\"\ - data\":{\"memberId\":null,\"isRegistered\":false,\"role\":[{\"\ - authority\":\"ROLE_GUEST\"}]}}" + data\":{\"memberId\":null,\"isRegistered\":false,\"withdrawal\"\ + :false,\"role\":[{\"authority\":\"ROLE_GUEST\"}]}}" /v1/meeting/accept/{meetingRequestId}: post: tags: @@ -1326,6 +1444,33 @@ components: status: type: string description: 응답 상태 + v1-auth-phone-validate-214253953: + type: object + properties: + data: + type: object + properties: + role: + type: array + items: + type: object + properties: + authority: + type: string + description: 회원 권한 + isRegistered: + type: boolean + description: 회원 가입 여부 + withdrawal: + type: boolean + description: 회원 탈퇴 여부 + description: 회원 가입이 되어있지 않은 사용자의 경우 null로 채워서 반환됨 + message: + type: string + description: 응답 메시지 + status: + type: string + description: 응답 상태 v1-auth-mail-validate521266742: type: object properties: @@ -1536,6 +1681,26 @@ components: allowMarketing: type: boolean description: 마케팅 수신 동의 여부 + v1-team-image486549215: + type: object + v1-team-image-453865333: + type: object + properties: + data: + type: array + description: 업로드된 이미지 URL 목록을 반환합니다. + items: + oneOf: + - type: object + - type: boolean + - type: string + - type: number + message: + type: string + description: 응답 메시지 + status: + type: string + description: 응답 상태 v1-team-teamId1238987508: type: object properties: @@ -1652,6 +1817,12 @@ components: status: type: string description: 응답 상태 + v1-recommend-330443167: + type: object + properties: + phoneNumber: + type: string + description: 위밋을 추천해준 추천인 전화번호 (+82101234xxxx) member-profile-image-upload: title: member-profile-image-upload type: object diff --git a/src/test/java/com/e2i/wemeet/config/security/filter/SmsLoginProcessingFilterTest.java b/src/test/java/com/e2i/wemeet/config/security/filter/SmsLoginProcessingFilterTest.java index 0d3513e6..6acd03e7 100644 --- a/src/test/java/com/e2i/wemeet/config/security/filter/SmsLoginProcessingFilterTest.java +++ b/src/test/java/com/e2i/wemeet/config/security/filter/SmsLoginProcessingFilterTest.java @@ -31,6 +31,7 @@ import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; @Profile("prod") @DisplayName("SMS 인증 테스트") @@ -77,6 +78,8 @@ Collection smsLoginProcessDynamic() { .content(toJson(loginRequestDto)) ); + perform.andDo(MockMvcResultHandlers.print()); + // then perform.andExpectAll( status().isOk(), @@ -189,6 +192,8 @@ private void writeRestDocs(ResultActions perform) throws Exception { .description("회원 아이디"), fieldWithPath("data.isRegistered") .description("회원 가입 여부"), + fieldWithPath("data.withdrawal") + .description("회원 탈퇴 여부"), fieldWithPath("data.role[].authority").type(JsonFieldType.STRING) .description("회원 권한") ) diff --git a/src/test/java/com/e2i/wemeet/controller/member/RecommendControllerTest.java b/src/test/java/com/e2i/wemeet/controller/member/RecommendControllerTest.java new file mode 100644 index 00000000..4172506f --- /dev/null +++ b/src/test/java/com/e2i/wemeet/controller/member/RecommendControllerTest.java @@ -0,0 +1,103 @@ +package com.e2i.wemeet.controller.member; + +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.e2i.wemeet.dto.request.member.RecommenderRequestDto; +import com.e2i.wemeet.support.config.AbstractControllerUnitTest; +import com.e2i.wemeet.support.config.WithCustomMockUser; +import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper; +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.ResultActions; + +class RecommendControllerTest extends AbstractControllerUnitTest { + + @DisplayName("입력한 번호가 형식에 어긋날 경우 요청에 실패한다.") + @WithCustomMockUser + @Test + void recommendWithInvalidPhone() throws Exception { + // given + final RecommenderRequestDto requestDto = new RecommenderRequestDto("01012345678"); + willDoNothing() + .given(recommendService).recommend(1L, "01012345678"); + + // when + ResultActions perform = mockMvc.perform(post("/v1/recommend") + .contentType(APPLICATION_JSON) + .content(toJson(requestDto))); + + // then + perform + .andExpectAll( + status().isOk(), + jsonPath("$.status").value("FAIL"), + jsonPath("$.message").value("형식에 맞지 않는 phoneNumber입니다."), + jsonPath("$.data").doesNotExist() + ); + verify(recommendService, times(0)) + .recommend(1L, "+821012345678"); + } + + @WithCustomMockUser + @DisplayName("추천인의 휴대폰 번호를 입력하여 추천인을 등록할 수 있다.") + @Test + void recommendWithRecommenderPhone() throws Exception { + // given + final RecommenderRequestDto requestDto = new RecommenderRequestDto("+821012345678"); + willDoNothing() + .given(recommendService).recommend(1L, "+821012345678"); + + // when + ResultActions perform = mockMvc.perform(post("/v1/recommend") + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(requestDto))); + + // then + perform + .andExpectAll( + status().isOk(), + jsonPath("$.status").value("SUCCESS"), + jsonPath("$.message").value("Recommend Success"), + jsonPath("$.data").doesNotExist() + ); + verify(recommendService).recommend(1L, "+821012345678"); + + writeRestDocsRecommend(perform); + } + + public void writeRestDocsRecommend(ResultActions perform) throws Exception { + perform + .andDo( + MockMvcRestDocumentationWrapper.document("추천인 등록", + ResourceSnippetParameters.builder() + .tag("회원 관련 API") + .summary("추천인을 등록합니다.") + .description( + """ + 위밋을 추천해준 추천인의 전화번호를 입력하여 추천인으로 등록합니다. + 계정당 1번만 가능하고 추천인 상대에게 20코인을 지급합니다. + """), + requestFields( + fieldWithPath("phoneNumber").type(JsonFieldType.STRING) + .description("위밋을 추천해준 추천인 전화번호 (+82101234xxxx)") + ), + responseFields( + fieldWithPath("status").type(JsonFieldType.STRING).description("응답 상태"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), + fieldWithPath("data").type(JsonFieldType.NULL).description("응답 데이터는 없습니다") + ) + )); + } +} \ No newline at end of file diff --git a/src/test/java/com/e2i/wemeet/controller/team/TeamImageControllerTest.java b/src/test/java/com/e2i/wemeet/controller/team/TeamImageControllerTest.java new file mode 100644 index 00000000..f8f95762 --- /dev/null +++ b/src/test/java/com/e2i/wemeet/controller/team/TeamImageControllerTest.java @@ -0,0 +1,197 @@ +package com.e2i.wemeet.controller.team; + +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.e2i.wemeet.dto.request.team.DeleteTeamImageRequestDto; +import com.e2i.wemeet.support.config.AbstractControllerUnitTest; +import com.e2i.wemeet.support.config.WithCustomMockUser; +import com.e2i.wemeet.support.fixture.TeamImagesFixture; +import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper; +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; + +class TeamImageControllerTest extends AbstractControllerUnitTest { + + @DisplayName("팀 이미지를 업로드할 수 있다.") + @WithCustomMockUser + @Test + void upload() throws Exception { + // given + MockMultipartFile images = new MockMultipartFile("images", "test.png", "image/png", + "test".getBytes()); + given(teamImageService.uploadTeamImage(anyLong(), anyList())) + .willReturn(TeamImagesFixture.BASIC_TEAM_IMAGE.getTeamImages()); + + // when + ResultActions perform = mockMvc.perform( + multipart("/v1/team/image") + .file(images) + .with(csrf()) + .contentType(MULTIPART_FORM_DATA)); + + // then + perform + .andExpectAll( + status().isOk(), + jsonPath("$.status").value("SUCCESS"), + jsonPath("$.message").value("Team Image Upload Success"), + jsonPath("$.data").isArray() + ); + + writeRestDocsUploadImages(perform); + } + + @DisplayName("팀 이미지를 수정할 수 있다.") + @WithCustomMockUser + @Test + void update() throws Exception { + // given + MockMultipartFile images = new MockMultipartFile("images", "test.png", "image/png", + "test".getBytes()); + given(teamImageService.updateTeamImage(anyLong(), anyList())) + .willReturn(TeamImagesFixture.BASIC_TEAM_IMAGE.getTeamImages()); + + // when + MockMultipartHttpServletRequestBuilder builder = + RestDocumentationRequestBuilders.multipart("/v1/team/image"); + builder.with(request -> { + request.setMethod("PUT"); + return request; + }); + + ResultActions perform = mockMvc.perform( + builder + .file(images) + .with(csrf()) + .contentType(MULTIPART_FORM_DATA)); + + // then + perform + .andExpectAll( + status().isOk(), + jsonPath("$.status").value("SUCCESS"), + jsonPath("$.message").value("Team Image Update Success"), + jsonPath("$.data").isArray() + ); + + writeRestDocsUpdateImages(perform); + } + + @DisplayName("팀 이미지를 삭제할 수 있다.") + @WithCustomMockUser + @Test + void delete() throws Exception { + // given + DeleteTeamImageRequestDto requestDto = new DeleteTeamImageRequestDto(List.of( + "https://image.s3.ap-northeast-2.amazonaws.com/v1/teams/1.png", + "https://image.s3.ap-northeast-2.amazonaws.com/v1/teams/2.png" + )); + willDoNothing(). + given(teamImageService).deleteTeamImage(anyList()); + + // when + ResultActions perform = mockMvc.perform(RestDocumentationRequestBuilders.delete("/v1/team/image") + .with(csrf()) + .contentType(APPLICATION_JSON) + .content(toJson(requestDto))); + + // then + perform + .andExpectAll( + status().isOk(), + jsonPath("$.status").value("SUCCESS"), + jsonPath("$.message").value("Delete Team Image Success"), + jsonPath("$.data").doesNotExist() + ); + + writeRestDocsDeleteImages(perform); + } + + private void writeRestDocsUploadImages(ResultActions perform) throws Exception { + perform + .andDo( + MockMvcRestDocumentationWrapper.document("팀 이미지 업로드", + ResourceSnippetParameters.builder() + .tag("팀 관련 API") + .summary("팀 이미지를 업로드합니다.") + .description(""" + 팀 이미지를 업로드 합니다. + 새로 업로드하는 이미지는 기존 이미지 뒤에 위치합니다. + 한 팀당 최대 10개의 이미지를 업로드 할 수 있습니다. + + multipart/form-data 데이터로 보내주어야함 (data는 json!) + "images": File[], // 기존 이미지 + 업로드 이미지 개수가 10개를 넘지 않아야 함 + """ + ), + responseFields( + fieldWithPath("status").type(JsonFieldType.STRING).description("응답 상태"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), + fieldWithPath("data").type(JsonFieldType.ARRAY).description("업로드된 이미지 URL 목록을 반환합니다.") + ) + ) + ); + } + + private void writeRestDocsUpdateImages(ResultActions perform) throws Exception { + perform + .andDo( + MockMvcRestDocumentationWrapper.document("팀 이미지 업데이트", + ResourceSnippetParameters.builder() + .tag("팀 관련 API") + .summary("팀 이미지를 업데이트합니다.") + .description(""" + 팀 이미지를 업데이트합니다. + 기존 이미지를 모두 삭제한 뒤, 새로운 이미지를 저장합니다. + + multipart/form-data 데이터로 보내주어야함 (data는 json!) + "images": File[], // 최소 1장, 최대 10장 + """ + ), + responseFields( + fieldWithPath("status").type(JsonFieldType.STRING).description("응답 상태"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), + fieldWithPath("data").type(JsonFieldType.ARRAY).description("업로드된 이미지 URL 목록을 반환합니다.") + ) + ) + ); + } + + private void writeRestDocsDeleteImages(ResultActions perform) throws Exception { + perform + .andDo( + MockMvcRestDocumentationWrapper.document("팀 이미지 삭제", + ResourceSnippetParameters.builder() + .tag("팀 관련 API") + .summary("팀 이미지를 삭제합니다.") + .description(""" + 팀 이미지를 삭제합니다. + 삭제할 url을 전달받아, 해당 이미지를 팀에서 삭제합니다. + """ + ), + responseFields( + fieldWithPath("status").type(JsonFieldType.STRING).description("응답 상태"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), + fieldWithPath("data").type(JsonFieldType.NULL).description("data는 아무것도 반환되지 않습니다.") + ) + ) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/e2i/wemeet/domain/team_image/TeamImageRepositoryTest.java b/src/test/java/com/e2i/wemeet/domain/team_image/TeamImageRepositoryTest.java new file mode 100644 index 00000000..09a3f428 --- /dev/null +++ b/src/test/java/com/e2i/wemeet/domain/team_image/TeamImageRepositoryTest.java @@ -0,0 +1,114 @@ +package com.e2i.wemeet.domain.team_image; + +import static com.e2i.wemeet.support.fixture.MemberFixture.KAI; +import static com.e2i.wemeet.support.fixture.TeamFixture.HONGDAE_TEAM_1; +import static com.e2i.wemeet.support.fixture.TeamImagesFixture.BASIC_TEAM_IMAGE; +import static com.e2i.wemeet.support.fixture.TeamMemberFixture.create_3_man; +import static org.assertj.core.api.Assertions.assertThat; + +import com.e2i.wemeet.domain.member.Member; +import com.e2i.wemeet.domain.member.MemberRepository; +import com.e2i.wemeet.domain.team.Team; +import com.e2i.wemeet.domain.team.TeamRepository; +import com.e2i.wemeet.support.module.AbstractRepositoryUnitTest; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class TeamImageRepositoryTest extends AbstractRepositoryUnitTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + private TeamImageRepository teamImageRepository; + + @DisplayName("팀 ID를 통해 팀 이미지를 모두 삭제할 수 있다.") + @Test + void deleteAllByTeamTeamId() { + // given + Member kai = memberRepository.save(KAI.create(ANYANG_CODE)); + Team team = teamRepository.save(HONGDAE_TEAM_1.create(kai, create_3_man())); + teamImageRepository.saveAll(BASIC_TEAM_IMAGE.createTeamImages(team)); + + // when + teamImageRepository.deleteAllByTeamTeamId(team.getTeamId()); + + // then + List teamImagesByTeamId = teamImageRepository.findTeamImagesByTeamId(team.getTeamId()); + assertThat(teamImagesByTeamId).isEmpty(); + } + + @DisplayName("유저 ID로 팀을 조회할 수 있다.") + @Test + void findTeamByMemberId() { + // given + Member kai = memberRepository.save(KAI.create(ANYANG_CODE)); + Team team = teamRepository.save(HONGDAE_TEAM_1.create(kai, create_3_man())); + entityManager.flush(); + entityManager.clear(); + + // when + Team findTeam = teamImageRepository.findTeamByMemberId(kai.getMemberId()) + .orElseThrow(); + + // then + assertThat(findTeam.getTeamId()).isEqualTo(team.getTeamId()); + } + + @DisplayName("팀 ID로 팀 이미지를 조회할 수 있다.") + @Test + void findTeamImagesByTeamId() { + // given + Member kai = memberRepository.save(KAI.create(ANYANG_CODE)); + Team team = teamRepository.save(HONGDAE_TEAM_1.create(kai, create_3_man())); + int imageSize = teamImageRepository.saveAll(BASIC_TEAM_IMAGE.createTeamImages(team)).size(); + + // when + List findImages = teamImageRepository.findTeamImagesByTeamId(team.getTeamId()); + + // then + assertThat(findImages).hasSize(imageSize) + .extracting("team") + .contains(team); + } + + @DisplayName("팀 ID로 팀 이미지의 개수를 조회할 수 있다.") + @Test + void countByTeamTeamId() { + // given + Member kai = memberRepository.save(KAI.create(ANYANG_CODE)); + Team team = teamRepository.save(HONGDAE_TEAM_1.create(kai, create_3_man())); + int imageSize = teamImageRepository.saveAll(BASIC_TEAM_IMAGE.createTeamImages(team)).size(); + + // when + int findSize = teamImageRepository.countByTeamTeamId(team.getTeamId()); + + // then + assertThat(imageSize).isEqualTo(findSize); + } + + @DisplayName("팀 이미지 URL로 팀 이미지를 모두 삭제할 수 있다.") + @Test + void deleteAllByTeamImageUrl() { + // given + Member kai = memberRepository.save(KAI.create(ANYANG_CODE)); + Team team = teamRepository.save(HONGDAE_TEAM_1.create(kai, create_3_man())); + List savedUrls = teamImageRepository.saveAll(BASIC_TEAM_IMAGE.createTeamImages(team)) + .stream() + .map(TeamImage::getTeamImageUrl) + .toList(); + + // when + teamImageRepository.deleteAllByTeamImageUrl(savedUrls); + + // then + List findImages = teamImageRepository.findTeamImagesByTeamId(team.getTeamId()); + assertThat(findImages).isEmpty(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/e2i/wemeet/service/aws/s3/S3ServiceImplTest.java b/src/test/java/com/e2i/wemeet/service/aws/s3/S3ServiceImplTest.java new file mode 100644 index 00000000..13fece67 --- /dev/null +++ b/src/test/java/com/e2i/wemeet/service/aws/s3/S3ServiceImplTest.java @@ -0,0 +1,133 @@ +package com.e2i.wemeet.service.aws.s3; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.e2i.wemeet.support.module.AbstractServiceTest; +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.mock.web.MockMultipartFile; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; +import software.amazon.awssdk.services.s3.model.S3Object; + +@Profile("test") +class S3ServiceImplTest extends AbstractServiceTest { + + @Autowired + private S3Service s3Service; + + @Autowired + private S3Client s3Client; + + @Value("${aws.s3.testBucket}") + public String testBucket; + + @DisplayName("S3에 오브젝트를 업로드하는데 성공한다.") + //@Test + void upload() { + // given + final String objectKey = "v1/test/" + UUID.randomUUID(); + MockMultipartFile uploadFile = new MockMultipartFile("file", "test.jpg", "image/jpg", "test".getBytes()); + + // when + s3Service.upload(uploadFile, objectKey, testBucket); + } + + @DisplayName("S3에 오브젝트를 업로드하고 삭제하는데 성공한다.") + @TestFactory + Collection testAllFeature() { + // given + final String objectPath = "v1/test/"; + final String objectKey1 = objectPath + "1/" + UUID.randomUUID(); + final String objectKey2 = objectPath + "2/" + UUID.randomUUID(); + MockMultipartFile uploadFile1 = new MockMultipartFile("file1", "test1.jpg", "image/jpg", "test1".getBytes()); + MockMultipartFile uploadFile2 = new MockMultipartFile("file2", "test2.jpg", "image/jpg", "test2".getBytes()); + + // when & then + return List.of( + // upload Test + DynamicTest.dynamicTest("S3에 오브젝트를 업로드한다.", () -> { + // when + s3Service.upload(uploadFile1, objectKey1, testBucket); + s3Service.upload(uploadFile2, objectKey2, testBucket); + + // then + ListObjectsV2Request listObjectsV2Request = ListObjectsV2Request.builder() + .bucket(testBucket) + .prefix(objectPath) + .build(); + ListObjectsV2Response listObjectsV2Response = s3Client.listObjectsV2(listObjectsV2Request); + List findKeys = listObjectsV2Response.contents().stream().map(S3Object::key).toList(); + + assertThat(findKeys).hasSize(2) + .containsOnly(objectKey1, objectKey2); + }), + // delete Test + DynamicTest.dynamicTest("S3에 업로드한 오브젝트를 삭제한다.", () -> { + // when + s3Service.delete(testBucket, objectPath); + + // then + ListObjectsV2Request listObjectsV2Request = ListObjectsV2Request.builder() + .bucket(testBucket) + .prefix(objectPath) + .build(); + + List contents = s3Client.listObjectsV2(listObjectsV2Request).contents(); + assertThat(contents).isEmpty(); + }) + ); + } + + @DisplayName("S3 오브젝트 삭제에 성공한다.") + //@Test + void deleteWithClient() { + // given + ListObjectsV2Request listObjectsV2Request = ListObjectsV2Request.builder() + .bucket("wemeet-static-profile-image") + .prefix("character_6") + .build(); + List targetKey = s3Client.listObjectsV2(listObjectsV2Request).contents() + .stream() + .map(S3Object::key) + .toList(); + + // when + targetKey.stream() + .map(key -> DeleteObjectRequest.builder() + .bucket("wemeet-static-profile-image") + .key(key) + .build() + ) + .forEach(deleteObjectRequest -> s3Client.deleteObject(deleteObjectRequest)); + } + + @DisplayName("특정 버킷의 오브젝트 목록을 조회한다.") + @Test + void list() { + // given + ListObjectsV2Request listObjectsV2Request = ListObjectsV2Request.builder() + .bucket("wemeet-static-team-image") + .prefix("v1/") + .build(); + + // when + ListObjectsV2Response listObjectsV2Response = s3Client.listObjectsV2(listObjectsV2Request); + + // then + listObjectsV2Response.contents() + .stream() + .map(S3Object::key) + .forEach(System.out::println); + } +} \ No newline at end of file diff --git a/src/test/java/com/e2i/wemeet/service/member/RecommendServiceTest.java b/src/test/java/com/e2i/wemeet/service/member/RecommendServiceTest.java new file mode 100644 index 00000000..043679a5 --- /dev/null +++ b/src/test/java/com/e2i/wemeet/service/member/RecommendServiceTest.java @@ -0,0 +1,88 @@ +package com.e2i.wemeet.service.member; + +import static com.e2i.wemeet.support.fixture.MemberFixture.JEONGYEOL; +import static com.e2i.wemeet.support.fixture.MemberFixture.KAI; +import static com.e2i.wemeet.support.fixture.MemberFixture.SEYUN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.e2i.wemeet.domain.member.Member; +import com.e2i.wemeet.domain.member.MemberRepository; +import com.e2i.wemeet.exception.badrequest.MemberHasBeenDeletedException; +import com.e2i.wemeet.exception.badrequest.RecommenderAlreadyExist; +import com.e2i.wemeet.support.module.AbstractServiceTest; +import java.time.LocalDateTime; +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; + +@Transactional +class RecommendServiceTest extends AbstractServiceTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private RecommendService recommendService; + + @DisplayName("추천인 전화번호를 입력하면 추천인에게 20코인을 지급한다.") + @Test + void recommend() { + // given + Member kai = memberRepository.save(KAI.create(ANYANG_CODE)); + Member seyun = memberRepository.save(SEYUN.create(KOREA_CODE)); + final int before = kai.getCredit(); + + setAuthentication(seyun.getMemberId(), "USER"); + + // when + recommendService.recommend(seyun.getMemberId(), kai.getPhoneNumber()); + + // then + final int after = kai.getCredit(); + final String recommenderPhone = seyun.getRecommenderPhone(); + final String kaiPhone = kai.getPhoneNumber(); + + assertThat(after - before).isEqualTo(20); + assertThat(recommenderPhone).isEqualTo(kaiPhone); + } + + @DisplayName("이미 추천인을 입력했을경우, 더 이상 추천인 입력 기능을 수행할 수 없다.") + @Test + void recommendMoreThanTwice() { + // given + Member kai = memberRepository.save(KAI.create(ANYANG_CODE)); + Member jeongyeol = memberRepository.save(JEONGYEOL.create(KOREA_CODE)); + Member seyun = memberRepository.save(SEYUN.create(KOREA_CODE)); + setAuthentication(seyun.getMemberId(), "USER"); + + // when + recommendService.recommend(seyun.getMemberId(), kai.getPhoneNumber()); + + // then + assertThatThrownBy(() -> recommendService.recommend(seyun.getMemberId(), jeongyeol.getPhoneNumber())) + .isInstanceOf(RecommenderAlreadyExist.class) + .hasMessage("recommender.already.exist"); + } + + @DisplayName("추천인이 삭제된 유저일 경우, 추천인 입력 기능을 수행할 수 없다.") + @Test + void recommendWithDeletedMember() { + // given + Member kai = memberRepository.save(KAI.create(ANYANG_CODE)); + Member seyun = memberRepository.save(SEYUN.create(KOREA_CODE)); + LocalDateTime deleteTime = LocalDateTime.of(2023, 9, 10, 13, 0, 0); + kai.delete(deleteTime); + entityManager.flush(); + entityManager.clear(); + + setAuthentication(seyun.getMemberId(), "USER"); + + // when + assertThatThrownBy(() -> recommendService.recommend(seyun.getMemberId(), kai.getPhoneNumber())) + .isInstanceOf(MemberHasBeenDeletedException.class) + .hasMessage("member.has.been.deleted"); + } + +} \ No newline at end of file diff --git a/src/test/java/com/e2i/wemeet/service/team/S3TeamImageServiceTest.java b/src/test/java/com/e2i/wemeet/service/team/S3TeamImageServiceTest.java new file mode 100644 index 00000000..09cba966 --- /dev/null +++ b/src/test/java/com/e2i/wemeet/service/team/S3TeamImageServiceTest.java @@ -0,0 +1,132 @@ +package com.e2i.wemeet.service.team; + +import static com.e2i.wemeet.support.fixture.MemberFixture.KAI; +import static com.e2i.wemeet.support.fixture.TeamFixture.HONGDAE_TEAM_1; +import static com.e2i.wemeet.support.fixture.TeamMemberFixture.create_3_man; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.e2i.wemeet.domain.member.Member; +import com.e2i.wemeet.domain.member.MemberRepository; +import com.e2i.wemeet.domain.team.Team; +import com.e2i.wemeet.domain.team.TeamRepository; +import com.e2i.wemeet.domain.team_image.TeamImage; +import com.e2i.wemeet.domain.team_image.TeamImageRepository; +import com.e2i.wemeet.support.module.AbstractServiceTest; +import java.util.Collection; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.transaction.annotation.Transactional; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; +import software.amazon.awssdk.services.s3.model.S3Object; + +@Profile("test") +@Transactional +class S3TeamImageServiceTest extends AbstractServiceTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + private TeamImageRepository teamImageRepository; + + @Autowired + private TeamImageService teamImageService; + + @Autowired + private S3Client s3Client; + + @Value("${aws.s3.teamImageBucket}") + public String teamBucket; + + @DisplayName("S3에 오브젝트를 업로드하고 삭제하는데 성공한다.") + //@TestFactory + Collection testAllFeature() { + // given + Member kai = memberRepository.save(KAI.create(ANYANG_CODE)); + Team kaiTeam = teamRepository.save(HONGDAE_TEAM_1.create(kai, create_3_man())); + + final String objectPath = S3TeamImageService.TEAM_IMAGE_PATH.formatted(kaiTeam.getTeamId()); + setAuthentication(kai.getMemberId(), "MANAGER"); + + // when & then + return List.of( + // upload Test + DynamicTest.dynamicTest("팀 사진을 등록할 수 있다.", () -> { + // given + MockMultipartFile uploadFile = new MockMultipartFile("file1", "test1.jpg", "image/jpg", "test1".getBytes()); + + // when + teamImageService.uploadTeamImage(kai.getMemberId(), List.of(uploadFile)); + + // then + List findKeys = findKeys(objectPath); + List findTeamImages = teamImageRepository.findTeamImagesByTeamId(kaiTeam.getTeamId()); + + assertAll( + () -> assertThat(findKeys).hasSize(1), + () -> assertThat(findKeys.get(0)).contains(objectPath), + () -> assertThat(findTeamImages).hasSize(1) + .extracting("sequence", "team") + .containsOnly(tuple(1, kaiTeam)) + ); + }), + // upload Test + DynamicTest.dynamicTest("팀 사진을 수정할 수 있다.", () -> { + // given + List keys = findKeys(objectPath); + final String key = keys.get(0); + final MockMultipartFile updateFile = new MockMultipartFile("update2", "update2.jpg", "image/jpg", "update".getBytes()); + + // when + teamImageService.updateTeamImage(kai.getMemberId(), List.of(updateFile)); + + // then + List updateKeys = findKeys(objectPath); + List findTeamImages = teamImageRepository.findTeamImagesByTeamId(kaiTeam.getTeamId()); + + assertAll( + () -> assertThat(updateKeys).hasSize(1), + () -> assertThat(updateKeys.get(0)).isNotEqualTo(key), + () -> assertThat(findTeamImages).hasSize(1) + .extracting("sequence", "team") + .containsOnly(tuple(1, kaiTeam)) + ); + }), + // delete Test + DynamicTest.dynamicTest("S3에 업로드한 오브젝트를 삭제한다.", () -> { + // when + teamImageService.deleteTeamImage(List.of(objectPath)); + + // then + List keys = findKeys(objectPath); + List findImages = teamImageRepository.findTeamImagesByTeamId(kaiTeam.getTeamId()); + assertThat(keys).isEmpty(); + }) + ); + } + + private List findKeys(String objectPath) { + ListObjectsV2Request listObjectsV2Request = ListObjectsV2Request.builder() + .bucket(teamBucket) + .prefix(objectPath) + .build(); + ListObjectsV2Response listObjectsV2Response = s3Client.listObjectsV2(listObjectsV2Request); + return listObjectsV2Response.contents() + .stream() + .map(S3Object::key) + .toList(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/e2i/wemeet/support/config/AbstractControllerUnitTest.java b/src/test/java/com/e2i/wemeet/support/config/AbstractControllerUnitTest.java index 363da482..7c58a276 100644 --- a/src/test/java/com/e2i/wemeet/support/config/AbstractControllerUnitTest.java +++ b/src/test/java/com/e2i/wemeet/support/config/AbstractControllerUnitTest.java @@ -7,16 +7,20 @@ import com.e2i.wemeet.controller.heart.HeartController; import com.e2i.wemeet.controller.meeting.MeetingController; import com.e2i.wemeet.controller.member.MemberController; +import com.e2i.wemeet.controller.member.RecommendController; import com.e2i.wemeet.controller.suggestion.SuggestionController; import com.e2i.wemeet.controller.team.TeamController; +import com.e2i.wemeet.controller.team.TeamImageController; import com.e2i.wemeet.service.code.CodeService; import com.e2i.wemeet.service.credit.CreditService; import com.e2i.wemeet.service.heart.HeartService; import com.e2i.wemeet.service.meeting.MeetingHandleService; import com.e2i.wemeet.service.meeting.MeetingListService; import com.e2i.wemeet.service.member.MemberService; +import com.e2i.wemeet.service.member.RecommendService; import com.e2i.wemeet.service.member_image.MemberImageService; import com.e2i.wemeet.service.suggestion.SuggestionService; +import com.e2i.wemeet.service.team.TeamImageService; import com.e2i.wemeet.service.team.TeamService; import com.e2i.wemeet.service.token.TokenService; import com.fasterxml.jackson.core.JsonProcessingException; @@ -46,7 +50,9 @@ MeetingController.class, SuggestionController.class, HeartController.class, - CreditController.class + CreditController.class, + RecommendController.class, + TeamImageController.class }) public abstract class AbstractControllerUnitTest { @@ -82,6 +88,12 @@ public abstract class AbstractControllerUnitTest { @MockBean protected CreditService creditService; + @MockBean + protected RecommendService recommendService; + + @MockBean + protected TeamImageService teamImageService; + protected MockMvc mockMvc; diff --git a/src/test/java/com/e2i/wemeet/support/module/AbstractIntegrationTest.java b/src/test/java/com/e2i/wemeet/support/module/AbstractIntegrationTest.java index 8848d2d4..a2dd8305 100644 --- a/src/test/java/com/e2i/wemeet/support/module/AbstractIntegrationTest.java +++ b/src/test/java/com/e2i/wemeet/support/module/AbstractIntegrationTest.java @@ -21,6 +21,7 @@ import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.WebApplicationContext; @@ -60,6 +61,7 @@ public abstract class AbstractIntegrationTest { void setup(WebApplicationContext context, RestDocumentationContextProvider restDocumentation) { DispatcherServlet dispatcherServlet = MockMvcBuilders.webAppContextSetup(context) .apply(documentationConfiguration(restDocumentation)) + .alwaysDo(MockMvcResultHandlers.print()) .build().getDispatcherServlet(); httpRequestEndPointChecker = new DispatcherServletEndPointChecker(dispatcherServlet); diff --git a/src/test/java/com/e2i/wemeet/support/module/AbstractServiceTest.java b/src/test/java/com/e2i/wemeet/support/module/AbstractServiceTest.java index b19ec196..fab3e46e 100644 --- a/src/test/java/com/e2i/wemeet/support/module/AbstractServiceTest.java +++ b/src/test/java/com/e2i/wemeet/support/module/AbstractServiceTest.java @@ -15,7 +15,9 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.TestPropertySource; +@TestPropertySource(locations = "classpath:/sub/application-aws.yml") @AutoConfigureTestDatabase(replace = Replace.NONE) @SpringBootTest public abstract class AbstractServiceTest {