-
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: PresignedUrl을 사용하여 이미지 업로드 기능 구현 (#106)
* chore: endpoint 값 추가하여 config 수정 * feat: 미션 기록 이미지 Presigned URL 생성 * feat: 미션 기록 이미지 업로드 완료 처리 * chore: storage.yml에 endpoint 환경변수 추가 * test: Test 코드 추가 * style: spotless * refactor: storage 저장 디렉토리 변경 * feat: MissionRecord 도메인 uploadStatus validation 예외처리 * refactor: 해당 미션이 접속한 유저와 일치하지 않을때 예외 발생추가 * style: spotless * test: test코드 추가와 예외처리 변경 * style: spotless * remove: 사용하지 않는 에러 제거 * style: ImageController Swagger Tag 넘버링 * refactor: ImageController RequestMapping 제거 * refactor: Transactional 클래스레벨로 추출 * style: spotless * test: ImageController RequestMapping 제거 후 테스트코드 수정 * remove: .gitkeep 삭제 * refactor: 메소드 네이밍 변경 * refactor: MISSION_RECORD_USER_MISMATCH Service 레이어로 추출 * style: conflict 해결 * style: spotless
- Loading branch information
Showing
16 changed files
with
787 additions
and
15 deletions.
There are no files selected for viewing
36 changes: 36 additions & 0 deletions
36
src/main/java/com/depromeet/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,36 @@ | ||
package com.depromeet.domain.image.api; | ||
|
||
import com.depromeet.domain.image.application.ImageService; | ||
import com.depromeet.domain.image.dto.request.MissionRecordImageCreateRequest; | ||
import com.depromeet.domain.image.dto.request.MissionRecordImageUploadCompleteRequest; | ||
import com.depromeet.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.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
@Tag(name = "4. [이미지]", description = "이미지 관련 API입니다.") | ||
@RestController | ||
@RequiredArgsConstructor | ||
public class ImageController { | ||
private final ImageService imageService; | ||
|
||
@Operation( | ||
summary = "미션 기록 이미지 Presigned URL 생성", | ||
description = "미션 기록 이미지 Presigned URL를 생성합니다.") | ||
@PostMapping("/records/upload-url") | ||
public PresignedUrlResponse missionRecordPresignedUrlCreate( | ||
@Valid @RequestBody MissionRecordImageCreateRequest request) { | ||
return imageService.createMissionRecordPresignedUrl(request); | ||
} | ||
|
||
@Operation(summary = "미션 기록 이미지 업로드 완료처리", description = "미션 기록 이미지 업로드 완료 시 호출하시면 됩니다.") | ||
@PostMapping("/records/upload-complete") | ||
public void missionRecordUploaded( | ||
@Valid @RequestBody MissionRecordImageUploadCompleteRequest request) { | ||
imageService.uploadCompleteMissionRecord(request); | ||
} | ||
} |
129 changes: 129 additions & 0 deletions
129
src/main/java/com/depromeet/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,129 @@ | ||
package com.depromeet.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.domain.image.domain.ImageFileExtension; | ||
import com.depromeet.domain.image.domain.ImageType; | ||
import com.depromeet.domain.image.dto.request.MissionRecordImageCreateRequest; | ||
import com.depromeet.domain.image.dto.request.MissionRecordImageUploadCompleteRequest; | ||
import com.depromeet.domain.image.dto.response.PresignedUrlResponse; | ||
import com.depromeet.domain.member.domain.Member; | ||
import com.depromeet.domain.mission.domain.Mission; | ||
import com.depromeet.domain.missionRecord.dao.MissionRecordRepository; | ||
import com.depromeet.domain.missionRecord.domain.MissionRecord; | ||
import com.depromeet.global.error.exception.CustomException; | ||
import com.depromeet.global.error.exception.ErrorCode; | ||
import com.depromeet.global.util.MemberUtil; | ||
import com.depromeet.global.util.SpringEnvironmentUtil; | ||
import com.depromeet.infra.config.storage.StorageProperties; | ||
import java.util.Date; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
@Transactional | ||
public class ImageService { | ||
private final MemberUtil memberUtil; | ||
private final SpringEnvironmentUtil springEnvironmentUtil; | ||
private final StorageProperties storageProperties; | ||
private final AmazonS3 amazonS3; | ||
private final MissionRecordRepository missionRecordRepository; | ||
|
||
public PresignedUrlResponse createMissionRecordPresignedUrl( | ||
MissionRecordImageCreateRequest request) { | ||
final Member currentMember = memberUtil.getCurrentMember(); | ||
|
||
MissionRecord missionRecord = findMissionRecordById(request.missionRecordId()); | ||
|
||
Mission mission = missionRecord.getMission(); | ||
validateMissionRecordUserMismatch(mission, currentMember); | ||
|
||
String fileName = | ||
createFileName( | ||
ImageType.MISSION_RECORD, | ||
request.missionRecordId(), | ||
request.imageFileExtension()); | ||
GeneratePresignedUrlRequest generatePresignedUrlRequest = | ||
createGeneratePreSignedUrlRequest( | ||
storageProperties.bucket(), | ||
fileName, | ||
request.imageFileExtension().getUploadExtension()); | ||
|
||
String presignedUrl = amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString(); | ||
|
||
missionRecord.updateUploadStatusPending(); | ||
return PresignedUrlResponse.from(presignedUrl); | ||
} | ||
|
||
private MissionRecord findMissionRecordById(Long request) { | ||
return missionRecordRepository | ||
.findById(request) | ||
.orElseThrow(() -> new CustomException(ErrorCode.MISSION_RECORD_NOT_FOUND)); | ||
} | ||
|
||
public void uploadCompleteMissionRecord(MissionRecordImageUploadCompleteRequest request) { | ||
final Member currentMember = memberUtil.getCurrentMember(); | ||
MissionRecord missionRecord = findMissionRecordById(request.missionRecordId()); | ||
|
||
Mission mission = missionRecord.getMission(); | ||
validateMissionRecordUserMismatch(mission, currentMember); | ||
|
||
String imageUrl = | ||
storageProperties.endpoint() | ||
+ "/" | ||
+ storageProperties.bucket() | ||
+ "/" | ||
+ springEnvironmentUtil.getCurrentProfile() | ||
+ "/" | ||
+ ImageType.MISSION_RECORD.getValue() | ||
+ "/" | ||
+ request.missionRecordId() | ||
+ "/image." | ||
+ request.imageFileExtension().getUploadExtension(); | ||
missionRecord.updateUploadStatusComplete(request.remark(), imageUrl); | ||
} | ||
|
||
private String createFileName( | ||
ImageType imageType, Long targetId, ImageFileExtension imageFileExtension) { | ||
return springEnvironmentUtil.getCurrentProfile() | ||
+ "/" | ||
+ imageType.getValue() | ||
+ "/" | ||
+ targetId | ||
+ "/image." | ||
+ imageFileExtension.getUploadExtension(); | ||
} | ||
|
||
private GeneratePresignedUrlRequest createGeneratePreSignedUrlRequest( | ||
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() { | ||
Date expiration = new Date(); | ||
var expTimeMillis = expiration.getTime(); | ||
expTimeMillis += 1000 * 60 * 30; | ||
expiration.setTime(expTimeMillis); | ||
return expiration; | ||
} | ||
|
||
private void validateMissionRecordUserMismatch(Mission mission, Member member) { | ||
if (!mission.getMember().getId().equals(member.getId())) { | ||
throw new CustomException(ErrorCode.MISSION_RECORD_USER_MISMATCH); | ||
} | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
src/main/java/com/depromeet/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,15 @@ | ||
package com.depromeet.domain.image.domain; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
public enum ImageFileExtension { | ||
JPEG("jpeg"), | ||
JPG("jpg"), | ||
PNG("png"), | ||
; | ||
|
||
private final String uploadExtension; | ||
} |
14 changes: 14 additions & 0 deletions
14
src/main/java/com/depromeet/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,14 @@ | ||
package com.depromeet.domain.image.domain; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
public enum ImageType { | ||
MISSION_RECORD("mission_record"), | ||
MEMBER_PROFILE("member_profile"), | ||
MEMBER_BACKGROUND("member_background"), | ||
; | ||
private final String value; | ||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/com/depromeet/domain/image/dto/request/MissionRecordImageCreateRequest.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.domain.image.dto.request; | ||
|
||
import com.depromeet.domain.image.domain.ImageFileExtension; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import jakarta.validation.constraints.NotNull; | ||
|
||
public record MissionRecordImageCreateRequest( | ||
@NotNull(message = "미션 기록 ID는 비워둘 수 없습니다.") | ||
@Schema(description = "미션 기록 ID", defaultValue = "1") | ||
Long missionRecordId, | ||
@NotNull(message = "이미지 파일의 확장자는 비워둘 수 없습니다.") | ||
@Schema(description = "이미지 파일의 확장자", defaultValue = "JPEG") | ||
ImageFileExtension imageFileExtension) {} |
15 changes: 15 additions & 0 deletions
15
.../java/com/depromeet/domain/image/dto/request/MissionRecordImageUploadCompleteRequest.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,15 @@ | ||
package com.depromeet.domain.image.dto.request; | ||
|
||
import com.depromeet.domain.image.domain.ImageFileExtension; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import jakarta.validation.constraints.NotNull; | ||
import jakarta.validation.constraints.Size; | ||
|
||
public record MissionRecordImageUploadCompleteRequest( | ||
@NotNull(message = "미션 기록 ID는 비워둘 수 없습니다.") | ||
@Schema(description = "미션 기록 ID", defaultValue = "1") | ||
Long missionRecordId, | ||
@NotNull(message = "이미지 파일의 확장자는 비워둘 수 없습니다.") | ||
@Schema(description = "이미지 파일의 확장자", defaultValue = "JPEG") | ||
ImageFileExtension imageFileExtension, | ||
@Size(max = 200, message = "미션 일지는 20자 이하까지만 입력 가능합니다.") String remark) {} |
9 changes: 9 additions & 0 deletions
9
src/main/java/com/depromeet/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.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
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.