Skip to content

Commit

Permalink
Merge pull request #14 from YAPP-Github/feature/#12
Browse files Browse the repository at this point in the history
feat: 사진 저장 API 구현
  • Loading branch information
CChuYong authored Jul 4, 2024
2 parents 47b81c7 + 4420b2b commit 0f84480
Show file tree
Hide file tree
Showing 17 changed files with 351 additions and 5 deletions.
2 changes: 1 addition & 1 deletion photo-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ dependencies {
implementation("org.projectlombok:lombok:1.18.32")
annotationProcessor("org.projectlombok:lombok:1.18.32")
implementation("com.github.f4b6a3:ulid-creator:5.2.3")

implementation("io.awspring.cloud:spring-cloud-starter-aws:2.4.4")
}

tasks.withType<Test> {
Expand Down
35 changes: 35 additions & 0 deletions photo-service/src/main/java/kr/mafoo/photo/config/NcpConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package kr.mafoo.photo.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class NcpConfig {

@Value("${cloud.aws.credentials.access-key}")
private String accessKey;

@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;

@Value("${cloud.aws.region.static}")
private String region;

@Value("${cloud.aws.s3.endpoint}")
private String endPoint;

@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey,secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endPoint, region))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package kr.mafoo.photo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;

@EnableWebFlux
Expand All @@ -12,4 +14,15 @@ public class WebFluxConfig implements WebFluxConfigurer {
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
configurer.addCustomResolver(new MemberIdParameterResolver());
}

@Bean("externalWebClient")
public WebClient externalServiceWebClient() {
return WebClient.builder()
.codecs(clientCodecConfigurer -> {
clientCodecConfigurer
.defaultCodecs()
.maxInMemorySize(16 * 1024 * 1024); // 16MB
})
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ public Mono<PhotoResponse> createPhoto(
String memberId,
PhotoCreateRequest request
){
return Mono.just(
new PhotoResponse("test_photo_id", "photo_url", null)
);
return photoService
.createNewPhoto(request.qrUrl(), memberId)
.map(PhotoResponse::fromEntity);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kr.mafoo.photo.controller.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import kr.mafoo.photo.domain.BrandType;
import kr.mafoo.photo.domain.PhotoEntity;

@Schema(description = "사진 응답")
Expand All @@ -11,6 +12,9 @@ public record PhotoResponse(
@Schema(description = "사진 URL", example = "photo_url")
String photoUrl,

@Schema(description = "사진 브랜드", example = "LIFE_FOUR_CUTS")
BrandType brand,

@Schema(description = "앨범 ID", example = "test_album_id")
String albumId
) {
Expand All @@ -20,6 +24,7 @@ public static PhotoResponse fromEntity(
return new PhotoResponse(
entity.getPhotoId(),
entity.getPhotoUrl(),
entity.getBrand(),
entity.getAlbumId()
);
}
Expand Down
32 changes: 32 additions & 0 deletions photo-service/src/main/java/kr/mafoo/photo/domain/BrandType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package kr.mafoo.photo.domain;

public enum BrandType {
LIFE_FOUR_CUTS("https://api.life4cut.net/"),
PHOTOISM("https://qr.seobuk.kr/"),
HARU_FILM( "http://haru6.mx2.co.kr/"),
DONT_LOOK_UP("https://x.dontlxxkup.kr/"),
;

private String urlFormat;

private BrandType(String urlFormat) {
this.urlFormat = urlFormat;
}

public String getUrlFormat() {
return urlFormat;
}

public boolean matches(String qrUrl) {
return qrUrl.startsWith(this.urlFormat);
}

public static BrandType matchBrandType(String qrUrl) {
for (BrandType brandType : BrandType.values()) {
if (brandType.matches(qrUrl)) {
return brandType;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class PhotoEntity implements Persistable<String> {
@Column("url")
private String photoUrl;

@Column("brand")
private BrandType brand;

@Column("owner_member_id")
private String ownerMemberId;

Expand Down Expand Up @@ -64,10 +67,11 @@ public PhotoEntity updateAlbumId(String albumId) {
return this;
}

public static PhotoEntity newPhoto(String photoId, String photoUrl, String ownerMemberId) {
public static PhotoEntity newPhoto(String photoId, String photoUrl, BrandType brandType, String ownerMemberId) {
PhotoEntity photo = new PhotoEntity();
photo.photoId = photoId;
photo.photoUrl = photoUrl;
photo.brand = brandType;
photo.ownerMemberId = ownerMemberId;
photo.isNew = true;
return photo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
@Getter
@RequiredArgsConstructor
public enum ErrorCode {

REDIRECT_URI_NOT_FOUND("EX001", "리다이렉트 URI를 찾을 수 없습니다"),

ALBUM_NOT_FOUND("AE0001", "앨범을 찾을 수 없습니다"),
PHOTO_NOT_FOUND("PE0001", "사진을 찾을 수 없습니다"),
PHOTO_BRAND_NOT_EXISTS("PE002", "사진 브랜드가 존재하지 않습니다"),
PHOTO_QR_URL_EXPIRED("PE003", "사진 저장을 위한 QR URL이 만료되었습니다"),

;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kr.mafoo.photo.exception;

public class PhotoBrandNotExistsException extends DomainException {
public PhotoBrandNotExistsException() {
super(ErrorCode.PHOTO_BRAND_NOT_EXISTS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kr.mafoo.photo.exception;

public class PhotoQrUrlExpiredException extends DomainException{
public PhotoQrUrlExpiredException() {
super(ErrorCode.PHOTO_QR_URL_EXPIRED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kr.mafoo.photo.exception;

public class RedirectUriNotFoundException extends DomainException {
public RedirectUriNotFoundException() {
super(ErrorCode.REDIRECT_URI_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package kr.mafoo.photo.service;

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Mono;

import java.io.ByteArrayInputStream;
import java.io.InputStream;

import java.util.UUID;

@Service
@RequiredArgsConstructor
public class ObjectStorageService {

private final AmazonS3Client amazonS3Client;

@Value("${cloud.aws.s3.bucket}")
private String bucketName;

public Mono<String> uploadFile(byte[] fileByte) {
String keyName = "/" + UUID.randomUUID();

ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(fileByte.length);
objectMetadata.setContentType("image/jpeg");

return Mono.fromCallable(() -> {
try (InputStream inputStream = new ByteArrayInputStream(fileByte)) {
amazonS3Client.putObject(
new PutObjectRequest(bucketName, keyName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));

return "https://kr.object.ncloudstorage.com/" + bucketName + "/" + keyName;
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}


}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package kr.mafoo.photo.service;

import kr.mafoo.photo.domain.BrandType;
import kr.mafoo.photo.domain.PhotoEntity;
import kr.mafoo.photo.exception.AlbumNotFoundException;
import kr.mafoo.photo.exception.PhotoNotFoundException;
import kr.mafoo.photo.repository.AlbumRepository;
import kr.mafoo.photo.repository.PhotoRepository;
import kr.mafoo.photo.util.IdGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
Expand All @@ -16,6 +18,20 @@ public class PhotoService {
private final PhotoRepository photoRepository;
private final AlbumRepository albumRepository;

private final QrService qrService;
private final ObjectStorageService objectStorageService;

public Mono<PhotoEntity> createNewPhoto(String qrUrl, String requestMemberId) {
return qrService
.getFileFromQrUrl(qrUrl)
.flatMap(fileDto -> objectStorageService.uploadFile(fileDto.fileByte())
.flatMap(photoUrl -> {
PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, fileDto.type(), requestMemberId);
return photoRepository.save(photoEntity);
})
);
}

public Flux<PhotoEntity> findAllByAlbumId(String albumId, String requestMemberId) {
return albumRepository
.findById(albumId)
Expand Down
Loading

0 comments on commit 0f84480

Please sign in to comment.