Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c08edf2
Merge pull request #77 from RealPawParazzi/feature/14/follow
Lee-Han-Jun Mar 12, 2025
5fae90b
refactor: followerCount, followingCount 역할 변경
Lee-Han-Jun Mar 12, 2025
065a7b7
Merge pull request #78 from RealPawParazzi/feature/14/follow
Lee-Han-Jun Mar 12, 2025
20ba52f
[FEAT] S3 member 이미지 처리
geg222 Mar 12, 2025
b0c8f01
Merge pull request #79 from RealPawParazzi/feature/75/s3
geg222 Mar 12, 2025
5dcf191
[FIX] backApplication .env 불러오기
geg222 Mar 12, 2025
ab38a12
Merge pull request #81 from RealPawParazzi/feature/80/FixBackApplic
geg222 Mar 12, 2025
4b9d8d1
[FIX] 에러 수정
geg222 Mar 13, 2025
e6cbf88
Merge pull request #83 from RealPawParazzi/feature/82/FixError
geg222 Mar 13, 2025
66f51cc
[FEAT] Pet 등록 S3 연동
geg222 Mar 14, 2025
fbefd40
Merge pull request #86 from RealPawParazzi/feature/85/PetS3
geg222 Mar 14, 2025
26fd781
[FEAT] Pet 수정, 삭제 S3 연동 기능 및 시각화
geg222 Mar 14, 2025
55d68cf
feat: 산책기록 저장/조회/삭제 기능 추가
Lee-Han-Jun Mar 16, 2025
6f220b6
feat: 날짜와 펫 아이디로 산책 기록 조회
Lee-Han-Jun Mar 16, 2025
23eeaaf
Merge pull request #87 from RealPawParazzi/feature/73/walk
Lee-Han-Jun Mar 16, 2025
55951fa
[FIX] kakao 오류 수정
geg222 Mar 16, 2025
7725990
Merge pull request #89 from RealPawParazzi/feature/88/FixKakao
geg222 Mar 16, 2025
ac7e2d7
[FIX] kakao 오류 수정
geg222 Mar 16, 2025
f914ca2
Merge remote-tracking branch 'origin/dev' into feature/73/walk
Lee-Han-Jun Mar 19, 2025
fceb123
refactor: 날짜 형식 ZonedDateTime에서 LocalDateTime으로 변경
Lee-Han-Jun Mar 19, 2025
815564d
Merge pull request #91 from RealPawParazzi/feature/73/walk
Lee-Han-Jun Mar 19, 2025
c210dbf
[FEAT] 게시물 S3 기능 연동
geg222 Mar 20, 2025
a20beeb
Merge pull request #92 from RealPawParazzi/feature/90/BoardS3Add
geg222 Mar 23, 2025
b2ce1e6
feat: Github Actions에 deploy.yml 추가
Lee-Han-Jun Mar 23, 2025
9249706
Merge pull request #94 from RealPawParazzi/feature/93/EC2
Lee-Han-Jun Mar 24, 2025
cbe8739
fix: deploy.yml에 실행 권한 스크립트 추가
Lee-Han-Jun Mar 24, 2025
cf9d675
Merge pull request #96 from RealPawParazzi/feature/95/EC2test
Lee-Han-Jun Mar 24, 2025
286835c
[Refactor] borad 불필요한 생성자 제거
geg222 Mar 24, 2025
3e118a3
Merge pull request #98 from RealPawParazzi/feature/97/boradRefactor2
geg222 Mar 24, 2025
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
44 changes: 44 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Deploy to EC2

on:
push:
branches: [ dev ] # dev 브랜치에 푸시될 때만 실행

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'

- name: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew build -x test

- name: Deploy to EC2
uses: appleboy/scp-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
source: "build/libs/*.jar"
target: "/home/ubuntu/deploy"
strip_components: 1

- name: Execute deploy script on EC2
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
cd /home/ubuntu/deploy
chmod +x deploy.sh
./deploy.sh
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ dependencies {
implementation 'io.github.cdimascio:java-dotenv:5.2.2'
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'


implementation 'software.amazon.awssdk:s3:2.20.140'
implementation 'software.amazon.awssdk:netty-nio-client:2.20.140'

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
Expand Down
22 changes: 10 additions & 12 deletions src/main/java/pawparazzi/back/BackApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,20 @@
import io.github.cdimascio.dotenv.Dotenv;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class BackApplication {
public static void main(String[] args) {
// .env 파일 로드
Dotenv dotenv = Dotenv.load();
System.setProperty("PAWPARAZZI_DB_URL", dotenv.get("PAWPARAZZI_DB_URL"));
System.setProperty("PAWPARAZZI_DB_USERNAME", dotenv.get("PAWPARAZZI_DB_USERNAME"));
System.setProperty("PAWPARAZZI_DB_PASSWORD", dotenv.get("PAWPARAZZI_DB_PASSWORD"));
System.setProperty("PAWPARAZZI_MONGO_URI", dotenv.get("PAWPARAZZI_MONGO_URI"));
System.setProperty("KAKAO_CLIENT_ID", dotenv.get("KAKAO_CLIENT_ID"));
System.setProperty("KAKAO_CLIENT_SECRET", dotenv.get("KAKAO_CLIENT_SECRET"));
System.setProperty("KAKAO_REDIRECT_URI", dotenv.get("KAKAO_REDIRECT_URI"));
System.setProperty("JWT_SECRET", dotenv.get("JWT_SECRET"));
System.setProperty("JWT_EXPIRATION", dotenv.get("JWT_EXPIRATION"));
Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();

dotenv.entries().forEach(entry -> {
if (System.getProperty(entry.getKey()) == null) {
System.setProperty(entry.getKey(), entry.getValue());
}
});

SpringApplication.run(BackApplication.class, args);
}
}
}
38 changes: 38 additions & 0 deletions src/main/java/pawparazzi/back/S3/S3AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package pawparazzi.back.S3;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;

import java.time.Duration;

@Configuration
public class S3AsyncConfig {

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

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

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

@Bean
public S3AsyncClient s3AsyncClient() {
return S3AsyncClient.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)
))
.httpClientBuilder(NettyNioAsyncHttpClient.builder()
.connectionTimeout(Duration.ofSeconds(10))
)
.build();
}
}
35 changes: 35 additions & 0 deletions src/main/java/pawparazzi/back/S3/S3UploadUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package pawparazzi.back.S3;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import pawparazzi.back.S3.service.S3AsyncService;

import java.io.IOException;
import java.util.concurrent.CompletableFuture;

@Component
public class S3UploadUtil {

private final S3AsyncService s3AsyncService;

public S3UploadUtil(S3AsyncService s3AsyncService) {
this.s3AsyncService = s3AsyncService;
}

@Async
public CompletableFuture<String> uploadImageAsync(MultipartFile file, String pathPrefix, String defaultImageUrl) {
if (file == null || file.isEmpty()) {
return CompletableFuture.completedFuture(defaultImageUrl);
}

try {
String fileName = pathPrefix + "_" + System.currentTimeMillis();
return s3AsyncService.uploadFile(fileName, file.getBytes(), file.getContentType());
} catch (IOException e) {
CompletableFuture<String> failedFuture = new CompletableFuture<>();
failedFuture.completeExceptionally(new RuntimeException("파일 업로드 실패: " + e.getMessage()));
return failedFuture;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package pawparazzi.back.S3.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import pawparazzi.back.S3.service.S3AsyncService;

import java.io.IOException;
import java.util.concurrent.CompletableFuture;

@RestController
@RequestMapping("/api/async/images")
public class ImageAsyncController {

private final S3AsyncService s3AsyncService;

public ImageAsyncController(S3AsyncService s3AsyncService) {
this.s3AsyncService = s3AsyncService;
}

/**
* 비동기적으로 이미지 업로드
*/
@PostMapping("/upload")
public CompletableFuture<ResponseEntity<String>> uploadImage(@RequestParam("file") MultipartFile file) {
try {
String contentType = file.getContentType();
byte[] fileBytes = file.getBytes();
String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();

return s3AsyncService.uploadFile(fileName, fileBytes, contentType)
.thenApply(url -> ResponseEntity.ok("File uploaded successfully: " + url));
} catch (IOException e) {
return CompletableFuture.completedFuture(ResponseEntity.badRequest().body("File upload failed: " + e.getMessage()));
}
}

/**
* 비동기적으로 이미지 삭제
*/
@DeleteMapping("/delete")
public CompletableFuture<ResponseEntity<String>> deleteImage(@RequestParam("fileName") String fileName) {
return s3AsyncService.deleteFile(fileName)
.thenApply(voidRes -> ResponseEntity.ok("File deleted successfully: " + fileName))
.exceptionally(ex -> ResponseEntity.badRequest().body("File delete failed: " + ex.getMessage()));
}
}
133 changes: 133 additions & 0 deletions src/main/java/pawparazzi/back/S3/service/S3AsyncService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package pawparazzi.back.S3.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.model.*;

import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

@Service
public class S3AsyncService {
private final S3AsyncClient s3AsyncClient;

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

public S3AsyncService(S3AsyncClient s3AsyncClient) {
this.s3AsyncClient = s3AsyncClient;
}

/**
* S3에 비동기적으로 파일 업로드
*/
@Async
public CompletableFuture<String> uploadFile(String fileName, byte[] fileData, String contentType) {
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(fileName)
.contentType(contentType)
.build();

return s3AsyncClient.putObject(putObjectRequest, AsyncRequestBody.fromByteBuffer(ByteBuffer.wrap(fileData)))
.thenApply(response -> {
if (response.sdkHttpResponse().isSuccessful()) {
return "https://" + bucketName + ".s3.amazonaws.com/" + fileName;
} else {
throw new RuntimeException("S3 업로드 실패: " + response.sdkHttpResponse().statusCode());
}
});
}

/**
* S3에서 비동기적으로 파일 삭제
*/
public CompletableFuture<Void> deleteFile(String key) {
return s3AsyncClient.listObjectVersions(ListObjectVersionsRequest.builder()
.bucket(bucketName)
.prefix(key)
.build())
.thenCompose(response -> {
List<ObjectIdentifier> objectsToDelete = response.versions().stream()
.map(version -> ObjectIdentifier.builder()
.key(version.key())
.versionId(version.versionId())
.build())
.collect(Collectors.toList());

if (objectsToDelete.isEmpty()) {
System.out.println("삭제할 버전 없음: " + key);
return CompletableFuture.completedFuture(null);
}

DeleteObjectsRequest deleteObjectsRequest = DeleteObjectsRequest.builder()
.bucket(bucketName)
.delete(Delete.builder().objects(objectsToDelete).build())
.build();

return s3AsyncClient.deleteObjects(deleteObjectsRequest)
.thenRun(() -> System.out.println("S3 삭제 성공 (버전 포함): " + key))
.exceptionally(ex -> {
System.err.println("S3 삭제 실패: " + ex.getMessage());
return null;
});
});
}

/**
* 기존 프로필 이미지 삭제 후 새 이미지 업로드
*/
public CompletableFuture<String> updateProfileImage(String existingImageUrl, String newFileName, byte[] newFileData, String contentType) {
if (existingImageUrl != null && !existingImageUrl.isBlank()) {
String oldFileName = extractFileName(existingImageUrl);
deleteFile(oldFileName).join();
}
return uploadFile(newFileName, newFileData, contentType);
}

/**
* S3 URL에서 파일 이름을 추출하는 메서드
*/
public String extractFileName(String imageUrl) {
return imageUrl.substring(imageUrl.lastIndexOf("/") + 1);
}


public List<String> listFilesInFolder(String folderPath) {
ListObjectsV2Request listRequest = ListObjectsV2Request.builder()
.bucket(bucketName)
.prefix(folderPath)
.build();

ListObjectsV2Response listResponse = s3AsyncClient.listObjectsV2(listRequest).join();

return listResponse.contents().stream()
.map(s3Object -> s3Object.key())
.collect(Collectors.toList());
}

public CompletableFuture<Void> deleteFiles(List<String> fileKeys) {
if (fileKeys.isEmpty()) {
return CompletableFuture.completedFuture(null);
}

DeleteObjectsRequest deleteRequest = DeleteObjectsRequest.builder()
.bucket(bucketName)
.delete(Delete.builder()
.objects(fileKeys.stream()
.map(key -> ObjectIdentifier.builder().key(key).build())
.collect(Collectors.toList()))
.build())
.build();

return s3AsyncClient.deleteObjects(deleteRequest)
.thenApply(response -> null);
}


}
Loading