Skip to content

Commit 2c37531

Browse files
committed
Merge branch 'develop' into feat/PRODUCT-262
# Conflicts: # src/main/java/eatda/repository/store/StoreRepository.java # src/main/java/eatda/service/store/StoreService.java # src/test/java/eatda/controller/BaseControllerTest.java # src/test/java/eatda/controller/store/StoreControllerTest.java # src/test/java/eatda/service/store/StoreServiceTest.java
2 parents 53f8b50 + 5abbd60 commit 2c37531

File tree

151 files changed

+2158
-1583
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

151 files changed

+2158
-1583
lines changed

.github/workflows/deploy-dev.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ jobs:
102102
echo "ECS_SERVICE=$(echo "$TF_OUTPUTS" | jq -r '.ecs_api_service_name.value')" >> $GITHUB_ENV
103103
echo "CONTAINER_NAME=$(echo "$TF_OUTPUTS" | jq -r '.ecs_api_container_name.value')" >> $GITHUB_ENV
104104
echo "PRIVATE_IP=$(echo "$TF_OUTPUTS" | jq -r '.ec2_private_ip.value')" >> $GITHUB_ENV
105+
echo "CLOUDFRONT_DOMAIN_NAME=$(echo "$TF_OUTPUTS" | jq -r '.cloudfront_domain_name.value')" >> $GITHUB_ENV
105106
106107
- name: Configure AWS credentials
107108
uses: aws-actions/configure-aws-credentials@v4
@@ -110,7 +111,7 @@ jobs:
110111
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
111112
aws-region: ${{ env.AWS_REGION }}
112113

113-
- name: Update MYSQL_URL in Parameter Store
114+
- name: Update MYSQL_URL, CLOUDFRONT_DOMAIN_NAME in Parameter Store
114115
run: |
115116
MYSQL_URL="jdbc:mysql://${PRIVATE_IP}:3306/eatda?useUnicode=true&characterEncoding=UTF-8"
116117
@@ -119,6 +120,12 @@ jobs:
119120
--type "SecureString" \
120121
--value "$MYSQL_URL" \
121122
--overwrite
123+
124+
aws ssm put-parameter \
125+
--name "/dev/CLOUDFRONT_DOMAIN_NAME" \
126+
--type "SecureString" \
127+
--value "$CLOUDFRONT_DOMAIN_NAME" \
128+
--overwrite
122129
123130
- name: Set up Node.js
124131
uses: actions/setup-node@v4

.github/workflows/deploy-prod.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ jobs:
107107
echo "ECS_SERVICE=$(echo "$TF_OUTPUTS" | jq -r '.ecs_api_service_name.value')" >> $GITHUB_ENV
108108
echo "CONTAINER_NAME=$(echo "$TF_OUTPUTS" | jq -r '.ecs_api_container_name.value')" >> $GITHUB_ENV
109109
echo "RDS_ENDPOINT=$(echo "$TF_OUTPUTS" | jq -r '.rds_endpoint.value')" >> $GITHUB_ENV
110+
echo "CLOUDFRONT_DOMAIN_NAME=$(echo "$TF_OUTPUTS" | jq -r '.cloudfront_domain_name.value')" >> $GITHUB_ENV
110111
111112
- name: Configure AWS credentials
112113
uses: aws-actions/configure-aws-credentials@v4
@@ -115,6 +116,14 @@ jobs:
115116
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
116117
aws-region: ${{ env.AWS_REGION }}
117118

119+
- name: Update CLOUDFRONT_DOMAIN_NAME in Parameter Store
120+
run: |
121+
aws ssm put-parameter \
122+
--name "/prod/CLOUDFRONT_DOMAIN_NAME" \
123+
--type "SecureString" \
124+
--value "$CLOUDFRONT_DOMAIN_NAME" \
125+
--overwrite
126+
118127
- name: Update DB URL in Parameter Store
119128
run: |
120129
aws ssm put-parameter --name "/prod/RDS_ENDPOINT" --value "jdbc:mysql://${{ env.RDS_ENDPOINT }}/eatda?useUnicode=true&characterEncoding=UTF-8" --type SecureString --overwrite

build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ dependencies {
6868
runtimeOnly 'com.mysql:mysql-connector-j'
6969
implementation 'org.flywaydb:flyway-core'
7070
implementation 'org.flywaydb:flyway-mysql'
71-
implementation 'com.github.ben-manes.caffeine:caffeine' // in-memory cache
7271

7372
// JWT
7473
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'

src/main/java/eatda/client/file/FileClient.java

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,94 @@
33
import eatda.exception.BusinessErrorCode;
44
import eatda.exception.BusinessException;
55
import java.time.Duration;
6+
import java.util.List;
7+
import java.util.concurrent.CompletableFuture;
8+
import java.util.concurrent.ExecutorService;
9+
import java.util.concurrent.Executors;
610
import org.springframework.beans.factory.annotation.Value;
711
import org.springframework.stereotype.Component;
8-
import org.springframework.web.multipart.MultipartFile;
9-
import software.amazon.awssdk.core.sync.RequestBody;
1012
import software.amazon.awssdk.services.s3.S3Client;
11-
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
13+
import software.amazon.awssdk.services.s3.model.CopyObjectRequest;
14+
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
1215
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
1316
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
14-
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
17+
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
1518

1619
@Component
1720
public class FileClient {
1821

22+
private static final int THREAD_POOL_SIZE = 5; // TODO 비동기 병렬처리 개선
1923
private final S3Client s3Client;
2024
private final String bucket;
2125
private final S3Presigner s3Presigner;
26+
private final ExecutorService executorService;
2227

2328
public FileClient(S3Client s3Client,
2429
@Value("${spring.cloud.aws.s3.bucket}") String bucket,
2530
S3Presigner s3Presigner) {
2631
this.s3Client = s3Client;
2732
this.bucket = bucket;
2833
this.s3Presigner = s3Presigner;
34+
this.executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
2935
}
3036

31-
public String upload(MultipartFile file, String fileKey) {
32-
PutObjectRequest request = PutObjectRequest.builder()
37+
public String generateUploadPresignedUrl(String fileKey, Duration signatureDuration) {
38+
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
3339
.bucket(bucket)
3440
.key(fileKey)
35-
.contentType(file.getContentType())
41+
.build();
42+
PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
43+
.putObjectRequest(putObjectRequest)
44+
.signatureDuration(signatureDuration)
3645
.build();
3746

3847
try {
39-
s3Client.putObject(request, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
40-
return fileKey;
48+
return s3Presigner.presignPutObject(presignRequest).url().toString();
4149
} catch (Exception exception) {
42-
throw new BusinessException(BusinessErrorCode.FILE_UPLOAD_FAILED);
50+
throw new BusinessException(BusinessErrorCode.PRESIGNED_URL_GENERATION_FAILED);
4351
}
44-
// TODO 발생 예외 별로 처리하기
45-
// InvalidRequestException, InvalidWriteOffsetException, TooManyPartsException, EncryptionTypeMismatchException,
46-
// AwsServiceException, SdkClientException, S3Exception
4752
}
4853

49-
public String getPreSignedUrl(String fileKey, Duration signatureDuration) {
50-
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
51-
.bucket(bucket)
52-
.key(fileKey)
53-
.build();
54-
GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
55-
.getObjectRequest(getObjectRequest)
56-
.signatureDuration(signatureDuration)
54+
public List<String> moveTempFilesToPermanent(String domainName, long domainId, List<String> tempImageKeys) {
55+
List<CompletableFuture<String>> futures = tempImageKeys.stream()
56+
.map(tempImageKey -> CompletableFuture.supplyAsync(() -> {
57+
String fileName = extractFileName(tempImageKey);
58+
String newPermanentKey = domainName + "/" + domainId + "/" + fileName;
59+
try {
60+
copyObject(tempImageKey, newPermanentKey);
61+
deleteObject(tempImageKey);
62+
return newPermanentKey;
63+
} catch (Exception e) { //TODO 근본 예외 추가 필요
64+
throw new BusinessException(BusinessErrorCode.FAIL_TEMP_IMAGE_PROCESS);
65+
}
66+
}, executorService))
67+
.toList();
68+
69+
return futures.stream()
70+
.map(CompletableFuture::join) // TODO 일부 파일 에러에도 처리하도록 개선
71+
.toList();
72+
}
73+
74+
private String extractFileName(String fullKey) {
75+
int index = fullKey.lastIndexOf('/');
76+
return index == -1 ? fullKey : fullKey.substring(index + 1);
77+
}
78+
79+
private void copyObject(String sourceKey, String destinationKey) {
80+
CopyObjectRequest copyReq = CopyObjectRequest.builder()
81+
.sourceBucket(bucket)
82+
.sourceKey(sourceKey)
83+
.destinationBucket(bucket)
84+
.destinationKey(destinationKey)
5785
.build();
86+
s3Client.copyObject(copyReq);
87+
}
5888

59-
try {
60-
return s3Presigner.presignGetObject(presignRequest).url().toString();
61-
} catch (Exception exception) {
62-
throw new BusinessException(BusinessErrorCode.PRESIGNED_URL_GENERATION_FAILED);
63-
}
89+
private void deleteObject(String key) {
90+
DeleteObjectRequest deleteReq = DeleteObjectRequest.builder()
91+
.bucket(bucket)
92+
.key(key)
93+
.build();
94+
s3Client.deleteObject(deleteReq);
6495
}
6596
}

src/main/java/eatda/config/CacheConfig.java

Lines changed: 0 additions & 36 deletions
This file was deleted.

src/main/java/eatda/controller/cheer/CheerController.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
import eatda.controller.web.auth.LoginMember;
44
import eatda.domain.ImageDomain;
5-
import eatda.domain.ImageKey;
65
import eatda.domain.store.StoreSearchResult;
76
import eatda.service.cheer.CheerService;
8-
import eatda.service.image.ImageService;
97
import eatda.service.store.StoreSearchService;
108
import jakarta.validation.constraints.Max;
119
import jakarta.validation.constraints.Min;
@@ -16,28 +14,24 @@
1614
import org.springframework.web.bind.annotation.GetMapping;
1715
import org.springframework.web.bind.annotation.PathVariable;
1816
import org.springframework.web.bind.annotation.PostMapping;
17+
import org.springframework.web.bind.annotation.RequestBody;
1918
import org.springframework.web.bind.annotation.RequestParam;
20-
import org.springframework.web.bind.annotation.RequestPart;
2119
import org.springframework.web.bind.annotation.RestController;
22-
import org.springframework.web.multipart.MultipartFile;
2320

2421
@Validated
2522
@RestController
2623
@RequiredArgsConstructor
2724
public class CheerController {
2825

2926
private final CheerService cheerService;
30-
private final ImageService imageService;
3127
private final StoreSearchService storeSearchService;
3228

3329
@PostMapping("/api/cheer")
34-
public ResponseEntity<CheerResponse> registerCheer(@RequestPart("request") CheerRegisterRequest request,
35-
@RequestPart(value = "image", required = false) MultipartFile image,
30+
public ResponseEntity<CheerResponse> registerCheer(@RequestBody CheerRegisterRequest request,
3631
LoginMember member) {
37-
ImageKey imageKey = imageService.uploadImage(ImageDomain.CHEER, image);
3832
StoreSearchResult searchResult = storeSearchService.searchStoreByKakaoId(
3933
request.storeName(), request.storeKakaoId());
40-
CheerResponse response = cheerService.registerCheer(request, searchResult, imageKey, member.id());
34+
CheerResponse response = cheerService.registerCheer(request, searchResult, member.id(), ImageDomain.CHEER);
4135
return ResponseEntity.status(HttpStatus.CREATED)
4236
.body(response);
4337
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package eatda.controller.cheer;
2+
3+
import eatda.domain.cheer.CheerImage;
4+
5+
public record CheerImageResponse(
6+
String imageKey,
7+
long orderIndex,
8+
String contentType,
9+
long fileSize,
10+
String url
11+
) {
12+
public CheerImageResponse(CheerImage cheerImage, String cdnBaseUrl) {
13+
this(
14+
cheerImage.getImageKey(),
15+
cheerImage.getOrderIndex(),
16+
cheerImage.getContentType(),
17+
cheerImage.getFileSize(),
18+
"https://" + cdnBaseUrl + "/" + cheerImage.getImageKey()
19+
);
20+
}
21+
}

src/main/java/eatda/controller/cheer/CheerPreviewResponse.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
public record CheerPreviewResponse(
88
long storeId,
9-
String imageUrl,
9+
List<CheerImageResponse> images,
1010
String storeName,
1111
String storeDistrict,
1212
String storeNeighborhood,
@@ -18,10 +18,10 @@ public record CheerPreviewResponse(
1818
String memberNickname
1919
) {
2020

21-
public CheerPreviewResponse(Cheer cheer, String imageUrl) {
21+
public CheerPreviewResponse(Cheer cheer, List<CheerImageResponse> images) {
2222
this(
2323
cheer.getStore().getId(),
24-
imageUrl,
24+
images,
2525
cheer.getStore().getName(),
2626
cheer.getStore().getAddressDistrict(),
2727
cheer.getStore().getAddressNeighborhood(),

src/main/java/eatda/controller/cheer/CheerRegisterRequest.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,22 @@ public record CheerRegisterRequest(
88
String storeKakaoId,
99
String storeName,
1010
String description,
11+
List<UploadedImageDetail> images,
1112
List<CheerTagName> tags
1213
) {
13-
1414
@Override
1515
public List<CheerTagName> tags() { // TODO : 클라이언트 태그 구현 완료 시 삭제
1616
if (tags == null) {
1717
return Collections.emptyList();
1818
}
1919
return tags;
2020
}
21+
22+
public record UploadedImageDetail(
23+
String imageKey,
24+
long orderIndex,
25+
String contentType,
26+
long fileSize
27+
) {
28+
}
2129
}

src/main/java/eatda/controller/cheer/CheerResponse.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,26 @@
33
import eatda.domain.cheer.Cheer;
44
import eatda.domain.cheer.CheerTagName;
55
import eatda.domain.store.Store;
6+
import java.util.Comparator;
67
import java.util.List;
8+
import java.util.stream.Collectors;
79

810
public record CheerResponse(
911
long storeId,
1012
long cheerId,
11-
String imageUrl,
13+
List<CheerImageResponse> images,
1214
String cheerDescription,
1315
List<CheerTagName> tags
1416
) {
1517

16-
public CheerResponse(Cheer cheer, Store store, String imageUrl) {
18+
public CheerResponse(Cheer cheer, Store store, String cdnBaseUrl) {
1719
this(
1820
store.getId(),
1921
cheer.getId(),
20-
imageUrl,
22+
cheer.getImages().stream()
23+
.map(img -> new CheerImageResponse(img, cdnBaseUrl)) // ✅ CDN 붙여줌
24+
.sorted(Comparator.comparingLong(CheerImageResponse::orderIndex))
25+
.collect(Collectors.toList()),
2126
cheer.getDescription(),
2227
cheer.getCheerTagNames()
2328
);

0 commit comments

Comments
 (0)