diff --git a/moonshot-api/src/main/java/org/moonshot/user/controller/ImageController.java b/moonshot-api/src/main/java/org/moonshot/user/controller/ImageController.java index 729e1b48..b672966e 100644 --- a/moonshot-api/src/main/java/org/moonshot/user/controller/ImageController.java +++ b/moonshot-api/src/main/java/org/moonshot/user/controller/ImageController.java @@ -1,11 +1,14 @@ package org.moonshot.user.controller; import lombok.RequiredArgsConstructor; +import org.moonshot.model.Logging; import org.moonshot.response.MoonshotResponse; import org.moonshot.response.SuccessType; import org.moonshot.s3.S3Service; +import org.moonshot.s3.dto.request.GetPresignedUrlRequestDto; import org.moonshot.s3.dto.request.NotifyImageSaveSuccessRequestDto; import org.moonshot.s3.dto.response.PresignedUrlVO; +import org.moonshot.user.model.LoginUser; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -15,27 +18,23 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/v1") +@RequestMapping("/v1/image") @RequiredArgsConstructor public class ImageController { private final S3Service s3Service; - //TODO - // 추후 로그인 유저를 확인하여 해당 유저에 대한 데이터로 getUploadPreSignedUrl로 username을 넘기는 로직으로 변경해야 함. - @GetMapping("/image") - public ResponseEntity> getPresignedUrl() { - return ResponseEntity.status(HttpStatus.OK).body( - MoonshotResponse.success( - SuccessType.GET_PRESIGNED_URL_SUCCESS, s3Service.getUploadPreSignedUrl("test", "SMC"))); + @GetMapping + @Logging(item = "Image", action = "Get") + public ResponseEntity> getPresignedUrl(@LoginUser Long userId, @RequestBody GetPresignedUrlRequestDto request) { + return ResponseEntity.status(HttpStatus.OK) + .body(MoonshotResponse.success(SuccessType.GET_PRESIGNED_URL_SUCCESS, s3Service.getUploadPreSignedUrl(request, userId))); } - //TODO - // 해당 API도 username을 @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : username") - // 등을 이용하여 Annotation화 하여 바로 username을 넘길 수 있도록 변경해야 함. - @PostMapping("/image") - public ResponseEntity> notifyImageSaveSuccess(@RequestBody final NotifyImageSaveSuccessRequestDto request) { - s3Service.notifyImageSaveSuccess(request); + @PostMapping + @Logging(item = "Image", action = "Post") + public ResponseEntity> notifyImageSaveSuccess(@LoginUser Long userId, @RequestBody final NotifyImageSaveSuccessRequestDto request) { + s3Service.notifyImageSaveSuccess(userId, request); return ResponseEntity.status(HttpStatus.CREATED).body(MoonshotResponse.success(SuccessType.POST_NOTIFY_IMAGE_SAVE_SUCCESS)); } diff --git a/moonshot-api/src/main/java/org/moonshot/user/dto/response/UserInfoResponse.java b/moonshot-api/src/main/java/org/moonshot/user/dto/response/UserInfoResponse.java index 128cd931..f2a509e0 100644 --- a/moonshot-api/src/main/java/org/moonshot/user/dto/response/UserInfoResponse.java +++ b/moonshot-api/src/main/java/org/moonshot/user/dto/response/UserInfoResponse.java @@ -12,7 +12,7 @@ public record UserInfoResponse( public static UserInfoResponse of(User user) { return new UserInfoResponse( user.getSocialPlatform().getValue(), - user.getProfileImage(), + user.getImageUrl(), user.getNickname(), user.getDescription()); } diff --git a/moonshot-api/src/main/java/org/moonshot/user/service/ImageEventListener.java b/moonshot-api/src/main/java/org/moonshot/user/service/ImageEventListener.java new file mode 100644 index 00000000..a29bcb54 --- /dev/null +++ b/moonshot-api/src/main/java/org/moonshot/user/service/ImageEventListener.java @@ -0,0 +1,20 @@ +package org.moonshot.user.service; + +import lombok.RequiredArgsConstructor; +import org.moonshot.s3.ImageEvent; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class ImageEventListener { + + private final UserService userService; + + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + public void handleImageEvent(ImageEvent imageEvent) { + userService.updateUserProfileImage(imageEvent.userId(), imageEvent.imageUrl()); + } + +} diff --git a/moonshot-api/src/main/java/org/moonshot/user/service/UserService.java b/moonshot-api/src/main/java/org/moonshot/user/service/UserService.java index c0b28c98..6aa18eda 100644 --- a/moonshot-api/src/main/java/org/moonshot/user/service/UserService.java +++ b/moonshot-api/src/main/java/org/moonshot/user/service/UserService.java @@ -92,7 +92,7 @@ public SocialLoginResponse googleLogin(final SocialLoginRequest request) { .socialId(userResponse.sub()) .socialPlatform(request.socialPlatform()) .name(userResponse.name()) - .profileImage(userResponse.picture()) + .imageUrl(userResponse.picture()) .email(userResponse.email()) .build()); user = newUser; @@ -121,7 +121,7 @@ public SocialLoginResponse kakaoLogin(final SocialLoginRequest request) { .socialId(userResponse.id()) .socialPlatform(request.socialPlatform()) .name(userResponse.kakaoAccount().profile().nickname()) - .profileImage(userResponse.kakaoAccount().profile().profileImageUrl()) + .imageUrl(userResponse.kakaoAccount().profile().profileImageUrl()) .email(null) .build()); user = newUser; @@ -176,6 +176,11 @@ public UserInfoResponse getMyProfile(final Long userId) { return UserInfoResponse.of(user); } + public void updateUserProfileImage(final Long userId, final String imageUrl) { + User user = userRepository.findById(userId).orElseThrow(() -> new NotFoundException(NOT_FOUND_USER)); + user.modifyProfileImage(imageUrl); + } + @Transactional(propagation = Propagation.REQUIRES_NEW) public void publishSignUpEvent(final User user) { eventPublisher.publishEvent(SignUpEvent.of( @@ -183,7 +188,7 @@ public void publishSignUpEvent(final User user) { user.getEmail() == null ? "" : user.getEmail(), user.getSocialPlatform().toString(), LocalDateTime.now(), - user.getProfileImage() + user.getImageUrl() )); } diff --git a/moonshot-api/src/main/resources/application.yml b/moonshot-api/src/main/resources/application.yml index 1ffc5891..bfcdf025 100644 --- a/moonshot-api/src/main/resources/application.yml +++ b/moonshot-api/src/main/resources/application.yml @@ -28,7 +28,7 @@ spring: flyway: baseline-on-migrate: false - baseline-version: 1 + baseline-version: 2 enabled: false google: diff --git a/moonshot-api/src/main/resources/db/migration/V3__DDL.sql b/moonshot-api/src/main/resources/db/migration/V3__DDL.sql new file mode 100644 index 00000000..ad13e30d --- /dev/null +++ b/moonshot-api/src/main/resources/db/migration/V3__DDL.sql @@ -0,0 +1 @@ +ALTER TABLE user CHANGE profile_image image_url varchar(255); \ No newline at end of file diff --git a/moonshot-domain/src/main/java/org/moonshot/keyresult/model/KRState.java b/moonshot-domain/src/main/java/org/moonshot/keyresult/model/KRState.java index f8c8357b..40660797 100644 --- a/moonshot-domain/src/main/java/org/moonshot/keyresult/model/KRState.java +++ b/moonshot-domain/src/main/java/org/moonshot/keyresult/model/KRState.java @@ -5,7 +5,7 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; -import org.moonshot.exception.MoonshotException; +import org.moonshot.exception.BadRequestException; import org.moonshot.response.ErrorType; @Getter @@ -31,7 +31,7 @@ public static KRState fromValue(String value) { return krState; } } - throw new MoonshotException(ErrorType.INVALID_TYPE); + throw new BadRequestException(ErrorType.INVALID_TYPE); } } diff --git a/moonshot-domain/src/main/java/org/moonshot/objective/model/Category.java b/moonshot-domain/src/main/java/org/moonshot/objective/model/Category.java index 822ce8a5..c477a49f 100644 --- a/moonshot-domain/src/main/java/org/moonshot/objective/model/Category.java +++ b/moonshot-domain/src/main/java/org/moonshot/objective/model/Category.java @@ -5,7 +5,7 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; -import org.moonshot.exception.MoonshotException; +import org.moonshot.exception.BadRequestException; import org.moonshot.response.ErrorType; @Getter @@ -33,7 +33,7 @@ public static Category fromValue(String value) { return category; } } - throw new MoonshotException(ErrorType.INVALID_TYPE); + throw new BadRequestException(ErrorType.INVALID_TYPE); } } diff --git a/moonshot-domain/src/main/java/org/moonshot/objective/model/Criteria.java b/moonshot-domain/src/main/java/org/moonshot/objective/model/Criteria.java index ac2d7740..6c04e1d3 100644 --- a/moonshot-domain/src/main/java/org/moonshot/objective/model/Criteria.java +++ b/moonshot-domain/src/main/java/org/moonshot/objective/model/Criteria.java @@ -5,7 +5,7 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; -import org.moonshot.exception.MoonshotException; +import org.moonshot.exception.BadRequestException; import org.moonshot.response.ErrorType; @Getter @@ -30,7 +30,7 @@ public static Criteria fromValue(String value) { return criteria; } } - throw new MoonshotException(ErrorType.INVALID_TYPE); + throw new BadRequestException(ErrorType.INVALID_TYPE); } } diff --git a/moonshot-domain/src/main/java/org/moonshot/user/model/User.java b/moonshot-domain/src/main/java/org/moonshot/user/model/User.java index 573eb434..f35516be 100644 --- a/moonshot-domain/src/main/java/org/moonshot/user/model/User.java +++ b/moonshot-domain/src/main/java/org/moonshot/user/model/User.java @@ -1,10 +1,18 @@ package org.moonshot.user.model; -import jakarta.persistence.*; -import lombok.*; - +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 java.time.LocalDateTime; -import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter @@ -32,7 +40,7 @@ public class User { private String name; @Column(nullable = false) - private String profileImage; + private String imageUrl; private String email; @@ -43,24 +51,24 @@ public class User { private LocalDateTime deleteAt; @Builder - private User(String socialId, SocialPlatform socialPlatform, String name, String profileImage, String email, + private User(String socialId, SocialPlatform socialPlatform, String name, String imageUrl, String email, String nickname, String description) { this.socialId = socialId; this.socialPlatform = socialPlatform; this.name = name; - this.profileImage = profileImage; + this.imageUrl = imageUrl; this.email = email; this.nickname = nickname; this.description = description; } @Builder(builderMethodName = "builderWithSignIn") - public static User of(String socialId, SocialPlatform socialPlatform, String name, String profileImage, String email) { + public static User of(String socialId, SocialPlatform socialPlatform, String name, String imageUrl, String email) { return builder() .socialId(socialId) .socialPlatform(socialPlatform) .name(name) - .profileImage(profileImage) + .imageUrl(imageUrl) .email(email) .build(); } @@ -69,9 +77,14 @@ public static User of(String socialId, SocialPlatform socialPlatform, String nam public void modifyDescription(String description) { this.description = description; } + public void modifyProfileImage(String imageUrl) { + this.imageUrl = imageUrl; + } + public void resetDeleteAt() { this.deleteAt = null; } + public void setDeleteAt(){ this.deleteAt = LocalDateTime.now().plusDays(USER_RETENTION_PERIOD); } diff --git a/moonshot-external/build.gradle b/moonshot-external/build.gradle index 9bec0460..a99c2d7d 100644 --- a/moonshot-external/build.gradle +++ b/moonshot-external/build.gradle @@ -8,8 +8,13 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' - // JWT + // Transaction Retry + implementation 'org.springframework.retry:spring-retry' + implementation 'org.springframework:spring-aspects' + + // JWT implementation group: "io.jsonwebtoken", name: "jjwt-api", version: "0.11.2" implementation group: "io.jsonwebtoken", name: "jjwt-impl", version: "0.11.2" implementation group: "io.jsonwebtoken", name: "jjwt-jackson", version: "0.11.2" diff --git a/moonshot-external/src/main/java/org/moonshot/s3/ImageEvent.java b/moonshot-external/src/main/java/org/moonshot/s3/ImageEvent.java new file mode 100644 index 00000000..92d77e8f --- /dev/null +++ b/moonshot-external/src/main/java/org/moonshot/s3/ImageEvent.java @@ -0,0 +1,9 @@ +package org.moonshot.s3; + +public record ImageEvent(Long userId, String imageUrl, ImageType imageType) { + + public static ImageEvent of(Long userId, String imageUrl, ImageType imageType) { + return new ImageEvent(userId, imageUrl, imageType); + } + +} diff --git a/moonshot-external/src/main/java/org/moonshot/s3/ImageType.java b/moonshot-external/src/main/java/org/moonshot/s3/ImageType.java new file mode 100644 index 00000000..62dcfa04 --- /dev/null +++ b/moonshot-external/src/main/java/org/moonshot/s3/ImageType.java @@ -0,0 +1,28 @@ +package org.moonshot.s3; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.moonshot.exception.BadRequestException; +import org.moonshot.response.ErrorType; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum ImageType { + + PROFILE("프로필"); + + private final String value; + + @JsonCreator + public static ImageType fromValue(String value) { + for (ImageType imageType : ImageType.values()) { + if (imageType.getValue().equals(value)) { + return imageType; + } + } + throw new BadRequestException(ErrorType.INVALID_TYPE); + } + +} diff --git a/moonshot-external/src/main/java/org/moonshot/s3/S3Service.java b/moonshot-external/src/main/java/org/moonshot/s3/S3Service.java index 00147aec..13a5a5c4 100644 --- a/moonshot-external/src/main/java/org/moonshot/s3/S3Service.java +++ b/moonshot-external/src/main/java/org/moonshot/s3/S3Service.java @@ -3,12 +3,17 @@ import java.text.SimpleDateFormat; import java.time.Duration; import java.util.Date; -import org.springframework.beans.factory.annotation.Value; import org.moonshot.config.AWSConfig; import org.moonshot.constants.AWSConstants; +import org.moonshot.s3.dto.request.GetPresignedUrlRequestDto; import org.moonshot.s3.dto.request.NotifyImageSaveSuccessRequestDto; import org.moonshot.s3.dto.response.PresignedUrlVO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; @@ -18,17 +23,19 @@ public class S3Service { private final String bucketName; private final AWSConfig awsConfig; + private final ApplicationEventPublisher eventPublisher; private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSSS"); public S3Service(@Value("${aws.s3-bucket-name}") final String bucketName, - AWSConfig awsConfig) { + AWSConfig awsConfig, ApplicationEventPublisher eventPublisher) { this.bucketName = bucketName; this.awsConfig = awsConfig; + this.eventPublisher = eventPublisher; } - public PresignedUrlVO getUploadPreSignedUrl(String prefix, String username) { - final String fileName = generateFileName(username); - final String key = prefix + "/" + fileName; + public PresignedUrlVO getUploadPreSignedUrl(final GetPresignedUrlRequestDto request, final Long userId) { + final String fileName = generateFileName(userId); + final String key = request.imageType().toString() + "/" + fileName; S3Presigner preSigner = awsConfig.getS3Presigner(); @@ -47,16 +54,23 @@ public PresignedUrlVO getUploadPreSignedUrl(String prefix, String username) { return PresignedUrlVO.of(key, url); } - public void notifyImageSaveSuccess(final NotifyImageSaveSuccessRequestDto request) { - //TODO - // 추후 User 엔티티 개발 후 - // username 아이디를 가진 User 정보에 profile image를 bucketName + key로 삽입하면 됨. - // 이는 UserService로 위임하여 데이터 처리하도록 하면 됨. - // 또한 기존의 S3에 저장되어 있던 이미지를 삭제해야 함. + @Transactional + @Retryable(maxAttempts = 3, backoff = @Backoff(2000)) + public void notifyImageSaveSuccess(final Long userId, final NotifyImageSaveSuccessRequestDto request) { + String imageUrl = getImageUrl(request.fileName()); + publishImageEvent(userId, imageUrl, request.imageType()); + } + + private String generateFileName(final Long userId) { + return userId + "-" + simpleDateFormat.format(new Date()); + } + + private String getImageUrl(final String fileName) { + return "https://" + bucketName + ".s3.ap-northeast-2.amazonaws.com/" + fileName; } - private String generateFileName(String username) { - return username + "-" + simpleDateFormat.format(new Date()); + private void publishImageEvent(final Long userId, final String imageUrl, final ImageType imageType) { + eventPublisher.publishEvent(ImageEvent.of(userId, imageUrl, imageType)); } } diff --git a/moonshot-external/src/main/java/org/moonshot/s3/dto/request/GetPresignedUrlRequestDto.java b/moonshot-external/src/main/java/org/moonshot/s3/dto/request/GetPresignedUrlRequestDto.java new file mode 100644 index 00000000..b426ef32 --- /dev/null +++ b/moonshot-external/src/main/java/org/moonshot/s3/dto/request/GetPresignedUrlRequestDto.java @@ -0,0 +1,6 @@ +package org.moonshot.s3.dto.request; + +import org.moonshot.s3.ImageType; + +public record GetPresignedUrlRequestDto(ImageType imageType) { +} diff --git a/moonshot-external/src/main/java/org/moonshot/s3/dto/request/NotifyImageSaveSuccessRequestDto.java b/moonshot-external/src/main/java/org/moonshot/s3/dto/request/NotifyImageSaveSuccessRequestDto.java index f7b787c1..086e0f5e 100644 --- a/moonshot-external/src/main/java/org/moonshot/s3/dto/request/NotifyImageSaveSuccessRequestDto.java +++ b/moonshot-external/src/main/java/org/moonshot/s3/dto/request/NotifyImageSaveSuccessRequestDto.java @@ -1,7 +1,9 @@ package org.moonshot.s3.dto.request; +import org.moonshot.s3.ImageType; + public record NotifyImageSaveSuccessRequestDto( - String key, - String username + String fileName, + ImageType imageType ) { }