Skip to content

Commit

Permalink
Merge pull request #148 from UMC5th-bias/develop
Browse files Browse the repository at this point in the history
[DEPLOY] main <- develop 병합
  • Loading branch information
JungYoonShin committed Sep 20, 2024
2 parents 25a159b + 37fb1a9 commit 1c9a926
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 12 deletions.
Binary file added Group [email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

| | | | |
|---------|--------------|----------|-------------|
| 담당 역할 | <li>Github Actions + AWS CodeDeploy CI/CD </li><li> ERD 및 엔티티 설계 </li><li>예외처리 </li><li>로그인 + 회원가입 API </li><li>상점 도메인 API</li><li>카카오 로그인(개발중)</li> | <li>API 명세서</li><li>성지순례 도메인 API</li><li>위치 기반 인증(웹 소켓 - 개발중)</li><li>마이페이지 도메인 API</li>| <li>ERD 및 엔티티 설계</li><li>홈 화면 API</li><li>커뮤니티 도메인 API</li><li>이미지 업로드 로직(GCP)</li><li>FCM 푸시 알림(개발중)</li><li>대댓글 API</li> |
| 담당 역할 | <li>Github Actions + AWS CodeDeploy CI/CD </li><li> ERD 및 엔티티 설계 </li><li>예외처리 </li><li>로그인 + 회원가입 API </li><li>상점 도메인 API</li><li>카카오 로그인</li><li>AWS S3 이미지 업로드</li> | <li>API 명세서</li><li>성지순례 도메인 API</li><li>위치 기반 인증(STOMP)</li><li>마이페이지 도메인 API</li>| <li>ERD 및 엔티티 설계</li><li>홈 화면 API</li><li>커뮤니티 도메인 API</li><li>FCM 푸시 알림</li><li>대댓글 API</li> |

</div>
<br/><br/>
Expand All @@ -34,4 +34,5 @@
- `develop`: 프로덕트 배포 전 기능을 개발하는 브랜치
- `feature`: 단위 기능을 개발하는 브랜치로 단위 기능 개발이 완료되면 develop 브랜치에 merge ex) feat/#이슈번호

## 💖 8월 말 ~ 9월 초 릴리즈 예정

## 💖 2024년 하반기 릴리즈 예정!
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ dependencies {
//image
implementation 'com.google.cloud:google-cloud-storage:2.20.1'
implementation 'org.springframework.cloud:spring-cloud-gcp-starter-storage:1.2.8.RELEASE'
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation 'net.coobird:thumbnailator:0.4.14'

// FCM
Expand Down
16 changes: 12 additions & 4 deletions src/main/java/com/favoriteplace/app/dto/member/MemberDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -33,7 +34,7 @@ public Member toEntity(String encodedPassword, String profileImg, Item titleItem
.password(encodedPassword)
.alarmAllowance(snsAllow)
.description(introduction)
.profileImageUrl(profileImg == null ? null : ConvertUuidToUrl.convertUuidToUrl(profileImg))
.profileImageUrl(profileImg)
.point(0L)
.loginType(LoginType.SELF)
.profileTitle(titleItem)
Expand Down Expand Up @@ -73,8 +74,11 @@ public static class EmailSendReqDto {
* 2)@기호를 기준으로 이메일 주소를 이루는 로컬호스트와 도메인 파트가 존재해야 한다.
* 3)도메인 파트는 최소하나의 점과 그 뒤에 최소한 2개의 알파벳을 가진다를 검증
*/
@Email
@NotEmpty(message = "이메일을 입력해주세요")
@NotEmpty(message = "이메일 입력은 필수 입니다.")
@Pattern(
regexp = "^[\\w\\.-]+@[\\w\\.-]+\\.[a-zA-Z]{2,}$",
message = "이메일 형식에 맞지 않습니다."
)
private String email;
}

Expand All @@ -95,8 +99,12 @@ public static class EmailDuplicateResDto {
@Getter
@NoArgsConstructor
public static class EmailCheckReqDto {
@Email

@NotEmpty(message = "이메일을 입력해 주세요")
@Pattern(
regexp = "^[\\w\\.-]+@[\\w\\.-]+\\.[a-zA-Z]{2,}$",
message = "이메일 형식에 맞지 않습니다."
)
private String email;

@NotNull(message = "인증 번호를 입력해 주세요")
Expand Down
12 changes: 9 additions & 3 deletions src/main/java/com/favoriteplace/app/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@
import com.favoriteplace.app.repository.MemberRepository;
import com.favoriteplace.global.exception.RestApiException;
import com.favoriteplace.global.gcpImage.UploadImage;
import com.favoriteplace.global.s3Image.AmazonS3ImageManager;
import com.favoriteplace.global.security.kakao.KakaoClient;
import com.favoriteplace.global.security.provider.JwtTokenProvider;
import com.favoriteplace.global.util.SecurityUtil;

import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
Expand All @@ -38,8 +42,8 @@ public class MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
public final SecurityUtil securityUtil;
private final UploadImage uploadImage;
private final SecurityUtil securityUtil;
private final AmazonS3ImageManager amazonS3ImageManager;
private final ItemRepository itemRepository;
private final RedisTemplate redisTemplate;
private final KakaoClient kakaoClient;
Expand Down Expand Up @@ -77,6 +81,8 @@ public MemberDto.MemberSignUpResDto kakaoSignUp(final String token, final KaKaoS
@Transactional
public MemberDto.MemberSignUpResDto signup(MemberSignUpReqDto memberSignUpReqDto, List<MultipartFile> images)
throws IOException {
List<CompletableFuture<String>> futures = new ArrayList<>();

memberRepository.findByEmail(memberSignUpReqDto.getEmail())
.ifPresent(
existingMember -> {
Expand All @@ -88,7 +94,7 @@ public MemberDto.MemberSignUpResDto signup(MemberSignUpReqDto memberSignUpReqDto
String password = passwordEncoder.encode(memberSignUpReqDto.getPassword());

if (images != null && !images.get(0).isEmpty()) {
profileImageUrl = uploadImage.uploadImageToCloud(images.get(0));
profileImageUrl = amazonS3ImageManager.upload(images.get(0)).join();
}

Item titleItem = itemRepository.findByName("새싹회원").get();
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/favoriteplace/global/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.favoriteplace.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@EnableAsync
@Configuration
public class AsyncConfig {

/**
corePoolSize = CPU 코어수 * CPU 사용률 * (1 + I/O입력시간대기효율)
*/
@Bean(name = "S3imageUploadExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

int numOfCores = Runtime.getRuntime().availableProcessors();
float targetCpuUtilization = 0.3f; // CPU 사용률
float blockingCoefficient = 0.1f; // I/O 입력시간 대기효율
int corePoolSize = (int) (numOfCores * targetCpuUtilization * (1 + blockingCoefficient));
executor.setCorePoolSize(corePoolSize);
executor.initialize();
return executor;
}
}
51 changes: 51 additions & 0 deletions src/main/java/com/favoriteplace/global/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.favoriteplace.global.config;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import lombok.Getter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;

import javax.annotation.PostConstruct;


@Configuration
@Getter
public class S3Config {

private AWSCredentials awsCredentials;

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

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

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

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

@Value("${cloud.aws.s3.path}")
private String filePath;

@PostConstruct
public void init() {
this.awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
}

@Bean
public AmazonS3Client amazonS3Client() {
AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);

return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -15,9 +16,11 @@
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import java.lang.reflect.Method;

@Slf4j
@RestControllerAdvice
public class ExceptionHandlerAdvice {
public class ExceptionHandlerAdvice implements AsyncUncaughtExceptionHandler {

//모든 에러 -> 하위 에러에서 못받을 때
@ExceptionHandler(Exception.class)
Expand Down Expand Up @@ -89,4 +92,10 @@ public ResponseEntity handleMaxUploadSizeException(MaxUploadSizeExceededExceptio
ErrorResponse errorResponse = ErrorResponse.of(errorCode.getHttpStatus(), errorCode.getCode(), errorCode.getMessage());
return ResponseEntity.status(errorResponse.getHttpStatus()).body(errorResponse.getMessage());
}

// 비동기 메서드 예외 처리를 위해
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.error("[ASYNC-ERROR] method: {} exception: {}", method.getName(), ex);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.favoriteplace.global.s3Image;

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.favoriteplace.global.config.S3Config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;

@Slf4j
@Component
@RequiredArgsConstructor
public class AmazonS3ImageManager {

private final AmazonS3Client amazonS3Client;
private final S3Config s3Config;

@Async("S3imageUploadExecutor")
public CompletableFuture<String> upload(MultipartFile multipartFile) throws IOException {
CompletableFuture<String> future = new CompletableFuture<>();

String s3FileName = UUID.randomUUID().toString().substring(0, 10) + multipartFile.getOriginalFilename();

File uploadFile = convert(multipartFile)
.orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패"));

future.complete(upload(uploadFile, s3FileName));

return future;
}

@Async("S3imageUploadExecutor")
public List<CompletableFuture<String>>upload(List<MultipartFile> multipartFiles) throws IOException {
List<CompletableFuture<String>> futures = new ArrayList<CompletableFuture<String>>();
CompletableFuture<String> future = new CompletableFuture<>();

for (MultipartFile file : multipartFiles) {
String s3FileName = UUID.randomUUID().toString().substring(0, 10) + file.getOriginalFilename();

File uploadFile = convert(file)
.orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패"));
future.complete((upload(uploadFile, s3FileName)));
}
futures.add(future);

return futures;
}

private String upload(File uploadFile, String s3FileName) {
String fileName = s3Config.getFilePath()+ "/" + s3FileName + uploadFile.getName();
String uploadImageUrl = putS3(uploadFile, fileName);
removeNewFile(uploadFile); // 로컬에 생성된 File 삭제 (MultipartFile -> File 전환 하며 로컬에 파일 생성됨)
return uploadImageUrl; // 업로드된 파일의 S3 URL 주소 반환
}

private String putS3(File uploadFile, String fileName) {
amazonS3Client.putObject(
new PutObjectRequest(s3Config.getBucket(), fileName, uploadFile)
.withCannedAcl(CannedAccessControlList.PublicRead) // PublicRead 권한으로 업로드 됨
);
return amazonS3Client.getUrl(s3Config.getBucket(), fileName).toString();
}

private void removeNewFile(File targetFile) {
if (targetFile.delete()) {
log.info("파일이 삭제되었습니다.");
} else {
log.info("파일이 삭제되지 못했습니다.");
}
}

private Optional<File> convert(MultipartFile file) throws IOException {
File convertFile = new File(Objects.requireNonNull(file.getOriginalFilename()));
if (convertFile.createNewFile()) {
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
fos.write(file.getBytes());
}
return Optional.of(convertFile);
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ public void configureMessageBroker(MessageBrokerRegistry registry) {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
.setAllowedOriginPatterns("*");
}

@Override
Expand Down

0 comments on commit 1c9a926

Please sign in to comment.