diff --git a/src/main/java/com/jeju/nanaland/domain/member/controller/MemberController.java b/src/main/java/com/jeju/nanaland/domain/member/controller/MemberController.java index 38594111..3f0d1016 100644 --- a/src/main/java/com/jeju/nanaland/domain/member/controller/MemberController.java +++ b/src/main/java/com/jeju/nanaland/domain/member/controller/MemberController.java @@ -42,7 +42,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -50,9 +49,7 @@ import org.springframework.web.bind.annotation.RequestHeader; 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.multipart.MultipartFile; @Slf4j @RestController @@ -75,12 +72,11 @@ public class MemberController { @ApiResponse(responseCode = "409", description = "이미 가입된 계정이 있는 경우, 닉네임이 중복되는 경우", content = @Content), @ApiResponse(responseCode = "500", description = "서버측 에러", content = @Content) }) - @PostMapping(value = "/join", - consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "/join") public BaseResponse join( - @RequestPart(value = "reqDto") @Valid MemberRequest.JoinDto joinDto, - @RequestPart(required = false) MultipartFile multipartFile) { - JwtDto jwtDto = memberLoginService.join(joinDto, multipartFile); + @RequestBody @Valid MemberRequest.JoinDto joinDto, + @RequestParam(required = false) String fileKey) { + JwtDto jwtDto = memberLoginService.join(joinDto, fileKey); return BaseResponse.success(JOIN_SUCCESS, jwtDto); } diff --git a/src/main/java/com/jeju/nanaland/domain/member/service/MemberLoginService.java b/src/main/java/com/jeju/nanaland/domain/member/service/MemberLoginService.java index f8e91ae0..626fbd23 100644 --- a/src/main/java/com/jeju/nanaland/domain/member/service/MemberLoginService.java +++ b/src/main/java/com/jeju/nanaland/domain/member/service/MemberLoginService.java @@ -9,7 +9,6 @@ import com.jeju.nanaland.domain.common.data.Language; import com.jeju.nanaland.domain.common.data.Status; import com.jeju.nanaland.domain.common.entity.ImageFile; -import com.jeju.nanaland.domain.common.service.FileService; import com.jeju.nanaland.domain.common.service.ImageFileService; import com.jeju.nanaland.domain.member.dto.MemberRequest; import com.jeju.nanaland.domain.member.dto.MemberResponse.MemberInfoDto; @@ -26,8 +25,10 @@ import com.jeju.nanaland.global.exception.ConflictException; import com.jeju.nanaland.global.exception.NotFoundException; import com.jeju.nanaland.global.exception.UnauthorizedException; +import com.jeju.nanaland.global.file.data.FileCategory; +import com.jeju.nanaland.global.file.service.FileUploadService; +import com.jeju.nanaland.global.image_upload.dto.S3ImageDto; import com.jeju.nanaland.global.util.JwtUtil; -import java.io.File; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -36,7 +37,6 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; @Service @RequiredArgsConstructor @@ -49,19 +49,19 @@ public class MemberLoginService { private final MemberConsentService memberConsentService; private final ImageFileService imageFileService; private final FcmTokenService fcmTokenService; - private final FileService fileService; private final MemberProfileService memberProfileService; + private final FileUploadService fileUploadService; /** * 회원 가입 * - * @param joinDto 회원 가입 정보 - * @param multipartFile 프로필 사진 + * @param joinDto 회원 가입 정보 + * @param fileKey 파일 키 * @return JWT * @throws ConflictException provider, providerId로 이미 가입된 회원이 존재하는 경우 */ @Transactional - public JwtDto join(MemberRequest.JoinDto joinDto, MultipartFile multipartFile) { + public JwtDto join(MemberRequest.JoinDto joinDto, String fileKey) { Optional savedMember = memberRepository.findByProviderAndProviderId( Provider.valueOf(joinDto.getProvider()), joinDto.getProviderId()); @@ -73,7 +73,7 @@ public JwtDto join(MemberRequest.JoinDto joinDto, MultipartFile multipartFile) { String nickname = determineNickname(joinDto); validateNickname(nickname); - ImageFile profileImageFile = memberProfileService.saveRandomProfileImageFile(); + ImageFile profileImageFile = createProfileImageFile(fileKey); Member member = createMember(joinDto, profileImageFile, nickname); // GUEST가 아닌 경우, 이용약관 저장 @@ -86,15 +86,26 @@ public JwtDto join(MemberRequest.JoinDto joinDto, MultipartFile multipartFile) { fcmTokenService.createFcmToken(member, joinDto.getFcmToken()); } - // 비동기 처리 - if (multipartFile != null && !multipartFile.isEmpty()) { - File convertedFile = fileService.convertMultipartFileToFile(multipartFile); - imageFileService.uploadMemberProfileImage(member.getId(), convertedFile); - } - return getJwtDto(member); } + /** + * 프로필 사진 업로드 및 저장. + * 프로필 사진이 없는 경우엔, 랜덤 프로필 사진 저장 + * + * @param fileKey 파일 키 + * @return 저장된 이미지 파일 또는 랜덤 프로필 사진 파일 + */ + private ImageFile createProfileImageFile(String fileKey) { + if (fileKey == null) { + return memberProfileService.saveRandomProfileImageFile(); + } + fileUploadService.validateFileExtension(fileKey, FileCategory.MEMBER_PROFILE); + S3ImageDto s3ImageDto = fileUploadService.getCloudImageUrls(fileKey); + return imageFileService.saveS3ImageFile(s3ImageDto); + } + + /** * 닉네임 설정. GUEST 유형의 경우 UUID를 사용하여 랜덤 닉네임 생성. GUEST가 아닌 경우, 제공된 닉네임을 반환. * @@ -202,7 +213,6 @@ private JwtDto getJwtDto(Member member) { * @param member 회원 * @throws NotFoundException 존재하는 회원 탈퇴 정보가 없는 경우 */ - @Transactional public void updateMemberActive(Member member) { if (member.getStatus().equals(Status.INACTIVE)) { member.updateStatus(Status.ACTIVE); @@ -219,7 +229,6 @@ public void updateMemberActive(Member member) { * @param loginDto 로그인 정보 * @param member 회원 */ - @Transactional public void updateLanguageDifferent(MemberRequest.LoginDto loginDto, Member member) { Language language = Language.valueOf(loginDto.getLocale()); if (!member.getLanguage().equals(language)) { @@ -316,7 +325,6 @@ public void withdrawal(MemberInfoDto memberInfoDto, MemberRequest.WithdrawalDto /** * 매일 0시 0분 0초에 실행되는 회원 탈퇴 스케줄러. 비활성화 후 3개월이 지난 회원 탈퇴 처리 */ - @Transactional @Scheduled(cron = "0 0 0 * * *") public void deleteWithdrawalMemberInfo() { List members = memberRepository.findAllInactiveMember(); diff --git a/src/main/java/com/jeju/nanaland/global/exception/ErrorCode.java b/src/main/java/com/jeju/nanaland/global/exception/ErrorCode.java index d9e6ec45..6b74c3a0 100644 --- a/src/main/java/com/jeju/nanaland/global/exception/ErrorCode.java +++ b/src/main/java/com/jeju/nanaland/global/exception/ErrorCode.java @@ -31,7 +31,6 @@ public enum ErrorCode { SELF_REPORT_NOT_ALLOWED(BAD_REQUEST, "본인을 신고하는 요청은 유효하지 않습니다."), ALREADY_REPORTED(BAD_REQUEST, "이미 신고되었습니다."), NO_NOTIFICATION_CONSENT(BAD_REQUEST, "알림 동의를 하지 않은 유저입니다."), - INVALID_FILE_SIZE(BAD_REQUEST, "파일 크기가 유효하지 않습니다."), INVALID_FILE_EXTENSION_TYPE(BAD_REQUEST, "해당 카테고리에서 지원하지 않는 파일 형식입니다."), NO_FILE_EXTENSION(BAD_REQUEST, "파일 확장자가 없습니다."), diff --git a/src/main/java/com/jeju/nanaland/global/file/dto/FileRequest.java b/src/main/java/com/jeju/nanaland/global/file/dto/FileRequest.java index 45c5f765..125dd6e8 100644 --- a/src/main/java/com/jeju/nanaland/global/file/dto/FileRequest.java +++ b/src/main/java/com/jeju/nanaland/global/file/dto/FileRequest.java @@ -3,6 +3,8 @@ import com.jeju.nanaland.domain.common.annotation.EnumValid; import com.jeju.nanaland.global.file.data.FileCategory; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -23,10 +25,12 @@ public static class InitCommandDto { @NotNull @Schema(description = "파일 크기") + @Max(30 * 1024 * 1024) private Long fileSize; @NotNull @Schema(description = "파일 파트 개수") + @Min(1) private int partCount; @EnumValid( diff --git a/src/main/java/com/jeju/nanaland/global/file/service/FileUploadService.java b/src/main/java/com/jeju/nanaland/global/file/service/FileUploadService.java index dc681810..206ca515 100644 --- a/src/main/java/com/jeju/nanaland/global/file/service/FileUploadService.java +++ b/src/main/java/com/jeju/nanaland/global/file/service/FileUploadService.java @@ -4,7 +4,6 @@ import static com.jeju.nanaland.global.exception.ErrorCode.FILE_S3_NOT_FOUNE; import static com.jeju.nanaland.global.exception.ErrorCode.FILE_UPLOAD_FAIL; import static com.jeju.nanaland.global.exception.ErrorCode.INVALID_FILE_EXTENSION_TYPE; -import static com.jeju.nanaland.global.exception.ErrorCode.INVALID_FILE_SIZE; import static com.jeju.nanaland.global.exception.ErrorCode.NO_FILE_EXTENSION; import com.amazonaws.HttpMethod; @@ -29,7 +28,6 @@ import com.jeju.nanaland.global.image_upload.dto.S3VideoDto; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import java.net.URL; import java.time.LocalDate; import java.time.LocalDateTime; @@ -51,7 +49,6 @@ public class FileUploadService { private static final int MAX_IMAGE_COUNT = 5; private static final int PRESIGNEDURL_EXPIRATION = 30; - private static final long MAX_FILE_SIZE = 30 * 1024 * 1024L; private final AmazonS3 amazonS3; private final AmazonS3Client amazonS3Client; @Value("${cloud.aws.cloudfront.domain}") @@ -68,9 +65,6 @@ public class FileUploadService { private String claimReportDirectory; public FileResponse.InitResultDto uploadInit(FileRequest.InitCommandDto initCommandDto) { - // 파일 크기 유효성 검사 - validateFileSize(initCommandDto.getFileSize()); - // 파일 형식 유효성 검사 String contentType = validateFileExtension(initCommandDto.getOriginalFileName(), FileCategory.valueOf(initCommandDto.getFileCategory())); @@ -121,12 +115,6 @@ public FileResponse.InitResultDto uploadInit(FileRequest.InitCommandDto initComm } } - private void validateFileSize(@NotNull Long fileSize) { - if (fileSize > MAX_FILE_SIZE) { - throw new BadRequestException(INVALID_FILE_SIZE.getMessage()); - } - } - /** * 파일 확장자 유효성 확인 * diff --git a/src/test/java/com/jeju/nanaland/domain/member/service/MemberLoginServiceTest.java b/src/test/java/com/jeju/nanaland/domain/member/service/MemberLoginServiceTest.java index 16ead2ff..2656ea3c 100644 --- a/src/test/java/com/jeju/nanaland/domain/member/service/MemberLoginServiceTest.java +++ b/src/test/java/com/jeju/nanaland/domain/member/service/MemberLoginServiceTest.java @@ -14,6 +14,7 @@ import com.jeju.nanaland.domain.common.data.Language; import com.jeju.nanaland.domain.common.data.Status; import com.jeju.nanaland.domain.common.entity.ImageFile; +import com.jeju.nanaland.domain.common.service.ImageFileService; import com.jeju.nanaland.domain.member.dto.MemberRequest; import com.jeju.nanaland.domain.member.dto.MemberRequest.JoinDto; import com.jeju.nanaland.domain.member.dto.MemberResponse.MemberInfoDto; @@ -32,6 +33,7 @@ import com.jeju.nanaland.global.exception.ErrorCode; import com.jeju.nanaland.global.exception.NotFoundException; import com.jeju.nanaland.global.exception.UnauthorizedException; +import com.jeju.nanaland.global.file.service.FileUploadService; import com.jeju.nanaland.global.util.JwtUtil; import java.time.LocalDate; import java.util.ArrayList; @@ -47,8 +49,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; @ExtendWith(MockitoExtension.class) @@ -71,6 +71,10 @@ class MemberLoginServiceTest { private FcmTokenService fcmTokenService; @Mock private MemberProfileService memberProfileService; + @Mock + private FileUploadService fileUploadService; + @Mock + private ImageFileService imageFileService; @InjectMocks private MemberLoginService memberLoginService; @@ -243,26 +247,22 @@ void joinSuccess_multipartFileExists() { // given: 프로필 사진이 있는 경우 Language language = Language.KOREAN; Member member = createMember(language, joinDto); - MultipartFile multipartFile = new MockMultipartFile("file", "test.jpg", "image/jpeg", - new byte[0]); + String fileKey = "test/1.png"; doReturn(Optional.empty()) .when(memberRepository).findByProviderAndProviderId(any(Provider.class), any(String.class)); doReturn(Optional.empty()).when(memberRepository).findByNickname(any(String.class)); - doReturn(imageFile).when(memberProfileService).saveRandomProfileImageFile(); doReturn(member).when(memberRepository).save(any(Member.class)); doReturn("accessToken").when(jwtUtil).createAccessToken(any(String.class), anySet()); doReturn("refreshToken").when(jwtUtil).createRefreshToken(any(String.class), anySet()); // when: 회원 가입 - JwtDto result = memberLoginService.join(joinDto, multipartFile); + JwtDto result = memberLoginService.join(joinDto, fileKey); // then: JWT 생성 확인, 이용약관 생성 확인, 프로필 사진 확인 assertThat(result).isNotNull(); assertThat(result.getAccessToken()).isEqualTo("accessToken"); assertThat(result.getRefreshToken()).isEqualTo("refreshToken"); verify(memberConsentService).createMemberConsents(any(Member.class), anyList()); - verify(memberRepository).save(argThat(savedMember -> - savedMember.getProfileImageFile().equals(imageFile))); } @Test @@ -273,27 +273,22 @@ void joinSuccess_fcmTokenExists(){ JoinDto joinDto2 = createJoinDto("GOOGLE"); joinDto2.setFcmToken("fcmToken"); Member member = createMember(language, joinDto2); - MultipartFile multipartFile = new MockMultipartFile("file", "test.jpg", "image/jpeg", - new byte[0]); doReturn(Optional.empty()) .when(memberRepository).findByProviderAndProviderId(any(Provider.class), any(String.class)); doReturn(Optional.empty()).when(memberRepository).findByNickname(any(String.class)); - doReturn(imageFile).when(memberProfileService).saveRandomProfileImageFile(); doReturn(member).when(memberRepository).save(any(Member.class)); doReturn("accessToken").when(jwtUtil).createAccessToken(any(String.class), anySet()); doReturn("refreshToken").when(jwtUtil).createRefreshToken(any(String.class), anySet()); // when: 회원 가입 - JwtDto result = memberLoginService.join(joinDto2, multipartFile); + JwtDto result = memberLoginService.join(joinDto2, null); - // then: JWT 생성 확인, 이용약관 생성 확인, fcmToken 생성 확인, 프로필 사진 확인 + // then: JWT 생성 확인, 이용약관 생성 확인, fcmToken 생성 확인 assertThat(result).isNotNull(); assertThat(result.getAccessToken()).isEqualTo("accessToken"); assertThat(result.getRefreshToken()).isEqualTo("refreshToken"); verify(memberConsentService).createMemberConsents(any(Member.class), anyList()); verify(fcmTokenService).createFcmToken(any(Member.class), any(String.class)); - verify(memberRepository).save(argThat(savedMember -> - savedMember.getProfileImageFile().equals(imageFile))); } }