diff --git a/build.gradle b/build.gradle index 24e2c8cf..2e3c9d44 100644 --- a/build.gradle +++ b/build.gradle @@ -75,7 +75,11 @@ dependencies { // WebSocket implementation 'org.springframework.boot:spring-boot-starter-websocket' + // Spring Validation implementation 'org.hibernate.validator:hibernate-validator' + + // AWS S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' } tasks.named('test') { diff --git a/src/main/java/com/example/mate/common/config/S3Config.java b/src/main/java/com/example/mate/common/config/S3Config.java new file mode 100644 index 00000000..26435842 --- /dev/null +++ b/src/main/java/com/example/mate/common/config/S3Config.java @@ -0,0 +1,33 @@ +package com.example.mate.common.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.credentials.access_key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret_key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + + return (AmazonS3Client) AmazonS3ClientBuilder + .standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/mate/common/utils/file/FileUploader.java b/src/main/java/com/example/mate/common/utils/file/FileUploader.java deleted file mode 100644 index 9652508b..00000000 --- a/src/main/java/com/example/mate/common/utils/file/FileUploader.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.example.mate.common.utils.file; - -import java.util.UUID; -import org.springframework.web.multipart.MultipartFile; - -public class FileUploader { - - // 파일명에서 허용하지 않는 문자들에 대한 정규식 - private static final String FILE_NAME_REGEX = "[^a-zA-Z0-9.\\-_]"; - private static final String FILE_NAME_REPLACEMENT = "_"; - - public static String uploadFile(MultipartFile file) { - String fileName = getFileName(file); - - // TODO : 2024/11/27 - S3 파일 업로드 기능, 경로를 포함한 url 반환 - - return "upload/" + fileName; - } - - public static boolean deleteFile(String imageUrl) { - // TODO : 2024/11/28 - S3 파일 삭제 기능, 삭제 여부를 boolean 값으로 반환 - - return true; - } - - /** - * 파일명에서 허용되지 않는 문자를 제거하고, UUID 를 추가한 새로운 파일명을 생성 - * - * @param file 업로드할 파일 - * @return UUID 를 포함한 새로운 파일명 - */ - private static String getFileName(MultipartFile file) { - String uuid = UUID.randomUUID().toString(); - return uuid + FILE_NAME_REPLACEMENT + cleanFileName(file.getOriginalFilename()); - } - - /** - * 파일 이름에서 허용되지 않는 문자를 대체 문자로 변경합니다. - * - * @param fileName 원본 파일명 - * @return 대체된 파일명 - */ - private static String cleanFileName(String fileName) { - return fileName.replaceAll(FILE_NAME_REGEX, FILE_NAME_REPLACEMENT); - } -} diff --git a/src/main/java/com/example/mate/domain/file/FileService.java b/src/main/java/com/example/mate/domain/file/FileService.java new file mode 100644 index 00000000..ff44dcdf --- /dev/null +++ b/src/main/java/com/example/mate/domain/file/FileService.java @@ -0,0 +1,89 @@ +package com.example.mate.domain.file; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.example.mate.common.error.CustomException; +import com.example.mate.common.error.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class FileService { + + // 파일명에서 허용하지 않는 문자들에 대한 정규식 + private static final String FILE_NAME_REGEX = "[^a-zA-Z0-9.\\-_]"; + private static final String FILE_NAME_REPLACEMENT = "_"; + + private final AmazonS3 amazonS3; + + @Getter + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + // 파일 업로드 + public String uploadFile(MultipartFile file) { + String fileName = getFileName(file); + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + metadata.setContentType(file.getContentType()); + + try { + amazonS3.putObject(bucket, fileName, file.getInputStream(), metadata); // 파일 저장 + } catch (Exception e) { + throw new CustomException(ErrorCode.FILE_UPLOAD_ERROR); + } + return amazonS3.getUrl(bucket, fileName).toString(); // 파일 저장된 URL 반환 + } + + // 파일 삭제 + public void deleteFile(String imageUrl) { + String key = extractKeyFromUrl(imageUrl); + try { + amazonS3.deleteObject(bucket, key); // 파일 삭제 + } catch (Exception e) { + throw new CustomException(ErrorCode.FILE_DELETE_ERROR); + } + } + + /** + * S3 URL에서 객체 키를 추출 + * + * @param imageUrl S3 URL + * @return 추출된 객체 키 + */ + private String extractKeyFromUrl(String imageUrl) { + return imageUrl.replace("https://" + bucket + ".s3.ap-northeast-2.amazonaws.com/", ""); + } + + /** + * 파일명에서 허용되지 않는 문자를 제거하고, UUID 를 추가한 새로운 파일명을 생성 + * + * @param file 업로드할 파일 + * @return UUID 를 포함한 새로운 파일명 + */ + private static String getFileName(MultipartFile file) { + String uuid = UUID.randomUUID().toString(); + return uuid + FILE_NAME_REPLACEMENT + cleanFileName(file.getOriginalFilename()); + } + + /** + * 파일 이름에서 허용되지 않는 문자를 대체 문자로 변경합니다. + * + * @param fileName 원본 파일명 + * @return 대체된 파일명 + */ + private static String cleanFileName(String fileName) { + return fileName.replaceAll(FILE_NAME_REGEX, FILE_NAME_REPLACEMENT); + } +} diff --git a/src/main/java/com/example/mate/common/utils/file/FileValidator.java b/src/main/java/com/example/mate/domain/file/FileValidator.java similarity index 75% rename from src/main/java/com/example/mate/common/utils/file/FileValidator.java rename to src/main/java/com/example/mate/domain/file/FileValidator.java index 1568fd63..695d2929 100644 --- a/src/main/java/com/example/mate/common/utils/file/FileValidator.java +++ b/src/main/java/com/example/mate/domain/file/FileValidator.java @@ -1,31 +1,27 @@ -package com.example.mate.common.utils.file; +package com.example.mate.domain.file; import com.example.mate.common.error.CustomException; import com.example.mate.common.error.ErrorCode; -import java.util.List; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + public class FileValidator { private static final int MAX_GOODS_POST_IMAGE_COUNT = 10; - // 굿즈 판매글 이미지 파일 유효성 검사 + // 여러 파일 유효성 검사 : 굿즈 판매글 public static void validateGoodsPostImages(List files) { - files.forEach(FileValidator::validateNotEmpty); - files.forEach(FileValidator::isNotImage); - if (files.size() > MAX_GOODS_POST_IMAGE_COUNT) { throw new CustomException(ErrorCode.FILE_UPLOAD_LIMIT_EXCEEDED); } - } - // 회원 프로필 이미지 파일 유효성 검사 - public static void validateMyProfileImage(MultipartFile file) { - isNotImage(file); + files.forEach(FileValidator::validateNotEmpty); + files.forEach(FileValidator::isNotImage); } - // 메이트 게시글 이미지 파일 유효성 검사 - public static void validateMatePostImage(MultipartFile file) { + // 단일 파일 유효성 검사 : 회원 프로필, 메이트 구인글 + public static void validateSingleImage(MultipartFile file) { isNotImage(file); } diff --git a/src/main/java/com/example/mate/domain/goods/controller/GoodsController.java b/src/main/java/com/example/mate/domain/goods/controller/GoodsController.java index f809603c..635d6cff 100644 --- a/src/main/java/com/example/mate/domain/goods/controller/GoodsController.java +++ b/src/main/java/com/example/mate/domain/goods/controller/GoodsController.java @@ -12,25 +12,17 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -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.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + @RestController @RequestMapping("/api/goods") @RequiredArgsConstructor @@ -45,7 +37,7 @@ public ResponseEntity> registerGoodsPost( @AuthenticationPrincipal AuthMember member, @Parameter(description = "판매글 등록 데이터", required = true) @Validated @RequestPart("data") GoodsPostRequest request, @Parameter(description = "판매글 이미지 리스트", required = true) @RequestPart("files") List files - ) { + ) { GoodsPostResponse response = goodsService.registerGoodsPost(member.getMemberId(), request, files); return ResponseEntity.ok(ApiResponse.success(response)); } @@ -66,8 +58,7 @@ public ResponseEntity> updateGoodsPost( @Operation(summary = "굿즈거래 판매글 삭제", description = "굿즈거래 판매글 상세 페이지에서 판매글을 삭제합니다.") public ResponseEntity deleteGoodsPost( @AuthenticationPrincipal AuthMember member, - @Parameter(description = "삭제할 판매글 ID", required = true) @PathVariable Long goodsPostId) - { + @Parameter(description = "삭제할 판매글 ID", required = true) @PathVariable Long goodsPostId) { goodsService.deleteGoodsPost(member.getMemberId(), goodsPostId); return ResponseEntity.noContent().build(); } @@ -106,7 +97,7 @@ public ResponseEntity> completeGoodsPost( @AuthenticationPrincipal AuthMember member, @Parameter(description = "판매글 ID", required = true) @PathVariable Long goodsPostId, @Parameter(description = "구매자 ID", required = true) @RequestParam Long buyerId - ) { + ) { goodsService.completeTransaction(member.getMemberId(), goodsPostId, buyerId); return ResponseEntity.ok(ApiResponse.success(null)); } diff --git a/src/main/java/com/example/mate/domain/goods/dto/request/GoodsPostRequest.java b/src/main/java/com/example/mate/domain/goods/dto/request/GoodsPostRequest.java index c9b83f5f..db9c2b21 100644 --- a/src/main/java/com/example/mate/domain/goods/dto/request/GoodsPostRequest.java +++ b/src/main/java/com/example/mate/domain/goods/dto/request/GoodsPostRequest.java @@ -5,11 +5,7 @@ import com.example.mate.domain.goods.entity.Category; import com.example.mate.domain.goods.entity.GoodsPost; import com.example.mate.domain.member.entity.Member; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Getter; @@ -40,7 +36,6 @@ public class GoodsPostRequest { @NotNull(message = "위치 정보는 필수 입력 값입니다.") private LocationInfo location; - public static GoodsPost toEntity(Member seller, GoodsPostRequest request) { LocationInfo locationInfo = request.getLocation(); diff --git a/src/main/java/com/example/mate/domain/goods/service/GoodsService.java b/src/main/java/com/example/mate/domain/goods/service/GoodsService.java index c9f8f75b..f15279f1 100644 --- a/src/main/java/com/example/mate/domain/goods/service/GoodsService.java +++ b/src/main/java/com/example/mate/domain/goods/service/GoodsService.java @@ -3,26 +3,20 @@ import com.example.mate.common.error.CustomException; import com.example.mate.common.error.ErrorCode; import com.example.mate.common.response.PageResponse; -import com.example.mate.common.utils.file.FileUploader; -import com.example.mate.common.utils.file.FileValidator; import com.example.mate.domain.constant.TeamInfo; +import com.example.mate.domain.file.FileService; +import com.example.mate.domain.file.FileValidator; import com.example.mate.domain.goods.dto.request.GoodsPostRequest; import com.example.mate.domain.goods.dto.request.GoodsReviewRequest; import com.example.mate.domain.goods.dto.response.GoodsPostResponse; import com.example.mate.domain.goods.dto.response.GoodsPostSummaryResponse; import com.example.mate.domain.goods.dto.response.GoodsReviewResponse; -import com.example.mate.domain.goods.entity.Category; -import com.example.mate.domain.goods.entity.GoodsPost; -import com.example.mate.domain.goods.entity.GoodsPostImage; -import com.example.mate.domain.goods.entity.GoodsReview; -import com.example.mate.domain.goods.entity.Status; +import com.example.mate.domain.goods.entity.*; import com.example.mate.domain.goods.repository.GoodsPostImageRepository; import com.example.mate.domain.goods.repository.GoodsPostRepository; import com.example.mate.domain.goods.repository.GoodsReviewRepository; import com.example.mate.domain.member.entity.Member; import com.example.mate.domain.member.repository.MemberRepository; -import java.util.ArrayList; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -31,6 +25,9 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.util.ArrayList; +import java.util.List; + @Service @Transactional @RequiredArgsConstructor @@ -40,6 +37,7 @@ public class GoodsService { private final GoodsPostRepository goodsPostRepository; private final GoodsPostImageRepository imageRepository; private final GoodsReviewRepository reviewRepository; + private final FileService fileService; public GoodsPostResponse registerGoodsPost(Long memberId, GoodsPostRequest request, List files) { Member seller = findMemberById(memberId); @@ -54,7 +52,8 @@ public GoodsPostResponse registerGoodsPost(Long memberId, GoodsPostRequest reque return GoodsPostResponse.of(savedPost); } - public GoodsPostResponse updateGoodsPost(Long memberId, Long goodsPostId, GoodsPostRequest request, List files) { + public GoodsPostResponse updateGoodsPost(Long memberId, Long goodsPostId, GoodsPostRequest request, + List files) { Member seller = findMemberById(memberId); GoodsPost goodsPost = findGoodsPostById(goodsPostId); @@ -96,11 +95,13 @@ public List getMainGoodsPosts(Long teamId) { } @Transactional(readOnly = true) - public PageResponse getPageGoodsPosts(Long teamId, String categoryVal, Pageable pageable) { + public PageResponse getPageGoodsPosts(Long teamId, String categoryVal, + Pageable pageable) { validateTeamInfo(teamId); Category category = Category.from(categoryVal); - Page pageGoodsPosts = goodsPostRepository.findPageGoodsPosts(teamId, Status.OPEN, category, pageable); + Page pageGoodsPosts = goodsPostRepository.findPageGoodsPosts(teamId, Status.OPEN, category, + pageable); List responses = pageGoodsPosts.getContent().stream() .map(this::convertToSummaryResponse).toList(); @@ -134,7 +135,7 @@ private List uploadImageFiles(List files, GoodsPo List images = new ArrayList<>(); for (MultipartFile file : files) { - String uploadUrl = FileUploader.uploadFile(file); + String uploadUrl = fileService.uploadFile(file); GoodsPostImage image = GoodsPostImage.builder() .imageUrl(uploadUrl) .post(savedPost) @@ -146,11 +147,7 @@ private List uploadImageFiles(List files, GoodsPo private void deleteExistingImageFiles(Long goodsPostId) { List imageUrls = imageRepository.getImageUrlsByPostId(goodsPostId); - imageUrls.forEach(url -> { - if (!FileUploader.deleteFile(url)) { - throw new CustomException(ErrorCode.FILE_DELETE_ERROR); - } - }); + imageUrls.forEach(fileService::deleteFile); imageRepository.deleteAllByPostId(goodsPostId); } diff --git a/src/main/java/com/example/mate/domain/mate/controller/MateController.java b/src/main/java/com/example/mate/domain/mate/controller/MateController.java index df1ed7e1..034c32f8 100644 --- a/src/main/java/com/example/mate/domain/mate/controller/MateController.java +++ b/src/main/java/com/example/mate/domain/mate/controller/MateController.java @@ -34,9 +34,9 @@ public class MateController { @PostMapping @Operation(summary = "메이트 구인글 등록", description = "메이트 구인글 페이지에서 등록합니다.") public ResponseEntity> createMatePost(@Parameter(description = "구인글 등록 데이터", required = true) - @Valid @RequestPart(value = "data") MatePostCreateRequest request, + @Valid @RequestPart(value = "data") MatePostCreateRequest request, @Parameter(description = "구인글 대표사진", required = true) - @RequestPart(value = "file", required = false) MultipartFile file) { + @RequestPart(value = "file", required = false) MultipartFile file) { //TODO - member 정보를 request가 아니라 @AuthenticationPrincipal Long memberId로 받도록 변경 MatePostResponse response = mateService.createMatePost(request, file); return ResponseEntity.ok(ApiResponse.success(response)); @@ -45,7 +45,7 @@ public ResponseEntity> createMatePost(@Parameter(d @GetMapping(value = "/main", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "메인페이지 메이트 구인글 조회", description = "메인 페이지에서 메이트 구인글을 요약한 4개의 리스트를 조회합니다.") public ResponseEntity>> getMainPagePosts(@Parameter(description = "팀 ID") - @RequestParam(required = false) Long teamId) { + @RequestParam(required = false) Long teamId) { List matePostMain = mateService.getMainPagePosts(teamId); return ResponseEntity.ok(ApiResponse.success(matePostMain)); @@ -54,19 +54,19 @@ public ResponseEntity>> getMainPagePos @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "메이트 구인글 페이징 조회", description = "메이트 구인글 페이지에서 팀/카테고리 기준으로 페이징 조회합니다.") public ResponseEntity>> getMatePagePosts(@Parameter(description = "팀 ID") - @RequestParam(required = false) Long teamId, + @RequestParam(required = false) Long teamId, @Parameter(description = "정렬 기준") - @RequestParam(required = false) String sortType, + @RequestParam(required = false) String sortType, @Parameter(description = "연령대 카테고리") - @RequestParam(required = false) String age, + @RequestParam(required = false) String age, @Parameter(description = "성별 카테고리") - @RequestParam(required = false) String gender, + @RequestParam(required = false) String gender, @Parameter(description = "모집인원 수") - @RequestParam(required = false) Integer maxParticipants, + @RequestParam(required = false) Integer maxParticipants, @Parameter(description = "이동수단 카테고리") - @RequestParam(required = false) String transportType, + @RequestParam(required = false) String transportType, @Parameter(description = "페이징 정보") - @PageableDefault Pageable pageable) { + @PageableDefault Pageable pageable) { MatePostSearchRequest request = MatePostSearchRequest.builder() .teamId(teamId) @@ -84,7 +84,7 @@ public ResponseEntity>> getMat @GetMapping("/{postId}") @Operation(summary = "메이트 구인글 상세 조회", description = "메이트 구인글 상세 페이지에서 조회합니다.") public ResponseEntity> getMatePostDetail(@Parameter(description = "조회할 구인글 ID", required = true) - @PathVariable Long postId) { + @PathVariable Long postId) { MatePostDetailResponse response = mateService.getMatePostDetail(postId); return ResponseEntity.ok(ApiResponse.success(response)); @@ -93,13 +93,13 @@ public ResponseEntity> getMatePostDetail(@Pa @PatchMapping("/{memberId}/{postId}") @Operation(summary = "메이트 구인글 수정", description = "메이트 구인글 상세 페이지에서 수정합니다.") public ResponseEntity> updateMatePost(@Parameter(description = "작성자 ID (삭제 예정)", required = true) - @PathVariable Long memberId, + @PathVariable Long memberId, @Parameter(description = "구인글 ID", required = true) @PathVariable Long postId, @Parameter(description = "수정할 구인글 데이터", required = true) - @Valid @RequestPart(value = "data") MatePostUpdateRequest request, + @Valid @RequestPart(value = "data") MatePostUpdateRequest request, @Parameter(description = "수정할 대표사진 파일 ", required = true) - @RequestPart(value = "file", required = false) MultipartFile file) { + @RequestPart(value = "file", required = false) MultipartFile file) { MatePostResponse response = mateService.updateMatePost(memberId, postId, request, file); return ResponseEntity.ok(ApiResponse.success(response)); @@ -111,11 +111,11 @@ public ResponseEntity> updateMatePost(@Parameter(d @PatchMapping("/{memberId}/{postId}/status") @Operation(summary = "메이트 구인글 모집상태 변경", description = "메이트 구인글 채팅방에서 모집상태를 변경합니다.") public ResponseEntity> updateMatePostStatus(@Parameter(description = "작성자 ID (삭제 예정)", required = true) - @PathVariable(value = "memberId") Long memberId, + @PathVariable(value = "memberId") Long memberId, @Parameter(description = "구인글 ID", required = true) - @PathVariable(value = "postId") Long postId, + @PathVariable(value = "postId") Long postId, @Parameter(description = "변경할 모집상태와 현재 참여자 리스트 ID", required = true) - @Valid @RequestBody MatePostStatusRequest request) { + @Valid @RequestBody MatePostStatusRequest request) { MatePostResponse response = mateService.updateMatePostStatus(memberId, postId, request); return ResponseEntity.ok(ApiResponse.success(response)); @@ -125,9 +125,9 @@ public ResponseEntity> updateMatePostStatus(@Param @DeleteMapping("/{memberId}/{postId}") @Operation(summary = "메이트 구인글 삭제", description = "메이트 구인글 상세 페이지에서 삭제합니다.") public ResponseEntity deleteMatePost(@Parameter(description = "작성자 ID (삭제 예정)", required = true) - @PathVariable Long memberId, + @PathVariable Long memberId, @Parameter(description = "삭제할 구인글 ID", required = true) - @PathVariable Long postId) { + @PathVariable Long postId) { mateService.deleteMatePost(memberId, postId); return ResponseEntity.noContent().build(); @@ -137,11 +137,11 @@ public ResponseEntity deleteMatePost(@Parameter(description = "작성자 I @PatchMapping("/{memberId}/{postId}/complete") @Operation(summary = "직관완료 처리", description = "메이트 구인글 채팅방에서 직관완료 처리를 진행합니다.") public ResponseEntity> completeVisit(@Parameter(description = "작성자 ID (삭제 예정)", required = true) - @PathVariable Long memberId, + @PathVariable Long memberId, @Parameter(description = "구인글 ID", required = true) - @PathVariable Long postId, + @PathVariable Long postId, @Parameter(description = "실제 직관 참여자 리스트 ID", required = true) - @Valid @RequestBody MatePostCompleteRequest request) { + @Valid @RequestBody MatePostCompleteRequest request) { MatePostCompleteResponse response = mateService.completeVisit(memberId, postId, request); return ResponseEntity.ok(ApiResponse.success(response)); @@ -151,11 +151,11 @@ public ResponseEntity> completeVisit(@Para @PostMapping("/{memberId}/{postId}/reviews") @Operation(summary = "메이트 직관 후기 등록", description = "직관 타임라인 페이지에서 메이트에 대한 후기를 등록합니다.") public ResponseEntity> createMateReview(@Parameter(description = "작성자 ID (삭제 예정)", required = true) - @PathVariable Long memberId, + @PathVariable Long memberId, @Parameter(description = "구인글 ID", required = true) - @PathVariable Long postId, + @PathVariable Long postId, @Parameter(description = "리뷰 대상자 ID와 평점 및 코멘트", required = true) - @Valid @RequestBody MateReviewCreateRequest request + @Valid @RequestBody MateReviewCreateRequest request ) { MateReviewCreateResponse response = mateService.createReview(postId, memberId, request); diff --git a/src/main/java/com/example/mate/domain/mate/entity/MatePost.java b/src/main/java/com/example/mate/domain/mate/entity/MatePost.java index 68ed9f71..d18532bc 100644 --- a/src/main/java/com/example/mate/domain/mate/entity/MatePost.java +++ b/src/main/java/com/example/mate/domain/mate/entity/MatePost.java @@ -108,4 +108,8 @@ public void complete(List participants) { this.status = Status.VISIT_COMPLETE; this.visit = Visit.createForComplete(this, participants); } + + public void changeImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } } diff --git a/src/main/java/com/example/mate/domain/mate/service/MateService.java b/src/main/java/com/example/mate/domain/mate/service/MateService.java index 044c094e..9d04a8b2 100644 --- a/src/main/java/com/example/mate/domain/mate/service/MateService.java +++ b/src/main/java/com/example/mate/domain/mate/service/MateService.java @@ -2,9 +2,9 @@ import com.example.mate.common.error.CustomException; import com.example.mate.common.response.PageResponse; -import com.example.mate.common.utils.file.FileUploader; -import com.example.mate.common.utils.file.FileValidator; import com.example.mate.domain.constant.TeamInfo; +import com.example.mate.domain.file.FileService; +import com.example.mate.domain.file.FileValidator; import com.example.mate.domain.match.entity.Match; import com.example.mate.domain.match.repository.MatchRepository; import com.example.mate.domain.mate.dto.request.*; @@ -39,6 +39,7 @@ public class MateService { private final MatchRepository matchRepository; private final MemberRepository memberRepository; private final MateReviewRepository mateReviewRepository; + private final FileService fileService; public MatePostResponse createMatePost(MatePostCreateRequest request, MultipartFile file) { Member author = findMemberById(request.getMemberId()); @@ -51,7 +52,7 @@ public MatePostResponse createMatePost(MatePostCreateRequest request, MultipartF .author(author) .teamId(request.getTeamId()) .match(match) - .imageUrl(null) //TODO - image 서비스 배포 후 구현 + .imageUrl(getDefaultMateImageUrl()) .title(request.getTitle()) .content(request.getContent()) .status(Status.OPEN) @@ -62,9 +63,19 @@ public MatePostResponse createMatePost(MatePostCreateRequest request, MultipartF .build(); MatePost savedPost = mateRepository.save(matePost); + handleFileUpload(file, matePost); + return MatePostResponse.from(savedPost); } + private void handleFileUpload(MultipartFile file, MatePost matePost) { + if (file != null && !file.isEmpty()) { + FileValidator.validateSingleImage(file); + String imageUrl = fileService.uploadFile(file); + matePost.changeImageUrl(imageUrl); + } + } + @Transactional(readOnly = true) public List getMainPagePosts(Long teamId) { if (teamId != null && !TeamInfo.existById(teamId)) { @@ -87,11 +98,11 @@ public List getMainPagePosts(Long teamId) { @Transactional(readOnly = true) public PageResponse getMatePagePosts(MatePostSearchRequest request, Pageable pageable) { - if (request.getTeamId()!= null && !TeamInfo.existById(request.getTeamId())) { + if (request.getTeamId() != null && !TeamInfo.existById(request.getTeamId())) { throw new CustomException(TEAM_NOT_FOUND); } - Page matePostPage = mateRepository.findMatePostsByFilter(request ,pageable); + Page matePostPage = mateRepository.findMatePostsByFilter(request, pageable); List content = matePostPage.getContent().stream() .map(MatePostSummaryResponse::from) @@ -114,7 +125,8 @@ public MatePostDetailResponse getMatePostDetail(Long postId) { return MatePostDetailResponse.from(matePost); } - public MatePostResponse updateMatePost(Long memberId, Long postId, MatePostUpdateRequest request, MultipartFile file) { + public MatePostResponse updateMatePost(Long memberId, Long postId, MatePostUpdateRequest request, + MultipartFile file) { MatePost matePost = findMatePostById(postId); validateAuthorization(matePost, memberId); validatePostStatus(matePost.getStatus()); @@ -144,16 +156,20 @@ private String updateImage(String currentImageUrl, MultipartFile newFile) { return currentImageUrl; } - // 새 파일이 있는 경우에만 유효성 검증 수행 - FileValidator.validateMatePostImage(newFile); + FileValidator.validateSingleImage(newFile); + deleteNonDefaultImage(currentImageUrl); - // 기존 파일이 있으면 삭제 - if (currentImageUrl != null) { - FileUploader.deleteFile(currentImageUrl); + return fileService.uploadFile(newFile); + } + + private void deleteNonDefaultImage(String imageUrl) { + if (!imageUrl.equals(getDefaultMateImageUrl())) { + fileService.deleteFile(imageUrl); } + } - // 새 파일 업로드 - return FileUploader.uploadFile(newFile); + private String getDefaultMateImageUrl() { + return "https://" + fileService.getBucket() + ".s3.ap-northeast-2.amazonaws.com/mate_default.svg"; } public MatePostResponse updateMatePostStatus(Long memberId, Long postId, MatePostStatusRequest request) { @@ -162,7 +178,7 @@ public MatePostResponse updateMatePostStatus(Long memberId, Long postId, MatePos validateAuthorization(matePost, memberId); validatePostStatus(matePost.getStatus()); - if(request.getStatus() == Status.CLOSED) { + if (request.getStatus() == Status.CLOSED) { findAndValidateParticipants(request.getParticipantIds(), matePost.getMaxParticipants()); } matePost.changeStatus(request.getStatus()); @@ -175,6 +191,7 @@ public void deleteMatePost(Long memberId, Long postId) { validateAuthorization(matePost, memberId); validatePostStatus(matePost.getStatus()); + deleteNonDefaultImage(matePost.getImageUrl()); mateRepository.delete(matePost); } @@ -192,7 +209,8 @@ public MatePostCompleteResponse completeVisit(Long memberId, Long postId, MatePo validateCompletionTime(matePost); validateCompletionStatus(matePost); - List participants = findAndValidateParticipants(request.getParticipantIds(), matePost.getMaxParticipants()); + List participants = findAndValidateParticipants(request.getParticipantIds(), + matePost.getMaxParticipants()); matePost.complete(participants); return MatePostCompleteResponse.from(matePost); diff --git a/src/main/java/com/example/mate/domain/member/controller/MemberController.java b/src/main/java/com/example/mate/domain/member/controller/MemberController.java index 0551495c..f81207f0 100644 --- a/src/main/java/com/example/mate/domain/member/controller/MemberController.java +++ b/src/main/java/com/example/mate/domain/member/controller/MemberController.java @@ -17,15 +17,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -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.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @RestController @@ -70,7 +62,7 @@ public ResponseEntity> findMemberInfo( @Operation(summary = "회원 내 정보 수정") @PutMapping(value = "/me") public ResponseEntity> updateMemberInfo( - @Parameter(description = "프로필 사진") @RequestPart(value = "image", required = false) MultipartFile image, + @Parameter(description = "프로필 사진") @RequestPart(value = "file", required = false) MultipartFile image, @Parameter(description = "수정할 회원 정보") @Valid @RequestPart(value = "data") MemberInfoUpdateRequest updateRequest, @Parameter(description = "회원 로그인 정보") @AuthenticationPrincipal AuthMember authMember) { updateRequest.setMemberId(authMember.getMemberId()); diff --git a/src/main/java/com/example/mate/domain/member/entity/Member.java b/src/main/java/com/example/mate/domain/member/entity/Member.java index 819390a1..ef1c9bf0 100644 --- a/src/main/java/com/example/mate/domain/member/entity/Member.java +++ b/src/main/java/com/example/mate/domain/member/entity/Member.java @@ -3,22 +3,12 @@ import com.example.mate.domain.constant.Gender; import com.example.mate.domain.constant.TeamInfo; import com.example.mate.domain.member.dto.request.JoinRequest; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.persistence.*; +import lombok.*; + import java.time.LocalDate; import java.util.HashMap; import java.util.Map; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; @Entity @Table(name = "member") @@ -42,9 +32,8 @@ public class Member { @Column(name = "email", length = 40, nullable = false, unique = true) private String email; - @Builder.Default @Column(name = "image_url", nullable = false) - private String imageUrl = "/images/default.png"; // TODO : 이미지 기본 경로 설정 필요 + private String imageUrl; @Column(name = "age", nullable = false) private Integer age; @@ -84,11 +73,12 @@ public void changeAboutMe(String aboutMe) { this.aboutMe = aboutMe; } - public static Member from(JoinRequest request) { + public static Member of(JoinRequest request, String imageUrl) { return Member.builder() .name(request.getName()) .nickname(request.getNickname()) .email(request.getEmail()) + .imageUrl(imageUrl) .age(LocalDate.now().getYear() - Integer.parseInt(request.getBirthyear())) .gender(Gender.fromCode(request.getGender())) .teamId(request.getTeamId()) diff --git a/src/main/java/com/example/mate/domain/member/service/MemberService.java b/src/main/java/com/example/mate/domain/member/service/MemberService.java index 0f91f4c2..7527e0c0 100644 --- a/src/main/java/com/example/mate/domain/member/service/MemberService.java +++ b/src/main/java/com/example/mate/domain/member/service/MemberService.java @@ -4,9 +4,9 @@ import com.example.mate.common.error.ErrorCode; import com.example.mate.common.jwt.JwtToken; import com.example.mate.common.security.util.JwtUtil; -import com.example.mate.common.utils.file.FileUploader; -import com.example.mate.common.utils.file.FileValidator; import com.example.mate.domain.constant.TeamInfo; +import com.example.mate.domain.file.FileService; +import com.example.mate.domain.file.FileValidator; import com.example.mate.domain.goods.entity.Status; import com.example.mate.domain.goods.repository.GoodsPostRepository; import com.example.mate.domain.goods.repository.GoodsReviewRepository; @@ -22,12 +22,13 @@ import com.example.mate.domain.member.entity.Member; import com.example.mate.domain.member.repository.FollowRepository; import com.example.mate.domain.member.repository.MemberRepository; -import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.util.Map; + @Service @Transactional @RequiredArgsConstructor @@ -40,16 +41,16 @@ public class MemberService { private final MateReviewRepository mateReviewRepository; private final VisitPartRepository visitPartRepository; private final JwtUtil jwtUtil; - - private static final String DEFAULT_IMAGE_URL = "image/default.png"; + private final FileService fileService; // 자체 회원가입 기능 public JoinResponse join(JoinRequest request) { - Member savedMember = memberRepository.save(Member.from(request)); + Member savedMember = memberRepository.save(Member.of(request, getDefaultMemberImageUrl())); return JoinResponse.from(savedMember); } // 자체 로그인 기능 + @Transactional(readOnly = true) public MemberLoginResponse loginByEmail(MemberLoginRequest request) { Member member = findByEmail(request.getEmail()); return MemberLoginResponse.from(member, makeToken(member)); @@ -73,27 +74,29 @@ private Member findByEmail(String email) { } // 내 프로필 조회 + @Transactional(readOnly = true) public MyProfileResponse getMyProfile(Long memberId) { return getProfile(memberId, MyProfileResponse.class); } // 다른 회원 프로필 조회 + @Transactional(readOnly = true) public MemberProfileResponse getMemberProfile(Long memberId) { return getProfile(memberId, MemberProfileResponse.class); } // 회원 정보 수정 - public MyProfileResponse updateMyProfile(MultipartFile image, MemberInfoUpdateRequest request) { + public MyProfileResponse updateMyProfile(MultipartFile file, MemberInfoUpdateRequest request) { Member member = findByMemberId(request.getMemberId()); - checkNickNameAndChange(member, request.getNickname()); // 닉네임 중복 검증한 뒤 바뀐 경우에만 수정 + checkNicknameAndChange(member, request.getNickname()); // 닉네임 중복 검증한 뒤 바뀐 경우에만 수정 member.changeTeam(TeamInfo.getById(request.getTeamId())); member.changeAboutMe(request.getAboutMe()); - if (image != null && !image.isEmpty()) { - FileValidator.validateMyProfileImage(image); - member.changeImageUrl(FileUploader.uploadFile(image)); // TODO : 실제 업로드 처리 필요 - } else { - member.changeImageUrl(DEFAULT_IMAGE_URL); + + if (file != null && !file.isEmpty()) { + FileValidator.validateSingleImage(file); + deleteNonDefaultImage(member.getImageUrl()); + member.changeImageUrl(fileService.uploadFile(file)); } return MyProfileResponse.from(memberRepository.save(member)); @@ -101,11 +104,22 @@ public MyProfileResponse updateMyProfile(MultipartFile image, MemberInfoUpdateRe // 회원 탈퇴 public void deleteMember(Long memberId) { - findByMemberId(memberId); + Member member = findByMemberId(memberId); + deleteNonDefaultImage(member.getImageUrl()); memberRepository.deleteById(memberId); } - private void checkNickNameAndChange(Member member, String request) { + private void deleteNonDefaultImage(String imageUrl) { + if (!imageUrl.equals(getDefaultMemberImageUrl())) { + fileService.deleteFile(imageUrl); + } + } + + private String getDefaultMemberImageUrl() { + return "https://" + fileService.getBucket() + ".s3.ap-northeast-2.amazonaws.com/member_default.svg"; + } + + private void checkNicknameAndChange(Member member, String request) { if (member.getNickname().equals(request)) { return; } diff --git a/src/main/resources/application-common.yml b/src/main/resources/application-common.yml index 728797a4..84b1578f 100644 --- a/src/main/resources/application-common.yml +++ b/src/main/resources/application-common.yml @@ -34,4 +34,17 @@ jwt: openweather: api: - key: ${OPENWEATHER_API_KEY} \ No newline at end of file + key: ${OPENWEATHER_API_KEY} + +cloud: + aws: + s3: + bucket: ${S3_BUCKET_NAME} + credentials: + access_key: ${IAM_ACCESS_KEY} + secret_key: ${IAM_SECRET_KEY} + region: + static: ap-northeast-2 + auto: false + stack: + auto: false \ No newline at end of file diff --git a/src/test/java/com/example/mate/common/utils/file/FileUploaderTest.java b/src/test/java/com/example/mate/common/utils/file/FileUploaderTest.java deleted file mode 100644 index 7feb915e..00000000 --- a/src/test/java/com/example/mate/common/utils/file/FileUploaderTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.mate.common.utils.file; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.web.multipart.MultipartFile; - -@ExtendWith(MockitoExtension.class) -class FileUploaderTest { - - @Test - @DisplayName("파일 업로드 시 저장할 수 없는 파일명은 대체 문자로 변경되어야 한다.") - void uploadFile_should_replace_invalid_characters() { - // given - String originalFileName = "test file name @123.jpg"; - MultipartFile mockFile = mock(MultipartFile.class); - when(mockFile.getOriginalFilename()).thenReturn(originalFileName); - - // when - String uploadUrl = FileUploader.uploadFile(mockFile); - // 파일명 추출 - String fileName = uploadUrl.substring(uploadUrl.lastIndexOf("/") + 1); - String cleanedFileName = fileName.substring(fileName.indexOf('_') + 1); - - // then - assertThat(cleanedFileName).isEqualTo("test_file_name__123.jpg"); - } - - @Test - @DisplayName("파일 업로드 시 파일명에 UUID 가 포함되어야 한다.") - void uploadFile_should_contain_uuid() { - // given - String originalFileName = "test_filename.jpg"; - MultipartFile mockFile = mock(MultipartFile.class); - when(mockFile.getOriginalFilename()).thenReturn(originalFileName); - - // when - String uploadUrl = FileUploader.uploadFile(mockFile); - String fileName = uploadUrl.substring(uploadUrl.lastIndexOf("/") + 1); - String uuid = fileName.substring(0, fileName.indexOf('_')); - - // then - assertThat(uuid).matches("[0-9a-fA-F-]{36}"); - assertThat(uuid).hasSize(36); - } -} \ No newline at end of file diff --git a/src/test/java/com/example/mate/common/utils/file/FileValidatorTest.java b/src/test/java/com/example/mate/common/utils/file/FileValidatorTest.java index b5a0b5e9..36720d1f 100644 --- a/src/test/java/com/example/mate/common/utils/file/FileValidatorTest.java +++ b/src/test/java/com/example/mate/common/utils/file/FileValidatorTest.java @@ -1,14 +1,8 @@ package com.example.mate.common.utils.file; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import com.example.mate.common.error.CustomException; import com.example.mate.common.error.ErrorCode; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import com.example.mate.domain.file.FileValidator; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,6 +12,14 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.web.multipart.MultipartFile; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class FileValidatorTest { @@ -77,9 +79,6 @@ void should_throw_CustomException_when_file_size_is_over_10() { MultipartFile mockFile = mock(MultipartFile.class); List files = new ArrayList<>(Collections.nCopies(11, mockFile)); - // when - when(mockFile.getContentType()).thenReturn("image/jpg"); - // then assertThatThrownBy(() -> FileValidator.validateGoodsPostImages(files)) .isInstanceOf(CustomException.class) diff --git a/src/test/java/com/example/mate/domain/goods/service/GoodsServiceTest.java b/src/test/java/com/example/mate/domain/goods/service/GoodsServiceTest.java index bbab3f56..9f84f753 100644 --- a/src/test/java/com/example/mate/domain/goods/service/GoodsServiceTest.java +++ b/src/test/java/com/example/mate/domain/goods/service/GoodsServiceTest.java @@ -1,35 +1,22 @@ package com.example.mate.domain.goods.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import com.example.mate.common.error.CustomException; import com.example.mate.common.error.ErrorCode; import com.example.mate.common.response.PageResponse; import com.example.mate.domain.constant.Rating; +import com.example.mate.domain.file.FileService; import com.example.mate.domain.goods.dto.LocationInfo; import com.example.mate.domain.goods.dto.request.GoodsPostRequest; import com.example.mate.domain.goods.dto.request.GoodsReviewRequest; import com.example.mate.domain.goods.dto.response.GoodsPostResponse; import com.example.mate.domain.goods.dto.response.GoodsPostSummaryResponse; import com.example.mate.domain.goods.dto.response.GoodsReviewResponse; -import com.example.mate.domain.goods.entity.Category; -import com.example.mate.domain.goods.entity.GoodsPost; -import com.example.mate.domain.goods.entity.GoodsPostImage; -import com.example.mate.domain.goods.entity.GoodsReview; -import com.example.mate.domain.goods.entity.Status; +import com.example.mate.domain.goods.entity.*; import com.example.mate.domain.goods.repository.GoodsPostImageRepository; import com.example.mate.domain.goods.repository.GoodsPostRepository; import com.example.mate.domain.goods.repository.GoodsReviewRepository; import com.example.mate.domain.member.entity.Member; import com.example.mate.domain.member.repository.MemberRepository; -import java.util.List; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -44,6 +31,15 @@ import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class GoodsServiceTest { @@ -62,6 +58,9 @@ class GoodsServiceTest { @Mock private GoodsReviewRepository reviewRepository; + @Mock + private FileService fileService; + private Member member; private GoodsPost goodsPost; @@ -201,7 +200,7 @@ void update_goods_post_success() { given(memberRepository.findById(member.getId())).willReturn(Optional.of(member)); given(goodsPostRepository.findById(goodsPostId)).willReturn(Optional.of(goodsPost)); - given(imageRepository.getImageUrlsByPostId(goodsPostId)).willReturn(List.of()); + given(imageRepository.getImageUrlsByPostId(goodsPostId)).willReturn(List.of("test.png")); // when GoodsPostResponse actual = goodsService.updateGoodsPost(member.getId(), goodsPostId, request, files); diff --git a/src/test/java/com/example/mate/domain/mate/integration/MateIntegrationTest.java b/src/test/java/com/example/mate/domain/mate/integration/MateIntegrationTest.java index 7e15d447..12a13a7c 100644 --- a/src/test/java/com/example/mate/domain/mate/integration/MateIntegrationTest.java +++ b/src/test/java/com/example/mate/domain/mate/integration/MateIntegrationTest.java @@ -1,19 +1,5 @@ package com.example.mate.domain.mate.integration; -import static com.example.mate.common.error.ErrorCode.MATCH_NOT_FOUND_BY_ID; -import static com.example.mate.common.error.ErrorCode.MATE_POST_NOT_FOUND_BY_ID; -import static com.example.mate.common.error.ErrorCode.MATE_POST_UPDATE_NOT_ALLOWED; -import static com.example.mate.common.error.ErrorCode.MEMBER_NOT_FOUND_BY_ID; -import static com.example.mate.common.error.ErrorCode.TEAM_NOT_FOUND; -import static com.example.mate.domain.match.entity.MatchStatus.SCHEDULED; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.example.mate.common.security.util.JwtUtil; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.match.entity.Match; @@ -28,8 +14,6 @@ import com.example.mate.domain.member.entity.Member; import com.example.mate.domain.member.repository.MemberRepository; import com.fasterxml.jackson.databind.ObjectMapper; -import java.time.LocalDateTime; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -44,6 +28,17 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; +import java.util.List; + +import static com.example.mate.common.error.ErrorCode.*; +import static com.example.mate.domain.match.entity.MatchStatus.SCHEDULED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @Transactional @@ -128,6 +123,7 @@ private MatePost createMatePost(Match match, Long teamId, Status status) { .content("테스트 내용") .status(status) .maxParticipants(4) + .imageUrl("imageUrl") .age(Age.TWENTIES) .gender(Gender.FEMALE) .transport(TransportType.PUBLIC) diff --git a/src/test/java/com/example/mate/domain/mate/service/MateServiceTest.java b/src/test/java/com/example/mate/domain/mate/service/MateServiceTest.java index 5bbc0e8f..e0df0e6d 100644 --- a/src/test/java/com/example/mate/domain/mate/service/MateServiceTest.java +++ b/src/test/java/com/example/mate/domain/mate/service/MateServiceTest.java @@ -4,6 +4,7 @@ import com.example.mate.common.response.PageResponse; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.constant.StadiumInfo; +import com.example.mate.domain.file.FileService; import com.example.mate.domain.match.entity.Match; import com.example.mate.domain.match.repository.MatchRepository; import com.example.mate.domain.mate.dto.request.MatePostCreateRequest; @@ -57,6 +58,9 @@ class MateServiceTest { @Mock private MemberRepository memberRepository; + @Mock + private FileService fileService; + private static final Long TEST_MEMBER_ID = 1L; private static final Long TEST_MATCH_ID = 1L; @@ -880,6 +884,7 @@ void deleteMatePost_Success() { .match(testMatch) .title("테스트 제목") .content("테스트 내용") + .imageUrl("image.png") .status(Status.OPEN) .maxParticipants(4) .age(Age.TWENTIES) @@ -911,6 +916,7 @@ void deleteMatePost_SuccessWithCompletedPost() { .author(testMember) .teamId(1L) .match(testMatch) + .imageUrl("image.png") .title("테스트 제목") .content("테스트 내용") .status(Status.OPEN) diff --git a/src/test/java/com/example/mate/domain/member/service/MemberServiceTest.java b/src/test/java/com/example/mate/domain/member/service/MemberServiceTest.java index cdded6cc..de614c98 100644 --- a/src/test/java/com/example/mate/domain/member/service/MemberServiceTest.java +++ b/src/test/java/com/example/mate/domain/member/service/MemberServiceTest.java @@ -1,16 +1,11 @@ package com.example.mate.domain.member.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; - import com.example.mate.common.error.CustomException; import com.example.mate.common.error.ErrorCode; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.constant.Rating; import com.example.mate.domain.constant.TeamInfo; +import com.example.mate.domain.file.FileService; import com.example.mate.domain.goods.entity.GoodsPost; import com.example.mate.domain.goods.entity.GoodsReview; import com.example.mate.domain.goods.entity.Status; @@ -29,7 +24,6 @@ import com.example.mate.domain.member.entity.Member; import com.example.mate.domain.member.repository.FollowRepository; import com.example.mate.domain.member.repository.MemberRepository; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -42,6 +36,14 @@ import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + @ExtendWith(MockitoExtension.class) class MemberServiceTest { @@ -66,6 +68,9 @@ class MemberServiceTest { @Mock private VisitPartRepository visitPartRepository; + @Mock + private FileService fileService; + private Member member; private Member member2; @@ -93,8 +98,10 @@ void setUp() { private void createTestMember() { member = Member.builder() .id(1L) + .imageUrl("image.png") .name("홍길동") .nickname("tester") + .imageUrl("image.png") .email("test@example.com") .age(30) .gender(Gender.MALE)