-
Notifications
You must be signed in to change notification settings - Fork 0
#8 Store CRUD 및 이미지 업로드 기능 추가 #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
a7e60d0
71155f8
68a5c2b
27a8dad
7624cb6
57d554c
cfb4cfb
95c2b71
eb1ee68
bf8a1de
2be16e4
05f9bb6
efa11ea
c2f4ffb
55a5b5d
455f6bf
b38796b
651c8d5
26cdf37
b241273
c7b4e21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.example.gtable.global.config; | ||
|
|
||
| import java.util.concurrent.Executor; | ||
|
|
||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | ||
|
|
||
| @Configuration | ||
| public class AsyncConfig { | ||
| @Bean(name = "s3UploadExecutor") | ||
| public Executor s3UploadExecutor() { | ||
| ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); | ||
| executor.setCorePoolSize(5); | ||
| executor.setMaxPoolSize(10); | ||
| executor.setQueueCapacity(100); | ||
| executor.setThreadNamePrefix("S3Upload-"); | ||
| executor.initialize(); | ||
| return executor; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.example.gtable.global.config; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| import com.amazonaws.auth.AWSStaticCredentialsProvider; | ||
| import com.amazonaws.auth.BasicAWSCredentials; | ||
| import com.amazonaws.services.s3.AmazonS3Client; | ||
| import com.amazonaws.services.s3.AmazonS3ClientBuilder; | ||
|
|
||
| @Configuration | ||
| public class AwsS3Config { | ||
|
|
||
| @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; | ||
|
|
||
| @Bean | ||
| public AmazonS3Client amazonS3Client() { | ||
| BasicAWSCredentials 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 |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package com.example.gtable.global.s3; | ||
|
|
||
| import java.io.InputStream; | ||
| import java.util.UUID; | ||
| import java.util.concurrent.CompletableFuture; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.scheduling.annotation.Async; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| import com.amazonaws.services.s3.AmazonS3Client; | ||
| import com.amazonaws.services.s3.model.ObjectMetadata; | ||
|
|
||
| import io.github.resilience4j.bulkhead.annotation.Bulkhead; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class S3Service { | ||
| private final AmazonS3Client amazonS3Client; | ||
|
|
||
| @Value("${cloud.aws.s3.bucket}") | ||
| private String bucket; | ||
|
|
||
| public record S3UploadResult(String key, String url) { | ||
| } | ||
|
|
||
| @Bulkhead(name = "s3UploadBulkhead", type = Bulkhead.Type.THREADPOOL) | ||
| @Async("s3UploadExecutor") | ||
| public CompletableFuture<S3UploadResult> upload(Long storeId, MultipartFile file) { | ||
| try (InputStream inputStream = file.getInputStream()) { | ||
| String key = createFileKey(storeId, file.getOriginalFilename()); | ||
| ObjectMetadata metadata = new ObjectMetadata(); | ||
| metadata.setContentLength(file.getSize()); | ||
|
|
||
| amazonS3Client.putObject(bucket, key, inputStream, metadata); | ||
| String url = amazonS3Client.getUrl(bucket, key).toString(); | ||
|
|
||
| return CompletableFuture.completedFuture(new S3UploadResult(key, url)); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("S3 업로드 실패", e); | ||
| } | ||
| } | ||
|
Comment on lines
+29
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 비동기 업로드 메서드의 에러 처리와 리소스 관리를 개선해 주세요. Bulkhead 패턴과 비동기 처리 적용은 좋습니다! 하지만 몇 가지 개선이 필요해 보입니다:
다음과 같이 개선해 주세요: @Bulkhead(name = "s3UploadBulkhead", type = Bulkhead.Type.THREADPOOL)
@Async("s3UploadExecutor")
public CompletableFuture<S3UploadResult> upload(Long storeId, MultipartFile file) {
try (InputStream inputStream = file.getInputStream()) {
String key = createFileKey(storeId, file.getOriginalFilename());
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
amazonS3Client.putObject(bucket, key, inputStream, metadata);
String url = amazonS3Client.getUrl(bucket, key).toString();
return CompletableFuture.completedFuture(new S3UploadResult(key, url));
} catch (Exception e) {
- throw new RuntimeException("S3 업로드 실패", e);
+ return CompletableFuture.failedFuture(new S3UploadException("S3 업로드 실패: " + file.getOriginalFilename(), e));
}
}🤖 Prompt for AI Agents |
||
|
|
||
| public void delete(String filename) { | ||
| try { | ||
| amazonS3Client.deleteObject(bucket, filename); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("S3 파일 삭제 실패", e); | ||
| } | ||
| } | ||
Jjiggu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private String createFileKey(Long storeId, String filename) { | ||
| return "store/" + storeId + "/" + UUID.randomUUID() + "-" + filename; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,9 @@ | |
| import com.example.gtable.store.dto.StoreUpdateRequest; | ||
| import com.example.gtable.store.model.Store; | ||
| import com.example.gtable.store.repository.StoreRepository; | ||
| import com.example.gtable.storeImage.dto.StoreImageUploadResponse; | ||
| import com.example.gtable.storeImage.model.StoreImage; | ||
| import com.example.gtable.storeImage.repository.StoreImageRepository; | ||
|
|
||
| import jakarta.persistence.EntityNotFoundException; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
@@ -21,6 +24,7 @@ | |
| public class StoreServiceImpl implements StoreService { | ||
|
|
||
| private final StoreRepository storeRepository; | ||
| private final StoreImageRepository storeImageRepository; | ||
|
|
||
| @Override | ||
| @Transactional | ||
|
|
@@ -38,15 +42,18 @@ public StoreReadResponse getAllStores() { | |
| List<Store> stores = storeRepository.findAllByDeletedFalse(); | ||
|
|
||
| List<StoreReadDto> storeRead = stores.stream() | ||
| .map(StoreReadDto::fromEntity) | ||
| .map(store -> { | ||
| List<StoreImage> images = storeImageRepository.findByStore(store); | ||
| List<StoreImageUploadResponse> imageDto = images.stream() | ||
| .map(StoreImageUploadResponse::fromEntity) | ||
| .toList(); | ||
| return StoreReadDto.fromEntity(store, imageDto); | ||
| }) | ||
|
Comment on lines
+45
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 이미지 데이터 통합 로직의 성능 최적화를 고려해보세요. 각 매장마다 개별적으로 이미지를 조회하는 방식은 N+1 쿼리 문제를 야기할 수 있습니다. 매장 수가 많아질 경우 성능에 영향을 줄 수 있습니다. 다음과 같은 개선을 고려해보시기 바랍니다: // 배치 조회를 위한 개선된 방법
List<Long> storeIds = stores.stream()
.map(Store::getStoreId)
.toList();
Map<Long, List<StoreImage>> imagesByStoreId = storeImageRepository
.findByStoreIdIn(storeIds)
.stream()
.collect(Collectors.groupingBy(image -> image.getStore().getStoreId()));
List<StoreReadDto> storeRead = stores.stream()
.map(store -> {
List<StoreImage> images = imagesByStoreId.getOrDefault(store.getStoreId(), List.of());
List<StoreImageUploadResponse> imageDto = images.stream()
.map(StoreImageUploadResponse::fromEntity)
.toList();
return StoreReadDto.fromEntity(store, imageDto);
})
.toList();🤖 Prompt for AI Agents |
||
| .toList(); | ||
|
|
||
| boolean hasNext = false; | ||
|
|
||
| return StoreReadResponse.fromEntity( | ||
| storeRead, | ||
| hasNext | ||
| ); | ||
| return StoreReadResponse.of(storeRead, hasNext); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -55,7 +62,12 @@ public StoreReadDto getStoreByStoreId(Long storeId) { | |
| Store store = storeRepository.findByStoreIdAndDeletedFalse(storeId) | ||
| .orElseThrow(() -> new EntityNotFoundException(storeId + " store not found.")); | ||
|
|
||
| return StoreReadDto.fromEntity(store); | ||
| List<StoreImage> images = storeImageRepository.findByStore(store); | ||
| List<StoreImageUploadResponse> imageDto = images.stream() | ||
| .map(StoreImageUploadResponse::fromEntity) | ||
| .toList(); | ||
|
|
||
| return StoreReadDto.fromEntity(store, imageDto); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -64,20 +76,20 @@ public StoreReadDto updateStore(Long storeId, StoreUpdateRequest request) { | |
| Store store = storeRepository.findByStoreIdAndDeletedFalse(storeId) | ||
| .orElseThrow(() -> new EntityNotFoundException(storeId + " store not found.")); | ||
|
|
||
| if (request.getName() != null) | ||
| store.setName(request.getName()); | ||
| if (request.getLocation() != null) | ||
| store.setLocation(request.getLocation()); | ||
| if (request.getDescription() != null) | ||
| store.setDescription(request.getDescription()); | ||
| if (request.getStoreImageUrl() != null) | ||
| store.setStoreImageUrl(request.getStoreImageUrl()); | ||
| if (request.getIsActive() != null) | ||
| store.setIsActive(request.getIsActive()); | ||
| store.updateInfo( | ||
| request.getName(), | ||
| request.getLocation(), | ||
| request.getDescription() | ||
| ); | ||
|
|
||
| Store updatedStore = storeRepository.save(store); | ||
|
|
||
| return StoreReadDto.fromEntity(updatedStore); | ||
| List<StoreImage> images = storeImageRepository.findByStore(updatedStore); | ||
| List<StoreImageUploadResponse> imageDto = images.stream() | ||
| .map(StoreImageUploadResponse::fromEntity) | ||
| .toList(); | ||
|
|
||
| return StoreReadDto.fromEntity(updatedStore, imageDto); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -86,7 +98,7 @@ public String deleteStore(Long storeId) { | |
| Store store = storeRepository.findByStoreIdAndDeletedFalse(storeId) | ||
| .orElseThrow(() -> new EntityNotFoundException(storeId + " store not found.")); | ||
|
|
||
| store.setDeleted(true); | ||
| store.markAsDeleted(); | ||
| storeRepository.save(store); | ||
|
|
||
| return "Store ID " + storeId + " 삭제되었습니다."; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.