Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'

implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.retrip.trip.application.in.request.image;

import com.retrip.trip.domain.vo.image.ImageFileExtension;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

@Schema(description = "이미지 Presigned Url 생성 Request")
public record PresignedUrlCreateRequest(

@Schema(description = "이미지 확장자", example = "JPG")
@NotNull
ImageFileExtension extension
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public record TripCreateResponse(
@Schema(description = "여행 카테고리")
String category,

@Schema(description = "여행 대표 이미지 url")
String imageUrl,

@Schema(description = "여행 일정 리스트")
List<ItineraryCreateResponse> itineraries
) {
Expand All @@ -56,6 +59,7 @@ public static TripCreateResponse of(Trip trip) {
trip.getTripParticipants().getMaxParticipants(),
trip.getHashTags().getValues().stream().map(TripHashTag::getName).toList(),
trip.getCategory().getViewName(),
trip.getImageUrl(),
trip.getItineraries() == null ? new ArrayList<>() :
trip.getItineraries().getValues().stream()
.map(i -> new ItineraryCreateResponse(i.getId(), i.getName(), i.getDate()))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.retrip.trip.application.in.response.image;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "이미지 Presigned Url 생성 Response")
public record PresignedUrlCreateResponse(

@Schema(description = "이미지 업로드용 presigned url")
String presignedUrl,

@Schema(description = "읽기용 이미지 url (presignedUrl로 이미지 업로드 했으면 해당 imageUrl로 이미지 다운로드 가능 하며 여행 생성시 해당 imageUrl 추가하면 됨")
String readImageUrl
) {
public static PresignedUrlCreateResponse of(String presignedUrl, String readImageUrl) {
return new PresignedUrlCreateResponse(presignedUrl, readImageUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.retrip.trip.application.in.service;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.retrip.trip.application.in.response.image.PresignedUrlCreateResponse;
import com.retrip.trip.application.in.usecase.ImageManageUseCase;
import com.retrip.trip.domain.vo.image.ImageFileExtension;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.UUID;

import static com.amazonaws.HttpMethod.PUT;
import static com.amazonaws.services.s3.Headers.S3_CANNED_ACL;
import static com.amazonaws.services.s3.model.CannedAccessControlList.PublicRead;
import static java.util.Locale.ENGLISH;

@RequiredArgsConstructor
@Transactional
@Service
public class ImageService implements ImageManageUseCase {

private static final String IMAGE_DOMAIN_URL = "https://retrip-media.s3.ap-northeast-2.amazonaws.com";
private static final String FORDER_NAME = "media";

@Value("${cloud.s3.bucket}")
private String bucket;
private final AmazonS3 amazonS3;

@Override
public PresignedUrlCreateResponse createImagePresignedUrl(UUID memberId, ImageFileExtension extension) {
String uuid = UUID.randomUUID().toString();
String presignedUrl = createPresignedUrl(uuid, extension);
String readImageUrl = createReadImageUrl(uuid, extension);
return PresignedUrlCreateResponse.of(presignedUrl, readImageUrl);
}

private String createPresignedUrl(String uuid, ImageFileExtension extension) {
String fileName = createFileName(uuid, extension);
GeneratePresignedUrlRequest request = createGeneratePresignedUrlRequest(fileName);
return amazonS3.generatePresignedUrl(request).toString();
}

private String createReadImageUrl(String uuid, ImageFileExtension extension) {
return IMAGE_DOMAIN_URL
+ "/" + createFileName(uuid, extension);
}

private String createFileName(String uuid, ImageFileExtension extension) {
return FORDER_NAME
+ "/" + uuid
+ "." + extension.name().toLowerCase(ENGLISH);
}

private GeneratePresignedUrlRequest createGeneratePresignedUrlRequest(String fileName) {
var request = new GeneratePresignedUrlRequest(bucket, fileName, PUT)
.withExpiration(createPresignedUrlExpiration());
request.addRequestParameter(S3_CANNED_ACL, PublicRead.toString());
return request;
}

private Date createPresignedUrlExpiration() {
Date expiration = new Date();
var expTimeMillis = expiration.getTime();
expTimeMillis += 1000 * 60 * 5;
expiration.setTime(expTimeMillis);
return expiration;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.retrip.trip.application.in.usecase;

import com.retrip.trip.application.in.response.image.PresignedUrlCreateResponse;
import com.retrip.trip.domain.vo.image.ImageFileExtension;

import java.util.UUID;

public interface ImageManageUseCase {
PresignedUrlCreateResponse createImagePresignedUrl(UUID uuid, ImageFileExtension extension);
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ public enum ErrorCode {
ITINERARY_DATE_MISMATCH(BAD_REQUEST, "Trip-040", "일정과 상세 일정 일자가 다릅니다."),
ITINERARY_TIME_DUPLICATED(BAD_REQUEST, "Trip-041", "해당 시간에는 이미 상세 일정이 있습니다."),
TRIP_PASSWORD_MISMATCH(BAD_REQUEST, "Trip-042", "여행 비밀번호가 일치하지 않습니다."),
VOTE_MODIFY_FORBIDDEN(FORBIDDEN, "Trip-043", "투표를 만든 사람이 아니면 수정, 종료, 삭제 할 수 없습니다.")
VOTE_MODIFY_FORBIDDEN(BAD_REQUEST, "Trip-043", "투표를 만든 사람이 아니면 수정, 종료, 삭제 할 수 없습니다."),
EXTENSION_NOT_FOUND(BAD_REQUEST, "Trip-044", "지원하지 않는 이미지 확장자입니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.retrip.trip.domain.vo.image;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.retrip.trip.domain.exception.common.BusinessException;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Arrays;

import static com.retrip.trip.domain.exception.common.ErrorCode.EXTENSION_NOT_FOUND;

@Getter
@AllArgsConstructor
public enum ImageFileExtension {

JPEG,
JPG,
PNG,
;

@JsonCreator
public static ImageFileExtension ofName(String value) {
return Arrays.stream(ImageFileExtension.values())
.filter(extensionType -> extensionType.name().equalsIgnoreCase(value))
.findFirst()
.orElseThrow(() -> new BusinessException(EXTENSION_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
pathLowercase.contains("actuator") ||
pathLowercase.contains("robots.txt") ||
pathLowercase.contains("status-check") ||
pathLowercase.contains("trips") ||
pathLowercase.contains("images") ||
pathLowercase.contains("/h2-console")) {
filterChain.doFilter(request, response);
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ public boolean supportsParameter(MethodParameter parameter) {
@Override
public UserContext resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
UserContext userContext = (UserContext) webRequest.getAttribute(
"userContext",
RequestAttributes.SCOPE_REQUEST
);
// UserContext userContext = (UserContext) webRequest.getAttribute(
// "userContext",
// RequestAttributes.SCOPE_REQUEST
// );

UserContext userContext = UserContext.mockOf();

if (userContext == null) {
throw new IllegalStateException("UserContext not found in request");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.retrip.trip.infra.adapter.in.presentation.rest;

import com.retrip.trip.application.in.request.*;
import com.retrip.trip.application.in.request.context.UserContext;
import com.retrip.trip.application.in.request.context.WithUserContext;
import com.retrip.trip.application.in.request.image.PresignedUrlCreateRequest;
import com.retrip.trip.application.in.response.*;
import com.retrip.trip.application.in.response.image.PresignedUrlCreateResponse;
import com.retrip.trip.application.in.usecase.*;
import com.retrip.trip.domain.exception.annotation.ApiErrorCodeExample;
import com.retrip.trip.domain.exception.annotation.ApiErrorCodeExamples;
import com.retrip.trip.domain.vo.TripCategory;
import com.retrip.trip.domain.vo.TripStatus;
import com.retrip.trip.infra.adapter.in.presentation.rest.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import static com.retrip.trip.domain.exception.common.ErrorCode.*;

@Tag(name = "Image", description = "이미지 관련 API")
@RequiredArgsConstructor
@RequestMapping("/images")
@RestController
public class ImageController {

private final ImageManageUseCase imageManageUseCase;

@ApiErrorCodeExamples({TRIP_DAY_MUST_BE_POSITIVE, INVALID_MAX_PARTICIPANTS_VALUE, INVALID_HASHTAG_LENGTH, PRIVATE_TRIP_PASSWORD_REQUIRED, TRIP_PASSWORD_INVALID})
@PostMapping("/presigned-url")
public ApiResponse<PresignedUrlCreateResponse> createImagePresignedUrl(
@WithUserContext UserContext userContext,
@RequestBody PresignedUrlCreateRequest request) {
PresignedUrlCreateResponse response = imageManageUseCase.createImagePresignedUrl(userContext.memberId(), request.extension());
return ApiResponse.created(response);
}

}
39 changes: 39 additions & 0 deletions src/main/java/com/retrip/trip/infra/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.retrip.trip.infra.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.auth.InstanceProfileCredentialsProvider;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
@RequiredArgsConstructor
public class S3Config {

@Value("${cloud.aws.region:ap-northeast-2}")
private String region;

@Bean
@Primary
public AmazonS3 amazonS3() {
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(DefaultAWSCredentialsProviderChain.getInstance())
.build();
}
//
// @Bean
// public AmazonS3Client amazonS3Client() {
// InstanceProfileCredentialsProvider provider= new InstanceProfileCredentialsProvider(true);
// return (AmazonS3Client) AmazonS3ClientBuilder.standard()
// .withCredentials(provider)
// .build();
// }
}
11 changes: 7 additions & 4 deletions src/main/java/com/retrip/trip/infra/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static com.retrip.trip.infra.adapter.in.presentation.rest.common.ApiResponse.*;

@Configuration
public class SwaggerConfig {
private final String jwtSchemeName = "jwtAuth";
Expand Down Expand Up @@ -89,10 +91,11 @@ private void addErrorResponse(Operation operation, ErrorCode errorCode) {
MediaType mediaType = apiResponse.getContent()
.computeIfAbsent("application/json", k -> new MediaType());

ErrorResponse exampleBody = ErrorResponse.of(
errorCode,
"Current API URL",
"Current API Method"
var exampleBody = of(ErrorResponse.of(
errorCode,
"Current API URL",
"Current API Method"
)
);

Example example = new Example();
Expand Down
11 changes: 10 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ spring:
enabled: true
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:tcp://localhost/~/trip;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;MODE=MySQL
url: jdbc:h2:mem:trip;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;MODE=MySQL
# url: jdbc:h2:tcp://localhost/~/trip;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;MODE=MySQL
username: sa
password:

Expand Down Expand Up @@ -45,4 +46,12 @@ jwt:
KucWwwIDAQAB
-----END PUBLIC KEY-----

cloud:
aws:
region: ap-northeast-2
credentials:
access-key: ${AWS_ACCESS_KEY_ID}
secret-key: ${AWS_SECRET_ACCESS_KEY}
s3:
bucket: retrip-media