-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: Image 도메인 및 DTO 정의 * feat: aws 의존성 추가 및 Image Controller, Service 추가 * fix: image 로직 임시 커밋 * [WALWAL-148] Dev, Prod 환경 분리 (#56) * feat: Environment 환경 분리 * test: dev profile swagger permitAll * fix: fixtureMokey 수정 * refactor: dev 환경 배포 테스트 * refactor: dev 환경 배포 테스트 * fix: fixtureMokey 수정 * test: dev profile swagger permitAll * refactor: dev 환경 배포 테스트 * fix: push branch develop으로 변경 * fix: profile 예외 메세지 * chore: s3 Config 추가 * feat: Member 이미지 업로드 기능 * fix: 이미지 업로드 로직 수정 * refactor: @dbscks97 피드백 반영 * refactor: @kwanok 피드백 반영 * fix: MissionCreateRequest test 코드
- Loading branch information
Showing
18 changed files
with
385 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
src/main/java/com/depromeet/stonebed/domain/image/api/ImageController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package com.depromeet.stonebed.domain.image.api; | ||
|
||
import com.depromeet.stonebed.domain.image.application.ImageService; | ||
import com.depromeet.stonebed.domain.image.dto.request.MemberProfileImageCreateRequest; | ||
import com.depromeet.stonebed.domain.image.dto.request.MemberProfileImageUploadCompleteRequest; | ||
import com.depromeet.stonebed.domain.image.dto.response.PresignedUrlResponse; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import jakarta.validation.Valid; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.ResponseEntity; | ||
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; | ||
|
||
@Tag(name = "3. [이미지]", description = "이미지 관련 API입니다.") | ||
@RestController | ||
@RequestMapping("/images") | ||
@RequiredArgsConstructor | ||
public class ImageController { | ||
|
||
private final ImageService imageService; | ||
|
||
@Operation( | ||
summary = "회원 프로필 이미지 Presigned URL 생성", | ||
description = "회원 프로필 이미지 Presigned URL을 생성합니다.") | ||
@PostMapping("/members/me/upload-url") | ||
public PresignedUrlResponse memberProfilePresignedUrlCreate( | ||
@Valid @RequestBody MemberProfileImageCreateRequest request) { | ||
return imageService.createMemberProfilePresignedUrl(request); | ||
} | ||
|
||
@Operation(summary = "회원 프로필 이미지 업로드 완료", description = "회원 프로필 이미지 업로드 완료 업로드 상태를 변경합니다.") | ||
@PostMapping("/members/me/upload-complete") | ||
public ResponseEntity<Void> memberProfileUploadedV2( | ||
@Valid @RequestBody MemberProfileImageUploadCompleteRequest request) { | ||
imageService.uploadCompleteMemberProfile(request); | ||
return ResponseEntity.ok().build(); | ||
} | ||
} |
155 changes: 155 additions & 0 deletions
155
src/main/java/com/depromeet/stonebed/domain/image/application/ImageService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package com.depromeet.stonebed.domain.image.application; | ||
|
||
import com.amazonaws.HttpMethod; | ||
import com.amazonaws.services.s3.AmazonS3; | ||
import com.amazonaws.services.s3.Headers; | ||
import com.amazonaws.services.s3.model.CannedAccessControlList; | ||
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; | ||
import com.depromeet.stonebed.domain.image.dao.ImageRepository; | ||
import com.depromeet.stonebed.domain.image.domain.Image; | ||
import com.depromeet.stonebed.domain.image.domain.ImageFileExtension; | ||
import com.depromeet.stonebed.domain.image.domain.ImageType; | ||
import com.depromeet.stonebed.domain.image.dto.request.MemberProfileImageCreateRequest; | ||
import com.depromeet.stonebed.domain.image.dto.request.MemberProfileImageUploadCompleteRequest; | ||
import com.depromeet.stonebed.domain.image.dto.response.PresignedUrlResponse; | ||
import com.depromeet.stonebed.domain.member.domain.Member; | ||
import com.depromeet.stonebed.domain.member.domain.Profile; | ||
import com.depromeet.stonebed.global.common.constants.UrlConstants; | ||
import com.depromeet.stonebed.global.error.ErrorCode; | ||
import com.depromeet.stonebed.global.error.exception.CustomException; | ||
import com.depromeet.stonebed.global.util.MemberUtil; | ||
import com.depromeet.stonebed.global.util.SpringEnvironmentUtil; | ||
import com.depromeet.stonebed.infra.properties.S3Properties; | ||
import java.net.URL; | ||
import java.util.Date; | ||
import java.util.UUID; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
@Transactional | ||
public class ImageService { | ||
|
||
private final ImageRepository imageRepository; | ||
private final AmazonS3 amazonS3; | ||
private final MemberUtil memberUtil; | ||
private final S3Properties s3Properties; | ||
private final SpringEnvironmentUtil springEnvironmentUtil; | ||
|
||
public PresignedUrlResponse createMemberProfilePresignedUrl( | ||
MemberProfileImageCreateRequest request) { | ||
final Member currentMember = memberUtil.getCurrentMember(); | ||
validateImageFileExtension(request.imageFileExtension()); | ||
String imageKey = generateUUID(); | ||
String fileName = | ||
createFileName( | ||
ImageType.MEMBER_PROFILE, | ||
currentMember.getId(), | ||
imageKey, | ||
request.imageFileExtension()); | ||
GeneratePresignedUrlRequest presignedUrlRequest = | ||
createPreSignedUrlRequest( | ||
s3Properties.bucket(), | ||
fileName, | ||
request.imageFileExtension().getUploadExtension()); | ||
|
||
URL presignedUrl = amazonS3.generatePresignedUrl(presignedUrlRequest); | ||
imageRepository.save( | ||
Image.createImage( | ||
ImageType.MEMBER_PROFILE, | ||
currentMember.getId(), | ||
imageKey, | ||
request.imageFileExtension())); | ||
return PresignedUrlResponse.from(presignedUrl.toString()); | ||
} | ||
|
||
public void uploadCompleteMemberProfile(MemberProfileImageUploadCompleteRequest request) { | ||
final Member currentMember = memberUtil.getCurrentMember(); | ||
validateImageFileExtension(request.imageFileExtension()); | ||
|
||
Image image = | ||
findImage( | ||
ImageType.MEMBER_PROFILE, | ||
currentMember.getId(), | ||
request.imageFileExtension()); | ||
String imageUrl = | ||
createReadImageUrl( | ||
ImageType.MEMBER_PROFILE, | ||
currentMember.getId(), | ||
image.getImageKey(), | ||
request.imageFileExtension()); | ||
currentMember.updateProfile(Profile.createProfile(request.nickname(), imageUrl)); | ||
} | ||
|
||
private void validateImageFileExtension(ImageFileExtension imageFileExtension) { | ||
if (imageFileExtension == null) { | ||
throw new CustomException(ErrorCode.IMAGE_FILE_EXTENSION_NOT_FOUND); | ||
} | ||
try { | ||
ImageFileExtension.of(imageFileExtension.getUploadExtension()); | ||
} catch (IllegalArgumentException e) { | ||
throw new CustomException(ErrorCode.INVALID_IMAGE_FILE_EXTENSION); | ||
} | ||
} | ||
|
||
private Image findImage( | ||
ImageType imageType, Long targetId, ImageFileExtension imageFileExtension) { | ||
return imageRepository | ||
.findTopByImageTypeAndTargetIdAndImageFileExtensionOrderByIdDesc( | ||
imageType, targetId, imageFileExtension) | ||
.orElseThrow(() -> new CustomException(ErrorCode.IMAGE_KEY_NOT_FOUND)); | ||
} | ||
|
||
private String generateUUID() { | ||
return UUID.randomUUID().toString(); | ||
} | ||
|
||
private String createFileName( | ||
ImageType imageType, | ||
Long targetId, | ||
String imageKey, | ||
ImageFileExtension imageFileExtension) { | ||
return String.format( | ||
"%s/%s/%d/%s.%s", | ||
springEnvironmentUtil.getCurrentProfile(), | ||
imageType.getValue(), | ||
targetId, | ||
imageKey, | ||
imageFileExtension.getUploadExtension()); | ||
} | ||
|
||
private String createReadImageUrl( | ||
ImageType imageType, | ||
Long targetId, | ||
String imageKey, | ||
ImageFileExtension imageFileExtension) { | ||
return String.format( | ||
"%s/%s/%s/%d/%s.%s", | ||
UrlConstants.IMAGE_DOMAIN_URL.getValue(), | ||
springEnvironmentUtil.getCurrentProfile(), | ||
imageType.getValue(), | ||
targetId, | ||
imageKey, | ||
imageFileExtension.getUploadExtension()); | ||
} | ||
|
||
private GeneratePresignedUrlRequest createPreSignedUrlRequest( | ||
String bucket, String fileName, String fileExtension) { | ||
GeneratePresignedUrlRequest generatePresignedUrlRequest = | ||
new GeneratePresignedUrlRequest(bucket, fileName, HttpMethod.PUT) | ||
.withKey(fileName) | ||
.withContentType("image/" + fileExtension) | ||
.withExpiration(getPreSignedUrlExpiration()); | ||
|
||
generatePresignedUrlRequest.addRequestParameter( | ||
Headers.S3_CANNED_ACL, CannedAccessControlList.PublicRead.toString()); | ||
|
||
return generatePresignedUrlRequest; | ||
} | ||
|
||
private Date getPreSignedUrlExpiration() { | ||
return new Date(System.currentTimeMillis() + 1000 * 60 * 30); | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
src/main/java/com/depromeet/stonebed/domain/image/dao/ImageRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.depromeet.stonebed.domain.image.dao; | ||
|
||
import com.depromeet.stonebed.domain.image.domain.Image; | ||
import com.depromeet.stonebed.domain.image.domain.ImageFileExtension; | ||
import com.depromeet.stonebed.domain.image.domain.ImageType; | ||
import java.util.Optional; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
public interface ImageRepository extends JpaRepository<Image, Long> { | ||
Optional<Image> findTopByImageTypeAndTargetIdAndImageFileExtensionOrderByIdDesc( | ||
ImageType imageType, Long targetId, ImageFileExtension imageFileExtension); | ||
} |
63 changes: 63 additions & 0 deletions
63
src/main/java/com/depromeet/stonebed/domain/image/domain/Image.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package com.depromeet.stonebed.domain.image.domain; | ||
|
||
import com.depromeet.stonebed.domain.common.BaseTimeEntity; | ||
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 lombok.AccessLevel; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Getter | ||
@Entity | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
public class Image extends BaseTimeEntity { | ||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
@Column(name = "image_id") | ||
private Long id; | ||
|
||
@Enumerated(EnumType.STRING) | ||
private ImageType imageType; | ||
|
||
// targetId 예시: missionRecord와 같이 이미지를 가지는 대상의 id | ||
private Long targetId; | ||
|
||
@Column(length = 36) | ||
private String imageKey; | ||
|
||
@Enumerated(EnumType.STRING) | ||
private ImageFileExtension imageFileExtension; | ||
|
||
@Builder(access = AccessLevel.PRIVATE) | ||
private Image( | ||
Long id, | ||
ImageType imageType, | ||
Long targetId, | ||
String imageKey, | ||
ImageFileExtension imageFileExtension) { | ||
this.id = id; | ||
this.imageType = imageType; | ||
this.targetId = targetId; | ||
this.imageKey = imageKey; | ||
this.imageFileExtension = imageFileExtension; | ||
} | ||
|
||
public static Image createImage( | ||
ImageType imageType, | ||
Long targetId, | ||
String imageKey, | ||
ImageFileExtension imageFileExtension) { | ||
return Image.builder() | ||
.imageType(imageType) | ||
.targetId(targetId) | ||
.imageKey(imageKey) | ||
.imageFileExtension(imageFileExtension) | ||
.build(); | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/com/depromeet/stonebed/domain/image/domain/ImageFileExtension.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.depromeet.stonebed.domain.image.domain; | ||
|
||
import com.depromeet.stonebed.global.error.ErrorCode; | ||
import com.depromeet.stonebed.global.error.exception.CustomException; | ||
import java.util.Arrays; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
public enum ImageFileExtension { | ||
JPEG("jpeg"), | ||
JPG("jpg"), | ||
PNG("png"), | ||
; | ||
|
||
private final String uploadExtension; | ||
|
||
public static ImageFileExtension of(String uploadExtension) { | ||
return Arrays.stream(values()) | ||
.filter( | ||
imageFileExtension -> | ||
imageFileExtension.uploadExtension.equals(uploadExtension)) | ||
.findFirst() | ||
.orElseThrow(() -> new CustomException(ErrorCode.IMAGE_FILE_EXTENSION_NOT_FOUND)); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/com/depromeet/stonebed/domain/image/domain/ImageType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.depromeet.stonebed.domain.image.domain; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
public enum ImageType { | ||
MISSION_RECORD("mission_record"), | ||
MEMBER_PROFILE("member_profile"), | ||
; | ||
private final String value; | ||
} |
10 changes: 10 additions & 0 deletions
10
...java/com/depromeet/stonebed/domain/image/dto/request/MemberProfileImageCreateRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.depromeet.stonebed.domain.image.dto.request; | ||
|
||
import com.depromeet.stonebed.domain.image.domain.ImageFileExtension; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import jakarta.validation.constraints.NotNull; | ||
|
||
public record MemberProfileImageCreateRequest( | ||
@NotNull(message = "이미지 파일의 확장자는 비워둘 수 없습니다.") | ||
@Schema(description = "이미지 파일의 확장자", defaultValue = "JPEG") | ||
ImageFileExtension imageFileExtension) {} |
9 changes: 9 additions & 0 deletions
9
.../depromeet/stonebed/domain/image/dto/request/MemberProfileImageUploadCompleteRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.depromeet.stonebed.domain.image.dto.request; | ||
|
||
import com.depromeet.stonebed.domain.image.domain.ImageFileExtension; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
|
||
public record MemberProfileImageUploadCompleteRequest( | ||
@Schema(description = "이미지 파일의 확장자", defaultValue = "JPEG") | ||
ImageFileExtension imageFileExtension, | ||
@Schema(description = "닉네임", defaultValue = "수정닉네임") String nickname) {} |
9 changes: 9 additions & 0 deletions
9
src/main/java/com/depromeet/stonebed/domain/image/dto/response/PresignedUrlResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.depromeet.stonebed.domain.image.dto.response; | ||
|
||
import io.swagger.v3.oas.annotations.media.Schema; | ||
|
||
public record PresignedUrlResponse(@Schema(description = "Presigned URL") String presignedUrl) { | ||
public static PresignedUrlResponse from(String presignedUrl) { | ||
return new PresignedUrlResponse(presignedUrl); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.